zidane 9.1.3 → 9.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{acp-CeCavNlg.js → acp-Cl4gCZN6.js} +3 -3
- package/dist/{acp-CeCavNlg.js.map → acp-Cl4gCZN6.js.map} +1 -1
- package/dist/acp-cli.js +4 -4
- package/dist/acp.js +1 -1
- package/dist/{auth-store-port-BKpjlJ8a.js → auth-store-port-DBl2JUC6.js} +2 -2
- package/dist/{auth-store-port-BKpjlJ8a.js.map → auth-store-port-DBl2JUC6.js.map} +1 -1
- package/dist/{auto-update-restart-CQJtndMG.js → auto-update-restart-CF_kjcJd.js} +10 -10
- package/dist/{auto-update-restart-CQJtndMG.js.map → auto-update-restart-CF_kjcJd.js.map} +1 -1
- package/dist/chat.js +9 -9
- package/dist/{cli-shared-DeRHgYvl.js → cli-shared-DIV9c5Nr.js} +3 -3
- package/dist/{cli-shared-DeRHgYvl.js.map → cli-shared-DIV9c5Nr.js.map} +1 -1
- package/dist/{credentials-Bqg-qNBM.js → credentials-D71duF64.js} +2 -2
- package/dist/{credentials-Bqg-qNBM.js.map → credentials-D71duF64.js.map} +1 -1
- package/dist/daemon-cli.js +2 -2
- package/dist/{discovery-CX7nbMCn.js → discovery-DD4wPWKS.js} +4 -4
- package/dist/{discovery-CX7nbMCn.js.map → discovery-DD4wPWKS.js.map} +1 -1
- package/dist/{engine-CxBGgU94.js → engine-BDmtgr2O.js} +3 -3
- package/dist/{engine-CxBGgU94.js.map → engine-BDmtgr2O.js.map} +1 -1
- package/dist/{executor-j_LFRBwS.js → executor-EWOhLyd9.js} +2 -2
- package/dist/{executor-j_LFRBwS.js.map → executor-EWOhLyd9.js.map} +1 -1
- package/dist/extensions/testing.js +2 -2
- package/dist/{extensions-ADlhBQ_1.js → extensions-DklXPjiq.js} +6 -6
- package/dist/{extensions-ADlhBQ_1.js.map → extensions-DklXPjiq.js.map} +1 -1
- package/dist/extensions.js +5 -5
- package/dist/{google-B-Dhmzzf.js → google-BtMW9KzL.js} +2 -2
- package/dist/{google-B-Dhmzzf.js.map → google-BtMW9KzL.js.map} +1 -1
- package/dist/{host-signals-Cb2TlGtN.js → host-signals-91zyko_i.js} +5 -5
- package/dist/{host-signals-Cb2TlGtN.js.map → host-signals-91zyko_i.js.map} +1 -1
- package/dist/{host-C61ntFBC.js → host-uP3t08w8.js} +9 -9
- package/dist/{host-C61ntFBC.js.map → host-uP3t08w8.js.map} +1 -1
- package/dist/{host-version-BIx4CuBM.js → host-version-_RHlAB29.js} +2 -2
- package/dist/{host-version-BIx4CuBM.js.map → host-version-_RHlAB29.js.map} +1 -1
- package/dist/index.js +5 -5
- package/dist/ipc/remote.js +3 -3
- package/dist/{ipc-DiH_P-wr.js → ipc-CatnsVEd.js} +4 -4
- package/dist/{ipc-DiH_P-wr.js.map → ipc-CatnsVEd.js.map} +1 -1
- package/dist/ipc.js +4 -4
- package/dist/{links-BbC_lOnn.js → links-a8C9eby4.js} +8 -8
- package/dist/{links-BbC_lOnn.js.map → links-a8C9eby4.js.map} +1 -1
- package/dist/{loaded-entry-C0vkCVNQ.js → loaded-entry-D6jzt5HQ.js} +2 -2
- package/dist/{loaded-entry-C0vkCVNQ.js.map → loaded-entry-D6jzt5HQ.js.map} +1 -1
- package/dist/{memory-C3w8nO8f.js → memory-CJy52DNi.js} +15 -15
- package/dist/{memory-C3w8nO8f.js.map → memory-CJy52DNi.js.map} +1 -1
- package/dist/{path-display-DRAEpKOU.js → path-display-CAWz-sMq.js} +3 -3
- package/dist/{path-display-DRAEpKOU.js.map → path-display-CAWz-sMq.js.map} +1 -1
- package/dist/providers/google.js +1 -1
- package/dist/{providers-CpXV_uLE.js → providers-zVRxiJDv.js} +2 -2
- package/dist/{providers-CpXV_uLE.js.map → providers-zVRxiJDv.js.map} +1 -1
- package/dist/providers.js +1 -1
- package/dist/{registry-CJHvmvaP.js → registry-CBoYDIND.js} +2 -2
- package/dist/{registry-CJHvmvaP.js.map → registry-CBoYDIND.js.map} +1 -1
- package/dist/{registry-CN6JlZKj.js → registry-CQOU5jvF.js} +2 -2
- package/dist/{registry-CN6JlZKj.js.map → registry-CQOU5jvF.js.map} +1 -1
- package/dist/{registry-CnnKaY9s.js → registry-YUUqbYGr.js} +2 -2
- package/dist/{registry-CnnKaY9s.js.map → registry-YUUqbYGr.js.map} +1 -1
- package/dist/{remote-hosts-DoO9CJUQ.js → remote-hosts-CT8Q95dR.js} +4 -4
- package/dist/{remote-hosts-DoO9CJUQ.js.map → remote-hosts-CT8Q95dR.js.map} +1 -1
- package/dist/{setting-D_FpKFMw.js → setting-_jh2OtiL.js} +2 -2
- package/dist/{setting-D_FpKFMw.js.map → setting-_jh2OtiL.js.map} +1 -1
- package/dist/{settings-keys-CW2UgHgb.js → settings-keys-BT0_oKjO.js} +2 -2
- package/dist/{settings-keys-CW2UgHgb.js.map → settings-keys-BT0_oKjO.js.map} +1 -1
- package/dist/{system-freeze-XgDPIwQS.js → system-freeze-DxJ-lRhu.js} +2 -2
- package/dist/{system-freeze-XgDPIwQS.js.map → system-freeze-DxJ-lRhu.js.map} +1 -1
- package/dist/tui.js +15 -15
- package/dist/{version-szdW1Od9.js → version-DwQI12WA.js} +2 -2
- package/dist/{version-szdW1Od9.js.map → version-DwQI12WA.js.map} +1 -1
- package/dist/{wire-C3l9jl9x.js → wire-Dxwxb7QQ.js} +2 -2
- package/dist/{wire-C3l9jl9x.js.map → wire-Dxwxb7QQ.js.map} +1 -1
- package/dist/{ws-CEHi0CiB.js → ws-CA3o2nie.js} +4 -4
- package/dist/{ws-CEHi0CiB.js.map → ws-CA3o2nie.js.map} +1 -1
- package/dist/{ws-client-t8XNWxcq.js → ws-client-C0iPAAzc.js} +2 -2
- package/dist/{ws-client-t8XNWxcq.js.map → ws-client-C0iPAAzc.js.map} +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setting-D_FpKFMw.js","names":[],"sources":["../src/chat/todos.ts","../src/chat/agents.ts","../src/chat/session-factory.ts","../src/durable/setting.ts"],"sourcesContent":["/**\n * Todos — `todowrite` / `todoread` tools for task checkpointing.\n *\n * The model uses these to plan multi-step work and stream progress as it\n * executes. The list is monolithic-replacement-by-design: every `todowrite`\n * overwrites the active list in full, mirroring Anthropic's reference\n * `TodoWrite` semantics.\n *\n * Persistence model — `session.metadata.todos: TodosBag`:\n *\n * ```ts\n * { session?: TodoItem[], byRun?: Record<runId, TodoItem[]> }\n * ```\n *\n * - Top-level runs (no `parentRunId`) **share** the `session` slot. A list\n * written in one prompt survives across the next prompt — the user\n * aborts a run with `in_progress` items, sends a follow-up, and the\n * model's `todoread` returns the same list to continue from. Matches\n * Claude Code's TodoWrite v1 keying (`agentId ?? sessionId`).\n * - Subagent runs (`parentRunId` set) get their own slot under `byRun`\n * keyed by the child's runId. Subagent state never bleeds into the\n * parent's session list — parallel children stay isolated from each\n * other and from the parent.\n * - When every item is `completed`, the slot is **auto-cleared** to `[]`\n * on write — prevents stale \"all done\" lists from shadowing the next\n * prompt's context. Same rationale as Claude Code's `TodoWriteTool.ts:70`.\n * - **Archive sidecar** (`bag.archive`): every non-empty write also\n * stashes the list under `archive.session` / `archive.byRun`. Empty\n * writes (auto-clear or explicit reset) preserve the archive. UI\n * surfaces fall back to it when the live slot is empty so the modal\n * keeps showing \"what was just completed\" — invisible to the model\n * (`todoread` strictly reads the live slot).\n * - Resume: the persisted `tool_result` block in `session.turns` carries the\n * latest snapshot. `todowrite` only mutates session memory (`setMeta`) and\n * the run loop does not save the bag, so a process that reloads the session\n * from the store can lack it (see {@link applyTodosSnapshot}).\n *\n * Policy is layered:\n * - Identical-payload dedup: handled **inside the tool body**, against the\n * current slot (session-shared for top-level, per-run for subagents).\n * Does NOT plumb through `behavior.dedupTools` — that cache is keyed\n * per session and would conflate top-level + subagent re-writes.\n * - Soft nudge (\"you've checkpointed N times, slow down\"): rides on the\n * tool's own `tool_result` string. The model sees the nudge in the\n * result it just received — no `system:transform` plumbing. Count is\n * scoped the same way the data is (session-shared for top-level,\n * per-run for subagents) so the nudge fires on cumulative over-use.\n * - Per-run write cap **(opt-in)**: available via\n * `createTodoTools({ maxWritesPerRun: N })`. Off by default —\n * `todowrite` is a state-transition tool, capping it punishes the\n * legitimate \"finish the N-item plan\" path (which needs ≥ N+1\n * calls). Hosts that explicitly want a hard ceiling can opt in.\n *\n * No OpenTUI imports — a GUI consumer reuses this module verbatim.\n */\nimport type { Preset } from '../presets'\nimport type { Session as StoredSession } from '../session'\nimport type { ToolContext, ToolDef } from '../tools/types'\nimport { definePreset } from '../presets'\n\nexport { TODO_STATUS_GLYPHS } from './todo-glyphs'\n\n/** Todos live in session metadata and are memory-only here: any session, store-less snapshots included. */\ntype Session = Omit<StoredSession, 'save'>\n\n// ---------------------------------------------------------------------------\n// Tool identities — canonical names persisted in `session.turns`. Stable\n// across the lifetime of the project; renaming would break resume.\n// ---------------------------------------------------------------------------\n\n/** Canonical name of the tool that replaces the todo list. */\nexport const TODOWRITE_TOOL = 'todowrite'\n/** Canonical name of the tool that reads the todo list. */\nexport const TODOREAD_TOOL = 'todoread'\n\n/**\n * True when `name` is one of the todo tool canonical names.\n *\n * @param name - Tool name to test.\n */\nexport function isTodoTool(name: string): boolean {\n return name === TODOWRITE_TOOL || name === TODOREAD_TOOL\n}\n\n// ---------------------------------------------------------------------------\n// Session metadata keys — exported so consumers (UI rehydrate, export\n// pipelines, fork helpers) read/write the same shape we do. Keep them\n// alongside the tool to make the contract obvious.\n// ---------------------------------------------------------------------------\n\n/** `session.metadata[TODOS_METADATA_KEY]: TodosBag` — see {@link TodosBag}. */\nexport const TODOS_METADATA_KEY = 'todos'\n/** `session.metadata[TODO_WRITE_COUNTS_METADATA_KEY]: CountsBag` — mirrors `TodosBag` shape. */\nexport const TODO_WRITE_COUNTS_METADATA_KEY = 'todoWriteCounts'\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Progress of one todo. */\nexport type TodoStatus = 'pending' | 'in_progress' | 'completed' | 'cancelled'\n\n/** One entry in the todo list. */\nexport interface TodoItem {\n /** Stable id — the model picks it. Reusing an id replaces that item. */\n id: string\n /** One-line task summary (Markdown allowed; renderers may sanitize). */\n content: string\n /** Current progress. */\n status: TodoStatus\n}\n\n/**\n * On-disk shape at `session.metadata[TODOS_METADATA_KEY]`.\n *\n * Sub-fields are absent (not empty) when they carry no data — keeps the\n * persisted metadata clean and lets `JSON.stringify` skip empty bags\n * entirely on session save.\n */\nexport interface TodosBag {\n /** Top-level slot — shared across runs whose `parentRunId` is unset. */\n session?: TodoItem[]\n /** Per-subagent-run slots, keyed by the child run's id. */\n byRun?: Record<string, TodoItem[]>\n /**\n * Most-recent non-empty list per slot — sidecar for UI surfaces, NOT\n * visible to `todoread`. Updated on every non-empty write; **preserved**\n * across the auto-clear path. Lets the modal keep showing \"what was just\n * completed\" after a fully-done batch wipes the live slot. Empty writes\n * never touch the archive — clearing the live list shouldn't erase the\n * audit trail.\n */\n archive?: {\n session?: TodoItem[]\n byRun?: Record<string, TodoItem[]>\n }\n}\n\n/** Mirror of {@link TodosBag} for the write-counter — same scope rules. */\ninterface CountsBag {\n session?: number\n byRun?: Record<string, number>\n}\n\nconst TODO_STATUS_VALUES: readonly TodoStatus[] = ['pending', 'in_progress', 'completed', 'cancelled']\n\n/**\n * Default ceiling on a single todo list. Exported so every write boundary caps\n * identically — a boundary with a different cap would let a list through that\n * the tool would have truncated.\n */\nexport const DEFAULT_MAX_TODO_ITEMS = 100\n\n/** Options for {@link createTodoTools}. */\nexport interface CreateTodoToolsOptions {\n /**\n * Maximum item count per `todowrite`. Excess items are truncated by the\n * validator and reported back to the model in the tool_result.\n *\n * Default: `100`. The default is generous; a planning agent that exceeds\n * 100 items per call is usually flailing.\n */\n maxItems?: number\n /**\n * Per-run cap on `todowrite` calls, plumbed into `behavior.toolBudgets`.\n * `onMaxWrites` chooses the reaction (steer / block) — see\n * {@link AgentBehavior.toolBudgets} for the contract.\n *\n * **Default: `0` (no cap).** `todowrite` is a state-transition tool —\n * a legitimate run of an N-item list needs at least `N + 1` calls\n * (initial plan + one transition per item) to complete, so a hard\n * cap punishes the legitimate path. The nudges that DO apply are the\n * `remindAfter` reminder (soft, in-band) and the identical-payload\n * dedup check (silent, structural). Hosts that want a true hard cap\n * can opt in by setting `maxWritesPerRun: N`.\n *\n * **Counting semantics** (when opted in): the budget counts every\n * dispatched call, including dedup substitutes (zidane spec). The\n * {@link remindAfter} counter counts execute-only dispatches —\n * dedup hits replay the prior result and never reach the tool body.\n * Use this budget (not the reminder) as the hard limit.\n */\n maxWritesPerRun?: number\n /**\n * Reaction when `maxWritesPerRun` is exceeded: `steer` nudges the model, `block` refuses the call.\n *\n * @default 'steer'\n */\n onMaxWrites?: 'steer' | 'block'\n /**\n * Hard stop for **consecutive identical** `todowrite` payloads — the\n * exact loop the in-body `dedupIdentical` check only *whispers* about\n * (\"No change — N items already tracked\"). Plumbs a run-scoped\n * `behavior.dedupTools.todowrite` entry in `block-after` mode: the\n * gate replays the unchanged result for the first `N - 1` identical\n * calls, then refuses the `N`th (and every subsequent identical one)\n * with a `Blocked: …` tool_result so the model breaks out instead of\n * re-checkpointing forever.\n *\n * This is safe to leave on by default — unlike {@link maxWritesPerRun}\n * it only ever blocks a payload byte-identical to the one already\n * tracked, so a legitimate \"finish the N-item plan\" run (which writes\n * a *different* list each transition) is never touched. The counter\n * resets the moment the model writes anything different.\n *\n * Run-scoped via `DedupToolConfig.scope: 'run'`, so a parent and a\n * subagent writing the same list never share the counter. A session-scoped\n * `behavior.dedupTools` entry would conflate them.\n *\n * Set to `0` to opt out (e.g. a host that installs its own loop\n * hardening). Default: `5`. Values `< 2` are clamped to `2` by the\n * dedup middleware (a threshold of 1 would block the first call).\n */\n maxIdenticalWrites?: number\n /**\n * Custom refusal text for the {@link maxIdenticalWrites} block. Receives\n * the 1-indexed identical-call count (including the blocked call). Leave\n * unset for the built-in \"you've written the same list N times in a\n * row\" message.\n */\n identicalBlockReason?: (count: number) => string\n /**\n * Append a \"you've checkpointed N times — only call this at significant\n * task transitions\" nudge to the tool_result once `todowrite` has fired\n * this many times in the current run. The nudge rides on the result\n * string, so the model sees it on its next turn without `system:transform`\n * plumbing.\n *\n * Counts only executed dispatches (dedup hits skip the tool body and so\n * skip the increment). For a hard cap that also covers dedup hits, use\n * {@link maxWritesPerRun}.\n *\n * Set to `0` to opt out. Default: `3`.\n */\n remindAfter?: number\n /**\n * Custom reminder text. Receives the post-call count and the items that\n * were just written. Return `''` (or `undefined`) to suppress the reminder\n * for this specific call. Default text nudges towards \"back to executing,\n * not re-planning.\"\n */\n reminderText?: (count: number, items: readonly TodoItem[]) => string | undefined\n /**\n * Short-circuit the tool body when the incoming payload is identical to\n * the **current slot** (session-shared for top-level runs, per-run for\n * subagents). The model gets a \"No change — N items already tracked.\"\n * acknowledgment instead of a fresh \"Updated N items.\" line.\n * Default: `true`.\n *\n * Read `session.metadata.todos` via {@link getTodosForRun} for the\n * comparison — so the resolution naturally follows the keying rule\n * (top-level reads the session slot, subagent reads its own). Does\n * NOT plumb through `behavior.dedupTools`: that cache lives at the\n * gate, is keyed per session, and would conflate top-level + subagent\n * re-writes of structurally identical payloads.\n *\n * Counter advances on identical re-writes regardless — the model\n * called the tool, that's a call. Lets the reminder threshold catch\n * spam. Set `false` to skip the check entirely (every call rewrites\n * the slot, useful when state-shape comparisons are pointless for\n * your usage).\n */\n dedupIdentical?: boolean\n /**\n * Override the JSON-schema-level descriptions surfaced to the model.\n * Useful for telemetry experiments. Leave unset to inherit the built-in\n * doctrine prose.\n */\n writeDescription?: string\n /** Replaces the `todoread` tool description shown to the model. */\n readDescription?: string\n}\n\n// ---------------------------------------------------------------------------\n// Built-in doctrine prose\n// ---------------------------------------------------------------------------\n\nconst WRITE_DESCRIPTION\n = 'Replace the active task list. Pass the full list of items every '\n + 'call; there is no partial update. The list persists across user '\n + 'prompts in the same session, so an aborted run with `in_progress` '\n + 'items reads back on the next `todoread` and you can pick up where '\n + 'you left off. When every item is `completed`, the list auto-clears '\n + 'so the next prompt starts fresh.\\n\\n'\n + 'Use the `status` field to track progress: `pending` (queued), '\n + '`in_progress` (currently working), `completed` (done), `cancelled` '\n + '(no longer relevant).\\n\\n'\n + 'Only checkpoint at significant transitions:\\n'\n + ' 1. On a multi-step task, or before a run that will change several '\n + 'files: write the plan with everything `pending` so the user sees it.\\n'\n + ' 2. When you transition between steps: mark the previous one '\n + '`completed` and the next one `in_progress`.\\n'\n + ' 3. When the user asks for the current status: re-emit the list '\n + 'unchanged so they can see it.\\n\\n'\n + 'Do NOT call this on every action. The list is for the user\\'s '\n + 'situational awareness, not for self-narrating.'\n\nconst READ_DESCRIPTION\n = 'Return the current active task list. Returns an empty list if nothing '\n + 'has been written yet, or if the previous batch was auto-cleared '\n + 'after all items completed. Use sparingly; you already see the '\n + 'latest list in your own `todowrite` tool_result above.'\n\nfunction defaultReminder(count: number): string {\n return `(You've called todowrite ${count} times. Make sure each `\n + `checkpoint reflects real progress; avoid re-planning every step.)`\n}\n\nfunction defaultIdenticalBlockReason(count: number): string {\n return `You've written the same todo list ${count} times in a row without `\n + `any change. Stop re-checkpointing: either do the next concrete `\n + `action (edit a file, run a command) and then write a list that `\n + `reflects real progress, or finish the task. Re-emitting the `\n + `identical list does nothing.`\n}\n\n/**\n * Stable fingerprint for a `todowrite` payload, used by the run-scoped\n * `block-after` dedup gate. Normalizes the incoming `{ todos }` the same\n * way the tool body does before {@link todosEqual} — sanitize to the\n * three known fields, last-wins id-dedup, order preserved — so the gate's\n * notion of \"identical\" lines up exactly with the body's \"unchanged\"\n * short-circuit. Returns `undefined` for shapes with no usable items so\n * the dedup middleware skips them (a malformed call shouldn't poison the\n * loop counter).\n */\nfunction hashTodoWriteInput(input: Record<string, unknown>): string | undefined {\n const raw = Array.isArray(input.todos) ? input.todos : []\n const items = sanitizeItems(raw, Number.MAX_SAFE_INTEGER)\n const byId = new Map<string, TodoItem>()\n for (const item of items)\n byId.set(item.id, item)\n const normalized = [...byId.values()]\n if (normalized.length === 0)\n return undefined\n return JSON.stringify(normalized.map(t => [t.id, t.status, t.content]))\n}\n\n// ---------------------------------------------------------------------------\n// Session metadata accessors — single source of truth for the `TodosBag`\n// shape. Tests, fork helpers, UI rehydrate, and the tool bodies all go\n// through these so the keying contract (top-level vs subagent) stays in\n// one place.\n// ---------------------------------------------------------------------------\n\n/**\n * `runId` when that run is a subagent (has a `parentRunId`), else `null`\n * (the session slot). Top-level runs, `null` (no run yet) and an unknown\n * run all map to the session slot: treating an orphan as top-level is the\n * right fallback since the session slot is the more general bucket.\n */\nfunction subagentRunId(session: Session, runId: string | null): string | null {\n if (runId === null)\n return null\n const run = session.runs.find(r => r.id === runId)\n return run?.parentRunId ? runId : null\n}\n\n/**\n * Read the active list for `runId`. Top-level runs see the session-shared\n * slot; subagent runs see their own. Returns a fresh `[]` (not stored in\n * metadata) when no slot exists — the caller can mutate the result\n * without affecting state.\n *\n * @param session - The session holding the todos.\n * @param runId - Run whose list to read; `null` before the first run (the session slot).\n */\nexport function getTodosForRun(session: Session, runId: string | null): TodoItem[] {\n const bag = readTodosBag(session)\n const subagent = subagentRunId(session, runId)\n if (subagent !== null) {\n const items = bag.byRun?.[subagent]\n return Array.isArray(items) ? [...items] : []\n }\n return Array.isArray(bag.session) ? [...bag.session] : []\n}\n\n/**\n * Mirror a todo snapshot from another process into this session's metadata.\n *\n * Needed because the run loop never persists the todos bag — `todowrite` calls\n * `setMeta`, which only mutates memory, and nothing saves it. That is harmless\n * in-process (the UI reads the very object the tool wrote) but fatal across a\n * process boundary: a window that reloads the session from the shared store\n * finds NO todos metadata and renders an empty list, even though the daemon's\n * agent has a full one.\n *\n * So the authoritative `todos:snapshot` signal is applied here instead of being\n * re-derived from disk. Writes BOTH slots: `todos` is what the model sees, and\n * `archive` is what the UI falls back to after an auto-clear — reconstructing\n * the archive from `todos` alone would blank the modal in exactly the\n * all-completed case the archive exists to cover.\n *\n * Memory-only by design (no `save()`): this is a local mirror of another\n * process's state, and persisting it would let a window's stale view overwrite\n * the owner's. Whoever owns the agent owns the durable copy.\n */\nexport function applyTodosSnapshot(\n session: Session,\n snapshot: { runId: string | null, todos: readonly TodoItem[], archive: readonly TodoItem[] },\n): void {\n const { runId } = snapshot\n // No run and nothing listed: an empty session's snapshot, nothing to mirror (and no list to wipe).\n if (!runId && snapshot.todos.length === 0 && snapshot.archive.length === 0)\n return\n const bag = readTodosBag(session)\n const subagent = subagentRunId(session, runId)\n const live = snapshot.todos.map(normalizeItem)\n const archived = snapshot.archive.map(normalizeItem)\n\n const assign = (\n target: { session?: TodoItem[], byRun?: Record<string, TodoItem[]> },\n items: TodoItem[],\n ): void => {\n if (subagent !== null) {\n const byRun = { ...(target.byRun ?? {}) }\n if (items.length === 0)\n delete byRun[subagent]\n else\n byRun[subagent] = items\n target.byRun = byRun\n return\n }\n if (items.length === 0)\n delete target.session\n else\n target.session = items\n }\n\n assign(bag, live)\n const archive = { ...(bag.archive ?? {}) }\n assign(archive, archived)\n bag.archive = archive\n writeTodosBag(session, bag)\n}\n\n/**\n * Replace the active list for `runId`. Routes to the session-shared slot\n * for top-level runs and to the per-run slot for subagents. Empty arrays\n * delete the slot entirely so persisted metadata stays clean.\n *\n * Side effect: a **non-empty** write also stashes the same list into\n * `bag.archive` (same routing). Empty writes never touch the archive —\n * clearing the live list (whether explicitly or via the tool's auto-\n * clear) preserves the \"last shown\" snapshot so UI surfaces can keep\n * rendering what was just completed. See {@link getArchivedTodosForRun}.\n *\n * `opts.clearArchive` opts OUT of that preservation, and exists because the\n * archive is only the right answer for the AUTO-CLEAR path. UI surfaces render\n * `todos.length > 0 ? todos : archive`, so a user who deletes every todo would\n * otherwise watch the archive fall straight back into view with the same items\n * — a destructive action they confirmed, with no visible effect, and the footer\n * chip still counting the list they just deleted. Pass it for user-initiated\n * writes; leave it off for the tool.\n *\n * @param session - The session to write into.\n * @param runId - Run whose list to replace; `null` before the first run writes the\n * session slot, which the first top-level run then reads as its own.\n * @param items - The new list. An empty list clears the live slot.\n * @param opts - Write options.\n * @param opts.clearArchive - Also drop the archived list when `items` is empty.\n */\nexport function setTodosForRun(\n session: Session,\n runId: string | null,\n items: readonly TodoItem[],\n opts: { clearArchive?: boolean } = {},\n): void {\n const bag = readTodosBag(session)\n const normalized = items.map(normalizeItem)\n const subagent = subagentRunId(session, runId)\n\n // Active slot.\n if (subagent) {\n const byRun = { ...(bag.byRun ?? {}) }\n if (normalized.length === 0)\n delete byRun[subagent]\n else\n byRun[subagent] = normalized\n bag.byRun = byRun\n }\n else if (normalized.length === 0) {\n delete bag.session\n }\n else {\n bag.session = normalized\n }\n\n // Archive — only on non-empty writes. Empty writes preserve the prior\n // archive so the modal keeps showing \"what was just done\" after the\n // live slot wipes.\n if (normalized.length > 0) {\n const archive = { ...(bag.archive ?? {}) }\n if (subagent) {\n const archiveByRun = { ...(archive.byRun ?? {}) }\n archiveByRun[subagent] = normalized\n archive.byRun = archiveByRun\n }\n else {\n archive.session = normalized\n }\n bag.archive = archive\n }\n else if (opts.clearArchive) {\n // Explicit user deletion — drop the snapshot too, or the UI's\n // `todos : archive` fallback would re-render exactly what was deleted.\n const archive = { ...(bag.archive ?? {}) }\n if (subagent) {\n const archiveByRun = { ...(archive.byRun ?? {}) }\n delete archiveByRun[subagent]\n archive.byRun = archiveByRun\n }\n else {\n delete archive.session\n }\n bag.archive = archive\n }\n\n writeTodosBag(session, bag)\n}\n\n/**\n * Read the archived (most-recent non-empty) list for `runId`. Same slot\n * routing as {@link getTodosForRun}, but reads `bag.archive` instead of\n * the live slot. Returns a fresh `[]` when no archive exists.\n *\n * UI surfaces use this to keep showing \"what was just completed\" after\n * the live list auto-clears. The model never sees the archive — it's a\n * read-only sidecar for human-facing rendering.\n *\n * @param session - The session holding the todos.\n * @param runId - Run whose archived list to read; `null` before the first run.\n */\nexport function getArchivedTodosForRun(session: Session, runId: string | null): TodoItem[] {\n const bag = readTodosBag(session)\n const archive = bag.archive\n if (!archive)\n return []\n const subagent = subagentRunId(session, runId)\n if (subagent !== null) {\n const items = archive.byRun?.[subagent]\n return Array.isArray(items) ? [...items] : []\n }\n return Array.isArray(archive.session) ? [...archive.session] : []\n}\n\n/**\n * Reconcile `session.metadata.todos.byRun` (live + archive) against\n * `session.runs`. Drops subagent slots whose runId isn't in the run\n * list. The top-level `session` slot is unaffected — it's session-\n * scoped, not per-run, so there's no orphan to GC.\n *\n * Useful after `session.setRuns()` (fork / restore) or to GC stale\n * metadata mutated by an external caller. Also prunes the parallel\n * counter bag so the two don't drift.\n *\n * `dropped` reports the live-byRun orphans only (archive-only orphans\n * are silently swept). The live half is the contract callers actually\n * observe.\n *\n * @param session - The session to reconcile.\n */\nexport function pruneTodosByRun(session: Session): { dropped: readonly string[] } {\n const validRunIds = new Set(session.runs.map(r => r.id))\n const dropped: string[] = []\n\n const bag = readTodosBag(session)\n let bagChanged = false\n if (bag.byRun) {\n const nextByRun: Record<string, TodoItem[]> = {}\n for (const [runId, items] of Object.entries(bag.byRun)) {\n if (validRunIds.has(runId)) {\n nextByRun[runId] = items\n }\n else {\n dropped.push(runId)\n bagChanged = true\n }\n }\n if (Object.keys(nextByRun).length !== Object.keys(bag.byRun).length)\n bag.byRun = nextByRun\n }\n if (bag.archive?.byRun) {\n const nextByRun: Record<string, TodoItem[]> = {}\n let archiveChanged = false\n for (const [runId, items] of Object.entries(bag.archive.byRun)) {\n if (validRunIds.has(runId))\n nextByRun[runId] = items\n else\n archiveChanged = true\n }\n if (archiveChanged) {\n bag.archive = { ...bag.archive, byRun: nextByRun }\n bagChanged = true\n }\n }\n if (bagChanged)\n writeTodosBag(session, bag)\n\n const counts = readCountsBag(session)\n if (counts.byRun) {\n const nextByRun: Record<string, number> = {}\n let changed = false\n for (const [runId, n] of Object.entries(counts.byRun)) {\n if (validRunIds.has(runId))\n nextByRun[runId] = n\n else\n changed = true\n }\n if (changed) {\n counts.byRun = nextByRun\n writeCountsBag(session, counts)\n }\n }\n\n return { dropped }\n}\n\n// ---------------------------------------------------------------------------\n// Tool factory\n// ---------------------------------------------------------------------------\n\n/**\n * Build a `Preset` carrying the `{ todowrite, todoread }` tool pair.\n *\n * Loop hardening is layered, soft → hard:\n * 1. **In-body identical dedup** (`dedupIdentical`, on) — resolves\n * against the active slot (session-shared for top-level runs,\n * per-run for subagents) and returns \"No change — N items already\n * tracked.\" Advisory; the model can ignore it.\n * 2. **In-band reminder** (`remindAfter`, on) — appends a \"you've\n * checkpointed N times\" nudge to the result. Also advisory.\n * 3. **Consecutive-identical hard block** (`maxIdenticalWrites`, ON by\n * default) — a run-scoped `behavior.dedupTools.todowrite` entry in\n * `block-after` mode. After N byte-identical writes in a row the\n * gate refuses with `Blocked: …` and the body never runs. This is\n * the real loop breaker, and it's safe-by-default precisely because\n * it only fires on *identical* payloads — a legitimate N-item plan\n * writes a *different* list each step and never trips it.\n *\n * By default the preset adds **no** `toolBudgets` entry — a blanket\n * per-call cap is the wrong abstraction for a state-transition tool (it\n * punishes the legitimate \"finish the N-item plan\" path). Hosts that\n * want a ceiling on *all* writes (not just identical ones) pass\n * `maxWritesPerRun: N` explicitly; the factory then also plumbs a\n * `behavior.toolBudgets.todowrite` entry.\n *\n * Returning a `Preset` (not a bare tool map) lets the result flow\n * through {@link composePresets} unchanged. `behavior.dedupTools` and\n * `behavior.toolBudgets` are tool-name-keyed records that\n * `composePresets` deep-merges, so a caller's entries for other tools\n * survive the layering, and a caller's override for `todowrite` itself\n * wins by being placed later in the chain.\n *\n * ```ts\n * import { basic, composePresets } from 'zidane/presets'\n * import { createTodoTools } from 'zidane/chat'\n *\n * // Default: soft dedup + reminder + a run-scoped hard block after 5\n * // identical writes in a row.\n * createAgent({ ...composePresets(basic, createTodoTools()), provider })\n *\n * // Tune (or disable) the identical-write block.\n * createTodoTools({ maxIdenticalWrites: 3 }) // stricter\n * createTodoTools({ maxIdenticalWrites: 0 }) // off — soft signals only\n *\n * // Opt-in blanket cap on *all* writes (counts distinct ones too).\n * createAgent({\n * ...composePresets(basic, createTodoTools({ maxWritesPerRun: 20 })),\n * provider,\n * })\n * ```\n *\n * For the trivial \"just add the tools to an existing config\" case, plain\n * spread is also fine — the returned `Preset` sets `tools` plus a\n * `behavior` block (carrying the default identical-write dedup):\n *\n * ```ts\n * createAgent({ ...basic, ...createTodoTools(), provider })\n * ```\n *\n * @param options - Limits, reminder and dedup settings for the todo tools.\n */\nexport function createTodoTools(options: CreateTodoToolsOptions = {}): Preset {\n const maxItems = options.maxItems ?? DEFAULT_MAX_TODO_ITEMS\n const remindAfter = options.remindAfter ?? 3\n const reminderText = options.reminderText ?? ((count, _items) => defaultReminder(count))\n const dedupIdentical = options.dedupIdentical ?? true\n // Default opt-out — `todowrite` is a state-transition tool, capping\n // it punishes legitimate \"finish the 5-item plan\" paths (which need\n // ≥6 calls). The `remindAfter` reminder + `dedupIdentical` check\n // handle spam in-band; a host that wants a hard ceiling can opt in\n // explicitly.\n const maxWritesPerRun = options.maxWritesPerRun ?? 0\n const onMaxWrites = options.onMaxWrites ?? 'steer'\n // On by default — a run-scoped, identical-payload-only hard stop. Only\n // ever blocks a byte-for-byte repeat of the already-tracked list, so it\n // can't punish a legitimate multi-step plan (every transition writes a\n // *different* list). See `maxIdenticalWrites` JSDoc.\n const maxIdenticalWrites = options.maxIdenticalWrites ?? 5\n\n const tools: Record<string, ToolDef> = {\n [TODOWRITE_TOOL]: createTodoWriteTool({\n maxItems,\n remindAfter,\n reminderText,\n dedupIdentical,\n description: options.writeDescription ?? WRITE_DESCRIPTION,\n }),\n [TODOREAD_TOOL]: createTodoReadTool({\n description: options.readDescription ?? READ_DESCRIPTION,\n }),\n }\n\n // `toolBudgets` is opt-in: the budget fires at the `tool:gate` layer to\n // enforce a hard cap. The soft identical-payload check lives in the tool body\n // instead of a session-scoped `behavior.dedupTools` entry, which would\n // silently drop cross-run re-writes of an identical list (see\n // `dedupIdentical` JSDoc). Only the run-scoped hard block below uses `dedupTools`.\n const behavior: NonNullable<Preset['behavior']> = {}\n if (maxWritesPerRun > 0)\n behavior.toolBudgets = { [TODOWRITE_TOOL]: { max: maxWritesPerRun, onExceed: onMaxWrites } }\n\n // Consecutive-identical-write loop breaker. Run-scoped so a parent and a\n // subagent never share the counter; `block-after` so the first few\n // identical re-writes replay the unchanged result (matching the in-body\n // soft dedup) before the gate hard-refuses. The hasher mirrors the\n // body's `todosEqual` normalization (order-sensitive, three fields) so\n // the gate's \"identical\" and the body's \"unchanged\" agree.\n if (maxIdenticalWrites > 0) {\n const reason = options.identicalBlockReason ?? defaultIdenticalBlockReason\n behavior.dedupTools = {\n [TODOWRITE_TOOL]: {\n hasher: hashTodoWriteInput,\n mode: 'block-after',\n threshold: maxIdenticalWrites,\n scope: 'run',\n reason: (_input, count) => reason(count),\n },\n }\n }\n\n return definePreset(\n Object.keys(behavior).length > 0\n ? { tools, behavior }\n : { tools },\n )\n}\n\n// ---------------------------------------------------------------------------\n// Tool implementations\n// ---------------------------------------------------------------------------\n\ninterface CreateTodoWriteOptions {\n maxItems: number\n remindAfter: number\n reminderText: (count: number, items: readonly TodoItem[]) => string | undefined\n dedupIdentical: boolean\n description: string\n}\n\nfunction createTodoWriteTool(opts: CreateTodoWriteOptions): ToolDef {\n return {\n spec: {\n name: TODOWRITE_TOOL,\n description: opts.description,\n inputSchema: {\n type: 'object',\n properties: {\n todos: {\n type: 'array',\n description: 'The complete task list. Replaces the prior list in full.',\n maxItems: opts.maxItems,\n items: {\n type: 'object',\n properties: {\n id: { type: 'string', description: 'Stable identifier (the model picks).' },\n content: { type: 'string', description: 'One-line task summary.' },\n status: {\n type: 'string',\n enum: [...TODO_STATUS_VALUES],\n description: '`pending`, `in_progress`, `completed`, or `cancelled`.',\n },\n },\n required: ['id', 'content', 'status'],\n },\n },\n },\n required: ['todos'],\n },\n },\n async execute(input, ctx) {\n const { session, runId } = requireSessionAndRun(ctx, TODOWRITE_TOOL)\n\n const rawItems = Array.isArray(input.todos) ? input.todos : []\n const items = sanitizeItems(rawItems, opts.maxItems)\n const dropped = rawItems.length - items.length\n\n // ID dedup — last-wins. Two items in one payload sharing an id is\n // a model mistake; quietly normalize rather than error.\n const byId = new Map<string, TodoItem>()\n for (const item of items)\n byId.set(item.id, item)\n const normalized = [...byId.values()]\n\n // Identical-payload short-circuit — compares against the slot the\n // active run resolves to (session-shared for top-level, per-run\n // for subagents). See `dedupIdentical` JSDoc.\n const current = getTodosForRun(session, runId)\n const unchanged = opts.dedupIdentical && todosEqual(current, normalized)\n\n // Counter advances on every call — including identical re-writes —\n // so the reminder threshold catches \"model keeps re-checkpointing\n // the same list\" spam. Scope mirrors the data (session-shared for\n // top-level, per-run for subagents) so the count reflects\n // cumulative use of the *active* slot, not noisy per-prompt resets.\n const count = incrementCount(session, runId)\n\n // Auto-clear when every item is `completed`. Matches Claude Code's\n // TodoWrite v1 behavior — once a batch is fully done, the next\n // prompt should start clean rather than re-read a wall of\n // checkmarks. The model's reply still summarizes the completion;\n // only the persisted slot is wiped.\n const allCompleted = normalized.length > 0 && normalized.every(t => t.status === 'completed')\n\n if (!unchanged) {\n if (allCompleted) {\n // Two-phase auto-clear: first persist the all-completed list\n // so the archive captures it (setTodosForRun copies non-empty\n // writes into the archive sidecar), then wipe the live slot.\n // The empty write preserves the archive, so the modal can\n // keep showing the close-out batch until the model writes\n // something new.\n setTodosForRun(session, runId, normalized)\n setTodosForRun(session, runId, [])\n }\n else {\n setTodosForRun(session, runId, normalized)\n }\n }\n\n return formatWriteResult({\n items: normalized,\n dropped,\n count,\n unchanged,\n // Only report the auto-clear when it actually fired (a no-op\n // re-write of an already-cleared list shouldn't shout\n // \"marked complete\" again).\n cleared: allCompleted && !unchanged,\n opts,\n })\n },\n }\n}\n\ninterface FormatWriteResultInput {\n items: readonly TodoItem[]\n dropped: number\n count: number\n unchanged: boolean\n cleared: boolean\n opts: CreateTodoWriteOptions\n}\n\nfunction formatWriteResult(input: FormatWriteResultInput): string {\n const { items, dropped, count, unchanged, cleared, opts } = input\n const lines: string[] = []\n const n = items.length\n const suffix = n === 1 ? '' : 's'\n if (cleared) {\n lines.push(`Marked ${n} item${suffix} complete; list cleared.`)\n }\n else if (unchanged) {\n lines.push(`No change; ${n} todo item${suffix} already tracked.`)\n }\n else {\n lines.push(`Updated ${n} todo item${suffix}.`)\n }\n // Skip the tally when the list was cleared — the headline already\n // captures \"all done\" and the tally would read as a redundant\n // restatement.\n if (!cleared) {\n const tally = summarizeStatuses(items)\n if (tally)\n lines.push(tally)\n }\n if (dropped > 0)\n lines.push(`Dropped ${dropped} malformed item${dropped === 1 ? '' : 's'}.`)\n const reminder = opts.remindAfter > 0 && count >= opts.remindAfter\n ? opts.reminderText(count, items)\n : undefined\n if (reminder && reminder.length > 0)\n lines.push(reminder)\n return lines.join('\\n')\n}\n\nfunction createTodoReadTool(opts: { description: string }): ToolDef {\n return {\n // Reads `session.metadata.todos` without mutation — concurrency-safe.\n isConcurrencySafe: true,\n replay: 'safe',\n spec: {\n name: TODOREAD_TOOL,\n description: opts.description,\n inputSchema: {\n type: 'object',\n properties: {},\n },\n },\n async execute(_input, ctx) {\n const { session, runId } = requireSessionAndRun(ctx, TODOREAD_TOOL)\n\n const items = getTodosForRun(session, runId)\n if (items.length === 0)\n return 'No todos yet. Call todowrite to start tracking tasks.'\n return JSON.stringify({ todos: items })\n },\n }\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Both tool bodies need a `Session` + `runId`. Centralize the guards so the\n * error messages stay symmetric and one fix lands in both places.\n */\nfunction requireSessionAndRun(\n ctx: ToolContext,\n toolName: string,\n): { session: Session, runId: string } {\n if (!ctx.session)\n throw new Error(`${toolName}: no session on tool context. Todos require a session via createSession().`)\n if (!ctx.runId)\n throw new Error(`${toolName}: no runId on tool context.`)\n return { session: ctx.session, runId: ctx.runId }\n}\n\n/*\n * No cap on the READ path. The list was already capped by `maxItems` when it\n * was written, and a host that raised that ceiling must not have its persisted\n * rows silently truncated on resume by a lower default here.\n */\nconst READ_ITEM_CAP = Number.MAX_SAFE_INTEGER\n\n/** Validate one persisted slot. Non-arrays and malformed rows drop out. */\nfunction readSlot(raw: unknown): TodoItem[] | undefined {\n if (!Array.isArray(raw))\n return undefined\n const items = sanitizeItems(raw, READ_ITEM_CAP)\n return items.length > 0 ? items : undefined\n}\n\n/** Validate a `runId -> items` map, dropping slots that end up empty. */\nfunction readSlotMap(raw: unknown): Record<string, TodoItem[]> | undefined {\n if (!isPlainObject(raw))\n return undefined\n const out: Record<string, TodoItem[]> = {}\n for (const [runId, value] of Object.entries(raw)) {\n const items = readSlot(value)\n if (items)\n out[runId] = items\n }\n return Object.keys(out).length > 0 ? out : undefined\n}\n\n/** Read the todos bag from session metadata, normalizing the live shape. */\nfunction readTodosBag(session: Session): TodosBag {\n const raw = session.metadata[TODOS_METADATA_KEY]\n if (!isPlainObject(raw))\n return {}\n /*\n * Persisted metadata is UNTRUSTED input, same as tool arguments: it comes off\n * disk, may have been written by an older build or hand-edited, and outlives\n * any single version of this file. Casting it with `as TodoItem[]` would let a\n * row with `status: \"done\"` (not a `TodoStatus`) type-check as valid and then\n * fall silently through the tally fold — counted in `total` but in none of\n * the four buckets.\n *\n * `sanitizeItems` is the same validator the tool-input path already uses, so\n * both entry points agree on what a well-formed item is. Malformed rows are\n * dropped rather than throwing: this is a resume path, and one bad row must\n * not cost the user the rest of their list.\n */\n const archive = isPlainObject(raw.archive)\n ? {\n session: readSlot(raw.archive.session),\n byRun: readSlotMap(raw.archive.byRun),\n }\n : undefined\n return {\n session: readSlot(raw.session),\n byRun: readSlotMap(raw.byRun),\n archive,\n }\n}\n\nfunction writeTodosBag(session: Session, bag: TodosBag): void {\n // Strip empty sub-fields so the persisted shape stays minimal.\n const clean: TodosBag = {}\n if (bag.session && bag.session.length > 0)\n clean.session = bag.session\n if (bag.byRun && Object.keys(bag.byRun).length > 0)\n clean.byRun = bag.byRun\n if (bag.archive) {\n const archive: NonNullable<TodosBag['archive']> = {}\n if (bag.archive.session && bag.archive.session.length > 0)\n archive.session = bag.archive.session\n if (bag.archive.byRun && Object.keys(bag.archive.byRun).length > 0)\n archive.byRun = bag.archive.byRun\n if (archive.session || archive.byRun)\n clean.archive = archive\n }\n session.setMeta(TODOS_METADATA_KEY, clean)\n}\n\nfunction readCountsBag(session: Session): CountsBag {\n const raw = session.metadata[TODO_WRITE_COUNTS_METADATA_KEY]\n if (!isPlainObject(raw))\n return {}\n return {\n session: typeof raw.session === 'number' ? raw.session : undefined,\n byRun: isPlainObject(raw.byRun) ? raw.byRun as Record<string, number> : undefined,\n }\n}\n\nfunction writeCountsBag(session: Session, bag: CountsBag): void {\n const clean: CountsBag = {}\n if (typeof bag.session === 'number' && bag.session > 0)\n clean.session = bag.session\n if (bag.byRun && Object.keys(bag.byRun).length > 0)\n clean.byRun = bag.byRun\n session.setMeta(TODO_WRITE_COUNTS_METADATA_KEY, clean)\n}\n\n/**\n * Bump the call counter for the active slot and return the new value.\n * Top-level runs increment `bag.session` (cumulative across prompts);\n * subagent runs increment their own entry under `bag.byRun`. Same\n * keying as {@link setTodosForRun} so the counter and the data move\n * together — a freshly seeded run's count starts at 1 on its first\n * write, a continuing top-level run's count picks up from where the\n * prior prompt left off.\n */\nfunction incrementCount(session: Session, runId: string): number {\n const bag = readCountsBag(session)\n let next: number\n if (subagentRunId(session, runId) !== null) {\n const byRun = { ...(bag.byRun ?? {}) }\n next = (byRun[runId] ?? 0) + 1\n byRun[runId] = next\n bag.byRun = byRun\n }\n else {\n next = (bag.session ?? 0) + 1\n bag.session = next\n }\n writeCountsBag(session, bag)\n return next\n}\n\n/** Narrow guard — `Record<string, unknown>` excluding arrays / null. */\nfunction isPlainObject(v: unknown): v is Record<string, unknown> {\n return !!v && typeof v === 'object' && !Array.isArray(v)\n}\n\nfunction normalizeItem(item: TodoItem): TodoItem {\n return { id: item.id, content: item.content, status: item.status }\n}\n\n/**\n * Validate an untrusted todo list, dropping malformed rows and capping length.\n *\n * The single validator for every write/read boundary: model tool input,\n * persisted metadata on read, and user edits arriving over IPC. Keeping them on\n * one implementation is what makes \"well-formed\" mean the same thing at each —\n * a boundary that only picked fields (as `normalizeItem` does) would admit an\n * invalid `status` that the read path then silently drops, so the write appears\n * to succeed and the row vanishes with nothing reported.\n *\n * @param raw - Untrusted list of items.\n * @param cap - Maximum number of items kept.\n */\nexport function sanitizeTodoItems(raw: readonly unknown[], cap: number = DEFAULT_MAX_TODO_ITEMS): TodoItem[] {\n return sanitizeItems([...raw], cap)\n}\n\nfunction sanitizeItems(raw: unknown[], cap: number): TodoItem[] {\n const out: TodoItem[] = []\n for (const item of raw) {\n if (!item || typeof item !== 'object' || Array.isArray(item))\n continue\n const obj = item as Record<string, unknown>\n const id = typeof obj.id === 'string' ? obj.id : undefined\n const content = typeof obj.content === 'string' ? obj.content : undefined\n const status = typeof obj.status === 'string' && (TODO_STATUS_VALUES as readonly string[]).includes(obj.status)\n ? obj.status as TodoStatus\n : undefined\n if (!id || !content || !status)\n continue\n out.push({ id, content, status })\n if (out.length >= cap)\n break\n }\n return out\n}\n\nfunction summarizeStatuses(items: readonly TodoItem[]): string | undefined {\n if (items.length === 0)\n return undefined\n const counts: Record<TodoStatus, number> = {\n pending: 0,\n in_progress: 0,\n completed: 0,\n cancelled: 0,\n }\n for (const item of items)\n counts[item.status] += 1\n const parts: string[] = []\n if (counts.completed)\n parts.push(`${counts.completed} completed`)\n if (counts.in_progress)\n parts.push(`${counts.in_progress} in progress`)\n if (counts.pending)\n parts.push(`${counts.pending} pending`)\n if (counts.cancelled)\n parts.push(`${counts.cancelled} cancelled`)\n return parts.length > 0 ? parts.join(' · ') : undefined\n}\n\n/**\n * Order-sensitive structural equality on todo lists. Used by the\n * `dedupIdentical` short-circuit to decide whether an incoming payload\n * matches the current run's stored slot.\n *\n * Order-sensitive on purpose: re-ordering the list is a meaningful state\n * change (\"the model reprioritized\") and should NOT collapse to a no-op.\n * Three-field comparison is exhaustive for `TodoItem` — `normalizeItem`\n * strips any extra fields before they reach the slot, so we never need to\n * compare beyond `{ id, content, status }`.\n */\nfunction todosEqual(a: readonly TodoItem[], b: readonly TodoItem[]): boolean {\n if (a.length !== b.length)\n return false\n for (let i = 0; i < a.length; i++) {\n const x = a[i]\n const y = b[i]\n if (x.id !== y.id || x.status !== y.status || x.content !== y.content)\n return false\n }\n return true\n}\n\n// ---------------------------------------------------------------------------\n// UI-facing derivation — active-run slice + tally + in-progress pick.\n//\n// Renderer-agnostic. The TUI's inline indicator + modal both consume\n// {@link useActiveTodos}; the GUI calls {@link selectActiveTodos} directly.\n// ---------------------------------------------------------------------------\n\n/** Number of todos in each status. */\nexport interface TodoTally {\n /** Todos not started. */\n pending: number\n /** Todos being worked on. */\n in_progress: number\n /** Todos finished. */\n completed: number\n /** Todos dropped. */\n cancelled: number\n}\n\n/** Todos of the run a UI surface should show. */\nexport interface ActiveTodosState {\n /** Run the slice came from. `null` ⇒ no session / no runs / no active run. */\n runId: string | null\n /**\n * Live list — what `todoread` would return. Empty when the slot was\n * cleared (e.g. auto-clear after every item completed) or never\n * written. Source of truth for the model's view.\n */\n todos: readonly TodoItem[]\n /**\n * Most-recent non-empty list for the active slot, preserved across\n * auto-clear. Equals `todos` when the live list is non-empty;\n * otherwise the last batch that was shown (commonly the all-`completed`\n * close-out). UI surfaces fall back to this when `todos` is empty so\n * the user can still glance back at \"what was just finished\" — until\n * the model writes a new list. Empty array when no list was ever\n * written for this slot.\n */\n archive: readonly TodoItem[]\n /**\n * The first `in_progress` item from `todos` (the **live** list), when\n * one exists. Drives the inline indicator above the prompt. Always\n * `null` when the live list is empty — even if the archive carries\n * an in-progress item, that item is by definition stale.\n */\n inProgress: TodoItem | null\n /**\n * Per-status counts. Computed over `todos` when non-empty, else over\n * `archive`. Zeros only when both are empty. Lets the modal header\n * show a meaningful tally for the archived \"last batch\" view.\n */\n tally: TodoTally\n}\n\nconst EMPTY_TALLY: TodoTally = { pending: 0, in_progress: 0, completed: 0, cancelled: 0 }\nconst EMPTY_ACTIVE: ActiveTodosState = {\n runId: null,\n todos: [],\n archive: [],\n inProgress: null,\n tally: EMPTY_TALLY,\n}\n\n/**\n * Pick the \"active\" run for UI surfaces that show one slot at a time\n * (todos indicator, todos modal, footer ctx indicator). The resolved\n * runId then routes through {@link getTodosForRun} — top-level runs\n * land on the session-shared slot, subagent runs on their own.\n *\n * - A currently-running run wins (latest-started one if several).\n * - Otherwise the most recently appended run wins.\n * - `null` when the session has no runs.\n *\n * Mirrors the \"most recent thing that mattered\" policy that drives\n * `lastContextSizeFromTurns` — but walks `session.runs` rather than\n * `session.turns` because the active todo slot is a function of the\n * run, not of message history.\n *\n * @param session - The session to inspect.\n */\nexport function pickActiveRunId(session: Session | null | undefined): string | null {\n if (!session)\n return null\n const runs = session.runs\n if (!runs || runs.length === 0)\n return null\n // Scan from the tail — latest-running wins, falling back to latest of any\n // status. Subagent runs are eligible like top-level runs: a child spawned\n // off the parent has its own runId / its own todo slot, and surfacing it\n // here matches the per-run isolation the data layer already guarantees.\n let latestRunning: string | null = null\n for (let i = runs.length - 1; i >= 0; i--) {\n if (runs[i].status === 'running') {\n latestRunning = runs[i].id\n break\n }\n }\n if (latestRunning)\n return latestRunning\n return runs[runs.length - 1].id\n}\n\n/**\n * Pure selector — derive the active-todos state from a session snapshot.\n * Used by {@link useActiveTodos} and directly testable without React.\n *\n * Resolves both `todos` (live, model-facing) and `archive` (last\n * non-empty snapshot for UI fallback). The tally folds over whichever\n * list is non-empty; `inProgress` is sourced strictly from `todos`\n * because an archived in-progress item is by definition stale. A subagent\n * run with no list and no archive of its own resolves to its parent run.\n *\n * @param session - The session snapshot, or a nullish value for none.\n */\nexport function selectActiveTodos(session: Session | null | undefined): ActiveTodosState {\n if (!session)\n return EMPTY_ACTIVE\n // No run yet: the session slot, which a user can fill before the first prompt.\n let runId: string | null = pickActiveRunId(session)\n // A subagent with no list of its own shows the run it works for: the\n // parent's plan is what is going on (a reloaded window showed it empty).\n const seen = new Set<string>()\n while (runId !== null && !seen.has(runId) && getTodosForRun(session, runId).length === 0 && getArchivedTodosForRun(session, runId).length === 0) {\n seen.add(runId)\n const parent = session.runs.find(r => r.id === runId)?.parentRunId\n if (!parent)\n break\n runId = parent\n }\n const todos = getTodosForRun(session, runId)\n const archive = getArchivedTodosForRun(session, runId)\n // Whichever list the modal would show. `todos` wins when live,\n // `archive` fills in after auto-clear, empty when neither exists.\n const display = todos.length > 0 ? todos : archive\n if (display.length === 0)\n return { runId, todos, archive, inProgress: null, tally: EMPTY_TALLY }\n const tally: TodoTally = { pending: 0, in_progress: 0, completed: 0, cancelled: 0 }\n let inProgress: TodoItem | null = null\n // Tally folds over `display`; `inProgress` only over `todos` so we\n // don't surface an item that's no longer being worked on.\n for (const t of display)\n tally[t.status] += 1\n if (todos.length > 0) {\n for (const t of todos) {\n if (t.status === 'in_progress') {\n inProgress = t\n break\n }\n }\n }\n return { runId, todos, archive, inProgress, tally }\n}\n\n/**\n * Progress label for the footer chip: `\"2/5\"`, or `null` when there is nothing\n * to advertise.\n *\n * The chip shows a position, not the in-progress todo's TITLE. Titles are\n * model-authored and arbitrarily long, so they would have to truncate, reflow\n * with terminal width, and still tell you nothing you couldn't get by opening\n * the list. A position reads at a glance and costs a fixed handful of columns\n * in a row that's already scanned for shortcuts.\n *\n * The numerator is the POSITION of the in-progress item, which answers\n * \"which one is live\". With nothing in progress (list finished,\n * or the archive is on screen) it falls back to the completed count, so a done\n * list reads `5/5` rather than `0/5`.\n *\n * Pure and renderer-agnostic, so the GUI and the tests can both use it.\n */\nexport function todoProgressLabel(state: ActiveTodosState): string | null {\n const display = state.todos.length > 0 ? state.todos : state.archive\n if (display.length === 0)\n return null\n const activeIndex = state.inProgress\n ? display.findIndex(t => t.id === state.inProgress!.id)\n : -1\n const numerator = activeIndex >= 0 ? activeIndex + 1 : state.tally.completed\n return `${numerator}/${display.length}`\n}\n\n/**\n * React hook — active-todos state for the supplied session. Recomputes\n * on every parent re-render; the work is O(runs + todos), both bounded\n * by small constants in practice.\n *\n * Why no memoization: `Session.runs` is mutated in place by `completeRun`\n * / `abortRun` (status flips without changing array length or item\n * identity), so any memo keyed off length / bag identity would silently\n * return stale state when e.g. a child subagent run completes and the\n * \"active\" run should reflow to the parent. The selector is cheap\n * enough that running it every parent render is the simpler, correct\n * answer — see `selectActiveTodos` for the O(n) cost.\n *\n * The hook lives in `chat/` so a GUI consumer can mount it verbatim.\n * Renderer-agnostic — no `@opentui/*` imports.\n *\n * @param session - The session snapshot, or a nullish value for none.\n */\nexport function useActiveTodos(session: Session | null | undefined): ActiveTodosState {\n return selectActiveTodos(session)\n}\n","/**\n * Agent profiles — named variants of the chat agent (Build, Plan, …).\n *\n * Each profile bundles a {@link Preset} with display metadata (label,\n * description, accent color) so the TUI can render a picker, persist the\n * user's choice, and rebuild the active agent on switch.\n *\n * The host's {@link ChatOptions.preset} is the single-agent shorthand; for\n * multi-profile setups, pass `agents` (an {@link AgentRegistry}) directly.\n * Built-ins ship as {@link BUILTIN_AGENTS} = `{ build, build-codemode, plan }`.\n *\n * A session's agent is built with one composition (tools + prompt): a\n * profile's own ({@link profileComposition}), or the one the session already\n * runs when the profile fits inside it ({@link compositionFits}), so a switch\n * keeps the prompt prefix.\n *\n * No OpenTUI dependency — this module is pure data so a GUI host can\n * consume the same registry.\n */\nimport type { Preset } from '../presets'\nimport type { ToolDef } from '../tools/types'\nimport type { AgentAccent } from './agent-accent'\nimport type { Settings } from './types'\nimport { DEFAULT_MAX_IMAGE_EDGE, DEFAULT_MEDIA_BUDGET } from '../media/defaults'\nimport { composePresets, definePreset } from '../presets'\nimport { edit, glob, grep, listFiles, multiEdit, readFile, shell, shellKill, waitTask, writeFile } from '../tools'\nimport { fetchUrl } from '../tools/fetch-url'\nimport { createSpawnTool } from '../tools/spawn'\nimport { webSearch } from '../tools/web-search'\nimport { buildBuildSystem, buildPlanSystem } from './agent-prompt'\nimport { createTodoTools } from './todos'\n\nexport { accentColor, type AgentAccent } from './agent-accent'\n\n/** A selectable agent profile: a labelled preset the user can switch between. */\nexport interface AgentProfile {\n /** Stable identifier. Each session stores the id of the profile it runs on. */\n id: string\n /** Human-readable label rendered in the picker and footer badge. */\n label: string\n /** One-line description shown next to the label in the picker. */\n description: string\n /**\n * Preset applied to `createAgent()` when this profile is active. Profiles\n * are self-contained: the host's chat-level `preset` (if any) is NOT\n * merged underneath. Hosts that want a shared base across profiles\n * should compose it explicitly in each profile's preset.\n */\n preset: Preset\n /** Theme token used by the picker / footer badge. Defaults to `'accent'`. */\n accent?: AgentAccent\n /**\n * A boolean setting that must be on for this profile to exist. While it is\n * off the profile is left out of every picker, and a session saved on it\n * builds as the default profile instead (see {@link availableAgents}).\n */\n requiresSetting?: keyof Settings\n /**\n * Profiles with the same family share one agent: its tools are every\n * member's tools, its prompt is the first member's (in registry order).\n * Switching between members keeps the agent and the prompt cache; a gate\n * refuses the tools the active member doesn't have, and a note tells the\n * model about the switch. Without a family the profile composes alone.\n */\n family?: string\n /**\n * What the model is told when the session switches to this profile without\n * rebuilding its agent. Default: the profile's label, description and\n * system prompt, then the tools that are off. On the profile a composition\n * is built from, it is what the model is told when the session comes back\n * to it (default: its instructions and tools apply again).\n */\n switchNote?: string | ((ctx: ProfileSwitchNoteContext) => string)\n}\n\n/** What a {@link AgentProfile.switchNote} function is given. */\nexport interface ProfileSwitchNoteContext {\n /** Names of the profile's own tools. Tools every profile gets (interaction, skills, MCP, …) are not listed. */\n tools: readonly string[]\n /** Interactive prompts (`present_plan`, `ask_user`) are on. */\n allowInteraction: boolean\n}\n\n/** Agent profiles keyed by profile id. */\nexport type AgentRegistry = Readonly<Record<string, AgentProfile>>\n\n/**\n * The profiles available under `settings`: those without a\n * {@link AgentProfile.requiresSetting}, or whose setting is on. Returns the\n * same registry object when nothing is filtered out.\n *\n * @param registry - The full agent registry.\n * @param settings - Current settings, checked against each profile's `requiresSetting`.\n */\nexport function availableAgents(registry: AgentRegistry, settings: Partial<Settings>): AgentRegistry {\n const hidden = Object.values(registry).filter(profile => profile.requiresSetting !== undefined && settings[profile.requiresSetting] !== true)\n if (hidden.length === 0)\n return registry\n return Object.fromEntries(Object.entries(registry).filter(([, profile]) => !hidden.includes(profile)))\n}\n\n/**\n * What a session's agent is built with: tools and prompt. `key` names it\n * (`family:<name>` or `profile:<id>`) and keys the session's frozen prompt.\n */\nexport interface AgentComposition {\n /** `family:<name>` for a family, `profile:<id>` for a profile without one. */\n key: string\n /** The profile whose prompt, behavior and hooks the agent is built from. */\n base: AgentProfile\n /** Every profile in the composition, in registry order, `base` first. */\n members: readonly AgentProfile[]\n}\n\n/**\n * A profile's own composition: its family (every registered member, in\n * registry order) or the profile alone.\n *\n * @param registry - The profiles available to the session.\n * @param profile - The profile to compose.\n */\nexport function profileComposition(registry: AgentRegistry, profile: AgentProfile): AgentComposition {\n if (profile.family === undefined)\n return { key: `profile:${profile.id}`, base: profile, members: [profile] }\n const members = Object.values(registry).filter(member => member.family === profile.family)\n if (!members.includes(profile))\n members.push(profile)\n return { key: `family:${profile.family}`, base: members[0]!, members }\n}\n\n/**\n * The composition a key names in `registry`, `undefined` when none of its\n * profiles is registered any more.\n *\n * @param registry - The profiles available to the session.\n * @param key - A composition key (`family:<name>` or `profile:<id>`).\n */\nexport function compositionByKey(registry: AgentRegistry, key: string): AgentComposition | undefined {\n if (key.startsWith('profile:')) {\n const profile = registry[key.slice('profile:'.length)]\n return profile && profile.family === undefined ? profileComposition(registry, profile) : undefined\n }\n if (key.startsWith('family:')) {\n const family = key.slice('family:'.length)\n const member = Object.values(registry).find(profile => profile.family === family)\n return member ? profileComposition(registry, member) : undefined\n }\n return undefined\n}\n\n/**\n * A profile's own tools by name. Tools the host gives every profile\n * (interaction, skills, MCP, …) are not part of it.\n *\n * @param profile - The profile.\n */\nexport function profileTools(profile: AgentProfile): ReadonlyMap<string, ToolDef> {\n const tools = new Map<string, ToolDef>()\n for (const tool of Object.values(profile.preset.tools ?? {})) {\n if (tool && !tools.has(tool.spec.name))\n tools.set(tool.spec.name, tool)\n }\n return tools\n}\n\n/**\n * The profile tools a composition's agent can call, by name: its base's,\n * then each other member's that the base doesn't have.\n *\n * @param composition - The composition.\n */\nexport function compositionTools(composition: AgentComposition): ReadonlyMap<string, ToolDef> {\n const tools = new Map(profileTools(composition.base))\n for (const member of composition.members) {\n for (const [name, tool] of profileTools(member)) {\n if (!tools.has(name))\n tools.set(name, tool)\n }\n }\n return tools\n}\n\nfunction sameTool(a: ToolDef, b: ToolDef): boolean {\n return a === b || JSON.stringify(a.spec) === JSON.stringify(b.spec)\n}\n\n/**\n * Whether `profile` runs on `composition` without a new agent: it is the\n * profile's own composition, or the profile's tools are a strict subset of\n * the composition's (same names and specs). Codemode scripts reach the same\n * tools, so a composition's tools count whether they are on the wire or not.\n *\n * @param registry - The profiles available to the session.\n * @param composition - The composition the session's agent is built with.\n * @param profile - The profile the session switches to.\n */\nexport function compositionFits(registry: AgentRegistry, composition: AgentComposition, profile: AgentProfile): boolean {\n if (profileComposition(registry, profile).key === composition.key)\n return true\n const available = compositionTools(composition)\n const own = profileTools(profile)\n if (own.size >= available.size)\n return false\n for (const [name, tool] of own) {\n const match = available.get(name)\n if (!match || !sameTool(match, tool))\n return false\n }\n return true\n}\n\n/**\n * The composition a session on `profileId` builds with: the one it ran on\n * (`currentKey`) while the profile fits it, else the profile's own.\n * `undefined` when `profileId` is not registered.\n *\n * @param registry - The profiles available to the session.\n * @param profileId - The session's profile.\n * @param currentKey - The key of the composition the session ran on, if any.\n */\nexport function sessionComposition(registry: AgentRegistry, profileId: string, currentKey: string | undefined): AgentComposition | undefined {\n const profile = registry[profileId]\n if (!profile)\n return undefined\n const current = currentKey === undefined ? undefined : compositionByKey(registry, currentKey)\n return current && compositionFits(registry, current, profile) ? current : profileComposition(registry, profile)\n}\n\n/** The built-in prompts the chat host composes (with env, AGENTS.md and SYSTEM.md). */\nexport type BuiltInPrompt = 'build' | 'plan'\n\nconst BUILT_IN_PROMPTS = new Map<string, BuiltInPrompt>([\n [buildBuildSystem(), 'build'],\n [buildPlanSystem(), 'plan'],\n])\n\n/**\n * The built-in prompt a profile runs, `undefined` for a profile with its own.\n * Decided by the preset's prompt, never the profile's id: a preset that uses\n * Build's or Plan's built-in prompt (`BUILD_AGENT.preset.system`) gets it\n * composed with the session's environment, AGENTS.md and SYSTEM.md; a host\n * profile that happens to be called `build` keeps its own prompt.\n *\n * @param profile - The profile.\n */\nexport function builtInPromptOf(profile: AgentProfile): BuiltInPrompt | undefined {\n return profile.preset.system === undefined ? undefined : BUILT_IN_PROMPTS.get(profile.preset.system)\n}\n\n/** Read-only tool slice shared by the Plan profile and any host-built read-only variant. */\nexport const READ_ONLY_TOOLS = { readFile, listFiles, glob, grep, webSearch, fetchUrl }\n\n/**\n * Build-mode tool slice — read + write + shell. Each Build profile adds its\n * own `spawn` tool.\n *\n * `shellKill` / `waitTask` ride along with `shell`: the shell tool's\n * description (and the `<task-notification>` stall hint in the agent\n * loop) instruct the model to call `shell_kill` / `wait_task` for\n * background tasks, so omitting them produces tool-not-found loops the\n * moment a `run_in_background` task needs terminating or blocking on.\n */\nconst BUILD_TOOLS = { shell, shellKill, waitTask, readFile, writeFile, listFiles, edit, multiEdit, glob, grep, webSearch, fetchUrl }\n\n/**\n * Tools the chat layer never persists to disk, regardless of output size.\n *\n * Rationale per entry:\n * - `read_file` — our reader is already paginated (`offset` + `limit`).\n * Persisting the slice the model asked for and pointing it at the\n * persisted path is circular; if the model wants more, it pages.\n * - `tool_search` — short metadata envelope. Never large.\n * - `skills_use` / `skills_read` — the `<skill_content>` body is\n * intentionally part of the prompt; persisting it defeats the purpose.\n * - `read_chat_turns` — previous-chat excerpts are explicitly loaded\n * conversation context; persisting them defeats the purpose.\n * - `present_plan` / `ask_user` — short JSON envelopes (decision +\n * answers). Never large.\n * - `spawn` — the subagent's final report IS the value of the call;\n * stubbing it out would defeat the orchestration pattern.\n * - `codemode` — a script prints what the model asked for, as a read\n * does, and caps its own output (`max_output_tokens`: start and end\n * inline, the full text in a file). Persisting it would force a re-read.\n *\n * Exported so SDK consumers building custom profiles can extend the list\n * (e.g. host-defined `screenshot` tool whose base64 payload should stay\n * inline). Set `behavior.persistExcludeTools` to your own list to fully\n * override.\n */\nexport const DEFAULT_PERSIST_EXCLUDE_TOOLS: readonly string[] = [\n 'read_file',\n 'tool_search',\n 'skills_use',\n 'skills_read',\n 'read_chat_turns',\n 'present_plan',\n 'ask_user',\n 'spawn',\n 'codemode',\n // Todos are short JSON envelopes (≤100 items) and the latest snapshot\n // is the only state of interest — persisting them to disk is circular.\n 'todowrite',\n 'todoread',\n]\n\n/**\n * Tools whose output is exempt from the per-turn `toolOutputBudget` accounting,\n * for SDK consumers that turn the budget on. The built-in profiles leave it\n * off.\n *\n * These tools exist to LOAD context into the conversation — their text output\n * IS the value of the call. Counting it against the budget steers the model\n * away from the very tool it needs to make progress (e.g. a 78 KiB\n * `tool_search` response for a 20-tool Notion MCP server would trigger the\n * \"summarize before calling more tools\" nudge on the very turn that\n * unlocked those tools, making them effectively unusable).\n *\n * Note this list is intentionally narrower than {@link DEFAULT_PERSIST_EXCLUDE_TOOLS}:\n * `read_file` is NOT exempt — a 200 KiB file load is exactly the case the\n * budget should steer against. Same for `spawn` (a sprawling subagent\n * report should still nudge a consolidation pass).\n *\n * Exported so SDK consumers building custom profiles can extend the list\n * for host-defined context-loading tools.\n */\nexport const DEFAULT_BUDGET_EXCLUDE_TOOLS: readonly string[] = [\n 'tool_search',\n 'skills_use',\n 'skills_read',\n 'read_chat_turns',\n]\n\n/**\n * Stable JSON of a tool input with keys sorted, used as a dedup cache key for\n * the read-only discovery tools. Covers EVERY input field, so `grep -i` never\n * collides with `grep` and a widened `limit` busts the cache. Empty objects\n * opt out (returns `undefined`) — a zero-arg `list_files` of cwd is cheap and\n * a stale replay there is more surprising than re-running it.\n */\nfunction discoveryDedupHash(input: Record<string, unknown>): string | undefined {\n const keys = Object.keys(input)\n if (keys.length === 0)\n return undefined\n keys.sort()\n const norm: Record<string, unknown> = {}\n for (const k of keys)\n norm[k] = input[k]\n return JSON.stringify(norm)\n}\n\n/**\n * Token-saving `AgentBehavior` defaults shared by the built-in profiles.\n *\n * These flip on at the chat layer, not at the SDK core (`src/agent.ts`) —\n * external `createAgent()` consumers keep the conservative defaults.\n * Rationale per knob:\n *\n * - `compactStrategy: 'tail'` — when the persisted history's tool-output\n * bytes exceed `compactThreshold`, stub older `tool_result` blocks at\n * the wire level (session storage is untouched). Without this, a single\n * long-lived chat keeps re-sending every read/grep result every turn.\n * - `elideStaleReads: false` — kept off; see the knob's comment below.\n * - `toolDisclosure: 'lazy'` — MCP tools advertise as name+description in\n * the system prompt and load their `inputSchema` on demand via\n * `tool_search`. No-op without MCPs (gated on `disclosure.lazyEntries.length > 0`\n * in `src/agent-tool-disclosure.ts`); substantial savings with a 50-tool server.\n * - No `toolOutputBudget`. Its nudge fires after the bytes are already in\n * context, so it removed nothing and cost a summarizing turn; over 145 real\n * hits the trigger was ordinary reads of one to three files. Size is bounded\n * per call instead (`readFileDefaults`, `persistThreshold`), and the\n * conversation by compaction.\n * - `readFileDefaults.maxBytes: 32 KiB` — default text cap for a `read_file`\n * that doesn't pass `maxBytes`; the cut read ends with a continue-at-offset\n * footer, and an explicit `maxBytes` still wins. Measured on 38.5k default\n * reads: 536 were over 32 KiB, and 4 of those (0.75%) were followed by an\n * edit of the same file within 3 turns.\n * - `persistThreshold: 8 KiB` + `persistExcludeTools` — tool results\n * above the threshold are written to disk and replaced inline with a\n * `<persisted-output>` stub (preview + path). The chat host wires\n * `persistDir` when it builds the agent; without a dir, persistence is off.\n * The user can disable persistence entirely via\n * `Settings.persistToolResults` (sets the threshold to 0 at agent build\n * time).\n */\nconst SHARED_BEHAVIOR = {\n compactStrategy: 'tail',\n compactThreshold: 128 * 1024,\n // Scale the byte threshold with the picked model's context window: the\n // effective threshold is max(128 KiB, 0.25 × window × 4 B/tok). Each tail\n // compaction frontier step rewrites an early message and re-bills the entire\n // suffix at the cache-write rate — measured in production as recurring\n // ~117k-token `messagePrefix` cache breaks on a 157k-token Fable session,\n // each ~50x the cost of keeping those bytes as cache reads. At 0.25 a 1M\n // window compacts from ~1 MiB of tool output, 200k from ~200 KiB, and\n // ≤128k models stay on the 128 KiB byte threshold. Long 1M sessions DO\n // reach it: over 12 days of real sessions the frontier sat ~6% into the\n // history and advanced a chunk about every 4 tool round trips — 156 steps\n // re-reading ~738k tokens each, ~60% of all re-read tokens.\n compactThresholdWindowFraction: 0.25,\n // Hysteresis: once kept tool output crosses the threshold, trim to half of\n // it in one step and wait for the next crossing, so steps are ~0.5 ×\n // threshold of new tool output apart instead of every few turns.\n compactLowWaterFraction: 0.5,\n // No client tail compaction on Anthropic ≥400k windows: each stub also\n // makes the API drop every later replayed thinking block (preserved-thinking\n // models), and autocompact stays the ceiling.\n compactSkip: { providers: ['anthropic'], minContextWindow: 400_000 },\n // Byte-budget eviction (see `applyTailCompaction`) keeps the largest recent\n // suffix of tool output that fits under `compactThreshold`, so for typical\n // small results the model retains far more than this many messages. This is\n // a FLOOR, not a cap: the trailing 8 messages (~4 tool round-trips) are\n // never elided, guaranteeing a freshly read file survives the\n // read → think → edit cadence even when individual results are large enough\n // that the byte budget alone would keep fewer. A floor of 4 was too tight:\n // a read got elided before the model could edit, forcing re-read loops.\n compactKeepTurns: 8,\n // Off. `'notice'` appended \"re-read before reasoning\" to nearly every\n // successful edit, almost always naming the file the model had just edited,\n // which made the edit tools noisier than an equivalent shell write. Edits\n // stay safe without it: `old_string` must resolve against current bytes, and\n // the mutating tools mark read-state `elided` so a re-read serves the full\n // body. `'stub'` would re-bill the prompt cache per newly-stale read (13\n // full-prefix invalidations on a 129-turn trace); see\n // `test-inference/cache-stability.inference.test.ts`.\n elideStaleReads: false,\n // Keep media inline; age it out only near the provider's hard request\n // limits. A fixed message window (`mediaKeepTurns`, 8 messages) rewrote an earlier\n // message every time a screenshot aged out, re-billing the whole\n // conversation: replayed on a 470k-token GUI session, it caused 158 full\n // prompt-cache resets, against ~1.6k tokens per screenshot to keep it as\n // a cache read. Aged media stays recoverable via\n // `read_file path=\"attachment://<hash>\"`.\n mediaBudget: DEFAULT_MEDIA_BUDGET,\n // Shrink Retina screenshots once, at entry (cache-stable), to 2000 px: a\n // request with more than 20 images is rejected if any one is larger, and\n // the inline budget routinely carries more than 20.\n maxImageEdge: DEFAULT_MAX_IMAGE_EDGE,\n // Loop-native auto-compaction: when a turn's effective input crosses 85% of\n // the model's context window, the loop summarizes the older turns mid-run\n // (real LLM summary + file/skill restoration) and continues. Compaction\n // happens inside the run (mid tool-loop AND end-of-run) with no double-fire.\n // The TUI/GUI inject the\n // picked model's `contextWindow` (and honor the user's enable/threshold\n // settings) per run; without a window source the trigger is inert.\n autoCompact: { threshold: 0.85 },\n toolDisclosure: 'lazy',\n readFileDefaults: { maxBytes: 32 * 1024 },\n persistThreshold: 8 * 1024,\n persistExcludeTools: DEFAULT_PERSIST_EXCLUDE_TOOLS,\n // `requireReadBeforeEdit` stays off (the library default). `edit`'s\n // `old_string` anchor already refuses edits against bytes that aren't on\n // disk, and the gate's \"shell reads don't register\" refusals taught models\n // to route edits through shell/python scripts to avoid it.\n requireReadBeforeEdit: false,\n // `shellToolNudges` — one factual line pointing at `read_file` / `grep` /\n // `edit` when a shell call does their job, at most twice per kind per\n // session. Rides the shell result itself (append-only), so it costs no\n // system-prompt bytes and appears only when the shell habit does.\n shellToolNudges: true,\n // `dedupTools` for the read-only discovery tools, in `replay` mode — an\n // identical re-issue replays the prior result from cache instead of\n // re-walking the filesystem. Free (no behavior change beyond cache replay)\n // and breaks the cheap-replay loop shape (model probing the same\n // `glob`/`grep`/`list_files` repeatedly). The hasher covers all input\n // fields so flag/limit changes bust the cache.\n dedupTools: {\n glob: discoveryDedupHash,\n grep: discoveryDedupHash,\n list_files: discoveryDedupHash,\n },\n // `repeatGuard` — identical-call detector. Defaults track the built-in\n // `shell` and `read_file` tools. Shell keeps the generic 4/8 block/abort\n // thresholds; `read_file` uses 6/12 so normal scaffolding re-visits don't\n // trip as quickly. The abort is the only layer that stops a model which\n // ignores the `Blocked:` reason and keeps retrying the same payload.\n //\n // `mode: 'both'` because strict chain counting is trivially defeated: ANY\n // intervening call resets it, and a spinning model always emits some. A real\n // run spent 123 turns and 10M input tokens re-issuing ONE byte-identical\n // `read_file` 59 times, alternating it with `grep`s on the same path; the\n // longest consecutive streak was 5 against a threshold of 6, so nothing ever\n // fired and the user had to cancel. The interleaved calls were `dedupTools`\n // cache replays, i.e. free for the model and (by design) chain boundaries\n // here — the guard was structurally unreachable. The sliding window counts\n // the same payload among that tool's own recent calls, so dilution slows the\n // trip instead of erasing it: the same run aborts at call 28 of 123.\n repeatGuard: { mode: 'both', windowSize: 40 },\n // `thinkingDecay` — after turn 10 the model has converged or is thrashing;\n // either way extended thinking earns less per token. STEP form (no\n // `factor`), not geometric: `thinking.budget_tokens` is part of Anthropic's\n // prompt-cache key, so every distinct budget a schedule emits is a FULL\n // cache re-bill — `cacheRead: 0`, scaffold included (measured; pinned in\n // `test-inference/cache-stability.inference.test.ts`). Geometric\n // halving (8192 → 4096 → 2048 → 1024) would cost three of those per run; one step\n // straight to the floor costs exactly one. No-op unless a thinking budget\n // is set.\n thinkingDecay: { afterTurn: 10, floor: 1024 },\n // One-hour prompt cache. These profiles only back interactive hosts, where\n // a human pause past five minutes would otherwise re-bill the whole\n // conversation at the cache-write rate (measured: ~80 such resets in one\n // long session). 1h writes cost 2x base input against 1.25x, which the\n // avoided resets repay. Verified live in\n // `test-inference/cache-stability.inference.test.ts` § extended TTL.\n // OAuth defaults to 1h; this extends it to API-key auth. Bedrock\n // degrades to the default TTL.\n cacheTtl: '1h',\n} as const\n\n/**\n * Build agent — the default profile. Full read/write/shell access plus\n * subagent spawning. Its tools are those of the `basic` preset plus `glob`,\n * `grep`, `web_search` and `fetch_url`.\n *\n * The system prompt is the cwd-less variant of {@link buildBuildSystem};\n * the TUI overrides it per-session with an env-anchored composition so\n * the prompt carries the current project root + date. SDK consumers\n * calling `createAgent({ ...BUILD_AGENT.preset })` directly get the\n * cwd-less prompt and can wrap their own env section.\n */\nexport const BUILD_AGENT: AgentProfile = {\n id: 'build',\n label: 'Build',\n description: 'full tool access: read, write, edit, shell, web search, and spawn subagents',\n accent: 'accent',\n // Shares an agent with Plan: a switch between them keeps the prompt cache.\n family: 'build',\n // `composePresets` layers `createTodoTools()` on top so the model gets\n // `todowrite` / `todoread` plus their per-run write budget. Top-level\n // runs share a persistent session slot (continuity across prompts);\n // subagent runs get their own isolated slot. Auto-clears when every\n // item is `completed`, so a fresh prompt after a successful batch\n // starts clean. See `src/chat/todos.ts` for the keying / hygiene\n // contract.\n //\n // The composition is per-tool-name deep-merge, so a downstream host\n // re-composing this profile with its own per-tool overrides gets the\n // expected last-wins-on-collision behavior.\n //\n // Plan mode deliberately omits todos — a read-only exploration session\n // has nothing to checkpoint.\n preset: composePresets(\n definePreset({\n name: 'build',\n system: buildBuildSystem(),\n behavior: { ...SHARED_BEHAVIOR },\n // `persist: true` shares the parent's session with every child agent so a\n // reloaded TUI can reconstruct the subagent tree from `session.turns`.\n tools: { ...BUILD_TOOLS, spawn: createSpawnTool({ persist: true }) },\n }),\n createTodoTools(),\n ),\n}\n\n/**\n * Codemode — Build's tools, reached through scripts. `codemode` in\n * `'only'` mode: every native tool a script can call leaves the wire and is\n * declared inside the `codemode` description, so the model works in scripts\n * by default. `spawn` is callable from scripts, so it leaves the wire too.\n * Tools scripts cannot call stay declared. Shown only while the Codemode\n * setting is on.\n */\nexport const BUILD_CODEMODE_AGENT: AgentProfile = {\n id: 'build-codemode',\n label: 'Codemode',\n description: 'Build, but the model works through scripts that call the tools in a sandbox',\n accent: 'accent',\n requiresSetting: 'codemode',\n preset: composePresets(\n definePreset({\n name: 'build-codemode',\n system: buildBuildSystem(),\n behavior: { ...SHARED_BEHAVIOR, codemode: { mode: 'only' } },\n tools: { ...BUILD_TOOLS, spawn: createSpawnTool({ persist: true }) },\n }),\n createTodoTools(),\n ),\n}\n\n/**\n * Plan's rules as a switch note: read-only, investigate, then propose. A\n * condensed form of its prompt (`buildPlanSystem`), for a session whose\n * agent was built with another prompt (Build's, in their family).\n */\nfunction planSwitchNote({ tools, allowInteraction }: ProfileSwitchNoteContext): string {\n const everyProfile = allowInteraction ? '`present_plan`, `ask_user`, skills, MCP tools' : 'skills, MCP tools'\n const steps = allowInteraction\n ? `1. Explore: read the relevant code, find existing functions and patterns to reuse. Check current docs via web search when the plan depends on external libraries.\n2. Ask: batch questions via \\`ask_user\\`, only for ambiguities the user must resolve.\n3. Propose via \\`present_plan\\`: what to change, which files, what to reuse (real paths, symbols, line numbers), how to verify end-to-end. Recommend one approach, not every alternative.\n\n- Every plan is delivered via \\`present_plan\\`, whatever its size; prose is not a deliverable.\n- \\`ask_user\\` when the answer changes the plan; batch related questions into one call.\n- Don't ask what the code can tell you.`\n : `1. Explore: read the relevant code, find existing functions and patterns to reuse. Check current docs via web search when the plan depends on external libraries.\n2. Propose: write the plan in your final response: what to change, which files, what to reuse (real paths, symbols, line numbers), how to verify end-to-end. Recommend one approach, not every alternative. Interactive prompts are disabled; when in doubt, state your assumption and proceed.`\n return `# Plan mode (read-only)\nYou are now in PLAN mode: investigate and design a proposal. You CANNOT modify files, run commands, or spawn subagents until the user switches you out of Plan mode. Allowed tools: ${tools.map(name => `\\`${name}\\``).join(', ')}, plus the tools every mode has (${everyProfile}). Any other call is refused.\n\n${steps}\n- Search first (\\`grep\\` / \\`glob\\`), then \\`read_file\\` the specific file. Don't re-read files or re-run searches whose output is still visible above.\n\nThe user switches to Build mode to execute ${allowInteraction ? 'an approved' : 'the'} plan.`\n}\n\n/**\n * Plan agent — read-only exploration mode. Locked down to file reading and\n * codebase search; no shell, no edits, no spawning. The system prompt\n * frames the conversation as planning rather than execution so the model\n * outputs proposals instead of attempting mutations.\n *\n * In Build's family: a session switching between them keeps Build's agent,\n * a gate enforces Plan's tools and its rules arrive as a note.\n */\nexport const PLAN_AGENT: AgentProfile = {\n id: 'plan',\n label: 'Plan',\n description: 'read-only: explore, analyze, search the web, and propose without modifying anything',\n accent: 'model',\n family: 'build',\n switchNote: planSwitchNote,\n preset: definePreset({\n name: 'plan',\n system: buildPlanSystem(),\n behavior: { ...SHARED_BEHAVIOR },\n tools: READ_ONLY_TOOLS,\n }),\n}\n\n/**\n * Default registry of the built-in profiles. Insertion order = picker order.\n * Hosts that want only one profile can pass `{ agents: { build: BUILD_AGENT } }`,\n * or a fully custom registry of their own profiles.\n */\nexport const BUILTIN_AGENTS: AgentRegistry = {\n 'build': BUILD_AGENT,\n 'build-codemode': BUILD_CODEMODE_AGENT,\n 'plan': PLAN_AGENT,\n}\n\n/** Id of the profile activated on first launch when nothing is persisted. */\nexport const DEFAULT_AGENT_ID = 'build'\n\n/**\n * Resolve an agent id against a registry with sensible fallbacks: the\n * requested id wins when present, then `defaultId`, then the first key.\n * Returns `null` when the registry is empty (host misconfiguration —\n * the caller should surface this rather than silently failing).\n *\n * @param registry - The agent registry to search.\n * @param requestedId - Preferred profile id, such as the persisted last agent.\n * @param defaultId - Fallback profile id used when the requested one is missing.\n */\nexport function resolveAgentId(\n registry: AgentRegistry,\n requestedId: string | undefined,\n defaultId: string | undefined,\n): string | null {\n if (requestedId && registry[requestedId])\n return requestedId\n if (defaultId && registry[defaultId])\n return defaultId\n const first = Object.keys(registry)[0]\n return first ?? null\n}\n\n/**\n * Wrap a single `Preset` into a single-profile registry. Used by\n * `resolveConfig` when the host passed `preset` without `agents`. The\n * registry has one entry, so the picker has only one option.\n *\n * @param preset - The preset to wrap. Its `name` becomes the profile label when set.\n */\nexport function singleAgentRegistry(preset: Preset): AgentRegistry {\n return {\n default: {\n id: 'default',\n label: typeof preset.name === 'string' && preset.name.length > 0 ? preset.name : 'Default',\n description: 'host-provided preset',\n preset,\n accent: 'accent',\n },\n }\n}\n","/**\n * Session lifecycle — the one way a session is created, forked, and deleted.\n *\n * Every surface (daemon, embedded GUI host, TUI local runtime, orchestration,\n * ACP) goes through these, so a field stamped at creation, carried by a fork,\n * or reclaimed on delete is decided HERE once instead of drifting per host.\n * Per-session state such as an environment lands in exactly these functions.\n *\n * Deliberately host-agnostic: nothing here knows about live agents. Stopping a\n * running agent before a delete is the host's job (`ChatHost.deleteSession`,\n * the TUI's teardown); this module owns the persisted data only.\n *\n * `createMainSession` layers the orchestration role on top; it is here rather\n * than in the host because the TUI's local runtime has no host and must still\n * be able to materialize the main (delivery of events does need the host; the\n * session itself does not).\n */\nimport type { Session, SessionData, SessionStore } from '../session/index'\nimport type { ProviderKey } from './auth'\nimport type { SessionEnvironment } from './environment'\nimport type { ModelFallbackTarget } from './model-chain-metadata'\nimport { randomUUID } from 'node:crypto'\nimport { basename } from 'node:path'\nimport { editSessionMetadata } from '../extension-authoring'\nimport { cleanupPersistedSession, rehomePersistedStubs, resolveLegacyPersistDir, resolveLegacySessionDir, resolvePersistDir, resolveSessionDir } from '../loop-persistence'\nimport { createSession } from '../session/index'\nimport { DEFAULT_AGENT_ID } from './agents'\nimport { ENVIRONMENT_METADATA_KEY, environmentMetadata, LOCAL_ENVIRONMENT, readSessionEnvironment } from './environment'\nimport { modelChainMetadata, SESSION_ACCOUNT_METADATA_KEY } from './model-chain-metadata'\nimport { MAIN_SESSION_TITLE, SESSION_ROLE_KEY, SIDE_OF_KEY } from './orchestration'\nimport { projectRootOf } from './project-root'\nimport { spaceMetadata } from './spaces'\nimport { truncateTurnsAt } from './turn-operations'\n\n// ---------------------------------------------------------------------------\n// Create\n// ---------------------------------------------------------------------------\n\n/** Fields stamped on a new session at creation. */\nexport interface CreateSessionInStoreParams {\n /**\n * Tool-execution directory; `projectRoot` is derived from it. `''` creates a\n * DIRECTORY-LESS session (the GUI's \"Chats\" bucket): no project, and\n * `resolveSessionCwd` anchors tool execution at the user's HOME at run time.\n */\n cwd: string\n /** Explicit id. Omitted → the store's generator, else a random UUID. */\n id?: string\n /** Initial session title. */\n title?: string\n /** Profile the session starts in. Defaults to `DEFAULT_AGENT_ID` (`build`). */\n agentId?: string\n /** Provider the session starts on. Applied together with `modelId`. */\n providerKey?: string\n /** Model the session starts on. Applied together with `providerKey`. */\n modelId?: string\n /** The stored account of `providerKey` to run on (credential id); omitted = its active account. */\n account?: string\n /** Fallback targets after the primary, stored as the session's model chain. */\n fallbacks?: readonly ModelFallbackTarget[]\n /** Mark as an extension-authoring session (auto-activates the docs skill). */\n extensionEdit?: boolean\n /** Mark as a skill-authoring session for the named skill (dir = `cwd`). */\n skillEdit?: { name: string }\n /**\n * Where the session's work happens (see `environment.ts`) — resolved by the\n * host (`initialEnvironment`), which also provisions it. Omitted or local +\n * ready → nothing is stamped: an absent environment IS the local one.\n */\n environment?: SessionEnvironment\n /**\n * The space the session belongs to (see `./spaces`). Omitted or Default →\n * nothing is stamped: an absent space id IS the Default space.\n */\n spaceId?: string\n /**\n * Make it a `/side` session of this parent (`metadata.sideOf`, role\n * `'side'`): hidden from lists, deleted with the parent.\n */\n sideOf?: string\n /**\n * Extra host-specific metadata (e.g. a pinned-skills seed). Applied FIRST,\n * so every canonical key stamped here wins on collision.\n */\n metadata?: Record<string, unknown>\n}\n\n/**\n * Build a new, UNSAVED session with every creation-time field stamped:\n * `projectRoot` (git root of `cwd`), `cwd`, `agentId`, `title`, the model and\n * its fallback chain, the authoring markers, and the environment.\n *\n * Unsaved on purpose: the TUI's local runtime creates the session when a chat\n * opens but only persists it on the first run, so opening and abandoning\n * `/new` never leaves an empty row behind. Everyone else wants\n * {@link createSessionInStore}.\n *\n * `store` may be `undefined` for an in-memory-only session (ACP without a\n * store): same stamping, no persistence.\n *\n * @param store - Store the session saves to later, or `undefined` for an in-memory session.\n * @param params - Creation fields for the session.\n */\nexport async function newSession(store: SessionStore | undefined, params: CreateSessionInStoreParams): Promise<Session> {\n const id = params.id ?? (await store?.generateSessionId?.()) ?? randomUUID()\n // The project the session BELONGS to (a linked worktree's main checkout),\n // not the checkout it runs in — that stays `metadata.cwd`. Directory-less\n // sessions carry NO project: `''` must not become a project root, or the\n // session lands in a bogus project group.\n const projectRoot = params.cwd ? projectRootOf(params.cwd) : undefined\n const session = await createSession({\n id,\n ...(store ? { store } : {}),\n ...(projectRoot ? { projectRoot } : {}),\n // Always stamped: the agent is a per-session mode, and a session without\n // one would otherwise inherit whatever profile the opening window shows.\n agentId: params.agentId ?? DEFAULT_AGENT_ID,\n ...(params.metadata ? { metadata: { ...params.metadata } } : {}),\n })\n session.setMeta('cwd', params.cwd)\n if (params.title)\n session.setMeta('title', params.title)\n if (params.providerKey && params.modelId) {\n session.setMeta('providerKey', params.providerKey)\n session.setMeta('modelId', params.modelId)\n if (params.account)\n session.setMeta(SESSION_ACCOUNT_METADATA_KEY, params.account)\n const chain = modelChainMetadata(\n { providerKey: params.providerKey as ProviderKey, modelId: params.modelId, ...(params.account ? { account: params.account } : {}) },\n params.fallbacks,\n )\n if (chain)\n session.setMeta('modelChain', chain)\n }\n // Authoring markers target the session's own dir; a directory-less\n // session has nothing to author.\n if (params.cwd && params.extensionEdit)\n setMetaEntries(session, editSessionMetadata({ kind: 'extension', name: basename(params.cwd), dir: params.cwd }))\n if (params.cwd && params.skillEdit)\n setMetaEntries(session, editSessionMetadata({ kind: 'skill', name: params.skillEdit.name, dir: params.cwd }))\n const environment = params.environment\n if (environment && !(environment.kind === LOCAL_ENVIRONMENT.kind && environment.state === 'ready'))\n setMetaEntries(session, environmentMetadata(environment))\n setMetaEntries(session, spaceMetadata(params.spaceId))\n if (params.sideOf) {\n session.setMeta(SIDE_OF_KEY, params.sideOf)\n session.setMeta(SESSION_ROLE_KEY, 'side')\n }\n return session\n}\n\n/**\n * Create and persist a session; returns its id.\n *\n * @param store - Store to persist the session in.\n * @param params - Creation fields for the session.\n */\nexport async function createSessionInStore(store: SessionStore, params: CreateSessionInStoreParams): Promise<string> {\n const session = await newSession(store, params)\n await session.save()\n return session.id\n}\n\n/**\n * Create the orchestration main session; returns its id.\n *\n * Directory-less on purpose. The main is GLOBAL — one per store, orchestrating\n * sessions in many directories — so binding it to whichever cwd happened to be\n * active when the toggle flipped put it in that project's group and hid it\n * from every other project's list. With no cwd it sits in the \"Chats\" bucket\n * and its own tools run from HOME, which is the only directory that is always\n * the user's and never a project's.\n *\n * @param store - Store to persist the main session in.\n */\nexport async function createMainSession(store: SessionStore): Promise<string> {\n const session = await newSession(store, { cwd: '', title: MAIN_SESSION_TITLE })\n session.setMeta(SESSION_ROLE_KEY, 'main')\n await session.save()\n return session.id\n}\n\n// ---------------------------------------------------------------------------\n// Fork\n// ---------------------------------------------------------------------------\n\n/**\n * Metadata a fork inherits from its parent: where it runs, the model\n * surface it runs on, and its space. Everything else (todos, archive flag, orchestration\n * role, artifacts…) belongs to the parent's history, not the new branch.\n * The environment is inherited too, but SHARED (see `forkSession`).\n */\nconst FORK_INHERITED_METADATA_KEYS = ['cwd', 'providerKey', 'modelId', SESSION_ACCOUNT_METADATA_KEY, 'modelChain', 'effort', 'modelOptions', 'spaceId'] as const\n\n/** What a fork reads from its parent — satisfied by `SessionData` and a live `Session`. */\nexport type ForkSource = Pick<SessionData, 'id' | 'turns' | 'runs' | 'metadata' | 'agentId' | 'projectRoot'>\n\n/** Inputs to {@link forkSession}. */\nexport interface ForkSessionParams {\n /**\n * Parent session — an id to load from `store`, or the parent itself (a\n * host holding the live `Session` passes it so unsaved turns fork too).\n */\n source: ForkSource | string\n /** Last turn the fork keeps (inclusive); the prefix is made protocol-clean. */\n turnId: string\n /**\n * Root of the per-session artifact tree (`<userDir>/sessions/<id>/`) that\n * the parent's persisted-output stubs point into. The fork gets its own\n * copies so deleting the parent can't strand the fork's content.\n */\n userDir: string\n /** Title override. Default: the parent's explicit title, if any. */\n title?: string\n}\n\n/** Outcome of {@link forkSession}: the new session id, or the reason the fork was refused. */\nexport type ForkSessionResult\n = | { ok: true, id: string }\n | { ok: false, reason: 'source-not-found' | 'turn-not-found' | 'environment-not-ready' }\n\n/**\n * The error a user sees when a fork is refused, the same on every surface.\n *\n * @param reason - The refusal reason from {@link forkSession}.\n * @param sessionId - Id of the session that was to be forked.\n * @param turnId - Id of the requested last turn.\n */\nexport function describeForkFailure(reason: Extract<ForkSessionResult, { ok: false }>['reason'], sessionId: string, turnId: string): string {\n switch (reason) {\n case 'source-not-found':\n return `Session \"${sessionId}\" not found.`\n case 'environment-not-ready':\n return `Session \"${sessionId}\" can't be forked until its environment is ready.`\n case 'turn-not-found':\n return `Turn \"${turnId}\" not found in session \"${sessionId}\".`\n }\n}\n\n/**\n * Create and persist a new session whose history is the parent's turns up to\n * and including `turnId`. The parent is untouched.\n *\n * Carries: the protocol-clean turn prefix, the run records those turns\n * reference (so run stats survive and the next run can't re-mint `run_1`\n * over an existing reference), persisted-output blobs (copied and re-pointed),\n * `projectRoot` (a fork belongs to its parent's project, wherever it was\n * forked from), `agentId`, {@link FORK_INHERITED_METADATA_KEYS}, and the\n * parent's environment — marked not-owned: the fork runs in the same\n * worktree / sandbox but deleting it never tears that down. Refused while\n * the parent's environment isn't `ready`: nothing would provision the fork's\n * copy, and provisioning it would mint an environment nobody owns.\n *\n * @param store - Store to load the parent from and persist the fork in.\n * @param params - Parent, last turn to keep, artifact root and optional title.\n */\nexport async function forkSession(store: SessionStore, params: ForkSessionParams): Promise<ForkSessionResult> {\n const source = typeof params.source === 'string' ? await store.load(params.source) : params.source\n if (!source)\n return { ok: false, reason: 'source-not-found' }\n const turns = truncateTurnsAt(source.turns, params.turnId, { inclusive: true })\n if (!turns || turns.length === 0)\n return { ok: false, reason: 'turn-not-found' }\n\n if (source.metadata[ENVIRONMENT_METADATA_KEY] !== undefined && readSessionEnvironment(source.metadata).state !== 'ready')\n return { ok: false, reason: 'environment-not-ready' }\n\n const referencedRunIds = new Set(turns.flatMap(turn => turn.runId ? [turn.runId] : []))\n // Shallow-clone so the fork's later mutations never bleed into the parent.\n const runs = source.runs.filter(run => referencedRunIds.has(run.id)).map(run => ({ ...run }))\n\n const inherited: Record<string, unknown> = {}\n for (const key of FORK_INHERITED_METADATA_KEYS) {\n if (source.metadata[key] !== undefined)\n inherited[key] = source.metadata[key]\n }\n if (source.metadata[ENVIRONMENT_METADATA_KEY] !== undefined)\n Object.assign(inherited, environmentMetadata({ ...readSessionEnvironment(source.metadata), owned: false }))\n const title = params.title ?? (typeof source.metadata.title === 'string' ? source.metadata.title : undefined)\n\n const fork = await createSession({\n id: (await store.generateSessionId?.()) ?? randomUUID(),\n store,\n agentId: source.agentId ?? DEFAULT_AGENT_ID,\n ...(source.projectRoot ? { projectRoot: source.projectRoot } : {}),\n metadata: { ...inherited, ...(title ? { title } : {}) },\n })\n\n /*\n * Persisted-output stubs embed the blob's absolute per-session path; left\n * as-is, the fork would lose that content when the parent is deleted (its\n * blob dir goes with it). Rehome both layouts: sessions from before the\n * session-first layout embed `<userDir>/tool-results/<id>/`, newer ones\n * `<userDir>/sessions/<id>/tool-results/`. The legacy pass matters most —\n * the parent's delete sweeps exactly that dir.\n */\n const forkPersistDir = resolvePersistDir({ userDir: params.userDir, sessionId: fork.id })\n let rehomed = await rehomePersistedStubs(turns, resolvePersistDir({ userDir: params.userDir, sessionId: source.id }), forkPersistDir)\n const legacyPersistDir = resolveLegacyPersistDir({ userDir: params.userDir, sessionId: source.id })\n if (legacyPersistDir !== undefined)\n rehomed = await rehomePersistedStubs(rehomed, legacyPersistDir, forkPersistDir)\n\n fork.setTurns(rehomed)\n fork.setRuns(runs)\n await fork.save()\n return { ok: true, id: fork.id }\n}\n\n// ---------------------------------------------------------------------------\n// Delete\n// ---------------------------------------------------------------------------\n\n/**\n * Delete a session's persisted data: the store row, then every on-disk\n * artifact it owns — the session-first tree (`<userDir>/sessions/<id>/`:\n * blobs, task logs, MCP warnings) plus the two pre-session-first locations\n * migration leaves behind. Blob cleanup is best-effort and never throws; it\n * runs AFTER the row delete so a cleanup failure can't leave a live row\n * pointing at removed blobs.\n *\n * Does NOT stop a live agent — hosts must retire it first (see\n * `ChatHost.deleteSession`), or its next save resurrects the row.\n *\n * @param store - Store holding the session row.\n * @param sessionId - Id of the session to delete.\n * @param opts - `userDir` is the root of the per-session artifact tree.\n */\nexport async function deleteSessionData(store: SessionStore, sessionId: string, opts: { userDir: string }): Promise<void> {\n await store.delete(sessionId)\n const dirs = [\n resolveSessionDir({ userDir: opts.userDir, sessionId }),\n resolveLegacyPersistDir({ userDir: opts.userDir, sessionId }),\n resolveLegacySessionDir({ userDir: opts.userDir, sessionId }),\n ]\n await Promise.all(dirs.flatMap(dir => dir === undefined ? [] : [cleanupPersistedSession(dir)]))\n}\n\nfunction setMetaEntries(session: Session, entries: Record<string, unknown>): void {\n for (const [key, value] of Object.entries(entries))\n session.setMeta(key, value)\n}\n","/** The values of the `durability` setting (`Settings.durability` in `zidane/chat`, `--durability` in the CLIs). */\n\n/**\n * Whether runs survive a crash: `on` keeps a checkpoint of each run and\n * resumes an interrupted one by itself, `resume-ask` keeps the checkpoint\n * and asks before resuming, `off` keeps none (an interrupted run's tool\n * calls are reported interrupted on the next run).\n */\nexport type DurabilityMode = 'on' | 'resume-ask' | 'off'\n\nconst MODES: ReadonlySet<string> = new Set<DurabilityMode>(['on', 'resume-ask', 'off'])\n\n/** Default durability: checkpoint, and ask before resuming. */\nexport const DEFAULT_DURABILITY: DurabilityMode = 'resume-ask'\n\nexport function isDurabilityMode(value: unknown): value is DurabilityMode {\n return typeof value === 'string' && MODES.has(value)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"setting-_jh2OtiL.js","names":[],"sources":["../src/chat/todos.ts","../src/chat/agents.ts","../src/chat/session-factory.ts","../src/durable/setting.ts"],"sourcesContent":["/**\n * Todos — `todowrite` / `todoread` tools for task checkpointing.\n *\n * The model uses these to plan multi-step work and stream progress as it\n * executes. The list is monolithic-replacement-by-design: every `todowrite`\n * overwrites the active list in full, mirroring Anthropic's reference\n * `TodoWrite` semantics.\n *\n * Persistence model — `session.metadata.todos: TodosBag`:\n *\n * ```ts\n * { session?: TodoItem[], byRun?: Record<runId, TodoItem[]> }\n * ```\n *\n * - Top-level runs (no `parentRunId`) **share** the `session` slot. A list\n * written in one prompt survives across the next prompt — the user\n * aborts a run with `in_progress` items, sends a follow-up, and the\n * model's `todoread` returns the same list to continue from. Matches\n * Claude Code's TodoWrite v1 keying (`agentId ?? sessionId`).\n * - Subagent runs (`parentRunId` set) get their own slot under `byRun`\n * keyed by the child's runId. Subagent state never bleeds into the\n * parent's session list — parallel children stay isolated from each\n * other and from the parent.\n * - When every item is `completed`, the slot is **auto-cleared** to `[]`\n * on write — prevents stale \"all done\" lists from shadowing the next\n * prompt's context. Same rationale as Claude Code's `TodoWriteTool.ts:70`.\n * - **Archive sidecar** (`bag.archive`): every non-empty write also\n * stashes the list under `archive.session` / `archive.byRun`. Empty\n * writes (auto-clear or explicit reset) preserve the archive. UI\n * surfaces fall back to it when the live slot is empty so the modal\n * keeps showing \"what was just completed\" — invisible to the model\n * (`todoread` strictly reads the live slot).\n * - Resume: the persisted `tool_result` block in `session.turns` carries the\n * latest snapshot. `todowrite` only mutates session memory (`setMeta`) and\n * the run loop does not save the bag, so a process that reloads the session\n * from the store can lack it (see {@link applyTodosSnapshot}).\n *\n * Policy is layered:\n * - Identical-payload dedup: handled **inside the tool body**, against the\n * current slot (session-shared for top-level, per-run for subagents).\n * Does NOT plumb through `behavior.dedupTools` — that cache is keyed\n * per session and would conflate top-level + subagent re-writes.\n * - Soft nudge (\"you've checkpointed N times, slow down\"): rides on the\n * tool's own `tool_result` string. The model sees the nudge in the\n * result it just received — no `system:transform` plumbing. Count is\n * scoped the same way the data is (session-shared for top-level,\n * per-run for subagents) so the nudge fires on cumulative over-use.\n * - Per-run write cap **(opt-in)**: available via\n * `createTodoTools({ maxWritesPerRun: N })`. Off by default —\n * `todowrite` is a state-transition tool, capping it punishes the\n * legitimate \"finish the N-item plan\" path (which needs ≥ N+1\n * calls). Hosts that explicitly want a hard ceiling can opt in.\n *\n * No OpenTUI imports — a GUI consumer reuses this module verbatim.\n */\nimport type { Preset } from '../presets'\nimport type { Session as StoredSession } from '../session'\nimport type { ToolContext, ToolDef } from '../tools/types'\nimport { definePreset } from '../presets'\n\nexport { TODO_STATUS_GLYPHS } from './todo-glyphs'\n\n/** Todos live in session metadata and are memory-only here: any session, store-less snapshots included. */\ntype Session = Omit<StoredSession, 'save'>\n\n// ---------------------------------------------------------------------------\n// Tool identities — canonical names persisted in `session.turns`. Stable\n// across the lifetime of the project; renaming would break resume.\n// ---------------------------------------------------------------------------\n\n/** Canonical name of the tool that replaces the todo list. */\nexport const TODOWRITE_TOOL = 'todowrite'\n/** Canonical name of the tool that reads the todo list. */\nexport const TODOREAD_TOOL = 'todoread'\n\n/**\n * True when `name` is one of the todo tool canonical names.\n *\n * @param name - Tool name to test.\n */\nexport function isTodoTool(name: string): boolean {\n return name === TODOWRITE_TOOL || name === TODOREAD_TOOL\n}\n\n// ---------------------------------------------------------------------------\n// Session metadata keys — exported so consumers (UI rehydrate, export\n// pipelines, fork helpers) read/write the same shape we do. Keep them\n// alongside the tool to make the contract obvious.\n// ---------------------------------------------------------------------------\n\n/** `session.metadata[TODOS_METADATA_KEY]: TodosBag` — see {@link TodosBag}. */\nexport const TODOS_METADATA_KEY = 'todos'\n/** `session.metadata[TODO_WRITE_COUNTS_METADATA_KEY]: CountsBag` — mirrors `TodosBag` shape. */\nexport const TODO_WRITE_COUNTS_METADATA_KEY = 'todoWriteCounts'\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Progress of one todo. */\nexport type TodoStatus = 'pending' | 'in_progress' | 'completed' | 'cancelled'\n\n/** One entry in the todo list. */\nexport interface TodoItem {\n /** Stable id — the model picks it. Reusing an id replaces that item. */\n id: string\n /** One-line task summary (Markdown allowed; renderers may sanitize). */\n content: string\n /** Current progress. */\n status: TodoStatus\n}\n\n/**\n * On-disk shape at `session.metadata[TODOS_METADATA_KEY]`.\n *\n * Sub-fields are absent (not empty) when they carry no data — keeps the\n * persisted metadata clean and lets `JSON.stringify` skip empty bags\n * entirely on session save.\n */\nexport interface TodosBag {\n /** Top-level slot — shared across runs whose `parentRunId` is unset. */\n session?: TodoItem[]\n /** Per-subagent-run slots, keyed by the child run's id. */\n byRun?: Record<string, TodoItem[]>\n /**\n * Most-recent non-empty list per slot — sidecar for UI surfaces, NOT\n * visible to `todoread`. Updated on every non-empty write; **preserved**\n * across the auto-clear path. Lets the modal keep showing \"what was just\n * completed\" after a fully-done batch wipes the live slot. Empty writes\n * never touch the archive — clearing the live list shouldn't erase the\n * audit trail.\n */\n archive?: {\n session?: TodoItem[]\n byRun?: Record<string, TodoItem[]>\n }\n}\n\n/** Mirror of {@link TodosBag} for the write-counter — same scope rules. */\ninterface CountsBag {\n session?: number\n byRun?: Record<string, number>\n}\n\nconst TODO_STATUS_VALUES: readonly TodoStatus[] = ['pending', 'in_progress', 'completed', 'cancelled']\n\n/**\n * Default ceiling on a single todo list. Exported so every write boundary caps\n * identically — a boundary with a different cap would let a list through that\n * the tool would have truncated.\n */\nexport const DEFAULT_MAX_TODO_ITEMS = 100\n\n/** Options for {@link createTodoTools}. */\nexport interface CreateTodoToolsOptions {\n /**\n * Maximum item count per `todowrite`. Excess items are truncated by the\n * validator and reported back to the model in the tool_result.\n *\n * Default: `100`. The default is generous; a planning agent that exceeds\n * 100 items per call is usually flailing.\n */\n maxItems?: number\n /**\n * Per-run cap on `todowrite` calls, plumbed into `behavior.toolBudgets`.\n * `onMaxWrites` chooses the reaction (steer / block) — see\n * {@link AgentBehavior.toolBudgets} for the contract.\n *\n * **Default: `0` (no cap).** `todowrite` is a state-transition tool —\n * a legitimate run of an N-item list needs at least `N + 1` calls\n * (initial plan + one transition per item) to complete, so a hard\n * cap punishes the legitimate path. The nudges that DO apply are the\n * `remindAfter` reminder (soft, in-band) and the identical-payload\n * dedup check (silent, structural). Hosts that want a true hard cap\n * can opt in by setting `maxWritesPerRun: N`.\n *\n * **Counting semantics** (when opted in): the budget counts every\n * dispatched call, including dedup substitutes (zidane spec). The\n * {@link remindAfter} counter counts execute-only dispatches —\n * dedup hits replay the prior result and never reach the tool body.\n * Use this budget (not the reminder) as the hard limit.\n */\n maxWritesPerRun?: number\n /**\n * Reaction when `maxWritesPerRun` is exceeded: `steer` nudges the model, `block` refuses the call.\n *\n * @default 'steer'\n */\n onMaxWrites?: 'steer' | 'block'\n /**\n * Hard stop for **consecutive identical** `todowrite` payloads — the\n * exact loop the in-body `dedupIdentical` check only *whispers* about\n * (\"No change — N items already tracked\"). Plumbs a run-scoped\n * `behavior.dedupTools.todowrite` entry in `block-after` mode: the\n * gate replays the unchanged result for the first `N - 1` identical\n * calls, then refuses the `N`th (and every subsequent identical one)\n * with a `Blocked: …` tool_result so the model breaks out instead of\n * re-checkpointing forever.\n *\n * This is safe to leave on by default — unlike {@link maxWritesPerRun}\n * it only ever blocks a payload byte-identical to the one already\n * tracked, so a legitimate \"finish the N-item plan\" run (which writes\n * a *different* list each transition) is never touched. The counter\n * resets the moment the model writes anything different.\n *\n * Run-scoped via `DedupToolConfig.scope: 'run'`, so a parent and a\n * subagent writing the same list never share the counter. A session-scoped\n * `behavior.dedupTools` entry would conflate them.\n *\n * Set to `0` to opt out (e.g. a host that installs its own loop\n * hardening). Default: `5`. Values `< 2` are clamped to `2` by the\n * dedup middleware (a threshold of 1 would block the first call).\n */\n maxIdenticalWrites?: number\n /**\n * Custom refusal text for the {@link maxIdenticalWrites} block. Receives\n * the 1-indexed identical-call count (including the blocked call). Leave\n * unset for the built-in \"you've written the same list N times in a\n * row\" message.\n */\n identicalBlockReason?: (count: number) => string\n /**\n * Append a \"you've checkpointed N times — only call this at significant\n * task transitions\" nudge to the tool_result once `todowrite` has fired\n * this many times in the current run. The nudge rides on the result\n * string, so the model sees it on its next turn without `system:transform`\n * plumbing.\n *\n * Counts only executed dispatches (dedup hits skip the tool body and so\n * skip the increment). For a hard cap that also covers dedup hits, use\n * {@link maxWritesPerRun}.\n *\n * Set to `0` to opt out. Default: `3`.\n */\n remindAfter?: number\n /**\n * Custom reminder text. Receives the post-call count and the items that\n * were just written. Return `''` (or `undefined`) to suppress the reminder\n * for this specific call. Default text nudges towards \"back to executing,\n * not re-planning.\"\n */\n reminderText?: (count: number, items: readonly TodoItem[]) => string | undefined\n /**\n * Short-circuit the tool body when the incoming payload is identical to\n * the **current slot** (session-shared for top-level runs, per-run for\n * subagents). The model gets a \"No change — N items already tracked.\"\n * acknowledgment instead of a fresh \"Updated N items.\" line.\n * Default: `true`.\n *\n * Read `session.metadata.todos` via {@link getTodosForRun} for the\n * comparison — so the resolution naturally follows the keying rule\n * (top-level reads the session slot, subagent reads its own). Does\n * NOT plumb through `behavior.dedupTools`: that cache lives at the\n * gate, is keyed per session, and would conflate top-level + subagent\n * re-writes of structurally identical payloads.\n *\n * Counter advances on identical re-writes regardless — the model\n * called the tool, that's a call. Lets the reminder threshold catch\n * spam. Set `false` to skip the check entirely (every call rewrites\n * the slot, useful when state-shape comparisons are pointless for\n * your usage).\n */\n dedupIdentical?: boolean\n /**\n * Override the JSON-schema-level descriptions surfaced to the model.\n * Useful for telemetry experiments. Leave unset to inherit the built-in\n * doctrine prose.\n */\n writeDescription?: string\n /** Replaces the `todoread` tool description shown to the model. */\n readDescription?: string\n}\n\n// ---------------------------------------------------------------------------\n// Built-in doctrine prose\n// ---------------------------------------------------------------------------\n\nconst WRITE_DESCRIPTION\n = 'Replace the active task list. Pass the full list of items every '\n + 'call; there is no partial update. The list persists across user '\n + 'prompts in the same session, so an aborted run with `in_progress` '\n + 'items reads back on the next `todoread` and you can pick up where '\n + 'you left off. When every item is `completed`, the list auto-clears '\n + 'so the next prompt starts fresh.\\n\\n'\n + 'Use the `status` field to track progress: `pending` (queued), '\n + '`in_progress` (currently working), `completed` (done), `cancelled` '\n + '(no longer relevant).\\n\\n'\n + 'Only checkpoint at significant transitions:\\n'\n + ' 1. On a multi-step task, or before a run that will change several '\n + 'files: write the plan with everything `pending` so the user sees it.\\n'\n + ' 2. When you transition between steps: mark the previous one '\n + '`completed` and the next one `in_progress`.\\n'\n + ' 3. When the user asks for the current status: re-emit the list '\n + 'unchanged so they can see it.\\n\\n'\n + 'Do NOT call this on every action. The list is for the user\\'s '\n + 'situational awareness, not for self-narrating.'\n\nconst READ_DESCRIPTION\n = 'Return the current active task list. Returns an empty list if nothing '\n + 'has been written yet, or if the previous batch was auto-cleared '\n + 'after all items completed. Use sparingly; you already see the '\n + 'latest list in your own `todowrite` tool_result above.'\n\nfunction defaultReminder(count: number): string {\n return `(You've called todowrite ${count} times. Make sure each `\n + `checkpoint reflects real progress; avoid re-planning every step.)`\n}\n\nfunction defaultIdenticalBlockReason(count: number): string {\n return `You've written the same todo list ${count} times in a row without `\n + `any change. Stop re-checkpointing: either do the next concrete `\n + `action (edit a file, run a command) and then write a list that `\n + `reflects real progress, or finish the task. Re-emitting the `\n + `identical list does nothing.`\n}\n\n/**\n * Stable fingerprint for a `todowrite` payload, used by the run-scoped\n * `block-after` dedup gate. Normalizes the incoming `{ todos }` the same\n * way the tool body does before {@link todosEqual} — sanitize to the\n * three known fields, last-wins id-dedup, order preserved — so the gate's\n * notion of \"identical\" lines up exactly with the body's \"unchanged\"\n * short-circuit. Returns `undefined` for shapes with no usable items so\n * the dedup middleware skips them (a malformed call shouldn't poison the\n * loop counter).\n */\nfunction hashTodoWriteInput(input: Record<string, unknown>): string | undefined {\n const raw = Array.isArray(input.todos) ? input.todos : []\n const items = sanitizeItems(raw, Number.MAX_SAFE_INTEGER)\n const byId = new Map<string, TodoItem>()\n for (const item of items)\n byId.set(item.id, item)\n const normalized = [...byId.values()]\n if (normalized.length === 0)\n return undefined\n return JSON.stringify(normalized.map(t => [t.id, t.status, t.content]))\n}\n\n// ---------------------------------------------------------------------------\n// Session metadata accessors — single source of truth for the `TodosBag`\n// shape. Tests, fork helpers, UI rehydrate, and the tool bodies all go\n// through these so the keying contract (top-level vs subagent) stays in\n// one place.\n// ---------------------------------------------------------------------------\n\n/**\n * `runId` when that run is a subagent (has a `parentRunId`), else `null`\n * (the session slot). Top-level runs, `null` (no run yet) and an unknown\n * run all map to the session slot: treating an orphan as top-level is the\n * right fallback since the session slot is the more general bucket.\n */\nfunction subagentRunId(session: Session, runId: string | null): string | null {\n if (runId === null)\n return null\n const run = session.runs.find(r => r.id === runId)\n return run?.parentRunId ? runId : null\n}\n\n/**\n * Read the active list for `runId`. Top-level runs see the session-shared\n * slot; subagent runs see their own. Returns a fresh `[]` (not stored in\n * metadata) when no slot exists — the caller can mutate the result\n * without affecting state.\n *\n * @param session - The session holding the todos.\n * @param runId - Run whose list to read; `null` before the first run (the session slot).\n */\nexport function getTodosForRun(session: Session, runId: string | null): TodoItem[] {\n const bag = readTodosBag(session)\n const subagent = subagentRunId(session, runId)\n if (subagent !== null) {\n const items = bag.byRun?.[subagent]\n return Array.isArray(items) ? [...items] : []\n }\n return Array.isArray(bag.session) ? [...bag.session] : []\n}\n\n/**\n * Mirror a todo snapshot from another process into this session's metadata.\n *\n * Needed because the run loop never persists the todos bag — `todowrite` calls\n * `setMeta`, which only mutates memory, and nothing saves it. That is harmless\n * in-process (the UI reads the very object the tool wrote) but fatal across a\n * process boundary: a window that reloads the session from the shared store\n * finds NO todos metadata and renders an empty list, even though the daemon's\n * agent has a full one.\n *\n * So the authoritative `todos:snapshot` signal is applied here instead of being\n * re-derived from disk. Writes BOTH slots: `todos` is what the model sees, and\n * `archive` is what the UI falls back to after an auto-clear — reconstructing\n * the archive from `todos` alone would blank the modal in exactly the\n * all-completed case the archive exists to cover.\n *\n * Memory-only by design (no `save()`): this is a local mirror of another\n * process's state, and persisting it would let a window's stale view overwrite\n * the owner's. Whoever owns the agent owns the durable copy.\n */\nexport function applyTodosSnapshot(\n session: Session,\n snapshot: { runId: string | null, todos: readonly TodoItem[], archive: readonly TodoItem[] },\n): void {\n const { runId } = snapshot\n // No run and nothing listed: an empty session's snapshot, nothing to mirror (and no list to wipe).\n if (!runId && snapshot.todos.length === 0 && snapshot.archive.length === 0)\n return\n const bag = readTodosBag(session)\n const subagent = subagentRunId(session, runId)\n const live = snapshot.todos.map(normalizeItem)\n const archived = snapshot.archive.map(normalizeItem)\n\n const assign = (\n target: { session?: TodoItem[], byRun?: Record<string, TodoItem[]> },\n items: TodoItem[],\n ): void => {\n if (subagent !== null) {\n const byRun = { ...(target.byRun ?? {}) }\n if (items.length === 0)\n delete byRun[subagent]\n else\n byRun[subagent] = items\n target.byRun = byRun\n return\n }\n if (items.length === 0)\n delete target.session\n else\n target.session = items\n }\n\n assign(bag, live)\n const archive = { ...(bag.archive ?? {}) }\n assign(archive, archived)\n bag.archive = archive\n writeTodosBag(session, bag)\n}\n\n/**\n * Replace the active list for `runId`. Routes to the session-shared slot\n * for top-level runs and to the per-run slot for subagents. Empty arrays\n * delete the slot entirely so persisted metadata stays clean.\n *\n * Side effect: a **non-empty** write also stashes the same list into\n * `bag.archive` (same routing). Empty writes never touch the archive —\n * clearing the live list (whether explicitly or via the tool's auto-\n * clear) preserves the \"last shown\" snapshot so UI surfaces can keep\n * rendering what was just completed. See {@link getArchivedTodosForRun}.\n *\n * `opts.clearArchive` opts OUT of that preservation, and exists because the\n * archive is only the right answer for the AUTO-CLEAR path. UI surfaces render\n * `todos.length > 0 ? todos : archive`, so a user who deletes every todo would\n * otherwise watch the archive fall straight back into view with the same items\n * — a destructive action they confirmed, with no visible effect, and the footer\n * chip still counting the list they just deleted. Pass it for user-initiated\n * writes; leave it off for the tool.\n *\n * @param session - The session to write into.\n * @param runId - Run whose list to replace; `null` before the first run writes the\n * session slot, which the first top-level run then reads as its own.\n * @param items - The new list. An empty list clears the live slot.\n * @param opts - Write options.\n * @param opts.clearArchive - Also drop the archived list when `items` is empty.\n */\nexport function setTodosForRun(\n session: Session,\n runId: string | null,\n items: readonly TodoItem[],\n opts: { clearArchive?: boolean } = {},\n): void {\n const bag = readTodosBag(session)\n const normalized = items.map(normalizeItem)\n const subagent = subagentRunId(session, runId)\n\n // Active slot.\n if (subagent) {\n const byRun = { ...(bag.byRun ?? {}) }\n if (normalized.length === 0)\n delete byRun[subagent]\n else\n byRun[subagent] = normalized\n bag.byRun = byRun\n }\n else if (normalized.length === 0) {\n delete bag.session\n }\n else {\n bag.session = normalized\n }\n\n // Archive — only on non-empty writes. Empty writes preserve the prior\n // archive so the modal keeps showing \"what was just done\" after the\n // live slot wipes.\n if (normalized.length > 0) {\n const archive = { ...(bag.archive ?? {}) }\n if (subagent) {\n const archiveByRun = { ...(archive.byRun ?? {}) }\n archiveByRun[subagent] = normalized\n archive.byRun = archiveByRun\n }\n else {\n archive.session = normalized\n }\n bag.archive = archive\n }\n else if (opts.clearArchive) {\n // Explicit user deletion — drop the snapshot too, or the UI's\n // `todos : archive` fallback would re-render exactly what was deleted.\n const archive = { ...(bag.archive ?? {}) }\n if (subagent) {\n const archiveByRun = { ...(archive.byRun ?? {}) }\n delete archiveByRun[subagent]\n archive.byRun = archiveByRun\n }\n else {\n delete archive.session\n }\n bag.archive = archive\n }\n\n writeTodosBag(session, bag)\n}\n\n/**\n * Read the archived (most-recent non-empty) list for `runId`. Same slot\n * routing as {@link getTodosForRun}, but reads `bag.archive` instead of\n * the live slot. Returns a fresh `[]` when no archive exists.\n *\n * UI surfaces use this to keep showing \"what was just completed\" after\n * the live list auto-clears. The model never sees the archive — it's a\n * read-only sidecar for human-facing rendering.\n *\n * @param session - The session holding the todos.\n * @param runId - Run whose archived list to read; `null` before the first run.\n */\nexport function getArchivedTodosForRun(session: Session, runId: string | null): TodoItem[] {\n const bag = readTodosBag(session)\n const archive = bag.archive\n if (!archive)\n return []\n const subagent = subagentRunId(session, runId)\n if (subagent !== null) {\n const items = archive.byRun?.[subagent]\n return Array.isArray(items) ? [...items] : []\n }\n return Array.isArray(archive.session) ? [...archive.session] : []\n}\n\n/**\n * Reconcile `session.metadata.todos.byRun` (live + archive) against\n * `session.runs`. Drops subagent slots whose runId isn't in the run\n * list. The top-level `session` slot is unaffected — it's session-\n * scoped, not per-run, so there's no orphan to GC.\n *\n * Useful after `session.setRuns()` (fork / restore) or to GC stale\n * metadata mutated by an external caller. Also prunes the parallel\n * counter bag so the two don't drift.\n *\n * `dropped` reports the live-byRun orphans only (archive-only orphans\n * are silently swept). The live half is the contract callers actually\n * observe.\n *\n * @param session - The session to reconcile.\n */\nexport function pruneTodosByRun(session: Session): { dropped: readonly string[] } {\n const validRunIds = new Set(session.runs.map(r => r.id))\n const dropped: string[] = []\n\n const bag = readTodosBag(session)\n let bagChanged = false\n if (bag.byRun) {\n const nextByRun: Record<string, TodoItem[]> = {}\n for (const [runId, items] of Object.entries(bag.byRun)) {\n if (validRunIds.has(runId)) {\n nextByRun[runId] = items\n }\n else {\n dropped.push(runId)\n bagChanged = true\n }\n }\n if (Object.keys(nextByRun).length !== Object.keys(bag.byRun).length)\n bag.byRun = nextByRun\n }\n if (bag.archive?.byRun) {\n const nextByRun: Record<string, TodoItem[]> = {}\n let archiveChanged = false\n for (const [runId, items] of Object.entries(bag.archive.byRun)) {\n if (validRunIds.has(runId))\n nextByRun[runId] = items\n else\n archiveChanged = true\n }\n if (archiveChanged) {\n bag.archive = { ...bag.archive, byRun: nextByRun }\n bagChanged = true\n }\n }\n if (bagChanged)\n writeTodosBag(session, bag)\n\n const counts = readCountsBag(session)\n if (counts.byRun) {\n const nextByRun: Record<string, number> = {}\n let changed = false\n for (const [runId, n] of Object.entries(counts.byRun)) {\n if (validRunIds.has(runId))\n nextByRun[runId] = n\n else\n changed = true\n }\n if (changed) {\n counts.byRun = nextByRun\n writeCountsBag(session, counts)\n }\n }\n\n return { dropped }\n}\n\n// ---------------------------------------------------------------------------\n// Tool factory\n// ---------------------------------------------------------------------------\n\n/**\n * Build a `Preset` carrying the `{ todowrite, todoread }` tool pair.\n *\n * Loop hardening is layered, soft → hard:\n * 1. **In-body identical dedup** (`dedupIdentical`, on) — resolves\n * against the active slot (session-shared for top-level runs,\n * per-run for subagents) and returns \"No change — N items already\n * tracked.\" Advisory; the model can ignore it.\n * 2. **In-band reminder** (`remindAfter`, on) — appends a \"you've\n * checkpointed N times\" nudge to the result. Also advisory.\n * 3. **Consecutive-identical hard block** (`maxIdenticalWrites`, ON by\n * default) — a run-scoped `behavior.dedupTools.todowrite` entry in\n * `block-after` mode. After N byte-identical writes in a row the\n * gate refuses with `Blocked: …` and the body never runs. This is\n * the real loop breaker, and it's safe-by-default precisely because\n * it only fires on *identical* payloads — a legitimate N-item plan\n * writes a *different* list each step and never trips it.\n *\n * By default the preset adds **no** `toolBudgets` entry — a blanket\n * per-call cap is the wrong abstraction for a state-transition tool (it\n * punishes the legitimate \"finish the N-item plan\" path). Hosts that\n * want a ceiling on *all* writes (not just identical ones) pass\n * `maxWritesPerRun: N` explicitly; the factory then also plumbs a\n * `behavior.toolBudgets.todowrite` entry.\n *\n * Returning a `Preset` (not a bare tool map) lets the result flow\n * through {@link composePresets} unchanged. `behavior.dedupTools` and\n * `behavior.toolBudgets` are tool-name-keyed records that\n * `composePresets` deep-merges, so a caller's entries for other tools\n * survive the layering, and a caller's override for `todowrite` itself\n * wins by being placed later in the chain.\n *\n * ```ts\n * import { basic, composePresets } from 'zidane/presets'\n * import { createTodoTools } from 'zidane/chat'\n *\n * // Default: soft dedup + reminder + a run-scoped hard block after 5\n * // identical writes in a row.\n * createAgent({ ...composePresets(basic, createTodoTools()), provider })\n *\n * // Tune (or disable) the identical-write block.\n * createTodoTools({ maxIdenticalWrites: 3 }) // stricter\n * createTodoTools({ maxIdenticalWrites: 0 }) // off — soft signals only\n *\n * // Opt-in blanket cap on *all* writes (counts distinct ones too).\n * createAgent({\n * ...composePresets(basic, createTodoTools({ maxWritesPerRun: 20 })),\n * provider,\n * })\n * ```\n *\n * For the trivial \"just add the tools to an existing config\" case, plain\n * spread is also fine — the returned `Preset` sets `tools` plus a\n * `behavior` block (carrying the default identical-write dedup):\n *\n * ```ts\n * createAgent({ ...basic, ...createTodoTools(), provider })\n * ```\n *\n * @param options - Limits, reminder and dedup settings for the todo tools.\n */\nexport function createTodoTools(options: CreateTodoToolsOptions = {}): Preset {\n const maxItems = options.maxItems ?? DEFAULT_MAX_TODO_ITEMS\n const remindAfter = options.remindAfter ?? 3\n const reminderText = options.reminderText ?? ((count, _items) => defaultReminder(count))\n const dedupIdentical = options.dedupIdentical ?? true\n // Default opt-out — `todowrite` is a state-transition tool, capping\n // it punishes legitimate \"finish the 5-item plan\" paths (which need\n // ≥6 calls). The `remindAfter` reminder + `dedupIdentical` check\n // handle spam in-band; a host that wants a hard ceiling can opt in\n // explicitly.\n const maxWritesPerRun = options.maxWritesPerRun ?? 0\n const onMaxWrites = options.onMaxWrites ?? 'steer'\n // On by default — a run-scoped, identical-payload-only hard stop. Only\n // ever blocks a byte-for-byte repeat of the already-tracked list, so it\n // can't punish a legitimate multi-step plan (every transition writes a\n // *different* list). See `maxIdenticalWrites` JSDoc.\n const maxIdenticalWrites = options.maxIdenticalWrites ?? 5\n\n const tools: Record<string, ToolDef> = {\n [TODOWRITE_TOOL]: createTodoWriteTool({\n maxItems,\n remindAfter,\n reminderText,\n dedupIdentical,\n description: options.writeDescription ?? WRITE_DESCRIPTION,\n }),\n [TODOREAD_TOOL]: createTodoReadTool({\n description: options.readDescription ?? READ_DESCRIPTION,\n }),\n }\n\n // `toolBudgets` is opt-in: the budget fires at the `tool:gate` layer to\n // enforce a hard cap. The soft identical-payload check lives in the tool body\n // instead of a session-scoped `behavior.dedupTools` entry, which would\n // silently drop cross-run re-writes of an identical list (see\n // `dedupIdentical` JSDoc). Only the run-scoped hard block below uses `dedupTools`.\n const behavior: NonNullable<Preset['behavior']> = {}\n if (maxWritesPerRun > 0)\n behavior.toolBudgets = { [TODOWRITE_TOOL]: { max: maxWritesPerRun, onExceed: onMaxWrites } }\n\n // Consecutive-identical-write loop breaker. Run-scoped so a parent and a\n // subagent never share the counter; `block-after` so the first few\n // identical re-writes replay the unchanged result (matching the in-body\n // soft dedup) before the gate hard-refuses. The hasher mirrors the\n // body's `todosEqual` normalization (order-sensitive, three fields) so\n // the gate's \"identical\" and the body's \"unchanged\" agree.\n if (maxIdenticalWrites > 0) {\n const reason = options.identicalBlockReason ?? defaultIdenticalBlockReason\n behavior.dedupTools = {\n [TODOWRITE_TOOL]: {\n hasher: hashTodoWriteInput,\n mode: 'block-after',\n threshold: maxIdenticalWrites,\n scope: 'run',\n reason: (_input, count) => reason(count),\n },\n }\n }\n\n return definePreset(\n Object.keys(behavior).length > 0\n ? { tools, behavior }\n : { tools },\n )\n}\n\n// ---------------------------------------------------------------------------\n// Tool implementations\n// ---------------------------------------------------------------------------\n\ninterface CreateTodoWriteOptions {\n maxItems: number\n remindAfter: number\n reminderText: (count: number, items: readonly TodoItem[]) => string | undefined\n dedupIdentical: boolean\n description: string\n}\n\nfunction createTodoWriteTool(opts: CreateTodoWriteOptions): ToolDef {\n return {\n spec: {\n name: TODOWRITE_TOOL,\n description: opts.description,\n inputSchema: {\n type: 'object',\n properties: {\n todos: {\n type: 'array',\n description: 'The complete task list. Replaces the prior list in full.',\n maxItems: opts.maxItems,\n items: {\n type: 'object',\n properties: {\n id: { type: 'string', description: 'Stable identifier (the model picks).' },\n content: { type: 'string', description: 'One-line task summary.' },\n status: {\n type: 'string',\n enum: [...TODO_STATUS_VALUES],\n description: '`pending`, `in_progress`, `completed`, or `cancelled`.',\n },\n },\n required: ['id', 'content', 'status'],\n },\n },\n },\n required: ['todos'],\n },\n },\n async execute(input, ctx) {\n const { session, runId } = requireSessionAndRun(ctx, TODOWRITE_TOOL)\n\n const rawItems = Array.isArray(input.todos) ? input.todos : []\n const items = sanitizeItems(rawItems, opts.maxItems)\n const dropped = rawItems.length - items.length\n\n // ID dedup — last-wins. Two items in one payload sharing an id is\n // a model mistake; quietly normalize rather than error.\n const byId = new Map<string, TodoItem>()\n for (const item of items)\n byId.set(item.id, item)\n const normalized = [...byId.values()]\n\n // Identical-payload short-circuit — compares against the slot the\n // active run resolves to (session-shared for top-level, per-run\n // for subagents). See `dedupIdentical` JSDoc.\n const current = getTodosForRun(session, runId)\n const unchanged = opts.dedupIdentical && todosEqual(current, normalized)\n\n // Counter advances on every call — including identical re-writes —\n // so the reminder threshold catches \"model keeps re-checkpointing\n // the same list\" spam. Scope mirrors the data (session-shared for\n // top-level, per-run for subagents) so the count reflects\n // cumulative use of the *active* slot, not noisy per-prompt resets.\n const count = incrementCount(session, runId)\n\n // Auto-clear when every item is `completed`. Matches Claude Code's\n // TodoWrite v1 behavior — once a batch is fully done, the next\n // prompt should start clean rather than re-read a wall of\n // checkmarks. The model's reply still summarizes the completion;\n // only the persisted slot is wiped.\n const allCompleted = normalized.length > 0 && normalized.every(t => t.status === 'completed')\n\n if (!unchanged) {\n if (allCompleted) {\n // Two-phase auto-clear: first persist the all-completed list\n // so the archive captures it (setTodosForRun copies non-empty\n // writes into the archive sidecar), then wipe the live slot.\n // The empty write preserves the archive, so the modal can\n // keep showing the close-out batch until the model writes\n // something new.\n setTodosForRun(session, runId, normalized)\n setTodosForRun(session, runId, [])\n }\n else {\n setTodosForRun(session, runId, normalized)\n }\n }\n\n return formatWriteResult({\n items: normalized,\n dropped,\n count,\n unchanged,\n // Only report the auto-clear when it actually fired (a no-op\n // re-write of an already-cleared list shouldn't shout\n // \"marked complete\" again).\n cleared: allCompleted && !unchanged,\n opts,\n })\n },\n }\n}\n\ninterface FormatWriteResultInput {\n items: readonly TodoItem[]\n dropped: number\n count: number\n unchanged: boolean\n cleared: boolean\n opts: CreateTodoWriteOptions\n}\n\nfunction formatWriteResult(input: FormatWriteResultInput): string {\n const { items, dropped, count, unchanged, cleared, opts } = input\n const lines: string[] = []\n const n = items.length\n const suffix = n === 1 ? '' : 's'\n if (cleared) {\n lines.push(`Marked ${n} item${suffix} complete; list cleared.`)\n }\n else if (unchanged) {\n lines.push(`No change; ${n} todo item${suffix} already tracked.`)\n }\n else {\n lines.push(`Updated ${n} todo item${suffix}.`)\n }\n // Skip the tally when the list was cleared — the headline already\n // captures \"all done\" and the tally would read as a redundant\n // restatement.\n if (!cleared) {\n const tally = summarizeStatuses(items)\n if (tally)\n lines.push(tally)\n }\n if (dropped > 0)\n lines.push(`Dropped ${dropped} malformed item${dropped === 1 ? '' : 's'}.`)\n const reminder = opts.remindAfter > 0 && count >= opts.remindAfter\n ? opts.reminderText(count, items)\n : undefined\n if (reminder && reminder.length > 0)\n lines.push(reminder)\n return lines.join('\\n')\n}\n\nfunction createTodoReadTool(opts: { description: string }): ToolDef {\n return {\n // Reads `session.metadata.todos` without mutation — concurrency-safe.\n isConcurrencySafe: true,\n replay: 'safe',\n spec: {\n name: TODOREAD_TOOL,\n description: opts.description,\n inputSchema: {\n type: 'object',\n properties: {},\n },\n },\n async execute(_input, ctx) {\n const { session, runId } = requireSessionAndRun(ctx, TODOREAD_TOOL)\n\n const items = getTodosForRun(session, runId)\n if (items.length === 0)\n return 'No todos yet. Call todowrite to start tracking tasks.'\n return JSON.stringify({ todos: items })\n },\n }\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Both tool bodies need a `Session` + `runId`. Centralize the guards so the\n * error messages stay symmetric and one fix lands in both places.\n */\nfunction requireSessionAndRun(\n ctx: ToolContext,\n toolName: string,\n): { session: Session, runId: string } {\n if (!ctx.session)\n throw new Error(`${toolName}: no session on tool context. Todos require a session via createSession().`)\n if (!ctx.runId)\n throw new Error(`${toolName}: no runId on tool context.`)\n return { session: ctx.session, runId: ctx.runId }\n}\n\n/*\n * No cap on the READ path. The list was already capped by `maxItems` when it\n * was written, and a host that raised that ceiling must not have its persisted\n * rows silently truncated on resume by a lower default here.\n */\nconst READ_ITEM_CAP = Number.MAX_SAFE_INTEGER\n\n/** Validate one persisted slot. Non-arrays and malformed rows drop out. */\nfunction readSlot(raw: unknown): TodoItem[] | undefined {\n if (!Array.isArray(raw))\n return undefined\n const items = sanitizeItems(raw, READ_ITEM_CAP)\n return items.length > 0 ? items : undefined\n}\n\n/** Validate a `runId -> items` map, dropping slots that end up empty. */\nfunction readSlotMap(raw: unknown): Record<string, TodoItem[]> | undefined {\n if (!isPlainObject(raw))\n return undefined\n const out: Record<string, TodoItem[]> = {}\n for (const [runId, value] of Object.entries(raw)) {\n const items = readSlot(value)\n if (items)\n out[runId] = items\n }\n return Object.keys(out).length > 0 ? out : undefined\n}\n\n/** Read the todos bag from session metadata, normalizing the live shape. */\nfunction readTodosBag(session: Session): TodosBag {\n const raw = session.metadata[TODOS_METADATA_KEY]\n if (!isPlainObject(raw))\n return {}\n /*\n * Persisted metadata is UNTRUSTED input, same as tool arguments: it comes off\n * disk, may have been written by an older build or hand-edited, and outlives\n * any single version of this file. Casting it with `as TodoItem[]` would let a\n * row with `status: \"done\"` (not a `TodoStatus`) type-check as valid and then\n * fall silently through the tally fold — counted in `total` but in none of\n * the four buckets.\n *\n * `sanitizeItems` is the same validator the tool-input path already uses, so\n * both entry points agree on what a well-formed item is. Malformed rows are\n * dropped rather than throwing: this is a resume path, and one bad row must\n * not cost the user the rest of their list.\n */\n const archive = isPlainObject(raw.archive)\n ? {\n session: readSlot(raw.archive.session),\n byRun: readSlotMap(raw.archive.byRun),\n }\n : undefined\n return {\n session: readSlot(raw.session),\n byRun: readSlotMap(raw.byRun),\n archive,\n }\n}\n\nfunction writeTodosBag(session: Session, bag: TodosBag): void {\n // Strip empty sub-fields so the persisted shape stays minimal.\n const clean: TodosBag = {}\n if (bag.session && bag.session.length > 0)\n clean.session = bag.session\n if (bag.byRun && Object.keys(bag.byRun).length > 0)\n clean.byRun = bag.byRun\n if (bag.archive) {\n const archive: NonNullable<TodosBag['archive']> = {}\n if (bag.archive.session && bag.archive.session.length > 0)\n archive.session = bag.archive.session\n if (bag.archive.byRun && Object.keys(bag.archive.byRun).length > 0)\n archive.byRun = bag.archive.byRun\n if (archive.session || archive.byRun)\n clean.archive = archive\n }\n session.setMeta(TODOS_METADATA_KEY, clean)\n}\n\nfunction readCountsBag(session: Session): CountsBag {\n const raw = session.metadata[TODO_WRITE_COUNTS_METADATA_KEY]\n if (!isPlainObject(raw))\n return {}\n return {\n session: typeof raw.session === 'number' ? raw.session : undefined,\n byRun: isPlainObject(raw.byRun) ? raw.byRun as Record<string, number> : undefined,\n }\n}\n\nfunction writeCountsBag(session: Session, bag: CountsBag): void {\n const clean: CountsBag = {}\n if (typeof bag.session === 'number' && bag.session > 0)\n clean.session = bag.session\n if (bag.byRun && Object.keys(bag.byRun).length > 0)\n clean.byRun = bag.byRun\n session.setMeta(TODO_WRITE_COUNTS_METADATA_KEY, clean)\n}\n\n/**\n * Bump the call counter for the active slot and return the new value.\n * Top-level runs increment `bag.session` (cumulative across prompts);\n * subagent runs increment their own entry under `bag.byRun`. Same\n * keying as {@link setTodosForRun} so the counter and the data move\n * together — a freshly seeded run's count starts at 1 on its first\n * write, a continuing top-level run's count picks up from where the\n * prior prompt left off.\n */\nfunction incrementCount(session: Session, runId: string): number {\n const bag = readCountsBag(session)\n let next: number\n if (subagentRunId(session, runId) !== null) {\n const byRun = { ...(bag.byRun ?? {}) }\n next = (byRun[runId] ?? 0) + 1\n byRun[runId] = next\n bag.byRun = byRun\n }\n else {\n next = (bag.session ?? 0) + 1\n bag.session = next\n }\n writeCountsBag(session, bag)\n return next\n}\n\n/** Narrow guard — `Record<string, unknown>` excluding arrays / null. */\nfunction isPlainObject(v: unknown): v is Record<string, unknown> {\n return !!v && typeof v === 'object' && !Array.isArray(v)\n}\n\nfunction normalizeItem(item: TodoItem): TodoItem {\n return { id: item.id, content: item.content, status: item.status }\n}\n\n/**\n * Validate an untrusted todo list, dropping malformed rows and capping length.\n *\n * The single validator for every write/read boundary: model tool input,\n * persisted metadata on read, and user edits arriving over IPC. Keeping them on\n * one implementation is what makes \"well-formed\" mean the same thing at each —\n * a boundary that only picked fields (as `normalizeItem` does) would admit an\n * invalid `status` that the read path then silently drops, so the write appears\n * to succeed and the row vanishes with nothing reported.\n *\n * @param raw - Untrusted list of items.\n * @param cap - Maximum number of items kept.\n */\nexport function sanitizeTodoItems(raw: readonly unknown[], cap: number = DEFAULT_MAX_TODO_ITEMS): TodoItem[] {\n return sanitizeItems([...raw], cap)\n}\n\nfunction sanitizeItems(raw: unknown[], cap: number): TodoItem[] {\n const out: TodoItem[] = []\n for (const item of raw) {\n if (!item || typeof item !== 'object' || Array.isArray(item))\n continue\n const obj = item as Record<string, unknown>\n const id = typeof obj.id === 'string' ? obj.id : undefined\n const content = typeof obj.content === 'string' ? obj.content : undefined\n const status = typeof obj.status === 'string' && (TODO_STATUS_VALUES as readonly string[]).includes(obj.status)\n ? obj.status as TodoStatus\n : undefined\n if (!id || !content || !status)\n continue\n out.push({ id, content, status })\n if (out.length >= cap)\n break\n }\n return out\n}\n\nfunction summarizeStatuses(items: readonly TodoItem[]): string | undefined {\n if (items.length === 0)\n return undefined\n const counts: Record<TodoStatus, number> = {\n pending: 0,\n in_progress: 0,\n completed: 0,\n cancelled: 0,\n }\n for (const item of items)\n counts[item.status] += 1\n const parts: string[] = []\n if (counts.completed)\n parts.push(`${counts.completed} completed`)\n if (counts.in_progress)\n parts.push(`${counts.in_progress} in progress`)\n if (counts.pending)\n parts.push(`${counts.pending} pending`)\n if (counts.cancelled)\n parts.push(`${counts.cancelled} cancelled`)\n return parts.length > 0 ? parts.join(' · ') : undefined\n}\n\n/**\n * Order-sensitive structural equality on todo lists. Used by the\n * `dedupIdentical` short-circuit to decide whether an incoming payload\n * matches the current run's stored slot.\n *\n * Order-sensitive on purpose: re-ordering the list is a meaningful state\n * change (\"the model reprioritized\") and should NOT collapse to a no-op.\n * Three-field comparison is exhaustive for `TodoItem` — `normalizeItem`\n * strips any extra fields before they reach the slot, so we never need to\n * compare beyond `{ id, content, status }`.\n */\nfunction todosEqual(a: readonly TodoItem[], b: readonly TodoItem[]): boolean {\n if (a.length !== b.length)\n return false\n for (let i = 0; i < a.length; i++) {\n const x = a[i]\n const y = b[i]\n if (x.id !== y.id || x.status !== y.status || x.content !== y.content)\n return false\n }\n return true\n}\n\n// ---------------------------------------------------------------------------\n// UI-facing derivation — active-run slice + tally + in-progress pick.\n//\n// Renderer-agnostic. The TUI's inline indicator + modal both consume\n// {@link useActiveTodos}; the GUI calls {@link selectActiveTodos} directly.\n// ---------------------------------------------------------------------------\n\n/** Number of todos in each status. */\nexport interface TodoTally {\n /** Todos not started. */\n pending: number\n /** Todos being worked on. */\n in_progress: number\n /** Todos finished. */\n completed: number\n /** Todos dropped. */\n cancelled: number\n}\n\n/** Todos of the run a UI surface should show. */\nexport interface ActiveTodosState {\n /** Run the slice came from. `null` ⇒ no session / no runs / no active run. */\n runId: string | null\n /**\n * Live list — what `todoread` would return. Empty when the slot was\n * cleared (e.g. auto-clear after every item completed) or never\n * written. Source of truth for the model's view.\n */\n todos: readonly TodoItem[]\n /**\n * Most-recent non-empty list for the active slot, preserved across\n * auto-clear. Equals `todos` when the live list is non-empty;\n * otherwise the last batch that was shown (commonly the all-`completed`\n * close-out). UI surfaces fall back to this when `todos` is empty so\n * the user can still glance back at \"what was just finished\" — until\n * the model writes a new list. Empty array when no list was ever\n * written for this slot.\n */\n archive: readonly TodoItem[]\n /**\n * The first `in_progress` item from `todos` (the **live** list), when\n * one exists. Drives the inline indicator above the prompt. Always\n * `null` when the live list is empty — even if the archive carries\n * an in-progress item, that item is by definition stale.\n */\n inProgress: TodoItem | null\n /**\n * Per-status counts. Computed over `todos` when non-empty, else over\n * `archive`. Zeros only when both are empty. Lets the modal header\n * show a meaningful tally for the archived \"last batch\" view.\n */\n tally: TodoTally\n}\n\nconst EMPTY_TALLY: TodoTally = { pending: 0, in_progress: 0, completed: 0, cancelled: 0 }\nconst EMPTY_ACTIVE: ActiveTodosState = {\n runId: null,\n todos: [],\n archive: [],\n inProgress: null,\n tally: EMPTY_TALLY,\n}\n\n/**\n * Pick the \"active\" run for UI surfaces that show one slot at a time\n * (todos indicator, todos modal, footer ctx indicator). The resolved\n * runId then routes through {@link getTodosForRun} — top-level runs\n * land on the session-shared slot, subagent runs on their own.\n *\n * - A currently-running run wins (latest-started one if several).\n * - Otherwise the most recently appended run wins.\n * - `null` when the session has no runs.\n *\n * Mirrors the \"most recent thing that mattered\" policy that drives\n * `lastContextSizeFromTurns` — but walks `session.runs` rather than\n * `session.turns` because the active todo slot is a function of the\n * run, not of message history.\n *\n * @param session - The session to inspect.\n */\nexport function pickActiveRunId(session: Session | null | undefined): string | null {\n if (!session)\n return null\n const runs = session.runs\n if (!runs || runs.length === 0)\n return null\n // Scan from the tail — latest-running wins, falling back to latest of any\n // status. Subagent runs are eligible like top-level runs: a child spawned\n // off the parent has its own runId / its own todo slot, and surfacing it\n // here matches the per-run isolation the data layer already guarantees.\n let latestRunning: string | null = null\n for (let i = runs.length - 1; i >= 0; i--) {\n if (runs[i].status === 'running') {\n latestRunning = runs[i].id\n break\n }\n }\n if (latestRunning)\n return latestRunning\n return runs[runs.length - 1].id\n}\n\n/**\n * Pure selector — derive the active-todos state from a session snapshot.\n * Used by {@link useActiveTodos} and directly testable without React.\n *\n * Resolves both `todos` (live, model-facing) and `archive` (last\n * non-empty snapshot for UI fallback). The tally folds over whichever\n * list is non-empty; `inProgress` is sourced strictly from `todos`\n * because an archived in-progress item is by definition stale. A subagent\n * run with no list and no archive of its own resolves to its parent run.\n *\n * @param session - The session snapshot, or a nullish value for none.\n */\nexport function selectActiveTodos(session: Session | null | undefined): ActiveTodosState {\n if (!session)\n return EMPTY_ACTIVE\n // No run yet: the session slot, which a user can fill before the first prompt.\n let runId: string | null = pickActiveRunId(session)\n // A subagent with no list of its own shows the run it works for: the\n // parent's plan is what is going on (a reloaded window showed it empty).\n const seen = new Set<string>()\n while (runId !== null && !seen.has(runId) && getTodosForRun(session, runId).length === 0 && getArchivedTodosForRun(session, runId).length === 0) {\n seen.add(runId)\n const parent = session.runs.find(r => r.id === runId)?.parentRunId\n if (!parent)\n break\n runId = parent\n }\n const todos = getTodosForRun(session, runId)\n const archive = getArchivedTodosForRun(session, runId)\n // Whichever list the modal would show. `todos` wins when live,\n // `archive` fills in after auto-clear, empty when neither exists.\n const display = todos.length > 0 ? todos : archive\n if (display.length === 0)\n return { runId, todos, archive, inProgress: null, tally: EMPTY_TALLY }\n const tally: TodoTally = { pending: 0, in_progress: 0, completed: 0, cancelled: 0 }\n let inProgress: TodoItem | null = null\n // Tally folds over `display`; `inProgress` only over `todos` so we\n // don't surface an item that's no longer being worked on.\n for (const t of display)\n tally[t.status] += 1\n if (todos.length > 0) {\n for (const t of todos) {\n if (t.status === 'in_progress') {\n inProgress = t\n break\n }\n }\n }\n return { runId, todos, archive, inProgress, tally }\n}\n\n/**\n * Progress label for the footer chip: `\"2/5\"`, or `null` when there is nothing\n * to advertise.\n *\n * The chip shows a position, not the in-progress todo's TITLE. Titles are\n * model-authored and arbitrarily long, so they would have to truncate, reflow\n * with terminal width, and still tell you nothing you couldn't get by opening\n * the list. A position reads at a glance and costs a fixed handful of columns\n * in a row that's already scanned for shortcuts.\n *\n * The numerator is the POSITION of the in-progress item, which answers\n * \"which one is live\". With nothing in progress (list finished,\n * or the archive is on screen) it falls back to the completed count, so a done\n * list reads `5/5` rather than `0/5`.\n *\n * Pure and renderer-agnostic, so the GUI and the tests can both use it.\n */\nexport function todoProgressLabel(state: ActiveTodosState): string | null {\n const display = state.todos.length > 0 ? state.todos : state.archive\n if (display.length === 0)\n return null\n const activeIndex = state.inProgress\n ? display.findIndex(t => t.id === state.inProgress!.id)\n : -1\n const numerator = activeIndex >= 0 ? activeIndex + 1 : state.tally.completed\n return `${numerator}/${display.length}`\n}\n\n/**\n * React hook — active-todos state for the supplied session. Recomputes\n * on every parent re-render; the work is O(runs + todos), both bounded\n * by small constants in practice.\n *\n * Why no memoization: `Session.runs` is mutated in place by `completeRun`\n * / `abortRun` (status flips without changing array length or item\n * identity), so any memo keyed off length / bag identity would silently\n * return stale state when e.g. a child subagent run completes and the\n * \"active\" run should reflow to the parent. The selector is cheap\n * enough that running it every parent render is the simpler, correct\n * answer — see `selectActiveTodos` for the O(n) cost.\n *\n * The hook lives in `chat/` so a GUI consumer can mount it verbatim.\n * Renderer-agnostic — no `@opentui/*` imports.\n *\n * @param session - The session snapshot, or a nullish value for none.\n */\nexport function useActiveTodos(session: Session | null | undefined): ActiveTodosState {\n return selectActiveTodos(session)\n}\n","/**\n * Agent profiles — named variants of the chat agent (Build, Plan, …).\n *\n * Each profile bundles a {@link Preset} with display metadata (label,\n * description, accent color) so the TUI can render a picker, persist the\n * user's choice, and rebuild the active agent on switch.\n *\n * The host's {@link ChatOptions.preset} is the single-agent shorthand; for\n * multi-profile setups, pass `agents` (an {@link AgentRegistry}) directly.\n * Built-ins ship as {@link BUILTIN_AGENTS} = `{ build, build-codemode, plan }`.\n *\n * A session's agent is built with one composition (tools + prompt): a\n * profile's own ({@link profileComposition}), or the one the session already\n * runs when the profile fits inside it ({@link compositionFits}), so a switch\n * keeps the prompt prefix.\n *\n * No OpenTUI dependency — this module is pure data so a GUI host can\n * consume the same registry.\n */\nimport type { Preset } from '../presets'\nimport type { ToolDef } from '../tools/types'\nimport type { AgentAccent } from './agent-accent'\nimport type { Settings } from './types'\nimport { DEFAULT_MAX_IMAGE_EDGE, DEFAULT_MEDIA_BUDGET } from '../media/defaults'\nimport { composePresets, definePreset } from '../presets'\nimport { edit, glob, grep, listFiles, multiEdit, readFile, shell, shellKill, waitTask, writeFile } from '../tools'\nimport { fetchUrl } from '../tools/fetch-url'\nimport { createSpawnTool } from '../tools/spawn'\nimport { webSearch } from '../tools/web-search'\nimport { buildBuildSystem, buildPlanSystem } from './agent-prompt'\nimport { createTodoTools } from './todos'\n\nexport { accentColor, type AgentAccent } from './agent-accent'\n\n/** A selectable agent profile: a labelled preset the user can switch between. */\nexport interface AgentProfile {\n /** Stable identifier. Each session stores the id of the profile it runs on. */\n id: string\n /** Human-readable label rendered in the picker and footer badge. */\n label: string\n /** One-line description shown next to the label in the picker. */\n description: string\n /**\n * Preset applied to `createAgent()` when this profile is active. Profiles\n * are self-contained: the host's chat-level `preset` (if any) is NOT\n * merged underneath. Hosts that want a shared base across profiles\n * should compose it explicitly in each profile's preset.\n */\n preset: Preset\n /** Theme token used by the picker / footer badge. Defaults to `'accent'`. */\n accent?: AgentAccent\n /**\n * A boolean setting that must be on for this profile to exist. While it is\n * off the profile is left out of every picker, and a session saved on it\n * builds as the default profile instead (see {@link availableAgents}).\n */\n requiresSetting?: keyof Settings\n /**\n * Profiles with the same family share one agent: its tools are every\n * member's tools, its prompt is the first member's (in registry order).\n * Switching between members keeps the agent and the prompt cache; a gate\n * refuses the tools the active member doesn't have, and a note tells the\n * model about the switch. Without a family the profile composes alone.\n */\n family?: string\n /**\n * What the model is told when the session switches to this profile without\n * rebuilding its agent. Default: the profile's label, description and\n * system prompt, then the tools that are off. On the profile a composition\n * is built from, it is what the model is told when the session comes back\n * to it (default: its instructions and tools apply again).\n */\n switchNote?: string | ((ctx: ProfileSwitchNoteContext) => string)\n}\n\n/** What a {@link AgentProfile.switchNote} function is given. */\nexport interface ProfileSwitchNoteContext {\n /** Names of the profile's own tools. Tools every profile gets (interaction, skills, MCP, …) are not listed. */\n tools: readonly string[]\n /** Interactive prompts (`present_plan`, `ask_user`) are on. */\n allowInteraction: boolean\n}\n\n/** Agent profiles keyed by profile id. */\nexport type AgentRegistry = Readonly<Record<string, AgentProfile>>\n\n/**\n * The profiles available under `settings`: those without a\n * {@link AgentProfile.requiresSetting}, or whose setting is on. Returns the\n * same registry object when nothing is filtered out.\n *\n * @param registry - The full agent registry.\n * @param settings - Current settings, checked against each profile's `requiresSetting`.\n */\nexport function availableAgents(registry: AgentRegistry, settings: Partial<Settings>): AgentRegistry {\n const hidden = Object.values(registry).filter(profile => profile.requiresSetting !== undefined && settings[profile.requiresSetting] !== true)\n if (hidden.length === 0)\n return registry\n return Object.fromEntries(Object.entries(registry).filter(([, profile]) => !hidden.includes(profile)))\n}\n\n/**\n * What a session's agent is built with: tools and prompt. `key` names it\n * (`family:<name>` or `profile:<id>`) and keys the session's frozen prompt.\n */\nexport interface AgentComposition {\n /** `family:<name>` for a family, `profile:<id>` for a profile without one. */\n key: string\n /** The profile whose prompt, behavior and hooks the agent is built from. */\n base: AgentProfile\n /** Every profile in the composition, in registry order, `base` first. */\n members: readonly AgentProfile[]\n}\n\n/**\n * A profile's own composition: its family (every registered member, in\n * registry order) or the profile alone.\n *\n * @param registry - The profiles available to the session.\n * @param profile - The profile to compose.\n */\nexport function profileComposition(registry: AgentRegistry, profile: AgentProfile): AgentComposition {\n if (profile.family === undefined)\n return { key: `profile:${profile.id}`, base: profile, members: [profile] }\n const members = Object.values(registry).filter(member => member.family === profile.family)\n if (!members.includes(profile))\n members.push(profile)\n return { key: `family:${profile.family}`, base: members[0]!, members }\n}\n\n/**\n * The composition a key names in `registry`, `undefined` when none of its\n * profiles is registered any more.\n *\n * @param registry - The profiles available to the session.\n * @param key - A composition key (`family:<name>` or `profile:<id>`).\n */\nexport function compositionByKey(registry: AgentRegistry, key: string): AgentComposition | undefined {\n if (key.startsWith('profile:')) {\n const profile = registry[key.slice('profile:'.length)]\n return profile && profile.family === undefined ? profileComposition(registry, profile) : undefined\n }\n if (key.startsWith('family:')) {\n const family = key.slice('family:'.length)\n const member = Object.values(registry).find(profile => profile.family === family)\n return member ? profileComposition(registry, member) : undefined\n }\n return undefined\n}\n\n/**\n * A profile's own tools by name. Tools the host gives every profile\n * (interaction, skills, MCP, …) are not part of it.\n *\n * @param profile - The profile.\n */\nexport function profileTools(profile: AgentProfile): ReadonlyMap<string, ToolDef> {\n const tools = new Map<string, ToolDef>()\n for (const tool of Object.values(profile.preset.tools ?? {})) {\n if (tool && !tools.has(tool.spec.name))\n tools.set(tool.spec.name, tool)\n }\n return tools\n}\n\n/**\n * The profile tools a composition's agent can call, by name: its base's,\n * then each other member's that the base doesn't have.\n *\n * @param composition - The composition.\n */\nexport function compositionTools(composition: AgentComposition): ReadonlyMap<string, ToolDef> {\n const tools = new Map(profileTools(composition.base))\n for (const member of composition.members) {\n for (const [name, tool] of profileTools(member)) {\n if (!tools.has(name))\n tools.set(name, tool)\n }\n }\n return tools\n}\n\nfunction sameTool(a: ToolDef, b: ToolDef): boolean {\n return a === b || JSON.stringify(a.spec) === JSON.stringify(b.spec)\n}\n\n/**\n * Whether `profile` runs on `composition` without a new agent: it is the\n * profile's own composition, or the profile's tools are a strict subset of\n * the composition's (same names and specs). Codemode scripts reach the same\n * tools, so a composition's tools count whether they are on the wire or not.\n *\n * @param registry - The profiles available to the session.\n * @param composition - The composition the session's agent is built with.\n * @param profile - The profile the session switches to.\n */\nexport function compositionFits(registry: AgentRegistry, composition: AgentComposition, profile: AgentProfile): boolean {\n if (profileComposition(registry, profile).key === composition.key)\n return true\n const available = compositionTools(composition)\n const own = profileTools(profile)\n if (own.size >= available.size)\n return false\n for (const [name, tool] of own) {\n const match = available.get(name)\n if (!match || !sameTool(match, tool))\n return false\n }\n return true\n}\n\n/**\n * The composition a session on `profileId` builds with: the one it ran on\n * (`currentKey`) while the profile fits it, else the profile's own.\n * `undefined` when `profileId` is not registered.\n *\n * @param registry - The profiles available to the session.\n * @param profileId - The session's profile.\n * @param currentKey - The key of the composition the session ran on, if any.\n */\nexport function sessionComposition(registry: AgentRegistry, profileId: string, currentKey: string | undefined): AgentComposition | undefined {\n const profile = registry[profileId]\n if (!profile)\n return undefined\n const current = currentKey === undefined ? undefined : compositionByKey(registry, currentKey)\n return current && compositionFits(registry, current, profile) ? current : profileComposition(registry, profile)\n}\n\n/** The built-in prompts the chat host composes (with env, AGENTS.md and SYSTEM.md). */\nexport type BuiltInPrompt = 'build' | 'plan'\n\nconst BUILT_IN_PROMPTS = new Map<string, BuiltInPrompt>([\n [buildBuildSystem(), 'build'],\n [buildPlanSystem(), 'plan'],\n])\n\n/**\n * The built-in prompt a profile runs, `undefined` for a profile with its own.\n * Decided by the preset's prompt, never the profile's id: a preset that uses\n * Build's or Plan's built-in prompt (`BUILD_AGENT.preset.system`) gets it\n * composed with the session's environment, AGENTS.md and SYSTEM.md; a host\n * profile that happens to be called `build` keeps its own prompt.\n *\n * @param profile - The profile.\n */\nexport function builtInPromptOf(profile: AgentProfile): BuiltInPrompt | undefined {\n return profile.preset.system === undefined ? undefined : BUILT_IN_PROMPTS.get(profile.preset.system)\n}\n\n/** Read-only tool slice shared by the Plan profile and any host-built read-only variant. */\nexport const READ_ONLY_TOOLS = { readFile, listFiles, glob, grep, webSearch, fetchUrl }\n\n/**\n * Build-mode tool slice — read + write + shell. Each Build profile adds its\n * own `spawn` tool.\n *\n * `shellKill` / `waitTask` ride along with `shell`: the shell tool's\n * description (and the `<task-notification>` stall hint in the agent\n * loop) instruct the model to call `shell_kill` / `wait_task` for\n * background tasks, so omitting them produces tool-not-found loops the\n * moment a `run_in_background` task needs terminating or blocking on.\n */\nconst BUILD_TOOLS = { shell, shellKill, waitTask, readFile, writeFile, listFiles, edit, multiEdit, glob, grep, webSearch, fetchUrl }\n\n/**\n * Tools the chat layer never persists to disk, regardless of output size.\n *\n * Rationale per entry:\n * - `read_file` — our reader is already paginated (`offset` + `limit`).\n * Persisting the slice the model asked for and pointing it at the\n * persisted path is circular; if the model wants more, it pages.\n * - `tool_search` — short metadata envelope. Never large.\n * - `skills_use` / `skills_read` — the `<skill_content>` body is\n * intentionally part of the prompt; persisting it defeats the purpose.\n * - `read_chat_turns` — previous-chat excerpts are explicitly loaded\n * conversation context; persisting them defeats the purpose.\n * - `present_plan` / `ask_user` — short JSON envelopes (decision +\n * answers). Never large.\n * - `spawn` — the subagent's final report IS the value of the call;\n * stubbing it out would defeat the orchestration pattern.\n * - `codemode` — a script prints what the model asked for, as a read\n * does, and caps its own output (`max_output_tokens`: start and end\n * inline, the full text in a file). Persisting it would force a re-read.\n *\n * Exported so SDK consumers building custom profiles can extend the list\n * (e.g. host-defined `screenshot` tool whose base64 payload should stay\n * inline). Set `behavior.persistExcludeTools` to your own list to fully\n * override.\n */\nexport const DEFAULT_PERSIST_EXCLUDE_TOOLS: readonly string[] = [\n 'read_file',\n 'tool_search',\n 'skills_use',\n 'skills_read',\n 'read_chat_turns',\n 'present_plan',\n 'ask_user',\n 'spawn',\n 'codemode',\n // Todos are short JSON envelopes (≤100 items) and the latest snapshot\n // is the only state of interest — persisting them to disk is circular.\n 'todowrite',\n 'todoread',\n]\n\n/**\n * Tools whose output is exempt from the per-turn `toolOutputBudget` accounting,\n * for SDK consumers that turn the budget on. The built-in profiles leave it\n * off.\n *\n * These tools exist to LOAD context into the conversation — their text output\n * IS the value of the call. Counting it against the budget steers the model\n * away from the very tool it needs to make progress (e.g. a 78 KiB\n * `tool_search` response for a 20-tool Notion MCP server would trigger the\n * \"summarize before calling more tools\" nudge on the very turn that\n * unlocked those tools, making them effectively unusable).\n *\n * Note this list is intentionally narrower than {@link DEFAULT_PERSIST_EXCLUDE_TOOLS}:\n * `read_file` is NOT exempt — a 200 KiB file load is exactly the case the\n * budget should steer against. Same for `spawn` (a sprawling subagent\n * report should still nudge a consolidation pass).\n *\n * Exported so SDK consumers building custom profiles can extend the list\n * for host-defined context-loading tools.\n */\nexport const DEFAULT_BUDGET_EXCLUDE_TOOLS: readonly string[] = [\n 'tool_search',\n 'skills_use',\n 'skills_read',\n 'read_chat_turns',\n]\n\n/**\n * Stable JSON of a tool input with keys sorted, used as a dedup cache key for\n * the read-only discovery tools. Covers EVERY input field, so `grep -i` never\n * collides with `grep` and a widened `limit` busts the cache. Empty objects\n * opt out (returns `undefined`) — a zero-arg `list_files` of cwd is cheap and\n * a stale replay there is more surprising than re-running it.\n */\nfunction discoveryDedupHash(input: Record<string, unknown>): string | undefined {\n const keys = Object.keys(input)\n if (keys.length === 0)\n return undefined\n keys.sort()\n const norm: Record<string, unknown> = {}\n for (const k of keys)\n norm[k] = input[k]\n return JSON.stringify(norm)\n}\n\n/**\n * Token-saving `AgentBehavior` defaults shared by the built-in profiles.\n *\n * These flip on at the chat layer, not at the SDK core (`src/agent.ts`) —\n * external `createAgent()` consumers keep the conservative defaults.\n * Rationale per knob:\n *\n * - `compactStrategy: 'tail'` — when the persisted history's tool-output\n * bytes exceed `compactThreshold`, stub older `tool_result` blocks at\n * the wire level (session storage is untouched). Without this, a single\n * long-lived chat keeps re-sending every read/grep result every turn.\n * - `elideStaleReads: false` — kept off; see the knob's comment below.\n * - `toolDisclosure: 'lazy'` — MCP tools advertise as name+description in\n * the system prompt and load their `inputSchema` on demand via\n * `tool_search`. No-op without MCPs (gated on `disclosure.lazyEntries.length > 0`\n * in `src/agent-tool-disclosure.ts`); substantial savings with a 50-tool server.\n * - No `toolOutputBudget`. Its nudge fires after the bytes are already in\n * context, so it removed nothing and cost a summarizing turn; over 145 real\n * hits the trigger was ordinary reads of one to three files. Size is bounded\n * per call instead (`readFileDefaults`, `persistThreshold`), and the\n * conversation by compaction.\n * - `readFileDefaults.maxBytes: 32 KiB` — default text cap for a `read_file`\n * that doesn't pass `maxBytes`; the cut read ends with a continue-at-offset\n * footer, and an explicit `maxBytes` still wins. Measured on 38.5k default\n * reads: 536 were over 32 KiB, and 4 of those (0.75%) were followed by an\n * edit of the same file within 3 turns.\n * - `persistThreshold: 8 KiB` + `persistExcludeTools` — tool results\n * above the threshold are written to disk and replaced inline with a\n * `<persisted-output>` stub (preview + path). The chat host wires\n * `persistDir` when it builds the agent; without a dir, persistence is off.\n * The user can disable persistence entirely via\n * `Settings.persistToolResults` (sets the threshold to 0 at agent build\n * time).\n */\nconst SHARED_BEHAVIOR = {\n compactStrategy: 'tail',\n compactThreshold: 128 * 1024,\n // Scale the byte threshold with the picked model's context window: the\n // effective threshold is max(128 KiB, 0.25 × window × 4 B/tok). Each tail\n // compaction frontier step rewrites an early message and re-bills the entire\n // suffix at the cache-write rate — measured in production as recurring\n // ~117k-token `messagePrefix` cache breaks on a 157k-token Fable session,\n // each ~50x the cost of keeping those bytes as cache reads. At 0.25 a 1M\n // window compacts from ~1 MiB of tool output, 200k from ~200 KiB, and\n // ≤128k models stay on the 128 KiB byte threshold. Long 1M sessions DO\n // reach it: over 12 days of real sessions the frontier sat ~6% into the\n // history and advanced a chunk about every 4 tool round trips — 156 steps\n // re-reading ~738k tokens each, ~60% of all re-read tokens.\n compactThresholdWindowFraction: 0.25,\n // Hysteresis: once kept tool output crosses the threshold, trim to half of\n // it in one step and wait for the next crossing, so steps are ~0.5 ×\n // threshold of new tool output apart instead of every few turns.\n compactLowWaterFraction: 0.5,\n // No client tail compaction on Anthropic ≥400k windows: each stub also\n // makes the API drop every later replayed thinking block (preserved-thinking\n // models), and autocompact stays the ceiling.\n compactSkip: { providers: ['anthropic'], minContextWindow: 400_000 },\n // Byte-budget eviction (see `applyTailCompaction`) keeps the largest recent\n // suffix of tool output that fits under `compactThreshold`, so for typical\n // small results the model retains far more than this many messages. This is\n // a FLOOR, not a cap: the trailing 8 messages (~4 tool round-trips) are\n // never elided, guaranteeing a freshly read file survives the\n // read → think → edit cadence even when individual results are large enough\n // that the byte budget alone would keep fewer. A floor of 4 was too tight:\n // a read got elided before the model could edit, forcing re-read loops.\n compactKeepTurns: 8,\n // Off. `'notice'` appended \"re-read before reasoning\" to nearly every\n // successful edit, almost always naming the file the model had just edited,\n // which made the edit tools noisier than an equivalent shell write. Edits\n // stay safe without it: `old_string` must resolve against current bytes, and\n // the mutating tools mark read-state `elided` so a re-read serves the full\n // body. `'stub'` would re-bill the prompt cache per newly-stale read (13\n // full-prefix invalidations on a 129-turn trace); see\n // `test-inference/cache-stability.inference.test.ts`.\n elideStaleReads: false,\n // Keep media inline; age it out only near the provider's hard request\n // limits. A fixed message window (`mediaKeepTurns`, 8 messages) rewrote an earlier\n // message every time a screenshot aged out, re-billing the whole\n // conversation: replayed on a 470k-token GUI session, it caused 158 full\n // prompt-cache resets, against ~1.6k tokens per screenshot to keep it as\n // a cache read. Aged media stays recoverable via\n // `read_file path=\"attachment://<hash>\"`.\n mediaBudget: DEFAULT_MEDIA_BUDGET,\n // Shrink Retina screenshots once, at entry (cache-stable), to 2000 px: a\n // request with more than 20 images is rejected if any one is larger, and\n // the inline budget routinely carries more than 20.\n maxImageEdge: DEFAULT_MAX_IMAGE_EDGE,\n // Loop-native auto-compaction: when a turn's effective input crosses 85% of\n // the model's context window, the loop summarizes the older turns mid-run\n // (real LLM summary + file/skill restoration) and continues. Compaction\n // happens inside the run (mid tool-loop AND end-of-run) with no double-fire.\n // The TUI/GUI inject the\n // picked model's `contextWindow` (and honor the user's enable/threshold\n // settings) per run; without a window source the trigger is inert.\n autoCompact: { threshold: 0.85 },\n toolDisclosure: 'lazy',\n readFileDefaults: { maxBytes: 32 * 1024 },\n persistThreshold: 8 * 1024,\n persistExcludeTools: DEFAULT_PERSIST_EXCLUDE_TOOLS,\n // `requireReadBeforeEdit` stays off (the library default). `edit`'s\n // `old_string` anchor already refuses edits against bytes that aren't on\n // disk, and the gate's \"shell reads don't register\" refusals taught models\n // to route edits through shell/python scripts to avoid it.\n requireReadBeforeEdit: false,\n // `shellToolNudges` — one factual line pointing at `read_file` / `grep` /\n // `edit` when a shell call does their job, at most twice per kind per\n // session. Rides the shell result itself (append-only), so it costs no\n // system-prompt bytes and appears only when the shell habit does.\n shellToolNudges: true,\n // `dedupTools` for the read-only discovery tools, in `replay` mode — an\n // identical re-issue replays the prior result from cache instead of\n // re-walking the filesystem. Free (no behavior change beyond cache replay)\n // and breaks the cheap-replay loop shape (model probing the same\n // `glob`/`grep`/`list_files` repeatedly). The hasher covers all input\n // fields so flag/limit changes bust the cache.\n dedupTools: {\n glob: discoveryDedupHash,\n grep: discoveryDedupHash,\n list_files: discoveryDedupHash,\n },\n // `repeatGuard` — identical-call detector. Defaults track the built-in\n // `shell` and `read_file` tools. Shell keeps the generic 4/8 block/abort\n // thresholds; `read_file` uses 6/12 so normal scaffolding re-visits don't\n // trip as quickly. The abort is the only layer that stops a model which\n // ignores the `Blocked:` reason and keeps retrying the same payload.\n //\n // `mode: 'both'` because strict chain counting is trivially defeated: ANY\n // intervening call resets it, and a spinning model always emits some. A real\n // run spent 123 turns and 10M input tokens re-issuing ONE byte-identical\n // `read_file` 59 times, alternating it with `grep`s on the same path; the\n // longest consecutive streak was 5 against a threshold of 6, so nothing ever\n // fired and the user had to cancel. The interleaved calls were `dedupTools`\n // cache replays, i.e. free for the model and (by design) chain boundaries\n // here — the guard was structurally unreachable. The sliding window counts\n // the same payload among that tool's own recent calls, so dilution slows the\n // trip instead of erasing it: the same run aborts at call 28 of 123.\n repeatGuard: { mode: 'both', windowSize: 40 },\n // `thinkingDecay` — after turn 10 the model has converged or is thrashing;\n // either way extended thinking earns less per token. STEP form (no\n // `factor`), not geometric: `thinking.budget_tokens` is part of Anthropic's\n // prompt-cache key, so every distinct budget a schedule emits is a FULL\n // cache re-bill — `cacheRead: 0`, scaffold included (measured; pinned in\n // `test-inference/cache-stability.inference.test.ts`). Geometric\n // halving (8192 → 4096 → 2048 → 1024) would cost three of those per run; one step\n // straight to the floor costs exactly one. No-op unless a thinking budget\n // is set.\n thinkingDecay: { afterTurn: 10, floor: 1024 },\n // One-hour prompt cache. These profiles only back interactive hosts, where\n // a human pause past five minutes would otherwise re-bill the whole\n // conversation at the cache-write rate (measured: ~80 such resets in one\n // long session). 1h writes cost 2x base input against 1.25x, which the\n // avoided resets repay. Verified live in\n // `test-inference/cache-stability.inference.test.ts` § extended TTL.\n // OAuth defaults to 1h; this extends it to API-key auth. Bedrock\n // degrades to the default TTL.\n cacheTtl: '1h',\n} as const\n\n/**\n * Build agent — the default profile. Full read/write/shell access plus\n * subagent spawning. Its tools are those of the `basic` preset plus `glob`,\n * `grep`, `web_search` and `fetch_url`.\n *\n * The system prompt is the cwd-less variant of {@link buildBuildSystem};\n * the TUI overrides it per-session with an env-anchored composition so\n * the prompt carries the current project root + date. SDK consumers\n * calling `createAgent({ ...BUILD_AGENT.preset })` directly get the\n * cwd-less prompt and can wrap their own env section.\n */\nexport const BUILD_AGENT: AgentProfile = {\n id: 'build',\n label: 'Build',\n description: 'full tool access: read, write, edit, shell, web search, and spawn subagents',\n accent: 'accent',\n // Shares an agent with Plan: a switch between them keeps the prompt cache.\n family: 'build',\n // `composePresets` layers `createTodoTools()` on top so the model gets\n // `todowrite` / `todoread` plus their per-run write budget. Top-level\n // runs share a persistent session slot (continuity across prompts);\n // subagent runs get their own isolated slot. Auto-clears when every\n // item is `completed`, so a fresh prompt after a successful batch\n // starts clean. See `src/chat/todos.ts` for the keying / hygiene\n // contract.\n //\n // The composition is per-tool-name deep-merge, so a downstream host\n // re-composing this profile with its own per-tool overrides gets the\n // expected last-wins-on-collision behavior.\n //\n // Plan mode deliberately omits todos — a read-only exploration session\n // has nothing to checkpoint.\n preset: composePresets(\n definePreset({\n name: 'build',\n system: buildBuildSystem(),\n behavior: { ...SHARED_BEHAVIOR },\n // `persist: true` shares the parent's session with every child agent so a\n // reloaded TUI can reconstruct the subagent tree from `session.turns`.\n tools: { ...BUILD_TOOLS, spawn: createSpawnTool({ persist: true }) },\n }),\n createTodoTools(),\n ),\n}\n\n/**\n * Codemode — Build's tools, reached through scripts. `codemode` in\n * `'only'` mode: every native tool a script can call leaves the wire and is\n * declared inside the `codemode` description, so the model works in scripts\n * by default. `spawn` is callable from scripts, so it leaves the wire too.\n * Tools scripts cannot call stay declared. Shown only while the Codemode\n * setting is on.\n */\nexport const BUILD_CODEMODE_AGENT: AgentProfile = {\n id: 'build-codemode',\n label: 'Codemode',\n description: 'Build, but the model works through scripts that call the tools in a sandbox',\n accent: 'accent',\n requiresSetting: 'codemode',\n preset: composePresets(\n definePreset({\n name: 'build-codemode',\n system: buildBuildSystem(),\n behavior: { ...SHARED_BEHAVIOR, codemode: { mode: 'only' } },\n tools: { ...BUILD_TOOLS, spawn: createSpawnTool({ persist: true }) },\n }),\n createTodoTools(),\n ),\n}\n\n/**\n * Plan's rules as a switch note: read-only, investigate, then propose. A\n * condensed form of its prompt (`buildPlanSystem`), for a session whose\n * agent was built with another prompt (Build's, in their family).\n */\nfunction planSwitchNote({ tools, allowInteraction }: ProfileSwitchNoteContext): string {\n const everyProfile = allowInteraction ? '`present_plan`, `ask_user`, skills, MCP tools' : 'skills, MCP tools'\n const steps = allowInteraction\n ? `1. Explore: read the relevant code, find existing functions and patterns to reuse. Check current docs via web search when the plan depends on external libraries.\n2. Ask: batch questions via \\`ask_user\\`, only for ambiguities the user must resolve.\n3. Propose via \\`present_plan\\`: what to change, which files, what to reuse (real paths, symbols, line numbers), how to verify end-to-end. Recommend one approach, not every alternative.\n\n- Every plan is delivered via \\`present_plan\\`, whatever its size; prose is not a deliverable.\n- \\`ask_user\\` when the answer changes the plan; batch related questions into one call.\n- Don't ask what the code can tell you.`\n : `1. Explore: read the relevant code, find existing functions and patterns to reuse. Check current docs via web search when the plan depends on external libraries.\n2. Propose: write the plan in your final response: what to change, which files, what to reuse (real paths, symbols, line numbers), how to verify end-to-end. Recommend one approach, not every alternative. Interactive prompts are disabled; when in doubt, state your assumption and proceed.`\n return `# Plan mode (read-only)\nYou are now in PLAN mode: investigate and design a proposal. You CANNOT modify files, run commands, or spawn subagents until the user switches you out of Plan mode. Allowed tools: ${tools.map(name => `\\`${name}\\``).join(', ')}, plus the tools every mode has (${everyProfile}). Any other call is refused.\n\n${steps}\n- Search first (\\`grep\\` / \\`glob\\`), then \\`read_file\\` the specific file. Don't re-read files or re-run searches whose output is still visible above.\n\nThe user switches to Build mode to execute ${allowInteraction ? 'an approved' : 'the'} plan.`\n}\n\n/**\n * Plan agent — read-only exploration mode. Locked down to file reading and\n * codebase search; no shell, no edits, no spawning. The system prompt\n * frames the conversation as planning rather than execution so the model\n * outputs proposals instead of attempting mutations.\n *\n * In Build's family: a session switching between them keeps Build's agent,\n * a gate enforces Plan's tools and its rules arrive as a note.\n */\nexport const PLAN_AGENT: AgentProfile = {\n id: 'plan',\n label: 'Plan',\n description: 'read-only: explore, analyze, search the web, and propose without modifying anything',\n accent: 'model',\n family: 'build',\n switchNote: planSwitchNote,\n preset: definePreset({\n name: 'plan',\n system: buildPlanSystem(),\n behavior: { ...SHARED_BEHAVIOR },\n tools: READ_ONLY_TOOLS,\n }),\n}\n\n/**\n * Default registry of the built-in profiles. Insertion order = picker order.\n * Hosts that want only one profile can pass `{ agents: { build: BUILD_AGENT } }`,\n * or a fully custom registry of their own profiles.\n */\nexport const BUILTIN_AGENTS: AgentRegistry = {\n 'build': BUILD_AGENT,\n 'build-codemode': BUILD_CODEMODE_AGENT,\n 'plan': PLAN_AGENT,\n}\n\n/** Id of the profile activated on first launch when nothing is persisted. */\nexport const DEFAULT_AGENT_ID = 'build'\n\n/**\n * Resolve an agent id against a registry with sensible fallbacks: the\n * requested id wins when present, then `defaultId`, then the first key.\n * Returns `null` when the registry is empty (host misconfiguration —\n * the caller should surface this rather than silently failing).\n *\n * @param registry - The agent registry to search.\n * @param requestedId - Preferred profile id, such as the persisted last agent.\n * @param defaultId - Fallback profile id used when the requested one is missing.\n */\nexport function resolveAgentId(\n registry: AgentRegistry,\n requestedId: string | undefined,\n defaultId: string | undefined,\n): string | null {\n if (requestedId && registry[requestedId])\n return requestedId\n if (defaultId && registry[defaultId])\n return defaultId\n const first = Object.keys(registry)[0]\n return first ?? null\n}\n\n/**\n * Wrap a single `Preset` into a single-profile registry. Used by\n * `resolveConfig` when the host passed `preset` without `agents`. The\n * registry has one entry, so the picker has only one option.\n *\n * @param preset - The preset to wrap. Its `name` becomes the profile label when set.\n */\nexport function singleAgentRegistry(preset: Preset): AgentRegistry {\n return {\n default: {\n id: 'default',\n label: typeof preset.name === 'string' && preset.name.length > 0 ? preset.name : 'Default',\n description: 'host-provided preset',\n preset,\n accent: 'accent',\n },\n }\n}\n","/**\n * Session lifecycle — the one way a session is created, forked, and deleted.\n *\n * Every surface (daemon, embedded GUI host, TUI local runtime, orchestration,\n * ACP) goes through these, so a field stamped at creation, carried by a fork,\n * or reclaimed on delete is decided HERE once instead of drifting per host.\n * Per-session state such as an environment lands in exactly these functions.\n *\n * Deliberately host-agnostic: nothing here knows about live agents. Stopping a\n * running agent before a delete is the host's job (`ChatHost.deleteSession`,\n * the TUI's teardown); this module owns the persisted data only.\n *\n * `createMainSession` layers the orchestration role on top; it is here rather\n * than in the host because the TUI's local runtime has no host and must still\n * be able to materialize the main (delivery of events does need the host; the\n * session itself does not).\n */\nimport type { Session, SessionData, SessionStore } from '../session/index'\nimport type { ProviderKey } from './auth'\nimport type { SessionEnvironment } from './environment'\nimport type { ModelFallbackTarget } from './model-chain-metadata'\nimport { randomUUID } from 'node:crypto'\nimport { basename } from 'node:path'\nimport { editSessionMetadata } from '../extension-authoring'\nimport { cleanupPersistedSession, rehomePersistedStubs, resolveLegacyPersistDir, resolveLegacySessionDir, resolvePersistDir, resolveSessionDir } from '../loop-persistence'\nimport { createSession } from '../session/index'\nimport { DEFAULT_AGENT_ID } from './agents'\nimport { ENVIRONMENT_METADATA_KEY, environmentMetadata, LOCAL_ENVIRONMENT, readSessionEnvironment } from './environment'\nimport { modelChainMetadata, SESSION_ACCOUNT_METADATA_KEY } from './model-chain-metadata'\nimport { MAIN_SESSION_TITLE, SESSION_ROLE_KEY, SIDE_OF_KEY } from './orchestration'\nimport { projectRootOf } from './project-root'\nimport { spaceMetadata } from './spaces'\nimport { truncateTurnsAt } from './turn-operations'\n\n// ---------------------------------------------------------------------------\n// Create\n// ---------------------------------------------------------------------------\n\n/** Fields stamped on a new session at creation. */\nexport interface CreateSessionInStoreParams {\n /**\n * Tool-execution directory; `projectRoot` is derived from it. `''` creates a\n * DIRECTORY-LESS session (the GUI's \"Chats\" bucket): no project, and\n * `resolveSessionCwd` anchors tool execution at the user's HOME at run time.\n */\n cwd: string\n /** Explicit id. Omitted → the store's generator, else a random UUID. */\n id?: string\n /** Initial session title. */\n title?: string\n /** Profile the session starts in. Defaults to `DEFAULT_AGENT_ID` (`build`). */\n agentId?: string\n /** Provider the session starts on. Applied together with `modelId`. */\n providerKey?: string\n /** Model the session starts on. Applied together with `providerKey`. */\n modelId?: string\n /** The stored account of `providerKey` to run on (credential id); omitted = its active account. */\n account?: string\n /** Fallback targets after the primary, stored as the session's model chain. */\n fallbacks?: readonly ModelFallbackTarget[]\n /** Mark as an extension-authoring session (auto-activates the docs skill). */\n extensionEdit?: boolean\n /** Mark as a skill-authoring session for the named skill (dir = `cwd`). */\n skillEdit?: { name: string }\n /**\n * Where the session's work happens (see `environment.ts`) — resolved by the\n * host (`initialEnvironment`), which also provisions it. Omitted or local +\n * ready → nothing is stamped: an absent environment IS the local one.\n */\n environment?: SessionEnvironment\n /**\n * The space the session belongs to (see `./spaces`). Omitted or Default →\n * nothing is stamped: an absent space id IS the Default space.\n */\n spaceId?: string\n /**\n * Make it a `/side` session of this parent (`metadata.sideOf`, role\n * `'side'`): hidden from lists, deleted with the parent.\n */\n sideOf?: string\n /**\n * Extra host-specific metadata (e.g. a pinned-skills seed). Applied FIRST,\n * so every canonical key stamped here wins on collision.\n */\n metadata?: Record<string, unknown>\n}\n\n/**\n * Build a new, UNSAVED session with every creation-time field stamped:\n * `projectRoot` (git root of `cwd`), `cwd`, `agentId`, `title`, the model and\n * its fallback chain, the authoring markers, and the environment.\n *\n * Unsaved on purpose: the TUI's local runtime creates the session when a chat\n * opens but only persists it on the first run, so opening and abandoning\n * `/new` never leaves an empty row behind. Everyone else wants\n * {@link createSessionInStore}.\n *\n * `store` may be `undefined` for an in-memory-only session (ACP without a\n * store): same stamping, no persistence.\n *\n * @param store - Store the session saves to later, or `undefined` for an in-memory session.\n * @param params - Creation fields for the session.\n */\nexport async function newSession(store: SessionStore | undefined, params: CreateSessionInStoreParams): Promise<Session> {\n const id = params.id ?? (await store?.generateSessionId?.()) ?? randomUUID()\n // The project the session BELONGS to (a linked worktree's main checkout),\n // not the checkout it runs in — that stays `metadata.cwd`. Directory-less\n // sessions carry NO project: `''` must not become a project root, or the\n // session lands in a bogus project group.\n const projectRoot = params.cwd ? projectRootOf(params.cwd) : undefined\n const session = await createSession({\n id,\n ...(store ? { store } : {}),\n ...(projectRoot ? { projectRoot } : {}),\n // Always stamped: the agent is a per-session mode, and a session without\n // one would otherwise inherit whatever profile the opening window shows.\n agentId: params.agentId ?? DEFAULT_AGENT_ID,\n ...(params.metadata ? { metadata: { ...params.metadata } } : {}),\n })\n session.setMeta('cwd', params.cwd)\n if (params.title)\n session.setMeta('title', params.title)\n if (params.providerKey && params.modelId) {\n session.setMeta('providerKey', params.providerKey)\n session.setMeta('modelId', params.modelId)\n if (params.account)\n session.setMeta(SESSION_ACCOUNT_METADATA_KEY, params.account)\n const chain = modelChainMetadata(\n { providerKey: params.providerKey as ProviderKey, modelId: params.modelId, ...(params.account ? { account: params.account } : {}) },\n params.fallbacks,\n )\n if (chain)\n session.setMeta('modelChain', chain)\n }\n // Authoring markers target the session's own dir; a directory-less\n // session has nothing to author.\n if (params.cwd && params.extensionEdit)\n setMetaEntries(session, editSessionMetadata({ kind: 'extension', name: basename(params.cwd), dir: params.cwd }))\n if (params.cwd && params.skillEdit)\n setMetaEntries(session, editSessionMetadata({ kind: 'skill', name: params.skillEdit.name, dir: params.cwd }))\n const environment = params.environment\n if (environment && !(environment.kind === LOCAL_ENVIRONMENT.kind && environment.state === 'ready'))\n setMetaEntries(session, environmentMetadata(environment))\n setMetaEntries(session, spaceMetadata(params.spaceId))\n if (params.sideOf) {\n session.setMeta(SIDE_OF_KEY, params.sideOf)\n session.setMeta(SESSION_ROLE_KEY, 'side')\n }\n return session\n}\n\n/**\n * Create and persist a session; returns its id.\n *\n * @param store - Store to persist the session in.\n * @param params - Creation fields for the session.\n */\nexport async function createSessionInStore(store: SessionStore, params: CreateSessionInStoreParams): Promise<string> {\n const session = await newSession(store, params)\n await session.save()\n return session.id\n}\n\n/**\n * Create the orchestration main session; returns its id.\n *\n * Directory-less on purpose. The main is GLOBAL — one per store, orchestrating\n * sessions in many directories — so binding it to whichever cwd happened to be\n * active when the toggle flipped put it in that project's group and hid it\n * from every other project's list. With no cwd it sits in the \"Chats\" bucket\n * and its own tools run from HOME, which is the only directory that is always\n * the user's and never a project's.\n *\n * @param store - Store to persist the main session in.\n */\nexport async function createMainSession(store: SessionStore): Promise<string> {\n const session = await newSession(store, { cwd: '', title: MAIN_SESSION_TITLE })\n session.setMeta(SESSION_ROLE_KEY, 'main')\n await session.save()\n return session.id\n}\n\n// ---------------------------------------------------------------------------\n// Fork\n// ---------------------------------------------------------------------------\n\n/**\n * Metadata a fork inherits from its parent: where it runs, the model\n * surface it runs on, and its space. Everything else (todos, archive flag, orchestration\n * role, artifacts…) belongs to the parent's history, not the new branch.\n * The environment is inherited too, but SHARED (see `forkSession`).\n */\nconst FORK_INHERITED_METADATA_KEYS = ['cwd', 'providerKey', 'modelId', SESSION_ACCOUNT_METADATA_KEY, 'modelChain', 'effort', 'modelOptions', 'spaceId'] as const\n\n/** What a fork reads from its parent — satisfied by `SessionData` and a live `Session`. */\nexport type ForkSource = Pick<SessionData, 'id' | 'turns' | 'runs' | 'metadata' | 'agentId' | 'projectRoot'>\n\n/** Inputs to {@link forkSession}. */\nexport interface ForkSessionParams {\n /**\n * Parent session — an id to load from `store`, or the parent itself (a\n * host holding the live `Session` passes it so unsaved turns fork too).\n */\n source: ForkSource | string\n /** Last turn the fork keeps (inclusive); the prefix is made protocol-clean. */\n turnId: string\n /**\n * Root of the per-session artifact tree (`<userDir>/sessions/<id>/`) that\n * the parent's persisted-output stubs point into. The fork gets its own\n * copies so deleting the parent can't strand the fork's content.\n */\n userDir: string\n /** Title override. Default: the parent's explicit title, if any. */\n title?: string\n}\n\n/** Outcome of {@link forkSession}: the new session id, or the reason the fork was refused. */\nexport type ForkSessionResult\n = | { ok: true, id: string }\n | { ok: false, reason: 'source-not-found' | 'turn-not-found' | 'environment-not-ready' }\n\n/**\n * The error a user sees when a fork is refused, the same on every surface.\n *\n * @param reason - The refusal reason from {@link forkSession}.\n * @param sessionId - Id of the session that was to be forked.\n * @param turnId - Id of the requested last turn.\n */\nexport function describeForkFailure(reason: Extract<ForkSessionResult, { ok: false }>['reason'], sessionId: string, turnId: string): string {\n switch (reason) {\n case 'source-not-found':\n return `Session \"${sessionId}\" not found.`\n case 'environment-not-ready':\n return `Session \"${sessionId}\" can't be forked until its environment is ready.`\n case 'turn-not-found':\n return `Turn \"${turnId}\" not found in session \"${sessionId}\".`\n }\n}\n\n/**\n * Create and persist a new session whose history is the parent's turns up to\n * and including `turnId`. The parent is untouched.\n *\n * Carries: the protocol-clean turn prefix, the run records those turns\n * reference (so run stats survive and the next run can't re-mint `run_1`\n * over an existing reference), persisted-output blobs (copied and re-pointed),\n * `projectRoot` (a fork belongs to its parent's project, wherever it was\n * forked from), `agentId`, {@link FORK_INHERITED_METADATA_KEYS}, and the\n * parent's environment — marked not-owned: the fork runs in the same\n * worktree / sandbox but deleting it never tears that down. Refused while\n * the parent's environment isn't `ready`: nothing would provision the fork's\n * copy, and provisioning it would mint an environment nobody owns.\n *\n * @param store - Store to load the parent from and persist the fork in.\n * @param params - Parent, last turn to keep, artifact root and optional title.\n */\nexport async function forkSession(store: SessionStore, params: ForkSessionParams): Promise<ForkSessionResult> {\n const source = typeof params.source === 'string' ? await store.load(params.source) : params.source\n if (!source)\n return { ok: false, reason: 'source-not-found' }\n const turns = truncateTurnsAt(source.turns, params.turnId, { inclusive: true })\n if (!turns || turns.length === 0)\n return { ok: false, reason: 'turn-not-found' }\n\n if (source.metadata[ENVIRONMENT_METADATA_KEY] !== undefined && readSessionEnvironment(source.metadata).state !== 'ready')\n return { ok: false, reason: 'environment-not-ready' }\n\n const referencedRunIds = new Set(turns.flatMap(turn => turn.runId ? [turn.runId] : []))\n // Shallow-clone so the fork's later mutations never bleed into the parent.\n const runs = source.runs.filter(run => referencedRunIds.has(run.id)).map(run => ({ ...run }))\n\n const inherited: Record<string, unknown> = {}\n for (const key of FORK_INHERITED_METADATA_KEYS) {\n if (source.metadata[key] !== undefined)\n inherited[key] = source.metadata[key]\n }\n if (source.metadata[ENVIRONMENT_METADATA_KEY] !== undefined)\n Object.assign(inherited, environmentMetadata({ ...readSessionEnvironment(source.metadata), owned: false }))\n const title = params.title ?? (typeof source.metadata.title === 'string' ? source.metadata.title : undefined)\n\n const fork = await createSession({\n id: (await store.generateSessionId?.()) ?? randomUUID(),\n store,\n agentId: source.agentId ?? DEFAULT_AGENT_ID,\n ...(source.projectRoot ? { projectRoot: source.projectRoot } : {}),\n metadata: { ...inherited, ...(title ? { title } : {}) },\n })\n\n /*\n * Persisted-output stubs embed the blob's absolute per-session path; left\n * as-is, the fork would lose that content when the parent is deleted (its\n * blob dir goes with it). Rehome both layouts: sessions from before the\n * session-first layout embed `<userDir>/tool-results/<id>/`, newer ones\n * `<userDir>/sessions/<id>/tool-results/`. The legacy pass matters most —\n * the parent's delete sweeps exactly that dir.\n */\n const forkPersistDir = resolvePersistDir({ userDir: params.userDir, sessionId: fork.id })\n let rehomed = await rehomePersistedStubs(turns, resolvePersistDir({ userDir: params.userDir, sessionId: source.id }), forkPersistDir)\n const legacyPersistDir = resolveLegacyPersistDir({ userDir: params.userDir, sessionId: source.id })\n if (legacyPersistDir !== undefined)\n rehomed = await rehomePersistedStubs(rehomed, legacyPersistDir, forkPersistDir)\n\n fork.setTurns(rehomed)\n fork.setRuns(runs)\n await fork.save()\n return { ok: true, id: fork.id }\n}\n\n// ---------------------------------------------------------------------------\n// Delete\n// ---------------------------------------------------------------------------\n\n/**\n * Delete a session's persisted data: the store row, then every on-disk\n * artifact it owns — the session-first tree (`<userDir>/sessions/<id>/`:\n * blobs, task logs, MCP warnings) plus the two pre-session-first locations\n * migration leaves behind. Blob cleanup is best-effort and never throws; it\n * runs AFTER the row delete so a cleanup failure can't leave a live row\n * pointing at removed blobs.\n *\n * Does NOT stop a live agent — hosts must retire it first (see\n * `ChatHost.deleteSession`), or its next save resurrects the row.\n *\n * @param store - Store holding the session row.\n * @param sessionId - Id of the session to delete.\n * @param opts - `userDir` is the root of the per-session artifact tree.\n */\nexport async function deleteSessionData(store: SessionStore, sessionId: string, opts: { userDir: string }): Promise<void> {\n await store.delete(sessionId)\n const dirs = [\n resolveSessionDir({ userDir: opts.userDir, sessionId }),\n resolveLegacyPersistDir({ userDir: opts.userDir, sessionId }),\n resolveLegacySessionDir({ userDir: opts.userDir, sessionId }),\n ]\n await Promise.all(dirs.flatMap(dir => dir === undefined ? [] : [cleanupPersistedSession(dir)]))\n}\n\nfunction setMetaEntries(session: Session, entries: Record<string, unknown>): void {\n for (const [key, value] of Object.entries(entries))\n session.setMeta(key, value)\n}\n","/** The values of the `durability` setting (`Settings.durability` in `zidane/chat`, `--durability` in the CLIs). */\n\n/**\n * Whether runs survive a crash: `on` keeps a checkpoint of each run and\n * resumes an interrupted one by itself, `resume-ask` keeps the checkpoint\n * and asks before resuming, `off` keeps none (an interrupted run's tool\n * calls are reported interrupted on the next run).\n */\nexport type DurabilityMode = 'on' | 'resume-ask' | 'off'\n\nconst MODES: ReadonlySet<string> = new Set<DurabilityMode>(['on', 'resume-ask', 'off'])\n\n/** Default durability: checkpoint, and ask before resuming. */\nexport const DEFAULT_DURABILITY: DurabilityMode = 'resume-ask'\n\nexport function isDurabilityMode(value: unknown): value is DurabilityMode {\n return typeof value === 'string' && MODES.has(value)\n}\n"],"mappings":"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"}
|