@aiworker/web 1.22.0 → 1.22.2
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/README.md +4 -4
- package/bin/cli.js +7 -4
- package/bin/cli.test.js +165 -73
- package/bin/lib/cli-api-target.js +2 -2
- package/bin/lib/cli-args.js +119 -119
- package/bin/lib/cli-lifecycle.js +9 -9
- package/bin/lib/cli-network.js +3 -3
- package/bin/lib/cli-paths.js +3 -3
- package/bin/lib/cli-ports.js +2 -2
- package/bin/lib/cli-process.js +4 -4
- package/bin/lib/cli-startup.js +64 -15
- package/bin/lib/cli-tunnel-profiles.js +1 -1
- package/bin/lib/cli-tunnel-utils.js +2 -2
- package/bin/lib/commands-connect-url.js +19 -19
- package/bin/lib/commands-lifecycle.js +27 -27
- package/bin/lib/commands-logs.js +5 -5
- package/bin/lib/commands-models.js +1 -1
- package/bin/lib/commands-projects.js +1 -1
- package/bin/lib/commands-schedule.js +1 -1
- package/bin/lib/commands-serve.js +29 -29
- package/bin/lib/commands-session.js +1 -1
- package/bin/lib/commands-startup.js +45 -10
- package/bin/lib/commands-status.js +1 -1
- package/bin/lib/commands-tunnel.js +40 -40
- package/bin/lib/commands-update.js +1 -1
- package/bin/lib/commands-update.test.js +6 -6
- package/dist/assets/{CodeMirrorCommentWidgets-B3u7UIcE.js → CodeMirrorCommentWidgets-CSICpWQD.js} +1 -1
- package/dist/assets/{DiffView-DluiRSla.js → DiffView-hDBKesA_.js} +3 -3
- package/dist/assets/DirtyBranchSwitchDialog-C22HdWj4.js +1 -0
- package/dist/assets/FilesView-CdWzeOp9.js +17 -0
- package/dist/assets/GitView-VQQkYuUw.js +6 -0
- package/dist/assets/{JsonTreeView-B0BdZEHG.js → JsonTreeView-6lrhwD3q.js} +1 -1
- package/dist/assets/LinearIssuesView-Ba6Q9569.js +1 -0
- package/dist/assets/{MarkdownImageGallery-ClOvuZxC.js → MarkdownImageGallery-Czv_mhvS.js} +1 -1
- package/dist/assets/MarkdownRendererImpl-BrMcswiy.js +12 -0
- package/dist/assets/{OnboardingScreen-D2DG7K6b.js → OnboardingScreen-ChxQoAkx.js} +1 -1
- package/dist/assets/{PatchDiff-DvfqDql7.js → PatchDiff-BzcZPlOq.js} +1 -1
- package/dist/assets/{PierreDiffViewer-B7m3f0d7.js → PierreDiffViewer-BchBtZlp.js} +2 -2
- package/dist/assets/PlanView-DyDt_nZ_.js +5 -0
- package/dist/assets/SettingsView-BKf5xBsP.js +28 -0
- package/dist/assets/{SettingsWindow-i45U1Cy1.js → SettingsWindow-DDQYaKWQ.js} +1 -1
- package/dist/assets/{ToolOutputDialog-BTwyh5OF.js → ToolOutputDialog-iYF8Y2bz.js} +6 -6
- package/dist/assets/ToolPartDiffPreview-CdjbOVMM.js +8 -0
- package/dist/assets/{VirtualizedFileDiff-DD8lDVej.js → VirtualizedFileDiff-fFFm2QgH.js} +1 -1
- package/dist/assets/{WalkthroughView-egQgxel3.js → WalkthroughView-DOeCsMju.js} +2 -2
- package/dist/assets/{computeVirtualFileMetrics-BMIRgMHv.js → computeVirtualFileMetrics-CzgLum_K.js} +2 -2
- package/dist/assets/{contextPanelEmbeddedChat-C3fjDbdg.js → contextPanelEmbeddedChat-C0ZLAepL.js} +1 -1
- package/dist/assets/{de-C1P6X5Rs.js → de-B5JaA3SA.js} +6 -5
- package/dist/assets/{es-CDgaF1_t.js → es-EPIH88ii.js} +6 -5
- package/dist/assets/{fr-Bgxqvvz_.js → fr-CAMfxUdZ.js} +6 -5
- package/dist/assets/{index-CODwDsU_.js → index-Bl3LSbjn.js} +1 -1
- package/dist/assets/index-DF5kMP-i.js +39 -0
- package/dist/assets/index-Dl3xbAB_.css +1 -0
- package/dist/assets/{isDiffPlainText-BqayB8d3.js → isDiffPlainText-Dx-dB_8l.js} +1 -1
- package/dist/assets/{isFilePlainText-QfoiKJNY.js → isFilePlainText-CWFPwmnU.js} +1 -1
- package/dist/assets/{ja-BDRrEcNP.js → ja-CifOE1VN.js} +7 -6
- package/dist/assets/{ko-Ba8sQV79.js → ko-BBDyAyYQ.js} +7 -6
- package/dist/assets/{main-C5DVOxc6.js → main-DnBAoktx.js} +2 -2
- package/dist/assets/main-MhSaOZoB.js +930 -0
- package/dist/assets/{markdownCore-CZwzz5gw.js → markdownCore-BrBwOOMn.js} +2 -2
- package/dist/assets/{miniChat-DUJ4irZc.js → miniChat-BKyvLseN.js} +2 -2
- package/dist/assets/{mobile-CcaxB0n4.js → mobile-DKjeFgMN.js} +2 -2
- package/dist/assets/{patchFileDiff-yNfWXN4E.js → patchFileDiff-o8ms1me8.js} +1 -1
- package/dist/assets/pl-DgEHYfFM.js +16 -0
- package/dist/assets/{pt-BR-DIs6ElwV.js → pt-BR-DuWHojUw.js} +6 -5
- package/dist/assets/{renderElectronMiniChatApp-BcKLHXKe.js → renderElectronMiniChatApp-CDgBwwCS.js} +2 -2
- package/dist/assets/renderMobileApp-DJIKIK1r.js +25 -0
- package/dist/assets/runtimeEndpointReset-CYJ9O_xX.js +17 -0
- package/dist/assets/{scheduledTasksApi-DLW0mkQK.js → scheduledTasksApi-D8yamh1b.js} +1 -1
- package/dist/assets/{shikiHighlight-DYOWuY65.js → shikiHighlight-TEQCmzW8.js} +1 -1
- package/dist/assets/tr-D5m2yIWL.js +7 -0
- package/dist/assets/{uk-ZrCNjM7Y.js → uk-D_q60oog.js} +6 -5
- package/dist/assets/{useAppFontEffects-CjrntdL0.js → useAppFontEffects-D7sNpuL2.js} +161 -190
- package/dist/assets/useInlineCommentController-Brz_cLcQ.js +2 -0
- package/dist/assets/{useRootScrollLock-BHYPkrLv.js → useRootScrollLock-lXEnWlaU.js} +1 -1
- package/dist/assets/{vendor-codemirror-language-data-DFZik9R2.js → vendor-codemirror-language-data-Dp3KMRQG.js} +2 -2
- package/dist/assets/vendor-zod-CK21ihK-.js +39 -0
- package/dist/assets/{web-components-Bhg83tjf.js → web-components-_mNEMjFY.js} +1 -1
- package/dist/assets/{zh-CN-iv_AV4EC.js → zh-CN-D8QvsVeg.js} +6 -5
- package/dist/assets/{zh-TW-CzEUk38j.js → zh-TW-B9t7rQQO.js} +6 -5
- package/dist/index.html +22 -22
- package/dist/mini-chat.html +7 -7
- package/dist/mobile.html +5 -5
- package/package.json +3 -3
- package/server/index.js +118 -85
- package/server/lib/agent-memory/actions.js +2 -2
- package/server/lib/agent-memory/feature-flag.js +1 -1
- package/server/lib/agent-memory/feature-flag.test.js +8 -8
- package/server/lib/agent-memory/routes.js +2 -2
- package/server/lib/agent-tool/runtime.js +37 -54
- package/server/lib/agent-tool/runtime.test.js +59 -59
- package/server/lib/browser-control/broker.js +3 -3
- package/server/lib/browser-control/broker.test.js +1 -1
- package/server/lib/changelog/update-notes.js +82 -0
- package/server/lib/changelog/update-notes.test.js +59 -0
- package/server/lib/client-auth/remote-clients.test.js +5 -5
- package/server/lib/context-obligatory/runtime.js +7 -7
- package/server/lib/context-obligatory/runtime.test.js +5 -5
- package/server/lib/dev-servers/parse.js +2 -2
- package/server/lib/dev-servers/parse.test.js +2 -2
- package/server/lib/dev-servers/routes.js +1 -1
- package/server/lib/dev-tunnel/DOCUMENTATION.md +13 -4
- package/server/lib/dev-tunnel/client.js +1 -1
- package/server/lib/dev-tunnel/runtime.js +5 -5
- package/server/lib/dev-tunnel/tunnel.test.js +21 -1
- package/server/lib/dictation/local/model-catalog.js +1 -1
- package/server/lib/dictation/local/worker-process.js +2 -2
- package/server/lib/event-stream/directory-ws-bridge.js +1 -1
- package/server/lib/event-stream/global-ws-bridge.js +1 -1
- package/server/lib/event-stream/protocol.test.js +4 -4
- package/server/lib/event-stream/runtime.test.js +6 -6
- package/server/lib/fs/DOCUMENTATION.md +2 -0
- package/server/lib/fs/routes.js +37 -26
- package/server/lib/fs/routes.test.js +98 -21
- package/server/lib/git/DOCUMENTATION.md +3 -1
- package/server/lib/git/identity-storage.js +1 -1
- package/server/lib/git/issue-2746-longpaths.test.js +4 -4
- package/server/lib/git/routes.js +17 -1
- package/server/lib/git/service.js +116 -26
- package/server/lib/git/service.test.js +174 -3
- package/server/lib/github/auth.js +8 -8
- package/server/lib/github/pr-status.js +1 -1
- package/server/lib/github/routes.js +2 -2
- package/server/lib/inherited-env.js +1 -1
- package/server/lib/inherited-env.test.js +2 -2
- package/server/lib/linear/auth.js +8 -8
- package/server/lib/linear/auth.test.js +24 -24
- package/server/lib/linear/issues.test.js +6 -6
- package/server/lib/linear/mapping.test.js +4 -4
- package/server/lib/linear/oauth.js +2 -2
- package/server/lib/linear/oauth.test.js +14 -14
- package/server/lib/linear/routes.js +5 -5
- package/server/lib/linear/routes.test.js +16 -16
- package/server/lib/linear/status-runtime.test.js +9 -9
- package/server/lib/linear/status.js +1 -1
- package/server/lib/linear/status.test.js +15 -15
- package/server/lib/linear/teams.test.js +4 -4
- package/server/lib/magic-prompts/routes.js +2 -2
- package/server/lib/mcp-reconnect/DOCUMENTATION.md +55 -0
- package/server/lib/mcp-reconnect/runtime.js +122 -0
- package/server/lib/mcp-reconnect/runtime.test.js +140 -0
- package/server/lib/message-queue/DOCUMENTATION.md +158 -0
- package/server/lib/message-queue/runtime.js +870 -0
- package/server/lib/message-queue/runtime.test.js +482 -0
- package/server/lib/notifications/apns-runtime.js +17 -17
- package/server/lib/notifications/apns-runtime.test.js +11 -11
- package/server/lib/notifications/emitter-runtime.js +1 -1
- package/server/lib/notifications/emitter-runtime.test.js +1 -1
- package/server/lib/notifications/push-runtime.js +7 -7
- package/server/lib/notifications/routes.js +1 -1
- package/server/lib/notifications/runtime.js +1 -1
- package/server/lib/openchamber-control/action-resolution.test.js +10 -10
- package/server/lib/openchamber-control/actions.js +24 -24
- package/server/lib/openchamber-control/error.js +4 -4
- package/server/lib/openchamber-control/routes.js +2 -2
- package/server/lib/openchamber-control/routes.test.js +5 -5
- package/server/lib/openchamber-control/screenshots.js +1 -1
- package/server/lib/openchamber-control/screenshots.test.js +1 -1
- package/server/lib/openchamber-control/service.js +50 -50
- package/server/lib/openchamber-control/service.test.js +5 -5
- package/server/lib/openchamber-sessions/routes.js +124 -22
- package/server/lib/openchamber-sessions/routes.test.js +99 -11
- package/server/lib/opencode/DOCUMENTATION.md +13 -5
- package/server/lib/opencode/auth.js +1 -1
- package/server/lib/opencode/bootstrap-runtime.js +6 -6
- package/server/lib/opencode/cli-options.js +8 -8
- package/server/lib/opencode/config-paths.test.js +116 -0
- package/server/lib/opencode/core-routes.js +8 -7
- package/server/lib/opencode/core-routes.test.js +11 -11
- package/server/lib/opencode/env-config.js +5 -5
- package/server/lib/opencode/env-config.test.js +7 -7
- package/server/lib/opencode/env-runtime.js +10 -10
- package/server/lib/opencode/env-runtime.test.js +18 -18
- package/server/lib/opencode/feature-routes-runtime.js +17 -12
- package/server/lib/opencode/hmr-state-runtime.js +2 -2
- package/server/lib/opencode/hmr-state-runtime.test.js +1 -1
- package/server/lib/opencode/input-history-scope.js +14 -0
- package/server/lib/opencode/lifecycle.js +8 -8
- package/server/lib/opencode/lifecycle.test.js +4 -4
- package/server/lib/opencode/managed-plugin-config.js +35 -0
- package/server/lib/opencode/managed-process-registry.js +3 -3
- package/server/lib/opencode/mcp-oauth-callback.test.js +2 -2
- package/server/lib/opencode/openchamber-routes.js +15 -15
- package/server/lib/opencode/openchamber-routes.test.js +11 -11
- package/server/lib/opencode/plugins.js +2 -2
- package/server/lib/opencode/plugins.test.js +1 -1
- package/server/lib/opencode/project-icon-routes.js +2 -2
- package/server/lib/opencode/project-icon-routes.test.js +1 -1
- package/server/lib/opencode/providers.js +44 -2
- package/server/lib/opencode/providers.test.js +131 -4
- package/server/lib/opencode/proxy.js +2 -2
- package/server/lib/opencode/pwa-manifest-routes.js +1 -1
- package/server/lib/opencode/restart-session-recovery.test.js +1 -1
- package/server/lib/opencode/routes-upgrade.test.js +4 -4
- package/server/lib/opencode/routes.js +14 -14
- package/server/lib/opencode/server-startup-runtime.js +6 -6
- package/server/lib/opencode/session-runtime.js +3 -3
- package/server/lib/opencode/session-runtime.test.js +6 -6
- package/server/lib/opencode/settings-helpers.js +31 -3
- package/server/lib/opencode/settings-helpers.test.js +189 -12
- package/server/lib/opencode/settings-runtime.js +2 -2
- package/server/lib/opencode/settings-runtime.test.js +29 -0
- package/server/lib/opencode/shared.js +5 -2
- package/server/lib/opencode/shared.test.js +7 -7
- package/server/lib/opencode/shutdown-runtime.js +2 -0
- package/server/lib/opencode/skill-routes.js +2 -1
- package/server/lib/opencode/snippets.js +1 -2
- package/server/lib/opencode/startup-performance.js +1 -1
- package/server/lib/opencode/startup-performance.test.js +6 -6
- package/server/lib/opencode/static-routes-runtime.js +8 -8
- package/server/lib/opencode/static-routes-runtime.test.js +7 -7
- package/server/lib/opencode/upgrade-capability.js +2 -2
- package/server/lib/opencode/upgrade-capability.test.js +3 -3
- package/server/lib/package-manager.js +17 -42
- package/server/lib/package-manager.test.js +19 -19
- package/server/lib/permission-auto-accept/runtime.js +1 -1
- package/server/lib/project-context/routes.js +1 -1
- package/server/lib/projects/project-config.js +2 -2
- package/server/lib/quota/DOCUMENTATION.md +9 -1
- package/server/lib/quota/credentials/providers.js +4 -0
- package/server/lib/quota/credentials/store.js +4 -4
- package/server/lib/quota/credentials/store.test.js +8 -8
- package/server/lib/quota/index.js +1 -0
- package/server/lib/quota/providers/exe-dev.js +75 -0
- package/server/lib/quota/providers/exe-dev.test.js +47 -0
- package/server/lib/quota/providers/hyper.js +118 -0
- package/server/lib/quota/providers/hyper.test.js +126 -0
- package/server/lib/quota/providers/index.js +15 -0
- package/server/lib/quota/providers/ollama-cloud.js +1 -1
- package/server/lib/quota/providers/opencode-go.js +2 -1
- package/server/lib/quota/providers/opencode-go.test.js +9 -5
- package/server/lib/quota/providers/xai.js +1 -1
- package/server/lib/quota/routes.js +2 -0
- package/server/lib/quota/utils/auth.js +1 -1
- package/server/lib/relay/DOCUMENTATION.md +1 -1
- package/server/lib/relay/host-client.js +1 -1
- package/server/lib/relay/host-client.test.js +9 -1
- package/server/lib/relay/host-lock.js +1 -1
- package/server/lib/relay/service.js +5 -5
- package/server/lib/relay/tunnel-host.js +3 -1
- package/server/lib/relay/tunnel-host.test.js +9 -1
- package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +2 -2
- package/server/lib/scheduled-tasks/routes.js +5 -5
- package/server/lib/scheduled-tasks/service.js +25 -25
- package/server/lib/scheduled-tasks/service.test.js +1 -1
- package/server/lib/security/bind-host.js +4 -4
- package/server/lib/security/bind-host.test.js +1 -1
- package/server/lib/security/request-security.js +11 -5
- package/server/lib/security/request-security.test.js +14 -14
- package/server/lib/session-assist/runtime.js +10 -9
- package/server/lib/session-folders/routes.js +2 -2
- package/server/lib/session-folders/routes.test.js +8 -8
- package/server/lib/session-goal/DOCUMENTATION.md +20 -7
- package/server/lib/session-goal/create.js +4 -2
- package/server/lib/session-goal/create.test.js +1 -1
- package/server/lib/session-goal/objectives.js +4 -4
- package/server/lib/session-goal/routes.js +1 -1
- package/server/lib/session-goal/runtime.js +88 -22
- package/server/lib/session-goal/runtime.test.js +428 -2
- package/server/lib/session-knowledge/runtime.js +11 -11
- package/server/lib/session-knowledge/runtime.test.js +5 -5
- package/server/lib/skills-catalog/DOCUMENTATION.md +1 -1
- package/server/lib/skills-catalog/cache.test.js +2 -2
- package/server/lib/skills-catalog/disk-cache.js +5 -5
- package/server/lib/skills-catalog/github-meta.test.js +2 -2
- package/server/lib/skills-catalog/install.js +3 -2
- package/server/lib/small-model/DOCUMENTATION.md +4 -0
- package/server/lib/small-model/call.js +9 -3
- package/server/lib/small-model/call.test.js +29 -6
- package/server/lib/small-model/index.js +8 -7
- package/server/lib/small-model/index.test.js +1 -1
- package/server/lib/small-model/resolve.js +1 -1
- package/server/lib/small-model/resolve.test.js +1 -1
- package/server/lib/small-model/routes.js +2 -1
- package/server/lib/system-prompt/runtime.js +3 -22
- package/server/lib/system-prompt/runtime.test.js +3 -3
- package/server/lib/terminal/DOCUMENTATION.md +11 -7
- package/server/lib/terminal/runtime.js +180 -26
- package/server/lib/terminal/runtime.test.js +554 -27
- package/server/lib/terminal/shells.js +20 -3
- package/server/lib/terminal/shells.test.js +33 -2
- package/server/lib/tts/base-url.js +3 -3
- package/server/lib/tts/routes.test.js +20 -20
- package/server/lib/ui-auth/ui-auth.js +12 -9
- package/server/lib/ui-auth/ui-auth.test.js +19 -3
- package/server/lib/ui-auth/ui-passkeys.js +7 -7
- package/server/lib/walkthrough/index.js +1 -0
- package/server/lib/walkthrough/jobs.test.js +1 -1
- package/server/lib/walkthrough/language.test.js +1 -1
- package/server/lib/walkthrough/model-settings.js +3 -3
- package/server/lib/walkthrough/model-settings.test.js +1 -1
- package/server/lib/walkthrough/pull-request.test.js +5 -5
- package/server/lib/walkthrough/reproduce-2607.test.js +2 -2
- package/server/lib/walkthrough/routes.test.js +26 -5
- package/server/lib/walkthrough/store.js +3 -3
- package/server/lib/walkthrough/store.test.js +1 -1
- package/server/opencode-proxy.test.js +2 -2
- package/server/proxy-headers.js +1 -1
- package/server/sse-routes.test.js +4 -4
- package/README.md.bak +0 -250
- package/bin/lib/DOCUMENTATION.md.bak +0 -163
- package/dist/assets/CommitSection-SGBCAW0w.js +0 -1
- package/dist/assets/FilesView-Bdq8ppva.js +0 -17
- package/dist/assets/GitView-Znn7VJcP.js +0 -6
- package/dist/assets/LinearIssuesView-jP-hXyac.js +0 -1
- package/dist/assets/MarkdownRendererImpl-Dt9mjTRN.js +0 -12
- package/dist/assets/PlanView-B0F3BtPZ.js +0 -5
- package/dist/assets/SettingsView-BBW-S-gh.js +0 -28
- package/dist/assets/ToolPartDiffPreview-CUNe28U1.js +0 -8
- package/dist/assets/index-C57jT0Sa.css +0 -1
- package/dist/assets/index-DW_E3rnL.js +0 -7
- package/dist/assets/main-Bv5Pkngh.js +0 -928
- package/dist/assets/pl-Dioz29BI.js +0 -15
- package/dist/assets/renderMobileApp-D9yQJy0L.js +0 -25
- package/dist/assets/runtimeEndpointReset-CjhvxLH0.js +0 -16
- package/dist/assets/tr-DFnMlPxY.js +0 -6
- package/dist/assets/useInlineCommentController-CDEuSlPH.js +0 -2
- package/dist/assets/vendor-zod-BLfBSgBh.js +0 -39
- package/server/index.d.ts.bak +0 -39
- package/server/lib/agent-tool/DOCUMENTATION.md.bak +0 -119
- package/server/lib/browser-control/DOCUMENTATION.md.bak +0 -50
- package/server/lib/context-obligatory/DOCUMENTATION.md.bak +0 -19
- package/server/lib/dev-tunnel/DOCUMENTATION.md.bak +0 -57
- package/server/lib/dictation/DOCUMENTATION.md.bak +0 -100
- package/server/lib/event-stream/DOCUMENTATION.md.bak +0 -62
- package/server/lib/fs/DOCUMENTATION.md.bak +0 -47
- package/server/lib/git/DOCUMENTATION.md.bak +0 -193
- package/server/lib/github/DOCUMENTATION.md.bak +0 -183
- package/server/lib/linear/DOCUMENTATION.md.bak +0 -97
- package/server/lib/linear/OAUTH-BROKER-HANDOFF.md.bak +0 -91
- package/server/lib/markdown-image-grants/DOCUMENTATION.md.bak +0 -40
- package/server/lib/notifications/APNS.md.bak +0 -135
- package/server/lib/notifications/DOCUMENTATION.md.bak +0 -138
- package/server/lib/openchamber-control/DOCUMENTATION.md.bak +0 -64
- package/server/lib/opencode/DOCUMENTATION.md.bak +0 -431
- package/server/lib/opencode/managed-process-registry.d.ts.bak +0 -13
- package/server/lib/opencode/provider-env-aliases.d.ts.bak +0 -3
- package/server/lib/permission-auto-accept/DOCUMENTATION.md.bak +0 -34
- package/server/lib/project-context/DOCUMENTATION.md.bak +0 -160
- package/server/lib/quota/DOCUMENTATION.md.bak +0 -123
- package/server/lib/relay/DOCUMENTATION.md.bak +0 -100
- package/server/lib/scheduled-tasks/DOCUMENTATION.md.bak +0 -182
- package/server/lib/session-assist/DOCUMENTATION.md.bak +0 -66
- package/server/lib/session-goal/DOCUMENTATION.md.bak +0 -213
- package/server/lib/session-knowledge/DOCUMENTATION.md.bak +0 -89
- package/server/lib/skills-catalog/DOCUMENTATION.md.bak +0 -151
- package/server/lib/small-model/DOCUMENTATION.md.bak +0 -212
- package/server/lib/system-prompt/DOCUMENTATION.md.bak +0 -36
- package/server/lib/terminal/DOCUMENTATION.md.bak +0 -50
- package/server/lib/text/DOCUMENTATION.md.bak +0 -35
- package/server/lib/tts/DOCUMENTATION.md.bak +0 -147
- package/server/lib/tunnels/DOCUMENTATION.md.bak +0 -21
- package/server/lib/ui-auth/DOCUMENTATION.md.bak +0 -44
- package/server/lib/walkthrough/DOCUMENTATION.md.bak +0 -411
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Session goal: a persisted, self-continuing objective attached to a session
|
|
2
|
-
// (metadata.
|
|
2
|
+
// (metadata.aiworker.goal). While the goal is active, the server keeps the
|
|
3
3
|
// session working toward it: after each busy→idle transition it accounts token
|
|
4
4
|
// usage, asks the small model to audit progress (continue / complete /
|
|
5
5
|
// blocked), and either re-prompts the session's own model with a continuation
|
|
@@ -20,16 +20,16 @@ import path from 'path';
|
|
|
20
20
|
|
|
21
21
|
import { GOAL_OBJECTIVE_CHAR_LIMIT, readObjective } from './objectives.js';
|
|
22
22
|
|
|
23
|
-
const
|
|
24
|
-
process.env.
|
|
25
|
-
? path.resolve(process.env.
|
|
26
|
-
: path.join(os.homedir(), '.config', '
|
|
23
|
+
const AIWORKER_SETTINGS_FILE = path.join(
|
|
24
|
+
process.env.AIWORKER_DATA_DIR
|
|
25
|
+
? path.resolve(process.env.AIWORKER_DATA_DIR)
|
|
26
|
+
: path.join(os.homedir(), '.config', 'aiworker'),
|
|
27
27
|
'settings.json',
|
|
28
28
|
);
|
|
29
29
|
|
|
30
30
|
const isSessionGoalEnabled = () => {
|
|
31
31
|
try {
|
|
32
|
-
const raw = fs.readFileSync(
|
|
32
|
+
const raw = fs.readFileSync(AIWORKER_SETTINGS_FILE, 'utf8');
|
|
33
33
|
const settings = JSON.parse(raw);
|
|
34
34
|
return settings?.sessionGoalEnabled !== false;
|
|
35
35
|
} catch {
|
|
@@ -188,7 +188,7 @@ const extractSessionUpdate = (payload) => {
|
|
|
188
188
|
const parseGoalMetadata = (session) => {
|
|
189
189
|
const metadata = session?.metadata;
|
|
190
190
|
if (!metadata || typeof metadata !== 'object') return null;
|
|
191
|
-
const namespace = metadata.
|
|
191
|
+
const namespace = metadata.aiworker;
|
|
192
192
|
if (!namespace || typeof namespace !== 'object') return null;
|
|
193
193
|
const goal = namespace.goal;
|
|
194
194
|
if (!goal || typeof goal !== 'object') return null;
|
|
@@ -244,6 +244,57 @@ const messageTokenTotal = (info) => {
|
|
|
244
244
|
return input + cachedRead + output;
|
|
245
245
|
};
|
|
246
246
|
|
|
247
|
+
const getErrorName = (error) => error?.name?.trim?.() ?? '';
|
|
248
|
+
|
|
249
|
+
const isLengthTruncated = (info, errorName = getErrorName(info?.error)) => {
|
|
250
|
+
const error = info?.error;
|
|
251
|
+
const hasError = error !== null && error !== undefined;
|
|
252
|
+
return errorName === 'MessageOutputLengthError' || (!hasError && info?.finish === 'length');
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
// Summary messages are assistant-shaped, but they are compaction turns rather
|
|
256
|
+
// than agent turns. They must not break or satisfy the consecutive truncation
|
|
257
|
+
// check; only completed, non-summary assistant turns participate. Chronology
|
|
258
|
+
// comes from `time.created`, never from message IDs; array position is only a
|
|
259
|
+
// tie-breaker for equal timestamps.
|
|
260
|
+
const hasRepeatedLengthTail = (messages, latestAssistant, goalCreatedAt) => {
|
|
261
|
+
const latestInfo = latestAssistant?.info;
|
|
262
|
+
if (latestInfo?.summary === true) return false;
|
|
263
|
+
const latestIndex = messages.indexOf(latestAssistant);
|
|
264
|
+
const latestCreated = latestInfo?.time?.created;
|
|
265
|
+
if (
|
|
266
|
+
latestIndex < 0
|
|
267
|
+
|| !(latestInfo?.time?.completed > 0)
|
|
268
|
+
|| !(Number.isFinite(latestCreated) && latestCreated > 0)
|
|
269
|
+
|| !isLengthTruncated(latestInfo)
|
|
270
|
+
) return false;
|
|
271
|
+
|
|
272
|
+
let previous = null;
|
|
273
|
+
for (let i = 0; i < messages.length; i += 1) {
|
|
274
|
+
const info = messages[i]?.info;
|
|
275
|
+
if (info?.role !== 'assistant' || info.summary === true || !(info.time?.completed > 0)) continue;
|
|
276
|
+
const created = info.time?.created;
|
|
277
|
+
// An unknown timestamp cannot safely participate in chronology. Ignore it
|
|
278
|
+
// rather than letting an unrelated older message hide known chronology.
|
|
279
|
+
if (!(Number.isFinite(created) && created > 0)) continue;
|
|
280
|
+
if (i === latestIndex) continue;
|
|
281
|
+
if (created > latestCreated || (created === latestCreated && i > latestIndex)) continue;
|
|
282
|
+
if (
|
|
283
|
+
!previous
|
|
284
|
+
|| created > previous.created
|
|
285
|
+
|| (created === previous.created && i > previous.index)
|
|
286
|
+
) {
|
|
287
|
+
previous = { info, created, index: i };
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return Boolean(
|
|
292
|
+
previous
|
|
293
|
+
&& previous.created > goalCreatedAt
|
|
294
|
+
&& isLengthTruncated(previous.info),
|
|
295
|
+
);
|
|
296
|
+
};
|
|
297
|
+
|
|
247
298
|
export const createSessionGoalRuntime = ({
|
|
248
299
|
buildOpenCodeUrl,
|
|
249
300
|
getOpenCodeAuthHeaders,
|
|
@@ -319,8 +370,8 @@ export const createSessionGoalRuntime = ({
|
|
|
319
370
|
if (!currentGoal || currentGoal.id !== expectedGoalId) return null;
|
|
320
371
|
const nextGoal = { ...currentGoal, ...mutate(currentGoal), updatedAt: Date.now() };
|
|
321
372
|
const currentMetadata = session?.metadata && typeof session.metadata === 'object' ? session.metadata : {};
|
|
322
|
-
const currentNamespace = currentMetadata.
|
|
323
|
-
? currentMetadata.
|
|
373
|
+
const currentNamespace = currentMetadata.aiworker && typeof currentMetadata.aiworker === 'object'
|
|
374
|
+
? currentMetadata.aiworker
|
|
324
375
|
: {};
|
|
325
376
|
await openCodeFetch(`/session/${encodeURIComponent(sessionId)}`, {
|
|
326
377
|
directory,
|
|
@@ -328,7 +379,7 @@ export const createSessionGoalRuntime = ({
|
|
|
328
379
|
body: {
|
|
329
380
|
metadata: {
|
|
330
381
|
...currentMetadata,
|
|
331
|
-
|
|
382
|
+
aiworker: { ...currentNamespace, goal: nextGoal },
|
|
332
383
|
},
|
|
333
384
|
},
|
|
334
385
|
});
|
|
@@ -378,6 +429,7 @@ export const createSessionGoalRuntime = ({
|
|
|
378
429
|
prompt: `The goal objective:\n\n<objective>\n${goal.objective}\n</objective>\n\nThe agent's latest turn:\n\n${assistantText}\n\nReturn the verdict JSON. Write the note in the SAME language as this sample from the objective: "${goal.objective.slice(0, 200).replace(/\s+/g, ' ').trim()}"`,
|
|
379
430
|
system: buildAuditSystemPrompt(),
|
|
380
431
|
directory,
|
|
432
|
+
sessionID: typeof lastAssistantInfo?.sessionID === 'string' ? lastAssistantInfo.sessionID : undefined,
|
|
381
433
|
preferredProviderID: typeof lastAssistantInfo?.providerID === 'string' ? lastAssistantInfo.providerID : undefined,
|
|
382
434
|
preferredModelID: typeof lastAssistantInfo?.modelID === 'string' ? lastAssistantInfo.modelID : undefined,
|
|
383
435
|
});
|
|
@@ -460,7 +512,7 @@ export const createSessionGoalRuntime = ({
|
|
|
460
512
|
if (!goal || goal.status !== 'active') return;
|
|
461
513
|
|
|
462
514
|
// File-backed objectives: the metadata carries only a flag; the objective
|
|
463
|
-
// TEXT lives under the
|
|
515
|
+
// TEXT lives under the AiWorker data dir keyed by session id and is
|
|
464
516
|
// read fresh on every tick (live-editable). A missing file falls back to
|
|
465
517
|
// whatever inline objective the metadata still has — the goal must never
|
|
466
518
|
// die just because a file went away.
|
|
@@ -608,7 +660,11 @@ export const createSessionGoalRuntime = ({
|
|
|
608
660
|
// resumed over an aborted tail: that is an explicit "keep going", so it
|
|
609
661
|
// falls through to the continuation below (skipping the audit — an
|
|
610
662
|
// aborted reply is not evidence of anything).
|
|
611
|
-
const
|
|
663
|
+
const error = lastAssistantInfo.error;
|
|
664
|
+
const errorName = getErrorName(error);
|
|
665
|
+
const hasError = error !== null && error !== undefined;
|
|
666
|
+
const abortedTail = errorName === 'MessageAbortedError';
|
|
667
|
+
const lengthTail = isLengthTruncated(lastAssistantInfo, errorName);
|
|
612
668
|
if (abortedTail && goal.statusReason !== 'resumed') {
|
|
613
669
|
await writeGoal(sessionId, directory, goal.id, () => ({
|
|
614
670
|
status: 'paused',
|
|
@@ -622,13 +678,12 @@ export const createSessionGoalRuntime = ({
|
|
|
622
678
|
return;
|
|
623
679
|
}
|
|
624
680
|
|
|
625
|
-
//
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
: 'assistant turn failed';
|
|
681
|
+
// Non-length turn error → blocked (prevents runaway auto-continuation into
|
|
682
|
+
// failures). Recognized length cutoffs are in-progress continuations, not
|
|
683
|
+
// hard failures.
|
|
684
|
+
if (!abortedTail && !lengthTail && hasError) {
|
|
630
685
|
await settleGoal({
|
|
631
|
-
sessionId, directory, goal, status: 'blocked', statusReason:
|
|
686
|
+
sessionId, directory, goal, status: 'blocked', statusReason: errorName || 'assistant turn failed', tokensUsed, tokensBaseline, tokensCommitted, lastAccountedMessageID,
|
|
632
687
|
});
|
|
633
688
|
return;
|
|
634
689
|
}
|
|
@@ -649,17 +704,28 @@ export const createSessionGoalRuntime = ({
|
|
|
649
704
|
return;
|
|
650
705
|
}
|
|
651
706
|
|
|
707
|
+
// A second consecutive completed, non-summary length-truncated turn is a
|
|
708
|
+
// bounded recovery failure. Derive this from the loaded transcript rather
|
|
709
|
+
// than persisting another goal counter.
|
|
710
|
+
if (lengthTail && goal.statusReason !== 'resumed' && hasRepeatedLengthTail(messages, lastAssistant, goal.createdAt)) {
|
|
711
|
+
await settleGoal({
|
|
712
|
+
sessionId, directory, goal, status: 'blocked', statusReason: 'repeated output truncation', tokensUsed, tokensBaseline, tokensCommitted, lastAccountedMessageID,
|
|
713
|
+
});
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
|
|
652
717
|
// --- Small-model audit: the sole termination authority besides the hard
|
|
653
718
|
// stops above (turn error, budget, continuation cap). The working agent
|
|
654
719
|
// has no channel to settle its own goal.
|
|
655
720
|
//
|
|
656
|
-
// Exception: when the latest message is a compaction summary
|
|
657
|
-
// by
|
|
658
|
-
// "in progress, not finished".
|
|
721
|
+
// Exception: when the latest message is a compaction summary or was cut off
|
|
722
|
+
// by the output token limit (length stop), the agent by definition ran into
|
|
723
|
+
// the context/output limit mid-work — that IS "in progress, not finished".
|
|
724
|
+
// No audit call; continue unconditionally.
|
|
659
725
|
let audit = null;
|
|
660
726
|
let blockedStreak = 0;
|
|
661
727
|
let auditFailStreak = goal.auditFailStreak;
|
|
662
|
-
if (lastAssistantInfo.summary === true || abortedTail) {
|
|
728
|
+
if (lastAssistantInfo.summary === true || abortedTail || lengthTail) {
|
|
663
729
|
blockedStreak = goal.blockedStreak;
|
|
664
730
|
} else {
|
|
665
731
|
audit = await runAudit({ goal: { ...goal, objective: effectiveObjective }, assistantText, directory, lastAssistantInfo: executionInfo ?? lastAssistantInfo });
|
|
@@ -18,7 +18,7 @@ const goal = {
|
|
|
18
18
|
const session = {
|
|
19
19
|
id: SESSION_ID,
|
|
20
20
|
directory: DIRECTORY,
|
|
21
|
-
metadata: {
|
|
21
|
+
metadata: { aiworker: { goal } },
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
const jsonResponse = (body, status = 200) => new Response(JSON.stringify(body), {
|
|
@@ -46,6 +46,78 @@ const startIdleTick = async (fetchImpl) => {
|
|
|
46
46
|
return { runtime, getSmallModelService };
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
+
const assistantMessage = (id, infoOverrides = {}) => ({
|
|
50
|
+
info: {
|
|
51
|
+
id,
|
|
52
|
+
sessionID: SESSION_ID,
|
|
53
|
+
role: 'assistant',
|
|
54
|
+
providerID: 'provider',
|
|
55
|
+
modelID: 'model',
|
|
56
|
+
time: { created: 2, completed: 2 },
|
|
57
|
+
tokens: { input: 1, output: 1, cache: { read: 0 } },
|
|
58
|
+
...infoOverrides,
|
|
59
|
+
},
|
|
60
|
+
parts: [{ type: 'text', text: 'The agent made progress.' }],
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const createRuntimeHarness = ({ messages, messageFactory, goalOverrides = {}, maxAutoTurns = 20 }) => {
|
|
64
|
+
const requests = [];
|
|
65
|
+
let messageFetchCount = 0;
|
|
66
|
+
const activeSession = {
|
|
67
|
+
...session,
|
|
68
|
+
metadata: { aiworker: { goal: { ...goal, ...goalOverrides } } },
|
|
69
|
+
};
|
|
70
|
+
const service = {
|
|
71
|
+
generateSmallModelText: vi.fn(async () => ({
|
|
72
|
+
text: '{"verdict":"continue","note":"More work remains"}',
|
|
73
|
+
providerID: 'provider',
|
|
74
|
+
modelID: 'model',
|
|
75
|
+
})),
|
|
76
|
+
};
|
|
77
|
+
const fetchImpl = vi.fn(async (input, init = {}) => {
|
|
78
|
+
const pathname = requestPath(input);
|
|
79
|
+
requests.push({ pathname, method: init.method ?? 'GET', body: init.body });
|
|
80
|
+
if (pathname === `/session/${SESSION_ID}` && init.method === 'PATCH') {
|
|
81
|
+
activeSession.metadata = JSON.parse(init.body).metadata;
|
|
82
|
+
return jsonResponse(activeSession);
|
|
83
|
+
}
|
|
84
|
+
if (pathname === `/session/${SESSION_ID}`) return jsonResponse(activeSession);
|
|
85
|
+
if (pathname === '/session/status') return jsonResponse({});
|
|
86
|
+
if (pathname === `/session/${SESSION_ID}/children`) return jsonResponse([]);
|
|
87
|
+
if (pathname === `/session/${SESSION_ID}/message`) {
|
|
88
|
+
const nextMessages = messageFactory ? messageFactory(messageFetchCount) : messages;
|
|
89
|
+
messageFetchCount += 1;
|
|
90
|
+
return jsonResponse(nextMessages);
|
|
91
|
+
}
|
|
92
|
+
if (pathname === `/session/${SESSION_ID}/prompt_async`) return jsonResponse({ ok: true });
|
|
93
|
+
throw new Error(`Unexpected request: ${pathname}`);
|
|
94
|
+
});
|
|
95
|
+
vi.stubGlobal('fetch', fetchImpl);
|
|
96
|
+
const runtime = createSessionGoalRuntime({
|
|
97
|
+
buildOpenCodeUrl: (pathname) => `http://opencode.test${pathname}`,
|
|
98
|
+
getOpenCodeAuthHeaders: () => ({}),
|
|
99
|
+
getSmallModelService: async () => service,
|
|
100
|
+
isEnabled: () => true,
|
|
101
|
+
idleQuietMs: 10,
|
|
102
|
+
maxAutoTurns,
|
|
103
|
+
});
|
|
104
|
+
return { runtime, requests, service, activeSession };
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const runIdleTick = async (runtime) => {
|
|
108
|
+
runtime.processPayload({
|
|
109
|
+
type: 'session.status',
|
|
110
|
+
properties: { sessionID: SESSION_ID, status: { type: 'idle' }, directory: DIRECTORY },
|
|
111
|
+
});
|
|
112
|
+
await vi.runOnlyPendingTimersAsync();
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const lastPatchedGoal = (requests) => {
|
|
116
|
+
const patches = requests.filter((request) => request.pathname === `/session/${SESSION_ID}` && request.method === 'PATCH');
|
|
117
|
+
expect(patches.length).toBeGreaterThan(0);
|
|
118
|
+
return JSON.parse(patches.at(-1).body).metadata.aiworker.goal;
|
|
119
|
+
};
|
|
120
|
+
|
|
49
121
|
describe('session goal live activity gate', () => {
|
|
50
122
|
beforeEach(() => {
|
|
51
123
|
vi.useFakeTimers();
|
|
@@ -170,7 +242,7 @@ describe('session goal live activity gate', () => {
|
|
|
170
242
|
expect(service.generateSmallModelText).toHaveBeenCalledOnce();
|
|
171
243
|
const patch = requests.find((request) => request.pathname === `/session/${SESSION_ID}` && request.method === 'PATCH');
|
|
172
244
|
expect(patch).toBeDefined();
|
|
173
|
-
const writtenGoal = JSON.parse(patch.body).metadata.
|
|
245
|
+
const writtenGoal = JSON.parse(patch.body).metadata.aiworker.goal;
|
|
174
246
|
expect(writtenGoal).toMatchObject({
|
|
175
247
|
status: 'complete',
|
|
176
248
|
evaluationProviderID: 'provider',
|
|
@@ -178,4 +250,358 @@ describe('session goal live activity gate', () => {
|
|
|
178
250
|
});
|
|
179
251
|
runtime.stop();
|
|
180
252
|
});
|
|
253
|
+
|
|
254
|
+
it('skips audit and sends continuation prompt when assistant message finishes with length stop', async () => {
|
|
255
|
+
const requests = [];
|
|
256
|
+
const fetchImpl = vi.fn(async (input, init = {}) => {
|
|
257
|
+
const pathname = requestPath(input);
|
|
258
|
+
requests.push({ pathname, method: init.method ?? 'GET', body: init.body });
|
|
259
|
+
if (pathname === `/session/${SESSION_ID}` && init.method === 'PATCH') return jsonResponse(session);
|
|
260
|
+
if (pathname === `/session/${SESSION_ID}`) return jsonResponse(session);
|
|
261
|
+
if (pathname === '/session/status') return jsonResponse({});
|
|
262
|
+
if (pathname === `/session/${SESSION_ID}/children`) return jsonResponse([]);
|
|
263
|
+
if (pathname === `/session/${SESSION_ID}/prompt_async`) return jsonResponse({ ok: true });
|
|
264
|
+
if (pathname === `/session/${SESSION_ID}/message`) {
|
|
265
|
+
return jsonResponse([{
|
|
266
|
+
info: {
|
|
267
|
+
id: 'msg_assistant_len',
|
|
268
|
+
sessionID: SESSION_ID,
|
|
269
|
+
role: 'assistant',
|
|
270
|
+
providerID: 'provider',
|
|
271
|
+
modelID: 'model',
|
|
272
|
+
finish: 'length',
|
|
273
|
+
time: { completed: 2 },
|
|
274
|
+
tokens: { input: 100, output: 4096, reasoning: 4096, cache: { read: 0 } },
|
|
275
|
+
},
|
|
276
|
+
parts: [{ type: 'reasoning', text: 'Drafting extensive implementation...' }],
|
|
277
|
+
}]);
|
|
278
|
+
}
|
|
279
|
+
throw new Error(`Unexpected request: ${pathname}`);
|
|
280
|
+
});
|
|
281
|
+
const service = {
|
|
282
|
+
generateSmallModelText: vi.fn(),
|
|
283
|
+
};
|
|
284
|
+
vi.stubGlobal('fetch', fetchImpl);
|
|
285
|
+
const runtime = createSessionGoalRuntime({
|
|
286
|
+
buildOpenCodeUrl: (pathname) => `http://opencode.test${pathname}`,
|
|
287
|
+
getOpenCodeAuthHeaders: () => ({}),
|
|
288
|
+
getSmallModelService: async () => service,
|
|
289
|
+
isEnabled: () => true,
|
|
290
|
+
idleQuietMs: 10,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
runtime.processPayload({
|
|
294
|
+
type: 'session.status',
|
|
295
|
+
properties: { sessionID: SESSION_ID, status: { type: 'idle' }, directory: DIRECTORY },
|
|
296
|
+
});
|
|
297
|
+
await vi.runOnlyPendingTimersAsync();
|
|
298
|
+
|
|
299
|
+
// Audit must be skipped on length-truncated turn
|
|
300
|
+
expect(service.generateSmallModelText).not.toHaveBeenCalled();
|
|
301
|
+
|
|
302
|
+
// Goal accounting is persisted and turnsUsed incremented
|
|
303
|
+
const patch = requests.find((request) => request.pathname === `/session/${SESSION_ID}` && request.method === 'PATCH');
|
|
304
|
+
expect(patch).toBeDefined();
|
|
305
|
+
const writtenGoal = JSON.parse(patch.body).metadata.aiworker.goal;
|
|
306
|
+
expect(writtenGoal).toMatchObject({
|
|
307
|
+
status: 'active',
|
|
308
|
+
turnsUsed: 2,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// Continuation prompt is dispatched
|
|
312
|
+
const promptAsync = requests.find((request) => request.pathname === `/session/${SESSION_ID}/prompt_async` && request.method === 'POST');
|
|
313
|
+
expect(promptAsync).toBeDefined();
|
|
314
|
+
const promptBody = JSON.parse(promptAsync.body);
|
|
315
|
+
expect(promptBody.parts[0].text).toContain('Continue working toward the active session goal.');
|
|
316
|
+
runtime.stop();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('skips audit and sends continuation prompt when assistant message carries MessageOutputLengthError', async () => {
|
|
320
|
+
const requests = [];
|
|
321
|
+
const fetchImpl = vi.fn(async (input, init = {}) => {
|
|
322
|
+
const pathname = requestPath(input);
|
|
323
|
+
requests.push({ pathname, method: init.method ?? 'GET', body: init.body });
|
|
324
|
+
if (pathname === `/session/${SESSION_ID}` && init.method === 'PATCH') return jsonResponse(session);
|
|
325
|
+
if (pathname === `/session/${SESSION_ID}`) return jsonResponse(session);
|
|
326
|
+
if (pathname === '/session/status') return jsonResponse({});
|
|
327
|
+
if (pathname === `/session/${SESSION_ID}/children`) return jsonResponse([]);
|
|
328
|
+
if (pathname === `/session/${SESSION_ID}/prompt_async`) return jsonResponse({ ok: true });
|
|
329
|
+
if (pathname === `/session/${SESSION_ID}/message`) {
|
|
330
|
+
return jsonResponse([{
|
|
331
|
+
info: {
|
|
332
|
+
id: 'msg_assistant_len_err',
|
|
333
|
+
sessionID: SESSION_ID,
|
|
334
|
+
role: 'assistant',
|
|
335
|
+
providerID: 'provider',
|
|
336
|
+
modelID: 'model',
|
|
337
|
+
error: { name: 'MessageOutputLengthError', message: 'Maximum token limit reached' },
|
|
338
|
+
time: { completed: 2 },
|
|
339
|
+
tokens: { input: 100, output: 4096, reasoning: 4096, cache: { read: 0 } },
|
|
340
|
+
},
|
|
341
|
+
parts: [{ type: 'reasoning', text: 'Drafting extensive implementation...' }],
|
|
342
|
+
}]);
|
|
343
|
+
}
|
|
344
|
+
throw new Error(`Unexpected request: ${pathname}`);
|
|
345
|
+
});
|
|
346
|
+
const service = {
|
|
347
|
+
generateSmallModelText: vi.fn(),
|
|
348
|
+
};
|
|
349
|
+
vi.stubGlobal('fetch', fetchImpl);
|
|
350
|
+
const runtime = createSessionGoalRuntime({
|
|
351
|
+
buildOpenCodeUrl: (pathname) => `http://opencode.test${pathname}`,
|
|
352
|
+
getOpenCodeAuthHeaders: () => ({}),
|
|
353
|
+
getSmallModelService: async () => service,
|
|
354
|
+
isEnabled: () => true,
|
|
355
|
+
idleQuietMs: 10,
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
runtime.processPayload({
|
|
359
|
+
type: 'session.status',
|
|
360
|
+
properties: { sessionID: SESSION_ID, status: { type: 'idle' }, directory: DIRECTORY },
|
|
361
|
+
});
|
|
362
|
+
await vi.runOnlyPendingTimersAsync();
|
|
363
|
+
|
|
364
|
+
// Audit must be skipped on MessageOutputLengthError
|
|
365
|
+
expect(service.generateSmallModelText).not.toHaveBeenCalled();
|
|
366
|
+
|
|
367
|
+
// Goal remains active and turnsUsed incremented
|
|
368
|
+
const patch = requests.find((request) => request.pathname === `/session/${SESSION_ID}` && request.method === 'PATCH');
|
|
369
|
+
expect(patch).toBeDefined();
|
|
370
|
+
const writtenGoal = JSON.parse(patch.body).metadata.aiworker.goal;
|
|
371
|
+
expect(writtenGoal).toMatchObject({
|
|
372
|
+
status: 'active',
|
|
373
|
+
turnsUsed: 2,
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// Continuation prompt is dispatched
|
|
377
|
+
const promptAsync = requests.find((request) => request.pathname === `/session/${SESSION_ID}/prompt_async` && request.method === 'POST');
|
|
378
|
+
expect(promptAsync).toBeDefined();
|
|
379
|
+
runtime.stop();
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it.each([
|
|
383
|
+
{ name: 'APIError', error: { name: 'APIError', message: 'provider failed' } },
|
|
384
|
+
{ name: 'StructuredOutputError', error: { name: 'StructuredOutputError', message: 'invalid output' } },
|
|
385
|
+
{ name: 'unnamed errors', error: {} },
|
|
386
|
+
])('blocks a length finish when the assistant also has a non-length $name', async ({ error }) => {
|
|
387
|
+
const { runtime, requests, service } = createRuntimeHarness({
|
|
388
|
+
messages: [assistantMessage('msg_assistant_error', { finish: 'length', error })],
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
await runIdleTick(runtime);
|
|
392
|
+
|
|
393
|
+
expect(service.generateSmallModelText).not.toHaveBeenCalled();
|
|
394
|
+
expect(requests.some((request) => request.pathname === `/session/${SESSION_ID}/prompt_async`)).toBe(false);
|
|
395
|
+
expect(lastPatchedGoal(requests)).toMatchObject({
|
|
396
|
+
status: 'blocked',
|
|
397
|
+
statusReason: error.name || 'assistant turn failed',
|
|
398
|
+
});
|
|
399
|
+
runtime.stop();
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('blocks after two consecutive length-truncated agent turns without persisting a counter, regardless of message IDs', async () => {
|
|
403
|
+
const firstLength = assistantMessage('z', { finish: 'length', time: { created: 10, completed: 11 } });
|
|
404
|
+
const summary = assistantMessage('summary', { summary: true, time: { created: 15, completed: 16 } });
|
|
405
|
+
const secondLength = assistantMessage('a', { finish: 'length', time: { created: 20, completed: 21 } });
|
|
406
|
+
const { runtime, requests, service } = createRuntimeHarness({
|
|
407
|
+
messageFactory: (fetchCount) => fetchCount < 2 ? [firstLength] : [firstLength, summary, secondLength],
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
await runIdleTick(runtime);
|
|
411
|
+
runtime.processPayload({
|
|
412
|
+
type: 'session.status',
|
|
413
|
+
properties: { sessionID: SESSION_ID, status: { type: 'idle' }, directory: DIRECTORY },
|
|
414
|
+
});
|
|
415
|
+
await vi.runOnlyPendingTimersAsync();
|
|
416
|
+
|
|
417
|
+
expect(service.generateSmallModelText).not.toHaveBeenCalled();
|
|
418
|
+
expect(requests.filter((request) => request.pathname === `/session/${SESSION_ID}/prompt_async`)).toHaveLength(1);
|
|
419
|
+
expect(lastPatchedGoal(requests)).toMatchObject({
|
|
420
|
+
status: 'blocked',
|
|
421
|
+
statusReason: 'repeated output truncation',
|
|
422
|
+
});
|
|
423
|
+
runtime.stop();
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it('allows one explicit Resume after repeated truncation, then blocks another cutoff', async () => {
|
|
427
|
+
const first = assistantMessage('first', { finish: 'length', time: { created: 10, completed: 11 } });
|
|
428
|
+
const second = assistantMessage('second', { finish: 'length', time: { created: 20, completed: 21 } });
|
|
429
|
+
const messages = [first, second];
|
|
430
|
+
const { runtime, requests, activeSession } = createRuntimeHarness({ messages });
|
|
431
|
+
try {
|
|
432
|
+
await runIdleTick(runtime);
|
|
433
|
+
expect(lastPatchedGoal(requests).status).toBe('blocked');
|
|
434
|
+
expect(requests.filter((request) => request.pathname.endsWith('/prompt_async'))).toHaveLength(0);
|
|
435
|
+
Object.assign(activeSession.metadata.aiworker.goal, { status: 'active', statusReason: 'resumed', turnsUsed: 0 });
|
|
436
|
+
await runIdleTick(runtime);
|
|
437
|
+
expect(lastPatchedGoal(requests)).toMatchObject({ status: 'active', statusReason: '', turnsUsed: 1 });
|
|
438
|
+
expect(requests.filter((request) => request.pathname.endsWith('/prompt_async'))).toHaveLength(1);
|
|
439
|
+
messages.push(assistantMessage('third', { finish: 'length', time: { created: 30, completed: 31 } }));
|
|
440
|
+
await runIdleTick(runtime);
|
|
441
|
+
expect(lastPatchedGoal(requests)).toMatchObject({ status: 'blocked', statusReason: 'repeated output truncation' });
|
|
442
|
+
expect(requests.filter((request) => request.pathname.endsWith('/prompt_async'))).toHaveLength(1);
|
|
443
|
+
} finally {
|
|
444
|
+
runtime.stop();
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it.each([
|
|
449
|
+
{ tokenBudget: 1, error: undefined, expectedStatus: 'budgetLimited' },
|
|
450
|
+
{ tokenBudget: null, error: { name: 'APIError' }, expectedStatus: 'blocked' },
|
|
451
|
+
])('keeps $expectedStatus protection on explicit Resume', async ({ tokenBudget, error, expectedStatus }) => {
|
|
452
|
+
const { runtime, requests } = createRuntimeHarness({
|
|
453
|
+
goalOverrides: { statusReason: 'resumed', tokenBudget },
|
|
454
|
+
messages: [assistantMessage('resumed', { finish: 'length', error })],
|
|
455
|
+
});
|
|
456
|
+
try {
|
|
457
|
+
await runIdleTick(runtime);
|
|
458
|
+
expect(lastPatchedGoal(requests).status).toBe(expectedStatus);
|
|
459
|
+
expect(requests.filter((request) => request.pathname.endsWith('/prompt_async'))).toHaveLength(0);
|
|
460
|
+
} finally {
|
|
461
|
+
runtime.stop();
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it('continues after a truncated agent turn followed by a length-finished summary', async () => {
|
|
466
|
+
const firstLength = assistantMessage('agent-length', { finish: 'length', time: { created: 10, completed: 11 } });
|
|
467
|
+
const summary = assistantMessage('summary', {
|
|
468
|
+
summary: true,
|
|
469
|
+
finish: 'length',
|
|
470
|
+
time: { created: 15, completed: 16 },
|
|
471
|
+
});
|
|
472
|
+
const { runtime, requests, service } = createRuntimeHarness({
|
|
473
|
+
messageFactory: (fetchCount) => fetchCount < 2 ? [firstLength] : [firstLength, summary],
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
await runIdleTick(runtime);
|
|
477
|
+
await runIdleTick(runtime);
|
|
478
|
+
|
|
479
|
+
expect(service.generateSmallModelText).not.toHaveBeenCalled();
|
|
480
|
+
expect(requests.filter((request) => request.pathname === `/session/${SESSION_ID}/prompt_async`)).toHaveLength(2);
|
|
481
|
+
expect(lastPatchedGoal(requests)).toMatchObject({ status: 'active' });
|
|
482
|
+
runtime.stop();
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it('does not infer a repeated streak when a completed assistant timestamp is missing', async () => {
|
|
486
|
+
const firstLength = assistantMessage('z', { finish: 'length', time: { completed: 11 } });
|
|
487
|
+
const secondLength = assistantMessage('a', { finish: 'length', time: { created: 20, completed: 21 } });
|
|
488
|
+
const { runtime, requests, service } = createRuntimeHarness({
|
|
489
|
+
messageFactory: (fetchCount) => fetchCount < 2 ? [firstLength] : [firstLength, secondLength],
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
await runIdleTick(runtime);
|
|
493
|
+
await runIdleTick(runtime);
|
|
494
|
+
|
|
495
|
+
expect(service.generateSmallModelText).not.toHaveBeenCalled();
|
|
496
|
+
expect(requests.filter((request) => request.pathname === `/session/${SESSION_ID}/prompt_async`)).toHaveLength(2);
|
|
497
|
+
expect(lastPatchedGoal(requests)).toMatchObject({ status: 'active' });
|
|
498
|
+
runtime.stop();
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it('ignores a missing timestamp on an older assistant when known post-goal turns establish a streak', async () => {
|
|
502
|
+
const olderLength = assistantMessage('legacy', { finish: 'length', time: { completed: 5 } });
|
|
503
|
+
const firstLength = assistantMessage('z', { finish: 'length', time: { created: 10, completed: 11 } });
|
|
504
|
+
const secondLength = assistantMessage('a', { finish: 'length', time: { created: 20, completed: 21 } });
|
|
505
|
+
const { runtime, requests, service } = createRuntimeHarness({
|
|
506
|
+
goalOverrides: { createdAt: 6 },
|
|
507
|
+
messageFactory: (fetchCount) => fetchCount < 2 ? [firstLength] : [olderLength, firstLength, secondLength],
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
await runIdleTick(runtime);
|
|
511
|
+
await runIdleTick(runtime);
|
|
512
|
+
|
|
513
|
+
expect(service.generateSmallModelText).not.toHaveBeenCalled();
|
|
514
|
+
expect(requests.filter((request) => request.pathname === `/session/${SESSION_ID}/prompt_async`)).toHaveLength(1);
|
|
515
|
+
expect(lastPatchedGoal(requests)).toMatchObject({
|
|
516
|
+
status: 'blocked',
|
|
517
|
+
statusReason: 'repeated output truncation',
|
|
518
|
+
});
|
|
519
|
+
runtime.stop();
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it('does not count a previous truncated turn created at the goal boundary', async () => {
|
|
523
|
+
const firstLength = assistantMessage('before-boundary', { finish: 'length', time: { created: 10, completed: 11 } });
|
|
524
|
+
const secondLength = assistantMessage('after-boundary', { finish: 'length', time: { created: 20, completed: 21 } });
|
|
525
|
+
const { runtime, requests, service } = createRuntimeHarness({
|
|
526
|
+
goalOverrides: { createdAt: 10 },
|
|
527
|
+
messageFactory: (fetchCount) => fetchCount < 2 ? [firstLength] : [firstLength, secondLength],
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
await runIdleTick(runtime);
|
|
531
|
+
await runIdleTick(runtime);
|
|
532
|
+
|
|
533
|
+
expect(service.generateSmallModelText).not.toHaveBeenCalled();
|
|
534
|
+
expect(requests.filter((request) => request.pathname === `/session/${SESSION_ID}/prompt_async`)).toHaveLength(2);
|
|
535
|
+
expect(lastPatchedGoal(requests)).toMatchObject({ status: 'active' });
|
|
536
|
+
runtime.stop();
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
it('allows length recovery after an ordinary assistant turn breaks the streak', async () => {
|
|
540
|
+
const firstLength = assistantMessage('msg_length_before_normal', { finish: 'length' });
|
|
541
|
+
const normal = assistantMessage('msg_normal');
|
|
542
|
+
const secondLength = assistantMessage('msg_length_after_normal', { finish: 'length' });
|
|
543
|
+
const { runtime, requests, service } = createRuntimeHarness({
|
|
544
|
+
messageFactory: (fetchCount) => fetchCount < 2
|
|
545
|
+
? [firstLength]
|
|
546
|
+
: [firstLength, normal, secondLength],
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
await runIdleTick(runtime);
|
|
550
|
+
await runIdleTick(runtime);
|
|
551
|
+
|
|
552
|
+
expect(service.generateSmallModelText).not.toHaveBeenCalled();
|
|
553
|
+
expect(requests.filter((request) => request.pathname === `/session/${SESSION_ID}/prompt_async`)).toHaveLength(2);
|
|
554
|
+
expect(lastPatchedGoal(requests)).toMatchObject({ status: 'active' });
|
|
555
|
+
runtime.stop();
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it('keeps MessageAbortedError pause behavior instead of continuing', async () => {
|
|
559
|
+
const { runtime, requests, service } = createRuntimeHarness({
|
|
560
|
+
messages: [assistantMessage('msg_aborted', { error: { name: 'MessageAbortedError' } })],
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
await runIdleTick(runtime);
|
|
564
|
+
|
|
565
|
+
expect(service.generateSmallModelText).not.toHaveBeenCalled();
|
|
566
|
+
expect(requests.some((request) => request.pathname === `/session/${SESSION_ID}/prompt_async`)).toBe(false);
|
|
567
|
+
expect(lastPatchedGoal(requests)).toMatchObject({
|
|
568
|
+
status: 'paused',
|
|
569
|
+
statusReason: 'paused after abort',
|
|
570
|
+
});
|
|
571
|
+
runtime.stop();
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
it('checks the token budget before allowing length recovery', async () => {
|
|
575
|
+
const { runtime, requests, service } = createRuntimeHarness({
|
|
576
|
+
goalOverrides: { tokenBudget: 5 },
|
|
577
|
+
messages: [assistantMessage('msg_budget_length', {
|
|
578
|
+
finish: 'length',
|
|
579
|
+
tokens: { input: 3, output: 3, cache: { read: 0 } },
|
|
580
|
+
})],
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
await runIdleTick(runtime);
|
|
584
|
+
|
|
585
|
+
expect(service.generateSmallModelText).not.toHaveBeenCalled();
|
|
586
|
+
expect(requests.some((request) => request.pathname === `/session/${SESSION_ID}/prompt_async`)).toBe(false);
|
|
587
|
+
expect(lastPatchedGoal(requests)).toMatchObject({ status: 'budgetLimited' });
|
|
588
|
+
runtime.stop();
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it('checks the auto-continuation cap before allowing length recovery', async () => {
|
|
592
|
+
const { runtime, requests, service } = createRuntimeHarness({
|
|
593
|
+
maxAutoTurns: 1,
|
|
594
|
+
messages: [assistantMessage('msg_cap_length', { finish: 'length' })],
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
await runIdleTick(runtime);
|
|
598
|
+
|
|
599
|
+
expect(service.generateSmallModelText).not.toHaveBeenCalled();
|
|
600
|
+
expect(requests.some((request) => request.pathname === `/session/${SESSION_ID}/prompt_async`)).toBe(false);
|
|
601
|
+
expect(lastPatchedGoal(requests)).toMatchObject({
|
|
602
|
+
status: 'blocked',
|
|
603
|
+
statusReason: 'auto-continuation limit reached',
|
|
604
|
+
});
|
|
605
|
+
runtime.stop();
|
|
606
|
+
});
|
|
181
607
|
});
|