Mobile notes capture
Status summary#
Convergence backend implemented and verified; awaiting review (PR #2483).
- Done: notes are markdown files with frontmatter in the dedicated notes repo; analysis writes title/tags into the file; settlement-debounced git commits; file-derived phone list with events as the live signal; "Open in docs" row action. All lanes green: 9 harness scenarios, lib tests, live e2e (frontmatter + git commit + glob discovery), Playwright spec.
- Known platform quirk found on the way: repos
create({type:"empty"})seeds the full config template (separate task chip filed); notes filter by stamp-shaped filenames.
Convergence decisions (grill session 2, supersede the backend half of the v1 decisions)#
- Files are truth: notes live at
/repos/notes/<utc-stamp>-<entropy>.md, written through the/workspaces/notesworkspace (fast overlay writes; lazy idempotent provisioning by first capture). - Facts ride the workspace's own stream (
/workspaces/notes), not a bespoke/notesaddress: afterwriteFile, the writer appendsnotes/captured {path}etc. Facts are notification/index only — nothing lives only in the stream. - File shape: frontmatter
capturedAt+ analysis-writtentitle/tags(plain YAML anyone edits) +attachments; identity = file path (noteKey dies); title absent → first-line fallback. - Analysis settles into frontmatter via workspace writeFile (re-read body guard →
superseded), plus anotes/analysis-settledevent; concurrency = frontmatter-only writes + last-writer-wins, flagged for a future git/collab upgrade. - Commits settlement-driven, ~10s debounce,
git.commit({scope: '/repos/notes'}), message from title/first line; recovery viagit.status()at head. - No
itx.notes.*— agents useglob/readFileslike any documents; NotesApp keeps only obligations + the commit lane.
- Mechanical: edit = recomposed writeFile +
notes/updated; delete = deleteFile +notes/deleted(git deletion via commit lane); pending queue unchanged; "Open in docs" row action; old vocabulary deleted outright (nothing merged).
Why#
Opening the mobile app usually means "I want to capture something." Today the capture surfaces are agent chat (wakes an LLM) and media (photos only). This adds frictionless note capture: a composer that's there the moment the app opens, persisting to a /notes stream per project, with media-style analysis, browseable on a notes screen, reachable by agents via itx.notes.*.
Decisions (from grill session)#
- Storage: dedicated
/notesstream via new userland starter apppackages/iterate/src/starter-apps/notes/, mirroring media's registration. Phone appendsnotes/capturedfacts. Git-commit-as-settlement is a later side-effect, not this round. - Surface: global capture composer overlay mounted in mobile
_layout.tsx+ anotes.tsxbrowse screen behind a standard drawer/notesentry. - Composer lifecycle: auto-appears (docked bottom bar, not auto-focused) on cold start and on foreground after >5 min backgrounded (tunable constant). ✕ collapses to a floating 📝 pill; tap re-expands. Hidden on screens with their own composer (chat).
- Target project: route-derived inside
project/[projectId]/*; anywhere else captures go to a local pending notes store. Drain prompt ("store pending notes here?") fires at project selection and on app open with a project selected. Yes → append; No → follow-up "delete or keep pending?"; keep → asked again next drain moment. - Offline/failure: failed appends land in the same pending store with a quiet "saved locally" toast. Capture never blocks or loses data.
- Content: text + photos.
+attach like the chat composer; images viafiles.put, referenced fromnotes/captured. Pending store persists attachment blobs locally until drained. - Photos double-append to
/mediawithsource: "note"provenance → MediaProcessor analyzes them for free; gallery stays filterable. Wipe-orphaning accepted as POC risk. - Text analysis in POC, always-on: obligation pattern —
notes/captured→ small-model call →notes/processed {title, tags}. First line of text is title fallback while pending/failed. No preference toggle (userland wiring means an agent can rewrite behavior later). - Agents:
itx.notes.list/searchcapability from day one, likeitx.media.*. - Notes screen: newest-first list (derived title, snippet, relative time, photo thumbnails; tap to expand; media-viewer for images), client-side text filter, long-press delete via
notes/deletedtombstone. No edit. - Packaging: this one draft PR off main, analysis included ("make it real").
Checklist#
Server: packages/iterate/src/starter-apps/notes/#
-
ref.ts—notesStreamPath = "/notes",notesWorkerRef(classNameNotesApp, durableWorkerKeyapp-notes-stream) done — ref.ts, dependency-free literal like media's -
processor.ts— contract slugnotes; eventsnotes/captured,notes/deleted,notes/processed,notes/reanalyze-requested; fold + analysis obligations (caughtUp-guarded, retries, settles exactly once) done — settlement event namednotes/analysis-settledper stream-processor doctrine (notnotes/processedas first drafted) -
analysis.ts— one small-model call →{title, tags}done — llama-4-scout text call, defensive JSON parse -
worker.ts—NotesApp extends StreamProcessorDurableObject,recovery = true, RPClist/search/getdone — recovery=true; search returns signed attachment URLs -
configured-worker.ts,app-ref.ts,index.ts(NotesApp.createfan-in +provideCapability({path: ["notes"]})) done — fan-in + provideCapability at path ["notes"], media shape - package.json exports (src + dist) + tsdown entries done
-
configs/default/worker.tswiring (#notesApp+processEvent) done — plus regenerated config-repo-template.generated.ts via pnpm lint:fix - media
uploadedevent: optionalsourcefield done differently — merged media usesmedia/capturedwith an existingsourcestring; extended the phone union with "note" and the schema doc, no server schema change needed
Mobile: apps/mobile/#
-
lib/notes.ts— event constants,buildNoteEvent/buildDeleteEvent,deriveNotesList(tombstones, processed titles, first-line fallback),filterNotesdone — pure fold mirrors server processor; displayTitle falls back to first line -
lib/pending-notes.ts— pending store (text + attachment blobs), drain logic, prompt-state helpers; pure core with injected IO done — AsyncStorage-shaped seam (AsyncStorage was already a dep; no expo-file-system needed), drain removes each note as it lands -
components/note-composer.tsx— global overlay + pill (state in query cache), route-derived target viauseSegments(), slug label, hidden on chat,+attach →files.put+media/uploaded {source:"note"}done — module-level AppState listener + query-cache state (no useEffect/useState beyond drafts); drain prompt runs inside a queryFn keyed on projectId+foreground generation -
app/project/[projectId]/notes.tsx—useLiveEventson/notes, filter box, long-press delete, media-viewer done — search box, long-press + expanded delete, Re-analyze, media-viewer for photos -
components/project-drawer.tsx—/notesentry + pathname union done - drain prompts on app open / project selection (native Alert fine for POC) done — native Alert two-step (store? → delete/keep) per D4
Tests#
- processor node harness: captured → obligation → processed; deleted tombstone; reanalyze done — 8 scenarios incl. eviction recovery, expiry-without-dial, full-stream replay
-
lib/notes.test.ts,lib/pending-notes.test.tsdone — 9 tests - optional
e2e/notes.e2e.test.tsdone — live proof: capture → real model-call settlement → worker RPC + itx.notes doors → tombstone
Ship#
-
pnpm typecheck && pnpm lint && pnpm knip && pnpm format && pnpm testall green - draft PR with screenshots/video, session id in body, comment monitors PR #2483; video-mode demo inline; CI + comment monitors armed
Implementation log#
- Server first commit: full obligation-pattern processor (docs/writing-stream-processors.md checklist), cribbed from github-ai-linter's publication obligation rather than media (merged media does analysis inline in a phone-issued runScript; notes wants instant dumb capture, so analysis is a server-side obligation instead).
- Attachments reuse the media pipeline wholesale: same content-hash file paths (mediaFilePath), and the phone fires media's buildProcessScript with source "note" fire-and-forget after the note append (D7 double-append).
- Composer state (open/pill, drain generation) lives in the query cache, flipped by a module-level AppState listener — no useEffect. Drain prompt is an Alert inside a queryFn keyed [projectId, foregroundGeneration]: re-prompts on project switch or foreground return, not on every pending change.
- notes/reanalyze-requested is consumed by the processor but deliberately not in the phone's NOTE_EVENT_TYPES read set (it changes no list state).
Follow-ups (post-review feedback)#
- "Chat" note action — from an expanded note, jump to the chat view with the note referenced (path prefilled in the input, or pre-added as a feed item the agent sees on next message). Needs design: how a note is addressed in agent context (path? quoted text? keyed context item?), and whether it targets a new or existing thread.
- Agent tag-writeback door — "when a note is created, classify it and tag it" almost works today (an agent can hook
notes/capturedin the config worker), but there's no clean write door for the tag:notes/analysis-settledis guarded by the open-obligation fold, so a foreign settlement no-ops. Options: anotes/taggedfact anyone may append (fold unions tags), or move the analysis prompt/taxonomy into config-repo data so the agent edits that.
Both intentionally left out of PR #2483 to keep it scoped; captured 2026-08-12 from review feedback.
- Starter-app rollout to existing projects — new starter apps only reach new projects (the template seeds at creation). Verified on nustom/preview-8:
itx.notesabsent, no analysis, because its config repo predates the PR. Manual rollout took two config-repo commits: (1) the 3-line NotesApp wiring inworker.ts, and (2) re-pinning theiteratedep inpackage.json— the first rebuild failed with "Failed to resolve 'iterate/starter-apps/notes'" because the repo pins an old commit sha and the deployment did NOT ref-pin the build. Worth a doc note or a platform story ("adopt latest template diff" as an agent task or one-click). - Notes will grow toward docs features (Misha, post-review): formatting, organization, richer search/federation, promote-to-doc are all wanted eventually. That makes the notes↔docs boundary a real design question, not a hypothetical — decide whether notes stays a separate stream that promotes into workspace documents (one-way valve, D1's deferred git-settlement as the mechanism), or whether notes becomes the capture door into the workspace-documents system itself (a note IS a nascent doc). Worth its own grill session before building any of those features.