@selesai/code 0.13.15 → 0.13.18
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 +37 -0
- package/README.md +2 -14
- package/dist/bun/cli.d.ts +3 -1
- package/dist/bun/cli.js +3 -9
- package/dist/bun/{register-bedrock.js → runtime-setup.js} +5 -0
- package/dist/bun/sandbox-env-setup.d.ts +1 -0
- package/dist/bun/sandbox-env-setup.js +3 -0
- package/dist/cli/auth-command.d.ts +23 -0
- package/dist/cli/auth-command.js +102 -0
- package/dist/cli/config-selector.js +2 -1
- package/dist/cli/setup.d.ts +1 -0
- package/dist/cli/setup.js +11 -0
- package/dist/cli.js +2 -14
- package/dist/config.d.ts +3 -2
- package/dist/config.js +9 -4
- package/dist/core/agent-session-runtime.js +15 -7
- package/dist/core/agent-session.d.ts +2 -6
- package/dist/core/agent-session.js +15 -4
- package/dist/core/compaction/branch-summarization.d.ts +1 -1
- package/dist/core/compaction/branch-summarization.js +2 -1
- package/dist/core/extensions/loader.js +3 -0
- package/dist/core/extensions/types.d.ts +16 -2
- package/dist/core/http-dispatcher.js +2 -0
- package/dist/core/keybindings.d.ts +5 -0
- package/dist/core/keybindings.js +4 -0
- package/dist/core/messages.d.ts +1 -1
- package/dist/core/model-config.d.ts +15 -0
- package/dist/core/model-config.js +3 -0
- package/dist/core/model-runtime.d.ts +1 -0
- package/dist/core/model-runtime.js +5 -2
- package/dist/core/provider-composer.js +8 -2
- package/dist/core/session-export.d.ts +3 -0
- package/dist/core/session-export.js +31 -0
- package/dist/core/session-manager.d.ts +3 -2
- package/dist/core/session-manager.js +40 -13
- package/dist/core/skills.d.ts +1 -1
- package/dist/core/skills.js +4 -2
- package/dist/core/system-prompt.js +10 -10
- package/dist/core/tools/bash.js +3 -117
- package/dist/core/tools/edit.d.ts +1 -12
- package/dist/core/tools/edit.js +5 -163
- package/dist/core/tools/find.js +4 -55
- package/dist/core/tools/grep.js +4 -60
- package/dist/core/tools/ls.js +4 -49
- package/dist/core/tools/read.d.ts +4 -0
- package/dist/core/tools/read.js +10 -122
- package/dist/core/tools/renderers/bash.d.ts +11 -0
- package/dist/core/tools/renderers/bash.js +129 -0
- package/dist/core/tools/renderers/edit.d.ts +22 -0
- package/dist/core/tools/renderers/edit.js +166 -0
- package/dist/core/tools/renderers/find.d.ts +9 -0
- package/dist/core/tools/renderers/find.js +63 -0
- package/dist/core/tools/renderers/grep.d.ts +9 -0
- package/dist/core/tools/renderers/grep.js +68 -0
- package/dist/core/tools/renderers/index.d.ts +27 -0
- package/dist/core/tools/renderers/index.js +46 -0
- package/dist/core/tools/renderers/ls.d.ts +9 -0
- package/dist/core/tools/renderers/ls.js +57 -0
- package/dist/core/tools/renderers/read.d.ts +10 -0
- package/dist/core/tools/renderers/read.js +128 -0
- package/dist/core/tools/renderers/write.d.ts +9 -0
- package/dist/core/tools/renderers/write.js +152 -0
- package/dist/core/tools/write.js +5 -146
- package/dist/extensions/capability-gateway/index.ts +12 -1
- package/dist/extensions/capability-gateway/integration.test.ts +23 -0
- package/dist/extensions/pi-hermes-memory/CHANGELOG.md +18 -0
- package/dist/extensions/pi-hermes-memory/README.md +16 -12
- package/dist/extensions/pi-hermes-memory/docs/ROADMAP.md +1 -1
- package/dist/extensions/pi-hermes-memory/package-lock.json +587 -62
- package/dist/extensions/pi-hermes-memory/package.json +4 -4
- package/dist/extensions/pi-hermes-memory/src/config.ts +32 -0
- package/dist/extensions/pi-hermes-memory/src/constants.ts +12 -2
- package/dist/extensions/pi-hermes-memory/src/handlers/background-review.ts +109 -51
- package/dist/extensions/pi-hermes-memory/src/handlers/index-sessions.ts +9 -3
- package/dist/extensions/pi-hermes-memory/src/handlers/review-memory-ops.ts +257 -98
- package/dist/extensions/pi-hermes-memory/src/handlers/session-backfill.ts +9 -2
- package/dist/extensions/pi-hermes-memory/src/handlers/sync-markdown-memories.ts +5 -0
- package/dist/extensions/pi-hermes-memory/src/index.ts +22 -2
- package/dist/extensions/pi-hermes-memory/src/paths.ts +1 -1
- package/dist/extensions/pi-hermes-memory/src/store/db.ts +6 -0
- package/dist/extensions/pi-hermes-memory/src/store/fts-query.ts +58 -1
- package/dist/extensions/pi-hermes-memory/src/store/memory-store.ts +60 -8
- package/dist/extensions/pi-hermes-memory/src/store/session-indexer.ts +169 -7
- package/dist/extensions/pi-hermes-memory/src/store/session-search.ts +7 -21
- package/dist/extensions/pi-hermes-memory/src/store/sqlite-memory-store.ts +134 -15
- package/dist/extensions/pi-hermes-memory/src/tools/memory-search-tool.ts +4 -2
- package/dist/extensions/pi-hermes-memory/src/tools/memory-tool.ts +24 -9
- package/dist/extensions/pi-hermes-memory/src/types.ts +11 -0
- package/dist/extensions/pi-hermes-memory/tests/config.test.ts +42 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/background-review.test.ts +159 -3
- package/dist/extensions/pi-hermes-memory/tests/handlers/review-memory-ops.test.ts +452 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/session-backfill.test.ts +39 -0
- package/dist/extensions/pi-hermes-memory/tests/integration/corruption-recovery-integration.test.ts +91 -0
- package/dist/extensions/pi-hermes-memory/tests/store/db.test.ts +29 -0
- package/dist/extensions/pi-hermes-memory/tests/store/fts-query.test.ts +116 -0
- package/dist/extensions/pi-hermes-memory/tests/store/memory-store.test.ts +163 -0
- package/dist/extensions/pi-hermes-memory/tests/store/session-indexer.test.ts +233 -0
- package/dist/extensions/pi-hermes-memory/tests/store/session-search.test.ts +12 -0
- package/dist/extensions/pi-hermes-memory/tests/store/sqlite-memory-store.test.ts +175 -0
- package/dist/extensions/pi-hermes-memory/tests/store/sqlite-native.test.ts +2 -2
- package/dist/extensions/pi-hermes-memory/tests/tools/memory-search-tool.test.ts +22 -0
- package/dist/extensions/pi-hermes-memory/tests/tools/memory-tool.test.ts +37 -1
- package/dist/extensions/pi-intercom/CHANGELOG.md +13 -0
- package/dist/extensions/pi-intercom/README.md +16 -2
- package/dist/extensions/pi-intercom/broker/spawn.test.ts +19 -2
- package/dist/extensions/pi-intercom/broker/spawn.ts +4 -4
- package/dist/extensions/pi-intercom/index.ts +1 -1
- package/dist/extensions/pi-intercom/intercom.integration.test.ts +121 -2
- package/dist/extensions/pi-intercom/package.json +1 -1
- package/dist/extensions/pi-intercom/project-agent.test.ts +1 -1
- package/dist/extensions/pi-intercom/project-agent.ts +1 -6
- package/dist/extensions/pi-intercom/skills/pi-intercom/SKILL.md +7 -6
- package/dist/extensions/pi-rewind-hook/index.test.ts +7 -0
- package/dist/extensions/pi-subagents/.github/workflows/test.yml +36 -4
- package/dist/extensions/pi-subagents/CHANGELOG.md +120 -0
- package/dist/extensions/pi-subagents/README.md +3 -3
- package/dist/extensions/pi-subagents/agents/researcher.md +23 -13
- package/dist/extensions/pi-subagents/agents/reviewer.md +1 -1
- package/dist/extensions/pi-subagents/agents/scout.md +1 -1
- package/dist/extensions/pi-subagents/docs/agents.md +32 -16
- package/dist/extensions/pi-subagents/docs/configuration.md +41 -15
- package/dist/extensions/pi-subagents/docs/extension-api.md +109 -6
- package/dist/extensions/pi-subagents/docs/missions.md +2 -0
- package/dist/extensions/pi-subagents/docs/models.md +58 -1
- package/dist/extensions/pi-subagents/docs/observability.md +50 -12
- package/dist/extensions/pi-subagents/docs/tool-reference.md +17 -10
- package/dist/extensions/pi-subagents/docs/watchdog.md +1 -1
- package/dist/extensions/pi-subagents/docs/workflows.md +26 -9
- package/dist/extensions/pi-subagents/package-lock.json +747 -177
- package/dist/extensions/pi-subagents/package.json +6 -4
- package/dist/extensions/pi-subagents/runner-server-preload.mjs +13 -0
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +2 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +21 -4
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/multi-lane-orchestration.md +2 -0
- package/dist/extensions/pi-subagents/src/agents/advertised-agent-prompt.ts +63 -0
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +61 -16
- package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +2 -0
- package/dist/extensions/pi-subagents/src/agents/agents.ts +8 -0
- package/dist/extensions/pi-subagents/src/api/capability-ceiling.ts +0 -1
- package/dist/extensions/pi-subagents/src/api/{pi-args.ts → child-tool-plan.ts} +1 -1
- package/dist/extensions/pi-subagents/src/api/preflight.ts +7 -4
- package/dist/extensions/pi-subagents/src/api/shared-types.ts +2 -2
- package/dist/extensions/pi-subagents/src/api/workflow-resources.ts +6 -0
- package/dist/extensions/pi-subagents/src/extension/config.ts +4 -2
- package/dist/extensions/pi-subagents/src/extension/doctor.ts +2 -10
- package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +9 -11
- package/dist/extensions/pi-subagents/src/extension/index.ts +99 -10
- package/dist/extensions/pi-subagents/src/extension/public-execution.ts +13 -0
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +7 -23
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +11 -8
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +25 -7
- package/dist/extensions/pi-subagents/src/integrations/pi-web-session-liveness.ts +73 -0
- package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +228 -136
- package/dist/extensions/pi-subagents/src/intercom/supervisor-ui.ts +244 -0
- package/dist/extensions/pi-subagents/src/missions/workflow-state.ts +37 -16
- package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +18 -18
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +51 -25
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +46 -3
- package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +14 -3
- package/dist/extensions/pi-subagents/src/runs/background/async-retention.ts +9 -0
- package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +10 -12
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +18 -15
- package/dist/extensions/pi-subagents/src/runs/background/auto-drain.ts +40 -29
- package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +8 -0
- package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +79 -247
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +88 -12
- package/dist/extensions/pi-subagents/src/runs/background/owned-process-tree.ts +6 -6
- package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +24 -24
- package/dist/extensions/pi-subagents/src/runs/background/retained-nested-route-tracker.ts +96 -0
- package/dist/extensions/pi-subagents/src/runs/background/run-child-session.ts +642 -0
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +75 -6
- package/dist/extensions/pi-subagents/src/runs/background/runner-aliases.ts +163 -0
- package/dist/extensions/pi-subagents/src/runs/background/runner-child-launch.ts +86 -0
- package/dist/extensions/pi-subagents/src/runs/background/runner-child-sessions.ts +31 -0
- package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +18 -4
- package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +3 -1
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +597 -1092
- package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +3 -0
- package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +4 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/async-steering-action.ts +20 -17
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +296 -394
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +4 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +3 -1
- package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +9 -5
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +630 -283
- package/dist/extensions/pi-subagents/src/runs/foreground/workflow-detach-reconcile.ts +8 -5
- package/dist/extensions/pi-subagents/src/runs/foreground/workflow-foreground-steering.ts +78 -98
- package/dist/extensions/pi-subagents/src/runs/shared/abort-recovery.ts +3 -3
- package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +16 -3
- package/dist/extensions/pi-subagents/src/runs/shared/agent-contract.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/shared/async-status-projection.ts +47 -47
- package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +1 -2
- package/dist/extensions/pi-subagents/src/runs/shared/child-hooks.ts +174 -0
- package/dist/extensions/pi-subagents/src/runs/shared/child-identity.ts +14 -3
- package/dist/extensions/pi-subagents/src/runs/shared/child-launch.ts +319 -0
- package/dist/extensions/pi-subagents/src/runs/shared/child-lifecycle.ts +25 -0
- package/dist/extensions/pi-subagents/src/runs/shared/child-runtime-config.ts +126 -0
- package/dist/extensions/pi-subagents/src/runs/shared/child-session.ts +373 -0
- package/dist/extensions/pi-subagents/src/runs/shared/child-tool-plan.ts +530 -0
- package/dist/extensions/pi-subagents/src/runs/shared/completion-evidence.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +2 -1
- package/dist/extensions/pi-subagents/src/runs/shared/external-cli-preflight.ts +16 -0
- package/dist/extensions/pi-subagents/src/runs/shared/host-step-status.ts +11 -11
- package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +30 -20
- package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +5 -4
- package/dist/extensions/pi-subagents/src/runs/shared/model-exclusions.ts +84 -15
- package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +88 -12
- package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +37 -53
- package/dist/extensions/pi-subagents/src/runs/shared/nested-path.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/shared/orca-progress-tabs.ts +18 -7
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +57 -12
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +3 -4
- package/dist/extensions/pi-subagents/src/runs/shared/permissions.ts +0 -11
- package/dist/extensions/pi-subagents/src/runs/shared/process-signal.ts +4 -1
- package/dist/extensions/pi-subagents/src/runs/shared/readonly-drain-observation.ts +42 -0
- package/dist/extensions/pi-subagents/src/runs/shared/readonly-model-continuation.ts +69 -0
- package/dist/extensions/pi-subagents/src/runs/shared/readonly-session-evidence.ts +307 -0
- package/dist/extensions/pi-subagents/src/runs/shared/run-fanout-budget.ts +8 -21
- package/dist/extensions/pi-subagents/src/runs/shared/runtime-acknowledged-extensions.ts +3 -30
- package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +17 -4
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +2 -0
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +99 -386
- package/dist/extensions/pi-subagents/src/runs/shared/tool-availability.ts +18 -62
- package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +0 -14
- package/dist/extensions/pi-subagents/src/runs/shared/worktree-cleanup-plan.ts +31 -9
- package/dist/extensions/pi-subagents/src/runs/shared/worktree-setup-command.ts +190 -0
- package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +506 -226
- package/dist/extensions/pi-subagents/src/shared/child-session-name.ts +1 -1
- package/dist/extensions/pi-subagents/src/shared/jsonl-writer.ts +11 -0
- package/dist/extensions/pi-subagents/src/shared/model-response-aliases.ts +13 -0
- package/dist/extensions/pi-subagents/src/shared/thinking-ceiling.ts +0 -6
- package/dist/extensions/pi-subagents/src/shared/types.ts +145 -89
- package/dist/extensions/pi-subagents/src/shared/utils.ts +13 -6
- package/dist/extensions/pi-subagents/src/shared/watch-strategy.ts +2 -0
- package/dist/extensions/pi-subagents/src/shared/workflow-child-permit.ts +18 -13
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +0 -6
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +1 -1
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +11 -6
- package/dist/extensions/pi-subagents/src/tui/render.ts +277 -46
- package/dist/extensions/pi-subagents/src/watchdog/child-status.ts +0 -1
- package/dist/extensions/pi-subagents/src/watchdog/register-child.ts +12 -12
- package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +3 -3
- package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +116 -15
- package/dist/extensions/pi-subagents/src/workflows/workflow-checklist.ts +15 -18
- package/dist/extensions/pi-subagents/src/workflows/workflow-child-summary.ts +57 -8
- package/dist/extensions/pi-subagents/src/workflows/workflow-preflight.ts +19 -19
- package/dist/extensions/pi-subagents/src/workflows/workflow-receipt.ts +3 -3
- package/dist/extensions/pi-subagents/src/workflows/workflow-resources.ts +97 -22
- package/dist/extensions/pi-subagents/src/workflows/workflow-settlement.ts +3 -0
- package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +336 -21
- package/dist/extensions/pi-subagents/test/integration/async-execution.part-1.test.ts +1181 -0
- package/dist/extensions/pi-subagents/test/integration/async-execution.part-2.test.ts +2240 -0
- package/dist/extensions/pi-subagents/test/integration/async-execution.part-3.test.ts +2268 -0
- package/dist/extensions/pi-subagents/test/integration/async-execution.part-4.test.ts +1512 -0
- package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +231 -16
- package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +4 -18
- package/dist/extensions/pi-subagents/test/integration/foreign-workflow-steering.test.ts +95 -0
- package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +9 -15
- package/dist/extensions/pi-subagents/test/integration/in-process-child.test.ts +374 -0
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +20 -10
- package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +18 -6
- package/dist/extensions/pi-subagents/test/integration/recovered-assistant-error.test.ts +151 -0
- package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +42 -1
- package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +150 -25
- package/dist/extensions/pi-subagents/test/integration/result-publication.test.ts +354 -0
- package/dist/extensions/pi-subagents/test/integration/shared-cwd-terminal-evidence.test.ts +117 -0
- package/dist/extensions/pi-subagents/test/integration/single-execution.part-1.test.ts +3678 -0
- package/dist/extensions/pi-subagents/test/integration/{single-execution.test.ts → single-execution.part-2.test.ts} +500 -4207
- package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +1 -2
- package/dist/extensions/pi-subagents/test/integration/workflow-result-publication.test.ts +127 -0
- package/dist/extensions/pi-subagents/test/integration/workflow-steer-inbox.test.ts +365 -0
- package/dist/extensions/pi-subagents/test/smoke/pi085-child.ts +31 -0
- package/dist/extensions/pi-subagents/test/smoke/pi085-clean-install.mjs +74 -0
- package/dist/extensions/pi-subagents/test/smoke/pi085-extension.ts +16 -0
- package/dist/extensions/pi-subagents/test/support/async-execution-fixture.ts +699 -0
- package/dist/extensions/pi-subagents/test/support/fake-child-session.ts +418 -0
- package/dist/extensions/pi-subagents/test/support/foreign-workflow-steer-host.mjs +13 -0
- package/dist/extensions/pi-subagents/test/support/mock-pi.ts +30 -76
- package/dist/extensions/pi-subagents/test/support/notify-diagnostics-fixture.ts +84 -0
- package/dist/extensions/pi-subagents/test/support/result-publication-capacity-preload.mjs +70 -0
- package/dist/extensions/pi-subagents/test/support/runner-child-session-factory.ts +17 -0
- package/dist/extensions/pi-subagents/test/support/single-execution-fixture.ts +390 -0
- package/dist/extensions/pi-subagents/test/unit/acceptance-compaction.test.ts +120 -0
- package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +24 -3
- package/dist/extensions/pi-subagents/test/unit/advertised-agent-prompt.test.ts +87 -0
- package/dist/extensions/pi-subagents/test/unit/advertised-agent-refresh.test.ts +163 -0
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +70 -31
- package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +78 -3
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +2 -8
- package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +73 -66
- package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +51 -1
- package/dist/extensions/pi-subagents/test/unit/async-retention.test.ts +49 -1
- package/dist/extensions/pi-subagents/test/unit/async-spawn-preload.test.ts +103 -0
- package/dist/extensions/pi-subagents/test/unit/async-status-projection.test.ts +3 -3
- package/dist/extensions/pi-subagents/test/unit/capability-ceiling-agent-allowlist.test.ts +9 -8
- package/dist/extensions/pi-subagents/test/unit/capability-ceiling.test.ts +10 -7
- package/dist/extensions/pi-subagents/test/unit/chain-root-attachment.test.ts +48 -6
- package/dist/extensions/pi-subagents/test/unit/child-runtime-config.test.ts +90 -0
- package/dist/extensions/pi-subagents/test/unit/{pi-args-permission-system.test.ts → child-tool-plan-permission-system.test.ts} +48 -86
- package/dist/extensions/pi-subagents/test/unit/child-tool-plan.test.ts +33 -0
- package/dist/extensions/pi-subagents/test/unit/compaction-resume.test.ts +18 -18
- package/dist/extensions/pi-subagents/test/unit/completion-evidence.test.ts +9 -9
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +8 -7
- package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +225 -65
- package/dist/extensions/pi-subagents/test/unit/extension-bindings.test.ts +17 -22
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +11 -1
- package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +47 -3
- package/dist/extensions/pi-subagents/test/unit/foreign-workflow-steering.test.ts +74 -0
- package/dist/extensions/pi-subagents/test/unit/fork-cache-key.test.ts +23 -29
- package/dist/extensions/pi-subagents/test/unit/host-peer-runtime-imports.test.ts +96 -4
- package/dist/extensions/pi-subagents/test/unit/host-step-status.test.ts +4 -4
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +116 -55
- package/dist/extensions/pi-subagents/test/unit/index-segment.test.ts +58 -1
- package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +104 -3
- package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +10 -1
- package/dist/extensions/pi-subagents/test/unit/mission-store.test.ts +31 -1
- package/dist/extensions/pi-subagents/test/unit/model-exclusions.test.ts +102 -51
- package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +196 -6
- package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +181 -38
- package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +62 -70
- package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +58 -53
- package/dist/extensions/pi-subagents/test/unit/notify-diagnostics.test.ts +42 -0
- package/dist/extensions/pi-subagents/test/unit/notify.test.ts +84 -0
- package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +13 -5
- package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +21 -5
- package/dist/extensions/pi-subagents/test/unit/parallel-handoff.test.ts +5 -4
- package/dist/extensions/pi-subagents/test/unit/permissions.test.ts +2 -6
- package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +28 -2
- package/dist/extensions/pi-subagents/test/unit/pi-web-session-liveness.test.ts +209 -0
- package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +38 -1
- package/dist/extensions/pi-subagents/test/unit/prompt-audit-optional-peers.test.ts +43 -0
- package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +24 -2
- package/dist/extensions/pi-subagents/test/unit/readonly-drain-observation.test.ts +174 -0
- package/dist/extensions/pi-subagents/test/unit/readonly-model-continuation.test.ts +94 -0
- package/dist/extensions/pi-subagents/test/unit/readonly-session-evidence.test.ts +1329 -0
- package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +29 -80
- package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +11 -8
- package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/retained-nested-route-tracker.test.ts +175 -0
- package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +38 -12
- package/dist/extensions/pi-subagents/test/unit/run-fanout-budget.test.ts +2 -5
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +89 -1
- package/dist/extensions/pi-subagents/test/unit/runtime-acknowledged-extensions.test.ts +2 -16
- package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +27 -3
- package/dist/extensions/pi-subagents/test/unit/scheduled-supervisor-demand.test.ts +215 -0
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +15 -11
- package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +237 -1
- package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +2 -5
- package/dist/extensions/pi-subagents/test/unit/streamed-progress-bounds.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/subagent-guide.test.ts +14 -1
- package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +130 -739
- package/dist/extensions/pi-subagents/test/unit/supervisor-ask-registration.test.ts +682 -0
- package/dist/extensions/pi-subagents/test/unit/supervisor-ui.test.ts +137 -0
- package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +1 -11
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +23 -3
- package/dist/extensions/pi-subagents/test/unit/wait-completions.test.ts +8 -1
- package/dist/extensions/pi-subagents/test/unit/watchdog-tool-actions.test.ts +19 -1
- package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +72 -4
- package/dist/extensions/pi-subagents/test/unit/workflow-checklist.test.ts +17 -3
- package/dist/extensions/pi-subagents/test/unit/workflow-child-summary.test.ts +34 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-detach-reconcile.test.ts +12 -2
- package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +46 -6
- package/dist/extensions/pi-subagents/test/unit/workflow-receipt.test.ts +3 -3
- package/dist/extensions/pi-subagents/test/unit/workflow-resources.test.ts +133 -1
- package/dist/extensions/pi-subagents/test/unit/workflow-resume-hint.test.ts +53 -2
- package/dist/extensions/pi-subagents/test/unit/worktree-cleanup-plan.test.ts +222 -14
- package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +526 -85
- package/dist/extensions/pi-web-agent/src/backends/config.ts +1 -0
- package/dist/extensions/pi-web-agent/src/backends/settings-reader.ts +5 -3
- package/dist/extensions/pi-web-agent/src/search/tokenin.ts +7 -3
- package/dist/extensions/pi-web-agent/src/types.ts +1 -1
- package/dist/extensions/pi-zentui/README.md +17 -0
- package/dist/extensions/pi-zentui/assets/screenshots/footer-status-hyperlink.png +0 -0
- package/dist/extensions/pi-zentui/docs/configuration.md +71 -2
- package/dist/extensions/pi-zentui/extensions/zentui/config.ts +11 -0
- package/dist/extensions/pi-zentui/extensions/zentui/editor-transfer.ts +14 -0
- package/dist/extensions/pi-zentui/extensions/zentui/extension-status.ts +29 -13
- package/dist/extensions/pi-zentui/extensions/zentui/footer-layout.ts +5 -4
- package/dist/extensions/pi-zentui/extensions/zentui/footer-text.ts +11 -0
- package/dist/extensions/pi-zentui/extensions/zentui/footer.ts +11 -10
- package/dist/extensions/pi-zentui/extensions/zentui/index.ts +77 -3
- package/dist/extensions/pi-zentui/extensions/zentui/presets.ts +77 -0
- package/dist/extensions/pi-zentui/extensions/zentui/settings-command.ts +103 -2
- package/dist/extensions/pi-zentui/extensions/zentui/working-line-extension-segments.ts +115 -0
- package/dist/extensions/pi-zentui/extensions/zentui/working-line.ts +87 -5
- package/dist/extensions/pi-zentui/package-lock.json +277 -517
- package/dist/extensions/pi-zentui/package.json +7 -4
- package/dist/extensions/pi-zentui/test/config-presets.test.ts +238 -0
- package/dist/extensions/pi-zentui/test/editor-transfer.test.ts +24 -1
- package/dist/extensions/pi-zentui/test/extension-compliance.test.ts +570 -2
- package/dist/extensions/pi-zentui/test/extension-status.test.ts +90 -0
- package/dist/extensions/pi-zentui/test/footer-hyperlink.test.ts +243 -0
- package/dist/extensions/pi-zentui/test/package-contents.mjs +1 -0
- package/dist/extensions/pi-zentui/test/presets.test.ts +121 -0
- package/dist/extensions/pi-zentui/test/settings-command.test.ts +205 -1
- package/dist/extensions/pi-zentui/test/working-line-extension-segments.test.ts +139 -0
- package/dist/extensions/pi-zentui/test/working-line-lifecycle.integration.test.ts +237 -3
- package/dist/extensions/pi-zentui/test/working-line.test.ts +94 -2
- package/dist/index.d.ts +1 -1
- package/dist/main.js +4 -1
- package/dist/modes/interactive/chat-viewport.d.ts +19 -0
- package/dist/modes/interactive/chat-viewport.js +27 -0
- package/dist/modes/interactive/components/assistant-message.d.ts +6 -2
- package/dist/modes/interactive/components/assistant-message.js +30 -13
- package/dist/modes/interactive/components/custom-editor.d.ts +10 -1
- package/dist/modes/interactive/components/custom-editor.js +39 -1
- package/dist/modes/interactive/components/first-time-setup.js +1 -1
- package/dist/modes/interactive/components/index.d.ts +1 -1
- package/dist/modes/interactive/components/markdown-transform.d.ts +2 -0
- package/dist/modes/interactive/components/markdown-transform.js +18 -0
- package/dist/modes/interactive/components/model-selector.js +19 -17
- package/dist/modes/interactive/components/scoped-models-selector.js +21 -23
- package/dist/modes/interactive/components/settings-selector.d.ts +7 -1
- package/dist/modes/interactive/components/settings-selector.js +128 -20
- package/dist/modes/interactive/components/settings-submenu.d.ts +71 -0
- package/dist/modes/interactive/components/settings-submenu.js +164 -0
- package/dist/modes/interactive/components/status-indicator.d.ts +3 -1
- package/dist/modes/interactive/components/status-indicator.js +10 -3
- package/dist/modes/interactive/components/thinking-selector.d.ts +15 -3
- package/dist/modes/interactive/components/thinking-selector.js +75 -18
- package/dist/modes/interactive/components/tool-execution.d.ts +21 -4
- package/dist/modes/interactive/components/tool-execution.js +35 -33
- package/dist/modes/interactive/components/trust-selector.js +2 -2
- package/dist/modes/interactive/interactive-mode.d.ts +13 -15
- package/dist/modes/interactive/interactive-mode.js +144 -80
- package/dist/modes/interactive/session-share.d.ts +15 -0
- package/dist/modes/interactive/session-share.js +183 -0
- package/dist/modes/interactive/theme/dark.json +2 -1
- package/dist/modes/interactive/theme/light.json +2 -1
- package/dist/modes/interactive/theme/theme-controller.d.ts +1 -0
- package/dist/modes/interactive/theme/theme-controller.js +5 -0
- package/dist/modes/interactive/theme/theme-json.d.ts +83 -0
- package/dist/modes/interactive/theme/theme-json.js +129 -0
- package/dist/modes/interactive/theme/theme-schema.json +9 -1
- package/dist/modes/interactive/theme/theme.d.ts +14 -5
- package/dist/modes/interactive/theme/theme.js +18 -119
- package/dist/modes/interactive/tui-renderer.d.ts +20 -0
- package/dist/modes/interactive/tui-renderer.js +65 -0
- package/dist/utils/exif-orientation.js +2 -3
- package/dist/utils/management-http.d.ts +24 -0
- package/dist/utils/management-http.js +53 -0
- package/dist/utils/text.d.ts +7 -0
- package/dist/utils/text.js +8 -0
- package/dist/utils/tools-manager.d.ts +1 -0
- package/dist/utils/tools-manager.js +43 -21
- package/docs/docs.json +0 -4
- package/package.json +4 -4
- package/dist/extensions/pi-subagents/src/runs/shared/child-protocol.ts +0 -415
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +0 -1059
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +0 -116
- package/dist/extensions/pi-subagents/src/shared/post-exit-stdio-guard.ts +0 -85
- package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +0 -224
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +0 -6986
- package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +0 -447
- package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +0 -185
- package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +0 -311
- package/dist/extensions/pi-subagents/test/unit/capability-ceiling-pi-args.test.ts +0 -84
- package/dist/extensions/pi-subagents/test/unit/child-protocol.test.ts +0 -202
- package/dist/extensions/pi-subagents/test/unit/close-grace-timer.test.ts +0 -101
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +0 -2347
- package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +0 -104
- package/dist/extensions/pi-subagents/test/unit/windows-hide-spawn.test.ts +0 -49
- package/dist/skills/imagegen-frontend-mobile/SKILL.md +0 -1465
- package/dist/skills/imagegen-frontend-web/SKILL.md +0 -987
- package/dist/skills/implanger/SKILL.md +0 -70
- package/dist/skills/output/SKILL.md +0 -49
- package/dist/skills/planger/SKILL.md +0 -167
- package/dist/skills/workflow/SKILL.md +0 -95
- package/docs/workflows.md +0 -18
- /package/dist/bun/{register-bedrock.d.ts → runtime-setup.d.ts} +0 -0
|
@@ -9,7 +9,6 @@ import { registerAgent } from "../../src/api/agents.ts";
|
|
|
9
9
|
import { clearRuntimeAgentsForPi } from "../../src/agents/runtime-agent-registry.ts";
|
|
10
10
|
import { scheduledRunStorePath } from "../../src/runs/background/scheduled-runs.ts";
|
|
11
11
|
import { updateActiveRunIndex } from "../../src/runs/background/active-run-index.ts";
|
|
12
|
-
import { SUBAGENT_FANOUT_CHILD_ENV } from "../../src/runs/shared/pi-args.ts";
|
|
13
12
|
import { getArtifactPaths, getArtifactsDir } from "../../src/shared/artifacts.ts";
|
|
14
13
|
import { ASYNC_DIR, DIRS } from "../../src/shared/types.ts";
|
|
15
14
|
import type { WatchdogReviewFunction } from "../../src/watchdog/runtime.ts";
|
|
@@ -1514,4 +1513,4 @@ describe("subagents-inspect-rpc command", { skip: !available ? "slash-commands.t
|
|
|
1514
1513
|
assert.equal(widgetCalls.length, 0);
|
|
1515
1514
|
assert.match(notifications[0] ?? "", /RPC surfaces/);
|
|
1516
1515
|
});
|
|
1517
|
-
});
|
|
1516
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { syncBuiltinESMExports } from "node:module";
|
|
5
|
+
import { describe, it } from "node:test";
|
|
6
|
+
import { createResultWatcher } from "../../src/runs/background/result-watcher.ts";
|
|
7
|
+
import registerSubagentNotify from "../../src/runs/background/notify.ts";
|
|
8
|
+
import { SUBAGENT_ASYNC_COMPLETE_EVENT, type SubagentState } from "../../src/shared/types.ts";
|
|
9
|
+
import { makeMinimalCtx } from "../support/helpers.ts";
|
|
10
|
+
import { installAsyncExecutionHooks, available, createSubagentExecutor, ASYNC_DIR, RESULTS_DIR, tempDir } from "../support/async-execution-fixture.ts";
|
|
11
|
+
|
|
12
|
+
function barrier() {
|
|
13
|
+
let resolve!: () => void;
|
|
14
|
+
const promise = new Promise<void>((done) => { resolve = done; });
|
|
15
|
+
return { promise, resolve };
|
|
16
|
+
}
|
|
17
|
+
async function bounded(promise: Promise<unknown>) {
|
|
18
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
19
|
+
try { await Promise.race([promise, new Promise((_, reject) => { timer = setTimeout(() => reject(new Error("publication barrier timed out")), 12000); })]); }
|
|
20
|
+
finally { clearTimeout(timer!); }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("host workflow result publication", { skip: !available }, () => {
|
|
24
|
+
installAsyncExecutionHooks();
|
|
25
|
+
for (const outcome of ["complete", "failed", "stopped", "retry-error"] as const) {
|
|
26
|
+
it(`retains real Darwin demand until deferred indexed workflow publication settles (${outcome})`, async () => {
|
|
27
|
+
const polled = barrier(), published = barrier(), delivered = barrier(), retired = barrier(), failed = barrier(), emitted = barrier();
|
|
28
|
+
let held = true, attempted = false, notifications = 0, refreshes = 0;
|
|
29
|
+
let statusWrites = 0, statusDeferred = false, statusRecovered = false;
|
|
30
|
+
let poll: ReturnType<typeof setInterval> | undefined;
|
|
31
|
+
const sessionId = "workflow-publication-session", owner = "workflow-publication-owner";
|
|
32
|
+
const state: SubagentState = { baseCwd: tempDir, currentSessionId: sessionId, completionOwnerId: owner,
|
|
33
|
+
asyncJobs: new Map(), foregroundControls: new Map(), lastForegroundControlId: null,
|
|
34
|
+
cleanupTimers: new Map(), lastUiContext: null, poller: null, completionSeen: new Map(),
|
|
35
|
+
watcher: null, watcherRestartTimer: null, resultFileCoalescer: { schedule: () => false, clear() {} },
|
|
36
|
+
};
|
|
37
|
+
const pi = { getSessionName: () => undefined, events: { on: () => () => {}, emit(event: string, data: unknown) {
|
|
38
|
+
if (event === SUBAGENT_ASYNC_COMPLETE_EVENT) { assert.equal((data as { completionOwnerId: string }).completionOwnerId, owner); delivered.resolve(); }
|
|
39
|
+
} }, sendMessage() { notifications++; } };
|
|
40
|
+
const notifier = registerSubagentNotify(pi, state, { batchConfig: { enabled: false } });
|
|
41
|
+
const watcher = createResultWatcher(pi, state, RESULTS_DIR, 60000, { platform: "darwin", coalesceDelayMs: 0, notifier,
|
|
42
|
+
hasDeliveryDemand: () => [...state.asyncJobs.values()].some((job) => job.status === "running" || job.status === "queued"),
|
|
43
|
+
timers: { setTimeout, clearTimeout, setInterval: ((handler: () => void, delay: number) => {
|
|
44
|
+
assert.equal(delay, 3000);
|
|
45
|
+
poll = setInterval(() => { handler(); if (attempted) polled.resolve(); }, delay); return poll;
|
|
46
|
+
}) as typeof setInterval, clearInterval: ((handle: ReturnType<typeof setInterval>) => { clearInterval(handle); poll = undefined; retired.resolve(); }) as typeof clearInterval },
|
|
47
|
+
});
|
|
48
|
+
const originalWrite = fs.writeFileSync, originalRename = fs.renameSync, originalAppend = fs.appendFileSync;
|
|
49
|
+
fs.writeFileSync = ((file, ...args) => {
|
|
50
|
+
if (String(file).includes(`${path.sep}result-pending${path.sep}`)) {
|
|
51
|
+
attempted = true;
|
|
52
|
+
if (held) throw Object.assign(new Error("injected workflow capacity failure"), { code: "ENOSPC" });
|
|
53
|
+
if (outcome === "retry-error") throw Object.assign(new Error("injected workflow retry EIO"), { code: "EIO" });
|
|
54
|
+
}
|
|
55
|
+
return originalWrite(file, ...args);
|
|
56
|
+
}) as typeof fs.writeFileSync;
|
|
57
|
+
fs.renameSync = ((from, to) => {
|
|
58
|
+
if (path.basename(String(to)) === "status.json") {
|
|
59
|
+
if (++statusWrites > 1 && !attempted) { statusDeferred = true; throw Object.assign(new Error("injected running status capacity failure"), { code: "ENOSPC" }); }
|
|
60
|
+
if (statusDeferred && held && attempted) statusRecovered = true;
|
|
61
|
+
}
|
|
62
|
+
originalRename(from, to); if (path.dirname(String(to)) === RESULTS_DIR) published.resolve();
|
|
63
|
+
}) as typeof fs.renameSync;
|
|
64
|
+
fs.appendFileSync = ((file, data, ...args) => {
|
|
65
|
+
originalAppend(file, data, ...args);
|
|
66
|
+
if (String(data).includes('"type":"subagent.workflow.result_write_failed"')) failed.resolve();
|
|
67
|
+
if (String(data).includes('"type":"subagent.workflow.emit"')) emitted.resolve();
|
|
68
|
+
}) as typeof fs.appendFileSync;
|
|
69
|
+
syncBuiltinESMExports();
|
|
70
|
+
try {
|
|
71
|
+
const executor = createSubagentExecutor!({ pi, state, config: {}, asyncByDefault: false, tempArtifactsDir: tempDir,
|
|
72
|
+
getSubagentSessionRoot: () => tempDir, expandTilde: (p: string) => p, discoverAgents: () => ({ agents: [] }),
|
|
73
|
+
refreshResultDelivery: () => { refreshes++; watcher.refreshResultDelivery(); },
|
|
74
|
+
});
|
|
75
|
+
const ctx = makeMinimalCtx(tempDir); ctx.sessionManager.getSessionId = () => sessionId;
|
|
76
|
+
const script = outcome === "stopped" ? "await new Promise(() => {})" : outcome === "failed" ? "throw new Error('workflow script failed')" : "return 'done'";
|
|
77
|
+
const launch = await executor.execute("workflow-publication", { workflowScript: `emit('progress'); ${script}`, async: true }, new AbortController().signal, undefined, ctx);
|
|
78
|
+
assert.notEqual(launch.isError, true);
|
|
79
|
+
const id = launch.details.asyncId!;
|
|
80
|
+
watcher.startResultWatcher();
|
|
81
|
+
if (outcome === "stopped") {
|
|
82
|
+
await bounded(emitted.promise);
|
|
83
|
+
const stop = await executor.execute("workflow-stop", { action: "stop", id }, new AbortController().signal, undefined, ctx);
|
|
84
|
+
assert.notEqual(stop.isError, true);
|
|
85
|
+
assert.match(stop.content[0]?.text ?? "", /Stop requested/);
|
|
86
|
+
assert.equal(state.workflowControllers?.get(id)?.signal.aborted, true);
|
|
87
|
+
}
|
|
88
|
+
await bounded(polled.promise);
|
|
89
|
+
const status = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, id, "status.json"), "utf8"));
|
|
90
|
+
assert.equal(status.state, "running", "terminal status must not precede indexed workflow result publication");
|
|
91
|
+
assert.equal(state.asyncJobs.get(id)?.status, "running");
|
|
92
|
+
assert.equal(statusDeferred, true); assert.equal(statusRecovered, true);
|
|
93
|
+
assert.equal(fs.existsSync(path.join(ASYNC_DIR, ".active-runs", id)), true);
|
|
94
|
+
assert.ok(poll); assert.equal(notifications, 0); assert.equal(refreshes, 0);
|
|
95
|
+
held = false;
|
|
96
|
+
if (outcome === "retry-error") {
|
|
97
|
+
await bounded(failed.promise);
|
|
98
|
+
// Allow the awaited failure callback to finish its existing cleanup.
|
|
99
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
100
|
+
assert.equal(state.workflowControllers?.has(id), false);
|
|
101
|
+
assert.equal(state.asyncJobs.get(id)?.status, "running");
|
|
102
|
+
assert.equal(notifications, 0); assert.equal(refreshes, 0);
|
|
103
|
+
assert.match(fs.readFileSync(path.join(ASYNC_DIR, id, "events.jsonl"), "utf8"), /injected workflow retry EIO/);
|
|
104
|
+
assert.equal(fs.existsSync(path.join(RESULTS_DIR, `${id}.json`)), false);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
await bounded(Promise.all([published.promise, delivered.promise]));
|
|
108
|
+
assert.equal(state.asyncJobs.get(id)?.status, outcome);
|
|
109
|
+
const terminalStatus = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, id, "status.json"), "utf8"));
|
|
110
|
+
assert.equal(terminalStatus.state, outcome);
|
|
111
|
+
if (outcome === "stopped") assert.equal(terminalStatus.stopped, true);
|
|
112
|
+
assert.equal(notifications, 1); assert.equal(refreshes, 1);
|
|
113
|
+
assert.equal(fs.existsSync(path.join(RESULTS_DIR, `${id}.json`)), false);
|
|
114
|
+
// The existing demand timer retires itself after terminal publication.
|
|
115
|
+
await bounded(retired.promise);
|
|
116
|
+
assert.equal(poll, undefined);
|
|
117
|
+
} finally {
|
|
118
|
+
held = false;
|
|
119
|
+
try { await bounded(outcome === "retry-error" ? failed.promise : published.promise); }
|
|
120
|
+
finally {
|
|
121
|
+
watcher.stopResultWatcher(); notifier.dispose();
|
|
122
|
+
fs.writeFileSync = originalWrite; fs.renameSync = originalRename; fs.appendFileSync = originalAppend; syncBuiltinESMExports();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
});
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for the durable steer inbox of async WORKFLOW runs.
|
|
3
|
+
*
|
|
4
|
+
* `watchAsyncControlInbox` is installed only by subagent-runner.ts, and the runner never executes
|
|
5
|
+
* workflow scripts, so before this fix no process consumed `control/steer-requests/` for a
|
|
6
|
+
* workflow run: a queued request file sat on disk until the run ended and the requester's
|
|
7
|
+
* "delivered" receipt was never backed by a delivery.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it } from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import { events, makeAgent, makeMinimalCtx } from "../support/helpers.ts";
|
|
15
|
+
import { requestAsyncSteer, steerInboxClosedPath, steerRequestsDir } from "../../src/runs/background/control-channel.ts";
|
|
16
|
+
import type { AsyncStatusPayload } from "../support/async-execution-fixture.ts";
|
|
17
|
+
import type { SteeringStatus } from "../../src/shared/types.ts";
|
|
18
|
+
import {
|
|
19
|
+
installAsyncExecutionHooks, available, isAsyncAvailable, ASYNC_DIR,
|
|
20
|
+
createSubagentExecutor, waitForMockPiCall, waitForAsyncState, tempDir, mockPi,
|
|
21
|
+
makeAsyncExecutor, readAsyncPayload, RESULTS_DIR, waitForAsyncEvent,
|
|
22
|
+
} from "../support/async-execution-fixture.ts";
|
|
23
|
+
|
|
24
|
+
type SteeringTargets = { steering?: { recent: Array<{ id: string; targets: Array<{ index: number; state: string; reason?: string }> }> } };
|
|
25
|
+
|
|
26
|
+
function recentSteering(status: AsyncStatusPayload, requestId: string): { index: number; state: string; reason?: string } | undefined {
|
|
27
|
+
return (status as SteeringTargets).steering?.recent?.find((request) => request.id === requestId)?.targets[0];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("async workflow steer inbox", { skip: !available ? "pi packages not available" : undefined }, () => {
|
|
31
|
+
installAsyncExecutionHooks();
|
|
32
|
+
|
|
33
|
+
for (const outcome of ["complete", "failed", "stopped"]) it(`settles steering without terminal status when result index publication fails (${outcome})`, { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
|
|
34
|
+
const release = path.join(tempDir, "index-failure-release");
|
|
35
|
+
const indexPath = path.join(RESULTS_DIR, "result-index");
|
|
36
|
+
mockPi.onCall({ steps: [{ waitForPath: release, jsonl: [events.assistantMessage("done")] }] });
|
|
37
|
+
const ctx = makeMinimalCtx(tempDir);
|
|
38
|
+
ctx.sessionManager.getSessionId = () => "session-index-steering";
|
|
39
|
+
const executor = makeAsyncExecutor([makeAgent("worker", { completionGuard: false })]);
|
|
40
|
+
const launch = await executor.execute("index-steering", {
|
|
41
|
+
workflowScript: `await runs.run("A", { agent: "worker", task: "Wait" }); ${outcome === "failed" ? 'throw new Error("script failed")' : 'return "done"'}`,
|
|
42
|
+
async: true,
|
|
43
|
+
}, new AbortController().signal, undefined, ctx);
|
|
44
|
+
assert.equal(launch.isError, undefined);
|
|
45
|
+
const runId = launch.details!.asyncId as string;
|
|
46
|
+
const asyncDir = path.join(ASYNC_DIR, runId);
|
|
47
|
+
await waitForMockPiCall(mockPi, 0, 10_000);
|
|
48
|
+
const session = mockPi.sessions[0]!.session!;
|
|
49
|
+
const originalSteer = session.steer;
|
|
50
|
+
let entered = false;
|
|
51
|
+
let releaseSteer!: () => void;
|
|
52
|
+
const blocked = new Promise<void>((resolve) => { releaseSteer = resolve; });
|
|
53
|
+
session.steer = async () => { entered = true; await blocked; };
|
|
54
|
+
try {
|
|
55
|
+
requestAsyncSteer(asyncDir, { id: "queued", message: "later", mode: "follow_up" });
|
|
56
|
+
requestAsyncSteer(asyncDir, { id: "unresolved", message: "pending" });
|
|
57
|
+
await waitForAsyncState(runId, (candidate) => entered && recentSteering(candidate, "queued")?.state === "queued" && recentSteering(candidate, "unresolved")?.state === "routed", 15_000);
|
|
58
|
+
fs.rmSync(indexPath, { recursive: true, force: true });
|
|
59
|
+
fs.mkdirSync(RESULTS_DIR, { recursive: true });
|
|
60
|
+
fs.writeFileSync(indexPath, "not a directory");
|
|
61
|
+
if (outcome === "stopped") {
|
|
62
|
+
const stop = await executor.execute("stop-index-steering", { action: "stop", id: runId }, new AbortController().signal, undefined, ctx);
|
|
63
|
+
assert.notEqual(stop.isError, true);
|
|
64
|
+
} else fs.writeFileSync(release, "go");
|
|
65
|
+
await waitForAsyncEvent(runId, "subagent.workflow.result_write_failed");
|
|
66
|
+
const before = fs.readFileSync(path.join(asyncDir, "status.json"), "utf8");
|
|
67
|
+
const status = JSON.parse(before) as { state: string; endedAt?: number; activityState?: string; steering: SteeringStatus };
|
|
68
|
+
assert.equal(status.state, "running", "failed index publication cannot authorize terminal status");
|
|
69
|
+
assert.equal(status.endedAt, undefined);
|
|
70
|
+
assert.equal(status.activityState, "needs_attention");
|
|
71
|
+
assert.equal(status.steering.pending, 0);
|
|
72
|
+
assert.equal(status.steering.failed, 2);
|
|
73
|
+
assert.equal(status.steering.delivered, 0);
|
|
74
|
+
assert.ok(status.steering.recent.every((request) => request.targets[0]?.state === "failed" && request.targets[0].reason === `run became ${outcome} before steering delivery settled; delivery unconfirmed`));
|
|
75
|
+
assert.equal(fs.existsSync(path.join(ASYNC_DIR, ".active-runs", runId)), true);
|
|
76
|
+
assert.equal(fs.existsSync(path.join(RESULTS_DIR, `${runId}.json`)), false);
|
|
77
|
+
assert.equal(fs.existsSync(path.join(RESULTS_DIR, "result-pending", "session-index-steering", `${runId}.json`)), true);
|
|
78
|
+
const unpublished = JSON.parse(fs.readFileSync(path.join(RESULTS_DIR, "result-pending", "session-index-steering", `${runId}.json`), "utf8"));
|
|
79
|
+
assert.equal(unpublished.state, outcome, "computed outcome is retained only in unpublished result");
|
|
80
|
+
if (outcome !== "complete") assert.equal(unpublished.activityState, "needs_attention", "failure finalization preserves steering attention");
|
|
81
|
+
assert.equal(fs.existsSync(steerInboxClosedPath(asyncDir)), true);
|
|
82
|
+
const journal = fs.readFileSync(path.join(asyncDir, "events.jsonl"), "utf8");
|
|
83
|
+
assert.equal(journal.split("\n").filter((line) => line.includes('"type":"subagent.steer.failed"')).length, 2);
|
|
84
|
+
assert.doesNotMatch(journal, /"type":"subagent.workflow.completed"/);
|
|
85
|
+
releaseSteer();
|
|
86
|
+
await new Promise((resolve) => setTimeout(resolve, 350));
|
|
87
|
+
assert.equal(fs.readFileSync(path.join(asyncDir, "status.json"), "utf8"), before);
|
|
88
|
+
assert.equal(fs.readFileSync(path.join(asyncDir, "events.jsonl"), "utf8"), journal);
|
|
89
|
+
} finally {
|
|
90
|
+
session.steer = originalSteer;
|
|
91
|
+
releaseSteer();
|
|
92
|
+
fs.writeFileSync(release, "go");
|
|
93
|
+
fs.rmSync(indexPath, { recursive: true, force: true });
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
for (const shutdown of [false, true]) it(`accounts for 21 same-scan requests beyond display history (${shutdown ? "shutdown" : "delivered"})`, { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
|
|
98
|
+
const release = path.join(tempDir, "batch-accounting-release");
|
|
99
|
+
mockPi.onCall({ steps: [{ waitForPath: release, jsonl: [events.assistantMessage("done")] }] });
|
|
100
|
+
const ctx = makeMinimalCtx(tempDir);
|
|
101
|
+
ctx.sessionManager.getSessionId = () => "session-batch-accounting";
|
|
102
|
+
const executor = makeAsyncExecutor([makeAgent("worker", { completionGuard: false })]);
|
|
103
|
+
const launch = await executor.execute("batch-accounting", { workflowScript: `return await runs.run("A", { agent: "worker", task: "Wait" });`, async: true }, new AbortController().signal, undefined, ctx);
|
|
104
|
+
assert.equal(launch.isError, undefined);
|
|
105
|
+
const runId = launch.details!.asyncId as string;
|
|
106
|
+
const asyncDir = path.join(ASYNC_DIR, runId);
|
|
107
|
+
await waitForMockPiCall(mockPi, 0, 10_000);
|
|
108
|
+
const session = mockPi.sessions[0]!.session!;
|
|
109
|
+
const originalSteer = session.steer.bind(session);
|
|
110
|
+
const originalFollowUp = session.followUp.bind(session);
|
|
111
|
+
let releaseSteers!: () => void;
|
|
112
|
+
const blocked = new Promise<void>((resolve) => { releaseSteers = resolve; });
|
|
113
|
+
let calls = 0;
|
|
114
|
+
session.followUp = async (text) => {
|
|
115
|
+
calls++;
|
|
116
|
+
assert.deepEqual(fs.readdirSync(steerRequestsDir(asyncDir)), []);
|
|
117
|
+
await originalFollowUp(text);
|
|
118
|
+
};
|
|
119
|
+
session.steer = async (text) => {
|
|
120
|
+
calls++;
|
|
121
|
+
assert.deepEqual(fs.readdirSync(steerRequestsDir(asyncDir)), [], "entire batch consumed before first delivery callback");
|
|
122
|
+
if (shutdown) await blocked;
|
|
123
|
+
else await originalSteer(text);
|
|
124
|
+
};
|
|
125
|
+
const accounting = (candidate: AsyncStatusPayload) => (candidate as { steering?: SteeringStatus }).steering;
|
|
126
|
+
try {
|
|
127
|
+
for (let index = 0; index < 21; index++) requestAsyncSteer(asyncDir, { id: `batch-${index}`, message: `batch-${index}`, targetIndex: 0, ts: index + 1, ...(shutdown && (index === 0 || index === 20) ? { mode: "follow_up" as const } : {}) });
|
|
128
|
+
const active = await waitForAsyncState(runId, (candidate) => calls === 21 && accounting(candidate)?.recent.every((request) => request.targets[0]?.state === (shutdown ? request.id === "batch-20" ? "queued" : "routed" : "delivered")) === true, 15_000);
|
|
129
|
+
assert.equal(accounting(active)!.requested, 21);
|
|
130
|
+
assert.equal(accounting(active)!.pending, shutdown ? 21 : 0, "queued acknowledgments still count once as pending");
|
|
131
|
+
if (shutdown) {
|
|
132
|
+
const queuedEvents = fs.readFileSync(path.join(asyncDir, "events.jsonl"), "utf8").trim().split("\n").map((line) => JSON.parse(line)).filter((event) => event.type === "subagent.steer.queued");
|
|
133
|
+
assert.deepEqual(queuedEvents.map((event) => event.requestId), ["batch-0", "batch-20"], "both evicted and retained queued receipts remain pending");
|
|
134
|
+
}
|
|
135
|
+
assert.equal(accounting(active)!.recent.length, 20);
|
|
136
|
+
assert.equal(accounting(active)!.recent.some((request) => request.id === "batch-0"), false, "first unresolved receipt was evicted");
|
|
137
|
+
fs.writeFileSync(release, "go");
|
|
138
|
+
const terminal = await waitForAsyncState(runId, (candidate) => candidate.state === "complete", 15_000);
|
|
139
|
+
const counters = accounting(terminal)!;
|
|
140
|
+
assert.equal(counters.pending, 0);
|
|
141
|
+
assert.equal(counters.delivered, shutdown ? 0 : 21);
|
|
142
|
+
assert.equal(counters.failed, shutdown ? 21 : 0);
|
|
143
|
+
assert.equal(counters.recent.length, 20, "history cap must not grow");
|
|
144
|
+
if (shutdown) assert.ok(counters.recent.every((request) => request.targets[0]?.state === "failed" && request.targets[0].reason?.includes("delivery unconfirmed")));
|
|
145
|
+
const before = fs.readFileSync(path.join(asyncDir, "status.json"), "utf8");
|
|
146
|
+
const journal = fs.readFileSync(path.join(asyncDir, "events.jsonl"), "utf8");
|
|
147
|
+
const terminalEvents = journal.trim().split("\n").map((line) => JSON.parse(line)).filter((event) => event.requestId?.startsWith("batch-") && event.type === `subagent.steer.${shutdown ? "failed" : "delivered"}`);
|
|
148
|
+
assert.equal(terminalEvents.length, counters.delivered + counters.failed);
|
|
149
|
+
assert.equal(new Set(terminalEvents.map((event) => event.requestId)).size, 21, "every request has exactly one terminal event");
|
|
150
|
+
releaseSteers();
|
|
151
|
+
await new Promise((resolve) => setTimeout(resolve, 350));
|
|
152
|
+
assert.equal(fs.readFileSync(path.join(asyncDir, "status.json"), "utf8"), before);
|
|
153
|
+
assert.equal(fs.readFileSync(path.join(asyncDir, "events.jsonl"), "utf8"), journal);
|
|
154
|
+
} finally {
|
|
155
|
+
session.steer = originalSteer;
|
|
156
|
+
session.followUp = originalFollowUp;
|
|
157
|
+
releaseSteers();
|
|
158
|
+
fs.writeFileSync(release, "go");
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
for (const stop of [false, true]) it(`settles consumed pending deliveries before ${stop ? "stop" : "completion"} and ignores late callbacks`, { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
|
|
163
|
+
const release = path.join(tempDir, "pending-steer-release");
|
|
164
|
+
mockPi.onCall({ steps: [{ waitForPath: release, jsonl: [events.assistantMessage("done")] }] });
|
|
165
|
+
const ctx = makeMinimalCtx(tempDir);
|
|
166
|
+
ctx.sessionManager.getSessionId = () => "session-pending-steer";
|
|
167
|
+
const executor = makeAsyncExecutor([makeAgent("worker", { completionGuard: false })]);
|
|
168
|
+
const launch = await executor.execute("pending-steer", { workflowScript: `return await runs.run("A", { agent: "worker", task: "Wait" });`, async: true }, new AbortController().signal, undefined, ctx);
|
|
169
|
+
assert.equal(launch.isError, undefined);
|
|
170
|
+
const runId = launch.details!.asyncId as string;
|
|
171
|
+
const asyncDir = path.join(ASYNC_DIR, runId);
|
|
172
|
+
await waitForMockPiCall(mockPi, 0, 10_000);
|
|
173
|
+
const session = mockPi.sessions[0]!.session!;
|
|
174
|
+
const originalSteer = session.steer;
|
|
175
|
+
let settle!: () => void;
|
|
176
|
+
let entered = false;
|
|
177
|
+
session.steer = async () => {
|
|
178
|
+
entered = true;
|
|
179
|
+
await new Promise<void>((resolve, reject) => { settle = stop ? () => reject(new Error("late rejection")) : resolve; });
|
|
180
|
+
};
|
|
181
|
+
try {
|
|
182
|
+
requestAsyncSteer(asyncDir, { id: "pending", message: "pending", targetIndex: 0 });
|
|
183
|
+
await waitForAsyncState(runId, (candidate) => entered && recentSteering(candidate, "pending")?.state === "routed", 15_000);
|
|
184
|
+
assert.deepEqual(fs.readdirSync(steerRequestsDir(asyncDir)), [], "pending request is consumed, not available to file drain");
|
|
185
|
+
if (stop) {
|
|
186
|
+
requestAsyncSteer(asyncDir, { id: "file-resident", message: "not consumed", targetIndexes: [0, 99] });
|
|
187
|
+
const stopped = await executor.execute("stop-pending", { action: "stop", id: runId }, new AbortController().signal, undefined, ctx);
|
|
188
|
+
assert.notEqual(stopped.isError, true, stopped.content[0]?.text);
|
|
189
|
+
} else fs.writeFileSync(release, "go");
|
|
190
|
+
const terminal = await waitForAsyncState(runId, (candidate) => candidate.state === (stop ? "stopped" : "complete"), 15_000);
|
|
191
|
+
assert.equal(recentSteering(terminal, "pending")?.state, "failed");
|
|
192
|
+
assert.match(recentSteering(terminal, "pending")!.reason!, /before steering delivery settled/);
|
|
193
|
+
assert.equal(fs.existsSync(steerInboxClosedPath(asyncDir)), true);
|
|
194
|
+
if (stop) {
|
|
195
|
+
const targets = (terminal as SteeringTargets).steering!.recent.find((request) => request.id === "file-resident")!.targets;
|
|
196
|
+
assert.deepEqual(targets.map(({ index, state }) => [index, state]), [[0, "failed"], [99, "failed"]]);
|
|
197
|
+
assert.ok(targets.every(({ reason }) => reason?.includes("before steering request was consumed")));
|
|
198
|
+
}
|
|
199
|
+
const before = fs.readFileSync(path.join(asyncDir, "status.json"), "utf8");
|
|
200
|
+
const journalBefore = fs.readFileSync(path.join(asyncDir, "events.jsonl"), "utf8");
|
|
201
|
+
assert.equal(journalBefore.split("\n").filter((line) => line.includes('"type":"subagent.steer.failed"') && line.includes('"requestId":"pending"')).length, 1);
|
|
202
|
+
settle();
|
|
203
|
+
await new Promise((resolve) => setTimeout(resolve, 350));
|
|
204
|
+
assert.equal(fs.readFileSync(path.join(asyncDir, "status.json"), "utf8"), before, "late callback must not rewrite durable terminal state");
|
|
205
|
+
assert.equal(fs.readFileSync(path.join(asyncDir, "events.jsonl"), "utf8"), journalBefore, "late callback must not append contradictory evidence");
|
|
206
|
+
} finally {
|
|
207
|
+
session.steer = originalSteer;
|
|
208
|
+
settle?.();
|
|
209
|
+
fs.writeFileSync(release, "go");
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("delivers an inbox steer request to a live async workflow child", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
|
|
214
|
+
const release = path.join(tempDir, "workflow-steer-release");
|
|
215
|
+
mockPi.onCall({ steps: [{ waitForPath: release, jsonl: [events.assistantMessage("steered workflow child")] }] });
|
|
216
|
+
const ctx = makeMinimalCtx(tempDir);
|
|
217
|
+
ctx.sessionManager.getSessionId = () => "session-workflow-steer";
|
|
218
|
+
const executor = makeAsyncExecutor([makeAgent("worker", { completionGuard: false })]);
|
|
219
|
+
|
|
220
|
+
const launch = await executor.execute(
|
|
221
|
+
`workflow-steer-inbox-${Date.now().toString(36)}`,
|
|
222
|
+
{ workflowScript: `return await runs.run("waits", { agent: "worker", task: "Wait for guidance" });`, async: true },
|
|
223
|
+
new AbortController().signal,
|
|
224
|
+
undefined,
|
|
225
|
+
ctx,
|
|
226
|
+
);
|
|
227
|
+
assert.equal(launch.isError, undefined, launch.content[0]?.text);
|
|
228
|
+
const runId = launch.details?.asyncId as string;
|
|
229
|
+
assert.ok(runId, "expected an async workflow run id");
|
|
230
|
+
const asyncDir = path.join(ASYNC_DIR, runId);
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
// The child is now live and parked on the release path, so the workflow run is genuinely
|
|
234
|
+
// steerable: the only question is whether anything consumes its inbox.
|
|
235
|
+
await waitForMockPiCall(mockPi, 0, 10_000);
|
|
236
|
+
requestAsyncSteer(asyncDir, { message: "Focus on the failing test.", id: "workflow-steer-1", ts: Date.now() });
|
|
237
|
+
|
|
238
|
+
const status = await waitForAsyncState(runId, (candidate) => recentSteering(candidate, "workflow-steer-1") !== undefined, 15_000);
|
|
239
|
+
const target = recentSteering(status, "workflow-steer-1");
|
|
240
|
+
assert.equal(target?.state, "delivered", `expected a real delivery, got ${JSON.stringify(target)}`);
|
|
241
|
+
// The request file must be consumed, not left on disk for the life of the run.
|
|
242
|
+
assert.deepEqual(fs.readdirSync(steerRequestsDir(asyncDir)), []);
|
|
243
|
+
|
|
244
|
+
const steers = fs.readFileSync(path.join(mockPi.dir, "steers.jsonl"), "utf-8").trim().split("\n").map((line) => JSON.parse(line) as { text: string });
|
|
245
|
+
assert.match(steers[0]?.text ?? "", /Focus on the failing test\./, "the live child must receive the steer text");
|
|
246
|
+
|
|
247
|
+
const eventsText = fs.readFileSync(path.join(asyncDir, "events.jsonl"), "utf-8");
|
|
248
|
+
assert.match(eventsText, /"type":"subagent\.steer\.routed"[^\n]*"requestId":"workflow-steer-1"/);
|
|
249
|
+
assert.match(eventsText, /"type":"subagent\.steer\.delivered"[^\n]*"requestId":"workflow-steer-1"/);
|
|
250
|
+
requestAsyncSteer(asyncDir, { message: "Follow up later", id: "workflow-follow-up", mode: "follow_up" });
|
|
251
|
+
const queued = await waitForAsyncState(runId, (candidate) => recentSteering(candidate, "workflow-follow-up")?.state === "queued", 15_000);
|
|
252
|
+
const counters = (queued as { steering: SteeringStatus }).steering;
|
|
253
|
+
assert.equal(counters.requested, 2);
|
|
254
|
+
assert.equal(counters.delivered, 1);
|
|
255
|
+
assert.equal(counters.pending, 1, "routed to queued must not double-count the pending target");
|
|
256
|
+
} finally {
|
|
257
|
+
fs.writeFileSync(release, "go");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const payload = await readAsyncPayload(runId);
|
|
261
|
+
assert.equal(payload.success, true);
|
|
262
|
+
const terminal = await waitForAsyncState(runId, (candidate) => candidate.state === "complete", 15_000);
|
|
263
|
+
const counters = (terminal as { steering: SteeringStatus }).steering;
|
|
264
|
+
assert.equal(counters.pending, 0);
|
|
265
|
+
assert.equal(counters.delivered, 1);
|
|
266
|
+
assert.equal(counters.failed, 1);
|
|
267
|
+
assert.equal(recentSteering(terminal, "workflow-follow-up")?.state, "failed");
|
|
268
|
+
assert.match(recentSteering(terminal, "workflow-follow-up")!.reason!, /delivery unconfirmed/);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
for (const parallel of [false, true]) it(`routes every explicit target without sibling fallback (${parallel ? "parallel" : "sequential"})`, { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
|
|
272
|
+
const releaseA = path.join(tempDir, "target-release-a");
|
|
273
|
+
const releaseB = path.join(tempDir, "target-release-b");
|
|
274
|
+
mockPi.onCall({ steps: [{ waitForPath: releaseA, jsonl: [events.assistantMessage("A done")] }] });
|
|
275
|
+
mockPi.onCall({ steps: [{ waitForPath: releaseB, jsonl: [events.assistantMessage("B done")] }] });
|
|
276
|
+
const ctx = makeMinimalCtx(tempDir);
|
|
277
|
+
ctx.sessionManager.getSessionId = () => "session-workflow-targets";
|
|
278
|
+
const executor = makeAsyncExecutor([makeAgent("worker", { completionGuard: false })]);
|
|
279
|
+
const launch = await executor.execute("workflow-targets", {
|
|
280
|
+
workflowScript: parallel
|
|
281
|
+
? `return await Promise.all([runs.run("A", { agent: "worker", task: "A" }), runs.run("B", { agent: "worker", task: "B" })]);`
|
|
282
|
+
: `await runs.run("A", { agent: "worker", task: "A" }); return await runs.run("B", { agent: "worker", task: "B" });`,
|
|
283
|
+
async: true,
|
|
284
|
+
}, new AbortController().signal, undefined, ctx);
|
|
285
|
+
assert.equal(launch.isError, undefined, launch.content[0]?.text);
|
|
286
|
+
const runId = launch.details?.asyncId as string;
|
|
287
|
+
const asyncDir = path.join(ASYNC_DIR, runId);
|
|
288
|
+
const deliveredTexts = (): string[] => {
|
|
289
|
+
const file = path.join(mockPi.dir, "steers.jsonl");
|
|
290
|
+
return fs.existsSync(file) ? fs.readFileSync(file, "utf8").trim().split("\n").filter(Boolean).map((line) => JSON.parse(line).text) : [];
|
|
291
|
+
};
|
|
292
|
+
const check = async (id: string, address: { targetIndex?: number; targetIndexes?: number[] }, expected: Array<[number, string]>, deliveries: number, alreadyWritten = false) => {
|
|
293
|
+
if (!alreadyWritten) requestAsyncSteer(asyncDir, { id, message: id, ...address });
|
|
294
|
+
const status = await waitForAsyncState(runId, (candidate) => {
|
|
295
|
+
const targets = (candidate as SteeringTargets).steering?.recent.find((request) => request.id === id)?.targets;
|
|
296
|
+
return targets !== undefined && targets.every((target) => target.state === "delivered" || target.state === "failed");
|
|
297
|
+
}, 15_000);
|
|
298
|
+
const targets = (status as SteeringTargets).steering!.recent.find((request) => request.id === id)!.targets;
|
|
299
|
+
assert.deepEqual(targets.map(({ index, state }) => [index, state]), expected);
|
|
300
|
+
assert.equal(deliveredTexts().filter((text) => text.includes(id)).length, deliveries);
|
|
301
|
+
const journal = fs.readFileSync(path.join(asyncDir, "events.jsonl"), "utf8").trim().split("\n").map((line) => JSON.parse(line)).filter((event) => event.requestId === id);
|
|
302
|
+
assert.equal(journal.filter((event) => event.type === "subagent.steer.requested").length, 1);
|
|
303
|
+
for (const [index, state] of expected) assert.equal(journal.filter((event) => event.index === index && event.type === `subagent.steer.${state}`).length, 1);
|
|
304
|
+
return targets;
|
|
305
|
+
};
|
|
306
|
+
try {
|
|
307
|
+
await waitForMockPiCall(mockPi, 0, 10_000);
|
|
308
|
+
// Hold a transport-written request outside the watched inbox until A disappears.
|
|
309
|
+
// This controls queue-to-consumption delay without changing the watcher or child lifecycle.
|
|
310
|
+
const heldDir = path.join(tempDir, "held-steer");
|
|
311
|
+
if (!parallel) {
|
|
312
|
+
requestAsyncSteer(heldDir, { id: "disappeared-A", message: "disappeared-A", targetIndex: 0 });
|
|
313
|
+
fs.writeFileSync(releaseA, "go");
|
|
314
|
+
}
|
|
315
|
+
await waitForMockPiCall(mockPi, 1, 10_000);
|
|
316
|
+
await waitForAsyncState(runId, (candidate) => (candidate.steps?.length ?? 0) === 2, 15_000);
|
|
317
|
+
if (parallel) {
|
|
318
|
+
const targets = await check("ambiguous", {}, [[0, "failed"]], 0);
|
|
319
|
+
assert.match(targets[0]!.reason!, /2 live children/);
|
|
320
|
+
} else {
|
|
321
|
+
for (const file of fs.readdirSync(steerRequestsDir(heldDir))) fs.renameSync(path.join(steerRequestsDir(heldDir), file), path.join(steerRequestsDir(asyncDir), file));
|
|
322
|
+
await check("disappeared-A", {}, [[0, "failed"]], 0, true);
|
|
323
|
+
const targets = await check("dead-A", { targetIndex: 0 }, [[0, "failed"]], 0);
|
|
324
|
+
assert.match(targets[0]!.reason!, /not live/);
|
|
325
|
+
await check("unique-B", {}, [[1, "delivered"]], 1);
|
|
326
|
+
}
|
|
327
|
+
await check("aggregate", { targetIndexes: [0, 1] }, [[0, parallel ? "delivered" : "failed"], [1, "delivered"]], parallel ? 2 : 1);
|
|
328
|
+
if (parallel) {
|
|
329
|
+
const deliveries = fs.readFileSync(path.join(mockPi.dir, "steers.jsonl"), "utf8").trim().split("\n").map((line) => JSON.parse(line) as { text: string; sessionId: string });
|
|
330
|
+
assert.equal(new Set(deliveries.filter(({ text }) => text.includes("aggregate")).map(({ sessionId }) => sessionId)).size, 2, "aggregate must reach two distinct children");
|
|
331
|
+
}
|
|
332
|
+
await check("out-of-range", { targetIndex: 99 }, [[99, "failed"]], 0);
|
|
333
|
+
await check("mixed-range", { targetIndexes: [1, 99] }, [[1, "delivered"], [99, "failed"]], 1);
|
|
334
|
+
} finally {
|
|
335
|
+
fs.writeFileSync(releaseA, "go");
|
|
336
|
+
fs.writeFileSync(releaseB, "go");
|
|
337
|
+
}
|
|
338
|
+
assert.equal((await readAsyncPayload(runId)).success, true);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("closes the inbox and fails a late steer request when the workflow ends", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
|
|
342
|
+
const ctx = makeMinimalCtx(tempDir);
|
|
343
|
+
ctx.sessionManager.getSessionId = () => "session-workflow-steer-closed";
|
|
344
|
+
const executor = makeAsyncExecutor([]);
|
|
345
|
+
|
|
346
|
+
const launch = await executor.execute(
|
|
347
|
+
`workflow-steer-closed-${Date.now().toString(36)}`,
|
|
348
|
+
{ workflowScript: "return 'done'", async: true },
|
|
349
|
+
new AbortController().signal,
|
|
350
|
+
undefined,
|
|
351
|
+
ctx,
|
|
352
|
+
);
|
|
353
|
+
assert.equal(launch.isError, undefined, launch.content[0]?.text);
|
|
354
|
+
const runId = launch.details?.asyncId as string;
|
|
355
|
+
const asyncDir = path.join(ASYNC_DIR, runId);
|
|
356
|
+
|
|
357
|
+
await waitForAsyncState(runId, (candidate) => candidate.state === "complete", 15_000);
|
|
358
|
+
// A terminal run must refuse new steering rather than accept a request nothing will read.
|
|
359
|
+
assert.equal(fs.existsSync(steerInboxClosedPath(asyncDir)), true, "the steer inbox must be closed at teardown");
|
|
360
|
+
assert.throws(
|
|
361
|
+
() => requestAsyncSteer(asyncDir, { message: "Too late.", id: "workflow-steer-late", ts: Date.now() }),
|
|
362
|
+
/no longer accepts steering requests/,
|
|
363
|
+
);
|
|
364
|
+
});
|
|
365
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
import { AgentSession } from "@selesai/code";
|
|
4
|
+
import { BACKGROUND_CONTEXT } from "@earendil-works/pi-agent-core";
|
|
5
|
+
import { createModels } from "@earendil-works/pi-ai";
|
|
6
|
+
|
|
7
|
+
const state = globalThis.__pi085Smoke = { AgentSession, BACKGROUND_CONTEXT, createModels, started: 0, stopped: 0, faux: undefined };
|
|
8
|
+
const { loadRunnerChildSessionFactory } = await import(pathToFileURL(`${process.env.SMOKE_EXTENSION}/src/runs/background/runner-child-sessions.ts`).href);
|
|
9
|
+
const factory = await loadRunnerChildSessionFactory({});
|
|
10
|
+
const errors = [];
|
|
11
|
+
let started = 0, stopped = 0;
|
|
12
|
+
try {
|
|
13
|
+
const child = await factory.create({
|
|
14
|
+
cwd: process.cwd(), storage: { kind: "memory" }, model: "pi085-smoke/local", tools: [],
|
|
15
|
+
extensionPaths: [`${process.cwd()}/pi085-extension.ts`], ambientExtensions: false,
|
|
16
|
+
noSkills: true, noContextFiles: true, runtime: {},
|
|
17
|
+
hooks: [{ name: "lifecycle", factory(pi) {
|
|
18
|
+
pi.on("session_start", () => { started++; });
|
|
19
|
+
pi.on("session_shutdown", () => { stopped++; });
|
|
20
|
+
} }], onExtensionError(error) { errors.push(error); },
|
|
21
|
+
});
|
|
22
|
+
try {
|
|
23
|
+
assert.equal(child.modelId, "pi085-smoke/local");
|
|
24
|
+
await child.prompt("Return the local scripted response.");
|
|
25
|
+
assert.equal(state.faux.state.callCount, 1);
|
|
26
|
+
assert.ok(child.messages.some(m => m.role === "assistant" && m.content.some(c => c.type === "text" && c.text === "local response verified")));
|
|
27
|
+
} finally { await child.dispose(); }
|
|
28
|
+
assert.deepEqual([started, stopped, state.started, state.stopped], [1, 1, 1, 1]);
|
|
29
|
+
assert.deepEqual(errors, []);
|
|
30
|
+
console.log("PASS public SDK/default child factory: local prompt, API identity, startup/shutdown");
|
|
31
|
+
} finally { await factory.dispose(); }
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Run with Node >=22.19.0: node --experimental-strip-types test/smoke/pi085-clean-install.mjs [artifact-dir] [0.85.0|0.85.1]
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
|
+
|
|
9
|
+
const source = fileURLToPath(new URL("../../", import.meta.url));
|
|
10
|
+
const version = process.argv[3] ?? "0.85.0";
|
|
11
|
+
assert.ok(["0.85.0", "0.85.1"].includes(version), "requires an explicitly supported smoke version");
|
|
12
|
+
const isPi0850 = version === "0.85.0";
|
|
13
|
+
const root = process.argv[2] ? path.resolve(process.argv[2]) : fs.mkdtempSync(path.join(os.tmpdir(), "pi085-smoke-"));
|
|
14
|
+
fs.mkdirSync(root, { recursive: true });
|
|
15
|
+
const host = path.join(root, "host");
|
|
16
|
+
const extension = path.join(root, "extension");
|
|
17
|
+
const cwd = path.join(root, "cwd");
|
|
18
|
+
for (const dir of [host, extension, cwd, path.join(root, "home")]) fs.mkdirSync(dir, { recursive: true });
|
|
19
|
+
assert.ok(!fs.existsSync(path.join(host, "node_modules")), "requires a pristine host install");
|
|
20
|
+
assert.ok(!fs.existsSync(path.join(extension, "node_modules")), "requires a pristine extension install");
|
|
21
|
+
const env = {
|
|
22
|
+
...process.env, HOME: path.join(root, "home"), USERPROFILE: path.join(root, "home"), SELESAI_CODING_AGENT_DIR: path.join(root, "agent"),
|
|
23
|
+
XDG_CACHE_HOME: path.join(root, "cache"), npm_config_cache: path.join(root, "npm-cache"),
|
|
24
|
+
NODE_COMPILE_CACHE: path.join(root, "node-cache"), JITI_FS_CACHE: "false", PI_OFFLINE: "1",
|
|
25
|
+
};
|
|
26
|
+
// Do not inherit loader/alias overrides or operator credentials into the child.
|
|
27
|
+
for (const key of Object.keys(env)) {
|
|
28
|
+
if (/API_KEY|TOKEN|SECRET|PASSWORD/.test(key) || ["NODE_OPTIONS", "NODE_PATH", "JITI_ALIAS"].includes(key)) delete env[key];
|
|
29
|
+
}
|
|
30
|
+
function run(name, command, args, workdir, extra = {}, success = true) {
|
|
31
|
+
const result = spawnSync(command, args, { cwd: workdir, env: { ...env, ...extra }, encoding: "utf8", timeout: 180000, maxBuffer: 10 * 1024 * 1024 });
|
|
32
|
+
fs.writeFileSync(path.join(root, `${name}.log`), `${result.stdout ?? ""}${result.stderr ?? ""}`);
|
|
33
|
+
assert.ifError(result.error);
|
|
34
|
+
if (success) assert.equal(result.status, 0, `${name}: ${result.stdout}\n${result.stderr}`);
|
|
35
|
+
else assert.notEqual(result.status, 0, `${name} unexpectedly passed`);
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
fs.writeFileSync(path.join(host, "package.json"), JSON.stringify({ private: true, dependencies: { "@selesai/code": version } }));
|
|
39
|
+
run("host-install", "npm", ["install", "--no-audit", "--no-fund"], host);
|
|
40
|
+
const packed = JSON.parse(run("pack", "npm", ["pack", "--json", "--pack-destination", root], source).stdout)[0];
|
|
41
|
+
assert.ok(packed.files.some(file => file.path === "runner-server-preload.mjs"), "preload must ship");
|
|
42
|
+
fs.writeFileSync(path.join(extension, "package.json"), JSON.stringify({ private: true, dependencies: { "pi-subagents": `file:${path.join(root, packed.filename)}` } }));
|
|
43
|
+
run("extension-install", "npm", ["install", "--no-audit", "--no-fund"], extension);
|
|
44
|
+
const installed = path.join(extension, "node_modules/pi-subagents");
|
|
45
|
+
const pi = path.join(host, "node_modules/@selesai/code");
|
|
46
|
+
const { createJiti } = await import(pathToFileURL(path.join(extension, "node_modules/jiti/lib/jiti.mjs")).href);
|
|
47
|
+
const jitiLoader = createJiti(import.meta.url, { fsCache: false });
|
|
48
|
+
const { resolveHostPeerAliases, findHostPeerPackageDir, resolvePackageSubpath } = await jitiLoader.import(path.join(installed, "src/runs/background/runner-aliases.ts"));
|
|
49
|
+
assert.equal(findHostPeerPackageDir(pi, "@earendil-works/pi-server"), undefined, "host must remain missing server");
|
|
50
|
+
if (!isPi0850) assert.equal(findHostPeerPackageDir(pi, "@earendil-works/pi-client"), undefined, "stable host must remain missing client");
|
|
51
|
+
const pristine = run("pristine-public-sdk", process.execPath, ["--input-type=module", "-e", `await import(${JSON.stringify(pathToFileURL(resolvePackageSubpath(pi, ".")).href)})`], cwd, {}, !isPi0850);
|
|
52
|
+
if (isPi0850) assert.match(pristine.stderr, /Cannot find package '@earendil-works\/pi-server'/);
|
|
53
|
+
const resolved = resolveHostPeerAliases(pi);
|
|
54
|
+
assert.deepEqual(resolved.missing, []);
|
|
55
|
+
assert.deepEqual(resolved.supplemental, isPi0850 ? ["@earendil-works/pi-server", "@earendil-works/pi-server/unix"] : []);
|
|
56
|
+
if (!isPi0850) {
|
|
57
|
+
for (const specifier of ["@earendil-works/pi-server", "@earendil-works/pi-server/unix", "@earendil-works/pi-client/unix"]) assert.equal(resolved.aliases[specifier], undefined);
|
|
58
|
+
}
|
|
59
|
+
fs.writeFileSync(path.join(root, "aliases.json"), JSON.stringify(resolved, null, 2));
|
|
60
|
+
for (const file of ["pi085-child.ts", "pi085-extension.ts"]) fs.copyFileSync(new URL(file, import.meta.url), path.join(cwd, file));
|
|
61
|
+
const childEnv = { SMOKE_EXTENSION: installed, JITI_ALIAS: JSON.stringify(resolved.aliases) };
|
|
62
|
+
const jiti = path.join(extension, "node_modules/jiti/lib/jiti-cli.mjs");
|
|
63
|
+
const args = [jiti, path.join(cwd, "pi085-child.ts")];
|
|
64
|
+
// The real file-extension gate must fail with aliases alone, not just an import mock.
|
|
65
|
+
if (isPi0850) {
|
|
66
|
+
const negative = run("without-preload", process.execPath, args, cwd, childEnv, false);
|
|
67
|
+
assert.match(negative.stderr, /Model "pi085-smoke\/local" not found/);
|
|
68
|
+
}
|
|
69
|
+
const preload = resolved.supplemental.length ? ["--import", pathToFileURL(path.join(installed, "runner-server-preload.mjs")).href] : [];
|
|
70
|
+
const positive = run("child", process.execPath, [...preload, ...args], cwd, childEnv);
|
|
71
|
+
assert.match(positive.stdout, /PASS public SDK\/default child factory/);
|
|
72
|
+
assert.equal(findHostPeerPackageDir(pi, "@earendil-works/pi-server"), undefined);
|
|
73
|
+
if (!isPi0850) assert.equal(findHostPeerPackageDir(pi, "@earendil-works/pi-client"), undefined);
|
|
74
|
+
console.log(`${positive.stdout.trim()}\nArtifacts: ${root}`);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { AgentSession } from "@selesai/code";
|
|
3
|
+
import { BACKGROUND_CONTEXT } from "@earendil-works/pi-agent-core";
|
|
4
|
+
import { createModels, fauxProvider, fauxAssistantMessage } from "@earendil-works/pi-ai";
|
|
5
|
+
|
|
6
|
+
export default function(pi) {
|
|
7
|
+
const state = globalThis.__pi085Smoke;
|
|
8
|
+
assert.equal(AgentSession, state.AgentSession);
|
|
9
|
+
assert.equal(BACKGROUND_CONTEXT, state.BACKGROUND_CONTEXT);
|
|
10
|
+
assert.equal(createModels, state.createModels);
|
|
11
|
+
state.faux = fauxProvider({ provider: "pi085-smoke", models: [{ id: "local" }], tokensPerSecond: 100000 });
|
|
12
|
+
state.faux.setResponses([fauxAssistantMessage("local response verified")]);
|
|
13
|
+
pi.registerProvider(state.faux.provider);
|
|
14
|
+
pi.on("session_start", () => { state.started++; });
|
|
15
|
+
pi.on("session_shutdown", () => { state.stopped++; });
|
|
16
|
+
}
|