@sema-agent/server 7.118.0 → 7.120.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 (139) hide show
  1. package/MIGRATION.md +139 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/USAGE.md +26 -13
  5. package/deploy/sema-up/README.md +2 -0
  6. package/deploy/sema-up/chart/templates/minio.yaml +2 -1
  7. package/deploy/sema-up/chart/templates/server-v4pro.yaml +1 -0
  8. package/deploy/sema-up/chart/templates/server.yaml +1 -0
  9. package/deploy/sema-up/chart/values.yaml +7 -1
  10. package/deploy/sema-up/cluster-up.sh +20 -8
  11. package/deploy/sema-up/kube-up.sh +29 -16
  12. package/deploy/sema-up/lib/buckets.sh +21 -0
  13. package/deploy/sema-up/lib/health.sh +33 -0
  14. package/deploy/sema-up/sema-up.sh +14 -9
  15. package/deploy/sema-up/smoke.sh +16 -6
  16. package/deploy/sema-up/stack/compose.yaml +1 -1
  17. package/deploy/sema-up/stack/sema-stack-cluster.yaml +4 -1
  18. package/dist/approval-ask-audit-store.js +2 -7
  19. package/dist/approval-card.d.ts +5 -5
  20. package/dist/approval.d.ts +51 -5
  21. package/dist/approval.js +76 -13
  22. package/dist/audit.d.ts +12 -1
  23. package/dist/audit.js +62 -2
  24. package/dist/auto-mode-face.d.ts +2 -3
  25. package/dist/bake-runner/main.js +1 -6
  26. package/dist/bake-runner/protocol.d.ts +18 -0
  27. package/dist/bake-runner/protocol.js +22 -2
  28. package/dist/bake-runner/runner.d.ts +12 -8
  29. package/dist/bake-runner/runner.js +5 -9
  30. package/dist/boot/config-center.js +6 -6
  31. package/dist/boot/coordinators.js +4 -1
  32. package/dist/boot/device-lane.d.ts +3 -3
  33. package/dist/boot/device-lane.js +1 -2
  34. package/dist/boot/execution-env.js +3 -2
  35. package/dist/boot/fatal-exit.d.ts +7 -1
  36. package/dist/boot/fatal-exit.js +2 -1
  37. package/dist/boot/leader.d.ts +4 -4
  38. package/dist/boot/leader.js +1 -2
  39. package/dist/boot/parked-revive-gate.d.ts +5 -1
  40. package/dist/boot/parked-revive-gate.js +8 -2
  41. package/dist/boot/resolve-spec.js +7 -4
  42. package/dist/boot/runner-deps.d.ts +4 -3
  43. package/dist/boot/runner-deps.js +7 -3
  44. package/dist/boot/stage-01-config.js +2 -0
  45. package/dist/boot/stage-07-capability-layer.js +5 -1
  46. package/dist/boot/stage-10-http-server.js +3 -2
  47. package/dist/boot/stores.js +12 -7
  48. package/dist/capabilities/builtin-tools.d.ts +31 -8
  49. package/dist/capabilities/builtin-tools.js +22 -1
  50. package/dist/capabilities/center-plugins.d.ts +2 -0
  51. package/dist/capabilities/center-plugins.js +24 -6
  52. package/dist/capabilities/code-review-council.js +8 -11
  53. package/dist/capabilities/scenarios.d.ts +42 -13
  54. package/dist/capabilities/scenarios.js +36 -9
  55. package/dist/capabilities/send-user-file-tool.d.ts +6 -3
  56. package/dist/capabilities/send-user-file-tool.js +1 -1
  57. package/dist/config-catalog.js +5 -5
  58. package/dist/config-lkg.d.ts +33 -5
  59. package/dist/config-lkg.js +55 -11
  60. package/dist/config-types.d.ts +8 -1
  61. package/dist/config.js +11 -11
  62. package/dist/deployment-governance.d.ts +8 -3
  63. package/dist/deployment-governance.js +7 -4
  64. package/dist/device-store.d.ts +7 -0
  65. package/dist/device-store.js +4 -0
  66. package/dist/device-ws-hub.js +17 -9
  67. package/dist/device-ws-protocol.d.ts +15 -0
  68. package/dist/host-decision.d.ts +52 -27
  69. package/dist/host-decision.js +14 -7
  70. package/dist/http/admission.js +6 -6
  71. package/dist/http/resume-legs.js +6 -1
  72. package/dist/http/routes/approvals-assistant.d.ts +25 -3
  73. package/dist/http/routes/approvals-assistant.js +9 -5
  74. package/dist/http/routes/capabilities.js +5 -3
  75. package/dist/http/routes/devices.js +3 -3
  76. package/dist/http/routes/memory-origin.d.ts +1 -1
  77. package/dist/http/routes/rules.js +13 -6
  78. package/dist/http/routes/runs.js +11 -5
  79. package/dist/http/routes/sessions.js +20 -16
  80. package/dist/http/wire-types.d.ts +12 -13
  81. package/dist/images/manifest.d.ts +7 -1
  82. package/dist/images/manifest.js +18 -16
  83. package/dist/list-entry-display.d.ts +14 -0
  84. package/dist/list-entry-display.js +14 -0
  85. package/dist/memory-store-refusal.d.ts +7 -12
  86. package/dist/memory-store-refusal.js +4 -5
  87. package/dist/namespaced-rule-name.d.ts +4 -3
  88. package/dist/namespaced-rule-name.js +2 -6
  89. package/dist/observability/fail-open.d.ts +9 -1
  90. package/dist/observability/fail-open.js +9 -1
  91. package/dist/operator-ask.d.ts +7 -6
  92. package/dist/plugins/approval-exemption-store.d.ts +4 -0
  93. package/dist/plugins/approval-exemption-store.js +1 -0
  94. package/dist/plugins/blob-backend.d.ts +13 -0
  95. package/dist/plugins/blob-backend.js +26 -0
  96. package/dist/plugins/local-session-store.d.ts +78 -47
  97. package/dist/plugins/local-session-store.js +152 -122
  98. package/dist/plugins/one-time-migrations.d.ts +14 -0
  99. package/dist/plugins/one-time-migrations.js +90 -3
  100. package/dist/plugins/permission-rule-store-file.js +2 -6
  101. package/dist/plugins/permission-rule-store-sql.d.ts +64 -6
  102. package/dist/plugins/permission-rule-store-sql.js +166 -59
  103. package/dist/plugins/remote-env-device.d.ts +12 -1
  104. package/dist/plugins/remote-env-device.js +13 -5
  105. package/dist/plugins/scheduler-support.d.ts +8 -4
  106. package/dist/plugins/scheduler-support.js +5 -5
  107. package/dist/plugins/snapshot-bucket-presence.d.ts +36 -0
  108. package/dist/plugins/snapshot-bucket-presence.js +40 -0
  109. package/dist/plugins/store-backend.d.ts +17 -6
  110. package/dist/plugins/store-backend.js +46 -16
  111. package/dist/plugins/store-open-wait.d.ts +33 -0
  112. package/dist/plugins/store-open-wait.js +73 -0
  113. package/dist/rule-entry-list.d.ts +0 -1
  114. package/dist/rule-entry-list.js +5 -10
  115. package/dist/rules-consent.d.ts +12 -3
  116. package/dist/rules-consent.js +5 -4
  117. package/dist/run-local.js +6 -3
  118. package/dist/security.d.ts +5 -3
  119. package/dist/security.js +4 -0
  120. package/dist/sema-config-dir.d.ts +19 -0
  121. package/dist/sema-config-dir.js +33 -0
  122. package/dist/session-policy-wire.js +10 -14
  123. package/dist/spec-fields.d.ts +7 -2
  124. package/dist/store-posture.d.ts +14 -6
  125. package/dist/store-posture.js +7 -4
  126. package/dist/task-settings.d.ts +30 -58
  127. package/dist/task-settings.js +6 -18
  128. package/dist/tool-approval.d.ts +97 -43
  129. package/dist/tool-approval.js +120 -31
  130. package/dist/trace/engine-notice-wire.d.ts +6 -4
  131. package/dist/trace/engine-notice-wire.js +16 -9
  132. package/dist/trace/redact.d.ts +6 -0
  133. package/dist/trace/redact.js +9 -9
  134. package/dist/trace/self-minted-wire.d.ts +15 -1
  135. package/dist/trace/self-minted-wire.js +2 -0
  136. package/dist/user-input-ledger.d.ts +19 -15
  137. package/dist/user-input-ledger.js +12 -20
  138. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +56 -14
  139. package/package.json +2 -2
@@ -1,7 +1,7 @@
1
1
  import { contentKindOf } from "./approval-content-kind.js";
2
- import { liveSettledBy, policyCannotAllow, readDecidedBy, DECIDED_BY_OUTSIDE_CLOSED_SET, POLICY_CANNOT_ALLOW_PERSON_ASK, SETTLED_BY_RETIRED } from "./host-decision.js";
2
+ import { heldAskOf, liveSettledBy, readDecidedBy, retiredDecisionKeyFacts, DECIDED_BY_OUTSIDE_CLOSED_SET, POLICY_CANNOT_ALLOW_PERSON_ASK } from "./host-decision.js";
3
3
  import { AsyncLocalStorage } from "node:async_hooks";
4
- import { uuidv7, classifyAsk, foldArbitrationRow, screenAskClassMembers, MAX_RULE_TEXT_CHARS, READ_ROOT_CANDIDATE_DIR_MAX, parseRuleText, pathConfinableWriteToolNames } from "@sema-agent/core";
4
+ import { uuidv7, classifyAsk, foldArbitrationRow, policyCannotAnswer, screenAskClassMembers, MAX_RULE_TEXT_CHARS, READ_ROOT_CANDIDATE_DIR_MAX, parseRuleText, pathConfinableWriteToolNames } from "@sema-agent/core";
5
5
  import { precheckCardRuleText } from "./rules-consent.js";
6
6
  import { redactDeep, redactSecrets, redactSecretsWithArms } from "./trace/redact.js";
7
7
  import { hasBidiControls } from "./text-bidi.js";
@@ -15,6 +15,8 @@ import { settleByPolicy } from "./approval-policy-settlement.js";
15
15
  import { governanceAskMarksFor, runWithGovernanceAskScope } from "./governance-ask-marks.js";
16
16
  import { governanceOriginOf } from "./runtime-governance.js";
17
17
  import { answerGrantsBeyondCall, grantBindingOf } from "./standing-answer-grant.js";
18
+ import { operatorAsk } from "./operator-ask.js";
19
+ import { APPROVAL_EXEMPTED_EVENT } from "./plugins/approval-exemption-store.js";
18
20
  const defaultLogger = createLogger();
19
21
  export const APPROVAL_GATE_KINDS = ["human", "irreversible_ask"];
20
22
  export function isApprovalGateKind(k) {
@@ -54,7 +56,7 @@ function durableRuleMaterial(material) {
54
56
  export const ASK_DECISION_CONSUMER_ABSENT = "absent";
55
57
  const MAX_INFLIGHT_TERMINAL_CLAIMS = 2;
56
58
  const DURABLE_CONVERGE_BACKOFF_MS = [50, 100, 200, 400, 800, 1600];
57
- const DURABLE_CONVERGE_READ_TIMEOUT_MS = 2_000;
59
+ export const DURABLE_CONVERGE_READ_TIMEOUT_MS = 2_000;
58
60
  const DURABLE_CALL_TIMEOUT_MS = 5_000;
59
61
  const TERMINAL_LADDER_WALL_MS = DURABLE_CALL_TIMEOUT_MS + DURABLE_CONVERGE_READ_TIMEOUT_MS + DURABLE_CONVERGE_BACKOFF_MS.reduce((a, b) => a + b, 0);
60
62
  const TERMINAL_CLAIM_CAP_POLL_MS = 50;
@@ -85,43 +87,65 @@ function sessionAllowCategory(toolName) {
85
87
  function sessionAllowKey(owner, sessionId, category, binding) {
86
88
  return JSON.stringify([owner, sessionId, category, binding.kind === "bound" ? binding.digest : null]);
87
89
  }
88
- function askStandingAnswerFacts(req, governanceForced) {
90
+ function askStandingAnswerFacts(req, governanceForced, deploymentListAsks) {
91
+ const classes = screenAskClassMembers(req.askClass, req.classes)?.classes;
89
92
  return {
90
93
  governanceForced,
91
94
  requiresRealApproval: req.requiresRealApproval === true,
92
95
  mandated: req.mandated === true,
93
96
  grantBinding: grantBindingOf(req),
94
- classes: screenAskClassMembers(req.askClass, req.classes)?.classes,
97
+ classes,
98
+ deploymentListQuestion: carriesDeploymentListQuestion(deploymentListAsks, classes),
95
99
  };
96
100
  }
97
- export function policyAskStandingAnswerFacts(req, ask) {
101
+ const NO_GATE_FACTS = { contentQuestion: false, markedUnresolvable: false, org: undefined, tightened: undefined, mandateMarks: {} };
102
+ const DEPLOYMENT_LIST_ASK_CLASSES = new Set(classifyAsk(operatorAsk("APPROVAL_REQUIRE", ""), NO_GATE_FACTS).classes);
103
+ export function carriesDeploymentListQuestion(deploymentListAsks, classes) {
104
+ return deploymentListAsks && classes !== undefined && classes.some((c) => DEPLOYMENT_LIST_ASK_CLASSES.has(c));
105
+ }
106
+ export function policyAskStandingAnswerFacts(req, ask, deploymentListQuestion) {
98
107
  return {
99
108
  governanceForced: false,
100
109
  requiresRealApproval: ask.requiresRealApproval === true,
101
110
  mandated: ask.mandated === true,
102
111
  grantBinding: grantBindingOf(req),
103
- classes: [...classifyAsk(ask, { contentQuestion: false, markedUnresolvable: false, org: undefined, tightened: undefined, mandateMarks: {} }).classes],
112
+ classes: [...classifyAsk(ask, NO_GATE_FACTS).classes],
113
+ deploymentListQuestion,
104
114
  };
105
115
  }
106
- export function standingAnswerMayRetire(facts) {
107
- return (!facts.governanceForced &&
108
- !facts.requiresRealApproval &&
109
- !facts.mandated &&
110
- facts.grantBinding.kind !== "unreadable" &&
111
- facts.classes !== undefined &&
112
- foldArbitrationRow(new Set(facts.classes)).ruleMayClear);
116
+ export function standingAnswerHome(facts) {
117
+ if (facts.governanceForced || facts.requiresRealApproval || facts.mandated || facts.classes === undefined)
118
+ return "none";
119
+ if (facts.deploymentListQuestion) {
120
+ return facts.grantBinding.kind === "none" && facts.classes.every((c) => DEPLOYMENT_LIST_ASK_CLASSES.has(c)) ? "server_store" : "none";
121
+ }
122
+ return facts.grantBinding.kind !== "unreadable" && foldArbitrationRow(new Set(facts.classes)).ruleMayClear ? "seat" : "none";
113
123
  }
114
124
  export function standingAnswerDurablyRecordable(facts) {
115
- return standingAnswerMayRetire(facts) && facts.grantBinding.kind === "none";
125
+ const home = standingAnswerHome(facts);
126
+ switch (home) {
127
+ case "none":
128
+ return false;
129
+ case "seat":
130
+ return facts.grantBinding.kind === "none";
131
+ case "server_store":
132
+ return true;
133
+ default: {
134
+ const unhandled = home;
135
+ throw new Error(`unhandled standing-answer home: ${String(unhandled)}`);
136
+ }
137
+ }
116
138
  }
117
- export function parkRowStandingAnswerFacts(pending, gate, governance) {
139
+ export function parkRowStandingAnswerFacts(pending, gate, governance, deploymentListAsks) {
118
140
  const riskDescriptor = "riskDescriptor" in gate ? gate.riskDescriptor : undefined;
141
+ const classes = screenAskClassMembers(pending.askClass, pending.classes)?.classes;
119
142
  return {
120
143
  governanceForced: governanceOriginOf({ riskDescriptor }, governance) === true,
121
144
  requiresRealApproval: readParkRowTrueSeat(pending, "requiresRealApproval"),
122
145
  mandated: readParkRowTrueSeat(pending, "mandated") || mandatedFromSeat(readRuleOffersAbsenceSeat(pending)),
123
146
  grantBinding: grantBindingOf(pending),
124
- classes: screenAskClassMembers(pending.askClass, pending.classes)?.classes,
147
+ classes,
148
+ deploymentListQuestion: carriesDeploymentListQuestion(deploymentListAsks, classes),
125
149
  };
126
150
  }
127
151
  function pendingEntryStandingAnswerFacts(entry) {
@@ -131,6 +155,7 @@ function pendingEntryStandingAnswerFacts(entry) {
131
155
  mandated: entry.mandated === true,
132
156
  grantBinding: entry.grantBinding,
133
157
  classes: entry.classes,
158
+ deploymentListQuestion: entry.deploymentListQuestion === true,
134
159
  };
135
160
  }
136
161
  function mandatedFromSeat(seat) {
@@ -151,6 +176,11 @@ export const READ_ROOT_GRANT_ALLOW_ONLY = 'readRootGrant is only meaningful with
151
176
  export const READ_ROOT_GRANT_SHAPE = `readRootGrant must be the card's readRootCandidate.dir sent back byte for byte — a non-empty string of at most ${READ_ROOT_CANDIDATE_DIR_MAX} characters`;
152
177
  export const PERSIST_RULE_SCOPE_REFUSED = "this endpoint does not accept a rule scope — the server mints it from where the approval happened (send neither `scope` nor `persistRule.scope`)";
153
178
  export function parseToolApprovalResponse(body) {
179
+ const retired = retiredDecisionKeyFacts(body);
180
+ const parsed = parseToolApprovalFields(body, retired);
181
+ return parsed.ok || retired === undefined ? parsed : { ...parsed, ...retired.extra };
182
+ }
183
+ function parseToolApprovalFields(body, retired) {
154
184
  if (body === null || typeof body !== "object" || Array.isArray(body))
155
185
  return { ok: false, error: "body must be an object" };
156
186
  const d = body.decision;
@@ -193,8 +223,8 @@ export function parseToolApprovalResponse(body) {
193
223
  }
194
224
  note = rawNote;
195
225
  }
196
- if (body.settledBy !== undefined)
197
- return { ok: false, error: SETTLED_BY_RETIRED };
226
+ if (retired !== undefined)
227
+ return { ok: false, error: retired.error };
198
228
  const decidedByReading = readDecidedBy(body.decidedBy, false);
199
229
  let decidedBy;
200
230
  switch (decidedByReading.kind) {
@@ -386,6 +416,7 @@ export class ToolApprovalCoordinator {
386
416
  parkedRedeem;
387
417
  askAudit;
388
418
  sessionGrants;
419
+ deploymentListAsks;
389
420
  shutdownDraining = false;
390
421
  asksInFlight = 0;
391
422
  parkCommits;
@@ -407,6 +438,10 @@ export class ToolApprovalCoordinator {
407
438
  this.parkTombstonePerPrincipalMax = opts?.parkTombstonePerPrincipalMax ?? DEFAULT_PARK_TOMBSTONE_PER_PRINCIPAL_MAX;
408
439
  this.askAudit = opts?.askAudit;
409
440
  this.sessionGrants = opts?.sessionGrants;
441
+ if (opts?.deploymentListAsks !== undefined && opts.sessionGrants === undefined) {
442
+ throw new Error("ToolApprovalCoordinator: deploymentListAsks needs sessionGrants — the deployment list's standing answers live in the session exemption store");
443
+ }
444
+ this.deploymentListAsks = opts?.deploymentListAsks;
410
445
  this.parkCommits = opts?.parkCommits;
411
446
  this.parkCommits?.onParkCommitted((calls) => this.noteParkCommitted(calls));
412
447
  }
@@ -758,9 +793,19 @@ export class ToolApprovalCoordinator {
758
793
  return (this.governanceAskMarks ?? governanceAskMarksFor(this.streamKey(origin.owner, origin.sessionId, origin.taskId))).isMarked(req.toolCallId);
759
794
  }
760
795
  async standingGrantSettles(origin, req, governanceForced) {
761
- const facts = askStandingAnswerFacts(req, governanceForced);
762
- if (!standingAnswerMayRetire(facts))
763
- return false;
796
+ const facts = askStandingAnswerFacts(req, governanceForced, this.deploymentListAsks?.(req) === true);
797
+ const home = standingAnswerHome(facts);
798
+ switch (home) {
799
+ case "seat":
800
+ break;
801
+ case "none":
802
+ case "server_store":
803
+ return false;
804
+ default: {
805
+ const unhandled = home;
806
+ throw new Error(`unhandled standing-answer home: ${String(unhandled)}`);
807
+ }
808
+ }
764
809
  const sessionId = origin.sessionId;
765
810
  if (!sessionId)
766
811
  return false;
@@ -771,7 +816,7 @@ export class ToolApprovalCoordinator {
771
816
  if (!(await this.sessionGrantUncontestedByRow(origin, req)))
772
817
  return false;
773
818
  if (durable)
774
- defaultLogger.info("approval_exempted", { sessionId, toolName: req.toolName, seat: "approval_seat" });
819
+ defaultLogger.info(APPROVAL_EXEMPTED_EVENT, { sessionId, toolName: req.toolName, seat: "approval_seat" });
775
820
  return true;
776
821
  }
777
822
  async durableGrantHas(sessionId, canonicalToolName) {
@@ -1052,6 +1097,7 @@ export class ToolApprovalCoordinator {
1052
1097
  if (signal?.aborted)
1053
1098
  return false;
1054
1099
  const governanceForced = this.governanceForcedOf(origin, req);
1100
+ const deploymentListAsks = this.deploymentListAsks?.(req) === true;
1055
1101
  if (await this.standingGrantSettles(origin, req, governanceForced))
1056
1102
  return true;
1057
1103
  const aliveCtxs = ctxs.filter((c) => !c.abortSignal?.aborted);
@@ -1107,7 +1153,7 @@ export class ToolApprovalCoordinator {
1107
1153
  ...(governanceForced ? { governanceForced: true } : {}),
1108
1154
  ...(req.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
1109
1155
  ...(req.mandated === true ? { mandated: true } : {}),
1110
- ...(standingAnswerMayRetire(askStandingAnswerFacts(req, governanceForced)) ? {} : { rememberRefused: true }),
1156
+ ...(standingAnswerHome(askStandingAnswerFacts(req, governanceForced, deploymentListAsks)) === "none" ? { rememberRefused: true } : {}),
1111
1157
  ...(typeof req.boundGrantDigest === "string" ? { boundGrantDigest: req.boundGrantDigest, ...(req.boundGrants !== undefined ? { boundGrants: req.boundGrants } : {}) } : {}),
1112
1158
  ...(noRulingCause !== undefined ? { classifierNoRuling: noRulingCause } : {}),
1113
1159
  ...(mandateWord !== undefined ? { mandate: mandateWord } : {}),
@@ -1637,13 +1683,15 @@ export class ToolApprovalCoordinator {
1637
1683
  }
1638
1684
  const entryGrantBinding = grantBindingOf(req);
1639
1685
  const entryClasses = askClassPair?.classes;
1640
- const rememberRefused = !standingAnswerMayRetire(pendingEntryStandingAnswerFacts({
1686
+ const entryDeploymentListQuestion = carriesDeploymentListQuestion(deploymentListAsks, entryClasses);
1687
+ const rememberRefused = standingAnswerHome(pendingEntryStandingAnswerFacts({
1641
1688
  ...(effectiveGovernanceForced ? { governanceForced: true } : {}),
1642
1689
  ...(req.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
1643
1690
  ...(req.mandated === true ? { mandated: true } : {}),
1644
1691
  grantBinding: entryGrantBinding,
1645
1692
  classes: entryClasses,
1646
- }));
1693
+ ...(entryDeploymentListQuestion ? { deploymentListQuestion: true } : {}),
1694
+ })) === "none";
1647
1695
  if (rememberRefused)
1648
1696
  frame.rememberRefused = true;
1649
1697
  else
@@ -1665,6 +1713,7 @@ export class ToolApprovalCoordinator {
1665
1713
  toolName: req.toolName,
1666
1714
  grantBinding: entryGrantBinding,
1667
1715
  ...(entryClasses !== undefined ? { classes: entryClasses } : {}),
1716
+ ...(entryDeploymentListQuestion ? { deploymentListQuestion: true } : {}),
1668
1717
  registeredAtMs: Date.now(),
1669
1718
  expiresAtMs: persistedExpiresAtMs,
1670
1719
  ...(ruleLaneMaterial !== undefined ? { ruleLane: ruleLaneMaterial } : {}),
@@ -1794,7 +1843,7 @@ export class ToolApprovalCoordinator {
1794
1843
  respond(id, principal, body, httpReq) {
1795
1844
  const parsed = parseToolApprovalResponse(body);
1796
1845
  if (!parsed.ok)
1797
- return { status: 400, body: { error: parsed.error, errorCode: "request.body_shape" } };
1846
+ return { status: 400, body: { error: parsed.error, errorCode: "request.body_shape", ...(parsed.field !== undefined ? { field: parsed.field } : {}) } };
1798
1847
  const entry = this.pending.get(id);
1799
1848
  if (!entry || (entry.owner !== null && entry.owner !== principal)) {
1800
1849
  return this.respondLate(id, principal, parsed, httpReq);
@@ -1802,17 +1851,43 @@ export class ToolApprovalCoordinator {
1802
1851
  const preRule = this.precheckEditedRuleBeforeDecision(entry, parsed);
1803
1852
  if (preRule !== undefined)
1804
1853
  return preRule;
1805
- if (policyCannotAllow(parsed.decidedBy, parsed.value !== "deny", entry.requiresRealApproval === true)) {
1854
+ if (parsed.value !== "deny" && policyCannotAnswer(heldAskOf(entry.requiresRealApproval === true), parsed.decidedBy)) {
1806
1855
  return { status: 400, body: { error: POLICY_CANNOT_ALLOW_PERSON_ASK, errorCode: "request.body_shape" } };
1807
1856
  }
1808
1857
  const ruleEligibility = { eligible: true };
1809
- const settled = this.askStore && entry.askId !== undefined && entry.batchId !== undefined
1858
+ const decided = this.askStore && entry.askId !== undefined && entry.batchId !== undefined
1810
1859
  ? this.respondWithCas(this.askStore, entry.askId, entry.batchId, id, entry, parsed, ruleEligibility)
1811
1860
  : this.finishRespond(id, entry, parsed);
1861
+ const settled = this.recordDeploymentListAnswer(decided, entry, parsed, principal, ruleEligibility);
1812
1862
  if (parsed.persistRule === undefined)
1813
1863
  return settled;
1814
1864
  return this.persistRuleAfterDecision(settled, entry, parsed.persistRule, parsed.updatedInput !== undefined, ruleEligibility);
1815
1865
  }
1866
+ recordDeploymentListAnswer(decided, entry, parsed, principal, eligibility) {
1867
+ const store = this.sessionGrants;
1868
+ const sessionId = entry.sessionId;
1869
+ if (parsed.value !== "allow_session" || !sessionId || store === undefined)
1870
+ return decided;
1871
+ if (standingAnswerHome(pendingEntryStandingAnswerFacts(entry)) !== "server_store")
1872
+ return decided;
1873
+ return (async () => {
1874
+ const result = await decided;
1875
+ if (result.status !== 200)
1876
+ return result;
1877
+ if (!eligibility.eligible)
1878
+ return { ...result, body: { ...result.body, rememberApplied: false } };
1879
+ let rememberApplied = false;
1880
+ try {
1881
+ await this.withStoreDeadline(store.grant(sessionId, entry.toolName, principal ?? entry.owner), "sessionGrants.grant(deployment-list)", DURABLE_CALL_TIMEOUT_MS);
1882
+ rememberApplied = true;
1883
+ defaultLogger.info("approval_exemption_granted", { sessionId, toolName: entry.toolName, grantedBy: principal ?? entry.owner });
1884
+ }
1885
+ catch (err) {
1886
+ defaultLogger.warn("approval_exemption_grant_failed", { sessionId, toolName: entry.toolName, error: String(err).slice(0, 200) });
1887
+ }
1888
+ return { ...result, body: { ...result.body, rememberApplied } };
1889
+ })();
1890
+ }
1816
1891
  precheckEditedRuleBeforeDecision(entry, parsed) {
1817
1892
  const persistRule = parsed.persistRule;
1818
1893
  if (persistRule === undefined || persistRule.kind !== "text" || !persistRule.edited)
@@ -2255,7 +2330,21 @@ export class ToolApprovalCoordinator {
2255
2330
  }
2256
2331
  finishRespond(id, entry, parsed, opts) {
2257
2332
  let rememberApplied;
2258
- const grantable = standingAnswerMayRetire(pendingEntryStandingAnswerFacts(entry));
2333
+ const home = standingAnswerHome(pendingEntryStandingAnswerFacts(entry));
2334
+ let grantable;
2335
+ switch (home) {
2336
+ case "seat":
2337
+ grantable = true;
2338
+ break;
2339
+ case "none":
2340
+ case "server_store":
2341
+ grantable = false;
2342
+ break;
2343
+ default: {
2344
+ const unhandled = home;
2345
+ throw new Error(`unhandled standing-answer home: ${String(unhandled)}`);
2346
+ }
2347
+ }
2259
2348
  if (parsed.value === "allow_session")
2260
2349
  rememberApplied = Boolean(entry.sessionId) && grantable;
2261
2350
  if (parsed.value === "allow_session" && entry.sessionId && grantable) {
@@ -2299,7 +2388,7 @@ export class ToolApprovalCoordinator {
2299
2388
  ...(e.requiresRealApproval ? { requiresRealApproval: true } : {}),
2300
2389
  ...(e.governanceForced ? { governanceForced: true } : {}),
2301
2390
  ...(e.mandated ? { mandated: true } : {}),
2302
- ...(standingAnswerMayRetire(pendingEntryStandingAnswerFacts(e)) ? {} : { rememberRefused: true }),
2391
+ ...(standingAnswerHome(pendingEntryStandingAnswerFacts(e)) === "none" ? { rememberRefused: true } : {}),
2303
2392
  ...(e.mandate !== undefined ? { mandate: e.mandate } : {}),
2304
2393
  ...(e.origin !== undefined ? { origin: e.origin } : {}),
2305
2394
  ...(e.originTaskId !== undefined ? { fromSubagent: true, originTaskId: e.originTaskId } : {}),
@@ -16,7 +16,7 @@
16
16
  * 同文件 G1 e2e 的全码扇出夹具、以及契约文档附录 D.3 那张表(含**小节标题里的码数** —— 那道门本批
17
17
  * 刚立,立完就在下一次加码时自己咬住了)。
18
18
  */
19
- export declare const ENGINE_NOTICE_WIRE_CODES: readonly ["memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "task.turn_interrupted", "steering.parked_input_blocked", "task.user_steer_undrained", "task.user_followup_undrained", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "task.halt_unconsumed", "task.late_approval", "config.durable_gate_unavailable", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "mcp.injection_dropped", "delegation.ask_unresolvable", "task.interrupt_unconsumed", "approval.read_root_granted", "approval.read_root_grant_rejected", "instructions.source_changed", "session.records_skipped", "server.model_unreachable", "classifier.unavailable", "config.permission_mode_unavailable", "permission.ask_released_by_mode", "permission.read_deny_unapplied", "permission.rule_store_unavailable", "permission.allowed_tool_not_granted", "task.tool_turn_incomplete"];
19
+ export declare const ENGINE_NOTICE_WIRE_CODES: readonly ["memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "task.turn_interrupted", "steering.parked_input_blocked", "task.user_steer_undrained", "task.user_followup_undrained", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "task.halt_unconsumed", "task.late_approval", "config.durable_gate_unavailable", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "mcp.injection_dropped", "delegation.ask_unresolvable", "task.interrupt_unconsumed", "approval.read_root_granted", "approval.read_root_grant_rejected", "instructions.source_changed", "session.records_skipped", "server.model_unreachable", "classifier.unavailable", "config.permission_mode_unavailable", "permission.ask_released_by_mode", "permission.read_deny_unapplied", "permission.rule_store_unavailable", "permission.rule_row_dropped", "permission.allowed_tool_not_granted", "task.tool_turn_incomplete"];
20
20
  export type EngineNoticeWireCode = (typeof ENGINE_NOTICE_WIRE_CODES)[number];
21
21
  /** 白名单谓词(单点):路由与门都读这一个,不许第二处手抄码串。 */
22
22
  export declare function isEngineNoticeWireCode(code: string): code is EngineNoticeWireCode;
@@ -63,10 +63,12 @@ export interface EngineNoticeWireRow {
63
63
  */
64
64
  export declare function sanitizeNoticeDetail(detail: Record<string, unknown> | undefined): Record<string, unknown>;
65
65
  /**
66
- * 通告 `detail` 里的**规则条目原串**过呈现口(`presentRuleEntry`:剥 C0 / 双向控制符、超长截断)—— 会话流行(经 {@link engineNoticeEventData})
67
- * 与运维日志行(`boot/runner-deps.ts` 的转发席)同调这一只。其余字段、其余码原样(多行文本不受影响)。
66
+ * 通告 `detail` 里的**规则文本**换成条目的脱敏原值({@link redactListEntry}:凭据换记号,不加引号、不转义 —— 机读位不是显示面,显示形只属于人读句)。
67
+ * 会话流行(经 {@link engineNoticeEventData},之后照常过 {@link sanitizeNoticeDetail})与运维日志行(`boot/runner-deps.ts` 的转发席,其余字段逐字)
68
+ * 同调这一只 ⇒ 两条出路上同一条规则文本逐字节同(会话流行另有 detail 串的 {@link NOTICE_TEXT_MAX} 字上限,超长的那一截只在日志行上),两只码里同一条
69
+ * 规则文本也同(引擎交来的两形本身一致时 —— `rule_row_dropped` 交的是引擎中和过的形,空白 / 超长的条目两只码今天不同,上游缝)。其余字段、其余码原样。
68
70
  */
69
- export declare function presentNoticeDetail(code: string, detail: Record<string, unknown> | undefined): Record<string, unknown> | undefined;
71
+ export declare function redactNoticeRuleText(code: string, detail: Record<string, unknown> | undefined): Record<string, unknown> | undefined;
70
72
  export declare function engineNoticeEventData(notice: {
71
73
  code: string;
72
74
  message: string;
@@ -1,5 +1,5 @@
1
1
  import { redactSecrets } from "./redact.js";
2
- import { presentRuleEntry } from "../rule-entry-list.js";
2
+ import { redactListEntry } from "../list-entry-display.js";
3
3
  import { recordFailOpen } from "../observability/fail-open.js";
4
4
  export const ENGINE_NOTICE_WIRE_CODES = [
5
5
  "memory.session_polluted",
@@ -34,6 +34,7 @@ export const ENGINE_NOTICE_WIRE_CODES = [
34
34
  "permission.ask_released_by_mode",
35
35
  "permission.read_deny_unapplied",
36
36
  "permission.rule_store_unavailable",
37
+ "permission.rule_row_dropped",
37
38
  "permission.allowed_tool_not_granted",
38
39
  "task.tool_turn_incomplete",
39
40
  ];
@@ -92,22 +93,28 @@ function sanitizeValue(v, depth) {
92
93
  }
93
94
  return undefined;
94
95
  }
95
- const RULE_ENTRY_DETAIL_ARRAYS = { "permission.allowed_tool_not_granted": "entries" };
96
- export function presentNoticeDetail(code, detail) {
97
- const key = Object.hasOwn(RULE_ENTRY_DETAIL_ARRAYS, code) ? RULE_ENTRY_DETAIL_ARRAYS[code] : undefined;
98
- if (key === undefined || detail === undefined)
96
+ const RULE_TEXT_DETAIL_FIELDS = {
97
+ "permission.allowed_tool_not_granted": { array: "entries", field: "entry" },
98
+ "permission.rule_row_dropped": { array: "rows", field: "rule" },
99
+ };
100
+ export function redactNoticeRuleText(code, detail) {
101
+ const at = Object.hasOwn(RULE_TEXT_DETAIL_FIELDS, code) ? RULE_TEXT_DETAIL_FIELDS[code] : undefined;
102
+ if (at === undefined || detail === undefined)
99
103
  return detail;
100
- const list = detail[key];
104
+ const list = detail[at.array];
101
105
  if (!Array.isArray(list))
102
106
  return detail;
103
- const presented = list.map((item) => item !== null && typeof item === "object" && typeof item.entry === "string" ? { ...item, entry: presentRuleEntry(item.entry) } : item);
104
- return { ...detail, [key]: presented };
107
+ const redacted = list.map((item) => {
108
+ const text = item !== null && typeof item === "object" ? item[at.field] : undefined;
109
+ return typeof text === "string" ? { ...item, [at.field]: redactListEntry(text) } : item;
110
+ });
111
+ return { ...detail, [at.array]: redacted };
105
112
  }
106
113
  export function engineNoticeEventData(notice, sessionId, nowMs) {
107
114
  return {
108
115
  code: notice.code,
109
116
  message: redactSecrets(String(notice.message)).slice(0, NOTICE_TEXT_MAX),
110
- detail: sanitizeNoticeDetail(presentNoticeDetail(notice.code, notice.detail)),
117
+ detail: sanitizeNoticeDetail(redactNoticeRuleText(notice.code, notice.detail)),
111
118
  sessionId,
112
119
  ts: nowMs,
113
120
  };
@@ -50,6 +50,7 @@ import { stripFormatCharacters } from "@sema-agent/core";
50
50
  * 现役消费方:`src/tool-approval.ts` 的 `durableRuleMaterial`(规则车道素材闸)与语料的 FP=0 格。
51
51
  */
52
52
  export declare const normalizeForRedaction: typeof stripFormatCharacters;
53
+ export declare const REDACTION_TOKEN_RE_SRC: string;
53
54
  export declare function armPoolBudgetForTest(): {
54
55
  spanCount: number;
55
56
  coveredPositions: number;
@@ -163,6 +164,11 @@ export declare function redactionSpansForTest(text: string, opts?: {
163
164
  * 让下游看到"这里有东西被省略了",而不是丢一个空对象或炸掉整条流。
164
165
  */
165
166
  export declare function redactDeep(value: unknown): unknown;
167
+ /** {@link redactDeep} 的**串叶处置可注入**形:同一只环 / 深度守卫的遍历,只有「一个串叶怎么脱」由调用方给。
168
+ * `redactDeep` 本身就是 `leaf = redactSecrets` 的那一档(行为逐字不变)。读面要的是另一档 —— 串叶走
169
+ * `redactModelText`(认出凭据形才改写字节,不含凭据形的串逐字节不变),遍历不该因此再写第二份。
170
+ * `leaf` 必须是纯的串→串函数且**自己不抛**(抛了就穿出本遍历)。 */
171
+ export declare function redactDeepWith(value: unknown, leaf: (s: string) => string): unknown;
166
172
  /**
167
173
  * A REDACTED, length-BOUNDED string preview of a structured value — for surfacing a pending tool call's `args`
168
174
  * in the assistant inbox ("what is this ask about") WITHOUT leaking secrets or dumping an unbounded payload (a
@@ -1,7 +1,7 @@
1
1
  import { displayUrlOf, stripFormatCharacters } from "@sema-agent/core";
2
2
  import { recordFailOpen } from "../observability/fail-open.js";
3
3
  export const normalizeForRedaction = stripFormatCharacters;
4
- const REDACTION_TOKEN_RE_SRC = String.raw `«redacted(?::[a-z-]{1,16}){0,2}»`;
4
+ export const REDACTION_TOKEN_RE_SRC = String.raw `«redacted(?::[a-z-]{1,16}){0,2}»`;
5
5
  const ARM_VALUE_CLASSES = [];
6
6
  const atLeast = (cls, min) => {
7
7
  ARM_VALUE_CLASSES.push(cls);
@@ -1797,11 +1797,14 @@ export function redactionSpansForTest(text, opts) {
1797
1797
  }
1798
1798
  const REDACT_MAX_DEPTH = 64;
1799
1799
  export function redactDeep(value) {
1800
- return redactDeepInner(value, 0, new WeakSet());
1800
+ return redactDeepWith(value, redactSecrets);
1801
1801
  }
1802
- function redactDeepInner(value, depth, seen) {
1802
+ export function redactDeepWith(value, leaf) {
1803
+ return redactDeepInner(value, 0, new WeakSet(), leaf);
1804
+ }
1805
+ function redactDeepInner(value, depth, seen, leaf) {
1803
1806
  if (typeof value === "string")
1804
- return redactSecrets(value);
1807
+ return leaf(value);
1805
1808
  if (!value || typeof value !== "object")
1806
1809
  return value;
1807
1810
  if (seen.has(value))
@@ -1811,11 +1814,8 @@ function redactDeepInner(value, depth, seen) {
1811
1814
  seen.add(value);
1812
1815
  try {
1813
1816
  if (Array.isArray(value))
1814
- return value.map((v) => redactDeepInner(v, depth + 1, seen));
1815
- const out = {};
1816
- for (const [k, v] of Object.entries(value))
1817
- out[k] = redactDeepInner(v, depth + 1, seen);
1818
- return out;
1817
+ return value.map((v) => redactDeepInner(v, depth + 1, seen, leaf));
1818
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, redactDeepInner(v, depth + 1, seen, leaf)]));
1819
1819
  }
1820
1820
  finally {
1821
1821
  seen.delete(value);
@@ -21,7 +21,7 @@ export declare const SELF_MINTED_WIRE: {
21
21
  /**
22
22
  * S-928(7.110.0):`engine_notice` `task.user_steer_undrained` 上**本仓铸**的那一臂。词表属主是 core(`USER_INPUT_UNDRAINED_CAUSES`,两族共用;
23
23
  * `leg_end` 由引擎铸、逐字过境,不在本表);本表登记的是「这个值自哪一版起由本服务出现在 wire 上」。`satisfies Record<CrashedCause, …>`:
24
- * core 删掉 `crashed` ⇒ `CrashedCause` 成 never ⇒ 本行编译红。码按受理时的车道选(`user-input-ledger.ts` `userInputLaneOf`):本行 = steer 车道。
24
+ * core 删掉 `crashed` ⇒ `CrashedCause` 成 never ⇒ 本行编译红。码按受理时的车道选(`user-input-ledger.ts` `acceptedInputKindOf`,车道 = core `injectionLaneOf`,码 = core `USER_INPUT_UNDRAINED_FAMILIES`):本行 = steer 车道。
25
25
  */
26
26
  readonly userSteerUndrainedServerCause: {
27
27
  readonly at: "engine_notice task.user_steer_undrained detail.cause(本服务铸的臂;下一条同会话 run 开通告口那一拍)";
@@ -183,6 +183,20 @@ export declare const SELF_MINTED_WIRE: {
183
183
  readonly attr: "redactErrorMessage";
184
184
  readonly since: "7.108.0";
185
185
  };
186
+ /**
187
+ * 7.120.0:通告 `detail` 里的规则文本 = 条目的脱敏原值(`list-entry-display.ts` `redactListEntry`,经 `engine-notice-wire.ts` 的
188
+ * `redactNoticeRuleText`;会话流行与运维日志行同一只;不加引号、不转义 —— 显示形只在人读句里)。修前(7.119.0):本仓手抄的呈现形
189
+ * (剥 C0 / 双向控制符、超过 160 字截断并标原长),运维日志行不脱敏。钉 test/notice-entry-presentation.test.ts。
190
+ */
191
+ readonly "engine_notice permission.allowed_tool_not_granted#detail.entries[].entry": {
192
+ readonly attr: "redactListEntry";
193
+ readonly since: "7.120.0";
194
+ };
195
+ /** 7.120.0:同上一行同一只写法(码本版入册;引擎交来的是它中和过的规则文本,本仓再过脱敏读口)。钉同上「两只码」组。 */
196
+ readonly "engine_notice permission.rule_row_dropped#detail.rows[].rule": {
197
+ readonly attr: "redactListEntry";
198
+ readonly since: "7.120.0";
199
+ };
186
200
  };
187
201
  };
188
202
  };
@@ -65,6 +65,8 @@ export const SELF_MINTED_WIRE = {
65
65
  members: {
66
66
  "GET /v1/runs/:id#result.errorMessage": { attr: "redactErrorMessage", since: "7.57.0" },
67
67
  "POST /v1/side-query#errorMessage": { attr: "redactErrorMessage", since: "7.108.0" },
68
+ "engine_notice permission.allowed_tool_not_granted#detail.entries[].entry": { attr: "redactListEntry", since: "7.120.0" },
69
+ "engine_notice permission.rule_row_dropped#detail.rows[].rule": { attr: "redactListEntry", since: "7.120.0" },
68
70
  },
69
71
  },
70
72
  };
@@ -23,8 +23,9 @@
23
23
  * 子代 resume(`…/subagents/:target/resume`)不往在飞的队列里投,起的是一条新的子代腿;follow-up 在本仓**没有**独立的受理口(`src/http/routes` 零
24
24
  * follow-up 入口)。steer 的 `later` 档在引擎里走 follow-up 车道(引擎在腿收场时按车道分账:没投出去的 `later` 记进 `task.user_followup_undrained`),
25
25
  * 但它仍是一次 `TaskStream.steer` 受理、消费帧同为 `human_input{delivery:"applied"}`(两车道同一只消费口)⇒ 同进本账,消费即结清。
26
- * **族 = 受理时它排进的引擎车道**({@link UserInputKind}:`steer` / `follow_up`,由档位经 {@link userInputLaneOf} 一只函数得出),崩溃告知的码按族选
27
- * (core 7.44.0 #1362:两族同一只成因词表 `USER_INPUT_UNDRAINED_CAUSES`,服务层的 `crashed` 臂按车道选码,与引擎自己的 `leg_end` 臂同一种分账)。
26
+ * **族 = 受理时它排进的引擎车道**(core `UserInputLane`,经引擎的 `injectionLaneOf(steerPriorityOf(…))` 读出 —— {@link acceptedInputKindOf};落盘 / wire 词 {@link UserInputKind}:
27
+ * `steer` / `follow_up`),崩溃告知的码与条数键按车道取 core `USER_INPUT_UNDRAINED_FAMILIES`(core 7.44.0 #1362:两族同一只成因词表 `USER_INPUT_UNDRAINED_CAUSES`,
28
+ * 服务层的 `crashed` 臂按车道选族,与引擎自己的 `leg_end` 臂同一种分账;core 7.45.0 #1408 起族表与车道读法在包根,本仓不再抄)。
28
29
  *
29
30
  * ## 五形行(append-only;行序 = 语义序;File 孪生的盘上形 = 下面的 {@link UserInputLedgerRecord},SQL 孪生 = 一行一受理 + CAS 列 + 删行,同义)
30
31
  * · `accepted` —— 受理点(`routes/runs.ts` 的 live 腿,**调 `TaskStream.steer` 之前**、回 200 之前)写;写不进 ⇒ 503
@@ -85,28 +86,30 @@
85
86
  * 不开旋钮(没有「关掉告知」的正当需求)· 不改 steer 的 200 体(回执仍是「已交付」,账本是它的影子)· 不重放丢失的输入(core 明写
86
87
  * NOT redelivered;本账只告知,不替人决定重发)· 不存输入正文(行里只有身份与时刻)。
87
88
  */
88
- import { type EngineNoticeCode, type SystemInjectionPriority, type TaskEvent, type UserInputUndrainedCause } from "@sema-agent/core";
89
+ import { type EngineNoticeCode, type SystemInjectionPriority, type TaskEvent, type UserInputLane, type UserInputUndrainedCause } from "@sema-agent/core";
89
90
  import { type RunRecord } from "./plugins/store-contracts.js";
90
91
  import { type EngineNoticeRouter } from "./trace/engine-notice-wire.js";
91
92
  /** local 数据根下的子目录(File 孪生;与 `approval-ask-audit/` 同层同形)。 */
92
93
  export declare const USER_INPUT_LEDGER_DIR = "user-input-ledger";
93
94
  /**
94
- * 账本收的输入族(闭集)= 受理时它排进的**引擎车道**:`steer`(`now` / `next` 档)/ `follow_up`(`later` 档)。崩溃告知的码按族选
95
- * ({@link UNDRAINED_NOTICE_OF}),读面行的 `kind` 就是它。`follow_up` 自 7.118.0 起写(之前的版本受理口一律写 `steer`,`later` 档的旧行照它当时的族读)。
95
+ * 账本收的输入族 = 受理时它排进的**引擎车道**(core `UserInputLane`:`steer` / `followUp`;车道怎么读、每条车道的告知码与条数键全归 core,见下)。
96
+ * 本表只做一件事:把车道写成本仓 7.118.0 起**已落盘、已上 wire**(`GET /v1/sessions/:id` 的 `userInputUndrained[].kind`)的那个词 —— `steer` / `follow_up`。
97
+ * `satisfies Record<UserInputLane, …>`:core 加一条车道 ⇒ 编译红,由人裁它落盘写什么。`follow_up` 自 7.118.0 起写(之前的版本受理口一律写 `steer`,`later` 档的旧行照它当时的族读)。
96
98
  */
97
- export type UserInputKind = "steer" | "follow_up";
98
- /** 读盘词表(`satisfies Record<…, true>` ⇒ 闭集加词而本表没跟 = 编译红;判定走 `Object.hasOwn`)。≤7.117.x 的读腿不认 `follow_up`(回滚须知在 MIGRATION)。 */
99
- export declare const USER_INPUT_KIND_WORDS: {
100
- readonly steer: true;
101
- readonly follow_up: true;
99
+ declare const LEDGER_KIND_OF_LANE: {
100
+ readonly steer: "steer";
101
+ readonly followUp: "follow_up";
102
102
  };
103
+ /** 落盘 / wire 的族词(闭集,派生自 {@link LEDGER_KIND_OF_LANE})。 */
104
+ export type UserInputKind = (typeof LEDGER_KIND_OF_LANE)[UserInputLane];
105
+ /** 读盘词表(判定走 `Object.hasOwn`;派生自 {@link LEDGER_KIND_OF_LANE})。≤7.117.x 的读腿不认 `follow_up`(回滚须知在 MIGRATION)。 */
106
+ export declare const USER_INPUT_KIND_WORDS: Readonly<Record<UserInputKind, true>>;
103
107
  /**
104
- * 一次受理排进哪条引擎车道 —— 三个受理口(run steer 的 live 腿 / interrupt 带 text 形 / 子代 steer)同调这一只。core 原句(`TaskStream.steer` 注):
105
- * 「a `"later"` steer rides the followUp lane, so its stranded account is `task.user_followup_undrained`」;`now` / `next` 走 steer 车道。
106
- * 档位型取 core 包根的 `SystemInjectionPriority`(`TaskStream.steer` 的 `priority`;缺席 = 引擎缺省 `next`),本仓不复述:core 加一档 ⇒ 下面的
107
- * 穷举 `never` 臂编译红,由人裁新档排进哪条车道。
108
+ * 一次受理的族词 —— 三个受理口(run steer 的 live 腿 / interrupt 带 text 形 / 子代 steer)同调这一只。车道是引擎的那一只读法
109
+ * `injectionLaneOf(steerPriorityOf(…))`(core 7.45.0 #1408:缺席档 = `next`;`later` 走 follow-up 车道,`now` / `next` 走 steer 车道;
110
+ * 档外值抛 `steering.invalid_content` —— 受理口在这之前已按 `isSystemInjectionPriority` 400 掉档外值,这一抛在本仓不可达)。
108
111
  */
109
- export declare function userInputLaneOf(tier: SystemInjectionPriority | undefined): UserInputKind;
112
+ export declare function acceptedInputKindOf(priority: SystemInjectionPriority | undefined): UserInputKind;
110
113
  /** `settled` 的两种收场(闭集,见顶注)。 */
111
114
  export type SettleHow = "leg_end" | "not_applied" | "consumed";
112
115
  /** 读盘词表(File 孪生的折叠守卫 / SQL 孪生的 `settle_how` 列读腿同读这一张;`consumed` 自 7.113.0 起,≤7.112.x 的读腿不认它 —— 回滚须知在 MIGRATION)。 */
@@ -380,4 +383,5 @@ export declare function createUserInputLedgerAtBoot(opts: Parameters<typeof crea
380
383
  * · `mysql` / `pg` —— 多副本共享一张表,「我启动之前写的」证明不了写者已死 ⇒ `run_store_verdict`(等 reaper 落款)。
381
384
  */
382
385
  export declare function writerGoneEvidenceFor(backendKind: "mysql" | "pg" | "local", runs: RunStoreReader): WriterGoneEvidence;
386
+ export {};
383
387
  //# sourceMappingURL=user-input-ledger.d.ts.map
@@ -1,22 +1,13 @@
1
- import { USER_INPUT_UNDRAINED_CAUSES } from "@sema-agent/core";
1
+ import { USER_INPUT_UNDRAINED_CAUSES, USER_INPUT_UNDRAINED_FAMILIES, injectionLaneOf, steerPriorityOf } from "@sema-agent/core";
2
2
  import { recordFailOpen } from "./observability/fail-open.js";
3
3
  import { isHolderDeathVerdict } from "./plugins/store-contracts.js";
4
4
  import { defaultEngineNoticeRouter } from "./trace/engine-notice-wire.js";
5
5
  export const USER_INPUT_LEDGER_DIR = "user-input-ledger";
6
- export const USER_INPUT_KIND_WORDS = { steer: true, follow_up: true };
7
- export function userInputLaneOf(tier) {
8
- switch (tier) {
9
- case "later":
10
- return "follow_up";
11
- case "now":
12
- case "next":
13
- case undefined:
14
- return "steer";
15
- default: {
16
- const unhandled = tier;
17
- throw new Error(`unhandled steer tier: ${String(unhandled)}`);
18
- }
19
- }
6
+ const LEDGER_KIND_OF_LANE = { steer: "steer", followUp: "follow_up" };
7
+ const LANE_OF_LEDGER_KIND = Object.fromEntries(Object.entries(LEDGER_KIND_OF_LANE).map(([lane, kind]) => [kind, lane]));
8
+ export const USER_INPUT_KIND_WORDS = Object.freeze(Object.fromEntries(Object.values(LEDGER_KIND_OF_LANE).map((kind) => [kind, true])));
9
+ export function acceptedInputKindOf(priority) {
10
+ return LEDGER_KIND_OF_LANE[injectionLaneOf(steerPriorityOf(priority === undefined ? {} : { priority }))];
20
11
  }
21
12
  export const SETTLE_HOW_WORDS = { leg_end: true, not_applied: true, consumed: true };
22
13
  const UNDRAINED_CAUSE_MINTER = { leg_end: "engine", crashed: "server" };
@@ -138,15 +129,16 @@ export function sortAcceptances(list) {
138
129
  export const USER_INPUT_OPEN_SCAN_MAX = 1000;
139
130
  export const USER_INPUT_OPEN_SCAN_PAGES = 100;
140
131
  export const USER_INPUT_SESSION_READ_MAX = 500;
141
- const UNDRAINED_NOTICE_OF = {
142
- steer: { code: "task.user_steer_undrained", countKey: "steer", what: "user steer(s)" },
143
- follow_up: { code: "task.user_followup_undrained", countKey: "followUp", what: "user steer(s) queued for the run's end (priority later)" },
132
+ const UNDRAINED_WHAT_OF_LANE = {
133
+ steer: "user steer(s)",
134
+ followUp: "user steer(s) queued for the run's end (priority later)",
144
135
  };
145
136
  export function buildCrashedUndrainedNotice(input) {
146
- const face = UNDRAINED_NOTICE_OF[input.family];
137
+ const lane = LANE_OF_LEDGER_KIND[input.family];
138
+ const face = USER_INPUT_UNDRAINED_FAMILIES[lane];
147
139
  return {
148
140
  code: face.code,
149
- message: `Check the earlier run's transcript and results before re-sending any of these steers to a live run. The server accepted ${input.count} ${face.what} for run ${input.taskId} (each answered 200 applied or 202 queued-immediate), but the process running that run ended before the engine confirmed consuming them. They are NOT redelivered.`,
141
+ message: `Check the earlier run's transcript and results before re-sending any of these steers to a live run. The server accepted ${input.count} ${UNDRAINED_WHAT_OF_LANE[lane]} for run ${input.taskId} (each answered 200 applied or 202 queued-immediate), but the process running that run ended before the engine confirmed consuming them. They are NOT redelivered.`,
150
142
  detail: { [face.countKey]: input.count, cause: CRASHED_CAUSE, taskId: input.taskId, sessionId: input.sessionId },
151
143
  sessionId: input.sessionId,
152
144
  };