@sema-agent/core 5.35.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 (34) hide show
  1. package/CHANGELOG.md +58 -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 +16 -0
  6. package/dist/core/context-guard.d.ts +41 -0
  7. package/dist/core/context-guard.js +76 -0
  8. package/dist/core/memory-engine/engine.js +1 -1
  9. package/dist/core/park-selfcheck.d.ts +5 -0
  10. package/dist/core/runner/assemble-result.d.ts +3 -0
  11. package/dist/core/runner/assemble-result.js +3 -0
  12. package/dist/core/runner/git-status-frame.d.ts +219 -0
  13. package/dist/core/runner/git-status-frame.js +212 -0
  14. package/dist/core/runner/prepare-task.d.ts +16 -0
  15. package/dist/core/runner/prepare-task.js +27 -34
  16. package/dist/core/runner/runtask.js +266 -5
  17. package/dist/core/task-registry-agent.d.ts +15 -0
  18. package/dist/core/task-registry-agent.js +9 -0
  19. package/dist/core/task-registry.d.ts +3 -0
  20. package/dist/core/task-registry.js +4 -1
  21. package/dist/core/types.d.ts +27 -7
  22. package/dist/engine/harness/types.d.ts +65 -1
  23. package/dist/engine/harness/types.js +20 -0
  24. package/dist/engine/session/import-validate.js +10 -1
  25. package/dist/engine/session/session.d.ts +37 -1
  26. package/dist/engine/session/session.js +56 -1
  27. package/dist/internal/harness-types.d.ts +1 -0
  28. package/dist/internal/harness.d.ts +2 -0
  29. package/dist/internal/harness.js +2 -0
  30. package/dist/prompt-assembly/epoch.js +1 -1
  31. package/dist/prompt-assembly/event-registry.js +1 -0
  32. package/dist/prompts/default.d.ts +20 -7
  33. package/dist/prompts/default.js +2 -7
  34. package/package.json +1 -1
@@ -3,6 +3,8 @@ 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
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";
@@ -868,6 +870,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
868
870
  workingFileAttachments: buildWorkingFileAttachments(spec, prepared),
869
871
  ...(prepared.onCompactionApplied ? { onApplied: prepared.onCompactionApplied } : {}),
870
872
  ...runnerHooks.seamCCompactionOptions(prepared),
873
+ ...gitRestateOption(prepared),
871
874
  ...windowSafetyOptions(event.model),
872
875
  ...runnerHooks.compactionHookOptions(spec, prepared.sessionId, passTrigger),
873
876
  });
@@ -996,6 +999,96 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
996
999
  };
997
1000
  return onTurnBoundary;
998
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
+ }
999
1092
  function makeHarnessHandlers(prepared, stats, rs, deps) {
1000
1093
  const { spec, queue, internals, ident, parentToolCallId, subagentName, pushContent, emitCommitted, startedToolCallIds, toolStartAt, writeFamilyOf, toolLabels, postToolBatchHook, batchArgs } = deps;
1001
1094
  const internalsNotifier = createSafeNotifier({
@@ -1040,6 +1133,19 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
1040
1133
  if (event.entryId !== undefined && (m.role === "user" || m.role === "assistant" || m.role === "toolResult")) {
1041
1134
  emitCommitted(event.entryId, m.role, m.role === "toolResult" ? m.toolCallId : undefined);
1042
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
+ }
1043
1149
  if (rs.degrade.degraded === undefined && m.role === "assistant") {
1044
1150
  const deg = readDegradation(m);
1045
1151
  if (deg)
@@ -1261,6 +1367,7 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
1261
1367
  type: "task_progress",
1262
1368
  taskId: rs.telemetry.taskId,
1263
1369
  ...(internals?.delegationTaskType !== undefined ? { taskType: internals.delegationTaskType } : {}),
1370
+ ...(internals?.cycleSeq !== undefined ? { seq: internals.cycleSeq } : {}),
1264
1371
  ...(internals?.parentTaskId !== undefined ? { parentTaskId: internals.parentTaskId } : {}),
1265
1372
  ...(subagentName ? { name: subagentName } : {}),
1266
1373
  usage: { totalTokens: stats.tokens, toolUses: stats.toolCalls, durationMs: Date.now() - rs.telemetry.taskStart },
@@ -2175,12 +2282,12 @@ export class Runner {
2175
2282
  return [];
2176
2283
  const full = textOf(e.message);
2177
2284
  if (m.engineMinted === true)
2178
- return [full];
2285
+ return [stripGitStatusUnits(full)];
2179
2286
  if (Array.isArray(m.engineSegments) && m.engineSegments.length > 0) {
2180
- 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))));
2181
2288
  }
2182
2289
  if (typeof m.enginePrefixChars === "number" && m.enginePrefixChars > 0)
2183
- return [full.slice(0, m.enginePrefixChars)];
2290
+ return [stripGitStatusUnits(full.slice(0, m.enginePrefixChars))];
2184
2291
  return [];
2185
2292
  });
2186
2293
  }
@@ -2692,6 +2799,7 @@ export class Runner {
2692
2799
  workingFileAttachments: buildWorkingFileAttachments(spec, prepared),
2693
2800
  ...(prepared.onCompactionApplied ? { onApplied: prepared.onCompactionApplied } : {}),
2694
2801
  ...this.seamCCompactionOptions(prepared),
2802
+ ...gitRestateOption(prepared),
2695
2803
  ...windowSafetyOptions(prepared.harness.getModel()),
2696
2804
  ...this.compactionHookOptions(spec, prepared.sessionId, "forced"),
2697
2805
  });
@@ -2762,6 +2870,80 @@ export class Runner {
2762
2870
  recordCompactionReuse: (p, c) => this.recordCompactionReuse(p, c),
2763
2871
  },
2764
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
+ });
2765
2947
  const unsubBoundary = prepared.harness.on("turn_boundary", onTurnBoundary);
2766
2948
  const unsub = prepared.harness.subscribe((event) => {
2767
2949
  switch (event.type) {
@@ -2839,6 +3021,25 @@ export class Runner {
2839
3021
  continuation += "\n\n" + formatHookFeedback(renderOrphanedBackgroundTasks(orphans));
2840
3022
  }
2841
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
+ }
2842
3043
  const engineSegments = continuation.length > 0 ? [{ start: 0, end: continuation.length }] : [];
2843
3044
  const resumeFrames = [
2844
3045
  ...readPendingSteerQueue(resume.cp.state).map((entry) => ({ entry, source: "steer" })),
@@ -2868,7 +3069,23 @@ export class Runner {
2868
3069
  }
2869
3070
  if (resume !== undefined)
2870
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
+ }
2871
3085
  final = await withBrainSinks(() => prepared.harness.prompt(continuation, { engineSegments }));
3086
+ await flushGitMirror();
3087
+ if (prepared.gitStatusRef.announced?.pending === true)
3088
+ await prepared.gitStatusRef.reassert?.();
2872
3089
  }
2873
3090
  }
2874
3091
  else {
@@ -2900,6 +3117,27 @@ export class Runner {
2900
3117
  promptBlocked = true;
2901
3118
  }
2902
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
+ }
2903
3141
  const firstFrames = [];
2904
3142
  if (rs.attach.attachState !== undefined) {
2905
3143
  const attach = rs.attach.attachState;
@@ -2937,17 +3175,18 @@ export class Runner {
2937
3175
  effectiveObjective = `${firstFrames.map((f) => `<system-reminder>\n${sanitizeUntrustedText(f.body)}\n</system-reminder>`).join("\n")}\n${effectiveObjective}`;
2938
3176
  }
2939
3177
  }
3178
+ const gitQueuedChars = gitLegDelivered !== undefined && gitLegDelivered.standalone ? gitLegDelivered.wrapped.length : 0;
2940
3179
  const precallMicroUsd = rs.budget.maxCostMicroUsd === undefined
2941
3180
  ? 0
2942
3181
  : computeCostMicroUsd({
2943
- totalInputTokens: Math.ceil(effectiveObjective.length / 4),
3182
+ totalInputTokens: Math.ceil((effectiveObjective.length + gitQueuedChars) / 4),
2944
3183
  cacheReadTokens: 0,
2945
3184
  cacheWriteTokens: 0,
2946
3185
  cacheWriteTokensLong: 0,
2947
3186
  outputTokens: prepared.model.maxTokens ?? DEFAULT_PRECALL_OUTPUT_TOKENS,
2948
3187
  }, rs.telemetry.pricing);
2949
3188
  const precallCeilingMicroUsd = prepared.suspendForResource !== undefined ? rs.budget.remainingMicroUsd : rs.budget.maxCostMicroUsd;
2950
- 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);
2951
3190
  const precallCeilingTokens = prepared.suspendForResource !== undefined ? rs.budget.remainingTokens : rs.budget.maxTokensWindow;
2952
3191
  const entryUsageRetryAfterMs = prepared.usageGovernance !== undefined ? await prepared.usageGovernance.check(Date.now()) : undefined;
2953
3192
  if (promptBlocked) {
@@ -2979,6 +3218,21 @@ export class Runner {
2979
3218
  const images = spec.images
2980
3219
  ? await Promise.all(spec.images.map((img) => toImageContent(img, this.deps.allowImageUrl)))
2981
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
+ }
2982
3236
  for (const f of firstFrames) {
2983
3237
  f.commit();
2984
3238
  queue.push({ type: "steering_injected", source: f.source, preview: f.body.slice(0, 220), ...ident() });
@@ -3004,6 +3258,9 @@ export class Runner {
3004
3258
  ...(enginePrefixChars > 0 ? { enginePrefixChars } : {}),
3005
3259
  ...(objectiveActor !== undefined ? { actor: objectiveActor } : {}),
3006
3260
  }));
3261
+ await flushGitMirror();
3262
+ if (prepared.gitStatusRef.announced?.pending === true)
3263
+ await prepared.gitStatusRef.reassert?.();
3007
3264
  }
3008
3265
  }
3009
3266
  loopLatch.ended = true;
@@ -3033,6 +3290,7 @@ export class Runner {
3033
3290
  }
3034
3291
  notificationLaneLive = false;
3035
3292
  unsubscribeTaskNotifications();
3293
+ unsubGitRetry();
3036
3294
  unsubBoundary();
3037
3295
  unsub();
3038
3296
  }
@@ -3176,6 +3434,7 @@ export class Runner {
3176
3434
  budgetAxis: rs.limits.budgetAxis,
3177
3435
  blockedReason: prepared.blockedRef.reason,
3178
3436
  conflict: prepared.conflictRef.hit,
3437
+ gitCoreOverBudget: prepared.gitStatusRef.terminalCode === "irreducible_core_over_budget",
3179
3438
  outputInvalid: rs.degrade.outputInvalid,
3180
3439
  suspendLoop: prepared.suspendLoopRef.hit,
3181
3440
  suspendRef: prepared.suspendRef.token !== undefined && prepared.suspendRef.gate !== undefined
@@ -3304,6 +3563,7 @@ export class Runner {
3304
3563
  type: "task_progress",
3305
3564
  taskId: rs.telemetry.taskId,
3306
3565
  ...(internals?.delegationTaskType !== undefined ? { taskType: internals.delegationTaskType } : {}),
3566
+ ...(internals?.cycleSeq !== undefined ? { seq: internals.cycleSeq } : {}),
3307
3567
  ...(internals?.parentTaskId !== undefined ? { parentTaskId: internals.parentTaskId } : {}),
3308
3568
  ...(subagentName ? { name: subagentName } : {}),
3309
3569
  usage: { totalTokens: stats.tokens, toolUses: stats.toolCalls, durationMs: Date.now() - rs.telemetry.taskStart },
@@ -4316,6 +4576,7 @@ export class Runner {
4316
4576
  workingFileAttachments: buildWorkingFileAttachments(spec, prepared),
4317
4577
  ...(prepared.onCompactionApplied ? { onApplied: prepared.onCompactionApplied } : {}),
4318
4578
  ...this.seamCCompactionOptions(prepared),
4579
+ ...gitRestateOption(prepared),
4319
4580
  ...this.compactionHookOptions(spec, prepared.sessionId, "auto"),
4320
4581
  });
4321
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}. */
@@ -10,7 +10,7 @@ import { registerWorkflowLane, pollWorkflowLane, stopWorkflowLane } from "./task
10
10
  import { mintCompletionId, canAccessWorkflowRun, formatWorkflowRun, clipTaskOutput, assertOwnership, sleepPollStep, statusFromBackground, rollSpoolText, accountDroppedBytes, renderSpoolBody, spoolDropNote, droppedGapNote, alreadyTerminalStopNote, terminalTaskSummary, TASK_OUTPUT_MAX_CHARS, MONITOR_BATCH_WINDOW_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, MONITOR_MAX_BATCHES_PER_MINUTE, canAccess, DURABLE_AGENT_HANDLE_RE, } from "./task-registry-shared.js";
11
11
  export { normalizeAgentName, DURABLE_AGENT_HEARTBEAT_MS, DURABLE_AGENT_HANDLE_RE, BG_AGENT_REAP_STOP_ERROR } from "./task-registry-shared.js";
12
12
  import { TASK_OUTPUT_TOOL_NAME, TASK_STOP_TOOL_NAME, TASK_OUTPUT_CONTRACT, TASK_STOP_CONTRACT, TASK_OUTPUT_MISSING_ID_MESSAGE, TASK_STOP_MISSING_ID_MESSAGE, TASK_STOP_PARAMS, resolveTaskIdArg, REGISTRY_TASK_TOOL_CAPS, composeTaskOutputDescription, composeTaskOutputParams, composeTaskStopDescription, } from "./task-tool-shape.js";
13
- import { durableAgentArmedLane, durableAgentRowProbeLane, beginDurableClaimLane, endDurableClaimLane, reapDurableAgentsLane, noteBackgroundAgentActivityLane, reapStaleSessionBackgroundAgentsLane, releaseDurableTranscriptAnchorLane, bindBackgroundAgentSessionLane, registerBackgroundAgentLane, recordBackgroundAgentOrgAdmissionLane, parkBackgroundAgentLane, reconcileParkedAgentsLane, claimParkedAgentLane, rollbackParkedClaimLane, consumeParkedFlipLane, finalizeParkedResumeLane, settleBackgroundAgentLane, abortBackgroundAgentsForOwnerLane, serveDurableAgentRowLane, resolveBackgroundAgentByNameLane, markRetainedContinuationLane, reviveBackgroundAgentLane, settleRevivedAgentLane, unmarkRetainedContinuationLane, attachAgentNotifyLane, deliverToRunningAgentLane, runningBackgroundAgentLabelsLane, runningAgentFooterLane, notFoundRunningAgentsTail, pollBackgroundAgentLane, stopBackgroundAgentLane, } from "./task-registry-agent.js";
13
+ import { durableAgentArmedLane, durableAgentRowProbeLane, beginDurableClaimLane, endDurableClaimLane, reapDurableAgentsLane, noteBackgroundAgentActivityLane, reapStaleSessionBackgroundAgentsLane, releaseDurableTranscriptAnchorLane, bindBackgroundAgentSessionLane, registerBackgroundAgentLane, recordBackgroundAgentOrgAdmissionLane, parkBackgroundAgentLane, reconcileParkedAgentsLane, claimParkedAgentLane, rollbackParkedClaimLane, consumeParkedFlipLane, finalizeParkedResumeLane, settleBackgroundAgentLane, abortBackgroundAgentsForOwnerLane, serveDurableAgentRowLane, resolveBackgroundAgentByNameLane, backgroundAgentCycleSeqLane, markRetainedContinuationLane, reviveBackgroundAgentLane, settleRevivedAgentLane, unmarkRetainedContinuationLane, attachAgentNotifyLane, deliverToRunningAgentLane, runningBackgroundAgentLabelsLane, runningAgentFooterLane, notFoundRunningAgentsTail, pollBackgroundAgentLane, stopBackgroundAgentLane, } from "./task-registry-agent.js";
14
14
  export { canAccessWorkflowRun, clipTaskOutput, MONITOR_BATCH_WINDOW_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, MONITOR_MAX_BATCHES_PER_MINUTE };
15
15
  const BLOCK_DEFAULT_TIMEOUT_MS = 30_000;
16
16
  const BLOCK_MAX_TIMEOUT_MS = 600_000;
@@ -191,6 +191,9 @@ export class TaskRegistry {
191
191
  markRetainedContinuation(id) {
192
192
  return markRetainedContinuationLane(this.core, id);
193
193
  }
194
+ backgroundAgentCycleSeq(id) {
195
+ return backgroundAgentCycleSeqLane(this.core, id);
196
+ }
194
197
  reviveBackgroundAgent(id, access, abort) {
195
198
  return reviveBackgroundAgentLane(this.core, id, access, abort);
196
199
  }
@@ -3647,9 +3647,15 @@ export type TaskEvent = ({
3647
3647
  * (one event per attachment; a multi-attachment boundary still coalesces into ONE steer message
3648
3648
  * on the model lane). [c209-C]: the listing family's FIRST-FRAME deliveries (initial roster /
3649
3649
  * `<skills>` block riding the first user message, not a steer) emit the same echo frames.
3650
+ *
3651
+ * `git_status` (env-tail migration, additive member): the git-status frame — the turn-dynamic
3652
+ * git facts' carrier since they left the system prompt. Emitted at the frame's append RECEIPT
3653
+ * (first-frame / resume-continuation / compaction re-assert deliveries alike). Its `preview`
3654
+ * is a CONSTANT wording on purpose (never frame bytes): branch names and status text are
3655
+ * repo-controlled and must not enter the event telemetry plane through this echo.
3650
3656
  */
3651
3657
  type: "steering_injected";
3652
- source: "limit_approach" | "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" | "final_verification";
3658
+ source: "limit_approach" | "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" | "final_verification" | "git_status";
3653
3659
  preview: string;
3654
3660
  } & TaskEventIdentity) | ({
3655
3661
  /**
@@ -3790,6 +3796,18 @@ export type TaskEvent = ({
3790
3796
  * read absence as "not an agent".
3791
3797
  */
3792
3798
  taskType?: DelegationTaskType;
3799
+ /**
3800
+ * #258 — the registry row's STOP-CYCLE generation this tick reports from (fresh
3801
+ * spawn = 1, every launched revival bumps it), the same counter `TaskNotificationPayload.seq`
3802
+ * and `BackgroundChildEvent.seq` already speak — one axis, not a third spelling. It answers
3803
+ * the one question a fleet consumer cannot otherwise decide when a frame arrives late: "late
3804
+ * first frame of the cycle I know (same value), or a revived run I have not folded yet
3805
+ * (higher value)?" Advisory observation, stamped at spawn/revive from the registry's own
3806
+ * counter; the settle-time ledger stays the authority. ABSENT is a fact, not a gap: a run
3807
+ * with no `a*` registry row (a SYNCHRONOUS delegated child, a workflow `wa*` agent, a
3808
+ * top-level run) has no generation concept, and absence must never be read as "cycle 1".
3809
+ */
3810
+ seq?: number;
3793
3811
  /** [1611] workflow-lane self-identification (server field-proof: the SSE-forwarded tick of a
3794
3812
  * WORKFLOW child previously carried a bare uuid with no workflow identity — indistinguishable
3795
3813
  * from an unknown nested subagent; the fleet lane had `wa*`+workflowRunId but this lane had
@@ -4294,12 +4312,14 @@ export interface BackgroundChildEvent {
4294
4312
  resumable?: boolean;
4295
4313
  /** terminal: the settled status. */
4296
4314
  status?: "completed" | "killed" | "failed";
4297
- /** terminal (PAIR-REVIEW-01 C-3 half): the stop-cycle number — MIRROR of the sibling
4298
- * task_notification's `TaskNotificationPayload.seq` (same settle, same X3 snapshot), so a
4299
- * fleet-lane consumer can key multi-cycle events (`taskId:status[:seq]`) without the notify lane.
4300
- * Same honest downgrade: present whenever a cycle number is knowable (the retain ledger's `cycleSeq`,
4301
- * or the durable row's `seq` on a tier-3 revival / parked resume), absent when neither carrier exists
4302
- * (single-cycle child; forging a period would lie). */
4315
+ /** terminal + spawn/tick (#258 widened the carriers): the stop-cycle number — on a TERMINAL frame
4316
+ * a MIRROR of the sibling task_notification's `TaskNotificationPayload.seq` (same settle, same X3
4317
+ * snapshot); on SPAWN and TICK frames the registry row's generation at emit (fresh spawn = 1, a
4318
+ * revived cycle's bumped counter), so a fleet consumer can tell a LATE first frame from a revived
4319
+ * cycle's frame without waiting for the terminal. One axis with `task_progress`'s `seq` the tick
4320
+ * mirrors the frame's own stamp. Same honest downgrade as ever: present whenever a cycle number is
4321
+ * knowable (the registry handle / retain ledger's `cycleSeq`, the durable row's `seq` on a tier-3
4322
+ * revival / parked resume), absent when no carrier exists (forging a period would lie). */
4303
4323
  seq?: number;
4304
4324
  /** terminal, P1-3(黑板 [1920]/[1921]/[1924]/[1925]): the cross-channel completion correlation id —
4305
4325
  * MIRROR of the sibling task_notification's `TaskNotificationPayload.completionId` (same settle,
@@ -498,6 +498,53 @@ export interface WorkspaceState {
498
498
  export interface WorkspaceStateEntry extends SessionTreeEntryBase, WorkspaceState {
499
499
  type: "workspace_state";
500
500
  }
501
+ /** The closed kind set of a git-status announcement (the availability half of the `(kind, hash)`
502
+ * comparison tuple): `full` = five-segment snapshot frame; `degraded` = branch+dirty two-line frame
503
+ * (the snapshot round-trip failed while the basic probe succeeded); `unavailable` / `non-repo` =
504
+ * tombstone frames announcing that earlier git frames no longer describe the tree. */
505
+ export type GitAnnouncementKind = "full" | "degraded" | "unavailable" | "non-repo";
506
+ /**
507
+ * The git-status frame ANNOUNCED STATE — which rendered git view the model has last been shown on
508
+ * this branch, as a `(kind, hash)` tuple (hash = content hash of the rendered frame body with the
509
+ * frame format version and the canonical repo root bound into the digest domain). Two-phase receipt
510
+ * protocol: `pending: true` = a re-announcement is OWED but its frame append has not produced a
511
+ * receipt (a compaction restates the tuple as pending in the same CAS that lands the summary; the
512
+ * announced form is written only once the frame's own append returned an entry id) — any reader
513
+ * that finds pending nearest MUST conservatively re-announce. `entryId` = the session entry of the
514
+ * message CARRYING the announced frame; an announced state whose entryId is not on the active
515
+ * branch (rewind cut it) is treated as pending by the read walk (branch-authority read ladder).
516
+ */
517
+ export interface GitAnnouncementState {
518
+ kind: GitAnnouncementKind;
519
+ hash: string;
520
+ /** Session entry id of the message carrying the announced frame (absent on a pending restatement). */
521
+ entryId?: string;
522
+ /** Two-phase receipt: the tuple is owed but its frame append has no receipt yet. */
523
+ pending?: true;
524
+ }
525
+ /**
526
+ * The git-status announced state as a FIRST-CLASS typed entry (same snapshot doctrine as
527
+ * {@link AnnouncedListingEntry}): the nearest carrier on the branch IS the announced state.
528
+ * Written at every frame COMMIT point (first-frame receipt / resume-continuation receipt /
529
+ * compaction re-assertion receipt); a compaction additionally restates the tuple as `pending`
530
+ * inside its own `details.gitAnnouncement` (same-CAS with the new baseline) until the re-asserted
531
+ * frame's append receipt lands. The checkpoint mirror (`CheckpointState.gitAnnouncement`) is the
532
+ * lower seed rung, consulted only when the branch carries no mirror at all.
533
+ */
534
+ export interface GitAnnouncementEntry extends SessionTreeEntryBase, GitAnnouncementState {
535
+ type: "git_announcement";
536
+ }
537
+ /** Bounded cap for {@link normalizeGitAnnouncement} entry ids (self-generated ids are short; an
538
+ * oversize value is a forgery signal, same posture as the sibling caps). */
539
+ export declare const GIT_ANNOUNCEMENT_MAX_ENTRY_ID_CHARS = 256;
540
+ /**
541
+ * Strict shape gate for the git announced state — the SINGLE normalization the read walk, the
542
+ * public append, and the import-validate door all use (the {@link normalizeAnnouncedListing}
543
+ * posture). Returns a shaped copy carrying ONLY the known keys, or undefined when the value is not
544
+ * structurally valid: closed kind set, full `sha256:<hex64>` hash grammar (a prefix-only check
545
+ * would accept junk), bounded entryId, `pending` only as literal `true`.
546
+ */
547
+ export declare function normalizeGitAnnouncement(v: unknown): GitAnnouncementState | undefined;
501
548
  /** Bounded cap for {@link normalizeWorkspaceState} paths (a sha is capped by its hex shape check). */
502
549
  export declare const WORKSPACE_STATE_MAX_PATH_CHARS = 4096;
503
550
  /**
@@ -544,7 +591,7 @@ export declare function normalizeAnnouncedListing(v: unknown): {
544
591
  models?: string[];
545
592
  } | undefined;
546
593
  /** All persisted session tree entry variants. */
547
- export type SessionTreeEntry = MessageEntry | ThinkingLevelChangeEntry | ModelChangeEntry | CompactionEntry | CustomEntry | CustomMessageEntry | LabelEntry | SessionInfoEntry | LeafEntry | PromptEpochEntry | AnnouncedListingEntry | WorkspaceStateEntry;
594
+ export type SessionTreeEntry = MessageEntry | ThinkingLevelChangeEntry | ModelChangeEntry | CompactionEntry | CustomEntry | CustomMessageEntry | LabelEntry | SessionInfoEntry | LeafEntry | PromptEpochEntry | AnnouncedListingEntry | GitAnnouncementEntry | WorkspaceStateEntry;
548
595
  export interface SessionContext {
549
596
  messages: AgentMessage[];
550
597
  thinkingLevel: string;
@@ -684,6 +731,23 @@ export interface Session<TMetadata extends SessionMetadata = SessionMetadata> {
684
731
  * epoch restatement) calls it with `?.()` — an implementer without it degrades to the pre-epoch
685
732
  * path (no restatement), never a crash. */
686
733
  getPromptEpoch?(): Promise<import("../../prompt-assembly/epoch.js").PromptEpochDescriptor | undefined>;
734
+ /** Persist the git-status announced state as a first-class {@link GitAnnouncementEntry}
735
+ * (snapshot semantics; see `Session.appendGitAnnouncement`). OPTIONAL on the interface — same
736
+ * external-implementer posture as `getPromptEpoch`; consumers call it with `?.()` and an
737
+ * implementer without it degrades to conservative re-announcement every leg (duplicate-tolerant
738
+ * by design), never a crash. */
739
+ appendGitAnnouncement?(state: GitAnnouncementState): Promise<string>;
740
+ /** Branch-authority read of the git announced state (see `Session.getGitAnnouncement`): nearest
741
+ * carrier on the active branch, with a pending restatement — or an announced state whose frame
742
+ * entryId is NOT on the active branch — surfaced as `status:"pending"` (the reader must
743
+ * conservatively re-announce). Undefined ⇒ no carrier visible on this branch. OPTIONAL, same
744
+ * posture as `appendGitAnnouncement`. */
745
+ getGitAnnouncement?(): Promise<{
746
+ kind: GitAnnouncementKind;
747
+ hash: string;
748
+ status: "announced" | "pending";
749
+ entryId?: string;
750
+ } | undefined>;
687
751
  }
688
752
  export interface SessionCreateOptions {
689
753
  id?: string;