@sema-agent/server 7.73.1 → 7.75.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 (46) hide show
  1. package/USAGE.md +8 -0
  2. package/dist/boot/runner-deps.d.ts +0 -4
  3. package/dist/boot/runner-deps.js +1 -2
  4. package/dist/boot/session-faces.js +0 -2
  5. package/dist/boot/stores.js +3 -16
  6. package/dist/config-types.d.ts +1 -1
  7. package/dist/deployment-governance.d.ts +23 -3
  8. package/dist/deployment-governance.js +23 -2
  9. package/dist/env-facts.d.ts +8 -1
  10. package/dist/http/route-ctx.d.ts +9 -8
  11. package/dist/http/routes/approvals-assistant.js +2 -2
  12. package/dist/http/routes/runs.js +1 -1
  13. package/dist/http/routes/workflows.js +4 -0
  14. package/dist/http/server.js +4 -3
  15. package/dist/main.js +0 -1
  16. package/dist/observability/fail-open.d.ts +2 -2
  17. package/dist/observability/fail-open.js +2 -2
  18. package/dist/observability/run-terminal-log.d.ts +2 -1
  19. package/dist/orchestration/workflow-agent-session-index.d.ts +41 -83
  20. package/dist/orchestration/workflow-agent-session-index.js +14 -33
  21. package/dist/parked-decide.d.ts +30 -33
  22. package/dist/parked-decide.js +11 -13
  23. package/dist/plugins/approval-ask-store-sql.js +1 -1
  24. package/dist/plugins/blob-backend.d.ts +6 -7
  25. package/dist/plugins/checkpoint-store-sql.d.ts +13 -10
  26. package/dist/plugins/checkpoint-store-sql.js +6 -24
  27. package/dist/plugins/file-run-store.d.ts +18 -0
  28. package/dist/plugins/file-run-store.js +27 -0
  29. package/dist/plugins/one-time-migrations.d.ts +66 -0
  30. package/dist/plugins/one-time-migrations.js +193 -0
  31. package/dist/plugins/pg-pool.d.ts +6 -0
  32. package/dist/plugins/pg-pool.js +5 -14
  33. package/dist/plugins/retention-store-sql.js +0 -1
  34. package/dist/plugins/store-backend.d.ts +11 -15
  35. package/dist/plugins/store-backend.js +7 -26
  36. package/dist/plugins/task-attachment-store.d.ts +2 -2
  37. package/dist/plugins/tidb-pool.js +11 -23
  38. package/dist/plugins/workflow-run-store-sql.d.ts +21 -21
  39. package/dist/plugins/workflow-run-store-sql.js +2 -16
  40. package/dist/task-cwd.d.ts +12 -1
  41. package/dist/task-settings.d.ts +6 -0
  42. package/dist/trace/core-keyset-guard.d.ts +11 -2
  43. package/dist/trace/project.d.ts +31 -31
  44. package/dist/trace/project.js +12 -17
  45. package/dist/trace/redact.js +295 -16
  46. package/package.json +3 -3
@@ -0,0 +1,193 @@
1
+ import { acquireEnsureSchemaLock, releaseEnsureSchemaLock } from "./tidb-pool.js";
2
+ import { ENSURE_SCHEMA_LOCK_KEY } from "./pg-pool.js";
3
+ export const NO_SQL_MIGRATIONS = {
4
+ retiredSnapshotTable: "absent",
5
+ retiredSnapshotRows: 0,
6
+ retiredSteerSeatColumn: "absent",
7
+ };
8
+ const RETIRED_SNAPSHOT_TABLE = "snapshot_manifest";
9
+ const RETIRED_STEER_SEAT_COLUMN = "pending_steer";
10
+ const RETIRED_ASK_ORIGIN = { from: "unresolvable", to: "ancestor_marked" };
11
+ const GATE_BEARING_LEDGER_EVENT_TYPE = "tool_end";
12
+ export class RetiredSteerSeatError extends Error {
13
+ rows;
14
+ code = "checkpoint.pending_steer_seat_retired";
15
+ constructor(rows) {
16
+ super(`checkpoint.pending_steer_seat_retired: ${rows} checkpoint row(s) still carry a value in the RETIRED single-seat ` +
17
+ `steering column \`${RETIRED_STEER_SEAT_COLUMN}\`. That seat is gone from the engine (a resume of such a row is ` +
18
+ `refused pre-CAS), so the value is an operator instruction nothing will ever deliver, and this service refuses ` +
19
+ `to guess what it should become (folding a bare seat into the queue would mint a queue entry the engine never ` +
20
+ `wrote). Note that no earlier version ever cleared that cell after delivering it, so most non-NULL cells sit on ` +
21
+ `rows that can never be resumed again — those are stale and safe to clear: ` +
22
+ `\`UPDATE checkpoint SET ${RETIRED_STEER_SEAT_COLUMN} = NULL WHERE ${RETIRED_STEER_SEAT_COLUMN} IS NOT NULL AND ` +
23
+ `(status = 'expired' OR execution_outcome IS NOT NULL)\`. Any cell left after that is on a still-live row and holds an ` +
24
+ `undelivered instruction: decide or cancel those runs on server 7.73.x first (or accept dropping the instruction), ` +
25
+ `then clear the rest (\`UPDATE checkpoint SET ${RETIRED_STEER_SEAT_COLUMN} = NULL WHERE ${RETIRED_STEER_SEAT_COLUMN} IS NOT NULL\`) ` +
26
+ `and start this version again.`);
27
+ this.rows = rows;
28
+ this.name = "RetiredSteerSeatError";
29
+ }
30
+ }
31
+ function mysqlDb(conn) {
32
+ return {
33
+ async rows(sql) {
34
+ const [rows] = await conn.query(sql);
35
+ return rows;
36
+ },
37
+ async exec(sql) {
38
+ const [r] = await conn.query(sql);
39
+ return Number(r.affectedRows ?? 0);
40
+ },
41
+ };
42
+ }
43
+ function pgDb(client) {
44
+ return {
45
+ async rows(sql) {
46
+ const r = await client.query(sql);
47
+ return r.rows;
48
+ },
49
+ async exec(sql) {
50
+ const r = await client.query(sql);
51
+ return r.rowCount ?? 0;
52
+ },
53
+ };
54
+ }
55
+ async function tableExists(db, dialect, table) {
56
+ const sql = dialect === "tidb"
57
+ ? `SELECT 1 AS present FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = '${table}'`
58
+ : `SELECT 1 AS present FROM information_schema.tables WHERE table_schema = current_schema() AND table_name = '${table}'`;
59
+ return (await db.rows(sql)).length > 0;
60
+ }
61
+ async function execUntil(db, sql, postcondition) {
62
+ try {
63
+ await db.exec(sql);
64
+ }
65
+ catch (e) {
66
+ if (!(await postcondition()))
67
+ throw e;
68
+ }
69
+ }
70
+ async function columnExists(db, dialect, table, column) {
71
+ const scope = dialect === "tidb" ? "DATABASE()" : "current_schema()";
72
+ const sql = `SELECT 1 AS present FROM information_schema.columns WHERE table_schema = ${scope} AND table_name = '${table}' AND column_name = '${column}'`;
73
+ return (await db.rows(sql)).length > 0;
74
+ }
75
+ export async function runSqlOneTimeMigrations(dialect, pool) {
76
+ return dialect === "tidb" ? withMysqlLock(pool, (db) => migrateOn(db, "tidb")) : withPgLock(pool, (db) => migrateOn(db, "pg"));
77
+ }
78
+ async function withMysqlLock(pool, run) {
79
+ const conn = await pool.getConnection();
80
+ let locked = false;
81
+ try {
82
+ locked = await acquireEnsureSchemaLock(conn, "oneTimeMigrations");
83
+ return await run(mysqlDb(conn));
84
+ }
85
+ finally {
86
+ if (locked)
87
+ await releaseEnsureSchemaLock(conn, "oneTimeMigrations");
88
+ conn.release();
89
+ }
90
+ }
91
+ async function withPgLock(pool, run) {
92
+ const client = await pool.connect();
93
+ let locked = false;
94
+ try {
95
+ try {
96
+ await client.query("SELECT pg_advisory_lock($1)", [ENSURE_SCHEMA_LOCK_KEY]);
97
+ locked = true;
98
+ }
99
+ catch (err) {
100
+ console.warn(`[oneTimeMigrations] pg_advisory_lock(${ENSURE_SCHEMA_LOCK_KEY}) failed; running the migration unserialized:`, err);
101
+ }
102
+ return await run(pgDb(client));
103
+ }
104
+ finally {
105
+ if (locked) {
106
+ await client
107
+ .query("SELECT pg_advisory_unlock($1)", [ENSURE_SCHEMA_LOCK_KEY])
108
+ .catch((err) => void console.warn(`[oneTimeMigrations] pg_advisory_unlock(${ENSURE_SCHEMA_LOCK_KEY}) failed (auto-freed on connection close):`, err));
109
+ }
110
+ client.release();
111
+ }
112
+ }
113
+ async function migrateOn(db, dialect) {
114
+ const seatColumnPresent = await columnExists(db, dialect, "checkpoint", RETIRED_STEER_SEAT_COLUMN);
115
+ if (seatColumnPresent) {
116
+ const seatRows = await db.rows(`SELECT COUNT(*) AS n FROM checkpoint WHERE ${RETIRED_STEER_SEAT_COLUMN} IS NOT NULL`);
117
+ const seats = Number(seatRows[0]?.n ?? 0);
118
+ if (seats > 0)
119
+ throw new RetiredSteerSeatError(seats);
120
+ }
121
+ const snapshotTablePresent = await tableExists(db, dialect, RETIRED_SNAPSHOT_TABLE);
122
+ const retiredSchemaPresent = seatColumnPresent || snapshotTablePresent;
123
+ const askOriginRowsMigrated = !retiredSchemaPresent
124
+ ? 0
125
+ : dialect === "tidb"
126
+ ? (await db.exec(`UPDATE task_event SET data = JSON_SET(data, '$.gate.origin', '${RETIRED_ASK_ORIGIN.to}') ` +
127
+ `WHERE type = '${GATE_BEARING_LEDGER_EVENT_TYPE}' AND JSON_UNQUOTE(JSON_EXTRACT(data, '$.gate.origin')) = '${RETIRED_ASK_ORIGIN.from}'`)) +
128
+ (await db.exec(`UPDATE checkpoint SET execution_outcome = JSON_SET(execution_outcome, '$.origin', '${RETIRED_ASK_ORIGIN.to}') ` +
129
+ `WHERE JSON_UNQUOTE(JSON_EXTRACT(execution_outcome, '$.origin')) = '${RETIRED_ASK_ORIGIN.from}'`))
130
+ : (await db.exec(`UPDATE task_event SET data = jsonb_set(data, '{gate,origin}', '"${RETIRED_ASK_ORIGIN.to}"') ` +
131
+ `WHERE type = '${GATE_BEARING_LEDGER_EVENT_TYPE}' AND data->'gate'->>'origin' = '${RETIRED_ASK_ORIGIN.from}'`)) +
132
+ (await db.exec(`UPDATE checkpoint SET execution_outcome = jsonb_set(execution_outcome, '{origin}', '"${RETIRED_ASK_ORIGIN.to}"') ` +
133
+ `WHERE execution_outcome->>'origin' = '${RETIRED_ASK_ORIGIN.from}'`));
134
+ let retiredSnapshotTable = "absent";
135
+ let retiredSnapshotRows = 0;
136
+ let retiredSteerSeatColumn = "absent";
137
+ const destroyed = [];
138
+ try {
139
+ const counted = !snapshotTablePresent
140
+ ? undefined
141
+ : await db.rows(`SELECT COUNT(*) AS n FROM ${RETIRED_SNAPSHOT_TABLE}`).then((rows) => Number(rows[0]?.n ?? 0), async (e) => {
142
+ if (await tableExists(db, dialect, RETIRED_SNAPSHOT_TABLE))
143
+ throw e;
144
+ return undefined;
145
+ });
146
+ if (counted !== undefined) {
147
+ retiredSnapshotRows = counted;
148
+ await execUntil(db, `DROP TABLE IF EXISTS ${RETIRED_SNAPSHOT_TABLE}`, async () => !(await tableExists(db, dialect, RETIRED_SNAPSHOT_TABLE)));
149
+ retiredSnapshotTable = "dropped";
150
+ destroyed.push(`dropped table ${RETIRED_SNAPSHOT_TABLE} (${retiredSnapshotRows} row(s))`);
151
+ }
152
+ if (seatColumnPresent) {
153
+ await execUntil(db, `ALTER TABLE checkpoint DROP COLUMN ${RETIRED_STEER_SEAT_COLUMN}`, async () => !(await columnExists(db, dialect, "checkpoint", RETIRED_STEER_SEAT_COLUMN)));
154
+ retiredSteerSeatColumn = "dropped";
155
+ destroyed.push(`dropped column checkpoint.${RETIRED_STEER_SEAT_COLUMN}`);
156
+ }
157
+ }
158
+ catch (e) {
159
+ if (destroyed.length === 0)
160
+ throw e;
161
+ throw new Error(`one-time migration failed AFTER completing destructive steps — record these before retrying, a retry cannot ` +
162
+ `report them again: ${destroyed.join("; ")}. Underlying failure: ${e instanceof Error ? e.message : String(e)}`, { cause: e });
163
+ }
164
+ return { retiredSnapshotTable, retiredSnapshotRows, retiredSteerSeatColumn, askOriginRowsMigrated };
165
+ }
166
+ export function migrateRetiredAskOrigin(type, data) {
167
+ if (type !== GATE_BEARING_LEDGER_EVENT_TYPE)
168
+ return undefined;
169
+ if (data === null || typeof data !== "object" || Array.isArray(data))
170
+ return undefined;
171
+ const d = data;
172
+ const gate = d.gate;
173
+ if (gate === null || typeof gate !== "object" || Array.isArray(gate))
174
+ return undefined;
175
+ const g = gate;
176
+ if (g.origin !== RETIRED_ASK_ORIGIN.from)
177
+ return undefined;
178
+ return { ...d, gate: { ...g, origin: RETIRED_ASK_ORIGIN.to } };
179
+ }
180
+ export function logOneTimeMigrations(log, backend, report) {
181
+ const changed = report.retiredSnapshotTable === "dropped" || report.retiredSteerSeatColumn === "dropped" || report.askOriginRowsMigrated > 0;
182
+ (changed ? log.warn : log.info).call(log, "one_time_migrations_applied", {
183
+ backend,
184
+ retiredSnapshotTable: report.retiredSnapshotTable,
185
+ retiredSnapshotRows: report.retiredSnapshotRows,
186
+ retiredSteerSeatColumn: report.retiredSteerSeatColumn,
187
+ askOriginRowsMigrated: report.askOriginRowsMigrated,
188
+ note: "one-time persisted-data migrations for this version's retirements (retired whole-tree snapshot index table " +
189
+ "dropped; retired single-seat steering column dropped; retired ask-origin spelling rewritten in place). This " +
190
+ "whole migration step is removed in the next minor — start this version at least once before upgrading past it.",
191
+ });
192
+ }
193
+ //# sourceMappingURL=one-time-migrations.js.map
@@ -69,6 +69,12 @@ export type PgPoolWarnSink = (msg: string, meta?: Record<string, unknown>) => vo
69
69
  * `onIdleClientError` 缺省 console.warn(与本文件 `ensurePgSchema` 的既有告警姿势一致)——生产
70
70
  * 装配(store-backend `createStoreBackend`)递结构化 logger 的 warn 座。 */
71
71
  export declare function createPgPool(opts: string | pg.PoolConfig, onIdleClientError?: PgPoolWarnSink): pg.Pool;
72
+ /** A fixed bigint key for the `pg_advisory_lock` that serializes the whole `ensurePgSchema` DDL run (see below).
73
+ * Arbitrary constant; only its uniqueness within this app matters (advisory-lock keys are app-private).
74
+ * Exported so **boot-time schema-shaped work outside this function** serializes against it under the SAME key —
75
+ * one lock identity for the whole class (the one-time migration段 is the current user; it runs after ensure and
76
+ * must not interleave with another replica's ensure). */
77
+ export declare const ENSURE_SCHEMA_LOCK_KEY = 4906;
72
78
  /** Idempotent central schema apply (the PG twin of tidb-pool.ts ensureSchema): the run/checkpoint tables here +
73
79
  * every PG store's own (disjoint) tables. Order is irrelevant (CREATE IF NOT EXISTS, no cross-table FKs).
74
80
  *
@@ -81,11 +81,10 @@ export const PG_SCHEMA_STATEMENTS = [
81
81
  terminal_at_ms BIGINT,
82
82
  gate_kind VARCHAR(32) COLLATE "C",
83
83
  bound_input_hash VARCHAR(190) COLLATE "C",
84
- -- pending_steer / pending_steer_queue / pending_steer_rev:队列化后的 durable steering(core 5.14.0
85
- -- #147)。三列的语义、「为什么队列另起一列而不是覆盖旧列」(滚动升级窗里旧副本的无条件覆盖写)、
86
- -- 48000 字节预算、以及「为什么另起一个整数 rev 列而不拿 JSON 旧值当 CAS 守卫」的完整由来见 MySQL
87
- -- 孪生(tidb-pool.ts 同三列的行内注)。PG text 无长度上限,预算由 core 侧统一执法。
88
- pending_steer TEXT COLLATE "C",
84
+ -- pending_steer_queue / pending_steer_rev:队列化后的 durable steering(core 5.14.0 #147)。两列的语义、
85
+ -- 退役的单座列 pending_steer 为何删列(以及存量库的一次性迁移与那条拒启臂)、48000 字节预算、
86
+ -- 以及「为什么另起一个整数 rev 列而不拿 JSON 旧值当 CAS 守卫」的完整由来见 MySQL 孪生
87
+ -- (tidb-pool.ts 同两列的行内注)。PG text 无长度上限,预算由 core 侧统一执法。
89
88
  pending_steer_queue TEXT COLLATE "C",
90
89
  pending_steer_rev BIGINT NOT NULL DEFAULT 0,
91
90
  risk_descriptor TEXT COLLATE "C",
@@ -155,14 +154,6 @@ export const PG_SCHEMA_STATEMENTS = [
155
154
  PRIMARY KEY (policy_key)
156
155
  )`,
157
156
  `CREATE INDEX IF NOT EXISTS idx_session_policy_session ON session_policy (session_id)`,
158
- `CREATE TABLE IF NOT EXISTS snapshot_manifest (
159
- scope VARCHAR(64) COLLATE "C" NOT NULL,
160
- snap_key VARCHAR(64) COLLATE "C" NOT NULL,
161
- rel_path VARCHAR(1024) COLLATE "C" NOT NULL,
162
- blob_hash CHAR(64) COLLATE "C" NOT NULL
163
- )`,
164
- `CREATE INDEX IF NOT EXISTS idx_snapshot_manifest_sk ON snapshot_manifest (scope, snap_key)`,
165
- `CREATE INDEX IF NOT EXISTS idx_snapshot_manifest_blob ON snapshot_manifest (blob_hash)`,
166
157
  `CREATE TABLE IF NOT EXISTS snapshot_blob (
167
158
  blob_hash CHAR(64) COLLATE "C" PRIMARY KEY,
168
159
  byte_len BIGINT NOT NULL,
@@ -400,7 +391,7 @@ export function createPgPool(opts, onIdleClientError) {
400
391
  });
401
392
  return pool;
402
393
  }
403
- const ENSURE_SCHEMA_LOCK_KEY = 4906;
394
+ export const ENSURE_SCHEMA_LOCK_KEY = 4906;
404
395
  export async function ensurePgSchema(pool) {
405
396
  const client = await pool.connect();
406
397
  let locked = false;
@@ -377,7 +377,6 @@ export class SqlRetentionStore {
377
377
  ["approval_exemption", "session_id"],
378
378
  ["session_policy", "session_id"],
379
379
  ...(this.opts.taskAttachmentTable ? [["task_attachment", "session_id"]] : []),
380
- ["snapshot_manifest", "scope"],
381
380
  ["file_history_tracked", "scope"],
382
381
  ["file_history_version", "scope"],
383
382
  ["file_history_boundary", "scope"],
@@ -4,6 +4,7 @@ import type { SessionStore, SessionPolicyStore, FileHistoryStore, WorkflowJourna
4
4
  import { type OutcomeSink } from "./file-outcome-sink.js";
5
5
  import { type InboxWarn } from "./workflow-run-store-sql.js";
6
6
  import { FileRunStore } from "./file-run-store.js";
7
+ import { type OneTimeMigrationReport } from "./one-time-migrations.js";
7
8
  import { LocalCheckpointStore } from "./local-checkpoint-store.js";
8
9
  import { FileResumeAnchorStore } from "./file-resume-anchor-store.js";
9
10
  import { type ApprovalExemptionStore } from "./approval-exemption-store.js";
@@ -221,22 +222,17 @@ export interface StoreBackend {
221
222
  * 于是单机默认部署上 `memoryCapture:"off"` 两条 ingress 结构性不可达(B-085)。
222
223
  * 刻意仍不给 local 一个**内存** twin:内存形会在重启时把一条隐私记录静默丢掉(比文件三腿严格更差)。 */
223
224
  sessionCaptureRecords(): SessionCaptureRecordSource | undefined;
224
- /** S-1 legacy-epoch discriminant (core `RunnerDeps.legacyRewindBoundaryProbe`): does the RETIRED whole-tree
225
- * snapshot epoch hold a boundary for (sessionId, entryId)? Reads the legacy `snapshot_manifest` table.
226
- * SQL backends only (local never had a whole-tree epoch table). Diagnostic input on the refusal path —
227
- * a probe that cannot tell answers undefined (reads exactly like an absent probe), never throws. */
228
- legacyRewindBoundaryProbe?(query: {
229
- sessionId: string;
230
- entryId: string;
231
- }): Promise<boolean | undefined>;
232
- /** Retired-epoch byte hygiene: GC grace-passed orphan `snapshot_blob` rows/objects (rows the retention
233
- * lane's `snapshot_manifest` sweeps keep orphaning — without this leg their BYTES would outlive every
234
- * manifest reference forever, incl. MinIO objects). SQL backends only; reaper-driven. */
225
+ /** Retired-epoch byte hygiene: GC grace-passed orphan `snapshot_blob` rows/objects — the whole-tree snapshot
226
+ * epoch is gone (its manifest index table retired with it), so EVERY row in that domain is an orphan and its
227
+ * BYTES (incl. MinIO objects) would otherwise outlive the epoch forever. SQL backends only; reaper-driven. */
235
228
  legacySnapshotBlobSweep?(): Promise<number>;
236
- /** Retired-epoch right-to-delete: drop the session's LEGACY `snapshot_manifest` rows on the online E21
237
- * purge (the retention lane already sweeps them on expiry — the two delete legs must converge on the
238
- * same row set; a user's DELETE must reach old-epoch data too). Bytes go via the async legacy sweep. */
239
- legacySnapshotPurge?(sessionId: string): Promise<number>;
229
+ /** 🔴 **一次性迁移(随 7.75.0 连同 `one-time-migrations.ts` 一起从接口上删)** —— 本版三处退役落在
230
+ * 已写进库里的字节上,升一次级改完、兼容臂当场删干净(硬 breaking 纪律:持久数据面 = 响亮拒 +
231
+ * 一次性迁移,窗过即删,永不长期双读)。逐条语义、幂等与拒启臂见该模块头注。
232
+ *
233
+ * **必选,不是 optional**:漏实装一支后端 = 那条车道的存量字节永远停在旧读法上,而读腿已经只认新的
234
+ * ⇒ 必须是编译期错误,不是运行期的一次静默跳过。 */
235
+ runOneTimeMigrations(): Promise<OneTimeMigrationReport>;
240
236
  /** SVC-2 (core CORE-7) durable WorkflowJournalStore — the cross-replica resume journal for an LLM-authored workflow
241
237
  * (RunWorkflowOptions.journalStore). REQUIRED on all backends (tidb/pg = durable twins; local = core's
242
238
  * InMemoryWorkflowJournalStore, single-replica/in-process).
@@ -7,6 +7,7 @@ import { TiDBOutcomeLedger, PgOutcomeLedger } from "./outcome-ledger-sql.js";
7
7
  import { FileOutcomeSink } from "./file-outcome-sink.js";
8
8
  import { TiDBWorkflowRunStore, TiDBWorkflowCompletionInbox, TiDBWorkflowNotifyJournalStore, TiDBWorkflowAgentSessionIndex, PgWorkflowRunStore, PgWorkflowCompletionInbox, PgWorkflowNotifyJournalStore, PgWorkflowAgentSessionIndex } from "./workflow-run-store-sql.js";
9
9
  import { FileRunStore, RunStoreStrictHydrateError } from "./file-run-store.js";
10
+ import { NO_SQL_MIGRATIONS, runSqlOneTimeMigrations } from "./one-time-migrations.js";
10
11
  import { LocalSessionStore } from "./local-session-store.js";
11
12
  import { LocalCheckpointStore } from "./local-checkpoint-store.js";
12
13
  import { FileResumeAnchorStore } from "./file-resume-anchor-store.js";
@@ -73,29 +74,8 @@ function legacyBlobBackend(config, dialect, pool) {
73
74
  ...(s.presignTtlSec ? { presignTtlSec: s.presignTtlSec } : {}),
74
75
  }, { dialect, pool });
75
76
  }
76
- async function legacyProbeImpl(dialect, pool, query) {
77
- try {
78
- if (dialect === "tidb") {
79
- const [rows] = await pool.query("SELECT 1 FROM snapshot_manifest WHERE scope = ? AND snap_key = ? LIMIT 1", [query.sessionId, query.entryId]);
80
- return rows.length > 0;
81
- }
82
- const r = await pool.query("SELECT 1 FROM snapshot_manifest WHERE scope = $1 AND snap_key = $2 LIMIT 1", [query.sessionId, query.entryId]);
83
- return r.rows.length > 0;
84
- }
85
- catch {
86
- return undefined;
87
- }
88
- }
89
- async function legacyPurgeImpl(dialect, pool, sessionId) {
90
- if (dialect === "tidb") {
91
- const [r] = await pool.query("DELETE FROM snapshot_manifest WHERE scope = ?", [sessionId]);
92
- return Number(r.affectedRows ?? 0);
93
- }
94
- const r = await pool.query("DELETE FROM snapshot_manifest WHERE scope = $1", [sessionId]);
95
- return r.rowCount ?? 0;
96
- }
97
77
  async function legacySweepImpl(config, dialect, pool) {
98
- const sql = "SELECT blob_hash FROM snapshot_blob WHERE blob_hash NOT IN (SELECT blob_hash FROM snapshot_manifest)";
78
+ const sql = "SELECT blob_hash FROM snapshot_blob";
99
79
  let orphans;
100
80
  if (dialect === "tidb") {
101
81
  const [rows] = await pool.query(sql);
@@ -140,9 +120,8 @@ class TiDBBackend {
140
120
  memoryOptOutGrant() { return new TiDBMemoryOptOutGrantStore(this.pool); }
141
121
  sessionCaptureRecords() { return () => (this.captureRecordsInst ??= new TiDBSessionCaptureRecordStore(this.pool)); }
142
122
  captureRecordsInst;
143
- legacyRewindBoundaryProbe(query) { return legacyProbeImpl("tidb", this.pool, query); }
144
123
  legacySnapshotBlobSweep() { return legacySweepImpl(this.config, "tidb", this.pool); }
145
- legacySnapshotPurge(sessionId) { return legacyPurgeImpl("tidb", this.pool, sessionId); }
124
+ runOneTimeMigrations() { return runSqlOneTimeMigrations("tidb", this.pool); }
146
125
  workflowJournal() { return new TiDBWorkflowJournalStore(this.pool); }
147
126
  outcomeSink() { return new TiDBOutcomeLedger(this.pool); }
148
127
  workflowRun(onWarn) { return new TiDBWorkflowRunStore(this.pool, onWarn); }
@@ -188,9 +167,8 @@ class PgBackend {
188
167
  memoryOptOutGrant() { return new PgMemoryOptOutGrantStore(this.pool); }
189
168
  sessionCaptureRecords() { return () => (this.captureRecordsInst ??= new PgSessionCaptureRecordStore(this.pool)); }
190
169
  captureRecordsInst;
191
- legacyRewindBoundaryProbe(query) { return legacyProbeImpl("pg", this.pool, query); }
192
170
  legacySnapshotBlobSweep() { return legacySweepImpl(this.config, "pg", this.pool); }
193
- legacySnapshotPurge(sessionId) { return legacyPurgeImpl("pg", this.pool, sessionId); }
171
+ runOneTimeMigrations() { return runSqlOneTimeMigrations("pg", this.pool); }
194
172
  workflowJournal() { return new PgWorkflowJournalStore(this.pool); }
195
173
  outcomeSink() { return new PgOutcomeLedger(this.pool); }
196
174
  workflowRun(onWarn) { return new PgWorkflowRunStore(this.pool, onWarn); }
@@ -269,6 +247,9 @@ class LocalBackend {
269
247
  : (plane) => fileSessionCaptureRecordStore(plane?.controlDir ?? ownControlDir);
270
248
  }
271
249
  async ensureSchema() { }
250
+ async runOneTimeMigrations() {
251
+ return { ...NO_SQL_MIGRATIONS, askOriginRowsMigrated: await this.runStore.migrateRetiredAskOriginRows() };
252
+ }
272
253
  async close() {
273
254
  await this.sessionStore.dispose();
274
255
  this.runStore.dispose();
@@ -2,8 +2,8 @@
2
2
  * TaskAttachmentStore —— D-1 通用文件上传的**独立附件 store**(clay 拍 2026-07-27:不复用 fileSnapshot
3
3
  * 的 content-addressed blob 面,新建)。TiDB/PG 双方言 + local 文件店(local-task-attachment-store.ts)。
4
4
  *
5
- * 为什么独立而不是骑 SqlBlobBackend/snapshot_blob:那套的引用追踪=「manifest 里没有的即 orphan」,
6
- * 附件不在任何 snapshot_manifest ⇒ 会被 sweepOrphanBlobs 在 1h grace 后误收。生命周期也不同:
5
+ * 为什么独立而不是骑 SqlBlobBackend/snapshot_blob:那套的引用追踪=「本域 manifest 里没有的即 orphan」,
6
+ * 附件不在任何一域的 manifest 里 ⇒ 会被孤儿扫在 1h grace 后误收。生命周期也不同:
7
7
  * snapshot blob 跟 session 的工作树;**附件跟 task/session 的绑定**(上传先行、提交时绑定、E21 随会话
8
8
  * 级联删、未绑定的按 TTL 收割)。
9
9
  *
@@ -222,20 +222,16 @@ export const SCHEMA_STATEMENTS = [
222
222
  -- D-1 TOCTOU binding guard is UNREACHABLE by the portal without it — it can then only do the unbound legacy
223
223
  -- fallback). NULL on PRE-D-1 rows ⇒ that unbound fallback.
224
224
  bound_input_hash VARCHAR(190) NULL,
225
- -- pending_steer (design/80 D-A, durable steering):**队列化之前**的单座列,存 {text,trusted}。
226
- -- core 5.14.0(#147 BREAKING)之后本服务**只读不写**它:存量行的那一座由 core readPendingSteerQueue
227
- -- 折成 member 0,零迁移。NULL = 没有旧座。Never touches status/resolve (design/80 inv #4)。
228
- pending_steer TEXT NULL,
229
- -- pending_steer_queue:core 5.14.0(#147)的**有界有序队列**(PendingSteerEntry[] 的 JSON)。
230
- -- 为什么队列另起一列、而不是把信封写回 pending_steer(codex 复审 2026-08-06 F3,采纳):滚动升级/
231
- -- 回滚窗里旧副本仍在跑,它对 pending_steer 是**无条件覆盖**写 —— 队列若躺在同一列,旧副本接的下一条
232
- -- steer 会把 5.14 排好的整队悄悄抹掉,正是 #147 要消灭的那种静默丢失换了个位置复发。分列之后:
233
- -- 旧副本写它的单座 ⇒ 落 pending_steer,新读侧把它折成 member 0,**两边都活着**;旧副本读不到队列
234
- -- 是 core 自己已披露的回滚代价(CHANGELOG:rolling back across the queue event loses steers),
235
- -- 那半无法在店面消除,但**写侧互相清零**这半可以,且代价只是一个可空列。
236
- -- 容量:core 的 PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES = 48000 字节是对 {pendingSteer,queue} 整信封的
237
- -- 硬上界(越界 fail-loud steering.queue_full,绝不驱逐)⇒ 本列恒小于它,MySQL TEXT 的 65535 字节
238
- -- 够用,**不升 LONGTEXT**(core 常量注里点名「under a MySQL TEXT column with UTF-8 headroom」)。
225
+ -- 🔴 队列化之前的**单座**列 pending_steer 已**退役删列**:那个席位在引擎侧没了(core 7.17.0 #717 d
226
+ -- 对带席的行 PRE-CAS 拒 seat_retired),留着一列没人读、没人写的字节只会让下一个人以为还有两种读法。
227
+ -- 存量库由启动期的一次性迁移处置(plugins/one-time-migrations.ts,随 7.75.0 删):列上还有非空值 ⇒
228
+ -- **响亮拒启**并给出两条出路(回 7.16.0 把那些 checkpoint resume 一次让座位排空 / 运维手工清列),
229
+ -- 清空之后 DROP COLUMN。刻意**不**自动把裸单座折进队列:那等于本仓替引擎铸一条它从没写过的队列条目。
230
+ -- pending_steer_queue:core 5.14.0(#147)的**有界有序队列**(PendingSteerEntry[] 的 JSON),现在是本表上
231
+ -- durable steering 的唯一载体。Never touches status/resolve (design/80 inv #4)。
232
+ -- 容量:core 的 PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES = 48000 字节是整信封的硬上界(越界 fail-loud
233
+ -- steering.queue_full,绝不驱逐)⇒ 本列恒小于它,MySQL TEXT 的 65535 字节够用,**不升 LONGTEXT**
234
+ -- (core 常量注里点名「under a MySQL TEXT column with UTF-8 headroom」)。
239
235
  pending_steer_queue TEXT NULL,
240
236
  -- pending_steer_rev:pending_steer_queue 列**自己**的乐观锁计数(与 resolve/reopen 的 rev 是两个轴,
241
237
  -- 刻意不复用)。队列化之后 setPendingSteer 从「一条无条件 UPDATE」变成「读-改-写」,中间那道窗必须
@@ -332,14 +328,6 @@ export const SCHEMA_STATEMENTS = [
332
328
  updated_at DATETIME(3) NOT NULL,
333
329
  PRIMARY KEY (policy_key),
334
330
  KEY idx_session_policy_session (session_id)
335
- ) COLLATE utf8mb4_bin`,
336
- `CREATE TABLE IF NOT EXISTS snapshot_manifest (
337
- scope VARCHAR(64) NOT NULL,
338
- snap_key VARCHAR(64) NOT NULL,
339
- rel_path VARCHAR(1024) NOT NULL,
340
- blob_hash CHAR(64) NOT NULL,
341
- KEY idx_snapshot_manifest_sk (scope, snap_key),
342
- KEY idx_snapshot_manifest_blob (blob_hash)
343
331
  ) COLLATE utf8mb4_bin`,
344
332
  `CREATE TABLE IF NOT EXISTS snapshot_blob (
345
333
  blob_hash CHAR(64) NOT NULL,
@@ -348,7 +336,7 @@ export const SCHEMA_STATEMENTS = [
348
336
  -- comment above; this column is where it is enforced). It was originally LONGBLOB NOT NULL back when the
349
337
  -- SQL store was the ONLY blob backend; the MinIO offload then required an INDEX-ONLY row here (bytes NULL,
350
338
  -- the bytes living in MinIO) so that gcOrphanBlobs / reap / deleteBySession (E21 purge) / sweepOrphanBlobs
351
- -- keep computing the orphan/grace SET from snapshot_blob for BOTH backends identically — and so an E21 purge
339
+ -- keep computing the grace SET from snapshot_blob for BOTH backends identically — and so an E21 purge
352
340
  -- can DELETE the MinIO object instead of leaking it. Re-adding NOT NULL here would silently break MinIO-mode
353
341
  -- reference tracking. See blob-backend.ts MinioBlobBackend.
354
342
  bytes LONGBLOB NULL,
@@ -78,29 +78,29 @@ export declare function withRedactedError<T extends {
78
78
  * write (including the TERMINAL one) keeps failing → the durable row is stuck `running` forever and the 24h
79
79
  * orphan sweep finalizes a genuinely-completed run as `abandoned`. Instead degrade the PAYLOAD, not the write.
80
80
  *
81
- * 🔴 **S-192(7.71.0):瘦身只裁展示载荷,parked 行的恢复身份是最后才放弃的东西。**
82
- * 病(修前逐字):step2 写作 `agents: []` ⇒ 一只 parked 的 workflow 子代的 `callKey` / `sessionId` /
83
- * `parkedCheckpointToken` 随整段消失。server 侧复核「这条 park 仍在此 token 下」的依据**只有这一行**(join 索引
84
- * 刻意不存它——它是能力;引擎自己的赎回键另住 resume journal),于是 `/decide` 的 workflow 车道在 `run.agents`
85
- * 里找不到那一行 ⇒ 落 legacy 腿拿一个**错因**的 409 `conflict.resume_context_unavailable`,人的批准无处兑付;
86
- * 同时 core resume 路径上那道「blob 里 parked 而 journal 缺条目 ⇒ 拒 resume」的防线(`prior.agents`)也随之失明。
81
+ * 🔴 **`parks[]` 是这条阶梯唯一不许碰的东西**(core 7.17.0 [ref] 义务②)。
82
+ * 它不是观测位,是**下一次 resume 的准入输入**:core 的 resume 准入在任何 agent 被派发之前读记录上的
83
+ * 这一位,读不到就整条拒(`workflow.park_truth_unreadable` / `parks_unreadable`)——不是「少一条 park」,
84
+ * 是**这条 run 再也 resume 不了**;它同时也是本仓 `/decide` 的 workflow 车道复核「这条 park 属于这条 run」
85
+ * 的**单一真源**(S-252)。实现上三级全部走 `{...上一级}` 展开、没有任何一级点名裁它,机器背书 =
86
+ * `test/slim-oversize-run.test.ts` 里那两格(最狠的一级上 `parks` 仍逐字完整 + 负控:记录本来没有
87
+ * 这一位时不许凭空补 `[]`)。缺席 ⇒ 不补:补 `[]` 会把「这版引擎没写这个位」伪造成「这条 run 没有 park」,
88
+ * 而 core 对两者的处置相反。
87
89
  *
88
- * 阶梯 = 一张**按价值排序的放弃清单**,一级一级试到装得下为止(**判据只有一条:恢复身份最后放弃**):
90
+ * 🔴 **S-252(7.75.0):阶梯不再为 park 另存第二份恢复身份。** 7.71.0 的 S-192 曾在 `agents[]` 里逐行保住
91
+ * `callKey`/`sessionId`/`parkedCheckpointToken`(外加两级 `label` 预算与一枚不可恢复标记),因为当时
92
+ * `/decide` 的复核只认那一行。core 7.17.0 之后同一条记录上有了 `parks[]`,那份保留就成了**第二个会漂的
93
+ * 定义**(真案:最后一级清空 `agents` 之后 token 仍躺在同一个 blob 的 `parks` 里,而 `/decide` 照拒)。
94
+ * ⇒ 整段退役,判别式改读单一真源。退役后的阶梯每一级都**不大于**退役前的对应级,物理下界不劣化。
95
+ *
96
+ * 阶梯 = 一张**按价值排序的放弃清单**,一级一级试到装得下为止:
89
97
  * step 1 —— 两个无界字符串(`result` / `error`)截断带标记;
90
- * step 2 —— `phases` / `groups` 整段丢,`agents` 收敛成**恢复身份行**
91
- * ({@link buildParkedRecoveryIdentities};非 parked 的行不带恢复身份 ⇒ 整行丢,与修前同);
92
- * step 3 —— 顶层**观测载荷**(codex r1-[high],红先复现后修):`effectiveArgs` 与 `resultFull` 整段丢、
93
- * `name` / `description` 先脱后切。core 对 `effectiveArgs` 的成文口径逐字是 OBSERVATION-ONLY
94
- * (never a gate input,never re-read by the engine;resume 的身份由 journal 的 callKey 携带,
95
- * 不是这个字段),`resultFull` 同类(取回面),两者都**无界**。修前这一级不存在,于是阶梯
96
- * 一边原样留着它们、一边把唯一那条 parked 行删掉盖标记 —— 自己违反了自己那条判据,
97
- * 一条本来赎得回的审批被判死(生产上界 4 MiB 上真复现);
98
- * step 4 —— 恢复身份行上的**展示名**(`label`)—— 它是这条行上唯一可弃的展示物,身份四位恒在
99
- * (codex r2-[high],红先复现后修:修前阶梯一边留着每行 200 字符的 label、一边把**全部**身份
100
- * 删掉盖标记 —— 同一条判据第二次被自己违反;6000 条 CJK label 的 parked 行在生产上界上
101
- * 实测 `agents:[]` + `parks:6000`,而清空 label 后 6000 条身份**全都装得下**);
102
- * step 5 —— 连只剩身份的行都装不下 ⇒ 丢掉它们并落**显式不可恢复标记**({@link PARK_IDENTITY_LOSS_KEY}),
103
- * `/decide` 与 SLA sweep 读到它就**响亮拒**,而不是让一条赎回不了的 park 看起来像普通 miss。
98
+ * step 2 —— 展示载荷整段丢(`phases` / `agents` / `groups`),`error` 收成短串;
99
+ * step 3 —— 顶层**观测载荷**:`effectiveArgs` 与 `resultFull` 整段丢、`name` / `description` 先脱后切。
100
+ * core 对 `effectiveArgs` 的成文口径逐字是 OBSERVATION-ONLY(never a gate input,never re-read by
101
+ * the engine;resume 的身份由 journal 的 callKey 携带,不是这个字段),`resultFull` 同类(取回面),
102
+ * 两者都**无界**;`name` / `description` 是脚本(LLM 撰写)经 `export const meta` 供的展示头,
103
+ * 同样无界 ⇒ **先脱后切**(切口会把读面脱敏所依赖的闭合引号一起切掉,半截凭据原样上 wire)。
104
104
  * Returns null only when even the marked skeleton is oversize (physics — caller keeps the prior revision,
105
105
  * matching the old behavior for that corner). Dialect-neutral (pure JS, no SQL) — shared verbatim by both twins. */
106
106
  export declare function slimOversizeRun(run: WorkflowRun & {
@@ -1,7 +1,7 @@
1
1
  import { redactErrorMessage, redactHead } from "../observability/run-terminal-log.js";
2
2
  import { summarizeWorkflowRun, isTerminalWorkflowStatus, WorkflowRunStoreError } from "@sema-agent/core";
3
3
  import { foldKeyFamily, MAX_PENDING_PER_SESSION, PURGE_FENCE_MS, SERVED_FENCE_MS, } from "../orchestration/workflow-completion-inbox.js";
4
- import { buildParkedRecoveryIdentities, buildParkIdentityLoss, PARK_IDENTITY_LOSS_KEY } from "../orchestration/workflow-agent-session-index.js";
4
+ import {} from "../orchestration/workflow-agent-session-index.js";
5
5
  import { mysqlDriver, pgDriver } from "./sql-driver.js";
6
6
  import { isDupKeyError } from "./sql-errors.js";
7
7
  export const MAX_RUN_BLOB_BYTES = 4 * 1024 * 1024;
@@ -24,9 +24,8 @@ export function slimOversizeRun(run, maxBytes) {
24
24
  let blob = JSON.stringify(step1);
25
25
  if (Buffer.byteLength(blob) <= maxBytes)
26
26
  return blob;
27
- const identities = buildParkedRecoveryIdentities(run.agents);
28
27
  const shortError = step1.error ? String(step1.error).slice(0, 1000) + MARK : MARK;
29
- const step2 = { ...step1, phases: [], agents: identities, groups: [], error: shortError };
28
+ const step2 = { ...step1, phases: [], agents: [], groups: [], error: shortError };
30
29
  blob = JSON.stringify(step2);
31
30
  if (Buffer.byteLength(blob) <= maxBytes)
32
31
  return blob;
@@ -38,19 +37,6 @@ export function slimOversizeRun(run, maxBytes) {
38
37
  if (typeof step3.description === "string")
39
38
  step3.description = redactHead(step3.description, SLIM_HEADER_MAX);
40
39
  blob = JSON.stringify(step3);
41
- if (Buffer.byteLength(blob) <= maxBytes)
42
- return blob;
43
- const bareIdentities = buildParkedRecoveryIdentities(run.agents, 0);
44
- const step4 = { ...step3, agents: bareIdentities };
45
- blob = JSON.stringify(step4);
46
- if (Buffer.byteLength(blob) <= maxBytes)
47
- return blob;
48
- const step5 = {
49
- ...step4,
50
- agents: [],
51
- ...(identities.length > 0 ? { [PARK_IDENTITY_LOSS_KEY]: buildParkIdentityLoss(identities.length) } : {}),
52
- };
53
- blob = JSON.stringify(step5);
54
40
  return Buffer.byteLength(blob) <= maxBytes ? blob : null;
55
41
  }
56
42
  export class SqlWorkflowRunStore {
@@ -5,7 +5,18 @@ export declare const MAX_CWD_CHARS = 4096;
5
5
  * over-long, and NUL-byte injection. NOT an existence check (the env surfaces a missing dir at first tool use).
6
6
  * Defense-in-depth (double-review LOW): `path.isAbsolute` is SYNTACTIC — `/a/../../etc` is "absolute" yet contains
7
7
  * `..` traversal that the host env would `path.resolve` away. Single-user host lane = the caller's own machine (no
8
- * cross-tenant escape), but we reject any path with a `..` segment so the honored cwd is the literal path sent. */
8
+ * cross-tenant escape), but we reject any path with a `..` segment so the honored cwd is the literal path sent.
9
+ *
10
+ * 🔴 **接缝登记(7.74.0 / core 7.17.0 [ref],今日无可达分歧 —— 登记的是「以后会分歧」)**:这条判断回答的是
11
+ * 「引擎眼里这条 cwd 算不算绝对」,而引擎自本版起按**树的家族**判(`isAbsoluteForFamily(pathFamilyOf({root,cwd}), p)`,
12
+ * 家族从 root/cwd 的**拼法**读,**从不**读 `process.platform`)。本函数用的是 Node 的宿主口径,两者:
13
+ * · **posix 家族逐字同判**(2026-09-12 实测六形 `/etc/passwd` / `a/b` / `\\host\share\x` / `C:\x` /
14
+ * `//host/share/x` / `/c/x` 六格全同)⇒ 今天(Linux 宿主 + posix 树,本位又只在单用户 `host` 车道上场)
15
+ * **零分歧**,不改;
16
+ * · **win 家族分歧在 `/`-开头但没有盘符的那一类**(`/etc/passwd`、`/c/x`):Node 的 win32 口径说「绝对」,
17
+ * 引擎说「rooted 但未完全限定」(要拼上基准的盘/共享根才成一条真路径)。⇒ **哪天 `host` 车道要接 win 树,
18
+ * 先改这里**:把判据换成 core 的 `isAbsoluteForFamily`,并把家族从这条 cwd 自己的拼法取。
19
+ * 钉见 `test/task-cwd.test.ts` 的「[ref] 路径家族接缝」格(实测读数,不是推断)。 */
9
20
  export declare function isValidCwd(cwd: unknown): cwd is string;
10
21
  /**
11
22
  * The deployment-level gate: does THIS deployment honor a caller-supplied `cwd`? TRUE only for the single-user
@@ -344,6 +344,12 @@ export interface FsWriteGateWiring {
344
344
  * to confine the exemption to POSIX-absolute canonical keys — core's `canonicalizeTarget` short-circuits
345
345
  * backslash-UNC forms to `ok:true, key=raw` WITHOUT consulting the env, and an unconfined name-keyed grant
346
346
  * would turn those unresolvable forms into an allow (codex review 2026-08-05).
347
+ * 🔴 **坐标刷新(7.74.0 / core 7.17.0 [ref];结论不变,前提收窄了)**:那条短路现在**只在 `win` 家族上**
348
+ * (`dist/tools/fs/safety.js` 的 `canonicalizeTarget`:`family === "win" && isUncSpelling(...)` 才折 UNC
349
+ * 并直接返回);在 `posix` 家族上,同一个反斜杠 UNC 拼法在更早一步的 `win32NamespaceScreen` 上就被
350
+ * **拒**(`unc_on_posix_tree`),压根到不了短路。⇒ 今日部署形(posix 树)这条腿的暴露面比当年更小,
351
+ * 而本臂的 POSIX-absolute 收敛**继续是对的**:它挡的是「未经 env 解析的 key 拿到 name-keyed 放行」,
352
+ * 在两个家族上都成立,且方向仍是 fail-closed。不改实现。
347
353
  * The probe is consulted by the gate RIGHT BEFORE it would ask (after the exempt/accept dir layering — dist 亲读:true ⇒
348
354
  * allow with decisionReason "rule", a THROWN probe ⇒ not exempt = fail-closed to ask). Same canonical
349
355
  * toolName key space as the ask-policy layer's exemption probe — ONE grant serves both layers; the gate-level
@@ -148,10 +148,16 @@ type _GuardSettlementReverse = AssertAllKeysHandled<Exclude<SettlementProjected,
148
148
  type SettlementWhoProjected = "party" | "approver" | "window";
149
149
  type _GuardSettlementWho = AssertAllKeysHandled<Exclude<SettlementWhoKeys, SettlementWhoProjected>>;
150
150
  type _GuardSettlementWhoReverse = AssertAllKeysHandled<Exclude<SettlementWhoProjected, SettlementWhoKeys>>;
151
- type WorkflowRunForwarded = "id" | "scope" | "status" | "startedAt" | "endedAt" | "createdAt" | "rev" | "name" | "description" | "result" | "error" | "errorCode" | "agentFailures" | "stats" | "phases" | "agents" | "groups";
151
+ type WorkflowRunForwarded = "id" | "scope" | "status" | "startedAt" | "endedAt" | "createdAt" | "rev" | "parks" | "name" | "description" | "result" | "error" | "errorCode" | "agentFailures" | "stats" | "phases" | "agents" | "groups";
152
152
  type WorkflowRunStripped = "sourceTaskId" | "originatingSessionId" | "effectiveArgs" | "budgetOvershoot" | "timeoutInterruption" | "resultFull" | "completionId" | "resume" | "journalSkips";
153
153
  type _GuardWorkflowRun = AssertAllKeysHandled<Exclude<keyof WorkflowRun, WorkflowRunForwarded | WorkflowRunStripped>>;
154
154
  type _GuardWorkflowRunReverse = AssertAllKeysHandled<Exclude<WorkflowRunForwarded | WorkflowRunStripped, keyof WorkflowRun>>;
155
+ type ParkRowKeys = keyof NonNullable<WorkflowRun["parks"]>[number];
156
+ type ParkRowProjected = "callKey" | "sessionId" | "originRunId";
157
+ type ParkRowWithheld = "token";
158
+ type _GuardParkRow = AssertAllKeysHandled<Exclude<ParkRowKeys, ParkRowProjected | ParkRowWithheld>>;
159
+ type _NoDoubleBookParkRow = AssertAllKeysHandled<Extract<ParkRowProjected, ParkRowWithheld>>;
160
+ type _GuardParkRowReverse = AssertAllKeysHandled<Exclude<ParkRowProjected | ParkRowWithheld, ParkRowKeys>>;
155
161
  export type CoreKeysetGuards = [
156
162
  _GuardNotification,
157
163
  _GuardNotificationReverse,
@@ -223,7 +229,10 @@ export type CoreKeysetGuards = [
223
229
  _GuardSettlementWho,
224
230
  _GuardSettlementWhoReverse,
225
231
  _GuardWorkflowRun,
226
- _GuardWorkflowRunReverse
232
+ _GuardWorkflowRunReverse,
233
+ _GuardParkRow,
234
+ _NoDoubleBookParkRow,
235
+ _GuardParkRowReverse
227
236
  ];
228
237
  export {};
229
238
  //# sourceMappingURL=core-keyset-guard.d.ts.map