@selesai/code 0.5.28 → 0.6.1
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 +24 -0
- package/README.md +1 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +9 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/config-selector.d.ts.map +1 -1
- package/dist/cli/config-selector.js +1 -1
- package/dist/cli/config-selector.js.map +1 -1
- package/dist/cli/credential-print.d.ts +23 -0
- package/dist/cli/credential-print.d.ts.map +1 -0
- package/dist/cli/credential-print.js +117 -0
- package/dist/cli/credential-print.js.map +1 -0
- package/dist/cli/startup-ui.d.ts.map +1 -1
- package/dist/cli/startup-ui.js +1 -1
- package/dist/cli/startup-ui.js.map +1 -1
- package/dist/core/agent-session-runtime.d.ts.map +1 -1
- package/dist/core/agent-session-runtime.js +3 -0
- package/dist/core/agent-session-runtime.js.map +1 -1
- package/dist/core/agent-session.d.ts +12 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +20 -14
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +11 -3
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +1 -0
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +11 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +14 -1
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/footer-data-provider.d.ts +10 -0
- package/dist/core/footer-data-provider.d.ts.map +1 -1
- package/dist/core/footer-data-provider.js +1 -1
- package/dist/core/footer-data-provider.js.map +1 -1
- package/dist/core/llama/provider.d.ts.map +1 -1
- package/dist/core/llama/provider.js +8 -3
- package/dist/core/llama/provider.js.map +1 -1
- package/dist/core/model-config.d.ts +30 -0
- package/dist/core/model-config.d.ts.map +1 -1
- package/dist/core/model-config.js +6 -0
- package/dist/core/model-config.js.map +1 -1
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +2 -2
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver.d.ts +1 -0
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +20 -3
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/model-runtime.d.ts +2 -0
- package/dist/core/model-runtime.d.ts.map +1 -1
- package/dist/core/model-runtime.js +5 -4
- package/dist/core/model-runtime.js.map +1 -1
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +13 -6
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/remote-catalog-provider.d.ts +1 -1
- package/dist/core/remote-catalog-provider.d.ts.map +1 -1
- package/dist/core/remote-catalog-provider.js +24 -11
- package/dist/core/remote-catalog-provider.js.map +1 -1
- package/dist/core/resource-loader.d.ts +15 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +66 -9
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/settings-manager.d.ts +1 -1
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +19 -1
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/system-prompt.test.d.ts +2 -0
- package/dist/core/system-prompt.test.d.ts.map +1 -0
- package/dist/core/system-prompt.test.js +89 -0
- package/dist/core/system-prompt.test.js.map +1 -0
- package/dist/core/tools/bash.d.ts +2 -0
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +34 -5
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -1
- package/dist/core/tools/tool-definition-wrapper.js +3 -1
- package/dist/core/tools/tool-definition-wrapper.js.map +1 -1
- package/dist/defaults/models.json +13 -45
- package/dist/defaults/settings.json +1 -2
- package/dist/extensions/copy-turn.test.ts +131 -0
- package/dist/extensions/copy-turn.ts +6 -1
- package/dist/extensions/package.json +0 -1
- package/dist/extensions/pi-intercom/CHANGELOG.md +249 -0
- package/dist/extensions/pi-intercom/README.md +102 -39
- package/dist/extensions/pi-intercom/broker/broker.ts +1233 -36
- package/dist/extensions/pi-intercom/broker/client.test.ts +83 -0
- package/dist/extensions/pi-intercom/broker/client.ts +315 -12
- package/dist/extensions/pi-intercom/broker/extension-state.ts +186 -0
- package/dist/extensions/pi-intercom/broker/extension.test.ts +387 -0
- package/dist/extensions/pi-intercom/broker/framing.test.ts +114 -0
- package/dist/extensions/pi-intercom/broker/framing.ts +82 -24
- package/dist/extensions/pi-intercom/broker/paths.test.ts +153 -0
- package/dist/extensions/pi-intercom/broker/paths.ts +117 -8
- package/dist/extensions/pi-intercom/broker/runtime-claim.test.ts +34 -0
- package/dist/extensions/pi-intercom/broker/runtime-claim.ts +21 -0
- package/dist/extensions/pi-intercom/broker/spawn.test.ts +160 -23
- package/dist/extensions/pi-intercom/broker/spawn.ts +113 -27
- package/dist/extensions/pi-intercom/config.test.ts +93 -0
- package/dist/extensions/pi-intercom/config.ts +55 -6
- package/dist/extensions/pi-intercom/cwd.test.ts +40 -0
- package/dist/extensions/pi-intercom/cwd.ts +31 -0
- package/dist/extensions/pi-intercom/extension-api.ts +44 -0
- package/dist/extensions/pi-intercom/format-context.test.ts +31 -0
- package/dist/extensions/pi-intercom/format-context.ts +32 -0
- package/dist/extensions/pi-intercom/index.ts +742 -145
- package/dist/extensions/pi-intercom/intercom.integration.test.ts +2646 -0
- package/dist/extensions/pi-intercom/package.json +15 -5
- package/dist/extensions/pi-intercom/reply-tracker.test.ts +134 -0
- package/dist/extensions/pi-intercom/reply-tracker.ts +31 -13
- package/dist/extensions/pi-intercom/skills/pi-intercom/SKILL.md +13 -11
- package/dist/extensions/pi-intercom/test/inline-message.test.ts +184 -0
- package/dist/extensions/pi-intercom/test/overlay-width.test.ts +66 -0
- package/dist/extensions/pi-intercom/types.ts +94 -4
- package/dist/extensions/pi-intercom/ui/compose.ts +8 -4
- package/dist/extensions/pi-intercom/ui/inline-message.ts +61 -25
- package/dist/extensions/pi-intercom/ui/session-list.ts +7 -3
- package/dist/extensions/pi-subagents/CHANGELOG.md +88 -0
- package/dist/extensions/pi-subagents/LICENSE +21 -0
- package/dist/extensions/pi-subagents/README.md +158 -69
- package/dist/extensions/pi-subagents/agents/architect.md +5 -4
- package/dist/extensions/pi-subagents/agents/builder.md +6 -4
- package/dist/extensions/pi-subagents/agents/commentator.md +3 -2
- package/dist/extensions/pi-subagents/agents/explorer.md +3 -2
- package/dist/extensions/pi-subagents/agents/recapper.md +3 -2
- package/dist/extensions/pi-subagents/agents/researcher.md +4 -3
- package/dist/extensions/pi-subagents/package-lock.json +2 -2
- package/dist/extensions/pi-subagents/package.json +3 -3
- package/dist/extensions/pi-subagents/prompts/review-loop.md +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +22 -988
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +257 -0
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +431 -0
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +144 -0
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +281 -0
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +125 -37
- package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +4 -0
- package/dist/extensions/pi-subagents/src/agents/agents.ts +118 -9
- package/dist/extensions/pi-subagents/src/agents/skills.ts +14 -12
- package/dist/extensions/pi-subagents/src/agents/task-aware-routing.ts +125 -0
- package/dist/extensions/pi-subagents/src/api/delegation.ts +3 -0
- package/dist/extensions/pi-subagents/src/api/preflight.ts +17 -13
- package/dist/extensions/pi-subagents/src/extension/chain-validation.ts +17 -1
- package/dist/extensions/pi-subagents/src/extension/index.ts +22 -8
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +248 -6
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +33 -8
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +32 -14
- package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +9 -4
- package/dist/extensions/pi-subagents/src/intercom/result-intercom.ts +33 -4
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +86 -21
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +24 -13
- package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +7 -5
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +18 -2
- package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +48 -5
- package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +68 -1
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +56 -5
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +92 -11
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +29 -3
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +388 -29
- package/dist/extensions/pi-subagents/src/runs/foreground/async-stop-action.ts +65 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +3 -3
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +214 -60
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +546 -252
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +42 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +553 -154
- package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +19 -13
- package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +51 -19
- package/dist/extensions/pi-subagents/src/runs/shared/chain-outputs.ts +3 -1
- package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +44 -11
- package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +8 -0
- package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +97 -20
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +43 -12
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +52 -4
- package/dist/extensions/pi-subagents/src/runs/shared/process-signal.ts +19 -0
- package/dist/extensions/pi-subagents/src/runs/shared/run-history.ts +45 -9
- package/dist/extensions/pi-subagents/src/runs/shared/runtime-acknowledged-extensions.ts +71 -0
- package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +63 -9
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +31 -1
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +101 -0
- package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +22 -1
- package/dist/extensions/pi-subagents/src/runs/shared/usage-budget.ts +65 -0
- package/dist/extensions/pi-subagents/src/runs/shared/workflow-graph.ts +26 -1
- package/dist/extensions/pi-subagents/src/shared/settings.ts +17 -1
- package/dist/extensions/pi-subagents/src/shared/types.ts +196 -12
- package/dist/extensions/pi-subagents/src/shared/utils.ts +102 -5
- package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +10 -1
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +46 -4
- package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +5 -3
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +87 -24
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +182 -9
- package/dist/extensions/pi-subagents/src/tui/render.ts +55 -34
- package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +14 -7
- package/dist/extensions/pi-subagents/src/watchdog/review.ts +5 -4
- package/dist/extensions/pi-subagents/src/watchdog/runtime.ts +170 -17
- package/dist/extensions/pi-subagents/src/watchdog/scope.ts +62 -0
- package/dist/extensions/pi-subagents/src/watchdog/settings.ts +41 -1
- package/dist/extensions/pi-subagents/src/watchdog/types.ts +10 -0
- package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +113 -8
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +419 -47
- package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +84 -0
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +4 -2
- package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +51 -2
- package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +62 -22
- package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +48 -0
- package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +8 -6
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +231 -19
- package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +48 -7
- package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +44 -0
- package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +199 -17
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +898 -16
- package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +112 -23
- package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +17 -5
- package/dist/extensions/pi-subagents/test/support/mock-pi.ts +2 -0
- package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +18 -2
- package/dist/extensions/pi-subagents/test/support/register-loader.mjs +3 -3
- package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +3 -1
- package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +198 -6
- package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +161 -1
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +47 -2
- package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +60 -0
- package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +8 -0
- package/dist/extensions/pi-subagents/test/unit/builtin-agent-documentation.test.ts +63 -0
- package/dist/extensions/pi-subagents/test/unit/capability-ceiling-agent-allowlist.test.ts +136 -0
- package/dist/extensions/pi-subagents/test/unit/capability-ceiling.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +6 -0
- package/dist/extensions/pi-subagents/test/unit/chain-validation.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +48 -48
- package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +23 -0
- package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +27 -0
- package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +28 -2
- package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +176 -8
- package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +164 -5
- package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +41 -1
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +17 -12
- package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +18 -2
- package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +20 -1
- package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +30 -2
- package/dist/extensions/pi-subagents/test/unit/notify.test.ts +39 -2
- package/dist/extensions/pi-subagents/test/unit/parallel-utils.test.ts +22 -0
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +103 -0
- package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +18 -0
- package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +48 -0
- package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +13 -1
- package/dist/extensions/pi-subagents/test/unit/result-intercom.test.ts +29 -4
- package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +223 -4
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +30 -0
- package/dist/extensions/pi-subagents/test/unit/runtime-acknowledged-extensions.test.ts +52 -0
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +46 -2
- package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +91 -1
- package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/streamed-progress-bounds.test.ts +78 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +41 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +77 -0
- package/dist/extensions/pi-subagents/test/unit/task-aware-routing.test.ts +213 -0
- package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +26 -1
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +61 -10
- package/dist/extensions/pi-subagents/test/unit/total-cost.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-runtime.test.ts +210 -1
- package/dist/extensions/pi-subagents/test/unit/watchdog-scope.test.ts +33 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-settings.test.ts +32 -0
- package/dist/extensions/pi-subagents/test/unit/writer-budget-guidance.test.ts +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +43 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/custom-message.d.ts +3 -1
- package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-message.js +10 -2
- package/dist/modes/interactive/components/custom-message.js.map +1 -1
- package/dist/modes/interactive/components/extension-editor.d.ts +1 -2
- package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-editor.js +16 -46
- package/dist/modes/interactive/components/extension-editor.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +4 -1
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.js +22 -13
- package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
- package/dist/modes/interactive/external-editor.d.ts +12 -0
- package/dist/modes/interactive/external-editor.d.ts.map +1 -0
- package/dist/modes/interactive/external-editor.js +37 -0
- package/dist/modes/interactive/external-editor.js.map +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +72 -64
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +14 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/skills/ponytail/SKILL.md +1 -3
- package/dist/utils/clipboard.d.ts.map +1 -1
- package/dist/utils/clipboard.js +19 -8
- package/dist/utils/clipboard.js.map +1 -1
- package/dist/utils/version-check.d.ts.map +1 -1
- package/dist/utils/version-check.js +1 -1
- package/dist/utils/version-check.js.map +1 -1
- package/docs/compaction.md +1 -1
- package/docs/custom-provider.md +14 -5
- package/docs/environment-variables.md +86 -0
- package/docs/extensions.md +15 -5
- package/docs/index.md +1 -0
- package/docs/models.md +9 -2
- package/docs/plans/subagent-delegation/phase-0-correctness.md +265 -0
- package/docs/plans/subagent-delegation/phase-1-behavioral-contract.md +486 -0
- package/docs/plans/subagent-delegation/phase-2-context-controls.md +282 -0
- package/docs/plans/subagent-delegation/phase-3-advisory-routing.md +362 -0
- package/docs/plans/subagent-delegation/phase-4-optional-enforcement.md +381 -0
- package/docs/providers.md +21 -2
- package/docs/rpc.md +23 -6
- package/docs/session-format.md +2 -0
- package/docs/settings.md +1 -1
- package/docs/usage.md +0 -13
- package/examples/extensions/custom-compaction.ts +5 -2
- package/examples/extensions/custom-provider-anthropic/index.ts +9 -3
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/handoff.ts +11 -3
- package/examples/extensions/message-renderer.ts +3 -3
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/summarize.ts +5 -2
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/examples/sdk/12-full-control.ts +3 -1
- package/package.json +16 -7
- package/dist/extensions/caveman/caveman-instructions.cjs +0 -11
- package/dist/extensions/caveman/index.js +0 -118
- package/dist/extensions/caveman/package.json +0 -8
- package/dist/extensions/caveman/test/extension.test.js +0 -203
- package/dist/extensions/caveman/test/helpers.test.js +0 -58
- package/dist/skills/caveman/SKILL.md +0 -50
|
@@ -81,6 +81,22 @@ interface ArtifactPaths {
|
|
|
81
81
|
metadataPath?: string;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
interface LaunchResolvedExtensions {
|
|
85
|
+
version?: number;
|
|
86
|
+
source?: string;
|
|
87
|
+
disableAmbientExtensions?: boolean;
|
|
88
|
+
runtime?: string[];
|
|
89
|
+
configured?: string[];
|
|
90
|
+
effective?: string[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface RuntimeAcknowledgedExtensions {
|
|
94
|
+
version?: number;
|
|
95
|
+
source?: string;
|
|
96
|
+
ids?: string[];
|
|
97
|
+
omitted?: number;
|
|
98
|
+
}
|
|
99
|
+
|
|
84
100
|
interface RunSyncResult {
|
|
85
101
|
exitCode: number;
|
|
86
102
|
agent: string;
|
|
@@ -99,6 +115,7 @@ interface RunSyncResult {
|
|
|
99
115
|
transcriptPath?: string;
|
|
100
116
|
transcriptError?: string;
|
|
101
117
|
finalOutput?: string;
|
|
118
|
+
processSignal?: string | null;
|
|
102
119
|
interrupted?: boolean;
|
|
103
120
|
timedOut?: boolean;
|
|
104
121
|
turnBudget?: { maxTurns: number; graceTurns: number; outcome: string; turnCount: number; wrapUpRequestedAtTurn?: number; exceededAtTurn?: number };
|
|
@@ -121,6 +138,8 @@ interface RunSyncResult {
|
|
|
121
138
|
verifyRuns?: Array<{ status?: string }>;
|
|
122
139
|
runtimeChecks?: Array<{ id?: string; status?: string; message?: string }>;
|
|
123
140
|
};
|
|
141
|
+
launchResolvedExtensions?: LaunchResolvedExtensions;
|
|
142
|
+
runtimeAcknowledgedExtensions?: RuntimeAcknowledgedExtensions;
|
|
124
143
|
}
|
|
125
144
|
|
|
126
145
|
interface MockPiCallRecord {
|
|
@@ -230,6 +249,7 @@ interface ExecutorModule {
|
|
|
230
249
|
execute: (...args: unknown[]) => Promise<ExecutorToolResult>;
|
|
231
250
|
executeDelegated: (...args: unknown[]) => Promise<ExecutorToolResult>;
|
|
232
251
|
};
|
|
252
|
+
DEFAULT_FOREGROUND_TIMEOUT_MS?: number;
|
|
233
253
|
}
|
|
234
254
|
|
|
235
255
|
const execution = await tryImport<ExecutionModule>("./src/runs/foreground/execution.ts");
|
|
@@ -354,7 +374,8 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
354
374
|
);
|
|
355
375
|
|
|
356
376
|
assert.equal(result.isError, undefined);
|
|
357
|
-
assert.match(result.content[0]?.text ?? "", /
|
|
377
|
+
assert.match(result.content[0]?.text ?? "", /Output saved to: /);
|
|
378
|
+
assert.doesNotMatch(result.content[0]?.text ?? "", /single alias finished/);
|
|
358
379
|
});
|
|
359
380
|
|
|
360
381
|
it("admits a zero run-level tool budget only for marked v2 delegated execution", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
@@ -392,7 +413,13 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
392
413
|
ctx,
|
|
393
414
|
);
|
|
394
415
|
assert.equal(v2Delegated.isError, undefined);
|
|
395
|
-
|
|
416
|
+
// Reference-first delegated completion: the full echoed env snapshot is
|
|
417
|
+
// persisted to the generated result file; the content is the reference.
|
|
418
|
+
const delegatedOutputsDir = path.join(tempDir, ".pi-subagents", "artifacts", "outputs");
|
|
419
|
+
const delegatedRunDirs = fs.readdirSync(delegatedOutputsDir);
|
|
420
|
+
assert.equal(delegatedRunDirs.length, 1);
|
|
421
|
+
const persistedEnv = fs.readFileSync(path.join(delegatedOutputsDir, delegatedRunDirs[0]!, "result.md"), "utf-8");
|
|
422
|
+
const env = JSON.parse(persistedEnv) as Record<string, string>;
|
|
396
423
|
assert.deepEqual(JSON.parse(env[TOOL_BUDGET_ENV] ?? "null"), zeroBudget);
|
|
397
424
|
assert.equal(env[TOOL_BUDGET_ZERO_AUTH_ENV], "1");
|
|
398
425
|
assert.equal(mockPi.callCount(), 1);
|
|
@@ -572,7 +599,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
572
599
|
extensionEvents.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, {
|
|
573
600
|
version: SUBAGENT_DELEGATION_PROTOCOL_VERSION,
|
|
574
601
|
requestId: "registered-a",
|
|
575
|
-
agent: "
|
|
602
|
+
agent: "builder",
|
|
576
603
|
task: "First registered delegated call",
|
|
577
604
|
context: "fresh",
|
|
578
605
|
cwd: tempDir,
|
|
@@ -581,7 +608,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
581
608
|
extensionEvents.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, {
|
|
582
609
|
version: SUBAGENT_DELEGATION_PROTOCOL_VERSION,
|
|
583
610
|
requestId: "registered-b",
|
|
584
|
-
agent: "
|
|
611
|
+
agent: "commentator",
|
|
585
612
|
task: "Second registered delegated call",
|
|
586
613
|
context: "fresh",
|
|
587
614
|
cwd: tempDir,
|
|
@@ -693,7 +720,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
693
720
|
requestId: "registered-v2-a",
|
|
694
721
|
ownerRunId: "owner-v2",
|
|
695
722
|
nodeId: "node-a",
|
|
696
|
-
agent: "
|
|
723
|
+
agent: "builder",
|
|
697
724
|
task: "Return literal JSON-looking text",
|
|
698
725
|
context: "fresh",
|
|
699
726
|
cwd: tempDir,
|
|
@@ -706,7 +733,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
706
733
|
requestId: "registered-v2-b",
|
|
707
734
|
ownerRunId: "owner-v2",
|
|
708
735
|
nodeId: "node-b",
|
|
709
|
-
agent: "
|
|
736
|
+
agent: "commentator",
|
|
710
737
|
task: "Run the second logical node",
|
|
711
738
|
context: "fresh",
|
|
712
739
|
cwd: tempDir,
|
|
@@ -745,6 +772,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
745
772
|
assert.equal(response.ownerRunId, "owner-v2");
|
|
746
773
|
assert.equal(response.model, "mock/test-model:high");
|
|
747
774
|
assert.equal(response.thinking, "high");
|
|
775
|
+
assert.match(response.launchContractDigest ?? "", /^[0-9a-f]{64}$/);
|
|
748
776
|
}
|
|
749
777
|
const literalResponse = terminalResponses.find((response) => response.result?.kind === "text" && response.result.text === literalJsonText);
|
|
750
778
|
assert.ok(literalResponse);
|
|
@@ -1049,6 +1077,58 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
1049
1077
|
assert.deepEqual(result.details?.totalCost, { inputTokens: 100, outputTokens: 50, costUsd: 0.001 });
|
|
1050
1078
|
});
|
|
1051
1079
|
|
|
1080
|
+
it("blocks later foreground chain children when hard reported usage is exhausted", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
1081
|
+
mockPi.onCall({ output: "first result" });
|
|
1082
|
+
const executor = makeExecutor([makeAgent("echo"), makeAgent("second")]);
|
|
1083
|
+
|
|
1084
|
+
const result = await executor.execute(
|
|
1085
|
+
"foreground-usage-budget",
|
|
1086
|
+
{
|
|
1087
|
+
chain: [
|
|
1088
|
+
{ agent: "echo", task: "First task" },
|
|
1089
|
+
{ agent: "second", task: "Second task" },
|
|
1090
|
+
],
|
|
1091
|
+
usageBudget: { tokens: { hard: 10 } },
|
|
1092
|
+
},
|
|
1093
|
+
new AbortController().signal,
|
|
1094
|
+
undefined,
|
|
1095
|
+
makeMinimalCtx(tempDir),
|
|
1096
|
+
);
|
|
1097
|
+
|
|
1098
|
+
assert.equal(result.isError, true);
|
|
1099
|
+
assert.match(result.content[0]?.text ?? "", /Usage budget exhausted/);
|
|
1100
|
+
assert.equal(mockPi.callCount(), 1);
|
|
1101
|
+
assert.equal(result.details?.usageBudget?.exhausted, true);
|
|
1102
|
+
assert.equal(result.details?.usageBudget?.reason, "tokens");
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
it("blocks queued foreground parallel children when hard reported usage is exhausted", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
1106
|
+
mockPi.onCall({ output: "first result" });
|
|
1107
|
+
const executor = makeExecutor([makeAgent("echo"), makeAgent("second")]);
|
|
1108
|
+
|
|
1109
|
+
const result = await executor.execute(
|
|
1110
|
+
"foreground-parallel-usage-budget",
|
|
1111
|
+
{
|
|
1112
|
+
tasks: [
|
|
1113
|
+
{ agent: "echo", task: "First task" },
|
|
1114
|
+
{ agent: "second", task: "Second task" },
|
|
1115
|
+
],
|
|
1116
|
+
concurrency: 1,
|
|
1117
|
+
usageBudget: { tokens: { hard: 10 } },
|
|
1118
|
+
},
|
|
1119
|
+
new AbortController().signal,
|
|
1120
|
+
undefined,
|
|
1121
|
+
makeMinimalCtx(tempDir),
|
|
1122
|
+
);
|
|
1123
|
+
|
|
1124
|
+
assert.equal(result.isError, undefined);
|
|
1125
|
+
assert.equal(mockPi.callCount(), 1);
|
|
1126
|
+
assert.equal(result.details?.results.length, 2);
|
|
1127
|
+
assert.equal(result.details?.results[1]?.skipped, true);
|
|
1128
|
+
assert.match(result.details?.results[1]?.error ?? "", /Usage budget exhausted/);
|
|
1129
|
+
assert.equal(result.details?.usageBudget?.exhausted, true);
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1052
1132
|
it("fails implementation runs that complete without mutation attempts", async () => {
|
|
1053
1133
|
mockPi.onCall({ output: "Validation:\nlet rawFilename = params.filename.trim();" });
|
|
1054
1134
|
const agents = [makeAgent("worker")];
|
|
@@ -1175,7 +1255,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
1175
1255
|
|
|
1176
1256
|
const result = await executor.execute(
|
|
1177
1257
|
"single-schema-file-only-missing-path",
|
|
1178
|
-
{ agent: "echo", task: "Return structured data", outputSchema: { type: "object", required: ["ok"], properties: { ok: { type: "boolean" } } }, outputMode: "file-only", acceptance: false, artifacts: false },
|
|
1258
|
+
{ agent: "echo", task: "Return structured data", outputSchema: { type: "object", required: ["ok"], properties: { ok: { type: "boolean" } } }, outputMode: "file-only", output: false, acceptance: false, artifacts: false },
|
|
1179
1259
|
new AbortController().signal,
|
|
1180
1260
|
undefined,
|
|
1181
1261
|
makeMinimalCtx(tempDir),
|
|
@@ -1232,7 +1312,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
1232
1312
|
|
|
1233
1313
|
const result = await executor.execute(
|
|
1234
1314
|
"failed-single-output",
|
|
1235
|
-
{ agent: "oracle", task: "Implement the approved file changes" },
|
|
1315
|
+
{ agent: "oracle", task: "Implement the approved file changes", artifacts: true },
|
|
1236
1316
|
new AbortController().signal,
|
|
1237
1317
|
undefined,
|
|
1238
1318
|
makeMinimalCtx(tempDir),
|
|
@@ -1241,8 +1321,14 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
1241
1321
|
const text = result.content[0]?.text ?? "";
|
|
1242
1322
|
assert.equal(result.isError, true);
|
|
1243
1323
|
assert.match(text, /completed without making edits/);
|
|
1244
|
-
|
|
1245
|
-
|
|
1324
|
+
// Reference-first failure: error/status plus the saved-output reference;
|
|
1325
|
+
// the full output stays readable in the persisted result file.
|
|
1326
|
+
assert.match(text, /Output saved to: /);
|
|
1327
|
+
assert.doesNotMatch(text, /Output:\nOracle review:/);
|
|
1328
|
+
const outputsDir = path.join(tempDir, ".pi-subagents", "artifacts", "outputs");
|
|
1329
|
+
const runDirs = fs.readdirSync(outputsDir);
|
|
1330
|
+
assert.equal(runDirs.length, 1);
|
|
1331
|
+
assert.equal(fs.readFileSync(path.join(outputsDir, runDirs[0]!, "result.md"), "utf-8"), "Oracle review:\n- finding one\n- finding two");
|
|
1246
1332
|
});
|
|
1247
1333
|
|
|
1248
1334
|
it("fails future-tense implementation summaries when no mutation attempt occurred", async () => {
|
|
@@ -1318,6 +1404,18 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
1318
1404
|
assert.equal(result.finalOutput, "Applied edit");
|
|
1319
1405
|
});
|
|
1320
1406
|
|
|
1407
|
+
it("resolves explicit agent aliases to canonical execution names", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
1408
|
+
mockPi.onCall({ output: "Implemented" });
|
|
1409
|
+
const executor = makeExecutor([makeAgent("worker", { aliases: ["developer"], completionGuard: false })]);
|
|
1410
|
+
|
|
1411
|
+
const result = await executor.execute("single", { agent: "developer", task: "Implement" }, new AbortController().signal, undefined, makeMinimalCtx(tempDir));
|
|
1412
|
+
|
|
1413
|
+
assert.equal(result.isError, undefined);
|
|
1414
|
+
assert.equal(result.details?.results[0]?.agent, "worker");
|
|
1415
|
+
assert.match(result.content[0]?.text ?? "", /Output saved to: /);
|
|
1416
|
+
assert.doesNotMatch(result.content[0]?.text ?? "", /^Implemented$/);
|
|
1417
|
+
});
|
|
1418
|
+
|
|
1321
1419
|
it("returns error for unknown agent", async () => {
|
|
1322
1420
|
const agents = makeAgentConfigs(["echo"]);
|
|
1323
1421
|
const result = await runSync(tempDir, agents, "nonexistent", "Do something", {});
|
|
@@ -1416,6 +1514,114 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
1416
1514
|
assert.ok(result.error?.includes("Something went wrong"));
|
|
1417
1515
|
});
|
|
1418
1516
|
|
|
1517
|
+
it("retries a zero-activity startup exit on the same model", async () => {
|
|
1518
|
+
mockPi.onCall({ exitCode: 1 });
|
|
1519
|
+
mockPi.onCall({ output: "Recovered after startup race" });
|
|
1520
|
+
const agents = [makeAgent("worker", {
|
|
1521
|
+
model: "openai/gpt-5-mini",
|
|
1522
|
+
fallbackModels: ["anthropic/claude-sonnet-4"],
|
|
1523
|
+
})];
|
|
1524
|
+
|
|
1525
|
+
const result = await runSync(tempDir, agents, "worker", "Do work", {
|
|
1526
|
+
runId: "startup-retry-sync",
|
|
1527
|
+
acceptance: false,
|
|
1528
|
+
});
|
|
1529
|
+
|
|
1530
|
+
assert.equal(result.exitCode, 0);
|
|
1531
|
+
assert.equal(result.model, "openai/gpt-5-mini");
|
|
1532
|
+
assert.equal(result.finalOutput, "Recovered after startup race");
|
|
1533
|
+
assert.deepEqual(result.attemptedModels, ["openai/gpt-5-mini"]);
|
|
1534
|
+
assert.deepEqual(result.modelAttempts?.map((attempt) => attempt.success), [false, true]);
|
|
1535
|
+
assert.match(result.progress.recentOutput.join("\n"), /\[startup-retry\].*same model/i);
|
|
1536
|
+
assert.equal(result.progress.recentOutput.filter((line) => line.startsWith("[startup-retry]")).length, 1);
|
|
1537
|
+
assert.equal(mockPi.callCount(), 2);
|
|
1538
|
+
});
|
|
1539
|
+
|
|
1540
|
+
it("does not retry a non-zero exit after tool activity", async () => {
|
|
1541
|
+
mockPi.onCall({ jsonl: [events.toolStart("read", { path: "package.json" })], exitCode: 1 });
|
|
1542
|
+
mockPi.onCall({ output: "must not run" });
|
|
1543
|
+
const agents = [makeAgent("worker", {
|
|
1544
|
+
model: "openai/gpt-5-mini",
|
|
1545
|
+
fallbackModels: ["anthropic/claude-sonnet-4"],
|
|
1546
|
+
})];
|
|
1547
|
+
|
|
1548
|
+
const result = await runSync(tempDir, agents, "worker", "Read a file", {
|
|
1549
|
+
runId: "startup-no-retry-after-tool",
|
|
1550
|
+
acceptance: false,
|
|
1551
|
+
});
|
|
1552
|
+
|
|
1553
|
+
assert.equal(result.exitCode, 1);
|
|
1554
|
+
assert.equal(result.modelAttempts?.length, 1);
|
|
1555
|
+
assert.equal(mockPi.callCount(), 1);
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1558
|
+
it("does not retry a signaled child exit", { skip: process.platform === "win32" ? "POSIX child signal reporting is unavailable on Windows" : false }, async () => {
|
|
1559
|
+
mockPi.onCall({ signal: "SIGKILL" });
|
|
1560
|
+
mockPi.onCall({ output: "must not run" });
|
|
1561
|
+
const agents = [makeAgent("worker", { model: "openai/gpt-5-mini" })];
|
|
1562
|
+
|
|
1563
|
+
const result = await runSync(tempDir, agents, "worker", "Do work", {
|
|
1564
|
+
runId: "startup-no-retry-after-signal",
|
|
1565
|
+
acceptance: false,
|
|
1566
|
+
});
|
|
1567
|
+
|
|
1568
|
+
assert.equal(result.exitCode, 1);
|
|
1569
|
+
assert.equal(result.processSignal, "SIGKILL");
|
|
1570
|
+
assert.equal(result.error, "Subagent process terminated by signal SIGKILL.");
|
|
1571
|
+
assert.equal(result.modelAttempts?.length, 1);
|
|
1572
|
+
assert.equal(mockPi.callCount(), 1);
|
|
1573
|
+
});
|
|
1574
|
+
|
|
1575
|
+
it("prefers signal termination errors over stderr tails", { skip: process.platform === "win32" ? "POSIX child signal reporting is unavailable on Windows" : false }, async () => {
|
|
1576
|
+
mockPi.onCall({ stderr: "INFO benign startup line\n", signal: "SIGTERM" });
|
|
1577
|
+
const agents = [makeAgent("worker", { model: "openai/gpt-5-mini" })];
|
|
1578
|
+
|
|
1579
|
+
const result = await runSync(tempDir, agents, "worker", "Do work", {
|
|
1580
|
+
runId: "signal-error-over-stderr",
|
|
1581
|
+
acceptance: false,
|
|
1582
|
+
});
|
|
1583
|
+
|
|
1584
|
+
assert.equal(result.exitCode, 1);
|
|
1585
|
+
assert.equal(result.processSignal, "SIGTERM");
|
|
1586
|
+
assert.equal(result.error, "Subagent process terminated by signal SIGTERM.");
|
|
1587
|
+
assert.doesNotMatch(result.error ?? "", /INFO benign startup line/);
|
|
1588
|
+
});
|
|
1589
|
+
|
|
1590
|
+
it("does not retry a child exit with raw stdout diagnostics", async () => {
|
|
1591
|
+
mockPi.onCall({ stdoutRaw: "configuration failed before protocol startup\n", exitCode: 1 });
|
|
1592
|
+
mockPi.onCall({ output: "must not run" });
|
|
1593
|
+
const agents = [makeAgent("worker", { model: "openai/gpt-5-mini" })];
|
|
1594
|
+
|
|
1595
|
+
const result = await runSync(tempDir, agents, "worker", "Do work", {
|
|
1596
|
+
runId: "startup-no-retry-after-stdout-diagnostic",
|
|
1597
|
+
acceptance: false,
|
|
1598
|
+
});
|
|
1599
|
+
|
|
1600
|
+
assert.equal(result.exitCode, 1);
|
|
1601
|
+
assert.match(result.error ?? "", /configuration failed before protocol startup/);
|
|
1602
|
+
assert.equal(result.modelAttempts?.length, 1);
|
|
1603
|
+
assert.equal(mockPi.callCount(), 1);
|
|
1604
|
+
});
|
|
1605
|
+
|
|
1606
|
+
it("reports an actionable error after startup retries are exhausted", async () => {
|
|
1607
|
+
mockPi.onCall({ exitCode: 1 });
|
|
1608
|
+
const agents = [makeAgent("worker", {
|
|
1609
|
+
model: "openai/gpt-5-mini",
|
|
1610
|
+
fallbackModels: ["anthropic/claude-sonnet-4"],
|
|
1611
|
+
})];
|
|
1612
|
+
|
|
1613
|
+
const result = await runSync(tempDir, agents, "worker", "Do work", {
|
|
1614
|
+
runId: "startup-retry-exhausted-sync",
|
|
1615
|
+
acceptance: false,
|
|
1616
|
+
});
|
|
1617
|
+
|
|
1618
|
+
assert.equal(result.exitCode, 1);
|
|
1619
|
+
assert.equal(result.modelAttempts?.length, 4);
|
|
1620
|
+
assert.deepEqual(result.attemptedModels, ["openai/gpt-5-mini"]);
|
|
1621
|
+
assert.match(result.error ?? "", /failed to start after 4 attempts.*concurrent Pi startup race/i);
|
|
1622
|
+
assert.equal(mockPi.callCount(), 4);
|
|
1623
|
+
});
|
|
1624
|
+
|
|
1419
1625
|
it("handles long tasks via temp file (ENAMETOOLONG prevention)", async () => {
|
|
1420
1626
|
mockPi.onCall({ output: "Got it" });
|
|
1421
1627
|
const longTask = "Analyze ".repeat(2000); // ~16KB
|
|
@@ -1534,6 +1740,42 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
1534
1740
|
assert.equal(result.usage.output, 50); // from mock
|
|
1535
1741
|
});
|
|
1536
1742
|
|
|
1743
|
+
it("advances to a fallback model after a recovered startup race and provider failure", async () => {
|
|
1744
|
+
mockPi.onCall({ exitCode: 1 });
|
|
1745
|
+
mockPi.onCall({
|
|
1746
|
+
jsonl: [{
|
|
1747
|
+
type: "message_end",
|
|
1748
|
+
message: {
|
|
1749
|
+
role: "assistant",
|
|
1750
|
+
content: [{ type: "text", text: "temporary provider failure" }],
|
|
1751
|
+
model: "openai/gpt-5-mini",
|
|
1752
|
+
errorMessage: "rate limit exceeded",
|
|
1753
|
+
usage: { input: 10, output: 5, cacheRead: 0, cacheWrite: 0, cost: { total: 0.01 } },
|
|
1754
|
+
},
|
|
1755
|
+
}],
|
|
1756
|
+
exitCode: 1,
|
|
1757
|
+
});
|
|
1758
|
+
mockPi.onCall({ output: "Recovered on fallback" });
|
|
1759
|
+
const agents = [makeAgent("echo", {
|
|
1760
|
+
model: "openai/gpt-5-mini",
|
|
1761
|
+
fallbackModels: ["anthropic/claude-sonnet-4"],
|
|
1762
|
+
})];
|
|
1763
|
+
|
|
1764
|
+
const result = await runSync(tempDir, agents, "echo", "Task", {
|
|
1765
|
+
runId: "startup-then-fallback-sync",
|
|
1766
|
+
acceptance: false,
|
|
1767
|
+
});
|
|
1768
|
+
|
|
1769
|
+
assert.equal(result.exitCode, 0);
|
|
1770
|
+
assert.equal(result.model, "anthropic/claude-sonnet-4");
|
|
1771
|
+
assert.deepEqual(result.attemptedModels, [
|
|
1772
|
+
"openai/gpt-5-mini",
|
|
1773
|
+
"anthropic/claude-sonnet-4",
|
|
1774
|
+
]);
|
|
1775
|
+
assert.deepEqual(result.modelAttempts?.map((attempt) => attempt.success), [false, false, true]);
|
|
1776
|
+
assert.equal(mockPi.callCount(), 3);
|
|
1777
|
+
});
|
|
1778
|
+
|
|
1537
1779
|
it("retries with fallback models on retryable provider failures", async () => {
|
|
1538
1780
|
mockPi.onCall({
|
|
1539
1781
|
jsonl: [{
|
|
@@ -2020,8 +2262,12 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
2020
2262
|
});
|
|
2021
2263
|
|
|
2022
2264
|
it("writes artifacts when configured", async () => {
|
|
2023
|
-
mockPi.onCall({
|
|
2024
|
-
|
|
2265
|
+
mockPi.onCall({
|
|
2266
|
+
output: "Result text",
|
|
2267
|
+
runtimeAcknowledgedExtensions: { version: 1, source: "child-runtime", ids: ["ext.ok"], omitted: 0 },
|
|
2268
|
+
});
|
|
2269
|
+
const privateExtension = path.join(tempDir, "extensions", "private-extension.ts");
|
|
2270
|
+
const agents = [makeAgent("echo", { extensions: [privateExtension] })];
|
|
2025
2271
|
const artifactsDir = path.join(tempDir, "artifacts");
|
|
2026
2272
|
|
|
2027
2273
|
const result = await runSync(tempDir, agents, "echo", "Task", {
|
|
@@ -2041,8 +2287,37 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
2041
2287
|
assert.match(transcript.at(-1)?.text ?? "", /^Result text/);
|
|
2042
2288
|
assert.equal(result.transcriptError, undefined);
|
|
2043
2289
|
assert.ok(fs.existsSync(artifactsDir), "artifacts dir should exist");
|
|
2044
|
-
const metadata = JSON.parse(fs.readFileSync(result.artifactPaths.metadataPath, "utf-8")) as { launchContractDigest?: string };
|
|
2290
|
+
const metadata = JSON.parse(fs.readFileSync(result.artifactPaths.metadataPath, "utf-8")) as { launchContractDigest?: string; launchResolvedExtensions?: LaunchResolvedExtensions; runtimeAcknowledgedExtensions?: RuntimeAcknowledgedExtensions };
|
|
2045
2291
|
assert.equal(metadata.launchContractDigest, result.launchContractDigest);
|
|
2292
|
+
assert.equal(result.launchResolvedExtensions?.source, "launch-resolved");
|
|
2293
|
+
assert.equal(result.launchResolvedExtensions?.disableAmbientExtensions, true);
|
|
2294
|
+
assert.deepEqual(metadata.launchResolvedExtensions, result.launchResolvedExtensions);
|
|
2295
|
+
assert.deepEqual(result.runtimeAcknowledgedExtensions, { version: 1, source: "child-runtime", ids: ["ext.ok"], omitted: 0 });
|
|
2296
|
+
assert.deepEqual(metadata.runtimeAcknowledgedExtensions, result.runtimeAcknowledgedExtensions);
|
|
2297
|
+
assert.ok(!JSON.stringify(result.launchResolvedExtensions).includes(tempDir), "projection should not expose raw extension paths");
|
|
2298
|
+
});
|
|
2299
|
+
|
|
2300
|
+
it("does not write debug artifacts by default", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
2301
|
+
mockPi.onCall({ output: "plain result" });
|
|
2302
|
+
const executor = makeExecutor([makeAgent("echo")]);
|
|
2303
|
+
const result = await executor.execute(
|
|
2304
|
+
"default-no-artifacts",
|
|
2305
|
+
{ agent: "echo", task: "Run without debug artifacts", runId: "default-no-artifacts" },
|
|
2306
|
+
new AbortController().signal,
|
|
2307
|
+
undefined,
|
|
2308
|
+
makeMinimalCtx(tempDir),
|
|
2309
|
+
);
|
|
2310
|
+
|
|
2311
|
+
assert.equal(result.isError, undefined);
|
|
2312
|
+
assert.equal(result.details?.artifacts, undefined);
|
|
2313
|
+
// Generated durable output persists even with artifacts:false; per-child
|
|
2314
|
+
// debug artifacts (_input/_output/_meta/_transcript) remain opt-in.
|
|
2315
|
+
const defaultOutputsDir = path.join(tempDir, ".pi-subagents", "artifacts", "outputs");
|
|
2316
|
+
const defaultRunDirs = fs.readdirSync(defaultOutputsDir);
|
|
2317
|
+
assert.equal(defaultRunDirs.length, 1);
|
|
2318
|
+
const defaultRunDir = path.join(defaultOutputsDir, defaultRunDirs[0]!);
|
|
2319
|
+
assert.equal(fs.readFileSync(path.join(defaultRunDir, "result.md"), "utf-8"), "plain result");
|
|
2320
|
+
assert.deepEqual(fs.readdirSync(defaultRunDir), ["result.md"]);
|
|
2046
2321
|
});
|
|
2047
2322
|
|
|
2048
2323
|
it("routes foreground artifacts to the configured session directory", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
@@ -2054,7 +2329,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
2054
2329
|
|
|
2055
2330
|
const result = await executor.execute(
|
|
2056
2331
|
"session-artifact-dir",
|
|
2057
|
-
{ agent: "echo", task: "Write session-scoped artifacts", runId: "session-artifacts" },
|
|
2332
|
+
{ agent: "echo", task: "Write session-scoped artifacts", runId: "session-artifacts", artifacts: true },
|
|
2058
2333
|
new AbortController().signal,
|
|
2059
2334
|
undefined,
|
|
2060
2335
|
ctx,
|
|
@@ -2331,6 +2606,53 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
2331
2606
|
assert.equal(mockPi.callCount(), 0);
|
|
2332
2607
|
});
|
|
2333
2608
|
|
|
2609
|
+
it("applies the foreground timeout default without overriding explicit or agent values", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
2610
|
+
mockPi.onCall({ output: "package default" });
|
|
2611
|
+
mockPi.onCall({ output: "explicit timeout" });
|
|
2612
|
+
mockPi.onCall({ output: "max runtime alias" });
|
|
2613
|
+
mockPi.onCall({ output: "agent timeout" });
|
|
2614
|
+
|
|
2615
|
+
const defaultExecutor = makeExecutor();
|
|
2616
|
+
const defaultResult = await defaultExecutor.execute(
|
|
2617
|
+
"foreground-timeout-default",
|
|
2618
|
+
{ agent: "echo", task: "Task" },
|
|
2619
|
+
new AbortController().signal,
|
|
2620
|
+
undefined,
|
|
2621
|
+
makeMinimalCtx(tempDir),
|
|
2622
|
+
);
|
|
2623
|
+
assert.equal(defaultResult.details?.timeoutMs, executorMod?.DEFAULT_FOREGROUND_TIMEOUT_MS);
|
|
2624
|
+
assert.equal(defaultResult.details?.timeoutMs, 30 * 60 * 1000);
|
|
2625
|
+
|
|
2626
|
+
const explicitResult = await defaultExecutor.execute(
|
|
2627
|
+
"foreground-timeout-explicit",
|
|
2628
|
+
{ agent: "echo", task: "Task", async: false, timeoutMs: 2_000 },
|
|
2629
|
+
new AbortController().signal,
|
|
2630
|
+
undefined,
|
|
2631
|
+
makeMinimalCtx(tempDir),
|
|
2632
|
+
);
|
|
2633
|
+
assert.equal(explicitResult.details?.timeoutMs, 2_000);
|
|
2634
|
+
|
|
2635
|
+
const aliasResult = await defaultExecutor.execute(
|
|
2636
|
+
"foreground-timeout-alias",
|
|
2637
|
+
{ agent: "echo", task: "Task", async: false, maxRuntimeMs: 3_000 },
|
|
2638
|
+
new AbortController().signal,
|
|
2639
|
+
undefined,
|
|
2640
|
+
makeMinimalCtx(tempDir),
|
|
2641
|
+
);
|
|
2642
|
+
assert.equal(aliasResult.details?.timeoutMs, 3_000);
|
|
2643
|
+
|
|
2644
|
+
const agentResult = await makeExecutor([
|
|
2645
|
+
makeAgent("echo", { defaultTimeoutMs: 4_000 }),
|
|
2646
|
+
]).execute(
|
|
2647
|
+
"foreground-timeout-agent-default",
|
|
2648
|
+
{ agent: "echo", task: "Task", async: false },
|
|
2649
|
+
new AbortController().signal,
|
|
2650
|
+
undefined,
|
|
2651
|
+
makeMinimalCtx(tempDir),
|
|
2652
|
+
);
|
|
2653
|
+
assert.equal(agentResult.details?.timeoutMs, 4_000);
|
|
2654
|
+
});
|
|
2655
|
+
|
|
2334
2656
|
it("applies agent frontmatter defaults to single-agent launches", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
2335
2657
|
const executor = makeExecutor([
|
|
2336
2658
|
makeAgent("echo", {
|
|
@@ -2401,7 +2723,8 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
2401
2723
|
);
|
|
2402
2724
|
|
|
2403
2725
|
assert.equal(result.isError, undefined);
|
|
2404
|
-
assert.match(result.content[0]?.text ?? "", /
|
|
2726
|
+
assert.match(result.content[0]?.text ?? "", /Output saved to: /);
|
|
2727
|
+
assert.doesNotMatch(result.content[0]?.text ?? "", /agent foreground default finished/);
|
|
2405
2728
|
assert.equal(result.details?.asyncId, undefined);
|
|
2406
2729
|
});
|
|
2407
2730
|
|
|
@@ -2430,7 +2753,8 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
2430
2753
|
);
|
|
2431
2754
|
|
|
2432
2755
|
assert.equal(result.isError, undefined);
|
|
2433
|
-
assert.match(result.content[0]?.text ?? "", /
|
|
2756
|
+
assert.match(result.content[0]?.text ?? "", /Output saved to: /);
|
|
2757
|
+
assert.doesNotMatch(result.content[0]?.text ?? "", /explicit foreground finished/);
|
|
2434
2758
|
assert.equal(result.details?.asyncId, undefined);
|
|
2435
2759
|
});
|
|
2436
2760
|
|
|
@@ -3006,6 +3330,464 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
3006
3330
|
assert.match(result.finalOutput ?? "", /Interrupted/);
|
|
3007
3331
|
});
|
|
3008
3332
|
|
|
3333
|
+
it("supports synchronous user detach and rejects duplicate and late detach calls", async () => {
|
|
3334
|
+
mockPi.onCall({ steps: [
|
|
3335
|
+
{ delay: 500, jsonl: [events.assistantMessage("completed after user detach")] },
|
|
3336
|
+
] });
|
|
3337
|
+
let detachActive: ((reason?: string) => boolean) | undefined;
|
|
3338
|
+
let detachAccepted = false;
|
|
3339
|
+
let duplicateAccepted = true;
|
|
3340
|
+
let recoveredResult: RunSyncResult | undefined;
|
|
3341
|
+
|
|
3342
|
+
const result = await runSync(tempDir, makeAgentConfigs(["echo"]), "echo", "Keep working", {
|
|
3343
|
+
runId: "user-foreground-detach",
|
|
3344
|
+
acceptance: false,
|
|
3345
|
+
onDetachReady: (detach: (reason?: string) => boolean) => {
|
|
3346
|
+
detachActive = detach;
|
|
3347
|
+
detachAccepted = detach("user request");
|
|
3348
|
+
duplicateAccepted = detach("user request");
|
|
3349
|
+
},
|
|
3350
|
+
onDetachedExit: (postExit) => { recoveredResult = postExit as RunSyncResult; },
|
|
3351
|
+
});
|
|
3352
|
+
|
|
3353
|
+
assert.equal(detachAccepted, true);
|
|
3354
|
+
assert.equal(duplicateAccepted, false);
|
|
3355
|
+
assert.equal(recoveredResult, undefined, "foreground result should return before the child completes");
|
|
3356
|
+
assert.equal(result.exitCode, -2);
|
|
3357
|
+
assert.equal(result.detached, true);
|
|
3358
|
+
assert.equal(result.detachedReason, "user request");
|
|
3359
|
+
assert.equal(result.finalOutput, "Detached at user request before task completion.");
|
|
3360
|
+
assert.equal(result.processSignal, undefined);
|
|
3361
|
+
|
|
3362
|
+
for (let attempt = 0; attempt < 100 && !recoveredResult; attempt++) await new Promise((resolve) => setTimeout(resolve, 20));
|
|
3363
|
+
assert.ok(recoveredResult);
|
|
3364
|
+
assert.equal(recoveredResult.exitCode, 0);
|
|
3365
|
+
assert.equal(recoveredResult.processSignal, undefined);
|
|
3366
|
+
assert.equal(recoveredResult.finalOutput, "completed after user detach");
|
|
3367
|
+
assert.equal(detachActive?.("user request"), false, "detach must reject calls after child exit");
|
|
3368
|
+
});
|
|
3369
|
+
|
|
3370
|
+
it("produces the same authoritative terminal result attached and detached", async () => {
|
|
3371
|
+
mockPi.onCall({ output: "authoritative answer" });
|
|
3372
|
+
mockPi.onCall({ steps: [{ delay: 75, jsonl: [events.assistantMessage("authoritative answer")] }] });
|
|
3373
|
+
const agents = makeAgentConfigs(["echo"]);
|
|
3374
|
+
const attached = await runSync(tempDir, agents, "echo", "Equivalent task", {
|
|
3375
|
+
runId: "attached-authoritative-result",
|
|
3376
|
+
acceptance: false,
|
|
3377
|
+
});
|
|
3378
|
+
let terminal: RunSyncResult | undefined;
|
|
3379
|
+
const receipt = await runSync(tempDir, agents, "echo", "Equivalent task", {
|
|
3380
|
+
runId: "detached-authoritative-result",
|
|
3381
|
+
acceptance: false,
|
|
3382
|
+
onDetachReady: (detach) => assert.equal(detach("user request"), true),
|
|
3383
|
+
onDetachedExit: (result) => { terminal = result as RunSyncResult; },
|
|
3384
|
+
});
|
|
3385
|
+
assert.equal(receipt.detached, true);
|
|
3386
|
+
for (let attempt = 0; attempt < 100 && !terminal; attempt++) await new Promise((resolve) => setTimeout(resolve, 20));
|
|
3387
|
+
assert.ok(terminal);
|
|
3388
|
+
assert.deepEqual(
|
|
3389
|
+
{
|
|
3390
|
+
exitCode: terminal.exitCode,
|
|
3391
|
+
finalOutput: terminal.finalOutput,
|
|
3392
|
+
usage: terminal.usage,
|
|
3393
|
+
progressStatus: terminal.progress.status,
|
|
3394
|
+
acceptanceStatus: terminal.acceptance?.status,
|
|
3395
|
+
},
|
|
3396
|
+
{
|
|
3397
|
+
exitCode: attached.exitCode,
|
|
3398
|
+
finalOutput: attached.finalOutput,
|
|
3399
|
+
usage: attached.usage,
|
|
3400
|
+
progressStatus: attached.progress.status,
|
|
3401
|
+
acceptanceStatus: attached.acceptance?.status,
|
|
3402
|
+
},
|
|
3403
|
+
);
|
|
3404
|
+
assert.equal(terminal.detached, undefined);
|
|
3405
|
+
assert.equal(terminal.detachedReason, "user request");
|
|
3406
|
+
});
|
|
3407
|
+
|
|
3408
|
+
it("isolates every nested detach receipt field from terminal completion and later sanitization", async () => {
|
|
3409
|
+
const receiptReport = [
|
|
3410
|
+
"receipt snapshot",
|
|
3411
|
+
"```acceptance-report",
|
|
3412
|
+
JSON.stringify({
|
|
3413
|
+
criteriaSatisfied: [{ id: "criterion-1", status: "satisfied", evidence: "receipt evidence" }],
|
|
3414
|
+
changedFiles: ["src/receipt.ts"],
|
|
3415
|
+
testsAddedOrUpdated: ["test/receipt.test.ts"],
|
|
3416
|
+
commandsRun: [{ command: "npm test", result: "passed", summary: "passed" }],
|
|
3417
|
+
residualRisks: [],
|
|
3418
|
+
noStagedFiles: true,
|
|
3419
|
+
}),
|
|
3420
|
+
"```",
|
|
3421
|
+
].join("\n");
|
|
3422
|
+
const terminalReport = [
|
|
3423
|
+
"terminal answer",
|
|
3424
|
+
"```acceptance-report",
|
|
3425
|
+
JSON.stringify({
|
|
3426
|
+
criteriaSatisfied: [{ id: "criterion-1", status: "satisfied", evidence: "terminal isolation verified" }],
|
|
3427
|
+
changedFiles: ["src/receipt.ts"],
|
|
3428
|
+
testsAddedOrUpdated: ["test/receipt.test.ts"],
|
|
3429
|
+
commandsRun: [{ command: "npm test", result: "passed", summary: "passed" }],
|
|
3430
|
+
residualRisks: [],
|
|
3431
|
+
noStagedFiles: true,
|
|
3432
|
+
}),
|
|
3433
|
+
"```",
|
|
3434
|
+
].join("\n");
|
|
3435
|
+
mockPi.onCall({ steps: [
|
|
3436
|
+
{ jsonl: [{
|
|
3437
|
+
type: "message_end",
|
|
3438
|
+
message: {
|
|
3439
|
+
role: "assistant",
|
|
3440
|
+
content: [{ type: "text", text: receiptReport }],
|
|
3441
|
+
model: "mock/test-model",
|
|
3442
|
+
stopReason: "toolUse",
|
|
3443
|
+
usage: { input: 7, output: 3, cacheRead: 0, cacheWrite: 0, cost: { total: 0.001 } },
|
|
3444
|
+
},
|
|
3445
|
+
}] },
|
|
3446
|
+
{ delay: 300, jsonl: [events.assistantMessage(terminalReport)] },
|
|
3447
|
+
] });
|
|
3448
|
+
let detach: ((reason?: string) => boolean) | undefined;
|
|
3449
|
+
let detached = false;
|
|
3450
|
+
let terminal: RunSyncResult | undefined;
|
|
3451
|
+
const receipt = await runSync(tempDir, makeAgentConfigs(["echo"]), "echo", "Keep the receipt isolated", {
|
|
3452
|
+
runId: "detached-deep-receipt-isolation",
|
|
3453
|
+
agentContract: { version: 1 },
|
|
3454
|
+
acceptance: {
|
|
3455
|
+
level: "checked",
|
|
3456
|
+
criteria: [{
|
|
3457
|
+
id: "criterion-1",
|
|
3458
|
+
must: "Keep detach receipt state isolated",
|
|
3459
|
+
evidence: ["changed-files", "tests-added", "commands-run", "residual-risks", "no-staged-files"],
|
|
3460
|
+
}],
|
|
3461
|
+
},
|
|
3462
|
+
onDetachReady: (detachAttempt) => { detach = detachAttempt; },
|
|
3463
|
+
onUpdate: (update: { content?: Array<{ text?: string }> }) => {
|
|
3464
|
+
if (detached || !update.content?.[0]?.text?.includes("receipt snapshot")) return;
|
|
3465
|
+
detached = detach?.("user request") === true;
|
|
3466
|
+
},
|
|
3467
|
+
onDetachedExit: (result) => { terminal = result as RunSyncResult; },
|
|
3468
|
+
});
|
|
3469
|
+
|
|
3470
|
+
assert.equal(receipt.detached, true);
|
|
3471
|
+
const receiptMessages = receipt.messages as Array<{
|
|
3472
|
+
role?: string;
|
|
3473
|
+
model?: string;
|
|
3474
|
+
content: Array<{ type?: string; text?: string; callerOwned?: boolean }>;
|
|
3475
|
+
}>;
|
|
3476
|
+
const callerOwnedReceiptText = `caller-owned mutation\n${receiptReport}`;
|
|
3477
|
+
(receipt.agentContract as unknown as { version: number }).version = 999;
|
|
3478
|
+
receiptMessages[0]!.model = "caller-owned/model";
|
|
3479
|
+
receiptMessages[0]!.content[0]!.text = callerOwnedReceiptText;
|
|
3480
|
+
receiptMessages[0]!.content[0]!.callerOwned = true;
|
|
3481
|
+
receiptMessages[0]!.content.push({ type: "text", text: "caller-only content" });
|
|
3482
|
+
receiptMessages.push({ role: "assistant", model: "caller-only/model", content: [{ type: "text", text: "caller-only message" }] });
|
|
3483
|
+
const mutableAcceptance = receipt.acceptance as unknown as {
|
|
3484
|
+
status: string;
|
|
3485
|
+
effectiveAcceptance: { level: string; criteria: Array<{ must: string }> };
|
|
3486
|
+
criteria: Array<{ must: string }>;
|
|
3487
|
+
};
|
|
3488
|
+
mutableAcceptance.status = "rejected";
|
|
3489
|
+
mutableAcceptance.effectiveAcceptance.level = "none";
|
|
3490
|
+
mutableAcceptance.effectiveAcceptance.criteria[0]!.must = "caller corrupted effective criterion";
|
|
3491
|
+
mutableAcceptance.criteria[0]!.must = "caller corrupted ledger criterion";
|
|
3492
|
+
receipt.progress.status = "failed";
|
|
3493
|
+
(receipt.progress as unknown as { recentOutput: string[] }).recentOutput.push("caller-only progress");
|
|
3494
|
+
receipt.usage.turns = 999;
|
|
3495
|
+
receipt.usage.input = 999;
|
|
3496
|
+
receipt.attemptedModels = ["caller-only/model"];
|
|
3497
|
+
receipt.modelAttempts = [{ success: false, exitCode: 99, error: "caller-only attempt" }];
|
|
3498
|
+
receipt.effects = { fileMutation: { status: "missing", expected: true, attempted: false, message: "caller-only effect" } };
|
|
3499
|
+
receipt.execution = { status: "failed", success: false, exitCode: 99 };
|
|
3500
|
+
receipt.review = { status: "blockers" };
|
|
3501
|
+
|
|
3502
|
+
for (let attempt = 0; attempt < 100 && !terminal; attempt++) await new Promise((resolve) => setTimeout(resolve, 20));
|
|
3503
|
+
assert.ok(terminal);
|
|
3504
|
+
assert.equal(terminal.exitCode, 0);
|
|
3505
|
+
assert.equal(terminal.finalOutput, "terminal answer");
|
|
3506
|
+
assert.deepEqual(terminal.agentContract, { version: 1 });
|
|
3507
|
+
assert.equal(terminal.acceptance?.status, "checked");
|
|
3508
|
+
assert.equal(terminal.acceptance?.runtimeChecks.every((check) => check.status === "passed"), true);
|
|
3509
|
+
assert.equal(terminal.progress.status, "completed");
|
|
3510
|
+
assert.deepEqual(terminal.usage, { turns: 2, input: 107, output: 53, cacheRead: 0, cacheWrite: 0, cost: 0.002 });
|
|
3511
|
+
assert.deepEqual(terminal.attemptedModels, ["mock/test-model"]);
|
|
3512
|
+
assert.deepEqual(terminal.modelAttempts?.map((attempt) => ({ success: attempt.success, exitCode: attempt.exitCode })), [{ success: true, exitCode: 0 }]);
|
|
3513
|
+
assert.equal(terminal.execution?.status, "completed");
|
|
3514
|
+
assert.equal(terminal.execution?.success, true);
|
|
3515
|
+
assert.equal(terminal.review?.status, "not-requested");
|
|
3516
|
+
assert.deepEqual(terminal.effects, {});
|
|
3517
|
+
assert.doesNotMatch(JSON.stringify(terminal.messages), /acceptance-report/);
|
|
3518
|
+
assert.equal(receiptMessages[0]!.content[0]!.text, callerOwnedReceiptText, "terminal report sanitization must not mutate the caller receipt");
|
|
3519
|
+
assert.equal(receiptMessages[0]!.content[0]!.callerOwned, true);
|
|
3520
|
+
assert.equal(receiptMessages.length, 2);
|
|
3521
|
+
});
|
|
3522
|
+
|
|
3523
|
+
it("keeps the full fallback loop and authoritative aggregation alive after detach", async () => {
|
|
3524
|
+
mockPi.onCall({
|
|
3525
|
+
jsonl: [{
|
|
3526
|
+
type: "message_end",
|
|
3527
|
+
message: {
|
|
3528
|
+
role: "assistant",
|
|
3529
|
+
content: [{ type: "text", text: "temporary provider failure" }],
|
|
3530
|
+
model: "openai/gpt-5-mini",
|
|
3531
|
+
errorMessage: "rate limit exceeded",
|
|
3532
|
+
usage: { input: 10, output: 5, cacheRead: 0, cacheWrite: 0, cost: { total: 0.01 } },
|
|
3533
|
+
},
|
|
3534
|
+
}],
|
|
3535
|
+
exitCode: 1,
|
|
3536
|
+
});
|
|
3537
|
+
mockPi.onCall({ output: "Recovered on detached fallback" });
|
|
3538
|
+
const agents = [makeAgent("echo", {
|
|
3539
|
+
model: "openai/gpt-5-mini",
|
|
3540
|
+
fallbackModels: ["anthropic/claude-sonnet-4"],
|
|
3541
|
+
})];
|
|
3542
|
+
let terminal: RunSyncResult | undefined;
|
|
3543
|
+
|
|
3544
|
+
const receipt = await runSync(tempDir, agents, "echo", "Task", {
|
|
3545
|
+
runId: "detached-fallback-loop",
|
|
3546
|
+
acceptance: false,
|
|
3547
|
+
onDetachReady: (detach) => assert.equal(detach("user request"), true),
|
|
3548
|
+
onDetachedExit: (result) => { terminal = result as RunSyncResult; },
|
|
3549
|
+
});
|
|
3550
|
+
|
|
3551
|
+
assert.equal(receipt.detached, true);
|
|
3552
|
+
for (let attempt = 0; attempt < 300 && !terminal; attempt++) await new Promise((resolve) => setTimeout(resolve, 20));
|
|
3553
|
+
assert.ok(terminal);
|
|
3554
|
+
assert.equal(terminal.detached, undefined, "terminal status must not remain detached");
|
|
3555
|
+
assert.equal(terminal.detachedReason, "user request");
|
|
3556
|
+
assert.equal(terminal.exitCode, 0);
|
|
3557
|
+
assert.equal(terminal.finalOutput, "Recovered on detached fallback");
|
|
3558
|
+
assert.deepEqual(terminal.attemptedModels, ["openai/gpt-5-mini", "anthropic/claude-sonnet-4"]);
|
|
3559
|
+
assert.deepEqual(terminal.modelAttempts?.map((attempt) => attempt.success), [false, true]);
|
|
3560
|
+
assert.equal(terminal.usage.turns, 2);
|
|
3561
|
+
assert.equal(mockPi.callCount(), 2);
|
|
3562
|
+
});
|
|
3563
|
+
|
|
3564
|
+
it("terminalizes a post-receipt completion pipeline throw exactly once with strict projections", async () => {
|
|
3565
|
+
mockPi.onCall({ steps: [{ delay: 75, jsonl: [events.assistantMessage("answer before callback failure")] }] });
|
|
3566
|
+
let terminal: RunSyncResult | undefined;
|
|
3567
|
+
let callbackCount = 0;
|
|
3568
|
+
const receipt = await runSync(tempDir, makeAgentConfigs(["echo"]), "echo", "Task", {
|
|
3569
|
+
runId: "detached-completion-pipeline-throw",
|
|
3570
|
+
acceptance: { level: "checked", criteria: ["result is checked"] },
|
|
3571
|
+
agentContract: { version: 1 },
|
|
3572
|
+
onDetachReady: (detach) => {
|
|
3573
|
+
assert.equal(detach("user request"), true);
|
|
3574
|
+
},
|
|
3575
|
+
onUpdate: (update: { details?: { progress?: Array<{ status?: string }> } }) => {
|
|
3576
|
+
if (update.details?.progress?.[0]?.status === "completed") throw new Error("terminal consumer update failed");
|
|
3577
|
+
},
|
|
3578
|
+
onDetachedExit: (result) => {
|
|
3579
|
+
callbackCount++;
|
|
3580
|
+
terminal = result as RunSyncResult;
|
|
3581
|
+
},
|
|
3582
|
+
});
|
|
3583
|
+
assert.equal(receipt.detached, true);
|
|
3584
|
+
(receipt.agentContract as unknown as { version: number }).version = 999;
|
|
3585
|
+
receipt.messages.push({ role: "assistant", content: [{ type: "text", text: "caller-only fallback message" }] });
|
|
3586
|
+
const mutableAcceptance = receipt.acceptance as unknown as {
|
|
3587
|
+
status: string;
|
|
3588
|
+
effectiveAcceptance: { level: string; criteria: Array<{ must: string }> };
|
|
3589
|
+
};
|
|
3590
|
+
mutableAcceptance.status = "accepted";
|
|
3591
|
+
mutableAcceptance.effectiveAcceptance.level = "none";
|
|
3592
|
+
mutableAcceptance.effectiveAcceptance.criteria[0]!.must = "caller-only fallback criterion";
|
|
3593
|
+
receipt.progress.status = "completed";
|
|
3594
|
+
receipt.usage.turns = 999;
|
|
3595
|
+
receipt.usage.input = 999;
|
|
3596
|
+
receipt.attemptedModels = ["caller-only/fallback"];
|
|
3597
|
+
receipt.modelAttempts = [{ success: true, exitCode: 0 }];
|
|
3598
|
+
receipt.effects = { fileMutation: { status: "observed", expected: false, attempted: true, message: "caller-only fallback effect" } };
|
|
3599
|
+
for (let attempt = 0; attempt < 100 && !terminal; attempt++) await new Promise((resolve) => setTimeout(resolve, 20));
|
|
3600
|
+
assert.ok(terminal);
|
|
3601
|
+
assert.equal(callbackCount, 1);
|
|
3602
|
+
assert.equal(terminal.exitCode, 1);
|
|
3603
|
+
assert.equal(terminal.detached, undefined);
|
|
3604
|
+
assert.equal(terminal.detachedReason, "user request");
|
|
3605
|
+
assert.equal(terminal.progress?.status, "failed");
|
|
3606
|
+
assert.equal(terminal.acceptance?.status, "rejected");
|
|
3607
|
+
assert.equal(terminal.acceptance?.runtimeChecks?.[0]?.id, "completion-pipeline");
|
|
3608
|
+
assert.equal(terminal.execution?.status, "failed");
|
|
3609
|
+
assert.equal(terminal.execution?.success, false);
|
|
3610
|
+
assert.equal(terminal.review?.status, "not-requested");
|
|
3611
|
+
assert.deepEqual(terminal.agentContract, { version: 1 });
|
|
3612
|
+
assert.deepEqual(terminal.effects, {});
|
|
3613
|
+
assert.equal(terminal.usage.turns, 0);
|
|
3614
|
+
assert.equal(terminal.attemptedModels, undefined);
|
|
3615
|
+
assert.equal(terminal.modelAttempts, undefined);
|
|
3616
|
+
assert.doesNotMatch(JSON.stringify(terminal.messages), /caller-only fallback message/);
|
|
3617
|
+
assert.match(terminal.error ?? "", /Detached completion pipeline failed after receipt/);
|
|
3618
|
+
});
|
|
3619
|
+
|
|
3620
|
+
it("contains a synchronous onDetachReady throw and completes attached", async () => {
|
|
3621
|
+
mockPi.onCall({ output: "completed while attached" });
|
|
3622
|
+
const result = await runSync(tempDir, makeAgentConfigs(["echo"]), "echo", "Task", {
|
|
3623
|
+
runId: "throwing-detach-ready-consumer",
|
|
3624
|
+
acceptance: false,
|
|
3625
|
+
onDetachReady: () => {
|
|
3626
|
+
throw new Error("bad detach consumer");
|
|
3627
|
+
},
|
|
3628
|
+
});
|
|
3629
|
+
assert.equal(result.exitCode, 0);
|
|
3630
|
+
assert.equal(result.detached, undefined);
|
|
3631
|
+
assert.equal(result.finalOutput, "completed while attached");
|
|
3632
|
+
assert.equal(result.progress.recentOutput.some((line) => /Foreground detach callback failed: bad detach consumer/.test(line)), true);
|
|
3633
|
+
});
|
|
3634
|
+
|
|
3635
|
+
it("reports expected artifact post-processing I/O failures without rejecting", async () => {
|
|
3636
|
+
mockPi.onCall({ steps: [
|
|
3637
|
+
{ jsonl: [events.toolStart("read", { path: "README.md" })] },
|
|
3638
|
+
{ delay: 50, jsonl: [events.assistantMessage("artifact answer")] },
|
|
3639
|
+
] });
|
|
3640
|
+
const artifactsDir = path.join(tempDir, "artifact-output-failure");
|
|
3641
|
+
let sabotaged = false;
|
|
3642
|
+
const result = await runSync(tempDir, makeAgentConfigs(["echo"]), "echo", "Task", {
|
|
3643
|
+
runId: "artifact-output-result-field",
|
|
3644
|
+
acceptance: false,
|
|
3645
|
+
artifactsDir,
|
|
3646
|
+
artifactConfig: { enabled: true },
|
|
3647
|
+
onUpdate: (update: { details?: { results?: Array<{ artifactPaths?: { outputPath?: string } }> } }) => {
|
|
3648
|
+
const outputPath = update.details?.results?.[0]?.artifactPaths?.outputPath;
|
|
3649
|
+
if (sabotaged || !outputPath) return;
|
|
3650
|
+
sabotaged = true;
|
|
3651
|
+
fs.mkdirSync(outputPath);
|
|
3652
|
+
},
|
|
3653
|
+
});
|
|
3654
|
+
assert.equal(sabotaged, true);
|
|
3655
|
+
assert.equal(result.exitCode, 0);
|
|
3656
|
+
assert.match(result.outputSaveError ?? "", /Artifact output post-processing failed/);
|
|
3657
|
+
});
|
|
3658
|
+
|
|
3659
|
+
it("publishes detach despite best-effort receipt metadata persistence failure", async () => {
|
|
3660
|
+
mockPi.onCall({ steps: [{ delay: 100, jsonl: [events.assistantMessage("completed after metadata recovery")] }] });
|
|
3661
|
+
const artifactsDir = path.join(tempDir, "receipt-metadata-failure");
|
|
3662
|
+
let terminal: RunSyncResult | undefined;
|
|
3663
|
+
const receipt = await runSync(tempDir, makeAgentConfigs(["echo"]), "echo", "Task", {
|
|
3664
|
+
runId: "detach-receipt-metadata-failure",
|
|
3665
|
+
acceptance: false,
|
|
3666
|
+
artifactsDir,
|
|
3667
|
+
artifactConfig: { enabled: true },
|
|
3668
|
+
onDetachReady: (detach) => {
|
|
3669
|
+
fs.rmSync(artifactsDir, { recursive: true, force: true });
|
|
3670
|
+
fs.writeFileSync(artifactsDir, "block metadata", "utf-8");
|
|
3671
|
+
assert.equal(detach("user request"), true);
|
|
3672
|
+
fs.rmSync(artifactsDir, { force: true });
|
|
3673
|
+
fs.mkdirSync(artifactsDir, { recursive: true });
|
|
3674
|
+
},
|
|
3675
|
+
onDetachedExit: (result) => { terminal = result as RunSyncResult; },
|
|
3676
|
+
});
|
|
3677
|
+
assert.equal(receipt.detached, true);
|
|
3678
|
+
assert.ok(receipt.metadataSaveError, "receipt should record best-effort metadata persistence failure");
|
|
3679
|
+
for (let attempt = 0; attempt < 100 && !terminal; attempt++) await new Promise((resolve) => setTimeout(resolve, 20));
|
|
3680
|
+
assert.equal(terminal?.exitCode, 0);
|
|
3681
|
+
});
|
|
3682
|
+
|
|
3683
|
+
it("contains a throwing detached-exit callback", async () => {
|
|
3684
|
+
mockPi.onCall({ steps: [{ delay: 50, jsonl: [events.assistantMessage("done")] }] });
|
|
3685
|
+
let callbackCount = 0;
|
|
3686
|
+
const receipt = await runSync(tempDir, makeAgentConfigs(["echo"]), "echo", "Task", {
|
|
3687
|
+
runId: "throwing-detached-exit-callback",
|
|
3688
|
+
acceptance: false,
|
|
3689
|
+
onDetachReady: (detach) => assert.equal(detach("user request"), true),
|
|
3690
|
+
onDetachedExit: () => {
|
|
3691
|
+
callbackCount++;
|
|
3692
|
+
throw new Error("consumer callback failed");
|
|
3693
|
+
},
|
|
3694
|
+
});
|
|
3695
|
+
assert.equal(receipt.detached, true);
|
|
3696
|
+
for (let attempt = 0; attempt < 100 && callbackCount === 0; attempt++) await new Promise((resolve) => setTimeout(resolve, 20));
|
|
3697
|
+
assert.equal(callbackCount, 1);
|
|
3698
|
+
});
|
|
3699
|
+
|
|
3700
|
+
it("skips acceptance evaluation when an explicitly interrupted detached result settles", async () => {
|
|
3701
|
+
mockPi.onCall({ steps: [{ delay: 10_000, jsonl: [events.assistantMessage("too late")] }] });
|
|
3702
|
+
const interrupt = new AbortController();
|
|
3703
|
+
let terminal: RunSyncResult | undefined;
|
|
3704
|
+
const receipt = await runSync(tempDir, makeAgentConfigs(["slow"]), "slow", "Task", {
|
|
3705
|
+
runId: "detached-interrupted-acceptance",
|
|
3706
|
+
acceptance: { level: "checked", criteria: ["result is checked"] },
|
|
3707
|
+
interruptSignal: interrupt.signal,
|
|
3708
|
+
onDetachReady: (detach) => {
|
|
3709
|
+
assert.equal(detach("user request"), true);
|
|
3710
|
+
setTimeout(() => interrupt.abort(), 25);
|
|
3711
|
+
},
|
|
3712
|
+
onDetachedExit: (result) => { terminal = result as RunSyncResult; },
|
|
3713
|
+
});
|
|
3714
|
+
assert.equal(receipt.detached, true);
|
|
3715
|
+
for (let attempt = 0; attempt < 100 && !terminal; attempt++) await new Promise((resolve) => setTimeout(resolve, 20));
|
|
3716
|
+
assert.ok(terminal);
|
|
3717
|
+
assert.equal(terminal.interrupted, true);
|
|
3718
|
+
assert.equal(terminal.exitCode, 0);
|
|
3719
|
+
assert.equal(terminal.acceptance?.status, "pending");
|
|
3720
|
+
assert.equal(terminal.acceptance?.runtimeChecks[0]?.status, "not-applicable");
|
|
3721
|
+
assert.equal(terminal.error, undefined);
|
|
3722
|
+
});
|
|
3723
|
+
|
|
3724
|
+
it("linearizes originating abort against detach and keeps explicit interrupt routable afterward", async () => {
|
|
3725
|
+
mockPi.onCall({ steps: [{ delay: 10_000, jsonl: [events.assistantMessage("too late")] }] });
|
|
3726
|
+
const origin = new AbortController();
|
|
3727
|
+
const interrupt = new AbortController();
|
|
3728
|
+
let terminal: RunSyncResult | undefined;
|
|
3729
|
+
const receipt = await runSync(tempDir, makeAgentConfigs(["slow"]), "slow", "Keep working", {
|
|
3730
|
+
runId: "detach-origin-abort-race",
|
|
3731
|
+
acceptance: false,
|
|
3732
|
+
signal: origin.signal,
|
|
3733
|
+
interruptSignal: interrupt.signal,
|
|
3734
|
+
onDetachReady: (detach) => {
|
|
3735
|
+
assert.equal(detach("user request"), true);
|
|
3736
|
+
origin.abort();
|
|
3737
|
+
setTimeout(() => interrupt.abort(), 50);
|
|
3738
|
+
},
|
|
3739
|
+
onDetachedExit: (result) => { terminal = result as RunSyncResult; },
|
|
3740
|
+
});
|
|
3741
|
+
|
|
3742
|
+
assert.equal(receipt.detached, true);
|
|
3743
|
+
for (let attempt = 0; attempt < 100 && !terminal; attempt++) await new Promise((resolve) => setTimeout(resolve, 20));
|
|
3744
|
+
assert.ok(terminal);
|
|
3745
|
+
assert.equal(terminal.interrupted, true, "explicit control interrupt must remain active after detach");
|
|
3746
|
+
assert.equal(terminal.processSignal, "SIGINT");
|
|
3747
|
+
assert.equal(terminal.detached, undefined);
|
|
3748
|
+
});
|
|
3749
|
+
|
|
3750
|
+
it("lets an already-observed originating abort win over detach", async () => {
|
|
3751
|
+
mockPi.onCall({ delay: 10_000 });
|
|
3752
|
+
const origin = new AbortController();
|
|
3753
|
+
let detachAccepted = true;
|
|
3754
|
+
const result = await runSync(tempDir, makeAgentConfigs(["slow"]), "slow", "Abort first", {
|
|
3755
|
+
runId: "origin-abort-wins-detach",
|
|
3756
|
+
signal: origin.signal,
|
|
3757
|
+
onDetachReady: (detach) => {
|
|
3758
|
+
origin.abort();
|
|
3759
|
+
detachAccepted = detach("user request");
|
|
3760
|
+
},
|
|
3761
|
+
});
|
|
3762
|
+
assert.equal(detachAccepted, false);
|
|
3763
|
+
assert.equal(result.detached, undefined);
|
|
3764
|
+
});
|
|
3765
|
+
|
|
3766
|
+
it("keeps the configured runtime timeout active after user detach", async () => {
|
|
3767
|
+
mockPi.onCall({ delay: 10_000 });
|
|
3768
|
+
let recoveredResult: RunSyncResult | undefined;
|
|
3769
|
+
const startedAt = Date.now();
|
|
3770
|
+
|
|
3771
|
+
const result = await runSync(tempDir, makeAgentConfigs(["slow"]), "slow", "Do not run forever", {
|
|
3772
|
+
runId: "user-detach-timeout",
|
|
3773
|
+
timeoutMs: 150,
|
|
3774
|
+
acceptance: false,
|
|
3775
|
+
onDetachReady: (detach: (reason?: string) => boolean) => {
|
|
3776
|
+
assert.equal(detach("user request"), true);
|
|
3777
|
+
},
|
|
3778
|
+
onDetachedExit: (postExit) => { recoveredResult = postExit as RunSyncResult; },
|
|
3779
|
+
});
|
|
3780
|
+
|
|
3781
|
+
assert.equal(result.detached, true);
|
|
3782
|
+
assert.ok(Date.now() - startedAt < 1_000, "detach should release the foreground waiter promptly");
|
|
3783
|
+
for (let attempt = 0; attempt < 300 && !recoveredResult; attempt++) await new Promise((resolve) => setTimeout(resolve, 20));
|
|
3784
|
+
assert.ok(recoveredResult, "configured timeout should terminate and recover the detached child");
|
|
3785
|
+
assert.equal(recoveredResult.timedOut, true);
|
|
3786
|
+
assert.equal(recoveredResult.error, "Subagent timed out after 150ms.");
|
|
3787
|
+
assert.equal(recoveredResult.progress.status, "failed");
|
|
3788
|
+
assert.ok(Date.now() - startedAt < 5_000, "detached child should remain bounded by runtime enforcement");
|
|
3789
|
+
});
|
|
3790
|
+
|
|
3009
3791
|
for (const toolName of ["intercom", "contact_supervisor"]) {
|
|
3010
3792
|
it(`detaches cleanly on ${toolName} handoff without aborting the child process`, async () => {
|
|
3011
3793
|
const eventBus = createEventBus();
|
|
@@ -3052,6 +3834,106 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
3052
3834
|
});
|
|
3053
3835
|
}
|
|
3054
3836
|
|
|
3837
|
+
it("reports intercom detach race losses and repeated requests as not accepted", async () => {
|
|
3838
|
+
const abortBus = createEventBus();
|
|
3839
|
+
const abortResponses: boolean[] = [];
|
|
3840
|
+
abortBus.on(INTERCOM_DETACH_RESPONSE_EVENT, (payload) => abortResponses.push((payload as { accepted: boolean }).accepted));
|
|
3841
|
+
mockPi.onCall({ steps: [{ jsonl: [events.toolStart("contact_supervisor", { reason: "need_decision", message: "Need decision" })] }, { delay: 10_000 }] });
|
|
3842
|
+
const origin = new AbortController();
|
|
3843
|
+
let requested = false;
|
|
3844
|
+
const abortedResult = await runSync(tempDir, makeAgentConfigs(["echo"]), "echo", "Task", {
|
|
3845
|
+
runId: "intercom-abort-race-loss",
|
|
3846
|
+
allowIntercomDetach: true,
|
|
3847
|
+
intercomEvents: abortBus,
|
|
3848
|
+
signal: origin.signal,
|
|
3849
|
+
onUpdate: (update) => {
|
|
3850
|
+
if (requested || !update.details?.progress?.some((item) => item.currentTool === "contact_supervisor")) return;
|
|
3851
|
+
requested = true;
|
|
3852
|
+
origin.abort();
|
|
3853
|
+
abortBus.emit(INTERCOM_DETACH_REQUEST_EVENT, { requestId: "abort-race" });
|
|
3854
|
+
},
|
|
3855
|
+
});
|
|
3856
|
+
assert.equal(abortedResult.detached, undefined);
|
|
3857
|
+
assert.deepEqual(abortResponses, [false]);
|
|
3858
|
+
|
|
3859
|
+
const repeatedBus = createEventBus();
|
|
3860
|
+
const repeatedResponses: boolean[] = [];
|
|
3861
|
+
repeatedBus.on(INTERCOM_DETACH_RESPONSE_EVENT, (payload) => repeatedResponses.push((payload as { accepted: boolean }).accepted));
|
|
3862
|
+
mockPi.onCall({ steps: [{ jsonl: [events.toolStart("contact_supervisor", { reason: "need_decision", message: "Need decision" })] }, { delay: 50, jsonl: [events.assistantMessage("done")] }] });
|
|
3863
|
+
let repeated = false;
|
|
3864
|
+
const repeatedReceipt = await runSync(tempDir, makeAgentConfigs(["echo"]), "echo", "Task", {
|
|
3865
|
+
runId: "intercom-repeated-detach",
|
|
3866
|
+
allowIntercomDetach: true,
|
|
3867
|
+
intercomEvents: repeatedBus,
|
|
3868
|
+
onUpdate: (update) => {
|
|
3869
|
+
if (repeated || !update.details?.progress?.some((item) => item.currentTool === "contact_supervisor")) return;
|
|
3870
|
+
repeated = true;
|
|
3871
|
+
repeatedBus.emit(INTERCOM_DETACH_REQUEST_EVENT, { requestId: "first" });
|
|
3872
|
+
repeatedBus.emit(INTERCOM_DETACH_REQUEST_EVENT, { requestId: "second" });
|
|
3873
|
+
},
|
|
3874
|
+
});
|
|
3875
|
+
assert.equal(repeatedReceipt.detached, true);
|
|
3876
|
+
assert.deepEqual(repeatedResponses, [true, false]);
|
|
3877
|
+
});
|
|
3878
|
+
|
|
3879
|
+
it("does not launch retries or fallbacks after intercom detach and keeps timeout enforcement", async () => {
|
|
3880
|
+
const fallbackBus = createEventBus();
|
|
3881
|
+
mockPi.onCall({
|
|
3882
|
+
steps: [{ jsonl: [events.toolStart("contact_supervisor", { reason: "need_decision", message: "Need decision" })] }],
|
|
3883
|
+
stderr: "rate limit exceeded",
|
|
3884
|
+
exitCode: 1,
|
|
3885
|
+
});
|
|
3886
|
+
mockPi.onCall({ output: "must not launch" });
|
|
3887
|
+
let resolveFallbackTerminal!: (result: RunSyncResult) => void;
|
|
3888
|
+
const fallbackTerminal = new Promise<RunSyncResult>((resolve) => { resolveFallbackTerminal = resolve; });
|
|
3889
|
+
let fallbackRequested = false;
|
|
3890
|
+
const receipt = await runSync(tempDir, [makeAgent("echo", { model: "openai/gpt-5-mini", fallbackModels: ["anthropic/claude-sonnet-4"] })], "echo", "Task", {
|
|
3891
|
+
runId: "intercom-no-fallback",
|
|
3892
|
+
acceptance: false,
|
|
3893
|
+
allowIntercomDetach: true,
|
|
3894
|
+
intercomEvents: fallbackBus,
|
|
3895
|
+
onUpdate: (update) => {
|
|
3896
|
+
if (fallbackRequested || !update.details?.progress?.some((item) => item.currentTool === "contact_supervisor")) return;
|
|
3897
|
+
fallbackRequested = true;
|
|
3898
|
+
fallbackBus.emit(INTERCOM_DETACH_REQUEST_EVENT, { requestId: "no-fallback" });
|
|
3899
|
+
},
|
|
3900
|
+
onDetachedExit: (result) => { resolveFallbackTerminal(result as RunSyncResult); },
|
|
3901
|
+
});
|
|
3902
|
+
assert.equal(receipt.detached, true);
|
|
3903
|
+
const fallbackResult = await fallbackTerminal;
|
|
3904
|
+
assert.equal(mockPi.callCount(), 1);
|
|
3905
|
+
assert.equal(fallbackResult.exitCode, 1);
|
|
3906
|
+
|
|
3907
|
+
const timeoutBus = createEventBus();
|
|
3908
|
+
mockPi.reset();
|
|
3909
|
+
mockPi.onCall({ delay: 10_000 });
|
|
3910
|
+
let resolveTimeoutTerminal!: (result: RunSyncResult) => void;
|
|
3911
|
+
const timeoutTerminal = new Promise<RunSyncResult>((resolve) => { resolveTimeoutTerminal = resolve; });
|
|
3912
|
+
let timeoutRequested = false;
|
|
3913
|
+
const timeoutReceipt = await runSync(tempDir, makeAgentConfigs(["slow"]), "slow", "Task", {
|
|
3914
|
+
runId: "intercom-timeout-enforced",
|
|
3915
|
+
acceptance: false,
|
|
3916
|
+
timeoutMs: 125,
|
|
3917
|
+
allowIntercomDetach: true,
|
|
3918
|
+
intercomEvents: timeoutBus,
|
|
3919
|
+
onDetachReady: () => {
|
|
3920
|
+
if (timeoutRequested) return;
|
|
3921
|
+
timeoutRequested = true;
|
|
3922
|
+
timeoutBus.emit(INTERCOM_DETACH_REQUEST_EVENT, {
|
|
3923
|
+
requestId: "timeout",
|
|
3924
|
+
runId: "intercom-timeout-enforced",
|
|
3925
|
+
agent: "slow",
|
|
3926
|
+
childIndex: 0,
|
|
3927
|
+
});
|
|
3928
|
+
},
|
|
3929
|
+
onDetachedExit: (result) => { resolveTimeoutTerminal(result as RunSyncResult); },
|
|
3930
|
+
});
|
|
3931
|
+
assert.equal(timeoutReceipt.detached, true);
|
|
3932
|
+
const timeoutResult = await timeoutTerminal;
|
|
3933
|
+
assert.equal(timeoutResult.timedOut, true);
|
|
3934
|
+
assert.equal(timeoutResult.exitCode, 1);
|
|
3935
|
+
});
|
|
3936
|
+
|
|
3055
3937
|
it("enforces the stdout protocol limit after foreground detachment", async () => {
|
|
3056
3938
|
const eventBus = createEventBus();
|
|
3057
3939
|
mockPi.onCall({ steps: [
|