@sema-agent/server 7.148.0 → 7.149.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 (247) hide show
  1. package/MIGRATION.md +429 -0
  2. package/README.md +16 -1
  3. package/README.zh-CN.md +4 -0
  4. package/USAGE.md +89 -55
  5. package/dist/adoption/wire.d.ts +3 -3
  6. package/dist/approval-card.d.ts +32 -32
  7. package/dist/approval-leg-origin.d.ts +33 -0
  8. package/dist/approval-leg-origin.js +4 -0
  9. package/dist/auth-bridge.d.ts +9 -1
  10. package/dist/auth-bridge.js +4 -4
  11. package/dist/auth-keys.d.ts +10 -1
  12. package/dist/auth-keys.js +8 -0
  13. package/dist/boot/execution-env.js +10 -11
  14. package/dist/boot/execution-lane.d.ts +6 -4
  15. package/dist/boot/execution-lane.js +3 -1
  16. package/dist/boot/org-delivery.d.ts +3 -2
  17. package/dist/boot/resolve-spec.js +6 -4
  18. package/dist/boot/runner-deps.js +3 -1
  19. package/dist/boot/stage-01-config.d.ts +1 -0
  20. package/dist/boot/stage-01-config.js +4 -3
  21. package/dist/boot/stage-02-web-admin-pull.d.ts +1 -0
  22. package/dist/boot/stage-02-web-admin-pull.js +10 -2
  23. package/dist/boot/stage-03-store-backend.d.ts +1 -0
  24. package/dist/boot/stage-03-store-backend.js +2 -2
  25. package/dist/boot/stage-04-stores.d.ts +1 -0
  26. package/dist/boot/stage-05-execution-env.d.ts +1 -0
  27. package/dist/boot/stage-06-runners.d.ts +2 -1
  28. package/dist/boot/stage-07-capability-layer.d.ts +2 -1
  29. package/dist/boot/stage-08-reapers.d.ts +2 -1
  30. package/dist/boot/stage-09-leader.d.ts +2 -1
  31. package/dist/boot/stage-10-http-server.d.ts +2 -1
  32. package/dist/boot/stores.d.ts +3 -0
  33. package/dist/boot/stores.js +22 -19
  34. package/dist/capabilities/tool-defer.d.ts +1 -1
  35. package/dist/capability-facts.d.ts +5 -4
  36. package/dist/capability-facts.js +3 -14
  37. package/dist/card-settle-stored-scope.d.ts +56 -0
  38. package/dist/card-settle-stored-scope.js +27 -0
  39. package/dist/card-void.d.ts +7 -12
  40. package/dist/card-void.js +2 -7
  41. package/dist/config-catalog.d.ts +12 -0
  42. package/dist/config-catalog.js +36 -32
  43. package/dist/config-invariants.d.ts +5 -4
  44. package/dist/config-invariants.js +4 -4
  45. package/dist/config-types.d.ts +37 -5
  46. package/dist/config.d.ts +6 -5
  47. package/dist/config.js +35 -24
  48. package/dist/context-window-notice.d.ts +31 -0
  49. package/dist/context-window-notice.js +28 -0
  50. package/dist/deployment-shape.d.ts +20 -11
  51. package/dist/deployment-shape.js +1 -0
  52. package/dist/env-name-allowlist-knobs.d.ts +6 -5
  53. package/dist/env-name-allowlist-knobs.js +5 -0
  54. package/dist/execution-lane-caps.d.ts +20 -5
  55. package/dist/execution-lane-caps.js +18 -4
  56. package/dist/hooks/command-spawn.d.ts +7 -2
  57. package/dist/hooks/command-spawn.js +13 -6
  58. package/dist/hooks/hook-runner.d.ts +5 -0
  59. package/dist/hooks/hook-runner.js +11 -7
  60. package/dist/http/admission.js +3 -3
  61. package/dist/http/approval-sweeps.js +3 -2
  62. package/dist/http/claim-heartbeat.d.ts +3 -3
  63. package/dist/http/claim-heartbeat.js +3 -3
  64. package/dist/http/claimed-run.d.ts +16 -9
  65. package/dist/http/claimed-run.js +8 -0
  66. package/dist/http/dispatch.js +5 -3
  67. package/dist/http/durable-run-leg.d.ts +8 -1
  68. package/dist/http/durable-run-leg.js +30 -11
  69. package/dist/http/execution-context.d.ts +115 -14
  70. package/dist/http/execution-context.js +130 -40
  71. package/dist/http/owner-access.d.ts +3 -2
  72. package/dist/http/owner-access.js +2 -3
  73. package/dist/http/principal-gate.d.ts +18 -11
  74. package/dist/http/principal-gate.js +3 -1
  75. package/dist/http/resume-ctx-read.d.ts +14 -3
  76. package/dist/http/resume-ctx-read.js +8 -2
  77. package/dist/http/resume-legs.d.ts +35 -3
  78. package/dist/http/resume-legs.js +244 -92
  79. package/dist/http/route-ctx.d.ts +6 -0
  80. package/dist/http/routes/a2a-serve.js +2 -0
  81. package/dist/http/routes/approvals-assistant.js +3 -2
  82. package/dist/http/routes/capabilities.js +1 -1
  83. package/dist/http/routes/diagnostics.d.ts +3 -1
  84. package/dist/http/routes/diagnostics.js +30 -5
  85. package/dist/http/routes/principal-inbox.d.ts +3 -1
  86. package/dist/http/routes/principal-inbox.js +3 -2
  87. package/dist/http/routes/runs.js +10 -7
  88. package/dist/http/routes/session-sync.d.ts +24 -5
  89. package/dist/http/routes/session-sync.js +46 -34
  90. package/dist/http/routes/sessions-list.d.ts +1 -1
  91. package/dist/http/routes/tasks.js +127 -93
  92. package/dist/http/server.d.ts +4 -5
  93. package/dist/http/server.js +30 -5
  94. package/dist/http/submit-ctx.d.ts +1 -0
  95. package/dist/http/submit-ctx.js +2 -3
  96. package/dist/index.d.ts +3 -1
  97. package/dist/index.js +1 -0
  98. package/dist/leg-facts.d.ts +7 -2
  99. package/dist/leg-facts.js +1 -0
  100. package/dist/local-store-declaration.d.ts +54 -0
  101. package/dist/local-store-declaration.js +59 -0
  102. package/dist/mcp-assertion-header.d.ts +1 -1
  103. package/dist/mcp-assertion-header.js +1 -1
  104. package/dist/memory-project-layer.js +1 -1
  105. package/dist/memory-scope.d.ts +4 -4
  106. package/dist/memory-scope.js +4 -4
  107. package/dist/memory-sync-client.js +2 -2
  108. package/dist/model-context-window.d.ts +50 -11
  109. package/dist/model-context-window.js +20 -1
  110. package/dist/model-entry-refusal.d.ts +5 -4
  111. package/dist/model-entry-refusal.js +17 -3
  112. package/dist/model-reach-scope.d.ts +2 -1
  113. package/dist/observability/fail-open.d.ts +14 -2
  114. package/dist/observability/fail-open.js +14 -2
  115. package/dist/observability/metrics.js +1 -1
  116. package/dist/observability/run-terminal.js +1 -0
  117. package/dist/observability/secret-env-scrub.d.ts +1 -1
  118. package/dist/observability/secret-env-scrub.js +1 -1
  119. package/dist/org-delivery-gate.d.ts +31 -6
  120. package/dist/org-delivery-gate.js +7 -4
  121. package/dist/owner-faces.d.ts +3 -2
  122. package/dist/park-row-commit.d.ts +67 -0
  123. package/dist/park-row-commit.js +76 -0
  124. package/dist/parked-decide.d.ts +9 -7
  125. package/dist/parked-decide.js +25 -6
  126. package/dist/peer-directory.d.ts +1 -1
  127. package/dist/plugins/adoption-log-sql.d.ts +3 -3
  128. package/dist/plugins/adoption-log-sql.js +19 -19
  129. package/dist/plugins/approval-ask-store-sql.d.ts +1 -1
  130. package/dist/plugins/approval-ask-store-sql.js +3 -5
  131. package/dist/plugins/approval-exemption-skill-cutoff.d.ts +64 -0
  132. package/dist/plugins/approval-exemption-skill-cutoff.js +122 -0
  133. package/dist/plugins/approval-exemption-skill-m46.d.ts +0 -6
  134. package/dist/plugins/approval-exemption-skill-m46.js +6 -10
  135. package/dist/plugins/approval-exemption-store.d.ts +26 -1
  136. package/dist/plugins/approval-exemption-store.js +28 -19
  137. package/dist/plugins/ensure-schema-lock-policy.d.ts +36 -3
  138. package/dist/plugins/ensure-schema-lock-policy.js +75 -9
  139. package/dist/plugins/file-run-store.d.ts +22 -9
  140. package/dist/plugins/file-run-store.js +50 -22
  141. package/dist/plugins/local-checkpoint-store.d.ts +4 -0
  142. package/dist/plugins/local-checkpoint-store.js +2 -1
  143. package/dist/plugins/local-data-root-admission.d.ts +52 -0
  144. package/dist/plugins/local-data-root-admission.js +100 -0
  145. package/dist/plugins/local-engine-high-water.d.ts +28 -7
  146. package/dist/plugins/local-engine-high-water.js +27 -22
  147. package/dist/plugins/local-session-store.d.ts +134 -49
  148. package/dist/plugins/local-session-store.js +149 -108
  149. package/dist/plugins/memory-run-store.d.ts +14 -9
  150. package/dist/plugins/memory-run-store.js +47 -21
  151. package/dist/plugins/one-time-migration-marker.d.ts +19 -3
  152. package/dist/plugins/one-time-migration-marker.js +7 -1
  153. package/dist/plugins/one-time-migrations.d.ts +46 -20
  154. package/dist/plugins/one-time-migrations.js +102 -79
  155. package/dist/plugins/org-delivery-quota-store.d.ts +34 -11
  156. package/dist/plugins/org-delivery-quota-store.js +29 -14
  157. package/dist/plugins/permission-rule-store-sql.d.ts +1 -1
  158. package/dist/plugins/permission-rule-store-sql.js +5 -9
  159. package/dist/plugins/pg-pool.d.ts +1 -1
  160. package/dist/plugins/pg-pool.js +10 -12
  161. package/dist/plugins/remote-env-device.d.ts +3 -2
  162. package/dist/plugins/remote-env-e2b.d.ts +1 -7
  163. package/dist/plugins/remote-env-e2b.js +10 -7
  164. package/dist/plugins/remote-env-host.d.ts +50 -9
  165. package/dist/plugins/remote-env-host.js +77 -20
  166. package/dist/plugins/remote-env-k8s.js +4 -2
  167. package/dist/plugins/remote-env-local-docker.d.ts +6 -3
  168. package/dist/plugins/remote-env-local-docker.js +8 -23
  169. package/dist/plugins/run-store-sql.d.ts +26 -7
  170. package/dist/plugins/run-store-sql.js +44 -16
  171. package/dist/plugins/scenario-prefix-switch-m42.d.ts +3 -1
  172. package/dist/plugins/scenario-prefix-switch-m42.js +4 -3
  173. package/dist/plugins/schema-consistency.d.ts +89 -5
  174. package/dist/plugins/schema-consistency.js +176 -7
  175. package/dist/plugins/schema-drift-sentence.d.ts +6 -5
  176. package/dist/plugins/schema-drift-sentence.js +13 -5
  177. package/dist/plugins/session-placement.d.ts +5 -4
  178. package/dist/plugins/shell-sentinel-owner-m48.d.ts +22 -0
  179. package/dist/plugins/shell-sentinel-owner-m48.js +6 -0
  180. package/dist/plugins/stop-then-start-sentence.d.ts +16 -0
  181. package/dist/plugins/stop-then-start-sentence.js +7 -0
  182. package/dist/plugins/store-backend.d.ts +22 -8
  183. package/dist/plugins/store-backend.js +58 -24
  184. package/dist/plugins/store-contracts.d.ts +33 -3
  185. package/dist/plugins/store-contracts.js +16 -0
  186. package/dist/plugins/tidb-pool.d.ts +11 -6
  187. package/dist/plugins/tidb-pool.js +18 -12
  188. package/dist/plugins/workspace-custody.d.ts +44 -0
  189. package/dist/plugins/workspace-custody.js +47 -0
  190. package/dist/project-identity.d.ts +8 -7
  191. package/dist/project-identity.js +40 -17
  192. package/dist/project-layer-key.d.ts +13 -2
  193. package/dist/project-layer-key.js +9 -1
  194. package/dist/read-deny-floor-notice.d.ts +36 -0
  195. package/dist/read-deny-floor-notice.js +40 -0
  196. package/dist/{plain-string-record.d.ts → request-declaration-disposition.d.ts} +9 -23
  197. package/dist/request-declaration-disposition.js +39 -0
  198. package/dist/run-cancel-card.d.ts +28 -0
  199. package/dist/run-cancel-card.js +19 -0
  200. package/dist/run-cancel-context.d.ts +10 -0
  201. package/dist/run-cancel-context.js +1 -0
  202. package/dist/run-local.js +28 -13
  203. package/dist/runs.d.ts +31 -7
  204. package/dist/runs.js +34 -16
  205. package/dist/security.d.ts +2 -2
  206. package/dist/server-secret-env.d.ts +34 -20
  207. package/dist/server-secret-env.js +61 -18
  208. package/dist/session-agent.d.ts +3 -2
  209. package/dist/session-children-retire.d.ts +6 -3
  210. package/dist/session-children-retire.js +2 -5
  211. package/dist/session-workspace.d.ts +3 -3
  212. package/dist/store-admission.d.ts +33 -0
  213. package/dist/store-admission.js +49 -0
  214. package/dist/store-posture.d.ts +5 -0
  215. package/dist/store-posture.js +18 -1
  216. package/dist/stores-sql/checkpoint-store-sql.d.ts +1 -1
  217. package/dist/stores-sql/checkpoint-store-sql.js +3 -5
  218. package/dist/stores-sql/file-history-store-sql.d.ts +1 -1
  219. package/dist/stores-sql/file-history-store-sql.js +3 -5
  220. package/dist/stores-sql/tool-result-store-sql.d.ts +1 -1
  221. package/dist/stores-sql/tool-result-store-sql.js +4 -6
  222. package/dist/task-a2a.d.ts +3 -8
  223. package/dist/task-a2a.js +25 -27
  224. package/dist/task-cwd.d.ts +23 -7
  225. package/dist/task-cwd.js +3 -0
  226. package/dist/task-mcp.d.ts +3 -3
  227. package/dist/task-mcp.js +1 -1
  228. package/dist/task-settings.js +1 -0
  229. package/dist/terminal.d.ts +2 -0
  230. package/dist/terminal.js +21 -6
  231. package/dist/tool-approval.d.ts +44 -36
  232. package/dist/tool-approval.js +66 -47
  233. package/dist/trace/engine-notice-wire.d.ts +24 -21
  234. package/dist/trace/engine-notice-wire.js +7 -3
  235. package/dist/trace/ledger-sink.d.ts +3 -1
  236. package/dist/trace/project.d.ts +4 -2
  237. package/dist/trace/project.js +8 -7
  238. package/dist/trace/redact.d.ts +12 -0
  239. package/dist/trace/redact.js +5 -1
  240. package/dist/trace/self-minted-wire.d.ts +131 -52
  241. package/dist/trace/self-minted-wire.js +71 -39
  242. package/dist/web-admin-client/apply-effective.js +22 -9
  243. package/docs/error-text-lexicon.json +317 -0
  244. package/package.json +3 -1
  245. package/scripts/embed-minimal.mjs +183 -0
  246. package/wire-changes/server@7.149.0.json +372 -0
  247. package/dist/plain-string-record.js +0 -70
@@ -0,0 +1,122 @@
1
+ import { readdirSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { SKILL_TOOL_NAME } from "@sema-agent/core";
4
+ import { APPROVAL_EXEMPTION_DIR, EXEMPTION_LOG_SUFFIX, readExemptionLog } from "./approval-exemption-store.js";
5
+ import { MIGRATION_MARKER_TABLE, SKILL_EXEMPTION_CUTOFF_ID } from "./one-time-migration-marker.js";
6
+ import { atomicWriteFileSync } from "./owner-only-file-mode.js";
7
+ import { readLiveColumns } from "./schema-live-columns.js";
8
+ import { SCHEMA_MISMATCH_ERROR_CODE, StoreBootRefusalError } from "./store-contracts.js";
9
+ export const SKILL_EXEMPTION_TOOL_NAME = SKILL_TOOL_NAME;
10
+ export const SKILL_EXEMPTION_CUTOFF_SINCE = "7.148.0";
11
+ export const LOCAL_SKILL_EXEMPTION_CUTOFF_FILE = "one-time-migration-M46.marker";
12
+ export const PRE_CUTOFF_SKILL_EXEMPTION_CONSEQUENCE = `Such a row was written when approving a \`${SKILL_TOOL_NAME}\` call meant activating a skill (and pre-approving the tools that skill lists); ` +
13
+ `this build would read it as a standing answer for any later call of a tool named \`${SKILL_TOOL_NAME}\` in that session — a wider grant than the one given.`;
14
+ function cutoffMsOf(raw) {
15
+ const n = typeof raw === "number" ? raw : typeof raw === "bigint" ? Number(raw) : typeof raw === "string" && /^-?\d+$/.test(raw.trim()) ? Number(raw.trim()) : Number.NaN;
16
+ return Number.isSafeInteger(n) && !Number.isNaN(new Date(n).getTime()) ? n : undefined;
17
+ }
18
+ export async function readSqlSkillExemptionCutoff(query, dialect) {
19
+ const [row] = await query(`SELECT marked_at_ms FROM ${MIGRATION_MARKER_TABLE} WHERE migration_id = ${dialect === "mysql" ? "?" : "$1"}`, [SKILL_EXEMPTION_CUTOFF_ID]);
20
+ if (row === undefined)
21
+ return undefined;
22
+ const ms = cutoffMsOf(row["marked_at_ms"]);
23
+ if (ms !== undefined)
24
+ return ms;
25
+ throw new StoreBootRefusalError(`${SCHEMA_MISMATCH_ERROR_CODE}: the Skill exemption cutoff row (${MIGRATION_MARKER_TABLE}, migration_id '${SKILL_EXEMPTION_CUTOFF_ID}') holds ${JSON.stringify(String(row["marked_at_ms"]))}, ` +
26
+ "which is not a time this build can read — refusing to start rather than guess which stored Skill session approval exemption rows predate it. " +
27
+ `Way out: DELETE FROM ${MIGRATION_MARKER_TABLE} WHERE migration_id = '${SKILL_EXEMPTION_CUTOFF_ID}'; then restart — the start records a new cutoff once the stored \`${SKILL_TOOL_NAME}\` rows are dealt with ` +
28
+ "(a release that still carries the one-time migration deletes them; a later release refuses until they are gone). This check only reads that row and changes nothing.");
29
+ }
30
+ export class LegacySkillExemptionsPresentError extends StoreBootRefusalError {
31
+ rows;
32
+ cutoff;
33
+ code = SCHEMA_MISMATCH_ERROR_CODE;
34
+ constructor(rows, cutoff) {
35
+ super(`${SCHEMA_MISMATCH_ERROR_CODE}: this database holds ${rows} \`${SKILL_TOOL_NAME}\` session approval exemption row(s) first granted before its Skill exemption cutoff ` +
36
+ `(${cutoff ?? "none recorded: no release that records it has started against this database"}) — refusing to start. ` +
37
+ `${PRE_CUTOFF_SKILL_EXEMPTION_CONSEQUENCE} ` +
38
+ `This build no longer carries the one-time migration (introduced in ${SKILL_EXEMPTION_CUTOFF_SINCE}) that deletes such rows. ` +
39
+ `Ways out: (1) start a server release at or above ${SKILL_EXEMPTION_CUTOFF_SINCE} that still carries that migration against this database once — it records the cutoff and deletes those rows — then start this build; ` +
40
+ `(2) or delete them yourself: DELETE FROM approval_exemption WHERE tool_name = '${SKILL_TOOL_NAME}'; — each session that held one is asked once more on its next \`${SKILL_TOOL_NAME}\` call. ` +
41
+ "The check behind this refusal is read-only and runs before this build creates any table: it changed nothing in the database.");
42
+ this.rows = rows;
43
+ this.cutoff = cutoff;
44
+ this.name = "LegacySkillExemptionsPresentError";
45
+ }
46
+ }
47
+ export async function judgeSqlSkillExemptionCutoff(query, dialect, carried) {
48
+ const live = await readLiveColumns((sql) => query(sql), dialect);
49
+ const cutoffMs = live.has(MIGRATION_MARKER_TABLE) ? await readSqlSkillExemptionCutoff(query, dialect) : undefined;
50
+ if (carried || !live.has("approval_exemption"))
51
+ return;
52
+ const ph = (i) => (dialect === "mysql" ? "?" : `$${i}`);
53
+ const [counted] = cutoffMs === undefined
54
+ ? await query(`SELECT COUNT(*) AS n FROM approval_exemption WHERE tool_name = ${ph(1)}`, [SKILL_EXEMPTION_TOOL_NAME])
55
+ : await query(`SELECT COUNT(*) AS n FROM approval_exemption WHERE tool_name = ${ph(1)} AND created_at < ${ph(2)}`, [SKILL_EXEMPTION_TOOL_NAME, new Date(cutoffMs)]);
56
+ const rows = Number(counted?.["n"] ?? 0);
57
+ if (rows > 0)
58
+ throw new LegacySkillExemptionsPresentError(rows, cutoffMs === undefined ? null : new Date(cutoffMs).toISOString());
59
+ }
60
+ export async function assertSkillExemptionCutoffHeldOnPool(target, carried) {
61
+ if (target.dialect === "mysql") {
62
+ const conn = await target.pool.getConnection();
63
+ try {
64
+ await judgeSqlSkillExemptionCutoff(async (sql, params) => (await conn.query(sql, params === undefined ? [] : [...params]))[0], "mysql", carried);
65
+ }
66
+ finally {
67
+ conn.release();
68
+ }
69
+ return;
70
+ }
71
+ const client = await target.pool.connect();
72
+ try {
73
+ await judgeSqlSkillExemptionCutoff(async (sql, params) => (await client.query(sql, params === undefined ? [] : [...params])).rows, "pg", carried);
74
+ }
75
+ finally {
76
+ client.release();
77
+ }
78
+ }
79
+ export function skillExemptionCutoffOfText(text) {
80
+ const t = Date.parse(text.trim());
81
+ return Number.isFinite(t) ? t : undefined;
82
+ }
83
+ export function writeLocalSkillExemptionCutoff(path, cutoffMs) {
84
+ atomicWriteFileSync(path, `${new Date(cutoffMs).toISOString()}\n`, { durability: "required" });
85
+ }
86
+ function exemptionLogNames(root) {
87
+ try {
88
+ return readdirSync(join(root, APPROVAL_EXEMPTION_DIR)).filter((name) => name.endsWith(EXEMPTION_LOG_SUFFIX)).sort();
89
+ }
90
+ catch (e) {
91
+ if (e?.code === "ENOENT")
92
+ return [];
93
+ throw e;
94
+ }
95
+ }
96
+ export function localSkillExemptionSessionsBefore(root, cutoffMs, warn) {
97
+ let sessions = 0;
98
+ for (const name of exemptionLogNames(root)) {
99
+ const { rows, gate } = readExemptionLog(join(root, APPROVAL_EXEMPTION_DIR, name), warn);
100
+ if (gate.tripped)
101
+ continue;
102
+ const row = rows.get(SKILL_EXEMPTION_TOOL_NAME);
103
+ if (row !== undefined && (cutoffMs === undefined || row.createdAt < cutoffMs))
104
+ sessions += 1;
105
+ }
106
+ return sessions;
107
+ }
108
+ export function exemptionLogFingerprints(root) {
109
+ return exemptionLogNames(root).map((name) => {
110
+ const st = statSync(join(root, APPROVAL_EXEMPTION_DIR, name));
111
+ return `${name}\u0000${st.size}\u0000${st.mtimeMs}`;
112
+ });
113
+ }
114
+ export function settleLocalSkillExemptionCutoff(physicalRoot, action, now = Date.now) {
115
+ switch (action) {
116
+ case "none":
117
+ return;
118
+ case "lay":
119
+ return writeLocalSkillExemptionCutoff(join(physicalRoot, LOCAL_SKILL_EXEMPTION_CUTOFF_FILE), now());
120
+ }
121
+ }
122
+ //# sourceMappingURL=approval-exemption-skill-cutoff.js.map
@@ -1,10 +1,4 @@
1
1
  import type { FileApprovalExemptionStore } from "./approval-exemption-store.js";
2
- /** 本条在标记表(`one-time-migration-marker.ts`)里的那一行的键。 */
3
- export declare const LEGACY_SKILL_EXEMPTION_MARKER_ID = "M46";
4
- /** local 车道的标记文件(数据根下,文件名即本条;正文 = T0 的 ISO 时刻一行,人可直接读)。 */
5
- export declare const LOCAL_LEGACY_SKILL_EXEMPTION_MARKER_FILE = "one-time-migration-M46.marker";
6
- /** 被删行的 wire 工具名 —— 引擎 `Skill` 工具的名字,core 导出(core 改名 ⇒ 谓词跟着走,不会静默指向空气)。 */
7
- export declare const LEGACY_SKILL_EXEMPTION_TOOL_NAME: string;
8
2
  /** SQL 腿一页的会话数(键集分批:`session_id` 升序,一页一次读、一句删)。 */
9
3
  export declare const LEGACY_SKILL_EXEMPTION_PAGE = 500;
10
4
  /**
@@ -1,25 +1,21 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
- import { SKILL_TOOL_NAME } from "@sema-agent/core";
3
- import { atomicWriteFileSync } from "./owner-only-file-mode.js";
4
2
  import { StoreBootRefusalError } from "./store-contracts.js";
5
- export const LEGACY_SKILL_EXEMPTION_MARKER_ID = "M46";
6
- export const LOCAL_LEGACY_SKILL_EXEMPTION_MARKER_FILE = "one-time-migration-M46.marker";
7
- export const LEGACY_SKILL_EXEMPTION_TOOL_NAME = SKILL_TOOL_NAME;
3
+ import { SKILL_EXEMPTION_TOOL_NAME, skillExemptionCutoffOfText, writeLocalSkillExemptionCutoff } from "./approval-exemption-skill-cutoff.js";
8
4
  export const LEGACY_SKILL_EXEMPTION_PAGE = 500;
9
5
  export function legacySkillExemptionCutoffLocal(markerPath, now = Date.now) {
10
6
  if (existsSync(markerPath)) {
11
- const marked = Date.parse(readFileSync(markerPath, "utf8").trim());
12
- if (!Number.isFinite(marked)) {
13
- throw new StoreBootRefusalError(`one-time migration ${LEGACY_SKILL_EXEMPTION_MARKER_ID}: ${markerPath} exists but does not hold a readable cutoff time (expected one ISO time, e.g. 2026-11-06T09:00:00.000Z); ` +
7
+ const marked = skillExemptionCutoffOfText(readFileSync(markerPath, "utf8"));
8
+ if (marked === undefined) {
9
+ throw new StoreBootRefusalError(`one-time migration M46: ${markerPath} exists but does not hold a readable cutoff time (expected one ISO time, e.g. 2026-11-06T09:00:00.000Z); ` +
14
10
  `refusing to start rather than guess which stored Skill exemption rows predate the upgrade. Delete the file and restart: the step then records a new cutoff (now) and deletes the stored Skill exemption rows older than it.`);
15
11
  }
16
12
  return marked;
17
13
  }
18
14
  const t0 = now();
19
- atomicWriteFileSync(markerPath, `${new Date(t0).toISOString()}\n`, { durability: "required" });
15
+ writeLocalSkillExemptionCutoff(markerPath, t0);
20
16
  return t0;
21
17
  }
22
18
  export function dropLocalLegacySkillExemptionsM46(markerPath, store) {
23
- return store.dropToolRowsCreatedBeforeM46(LEGACY_SKILL_EXEMPTION_TOOL_NAME, legacySkillExemptionCutoffLocal(markerPath));
19
+ return store.dropToolRowsCreatedBeforeM46(SKILL_EXEMPTION_TOOL_NAME, legacySkillExemptionCutoffLocal(markerPath));
24
20
  }
25
21
  //# sourceMappingURL=approval-exemption-skill-m46.js.map
@@ -1,5 +1,6 @@
1
1
  import type { Pool as MysqlPool } from "mysql2/promise";
2
2
  import type { Pool as PgPool } from "pg";
3
+ import { type JsonlStoreGate } from "../observability/corrupt-read-seat.js";
3
4
  import type { Logger } from "../observability/logger.js";
4
5
  /** The server's own audit event for a question a row answered without asking anyone — one name for both reading positions
5
6
  * (the list gate: field `seat: "deployment_list"`; the approval seat: `seat: "approval_seat"`). Not a settlement record: nobody
@@ -48,6 +49,30 @@ export declare class MemoryApprovalExemptionStore implements ApprovalExemptionSt
48
49
  revoke(sessionId: string, toolName: string): Promise<boolean>;
49
50
  deleteBySession(sessionId: string): Promise<number>;
50
51
  }
52
+ /** The File twin's directory under the data root (one JSONL log per session) and the log suffix — the store and the `Skill` cutoff's skip-version arm read the same layout. */
53
+ export declare const APPROVAL_EXEMPTION_DIR = "approval-exemptions";
54
+ export declare const EXEMPTION_LOG_SUFFIX = ".jsonl";
55
+ /**
56
+ * A record whose first-grant time cannot be read (no `ts`, or a `ts` that is not a time — only a damaged or hand-edited log; the writer always stamps it)
57
+ * reads as the EARLIEST time, never as "now": every reader that asks "granted before cutoff X?" (the `Skill` exemption cutoff arm, one-time migration M46)
58
+ * then answers yes — the fail-closed direction. ("Now" changed with every restart and, read after a cutoff was recorded, silently kept such a row.)
59
+ */
60
+ export declare const UNDATED_FIRST_GRANT_MS = 0;
61
+ /** One session log's replayed rows: per wire tool name, the row in force (`createdAt` = its FIRST grant since the last revoke; unreadable ⇒ {@link UNDATED_FIRST_GRANT_MS}). */
62
+ export type ExemptionLogRows = ReadonlyMap<string, {
63
+ readonly grantedBy: string | null;
64
+ readonly createdAt: number;
65
+ }>;
66
+ /**
67
+ * Replay ONE session log (the only reader of a log's bytes: the store's hydrate and the `Skill` cutoff's skip-version arm both call it —
68
+ * `approval-exemption-skill-cutoff.ts`). Torn-tail-safe, last-wins with revoke tombstones; a record in the middle that cannot be read closes
69
+ * that file's gate (`gate.tripped`) — the store then refuses every read of that session (ask anyway), so its rows answer nothing.
70
+ * A record without a parseable `ts` reads as granted now (the replay's own moment).
71
+ */
72
+ export declare function readExemptionLog(path: string, warn?: Logger["warn"]): {
73
+ readonly rows: ExemptionLogRows;
74
+ readonly gate: JsonlStoreGate;
75
+ };
51
76
  /**
52
77
  * 🔴 [ref] —— **fail-closed per session file.** A record in the MIDDLE of a session's log that cannot be read (bit rot,
53
78
  * a hand edit, a line `42`, an object missing `toolName`) used to be skipped silently. When that line was a REVOKE
@@ -73,7 +98,7 @@ export declare class FileApprovalExemptionStore implements ApprovalExemptionStor
73
98
  /** [ref]: first line of every judging / appending method (see the class note). */
74
99
  private assertUsable;
75
100
  private pathFor;
76
- /** Replay every session log (torn-tail-safe; last-wins with revoke tombstones). Index keyed by the SANITIZED id. */
101
+ /** Replay every session log (torn-tail-safe; last-wins with revoke tombstones). Index keyed by the SANITIZED id. One reader per file: {@link readExemptionLog}. */
77
102
  private hydrate;
78
103
  private logFor;
79
104
  grant(sessionId: string, toolName: string, grantedBy: string | null): Promise<void>;
@@ -115,6 +115,27 @@ const EXEMPTION_RECORD_SHAPE = {
115
115
  return { toolName, grantedBy: grantedBy ?? null, ts: ts ?? "", ...(revoked !== undefined ? { revoked } : {}) };
116
116
  },
117
117
  };
118
+ export const APPROVAL_EXEMPTION_DIR = "approval-exemptions";
119
+ export const EXEMPTION_LOG_SUFFIX = ".jsonl";
120
+ export const UNDATED_FIRST_GRANT_MS = 0;
121
+ const firstGrantMsOf = (ts) => {
122
+ const t = Date.parse(ts);
123
+ return Number.isNaN(t) ? UNDATED_FIRST_GRANT_MS : t;
124
+ };
125
+ export function readExemptionLog(path, warn) {
126
+ const { records, gate } = replayJsonlGuarded(path, "approval-exemption", EXEMPTION_RECORD_SHAPE, warn);
127
+ const rows = new Map();
128
+ for (const r of records) {
129
+ if (r.revoked) {
130
+ rows.delete(r.toolName);
131
+ }
132
+ else {
133
+ const prior = rows.get(r.toolName);
134
+ rows.set(r.toolName, { grantedBy: r.grantedBy ?? null, createdAt: prior?.createdAt ?? firstGrantMsOf(r.ts) });
135
+ }
136
+ }
137
+ return { rows, gate };
138
+ }
118
139
  export class FileApprovalExemptionStore {
119
140
  dir;
120
141
  bySession = new Map();
@@ -122,7 +143,7 @@ export class FileApprovalExemptionStore {
122
143
  corruptGates = new Map();
123
144
  warn;
124
145
  constructor(root, opts) {
125
- this.dir = join(root, "approval-exemptions");
146
+ this.dir = join(root, APPROVAL_EXEMPTION_DIR);
126
147
  this.warn = opts?.warn;
127
148
  ensureDir(this.dir);
128
149
  this.hydrate();
@@ -131,7 +152,7 @@ export class FileApprovalExemptionStore {
131
152
  this.corruptGates.get(key)?.assertUsable();
132
153
  }
133
154
  pathFor(sessionId) {
134
- return join(this.dir, `${sanitizePathComponent(sessionId)}.jsonl`);
155
+ return join(this.dir, `${sanitizePathComponent(sessionId)}${EXEMPTION_LOG_SUFFIX}`);
135
156
  }
136
157
  hydrate() {
137
158
  let names;
@@ -142,26 +163,14 @@ export class FileApprovalExemptionStore {
142
163
  return;
143
164
  }
144
165
  for (const name of names) {
145
- if (!name.endsWith(".jsonl"))
166
+ if (!name.endsWith(EXEMPTION_LOG_SUFFIX))
146
167
  continue;
147
- const key = name.slice(0, -".jsonl".length);
148
- const { records, gate } = replayJsonlGuarded(join(this.dir, name), "approval-exemption", EXEMPTION_RECORD_SHAPE, this.warn);
168
+ const key = name.slice(0, -EXEMPTION_LOG_SUFFIX.length);
169
+ const { rows, gate } = readExemptionLog(join(this.dir, name), this.warn);
149
170
  if (gate.tripped)
150
171
  this.corruptGates.set(key, gate);
151
- if (!records.length)
152
- continue;
153
- const m = this.bySession.get(key) ?? new Map();
154
- for (const r of records) {
155
- if (r.revoked) {
156
- m.delete(r.toolName);
157
- }
158
- else {
159
- const prior = m.get(r.toolName);
160
- m.set(r.toolName, { grantedBy: r.grantedBy ?? null, createdAt: prior?.createdAt ?? (Date.parse(r.ts) || Date.now()) });
161
- }
162
- }
163
- if (m.size)
164
- this.bySession.set(key, m);
172
+ if (rows.size)
173
+ this.bySession.set(key, new Map(rows));
165
174
  else
166
175
  this.bySession.delete(key);
167
176
  }
@@ -1,14 +1,47 @@
1
1
  export declare const LOCK_PROBE_INITIAL_JITTER_MS = 400;
2
2
  export declare const LOCK_PROBE_WARN_EVERY = 40;
3
3
  export declare const MAX_CONSECUTIVE_LOCK_PROBE_ERRORS = 5;
4
- /** 抖动退避(~0.1s..0.4s + 至多 0.15s 抖动)—— 两方言同一条曲线。 */
4
+ /** 抖动退避(~0.1s..0.4s + 至多 0.15s 抖动)—— 启动锁的曲线。 */
5
5
  export declare function lockProbeBackoffMs(attempt: number): number;
6
6
  /**
7
- * 轮到拿到为止。`tryOnce` = 方言自己的非阻塞探针:`true` 拿到、`false` 别人持着(继续轮)、抛 = 探针本身失败。
8
- * 初始抖动把 ~20 个在 t=0 同时开火的调用方(集成套扇出 / 同步的舰队启动)铺开。
7
+ * 一把库内具名锁的身份。两方言的「名字」不同形(引擎事实,不是口味):MySQL 协议一个字符串(服务器实例**全局**,上限 64 字符),
8
+ * PG 一个 bigint 键或一对 int4 键(**每库**;单键与双键是两个互不相交的键空间)。身份由属主模块各自铸(启动锁:`tidb-pool.ts` / `pg-pool.ts`;弧锁:`adoption-log-sql.ts`),
9
+ * 本模块只负责怎么取、怎么放。
10
+ */
11
+ export type AdvisoryLockId = {
12
+ readonly dialect: "mysql";
13
+ readonly name: string;
14
+ } | {
15
+ readonly dialect: "pg";
16
+ readonly keys: readonly [number] | readonly [number, number];
17
+ };
18
+ /**
19
+ * 取锁要的最小连接面:发一句带参 SQL、读回行。锁是**连接级**的 —— 调用方必须把取、放与锁罩住的全部语句跑在**同一条**连接上。
20
+ * `SqlTxConn`(`{ rows, affected }`)与 node-pg 的 `PoolClient`(`{ rows }`)天然满足;mysql2 的 `[rows, fields]` 由调用方一行适配。
21
+ */
22
+ export interface LockConn {
23
+ query(sql: string, params: unknown[]): Promise<{
24
+ readonly rows: ReadonlyArray<Record<string, unknown>>;
25
+ }>;
26
+ }
27
+ /**
28
+ * 轮到拿到为止(启动锁的预算)。`tryOnce` = 方言自己的非阻塞探针:`true` 拿到、`false` 别人持着(继续轮)、抛 = 探针本身失败。
29
+ * 取锁调用方走 {@link acquireAdvisoryLock};本函数留给只想验循环律、不带连接的单测。
9
30
  */
10
31
  export declare function pollUntilLocked(tryOnce: () => Promise<boolean>, opts: {
11
32
  tag: string;
12
33
  label: string;
13
34
  }): Promise<void>;
35
+ /**
36
+ * 在 `conn` 上取 `id`,**轮到拿到为止**(永不无锁跑;一条探针连续抛的连接上抛成起服失败)。
37
+ * 锁是连接级的:调用方必须把锁罩住的全部语句跑在这条 `conn` 上,并用 {@link releaseAdvisoryLock} 放。
38
+ */
39
+ export declare function acquireAdvisoryLock(conn: LockConn, id: AdvisoryLockId, tag: string): Promise<void>;
40
+ /**
41
+ * 在 `conn` 上取 `id`,**最多探 `attempts` 次、每次没拿到睡 `sleepMs`**(无开跑抖动:常路零额外延迟)。
42
+ * `true` = 拿到(调用方负责放);`false` = 用尽次数仍是「别人持着」(调用方按「在飞」应答)。其余同 {@link acquireAdvisoryLock}。
43
+ */
44
+ export declare function acquireAdvisoryLockWithin(conn: LockConn, id: AdvisoryLockId, tag: string, attempts: number, sleepMs: number): Promise<boolean>;
45
+ /** {@link acquireAdvisoryLock} / {@link acquireAdvisoryLockWithin} 的释放(尽力而为 —— 锁随连接关闭自动释放;失败留一行,不盖掉真正的结果)。 */
46
+ export declare function releaseAdvisoryLock(conn: LockConn, id: AdvisoryLockId, tag: string): Promise<void>;
14
47
  //# sourceMappingURL=ensure-schema-lock-policy.d.ts.map
@@ -6,27 +6,93 @@ export function lockProbeBackoffMs(attempt) {
6
6
  return 100 + Math.min(attempt, 3) * 100 + Math.floor(Math.random() * 150);
7
7
  }
8
8
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
9
- export async function pollUntilLocked(tryOnce, opts) {
10
- await sleep(Math.floor(Math.random() * LOCK_PROBE_INITIAL_JITTER_MS));
9
+ const TIDB_PESSIMISTIC_RETRY_LIMIT = "pessimistic lock retry limit reached";
10
+ function isTidbPessimisticRetryLimit(err) {
11
+ return err?.message === TIDB_PESSIMISTIC_RETRY_LIMIT;
12
+ }
13
+ function lockStatements(id) {
14
+ switch (id.dialect) {
15
+ case "mysql":
16
+ return {
17
+ probe: "SELECT GET_LOCK(?, 0) AS got",
18
+ release: "SELECT RELEASE_LOCK(?)",
19
+ params: [id.name],
20
+ label: `GET_LOCK('${id.name}')`,
21
+ held: (row) => Number(row?.got) === 1,
22
+ contended: isTidbPessimisticRetryLimit,
23
+ };
24
+ case "pg":
25
+ return id.keys.length === 1
26
+ ? { probe: "SELECT pg_try_advisory_lock($1) AS got", release: "SELECT pg_advisory_unlock($1)", params: [...id.keys], label: `pg advisory lock ${id.keys[0]}`, held: (row) => row?.got === true, contended: () => false }
27
+ : { probe: "SELECT pg_try_advisory_lock($1, $2) AS got", release: "SELECT pg_advisory_unlock($1, $2)", params: [...id.keys], label: `pg advisory lock ${id.keys[0]}/${id.keys[1]}`, held: (row) => row?.got === true, contended: () => false };
28
+ default: {
29
+ const unreachable = id;
30
+ throw new Error(`unreachable advisory-lock dialect: ${JSON.stringify(unreachable)}`);
31
+ }
32
+ }
33
+ }
34
+ const UNTIL_HELD = { initialJitterMs: LOCK_PROBE_INITIAL_JITTER_MS, maxAttempts: Number.POSITIVE_INFINITY, backoffMs: lockProbeBackoffMs };
35
+ async function pollLock(tryOnce, budget, opts) {
36
+ if (budget.initialJitterMs > 0)
37
+ await sleep(Math.floor(Math.random() * budget.initialJitterMs));
11
38
  let consecutiveErrors = 0;
12
- for (let attempt = 0;; attempt++) {
39
+ let lastMsg = "";
40
+ let lastErr;
41
+ for (let attempt = 0; attempt < budget.maxAttempts; attempt++) {
13
42
  try {
14
43
  if (await tryOnce())
15
- return;
44
+ return true;
16
45
  consecutiveErrors = 0;
17
46
  }
18
47
  catch (err) {
19
48
  consecutiveErrors += 1;
20
- const msg = err?.message ?? String(err);
21
- writeLogText("stderr", `[${opts.tag}] ${opts.label} probe attempt ${attempt + 1} threw (${consecutiveErrors}/${MAX_CONSECUTIVE_LOCK_PROBE_ERRORS}):`, msg);
49
+ lastErr = err;
50
+ lastMsg = err?.message ?? String(err);
51
+ writeLogText("stderr", `[${opts.tag}] ${opts.label} probe attempt ${attempt + 1} threw (${consecutiveErrors}/${MAX_CONSECUTIVE_LOCK_PROBE_ERRORS}):`, lastMsg);
22
52
  if (consecutiveErrors >= MAX_CONSECUTIVE_LOCK_PROBE_ERRORS) {
23
- throw new Error(`[${opts.tag}] ${opts.label}: lock probe threw ${MAX_CONSECUTIVE_LOCK_PROBE_ERRORS} times in a row — the connection itself is unusable, refusing to poll forever (last: ${msg})`, { cause: err });
53
+ throw new Error(`[${opts.tag}] ${opts.label}: lock probe threw ${MAX_CONSECUTIVE_LOCK_PROBE_ERRORS} times in a row — the connection itself is unusable, refusing to poll forever (last: ${lastMsg})`, { cause: err });
24
54
  }
25
55
  }
26
56
  if (attempt > 0 && attempt % LOCK_PROBE_WARN_EVERY === 0) {
27
- writeLogText("stderr", `[${opts.tag}] still waiting for ${opts.label} (another replica is running the serialized DDL; attempt ${attempt})`);
57
+ writeLogText("stderr", `[${opts.tag}] still waiting for ${opts.label} (another session holds it; attempt ${attempt})`);
28
58
  }
29
- await sleep(lockProbeBackoffMs(attempt));
59
+ if (attempt + 1 < budget.maxAttempts)
60
+ await sleep(budget.backoffMs(attempt));
61
+ }
62
+ if (consecutiveErrors > 0) {
63
+ throw new Error(`[${opts.tag}] ${opts.label}: the attempt budget (${budget.maxAttempts}) ran out while the lock probe was still throwing (${consecutiveErrors} in a row) — not reading that as "held by another" (last: ${lastMsg})`, { cause: lastErr });
64
+ }
65
+ return false;
66
+ }
67
+ export async function pollUntilLocked(tryOnce, opts) {
68
+ await pollLock(tryOnce, UNTIL_HELD, opts);
69
+ }
70
+ async function tryAdvisoryLock(conn, stmts) {
71
+ try {
72
+ const res = await conn.query(stmts.probe, stmts.params);
73
+ return stmts.held(res.rows[0]);
74
+ }
75
+ catch (err) {
76
+ if (stmts.contended(err))
77
+ return false;
78
+ throw err;
79
+ }
80
+ }
81
+ export async function acquireAdvisoryLock(conn, id, tag) {
82
+ const stmts = lockStatements(id);
83
+ await pollLock(() => tryAdvisoryLock(conn, stmts), UNTIL_HELD, { tag, label: stmts.label });
84
+ }
85
+ export async function acquireAdvisoryLockWithin(conn, id, tag, attempts, sleepMs) {
86
+ const stmts = lockStatements(id);
87
+ return pollLock(() => tryAdvisoryLock(conn, stmts), { initialJitterMs: 0, maxAttempts: attempts, backoffMs: () => sleepMs }, { tag, label: stmts.label });
88
+ }
89
+ export async function releaseAdvisoryLock(conn, id, tag) {
90
+ const stmts = lockStatements(id);
91
+ try {
92
+ await conn.query(stmts.release, stmts.params);
93
+ }
94
+ catch (err) {
95
+ writeLogText("stderr", `[${tag}] releasing ${stmts.label} failed (the lock auto-frees when the connection closes):`, err);
30
96
  }
31
97
  }
32
98
  //# sourceMappingURL=ensure-schema-lock-policy.js.map
@@ -204,15 +204,16 @@ export declare class FileRunStore implements InProcessClaimFence {
204
204
  /** The terminal write's one synchronous section (SQL twin: one transaction): the k8s space's pending commit record (when
205
205
  * offered) → the terminal row → the active-run claim release. Returns whether the pending record is now this run's. */
206
206
  private writeTerminal;
207
- /** Durable F4: park NON-terminal `suspended`, KEEP the task_active claim. CAS on running/suspended (reaper-revert guard).
208
- * [ref]:同时落 `result` —— 封口脱敏后的投影(`sealRunRowResult`,凭据不落;`null` = 没观察到 park 结果),SQL 孪生同注。 */
209
- setSuspended(taskId: string, result: PersistedTaskResult | null): Promise<void>;
210
- /** [ref] D-B: park NON-terminal `needs_review`, KEEP the claim. CAS on running/needs_review.
211
- * [ref]:同 {@link setSuspended} 落封口脱敏后的 `result`。 */
212
- setNeedsReview(taskId: string, result: PersistedTaskResult | null): Promise<void>;
213
- /** Durable F4 resume: flip suspended/needs_review → running (CAS) + reset cancel/preempt flags.
214
- * [ref](codex R1 [medium]):认领 = 接管 `instanceId`(= 当前持有副本;SQL 孪生同注)。CAS 输不改。 */
215
- markResuming(taskId: string, instanceId: string): Promise<boolean>;
207
+ /** Durable F4: park NON-terminal `suspended`, KEEP the task_active claim. CAS = 行仍归 `legId` ∧ running / suspended ∧ 没挂取消请求(三件的理由见 SQL 孪生)。
208
+ * [ref]:同时落 `result` —— 封口脱敏后的投影(`sealRunRowResult`,凭据不落;`null` = 没观察到 park 结果),SQL 孪生同注。
209
+ * 交回 CAS 的答(内存 / SQL 孪生同义):`true` ⇔ 这一写之后行停在这个词上;`false` ⇔ 三件有一件不成立(或行不在)。判与落盘同一个同步段。 */
210
+ setSuspended(taskId: string, result: PersistedTaskResult | null, legId: string): Promise<boolean>;
211
+ /** [ref] D-B: park NON-terminal `needs_review`, KEEP the claim. CAS = {@link setSuspended} 的三件(词换成 needs_review)。
212
+ * [ref]:同 {@link setSuspended} 落封口脱敏后的 `result`;CAS 的答同 {@link setSuspended}。 */
213
+ setNeedsReview(taskId: string, result: PersistedTaskResult | null, legId: string): Promise<boolean>;
214
+ /** Durable F4 resume: flip suspended/needs_review → running (CAS);认领 = 换持有腿(铸新的 `legId` 交回)+ 两面请求旗随旧腿清零(SQL 孪生同注)。
215
+ * [ref](codex R1 [medium]):认领 = 接管 `instanceId`(= 当前持有副本;SQL 孪生同注)。CAS 输不改、交回 `undefined`。 */
216
+ markResuming(taskId: string, instanceId: string): Promise<string | undefined>;
216
217
  getActiveTaskId(sessionId: string): Promise<string | undefined>;
217
218
  /** 认领当写栅栏用(契约 = `store-contracts.ts` 的 `InProcessClaimFence`):判、刷新活性(同步落盘)与 `write` 是同一个同步段。 */
218
219
  whileHoldingClaim(sessionId: string, taskId: string, write: (run: FencedRun) => FenceLanded): boolean;
@@ -222,6 +223,13 @@ export declare class FileRunStore implements InProcessClaimFence {
222
223
  * the error answer invites a retry, and a retry with the same key must run, not replay the refusal. The row stays (it
223
224
  * is the refusal's record); only the binding is freed. */
224
225
  releaseIdemKey(taskId: string): Promise<void>;
226
+ /** 放一条行上的提交幂等键绑定(盘先、索引后;`releaseIdemKey` 与 `withdrawMintedRun` 的非在跑臂同读)。同步段。 */
227
+ private dropIdemBinding;
228
+ /**
229
+ * 撤回本请求铸的这一条 run(SQL 孪生同判同序,律见那里的头注):行上的持有腿不是 `legId` ⇒ 不是本腿的行,一个字节不动;是 ⇒ 行仍在跑 ⇒ 严格删行({@link dropRun}:盘先、索引后;删不掉 ⇒ 抛,认领与行都没动)、
230
+ * 再放本行持有的那一枚认领;已终局 ⇒ 行零删(收割 / 终局先到,那一份就是这条行的结局),只放它上面的提交幂等键;停驻 ⇒ 一个字节不动。同步段,不让出事件循环。交回删没删。
231
+ */
232
+ withdrawMintedRun(taskId: string, legId: string): Promise<boolean>;
225
233
  /** The body fingerprint this row's binding of `idem.key` holds — binding `idem.bodyFingerprint` first when it holds none (first
226
234
  * arrival binds and is persisted, everyone after reads; the file twin of the SQL conditional UPDATE — one writer per data root,
227
235
  * single-threaded between the check and the write). `null` = no such row, or the row is not bound to this key (never written). */
@@ -422,8 +430,13 @@ export declare class FileRunStore implements InProcessClaimFence {
422
430
  /**
423
431
  * 一次性迁移 **M48 的 local 腿**(run 行半场;窗、删法与删后谁接住:窗口见 `plugins/one-time-migrations.ts` 登记表):属主恰为 cli 哨兵(`shell-sentinel-owner-m48.ts`)的 run 行
424
432
  * 改写成无主。盘上与内存同改(副本写律)。起服时调(`LocalBackend.runOneTimeMigrations`,任何读者之前);数据根独占 ⇒ 没有混跑窗。幂等:第二跑零命中零写盘。
433
+ * 命中的行由 {@link shellSentinelRunsM48} 说(与 {@link countShellSentinelOwnersM48} 同一只)。
425
434
  */
426
435
  clearShellSentinelOwnersM48(): number;
436
+ /** 一次性迁移 **M48 的 local 腿**(有身份边界的部署:只数不写):属主恰为 cli 哨兵的 run 行数 —— 命中判据与 {@link clearShellSentinelOwnersM48} 同一只({@link shellSentinelRunsM48})。读内存索引,零写。 */
437
+ countShellSentinelOwnersM48(): number;
438
+ /** M48(local 腿)命中的 run 行:属主恰为 cli 哨兵({@link isShellSentinelOwnerM48})—— 改写与留存计数共用的**唯一**判据。先取快照再改(改写会 `persist` 替换索引里的行)。随 M48 删。 */
439
+ private shellSentinelRunsM48;
427
440
  /**
428
441
  * 一次性迁移 **M32 的 local 腿**(执行放置回填;窗、删法与删后谁接住:窗口见 `plugins/one-time-migrations.ts` 登记表):有 run 行的会话 id(去重)。
429
442
  * 起服时调(`LocalBackend.runOneTimeMigrations`,任何读者之前);读内存索引(构造时已从盘上 hydrate),零写。