@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
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { createMessageQueueRuntime, parseQueuedItemInput } from './runtime.js';
|
|
6
|
+
|
|
7
|
+
const SESSION = 'ses_queue_test_1';
|
|
8
|
+
const DIRECTORY = '/repo';
|
|
9
|
+
|
|
10
|
+
const item = (overrides = {}) => ({
|
|
11
|
+
content: 'follow up',
|
|
12
|
+
text: 'follow up',
|
|
13
|
+
attachments: [],
|
|
14
|
+
sendConfig: { providerID: 'anthropic', modelID: 'claude', agent: 'build' },
|
|
15
|
+
...overrides,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const tempDirs = [];
|
|
19
|
+
const makeDataDir = () => {
|
|
20
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-message-queue-'));
|
|
21
|
+
tempDirs.push(dir);
|
|
22
|
+
return dir;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
vi.useRealTimers();
|
|
27
|
+
for (const dir of tempDirs.splice(0)) fs.rmSync(dir, { recursive: true, force: true });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A fake OpenCode: status map, message tail, command list, and a log of every
|
|
32
|
+
* prompt/command it received.
|
|
33
|
+
*/
|
|
34
|
+
const createOpenCode = () => {
|
|
35
|
+
const state = {
|
|
36
|
+
statuses: {},
|
|
37
|
+
tail: [],
|
|
38
|
+
commands: [],
|
|
39
|
+
sent: [],
|
|
40
|
+
failNext: null,
|
|
41
|
+
};
|
|
42
|
+
const fetchImpl = vi.fn(async (url, init = {}) => {
|
|
43
|
+
const { pathname } = new URL(url);
|
|
44
|
+
const method = init.method ?? 'GET';
|
|
45
|
+
if (state.failNext && state.failNext.test(pathname)) {
|
|
46
|
+
state.failNext = null;
|
|
47
|
+
return new Response('boom', { status: 500 });
|
|
48
|
+
}
|
|
49
|
+
if (pathname === '/session/status') return Response.json(state.statuses);
|
|
50
|
+
if (pathname.endsWith('/message')) return Response.json(state.tail);
|
|
51
|
+
if (pathname === '/command') return Response.json(state.commands);
|
|
52
|
+
if (method === 'POST' && (pathname.endsWith('/prompt_async') || pathname.endsWith('/command'))) {
|
|
53
|
+
state.sent.push({ path: pathname, body: JSON.parse(init.body) });
|
|
54
|
+
return new Response(null, { status: 204 });
|
|
55
|
+
}
|
|
56
|
+
return new Response('not found', { status: 404 });
|
|
57
|
+
});
|
|
58
|
+
return { state, fetchImpl };
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const createRuntime = ({ dataDir = makeDataDir(), openCode = createOpenCode(), knowledge = null, retryDelayMs } = {}) => {
|
|
62
|
+
let eventHandler = () => {};
|
|
63
|
+
let statusHandler = () => {};
|
|
64
|
+
const broadcasts = [];
|
|
65
|
+
const promptSent = [];
|
|
66
|
+
const options = {
|
|
67
|
+
globalEventHub: {
|
|
68
|
+
subscribeEvent(handler) { eventHandler = handler; return () => {}; },
|
|
69
|
+
subscribeStatus(handler) { statusHandler = handler; return () => {}; },
|
|
70
|
+
},
|
|
71
|
+
buildOpenCodeUrl: (fetchPath) => `http://opencode.test${fetchPath}`,
|
|
72
|
+
getOpenCodeAuthHeaders: () => ({}),
|
|
73
|
+
sessionKnowledgeRuntime: knowledge,
|
|
74
|
+
broadcastGlobalUiEvent: (event) => broadcasts.push(event),
|
|
75
|
+
onPromptSent: (sessionId) => promptSent.push(sessionId),
|
|
76
|
+
dataDir,
|
|
77
|
+
fetchImpl: openCode.fetchImpl,
|
|
78
|
+
dispatchQuietMs: 0,
|
|
79
|
+
abortHoldMs: 50,
|
|
80
|
+
};
|
|
81
|
+
if (retryDelayMs) options.retryDelayMs = retryDelayMs;
|
|
82
|
+
const runtime = createMessageQueueRuntime(options);
|
|
83
|
+
return {
|
|
84
|
+
runtime,
|
|
85
|
+
openCode,
|
|
86
|
+
dataDir,
|
|
87
|
+
broadcasts,
|
|
88
|
+
promptSent,
|
|
89
|
+
emit: (payload, directory = DIRECTORY) => eventHandler({ payload, directory }),
|
|
90
|
+
connect: () => statusHandler({ type: 'connect' }),
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const settle = async (ms = 30) => {
|
|
95
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
describe('parseQueuedItemInput', () => {
|
|
99
|
+
it('rejects an item the server could not deliver later', () => {
|
|
100
|
+
expect(() => parseQueuedItemInput({ content: 'x' })).toThrow(TypeError);
|
|
101
|
+
expect(() => parseQueuedItemInput(item({ content: '', text: '' }))).toThrow(TypeError);
|
|
102
|
+
expect(() => parseQueuedItemInput(item({ attachments: [{ filename: 'a.png' }] }))).toThrow(TypeError);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('keeps delivery fields and trims blank edges of the content', () => {
|
|
106
|
+
const parsed = parseQueuedItemInput(item({ content: '\n\nhello\n', text: 'hello', agentMention: 'reviewer' }));
|
|
107
|
+
expect(parsed).toEqual({
|
|
108
|
+
content: 'hello',
|
|
109
|
+
text: 'hello',
|
|
110
|
+
agentMention: 'reviewer',
|
|
111
|
+
attachments: [],
|
|
112
|
+
context: [],
|
|
113
|
+
sendConfig: { providerID: 'anthropic', modelID: 'claude', agent: 'build' },
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('keeps captured context and rejects a malformed part', () => {
|
|
118
|
+
const context = [
|
|
119
|
+
{ kind: 'context', text: 'Comment on `a.ts`', metadata: { aiworkerContext: { kind: 'code-comment' } }, instructions: '' },
|
|
120
|
+
{ kind: 'instruction', text: 'use the skill' },
|
|
121
|
+
{ kind: 'synthetic', text: 'conflict payload' },
|
|
122
|
+
];
|
|
123
|
+
expect(parseQueuedItemInput(item({ context })).context).toEqual([
|
|
124
|
+
{ kind: 'context', text: 'Comment on `a.ts`', metadata: { aiworkerContext: { kind: 'code-comment' } } },
|
|
125
|
+
{ kind: 'instruction', text: 'use the skill' },
|
|
126
|
+
{ kind: 'synthetic', text: 'conflict payload' },
|
|
127
|
+
]);
|
|
128
|
+
expect(() => parseQueuedItemInput(item({ context: [{ kind: 'context', text: 'no metadata' }] }))).toThrow(TypeError);
|
|
129
|
+
expect(() => parseQueuedItemInput(item({ context: [{ kind: 'other', text: 'x' }] }))).toThrow(TypeError);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('accepts an item that is only context', () => {
|
|
133
|
+
const parsed = parseQueuedItemInput(item({ content: '', text: '', context: [{ kind: 'synthetic', text: 'just context' }] }));
|
|
134
|
+
expect(parsed.text).toBe('');
|
|
135
|
+
expect(parsed.context).toHaveLength(1);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('message queue runtime', () => {
|
|
140
|
+
it('delivers the head of the queue when the session goes idle, in order', async () => {
|
|
141
|
+
const { runtime, openCode, emit, promptSent, broadcasts } = createRuntime();
|
|
142
|
+
runtime.start();
|
|
143
|
+
openCode.state.statuses = { [SESSION]: { type: 'busy' } };
|
|
144
|
+
|
|
145
|
+
await runtime.enqueue(SESSION, DIRECTORY, item({ content: 'first', text: 'first' }));
|
|
146
|
+
await runtime.enqueue(SESSION, DIRECTORY, item({ content: 'second', text: 'second' }));
|
|
147
|
+
await settle();
|
|
148
|
+
expect(openCode.state.sent).toHaveLength(0);
|
|
149
|
+
|
|
150
|
+
openCode.state.statuses = {};
|
|
151
|
+
emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'idle' } } });
|
|
152
|
+
await settle();
|
|
153
|
+
|
|
154
|
+
expect(openCode.state.sent).toHaveLength(1);
|
|
155
|
+
expect(openCode.state.sent[0].path).toBe(`/session/${SESSION}/prompt_async`);
|
|
156
|
+
expect(openCode.state.sent[0].body).toEqual({
|
|
157
|
+
model: { providerID: 'anthropic', modelID: 'claude' },
|
|
158
|
+
agent: 'build',
|
|
159
|
+
parts: [{ type: 'text', text: 'first' }],
|
|
160
|
+
});
|
|
161
|
+
expect(promptSent).toEqual([SESSION]);
|
|
162
|
+
expect(runtime.sessionSnapshot(SESSION).items.map((entry) => entry.content)).toEqual(['second']);
|
|
163
|
+
// Clients learned about the in-flight item and then the removal.
|
|
164
|
+
expect(broadcasts.at(-1)).toMatchObject({
|
|
165
|
+
type: 'aiworker:message-queue.updated',
|
|
166
|
+
properties: { session: { sessionId: SESSION, sendingId: null } },
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// The next turn: busy, then idle again — the second message goes out.
|
|
170
|
+
emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'busy' } } });
|
|
171
|
+
emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'idle' } } });
|
|
172
|
+
await settle();
|
|
173
|
+
expect(openCode.state.sent).toHaveLength(2);
|
|
174
|
+
expect(runtime.sessionSnapshot(SESSION).items).toEqual([]);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('does not send into a running turn even when the status event says idle', async () => {
|
|
178
|
+
const { runtime, openCode, emit } = createRuntime();
|
|
179
|
+
runtime.start();
|
|
180
|
+
openCode.state.tail = [{ info: { role: 'assistant', time: { created: 1 } } }];
|
|
181
|
+
await runtime.enqueue(SESSION, DIRECTORY, item());
|
|
182
|
+
emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'idle' } } });
|
|
183
|
+
await settle();
|
|
184
|
+
expect(openCode.state.sent).toHaveLength(0);
|
|
185
|
+
|
|
186
|
+
// The reply completes: that alone drains the queue (a missed idle event
|
|
187
|
+
// must not strand it).
|
|
188
|
+
openCode.state.tail = [{ info: { role: 'assistant', time: { created: 1, completed: 2 } } }];
|
|
189
|
+
emit({ type: 'message.updated', properties: { info: { role: 'assistant', sessionID: SESSION, time: { created: 1, completed: 2 } } } });
|
|
190
|
+
await settle();
|
|
191
|
+
expect(openCode.state.sent).toHaveLength(1);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('treats an unreachable OpenCode as unknown, not idle', async () => {
|
|
195
|
+
const { runtime, openCode, emit } = createRuntime({ retryDelayMs: () => 10 });
|
|
196
|
+
runtime.start();
|
|
197
|
+
await runtime.enqueue(SESSION, DIRECTORY, item());
|
|
198
|
+
openCode.state.failNext = /\/session\/status$/;
|
|
199
|
+
emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'idle' } } });
|
|
200
|
+
await settle(5);
|
|
201
|
+
expect(openCode.state.sent).toHaveLength(0);
|
|
202
|
+
// Retried after the status fetch recovers.
|
|
203
|
+
await settle(40);
|
|
204
|
+
expect(openCode.state.sent).toHaveLength(1);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('keeps a failed item and retries with backoff', async () => {
|
|
208
|
+
const { runtime, openCode, emit, broadcasts } = createRuntime({ retryDelayMs: () => 20 });
|
|
209
|
+
runtime.start();
|
|
210
|
+
await runtime.enqueue(SESSION, DIRECTORY, item());
|
|
211
|
+
openCode.state.failNext = /prompt_async$/;
|
|
212
|
+
emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'idle' } } });
|
|
213
|
+
await settle(10);
|
|
214
|
+
expect(openCode.state.sent).toHaveLength(0);
|
|
215
|
+
expect(runtime.sessionSnapshot(SESSION).items).toHaveLength(1);
|
|
216
|
+
expect(runtime.sessionSnapshot(SESSION).sendingId).toBeNull();
|
|
217
|
+
expect(broadcasts.at(-1).properties.session.sendingId).toBeNull();
|
|
218
|
+
await settle(40);
|
|
219
|
+
expect(openCode.state.sent).toHaveLength(1);
|
|
220
|
+
expect(runtime.sessionSnapshot(SESSION).items).toHaveLength(0);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('holds delivery briefly after a user abort', async () => {
|
|
224
|
+
const { runtime, openCode, emit } = createRuntime();
|
|
225
|
+
runtime.start();
|
|
226
|
+
await runtime.enqueue(SESSION, DIRECTORY, item());
|
|
227
|
+
emit({ type: 'message.updated', properties: { info: { role: 'assistant', sessionID: SESSION, error: { name: 'MessageAbortedError' } } } });
|
|
228
|
+
emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'idle' } } });
|
|
229
|
+
await settle(10);
|
|
230
|
+
expect(openCode.state.sent).toHaveLength(0);
|
|
231
|
+
await settle(80);
|
|
232
|
+
expect(openCode.state.sent).toHaveLength(1);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('honors a hold until it is released', async () => {
|
|
236
|
+
const { runtime, openCode, emit } = createRuntime();
|
|
237
|
+
runtime.start();
|
|
238
|
+
await runtime.enqueue(SESSION, DIRECTORY, item());
|
|
239
|
+
runtime.setHold(SESSION, true, 60_000);
|
|
240
|
+
emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'idle' } } });
|
|
241
|
+
await settle();
|
|
242
|
+
expect(openCode.state.sent).toHaveLength(0);
|
|
243
|
+
|
|
244
|
+
runtime.setHold(SESSION, false);
|
|
245
|
+
await settle();
|
|
246
|
+
expect(openCode.state.sent).toHaveLength(1);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('survives a restart and delivers once OpenCode reconnects', async () => {
|
|
250
|
+
const dataDir = makeDataDir();
|
|
251
|
+
const first = createRuntime({ dataDir });
|
|
252
|
+
first.runtime.start();
|
|
253
|
+
first.openCode.state.statuses = { [SESSION]: { type: 'busy' } };
|
|
254
|
+
await first.runtime.enqueue(SESSION, DIRECTORY, item({ content: 'persisted', text: 'persisted' }));
|
|
255
|
+
await first.runtime.flush();
|
|
256
|
+
first.runtime.stop();
|
|
257
|
+
|
|
258
|
+
const second = createRuntime({ dataDir });
|
|
259
|
+
second.runtime.start();
|
|
260
|
+
await second.runtime.load();
|
|
261
|
+
expect(second.runtime.sessionSnapshot(SESSION).items.map((entry) => entry.content)).toEqual(['persisted']);
|
|
262
|
+
second.connect();
|
|
263
|
+
await settle();
|
|
264
|
+
expect(second.openCode.state.sent).toHaveLength(1);
|
|
265
|
+
expect(second.openCode.state.sent[0].body.parts).toEqual([{ type: 'text', text: 'persisted' }]);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('moves an unreadable queue file aside instead of treating it as empty', async () => {
|
|
269
|
+
const dataDir = makeDataDir();
|
|
270
|
+
fs.writeFileSync(path.join(dataDir, 'message-queue.json'), '{ not json');
|
|
271
|
+
const { runtime } = createRuntime({ dataDir });
|
|
272
|
+
await runtime.load();
|
|
273
|
+
expect(runtime.snapshot().sessions).toEqual([]);
|
|
274
|
+
expect(fs.readdirSync(dataDir).some((name) => name.startsWith('message-queue.json.corrupt-'))).toBe(true);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('refuses to remove or take the item currently being sent', async () => {
|
|
278
|
+
const { runtime, openCode, emit } = createRuntime();
|
|
279
|
+
runtime.start();
|
|
280
|
+
let release;
|
|
281
|
+
// status map, message tail, then the prompt itself (held open until released)
|
|
282
|
+
openCode.fetchImpl.mockImplementationOnce(async () => Response.json({}))
|
|
283
|
+
.mockImplementationOnce(async () => Response.json([]))
|
|
284
|
+
.mockImplementationOnce(() => new Promise((resolve) => { release = () => resolve(new Response(null, { status: 204 })); }));
|
|
285
|
+
const { itemId } = await runtime.enqueue(SESSION, DIRECTORY, item());
|
|
286
|
+
emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'idle' } } });
|
|
287
|
+
await settle();
|
|
288
|
+
expect(runtime.sessionSnapshot(SESSION).sendingId).toBe(itemId);
|
|
289
|
+
|
|
290
|
+
await expect(runtime.remove(SESSION, itemId)).rejects.toMatchObject({ status: 409 });
|
|
291
|
+
await expect(runtime.take(SESSION, itemId)).rejects.toMatchObject({ status: 409 });
|
|
292
|
+
const taken = await runtime.takeAll(SESSION);
|
|
293
|
+
expect(taken.items).toEqual([]);
|
|
294
|
+
expect(runtime.sessionSnapshot(SESSION).items).toHaveLength(1);
|
|
295
|
+
|
|
296
|
+
release();
|
|
297
|
+
await settle();
|
|
298
|
+
expect(runtime.sessionSnapshot(SESSION).items).toHaveLength(0);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('take hands back the full payload and leaves the rest queued', async () => {
|
|
302
|
+
const { runtime } = createRuntime();
|
|
303
|
+
runtime.start();
|
|
304
|
+
const attachment = { id: 'a1', filename: 'shot.png', mimeType: 'image/png', size: 3, source: 'local', dataUrl: 'data:image/png;base64,AAA=' };
|
|
305
|
+
const first = await runtime.enqueue(SESSION, DIRECTORY, item({ content: 'with image', attachments: [attachment] }));
|
|
306
|
+
await runtime.enqueue(SESSION, DIRECTORY, item({ content: 'plain' }));
|
|
307
|
+
|
|
308
|
+
expect(runtime.sessionSnapshot(SESSION).items[0].attachments[0]).not.toHaveProperty('dataUrl');
|
|
309
|
+
const taken = await runtime.take(SESSION, first.itemId);
|
|
310
|
+
expect(taken.item.attachments[0].dataUrl).toBe(attachment.dataUrl);
|
|
311
|
+
expect(runtime.sessionSnapshot(SESSION).items.map((entry) => entry.content)).toEqual(['plain']);
|
|
312
|
+
|
|
313
|
+
const all = await runtime.takeAll(SESSION);
|
|
314
|
+
expect(all.items.map((entry) => entry.content)).toEqual(['plain']);
|
|
315
|
+
expect(runtime.snapshot().sessions).toEqual([]);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('names the directory in the broadcast that empties a queue', async () => {
|
|
319
|
+
// The UI keys its projection by directory; without it the client cannot
|
|
320
|
+
// tell which queue just delivered its last message and keeps showing it.
|
|
321
|
+
const { runtime, emit, broadcasts, openCode } = createRuntime();
|
|
322
|
+
runtime.start();
|
|
323
|
+
await runtime.enqueue(SESSION, DIRECTORY, item());
|
|
324
|
+
openCode.state.statuses = {};
|
|
325
|
+
emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'idle' } } });
|
|
326
|
+
await settle();
|
|
327
|
+
|
|
328
|
+
expect(openCode.state.sent).toHaveLength(1);
|
|
329
|
+
expect(runtime.snapshot().sessions).toEqual([]);
|
|
330
|
+
expect(broadcasts.at(-1).properties.session).toEqual({ sessionId: SESSION, directory: DIRECTORY, items: [], sendingId: null });
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('reorders only with a complete permutation', async () => {
|
|
334
|
+
const { runtime } = createRuntime();
|
|
335
|
+
runtime.start();
|
|
336
|
+
const a = await runtime.enqueue(SESSION, DIRECTORY, item({ content: 'a' }));
|
|
337
|
+
const b = await runtime.enqueue(SESSION, DIRECTORY, item({ content: 'b' }));
|
|
338
|
+
await expect(runtime.reorder(SESSION, [b.itemId])).rejects.toThrow(TypeError);
|
|
339
|
+
await runtime.reorder(SESSION, [b.itemId, a.itemId]);
|
|
340
|
+
expect(runtime.sessionSnapshot(SESSION).items.map((entry) => entry.content)).toEqual(['b', 'a']);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('drops the queue of a deleted session', async () => {
|
|
344
|
+
const { runtime, emit, broadcasts } = createRuntime();
|
|
345
|
+
runtime.start();
|
|
346
|
+
await runtime.enqueue(SESSION, DIRECTORY, item());
|
|
347
|
+
emit({ type: 'session.deleted', properties: { info: { id: SESSION } } });
|
|
348
|
+
expect(runtime.snapshot().sessions).toEqual([]);
|
|
349
|
+
expect(broadcasts.at(-1).properties.session).toMatchObject({ sessionId: SESSION, items: [] });
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('dispatches a queued slash command through the command endpoint', async () => {
|
|
353
|
+
const { runtime, openCode, emit } = createRuntime();
|
|
354
|
+
runtime.start();
|
|
355
|
+
openCode.state.commands = [{ name: 'review' }];
|
|
356
|
+
await runtime.enqueue(SESSION, DIRECTORY, item({ content: '/review src', text: '/review src', sendConfig: { providerID: 'p', modelID: 'm', agent: 'build', variant: 'max' } }));
|
|
357
|
+
emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'idle' } } });
|
|
358
|
+
await settle();
|
|
359
|
+
expect(openCode.state.sent).toHaveLength(1);
|
|
360
|
+
expect(openCode.state.sent[0].path).toBe(`/session/${SESSION}/command`);
|
|
361
|
+
expect(openCode.state.sent[0].body).toEqual({ command: 'review', arguments: 'src', model: 'p/m', agent: 'build', variant: 'max' });
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('delivers captured context as synthetic parts, instructions first, before project knowledge', async () => {
|
|
365
|
+
const knowledge = {
|
|
366
|
+
resolvePendingForSession: async () => ({ text: 'pinned notes', signature: 'sig-1' }),
|
|
367
|
+
recordDelivered: async () => {},
|
|
368
|
+
};
|
|
369
|
+
const { runtime, openCode, emit } = createRuntime({ knowledge });
|
|
370
|
+
runtime.start();
|
|
371
|
+
const metadata = { aiworkerContext: { kind: 'github-pr', number: 7, title: 'PR', url: 'https://x/pr/7' } };
|
|
372
|
+
await runtime.enqueue(SESSION, DIRECTORY, item({
|
|
373
|
+
agentMention: 'reviewer',
|
|
374
|
+
attachments: [{ id: 'a', filename: 'f.txt', mimeType: 'text/plain', size: 1, source: 'local', dataUrl: 'data:text/plain,hi' }],
|
|
375
|
+
context: [
|
|
376
|
+
{ kind: 'context', text: 'the diff', metadata, instructions: 'how to read it' },
|
|
377
|
+
{ kind: 'synthetic', text: 'conflict payload' },
|
|
378
|
+
{ kind: 'instruction', text: 'use the skill' },
|
|
379
|
+
],
|
|
380
|
+
}));
|
|
381
|
+
emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'idle' } } });
|
|
382
|
+
await settle();
|
|
383
|
+
expect(openCode.state.sent[0].body.parts).toEqual([
|
|
384
|
+
{ type: 'text', text: 'follow up' },
|
|
385
|
+
{ type: 'file', mime: 'text/plain', filename: 'f.txt', url: 'data:text/plain,hi' },
|
|
386
|
+
{ type: 'text', text: 'how to read it', synthetic: true },
|
|
387
|
+
{ type: 'text', text: 'the diff', synthetic: true, metadata },
|
|
388
|
+
{ type: 'text', text: 'conflict payload', synthetic: true },
|
|
389
|
+
{ type: 'text', text: 'use the skill', synthetic: true },
|
|
390
|
+
{ type: 'text', text: 'pinned notes', synthetic: true },
|
|
391
|
+
{ type: 'agent', name: 'reviewer' },
|
|
392
|
+
]);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('keeps files on the command route, which is all that route accepts', async () => {
|
|
396
|
+
const { runtime, openCode, emit } = createRuntime();
|
|
397
|
+
runtime.start();
|
|
398
|
+
openCode.state.commands = [{ name: 'review' }];
|
|
399
|
+
await runtime.enqueue(SESSION, DIRECTORY, item({
|
|
400
|
+
content: '/review',
|
|
401
|
+
text: '/review',
|
|
402
|
+
attachments: [{ id: 'a', filename: 'f.txt', mimeType: 'text/plain', size: 1, source: 'local', dataUrl: 'data:text/plain,hi' }],
|
|
403
|
+
}));
|
|
404
|
+
emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'idle' } } });
|
|
405
|
+
await settle();
|
|
406
|
+
expect(openCode.state.sent[0].path).toBe(`/session/${SESSION}/command`);
|
|
407
|
+
expect(openCode.state.sent[0].body.parts).toEqual([{ type: 'file', mime: 'text/plain', filename: 'f.txt', url: 'data:text/plain,hi' }]);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('sends a command queued with context as its expanded prompt, context included', async () => {
|
|
411
|
+
// The command route rejects text parts, so a command with captured
|
|
412
|
+
// context takes the prompt route with the template expanded, exactly as
|
|
413
|
+
// the composer does.
|
|
414
|
+
const { runtime, openCode, emit } = createRuntime();
|
|
415
|
+
runtime.start();
|
|
416
|
+
openCode.state.commands = [{ name: 'review', source: 'command', template: 'Review $1 with focus on $2' }];
|
|
417
|
+
const metadata = { aiworkerContext: { kind: 'chat-quote', quote: 'q', text: 'why?' } };
|
|
418
|
+
await runtime.enqueue(SESSION, DIRECTORY, item({
|
|
419
|
+
content: '/review src "error handling"',
|
|
420
|
+
text: '/review src "error handling"',
|
|
421
|
+
context: [{ kind: 'context', text: 'quoted', metadata }],
|
|
422
|
+
}));
|
|
423
|
+
emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'idle' } } });
|
|
424
|
+
await settle();
|
|
425
|
+
expect(openCode.state.sent[0].path).toBe(`/session/${SESSION}/prompt_async`);
|
|
426
|
+
expect(openCode.state.sent[0].body.parts).toEqual([
|
|
427
|
+
{ type: 'text', text: 'Review src with focus on error handling' },
|
|
428
|
+
{ type: 'text', text: 'quoted', synthetic: true, metadata },
|
|
429
|
+
]);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it('sends a skill queued with context as an explicit invocation, context included', async () => {
|
|
433
|
+
const { runtime, openCode, emit } = createRuntime();
|
|
434
|
+
runtime.start();
|
|
435
|
+
openCode.state.commands = [{ name: 'grill', source: 'skill', template: 'skill body' }];
|
|
436
|
+
await runtime.enqueue(SESSION, DIRECTORY, item({
|
|
437
|
+
content: '/grill auth',
|
|
438
|
+
text: '/grill auth',
|
|
439
|
+
context: [{ kind: 'synthetic', text: 'focus on tests' }],
|
|
440
|
+
}));
|
|
441
|
+
emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'idle' } } });
|
|
442
|
+
await settle();
|
|
443
|
+
expect(openCode.state.sent[0].path).toBe(`/session/${SESSION}/prompt_async`);
|
|
444
|
+
expect(openCode.state.sent[0].body.parts).toEqual([
|
|
445
|
+
{ type: 'text', text: '/grill auth' },
|
|
446
|
+
{ type: 'text', text: 'focus on tests', synthetic: true },
|
|
447
|
+
{ type: 'text', text: 'The user explicitly invoked the grill skill. Use the corresponding skill tool to handle this request.', synthetic: true },
|
|
448
|
+
]);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it('keeps captured context out of snapshots and broadcasts, and hands it back on take', async () => {
|
|
452
|
+
const { runtime, broadcasts } = createRuntime();
|
|
453
|
+
runtime.start();
|
|
454
|
+
const context = [{ kind: 'synthetic', text: 'a large diff' }];
|
|
455
|
+
const { itemId } = await runtime.enqueue(SESSION, DIRECTORY, item({ context }));
|
|
456
|
+
expect(runtime.sessionSnapshot(SESSION).items[0]).not.toHaveProperty('context');
|
|
457
|
+
expect(runtime.sessionSnapshot(SESSION).items[0].text).toBe('follow up');
|
|
458
|
+
expect(broadcasts.at(-1).properties.session.items[0]).not.toHaveProperty('context');
|
|
459
|
+
const taken = await runtime.take(SESSION, itemId);
|
|
460
|
+
expect(taken.item.context).toEqual(context);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it('attaches pending project knowledge and records its delivery', async () => {
|
|
464
|
+
const recorded = [];
|
|
465
|
+
const knowledge = {
|
|
466
|
+
resolvePendingForSession: async () => ({ text: 'pinned notes', signature: 'sig-1' }),
|
|
467
|
+
recordDelivered: async (sessionId, directory, signature) => { recorded.push({ sessionId, directory, signature }); },
|
|
468
|
+
};
|
|
469
|
+
const { runtime, openCode, emit } = createRuntime({ knowledge });
|
|
470
|
+
runtime.start();
|
|
471
|
+
await runtime.enqueue(SESSION, DIRECTORY, item({ agentMention: 'reviewer', attachments: [{ id: 'a', filename: 'f.txt', mimeType: 'text/plain', size: 1, source: 'local', dataUrl: 'data:text/plain,hi' }] }));
|
|
472
|
+
emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'idle' } } });
|
|
473
|
+
await settle();
|
|
474
|
+
expect(openCode.state.sent[0].body.parts).toEqual([
|
|
475
|
+
{ type: 'text', text: 'follow up' },
|
|
476
|
+
{ type: 'file', mime: 'text/plain', filename: 'f.txt', url: 'data:text/plain,hi' },
|
|
477
|
+
{ type: 'text', text: 'pinned notes', synthetic: true },
|
|
478
|
+
{ type: 'agent', name: 'reviewer' },
|
|
479
|
+
]);
|
|
480
|
+
expect(recorded).toEqual([{ sessionId: SESSION, directory: DIRECTORY, signature: 'sig-1' }]);
|
|
481
|
+
});
|
|
482
|
+
});
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// - Relay (default): POST tokens + generic text to the central Cloudflare relay, which
|
|
6
6
|
// holds the single project APNs key and signs+sends — so users configure nothing.
|
|
7
7
|
// - Direct (fallback): sign an ES256 JWT with Node crypto and send over HTTP/2 ourselves,
|
|
8
|
-
// for self-hosters who set
|
|
8
|
+
// for self-hosters who set AIWORKER_APNS_* and AIWORKER_PUSH_RELAY_DISABLED=true.
|
|
9
9
|
// Wired into the same trigger fanout as web push (see runtime.js); the relay carries only
|
|
10
10
|
// generic, model-based text (no session content) — see APNS.md.
|
|
11
11
|
|
|
@@ -19,8 +19,8 @@ const APNS_HOST_PRODUCTION = 'https://api.push.apple.com';
|
|
|
19
19
|
const APNS_HOST_SANDBOX = 'https://api.sandbox.push.apple.com';
|
|
20
20
|
// APNs rejects auth tokens older than 1h; refresh well inside that window.
|
|
21
21
|
const JWT_TTL_MS = 50 * 60 * 1000;
|
|
22
|
-
const DEFAULT_BUNDLE_ID = 'com.
|
|
23
|
-
const DEFAULT_RELAY_URL = 'https://api.
|
|
22
|
+
const DEFAULT_BUNDLE_ID = 'com.aiworker.app';
|
|
23
|
+
const DEFAULT_RELAY_URL = 'https://api.aiworker.dev/v1/push/send';
|
|
24
24
|
const MAX_TOKENS_PER_SESSION = 10;
|
|
25
25
|
// APNs reasons that mean the token is permanently invalid → drop it.
|
|
26
26
|
const DEAD_TOKEN_REASONS = new Set(['BadDeviceToken', 'Unregistered', 'DeviceTokenNotForTopic']);
|
|
@@ -227,18 +227,18 @@ export const createApnsRuntime = (deps) => {
|
|
|
227
227
|
// ---------------------------------------------------------------------------
|
|
228
228
|
|
|
229
229
|
const resolveApnsConfig = async () => {
|
|
230
|
-
let keyId = trimmedEnv('
|
|
231
|
-
let teamId = trimmedEnv('
|
|
232
|
-
let bundleId = trimmedEnv('
|
|
233
|
-
let environment = (trimmedEnv('
|
|
234
|
-
let p8 = normalizePem(process.env.
|
|
230
|
+
let keyId = trimmedEnv('AIWORKER_APNS_KEY_ID');
|
|
231
|
+
let teamId = trimmedEnv('AIWORKER_APNS_TEAM_ID');
|
|
232
|
+
let bundleId = trimmedEnv('AIWORKER_APNS_BUNDLE_ID');
|
|
233
|
+
let environment = (trimmedEnv('AIWORKER_APNS_ENVIRONMENT') || '').toLowerCase();
|
|
234
|
+
let p8 = normalizePem(process.env.AIWORKER_APNS_P8 || '');
|
|
235
235
|
|
|
236
|
-
const p8Path = trimmedEnv('
|
|
236
|
+
const p8Path = trimmedEnv('AIWORKER_APNS_P8_PATH');
|
|
237
237
|
if (!p8 && p8Path) {
|
|
238
238
|
try {
|
|
239
239
|
p8 = (await fsPromises.readFile(p8Path, 'utf8')).trim();
|
|
240
240
|
} catch (error) {
|
|
241
|
-
console.warn('[APNs] Failed to read
|
|
241
|
+
console.warn('[APNs] Failed to read AIWORKER_APNS_P8_PATH:', error?.message ?? error);
|
|
242
242
|
}
|
|
243
243
|
}
|
|
244
244
|
|
|
@@ -377,15 +377,15 @@ export const createApnsRuntime = (deps) => {
|
|
|
377
377
|
// Relay mode (default): the single APNs key lives in the central Cloudflare relay, not on
|
|
378
378
|
// each user's server — so users configure nothing. The server just POSTs device tokens +
|
|
379
379
|
// generic text; the relay signs + sends and reports which tokens to drop. Direct mode (below)
|
|
380
|
-
// is the fallback for self-hosters who set
|
|
380
|
+
// is the fallback for self-hosters who set AIWORKER_APNS_* and disable the relay.
|
|
381
381
|
const resolveRelayConfig = () => {
|
|
382
|
-
if (trimmedEnv('
|
|
383
|
-
const url = trimmedEnv('
|
|
384
|
-
const override = (trimmedEnv('
|
|
382
|
+
if (trimmedEnv('AIWORKER_PUSH_RELAY_DISABLED') === 'true') return null;
|
|
383
|
+
const url = trimmedEnv('AIWORKER_PUSH_RELAY_URL') || DEFAULT_RELAY_URL;
|
|
384
|
+
const override = (trimmedEnv('AIWORKER_APNS_ENVIRONMENT') || '').toLowerCase();
|
|
385
385
|
return {
|
|
386
386
|
url,
|
|
387
387
|
registerUrl: url.replace(/\/send$/, '/register-token'),
|
|
388
|
-
// Explicit
|
|
388
|
+
// Explicit AIWORKER_APNS_ENVIRONMENT forces every send to that environment; when
|
|
389
389
|
// unset (null), each token is delivered to the environment it registered with.
|
|
390
390
|
environment: override === 'sandbox' ? 'sandbox' : override === 'production' ? 'production' : null,
|
|
391
391
|
};
|
|
@@ -393,7 +393,7 @@ export const createApnsRuntime = (deps) => {
|
|
|
393
393
|
|
|
394
394
|
const sendViaRelay = async (deviceTokens, payload, relay, environment) => {
|
|
395
395
|
const tokens = deviceTokens.slice(0, 100);
|
|
396
|
-
const title = typeof payload?.title === 'string' && payload.title.length > 0 ? payload.title : '
|
|
396
|
+
const title = typeof payload?.title === 'string' && payload.title.length > 0 ? payload.title : 'AiWorker';
|
|
397
397
|
const { privateKey, publicJwk } = await getOrCreateRelayKeypair();
|
|
398
398
|
const ts = Date.now();
|
|
399
399
|
// Sign over the same canonical form the relay verifies: ts.sortedTokens.title.
|
|
@@ -438,7 +438,7 @@ export const createApnsRuntime = (deps) => {
|
|
|
438
438
|
if (!warnedUnconfigured) {
|
|
439
439
|
warnedUnconfigured = true;
|
|
440
440
|
console.warn(
|
|
441
|
-
'[APNs] Relay disabled and no direct config; set
|
|
441
|
+
'[APNs] Relay disabled and no direct config; set AIWORKER_APNS_KEY_ID / AIWORKER_APNS_TEAM_ID / AIWORKER_APNS_P8 for direct send.',
|
|
442
442
|
);
|
|
443
443
|
}
|
|
444
444
|
return;
|
|
@@ -6,7 +6,7 @@ import { createApnsRuntime } from './apns-runtime.js';
|
|
|
6
6
|
// A real P-256 key so the ES256 signing path (direct mode) runs for real.
|
|
7
7
|
const { privateKey } = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' });
|
|
8
8
|
const P8 = privateKey.export({ type: 'pkcs8', format: 'pem' }).toString();
|
|
9
|
-
const APNS_CONFIG = { keyId: 'KEY123', teamId: 'TEAM123', p8: P8, bundleId: 'com.
|
|
9
|
+
const APNS_CONFIG = { keyId: 'KEY123', teamId: 'TEAM123', p8: P8, bundleId: 'com.aiworker.app', environment: 'sandbox' };
|
|
10
10
|
|
|
11
11
|
// In-memory fs so add-then-read reflects within a test.
|
|
12
12
|
const createMemoryFs = () => {
|
|
@@ -67,9 +67,9 @@ const isSend = ([url]) => String(url) === 'https://relay.test/v1/push/send';
|
|
|
67
67
|
|
|
68
68
|
afterEach(() => {
|
|
69
69
|
vi.unstubAllGlobals();
|
|
70
|
-
delete process.env.
|
|
71
|
-
delete process.env.
|
|
72
|
-
delete process.env.
|
|
70
|
+
delete process.env.AIWORKER_PUSH_RELAY_URL;
|
|
71
|
+
delete process.env.AIWORKER_PUSH_RELAY_DISABLED;
|
|
72
|
+
delete process.env.AIWORKER_APNS_ENVIRONMENT;
|
|
73
73
|
});
|
|
74
74
|
|
|
75
75
|
describe('apns runtime relay mode (default)', () => {
|
|
@@ -85,7 +85,7 @@ describe('apns runtime relay mode (default)', () => {
|
|
|
85
85
|
}),
|
|
86
86
|
);
|
|
87
87
|
vi.stubGlobal('fetch', fetchMock);
|
|
88
|
-
process.env.
|
|
88
|
+
process.env.AIWORKER_PUSH_RELAY_URL = 'https://relay.test/v1/push/send';
|
|
89
89
|
|
|
90
90
|
const runtime = createApnsRuntime(makeDeps());
|
|
91
91
|
await runtime.addOrUpdateApnsToken('s1', 'tokenA');
|
|
@@ -132,7 +132,7 @@ describe('apns runtime relay mode (default)', () => {
|
|
|
132
132
|
it('reuses one persisted keypair (same serverId) across register + send', async () => {
|
|
133
133
|
const fetchMock = vi.fn(async () => jsonResponse({ ok: true, results: [] }));
|
|
134
134
|
vi.stubGlobal('fetch', fetchMock);
|
|
135
|
-
process.env.
|
|
135
|
+
process.env.AIWORKER_PUSH_RELAY_URL = 'https://relay.test/v1/push/send';
|
|
136
136
|
|
|
137
137
|
const deps = makeDeps();
|
|
138
138
|
const runtime = createApnsRuntime(deps);
|
|
@@ -149,8 +149,8 @@ describe('apns runtime relay mode (default)', () => {
|
|
|
149
149
|
it('honors an explicit sandbox environment override for every token', async () => {
|
|
150
150
|
const fetchMock = vi.fn(async () => jsonResponse({ ok: true, results: [] }));
|
|
151
151
|
vi.stubGlobal('fetch', fetchMock);
|
|
152
|
-
process.env.
|
|
153
|
-
process.env.
|
|
152
|
+
process.env.AIWORKER_PUSH_RELAY_URL = 'https://relay.test/v1/push/send';
|
|
153
|
+
process.env.AIWORKER_APNS_ENVIRONMENT = 'sandbox';
|
|
154
154
|
|
|
155
155
|
const runtime = createApnsRuntime(makeDeps());
|
|
156
156
|
await runtime.addOrUpdateApnsToken('s1', 'tokenA', undefined, 'ios', 'production');
|
|
@@ -163,7 +163,7 @@ describe('apns runtime relay mode (default)', () => {
|
|
|
163
163
|
it('routes each token to its registered environment (dev build sandbox, release production)', async () => {
|
|
164
164
|
const fetchMock = vi.fn(async () => jsonResponse({ ok: true, results: [] }));
|
|
165
165
|
vi.stubGlobal('fetch', fetchMock);
|
|
166
|
-
process.env.
|
|
166
|
+
process.env.AIWORKER_PUSH_RELAY_URL = 'https://relay.test/v1/push/send';
|
|
167
167
|
|
|
168
168
|
const runtime = createApnsRuntime(makeDeps());
|
|
169
169
|
await runtime.addOrUpdateApnsToken('s1', 'tokenXcode', undefined, 'ios', 'sandbox');
|
|
@@ -200,7 +200,7 @@ describe('apns runtime direct fallback (relay disabled)', () => {
|
|
|
200
200
|
});
|
|
201
201
|
|
|
202
202
|
it('sends each token to the APNs host of its registered environment', async () => {
|
|
203
|
-
process.env.
|
|
203
|
+
process.env.AIWORKER_PUSH_RELAY_DISABLED = 'true';
|
|
204
204
|
const { environment: _environment, ...configWithoutEnvironment } = APNS_CONFIG;
|
|
205
205
|
const hosts = [];
|
|
206
206
|
const http2 = {
|
|
@@ -241,7 +241,7 @@ describe('apns runtime direct fallback (relay disabled)', () => {
|
|
|
241
241
|
});
|
|
242
242
|
|
|
243
243
|
it('signs an ES256 JWT and sends over http2 when relay is disabled', async () => {
|
|
244
|
-
process.env.
|
|
244
|
+
process.env.AIWORKER_PUSH_RELAY_DISABLED = 'true';
|
|
245
245
|
const targeted = [];
|
|
246
246
|
const http2 = {
|
|
247
247
|
connect: () => ({
|
|
@@ -66,7 +66,7 @@ export const createNotificationEmitterRuntime = (dependencies) => {
|
|
|
66
66
|
const desktopNotificationDelivered = options.desktopNotificationDelivered === true;
|
|
67
67
|
|
|
68
68
|
const syntheticPayload = {
|
|
69
|
-
type: '
|
|
69
|
+
type: 'aiworker:notification',
|
|
70
70
|
properties: {
|
|
71
71
|
...payload,
|
|
72
72
|
// Tell local desktop UI whether a native channel already accepted this
|
|
@@ -36,7 +36,7 @@ describe('notification emitter runtime', () => {
|
|
|
36
36
|
runtime.broadcastUiNotification({ title: 'Ready' }, { desktopNotificationDelivered: true });
|
|
37
37
|
|
|
38
38
|
expect(broadcastGlobalUiEvent).toHaveBeenCalledWith({
|
|
39
|
-
type: '
|
|
39
|
+
type: 'aiworker:notification',
|
|
40
40
|
properties: {
|
|
41
41
|
title: 'Ready',
|
|
42
42
|
desktopNotificationDelivered: true,
|