@selesai/code 0.8.5 → 0.8.6
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 +12 -0
- package/dist/defaults/models.json +5 -0
- package/dist/extensions/agent-browser.test.ts +178 -2
- package/dist/extensions/agent-browser.ts +2 -0
- package/dist/extensions/copy-turn.test.ts +229 -0
- package/dist/extensions/copy-turn.ts +11 -0
- package/dist/extensions/grep-app/index.test.ts +356 -1
- package/dist/extensions/grep-app/index.ts +6 -1
- package/dist/extensions/handoff-new.test.ts +351 -161
- package/dist/extensions/handoff-new.ts +3 -0
- package/dist/extensions/inline-skills.test.ts +93 -0
- package/dist/extensions/inline-skills.ts +3 -0
- package/dist/extensions/pi-subagents/agents/builder.md +5 -2
- package/dist/extensions/pi-subagents/agents/commentator.md +1 -1
- package/dist/extensions/pi-subagents/docs/configuration.md +8 -0
- package/dist/extensions/pi-subagents/docs/workflows.md +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +1 -1
- package/dist/extensions/pi-subagents/src/extension/config.ts +6 -0
- package/dist/extensions/pi-subagents/src/extension/index.ts +83 -14
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +12 -6
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +3 -0
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +5 -2
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +19 -1
- package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +5 -2
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +7 -5
- package/dist/extensions/pi-subagents/src/runs/background/wait-subscriptions.ts +2 -1
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +7 -4
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +39 -9
- package/dist/extensions/pi-subagents/src/shared/session-lineage.ts +71 -0
- package/dist/extensions/pi-subagents/src/shared/types.ts +16 -0
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +1 -1
- package/dist/extensions/pi-subagents/src/workflows/workflow-auto-relaunch.ts +28 -0
- package/dist/extensions/pi-subagents/test/unit/handoff-adoption.test.ts +103 -0
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +22 -0
- package/dist/extensions/pi-subagents/test/unit/session-lineage.test.ts +73 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-auto-relaunch.test.ts +45 -0
- package/dist/extensions/ponytail/index.js +11 -0
- package/dist/extensions/ponytail/ponytail-config.cjs +2 -0
- package/dist/extensions/ponytail/ponytail-instructions.cjs +6 -0
- package/dist/extensions/ponytail/test/extension.test.js +274 -140
- package/dist/extensions/ponytail/test/helpers.test.js +280 -92
- package/dist/extensions/question/index.ts +18 -0
- package/dist/extensions/question/question-list.ts +22 -0
- package/dist/extensions/question/tests/batch.test.ts +103 -65
- package/dist/extensions/question/tests/helpers.test.ts +68 -35
- package/dist/extensions/question/tests/question-list.test.ts +506 -204
- package/dist/extensions/question/tests/row-layout.test.ts +180 -114
- package/dist/extensions/question/tests/schemas.test.ts +42 -19
- package/dist/extensions/question/tests/shortcuts.test.ts +106 -84
- package/dist/extensions/question/tests/wizard.test.ts +771 -50
- package/dist/extensions/rtk.test.ts +180 -1
- package/dist/extensions/tokenin-onboarding.ts +3 -0
- package/dist/extensions/undo.test.ts +720 -0
- package/dist/extensions/undo.ts +6 -0
- package/dist/extensions/web-agent-onboarding.test.ts +415 -0
- package/dist/extensions/workflow/extension.ts +3 -3
- package/dist/extensions/workflow/modes.ts +41 -19
- package/dist/skills/pi-subagents/references/execution-controls.md +1 -1
- package/docs/plans/workflow-autoloop-reference.md +1 -2
- package/docs/plans/workflow-handoff-carryover.md +108 -0
- package/package.json +4 -1
|
@@ -22,6 +22,9 @@ function getSkillName(command: SkillCommand): string {
|
|
|
22
22
|
|
|
23
23
|
function getInlineSkillToken(textBeforeCursor: string): string | undefined {
|
|
24
24
|
const match = textBeforeCursor.match(/(?:^|[^a-z0-9_-])\$([a-z0-9-]*)$/i);
|
|
25
|
+
// Group 1 is always present when the regex matches (it can be empty but
|
|
26
|
+
// never undefined), so the ?? fallback is unreachable.
|
|
27
|
+
/* v8 ignore next 1 */
|
|
25
28
|
return match ? match[1] ?? "" : undefined;
|
|
26
29
|
}
|
|
27
30
|
|
|
@@ -21,12 +21,15 @@ Rules:
|
|
|
21
21
|
- If a required product, architecture, or scope decision is not approved: when the injected bridge instructions make `contact_supervisor` available, use it with `reason: "need_decision"` and wait; otherwise stop, do not guess, and report the exact blocking decision in your final response.
|
|
22
22
|
- Do not launch subagents. Do not send routine completion handoffs.
|
|
23
23
|
- Do not claim success without making the requested edits, unless you are blocked and report why.
|
|
24
|
+
- If the task specifies a progress file path, append a `## Round N` entry to that file before finishing (use the round number from the task; if none is given, count existing `## Round` entries and add one). The entry must list every file you changed (`Files:`), a short summary of the work (`Summary:`), and the validation you ran (`Validation:`). If the task names no progress file, skip this.
|
|
24
25
|
|
|
25
26
|
Before finishing, verify the requirement, changed files, and relevant tests/checks.
|
|
26
27
|
|
|
27
28
|
Final response:
|
|
28
29
|
|
|
29
30
|
Implemented: ...
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
Progress:
|
|
32
|
+
Files: ... (every file changed)
|
|
33
|
+
Summary: ... (one or two lines on what was done)
|
|
34
|
+
Validation: ... (checks run and outcome)
|
|
32
35
|
Open risks/questions: ...
|
|
@@ -14,7 +14,7 @@ acceptanceRole: read-only
|
|
|
14
14
|
|
|
15
15
|
You are a review-only subagent. Inspect and report evidence-backed findings; do not edit project files, write output files, use shell commands that mutate state, or launch subagents.
|
|
16
16
|
|
|
17
|
-
Review the supplied target directly. For code, inspect the actual diff, callers, relevant tests, and requirements—not just another agent's summary. Use `bash` only for read-only inspection or test commands.
|
|
17
|
+
Review the supplied target directly. If the task names a progress file, read it first and scope your review to its latest round entry: inspect the diff restricted to the files that entry lists (`git diff -- <files>`). Older entries are already reviewed—re-inspect only files the latest entry repeats. If no progress file is named, or it is missing or empty, review the full uncommitted diff. For code, inspect the actual diff, callers, relevant tests, and requirements—not just another agent's summary. Use `bash` only for read-only inspection or test commands.
|
|
18
18
|
|
|
19
19
|
Check:
|
|
20
20
|
- correctness, regressions, edge cases, and plan/requirement adherence;
|
|
@@ -155,6 +155,14 @@ Caps cumulative logical child admissions in one top-level run tree. The default
|
|
|
155
155
|
|
|
156
156
|
The budget counts single launches, expanded `tasks`/`count`, static chain steps and parallel groups, actual dynamic `expand` items, appended chain steps, workflow children, and nested child calls. Static and materialized dynamic groups are admitted atomically. Startup retries, model fallback, and retained-child resume reuse the original logical child claim. Claims are never released or refunded. This cap is independent from the session-wide cumulative spawn budget and `globalConcurrencyLimit`.
|
|
157
157
|
|
|
158
|
+
## `maxWorkflowAutoRelaunches`
|
|
159
|
+
|
|
160
|
+
```json
|
|
161
|
+
{ "maxWorkflowAutoRelaunches": 12 }
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Maximum automatic relaunches when an async scripted workflow ends with a fan-out `budget` result before the goal is clean. Each relaunch re-runs the same `workflowScript` as a fresh top-level run with a new fan-out budget, the same mission, and the same progress file, so the loop keeps building until the commentator reports clean. Defaults to 12; `0` means unlimited. When the cap is reached, the workflow completes as `failed` with a note to raise the cap or relaunch manually. Foreground workflows are unaffected — their budget results surface to the parent inline.
|
|
165
|
+
|
|
158
166
|
## `maxActiveAsyncRunsPerSession`
|
|
159
167
|
|
|
160
168
|
```json
|
|
@@ -62,7 +62,7 @@ return runs.run("test", { agent: "worker", task });
|
|
|
62
62
|
|
|
63
63
|
A plain workflow creates one enclosing mission by default. Its children do not create separate missions. The result exposes the id as `details.missionId`, and human-readable output ends with `Mission: <id> (<status>)`. Pass `mission:false` for an ephemeral workflow with no mission or durable `state` global.
|
|
64
64
|
|
|
65
|
-
For watched same-repo workflows, pass `async:false` to show the live in-chat workflow card. `chatProgress` can force `off` or `live-card` when the automatic policy is not what you want. Foreground workflows default to a 30-minute timeout; async workflows have no default timeout. See the [tool reference](tool-reference.md) for the full parameter list.
|
|
65
|
+
For watched same-repo workflows, pass `async:false` to show the live in-chat workflow card. `chatProgress` can force `off` or `live-card` when the automatic policy is not what you want. Foreground workflows default to a 30-minute timeout; async workflows have no default timeout. Async workflows that end with a fan-out `budget` result auto-relaunch as a fresh top-level run (new fan-out budget, same mission and progress file) until the commentator reports clean or the `maxWorkflowAutoRelaunches` cap is reached (default 12; 0 = unlimited). See the [tool reference](tool-reference.md) for the full parameter list.
|
|
66
66
|
|
|
67
67
|
The legacy `/chain`, `/parallel`, and `/run-chain` commands are not registered.
|
|
68
68
|
|
|
@@ -55,7 +55,7 @@ its resolved launch context as `[fresh]` or `[fork]`. Aggregate headers show
|
|
|
55
55
|
|
|
56
56
|
`workflowScript` is the scripted orchestration surface. Use `runs.run(key, { agent, task, ... })` for one child, `runs.all([...])` for parallel children, and ordinary JavaScript for sequence, branching, filtering, retries, and aggregation. Scripts are ordinary JavaScript statement bodies, so use an explicit return such as `workflowScript: "return runs.run('main', { agent: 'worker', task: '...' })"` for a useful one-child result. Prefer a single scripted workflow whenever the parent is starting a coordinated wave, such as multiple reviews, review plus gate monitor, worker then monitor setup, cross-repo prep lanes, or a fanout that the parent will consume together; use the declarative `chain` / `tasks` modes when the shape is known up front.
|
|
57
57
|
|
|
58
|
-
**Workflow modes as `workflowScript` auto-loops.** The bundled `workflow` extension ships its four mode shapes (task, prototype, quicktype, loop) as `workflowScript` auto-loops launched via `/workflow-*`. Each mode runs its phases as `runs.run` steps and loops the build
|
|
58
|
+
**Workflow modes as `workflowScript` auto-loops.** The bundled `workflow` extension ships its four mode shapes (task, prototype, quicktype, loop) as `workflowScript` auto-loops launched via `/workflow-*`. Each mode runs its phases as `runs.run` steps and loops the build → review → fix round (a blocking review triggers a fix round that addresses only the findings) until the commentator reports clean. There are no checkpoints (one-shot-and-sleep); durable state is the auto-created mission per launch; recover via `mission.list`/`status`.
|
|
59
59
|
|
|
60
60
|
```js
|
|
61
61
|
subagent({
|
|
@@ -59,6 +59,12 @@ function validateConfig(config: Record<string, unknown>): void {
|
|
|
59
59
|
|| config.maxActiveAsyncRunsPerSession < 0)) {
|
|
60
60
|
throw new Error("config.maxActiveAsyncRunsPerSession must be a non-negative integer");
|
|
61
61
|
}
|
|
62
|
+
if (config.maxWorkflowAutoRelaunches !== undefined
|
|
63
|
+
&& (typeof config.maxWorkflowAutoRelaunches !== "number"
|
|
64
|
+
|| !Number.isInteger(config.maxWorkflowAutoRelaunches)
|
|
65
|
+
|| config.maxWorkflowAutoRelaunches < 0)) {
|
|
66
|
+
throw new Error("config.maxWorkflowAutoRelaunches must be a non-negative integer");
|
|
67
|
+
}
|
|
62
68
|
validateMissionStoreConfig(config.missions);
|
|
63
69
|
validateAuthorityPolicy(config.authorityPolicy);
|
|
64
70
|
validatePermissionConfig(config.permissions);
|
|
@@ -16,6 +16,7 @@ import { randomUUID } from "node:crypto";
|
|
|
16
16
|
import * as fs from "node:fs";
|
|
17
17
|
import * as os from "node:os";
|
|
18
18
|
import * as path from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
19
20
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
20
21
|
import { keyText, type ExtensionAPI, type ExtensionContext, type ToolDefinition } from "@selesai/code";
|
|
21
22
|
import { Box, Container, Spacer, Text, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component } from "@earendil-works/pi-tui";
|
|
@@ -23,6 +24,7 @@ import { discoverAgents } from "../agents/agents.ts";
|
|
|
23
24
|
import { ensureAccessibleDir } from "../shared/accessible-dir.ts";
|
|
24
25
|
import { cleanupAllArtifactDirs, cleanupOldArtifacts, getArtifactsDir } from "../shared/artifacts.ts";
|
|
25
26
|
import { resolveCurrentSessionId } from "../shared/session-identity.ts";
|
|
27
|
+
import { resolveSessionLineage } from "../shared/session-lineage.ts";
|
|
26
28
|
import { cleanupOldChainDirs } from "../shared/settings.ts";
|
|
27
29
|
import { clearLegacyResultAnimationTimer, renderSubagentResult, renderSubagentSummary } from "../tui/render.ts";
|
|
28
30
|
import { openSubagentFleet } from "../tui/fleet.ts";
|
|
@@ -376,6 +378,12 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
376
378
|
const artifactCleanupDays = config.artifactConfig?.cleanupDays ?? DEFAULT_ARTIFACT_CONFIG.cleanupDays;
|
|
377
379
|
cleanupAllArtifactDirs(artifactCleanupDays);
|
|
378
380
|
|
|
381
|
+
// Replay guard for lineage result adoption: accept result files written at
|
|
382
|
+
// most this long before the adoption window opened (covers completions that
|
|
383
|
+
// raced the session switch). Older files were left behind by a dead watcher
|
|
384
|
+
// and are ignored until their owning session is resumed.
|
|
385
|
+
const ADOPTED_RESULT_SLACK_MS = 60_000;
|
|
386
|
+
|
|
379
387
|
const state: SubagentState = {
|
|
380
388
|
baseCwd: "",
|
|
381
389
|
currentSessionId: null,
|
|
@@ -630,19 +638,26 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
630
638
|
|
|
631
639
|
pi.on("agent_end", async (_event, ctx) => {
|
|
632
640
|
if (!ctx.hasUI) await drainOutstandingWork({ state, events: pi.events });
|
|
633
|
-
const
|
|
634
|
-
|
|
641
|
+
const ownerSessionIds = state.sessionLineage?.length
|
|
642
|
+
? state.sessionLineage
|
|
643
|
+
: state.currentSessionId ? [state.currentSessionId] : [];
|
|
644
|
+
if (ownerSessionIds.length === 0) return;
|
|
635
645
|
goalTurnId += 1;
|
|
636
646
|
try {
|
|
637
647
|
const location = resolveMissionStoreLocation({ projectRoot: state.baseCwd, ...(config.missions ? { config: config.missions } : {}) });
|
|
638
|
-
const retainedChildren = listRetainedChildren(DIRS.async,
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
648
|
+
const retainedChildren = listRetainedChildren(DIRS.async, ownerSessionIds);
|
|
649
|
+
const emittedMissions = new Set<string>();
|
|
650
|
+
for (const ownerSessionId of ownerSessionIds) {
|
|
651
|
+
for (const notice of collectGoalContinuationNotices({ location, ownerSessionId, retainedChildren, turnId: goalTurnId })) {
|
|
652
|
+
if (emittedMissions.has(notice.missionId)) continue;
|
|
653
|
+
emittedMissions.add(notice.missionId);
|
|
654
|
+
handleSubagentControlNotice({
|
|
655
|
+
pi,
|
|
656
|
+
state,
|
|
657
|
+
visibleControlNotices: new Set(),
|
|
658
|
+
details: { source: "goal", event: notice.event, noticeText: notice.message },
|
|
659
|
+
});
|
|
660
|
+
}
|
|
646
661
|
}
|
|
647
662
|
} catch (error) {
|
|
648
663
|
console.error("Failed to evaluate goal missions:", error);
|
|
@@ -650,6 +665,11 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
650
665
|
});
|
|
651
666
|
|
|
652
667
|
registerSlashCommands(pi, state, { fleetKeybindings: config.fleetKeybindings });
|
|
668
|
+
pi.on("resources_discover", () => {
|
|
669
|
+
return {
|
|
670
|
+
promptPaths: [fileURLToPath(new URL("../../prompts", import.meta.url))],
|
|
671
|
+
};
|
|
672
|
+
});
|
|
653
673
|
|
|
654
674
|
const eventUnsubscribeStoreKey = "__piSubagentEventUnsubscribes";
|
|
655
675
|
const controlNoticeSeenStoreKey = "__piSubagentVisibleControlNotices";
|
|
@@ -758,14 +778,47 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
758
778
|
resolveMaxActiveAsyncRunsPerSession(config.maxActiveAsyncRunsPerSession),
|
|
759
779
|
{ liveWorkflowRunIds: new Set(state.workflowControllers?.keys() ?? []) },
|
|
760
780
|
);
|
|
781
|
+
const adoptedActive = [...state.asyncJobs.values()].filter((job) => job.adopted && (job.status === "queued" || job.status === "running")).length;
|
|
782
|
+
if (adoptedActive > 0) {
|
|
783
|
+
state.activeAsyncCapacity = {
|
|
784
|
+
...state.activeAsyncCapacity,
|
|
785
|
+
used: state.activeAsyncCapacity.used + adoptedActive,
|
|
786
|
+
};
|
|
787
|
+
}
|
|
761
788
|
};
|
|
762
789
|
|
|
763
|
-
const
|
|
790
|
+
const formatActiveWorkflowSummary = (target: SubagentState, intro: string): string => {
|
|
791
|
+
const active = [...target.asyncJobs.values()]
|
|
792
|
+
.filter((job) => job.status === "queued" || job.status === "running")
|
|
793
|
+
.sort((left, right) => (left.startedAt ?? 0) - (right.startedAt ?? 0));
|
|
794
|
+
const lines = active.map((job) => {
|
|
795
|
+
const label = job.mode === "workflow"
|
|
796
|
+
? `workflow[${job.workflowKey ?? "script"}]`
|
|
797
|
+
: `${job.mode ?? "run"}${job.agents?.length ? ` ${job.agents.join(", ")}` : ""}`;
|
|
798
|
+
const step = job.currentStep !== undefined
|
|
799
|
+
? ` · step ${job.currentStep + 1}/${job.chainStepCount ?? job.stepsTotal ?? "?"}`
|
|
800
|
+
: "";
|
|
801
|
+
const activity = job.currentTool ? ` · last: ${job.currentTool}` : "";
|
|
802
|
+
const adopted = job.adopted ? " (carried over from a previous session)" : "";
|
|
803
|
+
return `- ${job.asyncId} ${label}${step}${activity}${adopted}`;
|
|
804
|
+
});
|
|
805
|
+
return [
|
|
806
|
+
intro,
|
|
807
|
+
...lines,
|
|
808
|
+
"Monitor with subagent status <id>; interrupt/resume/steer by run id; children.list lists completed steps. Results arrive automatically.",
|
|
809
|
+
].join("\n");
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
const resetSessionState = (ctx: ExtensionContext, recovering: boolean, previousSessionFile?: string | null) => {
|
|
764
813
|
state.widgetsSuspended = false;
|
|
765
814
|
state.baseCwd = ctx.cwd;
|
|
766
815
|
goalTurnId = 0;
|
|
767
816
|
state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
|
|
768
817
|
state.parentSessionFile = ctx.sessionManager.getSessionFile();
|
|
818
|
+
state.sessionLineage = resolveSessionLineage({ sessionManager: ctx.sessionManager }, { parentHint: previousSessionFile ?? null });
|
|
819
|
+
state.handoffResumePending = false;
|
|
820
|
+
if (state.sessionLineage.length > 1) state.adoptedResultWindowStart = Date.now() - ADOPTED_RESULT_SLACK_MS;
|
|
821
|
+
else state.adoptedResultWindowStart = undefined;
|
|
769
822
|
state.subagentSpawns = {
|
|
770
823
|
sessionId: state.currentSessionId,
|
|
771
824
|
count: 0,
|
|
@@ -791,7 +844,8 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
791
844
|
state.lastForegroundControlId = null;
|
|
792
845
|
resetJobs(ctx);
|
|
793
846
|
restoreForegroundRunHistory(state, { resultsDir: DIRS.results });
|
|
794
|
-
restoreActiveJobs(ctx);
|
|
847
|
+
const adoptedJobs = restoreActiveJobs(ctx);
|
|
848
|
+
state.handoffResumePending = adoptedJobs > 0 && state.lastUiContext?.hasUI === true;
|
|
795
849
|
scheduledRunManager.bindSession(ctx);
|
|
796
850
|
restoreSlashFinalSnapshots(ctx.sessionManager.getEntries());
|
|
797
851
|
waitSubscriptionManager.restore();
|
|
@@ -807,6 +861,17 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
807
861
|
|
|
808
862
|
pi.on("agent_settled", () => {
|
|
809
863
|
resumeWidgetsAfterCompaction();
|
|
864
|
+
if (state.handoffResumePending && state.lastUiContext?.hasUI === true) {
|
|
865
|
+
state.handoffResumePending = false;
|
|
866
|
+
pi.sendMessage(
|
|
867
|
+
{
|
|
868
|
+
customType: "subagent-handoff-resume",
|
|
869
|
+
content: formatActiveWorkflowSummary(state, "Session handoff detected. The previous session's background workflows were carried over:"),
|
|
870
|
+
display: false,
|
|
871
|
+
},
|
|
872
|
+
{ triggerTurn: true },
|
|
873
|
+
);
|
|
874
|
+
}
|
|
810
875
|
});
|
|
811
876
|
|
|
812
877
|
pi.on("session_before_compact", (event) => {
|
|
@@ -819,7 +884,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
819
884
|
pi.sendMessage(
|
|
820
885
|
{
|
|
821
886
|
customType: "subagent-compaction-resume",
|
|
822
|
-
content: "Compaction is complete. Resume the parent task now;
|
|
887
|
+
content: formatActiveWorkflowSummary(state, "Compaction is complete. Resume the parent task now; results for the runs below arrive separately when ready:"),
|
|
823
888
|
display: false,
|
|
824
889
|
},
|
|
825
890
|
{ triggerTurn: true },
|
|
@@ -828,7 +893,8 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
828
893
|
|
|
829
894
|
pi.on("session_start", (event, ctx) => {
|
|
830
895
|
const recovering = event.reason === "startup" || event.reason === "reload" || event.reason === "resume";
|
|
831
|
-
|
|
896
|
+
const previousSessionFile = event.reason === "new" || event.reason === "fork" ? event.previousSessionFile : null;
|
|
897
|
+
resetSessionState(ctx, recovering, previousSessionFile);
|
|
832
898
|
herdrStatusBridge.sessionStarted({
|
|
833
899
|
hasUI: ctx.hasUI === true,
|
|
834
900
|
runs: activeHerdrRuns(),
|
|
@@ -842,6 +908,9 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
842
908
|
stopResultWatcher();
|
|
843
909
|
state.currentSessionId = null;
|
|
844
910
|
state.parentSessionFile = null;
|
|
911
|
+
state.sessionLineage = undefined;
|
|
912
|
+
state.adoptedResultWindowStart = undefined;
|
|
913
|
+
state.handoffResumePending = false;
|
|
845
914
|
completionNotifier.dispose();
|
|
846
915
|
delete process.env[SUBAGENT_PARENT_SESSION_ENV];
|
|
847
916
|
for (const unsubscribe of eventUnsubscribes) {
|
|
@@ -54,7 +54,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
54
54
|
handleStarted: (data: unknown) => void;
|
|
55
55
|
handleComplete: (data: unknown) => void;
|
|
56
56
|
resetJobs: (ctx?: ExtensionContext) => void;
|
|
57
|
-
restoreActiveJobs: (ctx?: ExtensionContext) =>
|
|
57
|
+
restoreActiveJobs: (ctx?: ExtensionContext) => number;
|
|
58
58
|
dispose: () => void;
|
|
59
59
|
} {
|
|
60
60
|
const completionRetentionMs = options.completionRetentionMs ?? 10000;
|
|
@@ -645,26 +645,32 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
645
645
|
}
|
|
646
646
|
};
|
|
647
647
|
|
|
648
|
-
const restoreActiveJobs = (ctx?: ExtensionContext) => {
|
|
648
|
+
const restoreActiveJobs = (ctx?: ExtensionContext): number => {
|
|
649
649
|
if (ctx?.hasUI) state.lastUiContext = ctx;
|
|
650
|
-
if (!state.currentSessionId) return;
|
|
650
|
+
if (!state.currentSessionId) return 0;
|
|
651
|
+
const sessionIds = state.sessionLineage?.length ? state.sessionLineage : [state.currentSessionId];
|
|
651
652
|
let runs: AsyncRunSummary[];
|
|
652
653
|
try {
|
|
653
|
-
runs = listAsyncRuns(asyncDirRoot, { states: ["queued", "running"],
|
|
654
|
+
runs = listAsyncRuns(asyncDirRoot, { states: ["queued", "running"], sessionIds, resultsDir, kill: options.kill, now: options.now });
|
|
654
655
|
} catch (error) {
|
|
655
656
|
console.error(`Failed to restore active async jobs from '${asyncDirRoot}':`, error);
|
|
656
|
-
return;
|
|
657
|
+
return 0;
|
|
657
658
|
}
|
|
659
|
+
let adopted = 0;
|
|
658
660
|
for (const run of runs) {
|
|
661
|
+
if (state.asyncJobs.has(run.id)) continue;
|
|
659
662
|
const job = summaryToJob(run);
|
|
663
|
+
job.adopted = run.sessionId !== undefined && run.sessionId !== state.currentSessionId;
|
|
660
664
|
state.asyncJobs.set(run.id, job);
|
|
661
665
|
if (job.status === "running") runningJobIds.add(job.asyncId);
|
|
662
666
|
rememberFleetJob(state, job);
|
|
663
667
|
watchJob(job);
|
|
668
|
+
if (job.adopted) adopted += 1;
|
|
664
669
|
}
|
|
665
|
-
if (runs.length === 0) return;
|
|
670
|
+
if (runs.length === 0) return 0;
|
|
666
671
|
ensurePoller();
|
|
667
672
|
rerenderLastWidget();
|
|
673
|
+
return adopted;
|
|
668
674
|
};
|
|
669
675
|
|
|
670
676
|
return { ensurePoller, refreshWidget, handleStarted, handleComplete, resetJobs, restoreActiveJobs, dispose };
|
|
@@ -120,6 +120,8 @@ export interface AsyncRunSummary {
|
|
|
120
120
|
interface AsyncRunListOptions {
|
|
121
121
|
states?: Array<AsyncRunSummary["state"]>;
|
|
122
122
|
sessionId?: string;
|
|
123
|
+
/** Accept runs owned by any of these session identities (union). */
|
|
124
|
+
sessionIds?: string[];
|
|
123
125
|
limit?: number;
|
|
124
126
|
/** Limits status-file reads after candidates are ordered by status mtime. */
|
|
125
127
|
entryLimit?: number;
|
|
@@ -468,6 +470,7 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
468
470
|
// match.
|
|
469
471
|
if (allowedStates && !allowedStates.has(status.state)) continue;
|
|
470
472
|
if (options.sessionId && status.sessionId !== options.sessionId) continue;
|
|
473
|
+
if (options.sessionIds && (status.sessionId === undefined || !options.sessionIds.includes(status.sessionId))) continue;
|
|
471
474
|
const nestedWarnings: string[] = [];
|
|
472
475
|
let nestedRoute: NestedRoute | undefined;
|
|
473
476
|
if (options.reconcile !== false) {
|
|
@@ -239,7 +239,7 @@ export function buildCompletionDetails(result: CompletionNotification): Subagent
|
|
|
239
239
|
|
|
240
240
|
export default function registerSubagentNotify(
|
|
241
241
|
pi: ExtensionAPI,
|
|
242
|
-
state: Pick<SubagentState, "currentSessionId">,
|
|
242
|
+
state: Pick<SubagentState, "currentSessionId" | "sessionLineage">,
|
|
243
243
|
options: RegisterSubagentNotifyOptions = {},
|
|
244
244
|
): CompletionNotifier {
|
|
245
245
|
const seen = new Map<string, number>();
|
|
@@ -250,6 +250,9 @@ export default function registerSubagentNotify(
|
|
|
250
250
|
const batchers = new Map<string, CompletionBatcher<PendingCompletion>>();
|
|
251
251
|
let disposed = false;
|
|
252
252
|
|
|
253
|
+
const ownsSession = (sessionId: string) =>
|
|
254
|
+
sessionId === state.currentSessionId || state.sessionLineage?.includes(sessionId) === true;
|
|
255
|
+
|
|
253
256
|
const settle = (items: PendingCompletion[], accepted: boolean) => {
|
|
254
257
|
for (const item of items) {
|
|
255
258
|
pending.delete(item.key);
|
|
@@ -274,7 +277,7 @@ export default function registerSubagentNotify(
|
|
|
274
277
|
};
|
|
275
278
|
|
|
276
279
|
const deliver = (result: CompletionNotification): Promise<boolean> => {
|
|
277
|
-
if (disposed || typeof result.sessionId !== "string" || result.sessionId
|
|
280
|
+
if (disposed || typeof result.sessionId !== "string" || !ownsSession(result.sessionId)) return Promise.resolve(false);
|
|
278
281
|
if (result.intercomDelivered === true) return Promise.resolve(true);
|
|
279
282
|
const key = buildCompletionKey(result, "notify");
|
|
280
283
|
const seenAt = seen.get(key);
|
|
@@ -168,7 +168,24 @@ export function createResultWatcher(
|
|
|
168
168
|
const ownsSession = (sessionId: string, epoch: number) => {
|
|
169
169
|
if (!deliveryActive || epoch !== deliveryEpoch) return false;
|
|
170
170
|
if (!activeSessionId && state.currentSessionId) activeSessionId = state.currentSessionId;
|
|
171
|
-
|
|
171
|
+
if (activeSessionId !== state.currentSessionId) return false;
|
|
172
|
+
if (sessionId === state.currentSessionId) return true;
|
|
173
|
+
return (state.sessionLineage ?? []).includes(sessionId);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const lineageOwnsResult = (identity: ResultFileIdentity, file: string): boolean => {
|
|
177
|
+
const lineage = state.sessionLineage;
|
|
178
|
+
if (!identity.sessionId || !lineage || !lineage.includes(identity.sessionId)) return false;
|
|
179
|
+
// Replay guard: only accept lineage results written around or after the
|
|
180
|
+
// adoption window. The previous session's watcher deleted every result it
|
|
181
|
+
// delivered, so older files are stale leftovers, not missed deliveries.
|
|
182
|
+
const windowStart = state.adoptedResultWindowStart;
|
|
183
|
+
if (windowStart === undefined) return false;
|
|
184
|
+
try {
|
|
185
|
+
return fsApi.statSync(path.join(resultsDir, file)).mtimeMs >= windowStart;
|
|
186
|
+
} catch {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
172
189
|
};
|
|
173
190
|
|
|
174
191
|
const scheduleResult = (file: string, triggerTurn: boolean, delayMs = 0) => {
|
|
@@ -210,6 +227,7 @@ export function createResultWatcher(
|
|
|
210
227
|
// files keep their existing diagnostics and compatibility behavior.
|
|
211
228
|
if (!identity.sessionId) return true;
|
|
212
229
|
if (identity.sessionId === state.currentSessionId) return true;
|
|
230
|
+
if (lineageOwnsResult(identity, file)) return true;
|
|
213
231
|
if (identity.asyncDir && fsApi.existsSync(path.join(identity.asyncDir, MISSION_BINDING_FILE))) return true;
|
|
214
232
|
if (identity.runId && (observed ?? observedRunIds()).has(identity.runId)) return true;
|
|
215
233
|
return Boolean(deps.observeCompletion && !deps.observedCompletionRunIds);
|
|
@@ -32,8 +32,8 @@ function boundedTaskSummary(value: string | undefined): string {
|
|
|
32
32
|
: normalized;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
export function listRetainedChildren(asyncDirRoot: string,
|
|
36
|
-
return listAsyncRuns(asyncDirRoot, {
|
|
35
|
+
export function listRetainedChildren(asyncDirRoot: string, sessionIds: string[]): RetainedChild[] {
|
|
36
|
+
return listAsyncRuns(asyncDirRoot, { sessionIds, states: ["complete"], reconcile: false })
|
|
37
37
|
.flatMap((run) => {
|
|
38
38
|
if (!run.parentWorkflowRunId || run.steps.length !== 1) return [];
|
|
39
39
|
const step = run.steps[0]!;
|
|
@@ -94,9 +94,10 @@ function toolCallIdAsyncLocations(toolCallId: string, asyncDirRoot: string, resu
|
|
|
94
94
|
|
|
95
95
|
function foregroundIds(state: SubagentState | undefined): string[] {
|
|
96
96
|
if (!state) return [];
|
|
97
|
+
const acceptedSessions = new Set(state.sessionLineage?.length ? state.sessionLineage : state.currentSessionId ? [state.currentSessionId] : []);
|
|
97
98
|
const remembered = state.currentSessionId
|
|
98
99
|
? [...(state.foregroundRuns?.values() ?? [])]
|
|
99
|
-
.filter((run) => run.sessionId
|
|
100
|
+
.filter((run) => run.sessionId !== undefined && acceptedSessions.has(run.sessionId))
|
|
100
101
|
.map((run) => run.runId)
|
|
101
102
|
: [];
|
|
102
103
|
return [...new Set([...state.foregroundControls.keys(), ...remembered])];
|
|
@@ -106,7 +107,9 @@ function hasExactForegroundId(state: SubagentState | undefined, id: string): boo
|
|
|
106
107
|
if (!state) return false;
|
|
107
108
|
if (state.foregroundControls.has(id)) return true;
|
|
108
109
|
const remembered = state.foregroundRuns?.get(id);
|
|
109
|
-
|
|
110
|
+
if (!remembered || !state.currentSessionId || remembered.sessionId === undefined) return false;
|
|
111
|
+
const acceptedSessions = new Set(state.sessionLineage?.length ? state.sessionLineage : [state.currentSessionId]);
|
|
112
|
+
return acceptedSessions.has(remembered.sessionId);
|
|
110
113
|
}
|
|
111
114
|
|
|
112
115
|
function nestedScopeFromState(state: SubagentState | undefined): NestedRunResolutionScope | undefined {
|
|
@@ -217,6 +217,8 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
217
217
|
const asyncDirRoot = deps.asyncDirRoot ?? DIRS.async;
|
|
218
218
|
const resultsDir = deps.resultsDir ?? DIRS.results;
|
|
219
219
|
const currentSessionId = deps.state?.currentSessionId ?? undefined;
|
|
220
|
+
const acceptedSessions = new Set<string>(deps.state?.sessionLineage?.length ? deps.state.sessionLineage : currentSessionId ? [currentSessionId] : []);
|
|
221
|
+
const ownsSession = (sessionId: string | undefined) => sessionId !== undefined && acceptedSessions.has(sessionId);
|
|
220
222
|
if (params.view && params.view !== "fleet" && params.view !== "transcript") {
|
|
221
223
|
return {
|
|
222
224
|
content: [{ type: "text", text: `Unknown status view: ${params.view}. Valid: fleet, transcript.` }],
|
|
@@ -236,7 +238,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
236
238
|
};
|
|
237
239
|
}
|
|
238
240
|
try {
|
|
239
|
-
const runs = listAsyncRuns(asyncDirRoot, { states: ["queued", "running"],
|
|
241
|
+
const runs = listAsyncRuns(asyncDirRoot, { states: ["queued", "running"], ...(acceptedSessions.size ? { sessionIds: [...acceptedSessions] } : {}), resultsDir, kill: deps.kill, now: deps.now });
|
|
240
242
|
if (params.view === "transcript") {
|
|
241
243
|
if (runs.length === 1) return inspectSubagentStatus({ ...params, id: runs[0]!.id }, deps);
|
|
242
244
|
return {
|
|
@@ -332,9 +334,9 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
332
334
|
const status = reconciliation.status;
|
|
333
335
|
if (!status && diskStatus?.displayDismissedAt !== undefined) {
|
|
334
336
|
if (params.view === "transcript") {
|
|
335
|
-
if (currentSessionId && diskStatus.sessionId
|
|
337
|
+
if (currentSessionId && !ownsSession(diskStatus.sessionId)) {
|
|
336
338
|
return {
|
|
337
|
-
content: [{ type: "text", text: "Transcript view is only available for async runs owned by the current session." }],
|
|
339
|
+
content: [{ type: "text", text: "Transcript view is only available for async runs owned by the current session or its carried-over predecessors." }],
|
|
338
340
|
isError: true,
|
|
339
341
|
details: { mode: "single", results: [] },
|
|
340
342
|
};
|
|
@@ -351,9 +353,9 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
351
353
|
const eventsPath = path.join(asyncDir, "events.jsonl");
|
|
352
354
|
if (status) {
|
|
353
355
|
if (params.view === "transcript") {
|
|
354
|
-
if (currentSessionId && status.sessionId
|
|
356
|
+
if (currentSessionId && !ownsSession(status.sessionId)) {
|
|
355
357
|
return {
|
|
356
|
-
content: [{ type: "text", text: "Transcript view is only available for async runs owned by the current session." }],
|
|
358
|
+
content: [{ type: "text", text: "Transcript view is only available for async runs owned by the current session or its carried-over predecessors." }],
|
|
357
359
|
isError: true,
|
|
358
360
|
details: { mode: "single", results: [] },
|
|
359
361
|
};
|
|
@@ -245,7 +245,8 @@ export function createWaitSubscriptionManager(
|
|
|
245
245
|
try {
|
|
246
246
|
const record = parseRecord(JSON.parse(fs.readFileSync(path.join(subscriptionsDir, file), "utf-8")));
|
|
247
247
|
if (!record || file !== `${record.token}.json`) continue;
|
|
248
|
-
|
|
248
|
+
const acceptedSessions = new Set(state.sessionLineage?.length ? state.sessionLineage : [state.currentSessionId]);
|
|
249
|
+
if (acceptedSessions.has(record.sessionId)) {
|
|
249
250
|
subscriptions.set(record.token, record);
|
|
250
251
|
if (record.targetKind === "foreground" && !state.foregroundRuns?.has(record.runId)) unresolvedRestoredForegroundTokens.add(record.token);
|
|
251
252
|
}
|
|
@@ -121,11 +121,14 @@ export function persistForegroundRunHistory(state: SubagentState, options: { res
|
|
|
121
121
|
writeAtomicJson(historyPath(resultsDir), { version: HISTORY_VERSION, runs });
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
export function restoreForegroundRunHistory(state: SubagentState, options: { resultsDir?: string; sessionId?: string | null; limit?: number } = {}): number {
|
|
125
|
-
const
|
|
126
|
-
|
|
124
|
+
export function restoreForegroundRunHistory(state: SubagentState, options: { resultsDir?: string; sessionId?: string | null; sessionIds?: string[]; limit?: number } = {}): number {
|
|
125
|
+
const sessionIds = options.sessionIds
|
|
126
|
+
?? (options.sessionId ? [options.sessionId] : state.sessionLineage)
|
|
127
|
+
?? (state.currentSessionId ? [state.currentSessionId] : []);
|
|
128
|
+
if (sessionIds.length === 0) return 0;
|
|
129
|
+
const accepted = new Set(sessionIds);
|
|
127
130
|
const index = readIndex(options.resultsDir ?? DIRS.results);
|
|
128
|
-
const runs = sortAndBound(index.runs.filter((run) => run.sessionId
|
|
131
|
+
const runs = sortAndBound(index.runs.filter((run) => run.sessionId !== undefined && accepted.has(run.sessionId)), options.limit ?? MAX_REMEMBERED_FOREGROUND_RUNS);
|
|
129
132
|
state.foregroundRuns ??= new Map();
|
|
130
133
|
let restored = 0;
|
|
131
134
|
for (const run of runs) {
|
|
@@ -115,6 +115,7 @@ import { resolveAuthorityDecision } from "../../policy/authority.ts";
|
|
|
115
115
|
import { handleHerdrInspectorAction, HERDR_INSPECTOR_ACTIONS } from "../../inspectors/herdr/actions.ts";
|
|
116
116
|
import { handleHerdrProjectPaneAction, HERDR_PROJECT_PANE_ACTIONS } from "../../inspectors/herdr/project-panes.ts";
|
|
117
117
|
import { previewSimpleWorkflowRun, runWorkflowScript, WorkflowScriptError, type WorkflowScriptChildResult } from "../../workflows/scripted-workflow.ts";
|
|
118
|
+
import { resolveAutoRelaunchDecision, resolveMaxWorkflowAutoRelaunches } from "../../workflows/workflow-auto-relaunch.ts";
|
|
118
119
|
import { renderWorkflowPrompt } from "../../shared/prompt-resources.ts";
|
|
119
120
|
import { resolveWorkflowChatProgress, type WorkflowChatProgressProjection } from "../../workflows/chat-progress.ts";
|
|
120
121
|
import {
|
|
@@ -781,7 +782,8 @@ function updateRememberedForegroundChild(state: SubagentState, input: { runId: s
|
|
|
781
782
|
function resolveForegroundResumeTarget(params: SubagentParamsLike, state: SubagentState): { runId: string; mode: SubagentRunMode; state: "complete"; agent: string; index: number; cwd: string; sessionFile: string; model?: string; thinking?: string; launchContractDigest?: string; capabilityCeiling?: ResolvedSubagentCapabilityCeiling } | undefined {
|
|
782
783
|
const requested = (params.id ?? params.runId)?.trim();
|
|
783
784
|
if (!requested || !state.foregroundRuns?.size || !state.currentSessionId) return undefined;
|
|
784
|
-
const
|
|
785
|
+
const acceptedSessions = new Set(state.sessionLineage?.length ? state.sessionLineage : [state.currentSessionId]);
|
|
786
|
+
const sessionRuns = [...state.foregroundRuns.values()].filter((run) => run.sessionId !== undefined && acceptedSessions.has(run.sessionId));
|
|
785
787
|
const direct = sessionRuns.find((run) => run.runId === requested);
|
|
786
788
|
const matches = direct ? [direct] : sessionRuns.filter((run) => run.runId.startsWith(requested));
|
|
787
789
|
if (matches.length === 0) return undefined;
|
|
@@ -4889,8 +4891,17 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
4889
4891
|
appendWorkflowEvent({ type: "subagent.workflow.trace", trace });
|
|
4890
4892
|
};
|
|
4891
4893
|
try {
|
|
4892
|
-
const
|
|
4893
|
-
|
|
4894
|
+
const autoRelaunchCap = resolveMaxWorkflowAutoRelaunches(deps.config.maxWorkflowAutoRelaunches);
|
|
4895
|
+
let relaunched = 0;
|
|
4896
|
+
let capReached = false;
|
|
4897
|
+
let workflow: Awaited<ReturnType<typeof runWorkflowScript>> | undefined;
|
|
4898
|
+
while (true) {
|
|
4899
|
+
if (relaunched > 0) {
|
|
4900
|
+
workflowFanoutBudget = createRunFanoutBudget(`${workflowRunId}#${relaunched + 1}`, workflowFanoutBudget.limit);
|
|
4901
|
+
status = { ...status, workflow: { trace: [], emits: [], console: [] } };
|
|
4902
|
+
}
|
|
4903
|
+
workflow = await runWorkflowScript({
|
|
4904
|
+
script: workflowScript,
|
|
4894
4905
|
timeoutMs: timeout,
|
|
4895
4906
|
signal: controller.signal,
|
|
4896
4907
|
prompts: workflowPrompts,
|
|
@@ -4988,14 +4999,33 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
4988
4999
|
},
|
|
4989
5000
|
status: async (keyOrRunId, workflowSignal) => workflowChildResult(keyOrRunId, await execute(randomUUID(), { action: "status", id: keyOrRunId }, workflowSignal, undefined, ctx, preserveActiveSession)),
|
|
4990
5001
|
});
|
|
5002
|
+
const decision = resolveAutoRelaunchDecision(workflow?.value, relaunched, autoRelaunchCap);
|
|
5003
|
+
if (!decision.relaunch) {
|
|
5004
|
+
capReached = decision.capped;
|
|
5005
|
+
break;
|
|
5006
|
+
}
|
|
5007
|
+
relaunched += 1;
|
|
5008
|
+
status = { ...status, workflowRelaunchCount: relaunched };
|
|
5009
|
+
persist();
|
|
5010
|
+
appendWorkflowEvent({ type: "subagent.workflow.relaunched", relaunched });
|
|
5011
|
+
}
|
|
5012
|
+
if (!workflow) throw new Error("Workflow script produced no result.");
|
|
4991
5013
|
const returnPreview = formatWorkflowValue(workflow.value).slice(0, 1_000);
|
|
4992
5014
|
const emitPreview = workflow.emits.length > 0 ? ` Emitted: ${workflow.emits.map(formatWorkflowValue).join(", ").slice(0, 1_000)}` : "";
|
|
4993
|
-
const
|
|
5015
|
+
const relaunchNote = relaunched > 0 ? `Auto-relaunched ${relaunched} time(s) with a fresh fan-out budget. ` : "";
|
|
5016
|
+
const summary = `${relaunchNote}Workflow completed with ${workflow.children.length} child run(s). Return: ${returnPreview}${emitPreview} Trace: ${workflow.trace.length} event(s).`;
|
|
4994
5017
|
const workflowUsage = sumResultsUsage(workflowResults);
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
5018
|
+
if (capReached) {
|
|
5019
|
+
status = compactOptional<AsyncStatus>({ ...status, state: "failed", error: `Workflow auto-relaunch cap reached after ${relaunched} relaunch(es); the goal is not clean yet. The progress file is current; raise config.maxWorkflowAutoRelaunches or relaunch manually.`, endedAt: Date.now(), workflow: { value: workflow.value, trace: workflow.trace, emits: workflow.emits, console: workflow.console } });
|
|
5020
|
+
writeAtomicJson(resultPath, { id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: false, state: "failed", summary: status.error, error: status.error, results: workflow.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd: workflowCwd, sessionId: currentSessionId, timestamp: Date.now(), durationMs: Date.now() - startedAt });
|
|
5021
|
+
persist();
|
|
5022
|
+
appendWorkflowEvent({ type: "subagent.workflow.completed", state: "failed", error: status.error });
|
|
5023
|
+
} else {
|
|
5024
|
+
status = { ...status, state: "complete", endedAt: Date.now(), workflow: { value: workflow.value, trace: workflow.trace, emits: workflow.emits, console: workflow.console }, totalTokens: { input: workflowUsage.input, output: workflowUsage.output, total: workflowUsage.input + workflowUsage.output }, totalCost: sumResultsCost(workflowResults) };
|
|
5025
|
+
writeAtomicJson(resultPath, { id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: true, state: "complete", summary, output: summary, results: workflow.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd: workflowCwd, sessionId: currentSessionId, timestamp: Date.now(), durationMs: Date.now() - startedAt });
|
|
5026
|
+
persist();
|
|
5027
|
+
appendWorkflowEvent({ type: "subagent.workflow.completed", state: "complete" });
|
|
5028
|
+
}
|
|
4999
5029
|
} catch (error) {
|
|
5000
5030
|
const partial = error instanceof WorkflowScriptError ? error.partial : { trace: [], emits: [], console: [], children: [] };
|
|
5001
5031
|
const stopped = controller.signal.aborted;
|
|
@@ -5326,7 +5356,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
5326
5356
|
}
|
|
5327
5357
|
if (action === "children.list") {
|
|
5328
5358
|
deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
|
|
5329
|
-
const children = listRetainedChildren(DIRS.async, deps.state.currentSessionId);
|
|
5359
|
+
const children = listRetainedChildren(DIRS.async, deps.state.sessionLineage?.length ? deps.state.sessionLineage : [deps.state.currentSessionId]);
|
|
5330
5360
|
return {
|
|
5331
5361
|
content: [{ type: "text", text: formatRetainedChildren(children) }],
|
|
5332
5362
|
details: { mode: "management", results: [] },
|