@sema-agent/server 7.125.1 → 7.126.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 +195 -2
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/USAGE.md +263 -140
- package/deploy/sema-up/README.md +1 -1
- package/deploy/sema-up/chart/templates/networkpolicy-sandbox.yaml +47 -0
- package/deploy/sema-up/chart/templates/sandbox-rbac.yaml +5 -1
- package/deploy/sema-up/chart/templates/server-v4pro.yaml +2 -2
- package/deploy/sema-up/chart/templates/server.yaml +1 -1
- package/deploy/sema-up/chart/values.yaml +12 -0
- package/deploy/sema-up/cluster-up.sh +10 -10
- package/deploy/sema-up/kube-up.sh +11 -10
- package/deploy/sema-up/lib/dataroot.sh +1 -1
- package/deploy/sema-up/lib/envf.sh +5 -5
- package/deploy/sema-up/lib/flagshape.sh +5 -5
- package/deploy/sema-up/lib/lanes.py +1 -1
- package/deploy/sema-up/lib/parse-machines.py +2 -2
- package/deploy/sema-up/lib/shell-access.sh +1 -1
- package/deploy/sema-up/preflight.sh +4 -4
- package/deploy/sema-up/sema-up.sh +33 -33
- package/deploy/sema-up/smoke.sh +3 -3
- package/deploy/sema-up/stack/compose.yaml +1 -1
- package/deploy/sema-up/stack/sema-stack-cluster.yaml +6 -2
- package/dist/approval-card.d.ts +20 -4
- package/dist/approval-content-kind.d.ts +38 -22
- package/dist/approval-content-kind.js +20 -4
- package/dist/audit.js +2 -0
- package/dist/bench/s1/arms.d.ts +9 -37
- package/dist/bench/s1/arms.js +4 -61
- package/dist/bench/s1/live-deps.d.ts +4 -10
- package/dist/bench/s1/live-deps.js +1 -222
- package/dist/bench/s1/oracle.d.ts +1 -1
- package/dist/bench/s1/reviewer.d.ts +1 -1
- package/dist/bench/s1/row.d.ts +5 -7
- package/dist/bench/s1/row.js +0 -2
- package/dist/bench/s1/run-firm.d.ts +2 -2
- package/dist/bench/s1/run-firm.js +1 -4
- package/dist/bench/s1/runner-ctx.d.ts +17 -29
- package/dist/bench/s1/runner-ctx.js +3 -7
- package/dist/bench/s1/tasks.d.ts +9 -46
- package/dist/bench/s1/tasks.js +4 -63
- package/dist/boot/execution-env.d.ts +38 -26
- package/dist/boot/execution-env.js +71 -38
- package/dist/boot/memory-boundary.d.ts +1 -1
- package/dist/boot/parked-revive-gate.d.ts +0 -3
- package/dist/boot/parked-revive-gate.js +2 -2
- package/dist/boot/resolve-spec.d.ts +0 -2
- package/dist/boot/resolve-spec.js +12 -17
- package/dist/boot/retention-lane.d.ts +12 -1
- package/dist/boot/retention-lane.js +2 -0
- package/dist/boot/runner-deps.d.ts +5 -5
- package/dist/boot/runner-deps.js +5 -16
- package/dist/boot/session-faces.d.ts +5 -1
- package/dist/boot/session-faces.js +37 -1
- package/dist/boot/stage-03-store-backend.d.ts +1 -0
- package/dist/boot/stage-03-store-backend.js +2 -2
- package/dist/boot/stage-04-stores.d.ts +1 -0
- package/dist/boot/stage-05-execution-env.d.ts +2 -2
- package/dist/boot/stage-05-execution-env.js +7 -4
- package/dist/boot/stage-06-runners.d.ts +2 -3
- package/dist/boot/stage-06-runners.js +8 -7
- package/dist/boot/stage-07-capability-layer.d.ts +2 -3
- package/dist/boot/stage-07-capability-layer.js +3 -4
- package/dist/boot/stage-08-reapers.d.ts +2 -3
- package/dist/boot/stage-08-reapers.js +2 -2
- package/dist/boot/stage-09-leader.d.ts +2 -3
- package/dist/boot/stage-09-leader.js +2 -1
- package/dist/boot/stage-10-http-server.d.ts +2 -3
- package/dist/boot/stage-10-http-server.js +2 -2
- package/dist/boot/stores.d.ts +1 -0
- package/dist/boot/stores.js +4 -3
- package/dist/boot/web-admin.d.ts +5 -0
- package/dist/boot/web-admin.js +28 -14
- package/dist/brain.d.ts +2 -2
- package/dist/brain.js +1 -1
- package/dist/capabilities/builtin-tools.d.ts +2 -1
- package/dist/capabilities/composite-members.d.ts +1 -1
- package/dist/capabilities/composite-members.js +1 -1
- package/dist/capabilities/hands-lane.d.ts +9 -6
- package/dist/capabilities/hands-lane.js +2 -2
- package/dist/capabilities/memory-notice.d.ts +27 -22
- package/dist/capabilities/memory-notice.js +15 -17
- package/dist/capabilities/scenarios.d.ts +9 -14
- package/dist/capabilities/scenarios.js +5 -50
- package/dist/capability-facts.d.ts +22 -11
- package/dist/capability-facts.js +13 -6
- package/dist/config-catalog.js +11 -4
- package/dist/config-provider.d.ts +5 -1
- package/dist/config-provider.js +23 -4
- package/dist/config-types.d.ts +14 -1
- package/dist/config.js +39 -6
- package/dist/deployment-governance.d.ts +19 -21
- package/dist/deployment-governance.js +7 -8
- package/dist/env-name-allowlist-knobs.d.ts +5 -5
- package/dist/env-name-allowlist-knobs.js +1 -0
- package/dist/execution-lane-caps.js +2 -2
- package/dist/hooks/hook-runner.d.ts +84 -30
- package/dist/hooks/hook-runner.js +74 -41
- package/dist/http/admission.js +12 -9
- package/dist/http/durable-run-leg.d.ts +5 -4
- package/dist/http/durable-run-leg.js +1 -1
- package/dist/http/idempotency.d.ts +28 -6
- package/dist/http/idempotency.js +10 -2
- package/dist/http/principal-gate.d.ts +14 -0
- package/dist/http/principal-gate.js +7 -0
- package/dist/http/request-guards.d.ts +2 -1
- package/dist/http/request-guards.js +14 -14
- package/dist/http/resume-legs.d.ts +2 -2
- package/dist/http/resume-legs.js +5 -4
- package/dist/http/route-ctx.d.ts +8 -6
- package/dist/http/routes/a2a-serve.js +11 -7
- package/dist/http/routes/approvals-assistant.js +18 -3
- package/dist/http/routes/background.d.ts +1 -1
- package/dist/http/routes/background.js +1 -1
- package/dist/http/routes/capabilities.js +10 -2
- package/dist/http/routes/mcp-reconnect.js +1 -1
- package/dist/http/routes/runs.d.ts +4 -3
- package/dist/http/routes/runs.js +12 -13
- package/dist/http/routes/session-sync.js +14 -11
- package/dist/http/routes/sessions.js +9 -9
- package/dist/http/routes/side-query.js +6 -19
- package/dist/http/routes/tasks.js +12 -11
- package/dist/http/server.d.ts +4 -0
- package/dist/http/server.js +2 -2
- package/dist/http/submit-replay.d.ts +15 -8
- package/dist/http/submit-replay.js +6 -0
- package/dist/http/wire-types.d.ts +7 -9
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/internal-run-env.d.ts +30 -0
- package/dist/internal-run-env.js +11 -0
- package/dist/leader/wire.js +3 -2
- package/dist/memory-scope.d.ts +19 -7
- package/dist/memory-scope.js +25 -8
- package/dist/observability/fail-open.d.ts +15 -3
- package/dist/observability/fail-open.js +15 -3
- package/dist/observability/metrics.d.ts +0 -1
- package/dist/observability/metrics.js +0 -3
- package/dist/observability/run-terminal.js +69 -4
- package/dist/permission-rule-vocab.d.ts +3 -2
- package/dist/permission-rule-vocab.js +1 -1
- package/dist/plan-cache-probe.d.ts +3 -2
- package/dist/plugins/approval-ask-store-sql.d.ts +1 -1
- package/dist/plugins/caching-session-store.d.ts +3 -0
- package/dist/plugins/caching-session-store.js +3 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +42 -28
- package/dist/plugins/checkpoint-store-sql.js +57 -11
- package/dist/plugins/file-run-store.d.ts +25 -21
- package/dist/plugins/file-run-store.js +70 -34
- package/dist/plugins/local-checkpoint-store.d.ts +8 -5
- package/dist/plugins/local-checkpoint-store.js +5 -15
- package/dist/plugins/local-session-store.d.ts +66 -7
- package/dist/plugins/local-session-store.js +168 -101
- package/dist/plugins/mailbox-recipient-lifecycle.d.ts +1 -1
- package/dist/plugins/memory-run-store.d.ts +19 -3
- package/dist/plugins/memory-run-store.js +38 -3
- package/dist/plugins/one-time-migrations.d.ts +141 -39
- package/dist/plugins/one-time-migrations.js +32 -143
- package/dist/plugins/pg-pool.js +11 -1
- package/dist/plugins/pg-session-storage.d.ts +2 -0
- package/dist/plugins/pg-session-storage.js +9 -5
- package/dist/plugins/remote-env-adb.d.ts +3 -0
- package/dist/plugins/remote-env-adb.js +1 -0
- package/dist/plugins/remote-env-device.d.ts +3 -0
- package/dist/plugins/remote-env-device.js +1 -0
- package/dist/plugins/remote-env-e2b.d.ts +4 -0
- package/dist/plugins/remote-env-e2b.js +2 -1
- package/dist/plugins/remote-env-host.d.ts +12 -0
- package/dist/plugins/remote-env-host.js +11 -1
- package/dist/plugins/remote-env-k8s.d.ts +227 -39
- package/dist/plugins/remote-env-k8s.js +450 -75
- package/dist/plugins/remote-env-local-docker.d.ts +4 -0
- package/dist/plugins/remote-env-local-docker.js +2 -1
- package/dist/plugins/remote-env-ssh.d.ts +3 -0
- package/dist/plugins/remote-env-ssh.js +1 -0
- package/dist/plugins/retention-store-sql.d.ts +4 -1
- package/dist/plugins/retention-store-sql.js +11 -9
- package/dist/plugins/run-store-sql.d.ts +44 -6
- package/dist/plugins/run-store-sql.js +92 -27
- package/dist/plugins/session-capture-record-store-sql.d.ts +2 -3
- package/dist/plugins/sql-driver.d.ts +2 -1
- package/dist/plugins/store-backend.js +1 -1
- package/dist/plugins/store-contracts.d.ts +17 -3
- package/dist/plugins/tidb-pool.js +15 -3
- package/dist/plugins/tidb-session-storage.js +3 -2
- package/dist/plugins/tidb-session-store.d.ts +2 -0
- package/dist/plugins/tidb-session-store.js +6 -2
- package/dist/request-key-closure.d.ts +12 -0
- package/dist/request-key-closure.js +3 -3
- package/dist/router/route-orchestration.d.ts +3 -11
- package/dist/router/route-orchestration.js +0 -4
- package/dist/rule-entry-list.js +0 -4
- package/dist/rules-consent.js +3 -0
- package/dist/run-local.d.ts +5 -4
- package/dist/run-local.js +23 -17
- package/dist/runs.js +1 -0
- package/dist/security.d.ts +7 -0
- package/dist/security.js +5 -0
- package/dist/server-secret-env.d.ts +10 -4
- package/dist/server-secret-env.js +4 -1
- package/dist/session-incarnation.d.ts +46 -2
- package/dist/session-incarnation.js +14 -3
- package/dist/session-sync-kernel.d.ts +10 -3
- package/dist/spec-fields.d.ts +24 -8
- package/dist/spec-fields.js +21 -2
- package/dist/task-workflow.d.ts +6 -4
- package/dist/task-workflow.js +3 -2
- package/dist/tool-approval.d.ts +78 -6
- package/dist/tool-approval.js +7 -3
- package/dist/trace/core-keyset-guard.d.ts +4 -4
- package/dist/trace/engine-notice-wire.d.ts +26 -1
- package/dist/trace/engine-notice-wire.js +7 -2
- package/dist/trace/project.d.ts +8 -3
- package/dist/trace/project.js +15 -1
- package/dist/trace/self-minted-wire.d.ts +104 -2
- package/dist/trace/self-minted-wire.js +35 -0
- package/dist/web-admin-client/apply-effective.d.ts +13 -7
- package/dist/web-admin-client/apply-effective.js +11 -26
- package/dist/web-admin-client/apply-ledger.d.ts +2 -2
- package/dist/web-admin-client/candidate-gate.d.ts +39 -4
- package/dist/web-admin-client/candidate-gate.js +9 -2
- package/dist/web-admin-client/facade.d.ts +2 -2
- package/dist/web-admin-client/http-client.d.ts +3 -1
- package/dist/web-admin-client/http-client.js +4 -3
- package/dist/web-admin-client/read-warnings.d.ts +33 -5
- package/dist/web-admin-client/read-warnings.js +41 -6
- package/dist/web-admin-client/types.d.ts +11 -5
- package/dist/workspace-cache.d.ts +45 -0
- package/dist/workspace-cache.js +30 -0
- package/dist/workspace-space-commit.d.ts +116 -0
- package/dist/workspace-space-commit.js +117 -0
- package/dist/workspace-space.d.ts +75 -9
- package/dist/workspace-space.js +52 -1
- package/package.json +4 -4
- package/dist/boot/session-incarnation-seat.d.ts +0 -41
- package/dist/boot/session-incarnation-seat.js +0 -69
- package/dist/capabilities/code-review-council.d.ts +0 -49
- package/dist/capabilities/code-review-council.js +0 -111
- package/dist/capabilities/collab-wire.d.ts +0 -36
- package/dist/capabilities/collab-wire.js +0 -2
- package/dist/capabilities/collab-workflows.d.ts +0 -47
- package/dist/capabilities/collab-workflows.js +0 -90
- package/dist/capabilities/prompts/code-review.d.ts +0 -25
- package/dist/capabilities/prompts/code-review.js +0 -39
- package/dist/capabilities/prompts/team.d.ts +0 -10
- package/dist/capabilities/prompts/team.js +0 -6
- package/dist/capabilities/team.d.ts +0 -113
- package/dist/capabilities/team.js +0 -70
|
@@ -73,11 +73,15 @@ export declare class RemoteLocalDockerExecutionEnv implements RemoteExecutionEnv
|
|
|
73
73
|
readonly capabilities: {
|
|
74
74
|
isolation: boolean;
|
|
75
75
|
suspendable: boolean;
|
|
76
|
+
canonicalPathAuthoritative: boolean;
|
|
76
77
|
};
|
|
77
78
|
/** Working directory inside the container; relative paths resolve against it (ExecutionEnv contract). */
|
|
78
79
|
cwd: string;
|
|
79
80
|
/** core `ExecutionEnv.homeDir`([ref]):本沙箱/目标机上执行用户的 home。见配置同名字段。 */
|
|
80
81
|
readonly homeDir?: string | undefined;
|
|
82
|
+
/** core 7.49.0 #1554:shell 子进程真拿到的 `CDPATH` 条目 —— 本车道的命令跑在容器里(镜像的 env),本类不读那份 env ⇒ `"unknown"`(core 契约:说不出就说 unknown,
|
|
83
|
+
* 不许报一张没读过的空表;删除守卫因此对裸 `cd <相对名>` 之后的删除问人,而不是按「没有 CDPATH」放行)。 */
|
|
84
|
+
readonly cdPath: "unknown";
|
|
81
85
|
private readonly cfg;
|
|
82
86
|
private readonly spawnFn;
|
|
83
87
|
/** Stable container NAME (also our handle id). Created on connect; targeted by every docker exec/cp/rm. */
|
|
@@ -18,9 +18,10 @@ const unsupported = (op) => ({
|
|
|
18
18
|
error: new RemoteExecutionError("unsupported", `${op} is not supported on a local-docker target (v1: a container is an isolation boundary but not a managed memory snapshot; capabilities.suspendable=false)`),
|
|
19
19
|
});
|
|
20
20
|
export class RemoteLocalDockerExecutionEnv {
|
|
21
|
-
capabilities = { isolation: true, suspendable: false };
|
|
21
|
+
capabilities = { isolation: true, suspendable: false, canonicalPathAuthoritative: true };
|
|
22
22
|
cwd;
|
|
23
23
|
homeDir;
|
|
24
|
+
cdPath = "unknown";
|
|
24
25
|
cfg;
|
|
25
26
|
spawnFn;
|
|
26
27
|
containerName;
|
|
@@ -94,6 +94,9 @@ export declare class RemoteSshExecutionEnv implements RemoteExecutionEnv {
|
|
|
94
94
|
private inFlight?;
|
|
95
95
|
/** core `ExecutionEnv.homeDir`([ref]):本沙箱/目标机上执行用户的 home。见配置同名字段。 */
|
|
96
96
|
readonly homeDir?: string | undefined;
|
|
97
|
+
/** core 7.49.0 #1554:shell 子进程真拿到的 `CDPATH` 条目 —— 本车道的命令跑在远端登录 shell 里(目标机的 profile / env),本类不读那份 env ⇒ `"unknown"`(core 契约:说不出就说 unknown,
|
|
98
|
+
* 不许报一张没读过的空表;删除守卫因此对裸 `cd <相对名>` 之后的删除问人,而不是按「没有 CDPATH」放行)。 */
|
|
99
|
+
readonly cdPath: "unknown";
|
|
97
100
|
private readonly cfg;
|
|
98
101
|
private readonly injectedConnectOnce?;
|
|
99
102
|
constructor(config: SshEnvConfig, deps?: SshEnvDeps);
|
|
@@ -50,6 +50,7 @@ import type { Pool as MySqlPool } from "mysql2/promise";
|
|
|
50
50
|
import type { Pool as PgPool } from "pg";
|
|
51
51
|
import { type SqlDriver, type SqlTxConn } from "./sql-driver.js";
|
|
52
52
|
import { type IndexSpec } from "./ensure-index.js";
|
|
53
|
+
import type { EndedSpace } from "../workspace-space.js";
|
|
53
54
|
/**
|
|
54
55
|
* 三只**被托管**的 SQL 店(session / checkpoint / tool-result)共用的 `retention` 声明常量。
|
|
55
56
|
*
|
|
@@ -419,7 +420,9 @@ export declare class SqlRetentionStore implements ManagedRetentionCapability {
|
|
|
419
420
|
* ——后者跨十余张表、随部署形态漂(附件表可能不在场),做不成稳定判据;而「幂等重跑 ⇒ 0」这条契约在
|
|
420
421
|
* 两种口径下同真。`tombstones` = 本次**新落地**的墓碑数(重跑时既有墓碑不重复计)。
|
|
421
422
|
*/
|
|
422
|
-
deleteExpiredSessions(input: RetentionCallInput): Promise<RetentionReceipt
|
|
423
|
+
deleteExpiredSessions(input: RetentionCallInput): Promise<RetentionReceipt & {
|
|
424
|
+
readonly endedSpaces?: readonly EndedSpace[];
|
|
425
|
+
}>;
|
|
423
426
|
/**
|
|
424
427
|
* E21 顺序契约的**事务化搬入**:同一组 DELETE、同一个顺序,一次性发在一条事务连接上。
|
|
425
428
|
* 顺序的两条承重理由(其余是可读性):
|
|
@@ -68,7 +68,7 @@ export const TIDB_RETENTION_STATEMENTS = [
|
|
|
68
68
|
row_key VARCHAR(518) NOT NULL,
|
|
69
69
|
deleted_at_ms BIGINT NOT NULL,
|
|
70
70
|
-- 🔴 主键**带 domain**(codex 对抗复审 R3-[high],验真后改):行键是 sessionId / tool_result.ref,而
|
|
71
|
-
-- 会话 id
|
|
71
|
+
-- 会话 id 是**调用方提供**的(7.125.x 及更早任意 ≤64 字符串,7.126.0 起 canonical uuid —— 调用方仍可挑任意 uuid)
|
|
72
72
|
-- ⇒ 两个租户先后用同一个 id 完全合法。旧形 PK(kind,row_key) 全局唯一,后果有两条,都不轻:
|
|
73
73
|
-- ① 租户 B 删同名会话时墓碑 INSERT IGNORE 空转 ⇒ 回执 deleted=1 而 tombstones=0(账对不上),
|
|
74
74
|
-- 且 B 的域从此**枚举不到**它的孤儿(F4 的病灶原样复发);
|
|
@@ -222,7 +222,7 @@ export class SqlRetentionStore {
|
|
|
222
222
|
async previewRetention(input) {
|
|
223
223
|
const [checkpoints, sessions, toolResults] = await Promise.all([
|
|
224
224
|
this.expiryCandidates(this.db, input).then((tokens) => tokens.length),
|
|
225
|
-
this.sessionCandidates(this.db, input).then((
|
|
225
|
+
this.sessionCandidates(this.db, input).then((rows) => rows.length),
|
|
226
226
|
this.countOrphanToolResultCandidates(this.db, input),
|
|
227
227
|
]);
|
|
228
228
|
return { checkpoints, sessions, toolResults };
|
|
@@ -299,10 +299,10 @@ export class SqlRetentionStore {
|
|
|
299
299
|
async sessionCandidates(exec, input, forUpdate = false) {
|
|
300
300
|
const w = this.domainWhere("sm.owner", input.domain, 1);
|
|
301
301
|
const suffix = forUpdate ? " FOR UPDATE" : "";
|
|
302
|
-
const { rows } = await exec.query(this.q(`SELECT sm.session_id FROM session_meta sm WHERE ${w.sql} AND sm.updated_at < ? AND NOT ${this.liveReferenceExists()} ` +
|
|
303
|
-
`ORDER BY sm.updated_at ASC LIMIT ?${suffix}`, `SELECT sm.session_id FROM session_meta sm WHERE ${w.sql} AND sm.updated_at < $2 AND NOT ${this.liveReferenceExists()} ` +
|
|
302
|
+
const { rows } = await exec.query(this.q(`SELECT sm.session_id, sm.incarnation FROM session_meta sm WHERE ${w.sql} AND sm.updated_at < ? AND NOT ${this.liveReferenceExists()} ` +
|
|
303
|
+
`ORDER BY sm.updated_at ASC LIMIT ?${suffix}`, `SELECT sm.session_id, sm.incarnation FROM session_meta sm WHERE ${w.sql} AND sm.updated_at < $2 AND NOT ${this.liveReferenceExists()} ` +
|
|
304
304
|
`ORDER BY sm.updated_at ASC LIMIT $3${suffix}`), [...w.params, new Date(input.cutoffMs), RETENTION_SESSION_BATCH]);
|
|
305
|
-
return rows.map((r) => String(r.session_id));
|
|
305
|
+
return rows.map((r) => ({ id: String(r.session_id), incarnation: typeof r.incarnation === "string" ? r.incarnation : "" }));
|
|
306
306
|
}
|
|
307
307
|
async countOrphanToolResultCandidates(exec, input) {
|
|
308
308
|
return (await this.orphanToolResultCandidates(exec, input)).length;
|
|
@@ -331,12 +331,14 @@ export class SqlRetentionStore {
|
|
|
331
331
|
skippedOnAbort = doomed.length + skipped;
|
|
332
332
|
let deleted = 0;
|
|
333
333
|
let tombstones = 0;
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
334
|
+
const ids = doomed.map((d) => d.id);
|
|
335
|
+
if (ids.length > 0) {
|
|
336
|
+
deleted = await this.purgeSessionTrees(conn, input.domain, ids);
|
|
337
|
+
tombstones = await this.writeTombstones(conn, input.domain, "session", ids);
|
|
337
338
|
}
|
|
338
339
|
await this.writeAuditRow(conn, { domain: input.domain, action: "deleted_sessions", audit, deleted, skipped, tombstones });
|
|
339
|
-
|
|
340
|
+
const endedSpaces = doomed.filter((d) => d.incarnation !== "").map((d) => ({ sessionId: d.id, spaceKey: d.incarnation }));
|
|
341
|
+
return { domain: input.domain, deleted, skipped, tombstones, ...(endedSpaces.length > 0 ? { endedSpaces } : {}) };
|
|
340
342
|
});
|
|
341
343
|
}
|
|
342
344
|
catch (err) {
|
|
@@ -2,9 +2,10 @@ import type { TaskStatus } from "@sema-agent/core";
|
|
|
2
2
|
import type { Pool as MySqlPool } from "mysql2/promise";
|
|
3
3
|
import type { Pool as PgPool } from "pg";
|
|
4
4
|
import { type UsageRow } from "../usage-analytics.js";
|
|
5
|
-
import { type CreateRunOutcome, type NewRunMeta, type RunRecord, type SessionSummary, type RunEvent, type PersistedTaskResult, type LatestRunRef } from "./store-contracts.js";
|
|
5
|
+
import { type CreateRunOutcome, type NewRunMeta, type RunIdemBinding, type RunRecord, type SessionSummary, type RunEvent, type PersistedTaskResult, type LatestRunRef } from "./store-contracts.js";
|
|
6
6
|
import { type SqlDriver } from "./sql-driver.js";
|
|
7
7
|
import type { LedgerEventType } from "../trace/ledger-events.js";
|
|
8
|
+
import { type SpaceCommitClaim, type SpaceCommitReading, type SpaceCommitState } from "../workspace-space-commit.js";
|
|
8
9
|
export type { RunRecord, SessionSummary, RunEvent, LatestRunRef } from "./store-contracts.js";
|
|
9
10
|
export declare class SqlRunStore {
|
|
10
11
|
protected readonly db: SqlDriver;
|
|
@@ -30,7 +31,8 @@ export declare class SqlRunStore {
|
|
|
30
31
|
*
|
|
31
32
|
* Submission idempotency rides the SAME mechanism on the row itself: `task_run` carries two unique keys a
|
|
32
33
|
* submission can name — `task_id` (a caller-minted `body.taskId`) and `idem_key` (the scoped
|
|
33
|
-
* `Idempotency-Key`; NULL = none, which neither dialect's UNIQUE constrains)
|
|
34
|
+
* `Idempotency-Key`; NULL = none, which neither dialect's UNIQUE constrains) — and, beside `idem_key`, the fingerprint
|
|
35
|
+
* of the request that bound it (`idem_body_fingerprint`; compared by the submit surface, never here). Claim + row are ONE
|
|
34
36
|
* transaction, so a concurrent same-key submit on another replica either commits first (we collide on the
|
|
35
37
|
* unique index — after its commit, PG/InnoDB/TiDB-pessimistic block on the uncommitted entry; TiDB-optimistic
|
|
36
38
|
* reports it at COMMIT, which is why the commit sits inside the same classification) or loses to us.
|
|
@@ -84,6 +86,35 @@ export declare class SqlRunStore {
|
|
|
84
86
|
* blocked/timeout terminals — a first write of those could still be overwritten. The claim DELETE stays
|
|
85
87
|
* unconditional (idempotent; the first writer already released it). */
|
|
86
88
|
setTerminal(taskId: string, status: TaskStatus, result: PersistedTaskResult | null, error: string | null): Promise<void>;
|
|
89
|
+
/** The space's row, or `undefined`. */
|
|
90
|
+
readSpaceCommit(spaceKey: string): Promise<SpaceCommitReading | undefined>;
|
|
91
|
+
/** Make the space's row this run's (its own transaction; the terminal write runs the same step inside its own). */
|
|
92
|
+
claimSpaceCommit(spaceKey: string, taskId: string, state: SpaceCommitState, claim: SpaceCommitClaim): Promise<boolean>;
|
|
93
|
+
/** Delete this run's row (never another run's). */
|
|
94
|
+
endSpaceCommit(spaceKey: string, taskId: string): Promise<void>;
|
|
95
|
+
/** Abandon run `taskId`'s commit (the reader's write, `workspace-space-commit.ts`): one transaction — lock the row, the
|
|
96
|
+
* clock read under that lock, the shared rule, then a DELETE of that run's row. A writer's renewal in flight holds the
|
|
97
|
+
* same lock, so the abandon judges the row it committed. */
|
|
98
|
+
abandonSpaceCommit(spaceKey: string, taskId: string): Promise<boolean>;
|
|
99
|
+
/** Dual-use read helper: pool-level plain read, or (`forUpdate`) the locking read inside a transaction. The row's
|
|
100
|
+
* deadline, THEN the database clock on the same executor (`sqlDbNowMs`): the MySQL protocol's `NOW()` is its statement's
|
|
101
|
+
* start, which a lock wait leaves behind — a deadline judged inside the locking statement would call a row that died
|
|
102
|
+
* while this claim waited for its lock alive, and the claim would revive it. */
|
|
103
|
+
private readSpaceCommitIn;
|
|
104
|
+
/**
|
|
105
|
+
* One claim inside a transaction: the row made to exist when this claim may create it, a locking read, the shared
|
|
106
|
+
* decision, one conditional write (the deadline = db now + THIS run's `claim.boundMs`).
|
|
107
|
+
* Lock shape — never a locking read of a key that may be ABSENT followed by an insert: on InnoDB (REPEATABLE READ) a
|
|
108
|
+
* `FOR UPDATE` that finds no row takes a GAP lock, and this table holds only in-flight commits (mostly empty ⇒ every
|
|
109
|
+
* space key falls into one gap), so two terminal transactions of DIFFERENT spaces would each hold that gap and wait on
|
|
110
|
+
* the other's insert ⇒ `ER_LOCK_DEADLOCK` rolls back a terminal write. So a claim that may create the row
|
|
111
|
+
* (`!requireOwn`: {@link decideSpaceCommitClaim}'s `insert` on an absent row) creates it FIRST — form A of the lock-shape
|
|
112
|
+
* gate: the insert, or an exclusive lock on the row already there (MySQL `ON DUPLICATE KEY UPDATE pk = pk`, PG
|
|
113
|
+
* `ON CONFLICT … DO UPDATE` — never a gap lock, never a shared lock to upgrade) — and only then reads it under that lock.
|
|
114
|
+
* A claim that may not create it (`requireOwn`) only reads: its own short transaction, and a gap lock it may take is
|
|
115
|
+
* never followed by an insert.
|
|
116
|
+
*/
|
|
117
|
+
private claimSpaceCommitIn;
|
|
87
118
|
/**
|
|
88
119
|
* Durable F4 ([ref]): a task suspended on a policy `ask`. Sets the run NON-terminal `suspended` and
|
|
89
120
|
* KEEPS the `task_active` claim — the session stays locked (no new run) until an operator resumes it to a
|
|
@@ -143,11 +174,18 @@ export declare class SqlRunStore {
|
|
|
143
174
|
/** The run a submission's `Idempotency-Key` names (`idem_key` = the scoped key, `idemKeyOf(raw, caller, "run")`), if any.
|
|
144
175
|
* `uq_task_run_idem` makes it at most one row. */
|
|
145
176
|
getRunByIdem(idemKey: string): Promise<RunRecord | undefined>;
|
|
146
|
-
/** Undo THIS submission's
|
|
147
|
-
* before any work and terminalizes the row it just claimed (the sync leg's zero-work refusal, a device-placement
|
|
148
|
-
* the error answer invites a retry, and a retry with the same key must run, not replay the refusal. The row stays (it
|
|
149
|
-
* is the refusal's record); only the
|
|
177
|
+
/** Undo THIS submission's binding (`idem_key` + `idem_body_fingerprint` → NULL) — called only where the submit surface refuses
|
|
178
|
+
* the submission before any work and terminalizes the row it just claimed (the sync leg's zero-work refusal, a device-placement
|
|
179
|
+
* reject): the error answer invites a retry, and a retry with the same key must run, not replay the refusal. The row stays (it
|
|
180
|
+
* is the refusal's record); only the binding is freed. */
|
|
150
181
|
releaseIdemKey(taskId: string): Promise<void>;
|
|
182
|
+
/** The body fingerprint this row's binding of `idem.key` holds — binding `idem.bodyFingerprint` first when it holds none (a key
|
|
183
|
+
* bound before the column existed): first arrival binds, everyone after reads. The fingerprint belongs to the KEY that bound
|
|
184
|
+
* the row, so a row this key is not bound to (hit by its `task_id` under another key, or made without a key) is never written
|
|
185
|
+
* and answers `null` — as does a missing row. One conditional UPDATE (atomic on the row on both dialects: a concurrent binder
|
|
186
|
+
* re-evaluates `IS NULL` after the winner commits and matches nothing), then a read of what the binding holds. The comparison
|
|
187
|
+
* is the submit surface's (`http/submit-replay.ts`). */
|
|
188
|
+
bindIdemBodyFingerprint(taskId: string, idem: RunIdemBinding): Promise<string | null>;
|
|
151
189
|
/** Task Trace list (S1): runs newest-first, keyset-paginated on (created_at, task_id). Optional status +
|
|
152
190
|
* jobId filters (jobId = the work-view group, served by idx_job). Caller passes `limit+1` to detect a next
|
|
153
191
|
* page. Cursor fields are bound as params (no SQL injection).
|
|
@@ -8,8 +8,9 @@ import { recordFailOpen } from "../observability/fail-open.js";
|
|
|
8
8
|
import { createObjectColumnReader, decodeJsonColumn, decodeNullableJsonColumn, foldJsonColumn, isJsonColumnBodyProblem, jsonColumnText, readJsonObjectColumn, readNonNullJsonValueColumn } from "./sql-json-column.js";
|
|
9
9
|
import { RUN_HOLDER_DEATH_CODES, RUN_STALLED_ERROR, readLegacyHolderDeathCode } from "./store-contracts.js";
|
|
10
10
|
import { notifyRunTerminal, runTerminalLogLevel } from "../observability/run-terminal.js";
|
|
11
|
-
import { mysqlDriver, pgDriver, dialectJsonEncoder } from "./sql-driver.js";
|
|
11
|
+
import { mysqlDriver, pgDriver, dialectJsonEncoder, SQL_DB_NOW_MS, sqlDbNowMs } from "./sql-driver.js";
|
|
12
12
|
import { isDupKeyError } from "./sql-errors.js";
|
|
13
|
+
import { decideSpaceCommitAbandon, decideSpaceCommitClaim, fenceSpaceCommitAtTerminal, spaceCommitStateOf } from "../workspace-space-commit.js";
|
|
13
14
|
function runRowCols(dialect) {
|
|
14
15
|
return (`task_id, session_id, owner, status, ${jsonColumnText(dialect, "result")}, error, error_code, job_id, source, ` +
|
|
15
16
|
"instance_id, cancel_requested, objective_preview, created_at, updated_at");
|
|
@@ -74,7 +75,7 @@ export class SqlRunStore {
|
|
|
74
75
|
}
|
|
75
76
|
}
|
|
76
77
|
async createRun(taskId, sessionId, owner, instanceId, meta = {}) {
|
|
77
|
-
const idemKey = meta.
|
|
78
|
+
const idemKey = meta.idem?.key ?? null;
|
|
78
79
|
const conn = await this.db.connect();
|
|
79
80
|
let stage = "claim";
|
|
80
81
|
try {
|
|
@@ -83,9 +84,9 @@ export class SqlRunStore {
|
|
|
83
84
|
try {
|
|
84
85
|
await conn.query(this.q("INSERT INTO task_active (session_id, task_id, since) VALUES (?,?,?)", "INSERT INTO task_active (session_id, task_id, since) VALUES ($1,$2,$3)"), [sessionId, taskId, now]);
|
|
85
86
|
stage = "row";
|
|
86
|
-
await conn.query(this.q("INSERT INTO task_run (task_id, session_id, owner, status, instance_id, job_id, source, objective_preview, idem_key, created_at, updated_at) " +
|
|
87
|
-
"VALUES (?,?,?,'running'
|
|
88
|
-
"VALUES ($1,$2,$3,'running',$4,$5,$6,$7,$8,$9,$10)"), [
|
|
87
|
+
await conn.query(this.q("INSERT INTO task_run (task_id, session_id, owner, status, instance_id, job_id, source, objective_preview, idem_key, idem_body_fingerprint, created_at, updated_at) " +
|
|
88
|
+
"VALUES (?,?,?,'running',?,?,?,?,?,?,?,?)", "INSERT INTO task_run (task_id, session_id, owner, status, instance_id, job_id, source, objective_preview, idem_key, idem_body_fingerprint, created_at, updated_at) " +
|
|
89
|
+
"VALUES ($1,$2,$3,'running',$4,$5,$6,$7,$8,$9,$10,$11)"), [
|
|
89
90
|
taskId,
|
|
90
91
|
sessionId,
|
|
91
92
|
owner,
|
|
@@ -98,6 +99,7 @@ export class SqlRunStore {
|
|
|
98
99
|
? null
|
|
99
100
|
: pgSanitizeText(meta.objectivePreview),
|
|
100
101
|
idemKey,
|
|
102
|
+
meta.idem?.bodyFingerprint ?? null,
|
|
101
103
|
now,
|
|
102
104
|
now,
|
|
103
105
|
]);
|
|
@@ -163,27 +165,33 @@ export class SqlRunStore {
|
|
|
163
165
|
const errorCode = result === null ? null : (outwardPlane(result.terminal).errorCode ?? null);
|
|
164
166
|
let won = false;
|
|
165
167
|
let observed;
|
|
166
|
-
await
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
result
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
168
|
+
await fenceSpaceCommitAtTerminal(taskId, async (spaceCommit) => {
|
|
169
|
+
let fenced = false;
|
|
170
|
+
await this.tx(async (conn) => {
|
|
171
|
+
const w = await conn.query(this.q("UPDATE task_run SET status = ?, result = ?, error = ?, error_code = ?, updated_at = ? WHERE task_id = ? AND status IN ('running','suspended','needs_review')", "UPDATE task_run SET status = $1, result = $2::jsonb, error = $3, error_code = $4, updated_at = $5 WHERE task_id = $6 AND status IN ('running','suspended','needs_review')"), [
|
|
172
|
+
status,
|
|
173
|
+
result == null ? null : this.json(result),
|
|
174
|
+
this.db.dialect === "tidb" ? error : error == null ? null : pgSanitizeText(error),
|
|
175
|
+
this.db.dialect === "tidb" ? errorCode : errorCode == null ? null : pgSanitizeText(errorCode),
|
|
176
|
+
new Date(),
|
|
177
|
+
taskId,
|
|
178
|
+
]);
|
|
179
|
+
won = w.affected > 0;
|
|
180
|
+
if (won && runTerminalLogLevel(status, errorCode ?? undefined) !== undefined) {
|
|
181
|
+
const { rows } = await conn.query(this.q("SELECT owner, session_id, created_at FROM task_run WHERE task_id = ?", "SELECT owner, session_id, created_at FROM task_run WHERE task_id = $1"), [taskId]);
|
|
182
|
+
const row = rows[0];
|
|
183
|
+
const createdAt = row ? Date.parse(iso(row.created_at)) : NaN;
|
|
184
|
+
observed = {
|
|
185
|
+
owner: row?.owner ?? null,
|
|
186
|
+
sessionId: String(row?.session_id ?? result?.sessionId ?? ""),
|
|
187
|
+
createdAt: Number.isFinite(createdAt) ? createdAt : undefined,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
if (spaceCommit !== undefined)
|
|
191
|
+
fenced = await this.claimSpaceCommitIn(conn, spaceCommit.spaceKey, taskId, "pending", spaceCommit.claim);
|
|
192
|
+
await conn.query(this.q("DELETE FROM task_active WHERE task_id = ?", "DELETE FROM task_active WHERE task_id = $1"), [taskId]);
|
|
193
|
+
});
|
|
194
|
+
return fenced;
|
|
187
195
|
});
|
|
188
196
|
if (won) {
|
|
189
197
|
notifyRunTerminal({
|
|
@@ -198,6 +206,57 @@ export class SqlRunStore {
|
|
|
198
206
|
});
|
|
199
207
|
}
|
|
200
208
|
}
|
|
209
|
+
async readSpaceCommit(spaceKey) {
|
|
210
|
+
return this.readSpaceCommitIn(this.db, spaceKey, false);
|
|
211
|
+
}
|
|
212
|
+
async claimSpaceCommit(spaceKey, taskId, state, claim) {
|
|
213
|
+
let claimed = false;
|
|
214
|
+
await this.tx(async (conn) => {
|
|
215
|
+
claimed = await this.claimSpaceCommitIn(conn, spaceKey, taskId, state, claim);
|
|
216
|
+
});
|
|
217
|
+
return claimed;
|
|
218
|
+
}
|
|
219
|
+
async endSpaceCommit(spaceKey, taskId) {
|
|
220
|
+
await this.db.query(this.q("DELETE FROM workspace_space_commit WHERE space_key = ? AND task_id = ?", "DELETE FROM workspace_space_commit WHERE space_key = $1 AND task_id = $2"), [spaceKey, taskId]);
|
|
221
|
+
}
|
|
222
|
+
async abandonSpaceCommit(spaceKey, taskId) {
|
|
223
|
+
let gone = false;
|
|
224
|
+
await this.tx(async (conn) => {
|
|
225
|
+
if (!decideSpaceCommitAbandon(await this.readSpaceCommitIn(conn, spaceKey, true), taskId))
|
|
226
|
+
return;
|
|
227
|
+
const w = await conn.query(this.q("DELETE FROM workspace_space_commit WHERE space_key = ? AND task_id = ?", "DELETE FROM workspace_space_commit WHERE space_key = $1 AND task_id = $2"), [spaceKey, taskId]);
|
|
228
|
+
gone = w.affected > 0;
|
|
229
|
+
});
|
|
230
|
+
return gone;
|
|
231
|
+
}
|
|
232
|
+
async readSpaceCommitIn(exec, spaceKey, forUpdate) {
|
|
233
|
+
const suffix = forUpdate ? " FOR UPDATE" : "";
|
|
234
|
+
const { rows } = await exec.query(this.q(`SELECT task_id, state, expires_at_ms FROM workspace_space_commit WHERE space_key = ?${suffix}`, `SELECT task_id, state, expires_at_ms FROM workspace_space_commit WHERE space_key = $1${suffix}`), [spaceKey]);
|
|
235
|
+
const row = rows[0];
|
|
236
|
+
if (row === undefined)
|
|
237
|
+
return undefined;
|
|
238
|
+
const expiresAt = Number(row["expires_at_ms"]);
|
|
239
|
+
if (typeof row["task_id"] !== "string" || !Number.isFinite(expiresAt))
|
|
240
|
+
throw new Error(`workspace_space_commit row for a space is unreadable (task_id ${String(row["task_id"])}, deadline ${String(row["expires_at_ms"])})`);
|
|
241
|
+
const now = await sqlDbNowMs({ dialect: this.db.dialect, query: (sql, params) => exec.query(sql, params) }, "workspace_space_commit");
|
|
242
|
+
return { taskId: row["task_id"], state: spaceCommitStateOf(row["state"]), remainingMs: expiresAt - now };
|
|
243
|
+
}
|
|
244
|
+
async claimSpaceCommitIn(conn, spaceKey, taskId, state, claim) {
|
|
245
|
+
if (!claim.requireOwn) {
|
|
246
|
+
await conn.query(this.q(`INSERT INTO workspace_space_commit (space_key, task_id, state, expires_at_ms) VALUES (?,?,?,${SQL_DB_NOW_MS.tidb} + ?) ON DUPLICATE KEY UPDATE space_key = space_key`, `INSERT INTO workspace_space_commit (space_key, task_id, state, expires_at_ms) VALUES ($1,$2,$3,${SQL_DB_NOW_MS.pg} + $4::bigint) ON CONFLICT (space_key) DO UPDATE SET space_key = EXCLUDED.space_key`), [spaceKey, taskId, state, claim.boundMs]);
|
|
247
|
+
}
|
|
248
|
+
const row = await this.readSpaceCommitIn(conn, spaceKey, true);
|
|
249
|
+
switch (decideSpaceCommitClaim(row, taskId, claim)) {
|
|
250
|
+
case "refuse":
|
|
251
|
+
return false;
|
|
252
|
+
case "insert":
|
|
253
|
+
throw new Error("workspace_space_commit: a space's row vanished under this transaction's own lock");
|
|
254
|
+
case "update": {
|
|
255
|
+
const w = await conn.query(this.q(`UPDATE workspace_space_commit SET task_id = ?, state = ?, expires_at_ms = ${SQL_DB_NOW_MS.tidb} + ? WHERE space_key = ? AND task_id = ?`, `UPDATE workspace_space_commit SET task_id = $1, state = $2, expires_at_ms = ${SQL_DB_NOW_MS.pg} + $3::bigint WHERE space_key = $4 AND task_id = $5`), [taskId, state, claim.boundMs, spaceKey, row.taskId]);
|
|
256
|
+
return w.affected > 0;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
201
260
|
async setSuspended(taskId, result) {
|
|
202
261
|
const sealed = sealRunRowResult(result);
|
|
203
262
|
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]);
|
|
@@ -250,7 +309,13 @@ export class SqlRunStore {
|
|
|
250
309
|
return rows[0] ? this.rowToRun(rows[0]) : undefined;
|
|
251
310
|
}
|
|
252
311
|
async releaseIdemKey(taskId) {
|
|
253
|
-
await this.db.query(this.q("UPDATE task_run SET idem_key = NULL WHERE task_id = ?", "UPDATE task_run SET idem_key = NULL WHERE task_id = $1"), [taskId]);
|
|
312
|
+
await this.db.query(this.q("UPDATE task_run SET idem_key = NULL, idem_body_fingerprint = NULL WHERE task_id = ?", "UPDATE task_run SET idem_key = NULL, idem_body_fingerprint = NULL WHERE task_id = $1"), [taskId]);
|
|
313
|
+
}
|
|
314
|
+
async bindIdemBodyFingerprint(taskId, idem) {
|
|
315
|
+
await this.db.query(this.q("UPDATE task_run SET idem_body_fingerprint = ? WHERE task_id = ? AND idem_key = ? AND idem_body_fingerprint IS NULL", "UPDATE task_run SET idem_body_fingerprint = $1 WHERE task_id = $2 AND idem_key = $3 AND idem_body_fingerprint IS NULL"), [idem.bodyFingerprint, taskId, idem.key]);
|
|
316
|
+
const { rows } = await this.db.query(this.q("SELECT idem_body_fingerprint FROM task_run WHERE task_id = ? AND idem_key = ?", "SELECT idem_body_fingerprint FROM task_run WHERE task_id = $1 AND idem_key = $2"), [taskId, idem.key]);
|
|
317
|
+
const held = rows[0]?.idem_body_fingerprint;
|
|
318
|
+
return typeof held === "string" ? held : null;
|
|
254
319
|
}
|
|
255
320
|
async listRuns(opts) {
|
|
256
321
|
if (opts.cursor && Number.isNaN(Date.parse(opts.cursor.createdAt)))
|
|
@@ -25,9 +25,8 @@
|
|
|
25
25
|
* 一条作者要求扣留的内容)。
|
|
26
26
|
*
|
|
27
27
|
* ── 键形法:**编码规范形**(codex R2-[high] 验真后改形;grant 店的「形状拒」在这里是错的)────────
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* 的真会话,店若按字节形状拒它,后果=该会话在 SQL 部署上「opt-out 永远落不住(unpersisted)+ 采集态
|
|
28
|
+
* 7.125.x 及更早的提交面把 sessionId 定为**任意不透明串**(≤64 字符,run-local `--session` / 匿名分享 id 等都合法;7.126.0 起
|
|
29
|
+
* 只收 canonical uuid)——存量里带空白/控制字符的 id 是**在契约内建出来**的真会话,店若按字节形状拒它,后果=该会话在 SQL 部署上「opt-out 永远落不住(unpersisted)+ 采集态
|
|
31
30
|
* 永远 indeterminate(commits 全抑制)」,是部署契约不兼容,不是输入加固。所以键列**不存生字节**,存
|
|
32
31
|
* `encodeURIComponent(sessionId)` 规范形 —— 与 **core 文件三腿给文件名做的编码逐字同源**(layout.js
|
|
33
32
|
* `captureOptOutPath` 亲读),两载体一个键法:
|
|
@@ -183,8 +183,9 @@ export declare const SQL_DB_NOW_MS: {
|
|
|
183
183
|
*
|
|
184
184
|
* `subject` 只进错误文案(出问题时运维要知道是哪一张表的判决卡住了)。读不出数 ⇒ **响亮抛**:
|
|
185
185
|
* 一次「钟读不出来」在授权面上是运维事件,不是一个可以猜的默认值,更不是掉回 `Date.now()` 的理由。
|
|
186
|
+
* `db` 只要方言与一个执行口:池,或事务里的那条连接(锁读之后要「拿到锁那一刻」的钟时,在同一条连接上问)。
|
|
186
187
|
*/
|
|
187
|
-
export declare function sqlDbNowMs(db: SqlDriver, subject: string): Promise<number>;
|
|
188
|
+
export declare function sqlDbNowMs(db: Pick<SqlDriver, "dialect" | "query">, subject: string): Promise<number>;
|
|
188
189
|
export interface SqlDriver extends SqlExec {
|
|
189
190
|
readonly dialect: SqlDialect;
|
|
190
191
|
/**
|
|
@@ -265,7 +265,7 @@ class LocalBackend {
|
|
|
265
265
|
}
|
|
266
266
|
async ensureSchema() { }
|
|
267
267
|
async runOneTimeMigrations() {
|
|
268
|
-
return { ...NO_SQL_MIGRATIONS,
|
|
268
|
+
return { ...NO_SQL_MIGRATIONS, holderDeathCodeRowsBackfilled: this.runStore.migrateLegacyHolderDeathCodes() };
|
|
269
269
|
}
|
|
270
270
|
async close() {
|
|
271
271
|
await this.sessionStore.dispose();
|
|
@@ -221,17 +221,31 @@ export interface RunRecord {
|
|
|
221
221
|
createdAt: string;
|
|
222
222
|
updatedAt: string;
|
|
223
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* 一次提交在 run 账本上的**绑定** —— 幂等键 + 绑定它的那次请求的体指纹。两样同铸(提交面读到 `Idempotency-Key` 的同一处)、
|
|
226
|
+
* 同落一行(`task_run.idem_key` / `idem_body_fingerprint`)、同清(`releaseIdemKey`),在飞合流也按这一对分区。
|
|
227
|
+
*
|
|
228
|
+
* · `key` —— `idemKeyOf(raw, caller, "run")`(`http/idempotency.ts`;sha256 hex 定宽 64)。
|
|
229
|
+
* · `bodyFingerprint` —— 绑定它的那次请求的体指纹(sha256 hex 定宽 64):HTTP 三条提交腿 = `submissionFingerprintOf`
|
|
230
|
+
* (同一只规范化函数);A2A `message/send` = 这条消息的内容指纹(它同时是 A2A 键的原始键 ⇒ 同键必同体)。
|
|
231
|
+
*
|
|
232
|
+
* 幂等 = 同一调用方、同一键、**同一请求**:命中账本而指纹不同 ⇒ 提交面的唯一回执函数答 409(`http/submit-replay.ts`)。
|
|
233
|
+
*/
|
|
234
|
+
export interface RunIdemBinding {
|
|
235
|
+
readonly key: string;
|
|
236
|
+
readonly bodyFingerprint: string;
|
|
237
|
+
}
|
|
224
238
|
/**
|
|
225
239
|
* `RunStore.createRun` 的行元数据(写进 `task_run` 的附加列)。
|
|
226
240
|
*
|
|
227
|
-
* `
|
|
228
|
-
*
|
|
241
|
+
* `idem` = 这次提交的账本绑定({@link RunIdemBinding})。缺席 / `null` = 调用方没带 `Idempotency-Key` ⇒ 两列 NULL,不参与去重
|
|
242
|
+
* (UNIQUE 不约束 NULL,两方言同形)。
|
|
229
243
|
*/
|
|
230
244
|
export interface NewRunMeta {
|
|
231
245
|
jobId?: string | null;
|
|
232
246
|
source?: string | null;
|
|
233
247
|
objectivePreview?: string | null;
|
|
234
|
-
|
|
248
|
+
idem?: RunIdemBinding | null;
|
|
235
249
|
}
|
|
236
250
|
/**
|
|
237
251
|
* `RunStore.createRun` 的结局 —— 三形,一形一句话:
|
|
@@ -170,6 +170,11 @@ export const SCHEMA_STATEMENTS = [
|
|
|
170
170
|
-- runs: uq_task_run_idem makes createRun an atomic win-or-observe across replicas and restarts. NULL = the caller
|
|
171
171
|
-- sent no key (UNIQUE does not constrain NULL; same on both dialects).
|
|
172
172
|
idem_key VARCHAR(64) NULL,
|
|
173
|
+
-- idem_body_fingerprint: the fingerprint of the request that bound idem_key (sha256 hex, fixed width 64; HTTP legs =
|
|
174
|
+
-- http/idempotency.ts submissionFingerprintOf, A2A = its message fingerprint). Idempotency = same caller, same key, SAME
|
|
175
|
+
-- request: a hit whose fingerprint differs is answered 409 conflict.idempotency_payload_mismatch. NULL = no key, or a key
|
|
176
|
+
-- bound before this column existed (the first request to hit it binds its fingerprint).
|
|
177
|
+
idem_body_fingerprint VARCHAR(64) NULL,
|
|
173
178
|
created_at DATETIME(3) NOT NULL,
|
|
174
179
|
updated_at DATETIME(3) NOT NULL,
|
|
175
180
|
PRIMARY KEY (task_id),
|
|
@@ -234,6 +239,13 @@ export const SCHEMA_STATEMENTS = [
|
|
|
234
239
|
since DATETIME(3) NOT NULL,
|
|
235
240
|
PRIMARY KEY (session_id),
|
|
236
241
|
KEY idx_task_active_task (task_id)
|
|
242
|
+
) COLLATE utf8mb4_bin`,
|
|
243
|
+
`CREATE TABLE IF NOT EXISTS workspace_space_commit (
|
|
244
|
+
space_key VARCHAR(64) NOT NULL,
|
|
245
|
+
task_id VARCHAR(64) NOT NULL,
|
|
246
|
+
state VARCHAR(16) NOT NULL,
|
|
247
|
+
expires_at_ms BIGINT NOT NULL,
|
|
248
|
+
PRIMARY KEY (space_key)
|
|
237
249
|
) COLLATE utf8mb4_bin`,
|
|
238
250
|
`CREATE TABLE IF NOT EXISTS checkpoint (
|
|
239
251
|
token VARCHAR(120) NOT NULL,
|
|
@@ -278,9 +290,9 @@ export const SCHEMA_STATEMENTS = [
|
|
|
278
290
|
bound_input_hash VARCHAR(190) NULL,
|
|
279
291
|
-- 🔴 队列化之前的**单座**列 pending_steer 已**退役删列**:那个席位在引擎侧没了(core 7.17.0 #717 d
|
|
280
292
|
-- 对带席的行 PRE-CAS 拒 seat_retired),留着一列没人读、没人写的字节只会让下一个人以为还有两种读法。
|
|
281
|
-
--
|
|
282
|
-
--
|
|
283
|
-
--
|
|
293
|
+
-- 存量库里仍带着这一列的(没经过带迁移的版本的跳版升级库)起服**响亮拒**(plugins/one-time-migrations.ts 的退役 schema 拒启臂,
|
|
294
|
+
-- config.schema_mismatch):出路是先在 server 7.125.x 上起服一次,让当年的一次性迁移把它处置掉,再升。刻意**不**自动
|
|
295
|
+
-- 把裸单座折进队列:那等于本仓替引擎铸一条它从没写过的队列条目。
|
|
284
296
|
-- pending_steer_queue:core 5.14.0(#147)的**有界有序队列**(PendingSteerEntry[] 的 JSON),现在是本表上
|
|
285
297
|
-- durable steering 的唯一载体。Never touches status/resolve (design/80 inv #4)。
|
|
286
298
|
-- 容量:core 的 PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES = 48000 字节是整信封的硬上界(越界 fail-loud
|
|
@@ -5,6 +5,7 @@ import { isMysqlDupKeyError } from "./sql-errors.js";
|
|
|
5
5
|
import { PLACEMENT_SELECT_COLUMNS, placementFromRow } from "./session-placement.js";
|
|
6
6
|
import { decodeSessionEventPayload } from "./session-event-payload.js";
|
|
7
7
|
import { jsonColumnText } from "./sql-json-column.js";
|
|
8
|
+
import { incarnationFromCell } from "../session-incarnation.js";
|
|
8
9
|
const SESSION_EVENT_BY_TYPE_HINT = "/*+ USE_INDEX(session_event, idx_session_event_type) */";
|
|
9
10
|
export const SESSION_EVENT_LATEST_BY_TYPE_SQL = {
|
|
10
11
|
floor: `SELECT ${SESSION_EVENT_BY_TYPE_HINT} ${jsonColumnText("tidb", "payload")} FROM session_event WHERE session_id = ? AND type = 'compaction' ORDER BY seq DESC LIMIT 1`,
|
|
@@ -39,7 +40,7 @@ export class TiDBSessionStorage extends BaseSessionStorage {
|
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
42
|
static async wake(pool, sessionId) {
|
|
42
|
-
const [metaRows] = await pool.query(`SELECT created_at, leaf_id, leaf_seq, ${PLACEMENT_SELECT_COLUMNS} FROM session_meta WHERE session_id = ?`, [sessionId]);
|
|
43
|
+
const [metaRows] = await pool.query(`SELECT created_at, leaf_id, leaf_seq, incarnation, ${PLACEMENT_SELECT_COLUMNS} FROM session_meta WHERE session_id = ?`, [sessionId]);
|
|
43
44
|
const meta = metaRows[0];
|
|
44
45
|
if (!meta)
|
|
45
46
|
return null;
|
|
@@ -47,7 +48,7 @@ export class TiDBSessionStorage extends BaseSessionStorage {
|
|
|
47
48
|
const [eventRows] = await pool.query(`SELECT seq, ${jsonColumnText("tidb", "payload")} FROM session_event WHERE session_id = ? AND seq >= ? ORDER BY seq ASC`, [sessionId, floorSeq]);
|
|
48
49
|
const entries = eventRows.map((r) => decodeSessionEventPayload(r.payload, sessionId, r.seq));
|
|
49
50
|
const placement = placementFromRow(meta);
|
|
50
|
-
const metadata = { id: sessionId, createdAt: toIso(meta.created_at), ...(placement !== undefined ? { placement } : {}) };
|
|
51
|
+
const metadata = { id: sessionId, createdAt: toIso(meta.created_at), incarnation: incarnationFromCell(sessionId, meta.incarnation), ...(placement !== undefined ? { placement } : {}) };
|
|
51
52
|
return new TiDBSessionStorage(pool, sessionId, metadata, entries, meta.leaf_id ?? null, floorEntryId, floorSeq);
|
|
52
53
|
}
|
|
53
54
|
static async computeFloor(pool, sessionId) {
|
|
@@ -63,6 +63,8 @@ export declare class TiDBSessionStore implements SessionStore {
|
|
|
63
63
|
/** S-790:属主 + 化身**一次读**(单行单 SELECT —— 两次独立读在删除 / 重登记的缝里能拼出混世代组合)。`undefined` = 无此会话;
|
|
64
64
|
* 行在而化身为空 ⇒ `SessionIncarnationMissingError`(响亮拒,判据唯一属主 `sessionFaceFromRow`)。 */
|
|
65
65
|
sessionFaceOf(sessionId: string): Promise<SessionFace | undefined>;
|
|
66
|
+
/** core `SessionStore.incarnationOf`(按 id 读住客,给没取会话的那一侧):{@link sessionFaceOf} 的化身那一半 —— 同一次读、同一只解码。 */
|
|
67
|
+
incarnationOf(sessionId: string): Promise<string | undefined>;
|
|
66
68
|
touch(sessionId: string): Promise<void>;
|
|
67
69
|
/** E18 — the session's current leaf `SessionTreeEntry.id` (`session_meta.leaf_id`, set transactionally on every
|
|
68
70
|
* persist). A single SELECT, NO acquire/lock/CAS — safe to call mid-run (write-safety is the append-time CAS, not
|
|
@@ -89,6 +89,9 @@ export class TiDBSessionStore {
|
|
|
89
89
|
return undefined;
|
|
90
90
|
return sessionFaceFromRow(sessionId, rows[0].owner ?? null, rows[0].incarnation);
|
|
91
91
|
}
|
|
92
|
+
async incarnationOf(sessionId) {
|
|
93
|
+
return (await this.sessionFaceOf(sessionId))?.incarnation;
|
|
94
|
+
}
|
|
92
95
|
async touch(sessionId) {
|
|
93
96
|
await this.pool.query("UPDATE session_meta SET updated_at = ? WHERE session_id = ?", [
|
|
94
97
|
new Date(),
|
|
@@ -486,13 +489,14 @@ class TiDBStagingHandle {
|
|
|
486
489
|
const now = new Date();
|
|
487
490
|
await conn.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq, incarnation) VALUES (?,?,?,?,?,?,?) " +
|
|
488
491
|
"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()]);
|
|
489
|
-
const [afterMeta] = await conn.query("SELECT placement_kind FROM session_meta WHERE session_id = ?", [this.realSessionId]);
|
|
492
|
+
const [afterMeta] = await conn.query("SELECT placement_kind, owner, incarnation FROM session_meta WHERE session_id = ?", [this.realSessionId]);
|
|
490
493
|
if (afterMeta[0]?.placement_kind != null) {
|
|
491
494
|
await conn.rollback();
|
|
492
495
|
throw new SessionError("placement_refused", `session ${this.realSessionId} belongs to the ${String(afterMeta[0].placement_kind)} placement partition — a session-sync staged commit cannot overwrite a delegation transcript`);
|
|
493
496
|
}
|
|
497
|
+
const face = sessionFaceFromRow(this.realSessionId, afterMeta[0]?.owner ?? null, afterMeta[0]?.incarnation);
|
|
494
498
|
await conn.commit();
|
|
495
|
-
return { relation: rel.relation };
|
|
499
|
+
return { relation: rel.relation, face };
|
|
496
500
|
}
|
|
497
501
|
catch (err) {
|
|
498
502
|
try {
|
|
@@ -19,6 +19,17 @@ export declare function scanKeyClosure(bag: object, prefix: string, accepted: Re
|
|
|
19
19
|
}): void;
|
|
20
20
|
/** 一张分类表的文案片段(封顶后只报数)。 */
|
|
21
21
|
export declare function keyClosureClause(names: string[], label: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* **场景透传键** —— 它们不属于共享提交面,各属一个场景,由 `capabilities/scenarios.ts` 经
|
|
24
|
+
* `ScenarioRequest` 的索引签名读(亲读那里的 `req.<key>`:除去已在 `TaskRequestBody` 上的 `objective` / `scenario`,
|
|
25
|
+
* 余下只有 `repo` 一个 —— 只喂 discuss 场景与 code-review council 腿的五键随那两条腿删除)。`TaskRequestBody` **刻意不声明**它们(那个类型的
|
|
26
|
+
* 头注逐字成文:场景键属于某一个场景,不属于共享提交面),所以它们只能在这里成文 —— 一个漏登记的场景键
|
|
27
|
+
* 会被本门 400 掉,而那正是「场景收不到它、调用方却拿到 200」的反面:响亮,不静默。
|
|
28
|
+
* 🔴 新场景要读一个新的体键 ⇒ 同批加进本行(加消费点而不加这行 = 那个场景在 wire 上不可用)。
|
|
29
|
+
*/
|
|
30
|
+
declare const TASK_BODY_SCENARIO_KEYS: readonly ["repo"];
|
|
31
|
+
/** 场景透传键的型(wire 变更清单的自铸登记表 `trace/self-minted-wire.ts` 的 `scenarioBodyKey` 按它 `satisfies`)。 */
|
|
32
|
+
export type ScenarioBodyKey = (typeof TASK_BODY_SCENARIO_KEYS)[number];
|
|
22
33
|
/** 剥掉索引签名,只留**具名**键 —— `TaskRequestBody` 上那条 `[k: string]: unknown` 会让
|
|
23
34
|
* `keyof` 退化成 `string`,双向门就永远恒真(零判别力)。
|
|
24
35
|
* 🔴 **导出**(S-405):姊妹门 `request-value-closure.ts` 的双向钉要同一个剥法 —— 抄第二份 = 两处哪天
|
|
@@ -34,4 +45,5 @@ export type NamedKeysOf<T> = {
|
|
|
34
45
|
* 「missing 'objective' string」那道门,免得同一个体被两句话抢答)。
|
|
35
46
|
*/
|
|
36
47
|
export declare function taskBodyKeyIssue(raw: unknown): KeyClosureIssue | null;
|
|
48
|
+
export {};
|
|
37
49
|
//# sourceMappingURL=request-key-closure.d.ts.map
|
|
@@ -18,9 +18,9 @@ export function keyClosureClause(names, label) {
|
|
|
18
18
|
return `${label}: ${shown.join(", ")}${more > 0 ? ` (+${more} more)` : ""}`;
|
|
19
19
|
}
|
|
20
20
|
const TASK_BODY_DECLARED_KEYS = [
|
|
21
|
-
"a2aPeers", "acceptPartialRestore", "additionalDirectories", "additionalReadDirectories", "agents",
|
|
21
|
+
"a2aPeers", "acceptPartialRestore", "additionalDirectories", "additionalReadDirectories", "agents", "allowTools",
|
|
22
22
|
"appendSystemPrompt", "approvalReach", "attachmentIds", "attachments", "capabilitiesNeeded", "cascade", "clientContext",
|
|
23
|
-
"compaction", "compactionModel", "
|
|
23
|
+
"compaction", "compactionModel", "cwd", "deferTools", "deviceId", "disableSkills", "enableFork",
|
|
24
24
|
"excludeAllTools", "excludeTools", "fileHistory",
|
|
25
25
|
"finalVerification", "forwardSubagentEvents", "images", "inheritEnv", "interactiveTools", "jobId", "limits", "maxCostUsd",
|
|
26
26
|
"maxTokens", "mcpServers", "memory", "memoryCapture", "memoryWrite", "model", "objective", "oneShot", "outputRetries", "outputStyle",
|
|
@@ -30,7 +30,7 @@ const TASK_BODY_DECLARED_KEYS = [
|
|
|
30
30
|
"sessionId", "settings", "skills", "suggestNextPrompts", "systemPrompt", "taskId", "toolMaterializeStrategy",
|
|
31
31
|
"userInvokedSkills", "verify", "verifyRounds",
|
|
32
32
|
];
|
|
33
|
-
const TASK_BODY_SCENARIO_KEYS = ["
|
|
33
|
+
const TASK_BODY_SCENARIO_KEYS = ["repo"];
|
|
34
34
|
const ACCEPTED_BODY = new Set([...TASK_BODY_DECLARED_KEYS, ...TASK_BODY_SCENARIO_KEYS]);
|
|
35
35
|
const UNSUPPORTED_BODY = new Set(["handsReadOnly", "readDenyPatterns", "readFace", "shellGate", "writeFace"]);
|
|
36
36
|
export function taskBodyKeyIssue(raw) {
|