@sema-agent/server 7.118.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 +85 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/USAGE.md +24 -13
- 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 +5 -5
- package/dist/approval.d.ts +51 -5
- package/dist/approval.js +76 -13
- 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 +5 -1
- package/dist/boot/parked-revive-gate.js +8 -2
- package/dist/boot/resolve-spec.js +7 -4
- package/dist/boot/stage-07-capability-layer.js +5 -1
- 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/scenarios.d.ts +34 -7
- package/dist/capabilities/scenarios.js +33 -6
- package/dist/config-catalog.js +3 -3
- package/dist/config.js +2 -2
- package/dist/deployment-governance.d.ts +5 -2
- package/dist/deployment-governance.js +4 -3
- package/dist/host-decision.d.ts +52 -27
- package/dist/host-decision.js +14 -7
- package/dist/http/admission.js +6 -6
- package/dist/http/resume-legs.js +2 -1
- package/dist/http/routes/approvals-assistant.d.ts +25 -3
- package/dist/http/routes/approvals-assistant.js +9 -5
- package/dist/http/routes/capabilities.js +4 -3
- package/dist/http/routes/memory-origin.d.ts +1 -1
- package/dist/http/routes/runs.js +11 -5
- package/dist/http/routes/sessions.js +20 -16
- package/dist/http/wire-types.d.ts +12 -13
- package/dist/images/manifest.d.ts +7 -1
- package/dist/images/manifest.js +18 -16
- package/dist/memory-store-refusal.d.ts +7 -12
- package/dist/memory-store-refusal.js +4 -5
- package/dist/namespaced-rule-name.d.ts +4 -3
- package/dist/observability/fail-open.d.ts +5 -1
- package/dist/observability/fail-open.js +5 -1
- package/dist/operator-ask.d.ts +6 -5
- 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 +6 -0
- package/dist/plugins/one-time-migrations.js +19 -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/rule-entry-list.js +1 -1
- package/dist/run-local.js +4 -3
- package/dist/security.d.ts +5 -3
- package/dist/security.js +4 -0
- package/dist/session-policy-wire.js +7 -12
- package/dist/spec-fields.d.ts +6 -2
- 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 +6 -18
- package/dist/tool-approval.d.ts +97 -43
- package/dist/tool-approval.js +120 -31
- package/dist/trace/redact.d.ts +6 -0
- package/dist/trace/redact.js +9 -9
- package/dist/trace/self-minted-wire.d.ts +1 -1
- package/dist/user-input-ledger.d.ts +19 -15
- package/dist/user-input-ledger.js +12 -20
- package/package.json +2 -2
|
@@ -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 };
|
|
@@ -65,6 +65,12 @@ export interface OneTimeMigrationWindow {
|
|
|
65
65
|
* 而留着 M3 会让 M3 永远判「跑过了」。三条同版引入、同一天到期,本来就同一次删。
|
|
66
66
|
*/
|
|
67
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;
|
|
68
74
|
/** local 车道上 SQL 独有的四条(M1 / M2 / M4 / M5)的读数(M3 / M6 两车道都有,local 腿的读数由 `LocalBackend.runOneTimeMigrations` 现算):那几张表 / 列在文件底座上从来不存在 ⇒ 如实答 absent / 0。 */
|
|
69
75
|
export declare const NO_SQL_MIGRATIONS: Omit<OneTimeMigrationReport, "askOriginRowsMigrated" | "holderDeathCodeRowsBackfilled">;
|
|
70
76
|
/** 启动期的日志口(logger 的子集)。两级各有其用,见 {@link logOneTimeMigrations}。 */
|
|
@@ -119,7 +119,24 @@ export const ONE_TIME_MIGRATIONS = [
|
|
|
119
119
|
"`decide.workflow_parks_unreadable`);出路不变(续跑发起它的那条 workflow run);随包排空判据没了,换钉前没有机器读数可数这些卡",
|
|
120
120
|
reportKeys: ["workflowParkIndexKey", "workflowParkIndexRowsCarried"],
|
|
121
121
|
},
|
|
122
|
+
{
|
|
123
|
+
id: "M10",
|
|
124
|
+
introducedIn: "7.119.0",
|
|
125
|
+
releasedOn: "2026-10-08",
|
|
126
|
+
removableAfter: "2026-11-07",
|
|
127
|
+
removal: "删 local 车道的认领臂:`LocalSessionStore.register` 里「属主从没被记下的存量会话 ⇒ 带 principal 的登记认领它」那一支、认领日志行 `session_owner_claimed`" +
|
|
128
|
+
"(连同店构造参数里只为它存在的 `log` 位)与读本表 M10 行的 `ownerClaimWindowOpen`;`security.ts` `createAuthorizer` 里对无主会话的那次认领 `register` 调用;" +
|
|
129
|
+
"`http/routes/sessions.ts` 409 `conflict.session_ownerless` 句里「Ways out: on the local backend, during the upgrade window …」那半句(连同冻结表 " +
|
|
130
|
+
"`test/api-error-text-freeze.test.ts` 那一行、契约码目录 `docs/ASSISTANT-WIRE-CONTRACT.md` 那一行);本文件的 `migrationWindowOpen`(若再无别的读者)",
|
|
131
|
+
afterRemoval: "没有响亮臂(删码本身零行为变化:窗关那一刻起认领臂已经不生效):属主从没被记下的存量会话(边档那一格只有化身 —— 跳过了带本条的版本直接升," +
|
|
132
|
+
"或窗内从没被带 principal 提交过)照旧答无主(`ownerOf` = null):它的会话规则这条路由读不了也改不了(409 `conflict.session_ownerless`)," +
|
|
133
|
+
"已存的规则对写下它们的 principal 的 run 照旧生效;出路 = 删会话(`DELETE /v1/sessions/:id`,规则随之删)",
|
|
134
|
+
reportKeys: [],
|
|
135
|
+
},
|
|
122
136
|
];
|
|
137
|
+
export function migrationWindowOpen(window, today) {
|
|
138
|
+
return window.removableAfter === null || today <= window.removableAfter;
|
|
139
|
+
}
|
|
123
140
|
export const NO_SQL_MIGRATIONS = {
|
|
124
141
|
retiredSnapshotTable: "absent",
|
|
125
142
|
retiredSnapshotRows: 0,
|
|
@@ -533,7 +550,8 @@ export function logOneTimeMigrations(log, backend, report) {
|
|
|
533
550
|
"run.orphaned; M7 writes nothing — it is the window of the refusal that keeps a suspended task whose stored allow list was written " +
|
|
534
551
|
"under an earlier reading from resuming under the current one; M8: parked rows' allow-layer disclosure written by an earlier engine " +
|
|
535
552
|
"under its old key shadowedRule rewritten to allowRuleShadowed; M9: the workflow park pointer index rebuilt with the key (session, run), " +
|
|
536
|
-
"its rows carried over
|
|
553
|
+
"its rows carried over; M10 writes nothing at boot — on the local backend, while its window is open, the first task submitted with a principal " +
|
|
554
|
+
"on a session written by an earlier version (which recorded no owner) claims that session for the principal, one session_owner_claimed line per claim). Each step " +
|
|
537
555
|
"runs on every boot until its window closes (the release that introduced it + 30 days; the per-step date is in " +
|
|
538
556
|
"removableAfter, null while that release is not published yet) and is deleted in the first minor after that — start a " +
|
|
539
557
|
"version that still carries a step at least once before upgrading past its deletion.",
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 对象存储配了快照桶却**不存在** = 部署错 —— boot 期核**一次**,缺 ⇒ 响亮拒启(不是每一拍 sweep 一条静默的 404)。
|
|
3
|
+
*
|
|
4
|
+
* ## 病
|
|
5
|
+
* `SESSION_SNAPSHOT_BUCKET`(缺省 `session-snapshots`)是 snapshot-BLOB offload 与 SendUserFile 私有 presigned 轨 / 沙箱 staging 的桶。server 不建桶
|
|
6
|
+
* (`docs/DEPLOY-PREREQS.md`「对象存储桶」),sema-up 的建桶清单又漏过它 ⇒ 起服绿,之后每一次对象写、每一拍孤儿 sweep 的列举(每分钟)都是
|
|
7
|
+
* 404 NoSuchBucket —— 运维读到的是一串「失败的请求」,不是「桶没建」。
|
|
8
|
+
*
|
|
9
|
+
* ## 形(一条规则,不加特例)
|
|
10
|
+
* · 核的是 {@link MinioBlobBackend.probeBucket}(presigned 列举 `max-keys=1`);**只有** 404 + `NoSuchBucket` 是「缺」,其余证明不了的形(连不上 / 403 / 5xx / 没有
|
|
11
|
+
* NoSuchBucket 的 404)⇒ **不拒**、照常起服,走登记过的 F 类兜底口 `recordFailOpen("server.snapshot-bucket.presence-unproven")`(计数 + 探针行 + 记录器的一次性 warn;
|
|
12
|
+
* 三问写在该 tag 的 note 里)—— 拒启要有证据,没有证据只留痕;
|
|
13
|
+
* · 缺 = 配置好的店资源还没就绪,与「库连不上」同一类处境:在 `STORE_LOCK_WAIT_MS` 窗内重试({@link openWithinWait},同一只窗、同一只节拍、同一条窗尾规则 —— 编排里
|
|
14
|
+
* 建桶晚于 server 起是常态),到窗时手上有缺桶的证据 ⇒ 拒启,拒句点名桶名、`MINIO_ENDPOINT` 的显示安全形(`buildDisplaySafeUrl` 的产物:userinfo 落地、
|
|
15
|
+
* query 值成记号,不带密钥)与出路;探针从头到尾没答过(每一次都挂到窗尾)⇒ 同样是「证明不了」,走上面那只兜底口、照常起服。
|
|
16
|
+
* · 只核**快照桶**(`config.snapshotBlobStore.bucket`;SendUserFile 的私桶缺省同一只);公桶 `sema-public` 是运维另配的匿名桶,不在本核之内。
|
|
17
|
+
*/
|
|
18
|
+
import type { ServiceConfig } from "../config-types.js";
|
|
19
|
+
import { type StoreOpenWaitClock } from "./store-open-wait.js";
|
|
20
|
+
type SnapshotBlobStoreConfig = NonNullable<ServiceConfig["snapshotBlobStore"]>;
|
|
21
|
+
/** boot 期单次探针的上界(ms):黑洞端点(丢包不拒连)不许把起服拖到对象存储 IO 的 120s;到点 ⇒ `unknown`(证明不了缺桶 ⇒ 不拒、报一次)。 */
|
|
22
|
+
export declare const SNAPSHOT_BUCKET_PROBE_TIMEOUT_MS = 10000;
|
|
23
|
+
/** 缺桶的拒启句(唯一铸点)。 */
|
|
24
|
+
/** @param endpoint `MINIO_ENDPOINT` 的**显示安全形**(调用方过 {@link buildDisplaySafeUrl};本函数不碰原串)。 */
|
|
25
|
+
export declare function buildSnapshotBucketRefusal(bucket: string, endpoint: string, waitedMs: number): string;
|
|
26
|
+
/** 探针到窗尾一次都没答过时,兜底口 detail 里的成因(证明不了缺桶的一形)。 */
|
|
27
|
+
export declare const SNAPSHOT_BUCKET_PROBE_PENDING = "probe still pending when the window ended";
|
|
28
|
+
/**
|
|
29
|
+
* boot 期核一次快照桶(`boot/stores.ts`)。缺桶 ⇒ 窗内重试、到窗拒启;证明不了 ⇒ 兜底口留痕、照常起服。
|
|
30
|
+
* @param waitMs `config.storeLockWaitMs`。`0` ⇒ 只核一次(缺就当场拒)。
|
|
31
|
+
*/
|
|
32
|
+
export declare function ensureSnapshotBucket(store: SnapshotBlobStoreConfig, waitMs: number, logger: {
|
|
33
|
+
warn(msg: string, meta?: Record<string, unknown>): void;
|
|
34
|
+
}, clock?: StoreOpenWaitClock): Promise<void>;
|
|
35
|
+
export {};
|
|
36
|
+
//# sourceMappingURL=snapshot-bucket-presence.d.ts.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { buildDisplaySafeUrl } from "../trace/redact.js";
|
|
2
|
+
import { MinioBlobBackend } from "./blob-backend.js";
|
|
3
|
+
import { recordFailOpen } from "../observability/fail-open.js";
|
|
4
|
+
import { isStoreOpenWindowExpired, openWithinWait } from "./store-open-wait.js";
|
|
5
|
+
export const SNAPSHOT_BUCKET_PROBE_TIMEOUT_MS = 10_000;
|
|
6
|
+
class SnapshotBucketMissing extends Error {
|
|
7
|
+
constructor() {
|
|
8
|
+
super("snapshot bucket missing");
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function buildSnapshotBucketRefusal(bucket, endpoint, waitedMs) {
|
|
12
|
+
const waited = waitedMs > 0 ? `after retrying for ${waitedMs}ms (STORE_LOCK_WAIT_MS)` : "on the first check (STORE_LOCK_WAIT_MS=0: no wait)";
|
|
13
|
+
return (`Object storage is configured (MINIO_ENDPOINT → ${endpoint}) but the snapshot bucket "${bucket}" (SESSION_SNAPSHOT_BUCKET; default session-snapshots) does not exist there ${waited} — refusing to boot: ` +
|
|
14
|
+
`the server never creates buckets, so every snapshot-offload write and SendUserFile private-track request would answer 404 NoSuchBucket (and the orphan sweep would log one every minute, forever). ` +
|
|
15
|
+
`Next: create the bucket (mc mb <alias>/${bucket}; this version's sema-up scripts create it — re-run them on an existing sema-up deployment), or set SESSION_SNAPSHOT_BUCKET to a bucket that exists.`);
|
|
16
|
+
}
|
|
17
|
+
export const SNAPSHOT_BUCKET_PROBE_PENDING = "probe still pending when the window ended";
|
|
18
|
+
export async function ensureSnapshotBucket(store, waitMs, logger, clock) {
|
|
19
|
+
const probe = new MinioBlobBackend({ endpoint: store.endpoint, bucket: store.bucket, accessKey: store.accessKey, secretKey: store.secretKey, ...(store.region ? { region: store.region } : {}), ...(store.keyPrefix ? { keyPrefix: store.keyPrefix } : {}) });
|
|
20
|
+
let verdict;
|
|
21
|
+
try {
|
|
22
|
+
verdict = await openWithinWait(async () => {
|
|
23
|
+
const r = await probe.probeBucket(SNAPSHOT_BUCKET_PROBE_TIMEOUT_MS);
|
|
24
|
+
if (r.state === "missing")
|
|
25
|
+
throw new SnapshotBucketMissing();
|
|
26
|
+
return r;
|
|
27
|
+
}, waitMs, (w) => logger.warn("snapshot_bucket_wait", { bucket: store.bucket, endpoint: buildDisplaySafeUrl(store.endpoint), waitMs: w.waitMs, error: w.error }), clock);
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
if (err instanceof SnapshotBucketMissing)
|
|
31
|
+
throw new Error(buildSnapshotBucketRefusal(store.bucket, buildDisplaySafeUrl(store.endpoint), waitMs), { cause: err });
|
|
32
|
+
if (!isStoreOpenWindowExpired(err))
|
|
33
|
+
throw err;
|
|
34
|
+
verdict = { state: "unknown", detail: SNAPSHOT_BUCKET_PROBE_PENDING };
|
|
35
|
+
}
|
|
36
|
+
if (verdict.state === "unknown") {
|
|
37
|
+
recordFailOpen("server.snapshot-bucket.presence-unproven", `bucket=${store.bucket} endpoint=${buildDisplaySafeUrl(store.endpoint)} ${verdict.detail}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=snapshot-bucket-presence.js.map
|
|
@@ -398,8 +398,11 @@ export declare function createStoreBackend(config: ServiceConfig, onWarn?: BootL
|
|
|
398
398
|
* 而 LocalBackend 的 `ensureSchema` 是**空实现**,真正会抛的 mkdir + 数据根 BootLock 在构造函数里、
|
|
399
399
|
* 落在 try 之外。于是那条降级臂对 local 形从来没生效过,注释宣称的行为与代码真做的事撕裂。
|
|
400
400
|
*
|
|
401
|
-
*
|
|
402
|
-
*
|
|
401
|
+
* **降级臂只有一条:缺省推导的 local**(`dbBackend=local && !dbBackendExplicit`)。🔴 **SQL 族没有降级臂** —— 配了 DSN 就是要这只库:
|
|
402
|
+
* `SESSION_BACKEND=auto` 与显式 `DB_BACKEND=mysql|pg` 同一待遇,库当下打不开 ⇒ 在 `STORE_LOCK_WAIT_MS` 的窗内重试
|
|
403
|
+
* ({@link openWithinWait}),到窗仍打不开 ⇒ 拒启(拒句 {@link buildSqlStoreOpenRefusal} 点名后端与库的显示安全形)。修前 `auto` 在这里
|
|
404
|
+
* 静默落内存(只留一行 warn):会话 / 停驻 / run 账本进进程内存、重启即丢,而 `/health` 照样 200 —— [ref] P 类 fail-open。
|
|
405
|
+
* 零 DSN 的内存是配置说的(`config.ts` 的 `auto` 在无坐标时就不探测),不经过本函数。**显式** DB_BACKEND=local 仍 fail-loud——
|
|
403
406
|
* operator 要了 durable,静默丢=丢数据。 */
|
|
404
407
|
export declare function openStoreBackendWithFallback(config: ServiceConfig, logger: {
|
|
405
408
|
warn: (msg: string, meta?: Record<string, unknown>) => void;
|
|
@@ -409,9 +412,17 @@ export declare function openStoreBackendWithFallback(config: ServiceConfig, logg
|
|
|
409
412
|
degradedReason: StoreDegradedReason | undefined;
|
|
410
413
|
}>;
|
|
411
414
|
/**
|
|
412
|
-
*
|
|
413
|
-
* `
|
|
414
|
-
* (
|
|
415
|
+
* 已配置的 SQL 库坐标 → 一条只含 `scheme://host:port` 的 URL 串,**交给**配置型 URL 的唯一显示铸点 {@link buildDisplaySafeUrl}
|
|
416
|
+
* (出口处写 `buildDisplaySafeUrl(sqlStoreCoordinatesAsUrl(config))`)。它**不是**一份显示投影:显示形只有铸点一只;这里只做「结构化坐标 → URL 串」的拼装
|
|
417
|
+
* (`config.tidb` / `config.pg` 本来就是结构化坐标,没有 DSN 字符串),user / password / database 一概不进产物。
|
|
418
|
+
* host 过不了闭字符集(拼进 URL 会把 `/` `@` `?` 之类带成别的成分)或无坐标 ⇒ 交出一个不是 URL 的串,铸点答它的占位 `«redacted:url»`。
|
|
419
|
+
* scheme 只是引擎名(`postgres` / `mysql`),不对应任何连接。
|
|
415
420
|
*/
|
|
416
|
-
export declare function
|
|
421
|
+
export declare function sqlStoreCoordinatesAsUrl(config: Pick<ServiceConfig, "dbBackend" | "tidb" | "pg">): string;
|
|
422
|
+
/**
|
|
423
|
+
* SQL 族「配了 DSN 而店打不开(窗内重试过了)」的拒启句(唯一铸点)。点名 `DB_BACKEND=<引擎>`、库的显示安全形(`postgres://host:port` / `mysql://host:port`,经 {@link sqlStoreCoordinatesAsUrl} 拼出、再过 {@link buildDisplaySafeUrl},
|
|
424
|
+
* 不带口令)、等了多久,出路两条:修坐标 / 连通性(窗是 `STORE_LOCK_WAIT_MS`);或**显式**内存部署(`DB_BACKEND=memory`)。🔴 没有第三条
|
|
425
|
+
* 「`SESSION_BACKEND=auto` 内存起服」—— 那一条就是被删掉的静默降级。驱动原文只取 message(与修前 `fatalExit` 打出的同一句),凭据不在其中(驱动不回显口令)。
|
|
426
|
+
*/
|
|
427
|
+
export declare function buildSqlStoreOpenRefusal(engine: "mysql" | "pg", endpoint: string, waitedMs: number, err: unknown): string;
|
|
417
428
|
//# sourceMappingURL=store-backend.d.ts.map
|