@sema-agent/core 7.30.1 → 7.31.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 (75) hide show
  1. package/CHANGELOG.md +242 -0
  2. package/dist/agents/cross-session-judge.d.ts +4 -12
  3. package/dist/agents/cross-session-judge.js +4 -11
  4. package/dist/agents/peer-session-drain.js +0 -5
  5. package/dist/agents/send-message-tool.js +1 -0
  6. package/dist/agents/subagent.d.ts +1 -0
  7. package/dist/agents/subagent.js +1 -0
  8. package/dist/core/ask-origin.d.ts +6 -0
  9. package/dist/core/ask-origin.js +10 -0
  10. package/dist/core/checkpoint-store.d.ts +14 -0
  11. package/dist/core/checkpoint-store.js +1 -0
  12. package/dist/core/engine-notice.d.ts +4 -0
  13. package/dist/core/governance-codes.d.ts +1 -1
  14. package/dist/core/governance-codes.js +3 -0
  15. package/dist/core/memory-engine/data-plane.d.ts +3 -1
  16. package/dist/core/memory-engine/data-plane.js +1 -1
  17. package/dist/core/memory-engine/delegation-settlement.d.ts +44 -3
  18. package/dist/core/memory-engine/delegation-settlement.js +38 -3
  19. package/dist/core/memory-engine/engine.d.ts +123 -26
  20. package/dist/core/memory-engine/engine.js +426 -149
  21. package/dist/core/memory-engine/entry-scan.d.ts +23 -0
  22. package/dist/core/memory-engine/entry-scan.js +55 -0
  23. package/dist/core/memory-engine/file-backend.d.ts +16 -23
  24. package/dist/core/memory-engine/file-backend.js +122 -99
  25. package/dist/core/memory-engine/layer-observations.d.ts +109 -0
  26. package/dist/core/memory-engine/layer-observations.js +148 -0
  27. package/dist/core/memory-engine/layout.d.ts +11 -0
  28. package/dist/core/memory-engine/layout.js +20 -4
  29. package/dist/core/memory-engine/types.d.ts +26 -2
  30. package/dist/core/parent-spec-seat.d.ts +4 -1
  31. package/dist/core/parent-spec-seat.js +2 -0
  32. package/dist/core/park-selfcheck.js +1 -0
  33. package/dist/core/runner/announce-once-ledger.d.ts +26 -0
  34. package/dist/core/runner/announce-once-ledger.js +37 -0
  35. package/dist/core/runner/contracts.d.ts +22 -5
  36. package/dist/core/runner/denial-limit-arms.d.ts +2 -2
  37. package/dist/core/runner/prepare-acquire-reconcile.d.ts +0 -4
  38. package/dist/core/runner/prepare-acquire-reconcile.js +1 -3
  39. package/dist/core/runner/prepare-file-history.d.ts +13 -15
  40. package/dist/core/runner/prepare-file-history.js +57 -43
  41. package/dist/core/runner/prepare-hands-readface.d.ts +2 -2
  42. package/dist/core/runner/prepare-listings.d.ts +3 -1
  43. package/dist/core/runner/prepare-listings.js +2 -2
  44. package/dist/core/runner/prepare-memory.js +16 -5
  45. package/dist/core/runner/prepare-park-ask.js +27 -16
  46. package/dist/core/runner/prepare-prompt-inputs.d.ts +8 -1
  47. package/dist/core/runner/prepare-prompt-inputs.js +5 -3
  48. package/dist/core/runner/prepare-task.js +7 -5
  49. package/dist/core/runner/prepare-wiring-manifest.d.ts +1 -0
  50. package/dist/core/runner/prepare-wiring-manifest.js +2 -1
  51. package/dist/core/runner/run-attachment-seats.js +1 -1
  52. package/dist/core/runner/run-leg.js +61 -52
  53. package/dist/core/runner/run-settle-and-teardown.js +4 -0
  54. package/dist/core/runner/run-turn-boundary.js +6 -6
  55. package/dist/core/runner/turn-attachments.d.ts +6 -6
  56. package/dist/core/runner-deps.d.ts +3 -1
  57. package/dist/core/secret-env.d.ts +11 -0
  58. package/dist/core/secret-env.js +20 -0
  59. package/dist/core/store-contracts/checkpoint-store-contract.d.ts +2 -0
  60. package/dist/core/store-contracts/checkpoint-store-contract.js +22 -0
  61. package/dist/core/task-result.d.ts +1 -1
  62. package/dist/core/task-spec.d.ts +17 -8
  63. package/dist/core/tool-spec.d.ts +1 -1
  64. package/dist/core/wiring-manifest.d.ts +10 -0
  65. package/dist/core/wiring-manifest.js +7 -1
  66. package/dist/engine/execution-env/node-execution-env.d.ts +6 -0
  67. package/dist/engine/execution-env/node-execution-env.js +15 -17
  68. package/dist/engine/harness/agent-harness.js +7 -5
  69. package/dist/engine/lsp/node-lsp-manager.d.ts +4 -0
  70. package/dist/engine/lsp/node-lsp-manager.js +10 -4
  71. package/dist/index.d.ts +1 -1
  72. package/dist/index.js +1 -1
  73. package/package.json +1 -1
  74. package/test/export-surface.snapshot.json +7 -1
  75. package/wire-changes/core@7.31.0.json +705 -0
@@ -30,7 +30,7 @@ import { nextHumanInputSeq } from "./steer-admission.js";
30
30
  import { settleTeardownLeg } from "./teardown-bounded.js";
31
31
  import { toolEndBodyFrom, toolResultMsg } from "./tool-end-body.js";
32
32
  import { announceListingTruncation } from "../skill-listing-budget.js";
33
- import { attachmentEnvelopeTags, commitAgentListing, commitSkillsListing, reduceToolEnd, renderAgentListingFrame, renderOrphanedBackgroundTasks, renderSkillsListingFrame, stampWriteAnchor } from "./turn-attachments.js";
33
+ import { attachmentEnvelopeTags, collectDateChange, commitAgentListing, commitSkillsListing, reduceToolEnd, renderAgentListingFrame, renderOrphanedBackgroundTasks, renderSkillsListingFrame, stampWriteAnchor } from "./turn-attachments.js";
34
34
  import { flushOwedAnnouncedSnapshot } from "./announce-once-ledger.js";
35
35
  import { asThrownError, thrownMarksOf, thrownText } from "../thrown-value.js";
36
36
  import { ruleSourcePhrase } from "../rule-denial-sentence.js";
@@ -235,22 +235,6 @@ export async function runLeg(input) {
235
235
  ...ident(),
236
236
  });
237
237
  }
238
- if (resume !== undefined)
239
- resume.decisionDelivered = true;
240
- if (gitResumeDelivered !== undefined) {
241
- const delivered = gitResumeDelivered;
242
- prepared.gitStatusRef.protectedText = delivered.wrapped;
243
- prepared.frameReceipts.pending.push({
244
- source: "git_status",
245
- text: continuation,
246
- commit: (entryId) => {
247
- prepared.gitStatusRef.announced = { kind: delivered.kind, hash: delivered.hash, entryId };
248
- queue.push({ type: "steering_injected", source: "git_status", preview: GIT_STATUS_ECHO_PREVIEW[delivered.kind], ...ident() });
249
- rs.attach.attachmentsInjected += 1;
250
- prepared.gitStatusRef.mirrorOwed = { kind: delivered.kind, hash: delivered.hash, entryId };
251
- },
252
- });
253
- }
254
238
  const pending = resume.cp.pendingAction;
255
239
  const rejectionHalt = pending.kind === "tool_approval" && resume.outcome.gate === "policy_ask"
256
240
  ? maybeHumanRejectionHalt({ gate: resume.outcome.gateOutcome, isDelegatedChild, toolName: pending.toolName, toolCallId: pending.toolCallId })
@@ -258,18 +242,36 @@ export async function runLeg(input) {
258
242
  if (rejectionHalt !== undefined && pending.kind === "tool_approval")
259
243
  prepared.batchHaltRef.current = { rejectedToolCallId: pending.toolCallId };
260
244
  prepared.undeliveredContinuationRef.current = { parkedSteers: resumeFrames.map(({ entry }) => entry).filter((e) => !withheldSteerIds.has(e.inputId)), orphanTasks };
261
- final = await withBrainSinks(() => prepared.harness.prompt(continuation, {
262
- engineSegments,
263
- settleUnansweredToolCalls: rejectionHalt !== undefined ? { halt: rejectionHalt } : {},
264
- onDelivered: () => {
265
- prepared.undeliveredContinuationRef.current = { parkedSteers: [], orphanTasks: [] };
266
- for (const e of appliedEvents)
267
- queue.push(e);
268
- },
269
- }));
270
- await flushGitMirror();
271
- if (prepared.gitStatusRef.announced?.pending === true)
272
- await prepared.gitStatusRef.reassert?.();
245
+ if (!prepared.abortController.signal.aborted) {
246
+ if (resume !== undefined)
247
+ resume.decisionDelivered = true;
248
+ if (gitResumeDelivered !== undefined) {
249
+ const delivered = gitResumeDelivered;
250
+ prepared.gitStatusRef.protectedText = delivered.wrapped;
251
+ prepared.frameReceipts.pending.push({
252
+ source: "git_status",
253
+ text: continuation,
254
+ commit: (entryId) => {
255
+ prepared.gitStatusRef.announced = { kind: delivered.kind, hash: delivered.hash, entryId };
256
+ queue.push({ type: "steering_injected", source: "git_status", preview: GIT_STATUS_ECHO_PREVIEW[delivered.kind], ...ident() });
257
+ rs.attach.attachmentsInjected += 1;
258
+ prepared.gitStatusRef.mirrorOwed = { kind: delivered.kind, hash: delivered.hash, entryId };
259
+ },
260
+ });
261
+ }
262
+ final = await withBrainSinks(() => prepared.harness.prompt(continuation, {
263
+ engineSegments,
264
+ settleUnansweredToolCalls: rejectionHalt !== undefined ? { halt: rejectionHalt } : {},
265
+ onDelivered: () => {
266
+ prepared.undeliveredContinuationRef.current = { parkedSteers: [], orphanTasks: [] };
267
+ for (const e of appliedEvents)
268
+ queue.push(e);
269
+ },
270
+ }));
271
+ await flushGitMirror();
272
+ if (prepared.gitStatusRef.announced?.pending === true)
273
+ await prepared.gitStatusRef.reassert?.();
274
+ }
273
275
  }
274
276
  }
275
277
  else {
@@ -339,6 +341,10 @@ export async function runLeg(input) {
339
341
  }
340
342
  }
341
343
  const firstFrames = [];
344
+ const legToday = rs.attach.dateState !== undefined ? prepared.dateChange.today() : undefined;
345
+ const dateFrame = legToday !== undefined ? collectDateChange(rs.attach.dateState, legToday) : undefined;
346
+ if (dateFrame !== undefined)
347
+ firstFrames.push({ source: "date_change", body: dateFrame.body, commit: () => void (rs.attach.dateState.announcedDate = legToday), disclose: () => undefined });
342
348
  if (rs.attach.attachState !== undefined) {
343
349
  const attach = rs.attach.attachState;
344
350
  if (rs.attach.agentListingOn && prepared.agentListing !== undefined) {
@@ -350,6 +356,7 @@ export async function runLeg(input) {
350
356
  body: frame.text,
351
357
  commit: () => {
352
358
  commitAgentListing(attach, listing.entries, listing.models);
359
+ prepared.frameReceipts.listingSnapshotOwed = true;
353
360
  prepared.announcedListingsRef.agents = listing.entries.map((e) => e.name);
354
361
  if (listing.models !== undefined)
355
362
  prepared.announcedListingsRef.models = [...listing.models];
@@ -367,15 +374,16 @@ export async function runLeg(input) {
367
374
  body: frame.text,
368
375
  commit: () => {
369
376
  commitSkillsListing(attach, listing.entries);
377
+ prepared.frameReceipts.listingSnapshotOwed = true;
370
378
  prepared.announcedListingsRef.skills = listing.entries.map((e) => e.name);
371
379
  },
372
380
  disclose: () => announceListingTruncation(runner.deps.onNotice, { listing: "skills", total: frame.listed, nameOnly: frame.nameOnly, dropped: frame.dropped, budget: listing.budget, sessionId: prepared.sessionId }),
373
381
  });
374
382
  }
375
383
  }
376
- if (firstFrames.length > 0) {
377
- effectiveObjective = `${firstFrames.map((f) => mintSystemReminder(sanitizeUntrustedText(f.body, attachmentEnvelopeTags(f.source)), prepared.reminderMark)).join("\n")}\n${effectiveObjective}`;
378
- }
384
+ }
385
+ if (firstFrames.length > 0) {
386
+ effectiveObjective = `${firstFrames.map((f) => mintSystemReminder(sanitizeUntrustedText(f.body, attachmentEnvelopeTags(f.source)), prepared.reminderMark)).join("\n")}\n${effectiveObjective}`;
379
387
  }
380
388
  if (composeApprovalPostureDirective(prepared.approvalPosture) !== undefined) {
381
389
  const postureDirective = composeApprovalPostureDirective(prepared.approvalPosture, await statedReminderBodiesOf(prepared.session, prepared.reminderMark));
@@ -451,32 +459,33 @@ export async function runLeg(input) {
451
459
  f.commit();
452
460
  queue.push({ type: "steering_injected", source: f.source, preview: redactThenCut(f.body, 220), ...ident() });
453
461
  rs.attach.attachmentsInjected += 1;
454
- prepared.frameReceipts.listingSnapshotOwed = true;
455
462
  f.disclose();
456
463
  },
457
464
  });
458
465
  }
459
466
  const enginePrefixChars = effectiveObjective.length - projectedObjective.length;
460
- queue.push({
461
- ...buildHumanInputEvent({
462
- carrier: "objective",
463
- source: "objective",
464
- delivery: "applied",
465
- sessionSeq: nextHumanInputSeq(prepared.harness),
467
+ if (!prepared.abortController.signal.aborted) {
468
+ queue.push({
469
+ ...buildHumanInputEvent({
470
+ carrier: "objective",
471
+ source: "objective",
472
+ delivery: "applied",
473
+ sessionSeq: nextHumanInputSeq(prepared.harness),
474
+ ...(objectiveActor !== undefined ? { actor: objectiveActor } : {}),
475
+ ...(objectiveActor?.issuer !== undefined ? { issuer: objectiveActor.issuer } : {}),
476
+ ...(spec.principal !== undefined ? { principal: spec.principal } : {}),
477
+ }),
478
+ ...ident(),
479
+ });
480
+ final = await withBrainSinks(() => prepared.harness.prompt(effectiveObjective, {
481
+ ...(images && images.length > 0 ? { images } : {}),
482
+ ...(enginePrefixChars > 0 ? { enginePrefixChars } : {}),
466
483
  ...(objectiveActor !== undefined ? { actor: objectiveActor } : {}),
467
- ...(objectiveActor?.issuer !== undefined ? { issuer: objectiveActor.issuer } : {}),
468
- ...(spec.principal !== undefined ? { principal: spec.principal } : {}),
469
- }),
470
- ...ident(),
471
- });
472
- final = await withBrainSinks(() => prepared.harness.prompt(effectiveObjective, {
473
- ...(images && images.length > 0 ? { images } : {}),
474
- ...(enginePrefixChars > 0 ? { enginePrefixChars } : {}),
475
- ...(objectiveActor !== undefined ? { actor: objectiveActor } : {}),
476
- }));
477
- await flushGitMirror();
478
- if (prepared.gitStatusRef.announced?.pending === true)
479
- await prepared.gitStatusRef.reassert?.();
484
+ }));
485
+ await flushGitMirror();
486
+ if (prepared.gitStatusRef.announced?.pending === true)
487
+ await prepared.gitStatusRef.reassert?.();
488
+ }
480
489
  }
481
490
  }
482
491
  loopLatch.ended = true;
@@ -19,6 +19,7 @@ import { resumeDecisionWasNegative } from "./decide-continuation.js";
19
19
  import { drainForwardedFramesBeforeDone, forwardsSubagentEvents } from "./prepare-run-refs.js";
20
20
  import { generatePromptSuggestions } from "./prompt-suggestions.js";
21
21
  import { settleTeardownLeg } from "./teardown-bounded.js";
22
+ import { owedDateAnchor } from "./announce-once-ledger.js";
22
23
  import { autoRunConsolidationFromRecommendation, autoRunOnRecommendationEffective, scopeHasUnconsumedRecommendation } from "./memory-consolidation-driver.js";
23
24
  import { amendTerminal, terminalProjection } from "./terminal-projection.js";
24
25
  import { mintCompactedEvent } from "../auto-compaction.js";
@@ -293,6 +294,9 @@ export async function runSettleAndTeardown(input) {
293
294
  }
294
295
  finally {
295
296
  settleOwedUserInput(owedUserInput, !prepared.suspendProgressRef.executedApproved, { taskId: spec.taskId ?? prepared.sessionId, sessionId: prepared.sessionId, runId: prepared.runId }, runner.deps.onNotice);
297
+ const writeDateAnchor = owedDateAnchor(prepared.session, prepared.dateChange, rs.attach.dateState);
298
+ if (writeDateAnchor !== undefined)
299
+ await settleTeardownLeg(writeDateAnchor, "date anchor (settle leg)", (e) => runner.deps.onError?.(e, { phase: "degraded", sessionId: prepared.sessionId }));
296
300
  const ownedEnvDying = prepared.ownedEnv !== undefined &&
297
301
  hasDestroy(prepared.ownedEnv) &&
298
302
  isIsolated(prepared.ownedEnv) &&
@@ -211,7 +211,7 @@ export function createTurnBoundary(input) {
211
211
  }
212
212
  let boundaryAttachmentBytes = 0;
213
213
  let attachmentsPayload;
214
- const listingCommits = [];
214
+ const receiptCommits = [];
215
215
  if ((rs.attach.dateState !== undefined ||
216
216
  rs.attach.instrState !== undefined ||
217
217
  rs.attach.sizeGuidelineState !== undefined ||
@@ -382,7 +382,7 @@ export function createTurnBoundary(input) {
382
382
  if (agentListingDue && due.some((a) => a.source === "agent_listing")) {
383
383
  const listing = prepared.agentListing;
384
384
  const frame = renderAgentListingFrame(attachState, listing.entries, listing.toolName, listing.models, listing.budget);
385
- listingCommits.push({
385
+ receiptCommits.push({
386
386
  source: "agent_listing",
387
387
  commit: () => {
388
388
  commitAgentListing(attachState, listing.entries, listing.models);
@@ -397,7 +397,7 @@ export function createTurnBoundary(input) {
397
397
  if (skillsListingDue && due.some((a) => a.source === "skills_listing")) {
398
398
  const listing = prepared.skillsListing;
399
399
  const frame = renderSkillsListingFrame(attachState, listing.entries, listing.budget);
400
- listingCommits.push({
400
+ receiptCommits.push({
401
401
  source: "skills_listing",
402
402
  commit: () => {
403
403
  commitSkillsListing(attachState, listing.entries);
@@ -410,12 +410,12 @@ export function createTurnBoundary(input) {
410
410
  if (announcedOnceDirty)
411
411
  prepared.frameReceipts.listingSnapshotOwed = true;
412
412
  }
413
- if (rs.attach.dateState !== undefined && rs.turn.lastTurnHadToolCalls) {
413
+ if (rs.attach.dateState !== undefined && rs.turn.lastTurnHadToolCalls && !prepared.frameReceipts.pending.some((r) => r.source === "date_change")) {
414
414
  const today = prepared.dateChange.today();
415
415
  const cand = collectDateChange(rs.attach.dateState, today);
416
416
  if (cand !== undefined) {
417
417
  due.unshift(cand);
418
- rs.attach.dateState.announcedDate = today;
418
+ receiptCommits.push({ source: "date_change", commit: () => void (rs.attach.dateState.announcedDate = today) });
419
419
  }
420
420
  }
421
421
  if (rs.attach.sizeGuidelineState !== undefined && rs.turn.lastTurnHadToolCalls) {
@@ -496,7 +496,7 @@ export function createTurnBoundary(input) {
496
496
  }
497
497
  if ((attachmentsPayload !== undefined || batchContextBlock !== undefined) && prepared.batchHaltRef.current === undefined) {
498
498
  const payload = attachmentsPayload !== undefined && batchContextBlock !== undefined ? `${attachmentsPayload}\n${batchContextBlock}` : (attachmentsPayload ?? batchContextBlock);
499
- for (const c of listingCommits)
499
+ for (const c of receiptCommits)
500
500
  prepared.frameReceipts.pending.push({ source: c.source, text: payload, commit: c.commit });
501
501
  void prepared.harness.steer(payload, { engineMinted: true }).catch(() => {
502
502
  prepared.frameReceipts.pending = prepared.frameReceipts.pending.filter((r) => r.text !== payload);
@@ -309,10 +309,10 @@ export interface AttachmentState {
309
309
  * A1-R — the date_change lane's OWN state, deliberately NOT an
310
310
  * {@link AttachmentState} member: allocating that for a date-only run would activate the cadence
311
311
  * content scan + projection reducers on every non-opted run (the reopen's exact finding). Two
312
- * fields, zero reducers. `announcedDate` starts at the leg's frozen prefix date; the RUN LOOP
313
- * commits it (the producer below is pure) — the date block rides ABOVE the 8KB bundle cap as a
314
- * fixed ~140-byte item, so no clipping face exists and commit-at-append is sound (contrast
315
- * tools_delta's drain-on-intact, which lives inside the capped bundle).
312
+ * fields, zero reducers. `announcedDate` starts at the session anchor's announced date; the RUN LOOP
313
+ * commits it (the producer below is pure) on the carrying message's receipt — the date block rides ABOVE
314
+ * the 8KB bundle cap as a fixed ~140-byte item, so no clipping face exists (contrast tools_delta's
315
+ * drain-on-intact, which lives inside the capped bundle).
316
316
  */
317
317
  export interface DateChangeState {
318
318
  announcedDate: string;
@@ -663,11 +663,11 @@ export declare function renderToolSearchUsageReminder(undiscovered: readonly str
663
663
  export declare function renderDateChange(newDate: string): string;
664
664
  /**
665
665
  * A1-R — the date_change producer (CC attachments.ts:1400-1443 parity): the system-prefix
666
- * date is FROZEN for the leg (upstream: "cached for the duration of the conversation" — re-rendering
666
+ * date is FROZEN for the session segment (upstream: "cached for the duration of the conversation" — re-rendering
667
667
  * the prefix at midnight costs ~920K effective tokens per overnight session, upstream's own comment);
668
668
  * when the boundary-time LOCAL date walks away from the last announced one, the model is told at the
669
669
  * TAIL instead. String INEQUALITY (backward NTP jumps announce too). PURE — the caller commits
670
- * `state.announcedDate = today` when it actually appends the attachment (see {@link DateChangeState}).
670
+ * `state.announcedDate = today` when the carrying message's receipt settles (see {@link DateChangeState}).
671
671
  */
672
672
  export declare function collectDateChange(state: DateChangeState, today: string): TurnAttachment | undefined;
673
673
  /**
@@ -520,7 +520,7 @@ export interface RunnerDeps {
520
520
  /**
521
521
  * design/381 — the per-edited-file rewind history backend (CC FileHistory topology; replaces the
522
522
  * E19 whole-tree `fileSnapshotStore`). WIRING IS THE OPT-IN (DV-9, the reference's own SDK
523
- * posture): with a store present, every Edit/Write/NotebookEdit first-touch is backed up before
523
+ * posture): with a store present (and {@link fileHistoryCapture} not `"off"`), every Edit/Write/NotebookEdit first-touch is backed up before
524
524
  * the byte changes, every turn mints a history boundary keyed by its own initial session entry
525
525
  * ("file state when this turn began"), and a {@link TaskSpec.resumeAt}+{@link TaskSpec.restoreFiles}
526
526
  * (or {@link TaskSpec.rewindFilesTo}) run converges the tracked set back to a boundary. Omitted ⇒
@@ -534,6 +534,8 @@ export interface RunnerDeps {
534
534
  * key earns on a filesystem-capable run (`classification: "file-history-no-store"`).
535
535
  */
536
536
  fileHistoryStore?: import("./file-history-store.js").FileHistoryStore | "disabled";
537
+ /** The deployment's CAPTURE switch, apart from the store seat: `"off"` (the only word, and only beside a wired store; anything else refuses prepare, `config.invalid_file_history_capture`) ⇒ no run records — no first-touch record, no turn boundary, no fork carry, no liveness advisory, manifest `fileHistory: "disabled"` — while a wired {@link fileHistoryStore} still serves {@link TaskSpec.restoreFiles} / {@link TaskSpec.rewindFilesTo} from what it already holds. */
538
+ fileHistoryCapture?: "off";
537
539
  /**
538
540
  * design/381 DV-14 (终审落锤) — what an Edit/Write/NotebookEdit does when its FIRST-TOUCH history
539
541
  * record cannot be persisted (`FileHistoryStore.trackEdit` answered `{ok:false}` = NO durable
@@ -17,6 +17,7 @@
17
17
  * per-adapter concern (each exec env owns how it composes its child env); only the secret PATTERN is shared.
18
18
  */
19
19
  import type { RedactionConfidence, RedactionSource } from "./arg-summary.js";
20
+ import { type EngineNotice } from "./engine-notice.js";
20
21
  /** Which rule classified a key as secret-shaped — the env arm's `kind` vocabulary. */
21
22
  export type SecretEnvFindingKind = "suffix-rule" | "exact-name";
22
23
  /** One env entry the `scrub` policy dropped: the key (the VALUE is never carried — this record is meant
@@ -42,3 +43,13 @@ export declare function isSecretEnvKey(key: string): boolean;
42
43
  * dropped and why, read it back after the call. Omitting it leaves this function byte-for-byte what it
43
44
  * was before the channel existed. */
44
45
  export declare function scrubSecretEnv(env: NodeJS.ProcessEnv, findings?: SecretEnvFinding[]): NodeJS.ProcessEnv;
46
+ /** Test seam: never called by production code. */
47
+ export declare function __resetSecretEnvScrubNoticeAnnouncement(): void;
48
+ /**
49
+ * The ONE disclosure point for a scrub's withheld keys. Routed through {@link deliverEngineNotice} — the
50
+ * engine's one notice throat — so a wired `onNotice` seat REPLACES the console line exactly as every
51
+ * other `EngineNotice` does, and an absent seat prints the same words `console.warn` always printed here.
52
+ * `detail.names` carries every withheld key. Once per PROCESS, never per call or per site; an EMPTY
53
+ * findings array is a no-op.
54
+ */
55
+ export declare function announceSecretEnvScrub(onNotice: ((notice: EngineNotice) => void) | undefined, findings: readonly SecretEnvFinding[]): void;
@@ -1,3 +1,4 @@
1
+ import { deliverEngineNotice } from "./engine-notice.js";
1
2
  const TOKEN_COUNT_WORDS = ["MAX", "OUTPUT", "INPUT", "THINKING", "NUM", "TOTAL"];
2
3
  const SECRET_ENV_RE = new RegExp(`(^|_)(KEY|KEYS|TOKEN|(?<!(?:^|_)(?:${TOKEN_COUNT_WORDS.join("|")})_)TOKENS|SECRET|SECRETS|PASSWORD|PASSWORDS|PASSWD|CREDENTIAL|CREDENTIALS|PRIVATEKEY|APIKEY|ACCESS_KEY|SESSION_TOKEN)(?:_(?:\\d+|ID))?$`, "i");
3
4
  const SECRET_ENV_EXACT_NAMES = new Set([
@@ -7,6 +8,10 @@ const SECRET_ENV_EXACT_NAMES = new Set([
7
8
  ]);
8
9
  const NON_SECRET_ENV_EXACT_NAMES = new Set([
9
10
  "PARTITION_KEY", "SORT_KEY", "RANGE_KEY", "IDEMPOTENCY_KEY", "CACHE_KEY", "ROUTING_KEY",
11
+ "DB_FOREIGN_KEYS", "JSON_SORT_KEYS",
12
+ "MAX_NEW_TOKENS", "MAX_COMPLETION_TOKENS", "AIDER_MAP_TOKENS", "AIDER_MAX_CHAT_HISTORY_TOKENS",
13
+ "MAX_BATCH_PREFILL_TOKENS", "MAX_WAITING_TOKENS", "MAX_TOP_N_TOKENS", "RATE_LIMIT_TOKENS",
14
+ "PROMPT_TOKENS", "CONTEXT_TOKENS",
10
15
  ]);
11
16
  const SECRET_ENV_KIND_CONFIDENCE = {
12
17
  "exact-name": "high",
@@ -45,3 +50,18 @@ export function scrubSecretEnv(env, findings) {
45
50
  }
46
51
  return out;
47
52
  }
53
+ let secretEnvScrubNoticeAnnounced = false;
54
+ export function __resetSecretEnvScrubNoticeAnnouncement() {
55
+ secretEnvScrubNoticeAnnounced = false;
56
+ }
57
+ export function announceSecretEnvScrub(onNotice, findings) {
58
+ if (findings.length === 0 || secretEnvScrubNoticeAnnounced)
59
+ return;
60
+ secretEnvScrubNoticeAnnounced = true;
61
+ const names = findings.map((f) => f.key);
62
+ deliverEngineNotice(onNotice, {
63
+ code: "config.secret_env_scrubbed",
64
+ message: `secret_env_scrubbed: ${findings.length} env key(s) withheld from a spawned child's environment (${names.join(", ")}) — once per process; wire onNotice for the structured form`,
65
+ detail: { names },
66
+ });
67
+ }
@@ -50,6 +50,7 @@ export declare const EXPECTED_LISTBYSCOPE_SUMMARIES: ({
50
50
  toolCallId: string;
51
51
  toolName: string;
52
52
  toolInput: string;
53
+ mandated: boolean;
53
54
  origin: string;
54
55
  spentMicroUsd?: undefined;
55
56
  } | {
@@ -66,5 +67,6 @@ export declare const EXPECTED_LISTBYSCOPE_SUMMARIES: ({
66
67
  toolCallId?: undefined;
67
68
  toolName?: undefined;
68
69
  toolInput?: undefined;
70
+ mandated?: undefined;
69
71
  origin?: undefined;
70
72
  })[];
@@ -343,6 +343,26 @@ export async function checkpointStoreContract(make, runAssertion) {
343
343
  assert.equal("previewWithheld" in byId.get("pw-absent"), false, "absent must project the key OMITTED, not null/empty");
344
344
  assert.equal("previewWithheld" in byId.get("pw-bogus"), false, "an out-of-contract spelling must read as absent, never project verbatim");
345
345
  });
346
+ run("#1079 mandated presence bit: a row's `true` survives + projects; absent stays absent; an out-of-contract `false` or string reads as absent", async () => {
347
+ const store = make();
348
+ const asserted = createCheckpointFixture({ token: mintCheckpointToken(), sessionId: "md-true" });
349
+ asserted.pendingAction.mandated = true;
350
+ const absent = createCheckpointFixture({ token: mintCheckpointToken(), sessionId: "md-absent" });
351
+ const falsed = createCheckpointFixture({ token: mintCheckpointToken(), sessionId: "md-false" });
352
+ falsed.pendingAction.mandated = false;
353
+ const stringed = createCheckpointFixture({ token: mintCheckpointToken(), sessionId: "md-string" });
354
+ stringed.pendingAction.mandated = "yes";
355
+ for (const cp of [asserted, absent, falsed, stringed])
356
+ await store.put(cp.token, cp);
357
+ const back = (await store.get(asserted.token)).pendingAction;
358
+ assert.equal(back.mandated, true, "the row's mandated bit must survive the round-trip verbatim");
359
+ assert.equal("mandated" in (await store.get(absent.token)).pendingAction, false, "an absent bit must stay OMITTED on the row");
360
+ const byId = new Map((await store.listByScope("tenant-a")).map((s) => [s.sessionId, s]));
361
+ assert.equal(byId.get("md-true")?.mandated, true);
362
+ assert.equal("mandated" in byId.get("md-absent"), false, "absent must project the key OMITTED, not null/false");
363
+ assert.equal("mandated" in byId.get("md-false"), false, "a written false is out-of-contract and must read as absent, never project");
364
+ assert.equal("mandated" in byId.get("md-string"), false, "a non-boolean spelling must read as absent, never project verbatim");
365
+ });
346
366
  await settle();
347
367
  }
348
368
  const sortByToken = (a, b) => a.token.localeCompare(b.token);
@@ -367,6 +387,7 @@ export async function checkpointListByScopeSummaries(store) {
367
387
  pendingAction: {
368
388
  kind: "tool_approval",
369
389
  origin: "policy",
390
+ mandated: true,
370
391
  toolCallId: "call-pr",
371
392
  toolName: "open_pr",
372
393
  args: { repo: "x" },
@@ -406,6 +427,7 @@ export const EXPECTED_LISTBYSCOPE_SUMMARIES = [
406
427
  toolCallId: "call-pr",
407
428
  toolName: "open_pr",
408
429
  toolInput: '{"repo":"x"}',
430
+ mandated: true,
409
431
  origin: "policy",
410
432
  },
411
433
  {
@@ -402,7 +402,7 @@ export interface TaskResult {
402
402
  * ignores the field is unchanged. A rewind that could NOT be delivered as asked is NOT a note — it is a
403
403
  * terminal failure (`rewind_snapshot.unresolvable` / `rewind.store_unconfigured` /
404
404
  * `rewind.restore_failed` / `rewind.invalid_spec` / `rewind.rewind_files_retired` /
405
- * `rewind.conflicting_targets` / `rewind.child_scope_unsupported`).
405
+ * `rewind.conflicting_targets` / `rewind.child_scope_unsupported` / `rewind.capture_disabled`).
406
406
  *
407
407
  * - `conversation_only` — {@link TaskSpec.resumeAt} branched the transcript WITHOUT
408
408
  * {@link TaskSpec.restoreFiles}, so the working tree was deliberately left where it was (CC's
@@ -213,12 +213,12 @@ export interface TaskSpec {
213
213
  * that could support this without a second root is a recorded design candidate, not built.)
214
214
  *
215
215
  * File-history interaction (design/381): with {@link restoreFiles}, the `"before"` file anchor is the
216
- * target's OWN entry id first — history boundaries are keyed by a turn's initial entry and mean "file
217
- * state when that turn began", so B(target) IS the before-state. When the target carries no boundary
218
- * (a mixed-epoch session, or a non-turn-initial target), the anchor falls back to the nearest
219
- * boundary-carrying ANCESTOR, so files and context land on the same side of the cut. If no such
220
- * boundary exists, prepare fails loud (`rewind_snapshot.unresolvable`) instead of silently leaving
221
- * the files at their pre-rewind state while the conversation moved back.
216
+ * target's OWN entry id — history boundaries are keyed by a turn's initial entry and mean "file state
217
+ * when that turn began", so B(target) IS the before-state. A target with no boundary of its own (a
218
+ * steer / follow-up entry, a turn that recorded no history, a mixed-epoch session) refuses at prepare
219
+ * (`rewind_snapshot.unresolvable`) with the files untouched: an earlier boundary would also revert edits
220
+ * the conversation keeps, so the anchor never falls back to an ancestor and never crosses into an
221
+ * earlier turn.
222
222
  * Fail-loud: setting this without {@link resumeAt}, or an unknown mode value, rejects at prepare.
223
223
  */
224
224
  resumeAtMode?: "at" | "before";
@@ -311,6 +311,15 @@ export interface TaskSpec {
311
311
  count?: number;
312
312
  role?: ModelRole;
313
313
  };
314
+ /**
315
+ * The per-run OFF SWITCH for file history (`"disabled"`, the only word; `checkpointStore`'s spelling). Omitted ⇒
316
+ * the deployment (`RunnerDeps.fileHistoryStore` / `fileHistoryCapture`) decides. `"disabled"` ⇒ this run makes no history-store call: no first-touch record
317
+ * (the edit proceeds), no turn boundary, no fork carry, no liveness advisory; a restore request ({@link restoreFiles}
318
+ * / {@link rewindFilesTo}) refuses `rewind.capture_disabled` (the store may be wired; the request turned it off).
319
+ * The manifest reports `fileHistory: "disabled"`. Children inherit it (parent spec seat; a retained child woken by
320
+ * such a run, for that cycle); no word turns it back on below. Any other value refuses (`rewind.invalid_spec`).
321
+ */
322
+ fileHistory?: "disabled";
314
323
  /**
315
324
  * design/381 (DV-9) — the RESTORE half of file rewind: when this task uses {@link resumeAt} to branch at
316
325
  * a prior entry, ALSO converge the working tree's **agent-edited files** (the tracked set — files this
@@ -323,8 +332,8 @@ export interface TaskSpec {
323
332
  * Fail-loud: setting this without {@link resumeAt} rejects at prepare (`rewind.invalid_spec`) — a
324
333
  * restore with no anchor is a contradiction, not a default.
325
334
  *
326
- * There is NO capture flag anymore: first-touch tracking is ALWAYS ON whenever a
327
- * `RunnerDeps.fileHistoryStore` is wired (per-edited-file backups are near-zero cost — no tree walk,
335
+ * There is NO capture request anymore: first-touch tracking is ON whenever a `RunnerDeps.fileHistoryStore`
336
+ * is wired, `RunnerDeps.fileHistoryCapture` is not `"off"`, and no {@link fileHistory} off switch is set (per-edited-file backups are near-zero cost — no tree walk,
328
337
  * no size gate), which is what retired the old `rewindFiles` capture request. The retired
329
338
  * `rewindFiles` spelling is handled at prepare: its capture sense (true, no `resumeAt`) is a
330
339
  * tolerated no-op with a deprecation disclosure (the intent is structurally satisfied); its restore
@@ -752,7 +752,7 @@ export interface ToolExecuteContext {
752
752
  parentCwd?: string;
753
753
  /**
754
754
  * The RUNNING task's file-history LINEAGE (Runner-filled, read-only, never a model/tool argument;
755
- * present iff a `RunnerDeps.fileHistoryStore` is live for the run): the history scope this run
755
+ * present iff the run records file history — a `RunnerDeps.fileHistoryStore` wired, capture not `"off"`, not `"disabled"`): the history scope this run
756
756
  * records its first-touch edits into and the TREE those records' keys are minted against — the
757
757
  * canonical root spelling plus the env's filesystem identity (`fs`: a remote workspace's handle,
758
758
  * `"host"` for the control-plane host's filesystem, else a per-env-instance token).
@@ -407,6 +407,14 @@ export interface WiringManifest {
407
407
  autoConsolidation?: {
408
408
  onRecommendation: true;
409
409
  };
410
+ /**
411
+ * EFFECTIVE half only, ALWAYS present on an engine-minted effective manifest: does this leg capture file history.
412
+ * `"on"` — it records (store wired, neither layer declared it off, a filesystem env mounted). `"disabled"` — it does
413
+ * not, whichever layer or missing half made it so (`TaskSpec.fileHistory`, `RunnerDeps.fileHistoryStore: "disabled"`, `RunnerDeps.fileHistoryCapture: "off"`,
414
+ * no store, no filesystem env); the host knows its own deployment seat. ABSENT = an older mint or an external
415
+ * derivation, never a state. Out of {@link configFingerprint}: the request word is a per-task seat (`hooks` reasoning).
416
+ */
417
+ fileHistory?: "on" | "disabled";
410
418
  /** EFFECTIVE half only — a short, non-sensitive fingerprint (sha256 prefix over the canonical
411
419
  * JSON of this manifest's own resolved facts; every field here is an enum/boolean/count, no
412
420
  * secrets) so an operator can correlate legs that ran under the same resolved assembly. */
@@ -498,6 +506,8 @@ export interface WiringFacts {
498
506
  /** Effective half only — `RunnerDeps.autoRunOnRecommendation === true`
499
507
  * ({@link WiringManifest.autoConsolidation}); false ⇒ no section (byte-identical manifest). */
500
508
  autoConsolidationOnRecommendation?: boolean;
509
+ /** Effective half only — the file-history phase's word ({@link WiringManifest.fileHistory}); the engine always passes it. */
510
+ fileHistory?: "on" | "disabled";
501
511
  }
502
512
  /** Named view of the deps seats the static half reads (a `Pick` of the real {@link RunnerDeps} —
503
513
  * single-source shapes, no parallel hand-copied interface). */
@@ -84,6 +84,11 @@ function readAutoModeFact(fact) {
84
84
  }
85
85
  return { armed, reason, ...(deniedSource !== undefined ? { deniedSource } : {}) };
86
86
  }
87
+ function readFileHistoryFact(word) {
88
+ if (word !== "on" && word !== "disabled")
89
+ throw new Error(`the file-history wiring fact ${JSON.stringify(word)} is not one of on|disabled`);
90
+ return word;
91
+ }
87
92
  export function deriveWiringManifest(facts) {
88
93
  if (facts.half === "static" && facts.leg !== undefined) {
89
94
  throw new Error("a static wiring manifest has no leg — leg identity is an effective-half fact");
@@ -146,13 +151,14 @@ export function deriveWiringManifest(facts) {
146
151
  ? { readDeny: { builtinTiers: [...facts.readDenyBuiltinTiersActive] } }
147
152
  : {}),
148
153
  ...(facts.half === "effective" && facts.autoConsolidationOnRecommendation === true ? { autoConsolidation: { onRecommendation: true } } : {}),
154
+ ...(facts.half === "effective" && facts.fileHistory !== undefined ? { fileHistory: readFileHistoryFact(facts.fileHistory) } : {}),
149
155
  ...(facts.half === "effective" && facts.teamRole !== undefined ? { teamRole: facts.teamRole } : {}),
150
156
  ...(facts.half === "effective" && facts.peerPresence !== undefined && (facts.peerPresence.agent !== undefined || facts.peerPresence.intent !== undefined)
151
157
  ? { peerPresence: { ...(facts.peerPresence.agent !== undefined ? { agent: facts.peerPresence.agent } : {}), ...(facts.peerPresence.intent !== undefined ? { intent: facts.peerPresence.intent } : {}) } }
152
158
  : {}),
153
159
  };
154
160
  if (facts.half === "effective") {
155
- const { leg: _leg, mcp: _mcp, tools: _tools, lsp: _lsp, hooks: _hooks, teamRole: _teamRole, peerPresence: _peerPresence, ...assembly } = manifest;
161
+ const { leg: _leg, mcp: _mcp, tools: _tools, lsp: _lsp, hooks: _hooks, teamRole: _teamRole, peerPresence: _peerPresence, fileHistory: _fileHistory, ...assembly } = manifest;
156
162
  const { provenance: _askSeat, ...askForHash } = assembly.ask;
157
163
  const { provenance: _questionSeat, ...questionForHash } = assembly.question;
158
164
  manifest.configFingerprint = createHash("sha256")
@@ -1,5 +1,6 @@
1
1
  import { type ExecResult, type ExecutionEnv, ExecutionError, type ExecutionEnvExecOptions, FileError, type FileInfo, type Result } from "../harness/types.js";
2
2
  import { type SecretEnvFinding } from "../../core/secret-env.js";
3
+ import type { EngineNotice } from "../../core/engine-notice.js";
3
4
  import type { SymlinkChain } from "../harness/types.js";
4
5
  import type { BackgroundShellCapability, BackgroundShellId, BackgroundPoll, BackgroundSpawnOptions } from "../../core/background-shell.js";
5
6
  import { BackgroundShellError } from "../../core/background-shell.js";
@@ -106,6 +107,7 @@ export declare class NodeExecutionEnv implements ExecutionEnv, BackgroundShellCa
106
107
  private bgCounter;
107
108
  private readonly retainBackgroundProcesses;
108
109
  private readonly onSecretEnvScrub?;
110
+ private readonly onNotice?;
109
111
  /** design/128 T1-1: lazily-created env-owned spool dir (mkdtemp under os tmp — unpredictable, never the cwd). */
110
112
  private bgSpoolDir?;
111
113
  readonly backgroundCapabilities: BackgroundShellCapability["backgroundCapabilities"];
@@ -135,6 +137,10 @@ export declare class NodeExecutionEnv implements ExecutionEnv, BackgroundShellCa
135
137
  * "scrub" leg withholds from a shell child. Absent ⇒ a once-per-process console summary (loud
136
138
  * enough to notice, quiet enough not to spam). Never called on the "all"/allowlist legs. */
137
139
  onSecretEnvScrub?: (findings: readonly SecretEnvFinding[]) => void;
140
+ /** #1081 — the structured-notice seat for the SAME withheld-keys disclosure (`EngineNotice`,
141
+ * `code: "config.secret_env_scrubbed"`, `detail.names`); independent of `onSecretEnvScrub` (a raw
142
+ * findings collector) except: that seat wired alone stays console-silent, as before this existed. */
143
+ onNotice?: (notice: EngineNotice) => void;
138
144
  });
139
145
  get schedulerCapabilities(): SchedulerCapability["schedulerCapabilities"];
140
146
  schedule(intent: ScheduledIntent, ctx: SchedulerContext): Promise<Result<{