@sema-agent/server 7.124.0 → 7.125.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 +138 -0
- package/USAGE.md +46 -8
- package/dist/audit.d.ts +27 -5
- package/dist/audit.js +42 -8
- package/dist/boot/execution-env.d.ts +33 -1
- package/dist/boot/execution-env.js +31 -2
- package/dist/boot/resolve-spec.js +4 -3
- package/dist/boot/session-faces.d.ts +14 -1
- package/dist/boot/session-faces.js +6 -5
- package/dist/boot/stage-02-web-admin-pull.js +5 -1
- package/dist/boot/stage-05-execution-env.js +6 -2
- package/dist/boot/stage-09-leader.d.ts +1 -1
- package/dist/boot/stage-10-http-server.d.ts +1 -1
- package/dist/boot/web-admin.js +130 -140
- package/dist/capabilities/center-wiring.d.ts +2 -1
- package/dist/capabilities/composite-members.d.ts +1 -0
- package/dist/capabilities/composite-members.js +1 -0
- package/dist/capability-facts.d.ts +28 -3
- package/dist/capability-facts.js +20 -0
- package/dist/config-catalog.js +4 -1
- package/dist/config-d-migration.js +2 -1
- package/dist/config-lkg.d.ts +4 -1
- package/dist/config-lkg.js +16 -12
- package/dist/config-provider.d.ts +4 -1
- package/dist/config-provider.js +1 -0
- package/dist/config-types.d.ts +10 -3
- package/dist/config.js +44 -14
- package/dist/env-facts.d.ts +2 -2
- package/dist/env-facts.js +3 -2
- package/dist/fleet/fleet-bus.d.ts +24 -12
- package/dist/fleet/fleet-bus.js +4 -1
- package/dist/fleet/workspace-snapshot-residual.d.ts +10 -0
- package/dist/fleet/workspace-snapshot-residual.js +35 -0
- package/dist/hooks/hook-runner.d.ts +15 -27
- package/dist/hooks/hook-runner.js +19 -59
- package/dist/http/active-run-conflict.d.ts +3 -0
- package/dist/http/active-run-conflict.js +3 -0
- package/dist/http/admission.js +7 -7
- package/dist/http/approval-sweeps.js +2 -4
- package/dist/http/device-placement.js +1 -0
- package/dist/http/dispatch.js +1 -1
- package/dist/http/durable-run-leg.d.ts +20 -6
- package/dist/http/durable-run-leg.js +4 -4
- package/dist/http/idempotency.d.ts +83 -38
- package/dist/http/idempotency.js +38 -13
- package/dist/http/request-guards.d.ts +18 -0
- package/dist/http/request-guards.js +12 -3
- package/dist/http/resume-legs.js +7 -1
- package/dist/http/route-ctx.d.ts +3 -3
- package/dist/http/route-ctx.js +1 -1
- package/dist/http/routes/a2a-serve.js +27 -11
- package/dist/http/routes/capabilities.js +1 -0
- package/dist/http/routes/fleet.js +2 -0
- package/dist/http/routes/images.js +2 -2
- package/dist/http/routes/leader.d.ts +4 -0
- package/dist/http/routes/leader.js +13 -3
- package/dist/http/routes/observability.js +2 -0
- package/dist/http/routes/runs.d.ts +6 -2
- package/dist/http/routes/runs.js +71 -101
- package/dist/http/routes/session-sync.d.ts +3 -0
- package/dist/http/routes/session-sync.js +30 -1
- package/dist/http/routes/sessions-list.d.ts +11 -0
- package/dist/http/routes/sessions-list.js +6 -6
- package/dist/http/routes/sessions.d.ts +6 -1
- package/dist/http/routes/sessions.js +41 -10
- package/dist/http/routes/tasks.js +63 -21
- package/dist/http/run-read-face.d.ts +26 -0
- package/dist/http/run-read-face.js +67 -0
- package/dist/http/server.d.ts +4 -4
- package/dist/http/server.js +1 -2
- package/dist/http/submit-replay.d.ts +40 -0
- package/dist/http/submit-replay.js +22 -0
- package/dist/leader/endpoint.d.ts +10 -5
- package/dist/leader/endpoint.js +27 -5
- package/dist/observability/cost-quota.d.ts +51 -7
- package/dist/observability/cost-quota.js +35 -17
- package/dist/observability/fail-open.d.ts +1 -1
- package/dist/observability/fail-open.js +1 -1
- package/dist/observability/run-terminal.d.ts +5 -9
- package/dist/observability/run-terminal.js +2 -7
- package/dist/plugins/approval-ask-store-sql.js +1 -1
- package/dist/plugins/caching-session-store.d.ts +7 -2
- package/dist/plugins/caching-session-store.js +6 -0
- package/dist/plugins/device-store-sql.js +1 -1
- package/dist/plugins/ensure-index.js +22 -0
- package/dist/plugins/file-run-store.d.ts +24 -14
- package/dist/plugins/file-run-store.js +52 -12
- package/dist/plugins/leader-run-store-sql.d.ts +1 -11
- package/dist/plugins/leader-run-store-sql.js +1 -5
- package/dist/plugins/local-session-store.d.ts +35 -6
- package/dist/plugins/local-session-store.js +87 -18
- package/dist/plugins/memory-run-store.d.ts +13 -12
- package/dist/plugins/memory-run-store.js +25 -0
- package/dist/plugins/one-time-migrations.js +1 -1
- package/dist/plugins/pg-cost-quota.d.ts +1 -1
- package/dist/plugins/pg-cost-quota.js +2 -5
- package/dist/plugins/pg-pool.js +3 -0
- package/dist/plugins/pg-session-storage.d.ts +11 -1
- package/dist/plugins/pg-session-storage.js +44 -8
- package/dist/plugins/remote-env-k8s.d.ts +127 -12
- package/dist/plugins/remote-env-k8s.js +270 -27
- package/dist/plugins/run-store-sql.d.ts +26 -12
- package/dist/plugins/run-store-sql.js +50 -25
- package/dist/plugins/schema-consistency.d.ts +1 -13
- package/dist/plugins/schema-consistency.js +1 -41
- package/dist/plugins/schema-indexes.js +1 -0
- package/dist/plugins/schema-live-columns.d.ts +20 -0
- package/dist/plugins/schema-live-columns.js +42 -0
- package/dist/plugins/store-contracts.d.ts +40 -2
- package/dist/plugins/tidb-cost-quota.d.ts +1 -1
- package/dist/plugins/tidb-cost-quota.js +2 -5
- package/dist/plugins/tidb-pool.js +14 -3
- package/dist/plugins/tidb-session-store.d.ts +22 -2
- package/dist/plugins/tidb-session-store.js +33 -6
- package/dist/plugins/write-behind-counter.d.ts +12 -5
- package/dist/plugins/write-behind-counter.js +14 -7
- package/dist/prompts-domain-validate.js +3 -2
- package/dist/rules-consent.d.ts +2 -2
- package/dist/rules-consent.js +2 -2
- package/dist/run-local.js +31 -23
- package/dist/runs.d.ts +9 -0
- package/dist/runs.js +8 -0
- package/dist/runtime-governance.d.ts +3 -3
- package/dist/runtime-governance.js +2 -4
- package/dist/security.d.ts +19 -2
- package/dist/session-row.d.ts +53 -0
- package/dist/session-row.js +47 -0
- package/dist/session-sync.d.ts +6 -0
- package/dist/session-sync.js +5 -1
- package/dist/session-titler.d.ts +9 -5
- package/dist/session-titler.js +3 -1
- package/dist/spec-fields.js +2 -2
- package/dist/task-cwd.d.ts +0 -20
- package/dist/task-cwd.js +2 -10
- package/dist/tool-approval.d.ts +5 -7
- package/dist/trace/redact.d.ts +23 -50
- package/dist/trace/redact.js +58 -18
- package/dist/trace/self-minted-wire.d.ts +25 -13
- package/dist/trace/self-minted-wire.js +9 -4
- package/dist/web-admin-client/apply-effective.d.ts +10 -12
- package/dist/web-admin-client/apply-effective.js +2 -3
- package/dist/web-admin-client/candidate-gate.d.ts +69 -0
- package/dist/web-admin-client/candidate-gate.js +27 -0
- package/dist/web-admin-client/http-client.d.ts +2 -1
- package/dist/web-admin-client/http-client.js +2 -1
- package/dist/web-admin-client/pull-state.d.ts +6 -4
- package/dist/web-admin-client/read-warnings.d.ts +29 -5
- package/dist/web-admin-client/read-warnings.js +24 -4
- package/dist/web-admin-client/stage-limits.js +4 -1
- package/dist/workspace-space.d.ts +41 -0
- package/dist/workspace-space.js +5 -0
- package/package.json +3 -3
|
@@ -34,7 +34,8 @@ function userText(entry) {
|
|
|
34
34
|
}
|
|
35
35
|
return null;
|
|
36
36
|
}
|
|
37
|
-
const SIDECAR_DEGRADES = { lastActivity: "recency", title: "
|
|
37
|
+
const SIDECAR_DEGRADES = { lastActivity: "recency", title: "title", pinned: "pin state", "instruction-source": "instruction-source baseline", "session-meta": "session owner + incarnation (the write that needed it failed loudly)" };
|
|
38
|
+
const readPinnedSidecar = createZodColumnReader(z.record(z.string(), z.literal(true)));
|
|
38
39
|
function readSessionMetaCell(cell) {
|
|
39
40
|
return typeof cell === "string" ? { incarnation: cell, owner: undefined } : cell;
|
|
40
41
|
}
|
|
@@ -156,6 +157,7 @@ export class LocalSessionStore {
|
|
|
156
157
|
lastWarnAt = 0;
|
|
157
158
|
sidecarPath;
|
|
158
159
|
titles;
|
|
160
|
+
pinned;
|
|
159
161
|
instructionSources;
|
|
160
162
|
meta;
|
|
161
163
|
log;
|
|
@@ -166,6 +168,7 @@ export class LocalSessionStore {
|
|
|
166
168
|
this.log = opts?.log ?? createLogger();
|
|
167
169
|
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);
|
|
168
170
|
this.titles = sidecar("session-titles.json", "title", readStringRecordColumn);
|
|
171
|
+
this.pinned = sidecar("session-pinned.json", "pinned", readPinnedSidecar);
|
|
169
172
|
this.instructionSources = sidecar("session-instruction-sources.json", "instruction-source", readStringRecordColumn);
|
|
170
173
|
this.meta = sidecar("session-incarnations.json", "session-meta", readSessionMetaSidecar, true);
|
|
171
174
|
}
|
|
@@ -229,6 +232,62 @@ export class LocalSessionStore {
|
|
|
229
232
|
await this.titles.persist();
|
|
230
233
|
return true;
|
|
231
234
|
}
|
|
235
|
+
async patchSessionMeta(sessionId, patch, face) {
|
|
236
|
+
await this.meta.ensureLoaded();
|
|
237
|
+
const occupied = () => this.occupiedBy(sessionId, face);
|
|
238
|
+
if (!occupied() || (await this.listedMeta(sessionId)) === undefined)
|
|
239
|
+
return undefined;
|
|
240
|
+
const { title, pinned } = patch;
|
|
241
|
+
if (title !== undefined) {
|
|
242
|
+
await this.titles.ensureLoaded();
|
|
243
|
+
const ok = await this.titles.commit((draft) => {
|
|
244
|
+
if (!occupied())
|
|
245
|
+
return false;
|
|
246
|
+
if (title === null)
|
|
247
|
+
return draft.delete(sessionId);
|
|
248
|
+
if (draft.get(sessionId) === title)
|
|
249
|
+
return false;
|
|
250
|
+
draft.set(sessionId, title);
|
|
251
|
+
return true;
|
|
252
|
+
});
|
|
253
|
+
if (!ok)
|
|
254
|
+
throw new SessionError("storage", `LocalSessionStore: title sidecar write failed for session ${sessionId} — the title was not recorded`);
|
|
255
|
+
}
|
|
256
|
+
if (pinned !== undefined) {
|
|
257
|
+
await this.pinned.ensureLoaded();
|
|
258
|
+
const ok = await this.pinned.commit((draft) => {
|
|
259
|
+
if (!occupied())
|
|
260
|
+
return false;
|
|
261
|
+
if (!pinned)
|
|
262
|
+
return draft.delete(sessionId);
|
|
263
|
+
if (draft.get(sessionId) === true)
|
|
264
|
+
return false;
|
|
265
|
+
draft.set(sessionId, true);
|
|
266
|
+
return true;
|
|
267
|
+
});
|
|
268
|
+
if (!ok)
|
|
269
|
+
throw new SessionError("storage", `LocalSessionStore: pin sidecar write failed for session ${sessionId} — the pin was not recorded`);
|
|
270
|
+
}
|
|
271
|
+
const rollups = await this.runRollups();
|
|
272
|
+
await this.loadRowSidecars();
|
|
273
|
+
const meta = await this.listedMeta(sessionId);
|
|
274
|
+
if (meta === undefined || !occupied())
|
|
275
|
+
return undefined;
|
|
276
|
+
const row = await this.summaryOf(meta, rollups);
|
|
277
|
+
return occupied() ? row : undefined;
|
|
278
|
+
}
|
|
279
|
+
occupiedBy(sessionId, face) {
|
|
280
|
+
const cell = this.meta.state.entries.get(sessionId);
|
|
281
|
+
return cell !== undefined && readSessionMetaCell(cell).incarnation === face.incarnation;
|
|
282
|
+
}
|
|
283
|
+
async sessionSummaryOf(sessionId) {
|
|
284
|
+
await this.loadRowSidecars();
|
|
285
|
+
const meta = await this.listedMeta(sessionId);
|
|
286
|
+
return meta === undefined ? undefined : this.summaryOf(meta, await this.runRollups());
|
|
287
|
+
}
|
|
288
|
+
async listedMeta(sessionId) {
|
|
289
|
+
return (await this.repo.list()).find((m) => m.id === sessionId);
|
|
290
|
+
}
|
|
232
291
|
async readInstructionSource(sessionId) {
|
|
233
292
|
await this.instructionSources.ensureLoaded();
|
|
234
293
|
return decodeInstructionSource(this.instructionSources.state.entries.get(sessionId));
|
|
@@ -400,28 +459,15 @@ export class LocalSessionStore {
|
|
|
400
459
|
this.pending.delete(sessionId);
|
|
401
460
|
}
|
|
402
461
|
async listSessions(opts) {
|
|
403
|
-
await this.
|
|
404
|
-
await this.titles.ensureLoaded();
|
|
405
|
-
await this.meta.ensureLoaded();
|
|
462
|
+
await this.loadRowSidecars();
|
|
406
463
|
const metas = await this.repo.list();
|
|
407
464
|
const rollups = await this.runRollups();
|
|
408
465
|
const all = [];
|
|
409
466
|
for (const meta of metas) {
|
|
410
|
-
const
|
|
411
|
-
const owner = cell === undefined ? null : (readSessionMetaCell(cell).owner ?? null);
|
|
467
|
+
const owner = this.ownerCellOf(meta.id);
|
|
412
468
|
if (opts.owner !== undefined && owner !== opts.owner && !(opts.includeUnowned === true && owner === null))
|
|
413
469
|
continue;
|
|
414
|
-
|
|
415
|
-
const lastActivityAt = this.lastActivity.get(meta.id) ?? firstActivityAt;
|
|
416
|
-
all.push({
|
|
417
|
-
sessionId: meta.id,
|
|
418
|
-
owner,
|
|
419
|
-
lastActivityAt,
|
|
420
|
-
firstActivityAt,
|
|
421
|
-
objectivePreview: await this.previewOf(meta),
|
|
422
|
-
...this.runProjectionOf(meta.id, rollups),
|
|
423
|
-
title: this.titles.state.entries.get(meta.id) ?? null,
|
|
424
|
-
});
|
|
470
|
+
all.push(await this.summaryOf(meta, rollups));
|
|
425
471
|
}
|
|
426
472
|
const qlc = opts.q?.toLowerCase();
|
|
427
473
|
const filtered = qlc ? all.filter((r) => r.objectivePreview?.toLowerCase().includes(qlc)) : all;
|
|
@@ -434,6 +480,29 @@ export class LocalSessionStore {
|
|
|
434
480
|
: filtered;
|
|
435
481
|
return after.slice(0, opts.limit);
|
|
436
482
|
}
|
|
483
|
+
async loadRowSidecars() {
|
|
484
|
+
await this.ensureLoaded();
|
|
485
|
+
await this.titles.ensureLoaded();
|
|
486
|
+
await this.pinned.ensureLoaded();
|
|
487
|
+
await this.meta.ensureLoaded();
|
|
488
|
+
}
|
|
489
|
+
ownerCellOf(sessionId) {
|
|
490
|
+
const cell = this.meta.state.entries.get(sessionId);
|
|
491
|
+
return cell === undefined ? null : (readSessionMetaCell(cell).owner ?? null);
|
|
492
|
+
}
|
|
493
|
+
async summaryOf(meta, rollups) {
|
|
494
|
+
const firstActivityAt = meta.createdAt || new Date(0).toISOString();
|
|
495
|
+
return {
|
|
496
|
+
sessionId: meta.id,
|
|
497
|
+
owner: this.ownerCellOf(meta.id),
|
|
498
|
+
lastActivityAt: this.lastActivity.get(meta.id) ?? firstActivityAt,
|
|
499
|
+
firstActivityAt,
|
|
500
|
+
objectivePreview: await this.previewOf(meta),
|
|
501
|
+
...this.runProjectionOf(meta.id, rollups),
|
|
502
|
+
title: this.titles.state.entries.get(meta.id) ?? null,
|
|
503
|
+
pinned: this.pinned.state.entries.get(meta.id) === true,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
437
506
|
async runRollups() {
|
|
438
507
|
const rows = await this.runs.listSessions({ limit: Number.MAX_SAFE_INTEGER });
|
|
439
508
|
return new Map(rows.map((r) => [r.sessionId, { runCount: r.runCount, lastStatus: r.lastStatus, lastRunId: r.lastRunId, ...(r.lastTaskRunId !== undefined ? { lastTaskRunId: r.lastTaskRunId } : {}) }]));
|
|
@@ -544,7 +613,7 @@ export class LocalSessionStore {
|
|
|
544
613
|
}
|
|
545
614
|
await this.dropMeta(sessionId);
|
|
546
615
|
this.lastActivity.delete(sessionId);
|
|
547
|
-
for (const map of [this.titles, this.instructionSources]) {
|
|
616
|
+
for (const map of [this.titles, this.pinned, this.instructionSources]) {
|
|
548
617
|
await map.ensureLoaded();
|
|
549
618
|
if (map.state.entries.delete(sessionId))
|
|
550
619
|
await map.persist();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type UsageRow } from "../usage-analytics.js";
|
|
2
2
|
import type { TaskStatus } from "@sema-agent/core";
|
|
3
|
-
import { type RunRecord, type SessionSummary, type RunEvent, type PersistedTaskResult, type LatestRunRef } from "./store-contracts.js";
|
|
3
|
+
import { type CreateRunOutcome, type NewRunMeta, type RunRecord, type SessionSummary, type RunEvent, type PersistedTaskResult, type LatestRunRef } from "./store-contracts.js";
|
|
4
4
|
import type { LedgerEventType } from "../trace/ledger-events.js";
|
|
5
5
|
/**
|
|
6
6
|
* [ref] P0 — the local lane's stand-in for the SQL twins' checkpoint-table JOIN. The file/memory run stores
|
|
@@ -25,6 +25,8 @@ export declare class MemoryRunStore {
|
|
|
25
25
|
private readonly runs;
|
|
26
26
|
/** sessionId → taskId — the single-active-run claim (the `task_active` table). */
|
|
27
27
|
private readonly active;
|
|
28
|
+
/** idemKey → taskId — the twin of `uq_task_run_idem`. */
|
|
29
|
+
private readonly byIdem;
|
|
28
30
|
/** taskId → events (kept seq-sorted on read). */
|
|
29
31
|
private readonly events;
|
|
30
32
|
/** [ref] the checkpoint-table stand-in (see {@link RunStoreCheckpointProbe}); absent ⇒ reapers NO-OP. */
|
|
@@ -32,17 +34,16 @@ export declare class MemoryRunStore {
|
|
|
32
34
|
/** [ref] wire the checkpoint probe (backend assembly, where checkpoint + run store are built together). */
|
|
33
35
|
setCheckpointProbe(probe: RunStoreCheckpointProbe): void;
|
|
34
36
|
private toRecord;
|
|
35
|
-
/** Create a run iff
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}>;
|
|
37
|
+
/** Create a run iff no submission key of this request names a row (idemKey first, then a caller-minted taskId —
|
|
38
|
+
* OBSERVED, see {@link CreateRunOutcome}) and the session has no active run (the task_active unique-key claim). */
|
|
39
|
+
createRun(taskId: string, sessionId: string, owner: string | null, instanceId: string, meta?: NewRunMeta): Promise<CreateRunOutcome>;
|
|
40
|
+
/** Undo THIS submission's key binding (`idem_key` → NULL) — called only where the submit surface refuses the submission
|
|
41
|
+
* before any work and terminalizes the row it just claimed (the sync leg's zero-work refusal, a device-placement reject):
|
|
42
|
+
* the error answer invites a retry, and a retry with the same key must run, not replay the refusal. The row stays (it
|
|
43
|
+
* is the refusal's record); only the key is freed. */
|
|
44
|
+
releaseIdemKey(taskId: string): Promise<void>;
|
|
45
|
+
/** The run a submission's scoped `Idempotency-Key` names, if any. */
|
|
46
|
+
getRunByIdem(idemKey: string): Promise<RunRecord | undefined>;
|
|
46
47
|
requestCancel(taskId: string, owner: string | null): Promise<boolean>;
|
|
47
48
|
isCancelRequested(taskId: string, owner: string | null): Promise<boolean>;
|
|
48
49
|
requestPreempt(taskId: string, owner: string | null): Promise<boolean>;
|
|
@@ -7,6 +7,7 @@ const ownerEq = (a, b) => (a ?? null) === (b ?? null);
|
|
|
7
7
|
export class MemoryRunStore {
|
|
8
8
|
runs = new Map();
|
|
9
9
|
active = new Map();
|
|
10
|
+
byIdem = new Map();
|
|
10
11
|
events = new Map();
|
|
11
12
|
checkpointProbe;
|
|
12
13
|
setCheckpointProbe(probe) {
|
|
@@ -31,6 +32,11 @@ export class MemoryRunStore {
|
|
|
31
32
|
};
|
|
32
33
|
}
|
|
33
34
|
async createRun(taskId, sessionId, owner, instanceId, meta = {}) {
|
|
35
|
+
const idemKey = meta.idemKey ?? null;
|
|
36
|
+
const byKey = idemKey !== null ? this.byIdem.get(idemKey) : undefined;
|
|
37
|
+
const existing = (byKey !== undefined ? this.runs.get(byKey) : undefined) ?? this.runs.get(taskId);
|
|
38
|
+
if (existing)
|
|
39
|
+
return { ok: false, existing: this.toRecord(existing) };
|
|
34
40
|
const claimed = this.active.get(sessionId);
|
|
35
41
|
if (claimed !== undefined)
|
|
36
42
|
return { ok: false, activeTaskId: claimed };
|
|
@@ -48,13 +54,29 @@ export class MemoryRunStore {
|
|
|
48
54
|
source: meta.source ?? null,
|
|
49
55
|
instanceId,
|
|
50
56
|
objectivePreview: meta.objectivePreview ?? null,
|
|
57
|
+
idemKey,
|
|
51
58
|
createdAt: now,
|
|
52
59
|
updatedAt: now,
|
|
53
60
|
cancelRequested: false,
|
|
54
61
|
preemptRequested: false,
|
|
55
62
|
});
|
|
63
|
+
if (idemKey !== null)
|
|
64
|
+
this.byIdem.set(idemKey, taskId);
|
|
56
65
|
return { ok: true };
|
|
57
66
|
}
|
|
67
|
+
async releaseIdemKey(taskId) {
|
|
68
|
+
const r = this.runs.get(taskId);
|
|
69
|
+
if (!r || r.idemKey === null)
|
|
70
|
+
return;
|
|
71
|
+
if (this.byIdem.get(r.idemKey) === taskId)
|
|
72
|
+
this.byIdem.delete(r.idemKey);
|
|
73
|
+
r.idemKey = null;
|
|
74
|
+
}
|
|
75
|
+
async getRunByIdem(idemKey) {
|
|
76
|
+
const taskId = this.byIdem.get(idemKey);
|
|
77
|
+
const r = taskId !== undefined ? this.runs.get(taskId) : undefined;
|
|
78
|
+
return r ? this.toRecord(r) : undefined;
|
|
79
|
+
}
|
|
58
80
|
async requestCancel(taskId, owner) {
|
|
59
81
|
const r = this.runs.get(taskId);
|
|
60
82
|
if (!r || !ownerEq(r.owner, owner) || r.status !== "running")
|
|
@@ -244,6 +266,7 @@ export class MemoryRunStore {
|
|
|
244
266
|
lastRunId: latest.taskId,
|
|
245
267
|
...(typeof latestRunId === "string" && latestRunId !== "" ? { lastTaskRunId: latestRunId } : {}),
|
|
246
268
|
title: null,
|
|
269
|
+
pinned: false,
|
|
247
270
|
});
|
|
248
271
|
}
|
|
249
272
|
return summaries
|
|
@@ -266,6 +289,8 @@ export class MemoryRunStore {
|
|
|
266
289
|
if (r.sessionId !== sessionId)
|
|
267
290
|
continue;
|
|
268
291
|
this.events.delete(taskId);
|
|
292
|
+
if (r.idemKey !== null && this.byIdem.get(r.idemKey) === taskId)
|
|
293
|
+
this.byIdem.delete(r.idemKey);
|
|
269
294
|
this.runs.delete(taskId);
|
|
270
295
|
removed += 1;
|
|
271
296
|
}
|
|
@@ -194,7 +194,7 @@ export const ONE_TIME_MIGRATIONS = [
|
|
|
194
194
|
introducedIn: "7.124.0",
|
|
195
195
|
releasedOn: "2026-10-11",
|
|
196
196
|
removableAfter: "2026-11-10",
|
|
197
|
-
removal: "删 `config-lkg.ts` 的载荷迁移:`migrateLkgPayload` 与其上的窗注、`loadLkg` 里调它的那一段(载荷照旧过 `
|
|
197
|
+
removal: "删 `config-lkg.ts` 的载荷迁移:`migrateLkgPayload` 与其上的窗注、`loadLkg` 里调它的那一段(载荷照旧过 `lkgPayloadJudged`)、`LkgReadMigration.receipts`" +
|
|
198
198
|
"(信封 2 → 3 的 M13 若已先删,`LkgReadMigration` 与 `migrated` 位一并删)、对 `@sema-agent/settings-schema/one-time-migrations` 的 import;`loadLkg` 的版本判据收成" +
|
|
199
199
|
"只认 4(3 回到「unknown formatVersion」拒);`boot/web-admin.ts` 读回后按 4 写回的那一段;文件头注 formatVersion 4 一段里「每次读回都过迁移」那一句;" +
|
|
200
200
|
"`test/ss11-stored-migrations.test.ts` 的 M15 组(4 原样读回那一格留)",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* and branched THERE. Only the table binding lives here.
|
|
13
13
|
*/
|
|
14
14
|
import type { Pool, PoolClient } from "pg";
|
|
15
|
-
import type
|
|
15
|
+
import { type QuotaDecision, type QuotaLimits, type QuotaTracker } from "../observability/cost-quota.js";
|
|
16
16
|
import { type CounterDegradeHook } from "./write-behind-counter.js";
|
|
17
17
|
import { type IndexSpec } from "./ensure-index.js";
|
|
18
18
|
export declare class PgCostQuota implements QuotaTracker {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { decideQuota } from "../observability/cost-quota.js";
|
|
1
2
|
import { PgWriteBehindCounter } from "./write-behind-counter.js";
|
|
2
3
|
import { ensureIndexes } from "./ensure-index.js";
|
|
3
4
|
const COST_QUOTA_TABLE = { table: "cost_quota", keyCol: "scope_key", valCol: "micro_usd" };
|
|
@@ -24,11 +25,7 @@ export class PgCostQuota {
|
|
|
24
25
|
this.counter.stop();
|
|
25
26
|
}
|
|
26
27
|
check(scopeKey) {
|
|
27
|
-
|
|
28
|
-
return { allowed: true, usedMicroUsd: 0, limitMicroUsd: 0, retryAfterSec: 0 };
|
|
29
|
-
const used = this.counter.used(scopeKey);
|
|
30
|
-
const allowed = used < this.limitMicroUsd;
|
|
31
|
-
return { allowed, usedMicroUsd: used, limitMicroUsd: this.limitMicroUsd, retryAfterSec: allowed ? 0 : this.counter.retryAfterSec() };
|
|
28
|
+
return decideQuota(this.limitMicroUsd, this.counter.window(), (bucket) => this.counter.usedIn(scopeKey, bucket));
|
|
32
29
|
}
|
|
33
30
|
add(scopeKey, micro) {
|
|
34
31
|
if (this.limitMicroUsd <= 0)
|
package/dist/plugins/pg-pool.js
CHANGED
|
@@ -42,6 +42,9 @@ export const PG_SCHEMA_STATEMENTS = [
|
|
|
42
42
|
objective_preview VARCHAR(160),
|
|
43
43
|
cancel_requested SMALLINT NOT NULL DEFAULT 0,
|
|
44
44
|
preempt_requested SMALLINT NOT NULL DEFAULT 0,
|
|
45
|
+
-- idem_key: the submission's scoped Idempotency-Key (idemKeyOf, 64 hex); its UNIQUE index uq_task_run_idem is
|
|
46
|
+
-- declared in schema-indexes.ts (PG has no inline KEY). NULL = no key (UNIQUE does not constrain NULL).
|
|
47
|
+
idem_key VARCHAR(64) COLLATE "C",
|
|
45
48
|
created_at TIMESTAMPTZ(3) NOT NULL,
|
|
46
49
|
updated_at TIMESTAMPTZ(3) NOT NULL,
|
|
47
50
|
PRIMARY KEY (task_id)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BaseSessionStorage, type PromptEpochDescriptor, type AcquiredSession, type PlacedSessionRow, type SessionMetadata, type SessionPlacement, type SessionPlacementRecord, type SessionStore, type SessionTreeEntry, type SessionWriteOptions } from "@sema-agent/core";
|
|
2
2
|
import type { Pool, PoolClient } from "pg";
|
|
3
3
|
import type { SessionSummary } from "./store-contracts.js";
|
|
4
|
+
import { type SessionRowPatch } from "../session-row.js";
|
|
4
5
|
import { type PlacementAcquireOpts } from "./session-placement.js";
|
|
5
6
|
import { type StagingHandle } from "../session-sync-kernel.js";
|
|
6
7
|
import { type InstructionSourceBaseline } from "../instruction-source-notice.js";
|
|
@@ -113,9 +114,18 @@ export declare class PgSessionStore implements SessionStore {
|
|
|
113
114
|
limit: number;
|
|
114
115
|
q?: string;
|
|
115
116
|
}): Promise<SessionSummary[]>;
|
|
117
|
+
/** @see TiDBSessionStore.sessionSummaryOf — the PG twin (same projection, run-preview subqueries scoped to the id). */
|
|
118
|
+
sessionSummaryOf(sessionId: string): Promise<SessionSummary | undefined>;
|
|
119
|
+
/** @see TiDBSessionStore.patchSessionMeta — the PG twin (the only writer of a user-set title / pin; the UPDATE and the read-back
|
|
120
|
+
* both conditioned on the verified occupant's incarnation; `updated_at` untouched). The read-back is a second statement, not a
|
|
121
|
+
* `RETURNING` CTE: the outer SELECT of a data-modifying CTE reads the pre-UPDATE snapshot of `session_meta`. */
|
|
122
|
+
patchSessionMeta(sessionId: string, patch: SessionRowPatch, face: SessionFace): Promise<SessionSummary | undefined>;
|
|
123
|
+
/** @see TiDBSessionStore.selectSummaries — the PG twin. `where` already carries `$n` placeholders numbered over `params`;
|
|
124
|
+
* the run scope and the LIMIT take the next numbers (PG binds by number, so text order does not matter). */
|
|
125
|
+
private selectSummaries;
|
|
116
126
|
/** @see TiDBSessionStore.probeTitle — the PG twin. */
|
|
117
127
|
probeTitle(sessionId: string): Promise<"none" | "untitled" | "titled">;
|
|
118
|
-
/** @see TiDBSessionStore.setTitleIfNull — the PG twin (
|
|
128
|
+
/** @see TiDBSessionStore.setTitleIfNull — the PG twin (the auto-titler's NULL-only write). */
|
|
119
129
|
setTitleIfNull(sessionId: string, title: string): Promise<boolean>;
|
|
120
130
|
/** @see TiDBSessionStore.readInstructionSource — the PG twin. */
|
|
121
131
|
readInstructionSource(sessionId: string): Promise<InstructionSourceBaseline>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { emitLeafAdvance } from "../session-leaf-bus.js";
|
|
2
2
|
import { BaseSessionStorage, StoredSession, SessionError, uuidv7, validateEntriesForImport, leafIdAfterEntry, normalizePromptEpoch, } from "@sema-agent/core";
|
|
3
|
+
import { SESSION_TITLE_MAX_CHARS } from "../session-row.js";
|
|
3
4
|
import { escapeLike } from "./sql-escape.js";
|
|
4
5
|
import { decodeSessionEventPayload } from "./session-event-payload.js";
|
|
5
6
|
import { jsonColumnText } from "./sql-json-column.js";
|
|
@@ -25,9 +26,12 @@ export const PG_SESSION_SCHEMA = [
|
|
|
25
26
|
updated_at TIMESTAMPTZ(3) NOT NULL,
|
|
26
27
|
leaf_id VARCHAR(64) COLLATE "C",
|
|
27
28
|
leaf_seq BIGINT NOT NULL DEFAULT 0,
|
|
28
|
-
--
|
|
29
|
-
--
|
|
30
|
-
|
|
29
|
+
-- title: one column, two writers (TiDB twin, same name / width; law in src/session-row.ts): the user's write
|
|
30
|
+
-- (patchSessionMeta, unconditional; null = cleared back to automatic) and the auto-titler (setTitleIfNull / probeTitle
|
|
31
|
+
-- — fills NULL only; NULL = "untitled", the three-state probe distinguishes it from "no such session").
|
|
32
|
+
title VARCHAR(${SESSION_TITLE_MAX_CHARS}) COLLATE "C",
|
|
33
|
+
-- pinned: the user's pin (TiDB twin); every list row carries it, false by default.
|
|
34
|
+
pinned BOOLEAN NOT NULL DEFAULT FALSE,
|
|
31
35
|
-- instruction_source(S-690):换源通告的基线 —— TiDB 孪生同名同宽,三态读法(NULL / '' / 链成员相对路径)见那里的列注。
|
|
32
36
|
instruction_source VARCHAR(64) COLLATE "C",
|
|
33
37
|
-- #292 C18(core 5.48.0 design/323)subagent 转录持久化 —— placement 元组(TiDB 孪生同名同宽,
|
|
@@ -327,14 +331,45 @@ export class PgSessionStore {
|
|
|
327
331
|
const sid = ph(opts.cursor.sessionId);
|
|
328
332
|
where.push(`(sm.updated_at < ${a} OR (sm.updated_at = ${b} AND sm.session_id < ${sid}))`);
|
|
329
333
|
}
|
|
330
|
-
|
|
334
|
+
return this.selectSummaries({ where, params, limit: opts.limit });
|
|
335
|
+
}
|
|
336
|
+
async sessionSummaryOf(sessionId) {
|
|
337
|
+
const rows = await this.selectSummaries({ where: ["sm.placement_kind IS NULL", "sm.session_id = $1"], params: [sessionId], limit: 1, runSessionId: sessionId });
|
|
338
|
+
return rows[0];
|
|
339
|
+
}
|
|
340
|
+
async patchSessionMeta(sessionId, patch, face) {
|
|
341
|
+
const sets = [];
|
|
342
|
+
const params = [];
|
|
343
|
+
if (patch.title !== undefined) {
|
|
344
|
+
params.push(patch.title);
|
|
345
|
+
sets.push(`title = $${params.length}`);
|
|
346
|
+
}
|
|
347
|
+
if (patch.pinned !== undefined) {
|
|
348
|
+
params.push(patch.pinned);
|
|
349
|
+
sets.push(`pinned = $${params.length}`);
|
|
350
|
+
}
|
|
351
|
+
if (sets.length > 0) {
|
|
352
|
+
params.push(sessionId, face.incarnation);
|
|
353
|
+
await this.pool.query(`UPDATE session_meta SET ${sets.join(", ")} WHERE session_id = $${params.length - 1} AND incarnation = $${params.length} AND placement_kind IS NULL`, params);
|
|
354
|
+
}
|
|
355
|
+
const rows = await this.selectSummaries({ where: ["sm.placement_kind IS NULL", "sm.session_id = $1", "sm.incarnation = $2"], params: [sessionId, face.incarnation], limit: 1, runSessionId: sessionId });
|
|
356
|
+
return rows[0];
|
|
357
|
+
}
|
|
358
|
+
async selectSummaries(q) {
|
|
359
|
+
const params = [...q.params];
|
|
360
|
+
const ph = (v) => {
|
|
361
|
+
params.push(v);
|
|
362
|
+
return `$${params.length}`;
|
|
363
|
+
};
|
|
364
|
+
const runWhere = q.runSessionId !== undefined ? ` WHERE session_id = ${ph(q.runSessionId)}` : "";
|
|
365
|
+
const sql = "SELECT sm.session_id, sm.owner, sm.updated_at AS last_activity, sm.created_at AS first_activity, sm.title, sm.pinned, " +
|
|
331
366
|
" COALESCE(r.run_count, 0) AS run_count, latest.objective_preview, latest.status, latest.task_id AS last_run_id, latest.last_task_run_id " +
|
|
332
367
|
"FROM session_meta sm " +
|
|
333
|
-
"LEFT JOIN (SELECT session_id, COUNT(*) AS run_count FROM task_run GROUP BY session_id) r ON r.session_id = sm.session_id " +
|
|
334
|
-
"LEFT JOIN (SELECT DISTINCT ON (session_id) session_id, objective_preview, status, task_id, result ->> 'runId' AS last_task_run_id FROM task_run
|
|
368
|
+
"LEFT JOIN (SELECT session_id, COUNT(*) AS run_count FROM task_run" + runWhere + " GROUP BY session_id) r ON r.session_id = sm.session_id " +
|
|
369
|
+
"LEFT JOIN (SELECT DISTINCT ON (session_id) session_id, objective_preview, status, task_id, result ->> 'runId' AS last_task_run_id FROM task_run" + runWhere +
|
|
335
370
|
" ORDER BY session_id, created_at DESC, task_id DESC) latest ON latest.session_id = sm.session_id " +
|
|
336
|
-
(where.length ? "WHERE " + where.join(" AND ") + " " : "") +
|
|
337
|
-
`ORDER BY sm.updated_at DESC, sm.session_id DESC LIMIT ${ph(
|
|
371
|
+
(q.where.length ? "WHERE " + q.where.join(" AND ") + " " : "") +
|
|
372
|
+
`ORDER BY sm.updated_at DESC, sm.session_id DESC LIMIT ${ph(q.limit)}`;
|
|
338
373
|
const res = await this.pool.query(sql, params);
|
|
339
374
|
return res.rows.map((r) => {
|
|
340
375
|
const runId = r.last_task_run_id;
|
|
@@ -349,6 +384,7 @@ export class PgSessionStore {
|
|
|
349
384
|
lastRunId: r.last_run_id ?? null,
|
|
350
385
|
...(typeof runId === "string" && runId !== "" ? { lastTaskRunId: runId } : {}),
|
|
351
386
|
title: r.title ?? null,
|
|
387
|
+
pinned: r.pinned === true,
|
|
352
388
|
};
|
|
353
389
|
});
|
|
354
390
|
}
|