@selesai/code 0.12.0 → 0.13.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 +17 -0
- package/dist/core/agent-session.d.ts +1 -1
- package/dist/core/agent-session.js +1 -1
- package/dist/core/sdk.d.ts +2 -2
- package/dist/core/sdk.js +1 -1
- package/dist/core/system-prompt.d.ts +1 -1
- package/dist/core/system-prompt.js +1 -1
- package/dist/core/usage-totals.d.ts +2 -0
- package/dist/core/usage-totals.js +10 -0
- package/dist/extensions/ascii.txt +9 -0
- package/dist/extensions/package.json +1 -2
- package/dist/extensions/pi-hermes-memory/CHANGELOG.md +371 -0
- package/dist/extensions/pi-hermes-memory/LICENSE +21 -0
- package/dist/extensions/pi-hermes-memory/README.md +643 -0
- package/dist/extensions/pi-hermes-memory/docs/0.1/TASKS.md +197 -0
- package/dist/extensions/pi-hermes-memory/docs/0.2/PLAN.md +290 -0
- package/dist/extensions/pi-hermes-memory/docs/0.2/TASKS.md +134 -0
- package/dist/extensions/pi-hermes-memory/docs/0.2/TEST-PLAN.md +216 -0
- package/dist/extensions/pi-hermes-memory/docs/0.3/PLAN.md +330 -0
- package/dist/extensions/pi-hermes-memory/docs/0.3/TASKS.md +125 -0
- package/dist/extensions/pi-hermes-memory/docs/0.4/PLAN.md +160 -0
- package/dist/extensions/pi-hermes-memory/docs/0.4/TASKS.md +113 -0
- package/dist/extensions/pi-hermes-memory/docs/0.6/CHANGELOG.md +57 -0
- package/dist/extensions/pi-hermes-memory/docs/0.6/PLAN.md +202 -0
- package/dist/extensions/pi-hermes-memory/docs/0.6/TASKS.md +144 -0
- package/dist/extensions/pi-hermes-memory/docs/0.7/PLAN.md +349 -0
- package/dist/extensions/pi-hermes-memory/docs/0.7/TASKS.md +110 -0
- package/dist/extensions/pi-hermes-memory/docs/PUBLISHING.md +149 -0
- package/dist/extensions/pi-hermes-memory/docs/ROADMAP.md +468 -0
- package/dist/extensions/pi-hermes-memory/docs/images/memory-architecture.svg +1 -0
- package/dist/extensions/pi-hermes-memory/docs/images/pi-logo.svg +22 -0
- package/dist/extensions/pi-hermes-memory/docs/images/pi_memory.png +0 -0
- package/dist/extensions/pi-hermes-memory/docs/images/pi_memory.svg +19 -0
- package/dist/extensions/pi-hermes-memory/docs/images/security-flow.svg +1 -0
- package/dist/extensions/pi-hermes-memory/docs/images/session-lifecycle.svg +1 -0
- package/dist/extensions/pi-hermes-memory/docs/images/source-architecture.svg +1 -0
- package/dist/extensions/pi-hermes-memory/docs/mermaid/memory-architecture.mmd +49 -0
- package/dist/extensions/pi-hermes-memory/docs/mermaid/security-flow.mmd +40 -0
- package/dist/extensions/pi-hermes-memory/docs/mermaid/session-lifecycle.mmd +56 -0
- package/dist/extensions/pi-hermes-memory/docs/mermaid/source-architecture.mmd +54 -0
- package/dist/extensions/pi-hermes-memory/package-lock.json +3799 -0
- package/dist/extensions/pi-hermes-memory/package.json +66 -0
- package/dist/extensions/pi-hermes-memory/scripts/check-min-sdk.mjs +99 -0
- package/dist/extensions/pi-hermes-memory/scripts/ensure-dev.mjs +57 -0
- package/dist/extensions/pi-hermes-memory/src/auto-consolidation-warning.ts +6 -0
- package/dist/extensions/pi-hermes-memory/src/config.ts +178 -0
- package/dist/extensions/pi-hermes-memory/src/constants.ts +424 -0
- package/dist/extensions/pi-hermes-memory/src/extension-root-migration.ts +639 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/auto-consolidate.ts +401 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/background-review.ts +297 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/child-process-watchdog.mjs +90 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/correction-detector.ts +300 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/index-sessions.ts +86 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/insights.ts +78 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/interview.ts +37 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/learn-memory.ts +186 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/message-parts.ts +27 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/pi-child-process.ts +485 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/preview-context.ts +105 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/review-memory-ops.ts +506 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/session-backfill.ts +145 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/session-flush.ts +151 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/session-live-index.ts +94 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/skills-command.ts +1334 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/standing-pin.ts +111 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/switch-project.ts +75 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/sync-markdown-memories.ts +266 -0
- package/dist/extensions/pi-hermes-memory/src/index.ts +378 -0
- package/dist/extensions/pi-hermes-memory/src/lifecycle-timing.ts +54 -0
- package/dist/extensions/pi-hermes-memory/src/paths.ts +61 -0
- package/dist/extensions/pi-hermes-memory/src/project-context.ts +13 -0
- package/dist/extensions/pi-hermes-memory/src/project-memory-migration.ts +93 -0
- package/dist/extensions/pi-hermes-memory/src/project.ts +157 -0
- package/dist/extensions/pi-hermes-memory/src/prompt-context.ts +55 -0
- package/dist/extensions/pi-hermes-memory/src/store/atomic-lock-coordinator.ts +375 -0
- package/dist/extensions/pi-hermes-memory/src/store/canonical-storage-path.ts +54 -0
- package/dist/extensions/pi-hermes-memory/src/store/content-scanner.ts +101 -0
- package/dist/extensions/pi-hermes-memory/src/store/db.ts +1180 -0
- package/dist/extensions/pi-hermes-memory/src/store/fts-query.ts +101 -0
- package/dist/extensions/pi-hermes-memory/src/store/markdown-mutation-lock.ts +38 -0
- package/dist/extensions/pi-hermes-memory/src/store/memory-lookup.ts +24 -0
- package/dist/extensions/pi-hermes-memory/src/store/memory-store.ts +1191 -0
- package/dist/extensions/pi-hermes-memory/src/store/recovery-maintenance.ts +66 -0
- package/dist/extensions/pi-hermes-memory/src/store/schema.ts +118 -0
- package/dist/extensions/pi-hermes-memory/src/store/session-anchor-search.ts +472 -0
- package/dist/extensions/pi-hermes-memory/src/store/session-indexer.ts +511 -0
- package/dist/extensions/pi-hermes-memory/src/store/session-parser.ts +213 -0
- package/dist/extensions/pi-hermes-memory/src/store/session-search.ts +234 -0
- package/dist/extensions/pi-hermes-memory/src/store/skill-store.ts +950 -0
- package/dist/extensions/pi-hermes-memory/src/store/skill-utils.ts +133 -0
- package/dist/extensions/pi-hermes-memory/src/store/sqlite-memory-store.ts +1002 -0
- package/dist/extensions/pi-hermes-memory/src/store/sqlite-native.ts +241 -0
- package/dist/extensions/pi-hermes-memory/src/store/standing-instructions.ts +247 -0
- package/dist/extensions/pi-hermes-memory/src/tools/memory-search-tool.ts +95 -0
- package/dist/extensions/pi-hermes-memory/src/tools/memory-tool.ts +469 -0
- package/dist/extensions/pi-hermes-memory/src/tools/session-search-tool.ts +246 -0
- package/dist/extensions/pi-hermes-memory/src/tools/shared-output-view.ts +170 -0
- package/dist/extensions/pi-hermes-memory/src/tools/skill-tool.ts +333 -0
- package/dist/extensions/pi-hermes-memory/src/tools/tool-result-views.ts +105 -0
- package/dist/extensions/pi-hermes-memory/src/types.ts +214 -0
- package/dist/extensions/pi-hermes-memory/tests/config.test.ts +434 -0
- package/dist/extensions/pi-hermes-memory/tests/extension-root-migration.test.ts +368 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/auto-consolidate.test.ts +984 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/background-review.test.ts +950 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/correction-detector.test.ts +671 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/insights.test.ts +157 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/interview.test.ts +127 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/pi-child-process.test.ts +905 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/preview-context.test.ts +152 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/prompt-context.test.ts +166 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/resources-discover.test.ts +71 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/review-memory-ops.test.ts +614 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/session-backfill.test.ts +192 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/session-flush.test.ts +677 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/session-live-index.test.ts +225 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/skills-command.test.ts +827 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/standing-pin.test.ts +134 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/sync-markdown-memories.test.ts +491 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/system-prompt.test.ts +199 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/thinking-max.test.ts +64 -0
- package/dist/extensions/pi-hermes-memory/tests/index.test.ts +30 -0
- package/dist/extensions/pi-hermes-memory/tests/integration/flow.test.ts +126 -0
- package/dist/extensions/pi-hermes-memory/tests/lifecycle-timing.test.ts +76 -0
- package/dist/extensions/pi-hermes-memory/tests/paths.test.ts +23 -0
- package/dist/extensions/pi-hermes-memory/tests/project-memory-migration.test.ts +80 -0
- package/dist/extensions/pi-hermes-memory/tests/project-rebinding.test.ts +115 -0
- package/dist/extensions/pi-hermes-memory/tests/project.test.ts +120 -0
- package/dist/extensions/pi-hermes-memory/tests/run-all-timeout.test.ts +107 -0
- package/dist/extensions/pi-hermes-memory/tests/run-all.sh +45 -0
- package/dist/extensions/pi-hermes-memory/tests/store/atomic-lock-coordinator.test.ts +381 -0
- package/dist/extensions/pi-hermes-memory/tests/store/content-scanner.test.ts +388 -0
- package/dist/extensions/pi-hermes-memory/tests/store/db.test.ts +1046 -0
- package/dist/extensions/pi-hermes-memory/tests/store/markdown-mutation-lock.test.ts +34 -0
- package/dist/extensions/pi-hermes-memory/tests/store/memory-store.test.ts +2150 -0
- package/dist/extensions/pi-hermes-memory/tests/store/recovery-maintenance.test.ts +170 -0
- package/dist/extensions/pi-hermes-memory/tests/store/session-anchor-search.test.ts +210 -0
- package/dist/extensions/pi-hermes-memory/tests/store/session-indexer.test.ts +643 -0
- package/dist/extensions/pi-hermes-memory/tests/store/session-parser.test.ts +282 -0
- package/dist/extensions/pi-hermes-memory/tests/store/session-search.test.ts +287 -0
- package/dist/extensions/pi-hermes-memory/tests/store/skill-store.test.ts +678 -0
- package/dist/extensions/pi-hermes-memory/tests/store/skill-utils.test.ts +21 -0
- package/dist/extensions/pi-hermes-memory/tests/store/sqlite-lazy-load.test.ts +78 -0
- package/dist/extensions/pi-hermes-memory/tests/store/sqlite-memory-store.test.ts +543 -0
- package/dist/extensions/pi-hermes-memory/tests/store/sqlite-native.test.ts +164 -0
- package/dist/extensions/pi-hermes-memory/tests/store/standing-instructions.test.ts +171 -0
- package/dist/extensions/pi-hermes-memory/tests/tools/memory-search-tool.test.ts +84 -0
- package/dist/extensions/pi-hermes-memory/tests/tools/memory-tool.test.ts +709 -0
- package/dist/extensions/pi-hermes-memory/tests/tools/session-search-tool.test.ts +345 -0
- package/dist/extensions/pi-hermes-memory/tests/tools/shared-output-view.test.ts +362 -0
- package/dist/extensions/pi-hermes-memory/tests/tools/skill-tool.test.ts +478 -0
- package/dist/extensions/pi-hermes-memory/tests/tools/tool-result-renderer-wiring.test.ts +40 -0
- package/dist/extensions/pi-hermes-memory/tsconfig.json +19 -0
- package/dist/extensions/pi-subagents/.oxlintrc.json +43 -0
- package/dist/extensions/pi-subagents/AGENTS.md +5 -0
- package/dist/extensions/pi-subagents/CHANGELOG.md +157 -0
- package/dist/extensions/pi-subagents/README.md +12 -6
- package/dist/extensions/pi-subagents/VISION.md +81 -0
- package/dist/extensions/pi-subagents/agents/claude-code-writer.md +15 -0
- package/dist/extensions/pi-subagents/agents/claude-code.md +15 -0
- package/dist/extensions/pi-subagents/agents/codex-exec-writer.md +15 -0
- package/dist/extensions/pi-subagents/agents/codex-exec.md +15 -0
- package/dist/extensions/pi-subagents/agents/cursor-agent-writer.md +14 -0
- package/dist/extensions/pi-subagents/agents/cursor-agent.md +14 -0
- package/dist/extensions/pi-subagents/agents/reviewer.md +1 -3
- package/dist/extensions/pi-subagents/docs/agents.md +488 -0
- package/dist/extensions/pi-subagents/docs/configuration.md +46 -9
- package/dist/extensions/pi-subagents/docs/extension-api.md +42 -3
- package/dist/extensions/pi-subagents/docs/models.md +27 -4
- package/dist/extensions/pi-subagents/docs/observability.md +10 -9
- package/dist/extensions/pi-subagents/docs/tool-reference.md +102 -6
- package/dist/extensions/pi-subagents/docs/workflows.md +135 -0
- package/dist/extensions/pi-subagents/package-lock.json +389 -2
- package/dist/extensions/pi-subagents/package.json +3 -1
- package/dist/extensions/pi-subagents/prompts/review-loop.md +2 -2
- package/dist/extensions/pi-subagents/skills/council-mode/SKILL.md +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +3 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +7 -3
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +26 -5
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +41 -22
- package/dist/extensions/pi-subagents/src/agents/agent-refinements.ts +4 -4
- package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +9 -2
- package/dist/extensions/pi-subagents/src/agents/agents.ts +322 -75
- package/dist/extensions/pi-subagents/src/agents/builtin-names.ts +6 -0
- package/dist/extensions/pi-subagents/src/agents/runtime-agent-events.ts +70 -0
- package/dist/extensions/pi-subagents/src/agents/runtime-agent-registry.ts +27 -20
- package/dist/extensions/pi-subagents/src/api/agents.ts +10 -5
- package/dist/extensions/pi-subagents/src/api/background-work.ts +5 -1
- package/dist/extensions/pi-subagents/src/api/delegation.ts +0 -7
- package/dist/extensions/pi-subagents/src/api/preflight.ts +34 -12
- package/dist/extensions/pi-subagents/src/extension/config.ts +70 -0
- package/dist/extensions/pi-subagents/src/extension/doctor.ts +3 -3
- package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +5 -3
- package/dist/extensions/pi-subagents/src/extension/index.ts +83 -30
- package/dist/extensions/pi-subagents/src/extension/public-execution.ts +44 -15
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +55 -19
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +41 -15
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +28 -31
- package/dist/extensions/pi-subagents/src/inspectors/herdr/actions.ts +2 -1
- package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +2 -10
- package/dist/extensions/pi-subagents/src/inspectors/herdr/session-roots-codec.ts +42 -0
- package/dist/extensions/pi-subagents/src/integrations/herdr-status.ts +51 -3
- package/dist/extensions/pi-subagents/src/intercom/result-intercom.ts +2 -0
- package/dist/extensions/pi-subagents/src/profiles/profiles.ts +5 -6
- package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +77 -10
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +176 -53
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +19 -12
- package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +29 -27
- package/dist/extensions/pi-subagents/src/runs/background/async-retention.ts +20 -3
- package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +23 -261
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +73 -7
- package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +8 -3
- package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +74 -8
- package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +37 -21
- package/dist/extensions/pi-subagents/src/runs/background/inspect-rpc.ts +8 -8
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +184 -9
- package/dist/extensions/pi-subagents/src/runs/background/result-delivery-ownership.ts +45 -0
- package/dist/extensions/pi-subagents/src/runs/background/result-files.ts +28 -14
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +38 -15
- package/dist/extensions/pi-subagents/src/runs/background/resume-guidance.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +50 -13
- package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +93 -8
- package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +10 -4
- package/dist/extensions/pi-subagents/src/runs/background/steering.ts +4 -14
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +549 -377
- package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +58 -10
- package/dist/extensions/pi-subagents/src/runs/background/terminal-run-index.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +26 -1
- package/dist/extensions/pi-subagents/src/runs/background/wait-config.ts +23 -9
- package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +9 -2
- package/dist/extensions/pi-subagents/src/runs/foreground/async-steering-action.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +204 -129
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +9 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +23 -1
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +773 -311
- package/dist/extensions/pi-subagents/src/runs/foreground/workflow-detach-reconcile.ts +140 -115
- package/dist/extensions/pi-subagents/src/runs/shared/abort-recovery.ts +119 -0
- package/dist/extensions/pi-subagents/src/runs/shared/async-status-projection.ts +463 -0
- package/dist/extensions/pi-subagents/src/runs/shared/child-identity.ts +19 -4
- package/dist/extensions/pi-subagents/src/runs/shared/child-launch-plan.ts +151 -0
- package/dist/extensions/pi-subagents/src/runs/shared/child-protocol.ts +21 -7
- package/dist/extensions/pi-subagents/src/runs/shared/claude-code-adapter.ts +129 -0
- package/dist/extensions/pi-subagents/src/runs/shared/codex-exec-adapter.ts +129 -0
- package/dist/extensions/pi-subagents/src/runs/shared/completion-evidence.ts +89 -0
- package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +5 -4
- package/dist/extensions/pi-subagents/src/runs/shared/cursor-agent-adapter.ts +114 -0
- package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +3 -3
- package/dist/extensions/pi-subagents/src/runs/shared/external-cli-contract.ts +167 -0
- package/dist/extensions/pi-subagents/src/runs/shared/external-cli-preflight.ts +122 -0
- package/dist/extensions/pi-subagents/src/runs/shared/external-cli-runner.ts +348 -55
- package/dist/extensions/pi-subagents/src/runs/shared/fast-mode-extension.ts +5 -5
- package/dist/extensions/pi-subagents/src/runs/shared/host-step-status.ts +230 -0
- package/dist/extensions/pi-subagents/src/runs/shared/lane-metadata.ts +105 -0
- package/dist/extensions/pi-subagents/src/runs/shared/launch-cwd.ts +16 -0
- package/dist/extensions/pi-subagents/src/runs/shared/long-running-guard.ts +2 -1
- package/dist/extensions/pi-subagents/src/runs/shared/mcp-config-sources.ts +422 -0
- package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +221 -158
- package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-grant.ts +197 -0
- package/dist/extensions/pi-subagents/src/runs/shared/model-exclusions.ts +69 -7
- package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +47 -6
- package/dist/extensions/pi-subagents/src/runs/shared/mutation-evidence.ts +7 -2
- package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +9 -3
- package/dist/extensions/pi-subagents/src/runs/shared/nested-render.ts +9 -5
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +419 -7
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +15 -1
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +81 -8
- package/dist/extensions/pi-subagents/src/runs/shared/process-signal.ts +13 -0
- package/dist/extensions/pi-subagents/src/runs/shared/run-history.ts +21 -1
- package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +44 -4
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +98 -13
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/shared/worktree-cleanup-plan.ts +847 -0
- package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +18 -0
- package/dist/extensions/pi-subagents/src/shared/child-session-name.ts +46 -0
- package/dist/extensions/pi-subagents/src/shared/extension-context.ts +24 -0
- package/dist/extensions/pi-subagents/src/shared/fork-context.ts +21 -0
- package/dist/extensions/pi-subagents/src/shared/formatters.ts +18 -3
- package/dist/extensions/pi-subagents/src/shared/launch-contract.ts +5 -1
- package/dist/extensions/pi-subagents/src/shared/pruned-fork.ts +450 -0
- package/dist/extensions/pi-subagents/src/shared/session-file-trust.ts +19 -0
- package/dist/extensions/pi-subagents/src/shared/session-tokens.ts +14 -3
- package/dist/extensions/pi-subagents/src/shared/settings.ts +19 -105
- package/dist/extensions/pi-subagents/src/shared/shortcuts.ts +17 -0
- package/dist/extensions/pi-subagents/src/shared/types.ts +359 -40
- package/dist/extensions/pi-subagents/src/shared/utils.ts +41 -84
- package/dist/extensions/pi-subagents/src/shared/workflow-child-permit.ts +116 -0
- package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +1 -9
- package/dist/extensions/pi-subagents/src/slash/delegation-request.ts +0 -4
- package/dist/extensions/pi-subagents/src/slash/slash-bridge.ts +1 -2
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +378 -88
- package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +22 -11
- package/dist/extensions/pi-subagents/src/slash/subagents-admin.ts +3 -0
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +147 -79
- package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +11 -5
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +39 -18
- package/dist/extensions/pi-subagents/src/tui/render.ts +369 -46
- package/dist/extensions/pi-subagents/src/watchdog/turn-delta.ts +1 -1
- package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +6 -3
- package/dist/extensions/pi-subagents/src/workflows/host-command.ts +230 -0
- package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +740 -58
- package/dist/extensions/pi-subagents/src/workflows/workflow-child-summary.ts +121 -0
- package/dist/extensions/pi-subagents/src/workflows/workflow-preflight.ts +270 -0
- package/dist/extensions/pi-subagents/src/workflows/workflow-receipt.ts +195 -6
- package/dist/extensions/pi-subagents/src/workflows/workflow-settlement.ts +246 -0
- package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +1114 -260
- package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +34 -0
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +150 -3
- package/dist/extensions/pi-subagents/test/integration/claude-code-smoke.test.ts +52 -0
- package/dist/extensions/pi-subagents/test/integration/claude-code-writer-smoke.test.ts +55 -0
- package/dist/extensions/pi-subagents/test/integration/codex-exec-smoke.test.ts +53 -0
- package/dist/extensions/pi-subagents/test/integration/codex-exec-writer-smoke.test.ts +57 -0
- package/dist/extensions/pi-subagents/test/integration/cursor-agent-smoke.test.ts +59 -0
- package/dist/extensions/pi-subagents/test/integration/cursor-agent-writer-smoke.test.ts +62 -0
- package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +57 -1
- package/dist/extensions/pi-subagents/test/integration/external-cli-runner.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +20 -0
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +4 -3
- package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +54 -4
- package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +482 -4
- package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +157 -9
- package/dist/extensions/pi-subagents/test/integration/session-tokens.test.ts +5 -5
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1508 -291
- package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +423 -1
- package/dist/extensions/pi-subagents/test/integration/template-resolution.test.ts +8 -0
- package/dist/extensions/pi-subagents/test/support/cursor-smoke-inputs.ts +51 -0
- package/dist/extensions/pi-subagents/test/support/helpers.ts +2 -0
- package/dist/extensions/pi-subagents/test/support/mock-pi.ts +5 -5
- package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +6 -6
- package/dist/extensions/pi-subagents/test/unit/abort-recovery.test.ts +152 -0
- package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +48 -0
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +184 -6
- package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +66 -6
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +77 -0
- package/dist/extensions/pi-subagents/test/unit/agent-refinements.test.ts +18 -0
- package/dist/extensions/pi-subagents/test/unit/anti-slop-oxlint.test.ts +86 -0
- package/dist/extensions/pi-subagents/test/unit/artifacts.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +101 -1
- package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +121 -0
- package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +28 -0
- package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +14 -7
- package/dist/extensions/pi-subagents/test/unit/async-retention.test.ts +53 -0
- package/dist/extensions/pi-subagents/test/unit/async-status-projection.test.ts +190 -0
- package/dist/extensions/pi-subagents/test/unit/async-status-snapshot.test.ts +42 -0
- package/dist/extensions/pi-subagents/test/unit/auto-drain.test.ts +8 -4
- package/dist/extensions/pi-subagents/test/unit/background-work.test.ts +23 -0
- package/dist/extensions/pi-subagents/test/unit/chain-root-attachment.test.ts +78 -1
- package/dist/extensions/pi-subagents/test/unit/child-launch-plan.test.ts +94 -0
- package/dist/extensions/pi-subagents/test/unit/child-protocol.test.ts +21 -0
- package/dist/extensions/pi-subagents/test/unit/child-session-name.test.ts +50 -0
- package/dist/extensions/pi-subagents/test/unit/claude-code-adapter.test.ts +245 -0
- package/dist/extensions/pi-subagents/test/unit/codex-exec-adapter.test.ts +192 -0
- package/dist/extensions/pi-subagents/test/unit/compaction-resume.test.ts +27 -0
- package/dist/extensions/pi-subagents/test/unit/completion-evidence.test.ts +150 -0
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +21 -0
- package/dist/extensions/pi-subagents/test/unit/cursor-agent-adapter.test.ts +259 -0
- package/dist/extensions/pi-subagents/test/unit/cursor-smoke-inputs.test.ts +77 -0
- package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +1 -4
- package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/extension-context.test.ts +27 -0
- package/dist/extensions/pi-subagents/test/unit/external-cli-runner.test.ts +223 -0
- package/dist/extensions/pi-subagents/test/unit/fast-mode-extension.test.ts +27 -0
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +208 -2
- package/dist/extensions/pi-subagents/test/unit/fleet-transcript.test.ts +29 -1
- package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +110 -1
- package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +10 -0
- package/dist/extensions/pi-subagents/test/unit/formatters.test.ts +14 -0
- package/dist/extensions/pi-subagents/test/unit/herdr-inspector.test.ts +18 -9
- package/dist/extensions/pi-subagents/test/unit/herdr-session-roots-codec.test.ts +63 -0
- package/dist/extensions/pi-subagents/test/unit/herdr-shell-command.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/herdr-status-bridge.test.ts +132 -0
- package/dist/extensions/pi-subagents/test/unit/host-command.test.ts +156 -0
- package/dist/extensions/pi-subagents/test/unit/host-step-status.test.ts +98 -0
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +178 -26
- package/dist/extensions/pi-subagents/test/unit/index-segment.test.ts +2 -1
- package/dist/extensions/pi-subagents/test/unit/inspect-rpc.test.ts +21 -0
- package/dist/extensions/pi-subagents/test/unit/mcp-direct-tool-grant.test.ts +138 -0
- package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/model-exclusions.test.ts +91 -0
- package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +72 -7
- package/dist/extensions/pi-subagents/test/unit/mutation-evidence.test.ts +21 -0
- package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +20 -2
- package/dist/extensions/pi-subagents/test/unit/notify.test.ts +129 -1
- package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +4 -0
- package/dist/extensions/pi-subagents/test/unit/parallel-handoff.test.ts +358 -0
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +574 -21
- package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +109 -1
- package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +71 -1
- package/dist/extensions/pi-subagents/test/unit/profiles.test.ts +7 -7
- package/dist/extensions/pi-subagents/test/unit/pruned-fork.test.ts +214 -0
- package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +29 -8
- package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +53 -0
- package/dist/extensions/pi-subagents/test/unit/result-delivery-ownership.test.ts +34 -0
- package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +101 -3
- package/dist/extensions/pi-subagents/test/unit/retained-children.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +10 -8
- package/dist/extensions/pi-subagents/test/unit/run-fanout-budget.test.ts +71 -1
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +41 -7
- package/dist/extensions/pi-subagents/test/unit/runtime-agent-registration.test.ts +136 -2
- package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +137 -0
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +33 -12
- package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +504 -1
- package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +8 -0
- package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +12 -1
- package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +3 -5
- package/dist/extensions/pi-subagents/test/unit/steering.test.ts +5 -6
- package/dist/extensions/pi-subagents/test/unit/subagent-action-recovery.test.ts +0 -8
- package/dist/extensions/pi-subagents/test/unit/subagent-guide.test.ts +5 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +157 -2
- package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +63 -4
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +27 -23
- package/dist/extensions/pi-subagents/test/unit/wait-completions.test.ts +44 -0
- package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +7 -5
- package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +106 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-detach-reconcile.test.ts +484 -9
- package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +38 -4
- package/dist/extensions/pi-subagents/test/unit/workflow-preflight.test.ts +155 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-receipt.test.ts +318 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-resume-hint.test.ts +365 -0
- package/dist/extensions/pi-subagents/test/unit/worktree-cleanup-plan.test.ts +349 -0
- package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +21 -0
- package/dist/extensions/pi-subagents/test/unit/writer-budget-guidance.test.ts +2 -7
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/effect/index.ts +13 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/effect/rules/no-service-constructor-imports.ts +52 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/index.ts +41 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-chained-type-assertions.ts +77 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-conditional-empty-object-spread.ts +49 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-known-value-widening.ts +247 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-module-mocking.ts +91 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-object-parameters.ts +126 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-reflect-apply.ts +28 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-reflect-get.ts +28 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-runtime-typeof.ts +67 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-shape-in-symbol-names.ts +39 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-unknown-parameters.ts +83 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-unknown-returns.ts +115 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-unknown-type-aliases.ts +72 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-unsafe-dictionary-type.ts +134 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-widen-then-assert.ts +366 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/require-safety-comment-for-type-assertion.ts +62 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/shared/dictionary-types.ts +502 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/shared/lexical-type-parameters.ts +60 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/shared/reflect-method.ts +35 -0
- package/dist/extensions/pi-tool-display/tests/capabilities.test.ts +1 -1
- package/dist/extensions/pi-tool-display/tests/settings-inspector.test.ts +2 -2
- package/dist/extensions/pi-tool-display/tests/tool-overrides-mcp.test.ts +1 -1
- package/dist/extensions/pi-zentui/extensions/zentui/format.ts +2 -1
- package/dist/extensions/pi-zentui/test/format.test.ts +15 -1
- package/dist/modes/interactive/components/footer.js +2 -2
- package/dist/modes/interactive/interactive-mode.js +11 -11
- package/dist/skills/workflow/SKILL.md +65 -50
- package/package.json +112 -110
- package/dist/extensions/enable-readonly-tools.test.ts +0 -63
- package/dist/extensions/enable-readonly-tools.ts +0 -24
- package/dist/extensions/model-prompt-injector/config.json +0 -16
- package/dist/extensions/model-prompt-injector/index.test.ts +0 -102
- package/dist/extensions/model-prompt-injector/index.ts +0 -148
- package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +0 -98
- package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +0 -250
|
@@ -80,7 +80,7 @@ interface AsyncResultPayload {
|
|
|
80
80
|
totalTokens?: { input: number; output: number; total: number };
|
|
81
81
|
totalCost?: { inputTokens: number; outputTokens: number; costUsd: number };
|
|
82
82
|
usageBudget?: UsageBudgetState;
|
|
83
|
-
results: Array<{ agent?: string; launchContractDigest?: string; launchResolvedExtensions?: LaunchResolvedExtensions; runtimeAcknowledgedExtensions?: RuntimeAcknowledgedExtensions; output?: string; outputState?: "present" | "absent" | "unknown"; success?: boolean; error?: string; protocolError?: { code?: string; stream?: string; limitBytes?: number; observedBytes?: number }; timedOut?: boolean; timeoutRecovery?: { changedFiles?: string[]; message?: string; warning?: string }; stopped?: boolean; turnBudget?: { maxTurns: number; graceTurns: number; outcome: string; turnCount: number; wrapUpRequestedAtTurn?: number; terminationDeferredAtTurn?: number; exceededAtTurn?: number }; turnBudgetExceeded?: boolean; wrapUpRequested?: boolean; model?: string; attemptedModels?: string[]; modelAttempts?: Array<{ success?: boolean; error?: string }>; totalCost?: { inputTokens: number; outputTokens: number; costUsd: number }; structuredOutput?: unknown; agentContract?: { version: 1 }; execution?: { status?: string; success?: boolean; exitCode?: number }; effects?: { fileMutation?: { status?: string; expected?: boolean; attempted?: boolean; message?: string } }; intercomTarget?: string; acceptance?: { status?: string; effectiveAcceptance?: { level?: string }; childReport?: unknown; runtimeChecks?: Array<{ id?: string; status?: string; message?: string }> }; artifactPaths?: { outputPath?: string; inputPath?: string; metadataPath?: string; transcriptPath?: string }; outputSaveError?: string; metadataSaveError?: string; capabilityCeiling?: { version?: number; allowedTools?: string[]; denyExtensions?: boolean; sources?: string[] }; capabilityAudit?: { effectiveTools?: string[]; removedTools?: string[]; extensionsDenied?: boolean } }>;
|
|
83
|
+
results: Array<{ agent?: string; sessionName?: string; launchContractDigest?: string; launchResolvedExtensions?: LaunchResolvedExtensions; runtimeAcknowledgedExtensions?: RuntimeAcknowledgedExtensions; output?: string; outputState?: "present" | "absent" | "unknown"; success?: boolean; error?: string; protocolError?: { code?: string; stream?: string; limitBytes?: number; observedBytes?: number }; timedOut?: boolean; timeoutRecovery?: { changedFiles?: string[]; message?: string; warning?: string }; stopped?: boolean; turnBudget?: { maxTurns: number; graceTurns: number; outcome: string; turnCount: number; wrapUpRequestedAtTurn?: number; terminationDeferredAtTurn?: number; exceededAtTurn?: number }; turnBudgetExceeded?: boolean; wrapUpRequested?: boolean; model?: string; attemptedModels?: string[]; modelAttempts?: Array<{ success?: boolean; error?: string }>; totalCost?: { inputTokens: number; outputTokens: number; costUsd: number }; usage?: { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number; turns: number }; structuredOutput?: unknown; agentContract?: { version: 1 }; execution?: { status?: string; success?: boolean; exitCode?: number }; effects?: { fileMutation?: { status?: string; expected?: boolean; attempted?: boolean; message?: string }; settlementDiagnostic?: { finalTextPresent?: boolean; mutation?: { expected?: boolean; attempted?: boolean; observed?: boolean }; requiredOutput?: { kind?: string; path?: string; missing?: boolean }; afterCompactionSettlement?: boolean } }; intercomTarget?: string; acceptance?: { status?: string; effectiveAcceptance?: { level?: string }; childReport?: unknown; runtimeChecks?: Array<{ id?: string; status?: string; message?: string }> }; artifactPaths?: { outputPath?: string; inputPath?: string; metadataPath?: string; transcriptPath?: string }; outputSaveError?: string; metadataSaveError?: string; capabilityCeiling?: { version?: number; allowedTools?: string[]; denyExtensions?: boolean; sources?: string[] }; capabilityAudit?: { effectiveTools?: string[]; removedTools?: string[]; extensionsDenied?: boolean } }>;
|
|
84
84
|
outputs?: Record<string, { text?: string; structured?: unknown }>;
|
|
85
85
|
workflowGraph?: { nodes?: Array<{ kind?: string; label?: string; phase?: string; status?: string; acceptanceStatus?: string; error?: string; outputName?: string; structured?: boolean; children?: Array<{ label?: string; outputName?: string; itemKey?: string; status?: string; acceptanceStatus?: string; error?: string }> }> };
|
|
86
86
|
parallelHandoff?: { version?: number; path?: string; groupCount?: number; childCount?: number; changedPatches?: number; cleanupState?: string };
|
|
@@ -116,6 +116,8 @@ interface AsyncStatusPayload {
|
|
|
116
116
|
capabilityCeiling?: { version?: number; allowedTools?: string[]; denyExtensions?: boolean; sources?: string[] };
|
|
117
117
|
capabilityAudit?: { effectiveTools?: string[]; removedTools?: string[]; extensionsDenied?: boolean };
|
|
118
118
|
steps?: Array<{
|
|
119
|
+
agent?: string;
|
|
120
|
+
sessionName?: string;
|
|
119
121
|
label?: string;
|
|
120
122
|
phase?: string;
|
|
121
123
|
outputName?: string;
|
|
@@ -137,8 +139,9 @@ interface AsyncStatusPayload {
|
|
|
137
139
|
launchResolvedExtensions?: LaunchResolvedExtensions;
|
|
138
140
|
runtimeAcknowledgedExtensions?: RuntimeAcknowledgedExtensions;
|
|
139
141
|
execution?: { status?: string; success?: boolean; exitCode?: number };
|
|
140
|
-
effects?: { fileMutation?: { status?: string; expected?: boolean; attempted?: boolean } };
|
|
142
|
+
effects?: { fileMutation?: { status?: string; expected?: boolean; attempted?: boolean }; settlementDiagnostic?: { finalTextPresent?: boolean; mutation?: { expected?: boolean; attempted?: boolean; observed?: boolean }; requiredOutput?: { kind?: string; path?: string; missing?: boolean }; afterCompactionSettlement?: boolean } };
|
|
141
143
|
acceptance?: { status?: string };
|
|
144
|
+
contextLimit?: number;
|
|
142
145
|
turnBudget?: { maxTurns: number; graceTurns: number; outcome: string; turnCount: number; wrapUpRequestedAtTurn?: number; terminationDeferredAtTurn?: number; exceededAtTurn?: number };
|
|
143
146
|
turnBudgetExceeded?: boolean;
|
|
144
147
|
wrapUpRequested?: boolean;
|
|
@@ -230,6 +233,10 @@ interface AsyncExecutionModule {
|
|
|
230
233
|
executeAsyncChain(id: string, params: Record<string, unknown>): AsyncExecutionResult;
|
|
231
234
|
}
|
|
232
235
|
|
|
236
|
+
interface AsyncStatusModule {
|
|
237
|
+
resolveTargetedAsyncRun(root: string, id: string, sessionId?: string): { kind: string };
|
|
238
|
+
}
|
|
239
|
+
|
|
233
240
|
interface UtilsModule {
|
|
234
241
|
readStatus(dir: string): { runId: string; state: string; mode: string } | null;
|
|
235
242
|
pruneStatusCacheForAsyncRoot(root: string, runIds: Iterable<string>): number;
|
|
@@ -248,6 +255,7 @@ interface ExecutorModule {
|
|
|
248
255
|
}
|
|
249
256
|
|
|
250
257
|
const asyncMod = await tryImport<AsyncExecutionModule>("./src/runs/background/async-execution.ts");
|
|
258
|
+
const asyncStatusMod = await tryImport<AsyncStatusModule>("./src/runs/background/async-status.ts");
|
|
251
259
|
const utils = await tryImport<UtilsModule>("./src/shared/utils.ts");
|
|
252
260
|
const typesMod = await tryImport<TypesModule>("./src/shared/types.ts");
|
|
253
261
|
const executorMod = await tryImport<ExecutorModule>("./src/runs/foreground/subagent-executor.ts");
|
|
@@ -256,6 +264,7 @@ const available = !!(asyncMod && utils && typesMod);
|
|
|
256
264
|
const isAsyncAvailable = asyncMod?.isAsyncAvailable;
|
|
257
265
|
const executeAsyncSingle = asyncMod?.executeAsyncSingle;
|
|
258
266
|
const executeAsyncChain = asyncMod?.executeAsyncChain;
|
|
267
|
+
const resolveTargetedAsyncRun = asyncStatusMod?.resolveTargetedAsyncRun;
|
|
259
268
|
const readStatus = utils?.readStatus;
|
|
260
269
|
const pruneStatusCacheForAsyncRoot = utils?.pruneStatusCacheForAsyncRoot;
|
|
261
270
|
const ASYNC_DIR = typesMod?.ASYNC_DIR;
|
|
@@ -358,24 +367,6 @@ async function waitForAsyncState(id: string, predicate: (status: AsyncStatusPayl
|
|
|
358
367
|
assert.fail(`Timed out waiting for async status: ${statusPath}`);
|
|
359
368
|
}
|
|
360
369
|
|
|
361
|
-
async function waitForDeferredTurnBudget(id: string, timeoutMs = 10_000): Promise<AsyncStatusPayload> {
|
|
362
|
-
const statusPath = path.join(ASYNC_DIR, id, "status.json");
|
|
363
|
-
const deadline = Date.now() + timeoutMs;
|
|
364
|
-
while (Date.now() <= deadline) {
|
|
365
|
-
if (fs.existsSync(statusPath)) {
|
|
366
|
-
const status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatusPayload;
|
|
367
|
-
if (status.state === "running"
|
|
368
|
-
&& status.turnBudget?.outcome === "termination-deferred"
|
|
369
|
-
&& status.steps?.[0]?.turnBudget?.outcome === "termination-deferred"
|
|
370
|
-
&& status.steps[0].currentTool === "bash") {
|
|
371
|
-
return status;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
375
|
-
}
|
|
376
|
-
assert.fail(`Timed out waiting for deferred turn-budget status: ${statusPath}`);
|
|
377
|
-
}
|
|
378
|
-
|
|
379
370
|
async function waitForMockPiCall(mockPi: MockPi, index: number, timeoutMs = 30_000): Promise<{ args: string[]; systemPrompts: NonNullable<MockPiCallRecord["systemPrompts"]> }> {
|
|
380
371
|
const deadline = Date.now() + timeoutMs;
|
|
381
372
|
for (;;) {
|
|
@@ -464,7 +455,11 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
464
455
|
removeTempDir(tempDir);
|
|
465
456
|
});
|
|
466
457
|
|
|
467
|
-
function makeAsyncExecutor(
|
|
458
|
+
function makeAsyncExecutor(
|
|
459
|
+
agents: ReturnType<typeof makeAgent>[],
|
|
460
|
+
config: Record<string, unknown> = {},
|
|
461
|
+
discoverOverride?: (cwd: string) => { agents: ReturnType<typeof makeAgent>[]; cwd?: string; scope?: "user" | "project" | "both"; directories?: Array<{ source: "builtin" | "package" | "user" | "project" | "runtime"; path: string; state: "absent" | "empty" | "candidates" | "unreadable" | "not-directory"; candidateCount?: number }> },
|
|
462
|
+
) {
|
|
468
463
|
return createSubagentExecutor!({
|
|
469
464
|
pi: { events: createEventBus(), getSessionName: () => undefined },
|
|
470
465
|
state: { baseCwd: tempDir, currentSessionId: null, asyncJobs: new Map(), foregroundControls: new Map(), lastForegroundControlId: null },
|
|
@@ -473,7 +468,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
473
468
|
tempArtifactsDir: tempDir,
|
|
474
469
|
getSubagentSessionRoot: () => tempDir,
|
|
475
470
|
expandTilde: (p: string) => p,
|
|
476
|
-
discoverAgents: () => ({ agents }),
|
|
471
|
+
discoverAgents: (cwd: string) => discoverOverride ? discoverOverride(cwd) : ({ agents }),
|
|
477
472
|
});
|
|
478
473
|
}
|
|
479
474
|
|
|
@@ -568,6 +563,54 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
568
563
|
assert.equal(status.endedAt !== undefined, true);
|
|
569
564
|
});
|
|
570
565
|
|
|
566
|
+
it("persists the bounded usage projection in async results and metadata", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
567
|
+
mockPi.onCall({ output: "accounted output" });
|
|
568
|
+
const id = `async-usage-artifact-${Date.now().toString(36)}`;
|
|
569
|
+
const artifactsDir = path.join(tempDir, ".pi-subagents", "artifacts");
|
|
570
|
+
executeAsyncSingle(id, {
|
|
571
|
+
agent: "worker",
|
|
572
|
+
task: "Persist usage",
|
|
573
|
+
agentConfig: makeAgent("worker", { completionGuard: false }),
|
|
574
|
+
ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-usage-artifact" },
|
|
575
|
+
artifactConfig: { enabled: true, includeInput: false, includeOutput: true, includeJsonl: false, includeMetadata: true, cleanupDays: 7 },
|
|
576
|
+
artifactsDir,
|
|
577
|
+
shareEnabled: false,
|
|
578
|
+
maxSubagentDepth: 2,
|
|
579
|
+
acceptance: false,
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
const payload = await readAsyncPayload(id);
|
|
583
|
+
const expectedUsage = { input: 100, output: 50, cacheRead: 0, cacheWrite: 0, cost: 0.001, turns: 1 };
|
|
584
|
+
assert.deepEqual(payload.results[0]?.usage, expectedUsage);
|
|
585
|
+
const metadataPath = payload.results[0]?.artifactPaths?.metadataPath;
|
|
586
|
+
assert.ok(metadataPath);
|
|
587
|
+
const metadata = JSON.parse(fs.readFileSync(metadataPath, "utf-8")) as { usage?: unknown };
|
|
588
|
+
assert.deepEqual(metadata.usage, expectedUsage);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it("makes a launched async run immediately visible to exact status lookup", { skip: !isAsyncAvailable() || !resolveTargetedAsyncRun ? "jiti not available" : undefined }, async () => {
|
|
592
|
+
mockPi.onCall({ delay: 500, output: "visible async done" });
|
|
593
|
+
const id = `async-initial-status-${Date.now().toString(36)}`;
|
|
594
|
+
const launch = executeAsyncSingle(id, {
|
|
595
|
+
agent: "worker",
|
|
596
|
+
task: "Remain visible while starting",
|
|
597
|
+
agentConfig: makeAgent("worker", { completionGuard: false, model: "mock/test-model" }),
|
|
598
|
+
ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-initial-status" },
|
|
599
|
+
availableModels: [{ provider: "mock", id: "test-model", fullId: "mock/test-model", contextWindow: 128_000 }],
|
|
600
|
+
artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
|
|
601
|
+
shareEnabled: false,
|
|
602
|
+
maxSubagentDepth: 2,
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
assert.equal(launch.isError, undefined);
|
|
606
|
+
assert.equal(resolveTargetedAsyncRun(ASYNC_DIR, id, "session-initial-status").kind, "exact");
|
|
607
|
+
const status = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, id, "status.json"), "utf-8")) as AsyncStatusPayload;
|
|
608
|
+
assert.equal(status.sessionId, "session-initial-status");
|
|
609
|
+
assert.equal(status.pid !== undefined, true);
|
|
610
|
+
assert.equal(status.steps?.[0]?.contextLimit, 128_000);
|
|
611
|
+
await waitForAsyncResultFile(id);
|
|
612
|
+
});
|
|
613
|
+
|
|
571
614
|
it("persists intercom detach receipts on failed async steps", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
572
615
|
mockPi.onCall({ output: "Detached for intercom coordination before task completion.", exitCode: -2 });
|
|
573
616
|
const id = `async-intercom-detach-${Date.now().toString(36)}`;
|
|
@@ -575,7 +618,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
575
618
|
await waitForAsyncResultFile(id);
|
|
576
619
|
const status = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, id, "status.json"), "utf-8")) as AsyncStatusPayload;
|
|
577
620
|
assert.equal(status.state, "failed");
|
|
578
|
-
assert.equal(status.error, "
|
|
621
|
+
assert.equal(status.error, "Detached for intercom coordination before task completion.");
|
|
579
622
|
assert.equal(status.steps?.[0]?.error, "Detached for intercom coordination before task completion.");
|
|
580
623
|
});
|
|
581
624
|
|
|
@@ -615,7 +658,6 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
615
658
|
it("matches preflight launch digest in equivalent foreground and async execution", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
616
659
|
const agentName = `contract-worker-${Date.now().toString(36)}`;
|
|
617
660
|
const task = "Compare the resolved launch inputs.";
|
|
618
|
-
const turnBudget = { maxTurns: 2, graceTurns: 1 } as const;
|
|
619
661
|
const previousAgentDir = process.env.SELESAI_CODING_AGENT_DIR;
|
|
620
662
|
const agentDir = path.join(tempDir, "agent-home");
|
|
621
663
|
process.env.SELESAI_CODING_AGENT_DIR = agentDir;
|
|
@@ -629,12 +671,12 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
629
671
|
try {
|
|
630
672
|
const discovered = discoverAgents(tempDir).agents.find((agent) => agent.name === agentName);
|
|
631
673
|
assert.ok(discovered, "expected temporary agent definition to be discovered");
|
|
632
|
-
const preflight = await resolveSubagentLaunchContract({ agent: agentName, cwd: tempDir, task,
|
|
674
|
+
const preflight = await resolveSubagentLaunchContract({ agent: agentName, cwd: tempDir, task, runId: "contract-preflight" });
|
|
633
675
|
assert.equal(preflight.ok, true);
|
|
634
676
|
assert.ok(preflight.contract.tools.extensionArgs.some((entry) => entry.endsWith(path.join("pi-permission-system", "src", "index.ts"))));
|
|
635
677
|
|
|
636
678
|
mockPi.onCall({ output: "foreground contract comparison" });
|
|
637
|
-
const foreground = await runSync(tempDir, [discovered], agentName, task, { runId: "contract-foreground", acceptance: false
|
|
679
|
+
const foreground = await runSync(tempDir, [discovered], agentName, task, { runId: "contract-foreground", acceptance: false });
|
|
638
680
|
assert.equal(foreground.exitCode, 0);
|
|
639
681
|
assert.equal(foreground.launchContractDigest, preflight.contract.launchContractDigest);
|
|
640
682
|
|
|
@@ -650,7 +692,6 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
650
692
|
sessionRoot: path.join(tempDir, "sessions"),
|
|
651
693
|
maxSubagentDepth: 2,
|
|
652
694
|
acceptance: false,
|
|
653
|
-
turnBudget,
|
|
654
695
|
});
|
|
655
696
|
const payload = await readAsyncPayload(asyncId);
|
|
656
697
|
assert.equal(launch.details.launchContractDigest, preflight.contract.launchContractDigest);
|
|
@@ -1170,6 +1211,20 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
1170
1211
|
assert.ok(Date.now() - startedAt >= 1200, "background runner must not terminate during the retry delay");
|
|
1171
1212
|
});
|
|
1172
1213
|
|
|
1214
|
+
it("background does not drain on settlement from a compaction attempt that will retry", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
1215
|
+
mockPi.onCall({ steps: [
|
|
1216
|
+
{ jsonl: [{ type: "compaction_end", willRetry: true }, { type: "agent_settled" }] },
|
|
1217
|
+
{ delay: 1400, jsonl: [events.assistantMessage("settled after compaction retry"), { type: "agent_start" }, { type: "agent_end", willRetry: false }, { type: "agent_settled" }] },
|
|
1218
|
+
] });
|
|
1219
|
+
const id = `async-lifecycle-compaction-retry-${Date.now().toString(36)}`;
|
|
1220
|
+
const startedAt = Date.now();
|
|
1221
|
+
launchProtocolTest(id);
|
|
1222
|
+
const payload = await readAsyncPayload(id);
|
|
1223
|
+
assert.equal(payload.success, true);
|
|
1224
|
+
assert.equal(payload.results[0]?.output, "settled after compaction retry");
|
|
1225
|
+
assert.ok(Date.now() - startedAt >= 1200, "background runner must not terminate during compaction retry");
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1173
1228
|
it("background treats agent_settled as a clean terminal watermark", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
1174
1229
|
mockPi.onCall({ jsonl: [mockAssistantMessage("settled async without a terminal assistant stop", "tool_use"), { type: "agent_settled" }], keepAliveAfterFinalMessageMs: 15_000 });
|
|
1175
1230
|
const id = `async-lifecycle-settled-${Date.now().toString(36)}`;
|
|
@@ -1182,6 +1237,23 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
1182
1237
|
assert.ok(Date.now() - startedAt < 10_000, "agent_settled should trigger bounded child cleanup");
|
|
1183
1238
|
});
|
|
1184
1239
|
|
|
1240
|
+
it("does not report successful compaction settlement as a failure", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
1241
|
+
mockPi.onCall({
|
|
1242
|
+
jsonl: [{ type: "compaction_start" }, mockAssistantMessage("settled after compaction"), { type: "agent_settled" }],
|
|
1243
|
+
keepAliveAfterFinalMessageMs: 15_000,
|
|
1244
|
+
});
|
|
1245
|
+
const id = `async-lifecycle-compaction-success-${Date.now().toString(36)}`;
|
|
1246
|
+
launchProtocolTest(id);
|
|
1247
|
+
const payload = await readAsyncPayload(id);
|
|
1248
|
+
assert.equal(payload.success, true);
|
|
1249
|
+
assert.equal(payload.results[0]?.success, true);
|
|
1250
|
+
assert.equal(payload.results[0]?.error, undefined);
|
|
1251
|
+
assert.equal(payload.results[0]?.output, "settled after compaction");
|
|
1252
|
+
assert.equal(payload.results[0]?.effects?.settlementDiagnostic, undefined);
|
|
1253
|
+
const status = await waitForAsyncState(id, (candidate) => candidate.state === "complete");
|
|
1254
|
+
assert.equal(status.steps?.[0]?.effects?.settlementDiagnostic, undefined);
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1185
1257
|
it("keeps named output references literal in async single tasks", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
1186
1258
|
const task = "Reply with OK. You may reference {outputs.name} if it helps.";
|
|
1187
1259
|
mockPi.onCall({ output: "OK" });
|
|
@@ -1556,6 +1628,36 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
1556
1628
|
}
|
|
1557
1629
|
});
|
|
1558
1630
|
|
|
1631
|
+
it("background keeps a terminal answer authoritative over an earlier tool timeout", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
1632
|
+
mockPi.onCall({
|
|
1633
|
+
steps: [
|
|
1634
|
+
{ jsonl: [events.toolStart("bash")] },
|
|
1635
|
+
{ delay: 50, jsonl: [events.assistantMessage("Done")] },
|
|
1636
|
+
],
|
|
1637
|
+
keepAliveAfterFinalMessageMs: 1_500,
|
|
1638
|
+
});
|
|
1639
|
+
const id = `async-terminal-tool-timeout-${Date.now().toString(36)}`;
|
|
1640
|
+
process.env.SELESAI_SUBAGENT_TOOL_TIMEOUT_MS = "600";
|
|
1641
|
+
try {
|
|
1642
|
+
executeAsyncChain(id, {
|
|
1643
|
+
chain: [{ agent: "one", task: "Do work" }],
|
|
1644
|
+
agents: [makeAgent("one")],
|
|
1645
|
+
ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
|
|
1646
|
+
artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
|
|
1647
|
+
shareEnabled: false,
|
|
1648
|
+
maxSubagentDepth: 2,
|
|
1649
|
+
timeoutMs: 5_000,
|
|
1650
|
+
});
|
|
1651
|
+
|
|
1652
|
+
const payload = await readAsyncPayload(id);
|
|
1653
|
+
assert.equal(payload.success, true);
|
|
1654
|
+
assert.equal(payload.results[0]?.timedOut, undefined);
|
|
1655
|
+
assert.equal(payload.results[0]?.output, "Done");
|
|
1656
|
+
} finally {
|
|
1657
|
+
delete process.env.SELESAI_SUBAGENT_TOOL_TIMEOUT_MS;
|
|
1658
|
+
}
|
|
1659
|
+
});
|
|
1660
|
+
|
|
1559
1661
|
it("keeps an earlier wedged tool armed when another tool starts and ends", { skip: !isAsyncAvailable() ? "jiti not available" : process.platform === "win32" ? "timeout signal delivery intermittent on Windows CI" : undefined }, async () => {
|
|
1560
1662
|
mockPi.onCall({
|
|
1561
1663
|
steps: [
|
|
@@ -1835,224 +1937,6 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
1835
1937
|
assert.ok(elapsedMs < timeoutMs + 4_000, `timeout should cancel acceptance verification well before the verify command completes, elapsed ${elapsedMs}ms`);
|
|
1836
1938
|
});
|
|
1837
1939
|
|
|
1838
|
-
it("async turn budget allows a terminal final grace turn", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
1839
|
-
mockPi.onCall({
|
|
1840
|
-
jsonl: [
|
|
1841
|
-
mockAssistantMessage("working before wrap-up", "tool_use"),
|
|
1842
|
-
mockAssistantMessage("final wrapped output", "stop"),
|
|
1843
|
-
],
|
|
1844
|
-
});
|
|
1845
|
-
const id = `async-turn-budget-soft-${Date.now().toString(36)}`;
|
|
1846
|
-
executeAsyncSingle(id, {
|
|
1847
|
-
agent: "worker",
|
|
1848
|
-
task: "Use the final grace turn to wrap up.",
|
|
1849
|
-
agentConfig: makeAgent("worker"),
|
|
1850
|
-
ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
|
|
1851
|
-
artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
|
|
1852
|
-
shareEnabled: false,
|
|
1853
|
-
maxSubagentDepth: 2,
|
|
1854
|
-
turnBudget: { maxTurns: 1, graceTurns: 1 },
|
|
1855
|
-
});
|
|
1856
|
-
|
|
1857
|
-
const resultPath = await waitForAsyncResultFile(id, 10_000);
|
|
1858
|
-
const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
|
|
1859
|
-
const status = await waitForAsyncState(id, (candidate) => candidate.state === "complete");
|
|
1860
|
-
assert.equal(payload.success, true);
|
|
1861
|
-
assert.equal(payload.state, "complete");
|
|
1862
|
-
assert.equal(payload.turnBudgetExceeded, undefined);
|
|
1863
|
-
assert.equal(payload.wrapUpRequested, true);
|
|
1864
|
-
assert.equal(payload.turnBudget?.outcome, "wrap-up-requested");
|
|
1865
|
-
assert.equal(payload.turnBudget?.turnCount, 2);
|
|
1866
|
-
assert.equal(payload.results[0]?.wrapUpRequested, true);
|
|
1867
|
-
assert.equal(payload.results[0]?.turnBudget?.turnCount, 2);
|
|
1868
|
-
assert.match(payload.results[0]?.output ?? "", /Turn budget wrap-up was requested after 1 assistant turn/);
|
|
1869
|
-
assert.match(payload.results[0]?.output ?? "", /final wrapped output/);
|
|
1870
|
-
assert.equal(status.wrapUpRequested, true);
|
|
1871
|
-
assert.equal(status.turnBudgetExceeded, undefined);
|
|
1872
|
-
assert.equal(status.steps?.[0]?.wrapUpRequested, true);
|
|
1873
|
-
assert.equal(status.steps?.[0]?.turnBudget?.turnCount, 2);
|
|
1874
|
-
});
|
|
1875
|
-
|
|
1876
|
-
it("async turn budget preserves a clean terminal response beyond the final grace turn", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
1877
|
-
mockPi.onCall({
|
|
1878
|
-
jsonl: [
|
|
1879
|
-
mockAssistantMessage("working before wrap-up", "tool_use"),
|
|
1880
|
-
mockAssistantMessage("starting final grace tool work", "tool_use"),
|
|
1881
|
-
mockAssistantMessage("safe assistant boundary after tool work", "stop"),
|
|
1882
|
-
],
|
|
1883
|
-
});
|
|
1884
|
-
const id = `async-turn-budget-hard-${Date.now().toString(36)}`;
|
|
1885
|
-
executeAsyncSingle(id, {
|
|
1886
|
-
agent: "worker",
|
|
1887
|
-
task: "Exceed the turn budget.",
|
|
1888
|
-
agentConfig: makeAgent("worker"),
|
|
1889
|
-
ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
|
|
1890
|
-
artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
|
|
1891
|
-
shareEnabled: false,
|
|
1892
|
-
maxSubagentDepth: 2,
|
|
1893
|
-
turnBudget: { maxTurns: 1, graceTurns: 1 },
|
|
1894
|
-
});
|
|
1895
|
-
|
|
1896
|
-
const resultPath = await waitForAsyncResultFile(id, 10_000);
|
|
1897
|
-
const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
|
|
1898
|
-
const status = await waitForAsyncState(id, (candidate) => candidate.state === "complete");
|
|
1899
|
-
assert.equal(payload.success, true);
|
|
1900
|
-
assert.equal(payload.state, "complete");
|
|
1901
|
-
assert.equal(payload.exitCode, 0);
|
|
1902
|
-
assert.equal(payload.turnBudgetExceeded, undefined);
|
|
1903
|
-
assert.equal(payload.wrapUpRequested, true);
|
|
1904
|
-
assert.equal(payload.turnBudget?.outcome, "wrap-up-requested");
|
|
1905
|
-
assert.equal(payload.turnBudget?.turnCount, 3);
|
|
1906
|
-
assert.equal(payload.results[0]?.turnBudgetExceeded, undefined);
|
|
1907
|
-
assert.match(payload.results[0]?.output ?? "", /safe assistant boundary after tool work/);
|
|
1908
|
-
assert.equal(status.state, "complete");
|
|
1909
|
-
assert.equal(status.turnBudgetExceeded, undefined);
|
|
1910
|
-
assert.equal(status.steps?.[0]?.turnBudgetExceeded, undefined);
|
|
1911
|
-
assert.equal(status.steps?.[0]?.turnBudget?.outcome, "wrap-up-requested");
|
|
1912
|
-
});
|
|
1913
|
-
|
|
1914
|
-
it("preserves an async clean completion after turn-budget work defers", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
1915
|
-
const id = `async-turn-budget-deferred-${Date.now().toString(36)}`;
|
|
1916
|
-
const releasePath = path.join(tempDir, `${id}.release`);
|
|
1917
|
-
mockPi.onCall({
|
|
1918
|
-
steps: [
|
|
1919
|
-
{
|
|
1920
|
-
jsonl: [
|
|
1921
|
-
mockAssistantMessage("starting required tool work", "tool_use"),
|
|
1922
|
-
events.toolStart("bash", { command: "node build.mjs" }),
|
|
1923
|
-
],
|
|
1924
|
-
},
|
|
1925
|
-
{
|
|
1926
|
-
waitForPath: releasePath,
|
|
1927
|
-
jsonl: [
|
|
1928
|
-
events.toolResult("bash", "build completed"),
|
|
1929
|
-
events.toolEnd("bash"),
|
|
1930
|
-
mockAssistantMessage("safe assistant boundary reached", "stop"),
|
|
1931
|
-
],
|
|
1932
|
-
},
|
|
1933
|
-
],
|
|
1934
|
-
});
|
|
1935
|
-
executeAsyncSingle(id, {
|
|
1936
|
-
agent: "worker",
|
|
1937
|
-
task: "Finish active tool work before enforcing the hard limit.",
|
|
1938
|
-
agentConfig: makeAgent("worker"),
|
|
1939
|
-
ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
|
|
1940
|
-
artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
|
|
1941
|
-
shareEnabled: false,
|
|
1942
|
-
maxSubagentDepth: 2,
|
|
1943
|
-
turnBudget: { maxTurns: 1, graceTurns: 0 },
|
|
1944
|
-
});
|
|
1945
|
-
|
|
1946
|
-
let pending: AsyncStatusPayload;
|
|
1947
|
-
try {
|
|
1948
|
-
pending = await waitForDeferredTurnBudget(id);
|
|
1949
|
-
} finally {
|
|
1950
|
-
fs.writeFileSync(releasePath, "release", "utf-8");
|
|
1951
|
-
}
|
|
1952
|
-
assert.equal(pending.error, undefined);
|
|
1953
|
-
assert.equal(pending.turnBudgetExceeded, undefined);
|
|
1954
|
-
assert.equal(pending.turnBudget?.terminationDeferredAtTurn, 1);
|
|
1955
|
-
assert.equal(pending.steps?.[0]?.status, "running");
|
|
1956
|
-
assert.equal(pending.steps?.[0]?.turnBudgetExceeded, undefined);
|
|
1957
|
-
|
|
1958
|
-
const resultPath = await waitForAsyncResultFile(id, 10_000);
|
|
1959
|
-
const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
|
|
1960
|
-
const status = await waitForAsyncState(id, (candidate) => candidate.state === "complete");
|
|
1961
|
-
assert.equal(payload.state, "complete");
|
|
1962
|
-
assert.equal(payload.turnBudgetExceeded, undefined);
|
|
1963
|
-
assert.equal(payload.turnBudget?.outcome, "wrap-up-requested");
|
|
1964
|
-
assert.equal(payload.turnBudget?.turnCount, 2);
|
|
1965
|
-
assert.equal(payload.results[0]?.turnBudgetExceeded, undefined);
|
|
1966
|
-
assert.match(payload.results[0]?.output ?? "", /safe assistant boundary reached/);
|
|
1967
|
-
assert.equal(status.state, "complete");
|
|
1968
|
-
assert.equal(status.turnBudgetExceeded, undefined);
|
|
1969
|
-
assert.equal(status.steps?.[0]?.turnBudget?.outcome, "wrap-up-requested");
|
|
1970
|
-
});
|
|
1971
|
-
|
|
1972
|
-
it("lets timeout delivery preempt deferred turn-budget termination", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
1973
|
-
mockPi.onCall({
|
|
1974
|
-
steps: [
|
|
1975
|
-
{
|
|
1976
|
-
jsonl: [
|
|
1977
|
-
mockAssistantMessage("starting long tool work", "tool_use"),
|
|
1978
|
-
events.toolStart("bash", { command: "node build.mjs" }),
|
|
1979
|
-
],
|
|
1980
|
-
},
|
|
1981
|
-
{ delay: 5_000, jsonl: [events.toolEnd("bash")] },
|
|
1982
|
-
],
|
|
1983
|
-
});
|
|
1984
|
-
const id = `async-turn-budget-timeout-${Date.now().toString(36)}`;
|
|
1985
|
-
executeAsyncSingle(id, {
|
|
1986
|
-
agent: "worker",
|
|
1987
|
-
task: "Timeout while hard termination is deferred.",
|
|
1988
|
-
agentConfig: makeAgent("worker"),
|
|
1989
|
-
ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
|
|
1990
|
-
artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
|
|
1991
|
-
shareEnabled: false,
|
|
1992
|
-
maxSubagentDepth: 2,
|
|
1993
|
-
turnBudget: { maxTurns: 1, graceTurns: 0 },
|
|
1994
|
-
timeoutMs: 30_000,
|
|
1995
|
-
});
|
|
1996
|
-
|
|
1997
|
-
const pending = await waitForDeferredTurnBudget(id);
|
|
1998
|
-
const asyncDir = path.join(ASYNC_DIR, id);
|
|
1999
|
-
deliverTimeoutRequest({ asyncDir, pid: pending.pid, source: "test" });
|
|
2000
|
-
const resultPath = await waitForAsyncResultFile(id, 10_000);
|
|
2001
|
-
const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
|
|
2002
|
-
const status = await waitForAsyncState(id, (candidate) => candidate.state === "failed");
|
|
2003
|
-
assert.equal(payload.state, "failed");
|
|
2004
|
-
assert.equal(payload.timedOut, true);
|
|
2005
|
-
assert.equal(payload.turnBudgetExceeded, undefined);
|
|
2006
|
-
assert.match(payload.error ?? payload.summary ?? "", /timed out after 30000ms/);
|
|
2007
|
-
assert.equal(payload.results[0]?.timedOut, true);
|
|
2008
|
-
assert.equal(payload.results[0]?.turnBudgetExceeded, undefined);
|
|
2009
|
-
assert.equal(status.timedOut, true);
|
|
2010
|
-
assert.equal(status.turnBudgetExceeded, undefined);
|
|
2011
|
-
assert.equal(status.steps?.[0]?.timedOut, true);
|
|
2012
|
-
});
|
|
2013
|
-
|
|
2014
|
-
it("lets explicit stop preempt deferred turn-budget termination", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
2015
|
-
mockPi.onCall({
|
|
2016
|
-
steps: [
|
|
2017
|
-
{
|
|
2018
|
-
jsonl: [
|
|
2019
|
-
mockAssistantMessage("starting long tool work", "tool_use"),
|
|
2020
|
-
events.toolStart("bash", { command: "node build.mjs" }),
|
|
2021
|
-
],
|
|
2022
|
-
},
|
|
2023
|
-
{ delay: 5_000, jsonl: [events.toolEnd("bash")] },
|
|
2024
|
-
],
|
|
2025
|
-
});
|
|
2026
|
-
const id = `async-turn-budget-stop-${Date.now().toString(36)}`;
|
|
2027
|
-
executeAsyncSingle(id, {
|
|
2028
|
-
agent: "worker",
|
|
2029
|
-
task: "Stop while hard termination is deferred.",
|
|
2030
|
-
agentConfig: makeAgent("worker"),
|
|
2031
|
-
ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
|
|
2032
|
-
artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
|
|
2033
|
-
shareEnabled: false,
|
|
2034
|
-
maxSubagentDepth: 2,
|
|
2035
|
-
turnBudget: { maxTurns: 1, graceTurns: 0 },
|
|
2036
|
-
});
|
|
2037
|
-
|
|
2038
|
-
const pending = await waitForDeferredTurnBudget(id);
|
|
2039
|
-
const asyncDir = path.join(ASYNC_DIR, id);
|
|
2040
|
-
deliverStopRequest({ asyncDir, pid: pending.pid, source: "test" });
|
|
2041
|
-
const resultPath = await waitForAsyncResultFile(id, 10_000);
|
|
2042
|
-
const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
|
|
2043
|
-
const status = await waitForAsyncState(id, (candidate) => candidate.state === "stopped");
|
|
2044
|
-
assert.equal(payload.state, "stopped");
|
|
2045
|
-
assert.equal(payload.stopped, true);
|
|
2046
|
-
assert.equal(payload.turnBudgetExceeded, undefined);
|
|
2047
|
-
assert.match(payload.error ?? payload.summary ?? "", /stopped by user/i);
|
|
2048
|
-
assert.equal(payload.results[0]?.stopped, true);
|
|
2049
|
-
assert.equal(payload.results[0]?.turnBudgetExceeded, undefined);
|
|
2050
|
-
assert.equal(status.state, "stopped");
|
|
2051
|
-
assert.equal(status.stopped, true);
|
|
2052
|
-
assert.equal(status.turnBudgetExceeded, undefined);
|
|
2053
|
-
assert.equal(status.steps?.[0]?.status, "stopped");
|
|
2054
|
-
});
|
|
2055
|
-
|
|
2056
1940
|
it("async launch messages tell the parent not to sleep-poll", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
2057
1941
|
const artifactConfig = {
|
|
2058
1942
|
enabled: false,
|
|
@@ -2530,7 +2414,6 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
2530
2414
|
parallel: {
|
|
2531
2415
|
agent: "reviewer",
|
|
2532
2416
|
task: "Review {target.path}",
|
|
2533
|
-
label: "Review {target.path}",
|
|
2534
2417
|
outputSchema: { type: "object" },
|
|
2535
2418
|
},
|
|
2536
2419
|
collect: { as: "reviews" },
|
|
@@ -2555,6 +2438,8 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
2555
2438
|
assert.match(readMockPiArgs(mockPi, 1).at(-1) ?? "", /Review src\/a\.ts/);
|
|
2556
2439
|
assert.match(readMockPiArgs(mockPi, 2).at(-1) ?? "", /Review src\/b\.ts/);
|
|
2557
2440
|
assert.match(readMockPiArgs(mockPi, 3).at(-1) ?? "", /"key":"src\/a\.ts"/);
|
|
2441
|
+
assert.deepEqual(status.steps?.slice(1, 3).map((step) => step.sessionName), ["reviewer: Review src/a.ts", "reviewer: Review src/b.ts"]);
|
|
2442
|
+
assert.deepEqual(payload.results.slice(1, 3).map((result) => result.sessionName), ["reviewer: Review src/a.ts", "reviewer: Review src/b.ts"]);
|
|
2558
2443
|
const collected = payload.outputs?.reviews?.structured as Array<{ key: string; structured: unknown }>;
|
|
2559
2444
|
assert.deepEqual(collected.map((item) => item.key), ["src/a.ts", "src/b.ts"]);
|
|
2560
2445
|
assert.deepEqual(collected.map((item) => item.structured), [{ ok: "a" }, { ok: "b" }]);
|
|
@@ -2984,7 +2869,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
2984
2869
|
content: [{ type: "text", text: "primary failed" }],
|
|
2985
2870
|
model: "openai/gpt-5-mini",
|
|
2986
2871
|
errorMessage: "rate limit exceeded",
|
|
2987
|
-
usage: { input: 10, output: 5, cacheRead:
|
|
2872
|
+
usage: { input: 10, output: 5, cacheRead: 300, cacheWrite: 0, cost: { total: 0.01 } },
|
|
2988
2873
|
},
|
|
2989
2874
|
}],
|
|
2990
2875
|
exitCode: 1,
|
|
@@ -2998,7 +2883,6 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
2998
2883
|
agent: "worker",
|
|
2999
2884
|
task: "Do work",
|
|
3000
2885
|
acceptance: { level: "none", reason: "descriptor persistence coverage" },
|
|
3001
|
-
turnBudget: { maxTurns: 8, graceTurns: 2 },
|
|
3002
2886
|
agentConfig: makeAgent("worker", {
|
|
3003
2887
|
model: "openai/gpt-5-mini:high",
|
|
3004
2888
|
fallbackModels: ["anthropic/claude-sonnet-4:low"],
|
|
@@ -3040,7 +2924,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
3040
2924
|
assert.equal(descriptor.cwd, tempDir);
|
|
3041
2925
|
assert.equal(descriptor.sessionDir, path.join(sessionRoot, `async-${id}`));
|
|
3042
2926
|
assert.deepEqual(descriptor.acceptance, { level: "none", reason: "descriptor persistence coverage" });
|
|
3043
|
-
assert.
|
|
2927
|
+
assert.equal(descriptor.initialTurnBudget, undefined);
|
|
3044
2928
|
assert.equal(Object.hasOwn(descriptor.acceptance, "explicit"), false);
|
|
3045
2929
|
assert.equal(Object.hasOwn(descriptor.acceptance, "inferredReason"), false);
|
|
3046
2930
|
assert.equal(Object.hasOwn(descriptor, "task"), false);
|
|
@@ -3067,6 +2951,10 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
3067
2951
|
assert.equal(statusPayload.steps[0]?.thinking, "low");
|
|
3068
2952
|
assert.ok(statusPayload.totalTokens!.total > 0);
|
|
3069
2953
|
assert.ok(statusPayload.steps[0]?.tokens!.total > 0);
|
|
2954
|
+
assert.equal(statusPayload.totalTokens!.window, 100);
|
|
2955
|
+
assert.equal(statusPayload.totalTokens!.windowPeak, 310);
|
|
2956
|
+
assert.equal(statusPayload.steps[0]?.tokens!.window, 100);
|
|
2957
|
+
assert.equal(statusPayload.steps[0]?.tokens!.windowPeak, 310);
|
|
3070
2958
|
assert.deepEqual(statusPayload.steps[0]?.totalCost, { inputTokens: 110, outputTokens: 55, costUsd: 0.011 });
|
|
3071
2959
|
assert.deepEqual(statusPayload.totalCost, { inputTokens: 110, outputTokens: 55, costUsd: 0.011 });
|
|
3072
2960
|
const events = fs.readFileSync(path.join(asyncDir, "events.jsonl"), "utf-8").trim().split("\n").map((line) => JSON.parse(line));
|
|
@@ -3200,6 +3088,54 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
3200
3088
|
assert.equal(mockPi.callCount(), 2);
|
|
3201
3089
|
});
|
|
3202
3090
|
|
|
3091
|
+
it("background runs retry the fallback model after a provider connection error", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
3092
|
+
mockPi.onCall({
|
|
3093
|
+
jsonl: [{
|
|
3094
|
+
type: "message_end",
|
|
3095
|
+
message: {
|
|
3096
|
+
role: "assistant",
|
|
3097
|
+
content: [],
|
|
3098
|
+
model: "openai/gpt-5-mini",
|
|
3099
|
+
errorMessage: "Connection error.",
|
|
3100
|
+
usage: { input: 10, output: 0, cacheRead: 0, cacheWrite: 0, cost: { total: 0.001 } },
|
|
3101
|
+
},
|
|
3102
|
+
}],
|
|
3103
|
+
exitCode: 1,
|
|
3104
|
+
});
|
|
3105
|
+
mockPi.onCall({ output: "Recovered after connection error" });
|
|
3106
|
+
const id = `async-fallback-connection-error-${Date.now().toString(36)}`;
|
|
3107
|
+
executeAsyncSingle(id, {
|
|
3108
|
+
agent: "worker",
|
|
3109
|
+
task: "Do work",
|
|
3110
|
+
agentConfig: makeAgent("worker", {
|
|
3111
|
+
model: "openai/gpt-5-mini:high",
|
|
3112
|
+
fallbackModels: ["anthropic/claude-sonnet-4:low"],
|
|
3113
|
+
}),
|
|
3114
|
+
ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
|
|
3115
|
+
availableModels: [
|
|
3116
|
+
{ provider: "openai", id: "gpt-5-mini", fullId: "openai/gpt-5-mini" },
|
|
3117
|
+
{ provider: "anthropic", id: "claude-sonnet-4", fullId: "anthropic/claude-sonnet-4" },
|
|
3118
|
+
],
|
|
3119
|
+
artifactConfig: {
|
|
3120
|
+
enabled: false,
|
|
3121
|
+
includeInput: false,
|
|
3122
|
+
includeOutput: false,
|
|
3123
|
+
includeJsonl: false,
|
|
3124
|
+
includeMetadata: false,
|
|
3125
|
+
cleanupDays: 7,
|
|
3126
|
+
},
|
|
3127
|
+
shareEnabled: false,
|
|
3128
|
+
maxSubagentDepth: 2,
|
|
3129
|
+
});
|
|
3130
|
+
|
|
3131
|
+
const payload = JSON.parse(fs.readFileSync(await waitForAsyncResultFile(id), "utf-8"));
|
|
3132
|
+
assert.equal(payload.success, true);
|
|
3133
|
+
assert.deepEqual(payload.results[0].attemptedModels, ["openai/gpt-5-mini:high", "anthropic/claude-sonnet-4:low"]);
|
|
3134
|
+
assert.match(payload.results[0].modelAttempts[0].error ?? "", /Connection error/u);
|
|
3135
|
+
assert.match(payload.results[0].output ?? "", /Recovered after connection error/u);
|
|
3136
|
+
assert.equal(mockPi.callCount(), 2);
|
|
3137
|
+
});
|
|
3138
|
+
|
|
3203
3139
|
it("background runs do not retry the fallback model for a trailing tool failure", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
3204
3140
|
mockPi.onCall({
|
|
3205
3141
|
jsonl: [
|
|
@@ -3242,29 +3178,297 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
3242
3178
|
assert.equal(mockPi.callCount(), 1);
|
|
3243
3179
|
});
|
|
3244
3180
|
|
|
3245
|
-
it("background
|
|
3181
|
+
it("background runs do not retry raw connection stderr after child activity", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
3246
3182
|
mockPi.onCall({
|
|
3247
3183
|
jsonl: [{
|
|
3248
3184
|
type: "message_end",
|
|
3249
3185
|
message: {
|
|
3250
3186
|
role: "assistant",
|
|
3251
|
-
content: [{ type: "text", text: "
|
|
3187
|
+
content: [{ type: "text", text: "completed side effect" }],
|
|
3252
3188
|
model: "openai/gpt-5-mini",
|
|
3253
|
-
|
|
3254
|
-
usage: { input: 10, output: 5, cacheRead: 0, cacheWrite: 0, cost: { total: 0.
|
|
3189
|
+
stopReason: "stop",
|
|
3190
|
+
usage: { input: 10, output: 5, cacheRead: 0, cacheWrite: 0, cost: { total: 0.001 } },
|
|
3255
3191
|
},
|
|
3256
3192
|
}],
|
|
3193
|
+
stderr: "APIConnectionError: Connection closed.",
|
|
3257
3194
|
exitCode: 1,
|
|
3258
3195
|
});
|
|
3259
|
-
mockPi.onCall({ output: "
|
|
3260
|
-
const id = `async-fallback-
|
|
3261
|
-
|
|
3196
|
+
mockPi.onCall({ output: "fallback must not run" });
|
|
3197
|
+
const id = `async-fallback-raw-stderr-${Date.now().toString(36)}`;
|
|
3198
|
+
executeAsyncSingle(id, {
|
|
3262
3199
|
agent: "worker",
|
|
3263
3200
|
task: "Do work",
|
|
3264
3201
|
agentConfig: makeAgent("worker", {
|
|
3265
3202
|
model: "openai/gpt-5-mini:high",
|
|
3266
3203
|
fallbackModels: ["anthropic/claude-sonnet-4:low"],
|
|
3267
|
-
|
|
3204
|
+
}),
|
|
3205
|
+
ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
|
|
3206
|
+
availableModels: [
|
|
3207
|
+
{ provider: "openai", id: "gpt-5-mini", fullId: "openai/gpt-5-mini" },
|
|
3208
|
+
{ provider: "anthropic", id: "claude-sonnet-4", fullId: "anthropic/claude-sonnet-4" },
|
|
3209
|
+
],
|
|
3210
|
+
artifactConfig: {
|
|
3211
|
+
enabled: false,
|
|
3212
|
+
includeInput: false,
|
|
3213
|
+
includeOutput: false,
|
|
3214
|
+
includeJsonl: false,
|
|
3215
|
+
includeMetadata: false,
|
|
3216
|
+
cleanupDays: 7,
|
|
3217
|
+
},
|
|
3218
|
+
shareEnabled: false,
|
|
3219
|
+
maxSubagentDepth: 2,
|
|
3220
|
+
});
|
|
3221
|
+
|
|
3222
|
+
const payload = JSON.parse(fs.readFileSync(await waitForAsyncResultFile(id), "utf-8"));
|
|
3223
|
+
assert.equal(payload.success, false);
|
|
3224
|
+
assert.equal(payload.results[0].modelAttempts.length, 1);
|
|
3225
|
+
assert.match(payload.results[0].error ?? "", /Connection closed/u);
|
|
3226
|
+
assert.equal(mockPi.callCount(), 1);
|
|
3227
|
+
});
|
|
3228
|
+
|
|
3229
|
+
it("background runs resume the retained session once after a compaction-induced abort following completed tool work", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
3230
|
+
const sessionFile = path.join(tempDir, "async-abort-recovery-session.jsonl");
|
|
3231
|
+
mockPi.onCall({
|
|
3232
|
+
jsonl: [
|
|
3233
|
+
events.toolStart("write", { path: "side-effect.txt", content: "done" }),
|
|
3234
|
+
events.toolEnd("write"),
|
|
3235
|
+
events.toolResult("write", "Wrote side-effect.txt"),
|
|
3236
|
+
{ type: "compaction_start" },
|
|
3237
|
+
{
|
|
3238
|
+
type: "message_end",
|
|
3239
|
+
message: {
|
|
3240
|
+
role: "assistant",
|
|
3241
|
+
content: [],
|
|
3242
|
+
model: "openai/gpt-5-mini",
|
|
3243
|
+
stopReason: "aborted",
|
|
3244
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: { total: 0 } },
|
|
3245
|
+
},
|
|
3246
|
+
},
|
|
3247
|
+
{ type: "agent_settled" },
|
|
3248
|
+
],
|
|
3249
|
+
writeFiles: [{ path: "side-effect.txt", content: "done" }, { path: sessionFile, content: "{}\n" }],
|
|
3250
|
+
keepAliveAfterFinalMessageMs: 5_000,
|
|
3251
|
+
exitCode: 0,
|
|
3252
|
+
});
|
|
3253
|
+
mockPi.onCall({ output: "Recovered asynchronously from retained session" });
|
|
3254
|
+
const id = `async-fallback-provider-after-tool-${Date.now().toString(36)}`;
|
|
3255
|
+
executeAsyncSingle(id, {
|
|
3256
|
+
agent: "worker",
|
|
3257
|
+
task: "Do work",
|
|
3258
|
+
sessionFile,
|
|
3259
|
+
agentConfig: makeAgent("worker", {
|
|
3260
|
+
model: "openai/gpt-5-mini:high",
|
|
3261
|
+
fallbackModels: ["anthropic/claude-sonnet-4:low"],
|
|
3262
|
+
}),
|
|
3263
|
+
ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
|
|
3264
|
+
availableModels: [
|
|
3265
|
+
{ provider: "openai", id: "gpt-5-mini", fullId: "openai/gpt-5-mini" },
|
|
3266
|
+
{ provider: "anthropic", id: "claude-sonnet-4", fullId: "anthropic/claude-sonnet-4" },
|
|
3267
|
+
],
|
|
3268
|
+
artifactConfig: {
|
|
3269
|
+
enabled: false,
|
|
3270
|
+
includeInput: false,
|
|
3271
|
+
includeOutput: false,
|
|
3272
|
+
includeJsonl: false,
|
|
3273
|
+
includeMetadata: false,
|
|
3274
|
+
cleanupDays: 7,
|
|
3275
|
+
},
|
|
3276
|
+
shareEnabled: false,
|
|
3277
|
+
maxSubagentDepth: 2,
|
|
3278
|
+
});
|
|
3279
|
+
|
|
3280
|
+
const payload = JSON.parse(fs.readFileSync(await waitForAsyncResultFile(id), "utf-8"));
|
|
3281
|
+
assert.equal(payload.success, true);
|
|
3282
|
+
assert.deepEqual(payload.results[0].attemptedModels, ["openai/gpt-5-mini:high"]);
|
|
3283
|
+
assert.deepEqual(payload.results[0].modelAttempts.map((attempt: { success: boolean }) => attempt.success), [false, true]);
|
|
3284
|
+
assert.equal(mockPi.callCount(), 2);
|
|
3285
|
+
const firstArgs = readMockPiArgs(mockPi, 0);
|
|
3286
|
+
const resumedArgs = readMockPiArgs(mockPi, 1);
|
|
3287
|
+
assert.equal(firstArgs[firstArgs.indexOf("--session") + 1], sessionFile);
|
|
3288
|
+
assert.equal(resumedArgs[resumedArgs.indexOf("--session") + 1], sessionFile);
|
|
3289
|
+
assert.match(resumedArgs.at(-1) ?? "", /Continue from the current files and transcript/);
|
|
3290
|
+
assert.equal(fs.readFileSync(path.join(tempDir, "side-effect.txt"), "utf-8"), "done");
|
|
3291
|
+
});
|
|
3292
|
+
|
|
3293
|
+
it("background compaction abort without a retained session does not fall back", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
3294
|
+
mockPi.onCall({
|
|
3295
|
+
jsonl: [
|
|
3296
|
+
events.assistantMessage("I inspected the source."),
|
|
3297
|
+
{ type: "compaction_start" },
|
|
3298
|
+
{
|
|
3299
|
+
type: "message_end",
|
|
3300
|
+
message: {
|
|
3301
|
+
role: "assistant",
|
|
3302
|
+
content: [],
|
|
3303
|
+
model: "openai/gpt-5-mini",
|
|
3304
|
+
stopReason: "aborted",
|
|
3305
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: { total: 0 } },
|
|
3306
|
+
},
|
|
3307
|
+
},
|
|
3308
|
+
{ type: "agent_settled" },
|
|
3309
|
+
],
|
|
3310
|
+
keepAliveAfterFinalMessageMs: 5_000,
|
|
3311
|
+
exitCode: 0,
|
|
3312
|
+
});
|
|
3313
|
+
mockPi.onCall({ output: "Fallback must not run" });
|
|
3314
|
+
const id = `async-compaction-abort-no-session-${Date.now().toString(36)}`;
|
|
3315
|
+
executeAsyncSingle(id, {
|
|
3316
|
+
agent: "worker",
|
|
3317
|
+
task: "Do work",
|
|
3318
|
+
agentConfig: makeAgent("worker", {
|
|
3319
|
+
model: "openai/gpt-5-mini:high",
|
|
3320
|
+
fallbackModels: ["anthropic/claude-sonnet-4:low"],
|
|
3321
|
+
}),
|
|
3322
|
+
ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
|
|
3323
|
+
availableModels: [
|
|
3324
|
+
{ provider: "openai", id: "gpt-5-mini", fullId: "openai/gpt-5-mini" },
|
|
3325
|
+
{ provider: "anthropic", id: "claude-sonnet-4", fullId: "anthropic/claude-sonnet-4" },
|
|
3326
|
+
],
|
|
3327
|
+
artifactConfig: {
|
|
3328
|
+
enabled: false,
|
|
3329
|
+
includeInput: false,
|
|
3330
|
+
includeOutput: false,
|
|
3331
|
+
includeJsonl: false,
|
|
3332
|
+
includeMetadata: false,
|
|
3333
|
+
cleanupDays: 7,
|
|
3334
|
+
},
|
|
3335
|
+
shareEnabled: false,
|
|
3336
|
+
maxSubagentDepth: 2,
|
|
3337
|
+
});
|
|
3338
|
+
|
|
3339
|
+
const payload = JSON.parse(fs.readFileSync(await waitForAsyncResultFile(id), "utf-8"));
|
|
3340
|
+
assert.equal(payload.success, false);
|
|
3341
|
+
assert.match(payload.results[0]?.error ?? "", /Compaction-induced child abort could not be resumed safely: retained session unavailable\./);
|
|
3342
|
+
assert.doesNotMatch(payload.results[0]?.output ?? "", /Fallback must not run/);
|
|
3343
|
+
assert.equal(mockPi.callCount(), 1);
|
|
3344
|
+
});
|
|
3345
|
+
|
|
3346
|
+
it("background does not use compaction recovery for a generic empty assistant abort after a compaction retry", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
3347
|
+
const sessionFile = path.join(tempDir, "async-generic-empty-after-compaction-retry-session.jsonl");
|
|
3348
|
+
mockPi.onCall({
|
|
3349
|
+
jsonl: [
|
|
3350
|
+
{ type: "compaction_start" },
|
|
3351
|
+
{ type: "compaction_end", willRetry: true },
|
|
3352
|
+
{ type: "agent_settled" },
|
|
3353
|
+
{ type: "agent_start" },
|
|
3354
|
+
events.assistantMessage("The compaction retry produced useful output.", "openai/gpt-5-mini"),
|
|
3355
|
+
{ type: "agent_settled" },
|
|
3356
|
+
{
|
|
3357
|
+
type: "message_end",
|
|
3358
|
+
message: {
|
|
3359
|
+
role: "assistant",
|
|
3360
|
+
content: [],
|
|
3361
|
+
model: "openai/gpt-5-mini",
|
|
3362
|
+
stopReason: "aborted",
|
|
3363
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: { total: 0 } },
|
|
3364
|
+
},
|
|
3365
|
+
},
|
|
3366
|
+
{ type: "agent_settled" },
|
|
3367
|
+
],
|
|
3368
|
+
writeFiles: [{ path: sessionFile, content: "{}\n" }],
|
|
3369
|
+
exitCode: 0,
|
|
3370
|
+
});
|
|
3371
|
+
mockPi.onCall({ output: "Compaction recovery must not run" });
|
|
3372
|
+
const id = `async-no-compaction-recovery-after-retry-${Date.now().toString(36)}`;
|
|
3373
|
+
executeAsyncSingle(id, {
|
|
3374
|
+
agent: "worker",
|
|
3375
|
+
task: "Do work",
|
|
3376
|
+
sessionFile,
|
|
3377
|
+
agentConfig: makeAgent("worker", { model: "openai/gpt-5-mini" }),
|
|
3378
|
+
ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
|
|
3379
|
+
availableModels: [{ provider: "openai", id: "gpt-5-mini", fullId: "openai/gpt-5-mini" }],
|
|
3380
|
+
artifactConfig: {
|
|
3381
|
+
enabled: false,
|
|
3382
|
+
includeInput: false,
|
|
3383
|
+
includeOutput: false,
|
|
3384
|
+
includeJsonl: false,
|
|
3385
|
+
includeMetadata: false,
|
|
3386
|
+
cleanupDays: 7,
|
|
3387
|
+
},
|
|
3388
|
+
shareEnabled: false,
|
|
3389
|
+
maxSubagentDepth: 2,
|
|
3390
|
+
});
|
|
3391
|
+
|
|
3392
|
+
const payload = JSON.parse(fs.readFileSync(await waitForAsyncResultFile(id), "utf-8"));
|
|
3393
|
+
assert.equal(payload.success, false);
|
|
3394
|
+
assert.match(payload.results[0]?.error ?? "", /Subagent produced no output after terminal assistant stopReason "aborted"\./u);
|
|
3395
|
+
assert.equal(payload.results[0]?.modelAttempts?.length, 1);
|
|
3396
|
+
assert.equal(mockPi.callCount(), 1);
|
|
3397
|
+
});
|
|
3398
|
+
|
|
3399
|
+
it("background does not use compaction recovery after compaction_end willRetry false and a continued agent turn", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
3400
|
+
const sessionFile = path.join(tempDir, "async-generic-empty-after-successful-compaction-session.jsonl");
|
|
3401
|
+
mockPi.onCall({
|
|
3402
|
+
jsonl: [
|
|
3403
|
+
{ type: "compaction_start" },
|
|
3404
|
+
{ type: "compaction_end", willRetry: false },
|
|
3405
|
+
{ type: "agent_start" },
|
|
3406
|
+
{
|
|
3407
|
+
type: "message_end",
|
|
3408
|
+
message: {
|
|
3409
|
+
role: "assistant",
|
|
3410
|
+
content: [],
|
|
3411
|
+
model: "openai/gpt-5-mini",
|
|
3412
|
+
stopReason: "aborted",
|
|
3413
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: { total: 0 } },
|
|
3414
|
+
},
|
|
3415
|
+
},
|
|
3416
|
+
{ type: "agent_settled" },
|
|
3417
|
+
],
|
|
3418
|
+
writeFiles: [{ path: sessionFile, content: "{}\n" }],
|
|
3419
|
+
exitCode: 0,
|
|
3420
|
+
});
|
|
3421
|
+
mockPi.onCall({ output: "Compaction recovery must not run" });
|
|
3422
|
+
const id = `async-no-compaction-recovery-after-successful-compaction-${Date.now().toString(36)}`;
|
|
3423
|
+
executeAsyncSingle(id, {
|
|
3424
|
+
agent: "worker",
|
|
3425
|
+
task: "Do work",
|
|
3426
|
+
sessionFile,
|
|
3427
|
+
agentConfig: makeAgent("worker", { model: "openai/gpt-5-mini" }),
|
|
3428
|
+
ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
|
|
3429
|
+
availableModels: [{ provider: "openai", id: "gpt-5-mini", fullId: "openai/gpt-5-mini" }],
|
|
3430
|
+
artifactConfig: {
|
|
3431
|
+
enabled: false,
|
|
3432
|
+
includeInput: false,
|
|
3433
|
+
includeOutput: false,
|
|
3434
|
+
includeJsonl: false,
|
|
3435
|
+
includeMetadata: false,
|
|
3436
|
+
cleanupDays: 7,
|
|
3437
|
+
},
|
|
3438
|
+
shareEnabled: false,
|
|
3439
|
+
maxSubagentDepth: 2,
|
|
3440
|
+
});
|
|
3441
|
+
|
|
3442
|
+
const payload = JSON.parse(fs.readFileSync(await waitForAsyncResultFile(id), "utf-8"));
|
|
3443
|
+
assert.equal(payload.success, false);
|
|
3444
|
+
assert.match(payload.results[0]?.error ?? "", /Subagent produced no output after terminal assistant stopReason "aborted"\./u);
|
|
3445
|
+
assert.equal(payload.results[0]?.modelAttempts?.length, 1);
|
|
3446
|
+
assert.equal(mockPi.callCount(), 1);
|
|
3447
|
+
});
|
|
3448
|
+
|
|
3449
|
+
it("background single thinking override replaces primary and fallback suffixes", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
3450
|
+
mockPi.onCall({
|
|
3451
|
+
jsonl: [{
|
|
3452
|
+
type: "message_end",
|
|
3453
|
+
message: {
|
|
3454
|
+
role: "assistant",
|
|
3455
|
+
content: [{ type: "text", text: "primary failed" }],
|
|
3456
|
+
model: "openai/gpt-5-mini",
|
|
3457
|
+
errorMessage: "rate limit exceeded",
|
|
3458
|
+
usage: { input: 10, output: 5, cacheRead: 0, cacheWrite: 0, cost: { total: 0.01 } },
|
|
3459
|
+
},
|
|
3460
|
+
}],
|
|
3461
|
+
exitCode: 1,
|
|
3462
|
+
});
|
|
3463
|
+
mockPi.onCall({ output: "Recovered asynchronously" });
|
|
3464
|
+
const id = `async-fallback-thinking-off-${Date.now().toString(36)}`;
|
|
3465
|
+
const run = executeAsyncSingle(id, {
|
|
3466
|
+
agent: "worker",
|
|
3467
|
+
task: "Do work",
|
|
3468
|
+
agentConfig: makeAgent("worker", {
|
|
3469
|
+
model: "openai/gpt-5-mini:high",
|
|
3470
|
+
fallbackModels: ["anthropic/claude-sonnet-4:low"],
|
|
3471
|
+
thinking: "high",
|
|
3268
3472
|
}),
|
|
3269
3473
|
ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
|
|
3270
3474
|
availableModels: [
|
|
@@ -3343,6 +3547,68 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
3343
3547
|
assert.equal(mockPi.callCount(), 2);
|
|
3344
3548
|
});
|
|
3345
3549
|
|
|
3550
|
+
it("background fails a zero-exit child that stops during a tool after earlier assistant output", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
3551
|
+
mockPi.onCall({
|
|
3552
|
+
jsonl: [
|
|
3553
|
+
events.assistantMessage("Work is in progress"),
|
|
3554
|
+
events.toolStart("bash", { command: "write files" }),
|
|
3555
|
+
],
|
|
3556
|
+
exitCode: 0,
|
|
3557
|
+
});
|
|
3558
|
+
const id = `async-mid-tool-exit-${Date.now().toString(36)}`;
|
|
3559
|
+
executeAsyncSingle(id, {
|
|
3560
|
+
agent: "worker",
|
|
3561
|
+
task: "Do work",
|
|
3562
|
+
agentConfig: makeAgent("worker", { model: "openai/gpt-5-mini" }),
|
|
3563
|
+
ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
|
|
3564
|
+
artifactConfig: {
|
|
3565
|
+
enabled: false,
|
|
3566
|
+
includeInput: false,
|
|
3567
|
+
includeOutput: false,
|
|
3568
|
+
includeJsonl: false,
|
|
3569
|
+
includeMetadata: false,
|
|
3570
|
+
cleanupDays: 7,
|
|
3571
|
+
},
|
|
3572
|
+
shareEnabled: false,
|
|
3573
|
+
maxSubagentDepth: 2,
|
|
3574
|
+
});
|
|
3575
|
+
|
|
3576
|
+
const resultPath = await waitForAsyncResultFile(id);
|
|
3577
|
+
const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
|
|
3578
|
+
assert.equal(payload.success, false);
|
|
3579
|
+
assert.equal(payload.results[0]?.success, false);
|
|
3580
|
+
assert.match(payload.results[0]?.error ?? "", /exited during 'bash' tool execution \(exit 0\)/);
|
|
3581
|
+
assert.match(payload.results[0]?.error ?? "", /Earlier assistant output is not a terminal result/);
|
|
3582
|
+
assert.doesNotMatch(payload.results[0]?.error ?? "", /cold-start/);
|
|
3583
|
+
assert.equal(mockPi.callCount(), 1);
|
|
3584
|
+
});
|
|
3585
|
+
|
|
3586
|
+
it("background retains an earlier open tool when a later overlapping tool completes", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
3587
|
+
mockPi.onCall({
|
|
3588
|
+
jsonl: [
|
|
3589
|
+
{ type: "tool_execution_start", toolCallId: "bash-1", toolName: "bash", args: { command: "wait" } },
|
|
3590
|
+
{ type: "tool_execution_start", toolCallId: "read-1", toolName: "read", args: { path: "README.md" } },
|
|
3591
|
+
{ type: "tool_execution_end", toolCallId: "read-1", toolName: "read" },
|
|
3592
|
+
],
|
|
3593
|
+
exitCode: 0,
|
|
3594
|
+
});
|
|
3595
|
+
const id = `async-overlap-mid-tool-exit-${Date.now().toString(36)}`;
|
|
3596
|
+
executeAsyncSingle(id, {
|
|
3597
|
+
agent: "worker",
|
|
3598
|
+
task: "Do work",
|
|
3599
|
+
agentConfig: makeAgent("worker", { model: "openai/gpt-5-mini" }),
|
|
3600
|
+
ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
|
|
3601
|
+
artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
|
|
3602
|
+
shareEnabled: false,
|
|
3603
|
+
maxSubagentDepth: 2,
|
|
3604
|
+
});
|
|
3605
|
+
|
|
3606
|
+
const resultPath = await waitForAsyncResultFile(id);
|
|
3607
|
+
const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
|
|
3608
|
+
assert.equal(payload.success, false);
|
|
3609
|
+
assert.match(payload.results[0]?.error ?? "", /exited during 'bash' tool execution \(exit 0\)/);
|
|
3610
|
+
});
|
|
3611
|
+
|
|
3346
3612
|
it("background runs prefer empty-output fallback over an earlier tool error", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
3347
3613
|
mockPi.onCall({
|
|
3348
3614
|
jsonl: [
|
|
@@ -3770,6 +4036,12 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
3770
4036
|
assert.equal(payload.results[0].success, false);
|
|
3771
4037
|
assert.match(String(payload.results[0].error ?? ""), /completed without making edits/);
|
|
3772
4038
|
assert.match(String(payload.results[0].modelAttempts?.[0]?.error ?? ""), /completed without making edits/);
|
|
4039
|
+
assert.deepEqual(payload.results[0].effects?.settlementDiagnostic?.mutation, {
|
|
4040
|
+
expected: true,
|
|
4041
|
+
attempted: false,
|
|
4042
|
+
observed: false,
|
|
4043
|
+
});
|
|
4044
|
+
assert.equal(payload.results[0].effects?.settlementDiagnostic?.finalTextPresent, true);
|
|
3773
4045
|
|
|
3774
4046
|
const eventsPath = path.join(ASYNC_DIR, id, "events.jsonl");
|
|
3775
4047
|
const eventsText = fs.readFileSync(eventsPath, "utf-8");
|
|
@@ -4080,7 +4352,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
4080
4352
|
assert.match(result.content[0]?.type === "text" ? result.content[0].text : "", /Active async run capacity exhausted: 1\/1 used/);
|
|
4081
4353
|
assert.equal(fs.existsSync(rejectedAsyncDir), false);
|
|
4082
4354
|
assert.equal(fs.existsSync(rejectedResultPath), false);
|
|
4083
|
-
assert.equal(fs.existsSync(path.join(tempDir, ".
|
|
4355
|
+
assert.equal(fs.existsSync(path.join(tempDir, ".pi-subagents")), false);
|
|
4084
4356
|
fs.rmSync(path.join(ACTIVE_ASYNC_CAPACITY_DIR, activeAsyncCapacitySessionKey("session-cap")), { recursive: true, force: true });
|
|
4085
4357
|
});
|
|
4086
4358
|
|
|
@@ -4136,6 +4408,50 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
4136
4408
|
await readAsyncPayload(launch.details.asyncId);
|
|
4137
4409
|
});
|
|
4138
4410
|
|
|
4411
|
+
it("scheduled owners without a model do not inherit the live session model", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
|
|
4412
|
+
mockPi.onCall({ output: "Scheduled owner work completed" });
|
|
4413
|
+
const liveCwd = path.join(tempDir, "live-model-project");
|
|
4414
|
+
fs.mkdirSync(liveCwd);
|
|
4415
|
+
const state = {
|
|
4416
|
+
baseCwd: liveCwd,
|
|
4417
|
+
currentSessionId: "session-live",
|
|
4418
|
+
lastParentModel: { provider: "router", id: "live-model" },
|
|
4419
|
+
asyncJobs: new Map(),
|
|
4420
|
+
fleetJobs: new Map(),
|
|
4421
|
+
foregroundControls: new Map(),
|
|
4422
|
+
lastForegroundControlId: null,
|
|
4423
|
+
};
|
|
4424
|
+
const executor = createSubagentExecutor!({
|
|
4425
|
+
pi: { events: createEventBus(), getSessionName: () => undefined },
|
|
4426
|
+
state,
|
|
4427
|
+
config: {},
|
|
4428
|
+
asyncByDefault: false,
|
|
4429
|
+
tempArtifactsDir: tempDir,
|
|
4430
|
+
getSubagentSessionRoot: () => path.join(tempDir, "sessions"),
|
|
4431
|
+
expandTilde: (p: string) => p,
|
|
4432
|
+
discoverAgents: () => ({ agents: [makeAgent("worker")] }),
|
|
4433
|
+
});
|
|
4434
|
+
const retainedCtx = makeMinimalCtx(tempDir);
|
|
4435
|
+
retainedCtx.sessionManager.getSessionId = () => "session-scheduled-owner";
|
|
4436
|
+
|
|
4437
|
+
const launch = await executor.executeScheduled(
|
|
4438
|
+
`scheduled-owner-no-model-${Date.now().toString(36)}`,
|
|
4439
|
+
{ agent: "worker", task: "Run owner without model", async: true, acceptance: false },
|
|
4440
|
+
new AbortController().signal,
|
|
4441
|
+
retainedCtx,
|
|
4442
|
+
) as AsyncExecutionResult;
|
|
4443
|
+
assert.equal(launch.isError, undefined);
|
|
4444
|
+
assert.ok(launch.details.asyncId);
|
|
4445
|
+
|
|
4446
|
+
const payload = await readAsyncPayload(launch.details.asyncId);
|
|
4447
|
+
assert.equal(payload.success, true);
|
|
4448
|
+
assert.equal(state.currentSessionId, "session-live");
|
|
4449
|
+
assert.deepEqual(state.lastParentModel, { provider: "router", id: "live-model" });
|
|
4450
|
+
const args = readMockPiArgsMatching(mockPi, "Run owner without model");
|
|
4451
|
+
assert.equal(args.includes("router/live-model"), false);
|
|
4452
|
+
assert.equal(args.includes("--model"), false);
|
|
4453
|
+
});
|
|
4454
|
+
|
|
4139
4455
|
it("scheduled workflows keep owner status and registries isolated from the live session", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
|
|
4140
4456
|
const liveCwd = path.join(tempDir, "live-workflow-project");
|
|
4141
4457
|
fs.mkdirSync(liveCwd);
|
|
@@ -4222,8 +4538,52 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
4222
4538
|
assert.equal(args[args.indexOf("--model") + 1], "deepseek/deepseek-v4-flash");
|
|
4223
4539
|
});
|
|
4224
4540
|
|
|
4225
|
-
|
|
4226
|
-
|
|
4541
|
+
it("async workflows snapshot the parent model before workflow setup reads session data", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
|
|
4542
|
+
mockPi.onCall({ output: "Inherited model work" });
|
|
4543
|
+
mockPi.onCall({ output: "Explicit model work" });
|
|
4544
|
+
const state = {
|
|
4545
|
+
baseCwd: tempDir,
|
|
4546
|
+
currentSessionId: null,
|
|
4547
|
+
asyncJobs: new Map(),
|
|
4548
|
+
foregroundControls: new Map(),
|
|
4549
|
+
lastForegroundControlId: null,
|
|
4550
|
+
};
|
|
4551
|
+
const executor = createSubagentExecutor!({
|
|
4552
|
+
pi: { events: createEventBus(), getSessionName: () => undefined },
|
|
4553
|
+
state,
|
|
4554
|
+
config: {},
|
|
4555
|
+
asyncByDefault: false,
|
|
4556
|
+
tempArtifactsDir: tempDir,
|
|
4557
|
+
getSubagentSessionRoot: () => path.join(tempDir, "sessions"),
|
|
4558
|
+
expandTilde: (p: string) => p,
|
|
4559
|
+
discoverAgents: () => ({ agents: [makeAgent("worker")] }),
|
|
4560
|
+
});
|
|
4561
|
+
const context = makeMinimalCtx(tempDir);
|
|
4562
|
+
context.sessionManager.getSessionId = () => "session-workflow-parent-model";
|
|
4563
|
+
context.model = { provider: "router", id: "openai-personal" };
|
|
4564
|
+
context.sessionManager.getSessionFile = () => {
|
|
4565
|
+
context.model = undefined;
|
|
4566
|
+
return null;
|
|
4567
|
+
};
|
|
4568
|
+
|
|
4569
|
+
const launch = await executor.execute(
|
|
4570
|
+
"workflow-parent-model",
|
|
4571
|
+
{ workflowScript: `await runs.run("inherited", { agent: "worker", task: "Do inherited work" }); return runs.run("explicit", { agent: "worker", task: "Do explicit work", model: "openai/gpt-5-mini" });`, async: true },
|
|
4572
|
+
new AbortController().signal,
|
|
4573
|
+
undefined,
|
|
4574
|
+
context,
|
|
4575
|
+
) as AsyncExecutionResult;
|
|
4576
|
+
assert.equal(launch.isError, undefined);
|
|
4577
|
+
assert.ok(launch.details.asyncId);
|
|
4578
|
+
|
|
4579
|
+
const payload = await readAsyncPayload(launch.details.asyncId);
|
|
4580
|
+
assert.equal(payload.success, true);
|
|
4581
|
+
const inheritedArgs = readMockPiArgsMatching(mockPi, "Do inherited work");
|
|
4582
|
+
const explicitArgs = readMockPiArgsMatching(mockPi, "Do explicit work");
|
|
4583
|
+
assert.equal(inheritedArgs[inheritedArgs.indexOf("--model") + 1], "router/openai-personal");
|
|
4584
|
+
assert.equal(explicitArgs[explicitArgs.indexOf("--model") + 1], "openai/gpt-5-mini");
|
|
4585
|
+
});
|
|
4586
|
+
|
|
4227
4587
|
it("background single runs inherit the parent session model when no model is set", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
4228
4588
|
mockPi.onCall({ output: "Done asynchronously" });
|
|
4229
4589
|
|
|
@@ -4329,6 +4689,91 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
4329
4689
|
assert.equal(payload.results[0]?.model, "gateway/parent-model");
|
|
4330
4690
|
});
|
|
4331
4691
|
|
|
4692
|
+
it("reports the retained discovery context when a resumed target agent is missing", { skip: !createSubagentExecutor ? "executor not available" : undefined }, async () => {
|
|
4693
|
+
const runId = `resume-missing-agent-${Date.now().toString(36)}`;
|
|
4694
|
+
const asyncDir = path.join(ASYNC_DIR, runId);
|
|
4695
|
+
const sessionFile = path.join(tempDir, "missing-agent-session.jsonl");
|
|
4696
|
+
const visible = makeAgent("visible");
|
|
4697
|
+
visible.source = "project";
|
|
4698
|
+
const evidenceDir = path.join(tempDir, "request-discovery", "agents");
|
|
4699
|
+
try {
|
|
4700
|
+
fs.mkdirSync(asyncDir, { recursive: true });
|
|
4701
|
+
fs.writeFileSync(sessionFile, "", "utf-8");
|
|
4702
|
+
fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
|
|
4703
|
+
runId,
|
|
4704
|
+
sessionId: "session-123",
|
|
4705
|
+
mode: "single",
|
|
4706
|
+
state: "complete",
|
|
4707
|
+
startedAt: 100,
|
|
4708
|
+
lastUpdate: 200,
|
|
4709
|
+
cwd: tempDir,
|
|
4710
|
+
sessionFile,
|
|
4711
|
+
steps: [{ agent: "vanished", status: "complete" }],
|
|
4712
|
+
}, null, 2), "utf-8");
|
|
4713
|
+
const executor = makeAsyncExecutor([visible], {}, (cwd) => ({
|
|
4714
|
+
agents: [visible],
|
|
4715
|
+
cwd: path.resolve(cwd),
|
|
4716
|
+
scope: "both",
|
|
4717
|
+
directories: [{ source: "project", path: evidenceDir, state: "empty" }],
|
|
4718
|
+
}));
|
|
4719
|
+
const result = await executor.execute(
|
|
4720
|
+
"resume-missing-agent",
|
|
4721
|
+
{ action: "resume", id: runId, message: "Continue" },
|
|
4722
|
+
new AbortController().signal,
|
|
4723
|
+
undefined,
|
|
4724
|
+
makeMinimalCtx(tempDir),
|
|
4725
|
+
) as AsyncExecutionResult;
|
|
4726
|
+
assert.equal(result.isError, true);
|
|
4727
|
+
assert.match(result.content[0]?.text ?? "", /^Unknown agent for resume: vanished\nEffective cwd: /);
|
|
4728
|
+
assert.match(result.content[0]?.text ?? "", new RegExp(evidenceDir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
|
|
4729
|
+
assert.match(result.content[0]?.text ?? "", /visible \(project\)/);
|
|
4730
|
+
} finally {
|
|
4731
|
+
fs.rmSync(asyncDir, { recursive: true, force: true });
|
|
4732
|
+
}
|
|
4733
|
+
});
|
|
4734
|
+
|
|
4735
|
+
it("uses the append request discovery context for a missing appended agent", { skip: !createSubagentExecutor ? "executor not available" : undefined }, async () => {
|
|
4736
|
+
const runId = `append-missing-agent-${Date.now().toString(36)}`;
|
|
4737
|
+
const asyncDir = path.join(ASYNC_DIR, runId);
|
|
4738
|
+
const visible = makeAgent("visible");
|
|
4739
|
+
visible.source = "project";
|
|
4740
|
+
const evidenceDir = path.join(tempDir, "append-request", "agents");
|
|
4741
|
+
const storedCwd = path.join(tempDir, "stored-run-cwd");
|
|
4742
|
+
try {
|
|
4743
|
+
fs.mkdirSync(asyncDir, { recursive: true });
|
|
4744
|
+
fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
|
|
4745
|
+
runId,
|
|
4746
|
+
sessionId: "session-123",
|
|
4747
|
+
mode: "chain",
|
|
4748
|
+
state: "running",
|
|
4749
|
+
startedAt: 100,
|
|
4750
|
+
lastUpdate: 200,
|
|
4751
|
+
cwd: storedCwd,
|
|
4752
|
+
steps: [{ agent: "visible", status: "running" }],
|
|
4753
|
+
}, null, 2), "utf-8");
|
|
4754
|
+
const executor = makeAsyncExecutor([visible], {}, (cwd) => ({
|
|
4755
|
+
agents: [visible],
|
|
4756
|
+
cwd: path.resolve(cwd),
|
|
4757
|
+
scope: "both",
|
|
4758
|
+
directories: [{ source: "project", path: evidenceDir, state: "empty" }],
|
|
4759
|
+
}));
|
|
4760
|
+
const result = await executor.execute(
|
|
4761
|
+
"append-missing-agent",
|
|
4762
|
+
{ action: "append-step", id: runId, step: { agent: "vanished", task: "Review" } },
|
|
4763
|
+
new AbortController().signal,
|
|
4764
|
+
undefined,
|
|
4765
|
+
makeMinimalCtx(tempDir),
|
|
4766
|
+
) as AsyncExecutionResult;
|
|
4767
|
+
assert.equal(result.isError, true);
|
|
4768
|
+
assert.match(result.content[0]?.text ?? "", /^Unknown agent: vanished\nEffective cwd: /);
|
|
4769
|
+
assert.match(result.content[0]?.text ?? "", new RegExp(evidenceDir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
|
|
4770
|
+
assert.match(result.content[0]?.text ?? "", /visible \(project\)/);
|
|
4771
|
+
assert.doesNotMatch(result.content[0]?.text ?? "", new RegExp(storedCwd.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
|
|
4772
|
+
} finally {
|
|
4773
|
+
fs.rmSync(asyncDir, { recursive: true, force: true });
|
|
4774
|
+
}
|
|
4775
|
+
});
|
|
4776
|
+
|
|
4332
4777
|
it("background chains inherit the parent session model when no step or agent model is set", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
4333
4778
|
mockPi.onCall({ output: "Done asynchronously" });
|
|
4334
4779
|
|
|
@@ -4735,6 +5180,35 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
4735
5180
|
assert.match(result.content[0]?.text ?? "", /async-cfg-/);
|
|
4736
5181
|
});
|
|
4737
5182
|
|
|
5183
|
+
it("does not start child work when initial async status cannot be written", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
5184
|
+
const id = `async-status-write-fail-${Date.now().toString(36)}`;
|
|
5185
|
+
fs.mkdirSync(path.join(ASYNC_DIR, id, "status.json"), { recursive: true });
|
|
5186
|
+
mockPi.onCall({ output: "must not run" });
|
|
5187
|
+
|
|
5188
|
+
const result = executeAsyncSingle(id, {
|
|
5189
|
+
agent: "worker",
|
|
5190
|
+
task: "Do not start",
|
|
5191
|
+
agentConfig: makeAgent("worker"),
|
|
5192
|
+
ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
|
|
5193
|
+
artifactConfig: {
|
|
5194
|
+
enabled: false,
|
|
5195
|
+
includeInput: false,
|
|
5196
|
+
includeOutput: false,
|
|
5197
|
+
includeJsonl: false,
|
|
5198
|
+
includeMetadata: false,
|
|
5199
|
+
cleanupDays: 7,
|
|
5200
|
+
},
|
|
5201
|
+
shareEnabled: false,
|
|
5202
|
+
sessionRoot: path.join(tempDir, "sessions"),
|
|
5203
|
+
maxSubagentDepth: 2,
|
|
5204
|
+
});
|
|
5205
|
+
|
|
5206
|
+
assert.equal(result.isError, true);
|
|
5207
|
+
assert.match(result.content[0]?.text ?? "", /Failed to persist initial async status/);
|
|
5208
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
5209
|
+
assert.equal(mockPi.callCount(), 0);
|
|
5210
|
+
});
|
|
5211
|
+
|
|
4738
5212
|
it("returns a tool error when an async run uses a missing cwd", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, () => {
|
|
4739
5213
|
const id = `async-missing-cwd-${Date.now().toString(36)}`;
|
|
4740
5214
|
const missingCwd = path.join(tempDir, "missing-cwd");
|
|
@@ -5077,6 +5551,386 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
5077
5551
|
assert.equal(payload.results[0].error, "provider exploded");
|
|
5078
5552
|
});
|
|
5079
5553
|
|
|
5554
|
+
it("reports terminal abort before a missing file-only handoff after mutation", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
5555
|
+
const partialOutput = "I’ll inspect the retained candidate before changing it.";
|
|
5556
|
+
const repo = createRepo("pi-subagents-missing-handoff-partial-");
|
|
5557
|
+
const outputPath = path.join(repo, "missing-challenge-report.md");
|
|
5558
|
+
mockPi.onCall({
|
|
5559
|
+
jsonl: [
|
|
5560
|
+
events.assistantMessage(partialOutput),
|
|
5561
|
+
{
|
|
5562
|
+
type: "message_end",
|
|
5563
|
+
message: {
|
|
5564
|
+
role: "assistant",
|
|
5565
|
+
content: [],
|
|
5566
|
+
model: "mock/test-model",
|
|
5567
|
+
stopReason: "aborted",
|
|
5568
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: { total: 0 } },
|
|
5569
|
+
},
|
|
5570
|
+
},
|
|
5571
|
+
],
|
|
5572
|
+
writeFiles: [{ path: "input.md", content: "changed by retained child\n" }],
|
|
5573
|
+
});
|
|
5574
|
+
|
|
5575
|
+
const task = [
|
|
5576
|
+
"You are reviving a previous subagent conversation.",
|
|
5577
|
+
"",
|
|
5578
|
+
"Original run: source-run",
|
|
5579
|
+
"Original agent: worker",
|
|
5580
|
+
"Original session file: /tmp/source-session.jsonl",
|
|
5581
|
+
"",
|
|
5582
|
+
"Use the stored session context as background. Answer the orchestrator's follow-up below. Do not assume the original child process is still alive.",
|
|
5583
|
+
"",
|
|
5584
|
+
"Follow-up:",
|
|
5585
|
+
"Implementation challenge pass 1 for the accepted candidate. Reconsider it and implement any better current-scope change.",
|
|
5586
|
+
].join("\n");
|
|
5587
|
+
const id = `async-missing-handoff-guard-${Date.now().toString(36)}`;
|
|
5588
|
+
try {
|
|
5589
|
+
executeAsyncSingle(id, {
|
|
5590
|
+
agent: "worker",
|
|
5591
|
+
task,
|
|
5592
|
+
agentConfig: makeAgent("worker"),
|
|
5593
|
+
ctx: { pi: { events: { emit() {} } }, cwd: repo, currentSessionId: "session-1" },
|
|
5594
|
+
artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
|
|
5595
|
+
shareEnabled: false,
|
|
5596
|
+
sessionRoot: path.join(tempDir, "sessions"),
|
|
5597
|
+
output: outputPath,
|
|
5598
|
+
outputMode: "file-only",
|
|
5599
|
+
maxSubagentDepth: 2,
|
|
5600
|
+
});
|
|
5601
|
+
|
|
5602
|
+
const resultPath = await waitForAsyncResultFile(id);
|
|
5603
|
+
const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
|
|
5604
|
+
const child = payload.results[0];
|
|
5605
|
+
const diagnostic = child?.error ?? "";
|
|
5606
|
+
assert.equal(payload.success, false);
|
|
5607
|
+
assert.equal(payload.state, "partial");
|
|
5608
|
+
assert.equal(child?.success, false);
|
|
5609
|
+
assert.match(diagnostic, /^Subagent produced no output after terminal assistant stopReason "aborted"\./);
|
|
5610
|
+
assert.match(diagnostic, new RegExp(`Required file-only output was not produced: ${escapeRegExp(outputPath)}`));
|
|
5611
|
+
assert.doesNotMatch(diagnostic, /completed without making edits/);
|
|
5612
|
+
assert.doesNotMatch(child?.modelAttempts?.[0]?.error ?? "", /completed without making edits/);
|
|
5613
|
+
assert.equal(child?.effects?.fileMutation?.status, "observed");
|
|
5614
|
+
assert.equal(child?.effects?.fileMutation?.attempted, true);
|
|
5615
|
+
assert.deepEqual(child?.effects?.fileMutation?.evidence?.changedFiles, ["input.md"]);
|
|
5616
|
+
assert.equal(child?.effects?.settlementDiagnostic?.requiredOutput?.missing, true);
|
|
5617
|
+
assert.equal(fs.existsSync(outputPath), false);
|
|
5618
|
+
|
|
5619
|
+
const status = await waitForAsyncState(id, (candidate) => candidate.state === "partial");
|
|
5620
|
+
assert.equal(status.activityState, "needs_attention");
|
|
5621
|
+
assert.equal(status.steps?.[0]?.activityState, "needs_attention");
|
|
5622
|
+
assert.equal(status.steps?.[0]?.error, diagnostic);
|
|
5623
|
+
const eventsText = fs.readFileSync(path.join(ASYNC_DIR, id, "events.jsonl"), "utf-8");
|
|
5624
|
+
assert.doesNotMatch(eventsText, /completed without making edits/);
|
|
5625
|
+
} finally {
|
|
5626
|
+
removeTempDir(repo);
|
|
5627
|
+
}
|
|
5628
|
+
});
|
|
5629
|
+
|
|
5630
|
+
it("preserves terminal empty-output diagnostics after useful child work", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
5631
|
+
const partialOutput = "I’ll inspect the retained candidate before changing it.";
|
|
5632
|
+
const outputPath = path.join(tempDir, "missing-aborted-report.md");
|
|
5633
|
+
mockPi.onCall({
|
|
5634
|
+
jsonl: [
|
|
5635
|
+
events.toolStart("read", { path: "src/index.ts" }),
|
|
5636
|
+
events.toolEnd("read"),
|
|
5637
|
+
events.toolResult("read", "file contents"),
|
|
5638
|
+
events.assistantMessage(partialOutput),
|
|
5639
|
+
{
|
|
5640
|
+
type: "message_end",
|
|
5641
|
+
message: {
|
|
5642
|
+
role: "assistant",
|
|
5643
|
+
content: [],
|
|
5644
|
+
model: "mock/test-model",
|
|
5645
|
+
stopReason: "aborted",
|
|
5646
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: { total: 0 } },
|
|
5647
|
+
},
|
|
5648
|
+
},
|
|
5649
|
+
],
|
|
5650
|
+
exitCode: 0,
|
|
5651
|
+
});
|
|
5652
|
+
|
|
5653
|
+
const id = `async-aborted-empty-handoff-${Date.now().toString(36)}`;
|
|
5654
|
+
executeAsyncSingle(id, {
|
|
5655
|
+
agent: "worker",
|
|
5656
|
+
task: "Implement the approved file changes",
|
|
5657
|
+
agentConfig: makeAgent("worker"),
|
|
5658
|
+
ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
|
|
5659
|
+
artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
|
|
5660
|
+
shareEnabled: false,
|
|
5661
|
+
sessionRoot: path.join(tempDir, "sessions"),
|
|
5662
|
+
output: outputPath,
|
|
5663
|
+
outputMode: "file-only",
|
|
5664
|
+
maxSubagentDepth: 2,
|
|
5665
|
+
});
|
|
5666
|
+
|
|
5667
|
+
const resultPath = await waitForAsyncResultFile(id);
|
|
5668
|
+
const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
|
|
5669
|
+
const child = payload.results[0];
|
|
5670
|
+
const diagnostic = child?.error ?? "";
|
|
5671
|
+
assert.equal(payload.success, false);
|
|
5672
|
+
assert.equal(child?.success, false);
|
|
5673
|
+
assert.match(diagnostic, /^Subagent produced no output after terminal assistant stopReason "aborted"\./);
|
|
5674
|
+
assert.match(diagnostic, /Required file-only output was not produced/);
|
|
5675
|
+
assert.doesNotMatch(diagnostic, /completed without making edits/);
|
|
5676
|
+
assert.doesNotMatch(child?.modelAttempts?.[0]?.error ?? "", /completed without making edits/);
|
|
5677
|
+
assert.equal(child?.effects?.settlementDiagnostic?.requiredOutput?.missing, true);
|
|
5678
|
+
assert.equal(child?.effects?.settlementDiagnostic?.finalTextPresent, true);
|
|
5679
|
+
assert.equal(fs.existsSync(outputPath), false);
|
|
5680
|
+
|
|
5681
|
+
const eventsText = fs.readFileSync(path.join(ASYNC_DIR, id, "events.jsonl"), "utf-8");
|
|
5682
|
+
assert.doesNotMatch(eventsText, /completed without making edits/);
|
|
5683
|
+
});
|
|
5684
|
+
|
|
5685
|
+
it("reports zero-exit terminal stderr before missing file-only output after settlement", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
5686
|
+
const outputPath = path.join(tempDir, "missing-stderr-report.md");
|
|
5687
|
+
const terminalStderr = "Extension error: stale ctx after session replacement";
|
|
5688
|
+
mockPi.onCall({
|
|
5689
|
+
jsonl: [
|
|
5690
|
+
events.toolStart("read", { path: "src/index.ts" }),
|
|
5691
|
+
events.toolEnd("read"),
|
|
5692
|
+
events.toolResult("read", "file contents"),
|
|
5693
|
+
events.assistantMessage("I found the candidate seam."),
|
|
5694
|
+
{
|
|
5695
|
+
type: "message_end",
|
|
5696
|
+
message: {
|
|
5697
|
+
role: "assistant",
|
|
5698
|
+
content: [],
|
|
5699
|
+
model: "mock/test-model",
|
|
5700
|
+
stopReason: "aborted",
|
|
5701
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: { total: 0 } },
|
|
5702
|
+
},
|
|
5703
|
+
},
|
|
5704
|
+
{ type: "agent_settled" },
|
|
5705
|
+
],
|
|
5706
|
+
stderr: terminalStderr,
|
|
5707
|
+
});
|
|
5708
|
+
|
|
5709
|
+
const id = `async-stderr-empty-handoff-${Date.now().toString(36)}`;
|
|
5710
|
+
executeAsyncSingle(id, {
|
|
5711
|
+
agent: "worker",
|
|
5712
|
+
task: "Implement the approved file changes",
|
|
5713
|
+
agentConfig: makeAgent("worker"),
|
|
5714
|
+
ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
|
|
5715
|
+
artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
|
|
5716
|
+
shareEnabled: false,
|
|
5717
|
+
sessionRoot: path.join(tempDir, "sessions"),
|
|
5718
|
+
output: outputPath,
|
|
5719
|
+
outputMode: "file-only",
|
|
5720
|
+
maxSubagentDepth: 2,
|
|
5721
|
+
});
|
|
5722
|
+
|
|
5723
|
+
const resultPath = await waitForAsyncResultFile(id);
|
|
5724
|
+
const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
|
|
5725
|
+
const child = payload.results[0];
|
|
5726
|
+
const diagnostic = child?.error ?? "";
|
|
5727
|
+
assert.equal(payload.success, false);
|
|
5728
|
+
assert.equal(child?.success, false);
|
|
5729
|
+
assert.match(diagnostic, new RegExp(`^${escapeRegExp(terminalStderr)}`));
|
|
5730
|
+
assert.match(diagnostic, /Required file-only output was not produced/);
|
|
5731
|
+
assert.doesNotMatch(diagnostic, /^Subagent produced no output \(possible model cold-start or empty response\)\./);
|
|
5732
|
+
assert.equal(fs.existsSync(outputPath), false);
|
|
5733
|
+
});
|
|
5734
|
+
|
|
5735
|
+
it("reports bounded compaction failure context when file-only output is missing", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
5736
|
+
const terminalError = `This operation was aborted\n${"x".repeat(12_000)}`;
|
|
5737
|
+
mockPi.onCall({
|
|
5738
|
+
jsonl: [
|
|
5739
|
+
events.toolStart("read", { path: "src/index.ts" }),
|
|
5740
|
+
events.toolEnd("read"),
|
|
5741
|
+
events.toolResult("read", "file contents"),
|
|
5742
|
+
{ type: "compaction_start" },
|
|
5743
|
+
{
|
|
5744
|
+
type: "message_end",
|
|
5745
|
+
message: {
|
|
5746
|
+
role: "assistant",
|
|
5747
|
+
content: [],
|
|
5748
|
+
model: "mock/test-model",
|
|
5749
|
+
stopReason: "error",
|
|
5750
|
+
errorMessage: terminalError,
|
|
5751
|
+
usage: { input: 100, output: 0, cacheRead: 0, cacheWrite: 0, cost: { total: 0.001 } },
|
|
5752
|
+
},
|
|
5753
|
+
},
|
|
5754
|
+
{ type: "agent_settled" },
|
|
5755
|
+
],
|
|
5756
|
+
exitCode: 0,
|
|
5757
|
+
});
|
|
5758
|
+
|
|
5759
|
+
const id = `async-compaction-file-only-error-${Date.now().toString(36)}`;
|
|
5760
|
+
const asyncDir = path.join(ASYNC_DIR, id);
|
|
5761
|
+
const outputPath = path.join(tempDir, "missing-oracle-report.md");
|
|
5762
|
+
executeAsyncSingle(id, {
|
|
5763
|
+
agent: "oracle",
|
|
5764
|
+
task: "Write a report",
|
|
5765
|
+
agentConfig: makeAgent("oracle"),
|
|
5766
|
+
ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
|
|
5767
|
+
artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
|
|
5768
|
+
shareEnabled: false,
|
|
5769
|
+
sessionRoot: path.join(tempDir, "sessions"),
|
|
5770
|
+
output: outputPath,
|
|
5771
|
+
outputMode: "file-only",
|
|
5772
|
+
maxSubagentDepth: 2,
|
|
5773
|
+
});
|
|
5774
|
+
|
|
5775
|
+
const resultPath = await waitForAsyncResultFile(id);
|
|
5776
|
+
const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
|
|
5777
|
+
const child = payload.results[0] as (AsyncResultPayload["results"][number] & Record<string, unknown>) | undefined;
|
|
5778
|
+
const diagnostic = child?.error ?? "";
|
|
5779
|
+
assert.equal(payload.success, false);
|
|
5780
|
+
assert.equal(child?.success, false);
|
|
5781
|
+
assert.match(diagnostic, /^This operation was aborted/);
|
|
5782
|
+
assert.match(diagnostic, /Compaction-induced child abort could not be resumed safely: retained session unavailable\./);
|
|
5783
|
+
assert.match(diagnostic, /failure followed session compaction and agent settlement/);
|
|
5784
|
+
assert.match(diagnostic, /Required file-only output was not produced/);
|
|
5785
|
+
assert.ok(diagnostic.length <= 8_192);
|
|
5786
|
+
assert.equal(fs.existsSync(outputPath), false);
|
|
5787
|
+
assert.equal(child?.output, "");
|
|
5788
|
+
assert.equal("savedOutputPath" in (child ?? {}), false);
|
|
5789
|
+
assert.equal("outputReference" in (child ?? {}), false);
|
|
5790
|
+
assert.equal(payload.summary, `oracle:\n${diagnostic}`);
|
|
5791
|
+
const status = await waitForAsyncState(id, (candidate) => candidate.state === "failed");
|
|
5792
|
+
assert.equal(status.steps?.[0]?.exitCode, 1);
|
|
5793
|
+
assert.equal(status.steps?.[0]?.error, diagnostic);
|
|
5794
|
+
const logPath = path.join(asyncDir, `subagent-log-${id}.md`);
|
|
5795
|
+
const deadline = Date.now() + 10_000;
|
|
5796
|
+
while (!fs.existsSync(logPath)) {
|
|
5797
|
+
if (Date.now() > deadline) assert.fail(`Timed out waiting for async run log: ${logPath}`);
|
|
5798
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
5799
|
+
}
|
|
5800
|
+
assert.ok(fs.readFileSync(logPath, "utf-8").includes(`## Summary\noracle:\n${diagnostic}`));
|
|
5801
|
+
});
|
|
5802
|
+
|
|
5803
|
+
it("preserves partial imported async roots with mutation evidence", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
5804
|
+
const sourceId = `partial-source-${Date.now().toString(36)}`;
|
|
5805
|
+
const sourceDir = path.join(ASYNC_DIR, sourceId);
|
|
5806
|
+
const message = "Required file-only output was not produced: report.md";
|
|
5807
|
+
const effects = { fileMutation: { status: "observed", expected: true, attempted: true, evidence: { source: "tracked-files", trackedOnly: true, cwd: tempDir, changedFiles: ["input.md"], attemptedMutation: true } } };
|
|
5808
|
+
fs.mkdirSync(sourceDir, { recursive: true });
|
|
5809
|
+
fs.writeFileSync(path.join(sourceDir, "status.json"), JSON.stringify({
|
|
5810
|
+
runId: sourceId,
|
|
5811
|
+
mode: "single",
|
|
5812
|
+
state: "partial",
|
|
5813
|
+
activityState: "needs_attention",
|
|
5814
|
+
startedAt: Date.now(),
|
|
5815
|
+
error: message,
|
|
5816
|
+
steps: [{ agent: "worker", status: "failed", activityState: "needs_attention", error: message, effects }],
|
|
5817
|
+
}), "utf-8");
|
|
5818
|
+
|
|
5819
|
+
const id = `async-imported-partial-${Date.now().toString(36)}`;
|
|
5820
|
+
executeAsyncChain(id, {
|
|
5821
|
+
chain: [],
|
|
5822
|
+
attachRoot: { runId: sourceId, asyncDir: sourceDir, resultPath: path.join(RESULTS_DIR, `${sourceId}.json`), index: 0, agent: "worker" },
|
|
5823
|
+
agents: [makeAgent("worker")],
|
|
5824
|
+
ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
|
|
5825
|
+
artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
|
|
5826
|
+
shareEnabled: false,
|
|
5827
|
+
sessionRoot: path.join(tempDir, "sessions"),
|
|
5828
|
+
maxSubagentDepth: 2,
|
|
5829
|
+
});
|
|
5830
|
+
|
|
5831
|
+
const resultPath = await waitForAsyncResultFile(id);
|
|
5832
|
+
const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
|
|
5833
|
+
assert.equal(payload.success, false);
|
|
5834
|
+
assert.equal(payload.state, "partial");
|
|
5835
|
+
assert.equal(payload.summary, message);
|
|
5836
|
+
assert.equal(payload.results[0]?.execution?.status, "partial");
|
|
5837
|
+
assert.deepEqual(payload.results[0]?.effects?.fileMutation?.evidence?.changedFiles, ["input.md"]);
|
|
5838
|
+
|
|
5839
|
+
const status = await waitForAsyncState(id, (candidate) => candidate.state === "partial");
|
|
5840
|
+
assert.equal(status.activityState, "needs_attention");
|
|
5841
|
+
assert.equal(status.steps?.[0]?.activityState, "needs_attention");
|
|
5842
|
+
});
|
|
5843
|
+
|
|
5844
|
+
it("keeps concrete sibling failures above partial mutation evidence", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
5845
|
+
const repo = createRepo("pi-subagents-partial-sibling-failure-");
|
|
5846
|
+
const outputPath = path.join(repo, "missing-report.md");
|
|
5847
|
+
mockPi.onCall({
|
|
5848
|
+
matchArgIncludes: "Write required report",
|
|
5849
|
+
jsonl: [
|
|
5850
|
+
events.assistantMessage("I changed the file but did not hand off the report."),
|
|
5851
|
+
{
|
|
5852
|
+
type: "message_end",
|
|
5853
|
+
message: {
|
|
5854
|
+
role: "assistant",
|
|
5855
|
+
content: [],
|
|
5856
|
+
model: "mock/test-model",
|
|
5857
|
+
stopReason: "aborted",
|
|
5858
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: { total: 0 } },
|
|
5859
|
+
},
|
|
5860
|
+
},
|
|
5861
|
+
],
|
|
5862
|
+
writeFiles: [{ path: "input.md", content: "changed before missing report\n" }],
|
|
5863
|
+
});
|
|
5864
|
+
mockPi.onCall({ matchArgIncludes: "Fail normally", stderr: "ordinary sibling failure", exitCode: 1 });
|
|
5865
|
+
|
|
5866
|
+
const id = `async-partial-sibling-failure-${Date.now().toString(36)}`;
|
|
5867
|
+
try {
|
|
5868
|
+
executeAsyncChain(id, {
|
|
5869
|
+
chain: [{
|
|
5870
|
+
parallel: [
|
|
5871
|
+
{ agent: "partial", task: "Write required report" },
|
|
5872
|
+
{ agent: "failure", task: "Fail normally" },
|
|
5873
|
+
],
|
|
5874
|
+
concurrency: 2,
|
|
5875
|
+
}],
|
|
5876
|
+
resultMode: "parallel",
|
|
5877
|
+
agents: [makeAgent("partial", { output: outputPath, outputMode: "file-only" }), makeAgent("failure", { completionGuard: false })],
|
|
5878
|
+
ctx: { pi: { events: { emit() {} } }, cwd: repo, currentSessionId: "session-1" },
|
|
5879
|
+
artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
|
|
5880
|
+
shareEnabled: false,
|
|
5881
|
+
maxSubagentDepth: 2,
|
|
5882
|
+
});
|
|
5883
|
+
|
|
5884
|
+
const payload = await readAsyncPayload(id);
|
|
5885
|
+
assert.equal(payload.success, false);
|
|
5886
|
+
assert.equal(payload.state, "failed");
|
|
5887
|
+
assert.match(payload.summary, /ordinary sibling failure/);
|
|
5888
|
+
assert.equal(payload.results[0]?.effects?.fileMutation?.status, "observed");
|
|
5889
|
+
assert.equal(payload.results[0]?.effects?.settlementDiagnostic?.requiredOutput?.missing, true);
|
|
5890
|
+
assert.match(payload.results[1]?.error ?? "", /ordinary sibling failure/);
|
|
5891
|
+
|
|
5892
|
+
const status = await waitForAsyncState(id, (candidate) => candidate.state === "failed");
|
|
5893
|
+
assert.equal(status.activityState, undefined);
|
|
5894
|
+
assert.match(status.error ?? "", /ordinary sibling failure/);
|
|
5895
|
+
} finally {
|
|
5896
|
+
removeTempDir(repo);
|
|
5897
|
+
}
|
|
5898
|
+
});
|
|
5899
|
+
|
|
5900
|
+
it("reports missing file-only output when a child exits without an error", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
5901
|
+
mockPi.onCall({ delay: 2_100, exitCode: 1 });
|
|
5902
|
+
|
|
5903
|
+
const id = `async-file-only-exit-without-error-${Date.now().toString(36)}`;
|
|
5904
|
+
const outputPath = path.join(tempDir, "missing-exit-report.md");
|
|
5905
|
+
executeAsyncSingle(id, {
|
|
5906
|
+
agent: "worker",
|
|
5907
|
+
task: "Write a report",
|
|
5908
|
+
agentConfig: makeAgent("worker", { completionGuard: false }),
|
|
5909
|
+
ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
|
|
5910
|
+
artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
|
|
5911
|
+
shareEnabled: false,
|
|
5912
|
+
sessionRoot: path.join(tempDir, "sessions"),
|
|
5913
|
+
output: outputPath,
|
|
5914
|
+
outputMode: "file-only",
|
|
5915
|
+
maxSubagentDepth: 2,
|
|
5916
|
+
});
|
|
5917
|
+
|
|
5918
|
+
const payload = await readAsyncPayload(id);
|
|
5919
|
+
const child = payload.results[0];
|
|
5920
|
+
const diagnostic = child?.error ?? "";
|
|
5921
|
+
assert.equal(payload.success, false);
|
|
5922
|
+
assert.equal(payload.exitCode, 1);
|
|
5923
|
+
assert.equal(child?.success, false);
|
|
5924
|
+
assert.match(diagnostic, /^Required file-only output was not produced:/);
|
|
5925
|
+
assert.equal(fs.existsSync(outputPath), false);
|
|
5926
|
+
assert.equal(child?.output, "");
|
|
5927
|
+
assert.equal(child?.effects?.settlementDiagnostic?.requiredOutput?.kind, "file-only");
|
|
5928
|
+
assert.equal(child?.effects?.settlementDiagnostic?.requiredOutput?.path, outputPath);
|
|
5929
|
+
assert.equal(child?.effects?.settlementDiagnostic?.requiredOutput?.missing, true);
|
|
5930
|
+
const status = await waitForAsyncState(id, (candidate) => candidate.state === "failed");
|
|
5931
|
+
assert.equal(status.steps?.[0]?.error, diagnostic);
|
|
5932
|
+
});
|
|
5933
|
+
|
|
5080
5934
|
it("background runs emit active-long-running control events from child turns", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
5081
5935
|
mockPi.onCall({
|
|
5082
5936
|
steps: [
|