M8: Long-conversation robustness and everyday chat features #9

Merged
kubus merged 15 commits from t3code/chat-improvements into main 2026-09-29 21:34:23 +00:00
Owner

Summary

  • Context-window management: trimming strategy splits long conversations into pages; old tool outputs and then oldest exchanges are replaced/dropped to fit the model's window.
  • Provider resilience: exponential backoff retries on rate limits (429), timeouts (408), and upstream failures (5xx); retries stop once text has streamed to avoid duplication.
  • Continue for cut-off replies: models that hit output limits get a Continue button to pick up where they left off; replies display as a single continuous message.
  • Per-conversation tuning: instructions appended to system prompt, temperature (0–2), and max reply tokens cap — travel with the conversation on fork/export.
  • Document attachments: text, code, and PDFs upload and extract as text; shown as file cards in the UI; scanned PDFs without extractable text are rejected with a reason.
  • Fork, export, import: fork any message to a new conversation holding its path; export active branch as Markdown or full tree as JSON; import recreates a conversation with fresh ids.
  • Message search: full-text index over user and assistant text; sidebar search from two characters; hits switch branch and scroll.
  • Session status: conversations show as idle, running, or waiting (blocked on approval/ask_user); absence of timeout means the turn waits until you answer or stop it.
  • Opt-in notifications: browser can notify you when a reply is ready or a turn is waiting while the tab is in the background.
  • Sandboxed external plugins: each plugin runs in its own Node child process with scrubbed env (Node ≥22.18 adds filesystem sandbox); set HAT_PLUGINS_ISOLATION=off to load in-process as before.
  • MCP readOnlyHint: tools marked readOnlyHint: true without destructiveHint run without approval; set trustReadOnlyHint: false per server to ignore hints.
  • Auto sandbox tier: HAT_EXEC_SANDBOX=auto probes for a runtime at startup and uses container tier if found, else host; container makes it mandatory; see README for uid:gid and env handling inside containers.
  • CI workflow: typecheck, unit tests, web build, and end-to-end smoke test on every push to main and pull request.
  • Mobile: new ConversationScreen for instructions/temperature/maxTokens; document picker for text/code/PDF; export via Share sheet; session status indicator.

Testing

  • Unit tests added for context fitting (turn-options.test.ts), plugin isolation protocol (isolation.test.ts), search indexing (search.test.ts), and transfer/documents.
  • Smoke test boots real server and runner, exercises turns with a fake provider, approvals, branching, and documents.
  • CI runs all three (typecheck, test, smoke) plus web build on every push to main and PR.
  • Manual verification needed: document uploads and server text extraction; fork/search/export in web UI; mobile ConversationScreen and document picker; MCP server readOnlyHint behavior (if used).
## Summary - **Context-window management:** trimming strategy splits long conversations into pages; old tool outputs and then oldest exchanges are replaced/dropped to fit the model's window. - **Provider resilience:** exponential backoff retries on rate limits (429), timeouts (408), and upstream failures (5xx); retries stop once text has streamed to avoid duplication. - **Continue for cut-off replies:** models that hit output limits get a **Continue** button to pick up where they left off; replies display as a single continuous message. - **Per-conversation tuning:** instructions appended to system prompt, temperature (0–2), and max reply tokens cap — travel with the conversation on fork/export. - **Document attachments:** text, code, and PDFs upload and extract as text; shown as file cards in the UI; scanned PDFs without extractable text are rejected with a reason. - **Fork, export, import:** fork any message to a new conversation holding its path; export active branch as Markdown or full tree as JSON; import recreates a conversation with fresh ids. - **Message search:** full-text index over user and assistant text; sidebar search from two characters; hits switch branch and scroll. - **Session status:** conversations show as idle, running, or waiting (blocked on approval/ask_user); absence of timeout means the turn waits until you answer or stop it. - **Opt-in notifications:** browser can notify you when a reply is ready or a turn is waiting while the tab is in the background. - **Sandboxed external plugins:** each plugin runs in its own Node child process with scrubbed env (Node ≥22.18 adds filesystem sandbox); set `HAT_PLUGINS_ISOLATION=off` to load in-process as before. - **MCP readOnlyHint:** tools marked `readOnlyHint: true` without `destructiveHint` run without approval; set `trustReadOnlyHint: false` per server to ignore hints. - **Auto sandbox tier:** `HAT_EXEC_SANDBOX=auto` probes for a runtime at startup and uses container tier if found, else host; `container` makes it mandatory; see README for uid:gid and env handling inside containers. - **CI workflow:** typecheck, unit tests, web build, and end-to-end smoke test on every push to main and pull request. - **Mobile:** new ConversationScreen for instructions/temperature/maxTokens; document picker for text/code/PDF; export via Share sheet; session status indicator. ## Testing - Unit tests added for context fitting (`turn-options.test.ts`), plugin isolation protocol (`isolation.test.ts`), search indexing (`search.test.ts`), and transfer/documents. - Smoke test boots real server and runner, exercises turns with a fake provider, approvals, branching, and documents. - CI runs all three (typecheck, test, smoke) plus web build on every push to `main` and PR. - Manual verification needed: document uploads and server text extraction; fork/search/export in web UI; mobile ConversationScreen and document picker; MCP server readOnlyHint behavior (if used).
Tools a server annotates readOnlyHint: true (and not destructiveHint: true)
no longer require approval; everything else still does. Annotations from
tools/list are parsed with zod, dropping malformed hints so they count as
absent. A per-server "trustReadOnlyHint": false opts a server out, and the
web MCP editor now round-trips that field instead of discarding it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
HAT_EXEC_SANDBOX gains an `auto` value, now the default. At startup the
runner probes docker, then podman (or only HAT_SANDBOX_RUNTIME when set)
with a 5s timeout and uses the container tier if one answers, otherwise
falls back to host. The chosen tier and the reason are logged. An explicit
`container` with no usable runtime now makes the runner exit at startup
instead of failing every job, and unknown values are rejected.

The effective tier is advertised as `caps.sandbox`, so GET /api/runners
shows it. The compose runner is pinned to `host` since it has no runtime
and a mounted socket would bind host paths, not its volume.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Now that `auto` puts shell_exec in a container wherever docker or podman
answers, fix what broke there:

- Workspace writes: run the container as the runner's own uid:gid instead
  of a fixed 65532, which could not write the runner-owned workspace. The
  probe now also reports whether the engine is rootless: rootless podman
  gets --userns=keep-id, rootless docker runs as container root (which
  maps to the runner's user). HOME is /workspace, mirroring the host tier.
- Env: per-command env is now forwarded as bare `-e KEY` flags with the
  values in the runtime CLI's environment, so they stay out of argv. That
  CLI now inherits the runner's env (DOCKER_HOST, XDG_RUNTIME_DIR, ...)
  instead of HOME=<workspace>, and per-command keys that would steer it
  (DOCKER_*, CONTAINER_*, XDG_*, proxies, ...) are dropped. The loader
  blocklist also now actually matches LD_* / DYLD_* prefixes; it used to
  block only the literal names `LD_` and `DYLD_`.
- Processes: background processes and the Python tool get their own
  tier, HAT_SANDBOX_PROCESSES=host|container. Unset, it is container only
  under an explicit HAT_EXEC_SANDBOX=container (as before) and host
  otherwise, so `auto` does not move them into an image with no python3
  and no network.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Killing the runtime CLI (SIGKILL to its process group) does not stop the
container it started, so cancelled, timed-out or capped jobs kept running
with their cpu/memory allowance until their command finished.

Each container is now named hat-<sanitized job/proc id>-<random>, and the
spawn plan carries a matching `<runtime> rm -f <name>`. Jobs and shell-mode
processes run it, best-effort with a 5s timeout, whenever they are killed:
cancel, timeout, output cap, link loss and runner shutdown. Shutdown now
waits for those removals before exiting. `--rm` still cleans up containers
that exit on their own.

Also fix link loss not aborting jobs at all: failAllPending cleared the
jobs map before killManaged could walk it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Kernel fits each request to the model's context window (elides old tool
  output, then drops the oldest exchanges, in cache-friendly pages) and
  warns when it trims. Stored history is never touched.
- Retryable provider failures (429/5xx/408, dropped connections, in-stream
  overloads) are retried with backoff, honouring Retry-After, but only
  before any output has streamed.
- Finish reasons are persisted; POST /continue resumes a reply cut off at
  the output limit via a synthetic user message that clients hide.
- Per-session instructions, temperature and max tokens.
- Text, code and PDF uploads are stored with their extracted text and
  inlined for the model; chat keeps a file chip.
- Fork from a message, JSON export/import of the whole tree (attachments
  inline), Markdown export of the active branch.
- Approvals and ask_user wait for the user by default
  (HAT_APPROVAL_TIMEOUT_MINUTES), and the session list reports
  idle/running/waiting.
- The chat view-model moves from the mobile app into @hat/core so every
  client shares it.
- Smoke: approval check asks for approval explicitly (sessions default to
  auto), plus coverage for all of the above.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
External plugins (HAT_PLUGINS_DIR) used to be imported into the server
process with full access to its env, secrets and filesystem. Each one now
runs in its own forked child:

- Scrubbed environment (only NODE_ENV/TZ/LANG) and, on Node >= 22.18,
  Node's permission model: fs reads limited to the plugin dir and the code
  it imports, no writes, child processes, workers or addons. TypeScript
  workspace sources load via native type stripping plus an in-thread
  .js -> .ts resolve hook, since tsx/esbuild can't run in the sandbox.
- A symmetric RPC bridge over IPC: proxy tools/providers forward execute,
  listModels and streaming chat; aborts become cancel messages. Zod
  schemas are converted to JSON Schema in the child (shared
  toJsonSchema helper in @hat/kernel) and args/config validated there.
- ctx.secrets serves only names in requiresSecrets; ctx.host is forwarded
  per tool call, pinned to its session and gated on runner:* permissions.
- A crashed child marks the plugin `error` and unregisters its
  contributions via the new PluginContext.fail(); deactivation kills it.
- HAT_PLUGINS_ISOLATION=off keeps the old in-process loading.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- App uses the @hat/core chat view-model (live usage now accumulates on
  in-flight messages); message rendering moves to MessageView.tsx.
- Sidebar: full-text search over messages (opens the hit on its branch and
  highlights it), running / needs-you status dots, JSON import.
- Header: per-conversation settings (instructions, temperature, max reply
  tokens) and Markdown/JSON export.
- Fork from any message; Continue for replies cut off at the output limit;
  context-window gauge next to the token count.
- Attach text, code and PDF files as well as images; very long pastes
  become a text attachment.
- Opt-in desktop notifications when a reply is ready or a conversation
  needs the user while the tab is hidden.
- Scrolling follows late layout growth while pinned to the bottom, and the
  scroll listener survives leaving the chat view.
- Server: generated titles also work for turns with attachments and ignore
  the Continue nudge.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
# Conflicts:
#	packages/kernel/src/index.ts
#	packages/server/src/config.ts
- .github/workflows/ci.yml: typecheck, unit tests, web build and the smoke
  test on pushes to main and on pull requests.
- engines.node >=22.18: node:sqlite and the plugin sandbox's permission
  model both need it; Node 20 could never run the server.
- README: context-window fitting, retries, Continue, per-conversation
  settings, documents, fork/search/export/import, notifications, approval
  wait semantics; drop the stale "restart the server" and "(M6 work)" notes
  and fix the default tool policy (auto).
- .env.example: HAT_APPROVAL_TIMEOUT_MINUTES, HAT_PROVIDER_RETRIES,
  HAT_PLUGINS_ISOLATION.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Usage now rides on the in-flight messages (the shared view-model's `usage`
  effect), so the separate `liveUsage` tally is gone rather than counting
  every token twice. `message.done` stamps the finish reason and ends the
  message's streaming state; promoted messages drop `streaming` too.
- Context fill beside the model name in the chat title (orange from 80%),
  with the detail in the options menu's Usage section.
- Continue under a reply cut off at the output limit (POST /continue).
- Instructions & Sampling: per-conversation instructions, temperature and
  max reply tokens, as a sheet from the chat menu and pushed from Settings.
- Fork from Here on replies, switching to the new conversation.
- Export as Markdown to the share sheet.
- Chats search also queries /api/search (debounced) and lists message hits
  with the matched terms in bold; a hit selects its branch and opens it.
- Session rows show Responding / Needs you, refetched on focus and polled
  while anything is active.
- Documents via expo-document-picker (~57.0.3, app version 0.2.0), sent with
  attachmentNames and shown as file cards on user messages.
- docs/ios-app.md covers all of the above, and notes that background
  notifications need APNs and a paid Apple developer account.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- The synthetic Continue message is no longer indexed for search.
- The session list counts the rows a client shows (user and assistant,
  minus synthetic nudges) rather than every stored message.
- /select, /regenerate and /edit reject a messageId from another
  conversation instead of wiring it into this one's tree.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Add Claude Code agent worktrees
Some checks failed
ci / check (pull_request) Has been cancelled
04d82be522
kubus merged commit cbbfcf0223 into main 2026-09-29 21:34:23 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
kubus/hatchat!9
No description provided.