@ai-agent-forge/agent-forge 0.88.2 → 0.88.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/README.md +1 -1
- package/bundled-plugins/agent-loop-policy/agent-forge.json +1 -1
- package/bundled-plugins/agent-loop-policy/plugin.json +1 -1
- package/bundled-plugins/coding-tools/agent-forge.json +1 -1
- package/bundled-plugins/coding-tools/plugin.json +1 -1
- package/bundled-plugins/compaction-policy/agent-forge.json +1 -1
- package/bundled-plugins/compaction-policy/plugin.json +1 -1
- package/bundled-plugins/overflow-policy/agent-forge.json +1 -1
- package/bundled-plugins/overflow-policy/plugin.json +1 -1
- package/bundled-plugins/provider-failover/agent-forge.json +1 -1
- package/bundled-plugins/provider-failover/plugin.json +1 -1
- package/bundled-plugins/retry-policy/agent-forge.json +1 -1
- package/bundled-plugins/retry-policy/plugin.json +1 -1
- package/dist/artifacts/paged-read.d.ts +6 -1
- package/dist/artifacts/paged-read.d.ts.map +1 -1
- package/dist/artifacts/paged-read.js.map +1 -1
- package/dist/bundle/.build-manifest.json +244 -234
- package/dist/bundle/chunks/{anthropic-messages-PLAKXR73.js → anthropic-messages-3FJHRAJL.js} +2 -2
- package/dist/bundle/chunks/anthropic.js +1 -1
- package/dist/bundle/chunks/{azure-openai-responses-OUWQ2AZD.js → azure-openai-responses-GCL47IDS.js} +1 -1
- package/dist/bundle/chunks/bedrock-converse-stream.js +1 -1
- package/dist/bundle/chunks/{chunk-6F5JWGKE.js → chunk-32YDZI4J.js} +1 -1
- package/dist/bundle/chunks/{chunk-LSP24WNH.js → chunk-755VNWIF.js} +12 -12
- package/dist/bundle/chunks/{chunk-DRQDKNXA.js → chunk-BVVSHK6W.js} +1 -1
- package/dist/bundle/chunks/{chunk-HTC5K5SM.js → chunk-HEL6TAWE.js} +95 -90
- package/dist/bundle/chunks/{chunk-6R7UI4IY.js → chunk-QDLOMKF6.js} +1 -1
- package/dist/bundle/chunks/{chunk-44I47ACN.js → chunk-V3ACW7VD.js} +1 -1
- package/dist/bundle/chunks/{google-generative-ai-DLII6DAT.js → google-generative-ai-6BMXWA44.js} +1 -1
- package/dist/bundle/chunks/{google-vertex-G7BZL7GM.js → google-vertex-6H7NLRTK.js} +1 -1
- package/dist/bundle/chunks/image-resize-worker.js +1 -1
- package/dist/bundle/chunks/{mistral-conversations-JXUK24ID.js → mistral-conversations-IXUHSFSJ.js} +1 -1
- package/dist/bundle/chunks/{openai-codex-responses-5GSGFIPG.js → openai-codex-responses-YHUAEVCM.js} +1 -1
- package/dist/bundle/chunks/openai-codex.js +1 -1
- package/dist/bundle/chunks/{openai-completions-EI2I6DTG.js → openai-completions-ULQCFC72.js} +1 -1
- package/dist/bundle/chunks/openai-responses-NLLAJDPS.js +2 -0
- package/dist/bundle/chunks/{openrouter-images-G4GQRUSG.js → openrouter-images-GZZQZXMS.js} +1 -1
- package/dist/bundle/chunks/openrouter.js +1 -1
- package/dist/bundle/chunks/{pi-messages-PWU2VJMR.js → pi-messages-EJTI2EJ3.js} +1 -1
- package/dist/bundle/chunks/virtual-modules-TQWEGNNZ.js +5 -0
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/client.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/cli/args.d.ts +0 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +1 -4
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/config-selector.d.ts.map +1 -1
- package/dist/cli/config-selector.js +1 -1
- package/dist/cli/config-selector.js.map +1 -1
- package/dist/cli/experimental/cli.d.ts +3 -3
- package/dist/cli/experimental/cli.d.ts.map +1 -1
- package/dist/cli/experimental/cli.js +2 -2
- package/dist/cli/experimental/cli.js.map +1 -1
- package/dist/cli/experimental/commands/agent-forge.d.ts +15 -0
- package/dist/cli/experimental/commands/agent-forge.d.ts.map +1 -0
- package/dist/cli/experimental/commands/{pi.js → agent-forge.js} +4 -4
- package/dist/cli/experimental/commands/agent-forge.js.map +1 -0
- package/dist/cli/startup-ui.d.ts.map +1 -1
- package/dist/cli/startup-ui.js +1 -1
- package/dist/cli/startup-ui.js.map +1 -1
- package/dist/config.d.ts +4 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +7 -7
- package/dist/config.js.map +1 -1
- package/dist/core/agent-forge-manifest.d.ts +7 -0
- package/dist/core/agent-forge-manifest.d.ts.map +1 -0
- package/dist/core/{pi-manifest.js → agent-forge-manifest.js} +4 -4
- package/dist/core/agent-forge-manifest.js.map +1 -0
- package/dist/core/agent-session-compaction.d.ts +1 -1
- package/dist/core/agent-session-compaction.d.ts.map +1 -1
- package/dist/core/agent-session-compaction.js +3 -3
- package/dist/core/agent-session-compaction.js.map +1 -1
- package/dist/core/agent-session-lifecycle.d.ts.map +1 -1
- package/dist/core/agent-session-lifecycle.js +12 -1
- package/dist/core/agent-session-lifecycle.js.map +1 -1
- package/dist/core/agent-session-projection.d.ts.map +1 -1
- package/dist/core/agent-session-projection.js +3 -3
- package/dist/core/agent-session-projection.js.map +1 -1
- package/dist/core/agent-session-services.d.ts +1 -1
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +1 -1
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/core/compaction/branch-summarization.js +1 -1
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/experimental.d.ts.map +1 -1
- package/dist/core/experimental.js +1 -3
- package/dist/core/experimental.js.map +1 -1
- package/dist/core/extensions/capability-plugin.d.ts.map +1 -1
- package/dist/core/extensions/capability-plugin.js +15 -8
- package/dist/core/extensions/capability-plugin.js.map +1 -1
- package/dist/core/extensions/capability-runtime-store.d.ts +20 -1
- package/dist/core/extensions/capability-runtime-store.d.ts.map +1 -1
- package/dist/core/extensions/capability-runtime-store.js +67 -0
- package/dist/core/extensions/capability-runtime-store.js.map +1 -1
- package/dist/core/extensions/capability-runtime-types.d.ts +14 -0
- package/dist/core/extensions/capability-runtime-types.d.ts.map +1 -1
- package/dist/core/extensions/capability-runtime-types.js +7 -1
- package/dist/core/extensions/capability-runtime-types.js.map +1 -1
- package/dist/core/extensions/capability-runtime.d.ts.map +1 -1
- package/dist/core/extensions/capability-runtime.js +144 -42
- package/dist/core/extensions/capability-runtime.js.map +1 -1
- package/dist/core/extensions/virtual-modules.d.ts.map +1 -1
- package/dist/core/extensions/virtual-modules.js +12 -4
- package/dist/core/extensions/virtual-modules.js.map +1 -1
- package/dist/core/facade/coding-agent-facade.d.ts +1 -1
- package/dist/core/facade/coding-agent-facade.d.ts.map +1 -1
- package/dist/core/facade/coding-agent-facade.js +1 -1
- package/dist/core/facade/coding-agent-facade.js.map +1 -1
- package/dist/core/i18n/locales/zh-cn/core-contract-final.d.ts +1 -1
- package/dist/core/i18n/locales/zh-cn/core-contract-final.d.ts.map +1 -1
- package/dist/core/i18n/locales/zh-cn/core-contract-final.js +3 -3
- package/dist/core/i18n/locales/zh-cn/core-contract-final.js.map +1 -1
- package/dist/core/i18n/locales/zh-cn/core-contract-plugins.d.ts.map +1 -1
- package/dist/core/i18n/locales/zh-cn/core-contract-plugins.js +1 -1
- package/dist/core/i18n/locales/zh-cn/core-contract-plugins.js.map +1 -1
- package/dist/core/i18n/locales/zh-cn/core-errors.d.ts.map +1 -1
- package/dist/core/i18n/locales/zh-cn/core-errors.js +1 -1
- package/dist/core/i18n/locales/zh-cn/core-errors.js.map +1 -1
- package/dist/core/i18n/locales/zh-cn/tui-components-misc.d.ts.map +1 -1
- package/dist/core/i18n/locales/zh-cn/tui-components-misc.js +2 -0
- package/dist/core/i18n/locales/zh-cn/tui-components-misc.js.map +1 -1
- package/dist/core/model-resolver.d.ts +2 -0
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +4 -1
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/package-resource-collection.d.ts.map +1 -1
- package/dist/core/package-resource-collection.js +5 -5
- package/dist/core/package-resource-collection.js.map +1 -1
- package/dist/core/provider-composer.d.ts.map +1 -1
- package/dist/core/provider-composer.js +3 -3
- package/dist/core/provider-composer.js.map +1 -1
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +1 -1
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/sdk.d.ts +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +6 -7
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +4 -4
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +5 -10
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/timings.d.ts.map +1 -1
- package/dist/core/timings.js +1 -3
- package/dist/core/timings.js.map +1 -1
- package/dist/core/tools/render-utils.d.ts +1 -1
- package/dist/core/tools/render-utils.d.ts.map +1 -1
- package/dist/core/tools/render-utils.js.map +1 -1
- package/dist/core/tools/tool-definition.d.ts +1 -1
- package/dist/core/tools/tool-definition.d.ts.map +1 -1
- package/dist/core/tools/tool-definition.js.map +1 -1
- package/dist/core/trace-retention.d.ts +16 -1
- package/dist/core/trace-retention.d.ts.map +1 -1
- package/dist/core/trace-retention.js +43 -20
- package/dist/core/trace-retention.js.map +1 -1
- package/dist/export-html/ansi-to-html.d.ts.map +1 -0
- package/dist/export-html/ansi-to-html.js.map +1 -0
- package/dist/{core/export-html → export-html}/index.d.ts +1 -1
- package/dist/export-html/index.d.ts.map +1 -0
- package/dist/{core/export-html → export-html}/index.js +5 -5
- package/dist/export-html/index.js.map +1 -0
- package/dist/{core/export-html → export-html}/tool-renderer.d.ts +2 -2
- package/dist/export-html/tool-renderer.d.ts.map +1 -0
- package/dist/export-html/tool-renderer.js.map +1 -0
- package/dist/{modes/interactive → host}/model-catalog-refresh.d.ts +1 -1
- package/dist/host/model-catalog-refresh.d.ts.map +1 -0
- package/dist/{modes/interactive → host}/model-catalog-refresh.js +1 -1
- package/dist/host/model-catalog-refresh.js.map +1 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -12
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +25 -14
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/agents-view.d.ts.map +1 -1
- package/dist/modes/interactive/components/agents-view.js +1 -1
- package/dist/modes/interactive/components/agents-view.js.map +1 -1
- package/dist/modes/interactive/components/armin.d.ts.map +1 -1
- package/dist/modes/interactive/components/armin.js +1 -1
- package/dist/modes/interactive/components/armin.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts +3 -0
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +83 -24
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/bash-execution.js +1 -1
- package/dist/modes/interactive/components/bash-execution.js.map +1 -1
- package/dist/modes/interactive/components/bordered-loader.d.ts +1 -1
- package/dist/modes/interactive/components/bordered-loader.d.ts.map +1 -1
- package/dist/modes/interactive/components/bordered-loader.js.map +1 -1
- package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/branch-summary-message.js +1 -1
- package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.js +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
- package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/config-selector.js +1 -1
- package/dist/modes/interactive/components/config-selector.js.map +1 -1
- package/dist/modes/interactive/components/custom-entry.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-entry.js +1 -1
- package/dist/modes/interactive/components/custom-entry.js.map +1 -1
- package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-message.js +1 -1
- package/dist/modes/interactive/components/custom-message.js.map +1 -1
- package/dist/modes/interactive/components/daxnuts.d.ts.map +1 -1
- package/dist/modes/interactive/components/daxnuts.js +1 -1
- package/dist/modes/interactive/components/daxnuts.js.map +1 -1
- package/dist/modes/interactive/components/dialog-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/dialog-editor.js +1 -1
- package/dist/modes/interactive/components/dialog-editor.js.map +1 -1
- package/dist/modes/interactive/components/dialog-input.d.ts.map +1 -1
- package/dist/modes/interactive/components/dialog-input.js +1 -1
- package/dist/modes/interactive/components/dialog-input.js.map +1 -1
- package/dist/modes/interactive/components/dialog-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/dialog-selector.js +1 -1
- package/dist/modes/interactive/components/dialog-selector.js.map +1 -1
- package/dist/modes/interactive/components/diff.d.ts.map +1 -1
- package/dist/modes/interactive/components/diff.js +1 -1
- package/dist/modes/interactive/components/diff.js.map +1 -1
- package/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
- package/dist/modes/interactive/components/dynamic-border.js +1 -1
- package/dist/modes/interactive/components/dynamic-border.js.map +1 -1
- package/dist/modes/interactive/components/first-time-setup.d.ts +1 -1
- package/dist/modes/interactive/components/first-time-setup.d.ts.map +1 -1
- package/dist/modes/interactive/components/first-time-setup.js +1 -1
- package/dist/modes/interactive/components/first-time-setup.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +1 -1
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -1
- package/dist/modes/interactive/components/keybinding-hints.js +1 -1
- package/dist/modes/interactive/components/keybinding-hints.js.map +1 -1
- package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/dist/modes/interactive/components/login-dialog.js +1 -1
- package/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/dist/modes/interactive/components/mermaid.d.ts +1 -1
- package/dist/modes/interactive/components/mermaid.d.ts.map +1 -1
- package/dist/modes/interactive/components/mermaid.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +2 -2
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.js +1 -1
- package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
- package/dist/modes/interactive/components/plugin-manager-entry.d.ts.map +1 -1
- package/dist/modes/interactive/components/plugin-manager-entry.js +1 -1
- package/dist/modes/interactive/components/plugin-manager-entry.js.map +1 -1
- package/dist/modes/interactive/components/plugin-selection-entry.d.ts.map +1 -1
- package/dist/modes/interactive/components/plugin-selection-entry.js +1 -1
- package/dist/modes/interactive/components/plugin-selection-entry.js.map +1 -1
- package/dist/modes/interactive/components/queue-manager-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/queue-manager-selector.js +1 -1
- package/dist/modes/interactive/components/queue-manager-selector.js.map +1 -1
- package/dist/modes/interactive/components/render-types.d.ts +1 -1
- package/dist/modes/interactive/components/render-types.d.ts.map +1 -1
- package/dist/modes/interactive/components/render-types.js.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.js +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector.js +2 -2
- package/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +1 -1
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-submenu.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-submenu.js +1 -1
- package/dist/modes/interactive/components/settings-submenu.js.map +1 -1
- package/dist/modes/interactive/components/show-images-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/show-images-selector.js +1 -1
- package/dist/modes/interactive/components/show-images-selector.js.map +1 -1
- package/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/skill-invocation-message.js +1 -1
- package/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
- package/dist/modes/interactive/components/status-indicator.d.ts.map +1 -1
- package/dist/modes/interactive/components/status-indicator.js +1 -1
- package/dist/modes/interactive/components/status-indicator.js.map +1 -1
- package/dist/modes/interactive/components/theme-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/theme-selector.js +1 -1
- package/dist/modes/interactive/components/theme-selector.js.map +1 -1
- package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/thinking-selector.js +1 -1
- package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +2 -0
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +96 -4
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/tool-run-group.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-run-group.js +1 -1
- package/dist/modes/interactive/components/tool-run-group.js.map +1 -1
- package/dist/modes/interactive/components/tree-list.d.ts.map +1 -1
- package/dist/modes/interactive/components/tree-list.js +1 -1
- package/dist/modes/interactive/components/tree-list.js.map +1 -1
- package/dist/modes/interactive/components/tree-render.d.ts.map +1 -1
- package/dist/modes/interactive/components/tree-render.js +1 -1
- package/dist/modes/interactive/components/tree-render.js.map +1 -1
- package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/tree-selector.js +1 -1
- package/dist/modes/interactive/components/tree-selector.js.map +1 -1
- package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/user-message-selector.js +1 -1
- package/dist/modes/interactive/components/user-message-selector.js.map +1 -1
- package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/user-message.js +1 -1
- package/dist/modes/interactive/components/user-message.js.map +1 -1
- package/dist/modes/interactive/components/workflow-view.d.ts.map +1 -1
- package/dist/modes/interactive/components/workflow-view.js +1 -1
- package/dist/modes/interactive/components/workflow-view.js.map +1 -1
- package/dist/modes/interactive/interactive-auth.d.ts +1 -1
- package/dist/modes/interactive/interactive-auth.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-auth.js +1 -1
- package/dist/modes/interactive/interactive-auth.js.map +1 -1
- package/dist/modes/interactive/interactive-commands.d.ts +123 -6
- package/dist/modes/interactive/interactive-commands.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-commands.js +691 -4
- package/dist/modes/interactive/interactive-commands.js.map +1 -1
- package/dist/modes/interactive/interactive-events.d.ts +174 -0
- package/dist/modes/interactive/interactive-events.d.ts.map +1 -0
- package/dist/modes/interactive/interactive-events.js +704 -0
- package/dist/modes/interactive/interactive-events.js.map +1 -0
- package/dist/modes/interactive/interactive-format.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-format.js +1 -1
- package/dist/modes/interactive/interactive-format.js.map +1 -1
- package/dist/modes/interactive/interactive-lifecycle.d.ts +94 -0
- package/dist/modes/interactive/interactive-lifecycle.d.ts.map +1 -0
- package/dist/modes/interactive/interactive-lifecycle.js +177 -0
- package/dist/modes/interactive/interactive-lifecycle.js.map +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts +240 -133
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +251 -2159
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/interactive-queues.d.ts +96 -0
- package/dist/modes/interactive/interactive-queues.d.ts.map +1 -0
- package/dist/modes/interactive/interactive-queues.js +183 -0
- package/dist/modes/interactive/interactive-queues.js.map +1 -0
- package/dist/modes/interactive/interactive-selectors.d.ts +95 -5
- package/dist/modes/interactive/interactive-selectors.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-selectors.js +517 -6
- package/dist/modes/interactive/interactive-selectors.js.map +1 -1
- package/dist/modes/interactive/startup-services.d.ts +31 -0
- package/dist/modes/interactive/startup-services.d.ts.map +1 -0
- package/dist/modes/interactive/startup-services.js +123 -0
- package/dist/modes/interactive/startup-services.js.map +1 -0
- package/dist/modes/memory-preheat.d.ts +11 -0
- package/dist/modes/memory-preheat.d.ts.map +1 -0
- package/dist/modes/memory-preheat.js +20 -0
- package/dist/modes/memory-preheat.js.map +1 -0
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +3 -1
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/plugins/ecosystem/node-package-install-host.d.ts +9 -1
- package/dist/plugins/ecosystem/node-package-install-host.d.ts.map +1 -1
- package/dist/plugins/ecosystem/node-package-install-host.js +79 -0
- package/dist/plugins/ecosystem/node-package-install-host.js.map +1 -1
- package/dist/remote/server-assembly.d.ts.map +1 -1
- package/dist/remote/server-assembly.js +7 -2
- package/dist/remote/server-assembly.js.map +1 -1
- package/dist/suites/builtin-suites.d.ts.map +1 -0
- package/dist/suites/builtin-suites.js.map +1 -0
- package/dist/suites/resolved-agent-profile.d.ts.map +1 -0
- package/dist/suites/resolved-agent-profile.js.map +1 -0
- package/dist/suites/suite-lifecycle.d.ts.map +1 -0
- package/dist/suites/suite-lifecycle.js.map +1 -0
- package/dist/suites/suite-loader.d.ts.map +1 -0
- package/dist/suites/suite-loader.js.map +1 -0
- package/dist/{modes/interactive/theme → theme}/dark.json +1 -1
- package/dist/{modes/interactive/theme → theme}/light.json +1 -1
- package/dist/{modes/interactive/theme → theme}/theme-controller.d.ts +1 -1
- package/dist/theme/theme-controller.d.ts.map +1 -0
- package/dist/{modes/interactive/theme → theme}/theme-controller.js +1 -1
- package/dist/theme/theme-controller.js.map +1 -0
- package/dist/{modes/interactive/theme → theme}/theme.d.ts +1 -1
- package/dist/theme/theme.d.ts.map +1 -0
- package/dist/{modes/interactive/theme → theme}/theme.js +5 -5
- package/dist/theme/theme.js.map +1 -0
- package/docs/compaction.md +7 -7
- package/docs/environment-variables.md +10 -4
- package/docs/keybindings.md +3 -3
- package/docs/models.md +17 -17
- package/docs/packages.md +1 -1
- package/docs/prompt-templates.md +1 -1
- package/docs/rpc.md +2 -2
- package/docs/sdk.md +5 -5
- package/docs/session-format.md +3 -3
- package/docs/settings.md +15 -1
- package/docs/shell-aliases.md +1 -1
- package/docs/skills.md +1 -1
- package/docs/terminal-setup.md +17 -17
- package/docs/themes.md +5 -5
- package/docs/tmux.md +4 -4
- package/docs/tui.md +1 -1
- package/docs/usage.md +292 -292
- package/docs/windows.md +1 -1
- package/examples/extensions/bash-spawn-hook/index.ts +1 -1
- package/examples/extensions/dynamic-resources/dynamic.json +1 -1
- package/examples/extensions/mcp-stdio/index.d.ts +64 -0
- package/examples/extensions/mcp-stdio/index.d.ts.map +1 -0
- package/examples/extensions/mcp-stdio/index.js +292 -0
- package/examples/extensions/mcp-stdio/index.js.map +1 -0
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -6
- package/examples/extensions/with-deps/plugin.json +1 -1
- package/package.json +2 -2
- package/dist/bundle/chunks/openai-responses-AUJQ3NWT.js +0 -2
- package/dist/bundle/chunks/virtual-modules-MRXD3RWG.js +0 -5
- package/dist/cli/experimental/commands/pi.d.ts +0 -15
- package/dist/cli/experimental/commands/pi.d.ts.map +0 -1
- package/dist/cli/experimental/commands/pi.js.map +0 -1
- package/dist/core/export-html/ansi-to-html.d.ts.map +0 -1
- package/dist/core/export-html/ansi-to-html.js.map +0 -1
- package/dist/core/export-html/index.d.ts.map +0 -1
- package/dist/core/export-html/index.js.map +0 -1
- package/dist/core/export-html/tool-renderer.d.ts.map +0 -1
- package/dist/core/export-html/tool-renderer.js.map +0 -1
- package/dist/core/pi-manifest.d.ts +0 -7
- package/dist/core/pi-manifest.d.ts.map +0 -1
- package/dist/core/pi-manifest.js.map +0 -1
- package/dist/modes/interactive/model-catalog-refresh.d.ts.map +0 -1
- package/dist/modes/interactive/model-catalog-refresh.js.map +0 -1
- package/dist/modes/interactive/theme/theme-controller.d.ts.map +0 -1
- package/dist/modes/interactive/theme/theme-controller.js.map +0 -1
- package/dist/modes/interactive/theme/theme.d.ts.map +0 -1
- package/dist/modes/interactive/theme/theme.js.map +0 -1
- package/dist/profiles/builtin-suites.d.ts.map +0 -1
- package/dist/profiles/builtin-suites.js.map +0 -1
- package/dist/profiles/resolved-agent-profile.d.ts.map +0 -1
- package/dist/profiles/resolved-agent-profile.js.map +0 -1
- package/dist/profiles/suite-lifecycle.d.ts.map +0 -1
- package/dist/profiles/suite-lifecycle.js.map +0 -1
- package/dist/profiles/suite-loader.d.ts.map +0 -1
- package/dist/profiles/suite-loader.js.map +0 -1
- package/dist/utils/deprecation.d.ts +0 -4
- package/dist/utils/deprecation.d.ts.map +0 -1
- package/dist/utils/deprecation.js +0 -13
- package/dist/utils/deprecation.js.map +0 -1
- /package/dist/{core/export-html → export-html}/ansi-to-html.d.ts +0 -0
- /package/dist/{core/export-html → export-html}/ansi-to-html.js +0 -0
- /package/dist/{core/export-html → export-html}/template.css +0 -0
- /package/dist/{core/export-html → export-html}/template.html +0 -0
- /package/dist/{core/export-html → export-html}/template.js +0 -0
- /package/dist/{core/export-html → export-html}/tool-renderer.js +0 -0
- /package/dist/{core/export-html → export-html}/vendor/highlight.min.js +0 -0
- /package/dist/{core/export-html → export-html}/vendor/marked.min.js +0 -0
- /package/dist/{profiles → suites}/builtin-suites.d.ts +0 -0
- /package/dist/{profiles → suites}/builtin-suites.js +0 -0
- /package/dist/{profiles → suites}/resolved-agent-profile.d.ts +0 -0
- /package/dist/{profiles → suites}/resolved-agent-profile.js +0 -0
- /package/dist/{profiles → suites}/suite-lifecycle.d.ts +0 -0
- /package/dist/{profiles → suites}/suite-lifecycle.js +0 -0
- /package/dist/{profiles → suites}/suite-loader.d.ts +0 -0
- /package/dist/{profiles → suites}/suite-loader.js +0 -0
- /package/dist/{modes/interactive/theme → theme}/theme-schema.json +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-assembly.d.ts","sourceRoot":"","sources":["../../src/remote/server-assembly.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Minimal coding-agent runtime -> remote server assembly (1B.7h).\n *\n * Adapts first-party `AgentSession` instances to the public `@agent-forge/server`\n * session boundary so a preinstalled session adapter can run a deterministic\n * server end to end. The adapter only consumes the public `AgentSession` surface\n * and never reads private session objects.\n *\n * Snapshot transcripts use positional ids (`t-<index>`) that are stable within\n * one revision but not across history rewrites such as compaction; snapshots\n * stay authoritative and durable replay is journal-based, so replay does not\n * depend on these ids. Streaming is reported as snapshot-change notifications;\n * amortized delta progress belongs to a later slice.\n *\n * Durable operations and artifacts (E4-c): when a `DurableServiceV1` is\n * supplied, the service exposes the server `operations`/`artifacts` surfaces as\n * a thin adapter over the durable service — it never creates a second artifact\n * store. Artifact records project the protocol `ArtifactRef` 1:1 onto the SDK\n * `OutputArtifactRef` and carry base64 bytes so remote `artifact_stat` /\n * `artifact_read` survive restarts via the durable log replay; the runtime\n * in-memory artifact store remains the live write path (built in\n * capability-runtime). Keyed mutation commands (`operationId` on prompt/steer/\n * abort/set_model/set_thinking) persist their successful response snapshots as\n * deterministic `mutation-response:<sessionId>:<operationId>` artifact records\n * so a retry after a restart replays the recorded response; the server's\n * in-process LRU stays the live dedup path. Without a durable service the\n * surfaces stay undefined and the server answers `durable_unavailable` /\n * `not_implemented` explicitly.\n *\n * Deployment boundary: the durable writer lease is process-local memory. This\n * assembly is single-instance per durable store — the durable backend's file\n * writes are cross-process mutually exclusive (its `.lock`), while the writer\n * lease single-instance semantics remain process-local: who should claim the\n * lease still needs deployment-level coordination. On\n * startup the service claims the writer lease and, when the restored state\n * marks a dead writer (snapshot import), recovers it; a live foreign writer\n * keeps the service unavailable instead of fighting over the lease. Operations\n * left pending by a crashed process are swept to a rejected terminal so no\n * submitted operation can hang as `accepted` forever.\n */\nimport { Buffer } from \"node:buffer\";\nimport { randomUUID } from \"node:crypto\";\nimport type { Api, Model } from \"@agent-forge/ai/compat\";\nimport type {\n\tArtifactRef,\n\tJsonValue,\n\tModelMetadata,\n\tModelRef,\n\tProtocolError,\n\tProtocolErrorCode,\n\tSessionMetadata,\n\tSessionPhase,\n\tSessionSnapshot,\n\tThinkingLevel,\n\tTranscriptItem,\n\tUsage,\n} from \"@agent-forge/protocol\";\nimport {\n\ttype CreateSessionOptions,\n\ttype MutationResponseStore,\n\tPiServer,\n\tPiServerError,\n\ttype PiServerOptions,\n\ttype PiServerService,\n\ttype PiSessionArtifactStore,\n\ttype PiSessionOperationAck,\n\ttype PiSessionOperationStore,\n\ttype PiSessionRuntime,\n\ttype PiSessionRuntimeEvent,\n} from \"@agent-forge/server\";\nimport type { AgentSession, AgentSessionEvent } from \"../core/agent-session.ts\";\nimport type { OutputArtifactRef } from \"../core/extensions/public-api.ts\";\nimport { DurableServiceErrorV1, type DurableServiceV1 } from \"../durable/durable-service.ts\";\n\n/** Server-visible facts about one durable coding-agent session. */\nexport interface CodingAgentSessionDescriptor {\n\treadonly id: string;\n\treadonly name?: string;\n\t/** Working directory; omitted when unknown (legacy sessions record none). */\n\treadonly cwd?: string;\n\t/** Model instance to attach at creation; resolved from the protocol ref by the service. */\n\treadonly model?: Model<Api>;\n\t/** Thinking level for the created session. */\n\treadonly thinkingLevel?: ThinkingLevel;\n\t/**\n\t * Optional creation-time profile lock (protocol create `profileId`/\n\t * `profileVersion`) passed by the remote create command: stored unchanged\n\t * and echoed in snapshots, never interpreted by the runtime. Persisted in\n\t * the session header and echoed verbatim in snapshots across restarts;\n\t * absent when the create command did not carry it.\n\t */\n\treadonly profileId?: string;\n\treadonly profileVersion?: string;\n\treadonly createdAt: number;\n\treadonly updatedAt: number;\n}\n\n/**\n * Persistence boundary owned by the host. `create`/`open` hand over exclusive\n * ownership of one `AgentSession` to the server runtime until `dispose()`.\n */\nexport interface CodingAgentServerSessionSource {\n\tlist(): Promise<readonly CodingAgentSessionDescriptor[]>;\n\tcreate(descriptor: CodingAgentSessionDescriptor): Promise<AgentSession>;\n\topen(descriptor: CodingAgentSessionDescriptor): Promise<AgentSession>;\n}\n\n/** Model catalog projection used for `listModels` and `set_model` resolution. */\nexport interface CodingAgentServerModels {\n\tlist(): Promise<readonly ModelMetadata[]>;\n\tresolve(ref: ModelRef): Promise<Model<Api>>;\n}\n\nexport interface CodingAgentServerAssemblyOptions {\n\treadonly sessions: CodingAgentServerSessionSource;\n\treadonly models: CodingAgentServerModels;\n\treadonly listeners: PiServerOptions[\"listeners\"];\n\treadonly journal?: PiServerOptions[\"journal\"];\n\treadonly authenticate?: PiServerOptions[\"authenticate\"];\n\treadonly authorize?: PiServerOptions[\"authorize\"];\n\treadonly serverId?: string;\n\treadonly maxFrameLength?: number;\n\treadonly handshakeTimeoutMs?: number;\n\treadonly onError?: (error: Error) => void;\n\t/**\n\t * Durable service backing operation and artifact records. Single-instance\n\t * per store: the writer lease is process-local memory. Omission keeps the\n\t * server `operations` surface undefined (clients see `durable_unavailable`)\n\t * and the `artifacts` surface unconfigured (clients see `not_implemented`).\n\t */\n\treadonly durable?: DurableServiceV1;\n\t/**\n\t * Writer id claimed on the durable service. Hosts that also pass the same\n\t * store into their session source (`agentTaskResults.writerId`) supply it\n\t * here so one writer serves the whole process; omission generates a fresh\n\t * id (claim/recover behavior is unchanged).\n\t */\n\treadonly writerId?: string;\n\t/** Executors for submitted operation types; unknown types terminate as rejected. */\n\treadonly operationExecutors?: readonly CodingAgentOperationExecutor[];\n\t/** Clock for descriptor timestamps; defaults to the host clock. */\n\treadonly now?: () => number;\n}\n\nfunction requireIdle(session: AgentSession, action: string): void {\n\tif (!session.isIdle) {\n\t\tthrow new PiServerError(\"busy\", `Cannot ${action} while the session is busy`);\n\t}\n}\n\nfunction toError(error: unknown): PiServerError {\n\tif (error instanceof PiServerError) return error;\n\tconst message = error instanceof Error ? error.message : String(error);\n\tif (/reload in progress/i.test(message)) return new PiServerError(\"busy\", message);\n\tif (/compaction is in progress/i.test(message)) return new PiServerError(\"busy\", message);\n\tif (/already processing/i.test(message)) return new PiServerError(\"busy\", message);\n\t// Operational session failures (missing credentials, no active model,\n\t// extension rejections) must reach the remote client with their original\n\t// message instead of being folded into an unspecific internal error.\n\treturn new PiServerError(\"invalid_request\", message);\n}\n\nfunction jsonSafe(value: unknown): boolean {\n\tif (value === null || typeof value === \"string\" || typeof value === \"boolean\") return true;\n\tif (typeof value === \"number\") return Number.isFinite(value);\n\tif (Array.isArray(value)) return value.every(jsonSafe);\n\tif (typeof value !== \"object\" || Object.getPrototypeOf(value) !== Object.prototype) return false;\n\treturn Object.values(value).every(jsonSafe);\n}\n\n/** Payload stored in `DurableOperationRecordV1.data`. Adapter-owned shape. */\ninterface DurableOperationPayloadV1 {\n\treadonly status: \"pending\" | \"completed\" | \"rejected\";\n\treadonly sessionId: string;\n\treadonly operationType: string;\n\treadonly idempotencyKey?: string;\n\treadonly result?: JsonValue;\n\treadonly error?: { readonly code: ProtocolErrorCode; readonly message: string };\n\treadonly submittedAt: number;\n\treadonly terminalAt?: number;\n}\n\n/** Payload stored in `DurableArtifactRecordV1.data`. Adapter-owned shape. */\ninterface DurableArtifactPayloadV1 {\n\treadonly ref: OutputArtifactRef;\n\t/** Base64 artifact bytes so records survive the JSONL log replay. */\n\treadonly bytes: string;\n}\n\n/** An operation id paired with its latest recorded payload snapshot. */\ninterface RecordedOperation {\n\treadonly operationId: string;\n\treadonly payload: DurableOperationPayloadV1;\n}\n\n/** Parses an adapter-owned operation payload; undefined for foreign or malformed records. */\nfunction parseOperationPayload(value: unknown): DurableOperationPayloadV1 | undefined {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) return undefined;\n\tconst record = value as Record<string, unknown>;\n\tif (record.status !== \"pending\" && record.status !== \"completed\" && record.status !== \"rejected\") return undefined;\n\tif (typeof record.sessionId !== \"string\" || record.sessionId === \"\") return undefined;\n\tif (typeof record.operationType !== \"string\") return undefined;\n\tif (typeof record.submittedAt !== \"number\" || !Number.isFinite(record.submittedAt)) return undefined;\n\tlet error: DurableOperationPayloadV1[\"error\"];\n\tif (record.error !== undefined) {\n\t\tif (typeof record.error !== \"object\" || record.error === null || Array.isArray(record.error)) return undefined;\n\t\tconst entry = record.error as Record<string, unknown>;\n\t\tif (typeof entry.code !== \"string\" || typeof entry.message !== \"string\") return undefined;\n\t\terror = { code: entry.code as ProtocolErrorCode, message: entry.message };\n\t}\n\treturn {\n\t\tstatus: record.status,\n\t\tsessionId: record.sessionId,\n\t\toperationType: record.operationType,\n\t\t...(typeof record.idempotencyKey === \"string\" ? { idempotencyKey: record.idempotencyKey } : {}),\n\t\t...(record.result === undefined ? {} : { result: record.result as JsonValue }),\n\t\t...(error === undefined ? {} : { error }),\n\t\tsubmittedAt: record.submittedAt,\n\t\t...(typeof record.terminalAt === \"number\" ? { terminalAt: record.terminalAt } : {}),\n\t};\n}\n\n/** Parses an adapter-owned artifact payload; undefined for foreign or malformed records. */\nfunction parseArtifactPayload(value: unknown): DurableArtifactPayloadV1 | undefined {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) return undefined;\n\tconst record = value as Record<string, unknown>;\n\tconst ref = record.ref;\n\tif (typeof ref !== \"object\" || ref === null || Array.isArray(ref)) return undefined;\n\tconst entry = ref as Record<string, unknown>;\n\tif (\n\t\ttypeof entry.sessionId !== \"string\" ||\n\t\ttypeof entry.operationId !== \"string\" ||\n\t\ttypeof entry.artifactId !== \"string\" ||\n\t\ttypeof entry.kind !== \"string\" ||\n\t\ttypeof entry.mediaType !== \"string\" ||\n\t\ttypeof entry.truncated !== \"boolean\" ||\n\t\ttypeof entry.createdAt !== \"number\" ||\n\t\ttypeof record.bytes !== \"string\"\n\t) {\n\t\treturn undefined;\n\t}\n\treturn {\n\t\tref: {\n\t\t\tsessionId: entry.sessionId,\n\t\t\toperationId: entry.operationId,\n\t\t\tartifactId: entry.artifactId,\n\t\t\tkind: entry.kind,\n\t\t\tmediaType: entry.mediaType,\n\t\t\t...(typeof entry.sizeBytes === \"number\" ? { sizeBytes: entry.sizeBytes } : {}),\n\t\t\ttruncated: entry.truncated,\n\t\t\tcreatedAt: entry.createdAt,\n\t\t},\n\t\tbytes: record.bytes,\n\t};\n}\n\n/**\n * Parses a stored mutation-response payload; undefined for foreign or\n * malformed records. The required-field assertion is deliberately minimal: the\n * full `SessionSnapshot` shape is already enforced by the wire codec when the\n * replayed snapshot is encoded into a response, so a damaged record only needs\n * to fail this check to fall back to a fresh execution.\n */\nfunction parseMutationResponsePayload(value: unknown, sessionId: string): SessionSnapshot | undefined {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) return undefined;\n\tconst record = value as Record<string, unknown>;\n\tif (\n\t\trecord.id !== sessionId ||\n\t\ttypeof record.cwd !== \"string\" ||\n\t\trecord.cwd === \"\" ||\n\t\ttypeof record.revision !== \"number\" ||\n\t\t!Number.isInteger(record.revision) ||\n\t\trecord.revision < 0 ||\n\t\t!Array.isArray(record.transcript)\n\t) {\n\t\treturn undefined;\n\t}\n\treturn value as SessionSnapshot;\n}\n\n/** Protocol `ArtifactRef` is a 1:1 projection of the SDK `OutputArtifactRef`. */\nfunction toProtocolArtifactRef(ref: OutputArtifactRef): ArtifactRef {\n\treturn {\n\t\tsessionId: ref.sessionId,\n\t\toperationId: ref.operationId,\n\t\tartifactId: ref.artifactId,\n\t\tkind: ref.kind,\n\t\tmediaType: ref.mediaType,\n\t\t...(ref.sizeBytes === undefined ? {} : { sizeBytes: ref.sizeBytes }),\n\t\ttruncated: ref.truncated,\n\t\tcreatedAt: ref.createdAt,\n\t};\n}\n\n/** Executes one submitted operation type; the abort signal fires on cancel requests. */\nexport interface CodingAgentOperationExecutor {\n\treadonly operationType: string;\n\texecute(input: {\n\t\treadonly sessionId: string;\n\t\treadonly operationId: string;\n\t\treadonly input?: JsonValue;\n\t\treadonly signal: AbortSignal;\n\t}): Promise<JsonValue>;\n}\n\nfunction projectUsage(usage: unknown): Usage | undefined {\n\tif (!usage || typeof usage !== \"object\" || Array.isArray(usage)) return undefined;\n\tconst record = usage as Record<string, unknown>;\n\tconst readCount = (key: string): number | undefined => {\n\t\tconst value = record[key];\n\t\treturn typeof value === \"number\" && Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : undefined;\n\t};\n\tconst input = readCount(\"input\");\n\tconst output = readCount(\"output\");\n\tconst cacheRead = readCount(\"cacheRead\");\n\tconst cacheWrite = readCount(\"cacheWrite\");\n\tconst totalTokens = readCount(\"totalTokens\");\n\tif (input === undefined || output === undefined || cacheRead === undefined || cacheWrite === undefined) {\n\t\treturn undefined;\n\t}\n\tif (totalTokens === undefined) return undefined;\n\tconst cost = record.cost;\n\tif (!cost || typeof cost !== \"object\" || Array.isArray(cost)) return undefined;\n\tconst costRecord = cost as Record<string, unknown>;\n\tconst readCost = (key: string): number | undefined => {\n\t\tconst value = costRecord[key];\n\t\treturn typeof value === \"number\" && Number.isFinite(value) ? value : undefined;\n\t};\n\tconst costInput = readCost(\"input\");\n\tconst costOutput = readCost(\"output\");\n\tconst costCacheRead = readCost(\"cacheRead\");\n\tconst costCacheWrite = readCost(\"cacheWrite\");\n\tconst costTotal = readCost(\"total\");\n\tif (\n\t\tcostInput === undefined ||\n\t\tcostOutput === undefined ||\n\t\tcostCacheRead === undefined ||\n\t\tcostCacheWrite === undefined ||\n\t\tcostTotal === undefined\n\t) {\n\t\treturn undefined;\n\t}\n\tconst reasoning = readCount(\"reasoning\");\n\treturn {\n\t\tinput,\n\t\toutput,\n\t\tcacheRead,\n\t\tcacheWrite,\n\t\t...(reasoning === undefined ? {} : { reasoning }),\n\t\ttotalTokens,\n\t\tcost: {\n\t\t\tinput: costInput,\n\t\t\toutput: costOutput,\n\t\t\tcacheRead: costCacheRead,\n\t\t\tcacheWrite: costCacheWrite,\n\t\t\ttotal: costTotal,\n\t\t},\n\t};\n}\n\ntype ImagePart = { type: \"image\"; data: string; mimeType: string };\ntype TextPart = { type: \"text\"; text: string };\n\nfunction projectUserContent(content: string | readonly unknown[]): Array<TextPart | ImagePart> {\n\tif (typeof content === \"string\") return [{ type: \"text\", text: content }];\n\tconst projected: Array<TextPart | ImagePart> = [];\n\tfor (const part of content) {\n\t\tif (!part || typeof part !== \"object\") continue;\n\t\tconst record = part as Record<string, unknown>;\n\t\tif (record.type === \"text\" && typeof record.text === \"string\") {\n\t\t\tprojected.push({ type: \"text\", text: record.text });\n\t\t} else if (record.type === \"image\" && typeof record.data === \"string\" && typeof record.mimeType === \"string\") {\n\t\t\tprojected.push({ type: \"image\", data: record.data, mimeType: record.mimeType });\n\t\t}\n\t}\n\treturn projected;\n}\n\nfunction projectToolResultContent(content: readonly unknown[]): Array<TextPart | ImagePart> {\n\treturn projectUserContent(content);\n}\n\n/**\n * Projects the current `AgentSession` messages onto the protocol transcript.\n * Tool inputs are recovered from the matching assistant `toolCall` content;\n * `null` marks an unrecoverable input instead of fabricating one. `!`-command\n * bash executions project as `bash` tool items. Session-level summary and\n * extension messages (`branchSummary`, `compactionSummary`, `custom`) project\n * as snapshot-only `note` items; they never enter streaming progress. Note\n * items carry the text/summary face only — custom-message `details` and image\n * content parts are not projected. Item ids\n * stay positional (`t-<index>`): they are stable within one revision but not\n * across history rewrites such as compaction.\n */\nexport function projectSessionTranscript(session: AgentSession): TranscriptItem[] {\n\tconst messages = session.messages;\n\tconst toolInputs = new Map<string, JsonValue>();\n\tfor (const message of messages) {\n\t\tif (message.role !== \"assistant\") continue;\n\t\tfor (const part of message.content) {\n\t\t\tif (part.type === \"toolCall\") toolInputs.set(part.id, part.arguments as JsonValue);\n\t\t}\n\t}\n\tconst items: TranscriptItem[] = [];\n\tfor (const message of messages) {\n\t\tconst id = `t-${items.length}`;\n\t\tconst timestamp = Math.max(0, Math.trunc(message.timestamp));\n\t\tif (message.role === \"user\") {\n\t\t\titems.push({\n\t\t\t\tid,\n\t\t\t\trole: \"user\",\n\t\t\t\tcontent: projectUserContent(message.content),\n\t\t\t\ttimestamp,\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\t\tif (message.role === \"toolResult\") {\n\t\t\tconst input = toolInputs.get(message.toolCallId);\n\t\t\tconst usage = projectUsage(message.usage);\n\t\t\tconst details = jsonSafe(message.details) ? (message.details as JsonValue) : undefined;\n\t\t\tconst base = {\n\t\t\t\tid,\n\t\t\t\trole: \"tool\" as const,\n\t\t\t\ttoolCallId: message.toolCallId,\n\t\t\t\ttoolName: message.toolName,\n\t\t\t\tinput: input !== undefined && jsonSafe(input) ? input : null,\n\t\t\t\tcontent: projectToolResultContent(message.content),\n\t\t\t\t...(details === undefined ? {} : { details }),\n\t\t\t\t...(usage === undefined ? {} : { usage }),\n\t\t\t\ttimestamp,\n\t\t\t};\n\t\t\tif (message.isError) {\n\t\t\t\tconst item: TranscriptItem = { ...base, status: \"error\", isError: true };\n\t\t\t\titems.push(item);\n\t\t\t} else {\n\t\t\t\tconst item: TranscriptItem = { ...base, status: \"complete\", isError: false };\n\t\t\t\titems.push(item);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tif (message.role === \"bashExecution\") {\n\t\t\tconst failed = message.cancelled || (message.exitCode !== undefined && message.exitCode !== 0);\n\t\t\tconst base = {\n\t\t\t\tid,\n\t\t\t\trole: \"tool\" as const,\n\t\t\t\ttoolCallId: `bash-${id}`,\n\t\t\t\ttoolName: \"bash\",\n\t\t\t\tinput: { command: message.command } satisfies JsonValue,\n\t\t\t\tcontent: [{ type: \"text\" as const, text: message.output }],\n\t\t\t\tdetails: {\n\t\t\t\t\texitCode: message.exitCode ?? null,\n\t\t\t\t\tcancelled: message.cancelled,\n\t\t\t\t\ttruncated: message.truncated,\n\t\t\t\t} satisfies JsonValue,\n\t\t\t\ttimestamp,\n\t\t\t};\n\t\t\tif (failed) {\n\t\t\t\tconst item: TranscriptItem = { ...base, status: \"error\", isError: true };\n\t\t\t\titems.push(item);\n\t\t\t} else {\n\t\t\t\tconst item: TranscriptItem = { ...base, status: \"complete\", isError: false };\n\t\t\t\titems.push(item);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tif (message.role === \"branchSummary\") {\n\t\t\titems.push({\n\t\t\t\tid,\n\t\t\t\trole: \"note\",\n\t\t\t\tkind: \"branchSummary\",\n\t\t\t\ttext: message.summary,\n\t\t\t\tfromId: message.fromId,\n\t\t\t\ttimestamp,\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\t\tif (message.role === \"compactionSummary\") {\n\t\t\titems.push({\n\t\t\t\tid,\n\t\t\t\trole: \"note\",\n\t\t\t\tkind: \"compactionSummary\",\n\t\t\t\ttext: message.summary,\n\t\t\t\ttokensBefore: Math.max(0, Math.trunc(message.tokensBefore)),\n\t\t\t\ttimestamp,\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\t\tif (message.role === \"custom\") {\n\t\t\tconst text =\n\t\t\t\ttypeof message.content === \"string\"\n\t\t\t\t\t? message.content\n\t\t\t\t\t: message.content\n\t\t\t\t\t\t\t.filter((part) => part.type === \"text\")\n\t\t\t\t\t\t\t.map((part) => part.text)\n\t\t\t\t\t\t\t.join(\"\\n\");\n\t\t\titems.push({\n\t\t\t\tid,\n\t\t\t\trole: \"note\",\n\t\t\t\tkind: \"custom\",\n\t\t\t\ttext,\n\t\t\t\tcustomType: message.customType,\n\t\t\t\tdisplay: message.display,\n\t\t\t\ttimestamp,\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\t\tif (message.role !== \"assistant\") continue;\n\t\tconst streaming = session.isStreaming && message === messages.at(-1);\n\t\tconst content: Array<\n\t\t\t| { type: \"text\"; text: string }\n\t\t\t| { type: \"thinking\"; thinking: string; redacted?: boolean }\n\t\t\t| { type: \"toolCall\"; toolCallId: string; toolName: string; input: JsonValue }\n\t\t> = message.content.map((part) => {\n\t\t\tif (part.type === \"text\") return { type: \"text\", text: part.text };\n\t\t\tif (part.type === \"thinking\") {\n\t\t\t\treturn {\n\t\t\t\t\ttype: \"thinking\",\n\t\t\t\t\tthinking: part.thinking,\n\t\t\t\t\t...(part.redacted === undefined ? {} : { redacted: part.redacted }),\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\ttype: \"toolCall\",\n\t\t\t\ttoolCallId: part.id,\n\t\t\t\ttoolName: part.name,\n\t\t\t\tinput: part.arguments as JsonValue,\n\t\t\t};\n\t\t});\n\t\tconst usage = projectUsage(message.usage);\n\t\tconst common = {\n\t\t\tid,\n\t\t\trole: \"assistant\" as const,\n\t\t\tcontent,\n\t\t\tmodel: { provider: message.provider, id: message.model } satisfies ModelRef,\n\t\t\t...(message.responseModel ? { responseModel: message.responseModel } : {}),\n\t\t\t...(usage === undefined ? {} : { usage }),\n\t\t\ttimestamp,\n\t\t};\n\t\tif (streaming || message.stopReason === \"pending\" || message.stopReason === \"deferred\") {\n\t\t\tconst item: TranscriptItem = { ...common, status: \"streaming\" };\n\t\t\titems.push(item);\n\t\t} else if (message.stopReason === \"error\") {\n\t\t\tconst item: TranscriptItem = {\n\t\t\t\t...common,\n\t\t\t\tstatus: \"error\",\n\t\t\t\tstopReason: \"error\",\n\t\t\t\t...(message.errorMessage ? { errorMessage: message.errorMessage } : {}),\n\t\t\t};\n\t\t\titems.push(item);\n\t\t} else if (message.stopReason === \"aborted\") {\n\t\t\tconst item: TranscriptItem = {\n\t\t\t\t...common,\n\t\t\t\tstatus: \"aborted\",\n\t\t\t\tstopReason: \"aborted\",\n\t\t\t\t...(message.errorMessage ? { errorMessage: message.errorMessage } : {}),\n\t\t\t};\n\t\t\titems.push(item);\n\t\t} else {\n\t\t\tconst item: TranscriptItem = { ...common, status: \"complete\", stopReason: message.stopReason };\n\t\t\titems.push(item);\n\t\t}\n\t}\n\treturn items;\n}\n\nfunction sessionPhase(session: AgentSession): SessionPhase {\n\tif (session.isCompacting) return \"compaction\";\n\tif (session.isStreaming) return \"turn\";\n\treturn \"idle\";\n}\n\n/** Server-side runtime over one exclusively owned coding-agent session. */\nexport class CodingAgentSessionRuntime implements PiSessionRuntime {\n\tprivate readonly session: AgentSession;\n\tprivate readonly descriptor: CodingAgentSessionDescriptor;\n\tprivate readonly models: CodingAgentServerModels;\n\tprivate readonly listeners = new Set<(event: PiSessionRuntimeEvent) => void>();\n\tprivate readonly unsubscribe: () => void;\n\tprivate readonly onDisposed?: () => void;\n\tprivate disposed = false;\n\n\tconstructor(\n\t\tsession: AgentSession,\n\t\tdescriptor: CodingAgentSessionDescriptor,\n\t\toptions?: { readonly models?: CodingAgentServerModels; readonly onDisposed?: () => void },\n\t) {\n\t\tthis.session = session;\n\t\tthis.descriptor = descriptor;\n\t\tthis.models = options?.models ?? {\n\t\t\tlist: async () => [],\n\t\t\tresolve: () => {\n\t\t\t\tthrow new PiServerError(\"not_implemented\", \"Model resolution is not configured\");\n\t\t\t},\n\t\t};\n\t\tthis.onDisposed = options?.onDisposed;\n\t\tthis.unsubscribe = session.subscribe((event: AgentSessionEvent) => this.onSessionEvent(event));\n\t}\n\n\tsnapshot(): SessionSnapshot {\n\t\tconst queuedSteer = this.session.getSteeringMessages();\n\t\tconst name = this.session.sessionName ?? this.descriptor.name;\n\t\treturn {\n\t\t\tid: this.descriptor.id,\n\t\t\t...(name === undefined ? {} : { name }),\n\t\t\t// The protocol requires a non-empty snapshot cwd. This runtime only\n\t\t\t// describes acquired sessions: creates validate a non-empty cwd and\n\t\t\t// opens fall back to a non-empty cwd inside the source, so the\n\t\t\t// SessionManager cwd is the authoritative value when the descriptor\n\t\t\t// carries none (legacy sessions record no cwd).\n\t\t\tcwd: this.descriptor.cwd ?? this.session.sessionManager.getCwd(),\n\t\t\tcreatedAt: this.descriptor.createdAt,\n\t\t\tupdatedAt: this.descriptor.updatedAt,\n\t\t\tphase: sessionPhase(this.session),\n\t\t\tmodel: this.currentModelRef(),\n\t\t\tthinkingLevel: this.session.thinkingLevel,\n\t\t\t...(this.descriptor.profileId === undefined ? {} : { profileId: this.descriptor.profileId }),\n\t\t\t...(this.descriptor.profileVersion === undefined ? {} : { profileVersion: this.descriptor.profileVersion }),\n\t\t\tattached: false,\n\t\t\tlocked: false,\n\t\t\t// Durable revision: the SessionManager entry sequence. It survives\n\t\t\t// reopen and only advances when a persisted entry lands.\n\t\t\trevision: this.session.getRevision().sequence,\n\t\t\ttranscript: projectSessionTranscript(this.session),\n\t\t\tqueuedSteer: queuedSteer.map((text, index) => ({\n\t\t\t\tid: `steer-${index}`,\n\t\t\t\trole: \"user\" as const,\n\t\t\t\tcontent: [{ type: \"text\" as const, text }],\n\t\t\t\ttimestamp: this.descriptor.updatedAt,\n\t\t\t})),\n\t\t\tqueuedSteerCount: queuedSteer.length,\n\t\t};\n\t}\n\n\tgetPhase(): SessionPhase {\n\t\treturn sessionPhase(this.session);\n\t}\n\n\tasync prompt(input: { readonly text: string }): Promise<void> {\n\t\tthis.assertUsable();\n\t\trequireIdle(this.session, \"submit a prompt\");\n\t\ttry {\n\t\t\tawait this.session.prompt(input.text);\n\t\t} catch (error) {\n\t\t\tthrow toError(error);\n\t\t}\n\t}\n\n\tasync steer(input: { readonly text: string }): Promise<void> {\n\t\tthis.assertUsable();\n\t\tif (this.session.isIdle) {\n\t\t\tthrow new PiServerError(\"busy\", \"There is no active prompt to steer\");\n\t\t}\n\t\ttry {\n\t\t\tawait this.session.steer(input.text);\n\t\t} catch (error) {\n\t\t\tthrow toError(error);\n\t\t}\n\t}\n\n\tasync abort(): Promise<void> {\n\t\tthis.assertUsable();\n\t\ttry {\n\t\t\tawait this.session.abort();\n\t\t} catch (error) {\n\t\t\tthrow toError(error);\n\t\t}\n\t}\n\n\tasync setModel(model: ModelRef): Promise<void> {\n\t\tthis.assertUsable();\n\t\trequireIdle(this.session, \"change the model\");\n\t\tlet resolved: Model<Api>;\n\t\ttry {\n\t\t\tresolved = await this.models.resolve(model);\n\t\t\tawait this.session.setModel(resolved);\n\t\t} catch (error) {\n\t\t\tthrow toError(error);\n\t\t}\n\t}\n\n\tasync setThinking(thinkingLevel: ThinkingLevel): Promise<void> {\n\t\tthis.assertUsable();\n\t\trequireIdle(this.session, \"change the thinking level\");\n\t\ttry {\n\t\t\tthis.session.setThinkingLevel(thinkingLevel);\n\t\t} catch (error) {\n\t\t\tthrow toError(error);\n\t\t}\n\t}\n\n\tsubscribe(listener: (event: PiSessionRuntimeEvent) => void): () => void {\n\t\tthis.listeners.add(listener);\n\t\treturn () => this.listeners.delete(listener);\n\t}\n\n\tasync dispose(): Promise<void> {\n\t\tif (this.disposed) return;\n\t\tthis.disposed = true;\n\t\tthis.unsubscribe();\n\t\ttry {\n\t\t\tawait this.session.dispose();\n\t\t} finally {\n\t\t\tthis.onDisposed?.();\n\t\t}\n\t}\n\n\tprivate assertUsable(): void {\n\t\tif (this.disposed) throw new PiServerError(\"invalid_request\", \"Session runtime is disposed\");\n\t}\n\n\tprivate onSessionEvent(_event: AgentSessionEvent): void {\n\t\t// Every event still broadcasts a snapshot, but the revision value is the\n\t\t// durable SessionManager sequence: events that persist no entry keep the\n\t\t// same revision number. Clients accept equal-revision snapshots (only\n\t\t// regressions are dropped), so no state is lost.\n\t\tfor (const listener of [...this.listeners]) {\n\t\t\ttry {\n\t\t\t\tlistener({ type: \"snapshot\" });\n\t\t\t} catch {\n\t\t\t\t// Snapshot observers must not affect the session.\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate currentModelRef(): ModelRef {\n\t\tconst model = this.session.model;\n\t\t// Defensive layer: the create/open preflights reject model-less sessions,\n\t\t// so a live runtime always has a model. A residual model-less runtime must\n\t\t// still fail diagnosably instead of surfacing as an unspecific internal\n\t\t// error through snapshot projection.\n\t\tif (!model) throw new PiServerError(\"invalid_request\", \"Session has no active model\");\n\t\treturn { provider: model.provider, id: model.id };\n\t}\n}\n\n/** Exclusive `PiServerService` over a coding-agent session source. */\nexport class CodingAgentServerService implements PiServerService {\n\tprivate readonly source: CodingAgentServerSessionSource;\n\tprivate readonly models: CodingAgentServerModels;\n\tprivate readonly now: () => number;\n\tprivate readonly acquired = new Map<string, CodingAgentSessionRuntime>();\n\tprivate readonly opening = new Set<string>();\n\tprivate readonly durable: DurableServiceV1 | undefined;\n\tprivate readonly executors = new Map<string, CodingAgentOperationExecutor>();\n\t/** In-memory operation store surface; undefined without a durable service. */\n\tprivate readonly operationStore: PiSessionOperationStore | undefined;\n\t/** In-memory artifact store surface; undefined without a durable service. */\n\tprivate readonly artifactStore: PiSessionArtifactStore | undefined;\n\t/** Durable mutation-response store surface; undefined without a durable service. */\n\tprivate readonly mutationResponseStore: MutationResponseStore | undefined;\n\tprivate readonly writerId: string | undefined;\n\tprivate leaseReady = false;\n\t/** Idempotency key (`sessionId\\0key`) -> recorded operation. */\n\tprivate readonly idempotencyIndex = new Map<string, RecordedOperation>();\n\t/** Operation id -> recorded payload snapshot for idempotent acks. */\n\tprivate readonly recordedOperations = new Map<string, RecordedOperation>();\n\t/** Active executor abort controllers by operation id. */\n\tprivate readonly inFlight = new Map<string, AbortController>();\n\t/** Cancel requests observed while the executor was still draining. */\n\tprivate readonly cancelRequested = new Set<string>();\n\n\tconstructor(options: {\n\t\treadonly sessions: CodingAgentServerSessionSource;\n\t\treadonly models: CodingAgentServerModels;\n\t\treadonly durable?: DurableServiceV1;\n\t\treadonly writerId?: string;\n\t\treadonly operationExecutors?: readonly CodingAgentOperationExecutor[];\n\t\treadonly now?: () => number;\n\t}) {\n\t\tthis.source = options.sessions;\n\t\tthis.models = options.models;\n\t\tthis.now = options.now ?? (() => Date.now());\n\t\tfor (const executor of options.operationExecutors ?? []) this.executors.set(executor.operationType, executor);\n\t\tif (options.durable) {\n\t\t\tthis.durable = options.durable;\n\t\t\tthis.writerId = options.writerId ?? randomUUID();\n\t\t\tthis.claimWriterLease();\n\t\t\tif (this.leaseReady) this.sweepInterruptedOperations();\n\t\t\tthis.operationStore = {\n\t\t\t\tsubmit: (input) => this.submitOperation(input),\n\t\t\t\tcancel: (input) => this.cancelOperation(input),\n\t\t\t\tresult: (input) => this.resultOperation(input),\n\t\t\t};\n\t\t\tthis.artifactStore = {\n\t\t\t\tstat: (sessionId, artifactId) => this.statArtifact(sessionId, artifactId),\n\t\t\t\tread: (sessionId, artifactId, readerOptions) => this.readArtifact(sessionId, artifactId, readerOptions),\n\t\t\t};\n\t\t\tthis.mutationResponseStore = {\n\t\t\t\tload: (sessionId, operationId) => this.loadMutationResponse(sessionId, operationId),\n\t\t\t\trecord: (sessionId, operationId, snapshot) => this.recordMutationResponse(sessionId, operationId, snapshot),\n\t\t\t};\n\t\t}\n\t}\n\n\t/** Remote artifact access; unconfigured without a durable service. */\n\tget artifacts(): PiSessionArtifactStore | undefined {\n\t\treturn this.artifactStore;\n\t}\n\n\t/** Durable operation commands; unconfigured without a durable service. */\n\tget operations(): PiSessionOperationStore | undefined {\n\t\treturn this.operationStore;\n\t}\n\n\t/** Durable replay of mutation-command responses; unconfigured without a durable service. */\n\tget mutationResponses(): MutationResponseStore | undefined {\n\t\treturn this.mutationResponseStore;\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Durable writer lease (single-instance boundary)\n\t// -------------------------------------------------------------------------\n\n\t/**\n\t * Claims the writer lease at startup. A holder found in restored state\n\t * belonged to a dead process (snapshot import), so it is recovered; a live\n\t * foreign writer keeps the lease and this service stays unavailable.\n\t */\n\tprivate claimWriterLease(): void {\n\t\tconst durable = this.durable;\n\t\tconst writerId = this.writerId;\n\t\tif (!durable || !writerId) return;\n\t\tif (durable.claimWriter(writerId).status === \"claimed\") {\n\t\t\tthis.leaseReady = true;\n\t\t\treturn;\n\t\t}\n\t\tthis.leaseReady = durable.recoverWriter(writerId).status === \"recovered\";\n\t}\n\n\t/**\n\t * Releases the store writer lease on graceful shutdown (D-067 phase 2 S2)\n\t * so a successor process can claim immediately. Best-effort: only a held\n\t * lease is released, and failures are swallowed — a crashed process leaves\n\t * the lease for the TTL recovery path instead.\n\t */\n\treleaseWriterLease(): void {\n\t\tconst durable = this.durable;\n\t\tconst writerId = this.writerId;\n\t\tif (!durable || !writerId || !this.leaseReady) return;\n\t\ttry {\n\t\t\tdurable.releaseWriter(writerId);\n\t\t\tthis.leaseReady = false;\n\t\t} catch {\n\t\t\t// Shutdown release is advisory; the TTL recovery path handles the rest.\n\t\t}\n\t}\n\n\t/** Persists one adapter-owned operation record; a lost lease degrades to `durable_unavailable`. */\n\tprivate persistOperation(operationId: string, sessionId: string, payload: DurableOperationPayloadV1): void {\n\t\ttry {\n\t\t\tthis.durable?.persistOperation({\n\t\t\t\twriterId: this.writerId ?? \"\",\n\t\t\t\toperationId,\n\t\t\t\tsessionId,\n\t\t\t\tdata: payload,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tif (error instanceof DurableServiceErrorV1) {\n\t\t\t\tthrow new PiServerError(\"durable_unavailable\", error.message);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tprivate requireDurable(): DurableServiceV1 {\n\t\tif (!this.durable || !this.leaseReady) {\n\t\t\tthrow new PiServerError(\"durable_unavailable\", \"Durable operation store is not ready\");\n\t\t}\n\t\treturn this.durable;\n\t}\n\n\t/**\n\t * Rejects operations that were pending when the previous process stopped:\n\t * no executor survives a crash, so replayed `pending` records are swept to\n\t * a rejected terminal and their idempotency keys keep returning that\n\t/**\n\t * recorded terminal.\n\t */\n\tprivate sweepInterruptedOperations(): void {\n\t\tconst durable = this.durable;\n\t\tif (!durable) return;\n\t\tfor (const record of durable.exportState().operations) {\n\t\t\tconst payload = parseOperationPayload(record.data);\n\t\t\tif (!payload) continue;\n\t\t\tconst resolved: DurableOperationPayloadV1 =\n\t\t\t\tpayload.status === \"pending\"\n\t\t\t\t\t? {\n\t\t\t\t\t\t\t...payload,\n\t\t\t\t\t\t\tstatus: \"rejected\",\n\t\t\t\t\t\t\terror: { code: \"internal_error\", message: \"Operation did not complete before the server stopped\" },\n\t\t\t\t\t\t\tterminalAt: this.now(),\n\t\t\t\t\t\t}\n\t\t\t\t\t: payload;\n\t\t\tif (resolved !== payload) this.persistOperation(record.operationId, payload.sessionId, resolved);\n\t\t\tthis.recordOperation(record.operationId, resolved);\n\t\t}\n\t}\n\n\tprivate recordOperation(operationId: string, payload: DurableOperationPayloadV1): void {\n\t\tthis.recordedOperations.set(operationId, { operationId, payload });\n\t\tif (payload.idempotencyKey !== undefined) {\n\t\t\tthis.idempotencyIndex.set(`${payload.sessionId}\\0${payload.idempotencyKey}`, { operationId, payload });\n\t\t}\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Operation commands (server `operations` surface)\n\t// -------------------------------------------------------------------------\n\n\tprivate async submitOperation(input: {\n\t\treadonly sessionId: string;\n\t\treadonly operationType: string;\n\t\treadonly input?: JsonValue;\n\t\treadonly idempotencyKey?: string;\n\t\treadonly expectedRevision?: number;\n\t}): Promise<PiSessionOperationAck> {\n\t\tthis.requireDurable();\n\t\t// Idempotent replay precedes the live-session check: a recorded terminal\n\t\t// must stay answerable after a restart even though no runtime is open.\n\t\tif (input.idempotencyKey !== undefined) {\n\t\t\tconst recorded = this.idempotencyIndex.get(`${input.sessionId}\\0${input.idempotencyKey}`);\n\t\t\tif (recorded) return this.ackOf(recorded);\n\t\t}\n\t\tconst runtime = this.acquired.get(input.sessionId);\n\t\tif (!runtime) throw new PiServerError(\"not_found\", `Session is not open: ${input.sessionId}`);\n\t\tif (input.expectedRevision !== undefined) {\n\t\t\tconst revision = (await runtime.snapshot()).revision;\n\t\t\tif (revision !== input.expectedRevision) {\n\t\t\t\tthrow new PiServerError(\n\t\t\t\t\t\"operation_conflict\",\n\t\t\t\t\t`Session revision is ${revision}, expectedRevision was ${input.expectedRevision}`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t\tconst operationId = randomUUID();\n\t\tconst payload: DurableOperationPayloadV1 = {\n\t\t\tstatus: \"pending\",\n\t\t\tsessionId: input.sessionId,\n\t\t\toperationType: input.operationType,\n\t\t\t...(input.idempotencyKey === undefined ? {} : { idempotencyKey: input.idempotencyKey }),\n\t\t\tsubmittedAt: this.now(),\n\t\t};\n\t\tthis.persistOperation(operationId, input.sessionId, payload);\n\t\tthis.recordOperation(operationId, payload);\n\t\t// Fire-and-forget execution: the ack must reach the client immediately,\n\t\t// otherwise a long operation hides its own operationId and cancel/result\n\t\t// stay unreachable. `executeOperation` records executor failures as\n\t\t// rejected terminals; only a terminal persist failure can still escape,\n\t\t// so pin the in-memory record to a rejected terminal carrying the\n\t\t// original error. The durable log keeps `pending` and the restart sweep\n\t\t// converges the record.\n\t\tthis.executeOperation(operationId, payload, input.input).catch((error: unknown) => {\n\t\t\tthis.recordOperation(operationId, {\n\t\t\t\t...payload,\n\t\t\t\tstatus: \"rejected\",\n\t\t\t\terror: { code: \"internal_error\", message: error instanceof Error ? error.message : String(error) },\n\t\t\t\tterminalAt: this.now(),\n\t\t\t});\n\t\t});\n\t\treturn this.ackOf({ operationId, payload });\n\t}\n\n\t/**\n\t * Runs the executor and persists the terminal state. Cancel is only a\n\t * request: the terminal record is written strictly after the executor\n\t * drains, so `cancel -> drain -> terminal` is the observable order.\n\t */\n\tprivate async executeOperation(\n\t\toperationId: string,\n\t\tpayload: DurableOperationPayloadV1,\n\t\tinput: JsonValue | undefined,\n\t): Promise<PiSessionOperationAck> {\n\t\tconst executor = this.executors.get(payload.operationType);\n\t\tif (!executor) {\n\t\t\treturn this.completeOperation(operationId, {\n\t\t\t\t...payload,\n\t\t\t\tstatus: \"rejected\",\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"not_implemented\",\n\t\t\t\t\tmessage: `No executor is registered for operation type ${payload.operationType}`,\n\t\t\t\t},\n\t\t\t\tterminalAt: this.now(),\n\t\t\t});\n\t\t}\n\t\tconst controller = new AbortController();\n\t\tthis.inFlight.set(operationId, controller);\n\t\tlet terminal: DurableOperationPayloadV1;\n\t\ttry {\n\t\t\tconst result = await executor.execute({\n\t\t\t\tsessionId: payload.sessionId,\n\t\t\t\toperationId,\n\t\t\t\t...(input === undefined ? {} : { input }),\n\t\t\t\tsignal: controller.signal,\n\t\t\t});\n\t\t\tterminal = { ...payload, status: \"completed\", result, terminalAt: this.now() };\n\t\t} catch (error) {\n\t\t\tterminal = {\n\t\t\t\t...payload,\n\t\t\t\tstatus: \"rejected\",\n\t\t\t\terror: this.toTerminalError(operationId, error),\n\t\t\t\tterminalAt: this.now(),\n\t\t\t};\n\t\t} finally {\n\t\t\tthis.inFlight.delete(operationId);\n\t\t\tthis.cancelRequested.delete(operationId);\n\t\t}\n\t\treturn this.completeOperation(operationId, terminal);\n\t}\n\n\tprivate toTerminalError(\n\t\toperationId: string,\n\t\terror: unknown,\n\t): {\n\t\treadonly code: ProtocolErrorCode;\n\t\treadonly message: string;\n\t} {\n\t\tif (this.cancelRequested.has(operationId)) {\n\t\t\treturn { code: \"operation_cancelled\", message: \"Operation was cancelled before completion\" };\n\t\t}\n\t\tif (error instanceof PiServerError) return { code: error.code, message: error.message };\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\treturn { code: \"invalid_request\", message };\n\t}\n\n\tprivate completeOperation(operationId: string, terminal: DurableOperationPayloadV1): PiSessionOperationAck {\n\t\tthis.persistOperation(operationId, terminal.sessionId, terminal);\n\t\tthis.recordOperation(operationId, terminal);\n\t\treturn this.ackOf({ operationId, payload: terminal });\n\t}\n\n\tprivate cancelOperation(input: { readonly sessionId: string; readonly operationId: string }): PiSessionOperationAck {\n\t\tthis.requireDurable();\n\t\tconst recorded = this.recordedOperations.get(input.operationId);\n\t\t// Session ownership is part of the lookup: a foreign session gets the\n\t\t// same not-found answer as a missing id, without leaking ownership.\n\t\tif (!recorded || recorded.payload.sessionId !== input.sessionId) {\n\t\t\tthrow new PiServerError(\"operation_not_found\", `Unknown operation: ${input.operationId}`);\n\t\t}\n\t\tif (recorded.payload.status !== \"pending\") return this.ackOf(recorded);\n\t\t// Record the cancel request and signal the executor. The terminal state\n\t\t// is only written once the executor drained (or nothing is draining).\n\t\tthis.cancelRequested.add(input.operationId);\n\t\tthis.inFlight.get(input.operationId)?.abort();\n\t\treturn { operationId: input.operationId, status: \"accepted\" };\n\t}\n\n\tprivate resultOperation(input: { readonly sessionId: string; readonly operationId: string }): PiSessionOperationAck {\n\t\tthis.requireDurable();\n\t\tconst recorded = this.recordedOperations.get(input.operationId);\n\t\t// Session ownership is part of the lookup: a foreign session gets the\n\t\t// same not-found answer as a missing id, without leaking ownership.\n\t\tif (!recorded || recorded.payload.sessionId !== input.sessionId) {\n\t\t\tthrow new PiServerError(\"operation_not_found\", `Unknown operation: ${input.operationId}`);\n\t\t}\n\t\treturn this.ackOf(recorded);\n\t}\n\n\tprivate ackOf(recorded: RecordedOperation): PiSessionOperationAck {\n\t\tconst { operationId, payload } = recorded;\n\t\tif (payload.status === \"completed\") {\n\t\t\treturn {\n\t\t\t\toperationId,\n\t\t\t\tstatus: \"completed\",\n\t\t\t\t...(payload.result === undefined ? {} : { result: payload.result }),\n\t\t\t};\n\t\t}\n\t\tif (payload.status === \"rejected\") {\n\t\t\treturn {\n\t\t\t\toperationId,\n\t\t\t\tstatus: \"rejected\",\n\t\t\t\t...(payload.error === undefined\n\t\t\t\t\t? {}\n\t\t\t\t\t: { error: { code: payload.error.code, message: payload.error.message } satisfies ProtocolError }),\n\t\t\t};\n\t\t}\n\t\treturn { operationId, status: \"accepted\" };\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Artifact adapter (output-artifacts face + durable persistence)\n\t// -------------------------------------------------------------------------\n\n\t/**\n\t * Mirrors one runtime output artifact into the durable store so remote\n\t * `artifact_stat`/`artifact_read` can serve it, including after a restart\n\t * via log replay. The runtime in-memory store (capability-runtime) stays\n\t * the live write path; this adds durable persistence, not a second store.\n\t */\n\tpersistOutputArtifact(sessionId: string, ref: OutputArtifactRef, bytes: Uint8Array): ArtifactRef {\n\t\tthis.requireDurable();\n\t\tif (ref.sessionId !== sessionId) {\n\t\t\tthrow new PiServerError(\"invalid_request\", \"Artifact sessionId must match the owning session\");\n\t\t}\n\t\tif (ref.artifactId === \"\") throw new PiServerError(\"invalid_request\", \"Artifact id must not be empty\");\n\t\tif (ref.sizeBytes !== undefined && ref.sizeBytes !== bytes.byteLength) {\n\t\t\tthrow new PiServerError(\"invalid_request\", \"Artifact sizeBytes must match the payload length\");\n\t\t}\n\t\ttry {\n\t\t\tthis.durable?.persistArtifact({\n\t\t\t\twriterId: this.writerId ?? \"\",\n\t\t\t\tartifactId: ref.artifactId,\n\t\t\t\tsessionId,\n\t\t\t\tdata: { ref, bytes: Buffer.from(bytes).toString(\"base64\") } satisfies DurableArtifactPayloadV1,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tif (error instanceof DurableServiceErrorV1) {\n\t\t\t\tthrow new PiServerError(\"durable_unavailable\", error.message);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t\treturn toProtocolArtifactRef(ref);\n\t}\n\n\tprivate statArtifact(sessionId: string, artifactId: string): ArtifactRef {\n\t\treturn toProtocolArtifactRef(this.loadArtifactPayload(sessionId, artifactId).ref);\n\t}\n\n\tprivate readArtifact(\n\t\tsessionId: string,\n\t\tartifactId: string,\n\t\toptions: { readonly offset: number; readonly maxBytes: number },\n\t): {\n\t\treadonly artifact: ArtifactRef;\n\t\treadonly data: Uint8Array;\n\t\treadonly nextOffset?: number;\n\t\treadonly eof: boolean;\n\t} {\n\t\tconst payload = this.loadArtifactPayload(sessionId, artifactId);\n\t\tconst bytes = Buffer.from(payload.bytes, \"base64\");\n\t\tconst end = Math.min(options.offset + options.maxBytes, bytes.byteLength);\n\t\tconst data = new Uint8Array(bytes.subarray(options.offset, end));\n\t\tconst eof = end >= bytes.byteLength;\n\t\treturn {\n\t\t\tartifact: toProtocolArtifactRef(payload.ref),\n\t\t\tdata,\n\t\t\t...(eof ? {} : { nextOffset: end }),\n\t\t\teof,\n\t\t};\n\t}\n\n\t/**\n\t * Loads one artifact payload through the durable service.\n\t */\n\tprivate loadArtifactPayload(sessionId: string, artifactId: string): DurableArtifactPayloadV1 {\n\t\tconst durable = this.requireDurable();\n\t\tconst record = durable.loadArtifact({ artifactId });\n\t\tif (!record || record.sessionId !== sessionId) {\n\t\t\tthrow new PiServerError(\"not_found\", `Unknown artifact: ${artifactId}`);\n\t\t}\n\t\tconst payload = parseArtifactPayload(record.data);\n\t\tif (!payload || payload.ref.sessionId !== sessionId || payload.ref.artifactId !== artifactId) {\n\t\t\tthrow new PiServerError(\"durable_unavailable\", \"Stored artifact record is malformed\");\n\t\t}\n\t\treturn payload;\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Mutation-response store (idempotent replay of keyed mutation commands)\n\t// -------------------------------------------------------------------------\n\n\t/**\n\t * Deterministic artifact id for one mutation-command response, following the\n\t * `agent-task:<taskId>` precedent for derived artifact ids.\n\t */\n\tprivate mutationResponseArtifactId(sessionId: string, operationId: string): string {\n\t\treturn `mutation-response:${sessionId}:${operationId}`;\n\t}\n\n\t/**\n\t * Loads a recorded response through the durable service. The durable log\n\t * was already replayed into memory at construction, so this read never\n\t * touches the disk. A missing or malformed record yields undefined so the\n\t * caller re-executes; unexpected durable failures propagate and are\n\t * reported (and treated as a miss) by the server's idempotency layer.\n\t */\n\tprivate async loadMutationResponse(sessionId: string, operationId: string): Promise<SessionSnapshot | undefined> {\n\t\tconst durable = this.requireDurable();\n\t\tconst record = durable.loadArtifact({\n\t\t\tartifactId: this.mutationResponseArtifactId(sessionId, operationId),\n\t\t});\n\t\tif (record === undefined || record.sessionId !== sessionId) return undefined;\n\t\tconst snapshot = parseMutationResponsePayload(record.data, sessionId);\n\t\treturn snapshot === undefined ? undefined : structuredClone(snapshot);\n\t}\n\n\t/**\n\t * Persists one successful response snapshot as a durable artifact record so\n\t * retries after a restart replay it. The payload is the `SessionSnapshot`\n\t * itself: every field is JSON-safe, so the JSONL durable log round-trips it\n\t * without an adapter-specific envelope.\n\t */\n\tprivate async recordMutationResponse(\n\t\tsessionId: string,\n\t\toperationId: string,\n\t\tsnapshot: SessionSnapshot,\n\t): Promise<void> {\n\t\tconst durable = this.requireDurable();\n\t\ttry {\n\t\t\tdurable.persistArtifact({\n\t\t\t\twriterId: this.writerId ?? \"\",\n\t\t\t\tartifactId: this.mutationResponseArtifactId(sessionId, operationId),\n\t\t\t\tsessionId,\n\t\t\t\tdata: snapshot,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tif (error instanceof DurableServiceErrorV1) {\n\t\t\t\tthrow new PiServerError(\"durable_unavailable\", error.message);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tasync listSessions(): Promise<SessionMetadata[]> {\n\t\tconst descriptors = await this.source.list();\n\t\treturn descriptors.map((descriptor) => ({\n\t\t\tid: descriptor.id,\n\t\t\tcreatedAt: descriptor.createdAt,\n\t\t\tupdatedAt: descriptor.updatedAt,\n\t\t\t...(descriptor.name === undefined ? {} : { sessionName: descriptor.name }),\n\t\t\t// Legacy sessions record no cwd; the protocol marks it optional.\n\t\t\t...(descriptor.cwd === undefined ? {} : { cwd: descriptor.cwd }),\n\t\t}));\n\t}\n\n\tasync listModels(): Promise<ModelMetadata[]> {\n\t\treturn [...(await this.models.list())];\n\t}\n\n\tasync createSession(options: CreateSessionOptions): Promise<PiSessionRuntime> {\n\t\tthis.assertNotAcquired(options.id);\n\t\tif (options.cwd === undefined || options.cwd.trim() === \"\") {\n\t\t\tthrow new PiServerError(\"invalid_request\", \"Creating a coding-agent session requires a cwd\");\n\t\t}\n\t\t// The protocol carries a model ref; resolve it to a Model instance the\n\t\t// same way runtime `set_model` does. An unknown ref fails the create\n\t\t// instead of being silently dropped.\n\t\tlet model: Model<Api> | undefined;\n\t\ttry {\n\t\t\tmodel = options.model === undefined ? undefined : await this.models.resolve(options.model);\n\t\t} catch (error) {\n\t\t\tthrow toError(error);\n\t\t}\n\t\tconst timestamp = Math.max(0, Math.trunc(this.now()));\n\t\tconst descriptor: CodingAgentSessionDescriptor = {\n\t\t\tid: options.id,\n\t\t\t...(options.name === undefined ? {} : { name: options.name }),\n\t\t\tcwd: options.cwd,\n\t\t\t...(model === undefined ? {} : { model }),\n\t\t\t...(options.thinkingLevel === undefined ? {} : { thinkingLevel: options.thinkingLevel }),\n\t\t\t...(options.profileId === undefined ? {} : { profileId: options.profileId }),\n\t\t\t...(options.profileVersion === undefined ? {} : { profileVersion: options.profileVersion }),\n\t\t\tcreatedAt: timestamp,\n\t\t\tupdatedAt: timestamp,\n\t\t};\n\t\tlet session: AgentSession;\n\t\ttry {\n\t\t\tsession = await this.openExclusively(descriptor.id, () => this.source.create(descriptor));\n\t\t} catch (error) {\n\t\t\tthrow toError(error);\n\t\t}\n\t\t// Lazy-model contract: createAgentSession does not fail when no model is\n\t\t// resolvable (no auth, empty catalog); it returns a session whose model is\n\t\t// undefined. A model-less live session cannot serve prompts and breaks\n\t\t// every client's list/server_snapshot, so the create fails explicitly.\n\t\t// Disposing rolls the session back before any registration; the\n\t\t// SessionManager has not flushed the JSONL yet (persistence defers until\n\t\t// the first user message), so nothing stays on disk, and a flushed\n\t\t// residue would still be harmless: reopening it runs into the same\n\t\t// preflight in openSession.\n\t\tif (session.model === undefined) {\n\t\t\t// Keep the structured rejection even if rollback fails; the cleanup\n\t\t\t// failure is appended to the message instead of masking it.\n\t\t\tlet cleanupNote = \"\";\n\t\t\ttry {\n\t\t\t\tawait session.dispose();\n\t\t\t} catch (disposeError) {\n\t\t\t\tcleanupNote = ` (session cleanup also failed: ${disposeError instanceof Error ? disposeError.message : String(disposeError)})`;\n\t\t\t}\n\t\t\tthrow new PiServerError(\n\t\t\t\t\"invalid_request\",\n\t\t\t\t`Creating a coding-agent session requires a resolvable model; none is available for this server's agent directory${cleanupNote}`,\n\t\t\t);\n\t\t}\n\t\treturn this.acquireRuntime(descriptor, session);\n\t}\n\n\tasync openSession(sessionId: string): Promise<PiSessionRuntime> {\n\t\tthis.assertNotAcquired(sessionId);\n\t\tconst descriptors = await this.source.list();\n\t\tconst descriptor = descriptors.find((entry) => entry.id === sessionId);\n\t\tif (!descriptor) throw new PiServerError(\"not_found\", `Unknown session: ${sessionId}`);\n\t\tlet session: AgentSession;\n\t\ttry {\n\t\t\tsession = await this.openExclusively(descriptor.id, () => this.source.open(descriptor));\n\t\t} catch (error) {\n\t\t\tthrow toError(error);\n\t\t}\n\t\t// Same preflight as create: a history session whose stored model no longer\n\t\t// resolves (model removed, auth missing) must fail the open instead of\n\t\t// going live model-less and breaking every client's list/server_snapshot.\n\t\tif (session.model === undefined) {\n\t\t\tlet cleanupNote = \"\";\n\t\t\ttry {\n\t\t\t\tawait session.dispose();\n\t\t\t} catch (disposeError) {\n\t\t\t\tcleanupNote = ` (session cleanup also failed: ${disposeError instanceof Error ? disposeError.message : String(disposeError)})`;\n\t\t\t}\n\t\t\tthrow new PiServerError(\n\t\t\t\t\"invalid_request\",\n\t\t\t\t`Reopening session ${sessionId} requires a resolvable model; none is available for this server's agent directory${cleanupNote}`,\n\t\t\t);\n\t\t}\n\t\treturn this.acquireRuntime(descriptor, session);\n\t}\n\n\t/** Serializes the acquired-check with the async source call to keep opens exclusive. */\n\tprivate async openExclusively(id: string, open: () => Promise<AgentSession>): Promise<AgentSession> {\n\t\tif (this.opening.has(id) || this.acquired.has(id)) {\n\t\t\tthrow new PiServerError(\"session_locked\", `Session is already open: ${id}`);\n\t\t}\n\t\tthis.opening.add(id);\n\t\ttry {\n\t\t\treturn await open();\n\t\t} finally {\n\t\t\tthis.opening.delete(id);\n\t\t}\n\t}\n\n\tprivate acquireRuntime(descriptor: CodingAgentSessionDescriptor, session: AgentSession): CodingAgentSessionRuntime {\n\t\t// Auto-mirror: with a durable service configured, every artifact committed\n\t\t// by the session's capability runtime is mirrored here, so the wire face\n\t\t// serves it without an explicit persist call. Without a durable service\n\t\t// nothing is registered (no behavior change). Mirror failures surface as\n\t\t// runtime commit-observer log records; they never fail the artifact put.\n\t\tconst unsubscribeArtifactMirror =\n\t\t\tthis.durable === undefined\n\t\t\t\t? undefined\n\t\t\t\t: session.onOutputArtifactCommit(({ ref, bytes }) => {\n\t\t\t\t\t\tthis.persistOutputArtifact(descriptor.id, ref, bytes);\n\t\t\t\t\t});\n\t\tconst runtime = new CodingAgentSessionRuntime(session, descriptor, {\n\t\t\tmodels: this.models,\n\t\t\tonDisposed: () => {\n\t\t\t\tunsubscribeArtifactMirror?.();\n\t\t\t\tthis.acquired.delete(descriptor.id);\n\t\t\t},\n\t\t});\n\t\tthis.acquired.set(descriptor.id, runtime);\n\t\treturn runtime;\n\t}\n\n\tprivate assertNotAcquired(id: string): void {\n\t\tif (this.acquired.has(id) || this.opening.has(id)) {\n\t\t\tthrow new PiServerError(\"session_locked\", `Session is already open: ${id}`);\n\t\t}\n\t}\n}\n\n/** Composes the coding-agent service with transport, journal, and auth hooks into one server. */\nexport function assembleCodingAgentServer(options: CodingAgentServerAssemblyOptions): {\n\treadonly server: PiServer;\n\treadonly service: CodingAgentServerService;\n} {\n\tconst service = new CodingAgentServerService({\n\t\tsessions: options.sessions,\n\t\tmodels: options.models,\n\t\tnow: options.now,\n\t\t...(options.durable === undefined ? {} : { durable: options.durable }),\n\t\t...(options.writerId === undefined ? {} : { writerId: options.writerId }),\n\t\t...(options.operationExecutors === undefined ? {} : { operationExecutors: options.operationExecutors }),\n\t});\n\tconst server = new PiServer(service, {\n\t\tlisteners: options.listeners,\n\t\t...(options.journal === undefined ? {} : { journal: options.journal }),\n\t\t...(options.authenticate === undefined ? {} : { authenticate: options.authenticate }),\n\t\t...(options.authorize === undefined ? {} : { authorize: options.authorize }),\n\t\t...(options.serverId === undefined ? {} : { serverId: options.serverId }),\n\t\t...(options.maxFrameLength === undefined ? {} : { maxFrameLength: options.maxFrameLength }),\n\t\t...(options.handshakeTimeoutMs === undefined ? {} : { handshakeTimeoutMs: options.handshakeTimeoutMs }),\n\t\t...(options.onError === undefined ? {} : { onError: options.onError }),\n\t});\n\treturn { server, service };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"server-assembly.d.ts","sourceRoot":"","sources":["../../src/remote/server-assembly.ts"],"names":[],"mappings":"AA0CA,OAAO,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,KAAK,EACX,WAAW,EACX,SAAS,EACT,aAAa,EACb,QAAQ,EAGR,eAAe,EACf,YAAY,EACZ,eAAe,EACf,aAAa,EACb,cAAc,EAEd,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACN,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,QAAQ,EAER,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAE3B,KAAK,uBAAuB,EAC5B,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC1B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,YAAY,EAAqB,MAAM,0BAA0B,CAAC;AAChF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AAC1E,OAAO,EAAyB,KAAK,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAE7F,mEAAmE;AACnE,MAAM,WAAW,4BAA4B;IAC5C,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,6EAA6E;IAC7E,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,2FAA2F;IAC3F,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAC5B,8CAA8C;IAC9C,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,CAAC;IACvC;;;;;;OAMG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC3B;AAED;;;GAGG;AACH,MAAM,WAAW,8BAA8B;IAC9C,IAAI,IAAI,OAAO,CAAC,SAAS,4BAA4B,EAAE,CAAC,CAAC;IACzD,MAAM,CAAC,UAAU,EAAE,4BAA4B,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IACxE,IAAI,CAAC,UAAU,EAAE,4BAA4B,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;CACtE;AAED,iFAAiF;AACjF,MAAM,WAAW,uBAAuB;IACvC,IAAI,IAAI,OAAO,CAAC,SAAS,aAAa,EAAE,CAAC,CAAC;IAC1C,OAAO,CAAC,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;CAC5C;AAED,MAAM,WAAW,gCAAgC;IAChD,QAAQ,CAAC,QAAQ,EAAE,8BAA8B,CAAC;IAClD,QAAQ,CAAC,MAAM,EAAE,uBAAuB,CAAC;IACzC,QAAQ,CAAC,SAAS,EAAE,eAAe,CAAC,WAAW,CAAC,CAAC;IACjD,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,CAAC,SAAS,CAAC,CAAC;IAC9C,QAAQ,CAAC,YAAY,CAAC,EAAE,eAAe,CAAC,cAAc,CAAC,CAAC;IACxD,QAAQ,CAAC,SAAS,CAAC,EAAE,eAAe,CAAC,WAAW,CAAC,CAAC;IAClD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IAC1C;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,gBAAgB,CAAC;IACpC;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,oFAAoF;IACpF,QAAQ,CAAC,kBAAkB,CAAC,EAAE,SAAS,4BAA4B,EAAE,CAAC;IACtE,mEAAmE;IACnE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC5B;AAwJD,wFAAwF;AACxF,MAAM,WAAW,4BAA4B;IAC5C,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,OAAO,CAAC,KAAK,EAAE;QACd,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;QAC7B,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,CAAC;QAC3B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;KAC7B,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;CACvB;AA+ED;;;;;;;;;;;GAWG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,YAAY,GAAG,cAAc,EAAE,CAyKhF;AAQD,2EAA2E;AAC3E,qBAAa,yBAA0B,YAAW,gBAAgB;IACjE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAe;IACvC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA+B;IAC1D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0B;IACjD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqD;IAC/E,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAa;IACzC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAa;IACzC,OAAO,CAAC,QAAQ,CAAS;IAEzB,YACC,OAAO,EAAE,YAAY,EACrB,UAAU,EAAE,4BAA4B,EACxC,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,uBAAuB,CAAC;QAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,IAAI,CAAA;KAAE,EAYzF;IAED,QAAQ,IAAI,eAAe,CAiC1B;IAED,QAAQ,IAAI,YAAY,CAEvB;IAEK,MAAM,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ5D;IAEK,KAAK,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAU3D;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAO3B;IAEK,QAAQ,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAU7C;IAEK,WAAW,CAAC,aAAa,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ7D;IAED,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,qBAAqB,KAAK,IAAI,GAAG,MAAM,IAAI,CAGtE;IAEK,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAS7B;IAED,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,cAAc;IActB,OAAO,CAAC,eAAe;CASvB;AAED,sEAAsE;AACtE,qBAAa,wBAAyB,YAAW,eAAe;IAC/D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiC;IACxD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0B;IACjD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAgD;IACzE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAC7C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA+B;IACvD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAmD;IAC7E,8EAA8E;IAC9E,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAsC;IACrE,6EAA6E;IAC7E,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqC;IACnE,oFAAoF;IACpF,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAoC;IAC1E,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqB;IAC9C,OAAO,CAAC,UAAU,CAAS;IAC3B,gEAAgE;IAChE,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAwC;IACzE,qEAAqE;IACrE,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAwC;IAC3E,yDAAyD;IACzD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAsC;IAC/D,sEAAsE;IACtE,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAqB;IAErD,YAAY,OAAO,EAAE;QACpB,QAAQ,CAAC,QAAQ,EAAE,8BAA8B,CAAC;QAClD,QAAQ,CAAC,MAAM,EAAE,uBAAuB,CAAC;QACzC,QAAQ,CAAC,OAAO,CAAC,EAAE,gBAAgB,CAAC;QACpC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,kBAAkB,CAAC,EAAE,SAAS,4BAA4B,EAAE,CAAC;QACtE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;KAC5B,EAwBA;IAED,sEAAsE;IACtE,IAAI,SAAS,IAAI,sBAAsB,GAAG,SAAS,CAElD;IAED,0EAA0E;IAC1E,IAAI,UAAU,IAAI,uBAAuB,GAAG,SAAS,CAEpD;IAED,4FAA4F;IAC5F,IAAI,iBAAiB,IAAI,qBAAqB,GAAG,SAAS,CAEzD;IAMD;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;IAWxB;;;;;OAKG;IACH,kBAAkB,IAAI,IAAI,CAUzB;IAED,mGAAmG;IACnG,OAAO,CAAC,gBAAgB;IAgBxB,OAAO,CAAC,cAAc;IAOtB;;;;;;OAMG;IACH,OAAO,CAAC,0BAA0B;IAoBlC,OAAO,CAAC,eAAe;YAWT,eAAe;YA0Df,gBAAgB;IA0C9B,OAAO,CAAC,eAAe;IAevB,OAAO,CAAC,iBAAiB;IAMzB,OAAO,CAAC,eAAe;IAgBvB,OAAO,CAAC,eAAe;IAWvB,OAAO,CAAC,KAAK;IAyBb;;;;;OAKG;IACH,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,iBAAiB,EAAE,KAAK,EAAE,UAAU,GAAG,WAAW,CA4B/F;IAED,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,YAAY;IAuBpB;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAiB3B;;;OAGG;IACH,OAAO,CAAC,0BAA0B;YAWpB,oBAAoB;YAgBpB,sBAAsB;IAqB9B,YAAY,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC,CAU/C;IAEK,UAAU,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC,CAE3C;IAEK,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAwD5E;IAEK,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA2B9D;YAGa,eAAe;IAY7B,OAAO,CAAC,cAAc;IAuBtB,OAAO,CAAC,iBAAiB;CAKzB;AAED,iGAAiG;AACjG,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,gCAAgC,GAAG;IACrF,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,wBAAwB,CAAC;CAC3C,CAoBA","sourcesContent":["/**\n * Minimal coding-agent runtime -> remote server assembly (1B.7h).\n *\n * Adapts first-party `AgentSession` instances to the public `@agent-forge/server`\n * session boundary so a preinstalled session adapter can run a deterministic\n * server end to end. The adapter only consumes the public `AgentSession` surface\n * and never reads private session objects.\n *\n * Snapshot transcripts use positional ids (`t-<index>`) that are stable within\n * one revision but not across history rewrites such as compaction; snapshots\n * stay authoritative and durable replay is journal-based, so replay does not\n * depend on these ids. Streaming is reported as snapshot-change notifications;\n * amortized delta progress belongs to a later slice.\n *\n * Durable operations and artifacts (E4-c): when a `DurableServiceV1` is\n * supplied, the service exposes the server `operations`/`artifacts` surfaces as\n * a thin adapter over the durable service — it never creates a second artifact\n * store. Artifact records project the protocol `ArtifactRef` 1:1 onto the SDK\n * `OutputArtifactRef` and carry base64 bytes so remote `artifact_stat` /\n * `artifact_read` survive restarts via the durable log replay; the runtime\n * in-memory artifact store remains the live write path (built in\n * capability-runtime). Keyed mutation commands (`operationId` on prompt/steer/\n * abort/set_model/set_thinking) persist their successful response snapshots as\n * deterministic `mutation-response:<sessionId>:<operationId>` artifact records\n * so a retry after a restart replays the recorded response; the server's\n * in-process LRU stays the live dedup path. Without a durable service the\n * surfaces stay undefined and the server answers `durable_unavailable` /\n * `not_implemented` explicitly.\n *\n * Deployment boundary: the durable writer lease is process-local memory. This\n * assembly is single-instance per durable store — the durable backend's file\n * writes are cross-process mutually exclusive (its `.lock`), while the writer\n * lease single-instance semantics remain process-local: who should claim the\n * lease still needs deployment-level coordination. On\n * startup the service claims the writer lease and, when the restored state\n * marks a dead writer (snapshot import), recovers it; a live foreign writer\n * keeps the service unavailable instead of fighting over the lease. Operations\n * left pending by a crashed process are swept to a rejected terminal so no\n * submitted operation can hang as `accepted` forever.\n */\nimport { Buffer } from \"node:buffer\";\nimport { randomUUID } from \"node:crypto\";\nimport type { Api, Model } from \"@agent-forge/ai/compat\";\nimport type {\n\tArtifactRef,\n\tJsonValue,\n\tModelMetadata,\n\tModelRef,\n\tProtocolError,\n\tProtocolErrorCode,\n\tSessionMetadata,\n\tSessionPhase,\n\tSessionSnapshot,\n\tThinkingLevel,\n\tTranscriptItem,\n\tUsage,\n} from \"@agent-forge/protocol\";\nimport {\n\ttype CreateSessionOptions,\n\ttype MutationResponseStore,\n\tPiServer,\n\tPiServerError,\n\ttype PiServerOptions,\n\ttype PiServerService,\n\ttype PiSessionArtifactStore,\n\ttype PiSessionOperationAck,\n\ttype PiSessionOperationStore,\n\ttype PiSessionRuntime,\n\ttype PiSessionRuntimeEvent,\n} from \"@agent-forge/server\";\nimport type { AgentSession, AgentSessionEvent } from \"../core/agent-session.ts\";\nimport type { OutputArtifactRef } from \"../core/extensions/public-api.ts\";\nimport { DurableServiceErrorV1, type DurableServiceV1 } from \"../durable/durable-service.ts\";\n\n/** Server-visible facts about one durable coding-agent session. */\nexport interface CodingAgentSessionDescriptor {\n\treadonly id: string;\n\treadonly name?: string;\n\t/** Working directory; omitted when unknown (legacy sessions record none). */\n\treadonly cwd?: string;\n\t/** Model instance to attach at creation; resolved from the protocol ref by the service. */\n\treadonly model?: Model<Api>;\n\t/** Thinking level for the created session. */\n\treadonly thinkingLevel?: ThinkingLevel;\n\t/**\n\t * Optional creation-time profile lock (protocol create `profileId`/\n\t * `profileVersion`) passed by the remote create command: stored unchanged\n\t * and echoed in snapshots, never interpreted by the runtime. Persisted in\n\t * the session header and echoed verbatim in snapshots across restarts;\n\t * absent when the create command did not carry it.\n\t */\n\treadonly profileId?: string;\n\treadonly profileVersion?: string;\n\treadonly createdAt: number;\n\treadonly updatedAt: number;\n}\n\n/**\n * Persistence boundary owned by the host. `create`/`open` hand over exclusive\n * ownership of one `AgentSession` to the server runtime until `dispose()`.\n */\nexport interface CodingAgentServerSessionSource {\n\tlist(): Promise<readonly CodingAgentSessionDescriptor[]>;\n\tcreate(descriptor: CodingAgentSessionDescriptor): Promise<AgentSession>;\n\topen(descriptor: CodingAgentSessionDescriptor): Promise<AgentSession>;\n}\n\n/** Model catalog projection used for `listModels` and `set_model` resolution. */\nexport interface CodingAgentServerModels {\n\tlist(): Promise<readonly ModelMetadata[]>;\n\tresolve(ref: ModelRef): Promise<Model<Api>>;\n}\n\nexport interface CodingAgentServerAssemblyOptions {\n\treadonly sessions: CodingAgentServerSessionSource;\n\treadonly models: CodingAgentServerModels;\n\treadonly listeners: PiServerOptions[\"listeners\"];\n\treadonly journal?: PiServerOptions[\"journal\"];\n\treadonly authenticate?: PiServerOptions[\"authenticate\"];\n\treadonly authorize?: PiServerOptions[\"authorize\"];\n\treadonly serverId?: string;\n\treadonly maxFrameLength?: number;\n\treadonly handshakeTimeoutMs?: number;\n\treadonly onError?: (error: Error) => void;\n\t/**\n\t * Durable service backing operation and artifact records. Single-instance\n\t * per store: the writer lease is process-local memory. Omission keeps the\n\t * server `operations` surface undefined (clients see `durable_unavailable`)\n\t * and the `artifacts` surface unconfigured (clients see `not_implemented`).\n\t */\n\treadonly durable?: DurableServiceV1;\n\t/**\n\t * Writer id claimed on the durable service. Hosts that also pass the same\n\t * store into their session source (`agentTaskResults.writerId`) supply it\n\t * here so one writer serves the whole process; omission generates a fresh\n\t * id (claim/recover behavior is unchanged).\n\t */\n\treadonly writerId?: string;\n\t/** Executors for submitted operation types; unknown types terminate as rejected. */\n\treadonly operationExecutors?: readonly CodingAgentOperationExecutor[];\n\t/** Clock for descriptor timestamps; defaults to the host clock. */\n\treadonly now?: () => number;\n}\n\nfunction requireIdle(session: AgentSession, action: string): void {\n\tif (!session.isIdle) {\n\t\tthrow new PiServerError(\"busy\", `Cannot ${action} while the session is busy`);\n\t}\n}\n\nfunction toError(error: unknown): PiServerError {\n\tif (error instanceof PiServerError) return error;\n\tconst message = error instanceof Error ? error.message : String(error);\n\tif (/reload in progress/i.test(message)) return new PiServerError(\"busy\", message);\n\tif (/compaction is in progress/i.test(message)) return new PiServerError(\"busy\", message);\n\tif (/already processing/i.test(message)) return new PiServerError(\"busy\", message);\n\t// Operational session failures (missing credentials, no active model,\n\t// extension rejections) must reach the remote client with their original\n\t// message instead of being folded into an unspecific internal error.\n\treturn new PiServerError(\"invalid_request\", message);\n}\n\nfunction jsonSafe(value: unknown): boolean {\n\tif (value === null || typeof value === \"string\" || typeof value === \"boolean\") return true;\n\tif (typeof value === \"number\") return Number.isFinite(value);\n\tif (Array.isArray(value)) return value.every(jsonSafe);\n\tif (typeof value !== \"object\" || Object.getPrototypeOf(value) !== Object.prototype) return false;\n\treturn Object.values(value).every(jsonSafe);\n}\n\n/** Payload stored in `DurableOperationRecordV1.data`. Adapter-owned shape. */\ninterface DurableOperationPayloadV1 {\n\treadonly status: \"pending\" | \"completed\" | \"rejected\";\n\treadonly sessionId: string;\n\treadonly operationType: string;\n\treadonly idempotencyKey?: string;\n\treadonly result?: JsonValue;\n\treadonly error?: { readonly code: ProtocolErrorCode; readonly message: string };\n\treadonly submittedAt: number;\n\treadonly terminalAt?: number;\n}\n\n/** Payload stored in `DurableArtifactRecordV1.data`. Adapter-owned shape. */\ninterface DurableArtifactPayloadV1 {\n\treadonly ref: OutputArtifactRef;\n\t/** Base64 artifact bytes so records survive the JSONL log replay. */\n\treadonly bytes: string;\n}\n\n/** An operation id paired with its latest recorded payload snapshot. */\ninterface RecordedOperation {\n\treadonly operationId: string;\n\treadonly payload: DurableOperationPayloadV1;\n}\n\n/** Parses an adapter-owned operation payload; undefined for foreign or malformed records. */\nfunction parseOperationPayload(value: unknown): DurableOperationPayloadV1 | undefined {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) return undefined;\n\tconst record = value as Record<string, unknown>;\n\tif (record.status !== \"pending\" && record.status !== \"completed\" && record.status !== \"rejected\") return undefined;\n\tif (typeof record.sessionId !== \"string\" || record.sessionId === \"\") return undefined;\n\tif (typeof record.operationType !== \"string\") return undefined;\n\tif (typeof record.submittedAt !== \"number\" || !Number.isFinite(record.submittedAt)) return undefined;\n\tlet error: DurableOperationPayloadV1[\"error\"];\n\tif (record.error !== undefined) {\n\t\tif (typeof record.error !== \"object\" || record.error === null || Array.isArray(record.error)) return undefined;\n\t\tconst entry = record.error as Record<string, unknown>;\n\t\tif (typeof entry.code !== \"string\" || typeof entry.message !== \"string\") return undefined;\n\t\terror = { code: entry.code as ProtocolErrorCode, message: entry.message };\n\t}\n\treturn {\n\t\tstatus: record.status,\n\t\tsessionId: record.sessionId,\n\t\toperationType: record.operationType,\n\t\t...(typeof record.idempotencyKey === \"string\" ? { idempotencyKey: record.idempotencyKey } : {}),\n\t\t...(record.result === undefined ? {} : { result: record.result as JsonValue }),\n\t\t...(error === undefined ? {} : { error }),\n\t\tsubmittedAt: record.submittedAt,\n\t\t...(typeof record.terminalAt === \"number\" ? { terminalAt: record.terminalAt } : {}),\n\t};\n}\n\n/** Parses an adapter-owned artifact payload; undefined for foreign or malformed records. */\nfunction parseArtifactPayload(value: unknown): DurableArtifactPayloadV1 | undefined {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) return undefined;\n\tconst record = value as Record<string, unknown>;\n\tconst ref = record.ref;\n\tif (typeof ref !== \"object\" || ref === null || Array.isArray(ref)) return undefined;\n\tconst entry = ref as Record<string, unknown>;\n\tif (\n\t\ttypeof entry.sessionId !== \"string\" ||\n\t\ttypeof entry.operationId !== \"string\" ||\n\t\ttypeof entry.artifactId !== \"string\" ||\n\t\ttypeof entry.kind !== \"string\" ||\n\t\ttypeof entry.mediaType !== \"string\" ||\n\t\ttypeof entry.truncated !== \"boolean\" ||\n\t\ttypeof entry.createdAt !== \"number\" ||\n\t\ttypeof record.bytes !== \"string\"\n\t) {\n\t\treturn undefined;\n\t}\n\treturn {\n\t\tref: {\n\t\t\tsessionId: entry.sessionId,\n\t\t\toperationId: entry.operationId,\n\t\t\tartifactId: entry.artifactId,\n\t\t\tkind: entry.kind,\n\t\t\tmediaType: entry.mediaType,\n\t\t\t...(typeof entry.sizeBytes === \"number\" ? { sizeBytes: entry.sizeBytes } : {}),\n\t\t\ttruncated: entry.truncated,\n\t\t\tcreatedAt: entry.createdAt,\n\t\t},\n\t\tbytes: record.bytes,\n\t};\n}\n\n/**\n * Parses a stored mutation-response payload; undefined for foreign or\n * malformed records. The required-field assertion is deliberately minimal: the\n * full `SessionSnapshot` shape is already enforced by the wire codec when the\n * replayed snapshot is encoded into a response, so a damaged record only needs\n * to fail this check to fall back to a fresh execution.\n */\nfunction parseMutationResponsePayload(value: unknown, sessionId: string): SessionSnapshot | undefined {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) return undefined;\n\tconst record = value as Record<string, unknown>;\n\tif (\n\t\trecord.id !== sessionId ||\n\t\ttypeof record.cwd !== \"string\" ||\n\t\trecord.cwd === \"\" ||\n\t\ttypeof record.revision !== \"number\" ||\n\t\t!Number.isInteger(record.revision) ||\n\t\trecord.revision < 0 ||\n\t\t!Array.isArray(record.transcript)\n\t) {\n\t\treturn undefined;\n\t}\n\treturn value as SessionSnapshot;\n}\n\n/** Protocol `ArtifactRef` is a 1:1 projection of the SDK `OutputArtifactRef`. */\nfunction toProtocolArtifactRef(ref: OutputArtifactRef): ArtifactRef {\n\treturn {\n\t\tsessionId: ref.sessionId,\n\t\toperationId: ref.operationId,\n\t\tartifactId: ref.artifactId,\n\t\tkind: ref.kind,\n\t\tmediaType: ref.mediaType,\n\t\t...(ref.sizeBytes === undefined ? {} : { sizeBytes: ref.sizeBytes }),\n\t\ttruncated: ref.truncated,\n\t\tcreatedAt: ref.createdAt,\n\t};\n}\n\n/** Executes one submitted operation type; the abort signal fires on cancel requests. */\nexport interface CodingAgentOperationExecutor {\n\treadonly operationType: string;\n\texecute(input: {\n\t\treadonly sessionId: string;\n\t\treadonly operationId: string;\n\t\treadonly input?: JsonValue;\n\t\treadonly signal: AbortSignal;\n\t}): Promise<JsonValue>;\n}\n\nfunction projectUsage(usage: unknown): Usage | undefined {\n\tif (!usage || typeof usage !== \"object\" || Array.isArray(usage)) return undefined;\n\tconst record = usage as Record<string, unknown>;\n\tconst readCount = (key: string): number | undefined => {\n\t\tconst value = record[key];\n\t\treturn typeof value === \"number\" && Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : undefined;\n\t};\n\tconst input = readCount(\"input\");\n\tconst output = readCount(\"output\");\n\tconst cacheRead = readCount(\"cacheRead\");\n\tconst cacheWrite = readCount(\"cacheWrite\");\n\tconst totalTokens = readCount(\"totalTokens\");\n\tif (input === undefined || output === undefined || cacheRead === undefined || cacheWrite === undefined) {\n\t\treturn undefined;\n\t}\n\tif (totalTokens === undefined) return undefined;\n\tconst cost = record.cost;\n\tif (!cost || typeof cost !== \"object\" || Array.isArray(cost)) return undefined;\n\tconst costRecord = cost as Record<string, unknown>;\n\tconst readCost = (key: string): number | undefined => {\n\t\tconst value = costRecord[key];\n\t\treturn typeof value === \"number\" && Number.isFinite(value) ? value : undefined;\n\t};\n\tconst costInput = readCost(\"input\");\n\tconst costOutput = readCost(\"output\");\n\tconst costCacheRead = readCost(\"cacheRead\");\n\tconst costCacheWrite = readCost(\"cacheWrite\");\n\tconst costTotal = readCost(\"total\");\n\tif (\n\t\tcostInput === undefined ||\n\t\tcostOutput === undefined ||\n\t\tcostCacheRead === undefined ||\n\t\tcostCacheWrite === undefined ||\n\t\tcostTotal === undefined\n\t) {\n\t\treturn undefined;\n\t}\n\tconst reasoning = readCount(\"reasoning\");\n\treturn {\n\t\tinput,\n\t\toutput,\n\t\tcacheRead,\n\t\tcacheWrite,\n\t\t...(reasoning === undefined ? {} : { reasoning }),\n\t\ttotalTokens,\n\t\tcost: {\n\t\t\tinput: costInput,\n\t\t\toutput: costOutput,\n\t\t\tcacheRead: costCacheRead,\n\t\t\tcacheWrite: costCacheWrite,\n\t\t\ttotal: costTotal,\n\t\t},\n\t};\n}\n\ntype ImagePart = { type: \"image\"; data: string; mimeType: string };\ntype TextPart = { type: \"text\"; text: string };\n\nfunction projectUserContent(content: string | readonly unknown[]): Array<TextPart | ImagePart> {\n\tif (typeof content === \"string\") return [{ type: \"text\", text: content }];\n\tconst projected: Array<TextPart | ImagePart> = [];\n\tfor (const part of content) {\n\t\tif (!part || typeof part !== \"object\") continue;\n\t\tconst record = part as Record<string, unknown>;\n\t\tif (record.type === \"text\" && typeof record.text === \"string\") {\n\t\t\tprojected.push({ type: \"text\", text: record.text });\n\t\t} else if (record.type === \"image\" && typeof record.data === \"string\" && typeof record.mimeType === \"string\") {\n\t\t\tprojected.push({ type: \"image\", data: record.data, mimeType: record.mimeType });\n\t\t}\n\t}\n\treturn projected;\n}\n\nfunction projectToolResultContent(content: readonly unknown[]): Array<TextPart | ImagePart> {\n\treturn projectUserContent(content);\n}\n\n/**\n * Projects the current `AgentSession` messages onto the protocol transcript.\n * Tool inputs are recovered from the matching assistant `toolCall` content;\n * `null` marks an unrecoverable input instead of fabricating one. `!`-command\n * bash executions project as `bash` tool items. Session-level summary and\n * extension messages (`branchSummary`, `compactionSummary`, `custom`) project\n * as snapshot-only `note` items; they never enter streaming progress. Note\n * items carry the text/summary face only — custom-message `details` and image\n * content parts are not projected. Item ids\n * stay positional (`t-<index>`): they are stable within one revision but not\n * across history rewrites such as compaction.\n */\nexport function projectSessionTranscript(session: AgentSession): TranscriptItem[] {\n\tconst messages = session.messages;\n\tconst toolInputs = new Map<string, JsonValue>();\n\tfor (const message of messages) {\n\t\tif (message.role !== \"assistant\") continue;\n\t\tfor (const part of message.content) {\n\t\t\tif (part.type === \"toolCall\") toolInputs.set(part.id, part.arguments as JsonValue);\n\t\t}\n\t}\n\tconst items: TranscriptItem[] = [];\n\tfor (const message of messages) {\n\t\tconst id = `t-${items.length}`;\n\t\tconst timestamp = Math.max(0, Math.trunc(message.timestamp));\n\t\tif (message.role === \"user\") {\n\t\t\titems.push({\n\t\t\t\tid,\n\t\t\t\trole: \"user\",\n\t\t\t\tcontent: projectUserContent(message.content),\n\t\t\t\ttimestamp,\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\t\tif (message.role === \"toolResult\") {\n\t\t\tconst input = toolInputs.get(message.toolCallId);\n\t\t\tconst usage = projectUsage(message.usage);\n\t\t\tconst details = jsonSafe(message.details) ? (message.details as JsonValue) : undefined;\n\t\t\tconst base = {\n\t\t\t\tid,\n\t\t\t\trole: \"tool\" as const,\n\t\t\t\ttoolCallId: message.toolCallId,\n\t\t\t\ttoolName: message.toolName,\n\t\t\t\tinput: input !== undefined && jsonSafe(input) ? input : null,\n\t\t\t\tcontent: projectToolResultContent(message.content),\n\t\t\t\t...(details === undefined ? {} : { details }),\n\t\t\t\t...(usage === undefined ? {} : { usage }),\n\t\t\t\ttimestamp,\n\t\t\t};\n\t\t\tif (message.isError) {\n\t\t\t\tconst item: TranscriptItem = { ...base, status: \"error\", isError: true };\n\t\t\t\titems.push(item);\n\t\t\t} else {\n\t\t\t\tconst item: TranscriptItem = { ...base, status: \"complete\", isError: false };\n\t\t\t\titems.push(item);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tif (message.role === \"bashExecution\") {\n\t\t\tconst failed = message.cancelled || (message.exitCode !== undefined && message.exitCode !== 0);\n\t\t\tconst base = {\n\t\t\t\tid,\n\t\t\t\trole: \"tool\" as const,\n\t\t\t\ttoolCallId: `bash-${id}`,\n\t\t\t\ttoolName: \"bash\",\n\t\t\t\tinput: { command: message.command } satisfies JsonValue,\n\t\t\t\tcontent: [{ type: \"text\" as const, text: message.output }],\n\t\t\t\tdetails: {\n\t\t\t\t\texitCode: message.exitCode ?? null,\n\t\t\t\t\tcancelled: message.cancelled,\n\t\t\t\t\ttruncated: message.truncated,\n\t\t\t\t} satisfies JsonValue,\n\t\t\t\ttimestamp,\n\t\t\t};\n\t\t\tif (failed) {\n\t\t\t\tconst item: TranscriptItem = { ...base, status: \"error\", isError: true };\n\t\t\t\titems.push(item);\n\t\t\t} else {\n\t\t\t\tconst item: TranscriptItem = { ...base, status: \"complete\", isError: false };\n\t\t\t\titems.push(item);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tif (message.role === \"branchSummary\") {\n\t\t\titems.push({\n\t\t\t\tid,\n\t\t\t\trole: \"note\",\n\t\t\t\tkind: \"branchSummary\",\n\t\t\t\ttext: message.summary,\n\t\t\t\tfromId: message.fromId,\n\t\t\t\ttimestamp,\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\t\tif (message.role === \"compactionSummary\") {\n\t\t\titems.push({\n\t\t\t\tid,\n\t\t\t\trole: \"note\",\n\t\t\t\tkind: \"compactionSummary\",\n\t\t\t\ttext: message.summary,\n\t\t\t\ttokensBefore: Math.max(0, Math.trunc(message.tokensBefore)),\n\t\t\t\ttimestamp,\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\t\tif (message.role === \"custom\") {\n\t\t\tconst text =\n\t\t\t\ttypeof message.content === \"string\"\n\t\t\t\t\t? message.content\n\t\t\t\t\t: message.content\n\t\t\t\t\t\t\t.filter((part) => part.type === \"text\")\n\t\t\t\t\t\t\t.map((part) => part.text)\n\t\t\t\t\t\t\t.join(\"\\n\");\n\t\t\titems.push({\n\t\t\t\tid,\n\t\t\t\trole: \"note\",\n\t\t\t\tkind: \"custom\",\n\t\t\t\ttext,\n\t\t\t\tcustomType: message.customType,\n\t\t\t\tdisplay: message.display,\n\t\t\t\ttimestamp,\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\t\tif (message.role !== \"assistant\") continue;\n\t\tconst streaming = session.isStreaming && message === messages.at(-1);\n\t\tconst content: Array<\n\t\t\t| { type: \"text\"; text: string }\n\t\t\t| { type: \"thinking\"; thinking: string; redacted?: boolean }\n\t\t\t| { type: \"toolCall\"; toolCallId: string; toolName: string; input: JsonValue }\n\t\t> = message.content.map((part) => {\n\t\t\tif (part.type === \"text\") return { type: \"text\", text: part.text };\n\t\t\tif (part.type === \"thinking\") {\n\t\t\t\treturn {\n\t\t\t\t\ttype: \"thinking\",\n\t\t\t\t\tthinking: part.thinking,\n\t\t\t\t\t...(part.redacted === undefined ? {} : { redacted: part.redacted }),\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\ttype: \"toolCall\",\n\t\t\t\ttoolCallId: part.id,\n\t\t\t\ttoolName: part.name,\n\t\t\t\tinput: part.arguments as JsonValue,\n\t\t\t};\n\t\t});\n\t\tconst usage = projectUsage(message.usage);\n\t\tconst common = {\n\t\t\tid,\n\t\t\trole: \"assistant\" as const,\n\t\t\tcontent,\n\t\t\tmodel: { provider: message.provider, id: message.model } satisfies ModelRef,\n\t\t\t...(message.responseModel ? { responseModel: message.responseModel } : {}),\n\t\t\t...(usage === undefined ? {} : { usage }),\n\t\t\ttimestamp,\n\t\t};\n\t\tif (streaming || message.stopReason === \"pending\" || message.stopReason === \"deferred\") {\n\t\t\tconst item: TranscriptItem = { ...common, status: \"streaming\" };\n\t\t\titems.push(item);\n\t\t} else if (message.stopReason === \"error\") {\n\t\t\tconst item: TranscriptItem = {\n\t\t\t\t...common,\n\t\t\t\tstatus: \"error\",\n\t\t\t\tstopReason: \"error\",\n\t\t\t\t...(message.errorMessage ? { errorMessage: message.errorMessage } : {}),\n\t\t\t};\n\t\t\titems.push(item);\n\t\t} else if (message.stopReason === \"aborted\") {\n\t\t\tconst item: TranscriptItem = {\n\t\t\t\t...common,\n\t\t\t\tstatus: \"aborted\",\n\t\t\t\tstopReason: \"aborted\",\n\t\t\t\t...(message.errorMessage ? { errorMessage: message.errorMessage } : {}),\n\t\t\t};\n\t\t\titems.push(item);\n\t\t} else {\n\t\t\tconst item: TranscriptItem = { ...common, status: \"complete\", stopReason: message.stopReason };\n\t\t\titems.push(item);\n\t\t}\n\t}\n\treturn items;\n}\n\nfunction sessionPhase(session: AgentSession): SessionPhase {\n\tif (session.isCompacting) return \"compaction\";\n\tif (session.isStreaming) return \"turn\";\n\treturn \"idle\";\n}\n\n/** Server-side runtime over one exclusively owned coding-agent session. */\nexport class CodingAgentSessionRuntime implements PiSessionRuntime {\n\tprivate readonly session: AgentSession;\n\tprivate readonly descriptor: CodingAgentSessionDescriptor;\n\tprivate readonly models: CodingAgentServerModels;\n\tprivate readonly listeners = new Set<(event: PiSessionRuntimeEvent) => void>();\n\tprivate readonly unsubscribe: () => void;\n\tprivate readonly onDisposed?: () => void;\n\tprivate disposed = false;\n\n\tconstructor(\n\t\tsession: AgentSession,\n\t\tdescriptor: CodingAgentSessionDescriptor,\n\t\toptions?: { readonly models?: CodingAgentServerModels; readonly onDisposed?: () => void },\n\t) {\n\t\tthis.session = session;\n\t\tthis.descriptor = descriptor;\n\t\tthis.models = options?.models ?? {\n\t\t\tlist: async () => [],\n\t\t\tresolve: () => {\n\t\t\t\tthrow new PiServerError(\"not_implemented\", \"Model resolution is not configured\");\n\t\t\t},\n\t\t};\n\t\tthis.onDisposed = options?.onDisposed;\n\t\tthis.unsubscribe = session.subscribe((event: AgentSessionEvent) => this.onSessionEvent(event));\n\t}\n\n\tsnapshot(): SessionSnapshot {\n\t\tconst queuedSteer = this.session.getSteeringMessages();\n\t\tconst name = this.session.sessionName ?? this.descriptor.name;\n\t\treturn {\n\t\t\tid: this.descriptor.id,\n\t\t\t...(name === undefined ? {} : { name }),\n\t\t\t// The protocol requires a non-empty snapshot cwd. This runtime only\n\t\t\t// describes acquired sessions: creates validate a non-empty cwd and\n\t\t\t// opens fall back to a non-empty cwd inside the source, so the\n\t\t\t// SessionManager cwd is the authoritative value when the descriptor\n\t\t\t// carries none (legacy sessions record no cwd).\n\t\t\tcwd: this.descriptor.cwd ?? this.session.sessionManager.getCwd(),\n\t\t\tcreatedAt: this.descriptor.createdAt,\n\t\t\tupdatedAt: this.descriptor.updatedAt,\n\t\t\tphase: sessionPhase(this.session),\n\t\t\tmodel: this.currentModelRef(),\n\t\t\tthinkingLevel: this.session.thinkingLevel,\n\t\t\t...(this.descriptor.profileId === undefined ? {} : { profileId: this.descriptor.profileId }),\n\t\t\t...(this.descriptor.profileVersion === undefined ? {} : { profileVersion: this.descriptor.profileVersion }),\n\t\t\tattached: false,\n\t\t\tlocked: false,\n\t\t\t// Durable revision: the SessionManager entry sequence. It survives\n\t\t\t// reopen and only advances when a persisted entry lands.\n\t\t\trevision: this.session.getRevision().sequence,\n\t\t\ttranscript: projectSessionTranscript(this.session),\n\t\t\tqueuedSteer: queuedSteer.map((text, index) => ({\n\t\t\t\tid: `steer-${index}`,\n\t\t\t\trole: \"user\" as const,\n\t\t\t\tcontent: [{ type: \"text\" as const, text }],\n\t\t\t\ttimestamp: this.descriptor.updatedAt,\n\t\t\t})),\n\t\t\tqueuedSteerCount: queuedSteer.length,\n\t\t};\n\t}\n\n\tgetPhase(): SessionPhase {\n\t\treturn sessionPhase(this.session);\n\t}\n\n\tasync prompt(input: { readonly text: string }): Promise<void> {\n\t\tthis.assertUsable();\n\t\trequireIdle(this.session, \"submit a prompt\");\n\t\ttry {\n\t\t\tawait this.session.prompt(input.text);\n\t\t} catch (error) {\n\t\t\tthrow toError(error);\n\t\t}\n\t}\n\n\tasync steer(input: { readonly text: string }): Promise<void> {\n\t\tthis.assertUsable();\n\t\tif (this.session.isIdle) {\n\t\t\tthrow new PiServerError(\"busy\", \"There is no active prompt to steer\");\n\t\t}\n\t\ttry {\n\t\t\tawait this.session.steer(input.text);\n\t\t} catch (error) {\n\t\t\tthrow toError(error);\n\t\t}\n\t}\n\n\tasync abort(): Promise<void> {\n\t\tthis.assertUsable();\n\t\ttry {\n\t\t\tawait this.session.abort();\n\t\t} catch (error) {\n\t\t\tthrow toError(error);\n\t\t}\n\t}\n\n\tasync setModel(model: ModelRef): Promise<void> {\n\t\tthis.assertUsable();\n\t\trequireIdle(this.session, \"change the model\");\n\t\tlet resolved: Model<Api>;\n\t\ttry {\n\t\t\tresolved = await this.models.resolve(model);\n\t\t\tawait this.session.setModel(resolved);\n\t\t} catch (error) {\n\t\t\tthrow toError(error);\n\t\t}\n\t}\n\n\tasync setThinking(thinkingLevel: ThinkingLevel): Promise<void> {\n\t\tthis.assertUsable();\n\t\trequireIdle(this.session, \"change the thinking level\");\n\t\ttry {\n\t\t\tthis.session.setThinkingLevel(thinkingLevel);\n\t\t} catch (error) {\n\t\t\tthrow toError(error);\n\t\t}\n\t}\n\n\tsubscribe(listener: (event: PiSessionRuntimeEvent) => void): () => void {\n\t\tthis.listeners.add(listener);\n\t\treturn () => this.listeners.delete(listener);\n\t}\n\n\tasync dispose(): Promise<void> {\n\t\tif (this.disposed) return;\n\t\tthis.disposed = true;\n\t\tthis.unsubscribe();\n\t\ttry {\n\t\t\tawait this.session.dispose();\n\t\t} finally {\n\t\t\tthis.onDisposed?.();\n\t\t}\n\t}\n\n\tprivate assertUsable(): void {\n\t\tif (this.disposed) throw new PiServerError(\"invalid_request\", \"Session runtime is disposed\");\n\t}\n\n\tprivate onSessionEvent(_event: AgentSessionEvent): void {\n\t\t// Every event still broadcasts a snapshot, but the revision value is the\n\t\t// durable SessionManager sequence: events that persist no entry keep the\n\t\t// same revision number. Clients accept equal-revision snapshots (only\n\t\t// regressions are dropped), so no state is lost.\n\t\tfor (const listener of [...this.listeners]) {\n\t\t\ttry {\n\t\t\t\tlistener({ type: \"snapshot\" });\n\t\t\t} catch {\n\t\t\t\t// Snapshot observers must not affect the session.\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate currentModelRef(): ModelRef {\n\t\tconst model = this.session.model;\n\t\t// Defensive layer: the create/open preflights reject model-less sessions,\n\t\t// so a live runtime always has a model. A residual model-less runtime must\n\t\t// still fail diagnosably instead of surfacing as an unspecific internal\n\t\t// error through snapshot projection.\n\t\tif (!model) throw new PiServerError(\"invalid_request\", \"Session has no active model\");\n\t\treturn { provider: model.provider, id: model.id };\n\t}\n}\n\n/** Exclusive `PiServerService` over a coding-agent session source. */\nexport class CodingAgentServerService implements PiServerService {\n\tprivate readonly source: CodingAgentServerSessionSource;\n\tprivate readonly models: CodingAgentServerModels;\n\tprivate readonly now: () => number;\n\tprivate readonly acquired = new Map<string, CodingAgentSessionRuntime>();\n\tprivate readonly opening = new Set<string>();\n\tprivate readonly durable: DurableServiceV1 | undefined;\n\tprivate readonly executors = new Map<string, CodingAgentOperationExecutor>();\n\t/** In-memory operation store surface; undefined without a durable service. */\n\tprivate readonly operationStore: PiSessionOperationStore | undefined;\n\t/** In-memory artifact store surface; undefined without a durable service. */\n\tprivate readonly artifactStore: PiSessionArtifactStore | undefined;\n\t/** Durable mutation-response store surface; undefined without a durable service. */\n\tprivate readonly mutationResponseStore: MutationResponseStore | undefined;\n\tprivate readonly writerId: string | undefined;\n\tprivate leaseReady = false;\n\t/** Idempotency key (`sessionId\\0key`) -> recorded operation. */\n\tprivate readonly idempotencyIndex = new Map<string, RecordedOperation>();\n\t/** Operation id -> recorded payload snapshot for idempotent acks. */\n\tprivate readonly recordedOperations = new Map<string, RecordedOperation>();\n\t/** Active executor abort controllers by operation id. */\n\tprivate readonly inFlight = new Map<string, AbortController>();\n\t/** Cancel requests observed while the executor was still draining. */\n\tprivate readonly cancelRequested = new Set<string>();\n\n\tconstructor(options: {\n\t\treadonly sessions: CodingAgentServerSessionSource;\n\t\treadonly models: CodingAgentServerModels;\n\t\treadonly durable?: DurableServiceV1;\n\t\treadonly writerId?: string;\n\t\treadonly operationExecutors?: readonly CodingAgentOperationExecutor[];\n\t\treadonly now?: () => number;\n\t}) {\n\t\tthis.source = options.sessions;\n\t\tthis.models = options.models;\n\t\tthis.now = options.now ?? (() => Date.now());\n\t\tfor (const executor of options.operationExecutors ?? []) this.executors.set(executor.operationType, executor);\n\t\tif (options.durable) {\n\t\t\tthis.durable = options.durable;\n\t\t\tthis.writerId = options.writerId ?? randomUUID();\n\t\t\tthis.claimWriterLease();\n\t\t\tif (this.leaseReady) this.sweepInterruptedOperations();\n\t\t\tthis.operationStore = {\n\t\t\t\tsubmit: (input) => this.submitOperation(input),\n\t\t\t\tcancel: (input) => this.cancelOperation(input),\n\t\t\t\tresult: (input) => this.resultOperation(input),\n\t\t\t};\n\t\t\tthis.artifactStore = {\n\t\t\t\tstat: (sessionId, artifactId) => this.statArtifact(sessionId, artifactId),\n\t\t\t\tread: (sessionId, artifactId, readerOptions) => this.readArtifact(sessionId, artifactId, readerOptions),\n\t\t\t};\n\t\t\tthis.mutationResponseStore = {\n\t\t\t\tload: (sessionId, operationId) => this.loadMutationResponse(sessionId, operationId),\n\t\t\t\trecord: (sessionId, operationId, snapshot) => this.recordMutationResponse(sessionId, operationId, snapshot),\n\t\t\t};\n\t\t}\n\t}\n\n\t/** Remote artifact access; unconfigured without a durable service. */\n\tget artifacts(): PiSessionArtifactStore | undefined {\n\t\treturn this.artifactStore;\n\t}\n\n\t/** Durable operation commands; unconfigured without a durable service. */\n\tget operations(): PiSessionOperationStore | undefined {\n\t\treturn this.operationStore;\n\t}\n\n\t/** Durable replay of mutation-command responses; unconfigured without a durable service. */\n\tget mutationResponses(): MutationResponseStore | undefined {\n\t\treturn this.mutationResponseStore;\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Durable writer lease (single-instance boundary)\n\t// -------------------------------------------------------------------------\n\n\t/**\n\t * Claims the writer lease at startup. A holder found in restored state\n\t * belonged to a dead process (snapshot import), so it is recovered; a live\n\t * foreign writer keeps the lease and this service stays unavailable.\n\t */\n\tprivate claimWriterLease(): void {\n\t\tconst durable = this.durable;\n\t\tconst writerId = this.writerId;\n\t\tif (!durable || !writerId) return;\n\t\tif (durable.claimWriter(writerId).status === \"claimed\") {\n\t\t\tthis.leaseReady = true;\n\t\t\treturn;\n\t\t}\n\t\tthis.leaseReady = durable.recoverWriter(writerId).status === \"recovered\";\n\t}\n\n\t/**\n\t * Releases the store writer lease on graceful shutdown (D-067 phase 2 S2)\n\t * so a successor process can claim immediately. Best-effort: only a held\n\t * lease is released, and failures are swallowed — a crashed process leaves\n\t * the lease for the TTL recovery path instead.\n\t */\n\treleaseWriterLease(): void {\n\t\tconst durable = this.durable;\n\t\tconst writerId = this.writerId;\n\t\tif (!durable || !writerId || !this.leaseReady) return;\n\t\ttry {\n\t\t\tdurable.releaseWriter(writerId);\n\t\t\tthis.leaseReady = false;\n\t\t} catch {\n\t\t\t// Shutdown release is advisory; the TTL recovery path handles the rest.\n\t\t}\n\t}\n\n\t/** Persists one adapter-owned operation record; a lost lease degrades to `durable_unavailable`. */\n\tprivate persistOperation(operationId: string, sessionId: string, payload: DurableOperationPayloadV1): void {\n\t\ttry {\n\t\t\tthis.durable?.persistOperation({\n\t\t\t\twriterId: this.writerId ?? \"\",\n\t\t\t\toperationId,\n\t\t\t\tsessionId,\n\t\t\t\tdata: payload,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tif (error instanceof DurableServiceErrorV1) {\n\t\t\t\tthrow new PiServerError(\"durable_unavailable\", error.message);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tprivate requireDurable(): DurableServiceV1 {\n\t\tif (!this.durable || !this.leaseReady) {\n\t\t\tthrow new PiServerError(\"durable_unavailable\", \"Durable operation store is not ready\");\n\t\t}\n\t\treturn this.durable;\n\t}\n\n\t/**\n\t * Rejects operations that were pending when the previous process stopped:\n\t * no executor survives a crash, so replayed `pending` records are swept to\n\t * a rejected terminal and their idempotency keys keep returning that\n\t/**\n\t * recorded terminal.\n\t */\n\tprivate sweepInterruptedOperations(): void {\n\t\tconst durable = this.durable;\n\t\tif (!durable) return;\n\t\tfor (const record of durable.exportState().operations) {\n\t\t\tconst payload = parseOperationPayload(record.data);\n\t\t\tif (!payload) continue;\n\t\t\tconst resolved: DurableOperationPayloadV1 =\n\t\t\t\tpayload.status === \"pending\"\n\t\t\t\t\t? {\n\t\t\t\t\t\t\t...payload,\n\t\t\t\t\t\t\tstatus: \"rejected\",\n\t\t\t\t\t\t\terror: { code: \"internal_error\", message: \"Operation did not complete before the server stopped\" },\n\t\t\t\t\t\t\tterminalAt: this.now(),\n\t\t\t\t\t\t}\n\t\t\t\t\t: payload;\n\t\t\tif (resolved !== payload) this.persistOperation(record.operationId, payload.sessionId, resolved);\n\t\t\tthis.recordOperation(record.operationId, resolved);\n\t\t}\n\t}\n\n\tprivate recordOperation(operationId: string, payload: DurableOperationPayloadV1): void {\n\t\tthis.recordedOperations.set(operationId, { operationId, payload });\n\t\tif (payload.idempotencyKey !== undefined) {\n\t\t\tthis.idempotencyIndex.set(`${payload.sessionId}\\0${payload.idempotencyKey}`, { operationId, payload });\n\t\t}\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Operation commands (server `operations` surface)\n\t// -------------------------------------------------------------------------\n\n\tprivate async submitOperation(input: {\n\t\treadonly sessionId: string;\n\t\treadonly operationType: string;\n\t\treadonly input?: JsonValue;\n\t\treadonly idempotencyKey?: string;\n\t\treadonly expectedRevision?: number;\n\t}): Promise<PiSessionOperationAck> {\n\t\tthis.requireDurable();\n\t\t// Idempotent replay precedes the live-session check: a recorded terminal\n\t\t// must stay answerable after a restart even though no runtime is open.\n\t\tif (input.idempotencyKey !== undefined) {\n\t\t\tconst recorded = this.idempotencyIndex.get(`${input.sessionId}\\0${input.idempotencyKey}`);\n\t\t\tif (recorded) return this.ackOf(recorded);\n\t\t}\n\t\tconst runtime = this.acquired.get(input.sessionId);\n\t\tif (!runtime) throw new PiServerError(\"not_found\", `Session is not open: ${input.sessionId}`);\n\t\tif (input.expectedRevision !== undefined) {\n\t\t\tconst revision = (await runtime.snapshot()).revision;\n\t\t\tif (revision !== input.expectedRevision) {\n\t\t\t\tthrow new PiServerError(\n\t\t\t\t\t\"operation_conflict\",\n\t\t\t\t\t`Session revision is ${revision}, expectedRevision was ${input.expectedRevision}`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t\tconst operationId = randomUUID();\n\t\tconst payload: DurableOperationPayloadV1 = {\n\t\t\tstatus: \"pending\",\n\t\t\tsessionId: input.sessionId,\n\t\t\toperationType: input.operationType,\n\t\t\t...(input.idempotencyKey === undefined ? {} : { idempotencyKey: input.idempotencyKey }),\n\t\t\tsubmittedAt: this.now(),\n\t\t};\n\t\tthis.persistOperation(operationId, input.sessionId, payload);\n\t\tthis.recordOperation(operationId, payload);\n\t\t// Fire-and-forget execution: the ack must reach the client immediately,\n\t\t// otherwise a long operation hides its own operationId and cancel/result\n\t\t// stay unreachable. `executeOperation` records executor failures as\n\t\t// rejected terminals; only a terminal persist failure can still escape,\n\t\t// so pin the in-memory record to a rejected terminal carrying the\n\t\t// original error. The durable log keeps `pending` and the restart sweep\n\t\t// converges the record.\n\t\tthis.executeOperation(operationId, payload, input.input).catch((error: unknown) => {\n\t\t\tthis.recordOperation(operationId, {\n\t\t\t\t...payload,\n\t\t\t\tstatus: \"rejected\",\n\t\t\t\terror: { code: \"internal_error\", message: error instanceof Error ? error.message : String(error) },\n\t\t\t\tterminalAt: this.now(),\n\t\t\t});\n\t\t});\n\t\treturn this.ackOf({ operationId, payload });\n\t}\n\n\t/**\n\t * Runs the executor and persists the terminal state. Cancel is only a\n\t * request: the terminal record is written strictly after the executor\n\t * drains, so `cancel -> drain -> terminal` is the observable order.\n\t */\n\tprivate async executeOperation(\n\t\toperationId: string,\n\t\tpayload: DurableOperationPayloadV1,\n\t\tinput: JsonValue | undefined,\n\t): Promise<PiSessionOperationAck> {\n\t\tconst executor = this.executors.get(payload.operationType);\n\t\tif (!executor) {\n\t\t\treturn this.completeOperation(operationId, {\n\t\t\t\t...payload,\n\t\t\t\tstatus: \"rejected\",\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"not_implemented\",\n\t\t\t\t\tmessage: `No executor is registered for operation type ${payload.operationType}`,\n\t\t\t\t},\n\t\t\t\tterminalAt: this.now(),\n\t\t\t});\n\t\t}\n\t\tconst controller = new AbortController();\n\t\tthis.inFlight.set(operationId, controller);\n\t\tlet terminal: DurableOperationPayloadV1;\n\t\ttry {\n\t\t\tconst result = await executor.execute({\n\t\t\t\tsessionId: payload.sessionId,\n\t\t\t\toperationId,\n\t\t\t\t...(input === undefined ? {} : { input }),\n\t\t\t\tsignal: controller.signal,\n\t\t\t});\n\t\t\tterminal = { ...payload, status: \"completed\", result, terminalAt: this.now() };\n\t\t} catch (error) {\n\t\t\tterminal = {\n\t\t\t\t...payload,\n\t\t\t\tstatus: \"rejected\",\n\t\t\t\terror: this.toTerminalError(operationId, error),\n\t\t\t\tterminalAt: this.now(),\n\t\t\t};\n\t\t} finally {\n\t\t\tthis.inFlight.delete(operationId);\n\t\t\tthis.cancelRequested.delete(operationId);\n\t\t}\n\t\treturn this.completeOperation(operationId, terminal);\n\t}\n\n\tprivate toTerminalError(\n\t\toperationId: string,\n\t\terror: unknown,\n\t): {\n\t\treadonly code: ProtocolErrorCode;\n\t\treadonly message: string;\n\t} {\n\t\tif (this.cancelRequested.has(operationId)) {\n\t\t\treturn { code: \"operation_cancelled\", message: \"Operation was cancelled before completion\" };\n\t\t}\n\t\tif (error instanceof PiServerError) return { code: error.code, message: error.message };\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\treturn { code: \"invalid_request\", message };\n\t}\n\n\tprivate completeOperation(operationId: string, terminal: DurableOperationPayloadV1): PiSessionOperationAck {\n\t\tthis.persistOperation(operationId, terminal.sessionId, terminal);\n\t\tthis.recordOperation(operationId, terminal);\n\t\treturn this.ackOf({ operationId, payload: terminal });\n\t}\n\n\tprivate cancelOperation(input: { readonly sessionId: string; readonly operationId: string }): PiSessionOperationAck {\n\t\tthis.requireDurable();\n\t\tconst recorded = this.recordedOperations.get(input.operationId);\n\t\t// Session ownership is part of the lookup: a foreign session gets the\n\t\t// same not-found answer as a missing id, without leaking ownership.\n\t\tif (!recorded || recorded.payload.sessionId !== input.sessionId) {\n\t\t\tthrow new PiServerError(\"operation_not_found\", `Unknown operation: ${input.operationId}`);\n\t\t}\n\t\tif (recorded.payload.status !== \"pending\") return this.ackOf(recorded);\n\t\t// Record the cancel request and signal the executor. The terminal state\n\t\t// is only written once the executor drained (or nothing is draining).\n\t\tthis.cancelRequested.add(input.operationId);\n\t\tthis.inFlight.get(input.operationId)?.abort();\n\t\treturn { operationId: input.operationId, status: \"accepted\" };\n\t}\n\n\tprivate resultOperation(input: { readonly sessionId: string; readonly operationId: string }): PiSessionOperationAck {\n\t\tthis.requireDurable();\n\t\tconst recorded = this.recordedOperations.get(input.operationId);\n\t\t// Session ownership is part of the lookup: a foreign session gets the\n\t\t// same not-found answer as a missing id, without leaking ownership.\n\t\tif (!recorded || recorded.payload.sessionId !== input.sessionId) {\n\t\t\tthrow new PiServerError(\"operation_not_found\", `Unknown operation: ${input.operationId}`);\n\t\t}\n\t\treturn this.ackOf(recorded);\n\t}\n\n\tprivate ackOf(recorded: RecordedOperation): PiSessionOperationAck {\n\t\tconst { operationId, payload } = recorded;\n\t\tif (payload.status === \"completed\") {\n\t\t\treturn {\n\t\t\t\toperationId,\n\t\t\t\tstatus: \"completed\",\n\t\t\t\t...(payload.result === undefined ? {} : { result: payload.result }),\n\t\t\t};\n\t\t}\n\t\tif (payload.status === \"rejected\") {\n\t\t\treturn {\n\t\t\t\toperationId,\n\t\t\t\tstatus: \"rejected\",\n\t\t\t\t...(payload.error === undefined\n\t\t\t\t\t? {}\n\t\t\t\t\t: { error: { code: payload.error.code, message: payload.error.message } satisfies ProtocolError }),\n\t\t\t};\n\t\t}\n\t\treturn { operationId, status: \"accepted\" };\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Artifact adapter (output-artifacts face + durable persistence)\n\t// -------------------------------------------------------------------------\n\n\t/**\n\t * Mirrors one runtime output artifact into the durable store so remote\n\t * `artifact_stat`/`artifact_read` can serve it, including after a restart\n\t * via log replay. The runtime in-memory store (capability-runtime) stays\n\t * the live write path; this adds durable persistence, not a second store.\n\t */\n\tpersistOutputArtifact(sessionId: string, ref: OutputArtifactRef, bytes: Uint8Array): ArtifactRef {\n\t\tthis.requireDurable();\n\t\tif (ref.sessionId !== sessionId) {\n\t\t\tthrow new PiServerError(\"invalid_request\", \"Artifact sessionId must match the owning session\");\n\t\t}\n\t\tif (ref.artifactId === \"\") throw new PiServerError(\"invalid_request\", \"Artifact id must not be empty\");\n\t\tif (ref.sizeBytes !== undefined) {\n\t\t\t// A truncated artifact's payload is the runtime's capped resident view\n\t\t\t// (head+marker+tail, see OUTPUT_ARTIFACT_MAX_BUFFER_BYTES), never the\n\t\t\t// full original, so it is only bounded above by sizeBytes.\n\t\t\tif (ref.truncated ? bytes.byteLength > ref.sizeBytes : bytes.byteLength !== ref.sizeBytes) {\n\t\t\t\tthrow new PiServerError(\"invalid_request\", \"Artifact sizeBytes must match the payload length\");\n\t\t\t}\n\t\t}\n\t\ttry {\n\t\t\tthis.durable?.persistArtifact({\n\t\t\t\twriterId: this.writerId ?? \"\",\n\t\t\t\tartifactId: ref.artifactId,\n\t\t\t\tsessionId,\n\t\t\t\tdata: { ref, bytes: Buffer.from(bytes).toString(\"base64\") } satisfies DurableArtifactPayloadV1,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tif (error instanceof DurableServiceErrorV1) {\n\t\t\t\tthrow new PiServerError(\"durable_unavailable\", error.message);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t\treturn toProtocolArtifactRef(ref);\n\t}\n\n\tprivate statArtifact(sessionId: string, artifactId: string): ArtifactRef {\n\t\treturn toProtocolArtifactRef(this.loadArtifactPayload(sessionId, artifactId).ref);\n\t}\n\n\tprivate readArtifact(\n\t\tsessionId: string,\n\t\tartifactId: string,\n\t\toptions: { readonly offset: number; readonly maxBytes: number },\n\t): {\n\t\treadonly artifact: ArtifactRef;\n\t\treadonly data: Uint8Array;\n\t\treadonly nextOffset?: number;\n\t\treadonly eof: boolean;\n\t} {\n\t\tconst payload = this.loadArtifactPayload(sessionId, artifactId);\n\t\tconst bytes = Buffer.from(payload.bytes, \"base64\");\n\t\tconst end = Math.min(options.offset + options.maxBytes, bytes.byteLength);\n\t\tconst data = new Uint8Array(bytes.subarray(options.offset, end));\n\t\tconst eof = end >= bytes.byteLength;\n\t\treturn {\n\t\t\tartifact: toProtocolArtifactRef(payload.ref),\n\t\t\tdata,\n\t\t\t...(eof ? {} : { nextOffset: end }),\n\t\t\teof,\n\t\t};\n\t}\n\n\t/**\n\t * Loads one artifact payload through the durable service.\n\t */\n\tprivate loadArtifactPayload(sessionId: string, artifactId: string): DurableArtifactPayloadV1 {\n\t\tconst durable = this.requireDurable();\n\t\tconst record = durable.loadArtifact({ artifactId });\n\t\tif (!record || record.sessionId !== sessionId) {\n\t\t\tthrow new PiServerError(\"not_found\", `Unknown artifact: ${artifactId}`);\n\t\t}\n\t\tconst payload = parseArtifactPayload(record.data);\n\t\tif (!payload || payload.ref.sessionId !== sessionId || payload.ref.artifactId !== artifactId) {\n\t\t\tthrow new PiServerError(\"durable_unavailable\", \"Stored artifact record is malformed\");\n\t\t}\n\t\treturn payload;\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Mutation-response store (idempotent replay of keyed mutation commands)\n\t// -------------------------------------------------------------------------\n\n\t/**\n\t * Deterministic artifact id for one mutation-command response, following the\n\t * `agent-task:<taskId>` precedent for derived artifact ids.\n\t */\n\tprivate mutationResponseArtifactId(sessionId: string, operationId: string): string {\n\t\treturn `mutation-response:${sessionId}:${operationId}`;\n\t}\n\n\t/**\n\t * Loads a recorded response through the durable service. The durable log\n\t * was already replayed into memory at construction, so this read never\n\t * touches the disk. A missing or malformed record yields undefined so the\n\t * caller re-executes; unexpected durable failures propagate and are\n\t * reported (and treated as a miss) by the server's idempotency layer.\n\t */\n\tprivate async loadMutationResponse(sessionId: string, operationId: string): Promise<SessionSnapshot | undefined> {\n\t\tconst durable = this.requireDurable();\n\t\tconst record = durable.loadArtifact({\n\t\t\tartifactId: this.mutationResponseArtifactId(sessionId, operationId),\n\t\t});\n\t\tif (record === undefined || record.sessionId !== sessionId) return undefined;\n\t\tconst snapshot = parseMutationResponsePayload(record.data, sessionId);\n\t\treturn snapshot === undefined ? undefined : structuredClone(snapshot);\n\t}\n\n\t/**\n\t * Persists one successful response snapshot as a durable artifact record so\n\t * retries after a restart replay it. The payload is the `SessionSnapshot`\n\t * itself: every field is JSON-safe, so the JSONL durable log round-trips it\n\t * without an adapter-specific envelope.\n\t */\n\tprivate async recordMutationResponse(\n\t\tsessionId: string,\n\t\toperationId: string,\n\t\tsnapshot: SessionSnapshot,\n\t): Promise<void> {\n\t\tconst durable = this.requireDurable();\n\t\ttry {\n\t\t\tdurable.persistArtifact({\n\t\t\t\twriterId: this.writerId ?? \"\",\n\t\t\t\tartifactId: this.mutationResponseArtifactId(sessionId, operationId),\n\t\t\t\tsessionId,\n\t\t\t\tdata: snapshot,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tif (error instanceof DurableServiceErrorV1) {\n\t\t\t\tthrow new PiServerError(\"durable_unavailable\", error.message);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tasync listSessions(): Promise<SessionMetadata[]> {\n\t\tconst descriptors = await this.source.list();\n\t\treturn descriptors.map((descriptor) => ({\n\t\t\tid: descriptor.id,\n\t\t\tcreatedAt: descriptor.createdAt,\n\t\t\tupdatedAt: descriptor.updatedAt,\n\t\t\t...(descriptor.name === undefined ? {} : { sessionName: descriptor.name }),\n\t\t\t// Legacy sessions record no cwd; the protocol marks it optional.\n\t\t\t...(descriptor.cwd === undefined ? {} : { cwd: descriptor.cwd }),\n\t\t}));\n\t}\n\n\tasync listModels(): Promise<ModelMetadata[]> {\n\t\treturn [...(await this.models.list())];\n\t}\n\n\tasync createSession(options: CreateSessionOptions): Promise<PiSessionRuntime> {\n\t\tthis.assertNotAcquired(options.id);\n\t\tif (options.cwd === undefined || options.cwd.trim() === \"\") {\n\t\t\tthrow new PiServerError(\"invalid_request\", \"Creating a coding-agent session requires a cwd\");\n\t\t}\n\t\t// The protocol carries a model ref; resolve it to a Model instance the\n\t\t// same way runtime `set_model` does. An unknown ref fails the create\n\t\t// instead of being silently dropped.\n\t\tlet model: Model<Api> | undefined;\n\t\ttry {\n\t\t\tmodel = options.model === undefined ? undefined : await this.models.resolve(options.model);\n\t\t} catch (error) {\n\t\t\tthrow toError(error);\n\t\t}\n\t\tconst timestamp = Math.max(0, Math.trunc(this.now()));\n\t\tconst descriptor: CodingAgentSessionDescriptor = {\n\t\t\tid: options.id,\n\t\t\t...(options.name === undefined ? {} : { name: options.name }),\n\t\t\tcwd: options.cwd,\n\t\t\t...(model === undefined ? {} : { model }),\n\t\t\t...(options.thinkingLevel === undefined ? {} : { thinkingLevel: options.thinkingLevel }),\n\t\t\t...(options.profileId === undefined ? {} : { profileId: options.profileId }),\n\t\t\t...(options.profileVersion === undefined ? {} : { profileVersion: options.profileVersion }),\n\t\t\tcreatedAt: timestamp,\n\t\t\tupdatedAt: timestamp,\n\t\t};\n\t\tlet session: AgentSession;\n\t\ttry {\n\t\t\tsession = await this.openExclusively(descriptor.id, () => this.source.create(descriptor));\n\t\t} catch (error) {\n\t\t\tthrow toError(error);\n\t\t}\n\t\t// Lazy-model contract: createAgentSession does not fail when no model is\n\t\t// resolvable (no auth, empty catalog); it returns a session whose model is\n\t\t// undefined. A model-less live session cannot serve prompts and breaks\n\t\t// every client's list/server_snapshot, so the create fails explicitly.\n\t\t// Disposing rolls the session back before any registration; the\n\t\t// SessionManager has not flushed the JSONL yet (persistence defers until\n\t\t// the first user message), so nothing stays on disk, and a flushed\n\t\t// residue would still be harmless: reopening it runs into the same\n\t\t// preflight in openSession.\n\t\tif (session.model === undefined) {\n\t\t\t// Keep the structured rejection even if rollback fails; the cleanup\n\t\t\t// failure is appended to the message instead of masking it.\n\t\t\tlet cleanupNote = \"\";\n\t\t\ttry {\n\t\t\t\tawait session.dispose();\n\t\t\t} catch (disposeError) {\n\t\t\t\tcleanupNote = ` (session cleanup also failed: ${disposeError instanceof Error ? disposeError.message : String(disposeError)})`;\n\t\t\t}\n\t\t\tthrow new PiServerError(\n\t\t\t\t\"invalid_request\",\n\t\t\t\t`Creating a coding-agent session requires a resolvable model; none is available for this server's agent directory${cleanupNote}`,\n\t\t\t);\n\t\t}\n\t\treturn this.acquireRuntime(descriptor, session);\n\t}\n\n\tasync openSession(sessionId: string): Promise<PiSessionRuntime> {\n\t\tthis.assertNotAcquired(sessionId);\n\t\tconst descriptors = await this.source.list();\n\t\tconst descriptor = descriptors.find((entry) => entry.id === sessionId);\n\t\tif (!descriptor) throw new PiServerError(\"not_found\", `Unknown session: ${sessionId}`);\n\t\tlet session: AgentSession;\n\t\ttry {\n\t\t\tsession = await this.openExclusively(descriptor.id, () => this.source.open(descriptor));\n\t\t} catch (error) {\n\t\t\tthrow toError(error);\n\t\t}\n\t\t// Same preflight as create: a history session whose stored model no longer\n\t\t// resolves (model removed, auth missing) must fail the open instead of\n\t\t// going live model-less and breaking every client's list/server_snapshot.\n\t\tif (session.model === undefined) {\n\t\t\tlet cleanupNote = \"\";\n\t\t\ttry {\n\t\t\t\tawait session.dispose();\n\t\t\t} catch (disposeError) {\n\t\t\t\tcleanupNote = ` (session cleanup also failed: ${disposeError instanceof Error ? disposeError.message : String(disposeError)})`;\n\t\t\t}\n\t\t\tthrow new PiServerError(\n\t\t\t\t\"invalid_request\",\n\t\t\t\t`Reopening session ${sessionId} requires a resolvable model; none is available for this server's agent directory${cleanupNote}`,\n\t\t\t);\n\t\t}\n\t\treturn this.acquireRuntime(descriptor, session);\n\t}\n\n\t/** Serializes the acquired-check with the async source call to keep opens exclusive. */\n\tprivate async openExclusively(id: string, open: () => Promise<AgentSession>): Promise<AgentSession> {\n\t\tif (this.opening.has(id) || this.acquired.has(id)) {\n\t\t\tthrow new PiServerError(\"session_locked\", `Session is already open: ${id}`);\n\t\t}\n\t\tthis.opening.add(id);\n\t\ttry {\n\t\t\treturn await open();\n\t\t} finally {\n\t\t\tthis.opening.delete(id);\n\t\t}\n\t}\n\n\tprivate acquireRuntime(descriptor: CodingAgentSessionDescriptor, session: AgentSession): CodingAgentSessionRuntime {\n\t\t// Auto-mirror: with a durable service configured, every artifact committed\n\t\t// by the session's capability runtime is mirrored here, so the wire face\n\t\t// serves it without an explicit persist call. Without a durable service\n\t\t// nothing is registered (no behavior change). Mirror failures surface as\n\t\t// runtime commit-observer log records; they never fail the artifact put.\n\t\tconst unsubscribeArtifactMirror =\n\t\t\tthis.durable === undefined\n\t\t\t\t? undefined\n\t\t\t\t: session.onOutputArtifactCommit(({ ref, bytes }) => {\n\t\t\t\t\t\tthis.persistOutputArtifact(descriptor.id, ref, bytes);\n\t\t\t\t\t});\n\t\tconst runtime = new CodingAgentSessionRuntime(session, descriptor, {\n\t\t\tmodels: this.models,\n\t\t\tonDisposed: () => {\n\t\t\t\tunsubscribeArtifactMirror?.();\n\t\t\t\tthis.acquired.delete(descriptor.id);\n\t\t\t},\n\t\t});\n\t\tthis.acquired.set(descriptor.id, runtime);\n\t\treturn runtime;\n\t}\n\n\tprivate assertNotAcquired(id: string): void {\n\t\tif (this.acquired.has(id) || this.opening.has(id)) {\n\t\t\tthrow new PiServerError(\"session_locked\", `Session is already open: ${id}`);\n\t\t}\n\t}\n}\n\n/** Composes the coding-agent service with transport, journal, and auth hooks into one server. */\nexport function assembleCodingAgentServer(options: CodingAgentServerAssemblyOptions): {\n\treadonly server: PiServer;\n\treadonly service: CodingAgentServerService;\n} {\n\tconst service = new CodingAgentServerService({\n\t\tsessions: options.sessions,\n\t\tmodels: options.models,\n\t\tnow: options.now,\n\t\t...(options.durable === undefined ? {} : { durable: options.durable }),\n\t\t...(options.writerId === undefined ? {} : { writerId: options.writerId }),\n\t\t...(options.operationExecutors === undefined ? {} : { operationExecutors: options.operationExecutors }),\n\t});\n\tconst server = new PiServer(service, {\n\t\tlisteners: options.listeners,\n\t\t...(options.journal === undefined ? {} : { journal: options.journal }),\n\t\t...(options.authenticate === undefined ? {} : { authenticate: options.authenticate }),\n\t\t...(options.authorize === undefined ? {} : { authorize: options.authorize }),\n\t\t...(options.serverId === undefined ? {} : { serverId: options.serverId }),\n\t\t...(options.maxFrameLength === undefined ? {} : { maxFrameLength: options.maxFrameLength }),\n\t\t...(options.handshakeTimeoutMs === undefined ? {} : { handshakeTimeoutMs: options.handshakeTimeoutMs }),\n\t\t...(options.onError === undefined ? {} : { onError: options.onError }),\n\t});\n\treturn { server, service };\n}\n"]}
|
|
@@ -925,8 +925,13 @@ export class CodingAgentServerService {
|
|
|
925
925
|
}
|
|
926
926
|
if (ref.artifactId === "")
|
|
927
927
|
throw new PiServerError("invalid_request", "Artifact id must not be empty");
|
|
928
|
-
if (ref.sizeBytes !== undefined
|
|
929
|
-
|
|
928
|
+
if (ref.sizeBytes !== undefined) {
|
|
929
|
+
// A truncated artifact's payload is the runtime's capped resident view
|
|
930
|
+
// (head+marker+tail, see OUTPUT_ARTIFACT_MAX_BUFFER_BYTES), never the
|
|
931
|
+
// full original, so it is only bounded above by sizeBytes.
|
|
932
|
+
if (ref.truncated ? bytes.byteLength > ref.sizeBytes : bytes.byteLength !== ref.sizeBytes) {
|
|
933
|
+
throw new PiServerError("invalid_request", "Artifact sizeBytes must match the payload length");
|
|
934
|
+
}
|
|
930
935
|
}
|
|
931
936
|
try {
|
|
932
937
|
this.durable?.persistArtifact({
|