@selesai/code 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/diagnostics.d.ts +1 -0
- package/dist/core/diagnostics.d.ts.map +1 -1
- package/dist/core/diagnostics.js.map +1 -1
- package/dist/core/package-manager.d.ts +10 -0
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +93 -8
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/package-manager.test.d.ts +2 -0
- package/dist/core/package-manager.test.d.ts.map +1 -0
- package/dist/core/package-manager.test.js +20 -0
- package/dist/core/package-manager.test.js.map +1 -0
- package/dist/core/resource-loader.d.ts +3 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +10 -0
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/settings-manager.d.ts +1 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/extensions/caveman/caveman-instructions.cjs +39 -0
- package/dist/extensions/caveman/index.js +116 -0
- package/dist/extensions/caveman/package.json +8 -0
- package/dist/extensions/caveman/test/extension.test.js +185 -0
- package/dist/extensions/caveman/test/helpers.test.js +67 -0
- package/dist/extensions/pi-intercom/broker/broker.ts +2 -3
- package/dist/extensions/pi-intercom/broker/paths.ts +10 -5
- package/dist/extensions/pi-intercom/broker/spawn.ts +2 -3
- package/dist/extensions/pi-intercom/config.ts +2 -2
- package/dist/extensions/pi-intercom/index.ts +2 -2
- package/dist/extensions/pi-intercom/package.json +2 -2
- package/dist/extensions/pi-intercom/ui/compose.ts +3 -3
- package/dist/extensions/pi-intercom/ui/inline-message.ts +3 -3
- package/dist/extensions/pi-intercom/ui/session-list.ts +3 -3
- package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +2 -1
- package/dist/extensions/pi-powerline-footer/index.ts +9 -16
- package/dist/extensions/pi-powerline-footer/welcome.ts +23 -16
- package/dist/extensions/pi-powerline-footer/working-vibes.ts +2 -9
- package/dist/extensions/pi-rewind-hook/index.ts +1 -1
- package/dist/extensions/pi-rewind-hook/package.json +1 -1
- package/dist/extensions/pi-subagents/README.md +224 -52
- package/dist/extensions/pi-subagents/install.mjs +93 -0
- package/dist/extensions/pi-subagents/package.json +4 -5
- package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +1 -1
- package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +76 -13
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +179 -2
- package/dist/extensions/pi-subagents/src/agents/agent-memory.ts +254 -0
- package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +11 -0
- package/dist/extensions/pi-subagents/src/agents/agents.ts +193 -19
- package/dist/extensions/pi-subagents/src/agents/chain-serializer.ts +27 -2
- package/dist/extensions/pi-subagents/src/extension/config.ts +27 -4
- package/dist/extensions/pi-subagents/src/extension/doctor.ts +1 -7
- package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +3 -2
- package/dist/extensions/pi-subagents/src/extension/index.ts +73 -71
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +369 -0
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +54 -10
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +200 -0
- package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +21 -253
- package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +519 -0
- package/dist/extensions/pi-subagents/src/profiles/profiles.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +195 -105
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +88 -2
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +67 -10
- package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +34 -4
- package/dist/extensions/pi-subagents/src/runs/background/completion-batcher.ts +166 -0
- package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +156 -1
- package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +515 -0
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +161 -44
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +1 -2
- package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +3 -2
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +167 -6
- package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +514 -0
- package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +28 -1
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +873 -204
- package/dist/extensions/pi-subagents/src/runs/background/wait.ts +353 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +123 -27
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +174 -70
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +569 -81
- package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +45 -22
- package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +3 -1
- package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +172 -21
- package/dist/extensions/pi-subagents/src/runs/shared/model-scope.ts +128 -0
- package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +89 -0
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +50 -1
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +35 -4
- package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +63 -45
- package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +2 -0
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +125 -5
- package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +74 -0
- package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +52 -0
- package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +28 -5
- package/dist/extensions/pi-subagents/src/shared/artifacts.ts +16 -1
- package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +15 -2
- package/dist/extensions/pi-subagents/src/shared/child-transcript.ts +212 -0
- package/dist/extensions/pi-subagents/src/shared/fork-context.ts +133 -22
- package/dist/extensions/pi-subagents/src/shared/settings.ts +3 -1
- package/dist/extensions/pi-subagents/src/shared/types.ts +197 -4
- package/dist/extensions/pi-subagents/src/shared/utils.ts +100 -15
- package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +330 -0
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +135 -4
- package/dist/extensions/pi-subagents/src/tui/render.ts +32 -12
- package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +94 -0
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2772 -0
- package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +1107 -0
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +463 -0
- package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +274 -0
- package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1535 -0
- package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +242 -0
- package/dist/extensions/pi-subagents/test/integration/doctor-executor.test.ts +128 -0
- package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +252 -0
- package/dist/extensions/pi-subagents/test/integration/foreground-result-size.test.ts +188 -0
- package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +1306 -0
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +1033 -0
- package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +397 -0
- package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +583 -0
- package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +691 -0
- package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +892 -0
- package/dist/extensions/pi-subagents/test/integration/session-tokens.test.ts +62 -0
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1824 -0
- package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +1284 -0
- package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +127 -0
- package/dist/extensions/pi-subagents/test/integration/template-resolution.test.ts +299 -0
- package/dist/extensions/pi-subagents/test/integration/top-level-async.test.ts +37 -0
- package/dist/extensions/pi-subagents/test/support/helpers.ts +189 -0
- package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +335 -0
- package/dist/extensions/pi-subagents/test/support/mock-pi.ts +129 -0
- package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +184 -0
- package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +285 -0
- package/dist/extensions/pi-subagents/test/support/register-loader.mjs +15 -0
- package/dist/extensions/pi-subagents/test/support/ts-loader.mjs +108 -0
- package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +421 -0
- package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +213 -0
- package/dist/extensions/pi-subagents/test/unit/agent-eject-disable.test.ts +343 -0
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +1199 -0
- package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +556 -0
- package/dist/extensions/pi-subagents/test/unit/agent-memory.test.ts +420 -0
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +579 -0
- package/dist/extensions/pi-subagents/test/unit/agent-scope.test.ts +20 -0
- package/dist/extensions/pi-subagents/test/unit/agent-selection.test.ts +88 -0
- package/dist/extensions/pi-subagents/test/unit/artifacts.test.ts +24 -0
- package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +86 -0
- package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +221 -0
- package/dist/extensions/pi-subagents/test/unit/async-permission-session.test.ts +98 -0
- package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +285 -0
- package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +112 -0
- package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +206 -0
- package/dist/extensions/pi-subagents/test/unit/chain-root-attachment.test.ts +159 -0
- package/dist/extensions/pi-subagents/test/unit/chain-serializer.test.ts +229 -0
- package/dist/extensions/pi-subagents/test/unit/child-transcript.test.ts +225 -0
- package/dist/extensions/pi-subagents/test/unit/close-grace-timer.test.ts +101 -0
- package/dist/extensions/pi-subagents/test/unit/completion-batcher.test.ts +228 -0
- package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +36 -0
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +152 -0
- package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +72 -0
- package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +327 -0
- package/dist/extensions/pi-subagents/test/unit/control-notices.test.ts +161 -0
- package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +159 -0
- package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +217 -0
- package/dist/extensions/pi-subagents/test/unit/extra-agent-dirs.test.ts +88 -0
- package/dist/extensions/pi-subagents/test/unit/file-coalescer.test.ts +68 -0
- package/dist/extensions/pi-subagents/test/unit/foreground-tool-call-compaction.test.ts +78 -0
- package/dist/extensions/pi-subagents/test/unit/fork-context.test.ts +415 -0
- package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +126 -0
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +271 -0
- package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +173 -0
- package/dist/extensions/pi-subagents/test/unit/jsonl-writer.test.ts +119 -0
- package/dist/extensions/pi-subagents/test/unit/live-async-resume.test.ts +170 -0
- package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +346 -0
- package/dist/extensions/pi-subagents/test/unit/model-info.test.ts +62 -0
- package/dist/extensions/pi-subagents/test/unit/model-scope-settings.test.ts +87 -0
- package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +145 -0
- package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +162 -0
- package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +470 -0
- package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +329 -0
- package/dist/extensions/pi-subagents/test/unit/notify.test.ts +319 -0
- package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +70 -0
- package/dist/extensions/pi-subagents/test/unit/parallel-utils.test.ts +247 -0
- package/dist/extensions/pi-subagents/test/unit/path-handling.test.ts +113 -0
- package/dist/extensions/pi-subagents/test/unit/path-resolution.test.ts +94 -0
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +803 -0
- package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +209 -0
- package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +238 -0
- package/dist/extensions/pi-subagents/test/unit/proactive-skills.test.ts +118 -0
- package/dist/extensions/pi-subagents/test/unit/profiles.test.ts +299 -0
- package/dist/extensions/pi-subagents/test/unit/prompt-template-bridge.test.ts +408 -0
- package/dist/extensions/pi-subagents/test/unit/prompt-workflows.test.ts +134 -0
- package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +257 -0
- package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +209 -0
- package/dist/extensions/pi-subagents/test/unit/result-intercom.test.ts +226 -0
- package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +294 -0
- package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +147 -0
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +997 -0
- package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +478 -0
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +562 -0
- package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +213 -0
- package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +438 -0
- package/dist/extensions/pi-subagents/test/unit/slash-bridge.test.ts +54 -0
- package/dist/extensions/pi-subagents/test/unit/slash-chain-groups.test.ts +464 -0
- package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +255 -0
- package/dist/extensions/pi-subagents/test/unit/status-format.test.ts +19 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +245 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +517 -0
- package/dist/extensions/pi-subagents/test/unit/temp-paths.test.ts +72 -0
- package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +52 -0
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +237 -0
- package/dist/extensions/pi-subagents/test/unit/total-cost.test.ts +60 -0
- package/dist/extensions/pi-subagents/test/unit/ts-loader.test.ts +33 -0
- package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +202 -0
- package/dist/extensions/pi-subagents/test/unit/types-fork-preamble.test.ts +23 -0
- package/dist/extensions/pi-subagents/test/unit/wait.test.ts +398 -0
- package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +88 -0
- package/dist/extensions/pi-subagents/test/unit/windows-hide-spawn.test.ts +26 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-graph.test.ts +134 -0
- package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +479 -0
- package/dist/extensions/pi-web-agent/src/changelog-notice.ts +5 -2
- package/dist/extensions/pi-web-agent/src/presentation/config-store.ts +6 -4
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +4 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/first-time-setup.d.ts +1 -1
- package/dist/modes/interactive/components/first-time-setup.d.ts.map +1 -1
- package/dist/modes/interactive/components/first-time-setup.js +19 -5
- package/dist/modes/interactive/components/first-time-setup.js.map +1 -1
- package/dist/skills/caveman/SKILL.md +49 -0
- package/dist/skills/handoff-text/SKILL.md +13 -0
- package/docs/docs.json +4 -0
- package/docs/extensions.md +2 -0
- package/docs/settings.md +15 -0
- package/docs/shared-host-extensions.md +109 -0
- package/package.json +1 -1
|
@@ -17,6 +17,7 @@ import { readStatus } from "../../shared/utils.ts";
|
|
|
17
17
|
import { normalizeParallelGroups } from "./parallel-groups.ts";
|
|
18
18
|
import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-reconciler.ts";
|
|
19
19
|
import { hasLiveNestedDescendants, updateAsyncJobNestedProjection } from "../shared/nested-events.ts";
|
|
20
|
+
import { listAsyncRuns, type AsyncRunSummary } from "./async-status.ts";
|
|
20
21
|
|
|
21
22
|
interface AsyncJobTrackerOptions {
|
|
22
23
|
completionRetentionMs?: number;
|
|
@@ -35,6 +36,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
35
36
|
handleStarted: (data: unknown) => void;
|
|
36
37
|
handleComplete: (data: unknown) => void;
|
|
37
38
|
resetJobs: (ctx?: ExtensionContext) => void;
|
|
39
|
+
restoreActiveJobs: (ctx?: ExtensionContext) => void;
|
|
38
40
|
} {
|
|
39
41
|
const completionRetentionMs = options.completionRetentionMs ?? 10000;
|
|
40
42
|
const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
@@ -43,6 +45,61 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
43
45
|
renderWidget(ctx, jobs);
|
|
44
46
|
ctx.ui.requestRender?.();
|
|
45
47
|
};
|
|
48
|
+
const restoredControlEventCursor = (asyncDir: string) => {
|
|
49
|
+
try {
|
|
50
|
+
return fs.statSync(path.join(asyncDir, "events.jsonl")).size;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return 0;
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const summaryToJob = (run: AsyncRunSummary): AsyncJobState => {
|
|
57
|
+
const groups = normalizeParallelGroups(run.parallelGroups, run.steps.length, run.chainStepCount ?? run.steps.length);
|
|
58
|
+
const activeGroup = run.currentStep !== undefined
|
|
59
|
+
? groups.find((group) => run.currentStep! >= group.start && run.currentStep! < group.start + group.count)
|
|
60
|
+
: undefined;
|
|
61
|
+
const visibleSteps = activeGroup
|
|
62
|
+
? run.steps.slice(activeGroup.start, activeGroup.start + activeGroup.count).map((step, index) => ({ ...step, index: activeGroup.start + index }))
|
|
63
|
+
: run.steps.map((step, index) => ({ ...step, index }));
|
|
64
|
+
return {
|
|
65
|
+
asyncId: run.id,
|
|
66
|
+
asyncDir: run.asyncDir,
|
|
67
|
+
status: run.state,
|
|
68
|
+
sessionId: run.sessionId,
|
|
69
|
+
activityState: run.activityState,
|
|
70
|
+
lastActivityAt: run.lastActivityAt,
|
|
71
|
+
currentTool: run.currentTool,
|
|
72
|
+
currentToolStartedAt: run.currentToolStartedAt,
|
|
73
|
+
currentPath: run.currentPath,
|
|
74
|
+
turnCount: run.turnCount,
|
|
75
|
+
toolCount: run.toolCount,
|
|
76
|
+
mode: run.mode,
|
|
77
|
+
agents: visibleSteps.map((step) => step.agent),
|
|
78
|
+
currentStep: run.currentStep,
|
|
79
|
+
chainStepCount: run.chainStepCount,
|
|
80
|
+
parallelGroups: groups,
|
|
81
|
+
steps: visibleSteps,
|
|
82
|
+
stepsTotal: visibleSteps.length,
|
|
83
|
+
runningSteps: visibleSteps.filter((step) => step.status === "running").length,
|
|
84
|
+
completedSteps: visibleSteps.filter((step) => step.status === "complete" || step.status === "completed").length,
|
|
85
|
+
hasParallelGroups: groups.length > 0,
|
|
86
|
+
activeParallelGroup: Boolean(activeGroup),
|
|
87
|
+
startedAt: run.startedAt,
|
|
88
|
+
updatedAt: run.lastUpdate ?? run.startedAt,
|
|
89
|
+
timeoutMs: run.timeoutMs,
|
|
90
|
+
deadlineAt: run.deadlineAt,
|
|
91
|
+
timedOut: run.timedOut,
|
|
92
|
+
turnBudget: run.turnBudget,
|
|
93
|
+
turnBudgetExceeded: run.turnBudgetExceeded,
|
|
94
|
+
wrapUpRequested: run.wrapUpRequested,
|
|
95
|
+
sessionDir: run.sessionDir,
|
|
96
|
+
outputFile: run.outputFile,
|
|
97
|
+
totalTokens: run.totalTokens,
|
|
98
|
+
sessionFile: run.sessionFile,
|
|
99
|
+
controlEventCursor: restoredControlEventCursor(run.asyncDir),
|
|
100
|
+
nestedChildren: run.nestedChildren,
|
|
101
|
+
};
|
|
102
|
+
};
|
|
46
103
|
const cancelCleanup = (asyncId: string) => {
|
|
47
104
|
const existingTimer = state.cleanupTimers.get(asyncId);
|
|
48
105
|
if (!existingTimer) return;
|
|
@@ -248,6 +305,12 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
248
305
|
job.sessionDir = status.sessionDir ?? job.sessionDir;
|
|
249
306
|
job.outputFile = status.outputFile ?? job.outputFile;
|
|
250
307
|
job.totalTokens = status.totalTokens ?? job.totalTokens;
|
|
308
|
+
job.timeoutMs = status.timeoutMs ?? job.timeoutMs;
|
|
309
|
+
job.deadlineAt = status.deadlineAt ?? job.deadlineAt;
|
|
310
|
+
job.timedOut = status.timedOut ?? job.timedOut;
|
|
311
|
+
job.turnBudget = status.turnBudget ?? job.turnBudget;
|
|
312
|
+
job.turnBudgetExceeded = status.turnBudgetExceeded ?? job.turnBudgetExceeded;
|
|
313
|
+
job.wrapUpRequested = status.wrapUpRequested ?? job.wrapUpRequested;
|
|
251
314
|
job.sessionFile = status.sessionFile ?? job.sessionFile;
|
|
252
315
|
if ((job.status === "complete" || job.status === "failed" || job.status === "paused") && !nestedRefreshFailed && !hasLiveNestedDescendants(job.nestedChildren) && (previousStatus !== job.status || !state.cleanupTimers.has(job.asyncId))) {
|
|
253
316
|
scheduleCleanup(job.asyncId);
|
|
@@ -280,6 +343,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
280
343
|
const handleStarted = (data: unknown) => {
|
|
281
344
|
const info = data as AsyncStartedEvent;
|
|
282
345
|
if (!info.id) return;
|
|
346
|
+
if (typeof state.currentSessionId === "string" && info.sessionId !== state.currentSessionId) return;
|
|
283
347
|
const now = Date.now();
|
|
284
348
|
const asyncDir = info.asyncDir ?? path.join(asyncDirRoot, info.id);
|
|
285
349
|
const rawAgents = info.agents?.length ? info.agents : info.chain && info.chain.length > 0 ? info.chain : info.agent ? [info.agent] : undefined;
|
|
@@ -305,6 +369,9 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
305
369
|
activeParallelGroup: Boolean(firstGroupCount && firstGroupCount > 0),
|
|
306
370
|
startedAt: now,
|
|
307
371
|
updatedAt: now,
|
|
372
|
+
timeoutMs: info.timeoutMs,
|
|
373
|
+
deadlineAt: info.deadlineAt,
|
|
374
|
+
turnBudget: info.turnBudget,
|
|
308
375
|
controlEventCursor: 0,
|
|
309
376
|
});
|
|
310
377
|
ensurePoller();
|
|
@@ -314,7 +381,8 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
314
381
|
};
|
|
315
382
|
|
|
316
383
|
const handleComplete = (data: unknown) => {
|
|
317
|
-
const result = data as { id?: string; success?: boolean; asyncDir?: string };
|
|
384
|
+
const result = data as { id?: string; success?: boolean; asyncDir?: string; sessionId?: string };
|
|
385
|
+
if (typeof state.currentSessionId === "string" && result.sessionId !== state.currentSessionId) return;
|
|
318
386
|
const asyncId = result.id;
|
|
319
387
|
if (!asyncId) return;
|
|
320
388
|
const job = state.asyncJobs.get(asyncId);
|
|
@@ -351,5 +419,23 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
351
419
|
}
|
|
352
420
|
};
|
|
353
421
|
|
|
354
|
-
|
|
422
|
+
const restoreActiveJobs = (ctx?: ExtensionContext) => {
|
|
423
|
+
if (ctx?.hasUI) state.lastUiContext = ctx;
|
|
424
|
+
if (!state.currentSessionId) return;
|
|
425
|
+
let runs: AsyncRunSummary[];
|
|
426
|
+
try {
|
|
427
|
+
runs = listAsyncRuns(asyncDirRoot, { states: ["queued", "running"], sessionId: state.currentSessionId, resultsDir, kill: options.kill, now: options.now });
|
|
428
|
+
} catch (error) {
|
|
429
|
+
console.error(`Failed to restore active async jobs from '${asyncDirRoot}':`, error);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
for (const run of runs) {
|
|
433
|
+
state.asyncJobs.set(run.id, summaryToJob(run));
|
|
434
|
+
}
|
|
435
|
+
if (runs.length === 0) return;
|
|
436
|
+
ensurePoller();
|
|
437
|
+
if (state.lastUiContext?.hasUI) rerenderWidget(state.lastUiContext);
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
return { ensurePoller, handleStarted, handleComplete, resetJobs, restoreActiveJobs };
|
|
355
441
|
}
|
|
@@ -2,9 +2,9 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "../../shared/formatters.ts";
|
|
4
4
|
import { formatActivityLabel, formatParallelOutcome } from "../../shared/status-format.ts";
|
|
5
|
-
import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type NestedRunSummary, type SubagentRunMode, type TokenUsage } from "../../shared/types.ts";
|
|
5
|
+
import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type CostSummary, type NestedRunSummary, type SubagentRunMode, type TokenUsage, type TurnBudgetState } from "../../shared/types.ts";
|
|
6
6
|
import { readStatus } from "../../shared/utils.ts";
|
|
7
|
-
import { attachRootChildrenToSteps,
|
|
7
|
+
import { attachRootChildrenToSteps, buildNestedRouteIndex, type NestedRoute, projectNestedEvents } from "../shared/nested-events.ts";
|
|
8
8
|
import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
|
|
9
9
|
import { flatToLogicalStepIndex, normalizeParallelGroups } from "./parallel-groups.ts";
|
|
10
10
|
import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-reconciler.ts";
|
|
@@ -27,13 +27,20 @@ interface AsyncRunStepSummary {
|
|
|
27
27
|
recentOutput?: string[];
|
|
28
28
|
turnCount?: number;
|
|
29
29
|
toolCount?: number;
|
|
30
|
+
steerCount?: number;
|
|
31
|
+
lastSteerAt?: number;
|
|
30
32
|
durationMs?: number;
|
|
31
33
|
tokens?: TokenUsage;
|
|
34
|
+
totalCost?: CostSummary;
|
|
32
35
|
skills?: string[];
|
|
33
36
|
model?: string;
|
|
34
37
|
thinking?: string;
|
|
35
38
|
attemptedModels?: string[];
|
|
36
39
|
error?: string;
|
|
40
|
+
timedOut?: boolean;
|
|
41
|
+
turnBudget?: TurnBudgetState;
|
|
42
|
+
turnBudgetExceeded?: boolean;
|
|
43
|
+
wrapUpRequested?: boolean;
|
|
37
44
|
children?: NestedRunSummary[];
|
|
38
45
|
}
|
|
39
46
|
|
|
@@ -42,6 +49,7 @@ export interface AsyncRunSummary {
|
|
|
42
49
|
asyncDir: string;
|
|
43
50
|
sessionId?: string;
|
|
44
51
|
state: "queued" | "running" | "complete" | "failed" | "paused";
|
|
52
|
+
error?: string;
|
|
45
53
|
activityState?: ActivityState;
|
|
46
54
|
lastActivityAt?: number;
|
|
47
55
|
currentTool?: string;
|
|
@@ -49,11 +57,19 @@ export interface AsyncRunSummary {
|
|
|
49
57
|
currentPath?: string;
|
|
50
58
|
turnCount?: number;
|
|
51
59
|
toolCount?: number;
|
|
60
|
+
steerCount?: number;
|
|
61
|
+
lastSteerAt?: number;
|
|
52
62
|
mode: SubagentRunMode;
|
|
53
63
|
cwd?: string;
|
|
54
64
|
startedAt: number;
|
|
55
65
|
lastUpdate?: number;
|
|
56
66
|
endedAt?: number;
|
|
67
|
+
timeoutMs?: number;
|
|
68
|
+
deadlineAt?: number;
|
|
69
|
+
timedOut?: boolean;
|
|
70
|
+
turnBudget?: TurnBudgetState;
|
|
71
|
+
turnBudgetExceeded?: boolean;
|
|
72
|
+
wrapUpRequested?: boolean;
|
|
57
73
|
currentStep?: number;
|
|
58
74
|
chainStepCount?: number;
|
|
59
75
|
pendingAppends?: number;
|
|
@@ -62,6 +78,7 @@ export interface AsyncRunSummary {
|
|
|
62
78
|
sessionDir?: string;
|
|
63
79
|
outputFile?: string;
|
|
64
80
|
totalTokens?: TokenUsage;
|
|
81
|
+
totalCost?: CostSummary;
|
|
65
82
|
sessionFile?: string;
|
|
66
83
|
nestedChildren?: NestedRunSummary[];
|
|
67
84
|
nestedWarnings?: string[];
|
|
@@ -122,7 +139,7 @@ function deriveAsyncActivityState(asyncDir: string, status: AsyncStatus): { acti
|
|
|
122
139
|
};
|
|
123
140
|
}
|
|
124
141
|
|
|
125
|
-
function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string }, nestedWarnings: string[] = []): AsyncRunSummary {
|
|
142
|
+
function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string }, nestedWarnings: string[] = [], nestedRoute?: NestedRoute): AsyncRunSummary {
|
|
126
143
|
if (status.sessionId !== undefined && typeof status.sessionId !== "string") {
|
|
127
144
|
throw new Error(`Invalid async status '${path.join(asyncDir, "status.json")}': sessionId must be a string.`);
|
|
128
145
|
}
|
|
@@ -131,9 +148,11 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
131
148
|
const chainStepCount = status.chainStepCount ?? steps.length;
|
|
132
149
|
const parallelGroups = normalizeParallelGroups(status.parallelGroups, steps.length, chainStepCount);
|
|
133
150
|
let nestedChildren: NestedRunSummary[] = [];
|
|
134
|
-
if (nestedWarnings.length === 0) {
|
|
151
|
+
if (nestedWarnings.length === 0 && nestedRoute) {
|
|
135
152
|
try {
|
|
136
|
-
|
|
153
|
+
// The route is resolved by the caller via buildNestedRouteIndex, so this
|
|
154
|
+
// avoids a fresh scan of the nested-events directory per run.
|
|
155
|
+
nestedChildren = projectNestedEvents(nestedRoute)?.children ?? [];
|
|
137
156
|
} catch (error) {
|
|
138
157
|
nestedWarnings.push(`Nested status unavailable: ${getErrorMessage(error)}`);
|
|
139
158
|
}
|
|
@@ -159,13 +178,20 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
159
178
|
...(step.recentOutput ? { recentOutput: [...step.recentOutput] } : {}),
|
|
160
179
|
...(step.turnCount !== undefined ? { turnCount: step.turnCount } : {}),
|
|
161
180
|
...(step.toolCount !== undefined ? { toolCount: step.toolCount } : {}),
|
|
181
|
+
...(step.steerCount !== undefined ? { steerCount: step.steerCount } : {}),
|
|
182
|
+
...(step.lastSteerAt !== undefined ? { lastSteerAt: step.lastSteerAt } : {}),
|
|
162
183
|
...(step.durationMs !== undefined ? { durationMs: step.durationMs } : {}),
|
|
163
184
|
...(step.tokens ? { tokens: step.tokens } : {}),
|
|
185
|
+
...(step.totalCost ? { totalCost: step.totalCost } : {}),
|
|
164
186
|
...(step.skills ? { skills: step.skills } : {}),
|
|
165
187
|
...(step.model ? { model: step.model } : {}),
|
|
166
188
|
...(step.thinking ? { thinking: step.thinking } : {}),
|
|
167
189
|
...(step.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
|
|
168
190
|
...(step.error ? { error: step.error } : {}),
|
|
191
|
+
...(step.timedOut !== undefined ? { timedOut: step.timedOut } : {}),
|
|
192
|
+
...(step.turnBudget ? { turnBudget: step.turnBudget } : {}),
|
|
193
|
+
...(step.turnBudgetExceeded !== undefined ? { turnBudgetExceeded: step.turnBudgetExceeded } : {}),
|
|
194
|
+
...(step.wrapUpRequested !== undefined ? { wrapUpRequested: step.wrapUpRequested } : {}),
|
|
169
195
|
...(step.children?.length ? { children: step.children } : {}),
|
|
170
196
|
};
|
|
171
197
|
});
|
|
@@ -175,6 +201,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
175
201
|
asyncDir,
|
|
176
202
|
...(status.sessionId ? { sessionId: status.sessionId } : {}),
|
|
177
203
|
state: status.state,
|
|
204
|
+
...(status.error ? { error: status.error } : {}),
|
|
178
205
|
activityState,
|
|
179
206
|
lastActivityAt,
|
|
180
207
|
currentTool: status.currentTool,
|
|
@@ -182,11 +209,19 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
182
209
|
currentPath: status.currentPath,
|
|
183
210
|
turnCount: status.turnCount,
|
|
184
211
|
toolCount: status.toolCount,
|
|
212
|
+
steerCount: status.steerCount,
|
|
213
|
+
lastSteerAt: status.lastSteerAt,
|
|
185
214
|
mode: status.mode,
|
|
186
215
|
cwd: status.cwd,
|
|
187
216
|
startedAt: status.startedAt,
|
|
188
217
|
lastUpdate: status.lastUpdate,
|
|
189
218
|
endedAt: status.endedAt,
|
|
219
|
+
...(status.timeoutMs !== undefined ? { timeoutMs: status.timeoutMs } : {}),
|
|
220
|
+
...(status.deadlineAt !== undefined ? { deadlineAt: status.deadlineAt } : {}),
|
|
221
|
+
...(status.timedOut !== undefined ? { timedOut: status.timedOut } : {}),
|
|
222
|
+
...(status.turnBudget ? { turnBudget: status.turnBudget } : {}),
|
|
223
|
+
...(status.turnBudgetExceeded !== undefined ? { turnBudgetExceeded: status.turnBudgetExceeded } : {}),
|
|
224
|
+
...(status.wrapUpRequested !== undefined ? { wrapUpRequested: status.wrapUpRequested } : {}),
|
|
190
225
|
currentStep: status.currentStep,
|
|
191
226
|
...(status.chainStepCount !== undefined ? { chainStepCount: status.chainStepCount } : {}),
|
|
192
227
|
...(status.pendingAppends !== undefined ? { pendingAppends: status.pendingAppends } : {}),
|
|
@@ -197,6 +232,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
197
232
|
...(status.sessionDir ? { sessionDir: status.sessionDir } : {}),
|
|
198
233
|
...(status.outputFile ? { outputFile: status.outputFile } : {}),
|
|
199
234
|
...(status.totalTokens ? { totalTokens: status.totalTokens } : {}),
|
|
235
|
+
...(status.totalCost ? { totalCost: status.totalCost } : {}),
|
|
200
236
|
...(status.sessionFile ? { sessionFile: status.sessionFile } : {}),
|
|
201
237
|
};
|
|
202
238
|
}
|
|
@@ -233,6 +269,16 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
233
269
|
|
|
234
270
|
const allowedStates = options.states ? new Set(options.states) : undefined;
|
|
235
271
|
const runs: AsyncRunSummary[] = [];
|
|
272
|
+
// Route resolution for every run shares a single index built from the
|
|
273
|
+
// nested-events directory, so the per-run lookup is O(1) instead of scanning
|
|
274
|
+
// the directory once per run. The index is built lazily on first use, so
|
|
275
|
+
// load-time restoration (which only wants queued/running runs) skips it
|
|
276
|
+
// entirely when no active runs match.
|
|
277
|
+
let nestedRouteIndex: Map<string, NestedRoute> | undefined;
|
|
278
|
+
const resolveNestedRoute = (rootRunId: string): NestedRoute | undefined => {
|
|
279
|
+
if (!nestedRouteIndex) nestedRouteIndex = buildNestedRouteIndex();
|
|
280
|
+
return nestedRouteIndex.get(rootRunId);
|
|
281
|
+
};
|
|
236
282
|
for (const entry of entries) {
|
|
237
283
|
const asyncDir = path.join(asyncDirRoot, entry);
|
|
238
284
|
const reconciliation = options.reconcile === false
|
|
@@ -240,16 +286,21 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
240
286
|
: reconcileAsyncRun(asyncDir, { resultsDir: options.resultsDir, kill: options.kill, now: options.now });
|
|
241
287
|
const status = (reconciliation?.status ?? readStatus(asyncDir)) as (AsyncStatus & { cwd?: string }) | null;
|
|
242
288
|
if (!status) continue;
|
|
289
|
+
// Filter before the nested-route lookup: the lookup builds an index over
|
|
290
|
+
// the nested-events directory, so deferring it for filtered-out runs keeps
|
|
291
|
+
// restoration at load from scanning that directory when no active runs
|
|
292
|
+
// match.
|
|
293
|
+
if (allowedStates && !allowedStates.has(status.state)) continue;
|
|
294
|
+
if (options.sessionId && status.sessionId !== options.sessionId) continue;
|
|
243
295
|
const nestedWarnings: string[] = [];
|
|
296
|
+
let nestedRoute: NestedRoute | undefined;
|
|
244
297
|
try {
|
|
245
|
-
|
|
298
|
+
nestedRoute = resolveNestedRoute(status.runId || path.basename(asyncDir));
|
|
246
299
|
if (nestedRoute) reconcileNestedAsyncDescendants(nestedRoute, { resultsDir: options.resultsDir, kill: options.kill, now: options.now });
|
|
247
300
|
} catch (error) {
|
|
248
301
|
nestedWarnings.push(`Nested status unavailable: ${getErrorMessage(error)}`);
|
|
249
302
|
}
|
|
250
|
-
const summary = statusToSummary(asyncDir, status, nestedWarnings);
|
|
251
|
-
if (allowedStates && !allowedStates.has(summary.state)) continue;
|
|
252
|
-
if (options.sessionId && summary.sessionId !== options.sessionId) continue;
|
|
303
|
+
const summary = statusToSummary(asyncDir, status, nestedWarnings, nestedRoute);
|
|
253
304
|
runs.push(summary);
|
|
254
305
|
}
|
|
255
306
|
|
|
@@ -257,13 +308,18 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
257
308
|
return options.limit !== undefined ? sorted.slice(0, options.limit) : sorted;
|
|
258
309
|
}
|
|
259
310
|
|
|
260
|
-
function formatActivityFacts(input: { activityState?: ActivityState; lastActivityAt?: number; currentTool?: string; currentToolStartedAt?: number; currentPath?: string; turnCount?: number; toolCount?: number }): string | undefined {
|
|
311
|
+
function formatActivityFacts(input: { activityState?: ActivityState; lastActivityAt?: number; currentTool?: string; currentToolStartedAt?: number; currentPath?: string; turnCount?: number; toolCount?: number; steerCount?: number; lastSteerAt?: number; turnBudget?: TurnBudgetState; turnBudgetExceeded?: boolean; wrapUpRequested?: boolean }): string | undefined {
|
|
261
312
|
const facts: string[] = [];
|
|
262
313
|
if (input.currentTool && input.currentToolStartedAt !== undefined) facts.push(`tool ${input.currentTool} ${formatDuration(Math.max(0, Date.now() - input.currentToolStartedAt))}`);
|
|
263
314
|
else if (input.currentTool) facts.push(`tool ${input.currentTool}`);
|
|
264
315
|
if (input.currentPath) facts.push(shortenPath(input.currentPath));
|
|
265
316
|
if (input.turnCount !== undefined) facts.push(`${input.turnCount} turns`);
|
|
317
|
+
if (input.turnBudgetExceeded && input.turnBudget) facts.push(`turn budget exceeded ${input.turnBudget.turnCount}/${input.turnBudget.maxTurns}+${input.turnBudget.graceTurns}`);
|
|
318
|
+
else if (input.wrapUpRequested && input.turnBudget) facts.push(`wrap-up requested ${input.turnBudget.turnCount}/${input.turnBudget.maxTurns}`);
|
|
319
|
+
else if (input.turnBudget) facts.push(`turn budget ${input.turnBudget.turnCount}/${input.turnBudget.maxTurns}+${input.turnBudget.graceTurns}`);
|
|
266
320
|
if (input.toolCount !== undefined) facts.push(`${input.toolCount} tools`);
|
|
321
|
+
if (input.steerCount !== undefined) facts.push(`${input.steerCount} steers`);
|
|
322
|
+
if (typeof input.lastSteerAt === "number" && Number.isFinite(input.lastSteerAt)) facts.push(`last steer ${new Date(input.lastSteerAt).toISOString()}`);
|
|
267
323
|
const activity = formatActivityLabel(input.lastActivityAt, input.activityState);
|
|
268
324
|
return activity || facts.length ? [activity, ...facts].filter(Boolean).join(" | ") : undefined;
|
|
269
325
|
}
|
|
@@ -328,6 +384,7 @@ export function formatAsyncRunList(runs: AsyncRunSummary[], heading = "Active as
|
|
|
328
384
|
const attached = new Set(run.steps.flatMap((step) => step.children?.map((child) => child.id) ?? []));
|
|
329
385
|
const unattached = run.nestedChildren?.filter((child) => !attached.has(child.id)) ?? [];
|
|
330
386
|
lines.push(...formatNestedRunStatusLines(unattached, { indent: " ", maxLines: 12 }));
|
|
387
|
+
if (run.error) lines.push(` Error: ${run.error}`);
|
|
331
388
|
for (const warning of run.nestedWarnings ?? []) lines.push(` Warning: ${warning}`);
|
|
332
389
|
const outputPath = formatAsyncRunOutputPath(run);
|
|
333
390
|
if (outputPath) lines.push(` output: ${shortenPath(outputPath)}`);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import type { AcceptanceLedger, AsyncStatus, ModelAttempt } from "../../shared/types.ts";
|
|
3
|
+
import type { AcceptanceLedger, AsyncStatus, CostSummary, ModelAttempt } from "../../shared/types.ts";
|
|
4
4
|
import { readStatus } from "../../shared/utils.ts";
|
|
5
5
|
|
|
6
6
|
export interface ImportedAsyncRoot {
|
|
@@ -21,26 +21,32 @@ export interface ImportedAsyncRootResult {
|
|
|
21
21
|
model?: string;
|
|
22
22
|
attemptedModels?: string[];
|
|
23
23
|
modelAttempts?: ModelAttempt[];
|
|
24
|
+
totalCost?: CostSummary;
|
|
24
25
|
structuredOutput?: unknown;
|
|
25
26
|
structuredOutputPath?: string;
|
|
26
27
|
structuredOutputSchemaPath?: string;
|
|
27
28
|
acceptance?: AcceptanceLedger;
|
|
29
|
+
timedOut?: boolean;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
interface AsyncResultFile {
|
|
31
33
|
state?: string;
|
|
32
34
|
success?: boolean;
|
|
33
35
|
summary?: string;
|
|
36
|
+
error?: string;
|
|
37
|
+
timedOut?: boolean;
|
|
34
38
|
results?: Array<{
|
|
35
39
|
agent?: string;
|
|
36
40
|
output?: string;
|
|
37
41
|
error?: string;
|
|
38
42
|
success?: boolean;
|
|
43
|
+
timedOut?: boolean;
|
|
39
44
|
sessionFile?: string;
|
|
40
45
|
intercomTarget?: string;
|
|
41
46
|
model?: string;
|
|
42
47
|
attemptedModels?: string[];
|
|
43
48
|
modelAttempts?: ModelAttempt[];
|
|
49
|
+
totalCost?: CostSummary;
|
|
44
50
|
structuredOutput?: unknown;
|
|
45
51
|
structuredOutputPath?: string;
|
|
46
52
|
structuredOutputSchemaPath?: string;
|
|
@@ -85,6 +91,7 @@ function resultState(result: AsyncResultFile | undefined, child: NonNullable<Asy
|
|
|
85
91
|
|
|
86
92
|
function outputFromTerminalStatus(root: ImportedAsyncRoot, status: AsyncStatus, step: NonNullable<AsyncStatus["steps"]>[number] | undefined): ImportedAsyncRootResult {
|
|
87
93
|
const agent = step?.agent ?? status.steps?.[root.index]?.agent ?? "subagent";
|
|
94
|
+
const timedOut = step?.timedOut === true || status.timedOut === true;
|
|
88
95
|
const message = step?.error ?? status.error ?? `Attached async root ${root.runId} ended without a result file at ${root.resultPath}.`;
|
|
89
96
|
return {
|
|
90
97
|
agent,
|
|
@@ -92,10 +99,12 @@ function outputFromTerminalStatus(root: ImportedAsyncRoot, status: AsyncStatus,
|
|
|
92
99
|
success: false,
|
|
93
100
|
exitCode: 1,
|
|
94
101
|
error: message,
|
|
102
|
+
...(timedOut ? { timedOut: true } : {}),
|
|
95
103
|
...(step?.sessionFile ?? status.sessionFile ? { sessionFile: step?.sessionFile ?? status.sessionFile } : {}),
|
|
96
104
|
...(step?.model ? { model: step.model } : {}),
|
|
97
105
|
...(step?.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
|
|
98
106
|
...(step?.modelAttempts ? { modelAttempts: step.modelAttempts } : {}),
|
|
107
|
+
...(step?.totalCost ? { totalCost: step.totalCost } : {}),
|
|
99
108
|
...(step?.structuredOutput !== undefined ? { structuredOutput: step.structuredOutput } : {}),
|
|
100
109
|
...(step?.structuredOutputPath ? { structuredOutputPath: step.structuredOutputPath } : {}),
|
|
101
110
|
...(step?.structuredOutputSchemaPath ? { structuredOutputSchemaPath: step.structuredOutputSchemaPath } : {}),
|
|
@@ -103,25 +112,45 @@ function outputFromTerminalStatus(root: ImportedAsyncRoot, status: AsyncStatus,
|
|
|
103
112
|
};
|
|
104
113
|
}
|
|
105
114
|
|
|
115
|
+
function outputFromTimeout(root: ImportedAsyncRoot, status: AsyncStatus | null, message: string): ImportedAsyncRootResult {
|
|
116
|
+
const step = selectedStatusStep(status, root.index);
|
|
117
|
+
return {
|
|
118
|
+
agent: step?.agent ?? status?.steps?.[root.index]?.agent ?? "subagent",
|
|
119
|
+
output: message,
|
|
120
|
+
success: false,
|
|
121
|
+
exitCode: 1,
|
|
122
|
+
error: message,
|
|
123
|
+
timedOut: true,
|
|
124
|
+
...(step?.sessionFile ?? status?.sessionFile ? { sessionFile: step?.sessionFile ?? status?.sessionFile } : {}),
|
|
125
|
+
...(step?.model ? { model: step.model } : {}),
|
|
126
|
+
...(step?.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
|
|
127
|
+
...(step?.modelAttempts ? { modelAttempts: step.modelAttempts } : {}),
|
|
128
|
+
...(step?.totalCost ? { totalCost: step.totalCost } : {}),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
106
132
|
function buildImportedResult(root: ImportedAsyncRoot, status: AsyncStatus | null, result: AsyncResultFile): ImportedAsyncRootResult {
|
|
107
133
|
const child = result.results?.[root.index];
|
|
108
134
|
const step = selectedStatusStep(status, root.index);
|
|
109
135
|
const state = resultState(result, child);
|
|
110
136
|
const agent = child?.agent ?? step?.agent ?? status?.steps?.[root.index]?.agent ?? "subagent";
|
|
111
137
|
const output = child?.output ?? result.summary ?? "";
|
|
112
|
-
const
|
|
113
|
-
const
|
|
138
|
+
const timedOut = child?.timedOut === true || step?.timedOut === true || result.timedOut === true || status?.timedOut === true;
|
|
139
|
+
const success = state === "complete" && !timedOut;
|
|
140
|
+
const error = child?.error ?? (success ? undefined : result.error ?? result.summary ?? status?.error ?? `Attached async root ${root.runId} did not complete successfully.`);
|
|
114
141
|
return {
|
|
115
142
|
agent,
|
|
116
143
|
output: success ? output : (output || error || ""),
|
|
117
144
|
success,
|
|
118
145
|
exitCode: success ? 0 : 1,
|
|
119
146
|
...(error ? { error } : {}),
|
|
147
|
+
...(timedOut ? { timedOut: true } : {}),
|
|
120
148
|
...(child?.sessionFile ?? step?.sessionFile ?? status?.sessionFile ? { sessionFile: child?.sessionFile ?? step?.sessionFile ?? status?.sessionFile } : {}),
|
|
121
149
|
...(child?.intercomTarget ? { intercomTarget: child.intercomTarget } : {}),
|
|
122
150
|
...(child?.model ?? step?.model ? { model: child?.model ?? step?.model } : {}),
|
|
123
151
|
...(child?.attemptedModels ?? step?.attemptedModels ? { attemptedModels: child?.attemptedModels ?? step?.attemptedModels } : {}),
|
|
124
152
|
...(child?.modelAttempts ?? step?.modelAttempts ? { modelAttempts: child?.modelAttempts ?? step?.modelAttempts } : {}),
|
|
153
|
+
...(child?.totalCost ?? step?.totalCost ? { totalCost: child?.totalCost ?? step?.totalCost } : {}),
|
|
125
154
|
...(child?.structuredOutput !== undefined ? { structuredOutput: child.structuredOutput } : step?.structuredOutput !== undefined ? { structuredOutput: step.structuredOutput } : {}),
|
|
126
155
|
...(child?.structuredOutputPath ?? step?.structuredOutputPath ? { structuredOutputPath: child?.structuredOutputPath ?? step?.structuredOutputPath } : {}),
|
|
127
156
|
...(child?.structuredOutputSchemaPath ?? step?.structuredOutputSchemaPath ? { structuredOutputSchemaPath: child?.structuredOutputSchemaPath ?? step?.structuredOutputSchemaPath } : {}),
|
|
@@ -131,7 +160,7 @@ function buildImportedResult(root: ImportedAsyncRoot, status: AsyncStatus | null
|
|
|
131
160
|
|
|
132
161
|
export async function waitForImportedAsyncRoot(
|
|
133
162
|
root: ImportedAsyncRoot,
|
|
134
|
-
options: { pollIntervalMs?: number; terminalResultGraceMs?: number; now?: () => number } = {},
|
|
163
|
+
options: { pollIntervalMs?: number; terminalResultGraceMs?: number; now?: () => number; shouldAbort?: () => boolean; timeoutMessage?: string } = {},
|
|
135
164
|
): Promise<ImportedAsyncRootResult> {
|
|
136
165
|
const pollIntervalMs = options.pollIntervalMs ?? 500;
|
|
137
166
|
const terminalResultGraceMs = options.terminalResultGraceMs ?? 1_000;
|
|
@@ -139,6 +168,7 @@ export async function waitForImportedAsyncRoot(
|
|
|
139
168
|
let terminalSince: number | undefined;
|
|
140
169
|
for (;;) {
|
|
141
170
|
const status = readStatus(root.asyncDir);
|
|
171
|
+
if (options.shouldAbort?.()) return outputFromTimeout(root, status, options.timeoutMessage ?? "Subagent timed out.");
|
|
142
172
|
const result = readResultFile(root.resultPath);
|
|
143
173
|
if (result) return buildImportedResult(root, status, result);
|
|
144
174
|
if (isTerminalStatus(status, root.index)) {
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart completion batching with straggler handling.
|
|
3
|
+
*
|
|
4
|
+
* Holds successful async-completion notifications briefly so sibling jobs that
|
|
5
|
+
* finish within a short window arrive as a single grouped message. A hard
|
|
6
|
+
* max-wait cap (measured from the first item in a group) prevents holding
|
|
7
|
+
* notifications indefinitely. After a group is emitted, late-finishing
|
|
8
|
+
* siblings that arrive within the straggler window join a shorter "straggler"
|
|
9
|
+
* group with reduced debounce and max-wait timers.
|
|
10
|
+
*
|
|
11
|
+
* Failure and attention signals bypass this batcher entirely. Callers must
|
|
12
|
+
* flush() held items and emit those signals immediately so failures and
|
|
13
|
+
* needs-attention notices are never delayed.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { CompletionBatchConfig } from "../../shared/types.ts";
|
|
17
|
+
|
|
18
|
+
export type { CompletionBatchConfig };
|
|
19
|
+
|
|
20
|
+
export interface ResolvedCompletionBatchConfig {
|
|
21
|
+
enabled: boolean;
|
|
22
|
+
debounceMs: number;
|
|
23
|
+
maxWaitMs: number;
|
|
24
|
+
stragglerDebounceMs: number;
|
|
25
|
+
stragglerMaxWaitMs: number;
|
|
26
|
+
stragglerWindowMs: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const DEFAULT_COMPLETION_BATCH_CONFIG: ResolvedCompletionBatchConfig = {
|
|
30
|
+
enabled: true,
|
|
31
|
+
debounceMs: 150,
|
|
32
|
+
maxWaitMs: 1000,
|
|
33
|
+
stragglerDebounceMs: 75,
|
|
34
|
+
stragglerMaxWaitMs: 400,
|
|
35
|
+
stragglerWindowMs: 2000,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function parsePositiveInt(value: unknown): number | undefined {
|
|
39
|
+
if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value) || value < 1) return undefined;
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function resolveCompletionBatchConfig(
|
|
44
|
+
globalConfig?: CompletionBatchConfig,
|
|
45
|
+
override?: CompletionBatchConfig,
|
|
46
|
+
): ResolvedCompletionBatchConfig {
|
|
47
|
+
const enabled = typeof override?.enabled === "boolean"
|
|
48
|
+
? override.enabled
|
|
49
|
+
: typeof globalConfig?.enabled === "boolean"
|
|
50
|
+
? globalConfig.enabled
|
|
51
|
+
: DEFAULT_COMPLETION_BATCH_CONFIG.enabled;
|
|
52
|
+
return {
|
|
53
|
+
enabled,
|
|
54
|
+
debounceMs: parsePositiveInt(override?.debounceMs) ?? parsePositiveInt(globalConfig?.debounceMs) ?? DEFAULT_COMPLETION_BATCH_CONFIG.debounceMs,
|
|
55
|
+
maxWaitMs: parsePositiveInt(override?.maxWaitMs) ?? parsePositiveInt(globalConfig?.maxWaitMs) ?? DEFAULT_COMPLETION_BATCH_CONFIG.maxWaitMs,
|
|
56
|
+
stragglerDebounceMs: parsePositiveInt(override?.stragglerDebounceMs) ?? parsePositiveInt(globalConfig?.stragglerDebounceMs) ?? DEFAULT_COMPLETION_BATCH_CONFIG.stragglerDebounceMs,
|
|
57
|
+
stragglerMaxWaitMs: parsePositiveInt(override?.stragglerMaxWaitMs) ?? parsePositiveInt(globalConfig?.stragglerMaxWaitMs) ?? DEFAULT_COMPLETION_BATCH_CONFIG.stragglerMaxWaitMs,
|
|
58
|
+
stragglerWindowMs: parsePositiveInt(override?.stragglerWindowMs) ?? parsePositiveInt(globalConfig?.stragglerWindowMs) ?? DEFAULT_COMPLETION_BATCH_CONFIG.stragglerWindowMs,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type TimerHandle = unknown;
|
|
63
|
+
|
|
64
|
+
interface TimerApi {
|
|
65
|
+
setTimeout(handler: () => void, delayMs: number): TimerHandle;
|
|
66
|
+
clearTimeout(handle: TimerHandle): void;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const defaultTimers: TimerApi = {
|
|
70
|
+
setTimeout: (handler, delayMs) => setTimeout(handler, delayMs),
|
|
71
|
+
clearTimeout: (handle) => clearTimeout(handle as ReturnType<typeof setTimeout>),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
function unrefHandle(handle: TimerHandle): void {
|
|
75
|
+
if (handle && typeof handle === "object" && "unref" in handle && typeof (handle as { unref: unknown }).unref === "function") {
|
|
76
|
+
(handle as { unref: () => void }).unref();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface CompletionBatcherOptions<T> {
|
|
81
|
+
config: ResolvedCompletionBatchConfig;
|
|
82
|
+
emit: (items: T[]) => void;
|
|
83
|
+
timers?: TimerApi;
|
|
84
|
+
now?: () => number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface CompletionBatcher<T> {
|
|
88
|
+
/** Add a batchable item. Emits immediately when batching is disabled. */
|
|
89
|
+
push(item: T): void;
|
|
90
|
+
/** Emit any held items immediately as a single group. */
|
|
91
|
+
flush(): void;
|
|
92
|
+
/** Clear timers without emitting. */
|
|
93
|
+
dispose(): void;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Create a completion batcher. The batcher is single-use per registration: it
|
|
98
|
+
* holds at most one open group. `flush` forces emission; `dispose` tears down
|
|
99
|
+
* timers for reload/shutdown without emitting.
|
|
100
|
+
*/
|
|
101
|
+
export function createCompletionBatcher<T>(options: CompletionBatcherOptions<T>): CompletionBatcher<T> {
|
|
102
|
+
const timers = options.timers ?? defaultTimers;
|
|
103
|
+
const now = options.now ?? Date.now;
|
|
104
|
+
const config = options.config;
|
|
105
|
+
|
|
106
|
+
if (!config.enabled) {
|
|
107
|
+
return {
|
|
108
|
+
push(item: T) {
|
|
109
|
+
options.emit([item]);
|
|
110
|
+
},
|
|
111
|
+
flush() {},
|
|
112
|
+
dispose() {},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let pending: T[] = [];
|
|
117
|
+
let debounceTimer: TimerHandle | null = null;
|
|
118
|
+
let maxWaitTimer: TimerHandle | null = null;
|
|
119
|
+
let straggler = false;
|
|
120
|
+
let lastEmitAt: number | null = null;
|
|
121
|
+
|
|
122
|
+
const clearTimers = () => {
|
|
123
|
+
if (debounceTimer) {
|
|
124
|
+
timers.clearTimeout(debounceTimer);
|
|
125
|
+
debounceTimer = null;
|
|
126
|
+
}
|
|
127
|
+
if (maxWaitTimer) {
|
|
128
|
+
timers.clearTimeout(maxWaitTimer);
|
|
129
|
+
maxWaitTimer = null;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const emitGroup = () => {
|
|
134
|
+
clearTimers();
|
|
135
|
+
if (pending.length === 0) return;
|
|
136
|
+
const items = pending;
|
|
137
|
+
pending = [];
|
|
138
|
+
lastEmitAt = now();
|
|
139
|
+
options.emit(items);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
push(item: T) {
|
|
144
|
+
if (pending.length === 0) {
|
|
145
|
+
straggler = lastEmitAt !== null && (now() - lastEmitAt) < config.stragglerWindowMs;
|
|
146
|
+
}
|
|
147
|
+
pending.push(item);
|
|
148
|
+
|
|
149
|
+
if (debounceTimer) timers.clearTimeout(debounceTimer);
|
|
150
|
+
const debounceDelay = straggler ? config.stragglerDebounceMs : config.debounceMs;
|
|
151
|
+
debounceTimer = timers.setTimeout(emitGroup, debounceDelay);
|
|
152
|
+
unrefHandle(debounceTimer);
|
|
153
|
+
|
|
154
|
+
if (!maxWaitTimer) {
|
|
155
|
+
const maxWaitDelay = straggler ? config.stragglerMaxWaitMs : config.maxWaitMs;
|
|
156
|
+
maxWaitTimer = timers.setTimeout(emitGroup, maxWaitDelay);
|
|
157
|
+
unrefHandle(maxWaitTimer);
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
flush: emitGroup,
|
|
161
|
+
dispose() {
|
|
162
|
+
clearTimers();
|
|
163
|
+
pending = [];
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|