@sema-agent/server 7.121.0 → 7.123.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 (136) hide show
  1. package/MIGRATION.md +175 -0
  2. package/README.md +6 -1
  3. package/README.zh-CN.md +6 -1
  4. package/USAGE.md +57 -18
  5. package/dist/approval-reconciler.d.ts +8 -0
  6. package/dist/approval-reconciler.js +72 -52
  7. package/dist/audit.d.ts +1 -1
  8. package/dist/audit.js +52 -22
  9. package/dist/auto-mode-face.d.ts +16 -5
  10. package/dist/auto-mode-face.js +7 -4
  11. package/dist/bench/s1/live-deps.js +1 -0
  12. package/dist/boot/config-center.js +3 -0
  13. package/dist/boot/leader.js +1 -0
  14. package/dist/boot/reapers.d.ts +3 -0
  15. package/dist/boot/reapers.js +2 -1
  16. package/dist/boot/resolve-spec.js +1 -4
  17. package/dist/boot/shutdown.js +6 -1
  18. package/dist/boot/stage-08-reapers.js +2 -2
  19. package/dist/capabilities/memory-notice.d.ts +1 -1
  20. package/dist/config-catalog.js +4 -4
  21. package/dist/config-center/apply-effective.d.ts +1 -1
  22. package/dist/config-center/apply-effective.js +5 -3
  23. package/dist/config-center/center-request.d.ts +6 -9
  24. package/dist/config-center/center-request.js +11 -27
  25. package/dist/config-center/http-client.js +3 -3
  26. package/dist/config-center/read-warnings.d.ts +56 -35
  27. package/dist/config-center/read-warnings.js +67 -19
  28. package/dist/config-center/types.d.ts +7 -8
  29. package/dist/config-lkg.d.ts +12 -9
  30. package/dist/config-lkg.js +38 -39
  31. package/dist/config-provider.d.ts +3 -2
  32. package/dist/config-provider.js +2 -2
  33. package/dist/config-types.d.ts +20 -9
  34. package/dist/config-types.js +1 -1
  35. package/dist/config.d.ts +10 -10
  36. package/dist/config.js +92 -127
  37. package/dist/fleet/drain-entry.d.ts +28 -3
  38. package/dist/fleet/drain-entry.js +3 -0
  39. package/dist/fleet/engine-signal.d.ts +54 -23
  40. package/dist/fleet/engine-signal.js +29 -5
  41. package/dist/fleet/serving-lease.js +13 -15
  42. package/dist/fleet-client.d.ts +8 -6
  43. package/dist/fleet-client.js +4 -3
  44. package/dist/governance-overrides.d.ts +7 -2
  45. package/dist/http/admission.js +7 -5
  46. package/dist/http/approval-sweeps.js +2 -2
  47. package/dist/http/checkpoint-ctx.d.ts +60 -11
  48. package/dist/http/checkpoint-ctx.js +23 -9
  49. package/dist/http/dispatch.js +22 -19
  50. package/dist/http/health-self-report.d.ts +3 -0
  51. package/dist/http/resume-legs.js +8 -8
  52. package/dist/http/route-ctx.d.ts +19 -3
  53. package/dist/http/route-table.d.ts +3 -0
  54. package/dist/http/route-table.js +4 -0
  55. package/dist/http/routes/admin-drain.d.ts +5 -2
  56. package/dist/http/routes/admin-drain.js +14 -2
  57. package/dist/http/routes/approvals-assistant.d.ts +1 -0
  58. package/dist/http/routes/approvals-assistant.js +6 -6
  59. package/dist/http/routes/capabilities.js +7 -5
  60. package/dist/http/routes/fleet.d.ts +1 -0
  61. package/dist/http/routes/fleet.js +1 -1
  62. package/dist/http/routes/images.d.ts +1 -0
  63. package/dist/http/routes/images.js +1 -1
  64. package/dist/http/routes/leader.js +6 -0
  65. package/dist/http/routes/runs.d.ts +2 -0
  66. package/dist/http/routes/runs.js +5 -4
  67. package/dist/http/routes/sessions.d.ts +1 -0
  68. package/dist/http/routes/sessions.js +2 -5
  69. package/dist/http/routes/trace-usage.d.ts +1 -0
  70. package/dist/http/routes/trace-usage.js +1 -1
  71. package/dist/http/routes/workflows.d.ts +1 -0
  72. package/dist/http/routes/workflows.js +1 -1
  73. package/dist/http/send.d.ts +13 -0
  74. package/dist/http/send.js +9 -0
  75. package/dist/http/server.d.ts +3 -0
  76. package/dist/http/stored-body-replay.d.ts +28 -0
  77. package/dist/http/stored-body-replay.js +22 -0
  78. package/dist/index.d.ts +2 -0
  79. package/dist/index.js +1 -0
  80. package/dist/leader/leader.d.ts +23 -1
  81. package/dist/leader/leader.js +6 -1
  82. package/dist/leader/repair-wire.d.ts +1 -1
  83. package/dist/leader/repair-wire.js +2 -2
  84. package/dist/leader/run-entry.d.ts +27 -0
  85. package/dist/leader/run-entry.js +29 -0
  86. package/dist/leader/wire.d.ts +10 -0
  87. package/dist/leader/wire.js +15 -10
  88. package/dist/model-entry-refusal.d.ts +18 -9
  89. package/dist/model-entry-refusal.js +19 -14
  90. package/dist/model-route-endpoint.d.ts +21 -1
  91. package/dist/model-route-endpoint.js +9 -3
  92. package/dist/model-route-gap.d.ts +103 -0
  93. package/dist/model-route-gap.js +80 -0
  94. package/dist/model-route-url.d.ts +13 -20
  95. package/dist/model-route-url.js +28 -24
  96. package/dist/namespaced-rule-name.d.ts +16 -30
  97. package/dist/namespaced-rule-name.js +2 -20
  98. package/dist/observability/fail-open.d.ts +23 -10
  99. package/dist/observability/fail-open.js +8 -8
  100. package/dist/plugins/approval-ask-store-file.d.ts +1 -0
  101. package/dist/plugins/approval-ask-store-file.js +3 -0
  102. package/dist/plugins/approval-ask-store-memory.d.ts +1 -0
  103. package/dist/plugins/approval-ask-store-memory.js +6 -0
  104. package/dist/plugins/approval-ask-store-sql.d.ts +12 -0
  105. package/dist/plugins/approval-ask-store-sql.js +4 -0
  106. package/dist/plugins/one-time-migrations.js +40 -0
  107. package/dist/plugins/park-commit-watch.d.ts +19 -5
  108. package/dist/plugins/park-commit-watch.js +24 -17
  109. package/dist/plugins/session-policy-store-sql.js +1 -2
  110. package/dist/plugins/workflow-run-store-sql.d.ts +4 -2
  111. package/dist/router/route-orchestration.d.ts +4 -2
  112. package/dist/router/route-orchestration.js +2 -1
  113. package/dist/rule-entry-list.d.ts +3 -13
  114. package/dist/rule-entry-list.js +1 -12
  115. package/dist/run-local.js +4 -2
  116. package/dist/runtime-governance.d.ts +67 -32
  117. package/dist/runtime-governance.js +28 -9
  118. package/dist/session-policy-wire.d.ts +47 -49
  119. package/dist/session-policy-wire.js +41 -36
  120. package/dist/session-sync.d.ts +5 -1
  121. package/dist/session-sync.js +2 -1
  122. package/dist/sighup-idle.d.ts +7 -1
  123. package/dist/sighup-idle.js +4 -4
  124. package/dist/spec-fields.d.ts +4 -4
  125. package/dist/spec-fields.js +1 -4
  126. package/dist/task-settings.d.ts +36 -14
  127. package/dist/task-settings.js +20 -23
  128. package/dist/tool-approval.d.ts +6 -0
  129. package/dist/tool-approval.js +13 -1
  130. package/dist/trace/redact.d.ts +3 -3
  131. package/dist/unattended-permission-mode.d.ts +17 -13
  132. package/dist/unattended-permission-mode.js +3 -1
  133. package/dist/url-shape.d.ts +31 -0
  134. package/dist/url-shape.js +41 -0
  135. package/package.json +3 -3
  136. package/scripts/drain-check-local.mjs +1 -1
@@ -6,6 +6,7 @@ import { buildRevokeFrame } from "./approval-card.js";
6
6
  import { encodeCheckpointScope } from "./security.js";
7
7
  const RECONCILE_STORE_TIMEOUT_MS = 10_000;
8
8
  const RECONCILE_SEGMENT_BUDGET_MS = 30_000;
9
+ const PARK_COMMIT_BIND_BUDGET_MS = 2_000;
9
10
  function withDeadline(op, label, timeoutMs = RECONCILE_STORE_TIMEOUT_MS) {
10
11
  let timer;
11
12
  return Promise.race([
@@ -80,6 +81,13 @@ export function decideReconcileAction(input) {
80
81
  }
81
82
  export function createApprovalReconciler(deps) {
82
83
  const { askStore, checkpoints, runs, logger, metrics, batchLimit, pendingGraceMs, adhocGraceMs, orphanTtlMs, unattendedPolicy, emitRevoke } = deps;
84
+ const warn = (event, fields) => {
85
+ try {
86
+ logger.warn(event, fields);
87
+ }
88
+ catch {
89
+ }
90
+ };
83
91
  const revoke = (row, askIds, reason, nowMs) => {
84
92
  if (askIds.length === 0 || !emitRevoke)
85
93
  return;
@@ -87,39 +95,32 @@ export function createApprovalReconciler(deps) {
87
95
  emitRevoke(buildRevokeFrame(row.batchId, askIds, reason, nowMs, row.taskId), { owner: row.owner, sessionId: row.sessionId, taskId: row.taskId });
88
96
  }
89
97
  catch (err) {
90
- try {
91
- logger.warn("approval_revoke_emit_failed", { batchId: row.batchId, count: askIds.length, err: err instanceof Error ? err.message : String(err) });
92
- }
93
- catch {
94
- }
98
+ warn("approval_revoke_emit_failed", { batchId: row.batchId, count: askIds.length, err: err instanceof Error ? err.message : String(err) });
95
99
  }
96
100
  };
97
- const reconcileOne = async (ask, nowMs) => {
101
+ const bindAtGate = async (ask, nowMs, deadlineMs) => {
102
+ const expired = () => deadlineMs !== undefined && Date.now() >= deadlineMs;
98
103
  const batchRow = await withDeadline(askStore.getBatch(ask.batchId), "getBatch");
99
104
  let batchState = batchRow?.state ?? "MISSING";
100
105
  let batchBoundAskId = batchRow?.boundAskId ?? null;
101
106
  const irreversibleBatch = batchState === "ABORTED" || (batchState === "ROUTING_BOUND" && batchBoundAskId !== null && batchBoundAskId !== ask.askId);
102
107
  let gateMatch;
103
- if (checkpoints && !irreversibleBatch) {
108
+ if (checkpoints && !irreversibleBatch && !expired()) {
104
109
  const candidates = await withDeadline(checkpoints.findCheckpointCandidatesForAsk(encodeCheckpointScope(ask.owner), ask.sessionId, ask.toolCallId, ask.createdAtMs), "findCheckpointCandidatesForAsk");
105
110
  gateMatch = classifyGateMatch(ask, candidates);
106
111
  if (gateMatch.kind === "hash_mismatch") {
107
- try {
108
- logger.warn("approval_reconcile_hash_mismatch", {
109
- askId: ask.askId,
110
- taskId: ask.taskId,
111
- toolCallId: ask.toolCallId,
112
- candidates: gateMatch.candidates,
113
- reason: "ask and checkpoint minted the same bound input under the same (sourceTaskId, toolCallId) identity but the digests differ — " +
114
- "the known causes are deployment-side (a hook/policy mutating an object it still holds a reference to between the two mints, " +
115
- "or a stateful getter returning a fresh value per read), not out-of-band interference; this ask keeps its fail-safe route (no park, no denial)",
116
- });
117
- }
118
- catch {
119
- }
112
+ warn("approval_reconcile_hash_mismatch", {
113
+ askId: ask.askId,
114
+ taskId: ask.taskId,
115
+ toolCallId: ask.toolCallId,
116
+ candidates: gateMatch.candidates,
117
+ reason: "ask and checkpoint minted the same bound input under the same (sourceTaskId, toolCallId) identity but the digests differ — " +
118
+ "the known causes are deployment-side (a hook/policy mutating an object it still holds a reference to between the two mints, " +
119
+ "or a stateful getter returning a fresh value per read), not out-of-band interference; this ask keeps its fail-safe route (no park, no denial)",
120
+ });
120
121
  }
121
122
  const match = gateMatch.kind === "match" ? gateMatch.candidate : undefined;
122
- if (match) {
123
+ if (match && !expired()) {
123
124
  const res = await withDeadline(askStore.bindBatch(ask.batchId, ask.askId, {
124
125
  gateToken: match.token,
125
126
  gateBoundCallId: match.boundCallId,
@@ -127,12 +128,20 @@ export function createApprovalReconciler(deps) {
127
128
  }), "bindBatch");
128
129
  if (res.ok) {
129
130
  revoke(ask, res.voidedSiblings, "superseded_by_park", nowMs);
130
- return { outcome: "parked", match: gateMatch };
131
+ return { bound: true, match: gateMatch };
131
132
  }
132
133
  batchState = res.batchState;
133
134
  batchBoundAskId = res.boundAskId ?? null;
134
135
  }
135
136
  }
137
+ return { bound: false, ...(gateMatch !== undefined ? { match: gateMatch } : {}), batchState, batchBoundAskId, irreversibleBatch };
138
+ };
139
+ const reconcileOne = async (ask, nowMs) => {
140
+ const attempt = await bindAtGate(ask, nowMs);
141
+ if (attempt.bound)
142
+ return { outcome: "parked", match: attempt.match };
143
+ const { batchState, batchBoundAskId, irreversibleBatch } = attempt;
144
+ const gateMatch = attempt.match;
136
145
  const run = runs && !irreversibleBatch ? ((await withDeadline(runs.getRun(ask.taskId), "getRun")) ?? null) : null;
137
146
  const action = decideReconcileAction({ ask, candidates: [], run, batchState, batchBoundAskId, nowMs, adhocGraceMs, orphanTtlMs, allowBind: false });
138
147
  switch (action.kind) {
@@ -149,7 +158,7 @@ export function createApprovalReconciler(deps) {
149
158
  if (!won)
150
159
  return { outcome: "held", match: gateMatch };
151
160
  if (action.reason === VOID_REASONS.ORPHAN_TTL_EXCEEDED) {
152
- logger.warn("approval_ask_orphan_voided", { askId: ask.askId, taskId: ask.taskId, ageMs: nowMs - ask.createdAtMs, ttlMs: orphanTtlMs });
161
+ warn("approval_ask_orphan_voided", { askId: ask.askId, taskId: ask.taskId, ageMs: nowMs - ask.createdAtMs, ttlMs: orphanTtlMs });
153
162
  }
154
163
  revoke(ask, [ask.askId], action.reason === VOID_REASONS.BATCH_BOUND_ELSEWHERE ? "superseded_by_park" : "aborted", nowMs);
155
164
  return { outcome: "voided", match: gateMatch };
@@ -160,6 +169,37 @@ export function createApprovalReconciler(deps) {
160
169
  }
161
170
  }
162
171
  };
172
+ const parkCommits = deps.parkCommits;
173
+ const budgetMs = deps.parkCommitBindBudgetMs ?? PARK_COMMIT_BIND_BUDGET_MS;
174
+ const bindCommittedPark = async (calls) => {
175
+ const deadlineMs = Date.now() + budgetMs;
176
+ const work = (async () => {
177
+ const rows = await askStore.listParkingBySession(calls.sessionId);
178
+ for (const ask of rows) {
179
+ if (!calls.toolCallIds.includes(ask.toolCallId))
180
+ continue;
181
+ if (Date.now() >= deadlineMs) {
182
+ warn("approval_park_commit_bind_failed", { askId: ask.askId, taskId: ask.taskId, err: `park-commit bind budget (${budgetMs}ms) spent before this row`, fallback: "periodic_sweep" });
183
+ return;
184
+ }
185
+ try {
186
+ if ((await bindAtGate(ask, Date.now(), deadlineMs)).bound)
187
+ metrics?.inc("approval_reconciled_total", { outcome: "parked" }, 1);
188
+ }
189
+ catch (err) {
190
+ warn("approval_park_commit_bind_failed", { askId: ask.askId, taskId: ask.taskId, err: err instanceof Error ? err.message : String(err), fallback: "periodic_sweep" });
191
+ }
192
+ }
193
+ })();
194
+ parkCommits?.track(work);
195
+ try {
196
+ await withDeadline(work, "bindCommittedPark", budgetMs);
197
+ }
198
+ catch (err) {
199
+ warn("approval_park_commit_bind_failed", { sessionId: calls.sessionId, err: err instanceof Error ? err.message : String(err), fallback: "periodic_sweep" });
200
+ }
201
+ };
202
+ parkCommits?.onParkCommitted(bindCommittedPark);
163
203
  return {
164
204
  async runOnce(nowMs) {
165
205
  const stats = {
@@ -184,11 +224,7 @@ export function createApprovalReconciler(deps) {
184
224
  for (const ask of parking) {
185
225
  if (Date.now() >= parkingDeadline) {
186
226
  stats.budgetExhausted += 1;
187
- try {
188
- logger.warn("approval_reconcile_budget_exhausted", { segment: "PARKING", processed: stats.parked + stats.denied + stats.voided + stats.held + stats.failed, scanned: stats.scanned, budgetMs: RECONCILE_SEGMENT_BUDGET_MS });
189
- }
190
- catch {
191
- }
227
+ warn("approval_reconcile_budget_exhausted", { segment: "PARKING", processed: stats.parked + stats.denied + stats.voided + stats.held + stats.failed, scanned: stats.scanned, budgetMs: RECONCILE_SEGMENT_BUDGET_MS });
192
228
  break;
193
229
  }
194
230
  if (ask.boundInputHash === null)
@@ -224,11 +260,7 @@ export function createApprovalReconciler(deps) {
224
260
  }
225
261
  catch (err) {
226
262
  stats.failed += 1;
227
- try {
228
- logger.warn("approval_reconcile_row_failed", { askId: ask.askId, taskId: ask.taskId, err: err instanceof Error ? err.message : String(err) });
229
- }
230
- catch {
231
- }
263
+ warn("approval_reconcile_row_failed", { askId: ask.askId, taskId: ask.taskId, err: err instanceof Error ? err.message : String(err) });
232
264
  }
233
265
  }
234
266
  const pending = await withDeadline(askStore.listByState("STREAM_PENDING", batchLimit), "listByState(STREAM_PENDING)");
@@ -236,11 +268,7 @@ export function createApprovalReconciler(deps) {
236
268
  for (const row of pending) {
237
269
  if (Date.now() >= orphanDeadline) {
238
270
  stats.budgetExhausted += 1;
239
- try {
240
- logger.warn("approval_reconcile_budget_exhausted", { segment: "STREAM_PENDING", expired: stats.orphansExpired, scanned: pending.length, budgetMs: RECONCILE_SEGMENT_BUDGET_MS });
241
- }
242
- catch {
243
- }
271
+ warn("approval_reconcile_budget_exhausted", { segment: "STREAM_PENDING", expired: stats.orphansExpired, scanned: pending.length, budgetMs: RECONCILE_SEGMENT_BUDGET_MS });
244
272
  break;
245
273
  }
246
274
  if (row.expiresAtMs + pendingGraceMs >= nowMs)
@@ -254,23 +282,15 @@ export function createApprovalReconciler(deps) {
254
282
  }
255
283
  catch (err) {
256
284
  stats.failed += 1;
257
- try {
258
- logger.warn("approval_orphan_expire_failed", { askId: row.askId, taskId: row.taskId, policy: unattendedPolicy, err: err instanceof Error ? err.message : String(err) });
259
- }
260
- catch {
261
- }
285
+ warn("approval_orphan_expire_failed", { askId: row.askId, taskId: row.taskId, policy: unattendedPolicy, err: err instanceof Error ? err.message : String(err) });
262
286
  }
263
287
  }
264
288
  if (stats.unmatchableNoHash > 0) {
265
- try {
266
- logger.warn("approval_reconcile_rows_unmatchable", {
267
- rows: stats.unmatchableNoHash,
268
- scanned: stats.scanned,
269
- reason: "bound_input_hash is NULL — criterion 1 (identity ∧ hash) cannot match; rows fall through to the run-terminal / adhoc / orphan arms",
270
- });
271
- }
272
- catch {
273
- }
289
+ warn("approval_reconcile_rows_unmatchable", {
290
+ rows: stats.unmatchableNoHash,
291
+ scanned: stats.scanned,
292
+ reason: "bound_input_hash is NULL — criterion 1 (identity ∧ hash) cannot match; rows fall through to the run-terminal / adhoc / orphan arms",
293
+ });
274
294
  }
275
295
  if (stats.parked + stats.denied + stats.voided + stats.orphansExpired > 0) {
276
296
  metrics?.inc("approval_reconciled_total", { outcome: "parked" }, stats.parked);
package/dist/audit.d.ts CHANGED
@@ -48,7 +48,7 @@ export declare function buildSessionAuditLocal(root: string, sessionId: string):
48
48
  * 🔴 **会话视图的读面口 —— `GET /v1/sessions/:id` 的全部取法(整读 / `?tail` / `?before` / `?blobCap` / `?message=N`)共用这一只**。
49
49
  * 次序 = **选 → 脱 → 截**(路由 `http/routes/sessions.ts`):整条消息的选窗不切字符串,先选窗不改任何一条的脱敏读数、只把脱敏的量降到选中的那几条;
50
50
  * `blobCap` 的截断会切字符串(预览切在密钥中间,半截密钥认不出来)⇒ 截断恒在脱敏之后。
51
- * `messages` 缺席 ⇒ 不铸键;非数组 ⇒ 深扫;其余键(`owner` / `promptEpoch` / `model` …)逐字。字节没变 ⇒ **同一引用**。
51
+ * `messages` 缺席 ⇒ 不铸键;非数组 ⇒ 深扫;其余键(`owner` / `promptEpoch` / `model` …)逐字。每条消息都回同一引用 ⇒ **同一引用**(带结构叶的消息恒重建,见 {@link readByTable})。
52
52
  */
53
53
  export declare function redactSessionViewForRead<V extends object>(view: V): V;
54
54
  /** Window `messages` — `tail=N` (last N) or `before=<offset>&limit=N` ([max(0,before-N), before)).
package/dist/audit.js CHANGED
@@ -45,48 +45,78 @@ async function auditFromStorage(storage, sessionId) {
45
45
  }
46
46
  const deepScrub = (v) => redactDeepWith(v, redactModelText);
47
47
  const textSlot = (v) => (typeof v === "string" ? redactModelText(v) : deepScrub(v));
48
- function scrubSlot(p, key, scan) {
49
- const v = p[key];
50
- if (v === undefined)
51
- return p;
52
- const next = scan(v);
53
- return next === v ? p : { ...p, [key]: next };
48
+ function readLeaf(treatment, v) {
49
+ switch (treatment) {
50
+ case "text": return textSlot(v);
51
+ case "structure": return deepScrub(v);
52
+ case "parts": return readParts(v);
53
+ case "verbatim": return v;
54
+ default: {
55
+ const unhandled = treatment;
56
+ throw new Error(`unhandled leaf treatment: ${String(unhandled)}`);
57
+ }
58
+ }
59
+ }
60
+ const isPlainRecord = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
61
+ function readByTable(node, table) {
62
+ let changed = false;
63
+ const entries = Object.entries(node).map(([key, v]) => {
64
+ const next = readLeaf(Object.hasOwn(table, key) ? table[key] : "structure", v);
65
+ if (next !== v)
66
+ changed = true;
67
+ return [key, next];
68
+ });
69
+ return changed ? Object.fromEntries(entries) : node;
54
70
  }
55
71
  const PART_READ_TREATMENT = {
56
- text: (p) => scrubSlot(p, "text", textSlot),
57
- thinking: (p) => scrubSlot(p, "thinking", textSlot),
58
- toolCall: (p) => scrubSlot(p, "arguments", deepScrub),
59
- image: (p) => p,
60
- document: (p) => p,
72
+ text: { type: "verbatim", text: "text" },
73
+ thinking: { type: "verbatim", thinking: "text", thinkingSignature: "verbatim", redacted: "verbatim" },
74
+ toolCall: { type: "verbatim", id: "verbatim", wireId: "verbatim", name: "verbatim", arguments: "structure", executionMode: "verbatim" },
75
+ image: { type: "verbatim", data: "verbatim", mimeType: "verbatim" },
76
+ document: { type: "verbatim", data: "verbatim", mimeType: "verbatim" },
61
77
  };
62
78
  function readParts(content) {
63
79
  if (!Array.isArray(content))
64
80
  return textSlot(content);
65
81
  const next = content.map((part) => {
66
- if (part !== null && typeof part === "object" && !Array.isArray(part)) {
82
+ if (isPlainRecord(part)) {
67
83
  const type = part.type;
68
84
  if (typeof type === "string" && Object.hasOwn(PART_READ_TREATMENT, type)) {
69
- return PART_READ_TREATMENT[type](part);
85
+ return readByTable(part, PART_READ_TREATMENT[type]);
70
86
  }
71
87
  }
72
88
  return deepScrub(part);
73
89
  });
74
90
  return next.every((x, i) => x === content[i]) ? content : next;
75
91
  }
76
- const readContent = (m) => scrubSlot(m, "content", readParts);
77
92
  const MESSAGE_READ_TREATMENT = {
78
- user: (m) => m,
79
- custom: (m) => m,
80
- assistant: (m) => readContent(m),
81
- toolResult: (m) => scrubSlot(readContent(m), "details", deepScrub),
82
- bashExecution: (m) => scrubSlot(scrubSlot(m, "command", textSlot), "output", textSlot),
83
- compactionSummary: (m) => scrubSlot(m, "summary", textSlot),
93
+ user: {
94
+ role: "verbatim", content: "verbatim", timestamp: "verbatim", enginePrefixChars: "verbatim", engineSegments: "verbatim",
95
+ engineMinted: "verbatim", engineNoteKey: "verbatim", provenance: "verbatim", actor: "verbatim",
96
+ },
97
+ custom: { role: "verbatim", customType: "verbatim", content: "verbatim", display: "verbatim", details: "verbatim", provenance: "verbatim", timestamp: "verbatim" },
98
+ assistant: {
99
+ role: "verbatim", content: "parts", errorMessage: "text", diagnostics: "structure", repetition: "structure",
100
+ api: "verbatim", provider: "verbatim", model: "verbatim", responseModel: "verbatim", responseId: "verbatim", requestId: "verbatim",
101
+ isApiErrorMessage: "verbatim", apiErrorStatus: "verbatim", apiErrorCode: "verbatim", usage: "verbatim", usageMissing: "verbatim",
102
+ stopReason: "verbatim", errorKind: "verbatim", inputTooLongRuledOut: "verbatim", partialFinalized: "verbatim",
103
+ staticReasoningCut: "verbatim", timestamp: "verbatim",
104
+ },
105
+ toolResult: { role: "verbatim", toolCallId: "verbatim", toolName: "verbatim", content: "parts", details: "structure", isError: "verbatim", timestamp: "verbatim" },
106
+ bashExecution: {
107
+ role: "verbatim", command: "text", output: "text", exitCode: "verbatim", cancelled: "verbatim", truncated: "verbatim",
108
+ fullOutputPath: "verbatim", timestamp: "verbatim", excludeFromContext: "verbatim",
109
+ },
110
+ compactionSummary: {
111
+ role: "verbatim", summary: "text", details: "structure", tokensBefore: "verbatim", timestamp: "verbatim", elidedMessages: "verbatim",
112
+ unsummarizedMessages: "verbatim", tokensAfter: "verbatim", firstKeptEntryId: "verbatim",
113
+ },
84
114
  };
85
115
  function readMessage(m) {
86
- if (m !== null && typeof m === "object" && !Array.isArray(m)) {
116
+ if (isPlainRecord(m)) {
87
117
  const role = m.role;
88
118
  if (typeof role === "string" && Object.hasOwn(MESSAGE_READ_TREATMENT, role)) {
89
- return MESSAGE_READ_TREATMENT[role](m);
119
+ return readByTable(m, MESSAGE_READ_TREATMENT[role]);
90
120
  }
91
121
  }
92
122
  return deepScrub(m);
@@ -15,7 +15,9 @@
15
15
  *
16
16
  * 本模块按**server 侧可知的同三项**算一份「若此刻提交会不会武装」的裁决,供壳(cli `sema doctor permissions`)
17
17
  * 与人自查。三项分别对应三个未武装原因(判序 = 谓词的合取序,先判到的先报):
18
- * · `mode_not_auto` —— 本次意图不是 auto(`?permissionMode=` 查询串给的;缺席 = 按 auto 意图答);
18
+ * · `mode_not_auto` —— 本次意图不是 auto(`?permissionMode=` 查询串给的;缺席 = 按 auto 意图答),且原样到得了引擎(本部署的入口地板没改它);
19
+ * · `operator_floor` —— 本部署的运维入口地板(`AUTONOMY=plan`)把本次意图改成了别的词、那个词不是 auto(`runtime-governance.ts`
20
+ * `entryPermissionMode` 的 `clamped`,与受理门同一只函数)—— 到得了引擎的词由运维定,出路在运维换档,不在请求的词;
19
21
  * · `deployment_incapable` —— 装配面没挂 `RunnerDeps.autoMode` 分类器席位(生产装配恒挂;桩/一次性 CLI 不挂);
20
22
  * · `org_denied` —— per-principal caps 的 `autoMode === false` 且本地 deny 未设(center 显式 deny、center
21
23
  * 硬失败的 fail-closed 拒项);
@@ -34,7 +36,8 @@
34
36
  * 规则集少一整支;**不再读任何 dist 文本**。
35
37
  *
36
38
  * ## 闭集纪律([ref])
37
- * 六词是 core [ref] seam ⑤ + [ref] PM 裁定的闭集;`AUTO_MODE_UNARMED_REASON_SEMANTICS` 是 `Record<Reason, string>` 锁步表——
39
+ * 七词:core [ref] seam ⑤ + [ref] PM 裁定的六词 + `operator_floor`(意图被运维入口地板改词;与 `local_denied` / `org_denied` 同理按「指路不同」拆出);
40
+ * `AUTO_MODE_UNARMED_REASON_SEMANTICS` 是 `Record<Reason, string>` 锁步表——
38
41
  * 词表加员而语义表不加、或反之,都是编译红。本面**不**给壳发 prose(机器码上 wire,人话归壳),语义表只
39
42
  * 服务于文档/测试对拍。
40
43
  *
@@ -75,12 +78,13 @@ export type ClassifierRoute = {
75
78
  readonly id: undefined;
76
79
  readonly fault: string;
77
80
  };
78
- /** core [ref] seam ⑤ 的未武装原因闭集(六词,[ref] 加 settings_denied;顺序 = 判序)。 */
79
- export declare const AUTO_MODE_UNARMED_REASONS: readonly ["mode_not_auto", "deployment_incapable", "org_denied", "local_denied", "settings_denied", "resolver_fault"];
81
+ /** 未武装原因闭集(七词:core [ref] seam ⑤ + [ref] settings_denied + 入口地板改词 operator_floor;顺序 = 判序)。 */
82
+ export declare const AUTO_MODE_UNARMED_REASONS: readonly ["mode_not_auto", "operator_floor", "deployment_incapable", "org_denied", "local_denied", "settings_denied", "resolver_fault"];
80
83
  export type AutoModeUnarmedReason = (typeof AUTO_MODE_UNARMED_REASONS)[number];
81
84
  /** 锁步表:词表与语义一一对应(加员/删员任一侧不同步 = tsc 红)。语义只供文档/测试,不上 wire。 */
82
85
  export declare const AUTO_MODE_UNARMED_REASON_SEMANTICS: {
83
- readonly mode_not_auto: "本次意图不是 auto(壳发的 permissionMode 不是 \"auto\")";
86
+ readonly mode_not_auto: "本次意图不是 auto(壳发的 permissionMode 不是 \"auto\"),且原样到得了引擎(本部署的运维入口地板没改它)—— 出路是请求 auto";
87
+ readonly operator_floor: "本部署的运维入口地板(AUTONOMY=plan)把本次意图改成了别的词、那个词不是 auto —— 与受理门同一只函数,提交时到得了引擎的词由运维定;出路是找运维换档,改请求的词到不了 auto";
84
88
  readonly deployment_incapable: "装配面没挂 RunnerDeps.autoMode 分类器席位(本部署/本进程形不具备分类器)";
85
89
  readonly org_denied: "per-principal caps.autoMode === false 且本地 deny 未设:center 显式 deny / center 硬失败 fail-closed 拒项";
86
90
  readonly local_denied: "本地 catalog 键 PERMISSIONS_DISABLE_AUTO_MODE=true(center/settings 键名 permissions.disableAutoMode)把 caps.autoMode 折成 false";
@@ -118,11 +122,18 @@ export type AutoModeVerdict = {
118
122
  * 三项合取的 server 侧裁决。`requestedMode` 缺席 = 按 auto 意图答(「若请求会不会武装」);`principal` =
119
123
  * 调用方经门验证的身份(匿名 ⇒ undefined ⇒ center 客户端答 undefined,本地 deny 层照样罩)。
120
124
  *
125
+ * 意图那一项判的是**提交之后到得了引擎的那个词**:本次意图先过本部署的运维入口地板(`floor`,调用方传 `runtime-governance.ts`
126
+ * `autonomyModeFloor(<活 config 的 AUTONOMY>)` —— 与受理门盖章读同一只属主),经 `entryPermissionMode`(受理门盖章调的同一只函数)。
127
+ * 到得了引擎的词不是 auto ⇒ 未武装,原因按「谁定了那个词」分:地板改了词(`clamped`)⇒ `operator_floor`;词原样 ⇒ `mode_not_auto`。
128
+ * `AUTONOMY=plan` 部署上意图缺席 / auto / 任何比 plan 松的词 ⇒ `operator_floor`;意图本身是 `plan` ⇒ `mode_not_auto`。
129
+ *
121
130
  * 🔴 resolver 抛错**不吞**:合成腿的每一层都自带 fail-closed/缺席臂(center 客户端 catch-all、grant 腿 onFault),
122
131
  * 走到这里还在抛 = 装配缺陷,读面 500 比一句编出来的 reason 诚实。
123
132
  */
124
133
  export declare function judgeAutoModeArming(face: AutoModeFaceDeps | undefined, input: {
125
134
  readonly requestedMode: PermissionMode | undefined;
135
+ /** 本部署此刻的运维入口地板(`autonomyModeFloor(config.autonomy)`;`undefined` = 没有地板)。必填:不传地板不是「没有地板」的合法拼法。 */
136
+ readonly floor: PermissionMode | undefined;
126
137
  readonly principal: string | undefined;
127
138
  /** 本地 `PERMISSIONS_DISABLE_AUTO_MODE`(= catalog/settings 键 `permissions.disableAutoMode`)是否置 true —— caps false 时分词
128
139
  * `local_denied` / `org_denied`(seam ⑤;两者对壳的指路不同:改本机 env vs 找组织)。 */
@@ -1,5 +1,6 @@
1
1
  import { classifierSeatModel } from "@sema-agent/core";
2
2
  import { resolveMainModel } from "./model-select.js";
3
+ import { entryPermissionMode } from "./runtime-governance.js";
3
4
  export function classifierRouteFor(deps) {
4
5
  try {
5
6
  const main = resolveMainModel(deps);
@@ -9,9 +10,10 @@ export function classifierRouteFor(deps) {
9
10
  return { id: undefined, fault: err instanceof Error ? err.message : String(err) };
10
11
  }
11
12
  }
12
- export const AUTO_MODE_UNARMED_REASONS = ["mode_not_auto", "deployment_incapable", "org_denied", "local_denied", "settings_denied", "resolver_fault"];
13
+ export const AUTO_MODE_UNARMED_REASONS = ["mode_not_auto", "operator_floor", "deployment_incapable", "org_denied", "local_denied", "settings_denied", "resolver_fault"];
13
14
  export const AUTO_MODE_UNARMED_REASON_SEMANTICS = {
14
- mode_not_auto: "本次意图不是 auto(壳发的 permissionMode 不是 \"auto\")",
15
+ mode_not_auto: "本次意图不是 auto(壳发的 permissionMode 不是 \"auto\"),且原样到得了引擎(本部署的运维入口地板没改它)—— 出路是请求 auto",
16
+ operator_floor: "本部署的运维入口地板(AUTONOMY=plan)把本次意图改成了别的词、那个词不是 auto —— 与受理门同一只函数,提交时到得了引擎的词由运维定;出路是找运维换档,改请求的词到不了 auto",
15
17
  deployment_incapable: "装配面没挂 RunnerDeps.autoMode 分类器席位(本部署/本进程形不具备分类器)",
16
18
  org_denied: "per-principal caps.autoMode === false 且本地 deny 未设:center 显式 deny / center 硬失败 fail-closed 拒项",
17
19
  local_denied: "本地 catalog 键 PERMISSIONS_DISABLE_AUTO_MODE=true(center/settings 键名 permissions.disableAutoMode)把 caps.autoMode 折成 false",
@@ -21,8 +23,9 @@ export const AUTO_MODE_UNARMED_REASON_SEMANTICS = {
21
23
  export async function judgeAutoModeArming(face, input) {
22
24
  const route = face?.seatMounted ? face.classifierModel?.() : undefined;
23
25
  const model = route !== undefined ? { model: route } : {};
24
- if (input.requestedMode !== undefined && input.requestedMode !== "auto")
25
- return { armed: false, reason: "mode_not_auto", ...model };
26
+ const entry = entryPermissionMode(input.requestedMode ?? "auto", input.floor);
27
+ if (entry.effective !== "auto")
28
+ return { armed: false, reason: entry.clamped ? "operator_floor" : "mode_not_auto", ...model };
26
29
  if (face === undefined || !face.seatMounted)
27
30
  return { armed: false, reason: "deployment_incapable" };
28
31
  let caps;
@@ -370,6 +370,7 @@ async function runLeaderForTrap(rt, brain, models, roles, pricing, trap, cellId,
370
370
  process.env.LEADER_WORKER_BUDGET_USD = String((rt.maxCostUsd ?? 1.0) / 2);
371
371
  const run = createLeaderRunner({
372
372
  brain, models, roles, pricing, e2bApiKey: rt.e2bApiKey, workspace: W,
373
+ autonomy: () => undefined,
373
374
  ...(s1DebugGit() ? { logger: { warn: (m, x) => console.error(`[S1_DEBUG_LEADER warn] ${m}`, x ? JSON.stringify(x).slice(0, 300) : ""), info: (m, x) => console.error(`[S1_DEBUG_LEADER info] ${m}`, x ? JSON.stringify(x).slice(0, 200) : "") } } : {}),
374
375
  });
375
376
  const teamSeedWrite = (trap.seedFiles ?? [])
@@ -133,6 +133,7 @@ export async function createConfigCenterRuntime(ctx) {
133
133
  logger.warn("config_entry_dropped", {
134
134
  domain: d.domain,
135
135
  entry: d.entry,
136
+ reasonCode: d.reasonCode,
136
137
  error: d.error,
137
138
  version: r.effective.version,
138
139
  phase,
@@ -419,6 +420,8 @@ export async function createConfigCenterRuntime(ctx) {
419
420
  logger.warn("config_lkg_unusable", { path: lkgPath, reason: "center credential identity changed while the LKG was being read — not used", note: "env fallback serves; the current identity's next pull re-establishes its own LKG" });
420
421
  return null;
421
422
  }
423
+ if (read.migrated !== undefined)
424
+ logger.info("config_lkg_migrated", { to: lkgPath, note: read.migrated });
422
425
  logger.info("config_loaded_lkg", { path: lkgPath, savedAt: read.lkg.savedAt, why, scope, note: lkgReadNote(scope) });
423
426
  const lkgEffective = read.lkg.effective;
424
427
  candidateIdentity.set(lkgEffective, identity);
@@ -51,6 +51,7 @@ export function createLeaderFace(ctx) {
51
51
  retentionPolicy: governanceSeams.retentionPolicy,
52
52
  },
53
53
  deploymentPosture: () => buildDeploymentPostureSeats(config),
54
+ autonomy: () => config.autonomy,
54
55
  ...(ctx.tracer ? { tracer: ctx.tracer } : {}),
55
56
  ...(config.usageWindows && ctx.usageWindowStore
56
57
  ? { usageWindows: config.usageWindows, usageWindowStore: ctx.usageWindowStore }
@@ -24,6 +24,7 @@ import type { TaskAttachmentStore } from "../plugins/task-attachment-store.js";
24
24
  import type { FileHistoryErasureFace } from "../file-history-capture.js";
25
25
  import type { ApprovalNonceStore, CheckpointStoreFull, CostQuotaStore, ImageBake, RateLimiterStore, ServiceWorkflowJournalStore, StoreBackend, ToolResultStoreFull } from "../plugins/store-backend.js";
26
26
  import type { ToolApprovalCoordinator } from "../tool-approval.js";
27
+ import type { ParkCommitWatch } from "../plugins/park-commit-watch.js";
27
28
  /** [ref] / [ref]§五(sema-comms `audits/failopen-governance-176.md` §7 "Z 类 1 条"):default consecutive-failure
28
29
  * count before a reaper leg's swallowed error gets a `warn` line. The ~15 `.catch(() => undefined)` sites
29
30
  * below are correct in DIRECTION (a hiccuping periodic sweep must never crash the whole tick loop — one bad
@@ -68,6 +69,8 @@ export interface ReapersCtx {
68
69
  subRunner: Runner;
69
70
  runStore: ReturnType<StoreBackend["run"]> | undefined;
70
71
  checkpointStore: CheckpointStoreFull | undefined;
72
+ /** S-1280:checkpoint 店的 park 提交观察层(stage-06 与店同一处铸;在场 ⇔ 有 checkpoint 店)。对账收敛器订阅它,在停车行提交那一拍收口 PARKING。 */
73
+ parkCommits: ParkCommitWatch | undefined;
71
74
  rateLimiter: RateLimiter | RateLimiterStore | undefined;
72
75
  costQuota: CostQuota | CostQuotaStore | undefined;
73
76
  toolResultStore: ToolResultStoreFull | undefined;
@@ -56,7 +56,7 @@ export function createPlacementAwareReapSessions(deps) {
56
56
  };
57
57
  }
58
58
  export function startReapers(ctx) {
59
- const { config, logger, metrics, localRoot, backend, subRunner, runStore, checkpointStore, rateLimiter, costQuota, toolResultStore, fileHistoryErasure, taskAttachmentStore, imageBakes, worktreeReap, workflowNotifyGate, workflowRecoverOpts, workflowJournalStore, sqlWorkflowRunStore, workflowNotifyJournal, rosterStore, backgroundAgentStore, mailboxStore, toolApproval, permissionRuleStores, approvalNonceStore, getRunDenySweep, userInputLedger, } = ctx;
59
+ const { config, logger, metrics, localRoot, backend, subRunner, runStore, checkpointStore, parkCommits, rateLimiter, costQuota, toolResultStore, fileHistoryErasure, taskAttachmentStore, imageBakes, worktreeReap, workflowNotifyGate, workflowRecoverOpts, workflowJournalStore, sqlWorkflowRunStore, workflowNotifyJournal, rosterStore, backgroundAgentStore, mailboxStore, toolApproval, permissionRuleStores, approvalNonceStore, getRunDenySweep, userInputLedger, } = ctx;
60
60
  const leg = (p) => {
61
61
  if (p !== undefined)
62
62
  trackMaintenanceLeg(p);
@@ -122,6 +122,7 @@ export function startReapers(ctx) {
122
122
  orphanTtlMs: config.streamApproval.orphanTtlMs,
123
123
  unattendedPolicy: config.unattendedApprovalPolicy,
124
124
  ...(toolApproval ? { emitRevoke: (frame, target) => void toolApproval.emitRevoke(frame, target) } : {}),
125
+ parkCommits,
125
126
  });
126
127
  })();
127
128
  const reapSessions = createPlacementAwareReapSessions({ host: backend?.session(), routed: subRunner.sessions });
@@ -26,7 +26,7 @@ import { resourceSuspendOptIn } from "../resource-suspend.js";
26
26
  import { isIsolatedExecEnv, routeServiceTask, supPostureOverrides } from "../router/route-orchestration.js";
27
27
  import { tightenGoverned } from "../governance-overrides.js";
28
28
  import { gateExecutionLane } from "../runtime-caps-resolver.js";
29
- import { applyRuntimeGovernance, stripDelegationTools } from "../runtime-governance.js";
29
+ import { applyRuntimeGovernance } from "../runtime-governance.js";
30
30
  import { HttpError, createCodedHttpError, encodeCheckpointScope } from "../security.js";
31
31
  import { memoryProjectKeyFor, memorySpecForRequest } from "../memory-scope.js";
32
32
  import { inheritEnvFieldIssue, singleMemberOffFieldIssue } from "../request-value-closure.js";
@@ -599,9 +599,6 @@ export function createResolveSpec(ctx) {
599
599
  throw new HttpError(422, `request settings cannot be applied as written: ${e.message}`);
600
600
  }
601
601
  }
602
- if (governed.handsReadOnly === true && Array.isArray(governed.tools)) {
603
- governed = { ...governed, tools: stripDelegationTools(governed.tools) };
604
- }
605
602
  if (shouldDiscloseNoPersistentMemory({
606
603
  memoryFaceMounted: memoryFaceMounted(governed.memory),
607
604
  declaredCapable: effectiveMemoryPersistenceCapable(config),
@@ -8,7 +8,7 @@ import { installProcessExitBoundary } from "./exit-boundary.js";
8
8
  import { createEngineLeaseTracker } from "./engine-lease.js";
9
9
  import { writeExitLast, readExitLast, clearExitLast } from "./exit-last.js";
10
10
  import { isScriptRealmRejection, describeRejectionReason } from "../orchestration/hardened-vm-runner.js";
11
- import { DRAIN_CAUSE_COUNTS_AS_FIRST_STOP_SIGNAL, HARD_STOP_ACTION_ID } from "../fleet/drain-entry.js";
11
+ import { DRAIN_CAUSE_COUNTS_AS_FIRST_STOP_SIGNAL, HARD_STOP_ACTION_ID, drainDeadlineBeyondBound } from "../fleet/drain-entry.js";
12
12
  const HARD_STOP_GRACE_MS = 10_000;
13
13
  const FINAL_EXIT_MS = 15_000;
14
14
  export function backgroundGraceRemainingMs(a) {
@@ -140,6 +140,7 @@ export function installShutdownHandlers(ctx) {
140
140
  });
141
141
  const finalExit = setTimeout(() => { logger.warn("hard_shutdown_final_deadline", { afterMs: FINAL_EXIT_MS }); process.exit(0); }, FINAL_EXIT_MS);
142
142
  finalExit.unref?.();
143
+ drainState.endSubscriptions?.();
143
144
  server.closeIdleConnections?.();
144
145
  const forceClose = setTimeout(() => server.closeAllConnections?.(), HARD_STOP_GRACE_MS);
145
146
  forceClose.unref?.();
@@ -154,6 +155,8 @@ export function installShutdownHandlers(ctx) {
154
155
  return { ...current, state: "already_draining" };
155
156
  if (closing)
156
157
  return { actionId: HARD_STOP_ACTION_ID, since: closingAt, deadlineAt: closingAt + FINAL_EXIT_MS, state: "already_draining" };
158
+ if (entry.deadlineBoundMs !== undefined && drainDeadlineBeyondBound(entry.deadlineAt, at, entry.deadlineBoundMs))
159
+ return { state: "deadline_beyond_bound", boundMs: entry.deadlineBoundMs };
157
160
  draining = true;
158
161
  drainStartedAt = performance.now();
159
162
  firstStopSignalAt = bySignal ? drainStartedAt : undefined;
@@ -167,6 +170,7 @@ export function installShutdownHandlers(ctx) {
167
170
  const deadlineMono = drainStartedAt + windowMs;
168
171
  deviceHub?.beginDrain({ graceMs: () => windowMs });
169
172
  drainState.yieldInflight?.();
173
+ drainState.endSubscriptions?.();
170
174
  toolApproval?.beginDrain();
171
175
  void fleetClient?.announceNow();
172
176
  const inflight = drainState.inflight?.() ?? 0;
@@ -234,6 +238,7 @@ export function installShutdownHandlers(ctx) {
234
238
  process.on("SIGINT", onSigint);
235
239
  const onSighup = createSighupIdleHandler({
236
240
  inflight: () => drainState.inflight?.() ?? 0,
241
+ attached: () => drainState.attachedStreams?.() ?? 0,
237
242
  lastActivityAt: () => drainState.lastActivityAt?.() ?? 0,
238
243
  isStopped: () => closing || draining,
239
244
  escalate: () => drainOnCause("signal:SIGHUP"),
@@ -5,9 +5,9 @@ import { startRetentionLane, RETENTION_LEASE_TTL_FACTOR } from "./retention-lane
5
5
  import { createLimitSync } from "./limit-sync.js";
6
6
  import { runDenySweep } from "./late-binds.js";
7
7
  export function runStage08(ctx) {
8
- const { approvalNonceStore, backend, backgroundAgentStore, ceilingVerdict, checkpointStore, config, configCenter, consolidationSeatWatch, costQuota, degradeRouteWatch, fileHistoryErasure, handslessRunner, handslessSubRunner, hookAgentRunner, imageBakes, instanceId, localRoot, logger, mailboxStore, metrics, permissionRuleStores, pricing, rateLimiter, retentionExecutor, retentionLaneStore, rosterStore, runStore, runner, sqlWorkflowRunStore, subRunner, taskAttachmentStore, toolApproval, toolResultStore, userInputLedger, workflowJournalStore, workflowNotifyGate, workflowNotifyJournal, workflowRecoverOpts, worktreeReap } = ctx;
8
+ const { approvalNonceStore, backend, backgroundAgentStore, ceilingVerdict, checkpointStore, parkCommits, config, configCenter, consolidationSeatWatch, costQuota, degradeRouteWatch, fileHistoryErasure, handslessRunner, handslessSubRunner, hookAgentRunner, imageBakes, instanceId, localRoot, logger, mailboxStore, metrics, permissionRuleStores, pricing, rateLimiter, retentionExecutor, retentionLaneStore, rosterStore, runStore, runner, sqlWorkflowRunStore, subRunner, taskAttachmentStore, toolApproval, toolResultStore, userInputLedger, workflowJournalStore, workflowNotifyGate, workflowNotifyJournal, workflowRecoverOpts, worktreeReap } = ctx;
9
9
  const reaper = startReapers({
10
- config, logger, metrics, localRoot, backend, subRunner, runStore, checkpointStore,
10
+ config, logger, metrics, localRoot, backend, subRunner, runStore, checkpointStore, parkCommits,
11
11
  rateLimiter, costQuota, toolResultStore, fileHistoryErasure, taskAttachmentStore, imageBakes, worktreeReap,
12
12
  workflowNotifyGate, workflowRecoverOpts, workflowJournalStore, sqlWorkflowRunStore, workflowNotifyJournal, rosterStore,
13
13
  backgroundAgentStore, mailboxStore, toolApproval, permissionRuleStores, approvalNonceStore, userInputLedger,
@@ -67,7 +67,7 @@ export interface MemoryFaceFacts {
67
67
  /** 本请求命中场景的手带表态([ref] 判别位)。`none` = 该场景跑在不挂 `executionEnvFactory` 的 Runner 上,
68
68
  * core 的手带 band(Bash/Edit/Write/…)整批不 mount。 */
69
69
  hands: ScenarioHands;
70
- /** 折完 governance+settings 后的最终 `handsReadOnly`(plan 模式 / autonomy 收窄)。手带只剩读腿。 */
70
+ /** 折完 governance+settings 后的最终 `handsReadOnly`(`AUTONOMY=read-only` 收窄;plan 模式不点亮它 —— plan 的手带照挂、调用时由引擎问)。手带只剩读腿。 */
71
71
  handsReadOnly: boolean;
72
72
  /** 折完之后的最终 `TaskSpec.excludeAllTools`(S-493;core 7.8.0 [ref])。`true` = **整只**工具面卸空
73
73
  * (core `prepareDeferClassify` 对它直接 `roster.unmountWhere(() => true)`)⇒ 手带在场也一条落盘路都不剩。