@sema-agent/server 7.55.0 → 7.57.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 (129) hide show
  1. package/MIGRATION.md +3 -3
  2. package/README.md +14 -7
  3. package/README.zh-CN.md +10 -6
  4. package/USAGE.md +207 -59
  5. package/deploy/sema-up/chart/values.yaml +1 -1
  6. package/dist/approval-ask-audit-store.d.ts +100 -1
  7. package/dist/approval-ask-audit-store.js +103 -2
  8. package/dist/approval-card.d.ts +148 -13
  9. package/dist/approval-card.js +82 -13
  10. package/dist/approval-content-kind.d.ts +22 -0
  11. package/dist/approval-content-kind.js +5 -0
  12. package/dist/approval.d.ts +31 -0
  13. package/dist/approval.js +18 -0
  14. package/dist/auto-mode-face.d.ts +111 -0
  15. package/dist/auto-mode-face.js +99 -0
  16. package/dist/bench/s1/arms.js +5 -5
  17. package/dist/bench/s1/live-deps.js +11 -11
  18. package/dist/bench/s1/run-firm.js +3 -0
  19. package/dist/bench/s1/runner-ctx.d.ts +4 -0
  20. package/dist/bench/s1/runner-ctx.js +7 -0
  21. package/dist/boot/config-center.js +23 -1
  22. package/dist/boot/coordinators.js +13 -1
  23. package/dist/boot/leader.d.ts +21 -0
  24. package/dist/boot/leader.js +6 -0
  25. package/dist/boot/parked-revive-gate.d.ts +7 -2
  26. package/dist/boot/parked-revive-gate.js +12 -1
  27. package/dist/boot/resolve-spec.d.ts +3 -0
  28. package/dist/boot/resolve-spec.js +3 -1
  29. package/dist/boot/runner-deps.d.ts +12 -0
  30. package/dist/boot/runner-deps.js +28 -3
  31. package/dist/boot/runtime-caps.d.ts +19 -8
  32. package/dist/boot/runtime-caps.js +50 -20
  33. package/dist/boot/session-shell-gate-registry.d.ts +39 -0
  34. package/dist/boot/session-shell-gate-registry.js +21 -0
  35. package/dist/config-catalog.js +14 -5
  36. package/dist/config-center/types.d.ts +2 -1
  37. package/dist/config-provider.js +1 -0
  38. package/dist/config-types.d.ts +41 -10
  39. package/dist/config.d.ts +21 -0
  40. package/dist/config.js +40 -5
  41. package/dist/http/route-ctx.d.ts +90 -3
  42. package/dist/http/routes/a2a-serve.js +5 -3
  43. package/dist/http/routes/approvals-assistant.js +21 -8
  44. package/dist/http/routes/capabilities.js +21 -4
  45. package/dist/http/routes/leader.js +2 -2
  46. package/dist/http/routes/memory-origin.d.ts +2 -2
  47. package/dist/http/routes/rules.js +3 -2
  48. package/dist/http/routes/runs.d.ts +0 -14
  49. package/dist/http/routes/runs.js +20 -8
  50. package/dist/http/routes/tasks.js +34 -8
  51. package/dist/http/routes/workflows.js +20 -5
  52. package/dist/http/server.d.ts +11 -1
  53. package/dist/http/server.js +220 -34
  54. package/dist/http/wire-types.d.ts +9 -4
  55. package/dist/leader/diffout.js +2 -1
  56. package/dist/leader/endpoint.js +49 -15
  57. package/dist/leader/fanout.js +6 -5
  58. package/dist/leader/leader.js +17 -14
  59. package/dist/leader/merge.js +17 -12
  60. package/dist/leader/planner.js +3 -2
  61. package/dist/leader/repair-oracle.js +6 -4
  62. package/dist/leader/repair-wire.js +5 -3
  63. package/dist/leader/wire.d.ts +80 -1
  64. package/dist/leader/wire.js +55 -26
  65. package/dist/main.js +44 -8
  66. package/dist/observability/err-text.d.ts +6 -0
  67. package/dist/observability/err-text.js +10 -0
  68. package/dist/observability/fail-open.d.ts +40 -0
  69. package/dist/observability/fail-open.js +19 -0
  70. package/dist/observability/metrics.js +1 -1
  71. package/dist/observability/run-terminal-log.d.ts +120 -0
  72. package/dist/observability/run-terminal-log.js +360 -0
  73. package/dist/orchestration/workflow-completion-inbox.d.ts +9 -0
  74. package/dist/orchestration/workflow-completion-inbox.js +8 -0
  75. package/dist/permission-rule-vocab.d.ts +40 -0
  76. package/dist/permission-rule-vocab.js +17 -0
  77. package/dist/plugins/checkpoint-store-sql.d.ts +26 -0
  78. package/dist/plugins/checkpoint-store-sql.js +23 -4
  79. package/dist/plugins/file-run-store.d.ts +3 -34
  80. package/dist/plugins/file-run-store.js +21 -0
  81. package/dist/plugins/local-checkpoint-store.d.ts +10 -0
  82. package/dist/plugins/local-checkpoint-store.js +5 -0
  83. package/dist/plugins/local-session-store.d.ts +9 -9
  84. package/dist/plugins/local-session-store.js +5 -3
  85. package/dist/plugins/memory-run-store.d.ts +3 -15
  86. package/dist/plugins/memory-run-store.js +21 -0
  87. package/dist/plugins/permission-rule-store-file.d.ts +22 -4
  88. package/dist/plugins/permission-rule-store-file.js +25 -11
  89. package/dist/plugins/permission-rule-store-sql.d.ts +60 -30
  90. package/dist/plugins/permission-rule-store-sql.js +39 -23
  91. package/dist/plugins/pg-session-storage.js +17 -13
  92. package/dist/plugins/remote-scratchpad.js +3 -2
  93. package/dist/plugins/run-store-sql.d.ts +3 -42
  94. package/dist/plugins/run-store-sql.js +36 -5
  95. package/dist/plugins/store-backend.d.ts +1 -1
  96. package/dist/plugins/store-contracts.d.ts +70 -1
  97. package/dist/plugins/tidb-session-store.js +18 -14
  98. package/dist/plugins/workflow-run-store-sql.d.ts +5 -0
  99. package/dist/plugins/workflow-run-store-sql.js +10 -2
  100. package/dist/rules-consent.d.ts +5 -4
  101. package/dist/rules-consent.js +25 -46
  102. package/dist/run-cancel-context.d.ts +13 -0
  103. package/dist/run-cancel-context.js +15 -0
  104. package/dist/run-local.js +2 -2
  105. package/dist/runs.d.ts +4 -1
  106. package/dist/runs.js +41 -10
  107. package/dist/runtime-caps-resolver.d.ts +133 -17
  108. package/dist/runtime-caps-resolver.js +44 -4
  109. package/dist/security.d.ts +7 -0
  110. package/dist/task-settings.d.ts +57 -3
  111. package/dist/task-settings.js +78 -5
  112. package/dist/task-workflow.d.ts +32 -2
  113. package/dist/task-workflow.js +8 -3
  114. package/dist/tool-approval.d.ts +38 -63
  115. package/dist/tool-approval.js +102 -74
  116. package/dist/trace/core-keyset-guard.d.ts +2 -2
  117. package/dist/trace/ledger-sink.d.ts +13 -4
  118. package/dist/trace/ledger-sink.js +14 -6
  119. package/dist/trace/project.d.ts +13 -0
  120. package/dist/trace/project.js +10 -1
  121. package/dist/trace/redact.d.ts +22 -11
  122. package/dist/trace/redact.js +655 -20
  123. package/dist/trace/sema-provenance.d.ts +26 -0
  124. package/dist/trace/sema-provenance.js +11 -0
  125. package/dist/turn-activity.d.ts +32 -2
  126. package/dist/turn-activity.js +29 -4
  127. package/package.json +3 -3
  128. package/skills/find-skills.md +1 -1
  129. package/skills/loop.md +1 -1
@@ -3,7 +3,7 @@ import { execFileSync } from "node:child_process";
3
3
  import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
- import { Runner, combinePolicies, createDurableQuestionPolicy, QUESTION_AWAITS_RESUME, delimitUntrusted, } from "@sema-agent/core";
6
+ import { Runner, combinePolicies, createDurableQuestionPolicy, QUESTION_AWAITS_RESUME, delimitUntrusted, hasDestroy, } from "@sema-agent/core";
7
7
  import { RemoteContainerExecutionEnv } from "../plugins/remote-env-e2b.js";
8
8
  import { createDurableAskPolicy } from "../approval.js";
9
9
  import { presignS3Url } from "../plugins/s3-presign.js";
@@ -13,6 +13,8 @@ import { runLeaderTask } from "./leader.js";
13
13
  import { attachRepairLoopDeps } from "./repair-wire.js";
14
14
  import { routePlanWithFallback, validateSubtasks } from "./planner.js";
15
15
  import { createEngineNoticeSeat } from "../boot/runner-deps.js";
16
+ import { currentPrincipal } from "../observability/principal-context.js";
17
+ import { redactHead, redactPatchHead } from "../observability/run-terminal-log.js";
16
18
  const REPAIR_LOOP_WIRED = true;
17
19
  const sh = (env) => async (cmd) => {
18
20
  const r = await env.exec(`bash -lc ${JSON.stringify(cmd)}`);
@@ -30,6 +32,24 @@ const asIntegrationEnv = (env) => ({
30
32
  writeFile: async (p, c) => { const r = await env.writeFile(p, c); if (!r.ok)
31
33
  throw r.error; },
32
34
  });
35
+ export function buildConflictResolverObjective(args) {
36
+ const { workerId, repoDir, patch, conflictText } = args;
37
+ return [
38
+ `A parallel worker '${workerId}' ported a module on its own branch, but applying its patch onto the already-integrated tree at ${repoDir} produced a MERGE CONFLICT (git apply --3way). The OTHER workers' patches already applied cleanly — integrate THIS worker's changes too, resolving the overlap.`,
39
+ `Resolve EVERY conflict in the working tree: open each file containing conflict markers (<<<<<<< / ======= / >>>>>>>) and merge BOTH sides' real intent (keep both workers' behaviour — never drop one side just to make it apply). Then apply any rejected hunks recorded in *.rej files by hand, and DELETE every *.rej and *.orig file.`,
40
+ `When done, NO conflict markers and NO *.rej files may remain under ${repoDir}. Verify yourself before finishing: grep -rIlE '^(<<<<<<<|>>>>>>>)' . ; find . -name '*.rej' — BOTH must print nothing.`,
41
+ `HARD RULES: do NOT git commit / git add / git push (the coordinator squashes at the end); do NOT modify build configuration; do NOT call AskUserQuestion — decide autonomously. Preserve both sides' ported semantics.`,
42
+ ``,
43
+ delimitUntrusted("CONFLICTING WORKER PATCH (untrusted)", redactPatchHead(patch, 6000)),
44
+ delimitUntrusted("CONFLICT DETAILS (untrusted)", conflictText),
45
+ ].join("\n");
46
+ }
47
+ export function createEnvReaper(env, what) {
48
+ if (!hasDestroy(env)) {
49
+ throw new Error(`${what}: the execution-env factory returned an env with no destroy() — the leader OWNS this dedicated sandbox and this closure is its only teardown, so it would leak (and keep billing) until the backend's own timeout reaps it. Deploy a factory whose env implements RemoteExecutionEnv.destroy for the leader lane.`);
50
+ }
51
+ return async () => void (await env.destroy());
52
+ }
33
53
  export async function stageWorkerEnv(env, opts) {
34
54
  const { seedCmd, repo, branch, uploadScriptPath, uploadScript } = opts;
35
55
  const probe = await env.exec(`bash -lc ${JSON.stringify(`if [ -f ${uploadScriptPath} ]; then exit 0; fi; ` +
@@ -130,6 +150,14 @@ export function createLeaderRunner(cfg) {
130
150
  const onNoticeSeat = createEngineNoticeSeat(cfg.logger);
131
151
  const governanceSeat = cfg.governance ?? {};
132
152
  const deploymentPostureSeat = cfg.deploymentPosture ?? {};
153
+ const tracerSeat = cfg.tracer ? { tracer: cfg.tracer } : {};
154
+ const usageWindowSeat = cfg.usageWindows && cfg.usageWindowStore ? { usageWindows: cfg.usageWindows, usageWindowStore: cfg.usageWindowStore } : {};
155
+ const promptSourceSeat = cfg.promptSource ? { promptSource: cfg.promptSource } : {};
156
+ const onErrorSeat = cfg.onError ? { onError: cfg.onError } : {};
157
+ const principalSpec = () => {
158
+ const p = currentPrincipal();
159
+ return p ? { principal: p } : {};
160
+ };
133
161
  const mkRepair = (rawEnv, repoDir, oracleFiles) => {
134
162
  if (repairRounds <= 0)
135
163
  return undefined;
@@ -143,7 +171,7 @@ export function createLeaderRunner(cfg) {
143
171
  throw new Error(`oracle still present after remove (${f.path}) — refusing repair (measurement integrity)`);
144
172
  }
145
173
  try {
146
- const runner = new Runner({ brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat, executionEnv: rawEnv, rootPath: repoDir });
174
+ const runner = new Runner({ brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat, ...tracerSeat, ...usageWindowSeat, ...promptSourceSeat, ...onErrorSeat, executionEnv: rawEnv, rootPath: repoDir });
147
175
  const objective = [
148
176
  `The integrated project at ${repoDir} fails its build/test. Make the MINIMAL change to the working tree so this command exits 0 (cd into the repo and run it yourself to confirm):`,
149
177
  ` ${testCmd}`,
@@ -155,6 +183,7 @@ export function createLeaderRunner(cfg) {
155
183
  const res = await runner
156
184
  .runTaskStream({
157
185
  objective,
186
+ ...principalSpec(),
158
187
  sessionId: `leader-repair-${Date.now()}-${round}`,
159
188
  limits: { maxCostUsd: repairBudgetUsd, maxWalltimeMs: Math.max(300_000, Math.floor(leaderTimeoutMs / 2)) },
160
189
  })
@@ -186,19 +215,12 @@ export function createLeaderRunner(cfg) {
186
215
  throw new Error(`oracle still present after remove (${f.path}) — refusing conflict-resolve (measurement integrity)`);
187
216
  }
188
217
  try {
189
- const runner = new Runner({ brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat, executionEnv: rawEnv, rootPath: repoDir });
190
- const objective = [
191
- `A parallel worker '${workerId}' ported a module on its own branch, but applying its patch onto the already-integrated tree at ${repoDir} produced a MERGE CONFLICT (git apply --3way). The OTHER workers' patches already applied cleanly — integrate THIS worker's changes too, resolving the overlap.`,
192
- `Resolve EVERY conflict in the working tree: open each file containing conflict markers (<<<<<<< / ======= / >>>>>>>) and merge BOTH sides' real intent (keep both workers' behaviour — never drop one side just to make it apply). Then apply any rejected hunks recorded in *.rej files by hand, and DELETE every *.rej and *.orig file.`,
193
- `When done, NO conflict markers and NO *.rej files may remain under ${repoDir}. Verify yourself before finishing: grep -rIlE '^(<<<<<<<|>>>>>>>)' . ; find . -name '*.rej' — BOTH must print nothing.`,
194
- `HARD RULES: do NOT git commit / git add / git push (the coordinator squashes at the end); do NOT modify build configuration; do NOT call AskUserQuestion — decide autonomously. Preserve both sides' ported semantics.`,
195
- ``,
196
- delimitUntrusted("CONFLICTING WORKER PATCH (untrusted)", patch.slice(0, 6000)),
197
- delimitUntrusted("CONFLICT DETAILS (untrusted)", conflictText),
198
- ].join("\n");
218
+ const runner = new Runner({ brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat, ...tracerSeat, ...usageWindowSeat, ...promptSourceSeat, ...onErrorSeat, executionEnv: rawEnv, rootPath: repoDir });
219
+ const objective = buildConflictResolverObjective({ workerId, repoDir, patch, conflictText });
199
220
  const res = await runner
200
221
  .runTaskStream({
201
222
  objective,
223
+ ...principalSpec(),
202
224
  sessionId: `leader-conflict-${Date.now()}-${round}`,
203
225
  limits: { maxCostUsd: repairBudgetUsd, maxWalltimeMs: Math.max(300_000, Math.floor(leaderTimeoutMs / 2)) },
204
226
  })
@@ -249,10 +271,11 @@ export function createLeaderRunner(cfg) {
249
271
  if (Array.isArray(body.subtasks) && body.subtasks.length > 0) {
250
272
  return validateSubtasks(body.subtasks);
251
273
  }
252
- const planRunner = new Runner({ brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat });
274
+ const planRunner = new Runner({ brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat, ...tracerSeat, ...usageWindowSeat, ...promptSourceSeat, ...onErrorSeat });
253
275
  const runRoute = async (prompt) => {
254
276
  const res = await planRunner.runTaskStream({
255
277
  objective: prompt,
278
+ ...principalSpec(),
256
279
  sessionId: `leader-route-${Date.now()}`,
257
280
  ...(cfg.routerModel ? { model: cfg.routerModel } : {}),
258
281
  }).result();
@@ -278,7 +301,7 @@ export function createLeaderRunner(cfg) {
278
301
  onQuestion: QUESTION_AWAITS_RESUME,
279
302
  };
280
303
  await d.checkpointStore.putCtx(sessionId, { body: { objective: sub.spec.objective } }).catch((e) => {
281
- cfg.logger?.warn?.("leader_putctx_failed", { workerId: sub.workerId, err: String(e) });
304
+ cfg.logger?.warn?.("leader_putctx_failed", { workerId: sub.workerId, err: redactHead(e instanceof Error ? e.message : String(e), 2000) });
282
305
  });
283
306
  }
284
307
  let resourceSpec = {};
@@ -339,15 +362,15 @@ export function createLeaderRunner(cfg) {
339
362
  workerId: sub.workerId, sessionId, branch: sub.branch,
340
363
  baseSha,
341
364
  runner: keepCtxWarm(new Runner({
342
- brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat,
365
+ brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat, ...tracerSeat, ...usageWindowSeat, ...promptSourceSeat, ...onErrorSeat,
343
366
  ...(cfg.toolResultStore ? { toolResultStore: cfg.toolResultStore } : {}),
344
367
  ...(cfg.sessionStore ? { sessionStore: cfg.sessionStore } : {}),
345
- executionEnvFactory: (ctx) => withStaging(envFactory(ctx), stage, async (e) => {
368
+ executionEnvFactory: async (ctx) => withStaging(await envFactory(ctx), stage, async (e) => {
346
369
  await sh(e)(`cd ${repo} && git add -A && git -c user.name='leader-belt' -c user.email='belt@leader' commit -qm 'belt: auto-commit uncommitted worker output' || true; sh ${W}/.leader-upload.sh`);
347
370
  }),
348
371
  })),
349
372
  fetchDiff: () => fetchUploadedDiff(getUrl),
350
- spec: { ...workerLimits, ...sub.spec, ...durableSpec, ...resourceSpec, objective: `${sub.spec.objective}${uploadStepSuffix(W)}` },
373
+ spec: { ...workerLimits, ...sub.spec, ...durableSpec, ...resourceSpec, ...principalSpec(), objective: `${sub.spec.objective}${uploadStepSuffix(W)}` },
351
374
  };
352
375
  }
353
376
  if (!cfg.e2bApiKey)
@@ -359,25 +382,30 @@ export function createLeaderRunner(cfg) {
359
382
  const baseSha = await sh(env)(`cd ${repo} && git rev-parse HEAD`);
360
383
  return {
361
384
  workerId: sub.workerId, sessionId, branch: sub.branch, baseSha,
362
- runner: keepCtxWarm(new Runner({ brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat, ...(cfg.toolResultStore ? { toolResultStore: cfg.toolResultStore } : {}), ...(cfg.sessionStore ? { sessionStore: cfg.sessionStore } : {}), executionEnv: env })),
385
+ runner: keepCtxWarm(new Runner({ brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat, ...tracerSeat, ...usageWindowSeat, ...promptSourceSeat, ...onErrorSeat, ...(cfg.toolResultStore ? { toolResultStore: cfg.toolResultStore } : {}), ...(cfg.sessionStore ? { sessionStore: cfg.sessionStore } : {}), executionEnv: env })),
363
386
  diffEnv: env,
364
387
  destroy: () => env.destroy().then(() => { }),
365
- spec: { ...workerLimits, ...sub.spec, ...durableSpec, ...resourceSpec },
388
+ spec: { ...workerLimits, ...sub.spec, ...durableSpec, ...resourceSpec, ...principalSpec() },
366
389
  };
367
390
  };
368
391
  const provisionIntegrationSandbox = async () => {
369
392
  if (cfg.envFactory) {
370
393
  const integSessionId = `leader-integ-${Date.now()}`;
371
- const env = cfg.envFactory({ sessionId: integSessionId, placementRootSessionId: integSessionId });
394
+ const env = await cfg.envFactory({ sessionId: integSessionId, placementRootSessionId: integSessionId });
395
+ const reap = createEnvReaper(env, "leader integration sandbox");
372
396
  try {
373
397
  await sh(env)(body.seedCmd);
374
398
  await injectOracles(env);
375
399
  }
376
400
  catch (e) {
377
- await Promise.resolve(env.destroy()).catch(() => { });
401
+ await reap().catch(() => { });
378
402
  throw e;
379
403
  }
380
- return { env: asIntegrationEnv(env), destroy: () => Promise.resolve(env.destroy()).then(() => { }), ...(mkRepair(env, repo, body.oracleFiles ?? []) ? { repair: mkRepair(env, repo, body.oracleFiles ?? []) } : {}), ...(mkConflictResolver(env, repo, body.oracleFiles ?? []) ? { conflictResolver: mkConflictResolver(env, repo, body.oracleFiles ?? []) } : {}) };
404
+ const rawEnv = env;
405
+ const oracleFiles = body.oracleFiles ?? [];
406
+ const repair = mkRepair(rawEnv, repo, oracleFiles);
407
+ const conflictResolver = mkConflictResolver(rawEnv, repo, oracleFiles);
408
+ return { env: asIntegrationEnv(env), destroy: reap, ...(repair ? { repair } : {}), ...(conflictResolver ? { conflictResolver } : {}) };
381
409
  }
382
410
  if (!cfg.e2bApiKey)
383
411
  throw new Error("leader wire: no envFactory and no e2bApiKey");
@@ -415,7 +443,7 @@ export function createLeaderRunner(cfg) {
415
443
  let env;
416
444
  if (cfg.envFactory) {
417
445
  const graderSessionId = `leader-grader-${Date.now()}`;
418
- env = cfg.envFactory({ sessionId: graderSessionId, placementRootSessionId: graderSessionId });
446
+ env = await cfg.envFactory({ sessionId: graderSessionId, placementRootSessionId: graderSessionId });
419
447
  }
420
448
  else {
421
449
  if (!cfg.e2bApiKey)
@@ -427,6 +455,7 @@ export function createLeaderRunner(cfg) {
427
455
  }
428
456
  env = e2b;
429
457
  }
458
+ const reapGrader = createEnvReaper(env, "leader repair-loop grader");
430
459
  let integBase;
431
460
  try {
432
461
  await sh(env)(body.seedCmd);
@@ -434,11 +463,11 @@ export function createLeaderRunner(cfg) {
434
463
  integBase = await sh(env)(`cd ${repo} && git rev-parse HEAD`);
435
464
  }
436
465
  catch (e) {
437
- await Promise.resolve(env.destroy()).catch(() => { });
466
+ await reapGrader().catch(() => { });
438
467
  throw e;
439
468
  }
440
469
  const grader = {
441
- env: env,
470
+ env,
442
471
  writeFile: async (p, c) => { const r = await env.writeFile(p, c); if (!r.ok)
443
472
  throw r.error; },
444
473
  repoDir: repo,
@@ -463,7 +492,7 @@ export function createLeaderRunner(cfg) {
463
492
  return {
464
493
  runner: solo.runner,
465
494
  grader,
466
- graderDestroy: () => Promise.resolve(env.destroy()).then(() => { }),
495
+ graderDestroy: reapGrader,
467
496
  pullWorkerDiff,
468
497
  oracleSteps,
469
498
  passTier,
package/dist/main.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
- import { Runner, InMemoryToolResultStore, TtlSessionStore, uuidv7, defaultTaskRegistry, createAllowDenyPolicy, workflowsCapability, probeSearchBackend, describeStaticWiring, DEFAULT_SUBAGENT_TOOL_NAME } from "@sema-agent/core";
4
+ import { Runner, InMemoryToolResultStore, TtlSessionStore, uuidv7, defaultTaskRegistry, createAllowDenyPolicy, workflowsCapability, probeSearchBackend, describeStaticWiring, expandTiers, DEFAULT_SUBAGENT_TOOL_NAME } from "@sema-agent/core";
5
5
  import { createSessionTitler } from "./session-titler.js";
6
6
  import { posIntEnv } from "./session-watch.js";
7
7
  import { selectEnvironmentTool } from "./capabilities/select-environment-tool.js";
8
8
  import { subagentSendUserFileExtraTools } from "./capabilities/send-user-file-tool.js";
9
9
  import { brainSummary } from "./brain.js";
10
- import { loadConfig, logConfigDiagnostics, resolveBindHost, splitLocalRoots, parseCapEnv, numEnv } from "./config.js";
10
+ import { loadConfig, logConfigDiagnostics, resolveBindHost, splitLocalRoots, parseCapEnv, parseNumOrFailNonNegative } from "./config.js";
11
11
  import { cardRuleScopeRoot } from "./task-cwd.js";
12
12
  import { isPricingConfigured } from "./budget.js";
13
13
  import { drainNumEnvWarnings } from "./plugins/remote-shell.js";
@@ -15,7 +15,7 @@ import { ensureChildSessionDurableWithPromotion } from "./plugins/session-store.
15
15
  import { ForkRoutingSessionStore } from "./plugins/fork-routing-session-store.js";
16
16
  import { webSearchConfigFromEnv, createWebSearchBackend, setWebSearchBadPayloadObserver, shouldProbeWebSearchOnBoot } from "./plugins/web-search.js";
17
17
  import { createAuthorizer, encodeCheckpointScope } from "./security.js";
18
- import { assertGateIntentServiceable, hasOperatorGateIntent } from "./approval.js";
18
+ import { assertGateIntentServiceable, buildForceDurableGateInertNotice, hasOperatorGateIntent } from "./approval.js";
19
19
  import { loadSkills } from "./capabilities/skills.js";
20
20
  import { createRepoClient } from "./capabilities/repo-tools.js";
21
21
  import { buildScenarios, builtinScenarioDetails } from "./capabilities/scenarios.js";
@@ -26,11 +26,13 @@ import { createRegistryJwtVerifier } from "./auth-bridge.js";
26
26
  import { createMetrics } from "./observability/metrics.js";
27
27
  import { installFailOpenRecorder } from "./observability/fail-open.js";
28
28
  import { installSecretEnvScrubRecorder } from "./observability/secret-env-scrub.js";
29
+ import { setRunTerminalObserver } from "./observability/run-terminal-log.js";
29
30
  import { setRedactionObserver, redactSecrets } from "./trace/redact.js";
30
31
  import { RateLimiter } from "./observability/rate-limit.js";
31
32
  import { createHttpServer, explicitOperatorOk, hasServiceAuth } from "./http/server.js";
32
33
  import { exportMemoryScope } from "./memory-export.js";
33
- import { workflowModelAllowlistFor } from "./task-workflow.js";
34
+ import { workflowModelAllowlistFor, selfOrchestrationDenial } from "./task-workflow.js";
35
+ import { centerEntitlementSourceWired } from "./runtime-caps-resolver.js";
34
36
  import { createMemoryBundleFaces } from "./memory-bundle-engine.js";
35
37
  import { backendOwnsControlPlane, createMemoryComplianceFaces, createMemoryOriginFaces, createSessionMemoryStatusFace } from "./memory-operator-faces.js";
36
38
  import { performMemorySync } from "./memory-sync.js";
@@ -63,6 +65,8 @@ import { createExecutionEnv } from "./boot/execution-env.js";
63
65
  import { createWorkflowOrchestration } from "./boot/workflow-orchestration.js";
64
66
  import { createLiveCoordinators } from "./boot/coordinators.js";
65
67
  import { createRuntimeCaps } from "./boot/runtime-caps.js";
68
+ import { classifierRouteFor, coreArmsAutoOnIntent } from "./auto-mode-face.js";
69
+ import { createSessionShellGateRegistry } from "./boot/session-shell-gate-registry.js";
66
70
  import { createRunnerDeps, createRunnerDepsOnAsk, createSharedRunnerDeps } from "./boot/runner-deps.js";
67
71
  import { createOrgMemoryAdmissionWiring } from "./boot/org-memory.js";
68
72
  import { createGovernanceSeams } from "./boot/governance-seams.js";
@@ -98,11 +102,17 @@ async function main() {
98
102
  ...(process.env.MODEL_VERIFIER ? { rejected: process.env.MODEL_VERIFIER, reason: "not in catalog" } : {}),
99
103
  });
100
104
  }
101
- setRedactionObserver((pattern, n) => metrics.inc("redactions_applied_total", { pattern }, n));
105
+ setRedactionObserver((pattern, n, meta) => metrics.inc("redactions_applied_total", { pattern, arm: meta.arm }, n));
102
106
  setWebSearchBadPayloadObserver((provider) => {
103
107
  metrics.inc("web_search_bad_payload_total", { provider });
104
108
  logger.warn("web_search_non_array_payload", { provider });
105
109
  });
110
+ setRunTerminalObserver((level, fields) => {
111
+ if (level === "error")
112
+ logger.error("run_failed", fields);
113
+ else
114
+ logger.info("run_failed", fields);
115
+ });
106
116
  const localRoot = config.configLocalDir ?? process.env.AGENT_DATA_DIR ?? join(homedir(), ".ai-agent");
107
117
  const rootsSplit = splitLocalRoots({
108
118
  localDataRoot: config.localDataRoot,
@@ -171,7 +181,7 @@ async function main() {
171
181
  ruleScopeRootFor: (sessionId) => cardRuleScopeRoot(sessionId !== undefined ? perSessionCwd.get(sessionId) : undefined, config),
172
182
  });
173
183
  const memoryOptOutGrantStore = backend?.memoryOptOutGrant();
174
- const { principalCaps, centerRuntimeCapsResolver, runtimeCapsResolver } = createRuntimeCaps({ config, logger, memoryOptOutGrant: memoryOptOutGrantStore });
184
+ const { principalCaps, centerRuntimeCapsResolver, runtimeCapsResolver, runtimeCapsPeekResolver } = createRuntimeCaps({ config, logger, memoryOptOutGrant: memoryOptOutGrantStore });
175
185
  const orgMemoryAdmission = createOrgMemoryAdmissionWiring({ config, logger, metrics });
176
186
  const sharedMemoryStore = orgMemoryAdmission.directory !== undefined
177
187
  ? backend?.sharedMemoryStore?.(createSharedMemoryScopeAuthorizer({
@@ -248,9 +258,11 @@ async function main() {
248
258
  });
249
259
  const mcpRevocations = configCenter.takeMcpRevocations();
250
260
  const workflowModelAllowlistSnapshot = workflowModelAllowlistFor(config) ?? [];
261
+ const sessionShellGateRegistry = createSessionShellGateRegistry();
251
262
  const runnerDeps = createRunnerDeps({
252
263
  hands: commitHands,
253
264
  mcpRevocations,
265
+ sessionShellGateExplicitlyOff: sessionShellGateRegistry.explicitlyOff,
254
266
  config, logger, metrics, localRoot, promptSource: configCenter.promptSource, rosterStore, backgroundAgentStore, mailboxStore, usageWindowStore, brain,
255
267
  pricing, tracer, outcomeSink, elicitation, question, toolApproval, sessionStore, memoryEngine,
256
268
  sessionCaptureRecordStore,
@@ -344,7 +356,7 @@ async function main() {
344
356
  const runStore = backend ? backend.run() : undefined;
345
357
  const resumeAnchorStore = backend ? backend.resumeAnchor() : undefined;
346
358
  const approvalExemptionStore = backend ? backend.approvalExemption() : undefined;
347
- const taskTimeoutSec = Math.max(0, Math.floor(numEnv("TASK_TIMEOUT_SEC", "0")));
359
+ const taskTimeoutSec = Math.floor(parseNumOrFailNonNegative("TASK_TIMEOUT_SEC", process.env.TASK_TIMEOUT_SEC || "0"));
348
360
  const capEnv = (name) => {
349
361
  const r = parseCapEnv(name, process.env[name]);
350
362
  if (r.invalid !== undefined) {
@@ -408,6 +420,17 @@ async function main() {
408
420
  logger.info("direct_door_active", { iss: config.principalJwtIss, aud: config.principalJwtAud, operators: config.operatorPrincipals.length });
409
421
  if (config.requirePrincipal && config.selfOrchestrationEnabled && !config.selfOrchestrationWorkerIsolation)
410
422
  logger.warn("self_orchestration_no_worker_isolation", { detail: "multi-tenant self-orchestration without SELF_ORCHESTRATION_WORKER_ISOLATION=true — a tenant workflow can starve the host event loop (availability risk). Set it to enable worker_thread resource isolation." });
423
+ if (selfOrchestrationDenial(config, centerEntitlementSourceWired(config)) !== null)
424
+ logger.warn("self_orchestration_no_entitlement_source", { detail: "SELF_ORCHESTRATION_ENABLED is on and REQUIRE_PRINCIPAL is on, but no per-principal entitlement source is wired (no config center, or it is in dry-run). core's allowWorkflows is tighten-only — an absent resolver leaves it ungated — so this service fail-closes self-orchestration for EVERY principal: no task on this worker can ever get the workflow engine. /v1/capabilities now reports workflows:false (workflowsGate.denial=\"entitlement_resolver_absent\") and a submit carrying selfOrchestration/settings.ultracode is refused 501 instead of silently downgraded. Wire a config center (SEMA_REGISTRY_URL + token, not dry-run), or set SELF_ORCHESTRATION_ENABLED=false." });
425
+ {
426
+ const inert = buildForceDurableGateInertNotice({
427
+ entitlementSourceWired: centerEntitlementSourceWired(config),
428
+ checkpointStorePresent: checkpointStore !== undefined,
429
+ durableApproval: config.durableApproval,
430
+ });
431
+ if (inert)
432
+ logger.warn(inert.event, inert.fields);
433
+ }
411
434
  if (config.requirePrincipal && !config.authToken && Object.keys(config.authTokens ?? {}).length === 0 && !config.traceToken)
412
435
  logger.warn("multi_tenant_no_service_token", { detail: "REQUIRE_PRINCIPAL is on but no SERVICE_AUTH_TOKEN/authTokens/TRACE_TOKEN — fleet/trace/session-list are owner-gated only (never fleet-wide, tenant-isolation safe). Set a token for cross-tenant ops visibility." });
413
436
  const rateLimiter = backend?.rateLimiter
@@ -536,7 +559,7 @@ async function main() {
536
559
  resumeAnchorStore, approvalExemptionStore, sessionPolicyStore, taskAttachmentStore, fileHistoryStore,
537
560
  workflowCompletionInbox, taskListLane,
538
561
  });
539
- const leaderEndpoint = createLeaderFace({ config, logger, brain, pricing, executionEnvFactory, toolResultStore, sessionStore, checkpointStore, governanceSeams, backend });
562
+ const leaderEndpoint = createLeaderFace({ config, logger, brain, pricing, executionEnvFactory, toolResultStore, sessionStore, checkpointStore, governanceSeams, backend, tracer, usageWindowStore, promptSource: configCenter.promptSource, onError: runnerDeps.onError });
540
563
  const drainState = { draining: false };
541
564
  const authBridgeIssuer = process.env.AUTH_BRIDGE_ISSUER || config.configCenter?.baseUrl;
542
565
  const registryJwtVerifier = authBridgeIssuer
@@ -592,6 +615,7 @@ async function main() {
592
615
  question: question ? question : undefined,
593
616
  toolApproval: toolApproval ? toolApproval : undefined,
594
617
  approvalAskAudit: approvalAskAudit ? approvalAskAudit : undefined,
618
+ approvalLateSettle: approvalAskAudit ? approvalAskAudit : undefined,
595
619
  workflowAgentRegistry: workflowAgentRegistry ? workflowAgentRegistry : undefined,
596
620
  subagentSteerRegistry,
597
621
  workflowCompletionInbox: workflowCompletionInbox ? workflowCompletionInbox : undefined,
@@ -647,6 +671,17 @@ async function main() {
647
671
  costQuota,
648
672
  fleetLease: fleetLease ? fleetLease : undefined,
649
673
  usageWindowStore: usageWindowStore ? usageWindowStore : undefined,
674
+ autoModeFace: {
675
+ seatMounted: runnerDeps.autoMode !== undefined,
676
+ engineArmsOnIntent: coreArmsAutoOnIntent(),
677
+ resolveCaps: runtimeCapsPeekResolver,
678
+ classifierModel: () => {
679
+ const route = classifierRouteFor({ models: expandTiers(config.models, config.tiers ?? {}) ?? config.models, roles: config.roles });
680
+ if (route.fault !== undefined)
681
+ logger.debug("auto_mode_classifier_route_unresolved", { fault: route.fault });
682
+ return route.id;
683
+ },
684
+ },
650
685
  };
651
686
  const storeLiveProbe = backend && backend.kind !== "local" && backend.dbNowMs && config.storeProbeIntervalMs > 0
652
687
  ? createStoreLiveProbe({ probe: () => backend.dbNowMs(), intervalMs: config.storeProbeIntervalMs, logger })
@@ -692,6 +727,7 @@ async function main() {
692
727
  getKeyResolver: () => configCenter.getKeyResolver(),
693
728
  taskAttachmentStore, perSessionCwd, setSessionCwd, setSessionShellEnv, fenceSessionOwner,
694
729
  hookLlm, hookAgent, fleetBus, hookWakeBus, resumeAnchorStore, ownerAware, taskLimitCaps, taskTimeoutSec,
730
+ noteSessionShellGate: sessionShellGateRegistry.note,
695
731
  selectEnvTool, sendUserFileToolSpec, memoryEngine, durableEnabled, approvalExemptionStore,
696
732
  singleUserAutoAcceptBaseline, checkpointStore, deploymentHooks, imageIndex, perTaskImage,
697
733
  sessionEnvSelection,
@@ -0,0 +1,6 @@
1
+ /** 把任意 rejection 值变成**一定能得到的字符串**——终局链(setTerminal 的 errorMessage 座、留痕腿)专用。
2
+ * S-107 codex r28/r31 + 第三十一/三十二轮重扫:rejection 值可以是 message getter / toString / Symbol.toPrimitive 会抛的对象,
3
+ * 或 `Error` 实例的 message getter 返回带会抛 length getter 的非字符串——裸写 `e instanceof Error ? e.message : String(e)`
4
+ * 在 catch 里自己抛,外层 catch 吞掉 setTerminal ⇒ run 卡 running、claim 悬挂到 reaper。这里**不抛、恒返字符串**。 */
5
+ export declare function errorText(e: unknown): string;
6
+ //# sourceMappingURL=err-text.d.ts.map
@@ -0,0 +1,10 @@
1
+ export function errorText(e) {
2
+ try {
3
+ const m = e instanceof Error ? e.message : e;
4
+ return typeof m === "string" ? m : String(m);
5
+ }
6
+ catch {
7
+ return "<unprintable>";
8
+ }
9
+ }
10
+ //# sourceMappingURL=err-text.js.map
@@ -13,6 +13,10 @@ export declare const FAIL_OPEN_TAGS: {
13
13
  readonly cls: "F";
14
14
  readonly note: "design/383 S-1(applyMemoryOptOutGrant):本地 grant 腿故障时的**披露钩子**(onFault → warn 日志)自己抛了。钩子只是观测面:吞掉它保住的是「resolver 不因一行日志失败而 reject」(那会把一条腿的故障扩成整车故障 —— core 的 resolver-throw 降级臂作废同一次解析里 center 已答好的 allowWorkflows/allowFork);代价是**那一行披露丢了**,而 `allowMemoryOptOut` 键缺席这件事本身不受影响(fail-closed/fail-safe 极性由 core 按姿态判)。本计数让「披露钩子一直在炸」显形。";
15
15
  };
16
+ readonly "server.memory-optout-verdict-absence.warn-hook-threw": {
17
+ readonly cls: "F";
18
+ readonly note: "S-76(applyGovernedVerdictAbsenceWarning):`governed` + center-only verdict 源的部署上,**首次**解析到 `allowMemoryOptOut` 缺席时那一行**一次性披露**钩子自己抛了。与上一条同族、同理由:钩子只是观测面,吞掉保住的是「resolver 不因一行日志失败而 reject」(core 的 resolver-throw 降级臂会作废同一次解析里 center 已答好的 allowWorkflows/allowFork);代价是**「center 从不发这个键」这件事少了一行运行期证据**(启动期那行 `memory_capture_governed_center_only_source` 仍在,拒/放的极性也不受影响 —— 缺席由 core 按姿态判 fail-closed)。本计数让「披露钩子一直在炸」显形。";
19
+ };
16
20
  readonly "server.run-store.file-hydrate-unreadable": {
17
21
  readonly cls: "P-DEBT";
18
22
  readonly note: "件6(#43 案 [4281]):`FileRunStore.hydrate()` 的 `readdirSync(runs/)` 失败 ⇒ 引擎以**空账**起动。旧形是空 catch + return:data root 不可读(权限/挂载丢失/路径被换走)时零告警,此后每一次 getRun 恒 miss —— 而「恒 miss」正是 #43 现场看到的形,一次环境故障被伪装成「本来就没有这条 run」。方向问题在于吞掉之后被读成的是「证不出有行 ⇒ 没有行」,两句话不等价。**缺省不拒启是刻意保留的行为**,所以这条留作债:响亮化(error 级启动日志带 runs 绝对路径+异常文案)+ 本计数,让它在遥测里显形而不是只活在一行日志里。⚠️ 2026-08-29(S-46 件B)起终局格已兑现:部署可用 `RUN_STORE_STRICT_HYDRATE=on` 把这一格换成 **fail-closed 启动门**(拒启,遗言同样带绝对路径+异常文案);本计数因此只在**缺省 off** 档上产生 —— 读遥测时别把「计数为零」直接读成「没有部署踩到这条路」,也可能是它们开了拒启门、根本起不来。";
@@ -61,6 +65,10 @@ export declare const FAIL_OPEN_TAGS: {
61
65
  readonly cls: "F";
62
66
  readonly note: "被拒的 CC 导入 prepare(候选超帽)未能收掉 core 已落盘的那条 pending 审批记录。放行的最坏后果 = 共享库里留一条**永远没人要的** pending 行(零权限影响:没有票就兑不动它,而且它连确认都没过)。不留痕就没人知道清理面在漏,故记 F 类;真解是给 permission_rule_approval / permission_rule_ticket 加保留期清扫腿(已列后续件)。";
63
67
  };
68
+ readonly "server.rule-offers.uncovered-detail-dropped": {
69
+ readonly cls: "F";
70
+ readonly note: "design/382 §3.5(S-44):batch offer 的 `uncoveredDetail` 明细座形不合(集外 reason / 非数组 / 行数与 count 座不等 / 超 MAX_UNCOVERED_DETAIL_ROWS)⇒ 只丢这一座、batch 本体照投。放行的最坏后果 = 卡上少几行「这段为什么还会问」的解释(count 座仍在,core 契约明写明细缺席不是断言);把整只 batch 判假才是错的方向(一只真可兑的合取批从卡上消失)。纯展示轴 ⇒ F 类;必须留痕,否则上游明细座漂形在遥测里零痕迹。";
71
+ };
64
72
  readonly "server.rules.ticket-claim-release-failed": {
65
73
  readonly cls: "F";
66
74
  readonly note: "CC 规则导入票的**认领回滚**失败(认领之后的某一步没成 ⇒ 本该把认领放回去,而这次放回本身也抛了)。放行的最坏后果 = 这张票留在已认领态、属主这一轮不能重试 —— 恰好等于加认领回滚**之前**的行为,不是新损失;票随 TTL 自然消失,属主重走一次 prepare 即可拿新票(导入按 core 的设计幂等:同一条规则再兑付只是同一个 dot 的重放)。方向上没有任何权限被放宽(规则**没有**落地才走到这条臂),故 F 类;必须留痕,否则「票为什么突然不能用了」在遥测里没有任何痕迹。";
@@ -165,6 +173,38 @@ export declare const FAIL_OPEN_TAGS: {
165
173
  readonly cls: "F";
166
174
  readonly note: "S-38 裁 (c) 审计半场:local 车道 File ask 审计店(`approval-ask-audit-store.ts`)的**运行期** append(铸造/决议/腿闭/收敛四类行)失败(盘满/只读/fd 失效)⇒ 丢**这一条**审计行,审批路径照常。方向刻意 fail-open:审批可用性不押在审计盘上 —— 盘满时人还得能批,把一次审计写失败升级成审批失败是把「痕迹缺一条」换成「门整个不可用」,方向更坏。boot 期(mkdir/重放)失败则**拒启**(File 店族同律,store 头注成文),不经本 tag。放行的最坏后果 = 该 ask 在崩溃收敛读面上缺席或分臂失真(与 A-075.55 的零痕迹病同向)—— 所以必须留痕:「审计一直在漏写」与「没有审批发生」在读面上同形,不计数则病灶永不显形。detail 带行类与异常文案。";
167
175
  };
176
+ readonly "server.run-terminal.redact-threw": {
177
+ readonly cls: "F";
178
+ readonly note: "四个发射座(errorMessage 终态文案 / 诊断文本 diag / 补丁头 patch-head / 工件 artifact,probe 前缀判别;普查 56 行):S-105 最终轮 [medium]:终态 errorMessage/blockedReason/remoteEnvFailures[].message 脱敏抛出(关键字/前缀臂对 ≥2^23 单段 RangeError,既有,S-106)⇒ 落固定占位 «redacted:oversized»——绝不落原文(安全轴 fail-closed),也绝不阻断终态翻转与 claim 释放(可用性)。方向刻意:一条超大错误体不得把 run 卡在 running。";
179
+ };
180
+ readonly "server.run-terminal.redact-capped": {
181
+ readonly cls: "F";
182
+ readonly note: "S-107:终态自由文本(errorMessage/blockedReason/remoteEnvFailures[].message/error 列/done·failed 行)长度 > REDACT_INPUT_CAP(64KB)⇒ 整段换 «redacted:oversized»(fail-closed 取证丢弃,不截断不扫);计数=多少条终态文案被整段丢弃";
183
+ };
184
+ readonly "server.run-terminal.artifact-capped": {
185
+ readonly cls: "F";
186
+ readonly note: "S-107:leader_run 行上的工件字节(salvaged[].patch / candidatePatch.patch / merge.candidatePatch / merge.rej)长度 > ARTIFACT_INPUT_CAP(256KB)⇒ 整份换 «redacted:oversized-artifact»(fail-closed 取证丢弃;原字节仍在 worker 分支/merge 咽喉);计数=多少份工件被整份丢弃";
187
+ };
188
+ readonly "server.run-terminal.diag-capped": {
189
+ readonly cls: "F";
190
+ readonly note: "S-107:诊断文本座(merge details/measure/grader trace、leader 日志座、planner 样本、journal 预览、workflow_run error cut)长度 > DIAG_INPUT_CAP(256KB)⇒ 整段换 «redacted:oversized-diagnostic» [len=N](fail-closed 取证丢弃;固定窗不 fail-closed 故不开窗);计数=多少段诊断文本被整段丢弃";
191
+ };
192
+ readonly "server.run-terminal.patch-head-cut": {
193
+ readonly cls: "F";
194
+ readonly note: "S-107:进模型的 worker 补丁(collapse 目标 / 冲突解决目标)> DIAG_INPUT_CAP(256KB)⇒ **不内联**,整份换 «redacted:oversized-patch-not-inlined» [len=N](第十三轮开的 256KB 头窗及其裁切守卫在五轮复审后撤回);计数=多少份补丁因超限未内联";
195
+ };
196
+ readonly "server.run-terminal.output-truncated": {
197
+ readonly cls: "F";
198
+ readonly note: "S-107:持久 error 脱后 UTF-8 字节 > 65,000(TiDB TEXT 65,535 字节)⇒ 脱后切并带标记(切在占位里不是泄漏);计数=多少条终态文案被字节封顶截断";
199
+ };
200
+ readonly "server.runs.terminal-ledger-write-failed": {
201
+ readonly cls: "F";
202
+ readonly note: "S-107(全量门 silent-degradation 棘轮抓出的五处静默 `catch {}`):run 终局链里**账本腿**(flush 缓冲 text/reasoning、append `failed` 行)与终态翻转(setTerminal)已不共命运——账本 INSERT 抛不再让 run 卡 running 到 reaper;但那条抛掉的账本行之前是**静默**吞的。detail `arm=` 四臂(flush-clean-cancel / append-clean-cancel / flush-catch / append-thrown-cancel / append-failed):缺的是 run_events 尾巴(缓冲文本或 failed 事件),run 行本身仍是 durable truth,读面/reaper 容忍缺失,故 F 类;计数=多少次终局账本写失败。";
203
+ };
204
+ readonly "server.redact.ppk-eat-to-end": {
205
+ readonly cls: "F";
206
+ readonly note: "S-107 第三十二轮重扫:PuTTY .ppk 容器扫描器(redact.ts redactPpkContainers)在「无合格终止 / 收口后同容器范围再见私钥字段 / 收口后到下一版本头之间夹非空白」三形下从头**吃到输入末尾**(fail-closed 取证丢弃)——终态 errorMessage 座上=整条错误文案变成 «redacted:private-key»,与 redactions_applied_total{private-key} 的诚实替换同形不可分,故要有自己的计数/探针;影子扫不计;计数=多少次整段吃尾。";
207
+ };
168
208
  readonly "server.pg-pool.idle-client-error": {
169
209
  readonly cls: "F";
170
210
  readonly note: "S-55(test [5864] 独立复现):pg 连接池里一条连接被外部终止(pg_terminate_backend / 云端故障切换 / LB 空闲回收)或自身网络错误。**两态一 tag,detail 判别**(`state=idle` / `state=checked-out`;先例=park 墓碑行的两臂 detail 判别):①idle 态 —— node-postgres 语义(pg-pool@3.14.0 makeIdleListener):池在 emit 'error' **之前**已 `_remove` 掉该 client,坏连接自然淘汰,下次 acquire 发新连接,在飞查询零影响;修前 Pool 上无 'error' 监听 ⇒ EventEmitter 把事件转 throw ⇒ uncaughtException ⇒ 整进程 exit 1。②checked-out 态(codex R1-[high] 补,同批修)—— pg-pool 借出时摘 idle 监听(index.js:344),`pool.connect()` 显式持有的连接(事务)在 checkout 期死时 client 直接 emit 'error' 同样打死进程;守卫只观察:中毒 client 的后续查询由 pg 以 `_queryable=false` 响亮拒,归还时池淘汰;同一次 checkout 的死可能双发事件(FATAL 消息 + socket end),warn 逐枚、计数按 checkout 去重。放行的最坏后果 = 下一次 acquire 多付一次建连,故 F 类。必须留痕:「连接不断被外部杀」(故障切换风暴 / 回收策略过激 / 有人在库上清连接)与「一切正常」在服务面同形;每次事件另有逐次结构化 warn(`pg_pool_idle_client_error` / `pg_pool_checked_out_client_error`,含 code/severity),本计数让频率曲线可读。`pool.query()` 快路不在洞内(pg-pool 自带 checkout 期 once('error') 守卫)。mysql2 孪生(tidb-pool)**不同病**:PoolConnection 构造器恒挂 once('error') 库内吞并淘汰、借出期同在(坐标成文在 createTidbPool 头注),故无同族 tag。";
@@ -4,6 +4,10 @@ export const FAIL_OPEN_TAGS = {
4
4
  cls: "F",
5
5
  note: "design/383 S-1(applyMemoryOptOutGrant):本地 grant 腿故障时的**披露钩子**(onFault → warn 日志)自己抛了。钩子只是观测面:吞掉它保住的是「resolver 不因一行日志失败而 reject」(那会把一条腿的故障扩成整车故障 —— core 的 resolver-throw 降级臂作废同一次解析里 center 已答好的 allowWorkflows/allowFork);代价是**那一行披露丢了**,而 `allowMemoryOptOut` 键缺席这件事本身不受影响(fail-closed/fail-safe 极性由 core 按姿态判)。本计数让「披露钩子一直在炸」显形。",
6
6
  },
7
+ "server.memory-optout-verdict-absence.warn-hook-threw": {
8
+ cls: "F",
9
+ note: "S-76(applyGovernedVerdictAbsenceWarning):`governed` + center-only verdict 源的部署上,**首次**解析到 `allowMemoryOptOut` 缺席时那一行**一次性披露**钩子自己抛了。与上一条同族、同理由:钩子只是观测面,吞掉保住的是「resolver 不因一行日志失败而 reject」(core 的 resolver-throw 降级臂会作废同一次解析里 center 已答好的 allowWorkflows/allowFork);代价是**「center 从不发这个键」这件事少了一行运行期证据**(启动期那行 `memory_capture_governed_center_only_source` 仍在,拒/放的极性也不受影响 —— 缺席由 core 按姿态判 fail-closed)。本计数让「披露钩子一直在炸」显形。",
10
+ },
7
11
  "server.run-store.file-hydrate-unreadable": {
8
12
  cls: "P-DEBT",
9
13
  note: "件6(#43 案 [4281]):`FileRunStore.hydrate()` 的 `readdirSync(runs/)` 失败 ⇒ 引擎以**空账**起动。旧形是空 catch + return:data root 不可读(权限/挂载丢失/路径被换走)时零告警,此后每一次 getRun 恒 miss —— 而「恒 miss」正是 #43 现场看到的形,一次环境故障被伪装成「本来就没有这条 run」。方向问题在于吞掉之后被读成的是「证不出有行 ⇒ 没有行」,两句话不等价。**缺省不拒启是刻意保留的行为**,所以这条留作债:响亮化(error 级启动日志带 runs 绝对路径+异常文案)+ 本计数,让它在遥测里显形而不是只活在一行日志里。⚠️ 2026-08-29(S-46 件B)起终局格已兑现:部署可用 `RUN_STORE_STRICT_HYDRATE=on` 把这一格换成 **fail-closed 启动门**(拒启,遗言同样带绝对路径+异常文案);本计数因此只在**缺省 off** 档上产生 —— 读遥测时别把「计数为零」直接读成「没有部署踩到这条路」,也可能是它们开了拒启门、根本起不来。",
@@ -52,6 +56,10 @@ export const FAIL_OPEN_TAGS = {
52
56
  cls: "F",
53
57
  note: "被拒的 CC 导入 prepare(候选超帽)未能收掉 core 已落盘的那条 pending 审批记录。放行的最坏后果 = 共享库里留一条**永远没人要的** pending 行(零权限影响:没有票就兑不动它,而且它连确认都没过)。不留痕就没人知道清理面在漏,故记 F 类;真解是给 permission_rule_approval / permission_rule_ticket 加保留期清扫腿(已列后续件)。",
54
58
  },
59
+ "server.rule-offers.uncovered-detail-dropped": {
60
+ cls: "F",
61
+ note: "design/382 §3.5(S-44):batch offer 的 `uncoveredDetail` 明细座形不合(集外 reason / 非数组 / 行数与 count 座不等 / 超 MAX_UNCOVERED_DETAIL_ROWS)⇒ 只丢这一座、batch 本体照投。放行的最坏后果 = 卡上少几行「这段为什么还会问」的解释(count 座仍在,core 契约明写明细缺席不是断言);把整只 batch 判假才是错的方向(一只真可兑的合取批从卡上消失)。纯展示轴 ⇒ F 类;必须留痕,否则上游明细座漂形在遥测里零痕迹。",
62
+ },
55
63
  "server.rules.ticket-claim-release-failed": {
56
64
  cls: "F",
57
65
  note: "CC 规则导入票的**认领回滚**失败(认领之后的某一步没成 ⇒ 本该把认领放回去,而这次放回本身也抛了)。放行的最坏后果 = 这张票留在已认领态、属主这一轮不能重试 —— 恰好等于加认领回滚**之前**的行为,不是新损失;票随 TTL 自然消失,属主重走一次 prepare 即可拿新票(导入按 core 的设计幂等:同一条规则再兑付只是同一个 dot 的重放)。方向上没有任何权限被放宽(规则**没有**落地才走到这条臂),故 F 类;必须留痕,否则「票为什么突然不能用了」在遥测里没有任何痕迹。",
@@ -156,6 +164,17 @@ export const FAIL_OPEN_TAGS = {
156
164
  cls: "F",
157
165
  note: "S-38 裁 (c) 审计半场:local 车道 File ask 审计店(`approval-ask-audit-store.ts`)的**运行期** append(铸造/决议/腿闭/收敛四类行)失败(盘满/只读/fd 失效)⇒ 丢**这一条**审计行,审批路径照常。方向刻意 fail-open:审批可用性不押在审计盘上 —— 盘满时人还得能批,把一次审计写失败升级成审批失败是把「痕迹缺一条」换成「门整个不可用」,方向更坏。boot 期(mkdir/重放)失败则**拒启**(File 店族同律,store 头注成文),不经本 tag。放行的最坏后果 = 该 ask 在崩溃收敛读面上缺席或分臂失真(与 A-075.55 的零痕迹病同向)—— 所以必须留痕:「审计一直在漏写」与「没有审批发生」在读面上同形,不计数则病灶永不显形。detail 带行类与异常文案。",
158
166
  },
167
+ "server.run-terminal.redact-threw": {
168
+ cls: "F",
169
+ note: "四个发射座(errorMessage 终态文案 / 诊断文本 diag / 补丁头 patch-head / 工件 artifact,probe 前缀判别;普查 56 行):S-105 最终轮 [medium]:终态 errorMessage/blockedReason/remoteEnvFailures[].message 脱敏抛出(关键字/前缀臂对 ≥2^23 单段 RangeError,既有,S-106)⇒ 落固定占位 «redacted:oversized»——绝不落原文(安全轴 fail-closed),也绝不阻断终态翻转与 claim 释放(可用性)。方向刻意:一条超大错误体不得把 run 卡在 running。",
170
+ },
171
+ "server.run-terminal.redact-capped": { cls: "F", note: "S-107:终态自由文本(errorMessage/blockedReason/remoteEnvFailures[].message/error 列/done·failed 行)长度 > REDACT_INPUT_CAP(64KB)⇒ 整段换 «redacted:oversized»(fail-closed 取证丢弃,不截断不扫);计数=多少条终态文案被整段丢弃" },
172
+ "server.run-terminal.artifact-capped": { cls: "F", note: "S-107:leader_run 行上的工件字节(salvaged[].patch / candidatePatch.patch / merge.candidatePatch / merge.rej)长度 > ARTIFACT_INPUT_CAP(256KB)⇒ 整份换 «redacted:oversized-artifact»(fail-closed 取证丢弃;原字节仍在 worker 分支/merge 咽喉);计数=多少份工件被整份丢弃" },
173
+ "server.run-terminal.diag-capped": { cls: "F", note: "S-107:诊断文本座(merge details/measure/grader trace、leader 日志座、planner 样本、journal 预览、workflow_run error cut)长度 > DIAG_INPUT_CAP(256KB)⇒ 整段换 «redacted:oversized-diagnostic» [len=N](fail-closed 取证丢弃;固定窗不 fail-closed 故不开窗);计数=多少段诊断文本被整段丢弃" },
174
+ "server.run-terminal.patch-head-cut": { cls: "F", note: "S-107:进模型的 worker 补丁(collapse 目标 / 冲突解决目标)> DIAG_INPUT_CAP(256KB)⇒ **不内联**,整份换 «redacted:oversized-patch-not-inlined» [len=N](第十三轮开的 256KB 头窗及其裁切守卫在五轮复审后撤回);计数=多少份补丁因超限未内联" },
175
+ "server.run-terminal.output-truncated": { cls: "F", note: "S-107:持久 error 脱后 UTF-8 字节 > 65,000(TiDB TEXT 65,535 字节)⇒ 脱后切并带标记(切在占位里不是泄漏);计数=多少条终态文案被字节封顶截断" },
176
+ "server.runs.terminal-ledger-write-failed": { cls: "F", note: "S-107(全量门 silent-degradation 棘轮抓出的五处静默 `catch {}`):run 终局链里**账本腿**(flush 缓冲 text/reasoning、append `failed` 行)与终态翻转(setTerminal)已不共命运——账本 INSERT 抛不再让 run 卡 running 到 reaper;但那条抛掉的账本行之前是**静默**吞的。detail `arm=` 四臂(flush-clean-cancel / append-clean-cancel / flush-catch / append-thrown-cancel / append-failed):缺的是 run_events 尾巴(缓冲文本或 failed 事件),run 行本身仍是 durable truth,读面/reaper 容忍缺失,故 F 类;计数=多少次终局账本写失败。" },
177
+ "server.redact.ppk-eat-to-end": { cls: "F", note: "S-107 第三十二轮重扫:PuTTY .ppk 容器扫描器(redact.ts redactPpkContainers)在「无合格终止 / 收口后同容器范围再见私钥字段 / 收口后到下一版本头之间夹非空白」三形下从头**吃到输入末尾**(fail-closed 取证丢弃)——终态 errorMessage 座上=整条错误文案变成 «redacted:private-key»,与 redactions_applied_total{private-key} 的诚实替换同形不可分,故要有自己的计数/探针;影子扫不计;计数=多少次整段吃尾。" },
159
178
  "server.pg-pool.idle-client-error": {
160
179
  cls: "F",
161
180
  note: "S-55(test [5864] 独立复现):pg 连接池里一条连接被外部终止(pg_terminate_backend / 云端故障切换 / LB 空闲回收)或自身网络错误。**两态一 tag,detail 判别**(`state=idle` / `state=checked-out`;先例=park 墓碑行的两臂 detail 判别):①idle 态 —— node-postgres 语义(pg-pool@3.14.0 makeIdleListener):池在 emit 'error' **之前**已 `_remove` 掉该 client,坏连接自然淘汰,下次 acquire 发新连接,在飞查询零影响;修前 Pool 上无 'error' 监听 ⇒ EventEmitter 把事件转 throw ⇒ uncaughtException ⇒ 整进程 exit 1。②checked-out 态(codex R1-[high] 补,同批修)—— pg-pool 借出时摘 idle 监听(index.js:344),`pool.connect()` 显式持有的连接(事务)在 checkout 期死时 client 直接 emit 'error' 同样打死进程;守卫只观察:中毒 client 的后续查询由 pg 以 `_queryable=false` 响亮拒,归还时池淘汰;同一次 checkout 的死可能双发事件(FATAL 消息 + socket end),warn 逐枚、计数按 checkout 去重。放行的最坏后果 = 下一次 acquire 多付一次建连,故 F 类。必须留痕:「连接不断被外部杀」(故障切换风暴 / 回收策略过激 / 有人在库上清连接)与「一切正常」在服务面同形;每次事件另有逐次结构化 warn(`pg_pool_idle_client_error` / `pg_pool_checked_out_client_error`,含 code/severity),本计数让频率曲线可读。`pool.query()` 快路不在洞内(pg-pool 自带 checkout 期 once('error') 守卫)。mysql2 孪生(tidb-pool)**不同病**:PoolConnection 构造器恒挂 once('error') 库内吞并淘汰、借出期同在(坐标成文在 createTidbPool 头注),故无同族 tag。",
@@ -256,7 +256,7 @@ export function createMetrics() {
256
256
  m.counter("memory_knn_query_failed_total", "Memory KNN searchScored queries that failed at runtime (S22)");
257
257
  m.counter("memory_dedup_folded_total", "Memory writes folded into an existing near-duplicate row (LOW)");
258
258
  m.counter("memory_dedup_probe_failed_total", "Memory dedup KNN probes that failed and fell through to INSERT (LOW)");
259
- m.counter("redactions_applied_total", "redactSecrets rewrites (S24), by pattern label");
259
+ m.counter("redactions_applied_total", "redactSecrets rewrites (S24), by pattern label and `arm` (S-88: full vs prefix-fragment arm — which RULE fired, NOT proof the text was upstream-truncated)");
260
260
  m.counter("stale_cache_conflict_total", "Warm session-cache entries evicted by a cross-instance write while cached (S25)");
261
261
  m.counter("breaker_writethrough_failed_total", "Circuit-breaker cross-replica write-through failures (LOW), by backend");
262
262
  m.counter("web_search_bad_payload_total", "Web-search provider responses whose results field was not an array (LOW), by provider");