@sema-agent/server 7.103.2 → 7.104.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 +346 -2
- package/README.md +6 -1
- package/README.zh-CN.md +5 -1
- package/USAGE.md +271 -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 +4 -0
- 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 +13 -28
- package/dist/config.js +53 -70
- 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/http/admission.d.ts +11 -4
- package/dist/http/admission.js +5 -1
- package/dist/http/dispatch.js +3 -3
- package/dist/http/principal-gate.d.ts +22 -0
- package/dist/http/principal-gate.js +20 -0
- package/dist/http/resume-legs.js +9 -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/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 +27 -2
- package/dist/http/routes/session-sync.js +8 -14
- package/dist/http/routes/sessions-list.js +3 -2
- package/dist/http/routes/sessions.js +44 -23
- package/dist/http/routes/shared-memory.js +2 -2
- package/dist/http/routes/tasks.js +22 -19
- package/dist/http/routes/trace-usage.js +4 -4
- package/dist/http/send.d.ts +12 -0
- package/dist/http/send.js +12 -1
- package/dist/http/server.d.ts +21 -4
- package/dist/http/server.js +7 -4
- 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 +12 -0
- package/dist/observability/fail-open.js +12 -0
- package/dist/observability/run-terminal.d.ts +15 -1
- package/dist/observability/run-terminal.js +4 -0
- 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/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/run-store-sql.d.ts +12 -4
- package/dist/plugins/run-store-sql.js +15 -13
- 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/store-backend.d.ts +7 -3
- package/dist/plugins/store-backend.js +10 -6
- package/dist/plugins/store-contracts.d.ts +7 -2
- package/dist/plugins/tidb-pool.js +8 -2
- 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/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/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-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 +1 -1
- package/dist/trace/project.js +9 -0
- package/dist/trace/wire-projection-faces.d.ts +10 -3
- package/dist/trace/wire-projection-faces.js +8 -2
- package/package.json +2 -2
|
@@ -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()]);
|
|
@@ -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
|
|
@@ -233,9 +233,13 @@ export interface StoreBackend {
|
|
|
233
233
|
* epoch is gone (its manifest index table retired with it), so EVERY row in that domain is an orphan and its
|
|
234
234
|
* BYTES (incl. MinIO objects) would otherwise outlive the epoch forever. SQL backends only; reaper-driven. */
|
|
235
235
|
legacySnapshotBlobSweep?(): Promise<number>;
|
|
236
|
-
/** 🔴
|
|
237
|
-
*
|
|
238
|
-
*
|
|
236
|
+
/** 🔴 **一次性迁移** —— 落在已写进库里的字节上的退役,升一次级改完、兼容臂当场删干净(硬 breaking 纪律:持久数据面 =
|
|
237
|
+
* 响亮拒 + 一次性迁移,窗过即删,永不长期双读)。逐条语义、幂等与拒启臂见该模块头注。
|
|
238
|
+
*
|
|
239
|
+
* **本成员什么时候删**(嵌入方按这里排期):每条迁移各有窗 —— 引入它的版本发布日 + 30 天,窗关之后的第一个 minor 删那一条
|
|
240
|
+
* 的码;逐条的日期只登记在 `plugins/one-time-migrations.ts` 的 `ONE_TIME_MIGRATIONS`(随包在
|
|
241
|
+
* `dist/plugins/one-time-migrations.d.ts`),起动日志行 `one_time_migrations_applied` 的 `removableAfter` 读数同源。
|
|
242
|
+
* **最后一条**删码时本成员随之从接口上删(硬 breaking:自建 `StoreBackend` 的实现方 tsc 红即通知)。
|
|
239
243
|
*
|
|
240
244
|
* **必选,不是 optional**:漏实装一支后端 = 那条车道的存量字节永远停在旧读法上,而读腿已经只认新的
|
|
241
245
|
* ⇒ 必须是编译期错误,不是运行期的一次静默跳过。 */
|
|
@@ -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)。
|
|
@@ -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
|
|
@@ -5,6 +5,7 @@ import { jsonColumnText } from "./sql-json-column.js";
|
|
|
5
5
|
import { escapeLike } from "./sql-escape.js";
|
|
6
6
|
import { BASELINE_IO_DEADLINE_MS, decodeInstructionSource, encodeInstructionSource } from "../instruction-source-notice.js";
|
|
7
7
|
import { MANAGED_RETENTION } from "./retention-store-sql.js";
|
|
8
|
+
import { carriedIncarnation, mintSessionIncarnation, sessionFaceFromRow } from "../session-incarnation.js";
|
|
8
9
|
import { contentForkRelation, fastForwardSharedContentDiverged } from "../session-sync-content.js";
|
|
9
10
|
import { assertPlacementAdmission, LIST_PLACED_BATCH, PLACEMENT_SELECT_COLUMNS, placedRowFromRow, placementFromRow, placementInsertValues, SQL_SESSION_PLACEMENTS, } from "./session-placement.js";
|
|
10
11
|
import { classifySyncRelationshipByIds, SyncConflictError, stagingIdFor, STAGING_ID_MARKER, STAGING_GC_GRACE_MS, } from "../session-sync-kernel.js";
|
|
@@ -52,9 +53,9 @@ export class TiDBSessionStore {
|
|
|
52
53
|
}
|
|
53
54
|
async register(sessionId, owner, placement) {
|
|
54
55
|
const now = new Date();
|
|
55
|
-
await this.pool.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq, " +
|
|
56
|
+
await this.pool.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq, incarnation, " +
|
|
56
57
|
"placement_kind, placement_scope, placement_parent_session_id, placement_root_session_id, placement_handle, placed_at) " +
|
|
57
|
-
"VALUES (?,?,?,?,NULL,0
|
|
58
|
+
"VALUES (?,?,?,?,NULL,0,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE session_id = session_id", [sessionId, owner, now, now, mintSessionIncarnation(), ...placementInsertValues(placement, now)]);
|
|
58
59
|
}
|
|
59
60
|
async placementOf(sessionId) {
|
|
60
61
|
const [rows] = await this.pool.query(`SELECT ${PLACEMENT_SELECT_COLUMNS} FROM session_meta WHERE session_id = ?`, [sessionId]);
|
|
@@ -82,6 +83,12 @@ export class TiDBSessionStore {
|
|
|
82
83
|
return undefined;
|
|
83
84
|
return rows[0].owner ?? null;
|
|
84
85
|
}
|
|
86
|
+
async sessionFaceOf(sessionId) {
|
|
87
|
+
const [rows] = await this.pool.query("SELECT owner, incarnation FROM session_meta WHERE session_id = ?", [sessionId]);
|
|
88
|
+
if (rows.length === 0)
|
|
89
|
+
return undefined;
|
|
90
|
+
return sessionFaceFromRow(sessionId, rows[0].owner ?? null, rows[0].incarnation);
|
|
91
|
+
}
|
|
85
92
|
async touch(sessionId) {
|
|
86
93
|
await this.pool.query("UPDATE session_meta SET updated_at = ? WHERE session_id = ?", [
|
|
87
94
|
new Date(),
|
|
@@ -213,7 +220,7 @@ export class TiDBSessionStore {
|
|
|
213
220
|
const payload = String(r.payload);
|
|
214
221
|
await conn.query("INSERT INTO session_event (session_id, seq, entry_id, parent_id, type, payload, ts) VALUES (?,?,?,?,?,?,?)", [newId, seq, r.entry_id, r.parent_id ?? null, r.type, payload, r.ts]);
|
|
215
222
|
}
|
|
216
|
-
await conn.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq) VALUES (
|
|
223
|
+
await conn.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq, incarnation) VALUES (?,?,?,?,?,?,?)", [newId, owner, now, now, leafId, eventRows.length, mintSessionIncarnation()]);
|
|
217
224
|
await conn.commit();
|
|
218
225
|
return newId;
|
|
219
226
|
}
|
|
@@ -267,7 +274,7 @@ export class TiDBSessionStore {
|
|
|
267
274
|
const conn = await this.pool.getConnection();
|
|
268
275
|
try {
|
|
269
276
|
await conn.beginTransaction();
|
|
270
|
-
await this.insertValidatedLog(conn, sessionId, owner, validated);
|
|
277
|
+
await this.insertValidatedLog(conn, sessionId, owner, validated, mintSessionIncarnation());
|
|
271
278
|
await conn.commit();
|
|
272
279
|
}
|
|
273
280
|
catch (err) {
|
|
@@ -290,14 +297,14 @@ export class TiDBSessionStore {
|
|
|
290
297
|
const conn = await this.pool.getConnection();
|
|
291
298
|
try {
|
|
292
299
|
await conn.beginTransaction();
|
|
293
|
-
const [placedRows] = await conn.query("SELECT placement_kind FROM session_meta WHERE session_id = ? FOR UPDATE", [sessionId]);
|
|
300
|
+
const [placedRows] = await conn.query("SELECT placement_kind, owner, incarnation FROM session_meta WHERE session_id = ? FOR UPDATE", [sessionId]);
|
|
294
301
|
if (placedRows[0]?.placement_kind != null) {
|
|
295
302
|
await conn.rollback();
|
|
296
303
|
throw new SessionError("placement_refused", `session ${sessionId} belongs to the ${String(placedRows[0].placement_kind)} placement partition — a session-sync replace cannot overwrite a delegation transcript`);
|
|
297
304
|
}
|
|
298
305
|
await conn.query("DELETE FROM session_event WHERE session_id = ?", [sessionId]);
|
|
299
306
|
await conn.query("DELETE FROM session_meta WHERE session_id = ? AND owner <=> ? AND placement_kind IS NULL", [sessionId, owner]);
|
|
300
|
-
await this.insertValidatedLog(conn, sessionId, owner, validated);
|
|
307
|
+
await this.insertValidatedLog(conn, sessionId, owner, validated, carriedIncarnation(placedRows[0] === undefined ? undefined : { owner: placedRows[0].owner, incarnation: placedRows[0].incarnation }, owner));
|
|
301
308
|
await conn.commit();
|
|
302
309
|
}
|
|
303
310
|
catch (err) {
|
|
@@ -314,7 +321,7 @@ export class TiDBSessionStore {
|
|
|
314
321
|
conn.release();
|
|
315
322
|
}
|
|
316
323
|
}
|
|
317
|
-
async insertValidatedLog(conn, sessionId, owner, validated) {
|
|
324
|
+
async insertValidatedLog(conn, sessionId, owner, validated, incarnation) {
|
|
318
325
|
let leafId = null;
|
|
319
326
|
for (const e of validated)
|
|
320
327
|
leafId = leafIdAfterEntry(e);
|
|
@@ -323,7 +330,7 @@ export class TiDBSessionStore {
|
|
|
323
330
|
const e = validated[seq];
|
|
324
331
|
await conn.query("INSERT INTO session_event (session_id, seq, entry_id, parent_id, type, payload, ts) VALUES (?,?,?,?,?,?,?)", [sessionId, seq, e.id, e.parentId ?? null, e.type, JSON.stringify(e), new Date(e.timestamp)]);
|
|
325
332
|
}
|
|
326
|
-
await conn.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq) VALUES (
|
|
333
|
+
await conn.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq, incarnation) VALUES (?,?,?,?,?,?,?)", [sessionId, owner, now, now, leafId, validated.length, incarnation]);
|
|
327
334
|
}
|
|
328
335
|
beginImportStaging(realSessionId, token) {
|
|
329
336
|
return new TiDBStagingHandle(this.pool, realSessionId, stagingIdFor(realSessionId, token));
|
|
@@ -450,8 +457,8 @@ class TiDBStagingHandle {
|
|
|
450
457
|
if (lastRow[0])
|
|
451
458
|
leafId = leafIdAfterEntry(decodeSessionEventPayload(lastRow[0].payload, this.realSessionId, "last staged row"));
|
|
452
459
|
const now = new Date();
|
|
453
|
-
await conn.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq) VALUES (
|
|
454
|
-
"ON DUPLICATE KEY UPDATE owner = VALUES(owner), updated_at = VALUES(updated_at), leaf_id = VALUES(leaf_id), leaf_seq = VALUES(leaf_seq)", [this.realSessionId, owner, now, now, leafId, srcIds.length]);
|
|
460
|
+
await conn.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq, incarnation) VALUES (?,?,?,?,?,?,?) " +
|
|
461
|
+
"ON DUPLICATE KEY UPDATE owner = VALUES(owner), updated_at = VALUES(updated_at), leaf_id = VALUES(leaf_id), leaf_seq = VALUES(leaf_seq)", [this.realSessionId, owner, now, now, leafId, srcIds.length, mintSessionIncarnation()]);
|
|
455
462
|
const [afterMeta] = await conn.query("SELECT placement_kind FROM session_meta WHERE session_id = ?", [this.realSessionId]);
|
|
456
463
|
if (afterMeta[0]?.placement_kind != null) {
|
|
457
464
|
await conn.rollback();
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
/** core 的项目身份声明席(`TaskSpec.memory.projectKey`)——
|
|
3
|
-
export type MemoryProjectKey
|
|
1
|
+
import type { MemoryProjectKey, MemoryProjectKeyAbsentReason } from "@sema-agent/core";
|
|
2
|
+
/** core 的项目身份声明席(`TaskSpec.memory.projectKey`)—— core 包根导出的型,原样转出(本仓调用方从这里取)。 */
|
|
3
|
+
export type { MemoryProjectKey };
|
|
4
4
|
/** 本仓会说的缺席词。`not_git` 刻意排除:裁 D 起非 git 目录按目录键控,不是缺席(core 闭集仍收它,本仓不铸)。 */
|
|
5
|
-
export type ProjectIdentityAbsentReason = Exclude<
|
|
6
|
-
absent: unknown;
|
|
7
|
-
}>["absent"], "not_git">;
|
|
5
|
+
export type ProjectIdentityAbsentReason = Exclude<MemoryProjectKeyAbsentReason, "not_git">;
|
|
8
6
|
/** 一个任务根的项目身份:canonical 项目根,或说得出理由的缺席。 */
|
|
9
7
|
export type ProjectIdentity = {
|
|
10
8
|
readonly root: string;
|
|
@@ -21,7 +21,7 @@ const TASK_BODY_DECLARED_KEYS = [
|
|
|
21
21
|
"a2aPeers", "acceptPartialRestore", "additionalDirectories", "additionalReadDirectories", "agents",
|
|
22
22
|
"appendSystemPrompt", "approverPosture", "attachmentIds", "attachments", "capabilitiesNeeded", "cascade", "clientContext",
|
|
23
23
|
"compaction", "compactionModel", "council", "cwd", "debate", "deferTools", "deviceId", "enableFork",
|
|
24
|
-
"excludeAllTools", "excludeTools",
|
|
24
|
+
"excludeAllTools", "excludeTools", "fileHistory",
|
|
25
25
|
"finalVerification", "forwardSubagentEvents", "images", "interactiveTools", "jobId", "limits", "maxCostUsd",
|
|
26
26
|
"maxTokens", "mcpServers", "memory", "memoryCapture", "memoryWrite", "model", "objective", "oneShot", "outputRetries",
|
|
27
27
|
"outputSchema", "permissionMode", "projectId", "promptProfile", "reasoningEffort", "requireExistingSession",
|
|
@@ -1,21 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 提交体的**值闭集门** —— `request-key-closure.ts`(键名闭集)的姊妹,同一条纪律的另一半。
|
|
3
|
+
*
|
|
4
|
+
* ## 这里解决的是哪一类病
|
|
5
|
+
*
|
|
6
|
+
* 键名对了、**值**拼错了:`memoryCapture: "OFF"`、`memory: "on"`、`memory: true`、`fileHistory: "off"`。这一族键有一个
|
|
7
|
+
* 共同的形 —— **单成员闭集的关断词**:合法值只有**一个**关断拼法(记忆族是 `"off"`;core 在 `fileHistory` /
|
|
8
|
+
* `checkpointStore` 上的拼法是 `"disabled"`),「开」的拼法**刻意不存在**(缺席就是「照常」的唯一形,声明只能单向收紧)。
|
|
9
|
+
* 于是坏值有且只有一种正确处置:**响亮拒**。按 truthiness 读成任一态都是灾难的一半 ——
|
|
10
|
+
* 一个敲错的隐私/收紧请求被静默丢掉(用户以为关了,其实没关),或者一个敲错的键把功能静默关掉。
|
|
11
|
+
*
|
|
12
|
+
* 🔴 **判别是「值域恰好是一个关断词」,不是「值域恰好是一个字面量」**(S-724 加 `fileHistory` 时定形):
|
|
13
|
+
* `approverPosture` 的值域也只有一个词(`"bypass"`),但它是一句**姿态声明**、闭集属主在 core、本仓刻意**零判据**
|
|
14
|
+
* (`http/wire-types.ts` 那一段逐字成文);把判别放宽成「单字面量」会把它也拖进本表 = 同一语义面第二个判官。
|
|
15
|
+
* 关断词全集 {@link OffSwitchWord} 是 core 的两种关断拼法,新增第三种拼法 = 加一个词(并逐键重判)。
|
|
16
|
+
*
|
|
17
|
+
* ## 为什么是一只判据函数,不是每处一抄
|
|
18
|
+
*
|
|
19
|
+
* 这道门必须落在**两腿**上:fresh 提交的 HTTP admission(400 `request.field_invalid`)与 **resume 重放**
|
|
20
|
+
* 共用的解析路径(`boot/resolve-spec.ts`;从这里逃出的 `HttpError` 被 `http/resume-legs.ts` 折成 409
|
|
21
|
+
* `resume_blocked_by_policy`(带 `blockedBy: e.code`)= 一个 typed 的「续跑被挡」,比静默放行诚实 —— 亲读
|
|
22
|
+
* `resume-legs.ts:776` / `:2366`,**不是**邻处旧注说的 `server.ts`,那句是切片搬家后的陈旧指路)。
|
|
23
|
+
* ⚠️ 本族**刻意不**照抄「fresh fail-loud / resume 宽容降级」那条双层姿势(`settings` / `outputStyle` /
|
|
24
|
+
* `permissionMode` 走的是它):那三处降级的是配置/名单类的**可恢复量**,而把一条收紧声明按打字错误折成缺席是
|
|
25
|
+
* **不可逆方向**的静默降级 —— 一条旧 worker 持久化的 `memoryCapture:'OFF'` 会在 resume 时静默恢复采集。
|
|
26
|
+
*
|
|
27
|
+
* 两腿各抄一份判据 + 一份文案,就是同一条 wire 语义上的**两个写者**;`memoryCapture` 从 [ref] 起正是这么长的
|
|
28
|
+
* (`http/admission.ts` 与 `boot/resolve-spec.ts` 各一份逐字手抄的 150 字文案)。S-405 给这一族添第二个键
|
|
29
|
+
* (`memory`)时把它收成**一张表、一只判据、一处文案属主**:新增一个同形键 = 加一行,而不是四处改。
|
|
30
|
+
* S-724 加第三个键 `fileHistory` 正是这么加的 —— 一行(带它自己的关断词),零处改判据。
|
|
31
|
+
* 文案的冻结职责随之从「桶扫描」搬到 `test/api-error-text-freeze.test.ts` 的常量锚格(与 `RUN_NOT_FOUND_MESSAGE` /
|
|
32
|
+
* `PERSIST_RULE_*` 两格同形、同理由)。
|
|
33
|
+
*
|
|
34
|
+
* ## 边界(与姊妹门同律)
|
|
35
|
+
*
|
|
36
|
+
* · 只判**本表登记的键**的值形;别的值形校验归各自既有的门,本门零抢话。
|
|
37
|
+
* 唯一的跨键句:`fileHistory` 关断词与还原同体({@link FILE_HISTORY_DISABLED_NO_RESTORE}),文案属主随关断词住在这里,
|
|
38
|
+
* 只在 fresh 受理门执行(理由在那一格)。
|
|
39
|
+
* · 只判**值**,不判键名(键名闭集是 `request-key-closure.ts` 的活)。
|
|
40
|
+
* · 体本身非对象 ⇒ 本门不判(留给「missing 'objective' string」那道根形门,免得一个体被两句话抢答)。
|
|
41
|
+
*/
|
|
42
|
+
import type { TaskRequestBody } from "./http/wire-types.js";
|
|
1
43
|
/** [ref] §2.1([ref]):`memoryCapture` 的拼写门文案。**wire 可见文本** —— 改一个字就是对端契约漂移
|
|
2
44
|
* (SDK/壳/文档锚它),字节冻结在 `test/api-error-text-freeze.test.ts` 的常量锚格。 */
|
|
3
45
|
export declare const MEMORY_CAPTURE_SPELLING_MESSAGE = "memoryCapture must be the string 'off' when present (the session-level memory-capture opt-out declaration; absent = capture as usual \u2014 there is no 'on' spelling)";
|
|
4
46
|
/** core [ref] r5 §5 W3(S-405):请求级「记忆整面关」`memory` 的拼写门文案。同上,wire 可见、字节冻结。
|
|
5
47
|
* 文案刻意点明 capture 的分工 —— 两个键都在记忆轴上、形又一模一样,拿错的人要在同一条回执里读到该用哪个。 */
|
|
6
48
|
export declare const MEMORY_OFF_SPELLING_MESSAGE = "memory must be the string 'off' when present (the request-level declaration that this run mounts no memory face at all; absent = memory as usual \u2014 there is no 'on' spelling; to keep memory mounted but stop this session's content from entering long-term memory, send memoryCapture: 'off' instead)";
|
|
49
|
+
/** S-724(core 7.31.0 #1048):请求级「文件历史关断」`fileHistory` 的拼写门文案。同上,wire 可见、字节冻结。
|
|
50
|
+
* 唯一词是 core 的拼法 `'disabled'`(与 `checkpointStore` 同),**不是**记忆族的 `'off'` —— 句子点名它,
|
|
51
|
+
* 顺带说清同一提交里不能再要还原(那一对的拒句是下面的 {@link FILE_HISTORY_DISABLED_NO_RESTORE})。 */
|
|
52
|
+
export declare const FILE_HISTORY_OFF_SPELLING_MESSAGE = "fileHistory must be the string 'disabled' when present (the request-level declaration that this run records no file history \u2014 no backup of an edited file, and no file restore in the same request; absent = the deployment's setting \u2014 there is no 'enabled' spelling)";
|
|
53
|
+
/**
|
|
54
|
+
* S-724 b′(主会话裁,车PW codex r1 [high] 复现后)—— `fileHistory: "disabled"` 与还原**同体**的拒句(`request.field_conflict`)。
|
|
55
|
+
* 与 `tool-approval.ts` 的 `SETTLED_BY_DENY_ONLY` / `READ_ROOT_GRANT_ALLOW_ONLY` **同一族**:「只看体就能判的矛盾,在裁决之前响亮说」。
|
|
56
|
+
* 这一对是调用方自己写出的矛盾(关掉这条 run 的文件历史,又要从文件历史还原);core 也拒(`rewind.capture_disabled`),但它的拒
|
|
57
|
+
* 落在 prepare 的**会话分叉之后**(`prepare-acquire-reconcile` 先按 `resumeAt` 把活动分支移过去,`prepare-file-history` 才判),
|
|
58
|
+
* 且拒后不回滚 —— 拒传不回来。所以本层在任何副作用之前拒。**这不是替 core 再判一次「捕获关了能不能还原」**:判的是体里两个键
|
|
59
|
+
* 能不能共存,不看部署、不看店。
|
|
60
|
+
* 判据:{@link fileHistoryRestoreConflict}。**只在 fresh 受理门**(`http/admission.ts`);续跑腿不过这道门(RESUME 重放不过受理门,
|
|
61
|
+
* 同律 —— 且续跑腿重建时整族剥掉一次性历史动作,矛盾在盘上的体里本就留不下来)。wire 可见、字节冻结。
|
|
62
|
+
*/
|
|
63
|
+
export declare const FILE_HISTORY_DISABLED_NO_RESTORE = "fileHistory: 'disabled' cannot ride with a file restore (restoreFiles, rewindFilesTo, or the retired rewindFiles) \u2014 the request turns this run's file history off and asks to restore files from it in the same breath; the engine would refuse it (rewind.capture_disabled) only after the session had already been branched at resumeAt, which it does not undo, so it is refused here, before any side effect. Drop fileHistory to restore, or drop the restore to run without file history";
|
|
7
64
|
/**
|
|
8
|
-
*
|
|
65
|
+
* 「关断词 ∧ 还原」这一对的判据(纯函数,只看体)。还原 = 三个键里任一个在请求:`restoreFiles: true`、非空串 `rewindFilesTo`
|
|
66
|
+
* (与 `boot/resolve-spec.ts` 的读法同:空串 / 非串按缺席)、退役拼法 `rewindFiles: true`(restore 义要一次还原;capture 义要的
|
|
67
|
+
* 正是这个词关掉的记录 —— 两义都与关断词矛盾)。调用前值形已由上面的单成员表与既有布尔门判过。
|
|
68
|
+
*/
|
|
69
|
+
export declare function fileHistoryRestoreConflict(body: Pick<TaskRequestBody, "fileHistory" | "restoreFiles" | "rewindFiles" | "rewindFilesTo">): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* **单成员闭集键表** —— 每键合法值只有它自己那一个关断词(`word`),缺席=照常,没有「开」的拼法。
|
|
9
72
|
*
|
|
10
|
-
* 🔴 顺序 =
|
|
73
|
+
* 🔴 顺序 = **裁决顺序**,且是**既存键在前**:一个多键都畸形的存量体,判决与加新键之前逐字节相同
|
|
11
74
|
* (新键不许改写既有体的回执)。新增同形键一律**追加在尾部**。
|
|
12
75
|
*/
|
|
13
76
|
declare const SINGLE_MEMBER_OFF_FIELDS: readonly [{
|
|
14
77
|
readonly field: "memoryCapture";
|
|
78
|
+
readonly word: "off";
|
|
15
79
|
readonly message: "memoryCapture must be the string 'off' when present (the session-level memory-capture opt-out declaration; absent = capture as usual — there is no 'on' spelling)";
|
|
16
80
|
}, {
|
|
17
81
|
readonly field: "memory";
|
|
82
|
+
readonly word: "off";
|
|
18
83
|
readonly message: "memory must be the string 'off' when present (the request-level declaration that this run mounts no memory face at all; absent = memory as usual — there is no 'on' spelling; to keep memory mounted but stop this session's content from entering long-term memory, send memoryCapture: 'off' instead)";
|
|
84
|
+
}, {
|
|
85
|
+
readonly field: "fileHistory";
|
|
86
|
+
readonly word: "disabled";
|
|
87
|
+
readonly message: "fileHistory must be the string 'disabled' when present (the request-level declaration that this run records no file history — no backup of an edited file, and no file restore in the same request; absent = the deployment's setting — there is no 'enabled' spelling)";
|
|
19
88
|
}];
|
|
20
89
|
/** 一次值闭集裁决的产物:出问题的键名(机读)+ 一句 wire 文案。 */
|
|
21
90
|
export interface SingleMemberOffFieldIssue {
|
|
@@ -23,7 +92,7 @@ export interface SingleMemberOffFieldIssue {
|
|
|
23
92
|
message: string;
|
|
24
93
|
}
|
|
25
94
|
/**
|
|
26
|
-
*
|
|
95
|
+
* 单成员闭集键的值形裁决:表内任一键在场且不是它自己那一个关断词 ⇒ 返回拒体(调用方 400 `request.field_invalid`);
|
|
27
96
|
* 全部合法或全部缺席 ⇒ `null`。**两腿(fresh admission / resume 重放)共用这一只**。
|
|
28
97
|
*/
|
|
29
98
|
export declare function singleMemberOffFieldIssue(raw: unknown): SingleMemberOffFieldIssue | null;
|
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
export const MEMORY_CAPTURE_SPELLING_MESSAGE = "memoryCapture must be the string 'off' when present (the session-level memory-capture opt-out declaration; absent = capture as usual — there is no 'on' spelling)";
|
|
2
2
|
export const MEMORY_OFF_SPELLING_MESSAGE = "memory must be the string 'off' when present (the request-level declaration that this run mounts no memory face at all; absent = memory as usual — there is no 'on' spelling; to keep memory mounted but stop this session's content from entering long-term memory, send memoryCapture: 'off' instead)";
|
|
3
|
+
export const FILE_HISTORY_OFF_SPELLING_MESSAGE = "fileHistory must be the string 'disabled' when present (the request-level declaration that this run records no file history — no backup of an edited file, and no file restore in the same request; absent = the deployment's setting — there is no 'enabled' spelling)";
|
|
4
|
+
export const FILE_HISTORY_DISABLED_NO_RESTORE = "fileHistory: 'disabled' cannot ride with a file restore (restoreFiles, rewindFilesTo, or the retired rewindFiles) — the request turns this run's file history off and asks to restore files from it in the same breath; the engine would refuse it (rewind.capture_disabled) only after the session had already been branched at resumeAt, which it does not undo, so it is refused here, before any side effect. Drop fileHistory to restore, or drop the restore to run without file history";
|
|
5
|
+
export function fileHistoryRestoreConflict(body) {
|
|
6
|
+
if (body.fileHistory !== "disabled")
|
|
7
|
+
return false;
|
|
8
|
+
return body.restoreFiles === true || body.rewindFiles === true || (typeof body.rewindFilesTo === "string" && body.rewindFilesTo.length > 0);
|
|
9
|
+
}
|
|
3
10
|
const SINGLE_MEMBER_OFF_FIELDS = [
|
|
4
|
-
{ field: "memoryCapture", message: MEMORY_CAPTURE_SPELLING_MESSAGE },
|
|
5
|
-
{ field: "memory", message: MEMORY_OFF_SPELLING_MESSAGE },
|
|
11
|
+
{ field: "memoryCapture", word: "off", message: MEMORY_CAPTURE_SPELLING_MESSAGE },
|
|
12
|
+
{ field: "memory", word: "off", message: MEMORY_OFF_SPELLING_MESSAGE },
|
|
13
|
+
{ field: "fileHistory", word: "disabled", message: FILE_HISTORY_OFF_SPELLING_MESSAGE },
|
|
6
14
|
];
|
|
7
15
|
export function singleMemberOffFieldIssue(raw) {
|
|
8
16
|
if (raw === null || raw === undefined || typeof raw !== "object" || Array.isArray(raw))
|
|
9
17
|
return null;
|
|
10
18
|
const body = raw;
|
|
11
|
-
for (const { field, message } of SINGLE_MEMBER_OFF_FIELDS) {
|
|
19
|
+
for (const { field, word, message } of SINGLE_MEMBER_OFF_FIELDS) {
|
|
12
20
|
const value = body[field];
|
|
13
|
-
if (value !== undefined && value !==
|
|
21
|
+
if (value !== undefined && value !== word)
|
|
14
22
|
return { field, message };
|
|
15
23
|
}
|
|
16
24
|
return null;
|