@sema-agent/core 5.45.0 → 5.46.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 (69) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/dist/agents/subagent.js +1 -1
  3. package/dist/core/checkpoint-store.d.ts +12 -0
  4. package/dist/core/hooks.d.ts +9 -2
  5. package/dist/core/hooks.js +6 -5
  6. package/dist/core/memory-engine/content-origin.d.ts +3 -1
  7. package/dist/core/memory-engine/engine.d.ts +50 -3
  8. package/dist/core/memory-engine/engine.js +194 -32
  9. package/dist/core/memory-engine/export-bundle.d.ts +10 -1
  10. package/dist/core/memory-engine/export-bundle.js +21 -0
  11. package/dist/core/memory-engine/file-backend.d.ts +33 -4
  12. package/dist/core/memory-engine/file-backend.js +165 -39
  13. package/dist/core/memory-engine/frontmatter.d.ts +42 -1
  14. package/dist/core/memory-engine/frontmatter.js +141 -1
  15. package/dist/core/memory-engine/header-hints.d.ts +17 -0
  16. package/dist/core/memory-engine/header-hints.js +6 -0
  17. package/dist/core/memory-engine/index.d.ts +4 -3
  18. package/dist/core/memory-engine/index.js +3 -2
  19. package/dist/core/memory-engine/layout.d.ts +25 -2
  20. package/dist/core/memory-engine/layout.js +25 -12
  21. package/dist/core/memory-engine/memory-backend-contract.js +65 -0
  22. package/dist/core/memory-engine/sync-client.d.ts +1 -1
  23. package/dist/core/memory-engine/sync-client.js +33 -1
  24. package/dist/core/memory-engine/tools.d.ts +7 -0
  25. package/dist/core/memory-engine/tools.js +3 -0
  26. package/dist/core/memory-engine/types.d.ts +75 -1
  27. package/dist/core/memory-engine/types.js +1 -1
  28. package/dist/core/reminder-mint.d.ts +70 -0
  29. package/dist/core/reminder-mint.js +25 -0
  30. package/dist/core/runner/git-status-frame.d.ts +3 -14
  31. package/dist/core/runner/git-status-frame.js +39 -14
  32. package/dist/core/runner/prepare-config-doors.d.ts +4 -0
  33. package/dist/core/runner/prepare-config-doors.js +15 -0
  34. package/dist/core/runner/prepare-hands-readface.d.ts +5 -11
  35. package/dist/core/runner/prepare-hands-readface.js +26 -0
  36. package/dist/core/runner/prepare-memory.d.ts +11 -0
  37. package/dist/core/runner/prepare-memory.js +12 -10
  38. package/dist/core/runner/prepare-task.d.ts +22 -1
  39. package/dist/core/runner/prepare-task.js +48 -13
  40. package/dist/core/runner/runtask.js +62 -55
  41. package/dist/core/side-query.d.ts +11 -1
  42. package/dist/core/side-query.js +3 -0
  43. package/dist/core/types.d.ts +47 -7
  44. package/dist/engine/harness/types.d.ts +46 -1
  45. package/dist/engine/harness/types.js +11 -0
  46. package/dist/engine/session/import-validate.js +6 -1
  47. package/dist/engine/session/session.d.ts +20 -0
  48. package/dist/engine/session/session.js +26 -1
  49. package/dist/index.d.ts +2 -1
  50. package/dist/index.js +2 -1
  51. package/dist/orchestration/run-workflow-tool.d.ts +16 -0
  52. package/dist/orchestration/run-workflow-tool.js +23 -3
  53. package/dist/orchestration/workflow-governance.d.ts +8 -1
  54. package/dist/prompt-assembly/epoch.js +2 -0
  55. package/dist/prompt-assembly/types.d.ts +6 -0
  56. package/dist/prompts/default.d.ts +13 -1
  57. package/dist/prompts/default.js +5 -1
  58. package/dist/tools/fs/fs-bash.d.ts +4 -0
  59. package/dist/tools/fs/fs-bash.js +1 -1
  60. package/dist/tools/fs/fs-read.d.ts +1 -1
  61. package/dist/tools/fs/fs-read.js +8 -7
  62. package/dist/tools/fs/fs-shared.d.ts +10 -4
  63. package/dist/tools/fs/fs-shared.js +6 -3
  64. package/dist/tools/fs/gh-rate-limit.d.ts +4 -1
  65. package/dist/tools/fs/gh-rate-limit.js +3 -2
  66. package/dist/tools/fs/index.d.ts +10 -2
  67. package/dist/tools/fs/index.js +2 -1
  68. package/package.json +1 -1
  69. package/test/export-surface.snapshot.json +12 -1
@@ -1,5 +1,6 @@
1
1
  import { persistedReadDenyEntryProblem } from "../../tools/fs/read-deny.js";
2
2
  import { createSafeNotifier, observeThenableRejection } from "../safe-notify.js";
3
+ import { mintSystemReminder, openSystemReminder } from "../reminder-mint.js";
3
4
  import { deliverDelegationLifecycle, deliverEngineNotice, undrainedUserInputNotices } from "../types.js";
4
5
  import { AgentHarness, DEFAULT_COMPACTION_SETTINGS, uuidv7 } from "../../internal/harness.js";
5
6
  import { snapshotActorAssertion } from "../../internal/llm.js";
@@ -122,12 +123,12 @@ const SNAPSHOT_TIMEOUT_MS = 30_000;
122
123
  const SNAPSHOT_TOO_LARGE_TTL_MS = 30 * 60_000;
123
124
  const LIMIT_APPROACH_DEFAULT_THRESHOLDS = [0.8, 0.95];
124
125
  const DEFAULT_PRECALL_OUTPUT_TOKENS = 4096;
125
- function limitApproachFrames(index, axis, percent) {
126
+ function limitApproachFrames(index, axis, percent, mark) {
126
127
  return index === 0
127
- ? `<system-reminder>[limit approach] This task has used about ${percent}% of its ${axis}. ` +
128
+ ? `${openSystemReminder(mark)}[limit approach] This task has used about ${percent}% of its ${axis}. ` +
128
129
  `Start converging: finish the step you are on, drop work that is not required for the deliverable, and ` +
129
130
  `avoid starting anything you cannot complete within what is left.</system-reminder>`
130
- : `<system-reminder>[limit approach] About ${percent}% of the ${axis} is gone. Deliver now: ` +
131
+ : `${openSystemReminder(mark)}[limit approach] About ${percent}% of the ${axis} is gone. Deliver now: ` +
131
132
  `stop exploring, write up what you have — including what is incomplete and what you would do next — ` +
132
133
  `and end the task. Work that is not reported before the allowance runs out is lost.</system-reminder>`;
133
134
  }
@@ -244,16 +245,16 @@ function assertOutcomeText(value, field) {
244
245
  throw new CheckpointError("checkpoint.invalid_outcome", `resume \`${field}\` is not a plain string (got ${typeof value}) — a decide's text payload is an operator's plain data, not a live object; refusing pre-CAS, the checkpoint stays pending`);
245
246
  }
246
247
  }
247
- function resumeContinuation(resume) {
248
+ function resumeContinuation(resume, mark) {
248
249
  if (resume.outcome.gate === "wake") {
249
250
  return formatHookFeedback("You were WOKEN from a parked pause by an operator. Before continuing, re-orient from the workspace: " +
250
251
  "run `git status` and review your recent changes / last commits to confirm what is already done, then " +
251
- "continue the remaining work. Do NOT restart the task or re-run work that is already committed.");
252
+ "continue the remaining work. Do NOT restart the task or re-run work that is already committed.", mark);
252
253
  }
253
254
  if (resume.outcome.gate === "resource_limit") {
254
255
  return formatHookFeedback("You were resumed after a pause. Before continuing, re-orient from the workspace: run `git status` and " +
255
256
  "review your recent changes / last commits to confirm what is already done, then continue the " +
256
- "remaining work. Do NOT restart the task or re-run work that is already committed.");
257
+ "remaining work. Do NOT restart the task or re-run work that is already committed.", mark);
257
258
  }
258
259
  if (resume.outcome.gate === "dry_run_review") {
259
260
  const verdict = resume.outcome.decision === "approve"
@@ -261,31 +262,31 @@ function resumeContinuation(resume) {
261
262
  : `Your predicted change was REVIEWED and REJECTED${resume.outcome.reason ? `: ${delimitUntrusted("reviewer note", resume.outcome.reason, REVIEWER_NOTE_MAX_BODY)}` : ""}; it was NOT applied`;
262
263
  return formatHookFeedback(`You were resumed after a dry-run review. ${verdict}. Before continuing, re-orient from the workspace ` +
263
264
  "(run `git status` and review your recent changes) to confirm the current state, then continue the " +
264
- "remaining work. Do NOT restart the task or re-run work that is already done.");
265
+ "remaining work. Do NOT restart the task or re-run work that is already done.", mark);
265
266
  }
266
267
  if (resume.outcome.gate === "plan_review") {
267
268
  if (resume.outcome.decision === "approve") {
268
269
  return formatHookFeedback("Your proposed PLAN was REVIEWED and APPROVED. Proceed with that plan now — begin executing it. " +
269
- "This is a RESUMED task; do NOT re-plan or restart from scratch, just carry out the approved plan.");
270
+ "This is a RESUMED task; do NOT re-plan or restart from scratch, just carry out the approved plan.", mark);
270
271
  }
271
272
  if (resume.outcome.decision === "edit") {
272
273
  if (!resume.outcome.editedPlan) {
273
274
  return formatHookFeedback("Your proposed PLAN was REVIEWED and EDITED, but no revised plan text was supplied — proceed with your " +
274
- "ORIGINAL plan as-is, begin executing it now. This is a RESUMED task; do NOT re-plan or restart from scratch.");
275
+ "ORIGINAL plan as-is, begin executing it now. This is a RESUMED task; do NOT re-plan or restart from scratch.", mark);
275
276
  }
276
277
  return formatHookFeedback("Your proposed PLAN was REVIEWED and EDITED by a human reviewer. Proceed with the REVISED plan below " +
277
278
  "(it supersedes your earlier plan); begin executing it now. This is a RESUMED task; do NOT re-plan " +
278
279
  "or restart from scratch, just carry out the revised plan.\n\nThe REVISED plan to follow is:\n" +
279
- delimitUntrusted("revised plan", resume.outcome.editedPlan));
280
+ delimitUntrusted("revised plan", resume.outcome.editedPlan), mark);
280
281
  }
281
282
  const why = resume.outcome.reason ? ` Reviewer note: ${delimitUntrusted("reviewer note", resume.outcome.reason, REVIEWER_NOTE_MAX_BODY)}` : "";
282
283
  return formatHookFeedback(`Your proposed PLAN was REVIEWED and REJECTED; it was NOT executed.${why} Produce a NEW plan that ` +
283
284
  "addresses the concern, then continue. This is a RESUMED task — re-plan from the current state; do " +
284
- "NOT execute the rejected plan.");
285
+ "NOT execute the rejected plan.", mark);
285
286
  }
286
287
  const { pendingAction } = resume.cp;
287
288
  if (pendingAction.kind !== "tool_approval") {
288
- return formatHookFeedback("This is a RESUMED task — continue from where you left off without restarting or re-running prior work.");
289
+ return formatHookFeedback("This is a RESUMED task — continue from where you left off without restarting or re-running prior work.", mark);
289
290
  }
290
291
  const decided = resume.outcome.decision === "allow"
291
292
  ? "was APPROVED and has now been executed — its result is in the tool results above"
@@ -297,7 +298,7 @@ function resumeContinuation(resume) {
297
298
  return formatHookFeedback(`The pending tool call "${pendingAction.toolName}" ${decided}.${batchNote} ` +
298
299
  `This is a RESUMED task — every step before this point is already done and its results are in the ` +
299
300
  `conversation above. Do NOT restart the task or re-run any tool you already ran; continue from this ` +
300
- `exact point, building on the existing results, and finish the remaining work.`);
301
+ `exact point, building on the existing results, and finish the remaining work.`, mark);
301
302
  }
302
303
  const ENV_DUE_GOVERNANCE_READ_BUDGET_MS = 5_000;
303
304
  const MAX_TIMER_DELAY_MS = 2_147_483_647;
@@ -514,7 +515,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
514
515
  if (tightest !== undefined && tightest.ratio >= thresholds[index]) {
515
516
  rs.counters.approachNoticesSent += 1;
516
517
  const percent = Math.min(100, Math.round(tightest.ratio * 100));
517
- const body = limitApproachFrames(index === 0 ? 0 : 1, tightest.axis, percent);
518
+ const body = limitApproachFrames(index === 0 ? 0 : 1, tightest.axis, percent, prepared.reminderMark);
518
519
  void prepared.harness.steer(body, { engineMinted: true }).catch(() => undefined);
519
520
  queue.push({ type: "steering_injected", source: "limit_approach", preview: body.slice(0, 220), ...ident() });
520
521
  boundarySteered = true;
@@ -739,7 +740,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
739
740
  }
740
741
  }
741
742
  if (due.length > 0) {
742
- attachmentsPayload = due.map((a) => `<system-reminder>\n${sanitizeUntrustedText(a.body)}\n</system-reminder>`).join("\n");
743
+ attachmentsPayload = due.map((a) => mintSystemReminder(sanitizeUntrustedText(a.body), prepared.reminderMark)).join("\n");
743
744
  for (const a of due)
744
745
  boundaryAttachmentBytes += Buffer.byteLength(a.body, "utf8");
745
746
  rs.attach.attachmentsInjected += due.length;
@@ -766,7 +767,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
766
767
  const clipped = clipToBytes(body, budget) === body
767
768
  ? body
768
769
  : `${clipToBytes(body, budget - Buffer.byteLength(BATCH_TRUNCATION_MARKER, "utf8"))}${BATCH_TRUNCATION_MARKER}`;
769
- batchContextBlock = `<system-reminder>\n${clipped}\n</system-reminder>`;
770
+ batchContextBlock = mintSystemReminder(clipped, prepared.reminderMark);
770
771
  }
771
772
  }
772
773
  }
@@ -1000,8 +1001,8 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
1000
1001
  };
1001
1002
  return onTurnBoundary;
1002
1003
  }
1003
- function wrapGitFrame(body) {
1004
- return `<system-reminder>\n${sanitizeUntrustedText(body)}\n</system-reminder>`;
1004
+ function wrapGitFrame(body, mark) {
1005
+ return mintSystemReminder(sanitizeUntrustedText(body), mark);
1005
1006
  }
1006
1007
  async function resolveGitLegDelivery(prepared, cpMirror, report) {
1007
1008
  const ref = prepared.gitStatusRef;
@@ -1044,7 +1045,7 @@ async function resolveGitLegDelivery(prepared, cpMirror, report) {
1044
1045
  }
1045
1046
  if (prior !== undefined && !pending && prior.entryId !== undefined) {
1046
1047
  try {
1047
- const newest = newestEngineGitFrame(await prepared.session.getBranch());
1048
+ const newest = newestEngineGitFrame(await prepared.session.getBranch(), prepared.reminderMark);
1048
1049
  if (newest === undefined || newest.entryId !== prior.entryId)
1049
1050
  pending = true;
1050
1051
  }
@@ -1060,7 +1061,7 @@ async function resolveGitLegDelivery(prepared, cpMirror, report) {
1060
1061
  if (prior === undefined && !pending) {
1061
1062
  let mustDisown;
1062
1063
  try {
1063
- mustDisown = branchCarriesVisiblePositiveGitFrame(await prepared.session.getBranch());
1064
+ mustDisown = branchCarriesVisiblePositiveGitFrame(await prepared.session.getBranch(), prepared.reminderMark);
1064
1065
  }
1065
1066
  catch (err) {
1066
1067
  mustDisown = true;
@@ -1073,9 +1074,9 @@ async function resolveGitLegDelivery(prepared, cpMirror, report) {
1073
1074
  return frame.body;
1074
1075
  }
1075
1076
  if (!pending && prior !== undefined && prior.kind === frame.kind && prior.hash === frame.hash) {
1076
- ref.protectedText = wrapGitFrame(frame.body);
1077
+ ref.protectedText = wrapGitFrame(frame.body, prepared.reminderMark);
1077
1078
  if (frame.kind === "full" && frame.shrunk !== undefined) {
1078
- ref.wrappedShrink = { find: ref.protectedText, replace: wrapGitFrame(frame.shrunk.body) };
1079
+ ref.wrappedShrink = { find: ref.protectedText, replace: wrapGitFrame(frame.shrunk.body, prepared.reminderMark) };
1079
1080
  }
1080
1081
  return undefined;
1081
1082
  }
@@ -1759,9 +1760,9 @@ export class Runner {
1759
1760
  const actorIn = options?.actor;
1760
1761
  const actor = actorIn === undefined ? undefined : snapshotActorAssertion(actorIn);
1761
1762
  const projected = projectHumanInput({ text, actor, source: "steer" });
1762
- const payload = trusted ? formatHookFeedback(projected) : projected;
1763
- const mintsAFrame = payload.trim().length !== 0;
1764
- const replay = { payload, trusted, ...(actor !== undefined ? { actor } : {}) };
1763
+ let payload;
1764
+ let mintsAFrame;
1765
+ let replay;
1765
1766
  const noteAccepted = (h) => {
1766
1767
  if (!mintsAFrame)
1767
1768
  return;
@@ -1790,6 +1791,9 @@ export class Runner {
1790
1791
  const h = handle ?? (await orTimeout(ready));
1791
1792
  if (!h)
1792
1793
  throw steeringError("the task is not running");
1794
+ payload = trusted ? formatHookFeedback(projected, h.reminderMark) : projected;
1795
+ mintsAFrame = payload.trim().length !== 0;
1796
+ replay = { payload, trusted, ...(actor !== undefined ? { actor } : {}) };
1793
1797
  if (typeof inputId === "string") {
1794
1798
  const prior = acceptedSteerInputs.get(inputId);
1795
1799
  if (prior !== undefined) {
@@ -2099,7 +2103,7 @@ export class Runner {
2099
2103
  }
2100
2104
  }
2101
2105
  const loopLatch = { ended: false };
2102
- onReady({ harness: prepared.harness, abortController: prepared.abortController, loop: loopLatch });
2106
+ onReady({ harness: prepared.harness, abortController: prepared.abortController, loop: loopLatch, reminderMark: prepared.reminderMark });
2103
2107
  const stats = { turns: 0, tokens: 0, toolCalls: 0, promptTokens: 0, totalInputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, cacheWriteTokensLong: 0, outputTokens: 0, costMicroUsd: 0 };
2104
2108
  prepared.liveSpendRef.get = () => ({ costMicroUsd: stats.costMicroUsd, tokens: stats.tokens, turns: stats.turns, walltimeMs: Math.round(performance.now() - rs.telemetry.taskStartMonotonic) });
2105
2109
  if (resume &&
@@ -2313,12 +2317,12 @@ export class Runner {
2313
2317
  return [];
2314
2318
  const full = textOf(e.message);
2315
2319
  if (m.engineMinted === true)
2316
- return [stripGitStatusUnits(full)];
2320
+ return [stripGitStatusUnits(full, prepared.reminderMark)];
2317
2321
  if (Array.isArray(m.engineSegments) && m.engineSegments.length > 0) {
2318
- return m.engineSegments.map((s) => stripGitStatusUnits(full.slice(Math.max(0, s.start), Math.max(0, s.end))));
2322
+ return m.engineSegments.map((s) => stripGitStatusUnits(full.slice(Math.max(0, s.start), Math.max(0, s.end)), prepared.reminderMark));
2319
2323
  }
2320
2324
  if (typeof m.enginePrefixChars === "number" && m.enginePrefixChars > 0)
2321
- return [stripGitStatusUnits(full.slice(0, m.enginePrefixChars))];
2325
+ return [stripGitStatusUnits(full.slice(0, m.enginePrefixChars), prepared.reminderMark)];
2322
2326
  return [];
2323
2327
  });
2324
2328
  }
@@ -2407,7 +2411,8 @@ export class Runner {
2407
2411
  const text = typeof m.content === "string"
2408
2412
  ? m.content
2409
2413
  : m.content.map((b) => (b !== null && typeof b === "object" && b.type === "text" ? String(b.text ?? "") : "")).join("\n");
2410
- if (text.includes("<system-reminder>[final verification]") && rs.counters.finalVerifyInjections < 2)
2414
+ const fvMarked = m.engineMinted === true && text.includes(`<system-reminder mark="${prepared.reminderMark}">[final verification]`);
2415
+ if ((fvMarked || text.includes("<system-reminder>[final verification]")) && rs.counters.finalVerifyInjections < 2)
2411
2416
  rs.counters.finalVerifyInjections += 1;
2412
2417
  }
2413
2418
  else if (m.role === "toolResult" && !rs.counters.wroteThisRun && (prepared.toolEffects.get(m.toolName) ?? "write") !== "read") {
@@ -2687,7 +2692,8 @@ export class Runner {
2687
2692
  finalVerifyBudgetFill() < 0.9) {
2688
2693
  rs.counters.finalVerifyInjections += 1;
2689
2694
  if (rs.counters.finalVerifyInjections === 2) {
2690
- const reentryBody = "<system-reminder>[final verification] Your tool calls in this run worked with raw bytes, structural parsing, " +
2695
+ const reentryBody = openSystemReminder(prepared.reminderMark) +
2696
+ "[final verification] Your tool calls in this run worked with raw bytes, structural parsing, " +
2691
2697
  "or checksum/digest computation — the deliverable very likely embeds verifiable structure (structural fields, an " +
2692
2698
  "embedded checksum-family value, reference data it must match, or a replayable deterministic path). You MUST " +
2693
2699
  "execute the grounding check that structure supports — recompute the embedded value and compare it against the " +
@@ -2706,7 +2712,8 @@ export class Runner {
2706
2712
  },
2707
2713
  ];
2708
2714
  }
2709
- const nudgeBody = "<system-reminder>[final verification] Before finishing: re-verify the FINAL deliverable through its REAL entry point, " +
2715
+ const nudgeBody = openSystemReminder(prepared.reminderMark) +
2716
+ "[final verification] Before finishing: re-verify the FINAL deliverable through its REAL entry point, " +
2710
2717
  "exactly as the acceptance criteria would exercise it — execute the binary/function/endpoint directly and read the ACTUAL " +
2711
2718
  "output and exit code. Do NOT rely on earlier self-tests, shell redirections, or assumptions (a program that prints to " +
2712
2719
  "stdout is not a program that writes the required file). If anything mismatches the task's requirements, fix it before " +
@@ -2770,7 +2777,7 @@ export class Runner {
2770
2777
  messages.push({
2771
2778
  role: "user",
2772
2779
  engineMinted: true,
2773
- content: formatHookFeedback(`Stop hook additional context: ${result.additionalContext}`),
2780
+ content: formatHookFeedback(`Stop hook additional context: ${result.additionalContext}`, prepared.reminderMark),
2774
2781
  timestamp: Date.now(),
2775
2782
  });
2776
2783
  }
@@ -2790,7 +2797,7 @@ export class Runner {
2790
2797
  messages.push({
2791
2798
  role: "user",
2792
2799
  engineMinted: true,
2793
- content: formatHookFeedback(`Stop hook stopped continuation: ${result.block}`),
2800
+ content: formatHookFeedback(`Stop hook stopped continuation: ${result.block}`, prepared.reminderMark),
2794
2801
  timestamp: Date.now(),
2795
2802
  });
2796
2803
  }
@@ -2912,7 +2919,7 @@ export class Runner {
2912
2919
  const use = ref.overBudgetShrunk && frame.shrunk !== undefined
2913
2920
  ? { kind: "degraded", body: frame.shrunk.body, hash: frame.shrunk.hash }
2914
2921
  : { kind: frame.kind, body: frame.body, hash: frame.hash };
2915
- const wrapped = wrapGitFrame(use.body);
2922
+ const wrapped = wrapGitFrame(use.body, prepared.reminderMark);
2916
2923
  try {
2917
2924
  const entryId = await prepared.session.appendMessage({
2918
2925
  role: "user",
@@ -2923,7 +2930,7 @@ export class Runner {
2923
2930
  ref.announced = { kind: use.kind, hash: use.hash, entryId };
2924
2931
  ref.protectedText = wrapped;
2925
2932
  if (use.kind === "full" && frame.shrunk !== undefined) {
2926
- ref.wrappedShrink = { find: wrapped, replace: wrapGitFrame(frame.shrunk.body) };
2933
+ ref.wrappedShrink = { find: wrapped, replace: wrapGitFrame(frame.shrunk.body, prepared.reminderMark) };
2927
2934
  }
2928
2935
  else {
2929
2936
  delete ref.wrappedShrink;
@@ -3045,13 +3052,13 @@ export class Runner {
3045
3052
  rs.limits.budgetAxis = "cost";
3046
3053
  }
3047
3054
  else {
3048
- let continuation = resumeContinuation(resume);
3055
+ let continuation = resumeContinuation(resume, prepared.reminderMark);
3049
3056
  const bgSnapshot = resume.cp.state.runningBackgroundTasks;
3050
3057
  if (bgSnapshot !== undefined && bgSnapshot.length > 0) {
3051
3058
  const alive = new Set(prepared.listBackgroundTasks().map((t) => t.id));
3052
3059
  const orphans = bgSnapshot.filter((t) => !alive.has(t.id));
3053
3060
  if (orphans.length > 0) {
3054
- continuation += "\n\n" + formatHookFeedback(renderOrphanedBackgroundTasks(orphans));
3061
+ continuation += "\n\n" + formatHookFeedback(renderOrphanedBackgroundTasks(orphans), prepared.reminderMark);
3055
3062
  }
3056
3063
  }
3057
3064
  let gitResumeDelivered;
@@ -3065,11 +3072,11 @@ export class Runner {
3065
3072
  });
3066
3073
  const gitFrame = prepared.gitStatusRef.frame;
3067
3074
  if (gitBody !== undefined && gitFrame !== undefined) {
3068
- const wrappedGit = wrapGitFrame(gitBody);
3075
+ const wrappedGit = wrapGitFrame(gitBody, prepared.reminderMark);
3069
3076
  continuation += "\n\n" + wrappedGit;
3070
3077
  gitResumeDelivered = { kind: gitFrame.kind, hash: gitFrame.hash, wrapped: wrappedGit };
3071
3078
  if (gitFrame.kind === "full" && gitFrame.shrunk !== undefined) {
3072
- prepared.gitStatusRef.wrappedShrink = { find: wrappedGit, replace: wrapGitFrame(gitFrame.shrunk.body) };
3079
+ prepared.gitStatusRef.wrappedShrink = { find: wrappedGit, replace: wrapGitFrame(gitFrame.shrunk.body, prepared.reminderMark) };
3073
3080
  }
3074
3081
  }
3075
3082
  }
@@ -3083,7 +3090,7 @@ export class Runner {
3083
3090
  const projected = projectHumanInput({ text: steer.text, actor: steer.actor, source });
3084
3091
  continuation +=
3085
3092
  "\n\n" +
3086
- (steer.trusted ? formatHookFeedback(projected) : delimitUntrusted("supervisor steering message", projected));
3093
+ (steer.trusted ? formatHookFeedback(projected, prepared.reminderMark) : delimitUntrusted("supervisor steering message", projected));
3087
3094
  if (steer.trusted)
3088
3095
  engineSegments.push({ start, end: continuation.length });
3089
3096
  queue.push({
@@ -3131,11 +3138,11 @@ export class Runner {
3131
3138
  try {
3132
3139
  const decision = await userPromptSubmit(spec.objective, { identity: prepared.hookIdentity });
3133
3140
  if (decision?.block) {
3134
- prepared.blockedRef.reason = formatHookFeedback(decision.block);
3141
+ prepared.blockedRef.reason = formatHookFeedback(decision.block, prepared.reminderMark);
3135
3142
  promptBlocked = true;
3136
3143
  }
3137
3144
  else if (decision?.additionalContext) {
3138
- effectiveObjective = `${formatHookFeedback(decision.additionalContext)}\n\n${projectedObjective}`;
3145
+ effectiveObjective = `${formatHookFeedback(decision.additionalContext, prepared.reminderMark)}\n\n${projectedObjective}`;
3139
3146
  }
3140
3147
  }
3141
3148
  catch (hookErr) {
@@ -3146,7 +3153,7 @@ export class Runner {
3146
3153
  catch {
3147
3154
  }
3148
3155
  prepared.blockedRef.reason = formatHookFeedback(`the deployment's userPromptSubmit hook crashed while screening this prompt (${err.message}); ` +
3149
- `the prompt was NOT submitted (fail-closed)`);
3156
+ `the prompt was NOT submitted (fail-closed)`, prepared.reminderMark);
3150
3157
  promptBlocked = true;
3151
3158
  }
3152
3159
  }
@@ -3161,13 +3168,13 @@ export class Runner {
3161
3168
  });
3162
3169
  const gitFrame = prepared.gitStatusRef.frame;
3163
3170
  if (gitBody !== undefined && gitFrame !== undefined) {
3164
- const wrappedGit = wrapGitFrame(gitBody);
3171
+ const wrappedGit = wrapGitFrame(gitBody, prepared.reminderMark);
3165
3172
  const standalone = spec.images !== undefined && spec.images.length > 0;
3166
3173
  if (!standalone)
3167
3174
  effectiveObjective = `${wrappedGit}\n${effectiveObjective}`;
3168
3175
  gitLegDelivered = { kind: gitFrame.kind, hash: gitFrame.hash, standalone, wrapped: wrappedGit };
3169
3176
  if (gitFrame.kind === "full" && gitFrame.shrunk !== undefined) {
3170
- prepared.gitStatusRef.wrappedShrink = { find: wrappedGit, replace: wrapGitFrame(gitFrame.shrunk.body) };
3177
+ prepared.gitStatusRef.wrappedShrink = { find: wrappedGit, replace: wrapGitFrame(gitFrame.shrunk.body, prepared.reminderMark) };
3171
3178
  }
3172
3179
  }
3173
3180
  }
@@ -3205,7 +3212,7 @@ export class Runner {
3205
3212
  }
3206
3213
  }
3207
3214
  if (firstFrames.length > 0) {
3208
- effectiveObjective = `${firstFrames.map((f) => `<system-reminder>\n${sanitizeUntrustedText(f.body)}\n</system-reminder>`).join("\n")}\n${effectiveObjective}`;
3215
+ effectiveObjective = `${firstFrames.map((f) => mintSystemReminder(sanitizeUntrustedText(f.body), prepared.reminderMark)).join("\n")}\n${effectiveObjective}`;
3209
3216
  }
3210
3217
  }
3211
3218
  const gitQueuedChars = gitLegDelivered !== undefined && gitLegDelivered.standalone ? gitLegDelivered.wrapped.length : 0;
@@ -4385,8 +4392,8 @@ export class Runner {
4385
4392
  return l !== undefined && l !== name ? { label: l } : {};
4386
4393
  })();
4387
4394
  emit({ type: "tool_start", toolCallId: id, toolName: name, ...displayLabel, args: deferredArgs.get(id) ?? {} });
4388
- emit({ type: "tool_end", toolCallId: id, toolName: name, ...displayLabel, isError: true, ...toolEndBodyFrom({ content: formatHookFeedback(DEFERRED_REISSUE) }, true) });
4389
- const eid = await prepared.session.appendMessage(toolResultMsg(id, name, formatHookFeedback(DEFERRED_REISSUE), true));
4395
+ emit({ type: "tool_end", toolCallId: id, toolName: name, ...displayLabel, isError: true, ...toolEndBodyFrom({ content: formatHookFeedback(DEFERRED_REISSUE, prepared.reminderMark) }, true) });
4396
+ const eid = await prepared.session.appendMessage(toolResultMsg(id, name, formatHookFeedback(DEFERRED_REISSUE, prepared.reminderMark), true));
4390
4397
  emitCommitted(eid, "toolResult", id);
4391
4398
  }
4392
4399
  }
@@ -4407,8 +4414,8 @@ export class Runner {
4407
4414
  ? `The approval for the pending tool call "${pendingAction.toolName}" ended without anyone deciding it (it was cancelled or could not be delivered), so it was not executed.`
4408
4415
  : `The pending tool call "${pendingAction.toolName}" was denied by an approver.`;
4409
4416
  const reason = outcome.reason ? delimitUntrusted("reviewer note", outcome.reason, REVIEWER_NOTE_MAX_BODY) : defaultDenial;
4410
- emitEnd(true, { content: formatHookFeedback(reason) });
4411
- const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, formatHookFeedback(reason), true));
4417
+ emitEnd(true, { content: formatHookFeedback(reason, prepared.reminderMark) });
4418
+ const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, formatHookFeedback(reason, prepared.reminderMark), true));
4412
4419
  emitCommitted(eid, "toolResult", pendingAction.toolCallId);
4413
4420
  return;
4414
4421
  }
@@ -4431,7 +4438,7 @@ export class Runner {
4431
4438
  const beltUnsafe = rechecked.action === "deny" ||
4432
4439
  (rechecked.updatedInput !== undefined && !deepJsonEqual(rechecked.updatedInput, resolvedArgs));
4433
4440
  if (beltUnsafe) {
4434
- const editedDenial = formatHookFeedback(`The approver EDITED this call's input; the edited call is refused by the deployment's tool policy and was not executed${rechecked.action === "deny" && rechecked.message ? `: ${rechecked.message}` : " (the policy rewrote the edit to a value that was not approved)"}.`);
4441
+ const editedDenial = formatHookFeedback(`The approver EDITED this call's input; the edited call is refused by the deployment's tool policy and was not executed${rechecked.action === "deny" && rechecked.message ? `: ${rechecked.message}` : " (the policy rewrote the edit to a value that was not approved)"}.`, prepared.reminderMark);
4435
4442
  emitEnd(true, { content: editedDenial });
4436
4443
  const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, editedDenial, true));
4437
4444
  emitCommitted(eid, "toolResult", pendingAction.toolCallId);
@@ -4441,7 +4448,7 @@ export class Runner {
4441
4448
  const gateRealApproval = resume.cp.gate.kind === "irreversible_ask" ? resume.cp.gate.realApproval : undefined;
4442
4449
  const gateOrgGoverned = gateRealApproval?.origin === "org_rule" || gateRealApproval?.origin === "org_unavailable";
4443
4450
  if (gateOrgGoverned && prepared.permissionRuleOrg === undefined) {
4444
- const unwiredDenial = formatHookFeedback(`The approved tool call "${pendingAction.toolName}" was not executed: its approval was minted under organization governance (${gateRealApproval.origin}), and this worker has no org adjudication wiring — a governed approval may only be redeemed where governance can be enforced. This approval is spent; re-issue the call on an org-wired worker.`);
4451
+ const unwiredDenial = formatHookFeedback(`The approved tool call "${pendingAction.toolName}" was not executed: its approval was minted under organization governance (${gateRealApproval.origin}), and this worker has no org adjudication wiring — a governed approval may only be redeemed where governance can be enforced. This approval is spent; re-issue the call on an org-wired worker.`, prepared.reminderMark);
4445
4452
  emitEnd(true, { content: unwiredDenial });
4446
4453
  const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, unwiredDenial, true));
4447
4454
  emitCommitted(eid, "toolResult", pendingAction.toolCallId);
@@ -4472,7 +4479,7 @@ export class Runner {
4472
4479
  const orgDisclosures = org.status === "unavailable" && org.disclosures.length > 0
4473
4480
  ? ` Governance disclosures: ${inlineUntrusted(org.disclosures.join("; "), ORG_DISCLOSURE_MAX_CHARS)}.`
4474
4481
  : "";
4475
- const orgDenial = formatHookFeedback(`The approved tool call "${pendingAction.toolName}" was not executed: ${blocked}.${orgDisclosures} This approval is spent — the call has to be re-issued and approved again once organization policy permits it.`);
4482
+ const orgDenial = formatHookFeedback(`The approved tool call "${pendingAction.toolName}" was not executed: ${blocked}.${orgDisclosures} This approval is spent — the call has to be re-issued and approved again once organization policy permits it.`, prepared.reminderMark);
4476
4483
  emitEnd(true, { content: orgDenial });
4477
4484
  const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, orgDenial, true));
4478
4485
  emitCommitted(eid, "toolResult", pendingAction.toolCallId);
@@ -4488,7 +4495,7 @@ export class Runner {
4488
4495
  ...(prepared.cwdRef !== undefined ? { cwd: prepared.cwdRef.current } : {}),
4489
4496
  }, prepared.abortController.signal));
4490
4497
  if (narrowed.action === "deny") {
4491
- const narrowedDenial = formatHookFeedback(`The approved tool call "${pendingAction.toolName}" is now denied by a session rule and was not executed${narrowed.message ? `: ${narrowed.message}` : ""}.`);
4498
+ const narrowedDenial = formatHookFeedback(`The approved tool call "${pendingAction.toolName}" is now denied by a session rule and was not executed${narrowed.message ? `: ${narrowed.message}` : ""}.`, prepared.reminderMark);
4492
4499
  emitEnd(true, { content: narrowedDenial });
4493
4500
  const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, narrowedDenial, true));
4494
4501
  emitCommitted(eid, "toolResult", pendingAction.toolCallId);
@@ -1,5 +1,5 @@
1
1
  import type { AssistantMessage, StopReason, Tool, ToolResultMessage, Usage, UserMessage } from "../internal/llm.js";
2
- import type { Brain, Model, ModelRef, ModelRole, ModelRoles, ThinkingLevel } from "./types.js";
2
+ import type { Brain, Model, ModelRef, ModelRole, ModelRoles, TaskSpec, ThinkingLevel } from "./types.js";
3
3
  import { type DegradationInfo } from "../brain/degrading.js";
4
4
  /** A tool DEFINITION exposed to the model in a side query (never executed by the engine). */
5
5
  export interface SideQueryToolDef {
@@ -54,6 +54,16 @@ export interface SideQuerySpec {
54
54
  tools?: SideQueryToolDef[];
55
55
  /** Per-request output cap (provider max_tokens). */
56
56
  maxOutputTokens?: number;
57
+ /**
58
+ * Per-model auth — MIRRORS {@link TaskSpec.getApiKeyAndHeaders} (same signature, resolved per
59
+ * call against the RESOLVED model, exactly like the task path's per-call hook). The brain
60
+ * contract is `options.apiKey ?? config.apiKey`, and a model's own `baseUrl` outranks the
61
+ * brain's — so before this seat existed, a side query routed to a model carrying its own
62
+ * `baseUrl` + per-model key fell back to the brain's construction-time credential and sent the
63
+ * GATEWAY key to the per-model (possibly external) URL: a credential leak the task path already
64
+ * prevents. Absent ⇒ construction-time credentials apply, options byte-identical to before.
65
+ */
66
+ getApiKeyAndHeaders?: TaskSpec["getApiKeyAndHeaders"];
57
67
  signal?: AbortSignal;
58
68
  }
59
69
  export interface SideQueryResult {
@@ -44,10 +44,13 @@ export async function runSideQuery(spec, deps) {
44
44
  : {}),
45
45
  };
46
46
  const thinking = resolved.thinking ?? resolved.model.defaultThinking;
47
+ const auth = await spec.getApiKeyAndHeaders?.(resolved.model);
47
48
  const options = {
48
49
  ...(spec.signal !== undefined ? { signal: spec.signal } : {}),
49
50
  ...(spec.maxOutputTokens !== undefined ? { maxTokens: spec.maxOutputTokens } : {}),
50
51
  ...(thinking !== undefined ? { reasoning: thinking } : {}),
52
+ ...(auth?.apiKey !== undefined ? { apiKey: auth.apiKey } : {}),
53
+ ...(auth?.headers !== undefined ? { headers: auth.headers } : {}),
51
54
  };
52
55
  const stream = await Promise.resolve(deps.brain.stream(resolved.model, context, options));
53
56
  const msg = (await stream.result());
@@ -311,8 +311,11 @@ export interface ToolSpec<TParams extends TSchema = TSchema> {
311
311
  * behavior reads it, and on a task without an engine-memory session it is fully inert:
312
312
  * - `"external"` — the tool takes content from outside the deployment's trust boundary (network
313
313
  * fetch/search families, external protocol channels). Invoking it marks the session's memory
314
- * POLLUTED (one-way, durable): the session's harvest quarantines instead of committing, so
315
- * third-party text cannot ride the automatic path into long-term memory.
314
+ * EXTERNALLY EXPOSED (one-way, durable). What the mark does at harvest is the deployment's
315
+ * {@link RunnerDeps.memoryProvenance} mode's question (design/336): under `"carry"` (default)
316
+ * ordinary entries commit WITH an engine-minted `origin` marker and instruction-form files are
317
+ * withheld; under `"off"` the harvest quarantines instead of committing — either way,
318
+ * third-party text never rides the automatic path into long-term memory UNMARKED.
316
319
  * - `"execution"` — general execution (shell/exec families). NOT polluting by default — an
317
320
  * execution tool CAN reach external content indirectly, but excluding every shell session from
318
321
  * memory would disable memory for the main coding scenario; the write-side scans/fences remain
@@ -631,6 +634,15 @@ export interface ToolExecuteContext {
631
634
  * the tool runs outside a Runner task.
632
635
  */
633
636
  parentCwd?: string;
637
+ /**
638
+ * design/319 (A ticket) — the RUNNING task's reminder provenance mark, Runner-filled, read-only,
639
+ * NEVER a model/tool argument. The Agent tool's FORK route threads it into the forked child's
640
+ * trusted internals (`RunInternals.reminderMark` — the fork runs under the parent's byte-identical
641
+ * system-prompt declaration, so its engine mints must carry the PARENT's mark); spawn routes
642
+ * deliberately do NOT copy it (a spawned context mints its own — "one declaration, one mark").
643
+ * Undefined when the tool runs outside a Runner task.
644
+ */
645
+ reminderMark?: string;
634
646
  /**
635
647
  * Report usage spent in a nested run this tool spawned (e.g. a sub-agent). The Runner accumulates
636
648
  * it into the parent task's `TaskResult.stats.nested`, so delegated cost — the multi-agent "~15×"
@@ -666,7 +678,11 @@ export interface ToolExecuteContext {
666
678
  *
667
679
  * A delegation tool (`createSubagentTool`) forwards it into every child it spawns (sync / background /
668
680
  * fork), wrapped so the approver learns which delegation raised the ask ({@link
669
- * import("./tool-policy.js").AskDelegationProvenance}). Without this seat, a deployment that wires its
681
+ * import("./tool-policy.js").AskDelegationProvenance}); the workflow lane forwards the SAME frozen
682
+ * value into every workflow-spawned agent (backlog #342: `RunWorkflowToolDeps.parentOnAsk`, folded
683
+ * onto the governed baseline's base slot with the same provenance wrapper — the auto-mounted tool's
684
+ * execute ctx is minimal, so the value rides a mount dep there; a deployment-pinned `base.onAsk`
685
+ * wins). Without this seat, a deployment that wires its
670
686
  * approver per TASK (one closure per attached connection — the common shape) had it reach the host run
671
687
  * only: the child's own asks resolved at `deps.onAsk`, i.e. the headless auto-deny, while a live
672
688
  * operator sat attached to the parent. The ancestor-constraint chain
@@ -4708,10 +4724,13 @@ export interface EngineNotice {
4708
4724
  * `detail: { steer, taskId? }` / `{ followUp, taskId? }`. Per-run, at most once per family
4709
4725
  * (the terminal sweep is a single site).
4710
4726
  *
4711
- * - `"memory.session_polluted"` (design/178 §3, #324a) this session's memory crossed into the
4712
- * one-way POLLUTED state (a tool classified as an external content source was invoked, directly
4713
- * or through a delegated child): its memory writes are no longer eligible for the long-term
4714
- * library. The notice states that VERDICT plus what a harvest does when it collects — it does
4727
+ * - `"memory.session_polluted"` (design/178 §3, #324a; message mode-aware since design/336)
4728
+ * this session's memory crossed into the one-way externally-exposed state (a tool classified
4729
+ * as an external content source was invoked, directly or through a delegated child). Under
4730
+ * `memoryProvenance: "carry"` (default) the message states the tag-admit semantics (ordinary
4731
+ * writes commit with an origin marker; instruction-form files are withheld); under `"off"` it
4732
+ * keeps the pre-336 sentence — writes no longer eligible for the long-term library. The
4733
+ * notice states that VERDICT plus what a harvest does when it collects — it does
4715
4734
  * not promise quarantine, because a `writeScope`-null layering and a declared-unavailable
4716
4735
  * session both reach the zero-admission harvest arm (nothing collected ⇒ nothing captured), and
4717
4736
  * a mark landing after the pre-commit pollution read leaves already-committed rows to the
@@ -4929,6 +4948,27 @@ export interface RunnerDeps {
4929
4948
  * never truthiness).
4930
4949
  */
4931
4950
  memoryDelegationEvidence?: "static-face" | "attested-only";
4951
+ /**
4952
+ * design/336 §13-3 — the memory-provenance master switch (F-020 深修):
4953
+ * - `"carry"` (absent ≡ this; the default) — an externally exposed session's ORDINARY memory
4954
+ * writes are committed WITH an engine-minted `origin` marker that travels with the entry
4955
+ * (backend/sync/bundle); instruction-form files (type: feedback / pinned/triggers/applies-when
4956
+ * hints) from exposed sessions are withheld and quarantined; the derived index's session prose
4957
+ * still rolls back; the content scan gates run unchanged (a marker is not an exemption).
4958
+ * - `"off"` — the pre-336 behavior: no origin marker is minted and an exposed session's harvest
4959
+ * admits nothing (full containment, quarantined for host review). Standing committed markers
4960
+ * still carry forward on edits — "off" stops minting, it never strips facts already recorded.
4961
+ * ORTHOGONAL to {@link memoryDelegationEvidence} on purpose: that key decides WHEN a delegation
4962
+ * marks the session exposed (the evidence standard); this key decides WHAT the mark does to the
4963
+ * session's writes (tag-admit vs containment). All four combinations are legal and pinned.
4964
+ * DEPLOYMENT seat ONLY (same posture as {@link memoryDelegationEvidence}): no TaskSpec twin and
4965
+ * not in the governed workflow whitelist — a task author or governed script gets no channel to
4966
+ * change the provenance posture below its deployment; an org administers it through this seat.
4967
+ * Not frozen into checkpoints: a resumed leg follows the CURRENT deployment configuration. Any
4968
+ * other value refuses loudly at prepare (`config.memory_provenance`, #123 — exact spellings,
4969
+ * never truthiness).
4970
+ */
4971
+ memoryProvenance?: "off" | "carry";
4932
4972
  /**
4933
4973
  * design/199 件A — the DEPLOYMENT's read-face declaration
4934
4974
  * ({@link import("../tools/fs/read-face.js").ReadFace}; see {@link TaskSpec.readFace} for the
@@ -498,6 +498,41 @@ export interface WorkspaceState {
498
498
  export interface WorkspaceStateEntry extends SessionTreeEntryBase, WorkspaceState {
499
499
  type: "workspace_state";
500
500
  }
501
+ /** Bounded cap for {@link normalizeReminderMark}: the current scheme's value is 22 chars; the bound
502
+ * is deliberately looser (an opaque bounded token) so the STORE never learns the scheme — the real
503
+ * value-shape verify lives in the mint home (core/reminder-mint.ts `isValidReminderMark`), applied
504
+ * by the consumer, which RE-MINTS on failure. Oversize/off-alphabet ⇒ structurally invalid
505
+ * (forgery signal, same posture as the sibling caps). */
506
+ export declare const REMINDER_MARK_MAX_CHARS = 64;
507
+ /**
508
+ * design/319 (A ticket): the session's reminder provenance mark as a FIRST-CLASS typed entry —
509
+ * which mark value this session's engine-minted `<system-reminder>` open tags and system-prompt
510
+ * declaration carry ("one declaration, one mark"). Ordinary continuation turns (a new task on the
511
+ * same `spec.sessionId`) have NO checkpoint seed, so without this entry every leg would re-mint:
512
+ * the system prompt's declaration bytes would churn per leg (a cross-leg prefix-cache break) and
513
+ * every PRIOR leg's engine reminders — including standing frames the model consults later (the
514
+ * skills/agents listing, the git status frame) — would demote to data under the new declaration.
515
+ * Restore ladder (the design/150/155 rung doctrine): the checkpoint seed stays FIRST on a durable
516
+ * resume (cut atomically with the leaf); the trusted spawn channel (`RunInternals.reminderMark`,
517
+ * the fork lane) is SECOND; the branch-nearest entry of this type is THIRD (plain continuations —
518
+ * and fork children, whose forked session copies the parent's entry); a fresh mint last. SNAPSHOT
519
+ * semantics — nearest wins. A session store's fork copies committed history, so a fork inherits
520
+ * the parent's mark BY CONSTRUCTION through this entry (matching its byte-identical inherited
521
+ * prompt declaration).
522
+ */
523
+ export interface ReminderMarkEntry extends SessionTreeEntryBase {
524
+ type: "reminder_mark";
525
+ /** The opaque mark token (bounded; scheme-verified by the consumer, never by the store). */
526
+ mark: string;
527
+ }
528
+ /**
529
+ * design/319: strict shape gate for the reminder-mark entry — the SINGLE normalization the read
530
+ * walk, the public append, and the import-validate door all use (the {@link normalizeAnnouncedListing}
531
+ * posture). SCHEME-AGNOSTIC on purpose: a bounded url-safe token, not the current 22-char shape —
532
+ * the store carries the value opaquely; the mint home's verify port decides adoption (and a value
533
+ * it does not recognize is re-minted, never trusted loosely).
534
+ */
535
+ export declare function normalizeReminderMark(v: unknown): string | undefined;
501
536
  /** The closed kind set of a git-status announcement (the availability half of the `(kind, hash)`
502
537
  * comparison tuple): `full` = five-segment snapshot frame; `degraded` = branch+dirty two-line frame
503
538
  * (the snapshot round-trip failed while the basic probe succeeded); `unavailable` / `non-repo` =
@@ -591,7 +626,7 @@ export declare function normalizeAnnouncedListing(v: unknown): {
591
626
  models?: string[];
592
627
  } | undefined;
593
628
  /** All persisted session tree entry variants. */
594
- export type SessionTreeEntry = MessageEntry | ThinkingLevelChangeEntry | ModelChangeEntry | CompactionEntry | CustomEntry | CustomMessageEntry | LabelEntry | SessionInfoEntry | LeafEntry | PromptEpochEntry | AnnouncedListingEntry | GitAnnouncementEntry | WorkspaceStateEntry;
629
+ export type SessionTreeEntry = MessageEntry | ThinkingLevelChangeEntry | ModelChangeEntry | CompactionEntry | CustomEntry | CustomMessageEntry | LabelEntry | SessionInfoEntry | LeafEntry | PromptEpochEntry | AnnouncedListingEntry | GitAnnouncementEntry | WorkspaceStateEntry | ReminderMarkEntry;
595
630
  export interface SessionContext {
596
631
  messages: AgentMessage[];
597
632
  thinkingLevel: string;
@@ -725,6 +760,16 @@ export interface Session<TMetadata extends SessionMetadata = SessionMetadata> {
725
760
  /** design/155: nearest workspace-state snapshot on the active branch (the continuation rung of the
726
761
  * cwd/worktree restore ladder — the checkpoint seed stays first); undefined ⇒ no snapshot visible. */
727
762
  getWorkspaceState(): Promise<WorkspaceState | undefined>;
763
+ /** design/319: persist the session's reminder provenance mark as a first-class
764
+ * {@link ReminderMarkEntry} (see `Session.appendReminderMark`). OPTIONAL on the interface — same
765
+ * external-implementer posture as `getPromptEpoch`; consumers call it with `?.()` and an
766
+ * implementer without it degrades to a per-leg re-mint under the strict declaration (fail-safe),
767
+ * never a crash. */
768
+ appendReminderMark?(mark: string): Promise<string>;
769
+ /** design/319: nearest reminder-mark entry on the active branch (the continuation rung of the
770
+ * mark restore ladder — the checkpoint seed stays first, the trusted fork channel second).
771
+ * Undefined ⇒ no entry visible. OPTIONAL, same posture as `appendReminderMark`. */
772
+ getReminderMark?(): Promise<string | undefined>;
728
773
  /** The branch's current prompt-epoch descriptor (semantics: `Session.getPromptEpoch`). OPTIONAL on
729
774
  * the interface (typecheck 车 2026-07-24): the built-in `Session` class always implements it;
730
775
  * optionality keeps external `Session` implementers compatible. The consumer (`maybeCompact`'s