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
|
@@ -39,6 +39,25 @@ function validateScheduledJob(job: unknown): boolean {
|
|
|
39
39
|
return typeof obj.id === "string" && obj.id.length > 0 && typeof obj.scheduleType === "string" && typeof obj.enabled === "boolean";
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* R6-2(c) (round-6 audit): a persisted `lastStatus:"running"` is an in-flight
|
|
44
|
+
* flag written by a process that may already be dead when the file is next
|
|
45
|
+
* read. It is meaningless at load time and lethal downstream: the scheduler's
|
|
46
|
+
* fire() treats "running" as backpressure (in-flight guard), so re-loading the
|
|
47
|
+
* flag verbatim made a job that was mid-run at crash/restart time skip EVERY
|
|
48
|
+
* later occurrence — scheduled jobs died after restart. Strip only the stale
|
|
49
|
+
* "running" flag at load; terminal statuses ("success"/"error") survive
|
|
50
|
+
* untouched, as does every other field.
|
|
51
|
+
*/
|
|
52
|
+
function normalizeStaleRunningJob(job: unknown): unknown {
|
|
53
|
+
if (!job || typeof job !== "object") return job;
|
|
54
|
+
const obj = job as Record<string, unknown>;
|
|
55
|
+
if (obj.lastStatus !== "running") return job;
|
|
56
|
+
const copy: Record<string, unknown> = { ...obj };
|
|
57
|
+
delete copy.lastStatus;
|
|
58
|
+
return copy;
|
|
59
|
+
}
|
|
60
|
+
|
|
42
61
|
function sanitizeSettings(raw: unknown): CrewSettings {
|
|
43
62
|
if (!raw || typeof raw !== "object") return {};
|
|
44
63
|
const r = raw as Record<string, unknown>;
|
|
@@ -74,9 +93,10 @@ function sanitizeSettings(raw: unknown): CrewSettings {
|
|
|
74
93
|
if (typeof r.notifierIntervalMs === "number" && r.notifierIntervalMs >= 1000) {
|
|
75
94
|
out.notifierIntervalMs = r.notifierIntervalMs;
|
|
76
95
|
}
|
|
77
|
-
// Pass through scheduledJobs after basic validation
|
|
96
|
+
// Pass through scheduledJobs after basic validation + stale-flag
|
|
97
|
+
// normalization (R6-2c — see normalizeStaleRunningJob).
|
|
78
98
|
if (Array.isArray(r.scheduledJobs)) {
|
|
79
|
-
out.scheduledJobs = (r.scheduledJobs as unknown[]).filter(validateScheduledJob);
|
|
99
|
+
out.scheduledJobs = (r.scheduledJobs as unknown[]).filter(validateScheduledJob).map(normalizeStaleRunningJob);
|
|
80
100
|
}
|
|
81
101
|
return out;
|
|
82
102
|
}
|
|
@@ -12,12 +12,14 @@
|
|
|
12
12
|
* onExit: MỘT interval 2s cho cả provider — mỗi tick gọi list-panes đúng
|
|
13
13
|
* một lần rồi đối chiếu mọi pane đang được theo dõi (không spawn N subprocess
|
|
14
14
|
* cho N pane). Pane biến khỏi list hoặc pane_dead=1 → "pane-closed" bắn đúng
|
|
15
|
-
* một lần. tmux call throw (binary mất đột ngột, server chết
|
|
15
|
+
* một lần. tmux call throw HAI lần liên tiếp (binary mất đột ngột, server chết
|
|
16
|
+
* — R7-13 strike counter; transient EAGAIN/EMFILE chỉ warn) → "mux-dead"
|
|
16
17
|
* cho mọi handle còn sống.
|
|
17
18
|
*/
|
|
18
19
|
|
|
19
20
|
import { execFileSync } from "node:child_process";
|
|
20
21
|
|
|
22
|
+
import { logInternalError } from "../../utils/internal-error.ts";
|
|
21
23
|
import type { SurfaceDetection, SurfaceExitReason, SurfaceHandle, SurfaceProvider, SurfaceSpawnOpts } from "./surface-provider.ts";
|
|
22
24
|
import { MAX_PANES_PER_TAB, splitDirectionFor } from "./surface-provider.ts";
|
|
23
25
|
|
|
@@ -111,6 +113,11 @@ export function createTmuxProvider(deps: TmuxProviderDeps = {}): SurfaceProvider
|
|
|
111
113
|
// Watcher theo pane id — MỘT timer chung cho mọi pane đang theo dõi.
|
|
112
114
|
const watchers = new Map<string, PaneWatcher>();
|
|
113
115
|
let timer: SurfaceTimer | null = null;
|
|
116
|
+
// R7-13 (DR-04 round-7): consecutive-failure strike counter cho poll list-panes.
|
|
117
|
+
// 1 lỗi transient (EAGAIN/EMFILE, binary hog dụng một chốc) KHÔNG được giết
|
|
118
|
+
// hết watcher — chỉ tăng strike + log warn; đến strike thứ 2 liên tiếp mới
|
|
119
|
+
// kết luận mux chết thật (semantics cũ). Poll thành công bất kỳ → reset về 0.
|
|
120
|
+
let muxFailStrikes = 0;
|
|
114
121
|
|
|
115
122
|
// Tab-layout (spec 2026-08-27-surface-tab-layout): tabKey(run) → MỌI window
|
|
116
123
|
// của run + số pane đã spawn trong window đang nhận. Tab mở khi run spawn
|
|
@@ -153,11 +160,25 @@ export function createTmuxProvider(deps: TmuxProviderDeps = {}): SurfaceProvider
|
|
|
153
160
|
// còn không -a thì chỉ thấy current window (pane-closed giả khi user
|
|
154
161
|
// switch window giữa chừng).
|
|
155
162
|
stdout = tmux(["list-panes", "-a", "-F", "#{pane_dead} #{pane_id}"]);
|
|
156
|
-
} catch {
|
|
157
|
-
//
|
|
163
|
+
} catch (error) {
|
|
164
|
+
// R7-13: strike counter — ≥2 lỗi list-panes LIÊN TIẾP mới coi là mux chết.
|
|
165
|
+
// Lỗi đơn lẻ (transient EAGAIN/EMFILE…) → giữ nguyên watchers + warn,
|
|
166
|
+
// tick kế vẫn poll (counter được reset bởi bất kỳ poll thành công nào).
|
|
167
|
+
muxFailStrikes += 1;
|
|
168
|
+
if (muxFailStrikes < 2) {
|
|
169
|
+
logInternalError(
|
|
170
|
+
"tmux-provider",
|
|
171
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
172
|
+
`list-panes failed (strike ${muxFailStrikes}/2) — keeping pane watchers alive`,
|
|
173
|
+
"warn",
|
|
174
|
+
);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
// tmux binary mất đột ngột / server chết thật sự — mọi handle còn sống thành mux-dead.
|
|
158
178
|
for (const paneId of [...watchers.keys()]) fire(paneId, "mux-dead");
|
|
159
179
|
return;
|
|
160
180
|
}
|
|
181
|
+
muxFailStrikes = 0;
|
|
161
182
|
const status = parsePaneStatus(stdout);
|
|
162
183
|
for (const [paneId, watcher] of [...watchers]) {
|
|
163
184
|
// Không có trong list (đã dọn) hoặc pane_dead=1 → đóng.
|
|
@@ -32,6 +32,10 @@ export interface TaskSummary {
|
|
|
32
32
|
id: string;
|
|
33
33
|
status: string;
|
|
34
34
|
stalledSince?: number;
|
|
35
|
+
/** L-6 (2026-10-16): last worker heartbeat (tasks already carry
|
|
36
|
+
* `heartbeat.lastSeenAt` — no new writer needed). Preferred over
|
|
37
|
+
* `stalledSince`, which has no writer. */
|
|
38
|
+
heartbeat?: { lastSeenAt: string };
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
export interface ManifestSummary {
|
|
@@ -47,7 +51,18 @@ export function computeRunHealth(manifest: ManifestSummary): RunHealth {
|
|
|
47
51
|
if (taskCount === 0) return { score: 100, grade: "A", penalties: [], deltas: [] };
|
|
48
52
|
|
|
49
53
|
const failedCount = tasks.filter((t) => t.status === "failed").length;
|
|
50
|
-
|
|
54
|
+
// L-6: stall detection derived from the task's own heartbeat (already
|
|
55
|
+
// written by worker heartbeat tracking — no new writer). Only RUNNING
|
|
56
|
+
// tasks can stall. `stalledSince` (no known writer) stays as a fallback
|
|
57
|
+
// for summaries without heartbeat data.
|
|
58
|
+
const stalledCount = tasks.filter((t) => {
|
|
59
|
+
if (t.status !== "running") return false;
|
|
60
|
+
if (t.heartbeat) {
|
|
61
|
+
const lastSeenAt = Date.parse(t.heartbeat.lastSeenAt);
|
|
62
|
+
return Number.isFinite(lastSeenAt) && Date.now() - lastSeenAt > STALLED_THRESHOLD_MS;
|
|
63
|
+
}
|
|
64
|
+
return t.stalledSince !== undefined && Date.now() - t.stalledSince > STALLED_THRESHOLD_MS;
|
|
65
|
+
}).length;
|
|
51
66
|
|
|
52
67
|
const failureRate = failedCount / taskCount;
|
|
53
68
|
if (failureRate > 0.2) {
|
|
@@ -6,9 +6,11 @@ import { atomicWriteFile } from "../state/atomic-write.ts";
|
|
|
6
6
|
import { writeArtifact } from "../state/stores/artifact-store.ts";
|
|
7
7
|
import type { ArtifactDescriptor, TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
8
8
|
import { resolveRealContainedPath } from "../utils/safe-paths.ts";
|
|
9
|
+
import { countTokens } from "../utils/token-counter.ts";
|
|
9
10
|
import type { WorkflowStep } from "../workflows/workflow-config.ts";
|
|
10
11
|
import { applyCompactPipeline } from "./compaction/compact-pipeline.ts";
|
|
11
12
|
import { ANSI_STRIP_STAGE, BLANK_COLLAPSE_STAGE, TruncationStage } from "./compaction/compact-stages/index.ts";
|
|
13
|
+
import { splitWithImportantLines } from "./compaction/important-line-classifier.ts";
|
|
12
14
|
import { DEFAULT_PRUNE_CONFIG, type FileEditEvent, pruneToolOutputs, type ToolResultEntry } from "./compaction/tool-output-pruner.ts";
|
|
13
15
|
|
|
14
16
|
export interface DependencyContextEntry {
|
|
@@ -47,6 +49,13 @@ export interface DependencyContextEntry {
|
|
|
47
49
|
* block for marked entries; `writeTaskInputsArtifact` still serializes the
|
|
48
50
|
* ORIGINAL (untrimmed) context — the trim is render-only, never persisted. */
|
|
49
51
|
budgetTrimmed?: boolean;
|
|
52
|
+
/** G6 (DR-02 round-5): set by {@link enforceDependencyInlineBudget} when the
|
|
53
|
+
* 96KB layer downgraded this dep to path-only (`resultSummary: ""`). The
|
|
54
|
+
* renderer uses it to emit "[result withheld by inline budget — see
|
|
55
|
+
* artifact path above]" instead of the false "(no result output)" — the
|
|
56
|
+
* output EXISTS on disk at `resultPath`; only the inline copy was
|
|
57
|
+
* budgeted away. */
|
|
58
|
+
resultWithheldByBudget?: boolean;
|
|
50
59
|
}
|
|
51
60
|
|
|
52
61
|
export interface DependencyOutputContext {
|
|
@@ -302,6 +311,22 @@ function aggregateUsage(task: TeamTaskState): DependencyContextEntry["usage"] {
|
|
|
302
311
|
return { inputTokens, outputTokens, durationMs };
|
|
303
312
|
}
|
|
304
313
|
|
|
314
|
+
/** L4 + handoff-budget shared priority score (higher = keep inline / keep
|
|
315
|
+
* FULL form longer). R5-5 (DR-02): the handoff-budget trim
|
|
316
|
+
* (`applyHandoffBudget`) reuses the exact scoring the 96KB L4 layer
|
|
317
|
+
* (`enforceDependencyInlineBudget`) uses so a failed/needs-attention dep is
|
|
318
|
+
* the LAST thing either budget layer trims.
|
|
319
|
+
*
|
|
320
|
+
* Score: (highPriority?1000:0) + (recency/1e10) + (relevance*10) - (index*0.001)
|
|
321
|
+
* The 1000 base ensures high-priority wins even with no recency/relevance.
|
|
322
|
+
* recency/1e10 keeps it in [0, 1] for ms timestamps up to 1e13 (~year 2286).
|
|
323
|
+
* relevance*10 puts it in [0, 10].
|
|
324
|
+
* index*0.001 keeps stable order within the same bucket. */
|
|
325
|
+
function inlinePriorityScore(dep: DependencyContextEntry, originalIndex: number): number {
|
|
326
|
+
const highPriority = dep.status === "failed" || dep.status === "needs-attention";
|
|
327
|
+
return (highPriority ? 1000 : 0) + (dep.recency ?? 0) / 1e10 + (dep.relevance ?? 0) * 10 - originalIndex * 0.001;
|
|
328
|
+
}
|
|
329
|
+
|
|
305
330
|
/**
|
|
306
331
|
* L4 total-budget trim. When the SUM of `inlineBytes` across all deps
|
|
307
332
|
* exceeds {@link MAX_TOTAL_DEP_INLINE_BYTES}, downgrade the lowest-priority
|
|
@@ -330,16 +355,11 @@ export function enforceDependencyInlineBudget(dependencies: DependencyContextEnt
|
|
|
330
355
|
// Build an index-keyed array so we can sort AND preserve the original
|
|
331
356
|
// position (for stable output order). Each entry gets a numeric
|
|
332
357
|
// priority score (higher = more important) and a stable tie-breaker.
|
|
333
|
-
const indexed = dependencies.map((dep, originalIndex) => {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
// relevance*10 puts it in [0, 10].
|
|
339
|
-
// index*0.001 keeps stable order within the same bucket.
|
|
340
|
-
const score = (highPriority ? 1000 : 0) + (dep.recency ?? 0) / 1e10 + (dep.relevance ?? 0) * 10 - originalIndex * 0.001;
|
|
341
|
-
return { dep, originalIndex, score };
|
|
342
|
-
});
|
|
358
|
+
const indexed = dependencies.map((dep, originalIndex) => ({
|
|
359
|
+
dep,
|
|
360
|
+
originalIndex,
|
|
361
|
+
score: inlinePriorityScore(dep, originalIndex),
|
|
362
|
+
}));
|
|
343
363
|
const sorted = [...indexed].sort((a, b) => b.score - a.score);
|
|
344
364
|
let used = 0;
|
|
345
365
|
const keepSet = new Set<number>(); // originalIndex values to KEEP inline
|
|
@@ -358,6 +378,10 @@ export function enforceDependencyInlineBudget(dependencies: DependencyContextEnt
|
|
|
358
378
|
resultSummary: "",
|
|
359
379
|
structuredResults: undefined,
|
|
360
380
|
inlineBytes: 0,
|
|
381
|
+
// G6 (DR-02): flag the path-only downgrade so the renderer says the
|
|
382
|
+
// output was withheld by the inline budget instead of falling into the
|
|
383
|
+
// false "(no result output)" (the output exists at resultPath).
|
|
384
|
+
resultWithheldByBudget: true,
|
|
361
385
|
};
|
|
362
386
|
});
|
|
363
387
|
}
|
|
@@ -548,34 +572,34 @@ function sanitizeFencedBody(body: string): string {
|
|
|
548
572
|
return body.replace(DEPENDENCY_CONTROL_CHAR_PATTERN, "").replace(/<\/dependency-context/g, "</dependency-context");
|
|
549
573
|
}
|
|
550
574
|
|
|
551
|
-
// ── Handoff budget (effective-token cap on this layer:
|
|
552
|
-
// anchored upward by measured
|
|
575
|
+
// ── Handoff budget (effective-token cap on this layer: countTokens — ──
|
|
576
|
+
// CJK-aware, in-tree utils/token-counter.ts — anchored upward by measured
|
|
577
|
+
// worker usage, U10) ───────────────────────────────────────────────────────
|
|
553
578
|
|
|
554
579
|
/** Default effective-token budget for the dynamic.dependencyContext layer
|
|
555
|
-
* (tokens as measured by
|
|
556
|
-
*
|
|
580
|
+
* (tokens as measured by `countTokens` (utils/token-counter.ts — R5-7,
|
|
581
|
+
* DR-02: CJK-aware; the old chars/4 heuristic underestimated CJK ~2-3x),
|
|
582
|
+
* floored by measured worker usage where available — U10). */
|
|
557
583
|
export const DEFAULT_HANDOFF_BUDGET_TOKENS = 1800;
|
|
558
584
|
/** Budget ceiling: a resolved budget above this is treated as the off-switch
|
|
559
585
|
* (same semantics as ≤0 — clearly not a real handoff cap, so render
|
|
560
586
|
* untrimmed instead of guessing). */
|
|
561
587
|
export const HANDOFF_BUDGET_CEILING_TOKENS = 1_000_000;
|
|
562
|
-
/** Summary head kept per dependency when the budget trims it.
|
|
588
|
+
/** Summary head+tail budget kept per dependency when the budget trims it.
|
|
589
|
+
* R5-5 (DR-02): the compact form is no longer a plain head slice — the
|
|
590
|
+
* summary runs through `splitWithImportantLines` (75% head + important
|
|
591
|
+
* middle lines + 25% tail) so error/failed/exception lines from ANYWHERE in
|
|
592
|
+
* the summary survive, matching the full-form TruncationStage behavior. */
|
|
563
593
|
export const HANDOFF_SUMMARY_HEAD_CHARS = 240;
|
|
594
|
+
/** Slack fraction of {@link HANDOFF_SUMMARY_HEAD_CHARS} available for
|
|
595
|
+
* important middle lines in the compact form (R5-5). The classifier default
|
|
596
|
+
* (0.15 = 36 chars) is too tight for realistic diagnostics; 0.25 (60 chars)
|
|
597
|
+
* admits 1-2 typical error lines while keeping the compact entry small. */
|
|
598
|
+
const HANDOFF_COMPACT_IMPORTANT_SLACK = 0.25;
|
|
564
599
|
|
|
565
600
|
/** Strict integer env syntax — "12abc" must NOT parse as 12. */
|
|
566
601
|
const HANDOFF_BUDGET_ENV_RE = /^[+-]?\d+$/;
|
|
567
602
|
|
|
568
|
-
/** Est tokens = chars/4 — the same heuristic as `estimateTokens(chars)` in
|
|
569
|
-
* src/runtime/task-runner/prompt-builder.ts (SR-02 breakdown). Duplicated
|
|
570
|
-
* here instead of imported: importing prompt-builder would pull its
|
|
571
|
-
* retrieval/workspace-tree graph into every task-output-context consumer
|
|
572
|
-
* (post-execution, aggregate outputs) for a one-line heuristic. Keep the
|
|
573
|
-
* two in sync if either changes; the U10 max(heuristic, measured) combine
|
|
574
|
-
* rule mirrors prompt-builder's `estimateTokens(chars, measuredTokens)`. */
|
|
575
|
-
function estimateHandoffTokens(chars: number): number {
|
|
576
|
-
return Math.round(chars / 4);
|
|
577
|
-
}
|
|
578
|
-
|
|
579
603
|
/** U10 measured-token anchor: Σ measured `usage.outputTokens` over the deps
|
|
580
604
|
* still rendered in FULL form. The usage record is the worker's REAL
|
|
581
605
|
* tokenizer measurement (message_end usage persisted on the task state via
|
|
@@ -590,9 +614,10 @@ function estimateHandoffTokens(chars: number): number {
|
|
|
590
614
|
* path is the compact form's artifact pointer.
|
|
591
615
|
*
|
|
592
616
|
* Compact (`budgetTrimmed`) deps are deliberately NOT charged: their inline
|
|
593
|
-
* body is a ≤240-char head + pointer, no longer the worker's output, so
|
|
594
|
-
*
|
|
595
|
-
* output-heavy dep could never fit any budget even fully
|
|
617
|
+
* body is a ≤240-char head/tail + pointer, no longer the worker's output, so
|
|
618
|
+
* the countTokens heuristic on the small body is the honest measure —
|
|
619
|
+
* otherwise an output-heavy dep could never fit any budget even fully
|
|
620
|
+
* trimmed. */
|
|
596
621
|
function measuredHandoffTokens(context: DependencyOutputContext): number {
|
|
597
622
|
let measured = 0;
|
|
598
623
|
for (const dep of context.dependencies) {
|
|
@@ -602,19 +627,18 @@ function measuredHandoffTokens(context: DependencyOutputContext): number {
|
|
|
602
627
|
return measured;
|
|
603
628
|
}
|
|
604
629
|
|
|
605
|
-
/** Measure a candidate context in EFFECTIVE tokens (U10): the
|
|
606
|
-
* heuristic on the rendered body,
|
|
607
|
-
*
|
|
608
|
-
* the
|
|
609
|
-
*
|
|
610
|
-
*
|
|
611
|
-
* (
|
|
612
|
-
*
|
|
630
|
+
/** Measure a candidate context in EFFECTIVE tokens (U10): the
|
|
631
|
+
* `countTokens` heuristic on the rendered body (R5-7, DR-02: CJK-aware —
|
|
632
|
+
* replaces the chars/4 estimate that under-counted CJK summaries ~2-3x),
|
|
633
|
+
* floored by the measured-token anchor — `max(heuristic, Σ measured output
|
|
634
|
+
* tokens of full-form deps)`, mirroring the render byte-for-byte (same parts
|
|
635
|
+
* builder; `.trim()` matches the render join). The max rule mirrors
|
|
636
|
+
* `estimateTokens(chars, measuredTokens)` in prompt-builder.ts: a real
|
|
637
|
+
* measurement can only TIGHTEN the estimate (spec U10 — the budget must
|
|
638
|
+
* never be exceeded when measured usage is larger than the heuristic),
|
|
639
|
+
* never loosen it below the heuristic. */
|
|
613
640
|
function measureHandoffTokens(context: DependencyOutputContext): number {
|
|
614
|
-
return Math.max(
|
|
615
|
-
estimateHandoffTokens(buildDependencyOutputBodyParts(context).join("\n").trim().length),
|
|
616
|
-
measuredHandoffTokens(context),
|
|
617
|
-
);
|
|
641
|
+
return Math.max(countTokens(buildDependencyOutputBodyParts(context).join("\n").trim()), measuredHandoffTokens(context));
|
|
618
642
|
}
|
|
619
643
|
|
|
620
644
|
/** Budget active iff it is a positive finite number at or below the ceiling;
|
|
@@ -646,7 +670,11 @@ function dependencyEntryLines(dep: DependencyContextEntry): string[] {
|
|
|
646
670
|
`Status: ${dep.status}`,
|
|
647
671
|
dep.resultPath ? `Result artifact: ${dep.resultPath}` : "",
|
|
648
672
|
"",
|
|
649
|
-
|
|
673
|
+
// G6 (DR-02 round-5): an L4 path-only downgrade has output on disk —
|
|
674
|
+
// never fall into the "(no result output)" lie for it.
|
|
675
|
+
dep.resultWithheldByBudget
|
|
676
|
+
? "[result withheld by inline budget — see artifact path above]"
|
|
677
|
+
: dep.resultSummary?.trim() || "(no result output)",
|
|
650
678
|
"",
|
|
651
679
|
];
|
|
652
680
|
// P1-A dependency tee-recovery hint: when the dependency's result was
|
|
@@ -662,18 +690,35 @@ function dependencyEntryLines(dep: DependencyContextEntry): string[] {
|
|
|
662
690
|
}
|
|
663
691
|
|
|
664
692
|
/** Compact handoff-budget form for one dependency (pre-sanitize): keeps the
|
|
665
|
-
* taskId/role/status lines + a
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
*
|
|
669
|
-
*
|
|
670
|
-
*
|
|
671
|
-
*
|
|
693
|
+
* taskId/role/status lines + a head/tail summary budget of
|
|
694
|
+
* {@link HANDOFF_SUMMARY_HEAD_CHARS} chars with important-line preservation
|
|
695
|
+
* (R5-5, DR-02: the old plain 240-char HEAD slice dropped a failed dep's
|
|
696
|
+
* error tail and any diagnostic beyond char 240 — the summary now runs
|
|
697
|
+
* through the same `splitWithImportantLines` the full-form TruncationStage
|
|
698
|
+
* path uses, so error/failed/exception lines from ANYWHERE in the summary
|
|
699
|
+
* survive) + a `[trimmed, N chars total]` marker when the summary was longer
|
|
700
|
+
* + a relative artifact pointer the worker can `read`. Drops
|
|
701
|
+
* structuredResults, usage, artifactsProduced and the absolute-path lines —
|
|
702
|
+
* all recoverable via the pointer. The pointer is built ONLY from
|
|
703
|
+
* manifest-derived runId/taskId (same trust level as the existing
|
|
704
|
+
* `## <taskId>` headers and the results/<taskId>.txt artifact naming);
|
|
705
|
+
* dep-controlled content never reaches it. */
|
|
672
706
|
function budgetTrimmedDependencyEntryLines(dep: DependencyContextEntry, runId: string | undefined): string[] {
|
|
673
707
|
const summary = dep.resultSummary?.trim() ?? "";
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
708
|
+
let body: string;
|
|
709
|
+
if (dep.resultWithheldByBudget) {
|
|
710
|
+
// G6 (DR-02): compact form of an L4-downgraded dep — same anti-lie
|
|
711
|
+
// marker; the pointer line below is the recovery path.
|
|
712
|
+
body = "[result withheld by inline budget — see full output path below]";
|
|
713
|
+
} else if (summary.length <= HANDOFF_SUMMARY_HEAD_CHARS) {
|
|
714
|
+
body = summary || "(no result output)";
|
|
715
|
+
} else {
|
|
716
|
+
const split = splitWithImportantLines(summary, HANDOFF_SUMMARY_HEAD_CHARS, {
|
|
717
|
+
slackFactor: HANDOFF_COMPACT_IMPORTANT_SLACK,
|
|
718
|
+
});
|
|
719
|
+
const segments = [split.head, ...split.importantLines, split.tail].filter((segment) => segment.length > 0);
|
|
720
|
+
body = `${segments.join("\n")}\n[trimmed, ${summary.length} chars total]`;
|
|
721
|
+
}
|
|
677
722
|
return [
|
|
678
723
|
`## ${dep.taskId} (${dep.role})`,
|
|
679
724
|
`Status: ${dep.status}`,
|
|
@@ -715,9 +760,10 @@ function buildDependencyOutputBodyParts(context: DependencyOutputContext): strin
|
|
|
715
760
|
|
|
716
761
|
/**
|
|
717
762
|
* Handoff budget: trim the dependency-output context to an EFFECTIVE-token
|
|
718
|
-
* budget (U10). Effective tokens = max(
|
|
719
|
-
* body
|
|
720
|
-
*
|
|
763
|
+
* budget (U10). Effective tokens = max(countTokens heuristic on the rendered
|
|
764
|
+
* body — R5-7 (DR-02): CJK-aware, from utils/token-counter.ts — Σ measured
|
|
765
|
+
* `usage.outputTokens` of full-form deps — real tokenizer numbers from
|
|
766
|
+
* message_end usage persisted on the task state). Measured
|
|
721
767
|
* usage can only tighten: when a worker reports MORE tokens than the
|
|
722
768
|
* heuristic estimates, the measured number governs and the trim fires at
|
|
723
769
|
* least as early as before (spec U10 acceptance — the budget is never
|
|
@@ -727,14 +773,20 @@ function buildDependencyOutputBodyParts(context: DependencyOutputContext): strin
|
|
|
727
773
|
* input reference unchanged when the budget is off (≤0 or >1_000_000) or
|
|
728
774
|
* the context already fits.
|
|
729
775
|
*
|
|
730
|
-
* Trim policy (
|
|
731
|
-
*
|
|
732
|
-
*
|
|
733
|
-
*
|
|
734
|
-
*
|
|
735
|
-
*
|
|
736
|
-
*
|
|
737
|
-
*
|
|
776
|
+
* Trim policy (R5-5, DR-02 — failed-priority, mirroring the 96KB layer):
|
|
777
|
+
* dependencies are probed from ALL-full downward in DOWNGRADE-PRIORITY
|
|
778
|
+
* order — the same score `enforceDependencyInlineBudget` uses
|
|
779
|
+
* (`inlinePriorityScore`: status failed/needs-attention gets the +1000
|
|
780
|
+
* boost, then recency, then relevance, then declaration index). The
|
|
781
|
+
* lowest-keep-priority deps are downgraded to the compact form
|
|
782
|
+
* (taskId/role/status + head/tail summary ≤240 chars with important-line
|
|
783
|
+
* preservation + `artifacts/<runId>/results/<taskId>.txt` pointer) FIRST;
|
|
784
|
+
* failed/needs-attention deps therefore keep their FULL form longest — the
|
|
785
|
+
* downstream worker must see the failure. Declaration order still
|
|
786
|
+
* guarantees that among same-priority deps, earlier ones keep ≥ as much as
|
|
787
|
+
* later ones. Long (>240 chars) sharedRead bodies are replaced by a trim
|
|
788
|
+
* marker (the Path pointer is already rendered); short shared reads pass
|
|
789
|
+
* through.
|
|
738
790
|
*
|
|
739
791
|
* When even the fully-trimmed form exceeds the budget (extreme dependency
|
|
740
792
|
* count), the fully-trimmed context is returned as the bounded best effort —
|
|
@@ -757,16 +809,36 @@ export function applyHandoffBudget(context: DependencyOutputContext, budgetToken
|
|
|
757
809
|
? { ...read, content: `[content trimmed, ${content.length} chars total — read Path above for the full file]` }
|
|
758
810
|
: read;
|
|
759
811
|
};
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
812
|
+
// R5-5 (DR-02): downgrade ORDER — ascending keep-priority (the FIRST
|
|
813
|
+
// entries are downgraded first). Reuses the exact scoring the 96KB L4
|
|
814
|
+
// layer uses (`inlinePriorityScore`) so failed/needs-attention deps land
|
|
815
|
+
// LAST and keep their FULL form longest; within the same priority bucket
|
|
816
|
+
// later-declared deps are downgraded before earlier ones.
|
|
817
|
+
const downgradeOrder = base.dependencies
|
|
818
|
+
.map((dep, originalIndex) => ({ originalIndex, score: inlinePriorityScore(dep, originalIndex) }))
|
|
819
|
+
.sort((a, b) => a.score - b.score)
|
|
820
|
+
.map((item) => item.originalIndex);
|
|
821
|
+
const candidateAt = (downgradedCount: number): DependencyOutputContext => {
|
|
822
|
+
// Downgrade the `downgradedCount` LOWEST-keep-priority deps first
|
|
823
|
+
// (R5-5): failed/needs-attention deps are last in `downgradeOrder`, so
|
|
824
|
+
// they keep their FULL form longest.
|
|
825
|
+
const downgradedIndexes = new Set(downgradeOrder.slice(0, downgradedCount));
|
|
826
|
+
return {
|
|
827
|
+
...base,
|
|
828
|
+
dependencies: base.dependencies.map((dep, index) => (downgradedIndexes.has(index) ? { ...dep, budgetTrimmed: true } : dep)),
|
|
829
|
+
sharedReads: base.sharedReads.map(trimmedRead),
|
|
830
|
+
};
|
|
831
|
+
};
|
|
832
|
+
// Probe from fewest-downgraded upward and return the first candidate that
|
|
833
|
+
// fits — the maximum FULL-form set under the failed-priority order. More
|
|
834
|
+
// downgrades strictly shrink the measure (compact form < full form for
|
|
835
|
+
// both the heuristic and the measured anchor), so the first fit is the
|
|
836
|
+
// best fit.
|
|
837
|
+
for (let downgradedCount = 0; downgradedCount <= base.dependencies.length; downgradedCount++) {
|
|
838
|
+
const candidate = candidateAt(downgradedCount);
|
|
767
839
|
if (measureHandoffTokens(candidate) <= budgetTokens) return candidate;
|
|
768
840
|
}
|
|
769
|
-
return candidateAt(
|
|
841
|
+
return candidateAt(base.dependencies.length);
|
|
770
842
|
}
|
|
771
843
|
|
|
772
844
|
export interface RenderDependencyOutputContextOptions {
|
|
@@ -11,6 +11,28 @@ import { generateTaskHashId } from "./task-id.ts";
|
|
|
11
11
|
// See: SECURITY-ISSUES.md SEC-007
|
|
12
12
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
13
13
|
|
|
14
|
+
export interface TaskTextSanitizeResult {
|
|
15
|
+
/** Sanitized text WITHOUT the redaction note (the note is composed by the
|
|
16
|
+
* caller — see {@link sanitizeTaskText} / {@link appendSecurityRedactionNote}). */
|
|
17
|
+
text: string;
|
|
18
|
+
/** G7 (DR-02 round-5): number of content redactions applied by rules 1b–9
|
|
19
|
+
* (each replaced match counts 1 — marker substitutions AND strip-to-empty
|
|
20
|
+
* removals). Zero-width stripping (rule 1) and blank-line collapse
|
|
21
|
+
* (rule 10) are cosmetic and never counted. */
|
|
22
|
+
redactionCount: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** G7 (DR-02 round-5): the single trailing note line appended when ≥1
|
|
26
|
+
* security redaction fired. Silent redaction left sanitized objectives
|
|
27
|
+
* like "do not [suspicious-write-redacted]s to logs" with no explanation —
|
|
28
|
+
* the worker could not tell meaning was removed. Count = number of
|
|
29
|
+
* replacements made. No note when nothing was redacted (byte-identical to
|
|
30
|
+
* the pre-G7 output for clean inputs). */
|
|
31
|
+
export function appendSecurityRedactionNote(text: string, redactionCount: number): string {
|
|
32
|
+
if (redactionCount <= 0) return text;
|
|
33
|
+
return `${text}\n\n(${redactionCount} security redaction(s) applied by task-text sanitizer — rephrase constraints avoiding trigger patterns if this removed intended meaning)`;
|
|
34
|
+
}
|
|
35
|
+
|
|
14
36
|
/**
|
|
15
37
|
* Sanitize workflow step task text to reduce injection risk.
|
|
16
38
|
*
|
|
@@ -23,51 +45,74 @@ import { generateTaskHashId } from "./task-id.ts";
|
|
|
23
45
|
* - Strip known prompt injection directive patterns
|
|
24
46
|
* - Strip base64/hex encoded payloads
|
|
25
47
|
* - Collapse excessive whitespace
|
|
48
|
+
*
|
|
49
|
+
* G7: every content redaction is counted; `sanitizeTaskText` appends ONE
|
|
50
|
+
* trailing note line when ≥1 fired (see {@link appendSecurityRedactionNote}).
|
|
26
51
|
*/
|
|
27
|
-
export function
|
|
52
|
+
export function sanitizeTaskTextWithRedactionCount(task: string): TaskTextSanitizeResult {
|
|
28
53
|
let sanitized = task;
|
|
54
|
+
let redactionCount = 0;
|
|
55
|
+
// Counting replacer: each replacement the pattern performs (marker
|
|
56
|
+
// substitution or strip-to-empty removal) counts as one redaction hit.
|
|
57
|
+
const counted = (replacement: string) => (): string => {
|
|
58
|
+
redactionCount += 1;
|
|
59
|
+
return replacement;
|
|
60
|
+
};
|
|
29
61
|
|
|
30
|
-
// 1. Strip zero-width and invisible Unicode characters
|
|
62
|
+
// 1. Strip zero-width and invisible Unicode characters (cosmetic — never
|
|
63
|
+
// counted: removing invisible chars cannot change intended meaning).
|
|
31
64
|
sanitized = sanitized.replace(/[\u200B-\u200F\u2028-\u202F\u2060-\u206F\uFEFF]/g, "");
|
|
32
65
|
|
|
33
66
|
// 1b. Strip HTML/JS comments and script tags (instruction hiding).
|
|
34
67
|
// SEC-4: bounded quantifier {0,8192} prevents polynomial O(n²) backtracking
|
|
35
68
|
// DoS on pathological inputs (e.g. an unclosed `<!--` with no matching `-->`).
|
|
36
|
-
sanitized = sanitized.replace(/<!--[\s\S]{0,8192}?-->|<\/?script[^>]*>/gi, "");
|
|
69
|
+
sanitized = sanitized.replace(/<!--[\s\S]{0,8192}?-->|<\/?script[^>]*>/gi, counted(""));
|
|
37
70
|
|
|
38
71
|
// 2. Strip known prompt injection directive patterns
|
|
39
72
|
sanitized = sanitized.replace(
|
|
40
73
|
/^\s*(?:SYSTEM|INSTRUCTION|IGNORE(?:\s+ALL)?\s+(?:PREVIOUS|INSTRUCTIONS)?|OVERRIDE|YOUR\s+ROLE\s+IS|MALICIOUS)\s*:.*$/gim,
|
|
41
|
-
"",
|
|
74
|
+
counted(""),
|
|
42
75
|
);
|
|
43
76
|
|
|
44
77
|
// 3. Strip base64/hex encoded command payloads
|
|
45
|
-
sanitized = sanitized.replace(/\b(?:base64|base32|hex)\s*['":]\s*([A-Za-z0-9+\/=]{16,})/gi, "[encoded-redacted]");
|
|
78
|
+
sanitized = sanitized.replace(/\b(?:base64|base32|hex)\s*['":]\s*([A-Za-z0-9+\/=]{16,})/gi, counted("[encoded-redacted]"));
|
|
46
79
|
|
|
47
80
|
// 4. Strip embedded instruction patterns in brackets (incl. MALICIOUS to match agent sanitizer)
|
|
48
|
-
sanitized = sanitized.replace(/\[(?:SYSTEM|INSTRUCTION|OVERRIDE|MALICIOUS)\s*:[^\]]*\]/gi, "");
|
|
81
|
+
sanitized = sanitized.replace(/\[(?:SYSTEM|INSTRUCTION|OVERRIDE|MALICIOUS)\s*:[^\]]*\]/gi, counted(""));
|
|
49
82
|
|
|
50
83
|
// 5. Strip eval/exec patterns with encoded content
|
|
51
|
-
sanitized = sanitized.replace(
|
|
84
|
+
sanitized = sanitized.replace(
|
|
85
|
+
/\b(?:eval|exec|spawn|subprocess)\s*\(\s*(?:base64|Buffer\.from)\s*\(/gi,
|
|
86
|
+
counted("[suspicious-call-redacted]"),
|
|
87
|
+
);
|
|
52
88
|
|
|
53
89
|
// 6. Strip markdown codeblocks that attempt to hide instructions.
|
|
54
90
|
// SEC-4: bounded quantifier {0,8192} prevents polynomial backtracking DoS.
|
|
55
|
-
sanitized = sanitized.replace(/```\s*(?:system|instruction|prompt)\n[\s\S]{0,8192}?```/gi, "");
|
|
91
|
+
sanitized = sanitized.replace(/```\s*(?:system|instruction|prompt)\n[\s\S]{0,8192}?```/gi, counted(""));
|
|
56
92
|
|
|
57
93
|
// 7. Strip YAML-like assignment patterns that could override behavior.
|
|
58
94
|
// Applied unconditionally — task text is always untrusted.
|
|
59
|
-
sanitized = sanitized.replace(/^\s*(?:role|persona|behavior|directive)\s*[=:].*$/gim, "");
|
|
95
|
+
sanitized = sanitized.replace(/^\s*(?:role|persona|behavior|directive)\s*[=:].*$/gim, counted(""));
|
|
60
96
|
|
|
61
97
|
// 8. Strip potential exfiltration patterns
|
|
62
|
-
sanitized = sanitized.replace(/\b(?:write|append)\s+.*(?:secrets?|keys?|token|credential)/gi, "[suspicious-write-redacted]");
|
|
98
|
+
sanitized = sanitized.replace(/\b(?:write|append)\s+.*(?:secrets?|keys?|token|credential)/gi, counted("[suspicious-write-redacted]"));
|
|
63
99
|
|
|
64
100
|
// 9. Strip network exfiltration patterns
|
|
65
|
-
sanitized = sanitized.replace(/\b(?:fetch|curl|wget|axios)\s+.*(?:exfil|steal|leak|send)/gi, "[suspicious-network-redacted]");
|
|
101
|
+
sanitized = sanitized.replace(/\b(?:fetch|curl|wget|axios)\s+.*(?:exfil|steal|leak|send)/gi, counted("[suspicious-network-redacted]"));
|
|
66
102
|
|
|
67
|
-
// 10. Collapse multiple blank lines (cleanup after removals)
|
|
103
|
+
// 10. Collapse multiple blank lines (cleanup after removals — cosmetic, never counted)
|
|
68
104
|
sanitized = sanitized.replace(/\n{3,}/g, "\n\n");
|
|
69
105
|
|
|
70
|
-
return sanitized.trim();
|
|
106
|
+
return { text: sanitized.trim(), redactionCount };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Sanitize task text and, when any content redaction fired, append ONE
|
|
111
|
+
* trailing note line explaining it (G7, DR-02 round-5).
|
|
112
|
+
*/
|
|
113
|
+
export function sanitizeTaskText(task: string): string {
|
|
114
|
+
const { text, redactionCount } = sanitizeTaskTextWithRedactionCount(task);
|
|
115
|
+
return appendSecurityRedactionNote(text, redactionCount);
|
|
71
116
|
}
|
|
72
117
|
|
|
73
118
|
export interface BuildTaskPacketInput {
|
|
@@ -120,9 +165,17 @@ export function buildTaskPacket(input: BuildTaskPacketInput): TaskPacket {
|
|
|
120
165
|
const scope = inferTaskScope(input.step);
|
|
121
166
|
const reads = input.step.reads === false ? [] : (input.step.reads ?? []);
|
|
122
167
|
const scopePath = reads.length === 1 ? reads[0] : reads.length > 1 ? reads.join(", ") : undefined;
|
|
123
|
-
// SEC-007: Sanitize task text before inserting into task packet
|
|
124
|
-
|
|
125
|
-
|
|
168
|
+
// SEC-007: Sanitize task text before inserting into task packet.
|
|
169
|
+
// G7 (DR-02 round-5): count redactions on BOTH the task and the goal — the
|
|
170
|
+
// packet objective carries ONE combined trailing note so the worker knows
|
|
171
|
+
// meaning may have been removed (per-text notes inside the {goal}
|
|
172
|
+
// substitution could land mid-objective and stack to two notes).
|
|
173
|
+
const sanitizedTaskResult = sanitizeTaskTextWithRedactionCount(input.step.task);
|
|
174
|
+
const sanitizedGoalResult = sanitizeTaskTextWithRedactionCount(input.manifest.goal);
|
|
175
|
+
const objective = appendSecurityRedactionNote(
|
|
176
|
+
sanitizedTaskResult.text.replaceAll("{goal}", sanitizedGoalResult.text),
|
|
177
|
+
sanitizedTaskResult.redactionCount + sanitizedGoalResult.redactionCount,
|
|
178
|
+
);
|
|
126
179
|
|
|
127
180
|
// Generate a deterministic hash-based task ID for traceability and logging.
|
|
128
181
|
// Uses goal + step ID + run ID as content parts.
|
|
@@ -130,7 +183,7 @@ export function buildTaskPacket(input: BuildTaskPacketInput): TaskPacket {
|
|
|
130
183
|
const _taskHashId = generateTaskHashId([input.manifest.goal, input.step.id, input.manifest.runId]);
|
|
131
184
|
|
|
132
185
|
return {
|
|
133
|
-
objective
|
|
186
|
+
objective,
|
|
134
187
|
...(declaredSpecRefs.length > 0 ? { specRefs: declaredSpecRefs, specSnapshots } : {}),
|
|
135
188
|
...(unresolvedSpecRefs.length > 0 ? { unresolvedSpecRefs } : {}),
|
|
136
189
|
...(input.specStrict === true ? { specStrict: true } : {}),
|