@gajae-code/coding-agent 0.18.11 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +760 -0
- package/dist/types/async/job-manager.d.ts +7 -3
- package/dist/types/async/result-formatting.d.ts +2 -0
- package/dist/types/autoresearch/git.d.ts +1 -1
- package/dist/types/capability/fs.d.ts +2 -2
- package/dist/types/capability/rule.d.ts +7 -2
- package/dist/types/cli/accounts-cli.d.ts +2 -2
- package/dist/types/cli/append-system-prompt.d.ts +6 -0
- package/dist/types/cli/args.d.ts +7 -2
- package/dist/types/cli/customize-doctor-precedence.d.ts +16 -0
- package/dist/types/cli/customize-doctor.d.ts +4 -3
- package/dist/types/cli/doctor/install-repairs.d.ts +4 -1
- package/dist/types/cli/install-activation.d.ts +10 -4
- package/dist/types/cli/public-command-errors.d.ts +2 -0
- package/dist/types/cli/root-flags.d.ts +0 -12
- package/dist/types/cli/update-cli.d.ts +21 -3
- package/dist/types/cli-main.d.ts +0 -6
- package/dist/types/cli-ordinary.d.ts +2 -2
- package/dist/types/commands/accounts.d.ts +2 -1
- package/dist/types/commands/launch.d.ts +0 -6
- package/dist/types/commands/migrate.d.ts +1 -1
- package/dist/types/commands/sdk.d.ts +13 -0
- package/dist/types/commit/changelog/index.d.ts +1 -0
- package/dist/types/commit/map-reduce/index.d.ts +4 -4
- package/dist/types/config/autorouting-tier-map.d.ts +53 -1
- package/dist/types/config/config-hot-reload-trust.d.ts +12 -0
- package/dist/types/config/config-hot-reload.d.ts +44 -0
- package/dist/types/config/file-lock.d.ts +52 -3
- package/dist/types/config/model-discovery-manager.d.ts +2 -0
- package/dist/types/config/model-profile-activation.d.ts +79 -9
- package/dist/types/config/model-registry.d.ts +72 -8
- package/dist/types/config/model-resolver.d.ts +7 -4
- package/dist/types/config/models-config-schema.d.ts +10 -0
- package/dist/types/config/settings-schema.d.ts +57 -4
- package/dist/types/config/settings.d.ts +33 -1
- package/dist/types/coordinator-mcp/server.d.ts +4 -0
- package/dist/types/customization/import.d.ts +2 -0
- package/dist/types/customization/types.d.ts +1 -1
- package/dist/types/dap/config.d.ts +8 -4
- package/dist/types/dap/session.d.ts +11 -2
- package/dist/types/debug/system-info.d.ts +6 -1
- package/dist/types/discovery/helpers.d.ts +7 -2
- package/dist/types/discovery/mcp-compat.d.ts +3 -1
- package/dist/types/edit/index.d.ts +5 -0
- package/dist/types/edit/modes/patch.d.ts +14 -0
- package/dist/types/edit/modes/replace.d.ts +3 -0
- package/dist/types/eval/py/runtime.d.ts +9 -0
- package/dist/types/exec/bash-executor.d.ts +12 -0
- package/dist/types/export/ttsr.d.ts +19 -0
- package/dist/types/extensibility/extensions/types.d.ts +5 -1
- package/dist/types/extensibility/slash-commands.d.ts +1 -0
- package/dist/types/gjc-runtime/managed-owner-admission.d.ts +3 -0
- package/dist/types/gjc-runtime/ralplan-current-final.d.ts +54 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +5 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
- package/dist/types/gjc-runtime/state-runtime.d.ts +0 -2
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +24 -0
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +6 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
- package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
- package/dist/types/hindsight/content.d.ts +5 -6
- package/dist/types/internal-urls/types.d.ts +2 -0
- package/dist/types/lsp/clients/swiftlint-client.d.ts +0 -4
- package/dist/types/main.d.ts +23 -18
- package/dist/types/migrate/action-planner.d.ts +10 -1
- package/dist/types/migrate/adapters/codex.d.ts +2 -1
- package/dist/types/migrate/adapters/index.d.ts +42 -1
- package/dist/types/migrate/executor.d.ts +3 -1
- package/dist/types/migrate/hook-module.d.ts +23 -0
- package/dist/types/migrate/report.d.ts +2 -1
- package/dist/types/migrate/types.d.ts +82 -3
- package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
- package/dist/types/modes/components/model-selector.d.ts +43 -0
- package/dist/types/modes/components/settings-selector.d.ts +6 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -0
- package/dist/types/modes/prompt-suggestion-controller.d.ts +1 -1
- package/dist/types/modes/theme/theme.d.ts +1 -0
- package/dist/types/modes/utils/host-status.d.ts +15 -11
- package/dist/types/runtime-mcp/content-limits.d.ts +4 -0
- package/dist/types/runtime-mcp/manager.d.ts +7 -0
- package/dist/types/runtime-mcp/plugin-mcp-name-filter.d.ts +22 -0
- package/dist/types/runtime-mcp/project-host-values.d.ts +11 -5
- package/dist/types/runtime-mcp/tool-bridge.d.ts +4 -0
- package/dist/types/sdk/broker/broker-exit.d.ts +3 -3
- package/dist/types/sdk/broker/broker.d.ts +29 -2
- package/dist/types/sdk/broker/daemon-entry.d.ts +11 -1
- package/dist/types/sdk/broker/doctor-restart.d.ts +1 -1
- package/dist/types/sdk/broker/ensure.d.ts +141 -9
- package/dist/types/sdk/broker/hop.d.ts +45 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +18 -10
- package/dist/types/sdk/broker/lifecycle.d.ts +22 -0
- package/dist/types/sdk/broker/lock-host-identity.d.ts +5 -0
- package/dist/types/sdk/broker/session-index.d.ts +6 -1
- package/dist/types/sdk/broker/transport.d.ts +2 -0
- package/dist/types/sdk/bus/chat-command-policy.d.ts +2 -1
- package/dist/types/sdk/bus/index.d.ts +44 -0
- package/dist/types/sdk/bus/reconciliation-store.d.ts +10 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +2 -0
- package/dist/types/sdk/bus/threaded-render.d.ts +2 -1
- package/dist/types/sdk/cli/session-cli.d.ts +0 -1
- package/dist/types/sdk/host/control/operations.d.ts +5 -1
- package/dist/types/sdk/host/events.d.ts +4 -0
- package/dist/types/sdk/host/host.d.ts +2 -1
- package/dist/types/sdk/host/prompt-image-upload.d.ts +43 -0
- package/dist/types/sdk/host/session-runtime.d.ts +40 -8
- package/dist/types/sdk/host/surface-policy.d.ts +2 -1
- package/dist/types/sdk/lifecycle/service.d.ts +12 -0
- package/dist/types/sdk/prompt-deadline-flush.d.ts +42 -4
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -0
- package/dist/types/sdk/prompt-status.d.ts +11 -4
- package/dist/types/sdk/protocol/adapter-validation.d.ts +10 -4
- package/dist/types/sdk/reconciliation-extensions.d.ts +1 -1
- package/dist/types/sdk/session-list.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +107 -9
- package/dist/types/session/internal/managed-session-scope.d.ts +2 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +1 -1
- package/dist/types/session/internal/native-publish-outcome.d.ts +1 -1
- package/dist/types/session/messages.d.ts +5 -1
- package/dist/types/session/session-manager.d.ts +29 -3
- package/dist/types/session/startup-auth-config.d.ts +24 -2
- package/dist/types/session/streaming-output.d.ts +11 -0
- package/dist/types/session/terminal-abort.d.ts +3 -1
- package/dist/types/session-import/service.d.ts +1 -1
- package/dist/types/skill-state/workflow-mutation-guard.d.ts +20 -4
- package/dist/types/slash-commands/helpers/mcp.d.ts +5 -0
- package/dist/types/slash-commands/helpers/provider-auth-status.d.ts +8 -0
- package/dist/types/ssh/ssh-executor.d.ts +8 -1
- package/dist/types/system-prompt.d.ts +3 -2
- package/dist/types/task/executor.d.ts +19 -2
- package/dist/types/task/role-body.d.ts +11 -0
- package/dist/types/tools/ast-edit-write-path.d.ts +10 -0
- package/dist/types/tools/atomic-file-write.d.ts +9 -7
- package/dist/types/tools/awaiting-input.d.ts +28 -0
- package/dist/types/tools/bash-allowed-prefixes.d.ts +17 -0
- package/dist/types/tools/bash-skill-urls.d.ts +4 -0
- package/dist/types/tools/bash.d.ts +7 -0
- package/dist/types/tools/descriptors.d.ts +15 -2
- package/dist/types/tools/fetch.d.ts +7 -1
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/irc.d.ts +5 -3
- package/dist/types/tools/python.d.ts +1 -1
- package/dist/types/tools/report-issue.d.ts +41 -0
- package/dist/types/tools/subagent.d.ts +23 -0
- package/dist/types/tools/tmux-self-injection-guard.d.ts +2 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +1 -1
- package/dist/types/tools/write.d.ts +2 -0
- package/dist/types/utils/file-mentions.d.ts +5 -0
- package/dist/types/utils/image-limits.d.ts +7 -0
- package/dist/types/utils/image-loading.d.ts +0 -1
- package/dist/types/utils/open.d.ts +0 -1
- package/dist/types/utils/pasted-image-loading.d.ts +1 -5
- package/dist/types/utils/prompt-suggestion.d.ts +2 -2
- package/dist/types/utils/shell-snapshot.d.ts +2 -0
- package/dist/types/utils/title-generator.d.ts +13 -3
- package/dist/types/web/scrapers/types.d.ts +8 -1
- package/dist/types/web/search/providers/codex.d.ts +6 -7
- package/dist/types/web/search/providers/insane.d.ts +7 -1
- package/dist/types/web/search/providers/searxng.d.ts +5 -0
- package/package.json +10 -9
- package/scripts/acp-conformance-agent.ts +12 -2
- package/scripts/build-binary.ts +2 -1
- package/scripts/capture-subagent-compact-panel-showcase.ts +105 -0
- package/scripts/generate-docs-index.ts +8 -1
- package/scripts/generate-sdk-operation-inventory.ts +38 -4
- package/src/async/job-manager.ts +356 -56
- package/src/async/result-formatting.ts +40 -0
- package/src/autoresearch/git.ts +9 -2
- package/src/autoresearch/prompts/prompt-setup.md +1 -1
- package/src/capability/fs.ts +58 -22
- package/src/capability/index.ts +2 -2
- package/src/capability/rule.ts +11 -2
- package/src/cli/accounts-cli.ts +176 -8
- package/src/cli/append-system-prompt.ts +12 -0
- package/src/cli/args.ts +11 -7
- package/src/cli/auth-broker-cli.ts +21 -11
- package/src/cli/customize-doctor-precedence.ts +31 -0
- package/src/cli/customize-doctor.ts +49 -39
- package/src/cli/doctor/install-repairs.ts +35 -5
- package/src/cli/fast-help.ts +1 -3
- package/src/cli/install-activation.ts +47 -4
- package/src/cli/migrate-cli.ts +19 -6
- package/src/cli/public-command-errors.ts +6 -0
- package/src/cli/root-flags.ts +2 -4
- package/src/cli/stats-cli.ts +9 -5
- package/src/cli/update-cli.ts +471 -13
- package/src/cli-ordinary.ts +7 -2
- package/src/commands/accounts.ts +10 -5
- package/src/commands/harness.ts +25 -15
- package/src/commands/migrate.ts +3 -3
- package/src/commands/sdk.ts +277 -50
- package/src/commands/ultragoal.ts +16 -8
- package/src/commit/agentic/agent.ts +6 -6
- package/src/commit/analysis/conventional.ts +14 -5
- package/src/commit/changelog/index.ts +39 -1
- package/src/commit/map-reduce/index.ts +7 -7
- package/src/commit/message.ts +10 -3
- package/src/commit/pipeline.ts +12 -12
- package/src/config/autorouting-tier-map.ts +50 -1
- package/src/config/config-file.ts +106 -3
- package/src/config/config-hot-reload-trust.ts +148 -0
- package/src/config/config-hot-reload.ts +742 -0
- package/src/config/file-lock.ts +1615 -328
- package/src/config/keybindings.ts +154 -7
- package/src/config/model-bindings-applier.ts +85 -47
- package/src/config/model-discovery-manager.ts +10 -0
- package/src/config/model-profile-activation.ts +889 -148
- package/src/config/model-registry.ts +1706 -205
- package/src/config/model-resolver.ts +52 -22
- package/src/config/models-config-schema.ts +6 -0
- package/src/config/settings-schema.ts +73 -4
- package/src/config/settings.ts +460 -24
- package/src/coordinator-mcp/policy.ts +26 -1
- package/src/coordinator-mcp/server.ts +37 -2
- package/src/crash/sanitize.ts +7 -1
- package/src/customization/import.ts +279 -8
- package/src/customization/inventory.ts +24 -8
- package/src/customization/types.ts +1 -0
- package/src/dap/client.ts +3 -2
- package/src/dap/config.ts +31 -11
- package/src/dap/defaults.json +1 -1
- package/src/dap/session.ts +161 -36
- package/src/debug/crash-diagnostics.ts +89 -6
- package/src/debug/report-bundle.ts +90 -2
- package/src/debug/system-info.ts +8 -3
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +21 -7
- package/src/discovery/agents-md.ts +1 -1
- package/src/discovery/builtin.ts +4 -3
- package/src/discovery/helpers.ts +25 -16
- package/src/discovery/mcp-compat.ts +120 -17
- package/src/edit/apply-patch/index.ts +12 -1
- package/src/edit/index.ts +204 -22
- package/src/edit/modes/patch.ts +214 -8
- package/src/edit/modes/replace.ts +136 -0
- package/src/eval/js/context-manager.ts +29 -3
- package/src/eval/js/worker-entry.ts +51 -18
- package/src/eval/py/executor.ts +14 -4
- package/src/eval/py/runner.py +144 -5
- package/src/eval/py/runtime.ts +44 -4
- package/src/exec/bash-executor.ts +81 -4
- package/src/exec/bash-shell-guardian.ts +4 -1
- package/src/exec/bash-shell-supervisor.ts +8 -4
- package/src/exec/isolated-shell.ts +5 -1
- package/src/exec/non-interactive-env.ts +0 -1
- package/src/export/html/index.ts +86 -6
- package/src/export/ttsr.ts +176 -1
- package/src/extensibility/custom-tools/wrapper.ts +3 -1
- package/src/extensibility/extensions/loader.ts +10 -1
- package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +63 -1
- package/src/extensibility/extensions/types.ts +5 -1
- package/src/extensibility/hooks/loader.ts +10 -2
- package/src/extensibility/plugins/marketplace/fetcher.ts +2 -2
- package/src/extensibility/plugins/marketplace/manager.ts +36 -10
- package/src/extensibility/slash-commands.ts +5 -0
- package/src/gjc-runtime/gc-runtime.ts +37 -7
- package/src/gjc-runtime/launch-tmux.ts +30 -15
- package/src/gjc-runtime/launch-worktree.test.ts +148 -0
- package/src/gjc-runtime/launch-worktree.ts +101 -14
- package/src/gjc-runtime/managed-owner-admission.ts +46 -2
- package/src/gjc-runtime/ralplan-current-final.ts +294 -0
- package/src/gjc-runtime/ralplan-runtime.ts +95 -1
- package/src/gjc-runtime/session-resolution.ts +38 -6
- package/src/gjc-runtime/session-state-lock.ts +59 -14
- package/src/gjc-runtime/state-runtime.ts +23 -19
- package/src/gjc-runtime/state-writer.ts +109 -5
- package/src/gjc-runtime/tmux-sessions.ts +93 -13
- package/src/gjc-runtime/ultragoal-change-set.ts +90 -11
- package/src/gjc-runtime/ultragoal-guard.ts +9 -1
- package/src/gjc-runtime/ultragoal-runtime.ts +15 -5
- package/src/harness-control-plane/finalize.ts +11 -0
- package/src/harness-control-plane/receipt-spool.ts +105 -28
- package/src/harness-control-plane/storage.ts +124 -6
- package/src/hindsight/content.ts +116 -16
- package/src/hindsight/mental-models.ts +22 -10
- package/src/hindsight/state.ts +7 -4
- package/src/hooks/native-skill-hook.ts +17 -12
- package/src/internal-urls/artifact-protocol.ts +3 -1
- package/src/internal-urls/docs-index.generated.ts +23 -22
- package/src/internal-urls/local-protocol.ts +167 -8
- package/src/internal-urls/types.ts +2 -0
- package/src/lsp/client.ts +2 -1
- package/src/lsp/clients/biome-client.ts +2 -0
- package/src/lsp/clients/swiftlint-client.ts +2 -0
- package/src/lsp/index.ts +29 -0
- package/src/lsp/workspace-path.ts +23 -0
- package/src/main.ts +330 -92
- package/src/memories/index.ts +25 -2
- package/src/migrate/action-planner.ts +574 -2
- package/src/migrate/adapters/claude-code.ts +162 -5
- package/src/migrate/adapters/codex.ts +77 -7
- package/src/migrate/adapters/index.ts +258 -5
- package/src/migrate/adapters/opencode.ts +10 -1
- package/src/migrate/executor.ts +222 -7
- package/src/migrate/hook-module.ts +193 -0
- package/src/migrate/report.ts +63 -2
- package/src/migrate/types.ts +84 -4
- package/src/modes/DESIGN.md +51 -0
- package/src/modes/acp/acp-agent.ts +10 -1
- package/src/modes/acp/permission-mode.ts +18 -1
- package/src/modes/components/agent-dashboard.ts +20 -4
- package/src/modes/components/assistant-message.ts +55 -1
- package/src/modes/components/customization/customization-dashboard.ts +7 -0
- package/src/modes/components/customization/import-wizard.ts +29 -3
- package/src/modes/components/model-selector.ts +157 -103
- package/src/modes/components/settings-selector.ts +103 -37
- package/src/modes/components/welcome.ts +3 -1
- package/src/modes/controllers/command-controller.ts +300 -9
- package/src/modes/controllers/event-controller.ts +1 -1
- package/src/modes/controllers/extension-ui-controller.ts +31 -1
- package/src/modes/controllers/goal-mode-controller.ts +79 -10
- package/src/modes/controllers/input-controller.ts +2 -0
- package/src/modes/controllers/plan-mode-controller.ts +66 -11
- package/src/modes/controllers/runtime-mcp-command-controller.ts +7 -6
- package/src/modes/controllers/selector-controller.ts +76 -7
- package/src/modes/interactive-mode.ts +123 -4
- package/src/modes/prompt-suggestion-controller.ts +1 -1
- package/src/modes/runtime-init.ts +5 -1
- package/src/modes/theme/theme.ts +15 -0
- package/src/modes/utils/host-status.ts +21 -10
- package/src/prompts/agent-fragments/browser-aside.md +2 -2
- package/src/prompts/system/irc-incoming.md +3 -1
- package/src/prompts/tools/debug.md +1 -1
- package/src/prompts/tools/irc.md +1 -1
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/report-issue.md +13 -0
- package/src/prompts/tools/task.md +1 -1
- package/src/runtime-credential-selector.ts +2 -2
- package/src/runtime-mcp/content-limits.ts +7 -0
- package/src/runtime-mcp/manager-pool.test.ts +5 -4
- package/src/runtime-mcp/manager.ts +40 -3
- package/src/runtime-mcp/oauth-flow.ts +32 -6
- package/src/runtime-mcp/plugin-mcp-name-filter.ts +62 -0
- package/src/runtime-mcp/project-host-values.ts +12 -8
- package/src/runtime-mcp/render.ts +16 -1
- package/src/runtime-mcp/tool-bridge.ts +194 -21
- package/src/runtime-mcp/transports/stdio.ts +2 -1
- package/src/sdk/broker/broker-exit.ts +10 -5
- package/src/sdk/broker/broker.ts +484 -85
- package/src/sdk/broker/daemon-entry.ts +338 -116
- package/src/sdk/broker/doctor-restart.ts +2 -1
- package/src/sdk/broker/ensure.ts +1214 -268
- package/src/sdk/broker/hop.ts +286 -0
- package/src/sdk/broker/lifecycle-ledger.ts +38 -11
- package/src/sdk/broker/lifecycle.ts +102 -6
- package/src/sdk/broker/lock-host-identity.ts +29 -0
- package/src/sdk/broker/session-index.ts +91 -16
- package/src/sdk/broker/transport.ts +17 -1
- package/src/sdk/bus/chat-command-policy.ts +16 -3
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/index.ts +460 -35
- package/src/sdk/bus/reconciliation-store.ts +201 -8
- package/src/sdk/bus/session-control.ts +2 -0
- package/src/sdk/bus/slack-daemon.ts +105 -9
- package/src/sdk/bus/telegram-daemon-contract.ts +9 -1
- package/src/sdk/bus/telegram-daemon.ts +10 -2
- package/src/sdk/bus/threaded-render.ts +20 -1
- package/src/sdk/cli/session-cli.ts +23 -6
- package/src/sdk/host/control/dispatch.ts +13 -1
- package/src/sdk/host/control/operations.ts +10 -1
- package/src/sdk/host/events.ts +67 -4
- package/src/sdk/host/host.ts +8 -2
- package/src/sdk/host/prompt-image-upload.ts +438 -0
- package/src/sdk/host/sdk-surface-parity.test.ts +15 -0
- package/src/sdk/host/session-runtime.test.ts +1737 -129
- package/src/sdk/host/session-runtime.ts +684 -83
- package/src/sdk/host/surface-policy.ts +4 -0
- package/src/sdk/host/websocket-transport.lifecycle.test.ts +90 -2
- package/src/sdk/host/websocket-transport.ts +19 -7
- package/src/sdk/lifecycle/service.ts +15 -1
- package/src/sdk/prompt-deadline-flush.ts +78 -5
- package/src/sdk/prompt-deadline-manager.ts +68 -14
- package/src/sdk/prompt-status.ts +6 -4
- package/src/sdk/protocol/adapter-validation.ts +81 -12
- package/src/sdk/protocol/operation-inventory.generated.json +261 -2
- package/src/sdk/protocol/operation-registry.ts +33 -3
- package/src/sdk/reconciliation-extensions.ts +1 -0
- package/src/sdk/session-list.ts +1 -0
- package/src/sdk/session.ts +237 -97
- package/src/session/agent-session.ts +2726 -479
- package/src/session/history-storage.ts +182 -12
- package/src/session/internal/managed-session-scope.ts +173 -57
- package/src/session/internal/managed-session-storage.ts +85 -12
- package/src/session/internal/native-publish-outcome.ts +5 -1
- package/src/session/messages.ts +47 -6
- package/src/session/session-manager.ts +326 -19
- package/src/session/startup-auth-config.ts +116 -8
- package/src/session/streaming-output.ts +262 -4
- package/src/session/terminal-abort.ts +14 -2
- package/src/session-import/redact.ts +53 -2
- package/src/session-import/service.ts +1 -0
- package/src/skill-state/workflow-mutation-guard.ts +2145 -206
- package/src/slash-commands/builtin-registry.ts +21 -6
- package/src/slash-commands/helpers/credential-switch.ts +10 -5
- package/src/slash-commands/helpers/mcp.ts +11 -3
- package/src/slash-commands/helpers/provider-auth-status.ts +456 -0
- package/src/ssh/ssh-executor.ts +9 -2
- package/src/stt/recorder.ts +26 -9
- package/src/system-prompt.ts +61 -16
- package/src/task/executor.ts +115 -20
- package/src/task/index.ts +389 -175
- package/src/task/role-body.ts +84 -0
- package/src/task/token-log-mode.test.ts +162 -0
- package/src/task/token-log.ts +91 -2
- package/src/tools/archive-reader.ts +650 -37
- package/src/tools/ask.ts +1 -0
- package/src/tools/ast-edit-write-path.ts +64 -0
- package/src/tools/ast-edit.ts +18 -1
- package/src/tools/ast-grep.ts +7 -2
- package/src/tools/atomic-file-write.ts +46 -15
- package/src/tools/auto-generated-guard.ts +15 -3
- package/src/tools/awaiting-input.ts +114 -0
- package/src/tools/bash-allowed-prefixes.ts +214 -6
- package/src/tools/bash-interactive.ts +14 -2
- package/src/tools/bash-skill-urls.ts +58 -5
- package/src/tools/bash.ts +113 -26
- package/src/tools/browser/tab-worker.ts +39 -13
- package/src/tools/browser.ts +17 -7
- package/src/tools/cron.ts +10 -1
- package/src/tools/debug.ts +16 -8
- package/src/tools/descriptors.test.ts +44 -0
- package/src/tools/descriptors.ts +50 -6
- package/src/tools/eval.ts +15 -0
- package/src/tools/fetch.ts +93 -4
- package/src/tools/gh.ts +27 -12
- package/src/tools/image-gen.ts +20 -6
- package/src/tools/index.ts +2 -0
- package/src/tools/irc.ts +5 -3
- package/src/tools/monitor.ts +9 -1
- package/src/tools/output-meta.ts +74 -5
- package/src/tools/python.ts +1 -1
- package/src/tools/read.ts +26 -37
- package/src/tools/recipe/runner.ts +23 -12
- package/src/tools/render-utils.ts +2 -2
- package/src/tools/report-issue.ts +184 -0
- package/src/tools/search.ts +3 -2
- package/src/tools/ssh.ts +274 -8
- package/src/tools/subagent-render.ts +114 -12
- package/src/tools/subagent.ts +118 -5
- package/src/tools/tmux-self-injection-guard.ts +3301 -122
- package/src/tools/todo-write.ts +41 -3
- package/src/tools/tool-catalog.generated.ts +32 -4
- package/src/tools/ultragoal-ask-guard.ts +13 -1
- package/src/tools/write.ts +42 -3
- package/src/tui/tree-list.ts +15 -1
- package/src/utils/commit-message-generator.ts +12 -4
- package/src/utils/external-editor.ts +21 -7
- package/src/utils/file-mentions.ts +75 -7
- package/src/utils/git.ts +38 -7
- package/src/utils/image-limits.ts +8 -0
- package/src/utils/image-loading.ts +1 -1
- package/src/utils/open.ts +73 -4
- package/src/utils/pasted-image-loading.ts +9 -8
- package/src/utils/prompt-suggestion.ts +5 -4
- package/src/utils/shell-snapshot.ts +9 -1
- package/src/utils/title-generator.ts +72 -22
- package/src/web/scrapers/index.ts +3 -1
- package/src/web/scrapers/types.ts +8 -0
- package/src/web/scrapers/youtube.ts +3 -2
- package/src/web/search/index.ts +2 -1
- package/src/web/search/providers/codex.ts +29 -32
- package/src/web/search/providers/insane.ts +91 -28
- package/src/web/search/providers/searxng.ts +62 -10
|
@@ -4,10 +4,23 @@ import * as fsPromises from "node:fs/promises";
|
|
|
4
4
|
import { chmod, mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
5
5
|
import * as os from "node:os";
|
|
6
6
|
import * as path from "node:path";
|
|
7
|
-
import {
|
|
7
|
+
import { deflateSync } from "node:zlib";
|
|
8
|
+
import {
|
|
9
|
+
Agent,
|
|
10
|
+
type AgentOptions,
|
|
11
|
+
createRunResourceLedger,
|
|
12
|
+
markNonDispatchedToolEvent,
|
|
13
|
+
ThinkingLevel,
|
|
14
|
+
} from "@gajae-code/agent-core";
|
|
8
15
|
import { createAttemptMinter } from "@gajae-code/agent-core/attempt-scope";
|
|
9
16
|
import { CompactionCancelledError } from "@gajae-code/agent-core/compaction";
|
|
10
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
type AssistantMessage,
|
|
19
|
+
getBundledModel,
|
|
20
|
+
type ImageContent,
|
|
21
|
+
type Model,
|
|
22
|
+
type UserMessage,
|
|
23
|
+
} from "@gajae-code/ai/core";
|
|
11
24
|
import { createMockModel } from "@gajae-code/ai/providers/mock";
|
|
12
25
|
import { AssistantMessageEventStream } from "@gajae-code/ai/utils/event-stream";
|
|
13
26
|
import { logger } from "@gajae-code/utils";
|
|
@@ -17,7 +30,7 @@ import { ModelRegistry } from "../../config/model-registry";
|
|
|
17
30
|
import { Settings } from "../../config/settings";
|
|
18
31
|
import type { ExtensionAPI, ExtensionContext, ExtensionTranscriptEntry } from "../../extensibility/extensions";
|
|
19
32
|
import { ExtensionRunner } from "../../extensibility/extensions/runner";
|
|
20
|
-
import { AgentSession } from "../../session/agent-session";
|
|
33
|
+
import { AgentSession, type QueuedRunStartFailure, type SendUserMessageOptions } from "../../session/agent-session";
|
|
21
34
|
import { AuthStorage } from "../../session/auth-storage";
|
|
22
35
|
import { SessionManager } from "../../session/session-manager";
|
|
23
36
|
import {
|
|
@@ -38,14 +51,17 @@ import {
|
|
|
38
51
|
} from "../bus/reconciliation-store";
|
|
39
52
|
import { PromptDeadlineManager } from "../prompt-deadline-manager";
|
|
40
53
|
import { BROKER_RUNTIME_ABORT_CAPABILITY_FIELD, setBrokerRuntimeAbortCapabilityForTest } from "./control/runtime-gate";
|
|
41
|
-
import { SESSION_HOST_OBSERVER_CAPABILITY, TURN_STREAM_CAPABILITY } from "./host";
|
|
54
|
+
import { SESSION_HOST_OBSERVER_CAPABILITY, TOOL_ACTIVITY_CAPABILITY, TURN_STREAM_CAPABILITY } from "./host";
|
|
55
|
+
import { getPromptImageProcessBudgetSnapshot, PromptImageUploadStore } from "./prompt-image-upload";
|
|
42
56
|
import { CursorRegistry, QueryHandlers, type QueryResponse, RevisionStore } from "./query";
|
|
43
57
|
import {
|
|
44
58
|
type CreateSdkSessionRuntimeOptions,
|
|
45
59
|
createInvocationReconciliation,
|
|
46
60
|
createSdkSessionRuntimeExtension,
|
|
47
61
|
createSdkSurfaceFactory,
|
|
62
|
+
type PersistenceInstrumentationEvent,
|
|
48
63
|
RetainedTerminalBoundaryRegistry,
|
|
64
|
+
type SdkOnlyDeadlineRecoveryCheckpoint,
|
|
49
65
|
type SdkOnlyInvocationRecord,
|
|
50
66
|
type SdkOnlyReconciliationStore,
|
|
51
67
|
type SdkOnlyTerminalAbortSeams,
|
|
@@ -143,7 +159,11 @@ test("thinking.set uses the control setter and reports the applied session level
|
|
|
143
159
|
const transport = memoryTransport(frame => {
|
|
144
160
|
if (frame.type === "control_response" && frame.id === "thinking-set") response.resolve(frame);
|
|
145
161
|
});
|
|
146
|
-
createSdkSessionRuntimeExtension(api, {
|
|
162
|
+
createSdkSessionRuntimeExtension(api, {
|
|
163
|
+
agentDir: cwd,
|
|
164
|
+
ensureBrokerImpl: async () => testBrokerDiscovery,
|
|
165
|
+
createTransport: async () => transport,
|
|
166
|
+
});
|
|
147
167
|
const ctx = extensionContext(transport.sessionId, cwd);
|
|
148
168
|
try {
|
|
149
169
|
await handlers.get("session_start")?.({}, ctx);
|
|
@@ -1655,6 +1675,62 @@ describe("SessionSdkSessionRuntime", () => {
|
|
|
1655
1675
|
expect(transport.sent).toContainEqual({ type: "event", kind: "message_update", payload: {} });
|
|
1656
1676
|
await runtime.stop();
|
|
1657
1677
|
});
|
|
1678
|
+
test("applies authoritative replay capabilities before live delivery when the native callback lags", async () => {
|
|
1679
|
+
const replayResponse = Promise.withResolvers<SdkFrame>();
|
|
1680
|
+
const transport = memoryTransport(frame => {
|
|
1681
|
+
if (frame.type === "event_replay_result" && frame.id === "replay-before-capability-update")
|
|
1682
|
+
replayResponse.resolve(frame);
|
|
1683
|
+
});
|
|
1684
|
+
let negotiateCapabilities: ((connectionId: string, capabilities: readonly string[]) => void) | undefined;
|
|
1685
|
+
const hookedTransport: SessionSdkTransport = {
|
|
1686
|
+
...transport,
|
|
1687
|
+
onNegotiatedCapabilities(handler) {
|
|
1688
|
+
negotiateCapabilities = handler;
|
|
1689
|
+
return () => {
|
|
1690
|
+
if (negotiateCapabilities === handler) negotiateCapabilities = undefined;
|
|
1691
|
+
};
|
|
1692
|
+
},
|
|
1693
|
+
};
|
|
1694
|
+
const runtime = new SessionSdkSessionRuntime({ transport: hookedTransport });
|
|
1695
|
+
await runtime.start();
|
|
1696
|
+
try {
|
|
1697
|
+
if (!negotiateCapabilities) throw new Error("transport capability callback was not registered");
|
|
1698
|
+
const observerCapabilities = [
|
|
1699
|
+
TOOL_ACTIVITY_CAPABILITY,
|
|
1700
|
+
TURN_STREAM_CAPABILITY,
|
|
1701
|
+
SESSION_HOST_OBSERVER_CAPABILITY,
|
|
1702
|
+
];
|
|
1703
|
+
runtime.emitEvent({ kind: "tool_activity", payload: { phase: "started" } });
|
|
1704
|
+
transport.feed("observer", {
|
|
1705
|
+
type: "event_replay",
|
|
1706
|
+
id: "replay-before-capability-update",
|
|
1707
|
+
sinceGeneration: runtime.generation,
|
|
1708
|
+
sinceSeq: 0,
|
|
1709
|
+
capabilities: observerCapabilities,
|
|
1710
|
+
});
|
|
1711
|
+
const replay = await replayResponse.promise;
|
|
1712
|
+
expect(replay.events).toEqual(expect.arrayContaining([expect.objectContaining({ kind: "tool_activity" })]));
|
|
1713
|
+
const observerRequirements = [TURN_STREAM_CAPABILITY, SESSION_HOST_OBSERVER_CAPABILITY];
|
|
1714
|
+
expect(runtime.connectionIdsWithCapabilities(observerRequirements)).toEqual(["observer"]);
|
|
1715
|
+
|
|
1716
|
+
runtime.sendFrameToCapabilities(observerRequirements, {
|
|
1717
|
+
type: "event",
|
|
1718
|
+
kind: "tool_activity",
|
|
1719
|
+
payload: { phase: "updated" },
|
|
1720
|
+
});
|
|
1721
|
+
expect(transport.sent).toContainEqual({
|
|
1722
|
+
type: "event",
|
|
1723
|
+
kind: "tool_activity",
|
|
1724
|
+
payload: { phase: "updated" },
|
|
1725
|
+
});
|
|
1726
|
+
|
|
1727
|
+
// A delayed hello snapshot can omit event_replay additions; capability sets only grow per connection.
|
|
1728
|
+
negotiateCapabilities("observer", [TOOL_ACTIVITY_CAPABILITY]);
|
|
1729
|
+
expect(runtime.connectionIdsWithCapabilities(observerRequirements)).toEqual(["observer"]);
|
|
1730
|
+
} finally {
|
|
1731
|
+
await runtime.stop();
|
|
1732
|
+
}
|
|
1733
|
+
});
|
|
1658
1734
|
test("ignores late negotiated capabilities after the authoritative connection closes", async () => {
|
|
1659
1735
|
const transport = memoryTransport();
|
|
1660
1736
|
let closeConnection: ((connectionId: string) => void) | undefined;
|
|
@@ -4443,7 +4519,10 @@ describe("SessionSdkSessionRuntime", () => {
|
|
|
4443
4519
|
function createTestRuntimeExtension(api: ExtensionAPI, options: CreateSdkSessionRuntimeOptions): void {
|
|
4444
4520
|
createSdkSessionRuntimeExtension(api, {
|
|
4445
4521
|
...options,
|
|
4446
|
-
ensureBrokerImpl:
|
|
4522
|
+
ensureBrokerImpl: async input => {
|
|
4523
|
+
if (options.ensureBrokerImpl) return await options.ensureBrokerImpl(input);
|
|
4524
|
+
return testBrokerDiscovery;
|
|
4525
|
+
},
|
|
4447
4526
|
});
|
|
4448
4527
|
}
|
|
4449
4528
|
|
|
@@ -4543,6 +4622,7 @@ interface PreflightHooks {
|
|
|
4543
4622
|
expectedSdkRunToken?: string;
|
|
4544
4623
|
preflightSignal?: AbortSignal;
|
|
4545
4624
|
onQueuedPromoted?: (promotion: { startsOwnRun?: boolean; removed?: boolean }) => void;
|
|
4625
|
+
onQueuedRunStartFailed?: (failure: QueuedRunStartFailure) => void;
|
|
4546
4626
|
}
|
|
4547
4627
|
|
|
4548
4628
|
interface ResponseFrame {
|
|
@@ -4558,6 +4638,59 @@ interface ResponseFrame {
|
|
|
4558
4638
|
};
|
|
4559
4639
|
}
|
|
4560
4640
|
|
|
4641
|
+
const TEST_PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
4642
|
+
|
|
4643
|
+
function testPngCrc32(bytes: Uint8Array): number {
|
|
4644
|
+
let crc = 0xffffffff;
|
|
4645
|
+
for (const byte of bytes) {
|
|
4646
|
+
crc ^= byte;
|
|
4647
|
+
for (let bit = 0; bit < 8; bit++) crc = crc & 1 ? (crc >>> 1) ^ 0xedb88320 : crc >>> 1;
|
|
4648
|
+
}
|
|
4649
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
4650
|
+
}
|
|
4651
|
+
|
|
4652
|
+
function testPngChunk(type: string, data: Uint8Array): Buffer {
|
|
4653
|
+
const typeBytes = Buffer.from(type, "ascii");
|
|
4654
|
+
const payload = Buffer.from(data);
|
|
4655
|
+
const chunk = Buffer.alloc(12 + payload.length);
|
|
4656
|
+
chunk.writeUInt32BE(payload.length, 0);
|
|
4657
|
+
typeBytes.copy(chunk, 4);
|
|
4658
|
+
payload.copy(chunk, 8);
|
|
4659
|
+
chunk.writeUInt32BE(testPngCrc32(Buffer.concat([typeBytes, payload])), 8 + payload.length);
|
|
4660
|
+
return chunk;
|
|
4661
|
+
}
|
|
4662
|
+
|
|
4663
|
+
/** A valid deterministic 300x300 PNG whose source exceeds the old 256 KiB frame limit. */
|
|
4664
|
+
function makeTestPng(): Buffer {
|
|
4665
|
+
const width = 300;
|
|
4666
|
+
const height = 300;
|
|
4667
|
+
const ihdr = Buffer.alloc(13);
|
|
4668
|
+
ihdr.writeUInt32BE(width, 0);
|
|
4669
|
+
ihdr.writeUInt32BE(height, 4);
|
|
4670
|
+
ihdr[8] = 8;
|
|
4671
|
+
ihdr[9] = 6;
|
|
4672
|
+
const stride = 1 + width * 4;
|
|
4673
|
+
const pixels = Buffer.alloc(stride * height);
|
|
4674
|
+
let state = 0x12345678;
|
|
4675
|
+
for (let y = 0; y < height; y++) {
|
|
4676
|
+
const row = y * stride;
|
|
4677
|
+
for (let x = 0; x < width; x++) {
|
|
4678
|
+
const pixel = row + 1 + x * 4;
|
|
4679
|
+
for (let channel = 0; channel < 3; channel++) {
|
|
4680
|
+
state = (Math.imul(state, 1664525) + 1013904223) >>> 0;
|
|
4681
|
+
pixels[pixel + channel] = state >>> 24;
|
|
4682
|
+
}
|
|
4683
|
+
pixels[pixel + 3] = 255;
|
|
4684
|
+
}
|
|
4685
|
+
}
|
|
4686
|
+
return Buffer.concat([
|
|
4687
|
+
TEST_PNG_SIGNATURE,
|
|
4688
|
+
testPngChunk("IHDR", ihdr),
|
|
4689
|
+
testPngChunk("IDAT", deflateSync(pixels, { level: 0 })),
|
|
4690
|
+
testPngChunk("IEND", Buffer.alloc(0)),
|
|
4691
|
+
]);
|
|
4692
|
+
}
|
|
4693
|
+
|
|
4561
4694
|
interface InvocationHarness {
|
|
4562
4695
|
control(operation: string, input: Record<string, unknown>): Promise<ResponseFrame>;
|
|
4563
4696
|
controlAs(
|
|
@@ -4574,6 +4707,8 @@ interface InvocationHarness {
|
|
|
4574
4707
|
sendToSession(sessionId: string, frame: Record<string, unknown>): void;
|
|
4575
4708
|
sent(sessionId: string): SdkFrame[];
|
|
4576
4709
|
readonly broadcasts: SdkFrame[];
|
|
4710
|
+
connect(connectionId: string): void;
|
|
4711
|
+
disconnect(connectionId: string): void;
|
|
4577
4712
|
stop(): Promise<void>;
|
|
4578
4713
|
}
|
|
4579
4714
|
|
|
@@ -4590,6 +4725,8 @@ async function invocationHarness(
|
|
|
4590
4725
|
abort?: () => void;
|
|
4591
4726
|
isIdle?: () => boolean;
|
|
4592
4727
|
settings?: Settings;
|
|
4728
|
+
/** Install the authoritative test transport liveness and disconnect callbacks. */
|
|
4729
|
+
connectionLiveness?: boolean;
|
|
4593
4730
|
/** Throw to inject a durable-write failure for matching transitions. */
|
|
4594
4731
|
persistInterceptor?: (transition: { type: string }) => void;
|
|
4595
4732
|
/** Hold one matching durable transition until the test releases it. */
|
|
@@ -4605,10 +4742,13 @@ async function invocationHarness(
|
|
|
4605
4742
|
type: string | undefined;
|
|
4606
4743
|
commandId?: string;
|
|
4607
4744
|
turnId?: string;
|
|
4745
|
+
terminalAt?: number;
|
|
4608
4746
|
outcome: "rejected" | "committed";
|
|
4609
4747
|
}) => void;
|
|
4610
4748
|
branch?: unknown[];
|
|
4611
4749
|
onInvocationCompletionReconciled?: (kind: string, correlation: { commandId: string; turnId: string }) => void;
|
|
4750
|
+
/** Test-only instrumentation of the persistence chain. */
|
|
4751
|
+
onPersistenceInstrumentation?: (event: PersistenceInstrumentationEvent) => void;
|
|
4612
4752
|
/** Override/extend the INTERNAL terminal-abort seams the runtime is threaded. */
|
|
4613
4753
|
terminalAbortSeams?: Partial<SdkOnlyTerminalAbortSeams>;
|
|
4614
4754
|
},
|
|
@@ -4617,6 +4757,8 @@ async function invocationHarness(
|
|
|
4617
4757
|
const handlers = new Map<string, (event: unknown, ctx: unknown) => Promise<void> | void>();
|
|
4618
4758
|
const broadcasts: SdkFrame[] = [];
|
|
4619
4759
|
let deliver: ((connectionId: string, frame: SdkFrame) => void) | undefined;
|
|
4760
|
+
const openConnections = new Set<string>();
|
|
4761
|
+
let connectionCloseHandler: ((connectionId: string) => void) | undefined;
|
|
4620
4762
|
const deliveries = new Map<string, (connectionId: string, frame: SdkFrame) => void>();
|
|
4621
4763
|
const sentFrames = new Map<string, SdkFrame[]>();
|
|
4622
4764
|
let nextId = 0;
|
|
@@ -4640,14 +4782,20 @@ async function invocationHarness(
|
|
|
4640
4782
|
createTestRuntimeExtension(api, {
|
|
4641
4783
|
agentDir: cwd,
|
|
4642
4784
|
...(hooks.onLifecycleDrainTimeout ? { onLifecycleDrainTimeoutForTests: hooks.onLifecycleDrainTimeout } : {}),
|
|
4643
|
-
...(interceptorStore || hooks.terminalAbortSeams
|
|
4785
|
+
...(interceptorStore || hooks.terminalAbortSeams || hooks.onPersistenceInstrumentation || hooks.connectionLiveness
|
|
4644
4786
|
? {
|
|
4645
4787
|
terminalAbortSeams: {
|
|
4646
4788
|
getTerminalTurnEpoch: () => undefined,
|
|
4647
|
-
getActivePromptHandle: () => undefined,
|
|
4789
|
+
getActivePromptHandle: () => (hooks.connectionLiveness ? "test-image-run" : undefined),
|
|
4648
4790
|
cancelPendingPreflightForTerminalAbort: () => {},
|
|
4649
4791
|
abortPromptAndWaitWithTerminal: async () => ({ status: "settled", terminalScope: {} }),
|
|
4792
|
+
...(hooks.connectionLiveness
|
|
4793
|
+
? { waitForRunSettlement: async () => ({ status: "settled" as const }) }
|
|
4794
|
+
: {}),
|
|
4650
4795
|
...(interceptorStore ? { getReconciliationStore: () => interceptorStore } : {}),
|
|
4796
|
+
...(hooks.onPersistenceInstrumentation
|
|
4797
|
+
? { onPersistenceInstrumentationForTests: hooks.onPersistenceInstrumentation }
|
|
4798
|
+
: {}),
|
|
4651
4799
|
...hooks.terminalAbortSeams,
|
|
4652
4800
|
},
|
|
4653
4801
|
}
|
|
@@ -4671,6 +4819,17 @@ async function invocationHarness(
|
|
|
4671
4819
|
if (deliveries.get(id) === handler) deliveries.delete(id);
|
|
4672
4820
|
};
|
|
4673
4821
|
},
|
|
4822
|
+
...(hooks.connectionLiveness
|
|
4823
|
+
? {
|
|
4824
|
+
isConnectionOpen: (connectionId: string) => openConnections.has(connectionId),
|
|
4825
|
+
onConnectionClose(handler: (connectionId: string) => void) {
|
|
4826
|
+
connectionCloseHandler = handler;
|
|
4827
|
+
return () => {
|
|
4828
|
+
if (connectionCloseHandler === handler) connectionCloseHandler = undefined;
|
|
4829
|
+
};
|
|
4830
|
+
},
|
|
4831
|
+
}
|
|
4832
|
+
: {}),
|
|
4674
4833
|
sendFrame(_connectionId, frame) {
|
|
4675
4834
|
const response = frame as ResponseFrame;
|
|
4676
4835
|
const frames = sentFrames.get(id) ?? [];
|
|
@@ -4684,93 +4843,1382 @@ async function invocationHarness(
|
|
|
4684
4843
|
hooks.broadcastInterceptor?.(frame);
|
|
4685
4844
|
broadcasts.push(frame);
|
|
4686
4845
|
},
|
|
4687
|
-
start: async () => ({ url: "ws://127.0.0.1:1" }),
|
|
4688
|
-
stop: async () => {},
|
|
4689
|
-
}),
|
|
4846
|
+
start: async () => ({ url: "ws://127.0.0.1:1" }),
|
|
4847
|
+
stop: async () => {},
|
|
4848
|
+
}),
|
|
4849
|
+
});
|
|
4850
|
+
const ctx = {
|
|
4851
|
+
cwd,
|
|
4852
|
+
workflowGate: undefined,
|
|
4853
|
+
sdkBindings: () => (hooks.invokeSkill ? ["invokeSkill"] : []),
|
|
4854
|
+
isIdle: hooks.isIdle ?? (() => true),
|
|
4855
|
+
abort: hooks.abort ?? (() => {}),
|
|
4856
|
+
...(hooks.invokeSkill ? { invokeSkill: hooks.invokeSkill } : {}),
|
|
4857
|
+
sessionManager: {
|
|
4858
|
+
getSessionId: () => sessionId,
|
|
4859
|
+
getSessionFile: () => path.join(cwd, ".gjc", "state", `${sessionId}.jsonl`),
|
|
4860
|
+
getSessionName: () => undefined,
|
|
4861
|
+
getBranch: () => hooks.branch ?? [],
|
|
4862
|
+
},
|
|
4863
|
+
};
|
|
4864
|
+
await handlers.get("session_start")?.({}, ctx);
|
|
4865
|
+
const request = (frame: Record<string, unknown>, connectionId = "client"): Promise<ResponseFrame> => {
|
|
4866
|
+
const id = `frame-${nextId}`;
|
|
4867
|
+
nextId += 1;
|
|
4868
|
+
const { promise, resolve } = Promise.withResolvers<ResponseFrame>();
|
|
4869
|
+
waiters.set(id, resolve);
|
|
4870
|
+
deliver?.(connectionId, { ...frame, id } as SdkFrame);
|
|
4871
|
+
return promise;
|
|
4872
|
+
};
|
|
4873
|
+
return {
|
|
4874
|
+
control: (operation, input) => request({ type: "control_request", operation, input }),
|
|
4875
|
+
controlAs: (connectionId, operation, input, idempotencyKey) =>
|
|
4876
|
+
request(
|
|
4877
|
+
{
|
|
4878
|
+
type: "control_request",
|
|
4879
|
+
operation,
|
|
4880
|
+
input,
|
|
4881
|
+
...(idempotencyKey === undefined ? {} : { idempotencyKey }),
|
|
4882
|
+
},
|
|
4883
|
+
connectionId,
|
|
4884
|
+
),
|
|
4885
|
+
query: (name, input) => request({ type: "query_request", query: name, input }),
|
|
4886
|
+
broadcasts,
|
|
4887
|
+
connect: connectionId => {
|
|
4888
|
+
openConnections.add(connectionId);
|
|
4889
|
+
},
|
|
4890
|
+
disconnect: connectionId => {
|
|
4891
|
+
openConnections.delete(connectionId);
|
|
4892
|
+
connectionCloseHandler?.(connectionId);
|
|
4893
|
+
},
|
|
4894
|
+
emit: async (event, payload) => {
|
|
4895
|
+
await handlers.get(event)?.(payload ?? {}, ctx);
|
|
4896
|
+
},
|
|
4897
|
+
stop: async () => {
|
|
4898
|
+
await handlers.get("session_shutdown")?.({}, ctx);
|
|
4899
|
+
},
|
|
4900
|
+
switchSession: async sessionId => {
|
|
4901
|
+
await handlers.get("session_switch")?.(
|
|
4902
|
+
{},
|
|
4903
|
+
{
|
|
4904
|
+
...ctx,
|
|
4905
|
+
sessionManager: {
|
|
4906
|
+
...ctx.sessionManager,
|
|
4907
|
+
getSessionId: () => sessionId,
|
|
4908
|
+
getSessionFile: () => path.join(cwd, ".gjc", "state", `${sessionId}.jsonl`),
|
|
4909
|
+
},
|
|
4910
|
+
},
|
|
4911
|
+
);
|
|
4912
|
+
},
|
|
4913
|
+
branch: async sessionId => {
|
|
4914
|
+
await handlers.get("session_branch")?.(
|
|
4915
|
+
{},
|
|
4916
|
+
{
|
|
4917
|
+
...ctx,
|
|
4918
|
+
sessionManager: {
|
|
4919
|
+
...ctx.sessionManager,
|
|
4920
|
+
getSessionId: () => sessionId,
|
|
4921
|
+
getSessionFile: () => path.join(cwd, ".gjc", "state", `${sessionId}.jsonl`),
|
|
4922
|
+
},
|
|
4923
|
+
},
|
|
4924
|
+
);
|
|
4925
|
+
},
|
|
4926
|
+
requestOnSession: (sessionId, frame) => {
|
|
4927
|
+
const id = `frame-${nextId}`;
|
|
4928
|
+
nextId += 1;
|
|
4929
|
+
const { promise, resolve } = Promise.withResolvers<ResponseFrame>();
|
|
4930
|
+
waiters.set(id, resolve);
|
|
4931
|
+
deliveries.get(sessionId)?.("client", { ...frame, id } as SdkFrame);
|
|
4932
|
+
return promise;
|
|
4933
|
+
},
|
|
4934
|
+
sendToSession: (sessionId, frame) => {
|
|
4935
|
+
deliveries.get(sessionId)?.("client", frame as SdkFrame);
|
|
4936
|
+
},
|
|
4937
|
+
sent: sessionId => sentFrames.get(sessionId) ?? [],
|
|
4938
|
+
};
|
|
4939
|
+
}
|
|
4940
|
+
|
|
4941
|
+
async function stageRuntimeImage(harness: InvocationHarness, bytes: Buffer, connectionId = "client"): Promise<string> {
|
|
4942
|
+
const begun = await harness.controlAs(connectionId, "turn.image.begin", {
|
|
4943
|
+
mimeType: "image/png",
|
|
4944
|
+
byteLength: bytes.length,
|
|
4945
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
4946
|
+
});
|
|
4947
|
+
expect(begun).toMatchObject({ ok: true });
|
|
4948
|
+
const id = (begun.result as { id?: unknown } | undefined)?.id;
|
|
4949
|
+
if (typeof id !== "string") throw new Error("SDK runtime did not return a staged image ID.");
|
|
4950
|
+
let sequence = 0;
|
|
4951
|
+
for (let offset = 0; offset < bytes.length; offset += 96 * 1024) {
|
|
4952
|
+
const data = bytes.subarray(offset, offset + 96 * 1024).toString("base64");
|
|
4953
|
+
expect(Buffer.byteLength(JSON.stringify({ id, sequence, data }))).toBeLessThan(256 * 1024);
|
|
4954
|
+
expect(await harness.controlAs(connectionId, "turn.image.append", { id, sequence, data })).toMatchObject({
|
|
4955
|
+
ok: true,
|
|
4956
|
+
});
|
|
4957
|
+
sequence++;
|
|
4958
|
+
}
|
|
4959
|
+
expect(await harness.controlAs(connectionId, "turn.image.finish", { id })).toMatchObject({ ok: true });
|
|
4960
|
+
return id;
|
|
4961
|
+
}
|
|
4962
|
+
|
|
4963
|
+
test.each([
|
|
4964
|
+
"liveness",
|
|
4965
|
+
"settlement",
|
|
4966
|
+
] as const)("SDK-only custom transports without authoritative image ownership omit image controls (%s)", async missing => {
|
|
4967
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-no-image-transport-"));
|
|
4968
|
+
let harness: InvocationHarness | undefined;
|
|
4969
|
+
try {
|
|
4970
|
+
harness = await invocationHarness("sdk-no-image-transport", cwd, {
|
|
4971
|
+
connectionLiveness: missing !== "liveness",
|
|
4972
|
+
terminalAbortSeams: {
|
|
4973
|
+
waitForRunSettlement: missing === "settlement" ? undefined : async () => ({ status: "settled" }),
|
|
4974
|
+
},
|
|
4975
|
+
});
|
|
4976
|
+
harness.connect("client");
|
|
4977
|
+
const capabilities = await harness.query("runtime.capabilities", {});
|
|
4978
|
+
const operations = (capabilities as unknown as { page?: { items?: Array<{ operations?: string[] }> } }).page
|
|
4979
|
+
?.items?.[0]?.operations;
|
|
4980
|
+
expect(operations?.some(operation => operation.startsWith("turn.image."))).toBe(false);
|
|
4981
|
+
const response = await harness.control("turn.image.begin", {
|
|
4982
|
+
mimeType: "image/png",
|
|
4983
|
+
byteLength: 1,
|
|
4984
|
+
sha256: "0".repeat(64),
|
|
4985
|
+
});
|
|
4986
|
+
expect(response).toMatchObject({ ok: false, error: { code: "operation_not_session_owned" } });
|
|
4987
|
+
} finally {
|
|
4988
|
+
await harness?.stop();
|
|
4989
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4990
|
+
}
|
|
4991
|
+
});
|
|
4992
|
+
|
|
4993
|
+
test("SDK-only private staged images are connection-owned, one-shot, and reach the provider losslessly", async () => {
|
|
4994
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-provider-staged-image-"));
|
|
4995
|
+
const bytes = makeTestPng();
|
|
4996
|
+
expect(bytes.length).toBeGreaterThan(256 * 1024);
|
|
4997
|
+
const releaseCalls: string[] = [];
|
|
4998
|
+
const originalRedeem = PromptImageUploadStore.prototype.redeem;
|
|
4999
|
+
const redeemSpy = spyOn(PromptImageUploadStore.prototype, "redeem").mockImplementation(function (
|
|
5000
|
+
this: PromptImageUploadStore,
|
|
5001
|
+
owner,
|
|
5002
|
+
ids,
|
|
5003
|
+
) {
|
|
5004
|
+
const reservation = originalRedeem.call(this, owner, ids);
|
|
5005
|
+
return {
|
|
5006
|
+
images: reservation.images,
|
|
5007
|
+
release: () => {
|
|
5008
|
+
releaseCalls.push("released");
|
|
5009
|
+
reservation.release();
|
|
5010
|
+
},
|
|
5011
|
+
};
|
|
5012
|
+
});
|
|
5013
|
+
let harness: InvocationHarness | undefined;
|
|
5014
|
+
let session: AgentSession | undefined;
|
|
5015
|
+
let authStorage: AuthStorage | undefined;
|
|
5016
|
+
try {
|
|
5017
|
+
const providerCalled = Promise.withResolvers<void>();
|
|
5018
|
+
let providerImages: ImageContent[] = [];
|
|
5019
|
+
const mock = createMockModel({ responses: [{ content: ["The staged image reached the model."] }] });
|
|
5020
|
+
const real = await createTerminalizationSession(cwd, (model, context, options) => {
|
|
5021
|
+
providerImages = context.messages.flatMap(message =>
|
|
5022
|
+
message.role === "user" && Array.isArray(message.content)
|
|
5023
|
+
? message.content.filter((block): block is ImageContent => block.type === "image")
|
|
5024
|
+
: [],
|
|
5025
|
+
);
|
|
5026
|
+
providerCalled.resolve();
|
|
5027
|
+
return mock.stream(model, context, options);
|
|
5028
|
+
});
|
|
5029
|
+
session = real.session;
|
|
5030
|
+
authStorage = real.authStorage;
|
|
5031
|
+
const send = realSendUserMessage(session);
|
|
5032
|
+
harness = await invocationHarness("sdk-provider-staged-image", cwd, {
|
|
5033
|
+
connectionLiveness: true,
|
|
5034
|
+
terminalAbortSeams: {
|
|
5035
|
+
getActivePromptHandle: () => real.session.activePromptHandle,
|
|
5036
|
+
waitForRunSettlement: handle =>
|
|
5037
|
+
real.session.agent.resourceLedger.waitForSettlement(handle, { graceMs: null }),
|
|
5038
|
+
},
|
|
5039
|
+
sendUserMessage: (content, options) => send(content, options),
|
|
5040
|
+
});
|
|
5041
|
+
harness.connect("image-owner");
|
|
5042
|
+
harness.connect("image-observer");
|
|
5043
|
+
session.subscribe(async event => {
|
|
5044
|
+
await harness?.emit(event.type, event);
|
|
5045
|
+
});
|
|
5046
|
+
|
|
5047
|
+
const id = await stageRuntimeImage(harness, bytes, "image-owner");
|
|
5048
|
+
const forgedOwnerPrompt = await harness.controlAs("image-observer", "turn.prompt", {
|
|
5049
|
+
text: "A different authenticated connection cannot redeem this image.",
|
|
5050
|
+
stagedImages: [{ id }],
|
|
5051
|
+
});
|
|
5052
|
+
expect(forgedOwnerPrompt).toMatchObject({ ok: false, error: { code: "image_upload_unavailable" } });
|
|
5053
|
+
const mixedPrompt = await harness.controlAs("image-owner", "turn.prompt", {
|
|
5054
|
+
text: "Direct and staged image blocks cannot be mixed.",
|
|
5055
|
+
images: [{ data: "cG5n", mimeType: "image/png" }],
|
|
5056
|
+
stagedImages: [{ id }],
|
|
5057
|
+
});
|
|
5058
|
+
expect(mixedPrompt).toMatchObject({ ok: false, error: { code: "invalid_input" } });
|
|
5059
|
+
expect(releaseCalls).toEqual([]);
|
|
5060
|
+
|
|
5061
|
+
const accepted = await harness.controlAs("image-owner", "turn.prompt", {
|
|
5062
|
+
text: "Inspect this losslessly staged image.",
|
|
5063
|
+
stagedImages: [{ id }],
|
|
5064
|
+
});
|
|
5065
|
+
expect(accepted).toMatchObject({ ok: true, result: { accepted: true } });
|
|
5066
|
+
await providerCalled.promise;
|
|
5067
|
+
expect(providerImages).toEqual([{ type: "image", data: bytes.toString("base64"), mimeType: "image/png" }]);
|
|
5068
|
+
await session.waitForIdle();
|
|
5069
|
+
await settledStatus(harness, "turn.result", {
|
|
5070
|
+
kind: "prompt",
|
|
5071
|
+
commandId: accepted.result?.commandId,
|
|
5072
|
+
turnId: accepted.result?.turnId,
|
|
5073
|
+
});
|
|
5074
|
+
for (let attempt = 0; releaseCalls.length === 0 && attempt < 200; attempt++) await Bun.sleep(10);
|
|
5075
|
+
expect(releaseCalls).toHaveLength(1);
|
|
5076
|
+
expect(
|
|
5077
|
+
await harness.controlAs("image-owner", "turn.prompt", {
|
|
5078
|
+
text: "The image descriptor is one-shot.",
|
|
5079
|
+
stagedImages: [{ id }],
|
|
5080
|
+
}),
|
|
5081
|
+
).toMatchObject({ ok: false, error: { code: "image_upload_unavailable" } });
|
|
5082
|
+
|
|
5083
|
+
const rejectedId = await stageRuntimeImage(harness, bytes, "image-owner");
|
|
5084
|
+
expect(
|
|
5085
|
+
await harness.controlAs("image-owner", "turn.prompt", {
|
|
5086
|
+
text: "Rejected before admission.",
|
|
5087
|
+
clientRef: " ",
|
|
5088
|
+
stagedImages: [{ id: rejectedId }],
|
|
5089
|
+
}),
|
|
5090
|
+
).toMatchObject({ ok: false, error: { code: "invalid_input" } });
|
|
5091
|
+
// Invalid clientRef admission must not consume or release staged bytes.
|
|
5092
|
+
expect(releaseCalls).toHaveLength(1);
|
|
5093
|
+
expect(await harness.controlAs("image-owner", "turn.image.discard", { id: rejectedId })).toMatchObject({
|
|
5094
|
+
ok: true,
|
|
5095
|
+
});
|
|
5096
|
+
|
|
5097
|
+
const disconnectedId = await stageRuntimeImage(harness, bytes, "image-owner");
|
|
5098
|
+
harness.disconnect("image-owner");
|
|
5099
|
+
expect(await harness.controlAs("image-owner", "turn.image.finish", { id: disconnectedId })).toMatchObject({
|
|
5100
|
+
ok: false,
|
|
5101
|
+
error: { code: "resource_gone" },
|
|
5102
|
+
});
|
|
5103
|
+
} finally {
|
|
5104
|
+
await session?.waitForIdle().catch(() => undefined);
|
|
5105
|
+
await harness?.stop();
|
|
5106
|
+
await session?.dispose();
|
|
5107
|
+
authStorage?.close();
|
|
5108
|
+
redeemSpy.mockRestore();
|
|
5109
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5110
|
+
}
|
|
5111
|
+
});
|
|
5112
|
+
|
|
5113
|
+
test("SDK-only real queued image refusal before agent_start fails Q26 and retires only its reservation", async () => {
|
|
5114
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-real-queued-image-refusal-"));
|
|
5115
|
+
const before = getPromptImageProcessBudgetSnapshot();
|
|
5116
|
+
const entered = Promise.withResolvers<void>();
|
|
5117
|
+
const releasePredecessor = Promise.withResolvers<void>();
|
|
5118
|
+
const enqueueReturned = Promise.withResolvers<void>();
|
|
5119
|
+
const released = Promise.withResolvers<void>();
|
|
5120
|
+
let releases = 0;
|
|
5121
|
+
let providerCalls = 0;
|
|
5122
|
+
const actualRefusals: QueuedRunStartFailure[] = [];
|
|
5123
|
+
let session: AgentSession | undefined;
|
|
5124
|
+
let authStorage: AuthStorage | undefined;
|
|
5125
|
+
let harness: InvocationHarness | undefined;
|
|
5126
|
+
const originalRedeem = PromptImageUploadStore.prototype.redeem;
|
|
5127
|
+
const redeemSpy = spyOn(PromptImageUploadStore.prototype, "redeem").mockImplementation(function (
|
|
5128
|
+
this: PromptImageUploadStore,
|
|
5129
|
+
owner,
|
|
5130
|
+
ids,
|
|
5131
|
+
) {
|
|
5132
|
+
const reservation = originalRedeem.call(this, owner, ids);
|
|
5133
|
+
return {
|
|
5134
|
+
images: reservation.images,
|
|
5135
|
+
release: () => {
|
|
5136
|
+
releases++;
|
|
5137
|
+
reservation.release();
|
|
5138
|
+
released.resolve();
|
|
5139
|
+
},
|
|
5140
|
+
};
|
|
5141
|
+
});
|
|
5142
|
+
try {
|
|
5143
|
+
const real = await createTerminalizationSession(cwd, (model, context, options) => {
|
|
5144
|
+
const call = ++providerCalls;
|
|
5145
|
+
return createMockModel({
|
|
5146
|
+
responses: [
|
|
5147
|
+
async () => {
|
|
5148
|
+
if (call === 1) {
|
|
5149
|
+
entered.resolve();
|
|
5150
|
+
await releasePredecessor.promise;
|
|
5151
|
+
}
|
|
5152
|
+
return { content: ["independent completion"] };
|
|
5153
|
+
},
|
|
5154
|
+
],
|
|
5155
|
+
}).stream(model, context, options);
|
|
5156
|
+
});
|
|
5157
|
+
session = real.session;
|
|
5158
|
+
authStorage = real.authStorage;
|
|
5159
|
+
const send = realSendUserMessage(session);
|
|
5160
|
+
harness = await invocationHarness("real-queued-image-refusal", cwd, {
|
|
5161
|
+
connectionLiveness: true,
|
|
5162
|
+
isIdle: () => !real.session.isStreaming,
|
|
5163
|
+
terminalAbortSeams: {
|
|
5164
|
+
getActivePromptHandle: () => real.session.activePromptHandle,
|
|
5165
|
+
waitForRunSettlement: handle =>
|
|
5166
|
+
real.session.agent.resourceLedger.waitForSettlement(handle, { graceMs: null }),
|
|
5167
|
+
},
|
|
5168
|
+
sendUserMessage: async (content, options) => {
|
|
5169
|
+
// Exercise the real follow-up queue with the staged-image SDK prompt.
|
|
5170
|
+
const image = Array.isArray(content) && content.some(block => block.type === "image");
|
|
5171
|
+
await send(
|
|
5172
|
+
content,
|
|
5173
|
+
image
|
|
5174
|
+
? {
|
|
5175
|
+
...options,
|
|
5176
|
+
deliverAs: "followUp",
|
|
5177
|
+
onQueuedRunStartFailed: failure => {
|
|
5178
|
+
actualRefusals.push(failure);
|
|
5179
|
+
options?.onQueuedRunStartFailed?.(failure);
|
|
5180
|
+
},
|
|
5181
|
+
}
|
|
5182
|
+
: options,
|
|
5183
|
+
);
|
|
5184
|
+
if (image) enqueueReturned.resolve();
|
|
5185
|
+
},
|
|
5186
|
+
});
|
|
5187
|
+
harness.connect("client");
|
|
5188
|
+
session.subscribe(event => harness?.emit(event.type, event));
|
|
5189
|
+
expect(await harness.control("turn.prompt", { text: "hold predecessor" })).toMatchObject({ ok: true });
|
|
5190
|
+
await entered.promise;
|
|
5191
|
+
const id = await stageRuntimeImage(harness, makeTestPng());
|
|
5192
|
+
const accepted = await harness.control("turn.prompt", {
|
|
5193
|
+
text: "queued image requires a model",
|
|
5194
|
+
stagedImages: [{ id }],
|
|
5195
|
+
clientRef: "real-refused-image",
|
|
5196
|
+
});
|
|
5197
|
+
expect(accepted).toMatchObject({ ok: true, result: { accepted: true } });
|
|
5198
|
+
await enqueueReturned.promise;
|
|
5199
|
+
expect(releases).toBe(0);
|
|
5200
|
+
expect(getPromptImageProcessBudgetSnapshot().sourceBytes).toBeGreaterThan(before.sourceBytes);
|
|
5201
|
+
session.agent.setModel(undefined);
|
|
5202
|
+
releasePredecessor.resolve();
|
|
5203
|
+
const failed = await settledStatus(harness, "turn.result", { kind: "prompt", clientRef: "real-refused-image" });
|
|
5204
|
+
expect(failed).toMatchObject({
|
|
5205
|
+
status: "failed",
|
|
5206
|
+
outcome: { kind: "failed" },
|
|
5207
|
+
error: { code: "internal", message: "Prompt submission failed." },
|
|
5208
|
+
});
|
|
5209
|
+
await released.promise;
|
|
5210
|
+
await session.waitForIdle();
|
|
5211
|
+
expect(actualRefusals).toHaveLength(1);
|
|
5212
|
+
expect(actualRefusals[0]).toMatchObject({
|
|
5213
|
+
disposition: "not-started",
|
|
5214
|
+
error: { message: "No model configured" },
|
|
5215
|
+
});
|
|
5216
|
+
expect(providerCalls).toBe(1);
|
|
5217
|
+
expect(releases).toBe(1);
|
|
5218
|
+
expect(getPromptImageProcessBudgetSnapshot()).toEqual(before);
|
|
5219
|
+
// The same session store can stage/discard bytes again after retirement.
|
|
5220
|
+
const laterImage = await stageRuntimeImage(harness, makeTestPng());
|
|
5221
|
+
expect(await harness.control("turn.image.discard", { id: laterImage })).toMatchObject({ ok: true });
|
|
5222
|
+
expect(getPromptImageProcessBudgetSnapshot()).toEqual(before);
|
|
5223
|
+
session.agent.setModel(real.model);
|
|
5224
|
+
const fresh = await harness.control("turn.prompt", { text: "restored model starts independent work" });
|
|
5225
|
+
expect(
|
|
5226
|
+
await settledStatus(harness, "turn.result", {
|
|
5227
|
+
kind: "prompt",
|
|
5228
|
+
commandId: fresh.result?.commandId,
|
|
5229
|
+
turnId: fresh.result?.turnId,
|
|
5230
|
+
}),
|
|
5231
|
+
).toMatchObject({ status: "terminal_ok" });
|
|
5232
|
+
expect(await harness.query("turn.result", { kind: "prompt", clientRef: "real-refused-image" })).toMatchObject({
|
|
5233
|
+
result: { commandId: accepted.result?.commandId, turnId: accepted.result?.turnId, status: "failed" },
|
|
5234
|
+
});
|
|
5235
|
+
expect(providerCalls).toBe(2);
|
|
5236
|
+
expect(releases).toBe(1);
|
|
5237
|
+
} finally {
|
|
5238
|
+
releasePredecessor.resolve();
|
|
5239
|
+
await session?.waitForIdle().catch(() => undefined);
|
|
5240
|
+
await harness?.stop();
|
|
5241
|
+
await session?.dispose();
|
|
5242
|
+
authStorage?.close();
|
|
5243
|
+
redeemSpy.mockRestore();
|
|
5244
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5245
|
+
}
|
|
5246
|
+
});
|
|
5247
|
+
|
|
5248
|
+
test.each([
|
|
5249
|
+
"pending-producer",
|
|
5250
|
+
"returned-producer",
|
|
5251
|
+
"promoted-returned-producer",
|
|
5252
|
+
"post-start",
|
|
5253
|
+
"same-text-rejection",
|
|
5254
|
+
"late-refusal",
|
|
5255
|
+
] as const)("SDK-only queued image refusal respects producer and execution authority (%s)", async mode => {
|
|
5256
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-image-refusal-authority-"));
|
|
5257
|
+
const before = getPromptImageProcessBudgetSnapshot();
|
|
5258
|
+
const producer = Promise.withResolvers<void>();
|
|
5259
|
+
const returned = Promise.withResolvers<void>();
|
|
5260
|
+
const released = Promise.withResolvers<void>();
|
|
5261
|
+
const proofRequested = Promise.withResolvers<void>();
|
|
5262
|
+
const settlement = Promise.withResolvers<{ status: "settled" }>();
|
|
5263
|
+
let callbacks: PreflightHooks | undefined;
|
|
5264
|
+
let releases = 0;
|
|
5265
|
+
const proofHandles: string[] = [];
|
|
5266
|
+
const cleanupReservations: Array<() => void> = [];
|
|
5267
|
+
const originalRedeem = PromptImageUploadStore.prototype.redeem;
|
|
5268
|
+
const redeemSpy = spyOn(PromptImageUploadStore.prototype, "redeem").mockImplementation(function (
|
|
5269
|
+
this: PromptImageUploadStore,
|
|
5270
|
+
owner,
|
|
5271
|
+
ids,
|
|
5272
|
+
) {
|
|
5273
|
+
const reservation = originalRedeem.call(this, owner, ids);
|
|
5274
|
+
cleanupReservations.push(reservation.release);
|
|
5275
|
+
return {
|
|
5276
|
+
images: reservation.images,
|
|
5277
|
+
release: () => {
|
|
5278
|
+
releases++;
|
|
5279
|
+
reservation.release();
|
|
5280
|
+
released.resolve();
|
|
5281
|
+
},
|
|
5282
|
+
};
|
|
5283
|
+
});
|
|
5284
|
+
let harness: InvocationHarness | undefined;
|
|
5285
|
+
try {
|
|
5286
|
+
harness = await invocationHarness(`image-refusal-${mode}`, cwd, {
|
|
5287
|
+
connectionLiveness: true,
|
|
5288
|
+
// Deliberately idle snapshot, then actual queue diversion.
|
|
5289
|
+
isIdle: () => true,
|
|
5290
|
+
terminalAbortSeams: {
|
|
5291
|
+
getActivePromptHandle: () => (mode === "post-start" ? "started-image-run" : undefined),
|
|
5292
|
+
waitForRunSettlement: handle => {
|
|
5293
|
+
proofHandles.push(handle);
|
|
5294
|
+
proofRequested.resolve();
|
|
5295
|
+
return settlement.promise;
|
|
5296
|
+
},
|
|
5297
|
+
},
|
|
5298
|
+
sendUserMessage: async (_content, options) => {
|
|
5299
|
+
callbacks = options;
|
|
5300
|
+
await options?.onPreflightAcceptCommit?.();
|
|
5301
|
+
options?.onDispatchDisposition?.({ startsOwnRun: false });
|
|
5302
|
+
try {
|
|
5303
|
+
await producer.promise;
|
|
5304
|
+
} finally {
|
|
5305
|
+
returned.resolve();
|
|
5306
|
+
}
|
|
5307
|
+
},
|
|
5308
|
+
});
|
|
5309
|
+
harness.connect("client");
|
|
5310
|
+
const id = await stageRuntimeImage(harness, makeTestPng());
|
|
5311
|
+
const accepted = await harness.control("turn.prompt", {
|
|
5312
|
+
text: "image queue authority",
|
|
5313
|
+
stagedImages: [{ id }],
|
|
5314
|
+
clientRef: `authority-${mode}`,
|
|
5315
|
+
});
|
|
5316
|
+
expect(accepted).toMatchObject({ ok: true, result: { accepted: true } });
|
|
5317
|
+
if (mode === "promoted-returned-producer") callbacks?.onQueuedPromoted?.({ startsOwnRun: true });
|
|
5318
|
+
if (mode === "returned-producer" || mode === "promoted-returned-producer") {
|
|
5319
|
+
producer.resolve();
|
|
5320
|
+
await returned.promise;
|
|
5321
|
+
// Yield through the submit.then fallback: the diverted idle snapshot is not completion.
|
|
5322
|
+
expect(await harness.query("turn.result", { kind: "prompt", clientRef: `authority-${mode}` })).toMatchObject({
|
|
5323
|
+
result: { status: "accepted" },
|
|
5324
|
+
});
|
|
5325
|
+
}
|
|
5326
|
+
if (mode !== "promoted-returned-producer") callbacks?.onQueuedPromoted?.({ startsOwnRun: true });
|
|
5327
|
+
if (mode === "post-start") await harness.emit("agent_start");
|
|
5328
|
+
const failure: QueuedRunStartFailure = { disposition: "not-started", error: new Error("No model configured") };
|
|
5329
|
+
if (mode === "same-text-rejection" || mode === "late-refusal") {
|
|
5330
|
+
producer.reject(
|
|
5331
|
+
Object.assign(
|
|
5332
|
+
new Error("No model configured"),
|
|
5333
|
+
mode === "late-refusal" ? { code: "original_failure" } : {},
|
|
5334
|
+
),
|
|
5335
|
+
);
|
|
5336
|
+
} else {
|
|
5337
|
+
callbacks?.onQueuedRunStartFailed?.(failure);
|
|
5338
|
+
callbacks?.onQueuedRunStartFailed?.(failure);
|
|
5339
|
+
}
|
|
5340
|
+
if (mode === "post-start") {
|
|
5341
|
+
expect(await harness.query("turn.result", { kind: "prompt", clientRef: `authority-${mode}` })).toMatchObject({
|
|
5342
|
+
result: { status: "in_flight" },
|
|
5343
|
+
});
|
|
5344
|
+
await harness.emit("agent_end", { messages: [{ role: "assistant", stopReason: "stop", content: "done" }] });
|
|
5345
|
+
await proofRequested.promise;
|
|
5346
|
+
expect(proofHandles).toEqual(["started-image-run"]);
|
|
5347
|
+
expect(releases).toBe(0);
|
|
5348
|
+
settlement.resolve({ status: "settled" });
|
|
5349
|
+
} else {
|
|
5350
|
+
expect(
|
|
5351
|
+
await settledStatus(harness, "turn.result", { kind: "prompt", clientRef: `authority-${mode}` }),
|
|
5352
|
+
).toMatchObject({
|
|
5353
|
+
status: "failed",
|
|
5354
|
+
outcome: { kind: "failed" },
|
|
5355
|
+
error: {
|
|
5356
|
+
code: mode === "late-refusal" ? "original_failure" : "internal",
|
|
5357
|
+
message: "Prompt submission failed.",
|
|
5358
|
+
},
|
|
5359
|
+
});
|
|
5360
|
+
if (mode !== "returned-producer" && mode !== "promoted-returned-producer") {
|
|
5361
|
+
expect(releases).toBe(0);
|
|
5362
|
+
expect(getPromptImageProcessBudgetSnapshot().sourceBytes).toBeGreaterThan(before.sourceBytes);
|
|
5363
|
+
}
|
|
5364
|
+
}
|
|
5365
|
+
producer.resolve();
|
|
5366
|
+
await returned.promise;
|
|
5367
|
+
if (mode === "late-refusal") callbacks?.onQueuedRunStartFailed?.(failure);
|
|
5368
|
+
if (mode === "same-text-rejection") {
|
|
5369
|
+
// A text-identical generic rejection is not proof; teardown cannot release either.
|
|
5370
|
+
await harness.stop();
|
|
5371
|
+
expect(releases).toBe(0);
|
|
5372
|
+
expect(proofHandles).toEqual([]);
|
|
5373
|
+
} else {
|
|
5374
|
+
await released.promise;
|
|
5375
|
+
expect(releases).toBe(1);
|
|
5376
|
+
expect(getPromptImageProcessBudgetSnapshot()).toEqual(before);
|
|
5377
|
+
callbacks?.onQueuedRunStartFailed?.(failure);
|
|
5378
|
+
if (mode !== "post-start") callbacks?.onQueuedPromoted?.({ startsOwnRun: true });
|
|
5379
|
+
await harness.emit("agent_start");
|
|
5380
|
+
await harness.emit("agent_end", { messages: [{ role: "assistant", stopReason: "stop", content: "later" }] });
|
|
5381
|
+
expect(await harness.query("turn.result", { kind: "prompt", clientRef: `authority-${mode}` })).toMatchObject({
|
|
5382
|
+
result: { status: mode === "post-start" ? "terminal_ok" : "failed" },
|
|
5383
|
+
});
|
|
5384
|
+
expect(releases).toBe(1);
|
|
5385
|
+
if (mode === "late-refusal")
|
|
5386
|
+
expect(
|
|
5387
|
+
await harness.query("turn.result", { kind: "prompt", clientRef: `authority-${mode}` }),
|
|
5388
|
+
).toMatchObject({ result: { error: { code: "original_failure" } } });
|
|
5389
|
+
}
|
|
5390
|
+
} finally {
|
|
5391
|
+
producer.resolve();
|
|
5392
|
+
settlement.resolve({ status: "settled" });
|
|
5393
|
+
await harness?.stop().catch(() => undefined);
|
|
5394
|
+
for (const release of cleanupReservations) release();
|
|
5395
|
+
redeemSpy.mockRestore();
|
|
5396
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5397
|
+
}
|
|
5398
|
+
});
|
|
5399
|
+
|
|
5400
|
+
test("SDK-only staged prompts reconcile clientRef before redeeming during preflight and in-flight execution", async () => {
|
|
5401
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-staged-client-ref-"));
|
|
5402
|
+
const allowAcceptance = Promise.withResolvers<void>();
|
|
5403
|
+
const producer = Promise.withResolvers<void>();
|
|
5404
|
+
const entered = Promise.withResolvers<void>();
|
|
5405
|
+
const originalRedeem = PromptImageUploadStore.prototype.redeem;
|
|
5406
|
+
let releases = 0;
|
|
5407
|
+
let sends = 0;
|
|
5408
|
+
const redeemSpy = spyOn(PromptImageUploadStore.prototype, "redeem").mockImplementation(function (
|
|
5409
|
+
this: PromptImageUploadStore,
|
|
5410
|
+
owner,
|
|
5411
|
+
ids,
|
|
5412
|
+
) {
|
|
5413
|
+
const reservation = originalRedeem.call(this, owner, ids);
|
|
5414
|
+
return {
|
|
5415
|
+
images: reservation.images,
|
|
5416
|
+
release: () => {
|
|
5417
|
+
releases++;
|
|
5418
|
+
reservation.release();
|
|
5419
|
+
},
|
|
5420
|
+
};
|
|
5421
|
+
});
|
|
5422
|
+
let harness: InvocationHarness | undefined;
|
|
5423
|
+
try {
|
|
5424
|
+
harness = await invocationHarness("staged-client-ref", cwd, {
|
|
5425
|
+
connectionLiveness: true,
|
|
5426
|
+
sendUserMessage: async (_content, options) => {
|
|
5427
|
+
sends++;
|
|
5428
|
+
entered.resolve();
|
|
5429
|
+
await allowAcceptance.promise;
|
|
5430
|
+
await options?.onPreflightAcceptCommit?.();
|
|
5431
|
+
await producer.promise;
|
|
5432
|
+
},
|
|
5433
|
+
});
|
|
5434
|
+
harness.connect("image-owner");
|
|
5435
|
+
harness.connect("reconnecting-client");
|
|
5436
|
+
const id = await stageRuntimeImage(harness, makeTestPng(), "image-owner");
|
|
5437
|
+
const input = { text: "Original staged producer", clientRef: "staged-ref", stagedImages: [{ id }] };
|
|
5438
|
+
const first = harness.controlAs("image-owner", "turn.prompt", input);
|
|
5439
|
+
void first.catch(() => {});
|
|
5440
|
+
await entered.promise;
|
|
5441
|
+
// Ordered controls serialize per session, even on separate connections.
|
|
5442
|
+
// A retry arriving during preflight cannot finish before the first receipt.
|
|
5443
|
+
const duplicate = harness.controlAs("reconnecting-client", "turn.prompt", input);
|
|
5444
|
+
void duplicate.catch(() => {});
|
|
5445
|
+
expect(redeemSpy.mock.calls).toHaveLength(1);
|
|
5446
|
+
expect(sends).toBe(1);
|
|
5447
|
+
expect(releases).toBe(0);
|
|
5448
|
+
allowAcceptance.resolve();
|
|
5449
|
+
const accepted = await first;
|
|
5450
|
+
expect(accepted).toMatchObject({ ok: true, result: { accepted: true, clientRef: "staged-ref" } });
|
|
5451
|
+
expect(await duplicate).toMatchObject({ ok: false, error: { code: "client_ref_conflict" } });
|
|
5452
|
+
await harness.emit("agent_start");
|
|
5453
|
+
// The original receipt may have been lost. Reconnection never replays it,
|
|
5454
|
+
// and cannot replace the original producer's retained image ownership.
|
|
5455
|
+
expect(await harness.controlAs("image-owner", "turn.prompt", input)).toMatchObject({
|
|
5456
|
+
ok: false,
|
|
5457
|
+
error: { code: "client_ref_conflict" },
|
|
5458
|
+
});
|
|
5459
|
+
expect(await harness.query("turn.result", { kind: "prompt", clientRef: "staged-ref" })).toMatchObject({
|
|
5460
|
+
result: { commandId: accepted.result?.commandId, turnId: accepted.result?.turnId, status: "in_flight" },
|
|
5461
|
+
});
|
|
5462
|
+
expect(redeemSpy.mock.calls).toHaveLength(1);
|
|
5463
|
+
expect(sends).toBe(1);
|
|
5464
|
+
expect(releases).toBe(0);
|
|
5465
|
+
await harness.emit("agent_end", {
|
|
5466
|
+
messages: [{ role: "assistant", stopReason: "stop", content: "done" }],
|
|
5467
|
+
});
|
|
5468
|
+
producer.resolve();
|
|
5469
|
+
await settledStatus(harness, "turn.result", { kind: "prompt", clientRef: "staged-ref" });
|
|
5470
|
+
expect(await harness.controlAs("image-owner", "turn.prompt", input)).toMatchObject({
|
|
5471
|
+
ok: false,
|
|
5472
|
+
error: { code: "client_ref_conflict" },
|
|
5473
|
+
});
|
|
5474
|
+
expect(redeemSpy.mock.calls).toHaveLength(1);
|
|
5475
|
+
expect(sends).toBe(1);
|
|
5476
|
+
} finally {
|
|
5477
|
+
allowAcceptance.resolve();
|
|
5478
|
+
producer.resolve();
|
|
5479
|
+
try {
|
|
5480
|
+
await harness?.stop();
|
|
5481
|
+
} finally {
|
|
5482
|
+
redeemSpy.mockRestore();
|
|
5483
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5484
|
+
}
|
|
5485
|
+
}
|
|
5486
|
+
});
|
|
5487
|
+
|
|
5488
|
+
test("SDK-only staged image reservations follow the exact consuming run across disconnect and unrelated end", async () => {
|
|
5489
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-staged-image-run-owner-"));
|
|
5490
|
+
const bytes = makeTestPng();
|
|
5491
|
+
const releases: string[] = [];
|
|
5492
|
+
const originalRedeem = PromptImageUploadStore.prototype.redeem;
|
|
5493
|
+
const redeemSpy = spyOn(PromptImageUploadStore.prototype, "redeem").mockImplementation(function (
|
|
5494
|
+
this: PromptImageUploadStore,
|
|
5495
|
+
owner,
|
|
5496
|
+
ids,
|
|
5497
|
+
) {
|
|
5498
|
+
const reservation = originalRedeem.call(this, owner, ids);
|
|
5499
|
+
return {
|
|
5500
|
+
images: reservation.images,
|
|
5501
|
+
release: () => {
|
|
5502
|
+
releases.push("released");
|
|
5503
|
+
reservation.release();
|
|
5504
|
+
},
|
|
5505
|
+
};
|
|
5506
|
+
});
|
|
5507
|
+
let harness: InvocationHarness | undefined;
|
|
5508
|
+
const imagePromotions: Array<PreflightHooks["onQueuedPromoted"]> = [];
|
|
5509
|
+
let promptDeadlineMs = 60_000;
|
|
5510
|
+
try {
|
|
5511
|
+
harness = await invocationHarness("staged-image-run-owner", cwd, {
|
|
5512
|
+
connectionLiveness: true,
|
|
5513
|
+
settings: {
|
|
5514
|
+
get: (key: string) =>
|
|
5515
|
+
key === "sdk.promptDeadlineMs"
|
|
5516
|
+
? promptDeadlineMs
|
|
5517
|
+
: key === "sdk.promptMaxRuntimeMs"
|
|
5518
|
+
? 60_000
|
|
5519
|
+
: undefined,
|
|
5520
|
+
} as unknown as Settings,
|
|
5521
|
+
sendUserMessage: async (content, options) => {
|
|
5522
|
+
await options?.onPreflightAcceptCommit?.();
|
|
5523
|
+
if (content === "root run") await neverSettlingPromise();
|
|
5524
|
+
if (options?.deliverAs === "followUp") {
|
|
5525
|
+
options.onDispatchDisposition?.({ startsOwnRun: false });
|
|
5526
|
+
options.preflightSignal?.addEventListener("abort", () => options.onQueuedPromoted?.({ removed: true }), {
|
|
5527
|
+
once: true,
|
|
5528
|
+
});
|
|
5529
|
+
return;
|
|
5530
|
+
}
|
|
5531
|
+
if (Array.isArray(content)) {
|
|
5532
|
+
expect(
|
|
5533
|
+
content.some(block => typeof block === "object" && block !== null && block.type === "image"),
|
|
5534
|
+
).toBe(true);
|
|
5535
|
+
options?.onDispatchDisposition?.({ startsOwnRun: false });
|
|
5536
|
+
imagePromotions.push(options?.onQueuedPromoted);
|
|
5537
|
+
}
|
|
5538
|
+
},
|
|
5539
|
+
});
|
|
5540
|
+
harness.connect("root-owner");
|
|
5541
|
+
harness.connect("image-owner");
|
|
5542
|
+
harness.connect("cancel-owner");
|
|
5543
|
+
const root = await harness.controlAs("root-owner", "turn.prompt", { text: "root run" });
|
|
5544
|
+
expect(root).toMatchObject({ ok: true, result: { accepted: true } });
|
|
5545
|
+
await harness.emit("agent_start");
|
|
5546
|
+
const id = await stageRuntimeImage(harness, bytes, "image-owner");
|
|
5547
|
+
const image = await harness.controlAs("image-owner", "turn.prompt", {
|
|
5548
|
+
text: "queued image run",
|
|
5549
|
+
stagedImages: [{ id }],
|
|
5550
|
+
});
|
|
5551
|
+
expect(image).toMatchObject({ ok: true, result: { accepted: true } });
|
|
5552
|
+
const correlation = { commandId: image.result?.commandId, turnId: image.result?.turnId };
|
|
5553
|
+
const cancelledId = await stageRuntimeImage(harness, bytes, "cancel-owner");
|
|
5554
|
+
const cancelled = await harness.controlAs("cancel-owner", "turn.prompt", {
|
|
5555
|
+
text: "queued image that will be removed",
|
|
5556
|
+
stagedImages: [{ id: cancelledId }],
|
|
5557
|
+
});
|
|
5558
|
+
expect(cancelled).toMatchObject({ ok: true, result: { accepted: true } });
|
|
5559
|
+
const cancelledCorrelation = { commandId: cancelled.result?.commandId, turnId: cancelled.result?.turnId };
|
|
5560
|
+
expect(imagePromotions).toHaveLength(2);
|
|
5561
|
+
expect(releases).toEqual([]);
|
|
5562
|
+
|
|
5563
|
+
// The stage lease was redeemed already. Losing its source connection and
|
|
5564
|
+
// completing the unrelated root run must not release quota owned by the
|
|
5565
|
+
// still-queued image submission.
|
|
5566
|
+
harness.disconnect("image-owner");
|
|
5567
|
+
await harness.emit("agent_end", {
|
|
5568
|
+
messages: [{ role: "assistant", stopReason: "stop", content: "root finished" }],
|
|
5569
|
+
});
|
|
5570
|
+
expect(releases).toEqual([]);
|
|
5571
|
+
|
|
5572
|
+
imagePromotions[1]?.({ startsOwnRun: false, removed: true });
|
|
5573
|
+
expect(await settledStatus(harness, "turn.result", { kind: "prompt", ...cancelledCorrelation })).toMatchObject({
|
|
5574
|
+
status: "failed",
|
|
5575
|
+
error: { code: "cancelled" },
|
|
5576
|
+
});
|
|
5577
|
+
expect(releases).toEqual(["released"]);
|
|
5578
|
+
|
|
5579
|
+
imagePromotions[0]?.({ startsOwnRun: true });
|
|
5580
|
+
await harness.emit("agent_start");
|
|
5581
|
+
expect(releases).toEqual(["released"]);
|
|
5582
|
+
promptDeadlineMs = 25;
|
|
5583
|
+
const unrelatedFollowUp = await harness.controlAs("cancel-owner", "turn.follow_up", {
|
|
5584
|
+
text: "unrelated follow-up past its admission deadline",
|
|
5585
|
+
});
|
|
5586
|
+
expect(unrelatedFollowUp).toMatchObject({ ok: true, result: { accepted: true } });
|
|
5587
|
+
await Bun.sleep(100);
|
|
5588
|
+
expect(
|
|
5589
|
+
await harness.query("turn.result", {
|
|
5590
|
+
kind: "prompt",
|
|
5591
|
+
commandId: unrelatedFollowUp.result?.commandId,
|
|
5592
|
+
turnId: unrelatedFollowUp.result?.turnId,
|
|
5593
|
+
}),
|
|
5594
|
+
).toMatchObject({ result: { status: "accepted" } });
|
|
5595
|
+
expect(releases).toEqual(["released"]);
|
|
5596
|
+
await harness.emit("agent_end", {
|
|
5597
|
+
messages: [{ role: "assistant", stopReason: "stop", content: "image finished" }],
|
|
5598
|
+
});
|
|
5599
|
+
expect(await settledStatus(harness, "turn.result", { kind: "prompt", ...correlation })).toMatchObject({
|
|
5600
|
+
status: "terminal_ok",
|
|
5601
|
+
});
|
|
5602
|
+
expect(releases).toEqual(["released", "released"]);
|
|
5603
|
+
} finally {
|
|
5604
|
+
try {
|
|
5605
|
+
await harness?.stop();
|
|
5606
|
+
} finally {
|
|
5607
|
+
redeemSpy.mockRestore();
|
|
5608
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5609
|
+
}
|
|
5610
|
+
}
|
|
5611
|
+
});
|
|
5612
|
+
|
|
5613
|
+
test("SDK-only terminal preflight abort retains staged-image quota until submission settlement", async () => {
|
|
5614
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-image-preflight-abort-"));
|
|
5615
|
+
const sessionId = "image-preflight-abort";
|
|
5616
|
+
const sessionFile = path.join(cwd, "session.json");
|
|
5617
|
+
await Bun.write(sessionFile, "");
|
|
5618
|
+
const reconciliationStore = createReconciliationStore({ sessionFile, sessionId });
|
|
5619
|
+
const producer = Promise.withResolvers<void>();
|
|
5620
|
+
const submissionStarted = Promise.withResolvers<void>();
|
|
5621
|
+
const preflightAborted = Promise.withResolvers<void>();
|
|
5622
|
+
const released = Promise.withResolvers<void>();
|
|
5623
|
+
let releases = 0;
|
|
5624
|
+
const originalRedeem = PromptImageUploadStore.prototype.redeem;
|
|
5625
|
+
const redeemSpy = spyOn(PromptImageUploadStore.prototype, "redeem").mockImplementation(function (
|
|
5626
|
+
this: PromptImageUploadStore,
|
|
5627
|
+
owner,
|
|
5628
|
+
ids,
|
|
5629
|
+
) {
|
|
5630
|
+
const reservation = originalRedeem.call(this, owner, ids);
|
|
5631
|
+
return {
|
|
5632
|
+
images: reservation.images,
|
|
5633
|
+
release: () => {
|
|
5634
|
+
releases++;
|
|
5635
|
+
reservation.release();
|
|
5636
|
+
released.resolve();
|
|
5637
|
+
},
|
|
5638
|
+
};
|
|
5639
|
+
});
|
|
5640
|
+
let harness: InvocationHarness | undefined;
|
|
5641
|
+
try {
|
|
5642
|
+
harness = await invocationHarness(sessionId, cwd, {
|
|
5643
|
+
connectionLiveness: true,
|
|
5644
|
+
terminalAbortSeams: {
|
|
5645
|
+
getReconciliationStore: () => reconciliationStore,
|
|
5646
|
+
getTerminalTurnEpoch: () => undefined,
|
|
5647
|
+
getActivePromptHandle: () => undefined,
|
|
5648
|
+
cancelPendingPreflightForTerminalAbort: () => {},
|
|
5649
|
+
abortPromptAndWaitWithTerminal: async () => {
|
|
5650
|
+
throw new Error("idle preflight abort must not stop an active run");
|
|
5651
|
+
},
|
|
5652
|
+
},
|
|
5653
|
+
sendUserMessage: async (content, options) => {
|
|
5654
|
+
expect(Array.isArray(content) && content.some(block => block.type === "image")).toBe(true);
|
|
5655
|
+
options?.preflightSignal?.addEventListener("abort", () => preflightAborted.resolve(), { once: true });
|
|
5656
|
+
submissionStarted.resolve();
|
|
5657
|
+
await producer.promise;
|
|
5658
|
+
expect(Array.isArray(content) && content.some(block => block.type === "image")).toBe(true);
|
|
5659
|
+
},
|
|
5660
|
+
});
|
|
5661
|
+
harness.connect("client");
|
|
5662
|
+
const id = await stageRuntimeImage(harness, makeTestPng());
|
|
5663
|
+
const prompt = harness.control("turn.prompt", {
|
|
5664
|
+
text: "hold the pending staged-image producer",
|
|
5665
|
+
stagedImages: [{ id }],
|
|
5666
|
+
});
|
|
5667
|
+
await submissionStarted.promise;
|
|
5668
|
+
const abort = await harness.controlAs("client", "turn.abort", { mode: "terminal" }, "image-preflight-abort-key");
|
|
5669
|
+
expect(abort).toMatchObject({
|
|
5670
|
+
ok: true,
|
|
5671
|
+
result: { turn: "no_active_turn", terminal: "terminal_no_effect" },
|
|
5672
|
+
});
|
|
5673
|
+
expect(await prompt).toMatchObject({ ok: false, error: { code: "busy" } });
|
|
5674
|
+
await preflightAborted.promise;
|
|
5675
|
+
expect(releases).toBe(0);
|
|
5676
|
+
|
|
5677
|
+
producer.resolve();
|
|
5678
|
+
await released.promise;
|
|
5679
|
+
expect(releases).toBe(1);
|
|
5680
|
+
} finally {
|
|
5681
|
+
producer.resolve();
|
|
5682
|
+
try {
|
|
5683
|
+
await harness?.stop();
|
|
5684
|
+
} finally {
|
|
5685
|
+
redeemSpy.mockRestore();
|
|
5686
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5687
|
+
}
|
|
5688
|
+
}
|
|
5689
|
+
});
|
|
5690
|
+
|
|
5691
|
+
test("SDK-only shutdown after acceptance retains unadopted staged-image quota until submission settlement", async () => {
|
|
5692
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-image-accepted-unadopted-"));
|
|
5693
|
+
const producer = Promise.withResolvers<void>();
|
|
5694
|
+
const released = Promise.withResolvers<void>();
|
|
5695
|
+
let releases = 0;
|
|
5696
|
+
let preflightSignal: AbortSignal | undefined;
|
|
5697
|
+
const originalRedeem = PromptImageUploadStore.prototype.redeem;
|
|
5698
|
+
const redeemSpy = spyOn(PromptImageUploadStore.prototype, "redeem").mockImplementation(function (
|
|
5699
|
+
this: PromptImageUploadStore,
|
|
5700
|
+
owner,
|
|
5701
|
+
ids,
|
|
5702
|
+
) {
|
|
5703
|
+
const reservation = originalRedeem.call(this, owner, ids);
|
|
5704
|
+
return {
|
|
5705
|
+
images: reservation.images,
|
|
5706
|
+
release: () => {
|
|
5707
|
+
releases++;
|
|
5708
|
+
reservation.release();
|
|
5709
|
+
released.resolve();
|
|
5710
|
+
},
|
|
5711
|
+
};
|
|
5712
|
+
});
|
|
5713
|
+
let harness: InvocationHarness | undefined;
|
|
5714
|
+
try {
|
|
5715
|
+
harness = await invocationHarness("image-accepted-unadopted", cwd, {
|
|
5716
|
+
connectionLiveness: true,
|
|
5717
|
+
sendUserMessage: async (content, options) => {
|
|
5718
|
+
preflightSignal = options?.preflightSignal;
|
|
5719
|
+
await options?.onPreflightAcceptCommit?.();
|
|
5720
|
+
await producer.promise;
|
|
5721
|
+
expect(Array.isArray(content) && content.some(block => block.type === "image")).toBe(true);
|
|
5722
|
+
},
|
|
5723
|
+
});
|
|
5724
|
+
harness.connect("client");
|
|
5725
|
+
const id = await stageRuntimeImage(harness, makeTestPng());
|
|
5726
|
+
const accepted = await harness.control("turn.prompt", {
|
|
5727
|
+
text: "accepted producer has not adopted an execution",
|
|
5728
|
+
stagedImages: [{ id }],
|
|
5729
|
+
});
|
|
5730
|
+
expect(accepted).toMatchObject({ ok: true, result: { accepted: true } });
|
|
5731
|
+
expect(releases).toBe(0);
|
|
5732
|
+
await harness.stop();
|
|
5733
|
+
expect(preflightSignal?.aborted).toBe(true);
|
|
5734
|
+
expect(releases).toBe(0);
|
|
5735
|
+
producer.resolve();
|
|
5736
|
+
await released.promise;
|
|
5737
|
+
expect(releases).toBe(1);
|
|
5738
|
+
} finally {
|
|
5739
|
+
producer.resolve();
|
|
5740
|
+
try {
|
|
5741
|
+
await harness?.stop();
|
|
5742
|
+
} finally {
|
|
5743
|
+
redeemSpy.mockRestore();
|
|
5744
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5745
|
+
}
|
|
5746
|
+
}
|
|
5747
|
+
});
|
|
5748
|
+
|
|
5749
|
+
test("SDK-only shutdown during durable preflight acceptance retains staged-image quota until submission settlement", async () => {
|
|
5750
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-image-shutdown-preflight-"));
|
|
5751
|
+
const sessionId = "image-shutdown-preflight";
|
|
5752
|
+
const sessionFile = path.join(cwd, "session.json");
|
|
5753
|
+
await Bun.write(sessionFile, "");
|
|
5754
|
+
const store = createReconciliationStore({ sessionFile, sessionId });
|
|
5755
|
+
const clientRef = "image-shutdown-preflight-client";
|
|
5756
|
+
const producer = Promise.withResolvers<void>();
|
|
5757
|
+
const acceptanceHeld = Promise.withResolvers<void>();
|
|
5758
|
+
const resumeAcceptance = Promise.withResolvers<void>();
|
|
5759
|
+
const acceptanceResumed = Promise.withResolvers<void>();
|
|
5760
|
+
const submissionFinished = Promise.withResolvers<void>();
|
|
5761
|
+
const released = Promise.withResolvers<void>();
|
|
5762
|
+
let submissionEntered = false;
|
|
5763
|
+
let held = false;
|
|
5764
|
+
let releases = 0;
|
|
5765
|
+
const originalRename = fsPromises.rename;
|
|
5766
|
+
const renameSpy = spyOn(fsPromises, "rename").mockImplementation(async (from, to) => {
|
|
5767
|
+
if (!held && String(to) === store.path) {
|
|
5768
|
+
const document = (await Bun.file(String(from)).json()) as ReconciliationStoreDocument;
|
|
5769
|
+
if (document.records.some(record => record.clientRef === clientRef && record.status === "accepted")) {
|
|
5770
|
+
held = true;
|
|
5771
|
+
acceptanceHeld.resolve();
|
|
5772
|
+
await resumeAcceptance.promise;
|
|
5773
|
+
}
|
|
5774
|
+
}
|
|
5775
|
+
await originalRename(from, to);
|
|
5776
|
+
});
|
|
5777
|
+
const originalRedeem = PromptImageUploadStore.prototype.redeem;
|
|
5778
|
+
const redeemSpy = spyOn(PromptImageUploadStore.prototype, "redeem").mockImplementation(function (
|
|
5779
|
+
this: PromptImageUploadStore,
|
|
5780
|
+
owner,
|
|
5781
|
+
ids,
|
|
5782
|
+
) {
|
|
5783
|
+
const reservation = originalRedeem.call(this, owner, ids);
|
|
5784
|
+
return {
|
|
5785
|
+
images: reservation.images,
|
|
5786
|
+
release: () => {
|
|
5787
|
+
releases++;
|
|
5788
|
+
reservation.release();
|
|
5789
|
+
released.resolve();
|
|
5790
|
+
},
|
|
5791
|
+
};
|
|
5792
|
+
});
|
|
5793
|
+
let harness: InvocationHarness | undefined;
|
|
5794
|
+
let preflightSignal: AbortSignal | undefined;
|
|
5795
|
+
try {
|
|
5796
|
+
harness = await invocationHarness(sessionId, cwd, {
|
|
5797
|
+
connectionLiveness: true,
|
|
5798
|
+
terminalAbortSeams: { getReconciliationStore: () => store },
|
|
5799
|
+
sendUserMessage: async (content, options) => {
|
|
5800
|
+
expect(Array.isArray(content) && content.some(block => block.type === "image")).toBe(true);
|
|
5801
|
+
submissionEntered = true;
|
|
5802
|
+
preflightSignal = options?.preflightSignal;
|
|
5803
|
+
await options?.onPreflightAcceptCommit?.();
|
|
5804
|
+
acceptanceResumed.resolve();
|
|
5805
|
+
await producer.promise;
|
|
5806
|
+
submissionFinished.resolve();
|
|
5807
|
+
expect(Array.isArray(content) && content.some(block => block.type === "image")).toBe(true);
|
|
5808
|
+
},
|
|
5809
|
+
});
|
|
5810
|
+
harness.connect("client");
|
|
5811
|
+
const id = await stageRuntimeImage(harness, makeTestPng());
|
|
5812
|
+
void harness
|
|
5813
|
+
.control("turn.prompt", {
|
|
5814
|
+
text: "hold the staged-image producer through shutdown acceptance",
|
|
5815
|
+
stagedImages: [{ id }],
|
|
5816
|
+
clientRef,
|
|
5817
|
+
})
|
|
5818
|
+
.catch(() => undefined);
|
|
5819
|
+
await acceptanceHeld.promise;
|
|
5820
|
+
expect(releases).toBe(0);
|
|
5821
|
+
|
|
5822
|
+
await harness.stop();
|
|
5823
|
+
expect(preflightSignal?.aborted).toBe(true);
|
|
5824
|
+
expect(releases).toBe(0);
|
|
5825
|
+
|
|
5826
|
+
resumeAcceptance.resolve();
|
|
5827
|
+
await acceptanceResumed.promise;
|
|
5828
|
+
expect(releases).toBe(0);
|
|
5829
|
+
producer.resolve();
|
|
5830
|
+
await submissionFinished.promise;
|
|
5831
|
+
await released.promise;
|
|
5832
|
+
expect(releases).toBe(1);
|
|
5833
|
+
} finally {
|
|
5834
|
+
resumeAcceptance.resolve();
|
|
5835
|
+
producer.resolve();
|
|
5836
|
+
if (submissionEntered) await submissionFinished.promise;
|
|
5837
|
+
await harness?.stop().catch(() => undefined);
|
|
5838
|
+
redeemSpy.mockRestore();
|
|
5839
|
+
renameSpy.mockRestore();
|
|
5840
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5841
|
+
}
|
|
5842
|
+
});
|
|
5843
|
+
|
|
5844
|
+
test("SDK-only queued image promotion retains quota through shutdown until exact-run settlement proof", async () => {
|
|
5845
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-image-queued-adoption-"));
|
|
5846
|
+
const proofAttempted = Promise.withResolvers<void>();
|
|
5847
|
+
const released = Promise.withResolvers<void>();
|
|
5848
|
+
const promotionReady = Promise.withResolvers<void>();
|
|
5849
|
+
let activeHandle: string | undefined;
|
|
5850
|
+
const settlement = Promise.withResolvers<{ status: "settled" }>();
|
|
5851
|
+
let releases = 0;
|
|
5852
|
+
let promoted: PreflightHooks["onQueuedPromoted"];
|
|
5853
|
+
const proofHandles: string[] = [];
|
|
5854
|
+
const originalRedeem = PromptImageUploadStore.prototype.redeem;
|
|
5855
|
+
const redeemSpy = spyOn(PromptImageUploadStore.prototype, "redeem").mockImplementation(function (
|
|
5856
|
+
this: PromptImageUploadStore,
|
|
5857
|
+
owner,
|
|
5858
|
+
ids,
|
|
5859
|
+
) {
|
|
5860
|
+
const reservation = originalRedeem.call(this, owner, ids);
|
|
5861
|
+
return {
|
|
5862
|
+
images: reservation.images,
|
|
5863
|
+
release: () => {
|
|
5864
|
+
releases++;
|
|
5865
|
+
reservation.release();
|
|
5866
|
+
released.resolve();
|
|
5867
|
+
},
|
|
5868
|
+
};
|
|
4690
5869
|
});
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
getBranch: () => hooks.branch ?? [],
|
|
4703
|
-
},
|
|
4704
|
-
};
|
|
4705
|
-
await handlers.get("session_start")?.({}, ctx);
|
|
4706
|
-
const request = (frame: Record<string, unknown>, connectionId = "client"): Promise<ResponseFrame> => {
|
|
4707
|
-
const id = `frame-${nextId}`;
|
|
4708
|
-
nextId += 1;
|
|
4709
|
-
const { promise, resolve } = Promise.withResolvers<ResponseFrame>();
|
|
4710
|
-
waiters.set(id, resolve);
|
|
4711
|
-
deliver?.(connectionId, { ...frame, id } as SdkFrame);
|
|
4712
|
-
return promise;
|
|
4713
|
-
};
|
|
4714
|
-
return {
|
|
4715
|
-
control: (operation, input) => request({ type: "control_request", operation, input }),
|
|
4716
|
-
controlAs: (connectionId, operation, input, idempotencyKey) =>
|
|
4717
|
-
request(
|
|
4718
|
-
{
|
|
4719
|
-
type: "control_request",
|
|
4720
|
-
operation,
|
|
4721
|
-
input,
|
|
4722
|
-
...(idempotencyKey === undefined ? {} : { idempotencyKey }),
|
|
4723
|
-
},
|
|
4724
|
-
connectionId,
|
|
4725
|
-
),
|
|
4726
|
-
query: (name, input) => request({ type: "query_request", query: name, input }),
|
|
4727
|
-
broadcasts,
|
|
4728
|
-
emit: async (event, payload) => {
|
|
4729
|
-
await handlers.get(event)?.(payload ?? {}, ctx);
|
|
4730
|
-
},
|
|
4731
|
-
stop: async () => {
|
|
4732
|
-
await handlers.get("session_shutdown")?.({}, ctx);
|
|
4733
|
-
},
|
|
4734
|
-
switchSession: async sessionId => {
|
|
4735
|
-
await handlers.get("session_switch")?.(
|
|
4736
|
-
{},
|
|
4737
|
-
{
|
|
4738
|
-
...ctx,
|
|
4739
|
-
sessionManager: {
|
|
4740
|
-
...ctx.sessionManager,
|
|
4741
|
-
getSessionId: () => sessionId,
|
|
4742
|
-
getSessionFile: () => path.join(cwd, ".gjc", "state", `${sessionId}.jsonl`),
|
|
4743
|
-
},
|
|
5870
|
+
let harness: InvocationHarness | undefined;
|
|
5871
|
+
try {
|
|
5872
|
+
harness = await invocationHarness("queued-image-adoption", cwd, {
|
|
5873
|
+
connectionLiveness: true,
|
|
5874
|
+
isIdle: () => false,
|
|
5875
|
+
terminalAbortSeams: {
|
|
5876
|
+
getActivePromptHandle: () => activeHandle,
|
|
5877
|
+
waitForRunSettlement: handle => {
|
|
5878
|
+
proofHandles.push(handle);
|
|
5879
|
+
proofAttempted.resolve();
|
|
5880
|
+
return settlement.promise;
|
|
4744
5881
|
},
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
{
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
5882
|
+
},
|
|
5883
|
+
sendUserMessage: async (content, options) => {
|
|
5884
|
+
await options?.onPreflightAcceptCommit?.();
|
|
5885
|
+
if (!Array.isArray(content) || !content.some(block => block.type === "image"))
|
|
5886
|
+
throw new Error("Expected the queued submission to retain its staged image.");
|
|
5887
|
+
options?.onDispatchDisposition?.({ startsOwnRun: false });
|
|
5888
|
+
promoted = options?.onQueuedPromoted;
|
|
5889
|
+
promotionReady.resolve();
|
|
5890
|
+
},
|
|
5891
|
+
});
|
|
5892
|
+
harness.connect("client");
|
|
5893
|
+
const id = await stageRuntimeImage(harness, makeTestPng());
|
|
5894
|
+
expect(
|
|
5895
|
+
await harness.control("turn.prompt", {
|
|
5896
|
+
text: "queue image work for a later own-run promotion",
|
|
5897
|
+
stagedImages: [{ id }],
|
|
5898
|
+
}),
|
|
5899
|
+
).toMatchObject({ ok: true, result: { accepted: true } });
|
|
5900
|
+
await promotionReady.promise;
|
|
5901
|
+
expect(promoted).toBeDefined();
|
|
5902
|
+
promoted?.({ startsOwnRun: true });
|
|
5903
|
+
await harness.stop();
|
|
5904
|
+
expect(releases).toBe(0);
|
|
5905
|
+
|
|
5906
|
+
activeHandle = "queued-image-run";
|
|
5907
|
+
await harness.emit("agent_start");
|
|
5908
|
+
await proofAttempted.promise;
|
|
5909
|
+
expect(proofHandles).toEqual(["queued-image-run"]);
|
|
5910
|
+
expect(releases).toBe(0);
|
|
5911
|
+
|
|
5912
|
+
await harness.stop();
|
|
5913
|
+
expect(proofHandles).toEqual(["queued-image-run"]);
|
|
5914
|
+
settlement.resolve({ status: "settled" });
|
|
5915
|
+
await released.promise;
|
|
5916
|
+
expect(releases).toBe(1);
|
|
5917
|
+
} finally {
|
|
5918
|
+
settlement.resolve({ status: "settled" });
|
|
5919
|
+
await harness?.stop().catch(() => undefined);
|
|
5920
|
+
redeemSpy.mockRestore();
|
|
5921
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5922
|
+
}
|
|
5923
|
+
});
|
|
5924
|
+
|
|
5925
|
+
test.each([
|
|
5926
|
+
"pending",
|
|
5927
|
+
"slow-pending",
|
|
5928
|
+
"shutdown-slow-pending",
|
|
5929
|
+
"quarantined",
|
|
5930
|
+
"held-start-pending",
|
|
5931
|
+
"held-start-quarantined",
|
|
5932
|
+
"held-start-missing-handle",
|
|
5933
|
+
] as const)("SDK-only forced image terminals retain unsettled producer capacity (%s)", async mode => {
|
|
5934
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-image-forced-"));
|
|
5935
|
+
const sourceBytesBefore = getPromptImageProcessBudgetSnapshot().sourceBytes;
|
|
5936
|
+
const ledger = createRunResourceLedger();
|
|
5937
|
+
ledger.open("image-run");
|
|
5938
|
+
const tool = Promise.withResolvers<void>();
|
|
5939
|
+
const run = Promise.withResolvers<void>();
|
|
5940
|
+
const released = Promise.withResolvers<void>();
|
|
5941
|
+
const startEntered = Promise.withResolvers<void>();
|
|
5942
|
+
const releaseStart = Promise.withResolvers<void>();
|
|
5943
|
+
const holdStart = mode.startsWith("held-start");
|
|
5944
|
+
const proofAttempted = Promise.withResolvers<void>();
|
|
5945
|
+
const proofHandles: string[] = [];
|
|
5946
|
+
let drainTimeouts = 0;
|
|
5947
|
+
ledger.track("image-run", "tool", "abort-ignoring-tool", tool.promise);
|
|
5948
|
+
let releases = 0;
|
|
5949
|
+
const cleanupReservations: Array<() => void> = [];
|
|
5950
|
+
const original = PromptImageUploadStore.prototype.redeem;
|
|
5951
|
+
const spy = spyOn(PromptImageUploadStore.prototype, "redeem").mockImplementation(function (
|
|
5952
|
+
this: PromptImageUploadStore,
|
|
5953
|
+
owner,
|
|
5954
|
+
ids,
|
|
5955
|
+
) {
|
|
5956
|
+
const reservation = original.call(this, owner, ids);
|
|
5957
|
+
cleanupReservations.push(reservation.release);
|
|
5958
|
+
return {
|
|
5959
|
+
images: reservation.images,
|
|
5960
|
+
release: () => {
|
|
5961
|
+
releases++;
|
|
5962
|
+
reservation.release();
|
|
5963
|
+
released.resolve();
|
|
5964
|
+
},
|
|
5965
|
+
};
|
|
5966
|
+
});
|
|
5967
|
+
let harness: InvocationHarness | undefined;
|
|
5968
|
+
try {
|
|
5969
|
+
harness = await invocationHarness(`image-forced-${mode}`, cwd, {
|
|
5970
|
+
connectionLiveness: true,
|
|
5971
|
+
...(holdStart
|
|
5972
|
+
? {
|
|
5973
|
+
persistInterceptor: () => {},
|
|
5974
|
+
persistHold: { type: "agent_start", onEntered: startEntered.resolve, release: releaseStart.promise },
|
|
5975
|
+
onLifecycleDrainTimeout: () => {
|
|
5976
|
+
drainTimeouts++;
|
|
5977
|
+
},
|
|
5978
|
+
}
|
|
5979
|
+
: {}),
|
|
5980
|
+
terminalAbortSeams: {
|
|
5981
|
+
getActivePromptHandle: () => (mode.endsWith("missing-handle") ? undefined : "image-run"),
|
|
5982
|
+
waitForRunSettlement: handle => {
|
|
5983
|
+
proofHandles.push(handle);
|
|
5984
|
+
proofAttempted.resolve();
|
|
5985
|
+
return ledger.waitForSettlement(handle, { graceMs: null });
|
|
4757
5986
|
},
|
|
5987
|
+
},
|
|
5988
|
+
sendUserMessage: async (_content, options) => {
|
|
5989
|
+
await options?.onPreflightAcceptCommit?.();
|
|
5990
|
+
await run.promise;
|
|
5991
|
+
},
|
|
5992
|
+
});
|
|
5993
|
+
harness.connect("client");
|
|
5994
|
+
const id = await stageRuntimeImage(harness, makeTestPng());
|
|
5995
|
+
expect(
|
|
5996
|
+
await harness.control("turn.prompt", { text: "hold the exact producer", stagedImages: [{ id }] }),
|
|
5997
|
+
).toMatchObject({ ok: true });
|
|
5998
|
+
await harness.emit("agent_start");
|
|
5999
|
+
if (holdStart) await startEntered.promise;
|
|
6000
|
+
harness.disconnect("client");
|
|
6001
|
+
ledger.seal("image-run");
|
|
6002
|
+
if (mode.endsWith("quarantined")) ledger.quarantine("image-run");
|
|
6003
|
+
if (holdStart) {
|
|
6004
|
+
await harness.stop();
|
|
6005
|
+
expect(drainTimeouts).toBe(1);
|
|
6006
|
+
expect(releases).toBe(0);
|
|
6007
|
+
releaseStart.resolve();
|
|
6008
|
+
} else await harness.emit("agent_end", { stopReason: "cancelled" });
|
|
6009
|
+
if (mode.includes("slow")) {
|
|
6010
|
+
await proofAttempted.promise;
|
|
6011
|
+
const chargedBytes = getPromptImageProcessBudgetSnapshot().sourceBytes;
|
|
6012
|
+
expect(chargedBytes).toBeGreaterThan(sourceBytesBefore);
|
|
6013
|
+
await harness.emit("agent_end", { stopReason: "cancelled" });
|
|
6014
|
+
if (mode.startsWith("shutdown")) {
|
|
6015
|
+
run.resolve();
|
|
6016
|
+
await harness.stop();
|
|
6017
|
+
await harness.stop();
|
|
6018
|
+
}
|
|
6019
|
+
// Exceed the retired helper's entire 100 x 100 ms polling window.
|
|
6020
|
+
await Bun.sleep(10_100);
|
|
6021
|
+
expect(getPromptImageProcessBudgetSnapshot().sourceBytes).toBe(chargedBytes);
|
|
6022
|
+
expect(proofHandles).toEqual(["image-run"]);
|
|
6023
|
+
} else await Bun.sleep(150);
|
|
6024
|
+
expect(releases).toBe(0);
|
|
6025
|
+
tool.resolve();
|
|
6026
|
+
if (mode.endsWith("pending")) {
|
|
6027
|
+
await released.promise;
|
|
6028
|
+
expect(releases).toBe(1);
|
|
6029
|
+
if (mode.includes("slow")) {
|
|
6030
|
+
expect(getPromptImageProcessBudgetSnapshot().sourceBytes).toBe(sourceBytesBefore);
|
|
6031
|
+
await harness.stop();
|
|
6032
|
+
expect(proofHandles).toEqual(["image-run"]);
|
|
6033
|
+
expect(releases).toBe(1);
|
|
6034
|
+
}
|
|
6035
|
+
} else {
|
|
6036
|
+
await harness.stop();
|
|
6037
|
+
await Bun.sleep(150);
|
|
6038
|
+
expect(releases).toBe(0);
|
|
6039
|
+
expect(await ledger.waitForSettlement("image-run", { graceMs: 0 })).toMatchObject(
|
|
6040
|
+
mode.endsWith("quarantined") ? { status: "unfenced", reason: "quarantined" } : { status: "settled" },
|
|
4758
6041
|
);
|
|
4759
|
-
}
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
}
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
6042
|
+
}
|
|
6043
|
+
} finally {
|
|
6044
|
+
releaseStart.resolve();
|
|
6045
|
+
tool.resolve();
|
|
6046
|
+
run.resolve();
|
|
6047
|
+
try {
|
|
6048
|
+
await harness?.stop();
|
|
6049
|
+
} finally {
|
|
6050
|
+
spy.mockRestore();
|
|
6051
|
+
for (const release of cleanupReservations) release();
|
|
6052
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6053
|
+
}
|
|
6054
|
+
}
|
|
6055
|
+
});
|
|
6056
|
+
|
|
6057
|
+
test("SDK-only failed queued cancellation retains the predecessor image cleanup owner for retry", async () => {
|
|
6058
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-image-cleanup-retry-"));
|
|
6059
|
+
let failTerminal = true;
|
|
6060
|
+
let deadlineMs = 60_000;
|
|
6061
|
+
let releases = 0;
|
|
6062
|
+
let queued: { commandId?: unknown; turnId?: unknown } | undefined;
|
|
6063
|
+
const producer = Promise.withResolvers<void>();
|
|
6064
|
+
const released = Promise.withResolvers<void>();
|
|
6065
|
+
const repaired = Promise.withResolvers<void>();
|
|
6066
|
+
const original = PromptImageUploadStore.prototype.redeem;
|
|
6067
|
+
const spy = spyOn(PromptImageUploadStore.prototype, "redeem").mockImplementation(function (
|
|
6068
|
+
this: PromptImageUploadStore,
|
|
6069
|
+
owner,
|
|
6070
|
+
ids,
|
|
6071
|
+
) {
|
|
6072
|
+
const reservation = original.call(this, owner, ids);
|
|
6073
|
+
return {
|
|
6074
|
+
images: reservation.images,
|
|
6075
|
+
release: () => {
|
|
6076
|
+
releases++;
|
|
6077
|
+
reservation.release();
|
|
6078
|
+
released.resolve();
|
|
6079
|
+
},
|
|
6080
|
+
};
|
|
6081
|
+
});
|
|
6082
|
+
let harness: InvocationHarness | undefined;
|
|
6083
|
+
try {
|
|
6084
|
+
harness = await invocationHarness("image-cleanup-retry", cwd, {
|
|
6085
|
+
connectionLiveness: true,
|
|
6086
|
+
settings: {
|
|
6087
|
+
get: (key: string) =>
|
|
6088
|
+
key === "sdk.promptDeadlineMs" ? deadlineMs : key === "sdk.promptMaxRuntimeMs" ? 60_000 : undefined,
|
|
6089
|
+
} as unknown as Settings,
|
|
6090
|
+
terminalAbortSeams: {
|
|
6091
|
+
getActivePromptHandle: () => undefined,
|
|
6092
|
+
},
|
|
6093
|
+
persistInterceptor: transition => {
|
|
6094
|
+
if (failTerminal && transition.type === "agent_end") throw new Error("held queue terminal persistence");
|
|
6095
|
+
},
|
|
6096
|
+
onDurableAttempt: attempt => {
|
|
6097
|
+
if (
|
|
6098
|
+
!failTerminal &&
|
|
6099
|
+
attempt.type === "agent_end" &&
|
|
6100
|
+
attempt.outcome === "committed" &&
|
|
6101
|
+
attempt.terminalAt !== undefined &&
|
|
6102
|
+
attempt.commandId === queued?.commandId &&
|
|
6103
|
+
attempt.turnId === queued?.turnId
|
|
6104
|
+
)
|
|
6105
|
+
repaired.resolve();
|
|
6106
|
+
},
|
|
6107
|
+
sendUserMessage: async (content, options) => {
|
|
6108
|
+
await options?.onPreflightAcceptCommit?.();
|
|
6109
|
+
if (Array.isArray(content)) {
|
|
6110
|
+
options?.onDispatchDisposition?.({ startsOwnRun: true });
|
|
6111
|
+
await producer.promise;
|
|
6112
|
+
}
|
|
6113
|
+
options?.onDispatchDisposition?.({ startsOwnRun: false });
|
|
6114
|
+
options?.preflightSignal?.addEventListener("abort", () => options.onQueuedPromoted?.({ removed: true }), {
|
|
6115
|
+
once: true,
|
|
6116
|
+
});
|
|
6117
|
+
},
|
|
6118
|
+
});
|
|
6119
|
+
harness.connect("client");
|
|
6120
|
+
const id = await stageRuntimeImage(harness, makeTestPng());
|
|
6121
|
+
expect(
|
|
6122
|
+
await harness.control("turn.prompt", { text: "retain predecessor image", stagedImages: [{ id }] }),
|
|
6123
|
+
).toMatchObject({ ok: true });
|
|
6124
|
+
deadlineMs = 100;
|
|
6125
|
+
const followUp = await harness.control("turn.follow_up", { text: "queue a removal with failed publication" });
|
|
6126
|
+
expect(followUp).toMatchObject({ ok: true });
|
|
6127
|
+
queued = followUp.result;
|
|
6128
|
+
await expect(harness.stop()).rejects.toMatchObject({ code: "sdk_reconciliation_teardown_failed" });
|
|
6129
|
+
expect(releases).toBe(0);
|
|
6130
|
+
failTerminal = false;
|
|
6131
|
+
await repaired.promise;
|
|
6132
|
+
expect(releases).toBe(0);
|
|
6133
|
+
// Either the original removal publisher or deadline recovery can retire
|
|
6134
|
+
// this cancellation. Until then shutdown must still fail closed.
|
|
6135
|
+
const retryUntil = Date.now() + 5_000;
|
|
6136
|
+
for (;;) {
|
|
6137
|
+
try {
|
|
6138
|
+
await harness.stop();
|
|
6139
|
+
break;
|
|
6140
|
+
} catch (error) {
|
|
6141
|
+
expect(error).toMatchObject({ code: "sdk_reconciliation_teardown_failed" });
|
|
6142
|
+
expect(releases).toBe(0);
|
|
6143
|
+
if (Date.now() >= retryUntil) throw error;
|
|
6144
|
+
await Bun.sleep(10);
|
|
6145
|
+
}
|
|
6146
|
+
}
|
|
6147
|
+
expect(releases).toBe(0);
|
|
6148
|
+
await harness.switchSession("image-cleanup-successor");
|
|
6149
|
+
expect(releases).toBe(0);
|
|
6150
|
+
producer.resolve();
|
|
6151
|
+
await released.promise;
|
|
6152
|
+
expect(releases).toBe(1);
|
|
6153
|
+
} finally {
|
|
6154
|
+
failTerminal = false;
|
|
6155
|
+
producer.resolve();
|
|
6156
|
+
try {
|
|
6157
|
+
await harness?.stop();
|
|
6158
|
+
} finally {
|
|
6159
|
+
spy.mockRestore();
|
|
6160
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6161
|
+
}
|
|
6162
|
+
}
|
|
6163
|
+
});
|
|
6164
|
+
|
|
6165
|
+
test("SDK-only shutdown retains staged-image quota when runtime stop fails and releases it after retry", async () => {
|
|
6166
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-staged-image-shutdown-"));
|
|
6167
|
+
const bytes = makeTestPng();
|
|
6168
|
+
const releases: string[] = [];
|
|
6169
|
+
const originalRedeem = PromptImageUploadStore.prototype.redeem;
|
|
6170
|
+
const redeemSpy = spyOn(PromptImageUploadStore.prototype, "redeem").mockImplementation(function (
|
|
6171
|
+
this: PromptImageUploadStore,
|
|
6172
|
+
owner,
|
|
6173
|
+
ids,
|
|
6174
|
+
) {
|
|
6175
|
+
const reservation = originalRedeem.call(this, owner, ids);
|
|
6176
|
+
return {
|
|
6177
|
+
images: reservation.images,
|
|
6178
|
+
release: () => {
|
|
6179
|
+
releases.push("released");
|
|
6180
|
+
reservation.release();
|
|
6181
|
+
},
|
|
6182
|
+
};
|
|
6183
|
+
});
|
|
6184
|
+
let harness: InvocationHarness | undefined;
|
|
6185
|
+
let stopSpy: { mockRestore(): void } | undefined;
|
|
6186
|
+
try {
|
|
6187
|
+
harness = await invocationHarness("staged-image-shutdown", cwd, {
|
|
6188
|
+
connectionLiveness: true,
|
|
6189
|
+
sendUserMessage: async (_content, options) => {
|
|
6190
|
+
await options?.onPreflightAcceptCommit?.();
|
|
6191
|
+
options?.onDispatchDisposition?.({ startsOwnRun: true });
|
|
6192
|
+
await neverSettlingPromise();
|
|
6193
|
+
},
|
|
6194
|
+
});
|
|
6195
|
+
harness.connect("client");
|
|
6196
|
+
const id = await stageRuntimeImage(harness, bytes);
|
|
6197
|
+
const accepted = await harness.control("turn.prompt", {
|
|
6198
|
+
text: "retain through failed stop",
|
|
6199
|
+
stagedImages: [{ id }],
|
|
6200
|
+
});
|
|
6201
|
+
expect(accepted).toMatchObject({ ok: true, result: { accepted: true } });
|
|
6202
|
+
await harness.emit("agent_start");
|
|
6203
|
+
expect(releases).toEqual([]);
|
|
6204
|
+
|
|
6205
|
+
stopSpy = spyOn(SessionSdkSessionRuntime.prototype, "stop").mockImplementationOnce(async () => {
|
|
6206
|
+
throw new Error("injected transport stop failure");
|
|
6207
|
+
});
|
|
6208
|
+
await expect(harness.stop()).rejects.toThrow("injected transport stop failure");
|
|
6209
|
+
expect(releases).toEqual([]);
|
|
6210
|
+
|
|
6211
|
+
stopSpy.mockRestore();
|
|
6212
|
+
stopSpy = undefined;
|
|
6213
|
+
await harness.stop();
|
|
6214
|
+
expect(releases).toEqual(["released"]);
|
|
6215
|
+
} finally {
|
|
6216
|
+
stopSpy?.mockRestore();
|
|
6217
|
+
await harness?.stop().catch(() => undefined);
|
|
6218
|
+
redeemSpy.mockRestore();
|
|
6219
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6220
|
+
}
|
|
6221
|
+
});
|
|
4774
6222
|
|
|
4775
6223
|
test.each(["natural", "removed"] as const)("SDK-only text follow-up has a durable terminal (%s)", async mode => {
|
|
4776
6224
|
const cwd = await mkdtemp(path.join(os.tmpdir(), `gjc-sdk-only-text-followup-${mode}-`));
|
|
@@ -5282,6 +6730,7 @@ function createInterceptorReconciliationStore(
|
|
|
5282
6730
|
type: string | undefined;
|
|
5283
6731
|
commandId?: string;
|
|
5284
6732
|
turnId?: string;
|
|
6733
|
+
terminalAt?: number;
|
|
5285
6734
|
outcome: "rejected" | "committed";
|
|
5286
6735
|
}) => void,
|
|
5287
6736
|
): SdkOnlyReconciliationStore {
|
|
@@ -5296,6 +6745,7 @@ function createInterceptorReconciliationStore(
|
|
|
5296
6745
|
let transitionType: string | undefined;
|
|
5297
6746
|
for (const record of records as Array<{
|
|
5298
6747
|
status?: string;
|
|
6748
|
+
startedAt?: number;
|
|
5299
6749
|
terminalAt?: number;
|
|
5300
6750
|
commandId?: string;
|
|
5301
6751
|
pendingOutcome?: unknown;
|
|
@@ -5324,6 +6774,7 @@ function createInterceptorReconciliationStore(
|
|
|
5324
6774
|
throw Object.assign(new Error("injected persistence failure"), { code: "io_error" });
|
|
5325
6775
|
}
|
|
5326
6776
|
const previous = backing.records.find(candidate => candidate.commandId === record.commandId);
|
|
6777
|
+
if (record.startedAt !== undefined && previous?.startedAt === undefined) transitionType ??= "agent_start";
|
|
5327
6778
|
const previousPendingOutcome = (
|
|
5328
6779
|
previous as
|
|
5329
6780
|
| (SdkOnlyInvocationRecord & { pendingOutcome?: { kind?: string; code?: string; reason?: string } })
|
|
@@ -5353,11 +6804,12 @@ function createInterceptorReconciliationStore(
|
|
|
5353
6804
|
hold.onEntered();
|
|
5354
6805
|
await hold.release;
|
|
5355
6806
|
}
|
|
5356
|
-
for (const record of records as Array<{ commandId?: string; turnId?: string }>)
|
|
6807
|
+
for (const record of records as Array<{ commandId?: string; turnId?: string; terminalAt?: number }>)
|
|
5357
6808
|
onDurableAttempt?.({
|
|
5358
6809
|
type: transitionType,
|
|
5359
6810
|
commandId: record?.commandId,
|
|
5360
6811
|
turnId: record?.turnId,
|
|
6812
|
+
terminalAt: record?.terminalAt,
|
|
5361
6813
|
outcome: "committed",
|
|
5362
6814
|
});
|
|
5363
6815
|
backing.records = records;
|
|
@@ -5494,7 +6946,7 @@ function realSendUserMessage(session: AgentSession) {
|
|
|
5494
6946
|
return async (content: unknown, options?: PreflightHooks & { deliverAs?: string }): Promise<void> => {
|
|
5495
6947
|
await options?.onPreflightAcceptCommit?.();
|
|
5496
6948
|
const { onPreflightAcceptCommit: _onPreflightAcceptCommit, ...dispatchOptions } = options ?? {};
|
|
5497
|
-
await session.sendUserMessage(content as string, dispatchOptions as
|
|
6949
|
+
await session.sendUserMessage(content as string, dispatchOptions as SendUserMessageOptions);
|
|
5498
6950
|
};
|
|
5499
6951
|
}
|
|
5500
6952
|
|
|
@@ -6176,6 +7628,7 @@ describe("post-acceptance invocation terminalization", () => {
|
|
|
6176
7628
|
let harness: InvocationHarness | undefined;
|
|
6177
7629
|
let session: AgentSession | undefined;
|
|
6178
7630
|
let authStorage: AuthStorage | undefined;
|
|
7631
|
+
let restoreEmission: (() => void) | undefined;
|
|
6179
7632
|
let providerCalls = 0;
|
|
6180
7633
|
try {
|
|
6181
7634
|
const real = await createTerminalizationSession(
|
|
@@ -6186,6 +7639,7 @@ describe("post-acceptance invocation terminalization", () => {
|
|
|
6186
7639
|
return createMockModel({ responses: [{ content: ["started"] }] }).stream(model, context, options);
|
|
6187
7640
|
},
|
|
6188
7641
|
{ "todo.enabled": true, "todo.reminders": true, "todo.reminders.max": 1, "retry.enabled": false },
|
|
7642
|
+
true,
|
|
6189
7643
|
);
|
|
6190
7644
|
session = real.session;
|
|
6191
7645
|
session.setConfiguredModelChain("default", [selector(real.model)], "terminal-throw-test");
|
|
@@ -6197,9 +7651,16 @@ describe("post-acceptance invocation terminalization", () => {
|
|
|
6197
7651
|
isIdle: () => !session?.isStreaming,
|
|
6198
7652
|
sendUserMessage: realSendUserMessage(session),
|
|
6199
7653
|
});
|
|
6200
|
-
|
|
6201
|
-
|
|
7654
|
+
const runner = session.extensionRunner;
|
|
7655
|
+
if (!runner) throw new Error("Expected the real awaited terminal extension bridge.");
|
|
7656
|
+
const emit = runner.emit.bind(runner);
|
|
7657
|
+
const emission = spyOn(runner, "emit").mockImplementation(async event => {
|
|
7658
|
+
const result = await emit(event);
|
|
7659
|
+
if (event.type === "agent_start" || event.type === "agent_failed" || event.type === "agent_end")
|
|
7660
|
+
await harness?.emit(event.type, event);
|
|
7661
|
+
return result;
|
|
6202
7662
|
});
|
|
7663
|
+
restoreEmission = () => emission.mockRestore();
|
|
6203
7664
|
const accepted = await harness.control("turn.prompt", { text: "finish the outstanding work" });
|
|
6204
7665
|
expect(accepted.ok).toBe(true);
|
|
6205
7666
|
const correlation = { commandId: accepted.result?.commandId, turnId: accepted.result?.turnId };
|
|
@@ -6226,6 +7687,7 @@ describe("post-acceptance invocation terminalization", () => {
|
|
|
6226
7687
|
outcome: { kind: "failed" },
|
|
6227
7688
|
});
|
|
6228
7689
|
} finally {
|
|
7690
|
+
restoreEmission?.();
|
|
6229
7691
|
await session?.dispose();
|
|
6230
7692
|
authStorage?.close();
|
|
6231
7693
|
await harness?.stop();
|
|
@@ -8516,40 +9978,80 @@ describe("accepted-control zero-execution bound (#4668)", () => {
|
|
|
8516
9978
|
}
|
|
8517
9979
|
});
|
|
8518
9980
|
|
|
8519
|
-
test("a captured cancelled end
|
|
9981
|
+
test("a captured cancelled end retains staged-image quota until confirmed tool recovery", async () => {
|
|
8520
9982
|
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-deadline-captured-uncertain-"));
|
|
9983
|
+
const bytes = makeTestPng();
|
|
9984
|
+
const releaseCalls: string[] = [];
|
|
9985
|
+
const originalRedeem = PromptImageUploadStore.prototype.redeem;
|
|
9986
|
+
const redeemSpy = spyOn(PromptImageUploadStore.prototype, "redeem").mockImplementation(function (
|
|
9987
|
+
this: PromptImageUploadStore,
|
|
9988
|
+
owner,
|
|
9989
|
+
ids,
|
|
9990
|
+
) {
|
|
9991
|
+
const reservation = originalRedeem.call(this, owner, ids);
|
|
9992
|
+
return {
|
|
9993
|
+
images: reservation.images,
|
|
9994
|
+
release: () => {
|
|
9995
|
+
releaseCalls.push("released");
|
|
9996
|
+
reservation.release();
|
|
9997
|
+
},
|
|
9998
|
+
};
|
|
9999
|
+
});
|
|
8521
10000
|
const sessionId = "deadline-captured-uncertain";
|
|
8522
10001
|
const sessionFile = path.join(cwd, ".gjc", "state", `${sessionId}.jsonl`);
|
|
8523
10002
|
await Bun.write(sessionFile, "");
|
|
8524
|
-
|
|
10003
|
+
// Chain-level evidence for the CI-only hang: every serialized store op
|
|
10004
|
+
// (including the terminal variants the runtime persistence log cannot see).
|
|
10005
|
+
const storeLog: string[] = [];
|
|
10006
|
+
const store = createReconciliationStore({
|
|
10007
|
+
sessionFile,
|
|
10008
|
+
sessionId,
|
|
10009
|
+
onInstrumentationForTests: event => {
|
|
10010
|
+
storeLog.push(
|
|
10011
|
+
`${event.at}:${event.phase}:${event.operation ?? ""}:${event.substep ?? ""}:${event.detail ?? ""}${event.error ? `:${event.error}` : ""}`,
|
|
10012
|
+
);
|
|
10013
|
+
},
|
|
10014
|
+
});
|
|
8525
10015
|
const activeTools = new Set(["unfenced-tool"]);
|
|
10016
|
+
const toolDrainObserved = Promise.withResolvers<void>();
|
|
10017
|
+
const terminalPublication = Promise.withResolvers<SdkOnlyDeadlineRecoveryCheckpoint>();
|
|
10018
|
+
const recoveryCheckpoints: SdkOnlyDeadlineRecoveryCheckpoint[] = [];
|
|
10019
|
+
const persistenceLog: Array<PersistenceInstrumentationEvent & { label: string }> = [];
|
|
8526
10020
|
let boundaryWaitStarted = false;
|
|
8527
10021
|
let abortCalls = 0;
|
|
10022
|
+
let sends = 0;
|
|
8528
10023
|
let harness: InvocationHarness | undefined;
|
|
8529
10024
|
try {
|
|
8530
10025
|
harness = await invocationHarness(sessionId, cwd, {
|
|
10026
|
+
connectionLiveness: true,
|
|
8531
10027
|
settings: {
|
|
8532
10028
|
// This test covers deferred terminal recovery, not deadline-triggered worktree persistence.
|
|
8533
10029
|
get: (key: string) =>
|
|
8534
|
-
key === "sdk.promptDeadlineMs"
|
|
8535
|
-
? 150
|
|
8536
|
-
: key === "sdk.promptMaxRuntimeMs"
|
|
8537
|
-
? 60_000
|
|
8538
|
-
: key === "sdk.flushWorktreeOnDeadline"
|
|
8539
|
-
? false
|
|
8540
|
-
: undefined,
|
|
10030
|
+
key === "sdk.promptDeadlineMs" ? 150 : key === "sdk.promptMaxRuntimeMs" ? 60_000 : undefined,
|
|
8541
10031
|
} as unknown as Settings,
|
|
8542
10032
|
sendUserMessage: async (_content, options) => {
|
|
10033
|
+
sends++;
|
|
8543
10034
|
await options?.onPreflightAcceptCommit?.();
|
|
8544
10035
|
await neverSettlingPromise();
|
|
8545
10036
|
},
|
|
10037
|
+
onPersistenceInstrumentation: event => {
|
|
10038
|
+
const label = `${event.phase}:${event.operation ?? "main"}:${event.substep ?? ""}`;
|
|
10039
|
+
persistenceLog.push({ ...event, label });
|
|
10040
|
+
},
|
|
8546
10041
|
terminalAbortSeams: {
|
|
8547
10042
|
getReconciliationStore: () => store,
|
|
8548
10043
|
getTerminalTurnEpoch: () => 109,
|
|
8549
10044
|
getActivePromptHandle: () => "deadline-captured-uncertain-run",
|
|
10045
|
+
onDeadlineRecoveryCheckpointForTests: checkpoint => {
|
|
10046
|
+
recoveryCheckpoints.push(checkpoint);
|
|
10047
|
+
if (checkpoint.stage === "publication-result" && checkpoint.result === "published")
|
|
10048
|
+
terminalPublication.resolve(checkpoint);
|
|
10049
|
+
},
|
|
8550
10050
|
pendingToolExecutions: () => {
|
|
8551
10051
|
if (activeTools.size > 0) boundaryWaitStarted = true;
|
|
8552
|
-
|
|
10052
|
+
const pending = [...activeTools];
|
|
10053
|
+
if (pending.length === 0) toolDrainObserved.resolve();
|
|
10054
|
+
return pending;
|
|
8553
10055
|
},
|
|
8554
10056
|
abortPromptAndWaitWithTerminal: async () => {
|
|
8555
10057
|
abortCalls += 1;
|
|
@@ -8557,9 +10059,18 @@ describe("accepted-control zero-execution bound (#4668)", () => {
|
|
|
8557
10059
|
},
|
|
8558
10060
|
},
|
|
8559
10061
|
});
|
|
8560
|
-
|
|
10062
|
+
harness.connect("client");
|
|
10063
|
+
const id = await stageRuntimeImage(harness, bytes);
|
|
10064
|
+
const accepted = await harness.control("turn.prompt", {
|
|
10065
|
+
text: "capture uncertain end",
|
|
10066
|
+
clientRef: "captured-uncertain-image",
|
|
10067
|
+
stagedImages: [{ id }],
|
|
10068
|
+
});
|
|
8561
10069
|
expect(accepted.ok).toBe(true);
|
|
8562
|
-
const
|
|
10070
|
+
const commandId = accepted.result?.commandId;
|
|
10071
|
+
const turnId = accepted.result?.turnId;
|
|
10072
|
+
if (commandId === undefined || turnId === undefined) throw new Error("Expected prompt correlation");
|
|
10073
|
+
const correlation = { commandId, turnId };
|
|
8563
10074
|
await harness.emit("agent_start");
|
|
8564
10075
|
await harness.emit("tool_execution_start", {
|
|
8565
10076
|
type: "tool_execution_start",
|
|
@@ -8573,6 +10084,7 @@ describe("accepted-control zero-execution bound (#4668)", () => {
|
|
|
8573
10084
|
await harness.emit("agent_end", { stopReason: "cancelled" });
|
|
8574
10085
|
await Bun.sleep(5_200);
|
|
8575
10086
|
expect(abortCalls).toBe(0);
|
|
10087
|
+
expect(releaseCalls).toEqual([]);
|
|
8576
10088
|
expect((await harness.query("turn.prompt_status", correlation)).result).toMatchObject({ status: "in_flight" });
|
|
8577
10089
|
expect(correlatedFrames(harness, correlation).filter(frame => frame.kind === "agent_end")).toEqual([]);
|
|
8578
10090
|
expect(correlatedFrames(harness, correlation).filter(frame => frame.kind === "agent_failed")).toEqual([]);
|
|
@@ -8584,6 +10096,17 @@ describe("accepted-control zero-execution bound (#4668)", () => {
|
|
|
8584
10096
|
deadlineRecoveryPending: true,
|
|
8585
10097
|
pendingOutcome: { kind: "stopped", reason: "cancelled" },
|
|
8586
10098
|
});
|
|
10099
|
+
// An uncertain retained outcome is reconciliation-only, never replayable.
|
|
10100
|
+
expect(
|
|
10101
|
+
await harness.control("turn.prompt", {
|
|
10102
|
+
text: "capture uncertain end",
|
|
10103
|
+
clientRef: "captured-uncertain-image",
|
|
10104
|
+
stagedImages: [{ id }],
|
|
10105
|
+
}),
|
|
10106
|
+
).toMatchObject({ ok: false, error: { code: "client_ref_conflict" } });
|
|
10107
|
+
expect(redeemSpy.mock.calls).toHaveLength(1);
|
|
10108
|
+
expect(sends).toBe(1);
|
|
10109
|
+
expect(releaseCalls).toEqual([]);
|
|
8587
10110
|
const reopened = createInvocationReconciliation({
|
|
8588
10111
|
store: createReconciliationStore({ sessionFile, sessionId }),
|
|
8589
10112
|
});
|
|
@@ -8597,14 +10120,45 @@ describe("accepted-control zero-execution bound (#4668)", () => {
|
|
|
8597
10120
|
});
|
|
8598
10121
|
|
|
8599
10122
|
activeTools.clear();
|
|
8600
|
-
//
|
|
8601
|
-
|
|
10123
|
+
// Wait until the deadline recovery loop has actually observed the exact
|
|
10124
|
+
// tool-set drain; clearing the test seam alone does not wake its bounded
|
|
10125
|
+
// retry timer.
|
|
10126
|
+
await toolDrainObserved.promise;
|
|
10127
|
+
// Wait for the durable terminal boundary itself, not an arbitrary status
|
|
10128
|
+
// polling budget that can expire while a slow CI filesystem is flushing it.
|
|
10129
|
+
const published = await terminalPublication.promise;
|
|
10130
|
+
expect(published.correlation).toEqual(correlation);
|
|
10131
|
+
let terminalStatus: NonNullable<ResponseFrame["result"]>;
|
|
10132
|
+
try {
|
|
10133
|
+
terminalStatus = await settledStatus(harness, "turn.prompt_status", correlation);
|
|
10134
|
+
} catch (error) {
|
|
10135
|
+
const finalRecord = store.snapshot().find(record => record.commandId === correlation.commandId) as
|
|
10136
|
+
| (SdkOnlyInvocationRecord & {
|
|
10137
|
+
pendingOutcome?: unknown;
|
|
10138
|
+
deadlineRecoveryPending?: boolean;
|
|
10139
|
+
terminalAt?: number;
|
|
10140
|
+
})
|
|
10141
|
+
| undefined;
|
|
10142
|
+
throw new Error(
|
|
10143
|
+
`${error instanceof Error ? error.message : String(error)}; persistence_log=${JSON.stringify(persistenceLog)}; store_log=${JSON.stringify(storeLog)}; deadline checkpoints=${JSON.stringify(recoveryCheckpoints)}; durable=${JSON.stringify(
|
|
10144
|
+
{
|
|
10145
|
+
status: finalRecord?.status,
|
|
10146
|
+
terminalAt: finalRecord?.terminalAt,
|
|
10147
|
+
deadlineRecoveryPending: finalRecord?.deadlineRecoveryPending,
|
|
10148
|
+
pendingOutcome: finalRecord?.pendingOutcome,
|
|
10149
|
+
},
|
|
10150
|
+
)}`,
|
|
10151
|
+
);
|
|
10152
|
+
}
|
|
10153
|
+
expect(terminalStatus).toMatchObject({
|
|
8602
10154
|
status: "terminal_ok",
|
|
8603
10155
|
outcome: { kind: "stopped", reason: "cancelled" },
|
|
8604
10156
|
});
|
|
8605
10157
|
expect(correlatedFrames(harness, correlation).filter(frame => frame.kind === "agent_end")).toHaveLength(1);
|
|
10158
|
+
expect(releaseCalls).toEqual(["released"]);
|
|
8606
10159
|
} finally {
|
|
8607
10160
|
await harness?.stop();
|
|
10161
|
+
redeemSpy.mockRestore();
|
|
8608
10162
|
await Bun.sleep(10);
|
|
8609
10163
|
await rm(cwd, { recursive: true, force: true });
|
|
8610
10164
|
}
|
|
@@ -8618,6 +10172,7 @@ describe("accepted-control zero-execution bound (#4668)", () => {
|
|
|
8618
10172
|
const sessionFile = path.join(cwd, "session.json");
|
|
8619
10173
|
await Bun.write(sessionFile, "");
|
|
8620
10174
|
const store = createReconciliationStore({ sessionFile, sessionId });
|
|
10175
|
+
const deadlineStarted = Promise.withResolvers<void>();
|
|
8621
10176
|
let abortCalls = 0;
|
|
8622
10177
|
let harness: InvocationHarness | undefined;
|
|
8623
10178
|
try {
|
|
@@ -8633,7 +10188,10 @@ describe("accepted-control zero-execution bound (#4668)", () => {
|
|
|
8633
10188
|
terminalAbortSeams: {
|
|
8634
10189
|
getReconciliationStore: () => store,
|
|
8635
10190
|
getTerminalTurnEpoch: () => 17,
|
|
8636
|
-
getActivePromptHandle: () =>
|
|
10191
|
+
getActivePromptHandle: () => {
|
|
10192
|
+
deadlineStarted.resolve();
|
|
10193
|
+
return "unobservable-tool-run";
|
|
10194
|
+
},
|
|
8637
10195
|
abortPromptAndWaitWithTerminal: async () => {
|
|
8638
10196
|
abortCalls += 1;
|
|
8639
10197
|
return { status: "settled" };
|
|
@@ -8647,7 +10205,10 @@ describe("accepted-control zero-execution bound (#4668)", () => {
|
|
|
8647
10205
|
await harness.emit("agent_failed", {
|
|
8648
10206
|
error: Object.assign(new Error("provider unavailable"), { code: "provider_unavailable" }),
|
|
8649
10207
|
});
|
|
8650
|
-
|
|
10208
|
+
// Expiry must install its recoverable observation before the real end
|
|
10209
|
+
// arrives; otherwise an end racing the durable uncertainty write can
|
|
10210
|
+
// terminalize from the diagnostic alone.
|
|
10211
|
+
await deadlineStarted.promise;
|
|
8651
10212
|
expect(abortCalls).toBe(0);
|
|
8652
10213
|
expect(await harness.query("turn.prompt_status", correlation)).toMatchObject({
|
|
8653
10214
|
result: { status: "in_flight" },
|
|
@@ -8655,7 +10216,6 @@ describe("accepted-control zero-execution bound (#4668)", () => {
|
|
|
8655
10216
|
expect(correlatedFrames(harness, correlation).filter(frame => frame.kind === "agent_failed")).toHaveLength(1);
|
|
8656
10217
|
expect(correlatedFrames(harness, correlation).filter(frame => frame.kind === "agent_end")).toEqual([]);
|
|
8657
10218
|
await harness.emit("agent_end", { stopReason: "cancelled" });
|
|
8658
|
-
await Bun.sleep(100);
|
|
8659
10219
|
expect(await harness.query("turn.prompt_status", correlation)).toMatchObject({
|
|
8660
10220
|
result: { status: "in_flight" },
|
|
8661
10221
|
});
|
|
@@ -8726,7 +10286,9 @@ describe("accepted-control zero-execution bound (#4668)", () => {
|
|
|
8726
10286
|
toolName: "bash",
|
|
8727
10287
|
args: {},
|
|
8728
10288
|
});
|
|
8729
|
-
|
|
10289
|
+
// This checks ordering, not prompt latency; allow the scheduler to reach
|
|
10290
|
+
// the boundary when many CI shards are contending for the event loop.
|
|
10291
|
+
const boundaryDeadline = Date.now() + 10_000;
|
|
8730
10292
|
while (!boundaryWaitStarted && Date.now() < boundaryDeadline) await Bun.sleep(10);
|
|
8731
10293
|
expect(boundaryWaitStarted).toBe(true);
|
|
8732
10294
|
expect(abortCalls).toHaveLength(0);
|
|
@@ -8740,7 +10302,7 @@ describe("accepted-control zero-execution bound (#4668)", () => {
|
|
|
8740
10302
|
});
|
|
8741
10303
|
await Bun.sleep(30);
|
|
8742
10304
|
expect(abortCalls).toHaveLength(0);
|
|
8743
|
-
const abortDeadline = Date.now() +
|
|
10305
|
+
const abortDeadline = Date.now() + 10_000;
|
|
8744
10306
|
while (abortCalls.length === 0 && Date.now() < abortDeadline) await Bun.sleep(10);
|
|
8745
10307
|
expect(abortCalls).toEqual([{ handle: "deadline-tool-run", scope: "owned", expectedEpoch: epoch }]);
|
|
8746
10308
|
expect(await settledStatus(harness, "turn.prompt_status", correlation)).toMatchObject({
|
|
@@ -8794,7 +10356,9 @@ describe("accepted-control zero-execution bound (#4668)", () => {
|
|
|
8794
10356
|
: key === "sdk.flushWorktreeOnDeadline"
|
|
8795
10357
|
? true
|
|
8796
10358
|
: undefined,
|
|
8797
|
-
|
|
10359
|
+
// Primary checkout: only a user/global true opts in. `has()` would
|
|
10360
|
+
// also be true for a project file, which must not authorize this.
|
|
10361
|
+
getGlobal: (key: string) => (key === "sdk.flushWorktreeOnDeadline" ? true : undefined),
|
|
8798
10362
|
getAgentDir: () => path.join(cwd, ".gjc", "agent"),
|
|
8799
10363
|
} as unknown as Settings,
|
|
8800
10364
|
sendUserMessage: async (_content, options) => {
|
|
@@ -9003,8 +10567,11 @@ describe("accepted-control zero-execution bound (#4668)", () => {
|
|
|
9003
10567
|
expect(await harness.query("turn.prompt_status", queuedIds)).toMatchObject({ result: { status: "accepted" } });
|
|
9004
10568
|
expect(abortCalls).toBe(0);
|
|
9005
10569
|
expect(correlatedFrames(harness, queuedIds).filter(frame => frame.kind === "agent_failed")).toHaveLength(0);
|
|
9006
|
-
promoted
|
|
10570
|
+
// The lease is captured when the follow-up is promoted, not when the
|
|
10571
|
+
// setting later changes. Restore the long deadline first so this run
|
|
10572
|
+
// does not inherit the 100ms queue probe and expire under settledStatus.
|
|
9007
10573
|
promptDeadlineMs = 600_000;
|
|
10574
|
+
promoted?.({ startsOwnRun: true });
|
|
9008
10575
|
await Bun.sleep(150);
|
|
9009
10576
|
expect((await harness.query("turn.prompt_status", queuedIds)).result?.status).toMatch(/accepted|in_flight/);
|
|
9010
10577
|
expect(await harness.query("turn.prompt_status", activeIds)).toMatchObject({
|
|
@@ -9039,6 +10606,8 @@ describe("accepted-control zero-execution bound (#4668)", () => {
|
|
|
9039
10606
|
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-deadline-start-race-"));
|
|
9040
10607
|
const claimEntered = Promise.withResolvers<void>();
|
|
9041
10608
|
const releaseClaim = Promise.withResolvers<void>();
|
|
10609
|
+
const persistenceEvents: PersistenceInstrumentationEvent[] = [];
|
|
10610
|
+
const durableAttempts: string[] = [];
|
|
9042
10611
|
const epoch = 83;
|
|
9043
10612
|
let harness: InvocationHarness | undefined;
|
|
9044
10613
|
let startPromise: Promise<void> | undefined;
|
|
@@ -9054,6 +10623,20 @@ describe("accepted-control zero-execution bound (#4668)", () => {
|
|
|
9054
10623
|
},
|
|
9055
10624
|
persistInterceptor: () => {},
|
|
9056
10625
|
agentFailedWriteFailures: 0,
|
|
10626
|
+
onPersistenceInstrumentation: event => {
|
|
10627
|
+
if (persistenceEvents.length < 64)
|
|
10628
|
+
persistenceEvents.push({
|
|
10629
|
+
...event,
|
|
10630
|
+
error: event.error?.slice(0, 256),
|
|
10631
|
+
detail: event.detail?.slice(0, 256),
|
|
10632
|
+
});
|
|
10633
|
+
},
|
|
10634
|
+
onDurableAttempt: attempt => {
|
|
10635
|
+
if (durableAttempts.length < 64)
|
|
10636
|
+
durableAttempts.push(
|
|
10637
|
+
`${attempt.type ?? "unclassified"}:${attempt.outcome}:${attempt.commandId ?? ""}:${attempt.turnId ?? ""}:${attempt.terminalAt ?? ""}`,
|
|
10638
|
+
);
|
|
10639
|
+
},
|
|
9057
10640
|
persistHolds: [
|
|
9058
10641
|
{
|
|
9059
10642
|
type: "deadline_claim",
|
|
@@ -9085,12 +10668,30 @@ describe("accepted-control zero-execution bound (#4668)", () => {
|
|
|
9085
10668
|
const accepted = await harness.control("turn.prompt", { text: "start while expiring" });
|
|
9086
10669
|
expect(accepted.ok).toBe(true);
|
|
9087
10670
|
const correlation = { commandId: accepted.result?.commandId, turnId: accepted.result?.turnId };
|
|
9088
|
-
const
|
|
10671
|
+
const claimWaitStartedAt = Date.now();
|
|
10672
|
+
const claimDeadline = claimWaitStartedAt + 10_000;
|
|
9089
10673
|
while (!claimWasEntered && Date.now() < claimDeadline) await Bun.sleep(5);
|
|
9090
|
-
|
|
10674
|
+
try {
|
|
10675
|
+
expect(claimWasEntered).toBe(true);
|
|
10676
|
+
} catch (error) {
|
|
10677
|
+
throw new Error(
|
|
10678
|
+
`${error instanceof Error ? error.message : String(error)}; claim_setup=${JSON.stringify({
|
|
10679
|
+
accepted,
|
|
10680
|
+
correlation,
|
|
10681
|
+
elapsedMs: Date.now() - claimWaitStartedAt,
|
|
10682
|
+
claimWasEntered,
|
|
10683
|
+
handleReads,
|
|
10684
|
+
abortCalls,
|
|
10685
|
+
persistenceEvents,
|
|
10686
|
+
durableAttempts,
|
|
10687
|
+
broadcasts: harness.broadcasts.slice(-16),
|
|
10688
|
+
})}`,
|
|
10689
|
+
{ cause: error },
|
|
10690
|
+
);
|
|
10691
|
+
}
|
|
9091
10692
|
await claimEntered.promise;
|
|
9092
10693
|
startPromise = harness.emit("agent_start");
|
|
9093
|
-
const captureDeadline = Date.now() +
|
|
10694
|
+
const captureDeadline = Date.now() + 10_000;
|
|
9094
10695
|
while (handleReads === 0 && Date.now() < captureDeadline) await Bun.sleep(5);
|
|
9095
10696
|
expect(handleReads).toBeGreaterThan(0);
|
|
9096
10697
|
releaseClaim.resolve();
|
|
@@ -9109,7 +10710,7 @@ describe("accepted-control zero-execution bound (#4668)", () => {
|
|
|
9109
10710
|
await harness?.stop();
|
|
9110
10711
|
await rm(cwd, { recursive: true, force: true });
|
|
9111
10712
|
}
|
|
9112
|
-
});
|
|
10713
|
+
}, 15_000);
|
|
9113
10714
|
|
|
9114
10715
|
test("agent_start during an expiry claim keeps the queued correlation owned", async () => {
|
|
9115
10716
|
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-deadline-start-race-"));
|
|
@@ -9122,8 +10723,8 @@ describe("accepted-control zero-execution bound (#4668)", () => {
|
|
|
9122
10723
|
let claimWasEntered = false;
|
|
9123
10724
|
let handleReads = 0;
|
|
9124
10725
|
let boundaryWaitStarted = false;
|
|
10726
|
+
let toolDrainWasObserved = false;
|
|
9125
10727
|
let abortCalls = 0;
|
|
9126
|
-
let terminalEventPromise: Promise<void> | undefined;
|
|
9127
10728
|
try {
|
|
9128
10729
|
harness = await invocationHarness("deadline-start-race", cwd, {
|
|
9129
10730
|
settings: zeroProgressSettings,
|
|
@@ -9151,8 +10752,7 @@ describe("accepted-control zero-execution bound (#4668)", () => {
|
|
|
9151
10752
|
},
|
|
9152
10753
|
pendingToolExecutions: () => {
|
|
9153
10754
|
if (activeTools.size > 0) boundaryWaitStarted = true;
|
|
9154
|
-
else if (boundaryWaitStarted
|
|
9155
|
-
terminalEventPromise = harness?.emit("agent_end", { stopReason: "cancelled" });
|
|
10755
|
+
else if (boundaryWaitStarted) toolDrainWasObserved = true;
|
|
9156
10756
|
return [...activeTools];
|
|
9157
10757
|
},
|
|
9158
10758
|
abortPromptAndWaitWithTerminal: async () => {
|
|
@@ -9177,15 +10777,14 @@ describe("accepted-control zero-execution bound (#4668)", () => {
|
|
|
9177
10777
|
while (!boundaryWaitStarted && Date.now() < boundaryDeadline) await Bun.sleep(5);
|
|
9178
10778
|
expect(boundaryWaitStarted).toBe(true);
|
|
9179
10779
|
expect(correlatedFrames(harness, correlation).filter(frame => frame.kind === "agent_start")).toHaveLength(1);
|
|
10780
|
+
// The producer emits its real end after tool settlement; the ledger observer stays read-only.
|
|
9180
10781
|
activeTools.clear();
|
|
9181
|
-
|
|
9182
|
-
while (terminalEventPromise === undefined && Date.now() < terminalEventDeadline) await Bun.sleep(5);
|
|
9183
|
-
expect(terminalEventPromise).toBeDefined();
|
|
9184
|
-
await terminalEventPromise;
|
|
10782
|
+
await harness.emit("agent_end", { stopReason: "cancelled" });
|
|
9185
10783
|
expect(await settledStatus(harness, "turn.prompt_status", correlation)).toMatchObject({
|
|
9186
10784
|
status: "terminal_ok",
|
|
9187
10785
|
outcome: { kind: "stopped", reason: "cancelled" },
|
|
9188
10786
|
});
|
|
10787
|
+
expect(toolDrainWasObserved).toBe(true);
|
|
9189
10788
|
expect(abortCalls).toBeLessThanOrEqual(1);
|
|
9190
10789
|
expect(correlatedFrames(harness, correlation).filter(frame => frame.kind === "agent_end")).toHaveLength(1);
|
|
9191
10790
|
expect(correlatedFrames(harness, correlation).filter(frame => frame.kind === "agent_failed")).toHaveLength(0);
|
|
@@ -9194,7 +10793,8 @@ describe("accepted-control zero-execution bound (#4668)", () => {
|
|
|
9194
10793
|
await harness?.stop();
|
|
9195
10794
|
await rm(cwd, { recursive: true, force: true });
|
|
9196
10795
|
}
|
|
9197
|
-
|
|
10796
|
+
// Real timers plus a reconciliation claim exceed the default 5s on a loaded shard.
|
|
10797
|
+
}, 20_000);
|
|
9198
10798
|
|
|
9199
10799
|
test("a sibling publication failure does not hide the expiring correlation's end", async () => {
|
|
9200
10800
|
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-deadline-sibling-publication-"));
|
|
@@ -11489,6 +13089,7 @@ test.each([
|
|
|
11489
13089
|
let transportCreations = 0;
|
|
11490
13090
|
createSdkSessionRuntimeExtension(api, {
|
|
11491
13091
|
agentDir: cwd,
|
|
13092
|
+
ensureBrokerImpl: async () => testBrokerDiscovery,
|
|
11492
13093
|
createTransport: async () => {
|
|
11493
13094
|
transportCreations++;
|
|
11494
13095
|
return transport;
|
|
@@ -11631,6 +13232,7 @@ test.each([
|
|
|
11631
13232
|
])("SDK-only queued terminal recovery permits replacement after EIO (early shutdown=%s)", async earlyShutdown => {
|
|
11632
13233
|
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-terminal-recovery-"));
|
|
11633
13234
|
const handlers = new Map<string, (event: unknown, ctx: ExtensionContext) => Promise<void> | void>();
|
|
13235
|
+
const recoveryRetired = Promise.withResolvers<SdkOnlyDeadlineRecoveryCheckpoint>();
|
|
11634
13236
|
let transport = memoryTransport();
|
|
11635
13237
|
const sessionId = transport.sessionId;
|
|
11636
13238
|
const sessionFile = path.join(cwd, `${sessionId}.json`);
|
|
@@ -11665,6 +13267,7 @@ test.each([
|
|
|
11665
13267
|
get: (key: string) =>
|
|
11666
13268
|
key === "sdk.promptDeadlineMs" ? 1_500 : key === "sdk.promptMaxRuntimeMs" ? 15_000 : undefined,
|
|
11667
13269
|
} as unknown as Settings,
|
|
13270
|
+
ensureBrokerImpl: async () => testBrokerDiscovery,
|
|
11668
13271
|
createTransport: async () => {
|
|
11669
13272
|
creations++;
|
|
11670
13273
|
transport = memoryTransport();
|
|
@@ -11676,6 +13279,9 @@ test.each([
|
|
|
11676
13279
|
getActivePromptHandle: () => "unrelated-handle",
|
|
11677
13280
|
cancelPendingPreflightForTerminalAbort: () => {},
|
|
11678
13281
|
abortPromptAndWaitWithTerminal: async () => ({ status: "settled", terminalScope: {} }),
|
|
13282
|
+
onDeadlineRecoveryCheckpointForTests: checkpoint => {
|
|
13283
|
+
if (checkpoint.stage === "deadline-recovery-retired") recoveryRetired.resolve(checkpoint);
|
|
13284
|
+
},
|
|
11679
13285
|
},
|
|
11680
13286
|
});
|
|
11681
13287
|
const ctx = { ...extensionContext(sessionId, cwd), isIdle: () => true } as unknown as ExtensionContext;
|
|
@@ -11717,7 +13323,7 @@ test.each([
|
|
|
11717
13323
|
expect(accepted.ok).toBe(true);
|
|
11718
13324
|
commandId = accepted.result?.commandId;
|
|
11719
13325
|
const turnId = accepted.result?.turnId;
|
|
11720
|
-
|
|
13326
|
+
if (commandId === undefined || turnId === undefined) throw new Error("Expected queued prompt correlation");
|
|
11721
13327
|
await waitFor(() => queued && queueSignal !== undefined, "queued admission");
|
|
11722
13328
|
expect(await request({ type: "control_request", operation: "turn.abort", input: {} })).toMatchObject({
|
|
11723
13329
|
ok: true,
|
|
@@ -11742,6 +13348,8 @@ test.each([
|
|
|
11742
13348
|
status: "failed",
|
|
11743
13349
|
error: { code: "cancelled" },
|
|
11744
13350
|
});
|
|
13351
|
+
const retired = await recoveryRetired.promise;
|
|
13352
|
+
expect(retired.correlation).toEqual({ commandId, turnId });
|
|
11745
13353
|
if (!earlyShutdown) await handlers.get("session_shutdown")?.({}, ctx);
|
|
11746
13354
|
await handlers.get("session_start")?.({}, ctx);
|
|
11747
13355
|
expect(creations).toBe(2);
|