@sema-agent/server 7.103.2 → 7.105.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 +378 -2
- package/README.md +6 -1
- package/README.zh-CN.md +5 -1
- package/USAGE.md +320 -16
- package/deploy/sema-up/README.md +57 -7
- package/deploy/sema-up/cluster-up.sh +18 -4
- package/deploy/sema-up/kube-up.sh +16 -0
- package/deploy/sema-up/lib/dataroot.sh +142 -0
- package/deploy/sema-up/lib/flagshape.sh +69 -0
- package/deploy/sema-up/lib/lanes.py +14 -4
- package/deploy/sema-up/lib/shell-access.sh +41 -0
- package/deploy/sema-up/preflight.sh +14 -1
- package/deploy/sema-up/sema-up.sh +134 -37
- package/deploy/sema-up/smoke.sh +5 -0
- package/dist/auto-mode-face.js +3 -4
- package/dist/boot/budget-tracing.js +1 -1
- package/dist/boot/config-center.d.ts +6 -2
- package/dist/boot/config-center.js +5 -59
- package/dist/boot/execution-env.d.ts +11 -0
- package/dist/boot/execution-env.js +14 -0
- package/dist/boot/execution-lane.d.ts +25 -0
- package/dist/boot/execution-lane.js +95 -0
- package/dist/boot/memory-consolidation.d.ts +185 -9
- package/dist/boot/memory-consolidation.js +192 -32
- package/dist/boot/resolve-spec.d.ts +2 -0
- package/dist/boot/resolve-spec.js +28 -24
- package/dist/boot/runner-deps.d.ts +6 -2
- package/dist/boot/runner-deps.js +6 -1
- package/dist/boot/session-faces.d.ts +5 -0
- package/dist/boot/session-faces.js +18 -2
- package/dist/boot/session-incarnation-seat.d.ts +41 -0
- package/dist/boot/session-incarnation-seat.js +69 -0
- package/dist/boot/stage-01-config.js +2 -2
- package/dist/boot/stage-02-registry-pull.js +3 -1
- package/dist/boot/stage-04-stores.d.ts +2 -0
- package/dist/boot/stage-04-stores.js +3 -1
- package/dist/boot/stage-05-execution-env.d.ts +4 -0
- package/dist/boot/stage-05-execution-env.js +2 -2
- package/dist/boot/stage-06-runners.d.ts +4 -1
- package/dist/boot/stage-06-runners.js +7 -14
- package/dist/boot/stage-07-capability-layer.d.ts +4 -1
- package/dist/boot/stage-08-reapers.d.ts +4 -1
- package/dist/boot/stage-09-leader.d.ts +6 -1
- package/dist/boot/stage-09-leader.js +2 -1
- package/dist/boot/stage-10-http-server.d.ts +6 -1
- package/dist/boot/stage-10-http-server.js +4 -1
- package/dist/brain.d.ts +1 -1
- package/dist/config-catalog.d.ts +16 -1
- package/dist/config-catalog.js +9 -9
- package/dist/config-center/restart-signal.d.ts +4 -4
- package/dist/config-invariants.d.ts +7 -1
- package/dist/config-invariants.js +37 -6
- package/dist/config-provider.d.ts +42 -10
- package/dist/config-provider.js +18 -16
- package/dist/config-types.d.ts +37 -16
- package/dist/config-types.js +1 -0
- package/dist/config.d.ts +28 -30
- package/dist/config.js +95 -77
- package/dist/env-facts.d.ts +13 -3
- package/dist/env-facts.js +15 -3
- package/dist/execution-lane-caps.d.ts +15 -0
- package/dist/execution-lane-caps.js +8 -0
- package/dist/file-history-capture.d.ts +42 -35
- package/dist/file-history-capture.js +15 -10
- package/dist/file-history-disclosure.d.ts +7 -31
- package/dist/file-history-disclosure.js +12 -0
- package/dist/fleet/fleet-bus.d.ts +5 -1
- package/dist/http/admission.d.ts +12 -5
- package/dist/http/admission.js +5 -1
- package/dist/http/dispatch.d.ts +18 -0
- package/dist/http/dispatch.js +19 -6
- package/dist/http/principal-gate.d.ts +22 -0
- package/dist/http/principal-gate.js +20 -0
- package/dist/http/resume-legs.d.ts +1 -1
- package/dist/http/resume-legs.js +9 -5
- package/dist/http/route-table.d.ts +15 -4
- package/dist/http/route-table.js +6 -5
- package/dist/http/routes/agents-roster.d.ts +7 -0
- package/dist/http/routes/agents-roster.js +6 -2
- package/dist/http/routes/approvals-assistant.d.ts +48 -19
- package/dist/http/routes/approvals-assistant.js +117 -50
- package/dist/http/routes/capabilities.js +3 -2
- package/dist/http/routes/config-catalog.js +1 -0
- package/dist/http/routes/fleet.d.ts +3 -2
- package/dist/http/routes/images.js +0 -1
- package/dist/http/routes/memory-consolidation.d.ts +3 -2
- package/dist/http/routes/memory-policy.js +3 -3
- package/dist/http/routes/rules.js +2 -2
- package/dist/http/routes/runs.js +29 -10
- package/dist/http/routes/session-sync.js +14 -15
- package/dist/http/routes/sessions-list.js +3 -2
- package/dist/http/routes/sessions.js +49 -25
- package/dist/http/routes/shared-memory.js +2 -2
- package/dist/http/routes/side-query.js +8 -1
- package/dist/http/routes/tasks.js +22 -19
- package/dist/http/routes/trace-usage.d.ts +2 -0
- package/dist/http/routes/trace-usage.js +8 -13
- package/dist/http/send.d.ts +12 -0
- package/dist/http/send.js +12 -1
- package/dist/http/server.d.ts +30 -8
- package/dist/http/server.js +11 -5
- package/dist/http/sse-lifecycle.d.ts +1 -1
- package/dist/http/sse-log.d.ts +12 -9
- package/dist/http/sse-log.js +1 -8
- package/dist/http/sync-submit-body.d.ts +43 -0
- package/dist/http/sync-submit-body.js +4 -0
- package/dist/http/wire-types.d.ts +23 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/leader/merge.d.ts +8 -3
- package/dist/memory-incident-sink.js +2 -1
- package/dist/model-select.d.ts +9 -0
- package/dist/model-select.js +5 -1
- package/dist/observability/corrupt-read-seat.d.ts +151 -16
- package/dist/observability/corrupt-read-seat.js +158 -15
- package/dist/observability/fail-open.d.ts +17 -1
- package/dist/observability/fail-open.js +17 -1
- package/dist/observability/metrics.js +1 -1
- package/dist/observability/run-terminal.d.ts +60 -10
- package/dist/observability/run-terminal.js +119 -9
- package/dist/parked-decide.d.ts +8 -3
- package/dist/parked-decide.js +56 -20
- package/dist/parked-revive-surface.d.ts +14 -7
- package/dist/parked-revive-surface.js +5 -1
- package/dist/plugins/approval-ask-store-file.d.ts +4 -1
- package/dist/plugins/approval-ask-store-file.js +9 -12
- package/dist/plugins/approval-ask-store-sql.d.ts +4 -0
- package/dist/plugins/approval-ask-store-sql.js +4 -1
- package/dist/plugins/approval-exemption-store.d.ts +21 -1
- package/dist/plugins/approval-exemption-store.js +36 -7
- package/dist/plugins/approval-nonce-store.d.ts +16 -2
- package/dist/plugins/approval-nonce-store.js +17 -7
- package/dist/plugins/caching-session-store.d.ts +5 -0
- package/dist/plugins/caching-session-store.js +3 -0
- package/dist/plugins/file-resume-anchor-store.d.ts +7 -2
- package/dist/plugins/file-resume-anchor-store.js +19 -5
- package/dist/plugins/file-run-store.d.ts +21 -8
- package/dist/plugins/file-run-store.js +60 -38
- package/dist/plugins/image-bake-store-sql.d.ts +0 -2
- package/dist/plugins/image-bake-store-sql.js +0 -5
- package/dist/plugins/local-session-store.d.ts +22 -0
- package/dist/plugins/local-session-store.js +74 -11
- package/dist/plugins/memory-run-store.d.ts +7 -5
- package/dist/plugins/memory-run-store.js +11 -4
- package/dist/plugins/one-time-migrations.d.ts +38 -1
- package/dist/plugins/one-time-migrations.js +143 -15
- package/dist/plugins/permission-rule-store-file.d.ts +9 -2
- package/dist/plugins/permission-rule-store-file.js +37 -51
- package/dist/plugins/pg-session-storage.d.ts +3 -0
- package/dist/plugins/pg-session-storage.js +19 -10
- package/dist/plugins/retention-store-sql.js +1 -0
- package/dist/plugins/run-store-sql.d.ts +15 -6
- package/dist/plugins/run-store-sql.js +15 -13
- package/dist/plugins/schema-indexes.js +1 -0
- package/dist/plugins/send-file-ledger.d.ts +5 -1
- package/dist/plugins/send-file-ledger.js +11 -6
- package/dist/plugins/session-event-payload.d.ts +14 -4
- package/dist/plugins/session-event-payload.js +5 -2
- package/dist/plugins/session-policy-store-sql.js +18 -5
- package/dist/plugins/store-backend.d.ts +13 -7
- package/dist/plugins/store-backend.js +10 -6
- package/dist/plugins/store-contracts.d.ts +7 -2
- package/dist/plugins/tidb-pool.js +12 -5
- package/dist/plugins/tidb-session-store.d.ts +6 -1
- package/dist/plugins/tidb-session-store.js +17 -10
- package/dist/project-identity.d.ts +4 -6
- package/dist/read-face-posture.d.ts +21 -0
- package/dist/read-face-posture.js +9 -0
- package/dist/request-key-closure.js +1 -1
- package/dist/request-value-closure.d.ts +72 -3
- package/dist/request-value-closure.js +12 -4
- package/dist/roster-bound-http-status.d.ts +6 -0
- package/dist/roster-bound-http-status.js +6 -0
- package/dist/rules-consent.js +0 -11
- package/dist/run-local.js +22 -27
- package/dist/runs.d.ts +22 -2
- package/dist/runs.js +29 -7
- package/dist/sealed-key.d.ts +31 -4
- package/dist/sealed-key.js +9 -0
- package/dist/security.d.ts +5 -1
- package/dist/session-children-retire.d.ts +92 -0
- package/dist/session-children-retire.js +85 -0
- package/dist/session-error-wire.d.ts +58 -0
- package/dist/session-error-wire.js +38 -0
- package/dist/session-incarnation.d.ts +61 -0
- package/dist/session-incarnation.js +43 -0
- package/dist/session-policy-wire.d.ts +98 -12
- package/dist/session-policy-wire.js +71 -8
- package/dist/session-records-skipped-notice.d.ts +20 -0
- package/dist/session-records-skipped-notice.js +27 -0
- package/dist/tool-approval.d.ts +65 -0
- package/dist/tool-approval.js +36 -9
- package/dist/trace/core-keyset-guard.d.ts +8 -7
- package/dist/trace/engine-notice-wire.d.ts +3 -1
- package/dist/trace/engine-notice-wire.js +2 -0
- package/dist/trace/project.d.ts +11 -6
- package/dist/trace/project.js +12 -3
- package/dist/trace/redact.d.ts +7 -0
- package/dist/trace/redact.js +153 -61
- package/dist/trace/wire-projection-faces.d.ts +10 -3
- package/dist/trace/wire-projection-faces.js +8 -2
- package/package.json +2 -2
- package/dist/plugins/memory-session-policy-store.d.ts +0 -46
- package/dist/plugins/memory-session-policy-store.js +0 -52
|
@@ -11,6 +11,7 @@ import { classifySyncRelationshipByIds, SyncConflictError, stagingIdFor, STAGING
|
|
|
11
11
|
import { isPgUniqueViolation } from "./sql-errors.js";
|
|
12
12
|
import { BASELINE_IO_DEADLINE_MS, decodeInstructionSource, encodeInstructionSource } from "../instruction-source-notice.js";
|
|
13
13
|
import { MANAGED_RETENTION } from "./retention-store-sql.js";
|
|
14
|
+
import { SESSION_META_INCARNATION_COLUMN, carriedIncarnation, mintSessionIncarnation, sessionFaceFromRow } from "../session-incarnation.js";
|
|
14
15
|
import { ensureIndexes } from "./ensure-index.js";
|
|
15
16
|
const isDupKey = isPgUniqueViolation;
|
|
16
17
|
function baselineQuery(text, values) {
|
|
@@ -40,6 +41,8 @@ export const PG_SESSION_SCHEMA = [
|
|
|
40
41
|
placement_root_session_id VARCHAR(190) COLLATE "C",
|
|
41
42
|
placement_handle VARCHAR(190) COLLATE "C",
|
|
42
43
|
placed_at TIMESTAMPTZ(3),
|
|
44
|
+
-- S-790 / S-506:会话化身(TiDB 孪生同名同义;列定义唯一属主 = src/session-incarnation.ts 的 SESSION_META_INCARNATION_COLUMN)。
|
|
45
|
+
${SESSION_META_INCARNATION_COLUMN.pg},
|
|
43
46
|
PRIMARY KEY (session_id)
|
|
44
47
|
)`,
|
|
45
48
|
`CREATE TABLE IF NOT EXISTS session_event (
|
|
@@ -217,9 +220,9 @@ export class PgSessionStore {
|
|
|
217
220
|
}
|
|
218
221
|
async register(sessionId, owner, placement) {
|
|
219
222
|
const now = new Date();
|
|
220
|
-
await this.pool.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq, " +
|
|
223
|
+
await this.pool.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq, incarnation, " +
|
|
221
224
|
"placement_kind, placement_scope, placement_parent_session_id, placement_root_session_id, placement_handle, placed_at) " +
|
|
222
|
-
"VALUES ($1,$2,$3,$4,NULL,0,$5,$6,$7,$8,$9,$10) ON CONFLICT (session_id) DO NOTHING", [sessionId, owner, now, now, ...placementInsertValues(placement, now)]);
|
|
225
|
+
"VALUES ($1,$2,$3,$4,NULL,0,$5,$6,$7,$8,$9,$10,$11) ON CONFLICT (session_id) DO NOTHING", [sessionId, owner, now, now, mintSessionIncarnation(), ...placementInsertValues(placement, now)]);
|
|
223
226
|
}
|
|
224
227
|
async placementOf(sessionId) {
|
|
225
228
|
const res = await this.pool.query(`SELECT ${PLACEMENT_SELECT_COLUMNS} FROM session_meta WHERE session_id = $1`, [sessionId]);
|
|
@@ -246,6 +249,12 @@ export class PgSessionStore {
|
|
|
246
249
|
return undefined;
|
|
247
250
|
return res.rows[0].owner ?? null;
|
|
248
251
|
}
|
|
252
|
+
async sessionFaceOf(sessionId) {
|
|
253
|
+
const res = await this.pool.query("SELECT owner, incarnation FROM session_meta WHERE session_id = $1", [sessionId]);
|
|
254
|
+
if (res.rows.length === 0)
|
|
255
|
+
return undefined;
|
|
256
|
+
return sessionFaceFromRow(sessionId, res.rows[0].owner ?? null, res.rows[0].incarnation);
|
|
257
|
+
}
|
|
249
258
|
async touch(sessionId) {
|
|
250
259
|
await this.pool.query("UPDATE session_meta SET updated_at = $1 WHERE session_id = $2", [
|
|
251
260
|
new Date(),
|
|
@@ -384,7 +393,7 @@ export class PgSessionStore {
|
|
|
384
393
|
const payload = String(r.payload);
|
|
385
394
|
await conn.query("INSERT INTO session_event (session_id, seq, entry_id, parent_id, type, payload, ts) VALUES ($1,$2,$3,$4,$5,$6::jsonb,$7)", [newId, seq, r.entry_id, r.parent_id ?? null, r.type, payload, r.ts]);
|
|
386
395
|
}
|
|
387
|
-
await conn.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq) VALUES ($1,$2,$3,$4,$5,$6)", [newId, owner, now, now, leafId, rows.length]);
|
|
396
|
+
await conn.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq, incarnation) VALUES ($1,$2,$3,$4,$5,$6,$7)", [newId, owner, now, now, leafId, rows.length, mintSessionIncarnation()]);
|
|
388
397
|
await conn.query("COMMIT");
|
|
389
398
|
return newId;
|
|
390
399
|
}
|
|
@@ -438,7 +447,7 @@ export class PgSessionStore {
|
|
|
438
447
|
const conn = await this.pool.connect();
|
|
439
448
|
try {
|
|
440
449
|
await conn.query("BEGIN");
|
|
441
|
-
await this.insertValidatedLog(conn, sessionId, owner, validated);
|
|
450
|
+
await this.insertValidatedLog(conn, sessionId, owner, validated, mintSessionIncarnation());
|
|
442
451
|
await conn.query("COMMIT");
|
|
443
452
|
}
|
|
444
453
|
catch (err) {
|
|
@@ -461,14 +470,14 @@ export class PgSessionStore {
|
|
|
461
470
|
const conn = await this.pool.connect();
|
|
462
471
|
try {
|
|
463
472
|
await conn.query("BEGIN");
|
|
464
|
-
const placedRes = await conn.query("SELECT placement_kind FROM session_meta WHERE session_id = $1 FOR UPDATE", [sessionId]);
|
|
473
|
+
const placedRes = await conn.query("SELECT placement_kind, owner, incarnation FROM session_meta WHERE session_id = $1 FOR UPDATE", [sessionId]);
|
|
465
474
|
if (placedRes.rows[0]?.placement_kind != null) {
|
|
466
475
|
await conn.query("ROLLBACK");
|
|
467
476
|
throw new SessionError("placement_refused", `session ${sessionId} belongs to the ${placedRes.rows[0].placement_kind} placement partition — a session-sync replace cannot overwrite a delegation transcript`);
|
|
468
477
|
}
|
|
469
478
|
await conn.query("DELETE FROM session_event WHERE session_id = $1", [sessionId]);
|
|
470
479
|
await conn.query("DELETE FROM session_meta WHERE session_id = $1 AND owner IS NOT DISTINCT FROM $2 AND placement_kind IS NULL", [sessionId, owner]);
|
|
471
|
-
await this.insertValidatedLog(conn, sessionId, owner, validated);
|
|
480
|
+
await this.insertValidatedLog(conn, sessionId, owner, validated, carriedIncarnation(placedRes.rows[0], owner));
|
|
472
481
|
await conn.query("COMMIT");
|
|
473
482
|
}
|
|
474
483
|
catch (err) {
|
|
@@ -485,7 +494,7 @@ export class PgSessionStore {
|
|
|
485
494
|
conn.release();
|
|
486
495
|
}
|
|
487
496
|
}
|
|
488
|
-
async insertValidatedLog(conn, sessionId, owner, validated) {
|
|
497
|
+
async insertValidatedLog(conn, sessionId, owner, validated, incarnation) {
|
|
489
498
|
let leafId = null;
|
|
490
499
|
for (const e of validated)
|
|
491
500
|
leafId = leafIdAfterEntry(e);
|
|
@@ -494,7 +503,7 @@ export class PgSessionStore {
|
|
|
494
503
|
const e = validated[seq];
|
|
495
504
|
await conn.query("INSERT INTO session_event (session_id, seq, entry_id, parent_id, type, payload, ts) VALUES ($1,$2,$3,$4,$5,$6::jsonb,$7)", [sessionId, seq, e.id, e.parentId ?? null, e.type, pgSafeJsonStringify(e), new Date(e.timestamp)]);
|
|
496
505
|
}
|
|
497
|
-
await conn.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq) VALUES ($1,$2,$3,$4,$5,$6)", [sessionId, owner, now, now, leafId, validated.length]);
|
|
506
|
+
await conn.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq, incarnation) VALUES ($1,$2,$3,$4,$5,$6,$7)", [sessionId, owner, now, now, leafId, validated.length, incarnation]);
|
|
498
507
|
}
|
|
499
508
|
beginImportStaging(realSessionId, token) {
|
|
500
509
|
return new PgStagingHandle(this.pool, realSessionId, stagingIdFor(realSessionId, token));
|
|
@@ -614,8 +623,8 @@ class PgStagingHandle {
|
|
|
614
623
|
if (lastRes.rows[0])
|
|
615
624
|
leafId = leafIdAfterEntry(decodeSessionEventPayload(lastRes.rows[0].payload, this.realSessionId, "last staged row"));
|
|
616
625
|
const now = new Date();
|
|
617
|
-
await conn.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq) VALUES ($1,$2,$3,$4,$5,$6) " +
|
|
618
|
-
"ON CONFLICT (session_id) DO UPDATE SET owner = EXCLUDED.owner, updated_at = EXCLUDED.updated_at, leaf_id = EXCLUDED.leaf_id, leaf_seq = EXCLUDED.leaf_seq", [this.realSessionId, owner, now, now, leafId, srcIds.length]);
|
|
626
|
+
await conn.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq, incarnation) VALUES ($1,$2,$3,$4,$5,$6,$7) " +
|
|
627
|
+
"ON CONFLICT (session_id) DO UPDATE SET owner = EXCLUDED.owner, updated_at = EXCLUDED.updated_at, leaf_id = EXCLUDED.leaf_id, leaf_seq = EXCLUDED.leaf_seq", [this.realSessionId, owner, now, now, leafId, srcIds.length, mintSessionIncarnation()]);
|
|
619
628
|
const afterMeta = await conn.query("SELECT placement_kind FROM session_meta WHERE session_id = $1", [this.realSessionId]);
|
|
620
629
|
if (afterMeta.rows[0]?.placement_kind != null) {
|
|
621
630
|
await conn.query("ROLLBACK");
|
|
@@ -380,6 +380,7 @@ export class SqlRetentionStore {
|
|
|
380
380
|
["resume_anchor", "session_id"],
|
|
381
381
|
["approval_exemption", "session_id"],
|
|
382
382
|
["session_policy", "session_id"],
|
|
383
|
+
["session_policy_rev_mark", "session_id"],
|
|
383
384
|
...(this.opts.taskAttachmentTable ? [["task_attachment", "session_id"]] : []),
|
|
384
385
|
["file_history_tracked", "scope"],
|
|
385
386
|
["file_history_version", "scope"],
|
|
@@ -83,10 +83,14 @@ export declare class SqlRunStore {
|
|
|
83
83
|
/**
|
|
84
84
|
* Durable F4 ([ref]): a task suspended on a policy `ask`. Sets the run NON-terminal `suspended` and
|
|
85
85
|
* KEEPS the `task_active` claim — the session stays locked (no new run) until an operator resumes it to a
|
|
86
|
-
* terminal state or the reaper expires it.
|
|
87
|
-
*
|
|
86
|
+
* terminal state or the reaper expires it.
|
|
87
|
+
*
|
|
88
|
+
* 🔴 S-173:park 也落 `result` 列 —— 存的是**摘掉恢复凭据、脱过敏的投影**(`sealRunRowResult`,与 `setTerminal`
|
|
89
|
+
* 同一只入口、同列同序列化),不是引擎交出的原件:`paused.token` / `failed.nestedPause.token` 两座在落列前摘掉,
|
|
90
|
+
* 所以 poll 这一行的调用方(`GET /v1/runs/:id → result`)拿到的是「停在哪扇门上」而拿不到 resume 能力。
|
|
91
|
+
* `result = null` = 这条腿没观察到 park 结果(断连竞态 / 重开 re-park),列写空 —— 与修前行形同。
|
|
88
92
|
*/
|
|
89
|
-
setSuspended(taskId: string): Promise<void>;
|
|
93
|
+
setSuspended(taskId: string, result: PersistedTaskResult | null): Promise<void>;
|
|
90
94
|
/**
|
|
91
95
|
* [ref] D-B: PARK a run that paused on a REVIEW gate — a `needs_review` result (a dry-run diff review OR a
|
|
92
96
|
* pre-action `plan_review`; core assemble-result slot 8.6). Like {@link setSuspended} this is a NON-terminal park
|
|
@@ -104,7 +108,7 @@ export declare class SqlRunStore {
|
|
|
104
108
|
* mocked run-store unit tests stayed green. A mock cannot observe a unique-key claim race, a rolled-back loser,
|
|
105
109
|
* or a multi-table reaper DELETE.
|
|
106
110
|
*/
|
|
107
|
-
setNeedsReview(taskId: string): Promise<void>;
|
|
111
|
+
setNeedsReview(taskId: string, result: PersistedTaskResult | null): Promise<void>;
|
|
108
112
|
/**
|
|
109
113
|
* Durable F4 resume: a suspended run is being actively resumed. Flip it back to `running` (CAS — only if
|
|
110
114
|
* still suspended) and refresh liveness, so the in-flight resume is protected by the running-run heartbeat
|
|
@@ -244,8 +248,9 @@ export declare class SqlRunStore {
|
|
|
244
248
|
rows: UsageRow[];
|
|
245
249
|
truncated: boolean;
|
|
246
250
|
}>;
|
|
247
|
-
/** Earliest retained event seq for a task (0 if none) → `retainedFrom`
|
|
248
|
-
*
|
|
251
|
+
/** Earliest retained event seq for a task (0 if none) → the `retainedFrom` field of `GET /v1/tasks/:id/turns`.
|
|
252
|
+
* Not an eviction boundary: the ledger has no partial head trim (the only deletion takes the whole session tree),
|
|
253
|
+
* so a value > 1 means head rows whose write failed after consuming their seq (S-211 ①). */
|
|
249
254
|
retainedFrom(taskId: string): Promise<number>;
|
|
250
255
|
/** Events with seq strictly greater than `afterSeq` (use 0 for the whole stream). */
|
|
251
256
|
getEvents(taskId: string, afterSeq: number): Promise<RunEvent[]>;
|
|
@@ -288,6 +293,10 @@ export declare class SqlRunStore {
|
|
|
288
293
|
* a NEW pending checkpoint — the `NOT EXISTS pending` clause excludes it. Idempotent across replicas. Returns count.
|
|
289
294
|
*
|
|
290
295
|
* Same alias-qualification divergence as {@link reapSuspended} — see its header note.
|
|
296
|
+
*
|
|
297
|
+
* S-173:两条收割腿都把 `result` 列写回 NULL —— park 行上那份 park 结果(`terminal.kind:"paused"`)说的是
|
|
298
|
+
* 「停在门上」,而这一刻行被打成 `failed`/`approval.expired`;留着它 = 一行两句互相矛盾的话(读面还会从它
|
|
299
|
+
* 投出 `review.pending` 之类的 park 码)。收割后的行形因此与 S-173 之前逐字同(结果空,码在 error_code 列)。
|
|
291
300
|
*/
|
|
292
301
|
failSuspendedWithExpiredCheckpoint(): Promise<number>;
|
|
293
302
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { redactErrorMessage, redactLedgerEventData,
|
|
1
|
+
import { redactErrorMessage, redactLedgerEventData, sealRunRowResult } from "../observability/run-terminal.js";
|
|
2
2
|
import { outwardPlane } from "../terminal.js";
|
|
3
3
|
import { projectUsageStats, USAGE_SCAN_LIMIT } from "../usage-analytics.js";
|
|
4
4
|
import { escapeLike } from "./sql-escape.js";
|
|
@@ -149,7 +149,7 @@ export class SqlRunStore {
|
|
|
149
149
|
return m == null ? 0 : Number(m);
|
|
150
150
|
}
|
|
151
151
|
async setTerminal(taskId, status, result, error) {
|
|
152
|
-
result =
|
|
152
|
+
result = sealRunRowResult(result);
|
|
153
153
|
error = error === null ? null : (redactErrorMessage(error) ?? null);
|
|
154
154
|
const errorCode = result === null ? null : (outwardPlane(result.terminal).errorCode ?? null);
|
|
155
155
|
let won = false;
|
|
@@ -189,14 +189,16 @@ export class SqlRunStore {
|
|
|
189
189
|
});
|
|
190
190
|
}
|
|
191
191
|
}
|
|
192
|
-
async setSuspended(taskId) {
|
|
193
|
-
|
|
192
|
+
async setSuspended(taskId, result) {
|
|
193
|
+
const sealed = sealRunRowResult(result);
|
|
194
|
+
await this.db.query(this.q("UPDATE task_run SET status = 'suspended', result = ?, updated_at = ? WHERE task_id = ? AND status IN ('running','suspended')", "UPDATE task_run SET status = 'suspended', result = $1::jsonb, updated_at = $2 WHERE task_id = $3 AND status IN ('running','suspended')"), [sealed == null ? null : this.json(sealed), new Date(), taskId]);
|
|
194
195
|
}
|
|
195
|
-
async setNeedsReview(taskId) {
|
|
196
|
-
|
|
196
|
+
async setNeedsReview(taskId, result) {
|
|
197
|
+
const sealed = sealRunRowResult(result);
|
|
198
|
+
await this.db.query(this.q("UPDATE task_run SET status = 'needs_review', result = ?, updated_at = ? WHERE task_id = ? AND status IN ('running','needs_review')", "UPDATE task_run SET status = 'needs_review', result = $1::jsonb, updated_at = $2 WHERE task_id = $3 AND status IN ('running','needs_review')"), [sealed == null ? null : this.json(sealed), new Date(), taskId]);
|
|
197
199
|
}
|
|
198
200
|
async markResuming(taskId, instanceId) {
|
|
199
|
-
const { affected } = await this.db.query(this.q("UPDATE task_run SET status = 'running', cancel_requested = 0, preempt_requested = 0, instance_id = ?, updated_at = ? WHERE task_id = ? AND status IN ('suspended','needs_review')", "UPDATE task_run SET status = 'running', cancel_requested = 0, preempt_requested = 0, instance_id = $1, updated_at = $2 WHERE task_id = $3 AND status IN ('suspended','needs_review')"), [instanceId, new Date(), taskId]);
|
|
201
|
+
const { affected } = await this.db.query(this.q("UPDATE task_run SET status = 'running', cancel_requested = 0, preempt_requested = 0, result = NULL, instance_id = ?, updated_at = ? WHERE task_id = ? AND status IN ('suspended','needs_review')", "UPDATE task_run SET status = 'running', cancel_requested = 0, preempt_requested = 0, result = NULL, instance_id = $1, updated_at = $2 WHERE task_id = $3 AND status IN ('suspended','needs_review')"), [instanceId, new Date(), taskId]);
|
|
200
202
|
return affected === 1;
|
|
201
203
|
}
|
|
202
204
|
async getActiveTaskId(sessionId) {
|
|
@@ -421,8 +423,8 @@ export class SqlRunStore {
|
|
|
421
423
|
const cutoff = new Date(Date.now() - olderThanMs);
|
|
422
424
|
let reaped = 0;
|
|
423
425
|
await this.tx(async (conn) => {
|
|
424
|
-
const { affected } = await conn.query(this.q("UPDATE task_run SET status = 'failed', error = 'run stalled (instance lost?)', updated_at = ? " +
|
|
425
|
-
"WHERE status = 'running' AND updated_at < ?", "UPDATE task_run SET status = 'failed', error = 'run stalled (instance lost?)', updated_at = $1 " +
|
|
426
|
+
const { affected } = await conn.query(this.q("UPDATE task_run SET status = 'failed', result = NULL, error = 'run stalled (instance lost?)', updated_at = ? " +
|
|
427
|
+
"WHERE status = 'running' AND updated_at < ?", "UPDATE task_run SET status = 'failed', result = NULL, error = 'run stalled (instance lost?)', updated_at = $1 " +
|
|
426
428
|
"WHERE status = 'running' AND updated_at < $2"), [new Date(), cutoff]);
|
|
427
429
|
reaped = affected;
|
|
428
430
|
await conn.query(this.q("DELETE ta FROM task_active ta JOIN task_run tr ON ta.task_id = tr.task_id WHERE tr.status NOT IN ('running','suspended','needs_review')", "DELETE FROM task_active ta USING task_run tr WHERE ta.task_id = tr.task_id AND tr.status NOT IN ('running','suspended','needs_review')"));
|
|
@@ -434,9 +436,9 @@ export class SqlRunStore {
|
|
|
434
436
|
const cutoff = new Date(Date.now() - olderThanMs);
|
|
435
437
|
let reaped = 0;
|
|
436
438
|
await this.tx(async (conn) => {
|
|
437
|
-
const { affected } = await conn.query(this.q("UPDATE task_run tr SET tr.status = 'failed', tr.error = 'approval expired before decision', tr.error_code = 'approval.expired', tr.updated_at = ? " +
|
|
439
|
+
const { affected } = await conn.query(this.q("UPDATE task_run tr SET tr.status = 'failed', tr.result = NULL, tr.error = 'approval expired before decision', tr.error_code = 'approval.expired', tr.updated_at = ? " +
|
|
438
440
|
"WHERE tr.status = 'suspended' AND tr.updated_at < ? " +
|
|
439
|
-
"AND NOT EXISTS (SELECT 1 FROM checkpoint c WHERE c.session_id = tr.session_id AND c.status = 'pending')", "UPDATE task_run AS tr SET status = 'failed', error = 'approval expired before decision', error_code = 'approval.expired', updated_at = $1 " +
|
|
441
|
+
"AND NOT EXISTS (SELECT 1 FROM checkpoint c WHERE c.session_id = tr.session_id AND c.status = 'pending')", "UPDATE task_run AS tr SET status = 'failed', result = NULL, error = 'approval expired before decision', error_code = 'approval.expired', updated_at = $1 " +
|
|
440
442
|
"WHERE tr.status = 'suspended' AND tr.updated_at < $2 " +
|
|
441
443
|
"AND NOT EXISTS (SELECT 1 FROM checkpoint c WHERE c.session_id = tr.session_id AND c.status = 'pending')"), [new Date(), cutoff]);
|
|
442
444
|
reaped = affected;
|
|
@@ -447,10 +449,10 @@ export class SqlRunStore {
|
|
|
447
449
|
async failSuspendedWithExpiredCheckpoint() {
|
|
448
450
|
let reaped = 0;
|
|
449
451
|
await this.tx(async (conn) => {
|
|
450
|
-
const { affected } = await conn.query(this.q("UPDATE task_run tr SET tr.status = 'failed', tr.error = 'approval expired before decision', tr.error_code = 'approval.expired', tr.updated_at = ? " +
|
|
452
|
+
const { affected } = await conn.query(this.q("UPDATE task_run tr SET tr.status = 'failed', tr.result = NULL, tr.error = 'approval expired before decision', tr.error_code = 'approval.expired', tr.updated_at = ? " +
|
|
451
453
|
"WHERE tr.status IN ('suspended','needs_review') " +
|
|
452
454
|
"AND EXISTS (SELECT 1 FROM checkpoint ce WHERE ce.session_id = tr.session_id AND ce.status = 'expired') " +
|
|
453
|
-
"AND NOT EXISTS (SELECT 1 FROM checkpoint cp WHERE cp.session_id = tr.session_id AND cp.status = 'pending')", "UPDATE task_run AS tr SET status = 'failed', error = 'approval expired before decision', error_code = 'approval.expired', updated_at = $1 " +
|
|
455
|
+
"AND NOT EXISTS (SELECT 1 FROM checkpoint cp WHERE cp.session_id = tr.session_id AND cp.status = 'pending')", "UPDATE task_run AS tr SET status = 'failed', result = NULL, error = 'approval expired before decision', error_code = 'approval.expired', updated_at = $1 " +
|
|
454
456
|
"WHERE tr.status IN ('suspended','needs_review') " +
|
|
455
457
|
"AND EXISTS (SELECT 1 FROM checkpoint ce WHERE ce.session_id = tr.session_id AND ce.status = 'expired') " +
|
|
456
458
|
"AND NOT EXISTS (SELECT 1 FROM checkpoint cp WHERE cp.session_id = tr.session_id AND cp.status = 'pending')"), [new Date()]);
|
|
@@ -10,6 +10,7 @@ export const CENTRAL_SCHEMA_INDEXES = [
|
|
|
10
10
|
{ table: "approval_nonce", name: "idx_approval_nonce_expires", columns: ["expires_at_ms"] },
|
|
11
11
|
{ table: "sendfile_link", name: "idx_sendfile_link_scope", columns: ["scope", "id"] },
|
|
12
12
|
{ table: "session_policy", name: "idx_session_policy_session", columns: ["session_id"] },
|
|
13
|
+
{ table: "session_policy_rev_mark", name: "idx_session_policy_rev_mark_session", columns: ["session_id"] },
|
|
13
14
|
{ table: "snapshot_blob", name: "idx_snapshot_blob_created", columns: ["created_at"] },
|
|
14
15
|
{ table: "file_history_version", name: "idx_file_history_version_blob", columns: ["blob_hash"] },
|
|
15
16
|
{ table: "file_history_boundary_file", name: "idx_file_history_bf_key", columns: ["scope", "key_hash"] },
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Pool as MysqlPool } from "mysql2/promise";
|
|
2
2
|
import type { Pool as PgPool } from "pg";
|
|
3
3
|
import type { IssuedFileLink } from "./send-user-file.js";
|
|
4
|
+
import type { Logger } from "../observability/logger.js";
|
|
4
5
|
/** Ledger scope sentinel for single-user issuance (no principal) — mirrors scopeSegment's "_" key segment. */
|
|
5
6
|
export declare const SENDFILE_SINGLE_USER_SCOPE = "_";
|
|
6
7
|
export interface SendFileLedgerEntry {
|
|
@@ -47,7 +48,10 @@ export declare class FileSendFileLedger implements SendFileLedger {
|
|
|
47
48
|
private readonly path;
|
|
48
49
|
private readonly mem;
|
|
49
50
|
private log;
|
|
50
|
-
|
|
51
|
+
/** @param opts.warn corrupt-read disclosure seat (S-582; absent ⇒ the replay owner falls back to `console.warn`). */
|
|
52
|
+
constructor(root: string, opts?: {
|
|
53
|
+
warn?: Logger["warn"];
|
|
54
|
+
});
|
|
51
55
|
record(entry: SendFileLedgerEntry): Promise<void>;
|
|
52
56
|
listByScope(scope: string, limit: number, beforeId?: string): Promise<SendFileLedgerEntry[]>;
|
|
53
57
|
/** Release the append-log fd (LocalBackend.close → graceful-restart re-open). */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
-
import { AppendLog, ensureDir,
|
|
2
|
+
import { AppendLog, ensureDir, uuidv7 } from "@sema-agent/core";
|
|
3
|
+
import { replayJsonlSkipping } from "../observability/corrupt-read-seat.js";
|
|
3
4
|
export const SENDFILE_SINGLE_USER_SCOPE = "_";
|
|
4
5
|
export class TiDBSendFileLedger {
|
|
5
6
|
pool;
|
|
@@ -68,19 +69,23 @@ export class MemorySendFileLedger {
|
|
|
68
69
|
.map((e) => ({ ...e }));
|
|
69
70
|
}
|
|
70
71
|
}
|
|
72
|
+
const LEDGER_ROW_SHAPE = {
|
|
73
|
+
what: "send-file ledger row without a string id / scope / objectKey",
|
|
74
|
+
read(record) {
|
|
75
|
+
const r = record;
|
|
76
|
+
return typeof r.id === "string" && typeof r.scope === "string" && typeof r.objectKey === "string" ? record : undefined;
|
|
77
|
+
},
|
|
78
|
+
};
|
|
71
79
|
export class FileSendFileLedger {
|
|
72
80
|
path;
|
|
73
81
|
mem = new MemorySendFileLedger();
|
|
74
82
|
log;
|
|
75
|
-
constructor(root) {
|
|
83
|
+
constructor(root, opts) {
|
|
76
84
|
const dir = join(root, "sendfile-ledger");
|
|
77
85
|
ensureDir(dir);
|
|
78
86
|
this.path = join(dir, "links.jsonl");
|
|
79
|
-
for (const r of
|
|
80
|
-
if (!r || typeof r.id !== "string" || typeof r.scope !== "string" || typeof r.objectKey !== "string")
|
|
81
|
-
continue;
|
|
87
|
+
for (const r of replayJsonlSkipping(this.path, "send-file-ledger", LEDGER_ROW_SHAPE, opts?.warn).records)
|
|
82
88
|
void this.mem.record(r);
|
|
83
|
-
}
|
|
84
89
|
}
|
|
85
90
|
async record(entry) {
|
|
86
91
|
this.log ??= new AppendLog(this.path);
|
|
@@ -14,10 +14,20 @@
|
|
|
14
14
|
* 会话事件日志是 **wake 的全部依据**:一行读不出形的 payload 被当成 `null` / 空对象端出去 ⇒ 这只会话的
|
|
15
15
|
* 上下文树里多一个没有 `id`、没有 `type` 的节点,`leafIdAfterEntry` 与 `buildContext` 在它上面的行为未定;
|
|
16
16
|
* 静默跳过它则是**一段对话从历史里消失**(而模型会照着一棵少了一截的树继续说话)。两者都比一次响亮的
|
|
17
|
-
* wake
|
|
18
|
-
*
|
|
17
|
+
* wake 失败坏。
|
|
18
|
+
*
|
|
19
|
+
* ## 拒的**词** = core 的 `SessionError("corrupt_session")`(S-612,server 7.104.0)
|
|
20
|
+
* 写这段代码时 core 的 `SessionError` 闭集里还没有读侧的 corrupt 词(那时这里折成 `StoreCorruptError` / 更早是裸
|
|
21
|
+
* `Error`,点名 `<表>.<列>` + 行标识 —— S-550 车①② 裁定)。core 7.30.0(#1041)给文件会话店铸了
|
|
22
|
+
* `corrupt_session`(「这只会话的存储读不出,无法唤醒」),于是同一件事在三种会话后端上说了两种话:文件后端上 wire
|
|
23
|
+
* 是 500 `corrupt_session`,SQL 后端上是一只未分类的失败。⇒ 改抛**同一个词**;句子只留坐标(`<表>.<列>` + 会话 +
|
|
24
|
+
* 行号 + 坏体种别),不带这一行的内容、不含任何盘上路径(codex r1 [high]:修前的句子带着坏体的前 64 个字符)。失败方向不变:仍然**拒**,不跳过(SQL 行没有「撕裂」这种自然形,坏行 = 被篡改或迁移
|
|
25
|
+
* 事故;文件店「跳过坏记录、其余照回放」的那一半在这里不成立,契约附录里如实写两种后端的差别)。
|
|
26
|
+
* 映射到 wire 的唯一属主是 `src/session-error-wire.ts`。
|
|
27
|
+
* 🔴 `not-text`(读腿没按文本投影 = 本仓的码漂)**不折**:原样上抛 —— 判别与 `foldJsonColumn` 的是同一只谓词
|
|
28
|
+
* `isJsonColumnBodyProblem`,不另写一份。
|
|
19
29
|
*/
|
|
20
|
-
import type
|
|
30
|
+
import { type SessionTreeEntry } from "@sema-agent/core";
|
|
21
31
|
import { type JsonColumnReader } from "./sql-json-column.js";
|
|
22
32
|
/**
|
|
23
33
|
* 形读者:core 自己声明的 `SessionTreeEntryBase` 里那两把**不可省**的串键。
|
|
@@ -32,6 +42,6 @@ import { type JsonColumnReader } from "./sql-json-column.js";
|
|
|
32
42
|
* core 也没有导出 `SessionTreeEntry` 的验证器 —— `validateEntriesForImport` 吃的是已经成形的对象)。
|
|
33
43
|
*/
|
|
34
44
|
export declare const readSessionEventPayload: JsonColumnReader<SessionTreeEntry>;
|
|
35
|
-
/** 一格 `session_event.payload`(已按文本投影)→ 一条事件。坏体 ⇒ 响亮拒(见文件头注)。 */
|
|
45
|
+
/** 一格 `session_event.payload`(已按文本投影)→ 一条事件。坏体 ⇒ 响亮拒 `SessionError("corrupt_session")`(见文件头注)。 */
|
|
36
46
|
export declare function decodeSessionEventPayload(cell: unknown, sessionId: string, rowId: unknown): SessionTreeEntry;
|
|
37
47
|
//# sourceMappingURL=session-event-payload.d.ts.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SessionError } from "@sema-agent/core";
|
|
2
|
+
import { decodeJsonColumn, isJsonColumnBodyProblem } from "./sql-json-column.js";
|
|
2
3
|
export const readSessionEventPayload = (body) => {
|
|
3
4
|
if (typeof body !== "object" || body === null || Array.isArray(body))
|
|
4
5
|
return { problem: "expected a JSON object (a SessionTreeEntry)" };
|
|
@@ -14,7 +15,9 @@ export function decodeSessionEventPayload(cell, sessionId, rowId) {
|
|
|
14
15
|
return decodeJsonColumn(cell, readSessionEventPayload, `session_event.payload for session ${sessionId} row ${String(rowId)}`);
|
|
15
16
|
}
|
|
16
17
|
catch (e) {
|
|
17
|
-
|
|
18
|
+
if (!isJsonColumnBodyProblem(e))
|
|
19
|
+
throw e;
|
|
20
|
+
throw new SessionError("corrupt_session", `session storage: ${e.where} is not a readable session entry (${e.problem}) — refusing rather than wake a session from a tree with a node nobody can read`, e);
|
|
18
21
|
}
|
|
19
22
|
}
|
|
20
23
|
//# sourceMappingURL=session-event-payload.js.map
|
|
@@ -22,18 +22,21 @@ function readRulesBody(text, where) {
|
|
|
22
22
|
throw new SessionPolicyError("corrupt", `${e.message} — an unreadable rules body served as an EMPTY rule set would silently drop every tightening this session carries`);
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
+
function isStoredCounter(n, floor) {
|
|
26
|
+
return Number.isSafeInteger(n) && n >= floor;
|
|
27
|
+
}
|
|
25
28
|
function rowToStored(r, where) {
|
|
26
29
|
const rev = Number(r.rev);
|
|
27
|
-
if (!
|
|
28
|
-
throw new SessionPolicyError("corrupt", `session rules row ${where} carries rev ${String(r.rev)} — a stored row's rev is always ≥ 1 (the stamp is floor+1), and rev 0 is the live reader's synthetic empty seed; refusing rather than serve it as rules`);
|
|
30
|
+
if (!isStoredCounter(rev, 1)) {
|
|
31
|
+
throw new SessionPolicyError("corrupt", `session rules row ${where} carries rev ${String(r.rev)} — a stored row's rev is always a whole number ≥ 1 within the safe-integer range (the stamp is floor+1), and rev 0 is the live reader's synthetic empty seed; refusing rather than serve it as rules`);
|
|
29
32
|
}
|
|
30
33
|
if (r.gen === undefined || r.gen === null) {
|
|
31
34
|
throw new Error(`session_policy row ${where} came back without a \`gen\` column — a read leg did not project it (the column is BIGINT NOT NULL on both dialects). ` +
|
|
32
35
|
"Refusing rather than judge this row's lineage as generation 0, which is a value nothing minted.");
|
|
33
36
|
}
|
|
34
37
|
const gen = Number(r.gen);
|
|
35
|
-
if (!
|
|
36
|
-
throw new SessionPolicyError("corrupt", `session rules row ${where} carries lineage gen ${String(r.gen)} — a generation is a whole number ≥ 0; refusing rather than let a reader judge lineage by a value nothing minted`);
|
|
38
|
+
if (!isStoredCounter(gen, 0)) {
|
|
39
|
+
throw new SessionPolicyError("corrupt", `session rules row ${where} carries lineage gen ${String(r.gen)} — a generation is a whole number ≥ 0 within the safe-integer range; refusing rather than let a reader judge lineage by a value nothing minted`);
|
|
37
40
|
}
|
|
38
41
|
return { ...readRulesBody(r.rules, `session_policy.rules ${where}`), rev, gen };
|
|
39
42
|
}
|
|
@@ -73,7 +76,7 @@ export class SqlSessionPolicyStore {
|
|
|
73
76
|
const { rows: markRows } = await conn.query(this.q("SELECT rev, gen FROM session_policy_rev_mark WHERE policy_key = ? FOR UPDATE", "SELECT rev, gen FROM session_policy_rev_mark WHERE policy_key = $1 FOR UPDATE"), [key]);
|
|
74
77
|
const markRev = markRows[0] ? Number(markRows[0].rev) : 0;
|
|
75
78
|
const markGen = markRows[0] ? Number(markRows[0].gen) : 0;
|
|
76
|
-
if (markRows[0] !== undefined && (!
|
|
79
|
+
if (markRows[0] !== undefined && (!isStoredCounter(markGen, 0) || !isStoredCounter(markRev, 0))) {
|
|
77
80
|
throw new SessionPolicyError("corrupt", `the session rules rev mark for session ${JSON.stringify(sessionId)} is unreadable (rev ${String(markRows[0].rev)}, gen ${String(markRows[0].gen)}) — refusing rather than stamp a rev/lineage nothing proves`);
|
|
78
81
|
}
|
|
79
82
|
const clean = normalizeRules(stripRev(rules));
|
|
@@ -88,6 +91,9 @@ export class SqlSessionPolicyStore {
|
|
|
88
91
|
}
|
|
89
92
|
const nextRev = Math.max(priorRev, markRev) + 1;
|
|
90
93
|
const nextGen = prior?.gen ?? markGen;
|
|
94
|
+
if (!isStoredCounter(nextRev, 1) || !isStoredCounter(nextGen, 0)) {
|
|
95
|
+
throw new SessionPolicyError("corrupt", `session rules for session ${JSON.stringify(sessionId)}: the next stamp (rev ${nextRev}, gen ${nextGen}) would leave the safe-integer range this store reads back — refusing before writing a row nothing could read`);
|
|
96
|
+
}
|
|
91
97
|
const json = JSON.stringify(clean);
|
|
92
98
|
const now = new Date();
|
|
93
99
|
if (prior) {
|
|
@@ -121,6 +127,13 @@ export class SqlSessionPolicyStore {
|
|
|
121
127
|
const now = new Date();
|
|
122
128
|
const keys = rows.map((r) => String(r.policy_key));
|
|
123
129
|
const params = [];
|
|
130
|
+
for (const r of rows) {
|
|
131
|
+
const rev = Number(r.rev);
|
|
132
|
+
const nextGen = Number(r.gen ?? 0) + 1;
|
|
133
|
+
if (!isStoredCounter(rev, 1) || !isStoredCounter(nextGen, 0)) {
|
|
134
|
+
throw new SessionPolicyError("corrupt", `session rules row for session ${JSON.stringify(sessionId)} carries rev ${String(r.rev)} / gen ${String(r.gen)} — the erase would leave a mark outside the safe-integer range; refusing before writing it`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
124
137
|
const tuples = rows.map((r) => {
|
|
125
138
|
params.push(String(r.policy_key), sessionId, Number(r.rev), Number(r.gen ?? 0) + 1, now);
|
|
126
139
|
const base = params.length - 5;
|
|
@@ -44,9 +44,11 @@ export type { ApprovalNonceStore } from "./approval-nonce-store.js";
|
|
|
44
44
|
/** [ref]([ref] v3.1 §3.0-§3.2 流内审批协议持久层):`ApprovalAskStore` 接口 + 行/补丁类型。
|
|
45
45
|
* 车3 刀 3a 起 `StoreBackend.approvalAsk()` 已在(见接口体的那条注)。 */
|
|
46
46
|
export type { ApprovalAskStore, AskRow, NewAskRow, AskTransitionPatch, DecideAskInput, DecideResult, ExpireResult, BindGateInput, BatchRow, AskDecision } from "./approval-ask-store-sql.js";
|
|
47
|
-
/** E6 SessionPolicyStore — core's interface
|
|
48
|
-
*
|
|
49
|
-
*
|
|
47
|
+
/** E6 SessionPolicyStore — core's interface, incl. its optional `deleteBySession` / `listBySession` seams (E21 purge / 2c
|
|
48
|
+
* export; core's File and InMemory stores carry both since core 7.25.0 — S-546 A7 retired the service's own in-memory
|
|
49
|
+
* twin that existed only because they once did not). tidb/pg = the SQL twin; local = core's FileSessionPolicyStore
|
|
50
|
+
* (works local — rules need no cloud-only infra). The rev-line contract kit runs the SQL twin against core's InMemory
|
|
51
|
+
* as oracle (`test/helpers/session-policy-rev-line-contract.ts`). */
|
|
50
52
|
export type ServiceSessionPolicyStore = SessionPolicyStore;
|
|
51
53
|
/** [ref] per-edited-file rewind FileHistoryStore — core's interface PLUS the service extras only the SQL twins
|
|
52
54
|
* carry (optional so the local lane's core `FileFileHistoryStore` still fits). Replaces the retired E19
|
|
@@ -192,7 +194,7 @@ export interface StoreBackend {
|
|
|
192
194
|
* mapping from URLs). REQUIRED on all backends (works local — one JSONL, like approvalExemption). */
|
|
193
195
|
sendFileLedger(): SendFileLedger;
|
|
194
196
|
/** E6 durable SessionPolicyStore (operator-tightened per-session tool rules). REQUIRED on all backends — works local
|
|
195
|
-
* (local = core's
|
|
197
|
+
* (local = core's FileSessionPolicyStore), like resumeAnchor. Backs core's RunnerDeps.sessionPolicyStore. */
|
|
196
198
|
sessionPolicy(): ServiceSessionPolicyStore;
|
|
197
199
|
/** [ref] per-edited-file rewind FileHistoryStore (trackEdit/annulTrack/snapshot/restore over the tracked set;
|
|
198
200
|
* also the 2c history-transfer store). REQUIRED on all backends (local = core's file-backed
|
|
@@ -233,9 +235,13 @@ export interface StoreBackend {
|
|
|
233
235
|
* epoch is gone (its manifest index table retired with it), so EVERY row in that domain is an orphan and its
|
|
234
236
|
* BYTES (incl. MinIO objects) would otherwise outlive the epoch forever. SQL backends only; reaper-driven. */
|
|
235
237
|
legacySnapshotBlobSweep?(): Promise<number>;
|
|
236
|
-
/** 🔴
|
|
237
|
-
*
|
|
238
|
-
*
|
|
238
|
+
/** 🔴 **一次性迁移** —— 落在已写进库里的字节上的退役,升一次级改完、兼容臂当场删干净(硬 breaking 纪律:持久数据面 =
|
|
239
|
+
* 响亮拒 + 一次性迁移,窗过即删,永不长期双读)。逐条语义、幂等与拒启臂见该模块头注。
|
|
240
|
+
*
|
|
241
|
+
* **本成员什么时候删**(嵌入方按这里排期):每条迁移各有窗 —— 引入它的版本发布日 + 30 天,窗关之后的第一个 minor 删那一条
|
|
242
|
+
* 的码;逐条的日期只登记在 `plugins/one-time-migrations.ts` 的 `ONE_TIME_MIGRATIONS`(随包在
|
|
243
|
+
* `dist/plugins/one-time-migrations.d.ts`),起动日志行 `one_time_migrations_applied` 的 `removableAfter` 读数同源。
|
|
244
|
+
* **最后一条**删码时本成员随之从接口上删(硬 breaking:自建 `StoreBackend` 的实现方 tsc 红即通知)。
|
|
239
245
|
*
|
|
240
246
|
* **必选,不是 optional**:漏实装一支后端 = 那条车道的存量字节永远停在旧读法上,而读腿已经只认新的
|
|
241
247
|
* ⇒ 必须是编译期错误,不是运行期的一次静默跳过。 */
|
|
@@ -13,6 +13,7 @@ import { buildStoreOpenRefusalTail } from "./store-open-refusal.js";
|
|
|
13
13
|
import { acquireLocalBackendWithWait } from "./store-lock-wait.js";
|
|
14
14
|
import { NO_SQL_MIGRATIONS, runSqlOneTimeMigrations } from "./one-time-migrations.js";
|
|
15
15
|
import { LocalSessionStore } from "./local-session-store.js";
|
|
16
|
+
import { noteReplayInSessionOpenScope } from "../session-records-skipped-notice.js";
|
|
16
17
|
import { LocalCheckpointStore } from "./local-checkpoint-store.js";
|
|
17
18
|
import { FileResumeAnchorStore } from "./file-resume-anchor-store.js";
|
|
18
19
|
import { TiDBApprovalExemptionStore, PgApprovalExemptionStore, FileApprovalExemptionStore } from "./approval-exemption-store.js";
|
|
@@ -234,17 +235,19 @@ class LocalBackend {
|
|
|
234
235
|
(tail === undefined ? "" : ` ${tail}`));
|
|
235
236
|
}
|
|
236
237
|
this.sessionStore = new LocalSessionStore(new FileSessionRepo(this.fileBackend.root, {
|
|
237
|
-
onCorruptRead: createCorruptReadSeat((msg, fields) => this.discloseWarn(msg, fields), "session_repo_corrupt_read"),
|
|
238
|
+
onCorruptRead: createCorruptReadSeat((msg, fields) => this.discloseWarn(msg, fields), "session_repo_corrupt_read", undefined, noteReplayInSessionOpenScope),
|
|
238
239
|
}), this.fileBackend.root);
|
|
240
|
+
const corruptReadWarn = (msg, fields) => this.discloseWarn(msg, fields);
|
|
239
241
|
this.runStore = new FileRunStore(this.fileBackend.root, {
|
|
240
242
|
error: (msg, fields) => void (this.onError ?? this.onWarn)?.(msg, fields),
|
|
241
243
|
strictHydrate: config.runStoreStrictHydrate,
|
|
244
|
+
warn: corruptReadWarn,
|
|
242
245
|
});
|
|
243
|
-
this.resumeAnchorStore = new FileResumeAnchorStore(this.fileBackend.root);
|
|
244
|
-
this.approvalExemptionStore = new FileApprovalExemptionStore(this.fileBackend.root);
|
|
245
|
-
this.approvalNonceStore = new FileApprovalNonceStore(this.fileBackend.root);
|
|
246
|
-
this.approvalAskStore = new FileApprovalAskStore(this.fileBackend.root);
|
|
247
|
-
this.sendFileLedgerStore = new FileSendFileLedger(this.fileBackend.root);
|
|
246
|
+
this.resumeAnchorStore = new FileResumeAnchorStore(this.fileBackend.root, { warn: corruptReadWarn });
|
|
247
|
+
this.approvalExemptionStore = new FileApprovalExemptionStore(this.fileBackend.root, { warn: corruptReadWarn });
|
|
248
|
+
this.approvalNonceStore = new FileApprovalNonceStore(this.fileBackend.root, { warn: corruptReadWarn });
|
|
249
|
+
this.approvalAskStore = new FileApprovalAskStore(this.fileBackend.root, { warn: corruptReadWarn });
|
|
250
|
+
this.sendFileLedgerStore = new FileSendFileLedger(this.fileBackend.root, { warn: corruptReadWarn });
|
|
248
251
|
this.workflowJournalStore = new FileWorkflowJournalStore(this.fileBackend.root, undefined, {
|
|
249
252
|
onCorruptRead: createCorruptReadSeat((msg, fields) => this.discloseWarn(msg, fields), "workflow_journal_corrupt_read"),
|
|
250
253
|
});
|
|
@@ -280,6 +283,7 @@ class LocalBackend {
|
|
|
280
283
|
if (!this.permissionRuleStoresInst) {
|
|
281
284
|
this.permissionRuleStoresInst = createFilePermissionRuleStores(this.fileBackend.root, {
|
|
282
285
|
onError: (message) => this.onWarn?.("permission_rule_store_disclosure", { message }),
|
|
286
|
+
warn: (msg, fields) => this.discloseWarn(msg, fields),
|
|
283
287
|
});
|
|
284
288
|
}
|
|
285
289
|
return this.permissionRuleStoresInst;
|
|
@@ -185,6 +185,11 @@ export declare const RUN_STATUS_CLASS: {
|
|
|
185
185
|
export type UnsuccessfulRunStatus = {
|
|
186
186
|
[K in RunRecord["status"]]: (typeof RUN_STATUS_CLASS)[K] extends "unsuccessful" ? K : never;
|
|
187
187
|
}[RunRecord["status"]];
|
|
188
|
+
/** 表判为「park」(停在门上)的词 —— **从表派生**,不是手抄的第二份清单。今天 = `"suspended" | "needs_review"`。
|
|
189
|
+
* 消费点(S-173:同步提交 200 体的 poll 回执 `status`,`http/sync-submit-body.ts`)拿它当型;表翻面 ⇒ 那里当场红。 */
|
|
190
|
+
export type ParkedRunStatus = {
|
|
191
|
+
[K in RunRecord["status"]]: (typeof RUN_STATUS_CLASS)[K] extends "parked" ? K : never;
|
|
192
|
+
}[RunRecord["status"]];
|
|
188
193
|
/**
|
|
189
194
|
* 一个状态词的类;**表里没有的词 ⇒ `undefined`**。
|
|
190
195
|
*
|
|
@@ -318,8 +323,8 @@ export interface SessionSummary {
|
|
|
318
323
|
/** K-5c (shell §K): the LATEST run's task_id — the anchor the shell uses to re-attach the live event tail in ONE
|
|
319
324
|
* hop (`GET /v1/runs/:lastRunId/events`) when resuming a picked session, instead of a second lookup. */
|
|
320
325
|
lastRunId: string | null;
|
|
321
|
-
/** [ref]([ref]/[ref]):latest
|
|
322
|
-
* 零迁移路线 —— result
|
|
326
|
+
/** [ref]([ref]/[ref]):latest run 的 engine runId(`TaskResult.runId`,latest.result JSON 提取的
|
|
327
|
+
* 零迁移路线 —— result 在终局与 park 时写、认领续跑即清空(S-173)⇒ 在跑 run 键诚实缺席);OMIT-when-absent,恒不铸 null。轴辨析与
|
|
323
328
|
* wire 半场见 `security.ts SessionListItem.lastTaskRunId`(同名同义)。 */
|
|
324
329
|
lastTaskRunId?: string;
|
|
325
330
|
/** The auto-generated session title (cheap-model one-liner, write-once). null = not (yet)
|
|
@@ -6,6 +6,7 @@ import { TIDB_LEADER_RUN_STATEMENTS } from "./leader-run-store-sql.js";
|
|
|
6
6
|
import { TIDB_ADOPTION_LOG_STATEMENTS } from "./adoption-log-sql.js";
|
|
7
7
|
import { TIDB_PERMISSION_RULE_STATEMENTS } from "./permission-rule-store-sql.js";
|
|
8
8
|
import { TIDB_RETENTION_STATEMENTS } from "./retention-store-sql.js";
|
|
9
|
+
import { SESSION_META_INCARNATION_COLUMN } from "../session-incarnation.js";
|
|
9
10
|
import { ensureIndexes } from "./ensure-index.js";
|
|
10
11
|
import { CENTRAL_SCHEMA_INDEXES } from "./schema-indexes.js";
|
|
11
12
|
import { SESSION_INDEXES } from "./pg-session-storage.js";
|
|
@@ -75,6 +76,11 @@ export const SCHEMA_STATEMENTS = [
|
|
|
75
76
|
placement_root_session_id VARCHAR(190) NULL,
|
|
76
77
|
placement_handle VARCHAR(190) NULL,
|
|
77
78
|
placed_at DATETIME(3) NULL,
|
|
79
|
+
-- S-790 / S-506(design/330 §2):会话化身 —— 同一个会话 id 上**这一位住客**的不透明令牌。铸点只在新建行(register /
|
|
80
|
+
-- 无 id 创建 / fork / 导入新行),删除随行消失,同 id 重登记铸新的;'' = 没铸过,读口响亮拒。列定义的唯一属主 =
|
|
81
|
+
-- src/session-incarnation.ts 的 SESSION_META_INCARNATION_COLUMN(存量库由一次性迁移 M5 用同一串补列 + 回填)。
|
|
82
|
+
-- 不上 wire、不进模型上下文、日志只打前 8 位。
|
|
83
|
+
${SESSION_META_INCARNATION_COLUMN.tidb},
|
|
78
84
|
PRIMARY KEY (session_id),
|
|
79
85
|
KEY idx_session_meta_owner (owner),
|
|
80
86
|
-- listPlaced 的检索腿(kind + 租户 + 年龄升序);普通会话这三列全 NULL ⇒ 不占索引选择性。
|
|
@@ -261,8 +267,8 @@ export const SCHEMA_STATEMENTS = [
|
|
|
261
267
|
bound_input_hash VARCHAR(190) NULL,
|
|
262
268
|
-- 🔴 队列化之前的**单座**列 pending_steer 已**退役删列**:那个席位在引擎侧没了(core 7.17.0 #717 d
|
|
263
269
|
-- 对带席的行 PRE-CAS 拒 seat_retired),留着一列没人读、没人写的字节只会让下一个人以为还有两种读法。
|
|
264
|
-
-- 存量库由启动期的一次性迁移处置(plugins/one-time-migrations.ts
|
|
265
|
-
-- **响亮拒启**并给出两条出路(
|
|
270
|
+
-- 存量库由启动期的一次性迁移处置(plugins/one-time-migrations.ts 的 M2,窗口见该文件登记表):列上还送得出去的格 ⇒
|
|
271
|
+
-- **响亮拒启**并给出两条出路(先在 server 7.73.x 上把那些 run 决掉或取消,让座位送达 / 运维手工清列),
|
|
266
272
|
-- 清空之后 DROP COLUMN。刻意**不**自动把裸单座折进队列:那等于本仓替引擎铸一条它从没写过的队列条目。
|
|
267
273
|
-- pending_steer_queue:core 5.14.0(#147)的**有界有序队列**(PendingSteerEntry[] 的 JSON),现在是本表上
|
|
268
274
|
-- durable steering 的唯一载体。Never touches status/resolve (design/80 inv #4)。
|
|
@@ -381,8 +387,8 @@ export const SCHEMA_STATEMENTS = [
|
|
|
381
387
|
) COLLATE utf8mb4_bin`,
|
|
382
388
|
`CREATE TABLE IF NOT EXISTS session_policy_rev_mark (
|
|
383
389
|
policy_key CHAR(64) NOT NULL,
|
|
384
|
-
--
|
|
385
|
-
--
|
|
390
|
+
-- 被删会话(**明文**会话 id —— 所以它必须随会话一起走,见上)。记号的读写按 policy_key 单键;托管留存的整树
|
|
391
|
+
-- 清扫按本列删(S-546 A9,索引 idx_session_policy_rev_mark_session)。
|
|
386
392
|
session_id VARCHAR(64) NOT NULL,
|
|
387
393
|
-- 抹除时刻该键带过的最高 rev(GREATEST 合并:记号只升不降)。
|
|
388
394
|
rev BIGINT NOT NULL,
|
|
@@ -391,7 +397,8 @@ export const SCHEMA_STATEMENTS = [
|
|
|
391
397
|
-- DEFAULT 0 = 本列到货前落下的记号(那些键的行也都是第 0 代 ⇒ 下一次抹除把它们抬到 1,前后自洽)。
|
|
392
398
|
gen BIGINT NOT NULL DEFAULT 0,
|
|
393
399
|
erased_at DATETIME(3) NOT NULL,
|
|
394
|
-
PRIMARY KEY (policy_key)
|
|
400
|
+
PRIMARY KEY (policy_key),
|
|
401
|
+
KEY idx_session_policy_rev_mark_session (session_id)
|
|
395
402
|
) COLLATE utf8mb4_bin`,
|
|
396
403
|
`CREATE TABLE IF NOT EXISTS snapshot_blob (
|
|
397
404
|
blob_hash CHAR(64) NOT NULL,
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { type AcquiredSession, type PlacedSessionRow, type SessionPlacement, type SessionPlacementRecord, type SessionStore, type SessionTreeEntry } from "@sema-agent/core";
|
|
14
14
|
import type { Pool } from "mysql2/promise";
|
|
15
15
|
import { type InstructionSourceBaseline } from "../instruction-source-notice.js";
|
|
16
|
+
import { type SessionFace } from "../session-incarnation.js";
|
|
16
17
|
import type { SessionSummary } from "./store-contracts.js";
|
|
17
18
|
import { type PlacementAcquireOpts } from "./session-placement.js";
|
|
18
19
|
import { type StagingHandle } from "../session-sync-kernel.js";
|
|
@@ -36,7 +37,8 @@ export declare class TiDBSessionStore implements SessionStore {
|
|
|
36
37
|
private load;
|
|
37
38
|
/** Idempotently create a session_meta row with an owner. Existing owner is never overwritten.
|
|
38
39
|
* [ref]:`placement` 在场时与行同一条 INSERT 落六列;ON DUPLICATE 臂是 no-op ⇒ 已存在的行的元组
|
|
39
|
-
* **永不被重写**(first-write 不可变,义务 5 的写侧半场)。
|
|
40
|
+
* **永不被重写**(first-write 不可变,义务 5 的写侧半场)。
|
|
41
|
+
* S-790:化身与属主同一条 first-write 律 —— 新行铸一只(`mintSessionIncarnation`),已存在的行 no-op 臂一个字节不动。 */
|
|
40
42
|
register(sessionId: string, owner: string | null, placement?: SessionPlacement): Promise<void>;
|
|
41
43
|
/**
|
|
42
44
|
* [ref] 义务 6 的廉价探针 —— 一行 PK SELECT:`undefined` = 普通会话或无此行。
|
|
@@ -57,6 +59,9 @@ export declare class TiDBSessionStore implements SessionStore {
|
|
|
57
59
|
}): Promise<PlacedSessionRow[]>;
|
|
58
60
|
/** Owner principal of a session, `null` if registered ownerless, `undefined` if no such session. */
|
|
59
61
|
ownerOf(sessionId: string): Promise<string | null | undefined>;
|
|
62
|
+
/** S-790:属主 + 化身**一次读**(单行单 SELECT —— 两次独立读在删除 / 重登记的缝里能拼出混世代组合)。`undefined` = 无此会话;
|
|
63
|
+
* 行在而化身为空 ⇒ `SessionIncarnationMissingError`(响亮拒,判据唯一属主 `sessionFaceFromRow`)。 */
|
|
64
|
+
sessionFaceOf(sessionId: string): Promise<SessionFace | undefined>;
|
|
60
65
|
touch(sessionId: string): Promise<void>;
|
|
61
66
|
/** E18 — the session's current leaf `SessionTreeEntry.id` (`session_meta.leaf_id`, set transactionally on every
|
|
62
67
|
* persist). A single SELECT, NO acquire/lock/CAS — safe to call mid-run (write-safety is the append-time CAS, not
|