@sema-agent/core 5.21.1 → 5.23.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 (96) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/dist/agents/send-message-tool.js +6 -3
  3. package/dist/agents/subagent.d.ts +6 -0
  4. package/dist/agents/subagent.js +48 -6
  5. package/dist/brain/errors.d.ts +20 -0
  6. package/dist/brain/errors.js +40 -0
  7. package/dist/brain/retry.d.ts +16 -2
  8. package/dist/brain/retry.js +3 -2
  9. package/dist/brain/status-sink.d.ts +9 -2
  10. package/dist/brain/stream-engine.d.ts +22 -0
  11. package/dist/brain/stream-engine.js +41 -10
  12. package/dist/core/ask-class.d.ts +48 -0
  13. package/dist/core/ask-class.js +33 -0
  14. package/dist/core/checkpoint-store.d.ts +103 -10
  15. package/dist/core/checkpoint-store.js +3 -1
  16. package/dist/core/governance-codes.d.ts +38 -0
  17. package/dist/core/governance-codes.js +14 -0
  18. package/dist/core/hooks.d.ts +101 -1
  19. package/dist/core/hooks.js +116 -14
  20. package/dist/core/locked-config.d.ts +7 -1
  21. package/dist/core/locked-config.js +2 -1
  22. package/dist/core/memory-engine/delegation-provenance.d.ts +62 -0
  23. package/dist/core/memory-engine/delegation-provenance.js +26 -0
  24. package/dist/core/memory-engine/engine.d.ts +94 -1
  25. package/dist/core/memory-engine/engine.js +329 -12
  26. package/dist/core/memory-engine/header-hints.d.ts +30 -0
  27. package/dist/core/memory-engine/header-hints.js +41 -0
  28. package/dist/core/memory-engine/index.d.ts +3 -2
  29. package/dist/core/memory-engine/index.js +3 -2
  30. package/dist/core/memory-engine/layout.d.ts +232 -0
  31. package/dist/core/memory-engine/layout.js +469 -1
  32. package/dist/core/memory-engine/tools.d.ts +30 -0
  33. package/dist/core/memory-engine/tools.js +108 -17
  34. package/dist/core/permission-rule-consent.d.ts +25 -9
  35. package/dist/core/permission-rule-consent.js +93 -21
  36. package/dist/core/permission-rule-model.d.ts +9 -1
  37. package/dist/core/permission-rule-model.js +2 -2
  38. package/dist/core/permission-rule-org.d.ts +195 -0
  39. package/dist/core/permission-rule-org.js +234 -0
  40. package/dist/core/permission-rule-store.d.ts +249 -6
  41. package/dist/core/permission-rule-store.js +314 -3
  42. package/dist/core/permission-rule-sync.d.ts +139 -0
  43. package/dist/core/permission-rule-sync.js +343 -0
  44. package/dist/core/runner/prepare-memory.js +35 -8
  45. package/dist/core/runner/prepare-task.d.ts +64 -3
  46. package/dist/core/runner/prepare-task.js +357 -32
  47. package/dist/core/runner/runtask.js +166 -6
  48. package/dist/core/shared-memory/contract.js +19 -4
  49. package/dist/core/shared-memory/normalize.d.ts +3 -1
  50. package/dist/core/shared-memory/tools.js +73 -17
  51. package/dist/core/shared-memory/types.d.ts +27 -1
  52. package/dist/core/store-contracts/permission-rule-sync-contract.d.ts +33 -0
  53. package/dist/core/store-contracts/permission-rule-sync-contract.js +186 -0
  54. package/dist/core/task-notification.d.ts +5 -2
  55. package/dist/core/task-registry-agent.d.ts +1 -1
  56. package/dist/core/task-registry-agent.js +6 -2
  57. package/dist/core/task-registry-shared.d.ts +9 -2
  58. package/dist/core/task-registry.d.ts +9 -3
  59. package/dist/core/task-registry.js +2 -0
  60. package/dist/core/tool-policy.d.ts +155 -4
  61. package/dist/core/tool-policy.js +148 -10
  62. package/dist/core/tool-result-store.d.ts +9 -1
  63. package/dist/core/tool-result-store.js +2 -1
  64. package/dist/core/trace.d.ts +79 -1
  65. package/dist/core/types.d.ts +94 -3
  66. package/dist/core/wiring-manifest.d.ts +16 -1
  67. package/dist/core/wiring-manifest.js +7 -1
  68. package/dist/index.d.ts +15 -8
  69. package/dist/index.js +12 -5
  70. package/dist/orchestration/goal.d.ts +10 -0
  71. package/dist/orchestration/goal.js +6 -5
  72. package/dist/stores/file/adoption/adopt.d.ts +146 -0
  73. package/dist/stores/file/adoption/adopt.js +616 -0
  74. package/dist/stores/file/adoption/marker.d.ts +194 -0
  75. package/dist/stores/file/adoption/marker.js +198 -0
  76. package/dist/stores/file/background-agent-store.js +2 -0
  77. package/dist/stores/file/checkpoint-store.d.ts +4 -0
  78. package/dist/stores/file/checkpoint-store.js +3 -0
  79. package/dist/stores/file/file-snapshot-store.js +2 -0
  80. package/dist/stores/file/index.d.ts +2 -0
  81. package/dist/stores/file/index.js +4 -0
  82. package/dist/stores/file/mailbox-store.js +2 -0
  83. package/dist/stores/file/memory-store.js +2 -0
  84. package/dist/stores/file/permission-rule-adopt.d.ts +62 -0
  85. package/dist/stores/file/permission-rule-adopt.js +95 -0
  86. package/dist/stores/file/permission-rule-store.d.ts +80 -2
  87. package/dist/stores/file/permission-rule-store.js +189 -46
  88. package/dist/stores/file/session-policy-store.js +2 -0
  89. package/dist/stores/file/session-store.js +2 -0
  90. package/dist/stores/file/task-list-store.js +2 -0
  91. package/dist/stores/file/tool-result-store.js +2 -0
  92. package/dist/stores/file/usage-window-store.js +2 -0
  93. package/dist/stores/file/workflow-journal-store.js +2 -0
  94. package/dist/stores/file/workflow-run-store.js +2 -0
  95. package/dist/tools/fs/bash-readonly-classifier.js +59 -10
  96. package/package.json +3 -2
@@ -1,7 +1,9 @@
1
1
  import { decisionText, refuseOutOfContractDecision } from "./tool-policy.js";
2
+ import { brandPolicyAskClass } from "./ask-class.js";
2
3
  import { inlineUntrusted } from "./untrusted-text.js";
3
4
  import { ASK_USER_QUESTION_TOOL_NAME } from "./ask-question.js";
4
5
  import { createSafeNotifier } from "./safe-notify.js";
6
+ import { ORG_ADJUDICATION_TIMEOUT_MS, ORG_RULE_DECISION_REASON, ORG_UNAVAILABLE_DECISION_REASON, settleOrgVerdictWithin } from "./permission-rule-org.js";
5
7
  export function cloneObserverInput(input) {
6
8
  try {
7
9
  return structuredClone(input);
@@ -99,7 +101,7 @@ function screenPreToolUseResult(r) {
99
101
  return refuseOutOfContractDecision(r);
100
102
  }
101
103
  export function createPreToolUseConstraintPolicy(preToolUse, env, onCrash) {
102
- return {
104
+ return brandPolicyAskClass({
103
105
  check: async (req) => {
104
106
  let r;
105
107
  try {
@@ -128,7 +130,7 @@ export function createPreToolUseConstraintPolicy(preToolUse, env, onCrash) {
128
130
  }
129
131
  return r.updatedInput !== undefined ? { action: "allow", updatedInput: r.updatedInput } : { action: "allow" };
130
132
  },
131
- };
133
+ }, "hook");
132
134
  }
133
135
  function traceHookCrash(input, err, notifier) {
134
136
  notifier.notify(() => input.onHookError?.(err), "toolGate.onHookError");
@@ -245,12 +247,73 @@ export async function runToolGate(input) {
245
247
  denySource = input.shellGated === true ? "shellGate" : "safety";
246
248
  }
247
249
  }
250
+ let orgRealApprovalRequired = false;
251
+ let orgTightenCount = 0;
252
+ const applyOrgLayer = async (current, args) => {
253
+ if (input.orgRules === undefined)
254
+ return current;
255
+ if (toolName === ASK_USER_QUESTION_TOOL_NAME && input.orgRules.contentAskToolMounted === true)
256
+ return current;
257
+ let decided = current;
258
+ const answer = await settleOrgVerdictWithin(input.orgRules
259
+ .adjudicate({ toolName, args, toolCallId })
260
+ .catch((err) => ({
261
+ status: "unavailable",
262
+ disclosures: [`the org adjudication face threw: ${err instanceof Error ? err.message : String(err)}`],
263
+ })), {
264
+ status: "unavailable",
265
+ disclosures: [`the org adjudication face did not answer within ${ORG_ADJUDICATION_TIMEOUT_MS}ms (or the task ended first)`],
266
+ }, { ...(input.abortSignal !== undefined ? { signal: input.abortSignal } : {}), timeoutMs: ORG_ADJUDICATION_TIMEOUT_MS });
267
+ if (answer.status === "unavailable") {
268
+ orgRealApprovalRequired = true;
269
+ await notifier.notifyAsync(() => input.orgRules?.onUnavailable?.({ toolName, toolCallId, message: answer.disclosures.join("; ") }), "toolGate.orgSnapshotUnavailable");
270
+ if (decided.action === "allow") {
271
+ decided = {
272
+ action: "ask",
273
+ message: `tool "${toolName}" needs approval: this deployment is org-governed and cannot currently adjudicate ` +
274
+ `against an organization policy snapshot — every allow tightens to a real approval until it can`,
275
+ decisionReason: ORG_UNAVAILABLE_DECISION_REASON,
276
+ requiresRealApproval: true,
277
+ };
278
+ orgTightenCount += 1;
279
+ denySource = "org";
280
+ }
281
+ else if (decided.action === "ask" && decided.requiresRealApproval !== true) {
282
+ decided = { ...decided, requiresRealApproval: true };
283
+ orgTightenCount += 1;
284
+ }
285
+ return decided;
286
+ }
287
+ if (answer.verdict === undefined)
288
+ return decided;
289
+ const { behavior, rule } = answer.verdict;
290
+ if (behavior === "deny") {
291
+ orgTightenCount += 1;
292
+ denySource = "org";
293
+ return { action: "deny", message: `an organization policy rule (${rule}) denies this call`, decisionReason: ORG_RULE_DECISION_REASON };
294
+ }
295
+ if (decided.action === "deny")
296
+ return decided;
297
+ orgTightenCount += 1;
298
+ denySource = "org";
299
+ orgRealApprovalRequired = true;
300
+ return decided.action === "ask"
301
+ ? { ...decided, requiresRealApproval: true }
302
+ : {
303
+ action: "ask",
304
+ message: `an organization policy rule (${rule}) requires approval for this call`,
305
+ decisionReason: ORG_RULE_DECISION_REASON,
306
+ requiresRealApproval: true,
307
+ };
308
+ };
309
+ decision = await applyOrgLayer(decision, policyRewrite !== undefined ? policyRewrite : currentInput);
248
310
  if (decision.action === "ask") {
249
311
  if (policyRewrite !== undefined) {
250
312
  currentInput = policyRewrite;
251
313
  req.args = policyRewrite;
252
314
  }
253
315
  if (input.persistedRules &&
316
+ !orgRealApprovalRequired &&
254
317
  decision.action === "ask" &&
255
318
  decision.requiresRealApproval !== true &&
256
319
  decision.decisionReason !== "hook" &&
@@ -268,6 +331,7 @@ export async function runToolGate(input) {
268
331
  }
269
332
  }
270
333
  if (input.autoMode &&
334
+ !orgRealApprovalRequired &&
271
335
  decision.action === "ask" &&
272
336
  req.toolName !== ASK_USER_QUESTION_TOOL_NAME &&
273
337
  input.isMarkedUnresolvable?.(input.event.toolCallId) !== true) {
@@ -293,6 +357,29 @@ export async function runToolGate(input) {
293
357
  denySource = "classifier";
294
358
  }
295
359
  }
360
+ if (input.sandboxAdmission !== undefined &&
361
+ decision.action === "ask" &&
362
+ req.toolName !== ASK_USER_QUESTION_TOOL_NAME &&
363
+ input.isMarkedUnresolvable?.(input.event.toolCallId) !== true &&
364
+ decision.requiresRealApproval !== true &&
365
+ decision.decisionReason !== "hook" &&
366
+ hookAsk === undefined &&
367
+ input.egress !== true &&
368
+ input.sandboxAdmission.boundaryCapable !== true) {
369
+ const recorded = input.sandboxAdmission.askClassesOf(toolCallId);
370
+ const admissible = recorded !== undefined && recorded.length > 0 && recorded.every((r) => r.cls === "sandbox_local");
371
+ if (admissible) {
372
+ const askClasses = recorded;
373
+ input.sandboxAdmission.onAdmitted({ toolCallId, toolName, askClasses, boundary: "sandbox_internal" });
374
+ preToolContext.push(`permission.sandbox_admitted: this call's pending approval was auto-admitted because the deployment declares an isolated sandbox execution env and the request stays inside it. Recorded for audit.`);
375
+ decision = {
376
+ action: "allow",
377
+ message: "admitted inside the deployment-declared sandbox (permission.sandbox_admitted)",
378
+ decisionReason: "sandbox",
379
+ ...(policyRewrite !== undefined ? { updatedInput: policyRewrite } : {}),
380
+ };
381
+ }
382
+ }
296
383
  const egressTool = input.egress === true;
297
384
  const irreversibleTool = input.irreversibility === "always" || input.irreversibility === "maybe";
298
385
  const safety = egressTool || irreversibleTool ? { egress: egressTool, irreversible: irreversibleTool } : undefined;
@@ -394,23 +481,34 @@ export async function runToolGate(input) {
394
481
  editArgs = hr.updatedInput;
395
482
  }
396
483
  }
397
- if (!adjudicate)
484
+ if (!adjudicate && input.orgRules === undefined)
398
485
  break;
399
- let recheck;
400
- try {
401
- recheck = await adjudicate({ toolName, args: editArgs, toolCallId });
402
- }
403
- catch (err) {
404
- recheck = {
405
- action: "deny",
406
- message: `policy re-check of the approved edit for "${toolName}" errored: ${err instanceof Error ? err.message : String(err)}`,
407
- };
486
+ let recheck = { action: "allow" };
487
+ if (adjudicate) {
488
+ try {
489
+ recheck = await adjudicate({ toolName, args: editArgs, toolCallId });
490
+ }
491
+ catch (err) {
492
+ recheck = {
493
+ action: "deny",
494
+ message: `policy re-check of the approved edit for "${toolName}" errored: ${err instanceof Error ? err.message : String(err)}`,
495
+ };
496
+ }
408
497
  }
409
498
  if (recheck.action === "deny") {
410
499
  editDenied = recheck;
411
500
  denySource = "policy";
412
501
  break;
413
502
  }
503
+ if (recheck.updatedInput !== undefined)
504
+ editArgs = recheck.updatedInput;
505
+ const orgTightensBefore = orgTightenCount;
506
+ recheck = await applyOrgLayer(recheck, editArgs);
507
+ const orgRaisedThisRound = orgTightenCount > orgTightensBefore;
508
+ if (recheck.action === "deny") {
509
+ editDenied = recheck;
510
+ break;
511
+ }
414
512
  if (recheck.action === "allow") {
415
513
  if (recheck.updatedInput !== undefined)
416
514
  editArgs = recheck.updatedInput;
@@ -421,11 +519,15 @@ export async function runToolGate(input) {
421
519
  const rr = await resolveAsk(recheck, { toolName, args: editArgs, toolCallId });
422
520
  if (rr.action !== "allow") {
423
521
  editDenied = rr;
424
- denySource = "policy";
522
+ if (!orgRaisedThisRound)
523
+ denySource = "policy";
425
524
  break;
426
525
  }
427
- if (rr.updatedInput === undefined)
526
+ if (rr.updatedInput === undefined) {
527
+ if (rr.presentedInput !== undefined)
528
+ editArgs = rr.presentedInput;
428
529
  break;
530
+ }
429
531
  editArgs = rr.updatedInput;
430
532
  }
431
533
  decision = editDenied ?? { ...decision, updatedInput: editArgs };
@@ -18,7 +18,7 @@ export declare const LOCKED_KEY_REGISTRY: {
18
18
  readonly specFields: readonly ["mcp"];
19
19
  };
20
20
  readonly toolPolicy: {
21
- readonly specFields: readonly ["toolPolicy"];
21
+ readonly specFields: readonly ["toolPolicy", "basePolicyForResumeEdit"];
22
22
  };
23
23
  readonly compliancePosture: {
24
24
  readonly specFields: readonly [];
@@ -46,6 +46,12 @@ export interface LockedPreflight {
46
46
  * always enforced, resolved ONCE here. With `toolPolicy` locked, the deployment's own
47
47
  * `deps.toolPolicy` is the whole answer (a spec-supplied policy has already been refused). */
48
48
  toolPolicy: ToolPolicy | undefined;
49
+ /** F-012 (codex round 8): the effective resume-edit re-adjudication OVERRIDE, resolved lock-aware in
50
+ * the SAME slot as {@link toolPolicy}. With `toolPolicy` locked, a spec-supplied
51
+ * `basePolicyForResumeEdit` has already been refused wholesale, so this is the deployment's own
52
+ * `deps.basePolicyForResumeEdit` (or undefined — the caller-policy fallback then applies at the use
53
+ * site). Unlocked: `spec.basePolicyForResumeEdit ?? deps.basePolicyForResumeEdit`. */
54
+ basePolicyForResumeEdit: ToolPolicy | undefined;
49
55
  }
50
56
  /**
51
57
  * Validate a deployment's lock declaration against the closed registry. Fail-loud on any unknown
@@ -1,6 +1,6 @@
1
1
  export const LOCKED_KEY_REGISTRY = {
2
2
  mcp: { specFields: ["mcp"] },
3
- toolPolicy: { specFields: ["toolPolicy"] },
3
+ toolPolicy: { specFields: ["toolPolicy", "basePolicyForResumeEdit"] },
4
4
  compliancePosture: { specFields: [] },
5
5
  retentionPolicy: { specFields: [] },
6
6
  };
@@ -38,5 +38,6 @@ export function preflightLockedConfig(spec, deps) {
38
38
  lockedKeys,
39
39
  mcp: lockedKeys.has("mcp") ? undefined : spec.mcp,
40
40
  toolPolicy: lockedKeys.has("toolPolicy") ? deps.toolPolicy : (spec.toolPolicy ?? deps.toolPolicy),
41
+ basePolicyForResumeEdit: lockedKeys.has("toolPolicy") ? deps.basePolicyForResumeEdit : (spec.basePolicyForResumeEdit ?? deps.basePolicyForResumeEdit),
41
42
  };
42
43
  }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * design/180 half A — delegation RUNTIME provenance: the three-value attestation and its monotonic
3
+ * aggregate. The static tool-face judgment ({@link import("./content-origin.js").delegationCallIsExternal})
4
+ * stays the PERMANENT FLOOR: runtime evidence may only retire the static verdict when a COMPLETE
5
+ * clean proof is in hand; any evidence gap falls back to the floor. "We could not prove it clean"
6
+ * and "it is clean" are different answers — replacing the floor outright would fail OPEN on every
7
+ * evidence gap (old child vintages, crashes, un-armed sessions).
8
+ *
9
+ * The aggregate is two MONOTONIC bits plus versioning:
10
+ * - `sawExternal` — an actually-observed polluting-class invocation (own recorder, or a delivered
11
+ * child attestation of `external`). Once set, never cleared.
12
+ * - `incomplete` — some stretch of the session's conduct is unprovable (a delegation delivered no /
13
+ * an unknown attestation, a resume lost the aggregate, the recorder was not armed from turn 0).
14
+ * Once set, never cleared.
15
+ * Reduction order is FIXED (design/180 r3): `sawExternal ⇒ external` > `incomplete ⇒ unknown` >
16
+ * `clean` — an observed external event always outranks an evidence gap (missing evidence can make
17
+ * "clean" unprovable; it can never make an observed event disappear).
18
+ */
19
+ /** The three-value delegation attestation. Only `clean` changes behavior (the parent skips the
20
+ * static mark); `unknown` is byte-equivalent to absence (static floor governs); `external` marks
21
+ * unconditionally, with the observed-event reason. */
22
+ export type DelegationAttestation = "clean" | "external" | "unknown";
23
+ /** The monotonic per-run provenance aggregate (in-process this slice; the durable-checkpoint
24
+ * persistence leg rides the background-face slice — absence after a resume reads `incomplete`,
25
+ * which is the fail-closed default this shape is built around). */
26
+ export interface DelegationProvenanceAggregate {
27
+ version: 1;
28
+ /** Turn index the recorder was armed from. 0 ⇔ armed at prepare (full coverage). */
29
+ coverageStartTurn: number;
30
+ sawExternal: boolean;
31
+ incomplete: boolean;
32
+ }
33
+ /** The frozen content-safety snapshot a parent hands its delegated children (design/180 A-2): the
34
+ * classification config the child's recorder must judge under. Inherit-narrow-only — see
35
+ * {@link narrowContentSafety}. */
36
+ export interface DelegationContentSafety {
37
+ trustedTools: readonly string[];
38
+ execIsExternalContent: boolean;
39
+ }
40
+ export declare function newDelegationProvenanceAggregate(): DelegationProvenanceAggregate;
41
+ /**
42
+ * Effective content-safety for a run: the parent chain's frozen snapshot ∩ the run's own config.
43
+ * A child may NARROW what the chain trusts (fewer trusted tools, stricter exec), never widen it —
44
+ * a wider child-side allowlist must not launder a chain-external tool into `local` on the way up.
45
+ * At least one input must be present (the caller only computes this when armed).
46
+ */
47
+ export declare function narrowContentSafety(parent: DelegationContentSafety | undefined, own: {
48
+ trustedTools: ReadonlySet<string>;
49
+ execIsExternalContent: boolean;
50
+ } | undefined): {
51
+ trustedTools: Set<string>;
52
+ execIsExternalContent: boolean;
53
+ };
54
+ /** Reduce an aggregate to the delivered attestation. `completed` = the child reached its terminal
55
+ * normally with its full result delivered — a salvage/partial/errored child can never prove clean
56
+ * (its conduct after the cut is unobserved), though an OBSERVED external event still reports. */
57
+ export declare function reduceDelegationAttestation(agg: DelegationProvenanceAggregate | undefined, opts: {
58
+ completed: boolean;
59
+ }): DelegationAttestation;
60
+ /** Read a delivered card's attestation field (the explicit-whitelist handback seat). Anything but
61
+ * the three spellings — including absence — reads as undefined, i.e. the static floor governs. */
62
+ export declare function readCardAttestation(details: unknown): DelegationAttestation | undefined;
@@ -0,0 +1,26 @@
1
+ export function newDelegationProvenanceAggregate() {
2
+ return { version: 1, coverageStartTurn: 0, sawExternal: false, incomplete: false };
3
+ }
4
+ export function narrowContentSafety(parent, own) {
5
+ if (parent === undefined) {
6
+ return { trustedTools: new Set(own?.trustedTools ?? []), execIsExternalContent: own?.execIsExternalContent ?? false };
7
+ }
8
+ const fromParent = new Set(parent.trustedTools);
9
+ const trustedTools = own === undefined ? fromParent : new Set([...fromParent].filter((t) => own.trustedTools.has(t)));
10
+ return { trustedTools, execIsExternalContent: parent.execIsExternalContent || (own?.execIsExternalContent ?? false) };
11
+ }
12
+ export function reduceDelegationAttestation(agg, opts) {
13
+ if (agg === undefined)
14
+ return "unknown";
15
+ if (agg.sawExternal)
16
+ return "external";
17
+ if (agg.incomplete || !opts.completed || agg.version !== 1 || agg.coverageStartTurn !== 0)
18
+ return "unknown";
19
+ return "clean";
20
+ }
21
+ export function readCardAttestation(details) {
22
+ if (details === null || typeof details !== "object")
23
+ return undefined;
24
+ const v = details.attestation;
25
+ return v === "clean" || v === "external" || v === "unknown" ? v : undefined;
26
+ }
@@ -1,4 +1,4 @@
1
- import { type MemoryPartitionIncidentSink, type RetrievedAccountRow, type SessionPollutionRecord } from "./layout.js";
1
+ import { type ChallengeAssignment, type ChallengeEvent, type ControlPlaneRebuildReceipt, type StrictControlPlaneLedger, type ChallengedHistoryRow, type LineagePendingTxn, type LineagePromotion, type MemoryPartitionIncidentSink, type RetrievedAccountRow, type SessionPollutionRecord } from "./layout.js";
2
2
  import type { HarvestReport, MemoryAnnouncement, MemoryBackend, MemorySessionHandle, ScanFinding } from "./types.js";
3
3
  /**
4
4
  * The CC `# Memory` instruction section — VERBATIM from the live capture
@@ -31,6 +31,15 @@ export declare function buildMemoryInstruction(memoryDir: string, instructionFil
31
31
  * dual-root instruction merge ("the write plane's instruction wins") never sees a second candidate.
32
32
  */
33
33
  export declare const MEMORY_RECALL_DISCIPLINE = "Before answering questions about earlier work, decisions, dates, people, or the user's preferences, look them up: `memory_search` finds entries by keyword and `memory_get` reads a full entry \u2014 the injected memory index only lists what exists. When a lookup comes up empty, say that you checked memory and found nothing instead of guessing.";
34
+ /**
35
+ * design/180 B-3 — the preference-header discipline paragraph. Same carriage as
36
+ * {@link MEMORY_RECALL_DISCIPLINE}: a runner-appended segment OUTSIDE `inject()`, so the
37
+ * CC-verbatim `# Memory` instruction stays byte-identical to its sha256-pinned capture. Appended
38
+ * only when a WRITE channel exists (prepare-memory gates it) — the lines it teaches are frontmatter
39
+ * the MODEL writes; the engine has no write arm for them (writing usage evidence for the model
40
+ * would forge the very account the three-tier discipline forbids forging).
41
+ */
42
+ export declare const MEMORY_PREFERENCE_DISCIPLINE = "When the user confirms a stored preference or fact still holds, refresh that entry's `last-confirmed: <YYYY-MM-DD>` frontmatter line (add it when absent). When you save a preference, add an `applies-when: <context>` frontmatter line naming when it applies. Both are plain frontmatter lines \u2014 write them yourself; nothing fills them in for you.";
34
43
  /** CC index-injection parameters: MEMORY.md's first 200 lines / 25KB enter the prompt. */
35
44
  export declare const MEMORY_INDEX_MAX_LINES = 200;
36
45
  export declare const MEMORY_INDEX_MAX_BYTES: number;
@@ -188,6 +197,89 @@ export declare class MemoryEngine {
188
197
  /** The session's pollution record (in-process mark first, then the durable marker) — undefined =
189
198
  * clean. Reads are side-effect-free. */
190
199
  sessionPollution(sessionId: string): SessionPollutionRecord | undefined;
200
+ /** Entry ids the session's memory tools RETURNED this process — the retrieved half of the
201
+ * challenged-history mechanical event (B-3). In-process on purpose: "same session" is one runner
202
+ * process by architecture, and the durable retrieved account is not session-keyed. */
203
+ private readonly retrievedThisSession;
204
+ /**
205
+ * The model-visible read faces' EXCLUSION view: entry id → why it is withheld.
206
+ * - `challenged` — an unresolved S-grade challenge generation exists (B-2');
207
+ * - `lineage_pending` — the dirty latch: a staged lineage row for this id has not been settled
208
+ * (credentialed-but-unpromoted, or the undecidable crash window) — "the account is not
209
+ * settled, the content does not go on the table" (r4).
210
+ * THROWS {@link import("./layout.js").ControlPlaneCorruptError} when either ledger's integrity is
211
+ * unknowable — consumers must refuse service loudly (corrupt ≠ clean), the same law the pollution
212
+ * marker already follows.
213
+ */
214
+ readChallengeExclusions(): Map<string, {
215
+ code: "challenged" | "lineage_pending";
216
+ generation?: number;
217
+ at?: number;
218
+ }>;
219
+ /** The shared challenge writer: append events idempotently, then announce OPAQUELY (id + code +
220
+ * generation only — the announcement is a model-visible face, so no name/description/free reason
221
+ * rides it; content stays on the host audit face). Failures are disclosed, never thrown past. */
222
+ private challengeAndAnnounce;
223
+ /** design/180 §10 — one disclosure per PROCESS per engine, not one per append: the size is a
224
+ * standing property of the account, and repeating it on every challenge is the spam that gets
225
+ * advisories muted (the same argument the durable-park disclosures are built on). */
226
+ private challengeLedgerSizeDisclosed;
227
+ /**
228
+ * design/180 §10 — the challenge ledger's size bound is a DISCLOSURE threshold, never a cap: the
229
+ * ledger cannot be rotated (the exclusion predicate is derived from the events, so dropping the
230
+ * oldest rows would un-withhold their entries), so crossing {@link CHALLENGE_LEDGER_MAX_EVENTS}
231
+ * changes nothing about what is withheld — it tells the deployment that an account on the
232
+ * per-tool-call read path has grown to where it costs the session latency.
233
+ */
234
+ private discloseChallengeLedgerSize;
235
+ /**
236
+ * design/180 §10 — the RECOVERY channel for a corrupt fail-closed ledger (lineage / challenges).
237
+ *
238
+ * The fail-closed family refuses every read on corruption so a silent "start fresh" cannot launder
239
+ * taint; permanent refusal is the point, and it left a deployment with genuinely corrupt bytes no way
240
+ * back except deleting engine files by hand. This is that way back, and it is explicit in every
241
+ * dimension: only a host call reaches it (nothing in the engine self-heals), a HEALTHY ledger is
242
+ * refused (`memory.control_plane_not_corrupt`), the corrupt bytes are quarantined beside the file
243
+ * rather than deleted, and the rebuild ANNOUNCES itself — resetting the challenge ledger un-withholds
244
+ * whatever it was withholding, which the model's next session must not learn about silently.
245
+ *
246
+ * `reason` is required and recorded in the announcement's shape (not its text — the announcement is a
247
+ * model-visible face and stays opaque); it is the operator's own note on the host error face.
248
+ */
249
+ rebuildCorruptControlPlaneLedger(ledger: StrictControlPlaneLedger, reason: string): ControlPlaneRebuildReceipt;
250
+ /**
251
+ * Host API (advisory, control-plane trust level — the same plane that can mark pollution):
252
+ * challenge entries by id. `requestId` is REQUIRED (the generation ledger's idempotency identity;
253
+ * this engine never mints one for the caller — a retried request must carry the SAME id so a lost
254
+ * ack cannot open a second generation). Each entry's eventId derives as `<requestId>:<entryId>`,
255
+ * so one batched request stays per-entry attributable.
256
+ */
257
+ challengeEntries(ids: readonly string[], reason: string, requestId: string): ChallengeAssignment[];
258
+ /** Host API: challenge every entry the lineage ledger attributes to `sessionId` (post-hoc source
259
+ * falsification — trustedTools misconfigured, a tool re-classified, late delegation evidence).
260
+ * Same requestId contract as {@link challengeEntries}. */
261
+ challengeSession(sessionId: string, reason: string, requestId: string): ChallengeAssignment[];
262
+ /** Host API: the TRUSTED resolve — closes exactly the named generation (a later challenge stays
263
+ * open; a resolve never blanket-clears, and there is no model-face path to this call). Returns
264
+ * false ⇔ no such generation was ever opened. */
265
+ resolveChallenge(entryId: string, generation: number, reason: string, requestId?: string): boolean;
266
+ /** Host API: adjudicate one UNDECIDABLE pending lineage transaction (the applyPatches-success →
267
+ * credential-write crash window; design/180 r5 — never auto-discarded). The host, holding its own
268
+ * deployment facts, states whether the commit landed. */
269
+ adjudicatePendingLineage(txnId: string, action: "promote" | "discard"): LineagePromotion[];
270
+ /** Host observability: the staged lineage transactions still awaiting settlement (their entry ids
271
+ * are latched — see {@link readChallengeExclusions}). Throws on a corrupt ledger. */
272
+ listPendingLineage(): Record<string, LineagePendingTxn>;
273
+ /** Host audit reads (throw on corruption — audit must not read a laundered ledger as clean). */
274
+ readChallengeLedger(): ChallengeEvent[];
275
+ /** Observability/tests — the collect-only challenged-history account (B-3; zero engine consumers). */
276
+ readChallengedHistoryAccount(): Record<string, ChallengedHistoryRow>;
277
+ /** Promotion settlement (shared by harvest step ⑤, reconciliation and host adjudication): a
278
+ * promoted contribution whose session ALREADY carries a durable pollution marker is challenged
279
+ * on the spot — "mark first, promote later" must converge exactly like the reverse order. The
280
+ * eventId anchors on the pollution record (sessionId + marked-at), so however many times this
281
+ * settlement replays the same pollution event, the generation is allocated once. */
282
+ private settlePromotions;
191
283
  materialize(scopes: readonly string[], writeScope: string | null): Promise<MemorySessionHandle>;
192
284
  inject(handle: MemorySessionHandle, opts?: {
193
285
  writeToolMounted?: boolean;
@@ -222,6 +314,7 @@ export declare class MemoryEngine {
222
314
  polluted?: {
223
315
  reason: string;
224
316
  };
317
+ sessionId?: string;
225
318
  }): Promise<HarvestReport>;
226
319
  private harvestCore;
227
320
  /**