@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,13 +1,17 @@
1
1
  import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
2
  import { join, resolve } from "node:path";
3
- import { SessionError, validateEntriesForImport } from "@sema-agent/core";
3
+ import { FileSessionRepo, SessionError, validateEntriesForImport } from "@sema-agent/core";
4
+ import { z } from "zod";
4
5
  import { contentForkRelation, fastForwardSharedContentDiverged, identicalIdsAlsoIdenticalContent } from "../session-sync-content.js";
5
6
  import { quarantineUnreadableSidecar } from "./local-sidecar-quarantine.js";
6
- import { decodeJsonColumn, readStringRecordColumn, SqlJsonColumnError } from "./sql-json-column.js";
7
+ import { createZodColumnReader, decodeJsonColumn, readStringRecordColumn, SqlJsonColumnError } from "./sql-json-column.js";
8
+ import { ONE_TIME_MIGRATIONS, migrationWindowOpen } from "./one-time-migrations.js";
9
+ import { createLogger } from "../observability/logger.js";
7
10
  import { UNMANAGED_RETENTION } from "./retention-store-sql.js";
8
11
  import { carriedIncarnation, mintSessionIncarnation, sessionFaceFromRow } from "../session-incarnation.js";
9
12
  import { decodeInstructionSource, encodeInstructionSource, sameInstructionSourceBaseline } from "../instruction-source-notice.js";
10
- import { openSessionForItself } from "../session-records-skipped-notice.js";
13
+ import { noteReplayInSessionOpenScope, openSessionForItself } from "../session-records-skipped-notice.js";
14
+ import { createCorruptReadFailOpenSeat } from "../observability/corrupt-read-seat.js";
11
15
  import { classifySyncRelationshipByIds, SyncConflictError, stagingIdFor, } from "../session-sync-kernel.js";
12
16
  const ownerEq = (a, b) => (a ?? null) === (b ?? null);
13
17
  const isNotFound = (e) => e instanceof SessionError && e.code === "not_found";
@@ -30,18 +34,32 @@ function userText(entry) {
30
34
  }
31
35
  return null;
32
36
  }
33
- const SIDECAR_DEGRADES = { lastActivity: "recency", title: "auto-title", "instruction-source": "instruction-source baseline", incarnation: "session-incarnation (the write that needed it failed loudly)" };
37
+ const SIDECAR_DEGRADES = { lastActivity: "recency", title: "auto-title", "instruction-source": "instruction-source baseline", "session-meta": "session owner + incarnation (the write that needed it failed loudly)" };
38
+ function readSessionMetaCell(cell) {
39
+ return typeof cell === "string" ? { incarnation: cell, owner: undefined } : cell;
40
+ }
41
+ const ownerRecorded = (cell) => cell !== undefined && readSessionMetaCell(cell).owner !== undefined;
42
+ const SESSION_META_SIDECAR = z.record(z.string(), z.union([z.string(), z.object({ incarnation: z.string(), owner: z.string().nullable() }).strict()]));
43
+ const readSessionMetaSidecar = createZodColumnReader(SESSION_META_SIDECAR);
44
+ function ownerClaimWindowOpen() {
45
+ const window = ONE_TIME_MIGRATIONS.find((w) => w.id === "M10");
46
+ if (window === undefined)
47
+ throw new Error("LocalSessionStore: the one-time migration registry has no M10 row (the local session-owner claim window) — the claim arm outlived its registry row; delete it with that row");
48
+ return migrationWindowOpen(window, new Date().toISOString().slice(0, 10));
49
+ }
34
50
  const SIDECAR_MAP_STATE_BY_PATH = new Map();
35
- class SidecarStringMap {
51
+ class SidecarMap {
36
52
  path;
37
53
  label;
54
+ read;
38
55
  onUnreadable;
39
56
  onWriteFailed;
40
57
  strict;
41
58
  own = { entries: new Map(), writeChain: Promise.resolve() };
42
- constructor(path, label, onUnreadable, onWriteFailed, strict = false) {
59
+ constructor(path, label, read, onUnreadable, onWriteFailed, strict = false) {
43
60
  this.path = path;
44
61
  this.label = label;
62
+ this.read = read;
45
63
  this.onUnreadable = onUnreadable;
46
64
  this.onWriteFailed = onWriteFailed;
47
65
  this.strict = strict;
@@ -59,7 +77,7 @@ class SidecarStringMap {
59
77
  if (!st.loaded) {
60
78
  st.loaded = (async () => {
61
79
  try {
62
- const parsed = decodeJsonColumn(await readFile(path, "utf8"), readStringRecordColumn, this.label);
80
+ const parsed = decodeJsonColumn(await readFile(path, "utf8"), this.read, this.label);
63
81
  for (const [id, v] of Object.entries(parsed)) {
64
82
  if (!st.entries.has(id))
65
83
  st.entries.set(id, v);
@@ -73,45 +91,60 @@ class SidecarStringMap {
73
91
  return;
74
92
  }
75
93
  const kind = e instanceof SqlJsonColumnError ? e.problem : (e?.code ?? "unreadable");
76
- throw new Error(`${this.label} is unreadable (${kind}) — refusing to read or mint session incarnations until it is repaired: a replacement ` +
77
- `token for an existing session would make it a different occupant (its memory opt-out would stop binding it), and a write ` +
78
- `would overwrite the unreadable bytes. The file is left in place at ${path}.`);
94
+ throw new Error(`${this.label} is unreadable (${kind}) — refusing to read or write session owners and incarnations until it is repaired: ` +
95
+ `reading it as empty would turn every session ownerless, and a replacement incarnation token for an existing session would ` +
96
+ `make it a different occupant (its memory opt-out would stop binding it); a write would overwrite the unreadable bytes. ` +
97
+ `The file is left in place at ${path}.`);
79
98
  }
80
99
  })();
81
100
  }
82
101
  return st.loaded;
83
102
  }
84
103
  persist() {
85
- return this.commit(() => () => undefined);
104
+ return this.commit(() => true);
86
105
  }
87
106
  commit(change) {
88
107
  const path = this.path;
89
108
  const st = this.state;
90
109
  const link = st.writeChain.then(async () => {
91
- const undo = change(st.entries);
92
- if (undo === undefined || !path)
93
- return true;
94
- try {
95
- const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
96
- await writeFile(tmp, JSON.stringify(Object.fromEntries(st.entries)), "utf8");
97
- await rename(tmp, path);
110
+ const base = new Map(st.entries);
111
+ const draft = new Map(base);
112
+ if (!change(draft))
98
113
  return true;
114
+ if (path) {
115
+ try {
116
+ const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
117
+ await writeFile(tmp, JSON.stringify(Object.fromEntries(draft)), "utf8");
118
+ await rename(tmp, path);
119
+ }
120
+ catch (e) {
121
+ this.onWriteFailed(e);
122
+ return false;
123
+ }
99
124
  }
100
- catch (e) {
101
- undo();
102
- this.onWriteFailed(e);
103
- return false;
125
+ for (const key of new Set([...base.keys(), ...draft.keys()])) {
126
+ if (draft.has(key) === base.has(key) && draft.get(key) === base.get(key))
127
+ continue;
128
+ if (draft.has(key))
129
+ st.entries.set(key, draft.get(key));
130
+ else
131
+ st.entries.delete(key);
104
132
  }
133
+ return true;
105
134
  });
106
135
  st.writeChain = link.then(() => undefined);
107
136
  return link;
108
137
  }
109
138
  }
139
+ export function createLocalSessionRepo(root) {
140
+ return new FileSessionRepo(root, {
141
+ onCorruptRead: createCorruptReadFailOpenSeat("server.session-repo.corrupt-record-skipped", noteReplayInSessionOpenScope),
142
+ });
143
+ }
110
144
  export class LocalSessionStore {
111
145
  repo;
112
146
  retention = UNMANAGED_RETENTION;
113
147
  pending = new Map();
114
- owners = new Map();
115
148
  lastActivity = new Map();
116
149
  lastTaskRun = new Map();
117
150
  stagings = new Map();
@@ -124,48 +157,63 @@ export class LocalSessionStore {
124
157
  sidecarPath;
125
158
  titles;
126
159
  instructionSources;
127
- incarnations;
128
- constructor(repo, dataDir) {
160
+ meta;
161
+ log;
162
+ constructor(repo, dataDir, opts) {
129
163
  this.repo = repo;
130
164
  this.sidecarPath = dataDir ? join(dataDir, "session-last-activity.json") : undefined;
131
- const sidecar = (file, which, strict = false) => new SidecarStringMap(dataDir ? resolve(join(dataDir, file)) : undefined, `${file} (the local ${which} sidecar)`, (path, e) => this.refuseSidecar(path, `LocalSessionStore ${which} sidecar`, e, which), (e) => this.warnSidecar("write", e, which), strict);
132
- this.titles = sidecar("session-titles.json", "title");
133
- this.instructionSources = sidecar("session-instruction-sources.json", "instruction-source");
134
- this.incarnations = sidecar("session-incarnations.json", "incarnation", true);
135
- }
136
- async writeIncarnation(sessionId, next) {
137
- await this.incarnations.ensureLoaded();
138
- const committed = await this.incarnations.commit((entries) => {
139
- const before = entries.get(sessionId);
140
- const value = next(before);
141
- if (value === undefined || value === before)
142
- return undefined;
143
- entries.set(sessionId, value);
144
- return () => {
145
- if (before === undefined)
146
- entries.delete(sessionId);
147
- else
148
- entries.set(sessionId, before);
149
- };
165
+ this.log = opts?.log ?? createLogger();
166
+ const sidecar = (file, which, read, strict = false) => new SidecarMap(dataDir ? resolve(join(dataDir, file)) : undefined, `${file} (the local ${which} sidecar)`, read, (path, e) => this.refuseSidecar(path, `LocalSessionStore ${which} sidecar`, e, which), (e) => this.warnSidecar("write", e, which), strict);
167
+ this.titles = sidecar("session-titles.json", "title", readStringRecordColumn);
168
+ this.instructionSources = sidecar("session-instruction-sources.json", "instruction-source", readStringRecordColumn);
169
+ this.meta = sidecar("session-incarnations.json", "session-meta", readSessionMetaSidecar, true);
170
+ }
171
+ async writeMeta(sessionId, next) {
172
+ await this.meta.ensureLoaded();
173
+ let changed = false;
174
+ const committed = await this.meta.commit((draft) => {
175
+ const value = next(draft.get(sessionId));
176
+ if (value === undefined)
177
+ return false;
178
+ draft.set(sessionId, value);
179
+ changed = true;
180
+ return true;
150
181
  });
151
182
  if (!committed)
152
- throw new SessionError("storage", `LocalSessionStore: session-incarnation sidecar write failed for session ${sessionId} — nothing was recorded`);
183
+ throw new SessionError("storage", `LocalSessionStore: session-meta sidecar write failed for session ${sessionId} — nothing was recorded`);
184
+ return changed;
153
185
  }
154
- mintIncarnation(sessionId) {
155
- return this.writeIncarnation(sessionId, () => mintSessionIncarnation());
186
+ async dropMeta(sessionId) {
187
+ await this.meta.ensureLoaded();
188
+ const committed = await this.meta.commit((draft) => draft.delete(sessionId));
189
+ if (!committed)
190
+ throw new SessionError("storage", `LocalSessionStore: session-meta sidecar write failed while deleting session ${sessionId} — its owner/incarnation cell is still on disk`);
156
191
  }
157
- ensureIncarnation(sessionId) {
158
- return this.writeIncarnation(sessionId, (before) => (before === undefined ? mintSessionIncarnation() : undefined));
192
+ async existsInRepo(sessionId) {
193
+ try {
194
+ await this.repo.open({ id: sessionId, createdAt: "" });
195
+ return true;
196
+ }
197
+ catch (e) {
198
+ if (isNotFound(e))
199
+ return false;
200
+ throw e;
201
+ }
159
202
  }
160
- async sessionFaceOf(sessionId) {
161
- const owner = await this.ownerOf(sessionId);
162
- if (owner === undefined)
163
- return undefined;
164
- await this.ensureIncarnation(sessionId);
165
- const incarnation = this.incarnations.state.entries.get(sessionId);
166
- if (!this.owners.has(sessionId) || (this.owners.get(sessionId) ?? null) !== owner)
203
+ async metaOf(sessionId) {
204
+ await this.meta.ensureLoaded();
205
+ const cell = this.meta.state.entries.get(sessionId);
206
+ if (cell !== undefined)
207
+ return readSessionMetaCell(cell);
208
+ if (!(await this.existsInRepo(sessionId)))
167
209
  return undefined;
168
- return sessionFaceFromRow(sessionId, owner, incarnation);
210
+ await this.writeMeta(sessionId, (before) => (before === undefined ? mintSessionIncarnation() : undefined));
211
+ const minted = this.meta.state.entries.get(sessionId);
212
+ return minted === undefined ? undefined : readSessionMetaCell(minted);
213
+ }
214
+ async sessionFaceOf(sessionId) {
215
+ const meta = await this.metaOf(sessionId);
216
+ return meta === undefined ? undefined : sessionFaceFromRow(sessionId, meta.owner ?? null, meta.incarnation);
169
217
  }
170
218
  async probeTitle(sessionId) {
171
219
  await this.titles.ensureLoaded();
@@ -186,17 +234,11 @@ export class LocalSessionStore {
186
234
  }
187
235
  async recordInstructionSource(sessionId, expected, source) {
188
236
  await this.instructionSources.ensureLoaded();
189
- const committed = await this.instructionSources.commit((entries) => {
190
- const before = entries.get(sessionId);
191
- if (!sameInstructionSourceBaseline(decodeInstructionSource(before), expected))
192
- return undefined;
193
- entries.set(sessionId, encodeInstructionSource(source));
194
- return () => {
195
- if (before === undefined)
196
- entries.delete(sessionId);
197
- else
198
- entries.set(sessionId, before);
199
- };
237
+ const committed = await this.instructionSources.commit((draft) => {
238
+ if (!sameInstructionSourceBaseline(decodeInstructionSource(draft.get(sessionId)), expected))
239
+ return false;
240
+ draft.set(sessionId, encodeInstructionSource(source));
241
+ return true;
200
242
  });
201
243
  if (!committed)
202
244
  throw new Error("LocalSessionStore: instruction-source sidecar write failed — baseline not recorded");
@@ -271,11 +313,10 @@ export class LocalSessionStore {
271
313
  }
272
314
  async acquire(sessionId, opts) {
273
315
  if (!sessionId) {
274
- await this.incarnations.ensureLoaded();
316
+ await this.meta.ensureLoaded();
275
317
  const session = await this.repo.create({});
276
318
  const id = (await session.getMetadata()).id;
277
- this.owners.set(id, null);
278
- await this.mintIncarnation(id);
319
+ await this.writeMeta(id, () => ({ incarnation: mintSessionIncarnation(), owner: null }));
279
320
  return { session: session, sessionId: id };
280
321
  }
281
322
  if (opts?.requireExisting)
@@ -290,8 +331,6 @@ export class LocalSessionStore {
290
331
  async load(sessionId, requireExisting = false) {
291
332
  try {
292
333
  const session = await openSessionForItself(sessionId, () => this.repo.open({ id: sessionId, createdAt: "" }));
293
- if (!this.owners.has(sessionId))
294
- this.owners.set(sessionId, null);
295
334
  return { session: session, sessionId };
296
335
  }
297
336
  catch (e) {
@@ -299,56 +338,47 @@ export class LocalSessionStore {
299
338
  throw e;
300
339
  if (requireExisting)
301
340
  throw e;
302
- await this.incarnations.ensureLoaded();
341
+ await this.meta.ensureLoaded();
303
342
  const session = await this.repo.create({ id: sessionId });
304
- await this.mintIncarnation(sessionId);
305
- if (!this.owners.has(sessionId))
306
- this.owners.set(sessionId, null);
343
+ await this.writeMeta(sessionId, (before) => ({ incarnation: mintSessionIncarnation(), owner: before !== undefined && ownerRecorded(before) ? (readSessionMetaCell(before).owner ?? null) : null }));
307
344
  return { session: session, sessionId };
308
345
  }
309
346
  }
310
347
  async register(sessionId, owner) {
311
- if (this.owners.has(sessionId))
348
+ await this.meta.ensureLoaded();
349
+ if (ownerRecorded(this.meta.state.entries.get(sessionId)))
312
350
  return;
313
- await this.incarnations.ensureLoaded();
314
- this.owners.set(sessionId, owner);
315
- try {
316
- await this.repo.open({ id: sessionId, createdAt: "" });
317
- await this.ensureIncarnation(sessionId);
318
- }
319
- catch (e) {
320
- if (!isNotFound(e))
321
- throw e;
351
+ const existed = await this.existsInRepo(sessionId);
352
+ const claim = existed && owner !== null && ownerClaimWindowOpen();
353
+ const wrote = await this.writeMeta(sessionId, (before) => {
354
+ if (ownerRecorded(before))
355
+ return undefined;
356
+ if (!existed || claim)
357
+ return { incarnation: existed && before !== undefined ? readSessionMetaCell(before).incarnation : mintSessionIncarnation(), owner };
358
+ return before === undefined ? mintSessionIncarnation() : undefined;
359
+ });
360
+ if (!existed && wrote)
322
361
  await this.repo.create({ id: sessionId });
323
- await this.mintIncarnation(sessionId);
324
- }
362
+ if (claim && wrote)
363
+ this.log.info("session_owner_claimed", { sessionId });
325
364
  }
326
365
  async ownerOf(sessionId) {
327
- if (this.owners.has(sessionId))
328
- return this.owners.get(sessionId) ?? null;
329
- try {
330
- await this.repo.open({ id: sessionId, createdAt: "" });
331
- this.owners.set(sessionId, null);
332
- return null;
333
- }
334
- catch (e) {
335
- if (isNotFound(e))
336
- return undefined;
337
- throw e;
338
- }
366
+ const meta = await this.metaOf(sessionId);
367
+ return meta === undefined ? undefined : (meta.owner ?? null);
339
368
  }
340
369
  async getHead(sessionId) {
341
- if (!this.owners.has(sessionId) && (await this.ownerOf(sessionId)) === undefined)
370
+ const before = await this.metaOf(sessionId);
371
+ if (before === undefined)
342
372
  return undefined;
343
- const owner = this.owners.get(sessionId) ?? null;
344
373
  const leafId = await this.getLeafId(sessionId);
345
- if (!this.owners.has(sessionId) || (this.owners.get(sessionId) ?? null) !== owner)
374
+ const cell = this.meta.state.entries.get(sessionId);
375
+ const after = cell === undefined ? undefined : readSessionMetaCell(cell);
376
+ if (after === undefined || after.incarnation !== before.incarnation || (after.owner ?? null) !== (before.owner ?? null))
346
377
  return undefined;
347
- return { owner, leafId };
378
+ return { owner: before.owner ?? null, leafId };
348
379
  }
349
- restampOwner(sessionId, owner) {
350
- if (this.owners.has(sessionId))
351
- this.owners.set(sessionId, owner);
380
+ async restampOwner(sessionId, owner) {
381
+ await this.writeMeta(sessionId, (before) => (before === undefined ? undefined : { incarnation: readSessionMetaCell(before).incarnation, owner }));
352
382
  }
353
383
  async touch(sessionId) {
354
384
  this.lastActivity.set(sessionId, new Date().toISOString());
@@ -371,10 +401,12 @@ export class LocalSessionStore {
371
401
  async listSessions(opts) {
372
402
  await this.ensureLoaded();
373
403
  await this.titles.ensureLoaded();
404
+ await this.meta.ensureLoaded();
374
405
  const metas = await this.repo.list();
375
406
  const all = [];
376
407
  for (const meta of metas) {
377
- const owner = this.owners.has(meta.id) ? (this.owners.get(meta.id) ?? null) : null;
408
+ const cell = this.meta.state.entries.get(meta.id);
409
+ const owner = cell === undefined ? null : (readSessionMetaCell(cell).owner ?? null);
378
410
  if (opts.owner !== undefined && owner !== opts.owner && !(opts.includeUnowned === true && owner === null))
379
411
  continue;
380
412
  const firstActivityAt = meta.createdAt || new Date(0).toISOString();
@@ -423,12 +455,11 @@ export class LocalSessionStore {
423
455
  }
424
456
  }
425
457
  async fork(sourceId, owner) {
426
- await this.incarnations.ensureLoaded();
458
+ await this.meta.ensureLoaded();
427
459
  try {
428
460
  const session = await this.repo.fork({ id: sourceId, createdAt: "" }, {});
429
461
  const id = (await session.getMetadata()).id;
430
- this.owners.set(id, owner);
431
- await this.mintIncarnation(id);
462
+ await this.writeMeta(id, () => ({ incarnation: mintSessionIncarnation(), owner }));
432
463
  return id;
433
464
  }
434
465
  catch (e) {
@@ -471,10 +502,9 @@ export class LocalSessionStore {
471
502
  async importEntriesAs(sessionId, owner, entries, incarnation) {
472
503
  if (!this.repo.importEntries)
473
504
  throw new SessionError("storage", "session import is not supported by this local backend");
474
- await this.incarnations.ensureLoaded();
505
+ await this.meta.ensureLoaded();
475
506
  await this.repo.importEntries(sessionId, owner ?? undefined, entries);
476
- this.owners.set(sessionId, owner);
477
- await this.writeIncarnation(sessionId, () => incarnation);
507
+ await this.writeMeta(sessionId, () => ({ incarnation, owner }));
478
508
  }
479
509
  async replaceEntries(sessionId, owner, entries) {
480
510
  if (!this.repo.importEntries)
@@ -482,8 +512,8 @@ export class LocalSessionStore {
482
512
  if (!this.repo.delete)
483
513
  throw new SessionError("storage", "session replace is not supported by this local backend (no delete seam)");
484
514
  validateEntriesForImport(entries);
485
- await this.incarnations.ensureLoaded();
486
- const prior = this.owners.has(sessionId) ? { owner: this.owners.get(sessionId) ?? null, incarnation: this.incarnations.state.entries.get(sessionId) } : undefined;
515
+ const current = await this.metaOf(sessionId);
516
+ const prior = current === undefined ? undefined : { owner: current.owner ?? null, incarnation: current.incarnation };
487
517
  await this.deleteSession(sessionId, owner);
488
518
  await this.importEntriesAs(sessionId, owner, entries, carriedIncarnation(prior, owner));
489
519
  }
@@ -499,8 +529,8 @@ export class LocalSessionStore {
499
529
  this.stagings.delete(stagingId);
500
530
  }
501
531
  async deleteSession(sessionId, owner) {
502
- const known = this.owners.has(sessionId);
503
- if (!known || !ownerEq(this.owners.get(sessionId) ?? null, owner))
532
+ const current = await this.ownerOf(sessionId);
533
+ if (current === undefined || !ownerEq(current, owner))
504
534
  return false;
505
535
  try {
506
536
  await this.repo.delete({ id: sessionId, createdAt: "" });
@@ -509,10 +539,10 @@ export class LocalSessionStore {
509
539
  if (!isNotFound(e))
510
540
  throw e;
511
541
  }
512
- this.owners.delete(sessionId);
542
+ await this.dropMeta(sessionId);
513
543
  this.lastActivity.delete(sessionId);
514
544
  this.lastTaskRun.delete(sessionId);
515
- for (const map of [this.titles, this.instructionSources, this.incarnations]) {
545
+ for (const map of [this.titles, this.instructionSources]) {
516
546
  await map.ensureLoaded();
517
547
  if (map.state.entries.delete(sessionId))
518
548
  await map.persist();
@@ -572,7 +602,7 @@ class LocalStagingHandle {
572
602
  await this.store.replaceEntries(this.realSessionId, owner, this.staged);
573
603
  }
574
604
  else {
575
- this.store.restampOwner(this.realSessionId, owner);
605
+ await this.store.restampOwner(this.realSessionId, owner);
576
606
  }
577
607
  this.store.forgetStaging(this.stagingId);
578
608
  return { relation: rel.relation };
@@ -28,6 +28,11 @@ export interface OneTimeMigrationReport {
28
28
  readonly allowRuleShadowRowsMigrated: number;
29
29
  /** M8:带旧键字样、但这一格的 JSON 读不懂 ⇒ 没改写的行数(留给读口自己的响亮臂;非零 = 运维该看一眼这些行)。SQL 独有;local 恒 0。 */
30
30
  readonly allowRuleShadowRowsUnreadable: number;
31
+ /** M9:workflow park 指针索引的主键 —— `reshaped` = 本次把只有 `session_id` 的主键改成 `(session_id, run_id)`;`current` = 早已是新形
32
+ * (新库 / 第二跑);`file-ledger` = local 文件车道(账本一行本来就是一条 run 的指针集,没有主键可改)。 */
33
+ readonly workflowParkIndexKey: "reshaped" | "current" | "file-ledger";
34
+ /** M9:改形时原样搬进新表的指针行数(`current` / `file-ledger` 时恒 0)。非零 = 这次迁移真重建了一张表,运维该看见一次。 */
35
+ readonly workflowParkIndexRowsCarried: number;
31
36
  }
32
37
  /**
33
38
  * S-590 —— 一条一次性迁移的窗(登记表的一行;纯数据)。字段注写着每个值的取证方式,改一行之前照着重核。
@@ -60,6 +65,12 @@ export interface OneTimeMigrationWindow {
60
65
  * 而留着 M3 会让 M3 永远判「跑过了」。三条同版引入、同一天到期,本来就同一次删。
61
66
  */
62
67
  export declare const ONE_TIME_MIGRATIONS: readonly OneTimeMigrationWindow[];
68
+ /**
69
+ * 一条登记行的窗**此刻**开着没有(`today` = UTC 日历日 YYYY-MM-DD):未发布形(`removableAfter: null`)⇒ 开;否则今天不晚于 `removableAfter` ⇒ 开
70
+ * (窗关当天仍开,与到期门「今天严格晚于才红」同一条边界)。
71
+ * 只给**窗内才生效的行为臂**用(M10 的认领):那种臂过了窗就停,不等删码车 —— 删码因此是零行为变化,而窗只写在本表这一处。
72
+ */
73
+ export declare function migrationWindowOpen(window: Pick<OneTimeMigrationWindow, "removableAfter">, today: string): boolean;
63
74
  /** local 车道上 SQL 独有的四条(M1 / M2 / M4 / M5)的读数(M3 / M6 两车道都有,local 腿的读数由 `LocalBackend.runOneTimeMigrations` 现算):那几张表 / 列在文件底座上从来不存在 ⇒ 如实答 absent / 0。 */
64
75
  export declare const NO_SQL_MIGRATIONS: Omit<OneTimeMigrationReport, "askOriginRowsMigrated" | "holderDeathCodeRowsBackfilled">;
65
76
  /** 启动期的日志口(logger 的子集)。两级各有其用,见 {@link logOneTimeMigrations}。 */