zidane 9.1.9 → 9.1.13

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.
Files changed (74) hide show
  1. package/dist/{acp-Bha_yCgU.js → acp-Cz5mFUjB.js} +3 -3
  2. package/dist/{acp-Bha_yCgU.js.map → acp-Cz5mFUjB.js.map} +1 -1
  3. package/dist/acp-cli.js +4 -4
  4. package/dist/acp.js +1 -1
  5. package/dist/{auth-store-port-BvxVXx9g.js → auth-store-port-CDJa8Q8U.js} +2 -2
  6. package/dist/{auth-store-port-BvxVXx9g.js.map → auth-store-port-CDJa8Q8U.js.map} +1 -1
  7. package/dist/{auto-update-restart-CF3uPnr4.js → auto-update-restart-B4pU7Qgf.js} +10 -10
  8. package/dist/{auto-update-restart-CF3uPnr4.js.map → auto-update-restart-B4pU7Qgf.js.map} +1 -1
  9. package/dist/chat.js +9 -9
  10. package/dist/{cli-shared-C17TUz6H.js → cli-shared-BVtiiICW.js} +3 -3
  11. package/dist/{cli-shared-C17TUz6H.js.map → cli-shared-BVtiiICW.js.map} +1 -1
  12. package/dist/{credentials-BZP-MgtZ.js → credentials-axv5VvMR.js} +2 -2
  13. package/dist/{credentials-BZP-MgtZ.js.map → credentials-axv5VvMR.js.map} +1 -1
  14. package/dist/daemon-cli.js +2 -2
  15. package/dist/{discovery-Czp7bZZP.js → discovery-D7qVZWzq.js} +4 -4
  16. package/dist/{discovery-Czp7bZZP.js.map → discovery-D7qVZWzq.js.map} +1 -1
  17. package/dist/{engine-CDuOhwbt.js → engine-DjM5NPIf.js} +3 -3
  18. package/dist/{engine-CDuOhwbt.js.map → engine-DjM5NPIf.js.map} +1 -1
  19. package/dist/{executor-CoHqHsCp.js → executor-DlW7qAeN.js} +2 -2
  20. package/dist/{executor-CoHqHsCp.js.map → executor-DlW7qAeN.js.map} +1 -1
  21. package/dist/extensions/testing.js +2 -2
  22. package/dist/{extensions-BEaWxX4e.js → extensions-D7DeZNpn.js} +6 -6
  23. package/dist/{extensions-BEaWxX4e.js.map → extensions-D7DeZNpn.js.map} +1 -1
  24. package/dist/extensions.js +5 -5
  25. package/dist/{google-Bzt7uvy9.js → google-D1zjAViO.js} +2 -2
  26. package/dist/{google-Bzt7uvy9.js.map → google-D1zjAViO.js.map} +1 -1
  27. package/dist/{host-DkqB2P7L.js → host-Ds3RDlG5.js} +9 -9
  28. package/dist/{host-DkqB2P7L.js.map → host-Ds3RDlG5.js.map} +1 -1
  29. package/dist/{host-signals-Ca_oUeYl.js → host-signals-C8x5Yso0.js} +5 -5
  30. package/dist/{host-signals-Ca_oUeYl.js.map → host-signals-C8x5Yso0.js.map} +1 -1
  31. package/dist/{host-version-BwON7XSr.js → host-version-BGZumnpZ.js} +2 -2
  32. package/dist/{host-version-BwON7XSr.js.map → host-version-BGZumnpZ.js.map} +1 -1
  33. package/dist/index.js +5 -5
  34. package/dist/ipc/remote.js +3 -3
  35. package/dist/{ipc-Dlqz_xIe.js → ipc-DGfuNEfR.js} +4 -4
  36. package/dist/{ipc-Dlqz_xIe.js.map → ipc-DGfuNEfR.js.map} +1 -1
  37. package/dist/ipc.js +4 -4
  38. package/dist/{links-B5Dz7jku.js → links-D_1RhPh5.js} +8 -8
  39. package/dist/{links-B5Dz7jku.js.map → links-D_1RhPh5.js.map} +1 -1
  40. package/dist/{loaded-entry-BGN69Fns.js → loaded-entry-CLva6GQT.js} +2 -2
  41. package/dist/{loaded-entry-BGN69Fns.js.map → loaded-entry-CLva6GQT.js.map} +1 -1
  42. package/dist/{memory-ZWFqa7di.js → memory-CaFXUvQs.js} +15 -15
  43. package/dist/{memory-ZWFqa7di.js.map → memory-CaFXUvQs.js.map} +1 -1
  44. package/dist/{path-display-B3Jeqx9E.js → path-display-ZIWdFaTI.js} +3 -3
  45. package/dist/{path-display-B3Jeqx9E.js.map → path-display-ZIWdFaTI.js.map} +1 -1
  46. package/dist/providers/google.js +1 -1
  47. package/dist/{providers-ChifjOYS.js → providers-CsYrxuX1.js} +2 -2
  48. package/dist/{providers-ChifjOYS.js.map → providers-CsYrxuX1.js.map} +1 -1
  49. package/dist/providers.js +1 -1
  50. package/dist/{registry-mmAX5qBS.js → registry-BY45AOA-.js} +2 -2
  51. package/dist/{registry-mmAX5qBS.js.map → registry-BY45AOA-.js.map} +1 -1
  52. package/dist/{registry-TW3C94iD.js → registry-DTotchq6.js} +2 -2
  53. package/dist/{registry-TW3C94iD.js.map → registry-DTotchq6.js.map} +1 -1
  54. package/dist/{registry-CYEN4C4M.js → registry-Dl6261Xh.js} +2 -2
  55. package/dist/{registry-CYEN4C4M.js.map → registry-Dl6261Xh.js.map} +1 -1
  56. package/dist/{remote-hosts-Ctg9fwQY.js → remote-hosts-usqMiMbs.js} +4 -4
  57. package/dist/{remote-hosts-Ctg9fwQY.js.map → remote-hosts-usqMiMbs.js.map} +1 -1
  58. package/dist/{setting-DPim5iCP.js → setting-DzTMJIV3.js} +2 -2
  59. package/dist/{setting-DPim5iCP.js.map → setting-DzTMJIV3.js.map} +1 -1
  60. package/dist/{settings-keys-BIWy4T_0.js → settings-keys-DDrfnBHi.js} +2 -2
  61. package/dist/{settings-keys-BIWy4T_0.js.map → settings-keys-DDrfnBHi.js.map} +1 -1
  62. package/dist/{system-freeze-CEuTQzNu.js → system-freeze-CY4Hc9WX.js} +2 -2
  63. package/dist/{system-freeze-CEuTQzNu.js.map → system-freeze-CY4Hc9WX.js.map} +1 -1
  64. package/dist/tui.js +15 -15
  65. package/dist/{version-CU8ppdCQ.js → version-Bu3Nc8-U.js} +2 -2
  66. package/dist/{version-CU8ppdCQ.js.map → version-Bu3Nc8-U.js.map} +1 -1
  67. package/dist/{wire-GwOPpxJg.js → wire-BKcREhuL.js} +2 -2
  68. package/dist/{wire-GwOPpxJg.js.map → wire-BKcREhuL.js.map} +1 -1
  69. package/dist/{ws-CqlZkB4w.js → ws-DriNRyP-.js} +4 -4
  70. package/dist/{ws-CqlZkB4w.js.map → ws-DriNRyP-.js.map} +1 -1
  71. package/dist/{ws-client-Bn_asPwE.js → ws-client-PZdbO0fj.js} +2 -2
  72. package/dist/{ws-client-Bn_asPwE.js.map → ws-client-PZdbO0fj.js.map} +1 -1
  73. package/docs/RELEASE.md +1 -1
  74. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  import { i as writeJsonFileAtomic, t as OWNER_ONLY } from "./atomic-write-BewQOXlD.js";
2
- import { C as writeCredentialEntry, u as readCredentialEntry } from "./credentials-BZP-MgtZ.js";
2
+ import { C as writeCredentialEntry, u as readCredentialEntry } from "./credentials-axv5VvMR.js";
3
3
  import { t as HostRegistryError } from "./types-CiUp8oom.js";
4
4
  import { i as urlCarriesSecrets, n as displayHostUrl, r as parseRemoteHostUrl, t as RemoteHostError } from "./url-znC3tmKE.js";
5
5
  import { resolve } from "node:path";
@@ -240,4 +240,4 @@ function createHostRegistry(options) {
240
240
  //#endregion
241
241
  export { probeUnsavedHost as i, hostCredentialKey as n, hostsFilePath as r, createHostRegistry as t };
242
242
 
243
- //# sourceMappingURL=registry-CYEN4C4M.js.map
243
+ //# sourceMappingURL=registry-Dl6261Xh.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"registry-CYEN4C4M.js","names":[],"sources":["../src/chat/hosts/registry.ts"],"sourcesContent":["/**\n * The client's saved hosts: `<dataDir>/hosts.json` (0600, atomic writes) for\n * the entries, the credentials store (`credentials.json`, entry\n * `hosts:<id>`) for their tokens, so tokens sit with every other secret and\n * never in the listable file.\n *\n * Every call reads the files fresh: TUI and GUI windows on one machine share\n * them. Connecting is injected (`probe`): `src/chat` cannot reach the\n * WebSocket client, `createWsHostRegistry` (`zidane/ipc`) wires it.\n */\nimport type { HostEntry, HostProbe, HostProbeResult, HostTestResult } from './types'\nimport { existsSync, readFileSync } from 'node:fs'\nimport { resolve } from 'node:path'\nimport { OWNER_ONLY, writeJsonFileAtomic } from '../../atomic-write'\nimport { readCredentialEntry, writeCredentialEntry } from '../credentials'\nimport { HostRegistryError } from './types'\nimport { displayHostUrl, parseRemoteHostUrl, RemoteHostError, urlCarriesSecrets } from './url'\n\nconst FILE_VERSION = 1\n\ninterface HostsFile {\n version: typeof FILE_VERSION\n hosts: HostEntry[]\n}\n\n/**\n * Returns the path of the file that stores the saved hosts.\n *\n * @param dataDir - Application data directory.\n */\nexport function hostsFilePath(dataDir: string): string {\n return resolve(dataDir, 'hosts.json')\n}\n\n/**\n * Where a host's token lives in the credentials store.\n *\n * @param id - Host id.\n */\nexport function hostCredentialKey(id: string): string {\n return `hosts:${id}`\n}\n\n/** Saved remote hosts and their tokens, persisted under the data directory. */\nexport interface HostRegistry {\n /** Returns every saved host. */\n list: () => HostEntry[]\n /** Returns the saved host with this id, or `undefined`. */\n get: (id: string) => HostEntry | undefined\n /**\n * Verify the host (it must answer as a zidane-server with this token),\n * then save it and its token. Adding a host already saved (same server\n * id) updates its URL and token in place, keeping its name unless one is\n * given. Throws {@link HostRegistryError}.\n */\n add: (input: { url: string, token: string, name?: string }) => Promise<HostEntry>\n /** Forget the entry and its token; `false` when it wasn't saved. */\n remove: (id: string) => boolean\n /** Renames a saved host. Throws {@link HostRegistryError} for an empty name, or when no saved host has this id. */\n rename: (id: string, name: string) => HostEntry\n /**\n * Move a saved host to a new URL, with its saved token: saved only once\n * the same server (same id) answers there. Name and token are kept.\n * `lastConnectedAt` and the server's name and version are refreshed.\n * Throws {@link HostRegistryError} (`identity` when another server answers).\n */\n setUrl: (id: string, url: string) => Promise<HostEntry>\n /** Connect with the saved token and say who answered; refreshes `lastConnectedAt` on success. */\n test: (id: string) => Promise<HostTestResult>\n /** The saved token, for transports. Never display it. */\n token: (id: string) => string | undefined\n}\n\nfunction isEntry(value: unknown): value is HostEntry {\n const e = value as Partial<HostEntry> | null\n return !!e && typeof e === 'object' && typeof e.id === 'string' && typeof e.name === 'string' && typeof e.url === 'string' && typeof e.addedAt === 'number'\n}\n\n/** An entry saved before URLs were checked for credentials: read without them (it fails auth, the user re-adds it). */\nfunction withoutUrlSecrets(entry: HostEntry): HostEntry {\n const raw = entry.url\n return urlCarriesSecrets(raw) ? { ...entry, url: displayHostUrl(raw) } : entry\n}\n\nfunction readHosts(dataDir: string): HostEntry[] {\n const path = hostsFilePath(dataDir)\n if (!existsSync(path))\n return []\n try {\n const parsed = JSON.parse(readFileSync(path, 'utf8')) as Partial<HostsFile>\n return Array.isArray(parsed.hosts) ? parsed.hosts.filter(isEntry).map(withoutUrlSecrets) : []\n }\n catch {\n return []\n }\n}\n\nfunction writeHosts(dataDir: string, hosts: HostEntry[]): void {\n const file: HostsFile = { version: FILE_VERSION, hosts }\n writeJsonFileAtomic(hostsFilePath(dataDir), file, OWNER_ONLY)\n}\n\nfunction cleanName(name: string): string {\n const trimmed = name.trim()\n if (!trimmed)\n throw new HostRegistryError('invalid-name', 'A host needs a name.')\n return trimmed\n}\n\nfunction checkedUrl(raw: string): string {\n try {\n return parseRemoteHostUrl(raw)\n }\n catch (err) {\n throw new HostRegistryError('invalid-url', err instanceof RemoteHostError ? err.message : String(err))\n }\n}\n\n/** The URL and token an `add` (or an unsaved test) takes, checked; throws {@link HostRegistryError}. */\nexport function checkHostInput(input: { url: string, token: string }): { url: string, token: string } {\n const url = checkedUrl(input.url)\n const token = input.token.trim()\n if (!token)\n throw new HostRegistryError('invalid-token', 'A host needs a token (zidane server token create --name <device>).')\n return { url, token }\n}\n\n/** A probe's answer, refused unless a zidane-server answered. */\nfunction asServer(url: string, probed: HostProbeResult): HostProbeResult {\n if (probed.ok && probed.hostRef.kind !== 'server')\n return { ok: false, error: 'protocol', message: `${url} is a ${probed.hostRef.kind} host, not a zidane-server.` }\n return probed\n}\n\n/** The add form's \"test\": `add`'s checks and probe, saving nothing. A refused input or a failed probe comes back as an error result, not a throw. */\nexport async function probeUnsavedHost(probe: HostProbe, input: { url: string, token: string }): Promise<HostProbeResult> {\n let checked: { url: string, token: string }\n try {\n checked = checkHostInput(input)\n }\n catch (err) {\n if (err instanceof HostRegistryError)\n return { ok: false, error: err.code, message: err.message }\n throw err\n }\n return asServer(checked.url, await probe(checked))\n}\n\n/**\n * Creates a host registry backed by `hosts.json` and the credentials store in `options.dataDir`.\n *\n * @param options - Where to store hosts (`dataDir`), how to reach a host (`probe`), and an optional clock (`now`, default `Date.now`).\n * @example\n * ```ts\n * import { createHostRegistry } from 'zidane/chat'\n *\n * const hosts = createHostRegistry({\n * dataDir: '/home/me/.zidane',\n * probe: async () => ({ ok: false, error: 'unreachable', message: 'No transport wired.' }),\n * })\n * console.log(hosts.list())\n * ```\n */\nexport function createHostRegistry(options: { dataDir: string, probe: HostProbe, now?: () => number }): HostRegistry {\n const { dataDir, probe } = options\n const now = options.now ?? Date.now\n const list = (): HostEntry[] => readHosts(dataDir)\n const get = (id: string): HostEntry | undefined => list().find(h => h.id === id)\n const token = (id: string): string | undefined => {\n const cred = readCredentialEntry(dataDir, hostCredentialKey(id))\n return cred?.kind === 'apikey' && cred.value ? cred.value : undefined\n }\n const update = (id: string, patch: (entry: HostEntry) => HostEntry): HostEntry | undefined => {\n const hosts = list()\n const i = hosts.findIndex(h => h.id === id)\n if (i < 0)\n return undefined\n hosts[i] = patch(hosts[i]!)\n writeHosts(dataDir, hosts)\n return hosts[i]\n }\n\n return {\n list,\n get,\n token,\n async add(input) {\n const { url, token: secret } = checkHostInput(input)\n const name = input.name === undefined ? undefined : cleanName(input.name)\n const probed = asServer(url, await probe({ url, token: secret }))\n if (!probed.ok)\n throw new HostRegistryError(probed.error, probed.message)\n const at = now()\n const fresh: Pick<HostEntry, 'url' | 'lastConnectedAt' | 'hostKind' | 'serverName' | 'serverVersion'> = {\n url,\n lastConnectedAt: at,\n hostKind: 'server',\n serverName: probed.hostRef.name,\n ...(probed.serverVersion !== undefined ? { serverVersion: probed.serverVersion } : {}),\n }\n const id = probed.hostRef.id\n // Token first: an entry is never saved without its token.\n writeCredentialEntry(dataDir, hostCredentialKey(id), { kind: 'apikey', value: secret })\n const hosts = list()\n const existing = hosts.find(h => h.id === id)\n const entry: HostEntry = existing\n ? { ...existing, ...fresh, ...(name !== undefined ? { name } : {}) }\n : { id, name: name ?? probed.hostRef.name, addedAt: at, ...fresh }\n writeHosts(dataDir, existing ? hosts.map(h => (h.id === id ? entry : h)) : [...hosts, entry])\n return entry\n },\n remove(id) {\n const hosts = list()\n const kept = hosts.filter(h => h.id !== id)\n const had = kept.length !== hosts.length\n if (had)\n writeHosts(dataDir, kept)\n const hadToken = token(id) !== undefined\n writeCredentialEntry(dataDir, hostCredentialKey(id), undefined)\n return had || hadToken\n },\n rename(id, name) {\n const clean = cleanName(name)\n const entry = update(id, e => ({ ...e, name: clean }))\n if (!entry)\n throw new HostRegistryError('unknown-host', `No host \"${id}\".`)\n return entry\n },\n async setUrl(id, raw) {\n const url = checkedUrl(raw)\n const entry = get(id)\n const secret = entry ? token(id) : undefined\n if (!entry)\n throw new HostRegistryError('unknown-host', `No host \"${id}\".`)\n if (secret === undefined)\n throw new HostRegistryError('unknown-host', `No token saved for \"${entry.name}\": add it again.`)\n const probed = asServer(url, await probe({ url, token: secret }))\n if (!probed.ok)\n throw new HostRegistryError(probed.error, probed.message)\n if (probed.hostRef.id !== entry.id)\n throw new HostRegistryError('identity', `A different host (\"${probed.hostRef.name}\") answers at ${displayHostUrl(url)}, not \"${entry.name}\".`)\n const moved = update(id, e => ({\n ...e,\n url,\n lastConnectedAt: now(),\n serverName: probed.hostRef.name,\n ...(probed.serverVersion !== undefined ? { serverVersion: probed.serverVersion } : {}),\n }))\n if (!moved)\n throw new HostRegistryError('unknown-host', `No host \"${id}\".`)\n return moved\n },\n async test(id) {\n const entry = get(id)\n const secret = entry ? token(id) : undefined\n if (!entry || secret === undefined)\n return { ok: false, error: 'unknown-host', message: entry ? `No token saved for \"${entry.name}\": add it again.` : `No host \"${id}\".` }\n const probed = await probe({ url: entry.url, token: secret })\n if (!probed.ok)\n return probed\n if (probed.hostRef.id !== entry.id)\n return { ok: false, error: 'identity', message: `A different host (\"${probed.hostRef.name}\") answers at ${entry.url}. Remove \"${entry.name}\" and add it again if it was reinstalled.` }\n update(id, e => ({\n ...e,\n lastConnectedAt: now(),\n serverName: probed.hostRef.name,\n ...(probed.serverVersion !== undefined ? { serverVersion: probed.serverVersion } : {}),\n }))\n return probed\n },\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"registry-Dl6261Xh.js","names":[],"sources":["../src/chat/hosts/registry.ts"],"sourcesContent":["/**\n * The client's saved hosts: `<dataDir>/hosts.json` (0600, atomic writes) for\n * the entries, the credentials store (`credentials.json`, entry\n * `hosts:<id>`) for their tokens, so tokens sit with every other secret and\n * never in the listable file.\n *\n * Every call reads the files fresh: TUI and GUI windows on one machine share\n * them. Connecting is injected (`probe`): `src/chat` cannot reach the\n * WebSocket client, `createWsHostRegistry` (`zidane/ipc`) wires it.\n */\nimport type { HostEntry, HostProbe, HostProbeResult, HostTestResult } from './types'\nimport { existsSync, readFileSync } from 'node:fs'\nimport { resolve } from 'node:path'\nimport { OWNER_ONLY, writeJsonFileAtomic } from '../../atomic-write'\nimport { readCredentialEntry, writeCredentialEntry } from '../credentials'\nimport { HostRegistryError } from './types'\nimport { displayHostUrl, parseRemoteHostUrl, RemoteHostError, urlCarriesSecrets } from './url'\n\nconst FILE_VERSION = 1\n\ninterface HostsFile {\n version: typeof FILE_VERSION\n hosts: HostEntry[]\n}\n\n/**\n * Returns the path of the file that stores the saved hosts.\n *\n * @param dataDir - Application data directory.\n */\nexport function hostsFilePath(dataDir: string): string {\n return resolve(dataDir, 'hosts.json')\n}\n\n/**\n * Where a host's token lives in the credentials store.\n *\n * @param id - Host id.\n */\nexport function hostCredentialKey(id: string): string {\n return `hosts:${id}`\n}\n\n/** Saved remote hosts and their tokens, persisted under the data directory. */\nexport interface HostRegistry {\n /** Returns every saved host. */\n list: () => HostEntry[]\n /** Returns the saved host with this id, or `undefined`. */\n get: (id: string) => HostEntry | undefined\n /**\n * Verify the host (it must answer as a zidane-server with this token),\n * then save it and its token. Adding a host already saved (same server\n * id) updates its URL and token in place, keeping its name unless one is\n * given. Throws {@link HostRegistryError}.\n */\n add: (input: { url: string, token: string, name?: string }) => Promise<HostEntry>\n /** Forget the entry and its token; `false` when it wasn't saved. */\n remove: (id: string) => boolean\n /** Renames a saved host. Throws {@link HostRegistryError} for an empty name, or when no saved host has this id. */\n rename: (id: string, name: string) => HostEntry\n /**\n * Move a saved host to a new URL, with its saved token: saved only once\n * the same server (same id) answers there. Name and token are kept.\n * `lastConnectedAt` and the server's name and version are refreshed.\n * Throws {@link HostRegistryError} (`identity` when another server answers).\n */\n setUrl: (id: string, url: string) => Promise<HostEntry>\n /** Connect with the saved token and say who answered; refreshes `lastConnectedAt` on success. */\n test: (id: string) => Promise<HostTestResult>\n /** The saved token, for transports. Never display it. */\n token: (id: string) => string | undefined\n}\n\nfunction isEntry(value: unknown): value is HostEntry {\n const e = value as Partial<HostEntry> | null\n return !!e && typeof e === 'object' && typeof e.id === 'string' && typeof e.name === 'string' && typeof e.url === 'string' && typeof e.addedAt === 'number'\n}\n\n/** An entry saved before URLs were checked for credentials: read without them (it fails auth, the user re-adds it). */\nfunction withoutUrlSecrets(entry: HostEntry): HostEntry {\n const raw = entry.url\n return urlCarriesSecrets(raw) ? { ...entry, url: displayHostUrl(raw) } : entry\n}\n\nfunction readHosts(dataDir: string): HostEntry[] {\n const path = hostsFilePath(dataDir)\n if (!existsSync(path))\n return []\n try {\n const parsed = JSON.parse(readFileSync(path, 'utf8')) as Partial<HostsFile>\n return Array.isArray(parsed.hosts) ? parsed.hosts.filter(isEntry).map(withoutUrlSecrets) : []\n }\n catch {\n return []\n }\n}\n\nfunction writeHosts(dataDir: string, hosts: HostEntry[]): void {\n const file: HostsFile = { version: FILE_VERSION, hosts }\n writeJsonFileAtomic(hostsFilePath(dataDir), file, OWNER_ONLY)\n}\n\nfunction cleanName(name: string): string {\n const trimmed = name.trim()\n if (!trimmed)\n throw new HostRegistryError('invalid-name', 'A host needs a name.')\n return trimmed\n}\n\nfunction checkedUrl(raw: string): string {\n try {\n return parseRemoteHostUrl(raw)\n }\n catch (err) {\n throw new HostRegistryError('invalid-url', err instanceof RemoteHostError ? err.message : String(err))\n }\n}\n\n/** The URL and token an `add` (or an unsaved test) takes, checked; throws {@link HostRegistryError}. */\nexport function checkHostInput(input: { url: string, token: string }): { url: string, token: string } {\n const url = checkedUrl(input.url)\n const token = input.token.trim()\n if (!token)\n throw new HostRegistryError('invalid-token', 'A host needs a token (zidane server token create --name <device>).')\n return { url, token }\n}\n\n/** A probe's answer, refused unless a zidane-server answered. */\nfunction asServer(url: string, probed: HostProbeResult): HostProbeResult {\n if (probed.ok && probed.hostRef.kind !== 'server')\n return { ok: false, error: 'protocol', message: `${url} is a ${probed.hostRef.kind} host, not a zidane-server.` }\n return probed\n}\n\n/** The add form's \"test\": `add`'s checks and probe, saving nothing. A refused input or a failed probe comes back as an error result, not a throw. */\nexport async function probeUnsavedHost(probe: HostProbe, input: { url: string, token: string }): Promise<HostProbeResult> {\n let checked: { url: string, token: string }\n try {\n checked = checkHostInput(input)\n }\n catch (err) {\n if (err instanceof HostRegistryError)\n return { ok: false, error: err.code, message: err.message }\n throw err\n }\n return asServer(checked.url, await probe(checked))\n}\n\n/**\n * Creates a host registry backed by `hosts.json` and the credentials store in `options.dataDir`.\n *\n * @param options - Where to store hosts (`dataDir`), how to reach a host (`probe`), and an optional clock (`now`, default `Date.now`).\n * @example\n * ```ts\n * import { createHostRegistry } from 'zidane/chat'\n *\n * const hosts = createHostRegistry({\n * dataDir: '/home/me/.zidane',\n * probe: async () => ({ ok: false, error: 'unreachable', message: 'No transport wired.' }),\n * })\n * console.log(hosts.list())\n * ```\n */\nexport function createHostRegistry(options: { dataDir: string, probe: HostProbe, now?: () => number }): HostRegistry {\n const { dataDir, probe } = options\n const now = options.now ?? Date.now\n const list = (): HostEntry[] => readHosts(dataDir)\n const get = (id: string): HostEntry | undefined => list().find(h => h.id === id)\n const token = (id: string): string | undefined => {\n const cred = readCredentialEntry(dataDir, hostCredentialKey(id))\n return cred?.kind === 'apikey' && cred.value ? cred.value : undefined\n }\n const update = (id: string, patch: (entry: HostEntry) => HostEntry): HostEntry | undefined => {\n const hosts = list()\n const i = hosts.findIndex(h => h.id === id)\n if (i < 0)\n return undefined\n hosts[i] = patch(hosts[i]!)\n writeHosts(dataDir, hosts)\n return hosts[i]\n }\n\n return {\n list,\n get,\n token,\n async add(input) {\n const { url, token: secret } = checkHostInput(input)\n const name = input.name === undefined ? undefined : cleanName(input.name)\n const probed = asServer(url, await probe({ url, token: secret }))\n if (!probed.ok)\n throw new HostRegistryError(probed.error, probed.message)\n const at = now()\n const fresh: Pick<HostEntry, 'url' | 'lastConnectedAt' | 'hostKind' | 'serverName' | 'serverVersion'> = {\n url,\n lastConnectedAt: at,\n hostKind: 'server',\n serverName: probed.hostRef.name,\n ...(probed.serverVersion !== undefined ? { serverVersion: probed.serverVersion } : {}),\n }\n const id = probed.hostRef.id\n // Token first: an entry is never saved without its token.\n writeCredentialEntry(dataDir, hostCredentialKey(id), { kind: 'apikey', value: secret })\n const hosts = list()\n const existing = hosts.find(h => h.id === id)\n const entry: HostEntry = existing\n ? { ...existing, ...fresh, ...(name !== undefined ? { name } : {}) }\n : { id, name: name ?? probed.hostRef.name, addedAt: at, ...fresh }\n writeHosts(dataDir, existing ? hosts.map(h => (h.id === id ? entry : h)) : [...hosts, entry])\n return entry\n },\n remove(id) {\n const hosts = list()\n const kept = hosts.filter(h => h.id !== id)\n const had = kept.length !== hosts.length\n if (had)\n writeHosts(dataDir, kept)\n const hadToken = token(id) !== undefined\n writeCredentialEntry(dataDir, hostCredentialKey(id), undefined)\n return had || hadToken\n },\n rename(id, name) {\n const clean = cleanName(name)\n const entry = update(id, e => ({ ...e, name: clean }))\n if (!entry)\n throw new HostRegistryError('unknown-host', `No host \"${id}\".`)\n return entry\n },\n async setUrl(id, raw) {\n const url = checkedUrl(raw)\n const entry = get(id)\n const secret = entry ? token(id) : undefined\n if (!entry)\n throw new HostRegistryError('unknown-host', `No host \"${id}\".`)\n if (secret === undefined)\n throw new HostRegistryError('unknown-host', `No token saved for \"${entry.name}\": add it again.`)\n const probed = asServer(url, await probe({ url, token: secret }))\n if (!probed.ok)\n throw new HostRegistryError(probed.error, probed.message)\n if (probed.hostRef.id !== entry.id)\n throw new HostRegistryError('identity', `A different host (\"${probed.hostRef.name}\") answers at ${displayHostUrl(url)}, not \"${entry.name}\".`)\n const moved = update(id, e => ({\n ...e,\n url,\n lastConnectedAt: now(),\n serverName: probed.hostRef.name,\n ...(probed.serverVersion !== undefined ? { serverVersion: probed.serverVersion } : {}),\n }))\n if (!moved)\n throw new HostRegistryError('unknown-host', `No host \"${id}\".`)\n return moved\n },\n async test(id) {\n const entry = get(id)\n const secret = entry ? token(id) : undefined\n if (!entry || secret === undefined)\n return { ok: false, error: 'unknown-host', message: entry ? `No token saved for \"${entry.name}\": add it again.` : `No host \"${id}\".` }\n const probed = await probe({ url: entry.url, token: secret })\n if (!probed.ok)\n return probed\n if (probed.hostRef.id !== entry.id)\n return { ok: false, error: 'identity', message: `A different host (\"${probed.hostRef.name}\") answers at ${entry.url}. Remove \"${entry.name}\" and add it again if it was reinstalled.` }\n update(id, e => ({\n ...e,\n lastConnectedAt: now(),\n serverName: probed.hostRef.name,\n ...(probed.serverVersion !== undefined ? { serverVersion: probed.serverVersion } : {}),\n }))\n return probed\n },\n }\n}\n"],"mappings":";;;;;;;AAkBA,MAAM,eAAe;;;;;;AAYrB,SAAgB,cAAc,SAAyB;CACrD,OAAO,QAAQ,SAAS,YAAY;AACtC;;;;;;AAOA,SAAgB,kBAAkB,IAAoB;CACpD,OAAO,SAAS;AAClB;AAgCA,SAAS,QAAQ,OAAoC;CACnD,MAAM,IAAI;CACV,OAAO,CAAC,CAAC,KAAK,OAAO,MAAM,YAAY,OAAO,EAAE,OAAO,YAAY,OAAO,EAAE,SAAS,YAAY,OAAO,EAAE,QAAQ,YAAY,OAAO,EAAE,YAAY;AACrJ;;AAGA,SAAS,kBAAkB,OAA6B;CACtD,MAAM,MAAM,MAAM;CAClB,OAAO,kBAAkB,GAAG,IAAI;EAAE,GAAG;EAAO,KAAK,eAAe,GAAG;CAAE,IAAI;AAC3E;AAEA,SAAS,UAAU,SAA8B;CAC/C,MAAM,OAAO,cAAc,OAAO;CAClC,IAAI,CAAC,WAAW,IAAI,GAClB,OAAO,CAAC;CACV,IAAI;EACF,MAAM,SAAS,KAAK,MAAM,aAAa,MAAM,MAAM,CAAC;EACpD,OAAO,MAAM,QAAQ,OAAO,KAAK,IAAI,OAAO,MAAM,OAAO,OAAO,CAAC,CAAC,IAAI,iBAAiB,IAAI,CAAC;CAC9F,QACM;EACJ,OAAO,CAAC;CACV;AACF;AAEA,SAAS,WAAW,SAAiB,OAA0B;CAC7D,MAAM,OAAkB;EAAE,SAAS;EAAc;CAAM;CACvD,oBAAoB,cAAc,OAAO,GAAG,MAAM,UAAU;AAC9D;AAEA,SAAS,UAAU,MAAsB;CACvC,MAAM,UAAU,KAAK,KAAK;CAC1B,IAAI,CAAC,SACH,MAAM,IAAI,kBAAkB,gBAAgB,sBAAsB;CACpE,OAAO;AACT;AAEA,SAAS,WAAW,KAAqB;CACvC,IAAI;EACF,OAAO,mBAAmB,GAAG;CAC/B,SACO,KAAK;EACV,MAAM,IAAI,kBAAkB,eAAe,eAAe,kBAAkB,IAAI,UAAU,OAAO,GAAG,CAAC;CACvG;AACF;;AAGA,SAAgB,eAAe,OAAuE;CACpG,MAAM,MAAM,WAAW,MAAM,GAAG;CAChC,MAAM,QAAQ,MAAM,MAAM,KAAK;CAC/B,IAAI,CAAC,OACH,MAAM,IAAI,kBAAkB,iBAAiB,oEAAoE;CACnH,OAAO;EAAE;EAAK;CAAM;AACtB;;AAGA,SAAS,SAAS,KAAa,QAA0C;CACvE,IAAI,OAAO,MAAM,OAAO,QAAQ,SAAS,UACvC,OAAO;EAAE,IAAI;EAAO,OAAO;EAAY,SAAS,GAAG,IAAI,QAAQ,OAAO,QAAQ,KAAK;CAA6B;CAClH,OAAO;AACT;;AAGA,eAAsB,iBAAiB,OAAkB,OAAiE;CACxH,IAAI;CACJ,IAAI;EACF,UAAU,eAAe,KAAK;CAChC,SACO,KAAK;EACV,IAAI,eAAe,mBACjB,OAAO;GAAE,IAAI;GAAO,OAAO,IAAI;GAAM,SAAS,IAAI;EAAQ;EAC5D,MAAM;CACR;CACA,OAAO,SAAS,QAAQ,KAAK,MAAM,MAAM,OAAO,CAAC;AACnD;;;;;;;;;;;;;;;;AAiBA,SAAgB,mBAAmB,SAAkF;CACnH,MAAM,EAAE,SAAS,UAAU;CAC3B,MAAM,MAAM,QAAQ,OAAO,KAAK;CAChC,MAAM,aAA0B,UAAU,OAAO;CACjD,MAAM,OAAO,OAAsC,KAAK,CAAC,CAAC,MAAK,MAAK,EAAE,OAAO,EAAE;CAC/E,MAAM,SAAS,OAAmC;EAChD,MAAM,OAAO,oBAAoB,SAAS,kBAAkB,EAAE,CAAC;EAC/D,OAAO,MAAM,SAAS,YAAY,KAAK,QAAQ,KAAK,QAAQ,KAAA;CAC9D;CACA,MAAM,UAAU,IAAY,UAAkE;EAC5F,MAAM,QAAQ,KAAK;EACnB,MAAM,IAAI,MAAM,WAAU,MAAK,EAAE,OAAO,EAAE;EAC1C,IAAI,IAAI,GACN,OAAO,KAAA;EACT,MAAM,KAAK,MAAM,MAAM,EAAG;EAC1B,WAAW,SAAS,KAAK;EACzB,OAAO,MAAM;CACf;CAEA,OAAO;EACL;EACA;EACA;EACA,MAAM,IAAI,OAAO;GACf,MAAM,EAAE,KAAK,OAAO,WAAW,eAAe,KAAK;GACnD,MAAM,OAAO,MAAM,SAAS,KAAA,IAAY,KAAA,IAAY,UAAU,MAAM,IAAI;GACxE,MAAM,SAAS,SAAS,KAAK,MAAM,MAAM;IAAE;IAAK,OAAO;GAAO,CAAC,CAAC;GAChE,IAAI,CAAC,OAAO,IACV,MAAM,IAAI,kBAAkB,OAAO,OAAO,OAAO,OAAO;GAC1D,MAAM,KAAK,IAAI;GACf,MAAM,QAAkG;IACtG;IACA,iBAAiB;IACjB,UAAU;IACV,YAAY,OAAO,QAAQ;IAC3B,GAAI,OAAO,kBAAkB,KAAA,IAAY,EAAE,eAAe,OAAO,cAAc,IAAI,CAAC;GACtF;GACA,MAAM,KAAK,OAAO,QAAQ;GAE1B,qBAAqB,SAAS,kBAAkB,EAAE,GAAG;IAAE,MAAM;IAAU,OAAO;GAAO,CAAC;GACtF,MAAM,QAAQ,KAAK;GACnB,MAAM,WAAW,MAAM,MAAK,MAAK,EAAE,OAAO,EAAE;GAC5C,MAAM,QAAmB,WACrB;IAAE,GAAG;IAAU,GAAG;IAAO,GAAI,SAAS,KAAA,IAAY,EAAE,KAAK,IAAI,CAAC;GAAG,IACjE;IAAE;IAAI,MAAM,QAAQ,OAAO,QAAQ;IAAM,SAAS;IAAI,GAAG;GAAM;GACnE,WAAW,SAAS,WAAW,MAAM,KAAI,MAAM,EAAE,OAAO,KAAK,QAAQ,CAAE,IAAI,CAAC,GAAG,OAAO,KAAK,CAAC;GAC5F,OAAO;EACT;EACA,OAAO,IAAI;GACT,MAAM,QAAQ,KAAK;GACnB,MAAM,OAAO,MAAM,QAAO,MAAK,EAAE,OAAO,EAAE;GAC1C,MAAM,MAAM,KAAK,WAAW,MAAM;GAClC,IAAI,KACF,WAAW,SAAS,IAAI;GAC1B,MAAM,WAAW,MAAM,EAAE,MAAM,KAAA;GAC/B,qBAAqB,SAAS,kBAAkB,EAAE,GAAG,KAAA,CAAS;GAC9D,OAAO,OAAO;EAChB;EACA,OAAO,IAAI,MAAM;GACf,MAAM,QAAQ,UAAU,IAAI;GAC5B,MAAM,QAAQ,OAAO,KAAI,OAAM;IAAE,GAAG;IAAG,MAAM;GAAM,EAAE;GACrD,IAAI,CAAC,OACH,MAAM,IAAI,kBAAkB,gBAAgB,YAAY,GAAG,GAAG;GAChE,OAAO;EACT;EACA,MAAM,OAAO,IAAI,KAAK;GACpB,MAAM,MAAM,WAAW,GAAG;GAC1B,MAAM,QAAQ,IAAI,EAAE;GACpB,MAAM,SAAS,QAAQ,MAAM,EAAE,IAAI,KAAA;GACnC,IAAI,CAAC,OACH,MAAM,IAAI,kBAAkB,gBAAgB,YAAY,GAAG,GAAG;GAChE,IAAI,WAAW,KAAA,GACb,MAAM,IAAI,kBAAkB,gBAAgB,uBAAuB,MAAM,KAAK,iBAAiB;GACjG,MAAM,SAAS,SAAS,KAAK,MAAM,MAAM;IAAE;IAAK,OAAO;GAAO,CAAC,CAAC;GAChE,IAAI,CAAC,OAAO,IACV,MAAM,IAAI,kBAAkB,OAAO,OAAO,OAAO,OAAO;GAC1D,IAAI,OAAO,QAAQ,OAAO,MAAM,IAC9B,MAAM,IAAI,kBAAkB,YAAY,sBAAsB,OAAO,QAAQ,KAAK,gBAAgB,eAAe,GAAG,EAAE,SAAS,MAAM,KAAK,GAAG;GAC/I,MAAM,QAAQ,OAAO,KAAI,OAAM;IAC7B,GAAG;IACH;IACA,iBAAiB,IAAI;IACrB,YAAY,OAAO,QAAQ;IAC3B,GAAI,OAAO,kBAAkB,KAAA,IAAY,EAAE,eAAe,OAAO,cAAc,IAAI,CAAC;GACtF,EAAE;GACF,IAAI,CAAC,OACH,MAAM,IAAI,kBAAkB,gBAAgB,YAAY,GAAG,GAAG;GAChE,OAAO;EACT;EACA,MAAM,KAAK,IAAI;GACb,MAAM,QAAQ,IAAI,EAAE;GACpB,MAAM,SAAS,QAAQ,MAAM,EAAE,IAAI,KAAA;GACnC,IAAI,CAAC,SAAS,WAAW,KAAA,GACvB,OAAO;IAAE,IAAI;IAAO,OAAO;IAAgB,SAAS,QAAQ,uBAAuB,MAAM,KAAK,oBAAoB,YAAY,GAAG;GAAI;GACvI,MAAM,SAAS,MAAM,MAAM;IAAE,KAAK,MAAM;IAAK,OAAO;GAAO,CAAC;GAC5D,IAAI,CAAC,OAAO,IACV,OAAO;GACT,IAAI,OAAO,QAAQ,OAAO,MAAM,IAC9B,OAAO;IAAE,IAAI;IAAO,OAAO;IAAY,SAAS,sBAAsB,OAAO,QAAQ,KAAK,gBAAgB,MAAM,IAAI,YAAY,MAAM,KAAK;GAA2C;GACxL,OAAO,KAAI,OAAM;IACf,GAAG;IACH,iBAAiB,IAAI;IACrB,YAAY,OAAO,QAAQ;IAC3B,GAAI,OAAO,kBAAkB,KAAA,IAAY,EAAE,eAAe,OAAO,cAAc,IAAI,CAAC;GACtF,EAAE;GACF,OAAO;EACT;CACF;AACF"}
@@ -1,6 +1,6 @@
1
- import { t as createSyncEngine } from "./engine-CDuOhwbt.js";
2
- import { r as startWsSavedHosts } from "./ws-CqlZkB4w.js";
3
- import { i as wsTransport, t as hostSignalHandlers } from "./host-signals-Ca_oUeYl.js";
1
+ import { t as createSyncEngine } from "./engine-DjM5NPIf.js";
2
+ import { r as startWsSavedHosts } from "./ws-DriNRyP-.js";
3
+ import { i as wsTransport, t as hostSignalHandlers } from "./host-signals-C8x5Yso0.js";
4
4
  //#region src/tui/host/remote-hosts.ts
5
5
  /** The TUI's directory, as the shared saved-hosts sync sees it. */
6
6
  function savedHostsDirectory(directory) {
@@ -95,4 +95,4 @@ function startRemoteHosts(options, registry) {
95
95
  //#endregion
96
96
  export { startRemoteHosts };
97
97
 
98
- //# sourceMappingURL=remote-hosts-Ctg9fwQY.js.map
98
+ //# sourceMappingURL=remote-hosts-usqMiMbs.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"remote-hosts-Ctg9fwQY.js","names":[],"sources":["../src/tui/host/remote-hosts.ts"],"sourcesContent":["/**\n * Saved servers in the window's host directory: loaded only when the\n * `remoteHosts` setting is on (`use-remote-hosts` imports this module\n * dynamically), so a window with it off never reads `hosts.json`, never\n * loads the WebSocket client and never dials anything.\n *\n * The registry ↔ directory sync and the connection loops are the shared\n * ones (`startWsSavedHosts`, `src/ipc/hosts`, the GUI's too); this module is\n * the TUI's glue: a directory entry per server carries a WebSocket\n * transport (the session controllers and session data dial through it),\n * and the server's presence and session changes feed the window.\n */\nimport type { HostEntry, HostRegistry } from '../../chat/hosts'\nimport type { SyncEngine, SyncEngineOptions } from '../../chat/sync/engine'\nimport type { SavedHostsController, SavedHostsDirectory } from '../../ipc/hosts'\nimport type { SessionPresence } from '../../ipc/index'\nimport type { HostDirectory } from './host-directory'\nimport { createSyncEngine } from '../../chat/sync/engine'\nimport { startWsSavedHosts } from '../../ipc/hosts'\nimport { wsTransport } from '../runtime/transport'\nimport { hostSignalHandlers } from './host-signals'\n\nexport interface RemoteHostsOptions {\n dataDir: string\n directory: HostDirectory\n onPresence?: (key: string, rows: readonly SessionPresence[]) => void\n /** A server (re)connected: its sessions can be listed. */\n onConnected?: (key: string) => void\n /** A server's sessions changed (another window created, renamed or deleted one). */\n onSessionsChanged?: (key: string) => void\n /** Settings sync to the saved servers (`src/chat/sync`): this computer's files. Absent: no sync. */\n sync?: Omit<SyncEngineOptions, 'dataDir'>\n}\n\nexport type RemoteHostsController = SavedHostsController & {\n /** Settings sync to each saved server (Settings → Hosts → a server → Sync). */\n sync?: SyncEngine | null\n}\n\n/** The TUI's directory, as the shared saved-hosts sync sees it. */\nfunction savedHostsDirectory(directory: HostDirectory): SavedHostsDirectory {\n return {\n saved: () => directory.servers().filter(host => !host.adHoc).map(host => ({ key: host.key, url: host.url ?? '' })),\n upsert: (entry: HostEntry, token) => {\n // Every dial through it (the connection loop, session data, controllers) checks it is still this server.\n const transport = wsTransport({ url: entry.url, expectHostId: entry.id, ...(token ? { token } : {}) })\n const known = directory.get(entry.id)\n if (known) {\n // In place: the entry (and the sessions routed to it) stays.\n directory.update(entry.id, { name: entry.name, url: entry.url, transport, client: null, status: 'connecting', error: undefined })\n return\n }\n directory.put({\n key: entry.id,\n kind: 'server',\n name: entry.name,\n url: entry.url,\n transport,\n client: null,\n status: 'connecting',\n ...(entry.serverName ? { serverName: entry.serverName } : {}),\n ...(entry.serverVersion ? { serverVersion: entry.serverVersion } : {}),\n })\n },\n update: (key, patch) => directory.update(key, patch),\n remove: key => directory.remove(key),\n }\n}\n\nexport function startRemoteHosts(options: RemoteHostsOptions, registry?: HostRegistry): RemoteHostsController {\n const { directory } = options\n /** `--host` already names this server: its saved entry would be a second connection to it. */\n const adHocUrl = directory.servers().find(host => host.adHoc)?.url\n const sync = options.sync ? createSyncEngine({ dataDir: options.dataDir, ...options.sync }) : null\n const handlers = hostSignalHandlers(options)\n const controller = startWsSavedHosts({\n dataDir: options.dataDir,\n ...(registry ? { registry } : {}),\n directory: savedHostsDirectory(directory),\n skip: entry => entry.url === adHocUrl,\n connect: (entry, _token, attempt) => {\n const host = directory.get(entry.id)\n if (!host)\n return Promise.reject(new Error(`${entry.name} is no longer in this window's hosts.`))\n return host.transport.connect({ appVersion: 'tui-hosts', passive: true, ...attempt })\n },\n onSignal: (key, signal) => {\n handlers.onSignal(key, signal)\n if (signal.kind === 'sync:changed')\n sync?.serverChanged(key)\n },\n onConnected: (key, client) => {\n handlers.onConnected(key, client)\n sync?.connected(key, client)\n },\n onDisconnected: (key) => {\n handlers.onDisconnected(key)\n sync?.disconnected(key)\n },\n })\n return {\n ...controller,\n // Removing a server keeps what it has; this computer stops syncing to it.\n remove: (id) => {\n const removed = controller.remove(id)\n if (removed)\n sync?.removed(id)\n return removed\n },\n stop: () => {\n sync?.stop()\n controller.stop()\n },\n sync,\n }\n}\n"],"mappings":";;;;;AAwCA,SAAS,oBAAoB,WAA+C;CAC1E,OAAO;EACL,aAAa,UAAU,QAAQ,CAAC,CAAC,QAAO,SAAQ,CAAC,KAAK,KAAK,CAAC,CAAC,KAAI,UAAS;GAAE,KAAK,KAAK;GAAK,KAAK,KAAK,OAAO;EAAG,EAAE;EACjH,SAAS,OAAkB,UAAU;GAEnC,MAAM,YAAY,YAAY;IAAE,KAAK,MAAM;IAAK,cAAc,MAAM;IAAI,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;GAAG,CAAC;GAErG,IADc,UAAU,IAAI,MAAM,EAC1B,GAAG;IAET,UAAU,OAAO,MAAM,IAAI;KAAE,MAAM,MAAM;KAAM,KAAK,MAAM;KAAK;KAAW,QAAQ;KAAM,QAAQ;KAAc,OAAO,KAAA;IAAU,CAAC;IAChI;GACF;GACA,UAAU,IAAI;IACZ,KAAK,MAAM;IACX,MAAM;IACN,MAAM,MAAM;IACZ,KAAK,MAAM;IACX;IACA,QAAQ;IACR,QAAQ;IACR,GAAI,MAAM,aAAa,EAAE,YAAY,MAAM,WAAW,IAAI,CAAC;IAC3D,GAAI,MAAM,gBAAgB,EAAE,eAAe,MAAM,cAAc,IAAI,CAAC;GACtE,CAAC;EACH;EACA,SAAS,KAAK,UAAU,UAAU,OAAO,KAAK,KAAK;EACnD,SAAQ,QAAO,UAAU,OAAO,GAAG;CACrC;AACF;AAEA,SAAgB,iBAAiB,SAA6B,UAAgD;CAC5G,MAAM,EAAE,cAAc;;CAEtB,MAAM,WAAW,UAAU,QAAQ,CAAC,CAAC,MAAK,SAAQ,KAAK,KAAK,CAAC,EAAE;CAC/D,MAAM,OAAO,QAAQ,OAAO,iBAAiB;EAAE,SAAS,QAAQ;EAAS,GAAG,QAAQ;CAAK,CAAC,IAAI;CAC9F,MAAM,WAAW,mBAAmB,OAAO;CAC3C,MAAM,aAAa,kBAAkB;EACnC,SAAS,QAAQ;EACjB,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;EAC/B,WAAW,oBAAoB,SAAS;EACxC,OAAM,UAAS,MAAM,QAAQ;EAC7B,UAAU,OAAO,QAAQ,YAAY;GACnC,MAAM,OAAO,UAAU,IAAI,MAAM,EAAE;GACnC,IAAI,CAAC,MACH,OAAO,QAAQ,uBAAO,IAAI,MAAM,GAAG,MAAM,KAAK,sCAAsC,CAAC;GACvF,OAAO,KAAK,UAAU,QAAQ;IAAE,YAAY;IAAa,SAAS;IAAM,GAAG;GAAQ,CAAC;EACtF;EACA,WAAW,KAAK,WAAW;GACzB,SAAS,SAAS,KAAK,MAAM;GAC7B,IAAI,OAAO,SAAS,gBAClB,MAAM,cAAc,GAAG;EAC3B;EACA,cAAc,KAAK,WAAW;GAC5B,SAAS,YAAY,KAAK,MAAM;GAChC,MAAM,UAAU,KAAK,MAAM;EAC7B;EACA,iBAAiB,QAAQ;GACvB,SAAS,eAAe,GAAG;GAC3B,MAAM,aAAa,GAAG;EACxB;CACF,CAAC;CACD,OAAO;EACL,GAAG;EAEH,SAAS,OAAO;GACd,MAAM,UAAU,WAAW,OAAO,EAAE;GACpC,IAAI,SACF,MAAM,QAAQ,EAAE;GAClB,OAAO;EACT;EACA,YAAY;GACV,MAAM,KAAK;GACX,WAAW,KAAK;EAClB;EACA;CACF;AACF"}
1
+ {"version":3,"file":"remote-hosts-usqMiMbs.js","names":[],"sources":["../src/tui/host/remote-hosts.ts"],"sourcesContent":["/**\n * Saved servers in the window's host directory: loaded only when the\n * `remoteHosts` setting is on (`use-remote-hosts` imports this module\n * dynamically), so a window with it off never reads `hosts.json`, never\n * loads the WebSocket client and never dials anything.\n *\n * The registry ↔ directory sync and the connection loops are the shared\n * ones (`startWsSavedHosts`, `src/ipc/hosts`, the GUI's too); this module is\n * the TUI's glue: a directory entry per server carries a WebSocket\n * transport (the session controllers and session data dial through it),\n * and the server's presence and session changes feed the window.\n */\nimport type { HostEntry, HostRegistry } from '../../chat/hosts'\nimport type { SyncEngine, SyncEngineOptions } from '../../chat/sync/engine'\nimport type { SavedHostsController, SavedHostsDirectory } from '../../ipc/hosts'\nimport type { SessionPresence } from '../../ipc/index'\nimport type { HostDirectory } from './host-directory'\nimport { createSyncEngine } from '../../chat/sync/engine'\nimport { startWsSavedHosts } from '../../ipc/hosts'\nimport { wsTransport } from '../runtime/transport'\nimport { hostSignalHandlers } from './host-signals'\n\nexport interface RemoteHostsOptions {\n dataDir: string\n directory: HostDirectory\n onPresence?: (key: string, rows: readonly SessionPresence[]) => void\n /** A server (re)connected: its sessions can be listed. */\n onConnected?: (key: string) => void\n /** A server's sessions changed (another window created, renamed or deleted one). */\n onSessionsChanged?: (key: string) => void\n /** Settings sync to the saved servers (`src/chat/sync`): this computer's files. Absent: no sync. */\n sync?: Omit<SyncEngineOptions, 'dataDir'>\n}\n\nexport type RemoteHostsController = SavedHostsController & {\n /** Settings sync to each saved server (Settings → Hosts → a server → Sync). */\n sync?: SyncEngine | null\n}\n\n/** The TUI's directory, as the shared saved-hosts sync sees it. */\nfunction savedHostsDirectory(directory: HostDirectory): SavedHostsDirectory {\n return {\n saved: () => directory.servers().filter(host => !host.adHoc).map(host => ({ key: host.key, url: host.url ?? '' })),\n upsert: (entry: HostEntry, token) => {\n // Every dial through it (the connection loop, session data, controllers) checks it is still this server.\n const transport = wsTransport({ url: entry.url, expectHostId: entry.id, ...(token ? { token } : {}) })\n const known = directory.get(entry.id)\n if (known) {\n // In place: the entry (and the sessions routed to it) stays.\n directory.update(entry.id, { name: entry.name, url: entry.url, transport, client: null, status: 'connecting', error: undefined })\n return\n }\n directory.put({\n key: entry.id,\n kind: 'server',\n name: entry.name,\n url: entry.url,\n transport,\n client: null,\n status: 'connecting',\n ...(entry.serverName ? { serverName: entry.serverName } : {}),\n ...(entry.serverVersion ? { serverVersion: entry.serverVersion } : {}),\n })\n },\n update: (key, patch) => directory.update(key, patch),\n remove: key => directory.remove(key),\n }\n}\n\nexport function startRemoteHosts(options: RemoteHostsOptions, registry?: HostRegistry): RemoteHostsController {\n const { directory } = options\n /** `--host` already names this server: its saved entry would be a second connection to it. */\n const adHocUrl = directory.servers().find(host => host.adHoc)?.url\n const sync = options.sync ? createSyncEngine({ dataDir: options.dataDir, ...options.sync }) : null\n const handlers = hostSignalHandlers(options)\n const controller = startWsSavedHosts({\n dataDir: options.dataDir,\n ...(registry ? { registry } : {}),\n directory: savedHostsDirectory(directory),\n skip: entry => entry.url === adHocUrl,\n connect: (entry, _token, attempt) => {\n const host = directory.get(entry.id)\n if (!host)\n return Promise.reject(new Error(`${entry.name} is no longer in this window's hosts.`))\n return host.transport.connect({ appVersion: 'tui-hosts', passive: true, ...attempt })\n },\n onSignal: (key, signal) => {\n handlers.onSignal(key, signal)\n if (signal.kind === 'sync:changed')\n sync?.serverChanged(key)\n },\n onConnected: (key, client) => {\n handlers.onConnected(key, client)\n sync?.connected(key, client)\n },\n onDisconnected: (key) => {\n handlers.onDisconnected(key)\n sync?.disconnected(key)\n },\n })\n return {\n ...controller,\n // Removing a server keeps what it has; this computer stops syncing to it.\n remove: (id) => {\n const removed = controller.remove(id)\n if (removed)\n sync?.removed(id)\n return removed\n },\n stop: () => {\n sync?.stop()\n controller.stop()\n },\n sync,\n }\n}\n"],"mappings":";;;;;AAwCA,SAAS,oBAAoB,WAA+C;CAC1E,OAAO;EACL,aAAa,UAAU,QAAQ,CAAC,CAAC,QAAO,SAAQ,CAAC,KAAK,KAAK,CAAC,CAAC,KAAI,UAAS;GAAE,KAAK,KAAK;GAAK,KAAK,KAAK,OAAO;EAAG,EAAE;EACjH,SAAS,OAAkB,UAAU;GAEnC,MAAM,YAAY,YAAY;IAAE,KAAK,MAAM;IAAK,cAAc,MAAM;IAAI,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;GAAG,CAAC;GAErG,IADc,UAAU,IAAI,MAAM,EAC1B,GAAG;IAET,UAAU,OAAO,MAAM,IAAI;KAAE,MAAM,MAAM;KAAM,KAAK,MAAM;KAAK;KAAW,QAAQ;KAAM,QAAQ;KAAc,OAAO,KAAA;IAAU,CAAC;IAChI;GACF;GACA,UAAU,IAAI;IACZ,KAAK,MAAM;IACX,MAAM;IACN,MAAM,MAAM;IACZ,KAAK,MAAM;IACX;IACA,QAAQ;IACR,QAAQ;IACR,GAAI,MAAM,aAAa,EAAE,YAAY,MAAM,WAAW,IAAI,CAAC;IAC3D,GAAI,MAAM,gBAAgB,EAAE,eAAe,MAAM,cAAc,IAAI,CAAC;GACtE,CAAC;EACH;EACA,SAAS,KAAK,UAAU,UAAU,OAAO,KAAK,KAAK;EACnD,SAAQ,QAAO,UAAU,OAAO,GAAG;CACrC;AACF;AAEA,SAAgB,iBAAiB,SAA6B,UAAgD;CAC5G,MAAM,EAAE,cAAc;;CAEtB,MAAM,WAAW,UAAU,QAAQ,CAAC,CAAC,MAAK,SAAQ,KAAK,KAAK,CAAC,EAAE;CAC/D,MAAM,OAAO,QAAQ,OAAO,iBAAiB;EAAE,SAAS,QAAQ;EAAS,GAAG,QAAQ;CAAK,CAAC,IAAI;CAC9F,MAAM,WAAW,mBAAmB,OAAO;CAC3C,MAAM,aAAa,kBAAkB;EACnC,SAAS,QAAQ;EACjB,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;EAC/B,WAAW,oBAAoB,SAAS;EACxC,OAAM,UAAS,MAAM,QAAQ;EAC7B,UAAU,OAAO,QAAQ,YAAY;GACnC,MAAM,OAAO,UAAU,IAAI,MAAM,EAAE;GACnC,IAAI,CAAC,MACH,OAAO,QAAQ,uBAAO,IAAI,MAAM,GAAG,MAAM,KAAK,sCAAsC,CAAC;GACvF,OAAO,KAAK,UAAU,QAAQ;IAAE,YAAY;IAAa,SAAS;IAAM,GAAG;GAAQ,CAAC;EACtF;EACA,WAAW,KAAK,WAAW;GACzB,SAAS,SAAS,KAAK,MAAM;GAC7B,IAAI,OAAO,SAAS,gBAClB,MAAM,cAAc,GAAG;EAC3B;EACA,cAAc,KAAK,WAAW;GAC5B,SAAS,YAAY,KAAK,MAAM;GAChC,MAAM,UAAU,KAAK,MAAM;EAC7B;EACA,iBAAiB,QAAQ;GACvB,SAAS,eAAe,GAAG;GAC3B,MAAM,aAAa,GAAG;EACxB;CACF,CAAC;CACD,OAAO;EACL,GAAG;EAEH,SAAS,OAAO;GACd,MAAM,UAAU,WAAW,OAAO,EAAE;GACpC,IAAI,SACF,MAAM,QAAQ,EAAE;GAClB,OAAO;EACT;EACA,YAAY;GACV,MAAM,KAAK;GACX,WAAW,KAAK;EAClB;EACA;CACF;AACF"}
@@ -5,7 +5,7 @@ import { a as shell, n as waitTask, r as shellKill } from "./wait-task-MYVKPrDt.
5
5
  import { c as projectRootOf } from "./project-root-BZcgGJMi.js";
6
6
  import { c as edit, f as createSpawnTool, n as readFile, o as multiEdit, s as listFiles, t as writeFile } from "./write-file-qPCreaSI.js";
7
7
  import { n as DEFAULT_MEDIA_BUDGET, t as DEFAULT_MAX_IMAGE_EDGE } from "./defaults-DjjIPknt.js";
8
- import { h as editSessionMetadata } from "./system-freeze-CEuTQzNu.js";
8
+ import { h as editSessionMetadata } from "./system-freeze-CY4Hc9WX.js";
9
9
  import { n as grep, r as glob } from "./tools-E7HG_n5f.js";
10
10
  import { r as fetchUrl } from "./fetch-url-DPo_WZqL.js";
11
11
  import { webSearch } from "./tools/web-search.js";
@@ -984,4 +984,4 @@ function isDurabilityMode(value) {
984
984
  //#endregion
985
985
  export { createPlanTools as A, profileTools as C, DEFAULT_MAX_TODO_ITEMS as D, singleAgentRegistry as E, sanitizeTodoItems as M, createPlanLifecycleHooks as N, TODOREAD_TOOL as O, profileComposition as S, sessionComposition as T, availableAgents as _, deleteSessionData as a, compositionFits as b, newSession as c, BUILTIN_AGENTS as d, DEFAULT_AGENT_ID as f, READ_ONLY_TOOLS as g, PLAN_AGENT as h, createSessionInStore as i, isTodoTool as j, TODOWRITE_TOOL as k, BUILD_AGENT as l, DEFAULT_PERSIST_EXCLUDE_TOOLS as m, isDurabilityMode as n, describeForkFailure as o, DEFAULT_BUDGET_EXCLUDE_TOOLS as p, createMainSession as r, forkSession as s, DEFAULT_DURABILITY as t, BUILD_CODEMODE_AGENT as u, builtInPromptOf as v, resolveAgentId as w, compositionTools as x, compositionByKey as y };
986
986
 
987
- //# sourceMappingURL=setting-DPim5iCP.js.map
987
+ //# sourceMappingURL=setting-DzTMJIV3.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"setting-DPim5iCP.js","names":[],"sources":["../src/chat/plan-hooks.ts","../src/chat/todos.ts","../src/chat/agents.ts","../src/chat/session-factory.ts","../src/durable/setting.ts"],"sourcesContent":["/**\n * The plan lifecycle on the agent's own run path, so every embedder (the\n * chat host, channels, ACP, headless runs) closes plans the same way:\n *\n * - `run:beforeEnd`: a run about to finish with open steps in the plan it\n * is applying gets ONE reminder turn to update or cancel them. Never in a\n * sub-agent (its plan closes with it), never twice in a run, and never\n * after an abort or an error (the hook does not fire there).\n * - `session:end`: every exit (completed, aborted, error, a throw) stops\n * the plans that run applied. The event id is `runEnd:<runId>`, so a\n * second report of the same end changes nothing.\n */\nimport type { AgentHookMap } from '../agent'\nimport { applyingPlan, formatOpenStepsReminder, formatPlanForModel, formatStoppedNotice, OPEN_STEPS_REMINDER_MARK, planProgress, SESSION_SCOPE } from './plans'\nimport { commitPlanEvent, planScopeForRun, readSessionPlans } from './plans-session'\n\n/** `todoread` (named here: `./todos` imports this module). */\nconst TODOREAD_TOOL_NAME = 'todoread'\n\n/** Options for {@link createPlanLifecycleHooks}. */\nexport interface PlanLifecycleHooksOptions {\n /** Send the open-steps reminder. @default true */\n remindOpenSteps?: boolean\n}\n\n/**\n * Hooks that close plans when runs end, and remind a run once about open steps.\n *\n * @param options - Whether to send the reminder.\n */\nexport function createPlanLifecycleHooks(options: PlanLifecycleHooksOptions = {}): AgentHookMap {\n const remind = options.remindOpenSteps !== false\n // Runs already reminded in this process. A replay takes the journaled\n // decision without calling the hook, so a resumed run also looks for the\n // reminder among its saved turns.\n const reminded = new Set<string>()\n return {\n 'run:beforeEnd': (ctx) => {\n const session = ctx.session\n if (!remind || !session || reminded.has(ctx.runId))\n return\n if (session.turns.some(t => t.runId === ctx.runId && t.role === 'user' && t.content.some(b => b.type === 'text' && b.text.includes(OPEN_STEPS_REMINDER_MARK))))\n return\n if (planScopeForRun(session.runs, ctx.runId) !== SESSION_SCOPE)\n return\n const plan = applyingPlan(readSessionPlans(session), SESSION_SCOPE)\n if (!plan || plan.runId !== ctx.runId)\n return\n const p = planProgress(plan)\n if (p.pending + p.inProgress === 0)\n return\n reminded.add(ctx.runId)\n ctx.followUp(formatOpenStepsReminder(plan))\n },\n // A compaction keeps the plan: the summary may lose its steps, so the\n // plan rides the re-attached turns as a `todoread` result.\n 'compact:restore': (ctx) => {\n if (!ctx.session)\n return\n const bag = readSessionPlans(ctx.session)\n const plan = applyingPlan(bag, SESSION_SCOPE) ?? [...bag.plans].reverse().find(p => p.scope === SESSION_SCOPE && p.state === 'stopped')\n if (plan)\n ctx.add({ name: TODOREAD_TOOL_NAME, input: {}, output: plan.state === 'stopped' ? `${formatPlanForModel(plan)}\\n\\n${formatStoppedNotice(plan)}` : formatPlanForModel(plan) })\n },\n 'session:end': async (ctx) => {\n reminded.delete(ctx.runId)\n if (!ctx.session)\n return\n await commitPlanEvent(ctx.session, { type: 'runEnd', id: `runEnd:${ctx.runId}`, at: Date.now(), runId: ctx.runId, outcome: ctx.status })\n },\n }\n}\n","/**\n * `todowrite` / `todoread`: the agent's progress tracker, over plans\n * (`./plans`). The steps of the scope's applying plan ARE the todo list:\n *\n * - With an approved `present_plan` applying, `todowrite` updates its\n * steps by id. Steps it leaves out keep their status (a write may name\n * only what changed); unknown ids become added steps. A step is dropped\n * by writing it `cancelled`, never by leaving it out.\n * - Without one, `todowrite` starts (or updates) an adhoc plan. A write\n * of a whole new list, none of whose ids the adhoc plan holds, replaces\n * it (`superseded`) instead of adding beside steps left open.\n * - A plan whose steps are all finished closes on that write: `done`, or\n * `dropped` when everything was cancelled.\n * - A stopped plan resumes when the agent writes its step ids again; any\n * other write leaves it behind (`superseded`).\n *\n * Top-level runs share the session scope (a list outlives one prompt, so an\n * aborted run can be resumed); a sub-agent run writes its own scope, nested\n * under the parent's applying plan.\n *\n * Every write is persisted before the tool returns (`commitPlanEvent`), so a\n * durable replay that skips the tool body finds it stored, and its event id\n * (the call id) makes a re-run body a no-op.\n *\n * The result carries the whole plan, so the agent's view lives in the\n * transcript, which never changes once written: no system-prompt section\n * re-renders on each write and invalidates the cached conversation.\n *\n * No OpenTUI imports: a GUI consumer reuses this module verbatim.\n */\nimport type { Preset } from '../presets'\nimport type { ToolContext, ToolDef } from '../tools/types'\nimport type { Plan } from './plans'\nimport { definePreset } from '../presets'\nimport { jsonArray } from './interactions-protocol'\nimport { createPlanLifecycleHooks } from './plan-hooks'\nimport { applyingPlan, formatPlanForModel, formatStoppedNotice, MAX_PLAN_STEPS } from './plans'\nimport { commitPlanEvent, parentPlanScope, planScopeForRun, readSessionPlans } from './plans-session'\n\nexport { TODO_STATUS_GLYPHS } from './todo-glyphs'\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/** Progress of one todo (one plan step). */\nexport type TodoStatus = 'pending' | 'in_progress' | 'completed' | 'cancelled'\n\n/** One item of a `todowrite` call. */\nexport interface TodoItem {\n /** Stable id: the model picks it, or reuses a plan step's id. */\n id: string\n /** One-line task summary. */\n content: string\n /** Current progress. */\n status: TodoStatus\n}\n\nconst TODO_STATUS_VALUES: readonly TodoStatus[] = ['pending', 'in_progress', 'completed', 'cancelled']\n\n/** Ceiling on one list; every write boundary caps identically. */\nexport const DEFAULT_MAX_TODO_ITEMS = MAX_PLAN_STEPS\n\n/** Options for {@link createPlanTools}. */\nexport interface CreatePlanToolsOptions {\n /** Maximum item count per `todowrite`; excess items are dropped and reported. Default: `100`. */\n maxItems?: number\n /** Opt-in hard cap on `todowrite` calls per run (`behavior.toolBudgets`). Default: `0` (none). */\n maxWritesPerRun?: number\n /** Reaction when `maxWritesPerRun` is exceeded. @default 'steer' */\n onMaxWrites?: 'steer' | 'block'\n /**\n * Hard stop after this many byte-identical `todowrite` payloads in a row\n * (a run-scoped `behavior.dedupTools` entry). `0` turns it off. Default: `5`.\n */\n maxIdenticalWrites?: number\n /** Refusal text for the {@link maxIdenticalWrites} block, given the identical-call count. */\n identicalBlockReason?: (count: number) => string\n /**\n * Send the agent one reminder when a run is about to finish with open\n * steps in the plan it is applying (never after an abort or an error,\n * never in a sub-agent). Headless and eval hosts may turn it off.\n * Default: `true`.\n */\n remindOpenSteps?: boolean\n /** Replaces the `todowrite` description. Leave unset for the built-in text. */\n writeDescription?: string\n /** Replaces the `todoread` description. */\n readDescription?: string\n}\n\nconst WRITE_DESCRIPTION\n = 'Track the steps of your current task. Each call updates the steps it names, by id; steps you leave out keep their status, so you may send only the steps that changed.\\n\\n'\n + 'Statuses: `pending` (queued), `in_progress` (working on it now), `completed` (done), `cancelled` (not doing it).\\n\\n'\n + 'After the user approves a `present_plan`, use its step ids here: the list IS that plan, and the user watches it. '\n + 'A new id is recorded as an added step. To drop a step, write it `cancelled`.\\n\\n'\n + 'Checkpoint at real transitions:\\n'\n + ' 1. On a multi-step task, write every step `pending` before you start.\\n'\n + ' 2. When you move on, mark the finished step `completed` (or `cancelled`) and the next one `in_progress`.\\n'\n + ' 3. Before you finish, make sure no step is left `pending` or `in_progress` unless you will continue it later.\\n\\n'\n + 'When every step is completed or cancelled, the list closes. Do not call this on every action.'\n\nconst READ_DESCRIPTION\n = 'Return your current task list (the steps of the plan you are applying), and any plan that stopped and can be resumed. '\n + 'Use sparingly: your last `todowrite` result already shows the list.'\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 of a `todowrite` payload for the identical-write gate:\n * sanitized, last-wins id dedup, order kept. `undefined` for a payload with\n * no usable item, so a malformed call never counts towards the block.\n */\nfunction hashTodoWriteInput(input: Record<string, unknown>): string | undefined {\n const items = dedupById(sanitizeItems(jsonArray(input.todos) ?? [], Number.MAX_SAFE_INTEGER))\n if (items.length === 0)\n return undefined\n return JSON.stringify(items.map(t => [t.id, t.status, t.content]))\n}\n\n/**\n * The `{ todowrite, todoread }` tools plus the plan lifecycle hooks (the\n * open-steps reminder, closing a run's plans when it ends).\n *\n * ```ts\n * createAgent({ ...composePresets(basic, createPlanTools()), provider })\n * ```\n *\n * @param options - Limits, loop hardening and reminder settings.\n */\nexport function createPlanTools(options: CreatePlanToolsOptions = {}): Preset {\n const maxItems = options.maxItems ?? DEFAULT_MAX_TODO_ITEMS\n const maxWritesPerRun = options.maxWritesPerRun ?? 0\n const maxIdenticalWrites = options.maxIdenticalWrites ?? 5\n\n const tools: Record<string, ToolDef> = {\n [TODOWRITE_TOOL]: createTodoWriteTool(maxItems, options.writeDescription ?? WRITE_DESCRIPTION),\n [TODOREAD_TOOL]: createTodoReadTool(options.readDescription ?? READ_DESCRIPTION),\n }\n\n const behavior: NonNullable<Preset['behavior']> = {}\n if (maxWritesPerRun > 0)\n behavior.toolBudgets = { [TODOWRITE_TOOL]: { max: maxWritesPerRun, onExceed: options.onMaxWrites ?? 'steer' } }\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 tools,\n hooks: createPlanLifecycleHooks({ remindOpenSteps: options.remindOpenSteps ?? true }),\n ...(Object.keys(behavior).length > 0 ? { behavior } : {}),\n })\n}\n\nfunction createTodoWriteTool(maxItems: number, description: string): ToolDef {\n return {\n spec: {\n name: TODOWRITE_TOOL,\n description,\n inputSchema: {\n type: 'object',\n properties: {\n todos: {\n type: 'array',\n description: 'The steps to update, by id: the whole list, or only the steps that changed.',\n maxItems,\n items: {\n type: 'object',\n properties: {\n id: { type: 'string', description: 'Stable identifier; reuse the plan step id when applying an approved plan.' },\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 const raw = jsonArray(input.todos) ?? []\n const items = dedupById(sanitizeItems(raw, maxItems))\n const dropped = raw.length - items.length\n const scope = planScopeForRun(session.runs, runId)\n const parentScope = parentPlanScope(session.runs, runId)\n const before = applyingPlan(readSessionPlans(session), scope)\n const result = await commitPlanEvent(session, {\n type: 'write',\n id: ctx.callId,\n at: Date.now(),\n scope,\n runId,\n items,\n ...(parentScope ? { parentScope } : {}),\n })\n return formatWriteResult(result.plan ?? before, { unchanged: !result.changed, dropped })\n },\n }\n}\n\nfunction formatWriteResult(plan: Plan | undefined, { unchanged, dropped }: { unchanged: boolean, dropped: number }): string {\n const lines: string[] = []\n if (!plan) {\n lines.push('No task list: nothing to track.')\n }\n else {\n if (unchanged)\n lines.push('No change.')\n else if (plan.state === 'done')\n lines.push('Every step is finished; the list is closed.')\n else if (plan.state === 'dropped')\n lines.push('Every step was cancelled; the list is closed.')\n lines.push(formatPlanForModel(plan))\n }\n if (dropped > 0)\n lines.push(`Dropped ${dropped} malformed or duplicate item${dropped === 1 ? '' : 's'}.`)\n return lines.join('\\n')\n}\n\nfunction createTodoReadTool(description: string): ToolDef {\n return {\n isConcurrencySafe: true,\n replay: 'safe',\n spec: {\n name: TODOREAD_TOOL,\n description,\n inputSchema: { type: 'object', properties: {} },\n },\n async execute(_input, ctx) {\n const { session, runId } = requireSessionAndRun(ctx, TODOREAD_TOOL)\n const bag = readSessionPlans(session)\n const scope = planScopeForRun(session.runs, runId)\n const plan = applyingPlan(bag, scope)\n const stopped = [...bag.plans].reverse().find(p => p.scope === scope && p.state === 'stopped')\n const lines: string[] = []\n if (plan)\n lines.push(formatPlanForModel(plan))\n else\n lines.push('No task list. Call todowrite to start tracking steps.')\n if (stopped)\n lines.push(formatStoppedNotice(stopped))\n return lines.join('\\n\\n')\n },\n }\n}\n\nfunction requireSessionAndRun(ctx: ToolContext, toolName: string): { session: NonNullable<ToolContext['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 * Validates untrusted todo items: rows without a string id and content and a\n * known status drop out, then the list is capped. The same validator guards\n * the tool input and user edits.\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\n/** Last-wins id dedup, first-seen order kept. */\nfunction dedupById(items: readonly TodoItem[]): TodoItem[] {\n const byId = new Map<string, TodoItem>()\n for (const item of items)\n byId.set(item.id, item)\n return [...byId.values()]\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 { createPlanTools } 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 `createPlanTools()` on top so the model gets\n // `todowrite` / `todoread` over the session's plans (`src/chat/plans.ts`)\n // and the lifecycle hooks that close a run's plans when it ends.\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 createPlanTools(),\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 createPlanTools(),\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-DzTMJIV3.js","names":[],"sources":["../src/chat/plan-hooks.ts","../src/chat/todos.ts","../src/chat/agents.ts","../src/chat/session-factory.ts","../src/durable/setting.ts"],"sourcesContent":["/**\n * The plan lifecycle on the agent's own run path, so every embedder (the\n * chat host, channels, ACP, headless runs) closes plans the same way:\n *\n * - `run:beforeEnd`: a run about to finish with open steps in the plan it\n * is applying gets ONE reminder turn to update or cancel them. Never in a\n * sub-agent (its plan closes with it), never twice in a run, and never\n * after an abort or an error (the hook does not fire there).\n * - `session:end`: every exit (completed, aborted, error, a throw) stops\n * the plans that run applied. The event id is `runEnd:<runId>`, so a\n * second report of the same end changes nothing.\n */\nimport type { AgentHookMap } from '../agent'\nimport { applyingPlan, formatOpenStepsReminder, formatPlanForModel, formatStoppedNotice, OPEN_STEPS_REMINDER_MARK, planProgress, SESSION_SCOPE } from './plans'\nimport { commitPlanEvent, planScopeForRun, readSessionPlans } from './plans-session'\n\n/** `todoread` (named here: `./todos` imports this module). */\nconst TODOREAD_TOOL_NAME = 'todoread'\n\n/** Options for {@link createPlanLifecycleHooks}. */\nexport interface PlanLifecycleHooksOptions {\n /** Send the open-steps reminder. @default true */\n remindOpenSteps?: boolean\n}\n\n/**\n * Hooks that close plans when runs end, and remind a run once about open steps.\n *\n * @param options - Whether to send the reminder.\n */\nexport function createPlanLifecycleHooks(options: PlanLifecycleHooksOptions = {}): AgentHookMap {\n const remind = options.remindOpenSteps !== false\n // Runs already reminded in this process. A replay takes the journaled\n // decision without calling the hook, so a resumed run also looks for the\n // reminder among its saved turns.\n const reminded = new Set<string>()\n return {\n 'run:beforeEnd': (ctx) => {\n const session = ctx.session\n if (!remind || !session || reminded.has(ctx.runId))\n return\n if (session.turns.some(t => t.runId === ctx.runId && t.role === 'user' && t.content.some(b => b.type === 'text' && b.text.includes(OPEN_STEPS_REMINDER_MARK))))\n return\n if (planScopeForRun(session.runs, ctx.runId) !== SESSION_SCOPE)\n return\n const plan = applyingPlan(readSessionPlans(session), SESSION_SCOPE)\n if (!plan || plan.runId !== ctx.runId)\n return\n const p = planProgress(plan)\n if (p.pending + p.inProgress === 0)\n return\n reminded.add(ctx.runId)\n ctx.followUp(formatOpenStepsReminder(plan))\n },\n // A compaction keeps the plan: the summary may lose its steps, so the\n // plan rides the re-attached turns as a `todoread` result.\n 'compact:restore': (ctx) => {\n if (!ctx.session)\n return\n const bag = readSessionPlans(ctx.session)\n const plan = applyingPlan(bag, SESSION_SCOPE) ?? [...bag.plans].reverse().find(p => p.scope === SESSION_SCOPE && p.state === 'stopped')\n if (plan)\n ctx.add({ name: TODOREAD_TOOL_NAME, input: {}, output: plan.state === 'stopped' ? `${formatPlanForModel(plan)}\\n\\n${formatStoppedNotice(plan)}` : formatPlanForModel(plan) })\n },\n 'session:end': async (ctx) => {\n reminded.delete(ctx.runId)\n if (!ctx.session)\n return\n await commitPlanEvent(ctx.session, { type: 'runEnd', id: `runEnd:${ctx.runId}`, at: Date.now(), runId: ctx.runId, outcome: ctx.status })\n },\n }\n}\n","/**\n * `todowrite` / `todoread`: the agent's progress tracker, over plans\n * (`./plans`). The steps of the scope's applying plan ARE the todo list:\n *\n * - With an approved `present_plan` applying, `todowrite` updates its\n * steps by id. Steps it leaves out keep their status (a write may name\n * only what changed); unknown ids become added steps. A step is dropped\n * by writing it `cancelled`, never by leaving it out.\n * - Without one, `todowrite` starts (or updates) an adhoc plan. A write\n * of a whole new list, none of whose ids the adhoc plan holds, replaces\n * it (`superseded`) instead of adding beside steps left open.\n * - A plan whose steps are all finished closes on that write: `done`, or\n * `dropped` when everything was cancelled.\n * - A stopped plan resumes when the agent writes its step ids again; any\n * other write leaves it behind (`superseded`).\n *\n * Top-level runs share the session scope (a list outlives one prompt, so an\n * aborted run can be resumed); a sub-agent run writes its own scope, nested\n * under the parent's applying plan.\n *\n * Every write is persisted before the tool returns (`commitPlanEvent`), so a\n * durable replay that skips the tool body finds it stored, and its event id\n * (the call id) makes a re-run body a no-op.\n *\n * The result carries the whole plan, so the agent's view lives in the\n * transcript, which never changes once written: no system-prompt section\n * re-renders on each write and invalidates the cached conversation.\n *\n * No OpenTUI imports: a GUI consumer reuses this module verbatim.\n */\nimport type { Preset } from '../presets'\nimport type { ToolContext, ToolDef } from '../tools/types'\nimport type { Plan } from './plans'\nimport { definePreset } from '../presets'\nimport { jsonArray } from './interactions-protocol'\nimport { createPlanLifecycleHooks } from './plan-hooks'\nimport { applyingPlan, formatPlanForModel, formatStoppedNotice, MAX_PLAN_STEPS } from './plans'\nimport { commitPlanEvent, parentPlanScope, planScopeForRun, readSessionPlans } from './plans-session'\n\nexport { TODO_STATUS_GLYPHS } from './todo-glyphs'\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/** Progress of one todo (one plan step). */\nexport type TodoStatus = 'pending' | 'in_progress' | 'completed' | 'cancelled'\n\n/** One item of a `todowrite` call. */\nexport interface TodoItem {\n /** Stable id: the model picks it, or reuses a plan step's id. */\n id: string\n /** One-line task summary. */\n content: string\n /** Current progress. */\n status: TodoStatus\n}\n\nconst TODO_STATUS_VALUES: readonly TodoStatus[] = ['pending', 'in_progress', 'completed', 'cancelled']\n\n/** Ceiling on one list; every write boundary caps identically. */\nexport const DEFAULT_MAX_TODO_ITEMS = MAX_PLAN_STEPS\n\n/** Options for {@link createPlanTools}. */\nexport interface CreatePlanToolsOptions {\n /** Maximum item count per `todowrite`; excess items are dropped and reported. Default: `100`. */\n maxItems?: number\n /** Opt-in hard cap on `todowrite` calls per run (`behavior.toolBudgets`). Default: `0` (none). */\n maxWritesPerRun?: number\n /** Reaction when `maxWritesPerRun` is exceeded. @default 'steer' */\n onMaxWrites?: 'steer' | 'block'\n /**\n * Hard stop after this many byte-identical `todowrite` payloads in a row\n * (a run-scoped `behavior.dedupTools` entry). `0` turns it off. Default: `5`.\n */\n maxIdenticalWrites?: number\n /** Refusal text for the {@link maxIdenticalWrites} block, given the identical-call count. */\n identicalBlockReason?: (count: number) => string\n /**\n * Send the agent one reminder when a run is about to finish with open\n * steps in the plan it is applying (never after an abort or an error,\n * never in a sub-agent). Headless and eval hosts may turn it off.\n * Default: `true`.\n */\n remindOpenSteps?: boolean\n /** Replaces the `todowrite` description. Leave unset for the built-in text. */\n writeDescription?: string\n /** Replaces the `todoread` description. */\n readDescription?: string\n}\n\nconst WRITE_DESCRIPTION\n = 'Track the steps of your current task. Each call updates the steps it names, by id; steps you leave out keep their status, so you may send only the steps that changed.\\n\\n'\n + 'Statuses: `pending` (queued), `in_progress` (working on it now), `completed` (done), `cancelled` (not doing it).\\n\\n'\n + 'After the user approves a `present_plan`, use its step ids here: the list IS that plan, and the user watches it. '\n + 'A new id is recorded as an added step. To drop a step, write it `cancelled`.\\n\\n'\n + 'Checkpoint at real transitions:\\n'\n + ' 1. On a multi-step task, write every step `pending` before you start.\\n'\n + ' 2. When you move on, mark the finished step `completed` (or `cancelled`) and the next one `in_progress`.\\n'\n + ' 3. Before you finish, make sure no step is left `pending` or `in_progress` unless you will continue it later.\\n\\n'\n + 'When every step is completed or cancelled, the list closes. Do not call this on every action.'\n\nconst READ_DESCRIPTION\n = 'Return your current task list (the steps of the plan you are applying), and any plan that stopped and can be resumed. '\n + 'Use sparingly: your last `todowrite` result already shows the list.'\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 of a `todowrite` payload for the identical-write gate:\n * sanitized, last-wins id dedup, order kept. `undefined` for a payload with\n * no usable item, so a malformed call never counts towards the block.\n */\nfunction hashTodoWriteInput(input: Record<string, unknown>): string | undefined {\n const items = dedupById(sanitizeItems(jsonArray(input.todos) ?? [], Number.MAX_SAFE_INTEGER))\n if (items.length === 0)\n return undefined\n return JSON.stringify(items.map(t => [t.id, t.status, t.content]))\n}\n\n/**\n * The `{ todowrite, todoread }` tools plus the plan lifecycle hooks (the\n * open-steps reminder, closing a run's plans when it ends).\n *\n * ```ts\n * createAgent({ ...composePresets(basic, createPlanTools()), provider })\n * ```\n *\n * @param options - Limits, loop hardening and reminder settings.\n */\nexport function createPlanTools(options: CreatePlanToolsOptions = {}): Preset {\n const maxItems = options.maxItems ?? DEFAULT_MAX_TODO_ITEMS\n const maxWritesPerRun = options.maxWritesPerRun ?? 0\n const maxIdenticalWrites = options.maxIdenticalWrites ?? 5\n\n const tools: Record<string, ToolDef> = {\n [TODOWRITE_TOOL]: createTodoWriteTool(maxItems, options.writeDescription ?? WRITE_DESCRIPTION),\n [TODOREAD_TOOL]: createTodoReadTool(options.readDescription ?? READ_DESCRIPTION),\n }\n\n const behavior: NonNullable<Preset['behavior']> = {}\n if (maxWritesPerRun > 0)\n behavior.toolBudgets = { [TODOWRITE_TOOL]: { max: maxWritesPerRun, onExceed: options.onMaxWrites ?? 'steer' } }\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 tools,\n hooks: createPlanLifecycleHooks({ remindOpenSteps: options.remindOpenSteps ?? true }),\n ...(Object.keys(behavior).length > 0 ? { behavior } : {}),\n })\n}\n\nfunction createTodoWriteTool(maxItems: number, description: string): ToolDef {\n return {\n spec: {\n name: TODOWRITE_TOOL,\n description,\n inputSchema: {\n type: 'object',\n properties: {\n todos: {\n type: 'array',\n description: 'The steps to update, by id: the whole list, or only the steps that changed.',\n maxItems,\n items: {\n type: 'object',\n properties: {\n id: { type: 'string', description: 'Stable identifier; reuse the plan step id when applying an approved plan.' },\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 const raw = jsonArray(input.todos) ?? []\n const items = dedupById(sanitizeItems(raw, maxItems))\n const dropped = raw.length - items.length\n const scope = planScopeForRun(session.runs, runId)\n const parentScope = parentPlanScope(session.runs, runId)\n const before = applyingPlan(readSessionPlans(session), scope)\n const result = await commitPlanEvent(session, {\n type: 'write',\n id: ctx.callId,\n at: Date.now(),\n scope,\n runId,\n items,\n ...(parentScope ? { parentScope } : {}),\n })\n return formatWriteResult(result.plan ?? before, { unchanged: !result.changed, dropped })\n },\n }\n}\n\nfunction formatWriteResult(plan: Plan | undefined, { unchanged, dropped }: { unchanged: boolean, dropped: number }): string {\n const lines: string[] = []\n if (!plan) {\n lines.push('No task list: nothing to track.')\n }\n else {\n if (unchanged)\n lines.push('No change.')\n else if (plan.state === 'done')\n lines.push('Every step is finished; the list is closed.')\n else if (plan.state === 'dropped')\n lines.push('Every step was cancelled; the list is closed.')\n lines.push(formatPlanForModel(plan))\n }\n if (dropped > 0)\n lines.push(`Dropped ${dropped} malformed or duplicate item${dropped === 1 ? '' : 's'}.`)\n return lines.join('\\n')\n}\n\nfunction createTodoReadTool(description: string): ToolDef {\n return {\n isConcurrencySafe: true,\n replay: 'safe',\n spec: {\n name: TODOREAD_TOOL,\n description,\n inputSchema: { type: 'object', properties: {} },\n },\n async execute(_input, ctx) {\n const { session, runId } = requireSessionAndRun(ctx, TODOREAD_TOOL)\n const bag = readSessionPlans(session)\n const scope = planScopeForRun(session.runs, runId)\n const plan = applyingPlan(bag, scope)\n const stopped = [...bag.plans].reverse().find(p => p.scope === scope && p.state === 'stopped')\n const lines: string[] = []\n if (plan)\n lines.push(formatPlanForModel(plan))\n else\n lines.push('No task list. Call todowrite to start tracking steps.')\n if (stopped)\n lines.push(formatStoppedNotice(stopped))\n return lines.join('\\n\\n')\n },\n }\n}\n\nfunction requireSessionAndRun(ctx: ToolContext, toolName: string): { session: NonNullable<ToolContext['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 * Validates untrusted todo items: rows without a string id and content and a\n * known status drop out, then the list is capped. The same validator guards\n * the tool input and user edits.\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\n/** Last-wins id dedup, first-seen order kept. */\nfunction dedupById(items: readonly TodoItem[]): TodoItem[] {\n const byId = new Map<string, TodoItem>()\n for (const item of items)\n byId.set(item.id, item)\n return [...byId.values()]\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 { createPlanTools } 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 `createPlanTools()` on top so the model gets\n // `todowrite` / `todoread` over the session's plans (`src/chat/plans.ts`)\n // and the lifecycle hooks that close a run's plans when it ends.\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 createPlanTools(),\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 createPlanTools(),\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,4 +1,4 @@
1
- import { n as compareVersions, t as ZIDANE_VERSION } from "./version-CU8ppdCQ.js";
1
+ import { n as compareVersions, t as ZIDANE_VERSION } from "./version-Bu3Nc8-U.js";
2
2
  import { t as resolveStorageDirs } from "./xdg-DFSAQQG8.js";
3
3
  import { n as JsonRpcRemoteError, t as JsonRpcConnectionClosedError } from "./json-rpc-G0rBfEar.js";
4
4
  import { r as isWindowOnlySetting, t as HOST_ENABLED_LISTS } from "./host-scoped-settings-Dh4EILMN.js";
@@ -404,4 +404,4 @@ function isSyncableSetting(key) {
404
404
  //#endregion
405
405
  export { createSignalListeners as a, daemonSocketName as c, legacySocketPath as d, probeSocket as f, IpcVersionMismatchError as i, defaultSocketPath as l, hashValue as n, openIpcClient as o, selfDaemonSpawnCommand as p, isRecord as r, IPC_BUILD_ID as s, isSyncableSetting as t, ensureDaemonRunning as u };
406
406
 
407
- //# sourceMappingURL=settings-keys-BIWy4T_0.js.map
407
+ //# sourceMappingURL=settings-keys-DDrfnBHi.js.map