@sema-agent/core 5.2.0 → 5.4.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 +39 -0
- package/dist/brain/anthropic.js +12 -2
- package/dist/brain/media-degrade.d.ts +14 -0
- package/dist/brain/media-degrade.js +30 -0
- package/dist/brain/openai.js +9 -11
- package/dist/brain/timeout.d.ts +14 -0
- package/dist/brain/timeout.js +114 -0
- package/dist/core/a2a-task-state.d.ts +15 -0
- package/dist/core/a2a-task-state.js +68 -0
- package/dist/core/a2a.d.ts +42 -0
- package/dist/core/a2a.js +651 -0
- package/dist/core/checkpoint-store.d.ts +6 -1
- package/dist/core/checkpoint-store.js +3 -1
- package/dist/core/exec-output-tail.d.ts +0 -1
- package/dist/core/exec-output-tail.js +0 -6
- package/dist/core/mcp.d.ts +11 -4
- package/dist/core/mcp.js +11 -31
- package/dist/core/protocol-naming.d.ts +7 -0
- package/dist/core/protocol-naming.js +32 -0
- package/dist/core/protocol-table.d.ts +3 -1
- package/dist/core/protocol-table.js +37 -32
- package/dist/core/runner/prepare-task.d.ts +9 -0
- package/dist/core/runner/prepare-task.js +52 -15
- package/dist/core/runner/runtask.js +34 -4
- package/dist/core/runner/tool-disclosure.d.ts +1 -1
- package/dist/core/runner/tool-disclosure.js +1 -1
- package/dist/core/runner/turn-attachments.d.ts +1 -2
- package/dist/core/runner/turn-attachments.js +1 -12
- package/dist/core/store-contracts/background-agent-store-contract.d.ts +5 -0
- package/dist/core/store-contracts/background-agent-store-contract.js +213 -0
- package/dist/core/task-registry-agent.d.ts +14 -1
- package/dist/core/task-registry-agent.js +2 -2
- package/dist/core/types.d.ts +18 -2
- package/dist/engine/execution-env/node-execution-env.d.ts +0 -1
- package/dist/engine/execution-env/node-execution-env.js +12 -30
- package/dist/index.d.ts +8 -3
- package/dist/index.js +6 -2
- package/dist/prompt-assembly/event-registry.js +1 -0
- package/dist/tools/fs/fs-read.js +5 -3
- package/dist/tools/fs/fs-shared.d.ts +1 -0
- package/dist/tools/fs/fs-shared.js +1 -0
- package/dist/tools/web.js +12 -4
- package/package.json +1 -1
|
@@ -40,6 +40,7 @@ import { refuseOutOfContractDecision, toolPolicyNameSets } from "../tool-policy.
|
|
|
40
40
|
import { defaultTaskRegistry } from "../task-registry.js";
|
|
41
41
|
import { discloseDroppedPending, PendingSessionNotifications, renderTaskNotificationXml, SystemInjectionQueue, taskNotificationDedupKey } from "../task-notification.js";
|
|
42
42
|
import { ToolDetachHub } from "../tool-detach.js";
|
|
43
|
+
import { workflowSizeGuidelineChangeNotice } from "../../orchestration/workflow-size-guideline.js";
|
|
43
44
|
import { DEFAULT_MAX_TURNS } from "../../config/defaults.js";
|
|
44
45
|
import { structuredFrom, toolOutputFrom } from "./tool-output-projection.js";
|
|
45
46
|
export { DEFAULT_MAX_TURNS };
|
|
@@ -51,7 +52,7 @@ function createRunState() {
|
|
|
51
52
|
budget: { remainingMicroUsd: undefined, maxCostMicroUsd: undefined, overBudget: () => false, streamCancel: false, callOutputChars: 0, lastStreamBudgetCheck: 0, projectedOverBudget: () => false },
|
|
52
53
|
turn: { callStartAt: undefined, firstTokenAt: undefined, turnUsage: undefined, turnUsageMissing: false, turnStopReason: undefined, lastTurnHadToolCalls: false, toolBatch: [] },
|
|
53
54
|
counters: { nudgesSent: 0, nudgeIdx: 0, finalizeInjected: false, walltimeSyncBackstopFired: false, compactionFloor: 0, trimForceBackoff: false, callCutoffs: 0, repetitionCuts: 0, repetitionSpared: 0, repetitionEvents: [], REPETITION_EVENTS_CAP: 0, preemptIgnoredReported: false, wroteThisRun: false, finalVerifyInjections: 0, groundingSignalPreR9: false, groundingSignalPostR9: false, cadenceTurns: 0 },
|
|
54
|
-
attach: { attachmentsCfg: undefined, agentListingOn: false, skillsListingOn: false, attachState: undefined, dateState: undefined, instrProbe: undefined, instrState: undefined, attachmentsInjected: 0 },
|
|
55
|
+
attach: { attachmentsCfg: undefined, agentListingOn: false, skillsListingOn: false, attachState: undefined, dateState: undefined, instrProbe: undefined, instrState: undefined, sizeGuidelineState: undefined, attachmentsInjected: 0 },
|
|
55
56
|
};
|
|
56
57
|
}
|
|
57
58
|
const MAX_CONSECUTIVE_COMPACTION_FAILURES = 3;
|
|
@@ -314,7 +315,10 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
|
|
|
314
315
|
}
|
|
315
316
|
let boundaryAttachmentBytes = 0;
|
|
316
317
|
let attachmentsPayload;
|
|
317
|
-
if ((rs.attach.dateState !== undefined ||
|
|
318
|
+
if ((rs.attach.dateState !== undefined ||
|
|
319
|
+
rs.attach.instrState !== undefined ||
|
|
320
|
+
rs.attach.sizeGuidelineState !== undefined ||
|
|
321
|
+
rs.attach.attachState !== undefined) &&
|
|
318
322
|
!boundarySteered &&
|
|
319
323
|
!rs.counters.finalizeInjected &&
|
|
320
324
|
rs.counters.finalVerifyInjections === 0 &&
|
|
@@ -457,6 +461,16 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
|
|
|
457
461
|
rs.attach.dateState.announcedDate = today;
|
|
458
462
|
}
|
|
459
463
|
}
|
|
464
|
+
if (rs.attach.sizeGuidelineState !== undefined && rs.turn.lastTurnHadToolCalls) {
|
|
465
|
+
const guideline = rs.attach.sizeGuidelineState.current();
|
|
466
|
+
if (guideline !== rs.attach.sizeGuidelineState.announcedGuideline) {
|
|
467
|
+
due.splice(due.length > 0 && due[0].source === "date_change" ? 1 : 0, 0, {
|
|
468
|
+
source: "workflow_size_guideline_change",
|
|
469
|
+
body: workflowSizeGuidelineChangeNotice(guideline),
|
|
470
|
+
});
|
|
471
|
+
rs.attach.sizeGuidelineState.announcedGuideline = guideline;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
460
474
|
if (rs.attach.instrState !== undefined && rs.attach.instrProbe !== undefined && rs.turn.lastTurnHadToolCalls) {
|
|
461
475
|
let probed = null;
|
|
462
476
|
try {
|
|
@@ -1770,6 +1784,10 @@ export class Runner {
|
|
|
1770
1784
|
rs.attach.instrProbe !== undefined && prepared.instructionSources !== undefined && prepared.instructionSources.length > 0
|
|
1771
1785
|
? { lastAnnouncedHash: new Map(prepared.instructionSources.map((s) => [s.path, s.contentHash])) }
|
|
1772
1786
|
: undefined;
|
|
1787
|
+
rs.attach.sizeGuidelineState =
|
|
1788
|
+
prepared.workflowSizeGuideline !== undefined
|
|
1789
|
+
? { announcedGuideline: prepared.workflowSizeGuideline.legGuideline, current: prepared.workflowSizeGuideline.current }
|
|
1790
|
+
: undefined;
|
|
1773
1791
|
rs.counters.cadenceTurns = 0;
|
|
1774
1792
|
rs.turn.lastTurnHadToolCalls = false;
|
|
1775
1793
|
if (rs.attach.attachState !== undefined && rs.attach.attachmentsCfg?.backgroundTasks === true) {
|
|
@@ -2620,6 +2638,7 @@ export class Runner {
|
|
|
2620
2638
|
catch (err) {
|
|
2621
2639
|
if (errorCodeOf(err) === "resume.tool_unavailable") {
|
|
2622
2640
|
await settleTeardownLeg(() => prepared.mcp.dispose(), "mcp.dispose (tool_unavailable leg)", (e) => this.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
|
|
2641
|
+
await settleTeardownLeg(() => prepared.a2a?.dispose(), "a2a.dispose (tool_unavailable leg)", (e) => this.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
|
|
2623
2642
|
await settleTeardownLeg(() => (prepared.ownedEnv && hasDestroy(prepared.ownedEnv) ? prepared.ownedEnv.destroy() : undefined), "ownedEnv.destroy (tool_unavailable leg)", (e) => this.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
|
|
2624
2643
|
throw err;
|
|
2625
2644
|
}
|
|
@@ -2640,6 +2659,9 @@ export class Runner {
|
|
|
2640
2659
|
unsubBoundary();
|
|
2641
2660
|
unsub();
|
|
2642
2661
|
}
|
|
2662
|
+
if (threw === undefined && prepared.brainCallGuardrailRef.timedOut !== undefined) {
|
|
2663
|
+
threw = prepared.brainCallGuardrailRef.timedOut;
|
|
2664
|
+
}
|
|
2643
2665
|
const suspended = prepared.suspendRef.token !== undefined && prepared.suspendRef.gate !== undefined;
|
|
2644
2666
|
const reviewPaused = prepared.reviewRef.token !== undefined && prepared.reviewRef.gate !== undefined;
|
|
2645
2667
|
const durablyPaused = suspended || reviewPaused;
|
|
@@ -3198,12 +3220,12 @@ export class Runner {
|
|
|
3198
3220
|
if (outcome.gate === "policy_ask") {
|
|
3199
3221
|
const boundTo = cp.pendingAction.kind === "tool_approval" ? cp.pendingAction.toolCallId : undefined;
|
|
3200
3222
|
if (outcome.boundCallId !== boundTo) {
|
|
3201
|
-
throw new CheckpointError("checkpoint.invalid_outcome", `resume boundCallId "${outcome.boundCallId}" does not match the checkpoint's pending tool call "${boundTo ?? "(none)"}" — the decision-action binding (design/80 D-1) failed; refusing to apply a decision bound to a different action
|
|
3223
|
+
throw new CheckpointError("checkpoint.invalid_outcome", `resume boundCallId "${outcome.boundCallId}" does not match the checkpoint's pending tool call "${boundTo ?? "(none)"}" — the decision-action binding (design/80 D-1) failed; refusing to apply a decision bound to a different action`, { field: "boundCallId" });
|
|
3202
3224
|
}
|
|
3203
3225
|
const boundHash = cp.pendingAction.kind === "tool_approval" ? cp.pendingAction.boundInputHash : undefined;
|
|
3204
3226
|
if (boundHash !== undefined) {
|
|
3205
3227
|
if (outcome.boundInputHash !== boundHash) {
|
|
3206
|
-
throw new CheckpointError("checkpoint.invalid_outcome", "resume boundInputHash does not match the checkpoint's pending tool call input — the decision-action input binding (design/80 D-1 §2) failed; refusing to apply an approval bound to a different input (TOCTOU re-mint guard)");
|
|
3228
|
+
throw new CheckpointError("checkpoint.invalid_outcome", "resume boundInputHash does not match the checkpoint's pending tool call input — the decision-action input binding (design/80 D-1 §2) failed; refusing to apply an approval bound to a different input (TOCTOU re-mint guard)", { field: "boundInputHash" });
|
|
3207
3229
|
}
|
|
3208
3230
|
}
|
|
3209
3231
|
else if (checkpointVersionOf(cp) >= BINDING_CHECKPOINT_VERSION) {
|
|
@@ -3520,6 +3542,14 @@ export class Runner {
|
|
|
3520
3542
|
catch (err) {
|
|
3521
3543
|
this.deps.onError?.(err, { phase: "mcp", sessionId: prepared.sessionId });
|
|
3522
3544
|
}
|
|
3545
|
+
if (prepared.a2a !== undefined) {
|
|
3546
|
+
try {
|
|
3547
|
+
await prepared.a2a.dispose();
|
|
3548
|
+
}
|
|
3549
|
+
catch (err) {
|
|
3550
|
+
this.deps.onError?.(err, { phase: "a2a", sessionId: prepared.sessionId });
|
|
3551
|
+
}
|
|
3552
|
+
}
|
|
3523
3553
|
return comp;
|
|
3524
3554
|
}
|
|
3525
3555
|
async teardownOwnedEnv(prepared) {
|
|
@@ -14,7 +14,7 @@ export declare function deferHint(description: string, max?: number): string;
|
|
|
14
14
|
export declare function safeName(name: string): string;
|
|
15
15
|
export declare function classifyDeferred(opts: {
|
|
16
16
|
specs: ReadonlyArray<ToolSpec>;
|
|
17
|
-
|
|
17
|
+
protocolToolNames: ReadonlyArray<string>;
|
|
18
18
|
fullTools: ReadonlyArray<ToolFingerprintInput>;
|
|
19
19
|
deferMode?: "auto";
|
|
20
20
|
model: Model;
|
|
@@ -35,7 +35,7 @@ export function classifyDeferred(opts) {
|
|
|
35
35
|
if (s.defer === true && !pinned.has(s.name))
|
|
36
36
|
deferred.add(s.name);
|
|
37
37
|
}
|
|
38
|
-
for (const name of opts.
|
|
38
|
+
for (const name of opts.protocolToolNames)
|
|
39
39
|
if (!pinned.has(name))
|
|
40
40
|
deferred.add(name);
|
|
41
41
|
for (const name of opts.deferNames ?? [])
|
|
@@ -15,7 +15,7 @@ export declare const TOOL_SEARCH_REMINDER_CONFIG: {
|
|
|
15
15
|
export declare const CHANGED_FILES_MAX = 20;
|
|
16
16
|
export declare const CHANGED_FILES_MTIME_EPS_MS = 2000;
|
|
17
17
|
export declare const ATTACHMENT_BYTE_CAP: number;
|
|
18
|
-
export type AttachmentSource = "todo_reminder" | "task_reminder" | "tool_search_usage_reminder" | "changed_files" | "plan_mode" | "date_change" | "instructions_change" | "budget_usd" | "background_tasks" | "tools_delta" | "agent_listing" | "skills_listing" | "mcp_instructions" | "mcp_dropped_tools";
|
|
18
|
+
export type AttachmentSource = "todo_reminder" | "task_reminder" | "tool_search_usage_reminder" | "changed_files" | "plan_mode" | "date_change" | "instructions_change" | "workflow_size_guideline_change" | "budget_usd" | "background_tasks" | "tools_delta" | "agent_listing" | "skills_listing" | "mcp_instructions" | "mcp_dropped_tools";
|
|
19
19
|
export interface AgentListingEntry {
|
|
20
20
|
name: string;
|
|
21
21
|
description: string;
|
|
@@ -31,7 +31,6 @@ export interface TurnAttachment {
|
|
|
31
31
|
body: string;
|
|
32
32
|
}
|
|
33
33
|
export interface ListProjection {
|
|
34
|
-
statusCounts: Record<string, number>;
|
|
35
34
|
items: Array<{
|
|
36
35
|
content: string;
|
|
37
36
|
status: string;
|
|
@@ -47,12 +47,7 @@ export function reduceToolEnd(state, details) {
|
|
|
47
47
|
: [];
|
|
48
48
|
});
|
|
49
49
|
const allDone = shaped.length > 0 && shaped.every((t) => t.status === "completed");
|
|
50
|
-
state.todo = allDone
|
|
51
|
-
? { statusCounts: {}, items: [] }
|
|
52
|
-
: {
|
|
53
|
-
statusCounts: countStatuses(shaped),
|
|
54
|
-
items: shaped.slice(0, PROJECTION_ITEMS_MAX),
|
|
55
|
-
};
|
|
50
|
+
state.todo = allDone ? { items: [] } : { items: shaped.slice(0, PROJECTION_ITEMS_MAX) };
|
|
56
51
|
}
|
|
57
52
|
else if (d.type === "task") {
|
|
58
53
|
const map = state.taskItems ?? (state.taskItems = new Map());
|
|
@@ -120,12 +115,6 @@ export function rebaseCadenceWindows(state, clock) {
|
|
|
120
115
|
state.toolSearchLastUseTurn = clock;
|
|
121
116
|
state.lastToolSearchReminderTurn = clock;
|
|
122
117
|
}
|
|
123
|
-
function countStatuses(items) {
|
|
124
|
-
const counts = {};
|
|
125
|
-
for (const t of items)
|
|
126
|
-
counts[t.status] = (counts[t.status] ?? 0) + 1;
|
|
127
|
-
return counts;
|
|
128
|
-
}
|
|
129
118
|
const EMPTY = Object.freeze([]);
|
|
130
119
|
export function collectDueAttachments(state, inp) {
|
|
131
120
|
let out;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type BackgroundAgentStore } from "../background-agent-store.js";
|
|
2
|
+
import { type ContractAssertionRunner } from "./contract-harness.js";
|
|
3
|
+
export declare const BACKGROUND_AGENT_CONTRACT_SCOPE = "tenant-A";
|
|
4
|
+
export declare function backgroundAgentStoreContract(mk: () => BackgroundAgentStore, runAssertion?: ContractAssertionRunner): Promise<void>;
|
|
5
|
+
export declare function backgroundAgentStoreScopesContract(mk: () => BackgroundAgentStore, runAssertion?: ContractAssertionRunner): Promise<void>;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { strict as assert } from "node:assert";
|
|
2
|
+
import { STALE_RUNNING_REAP_ATTRIBUTION, } from "../background-agent-store.js";
|
|
3
|
+
import { beginContract } from "./contract-harness.js";
|
|
4
|
+
export const BACKGROUND_AGENT_CONTRACT_SCOPE = "tenant-A";
|
|
5
|
+
function record(over = {}) {
|
|
6
|
+
return {
|
|
7
|
+
handle: "a0000000000000001",
|
|
8
|
+
scope: BACKGROUND_AGENT_CONTRACT_SCOPE,
|
|
9
|
+
owner: "task-1",
|
|
10
|
+
sessionScoped: false,
|
|
11
|
+
parentSessionId: "sess-1",
|
|
12
|
+
writerId: "writer-1",
|
|
13
|
+
spawnedAt: 1_000,
|
|
14
|
+
updatedAt: 1_000,
|
|
15
|
+
status: "running",
|
|
16
|
+
rev: 0,
|
|
17
|
+
...over,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
async function withStores(mk, fn) {
|
|
21
|
+
const live = [];
|
|
22
|
+
const make = () => {
|
|
23
|
+
const s = mk();
|
|
24
|
+
live.push(s);
|
|
25
|
+
return s;
|
|
26
|
+
};
|
|
27
|
+
try {
|
|
28
|
+
await fn(make);
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
for (const s of live.splice(0))
|
|
32
|
+
s.close?.();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function held(store, handle, scope = BACKGROUND_AGENT_CONTRACT_SCOPE) {
|
|
36
|
+
const row = await store.get(handle, scope);
|
|
37
|
+
assert.notEqual(row, null, `row ${handle} should exist in scope ${scope}`);
|
|
38
|
+
return row;
|
|
39
|
+
}
|
|
40
|
+
export async function backgroundAgentStoreContract(mk, runAssertion) {
|
|
41
|
+
const { run: runRaw, settle } = beginContract(runAssertion);
|
|
42
|
+
const run = (name, fn) => runRaw(name, () => withStores(mk, fn));
|
|
43
|
+
const S = BACKGROUND_AGENT_CONTRACT_SCOPE;
|
|
44
|
+
run("put is create-once and scope-partitioned; get round-trips the row and is null cross-scope", async (make) => {
|
|
45
|
+
const store = make();
|
|
46
|
+
await store.put(record({ handle: "a1", finalOutput: "result-text" }));
|
|
47
|
+
await assert.rejects(store.put(record({ handle: "a1" })), (e) => e.code === "agent_record.already_exists", "a second put under the same (handle, scope) must throw the typed already_exists error");
|
|
48
|
+
await store.put(record({ handle: "a1", scope: "tenant-B" }));
|
|
49
|
+
const row = await held(store, "a1");
|
|
50
|
+
assert.equal(row.finalOutput, "result-text");
|
|
51
|
+
assert.equal(row.owner, "task-1");
|
|
52
|
+
assert.equal(await store.get("a1", "tenant-C"), null, "an unrelated scope must not see the row");
|
|
53
|
+
assert.equal(await store.get("a-missing", S), null);
|
|
54
|
+
});
|
|
55
|
+
run("put refuses a scopeless row (a row with no tenant partition would be world-readable)", async (make) => {
|
|
56
|
+
const store = make();
|
|
57
|
+
await assert.rejects(store.put(record({ scope: "" })), "an empty scope must be refused loudly, not stored");
|
|
58
|
+
});
|
|
59
|
+
run("get returns DETACHED copies: mutating a read never reaches the stored row", async (make) => {
|
|
60
|
+
const store = make();
|
|
61
|
+
await store.put(record({ handle: "a1", usage: { totalTokens: 5 } }));
|
|
62
|
+
const first = await held(store, "a1");
|
|
63
|
+
first.status = "failed";
|
|
64
|
+
first.usage.totalTokens = 999;
|
|
65
|
+
const second = await held(store, "a1");
|
|
66
|
+
assert.equal(second.status, "running");
|
|
67
|
+
assert.equal(second.usage?.totalTokens, 5);
|
|
68
|
+
});
|
|
69
|
+
run("update is a rev-CAS: stale rev loses, wrong scope loses, the winner bumps rev by EXACTLY 1", async (make) => {
|
|
70
|
+
const store = make();
|
|
71
|
+
await store.put(record({ handle: "a1" }));
|
|
72
|
+
const row = await held(store, "a1");
|
|
73
|
+
assert.equal(await store.update("a1", "tenant-B", { ...row, status: "completed" }, { rev: row.rev }), false, "wrong scope");
|
|
74
|
+
assert.equal(await store.update("a1", S, { ...row, status: "completed" }, { rev: row.rev + 5 }), false, "rev ahead");
|
|
75
|
+
assert.equal(await store.update("a-missing", S, { ...row, status: "completed" }, { rev: row.rev }), false, "missing row");
|
|
76
|
+
assert.equal(await store.update("a1", S, { ...row, status: "completed" }, { rev: row.rev }), true);
|
|
77
|
+
const after = await held(store, "a1");
|
|
78
|
+
assert.equal(after.status, "completed");
|
|
79
|
+
assert.equal(after.rev, row.rev + 1);
|
|
80
|
+
assert.equal(await store.update("a1", S, { ...row, status: "failed" }, { rev: row.rev }), false);
|
|
81
|
+
assert.equal((await held(store, "a1")).status, "completed");
|
|
82
|
+
});
|
|
83
|
+
run("update keeps the KEY authoritative: a payload naming another handle/scope cannot repartition the row", async (make) => {
|
|
84
|
+
const store = make();
|
|
85
|
+
await store.put(record({ handle: "a1" }));
|
|
86
|
+
const row = await held(store, "a1");
|
|
87
|
+
assert.equal(await store.update("a1", S, { ...row, handle: "a-other", scope: "tenant-B", status: "failed" }, { rev: row.rev }), true);
|
|
88
|
+
const after = await held(store, "a1");
|
|
89
|
+
assert.equal(after.handle, "a1", "the key's handle wins over the payload's");
|
|
90
|
+
assert.equal(after.scope, S, "scope is the partition key and an update must never rewrite it");
|
|
91
|
+
assert.equal(await store.get("a-other", "tenant-B"), null, "no second row may appear");
|
|
92
|
+
});
|
|
93
|
+
run("updateIf keeps the rev guard AND adds the field guards (status / checkpoint token / claim id)", async (make) => {
|
|
94
|
+
const store = make();
|
|
95
|
+
await store.put(record({ handle: "a1", status: "parked", parkedCheckpointToken: "cp-1" }));
|
|
96
|
+
const row = await held(store, "a1");
|
|
97
|
+
assert.equal(await store.updateIf("a1", S, { ...row, status: "running" }, { rev: row.rev + 3 }), false, "stale rev");
|
|
98
|
+
assert.equal(await store.updateIf("a1", S, { ...row, status: "running" }, { rev: row.rev, status: "running" }), false, "status mismatch");
|
|
99
|
+
assert.equal(await store.updateIf("a1", S, { ...row, status: "running" }, { rev: row.rev, status: "parked", parkedCheckpointToken: "cp-OTHER" }), false, "checkpoint token mismatch");
|
|
100
|
+
assert.equal(await store.updateIf("a1", S, { ...row, status: "running" }, { rev: row.rev, parkedCheckpointToken: null }), false, "null must mean 'this field is absent', and it is present");
|
|
101
|
+
assert.equal(await store.updateIf("a1", S, { ...row, status: "running" }, { rev: row.rev, parkClaimId: "claim-1" }), false, "claim id expected but absent");
|
|
102
|
+
const next = { ...row, status: "running", parkClaimId: "claim-1" };
|
|
103
|
+
assert.equal(await store.updateIf("a1", S, next, { rev: row.rev, status: "parked", parkedCheckpointToken: "cp-1", parkClaimId: null }), true, "matching guards (incl. null-as-absent on the claim id) must win");
|
|
104
|
+
const after = await held(store, "a1");
|
|
105
|
+
assert.equal(after.status, "running");
|
|
106
|
+
assert.equal(after.parkClaimId, "claim-1");
|
|
107
|
+
assert.equal(after.rev, row.rev + 1);
|
|
108
|
+
});
|
|
109
|
+
run("listBySession anchors on (sessionScoped ? owner : parentSessionId) OR rootSessionId, newest-first", async (make) => {
|
|
110
|
+
const store = make();
|
|
111
|
+
await store.put(record({ handle: "a1", spawnedAt: 1_000, finalOutput: "SECRET", summary: "s", error: "e", recentSteps: [] }));
|
|
112
|
+
await store.put(record({ handle: "a2", owner: "sess-1", sessionScoped: true, parentSessionId: undefined, spawnedAt: 2_000 }));
|
|
113
|
+
await store.put(record({ handle: "a3", parentSessionId: "sess-mid", rootSessionId: "sess-1", spawnedAt: 3_000 }));
|
|
114
|
+
await store.put(record({ handle: "a4", parentSessionId: "sess-other", spawnedAt: 4_000 }));
|
|
115
|
+
const rows = await store.listBySession(S, "sess-1");
|
|
116
|
+
assert.deepEqual(rows.map((r) => r.handle), ["a3", "a2", "a1"], "newest-first by spawnedAt");
|
|
117
|
+
for (const row of rows) {
|
|
118
|
+
for (const field of ["finalOutput", "summary", "error", "recentSteps", "editedFiles"]) {
|
|
119
|
+
assert.equal(Object.hasOwn(row, field), false, `list projections must not carry ${field}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
assert.deepEqual((await store.listBySession(S, "sess-1", { limit: 2 })).map((r) => r.handle), ["a3", "a2"], "limit keeps the NEWEST n");
|
|
123
|
+
assert.deepEqual(await store.listBySession("tenant-B", "sess-1"), [], "another tenant sees nothing");
|
|
124
|
+
assert.deepEqual((await store.listBySession(S, "sess-mid")).map((r) => r.handle), ["a3"], "the intermediate anchor still lists its direct child");
|
|
125
|
+
});
|
|
126
|
+
run("listBySession/listByScope filter one status; listByScope enumerates the whole tenant", async (make) => {
|
|
127
|
+
const store = make();
|
|
128
|
+
await store.put(record({ handle: "a1", status: "running", spawnedAt: 1_000 }));
|
|
129
|
+
await store.put(record({ handle: "a2", status: "completed", spawnedAt: 2_000 }));
|
|
130
|
+
await store.put(record({ handle: "a3", status: "completed", parentSessionId: "sess-other", spawnedAt: 3_000 }));
|
|
131
|
+
await store.put(record({ handle: "a4", status: "completed", scope: "tenant-B", spawnedAt: 4_000 }));
|
|
132
|
+
assert.deepEqual((await store.listByScope(S)).map((r) => r.handle), ["a3", "a2", "a1"], "scope-wide, newest-first");
|
|
133
|
+
assert.deepEqual((await store.listByScope(S, { status: "completed" })).map((r) => r.handle), ["a3", "a2"]);
|
|
134
|
+
assert.deepEqual((await store.listByScope(S, { status: "completed", limit: 1 })).map((r) => r.handle), ["a3"]);
|
|
135
|
+
assert.deepEqual((await store.listBySession(S, "sess-1", { status: "completed" })).map((r) => r.handle), ["a2"]);
|
|
136
|
+
assert.deepEqual((await store.listByScope("tenant-B")).map((r) => r.handle), ["a4"], "tenants are separate ledgers");
|
|
137
|
+
});
|
|
138
|
+
run("delete is row-precise and CONDITIONAL when a rev is supplied (a concurrent revival must win)", async (make) => {
|
|
139
|
+
const store = make();
|
|
140
|
+
await store.put(record({ handle: "a1", status: "completed" }));
|
|
141
|
+
await store.put(record({ handle: "a2", status: "completed" }));
|
|
142
|
+
const row = await held(store, "a1");
|
|
143
|
+
assert.equal(await store.delete("a1", S, { rev: row.rev + 1 }), false, "a stale rev must not delete");
|
|
144
|
+
assert.notEqual(await store.get("a1", S), null);
|
|
145
|
+
assert.equal(await store.delete("a1", "tenant-B"), false, "cross-scope delete is a miss");
|
|
146
|
+
assert.equal(await store.delete("a1", S, { rev: row.rev }), true);
|
|
147
|
+
assert.equal(await store.get("a1", S), null);
|
|
148
|
+
assert.equal(await store.delete("a1", S), false, "deleting a gone row reports false, not true");
|
|
149
|
+
assert.equal(await store.delete("a2", S), true, "an unconditional delete still works");
|
|
150
|
+
});
|
|
151
|
+
run("reap with NO policy is a no-op (retention is always explicit)", async (make) => {
|
|
152
|
+
const store = make();
|
|
153
|
+
await store.put(record({ handle: "a1", status: "completed", settledAt: 1_000 }));
|
|
154
|
+
assert.equal(await store.reap(S, 100_000), 0);
|
|
155
|
+
assert.equal(await store.reap(S, 100_000, {}), 0);
|
|
156
|
+
assert.notEqual(await store.get("a1", S), null);
|
|
157
|
+
});
|
|
158
|
+
run("reap deletes terminal rows by age and by keep-bound; running and parked rows survive BOTH", async (make) => {
|
|
159
|
+
const store = make();
|
|
160
|
+
await store.put(record({ handle: "a1", status: "completed", spawnedAt: 1_000, settledAt: 1_000 }));
|
|
161
|
+
await store.put(record({ handle: "a2", status: "running", spawnedAt: 1_000, updatedAt: 1_000 }));
|
|
162
|
+
await store.put(record({ handle: "a3", status: "parked", spawnedAt: 1_000, updatedAt: 1_000, parkedCheckpointToken: "cp-1" }));
|
|
163
|
+
assert.equal(await store.reap(S, 100_000, { maxAgeMs: 10 }), 1, "only the terminal row ages out");
|
|
164
|
+
assert.equal(await store.get("a1", S), null);
|
|
165
|
+
assert.equal((await held(store, "a2")).status, "running");
|
|
166
|
+
assert.equal((await held(store, "a3")).status, "parked");
|
|
167
|
+
for (const i of [1, 2, 3])
|
|
168
|
+
await store.put(record({ handle: `b${i}`, status: "completed", spawnedAt: 1_000 + i, settledAt: 2_000 + i }));
|
|
169
|
+
assert.equal(await store.reap(S, 100_000, { keep: 2 }), 1);
|
|
170
|
+
assert.deepEqual((await store.listByScope(S, { status: "completed" })).map((r) => r.handle), ["b3", "b2"]);
|
|
171
|
+
assert.notEqual(await store.get("a2", S), null);
|
|
172
|
+
assert.notEqual(await store.get("a3", S), null);
|
|
173
|
+
});
|
|
174
|
+
run("reap's stale-running arm flips to a terminal `failed` WITH the shared attribution, and never overwrites real failure text", async (make) => {
|
|
175
|
+
const store = make();
|
|
176
|
+
await store.put(record({ handle: "a1", status: "running", updatedAt: 1_000 }));
|
|
177
|
+
await store.put(record({ handle: "a2", status: "running", updatedAt: 1_000, error: "the child reported a real failure" }));
|
|
178
|
+
assert.equal(await store.reap(S, 100_000, { staleRunningMaxAgeMs: 50_000 }), 2);
|
|
179
|
+
const flipped = await held(store, "a1");
|
|
180
|
+
assert.equal(flipped.status, "failed", "an interrupted host's row settles as a terminal, not a perpetual running");
|
|
181
|
+
assert.equal(flipped.stoppedBy, "system");
|
|
182
|
+
assert.equal(flipped.settledAt, 100_000);
|
|
183
|
+
assert.equal(flipped.updatedAt, 100_000);
|
|
184
|
+
assert.equal(flipped.error, STALE_RUNNING_REAP_ATTRIBUTION);
|
|
185
|
+
assert.equal(flipped.summary, STALE_RUNNING_REAP_ATTRIBUTION);
|
|
186
|
+
assert.equal((await held(store, "a2")).error, "the child reported a real failure");
|
|
187
|
+
await store.put(record({ handle: "a3", status: "running", updatedAt: 99_000 }));
|
|
188
|
+
assert.equal(await store.reap(S, 100_000, { staleRunningMaxAgeMs: 50_000 }), 0);
|
|
189
|
+
assert.equal((await held(store, "a3")).status, "running");
|
|
190
|
+
});
|
|
191
|
+
run("reap counts each ROW once even when the same sweep flips it AND removes it", async (make) => {
|
|
192
|
+
const store = make();
|
|
193
|
+
await store.put(record({ handle: "a1", status: "running", spawnedAt: 1_000, updatedAt: 1_000 }));
|
|
194
|
+
assert.equal(await store.reap(S, 100_000, { staleRunningMaxAgeMs: 50_000, keep: 0 }), 1, "one row changed, so the count is 1");
|
|
195
|
+
assert.equal(await store.get("a1", S), null, "and it really is gone (flipped, then trimmed)");
|
|
196
|
+
});
|
|
197
|
+
await settle();
|
|
198
|
+
}
|
|
199
|
+
export async function backgroundAgentStoreScopesContract(mk, runAssertion) {
|
|
200
|
+
const { run: runRaw, settle } = beginContract(runAssertion);
|
|
201
|
+
const run = (name, fn) => runRaw(name, () => withStores(mk, fn));
|
|
202
|
+
run("listScopes enumerates the scopes that currently HOLD rows, deduplicated and sorted", async (make) => {
|
|
203
|
+
const store = make();
|
|
204
|
+
assert.equal(typeof store.listScopes, "function", "this layer is only for backends that implement listScopes");
|
|
205
|
+
await store.put(record({ handle: "a1", scope: "tenant-B" }));
|
|
206
|
+
await store.put(record({ handle: "a2", scope: "tenant-A" }));
|
|
207
|
+
await store.put(record({ handle: "a3", scope: "tenant-A" }));
|
|
208
|
+
assert.deepEqual(await store.listScopes(), ["tenant-A", "tenant-B"], "deduplicated and sorted — a retention loop drives itself off this");
|
|
209
|
+
await store.delete("a1", "tenant-B");
|
|
210
|
+
assert.deepEqual(await store.listScopes(), ["tenant-A"]);
|
|
211
|
+
});
|
|
212
|
+
await settle();
|
|
213
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type BackgroundAgentRecord, type BackgroundAgentStore } from "./background-agent-store.js";
|
|
2
|
-
import { type StopSource, type TaskAccess, type UnifiedTaskResult, type BackgroundAgentTaskHandle, type DurableAgentCore, type ParkedClaimTicket, type RegisterBackgroundAgentInput } from "./task-registry-shared.js";
|
|
2
|
+
import { type StopSource, type TaskAccess, type TaskRetrievalStatus, type UnifiedTaskOutput, type UnifiedTaskResult, type BackgroundAgentTaskHandle, type DurableAgentCore, type ParkedClaimTicket, type RegisterBackgroundAgentInput } from "./task-registry-shared.js";
|
|
3
3
|
import { type ToolResultStore } from "./tool-result-store.js";
|
|
4
4
|
export declare function ensureDurableHeartbeatLane(core: DurableAgentCore): void;
|
|
5
5
|
export declare function durableAgentWriteLane(handle: BackgroundAgentTaskHandle, patch: Partial<BackgroundAgentRecord>, clear?: readonly (keyof BackgroundAgentRecord)[]): void;
|
|
@@ -126,6 +126,19 @@ export declare function notFoundRunningAgentsTail(footer: {
|
|
|
126
126
|
named: string[];
|
|
127
127
|
background: string[];
|
|
128
128
|
}): string;
|
|
129
|
+
export interface AgentPollDetailsInput {
|
|
130
|
+
taskId: string;
|
|
131
|
+
status: UnifiedTaskOutput["status"];
|
|
132
|
+
retrievalStatus: TaskRetrievalStatus;
|
|
133
|
+
seq?: number;
|
|
134
|
+
stoppedBy?: StopSource;
|
|
135
|
+
error?: string;
|
|
136
|
+
errorCode?: string;
|
|
137
|
+
errorRetryable?: boolean;
|
|
138
|
+
resultIsPartial?: boolean;
|
|
139
|
+
completionId?: string;
|
|
140
|
+
}
|
|
141
|
+
export declare function buildAgentPollDetails(input: AgentPollDetailsInput): UnifiedTaskOutput;
|
|
129
142
|
export declare function serveDurableAgentRowLane(row: BackgroundAgentRecord): UnifiedTaskResult;
|
|
130
143
|
export declare function spillClippedAgentResult(handle: BackgroundAgentTaskHandle, full: string, clipped: string, store: ToolResultStore | undefined, sessionId: string | undefined): Promise<string>;
|
|
131
144
|
export declare function pollBackgroundAgentLane(handle: BackgroundAgentTaskHandle, deadline?: number, signal?: AbortSignal, oneShot?: boolean, store?: ToolResultStore, sessionId?: string): Promise<UnifiedTaskResult>;
|
|
@@ -1008,7 +1008,7 @@ export function notFoundRunningAgentsTail(footer) {
|
|
|
1008
1008
|
return ((footer.named.length > 0 ? `. Running named agents: ${footer.named.join(", ")}` : "") +
|
|
1009
1009
|
(footer.background.length > 0 ? `. Running background agents: ${footer.background.join(", ")}` : ""));
|
|
1010
1010
|
}
|
|
1011
|
-
function buildAgentPollDetails(input) {
|
|
1011
|
+
export function buildAgentPollDetails(input) {
|
|
1012
1012
|
const failed = input.status === "failed";
|
|
1013
1013
|
return {
|
|
1014
1014
|
task_id: input.taskId,
|
|
@@ -1016,7 +1016,7 @@ function buildAgentPollDetails(input) {
|
|
|
1016
1016
|
status: input.status,
|
|
1017
1017
|
retrieval_status: input.retrievalStatus,
|
|
1018
1018
|
...(input.seq !== undefined ? { seq: input.seq } : {}),
|
|
1019
|
-
...(input.
|
|
1019
|
+
...(input.stoppedBy !== undefined ? { stoppedBy: input.stoppedBy } : {}),
|
|
1020
1020
|
...(failed && input.error !== undefined ? { error: delimitUntrusted("agent error", boundedRedactedSummary(input.error, 300)) } : {}),
|
|
1021
1021
|
...(failed && input.errorCode !== undefined ? { errorCode: input.errorCode } : {}),
|
|
1022
1022
|
...(failed && input.errorRetryable !== undefined ? { retryable: input.errorRetryable } : {}),
|
package/dist/core/types.d.ts
CHANGED
|
@@ -197,6 +197,19 @@ export interface McpServerSpec {
|
|
|
197
197
|
irreversibility?: "always" | "never";
|
|
198
198
|
}>;
|
|
199
199
|
}
|
|
200
|
+
export interface A2aServerSpec {
|
|
201
|
+
name: string;
|
|
202
|
+
url: string;
|
|
203
|
+
cardUrl?: string;
|
|
204
|
+
headers?: Record<string, string>;
|
|
205
|
+
principalHeader?: string;
|
|
206
|
+
allowSkills?: string[];
|
|
207
|
+
toolAxes?: Record<string, {
|
|
208
|
+
effect?: ToolEffect;
|
|
209
|
+
egress?: boolean;
|
|
210
|
+
irreversibility?: "always" | "never";
|
|
211
|
+
}>;
|
|
212
|
+
}
|
|
200
213
|
export interface McpElicitRequest {
|
|
201
214
|
server: string;
|
|
202
215
|
message: string;
|
|
@@ -296,6 +309,7 @@ export interface TaskSpec {
|
|
|
296
309
|
enableFork?: boolean;
|
|
297
310
|
shellGate?: "off" | "always" | "classify";
|
|
298
311
|
mcp?: McpServerSpec[];
|
|
312
|
+
a2a?: A2aServerSpec[];
|
|
299
313
|
skills?: SkillSpec[];
|
|
300
314
|
backgroundScope?: "task" | "session";
|
|
301
315
|
envFacts?: {
|
|
@@ -319,6 +333,7 @@ export interface TaskSpec {
|
|
|
319
333
|
};
|
|
320
334
|
callCapByDeadline?: false;
|
|
321
335
|
gracefulFinalize?: false;
|
|
336
|
+
brainCallGuardrailMs?: import("../brain/timeout.js").BrainCallGuardrailKnob;
|
|
322
337
|
};
|
|
323
338
|
configOverrides?: ReadonlyArray<import("../config/catalog.js").ConfigOverrideDeclaration>;
|
|
324
339
|
forwardSubagentEvents?: boolean;
|
|
@@ -562,7 +577,7 @@ export type TaskEvent = ({
|
|
|
562
577
|
reason?: string;
|
|
563
578
|
} & TaskEventIdentity) | ({
|
|
564
579
|
type: "steering_injected";
|
|
565
|
-
source: "deadline_nudge" | "finalize" | "todo_reminder" | "task_reminder" | "tool_search_usage_reminder" | "changed_files" | "plan_mode" | "date_change" | "instructions_change" | "budget_usd" | "background_tasks" | "tools_delta" | "agent_listing" | "skills_listing" | "mcp_instructions" | "mcp_dropped_tools";
|
|
580
|
+
source: "deadline_nudge" | "finalize" | "todo_reminder" | "task_reminder" | "tool_search_usage_reminder" | "changed_files" | "plan_mode" | "date_change" | "instructions_change" | "workflow_size_guideline_change" | "budget_usd" | "background_tasks" | "tools_delta" | "agent_listing" | "skills_listing" | "mcp_instructions" | "mcp_dropped_tools";
|
|
566
581
|
preview: string;
|
|
567
582
|
} & TaskEventIdentity) | ({
|
|
568
583
|
type: "diagnostics";
|
|
@@ -740,6 +755,7 @@ export interface RunnerDeps {
|
|
|
740
755
|
builtinWorkflows?: boolean;
|
|
741
756
|
onWorkflowAgentSpawn?: (handle: import("../orchestration/workflow.js").WorkflowAgentHandle) => void;
|
|
742
757
|
maxSuspends?: number;
|
|
758
|
+
brainCallGuardrailMs?: import("../brain/timeout.js").BrainCallGuardrailKnob;
|
|
743
759
|
now?: () => number;
|
|
744
760
|
memoryBackend?: import("./memory-engine/types.js").MemoryBackend;
|
|
745
761
|
memoryEngineDir?: string;
|
|
@@ -772,7 +788,7 @@ export interface RunnerDeps {
|
|
|
772
788
|
hooks?: import("./hooks.js").Hooks;
|
|
773
789
|
allowImageUrl?: (url: string) => boolean;
|
|
774
790
|
onError?: (err: unknown, context: {
|
|
775
|
-
phase: "compaction" | "prompt-cache" | "prompt-constitution" | "degraded" | "config" | "memory" | "mcp" | "interrupt-reconcile" | "suggestions" | "rewind" | "hook";
|
|
791
|
+
phase: "compaction" | "prompt-cache" | "prompt-constitution" | "degraded" | "config" | "memory" | "mcp" | "a2a" | "interrupt-reconcile" | "suggestions" | "rewind" | "hook";
|
|
776
792
|
sessionId: string;
|
|
777
793
|
classification?: string;
|
|
778
794
|
}) => void;
|
|
@@ -8,7 +8,7 @@ import { createInterface } from "node:readline";
|
|
|
8
8
|
import { ExecutionError, err, FileError, ok, toError, } from "../harness/types.js";
|
|
9
9
|
import { killProcessTree, shutdownDebug } from "./kill-tree.js";
|
|
10
10
|
import { scrubSecretEnv } from "../../core/secret-env.js";
|
|
11
|
-
import { RollingTailBuffer,
|
|
11
|
+
import { RollingTailBuffer, markTruncated, newStreamCursorState, sliceStreamIncrement, } from "../../core/exec-output-tail.js";
|
|
12
12
|
import { BackgroundShellError } from "../../core/background-shell.js";
|
|
13
13
|
import { SchedulerError } from "../../core/scheduler.js";
|
|
14
14
|
const MAX_TIMER_TIMEOUT_MS = 2_147_000_000;
|
|
@@ -216,18 +216,14 @@ const EXEC_SPOOL_HARD_CAP_BYTES = 16 * 1024 * 1024;
|
|
|
216
216
|
export function decideSpoolReclaim(args) {
|
|
217
217
|
const rotate = args.rotateBytes ?? EXEC_SPOOL_ROTATE_BYTES;
|
|
218
218
|
const hardCap = args.hardCapBytes ?? EXEC_SPOOL_HARD_CAP_BYTES;
|
|
219
|
-
if (args.
|
|
219
|
+
if (args.writerLive)
|
|
220
220
|
return { reclaim: false };
|
|
221
|
-
if (args.
|
|
222
|
-
if (!args.writerLive)
|
|
223
|
-
return { reclaim: true, live: false, skippedBytes: 0 };
|
|
224
|
-
if (args.size >= hardCap)
|
|
225
|
-
return { reclaim: true, live: true, skippedBytes: 0 };
|
|
221
|
+
if (args.cursor < rotate)
|
|
226
222
|
return { reclaim: false };
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
223
|
+
if (args.size === args.cursor)
|
|
224
|
+
return { reclaim: true, skippedBytes: 0 };
|
|
225
|
+
if (args.size > args.cursor && args.size >= hardCap)
|
|
226
|
+
return { reclaim: true, skippedBytes: args.size - args.cursor };
|
|
231
227
|
return { reclaim: false };
|
|
232
228
|
}
|
|
233
229
|
const SIGNUM = { SIGHUP: 1, SIGINT: 2, SIGQUIT: 3, SIGABRT: 6, SIGKILL: 9, SIGSEGV: 11, SIGPIPE: 13, SIGTERM: 15 };
|
|
@@ -330,8 +326,6 @@ export class NodeExecutionEnv {
|
|
|
330
326
|
const adoptOnTimeoutRef = {};
|
|
331
327
|
const pumpSpoolRef = {};
|
|
332
328
|
const drainSpoolRef = {};
|
|
333
|
-
const liveRotations = { out: 0, err: 0 };
|
|
334
|
-
const liveRotationsTotal = () => liveRotations.out + liveRotations.err;
|
|
335
329
|
const writerLive = () => !settled && child !== undefined && child.exitCode === null && child.signalCode === null;
|
|
336
330
|
const armForceSettle = (result) => {
|
|
337
331
|
if (forceSettleRef.current || settled)
|
|
@@ -351,7 +345,7 @@ export class NodeExecutionEnv {
|
|
|
351
345
|
drainSpoolRef.current?.();
|
|
352
346
|
const so = stdoutTail.result();
|
|
353
347
|
const se = stderrTail.result();
|
|
354
|
-
return { stdout: markTruncated(so.text, so.droppedBytes), stderr:
|
|
348
|
+
return { stdout: markTruncated(so.text, so.droppedBytes), stderr: markTruncated(se.text, se.droppedBytes) };
|
|
355
349
|
};
|
|
356
350
|
const onAbort = () => {
|
|
357
351
|
abortKillInFlight = true;
|
|
@@ -545,8 +539,6 @@ export class NodeExecutionEnv {
|
|
|
545
539
|
return;
|
|
546
540
|
if (d.skippedBytes > 0)
|
|
547
541
|
lane.tail.recordSkippedBytes(d.skippedBytes);
|
|
548
|
-
if (d.live)
|
|
549
|
-
liveRotations[lane.cursor]++;
|
|
550
542
|
truncateSync(lane.path, 0);
|
|
551
543
|
spoolCursor[lane.cursor] = 0;
|
|
552
544
|
}
|
|
@@ -583,9 +575,7 @@ export class NodeExecutionEnv {
|
|
|
583
575
|
if (settled || !child)
|
|
584
576
|
return false;
|
|
585
577
|
pumpSpoolRef.current?.();
|
|
586
|
-
const shellId = this.adoptRunningChild(child, stdoutTail.result(), stderrTail.result(), execSpoolRef.current !== undefined
|
|
587
|
-
? { spool: execSpoolRef.current, cursors: { out: spoolCursor.out, err: spoolCursor.err }, liveRotations: liveRotationsTotal() }
|
|
588
|
-
: undefined);
|
|
578
|
+
const shellId = this.adoptRunningChild(child, stdoutTail.result(), stderrTail.result(), execSpoolRef.current !== undefined ? { spool: execSpoolRef.current, cursors: { out: spoolCursor.out, err: spoolCursor.err } } : undefined);
|
|
589
579
|
if (shellId === undefined)
|
|
590
580
|
return false;
|
|
591
581
|
if (spoolPumpRef.current)
|
|
@@ -678,12 +668,12 @@ export class NodeExecutionEnv {
|
|
|
678
668
|
const signum = { SIGHUP: 1, SIGINT: 2, SIGQUIT: 3, SIGABRT: 6, SIGKILL: 9, SIGSEGV: 11, SIGPIPE: 13, SIGTERM: 15 };
|
|
679
669
|
settle(ok({
|
|
680
670
|
stdout: markTruncated(so.text, so.droppedBytes),
|
|
681
|
-
stderr:
|
|
671
|
+
stderr: `${markTruncated(se.text, se.droppedBytes)}${se.text.endsWith("\n") || se.text === "" ? "" : "\n"}[process terminated by ${signal}]`,
|
|
682
672
|
exitCode: 128 + (signum[signal] ?? 9),
|
|
683
673
|
}));
|
|
684
674
|
return;
|
|
685
675
|
}
|
|
686
|
-
settle(ok({ stdout: markTruncated(so.text, so.droppedBytes), stderr:
|
|
676
|
+
settle(ok({ stdout: markTruncated(so.text, so.droppedBytes), stderr: markTruncated(se.text, se.droppedBytes), exitCode: code ?? 0 }));
|
|
687
677
|
};
|
|
688
678
|
child.on("close", (code, signal) => settleFromTermination(code, signal));
|
|
689
679
|
child.on("exit", (code, signal) => {
|
|
@@ -1051,7 +1041,6 @@ export class NodeExecutionEnv {
|
|
|
1051
1041
|
...(handover !== undefined
|
|
1052
1042
|
? { spool: { outPath: handover.spool.outPath, errPath: handover.spool.errPath, fileCursor: { ...handover.cursors }, ephemeral: true } }
|
|
1053
1043
|
: {}),
|
|
1054
|
-
...(handover !== undefined && (handover.liveRotations ?? 0) > 0 ? { liveRotations: handover.liveRotations } : {}),
|
|
1055
1044
|
};
|
|
1056
1045
|
const seedOut = Buffer.from(stdoutSoFar.text, "utf8");
|
|
1057
1046
|
const seedErr = Buffer.from(stderrSoFar.text, "utf8");
|
|
@@ -1241,14 +1230,9 @@ export class NodeExecutionEnv {
|
|
|
1241
1230
|
const out = sliceStreamIncrement(entry.stdout, terminal);
|
|
1242
1231
|
const er = sliceStreamIncrement(entry.stderr, terminal);
|
|
1243
1232
|
const droppedBeforeCursor = out.droppedBeforeCursor + er.droppedBeforeCursor;
|
|
1244
|
-
let stderrInc = er.inc;
|
|
1245
|
-
if (terminal && (entry.liveRotations ?? 0) > 0 && entry.liveRotationsDisclosed !== true) {
|
|
1246
|
-
entry.liveRotationsDisclosed = true;
|
|
1247
|
-
stderrInc = markLiveSpoolRotations(er.inc, entry.liveRotations ?? 0);
|
|
1248
|
-
}
|
|
1249
1233
|
return ok({
|
|
1250
1234
|
stdout: out.inc,
|
|
1251
|
-
stderr:
|
|
1235
|
+
stderr: er.inc,
|
|
1252
1236
|
status: entry.status,
|
|
1253
1237
|
...(entry.exitCode !== undefined ? { exitCode: entry.exitCode } : {}),
|
|
1254
1238
|
...(droppedBeforeCursor > 0 ? { truncated: true, bytesDroppedBeforeCursor: droppedBeforeCursor } : {}),
|
|
@@ -1320,8 +1304,6 @@ export class NodeExecutionEnv {
|
|
|
1320
1304
|
stream.tail.recordSkippedBytes(d.skippedBytes);
|
|
1321
1305
|
stream.totalBytes += d.skippedBytes;
|
|
1322
1306
|
}
|
|
1323
|
-
if (d.live)
|
|
1324
|
-
entry.liveRotations = (entry.liveRotations ?? 0) + 1;
|
|
1325
1307
|
truncateSync(path, 0);
|
|
1326
1308
|
spool.fileCursor[lane] = 0;
|
|
1327
1309
|
}
|