@sema-agent/core 5.34.0 → 5.36.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/dist/agents/subagent.js +29 -2
  3. package/dist/core/auto-compaction.d.ts +23 -0
  4. package/dist/core/auto-compaction.js +8 -0
  5. package/dist/core/checkpoint-store.d.ts +49 -4
  6. package/dist/core/context-guard.d.ts +41 -0
  7. package/dist/core/context-guard.js +76 -0
  8. package/dist/core/hooks.d.ts +98 -3
  9. package/dist/core/hooks.js +146 -8
  10. package/dist/core/memory-engine/engine.js +1 -1
  11. package/dist/core/park-selfcheck.d.ts +161 -0
  12. package/dist/core/park-selfcheck.js +251 -0
  13. package/dist/core/runner/assemble-result.d.ts +3 -0
  14. package/dist/core/runner/assemble-result.js +3 -0
  15. package/dist/core/runner/git-status-frame.d.ts +219 -0
  16. package/dist/core/runner/git-status-frame.js +212 -0
  17. package/dist/core/runner/prepare-acquire-reconcile.d.ts +6 -0
  18. package/dist/core/runner/prepare-acquire-reconcile.js +2 -1
  19. package/dist/core/runner/prepare-task.d.ts +28 -4
  20. package/dist/core/runner/prepare-task.js +86 -52
  21. package/dist/core/runner/runtask.d.ts +6 -1
  22. package/dist/core/runner/runtask.js +330 -19
  23. package/dist/core/task-registry-agent.d.ts +15 -0
  24. package/dist/core/task-registry-agent.js +9 -0
  25. package/dist/core/task-registry.d.ts +3 -0
  26. package/dist/core/task-registry.js +4 -1
  27. package/dist/core/tool-errors.d.ts +2 -2
  28. package/dist/core/tool-policy.d.ts +125 -0
  29. package/dist/core/tool-policy.js +35 -2
  30. package/dist/core/types.d.ts +98 -9
  31. package/dist/engine/harness/types.d.ts +65 -1
  32. package/dist/engine/harness/types.js +20 -0
  33. package/dist/engine/session/import-validate.js +10 -1
  34. package/dist/engine/session/session.d.ts +37 -1
  35. package/dist/engine/session/session.js +56 -1
  36. package/dist/index.d.ts +3 -2
  37. package/dist/index.js +3 -2
  38. package/dist/internal/harness-types.d.ts +1 -0
  39. package/dist/internal/harness.d.ts +2 -0
  40. package/dist/internal/harness.js +2 -0
  41. package/dist/orchestration/workflow.d.ts +1 -1
  42. package/dist/prompt-assembly/epoch.js +1 -1
  43. package/dist/prompt-assembly/event-registry.js +1 -0
  44. package/dist/prompts/default.d.ts +20 -7
  45. package/dist/prompts/default.js +2 -7
  46. package/package.json +1 -1
  47. package/test/export-surface.snapshot.json +17 -1
@@ -2,7 +2,9 @@ import { persistedReadDenyEntryProblem } from "../../tools/fs/read-deny.js";
2
2
  import { createSafeNotifier, observeThenableRejection } from "../safe-notify.js";
3
3
  import { AgentHarness, DEFAULT_COMPACTION_SETTINGS, uuidv7 } from "../../internal/harness.js";
4
4
  import { snapshotActorAssertion } from "../../internal/llm.js";
5
- import { CheckpointError, BINDING_CHECKPOINT_VERSION, checkpointVersionOf, F012_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, FACE_CHECKPOINT_VERSION, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, validatePendingSteer, winnerFromOutcome, } from "../checkpoint-store.js";
5
+ import { CheckpointError, BINDING_CHECKPOINT_VERSION, checkpointVersionOf, F012_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, FACE_CHECKPOINT_VERSION, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, LEGACY_PENDING_STEER_INPUT_ID, MAX_STEER_INPUT_ID_CHARS, validatePendingSteer, winnerFromOutcome, } from "../checkpoint-store.js";
6
+ import { GIT_STATUS_ECHO_PREVIEW, branchCarriesVisiblePositiveGitFrame, newestEngineGitFrame, stripGitStatusUnits } from "./git-status-frame.js";
7
+ import { gitFrameContextVisible, normalizeGitAnnouncement } from "../../internal/harness.js";
6
8
  import { engineVersion } from "../version.js";
7
9
  import { CONFIG_CATALOG_VERSION, declarationReasons, resolveEffectiveConfig } from "../../config/catalog.js";
8
10
  import { eventDefaultOn } from "../../prompt-assembly/event-registry.js";
@@ -41,7 +43,7 @@ import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY, sanitizeUntr
41
43
  import { reconcileInterruptedSession } from "../session-reconcile.js";
42
44
  import { RunnerSharedToolResultStore } from "../tool-result-store.js";
43
45
  import { formatDiagnosticsBlock } from "../lsp-diagnostics.js";
44
- import { checkToolPolicyProjection, constraintChainDigest, constraintChainEntryOf, isApprovalSettledBy, refuseOutOfContractDecision, toolPolicyNameSets } from "../tool-policy.js";
46
+ import { checkToolPolicyProjection, constraintChainDigest, constraintChainEntryOf, isApprovalSettledBy, refuseOutOfContractDecision, screenApproverAttribution, toolPolicyNameSets } from "../tool-policy.js";
45
47
  import { defaultTaskRegistry } from "../task-registry.js";
46
48
  import { discloseDroppedPending, isDelegatedAgentTerminal, PendingSessionNotifications, renderTaskNotificationXml, SystemInjectionQueue, taskNotificationDedupKey } from "../task-notification.js";
47
49
  import { ToolDetachHub } from "../tool-detach.js";
@@ -68,6 +70,13 @@ function nextHumanInputSeq(key) {
68
70
  }
69
71
  return ++box.n;
70
72
  }
73
+ function sameAcceptedSteerInput(a, b) {
74
+ return (a.payload === b.payload &&
75
+ a.trusted === b.trusted &&
76
+ a.actor?.id === b.actor?.id &&
77
+ a.actor?.hostAsserted === b.actor?.hostAsserted &&
78
+ a.actor?.issuer === b.actor?.issuer);
79
+ }
71
80
  const MAX_CONSECUTIVE_COMPACTION_FAILURES = 3;
72
81
  const STOP_HOOK_BLOCK_CAP = 8;
73
82
  const COMPACTION_REGROWTH_FACTOR = 1.5;
@@ -130,7 +139,7 @@ function resumeDecisionWasNegative(resume) {
130
139
  }
131
140
  const DEFERRED_REISSUE = "[DEFERRED] This tool call shared a batch with a call that suspended for durable approval, so it was " +
132
141
  "NOT executed on resume. If you still need it, issue it again now.";
133
- function toolEndBodyFrom(result, isError, settledBy) {
142
+ function toolEndBodyFrom(result, isError, settledBy, approver) {
134
143
  const o = toolOutputFrom(result);
135
144
  const st = structuredFrom(result);
136
145
  const det = isError ? result?.details : undefined;
@@ -142,6 +151,7 @@ function toolEndBodyFrom(result, isError, settledBy) {
142
151
  ...(st !== undefined ? { structured: st } : {}),
143
152
  ...(typeof code === "string" ? { errorCode: code } : {}),
144
153
  ...(settledBy !== undefined ? { settledBy } : {}),
154
+ ...(approver !== undefined ? { approver } : {}),
145
155
  };
146
156
  }
147
157
  export function reconciledToolEndBody(orphan) {
@@ -860,6 +870,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
860
870
  workingFileAttachments: buildWorkingFileAttachments(spec, prepared),
861
871
  ...(prepared.onCompactionApplied ? { onApplied: prepared.onCompactionApplied } : {}),
862
872
  ...runnerHooks.seamCCompactionOptions(prepared),
873
+ ...gitRestateOption(prepared),
863
874
  ...windowSafetyOptions(event.model),
864
875
  ...runnerHooks.compactionHookOptions(spec, prepared.sessionId, passTrigger),
865
876
  });
@@ -988,6 +999,96 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
988
999
  };
989
1000
  return onTurnBoundary;
990
1001
  }
1002
+ function wrapGitFrame(body) {
1003
+ return `<system-reminder>\n${sanitizeUntrustedText(body)}\n</system-reminder>`;
1004
+ }
1005
+ async function resolveGitLegDelivery(prepared, cpMirror, report) {
1006
+ const ref = prepared.gitStatusRef;
1007
+ const frame = ref.frame;
1008
+ if (frame === undefined)
1009
+ return undefined;
1010
+ let prior;
1011
+ let pending = false;
1012
+ try {
1013
+ const branchReadout = await prepared.session.getGitAnnouncement?.();
1014
+ if (branchReadout !== undefined) {
1015
+ if (branchReadout.status === "pending") {
1016
+ pending = true;
1017
+ prior = { kind: branchReadout.kind, hash: branchReadout.hash };
1018
+ }
1019
+ else {
1020
+ prior = { kind: branchReadout.kind, hash: branchReadout.hash, ...(branchReadout.entryId !== undefined ? { entryId: branchReadout.entryId } : {}) };
1021
+ }
1022
+ }
1023
+ else {
1024
+ const shaped = normalizeGitAnnouncement(cpMirror);
1025
+ if (shaped !== undefined) {
1026
+ if (shaped.pending === true || shaped.entryId === undefined) {
1027
+ pending = true;
1028
+ prior = { kind: shaped.kind, hash: shaped.hash };
1029
+ }
1030
+ else if (gitFrameContextVisible(await prepared.session.getBranch(), shaped.entryId)) {
1031
+ prior = { kind: shaped.kind, hash: shaped.hash, entryId: shaped.entryId };
1032
+ }
1033
+ else {
1034
+ pending = true;
1035
+ prior = { kind: shaped.kind, hash: shaped.hash };
1036
+ }
1037
+ }
1038
+ }
1039
+ }
1040
+ catch (err) {
1041
+ pending = true;
1042
+ report(err instanceof Error ? err : new Error(String(err)));
1043
+ }
1044
+ if (prior !== undefined && !pending && prior.entryId !== undefined) {
1045
+ try {
1046
+ const newest = newestEngineGitFrame(await prepared.session.getBranch());
1047
+ if (newest === undefined || newest.entryId !== prior.entryId)
1048
+ pending = true;
1049
+ }
1050
+ catch (err) {
1051
+ pending = true;
1052
+ report(err instanceof Error ? err : new Error(String(err)));
1053
+ }
1054
+ }
1055
+ if (prior !== undefined)
1056
+ ref.announced = pending ? { ...prior, pending: true } : { ...prior };
1057
+ const negative = frame.kind === "unavailable" || frame.kind === "non-repo";
1058
+ if (negative) {
1059
+ if (prior === undefined && !pending) {
1060
+ let mustDisown;
1061
+ try {
1062
+ mustDisown = branchCarriesVisiblePositiveGitFrame(await prepared.session.getBranch());
1063
+ }
1064
+ catch (err) {
1065
+ mustDisown = true;
1066
+ report(err instanceof Error ? err : new Error(String(err)));
1067
+ }
1068
+ return mustDisown ? frame.body : undefined;
1069
+ }
1070
+ if (prior !== undefined && !pending && prior.kind === frame.kind && prior.hash === frame.hash)
1071
+ return undefined;
1072
+ return frame.body;
1073
+ }
1074
+ if (!pending && prior !== undefined && prior.kind === frame.kind && prior.hash === frame.hash) {
1075
+ ref.protectedText = wrapGitFrame(frame.body);
1076
+ if (frame.kind === "full" && frame.shrunk !== undefined) {
1077
+ ref.wrappedShrink = { find: ref.protectedText, replace: wrapGitFrame(frame.shrunk.body) };
1078
+ }
1079
+ return undefined;
1080
+ }
1081
+ return frame.body;
1082
+ }
1083
+ function gitRestateOption(prepared) {
1084
+ const ref = prepared.gitStatusRef;
1085
+ if (ref.frame === undefined || ref.announced === undefined || ref.reassert === undefined)
1086
+ return {};
1087
+ const use = ref.overBudgetShrunk && ref.frame.shrunk !== undefined
1088
+ ? { kind: "degraded", hash: ref.frame.shrunk.hash }
1089
+ : { kind: ref.frame.kind, hash: ref.frame.hash };
1090
+ return { gitRestate: { pending: use, land: ref.reassert } };
1091
+ }
991
1092
  function makeHarnessHandlers(prepared, stats, rs, deps) {
992
1093
  const { spec, queue, internals, ident, parentToolCallId, subagentName, pushContent, emitCommitted, startedToolCallIds, toolStartAt, writeFamilyOf, toolLabels, postToolBatchHook, batchArgs } = deps;
993
1094
  const internalsNotifier = createSafeNotifier({
@@ -1032,6 +1133,19 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
1032
1133
  if (event.entryId !== undefined && (m.role === "user" || m.role === "assistant" || m.role === "toolResult")) {
1033
1134
  emitCommitted(event.entryId, m.role, m.role === "toolResult" ? m.toolCallId : undefined);
1034
1135
  }
1136
+ if (event.entryId !== undefined && m.role === "user" && prepared.gitStatusRef.pendingReceipt !== undefined) {
1137
+ const pr = prepared.gitStatusRef.pendingReceipt;
1138
+ const c = m.content;
1139
+ const text = typeof c === "string"
1140
+ ? c
1141
+ : Array.isArray(c) && c.length >= 1 && c[0].type === "text"
1142
+ ? c[0].text
1143
+ : undefined;
1144
+ if (text === pr.text) {
1145
+ delete prepared.gitStatusRef.pendingReceipt;
1146
+ pr.commit(event.entryId);
1147
+ }
1148
+ }
1035
1149
  if (rs.degrade.degraded === undefined && m.role === "assistant") {
1036
1150
  const deg = readDegradation(m);
1037
1151
  if (deg)
@@ -1213,16 +1327,16 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
1213
1327
  prepared.lspDiagnostics.nudge(p);
1214
1328
  }
1215
1329
  }
1216
- const settledBy = prepared.approvalSettledBy.get(event.toolCallId);
1217
- if (settledBy !== undefined)
1218
- prepared.approvalSettledBy.delete(event.toolCallId);
1330
+ const settlement = prepared.approvalSettlement.get(event.toolCallId);
1331
+ if (settlement !== undefined)
1332
+ prepared.approvalSettlement.delete(event.toolCallId);
1219
1333
  pushContent({
1220
1334
  type: "tool_end",
1221
1335
  toolCallId: event.toolCallId,
1222
1336
  toolName: event.toolName,
1223
1337
  ...(toolLabels.get(event.toolName) !== undefined ? { label: toolLabels.get(event.toolName) } : {}),
1224
1338
  isError: event.isError,
1225
- ...toolEndBodyFrom(event.result, event.isError, settledBy),
1339
+ ...toolEndBodyFrom(event.result, event.isError, settlement?.settledBy, settlement?.approver),
1226
1340
  ...ident(),
1227
1341
  });
1228
1342
  announceWorkspaceMove();
@@ -1253,6 +1367,7 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
1253
1367
  type: "task_progress",
1254
1368
  taskId: rs.telemetry.taskId,
1255
1369
  ...(internals?.delegationTaskType !== undefined ? { taskType: internals.delegationTaskType } : {}),
1370
+ ...(internals?.cycleSeq !== undefined ? { seq: internals.cycleSeq } : {}),
1256
1371
  ...(internals?.parentTaskId !== undefined ? { parentTaskId: internals.parentTaskId } : {}),
1257
1372
  ...(subagentName ? { name: subagentName } : {}),
1258
1373
  usage: { totalTokens: stats.tokens, toolUses: stats.toolCalls, durationMs: Date.now() - rs.telemetry.taskStart },
@@ -1444,6 +1559,7 @@ export class Runner {
1444
1559
  };
1445
1560
  let reapHandle;
1446
1561
  let steerChain = Promise.resolve();
1562
+ const acceptedSteerInputs = new Map();
1447
1563
  const notifyRef = {};
1448
1564
  const manualCompactRef = { requested: false, waiters: [] };
1449
1565
  const drainManualCompactWaiters = (outcome) => {
@@ -1565,6 +1681,7 @@ export class Runner {
1565
1681
  queue.push({ type: "done", result: resultValue });
1566
1682
  queue.close();
1567
1683
  });
1684
+ void settled.then(() => acceptedSteerInputs.clear(), () => acceptedSteerInputs.clear());
1568
1685
  const steeringError = (msg, code = "steering.not_running") => {
1569
1686
  const e = new Error(`cannot steer: ${msg}`);
1570
1687
  e.code = code;
@@ -1613,22 +1730,40 @@ export class Runner {
1613
1730
  return suggestionsDone.catch(() => []);
1614
1731
  },
1615
1732
  steer: async (text, options) => {
1616
- if (options?.trusted && sanitizeUntrustedText(text) !== text) {
1733
+ const trusted = options?.trusted ? true : false;
1734
+ if (trusted && sanitizeUntrustedText(text) !== text) {
1617
1735
  throw steeringError("trusted steering text must not contain a </system-reminder> tag", "steering.invalid_content");
1618
1736
  }
1619
- const actor = options?.actor === undefined ? undefined : snapshotActorAssertion(options.actor);
1737
+ const inputId = options?.inputId;
1738
+ if (inputId !== undefined) {
1739
+ if (typeof inputId !== "string") {
1740
+ throw steeringError("inputId must be a string when supplied", "steering.invalid_content");
1741
+ }
1742
+ if (inputId === "" || inputId.length > MAX_STEER_INPUT_ID_CHARS) {
1743
+ throw steeringError(`inputId must be a non-empty string of at most ${MAX_STEER_INPUT_ID_CHARS} characters`, "steering.invalid_content");
1744
+ }
1745
+ if (inputId === LEGACY_PENDING_STEER_INPUT_ID) {
1746
+ throw steeringError(`inputId "${LEGACY_PENDING_STEER_INPUT_ID}" is reserved for a pre-queue parked steer and cannot be supplied by a caller`, "steering.invalid_content");
1747
+ }
1748
+ }
1749
+ const actorIn = options?.actor;
1750
+ const actor = actorIn === undefined ? undefined : snapshotActorAssertion(actorIn);
1620
1751
  const projected = projectHumanInput({ text, actor, source: "steer" });
1621
- const payload = options?.trusted ? formatHookFeedback(projected) : projected;
1752
+ const payload = trusted ? formatHookFeedback(projected) : projected;
1622
1753
  const mintsAFrame = payload.trim().length !== 0;
1623
- const emitAccepted = (h) => {
1754
+ const replay = { payload, trusted, ...(actor !== undefined ? { actor } : {}) };
1755
+ const noteAccepted = (h) => {
1624
1756
  if (!mintsAFrame)
1625
1757
  return;
1758
+ if (typeof inputId === "string")
1759
+ acceptedSteerInputs.set(inputId, replay);
1626
1760
  queue.push({
1627
1761
  ...buildHumanInputEvent({
1628
1762
  carrier: "steer",
1629
1763
  source: "steer",
1630
1764
  delivery: "queued",
1631
1765
  sessionSeq: nextHumanInputSeq(h.harness),
1766
+ ...(typeof inputId === "string" ? { inputId } : {}),
1632
1767
  ...(actor !== undefined ? { actor } : {}),
1633
1768
  ...(actor?.issuer !== undefined ? { issuer: actor.issuer } : {}),
1634
1769
  ...(spec.principal !== undefined ? { principal: spec.principal } : {}),
@@ -1645,9 +1780,21 @@ export class Runner {
1645
1780
  const h = handle ?? (await orTimeout(ready));
1646
1781
  if (!h)
1647
1782
  throw steeringError("the task is not running");
1783
+ if (typeof inputId === "string") {
1784
+ const prior = acceptedSteerInputs.get(inputId);
1785
+ if (prior !== undefined) {
1786
+ if (resultValue !== undefined || h.loop.ended)
1787
+ throw steeringError("the task is no longer running");
1788
+ if (!sameAcceptedSteerInput(prior, replay)) {
1789
+ throw steeringError("a different steering instruction was already accepted under this inputId — re-issue this one with a fresh inputId " +
1790
+ "(an identical payload would have been an idempotent retry)", "steering.duplicate_input_id");
1791
+ }
1792
+ return;
1793
+ }
1794
+ }
1648
1795
  try {
1649
1796
  await h.harness.steer(payload, { provenance: "engine-note", ...(actor !== undefined ? { actor } : {}) });
1650
- emitAccepted(h);
1797
+ noteAccepted(h);
1651
1798
  return;
1652
1799
  }
1653
1800
  catch (e) {
@@ -1658,7 +1805,7 @@ export class Runner {
1658
1805
  while (resultValue === undefined && !h.loop.ended && Date.now() < birthDeadline) {
1659
1806
  try {
1660
1807
  await h.harness.steer(payload, { provenance: "engine-note", ...(actor !== undefined ? { actor } : {}) });
1661
- emitAccepted(h);
1808
+ noteAccepted(h);
1662
1809
  return;
1663
1810
  }
1664
1811
  catch (e2) {
@@ -2135,12 +2282,12 @@ export class Runner {
2135
2282
  return [];
2136
2283
  const full = textOf(e.message);
2137
2284
  if (m.engineMinted === true)
2138
- return [full];
2285
+ return [stripGitStatusUnits(full)];
2139
2286
  if (Array.isArray(m.engineSegments) && m.engineSegments.length > 0) {
2140
- return m.engineSegments.map((s) => full.slice(Math.max(0, s.start), Math.max(0, s.end)));
2287
+ return m.engineSegments.map((s) => stripGitStatusUnits(full.slice(Math.max(0, s.start), Math.max(0, s.end))));
2141
2288
  }
2142
2289
  if (typeof m.enginePrefixChars === "number" && m.enginePrefixChars > 0)
2143
- return [full.slice(0, m.enginePrefixChars)];
2290
+ return [stripGitStatusUnits(full.slice(0, m.enginePrefixChars))];
2144
2291
  return [];
2145
2292
  });
2146
2293
  }
@@ -2652,6 +2799,7 @@ export class Runner {
2652
2799
  workingFileAttachments: buildWorkingFileAttachments(spec, prepared),
2653
2800
  ...(prepared.onCompactionApplied ? { onApplied: prepared.onCompactionApplied } : {}),
2654
2801
  ...this.seamCCompactionOptions(prepared),
2802
+ ...gitRestateOption(prepared),
2655
2803
  ...windowSafetyOptions(prepared.harness.getModel()),
2656
2804
  ...this.compactionHookOptions(spec, prepared.sessionId, "forced"),
2657
2805
  });
@@ -2722,6 +2870,80 @@ export class Runner {
2722
2870
  recordCompactionReuse: (p, c) => this.recordCompactionReuse(p, c),
2723
2871
  },
2724
2872
  });
2873
+ prepared.gitStatusRef.reassert = async () => {
2874
+ const ref = prepared.gitStatusRef;
2875
+ const frame = ref.frame;
2876
+ if (frame === undefined)
2877
+ return;
2878
+ delete ref.mirrorOwed;
2879
+ const use = ref.overBudgetShrunk && frame.shrunk !== undefined
2880
+ ? { kind: "degraded", body: frame.shrunk.body, hash: frame.shrunk.hash }
2881
+ : { kind: frame.kind, body: frame.body, hash: frame.hash };
2882
+ const wrapped = wrapGitFrame(use.body);
2883
+ try {
2884
+ const entryId = await prepared.session.appendMessage({
2885
+ role: "user",
2886
+ content: [{ type: "text", text: wrapped }],
2887
+ timestamp: Date.now(),
2888
+ engineMinted: true,
2889
+ });
2890
+ ref.announced = { kind: use.kind, hash: use.hash, entryId };
2891
+ ref.protectedText = wrapped;
2892
+ if (use.kind === "full" && frame.shrunk !== undefined) {
2893
+ ref.wrappedShrink = { find: wrapped, replace: wrapGitFrame(frame.shrunk.body) };
2894
+ }
2895
+ else {
2896
+ delete ref.wrappedShrink;
2897
+ }
2898
+ queue.push({ type: "steering_injected", source: "git_status", preview: GIT_STATUS_ECHO_PREVIEW[use.kind], ...ident() });
2899
+ rs.attach.attachmentsInjected += 1;
2900
+ try {
2901
+ await prepared.session.appendGitAnnouncement?.({ kind: use.kind, hash: use.hash, entryId });
2902
+ }
2903
+ catch (mirrorErr) {
2904
+ try {
2905
+ this.deps.onError?.(new Error(`git announcement mirror write failed (frame delivered; next leg re-announces): ${mirrorErr instanceof Error ? mirrorErr.message : String(mirrorErr)}`), { phase: "degraded", sessionId: prepared.sessionId });
2906
+ }
2907
+ catch {
2908
+ }
2909
+ }
2910
+ }
2911
+ catch (err) {
2912
+ ref.announced = { kind: use.kind, hash: use.hash, pending: true };
2913
+ try {
2914
+ this.deps.onError?.(new Error(`git status frame re-assert append failed (pending; retried at the next boundary): ${err instanceof Error ? err.message : String(err)}`), { phase: "degraded", sessionId: prepared.sessionId });
2915
+ }
2916
+ catch {
2917
+ }
2918
+ }
2919
+ };
2920
+ const flushGitMirror = async () => {
2921
+ const owed = prepared.gitStatusRef.mirrorOwed;
2922
+ if (owed === undefined)
2923
+ return;
2924
+ if (prepared.gitStatusRef.overBudgetShrunk && owed.kind === "full") {
2925
+ delete prepared.gitStatusRef.mirrorOwed;
2926
+ return;
2927
+ }
2928
+ delete prepared.gitStatusRef.mirrorOwed;
2929
+ try {
2930
+ await prepared.session.appendGitAnnouncement?.(owed);
2931
+ }
2932
+ catch (mirrorErr) {
2933
+ prepared.gitStatusRef.mirrorOwed = owed;
2934
+ try {
2935
+ this.deps.onError?.(new Error(`git announcement mirror write failed (frame delivered; retried at the next serialization point): ${mirrorErr instanceof Error ? mirrorErr.message : String(mirrorErr)}`), { phase: "degraded", sessionId: prepared.sessionId });
2936
+ }
2937
+ catch {
2938
+ }
2939
+ }
2940
+ };
2941
+ const unsubGitRetry = prepared.harness.on("turn_boundary", async () => {
2942
+ await flushGitMirror();
2943
+ if (prepared.gitStatusRef.announced?.pending === true)
2944
+ await prepared.gitStatusRef.reassert?.();
2945
+ return undefined;
2946
+ });
2725
2947
  const unsubBoundary = prepared.harness.on("turn_boundary", onTurnBoundary);
2726
2948
  const unsub = prepared.harness.subscribe((event) => {
2727
2949
  switch (event.type) {
@@ -2799,6 +3021,25 @@ export class Runner {
2799
3021
  continuation += "\n\n" + formatHookFeedback(renderOrphanedBackgroundTasks(orphans));
2800
3022
  }
2801
3023
  }
3024
+ let gitResumeDelivered;
3025
+ {
3026
+ const gitBody = await resolveGitLegDelivery(prepared, resume.cp.state.gitAnnouncement, (err) => {
3027
+ try {
3028
+ this.deps.onError?.(new Error(`git announcement ladder unreadable (conservative re-announce): ${err.message}`), { phase: "degraded", sessionId: prepared.sessionId });
3029
+ }
3030
+ catch {
3031
+ }
3032
+ });
3033
+ const gitFrame = prepared.gitStatusRef.frame;
3034
+ if (gitBody !== undefined && gitFrame !== undefined) {
3035
+ const wrappedGit = wrapGitFrame(gitBody);
3036
+ continuation += "\n\n" + wrappedGit;
3037
+ gitResumeDelivered = { kind: gitFrame.kind, hash: gitFrame.hash, wrapped: wrappedGit };
3038
+ if (gitFrame.kind === "full" && gitFrame.shrunk !== undefined) {
3039
+ prepared.gitStatusRef.wrappedShrink = { find: wrappedGit, replace: wrapGitFrame(gitFrame.shrunk.body) };
3040
+ }
3041
+ }
3042
+ }
2802
3043
  const engineSegments = continuation.length > 0 ? [{ start: 0, end: continuation.length }] : [];
2803
3044
  const resumeFrames = [
2804
3045
  ...readPendingSteerQueue(resume.cp.state).map((entry) => ({ entry, source: "steer" })),
@@ -2828,7 +3069,23 @@ export class Runner {
2828
3069
  }
2829
3070
  if (resume !== undefined)
2830
3071
  resume.decisionDelivered = true;
3072
+ if (gitResumeDelivered !== undefined) {
3073
+ const delivered = gitResumeDelivered;
3074
+ prepared.gitStatusRef.protectedText = delivered.wrapped;
3075
+ prepared.gitStatusRef.pendingReceipt = {
3076
+ text: continuation,
3077
+ commit: (entryId) => {
3078
+ prepared.gitStatusRef.announced = { kind: delivered.kind, hash: delivered.hash, entryId };
3079
+ queue.push({ type: "steering_injected", source: "git_status", preview: GIT_STATUS_ECHO_PREVIEW[delivered.kind], ...ident() });
3080
+ rs.attach.attachmentsInjected += 1;
3081
+ prepared.gitStatusRef.mirrorOwed = { kind: delivered.kind, hash: delivered.hash, entryId };
3082
+ },
3083
+ };
3084
+ }
2831
3085
  final = await withBrainSinks(() => prepared.harness.prompt(continuation, { engineSegments }));
3086
+ await flushGitMirror();
3087
+ if (prepared.gitStatusRef.announced?.pending === true)
3088
+ await prepared.gitStatusRef.reassert?.();
2832
3089
  }
2833
3090
  }
2834
3091
  else {
@@ -2860,6 +3117,27 @@ export class Runner {
2860
3117
  promptBlocked = true;
2861
3118
  }
2862
3119
  }
3120
+ let gitLegDelivered;
3121
+ if (!promptBlocked) {
3122
+ const gitBody = await resolveGitLegDelivery(prepared, undefined, (err) => {
3123
+ try {
3124
+ this.deps.onError?.(new Error(`git announcement ladder unreadable (conservative re-announce): ${err.message}`), { phase: "degraded", sessionId: prepared.sessionId });
3125
+ }
3126
+ catch {
3127
+ }
3128
+ });
3129
+ const gitFrame = prepared.gitStatusRef.frame;
3130
+ if (gitBody !== undefined && gitFrame !== undefined) {
3131
+ const wrappedGit = wrapGitFrame(gitBody);
3132
+ const standalone = spec.images !== undefined && spec.images.length > 0;
3133
+ if (!standalone)
3134
+ effectiveObjective = `${wrappedGit}\n${effectiveObjective}`;
3135
+ gitLegDelivered = { kind: gitFrame.kind, hash: gitFrame.hash, standalone, wrapped: wrappedGit };
3136
+ if (gitFrame.kind === "full" && gitFrame.shrunk !== undefined) {
3137
+ prepared.gitStatusRef.wrappedShrink = { find: wrappedGit, replace: wrapGitFrame(gitFrame.shrunk.body) };
3138
+ }
3139
+ }
3140
+ }
2863
3141
  const firstFrames = [];
2864
3142
  if (rs.attach.attachState !== undefined) {
2865
3143
  const attach = rs.attach.attachState;
@@ -2897,17 +3175,18 @@ export class Runner {
2897
3175
  effectiveObjective = `${firstFrames.map((f) => `<system-reminder>\n${sanitizeUntrustedText(f.body)}\n</system-reminder>`).join("\n")}\n${effectiveObjective}`;
2898
3176
  }
2899
3177
  }
3178
+ const gitQueuedChars = gitLegDelivered !== undefined && gitLegDelivered.standalone ? gitLegDelivered.wrapped.length : 0;
2900
3179
  const precallMicroUsd = rs.budget.maxCostMicroUsd === undefined
2901
3180
  ? 0
2902
3181
  : computeCostMicroUsd({
2903
- totalInputTokens: Math.ceil(effectiveObjective.length / 4),
3182
+ totalInputTokens: Math.ceil((effectiveObjective.length + gitQueuedChars) / 4),
2904
3183
  cacheReadTokens: 0,
2905
3184
  cacheWriteTokens: 0,
2906
3185
  cacheWriteTokensLong: 0,
2907
3186
  outputTokens: prepared.model.maxTokens ?? DEFAULT_PRECALL_OUTPUT_TOKENS,
2908
3187
  }, rs.telemetry.pricing);
2909
3188
  const precallCeilingMicroUsd = prepared.suspendForResource !== undefined ? rs.budget.remainingMicroUsd : rs.budget.maxCostMicroUsd;
2910
- const precallTokens = Math.ceil(effectiveObjective.length / 4) + (prepared.model.maxTokens ?? DEFAULT_PRECALL_OUTPUT_TOKENS);
3189
+ const precallTokens = Math.ceil((effectiveObjective.length + gitQueuedChars) / 4) + (prepared.model.maxTokens ?? DEFAULT_PRECALL_OUTPUT_TOKENS);
2911
3190
  const precallCeilingTokens = prepared.suspendForResource !== undefined ? rs.budget.remainingTokens : rs.budget.maxTokensWindow;
2912
3191
  const entryUsageRetryAfterMs = prepared.usageGovernance !== undefined ? await prepared.usageGovernance.check(Date.now()) : undefined;
2913
3192
  if (promptBlocked) {
@@ -2939,6 +3218,21 @@ export class Runner {
2939
3218
  const images = spec.images
2940
3219
  ? await Promise.all(spec.images.map((img) => toImageContent(img, this.deps.allowImageUrl)))
2941
3220
  : undefined;
3221
+ if (gitLegDelivered !== undefined) {
3222
+ const delivered = gitLegDelivered;
3223
+ if (delivered.standalone)
3224
+ await prepared.harness.nextTurn(delivered.wrapped, { engineMinted: true });
3225
+ prepared.gitStatusRef.protectedText = delivered.wrapped;
3226
+ prepared.gitStatusRef.pendingReceipt = {
3227
+ text: delivered.standalone ? delivered.wrapped : effectiveObjective,
3228
+ commit: (entryId) => {
3229
+ prepared.gitStatusRef.announced = { kind: delivered.kind, hash: delivered.hash, entryId };
3230
+ queue.push({ type: "steering_injected", source: "git_status", preview: GIT_STATUS_ECHO_PREVIEW[delivered.kind], ...ident() });
3231
+ rs.attach.attachmentsInjected += 1;
3232
+ prepared.gitStatusRef.mirrorOwed = { kind: delivered.kind, hash: delivered.hash, entryId };
3233
+ },
3234
+ };
3235
+ }
2942
3236
  for (const f of firstFrames) {
2943
3237
  f.commit();
2944
3238
  queue.push({ type: "steering_injected", source: f.source, preview: f.body.slice(0, 220), ...ident() });
@@ -2964,6 +3258,9 @@ export class Runner {
2964
3258
  ...(enginePrefixChars > 0 ? { enginePrefixChars } : {}),
2965
3259
  ...(objectiveActor !== undefined ? { actor: objectiveActor } : {}),
2966
3260
  }));
3261
+ await flushGitMirror();
3262
+ if (prepared.gitStatusRef.announced?.pending === true)
3263
+ await prepared.gitStatusRef.reassert?.();
2967
3264
  }
2968
3265
  }
2969
3266
  loopLatch.ended = true;
@@ -2993,6 +3290,7 @@ export class Runner {
2993
3290
  }
2994
3291
  notificationLaneLive = false;
2995
3292
  unsubscribeTaskNotifications();
3293
+ unsubGitRetry();
2996
3294
  unsubBoundary();
2997
3295
  unsub();
2998
3296
  }
@@ -3136,6 +3434,7 @@ export class Runner {
3136
3434
  budgetAxis: rs.limits.budgetAxis,
3137
3435
  blockedReason: prepared.blockedRef.reason,
3138
3436
  conflict: prepared.conflictRef.hit,
3437
+ gitCoreOverBudget: prepared.gitStatusRef.terminalCode === "irreducible_core_over_budget",
3139
3438
  outputInvalid: rs.degrade.outputInvalid,
3140
3439
  suspendLoop: prepared.suspendLoopRef.hit,
3141
3440
  suspendRef: prepared.suspendRef.token !== undefined && prepared.suspendRef.gate !== undefined
@@ -3264,6 +3563,7 @@ export class Runner {
3264
3563
  type: "task_progress",
3265
3564
  taskId: rs.telemetry.taskId,
3266
3565
  ...(internals?.delegationTaskType !== undefined ? { taskType: internals.delegationTaskType } : {}),
3566
+ ...(internals?.cycleSeq !== undefined ? { seq: internals.cycleSeq } : {}),
3267
3567
  ...(internals?.parentTaskId !== undefined ? { parentTaskId: internals.parentTaskId } : {}),
3268
3568
  ...(subagentName ? { name: subagentName } : {}),
3269
3569
  usage: { totalTokens: stats.tokens, toolUses: stats.toolCalls, durationMs: Date.now() - rs.telemetry.taskStart },
@@ -3664,6 +3964,15 @@ export class Runner {
3664
3964
  throw new CheckpointError("checkpoint.invalid_outcome", `resume carries decision "allow" with settledBy "${settledBy}" — a non-human settlement is a fail-closed end (nobody answered), so it cannot be the source of an approval that EXECUTES; ` +
3665
3965
  "supply \"human\", or omit the field if the allow came from configuration rather than a person; refusing pre-CAS, the checkpoint stays pending", { field: "settledBy" });
3666
3966
  }
3967
+ const approver = decide.approver;
3968
+ const attribution = screenApproverAttribution(approver);
3969
+ if (attribution.defect !== undefined) {
3970
+ throw new CheckpointError("checkpoint.invalid_outcome", `resume carries an approver attribution this engine refuses: ${attribution.defect}; refusing pre-CAS, the checkpoint stays pending`, { field: "approver" });
3971
+ }
3972
+ if (attribution.approver !== undefined && settledBy === undefined) {
3973
+ throw new CheckpointError("checkpoint.invalid_outcome", `resume names an approver but no settledBy — an attribution says WHOSE settlement it was and needs the word that says what KIND of end it was beside it; ` +
3974
+ `supply settledBy ("human" / "timeout" / "aborted"), or omit the approver; refusing pre-CAS, the checkpoint stays pending`, { field: "approver" });
3975
+ }
3667
3976
  const reason = decide.reason;
3668
3977
  assertOutcomeText(reason, "reason");
3669
3978
  plainPolicyOutcome = {
@@ -3675,6 +3984,7 @@ export class Runner {
3675
3984
  ...(reason !== undefined ? { reason } : {}),
3676
3985
  ...(redeemedAnswer !== undefined ? { answer: redeemedAnswer } : {}),
3677
3986
  ...(settledBy !== undefined ? { settledBy } : {}),
3987
+ ...(attribution.approver !== undefined ? { approver: attribution.approver } : {}),
3678
3988
  };
3679
3989
  if (plainPolicyOutcome.decision === "deny" && plainPolicyOutcome.reason && sanitizeUntrustedText(plainPolicyOutcome.reason) !== plainPolicyOutcome.reason) {
3680
3990
  throw new CheckpointError("checkpoint.invalid_outcome", "resume deny/reject reason must not contain a </system-reminder> tag");
@@ -4037,7 +4347,7 @@ export class Runner {
4037
4347
  const resolvedArgs = pendingAction.toolName === ASK_USER_QUESTION_TOOL_NAME ? pendingAction.args : (outcome.updatedInput !== undefined ? outcome.updatedInput : pendingAction.args);
4038
4348
  const pendingLabel = (() => { const l = prepared.tools.find((t) => t.name === pendingAction.toolName)?.label; return l !== undefined && l !== pendingAction.toolName ? { label: l } : {}; })();
4039
4349
  emit({ type: "tool_start", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, args: resolvedArgs });
4040
- const emitEnd = (isError, result) => emit({ type: "tool_end", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, isError, ...toolEndBodyFrom(result, isError, outcome.settledBy) });
4350
+ const emitEnd = (isError, result) => emit({ type: "tool_end", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, isError, ...toolEndBodyFrom(result, isError, outcome.settledBy, outcome.approver) });
4041
4351
  if (outcome.decision === "deny") {
4042
4352
  const defaultDenial = outcome.settledBy === "timeout"
4043
4353
  ? `No one answered the approval request for the pending tool call "${pendingAction.toolName}" — the approval window elapsed with no answer, so it was not executed.`
@@ -4266,6 +4576,7 @@ export class Runner {
4266
4576
  workingFileAttachments: buildWorkingFileAttachments(spec, prepared),
4267
4577
  ...(prepared.onCompactionApplied ? { onApplied: prepared.onCompactionApplied } : {}),
4268
4578
  ...this.seamCCompactionOptions(prepared),
4579
+ ...gitRestateOption(prepared),
4269
4580
  ...this.compactionHookOptions(spec, prepared.sessionId, "auto"),
4270
4581
  });
4271
4582
  this.recordCompactionReuse(prepared, finishComp);
@@ -275,6 +275,21 @@ export declare function resolveBackgroundAgentByNameLane(core: DurableAgentCore,
275
275
  * alone does NOT retain the child session, so declaring at register time acknowledged parks that
276
276
  * a capacity/pin failure (or plain session-scope) could never drain. No-op on unknown ids. */
277
277
  export declare function markRetainedContinuationLane(core: DurableAgentCore, id: string): void;
278
+ /**
279
+ * #258 — the stop-cycle generation of the RUN BEING SPAWNED under this row (the same counter the
280
+ * register/revive lanes keep on the handle: fresh spawn = 1, durable-seeded revive = the claimed
281
+ * row's seq, in-memory wake = the bump). The spawn lanes read it ONCE, right after registering, to
282
+ * thread into the child's `RunInternals.cycleSeq` — one authoritative source instead of each lane
283
+ * re-deriving the seed-vs-fresh arithmetic. `undefined` for an unknown id or a non-agent row.
284
+ *
285
+ * PARKED-BORN arm (falsification F1): a parked handle's counter still names the cycle that PARKED —
286
+ * the run this spawn lane is about to drive begins at the consume flip, which installs the bumped
287
+ * value (`flipped.seq = seq + 1`, the §7.2d consume write). Answering the pre-flip number made one
288
+ * resumed run speak two generations (spawn/ticks at n, terminal at n+1), so the parked arm answers
289
+ * the flip's target instead. A resume that loses the consume arbitration never runs, so the answer
290
+ * is never attached to a live cycle it doesn't describe.
291
+ */
292
+ export declare function backgroundAgentCycleSeqLane(core: DurableAgentCore, id: string): number | undefined;
278
293
  /** S2b RB-27② — REVIVE a settled background-agent row for a retained-session RESUME cycle: the
279
294
  * row flips back to "running" with a fresh "attaching" channel and a bumped revive-cycle stamp
280
295
  * (returned; the resume leg threads it through attach and settle so a stale cycle's late calls
@@ -867,6 +867,12 @@ export function markRetainedContinuationLane(core, id) {
867
867
  if (handle && handle.type === "background_agent")
868
868
  handle.retainedContinuation = true;
869
869
  }
870
+ export function backgroundAgentCycleSeqLane(core, id) {
871
+ const handle = core.handles.get(id);
872
+ if (handle === undefined || handle.type !== "background_agent")
873
+ return undefined;
874
+ return handle.status === "parked" ? (handle.cycleSeq ?? 1) + 1 : handle.cycleSeq;
875
+ }
870
876
  async function claimTerminalRowForRevive(core, store, handle, scope) {
871
877
  for (let attempt = 0; attempt < 3; attempt++) {
872
878
  let live;
@@ -972,6 +978,9 @@ export async function reviveBackgroundAgentLane(core, id, access, abort) {
972
978
  return { ok: false, reason: "still_running" };
973
979
  }
974
980
  ensureDurableHeartbeatLane(core);
981
+ if (claim.status === "claimed" && typeof claim.row.seq === "number" && (handle.cycleSeq ?? 1) < claim.row.seq) {
982
+ handle.cycleSeq = claim.row.seq;
983
+ }
975
984
  }
976
985
  handle.status = "running";
977
986
  handle.channelState = "attaching";
@@ -267,6 +267,9 @@ export declare class TaskRegistry {
267
267
  suggestion?: string;
268
268
  };
269
269
  markRetainedContinuation(id: string): void;
270
+ /** #258 — the row's current stop-cycle counter, read by the spawn lanes right after registering to
271
+ * thread into the child's `RunInternals.cycleSeq`; see {@link backgroundAgentCycleSeqLane}. */
272
+ backgroundAgentCycleSeq(id: string): number | undefined;
270
273
  /** ASYNC (revive arbitration) — the durable half is a guarded ownership claim on the row (awaited before the
271
274
  * in-memory flip), so this leg and a cross-process claim arbitrate in one domain instead of both
272
275
  * believing they own the cycle. See {@link reviveBackgroundAgentLane}. */