@selesai/code 0.5.20 → 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-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 +1 -1
|
@@ -1,57 +1,54 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Completion notification delivery.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* immediately, flushing any held successes first, so failure and attention
|
|
8
|
-
* signals are never delayed.
|
|
4
|
+
* Async result files call this notifier directly and are deleted only after
|
|
5
|
+
* `sendMessage()` accepts the notification. The event bus remains an
|
|
6
|
+
* observation channel, not a delivery acknowledgement.
|
|
9
7
|
*/
|
|
10
8
|
|
|
11
9
|
import type { ExtensionAPI } from "@selesai/code";
|
|
12
|
-
import { buildCompletionKey,
|
|
10
|
+
import { buildCompletionKey, markSeenWithTtl } from "./completion-dedupe.ts";
|
|
13
11
|
import {
|
|
14
12
|
type CompletionBatchConfig,
|
|
15
13
|
type CompletionBatcher,
|
|
16
14
|
createCompletionBatcher,
|
|
17
15
|
resolveCompletionBatchConfig,
|
|
18
16
|
} from "./completion-batcher.ts";
|
|
19
|
-
import { SUBAGENT_ASYNC_COMPLETE_EVENT, type SubagentState } from "../../shared/types.ts";
|
|
20
|
-
|
|
21
|
-
interface ChainStepResult {
|
|
22
|
-
agent: string;
|
|
23
|
-
output: string;
|
|
24
|
-
success: boolean;
|
|
25
|
-
}
|
|
17
|
+
import { SUBAGENT_ASYNC_COMPLETE_EVENT, SUBAGENT_FOREGROUND_COMPLETE_EVENT, type ParallelHandoffReference, type SubagentState } from "../../shared/types.ts";
|
|
26
18
|
|
|
27
19
|
export interface SubagentNotifyDetails {
|
|
28
20
|
agent: string;
|
|
29
21
|
status: "completed" | "failed" | "paused";
|
|
22
|
+
source?: "async" | "foreground";
|
|
30
23
|
taskInfo?: string;
|
|
31
24
|
resultPreview: string;
|
|
32
25
|
durationMs?: number;
|
|
33
26
|
sessionLabel?: string;
|
|
34
27
|
sessionValue?: string;
|
|
28
|
+
handoffPath?: string;
|
|
35
29
|
}
|
|
36
30
|
|
|
37
|
-
interface
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
31
|
+
export interface CompletionNotification {
|
|
32
|
+
[key: string]: unknown;
|
|
33
|
+
id?: string | null;
|
|
34
|
+
source?: "async" | "foreground";
|
|
35
|
+
agent?: string | null;
|
|
36
|
+
success?: boolean;
|
|
37
|
+
summary?: string;
|
|
42
38
|
exitCode?: number;
|
|
43
39
|
state?: string;
|
|
44
|
-
timestamp
|
|
40
|
+
timestamp?: number;
|
|
45
41
|
durationMs?: number;
|
|
46
42
|
cwd?: string;
|
|
47
43
|
sessionFile?: string;
|
|
48
44
|
shareUrl?: string;
|
|
49
45
|
gistUrl?: string;
|
|
50
46
|
shareError?: string;
|
|
51
|
-
results?: ChainStepResult[];
|
|
52
47
|
taskIndex?: number;
|
|
53
48
|
totalTasks?: number;
|
|
54
49
|
sessionId?: string | null;
|
|
50
|
+
triggerTurn?: boolean;
|
|
51
|
+
parallelHandoff?: ParallelHandoffReference;
|
|
55
52
|
}
|
|
56
53
|
|
|
57
54
|
interface NotifyTimerApi {
|
|
@@ -65,6 +62,11 @@ export interface RegisterSubagentNotifyOptions {
|
|
|
65
62
|
now?: () => number;
|
|
66
63
|
}
|
|
67
64
|
|
|
65
|
+
export interface CompletionNotifier {
|
|
66
|
+
deliver(result: CompletionNotification): Promise<boolean>;
|
|
67
|
+
dispose(): void;
|
|
68
|
+
}
|
|
69
|
+
|
|
68
70
|
function formatSessionLine(details: SubagentNotifyDetails): string | undefined {
|
|
69
71
|
if (!details.sessionValue) return undefined;
|
|
70
72
|
return details.sessionLabel ? `${details.sessionLabel}: ${details.sessionValue}` : details.sessionValue;
|
|
@@ -72,10 +74,13 @@ function formatSessionLine(details: SubagentNotifyDetails): string | undefined {
|
|
|
72
74
|
|
|
73
75
|
export function formatSingleCompletion(details: SubagentNotifyDetails): string {
|
|
74
76
|
const sessionLine = formatSessionLine(details);
|
|
77
|
+
const taskKind = details.source === "foreground" ? "Detached foreground task" : "Background task";
|
|
75
78
|
return [
|
|
76
|
-
|
|
79
|
+
`${taskKind} ${details.status}: **${details.agent}**${details.taskInfo ?? ""}`,
|
|
77
80
|
"",
|
|
78
81
|
details.resultPreview.trim() ? details.resultPreview : "(no output)",
|
|
82
|
+
details.handoffPath ? "" : undefined,
|
|
83
|
+
details.handoffPath ? `Parallel handoff: ${details.handoffPath}` : undefined,
|
|
79
84
|
sessionLine ? "" : undefined,
|
|
80
85
|
sessionLine,
|
|
81
86
|
]
|
|
@@ -83,6 +88,43 @@ export function formatSingleCompletion(details: SubagentNotifyDetails): string {
|
|
|
83
88
|
.join("\n");
|
|
84
89
|
}
|
|
85
90
|
|
|
91
|
+
export function parseSubagentNotifyContent(content: string): SubagentNotifyDetails | undefined {
|
|
92
|
+
const lines = content.split("\n");
|
|
93
|
+
const match = (lines[0] ?? "").match(/^(Background task|Detached foreground task) (completed|failed|paused): \*\*(.+?)\*\*(?:\s+(\([^)]*\)))?$/);
|
|
94
|
+
if (!match) return undefined;
|
|
95
|
+
const body = lines.slice(2);
|
|
96
|
+
let sessionIndex = -1;
|
|
97
|
+
for (let i = body.length - 1; i >= 1; i--) {
|
|
98
|
+
if (body[i - 1]?.trim() === "" && /^(Session|Session file|Session share error):\s+/.test(body[i]!)) {
|
|
99
|
+
sessionIndex = i;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const sessionLine = sessionIndex >= 0 ? body[sessionIndex] : undefined;
|
|
104
|
+
const handoffIndex = body.findIndex((line) => line.startsWith("Parallel handoff: "));
|
|
105
|
+
const metadataIndexes = [sessionIndex, handoffIndex].filter((index) => index >= 0);
|
|
106
|
+
const firstMetadataIndex = metadataIndexes.length ? Math.min(...metadataIndexes) : body.length;
|
|
107
|
+
const resultEnd = firstMetadataIndex > 0 && body[firstMetadataIndex - 1]?.trim() === "" ? firstMetadataIndex - 1 : firstMetadataIndex;
|
|
108
|
+
const resultPreview = body.slice(0, resultEnd).join("\n").trim() || "(no output)";
|
|
109
|
+
const handoffPath = handoffIndex >= 0 ? body[handoffIndex]!.slice("Parallel handoff: ".length).trim() : undefined;
|
|
110
|
+
let sessionLabel: string | undefined;
|
|
111
|
+
let sessionValue: string | undefined;
|
|
112
|
+
if (sessionLine) {
|
|
113
|
+
const separator = sessionLine.indexOf(":");
|
|
114
|
+
sessionLabel = sessionLine.slice(0, separator).toLowerCase();
|
|
115
|
+
sessionValue = sessionLine.slice(separator + 1).trim();
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
agent: match[3]!,
|
|
119
|
+
status: match[2] as SubagentNotifyDetails["status"],
|
|
120
|
+
...(match[1] === "Detached foreground task" ? { source: "foreground" as const } : {}),
|
|
121
|
+
...(match[4] ? { taskInfo: match[4] } : {}),
|
|
122
|
+
resultPreview,
|
|
123
|
+
...(handoffPath ? { handoffPath } : {}),
|
|
124
|
+
...(sessionLabel && sessionValue ? { sessionLabel, sessionValue } : {}),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
86
128
|
export function formatGroupedCompletion(details: SubagentNotifyDetails[]): string {
|
|
87
129
|
const header = `Background tasks completed (${details.length}): ${details.map((d) => `**${d.agent}**${d.taskInfo ?? ""}`).join(", ")}`;
|
|
88
130
|
const blocks: string[] = [header, ""];
|
|
@@ -92,35 +134,47 @@ export function formatGroupedCompletion(details: SubagentNotifyDetails[]): strin
|
|
|
92
134
|
const sessionLine = formatSessionLine(detail);
|
|
93
135
|
blocks.push(`${index + 1}. ${detail.agent}${detail.taskInfo ?? ""}`);
|
|
94
136
|
blocks.push(detail.resultPreview.trim() ? detail.resultPreview : "(no output)");
|
|
137
|
+
if (detail.handoffPath) blocks.push(`Parallel handoff: ${detail.handoffPath}`);
|
|
95
138
|
if (sessionLine) blocks.push(sessionLine);
|
|
96
139
|
blocks.push("");
|
|
97
140
|
}
|
|
98
141
|
return blocks.join("\n").trimEnd();
|
|
99
142
|
}
|
|
100
143
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
144
|
+
interface PendingCompletion {
|
|
145
|
+
key: string;
|
|
146
|
+
details: SubagentNotifyDetails;
|
|
147
|
+
triggerTurn: boolean;
|
|
148
|
+
resolve(accepted: boolean): void;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function sendCompletion(pi: Pick<ExtensionAPI, "sendMessage">, items: PendingCompletion[]): boolean {
|
|
152
|
+
if (items.length === 0) return true;
|
|
153
|
+
const details = items.map((item) => item.details);
|
|
154
|
+
const content = details.length === 1 ? formatSingleCompletion(details[0]!) : formatGroupedCompletion(details);
|
|
155
|
+
try {
|
|
156
|
+
pi.sendMessage(
|
|
157
|
+
{
|
|
158
|
+
customType: "subagent-notify",
|
|
159
|
+
content,
|
|
160
|
+
display: true,
|
|
161
|
+
},
|
|
162
|
+
{ triggerTurn: items.some((item) => item.triggerTurn) },
|
|
163
|
+
);
|
|
164
|
+
return true;
|
|
165
|
+
} catch {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
114
168
|
}
|
|
115
169
|
|
|
116
|
-
function completionBatchKey(result:
|
|
170
|
+
function completionBatchKey(result: CompletionNotification): string {
|
|
117
171
|
const sessionId = typeof result.sessionId === "string" ? result.sessionId.trim() : "";
|
|
118
172
|
if (sessionId) return `session:${sessionId}`;
|
|
119
173
|
const cwd = typeof result.cwd === "string" ? result.cwd.trim() : "";
|
|
120
174
|
return cwd ? `cwd:${cwd}` : "unknown";
|
|
121
175
|
}
|
|
122
176
|
|
|
123
|
-
export function buildCompletionDetails(result:
|
|
177
|
+
export function buildCompletionDetails(result: CompletionNotification): SubagentNotifyDetails {
|
|
124
178
|
const agent = result.agent ?? "unknown";
|
|
125
179
|
const summary = typeof result.summary === "string" ? result.summary : "";
|
|
126
180
|
const paused = !result.success && (
|
|
@@ -129,12 +183,15 @@ export function buildCompletionDetails(result: SubagentResult): SubagentNotifyDe
|
|
|
129
183
|
|| summary.startsWith("Paused after interrupt.")
|
|
130
184
|
);
|
|
131
185
|
const status = paused ? "paused" : result.success ? "completed" : "failed";
|
|
132
|
-
|
|
133
186
|
const taskInfo =
|
|
134
187
|
result.taskIndex !== undefined && result.totalTasks !== undefined
|
|
135
188
|
? ` (${result.taskIndex + 1}/${result.totalTasks})`
|
|
136
189
|
: undefined;
|
|
137
190
|
|
|
191
|
+
const parallelHandoff = result.parallelHandoff && typeof result.parallelHandoff === "object"
|
|
192
|
+
? result.parallelHandoff as { path?: unknown }
|
|
193
|
+
: undefined;
|
|
194
|
+
const handoffPath = typeof parallelHandoff?.path === "string" ? parallelHandoff.path : undefined;
|
|
138
195
|
const session =
|
|
139
196
|
result.shareUrl
|
|
140
197
|
? { label: "Session", value: result.shareUrl }
|
|
@@ -143,13 +200,14 @@ export function buildCompletionDetails(result: SubagentResult): SubagentNotifyDe
|
|
|
143
200
|
: result.sessionFile
|
|
144
201
|
? { label: "Session file", value: result.sessionFile }
|
|
145
202
|
: undefined;
|
|
146
|
-
|
|
147
203
|
return {
|
|
148
204
|
agent,
|
|
149
205
|
status,
|
|
206
|
+
...(result.source ? { source: result.source } : {}),
|
|
150
207
|
...(taskInfo ? { taskInfo } : {}),
|
|
151
208
|
resultPreview: summary,
|
|
152
209
|
...(typeof result.durationMs === "number" ? { durationMs: result.durationMs } : {}),
|
|
210
|
+
...(handoffPath ? { handoffPath } : {}),
|
|
153
211
|
...(session ? { sessionLabel: session.label, sessionValue: session.value } : {}),
|
|
154
212
|
};
|
|
155
213
|
}
|
|
@@ -158,68 +216,91 @@ export default function registerSubagentNotify(
|
|
|
158
216
|
pi: ExtensionAPI,
|
|
159
217
|
state: Pick<SubagentState, "currentSessionId">,
|
|
160
218
|
options: RegisterSubagentNotifyOptions = {},
|
|
161
|
-
):
|
|
162
|
-
const
|
|
163
|
-
const
|
|
164
|
-
const globalStore = globalThis as Record<string, unknown>;
|
|
165
|
-
const previousUnsubscribe = globalStore[unsubscribeStoreKey];
|
|
166
|
-
if (typeof previousUnsubscribe === "function") {
|
|
167
|
-
try {
|
|
168
|
-
previousUnsubscribe();
|
|
169
|
-
} catch {
|
|
170
|
-
// Best effort cleanup for stale handlers from an older reload.
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
const previousBatcher = globalStore[batcherStoreKey];
|
|
174
|
-
if (previousBatcher && typeof (previousBatcher as { dispose?: () => void }).dispose === "function") {
|
|
175
|
-
try {
|
|
176
|
-
(previousBatcher as { dispose: () => void }).dispose();
|
|
177
|
-
} catch {
|
|
178
|
-
// Best effort cleanup for a stale batcher from an older reload.
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const seen = getGlobalSeenMap("__pi_subagents_notify_seen__");
|
|
219
|
+
): CompletionNotifier {
|
|
220
|
+
const seen = new Map<string, number>();
|
|
221
|
+
const pending = new Map<string, Promise<boolean>>();
|
|
183
222
|
const ttlMs = 10 * 60 * 1000;
|
|
184
|
-
const
|
|
223
|
+
const now = options.now ?? Date.now;
|
|
185
224
|
const batchConfig = resolveCompletionBatchConfig(options.batchConfig);
|
|
186
|
-
const batchers = new Map<string, CompletionBatcher<
|
|
187
|
-
|
|
188
|
-
dispose() {
|
|
189
|
-
for (const batcher of batchers.values()) batcher.dispose();
|
|
190
|
-
batchers.clear();
|
|
191
|
-
},
|
|
192
|
-
};
|
|
225
|
+
const batchers = new Map<string, CompletionBatcher<PendingCompletion>>();
|
|
226
|
+
let disposed = false;
|
|
193
227
|
|
|
194
|
-
const
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
228
|
+
const settle = (items: PendingCompletion[], accepted: boolean) => {
|
|
229
|
+
for (const item of items) {
|
|
230
|
+
pending.delete(item.key);
|
|
231
|
+
if (accepted) markSeenWithTtl(seen, item.key, now(), ttlMs);
|
|
232
|
+
item.resolve(accepted);
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
const emit = (items: PendingCompletion[]) => settle(items, sendCompletion(pi, items));
|
|
236
|
+
const getBatcher = (result: CompletionNotification) => {
|
|
237
|
+
const key = completionBatchKey(result);
|
|
238
|
+
let batcher = batchers.get(key);
|
|
204
239
|
if (!batcher) {
|
|
205
|
-
batcher = createCompletionBatcher<
|
|
240
|
+
batcher = createCompletionBatcher<PendingCompletion>({
|
|
206
241
|
config: batchConfig,
|
|
207
|
-
emit
|
|
242
|
+
emit,
|
|
208
243
|
...(options.timers ? { timers: options.timers } : {}),
|
|
209
|
-
now
|
|
244
|
+
now,
|
|
210
245
|
});
|
|
211
|
-
batchers.set(
|
|
246
|
+
batchers.set(key, batcher);
|
|
212
247
|
}
|
|
248
|
+
return batcher;
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const deliver = (result: CompletionNotification): Promise<boolean> => {
|
|
252
|
+
if (disposed || typeof result.sessionId !== "string" || result.sessionId !== state.currentSessionId) return Promise.resolve(false);
|
|
253
|
+
const key = buildCompletionKey(result, "notify");
|
|
254
|
+
const seenAt = seen.get(key);
|
|
255
|
+
if (seenAt !== undefined && now() - seenAt <= ttlMs) return Promise.resolve(true);
|
|
256
|
+
if (seenAt !== undefined) seen.delete(key);
|
|
257
|
+
const inFlight = pending.get(key);
|
|
258
|
+
if (inFlight) return inFlight;
|
|
259
|
+
const details = buildCompletionDetails(result);
|
|
260
|
+
let resolve!: (accepted: boolean) => void;
|
|
261
|
+
const completion = new Promise<boolean>((settleCompletion) => { resolve = settleCompletion; });
|
|
262
|
+
pending.set(key, completion);
|
|
263
|
+
const item: PendingCompletion = {
|
|
264
|
+
key,
|
|
265
|
+
details,
|
|
266
|
+
triggerTurn: result.triggerTurn !== false,
|
|
267
|
+
resolve,
|
|
268
|
+
};
|
|
269
|
+
if (details.source === "foreground") {
|
|
270
|
+
emit([item]);
|
|
271
|
+
return completion;
|
|
272
|
+
}
|
|
273
|
+
const batcher = getBatcher(result);
|
|
213
274
|
if (details.status !== "completed") {
|
|
214
|
-
// Failures and paused runs bypass grouping. Flush any held
|
|
215
|
-
// successes for the same owner first so they are not stranded
|
|
216
|
-
// behind this signal, then emit the non-completion result immediately.
|
|
217
275
|
batcher.flush();
|
|
218
|
-
|
|
219
|
-
return;
|
|
276
|
+
emit([item]);
|
|
277
|
+
return completion;
|
|
220
278
|
}
|
|
221
|
-
batcher.push(
|
|
279
|
+
batcher.push(item);
|
|
280
|
+
return completion;
|
|
222
281
|
};
|
|
223
282
|
|
|
224
|
-
|
|
283
|
+
const unsubscribeAsync = pi.events.on(SUBAGENT_ASYNC_COMPLETE_EVENT, (data) => {
|
|
284
|
+
void deliver(data as CompletionNotification);
|
|
285
|
+
});
|
|
286
|
+
const unsubscribeForeground = pi.events.on(SUBAGENT_FOREGROUND_COMPLETE_EVENT, (data) => {
|
|
287
|
+
void deliver(data as CompletionNotification);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
deliver,
|
|
292
|
+
dispose() {
|
|
293
|
+
if (disposed) return;
|
|
294
|
+
disposed = true;
|
|
295
|
+
for (const batcher of batchers.values()) settle(batcher.dispose(), false);
|
|
296
|
+
batchers.clear();
|
|
297
|
+
for (const unsubscribe of [unsubscribeAsync, unsubscribeForeground]) {
|
|
298
|
+
try {
|
|
299
|
+
unsubscribe?.();
|
|
300
|
+
} catch {
|
|
301
|
+
// The runtime is already shutting down; pending records stay on disk.
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
};
|
|
225
306
|
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { writeAtomicJson, writePrivateAtomicJson } from "../../shared/atomic-json.ts";
|
|
4
|
+
import {
|
|
5
|
+
SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
|
|
6
|
+
type AsyncStatus,
|
|
7
|
+
type CanonicalSessionTerminalV1,
|
|
8
|
+
type ProcessInstanceExitV1,
|
|
9
|
+
type ProcessTerminalReason,
|
|
10
|
+
type ProcessTerminalV1,
|
|
11
|
+
} from "../../shared/types.ts";
|
|
12
|
+
import { canonicalSessionId, inspectSessionLease } from "../shared/session-lease.ts";
|
|
13
|
+
|
|
14
|
+
export interface ProcessTerminalCandidate {
|
|
15
|
+
version: 1;
|
|
16
|
+
runId: string;
|
|
17
|
+
runnerProcessInstanceId: string;
|
|
18
|
+
writers: Record<string, ProcessInstanceExitV1[]>;
|
|
19
|
+
expectedWriters?: Record<string, number>;
|
|
20
|
+
sessionFile?: string;
|
|
21
|
+
revivalLeaseToken?: string;
|
|
22
|
+
revivalLeaseReleaseAcknowledged?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface RunnerCloseObservation {
|
|
26
|
+
processInstanceId: string;
|
|
27
|
+
closeObservedAt: number;
|
|
28
|
+
exitCode: number | null;
|
|
29
|
+
signal: string | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
33
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function validProcessInstance(value: unknown, kind?: "runner" | "pi-writer"): value is ProcessInstanceExitV1 {
|
|
37
|
+
if (!isRecord(value)) return false;
|
|
38
|
+
if (typeof value.processInstanceId !== "string" || value.processInstanceId.length === 0) return false;
|
|
39
|
+
if (kind ? value.kind !== kind : (value.kind !== "runner" && value.kind !== "pi-writer")) return false;
|
|
40
|
+
if (typeof value.closeObservedAt !== "number" || !Number.isFinite(value.closeObservedAt)) return false;
|
|
41
|
+
if (typeof value.exitCode !== "number" && value.exitCode !== null) return false;
|
|
42
|
+
if (typeof value.signal !== "string" && value.signal !== null) return false;
|
|
43
|
+
return value.kind === "runner"
|
|
44
|
+
? value.attempt === undefined
|
|
45
|
+
: typeof value.attempt === "number" && Number.isInteger(value.attempt) && value.attempt >= 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function validInstance(value: unknown): value is ProcessInstanceExitV1 {
|
|
49
|
+
return validProcessInstance(value, "pi-writer");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function processTerminalCandidatePath(asyncDir: string): string {
|
|
53
|
+
return path.join(asyncDir, "process-terminal-candidate.json");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function processTerminalPath(asyncDir: string): string {
|
|
57
|
+
return path.join(asyncDir, "process-terminal.json");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function errorMessage(error: unknown): string {
|
|
61
|
+
return error instanceof Error ? error.message : String(error);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function readProcessTerminalCandidate(asyncDir: string): ProcessTerminalCandidate | undefined {
|
|
65
|
+
try {
|
|
66
|
+
const raw = JSON.parse(fs.readFileSync(processTerminalCandidatePath(asyncDir), "utf-8")) as unknown;
|
|
67
|
+
if (!isRecord(raw) || raw.version !== 1 || typeof raw.runId !== "string" || typeof raw.runnerProcessInstanceId !== "string" || !isRecord(raw.writers)) {
|
|
68
|
+
throw new Error(`Invalid process-terminal candidate in '${asyncDir}'.`);
|
|
69
|
+
}
|
|
70
|
+
const writers: Record<string, ProcessInstanceExitV1[]> = {};
|
|
71
|
+
for (const [index, entries] of Object.entries(raw.writers)) {
|
|
72
|
+
if (!Array.isArray(entries) || !entries.every(validInstance)) throw new Error(`Invalid writer process records for child '${index}'.`);
|
|
73
|
+
writers[index] = entries;
|
|
74
|
+
}
|
|
75
|
+
let expectedWriters: Record<string, number> | undefined;
|
|
76
|
+
if (raw.expectedWriters !== undefined) {
|
|
77
|
+
if (!isRecord(raw.expectedWriters)) throw new Error("Invalid expected writer process records.");
|
|
78
|
+
expectedWriters = {};
|
|
79
|
+
for (const [index, count] of Object.entries(raw.expectedWriters)) {
|
|
80
|
+
if (typeof count !== "number" || !Number.isInteger(count) || count < 0) throw new Error(`Invalid expected writer count for child '${index}'.`);
|
|
81
|
+
expectedWriters[index] = count;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (raw.sessionFile !== undefined && typeof raw.sessionFile !== "string") throw new Error("Invalid process-terminal candidate sessionFile.");
|
|
85
|
+
if (raw.revivalLeaseToken !== undefined && typeof raw.revivalLeaseToken !== "string") throw new Error("Invalid process-terminal candidate lease token.");
|
|
86
|
+
if (raw.revivalLeaseReleaseAcknowledged !== undefined && typeof raw.revivalLeaseReleaseAcknowledged !== "boolean") throw new Error("Invalid process-terminal lease release acknowledgement.");
|
|
87
|
+
return {
|
|
88
|
+
version: 1,
|
|
89
|
+
runId: raw.runId,
|
|
90
|
+
runnerProcessInstanceId: raw.runnerProcessInstanceId,
|
|
91
|
+
writers,
|
|
92
|
+
...(expectedWriters ? { expectedWriters } : {}),
|
|
93
|
+
...(raw.sessionFile ? { sessionFile: raw.sessionFile } : {}),
|
|
94
|
+
...(raw.revivalLeaseToken ? { revivalLeaseToken: raw.revivalLeaseToken } : {}),
|
|
95
|
+
...(raw.revivalLeaseReleaseAcknowledged !== undefined ? { revivalLeaseReleaseAcknowledged: raw.revivalLeaseReleaseAcknowledged } : {}),
|
|
96
|
+
};
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function writeProcessTerminalCandidate(asyncDir: string, candidate: ProcessTerminalCandidate): void {
|
|
104
|
+
writePrivateAtomicJson(processTerminalCandidatePath(asyncDir), candidate);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function markProcessTerminalCandidateLeaseRelease(asyncDir: string, token: string, acknowledged: boolean): void {
|
|
108
|
+
const candidate = readProcessTerminalCandidate(asyncDir);
|
|
109
|
+
if (!candidate || candidate.revivalLeaseToken !== token) return;
|
|
110
|
+
writeProcessTerminalCandidate(asyncDir, { ...candidate, revivalLeaseReleaseAcknowledged: acknowledged });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function unknownProof(runId: string, runnerProcessInstanceId: string, reason: ProcessTerminalReason, diagnostic?: string): ProcessTerminalV1 {
|
|
114
|
+
return { version: 1, state: "unknown", runId, runnerProcessInstanceId, reason, ...(diagnostic ? { diagnostic } : {}) };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function resumeDisposition(state: string | undefined, sessionFile: string | undefined): "resumable" | "non-resumable" | "unavailable" {
|
|
118
|
+
if (state === "stopped") return "non-resumable";
|
|
119
|
+
if (state !== "complete" && state !== "completed" && state !== "failed" && state !== "paused") return "unavailable";
|
|
120
|
+
return sessionFile && fs.existsSync(sessionFile) ? "resumable" : "unavailable";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function sessionProjection(candidate: ProcessTerminalCandidate, lease: ReturnType<typeof inspectSessionLease>): CanonicalSessionTerminalV1 | undefined {
|
|
124
|
+
if (!candidate.sessionFile || lease.state !== "free") return undefined;
|
|
125
|
+
if (candidate.revivalLeaseToken && candidate.revivalLeaseReleaseAcknowledged !== true) return undefined;
|
|
126
|
+
return {
|
|
127
|
+
canonicalSessionId: canonicalSessionId(candidate.sessionFile),
|
|
128
|
+
leaseDisposition: candidate.revivalLeaseToken ? "released" : "not-held",
|
|
129
|
+
freeAtObservation: true,
|
|
130
|
+
...(candidate.revivalLeaseToken ? { canonicalSessionLeaseReleased: true } : {}),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function validateProof(raw: unknown, asyncDir: string, fallback?: { runId?: string; runnerProcessInstanceId?: string }): raw is ProcessTerminalV1 {
|
|
135
|
+
if (!isRecord(raw) || raw.version !== 1 || !["pending", "observed", "unknown", "not-started"].includes(String(raw.state)) || typeof raw.runId !== "string" || !raw.runId || typeof raw.runnerProcessInstanceId !== "string" || !raw.runnerProcessInstanceId) {
|
|
136
|
+
throw new Error(`Invalid process-terminal proof in '${asyncDir}'.`);
|
|
137
|
+
}
|
|
138
|
+
if (fallback?.runId && raw.runId !== fallback.runId) throw new Error(`Process-terminal proof in '${asyncDir}' belongs to run '${raw.runId}', expected '${fallback.runId}'.`);
|
|
139
|
+
if (fallback?.runnerProcessInstanceId && raw.runnerProcessInstanceId !== fallback.runnerProcessInstanceId) throw new Error(`Process-terminal proof in '${asyncDir}' belongs to runner '${raw.runnerProcessInstanceId}', expected '${fallback.runnerProcessInstanceId}'.`);
|
|
140
|
+
if (raw.instances !== undefined && (!Array.isArray(raw.instances) || !raw.instances.every((entry) => validProcessInstance(entry)))) {
|
|
141
|
+
throw new Error(`Invalid process-terminal instances in '${asyncDir}'.`);
|
|
142
|
+
}
|
|
143
|
+
if (raw.state === "observed") {
|
|
144
|
+
if (typeof raw.observedAt !== "number" || !Number.isFinite(raw.observedAt)) throw new Error(`Observed process-terminal proof in '${asyncDir}' is missing observedAt.`);
|
|
145
|
+
if (!Array.isArray(raw.instances)) throw new Error(`Observed process-terminal proof in '${asyncDir}' is missing instances.`);
|
|
146
|
+
const runner = raw.instances.find((entry) => isRecord(entry) && entry.kind === "runner");
|
|
147
|
+
if (!validProcessInstance(runner, "runner") || runner.processInstanceId !== raw.runnerProcessInstanceId) throw new Error(`Observed process-terminal proof in '${asyncDir}' has no matching runner instance.`);
|
|
148
|
+
}
|
|
149
|
+
if (raw.resumeDisposition !== undefined && !["resumable", "non-resumable", "unavailable"].includes(String(raw.resumeDisposition))) throw new Error(`Invalid process-terminal resume disposition in '${asyncDir}'.`);
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function sanitizeProcessTerminal(value: unknown, fallback: { runId?: string; runnerProcessInstanceId?: string }, label = "status"): ProcessTerminalV1 | undefined {
|
|
154
|
+
if (value === undefined) return undefined;
|
|
155
|
+
try {
|
|
156
|
+
validateProof(value, label, fallback);
|
|
157
|
+
return value;
|
|
158
|
+
} catch (error) {
|
|
159
|
+
return unknownProof(fallback.runId ?? label, fallback.runnerProcessInstanceId ?? "unknown", "proof-write-failed", errorMessage(error));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function readProcessTerminal(asyncDir: string, fallback?: { runId?: string; runnerProcessInstanceId?: string }): ProcessTerminalV1 | undefined {
|
|
164
|
+
try {
|
|
165
|
+
const raw = JSON.parse(fs.readFileSync(processTerminalPath(asyncDir), "utf-8")) as unknown;
|
|
166
|
+
validateProof(raw, asyncDir, fallback);
|
|
167
|
+
return raw;
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
170
|
+
return unknownProof(fallback?.runId ?? path.basename(asyncDir), fallback?.runnerProcessInstanceId ?? "unknown", "proof-write-failed", errorMessage(error));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function stepProcessTerminalProof(
|
|
175
|
+
proof: ProcessTerminalV1,
|
|
176
|
+
childIndex: number,
|
|
177
|
+
state: ProcessTerminalV1["state"],
|
|
178
|
+
records: ProcessInstanceExitV1[],
|
|
179
|
+
resumeDispositionValue: ProcessTerminalV1["resumeDisposition"],
|
|
180
|
+
): ProcessTerminalV1 {
|
|
181
|
+
const base = {
|
|
182
|
+
version: 1 as const,
|
|
183
|
+
runId: proof.runId,
|
|
184
|
+
childIndex,
|
|
185
|
+
runnerProcessInstanceId: proof.runnerProcessInstanceId,
|
|
186
|
+
...(resumeDispositionValue ? { resumeDisposition: resumeDispositionValue } : {}),
|
|
187
|
+
};
|
|
188
|
+
if (state === "observed") {
|
|
189
|
+
return { ...base, state, observedAt: proof.state === "observed" ? proof.observedAt : Date.now(), instances: records };
|
|
190
|
+
}
|
|
191
|
+
if (state === "unknown") {
|
|
192
|
+
return { ...base, state, reason: proof.state === "unknown" ? proof.reason : "writer-close-unverified" };
|
|
193
|
+
}
|
|
194
|
+
return { ...base, state };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function overlayStatus(asyncDir: string, proof: ProcessTerminalV1, candidate?: ProcessTerminalCandidate): void {
|
|
198
|
+
const statusPath = path.join(asyncDir, "status.json");
|
|
199
|
+
try {
|
|
200
|
+
const status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatus;
|
|
201
|
+
status.processTerminal = proof;
|
|
202
|
+
if (status.steps) {
|
|
203
|
+
for (const [index, step] of status.steps.entries()) {
|
|
204
|
+
const records = candidate?.writers[String(index)] ?? [];
|
|
205
|
+
const expected = candidate?.expectedWriters?.[String(index)] ?? (records.length > 0 ? records.length : 0);
|
|
206
|
+
const stepState = expected === 0 ? "not-started" : proof.state === "observed" && records.length === expected ? "observed" : proof.state === "pending" ? "pending" : "unknown";
|
|
207
|
+
step.processTerminal = stepProcessTerminalProof(proof, index, stepState, records, resumeDisposition(step.status, step.sessionFile ?? candidate?.sessionFile));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
writeAtomicJson(statusPath, status);
|
|
211
|
+
} catch {
|
|
212
|
+
// The proof sidecar remains authoritative when terminal status is unavailable.
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function finalizeProcessTerminal(
|
|
217
|
+
asyncDir: string,
|
|
218
|
+
runId: string,
|
|
219
|
+
runnerClose: RunnerCloseObservation,
|
|
220
|
+
): ProcessTerminalV1 {
|
|
221
|
+
const existing = readProcessTerminal(asyncDir, { runId, runnerProcessInstanceId: runnerClose.processInstanceId });
|
|
222
|
+
if (existing && fs.existsSync(processTerminalPath(asyncDir))) {
|
|
223
|
+
if (existing.state === "observed" && existing.runId === runId && existing.runnerProcessInstanceId === runnerClose.processInstanceId) return existing;
|
|
224
|
+
if (existing.state === "unknown") return existing;
|
|
225
|
+
}
|
|
226
|
+
let proof: ProcessTerminalV1;
|
|
227
|
+
let candidateForOverlay: ProcessTerminalCandidate | undefined;
|
|
228
|
+
try {
|
|
229
|
+
const candidate = readProcessTerminalCandidate(asyncDir);
|
|
230
|
+
candidateForOverlay = candidate;
|
|
231
|
+
if (!candidate) proof = unknownProof(runId, runnerClose.processInstanceId, "runner-candidate-missing");
|
|
232
|
+
else if (candidate.runId !== runId || candidate.runnerProcessInstanceId !== runnerClose.processInstanceId) proof = unknownProof(runId, runnerClose.processInstanceId, "runner-instance-mismatch");
|
|
233
|
+
else {
|
|
234
|
+
const allWriters = Object.values(candidate.writers).flat();
|
|
235
|
+
const status = (() => {
|
|
236
|
+
try { return JSON.parse(fs.readFileSync(path.join(asyncDir, "status.json"), "utf-8")) as AsyncStatus; } catch { return undefined; }
|
|
237
|
+
})();
|
|
238
|
+
const session = candidate.sessionFile ? inspectSessionLease(candidate.sessionFile) : undefined;
|
|
239
|
+
const writerEntries = Object.entries(candidate.writers);
|
|
240
|
+
const expectedWriters = candidate.expectedWriters ?? Object.fromEntries(writerEntries.map(([index, records]) => [index, records.length]));
|
|
241
|
+
const expectedEntries = Object.entries(expectedWriters);
|
|
242
|
+
const expectedIndexes = new Set(expectedEntries.map(([index]) => index));
|
|
243
|
+
const writerIndexes = new Set(writerEntries.map(([index]) => index));
|
|
244
|
+
const inconsistentWriters = writerEntries.some(([index, records]) => !expectedIndexes.has(index) || records.length !== expectedWriters[index])
|
|
245
|
+
|| expectedEntries.some(([index, expected]) => !writerIndexes.has(index) && expected !== 0);
|
|
246
|
+
if (session && session.state !== "free") {
|
|
247
|
+
proof = unknownProof(runId, runnerClose.processInstanceId, session.state === "owned" ? "canonical-session-lease-active" : "canonical-session-unavailable");
|
|
248
|
+
} else if (candidate.revivalLeaseToken && candidate.revivalLeaseReleaseAcknowledged !== true) {
|
|
249
|
+
proof = unknownProof(runId, runnerClose.processInstanceId, "canonical-session-release-unverified");
|
|
250
|
+
} else if (inconsistentWriters || (allWriters.length === 0 && expectedEntries.length === 0)) {
|
|
251
|
+
proof = unknownProof(runId, runnerClose.processInstanceId, "writer-close-unverified");
|
|
252
|
+
} else {
|
|
253
|
+
const runner: ProcessInstanceExitV1 = { kind: "runner", ...runnerClose };
|
|
254
|
+
const canonicalSession = session && sessionProjection(candidate, session);
|
|
255
|
+
proof = {
|
|
256
|
+
version: 1,
|
|
257
|
+
state: "observed",
|
|
258
|
+
runId,
|
|
259
|
+
runnerProcessInstanceId: runnerClose.processInstanceId,
|
|
260
|
+
observedAt: runnerClose.closeObservedAt,
|
|
261
|
+
instances: [runner, ...allWriters],
|
|
262
|
+
resumeDisposition: resumeDisposition(status?.state, candidate.sessionFile ?? status?.sessionFile),
|
|
263
|
+
...(canonicalSession ? { canonicalSession } : {}),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
} catch (error) {
|
|
268
|
+
proof = unknownProof(runId, runnerClose.processInstanceId, "proof-write-failed", errorMessage(error));
|
|
269
|
+
}
|
|
270
|
+
let durable = false;
|
|
271
|
+
try {
|
|
272
|
+
writeAtomicJson(processTerminalPath(asyncDir), proof);
|
|
273
|
+
durable = true;
|
|
274
|
+
overlayStatus(asyncDir, proof, candidateForOverlay);
|
|
275
|
+
fs.appendFileSync(path.join(asyncDir, "events.jsonl"), `${JSON.stringify({ type: "subagent.run.process_terminal", lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION, ts: Date.now(), runId, processTerminal: proof })}\n`, "utf-8");
|
|
276
|
+
} catch {
|
|
277
|
+
// Do not emit a process-terminal event when the proof sidecar was not durable.
|
|
278
|
+
}
|
|
279
|
+
return durable ? proof : unknownProof(runId, runnerClose.processInstanceId, "proof-write-failed", "Failed to persist process-terminal proof.");
|
|
280
|
+
}
|