summaryrefslogtreecommitdiffhomepage
path: root/notes/text-file-embeds-implementation.md
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-10-24 18:48:54 +0900
committerAdam Malczewski <[email protected]>2026-10-24 18:48:54 +0900
commit34526ff48985643bed532b102e8943fa69e77a79 (patch)
tree4208ec0f5f1ef2cf1a9cd9710a442ce10a5f0c61 /notes/text-file-embeds-implementation.md
parentdaefad66bdf8dee132d701a3b43ce0d227dde241 (diff)
parentb666d77df8cf4da6ae40fb5637268926f02cc44d (diff)
downloaddispatch-web-34526ff48985643bed532b102e8943fa69e77a79.tar.gz
dispatch-web-34526ff48985643bed532b102e8943fa69e77a79.zip
Merge branch 'feature/text-file-embeds' into dev
Diffstat (limited to 'notes/text-file-embeds-implementation.md')
-rw-r--r--notes/text-file-embeds-implementation.md294
1 files changed, 294 insertions, 0 deletions
diff --git a/notes/text-file-embeds-implementation.md b/notes/text-file-embeds-implementation.md
new file mode 100644
index 0000000..2f23a41
--- /dev/null
+++ b/notes/text-file-embeds-implementation.md
@@ -0,0 +1,294 @@
+# Text-file embeds — implementation report
+
+## Delivered behavior and syntax
+
+Save the document completely, then emit a short summary and a **standalone paragraph**:
+
+```markdown
+I saved the implementation plan; the key change is the new validation boundary.
+
+[Implementation plan](text-file:notes/plan.md)
+```
+
+Absolute path and filename encoding examples:
+
+```markdown
+[Research](text-file:/tmp/research.md)
+
+[Research with spaces](<text-file:notes/research%20report.md>)
+
+[Literal percent](text-file:notes/100%25.md)
+```
+
+The prefix is exactly `text-file:` (not image syntax). Destinations are POSIX
+absolute paths or paths relative to the conversation's authoritative effective
+working directory. Angle-bracket destinations can also contain literal spaces.
+Markdown escapes are interpreted by marked, then percent escapes are decoded
+**once**. `%2520` denotes a literal `%20` filename; `%23` and `%3F` denote filename
+`#` and `?`. The HTTP adapter uses URLSearchParams to transport that literal
+filesystem path; the backend does not perform another destination decode.
+
+Only a complete, single-line, top-level inline-link paragraph opts in. Ordinary
+links, images, inline/fenced/indented code, reference-style links, titled links,
+list/blockquote links, and links surrounded by prose retain ordinary Markdown
+behavior and never trigger text-file reads. While streaming, an embed waits for
+a blank-line paragraph boundary or completion of the message, avoiding speculative
+fetches for incomplete links. Examples inside code never fetch. Unsupported or
+malformed destinations remain inert ordinary Markdown, not browser/server URL
+fetches. Raw HTML cannot forge a file-fetching block.
+
+## Presentation
+
+The document appears inside the existing **inset-well**, matching the recessed
+panel style of tool-call/thinking blocks. It includes its label/path and
+Refresh/Retry controls. Markdown extensions `.md`, `.markdown`, `.mdown`, `.mkd`,
+`.mkdn` render through the existing marked/highlight.js/DOMPurify pipeline.
+Other valid text/code files render safely as literal whitespace-preserving code,
+without guessing a language or interpreting their HTML/Markdown.
+
+The default preview renders approximately the **first six and last six source
+lines**, with an omitted-source-line count. Files of twelve lines or fewer are
+not duplicated. Expand document shows the full rendered document; Collapse
+document returns to the preview. The control exposes `aria-expanded`.
+Top-level code-fence context is restored around preview fragments. Previews are
+source-line excerpts, not a six-visual-line layout clamp: wrapping, headings,
+tables and multiline constructs can change their visual height/formatting. Some
+constructs crossing excerpt boundaries (lists, tables, reference definitions,
+raw HTML) are necessarily approximate; expansion renders the authoritative full
+Markdown.
+
+Both preview and full content are sanitized. Existing raw-HTML/link policy and
+remote-image click consent are preserved. Omitted images are not mounted until
+expansion. Relative local images resolve against the returned document's directory
+through the existing local-image route. Embedded documents receive no text-file
+client, so nested embed references are inert and cannot recursively fetch.
+
+Loading, empty-file, transport/validation failure and retry states are visible;
+failures do not break surrounding chat. File text is display data, never a
+conversation/history mutation.
+
+## File lifecycle and resource policy
+
+- **Live view, not snapshot.** Only the short reference remains in stored chat.
+ No document body or hash/snapshot is persisted with the message.
+- A mounted chat fetches current disk bytes on first reference. Repeated exact
+ decoded paths share an in-flight/result entry while subscribed. There is no
+ file watcher or polling. Already displayed content does not update itself.
+- Refresh (or Retry after failure) invalidates **all live files in that mounted
+ chat**, refetches authoritative context, and rereads their current contents.
+ This prevents mixing old and refreshed routing contexts. Expansion alone does
+ not fetch. File deletion produces a not-found state on the next read; recreation
+ can recover through Retry.
+- When the last subscriber leaves, its entry is aborted/released. Returning to
+ chat/reloading/remounting reads current disk state, which can differ from the
+ original message. No historical immutability is implied.
+- Known cwd/computer setting changes and conversation/workspace switches replace
+ the context-bound client, abort old requests and discard its cache. Late
+ responses cannot publish into a new client/path. External context edits not
+ signaled to the UI are detected by backend guards on subsequent reads or by
+ Refresh; existing displayed text is not watched for external changes.
+- Limit **256 KiB (262144 bytes)** per file, strict UTF-8, allowing tab/LF/CR but
+ rejecting NUL and other C0/C1 binary controls. Exactly the limit is accepted;
+ oversize files are rejected, never silently truncated.
+- At most **8 embeds per message**, **32 active distinct paths per mounted chat**
+ (at most 8 MiB source text), and **4 concurrent file loads**. The per-message
+ limit is explained inline; active-cache overflow has a visible error.
+- Backend opens a single nonblocking/no-follow descriptor, checks regular-file
+ status and reads at most limit+1 bytes, including a final growth check. FIFOs,
+ devices, directories and final-component symlinks are rejected. Ancestor
+ directory symlinks and ordinary `..`/absolute paths are allowed: this is **not a
+ cwd sandbox**. Reads have a byte/work bound but no special filesystem timeout
+ or guarantee of a coherent snapshot while another process modifies a file.
+
+Unsupported: file URLs, HTTP/network destinations, tilde expansion, Windows paths,
+backslashes, control characters and remote-computer embeds. Remote targets are
+explicitly rejected, never read from an identically named local path.
+
+## Backend seam and trust boundary
+
+Existing `transport-http` is extended; no new extension/tier or kernel feature.
+Additive `transport-contract` interfaces are `TextFileQuery`, `TextFileResponse`,
+`TextFileContextResponse`; package version remains 0.26.0 with local `file:` deps,
+and the frontend's tracked public contract mirror is updated.
+
+```text
+GET /conversations/:id/text-file-context
+ -> { conversationId, cwd: string|null, computerId: string|null }
+
+GET /conversations/:id/text-file?path=...&expectedCwd=...&expectedComputerId=...
+ -> { path, text, format: "markdown"|"text", bytes, maxBytes }
+```
+
+Context preflight performs no LSP or file work. Both routes resolve persisted
+conversation settings and workspace defaults server-side, not browser state or
+server process cwd. Relative conversation cwd is resolved against an absolute
+workspace default; an unresolvable configured relative cwd returns 422. With no
+cwd configured, context is null, absolute local file references work, and relative
+file references return 400. The frontend always supplies both preflight-derived
+guards. Empty guard values assert unset cwd/local computer; absent guards skip
+comparison. Guards never override server routing. The route rechecks context
+after I/O before returning successful text.
+
+All endpoint responses use `Cache-Control: no-store`, including middleware errors.
+Relevant statuses: 400 invalid/non-text/nonregular, 403 unreadable or rejected
+Origin, 404 unknown conversation/missing file, 409 stale context, 413 oversized,
+422 unsupported remote/unresolvable configured context, 500 read failure. Existing
+Host rejection remains 421.
+
+**Authentication limitation, explicitly audited:** this application currently uses
+Host/Origin checks plus loopback/Tailscale deployment restrictions, not bearer or
+per-user session authentication. The new routes retain exactly that existing trust
+boundary; they do not invent an authentication guarantee. A trusted client with
+access to this API can read supported local files accessible to the backend UID,
+including absolute paths outside cwd. Do not expose this transport publicly or to
+untrusted users. No arbitrary HTTP fetch is added.
+
+## Initial AI context, custom prompts and persistence
+
+Mandatory application guidance is appended **after template variable resolution**
+in `SystemPromptService.construct`. The unchanged default template and exact
+user-authored custom-template bytes are preserved. An empty custom template now
+constructs guidance-only context. Guidance includes actual syntax/examples,
+save-before-reference, short summaries, supported paths/limits, local-only/live
+semantics, preview/expand behavior and the distinction between user display and
+reading a file for the model's own understanding.
+
+New conversations and normal prompt reconstruction (compaction, missing resolved
+prompt, cwd/computer metadata invalidation) receive it, including installations
+with existing custom templates. Resolved prompts remain persisted per conversation.
+Existing matching resolved prompts, including legacy empty prompts, are returned
+**byte-for-byte unchanged** until ordinary reconstruction; changing the global
+template alone does not retrofit existing conversations. There is no migration or
+read-time mutation. The pure suffix-idempotent `appendTextFileGuidance` helper is
+also used for missing/stale read-only cache-warming assembly, yielding the same
+provider system prompt as the next real turn without warm persistence writes.
+Generic `resolveText` remains generic and does not append application guidance.
+
+The session-orchestrator tests capture actual provider requests using real prompt
+service, conversation store, assembly and kernel turn loop: default/custom first
+turns, legacy continued/warm prompts, missing/cwd/computer invalidation, warm/real
+parity, compaction and later reuse. History retains the reference, not the file
+body. This is verified delivery, not merely a default-string assertion.
+
+Explicit caller-owned per-turn `systemPrompt` overrides (e.g. heartbeat) retain
+their existing **AS-IS bypass** contract; they do not receive appended guidance.
+The initial instructions in already-running/persisted conversations are not
+retroactively changed, and this task did not redeploy/restart the shared service.
+
+## Initial verification (superseded by Bun-native follow-up below)
+
+`bun install` completed inside both feature worktrees before build/testing; frontend
+was refreshed after backend declarations were built. All work is on
+`feature/text-file-embeds`, with no merge or push. After the user's stop request,
+all delegated generations were stopped and the lead completed fixes, tests,
+verification, documentation and commits directly; no further subagents.
+
+### Frontend
+
+- `bun run typecheck`: **0 errors / 0 warnings**.
+- `bun run check`: clean, 308 files (no warnings/infos).
+- `NODE_OPTIONS=--no-experimental-webstorage bun run test`: **2586 passed,
+ 2 opt-in live tests skipped**, twice, 130 passed files / one skipped file.
+- Markdown-specific tests: **236 passed** (parser, streaming, sanitized UI,
+ image consent, head/tail/expand, code/plain text, request effects and limits).
+- `bun run build`: succeeds; existing Svelte initial-value capture diagnostics
+ and large-chunk advisory remain.
+- `TEXT_FILE_LIVE_PROBE=1 bunx vitest run scripts/text-file-live.test.ts`:
+ **2 passed** against a freshly spawned ephemeral Bun HTTP app using a real
+ conversation store and temp disk files. The actual Svelte component and client
+ render a saved plan from only summary+reference, expand the omitted middle,
+ refresh updated content, remount/reload current bytes, show deletion, recover on
+ Retry and reject authoritative remote context. The fixture process/files are
+ cleaned up. No deployed service is used or restarted.
+- Plain `bun run test` on host **Node v26.11.1** reproduces 26 pre-existing
+ localStorage-related failures in untouched `tile-layout-store` and
+ `WorkspacesHome` suites. A temporary `git archive HEAD` baseline with the same
+ dependencies independently reproduced exactly those 26 failures. Disabling
+ Node's experimental Web Storage lets jsdom supply browser storage and makes
+ the whole suite pass; no unrelated application/test code was modified for this.
+
+### Backend
+
+- `bun run typecheck`: succeeds.
+- `bun run test`: **4277 passed / 10 skipped**, 221 passed files / one skipped.
+- `bun run test:bun`: **190 passed / 0 failed**, including two real ephemeral
+ loopback text-file HTTP tests in the standard Bun test target.
+- `bun run check`: clean, 754 files (no warnings/infos).
+- `bun build packages/host-bin/src/main.ts --compile --outfile dist/dispatch-server --minify`
+ and equivalent CLI compile: both succeed, worktree-local ignored outputs only.
+- Standard `bin/build --no-frontend` cannot complete here: its existing sibling
+ `claude` symlink points at the separate main Claude checkout, where bundling
+ fails resolving `@dispatch/kernel`. The wrapper may have refreshed ignored compiler outputs there before failing;
+ no dependency install or tracked edits were made there; the standalone feature backend/CLI compile above is the verified
+ build. External Claude bundling is **not** claimed verified.
+- Backend tests cover pure path/query/encoding policy, effective context,
+ strict UTF-8/binary/empty/exact-limit/oversize files, directories/devices/FIFO/
+ symlink refusal, unknown conversation, stale guards, remote inheritance with
+ an existing local path, live changes/deletion and Host/Origin rejection.
+- `git diff --check` clean in both repositories. No probe server/collector leaks.
+
+### Remaining manual validation
+
+Automated UI tests use jsdom, not a graphical browser. After deploying both
+commits, visually confirm block styling/responsiveness on desktop/mobile, expand
+and collapse a longer Markdown plan, and click a remote-image consent button.
+No screenshots, graphical-browser layout check, AI-provider-generated live reply,
+remote file support, watchers, snapshots or editor/attachment redesign are claimed.
+
+## Bun-native verification follow-up — 2026-10-24
+
+This is the **authoritative final verification**, superseding the initial
+Node-backed Vitest results above. `bun run` previously respected Vitest's
+`#!/usr/bin/env node` launcher. Both repositories now explicitly execute
+`bun --bun node_modules/vitest/vitest.mjs` in `test` and `test:watch`, so ordinary
+`bun run test` uses Bun itself, including its workers. No `NODE_OPTIONS` is needed;
+all following runs explicitly unset it. Runtime: **Bun 1.4.3**.
+
+Bun-native runs exposed existing test assumptions which have been corrected, not
+skipped and not hidden with larger timeouts:
+
+- HTTP/WS listener tests now spy on **Bun.serve only** and restore it afterward,
+ instead of redefining the non-configurable global `Bun` object. The fallback
+ global stub remains only for non-Bun supplemental runners.
+- The image migration test sorts a directory listing before comparing its
+ contents; native filesystem enumeration order is not a contract.
+- The local-image home-expansion test uses a path relative to the actual OS home
+ into its temporary fixture; it no longer assumes mutating HOME changes Bun's
+ cached homedir result. No real home-directory file is created.
+- The frontend cyclic tool-input fallback was genuinely slow under Bun's pretty
+ JSON serializer. A local ancestor-stack replacer detects cycles before native
+ recursion, preserving the existing fallback text. Added a regression case for
+ nested cycles and **shared non-cyclic references** so valid JSON isn't rejected.
+ The original timeout-prone test now completes promptly; no timeout was raised.
+- The opt-in live UI/network test no longer depends on Node-only
+ `transferableAbortController`. Its scoped jsdom environment retains the
+ runtime's matching native AbortController/AbortSignal pair, so actual Bun fetch
+ and cancellation are exercised. The production adapter remains unchanged.
+
+Final commands/results:
+
+| Worktree | Command | Result |
+| --- | --- | --- |
+| frontend | `bun run test` | **2587 passed / 2 opt-in skipped**, repeated green (130 passed files / one skipped) |
+| frontend | `bun run test:text-file-live` | **2 passed**, real Svelte + ephemeral Bun HTTP + temp disk files |
+| frontend | `bun --bun run typecheck` | **0 errors / 0 warnings** |
+| frontend | `bun --bun run check` | **309 files clean**, no warnings/infos |
+| frontend | `bun --bun run build` | **passed**, existing initial-value/large-chunk advisories remain |
+| backend | `bun run test` | **4278 passed / 9 skipped**, twice (221 passed files / one skipped) |
+| backend | `bun run test:bun` | **190 passed / 0 failed** |
+| backend | `bun --bun run typecheck` | **passed** |
+| backend | `bun --bun run check` | **754 files clean**, no warnings/infos |
+| backend | `bun build ... --compile` (server and CLI) | **both passed**, ignored worktree-local binaries |
+
+The nine backend skips are the existing SSH integration suite; an additional
+Bun-only test previously skipped under Node now runs, hence 4278 rather than
+4277 passing. The frontend count increases by one for the new shared-reference
+regression test. The two frontend live tests are intentionally opt-in but were
+run separately and passed. No tests were excluded to obtain green results.
+
+Dependencies were refreshed using `bun install` inside both feature worktrees.
+The external-Claude standard-build wrapper limitation described above remains;
+no external/main checkout edits, installs, service restarts, merge or push were
+performed in this verification follow-up. No subagents were used. Native probe
+processes and temporary fixture files were cleaned up. Browser visual validation
+remains manual as documented above.