pi-subagents 0.80.0 → 0.82.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +66 -0
- package/bundle/agents.js +18 -0
- package/bundle/background-work.js +16 -0
- package/bundle/capability-ceiling.js +22 -0
- package/bundle/child-tool-plan.js +11 -0
- package/bundle/chunk-43SCHIMD.js +57 -0
- package/bundle/chunk-4QFZZV3O.js +57 -0
- package/bundle/chunk-4SDU2BZX.js +1190 -0
- package/bundle/chunk-52UBALJL.js +753 -0
- package/bundle/chunk-5LFLVSTM.js +260 -0
- package/bundle/chunk-5TYMSZX3.js +235 -0
- package/bundle/chunk-62OCY2S4.js +14 -0
- package/bundle/chunk-7F6RKT6W.js +132 -0
- package/bundle/chunk-7IVSUMQQ.js +1337 -0
- package/bundle/chunk-7J4ZXMY7.js +18 -0
- package/bundle/chunk-7PCKZHR5.js +1234 -0
- package/bundle/chunk-7YMXOYEU.js +1373 -0
- package/bundle/chunk-B5EKXDHE.js +16870 -0
- package/bundle/chunk-CIZQXJ5Q.js +8619 -0
- package/bundle/chunk-CTPAE36Y.js +94 -0
- package/bundle/chunk-D4EHT5SJ.js +14 -0
- package/bundle/chunk-GC4DSPMH.js +946 -0
- package/bundle/chunk-GJECHSAR.js +179 -0
- package/bundle/chunk-M2CTFYT3.js +4164 -0
- package/bundle/chunk-MHZ4G23A.js +1296 -0
- package/bundle/chunk-N2BN7BF5.js +159 -0
- package/bundle/chunk-NF7IEKBH.js +490 -0
- package/bundle/chunk-NGGAFDL6.js +742 -0
- package/bundle/chunk-NIXWDZZV.js +9 -0
- package/bundle/chunk-NRQ75MGH.js +8327 -0
- package/bundle/chunk-OYF6AFDY.js +458 -0
- package/bundle/chunk-PLCCBLPY.js +526 -0
- package/bundle/chunk-RMQFMMAT.js +416 -0
- package/bundle/chunk-SIJ4S3EN.js +276 -0
- package/bundle/chunk-UKG4V45O.js +228 -0
- package/bundle/chunk-VH5OAWVG.js +4096 -0
- package/bundle/chunk-W2SZ22EM.js +5462 -0
- package/bundle/chunk-WVD4UEL4.js +9367 -0
- package/bundle/chunk-XSWOTIA3.js +175 -0
- package/bundle/chunk-YFCK7H2S.js +129 -0
- package/bundle/chunk-YKEWIB4M.js +380 -0
- package/bundle/chunk-YYREO77V.js +137 -0
- package/bundle/chunk-Z7KVHQHL.js +173 -0
- package/bundle/chunk-ZLINIYTP.js +8613 -0
- package/bundle/chunk-ZQGSDHL6.js +128 -0
- package/bundle/control-channel.js +11 -0
- package/bundle/delegation.js +14 -0
- package/bundle/external-job-provider.js +20 -0
- package/bundle/external-runs.js +21 -0
- package/bundle/index.js +30 -0
- package/bundle/inspectors.js +8 -0
- package/bundle/intercom-bridge.js +11 -0
- package/bundle/preflight.js +425 -0
- package/bundle/project-panes.js +28 -0
- package/bundle/required-child-extensions.js +6 -0
- package/bundle/shared-types.js +8 -0
- package/bundle/workflow-resources.js +7 -0
- package/docs/agents.md +31 -0
- package/docs/configuration.md +9 -1
- package/docs/extension-api.md +27 -0
- package/docs/observability.md +6 -2
- package/docs/tool-reference.md +3 -1
- package/docs/workflows.md +16 -0
- package/package.json +19 -19
- package/skills/pi-subagents/references/execution-controls.md +5 -4
- package/src/agents/agent-management.js +3 -3
- package/src/agents/agent-memory.js +3 -1
- package/src/agents/agents.js +6 -6
- package/src/agents/frontmatter.d.ts +4 -0
- package/src/agents/frontmatter.js +24 -3
- package/src/agents/skills.js +2 -1
- package/src/api/delegation.d.ts +20 -0
- package/src/extension/config.js +3 -0
- package/src/extension/herdr-pi-bridge.js +17 -10
- package/src/extension/index.js +109 -9
- package/src/extension/schemas.js +3 -3
- package/src/intercom/native-supervisor-channel.js +20 -0
- package/src/missions/lifecycle.js +7 -1
- package/src/runs/background/active-async-capacity.d.ts +1 -0
- package/src/runs/background/active-async-capacity.js +9 -3
- package/src/runs/background/async-execution.js +20 -10
- package/src/runs/background/async-job-tracker.d.ts +6 -1
- package/src/runs/background/async-job-tracker.js +9 -4
- package/src/runs/background/async-status.d.ts +2 -0
- package/src/runs/background/async-status.js +21 -2
- package/src/runs/background/await-async-run.js +3 -7
- package/src/runs/background/boot-id.d.ts +12 -0
- package/src/runs/background/boot-id.js +47 -0
- package/src/runs/background/interrupted-elsewhere.d.ts +22 -0
- package/src/runs/background/interrupted-elsewhere.js +68 -0
- package/src/runs/background/notify.d.ts +2 -0
- package/src/runs/background/notify.js +13 -0
- package/src/runs/background/result-delivery-ownership.d.ts +6 -0
- package/src/runs/background/result-delivery-ownership.js +19 -1
- package/src/runs/background/result-watcher.d.ts +3 -1
- package/src/runs/background/result-watcher.js +24 -8
- package/src/runs/background/run-child-session.d.ts +1 -6
- package/src/runs/background/run-child-session.js +19 -1
- package/src/runs/background/run-status.js +2 -1
- package/src/runs/background/stale-run-reconciler.d.ts +25 -0
- package/src/runs/background/stale-run-reconciler.js +54 -13
- package/src/runs/background/subagent-runner-bootstrap.js +3 -0
- package/src/runs/background/subagent-runner.d.ts +4 -0
- package/src/runs/background/subagent-runner.js +13 -5
- package/src/runs/background/subagent-wait.d.ts +1 -1
- package/src/runs/background/subagent-wait.js +6 -1
- package/src/runs/background/terminal-run-index.d.ts +1 -0
- package/src/runs/background/terminal-run-index.js +4 -2
- package/src/runs/foreground/async-stop-action.js +3 -1
- package/src/runs/foreground/execution.js +45 -5
- package/src/runs/foreground/foreground-history.d.ts +16 -2
- package/src/runs/foreground/foreground-history.js +133 -31
- package/src/runs/foreground/subagent-executor.d.ts +11 -0
- package/src/runs/foreground/subagent-executor.js +277 -74
- package/src/runs/shared/child-launch.d.ts +2 -2
- package/src/runs/shared/child-session.d.ts +3 -2
- package/src/runs/shared/child-session.js +51 -8
- package/src/runs/shared/child-tool-plan.d.ts +7 -0
- package/src/runs/shared/child-tool-plan.js +21 -6
- package/src/runs/shared/herdr-connection.d.ts +17 -8
- package/src/runs/shared/herdr-connection.js +19 -7
- package/src/runs/shared/herdr-external-adapters.d.ts +3 -2
- package/src/runs/shared/herdr-external-adapters.js +1 -1
- package/src/runs/shared/herdr-machine.d.ts +12 -2
- package/src/runs/shared/herdr-machine.js +55 -9
- package/src/runs/shared/herdr-pane-host.d.ts +88 -0
- package/src/runs/shared/herdr-pane-host.js +155 -0
- package/src/runs/shared/herdr-placed-run.d.ts +27 -150
- package/src/runs/shared/herdr-placed-run.js +48 -395
- package/src/runs/shared/local-machine-access.d.ts +5 -0
- package/src/runs/shared/local-machine-access.js +28 -0
- package/src/runs/shared/orca-progress-tabs.d.ts +0 -1
- package/src/runs/shared/orca-progress-tabs.js +1 -24
- package/src/runs/shared/parallel-utils.d.ts +1 -1
- package/src/runs/shared/placed-run-owner.d.ts +49 -0
- package/src/runs/shared/placed-run-owner.js +93 -0
- package/src/runs/shared/placement-host.d.ts +99 -0
- package/src/runs/shared/placement-host.js +157 -0
- package/src/runs/shared/rex-machine.d.ts +25 -0
- package/src/runs/shared/rex-machine.js +83 -0
- package/src/runs/shared/rex-pane-host.d.ts +62 -0
- package/src/runs/shared/rex-pane-host.js +327 -0
- package/src/runs/shared/structured-output.d.ts +1 -0
- package/src/runs/shared/structured-output.js +1 -0
- package/src/shared/accessible-dir.js +4 -1
- package/src/shared/completion-owner.d.ts +5 -0
- package/src/shared/completion-owner.js +5 -0
- package/src/shared/provider-diagnostics.d.ts +9 -0
- package/src/shared/provider-diagnostics.js +75 -0
- package/src/shared/session-identity.d.ts +2 -0
- package/src/shared/session-identity.js +26 -0
- package/src/shared/types.d.ts +48 -6
- package/src/slash/delegation-adapters.d.ts +3 -1
- package/src/slash/delegation-adapters.js +7 -0
- package/src/slash/prompt-template-bridge.js +3 -1
- package/src/slash/subagents-admin.d.ts +1 -0
- package/src/slash/subagents-admin.js +1 -1
- package/src/tui/fleet-layout.d.ts +20 -0
- package/src/tui/fleet-layout.js +17 -0
- package/src/tui/fleet-viewport.d.ts +12 -0
- package/src/tui/fleet-viewport.js +35 -0
- package/src/tui/fleet.d.ts +6 -6
- package/src/tui/fleet.js +93 -66
- package/src/tui/render.d.ts +6 -0
- package/src/tui/render.js +3 -3
- package/src/tui/subagent-messages.js +1 -0
- package/src/workflows/scripted-workflow.d.ts +1 -1
- package/src/workflows/structured-workflow-scripts.d.ts +1 -0
- package/src/workflows/structured-workflow-scripts.js +5 -4
- package/src/workflows/workflow-auto-resume.d.ts +32 -0
- package/src/workflows/workflow-auto-resume.js +102 -0
- package/src/workflows/workflow-launch-record.d.ts +27 -0
- package/src/workflows/workflow-launch-record.js +48 -0
- package/src/workflows/workflow-resources.js +2 -2
- package/src/workflows/workflow-resume-claim.d.ts +82 -0
- package/src/workflows/workflow-resume-claim.js +210 -0
- package/src/workflows/workflow-resume.d.ts +56 -0
- package/src/workflows/workflow-resume.js +106 -0
- package/src/workflows/workflow-reuse.d.ts +15 -9
- package/src/workflows/workflow-reuse.js +77 -14
- package/src/workflows/workflow-revival.d.ts +2 -0
- package/src/workflows/workflow-revival.js +2 -1
- package/src/workflows/workflow-stop-cause.d.ts +6 -0
- package/src/workflows/workflow-stop-cause.js +8 -0
package/src/tui/fleet.js
CHANGED
|
@@ -2,6 +2,8 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
5
|
+
import { fleetContains, fleetLayout } from "./fleet-layout.js";
|
|
6
|
+
import { FleetViewport } from "./fleet-viewport.js";
|
|
5
7
|
import { snapshotExternalRuns } from "../api/external-runs.js";
|
|
6
8
|
import { getArtifactPaths, getArtifactsDir } from "../shared/artifacts.js";
|
|
7
9
|
import { formatDuration, formatModelThinking, formatTokens, formatTokenUsage, shortenPath } from "../shared/formatters.js";
|
|
@@ -14,7 +16,9 @@ import { steerAsyncRun } from "../runs/foreground/async-steering-action.js";
|
|
|
14
16
|
import { stopAsyncRun } from "../runs/foreground/async-stop-action.js";
|
|
15
17
|
import { resolveWorkflowForegroundSteeringTarget, steerWorkflowForegroundTarget } from "../runs/foreground/workflow-foreground-steering.js";
|
|
16
18
|
import { contextModeBadge, contextModeLabel } from "../runs/shared/context-mode.js";
|
|
19
|
+
import { foregroundChildStateLabel } from "../runs/foreground/foreground-history.js";
|
|
17
20
|
import { FLEET_STATUS_WIDGET_KEY, reportFleetWarning } from "./fleet-status.js";
|
|
21
|
+
import { stepTaskSummary } from "./render.js";
|
|
18
22
|
import { readFleetTranscript, renderFleetTranscript } from "./fleet-transcript.js";
|
|
19
23
|
import { runningTone } from "./running-tone.js";
|
|
20
24
|
import { childThinkingLevel } from "../shared/model-info.js";
|
|
@@ -108,18 +112,22 @@ function asyncItems(run, description) {
|
|
|
108
112
|
if (run.steps.length === 0 || run.mode === "workflow") {
|
|
109
113
|
return [{ key: `async:${run.id}`, kind: "async", runId: run.id, agent: run.mode, state: run.state, updatedAt, run, ...(description ? { description } : {}) }];
|
|
110
114
|
}
|
|
111
|
-
return run.steps.map((step) =>
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
115
|
+
return run.steps.map((step) => {
|
|
116
|
+
const task = stepTaskSummary(step, step.agent);
|
|
117
|
+
return {
|
|
118
|
+
key: `async:${run.id}:${step.index}`,
|
|
119
|
+
kind: "async",
|
|
120
|
+
runId: run.id,
|
|
121
|
+
index: step.index,
|
|
122
|
+
agent: step.label ? `${step.label} (${step.agent})` : step.agent,
|
|
123
|
+
state: step.status,
|
|
124
|
+
updatedAt: step.lastActivityAt ?? updatedAt,
|
|
125
|
+
run,
|
|
126
|
+
step,
|
|
127
|
+
...(task ? { task } : {}),
|
|
128
|
+
...(description ? { description } : {}),
|
|
129
|
+
};
|
|
130
|
+
});
|
|
123
131
|
}
|
|
124
132
|
function orderFleetAsyncRuns(runs, terminalLimit, trackedJobs) {
|
|
125
133
|
const updatedAt = (run) => run.lastUpdate ?? run.endedAt ?? run.startedAt;
|
|
@@ -443,7 +451,7 @@ function foregroundRecentDetail(item, state) {
|
|
|
443
451
|
const lines = [
|
|
444
452
|
`Run: ${item.runId}`,
|
|
445
453
|
"Source: foreground",
|
|
446
|
-
`State: ${child
|
|
454
|
+
`State: ${foregroundChildStateLabel(run, child)}`,
|
|
447
455
|
`Mode: ${run.mode}`,
|
|
448
456
|
`Child: ${child.index} (${child.agent})${contextModeLabel(child.context) ? ` ${contextModeLabel(child.context)}` : ""}`,
|
|
449
457
|
modelThinking ? `Model: ${modelThinking}` : undefined,
|
|
@@ -746,11 +754,8 @@ export class SubagentFleetComponent {
|
|
|
746
754
|
snapshot = { items: [] };
|
|
747
755
|
selected = 0;
|
|
748
756
|
selectedKey;
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
detailLineCount = 0;
|
|
752
|
-
detailViewportHeight = 8;
|
|
753
|
-
bodyHeight = 8;
|
|
757
|
+
viewport;
|
|
758
|
+
renderedLayout;
|
|
754
759
|
expandedTools = false;
|
|
755
760
|
promptAuditOpen = false;
|
|
756
761
|
promptAuditView = "authored";
|
|
@@ -773,6 +778,7 @@ export class SubagentFleetComponent {
|
|
|
773
778
|
keybindings;
|
|
774
779
|
constructor(tui, theme, state, done, options = {}) {
|
|
775
780
|
this.tui = tui;
|
|
781
|
+
this.viewport = new FleetViewport(() => this.tui.requestRender());
|
|
776
782
|
this.theme = theme;
|
|
777
783
|
this.markdownTheme = options.markdownTheme ?? getMarkdownTheme();
|
|
778
784
|
this.state = state;
|
|
@@ -820,11 +826,14 @@ export class SubagentFleetComponent {
|
|
|
820
826
|
this.selectedKey = this.snapshot.items[this.selected]?.key;
|
|
821
827
|
}
|
|
822
828
|
moveSelection(delta) {
|
|
823
|
-
|
|
829
|
+
this.selectItem(Math.max(0, Math.min(this.snapshot.items.length - 1, this.selected + delta)));
|
|
830
|
+
}
|
|
831
|
+
selectItem(index) {
|
|
832
|
+
if (!this.snapshot.items[index])
|
|
824
833
|
return;
|
|
825
|
-
this.selected =
|
|
834
|
+
this.selected = index;
|
|
826
835
|
this.selectedKey = this.snapshot.items[this.selected]?.key;
|
|
827
|
-
this.
|
|
836
|
+
this.viewport.follow();
|
|
828
837
|
this.resetActionInput();
|
|
829
838
|
this.tui.requestRender();
|
|
830
839
|
}
|
|
@@ -862,8 +871,7 @@ export class SubagentFleetComponent {
|
|
|
862
871
|
return;
|
|
863
872
|
this.selected = this.snapshot.items.findIndex((item) => item.key === target.key);
|
|
864
873
|
this.selectedKey = target.key;
|
|
865
|
-
this.
|
|
866
|
-
this.detailAutoFollow = false;
|
|
874
|
+
this.viewport.resetTop();
|
|
867
875
|
this.tui.requestRender();
|
|
868
876
|
}
|
|
869
877
|
resetActionInput() {
|
|
@@ -951,8 +959,7 @@ export class SubagentFleetComponent {
|
|
|
951
959
|
setActionNotice(result) {
|
|
952
960
|
this.actionNotice = result;
|
|
953
961
|
this.resetActionInput();
|
|
954
|
-
this.
|
|
955
|
-
this.detailScroll = 0;
|
|
962
|
+
this.viewport.resetTop();
|
|
956
963
|
this.refresh();
|
|
957
964
|
this.tui.requestRender();
|
|
958
965
|
}
|
|
@@ -972,10 +979,35 @@ export class SubagentFleetComponent {
|
|
|
972
979
|
});
|
|
973
980
|
}
|
|
974
981
|
scrollDetail(delta) {
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
982
|
+
this.viewport.scroll.scrollBy(delta);
|
|
983
|
+
}
|
|
984
|
+
handleMouse(event) {
|
|
985
|
+
// Block pointer fallback as well as inspector actions while a text/confirmation modal owns input.
|
|
986
|
+
if (this.steerDraft !== undefined || this.redoGuidanceDraft !== undefined || this.stopConfirming || this.actionBusy) {
|
|
987
|
+
return { handled: true, render: false };
|
|
988
|
+
}
|
|
989
|
+
const layout = this.renderedLayout;
|
|
990
|
+
if (!layout || layout.width !== event.width || layout.rows !== (this.tui.terminal?.rows ?? 32)) {
|
|
991
|
+
// Never use hit regions from a different size; wheel must not escape to the transcript.
|
|
992
|
+
return event.type === "wheel" ? { handled: true, render: false } : undefined;
|
|
993
|
+
}
|
|
994
|
+
if (event.type === "wheel" && fleetContains(layout.output, event.x, event.y)) {
|
|
995
|
+
this.scrollDetail(event.wheelDelta ?? 0);
|
|
996
|
+
return { handled: true, render: false };
|
|
997
|
+
}
|
|
998
|
+
if (this.promptAuditOpen)
|
|
999
|
+
return { handled: true, render: false };
|
|
1000
|
+
if (event.type === "click" && event.button === "left" && !event.shift && !event.alt && !event.ctrl
|
|
1001
|
+
&& fleetContains(layout.roster, event.x, event.y)) {
|
|
1002
|
+
const key = layout.rosterKeys[event.y - layout.roster.y];
|
|
1003
|
+
const index = key === undefined ? -1 : this.snapshot.items.findIndex((item) => item.key === key);
|
|
1004
|
+
if (index < 0)
|
|
1005
|
+
return undefined;
|
|
1006
|
+
this.selectItem(index);
|
|
1007
|
+
return { handled: true, render: false };
|
|
1008
|
+
}
|
|
1009
|
+
// Unowned wheel inside the overlay would otherwise scroll the transcript behind it.
|
|
1010
|
+
return event.type === "wheel" ? { handled: true, render: false } : undefined;
|
|
979
1011
|
}
|
|
980
1012
|
handleInput(data) {
|
|
981
1013
|
if (this.promptAuditOpen && this.redoGuidanceDraft !== undefined) {
|
|
@@ -1012,7 +1044,7 @@ export class SubagentFleetComponent {
|
|
|
1012
1044
|
if (this.promptAuditOpen) {
|
|
1013
1045
|
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
|
|
1014
1046
|
this.promptAuditOpen = false;
|
|
1015
|
-
this.
|
|
1047
|
+
this.viewport.follow();
|
|
1016
1048
|
this.tui.requestRender();
|
|
1017
1049
|
return;
|
|
1018
1050
|
}
|
|
@@ -1022,7 +1054,7 @@ export class SubagentFleetComponent {
|
|
|
1022
1054
|
return this.movePromptSelection(-1);
|
|
1023
1055
|
if (data === "1" || data === "2" || data === "3") {
|
|
1024
1056
|
this.promptAuditView = data === "1" ? "authored" : data === "2" ? "runtime" : "effective";
|
|
1025
|
-
this.
|
|
1057
|
+
this.viewport.resetTop();
|
|
1026
1058
|
this.tui.requestRender();
|
|
1027
1059
|
return;
|
|
1028
1060
|
}
|
|
@@ -1037,8 +1069,7 @@ export class SubagentFleetComponent {
|
|
|
1037
1069
|
else {
|
|
1038
1070
|
this.actionNotice = undefined;
|
|
1039
1071
|
this.redoGuidanceDraft = "";
|
|
1040
|
-
this.
|
|
1041
|
-
this.detailScroll = 0;
|
|
1072
|
+
this.viewport.resetTop();
|
|
1042
1073
|
this.tui.requestRender();
|
|
1043
1074
|
}
|
|
1044
1075
|
return;
|
|
@@ -1061,9 +1092,9 @@ export class SubagentFleetComponent {
|
|
|
1061
1092
|
if (matchesFleetAction(data, this.keybindings, "scrollDown"))
|
|
1062
1093
|
return this.scrollDetail(1);
|
|
1063
1094
|
if (matchesFleetAction(data, this.keybindings, "pageUp"))
|
|
1064
|
-
return this.scrollDetail(-this.
|
|
1095
|
+
return this.scrollDetail(-this.viewport.scroll.viewportHeight);
|
|
1065
1096
|
if (matchesFleetAction(data, this.keybindings, "pageDown"))
|
|
1066
|
-
return this.scrollDetail(this.
|
|
1097
|
+
return this.scrollDetail(this.viewport.scroll.viewportHeight);
|
|
1067
1098
|
return;
|
|
1068
1099
|
}
|
|
1069
1100
|
if (this.steerDraft !== undefined) {
|
|
@@ -1137,9 +1168,9 @@ export class SubagentFleetComponent {
|
|
|
1137
1168
|
if (matchesFleetAction(data, this.keybindings, "selectLast"))
|
|
1138
1169
|
return this.moveSelection(this.snapshot.items.length);
|
|
1139
1170
|
if (matchesFleetAction(data, this.keybindings, "pageUp"))
|
|
1140
|
-
return this.scrollDetail(-this.
|
|
1171
|
+
return this.scrollDetail(-this.viewport.scroll.viewportHeight);
|
|
1141
1172
|
if (matchesFleetAction(data, this.keybindings, "pageDown"))
|
|
1142
|
-
return this.scrollDetail(this.
|
|
1173
|
+
return this.scrollDetail(this.viewport.scroll.viewportHeight);
|
|
1143
1174
|
if (matchesFleetAction(data, this.keybindings, "refresh")) {
|
|
1144
1175
|
this.transcriptCache = undefined;
|
|
1145
1176
|
this.refresh();
|
|
@@ -1152,8 +1183,7 @@ export class SubagentFleetComponent {
|
|
|
1152
1183
|
else {
|
|
1153
1184
|
this.promptAuditOpen = true;
|
|
1154
1185
|
this.promptAuditView = "authored";
|
|
1155
|
-
this.
|
|
1156
|
-
this.detailAutoFollow = false;
|
|
1186
|
+
this.viewport.resetTop();
|
|
1157
1187
|
this.actionNotice = undefined;
|
|
1158
1188
|
this.tui.requestRender();
|
|
1159
1189
|
}
|
|
@@ -1166,8 +1196,7 @@ export class SubagentFleetComponent {
|
|
|
1166
1196
|
else {
|
|
1167
1197
|
this.actionNotice = undefined;
|
|
1168
1198
|
this.steerDraft = "";
|
|
1169
|
-
this.
|
|
1170
|
-
this.detailScroll = 0;
|
|
1199
|
+
this.viewport.resetTop();
|
|
1171
1200
|
this.tui.requestRender();
|
|
1172
1201
|
}
|
|
1173
1202
|
return;
|
|
@@ -1179,8 +1208,7 @@ export class SubagentFleetComponent {
|
|
|
1179
1208
|
else {
|
|
1180
1209
|
this.actionNotice = undefined;
|
|
1181
1210
|
this.stopConfirming = true;
|
|
1182
|
-
this.
|
|
1183
|
-
this.detailScroll = 0;
|
|
1211
|
+
this.viewport.resetTop();
|
|
1184
1212
|
this.tui.requestRender();
|
|
1185
1213
|
}
|
|
1186
1214
|
return;
|
|
@@ -1196,18 +1224,21 @@ export class SubagentFleetComponent {
|
|
|
1196
1224
|
return;
|
|
1197
1225
|
}
|
|
1198
1226
|
}
|
|
1199
|
-
rosterLines(
|
|
1227
|
+
rosterLines(layout) {
|
|
1200
1228
|
if (this.snapshot.items.length === 0)
|
|
1201
1229
|
return [this.theme.fg("dim", "No tracked children")];
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
const
|
|
1230
|
+
return layout.rosterKeys.map((_key, offset) => {
|
|
1231
|
+
const index = layout.rosterStart + offset;
|
|
1232
|
+
const item = this.snapshot.items[index];
|
|
1205
1233
|
const marker = index === this.selected ? this.theme.fg("accent", "›") : " ";
|
|
1206
1234
|
const context = item.kind === "async" ? contextModeBadge(this.theme, item.step?.context ?? item.run.context) : item.kind === "foreground-recent" ? contextModeBadge(this.theme, item.child.context) : "";
|
|
1207
|
-
|
|
1208
|
-
const
|
|
1235
|
+
// A child's task tells rows of one run apart; the run id stays in the detail header.
|
|
1236
|
+
const task = item.kind === "async" ? item.task : undefined;
|
|
1237
|
+
const name = task && item.kind === "async" && item.step ? item.step.agent : item.agent;
|
|
1238
|
+
const agent = index === this.selected ? this.theme.bold(name) : name;
|
|
1239
|
+
const identity = task ?? item.runId.slice(0, 8);
|
|
1209
1240
|
const left = `${marker} ${statusGlyph(item, this.theme)} ${agent}${context} ${this.theme.fg("dim", `· ${identity}`)}`;
|
|
1210
|
-
return rightAligned(left, this.theme.fg("dim", item.state), width);
|
|
1241
|
+
return rightAligned(left, this.theme.fg("dim", item.state), layout.roster.width);
|
|
1211
1242
|
});
|
|
1212
1243
|
}
|
|
1213
1244
|
renderedTranscript(target, width) {
|
|
@@ -1305,26 +1336,21 @@ export class SubagentFleetComponent {
|
|
|
1305
1336
|
return { header: [], body: this.withActionLines(lines) };
|
|
1306
1337
|
}
|
|
1307
1338
|
render(width) {
|
|
1308
|
-
|
|
1339
|
+
const layout = fleetLayout(width, this.tui.terminal?.rows ?? 32, this.selected, this.snapshot.items);
|
|
1340
|
+
this.renderedLayout = layout;
|
|
1341
|
+
if (!layout)
|
|
1309
1342
|
return [truncateToWidth("Subagent fleet needs at least 36 columns. Esc closes.", width)];
|
|
1310
1343
|
const innerWidth = width - 2;
|
|
1311
|
-
const
|
|
1312
|
-
|
|
1313
|
-
const
|
|
1314
|
-
const
|
|
1315
|
-
const roster = this.rosterLines(rosterWidth);
|
|
1344
|
+
const { bodyHeight } = layout;
|
|
1345
|
+
const rosterWidth = layout.roster.width;
|
|
1346
|
+
const detailWidth = layout.output.width;
|
|
1347
|
+
const roster = this.rosterLines(layout);
|
|
1316
1348
|
const detail = this.wrappedDetail(detailWidth);
|
|
1317
|
-
const detailHeader = detail.header.slice(0, Math.max(0,
|
|
1318
|
-
|
|
1319
|
-
this.detailLineCount = detail.body.length;
|
|
1320
|
-
const maxDetailScroll = Math.max(0, detail.body.length - this.detailViewportHeight);
|
|
1321
|
-
if (this.detailAutoFollow)
|
|
1322
|
-
this.detailScroll = maxDetailScroll;
|
|
1323
|
-
else if (this.detailScroll > maxDetailScroll)
|
|
1324
|
-
this.detailScroll = maxDetailScroll;
|
|
1349
|
+
const detailHeader = detail.header.slice(0, Math.max(0, bodyHeight - 1));
|
|
1350
|
+
const outputBodyHeight = bodyHeight - detailHeader.length;
|
|
1325
1351
|
const visibleDetails = [
|
|
1326
1352
|
...detailHeader,
|
|
1327
|
-
...
|
|
1353
|
+
...this.viewport.render(detail.body, detailWidth, outputBodyHeight),
|
|
1328
1354
|
];
|
|
1329
1355
|
const lines = [this.theme.fg("border", `╭${"─".repeat(innerWidth)}╮`)];
|
|
1330
1356
|
const selected = this.snapshot.items[this.selected];
|
|
@@ -1336,7 +1362,7 @@ export class SubagentFleetComponent {
|
|
|
1336
1362
|
: this.theme.fg("dim", "no children ");
|
|
1337
1363
|
lines.push(this.theme.fg("border", "│") + rightAligned(title, selectedStatus, innerWidth) + this.theme.fg("border", "│"));
|
|
1338
1364
|
lines.push(this.theme.fg("border", `├${"─".repeat(rosterWidth)}┬${"─".repeat(detailWidth)}┤`));
|
|
1339
|
-
for (let index = 0; index <
|
|
1365
|
+
for (let index = 0; index < bodyHeight; index++) {
|
|
1340
1366
|
lines.push(this.theme.fg("border", "│")
|
|
1341
1367
|
+ fit(roster[index] ?? "", rosterWidth)
|
|
1342
1368
|
+ this.theme.fg("border", "│")
|
|
@@ -1355,6 +1381,7 @@ export class SubagentFleetComponent {
|
|
|
1355
1381
|
return lines.map((line) => truncateToWidth(line, width));
|
|
1356
1382
|
}
|
|
1357
1383
|
invalidate() {
|
|
1384
|
+
this.renderedLayout = undefined;
|
|
1358
1385
|
this.transcriptCache = undefined;
|
|
1359
1386
|
this.refresh();
|
|
1360
1387
|
}
|
package/src/tui/render.d.ts
CHANGED
|
@@ -39,6 +39,12 @@ export interface AsyncLaneProjection {
|
|
|
39
39
|
/** Project already-loaded async status facts into one bounded, render-only lane row. */
|
|
40
40
|
export declare function projectAsyncLane(job: AsyncJobState, ...args: [selectedStep?: AsyncJobStep]): AsyncLaneProjection | undefined;
|
|
41
41
|
export declare function widgetRenderKey(job: AsyncJobState, expanded?: boolean): string;
|
|
42
|
+
/** A child's task in one line: its label and task, else its session name without the repeated agent name. */
|
|
43
|
+
export declare function stepTaskSummary(step: {
|
|
44
|
+
description?: string;
|
|
45
|
+
label?: string;
|
|
46
|
+
sessionName?: string;
|
|
47
|
+
}, agentName: string | undefined): string | undefined;
|
|
42
48
|
/** Structural identity of the workflow rows that Fleet can cover. */
|
|
43
49
|
export declare function inlineWorkflowRenderKey(job: AsyncJobState, children: AsyncJobState[]): string;
|
|
44
50
|
/** Presentation-only coverage from the mounted inline Fleet roster, never configuration. */
|
package/src/tui/render.js
CHANGED
|
@@ -1104,14 +1104,14 @@ function shouldCollapseSingleChildDetails(job, step) {
|
|
|
1104
1104
|
return isCompletedWidgetStepStatus(step.status);
|
|
1105
1105
|
return false;
|
|
1106
1106
|
}
|
|
1107
|
-
|
|
1107
|
+
/** A child's task in one line: its label and task, else its session name without the repeated agent name. */
|
|
1108
|
+
export function stepTaskSummary(step, agentName) {
|
|
1108
1109
|
const task = compactTaskText(step.description, step.label);
|
|
1109
1110
|
if (task)
|
|
1110
1111
|
return task;
|
|
1111
1112
|
const sessionName = step.sessionName?.trim();
|
|
1112
1113
|
if (!sessionName)
|
|
1113
1114
|
return undefined;
|
|
1114
|
-
const agentName = singleChildAgentName(job, step);
|
|
1115
1115
|
const sessionTask = stripRepeatedAgentPrefix(sessionName, agentName);
|
|
1116
1116
|
return sessionTask === agentName ? undefined : compactTaskText(sessionTask);
|
|
1117
1117
|
}
|
|
@@ -2223,7 +2223,7 @@ function foregroundStyleWidgetStepLines(job, theme, step, itemTitle, index, tota
|
|
|
2223
2223
|
const lane = projectAsyncLane(job, step);
|
|
2224
2224
|
if (lane)
|
|
2225
2225
|
lines.push(...formatLaneProjectionLines(lane, theme, detailIndent));
|
|
2226
|
-
const task = collapseDetails ?
|
|
2226
|
+
const task = collapseDetails ? stepTaskSummary(step, displayName) : compactTaskText(step.description, step.label);
|
|
2227
2227
|
if (task)
|
|
2228
2228
|
lines.push(`${detailIndent}${theme.fg("dim", `task: ${task}`)}`);
|
|
2229
2229
|
const activity = widgetStepActivityLine(step, width, expanded, job.updatedAt);
|
|
@@ -183,6 +183,7 @@ const MESSAGE_VIEWS = new Map([
|
|
|
183
183
|
["subagent-wait-subscription", readsContent("result", (message) => waitSubscriptionHeadline(message.details))],
|
|
184
184
|
["subagent-incremental-child-notify", readsContent("result", workflowChildHeadline)],
|
|
185
185
|
["subagent-workflow-result-write-failed", readsContent("result", () => ["workflow result ", { status: "save failed" }])],
|
|
186
|
+
["subagent-auto-resume", readsContent("result", () => ["workflow auto-resume ", { status: "failed" }])],
|
|
186
187
|
[SUBAGENT_CONTROL_MESSAGE_TYPE, readsContent("attention", (message) => controlNoticeHeadline(message.details))],
|
|
187
188
|
[SUBAGENT_STEERING_MESSAGE_TYPE, readsContent("attention", (message) => steeringNoticeHeadline(message.details))],
|
|
188
189
|
[SUBAGENT_WATCHDOG_WARNING_TYPE, (message) => watchdogWarningView(message.details, contentText(message.content))],
|
|
@@ -60,7 +60,7 @@ export interface WorkflowScriptChildResult {
|
|
|
60
60
|
};
|
|
61
61
|
artifactPaths: string[];
|
|
62
62
|
results?: SingleResult[];
|
|
63
|
-
/** Came from a runtime-replaced run of the same script (and args, unless named by options.reuseFrom): its saved result, or its still-running child re-attached; this run launched nothing. */
|
|
63
|
+
/** Came from a runtime-replaced or owner-exited run of the same script (and args, unless named by options.reuseFrom): its saved result, or its still-running child re-attached; this run launched nothing. */
|
|
64
64
|
reused?: boolean;
|
|
65
65
|
}
|
|
66
66
|
export interface WorkflowScriptTraceEntry {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* and never becomes code. Scripts run on the ordinary workflow runtime.
|
|
5
5
|
*/
|
|
6
6
|
export type StructuredWorkflowKind = "tasks" | "chain";
|
|
7
|
+
export declare const TOP_LEVEL_CONTROLS_HINT = " Put worktree and other child controls on the subagent call itself; they apply to every child.";
|
|
7
8
|
export declare function isPlainRecord(value: unknown): value is Record<string, unknown>;
|
|
8
9
|
/** Validate exact data shapes and expand them into a package-owned workflow script. */
|
|
9
10
|
export declare function buildStructuredWorkflowScript(kind: StructuredWorkflowKind, steps: unknown, originalTask: string | undefined): string;
|
|
@@ -7,6 +7,7 @@ const OUTPUT_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
|
7
7
|
const PLACEHOLDER_PATTERN = /\{(?:task|previous|outputs\.([A-Za-z_][A-Za-z0-9_]*))\}/g;
|
|
8
8
|
/** Optional per-child launch fields copied verbatim onto the generated runs.all item. */
|
|
9
9
|
const CHILD_FIELDS = ["cwd", "model"];
|
|
10
|
+
export const TOP_LEVEL_CONTROLS_HINT = " Put worktree and other child controls on the subagent call itself; they apply to every child.";
|
|
10
11
|
export function isPlainRecord(value) {
|
|
11
12
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
12
13
|
return false;
|
|
@@ -97,7 +98,7 @@ function buildTasksScript(steps, originalTask) {
|
|
|
97
98
|
throw new Error("tasks must be a non-empty array of { agent, task } items.");
|
|
98
99
|
const children = steps.map((item, index) => {
|
|
99
100
|
const path = `tasks[${index}]`;
|
|
100
|
-
const record = exactFields(item, path, ["agent", "task", ...CHILD_FIELDS],
|
|
101
|
+
const record = exactFields(item, path, ["agent", "task", ...CHILD_FIELDS], `Each tasks item accepts agent and task, plus optional cwd and model.${TOP_LEVEL_CONTROLS_HINT}`);
|
|
101
102
|
const agent = nonEmptyString(record.agent, `${path}.agent`);
|
|
102
103
|
const task = nonEmptyString(record.task, `${path}.task`);
|
|
103
104
|
const taskExpression = compileTemplate(task, {
|
|
@@ -131,20 +132,20 @@ function buildChainScript(steps, originalTask) {
|
|
|
131
132
|
let stepChildren;
|
|
132
133
|
let outputName;
|
|
133
134
|
if (isPlainRecord(step) && Object.hasOwn(step, "parallel")) {
|
|
134
|
-
const record = exactFields(step, path, ["parallel"],
|
|
135
|
+
const record = exactFields(step, path, ["parallel"], `A parallel step accepts only parallel; as is supported on sequential steps only.${TOP_LEVEL_CONTROLS_HINT}`);
|
|
135
136
|
const items = record.parallel;
|
|
136
137
|
if (!Array.isArray(items) || items.length === 0)
|
|
137
138
|
throw new Error(`${path}.parallel must be a non-empty array of { agent, task } items.`);
|
|
138
139
|
stepChildren = items.map((item, itemIndex) => {
|
|
139
140
|
const itemPath = `${path}.parallel[${itemIndex}]`;
|
|
140
|
-
const itemRecord = exactFields(item, itemPath, ["agent", "task", ...CHILD_FIELDS],
|
|
141
|
+
const itemRecord = exactFields(item, itemPath, ["agent", "task", ...CHILD_FIELDS], `Each parallel item accepts agent and task, plus optional cwd and model.${TOP_LEVEL_CONTROLS_HINT}`);
|
|
141
142
|
const agent = nonEmptyString(itemRecord.agent, `${itemPath}.agent`);
|
|
142
143
|
const task = nonEmptyString(itemRecord.task, `${itemPath}.task`);
|
|
143
144
|
return { key: `step-${number}-${itemIndex + 1}`, agent, fields: childFields(itemRecord, itemPath), taskExpression: compileTemplate(task, { path: `${itemPath}.task`, hasOriginalTask, previous, outputs }) };
|
|
144
145
|
});
|
|
145
146
|
}
|
|
146
147
|
else {
|
|
147
|
-
const record = exactFields(step, path, ["agent", "task", "as", ...CHILD_FIELDS],
|
|
148
|
+
const record = exactFields(step, path, ["agent", "task", "as", ...CHILD_FIELDS], `A sequential step accepts agent, task, as, cwd and model; a parallel step accepts only parallel.${TOP_LEVEL_CONTROLS_HINT}`);
|
|
148
149
|
const agent = nonEmptyString(record.agent, `${path}.agent`);
|
|
149
150
|
let task;
|
|
150
151
|
if (record.task === undefined) {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type PreparedWorkflowResume, type WorkflowResumeRefusalReason } from "./workflow-resume.ts";
|
|
2
|
+
/** Custom message posted when an auto-resume launch did not start. */
|
|
3
|
+
export declare const WORKFLOW_AUTO_RESUME_MESSAGE_TYPE = "subagent-auto-resume";
|
|
4
|
+
/** One planned auto-resume: its claim is held from session start until the launch starts or is cancelled. */
|
|
5
|
+
export interface PendingWorkflowAutoResume {
|
|
6
|
+
readonly sessionId: string;
|
|
7
|
+
/** The extension runtime that planned it. */
|
|
8
|
+
readonly owner: object;
|
|
9
|
+
readonly prepared: PreparedWorkflowResume;
|
|
10
|
+
/** `cancelled` before it started: its run id is reused by the next scan. */
|
|
11
|
+
state: "pending" | "started" | "cancelled";
|
|
12
|
+
}
|
|
13
|
+
/** Cancels matching launches that have not started and gives their claims back, synchronously. */
|
|
14
|
+
export declare function cancelPendingWorkflowAutoResumes(match: (entry: PendingWorkflowAutoResume) => boolean): void;
|
|
15
|
+
/** A cancelled entry stays recorded until its session's next scan reuses its run id. */
|
|
16
|
+
export declare function finishWorkflowAutoResume(entry: PendingWorkflowAutoResume): void;
|
|
17
|
+
/**
|
|
18
|
+
* Claims this session's interrupted top-level workflows for auto-resume. Synchronous and bounded:
|
|
19
|
+
* one listing of the session's terminal index, reads only for runs that ended in the last 24 h,
|
|
20
|
+
* then the cheap filters before any claim. A refusal is recorded in the run, so each run gets one
|
|
21
|
+
* automatic attempt; the launch itself happens later, off session start. Launches of this session
|
|
22
|
+
* that another runtime (or an earlier session start) planned but did not start are cancelled first,
|
|
23
|
+
* so their claims do not hide the runs from this scan.
|
|
24
|
+
*/
|
|
25
|
+
export declare function planWorkflowAutoResumes(input: {
|
|
26
|
+
asyncDirRoot: string;
|
|
27
|
+
currentSessionId: string;
|
|
28
|
+
now: number;
|
|
29
|
+
owner: object;
|
|
30
|
+
}): PendingWorkflowAutoResume[];
|
|
31
|
+
/** The one line posted when an auto-resume launch did not start. */
|
|
32
|
+
export declare function autoResumeFailureText(sourceRunId: string, refusal: Pick<WorkflowResumeRefusalReason, "reason" | "byHand">): string;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { readRecentTerminalRunIndex } from "../runs/background/terminal-run-index.js";
|
|
3
|
+
import { readStatus } from "../shared/utils.js";
|
|
4
|
+
import { hasWorkflowResumeRefusal, recordWorkflowResumeRefusal, refusalSentence, releaseWorkflowResumeClaim, workflowResumeClaimState } from "./workflow-resume-claim.js";
|
|
5
|
+
import { prepareWorkflowResume } from "./workflow-resume.js";
|
|
6
|
+
import { isReusableWorkflowStopCause, WORKFLOW_STOP_CAUSE_OWNER_EXITED } from "./workflow-stop-cause.js";
|
|
7
|
+
/** Custom message posted when an auto-resume launch did not start. */
|
|
8
|
+
export const WORKFLOW_AUTO_RESUME_MESSAGE_TYPE = "subagent-auto-resume";
|
|
9
|
+
/** Interrupted workflows older than this are left for a resume by hand. */
|
|
10
|
+
const AUTO_RESUME_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
11
|
+
const AUTO_RESUME_SCAN_LIMIT = 50;
|
|
12
|
+
/** When the run was interrupted: the dead owner's last update, or the runtime replacement. */
|
|
13
|
+
function interruptedAt(status) {
|
|
14
|
+
return status.workflow?.stopCause === WORKFLOW_STOP_CAUSE_OWNER_EXITED ? status.workflow.ownerExitedAt : status.endedAt;
|
|
15
|
+
}
|
|
16
|
+
// Shared by every runtime in the process: a reload's new runtime must see the old runtime's pending claims.
|
|
17
|
+
const registryKey = Symbol.for("pi-subagents.workflow-auto-resume.v1");
|
|
18
|
+
const registryGlobal = globalThis;
|
|
19
|
+
const pendingAutoResumes = registryGlobal[registryKey] ?? (registryGlobal[registryKey] = new Set());
|
|
20
|
+
/** Cancels matching launches that have not started and gives their claims back, synchronously. */
|
|
21
|
+
export function cancelPendingWorkflowAutoResumes(match) {
|
|
22
|
+
for (const entry of pendingAutoResumes) {
|
|
23
|
+
if (entry.state !== "pending" || !match(entry))
|
|
24
|
+
continue;
|
|
25
|
+
// It stays in the set, so the next scan of its session claims the run again under the same id and
|
|
26
|
+
// a notice that already said `Auto-resumed as <id>.` stays true.
|
|
27
|
+
entry.state = "cancelled";
|
|
28
|
+
try {
|
|
29
|
+
releaseWorkflowResumeClaim(entry.prepared.sourceAsyncDir, entry.prepared.newRunId);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
console.error(`Failed to give back the auto-resume claim of workflow ${entry.prepared.sourceRunId}:`, error);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** A cancelled entry stays recorded until its session's next scan reuses its run id. */
|
|
37
|
+
export function finishWorkflowAutoResume(entry) {
|
|
38
|
+
if (entry.state !== "cancelled")
|
|
39
|
+
pendingAutoResumes.delete(entry);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Claims this session's interrupted top-level workflows for auto-resume. Synchronous and bounded:
|
|
43
|
+
* one listing of the session's terminal index, reads only for runs that ended in the last 24 h,
|
|
44
|
+
* then the cheap filters before any claim. A refusal is recorded in the run, so each run gets one
|
|
45
|
+
* automatic attempt; the launch itself happens later, off session start. Launches of this session
|
|
46
|
+
* that another runtime (or an earlier session start) planned but did not start are cancelled first,
|
|
47
|
+
* so their claims do not hide the runs from this scan.
|
|
48
|
+
*/
|
|
49
|
+
export function planWorkflowAutoResumes(input) {
|
|
50
|
+
const { asyncDirRoot, currentSessionId, now } = input;
|
|
51
|
+
cancelPendingWorkflowAutoResumes((entry) => entry.sessionId === currentSessionId);
|
|
52
|
+
const reusableRunIds = new Map();
|
|
53
|
+
for (const entry of pendingAutoResumes) {
|
|
54
|
+
if (entry.sessionId !== currentSessionId || entry.state !== "cancelled")
|
|
55
|
+
continue;
|
|
56
|
+
reusableRunIds.set(entry.prepared.sourceRunId, entry.prepared.newRunId);
|
|
57
|
+
pendingAutoResumes.delete(entry);
|
|
58
|
+
}
|
|
59
|
+
let runIds;
|
|
60
|
+
try {
|
|
61
|
+
runIds = readRecentTerminalRunIndex(asyncDirRoot, { sessionId: currentSessionId, limit: AUTO_RESUME_SCAN_LIMIT, endedAfter: now - AUTO_RESUME_MAX_AGE_MS });
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
console.error("Failed to read interrupted workflows for auto-resume:", error);
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
const planned = [];
|
|
68
|
+
for (const runId of runIds) {
|
|
69
|
+
const asyncDir = path.join(asyncDirRoot, runId);
|
|
70
|
+
try {
|
|
71
|
+
const status = readStatus(asyncDir);
|
|
72
|
+
if (!status || status.mode !== "workflow" || status.parentWorkflowRunId !== undefined || status.sessionId !== currentSessionId)
|
|
73
|
+
continue;
|
|
74
|
+
if (!isReusableWorkflowStopCause(status.workflow?.stopCause))
|
|
75
|
+
continue;
|
|
76
|
+
const at = interruptedAt(status);
|
|
77
|
+
if (at === undefined || now - at > AUTO_RESUME_MAX_AGE_MS)
|
|
78
|
+
continue;
|
|
79
|
+
if (workflowResumeClaimState(asyncDir).kind !== "none" || hasWorkflowResumeRefusal(asyncDir))
|
|
80
|
+
continue;
|
|
81
|
+
const preparation = prepareWorkflowResume({ asyncDirRoot, sourceRunId: runId, trigger: "auto", now, currentSessionId, newRunId: reusableRunIds.get(runId) });
|
|
82
|
+
if (preparation.ok) {
|
|
83
|
+
const entry = { sessionId: currentSessionId, owner: input.owner, prepared: preparation.prepared, state: "pending" };
|
|
84
|
+
pendingAutoResumes.add(entry);
|
|
85
|
+
planned.push(entry);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
// Another claimant continues it; nothing to record.
|
|
89
|
+
if (preparation.code === "claim-busy" || preparation.code === "claim-resumed")
|
|
90
|
+
continue;
|
|
91
|
+
recordWorkflowResumeRefusal(asyncDir, { reason: preparation.reason, byHand: preparation.byHand }, now);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
console.error(`Failed to prepare auto-resume of workflow ${runId}:`, error);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return planned;
|
|
98
|
+
}
|
|
99
|
+
/** The one line posted when an auto-resume launch did not start. */
|
|
100
|
+
export function autoResumeFailureText(sourceRunId, refusal) {
|
|
101
|
+
return `Workflow ${sourceRunId} was not auto-resumed: ${refusalSentence(refusal.reason, refusal.byHand)}`;
|
|
102
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The public launch of an async top-level workflow, saved in its run directory so the run can be
|
|
3
|
+
* relaunched later. A named resource is saved by name (its `runs.host` permit cannot be saved);
|
|
4
|
+
* structured tasks/chain launches save no params, so they cannot be relaunched from this record.
|
|
5
|
+
*/
|
|
6
|
+
export type WorkflowLaunch = {
|
|
7
|
+
source: "script" | "resource";
|
|
8
|
+
params: Record<string, unknown>;
|
|
9
|
+
} | {
|
|
10
|
+
source: "structured";
|
|
11
|
+
};
|
|
12
|
+
export type WorkflowLaunchRecord = WorkflowLaunch & {
|
|
13
|
+
version: 1;
|
|
14
|
+
scriptDigest: string;
|
|
15
|
+
};
|
|
16
|
+
type WorkflowLaunchRecordRead = {
|
|
17
|
+
ok: true;
|
|
18
|
+
record: WorkflowLaunchRecord;
|
|
19
|
+
} | {
|
|
20
|
+
ok: false;
|
|
21
|
+
missing: boolean;
|
|
22
|
+
error: string;
|
|
23
|
+
};
|
|
24
|
+
/** Throws on a write failure; the launch must fail rather than start a run it cannot relaunch. */
|
|
25
|
+
export declare function writeWorkflowLaunchRecord(asyncDir: string, record: WorkflowLaunchRecord): void;
|
|
26
|
+
export declare function readWorkflowLaunchRecord(asyncDir: string): WorkflowLaunchRecordRead;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { writeAtomicJson } from "../shared/atomic-json.js";
|
|
4
|
+
const WORKFLOW_LAUNCH_RECORD_FILE = "launch.json";
|
|
5
|
+
function launchRecordPath(asyncDir) {
|
|
6
|
+
return path.join(asyncDir, WORKFLOW_LAUNCH_RECORD_FILE);
|
|
7
|
+
}
|
|
8
|
+
/** Throws on a write failure; the launch must fail rather than start a run it cannot relaunch. */
|
|
9
|
+
export function writeWorkflowLaunchRecord(asyncDir, record) {
|
|
10
|
+
writeAtomicJson(launchRecordPath(asyncDir), record);
|
|
11
|
+
}
|
|
12
|
+
function isPlainObject(value) {
|
|
13
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
function invalid(error) {
|
|
16
|
+
return { ok: false, missing: false, error };
|
|
17
|
+
}
|
|
18
|
+
export function readWorkflowLaunchRecord(asyncDir) {
|
|
19
|
+
let text;
|
|
20
|
+
try {
|
|
21
|
+
text = fs.readFileSync(launchRecordPath(asyncDir), "utf-8");
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error.code === "ENOENT")
|
|
25
|
+
return { ok: false, missing: true, error: "launch.json is missing" };
|
|
26
|
+
return invalid(`could not be read: ${error instanceof Error ? error.message : String(error)}`);
|
|
27
|
+
}
|
|
28
|
+
let value;
|
|
29
|
+
try {
|
|
30
|
+
value = JSON.parse(text);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
return invalid(`not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
34
|
+
}
|
|
35
|
+
if (!isPlainObject(value))
|
|
36
|
+
return invalid("not a JSON object");
|
|
37
|
+
if (value.version !== 1)
|
|
38
|
+
return invalid("version must be 1");
|
|
39
|
+
if (typeof value.scriptDigest !== "string" || !value.scriptDigest)
|
|
40
|
+
return invalid("scriptDigest must be a non-empty string");
|
|
41
|
+
if (value.source === "structured")
|
|
42
|
+
return { ok: true, record: { version: 1, source: "structured", scriptDigest: value.scriptDigest } };
|
|
43
|
+
if (value.source !== "script" && value.source !== "resource")
|
|
44
|
+
return invalid("source must be \"script\", \"resource\", or \"structured\"");
|
|
45
|
+
if (!isPlainObject(value.params))
|
|
46
|
+
return invalid("params must be an object");
|
|
47
|
+
return { ok: true, record: { version: 1, source: value.source, params: value.params, scriptDigest: value.scriptDigest } };
|
|
48
|
+
}
|