@selesai/code 0.5.19 → 0.5.21
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/dist/extensions/handoff-new.test.ts +8 -9
- package/dist/extensions/handoff-new.ts +6 -8
- package/dist/extensions/pi-intercom/broker/spawn.test.ts +31 -0
- package/dist/extensions/pi-intercom/broker/spawn.ts +5 -3
- package/dist/extensions/pi-subagents/CHANGELOG.md +1367 -0
- package/dist/extensions/pi-subagents/README.md +602 -220
- package/dist/extensions/pi-subagents/banner.png +0 -0
- package/dist/extensions/pi-subagents/index.ts +1 -0
- package/dist/extensions/pi-subagents/install.mjs +3 -3
- package/dist/extensions/pi-subagents/package-lock.json +3405 -0
- package/dist/extensions/pi-subagents/package.json +26 -14
- package/dist/extensions/pi-subagents/prompts/gather-context-and-clarify.md +1 -1
- package/dist/extensions/pi-subagents/prompts/parallel-cleanup.md +9 -9
- package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +3 -3
- package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +7 -7
- package/dist/extensions/pi-subagents/prompts/parallel-research.md +6 -6
- package/dist/extensions/pi-subagents/prompts/parallel-review.md +8 -8
- package/dist/extensions/pi-subagents/prompts/review-loop.md +13 -11
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +267 -176
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +89 -16
- package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +19 -0
- package/dist/extensions/pi-subagents/src/agents/agents.ts +218 -120
- package/dist/extensions/pi-subagents/src/agents/frontmatter.ts +67 -13
- package/dist/extensions/pi-subagents/src/agents/proactive-skills.ts +2 -2
- package/dist/extensions/pi-subagents/src/agents/skills.ts +25 -12
- package/dist/extensions/pi-subagents/src/api/background-work.ts +197 -0
- package/dist/extensions/pi-subagents/src/api/capability-ceiling.ts +17 -0
- package/dist/extensions/pi-subagents/src/api/delegation.ts +285 -0
- package/dist/extensions/pi-subagents/src/api/preflight.ts +399 -0
- package/dist/extensions/pi-subagents/src/extension/chain-validation.ts +165 -0
- package/dist/extensions/pi-subagents/src/extension/config.ts +7 -1
- package/dist/extensions/pi-subagents/src/extension/doctor.ts +15 -0
- package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +3 -1
- package/dist/extensions/pi-subagents/src/extension/index.ts +113 -157
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +41 -4
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +45 -15
- package/dist/extensions/pi-subagents/src/extension/steering-notices.ts +35 -0
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +28 -15
- package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +4 -3
- package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +225 -29
- package/dist/extensions/pi-subagents/src/intercom/result-intercom.ts +11 -0
- package/dist/extensions/pi-subagents/src/profiles/profiles.ts +8 -10
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +452 -62
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +60 -9
- package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +198 -52
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +119 -21
- package/dist/extensions/pi-subagents/src/runs/background/auto-drain.ts +67 -0
- package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +2 -0
- package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +16 -8
- package/dist/extensions/pi-subagents/src/runs/background/completion-batcher.ts +6 -4
- package/dist/extensions/pi-subagents/src/runs/background/completion-dedupe.ts +2 -11
- package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +260 -13
- package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +32 -6
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +171 -90
- package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +280 -0
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +149 -88
- package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +14 -2
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +33 -17
- package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +9 -1
- package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +38 -10
- package/dist/extensions/pi-subagents/src/runs/background/steering.ts +237 -0
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +1297 -307
- package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +610 -0
- package/dist/extensions/pi-subagents/src/runs/background/top-level-async.ts +2 -1
- package/dist/extensions/pi-subagents/src/runs/background/wait-config.ts +36 -0
- package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +26 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/async-steering-action.ts +230 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +22 -6
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +228 -140
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +452 -138
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +90 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +1001 -416
- package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +443 -132
- package/dist/extensions/pi-subagents/src/runs/shared/agent-contract.ts +38 -0
- package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +178 -0
- package/dist/extensions/pi-subagents/src/runs/shared/child-protocol.ts +121 -0
- package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +41 -93
- package/dist/extensions/pi-subagents/src/runs/shared/context-mode.ts +44 -0
- package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +11 -9
- package/dist/extensions/pi-subagents/src/runs/shared/long-running-guard.ts +4 -0
- package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +12 -6
- package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +36 -0
- package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +44 -7
- package/dist/extensions/pi-subagents/src/runs/shared/nested-render.ts +4 -1
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +154 -0
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +18 -0
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +174 -54
- package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +38 -31
- package/dist/extensions/pi-subagents/src/runs/shared/run-history.ts +90 -5
- package/dist/extensions/pi-subagents/src/runs/shared/session-lease.ts +299 -0
- package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +61 -6
- package/dist/extensions/pi-subagents/src/runs/shared/spawn-budget.ts +128 -0
- package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +112 -7
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +14 -6
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +149 -36
- package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +181 -0
- package/dist/extensions/pi-subagents/src/runs/shared/tool-availability.ts +83 -0
- package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +11 -5
- package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +50 -4
- package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +63 -14
- package/dist/extensions/pi-subagents/src/shared/accessible-dir.ts +25 -0
- package/dist/extensions/pi-subagents/src/shared/artifacts.ts +37 -7
- package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +18 -43
- package/dist/extensions/pi-subagents/src/shared/child-transcript.ts +52 -0
- package/dist/extensions/pi-subagents/src/shared/env.ts +16 -0
- package/dist/extensions/pi-subagents/src/shared/file-system-retry.ts +47 -0
- package/dist/extensions/pi-subagents/src/shared/fork-context.ts +28 -3
- package/dist/extensions/pi-subagents/src/shared/launch-contract.ts +123 -0
- package/dist/extensions/pi-subagents/src/shared/model-info.ts +8 -5
- package/dist/extensions/pi-subagents/src/shared/settings.ts +9 -1
- package/dist/extensions/pi-subagents/src/shared/status-format.ts +7 -1
- package/dist/extensions/pi-subagents/src/shared/types.ts +522 -51
- package/dist/extensions/pi-subagents/src/shared/utils.ts +59 -53
- package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +604 -0
- package/dist/extensions/pi-subagents/src/slash/delegation-json.ts +108 -0
- package/dist/extensions/pi-subagents/src/slash/delegation-request.ts +249 -0
- package/dist/extensions/pi-subagents/src/slash/prompt-template-bridge.ts +353 -345
- package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +2 -2
- package/dist/extensions/pi-subagents/src/slash/selector.ts +147 -0
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +265 -23
- package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +2 -2
- package/dist/extensions/pi-subagents/src/slash/subagents-admin.ts +428 -0
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +362 -0
- package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +472 -0
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +664 -0
- package/dist/extensions/pi-subagents/src/tui/render.ts +115 -31
- package/dist/extensions/pi-subagents/src/watchdog/change-signature.ts +220 -0
- package/dist/extensions/pi-subagents/src/watchdog/child-status.ts +205 -0
- package/dist/extensions/pi-subagents/src/watchdog/emission-guard.ts +123 -0
- package/dist/extensions/pi-subagents/src/watchdog/lsp-diagnostics.ts +532 -0
- package/dist/extensions/pi-subagents/src/watchdog/model-selection.ts +167 -0
- package/dist/extensions/pi-subagents/src/watchdog/register-child.ts +117 -0
- package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +433 -0
- package/dist/extensions/pi-subagents/src/watchdog/render.ts +54 -0
- package/dist/extensions/pi-subagents/src/watchdog/review.ts +298 -0
- package/dist/extensions/pi-subagents/src/watchdog/runtime.ts +714 -0
- package/dist/extensions/pi-subagents/src/watchdog/settings.ts +528 -0
- package/dist/extensions/pi-subagents/src/watchdog/tool-actions.ts +155 -0
- package/dist/extensions/pi-subagents/src/watchdog/turn-delta.ts +161 -0
- package/dist/extensions/pi-subagents/src/watchdog/types.ts +188 -0
- package/dist/extensions/pi-subagents/src/watchdog/warning-format.ts +73 -0
- package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +130 -2
- package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +439 -0
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +1632 -61
- package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +54 -1
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +112 -0
- package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +98 -1
- package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +179 -9
- package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +16 -9
- package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +6 -5
- package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +589 -9
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +528 -21
- package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +227 -3
- package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +235 -7
- package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +27 -0
- package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +172 -2
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1502 -18
- package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +993 -4
- package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +27 -0
- package/dist/extensions/pi-subagents/test/integration/top-level-async.test.ts +7 -1
- package/dist/extensions/pi-subagents/test/support/helpers.ts +32 -0
- package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +59 -2
- package/dist/extensions/pi-subagents/test/support/mock-pi.ts +32 -6
- package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +25 -24
- package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +59 -35
- package/dist/extensions/pi-subagents/test/support/session-lease-child.mjs +42 -0
- package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +703 -69
- package/dist/extensions/pi-subagents/test/unit/accessible-dir.test.ts +82 -0
- package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +15 -15
- package/dist/extensions/pi-subagents/test/unit/agent-eject-disable.test.ts +66 -66
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +478 -19
- package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +228 -7
- package/dist/extensions/pi-subagents/test/unit/agent-memory.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +198 -78
- package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +15 -1
- package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +102 -7
- package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +439 -6
- package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +41 -1
- package/dist/extensions/pi-subagents/test/unit/auto-drain.test.ts +79 -0
- package/dist/extensions/pi-subagents/test/unit/background-work.test.ts +285 -0
- package/dist/extensions/pi-subagents/test/unit/capability-ceiling-pi-args.test.ts +84 -0
- package/dist/extensions/pi-subagents/test/unit/capability-ceiling.test.ts +68 -0
- package/dist/extensions/pi-subagents/test/unit/chain-validation.test.ts +326 -0
- package/dist/extensions/pi-subagents/test/unit/child-protocol.test.ts +82 -0
- package/dist/extensions/pi-subagents/test/unit/child-transcript.test.ts +46 -4
- package/dist/extensions/pi-subagents/test/unit/completion-batcher.test.ts +3 -1
- package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +9 -17
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +175 -3
- package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +10 -5
- package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +126 -6
- package/dist/extensions/pi-subagents/test/unit/default-extensions.test.ts +219 -0
- package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +913 -0
- package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +13 -2
- package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +3 -3
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +351 -0
- package/dist/extensions/pi-subagents/test/unit/fleet-transcript.test.ts +280 -0
- package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +575 -0
- package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +65 -0
- package/dist/extensions/pi-subagents/test/unit/fork-context.test.ts +94 -1
- package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +23 -0
- package/dist/extensions/pi-subagents/test/unit/host-peer-runtime-imports.test.ts +157 -0
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +367 -5
- package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +11 -0
- package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +36 -0
- package/dist/extensions/pi-subagents/test/unit/model-info.test.ts +25 -3
- package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +341 -10
- package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +16 -0
- package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +52 -0
- package/dist/extensions/pi-subagents/test/unit/notify.test.ts +168 -7
- package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +85 -7
- package/dist/extensions/pi-subagents/test/unit/parallel-handoff.test.ts +175 -0
- package/dist/extensions/pi-subagents/test/unit/path-resolution.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +243 -16
- package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +79 -36
- package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +174 -56
- package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +322 -0
- package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +204 -0
- package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +20 -12
- package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +117 -4
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +40 -7
- package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +48 -1
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +21 -3
- package/dist/extensions/pi-subagents/test/unit/selector.test.ts +57 -0
- package/dist/extensions/pi-subagents/test/unit/session-lease.test.ts +326 -0
- package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +101 -2
- package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +111 -1
- package/dist/extensions/pi-subagents/test/unit/spawn-budget.test.ts +124 -0
- package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +6 -4
- package/dist/extensions/pi-subagents/test/unit/status-format.test.ts +7 -0
- package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +397 -0
- package/dist/extensions/pi-subagents/test/unit/steering-notices.test.ts +58 -0
- package/dist/extensions/pi-subagents/test/unit/steering.test.ts +173 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +25 -4
- package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +390 -25
- package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +837 -0
- package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +86 -0
- package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +15 -0
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +47 -1
- package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +56 -10
- package/dist/extensions/pi-subagents/test/unit/watchdog-change-signature.test.ts +387 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-child-status.test.ts +110 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-emission-guard.test.ts +62 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-lsp-diagnostics.test.ts +144 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-model-selection.test.ts +121 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-render.test.ts +76 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-review.test.ts +327 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-runtime.test.ts +863 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-settings.test.ts +290 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-tool-actions.test.ts +91 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-turn-delta.test.ts +130 -0
- package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +32 -4
- package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +5 -1
- package/dist/extensions/pi-subagents/test/unit/writer-budget-guidance.test.ts +30 -0
- package/dist/extensions/question/batch.ts +39 -0
- package/dist/extensions/question/dialog-adapter.ts +23 -1
- package/dist/extensions/question/index.ts +237 -7
- package/dist/extensions/question/schemas.ts +28 -15
- package/dist/extensions/question/tests/batch.test.ts +26 -0
- package/dist/extensions/question/tests/ui-protocol.test.ts +23 -3
- package/dist/extensions/question/tui-adapter.ts +2 -3
- package/dist/extensions/question/types.ts +37 -0
- package/dist/extensions/question/ui-protocol.ts +3 -3
- package/dist/skills/batch-grill-me/SKILL.md +3 -1
- package/package.json +2 -1
|
@@ -0,0 +1,837 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { describe, it } from "node:test";
|
|
6
|
+
import { WAIT_TOOL_ENABLED_ENV, resolveWaitToolConfig, waitForSubagents, type SubagentWaitDeps } from "../../src/runs/background/subagent-wait.ts";
|
|
7
|
+
import type { SubagentState } from "../../src/shared/types.ts";
|
|
8
|
+
|
|
9
|
+
function writeStatus(asyncRoot: string, runId: string, state: string, extra: object = {}): void {
|
|
10
|
+
const dir = path.join(asyncRoot, runId);
|
|
11
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
12
|
+
// Use a recent timestamp so the stale-run reconciler doesn't mark a live
|
|
13
|
+
// "running" fixture as failed for having a stale heartbeat.
|
|
14
|
+
const nowMs = Date.now();
|
|
15
|
+
fs.writeFileSync(
|
|
16
|
+
path.join(dir, "status.json"),
|
|
17
|
+
JSON.stringify({
|
|
18
|
+
runId,
|
|
19
|
+
mode: "single",
|
|
20
|
+
state,
|
|
21
|
+
startedAt: nowMs,
|
|
22
|
+
lastUpdate: nowMs,
|
|
23
|
+
steps: [{ agent: "worker", status: state }],
|
|
24
|
+
...extra,
|
|
25
|
+
}),
|
|
26
|
+
"utf-8",
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function makeState(sessionId: string | null): SubagentState {
|
|
31
|
+
return {
|
|
32
|
+
baseCwd: "",
|
|
33
|
+
currentSessionId: sessionId,
|
|
34
|
+
asyncJobs: new Map(),
|
|
35
|
+
foregroundControls: new Map(),
|
|
36
|
+
lastForegroundControlId: null,
|
|
37
|
+
cleanupTimers: new Map(),
|
|
38
|
+
lastUiContext: null,
|
|
39
|
+
poller: null,
|
|
40
|
+
completionSeen: new Map(),
|
|
41
|
+
watcher: null,
|
|
42
|
+
watcherRestartTimer: null,
|
|
43
|
+
resultFileCoalescer: { schedule: () => false, clear: () => {} },
|
|
44
|
+
} as SubagentState;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function textOf(result: { content: Array<{ type: string; text?: string }> }): string {
|
|
48
|
+
return result.content.map((c) => c.text ?? "").join("");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function baseDeps(root: string, state: SubagentState, overrides: Partial<SubagentWaitDeps> = {}): SubagentWaitDeps {
|
|
52
|
+
return {
|
|
53
|
+
state,
|
|
54
|
+
asyncDirRoot: path.join(root, "runs"),
|
|
55
|
+
resultsDir: path.join(root, "results"),
|
|
56
|
+
// Never probe real PIDs in tests — treat every recorded pid as alive so
|
|
57
|
+
// reconciliation doesn't flip a "running" fixture to failed.
|
|
58
|
+
kill: () => true,
|
|
59
|
+
pollIntervalMs: 250,
|
|
60
|
+
...overrides,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
describe("subagent_wait tool", () => {
|
|
65
|
+
it("resolves waitTool config and environment overrides strictly", () => {
|
|
66
|
+
assert.deepEqual(resolveWaitToolConfig(undefined, {}), { enabled: true });
|
|
67
|
+
assert.deepEqual(resolveWaitToolConfig(false, {}), { enabled: false });
|
|
68
|
+
assert.deepEqual(resolveWaitToolConfig({ enabled: false }, {}), { enabled: false });
|
|
69
|
+
assert.deepEqual(resolveWaitToolConfig({ enabled: false }, { [WAIT_TOOL_ENABLED_ENV]: "true" }), { enabled: true });
|
|
70
|
+
assert.deepEqual(resolveWaitToolConfig(true, { [WAIT_TOOL_ENABLED_ENV]: "off" }), { enabled: false });
|
|
71
|
+
assert.throws(() => resolveWaitToolConfig("false" as never, {}), /config\.waitTool/);
|
|
72
|
+
assert.throws(() => resolveWaitToolConfig({ enabled: "false" } as never, {}), /config\.waitTool\.enabled/);
|
|
73
|
+
assert.throws(() => resolveWaitToolConfig(undefined, { [WAIT_TOOL_ENABLED_ENV]: "maybe" }), /SELESAI_SUBAGENT_WAIT_TOOL_ENABLED/);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("returns immediately without polling when waitTool is disabled", async () => {
|
|
77
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-disabled-"));
|
|
78
|
+
try {
|
|
79
|
+
const asyncRoot = path.join(root, "runs");
|
|
80
|
+
const state = makeState("sess-1");
|
|
81
|
+
writeStatus(asyncRoot, "run-a", "running", { sessionId: "sess-1", pid: 999999 });
|
|
82
|
+
let slept = false;
|
|
83
|
+
const result = await waitForSubagents({}, undefined, baseDeps(root, state, {
|
|
84
|
+
enabled: false,
|
|
85
|
+
sleep: async () => {
|
|
86
|
+
slept = true;
|
|
87
|
+
throw new Error("disabled subagent_wait should not sleep");
|
|
88
|
+
},
|
|
89
|
+
}));
|
|
90
|
+
|
|
91
|
+
assert.equal(result.isError, undefined);
|
|
92
|
+
assert.match(textOf(result), /disabled/i);
|
|
93
|
+
assert.match(textOf(result), /without blocking/i);
|
|
94
|
+
assert.equal(slept, false);
|
|
95
|
+
} finally {
|
|
96
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("returns immediately when there is nothing to wait for", async () => {
|
|
101
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-empty-"));
|
|
102
|
+
try {
|
|
103
|
+
const state = makeState("sess-1");
|
|
104
|
+
const result = await waitForSubagents({}, undefined, baseDeps(root, state));
|
|
105
|
+
assert.equal(result.isError, undefined);
|
|
106
|
+
assert.match(textOf(result), /nothing to wait for/i);
|
|
107
|
+
} finally {
|
|
108
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("with all:true, resolves once every active run reaches a terminal state", async () => {
|
|
113
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-resolve-"));
|
|
114
|
+
try {
|
|
115
|
+
const asyncRoot = path.join(root, "runs");
|
|
116
|
+
const state = makeState("sess-1");
|
|
117
|
+
writeStatus(asyncRoot, "run-a", "running", { sessionId: "sess-1", pid: 999999 });
|
|
118
|
+
writeStatus(asyncRoot, "run-b", "queued", { sessionId: "sess-1", pid: 999998 });
|
|
119
|
+
|
|
120
|
+
// Flip one run terminal on the first poll, the other on the second — so
|
|
121
|
+
// all:true must keep waiting past the first completion.
|
|
122
|
+
let polls = 0;
|
|
123
|
+
const sleep = async () => {
|
|
124
|
+
polls += 1;
|
|
125
|
+
if (polls === 1) writeStatus(asyncRoot, "run-a", "complete", { sessionId: "sess-1" });
|
|
126
|
+
if (polls === 2) writeStatus(asyncRoot, "run-b", "failed", { sessionId: "sess-1" });
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const result = await waitForSubagents({ all: true }, undefined, baseDeps(root, state, { sleep }));
|
|
130
|
+
assert.equal(result.isError, undefined);
|
|
131
|
+
const text = textOf(result);
|
|
132
|
+
assert.match(text, /done/i);
|
|
133
|
+
assert.match(text, /1 complete/);
|
|
134
|
+
assert.match(text, /1 failed/);
|
|
135
|
+
assert.ok(polls >= 2, "all:true should wait for both completions");
|
|
136
|
+
} finally {
|
|
137
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("surfaces failed terminal runs as errors only for internal auto-drain", async () => {
|
|
142
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-drain-failure-"));
|
|
143
|
+
try {
|
|
144
|
+
const asyncRoot = path.join(root, "runs");
|
|
145
|
+
const state = makeState("sess-1");
|
|
146
|
+
writeStatus(asyncRoot, "run-failed", "running", { sessionId: "sess-1", pid: 999999 });
|
|
147
|
+
const result = await waitForSubagents({ all: true }, undefined, baseDeps(root, state, {
|
|
148
|
+
failOnFailedRuns: true,
|
|
149
|
+
sleep: async () => writeStatus(asyncRoot, "run-failed", "failed", { sessionId: "sess-1" }),
|
|
150
|
+
}));
|
|
151
|
+
assert.equal(result.isError, true);
|
|
152
|
+
assert.match(textOf(result), /1 failed/);
|
|
153
|
+
} finally {
|
|
154
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("wakes when a run needs attention, not only on completion", async () => {
|
|
159
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-attn-"));
|
|
160
|
+
try {
|
|
161
|
+
const asyncRoot = path.join(root, "runs");
|
|
162
|
+
const state = makeState("sess-1");
|
|
163
|
+
// Two runs, all:true so it would normally block until both finish.
|
|
164
|
+
writeStatus(asyncRoot, "run-a", "running", { sessionId: "sess-1", pid: 999999 });
|
|
165
|
+
writeStatus(asyncRoot, "run-b", "running", { sessionId: "sess-1", pid: 999998 });
|
|
166
|
+
|
|
167
|
+
// Neither completes; run-a flags needs_attention (blocked for a decision).
|
|
168
|
+
let polls = 0;
|
|
169
|
+
const sleep = async () => {
|
|
170
|
+
polls += 1;
|
|
171
|
+
if (polls === 1) {
|
|
172
|
+
writeStatus(asyncRoot, "run-a", "running", { sessionId: "sess-1", pid: 999999, activityState: "needs_attention" });
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const result = await waitForSubagents({ all: true }, undefined, baseDeps(root, state, { sleep }));
|
|
177
|
+
assert.equal(result.isError, undefined);
|
|
178
|
+
const text = textOf(result);
|
|
179
|
+
assert.match(text, /need attention/i, "should report the attention run");
|
|
180
|
+
assert.match(text, /steer a top-level live async child, resume a paused\/completed\/failed child, or interrupt explicitly/);
|
|
181
|
+
assert.match(text, /run-a/, "should name the attention run");
|
|
182
|
+
assert.ok(polls <= 2, `should break on attention promptly, polled ${polls}`);
|
|
183
|
+
} finally {
|
|
184
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("reports runs that already need attention before waiting starts", async () => {
|
|
189
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-initial-attn-"));
|
|
190
|
+
try {
|
|
191
|
+
const asyncRoot = path.join(root, "runs");
|
|
192
|
+
const state = makeState("sess-1");
|
|
193
|
+
writeStatus(asyncRoot, "run-blocked", "running", { sessionId: "sess-1", pid: 999999, activityState: "needs_attention" });
|
|
194
|
+
|
|
195
|
+
let polls = 0;
|
|
196
|
+
const result = await waitForSubagents({}, undefined, baseDeps(root, state, {
|
|
197
|
+
sleep: async () => { polls += 1; },
|
|
198
|
+
}));
|
|
199
|
+
|
|
200
|
+
assert.equal(result.isError, undefined);
|
|
201
|
+
const text = textOf(result);
|
|
202
|
+
assert.doesNotMatch(text, /nothing to wait for/i);
|
|
203
|
+
assert.match(text, /need attention/i);
|
|
204
|
+
assert.match(text, /run-blocked/);
|
|
205
|
+
assert.equal(polls, 0, "initial attention should return without polling");
|
|
206
|
+
} finally {
|
|
207
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("reports step-level attention even when the aggregate flag has not caught up", async () => {
|
|
212
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-step-attn-"));
|
|
213
|
+
try {
|
|
214
|
+
const asyncRoot = path.join(root, "runs");
|
|
215
|
+
const state = makeState("sess-1");
|
|
216
|
+
writeStatus(asyncRoot, "run-step-blocked", "running", {
|
|
217
|
+
sessionId: "sess-1",
|
|
218
|
+
pid: 999999,
|
|
219
|
+
steps: [{ agent: "worker", status: "running", activityState: "needs_attention" }],
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
let polls = 0;
|
|
223
|
+
const result = await waitForSubagents({}, undefined, baseDeps(root, state, {
|
|
224
|
+
sleep: async () => { polls += 1; },
|
|
225
|
+
}));
|
|
226
|
+
|
|
227
|
+
assert.equal(result.isError, undefined);
|
|
228
|
+
assert.match(textOf(result), /need attention/i);
|
|
229
|
+
assert.match(textOf(result), /run-step-blocked/);
|
|
230
|
+
assert.equal(polls, 0, "step-level attention should return without polling");
|
|
231
|
+
} finally {
|
|
232
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("by default returns as soon as the FIRST run finishes, leaving the rest in flight", async () => {
|
|
237
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-first-"));
|
|
238
|
+
try {
|
|
239
|
+
const asyncRoot = path.join(root, "runs");
|
|
240
|
+
const state = makeState("sess-1");
|
|
241
|
+
writeStatus(asyncRoot, "run-a", "running", { sessionId: "sess-1", pid: 999999 });
|
|
242
|
+
writeStatus(asyncRoot, "run-b", "running", { sessionId: "sess-1", pid: 999998 });
|
|
243
|
+
writeStatus(asyncRoot, "run-c", "running", { sessionId: "sess-1", pid: 999997 });
|
|
244
|
+
|
|
245
|
+
// Only run-a finishes; b and c stay running forever.
|
|
246
|
+
let polls = 0;
|
|
247
|
+
const sleep = async () => {
|
|
248
|
+
polls += 1;
|
|
249
|
+
if (polls === 1) writeStatus(asyncRoot, "run-a", "complete", { sessionId: "sess-1" });
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const result = await waitForSubagents({}, undefined, baseDeps(root, state, { sleep }));
|
|
253
|
+
assert.equal(result.isError, undefined);
|
|
254
|
+
const text = textOf(result);
|
|
255
|
+
assert.match(text, /1 of 3 run\(s\) finished/);
|
|
256
|
+
assert.match(text, /1 complete/);
|
|
257
|
+
assert.match(text, /2 run\(s\) still in flight/);
|
|
258
|
+
// Must not have blocked on b and c: a bounded number of polls.
|
|
259
|
+
assert.ok(polls <= 2, `first-completion should return promptly, polled ${polls}`);
|
|
260
|
+
} finally {
|
|
261
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("only waits for runs belonging to the current session", async () => {
|
|
266
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-session-"));
|
|
267
|
+
try {
|
|
268
|
+
const asyncRoot = path.join(root, "runs");
|
|
269
|
+
const state = makeState("sess-1");
|
|
270
|
+
// A run from another session must be ignored.
|
|
271
|
+
writeStatus(asyncRoot, "run-other", "running", { sessionId: "sess-2", pid: 999999 });
|
|
272
|
+
const result = await waitForSubagents({}, undefined, baseDeps(root, state));
|
|
273
|
+
assert.equal(result.isError, undefined);
|
|
274
|
+
assert.match(textOf(result), /nothing to wait for/i);
|
|
275
|
+
} finally {
|
|
276
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("summarizes only runs that were active when waiting began", async () => {
|
|
281
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-summary-scope-"));
|
|
282
|
+
try {
|
|
283
|
+
const asyncRoot = path.join(root, "runs");
|
|
284
|
+
const state = makeState("sess-1");
|
|
285
|
+
writeStatus(asyncRoot, "old-complete", "complete", { sessionId: "sess-1" });
|
|
286
|
+
writeStatus(asyncRoot, "run-a", "running", { sessionId: "sess-1", pid: 999999 });
|
|
287
|
+
|
|
288
|
+
const sleep = async () => {
|
|
289
|
+
writeStatus(asyncRoot, "run-a", "complete", { sessionId: "sess-1" });
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const result = await waitForSubagents({ all: true }, undefined, baseDeps(root, state, { sleep }));
|
|
293
|
+
assert.equal(result.isError, undefined);
|
|
294
|
+
const text = textOf(result);
|
|
295
|
+
assert.match(text, /1 complete/);
|
|
296
|
+
assert.doesNotMatch(text, /2 complete/);
|
|
297
|
+
} finally {
|
|
298
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("waits for a remembered detached foreground run by id and ignores other sessions", async () => {
|
|
303
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-foreground-"));
|
|
304
|
+
try {
|
|
305
|
+
const state = makeState("sess-1");
|
|
306
|
+
state.foregroundRuns = new Map([
|
|
307
|
+
["foreground-alpha", {
|
|
308
|
+
runId: "foreground-alpha",
|
|
309
|
+
mode: "single",
|
|
310
|
+
cwd: root,
|
|
311
|
+
sessionId: "sess-1",
|
|
312
|
+
updatedAt: 1,
|
|
313
|
+
children: [{ agent: "reviewer", index: 0, status: "detached", updatedAt: 1 }],
|
|
314
|
+
}],
|
|
315
|
+
["foreground-other", {
|
|
316
|
+
runId: "foreground-other",
|
|
317
|
+
mode: "single",
|
|
318
|
+
cwd: root,
|
|
319
|
+
sessionId: "sess-2",
|
|
320
|
+
updatedAt: 1,
|
|
321
|
+
children: [{ agent: "worker", index: 0, status: "detached", updatedAt: 1 }],
|
|
322
|
+
}],
|
|
323
|
+
]);
|
|
324
|
+
let polls = 0;
|
|
325
|
+
const result = await waitForSubagents({ id: "foreground-al" }, undefined, baseDeps(root, state, {
|
|
326
|
+
sleep: async () => {
|
|
327
|
+
polls += 1;
|
|
328
|
+
state.foregroundRuns!.get("foreground-alpha")!.children[0] = {
|
|
329
|
+
agent: "reviewer",
|
|
330
|
+
index: 0,
|
|
331
|
+
status: "completed",
|
|
332
|
+
finalOutput: "Recovered review",
|
|
333
|
+
updatedAt: 2,
|
|
334
|
+
};
|
|
335
|
+
},
|
|
336
|
+
}));
|
|
337
|
+
|
|
338
|
+
assert.equal(result.isError, undefined);
|
|
339
|
+
assert.match(textOf(result), /remembered detached foreground run "foreground-alpha"/i);
|
|
340
|
+
assert.match(textOf(result), /1 completed/);
|
|
341
|
+
assert.equal(polls, 1);
|
|
342
|
+
|
|
343
|
+
const otherSession = await waitForSubagents({ id: "foreground-other" }, undefined, baseDeps(root, state));
|
|
344
|
+
assert.match(textOf(otherSession), /No active run matched/);
|
|
345
|
+
} finally {
|
|
346
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("wakes when a remembered detached foreground run needs attention", async () => {
|
|
351
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-foreground-attn-"));
|
|
352
|
+
try {
|
|
353
|
+
const state = makeState("sess-1");
|
|
354
|
+
state.foregroundRuns = new Map([["foreground-blocked", {
|
|
355
|
+
runId: "foreground-blocked",
|
|
356
|
+
mode: "single",
|
|
357
|
+
cwd: root,
|
|
358
|
+
sessionId: "sess-1",
|
|
359
|
+
updatedAt: 1,
|
|
360
|
+
children: [{ agent: "researcher", index: 0, status: "detached", updatedAt: 1 }],
|
|
361
|
+
}]]);
|
|
362
|
+
let polls = 0;
|
|
363
|
+
const result = await waitForSubagents({ id: "foreground-blocked", timeoutMs: 30_000 }, undefined, baseDeps(root, state, {
|
|
364
|
+
sleep: async () => {
|
|
365
|
+
polls += 1;
|
|
366
|
+
state.foregroundRuns!.get("foreground-blocked")!.children[0] = {
|
|
367
|
+
agent: "researcher",
|
|
368
|
+
index: 0,
|
|
369
|
+
status: "detached",
|
|
370
|
+
activityState: "needs_attention",
|
|
371
|
+
updatedAt: 2,
|
|
372
|
+
};
|
|
373
|
+
},
|
|
374
|
+
}));
|
|
375
|
+
|
|
376
|
+
assert.equal(result.isError, undefined);
|
|
377
|
+
assert.match(textOf(result), /attention required/i);
|
|
378
|
+
assert.match(textOf(result), /foreground-blocked/);
|
|
379
|
+
assert.match(textOf(result), /researcher#0/);
|
|
380
|
+
assert.match(textOf(result), /Reply to any pending supervisor request/);
|
|
381
|
+
assert.equal(polls, 1);
|
|
382
|
+
} finally {
|
|
383
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it("wakes a remembered foreground wait on a repeated supervisor request event", async () => {
|
|
388
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-foreground-supervisor-"));
|
|
389
|
+
try {
|
|
390
|
+
const state = makeState("sess-1");
|
|
391
|
+
state.foregroundRuns = new Map([["foreground-repeated", {
|
|
392
|
+
runId: "foreground-repeated",
|
|
393
|
+
mode: "single",
|
|
394
|
+
cwd: root,
|
|
395
|
+
sessionId: "sess-1",
|
|
396
|
+
updatedAt: 1,
|
|
397
|
+
children: [{ agent: "worker", index: 0, status: "detached", updatedAt: 1 }],
|
|
398
|
+
}]]);
|
|
399
|
+
const handlers = new Map<string, Array<(data: unknown) => void>>();
|
|
400
|
+
const events = {
|
|
401
|
+
on(channel: string, handler: (data: unknown) => void) {
|
|
402
|
+
const list = handlers.get(channel) ?? [];
|
|
403
|
+
list.push(handler);
|
|
404
|
+
handlers.set(channel, list);
|
|
405
|
+
return () => handlers.set(channel, (handlers.get(channel) ?? []).filter((candidate) => candidate !== handler));
|
|
406
|
+
},
|
|
407
|
+
emit(channel: string, data: unknown) {
|
|
408
|
+
for (const handler of handlers.get(channel) ?? []) handler(data);
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
const startedAt = Date.now();
|
|
412
|
+
const waiting = waitForSubagents({ id: "foreground-repeated", timeoutMs: 30_000 }, undefined, baseDeps(root, state, {
|
|
413
|
+
events,
|
|
414
|
+
pollIntervalMs: 10_000,
|
|
415
|
+
}));
|
|
416
|
+
|
|
417
|
+
setTimeout(() => {
|
|
418
|
+
const child = state.foregroundRuns!.get("foreground-repeated")!.children[0]!;
|
|
419
|
+
child.activityState = "needs_attention";
|
|
420
|
+
events.emit("pi-intercom:detach-request", { runId: "foreground-repeated", childIndex: 0 });
|
|
421
|
+
}, 15);
|
|
422
|
+
|
|
423
|
+
const result = await waiting;
|
|
424
|
+
assert.match(textOf(result), /attention required/i);
|
|
425
|
+
assert.ok(Date.now() - startedAt < 5_000, "supervisor request should wake wait before the poll interval");
|
|
426
|
+
} finally {
|
|
427
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("streams detached foreground transcript activity while waiting", async () => {
|
|
432
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-foreground-progress-"));
|
|
433
|
+
try {
|
|
434
|
+
const transcriptPath = path.join(root, "foreground-transcript.jsonl");
|
|
435
|
+
const ts = Date.now();
|
|
436
|
+
fs.writeFileSync(transcriptPath, [
|
|
437
|
+
JSON.stringify({ version: 1, recordType: "message", sourceEventType: "initial_prompt", role: "user", text: "large delegated task", ts }),
|
|
438
|
+
JSON.stringify({ version: 1, recordType: "message", sourceEventType: "message_end", role: "assistant", text: "I found the relevant renderer path.", ts: ts + 1 }),
|
|
439
|
+
JSON.stringify({ version: 1, recordType: "tool_start", toolName: "edit", argsPreview: "{ path: src/render.ts }", ts: ts + 2 }),
|
|
440
|
+
"{\"recordType\":\"message\"", // Simulate a concurrent partial append.
|
|
441
|
+
].join("\n"), "utf-8");
|
|
442
|
+
|
|
443
|
+
const state = makeState("sess-1");
|
|
444
|
+
state.foregroundRuns = new Map([["foreground-live", {
|
|
445
|
+
runId: "foreground-live",
|
|
446
|
+
mode: "single",
|
|
447
|
+
cwd: root,
|
|
448
|
+
sessionId: "sess-1",
|
|
449
|
+
updatedAt: ts,
|
|
450
|
+
children: [{ agent: "worker", index: 0, status: "detached", transcriptPath, updatedAt: ts }],
|
|
451
|
+
}]]);
|
|
452
|
+
const updates: string[] = [];
|
|
453
|
+
const result = await waitForSubagents({ id: "foreground-live" }, undefined, baseDeps(root, state, {
|
|
454
|
+
onUpdate: (update) => updates.push(textOf(update)),
|
|
455
|
+
sleep: async () => {
|
|
456
|
+
state.foregroundRuns!.get("foreground-live")!.children[0] = {
|
|
457
|
+
agent: "worker",
|
|
458
|
+
index: 0,
|
|
459
|
+
status: "completed",
|
|
460
|
+
finalOutput: "Implemented the renderer fix.",
|
|
461
|
+
transcriptPath,
|
|
462
|
+
updatedAt: ts + 3,
|
|
463
|
+
};
|
|
464
|
+
},
|
|
465
|
+
}));
|
|
466
|
+
|
|
467
|
+
assert.equal(result.isError, undefined);
|
|
468
|
+
assert.equal(updates.length, 1);
|
|
469
|
+
assert.match(updates[0]!, /Waiting for detached foreground run "foreground-live"/);
|
|
470
|
+
assert.match(updates[0]!, /worker · working after supervisor handoff/);
|
|
471
|
+
assert.match(updates[0]!, /current: edit: \{ path: src\/render\.ts \}/);
|
|
472
|
+
assert.match(updates[0]!, /assistant: I found the relevant renderer path\./);
|
|
473
|
+
assert.doesNotMatch(updates[0]!, /large delegated task/);
|
|
474
|
+
} finally {
|
|
475
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it("does not claim completion when a detached foreground run disappears or the active session changes", async () => {
|
|
480
|
+
for (const scenario of ["missing", "session-change"] as const) {
|
|
481
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), `pi-wait-foreground-${scenario}-`));
|
|
482
|
+
try {
|
|
483
|
+
const state = makeState("sess-1");
|
|
484
|
+
state.foregroundRuns = new Map([["foreground-still-live", {
|
|
485
|
+
runId: "foreground-still-live",
|
|
486
|
+
mode: "single",
|
|
487
|
+
cwd: root,
|
|
488
|
+
sessionId: "sess-1",
|
|
489
|
+
updatedAt: 1,
|
|
490
|
+
children: [{ agent: "reviewer", index: 0, status: "detached", updatedAt: 1 }],
|
|
491
|
+
}]]);
|
|
492
|
+
const result = await waitForSubagents({ id: "foreground-still", timeoutMs: 5000 }, undefined, baseDeps(root, state, {
|
|
493
|
+
sleep: async () => {
|
|
494
|
+
if (scenario === "missing") state.foregroundRuns!.delete("foreground-still-live");
|
|
495
|
+
else state.currentSessionId = "sess-2";
|
|
496
|
+
},
|
|
497
|
+
}));
|
|
498
|
+
|
|
499
|
+
assert.equal(result.isError, true);
|
|
500
|
+
assert.doesNotMatch(textOf(result), /; done\./);
|
|
501
|
+
assert.match(textOf(result), scenario === "missing" ? /disappeared before a terminal child result/ : /active session changed/);
|
|
502
|
+
} finally {
|
|
503
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it("rejects ambiguous prefixes across async and remembered foreground runs", async () => {
|
|
509
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-cross-kind-"));
|
|
510
|
+
try {
|
|
511
|
+
const state = makeState("sess-1");
|
|
512
|
+
writeStatus(path.join(root, "runs"), "shared-async", "running", { sessionId: "sess-1", pid: 999999 });
|
|
513
|
+
state.foregroundRuns = new Map([["shared-foreground", {
|
|
514
|
+
runId: "shared-foreground",
|
|
515
|
+
mode: "single",
|
|
516
|
+
cwd: root,
|
|
517
|
+
sessionId: "sess-1",
|
|
518
|
+
updatedAt: 1,
|
|
519
|
+
children: [{ agent: "reviewer", index: 0, status: "detached", updatedAt: 1 }],
|
|
520
|
+
}]]);
|
|
521
|
+
|
|
522
|
+
const result = await waitForSubagents({ id: "shared" }, undefined, baseDeps(root, state));
|
|
523
|
+
assert.equal(result.isError, true);
|
|
524
|
+
assert.match(textOf(result), /Ambiguous subagent run id prefix "shared"/);
|
|
525
|
+
assert.match(textOf(result), /shared-async/);
|
|
526
|
+
assert.match(textOf(result), /shared-foreground/);
|
|
527
|
+
} finally {
|
|
528
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it("preserves current-session prefix matches when a foreign exact id collides", async () => {
|
|
533
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-session-prefix-collision-"));
|
|
534
|
+
try {
|
|
535
|
+
const asyncRoot = path.join(root, "runs");
|
|
536
|
+
const state = makeState("sess-1");
|
|
537
|
+
writeStatus(asyncRoot, "run-al", "running", { sessionId: "sess-2", pid: 999999 });
|
|
538
|
+
writeStatus(asyncRoot, "run-alpha", "running", { sessionId: "sess-1", pid: 999998 });
|
|
539
|
+
|
|
540
|
+
const result = await waitForSubagents({ id: "run-al" }, undefined, baseDeps(root, state, {
|
|
541
|
+
sleep: async () => writeStatus(asyncRoot, "run-alpha", "complete", { sessionId: "sess-1" }),
|
|
542
|
+
}));
|
|
543
|
+
|
|
544
|
+
assert.equal(result.isError, undefined);
|
|
545
|
+
assert.match(textOf(result), /run "run-al".*done/is);
|
|
546
|
+
} finally {
|
|
547
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it("rejects symlinked prefix entries during foreign exact-id fallback", async () => {
|
|
552
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-session-prefix-symlink-"));
|
|
553
|
+
try {
|
|
554
|
+
const asyncRoot = path.join(root, "runs");
|
|
555
|
+
const outsideRoot = path.join(root, "outside");
|
|
556
|
+
const outsideRun = path.join(outsideRoot, "run-alpha");
|
|
557
|
+
const state = makeState("sess-1");
|
|
558
|
+
writeStatus(asyncRoot, "run", "running", { sessionId: "sess-2", pid: 999998 });
|
|
559
|
+
writeStatus(outsideRoot, "run-alpha", "running", { sessionId: "sess-1", pid: 999999 });
|
|
560
|
+
fs.symlinkSync(outsideRun, path.join(asyncRoot, "run-alpha"), "dir");
|
|
561
|
+
const reconciledPids: number[] = [];
|
|
562
|
+
let sleeps = 0;
|
|
563
|
+
|
|
564
|
+
const result = await waitForSubagents({ id: "run" }, undefined, baseDeps(root, state, {
|
|
565
|
+
kill: (pid) => {
|
|
566
|
+
reconciledPids.push(pid);
|
|
567
|
+
return true;
|
|
568
|
+
},
|
|
569
|
+
sleep: async () => {
|
|
570
|
+
sleeps += 1;
|
|
571
|
+
writeStatus(outsideRoot, "run-alpha", "complete", { sessionId: "sess-1" });
|
|
572
|
+
},
|
|
573
|
+
}));
|
|
574
|
+
|
|
575
|
+
assert.equal(result.isError, undefined);
|
|
576
|
+
assert.match(textOf(result), /No active run matched "run"/);
|
|
577
|
+
assert.deepEqual(reconciledPids, [], "foreign and outside runs must not be reconciled");
|
|
578
|
+
assert.equal(sleeps, 0, "outside symlink target must not be returned as active");
|
|
579
|
+
} finally {
|
|
580
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it("rejects symlinked targeted run directories before reconciliation", async () => {
|
|
585
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-symlink-id-"));
|
|
586
|
+
try {
|
|
587
|
+
const asyncRoot = path.join(root, "runs");
|
|
588
|
+
const outsideRun = path.join(root, "outside-run");
|
|
589
|
+
const state = makeState("sess-1");
|
|
590
|
+
writeStatus(path.dirname(outsideRun), path.basename(outsideRun), "running", { sessionId: "sess-1", pid: 999999 });
|
|
591
|
+
fs.mkdirSync(asyncRoot, { recursive: true });
|
|
592
|
+
fs.symlinkSync(outsideRun, path.join(asyncRoot, "link"), "dir");
|
|
593
|
+
let reconciled = false;
|
|
594
|
+
|
|
595
|
+
const result = await waitForSubagents({ id: "link" }, undefined, baseDeps(root, state, {
|
|
596
|
+
kill: () => {
|
|
597
|
+
reconciled = true;
|
|
598
|
+
throw new Error("symlink target should not be reconciled");
|
|
599
|
+
},
|
|
600
|
+
}));
|
|
601
|
+
|
|
602
|
+
assert.equal(result.isError, undefined);
|
|
603
|
+
assert.match(textOf(result), /No active run matched "link"/);
|
|
604
|
+
assert.equal(reconciled, false);
|
|
605
|
+
} finally {
|
|
606
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
it("can target a single run by id prefix", async () => {
|
|
611
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-id-"));
|
|
612
|
+
try {
|
|
613
|
+
const asyncRoot = path.join(root, "runs");
|
|
614
|
+
const state = makeState("sess-1");
|
|
615
|
+
writeStatus(asyncRoot, "run-alpha", "running", { sessionId: "sess-1", pid: 999999 });
|
|
616
|
+
writeStatus(asyncRoot, "run-beta", "running", { sessionId: "sess-1", pid: 999998 });
|
|
617
|
+
|
|
618
|
+
let polls = 0;
|
|
619
|
+
const sleep = async () => {
|
|
620
|
+
polls += 1;
|
|
621
|
+
// Only alpha finishes; beta stays running but we're not waiting on it.
|
|
622
|
+
if (polls === 1) writeStatus(asyncRoot, "run-alpha", "complete", { sessionId: "sess-1" });
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
const result = await waitForSubagents({ id: "run-al" }, undefined, baseDeps(root, state, { sleep }));
|
|
626
|
+
assert.equal(result.isError, undefined);
|
|
627
|
+
assert.match(textOf(result), /run "run-al".*done/is);
|
|
628
|
+
} finally {
|
|
629
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it("rejects ambiguous id prefixes but lets exact ids win", async () => {
|
|
634
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-ambiguous-id-"));
|
|
635
|
+
try {
|
|
636
|
+
const asyncRoot = path.join(root, "runs");
|
|
637
|
+
const state = makeState("sess-1");
|
|
638
|
+
writeStatus(asyncRoot, "run", "running", { sessionId: "sess-1", pid: 999999 });
|
|
639
|
+
writeStatus(asyncRoot, "run-alpha", "running", { sessionId: "sess-1", pid: 999998 });
|
|
640
|
+
|
|
641
|
+
const ambiguous = await waitForSubagents({ id: "ru" }, undefined, baseDeps(root, state));
|
|
642
|
+
assert.equal(ambiguous.isError, true);
|
|
643
|
+
assert.match(textOf(ambiguous), /Ambiguous subagent run id prefix "ru"/);
|
|
644
|
+
assert.match(textOf(ambiguous), /run-alpha/);
|
|
645
|
+
|
|
646
|
+
let polls = 0;
|
|
647
|
+
const exact = await waitForSubagents({ id: "run" }, undefined, baseDeps(root, state, {
|
|
648
|
+
sleep: async () => {
|
|
649
|
+
polls += 1;
|
|
650
|
+
writeStatus(asyncRoot, "run", "complete", { sessionId: "sess-1" });
|
|
651
|
+
},
|
|
652
|
+
}));
|
|
653
|
+
|
|
654
|
+
assert.equal(exact.isError, undefined);
|
|
655
|
+
assert.match(textOf(exact), /run "run".*done/is);
|
|
656
|
+
assert.equal(polls, 1);
|
|
657
|
+
} finally {
|
|
658
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
it("does not reconcile unrelated runs when waiting for an exact id", async () => {
|
|
663
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-exact-id-scan-"));
|
|
664
|
+
try {
|
|
665
|
+
const asyncRoot = path.join(root, "runs");
|
|
666
|
+
const state = makeState("sess-1");
|
|
667
|
+
writeStatus(asyncRoot, "target-run", "running", { sessionId: "sess-1", pid: 999999 });
|
|
668
|
+
writeStatus(asyncRoot, "other-run-a", "running", { sessionId: "sess-1", pid: 999998 });
|
|
669
|
+
writeStatus(asyncRoot, "other-run-b", "running", { sessionId: "sess-1", pid: 999997 });
|
|
670
|
+
|
|
671
|
+
let probes = 0;
|
|
672
|
+
const result = await waitForSubagents({ id: "target-run" }, undefined, baseDeps(root, state, {
|
|
673
|
+
kill: () => {
|
|
674
|
+
probes++;
|
|
675
|
+
return true;
|
|
676
|
+
},
|
|
677
|
+
sleep: async () => writeStatus(asyncRoot, "target-run", "complete", { sessionId: "sess-1" }),
|
|
678
|
+
}));
|
|
679
|
+
|
|
680
|
+
assert.equal(result.isError, undefined);
|
|
681
|
+
assert.match(textOf(result), /run "target-run".*done/is);
|
|
682
|
+
assert.equal(probes, 1, "exact-id waits should reconcile only the selected run");
|
|
683
|
+
} finally {
|
|
684
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
it("rejects dot-segment ids before probing outside the async root", async () => {
|
|
689
|
+
for (const id of [".", ".."]) {
|
|
690
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-dot-id-"));
|
|
691
|
+
try {
|
|
692
|
+
const asyncRoot = path.join(root, "runs");
|
|
693
|
+
const state = makeState("sess-1");
|
|
694
|
+
fs.writeFileSync(path.join(root, "status.json"), JSON.stringify({
|
|
695
|
+
runId: id,
|
|
696
|
+
mode: "single",
|
|
697
|
+
state: "running",
|
|
698
|
+
startedAt: Date.now(),
|
|
699
|
+
lastUpdate: Date.now(),
|
|
700
|
+
sessionId: "sess-1",
|
|
701
|
+
steps: [{ agent: "outside", status: "running" }],
|
|
702
|
+
}), "utf-8");
|
|
703
|
+
|
|
704
|
+
const result = await waitForSubagents({ id }, undefined, baseDeps(root, state));
|
|
705
|
+
|
|
706
|
+
assert.equal(result.isError, undefined);
|
|
707
|
+
assert.ok(textOf(result).includes(`No active run matched "${id}"`));
|
|
708
|
+
assert.equal(fs.existsSync(path.join(asyncRoot, "status.json")), false);
|
|
709
|
+
} finally {
|
|
710
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
it("times out while runs are still active and reports them", async () => {
|
|
716
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-timeout-"));
|
|
717
|
+
try {
|
|
718
|
+
const asyncRoot = path.join(root, "runs");
|
|
719
|
+
const state = makeState("sess-1");
|
|
720
|
+
writeStatus(asyncRoot, "run-stuck", "running", { sessionId: "sess-1", pid: 999999 });
|
|
721
|
+
|
|
722
|
+
// Virtual clock that jumps past the timeout on the first sleep.
|
|
723
|
+
let clock = 0;
|
|
724
|
+
const now = () => clock;
|
|
725
|
+
const sleep = async (ms: number) => {
|
|
726
|
+
clock += ms + 10_000;
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
const result = await waitForSubagents({ timeoutMs: 5_000 }, undefined, baseDeps(root, state, { now, sleep }));
|
|
730
|
+
assert.equal(result.isError, true);
|
|
731
|
+
const text = textOf(result);
|
|
732
|
+
assert.match(text, /timed out/i);
|
|
733
|
+
assert.match(text, /run-stuck \(running\)/);
|
|
734
|
+
} finally {
|
|
735
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
it("resolves early when the turn is aborted", async () => {
|
|
740
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-abort-"));
|
|
741
|
+
try {
|
|
742
|
+
const asyncRoot = path.join(root, "runs");
|
|
743
|
+
const state = makeState("sess-1");
|
|
744
|
+
writeStatus(asyncRoot, "run-x", "running", { sessionId: "sess-1", pid: 999999 });
|
|
745
|
+
|
|
746
|
+
const controller = new AbortController();
|
|
747
|
+
const sleep = async () => {
|
|
748
|
+
controller.abort();
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
const result = await waitForSubagents({}, controller.signal, baseDeps(root, state, { sleep }));
|
|
752
|
+
assert.equal(result.isError, true);
|
|
753
|
+
assert.match(textOf(result), /aborted/i);
|
|
754
|
+
assert.match(textOf(result), /run-x \(running\)/);
|
|
755
|
+
} finally {
|
|
756
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
it("wakes immediately on an event bus emission instead of waiting the poll interval", async () => {
|
|
761
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-event-"));
|
|
762
|
+
try {
|
|
763
|
+
const asyncRoot = path.join(root, "runs");
|
|
764
|
+
const state = makeState("sess-1");
|
|
765
|
+
writeStatus(asyncRoot, "run-a", "running", { sessionId: "sess-1", pid: 999999 });
|
|
766
|
+
|
|
767
|
+
// Fake bus. Emitting on a wake channel should end wait's sleep early.
|
|
768
|
+
const handlers = new Map<string, Array<(d: unknown) => void>>();
|
|
769
|
+
const events = {
|
|
770
|
+
on(channel: string, handler: (d: unknown) => void) {
|
|
771
|
+
const list = handlers.get(channel) ?? [];
|
|
772
|
+
list.push(handler);
|
|
773
|
+
handlers.set(channel, list);
|
|
774
|
+
return () => {
|
|
775
|
+
const l = handlers.get(channel) ?? [];
|
|
776
|
+
handlers.set(channel, l.filter((h) => h !== handler));
|
|
777
|
+
};
|
|
778
|
+
},
|
|
779
|
+
emit(channel: string, data: unknown) {
|
|
780
|
+
for (const h of handlers.get(channel) ?? []) h(data);
|
|
781
|
+
},
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
// A real timer-based sleep with a LONG poll interval; if wait waited for
|
|
785
|
+
// the poll it would take ~10s. The event should wake it in ~10ms.
|
|
786
|
+
let sleepCalls = 0;
|
|
787
|
+
const realSleep = (ms: number, signal?: AbortSignal) => new Promise<void>((resolve) => {
|
|
788
|
+
sleepCalls += 1;
|
|
789
|
+
const t = setTimeout(resolve, ms);
|
|
790
|
+
signal?.addEventListener("abort", () => { clearTimeout(t); resolve(); }, { once: true });
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
const startedAt = Date.now();
|
|
794
|
+
const p = waitForSubagents({ all: true }, undefined, baseDeps(root, state, {
|
|
795
|
+
events,
|
|
796
|
+
pollIntervalMs: 10_000,
|
|
797
|
+
sleep: realSleep,
|
|
798
|
+
}));
|
|
799
|
+
|
|
800
|
+
// After a short delay, flip the run terminal and emit a completion event.
|
|
801
|
+
setTimeout(() => {
|
|
802
|
+
writeStatus(asyncRoot, "run-a", "complete", { sessionId: "sess-1" });
|
|
803
|
+
events.emit("subagent:async-complete", { id: "run-a" });
|
|
804
|
+
}, 15);
|
|
805
|
+
|
|
806
|
+
const result = await p;
|
|
807
|
+
const elapsed = Date.now() - startedAt;
|
|
808
|
+
assert.equal(result.isError, undefined);
|
|
809
|
+
assert.match(textOf(result), /done/i);
|
|
810
|
+
assert.ok(elapsed < 5_000, `should wake via event, not the 10s poll; took ${elapsed}ms`);
|
|
811
|
+
assert.ok(sleepCalls >= 1, "poll-interval sleep still armed as fallback");
|
|
812
|
+
} finally {
|
|
813
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
it("still resolves via poll when no event bus is provided (fallback)", async () => {
|
|
818
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-nobus-"));
|
|
819
|
+
try {
|
|
820
|
+
const asyncRoot = path.join(root, "runs");
|
|
821
|
+
const state = makeState("sess-1");
|
|
822
|
+
writeStatus(asyncRoot, "run-a", "running", { sessionId: "sess-1", pid: 999999 });
|
|
823
|
+
let polls = 0;
|
|
824
|
+
const sleep = async () => {
|
|
825
|
+
polls += 1;
|
|
826
|
+
if (polls === 1) writeStatus(asyncRoot, "run-a", "complete", { sessionId: "sess-1" });
|
|
827
|
+
};
|
|
828
|
+
// No `events` in deps → pure poll path.
|
|
829
|
+
const result = await waitForSubagents({ all: true }, undefined, baseDeps(root, state, { sleep }));
|
|
830
|
+
assert.equal(result.isError, undefined);
|
|
831
|
+
assert.match(textOf(result), /done/i);
|
|
832
|
+
assert.ok(polls >= 1);
|
|
833
|
+
} finally {
|
|
834
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
});
|