@sema-agent/core 5.58.0 → 5.60.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 (59) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/dist/brain/anthropic.js +15 -5
  3. package/dist/brain/errors.d.ts +18 -1
  4. package/dist/brain/errors.js +7 -1
  5. package/dist/brain/input-too-long.d.ts +57 -0
  6. package/dist/brain/input-too-long.js +35 -0
  7. package/dist/brain/route-adjudicator.d.ts +8 -1
  8. package/dist/brain/route-adjudicator.js +8 -1
  9. package/dist/brain/stream-engine.js +9 -1
  10. package/dist/core/auto-compaction.js +2 -2
  11. package/dist/core/checkpoint-store.d.ts +116 -19
  12. package/dist/core/checkpoint-store.js +15 -8
  13. package/dist/core/context-edit.d.ts +243 -41
  14. package/dist/core/context-edit.js +247 -32
  15. package/dist/core/governance-codes.d.ts +37 -10
  16. package/dist/core/governance-codes.js +57 -1
  17. package/dist/core/locked-config.d.ts +36 -4
  18. package/dist/core/locked-config.js +34 -1
  19. package/dist/core/mcp.js +10 -6
  20. package/dist/core/memory-engine/consolidation-driver.d.ts +6 -2
  21. package/dist/core/memory-engine/consolidation-driver.js +54 -5
  22. package/dist/core/memory-engine/consolidation.d.ts +73 -1
  23. package/dist/core/memory-engine/consolidation.js +21 -1
  24. package/dist/core/memory-engine/content-origin.d.ts +24 -2
  25. package/dist/core/memory-engine/content-origin.js +6 -1
  26. package/dist/core/memory-engine/engine.d.ts +97 -8
  27. package/dist/core/memory-engine/engine.js +112 -20
  28. package/dist/core/memory-engine/file-backend.d.ts +13 -1
  29. package/dist/core/memory-engine/file-backend.js +3 -0
  30. package/dist/core/memory-engine/index.d.ts +4 -3
  31. package/dist/core/memory-engine/layout.js +20 -6
  32. package/dist/core/memory-engine/types.d.ts +17 -0
  33. package/dist/core/memory.d.ts +10 -0
  34. package/dist/core/park-selfcheck.js +1 -0
  35. package/dist/core/permission-rule-consent.js +9 -5
  36. package/dist/core/permission-rule-model.d.ts +42 -1
  37. package/dist/core/permission-rule-model.js +12 -0
  38. package/dist/core/runner/prepare-config-doors.d.ts +22 -1
  39. package/dist/core/runner/prepare-config-doors.js +36 -0
  40. package/dist/core/runner/prepare-task.d.ts +28 -1
  41. package/dist/core/runner/prepare-task.js +109 -11
  42. package/dist/core/runner/runtask.js +45 -8
  43. package/dist/core/store-contracts/checkpoint-store-contract.js +32 -0
  44. package/dist/core/tool-policy.d.ts +74 -0
  45. package/dist/core/tool-policy.js +80 -1
  46. package/dist/core/tools.js +1 -1
  47. package/dist/core/trace.d.ts +36 -0
  48. package/dist/core/types.d.ts +172 -22
  49. package/dist/core/types.js +4 -3
  50. package/dist/core/untrusted-text.d.ts +11 -0
  51. package/dist/core/untrusted-text.js +1 -0
  52. package/dist/engine/llm/types.d.ts +21 -2
  53. package/dist/engine/loop/agent-loop.js +7 -1
  54. package/dist/engine/loop/types.d.ts +4 -1
  55. package/dist/index.d.ts +3 -3
  56. package/dist/index.js +2 -2
  57. package/dist/tools/fs/fs-bash.js +1 -2
  58. package/package.json +1 -1
  59. package/test/export-surface.snapshot.json +1771 -1
@@ -9,7 +9,7 @@ import { formatMemoryAge } from "../memory-recall.js";
9
9
  import { committedDistilledOf, committedOriginOf, computeEntryRev, distilledEquals, hasDistilledFormExtra, hasOriginFormExtra, isValidEntryId, originEquals, parseEntryFile, serializeEntryFile, stripModelWrittenDistilled, stripModelWrittenOrigin } from "./frontmatter.js";
10
10
  import { MEMORY_PROVENANCE_RECALL_SENTENCE, memoryExposureIndexRow, parseMemoryExposureIndexRow } from "./provenance-wording.js";
11
11
  import { isInstructionEntry } from "./header-hints.js";
12
- import { ConsolidationRefusedError, CONSOLIDATION_FORCE_THROTTLE_FLOOR_MS, CONSOLIDATION_SEAT_FREEZE_GRACE_MS, consolidationTypeEligible, deriveProductSlug, deriveSupersededSet, isValidConsolidationPlanId, listConsolidationPlanIds, productAddPatch, quarantineCorruptPlan, readConsolidationGateFile, readConsolidationPlan, readIntentCredentials, recordActiveSetBaseline, recordConsolidationSession, recordIntentCredentials, screenConsolidationOptions, supersessionFuseCeiling, updateConsolidationGate, writeConsolidationPlan, } from "./consolidation.js";
12
+ import { ConsolidationRefusedError, CONSOLIDATION_FORCE_THROTTLE_FLOOR_MS, CONSOLIDATION_SEAT_FREEZE_GRACE_MS, consolidationTypeEligible, deriveProductSlug, deriveSupersededSet, isValidConsolidationPlanId, listConsolidationPlanIds, productAddPatch, quarantineCorruptPlan, readConsolidationGateFile, readConsolidationPlan, readIntentCredentials, recordActiveSetBaseline, recordConsolidationSession, recordIntentCredentials, scanRefusalCeiling, screenConsolidationOptions, supersessionFuseCeiling, updateConsolidationGate, writeConsolidationPlan, } from "./consolidation.js";
13
13
  import { classifySessionSettlements, clearHoldResolution, closeSessionAccount, disposeHold, effectiveSettlements, expireOverdueSettlements, foreignDanglingSessionAccounts, markHoldReleased, openInstructionHold, openSessionAccount, readHoldCustody, readHolds, reconcileHolds, replayExternalSettlementEffects, resolveHoldRecord, resolveSessionAccountRecord, resolveSettlementRecord, sessionSettlements, sessionUnattributedSet, } from "./delegation-settlement.js";
14
14
  import { noReplaceRestore } from "./delegation-settlement.js";
15
15
  import { markOriginClearanceTombstoned, openOriginClearance, readOriginClearances, settleOriginClearance } from "./origin-clearance.js";
@@ -2656,6 +2656,9 @@ export class MemoryEngine {
2656
2656
  else if (p.inputs.length > screened.maxInputsPerProduct)
2657
2657
  reasons.push(`product #${i}: ${p.inputs.length} inputs > maxInputsPerProduct ${screened.maxInputsPerProduct}`);
2658
2658
  }
2659
+ if (proposal.products.length === 0 && intents.length === 0) {
2660
+ reasons.push("empty plan: zero products and zero directed intents — a plan with nothing to apply must not settle as a completed run (the vacuous completion would stamp the eligible set into the fingerprint and blind the incremental face)");
2661
+ }
2659
2662
  if (reasons.length > 0)
2660
2663
  throw new ConsolidationRefusedError("memory.consolidation_refused", "memory consolidation plan refused (structural)", reasons);
2661
2664
  const planId = uuidv7();
@@ -2737,21 +2740,7 @@ export class MemoryEngine {
2737
2740
  }
2738
2741
  }
2739
2742
  const activeSetSize = headers.filter((h) => !superseded.has(h.id) && !exclusions.has(h.id)).length;
2740
- const edgeTargets = new Set();
2741
- for (const p of proposal.products)
2742
- for (const i2 of p.inputs)
2743
- if (i2.supersede === true)
2744
- edgeTargets.add(i2.id);
2745
2743
  const ceiling = supersessionFuseCeiling(activeSetSize, screened);
2746
- if (edgeTargets.size > ceiling) {
2747
- wholeReasons.push(`supersession fuse: ${edgeTargets.size} distinct targets > ceiling ${ceiling} (active set ${activeSetSize}, ratio ${screened.supersedeRatioCap}, floor ${screened.supersedeAbsoluteFloor}, N-1 unconditional)`);
2748
- try {
2749
- enqueueMemoryAnnouncement(this.controlDir, { kind: "gate", at, items: [`memory consolidation plan for scope ${inlineUntrusted(scope, 80)} REFUSED by the supersession fuse (${edgeTargets.size} targets over the ${ceiling} ceiling)`] });
2750
- }
2751
- catch (err) {
2752
- this.discloseAnnounceFailure("consolidation fuse enqueue", err);
2753
- }
2754
- }
2755
2744
  let visibleMarked = snapshot.marked;
2756
2745
  if (!visibleMarked) {
2757
2746
  const inputIds = [...new Set(proposal.products.flatMap((p) => p.inputs.map((i2) => i2.id)))];
@@ -2761,12 +2750,18 @@ export class MemoryEngine {
2761
2750
  const foldedOrigin = visibleMarked ? { taint: "external", cause: "derived", at } : undefined;
2762
2751
  for (let i = 0; i < proposal.products.length; i++) {
2763
2752
  const p = proposal.products[i];
2764
- if (visibleMarked && isInstructionEntry({ ...(p.type !== undefined ? { type: p.type } : {}) })) {
2765
- wholeReasons.push(`product #${i}: instruction-form product (type ${JSON.stringify(p.type)}) refused — the run's visible set contains marked content (the laundering hard gate, G7/G26)`);
2753
+ if (!isInstructionEntry({ ...(p.type !== undefined ? { type: p.type } : {}) }))
2754
+ continue;
2755
+ if (visibleMarked) {
2756
+ wholeReasons.push(`product #${i}: instruction-form product (type ${JSON.stringify(p.type)}) refused — the run's visible set contains marked content (the laundering hard gate, G7/G26; no option opens this arm)`);
2757
+ }
2758
+ else if (!screened.allowInstructionProducts) {
2759
+ wholeReasons.push(`product #${i}: instruction-form product (type ${JSON.stringify(p.type)}) refused — instruction-form consolidation products are refused unconditionally by default (a consolidation must not mint privileged entries out of ordinary notes); the host escape hatch is consolidation.allowInstructionProducts: true`);
2766
2760
  }
2767
2761
  }
2768
2762
  const assembled = [];
2769
2763
  const refusedProducts = [];
2764
+ const scanRefused = [];
2770
2765
  for (let i = 0; i < proposal.products.length; i++) {
2771
2766
  const p = proposal.products[i];
2772
2767
  const drift = [];
@@ -2813,11 +2808,31 @@ export class MemoryEngine {
2813
2808
  const nameFinding = scanMemoryFileName(`${slug}.md`);
2814
2809
  const findings = [...(nameFinding !== undefined ? [nameFinding] : []), ...scanMemoryWrite(serializeEntryFile(entry), { maxBytes: this.perFileBytes })];
2815
2810
  if (findings.length > 0) {
2816
- wholeReasons.push(`product #${i}: scan gate (${findings.map((f) => `${f.code}: ${f.reason}`).join("; ")})`);
2811
+ const reason = `scan gate (${findings.map((f) => `${f.code}: ${f.reason}`).join("; ")})`;
2812
+ refusedProducts.push({ index: i, reason });
2813
+ scanRefused.push({ index: i, reason });
2817
2814
  continue;
2818
2815
  }
2819
2816
  assembled.push(entry);
2820
2817
  }
2818
+ const edgeTargets = new Set();
2819
+ for (const e of assembled)
2820
+ for (const i2 of e.frontmatter.distilled?.inputs ?? [])
2821
+ if (i2.superseded === true)
2822
+ edgeTargets.add(i2.id);
2823
+ if (edgeTargets.size > ceiling) {
2824
+ wholeReasons.push(`supersession fuse: ${edgeTargets.size} distinct targets > ceiling ${ceiling} (active set ${activeSetSize}, ratio ${screened.supersedeRatioCap}, floor ${screened.supersedeAbsoluteFloor}, N-1 unconditional)`);
2825
+ try {
2826
+ enqueueMemoryAnnouncement(this.controlDir, { kind: "gate", at, items: [`memory consolidation plan for scope ${inlineUntrusted(scope, 80)} REFUSED by the supersession fuse (${edgeTargets.size} targets over the ${ceiling} ceiling)`] });
2827
+ }
2828
+ catch (err) {
2829
+ this.discloseAnnounceFailure("consolidation fuse enqueue", err);
2830
+ }
2831
+ }
2832
+ const scanCeiling = scanRefusalCeiling(proposal.products.length, screened);
2833
+ if (scanRefused.length > scanCeiling) {
2834
+ wholeReasons.push(`scan-refusal fuse: ${scanRefused.length} of ${proposal.products.length} product(s) failed the write scan > ceiling ${scanCeiling} (ratio ${screened.scanRefusalRatioCap}, per-plan max ${screened.scanRefusalMaxPerPlan}) — ${scanRefused.map((r) => `#${r.index}: ${r.reason}`).join(" | ")}`);
2835
+ }
2821
2836
  const directed = [];
2822
2837
  const intents = proposal.intents ?? [];
2823
2838
  const targetIds = intents.map((it) => it.entryId);
@@ -2875,6 +2890,9 @@ export class MemoryEngine {
2875
2890
  directed.push({ intentRequestId: intent.requestId, op: "update", id: intent.entryId, baseRev: committed.rev, plannedPostRev: terminal.rev, entry: terminal, state: "pending" });
2876
2891
  }
2877
2892
  }
2893
+ if (wholeReasons.length === 0 && assembled.length === 0 && directed.length === 0) {
2894
+ wholeReasons.push(`empty plan after freeze: every product was refused (${refusedProducts.map((r) => `#${r.index}: ${r.reason}`).join(" | ")}) — a plan with nothing left to apply must not settle as a completed run (the vacuous completion would stamp the eligible set into the fingerprint and blind the incremental face)`);
2895
+ }
2878
2896
  if (wholeReasons.length > 0)
2879
2897
  throw new ConsolidationRefusedError("memory.consolidation_refused", "memory consolidation plan refused", wholeReasons);
2880
2898
  try {
@@ -2888,8 +2906,9 @@ export class MemoryEngine {
2888
2906
  throw err;
2889
2907
  throw new ConsolidationRefusedError("memory.consolidation_gate_corrupt", `memory consolidation refused: the gate store could not be re-read at the freeze boundary (fail-closed): ${err instanceof Error ? err.message : String(err)}`);
2890
2908
  }
2909
+ const freezeRefusedInputIds = [...new Set(refusedProducts.flatMap((r) => (proposal.products[r.index]?.inputs ?? []).map((i2) => i2.id)))].sort();
2891
2910
  const plan = {
2892
- v: 1,
2911
+ v: freezeRefusedInputIds.length > 0 ? 2 : 1,
2893
2912
  planId,
2894
2913
  scope,
2895
2914
  requestId: input.requestId,
@@ -2900,6 +2919,7 @@ export class MemoryEngine {
2900
2919
  ...(foldedOrigin !== undefined ? { foldedOrigin } : {}),
2901
2920
  products: assembled,
2902
2921
  productStates: Object.fromEntries(assembled.map((e) => [e.id, "pending"])),
2922
+ ...(freezeRefusedInputIds.length > 0 ? { freezeRefusedInputIds } : {}),
2903
2923
  directed,
2904
2924
  intents: intents.map((it) => ({ requestId: it.requestId, state: "pending" })),
2905
2925
  state: "open",
@@ -3099,6 +3119,7 @@ export class MemoryEngine {
3099
3119
  }
3100
3120
  writeConsolidationPlan(this.controlDir, plan);
3101
3121
  const currentlyEligible = await this.settleFingerprintFilter(plan.scope);
3122
+ const freezeRefused = new Set(plan.freezeRefusedInputIds ?? []);
3102
3123
  updateConsolidationGate(this.controlDir, (file) => {
3103
3124
  const row = file.scopes[plan.scope];
3104
3125
  if (row?.openPlanId !== plan.planId)
@@ -3111,6 +3132,8 @@ export class MemoryEngine {
3111
3132
  if (currentlyEligible !== undefined) {
3112
3133
  const fp = {};
3113
3134
  for (const [id, rev] of Object.entries(row.snapshot.eligible)) {
3135
+ if (freezeRefused.has(id))
3136
+ continue;
3114
3137
  if (!currentlyEligible(id, rev))
3115
3138
  continue;
3116
3139
  fp[id] = { rev };
@@ -3234,6 +3257,7 @@ export class MemoryEngine {
3234
3257
  }
3235
3258
  async settleGateForTerminalPlan(plan) {
3236
3259
  const currentlyEligible = await this.settleFingerprintFilter(plan.scope);
3260
+ const freezeRefused = new Set(plan.freezeRefusedInputIds ?? []);
3237
3261
  updateConsolidationGate(this.controlDir, (file) => {
3238
3262
  const row = file.scopes[plan.scope];
3239
3263
  if (row?.openPlanId !== plan.planId)
@@ -3247,6 +3271,8 @@ export class MemoryEngine {
3247
3271
  if (currentlyEligible !== undefined) {
3248
3272
  const fp = {};
3249
3273
  for (const [id, rev] of Object.entries(row.snapshot.eligible)) {
3274
+ if (freezeRefused.has(id))
3275
+ continue;
3250
3276
  if (!currentlyEligible(id, rev))
3251
3277
  continue;
3252
3278
  fp[id] = { rev };
@@ -3348,6 +3374,24 @@ export class MemoryEngine {
3348
3374
  throw new ConsolidationRefusedError("memory.consolidation_gate_corrupt", `memory consolidation retry refused: the intent-credential account cannot be trusted (fail-closed): ${err instanceof Error ? err.message : String(err)}`);
3349
3375
  }
3350
3376
  const pendingIntents = plan.intents.filter((i) => i.state === "pending" && standingCredentials[i.requestId] === undefined).map((i) => i.requestId);
3377
+ if (pendingIntents.length === 0) {
3378
+ throw new ConsolidationRefusedError("memory.consolidation_refused", `memory consolidation retry refused: plan ${planId} has nothing to re-judge — ` +
3379
+ (plan.intents.length === 0
3380
+ ? "it carries no intents (a conflicted product never rides a retry)"
3381
+ : "every unfulfilled intent already carries a credential (consumed by an earlier retry or a racing plan)") +
3382
+ ` — a plan with nothing to apply must not settle as a completed run (the vacuous completion would stamp the eligible set into the fingerprint and blind the incremental face); resolve with "discard" to release the seat.`);
3383
+ }
3384
+ const conflictProductInputIds = [];
3385
+ for (const p of plan.products) {
3386
+ if (plan.productStates[p.id] !== "conflict")
3387
+ continue;
3388
+ const inputs = p.frontmatter?.distilled?.inputs;
3389
+ if (!Array.isArray(inputs) || inputs.some((i) => typeof i?.["id"] !== "string")) {
3390
+ throw new ConsolidationRefusedError("memory.consolidation_refused", `memory consolidation retry refused: conflicted product ${p.id} carries no readable distilled attribution rows (every frozen product is minted with them — this is control-plane damage); the plan stays parked, resolve with "discard" (the bytes remain on the plan file for host review).`);
3391
+ }
3392
+ for (const i of inputs)
3393
+ conflictProductInputIds.push(i.id);
3394
+ }
3351
3395
  const face = this.committedAuditFace();
3352
3396
  const originalIntents = await this.reconstructIntentsForRetry(plan, pendingIntents, face);
3353
3397
  const retryPlanId = uuidv7();
@@ -3361,8 +3405,9 @@ export class MemoryEngine {
3361
3405
  file.scopes[plan.scope] = row;
3362
3406
  return { next: file, result: undefined };
3363
3407
  });
3408
+ const inheritedRefusals = [...new Set([...(plan.freezeRefusedInputIds ?? []), ...conflictProductInputIds])].sort();
3364
3409
  const retryPlan = {
3365
- v: 1,
3410
+ v: inheritedRefusals.length > 0 ? 2 : 1,
3366
3411
  planId: retryPlanId,
3367
3412
  scope: plan.scope,
3368
3413
  requestId: opts.requestId,
@@ -3371,6 +3416,7 @@ export class MemoryEngine {
3371
3416
  visibleMarked: false,
3372
3417
  products: [],
3373
3418
  productStates: {},
3419
+ ...(inheritedRefusals.length > 0 ? { freezeRefusedInputIds: inheritedRefusals } : {}),
3374
3420
  directed: originalIntents.directed,
3375
3421
  intents: pendingIntents.map((r) => ({ requestId: r, state: "pending" })),
3376
3422
  state: "open",
@@ -3450,6 +3496,35 @@ export class MemoryEngine {
3450
3496
  }
3451
3497
  return out;
3452
3498
  }
3499
+ readConsolidationPlanFoldEvidence(planId) {
3500
+ const read = readConsolidationPlan(this.controlDir, planId);
3501
+ if (read.state === "absent")
3502
+ return { state: "absent" };
3503
+ if (read.state === "corrupt")
3504
+ return { state: "corrupt", detail: read.detail };
3505
+ const p = read.plan;
3506
+ const targets = new Set();
3507
+ for (const prod of p.products) {
3508
+ if (p.productStates[prod.id] !== "applied")
3509
+ continue;
3510
+ const inputs = prod.frontmatter?.distilled?.inputs;
3511
+ const anchors = { scope: p.scope, requestId: p.requestId };
3512
+ if (inputs === undefined)
3513
+ return { state: "corrupt", detail: `applied product ${prod.id} carries no distilled.inputs (every frozen product is minted with its attribution rows)`, ...anchors };
3514
+ if (!Array.isArray(inputs))
3515
+ return { state: "corrupt", detail: `applied product ${prod.id} carries a non-array distilled.inputs`, ...anchors };
3516
+ for (const i of inputs) {
3517
+ if (typeof i !== "object" || i === null)
3518
+ return { state: "corrupt", detail: `applied product ${prod.id} carries a malformed distilled input row`, ...anchors };
3519
+ if (i.superseded !== true)
3520
+ continue;
3521
+ if (typeof i.id !== "string")
3522
+ return { state: "corrupt", detail: `applied product ${prod.id} carries a supersession edge without a string target id`, ...anchors };
3523
+ targets.add(i.id);
3524
+ }
3525
+ }
3526
+ return { state: "ok", scope: p.scope, requestId: p.requestId, planState: p.state, supersededTargets: [...targets].sort() };
3527
+ }
3453
3528
  readConsolidationGate(scope) {
3454
3529
  try {
3455
3530
  return readConsolidationGateFile(this.controlDir).scopes[scope];
@@ -3458,6 +3533,16 @@ export class MemoryEngine {
3458
3533
  return undefined;
3459
3534
  }
3460
3535
  }
3536
+ readConsolidationGateState(scope) {
3537
+ let row;
3538
+ try {
3539
+ row = readConsolidationGateFile(this.controlDir).scopes[scope];
3540
+ }
3541
+ catch (err) {
3542
+ return { state: "corrupt", detail: err instanceof Error ? err.message : String(err) };
3543
+ }
3544
+ return row !== undefined ? { state: "present", row } : { state: "absent" };
3545
+ }
3461
3546
  async readConsolidationFoldState(scope) {
3462
3547
  const face = this.committedAuditFace();
3463
3548
  const headers = await face.listHeaders([scope]);
@@ -3478,6 +3563,13 @@ export class MemoryEngine {
3478
3563
  get controlPlaneRoot() {
3479
3564
  return this.controlDir;
3480
3565
  }
3566
+ async listMemoryScopes() {
3567
+ const b = this.backend;
3568
+ if (typeof b.listScopes !== "function") {
3569
+ return { supported: false, reason: "the backend does not implement scope enumeration (listScopes) — an absent capability must never read as an empty scope list" };
3570
+ }
3571
+ return await b.listScopes();
3572
+ }
3481
3573
  committedAuditFace() {
3482
3574
  const b = this.backend;
3483
3575
  return b.restrictedAdoptionView?.({ audit: false }) ?? b.retrievalView?.() ?? this.backend;
@@ -1,6 +1,6 @@
1
1
  import { scopeDirName, type ChallengeEvent, type MemoryPartitionIncidentSink } from "./layout.js";
2
2
  import type { MemoryBundleImportPlan, MemoryImportReport } from "./export-bundle.js";
3
- import type { HarvestRejection, MemoryBackend, MemoryEntry, MemoryEntryHeader, NotePatch, PatchReport, ScoredMemoryEntry } from "./types.js";
3
+ import type { HarvestRejection, MemoryBackend, MemoryEntry, MemoryEntryHeader, MemoryScopeEnumeration, NotePatch, PatchReport, ScoredMemoryEntry } from "./types.js";
4
4
  /** Files/dirs the entry scan never treats as entries: the derived index, dotfiles (`.hydrate`,
5
5
  * transaction staging files), and anything not `.md`. */
6
6
  export declare const MEMORY_INDEX_FILENAME = "MEMORY.md";
@@ -756,6 +756,18 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
756
756
  * while a live holder exists ({@link txnInFlight}), which now covers adoption holds too.
757
757
  */
758
758
  private readScopeAdopting;
759
+ /**
760
+ * design/376 slice ③ (#437③) — the File leg's scope-enumeration capability face: every scope
761
+ * this store has hosted, derived from the store root's durable scope registry (the control
762
+ * plane's `scopes.json` — the one source that can invert a scope's sanitized directory label
763
+ * back to its name; a listing of raw subdirectories could not). Side-effect-free committed
764
+ * read; a corrupt/unreadable registry THROWS its fail-closed refusal — enumeration over an
765
+ * untrustworthy registry must never answer an empty (= clean-looking) list. Honest residual,
766
+ * disclosed: the registry records scopes as they are first touched THROUGH this control plane,
767
+ * so a hand-assembled tree's never-touched scope dirs appear here only once a caller first
768
+ * names them — store-level fact, session-roster-free either way.
769
+ */
770
+ listScopes(): MemoryScopeEnumeration;
759
771
  listHeaders(scopes: readonly string[]): Promise<MemoryEntryHeader[]>;
760
772
  private listHeadersFrom;
761
773
  getByIds(ids: readonly string[]): Promise<MemoryEntry[]>;
@@ -1780,6 +1780,9 @@ export class FileMemoryEngineBackend {
1780
1780
  lock.release();
1781
1781
  }
1782
1782
  }
1783
+ listScopes() {
1784
+ return { supported: true, scopes: Object.keys(registeredScopes(this.controlPlaneRoot)).sort() };
1785
+ }
1783
1786
  async listHeaders(scopes) {
1784
1787
  return this.listHeadersFrom(scopes, (s) => this.readScopeAdopting(s));
1785
1788
  }
@@ -1,4 +1,4 @@
1
- export { MemoryEngine, buildMemoryInstruction, truncateIndex, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, memoryRecallDisciplineSegment, entryFileHeadCarriesOrigin, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_READONLY_NOTICE, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, renderAnnouncements, type MemoryEngineOptions, type MemoryInjection, type EntryProvenanceAccount, memoryConsolidationRecommendedNotice, memoryConsolidationCommittedNotice, memoryConsolidationConflictNotice, memoryConsolidationIncompleteNotice, memoryConsolidationRefusedNotice, type ConsolidationCommitReceipt, type ConsolidationReconcileReport, type ConsolidationResolveReceipt, type ConsolidationPlanSummary, } from "./engine.js";
1
+ export { MemoryEngine, buildMemoryInstruction, truncateIndex, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, memoryRecallDisciplineSegment, entryFileHeadCarriesOrigin, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_READONLY_NOTICE, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, renderAnnouncements, type MemoryEngineOptions, type MemoryInjection, type EntryProvenanceAccount, memoryConsolidationRecommendedNotice, memoryConsolidationCommittedNotice, memoryConsolidationConflictNotice, memoryConsolidationIncompleteNotice, memoryConsolidationRefusedNotice, type ConsolidationCommitReceipt, type ConsolidationReconcileReport, type ConsolidationResolveReceipt, type ConsolidationPlanSummary, type ConsolidationPlanFoldEvidence, } from "./engine.js";
2
2
  export { MEMORY_SEARCH_TOOL_NAME, MEMORY_GET_TOOL_NAME, MEMORY_ENGINE_TOOL_NAMES, type MemorySearchDetails, type MemorySearchHit, type CleanMemorySearchHit, type ExposedMemorySearchHit, type MemoryGetDetails, } from "./tools.js";
3
3
  export { MEMORY_EXPOSURE_BANNER, MEMORY_EXPOSURE_HANDLE_TAG, MEMORY_PROVENANCE_RECALL_SENTENCE, MEMORY_PROVENANCE_SEARCH_SENTENCE, memoryExposureIndexRow, parseMemoryExposureIndexRow, } from "./provenance-wording.js";
4
4
  export { scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE } from "./scan.js";
@@ -8,11 +8,12 @@ export { ControlPlaneCorruptError, deriveControlPlaneDir, deriveRepoControlPlane
8
8
  export { readV2HeaderHints, isInstructionEntry, type V2HeaderHints } from "./header-hints.js";
9
9
  export { parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, committedOriginOf, originEquals, ambiguousOriginRepresentation, type ParsedEntryFile } from "./frontmatter.js";
10
10
  export { committedDistilledOf, distilledEquals } from "./frontmatter.js";
11
- export { CONSOLIDATION_DEFAULTS, ConsolidationRefusedError, MEMORY_SEARCH_SUPERSEDED_TAG, consolidationTypeEligible, deriveSupersededSet, memorySupersededNote, readIntentCredentials, supersessionFuseCeiling, type ConsolidationGateRow, type ConsolidationIntent, type ConsolidationIntentCredentialRow, type ConsolidationLeaseSeat, type ConsolidationProductProposal, type ConsolidationProposal, type MemoryConsolidationOptions, CONSOLIDATION_RUN_STOP_REASONS, type ConsolidationRunStopReason, } from "./consolidation.js";
11
+ export { CONSOLIDATION_DEFAULTS, ConsolidationRefusedError, MEMORY_SEARCH_SUPERSEDED_TAG, consolidationTypeEligible, deriveSupersededSet, memorySupersededNote, readIntentCredentials, supersessionFuseCeiling, type ConsolidationGateRead, type ConsolidationGateRow, type ConsolidationIntent, type ConsolidationIntentCredentialRow, type ConsolidationLeaseSeat, type ConsolidationProductProposal, type ConsolidationProposal, type MemoryConsolidationOptions, CONSOLIDATION_RUN_STOP_REASONS, type ConsolidationRunStopReason, } from "./consolidation.js";
12
12
  export { DISTILLER_DEFAULT_MAX_INPUTS_PER_PRODUCT, LLM_DISTILLER_CONTRACT, LLM_DISTILLER_CONTRACT_DL2, LLM_DISTILLER_CONTRACT_DL3, LLM_DISTILLER_CONTRACTS, MEMORY_DISTILLER_CONTRACT_V1, contractGroupingDiff, driveConsolidationToFixpoint, isAliasModelId, llmPlanDistiller, mintLlmConsolidationPlan, openAiCompatChatSeat, parseJsonAnswer, planParseRepairs, sanitizeLlmGroups, scheduleUnderFuse, type ConsolidationDistillFn, type ConsolidationDriveCycleRow, type ConsolidationDriveEngine, type ConsolidationDriveResult, type ConsolidationFoldState, type DistillerCandidate, type DistillerChatAnswer, type DistillerChatFn, type DistillerChatRequest, type FuseSchedule, type LlmConsolidationPlan, type LlmConsolidationPlanProduct, type LlmDistillerContract, type MintLlmConsolidationPlanResult, type PlanParseRepairs, type SanitizedLlmGroups, } from "./distiller.js";
13
13
  export { CONSOLIDATION_DRIVER_PLANS_DIR, CONSOLIDATION_DRIVER_RUNS_FILE, archiveDistillerPlan, readConsolidationDriverRun, runMemoryConsolidationDriver, type ConsolidationDriverEngine, type ConsolidationDriverRunRow, type ConsolidationRunReceipt, type RunMemoryConsolidationOptions, } from "./consolidation-driver.js";
14
+ export type { OriginClearanceRow, OriginClearanceEvent } from "./origin-clearance.js";
14
15
  export { MEMORY_ORIGIN_CAUSES } from "./types.js";
15
- export type { MemoryBackend, MemoryEntry, MemoryEntryFrontmatter, MemoryEntryOrigin, MemoryOriginCause, MemoryEntryDistilled, MemoryEntryDistilledInput, MemoryEntryHeader, ScoredMemoryEntry, NotePatch, PatchReport, MaterializedFile, MemorySessionHandle, HarvestReport, HarvestRejection, HarvestRejectionCode, MemoryAnnouncement, ScanFinding, } from "./types.js";
16
+ export type { MemoryBackend, MemoryEntry, MemoryEntryFrontmatter, MemoryEntryOrigin, MemoryOriginCause, MemoryScopeEnumeration, MemoryEntryDistilled, MemoryEntryDistilledInput, MemoryEntryHeader, ScoredMemoryEntry, NotePatch, PatchReport, MaterializedFile, MemorySessionHandle, HarvestReport, HarvestRejection, HarvestRejectionCode, MemoryAnnouncement, ScanFinding, } from "./types.js";
16
17
  export { memoryBackendContract, assertMemoryBackendSearchEquivalence, type MemoryBackendContractHooks, } from "./memory-backend-contract.js";
17
18
  export { SCOPE_SEGMENT_MAX_ENCODED, PROJECT_MARKER_PATH, encodeScopeSegment, decodeScopeSegment, parseScopeKey, formatUserScope, formatOrgScope, formatProjScope, formatUserProjScope, isPersonalScope, assertScopeContractPlacement, formatProjectMarker, parseProjectMarker, resolveProjectId, PROJECT_ID_REGEX, type ParsedScopeKey, } from "./scope-contract.js";
18
19
  export { migrateScope, type MigrateScopeReport } from "./migrate.js";
@@ -303,21 +303,35 @@ export function dirCaseFolds(dir) {
303
303
  }
304
304
  function readScopesRecord(controlDir) {
305
305
  const path = join(controlDir, SCOPES_FILE);
306
+ const journalPath = `${path}.journal`;
306
307
  let raw;
308
+ let sourcePath = journalPath;
307
309
  try {
308
- raw = readFileSync(path, "utf8");
310
+ raw = readFileSync(journalPath, "utf8");
309
311
  }
310
312
  catch (err) {
311
- if (err.code === "ENOENT")
312
- return {};
313
- throw new ControlPlaneCorruptError(`scope registry unreadable (${err.code ?? "io error"}): ${path}`, { cause: err });
313
+ if (err.code !== "ENOENT") {
314
+ throw new ControlPlaneCorruptError(`scope registry journal unreadable (${err.code ?? "io error"}) — a committed next state may exist that cannot be proven: ${journalPath}`, { cause: err });
315
+ }
316
+ raw = undefined;
317
+ }
318
+ if (raw === undefined) {
319
+ sourcePath = path;
320
+ try {
321
+ raw = readFileSync(path, "utf8");
322
+ }
323
+ catch (err) {
324
+ if (err.code === "ENOENT")
325
+ return {};
326
+ throw new ControlPlaneCorruptError(`scope registry unreadable (${err.code ?? "io error"}): ${path}`, { cause: err });
327
+ }
314
328
  }
315
329
  let parsed;
316
330
  try {
317
331
  parsed = JSON.parse(raw);
318
332
  }
319
333
  catch {
320
- throw new ControlPlaneCorruptError(`scope registry is unparseable: ${path}`);
334
+ throw new ControlPlaneCorruptError(`scope registry is unparseable: ${sourcePath}`);
321
335
  }
322
336
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
323
337
  throw new ControlPlaneCorruptError(`scope registry has the wrong shape: ${path}`);
@@ -351,7 +365,7 @@ function lockedScopesUpdate(controlDir, fn) {
351
365
  const lock = `${file}.lock`;
352
366
  const token = acquireSidecarLock(lock, { onDeadline: "throw" });
353
367
  try {
354
- rollForwardSidecar(file, journal);
368
+ rollForwardStrictSidecar(file, journal);
355
369
  const rec = readScopesRecord(controlDir);
356
370
  const { next, result } = fn(rec);
357
371
  if (next !== undefined) {
@@ -246,6 +246,23 @@ export interface NotePatch {
246
246
  * Ignored on `update`/`delete`. */
247
247
  guard?: "absent";
248
248
  }
249
+ /**
250
+ * design/376 slice ③ (#437③) — a backend's scope-enumeration answer, DISCRIMINATED on purpose:
251
+ * "this backend cannot enumerate" and "this store hosts no scopes" must never share a spelling
252
+ * (an unsupported backend read as `[]` would make every audit sweep over it silently claim a
253
+ * clean store). The capability itself is an OPTIONAL backend face (`listScopes`, probed like the
254
+ * File class's audit faces): a backend without it is answered `supported: false` by
255
+ * `MemoryEngine.listMemoryScopes`, never an empty array. The File leg derives from the store
256
+ * root's durable scope registry; the SQL twins' leg is a store-contracts extension (separate
257
+ * ticket) — until it lands they simply do not implement the face.
258
+ */
259
+ export type MemoryScopeEnumeration = {
260
+ supported: true;
261
+ scopes: string[];
262
+ } | {
263
+ supported: false;
264
+ reason: string;
265
+ };
249
266
  /** Result of {@link MemoryBackend.applyPatches} — applied ops + per-id CAS conflicts (reported, not thrown). */
250
267
  export interface PatchReport {
251
268
  applied: Array<{
@@ -477,6 +477,16 @@ export interface NormalizedMemorySpec {
477
477
  * of the fail-closed `"external"` default. The explicit-config channel for a deployment whose custom
478
478
  * tools predate the {@link import("./types.js").ToolSpec.contentOrigin} declaration; it never
479
479
  * overrides a tool's OWN declaration. Normalized: trimmed, deduped, empties dropped.
480
+ *
481
+ * NOT the channel for MCP servers (design/378). A per-name entry here keys on the MINTED name — the
482
+ * host has to predict the namespacing and charset normalization — covers nothing a mid-task refresh
483
+ * adds, and says "exempt this undeclared tool" where the fact is "this whole server is mine".
484
+ * {@link import("./types.js").McpServerSpec.contentOrigin} states that fact at the entry, and being
485
+ * a DECLARATION it wins over this allowlist in both directions: a declared server's tools leave this
486
+ * list's reach entirely, an explicit `"external"` declaration included (that is the one way a
487
+ * deployment can pin a server's tools BEYOND this exemption). This list is not retired — it remains
488
+ * the in-register channel for undeclared HOST tools and for a deployment not ready to touch its
489
+ * server entries.
480
490
  */
481
491
  trustedTools?: string[];
482
492
  /**
@@ -70,6 +70,7 @@ function syntheticCheckpoint(scope) {
70
70
  toolName: "SelfCheck",
71
71
  args: { probe: true, nested: { depth: 2, list: [1, 2, 3] } },
72
72
  boundInputHash: boundInputHashOf({ probe: true, nested: { depth: 2, list: [1, 2, 3] } }),
73
+ hasBidiControls: true,
73
74
  batchToolCallIds: [`${scope}:call`],
74
75
  completedCallIds: [],
75
76
  },
@@ -1,5 +1,5 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { hasUnrenderableCharacters, parseAllowRuleText, ruleAdmitsCommand, segmentCoverageOf, suggestRulesForCommand, } from "./permission-rule-model.js";
2
+ import { escapeForDisclosure, hasUnrenderableCharacters, parseAllowRuleText, ruleAdmitsCommand, segmentCoverageOf, suggestRulesForCommand, } from "./permission-rule-model.js";
3
3
  import { errText, sameRuleOwner, sameScope, writerOf } from "./permission-rule-store.js";
4
4
  export class InMemoryRuleApprovalRecordStore {
5
5
  rows = new Map();
@@ -115,7 +115,7 @@ function approvalRecordDamageOf(rec) {
115
115
  const seen = new Set();
116
116
  for (const member of members) {
117
117
  if (!Number.isInteger(member) || member < 0 || rec.candidates[member] === undefined) {
118
- return `offer ${at} references candidate ${member}, which the record does not carry`;
118
+ return `offer ${at} references candidate ${escapeForDisclosure(member)}, which the record does not carry`;
119
119
  }
120
120
  const cand = rec.candidates[member];
121
121
  if (typeof cand !== "object" || cand === null || typeof cand.rule !== "string") {
@@ -138,10 +138,14 @@ function approvalRecordDamageOf(rec) {
138
138
  }
139
139
  }
140
140
  else if (offer.segments !== undefined || offer.uncoveredSegments !== undefined) {
141
- return `${rec.kind} batch offer ${at} must not carry segment metadata — there is no compound command behind it`;
141
+ return `${escapeForDisclosure(rec.kind)} batch offer ${at} must not carry segment metadata — there is no compound command behind it`;
142
142
  }
143
143
  }
144
144
  }
145
+ for (const [at] of rec.candidates.entries()) {
146
+ if (!claimed.has(at))
147
+ return `candidate ${at} belongs to no offer — every candidate row is claimed by exactly one offer`;
148
+ }
145
149
  return undefined;
146
150
  }
147
151
  export function ruleOffersOfRecord(rec) {
@@ -325,7 +329,7 @@ function checkEditedRuleText(text, command) {
325
329
  if (!ruleAdmitsCommand(parsed.rule, command)) {
326
330
  return {
327
331
  ok: false,
328
- message: `the edited rule "${parsed.rule.rule}" does not admit the command that was decided ("${command}") — a card's edit may widen how much the rule covers, never move it to a different grant`,
332
+ message: `the edited rule "${parsed.rule.rule}" does not admit the command that was decided ("${escapeForDisclosure(command)}") — a card's edit may widen how much the rule covers, never move it to a different grant`,
329
333
  };
330
334
  }
331
335
  return { ok: true, canonicalRule: parsed.rule.rule };
@@ -342,7 +346,7 @@ export function precheckEditedRuleText(text, command) {
342
346
  throw e;
343
347
  }
344
348
  if (hasUnrenderableCharacters(command)) {
345
- const e = new Error("precheckEditedRuleText takes a command this lane can read — one carrying control or format characters is not a command any card was drawn for, and the refusal line naming it would carry those bytes onto a display surface");
349
+ const e = new Error("precheckEditedRuleText takes a command this lane can read — one carrying control or format characters is refused at this preflight face (likelier a caller wiring fault than a card's command; the in-engine confirmation arm still judges such a card, with the command escaped on its refusal line)");
346
350
  e.code = "config.invalid_argument";
347
351
  throw e;
348
352
  }
@@ -282,6 +282,40 @@ export declare const SUGGESTION_LEXICON: readonly string[];
282
282
  * through, so an ordinary rule text reads normally. The result is length-bounded.
283
283
  */
284
284
  export declare function escapeForDisclosure(value: unknown): string;
285
+ /**
286
+ * Render raw, untrusted COMMAND text for a display surface — the minimal safe baseline for the seats
287
+ * that carry post-rewrite command bytes verbatim ({@link SegmentRuleSuggestion.segment} is the
288
+ * motivating one; a card's own `command`/`args` echo has the same hazard).
289
+ *
290
+ * WHAT IT DOES, and why the two character families are not handled the same way:
291
+ * · `\p{Cf}` (the bidirectional controls U+061C/U+200E/U+200F/U+202A–U+202E/U+2066–U+2069 and the
292
+ * zero-width formatters ZWSP/ZWNJ/ZWJ/WORD JOINER/SOFT HYPHEN/BOM) is REMOVED. These occupy no
293
+ * width, so deleting them is what makes the rendered line agree with what a reader believes they
294
+ * are reading — a `rm -rf x` that DISPLAYS as something benign is exactly the deception the seat
295
+ * would otherwise hand on. This is the same family
296
+ * {@link import("./tool-policy.js").AskRequest.hasBidiControls} raises its warning bit for; the
297
+ * bit says "the raw bytes carry them", this function is what a surface renders instead.
298
+ * · Controls and line/paragraph separators (C0 incl. TAB and NEWLINE, DEL, C1, U+2028/U+2029) are
299
+ * FOLDED TO A SPACE with runs collapsed, not removed: they occupy width (or a whole line), and
300
+ * deleting them would glue two tokens into one word that reads as a different command. TAB is not
301
+ * excepted here — a one-line display value has no use for one, and a rule text's foldable-tab
302
+ * exception belongs to the VALIDATOR class, not to this one.
303
+ * The result is then capped by code point (never splitting a surrogate pair) with a `…` marker, and
304
+ * carries the shared authority-envelope neutralization every untrusted inline value gets — this text
305
+ * can land on a model-visible line as easily as on a human one.
306
+ *
307
+ * WHAT IT IS NOT. A DISPLAY baseline, never an identity channel and never adjudication input: the
308
+ * transform is LOSSY and NOT injective (two distinct commands can render identically — that is the
309
+ * price of a readable line). Correlate, match, store and adjudicate on the RAW bytes, which the
310
+ * carrying seats keep unchanged: this function neither changes what those seats contain nor what any
311
+ * of them means. A caller that must instead tell two hostile texts APART wants
312
+ * {@link escapeForDisclosure} (which escapes rather than folds, so nothing collides) or
313
+ * {@link hasUnrenderableCharacters} (refuse instead of render).
314
+ *
315
+ * @param text the raw untrusted text (a `String(x)` is applied to anything that is not one).
316
+ * @param maxLen code-point cap for the rendered result; defaults to the disclosure-line budget.
317
+ */
318
+ export declare function renderUntrustedCommandText(text: unknown, maxLen?: number): string;
285
319
  /**
286
320
  * Does this text carry a character of the class {@link escapeForDisclosure} exists for — the class
287
321
  * {@link CONTROL_CHARS_RE} names? Asked by a caller that must REFUSE such a value rather than render
@@ -488,7 +522,14 @@ export interface SegmentRuleSuggestion {
488
522
  readonly command: string;
489
523
  /** The (folded) segment this suggestion came from. Raw post-rewrite command bytes — a substring
490
524
  * of the same call's `args`/`command` values riding beside it on every surface that carries
491
- * this seat — so render it with contextual escaping, exactly like those. */
525
+ * this seat — so render it with contextual escaping, exactly like those.
526
+ *
527
+ * RAW is the contract here and does not change: this seat carries the bytes, because the bytes are
528
+ * what a rule would be minted from and what a correlation keys on. The rendering side of that
529
+ * contract is {@link renderUntrustedCommandText} — the shipped display baseline (bidi and
530
+ * zero-width controls removed, controls/line separators folded, length capped, envelope-neutralized)
531
+ * a surface can adopt instead of writing its own, since raw command bytes displayed unprocessed can
532
+ * show a reader something other than what they authorize. */
492
533
  readonly segment: string;
493
534
  }
494
535
  /**
@@ -1,5 +1,6 @@
1
1
  import { parsePermissionRule } from "./permission-rules.js";
2
2
  import { parseLeadingCommandName, splitShellCompoundSegments } from "../tools/fs/bash-readonly-classifier.js";
3
+ import { inlineUntrusted } from "./untrusted-text.js";
3
4
  export const MAX_RULE_TEXT_CHARS = 512;
4
5
  export const BARE_INTERPRETER_NAMES = new Set([
5
6
  "node", "deno", "bun", "python", "python2", "python3", "perl", "ruby", "php", "osascript",
@@ -162,6 +163,17 @@ export function escapeForDisclosure(value) {
162
163
  });
163
164
  return escaped.length <= DISCLOSED_RULE_TEXT_MAX_CHARS ? escaped : `${escaped.slice(0, DISCLOSED_RULE_TEXT_MAX_CHARS)}…`;
164
165
  }
166
+ const DISPLAY_STRIP_FORMAT_RE = /\p{Cf}/gu;
167
+ export function renderUntrustedCommandText(text, maxLen = DISCLOSED_RULE_TEXT_MAX_CHARS) {
168
+ let raw;
169
+ try {
170
+ raw = typeof text === "string" ? text : String(text);
171
+ }
172
+ catch {
173
+ return "<unprintable>";
174
+ }
175
+ return inlineUntrusted(raw.replace(DISPLAY_STRIP_FORMAT_RE, ""), maxLen);
176
+ }
165
177
  export function hasUnrenderableCharacters(text) {
166
178
  return CONTROL_CHARS_RE.test(text);
167
179
  }
@@ -22,6 +22,7 @@
22
22
  import { type BrainCallGuardrailRef } from "../../brain/timeout.js";
23
23
  import type { Model } from "../../internal/llm.js";
24
24
  import type { ThinkingLevel } from "../../internal/harness.js";
25
+ import type { ContextEditMachine } from "../context-edit.js";
25
26
  import { type LockedPreflight } from "../locked-config.js";
26
27
  import { type ResolvedRole } from "../roles.js";
27
28
  import type { SessionStore } from "../session.js";
@@ -119,6 +120,13 @@ export interface PrepareConfigDoorsResult {
119
120
  /** owned — the resolved interaction posture (spec > spawning run > deployment); the
120
121
  * AskUserQuestion mount and the child-ctx injection read this same value. */
121
122
  resolvedInteractionPosture: "interactive" | "headless" | undefined;
123
+ /** owned — the validated/resolved microCompact knob (design/374; {@link resolveMicroCompactKnob}
124
+ * for the door + placement reasoning). The per-run state builder consumes exactly these values —
125
+ * the deps bag is never re-read after this door. */
126
+ microCompactKnob: {
127
+ machine: ContextEditMachine;
128
+ clearOnRejection: boolean;
129
+ };
122
130
  /** owned — the resolved role; its `systemPrompt` seat is still read at prompt-input time. */
123
131
  resolvedRole: ResolvedRole;
124
132
  /** owned — the resolved main model (`resolvedRole.model`, re-exposed as the name every later
@@ -163,5 +171,18 @@ export interface PrepareConfigDoorsResult {
163
171
  * the resolver's off states). */
164
172
  brainCallGuardrailMs: number | undefined;
165
173
  }
166
- /** The B-1 phase body — the config-doors slice, verbatim (see the module header for the contract). */
174
+ /** design/374 — the microCompact knob door (坏值响亮度默认律, the sibling knob doors' form:
175
+ * interactionPosture / toolModelGate / staleToolResultOffload). Placement is load-bearing (review
176
+ * r3): it runs in THIS pre-first-await config-doors phase so a malformed declaration refuses
177
+ * BEFORE any resource-bearing preparation — session acquisition, execution-env creation, the MCP
178
+ * dial — whose external effects a late refusal's teardown cannot undo. SINGLE READ per seat, and
179
+ * the CONTAINER is judged first (reviews r1/r2): an accessor-backed bag answering a legal value at
180
+ * one read and junk at another must be caught by whichever read this door takes, and a
181
+ * `microCompact: "cc"` / null / number / array bag makes both property reads answer undefined —
182
+ * it must refuse exactly like a malformed field, never fold to the defaults. The RESOLVED values
183
+ * returned here are immutable and are the state builder's ONLY source — nothing re-reads the bag. */
184
+ export declare function resolveMicroCompactKnob(bag: RunnerDeps["microCompact"]): {
185
+ machine: ContextEditMachine;
186
+ clearOnRejection: boolean;
187
+ };
167
188
  export declare function prepareConfigDoors(input: PrepareConfigDoorsInput): PrepareConfigDoorsResult;