@sema-agent/core 5.49.0 → 5.51.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 +112 -0
- package/dist/agents/roster-store.js +4 -1
- package/dist/agents/send-message-tool.js +5 -5
- package/dist/agents/subagent.d.ts +6 -0
- package/dist/agents/subagent.js +142 -5
- package/dist/agents/teacher.js +4 -1
- package/dist/brain/anthropic.js +11 -20
- package/dist/brain/open-responses.js +6 -14
- package/dist/brain/openai.js +6 -18
- package/dist/brain/reasoning.d.ts +100 -8
- package/dist/brain/reasoning.js +39 -15
- package/dist/brain/request-params.d.ts +37 -1
- package/dist/brain/request-params.js +40 -2
- package/dist/core/auto-mode-prompt.js +9 -1
- package/dist/core/hooks.d.ts +24 -1
- package/dist/core/hooks.js +26 -4
- package/dist/core/mcp.d.ts +7 -1
- package/dist/core/mcp.js +64 -8
- package/dist/core/memory-engine/engine.d.ts +30 -1
- package/dist/core/memory-engine/engine.js +219 -18
- package/dist/core/memory-engine/layout.d.ts +43 -0
- package/dist/core/memory-engine/layout.js +59 -0
- package/dist/core/memory-engine/memory-backend-contract.js +87 -0
- package/dist/core/memory-engine/types.d.ts +13 -1
- package/dist/core/runner/assemble-result.d.ts +6 -0
- package/dist/core/runner/assemble-result.js +1 -1
- package/dist/core/runner/prepare-task.d.ts +15 -0
- package/dist/core/runner/prepare-task.js +104 -44
- package/dist/core/runner/runtask.d.ts +5 -1
- package/dist/core/runner/runtask.js +14 -7
- package/dist/core/task-registry-agent.js +9 -3
- package/dist/core/task-registry-shared.d.ts +6 -0
- package/dist/core/task-registry.js +4 -2
- package/dist/core/tool-policy.d.ts +37 -0
- package/dist/core/tool-policy.js +36 -3
- package/dist/core/tools.js +7 -0
- package/dist/core/types.d.ts +53 -1
- package/dist/engine/loop/agent-loop.js +95 -30
- package/dist/engine/loop/types.d.ts +32 -0
- package/dist/orchestration/run-workflow-tool.d.ts +12 -0
- package/dist/orchestration/run-workflow-tool.js +1 -1
- package/dist/orchestration/workflow-governance.d.ts +27 -0
- package/dist/orchestration/workflow-governance.js +13 -0
- package/dist/orchestration/workflow-primitives.d.ts +8 -1
- package/dist/orchestration/workflow-primitives.js +11 -3
- package/package.json +1 -1
|
@@ -1504,6 +1504,65 @@ export function recordChallengedHistory(controlDir, rows, now) {
|
|
|
1504
1504
|
export function readChallengedHistory(controlDir) {
|
|
1505
1505
|
return coerceChallengedHistory(readSidecarJson(controlDir, CHALLENGED_HISTORY_FILE));
|
|
1506
1506
|
}
|
|
1507
|
+
export const PROJECTION_DEBTS_FILE = "projection-debts.json";
|
|
1508
|
+
function coerceProjectionDebts(raw) {
|
|
1509
|
+
if (raw === undefined)
|
|
1510
|
+
return { version: 1, rows: [] };
|
|
1511
|
+
const rec = raw;
|
|
1512
|
+
if (typeof rec !== "object" || rec === null || rec.version !== 1 || !Array.isArray(rec.rows)) {
|
|
1513
|
+
throw new ControlPlaneCorruptError("memory projection-debt ledger has the wrong shape — refusing (fail-closed; an unreadable debt account must not read as 'no debts')");
|
|
1514
|
+
}
|
|
1515
|
+
for (const r of rec.rows) {
|
|
1516
|
+
const row = r;
|
|
1517
|
+
if (typeof row !== "object" ||
|
|
1518
|
+
row === null ||
|
|
1519
|
+
typeof row.relPath !== "string" ||
|
|
1520
|
+
row.relPath.length === 0 ||
|
|
1521
|
+
row.relPath.startsWith("/") ||
|
|
1522
|
+
row.relPath.split(/[\\/]/).includes("..") ||
|
|
1523
|
+
typeof row.entryId !== "string" ||
|
|
1524
|
+
row.entryId.length === 0 ||
|
|
1525
|
+
typeof row.rev !== "string" ||
|
|
1526
|
+
row.rev.length === 0 ||
|
|
1527
|
+
typeof row.at !== "number" ||
|
|
1528
|
+
!Number.isFinite(row.at)) {
|
|
1529
|
+
throw new ControlPlaneCorruptError("memory projection-debt ledger: a row is malformed — refusing (fail-closed)");
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
return rec;
|
|
1533
|
+
}
|
|
1534
|
+
export function stageProjectionDebts(controlDir, rows, now) {
|
|
1535
|
+
if (rows.length === 0)
|
|
1536
|
+
return { refused: [] };
|
|
1537
|
+
const at = now();
|
|
1538
|
+
return lockedStrictUpdate(controlDir, PROJECTION_DEBTS_FILE, "memory projection-debt ledger", coerceProjectionDebts, (rec) => {
|
|
1539
|
+
const byPath = new Map(rec.rows.map((r) => [r.relPath, r]));
|
|
1540
|
+
const refused = [];
|
|
1541
|
+
for (const r of rows) {
|
|
1542
|
+
const existing = byPath.get(r.relPath);
|
|
1543
|
+
if (existing !== undefined && existing.entryId !== r.entryId && existing.entryId !== r.replaces) {
|
|
1544
|
+
refused.push({ relPath: r.relPath, entryId: r.entryId });
|
|
1545
|
+
continue;
|
|
1546
|
+
}
|
|
1547
|
+
byPath.set(r.relPath, { relPath: r.relPath, entryId: r.entryId, rev: r.rev, at });
|
|
1548
|
+
}
|
|
1549
|
+
return { next: { version: 1, rows: [...byPath.values()] }, result: { refused } };
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
export function settleProjectionDebts(controlDir, rows) {
|
|
1553
|
+
if (rows.length === 0)
|
|
1554
|
+
return;
|
|
1555
|
+
const drop = new Set(rows.map((r) => JSON.stringify([r.entryId, r.relPath])));
|
|
1556
|
+
lockedStrictUpdate(controlDir, PROJECTION_DEBTS_FILE, "memory projection-debt ledger", coerceProjectionDebts, (rec) => {
|
|
1557
|
+
const kept = rec.rows.filter((r) => !drop.has(JSON.stringify([r.entryId, r.relPath])));
|
|
1558
|
+
if (kept.length === rec.rows.length)
|
|
1559
|
+
return { result: undefined };
|
|
1560
|
+
return { next: { version: 1, rows: kept }, result: undefined };
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
export function readProjectionDebts(controlDir) {
|
|
1564
|
+
return coerceProjectionDebts(readStrictSidecar(controlDir, PROJECTION_DEBTS_FILE, "memory projection-debt ledger")).rows;
|
|
1565
|
+
}
|
|
1507
1566
|
export function writeAllSync(fd, data) {
|
|
1508
1567
|
const buf = Buffer.from(data, "utf8");
|
|
1509
1568
|
let written = 0;
|
|
@@ -426,6 +426,93 @@ export async function memoryBackendContract(hooks) {
|
|
|
426
426
|
const flat = await b.search("same words", ["s1"], { limit: 3 });
|
|
427
427
|
assert.deepStrictEqual(flat.map((h) => h.id), ["id-band-a-01", "id-band-b-01", "id-band-c-01"]);
|
|
428
428
|
});
|
|
429
|
+
defer("design/336 §4 hold protocol (update form): mid-hold writes apply plainly (backend is hold-unaware); the release replay at the capture anchor is a reported conflict carrying currentRev; a clean release applies and the snapshot face answers its committed triple", async () => {
|
|
430
|
+
const b = await hooks.make();
|
|
431
|
+
const e1v0 = entry("id-hold-upd1", "s1", "held-clean", "committed v0");
|
|
432
|
+
assert.deepStrictEqual((await b.applyPatches([{ op: "add", id: e1v0.id, entry: e1v0 }])).conflicts, []);
|
|
433
|
+
const e1rel = entry("id-hold-upd1", "s1", "held-clean", "the released capture");
|
|
434
|
+
const rep1 = await b.applyPatches([{ op: "update", id: e1v0.id, entry: e1rel, baseRev: e1v0.rev }]);
|
|
435
|
+
assert.deepStrictEqual(rep1.conflicts, [], "a release onto an untouched anchor applies");
|
|
436
|
+
const snapFace = b.committedSnapshotOf;
|
|
437
|
+
if (typeof snapFace === "function") {
|
|
438
|
+
const snap = await snapFace.call(b, e1v0.id);
|
|
439
|
+
assert.strictEqual(snap.state, "row");
|
|
440
|
+
if (snap.state === "row") {
|
|
441
|
+
assert.strictEqual(snap.rev, e1rel.rev, "the snapshot tracks the UPDATE — not the add-time state");
|
|
442
|
+
assert.strictEqual(snap.binding.state, "bound");
|
|
443
|
+
if (snap.binding.state === "bound") {
|
|
444
|
+
assert.strictEqual(snap.binding.scope, "s1");
|
|
445
|
+
assert.strictEqual(snap.binding.slug, "held-clean");
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
const rep1b = await b.applyPatches([{ op: "update", id: e1v0.id, entry: e1rel, baseRev: e1v0.rev }]);
|
|
450
|
+
assert.strictEqual(rep1b.applied.length, 0);
|
|
451
|
+
assert.strictEqual(rep1b.conflicts.length, 1);
|
|
452
|
+
assert.strictEqual(rep1b.conflicts[0]?.currentRev, e1rel.rev, "currentRev is the retry's idempotency evidence");
|
|
453
|
+
const e2v0 = entry("id-hold-upd2", "s1", "held-raced", "committed v0");
|
|
454
|
+
assert.deepStrictEqual((await b.applyPatches([{ op: "add", id: e2v0.id, entry: e2v0 }])).conflicts, []);
|
|
455
|
+
const mid = entry("id-hold-upd2", "s1", "held-raced", "mid-hold edit by another writer");
|
|
456
|
+
const repMid = await b.applyPatches([{ op: "update", id: e2v0.id, entry: mid, baseRev: e2v0.rev }]);
|
|
457
|
+
assert.deepStrictEqual(repMid.conflicts, [], "a mid-hold write applies plainly — the backend is hold-unaware");
|
|
458
|
+
assert.strictEqual((await b.getByIds([e2v0.id])).length, 1, "the held id keeps serving reads during the pendency window");
|
|
459
|
+
const e2rel = entry("id-hold-upd2", "s1", "held-raced", "the captured bytes");
|
|
460
|
+
const rep2 = await b.applyPatches([{ op: "update", id: e2v0.id, entry: e2rel, baseRev: e2v0.rev }]);
|
|
461
|
+
assert.strictEqual(rep2.applied.length, 0, "the release never blind-writes over a mid-hold winner");
|
|
462
|
+
assert.strictEqual(rep2.conflicts.length, 1);
|
|
463
|
+
assert.strictEqual(rep2.conflicts[0]?.baseRev, e2v0.rev);
|
|
464
|
+
assert.strictEqual(rep2.conflicts[0]?.currentRev, mid.rev, "the conflict names the winner — the dispose('conflict') evidence");
|
|
465
|
+
assert.strictEqual((await b.getByIds([e2v0.id]))[0]?.body.replace(/\s+$/, ""), "mid-hold edit by another writer", "the winner's content survives");
|
|
466
|
+
});
|
|
467
|
+
defer("design/336 §4 hold protocol (add form): a guard-absent release applies onto a free id; the crash retry re-applies idempotently (one row, original slug); a concurrent claim answers the guarded conflict with currentRev and survives", async () => {
|
|
468
|
+
const b = await hooks.make();
|
|
469
|
+
const rel = entry("id-hold-add1", "s1", "held-note", "instruction body");
|
|
470
|
+
assert.deepStrictEqual((await b.applyPatches([{ op: "add", id: rel.id, entry: rel, guard: "absent" }])).conflicts, []);
|
|
471
|
+
const rep2 = await b.applyPatches([{ op: "add", id: rel.id, entry: rel, guard: "absent" }]);
|
|
472
|
+
assert.deepStrictEqual(rep2.conflicts, [], "the crash retry of a committed release re-applies idempotently");
|
|
473
|
+
assert.deepStrictEqual(rep2.applied, [{ op: "add", id: rel.id, slug: "held-note" }]);
|
|
474
|
+
assert.deepStrictEqual((await b.listHeaders(["s1"])).filter((h) => h.id === rel.id).map((h) => h.slug), ["held-note"], "ONE row at the ORIGINAL slug — never a suffixed duplicate");
|
|
475
|
+
const claimed = entry("id-hold-add2", "s1", "claimed-note", "the claimant's content");
|
|
476
|
+
assert.deepStrictEqual((await b.applyPatches([{ op: "add", id: claimed.id, entry: claimed }])).conflicts, []);
|
|
477
|
+
const rel2 = entry("id-hold-add2", "s1", "claimed-note", "the captured bytes");
|
|
478
|
+
const rep3 = await b.applyPatches([{ op: "add", id: rel2.id, entry: rel2, guard: "absent" }]);
|
|
479
|
+
assert.strictEqual(rep3.applied.length, 0);
|
|
480
|
+
assert.strictEqual(rep3.conflicts.length, 1);
|
|
481
|
+
assert.match(rep3.conflicts[0]?.reason ?? "", /add_guard_absent_conflict/);
|
|
482
|
+
assert.strictEqual(rep3.conflicts[0]?.currentRev, claimed.rev, "currentRev distinguishes 'claimed by another writer' from the idempotent retry");
|
|
483
|
+
assert.strictEqual((await b.getByIds([claimed.id]))[0]?.body.replace(/\s+$/, ""), "the claimant's content", "the claimant survives");
|
|
484
|
+
});
|
|
485
|
+
defer("design/336: the whitewash judgment precedes CAS — an origin/trust strip with a STALE baseRev still answers the malformed refusal, never the rev-mismatch conflict", async () => {
|
|
486
|
+
const b = await hooks.make();
|
|
487
|
+
const origin = { taint: "external", cause: "observed", at: 1700000000900 };
|
|
488
|
+
const m0 = { id: "id-prec-0001", scope: "s1", slug: "prec-marked", frontmatter: { name: "prec", origin }, body: "b0", rev: "" };
|
|
489
|
+
m0.rev = computeEntryRev(m0);
|
|
490
|
+
assert.deepStrictEqual((await b.applyPatches([{ op: "add", id: m0.id, entry: m0 }])).conflicts, []);
|
|
491
|
+
const m1 = { ...m0, body: "b1", rev: "" };
|
|
492
|
+
m1.rev = computeEntryRev(m1);
|
|
493
|
+
assert.deepStrictEqual((await b.applyPatches([{ op: "update", id: m0.id, entry: m1, baseRev: m0.rev }])).conflicts, []);
|
|
494
|
+
const stripped = { id: m0.id, scope: "s1", slug: "prec-marked", frontmatter: { name: "prec" }, body: "b2", rev: "" };
|
|
495
|
+
stripped.rev = computeEntryRev(stripped);
|
|
496
|
+
const rep = await b.applyPatches([{ op: "update", id: m0.id, entry: stripped, baseRev: m0.rev }]);
|
|
497
|
+
assert.strictEqual(rep.applied.length, 0);
|
|
498
|
+
assert.strictEqual(rep.conflicts.length, 1);
|
|
499
|
+
assert.match(rep.conflicts[0]?.reason ?? "", /malformed patch refused/, "the stronger refusal answers — a CAS conflict here would hide the whitewash");
|
|
500
|
+
const prov = { kind: "repo_file", path: "AGENTS.md", contentHash: "sha256:cccc3333", ingestedAt: 1700000001000 };
|
|
501
|
+
const p0 = { id: "id-prec-0002", scope: "s1", slug: "prec-repo", frontmatter: { name: "prec-repo", provenance: prov, trust: "untrusted" }, body: "p0", rev: "" };
|
|
502
|
+
p0.rev = computeEntryRev(p0);
|
|
503
|
+
assert.deepStrictEqual((await b.applyPatches([{ op: "add", id: p0.id, entry: p0 }])).conflicts, []);
|
|
504
|
+
const p1 = { ...p0, body: "p1", rev: "" };
|
|
505
|
+
p1.rev = computeEntryRev(p1);
|
|
506
|
+
assert.deepStrictEqual((await b.applyPatches([{ op: "update", id: p0.id, entry: p1, baseRev: p0.rev }])).conflicts, []);
|
|
507
|
+
const strippedTrust = { id: p0.id, scope: "s1", slug: "prec-repo", frontmatter: { name: "prec-repo", provenance: prov }, body: "p2", rev: "" };
|
|
508
|
+
strippedTrust.rev = computeEntryRev(strippedTrust);
|
|
509
|
+
const rep2 = await b.applyPatches([{ op: "update", id: p0.id, entry: strippedTrust, baseRev: p0.rev }]);
|
|
510
|
+
assert.strictEqual(rep2.applied.length, 0);
|
|
511
|
+
assert.strictEqual(rep2.conflicts.length, 1);
|
|
512
|
+
assert.match(rep2.conflicts[0]?.reason ?? "", /malformed patch refused/);
|
|
513
|
+
assert.deepStrictEqual((await b.getByIds([m0.id]))[0]?.frontmatter.origin, origin);
|
|
514
|
+
assert.strictEqual((await b.getByIds([p0.id]))[0]?.frontmatter.trust, "untrusted");
|
|
515
|
+
});
|
|
429
516
|
defer("consolidation cursor round-trips per scope; unset → undefined", async () => {
|
|
430
517
|
const b = await hooks.make();
|
|
431
518
|
assert.strictEqual(await b.getConsolidationCursor("s1"), undefined);
|
|
@@ -254,6 +254,18 @@ export interface MemoryBackend {
|
|
|
254
254
|
* earlier delete in the same batch does not blank the baseline). The one legal exit is the
|
|
255
255
|
* COMMITTED tombstone: after a delete commits, the marker's life ends with the id (a fresh id —
|
|
256
256
|
* or the same id in a LATER batch — starts an unmarked life; the engine re-judges its session);
|
|
257
|
+
* - PRECEDENCE (FAM-1 #5, every whitewash spelling): the malformed judgment answers BEFORE
|
|
258
|
+
* guard/CAS arithmetic — a strip riding a stale `baseRev` (or a guard conflict) still answers
|
|
259
|
+
* /malformed patch refused/, never the ordinary rev-mismatch conflict (a whitewash is illegal
|
|
260
|
+
* at ANY rev, and the weaker conflict would tell the caller's ladder to rebase and retry it);
|
|
261
|
+
* - design/336 §4 hold protocol: the backend is hold-UNAWARE — instruction holds live on the
|
|
262
|
+
* ENGINE's control plane, so a held id keeps serving reads and ordinary add/update patches for
|
|
263
|
+
* it apply plainly (no queueing, no refusal). The settlement-period write interaction resolves
|
|
264
|
+
* at release time through the clauses above: the release replays the capture-time anchor
|
|
265
|
+
* (update+`baseRev` / `guard: "absent"` add), a mid-hold winner surfaces as the REPORTED
|
|
266
|
+
* conflict carrying `currentRev` (the engine's conflict-disposition evidence), and the
|
|
267
|
+
* crash-idempotent retry reads the optional committed-snapshot face (post-update rev + bound
|
|
268
|
+
* projection) — all asserted by the suite's two hold-protocol cases;
|
|
257
269
|
* - conflicts are per-patch and non-fatal: the rest of the batch still applies.
|
|
258
270
|
*/
|
|
259
271
|
applyPatches(patches: readonly NotePatch[]): Promise<PatchReport>;
|
|
@@ -338,7 +350,7 @@ export interface MemorySessionHandle {
|
|
|
338
350
|
adoptionRestricted?: boolean;
|
|
339
351
|
}
|
|
340
352
|
/** Stable rejection codes a harvest gate can produce (model-visible gate events — 镜头 I). */
|
|
341
|
-
export type HarvestRejectionCode = "outside_root" | "symlink" | "secret" | "injection" | "filename" | "too_large" | "file_cap" | "readonly_layer" | "stub_modified" | "nested_too_deep" | "quarantine_failed" | "unreadable" | "polluted" | "restricted_divergence" | "invalid";
|
|
353
|
+
export type HarvestRejectionCode = "outside_root" | "symlink" | "secret" | "injection" | "filename" | "too_large" | "file_cap" | "readonly_layer" | "stub_modified" | "nested_too_deep" | "quarantine_failed" | "unreadable" | "deferred" | "polluted" | "restricted_divergence" | "invalid";
|
|
342
354
|
/** One rejected file: path (relative to the memory dir), stable code, and a model-readable reason. */
|
|
343
355
|
export interface HarvestRejection {
|
|
344
356
|
path: string;
|
|
@@ -112,6 +112,12 @@ export interface ResultFlags {
|
|
|
112
112
|
* Pure pass-through — assembly neither adds nor filters (a rewind that FAILED never reaches here; it
|
|
113
113
|
* throws at prepare and lands in the `threw` slot as a terminal errorCode). */
|
|
114
114
|
rewindNotes?: TaskResult["rewindNotes"];
|
|
115
|
+
/** The run's final turn was halted by a person's BARE rejection of a tool call (the parent-thread
|
|
116
|
+
* control-flow boundary) — echoed on `TaskResult.haltedOnUserRejection`. Pure pass-through on
|
|
117
|
+
* every terminal: the fact is about the leg that ran, whatever terminal it reached (on the normal
|
|
118
|
+
* path the terminal is `completed`, and this is what tells that completion apart from a natural
|
|
119
|
+
* one — the model did not finish; the person stopped it and the run awaits their direction). */
|
|
120
|
+
haltedOnUserRejection?: boolean;
|
|
115
121
|
/** design/174 final-round: call ids of answered-but-never-collected questions, echoed on
|
|
116
122
|
* `TaskResult.strandedHumanAnswers`. Pure pass-through; empty/absent ⇒ the field is omitted. The
|
|
117
123
|
* optional `onError` alert is NOT the disclosure — this mandatory result face is. */
|
|
@@ -165,5 +165,5 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
|
|
|
165
165
|
void _internalCompaction;
|
|
166
166
|
if (flags.unpricedSpend)
|
|
167
167
|
delete publicStats.costMicroUsd;
|
|
168
|
-
return { taskId, sessionId, status, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, blockedReason, errorMessage, errorCode, ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), checkpointToken, ...(checkpointId !== undefined ? { checkpointId } : {}), checkpointGate, ...(workspaceRestoreMode !== undefined ? { workspaceRestoreMode } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: flags.remoteEnvFailures } : {}), ...(flags.strandedHumanAnswers !== undefined && flags.strandedHumanAnswers.length > 0 ? { strandedHumanAnswers: flags.strandedHumanAnswers } : {}), ...(flags.effectiveReadFace !== undefined ? { effectiveReadFace: flags.effectiveReadFace } : {}), ...(flags.effectiveReadDenyPatterns !== undefined && flags.effectiveReadDenyPatterns.length > 0 ? { effectiveReadDenyPatterns: flags.effectiveReadDenyPatterns } : {}), ...(flags.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: flags.effectiveMemoryScopes } : {}), ...(flags.effectiveReasoning !== undefined ? { effectiveReasoning: flags.effectiveReasoning } : {}), stats: publicStats };
|
|
168
|
+
return { taskId, sessionId, status, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, blockedReason, errorMessage, errorCode, ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), checkpointToken, ...(checkpointId !== undefined ? { checkpointId } : {}), checkpointGate, ...(workspaceRestoreMode !== undefined ? { workspaceRestoreMode } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.haltedOnUserRejection === true ? { haltedOnUserRejection: true } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: flags.remoteEnvFailures } : {}), ...(flags.strandedHumanAnswers !== undefined && flags.strandedHumanAnswers.length > 0 ? { strandedHumanAnswers: flags.strandedHumanAnswers } : {}), ...(flags.effectiveReadFace !== undefined ? { effectiveReadFace: flags.effectiveReadFace } : {}), ...(flags.effectiveReadDenyPatterns !== undefined && flags.effectiveReadDenyPatterns.length > 0 ? { effectiveReadDenyPatterns: flags.effectiveReadDenyPatterns } : {}), ...(flags.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: flags.effectiveMemoryScopes } : {}), ...(flags.effectiveReasoning !== undefined ? { effectiveReasoning: flags.effectiveReasoning } : {}), stats: publicStats };
|
|
169
169
|
}
|
|
@@ -200,7 +200,22 @@ export interface Prepared {
|
|
|
200
200
|
approvalSettlement: Map<string, {
|
|
201
201
|
settledBy?: import("../tool-policy.js").ApprovalSettledBy;
|
|
202
202
|
approver?: string;
|
|
203
|
+
resolution?: import("../tool-policy.js").AskDenyResolution;
|
|
203
204
|
}>;
|
|
205
|
+
/**
|
|
206
|
+
* The parent-thread human-rejection halt fact (see `maybeHumanRejectionHalt`): present from the
|
|
207
|
+
* moment a bare human rejection halts the turn's batch until the run ends or the NEXT provider
|
|
208
|
+
* request begins (user input continuing the run clears it). Consumers: the runner's stop gate
|
|
209
|
+
* (suppress natural-end pushback / final-verify injection — engine continuations must not restart
|
|
210
|
+
* a run a person just stopped), the turn-boundary engine steers (same reason), and the result
|
|
211
|
+
* stamp (`TaskResult.haltedOnUserRejection` — a human-halted run must not read as an ordinary
|
|
212
|
+
* completion). Engine-owned sideband, same trust reasoning as `approvalSettlement` above.
|
|
213
|
+
*/
|
|
214
|
+
batchHaltRef: {
|
|
215
|
+
current?: {
|
|
216
|
+
rejectedToolCallId: string;
|
|
217
|
+
};
|
|
218
|
+
};
|
|
204
219
|
/** Summed usage of nested sub-runs (sub-agents) spawned by this task's tools. */
|
|
205
220
|
nestedStats: NestedUsageAccum;
|
|
206
221
|
/** RB-430-a: prepare-time rewind disclosures (conversation-only branch / no snapshot backend / no file
|
|
@@ -20,7 +20,7 @@ import { createSubagentWorktreeHelper, forkGovernanceDenial, resolveDelegationEn
|
|
|
20
20
|
import { createAgentTranscriptTool, AGENT_TRANSCRIPT_TOOL_NAME } from "../../agents/agent-transcript-tool.js";
|
|
21
21
|
import { createSendMessageTool, SEND_MESSAGE_TOOL_NAME } from "../../agents/send-message-tool.js";
|
|
22
22
|
import { SubagentRetainLedger } from "../../agents/retain-ledger.js";
|
|
23
|
-
import { askApproverIdentity, checkToolPolicyProjection, combinePolicies, constraintChainDigest, constraintChainEntryOf, isApprovalSettledBy, screenApproverAttribution, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, describeThrown, refuseOutOfContractDecision, resolveAsk, toolPolicyNameSets, tryCloneArgs } from "../tool-policy.js";
|
|
23
|
+
import { askApproverIdentity, checkToolPolicyProjection, combinePolicies, constraintChainDigest, constraintChainEntryOf, isApprovalSettledBy, isAskDenyResolution, screenApproverAttribution, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, describeThrown, refuseOutOfContractDecision, resolveAsk, toolPolicyNameSets, tryCloneArgs } from "../tool-policy.js";
|
|
24
24
|
const PERSISTED_RULE_TOOL = "Bash";
|
|
25
25
|
import { findAdmittingRule, suggestRulesForCommand } from "../permission-rule-model.js";
|
|
26
26
|
import { ActiveSkillScope, createActiveSkillScopePolicy } from "./active-skill-scope.js";
|
|
@@ -277,6 +277,19 @@ function raceAbort(p, signal, onAbort) {
|
|
|
277
277
|
p.then(finish, () => finish(onAbort()));
|
|
278
278
|
});
|
|
279
279
|
}
|
|
280
|
+
function mcpRevocationWiring(deps) {
|
|
281
|
+
if (deps.mcpRevocations === undefined)
|
|
282
|
+
return undefined;
|
|
283
|
+
const ledger = deps.mcpRevocations;
|
|
284
|
+
return {
|
|
285
|
+
isRevoked: (name) => ledger.isRevoked(name),
|
|
286
|
+
onProbeFailure: (e) => deliverEngineNotice(deps.onNotice, {
|
|
287
|
+
code: "mcp.revocation_probe_failed",
|
|
288
|
+
message: `the mcpRevocations.isRevoked probe threw — MCP dispatch fails OPEN (no server treated as revoked) until the probe recovers: ${e instanceof Error ? e.message : String(e)}`,
|
|
289
|
+
detail: { message: e instanceof Error ? e.message : String(e) },
|
|
290
|
+
}),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
280
293
|
async function forgetQuietly(sessions, sessionId) {
|
|
281
294
|
try {
|
|
282
295
|
if (sessions.forget)
|
|
@@ -301,11 +314,72 @@ function screenGateSettlement(result, settling) {
|
|
|
301
314
|
if (attribution.defect !== undefined) {
|
|
302
315
|
defects.push(`a tool-gate settlement reported an attribution this engine refuses: ${attribution.defect}; the frame carries no approver`);
|
|
303
316
|
}
|
|
317
|
+
const reportedResolution = result.resolution;
|
|
318
|
+
let resolution;
|
|
319
|
+
if (reportedResolution !== undefined) {
|
|
320
|
+
if (!isAskDenyResolution(reportedResolution) || !settling) {
|
|
321
|
+
defects.push(`a tool-gate settlement reported a deny resolution "${String(reportedResolution)}" on ${settling ? "a blocked" : "an executing"} call — ` +
|
|
322
|
+
`it is one of the closed ask-deny vocabulary and only a BLOCKED call can carry one; the frame carries no resolution`);
|
|
323
|
+
}
|
|
324
|
+
else
|
|
325
|
+
resolution = reportedResolution;
|
|
326
|
+
}
|
|
304
327
|
const record = {
|
|
305
328
|
...(settledBy !== undefined ? { settledBy } : {}),
|
|
306
329
|
...(attribution.approver !== undefined ? { approver: attribution.approver } : {}),
|
|
330
|
+
...(resolution !== undefined ? { resolution } : {}),
|
|
307
331
|
};
|
|
308
|
-
return { ...(settledBy !== undefined || attribution.approver !== undefined ? { record } : {}), defects };
|
|
332
|
+
return { ...(settledBy !== undefined || attribution.approver !== undefined || resolution !== undefined ? { record } : {}), defects };
|
|
333
|
+
}
|
|
334
|
+
function maybeHumanRejectionHalt(input) {
|
|
335
|
+
if (!input.bare || input.settledBy !== "human" || input.isDelegatedChild)
|
|
336
|
+
return undefined;
|
|
337
|
+
return {
|
|
338
|
+
reason: `This tool call was NOT executed: the user rejected the "${input.toolName}" tool call in the same ` +
|
|
339
|
+
`assistant message, which stops the rest of the batch. Nothing was run for this call — ` +
|
|
340
|
+
`re-issue it after the user's direction only if it is still needed.`,
|
|
341
|
+
details: {
|
|
342
|
+
error: "gate.batch_halted",
|
|
343
|
+
code: "gate.batch_halted",
|
|
344
|
+
rejectedToolCallId: input.toolCallId,
|
|
345
|
+
rejectedToolName: input.toolName,
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
const sanitizePreview = (node, depth = 0) => {
|
|
350
|
+
if (depth > 6)
|
|
351
|
+
return undefined;
|
|
352
|
+
if (typeof node === "string") {
|
|
353
|
+
return node.replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, "\u2400");
|
|
354
|
+
}
|
|
355
|
+
if (node === null || typeof node !== "object")
|
|
356
|
+
return node;
|
|
357
|
+
if (Array.isArray(node))
|
|
358
|
+
return node.map((v) => sanitizePreview(v, depth + 1));
|
|
359
|
+
const out = {};
|
|
360
|
+
for (const [k, v] of Object.entries(node)) {
|
|
361
|
+
out[sanitizePreview(k, depth + 1)] = sanitizePreview(v, depth + 1);
|
|
362
|
+
}
|
|
363
|
+
return out;
|
|
364
|
+
};
|
|
365
|
+
function resolveApprovalPreview(tools, toolName, args) {
|
|
366
|
+
const t = tools.find((x) => x.name === toolName || (x.aliases?.includes(toolName) ?? false));
|
|
367
|
+
if (t?.approvalPreview === undefined)
|
|
368
|
+
return undefined;
|
|
369
|
+
try {
|
|
370
|
+
const raw = t.approvalPreview(args);
|
|
371
|
+
if (raw === undefined)
|
|
372
|
+
return undefined;
|
|
373
|
+
const bytes = JSON.stringify(raw);
|
|
374
|
+
if (bytes === undefined)
|
|
375
|
+
return undefined;
|
|
376
|
+
if (bytes.length > 16_384)
|
|
377
|
+
return { truncated: true, note: `approval preview exceeded 16KiB (${bytes.length} chars serialized)` };
|
|
378
|
+
return sanitizePreview(raw);
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
return undefined;
|
|
382
|
+
}
|
|
309
383
|
}
|
|
310
384
|
function inheritedAskRuleEvidence(deps) {
|
|
311
385
|
const org = deps.permissionRuleOrg === undefined ? "not_wired" : "not_adjudicated";
|
|
@@ -1030,6 +1104,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1030
1104
|
parentThinking: () => harnessRef.current?.getThinkingLevel() ?? thinking,
|
|
1031
1105
|
parentReadFace: () => carrierReadFace(),
|
|
1032
1106
|
parentReadDenyPatterns: () => (readDenyAdditionsNormalized.length > 0 ? readDenyAdditionsNormalized : undefined),
|
|
1107
|
+
...(spec.handsReadOnly === true ? { parentHandsReadOnly: true } : {}), ...(spec.interactiveTools === false ? { parentInteractiveTools: false } : {}),
|
|
1033
1108
|
onNotice: deps.onNotice,
|
|
1034
1109
|
parentCheckpointStoreDisabled: spec.checkpointStore === null,
|
|
1035
1110
|
parentCenterArtifactDigest: () => centerAdoption?.artifact.artifactDigest,
|
|
@@ -1101,7 +1176,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1101
1176
|
tools.push(createOutputTool(outputRef, spec.outputSchema, compiled.strict ? compiled.modelSchema : undefined));
|
|
1102
1177
|
}
|
|
1103
1178
|
mcp = lockedPreflight.mcp?.length
|
|
1104
|
-
? await materializeMcpTools(lockedPreflight.mcp, spec.principal, deps.onElicit, deps.mcpImageResizer, { reminderMark, counts: reminderDisclosureCounts })
|
|
1179
|
+
? await materializeMcpTools(lockedPreflight.mcp, spec.principal, deps.onElicit, deps.mcpImageResizer, { reminderMark, counts: reminderDisclosureCounts }, mcpRevocationWiring(deps))
|
|
1105
1180
|
: { tools: [], toolAxes: [], warnings: [], serverInstructions: [], instructionsDelta: { pendingAdds: [], pendingRemovals: [] }, droppedTools: [], statuses: [], refresh: async () => [], dispose: async () => { } };
|
|
1106
1181
|
for (const w of mcp.warnings)
|
|
1107
1182
|
deps.onError?.(w, { phase: "mcp", sessionId });
|
|
@@ -3277,6 +3352,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3277
3352
|
const preToolContexts = new Map();
|
|
3278
3353
|
const blockedToolCalls = new Set();
|
|
3279
3354
|
const approvalSettlement = new Map();
|
|
3355
|
+
const humanBareRejections = new Set();
|
|
3356
|
+
const batchHaltRef = {};
|
|
3280
3357
|
const blockedTracked = Boolean(hooks?.postToolUse || hooks?.preToolUse || hooks?.postToolUseFailure || hooks?.postToolBatch);
|
|
3281
3358
|
const restoreSurfaceGap = ownedEnv !== undefined && isRemoteExecutionEnv(ownedEnv) && ownedEnv.capabilities.suspendable ? missingRestoreSurface(ownedEnv) : [];
|
|
3282
3359
|
const incompleteSuspendAdapter = restoreSurfaceGap.length > 0 ? restoreSurfaceGap : undefined;
|
|
@@ -3380,41 +3457,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3380
3457
|
message: "policy check aborted (task timed out or cancelled)",
|
|
3381
3458
|
}))
|
|
3382
3459
|
: undefined;
|
|
3383
|
-
const
|
|
3384
|
-
if (depth > 6)
|
|
3385
|
-
return undefined;
|
|
3386
|
-
if (typeof node === "string") {
|
|
3387
|
-
return node.replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, "\u2400");
|
|
3388
|
-
}
|
|
3389
|
-
if (node === null || typeof node !== "object")
|
|
3390
|
-
return node;
|
|
3391
|
-
if (Array.isArray(node))
|
|
3392
|
-
return node.map((v) => sanitizePreview(v, depth + 1));
|
|
3393
|
-
const out = {};
|
|
3394
|
-
for (const [k, v] of Object.entries(node)) {
|
|
3395
|
-
out[sanitizePreview(k, depth + 1)] = sanitizePreview(v, depth + 1);
|
|
3396
|
-
}
|
|
3397
|
-
return out;
|
|
3398
|
-
};
|
|
3399
|
-
const approvalPreviewOf = (toolName, args) => {
|
|
3400
|
-
const t = tools.find((x) => x.name === toolName || (x.aliases?.includes(toolName) ?? false));
|
|
3401
|
-
if (t?.approvalPreview === undefined)
|
|
3402
|
-
return undefined;
|
|
3403
|
-
try {
|
|
3404
|
-
const raw = t.approvalPreview(args);
|
|
3405
|
-
if (raw === undefined)
|
|
3406
|
-
return undefined;
|
|
3407
|
-
const bytes = JSON.stringify(raw);
|
|
3408
|
-
if (bytes === undefined)
|
|
3409
|
-
return undefined;
|
|
3410
|
-
if (bytes.length > 16_384)
|
|
3411
|
-
return { truncated: true, note: `approval preview exceeded 16KiB (${bytes.length} chars serialized)` };
|
|
3412
|
-
return sanitizePreview(raw);
|
|
3413
|
-
}
|
|
3414
|
-
catch {
|
|
3415
|
-
return undefined;
|
|
3416
|
-
}
|
|
3417
|
-
};
|
|
3460
|
+
const approvalPreviewOf = (toolName, args) => resolveApprovalPreview(tools, toolName, args);
|
|
3418
3461
|
const resolveAskBound = async (decision, req) => {
|
|
3419
3462
|
if (inheritedUnavailableAsks.delete(req.toolCallId)) {
|
|
3420
3463
|
return {
|
|
@@ -3475,6 +3518,9 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3475
3518
|
...(toolArg !== undefined ? { toolArg } : {}),
|
|
3476
3519
|
});
|
|
3477
3520
|
}
|
|
3521
|
+
if (resolved.action === "deny" && resolved.settledBy === "human" && resolved.humanRefusalNote !== true) {
|
|
3522
|
+
humanBareRejections.add(req.toolCallId);
|
|
3523
|
+
}
|
|
3478
3524
|
return resolved;
|
|
3479
3525
|
};
|
|
3480
3526
|
const hooksWithPermissionDenied = hooks?.permissionDenied ? hooks : undefined;
|
|
@@ -4183,6 +4229,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4183
4229
|
harness.on("tool_call", async (e) => {
|
|
4184
4230
|
blockedToolCalls.delete(e.toolCallId);
|
|
4185
4231
|
inheritedAskGrants.delete(e.toolCallId);
|
|
4232
|
+
humanBareRejections.delete(e.toolCallId);
|
|
4186
4233
|
{
|
|
4187
4234
|
const complianceDeny = complianceCallDenial(complianceDenies, e.toolName);
|
|
4188
4235
|
if (complianceDeny !== undefined) {
|
|
@@ -4209,6 +4256,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4209
4256
|
};
|
|
4210
4257
|
}
|
|
4211
4258
|
let result;
|
|
4259
|
+
let humanBareRejection = false;
|
|
4212
4260
|
try {
|
|
4213
4261
|
result = await runToolGate({
|
|
4214
4262
|
onNotifyError: (f) => emitTrace(deps.tracer, () => ({ kind: "observer.notify_failed", version: 1, taskId: spec.taskId ?? sessionId, site: f.site, message: f.error.message, ts: Date.now() })),
|
|
@@ -4280,6 +4328,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4280
4328
|
inheritedUnavailableAsks.delete(e.toolCallId);
|
|
4281
4329
|
inheritedAskGrants.delete(e.toolCallId);
|
|
4282
4330
|
foldAskClasses.delete(e.toolCallId);
|
|
4331
|
+
humanBareRejection = humanBareRejections.delete(e.toolCallId);
|
|
4283
4332
|
}
|
|
4284
4333
|
const ancestorAdmitted = ancestorSandboxAdmissions.get(e.toolCallId);
|
|
4285
4334
|
ancestorSandboxAdmissions.delete(e.toolCallId);
|
|
@@ -4298,11 +4347,21 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4298
4347
|
deps.onError?.(new Error(defect), { phase: "config", sessionId });
|
|
4299
4348
|
if (settlement.record !== undefined)
|
|
4300
4349
|
approvalSettlement.set(e.toolCallId, settlement.record);
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
:
|
|
4350
|
+
if (result.block) {
|
|
4351
|
+
const haltRemaining = maybeHumanRejectionHalt({
|
|
4352
|
+
bare: humanBareRejection,
|
|
4353
|
+
settledBy: result.settledBy,
|
|
4354
|
+
isDelegatedChild: delegation.isDelegatedChild === true,
|
|
4355
|
+
toolName: e.toolName,
|
|
4356
|
+
toolCallId: e.toolCallId,
|
|
4357
|
+
});
|
|
4358
|
+
if (haltRemaining !== undefined) {
|
|
4359
|
+
batchHaltRef.current = { rejectedToolCallId: e.toolCallId };
|
|
4360
|
+
return { block: true, reason: result.reason, haltRemaining };
|
|
4361
|
+
}
|
|
4362
|
+
return { block: true, reason: result.reason };
|
|
4363
|
+
}
|
|
4364
|
+
return result.updatedInput !== undefined ? { updatedInput: result.updatedInput } : undefined;
|
|
4306
4365
|
});
|
|
4307
4366
|
}
|
|
4308
4367
|
}
|
|
@@ -4360,6 +4419,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4360
4419
|
const guardAt = guardBudget(model);
|
|
4361
4420
|
const charsPerToken = model.charsPerToken ?? DEFAULT_CHARS_PER_TOKEN;
|
|
4362
4421
|
harness.on("context", async ({ messages }) => {
|
|
4422
|
+
batchHaltRef.current = undefined;
|
|
4363
4423
|
const healed = dropEmptyFailureAssistants(messages);
|
|
4364
4424
|
const capped = await capAggregateToolResults(healed, {
|
|
4365
4425
|
store: offloadStore,
|
|
@@ -4624,7 +4684,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4624
4684
|
const effectiveReadFaceObserved = carrierReadFace();
|
|
4625
4685
|
const effectiveReadDenyObserved = readDenyAdditionsNormalized.length > 0 ? readDenyAdditionsNormalized.map((e) => ({ ...e })) : undefined;
|
|
4626
4686
|
const preparedHolder = {};
|
|
4627
|
-
const buildPrepared = () => ({ harness, session, sessionId, reminderMark, reminderDisclosureCounts, taskRootPath: taskRootFinal, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, approvalSettlement, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), ...(effectiveReadFaceObserved !== undefined ? { effectiveReadFace: effectiveReadFaceObserved } : {}), ...(effectiveReadDenyObserved !== undefined ? { effectiveReadDenyPatterns: effectiveReadDenyObserved } : {}), effectiveMemoryScopes: memoryEffectiveScopes, cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), ...(permissionRuleOrgLane !== undefined ? { permissionRuleOrg: permissionRuleOrgLane } : {}), releaseSignal, settleContentAskBindings, cacheBreakDetector, cacheFingerprint, wiringManifest, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, deferDirectCall, ...(staticFaceForRef.current !== undefined ? { staticFaceFor: staticFaceForRef.current } : {}), ownedEnv, suspendRef, suspendProgressRef, reviewRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, brainCallGuardrailRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, wakeRecovered, promptOverheadTokens, lastBrainContext, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit, runIdent: lspRunIdent } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, gitStatusRef, listBackgroundTasks, hookIdentity, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
|
|
4687
|
+
const buildPrepared = () => ({ harness, session, sessionId, reminderMark, reminderDisclosureCounts, taskRootPath: taskRootFinal, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, approvalSettlement, batchHaltRef, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), ...(effectiveReadFaceObserved !== undefined ? { effectiveReadFace: effectiveReadFaceObserved } : {}), ...(effectiveReadDenyObserved !== undefined ? { effectiveReadDenyPatterns: effectiveReadDenyObserved } : {}), effectiveMemoryScopes: memoryEffectiveScopes, cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), ...(permissionRuleOrgLane !== undefined ? { permissionRuleOrg: permissionRuleOrgLane } : {}), releaseSignal, settleContentAskBindings, cacheBreakDetector, cacheFingerprint, wiringManifest, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, deferDirectCall, ...(staticFaceForRef.current !== undefined ? { staticFaceFor: staticFaceForRef.current } : {}), ownedEnv, suspendRef, suspendProgressRef, reviewRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, brainCallGuardrailRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, wakeRecovered, promptOverheadTokens, lastBrainContext, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit, runIdent: lspRunIdent } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, gitStatusRef, listBackgroundTasks, hookIdentity, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
|
|
4628
4688
|
const prepared = buildPrepared();
|
|
4629
4689
|
preparedHolder.current = prepared;
|
|
4630
4690
|
return prepared;
|
|
@@ -84,7 +84,10 @@ settledBy?: ApprovalSettledBy,
|
|
|
84
84
|
/** design/252 G-7 — WHOSE settlement, from the same caller and the same channel as `settledBy`, and
|
|
85
85
|
* for the same reason it is a parameter: an attribution read out of a tool's own result would let a
|
|
86
86
|
* tool name the person who approved it. Omitted ⇒ this call's settlement named nobody. */
|
|
87
|
-
approver?: string
|
|
87
|
+
approver?: string,
|
|
88
|
+
/** The ask resolver's deny-arm classification — same caller, same engine-owned channel and the same
|
|
89
|
+
* never-derived-from-`result` posture as the two above. Omitted ⇒ not an ask-resolution deny. */
|
|
90
|
+
resolution?: import("../tool-policy.js").AskDenyResolution): {
|
|
88
91
|
output?: unknown;
|
|
89
92
|
truncated?: boolean;
|
|
90
93
|
totalChars?: number;
|
|
@@ -92,6 +95,7 @@ approver?: string): {
|
|
|
92
95
|
errorCode?: string;
|
|
93
96
|
settledBy?: ApprovalSettledBy;
|
|
94
97
|
approver?: string;
|
|
98
|
+
resolution?: import("../tool-policy.js").AskDenyResolution;
|
|
95
99
|
};
|
|
96
100
|
/**
|
|
97
101
|
* scan-1/A1 — the BODY of the synthetic `tool_end` that closes a reconcile-recovered orphan. ONE
|
|
@@ -141,7 +141,7 @@ function resumeDecisionWasNegative(resume) {
|
|
|
141
141
|
}
|
|
142
142
|
const DEFERRED_REISSUE = "[DEFERRED] This tool call shared a batch with a call that suspended for durable approval, so it was " +
|
|
143
143
|
"NOT executed on resume. If you still need it, issue it again now.";
|
|
144
|
-
function toolEndBodyFrom(result, isError, settledBy, approver) {
|
|
144
|
+
function toolEndBodyFrom(result, isError, settledBy, approver, resolution) {
|
|
145
145
|
const o = toolOutputFrom(result);
|
|
146
146
|
const st = structuredFrom(result);
|
|
147
147
|
const det = isError ? result?.details : undefined;
|
|
@@ -154,6 +154,7 @@ function toolEndBodyFrom(result, isError, settledBy, approver) {
|
|
|
154
154
|
...(typeof code === "string" ? { errorCode: code } : {}),
|
|
155
155
|
...(settledBy !== undefined ? { settledBy } : {}),
|
|
156
156
|
...(approver !== undefined ? { approver } : {}),
|
|
157
|
+
...(resolution !== undefined ? { resolution } : {}),
|
|
157
158
|
};
|
|
158
159
|
}
|
|
159
160
|
export function reconciledToolEndBody(orphan) {
|
|
@@ -487,6 +488,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
|
|
|
487
488
|
rs.counters.approachNoticesSent < LIMIT_APPROACH_DEFAULT_THRESHOLDS.length &&
|
|
488
489
|
prepared.suspendForResource === undefined &&
|
|
489
490
|
rs.turn.lastTurnHadToolCalls &&
|
|
491
|
+
prepared.batchHaltRef.current === undefined &&
|
|
490
492
|
!boundarySteered &&
|
|
491
493
|
!prepared.abortController.signal.aborted) {
|
|
492
494
|
const thresholds = approachCfg?.at ?? LIMIT_APPROACH_DEFAULT_THRESHOLDS;
|
|
@@ -534,7 +536,10 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
|
|
|
534
536
|
}
|
|
535
537
|
emitTrace(rs.telemetry.tracer, () => ({ kind: "review.dropped", version: 1, taskId: rs.telemetry.taskId, ts: Date.now() }));
|
|
536
538
|
}
|
|
537
|
-
if (prepared.lspDiagnostics &&
|
|
539
|
+
if (prepared.lspDiagnostics &&
|
|
540
|
+
!prepared.lspDiagnostics.registry.isEmpty() &&
|
|
541
|
+
!prepared.abortController.signal.aborted &&
|
|
542
|
+
prepared.batchHaltRef.current === undefined) {
|
|
538
543
|
const files = prepared.lspDiagnostics.registry.drain(prepared.lspDiagnostics.runIdent);
|
|
539
544
|
if (files.length > 0) {
|
|
540
545
|
queue.push({ type: "diagnostics", files, isNew: true, ...ident() });
|
|
@@ -550,6 +555,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
|
|
|
550
555
|
rs.attach.sizeGuidelineState !== undefined ||
|
|
551
556
|
rs.attach.attachState !== undefined) &&
|
|
552
557
|
!boundarySteered &&
|
|
558
|
+
prepared.batchHaltRef.current === undefined &&
|
|
553
559
|
rs.counters.finalVerifyInjections === 0 &&
|
|
554
560
|
!prepared.abortController.signal.aborted) {
|
|
555
561
|
const due = [];
|
|
@@ -780,7 +786,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
|
|
|
780
786
|
}
|
|
781
787
|
}
|
|
782
788
|
}
|
|
783
|
-
if (attachmentsPayload !== undefined || batchContextBlock !== undefined) {
|
|
789
|
+
if ((attachmentsPayload !== undefined || batchContextBlock !== undefined) && prepared.batchHaltRef.current === undefined) {
|
|
784
790
|
const payload = attachmentsPayload !== undefined && batchContextBlock !== undefined ? `${attachmentsPayload}\n${batchContextBlock}` : (attachmentsPayload ?? batchContextBlock);
|
|
785
791
|
void prepared.harness.steer(payload, { engineMinted: true }).catch(() => { });
|
|
786
792
|
}
|
|
@@ -1293,7 +1299,7 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
|
|
|
1293
1299
|
reduceToolEnd(rs.attach.attachState, det);
|
|
1294
1300
|
}
|
|
1295
1301
|
if (postToolBatchHook !== undefined) {
|
|
1296
|
-
if (prepared.blockedToolCalls.delete(event.toolCallId)) {
|
|
1302
|
+
if (prepared.blockedToolCalls.delete(event.toolCallId) || event.notExecuted === true) {
|
|
1297
1303
|
batchArgs?.delete(event.toolCallId);
|
|
1298
1304
|
}
|
|
1299
1305
|
else {
|
|
@@ -1338,7 +1344,7 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
|
|
|
1338
1344
|
toolName: event.toolName,
|
|
1339
1345
|
...(toolLabels.get(event.toolName) !== undefined ? { label: toolLabels.get(event.toolName) } : {}),
|
|
1340
1346
|
isError: event.isError,
|
|
1341
|
-
...toolEndBodyFrom(event.result, event.isError, settlement?.settledBy, settlement?.approver),
|
|
1347
|
+
...toolEndBodyFrom(event.result, event.isError, settlement?.settledBy, settlement?.approver, settlement?.resolution),
|
|
1342
1348
|
...ident(),
|
|
1343
1349
|
});
|
|
1344
1350
|
announceWorkspaceMove();
|
|
@@ -2006,7 +2012,7 @@ export class Runner {
|
|
|
2006
2012
|
};
|
|
2007
2013
|
const unsubscribeTaskNotifications = taskNotificationQueue.subscribe((item) => {
|
|
2008
2014
|
queue.push({ type: "task_notification", notification: item.payload, ...notificationIdent() });
|
|
2009
|
-
if (notificationHarness) {
|
|
2015
|
+
if (notificationHarness && prepared.batchHaltRef.current === undefined) {
|
|
2010
2016
|
const xml = renderTaskNotificationXml(item.payload);
|
|
2011
2017
|
const deliver = notificationHarness.steer(xml, { provenance: "engine-note", enginePayload: item.payload });
|
|
2012
2018
|
void deliver.then(() => item.onDisposition?.("queued"), () => {
|
|
@@ -2698,7 +2704,7 @@ export class Runner {
|
|
|
2698
2704
|
if (stopHook || finalVerificationOn) {
|
|
2699
2705
|
let consecutiveBlocks = 0;
|
|
2700
2706
|
prepared.harness.setStopGate(async () => {
|
|
2701
|
-
if (prepared.abortController.signal.aborted || prepared.suspendRef.token !== undefined)
|
|
2707
|
+
if (prepared.abortController.signal.aborted || prepared.suspendRef.token !== undefined || prepared.batchHaltRef.current !== undefined)
|
|
2702
2708
|
return [];
|
|
2703
2709
|
if (finalVerificationOn &&
|
|
2704
2710
|
(rs.counters.finalVerifyInjections === 0 || (rs.counters.finalVerifyInjections === 1 && rs.counters.groundingSignalPreR9 && !rs.counters.groundingSignalPostR9)) &&
|
|
@@ -3480,6 +3486,7 @@ export class Runner {
|
|
|
3480
3486
|
model: prepared.model.id,
|
|
3481
3487
|
unpricedSpend: rs.telemetry.unpricedSpend,
|
|
3482
3488
|
rewindNotes: prepared.rewindNotes,
|
|
3489
|
+
haltedOnUserRejection: prepared.batchHaltRef.current !== undefined,
|
|
3483
3490
|
strandedHumanAnswers,
|
|
3484
3491
|
remoteEnvFailures: prepared.remoteEnvFailures,
|
|
3485
3492
|
effectiveReadFace: prepared.effectiveReadFace,
|