@sema-agent/server 7.143.0 → 7.145.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.
- package/MIGRATION.md +295 -0
- package/README.md +3 -0
- package/README.zh-CN.md +1 -0
- package/USAGE.md +34 -16
- package/dist/approval-card.d.ts +31 -14
- package/dist/approval-card.js +8 -8
- package/dist/audit.d.ts +2 -21
- package/dist/audit.js +0 -9
- package/dist/boot/execution-env.js +1 -1
- package/dist/boot/runner-deps.d.ts +10 -5
- package/dist/boot/runner-deps.js +5 -4
- package/dist/boot/session-faces.d.ts +1 -1
- package/dist/boot/session-faces.js +3 -3
- package/dist/boot/shutdown.d.ts +1 -1
- package/dist/boot/shutdown.js +4 -2
- package/dist/boot/stage-01-config.js +2 -0
- package/dist/boot/stage-07-capability-layer.js +1 -1
- package/dist/boot/stage-09-leader.d.ts +1 -1
- package/dist/boot/stage-10-http-server.d.ts +1 -1
- package/dist/boot/stage-10-http-server.js +4 -9
- package/dist/capabilities/scenarios.d.ts +14 -18
- package/dist/capabilities/scenarios.js +1 -2
- package/dist/config-d-migration.d.ts +3 -0
- package/dist/delegated-session-owner.d.ts +33 -151
- package/dist/delegated-session-owner.js +1 -83
- package/dist/entry-mode-clamp-notice.d.ts +1 -1
- package/dist/execution-placement.d.ts +7 -4
- package/dist/fleet/fleet-bus.d.ts +13 -3
- package/dist/fleet/fleet-bus.js +17 -3
- package/dist/hooks/classic-stations.d.ts +3 -1
- package/dist/hooks/classic-stations.js +2 -0
- package/dist/http/admission.js +2 -1
- package/dist/http/checkpoint-ctx.d.ts +1 -0
- package/dist/http/checkpoint-ctx.js +1 -0
- package/dist/http/durable-run-leg.js +7 -6
- package/dist/http/principal-gate.d.ts +1 -1
- package/dist/http/refusal-body.d.ts +7 -3
- package/dist/http/refusal-body.js +1 -0
- package/dist/http/resume-legs.js +16 -7
- package/dist/http/route-ctx.d.ts +15 -0
- package/dist/http/routes/approvals-assistant.d.ts +20 -1
- package/dist/http/routes/approvals-assistant.js +28 -6
- package/dist/http/routes/rules.d.ts +1 -1
- package/dist/http/routes/rules.js +5 -2
- package/dist/http/routes/session-sync.js +12 -0
- package/dist/http/routes/tasks.js +12 -9
- package/dist/http/run-meta.d.ts +14 -7
- package/dist/http/run-meta.js +5 -2
- package/dist/http/server.d.ts +13 -5
- package/dist/http/server.js +3 -0
- package/dist/http/submit-ctx.d.ts +12 -0
- package/dist/http/submit-ctx.js +23 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lane-home-probe.d.ts +15 -0
- package/dist/lane-home-probe.js +4 -0
- package/dist/observability/corrupt-read-seat.d.ts +5 -9
- package/dist/observability/corrupt-read-seat.js +1 -2
- package/dist/observability/fail-open.d.ts +26 -10
- package/dist/observability/fail-open.js +26 -10
- package/dist/observability/run-terminal.d.ts +1 -1
- package/dist/observability/run-terminal.js +2 -2
- package/dist/parked-revive-surface.d.ts +6 -7
- package/dist/peer-inbound-face.d.ts +29 -34
- package/dist/peer-inbound-face.js +28 -63
- package/dist/plugins/approval-ask-store-file.d.ts +16 -2
- package/dist/plugins/approval-ask-store-file.js +21 -1
- package/dist/plugins/approval-ask-store-memory.d.ts +8 -2
- package/dist/plugins/approval-ask-store-memory.js +11 -8
- package/dist/plugins/caching-session-store.d.ts +3 -3
- package/dist/plugins/caching-session-store.js +3 -3
- package/dist/plugins/checkpoint-store-sql.d.ts +62 -37
- package/dist/plugins/checkpoint-store-sql.js +123 -40
- package/dist/plugins/file-run-store.d.ts +13 -1
- package/dist/plugins/file-run-store.js +11 -1
- package/dist/plugins/fork-routing-session-store.d.ts +8 -1
- package/dist/plugins/fork-routing-session-store.js +6 -0
- package/dist/plugins/local-checkpoint-store.d.ts +17 -8
- package/dist/plugins/local-checkpoint-store.js +26 -7
- package/dist/plugins/local-session-store.d.ts +105 -34
- package/dist/plugins/local-session-store.js +168 -65
- package/dist/plugins/local-session-window.d.ts +4 -2
- package/dist/plugins/memory-run-store.d.ts +2 -0
- package/dist/plugins/memory-run-store.js +6 -0
- package/dist/plugins/one-time-migrations.d.ts +12 -12
- package/dist/plugins/one-time-migrations.js +13 -13
- package/dist/plugins/pg-session-storage.d.ts +1 -9
- package/dist/plugins/pg-session-storage.js +2 -19
- package/dist/plugins/remote-env-host.d.ts +8 -4
- package/dist/plugins/remote-env-host.js +7 -9
- package/dist/plugins/remote-shell.js +12 -2
- package/dist/plugins/run-store-sql.d.ts +4 -0
- package/dist/plugins/run-store-sql.js +3 -0
- package/dist/plugins/schema-consistency.d.ts +5 -1
- package/dist/plugins/session-fork.d.ts +1 -1
- package/dist/plugins/session-ownership.d.ts +9 -0
- package/dist/plugins/session-ownership.js +6 -0
- package/dist/plugins/session-store.d.ts +13 -16
- package/dist/plugins/session-store.js +9 -10
- package/dist/plugins/sql-json-column.d.ts +24 -2
- package/dist/plugins/sql-json-column.js +38 -3
- package/dist/plugins/store-backend.js +3 -2
- package/dist/plugins/tidb-session-store.d.ts +2 -14
- package/dist/plugins/tidb-session-store.js +2 -20
- package/dist/prompt-screen.d.ts +21 -0
- package/dist/prompt-screen.js +8 -0
- package/dist/rules-consent.d.ts +63 -51
- package/dist/rules-consent.js +6 -10
- package/dist/run-acceptance.d.ts +40 -10
- package/dist/run-acceptance.js +38 -5
- package/dist/run-local.js +2 -1
- package/dist/runs.js +5 -1
- package/dist/security.d.ts +10 -7
- package/dist/session-agent.d.ts +39 -0
- package/dist/session-agent.js +16 -0
- package/dist/session-error-wire.d.ts +25 -26
- package/dist/session-error-wire.js +4 -5
- package/dist/session-incarnation.d.ts +2 -2
- package/dist/session-notify-door.d.ts +2 -2
- package/dist/session-notify-door.js +5 -1
- package/dist/tool-approval.d.ts +3 -1
- package/dist/tool-approval.js +47 -18
- package/dist/trace/core-keyset-guard.d.ts +1 -1
- package/dist/trace/engine-notice-wire.d.ts +1 -1
- package/dist/trace/engine-notice-wire.js +1 -1
- package/dist/trace/ledger-sink.js +12 -4
- package/dist/trace/model-segment.d.ts +26 -6
- package/dist/trace/model-segment.js +58 -9
- package/dist/trace/project.d.ts +8 -2
- package/dist/trace/project.js +6 -4
- package/dist/trace/redact.d.ts +77 -2
- package/dist/trace/redact.js +234 -99
- package/dist/trace/self-minted-wire.d.ts +160 -9
- package/dist/trace/self-minted-wire.js +52 -3
- package/package.json +3 -3
- package/wire-changes/server@7.144.0.json +291 -0
- package/wire-changes/server@7.145.0.json +146 -0
- package/dist/session-records-skipped-notice.d.ts +0 -21
- package/dist/session-records-skipped-notice.js +0 -27
|
@@ -3,17 +3,17 @@ import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readdirSync, rea
|
|
|
3
3
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
4
4
|
import { isDeepStrictEqual } from "node:util";
|
|
5
5
|
import { dirname, join, resolve } from "node:path";
|
|
6
|
-
import { assertFirstOwnerWritable, forkOwnerOf, importerOwnerOf } from "./session-ownership.js";
|
|
6
|
+
import { assertFirstOwnerWritable, forkOwnerOf, importerOwnerOf, placedSessionOwnerOf } from "./session-ownership.js";
|
|
7
7
|
import { SessionOwnerUnreadableError, StampedSessionArchiveError, sessionUnreadableOf } from "../session-error-wire.js";
|
|
8
8
|
import { FileSessionRepo, SessionError, StoredSession, admitSessionPlacement, atomicWriteFile, sessionLogMarkOf, settledForkOptions, uuidv7, validateEntriesForImport } from "@sema-agent/core";
|
|
9
|
-
import { createPlacedSessionRow, sourceFacesVia } from "../delegated-session-owner.js";
|
|
10
9
|
import { placementCapabilityOf } from "./session-placement.js";
|
|
11
10
|
import { z } from "zod";
|
|
12
11
|
import { contentForkRelation, fastForwardSharedContentDiverged, identicalIdsAlsoIdenticalContent } from "../session-sync-content.js";
|
|
13
12
|
import { StoreBootRefusalError } from "./store-contracts.js";
|
|
14
13
|
import { judgeSessionRowWrite, sessionRowTagKey, settleSessionRowWrite, } from "../session-row.js";
|
|
15
14
|
import { quarantineUnreadableSidecar } from "./local-sidecar-quarantine.js";
|
|
16
|
-
import { createZodColumnReader, decodeJsonColumn, readStringRecordColumn, SqlJsonColumnError } from "./sql-json-column.js";
|
|
15
|
+
import { createZodColumnReader, decodeJsonColumn, isJsonColumnBodyProblem, readStringRecordColumn, SqlJsonColumnError } from "./sql-json-column.js";
|
|
16
|
+
import { SCHEMA_MISMATCH_ERROR_CODE } from "./schema-consistency.js";
|
|
17
17
|
import { ONE_TIME_MIGRATIONS, migrationWindowOpen } from "./one-time-migrations.js";
|
|
18
18
|
import { recordFailOpen } from "../observability/fail-open.js";
|
|
19
19
|
import { createLogger } from "../observability/logger.js";
|
|
@@ -24,7 +24,6 @@ import { SessionForkInProgressError, SessionWriterPresentError, WRITABLE, assert
|
|
|
24
24
|
import { classifyForkTarget, forkByFreshId } from "./session-fork.js";
|
|
25
25
|
import { decodeInstructionSource, encodeInstructionSource, sameInstructionSourceBaseline } from "../instruction-source-notice.js";
|
|
26
26
|
import { decodeSessionWorkspaceCell, encodeSessionWorkspaceCell } from "./session-workspace-cell.js";
|
|
27
|
-
import { noteReplayInSessionOpenScope, openSessionForItself } from "../session-records-skipped-notice.js";
|
|
28
27
|
import { createCorruptReadFailOpenSeat } from "../observability/corrupt-read-seat.js";
|
|
29
28
|
import { buildObjectivePreview, isAttachmentFrame } from "../user-authored-text.js";
|
|
30
29
|
import { classifySyncRelationshipByIds, ownerCellIs, SyncConflictError, StagedCommitOwnerChangedError, stagingIdFor, } from "../session-sync-kernel.js";
|
|
@@ -35,8 +34,8 @@ function archiveFaultWord(code) {
|
|
|
35
34
|
case "not_found":
|
|
36
35
|
return "absent";
|
|
37
36
|
case "corrupt_session":
|
|
38
|
-
case "invalid_session":
|
|
39
37
|
return code;
|
|
38
|
+
case "invalid_session":
|
|
40
39
|
case "invalid_entry":
|
|
41
40
|
case "invalid_fork_target":
|
|
42
41
|
case "storage":
|
|
@@ -58,7 +57,7 @@ function ownerUnreadableArchive(probe) {
|
|
|
58
57
|
const { fault } = probe;
|
|
59
58
|
if (probe.word === "storage" || !(fault instanceof SessionError))
|
|
60
59
|
return probe;
|
|
61
|
-
return { ...probe, fault: new SessionOwnerUnreadableError(
|
|
60
|
+
return { ...probe, fault: new SessionOwnerUnreadableError(fault) };
|
|
62
61
|
}
|
|
63
62
|
function importRowRefused(sessionId, landing) {
|
|
64
63
|
return new SessionError("conflict", landing === "create"
|
|
@@ -154,6 +153,32 @@ function ownerClaimWindowOpen() {
|
|
|
154
153
|
}
|
|
155
154
|
const SIDECAR_MAP_STATE_BY_PATH = new Map();
|
|
156
155
|
const SESSION_WRITE_LOCKS_BY_ROOT = new Map();
|
|
156
|
+
const quotedFieldNames = (names) => `${names.slice(0, 8).map((name) => JSON.stringify(name.slice(0, 64))).join(", ")}${names.length > 8 ? `, … (${names.length} in all)` : ""}`;
|
|
157
|
+
function describeLocalDataRootMismatch(findings, when) {
|
|
158
|
+
const lines = [
|
|
159
|
+
when === "boot"
|
|
160
|
+
? `${SCHEMA_MISMATCH_ERROR_CODE}: the local data root does not match this build's shape — refusing to start.`
|
|
161
|
+
: `${SCHEMA_MISMATCH_ERROR_CODE}: the local data root does not match this build's shape — refusing to read or write the file(s) named below until this process is restarted.`,
|
|
162
|
+
];
|
|
163
|
+
for (const f of findings) {
|
|
164
|
+
lines.push(f.unknownFields.length > 0
|
|
165
|
+
? ` ${f.label} at ${f.path}: UNKNOWN field(s) ${quotedFieldNames(f.unknownFields)} — the data root is NEWER than this build (a newer server engine wrote it).`
|
|
166
|
+
: ` ${f.label} at ${f.path}: unreadable (${f.problem}) — this build cannot read it, and an unreadable file is treated as a mismatch, not as empty.`);
|
|
167
|
+
}
|
|
168
|
+
lines.push("This build never reads such a file as empty and never writes over it: either would silently change what the file records.", "", " Ways out (these three and no others):", " (1) the data root is NEWER than this build (unknown fields): deploy the build that wrote it;", " (2) roll back: restore the data-root backup taken before the upgrade, then start this build — also the way out when the file's bytes are damaged (not-json / malformed);", " (3) the file could not be read at all (an I/O error such as EACCES): repair access to it (permissions, mount, disk) and restart — nothing in the file needs changing.", "The check behind this refusal only reads: the file was left in place and nothing in the data root was changed.");
|
|
169
|
+
return lines.join("\n");
|
|
170
|
+
}
|
|
171
|
+
export class LocalDataRootSchemaMismatchError extends StoreBootRefusalError {
|
|
172
|
+
findings;
|
|
173
|
+
when;
|
|
174
|
+
code = SCHEMA_MISMATCH_ERROR_CODE;
|
|
175
|
+
constructor(findings, when) {
|
|
176
|
+
super(describeLocalDataRootMismatch(findings, when));
|
|
177
|
+
this.findings = findings;
|
|
178
|
+
this.when = when;
|
|
179
|
+
this.name = "LocalDataRootSchemaMismatchError";
|
|
180
|
+
}
|
|
181
|
+
}
|
|
157
182
|
class SidecarMap {
|
|
158
183
|
path;
|
|
159
184
|
label;
|
|
@@ -180,32 +205,68 @@ class SidecarMap {
|
|
|
180
205
|
if (!path)
|
|
181
206
|
return Promise.resolve();
|
|
182
207
|
const st = this.state;
|
|
208
|
+
if (st.refused !== undefined)
|
|
209
|
+
return Promise.reject(st.refused);
|
|
183
210
|
if (!st.loaded) {
|
|
184
211
|
st.loaded = (async () => {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
212
|
+
const disk = await this.readDisk(path);
|
|
213
|
+
if (disk.state === "absent")
|
|
214
|
+
return;
|
|
215
|
+
if (disk.state === "read") {
|
|
216
|
+
for (const [id, v] of Object.entries(disk.entries)) {
|
|
188
217
|
if (!st.entries.has(id))
|
|
189
218
|
st.entries.set(id, v);
|
|
190
219
|
}
|
|
220
|
+
return;
|
|
191
221
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
if (!this.strict) {
|
|
196
|
-
this.onUnreadable(path, e);
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
const kind = e instanceof SqlJsonColumnError ? e.problem : (e?.code ?? "unreadable");
|
|
200
|
-
throw new Error(`${this.label} is unreadable (${kind}) — refusing to read or write session owners and incarnations until it is repaired: ` +
|
|
201
|
-
`reading it as empty would turn every session ownerless, and a replacement incarnation token for an existing session would ` +
|
|
202
|
-
`make it a different occupant (its memory opt-out would stop binding it); a write would overwrite the unreadable bytes. ` +
|
|
203
|
-
`The file is left in place at ${path}.`);
|
|
222
|
+
if (!this.strict) {
|
|
223
|
+
this.onUnreadable(path, disk.fault);
|
|
224
|
+
return;
|
|
204
225
|
}
|
|
226
|
+
st.loaded = undefined;
|
|
227
|
+
throw this.refuse(path, disk);
|
|
205
228
|
})();
|
|
206
229
|
}
|
|
207
230
|
return st.loaded;
|
|
208
231
|
}
|
|
232
|
+
async reconcile() {
|
|
233
|
+
const path = this.path;
|
|
234
|
+
if (!path)
|
|
235
|
+
return;
|
|
236
|
+
const st = this.state;
|
|
237
|
+
if (st.refused !== undefined)
|
|
238
|
+
throw st.refused;
|
|
239
|
+
if (!st.loaded)
|
|
240
|
+
return this.ensureLoaded();
|
|
241
|
+
await st.loaded;
|
|
242
|
+
const disk = await this.readDisk(path);
|
|
243
|
+
if (disk.state === "unreadable")
|
|
244
|
+
throw this.refuse(path, disk);
|
|
245
|
+
}
|
|
246
|
+
async readDisk(path) {
|
|
247
|
+
let text;
|
|
248
|
+
try {
|
|
249
|
+
text = await readFile(path, "utf8");
|
|
250
|
+
}
|
|
251
|
+
catch (e) {
|
|
252
|
+
return e?.code === "ENOENT" ? { state: "absent" } : { state: "unreadable", fault: e };
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
return { state: "read", entries: decodeJsonColumn(text, this.read, this.label) };
|
|
256
|
+
}
|
|
257
|
+
catch (e) {
|
|
258
|
+
return { state: "unreadable", fault: e };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
refuse(path, disk) {
|
|
262
|
+
const shape = isJsonColumnBodyProblem(disk.fault);
|
|
263
|
+
const problem = disk.fault instanceof SqlJsonColumnError ? disk.fault.problem : (disk.fault?.code ?? "unreadable");
|
|
264
|
+
const unknownFields = disk.fault instanceof SqlJsonColumnError ? disk.fault.unknownKeys : [];
|
|
265
|
+
const refusal = new LocalDataRootSchemaMismatchError([{ path, label: this.label, problem, unknownFields }], "serving");
|
|
266
|
+
if (shape)
|
|
267
|
+
this.state.refused = refusal;
|
|
268
|
+
return refusal;
|
|
269
|
+
}
|
|
209
270
|
persist() {
|
|
210
271
|
return this.commit(() => true);
|
|
211
272
|
}
|
|
@@ -222,11 +283,22 @@ class SidecarMap {
|
|
|
222
283
|
const path = this.path;
|
|
223
284
|
const st = this.state;
|
|
224
285
|
const link = st.writeChain.then(async () => {
|
|
286
|
+
if (st.refused !== undefined)
|
|
287
|
+
throw st.refused;
|
|
225
288
|
const base = new Map(st.entries);
|
|
226
289
|
const draft = new Map(base);
|
|
227
290
|
if (!change(draft))
|
|
228
291
|
return true;
|
|
229
292
|
if (path) {
|
|
293
|
+
if (this.strict) {
|
|
294
|
+
const disk = await this.readDisk(path);
|
|
295
|
+
if (disk.state === "unreadable") {
|
|
296
|
+
if (isJsonColumnBodyProblem(disk.fault))
|
|
297
|
+
throw this.refuse(path, disk);
|
|
298
|
+
this.onWriteFailed(disk.fault);
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
230
302
|
try {
|
|
231
303
|
const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
232
304
|
await writeFile(tmp, JSON.stringify(Object.fromEntries(draft)), "utf8");
|
|
@@ -247,7 +319,7 @@ class SidecarMap {
|
|
|
247
319
|
}
|
|
248
320
|
return true;
|
|
249
321
|
});
|
|
250
|
-
st.writeChain = link.then(() => undefined);
|
|
322
|
+
st.writeChain = link.then(() => undefined, () => undefined);
|
|
251
323
|
return link;
|
|
252
324
|
}
|
|
253
325
|
}
|
|
@@ -261,10 +333,7 @@ function noteReplayInArchiveRewriteRead(info) {
|
|
|
261
333
|
}
|
|
262
334
|
export function createLocalSessionRepo(root) {
|
|
263
335
|
return new FileSessionRepo(root, {
|
|
264
|
-
onCorruptRead: createCorruptReadFailOpenSeat("server.session-repo.corrupt-record-skipped",
|
|
265
|
-
noteReplayInSessionOpenScope(info);
|
|
266
|
-
noteReplayInArchiveRewriteRead(info);
|
|
267
|
-
}),
|
|
336
|
+
onCorruptRead: createCorruptReadFailOpenSeat("server.session-repo.corrupt-record-skipped", noteReplayInArchiveRewriteRead),
|
|
268
337
|
});
|
|
269
338
|
}
|
|
270
339
|
function occupiedBy(session, row) {
|
|
@@ -357,7 +426,6 @@ export class LocalSessionStore {
|
|
|
357
426
|
placements;
|
|
358
427
|
placementOf;
|
|
359
428
|
listPlaced;
|
|
360
|
-
claimOwnerless;
|
|
361
429
|
recordedPlacementOf;
|
|
362
430
|
pending = new Map();
|
|
363
431
|
rowsDropped = new Set();
|
|
@@ -375,6 +443,7 @@ export class LocalSessionStore {
|
|
|
375
443
|
instructionSources;
|
|
376
444
|
workspaces;
|
|
377
445
|
meta;
|
|
446
|
+
strictSidecars = [];
|
|
378
447
|
log;
|
|
379
448
|
sessionWriteLocks;
|
|
380
449
|
executionPlacement;
|
|
@@ -395,7 +464,6 @@ export class LocalSessionStore {
|
|
|
395
464
|
this.recordedPlacementOf = (sessionId) => repoPlacementOf.call(repo, sessionId);
|
|
396
465
|
this.placementOf = this.recordedPlacementOf;
|
|
397
466
|
this.listPlaced = (kind, o) => repoListPlaced.call(repo, kind, o);
|
|
398
|
-
this.claimOwnerless = (sessionId, incarnation, owner) => this.claimOwnerlessPlaced(sessionId, incarnation, owner);
|
|
399
467
|
}
|
|
400
468
|
else {
|
|
401
469
|
this.recordedPlacementOf = async () => undefined;
|
|
@@ -415,7 +483,12 @@ export class LocalSessionStore {
|
|
|
415
483
|
}
|
|
416
484
|
this.durability = opts.durability;
|
|
417
485
|
}
|
|
418
|
-
const sidecar = (file, which, read, strict = false) =>
|
|
486
|
+
const sidecar = (file, which, read, strict = false) => {
|
|
487
|
+
const map = 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);
|
|
488
|
+
if (strict)
|
|
489
|
+
this.strictSidecars.push(map);
|
|
490
|
+
return map;
|
|
491
|
+
};
|
|
419
492
|
this.titles = sidecar("session-titles.json", "title", readStringRecordColumn);
|
|
420
493
|
this.pinned = sidecar("session-pinned.json", "pinned", readPinnedSidecar);
|
|
421
494
|
this.instructionSources = sidecar("session-instruction-sources.json", "instruction-source", readStringRecordColumn);
|
|
@@ -436,6 +509,19 @@ export class LocalSessionStore {
|
|
|
436
509
|
place: (sessionId, occupant, cell) => this.placeExecution(sessionId, occupant, cell),
|
|
437
510
|
};
|
|
438
511
|
}
|
|
512
|
+
async assertStrictSidecarsReadable() {
|
|
513
|
+
const loads = await Promise.allSettled(this.strictSidecars.map((map) => map.reconcile()));
|
|
514
|
+
const findings = [];
|
|
515
|
+
for (const load of loads) {
|
|
516
|
+
if (load.status === "fulfilled")
|
|
517
|
+
continue;
|
|
518
|
+
if (!(load.reason instanceof LocalDataRootSchemaMismatchError))
|
|
519
|
+
throw load.reason;
|
|
520
|
+
findings.push(...load.reason.findings);
|
|
521
|
+
}
|
|
522
|
+
if (findings.length > 0)
|
|
523
|
+
throw new LocalDataRootSchemaMismatchError(findings, "boot");
|
|
524
|
+
}
|
|
439
525
|
async forkingRead(sessionId, meta) {
|
|
440
526
|
if (!isForkCreatedCell(meta))
|
|
441
527
|
return false;
|
|
@@ -928,7 +1014,7 @@ export class LocalSessionStore {
|
|
|
928
1014
|
}
|
|
929
1015
|
async openForItselfHeld(sessionId, opts) {
|
|
930
1016
|
await this.meta.ensureLoaded();
|
|
931
|
-
const session = await
|
|
1017
|
+
const session = await this.repo.open({ id: sessionId, createdAt: "" });
|
|
932
1018
|
admitSessionPlacement(sessionId, (await session.getMetadata()).placement, opts ?? {}, placementCapabilityOf(this));
|
|
933
1019
|
return this.handOut(sessionId, session);
|
|
934
1020
|
}
|
|
@@ -936,18 +1022,51 @@ export class LocalSessionStore {
|
|
|
936
1022
|
if (this.metaCellOf(sessionId) !== undefined) {
|
|
937
1023
|
throw new SessionError("placement_refused", `session ${sessionId} is already registered as an ordinary session — refusing to create a subagent transcript on its id`);
|
|
938
1024
|
}
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1025
|
+
const { owner: treeOwner, ...tuple } = placement;
|
|
1026
|
+
const owner = placedSessionOwnerOf(sessionId, { owner: treeOwner });
|
|
1027
|
+
const created = await this.writeMeta(sessionId, (before) => (before === undefined ? { incarnation: mintSessionIncarnation(), owner, createdAt: rowBornNow() } : undefined));
|
|
1028
|
+
if (!created)
|
|
1029
|
+
throw new SessionError("storage", `LocalSessionStore: session ${sessionId} gained a session-meta cell under its own write lock — refusing to guess`);
|
|
1030
|
+
const stuck = (cause, drop) => `LocalSessionStore: creating the subagent transcript ${sessionId} failed (${cause}) and its session-meta cell could not be withdrawn (${drop}) — the cell stays on disk and a placed create on this id is refused; delete the session to clear it`;
|
|
1031
|
+
const unsure = async (err) => {
|
|
1032
|
+
const foreign = err instanceof SessionError && err.code === "conflict";
|
|
1033
|
+
if (foreign || !(await this.holdsThisPlacement(sessionId, opts)))
|
|
1034
|
+
await this.withdrawCreatedCell(sessionId, err, stuck);
|
|
1035
|
+
throw err;
|
|
1036
|
+
};
|
|
1037
|
+
const session = await this.repo.create({ id: sessionId, placement: tuple }).catch(unsure);
|
|
1038
|
+
const persisted = await session.getMetadata().then((meta) => meta.placement, unsure);
|
|
1039
|
+
try {
|
|
1040
|
+
admitSessionPlacement(sessionId, persisted, opts, placementCapabilityOf(this));
|
|
1041
|
+
}
|
|
1042
|
+
catch (err) {
|
|
1043
|
+
await this.withdrawCreatedCell(sessionId, err, stuck);
|
|
1044
|
+
throw err;
|
|
1045
|
+
}
|
|
949
1046
|
return this.handOut(sessionId, session);
|
|
950
1047
|
}
|
|
1048
|
+
async holdsThisPlacement(sessionId, opts) {
|
|
1049
|
+
const probe = await this.placementProbe(sessionId);
|
|
1050
|
+
if (probe.state === "unusable")
|
|
1051
|
+
return true;
|
|
1052
|
+
try {
|
|
1053
|
+
admitSessionPlacement(sessionId, probe.state === "placed" ? probe.record : undefined, opts, placementCapabilityOf(this));
|
|
1054
|
+
return true;
|
|
1055
|
+
}
|
|
1056
|
+
catch (e) {
|
|
1057
|
+
if (e instanceof SessionError && e.code === "placement_refused")
|
|
1058
|
+
return false;
|
|
1059
|
+
throw e;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
async withdrawCreatedCell(sessionId, cause, stuck) {
|
|
1063
|
+
try {
|
|
1064
|
+
await this.dropMeta(sessionId);
|
|
1065
|
+
}
|
|
1066
|
+
catch (dropErr) {
|
|
1067
|
+
throw new SessionError("storage", stuck(cause instanceof Error ? cause.message : String(cause), dropErr instanceof Error ? dropErr.message : String(dropErr)), cause instanceof Error ? cause : undefined);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
951
1070
|
async register(sessionId, owner) {
|
|
952
1071
|
await this.withSessionWriteLock(sessionId, async () => {
|
|
953
1072
|
await this.meta.ensureLoaded();
|
|
@@ -986,20 +1105,6 @@ export class LocalSessionStore {
|
|
|
986
1105
|
await this.repo.delete({ id: sessionId, createdAt: "" });
|
|
987
1106
|
});
|
|
988
1107
|
}
|
|
989
|
-
async claimOwnerlessPlaced(sessionId, incarnation, owner) {
|
|
990
|
-
return this.withSessionWriteLock(sessionId, async () => {
|
|
991
|
-
if ((await this.placedRecordOrThrow(sessionId)) === undefined)
|
|
992
|
-
return false;
|
|
993
|
-
return this.writeMeta(sessionId, (before) => {
|
|
994
|
-
if (before === undefined)
|
|
995
|
-
return undefined;
|
|
996
|
-
const cell = readSessionMetaCell(before);
|
|
997
|
-
if (cell.incarnation !== incarnation || (cell.owner ?? null) !== null)
|
|
998
|
-
return undefined;
|
|
999
|
-
return cellWith(before, { owner });
|
|
1000
|
-
});
|
|
1001
|
-
});
|
|
1002
|
-
}
|
|
1003
1108
|
async listSessions(opts) {
|
|
1004
1109
|
await this.loadRowSidecars();
|
|
1005
1110
|
const rows = [];
|
|
@@ -1105,8 +1210,7 @@ export class LocalSessionStore {
|
|
|
1105
1210
|
return null;
|
|
1106
1211
|
throw e;
|
|
1107
1212
|
}
|
|
1108
|
-
|
|
1109
|
-
throw new StampedSessionArchiveError(sourceId);
|
|
1213
|
+
await this.assertArchiveAnonymous(sourceId);
|
|
1110
1214
|
const forkOptions = await settledForkOptions(source);
|
|
1111
1215
|
const forkOwner = forkOwnerOf(owner, this.metaCellOf(sourceId)?.owner);
|
|
1112
1216
|
const existing = this.metaCellOf(target.id);
|
|
@@ -1122,12 +1226,7 @@ export class LocalSessionStore {
|
|
|
1122
1226
|
await copy();
|
|
1123
1227
|
}
|
|
1124
1228
|
catch (err) {
|
|
1125
|
-
|
|
1126
|
-
await this.dropMeta(target.id);
|
|
1127
|
-
}
|
|
1128
|
-
catch (dropErr) {
|
|
1129
|
-
throw new SessionError("storage", `LocalSessionStore: fork into ${target.id} failed while copying (${err instanceof Error ? err.message : String(err)}) and its target cell could not be withdrawn (${dropErr instanceof Error ? dropErr.message : String(dropErr)}) — the target stays in the fork-in-progress state; retry the same fork or delete the session`, err instanceof Error ? err : undefined);
|
|
1130
|
-
}
|
|
1229
|
+
await this.withdrawCreatedCell(target.id, err, (cause, drop) => `LocalSessionStore: fork into ${target.id} failed while copying (${cause}) and its target cell could not be withdrawn (${drop}) — the target stays in the fork-in-progress state; retry the same fork or delete the session`);
|
|
1131
1230
|
throw err;
|
|
1132
1231
|
}
|
|
1133
1232
|
return { outcome: "created", id: target.id };
|
|
@@ -1195,8 +1294,6 @@ export class LocalSessionStore {
|
|
|
1195
1294
|
case "judged":
|
|
1196
1295
|
try {
|
|
1197
1296
|
const archive = (await this.archiveHeadOf(sessionId)) ?? null;
|
|
1198
|
-
if ((archive?.owner ?? null) !== null)
|
|
1199
|
-
throw new StampedSessionArchiveError(sessionId);
|
|
1200
1297
|
await this.repo.importEntries(sessionId, undefined, entries, { expect: { row: archive } });
|
|
1201
1298
|
}
|
|
1202
1299
|
catch (err) {
|
|
@@ -1274,6 +1371,10 @@ export class LocalSessionStore {
|
|
|
1274
1371
|
forgetStaging(stagingId) {
|
|
1275
1372
|
this.stagings.delete(stagingId);
|
|
1276
1373
|
}
|
|
1374
|
+
async assertArchiveAnonymous(sessionId) {
|
|
1375
|
+
if (((await this.archiveHeadOf(sessionId))?.owner ?? null) !== null)
|
|
1376
|
+
throw new StampedSessionArchiveError(sessionId);
|
|
1377
|
+
}
|
|
1277
1378
|
async commitStagedImport(sessionId, owner, staged, opts) {
|
|
1278
1379
|
return this.withImportLock(sessionId, owner, async (importer) => {
|
|
1279
1380
|
const current = await this.metaOf(sessionId);
|
|
@@ -1284,6 +1385,9 @@ export class LocalSessionStore {
|
|
|
1284
1385
|
await this.assertNotPlacedHeld(sessionId);
|
|
1285
1386
|
const dstEntries = await this.exportEntries(sessionId);
|
|
1286
1387
|
const rel = classifySyncRelationshipByIds(staged.map((e) => e.id), dstEntries === null ? null : dstEntries.map((e) => e.id));
|
|
1388
|
+
const contentEqual = rel.relation === "identical" && identicalIdsAlsoIdenticalContent(staged, dstEntries);
|
|
1389
|
+
if (!contentEqual)
|
|
1390
|
+
await this.assertArchiveAnonymous(sessionId);
|
|
1287
1391
|
if ((rel.relation === "fork" || rel.relation === "stale") && opts.resolution !== "overwrite-dst") {
|
|
1288
1392
|
throw new SyncConflictError(sessionId, rel);
|
|
1289
1393
|
}
|
|
@@ -1293,7 +1397,6 @@ export class LocalSessionStore {
|
|
|
1293
1397
|
if (diverged)
|
|
1294
1398
|
throw new SyncConflictError(sessionId, contentForkRelation(diverged, rel.newEntryIds));
|
|
1295
1399
|
}
|
|
1296
|
-
const contentEqual = rel.relation === "identical" && identicalIdsAlsoIdenticalContent(staged, dstEntries);
|
|
1297
1400
|
if (current?.execution?.role === "writer")
|
|
1298
1401
|
throw new SessionWriterPresentError(sessionId, current.execution.lane);
|
|
1299
1402
|
if (rel.relation !== "identical" || !contentEqual) {
|
|
@@ -24,12 +24,14 @@
|
|
|
24
24
|
*
|
|
25
25
|
* ## Cost: one linear walk, the engine's
|
|
26
26
|
* The branch is core's own `getPathToRoot` over the whole log (the walk every engine read of a local session makes, with its stop
|
|
27
|
-
* rules: an entry without a parent ends it, a parent the log does not hold is `invalid_session
|
|
27
|
+
* rules: an entry without a parent ends it, a parent the log does not hold is `corrupt_session` — `invalid_session` up to core 7.61.x —, an unknown
|
|
28
|
+
* leaf is `not_found`). A missing parent whose replay dropped records is the one break a RUN does not refuse since core 7.62.0 ([ref]: it re-roots
|
|
29
|
+
* and says `session.lineage_rerooted`); this view is not a run and takes the walk's refusal as is.
|
|
28
30
|
* Since core 7.54.0 ([ref]) that walk is linear; before, it built the path with `unshift` (quadratic — seconds on a long session, on
|
|
29
31
|
* the event loop) and this module walked the `parentId` chain itself. The second walker is retired: the view reads the branch the
|
|
30
32
|
* engine reads, by the engine's reading. Every later read of the window goes through the same walk, bounded by the floor.
|
|
31
33
|
* A parent CYCLE (a corrupted log; the engine never writes one) is refused by that same walk: since core 7.54.1 it carries one
|
|
32
|
-
* `ParentChainGuard`, and the entry it stands on twice ends it with `invalid_session` naming that entry — `wake` refuses a looped log
|
|
34
|
+
* `ParentChainGuard`, and the entry it stands on twice ends it with `corrupt_session` (`invalid_session` up to core 7.61.x) naming that entry — `wake` refuses a looped log
|
|
33
35
|
* the way it refuses one whose parent is missing. Core 7.54.0's walk had no guard (a looped log spun it forever on the event loop,
|
|
34
36
|
* and the view's wake with it). The retired walker's own cycle refusal went with it: the engine's guard is the one refusal now, shared
|
|
35
37
|
* by `getPathToRoot` and the run's resume ancestor scan (the two walks core 7.54.1 guards; this module has no walker of its own left).
|
|
@@ -57,6 +57,8 @@ export declare class MemoryRunStore implements InProcessClaimFence {
|
|
|
57
57
|
bindIdemBodyFingerprint(taskId: string, idem: RunIdemBinding): Promise<string | null>;
|
|
58
58
|
/** The run a submission's scoped `Idempotency-Key` names, if any. */
|
|
59
59
|
getRunByIdem(idemKey: string): Promise<RunRecord | undefined>;
|
|
60
|
+
/** run 行预览的写一次补写(SQL 孪生同义:列仍是 null 才写,建行写过 / 已补过的不动;行不在 ⇒ 无事)。 */
|
|
61
|
+
fillObjectivePreview(taskId: string, preview: string): Promise<void>;
|
|
60
62
|
requestCancel(taskId: string, owner: string | null): Promise<boolean>;
|
|
61
63
|
isCancelRequested(taskId: string, owner: string | null): Promise<boolean>;
|
|
62
64
|
requestPreempt(taskId: string, owner: string | null): Promise<boolean>;
|
|
@@ -109,6 +109,12 @@ export class MemoryRunStore {
|
|
|
109
109
|
const r = taskId !== undefined ? this.runs.get(taskId) : undefined;
|
|
110
110
|
return r ? this.toRecord(r) : undefined;
|
|
111
111
|
}
|
|
112
|
+
async fillObjectivePreview(taskId, preview) {
|
|
113
|
+
const r = this.runs.get(taskId);
|
|
114
|
+
if (!r || r.objectivePreview !== null)
|
|
115
|
+
return;
|
|
116
|
+
r.objectivePreview = preview;
|
|
117
|
+
}
|
|
112
118
|
async requestCancel(taskId, owner) {
|
|
113
119
|
const r = this.runs.get(taskId);
|
|
114
120
|
if (!r || !ownerEq(r.owner, owner) || r.status !== "running")
|
|
@@ -194,9 +194,10 @@
|
|
|
194
194
|
* (拒因点名版本与键);不带 ⇒ 载荷零改动读回,读回方按 5 写回原位,启动日志 `config_lkg_migrated` 一行。规则在 `config-lkg.ts` 顶注 formatVersion 5 一段与
|
|
195
195
|
* `migrateV4Lkg`。没有读数键(它不写库)。
|
|
196
196
|
* · **M23 存量 placed 子代行的属主**(7.133.0)—— 7.132.x 及以前两只 SQL 店建委派子代的 placed 行时属主恒 NULL,会话面的属主谓词对 NULL 放行 ⇒ 谁拿到子代的会话 id
|
|
197
|
-
* (父客户端在子代帧的 `transcriptId` 上收到)谁就读得到整段转录。7.133.0
|
|
197
|
+
* (父客户端在子代帧的 `transcriptId` 上收到)谁就读得到整段转录。7.133.0–7.143.x 建行那一笔按本仓委派继承律写属主,7.144.0 起记引擎交的 `placement.owner`
|
|
198
|
+
* (core 7.62.0 [ref];`src/delegated-session-owner.ts` 头注),
|
|
198
199
|
* 属主为 NULL 的 placed 行只对 fleet 调用方放行(无主放行臂的唯一判据 `security.ts` 的 `ownerlessRowOpen`,常设的读面律,不随本条删);存量行 ⇒ **每次起服的幂等回填**,
|
|
199
|
-
*
|
|
200
|
+
* 判据 = 旧律(`judgeDelegatedOwner`,属主 = 判断时见证到的;引擎当年的读数没有落库,读不回):根会话的行在 ⇒ 它的属主(根无主 ⇒ 子代仍无主,不写);根行不在 ⇒ 父会话的行;都不在(孤行)⇒
|
|
200
201
|
* 哨兵属主 `_orphan:<id>`(保留主体前缀,任何已验身份都不等于它 —— 不留 NULL,宁可没人能读),计数 + 起服一行 warn `placed_session_owners_orphaned` 点名条数。记下的根 / 父是空串 ⇒ 读作没有那一格。
|
|
201
202
|
* **单趟**:按 `session_id` 键集分页扫 `placement_kind IS NOT NULL ∧ owner IS NULL` 的行,每一页一条语句连读根 / 父两行(属主、住客、是不是 placed 行;连接键套
|
|
202
203
|
* `NULLIF(…, '')`);回写 = 认领句(`CLAIM_OWNERLESS_PLACED_SQL`:只 CAS 子行自己 —— 扫描读到的化身 + `owner IS NULL` + `placement_kind IS NOT NULL`;扫描与回写之间根 / 父被删、
|
|
@@ -204,10 +205,9 @@
|
|
|
204
205
|
* 化身为空的行(子代或来源行)⇒ 响亮拒(全仓「读到空化身」同一条律)。读数三格:写成的(`placedSessionOwnersInherited` / `placedSessionOwnersOrphaned`)与
|
|
205
206
|
* **残余** `placedSessionOwnersRemaining` = 律这一读判不出、因为它读到的来源本身还是一只无主的委派子代行的行(根已删、父是同样待回填的行,且排在它之后或在同一页里)。
|
|
206
207
|
* 残余非零 ⇒ 读数行 warn;**不拒启、不重扫**:本文件的每一条在窗内每次起服都跑(「窗」一节),残余行只对 fleet 调用方放行(读面律),由之后起服的本条(一次至少再定下一层)
|
|
207
|
-
*
|
|
208
|
-
* **够不到的一格(如实登记)
|
|
209
|
-
*
|
|
210
|
-
* local 带身份的部署升级前先把停驻待批的审批处理完)。窗 = 发布日 + 30 天(与 M19 / M21 同一条论证:
|
|
208
|
+
* 定下(7.144.0 起停驻挂点只比对、不认领)。根无主的子代(律就让它无主)不是残余,每次起服照样被扫到、照样不写。
|
|
209
|
+
* **够不到的一格(如实登记)**:本条只扫 SQL 两店,不碰 local 的会话边档 ⇒ 经晋升腿进 local 宿主店的升级前子代行(普通行)仍无主;它们再次停驻时停驻挂点只比对
|
|
210
|
+
* 引擎交来的属主(复活腿 = 那一行自己的属主 ⇒ 无主照无主比对;随包文字的补偿:local 带身份的部署升级前先把停驻待批的审批处理完)。窗 = 发布日 + 30 天(与 M19 / M21 同一条论证:
|
|
211
211
|
* 窗关时缺省部署上升级前的子代转录已被联合收割按 `BG_AGENT_RETENTION_MS` 收走;调长了留存的部署、滚动升级里旧副本在窗内新写的行如实登记,不为它们加长窗)。
|
|
212
212
|
* · **M24 workflow journal 的属主 scope 搬家**(7.133.0)—— 7.132.x 及以前 `workflow_journal` 的 scope 是**每一行**上的一列:读面按它判,写面(按
|
|
213
213
|
* `(run_id, ordinal)` 的 upsert)从不判它 ⇒ 别的 scope 的 append 落新序数插进一条 run、落既有序数覆盖属主的结果。7.133.0 起一条 run 的属主 scope 只记在
|
|
@@ -310,7 +310,7 @@
|
|
|
310
310
|
* · **local 车道**:没有绑定表、没有 `session_meta`(device 车道只在 SQL 后端)⇒ 只有 ④ 与回填前提:读本地 run 店有 run 的会话,执行席读出 absent 的 ⇒ 经同一只席 `place` 成 writer(表唯一项)
|
|
311
311
|
* (`LocalBackend.runOneTimeMigrations`);local 没有「切换收尾」的形可读 ⇒ 窗内每次起服都判,首跑之后它只碰认领与放置之间被打断的会话(与 `place` 会落下的格同值)。
|
|
312
312
|
* 有 run、而**存档读不出**的会话 —— 会话元数据格缺(升级链上从没被再读过的老会话)而探针读不出,或 fork 创建的格而探针说不出存档在不在 ——:回填跳过它并点名
|
|
313
|
-
* (读数 `executionPlacementSessionsUnreadable` + 一行一条 warn `execution_placement_session_unreadable`,带会话 id 与故障词〔闭集 `corrupt_session` / `
|
|
313
|
+
* (读数 `executionPlacementSessionsUnreadable` + 一行一条 warn `execution_placement_session_unreadable`,带会话 id 与故障词〔闭集 `corrupt_session` / `storage`;
|
|
314
314
|
* `storage` = 读存档文件时抛了别的故障:权限 / 磁盘 I/O / 超大,再带 `errno`〕),**不拒启**、一个字节没写 —— 回填只补一格放置、不动存档,读不出的存档不归它判
|
|
315
315
|
* (这一支只收**探针对这只会话的读故障**;会话元数据边档本身读不出、问账日志压实失败等整库级故障照旧拒启);该会话的请求照旧答存档的故障,存档修好(从留存副本恢复,
|
|
316
316
|
* 或 `storage` 时权限 / 磁盘修好)后的下一次起服照常补放置;存档移走则它不再是本服务的会话(不补放置、不再点名;fork 创建的会话例外 —— 它的格还在,读作 fork 未落定,出路是同键重试那次 fork 或删除该会话)。
|
|
@@ -430,7 +430,7 @@ export interface OneTimeMigrationReport {
|
|
|
430
430
|
* 行数(SQL 独有;local 恒 0)。非零 ⇒ 另一行 warn 点名条数。 */
|
|
431
431
|
readonly placedSessionOwnersOrphaned: number;
|
|
432
432
|
/** M23:这一次起服留着无主的存量 placed 子代行 —— 律这一读判不出,因为它读到的来源本身还是一只无主的委派子代行(根已删、父待回填)。它们只对 fleet 调用方
|
|
433
|
-
* 放行(读面律)
|
|
433
|
+
* 放行(读面律),由之后起服的本条定下。根无主的子代(律就让它无主)不算。行数(SQL 独有;local 恒 0)。非零 ⇒ 读数行 warn(不拒启)。 */
|
|
434
434
|
readonly placedSessionOwnersRemaining: number;
|
|
435
435
|
/** M24:workflow journal 的属主 scope —— `moved` = 本次把行上的 scope 列搬进 `workflow_journal_owner`(回填 + 删跨 scope 行 + 删列);`current` = 早已是新形
|
|
436
436
|
* (新库 / 第二跑:`workflow_journal` 上没有 scope 列);`file-store` = local 车道(core 的 File 店,属主 = 每条 run 的 journal 文件首行)。 */
|
|
@@ -528,7 +528,7 @@ export interface ParkedApprovalCardRowTooLong extends ParkedApprovalCardRowUnrea
|
|
|
528
528
|
readonly bytes: number;
|
|
529
529
|
}
|
|
530
530
|
/**
|
|
531
|
-
* M32 local 腿被跳过的一只会话:会话 id 是点名坐标;故障词是闭集 {@link UnusableArchiveWord}(`corrupt_session` =
|
|
531
|
+
* M32 local 腿被跳过的一只会话:会话 id 是点名坐标;故障词是闭集 {@link UnusableArchiveWord}(`corrupt_session` = 会话头读不出,或读得出而日志走不成一棵树;
|
|
532
532
|
* `storage` = 读的时候抛了别的故障,I/O 级 —— 权限 / 磁盘 / 超大,此时 `errno` 带那只故障的 `code`)。盘上路径与故障句不出店。
|
|
533
533
|
*/
|
|
534
534
|
export interface ExecutionPlacementSessionUnreadable {
|
|
@@ -846,13 +846,13 @@ export declare function repairParkedRows(db: MigrationDb, dialect: SqlDialect, r
|
|
|
846
846
|
export declare function backfillParkedApprovalCards(db: MigrationDb, dialect: SqlDialect, receipts: MigrationReceipts): Promise<void>;
|
|
847
847
|
/**
|
|
848
848
|
* M23 —— 存量 placed 子代行的属主回填,**每次起服单趟、幂等**(规则、为什么、够不到的一格见头注 M23 段):键集分页扫完 placed ∧ 属主 NULL 的行,每一行按唯一判据判
|
|
849
|
-
* (`judgeDelegatedOwner
|
|
850
|
-
* · 判出属主 ⇒ {@link CLAIM_OWNERLESS_PLACED_SQL}(只 CAS
|
|
849
|
+
* (`judgeDelegatedOwner`,旧律:来源顺序根、父;属主 = 这一读见证到的):
|
|
850
|
+
* · 判出属主 ⇒ {@link CLAIM_OWNERLESS_PLACED_SQL}(只 CAS 子行自己,绑扫描读到的化身;第二跑零命中);写成的按属主计进
|
|
851
851
|
* `placedSessionOwnersOrphaned`(谁都不匹配:孤行哨兵,或继承自孤行祖先的同一只哨兵)或 `placedSessionOwnersInherited`,每一次提交当场记进收据账;零命中(这一行在扫描之后
|
|
852
852
|
* 被写过 / 删过 / 换了住客)⇒ 不计,下一次起服按那时的律重判;
|
|
853
853
|
* · 判出无主:读到的来源本身是一只 placed 行(还没定下的委派子代行)⇒ 计进 `placedSessionOwnersRemaining`(残余);来源是普通会话(根无主)⇒ 律就让它无主,不计;
|
|
854
854
|
* · 扫到的任何一行化身为空 ⇒ 响亮拒(不论判不判得出属主;来源行同一条律)。
|
|
855
|
-
*
|
|
855
|
+
* 不重扫、不拒启:残余由下一次起服定下,期间只对 fleet 调用方放行(头注 M23 段)。
|
|
856
856
|
*/
|
|
857
857
|
export declare function inheritPlacedSessionOwners(db: MigrationDb, dialect: SqlDialect, receipts: MigrationReceipts): Promise<void>;
|
|
858
858
|
/**
|
|
@@ -404,17 +404,17 @@ export const ONE_TIME_MIGRATIONS = [
|
|
|
404
404
|
introducedIn: "7.133.0",
|
|
405
405
|
releasedOn: "2026-10-21",
|
|
406
406
|
removableAfter: "2026-11-20",
|
|
407
|
-
removal: "删 migrateOn 的 M23 步(`inheritPlacedSessionOwners`:placed ∧ 属主 NULL 的行按 session_id 键集分页单趟扫描 + 同一条语句连读根 / 父两行 + 按认领句回写属主)与 `PLACED_OWNER_SCAN_PAGE` / `placedOwnerScanSql` / `scannedPlacement` / `scannedFaces` / `scannedPlacedSources
|
|
408
|
-
"**同窗同删的另一半**(
|
|
409
|
-
"`
|
|
410
|
-
"两只 SQL 店(`tidb-session-store.ts` / `pg-session-storage.ts`)与 local 店(`local-session-store.ts` 的 `claimOwnerless` 字段与 `claimOwnerlessPlaced`)的 `claimOwnerless`、`CachingSessionStore` 的转发、`OwnerAwareSessionStore.claimOwnerless`、`delegatedOwnerHostOf` 对它的「与 `placementOf` 同在或同缺」;" +
|
|
407
|
+
removal: "删 migrateOn 的 M23 步(`inheritPlacedSessionOwners`:placed ∧ 属主 NULL 的行按 session_id 键集分页单趟扫描 + 同一条语句连读根 / 父两行 + 按认领句回写属主)与 `PLACED_OWNER_SCAN_PAGE` / `placedOwnerScanSql` / `scannedPlacement` / `scannedFaces` / `scannedPlacedSources`;" +
|
|
408
|
+
"**同窗同删的另一半**(旧律的判据与写句 —— 7.144.0 起建行与停驻挂点都不再读它们,M23 是唯一读者):`delegated-session-owner.ts` 头注 M23 一节与 `judgeDelegatedOwner` / `inheritedOwnerOf` / `ownerSourcesOf` / " +
|
|
409
|
+
"`orphanSessionOwner` / `isOrphanSessionOwner` / `SourceOwnerReading` / `InheritedOwner` / `PlacementSources` / `CLAIM_OWNERLESS_PLACED_SQL` / `claimOwnerlessParams`;" +
|
|
411
410
|
"报告、日志行与 `NO_SQL_MIGRATIONS` 的 `placedSessionOwnersInherited` / `placedSessionOwnersOrphaned` / `placedSessionOwnersRemaining` 与 `placed_session_owners_orphaned` 那一行 warn;" +
|
|
412
411
|
"`test/one-time-migrations.test.ts` 的 M23 组与 `test/one-time-migrations-db-integration.test.ts` 的 M23 格。" +
|
|
413
|
-
"
|
|
412
|
+
"**不随本条删**:`ORPHAN_SESSION_OWNER_PREFIX` 与它在 `security.ts` 保留主体前缀里的登记(已落库的行可能带哨兵,永久保留);" +
|
|
413
|
+
"属主为 NULL 的 placed 行只对 fleet 调用方放行(`security.ts` 的 `ownerlessRowOpen` / `sessionOwnerCellOf` 与各门读 `placementOf` 的那一次)—— 那是常设的读面律",
|
|
414
414
|
afterRemoval: "会话面的读面律(常设,不随本条删)接住:跳过了带本条的版本直接升的库里,属主为 NULL 的存量 placed 子代行只对 fleet 调用方放行(出示 service 凭据、不报主体;dev-open)," +
|
|
415
415
|
"其余调用方答 404(与别人的行同体)—— 出路是升级时先过一次带本条的版本,或运维按 `placement_root_session_id`(缺则 `placement_parent_session_id`)那一行的属主手工回填、" +
|
|
416
|
-
"
|
|
417
|
-
"
|
|
416
|
+
"推不出的写一个任何主体都不匹配的值;停驻挂点只比对宿主行属主与引擎交来的属主(复活腿交那一行自己的属主),不写无主行;" +
|
|
417
|
+
"建行那一笔记 `placement.owner` 不随本条删(那是常设的写法 —— core 盒义务 7,不是迁移)",
|
|
418
418
|
reportKeys: ["placedSessionOwnersInherited", "placedSessionOwnersOrphaned", "placedSessionOwnersRemaining"],
|
|
419
419
|
},
|
|
420
420
|
{
|
|
@@ -570,8 +570,8 @@ export const ONE_TIME_MIGRATIONS = [
|
|
|
570
570
|
"报告 / 收据 / `changedAnything` / `readingsSnapshot` / `readingFields` 里的 `localArchiveRowFactsBackfilled` / `localArchivesAnonymized` / `localArchivesKeptStamped` / `localArchivesUnreadable` / `localArchiveSnapshotsRecovered`、" +
|
|
571
571
|
"`local_archive_kept_stamped` / `local_archive_unreadable` 两行 warn、读数行 note 里的 M33 句与头注 M33 段;`test/local-archive-owner.test.ts` 里标题含 `P3` / `P4` / `P5` / `P6` / `F1` / `F2` / `F3` 的格与 `legacyRoot` / `writeArchive` / `boot` / `M33_ZERO` / `m33Of` 助手。" +
|
|
572
572
|
"**不随本条删**:行格的 `createdAt` / `forkedFrom` 两键(常设的数据形)、`LocalArchiveSnapshotsPresentError` / `assertNoArchiveSnapshots` / `LOCAL_ARCHIVE_SNAPSHOT_DIR` 与 `LocalBackend.ensureSchema` 里调它的那一句、" +
|
|
573
|
-
"`session-error-wire.ts` 的 `StampedSessionArchiveError` / `STAMPED_SESSION_ARCHIVE_SENTENCE` / `sessionErrorWireOf`
|
|
574
|
-
afterRemoval: "三只常设臂接住:① 数据根下的快照目录非空 ⇒ 拒启(`config.local_archive_snapshot_present`,点名文件与两条出路);② 跳过了带本条的版本直接升的数据根(或本条留着的存档),带属主戳的存档被 fork
|
|
573
|
+
"`session-error-wire.ts` 的 `StampedSessionArchiveError` / `STAMPED_SESSION_ARCHIVE_SENTENCE` / `sessionErrorWireOf` 与本店带戳判的唯一读口 `assertArchiveAnonymous`(fork 的源、改日志推送的落点、会话同步导入第一阶段的早拒三处问它)、行上缺建档时刻的 F 类读法(`server.local-session-row.created-at-absent`)",
|
|
574
|
+
afterRemoval: "三只常设臂接住:① 数据根下的快照目录非空 ⇒ 拒启(`config.local_archive_snapshot_present`,点名文件与两条出路);② 跳过了带本条的版本直接升的数据根(或本条留着的存档),带属主戳的存档被 fork、改日志的推送与会话同步导入第一阶段对 fork / stale 的早拒同一只具名拒(409 `conflict.session_archive_stamped` + 固定句,零写 / 行格净零写)," +
|
|
575
575
|
"读 / 续跑 / 拉取照常 —— 出路 = 删掉会话后重推,或升级时先过一次带本条的版本并在窗内起服;③ 行上没有建档时刻的格(同一类数据根)⇒ 列表 / 会话视图 / 交出的元数据读纪元(F 类 tag 计数)",
|
|
576
576
|
reportKeys: ["localArchiveRowFactsBackfilled", "localArchivesAnonymized", "localArchivesKeptStamped", "localArchivesUnreadable", "localArchiveSnapshotsRecovered"],
|
|
577
577
|
},
|
|
@@ -1687,10 +1687,10 @@ export async function inheritPlacedSessionOwners(db, dialect, receipts) {
|
|
|
1687
1687
|
const sessionId = String(r.session_id);
|
|
1688
1688
|
after = sessionId;
|
|
1689
1689
|
const incarnation = incarnationFromCell(sessionId, r.incarnation);
|
|
1690
|
-
const
|
|
1691
|
-
const owner =
|
|
1690
|
+
const inherited = judgeDelegatedOwner(sessionId, scannedPlacement(r), scannedFaces(r));
|
|
1691
|
+
const owner = inherited.owner;
|
|
1692
1692
|
if (owner === null) {
|
|
1693
|
-
if (
|
|
1693
|
+
if (inherited.kind === "inherited" && scannedPlacedSources(r).has(inherited.from))
|
|
1694
1694
|
receipts.placedSessionOwnersRemaining += 1;
|
|
1695
1695
|
continue;
|
|
1696
1696
|
}
|
|
@@ -1858,7 +1858,7 @@ function logReceiptRows(log, backend, r) {
|
|
|
1858
1858
|
...(row.errno !== undefined ? { errno: row.errno } : {}),
|
|
1859
1859
|
note: "a local session that has runs (and either no session-metadata cell — it was written before the cell existed and has not been read since — or a fork-created cell) has an archive the server cannot read, " +
|
|
1860
1860
|
"so its execution placement could not be backfilled. The session was skipped and nothing was written or changed: boot is not blocked, and every request that needs the " +
|
|
1861
|
-
"session keeps answering the archive's own fault (fault names it: corrupt_session = the session header cannot be read
|
|
1861
|
+
"session keeps answering the archive's own fault (fault names it: corrupt_session = the session header cannot be read, or the stored log does not form one tree; " +
|
|
1862
1862
|
"storage = reading the archive file itself failed — errno names it, e.g. EACCES = no permission, EIO = disk I/O error, ERR_STRING_TOO_LONG = the file is too large to read) until the " +
|
|
1863
1863
|
"archive file under the data root's sessions directory is readable again — restored from a kept copy, or (storage) its permissions or disk repaired; the next boot then backfills its placement as for any other session. " +
|
|
1864
1864
|
"(Removing the archive file ends the warning but places nothing: the session then no longer exists on this server — except a fork-created session, whose cell stays and reads as a fork that has not landed: retry the same fork, or delete the session.) The session is named on every boot until then",
|