@sema-agent/server 7.117.0 → 7.119.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 (159) hide show
  1. package/MIGRATION.md +182 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/USAGE.md +58 -28
  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 +40 -10
  20. package/dist/approval-card.js +11 -1
  21. package/dist/approval.d.ts +55 -17
  22. package/dist/approval.js +74 -33
  23. package/dist/audit.d.ts +12 -1
  24. package/dist/audit.js +62 -2
  25. package/dist/auto-mode-face.d.ts +2 -3
  26. package/dist/bake-runner/main.js +1 -6
  27. package/dist/bake-runner/protocol.d.ts +18 -0
  28. package/dist/bake-runner/protocol.js +22 -2
  29. package/dist/bake-runner/runner.d.ts +12 -8
  30. package/dist/bake-runner/runner.js +5 -9
  31. package/dist/boot/config-center.js +1 -1
  32. package/dist/boot/coordinators.js +4 -1
  33. package/dist/boot/device-lane.d.ts +3 -3
  34. package/dist/boot/device-lane.js +1 -2
  35. package/dist/boot/leader.d.ts +4 -4
  36. package/dist/boot/leader.js +1 -2
  37. package/dist/boot/parked-revive-gate.d.ts +4 -6
  38. package/dist/boot/parked-revive-gate.js +2 -6
  39. package/dist/boot/resolve-spec.js +9 -11
  40. package/dist/boot/runner-deps.js +2 -2
  41. package/dist/boot/stage-07-capability-layer.js +7 -3
  42. package/dist/boot/stage-10-http-server.js +3 -2
  43. package/dist/boot/stores.js +12 -7
  44. package/dist/capabilities/center-plugins.d.ts +2 -0
  45. package/dist/capabilities/center-plugins.js +24 -6
  46. package/dist/capabilities/code-review-council.js +8 -11
  47. package/dist/capabilities/hands-lane.d.ts +25 -0
  48. package/dist/capabilities/hands-lane.js +1 -1
  49. package/dist/capabilities/mcp-probe.d.ts +2 -2
  50. package/dist/capabilities/scenarios.d.ts +34 -7
  51. package/dist/capabilities/scenarios.js +33 -6
  52. package/dist/config-catalog.js +3 -3
  53. package/dist/config.d.ts +14 -18
  54. package/dist/config.js +15 -26
  55. package/dist/deployment-governance.d.ts +5 -7
  56. package/dist/deployment-governance.js +4 -5
  57. package/dist/host-decision.d.ts +114 -31
  58. package/dist/host-decision.js +39 -2
  59. package/dist/http/admission.js +12 -6
  60. package/dist/http/approval-sweeps.js +1 -1
  61. package/dist/http/mcp-dial-deadline.d.ts +35 -0
  62. package/dist/http/mcp-dial-deadline.js +21 -0
  63. package/dist/http/memory-compliance-failure-reply.d.ts +11 -5
  64. package/dist/http/memory-compliance-failure-reply.js +17 -8
  65. package/dist/http/memory-store-refusal-reply.d.ts +14 -11
  66. package/dist/http/memory-store-refusal-reply.js +16 -5
  67. package/dist/http/resume-legs.js +8 -3
  68. package/dist/http/routes/approvals-assistant.d.ts +25 -3
  69. package/dist/http/routes/approvals-assistant.js +27 -26
  70. package/dist/http/routes/capabilities.js +6 -3
  71. package/dist/http/routes/mcp-reconnect.d.ts +17 -0
  72. package/dist/http/routes/mcp-reconnect.js +6 -1
  73. package/dist/http/routes/memory-bundle.js +1 -1
  74. package/dist/http/routes/memory-compliance.js +1 -1
  75. package/dist/http/routes/memory-origin.d.ts +1 -1
  76. package/dist/http/routes/memory-origin.js +1 -1
  77. package/dist/http/routes/runs.js +47 -33
  78. package/dist/http/routes/sessions.d.ts +1 -1
  79. package/dist/http/routes/sessions.js +23 -22
  80. package/dist/http/routes/workflows.js +2 -0
  81. package/dist/http/wire-types.d.ts +17 -13
  82. package/dist/images/manifest.d.ts +7 -1
  83. package/dist/images/manifest.js +18 -16
  84. package/dist/memory-store-refusal.d.ts +38 -18
  85. package/dist/memory-store-refusal.js +25 -7
  86. package/dist/model-unreachable-notice.d.ts +1 -1
  87. package/dist/namespaced-rule-name.d.ts +46 -41
  88. package/dist/namespaced-rule-name.js +36 -22
  89. package/dist/observability/corrupt-read-seat.d.ts +4 -0
  90. package/dist/observability/corrupt-read-seat.js +1 -0
  91. package/dist/observability/fail-open.d.ts +19 -3
  92. package/dist/observability/fail-open.js +19 -3
  93. package/dist/operator-ask.d.ts +6 -2
  94. package/dist/orchestration/workflow-agent-session-index.d.ts +82 -59
  95. package/dist/orchestration/workflow-agent-session-index.js +80 -14
  96. package/dist/orchestration/workflow-completion-inbox.d.ts +5 -0
  97. package/dist/orchestration/workflow-completion-inbox.js +39 -17
  98. package/dist/orchestration/workflow-notify-journal.d.ts +1 -1
  99. package/dist/orchestration/workflow-notify-journal.js +7 -2
  100. package/dist/outbound-dispatcher.d.ts +29 -20
  101. package/dist/outbound-dispatcher.js +105 -16
  102. package/dist/parked-decide.d.ts +43 -48
  103. package/dist/parked-decide.js +54 -16
  104. package/dist/permission-rule-vocab.d.ts +4 -3
  105. package/dist/permission-rule-vocab.js +1 -1
  106. package/dist/plugins/approval-ask-store-sql.d.ts +17 -20
  107. package/dist/plugins/approval-ask-store-sql.js +0 -3
  108. package/dist/plugins/approval-exemption-store.d.ts +4 -0
  109. package/dist/plugins/approval-exemption-store.js +1 -0
  110. package/dist/plugins/blob-backend.d.ts +13 -0
  111. package/dist/plugins/blob-backend.js +26 -0
  112. package/dist/plugins/local-session-store.d.ts +78 -47
  113. package/dist/plugins/local-session-store.js +152 -122
  114. package/dist/plugins/one-time-migrations.d.ts +11 -0
  115. package/dist/plugins/one-time-migrations.js +128 -4
  116. package/dist/plugins/pg-pool.js +1 -1
  117. package/dist/plugins/snapshot-bucket-presence.d.ts +36 -0
  118. package/dist/plugins/snapshot-bucket-presence.js +40 -0
  119. package/dist/plugins/store-backend.d.ts +17 -6
  120. package/dist/plugins/store-backend.js +46 -16
  121. package/dist/plugins/store-open-wait.d.ts +33 -0
  122. package/dist/plugins/store-open-wait.js +73 -0
  123. package/dist/plugins/tidb-pool.js +1 -1
  124. package/dist/plugins/web-search.d.ts +20 -1
  125. package/dist/plugins/web-search.js +39 -10
  126. package/dist/plugins/workflow-run-store-sql.d.ts +7 -6
  127. package/dist/plugins/workflow-run-store-sql.js +5 -7
  128. package/dist/request-key-closure.js +1 -1
  129. package/dist/request-value-closure.d.ts +1 -1
  130. package/dist/rule-entry-list.d.ts +35 -46
  131. package/dist/rule-entry-list.js +18 -26
  132. package/dist/rules-consent.js +3 -0
  133. package/dist/run-local.js +5 -3
  134. package/dist/runs.d.ts +1 -1
  135. package/dist/security.d.ts +5 -3
  136. package/dist/security.js +4 -0
  137. package/dist/session-policy-wire.d.ts +13 -12
  138. package/dist/session-policy-wire.js +11 -11
  139. package/dist/spec-fields.d.ts +13 -11
  140. package/dist/spec-fields.js +4 -7
  141. package/dist/standing-answer-grant.d.ts +14 -2
  142. package/dist/standing-answer-grant.js +8 -1
  143. package/dist/store-posture.d.ts +14 -6
  144. package/dist/store-posture.js +7 -4
  145. package/dist/task-settings.d.ts +27 -55
  146. package/dist/task-settings.js +13 -21
  147. package/dist/tool-approval.d.ts +133 -138
  148. package/dist/tool-approval.js +175 -66
  149. package/dist/trace/core-keyset-guard.d.ts +5 -5
  150. package/dist/trace/redact.d.ts +6 -0
  151. package/dist/trace/redact.js +9 -9
  152. package/dist/trace/self-minted-wire.d.ts +16 -2
  153. package/dist/trace/self-minted-wire.js +7 -0
  154. package/dist/user-input-ledger.d.ts +30 -14
  155. package/dist/user-input-ledger.js +25 -14
  156. package/package.json +2 -2
  157. package/scripts/drain-check-local.mjs +74 -3
  158. package/scripts/drain-check.mysql.sql +43 -17
  159. package/scripts/drain-check.pg.sql +46 -20
@@ -1,5 +1,7 @@
1
- import { acquireEnsureSchemaLock, releaseEnsureSchemaLock } from "./tidb-pool.js";
2
- import { acquirePgEnsureSchemaLock, releasePgEnsureSchemaLock } from "./pg-pool.js";
1
+ import { SCHEMA_STATEMENTS, acquireEnsureSchemaLock, releaseEnsureSchemaLock } from "./tidb-pool.js";
2
+ import { PG_SCHEMA_STATEMENTS, acquirePgEnsureSchemaLock, releasePgEnsureSchemaLock } from "./pg-pool.js";
3
+ import { CENTRAL_SCHEMA_INDEXES } from "./schema-indexes.js";
4
+ import { ensureIndexes } from "./ensure-index.js";
3
5
  import { decodeJsonColumn, isJsonColumnBodyProblem, jsonColumnText, jsonColumnTextExpr, readNonNullJsonValueColumn } from "./sql-json-column.js";
4
6
  import { SESSION_META_INCARNATION_COLUMN, mintSessionIncarnation } from "../session-incarnation.js";
5
7
  import { RUN_HOLDER_DEATH_CODES, RUN_ORPHANED_ERROR, RUN_STALLED_ERROR } from "./store-contracts.js";
@@ -99,7 +101,42 @@ export const ONE_TIME_MIGRATIONS = [
99
101
  "「认不得的键按内容脱敏」一条脱敏后原样显示,卡上 `message` 里引擎写好的那句话不受影响",
100
102
  reportKeys: ["allowRuleShadowRowsMigrated", "allowRuleShadowRowsUnreadable"],
101
103
  },
104
+ {
105
+ id: "M9",
106
+ introducedIn: "7.118.0",
107
+ releasedOn: "2026-10-07",
108
+ removableAfter: "2026-11-06",
109
+ removal: "删 migrateOn 的 M9 步(`reshapeWorkflowParkIndex` / `execAtomically` / `finishParkIndexShape`:收尾段、主键宽度探针、暂存表复制、换名、删旧表、索引补建、主键约束名)与 " +
110
+ "`primaryKeyWidth` / `WORKFLOW_PARK_INDEX_TABLE` / `WORKFLOW_PARK_INDEX_STAGING` / `WORKFLOW_PARK_INDEX_RETIRED`;报告、日志行与 `NO_SQL_MIGRATIONS` 的 " +
111
+ "`workflowParkIndexKey` / `workflowParkIndexRowsCarried`;**连同读侧半场一起删**(都是「引擎 7.43 及以前写下的 workflow 子代停驻记录」这同一件事,同窗同删,不另起登记行):" +
112
+ "`orchestration/workflow-agent-session-index.ts` 的 `older_engine` 读臂(`routeWorkflowPark` 规则 ③、`WorkflowParkRoute` 与 `parked-decide.ts` `ParkOrigin` 上 `cause` 里的这一词," +
113
+ "`denyWorkflowParksUnreadable` 的对应分支;`unreadable` 成为唯一成因)与随包排空判据 `workflow-park-holder`(`scripts/drain-check.mysql.sql` / `scripts/drain-check.pg.sql` 的那一段" +
114
+ "与文件头判据清单里的那一行、`scripts/drain-check-local.mjs` 的 `DRAIN_CRITERIA` 那一条与 `readParkIndex`,以及 `MIGRATION.md` / `docs/DEPLOY-PREREQS.md` 里点名它的说明)",
115
+ afterRemoval: "没有响亮臂:删码之后仍是旧主键(只有 `session_id`)的库(跳过了带本条的版本直接升)上,同一个子会话只能有一枚指针 —— 续跑链 / 分叉里" +
116
+ "后写的那条记录覆盖先写的,判决只看得到一条记录:分叉判不出、链尾可能被更早的记录盖掉(决定投给那一条)。运维手工照中央 DDL 重建这张表即可" +
117
+ "(它是派生投影,丢了只丢升级那一刻还停驻着的旧卡的指针 —— 那些卡改答诚实 miss)。读侧半场删后:升级那一刻还停驻着、点名它的条目没有持有者位的存量卡" +
118
+ "(跳过了带本条的版本直接升的库)路由判 `none` ⇒ 落 legacy 腿,答 `409 conflict.resume_context_unavailable`(响亮,但成因是子会话没有续跑上下文,不再是 " +
119
+ "`decide.workflow_parks_unreadable`);出路不变(续跑发起它的那条 workflow run);随包排空判据没了,换钉前没有机器读数可数这些卡",
120
+ reportKeys: ["workflowParkIndexKey", "workflowParkIndexRowsCarried"],
121
+ },
122
+ {
123
+ id: "M10",
124
+ introducedIn: "7.119.0",
125
+ releasedOn: "2026-10-08",
126
+ removableAfter: "2026-11-07",
127
+ removal: "删 local 车道的认领臂:`LocalSessionStore.register` 里「属主从没被记下的存量会话 ⇒ 带 principal 的登记认领它」那一支、认领日志行 `session_owner_claimed`" +
128
+ "(连同店构造参数里只为它存在的 `log` 位)与读本表 M10 行的 `ownerClaimWindowOpen`;`security.ts` `createAuthorizer` 里对无主会话的那次认领 `register` 调用;" +
129
+ "`http/routes/sessions.ts` 409 `conflict.session_ownerless` 句里「Ways out: on the local backend, during the upgrade window …」那半句(连同冻结表 " +
130
+ "`test/api-error-text-freeze.test.ts` 那一行、契约码目录 `docs/ASSISTANT-WIRE-CONTRACT.md` 那一行);本文件的 `migrationWindowOpen`(若再无别的读者)",
131
+ afterRemoval: "没有响亮臂(删码本身零行为变化:窗关那一刻起认领臂已经不生效):属主从没被记下的存量会话(边档那一格只有化身 —— 跳过了带本条的版本直接升," +
132
+ "或窗内从没被带 principal 提交过)照旧答无主(`ownerOf` = null):它的会话规则这条路由读不了也改不了(409 `conflict.session_ownerless`)," +
133
+ "已存的规则对写下它们的 principal 的 run 照旧生效;出路 = 删会话(`DELETE /v1/sessions/:id`,规则随之删)",
134
+ reportKeys: [],
135
+ },
102
136
  ];
137
+ export function migrationWindowOpen(window, today) {
138
+ return window.removableAfter === null || today <= window.removableAfter;
139
+ }
103
140
  export const NO_SQL_MIGRATIONS = {
104
141
  retiredSnapshotTable: "absent",
105
142
  retiredSnapshotRows: 0,
@@ -110,6 +147,8 @@ export const NO_SQL_MIGRATIONS = {
110
147
  sessionIncarnationsBackfilled: 0,
111
148
  allowRuleShadowRowsMigrated: 0,
112
149
  allowRuleShadowRowsUnreadable: 0,
150
+ workflowParkIndexKey: "file-ledger",
151
+ workflowParkIndexRowsCarried: 0,
113
152
  };
114
153
  const RETIRED_SNAPSHOT_TABLE = "snapshot_manifest";
115
154
  const RETIRED_STEER_SEAT_COLUMN = "pending_steer";
@@ -250,6 +289,7 @@ async function migrateOn(db, dialect) {
250
289
  ? "UPDATE task_run SET error_code = ? WHERE status = 'failed' AND error_code IS NULL AND (error = ? OR error LIKE ?)"
251
290
  : "UPDATE task_run SET error_code = $1 WHERE status = 'failed' AND error_code IS NULL AND (error = $2 OR error LIKE $3)", [RUN_HOLDER_DEATH_CODES.orphaned, RUN_ORPHANED_ERROR, `${RUN_ORPHANED_ERROR} (%`]));
252
291
  const { migrated: allowRuleShadowRowsMigrated, unreadable: allowRuleShadowRowsUnreadable } = await migrateShadowedRuleKey(db, dialect);
292
+ const { key: workflowParkIndexKey, carried: workflowParkIndexRowsCarried } = await reshapeWorkflowParkIndex(db, dialect);
253
293
  let retiredSnapshotTable = "absent";
254
294
  let retiredSnapshotRows = 0;
255
295
  let retiredSteerSeatColumn = "absent";
@@ -283,8 +323,87 @@ async function migrateOn(db, dialect) {
283
323
  return {
284
324
  retiredSnapshotTable, retiredSnapshotRows, retiredSteerSeatColumn, retiredSteerSeatStaleRows, askOriginRowsMigrated, auditDetailJsonNullRows,
285
325
  sessionIncarnationColumn, sessionIncarnationsBackfilled, holderDeathCodeRowsBackfilled, allowRuleShadowRowsMigrated, allowRuleShadowRowsUnreadable,
326
+ workflowParkIndexKey, workflowParkIndexRowsCarried,
286
327
  };
287
328
  }
329
+ const WORKFLOW_PARK_INDEX_TABLE = "workflow_agent_session";
330
+ async function primaryKeyWidth(db, dialect, table) {
331
+ const sql = dialect === "tidb"
332
+ ? `SELECT COUNT(*) AS n FROM information_schema.key_column_usage WHERE table_schema = DATABASE() AND table_name = '${table}' AND constraint_name = 'PRIMARY'`
333
+ : `SELECT COUNT(*) AS n FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage kcu ` +
334
+ `ON kcu.constraint_name = tc.constraint_name AND kcu.table_schema = tc.table_schema AND kcu.table_name = tc.table_name ` +
335
+ `WHERE tc.table_schema = current_schema() AND tc.table_name = '${table}' AND tc.constraint_type = 'PRIMARY KEY'`;
336
+ return Number((await db.rows(sql))[0]?.n ?? 0);
337
+ }
338
+ const WORKFLOW_PARK_INDEX_STAGING = `${WORKFLOW_PARK_INDEX_TABLE}_m9`;
339
+ const WORKFLOW_PARK_INDEX_RETIRED = `${WORKFLOW_PARK_INDEX_TABLE}_m9_old`;
340
+ async function reshapeWorkflowParkIndex(db, dialect) {
341
+ const T = WORKFLOW_PARK_INDEX_TABLE;
342
+ const S = WORKFLOW_PARK_INDEX_STAGING;
343
+ const O = WORKFLOW_PARK_INDEX_RETIRED;
344
+ const exists = (t) => tableExists(db, dialect, t);
345
+ const ddl = (dialect === "tidb" ? SCHEMA_STATEMENTS : PG_SCHEMA_STATEMENTS).find((stmt) => stmt.includes(`CREATE TABLE IF NOT EXISTS ${T} (`));
346
+ if (ddl === undefined)
347
+ throw new Error(`one-time migration M9: the central schema carries no definition of ${T} — refusing to reshape a table it cannot rebuild`);
348
+ const copyPointers = (to, from) => db.exec(dialect === "tidb"
349
+ ? `INSERT IGNORE INTO ${to} (session_id, run_id, agent_key, scope) SELECT session_id, run_id, agent_key, scope FROM ${from}`
350
+ : `INSERT INTO ${to} (session_id, run_id, agent_key, scope) SELECT session_id, run_id, agent_key, scope FROM ${from} ON CONFLICT DO NOTHING`);
351
+ let carried = 0;
352
+ const resumed = await exists(S);
353
+ if (resumed) {
354
+ await db.exec(ddl);
355
+ carried += await copyPointers(T, S);
356
+ await execUntil(db, `DROP TABLE ${S}`, async () => !(await exists(S)));
357
+ }
358
+ if (await exists(O))
359
+ await execUntil(db, `DROP TABLE ${O}`, async () => !(await exists(O)));
360
+ if ((await primaryKeyWidth(db, dialect, T)) !== 1) {
361
+ if (await exists(T))
362
+ await finishParkIndexShape(db, dialect);
363
+ return resumed ? { key: "reshaped", carried } : { key: "current", carried: 0 };
364
+ }
365
+ await db.exec(ddl.replace(`CREATE TABLE IF NOT EXISTS ${T} (`, `CREATE TABLE IF NOT EXISTS ${S} (`));
366
+ carried += await copyPointers(S, T);
367
+ const swapped = async () => !(await exists(S)) && (await exists(T)) && (await primaryKeyWidth(db, dialect, T)) !== 1;
368
+ if (dialect === "tidb") {
369
+ await execUntil(db, `RENAME TABLE ${T} TO ${O}, ${S} TO ${T}`, swapped);
370
+ await execUntil(db, `DROP TABLE IF EXISTS ${O}`, async () => !(await exists(O)));
371
+ }
372
+ else {
373
+ await execAtomically(db, [`ALTER TABLE ${T} RENAME TO ${O}`, `ALTER TABLE ${S} RENAME TO ${T}`, `DROP TABLE ${O}`], async () => (await swapped()) && !(await exists(O)));
374
+ }
375
+ await finishParkIndexShape(db, dialect);
376
+ return { key: "reshaped", carried };
377
+ }
378
+ async function execAtomically(db, statements, postcondition) {
379
+ try {
380
+ await db.exec("BEGIN");
381
+ for (const sql of statements)
382
+ await db.exec(sql);
383
+ await db.exec("COMMIT");
384
+ }
385
+ catch (e) {
386
+ try {
387
+ await db.exec("ROLLBACK");
388
+ }
389
+ catch (rollbackError) {
390
+ throw new Error(`one-time migration: a transactional DDL step failed (${e instanceof Error ? e.message : String(e)}) and could not be rolled back on this connection ` +
391
+ `(${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}) — the database rolls the transaction back when the connection closes; the next start resumes`, { cause: e });
392
+ }
393
+ if (!(await postcondition()))
394
+ throw e;
395
+ }
396
+ }
397
+ async function finishParkIndexShape(db, dialect) {
398
+ await ensureIndexes(async (sql, params) => (dialect === "tidb" ? [await db.rows(sql, params)] : { rows: await db.rows(sql, params) }), dialect === "tidb" ? "mysql" : "pg", CENTRAL_SCHEMA_INDEXES.filter((spec) => spec.table === WORKFLOW_PARK_INDEX_TABLE));
399
+ if (dialect !== "pg")
400
+ return;
401
+ const conventional = `${WORKFLOW_PARK_INDEX_TABLE}_pkey`;
402
+ const pk = await db.rows(`SELECT constraint_name FROM information_schema.table_constraints WHERE table_schema = current_schema() AND table_name = '${WORKFLOW_PARK_INDEX_TABLE}' AND constraint_type = 'PRIMARY KEY'`);
403
+ const name = pk[0]?.constraint_name;
404
+ if (typeof name === "string" && name !== conventional)
405
+ await db.exec(`ALTER TABLE ${WORKFLOW_PARK_INDEX_TABLE} RENAME CONSTRAINT ${name} TO ${conventional}`);
406
+ }
288
407
  const RETIRED_SHADOWED_RULE_KEY = "shadowedRule";
289
408
  export function migrateShadowedRuleDescriptor(text) {
290
409
  let parsed;
@@ -405,7 +524,8 @@ export function migrateRetiredAskOrigin(type, data) {
405
524
  }
406
525
  export function logOneTimeMigrations(log, backend, report) {
407
526
  const changed = report.retiredSnapshotTable === "dropped" || report.retiredSteerSeatColumn === "dropped" || report.askOriginRowsMigrated > 0 || report.auditDetailJsonNullRows > 0 ||
408
- report.sessionIncarnationColumn === "added" || report.sessionIncarnationsBackfilled > 0 || report.holderDeathCodeRowsBackfilled > 0 || report.allowRuleShadowRowsMigrated > 0 || report.allowRuleShadowRowsUnreadable > 0;
527
+ report.sessionIncarnationColumn === "added" || report.sessionIncarnationsBackfilled > 0 || report.holderDeathCodeRowsBackfilled > 0 || report.allowRuleShadowRowsMigrated > 0 || report.allowRuleShadowRowsUnreadable > 0 ||
528
+ report.workflowParkIndexKey === "reshaped";
409
529
  (changed ? log.warn : log.info).call(log, "one_time_migrations_applied", {
410
530
  backend,
411
531
  retiredSnapshotTable: report.retiredSnapshotTable,
@@ -419,6 +539,8 @@ export function logOneTimeMigrations(log, backend, report) {
419
539
  holderDeathCodeRowsBackfilled: report.holderDeathCodeRowsBackfilled,
420
540
  allowRuleShadowRowsMigrated: report.allowRuleShadowRowsMigrated,
421
541
  allowRuleShadowRowsUnreadable: report.allowRuleShadowRowsUnreadable,
542
+ workflowParkIndexKey: report.workflowParkIndexKey,
543
+ workflowParkIndexRowsCarried: report.workflowParkIndexRowsCarried,
422
544
  removableAfter: Object.fromEntries(ONE_TIME_MIGRATIONS.map((w) => [w.id, w.removableAfter])),
423
545
  note: "one-time persisted-data migrations (M1: retired whole-tree snapshot index table dropped; M2: retired single-seat " +
424
546
  "steering column dropped; M3: retired ask-origin spelling rewritten in place; M4: audit details written as the JSON " +
@@ -427,7 +549,9 @@ export function logOneTimeMigrations(log, backend, report) {
427
549
  "holder-death verdict written by an earlier version with only the sentence got the machine-readable code run.stalled / " +
428
550
  "run.orphaned; M7 writes nothing — it is the window of the refusal that keeps a suspended task whose stored allow list was written " +
429
551
  "under an earlier reading from resuming under the current one; M8: parked rows' allow-layer disclosure written by an earlier engine " +
430
- "under its old key shadowedRule rewritten to allowRuleShadowed). Each step " +
552
+ "under its old key shadowedRule rewritten to allowRuleShadowed; M9: the workflow park pointer index rebuilt with the key (session, run), " +
553
+ "its rows carried over; M10 writes nothing at boot — on the local backend, while its window is open, the first task submitted with a principal " +
554
+ "on a session written by an earlier version (which recorded no owner) claims that session for the principal, one session_owner_claimed line per claim). Each step " +
431
555
  "runs on every boot until its window closes (the release that introduced it + 30 days; the per-step date is in " +
432
556
  "removableAfter, null while that release is not published yet) and is deleted in the first minor after that — start a " +
433
557
  "version that still carries a step at least once before upgrading past its deletion.",
@@ -282,7 +282,7 @@ export const PG_SCHEMA_STATEMENTS = [
282
282
  run_id VARCHAR(191) COLLATE "C" NOT NULL,
283
283
  agent_key VARCHAR(191) COLLATE "C" NOT NULL,
284
284
  scope VARCHAR(190) COLLATE "C" NOT NULL,
285
- PRIMARY KEY (session_id)
285
+ PRIMARY KEY (session_id, run_id)
286
286
  )`,
287
287
  `CREATE TABLE IF NOT EXISTS workflow_completion_inbox (
288
288
  seq BIGSERIAL,
@@ -0,0 +1,36 @@
1
+ /**
2
+ * 对象存储配了快照桶却**不存在** = 部署错 —— boot 期核**一次**,缺 ⇒ 响亮拒启(不是每一拍 sweep 一条静默的 404)。
3
+ *
4
+ * ## 病
5
+ * `SESSION_SNAPSHOT_BUCKET`(缺省 `session-snapshots`)是 snapshot-BLOB offload 与 SendUserFile 私有 presigned 轨 / 沙箱 staging 的桶。server 不建桶
6
+ * (`docs/DEPLOY-PREREQS.md`「对象存储桶」),sema-up 的建桶清单又漏过它 ⇒ 起服绿,之后每一次对象写、每一拍孤儿 sweep 的列举(每分钟)都是
7
+ * 404 NoSuchBucket —— 运维读到的是一串「失败的请求」,不是「桶没建」。
8
+ *
9
+ * ## 形(一条规则,不加特例)
10
+ * · 核的是 {@link MinioBlobBackend.probeBucket}(presigned 列举 `max-keys=1`);**只有** 404 + `NoSuchBucket` 是「缺」,其余证明不了的形(连不上 / 403 / 5xx / 没有
11
+ * NoSuchBucket 的 404)⇒ **不拒**、照常起服,走登记过的 F 类兜底口 `recordFailOpen("server.snapshot-bucket.presence-unproven")`(计数 + 探针行 + 记录器的一次性 warn;
12
+ * 三问写在该 tag 的 note 里)—— 拒启要有证据,没有证据只留痕;
13
+ * · 缺 = 配置好的店资源还没就绪,与「库连不上」同一类处境:在 `STORE_LOCK_WAIT_MS` 窗内重试({@link openWithinWait},同一只窗、同一只节拍、同一条窗尾规则 —— 编排里
14
+ * 建桶晚于 server 起是常态),到窗时手上有缺桶的证据 ⇒ 拒启,拒句点名桶名、`MINIO_ENDPOINT` 的显示安全形(`buildDisplaySafeUrl` 的产物:userinfo 落地、
15
+ * query 值成记号,不带密钥)与出路;探针从头到尾没答过(每一次都挂到窗尾)⇒ 同样是「证明不了」,走上面那只兜底口、照常起服。
16
+ * · 只核**快照桶**(`config.snapshotBlobStore.bucket`;SendUserFile 的私桶缺省同一只);公桶 `sema-public` 是运维另配的匿名桶,不在本核之内。
17
+ */
18
+ import type { ServiceConfig } from "../config-types.js";
19
+ import { type StoreOpenWaitClock } from "./store-open-wait.js";
20
+ type SnapshotBlobStoreConfig = NonNullable<ServiceConfig["snapshotBlobStore"]>;
21
+ /** boot 期单次探针的上界(ms):黑洞端点(丢包不拒连)不许把起服拖到对象存储 IO 的 120s;到点 ⇒ `unknown`(证明不了缺桶 ⇒ 不拒、报一次)。 */
22
+ export declare const SNAPSHOT_BUCKET_PROBE_TIMEOUT_MS = 10000;
23
+ /** 缺桶的拒启句(唯一铸点)。 */
24
+ /** @param endpoint `MINIO_ENDPOINT` 的**显示安全形**(调用方过 {@link buildDisplaySafeUrl};本函数不碰原串)。 */
25
+ export declare function buildSnapshotBucketRefusal(bucket: string, endpoint: string, waitedMs: number): string;
26
+ /** 探针到窗尾一次都没答过时,兜底口 detail 里的成因(证明不了缺桶的一形)。 */
27
+ export declare const SNAPSHOT_BUCKET_PROBE_PENDING = "probe still pending when the window ended";
28
+ /**
29
+ * boot 期核一次快照桶(`boot/stores.ts`)。缺桶 ⇒ 窗内重试、到窗拒启;证明不了 ⇒ 兜底口留痕、照常起服。
30
+ * @param waitMs `config.storeLockWaitMs`。`0` ⇒ 只核一次(缺就当场拒)。
31
+ */
32
+ export declare function ensureSnapshotBucket(store: SnapshotBlobStoreConfig, waitMs: number, logger: {
33
+ warn(msg: string, meta?: Record<string, unknown>): void;
34
+ }, clock?: StoreOpenWaitClock): Promise<void>;
35
+ export {};
36
+ //# sourceMappingURL=snapshot-bucket-presence.d.ts.map
@@ -0,0 +1,40 @@
1
+ import { buildDisplaySafeUrl } from "../trace/redact.js";
2
+ import { MinioBlobBackend } from "./blob-backend.js";
3
+ import { recordFailOpen } from "../observability/fail-open.js";
4
+ import { isStoreOpenWindowExpired, openWithinWait } from "./store-open-wait.js";
5
+ export const SNAPSHOT_BUCKET_PROBE_TIMEOUT_MS = 10_000;
6
+ class SnapshotBucketMissing extends Error {
7
+ constructor() {
8
+ super("snapshot bucket missing");
9
+ }
10
+ }
11
+ export function buildSnapshotBucketRefusal(bucket, endpoint, waitedMs) {
12
+ const waited = waitedMs > 0 ? `after retrying for ${waitedMs}ms (STORE_LOCK_WAIT_MS)` : "on the first check (STORE_LOCK_WAIT_MS=0: no wait)";
13
+ return (`Object storage is configured (MINIO_ENDPOINT → ${endpoint}) but the snapshot bucket "${bucket}" (SESSION_SNAPSHOT_BUCKET; default session-snapshots) does not exist there ${waited} — refusing to boot: ` +
14
+ `the server never creates buckets, so every snapshot-offload write and SendUserFile private-track request would answer 404 NoSuchBucket (and the orphan sweep would log one every minute, forever). ` +
15
+ `Next: create the bucket (mc mb <alias>/${bucket}; this version's sema-up scripts create it — re-run them on an existing sema-up deployment), or set SESSION_SNAPSHOT_BUCKET to a bucket that exists.`);
16
+ }
17
+ export const SNAPSHOT_BUCKET_PROBE_PENDING = "probe still pending when the window ended";
18
+ export async function ensureSnapshotBucket(store, waitMs, logger, clock) {
19
+ const probe = new MinioBlobBackend({ endpoint: store.endpoint, bucket: store.bucket, accessKey: store.accessKey, secretKey: store.secretKey, ...(store.region ? { region: store.region } : {}), ...(store.keyPrefix ? { keyPrefix: store.keyPrefix } : {}) });
20
+ let verdict;
21
+ try {
22
+ verdict = await openWithinWait(async () => {
23
+ const r = await probe.probeBucket(SNAPSHOT_BUCKET_PROBE_TIMEOUT_MS);
24
+ if (r.state === "missing")
25
+ throw new SnapshotBucketMissing();
26
+ return r;
27
+ }, waitMs, (w) => logger.warn("snapshot_bucket_wait", { bucket: store.bucket, endpoint: buildDisplaySafeUrl(store.endpoint), waitMs: w.waitMs, error: w.error }), clock);
28
+ }
29
+ catch (err) {
30
+ if (err instanceof SnapshotBucketMissing)
31
+ throw new Error(buildSnapshotBucketRefusal(store.bucket, buildDisplaySafeUrl(store.endpoint), waitMs), { cause: err });
32
+ if (!isStoreOpenWindowExpired(err))
33
+ throw err;
34
+ verdict = { state: "unknown", detail: SNAPSHOT_BUCKET_PROBE_PENDING };
35
+ }
36
+ if (verdict.state === "unknown") {
37
+ recordFailOpen("server.snapshot-bucket.presence-unproven", `bucket=${store.bucket} endpoint=${buildDisplaySafeUrl(store.endpoint)} ${verdict.detail}`);
38
+ }
39
+ }
40
+ //# sourceMappingURL=snapshot-bucket-presence.js.map
@@ -398,8 +398,11 @@ export declare function createStoreBackend(config: ServiceConfig, onWarn?: BootL
398
398
  * 而 LocalBackend 的 `ensureSchema` 是**空实现**,真正会抛的 mkdir + 数据根 BootLock 在构造函数里、
399
399
  * 落在 try 之外。于是那条降级臂对 local 形从来没生效过,注释宣称的行为与代码真做的事撕裂。
400
400
  *
401
- * 降级条件不变(两形同一姿势):`sessionBackend=auto`(配置的 DB 不可达,S5 原形)或**默认推导**的
402
- * local(`dbBackend=local && !dbBackendExplicit`)。**显式** DB_BACKEND=local / tidb 仍 fail-loud——
401
+ * **降级臂只有一条:缺省推导的 local**(`dbBackend=local && !dbBackendExplicit`)。🔴 **SQL 族没有降级臂** —— 配了 DSN 就是要这只库:
402
+ * `SESSION_BACKEND=auto` 与显式 `DB_BACKEND=mysql|pg` 同一待遇,库当下打不开 ⇒ 在 `STORE_LOCK_WAIT_MS` 的窗内重试
403
+ * ({@link openWithinWait}),到窗仍打不开 ⇒ 拒启(拒句 {@link buildSqlStoreOpenRefusal} 点名后端与库的显示安全形)。修前 `auto` 在这里
404
+ * 静默落内存(只留一行 warn):会话 / 停驻 / run 账本进进程内存、重启即丢,而 `/health` 照样 200 —— [ref] P 类 fail-open。
405
+ * 零 DSN 的内存是配置说的(`config.ts` 的 `auto` 在无坐标时就不探测),不经过本函数。**显式** DB_BACKEND=local 仍 fail-loud——
403
406
  * operator 要了 durable,静默丢=丢数据。 */
404
407
  export declare function openStoreBackendWithFallback(config: ServiceConfig, logger: {
405
408
  warn: (msg: string, meta?: Record<string, unknown>) => void;
@@ -409,9 +412,17 @@ export declare function openStoreBackendWithFallback(config: ServiceConfig, logg
409
412
  degradedReason: StoreDegradedReason | undefined;
410
413
  }>;
411
414
  /**
412
- * S-866 ⑤c / G4 —— SQL 族「显式要了持久而店打不开」的拒启句(唯一铸点)。点名 `DB_BACKEND=<引擎>`,给三条出路:修坐标 / 连通性;
413
- * `SESSION_BACKEND=auto`(尽力而为:店不可达时以内存起服,什么都不持久);`DB_BACKEND=memory`(显式内存部署)。驱动原文只取 message
414
- * (与修前 `fatalExit` 打出的同一句),凭据不在其中(驱动不回显口令)。
415
+ * 已配置的 SQL 库坐标 → 一条只含 `scheme://host:port` 的 URL 串,**交给**配置型 URL 的唯一显示铸点 {@link buildDisplaySafeUrl}
416
+ * (出口处写 `buildDisplaySafeUrl(sqlStoreCoordinatesAsUrl(config))`)。它**不是**一份显示投影:显示形只有铸点一只;这里只做「结构化坐标 → URL 串」的拼装
417
+ * (`config.tidb` / `config.pg` 本来就是结构化坐标,没有 DSN 字符串),user / password / database 一概不进产物。
418
+ * host 过不了闭字符集(拼进 URL 会把 `/` `@` `?` 之类带成别的成分)或无坐标 ⇒ 交出一个不是 URL 的串,铸点答它的占位 `«redacted:url»`。
419
+ * scheme 只是引擎名(`postgres` / `mysql`),不对应任何连接。
415
420
  */
416
- export declare function buildSqlStoreOpenRefusal(engine: "mysql" | "pg", err: unknown): string;
421
+ export declare function sqlStoreCoordinatesAsUrl(config: Pick<ServiceConfig, "dbBackend" | "tidb" | "pg">): string;
422
+ /**
423
+ * SQL 族「配了 DSN 而店打不开(窗内重试过了)」的拒启句(唯一铸点)。点名 `DB_BACKEND=<引擎>`、库的显示安全形(`postgres://host:port` / `mysql://host:port`,经 {@link sqlStoreCoordinatesAsUrl} 拼出、再过 {@link buildDisplaySafeUrl},
424
+ * 不带口令)、等了多久,出路两条:修坐标 / 连通性(窗是 `STORE_LOCK_WAIT_MS`);或**显式**内存部署(`DB_BACKEND=memory`)。🔴 没有第三条
425
+ * 「`SESSION_BACKEND=auto` 内存起服」—— 那一条就是被删掉的静默降级。驱动原文只取 message(与修前 `fatalExit` 打出的同一句),凭据不在其中(驱动不回显口令)。
426
+ */
427
+ export declare function buildSqlStoreOpenRefusal(engine: "mysql" | "pg", endpoint: string, waitedMs: number, err: unknown): string;
417
428
  //# sourceMappingURL=store-backend.d.ts.map
@@ -1,7 +1,8 @@
1
1
  import { homedir } from "node:os";
2
2
  import { createCorruptReadFailOpenSeat, createDurableAbsenceSeat } from "../observability/corrupt-read-seat.js";
3
+ import { createLogger } from "../observability/logger.js";
3
4
  import { join } from "node:path";
4
- import { AdoptionError, FileStorageBackend, FileSessionRepo } from "@sema-agent/core";
5
+ import { AdoptionError, FileStorageBackend } from "@sema-agent/core";
5
6
  import { FileWorkflowJournalStore } from "@sema-agent/core";
6
7
  import { fileSessionCaptureRecordStore } from "@sema-agent/core";
7
8
  import { TiDBOutcomeLedger, PgOutcomeLedger } from "./outcome-ledger-sql.js";
@@ -12,8 +13,7 @@ import { StoreBootRefusalError } from "./store-contracts.js";
12
13
  import { buildStoreOpenRefusalTail } from "./store-open-refusal.js";
13
14
  import { acquireLocalBackendWithWait } from "./store-lock-wait.js";
14
15
  import { NO_SQL_MIGRATIONS, runSqlOneTimeMigrations } from "./one-time-migrations.js";
15
- import { LocalSessionStore } from "./local-session-store.js";
16
- import { noteReplayInSessionOpenScope } from "../session-records-skipped-notice.js";
16
+ import { LocalSessionStore, createLocalSessionRepo } from "./local-session-store.js";
17
17
  import { LocalCheckpointStore } from "./local-checkpoint-store.js";
18
18
  import { FileResumeAnchorStore } from "./file-resume-anchor-store.js";
19
19
  import { TiDBApprovalExemptionStore, PgApprovalExemptionStore, FileApprovalExemptionStore } from "./approval-exemption-store.js";
@@ -30,6 +30,7 @@ import { TiDBSessionPolicyStore, PgSessionPolicyStore } from "./session-policy-s
30
30
  import { TiDBWorkflowJournalStore, PgWorkflowJournalStore } from "./workflow-journal-store-sql.js";
31
31
  import { TiDBFileHistoryStore, PgFileHistoryStore, FILE_HISTORY_BLOB_TABLE, FILE_HISTORY_BLOB_KEY_PREFIX } from "./file-history-store-sql.js";
32
32
  import { MinioBlobBackend, SqlBlobBackend } from "./blob-backend.js";
33
+ import { buildDisplaySafeUrl } from "../trace/redact.js";
33
34
  import { fileMemoryControlDirFor } from "../memory-scope.js";
34
35
  import { createTidbPool, ensureSchema as ensureTidbSchema } from "./tidb-pool.js";
35
36
  import { createPgPool, ensurePgSchema, pgPoolOptions } from "./pg-pool.js";
@@ -55,6 +56,7 @@ import { createSqlPermissionRuleStores } from "./permission-rule-store-sql.js";
55
56
  import { createFilePermissionRuleStores } from "./permission-rule-store-file.js";
56
57
  import { mysqlDriver, pgDriver } from "./sql-driver.js";
57
58
  import { storeDegradedReasonFor } from "../store-posture.js";
59
+ import { openWithinWait } from "./store-open-wait.js";
58
60
  function historyBlobBackend(config, dialect, pool) {
59
61
  const s = config.snapshotBlobStore;
60
62
  if (!s)
@@ -240,9 +242,7 @@ class LocalBackend {
240
242
  throw new Error(`DB_BACKEND=local cannot open data dir ${root}: ${e instanceof Error ? e.message : String(e)}` +
241
243
  (tail === undefined ? "" : ` ${tail}`));
242
244
  }
243
- this.sessionStore = new LocalSessionStore(new FileSessionRepo(this.fileBackend.root, {
244
- onCorruptRead: createCorruptReadFailOpenSeat("server.session-repo.corrupt-record-skipped", noteReplayInSessionOpenScope),
245
- }), this.fileBackend.root);
245
+ this.sessionStore = new LocalSessionStore(createLocalSessionRepo(this.fileBackend.root), this.fileBackend.root, { log: createLogger(config.logLevel) });
246
246
  const corruptReadWarn = (msg, fields) => this.discloseWarn(msg, fields);
247
247
  this.runStore = new FileRunStore(this.fileBackend.root, {
248
248
  error: (msg, fields) => void (this.onError ?? this.onWarn)?.(msg, fields),
@@ -344,21 +344,41 @@ export function createStoreBackend(config, onWarn, onError) {
344
344
  return new TiDBBackend(createTidbPool(config), config);
345
345
  }
346
346
  export async function openStoreBackendWithFallback(config, logger) {
347
- const mayFallback = config.dbBackend === "local" ? !config.dbBackendExplicit : config.sessionBackend === "auto";
347
+ const sqlEngine = config.dbBackend === "mysql" || config.dbBackend === "pg" ? config.dbBackend : undefined;
348
+ const mayFallback = config.dbBackend === "local" && !config.dbBackendExplicit;
348
349
  let backend;
350
+ let inFlight;
349
351
  try {
350
- backend = createStoreBackend(config, (msg, meta) => logger.warn(msg, meta), logger.error === undefined ? undefined : (msg, meta) => logger.error(msg, meta));
351
- if (backend)
352
- await backend.ensureSchema();
352
+ const attempt = async () => {
353
+ let built;
354
+ try {
355
+ built = createStoreBackend(config, (msg, meta) => logger.warn(msg, meta), logger.error === undefined ? undefined : (msg, meta) => logger.error(msg, meta));
356
+ inFlight = built;
357
+ if (built)
358
+ await built.ensureSchema();
359
+ inFlight = undefined;
360
+ return built;
361
+ }
362
+ catch (err) {
363
+ inFlight = undefined;
364
+ await built?.close().catch(() => undefined);
365
+ throw err;
366
+ }
367
+ };
368
+ backend =
369
+ sqlEngine === undefined
370
+ ? await attempt()
371
+ : await openWithinWait(attempt, config.storeLockWaitMs, (w) => logger.warn("store_sql_open_wait", { engine: sqlEngine, endpoint: buildDisplaySafeUrl(sqlStoreCoordinatesAsUrl(config)), waitMs: w.waitMs, error: w.error }));
353
372
  }
354
373
  catch (err) {
374
+ void inFlight?.close().catch(() => undefined);
355
375
  if (err instanceof AdoptionError)
356
376
  throw err;
357
377
  if (err instanceof StoreBootRefusalError)
358
378
  throw err;
359
379
  if (!mayFallback) {
360
- if (config.dbBackend === "mysql" || config.dbBackend === "pg")
361
- throw new Error(buildSqlStoreOpenRefusal(config.dbBackend, err), { cause: err });
380
+ if (sqlEngine !== undefined)
381
+ throw new Error(buildSqlStoreOpenRefusal(sqlEngine, buildDisplaySafeUrl(sqlStoreCoordinatesAsUrl(config)), config.storeLockWaitMs, err), { cause: err });
362
382
  throw err;
363
383
  }
364
384
  logger.warn("store_db_unreachable_fallback_memory", {
@@ -370,11 +390,21 @@ export async function openStoreBackendWithFallback(config, logger) {
370
390
  }
371
391
  return { backend, degradedReason: undefined };
372
392
  }
373
- export function buildSqlStoreOpenRefusal(engine, err) {
393
+ const SQL_HOST = /^(?:[A-Za-z0-9._-]{1,253}|\[[0-9A-Fa-f:.]{2,45}\])$/;
394
+ const SQL_COORDINATES_UNPRINTABLE = "<unprintable-coordinates>";
395
+ export function sqlStoreCoordinatesAsUrl(config) {
396
+ const coords = config.dbBackend === "pg" ? config.pg : config.tidb;
397
+ if (coords === undefined || !SQL_HOST.test(coords.host))
398
+ return SQL_COORDINATES_UNPRINTABLE;
399
+ return `${config.dbBackend === "pg" ? "postgres" : "mysql"}://${coords.host}:${coords.port}`;
400
+ }
401
+ export function buildSqlStoreOpenRefusal(engine, endpoint, waitedMs, err) {
374
402
  const coords = engine === "pg" ? "PG_*" : "MYSQL_*";
375
403
  const detail = err instanceof Error ? err.message : String(err);
376
- return (`DB_BACKEND=${engine}: cannot open the configured ${engine === "pg" ? "PostgreSQL" : "MySQL-protocol"} store (${detail}) — refusing to boot, because DB_BACKEND asks for a durable store and SESSION_BACKEND does not allow running without it. ` +
377
- `Next: fix the ${coords} coordinates or the store's reachability; or set SESSION_BACKEND=auto to boot in memory while the store is unreachable (best effort — nothing persists, and /health reports storeDegraded); ` +
378
- `or set DB_BACKEND=memory for an explicitly in-memory deployment.`);
404
+ const waited = waitedMs > 0 ? `after retrying for ${waitedMs}ms (STORE_LOCK_WAIT_MS)` : "on the first attempt (STORE_LOCK_WAIT_MS=0: no wait)";
405
+ return (`DB_BACKEND=${engine}: cannot open the configured ${engine === "pg" ? "PostgreSQL" : "MySQL-protocol"} store at ${endpoint} ${waited} (${detail}) — refusing to boot, because a configured SQL store is never replaced by an in-memory one ` +
406
+ `(SESSION_BACKEND=auto included: sessions, parked runs and every durable record would silently live in memory and vanish on restart). ` +
407
+ `Next: fix the ${coords} coordinates or the store's reachability (the boot wait is STORE_LOCK_WAIT_MS); ` +
408
+ `or run explicitly in memory with DB_BACKEND=memory (and remove the ${coords} coordinates).`);
379
409
  }
380
410
  //# sourceMappingURL=store-backend.js.map
@@ -0,0 +1,33 @@
1
+ /** 进入等待腿时的**一次性**披露(结构上只在第一次失败时调一次,不是靠一个 flag 挡刷屏)。 */
2
+ export type StoreOpenWaitSink = (fields: {
3
+ readonly waitMs: number;
4
+ readonly error: string;
5
+ }) => void;
6
+ /** 时钟三面(异步形:`sleep` 返回 Promise)。三面都是形参,判据不靠真时间。`deadline` = 一只可取消的到点信号(窗尾打断挂起的尝试用)。 */
7
+ export interface StoreOpenWaitClock {
8
+ now(): number;
9
+ sleep(ms: number): Promise<void>;
10
+ deadline(ms: number): {
11
+ readonly expired: Promise<void>;
12
+ cancel(): void;
13
+ };
14
+ }
15
+ /** {@link openWithinWait} 抛出的是不是「到窗仍未返回、且从没有过一次真失败」那一形(调用方据此区分「没有证据」与「有一次确定的失败」)。 */
16
+ export declare function isStoreOpenWindowExpired(err: unknown): boolean;
17
+ /**
18
+ * 跑 `open`;失败且窗为正 ⇒ 在窗内每 {@link STORE_LOCK_POLL_MS} 重试。
19
+ *
20
+ * 🔴 **窗尾只有一条规则**:到窗时手上有一次**完成了的**真失败 ⇒ **原样**抛最近那一次(同一个对象,不重铸 —— 库的拒句读驱动原文,桶的拒句读
21
+ * 「缺桶」那只错);**从头到尾一次真失败都没有**(每次尝试都挂着)⇒ 抛 {@link StoreOpenWindowExpired}(「到窗仍未返回」,{@link isStoreOpenWindowExpired} 判别)。
22
+ * · 窗从**第一次尝试开始**算,每次尝试与**剩余**窗赛跑 —— 挂起的尝试到窗尾也被打断(库接受了 TCP 却永不回握手、DDL 被长期锁住);被打断的那次尝试
23
+ * 不再有人等它:它迟到的结局被吞掉(没人接 = 未处理拒绝会杀进程),它手上的资源由调用方的 `open` 自己收(见 `openStoreBackendWithFallback` 的在飞登记)。
24
+ * · 剩余窗 ≤ 0 **不再起新一次尝试**:一次失败之后的等待吃完了窗,就抛那次失败 —— 不起一次注定跟 0 ms 的到点赛跑、又几乎总是输的尝试
25
+ * (那一形会把一次确定的失败换成「到窗仍未返回」:口令错读成库挂住、缺桶读成没有证据)。
26
+ * 窗为 0 ⇒ **只试一次、不设界**(与 7.118.x 同形:一次尝试挂多久由驱动自己的超时决定 —— 本腿不替它造一个)。
27
+ *
28
+ * @param open 一次完整尝试(造后端 + `ensureSchema()` / 一次桶探针);**失败时必须自己把这次造出的池还回去**,窗内会被再调用。
29
+ * @param waitMs `config.storeLockWaitMs`。`> 0` 才等;`0` / 非数 ⇒ 只试一次(`!(waitMs > 0)` 同时盖住「键缺席」的手搓 config)。
30
+ * @param onWait 进入等待腿的一次性披露口(第一次完成的失败时调一次)。
31
+ */
32
+ export declare function openWithinWait<T>(open: () => Promise<T>, waitMs: number, onWait: StoreOpenWaitSink, clock?: StoreOpenWaitClock): Promise<T>;
33
+ //# sourceMappingURL=store-open-wait.d.ts.map
@@ -0,0 +1,73 @@
1
+ import { STORE_LOCK_POLL_MS } from "./store-lock-wait.js";
2
+ const REAL_CLOCK = {
3
+ now: () => performance.now(),
4
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
5
+ deadline: (ms) => {
6
+ let timer;
7
+ const expired = new Promise((resolve) => {
8
+ timer = setTimeout(resolve, ms);
9
+ });
10
+ return { expired, cancel: () => clearTimeout(timer) };
11
+ },
12
+ };
13
+ class StoreOpenWindowExpired extends Error {
14
+ constructor(waitMs) {
15
+ super(`the open attempt was still pending when the ${waitMs}ms window ended`);
16
+ }
17
+ }
18
+ export function isStoreOpenWindowExpired(err) {
19
+ return err instanceof StoreOpenWindowExpired;
20
+ }
21
+ export async function openWithinWait(open, waitMs, onWait, clock = REAL_CLOCK) {
22
+ if (!(waitMs > 0))
23
+ return await open();
24
+ const startedAt = clock.now();
25
+ let lastFailure;
26
+ for (;;) {
27
+ const remainingMs = waitMs - (clock.now() - startedAt);
28
+ if (lastFailure !== undefined && remainingMs <= 0)
29
+ throw lastFailure.error;
30
+ const deadline = clock.deadline(Math.max(remainingMs, 0));
31
+ let outcome;
32
+ try {
33
+ outcome = await Promise.race([
34
+ attemptOnce(open).then((value) => ({ kind: "opened", value }), (error) => ({ kind: "failed", error })),
35
+ deadline.expired.then(() => ({ kind: "window_ended" })),
36
+ ]);
37
+ }
38
+ finally {
39
+ deadline.cancel();
40
+ }
41
+ switch (outcome.kind) {
42
+ case "opened":
43
+ return outcome.value;
44
+ case "window_ended":
45
+ if (lastFailure !== undefined)
46
+ throw lastFailure.error;
47
+ throw new StoreOpenWindowExpired(waitMs);
48
+ case "failed": {
49
+ if (lastFailure === undefined)
50
+ onWait({ waitMs, error: outcome.error instanceof Error ? outcome.error.message : String(outcome.error) });
51
+ lastFailure = { error: outcome.error };
52
+ const left = waitMs - (clock.now() - startedAt);
53
+ if (left <= 0)
54
+ throw outcome.error;
55
+ await clock.sleep(Math.min(STORE_LOCK_POLL_MS, left));
56
+ break;
57
+ }
58
+ default: {
59
+ const unhandled = outcome;
60
+ throw new Error(`unhandled open attempt outcome: ${JSON.stringify(unhandled)}`);
61
+ }
62
+ }
63
+ }
64
+ }
65
+ function attemptOnce(open) {
66
+ try {
67
+ return open();
68
+ }
69
+ catch (err) {
70
+ return Promise.reject(err);
71
+ }
72
+ }
73
+ //# sourceMappingURL=store-open-wait.js.map
@@ -528,7 +528,7 @@ export const SCHEMA_STATEMENTS = [
528
528
  run_id VARCHAR(191) NOT NULL,
529
529
  agent_key VARCHAR(191) NOT NULL,
530
530
  scope VARCHAR(190) NOT NULL,
531
- PRIMARY KEY (session_id),
531
+ PRIMARY KEY (session_id, run_id),
532
532
  KEY idx_wfagentsess_run (run_id)
533
533
  ) COLLATE utf8mb4_bin`,
534
534
  `CREATE TABLE IF NOT EXISTS workflow_completion_inbox (
@@ -106,7 +106,7 @@ export interface WebSearchCapability {
106
106
  export declare function projectWebSearchCapability(provider: WebSearchProvider | undefined): WebSearchCapability;
107
107
  export interface WebSearchBackendConfig {
108
108
  readonly provider: WebSearchProvider;
109
- /** API key (brave/tavily). From `WEB_SEARCH_API_KEY` — never reaches the model. */
109
+ /** API key (brave/tavily). From `WEB_SEARCH_API_KEY` — never reaches the model. Must be usable as an HTTP header value ({@link isHttpHeaderValue}); the judges refuse one that is not, and the brave / tavily legs refuse it again before fetch. */
110
110
  readonly apiKey?: string;
111
111
  /** SearXNG 实例特定查询参数(`engines=` / `language=` 等)——透传给 core adapter 的 `extraParams`。
112
112
  * 一键装 SearXNG 的向导用它把「选了哪些上游引擎」落到引擎侧。仅 searxng 腿消费。 */
@@ -136,6 +136,25 @@ export interface WebSearchBackend {
136
136
  search: WebSearchFn;
137
137
  maxResults: number;
138
138
  }
139
+ /**
140
+ * 🔴 「这个串能不能当 HTTP 头值」的**唯一**判据 —— 受理面(请求腿 `settings.webSearch.apiKey`)、部署 env 腿
141
+ * (`WEB_SEARCH_API_KEY`)与后端腿(brave / tavily 交给 fetch 之前)**同问这一只**。
142
+ *
143
+ * ## 病
144
+ * `apiKey` 以前只判「是串」。带换行 / NUL / 其它控制字符 / U+00FF 以上字符的 key 两条载体都采纳,到 fetch 构造或发出请求头的那一刻运行时当场抛;
145
+ * 构造腿那一抛是 `Headers.append: "<整串 key>" is an invalid header value.` —— 凭据明文就在 message 里,经 WebSearch 工具失败卡进模型与转写面。
146
+ *
147
+ * ## 判据就是运行时的那条流水线,问的是 Node 自带的两只校验
148
+ * fetch 对一个头值做两步:① `Headers` 构造 —— 首尾 HTTP 空白(含 CR / LF)归一化剥掉,其余位置的 CR / LF / NUL 与 U+00FF 以上字符抛;
149
+ * ② 发送层(undici)对**归一化后的值**再过一遍字符表(HT、U+0020..U+007E、U+0080..U+00FF 可发;其余控制字符与 U+007F 抛 `invalid <name> header`)。
150
+ * 本判据逐步问同样两只:`new Headers(...)` 取归一化值,再交 `node:http` 的 `validateHeaderValue`(与发送层同一张字符表)。**不自写字符表**
151
+ * (第二份表迟早与运行时漂开);两步合起来与真 fetch 的对拍钉在 `test/web-search.test.ts`(本地受控端点,U+0000..U+017F 逐字符全扫)—— 运行时改了头值律,那一格先红。
152
+ * 只问 `Headers` 构造是不够的:它接受 U+0001 / U+007F,发送层拒绝(两步缺一步 ⇒ 判官收下、每次搜索必失败)。
153
+ * 镜像运行时的后果如实:首尾空白(含 CR / LF)归一化后不剩,所以 `"key\n"` 这种从密钥文件带出尾随换行的串**照收**、发出去的是剥后的值 ——
154
+ * 与 {@link judgeEndpoint} 对 `https://@host` 同一条姿势:不比运行时多拒。
155
+ * 抛出的错误对象在这里**当场丢弃**(构造腿那条的 message 就是那串明文),调用方拿到的只有布尔。
156
+ */
157
+ export declare function isHttpHeaderValue(value: string): boolean;
139
158
  export declare function setWebSearchBadPayloadObserver(fn: ((provider: string) => void) | undefined): void;
140
159
  /**
141
160
  * Build the `WebSearchConfig` (a `search` fn + `maxResults`) for `assembleFullBodyTools({ webSearch })`. The API key