@sema-agent/core 5.59.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 (40) hide show
  1. package/CHANGELOG.md +40 -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/stream-engine.js +9 -1
  8. package/dist/core/auto-compaction.js +2 -2
  9. package/dist/core/checkpoint-store.d.ts +116 -19
  10. package/dist/core/checkpoint-store.js +15 -8
  11. package/dist/core/context-edit.d.ts +243 -41
  12. package/dist/core/context-edit.js +247 -32
  13. package/dist/core/governance-codes.d.ts +1 -1
  14. package/dist/core/governance-codes.js +5 -0
  15. package/dist/core/locked-config.d.ts +36 -4
  16. package/dist/core/locked-config.js +34 -1
  17. package/dist/core/mcp.js +10 -6
  18. package/dist/core/memory-engine/content-origin.d.ts +24 -2
  19. package/dist/core/memory-engine/content-origin.js +6 -1
  20. package/dist/core/memory.d.ts +10 -0
  21. package/dist/core/park-selfcheck.js +1 -0
  22. package/dist/core/permission-rule-consent.js +9 -5
  23. package/dist/core/runner/prepare-config-doors.d.ts +22 -1
  24. package/dist/core/runner/prepare-config-doors.js +36 -0
  25. package/dist/core/runner/prepare-task.d.ts +28 -1
  26. package/dist/core/runner/prepare-task.js +106 -8
  27. package/dist/core/runner/runtask.js +41 -4
  28. package/dist/core/store-contracts/checkpoint-store-contract.js +32 -0
  29. package/dist/core/tool-policy.d.ts +24 -8
  30. package/dist/core/tool-policy.js +3 -3
  31. package/dist/core/tools.js +1 -1
  32. package/dist/core/trace.d.ts +36 -0
  33. package/dist/core/types.d.ts +127 -2
  34. package/dist/core/untrusted-text.d.ts +11 -0
  35. package/dist/core/untrusted-text.js +1 -0
  36. package/dist/engine/llm/types.d.ts +21 -2
  37. package/dist/engine/loop/agent-loop.js +7 -1
  38. package/dist/engine/loop/types.d.ts +4 -1
  39. package/dist/tools/fs/fs-bash.js +1 -2
  40. package/package.json +1 -1
@@ -93,7 +93,7 @@ export type NoticeAudience = "user" | "operator";
93
93
  * src/ for notice mint shapes and names any code that is minted but unregistered, or registered but
94
94
  * no longer minted.
95
95
  */
96
- export declare const ENGINE_NOTICE_CODES: readonly ["config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "delegation.transcript_integrity", "mcp.revocation_probe_failed", "workflow.governance_key_stripped", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "route.fallback_to_primary", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "task.injection_priority_unimplemented", "tool_result.offload_put_failed"];
96
+ export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "delegation.transcript_integrity", "mcp.revocation_probe_failed", "workflow.governance_key_stripped", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "route.fallback_to_primary", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "task.injection_priority_unimplemented", "tool_result.offload_put_failed"];
97
97
  /** A code this engine mints (see {@link ENGINE_NOTICE_CODES}). NOT the type of
98
98
  * `EngineNotice.code`, which stays `string` — a host forwarding its own notices through the same
99
99
  * sink is a supported shape, and narrowing that field would break it. */
@@ -17,6 +17,7 @@ export const NON_GOVERNANCE_MEMORY_CODES = new Set([
17
17
  "memory.session_polluted",
18
18
  "memory.harvest_quarantined",
19
19
  "memory.delegation_static_mark_waived",
20
+ "memory.content_class_declared",
20
21
  "memory.consolidation_driver_superseded",
21
22
  "memory.challenge_sweep_failed",
22
23
  "memory.lineage_settle_failed",
@@ -85,6 +86,7 @@ export const RULE_SYNC_DROP_CODES = {
85
86
  server_rejected: "local-quarantined",
86
87
  };
87
88
  export const ENGINE_NOTICE_CODES = [
89
+ "config.autocompact_window_clamped",
88
90
  "config.env_timeout_discarded",
89
91
  "config.materialize_env_discarded",
90
92
  "config.models_swapped",
@@ -98,6 +100,7 @@ export const ENGINE_NOTICE_CODES = [
98
100
  "memory.session_polluted",
99
101
  "memory.harvest_quarantined",
100
102
  "memory.delegation_static_mark_waived",
103
+ "memory.content_class_declared",
101
104
  "memory.hold_opened",
102
105
  "memory.hold_released",
103
106
  "memory.hold_disposed",
@@ -122,6 +125,7 @@ const NOTICE_AUDIENCE_TABLE = {
122
125
  "memory.hold_disposed": "user",
123
126
  "task.user_steer_undrained": "user",
124
127
  "task.user_followup_undrained": "user",
128
+ "config.autocompact_window_clamped": "operator",
125
129
  "config.env_timeout_discarded": "operator",
126
130
  "config.materialize_env_discarded": "operator",
127
131
  "config.models_swapped": "operator",
@@ -132,6 +136,7 @@ const NOTICE_AUDIENCE_TABLE = {
132
136
  "delegation.transcript_integrity": "operator",
133
137
  "mcp.revocation_probe_failed": "operator",
134
138
  "workflow.governance_key_stripped": "operator",
139
+ "memory.content_class_declared": "operator",
135
140
  "memory.consolidation_recommended": "operator",
136
141
  "memory.consolidation_committed": "operator",
137
142
  "memory.consolidation_conflict": "operator",
@@ -1,4 +1,4 @@
1
- import type { RunnerDeps, TaskSpec } from "./types.js";
1
+ import type { McpServerSpec, RunnerDeps, TaskSpec } from "./types.js";
2
2
  import type { ToolPolicy } from "./tool-policy.js";
3
3
  /** The closed set of administratively lockable configuration keys. Adding a member is a deliberate
4
4
  * edit HERE (tsc forces the registry row), never a free-form string. */
@@ -38,9 +38,12 @@ export interface LockedConfig {
38
38
  export interface LockedPreflight {
39
39
  /** The validated lock set (empty when the deployment declares none). */
40
40
  lockedKeys: ReadonlySet<LockedKey>;
41
- /** Effective MCP server list for this task: `spec.mcp` when the key is unlocked; with `mcp`
42
- * locked a spec-supplied value has already been refused, so this is always `undefined` (core has
43
- * no deployment-level MCP seat a locked deployment mounts no task-supplied servers). */
41
+ /** Effective MCP server list for this task: a validated, frozen SNAPSHOT of `spec.mcp` when the
42
+ * key is unlocked (see {@link snapshotAndValidateMcpEntries} the content-class declaration is read once
43
+ * and judged here, so no mint site downstream can be handed a different value than the door
44
+ * approved); with `mcp` locked a spec-supplied value has already been refused, so this is always
45
+ * `undefined` (core has no deployment-level MCP seat — a locked deployment mounts no
46
+ * task-supplied servers). */
44
47
  mcp: TaskSpec["mcp"];
45
48
  /** Effective caller tool policy: the same `spec.toolPolicy ?? deps.toolPolicy` slot the gate has
46
49
  * always enforced, resolved ONCE here. With `toolPolicy` locked, the deployment's own
@@ -61,6 +64,35 @@ export interface LockedPreflight {
61
64
  * then fails the first prepare instead of silently guarding nothing).
62
65
  */
63
66
  export declare function resolveLockedKeys(config: LockedConfig | undefined): ReadonlySet<LockedKey>;
67
+ /**
68
+ * design/378 — turn a caller-owned MCP entry list into FROZEN PLAIN DATA, reading every own property
69
+ * exactly once.
70
+ *
71
+ * WHY A COPY. `McpServerSpec.contentOrigin` decides whether a server's tools mark the session's
72
+ * memory, and the entry objects travel a long way: the preparation door validates them, then
73
+ * `materializeMcpTools` connects them, then every mid-task refresh re-reads the same object. A value
74
+ * read off a LIVE object is only true for as long as the object holds still, and the fail-open
75
+ * direction is the reachable one — so both boundaries consume data instead.
76
+ *
77
+ * The read discipline is the load-bearing part, and it is why this is a spread rather than
78
+ * "read the property, then copy the object": a getter answering ALTERNATING LEGAL values would
79
+ * satisfy any number of re-reads (`"external"` to a validator, `"local"` to a mint site), and no
80
+ * vocabulary guard anywhere has purchase on a legal value. One spread evaluates every own property
81
+ * EXACTLY ONCE and materializes the result as plain data; every later reader — validator, connect,
82
+ * refresh — then sees the same byte. Frozen so nothing downstream rewrites it.
83
+ *
84
+ * Boundary, stated: the copy is one level deep — nested option objects (transport, headers, env,
85
+ * toolAxes) stay shared with the caller's entry exactly as before, because the axis this owns is the
86
+ * content class; readers of the nested shapes normalize their own values (the transport DISCRIMINATOR
87
+ * is derived, never echoed raw). Own ENUMERABLE properties only: an entry whose fields live on a
88
+ * prototype was never the plain configuration record this type describes, and its absent class reads
89
+ * fail-closed like any other absence.
90
+ *
91
+ * Exported because the SECOND boundary needs the identical guarantee: `materializeMcpTools` is public
92
+ * and a library caller reaches it without passing any door, so it snapshots its own input through
93
+ * this same function (idempotent over an already-frozen entry). One spelling, both entrances.
94
+ */
95
+ export declare function snapshotMcpEntries(entries: readonly McpServerSpec[]): McpServerSpec[];
64
96
  /**
65
97
  * The single preflight resolver: validate the lock declaration, refuse a spec that tries to occupy
66
98
  * a locked slot (`config.locked_key`, two-state — the whole prepare is rejected, nothing is
@@ -1,3 +1,5 @@
1
+ import { isToolContentOrigin, TOOL_CONTENT_ORIGINS } from "./memory-engine/content-origin.js";
2
+ import { inlineUntrusted } from "./untrusted-text.js";
1
3
  export const LOCKED_KEY_REGISTRY = {
2
4
  mcp: { specFields: ["mcp"] },
3
5
  toolPolicy: { specFields: ["toolPolicy", "basePolicyForResumeEdit"] },
@@ -23,6 +25,37 @@ export function resolveLockedKeys(config) {
23
25
  }
24
26
  return keys;
25
27
  }
28
+ export function snapshotMcpEntries(entries) {
29
+ return entries.map((entry) => Object.freeze({ ...entry }));
30
+ }
31
+ function describeDeclared(value) {
32
+ try {
33
+ return JSON.stringify(value) ?? String(value);
34
+ }
35
+ catch {
36
+ try {
37
+ return String(value);
38
+ }
39
+ catch {
40
+ return `[unrepresentable ${typeof value}]`;
41
+ }
42
+ }
43
+ }
44
+ function snapshotAndValidateMcpEntries(entries) {
45
+ const copies = snapshotMcpEntries(entries);
46
+ for (const copy of copies) {
47
+ const declared = copy.contentOrigin;
48
+ if (declared !== undefined && !isToolContentOrigin(declared)) {
49
+ throw codedError("config.mcp_content_class", `MCP server ${inlineUntrusted(typeof copy.name === "string" ? JSON.stringify(copy.name) : describeDeclared(copy.name), 80)} declares contentOrigin ` +
50
+ `${inlineUntrusted(describeDeclared(declared), 80)}, which is not one of ` +
51
+ `${TOOL_CONTENT_ORIGINS.map((v) => `"${v}"`).join(" | ")}. The declaration decides whether this server's tools ` +
52
+ `mark the session's memory externally exposed — a value this engine cannot read is refused at the door rather ` +
53
+ `than folded to a class, because folding it would let a deployment believe a declaration is in force while every ` +
54
+ `call keeps writing a one-way durable mark.`);
55
+ }
56
+ }
57
+ return copies;
58
+ }
26
59
  export function preflightLockedConfig(spec, deps) {
27
60
  const lockedKeys = resolveLockedKeys(deps.lockedConfig);
28
61
  for (const key of lockedKeys) {
@@ -36,7 +69,7 @@ export function preflightLockedConfig(spec, deps) {
36
69
  }
37
70
  return {
38
71
  lockedKeys,
39
- mcp: lockedKeys.has("mcp") ? undefined : spec.mcp,
72
+ mcp: lockedKeys.has("mcp") || spec.mcp === undefined ? undefined : snapshotAndValidateMcpEntries(spec.mcp),
40
73
  toolPolicy: lockedKeys.has("toolPolicy") ? deps.toolPolicy : (spec.toolPolicy ?? deps.toolPolicy),
41
74
  basePolicyForResumeEdit: lockedKeys.has("toolPolicy") ? deps.basePolicyForResumeEdit : (spec.basePolicyForResumeEdit ?? deps.basePolicyForResumeEdit),
42
75
  };
package/dist/core/mcp.js CHANGED
@@ -14,6 +14,7 @@ import { truncateError } from "./tool-errors.js";
14
14
  import { delimitUntrusted, inlineUntrusted, sanitizeUntrustedText } from "./untrusted-text.js";
15
15
  import { discloseReminderShaped, observeReminderMarkEcho } from "./reminder-disclosure.js";
16
16
  import { withContentOrigin } from "./memory-engine/content-origin.js";
17
+ import { snapshotMcpEntries } from "./locked-config.js";
17
18
  import { validateJsonSchemaShape } from "./runner/strict-output-schema.js";
18
19
  export const MCP_PREFIX = MCP_NAMESPACE.prefix;
19
20
  const MCP_OUTPUT_TOKENS_DEFAULT = 25_000;
@@ -753,8 +754,9 @@ export function mcpToolSchemaProblem(schema) {
753
754
  return undefined;
754
755
  }
755
756
  export async function materializeMcpTools(specs, principal, onElicit, imageResizer, reminderDisclosure, mcpRevocations) {
757
+ const entries = snapshotMcpEntries(specs);
756
758
  {
757
- const collision = findNamespacePrefixCollision(MCP_NAMESPACE, specs.map((s) => s.name));
759
+ const collision = findNamespacePrefixCollision(MCP_NAMESPACE, entries.map((s) => s.name));
758
760
  if (collision) {
759
761
  const [a, b] = collision.peers;
760
762
  const e = new Error(a === b
@@ -801,11 +803,11 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
801
803
  const droppedTools = [];
802
804
  let disposing = false;
803
805
  const serverHandles = [];
804
- const settled = await Promise.allSettled(specs.map((spec) => connectServer(spec, principal, onElicit, imageResizer, mcpDisclosure, isServerRevoked)));
806
+ const settled = await Promise.allSettled(entries.map((spec) => connectServer(spec, principal, onElicit, imageResizer, mcpDisclosure, isServerRevoked)));
805
807
  try {
806
- for (let i = 0; i < specs.length; i++) {
808
+ for (let i = 0; i < entries.length; i++) {
807
809
  const r = settled[i];
808
- const spec = specs[i];
810
+ const spec = entries[i];
809
811
  if (r.status === "fulfilled") {
810
812
  const s = r.value;
811
813
  clients.push(s.client);
@@ -1513,6 +1515,7 @@ async function connectServer(spec, principal, onElicit, imageResizer, reminderDi
1513
1515
  }
1514
1516
  }
1515
1517
  function intakeListedTools(listed, spec, client, health, imageResizer, reminderDisclosure, isServerRevoked) {
1518
+ const declaredContentOrigin = spec.contentOrigin;
1516
1519
  const serverTools = [];
1517
1520
  const serverAxes = [];
1518
1521
  const dropped = [];
@@ -1576,7 +1579,7 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
1576
1579
  const mcpToolMeta = t._meta;
1577
1580
  const mcpMaxResultSizeChars = resolveMcpDeclaredResultSize(mcpToolMeta);
1578
1581
  const mcpAlwaysLoad = mcpToolMeta?.["anthropic/alwaysLoad"] === true;
1579
- serverTools.push({
1582
+ const mounted = {
1580
1583
  name: namespacedName,
1581
1584
  description: capMcpToolDescription(effectiveDescription ?? `MCP tool ${inlineUntrusted(sanitizeMcpModelFacingText(remoteName))} from ${spec.name}`),
1582
1585
  label: `${spec.name}:${remoteName}`,
@@ -1678,7 +1681,8 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
1678
1681
  return { content, details: { type: "mcp", structuredContent: sc }, terminate: false };
1679
1682
  return { content, details: res, terminate: false };
1680
1683
  },
1681
- });
1684
+ };
1685
+ serverTools.push(declaredContentOrigin !== undefined ? withContentOrigin(mounted, declaredContentOrigin) : mounted);
1682
1686
  }
1683
1687
  return { serverTools, serverAxes, dropped, advisories };
1684
1688
  }
@@ -22,13 +22,22 @@
22
22
  * does not, the default is EXTERNAL (fail-closed: unknown = external), overridable only through the
23
23
  * explicit `TaskSpec.memory.trustedTools` allowlist. A core-mounted built-in without a declaration
24
24
  * defaults local: core owns those definitions, and the fail-closed default exists for tools core
25
- * CANNOT vouch for, not for its own.
25
+ * CANNOT vouch for, not for its own. A declaration is honored only when it SPELLS a member of the
26
+ * closed vocabulary — an unreadable one is not a declaration.
27
+ *
28
+ * The three tier names are BOUNDARY-RELATIVE, not statements about topology: `"local"` means "does
29
+ * not bring content from outside the deployment's trust boundary", which a unix socket to another
30
+ * process the deployment itself runs satisfies and a loopback HTTP call to somebody else's service
31
+ * does not. Distance is not the axis; whose content it is, is.
26
32
  *
27
33
  * There is deliberately no "gate inactive" state: every mounted tool gets a class.
28
34
  */
29
35
  import type { ToolContentOrigin } from "../types.js";
30
36
  export interface ClassifyToolContentOriginInput {
31
- /** The tool's own `contentOrigin` declaration (ToolSpec/AgentTool carry) — wins when present. */
37
+ /** The tool's own `contentOrigin` declaration (ToolSpec/AgentTool carry) — wins when present AND
38
+ * a member of the closed vocabulary ({@link isToolContentOrigin}); a present non-member is the
39
+ * unreadable-declaration case and classifies EXTERNAL. The annotation is intent, not a guarantee:
40
+ * the property rides tool objects untyped. */
32
41
  declared?: ToolContentOrigin;
33
42
  /** True ⇔ the name belongs to a protocol namespace (mcp__/a2a__ …) — an external channel. */
34
43
  isProtocolTool: boolean;
@@ -37,6 +46,10 @@ export interface ClassifyToolContentOriginInput {
37
46
  /** True ⇔ `TaskSpec.memory.trustedTools` names this tool (explicit host exemption). */
38
47
  trusted: boolean;
39
48
  }
49
+ /** The vocabulary in wire/message order — the one spelling list a refusal quotes back. */
50
+ export declare const TOOL_CONTENT_ORIGINS: readonly ToolContentOrigin[];
51
+ /** Runtime membership in the closed vocabulary. */
52
+ export declare function isToolContentOrigin(value: unknown): value is ToolContentOrigin;
40
53
  export declare function classifyToolContentOrigin(input: ClassifyToolContentOriginInput): ToolContentOrigin;
41
54
  /**
42
55
  * Declare `origin` on a tool object built OUTSIDE `defineTool` — a raw AgentTool literal, which is
@@ -60,6 +73,15 @@ export interface AgentToolFace {
60
73
  export interface AgentPoolTool {
61
74
  name: string;
62
75
  aliases?: readonly string[];
76
+ /** The pool row's own declaration — a STATIC surface the deployment writes when it composes the
77
+ * delegation tool, and the only class information this judgment has about the child's roster.
78
+ *
79
+ * A per-entry MCP declaration (design/378, `McpServerSpec.contentOrigin`) does NOT reach here: the
80
+ * pool is a declaration, not a live roster, and this judgment never sees the server entries. A
81
+ * deployment that hands a declared server's `mcp__` tools to children MIRRORS the value onto the
82
+ * matching pool rows; not mirroring it leaves them on the protocol arm, so the delegation reads
83
+ * external and the parent over-marks — the safe direction, and the reason this is a documented
84
+ * duty rather than a refusal. */
63
85
  contentOrigin?: ToolContentOrigin;
64
86
  }
65
87
  /**
@@ -1,6 +1,11 @@
1
+ const TOOL_CONTENT_ORIGIN_SET = { external: true, execution: true, local: true };
2
+ export const TOOL_CONTENT_ORIGINS = Object.keys(TOOL_CONTENT_ORIGIN_SET);
3
+ export function isToolContentOrigin(value) {
4
+ return typeof value === "string" && Object.prototype.hasOwnProperty.call(TOOL_CONTENT_ORIGIN_SET, value);
5
+ }
1
6
  export function classifyToolContentOrigin(input) {
2
7
  if (input.declared !== undefined)
3
- return input.declared;
8
+ return isToolContentOrigin(input.declared) ? input.declared : "external";
4
9
  if (input.trusted)
5
10
  return "local";
6
11
  if (input.isProtocolTool)
@@ -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
  }
@@ -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;
@@ -83,10 +83,45 @@ export function resolveModelPromptTraits(model, spec, internals) {
83
83
  fableMitigations: isFableFamilyModelId(model.id),
84
84
  };
85
85
  }
86
+ function microCompactConfigError(field, value, legal) {
87
+ let shown;
88
+ try {
89
+ shown = JSON.stringify(value) ?? String(value);
90
+ }
91
+ catch {
92
+ try {
93
+ shown = String(value);
94
+ }
95
+ catch {
96
+ shown = `[unrepresentable ${typeof value}]`;
97
+ }
98
+ }
99
+ const seat = field === undefined ? "microCompact" : `microCompact.${field}`;
100
+ const e = new Error(`${seat} ${shown} is not ${legal} — an unevaluable declaration is refused loudly, ` +
101
+ `never folded to the default: a silently-ignored opt-in would run the pre-374 machine while the ` +
102
+ `deployment believes it opted in.`);
103
+ e.code = "config.microcompact_invalid";
104
+ return e;
105
+ }
106
+ export function resolveMicroCompactKnob(bag) {
107
+ if (bag !== undefined && (typeof bag !== "object" || bag === null || Array.isArray(bag))) {
108
+ throw microCompactConfigError(undefined, bag, "an object carrying optional machine/clearOnRejection keys");
109
+ }
110
+ const declaredMachine = bag?.machine;
111
+ if (declaredMachine !== undefined && declaredMachine !== "legacy" && declaredMachine !== "cc") {
112
+ throw microCompactConfigError("machine", declaredMachine, `"legacy" | "cc"`);
113
+ }
114
+ const declaredClearOnRejection = bag?.clearOnRejection;
115
+ if (declaredClearOnRejection !== undefined && typeof declaredClearOnRejection !== "boolean") {
116
+ throw microCompactConfigError("clearOnRejection", declaredClearOnRejection, "a boolean");
117
+ }
118
+ return { machine: declaredMachine ?? "legacy", clearOnRejection: declaredClearOnRejection === true };
119
+ }
86
120
  export function prepareConfigDoors(input) {
87
121
  const { deps, sessions, resume, internals } = input;
88
122
  let spec = input.spec;
89
123
  assertRestoreGatedToolsValue(spec.restoreGatedTools);
124
+ const microCompactKnob = resolveMicroCompactKnob(deps.microCompact);
90
125
  const assertToolNameListValue = (value, seat) => {
91
126
  if (value === undefined)
92
127
  return;
@@ -318,6 +353,7 @@ export function prepareConfigDoors(input) {
318
353
  promptProfile,
319
354
  lockedPreflight,
320
355
  resolvedInteractionPosture,
356
+ microCompactKnob,
321
357
  resolvedRole,
322
358
  model,
323
359
  thinking,
@@ -20,6 +20,7 @@ import type { MemoryEngine } from "../memory-engine/engine.js";
20
20
  import { type GitStatusLaneRef } from "./git-status-frame.js";
21
21
  import { type ToolManifestRow } from "../../prompt-assembly/tool-catalog.js";
22
22
  import type { ToolDisclosureManifest } from "../trace.js";
23
+ import { type ClearedProjectionLedger, type ContextEditMachine, type OccurrenceIndex } from "../context-edit.js";
23
24
  import type { TaskNotificationPayload } from "../task-notification.js";
24
25
  import { type CwdRef, type ReadFace } from "../../tools/fs/index.js";
25
26
  import { type WorkflowSizeGuideline } from "../../orchestration/workflow-size-guideline.js";
@@ -134,7 +135,6 @@ export declare function checkpointScopeOf(spec: {
134
135
  export { resolveCheckpointStore } from "../checkpoint-store.js";
135
136
  export { isFableFamilyModelId, resolveModelPromptTraits, resolveTaskLimits } from "./prepare-config-doors.js";
136
137
  export { rebaseWorkspacePath, rebaseWorkspacePathAcross } from "./prepare-workspace-restore.js";
137
- /** Everything the run loop needs, built once by {@link prepareTask} (task setup, isolated from the loop). */
138
138
  export interface Prepared {
139
139
  harness: AgentHarness;
140
140
  /** The CONCRETE built-in session (engine-internal: prepare constructs/acquires `StoredSession` itself,
@@ -876,6 +876,33 @@ export interface Prepared {
876
876
  trimPressureRef: {
877
877
  droppedMessages: boolean;
878
878
  };
879
+ /** design/374 slices 1b/2 — the microCompact machine state this run: the selected clearing
880
+ * machine, the cleared-projection ledger (request-view application, durable decisions — see
881
+ * `context-edit.ts`'s ledger note; per-run in-memory, so durable resume / `resumeAt` rebuilds
882
+ * start EMPTY by construction), the last request's projection seat (what the provider actually
883
+ * saw — the MC-R rejection arm computes its candidates and savings on THIS view, never on the
884
+ * raw session rebuild), and the MC-R knob. Default machine "legacy" + MC-R off ⇒ the ledger
885
+ * never gains an entry and every replay is a same-reference no-op (default bytes unchanged). */
886
+ microCompact: PreparedMicroCompact;
887
+ }
888
+ /** See {@link Prepared.microCompact}. */
889
+ export interface PreparedMicroCompact {
890
+ machine: ContextEditMachine;
891
+ /** MC-R (design/374 §3.2): one-shot clear-and-retry on a provider input-too-long rejection.
892
+ * Default false (X2: lands with the machinery, flips with slice 3). */
893
+ clearOnRejection: boolean;
894
+ ledger: ClearedProjectionLedger;
895
+ projectionRef: {
896
+ current?: {
897
+ /** The FINAL projected view of the last provider request (post trim/sweep). */
898
+ messages: AgentMessage[];
899
+ /** Occurrence coordinates of that view (object-identity first, unambiguous-group fallback). */
900
+ keyOf: OccurrenceIndex["keyOf"];
901
+ };
902
+ };
903
+ /** The same offload persist seat the frontier machine uses (write-once, idempotent), so MC-R
904
+ * clears compose identical markers. Absent when no offload store is configured. */
905
+ offloadPersist?: (toolCallId: string, fullText: string) => string;
879
906
  }
880
907
  /**
881
908
  * WHICH tool call a committed durable park is holding this run — `undefined` when nothing parked, or