pi-crew 0.11.21 → 0.11.22
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 +30 -0
- package/dist/index.mjs +537 -526
- package/package.json +2 -1
- package/src/errors.ts +14 -1
- package/src/extension/async-notifier.ts +42 -17
- package/src/extension/cross-extension-rpc.ts +42 -5
- package/src/extension/notification-router.ts +15 -2
- package/src/extension/registration/lifecycle-handlers.ts +36 -13
- package/src/extension/registration/runtime-cleanup.ts +17 -0
- package/src/extension/registration/wire-cross-extension.ts +26 -0
- package/src/extension/rpc-hmac.ts +53 -2
- package/src/extension/team-tool/cancel.ts +34 -21
- package/src/extension/team-tool/dispatch/index.ts +6 -5
- package/src/extension/team-tool/dispatch/status.ts +42 -2
- package/src/extension/team-tool.ts +24 -2
- package/src/prompt/inbox-poll.ts +77 -25
- package/src/prompt/message-tool.ts +62 -4
- package/src/prompt/prompt-runtime.ts +82 -32
- package/src/runtime/broker/crew-broker-client.ts +74 -16
- package/src/runtime/broker/crew-broker.ts +111 -77
- package/src/runtime/broker/protocol/msg-send-targets.ts +74 -0
- package/src/runtime/broker/wait-status-cache.ts +14 -5
- package/src/runtime/child-pi/child-pi-spawn.ts +47 -9
- package/src/runtime/child-pi/child-pi.ts +20 -2
- package/src/runtime/child-pi/mock-fixtures.ts +23 -0
- package/src/runtime/compaction/compact-stages/truncation-stage.ts +45 -3
- package/src/runtime/compaction/important-line-classifier.ts +85 -11
- package/src/runtime/compaction/tool-output-pruner.ts +35 -11
- package/src/runtime/crew-agent-records.ts +7 -1
- package/src/runtime/delivery-coordinator.ts +30 -0
- package/src/runtime/dwf-state-store.ts +41 -7
- package/src/runtime/finalize-run.ts +13 -2
- package/src/runtime/goal-workflow/dynamic-workflow-context.ts +192 -64
- package/src/runtime/goal-workflow/goal-loop-runner.ts +130 -3
- package/src/runtime/heartbeat/heartbeat-watcher.ts +7 -0
- package/src/runtime/live-session/embedded-tier.ts +3 -1
- package/src/runtime/live-session/live-session-runtime.ts +54 -1
- package/src/runtime/model/model-fallback.ts +23 -1
- package/src/runtime/output/pi-json-output.ts +26 -1
- package/src/runtime/policy-engine.ts +18 -0
- package/src/runtime/process/zombie-scanner.ts +78 -14
- package/src/runtime/process-status.ts +6 -1
- package/src/runtime/recovery/crash-recovery.ts +170 -28
- package/src/runtime/rpc/rpc-worker.ts +44 -1
- package/src/runtime/scheduling/run-coalesced-task-group.ts +64 -3
- package/src/runtime/scheduling/scheduler.ts +74 -11
- package/src/runtime/scratchpad/engine.ts +26 -3
- package/src/runtime/settings-store.ts +22 -2
- package/src/runtime/surface/tmux-provider.ts +24 -3
- package/src/runtime/task-health.ts +16 -1
- package/src/runtime/task-output-context.ts +143 -71
- package/src/runtime/task-packet.ts +70 -17
- package/src/runtime/task-runner/child-executor.ts +27 -0
- package/src/runtime/task-runner/output-splitter.ts +79 -16
- package/src/runtime/task-runner/post-execution.ts +75 -15
- package/src/runtime/task-runner/state-helpers.ts +92 -2
- package/src/runtime/usage-tracker.ts +11 -0
- package/src/runtime/verification/green-contract.ts +17 -0
- package/src/runtime/verification/verification-gates.ts +24 -6
- package/src/runtime/workspace-lock.ts +53 -0
- package/src/schema/team-tool-schema.ts +10 -4
- package/src/state/coordination/mailbox.ts +133 -38
- package/src/state/decision-ledger.ts +56 -8
- package/src/state/event-log/event-log-rotation.ts +83 -5
- package/src/state/event-log/event-log.ts +181 -12
- package/src/state/stores/active-run-registry.ts +10 -3
- package/src/state/types.ts +9 -1
- package/src/ui/agents-jobs-browser.ts +22 -8
- package/src/ui/dashboard-panes/agents-pane.ts +5 -2
- package/src/ui/events-state-source.ts +67 -28
- package/src/ui/inline-panel/agent-pane.ts +13 -6
- package/src/ui/inline-panel/agent-transcript.ts +33 -21
- package/src/ui/inline-panel/agent-view-overlay.ts +18 -3
- package/src/ui/inline-panel/index.ts +21 -1
- package/src/ui/live-run-sidebar.ts +14 -3
- package/src/ui/overlays/mailbox-compose-overlay.ts +10 -4
- package/src/ui/powerbar-publisher.ts +6 -3
- package/src/ui/rail.ts +7 -1
- package/src/ui/render-scheduler.ts +53 -1
- package/src/ui/run-action-dispatcher.ts +68 -20
- package/src/ui/status-colors.ts +9 -0
- package/src/utils/env-filter.ts +8 -0
|
@@ -20,7 +20,55 @@
|
|
|
20
20
|
* "compacted ... chars, head+tail preserved") and `readIfSmall` (marker
|
|
21
21
|
* "truncated ... bytes, head+tail preserved") backward-compatible when no
|
|
22
22
|
* important lines are present.
|
|
23
|
+
*
|
|
24
|
+
* K6/K7 (DR-03 round-6):
|
|
25
|
+
* - `reserveChars` lets the caller (TruncationStage) subtract the composed
|
|
26
|
+
* marker + separator + joined-important-line overhead from the head+tail
|
|
27
|
+
* budget, so the FINAL composed result fits `maxChars` + small constant
|
|
28
|
+
* instead of stacking unbudgeted overhead on top (~1.15×maxChars).
|
|
29
|
+
* - Head/tail/middle boundaries are surrogate-pair-safe: a code-unit index
|
|
30
|
+
* that would split an astral pair (emoji, CJK extensions) is backed off
|
|
31
|
+
* so every emitted part contains only whole code points.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/** Default fraction of `maxChars` available for important-line content. */
|
|
35
|
+
export const DEFAULT_SLACK_FACTOR = 0.15;
|
|
36
|
+
/** Default cap on candidate lines before slack-budget selection. */
|
|
37
|
+
export const DEFAULT_MAX_IMPORTANT_LINES = 30;
|
|
38
|
+
|
|
39
|
+
function isHighSurrogateUnit(codeUnit: number): boolean {
|
|
40
|
+
return codeUnit >= 0xd800 && codeUnit <= 0xdbff;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isLowSurrogateUnit(codeUnit: number): boolean {
|
|
44
|
+
return codeUnit >= 0xdc00 && codeUnit <= 0xdfff;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* K7: back a head-end boundary off while it would split a surrogate pair.
|
|
49
|
+
* `value.slice(0, boundary)` must never end on a lone high surrogate — if the
|
|
50
|
+
* code unit at `boundary - 1` is a high surrogate, its low partner lives at
|
|
51
|
+
* `boundary` and slicing there would orphan both halves; the boundary walks
|
|
52
|
+
* back so the whole pair falls into the (dropped) middle instead.
|
|
23
53
|
*/
|
|
54
|
+
export function safeHeadBoundary(value: string, end: number): number {
|
|
55
|
+
let boundary = Math.max(0, Math.min(end, value.length));
|
|
56
|
+
while (boundary > 0 && isHighSurrogateUnit(value.charCodeAt(boundary - 1))) boundary -= 1;
|
|
57
|
+
return boundary;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* K7: advance a tail-start boundary while it would split a surrogate pair.
|
|
62
|
+
* `value.slice(boundary)` must never start on a lone low surrogate — if the
|
|
63
|
+
* code unit at `boundary` is a low surrogate, its high partner lives at
|
|
64
|
+
* `boundary - 1` and slicing there would orphan both halves; the boundary
|
|
65
|
+
* walks forward so the whole pair stays in the (dropped) middle instead.
|
|
66
|
+
*/
|
|
67
|
+
export function safeTailBoundary(value: string, start: number): number {
|
|
68
|
+
let boundary = Math.max(0, Math.min(start, value.length));
|
|
69
|
+
while (boundary < value.length && isLowSurrogateUnit(value.charCodeAt(boundary))) boundary += 1;
|
|
70
|
+
return boundary;
|
|
71
|
+
}
|
|
24
72
|
|
|
25
73
|
/** Diagnostic patterns. Anchored where safe to avoid matching noise. */
|
|
26
74
|
export const IMPORTANT_LINE_PATTERNS: readonly RegExp[] = [
|
|
@@ -75,7 +123,9 @@ export interface TruncationSplit {
|
|
|
75
123
|
* fit the slack budget.
|
|
76
124
|
*/
|
|
77
125
|
importantLines: string[];
|
|
78
|
-
/** `value.length -
|
|
126
|
+
/** `value.length - headTailBudget` — chars dropped if no important lines
|
|
127
|
+
* preserved. `headTailBudget` is `maxChars - reserveChars`; with no
|
|
128
|
+
* reserve (the default) this equals `value.length - maxChars`. */
|
|
79
129
|
baseDropped: number;
|
|
80
130
|
}
|
|
81
131
|
|
|
@@ -84,8 +134,19 @@ export interface SplitOptions {
|
|
|
84
134
|
preserveImportant?: boolean;
|
|
85
135
|
/** Hard cap on candidate lines before slack-budget selection. Default 30. */
|
|
86
136
|
maxImportantLines?: number;
|
|
87
|
-
/**
|
|
137
|
+
/**
|
|
138
|
+
* Fraction of `maxChars` available for important-line content. Default 0.15
|
|
139
|
+
* ({@link DEFAULT_SLACK_FACTOR}).
|
|
140
|
+
*/
|
|
88
141
|
slackFactor?: number;
|
|
142
|
+
/**
|
|
143
|
+
* K6: code units the CALLER will compose AROUND head+tail (marker text,
|
|
144
|
+
* separators, joined important lines). The head+tail budget shrinks by
|
|
145
|
+
* this reserve so the caller's final composed result fits `maxChars` +
|
|
146
|
+
* small constant instead of stacking unbudgeted overhead on top. Default
|
|
147
|
+
* 0 — legacy semantics where head+tail alone consume the full maxChars.
|
|
148
|
+
*/
|
|
149
|
+
reserveChars?: number;
|
|
89
150
|
}
|
|
90
151
|
|
|
91
152
|
/**
|
|
@@ -101,24 +162,37 @@ export function splitWithImportantLines(value: string, maxChars: number, opts: S
|
|
|
101
162
|
if (value.length <= maxChars) {
|
|
102
163
|
return { head: value, tail: "", importantLines: [], baseDropped: 0 };
|
|
103
164
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
165
|
+
// K6: reserve the caller's composed overhead (marker + separators +
|
|
166
|
+
// joined important lines) out of the head+tail budget. Clamped so at
|
|
167
|
+
// least one code unit of budget survives even when the reserve exceeds
|
|
168
|
+
// maxChars (pathologically small budgets — the marker alone dominates).
|
|
169
|
+
const reserve = Math.max(0, Math.min(Math.floor(opts.reserveChars ?? 0), maxChars - 1));
|
|
170
|
+
const headTailBudget = Math.max(1, maxChars - reserve);
|
|
171
|
+
const headLen = Math.floor(headTailBudget * 0.75);
|
|
172
|
+
const tailLen = headTailBudget - headLen;
|
|
173
|
+
// K7: surrogate-safe boundaries. Raw code-unit slices previously split
|
|
174
|
+
// astral pairs at the 75% head boundary and the tail start, emitting
|
|
175
|
+
// lone surrogates (mojibake) into the compacted output. Back off while
|
|
176
|
+
// the boundary would split a pair — the whole pair then falls into the
|
|
177
|
+
// middle (dropped) region instead of being broken across head/tail.
|
|
178
|
+
const headEnd = safeHeadBoundary(value, headLen);
|
|
179
|
+
const tailStart = safeTailBoundary(value, value.length - tailLen);
|
|
180
|
+
const head = value.slice(0, headEnd);
|
|
181
|
+
const tail = value.slice(tailStart);
|
|
108
182
|
|
|
109
183
|
if (opts.preserveImportant === false) {
|
|
110
184
|
return {
|
|
111
185
|
head,
|
|
112
186
|
tail,
|
|
113
187
|
importantLines: [],
|
|
114
|
-
baseDropped: value.length -
|
|
188
|
+
baseDropped: value.length - headTailBudget,
|
|
115
189
|
};
|
|
116
190
|
}
|
|
117
191
|
|
|
118
|
-
const slackFactor = opts.slackFactor ??
|
|
192
|
+
const slackFactor = opts.slackFactor ?? DEFAULT_SLACK_FACTOR;
|
|
119
193
|
const slackChars = Math.max(0, Math.floor(maxChars * slackFactor));
|
|
120
|
-
const maxCandidates = opts.maxImportantLines ??
|
|
121
|
-
const middle = value.slice(
|
|
194
|
+
const maxCandidates = opts.maxImportantLines ?? DEFAULT_MAX_IMPORTANT_LINES;
|
|
195
|
+
const middle = value.slice(headEnd, tailStart);
|
|
122
196
|
const candidates = extractImportantLines(middle, maxCandidates);
|
|
123
197
|
|
|
124
198
|
// Greedily pick whole lines that fit in the slack budget.
|
|
@@ -135,6 +209,6 @@ export function splitWithImportantLines(value: string, maxChars: number, opts: S
|
|
|
135
209
|
head,
|
|
136
210
|
tail,
|
|
137
211
|
importantLines: chosen,
|
|
138
|
-
baseDropped: value.length -
|
|
212
|
+
baseDropped: value.length - headTailBudget,
|
|
139
213
|
};
|
|
140
214
|
}
|
|
@@ -39,6 +39,14 @@ export interface ToolResultEntry {
|
|
|
39
39
|
content: string;
|
|
40
40
|
/** Whether the tool result represents an error. */
|
|
41
41
|
isError?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* K8: the tool's REAL exit code when the original result carries one
|
|
44
|
+
* (e.g. a bash exit status). Passed through to the digest verbatim
|
|
45
|
+
* instead of the historical `isError ? 1 : 0` guess — exit 127 digests
|
|
46
|
+
* as `exit=127`. Optional: callers whose source shape has no exit code
|
|
47
|
+
* omit it and keep the legacy guess.
|
|
48
|
+
*/
|
|
49
|
+
exitCode?: number;
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
/** A file mutation event (edit/write) that can invalidate earlier reads. */
|
|
@@ -102,11 +110,23 @@ function estimateTokens(text: string): number {
|
|
|
102
110
|
|
|
103
111
|
const DIGEST_NOTICE_TOKEN_CAP_MULTIPLIER = 1.25;
|
|
104
112
|
|
|
105
|
-
function firstErrorLine(text: string): string | undefined {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
113
|
+
function firstErrorLine(text: string, isError = false): string | undefined {
|
|
114
|
+
const lines = text.split(/\r?\n/);
|
|
115
|
+
const matched = lines.find((line) => /error|failed|exception|panic/i.test(line))?.trim();
|
|
116
|
+
if (matched !== undefined) return matched;
|
|
117
|
+
// K8: multi-line stderr/stack whose HEAD carries the diagnostic but no
|
|
118
|
+
// line matches the keyword patterns (localized messages, shell errors,
|
|
119
|
+
// stack frames without a keyword). Fall back to the first 3 non-empty
|
|
120
|
+
// lines of the content head — the head names the failing frame; the old
|
|
121
|
+
// digest kept only the TAIL line, which for a stack trace is the least
|
|
122
|
+
// informative frame. Gated on isError so success output never grows a
|
|
123
|
+
// bogus error= part.
|
|
124
|
+
if (!isError) return undefined;
|
|
125
|
+
const head = lines
|
|
126
|
+
.map((line) => line.trim())
|
|
127
|
+
.filter((line) => line.length > 0)
|
|
128
|
+
.slice(0, 3);
|
|
129
|
+
return head.length > 0 ? head.join(" | ") : undefined;
|
|
110
130
|
}
|
|
111
131
|
|
|
112
132
|
function truncateField(value: string, maxLength: number): string {
|
|
@@ -119,22 +139,26 @@ function truncateField(value: string, maxLength: number): string {
|
|
|
119
139
|
* Generate a compact digest of a tool result for the digest notice.
|
|
120
140
|
* Supports bash (exit code + tail line), grep/search (match/file counts),
|
|
121
141
|
* and falls back to undefined for tools without a known digest format.
|
|
142
|
+
*
|
|
143
|
+
* K8: `exitCode` passes the REAL exit status through when the original
|
|
144
|
+
* result carries one (exit 127 → `exit=127`, not the hardcoded `exit=1`);
|
|
145
|
+
* when absent the legacy `isError ? 1 : 0` guess is kept.
|
|
122
146
|
*/
|
|
123
|
-
export function resultDigest(toolName: string, content: string, isError?: boolean): string | undefined {
|
|
147
|
+
export function resultDigest(toolName: string, content: string, isError?: boolean, exitCode?: number): string | undefined {
|
|
124
148
|
const name = toolName.toLowerCase();
|
|
125
149
|
const text = content ?? "";
|
|
126
150
|
if (name === "bash") {
|
|
127
|
-
const
|
|
151
|
+
const exitCodeValue = typeof exitCode === "number" && Number.isFinite(exitCode) ? Math.trunc(exitCode) : isError ? 1 : 0;
|
|
128
152
|
const tail = text.trim().split(/\r?\n/).filter(Boolean).at(-1) ?? "";
|
|
129
|
-
const error = firstErrorLine(text);
|
|
130
|
-
return [`exit=${
|
|
153
|
+
const error = firstErrorLine(text, isError);
|
|
154
|
+
return [`exit=${exitCodeValue}`, tail ? `tail=${tail}` : undefined, error ? `error=${error}` : undefined]
|
|
131
155
|
.filter((part): part is string => part !== undefined)
|
|
132
156
|
.join("; ");
|
|
133
157
|
}
|
|
134
158
|
if (name === "search" || name === "grep") {
|
|
135
159
|
const match = text.match(/(\d+)\s+matches?/i) ?? text.match(/totalMatches["']?:\s*(\d+)/i);
|
|
136
160
|
const files = text.match(/(\d+)\s+files?/i) ?? text.match(/filesWithMatches["']?:\s*(\d+)/i);
|
|
137
|
-
const error = firstErrorLine(text);
|
|
161
|
+
const error = firstErrorLine(text, isError);
|
|
138
162
|
return (
|
|
139
163
|
[match ? `matches=${match[1]}` : undefined, files ? `files=${files[1]}` : undefined, error ? `error=${error}` : undefined]
|
|
140
164
|
.filter((part): part is string => part !== undefined)
|
|
@@ -146,7 +170,7 @@ export function resultDigest(toolName: string, content: string, isError?: boolea
|
|
|
146
170
|
|
|
147
171
|
function createPrunedNotice(tokens: number, entry: ToolResultEntry): string {
|
|
148
172
|
const generic = `[Output pruned — ${tokens} tokens]`;
|
|
149
|
-
const digest = resultDigest(entry.toolName, entry.content, entry.isError);
|
|
173
|
+
const digest = resultDigest(entry.toolName, entry.content, entry.isError, entry.exitCode);
|
|
150
174
|
if (!digest) return generic;
|
|
151
175
|
const genericTokens = countTokens(generic);
|
|
152
176
|
const maxTokens = Math.max(genericTokens, Math.floor(genericTokens * DIGEST_NOTICE_TOKEN_CAP_MULTIPLIER));
|
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import {
|
|
5
|
+
atomicWriteFile,
|
|
5
6
|
atomicWriteJson,
|
|
6
7
|
atomicWriteJsonCoalesced,
|
|
7
8
|
cancelPendingCoalescedWriteForPath,
|
|
@@ -686,7 +687,12 @@ function readSeqFromSidecar(filePath: string): number | undefined {
|
|
|
686
687
|
|
|
687
688
|
function writeSeqToSidecar(filePath: string, seq: number): void {
|
|
688
689
|
try {
|
|
689
|
-
|
|
690
|
+
// D6 (DR-02): tmp+rename so a concurrent reader (nextAgentEventSeq on
|
|
691
|
+
// another thread/process) never observes a torn or empty .seq.
|
|
692
|
+
// best-effort durability per F4: the sidecar is only an O(1) lookup
|
|
693
|
+
// optimization — losing it on a crash costs the O(n) scan fallback,
|
|
694
|
+
// never correctness. Path and content format unchanged (String(seq)).
|
|
695
|
+
atomicWriteFile(`${filePath}.${AGENT_EVENT_SEQ_SIDECAR}`, String(seq), { durability: "best-effort" });
|
|
690
696
|
} catch (error) {
|
|
691
697
|
logInternalError("crew-agent-records.seq-sidecar", error, `filePath=${filePath}`);
|
|
692
698
|
}
|
|
@@ -26,6 +26,12 @@ export interface DeliveryCoordinatorDeps {
|
|
|
26
26
|
const PENDING_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
27
27
|
|
|
28
28
|
export class DeliveryCoordinator {
|
|
29
|
+
// DR-02 C3: the queue was TTL-bounded ONLY (24h) — an extension that stays
|
|
30
|
+
// sessionless while runs keep completing could grow `pending` unboundedly.
|
|
31
|
+
// Hard count cap with drop-oldest (oldest delivery is the least likely to
|
|
32
|
+
// still matter); the TTL eviction below stays unchanged. Static (not
|
|
33
|
+
// module-const) so tests can read/patch it like Semaphore.MAX_QUEUE.
|
|
34
|
+
static readonly MAX_PENDING = 1_000;
|
|
29
35
|
private active = false;
|
|
30
36
|
private generation = 0;
|
|
31
37
|
private pending: PendingDelivery[] = [];
|
|
@@ -36,6 +42,9 @@ export class DeliveryCoordinator {
|
|
|
36
42
|
/** The session id passed to the most recent activate(); used by flushQueuedResults
|
|
37
43
|
* to park deliveries owned by a different session (vector #12). */
|
|
38
44
|
private activeSessionId?: string;
|
|
45
|
+
/** DR-02 C3: set after the first warn of an overflow episode; cleared once
|
|
46
|
+
* the queue is back at/below the cap so the NEXT episode warns again. */
|
|
47
|
+
private pendingOverflowWarned = false;
|
|
39
48
|
|
|
40
49
|
constructor(deps: DeliveryCoordinatorDeps) {
|
|
41
50
|
this.deps = deps;
|
|
@@ -215,6 +224,27 @@ export class DeliveryCoordinator {
|
|
|
215
224
|
this.ttlTimer.unref();
|
|
216
225
|
}
|
|
217
226
|
this.pending.push({ ...delivery, generation: this.generation });
|
|
227
|
+
// DR-02 C3: count cap — drop-oldest while over cap, warn ONCE per
|
|
228
|
+
// overflow episode (flag resets once the queue is back under cap, so a
|
|
229
|
+
// later separate overflow is visible again).
|
|
230
|
+
if (this.pending.length <= DeliveryCoordinator.MAX_PENDING) {
|
|
231
|
+
this.pendingOverflowWarned = false;
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
let dropped = 0;
|
|
235
|
+
while (this.pending.length > DeliveryCoordinator.MAX_PENDING) {
|
|
236
|
+
if (this.pending.shift() === undefined) break;
|
|
237
|
+
dropped++;
|
|
238
|
+
}
|
|
239
|
+
if (dropped > 0 && !this.pendingOverflowWarned) {
|
|
240
|
+
this.pendingOverflowWarned = true;
|
|
241
|
+
logInternalError(
|
|
242
|
+
"delivery-coordinator.pending-overflow",
|
|
243
|
+
undefined,
|
|
244
|
+
`dropped=${dropped} max=${DeliveryCoordinator.MAX_PENDING} remaining=${this.pending.length} runId=${delivery.runId}`,
|
|
245
|
+
"warn",
|
|
246
|
+
);
|
|
247
|
+
}
|
|
218
248
|
}
|
|
219
249
|
|
|
220
250
|
private evictExpired(): void {
|
|
@@ -19,8 +19,21 @@ import { existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
|
19
19
|
import { dirname } from "node:path";
|
|
20
20
|
import { atomicWriteJson } from "../state/atomic-write.ts";
|
|
21
21
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
22
|
+
import type { AgentResult } from "./goal-workflow/dynamic-workflow-context.ts";
|
|
23
|
+
|
|
24
|
+
/** R6-12 (DR-03 round-6, 2026-10-16): current DWF checkpoint format version.
|
|
25
|
+
* Bumped whenever the persisted shape changes incompatibly — the loader
|
|
26
|
+
* rejects unknown FUTURE versions (see DwfStore.load) so a newer checkpoint
|
|
27
|
+
* is never silently mis-hydrated by an older binary. */
|
|
28
|
+
export const DWF_CHECKPOINT_VERSION = 1;
|
|
22
29
|
|
|
23
30
|
export interface DwfCheckpointState {
|
|
31
|
+
/** R6-12: format version. Always DWF_CHECKPOINT_VERSION on write (save()
|
|
32
|
+
* stamps it like updatedAt); load() tolerates a missing version (legacy
|
|
33
|
+
* pre-R6-12 checkpoints written by the version-less code — otherwise an
|
|
34
|
+
* in-flight run would lose its resume state across an upgrade) but rejects
|
|
35
|
+
* any other value. */
|
|
36
|
+
version: typeof DWF_CHECKPOINT_VERSION;
|
|
24
37
|
runId: string;
|
|
25
38
|
vars: Record<string, unknown>;
|
|
26
39
|
phases: string[];
|
|
@@ -28,11 +41,14 @@ export interface DwfCheckpointState {
|
|
|
28
41
|
logs: string[]; // capped copy (≤1000); the events log (dwf.log) is the durable source of truth
|
|
29
42
|
spent: number; // budget accumulator (round-14 P1-2)
|
|
30
43
|
agentCount: number;
|
|
31
|
-
/** PERS-1: per-agent-call idempotency cache. Maps a deterministic call ID to
|
|
32
|
-
* cached
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
|
|
44
|
+
/** PERS-1: per-agent-call idempotency cache. Maps a deterministic call ID to
|
|
45
|
+
* the cached FULL AgentResult (R6-3: text + usage + structured + artifactPath
|
|
46
|
+
* + actual durationMs — previously only {text,usage}, so replayed steps lost
|
|
47
|
+
* structured output and artifact linkage) so a DWF resume skips
|
|
48
|
+
* already-completed agent calls (avoids duplicating artifacts/mailbox/tokens).
|
|
49
|
+
* Uses Record (not Map) for JSON serialization safety. Optional: legacy
|
|
50
|
+
* checkpoints (and fresh states) omit it. */
|
|
51
|
+
completedAgentCalls?: Record<string, AgentResult>;
|
|
36
52
|
updatedAt: string;
|
|
37
53
|
}
|
|
38
54
|
|
|
@@ -70,16 +86,34 @@ export class DwfStore {
|
|
|
70
86
|
// Corrupt-guard: a valid checkpoint must be an object with a string runId
|
|
71
87
|
// (mirrors GoalStore.load's typeof parsed.goalId !== "string" check).
|
|
72
88
|
if (!parsed || typeof parsed !== "object" || typeof parsed.runId !== "string") return undefined;
|
|
89
|
+
// R6-12: version guard, mirroring the recovery/checkpoint.ts runId-guard
|
|
90
|
+
// style (typed mismatch → treat as not-loadable). A checkpoint from an
|
|
91
|
+
// unknown FUTURE version is REJECTED (→ undefined → fresh run) rather than
|
|
92
|
+
// mis-hydrated — a newer format's fields mean nothing to this binary.
|
|
93
|
+
// A MISSING version is accepted as legacy (written by the pre-R6-12 code,
|
|
94
|
+
// shape-identical otherwise) so an in-flight run survives an upgrade.
|
|
95
|
+
// Logged (not silent) so the quarantine is observable in diagnostics.
|
|
96
|
+
if (parsed.version !== undefined && parsed.version !== DWF_CHECKPOINT_VERSION) {
|
|
97
|
+
logInternalError(
|
|
98
|
+
"dwf-state-store.load",
|
|
99
|
+
new Error(
|
|
100
|
+
`unknown dwf-checkpoint version ${String(parsed.version)} (expected ${DWF_CHECKPOINT_VERSION}) — rejecting checkpoint, run restarts fresh`,
|
|
101
|
+
),
|
|
102
|
+
`runId=${parsed.runId}`,
|
|
103
|
+
);
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
73
106
|
return parsed as DwfCheckpointState;
|
|
74
107
|
} catch {
|
|
75
108
|
return undefined;
|
|
76
109
|
}
|
|
77
110
|
}
|
|
78
111
|
|
|
79
|
-
/** Atomically persist a checkpoint state. Stamps `
|
|
112
|
+
/** Atomically persist a checkpoint state. Stamps `version` (R6-12) and
|
|
113
|
+
* `updatedAt` (callers need not set either). */
|
|
80
114
|
save(state: DwfCheckpointState): void {
|
|
81
115
|
const path = this.path;
|
|
82
|
-
const next = { ...state, updatedAt: new Date().toISOString() };
|
|
116
|
+
const next = { ...state, version: DWF_CHECKPOINT_VERSION, updatedAt: new Date().toISOString() };
|
|
83
117
|
try {
|
|
84
118
|
mkdirSync(dirname(path), { recursive: true });
|
|
85
119
|
atomicWriteJson(path, next);
|
|
@@ -439,9 +439,20 @@ export async function finalizeRun(ctx: SchedulerContext): Promise<{ manifest: Te
|
|
|
439
439
|
// (No updateRunStatus call: from===to is a no-op, but the intent here is
|
|
440
440
|
// explicitly "leave the earlier decision intact".)
|
|
441
441
|
} else {
|
|
442
|
+
// R4-4 (DR-02 wave 2): needs_attention is terminal-without-result and the
|
|
443
|
+
// scheduler counts it as done, so the run still COMPLETES — but the green
|
|
444
|
+
// "Team workflow completed." summary must not hide that a task's result
|
|
445
|
+
// was never submitted. Detection-first surfacing only: status stays
|
|
446
|
+
// "completed" (no flip, no block); the policy engine emits a matching
|
|
447
|
+
// non-blocking notify decision listing the task ids.
|
|
448
|
+
const needsAttentionCount = tasks.filter((task) => task.status === "needs_attention").length;
|
|
442
449
|
manifest = applyStatusInMemory(
|
|
443
450
|
"completed",
|
|
444
|
-
|
|
451
|
+
needsAttentionCount > 0
|
|
452
|
+
? `Team workflow completed with ${needsAttentionCount} task(s) needing attention (no result submitted).`
|
|
453
|
+
: input.executeWorkers
|
|
454
|
+
? "Team workflow completed."
|
|
455
|
+
: "Team workflow scaffold completed without launching child workers.",
|
|
445
456
|
);
|
|
446
457
|
}
|
|
447
458
|
manifest = writeProgress(manifest, tasks, "team-runner", input.executeWorkers, input.runtimeConfig);
|
|
@@ -553,7 +564,7 @@ export async function finalizeRun(ctx: SchedulerContext): Promise<{ manifest: Te
|
|
|
553
564
|
const healthStore = new HealthStore(crewRoot);
|
|
554
565
|
healthStore.saveSnapshot({
|
|
555
566
|
runId: finalManifest.manifest.runId,
|
|
556
|
-
tasks: finalTasks.map((t) => ({ id: t.id, status: t.status })),
|
|
567
|
+
tasks: finalTasks.map((t) => ({ id: t.id, status: t.status, heartbeat: t.heartbeat })),
|
|
557
568
|
createdAt: finalManifest.manifest.createdAt,
|
|
558
569
|
});
|
|
559
570
|
ctx.manifest = manifest;
|