@sema-agent/server 7.125.1 → 7.127.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 +229 -2
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/USAGE.md +346 -145
- package/deploy/sema-up/README.md +1 -1
- package/deploy/sema-up/chart/templates/networkpolicy-sandbox.yaml +57 -0
- package/deploy/sema-up/chart/templates/sandbox-rbac.yaml +32 -1
- package/deploy/sema-up/chart/templates/server-v4pro.yaml +3 -2
- package/deploy/sema-up/chart/templates/server.yaml +2 -1
- package/deploy/sema-up/chart/values.yaml +22 -0
- package/deploy/sema-up/cluster-up.sh +10 -10
- package/deploy/sema-up/kube-up.sh +20 -13
- 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/node-addrs.sh +42 -0
- 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 +26 -3
- 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 +3 -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 +87 -41
- package/dist/boot/leader.d.ts +2 -1
- package/dist/boot/memory-boundary.d.ts +1 -1
- package/dist/boot/memory-consolidation.d.ts +37 -3
- package/dist/boot/memory-consolidation.js +18 -2
- 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 +7 -6
- package/dist/boot/runner-deps.js +5 -16
- package/dist/boot/session-faces.d.ts +7 -2
- 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 +13 -9
- 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 +14 -5
- package/dist/config-provider.d.ts +5 -1
- package/dist/config-provider.js +23 -4
- package/dist/config-types.d.ts +19 -2
- package/dist/config.js +41 -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.d.ts +12 -0
- package/dist/execution-lane-caps.js +18 -4
- package/dist/fleet/subagent-tail-bus.js +15 -5
- package/dist/hooks/hook-runner.d.ts +84 -30
- package/dist/hooks/hook-runner.js +74 -41
- package/dist/http/admission.js +17 -10
- package/dist/http/durable-run-leg.d.ts +5 -4
- package/dist/http/durable-run-leg.js +2 -4
- 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 +20 -10
- package/dist/http/route-ctx.d.ts +15 -7
- 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 +35 -14
- 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 +29 -19
- package/dist/http/routes/workflows.js +8 -0
- package/dist/http/server.d.ts +4 -0
- package/dist/http/server.js +3 -5
- 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 +1 -1
- package/dist/index.js +1 -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/lsp/manager.js +1 -1
- package/dist/memory-scope.d.ts +19 -7
- package/dist/memory-scope.js +25 -8
- package/dist/observability/fail-open.d.ts +31 -3
- package/dist/observability/fail-open.js +31 -3
- package/dist/observability/metrics.d.ts +0 -1
- package/dist/observability/metrics.js +1 -4
- package/dist/observability/run-terminal.js +72 -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 +42 -22
- package/dist/plugins/caching-session-store.js +19 -9
- 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/k8s-exec-protocol.js +3 -0
- 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 +165 -39
- package/dist/plugins/one-time-migrations.js +51 -144
- 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 +23 -1
- package/dist/plugins/remote-env-host.js +20 -2
- package/dist/plugins/remote-env-k8s.d.ts +264 -41
- package/dist/plugins/remote-env-k8s.js +551 -82
- 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/session-store.d.ts +4 -3
- package/dist/plugins/session-store.js +1 -1
- package/dist/plugins/sql-driver.d.ts +2 -1
- package/dist/plugins/store-backend.d.ts +12 -5
- package/dist/plugins/store-backend.js +13 -4
- package/dist/plugins/store-contracts.d.ts +20 -3
- package/dist/plugins/store-open-wait.js +3 -0
- 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 +32 -7
- package/dist/request-key-closure.js +25 -9
- 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-acceptance.d.ts +51 -0
- package/dist/run-acceptance.js +27 -0
- package/dist/run-local.d.ts +5 -4
- package/dist/run-local.js +23 -17
- package/dist/runs.d.ts +7 -11
- package/dist/runs.js +6 -16
- package/dist/sandbox-egress-policy.d.ts +61 -0
- package/dist/sandbox-egress-policy.js +235 -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-error-wire.d.ts +2 -2
- 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/session-titler.d.ts +5 -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 +6 -5
- package/dist/trace/engine-notice-wire.d.ts +26 -1
- package/dist/trace/engine-notice-wire.js +7 -2
- package/dist/trace/ledger-events.d.ts +9 -3
- package/dist/trace/ledger-sink.d.ts +7 -0
- package/dist/trace/ledger-sink.js +14 -4
- package/dist/trace/project.d.ts +57 -11
- package/dist/trace/project.js +54 -8
- package/dist/trace/self-minted-wire.d.ts +94 -2
- package/dist/trace/self-minted-wire.js +28 -0
- package/dist/trace/tool-output-media.d.ts +26 -0
- package/dist/trace/tool-output-media.js +57 -0
- package/dist/trace/wire-projection-faces.d.ts +7 -1
- package/dist/trace/wire-projection-faces.js +2 -1
- 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` 亲读),两载体一个键法:
|
|
@@ -7,10 +7,11 @@ import type { StoreBackend } from "./store-backend.js";
|
|
|
7
7
|
* - `memory` (default): core's in-memory `TtlSessionStore` (lost on restart, 7-day idle TTL).
|
|
8
8
|
* - `tidb`: the durable "Session event-log center" (L1) — append-only events + wake/resume +
|
|
9
9
|
* cross-instance CAS (F2), on whichever SQL engine `DB_BACKEND` selects (TiDB or PG, via the store backend).
|
|
10
|
-
* Optionally wrapped in a warm cache (reuse a hot session across turns;
|
|
11
|
-
*
|
|
10
|
+
* Optionally wrapped in a warm cache (reuse a hot session across turns; a hit is served only while the store's
|
|
11
|
+
* head still names the cached leaf, so it is correct under any routing across replicas) — set
|
|
12
|
+
* `SESSION_CACHE_TTL_SEC=0` to disable.
|
|
12
13
|
*/
|
|
13
|
-
export declare function createSessionStore(config: ServiceConfig, backend?: StoreBackend, metrics?: {
|
|
14
|
+
export declare function createSessionStore(config: Pick<ServiceConfig, "sessionBackend" | "sessionCacheTtlSec">, backend?: Pick<StoreBackend, "session">, metrics?: {
|
|
14
15
|
inc(name: string, labels?: Record<string, string>, n?: number): void;
|
|
15
16
|
}): SessionStore;
|
|
16
17
|
/**
|
|
@@ -6,7 +6,7 @@ export function createSessionStore(config, backend, metrics) {
|
|
|
6
6
|
throw new Error("a DB session backend requires a store backend (DB_BACKEND=mysql|pg + a reachable DB).");
|
|
7
7
|
const store = backend.session();
|
|
8
8
|
return config.sessionCacheTtlSec > 0
|
|
9
|
-
? new CachingSessionStore(store, { ttlSec: config.sessionCacheTtlSec,
|
|
9
|
+
? new CachingSessionStore(store, { ttlSec: config.sessionCacheTtlSec, onStaleHit: metrics ? () => metrics.inc("session_cache_stale_hit_total") : undefined })
|
|
10
10
|
: store;
|
|
11
11
|
}
|
|
12
12
|
return new TtlSessionStore({ defaultTtlDays: 7 });
|
|
@@ -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
|
/**
|
|
@@ -5,6 +5,7 @@ import { type OutcomeSink } from "./file-outcome-sink.js";
|
|
|
5
5
|
import { type InboxWarn } from "./workflow-run-store-sql.js";
|
|
6
6
|
import { FileRunStore } from "./file-run-store.js";
|
|
7
7
|
import { type OneTimeMigrationReport } from "./one-time-migrations.js";
|
|
8
|
+
import type { SessionHeadReader } from "./caching-session-store.js";
|
|
8
9
|
import { LocalCheckpointStore } from "./local-checkpoint-store.js";
|
|
9
10
|
import { FileResumeAnchorStore } from "./file-resume-anchor-store.js";
|
|
10
11
|
import { type ApprovalExemptionStore } from "./approval-exemption-store.js";
|
|
@@ -144,7 +145,11 @@ export interface StoreBackend {
|
|
|
144
145
|
/** Protocol-truth kind (clay 2026-07-06 wording): "mysql" = any MySQL-protocol server (MySQL / TiDB /
|
|
145
146
|
* MariaDB) behind the one mysql2 pool — the startup summary prints durable(mysql) for all of them. */
|
|
146
147
|
readonly kind: "mysql" | "pg" | "local";
|
|
147
|
-
/** Connect + create schema (also the reachability probe).
|
|
148
|
+
/** Connect + create schema (also the reachability probe). On the SQL backends it first checks the database for the
|
|
149
|
+
* schema that the one-time migrations of server 7.74.0–7.125.x retired and **refuses** with `RetiredSchemaPresentError`
|
|
150
|
+
* (code `config.schema_mismatch`, exported from the package root) before it issues the first CREATE; only then does it create.
|
|
151
|
+
* The check lives in this method itself, so the public sequence `createStoreBackend()` → `ensureSchema()` → `runOneTimeMigrations()`
|
|
152
|
+
* needs no step of its own. The LOCAL backend has no SQL pool and nothing to probe. */
|
|
148
153
|
ensureSchema(): Promise<void>;
|
|
149
154
|
close(): Promise<void>;
|
|
150
155
|
run(): RunStore;
|
|
@@ -337,8 +342,9 @@ export interface StoreBackend {
|
|
|
337
342
|
breaker?(onWriteFail?: (streak: number) => void): BreakerStateStore;
|
|
338
343
|
costQuota?(limitMicroUsd: number, windowMs: number, onDegraded?: CounterDegradeHook): CostQuotaStore;
|
|
339
344
|
rateLimiter?(limit: number, onDegraded?: CounterDegradeHook): RateLimiterStore;
|
|
340
|
-
/** Session store (caller wraps caching). DB-backed on tidb/pg; the in-memory/file `LocalSessionStore` on local.
|
|
341
|
-
|
|
345
|
+
/** Session store (caller wraps caching). DB-backed on tidb/pg; the in-memory/file `LocalSessionStore` on local. Carries the
|
|
346
|
+
* head read the warm cache checks every hit against. */
|
|
347
|
+
session(): SessionStore & SessionHeadReader;
|
|
342
348
|
/** The raw mysql2 pool — ONLY on the TiDB backend (undefined on PG). For the few TiDB-specific raw-SQL
|
|
343
349
|
* consumers outside the store abstraction (session audit, degenerate-output instrument); those features are
|
|
344
350
|
* no-ops on PG until ported. */
|
|
@@ -381,8 +387,9 @@ export declare function assertCloudSnapshotBlobPosture(kind: "mysql" | "pg" | "l
|
|
|
381
387
|
* 而兄弟行都写 `shared(${kind})`。)
|
|
382
388
|
*/
|
|
383
389
|
export declare function counterStoreLabel(store: unknown, kind: string | undefined): string;
|
|
384
|
-
/** Build the durable-store backend for `config.dbBackend`. Does NOT connect (the pool connects lazily; the
|
|
385
|
-
*
|
|
390
|
+
/** Build the durable-store backend for `config.dbBackend`. Does NOT connect (the pool connects lazily); the next
|
|
391
|
+
* step is `ensureSchema()` (retired-schema check, then the CREATEs; it is also the reachability probe), then
|
|
392
|
+
* `runOneTimeMigrations()`. Returns undefined when no DB is configured.
|
|
386
393
|
*
|
|
387
394
|
* `onWarn` (additive, optional) is the boot-time warning sink the LOCAL backend needs for core's
|
|
388
395
|
* permission-rule DISCLOSURE face ([ref] §1): an unreadable / checksum-mismatched / symlinked rule file
|
|
@@ -12,7 +12,7 @@ import { FileRunStore } from "./file-run-store.js";
|
|
|
12
12
|
import { StoreBootRefusalError } from "./store-contracts.js";
|
|
13
13
|
import { buildStoreOpenRefusalTail } from "./store-open-refusal.js";
|
|
14
14
|
import { acquireLocalBackendWithWait } from "./store-lock-wait.js";
|
|
15
|
-
import { NO_SQL_MIGRATIONS, runSqlOneTimeMigrations } from "./one-time-migrations.js";
|
|
15
|
+
import { NO_SQL_MIGRATIONS, assertRetiredSchemaAbsentOnPool, runSqlOneTimeMigrations } from "./one-time-migrations.js";
|
|
16
16
|
import { LocalSessionStore, createLocalSessionRepo } from "./local-session-store.js";
|
|
17
17
|
import { LocalCheckpointStore } from "./local-checkpoint-store.js";
|
|
18
18
|
import { FileResumeAnchorStore } from "./file-resume-anchor-store.js";
|
|
@@ -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();
|
|
@@ -337,11 +337,20 @@ export function createStoreBackend(config, onWarn, onError) {
|
|
|
337
337
|
if (!config.pg)
|
|
338
338
|
return undefined;
|
|
339
339
|
const pool = createPgPool(pgPoolOptions(config.pg, config.dbQueryTimeoutMs), onWarn);
|
|
340
|
-
return new PgBackend(pool, config);
|
|
340
|
+
return withRetiredSchemaProbe(new PgBackend(pool, config), { dialect: "pg", pool });
|
|
341
341
|
}
|
|
342
342
|
if (!config.tidb)
|
|
343
343
|
return undefined;
|
|
344
|
-
|
|
344
|
+
const pool = createTidbPool(config);
|
|
345
|
+
return withRetiredSchemaProbe(new TiDBBackend(pool, config), { dialect: "tidb", pool });
|
|
346
|
+
}
|
|
347
|
+
function withRetiredSchemaProbe(inner, target) {
|
|
348
|
+
const guarded = Object.create(inner);
|
|
349
|
+
guarded.ensureSchema = async () => {
|
|
350
|
+
await assertRetiredSchemaAbsentOnPool(target);
|
|
351
|
+
await inner.ensureSchema();
|
|
352
|
+
};
|
|
353
|
+
return guarded;
|
|
345
354
|
}
|
|
346
355
|
export async function openStoreBackendWithFallback(config, logger) {
|
|
347
356
|
const sqlEngine = config.dbBackend === "mysql" || config.dbBackend === "pg" ? config.dbBackend : undefined;
|
|
@@ -36,6 +36,9 @@ import type { PeerSessionTempo, TaskResult, TaskStatus } from "@sema-agent/core"
|
|
|
36
36
|
* 收法:**一个基类,一条重抛**。新 File 店的拒启只要 `extends StoreBootRefusalError` 就自动落在正确的
|
|
37
37
|
* 一侧;忘了继承是**可见**的(它是一个必须显式选择的父类),而忘了往 if 链里加一行是不可见的。
|
|
38
38
|
* (core 的 `AdoptionError` 不在本仓,改不了它的继承链 ⇒ 降级臂保留它那一条 `instanceof`,共两条。)
|
|
39
|
+
*
|
|
40
|
+
* 族的含义是「**确定的拒启表态**」,不限 File 店:SQL `ensureSchema()` 里库带着已退役 schema 的拒启(`RetiredSchemaPresentError`)也属此族。三处读它,读法一致 ——
|
|
41
|
+
* 降级臂不吞(穿过 `mayFallback`)、SQL 开库的等待窗不重试它(`store-open-wait.ts`:再等也等不来另一个答案)、「库打不开」的拒句不改写它(它自己的拒句才是真话)。
|
|
39
42
|
*/
|
|
40
43
|
export declare class StoreBootRefusalError extends Error {
|
|
41
44
|
}
|
|
@@ -221,17 +224,31 @@ export interface RunRecord {
|
|
|
221
224
|
createdAt: string;
|
|
222
225
|
updatedAt: string;
|
|
223
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* 一次提交在 run 账本上的**绑定** —— 幂等键 + 绑定它的那次请求的体指纹。两样同铸(提交面读到 `Idempotency-Key` 的同一处)、
|
|
229
|
+
* 同落一行(`task_run.idem_key` / `idem_body_fingerprint`)、同清(`releaseIdemKey`),在飞合流也按这一对分区。
|
|
230
|
+
*
|
|
231
|
+
* · `key` —— `idemKeyOf(raw, caller, "run")`(`http/idempotency.ts`;sha256 hex 定宽 64)。
|
|
232
|
+
* · `bodyFingerprint` —— 绑定它的那次请求的体指纹(sha256 hex 定宽 64):HTTP 三条提交腿 = `submissionFingerprintOf`
|
|
233
|
+
* (同一只规范化函数);A2A `message/send` = 这条消息的内容指纹(它同时是 A2A 键的原始键 ⇒ 同键必同体)。
|
|
234
|
+
*
|
|
235
|
+
* 幂等 = 同一调用方、同一键、**同一请求**:命中账本而指纹不同 ⇒ 提交面的唯一回执函数答 409(`http/submit-replay.ts`)。
|
|
236
|
+
*/
|
|
237
|
+
export interface RunIdemBinding {
|
|
238
|
+
readonly key: string;
|
|
239
|
+
readonly bodyFingerprint: string;
|
|
240
|
+
}
|
|
224
241
|
/**
|
|
225
242
|
* `RunStore.createRun` 的行元数据(写进 `task_run` 的附加列)。
|
|
226
243
|
*
|
|
227
|
-
* `
|
|
228
|
-
*
|
|
244
|
+
* `idem` = 这次提交的账本绑定({@link RunIdemBinding})。缺席 / `null` = 调用方没带 `Idempotency-Key` ⇒ 两列 NULL,不参与去重
|
|
245
|
+
* (UNIQUE 不约束 NULL,两方言同形)。
|
|
229
246
|
*/
|
|
230
247
|
export interface NewRunMeta {
|
|
231
248
|
jobId?: string | null;
|
|
232
249
|
source?: string | null;
|
|
233
250
|
objectivePreview?: string | null;
|
|
234
|
-
|
|
251
|
+
idem?: RunIdemBinding | null;
|
|
235
252
|
}
|
|
236
253
|
/**
|
|
237
254
|
* `RunStore.createRun` 的结局 —— 三形,一形一句话:
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { STORE_LOCK_POLL_MS } from "./store-lock-wait.js";
|
|
2
|
+
import { StoreBootRefusalError } from "./store-contracts.js";
|
|
2
3
|
const REAL_CLOCK = {
|
|
3
4
|
now: () => performance.now(),
|
|
4
5
|
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
@@ -46,6 +47,8 @@ export async function openWithinWait(open, waitMs, onWait, clock = REAL_CLOCK) {
|
|
|
46
47
|
throw lastFailure.error;
|
|
47
48
|
throw new StoreOpenWindowExpired(waitMs);
|
|
48
49
|
case "failed": {
|
|
50
|
+
if (outcome.error instanceof StoreBootRefusalError)
|
|
51
|
+
throw outcome.error;
|
|
49
52
|
if (lastFailure === undefined)
|
|
50
53
|
onWait({ waitMs, error: outcome.error instanceof Error ? outcome.error.message : String(outcome.error) });
|
|
51
54
|
lastFailure = { error: outcome.error };
|
|
@@ -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
|