@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.
- package/MIGRATION.md +182 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/USAGE.md +58 -28
- package/deploy/sema-up/README.md +2 -0
- package/deploy/sema-up/chart/templates/minio.yaml +2 -1
- package/deploy/sema-up/chart/templates/server-v4pro.yaml +1 -0
- package/deploy/sema-up/chart/templates/server.yaml +1 -0
- package/deploy/sema-up/chart/values.yaml +7 -1
- package/deploy/sema-up/cluster-up.sh +20 -8
- package/deploy/sema-up/kube-up.sh +29 -16
- package/deploy/sema-up/lib/buckets.sh +21 -0
- package/deploy/sema-up/lib/health.sh +33 -0
- package/deploy/sema-up/sema-up.sh +14 -9
- package/deploy/sema-up/smoke.sh +16 -6
- package/deploy/sema-up/stack/compose.yaml +1 -1
- package/deploy/sema-up/stack/sema-stack-cluster.yaml +4 -1
- package/dist/approval-ask-audit-store.js +2 -7
- package/dist/approval-card.d.ts +40 -10
- package/dist/approval-card.js +11 -1
- package/dist/approval.d.ts +55 -17
- package/dist/approval.js +74 -33
- package/dist/audit.d.ts +12 -1
- package/dist/audit.js +62 -2
- package/dist/auto-mode-face.d.ts +2 -3
- package/dist/bake-runner/main.js +1 -6
- package/dist/bake-runner/protocol.d.ts +18 -0
- package/dist/bake-runner/protocol.js +22 -2
- package/dist/bake-runner/runner.d.ts +12 -8
- package/dist/bake-runner/runner.js +5 -9
- package/dist/boot/config-center.js +1 -1
- package/dist/boot/coordinators.js +4 -1
- package/dist/boot/device-lane.d.ts +3 -3
- package/dist/boot/device-lane.js +1 -2
- package/dist/boot/leader.d.ts +4 -4
- package/dist/boot/leader.js +1 -2
- package/dist/boot/parked-revive-gate.d.ts +4 -6
- package/dist/boot/parked-revive-gate.js +2 -6
- package/dist/boot/resolve-spec.js +9 -11
- package/dist/boot/runner-deps.js +2 -2
- package/dist/boot/stage-07-capability-layer.js +7 -3
- package/dist/boot/stage-10-http-server.js +3 -2
- package/dist/boot/stores.js +12 -7
- package/dist/capabilities/center-plugins.d.ts +2 -0
- package/dist/capabilities/center-plugins.js +24 -6
- package/dist/capabilities/code-review-council.js +8 -11
- package/dist/capabilities/hands-lane.d.ts +25 -0
- package/dist/capabilities/hands-lane.js +1 -1
- package/dist/capabilities/mcp-probe.d.ts +2 -2
- package/dist/capabilities/scenarios.d.ts +34 -7
- package/dist/capabilities/scenarios.js +33 -6
- package/dist/config-catalog.js +3 -3
- package/dist/config.d.ts +14 -18
- package/dist/config.js +15 -26
- package/dist/deployment-governance.d.ts +5 -7
- package/dist/deployment-governance.js +4 -5
- package/dist/host-decision.d.ts +114 -31
- package/dist/host-decision.js +39 -2
- package/dist/http/admission.js +12 -6
- package/dist/http/approval-sweeps.js +1 -1
- package/dist/http/mcp-dial-deadline.d.ts +35 -0
- package/dist/http/mcp-dial-deadline.js +21 -0
- package/dist/http/memory-compliance-failure-reply.d.ts +11 -5
- package/dist/http/memory-compliance-failure-reply.js +17 -8
- package/dist/http/memory-store-refusal-reply.d.ts +14 -11
- package/dist/http/memory-store-refusal-reply.js +16 -5
- package/dist/http/resume-legs.js +8 -3
- package/dist/http/routes/approvals-assistant.d.ts +25 -3
- package/dist/http/routes/approvals-assistant.js +27 -26
- package/dist/http/routes/capabilities.js +6 -3
- package/dist/http/routes/mcp-reconnect.d.ts +17 -0
- package/dist/http/routes/mcp-reconnect.js +6 -1
- package/dist/http/routes/memory-bundle.js +1 -1
- package/dist/http/routes/memory-compliance.js +1 -1
- package/dist/http/routes/memory-origin.d.ts +1 -1
- package/dist/http/routes/memory-origin.js +1 -1
- package/dist/http/routes/runs.js +47 -33
- package/dist/http/routes/sessions.d.ts +1 -1
- package/dist/http/routes/sessions.js +23 -22
- package/dist/http/routes/workflows.js +2 -0
- package/dist/http/wire-types.d.ts +17 -13
- package/dist/images/manifest.d.ts +7 -1
- package/dist/images/manifest.js +18 -16
- package/dist/memory-store-refusal.d.ts +38 -18
- package/dist/memory-store-refusal.js +25 -7
- package/dist/model-unreachable-notice.d.ts +1 -1
- package/dist/namespaced-rule-name.d.ts +46 -41
- package/dist/namespaced-rule-name.js +36 -22
- package/dist/observability/corrupt-read-seat.d.ts +4 -0
- package/dist/observability/corrupt-read-seat.js +1 -0
- package/dist/observability/fail-open.d.ts +19 -3
- package/dist/observability/fail-open.js +19 -3
- package/dist/operator-ask.d.ts +6 -2
- package/dist/orchestration/workflow-agent-session-index.d.ts +82 -59
- package/dist/orchestration/workflow-agent-session-index.js +80 -14
- package/dist/orchestration/workflow-completion-inbox.d.ts +5 -0
- package/dist/orchestration/workflow-completion-inbox.js +39 -17
- package/dist/orchestration/workflow-notify-journal.d.ts +1 -1
- package/dist/orchestration/workflow-notify-journal.js +7 -2
- package/dist/outbound-dispatcher.d.ts +29 -20
- package/dist/outbound-dispatcher.js +105 -16
- package/dist/parked-decide.d.ts +43 -48
- package/dist/parked-decide.js +54 -16
- package/dist/permission-rule-vocab.d.ts +4 -3
- package/dist/permission-rule-vocab.js +1 -1
- package/dist/plugins/approval-ask-store-sql.d.ts +17 -20
- package/dist/plugins/approval-ask-store-sql.js +0 -3
- package/dist/plugins/approval-exemption-store.d.ts +4 -0
- package/dist/plugins/approval-exemption-store.js +1 -0
- package/dist/plugins/blob-backend.d.ts +13 -0
- package/dist/plugins/blob-backend.js +26 -0
- package/dist/plugins/local-session-store.d.ts +78 -47
- package/dist/plugins/local-session-store.js +152 -122
- package/dist/plugins/one-time-migrations.d.ts +11 -0
- package/dist/plugins/one-time-migrations.js +128 -4
- package/dist/plugins/pg-pool.js +1 -1
- package/dist/plugins/snapshot-bucket-presence.d.ts +36 -0
- package/dist/plugins/snapshot-bucket-presence.js +40 -0
- package/dist/plugins/store-backend.d.ts +17 -6
- package/dist/plugins/store-backend.js +46 -16
- package/dist/plugins/store-open-wait.d.ts +33 -0
- package/dist/plugins/store-open-wait.js +73 -0
- package/dist/plugins/tidb-pool.js +1 -1
- package/dist/plugins/web-search.d.ts +20 -1
- package/dist/plugins/web-search.js +39 -10
- package/dist/plugins/workflow-run-store-sql.d.ts +7 -6
- package/dist/plugins/workflow-run-store-sql.js +5 -7
- package/dist/request-key-closure.js +1 -1
- package/dist/request-value-closure.d.ts +1 -1
- package/dist/rule-entry-list.d.ts +35 -46
- package/dist/rule-entry-list.js +18 -26
- package/dist/rules-consent.js +3 -0
- package/dist/run-local.js +5 -3
- package/dist/runs.d.ts +1 -1
- package/dist/security.d.ts +5 -3
- package/dist/security.js +4 -0
- package/dist/session-policy-wire.d.ts +13 -12
- package/dist/session-policy-wire.js +11 -11
- package/dist/spec-fields.d.ts +13 -11
- package/dist/spec-fields.js +4 -7
- package/dist/standing-answer-grant.d.ts +14 -2
- package/dist/standing-answer-grant.js +8 -1
- package/dist/store-posture.d.ts +14 -6
- package/dist/store-posture.js +7 -4
- package/dist/task-settings.d.ts +27 -55
- package/dist/task-settings.js +13 -21
- package/dist/tool-approval.d.ts +133 -138
- package/dist/tool-approval.js +175 -66
- package/dist/trace/core-keyset-guard.d.ts +5 -5
- package/dist/trace/redact.d.ts +6 -0
- package/dist/trace/redact.js +9 -9
- package/dist/trace/self-minted-wire.d.ts +16 -2
- package/dist/trace/self-minted-wire.js +7 -0
- package/dist/user-input-ledger.d.ts +30 -14
- package/dist/user-input-ledger.js +25 -14
- package/package.json +2 -2
- package/scripts/drain-check-local.mjs +74 -3
- package/scripts/drain-check.mysql.sql +43 -17
- 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",
|
|
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
|
|
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"),
|
|
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
|
|
77
|
-
`
|
|
78
|
-
`would
|
|
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(() =>
|
|
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
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
128
|
-
|
|
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
|
-
|
|
132
|
-
this.
|
|
133
|
-
this.
|
|
134
|
-
this.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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-
|
|
183
|
+
throw new SessionError("storage", `LocalSessionStore: session-meta sidecar write failed for session ${sessionId} — nothing was recorded`);
|
|
184
|
+
return changed;
|
|
153
185
|
}
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
158
|
-
|
|
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
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
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((
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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.
|
|
316
|
+
await this.meta.ensureLoaded();
|
|
275
317
|
const session = await this.repo.create({});
|
|
276
318
|
const id = (await session.getMetadata()).id;
|
|
277
|
-
this.
|
|
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.
|
|
341
|
+
await this.meta.ensureLoaded();
|
|
303
342
|
const session = await this.repo.create({ id: sessionId });
|
|
304
|
-
await this.
|
|
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
|
-
|
|
348
|
+
await this.meta.ensureLoaded();
|
|
349
|
+
if (ownerRecorded(this.meta.state.entries.get(sessionId)))
|
|
312
350
|
return;
|
|
313
|
-
await this.
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
324
|
-
|
|
362
|
+
if (claim && wrote)
|
|
363
|
+
this.log.info("session_owner_claimed", { sessionId });
|
|
325
364
|
}
|
|
326
365
|
async ownerOf(sessionId) {
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
505
|
+
await this.meta.ensureLoaded();
|
|
475
506
|
await this.repo.importEntries(sessionId, owner ?? undefined, entries);
|
|
476
|
-
this.
|
|
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.
|
|
486
|
-
const prior =
|
|
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
|
|
503
|
-
if (
|
|
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.
|
|
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
|
|
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}。 */
|