pi-subagents 0.50.0 → 0.52.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 +103 -0
- package/agents/oracle.md +3 -1
- package/agents/reviewer.md +1 -0
- package/agents/scout.md +2 -2
- package/agents/worker.md +2 -1
- package/async-retention-discovery-worker.mjs +180 -0
- package/docs/agents.md +40 -2
- package/docs/configuration.md +30 -12
- package/docs/extension-api.md +42 -1
- package/docs/models.md +2 -0
- package/docs/observability.md +41 -5
- package/docs/tool-reference.md +38 -39
- package/docs/workflows.md +171 -3
- package/package.json +4 -2
- package/skills/pi-subagents/SKILL.md +6 -4
- package/skills/pi-subagents/references/constraints-and-recipes.md +12 -6
- package/skills/pi-subagents/references/execution-controls.md +29 -15
- package/skills/pi-subagents/references/management-authoring-rpc.md +3 -3
- package/skills/pi-subagents/references/prompting-and-roles.md +4 -2
- package/src/agents/agent-management.ts +124 -351
- package/src/agents/agents.ts +157 -31
- package/src/agents/skills.ts +1 -1
- package/src/api/external-job-provider.ts +185 -0
- package/src/api/preflight.ts +36 -8
- package/src/api/shared-types.ts +2 -0
- package/src/extension/config.ts +3 -3
- package/src/extension/doctor.ts +3 -6
- package/src/extension/fanout-child.ts +2 -2
- package/src/extension/index.ts +166 -88
- package/src/extension/public-execution.ts +31 -2
- package/src/extension/schemas.ts +12 -35
- package/src/extension/tool-description.ts +35 -24
- package/src/inspectors/herdr/actions.ts +2 -2
- package/src/inspectors/herdr/inspector-runner.ts +2 -1
- package/src/inspectors/herdr/project-panes.ts +2 -2
- package/src/intercom/native-supervisor-channel.ts +32 -10
- package/src/missions/lifecycle.ts +6 -1
- package/src/missions/store.ts +4 -9
- package/src/missions/workflow-state.ts +2 -2
- package/src/profiles/profiles.ts +3 -1
- package/src/runs/background/active-run-index.ts +31 -8
- package/src/runs/background/async-execution.ts +68 -40
- package/src/runs/background/async-job-tracker.ts +20 -4
- package/src/runs/background/async-resume.ts +47 -15
- package/src/runs/background/async-retention.ts +886 -0
- package/src/runs/background/async-status.ts +39 -53
- package/src/runs/background/chain-append.ts +3 -33
- package/src/runs/background/completion-dedupe.ts +5 -1
- package/src/runs/background/completion-replay.ts +22 -12
- package/src/runs/background/control-channel.ts +14 -68
- package/src/runs/background/fleet-view.ts +68 -19
- package/src/runs/background/index-segment.ts +59 -0
- package/src/runs/background/inspect-rpc.ts +443 -0
- package/src/runs/background/notify.ts +31 -5
- package/src/runs/background/result-files.ts +158 -90
- package/src/runs/background/result-watcher.ts +116 -20
- package/src/runs/background/resume-guidance.ts +8 -5
- package/src/runs/background/retained-children.ts +13 -3
- package/src/runs/background/run-id-query.ts +7 -0
- package/src/runs/background/run-id-resolver.ts +11 -9
- package/src/runs/background/run-status.ts +27 -18
- package/src/runs/background/scheduled-runs.ts +24 -4
- package/src/runs/background/stale-run-reconciler.ts +6 -3
- package/src/runs/background/steering.ts +11 -1
- package/src/runs/background/subagent-runner.ts +253 -140
- package/src/runs/background/subagent-wait.ts +8 -8
- package/src/runs/background/terminal-run-index.ts +129 -0
- package/src/runs/background/wait-completions.ts +21 -4
- package/src/runs/background/wait-subscriptions.ts +81 -2
- package/src/runs/foreground/async-steering-action.ts +21 -14
- package/src/runs/foreground/execution.ts +3 -1
- package/src/runs/foreground/subagent-executor.ts +666 -1579
- package/src/runs/foreground/workflow-detach-reconcile.ts +194 -0
- package/src/runs/foreground/workflow-foreground-steering.ts +6 -5
- package/src/runs/shared/acceptance.ts +4 -4
- package/src/runs/shared/chain-outputs.ts +1 -3
- package/src/runs/shared/external-job-bridge.ts +444 -0
- package/src/runs/shared/external-job-runner.ts +286 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +14 -0
- package/src/runs/shared/model-fallback.ts +98 -38
- package/src/runs/shared/orca-progress-tabs.ts +84 -22
- package/src/runs/shared/parallel-handoff.ts +46 -4
- package/src/runs/shared/parallel-utils.ts +4 -15
- package/src/runs/shared/permissions.ts +5 -1
- package/src/runs/shared/pi-args.ts +8 -1
- package/src/runs/shared/session-lease.ts +0 -6
- package/src/runs/shared/subagent-control.ts +26 -4
- package/src/runs/shared/subagent-prompt-runtime.ts +12 -6
- package/src/runs/shared/workflow-graph.ts +1 -23
- package/src/runs/shared/worktree.ts +14 -2
- package/src/shared/atomic-json.ts +22 -2
- package/src/shared/capacity-resilient-json.ts +102 -0
- package/src/shared/completion-owner.ts +14 -0
- package/src/shared/file-system-retry.ts +49 -1
- package/src/shared/fork-context.ts +42 -0
- package/src/shared/prompt-resources.ts +0 -40
- package/src/shared/settings.ts +3 -27
- package/src/shared/types.ts +71 -26
- package/src/shared/utils.ts +8 -0
- package/src/shared/watch-strategy.ts +10 -0
- package/src/slash/slash-bridge.ts +2 -1
- package/src/slash/slash-commands.ts +29 -3
- package/src/tui/fleet.ts +63 -15
- package/src/watchdog/change-signature.ts +1 -1
- package/src/watchdog/lsp-diagnostics.ts +1 -0
- package/src/workflows/chat-progress.ts +2 -2
- package/src/workflows/scripted-workflow.ts +220 -67
- package/src/runs/foreground/chain-clarify.ts +0 -1354
- package/src/runs/foreground/chain-execution.ts +0 -1581
|
@@ -2,9 +2,9 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "../../shared/formatters.ts";
|
|
4
4
|
import { formatActivityLabel, formatParallelOutcome } from "../../shared/status-format.ts";
|
|
5
|
-
import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type CostSummary, type Details, type LaunchResolvedChildExtensionsV1, type RuntimeAcknowledgedChildExtensionsV1, type NestedRunSummary, type SteeringStatus, type SubagentRunMode, type TokenUsage, type TurnBudgetState, type UsageBudgetState
|
|
5
|
+
import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type CostSummary, type Details, type LaunchResolvedChildExtensionsV1, type RuntimeAcknowledgedChildExtensionsV1, type NestedRunSummary, type SteeringStatus, type SubagentRunMode, type TokenUsage, type TurnBudgetState, type UsageBudgetState } from "../../shared/types.ts";
|
|
6
6
|
import type { ResolvedSubagentCapabilityCeiling, SubagentCapabilityAudit } from "../shared/capability-ceiling.ts";
|
|
7
|
-
import {
|
|
7
|
+
import { readStatus } from "../../shared/utils.ts";
|
|
8
8
|
import { attachRootChildrenToSteps, buildNestedRouteIndex, findNestedRouteForRootId, type NestedRoute, projectNestedEvents } from "../shared/nested-events.ts";
|
|
9
9
|
import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
|
|
10
10
|
import { formatRunFanoutBudget, getRunFanoutBudgetSnapshot, readRunFanoutBudgetDescriptor } from "../shared/run-fanout-budget.ts";
|
|
@@ -13,6 +13,8 @@ import { contextModeLabel, summarizeContextModes, type ContextMode, type Context
|
|
|
13
13
|
import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-reconciler.ts";
|
|
14
14
|
import { readProcessTerminal, sanitizeProcessTerminal } from "./process-terminal.ts";
|
|
15
15
|
import { ACTIVE_RUN_INDEX_DIR, DEFAULT_STALE_TERMINAL_ACTIVE_MARKER_MS, activeRunMarkerAgeMs, isActiveAsyncState, readActiveRunIndex, releaseActiveRunIndex, updateActiveRunIndex } from "./active-run-index.ts";
|
|
16
|
+
import { readRecentTerminalRunIndex, TERMINAL_RUN_INDEX_DIR } from "./terminal-run-index.ts";
|
|
17
|
+
import { canScanAsyncRunPrefix } from "./run-id-query.ts";
|
|
16
18
|
|
|
17
19
|
interface AsyncRunStepSummary {
|
|
18
20
|
index: number;
|
|
@@ -23,7 +25,6 @@ interface AsyncRunStepSummary {
|
|
|
23
25
|
phase?: string;
|
|
24
26
|
outputName?: string;
|
|
25
27
|
structured?: boolean;
|
|
26
|
-
checkpoint?: ChainCheckpointState;
|
|
27
28
|
status: AsyncJobStep["status"];
|
|
28
29
|
runner?: AsyncJobStep["runner"];
|
|
29
30
|
activityState?: ActivityState;
|
|
@@ -83,6 +84,7 @@ export interface AsyncRunSummary {
|
|
|
83
84
|
mode: SubagentRunMode;
|
|
84
85
|
context?: ContextSummary;
|
|
85
86
|
cwd?: string;
|
|
87
|
+
sessionRoot?: string;
|
|
86
88
|
startedAt: number;
|
|
87
89
|
lastUpdate?: number;
|
|
88
90
|
endedAt?: number;
|
|
@@ -98,7 +100,6 @@ export interface AsyncRunSummary {
|
|
|
98
100
|
pendingAppends?: number;
|
|
99
101
|
parallelGroups?: AsyncParallelGroupStatus[];
|
|
100
102
|
steps: AsyncRunStepSummary[];
|
|
101
|
-
checkpoint?: ChainCheckpointState;
|
|
102
103
|
sessionDir?: string;
|
|
103
104
|
outputFile?: string;
|
|
104
105
|
totalTokens?: TokenUsage;
|
|
@@ -122,14 +123,18 @@ interface AsyncRunListOptions {
|
|
|
122
123
|
states?: Array<AsyncRunSummary["state"]>;
|
|
123
124
|
sessionId?: string;
|
|
124
125
|
limit?: number;
|
|
125
|
-
/** Limits
|
|
126
|
+
/** Limits terminal candidates using the timestamp embedded in index marker names. */
|
|
126
127
|
entryLimit?: number;
|
|
127
128
|
resultsDir?: string;
|
|
128
129
|
kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
|
|
129
130
|
now?: () => number;
|
|
130
131
|
reconcile?: boolean;
|
|
131
132
|
runId?: string;
|
|
133
|
+
/** The caller already holds a canonical run id; never interpret a miss as a prefix. */
|
|
134
|
+
exactRunId?: boolean;
|
|
132
135
|
includeNested?: boolean;
|
|
136
|
+
/** Explicit repair/debug escape hatch. Normal runtime paths must not set this. */
|
|
137
|
+
repairScan?: boolean;
|
|
133
138
|
}
|
|
134
139
|
|
|
135
140
|
function getErrorMessage(error: unknown): string {
|
|
@@ -157,7 +162,7 @@ function isAsyncRunDir(root: string, entry: string): boolean {
|
|
|
157
162
|
|
|
158
163
|
type TargetedAsyncRunResolution =
|
|
159
164
|
| { kind: "exact"; id: string }
|
|
160
|
-
| { kind: "
|
|
165
|
+
| { kind: "prefix" }
|
|
161
166
|
| { kind: "reject" };
|
|
162
167
|
|
|
163
168
|
/**
|
|
@@ -165,13 +170,13 @@ type TargetedAsyncRunResolution =
|
|
|
165
170
|
* accepting a path whose canonical location escaped the async root.
|
|
166
171
|
*/
|
|
167
172
|
export function resolveTargetedAsyncRun(asyncDirRoot: string, id: string, sessionId?: string): TargetedAsyncRunResolution {
|
|
168
|
-
if (!id || id === "." || id === ".." || id === ACTIVE_RUN_INDEX_DIR || path.basename(id) !== id) return { kind: "reject" };
|
|
173
|
+
if (!id || id === "." || id === ".." || id === ACTIVE_RUN_INDEX_DIR || id === TERMINAL_RUN_INDEX_DIR || path.basename(id) !== id) return { kind: "reject" };
|
|
169
174
|
const asyncDir = path.join(asyncDirRoot, id);
|
|
170
175
|
let entryStat: fs.Stats;
|
|
171
176
|
try {
|
|
172
177
|
entryStat = fs.lstatSync(asyncDir);
|
|
173
178
|
} catch (error) {
|
|
174
|
-
if (isNotFoundError(error)) return { kind: "
|
|
179
|
+
if (isNotFoundError(error)) return canScanAsyncRunPrefix(id) ? { kind: "prefix" } : { kind: "reject" };
|
|
175
180
|
throw new Error(`Failed to inspect async run path '${asyncDir}': ${getErrorMessage(error)}`, {
|
|
176
181
|
cause: error instanceof Error ? error : undefined,
|
|
177
182
|
});
|
|
@@ -189,7 +194,7 @@ export function resolveTargetedAsyncRun(asyncDirRoot: string, id: string, sessio
|
|
|
189
194
|
}
|
|
190
195
|
if (sessionId !== undefined) {
|
|
191
196
|
const status = readStatus(asyncDir);
|
|
192
|
-
if (status?.sessionId !== sessionId) return { kind: "
|
|
197
|
+
if (status?.sessionId !== sessionId) return canScanAsyncRunPrefix(id) ? { kind: "prefix" } : { kind: "reject" };
|
|
193
198
|
}
|
|
194
199
|
return { kind: "exact", id };
|
|
195
200
|
}
|
|
@@ -253,7 +258,6 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
253
258
|
...(step.phase ? { phase: step.phase } : {}),
|
|
254
259
|
...(step.outputName ? { outputName: step.outputName } : {}),
|
|
255
260
|
...(step.structured ? { structured: step.structured } : {}),
|
|
256
|
-
...(step.checkpoint ? { checkpoint: step.checkpoint } : {}),
|
|
257
261
|
status: step.status,
|
|
258
262
|
...(step.runner ? { runner: step.runner } : {}),
|
|
259
263
|
...(stepActivityState ? { activityState: stepActivityState } : {}),
|
|
@@ -315,6 +319,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
315
319
|
mode: status.mode,
|
|
316
320
|
...(summarizeContextModes(summarizedSteps.map((step) => step.context)) ? { context: summarizeContextModes(summarizedSteps.map((step) => step.context)) } : {}),
|
|
317
321
|
cwd: status.cwd,
|
|
322
|
+
...(status.sessionRoot ? { sessionRoot: status.sessionRoot } : {}),
|
|
318
323
|
startedAt: status.startedAt,
|
|
319
324
|
lastUpdate: status.lastUpdate,
|
|
320
325
|
endedAt: status.endedAt,
|
|
@@ -330,7 +335,6 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
330
335
|
...(status.pendingAppends !== undefined ? { pendingAppends: status.pendingAppends } : {}),
|
|
331
336
|
...(parallelGroups.length ? { parallelGroups } : {}),
|
|
332
337
|
steps: summarizedSteps,
|
|
333
|
-
...(status.checkpoint ? { checkpoint: status.checkpoint } : {}),
|
|
334
338
|
...(nestedChildren.length ? { nestedChildren } : {}),
|
|
335
339
|
...(nestedWarnings.length ? { nestedWarnings } : {}),
|
|
336
340
|
...(processTerminal ? { processTerminal } : {}),
|
|
@@ -379,35 +383,39 @@ function sortRuns(runs: AsyncRunSummary[]): AsyncRunSummary[] {
|
|
|
379
383
|
|
|
380
384
|
export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions = {}): AsyncRunSummary[] {
|
|
381
385
|
let entries: string[];
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
const
|
|
385
|
-
&& options.entryLimit === undefined
|
|
386
|
-
&& options.states !== undefined
|
|
387
|
-
&& options.states.length > 0
|
|
388
|
-
&& options.states.every(isActiveAsyncState);
|
|
386
|
+
const activeEntries = new Set<string>();
|
|
387
|
+
const wantsActive = options.states === undefined || options.states.some(isActiveAsyncState);
|
|
388
|
+
const wantsTerminal = options.states === undefined || options.states.some((state) => !isActiveAsyncState(state));
|
|
389
389
|
const includeNested = options.includeNested !== false;
|
|
390
390
|
try {
|
|
391
391
|
if (options.runId !== undefined) {
|
|
392
392
|
const resolution = resolveTargetedAsyncRun(asyncDirRoot, options.runId, options.sessionId);
|
|
393
393
|
entries = resolution.kind === "exact"
|
|
394
394
|
? [resolution.id]
|
|
395
|
-
: resolution.kind === "
|
|
395
|
+
: resolution.kind === "prefix" && options.exactRunId !== true
|
|
396
396
|
? fs.readdirSync(asyncDirRoot).filter((entry) =>
|
|
397
397
|
(entry === options.runId || entry.startsWith(options.runId!))
|
|
398
398
|
&& resolveTargetedAsyncRun(asyncDirRoot, entry, options.sessionId).kind === "exact"
|
|
399
399
|
)
|
|
400
400
|
: [];
|
|
401
|
-
} else if (
|
|
402
|
-
entries = (
|
|
403
|
-
if (resolveTargetedAsyncRun(asyncDirRoot, entry).kind === "exact") return true;
|
|
404
|
-
updateActiveRunIndex(path.join(asyncDirRoot, entry), "failed");
|
|
405
|
-
return false;
|
|
406
|
-
});
|
|
407
|
-
usedActiveIndex = true;
|
|
401
|
+
} else if (options.repairScan === true) {
|
|
402
|
+
entries = fs.readdirSync(asyncDirRoot).filter((entry) => entry !== ACTIVE_RUN_INDEX_DIR && entry !== TERMINAL_RUN_INDEX_DIR && isAsyncRunDir(asyncDirRoot, entry));
|
|
408
403
|
} else {
|
|
409
|
-
|
|
410
|
-
|
|
404
|
+
const indexed = new Set<string>();
|
|
405
|
+
if (wantsActive) {
|
|
406
|
+
for (const entry of readActiveRunIndex(asyncDirRoot) ?? []) {
|
|
407
|
+
if (resolveTargetedAsyncRun(asyncDirRoot, entry).kind === "exact") {
|
|
408
|
+
indexed.add(entry);
|
|
409
|
+
activeEntries.add(entry);
|
|
410
|
+
} else {
|
|
411
|
+
updateActiveRunIndex(path.join(asyncDirRoot, entry), "failed");
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (wantsTerminal) {
|
|
416
|
+
for (const entry of readRecentTerminalRunIndex(asyncDirRoot, { sessionId: options.sessionId, ...(options.entryLimit !== undefined ? { limit: options.entryLimit } : {}) })) indexed.add(entry);
|
|
417
|
+
}
|
|
418
|
+
entries = [...indexed];
|
|
411
419
|
}
|
|
412
420
|
} catch (error) {
|
|
413
421
|
if (isNotFoundError(error)) return [];
|
|
@@ -416,28 +424,6 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
416
424
|
});
|
|
417
425
|
}
|
|
418
426
|
|
|
419
|
-
if (options.entryLimit !== undefined) {
|
|
420
|
-
scannedCompleteRoot = false;
|
|
421
|
-
const limit = Math.max(0, Math.floor(options.entryLimit));
|
|
422
|
-
entries = entries
|
|
423
|
-
.map((entry) => {
|
|
424
|
-
try {
|
|
425
|
-
return { entry, mtimeMs: fs.statSync(path.join(asyncDirRoot, entry, "status.json")).mtimeMs };
|
|
426
|
-
} catch (error) {
|
|
427
|
-
if (isNotFoundError(error)) return undefined;
|
|
428
|
-
throw new Error(`Failed to inspect async status file for '${entry}': ${getErrorMessage(error)}`, {
|
|
429
|
-
cause: error instanceof Error ? error : undefined,
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
})
|
|
433
|
-
.filter((candidate): candidate is { entry: string; mtimeMs: number } => candidate !== undefined)
|
|
434
|
-
.sort((left, right) => right.mtimeMs - left.mtimeMs)
|
|
435
|
-
.slice(0, limit)
|
|
436
|
-
.map((candidate) => candidate.entry);
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
if (scannedCompleteRoot) pruneStatusCacheForAsyncRoot(asyncDirRoot, entries);
|
|
440
|
-
|
|
441
427
|
const allowedStates = options.states ? new Set(options.states) : undefined;
|
|
442
428
|
const runs: AsyncRunSummary[] = [];
|
|
443
429
|
// Route resolution for every run shares a single index built from the
|
|
@@ -448,7 +434,7 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
448
434
|
let nestedRouteIndex: Map<string, NestedRoute> | undefined;
|
|
449
435
|
const resolveNestedRoute = (rootRunId: string): NestedRoute | undefined => {
|
|
450
436
|
if (!includeNested) return undefined;
|
|
451
|
-
if (
|
|
437
|
+
if (activeEntries.has(rootRunId)) return findNestedRouteForRootId(rootRunId);
|
|
452
438
|
if (!nestedRouteIndex) nestedRouteIndex = buildNestedRouteIndex();
|
|
453
439
|
return nestedRouteIndex.get(rootRunId);
|
|
454
440
|
};
|
|
@@ -459,10 +445,10 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
459
445
|
: reconcileAsyncRun(asyncDir, { resultsDir: options.resultsDir, kill: options.kill, now: options.now });
|
|
460
446
|
const status = (reconciliation?.status ?? readStatus(asyncDir)) as (AsyncStatus & { cwd?: string }) | null;
|
|
461
447
|
if (!status) {
|
|
462
|
-
if (
|
|
448
|
+
if (activeEntries.has(entry)) updateActiveRunIndex(asyncDir, "failed");
|
|
463
449
|
continue;
|
|
464
450
|
}
|
|
465
|
-
if (
|
|
451
|
+
if (activeEntries.has(entry) && !isActiveAsyncState(status.state)) {
|
|
466
452
|
const processTerminal = readProcessTerminal(asyncDir, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId });
|
|
467
453
|
if (processTerminal?.state === "observed" || (activeRunMarkerAgeMs(asyncDir, options.now?.()) ?? 0) > DEFAULT_STALE_TERMINAL_ACTIVE_MARKER_MS) releaseActiveRunIndex(asyncDir);
|
|
468
454
|
}
|
|
@@ -5,8 +5,8 @@ import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
|
5
5
|
import { appendJsonl } from "../../shared/artifacts.ts";
|
|
6
6
|
import type { AsyncParallelGroupStatus, AsyncStatus, WorkflowGraphNode, WorkflowGraphSnapshot } from "../../shared/types.ts";
|
|
7
7
|
import { PROMPT_REDACTED, readStatus } from "../../shared/utils.ts";
|
|
8
|
-
import type { DynamicRunnerGroup, ParallelStepGroup, RunnerStep, RunnerSubagentStep
|
|
9
|
-
import {
|
|
8
|
+
import type { DynamicRunnerGroup, ParallelStepGroup, RunnerStep, RunnerSubagentStep } from "../shared/parallel-utils.ts";
|
|
9
|
+
import { isDynamicRunnerGroup, isParallelGroup } from "../shared/parallel-utils.ts";
|
|
10
10
|
|
|
11
11
|
const APPEND_REQUESTS_DIR = "append-requests";
|
|
12
12
|
|
|
@@ -52,9 +52,7 @@ export function countPendingChainAppendRequests(asyncDir: string): number {
|
|
|
52
52
|
export function runnerStepOutputNames(steps: RunnerStep[]): string[] {
|
|
53
53
|
const names: string[] = [];
|
|
54
54
|
for (const step of steps) {
|
|
55
|
-
if (
|
|
56
|
-
continue;
|
|
57
|
-
} else if (isParallelGroup(step)) {
|
|
55
|
+
if (isParallelGroup(step)) {
|
|
58
56
|
names.push(...step.parallel.map((task) => task.outputName).filter((name): name is string => Boolean(name)));
|
|
59
57
|
} else if (isDynamicRunnerGroup(step)) {
|
|
60
58
|
if (step.collect.as) names.push(step.collect.as);
|
|
@@ -181,20 +179,7 @@ function statusStepForTask(task: RunnerSubagentStep): StatusStep {
|
|
|
181
179
|
};
|
|
182
180
|
}
|
|
183
181
|
|
|
184
|
-
function statusStepForCheckpoint(step: RunnerCheckpointStep, stepIndex: number): StatusStep {
|
|
185
|
-
return {
|
|
186
|
-
agent: `checkpoint:${step.checkpoint}`,
|
|
187
|
-
phase: step.phase,
|
|
188
|
-
label: step.label ?? step.checkpoint,
|
|
189
|
-
status: "pending",
|
|
190
|
-
checkpoint: { name: step.checkpoint, ...(step.message ? { message: step.message } : {}), status: "pending", stepIndex },
|
|
191
|
-
recentTools: [],
|
|
192
|
-
recentOutput: [],
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
|
|
196
182
|
function statusStepsForRunnerStep(step: RunnerStep, stepIndex: number): StatusStep[] {
|
|
197
|
-
if (isCheckpointRunnerStep(step)) return [statusStepForCheckpoint(step, stepIndex)];
|
|
198
183
|
if (isParallelGroup(step)) return step.parallel.map(statusStepForTask);
|
|
199
184
|
if (isDynamicRunnerGroup(step)) {
|
|
200
185
|
return [{
|
|
@@ -286,21 +271,6 @@ function graphNodeForDynamic(step: DynamicRunnerGroup, stepIndex: number): Workf
|
|
|
286
271
|
|
|
287
272
|
function appendWorkflowNode(graph: WorkflowGraphSnapshot | undefined, step: RunnerStep, stepIndex: number, flatIndex: number): void {
|
|
288
273
|
if (!graph) return;
|
|
289
|
-
if (isCheckpointRunnerStep(step)) {
|
|
290
|
-
const node: WorkflowGraphNode = {
|
|
291
|
-
id: `step-${stepIndex}`,
|
|
292
|
-
kind: "checkpoint",
|
|
293
|
-
phase: step.phase,
|
|
294
|
-
label: step.label?.trim() || step.checkpoint,
|
|
295
|
-
status: "pending",
|
|
296
|
-
flatIndex,
|
|
297
|
-
stepIndex,
|
|
298
|
-
checkpoint: { name: step.checkpoint, ...(step.message ? { message: step.message } : {}), status: "pending", stepIndex },
|
|
299
|
-
};
|
|
300
|
-
graph.nodes.push(node);
|
|
301
|
-
pushPhase(graph, step.phase, node.id);
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
274
|
if (isParallelGroup(step)) {
|
|
305
275
|
graph.nodes.push(graphNodeForParallel(step, stepIndex, flatIndex, graph));
|
|
306
276
|
return;
|
|
@@ -3,6 +3,7 @@ interface CompletionDataLike {
|
|
|
3
3
|
agent?: unknown;
|
|
4
4
|
timestamp?: unknown;
|
|
5
5
|
sessionId?: unknown;
|
|
6
|
+
state?: unknown;
|
|
6
7
|
taskIndex?: unknown;
|
|
7
8
|
totalTasks?: unknown;
|
|
8
9
|
success?: unknown;
|
|
@@ -22,7 +23,10 @@ function asFiniteNumber(value: unknown): number | undefined {
|
|
|
22
23
|
export function buildCompletionKey(data: CompletionDataLike, fallback: string): string {
|
|
23
24
|
const sessionId = asNonEmptyString(data.sessionId) ?? "no-session";
|
|
24
25
|
const id = asNonEmptyString(data.id);
|
|
25
|
-
|
|
26
|
+
const state = asNonEmptyString(data.state);
|
|
27
|
+
if (id) return state
|
|
28
|
+
? `session:${sessionId}:id:${id}:state:${state}`
|
|
29
|
+
: `session:${sessionId}:id:${id}`;
|
|
26
30
|
const agent = asNonEmptyString(data.agent) ?? "unknown";
|
|
27
31
|
const timestamp = asFiniteNumber(data.timestamp);
|
|
28
32
|
const taskIndex = asFiniteNumber(data.taskIndex);
|
|
@@ -14,6 +14,7 @@ const lastCleanupByResultsDir = new Map<string, number>();
|
|
|
14
14
|
|
|
15
15
|
export interface CompletionArchiveEntry {
|
|
16
16
|
agent?: string;
|
|
17
|
+
resultIndex?: number;
|
|
17
18
|
source: "output-artifact" | "session" | "result-tail";
|
|
18
19
|
path?: string;
|
|
19
20
|
text?: string;
|
|
@@ -71,39 +72,45 @@ function outputArtifactPath(child: Record<string, unknown>): string | undefined
|
|
|
71
72
|
/** Create a small archive that references saved child artifacts and retains only bounded fallback output text. */
|
|
72
73
|
export function writeCompletionArchive(resultsDir: string, runId: string, data: Record<string, unknown>, createdAt: number): string {
|
|
73
74
|
const entries: CompletionArchiveEntry[] = [];
|
|
74
|
-
const fallback: string[] = [];
|
|
75
75
|
const results = Array.isArray(data.results) ? data.results : [];
|
|
76
|
-
for (
|
|
76
|
+
for (let resultIndex = 0; resultIndex < results.length; resultIndex++) {
|
|
77
|
+
const value = results[resultIndex];
|
|
77
78
|
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
78
79
|
const child = value as Record<string, unknown>;
|
|
79
80
|
const agent = nonEmptyString(child.agent);
|
|
80
81
|
const artifactPath = outputArtifactPath(child);
|
|
81
82
|
if (artifactPath) {
|
|
82
|
-
entries.push({ ...(agent ? { agent } : {}), source: "output-artifact", path: artifactPath });
|
|
83
|
+
entries.push({ ...(agent ? { agent } : {}), resultIndex, source: "output-artifact", path: artifactPath });
|
|
83
84
|
continue;
|
|
84
85
|
}
|
|
85
86
|
const sessionPath = existingFile(child.sessionFile);
|
|
86
87
|
if (sessionPath) {
|
|
87
|
-
entries.push({ ...(agent ? { agent } : {}), source: "session", path: sessionPath });
|
|
88
|
+
entries.push({ ...(agent ? { agent } : {}), resultIndex, source: "session", path: sessionPath });
|
|
88
89
|
continue;
|
|
89
90
|
}
|
|
90
91
|
const output = nonEmptyString(child.output);
|
|
91
92
|
const error = nonEmptyString(child.error);
|
|
92
93
|
if (output || error) {
|
|
93
|
-
|
|
94
|
+
const bounded = utf8Tail([error ? `Error: ${error}` : undefined, output].filter(Boolean).join("\n"), ARCHIVE_TEXT_LIMIT_BYTES);
|
|
95
|
+
entries.push({
|
|
96
|
+
...(agent ? { agent } : {}),
|
|
97
|
+
resultIndex,
|
|
98
|
+
source: "result-tail",
|
|
99
|
+
text: bounded.text,
|
|
100
|
+
...(bounded.truncated ? { truncated: true } : {}),
|
|
101
|
+
});
|
|
94
102
|
}
|
|
95
103
|
}
|
|
96
104
|
if (results.length === 0) {
|
|
97
105
|
const sessionPath = existingFile(data.sessionFile);
|
|
98
106
|
if (sessionPath) entries.push({ source: "session", path: sessionPath });
|
|
99
107
|
}
|
|
100
|
-
if (entries.length === 0
|
|
108
|
+
if (entries.length === 0) {
|
|
101
109
|
const summary = nonEmptyString(data.summary);
|
|
102
|
-
if (summary)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
entries.push({ source: "result-tail", text: bounded.text, ...(bounded.truncated ? { truncated: true } : {}) });
|
|
110
|
+
if (summary) {
|
|
111
|
+
const bounded = utf8Tail(summary, ARCHIVE_TEXT_LIMIT_BYTES);
|
|
112
|
+
entries.push({ source: "result-tail", text: bounded.text, ...(bounded.truncated ? { truncated: true } : {}) });
|
|
113
|
+
}
|
|
107
114
|
}
|
|
108
115
|
const archive: CompletionArchive = { version: ARCHIVE_VERSION, runId, createdAt, entries };
|
|
109
116
|
const archivePath = completionArchivePath(resultsDir, runId);
|
|
@@ -165,6 +172,7 @@ function parseArchive(value: unknown): CompletionArchive | undefined {
|
|
|
165
172
|
if (entry.source !== "output-artifact" && entry.source !== "session" && entry.source !== "result-tail") return [];
|
|
166
173
|
return [{
|
|
167
174
|
...(typeof entry.agent === "string" ? { agent: entry.agent } : {}),
|
|
175
|
+
...(typeof entry.resultIndex === "number" && Number.isSafeInteger(entry.resultIndex) && entry.resultIndex >= 0 ? { resultIndex: entry.resultIndex } : {}),
|
|
168
176
|
source: entry.source,
|
|
169
177
|
...(typeof entry.path === "string" ? { path: entry.path } : {}),
|
|
170
178
|
...(typeof entry.text === "string" ? { text: entry.text } : {}),
|
|
@@ -228,7 +236,9 @@ export function readCompletionReplay(resultsDir: string, runId: string, options:
|
|
|
228
236
|
|
|
229
237
|
export function readCompletionArchive(archivePath: string): CompletionArchive | undefined {
|
|
230
238
|
try {
|
|
231
|
-
|
|
239
|
+
const archive = parseArchive(JSON.parse(fs.readFileSync(archivePath, "utf-8")));
|
|
240
|
+
if (!archive) throw new Error("Completion archive is malformed.");
|
|
241
|
+
return archive;
|
|
232
242
|
} catch (error) {
|
|
233
243
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
234
244
|
throw error;
|
|
@@ -18,6 +18,7 @@ import * as fs from "node:fs";
|
|
|
18
18
|
import * as path from "node:path";
|
|
19
19
|
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
20
20
|
import { POLL_INTERVAL_MS } from "../../shared/types.ts";
|
|
21
|
+
import { shouldUseNativeFsWatch } from "../../shared/watch-strategy.ts";
|
|
21
22
|
import { resolveWatchPath } from "../../shared/utils.ts";
|
|
22
23
|
|
|
23
24
|
export type ControlChannelFs = Pick<typeof fs, "mkdirSync" | "existsSync" | "rmSync" | "watch" | "readdirSync" | "readFileSync" | "realpathSync">;
|
|
@@ -56,13 +57,6 @@ export interface StopRequest {
|
|
|
56
57
|
reason?: string;
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
export interface CheckpointDecisionRequest {
|
|
60
|
-
type: "approve-checkpoint" | "reject-checkpoint";
|
|
61
|
-
ts?: number;
|
|
62
|
-
source?: string;
|
|
63
|
-
reason?: string;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
60
|
export type SteerDeliveryMode = "steer" | "follow_up" | "auto";
|
|
67
61
|
export type SteerDeliveryStatus = "delivered" | "queued";
|
|
68
62
|
|
|
@@ -127,14 +121,6 @@ export function stopRequestPath(asyncDir: string): string {
|
|
|
127
121
|
return path.join(controlInboxDir(asyncDir), "stop.json");
|
|
128
122
|
}
|
|
129
123
|
|
|
130
|
-
export function approveCheckpointRequestPath(asyncDir: string): string {
|
|
131
|
-
return path.join(controlInboxDir(asyncDir), "approve-checkpoint.json");
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export function rejectCheckpointRequestPath(asyncDir: string): string {
|
|
135
|
-
return path.join(controlInboxDir(asyncDir), "reject-checkpoint.json");
|
|
136
|
-
}
|
|
137
|
-
|
|
138
124
|
/** Directory of parent-to-runner steering requests. */
|
|
139
125
|
export function steerRequestsDir(asyncDir: string): string {
|
|
140
126
|
return path.join(controlInboxDir(asyncDir), STEER_REQUESTS_DIR);
|
|
@@ -144,8 +130,8 @@ export function steerInboxClosedPath(asyncDir: string): string {
|
|
|
144
130
|
return path.join(controlInboxDir(asyncDir), STEER_INBOX_CLOSED_FILE);
|
|
145
131
|
}
|
|
146
132
|
|
|
147
|
-
export function closeSteerInbox(asyncDir: string, state: string): void {
|
|
148
|
-
|
|
133
|
+
export function closeSteerInbox(asyncDir: string, state: string, write: (filePath: string, payload: object) => void = writeAtomicJson): void {
|
|
134
|
+
write(steerInboxClosedPath(asyncDir), { version: 1, closedAt: Date.now(), state });
|
|
149
135
|
}
|
|
150
136
|
|
|
151
137
|
/** Per-child inbox consumed by the child prompt runtime inside the Pi process. */
|
|
@@ -299,18 +285,6 @@ export function requestAsyncStop(
|
|
|
299
285
|
return requestPath;
|
|
300
286
|
}
|
|
301
287
|
|
|
302
|
-
export function requestAsyncCheckpointDecision(
|
|
303
|
-
asyncDir: string,
|
|
304
|
-
type: CheckpointDecisionRequest["type"],
|
|
305
|
-
payload: Omit<CheckpointDecisionRequest, "type"> = {},
|
|
306
|
-
deps: { now?: () => number } = {},
|
|
307
|
-
): string {
|
|
308
|
-
const requestPath = type === "approve-checkpoint" ? approveCheckpointRequestPath(asyncDir) : rejectCheckpointRequestPath(asyncDir);
|
|
309
|
-
const request: CheckpointDecisionRequest = { ...payload, ts: payload.ts ?? deps.now?.() ?? Date.now(), type };
|
|
310
|
-
writeAtomicJson(requestPath, request);
|
|
311
|
-
return requestPath;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
288
|
export function requestAsyncSteer(
|
|
315
289
|
asyncDir: string,
|
|
316
290
|
payload: { message: string; mode?: SteerDeliveryMode; targetIndex?: number; targetIndexes?: number[]; source?: string; id?: string; ts?: number },
|
|
@@ -559,21 +533,6 @@ export function consumeStopRequest(
|
|
|
559
533
|
return true;
|
|
560
534
|
}
|
|
561
535
|
|
|
562
|
-
export function consumeCheckpointDecisionRequest(
|
|
563
|
-
asyncDir: string,
|
|
564
|
-
fsImpl: Pick<typeof fs, "existsSync" | "rmSync"> = fs,
|
|
565
|
-
): "approved" | "rejected" | undefined {
|
|
566
|
-
if (fsImpl.existsSync(rejectCheckpointRequestPath(asyncDir))) {
|
|
567
|
-
try { fsImpl.rmSync(rejectCheckpointRequestPath(asyncDir), { force: true, recursive: true }); } catch {}
|
|
568
|
-
return "rejected";
|
|
569
|
-
}
|
|
570
|
-
if (fsImpl.existsSync(approveCheckpointRequestPath(asyncDir))) {
|
|
571
|
-
try { fsImpl.rmSync(approveCheckpointRequestPath(asyncDir), { force: true, recursive: true }); } catch {}
|
|
572
|
-
return "approved";
|
|
573
|
-
}
|
|
574
|
-
return undefined;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
536
|
/** Parent side: write the authoritative portable interrupt request. */
|
|
578
537
|
export function deliverInterruptRequest(input: {
|
|
579
538
|
asyncDir: string;
|
|
@@ -605,21 +564,6 @@ export function deliverStopRequest(input: {
|
|
|
605
564
|
requestAsyncStop(input.asyncDir, input.source ? { source: input.source } : {}, { now: input.now });
|
|
606
565
|
}
|
|
607
566
|
|
|
608
|
-
export function deliverCheckpointDecisionRequest(input: {
|
|
609
|
-
asyncDir: string;
|
|
610
|
-
decision: "approved" | "rejected";
|
|
611
|
-
now?: () => number;
|
|
612
|
-
source?: string;
|
|
613
|
-
reason?: string;
|
|
614
|
-
}): void {
|
|
615
|
-
requestAsyncCheckpointDecision(
|
|
616
|
-
input.asyncDir,
|
|
617
|
-
input.decision === "approved" ? "approve-checkpoint" : "reject-checkpoint",
|
|
618
|
-
{ ...(input.source ? { source: input.source } : {}), ...(input.reason ? { reason: input.reason } : {}) },
|
|
619
|
-
{ now: input.now },
|
|
620
|
-
);
|
|
621
|
-
}
|
|
622
|
-
|
|
623
567
|
/**
|
|
624
568
|
* Runner side: watch the control inbox and route interrupt requests into
|
|
625
569
|
* `onInterrupt`. Uses `fs.watch` when available and starts interval polling
|
|
@@ -633,11 +577,11 @@ export function watchAsyncControlInbox(
|
|
|
633
577
|
onTimeout?: () => void;
|
|
634
578
|
onStop?: () => void;
|
|
635
579
|
onSteer?: (request: SteerRequest) => void;
|
|
636
|
-
onCheckpointDecision?: (decision: "approved" | "rejected") => void;
|
|
637
580
|
onSteerCapability?: (capability: SteerCapability) => void;
|
|
638
581
|
onSteerAck?: (ack: SteerAck) => void;
|
|
639
582
|
pollIntervalMs?: number;
|
|
640
583
|
safetyPollIntervalMs?: number;
|
|
584
|
+
platform?: NodeJS.Platform;
|
|
641
585
|
fs?: ControlChannelFs;
|
|
642
586
|
timers?: ControlChannelTimers;
|
|
643
587
|
},
|
|
@@ -658,8 +602,6 @@ export function watchAsyncControlInbox(
|
|
|
658
602
|
if (consumeStopRequest(asyncDir, fsImpl)) opts.onStop?.();
|
|
659
603
|
if (consumeTimeoutRequest(asyncDir, fsImpl)) opts.onTimeout?.();
|
|
660
604
|
if (consumeInterruptRequest(asyncDir, fsImpl)) opts.onInterrupt();
|
|
661
|
-
const checkpointDecision = consumeCheckpointDecisionRequest(asyncDir, fsImpl);
|
|
662
|
-
if (checkpointDecision) opts.onCheckpointDecision?.(checkpointDecision);
|
|
663
605
|
for (const request of consumeSteerRequests(asyncDir, fsImpl)) opts.onSteer?.(request);
|
|
664
606
|
for (const capability of consumeSteerCapabilities(asyncDir, fsImpl)) opts.onSteerCapability?.(capability);
|
|
665
607
|
for (const ack of consumeSteerAcks(asyncDir, fsImpl)) opts.onSteerAck?.(ack);
|
|
@@ -707,12 +649,16 @@ export function watchAsyncControlInbox(
|
|
|
707
649
|
for (const entry of entries) watchDir(path.join(ackRoot, entry));
|
|
708
650
|
};
|
|
709
651
|
try {
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
652
|
+
if (shouldUseNativeFsWatch("runner-control-inbox", opts.platform)) {
|
|
653
|
+
watchDir(dir);
|
|
654
|
+
watchDir(steerRequestsDir(asyncDir), true);
|
|
655
|
+
watchDir(steerCapabilitiesDir(asyncDir), true);
|
|
656
|
+
watchDir(path.join(dir, STEER_ACKS_DIR), true);
|
|
657
|
+
watchExistingSteerAckDirs();
|
|
658
|
+
startSafetyPolling();
|
|
659
|
+
} else {
|
|
660
|
+
startPolling();
|
|
661
|
+
}
|
|
716
662
|
} catch {
|
|
717
663
|
startPolling();
|
|
718
664
|
}
|
|
@@ -156,34 +156,83 @@ function stringifyJsonPreview(value: unknown, maxLength = 240): string {
|
|
|
156
156
|
return raw.length > maxLength ? `${raw.slice(0, maxLength)}…` : raw;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
159
|
+
/** One structured content part of a session JSONL record. Shared by the prose transcript formatter and inspect RPC. */
|
|
160
|
+
export interface SessionTranscriptMessage {
|
|
161
|
+
role: string;
|
|
162
|
+
kind: "text" | "toolCall" | "toolResult";
|
|
163
|
+
text: string;
|
|
164
|
+
name?: string;
|
|
165
|
+
isError?: boolean;
|
|
166
|
+
/** Ordinal of the session record this part came from, so consumers can
|
|
167
|
+
* regroup multi-part messages (one session record can yield several parts). */
|
|
168
|
+
recordIndex?: number;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function sessionMessageParts(record: unknown): SessionTranscriptMessage[] {
|
|
172
|
+
if (!record || typeof record !== "object") return [];
|
|
173
|
+
const outer = record as { message?: unknown; role?: unknown; content?: unknown; type?: unknown };
|
|
174
|
+
const message = outer.message && typeof outer.message === "object" ? outer.message as { role?: unknown; content?: unknown } : outer;
|
|
175
|
+
const role = typeof message.role === "string" ? message.role : undefined;
|
|
176
|
+
if (!role) return [];
|
|
177
|
+
const content = message.content;
|
|
178
|
+
if (typeof content === "string") return content.trim() ? [{ role, kind: "text", text: content }] : [];
|
|
179
|
+
if (!Array.isArray(content)) return [];
|
|
180
|
+
const parts: SessionTranscriptMessage[] = [];
|
|
181
|
+
for (const part of content) {
|
|
182
|
+
if (!part || typeof part !== "object") continue;
|
|
183
|
+
const entry = part as { type?: unknown; text?: unknown; name?: unknown; toolName?: unknown; args?: unknown; result?: unknown; content?: unknown; isError?: unknown };
|
|
184
|
+
if (typeof entry.text === "string") {
|
|
185
|
+
if (entry.text.trim()) parts.push({ role, kind: "text", text: entry.text });
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
166
188
|
if (entry.type === "toolCall" || entry.type === "tool_call") {
|
|
167
189
|
const name = typeof entry.name === "string" ? entry.name : typeof entry.toolName === "string" ? entry.toolName : "tool";
|
|
168
|
-
|
|
190
|
+
parts.push({ role, kind: "toolCall", name, text: `[tool: ${name}${entry.args === undefined ? "" : ` ${stringifyJsonPreview(entry.args)}`}]` });
|
|
191
|
+
continue;
|
|
169
192
|
}
|
|
170
193
|
if (entry.type === "toolResult" || entry.type === "tool_result") {
|
|
171
|
-
|
|
194
|
+
const name = typeof entry.name === "string" ? entry.name : typeof entry.toolName === "string" ? entry.toolName : undefined;
|
|
195
|
+
parts.push({ role, kind: "toolResult", ...(name ? { name } : {}), ...(entry.isError === true ? { isError: true } : {}), text: `[tool result${entry.result === undefined ? "" : `: ${stringifyJsonPreview(entry.result)}`}]` });
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (entry.content !== undefined) {
|
|
199
|
+
const preview = stringifyJsonPreview(entry.content);
|
|
200
|
+
if (preview.trim()) parts.push({ role, kind: "text", text: preview });
|
|
172
201
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}).filter(Boolean).join("\n");
|
|
202
|
+
}
|
|
203
|
+
return parts;
|
|
176
204
|
}
|
|
177
205
|
|
|
178
206
|
function sessionMessageLine(record: unknown): string | undefined {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
const role = typeof message.role === "string" ? message.role : undefined;
|
|
183
|
-
if (!role) return undefined;
|
|
184
|
-
const text = contentText(message.content).trim();
|
|
207
|
+
const parts = sessionMessageParts(record);
|
|
208
|
+
if (parts.length === 0) return undefined;
|
|
209
|
+
const text = parts.map((part) => part.text).join("\n").trim();
|
|
185
210
|
if (!text) return undefined;
|
|
186
|
-
return `${role}: ${text}`;
|
|
211
|
+
return `${parts[0]!.role}: ${text}`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Structured session tail: parsed content parts, newest last, bounded by
|
|
215
|
+
* maxMessages. Same trusted-root containment as the prose transcript tail. */
|
|
216
|
+
export function readSessionMessagesTail(sessionFile: string, maxMessages: number, trustedRoots: string[]): { messages: SessionTranscriptMessage[]; warnings: string[]; truncated: boolean } {
|
|
217
|
+
const tail = readContainedTextTail(sessionFile, Math.max(maxMessages * 4, maxMessages), trustedRoots, "session");
|
|
218
|
+
const warnings: string[] = [];
|
|
219
|
+
if (tail.error) warnings.push(`Session read failed for ${sessionFile}: ${tail.error}`);
|
|
220
|
+
const parsedMessages: SessionTranscriptMessage[] = [];
|
|
221
|
+
let malformed = 0;
|
|
222
|
+
let recordIndex = 0;
|
|
223
|
+
for (const line of tail.lines) {
|
|
224
|
+
if (!line.trim()) continue;
|
|
225
|
+
try {
|
|
226
|
+
const parsed = JSON.parse(line) as unknown;
|
|
227
|
+
for (const part of sessionMessageParts(parsed)) parsedMessages.push({ ...part, recordIndex });
|
|
228
|
+
} catch {
|
|
229
|
+
malformed++;
|
|
230
|
+
}
|
|
231
|
+
recordIndex++;
|
|
232
|
+
}
|
|
233
|
+
if (malformed > 0) warnings.push(`Skipped ${malformed} malformed session tail line${malformed === 1 ? "" : "s"}.`);
|
|
234
|
+
const messages = parsedMessages.slice(-maxMessages);
|
|
235
|
+
return { messages, warnings, truncated: tail.truncated || parsedMessages.length > messages.length };
|
|
187
236
|
}
|
|
188
237
|
|
|
189
238
|
function readSessionTranscriptTail(sessionFile: string, maxLines: number, trustedRoots: string[]): { lines: string[]; warnings: string[] } {
|