@sema-agent/server 7.138.0 → 7.139.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 +276 -7
- package/README.md +7 -4
- package/README.zh-CN.md +6 -4
- package/USAGE.md +73 -29
- package/deploy/sema-up/README.md +1 -1
- package/deploy/sema-up/lib/lanes.json +3 -2
- package/dist/adoption/wire.d.ts +3 -3
- package/dist/approval-card.d.ts +20 -20
- package/dist/bench/s1/arms.d.ts +1 -1
- package/dist/bench/s1/arms.js +1 -3
- package/dist/bench/s1/run-firm.js +2 -2
- package/dist/boot/device-lane.d.ts +8 -3
- package/dist/boot/device-lane.js +7 -2
- package/dist/boot/execution-env.d.ts +4 -2
- package/dist/boot/execution-env.js +16 -24
- package/dist/boot/execution-lane.d.ts +1 -1
- package/dist/boot/governance-seams.d.ts +3 -2
- package/dist/boot/governance-seams.js +9 -11
- package/dist/boot/memory-consolidation.js +2 -2
- package/dist/boot/resolve-spec.d.ts +1 -1
- package/dist/boot/resolve-spec.js +8 -9
- package/dist/boot/retention-lane.d.ts +5 -5
- package/dist/boot/retention-lane.js +0 -6
- package/dist/boot/runner-deps.d.ts +3 -1
- package/dist/boot/runner-deps.js +2 -1
- package/dist/boot/session-faces.d.ts +0 -3
- package/dist/boot/session-faces.js +15 -8
- package/dist/boot/stage-02-web-admin-pull.js +2 -2
- package/dist/boot/stage-05-execution-env.js +2 -2
- package/dist/boot/stage-06-runners.js +0 -1
- package/dist/boot/stage-09-leader.js +1 -2
- package/dist/boot/stage-10-http-server.js +2 -2
- package/dist/boot/stores.js +4 -4
- package/dist/boot/web-admin.js +2 -2
- package/dist/capabilities/mcp-probe.js +1 -1
- package/dist/capabilities/scenarios.d.ts +1 -1
- package/dist/capability-facts.d.ts +15 -7
- package/dist/capability-facts.js +8 -3
- package/dist/config-catalog.js +7 -7
- package/dist/config-invariants.d.ts +23 -3
- package/dist/config-invariants.js +42 -0
- package/dist/config-provider.d.ts +19 -24
- package/dist/config-provider.js +27 -12
- package/dist/config-types.d.ts +10 -5
- package/dist/config.d.ts +33 -0
- package/dist/config.js +44 -15
- package/dist/delegated-session-owner.d.ts +2 -2
- package/dist/deployment-shape.d.ts +1 -1
- package/dist/device-enrollment.d.ts +41 -2
- package/dist/device-enrollment.js +27 -12
- package/dist/device-store.d.ts +58 -85
- package/dist/device-store.js +19 -49
- package/dist/device-ws-hub.js +2 -1
- package/dist/env-name-allowlist-knobs.d.ts +5 -4
- package/dist/exec-cut.d.ts +60 -0
- package/dist/exec-cut.js +36 -0
- package/dist/execution-lane-caps.d.ts +19 -0
- package/dist/execution-lane-caps.js +10 -0
- package/dist/execution-placement.d.ts +259 -0
- package/dist/execution-placement.js +199 -0
- package/dist/field-conflict.d.ts +3 -2
- package/dist/field-conflict.js +1 -0
- package/dist/hosted-posture.d.ts +2 -2
- package/dist/http/admission.js +8 -19
- package/dist/http/checkpoint-ctx.d.ts +21 -12
- package/dist/http/checkpoint-ctx.js +4 -2
- package/dist/http/claimed-run.d.ts +1 -1
- package/dist/http/durable-run-leg.js +9 -5
- package/dist/http/execution-context.d.ts +218 -0
- package/dist/http/execution-context.js +330 -0
- package/dist/http/resume-legs.js +50 -20
- package/dist/http/route-ctx.d.ts +14 -8
- package/dist/http/routes/background.d.ts +29 -1
- package/dist/http/routes/background.js +29 -2
- package/dist/http/routes/devices.d.ts +13 -19
- package/dist/http/routes/devices.js +21 -26
- package/dist/http/routes/session-sync.js +110 -15
- package/dist/http/routes/sessions.d.ts +6 -0
- package/dist/http/routes/sessions.js +115 -11
- package/dist/http/routes/side-query.d.ts +12 -4
- package/dist/http/routes/side-query.js +4 -5
- package/dist/http/routes/tasks.js +37 -31
- package/dist/http/server.d.ts +35 -19
- package/dist/http/server.js +11 -0
- package/dist/http/session-inherit-env.d.ts +3 -4
- package/dist/http/session-inherit-env.js +1 -1
- package/dist/http/wire-types.d.ts +8 -12
- package/dist/index.d.ts +1 -0
- package/dist/memory-sync-client.d.ts +53 -14
- package/dist/memory-sync-client.js +196 -116
- package/dist/memory-sync.d.ts +1 -1
- package/dist/memory-sync.js +3 -2
- package/dist/observability/cost-taxonomy.d.ts +2 -2
- package/dist/observability/fail-open.d.ts +12 -0
- package/dist/observability/fail-open.js +12 -0
- package/dist/observability/run-terminal.js +0 -1
- package/dist/orchestration/workflow-notify-journal.js +4 -4
- package/dist/org-memory-admission.d.ts +4 -3
- package/dist/org-memory-admission.js +6 -6
- package/dist/parked-decide.d.ts +34 -9
- package/dist/parked-decide.js +12 -19
- package/dist/parked-revive-surface.d.ts +12 -15
- package/dist/parked-revive-surface.js +3 -6
- package/dist/peer-seat.d.ts +13 -7
- package/dist/peer-seat.js +37 -3
- package/dist/plugins/approval-ask-store-file.d.ts +7 -0
- package/dist/plugins/approval-ask-store-file.js +11 -0
- package/dist/plugins/approval-ask-store-memory.d.ts +6 -0
- package/dist/plugins/approval-ask-store-memory.js +12 -0
- package/dist/plugins/approval-ask-store-sql.d.ts +22 -5
- package/dist/plugins/approval-ask-store-sql.js +30 -12
- package/dist/plugins/caching-session-store.d.ts +6 -2
- package/dist/plugins/caching-session-store.js +9 -3
- package/dist/plugins/checkpoint-store-sql.d.ts +6 -5
- package/dist/plugins/checkpoint-store-sql.js +5 -7
- package/dist/plugins/device-store-sql.d.ts +25 -51
- package/dist/plugins/device-store-sql.js +49 -103
- package/dist/plugins/execution-placement-sql.d.ts +151 -0
- package/dist/plugins/execution-placement-sql.js +223 -0
- package/dist/plugins/file-history-store-sql.d.ts +7 -6
- package/dist/plugins/file-history-store-sql.js +5 -5
- package/dist/plugins/file-run-store.d.ts +6 -1
- package/dist/plugins/file-run-store.js +4 -0
- package/dist/plugins/fork-routing-session-store.d.ts +6 -0
- package/dist/plugins/fork-routing-session-store.js +16 -0
- package/dist/plugins/local-session-store.d.ts +149 -50
- package/dist/plugins/local-session-store.js +382 -107
- package/dist/plugins/memory-key-guards.d.ts +4 -0
- package/dist/plugins/memory-key-guards.js +1 -0
- package/dist/plugins/memory-run-store.d.ts +1 -1
- package/dist/plugins/one-time-migrations.d.ts +111 -3
- package/dist/plugins/one-time-migrations.js +380 -10
- package/dist/plugins/outcome-ledger-sql.d.ts +2 -2
- package/dist/plugins/permission-rule-store-sql.d.ts +13 -15
- package/dist/plugins/permission-rule-store-sql.js +13 -16
- package/dist/plugins/pg-pool.d.ts +7 -3
- package/dist/plugins/pg-session-storage.d.ts +31 -20
- package/dist/plugins/pg-session-storage.js +120 -52
- package/dist/plugins/remote-env-adb.d.ts +20 -4
- package/dist/plugins/remote-env-adb.js +70 -62
- package/dist/plugins/remote-env-device.d.ts +30 -12
- package/dist/plugins/remote-env-device.js +18 -17
- package/dist/plugins/remote-env-e2b.d.ts +18 -4
- package/dist/plugins/remote-env-e2b.js +41 -42
- package/dist/plugins/remote-env-file-error.d.ts +2 -0
- package/dist/plugins/remote-env-file-error.js +1 -1
- package/dist/plugins/remote-env-host.d.ts +21 -8
- package/dist/plugins/remote-env-host.js +66 -70
- package/dist/plugins/remote-env-k8s.d.ts +11 -16
- package/dist/plugins/remote-env-k8s.js +61 -54
- package/dist/plugins/remote-env-local-docker.d.ts +29 -6
- package/dist/plugins/remote-env-local-docker.js +46 -59
- package/dist/plugins/remote-env-ssh.d.ts +16 -4
- package/dist/plugins/remote-env-ssh.js +72 -56
- package/dist/plugins/remote-shell.d.ts +100 -3
- package/dist/plugins/remote-shell.js +166 -2
- package/dist/plugins/retention-store-sql.d.ts +9 -6
- package/dist/plugins/retention-store-sql.js +7 -1
- package/dist/plugins/run-store-sql.d.ts +13 -3
- package/dist/plugins/run-store-sql.js +5 -0
- package/dist/plugins/schema-consistency.d.ts +15 -5
- package/dist/plugins/schema-consistency.js +14 -8
- package/dist/plugins/schema-drift-sentence.d.ts +27 -0
- package/dist/plugins/schema-drift-sentence.js +38 -0
- package/dist/plugins/session-fork.d.ts +54 -0
- package/dist/plugins/session-fork.js +38 -1
- package/dist/plugins/session-row-sql.d.ts +22 -0
- package/dist/plugins/session-row-sql.js +81 -0
- package/dist/plugins/session-store.d.ts +1 -1
- package/dist/plugins/store-backend.d.ts +1 -1
- package/dist/plugins/store-backend.js +19 -7
- package/dist/plugins/store-contracts.d.ts +25 -5
- package/dist/plugins/tidb-pool.d.ts +12 -7
- package/dist/plugins/tidb-pool.js +9 -3
- package/dist/plugins/tidb-session-store.d.ts +57 -26
- package/dist/plugins/tidb-session-store.js +118 -50
- package/dist/plugins/tool-result-store-sql.d.ts +3 -2
- package/dist/plugins/tool-result-store-sql.js +18 -10
- package/dist/project-memory.d.ts +9 -15
- package/dist/project-memory.js +5 -4
- package/dist/request-key-closure.js +1 -1
- package/dist/request-value-closure.d.ts +6 -7
- package/dist/request-value-closure.js +1 -3
- package/dist/run-local.d.ts +6 -0
- package/dist/run-local.js +24 -20
- package/dist/runs.d.ts +2 -1
- package/dist/runs.js +3 -1
- package/dist/security.d.ts +38 -12
- package/dist/security.js +8 -2
- package/dist/server-secret-env.d.ts +9 -5
- package/dist/session-children-retire.d.ts +27 -9
- package/dist/session-children-retire.js +9 -5
- package/dist/session-incarnation.d.ts +19 -9
- package/dist/session-incarnation.js +4 -2
- package/dist/session-row.d.ts +114 -5
- package/dist/session-row.js +55 -5
- package/dist/session-titler.d.ts +2 -2
- package/dist/task-a2a.d.ts +1 -1
- package/dist/task-cwd.d.ts +6 -1
- package/dist/task-mcp.d.ts +3 -3
- package/dist/tool-approval.d.ts +6 -0
- package/dist/tool-approval.js +8 -1
- package/dist/trace/project.d.ts +1 -0
- package/dist/trace/project.js +1 -0
- package/dist/trace/self-minted-wire.d.ts +111 -2
- package/dist/trace/self-minted-wire.js +38 -1
- package/dist/url-refusal.d.ts +1 -1
- package/dist/web-admin-client/apply-effective.d.ts +17 -3
- package/dist/web-admin-client/apply-effective.js +20 -10
- package/dist/web-admin-client/skills-mcp.js +3 -2
- package/dist/web-admin-client/types.d.ts +6 -0
- package/package.json +3 -3
- package/wire-changes/server@7.139.0.json +348 -0
- package/dist/http/device-placement.d.ts +0 -175
- package/dist/http/device-placement.js +0 -145
|
@@ -3,6 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
import { applyTombstones, assertDeleteDeltaCarriesNoAdd, assertRedemptionNotQuarantined, assertWriteDeltaScopeDurable, foldDelta, isValidDurableScope, parseRuleText, readRowBehavior, screenRuleSyncState, PERMISSION_RULE_WRITER, RULE_ADD_ORIGINS, RULE_APPROVAL_KINDS, RULE_APPROVAL_RECORD_SCHEMA, RULE_APPROVAL_STATES, RULE_BEHAVIORS, RULE_SYNC_DROP_CODES, EDITED_RULE_BREADTH_WARNING_CODES, PERSISTED_RULE_MATCHES, EMPTY_DURABLE_RULE_PARTITION, PERMISSION_RULES_LATER_GRAMMAR_CODE, SESSION_RULES_GRAMMAR, readRuleGrammarStamp, readRuleOwner, principalNameOf, readRuleOffer2, } from "@sema-agent/core";
|
|
4
4
|
import { dialectProtocolJsonEncoder, sqlDbNowMs, SQL_DB_NOW_MS } from "./sql-driver.js";
|
|
5
5
|
import { isMissingColumnError } from "./sql-errors.js";
|
|
6
|
+
import { describeProbedTableDrift } from "./schema-drift-sentence.js";
|
|
6
7
|
import { canonicalJsonStringify } from "@sema-agent/core";
|
|
7
8
|
import { ensureIndexes } from "./ensure-index.js";
|
|
8
9
|
import { createZodColumnReader, decodeJsonColumn, foldJsonColumn, jsonColumnText, readJsonObjectColumn, SqlJsonColumnError } from "./sql-json-column.js";
|
|
@@ -144,8 +145,8 @@ export const TIDB_PERMISSION_RULE_STATEMENTS = [
|
|
|
144
145
|
-- version:**数据格式版本**(= core \`RuleApprovalRecord.schema\`,本形恒 4 —— core 7.9.0 #625 由 3 升,
|
|
145
146
|
-- 代码里的单点是 RULE_APPROVAL_RECORD_SCHEMA;OCC 守卫另在 rev,两轴禁混,schema-naming 门③ 白名单
|
|
146
147
|
-- 登记)。读侧对非 4 的行(含上一形的 3)交还 design/375 §4.5 的
|
|
147
|
-
-- StaleRuleApprovalRecord 信封(identity 键 only)——
|
|
148
|
-
--
|
|
148
|
+
-- StaleRuleApprovalRecord 信封(identity 键 only)—— 这一臂是「未来再换形」的防御位,
|
|
149
|
+
-- 绝不静默 widen 成一条本形担保不了的记录。
|
|
149
150
|
version INT NOT NULL,
|
|
150
151
|
-- offers_json:core \`RuleOffer2[]\`(design/375 §4.1)—— 按 index 引用 candidates 的 OPTION 结构,
|
|
151
152
|
-- **绝不存规则文本副本**(显示三元组由 core \`ruleOffersOfRecord\` 从候选文本投影;存副本=造一个
|
|
@@ -240,13 +241,11 @@ export async function assertPermissionRuleApprovalSchema(query, dialect) {
|
|
|
240
241
|
`#340 card-edit columns (command / edited_json) and the design/375 record-form columns ` +
|
|
241
242
|
`(version / offers_json / selected_offer): the approval record now stores its OPTION structure and the ` +
|
|
242
243
|
`chosen offer, and the store refuses to read rows whose record-form version it cannot vouch for. ` +
|
|
243
|
-
`This
|
|
244
|
-
`
|
|
245
|
-
|
|
246
|
-
`
|
|
247
|
-
|
|
248
|
-
`loses in-flight CC-import previews (re-runnable) and the historical record trail. ` +
|
|
249
|
-
`Underlying probe error: ${err instanceof Error ? err.message : String(err)}`);
|
|
244
|
+
`This table holds pending/settled approval RECORDS (the audit trail of who said yes to which ` +
|
|
245
|
+
`rule), not the rules themselves — persisted rules live in ${PERMISSION_RULE_TABLE} and are untouched; ` +
|
|
246
|
+
`an in-place ALTER keeps the record trail.\n` +
|
|
247
|
+
`Underlying probe error: ${err instanceof Error ? err.message : String(err)}\n\n` +
|
|
248
|
+
(await describeProbedTableDrift(query, dialect, [PERMISSION_RULE_APPROVAL_TABLE])));
|
|
250
249
|
}
|
|
251
250
|
}
|
|
252
251
|
export async function assertPermissionRuleTicketSchema(query, dialect) {
|
|
@@ -260,12 +259,10 @@ export async function assertPermissionRuleTicketSchema(query, dialect) {
|
|
|
260
259
|
`the #265 A7 three-state redemption columns (MINTED → CLAIMED → SETTLED, the claim generation that fences a superseded ` +
|
|
261
260
|
`claimant, and the lane binding that keeps cc-import and local-import tickets from being interchangeable): without them a ` +
|
|
262
261
|
`local-import ticket can be claimed but never settled, so a redemption that already wrote rules to the store answers its ` +
|
|
263
|
-
`owner with a retry-later it can never satisfy. This
|
|
264
|
-
|
|
265
|
-
`
|
|
266
|
-
|
|
267
|
-
`persisted rules live in ${PERMISSION_RULE_TABLE} and are untouched. ` +
|
|
268
|
-
`Underlying probe error: ${err instanceof Error ? err.message : String(err)}`);
|
|
262
|
+
`owner with a retry-later it can never satisfy. This table holds minute-TTL single-use import ` +
|
|
263
|
+
`tickets (a governance transient that dies on expiry); persisted rules live in ${PERMISSION_RULE_TABLE} and are untouched.\n` +
|
|
264
|
+
`Underlying probe error: ${err instanceof Error ? err.message : String(err)}\n\n` +
|
|
265
|
+
(await describeProbedTableDrift(query, dialect, [PERMISSION_RULE_TICKET_TABLE])));
|
|
269
266
|
}
|
|
270
267
|
}
|
|
271
268
|
export const PERMISSION_RULE_INDEXES = [
|
|
@@ -537,7 +534,7 @@ class SqlDurableRulePartition {
|
|
|
537
534
|
},
|
|
538
535
|
apply: async (delta, opts) => {
|
|
539
536
|
if (delta.kind === "sync-join") {
|
|
540
|
-
throw new Error("the SQL permission-rule backend does not implement the sync-join landing
|
|
537
|
+
throw new Error("the SQL permission-rule backend does not implement the sync-join landing arm; " +
|
|
541
538
|
"no sync client is wired in this deployment, so this arm has no caller — see permission-rule-store-sql.ts header");
|
|
542
539
|
}
|
|
543
540
|
assertWriteDeltaScopeDurable(delta);
|
|
@@ -10,9 +10,13 @@
|
|
|
10
10
|
* (run-store + checkpoint); the remaining tidb-* tables are added as their PG stores land (tracked).
|
|
11
11
|
*
|
|
12
12
|
* ─────────────────────────────── SCHEMA POLICY (clay 裁 2026-07-26) ───────────────────────────────
|
|
13
|
-
* THIS CODE IS THE SINGLE SOURCE OF TRUTH for the PG schema
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* THIS CODE IS THE SINGLE SOURCE OF TRUTH for the PG schema, and there is NO in-place migration seam in this statement array: a database that
|
|
14
|
+
* lags behind the code is refused at boot with the exact `ALTER TABLE … ADD COLUMN …` statements printed, and the
|
|
15
|
+
* deployer runs them themselves: the column-set check and its refusal never alter a database. The only DDL this
|
|
16
|
+
* process runs by itself on a table that already exists is idempotent index creation (`ensureIndex`: indexes only,
|
|
17
|
+
* never a column) and the registered one-time migrations (each has a window and a registry row in
|
|
18
|
+
* `one-time-migrations.ts`) — adding / changing / renaming a column or dropping a table is done by those migrations
|
|
19
|
+
* and by nothing else.
|
|
16
20
|
*
|
|
17
21
|
* Consequence, and it is a hard rule: **do NOT add new `ALTER TABLE` seams here.** Every column / default /
|
|
18
22
|
* constraint change is folded into its own `CREATE TABLE`, and this array stays pure `CREATE TABLE` +
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
import { BaseSessionStorage, type AcquiredSession, type PlacedSessionRow, type SessionHead, type SessionMetadata, type SessionPlacementRecord, type SessionStore, type SessionTreeEntry, type SessionWriteOptions } from "@sema-agent/core";
|
|
2
2
|
import type { Pool, PoolClient } from "pg";
|
|
3
3
|
import type { SessionSummary } from "./store-contracts.js";
|
|
4
|
-
import { type SessionRowPatch } from "../session-row.js";
|
|
4
|
+
import { type ClientSessionRowWrite, type ReplaySessionRowWrite, type ReplaySessionRowWriteOutcome, type SessionRowPatch, type SessionRowWriteOutcome } from "../session-row.js";
|
|
5
5
|
import { type PlacementAcquireOpts } from "./session-placement.js";
|
|
6
6
|
import { type OrphanOwnerLog } from "../delegated-session-owner.js";
|
|
7
7
|
import { type StagingHandle } from "../session-sync-kernel.js";
|
|
8
8
|
import { type InstructionSourceBaseline } from "../instruction-source-notice.js";
|
|
9
9
|
import type { SessionWorkspace } from "../session-workspace.js";
|
|
10
10
|
import type { RetentionDeclaration } from "@sema-agent/core";
|
|
11
|
-
import { type SessionFace } from "../session-incarnation.js";
|
|
12
11
|
import { type IndexSpec } from "./ensure-index.js";
|
|
12
|
+
import { type ForkTarget, type ForkToOutcome } from "./session-fork.js";
|
|
13
|
+
import { type ExecutionPlacementSeat } from "../execution-placement.js";
|
|
13
14
|
/**
|
|
14
15
|
* PG-dialect DDL for the L1 session log, translated from tidb-pool.ts SCHEMA_STATEMENTS:
|
|
15
16
|
* JSON→JSONB, DATETIME(3)→TIMESTAMPTZ(3), inline `KEY`/`UNIQUE KEY`→separate CREATE INDEX (UNIQUE for uq_session_event_entry).
|
|
16
17
|
*
|
|
17
|
-
* SCHEMA POLICY: see the header of pg-pool.ts — the code is the single source of truth,
|
|
18
|
-
*
|
|
18
|
+
* SCHEMA POLICY: see the header of pg-pool.ts — the code is the single source of truth, there is no in-place
|
|
19
|
+
* migration, and NO new `ALTER TABLE` seams are added here; fold into the CREATE instead.
|
|
19
20
|
*/
|
|
20
21
|
export declare const PG_SESSION_SCHEMA: string[];
|
|
21
22
|
/** [ref]:本 store 的索引**声明**(两方言共用一份)。PG 侧由下面的 `ensureSchema` 应用,MySQL 侧由 `tidb-pool.ts` 的中央 `ensureSchema` 应用(那里内联 `KEY` 已在 `CREATE TABLE` 里 ⇒ 新建库探到即零 DDL,存量库缺谁补谁)。加索引以外的 schema 变更仍归运维,见 `plugins/ensure-index.ts` 头注。 */
|
|
@@ -70,6 +71,10 @@ export declare class PgSessionStore implements SessionStore {
|
|
|
70
71
|
private readonly pending;
|
|
71
72
|
/** 委派子代孤行那一行 warn 的去处(TiDB 孪生同格)。 */
|
|
72
73
|
private readonly log;
|
|
74
|
+
/** 执行席(TiDB 孪生同格;实现与语句的唯一属主 = `execution-placement-sql.ts`,本店只交自己的池)。 */
|
|
75
|
+
readonly executionPlacement: ExecutionPlacementSeat;
|
|
76
|
+
/** 本店的池包成的 SqlDriver(TiDB 孪生同格):执行席与会话行格写原语的事务都经它借连接。 */
|
|
77
|
+
private readonly db;
|
|
73
78
|
constructor(pool: Pool, opts?: {
|
|
74
79
|
readonly log?: OrphanOwnerLog;
|
|
75
80
|
readonly retention?: RetentionDeclaration;
|
|
@@ -121,13 +126,14 @@ export declare class PgSessionStore implements SessionStore {
|
|
|
121
126
|
}): Promise<SessionSummary[]>;
|
|
122
127
|
/** @see TiDBSessionStore.sessionSummaryOf — the PG twin (same projection, run-preview subqueries scoped to the id). */
|
|
123
128
|
sessionSummaryOf(sessionId: string): Promise<SessionSummary | undefined>;
|
|
124
|
-
/** @see TiDBSessionStore.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
+
/** @see TiDBSessionStore.listRowOf — the PG twin. */
|
|
130
|
+
private listRowOf;
|
|
131
|
+
/** @see TiDBSessionStore.writeSessionRowCells — the PG twin (the same SQL half, `session-row-sql.ts`; only the statement text differs). */
|
|
132
|
+
writeSessionRowCells(sessionId: string, cells: SessionRowPatch, how: ClientSessionRowWrite): Promise<SessionRowWriteOutcome<SessionSummary>>;
|
|
133
|
+
writeSessionRowCells(sessionId: string, cells: SessionRowPatch, how: ReplaySessionRowWrite): Promise<ReplaySessionRowWriteOutcome<SessionSummary>>;
|
|
134
|
+
/** @see TiDBSessionStore.selectListRows — the PG twin. `where` already carries `$n` placeholders numbered over `params`;
|
|
129
135
|
* the run scope and the LIMIT take the next numbers (PG binds by number, so text order does not matter). */
|
|
130
|
-
private
|
|
136
|
+
private selectListRows;
|
|
131
137
|
/** @see TiDBSessionStore.probeTitle — the PG twin. */
|
|
132
138
|
probeTitle(sessionId: string): Promise<"none" | "untitled" | "titled">;
|
|
133
139
|
/** @see TiDBSessionStore.setTitleIfNull — the PG twin (the auto-titler's NULL-only write). */
|
|
@@ -143,9 +149,13 @@ export declare class PgSessionStore implements SessionStore {
|
|
|
143
149
|
/** @see TiDBSessionStore.recordSessionWorkspace — the PG twin (whole-cell overwrite fenced to the occupant; `pgSafeJsonStringify` + `::jsonb`, like every PG JSON write;
|
|
144
150
|
* the general query surface — the pool-wide `DB_QUERY_TIMEOUT_MS` server-side `statement_timeout` bounds it, no per-call client timer). */
|
|
145
151
|
recordSessionWorkspace(sessionId: string, incarnation: string, workspace: SessionWorkspace): Promise<void>;
|
|
146
|
-
/** @see TiDBSessionStore.fork —
|
|
147
|
-
* NOT wake's bounded tail) to a NEW uuidv7 session, re-seq dense from 0, ids/parents verbatim, owner=caller. */
|
|
152
|
+
/** @see TiDBSessionStore.fork — core 契约 `fork(sourceId, owner?)` = {@link forkTo} 的投影(新铸 uuidv7 目标,`forkByFreshId`;三店同一只)。 */
|
|
148
153
|
fork(sourceId: string, owner?: string | null): Promise<string | null>;
|
|
154
|
+
/** @see TiDBSessionStore.forkTo — fork 续写的店契约(PG 孪生逐字同义):目标行 meta 先、主键排他;撞已在 ⇒ 回滚后在新视图读目标按属主与 fork 键分类;
|
|
155
|
+
* 读不到 ⇒ 整笔重跑一次,再不行 ⇒ {@link ForkTargetUnsettledError}。 */
|
|
156
|
+
forkTo(sourceId: string, owner: string | null, target: ForkTarget): Promise<ForkToOutcome | null>;
|
|
157
|
+
/** @see TiDBSessionStore.forkToOnce — 一次尝试(PG 孪生):拷**整份**持久日志(压缩地板绕过,不是 wake 的有界尾),seq 从 0 重铸稠密,id / 父 / 载荷原样。 */
|
|
158
|
+
private forkToOnce;
|
|
149
159
|
/** @see TiDBSessionStore.exportEntries — 2c session-sync EXPORT: the FULL durable session_event log (compaction floor
|
|
150
160
|
* bypassed, NOT wake's tail) as `SessionTreeEntry[]`, oldest-first, verbatim. null if the source is unknown. */
|
|
151
161
|
exportEntries(sessionId: string): Promise<SessionTreeEntry[] | null>;
|
|
@@ -163,18 +173,19 @@ export declare class PgSessionStore implements SessionStore {
|
|
|
163
173
|
batchSize?: number;
|
|
164
174
|
}): Promise<AsyncIterable<SessionTreeEntry> | null>;
|
|
165
175
|
/** @see TiDBSessionStore.importEntries — 2c session-sync IMPORT: core `validateEntriesForImport` gate FIRST (fail-closed),
|
|
166
|
-
* then persist its verbatim output (re-seq dense from 0, ids/parents/payload/ts unchanged) in ONE txn
|
|
167
|
-
*
|
|
176
|
+
* then persist its verbatim output (re-seq dense from 0, ids/parents/payload/ts unchanged) in ONE txn, **lock-or-create, meta first**
|
|
177
|
+
* (读写序律:占行 → 锁行判是不是本次占下的 → 锁定读认领 → 事件);owner = the RE-STAMPED authenticated principal (never the bundle's). */
|
|
168
178
|
importEntries(sessionId: string, owner: string | null, entries: SessionTreeEntry[]): Promise<void>;
|
|
169
179
|
/** @see TiDBSessionStore.replaceEntries — 2c (§7/§8) IDEMPOTENT REPLACE: core `validateEntriesForImport` gate FIRST
|
|
170
|
-
* (fail-closed), then
|
|
171
|
-
*
|
|
180
|
+
* (fail-closed), then the owner-guarded meta delete (`owner IS NOT DISTINCT FROM $n` = the PG `<=>`, defense-in-depth), then the
|
|
181
|
+
* SHARED body (session_meta first, the in-txn claim read, then the old events out and the new ones in), ALL in
|
|
172
182
|
* ONE txn. Safe to run OVER an existing session (fast-forward / §8 retry) — unlike importEntries' fresh-only INSERT. */
|
|
173
183
|
replaceEntries(sessionId: string, owner: string | null, entries: SessionTreeEntry[]): Promise<void>;
|
|
174
|
-
/** @see TiDBSessionStore.
|
|
175
|
-
*
|
|
184
|
+
/** @see TiDBSessionStore.writeImportedLog — the shared write half of importEntries / replaceEntries (ONE source, no drift):
|
|
185
|
+
* session_meta FIRST (create = occupy the row, an existing one ⇒ refused; replace = plain INSERT after the caller's owner-guarded
|
|
186
|
+
* delete), the in-txn claim read on THIS connection (读写序律), then the entries re-seq dense from 0 (ids/parents/payload/ts verbatim).
|
|
176
187
|
* Runs inside the caller's open txn (`conn` already in BEGIN); the caller owns COMMIT/ROLLBACK + the validate gate. */
|
|
177
|
-
private
|
|
188
|
+
private writeImportedLog;
|
|
178
189
|
/** @see TiDBSessionStore.beginImportStaging — 2c P1d-β PUSH staging: stream batches into the shadow id
|
|
179
190
|
* `${realSessionId}#stg-${token}` (route-minted token), then commit (atomic swap) / abort. */
|
|
180
191
|
beginImportStaging(realSessionId: string, token: string): StagingHandle;
|
|
@@ -188,7 +199,7 @@ export declare class PgSessionStore implements SessionStore {
|
|
|
188
199
|
sweepStagingSessions(): Promise<number>;
|
|
189
200
|
/** @see TiDBSessionStore.deleteSession — purge session_meta + session_event; idempotent (false if absent).
|
|
190
201
|
* Owner-guarded on session_meta.owner (`IS NOT DISTINCT FROM` = the PG `<=>`); the event purge is gated on the
|
|
191
|
-
* owner-matched meta delete (session_event has no owner column). */
|
|
202
|
+
* owner-matched meta delete (session_event has no owner column). A live run claim read inside the txn ⇒ `SessionActiveRunError`. */
|
|
192
203
|
deleteSession(sessionId: string, owner: string | null): Promise<boolean>;
|
|
193
204
|
get size(): number;
|
|
194
205
|
dispose(): void;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { emitLeafAdvance } from "../session-leaf-bus.js";
|
|
2
2
|
import { BaseSessionStorage, StoredSession, SessionError, uuidv7, validateEntriesForImport, leafIdAfterEntry, admitSessionPlacement, } from "@sema-agent/core";
|
|
3
|
-
import { SESSION_TITLE_MAX_CHARS } from "../session-row.js";
|
|
3
|
+
import { SESSION_TITLE_MAX_CHARS, sessionRowTagKey } from "../session-row.js";
|
|
4
|
+
import { writeSessionRowCellsSql } from "./session-row-sql.js";
|
|
4
5
|
import { escapeLike } from "./sql-escape.js";
|
|
5
6
|
import { decodeSessionEventPayload } from "./session-event-payload.js";
|
|
6
7
|
import { scanEpochAnchor } from "./session-epoch-anchor.js";
|
|
@@ -16,11 +17,15 @@ import { isPgUniqueViolation } from "./sql-errors.js";
|
|
|
16
17
|
import { BASELINE_IO_DEADLINE_MS, decodeInstructionSource, encodeInstructionSource } from "../instruction-source-notice.js";
|
|
17
18
|
import { SESSION_META_WORKSPACE_COLUMN, decodeSessionWorkspaceCell, encodeSessionWorkspaceCell } from "./session-workspace-cell.js";
|
|
18
19
|
import { MANAGED_RETENTION, UNMANAGED_RETENTION } from "./retention-store-sql.js";
|
|
19
|
-
import { SESSION_META_INCARNATION_COLUMN,
|
|
20
|
+
import { SESSION_META_INCARNATION_COLUMN, carriedRowCells, headFromRow, incarnationFromCell, mintSessionIncarnation, sessionFaceFromRow } from "../session-incarnation.js";
|
|
20
21
|
import { ensureIndexes } from "./ensure-index.js";
|
|
21
22
|
import { assertFirstOwnerWritable, forkOwnerOf, isSessionOwnerValue } from "./session-ownership.js";
|
|
22
23
|
import { selectForkRows } from "@sema-agent/core";
|
|
23
|
-
import { FORK_LOG_ROW_SHAPE, forkPointOf } from "./session-fork.js";
|
|
24
|
+
import { FORK_LOG_ROW_SHAPE, ForkTargetUnsettledError, classifyForkTarget, forkByFreshId, forkPointOf } from "./session-fork.js";
|
|
25
|
+
import { pgDriver } from "./sql-driver.js";
|
|
26
|
+
import { MIRROR_ON_COMMIT_GUARD, MIRROR_ON_COMMIT_SET, SESSION_META_PLACEMENT_READ_COLUMNS, createSqlExecutionPlacementSeat, importedRowCells, lockRootAndReadClaim, placementCellFromRow, readPlacementRowStatement, sessionMetaPlacementColumnsDdl } from "./execution-placement-sql.js";
|
|
27
|
+
import { SessionActiveRunError, SessionWriterPresentError } from "../execution-placement.js";
|
|
28
|
+
const rowsOnClient = (conn) => async (sql, values) => (await conn.query(sql, [...values])).rows;
|
|
24
29
|
const isDupKey = isPgUniqueViolation;
|
|
25
30
|
function baselineQuery(text, values) {
|
|
26
31
|
return { text, values, query_timeout: BASELINE_IO_DEADLINE_MS };
|
|
@@ -34,7 +39,7 @@ export const PG_SESSION_SCHEMA = [
|
|
|
34
39
|
leaf_id VARCHAR(64) COLLATE "C",
|
|
35
40
|
leaf_seq BIGINT NOT NULL DEFAULT 0,
|
|
36
41
|
-- title: one column, two writers (TiDB twin, same name / width; law in src/session-row.ts): the user's write
|
|
37
|
-
-- (
|
|
42
|
+
-- (writeSessionRowCells, unconditional unless the PATCH carries expectedRowTag; null = cleared back to automatic) and the auto-titler (setTitleIfNull / probeTitle
|
|
38
43
|
-- — fills NULL only; NULL = "untitled", the three-state probe distinguishes it from "no such session").
|
|
39
44
|
title VARCHAR(${SESSION_TITLE_MAX_CHARS}) COLLATE "C",
|
|
40
45
|
-- pinned: the user's pin (TiDB twin); every list row carries it, false by default.
|
|
@@ -56,6 +61,8 @@ export const PG_SESSION_SCHEMA = [
|
|
|
56
61
|
placed_at TIMESTAMPTZ(3),
|
|
57
62
|
-- S-790 / S-506:会话化身(TiDB 孪生同名同义;列定义唯一属主 = src/session-incarnation.ts 的 SESSION_META_INCARNATION_COLUMN)。
|
|
58
63
|
${SESSION_META_INCARNATION_COLUMN.pg},
|
|
64
|
+
-- 执行放置(根会话行上这一位住客的一格:execution_* 七列)+ fork 键(fork_key)—— TiDB 孪生同名同义;列定义与语句的唯一属主 = src/plugins/execution-placement-sql.ts。
|
|
65
|
+
${sessionMetaPlacementColumnsDdl("pg")},
|
|
59
66
|
PRIMARY KEY (session_id)
|
|
60
67
|
)`,
|
|
61
68
|
`CREATE TABLE IF NOT EXISTS session_event (
|
|
@@ -184,10 +191,14 @@ export class PgSessionStore {
|
|
|
184
191
|
placements = SQL_SESSION_PLACEMENTS;
|
|
185
192
|
pending = new Map();
|
|
186
193
|
log;
|
|
194
|
+
executionPlacement;
|
|
195
|
+
db;
|
|
187
196
|
constructor(pool, opts = {}) {
|
|
188
197
|
this.pool = pool;
|
|
189
198
|
this.log = opts.log ?? createLogger();
|
|
190
199
|
this.retention = opts.retention ?? UNMANAGED_RETENTION;
|
|
200
|
+
this.db = pgDriver(pool);
|
|
201
|
+
this.executionPlacement = createSqlExecutionPlacementSeat(this.db);
|
|
191
202
|
}
|
|
192
203
|
async acquire(sessionId, opts) {
|
|
193
204
|
if (!sessionId) {
|
|
@@ -341,38 +352,25 @@ export class PgSessionStore {
|
|
|
341
352
|
const sid = ph(opts.cursor.sessionId);
|
|
342
353
|
where.push(`(sm.updated_at < ${a} OR (sm.updated_at = ${b} AND sm.session_id < ${sid}))`);
|
|
343
354
|
}
|
|
344
|
-
return this.
|
|
355
|
+
return (await this.selectListRows({ where, params, limit: opts.limit })).map((a) => a.row);
|
|
345
356
|
}
|
|
346
357
|
async sessionSummaryOf(sessionId) {
|
|
347
|
-
|
|
348
|
-
return rows[0];
|
|
358
|
+
return (await this.listRowOf(sessionId))?.row;
|
|
349
359
|
}
|
|
350
|
-
async
|
|
351
|
-
|
|
352
|
-
const params = [];
|
|
353
|
-
if (patch.title !== undefined) {
|
|
354
|
-
params.push(patch.title);
|
|
355
|
-
sets.push(`title = $${params.length}`);
|
|
356
|
-
}
|
|
357
|
-
if (patch.pinned !== undefined) {
|
|
358
|
-
params.push(patch.pinned);
|
|
359
|
-
sets.push(`pinned = $${params.length}`);
|
|
360
|
-
}
|
|
361
|
-
if (sets.length > 0) {
|
|
362
|
-
params.push(sessionId, face.incarnation);
|
|
363
|
-
await this.pool.query(`UPDATE session_meta SET ${sets.join(", ")} WHERE session_id = $${params.length - 1} AND incarnation = $${params.length} AND placement_kind IS NULL`, params);
|
|
364
|
-
}
|
|
365
|
-
const rows = await this.selectSummaries({ where: ["sm.placement_kind IS NULL", "sm.session_id = $1", "sm.incarnation = $2"], params: [sessionId, face.incarnation], limit: 1, runSessionId: sessionId });
|
|
366
|
-
return rows[0];
|
|
360
|
+
async listRowOf(sessionId) {
|
|
361
|
+
return (await this.selectListRows({ where: ["sm.placement_kind IS NULL", "sm.session_id = $1"], params: [sessionId], limit: 1, runSessionId: sessionId }))[0];
|
|
367
362
|
}
|
|
368
|
-
async
|
|
363
|
+
async writeSessionRowCells(sessionId, cells, how) {
|
|
364
|
+
return writeSessionRowCellsSql(this.db, sessionId, cells, how, (id) => this.listRowOf(id));
|
|
365
|
+
}
|
|
366
|
+
async selectListRows(q) {
|
|
369
367
|
const params = [...q.params];
|
|
370
368
|
const ph = (v) => {
|
|
371
369
|
params.push(v);
|
|
372
370
|
return `$${params.length}`;
|
|
373
371
|
};
|
|
374
372
|
const runWhere = q.runSessionId !== undefined ? ` WHERE session_id = ${ph(q.runSessionId)}` : "";
|
|
375
|
-
const sql = "SELECT sm.session_id, sm.owner, sm.updated_at AS last_activity, sm.created_at AS first_activity, sm.title, sm.pinned, " +
|
|
373
|
+
const sql = "SELECT sm.session_id, sm.owner, sm.incarnation, sm.updated_at AS last_activity, sm.created_at AS first_activity, sm.title, sm.pinned, " +
|
|
376
374
|
" COALESCE(r.run_count, 0) AS run_count, latest.objective_preview, latest.status, latest.task_id AS last_run_id, latest.last_task_run_id " +
|
|
377
375
|
"FROM session_meta sm " +
|
|
378
376
|
"LEFT JOIN (SELECT session_id, COUNT(*) AS run_count FROM task_run" + runWhere + " GROUP BY session_id) r ON r.session_id = sm.session_id " +
|
|
@@ -383,9 +381,14 @@ export class PgSessionStore {
|
|
|
383
381
|
const res = await this.pool.query(sql, params);
|
|
384
382
|
return res.rows.map((r) => {
|
|
385
383
|
const runId = r.last_task_run_id;
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
384
|
+
const sessionId = String(r.session_id);
|
|
385
|
+
const owner = r.owner ?? null;
|
|
386
|
+
const incarnation = incarnationFromCell(sessionId, r.incarnation, "absent");
|
|
387
|
+
const title = r.title ?? null;
|
|
388
|
+
const pinned = r.pinned === true;
|
|
389
|
+
const row = {
|
|
390
|
+
sessionId,
|
|
391
|
+
owner,
|
|
389
392
|
lastActivityAt: r.last_activity instanceof Date ? r.last_activity.toISOString() : String(r.last_activity),
|
|
390
393
|
firstActivityAt: r.first_activity instanceof Date ? r.first_activity.toISOString() : String(r.first_activity),
|
|
391
394
|
runCount: Number(r.run_count),
|
|
@@ -393,9 +396,11 @@ export class PgSessionStore {
|
|
|
393
396
|
lastStatus: r.status == null ? "" : String(r.status),
|
|
394
397
|
lastRunId: r.last_run_id ?? null,
|
|
395
398
|
...(typeof runId === "string" && runId !== "" ? { lastTaskRunId: runId } : {}),
|
|
396
|
-
title
|
|
397
|
-
pinned
|
|
399
|
+
title,
|
|
400
|
+
pinned,
|
|
401
|
+
...sessionRowTagKey({ sessionId, owner, incarnation, cells: { title, pinned } }),
|
|
398
402
|
};
|
|
403
|
+
return { row, occupant: { owner, incarnation } };
|
|
399
404
|
});
|
|
400
405
|
}
|
|
401
406
|
async probeTitle(sessionId) {
|
|
@@ -431,6 +436,17 @@ export class PgSessionStore {
|
|
|
431
436
|
await this.pool.query("UPDATE session_meta SET workspace = $1::jsonb WHERE session_id = $2 AND incarnation = $3", [encodeSessionWorkspaceCell(workspace, pgSafeJsonStringify, sessionId), sessionId, incarnation]);
|
|
432
437
|
}
|
|
433
438
|
async fork(sourceId, owner) {
|
|
439
|
+
return forkByFreshId((s, o, t) => this.forkTo(s, o, t), sourceId, owner);
|
|
440
|
+
}
|
|
441
|
+
async forkTo(sourceId, owner, target) {
|
|
442
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
443
|
+
const out = await this.forkToOnce(sourceId, owner, target);
|
|
444
|
+
if (out !== "unsettled")
|
|
445
|
+
return out;
|
|
446
|
+
}
|
|
447
|
+
throw new ForkTargetUnsettledError(target.id);
|
|
448
|
+
}
|
|
449
|
+
async forkToOnce(sourceId, owner, target) {
|
|
434
450
|
const metaRes = await this.pool.query("SELECT leaf_id, owner FROM session_meta WHERE session_id = $1", [sourceId]);
|
|
435
451
|
if (!metaRes.rows[0])
|
|
436
452
|
return null;
|
|
@@ -442,19 +458,29 @@ export class PgSessionStore {
|
|
|
442
458
|
const point = await forkPointOf(storage);
|
|
443
459
|
const eventRes = await this.pool.query(`SELECT entry_id, parent_id, type, ${jsonColumnText("pg", "payload")}, ts FROM session_event WHERE session_id = $1 ORDER BY seq ASC`, [sourceId]);
|
|
444
460
|
const { rows, leafId: forkLeafId } = selectForkRows(eventRes.rows, leafId, point, FORK_LOG_ROW_SHAPE);
|
|
445
|
-
const newId = uuidv7();
|
|
446
461
|
const now = new Date();
|
|
447
462
|
const conn = await this.pool.connect();
|
|
463
|
+
let collided = false;
|
|
448
464
|
try {
|
|
449
465
|
await conn.query("BEGIN");
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
const payload = String(r.payload);
|
|
453
|
-
await conn.query("INSERT INTO session_event (session_id, seq, entry_id, parent_id, type, payload, ts) VALUES ($1,$2,$3,$4,$5,$6::jsonb,$7)", [newId, seq, r.entry_id, r.parent_id ?? null, r.type, payload, r.ts]);
|
|
466
|
+
try {
|
|
467
|
+
await conn.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq, incarnation, fork_key) VALUES ($1,$2,$3,$4,$5,$6,$7,$8)", [target.id, forkOwner, now, now, forkLeafId, rows.length, mintSessionIncarnation(), target.key ?? null]);
|
|
454
468
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
469
|
+
catch (err) {
|
|
470
|
+
if (!isDupKey(err))
|
|
471
|
+
throw err;
|
|
472
|
+
collided = true;
|
|
473
|
+
}
|
|
474
|
+
if (!collided) {
|
|
475
|
+
for (let seq = 0; seq < rows.length; seq++) {
|
|
476
|
+
const r = rows[seq];
|
|
477
|
+
const payload = String(r.payload);
|
|
478
|
+
await conn.query("INSERT INTO session_event (session_id, seq, entry_id, parent_id, type, payload, ts) VALUES ($1,$2,$3,$4,$5,$6::jsonb,$7)", [target.id, seq, r.entry_id, r.parent_id ?? null, r.type, payload, r.ts]);
|
|
479
|
+
}
|
|
480
|
+
await conn.query("COMMIT");
|
|
481
|
+
return { outcome: "created", id: target.id };
|
|
482
|
+
}
|
|
483
|
+
await conn.query("ROLLBACK");
|
|
458
484
|
}
|
|
459
485
|
catch (err) {
|
|
460
486
|
try {
|
|
@@ -467,6 +493,9 @@ export class PgSessionStore {
|
|
|
467
493
|
finally {
|
|
468
494
|
conn.release();
|
|
469
495
|
}
|
|
496
|
+
const existing = await this.pool.query("SELECT owner, fork_key FROM session_meta WHERE session_id = $1", [target.id]);
|
|
497
|
+
const row = existing.rows[0];
|
|
498
|
+
return row === undefined ? "unsettled" : classifyForkTarget(target, { owner: row.owner ?? null, forkKey: row.fork_key ?? null }, forkOwner);
|
|
470
499
|
}
|
|
471
500
|
async exportEntries(sessionId) {
|
|
472
501
|
const metaRes = await this.pool.query("SELECT 1 FROM session_meta WHERE session_id = $1", [sessionId]);
|
|
@@ -506,7 +535,7 @@ export class PgSessionStore {
|
|
|
506
535
|
const conn = await this.pool.connect();
|
|
507
536
|
try {
|
|
508
537
|
await conn.query("BEGIN");
|
|
509
|
-
await this.
|
|
538
|
+
await this.writeImportedLog(conn, sessionId, owner, validated, { incarnation: mintSessionIncarnation(), forkKey: null }, "create");
|
|
510
539
|
await conn.query("COMMIT");
|
|
511
540
|
}
|
|
512
541
|
catch (err) {
|
|
@@ -515,7 +544,7 @@ export class PgSessionStore {
|
|
|
515
544
|
}
|
|
516
545
|
catch {
|
|
517
546
|
}
|
|
518
|
-
if (err instanceof SessionError)
|
|
547
|
+
if (err instanceof SessionError || err instanceof SessionActiveRunError)
|
|
519
548
|
throw err;
|
|
520
549
|
throw new SessionError("storage", `import of session ${sessionId} failed`, err instanceof Error ? err : undefined);
|
|
521
550
|
}
|
|
@@ -529,14 +558,21 @@ export class PgSessionStore {
|
|
|
529
558
|
const conn = await this.pool.connect();
|
|
530
559
|
try {
|
|
531
560
|
await conn.query("BEGIN");
|
|
532
|
-
const placedRes = await conn.query(
|
|
533
|
-
|
|
561
|
+
const placedRes = await conn.query(`SELECT ${SESSION_META_PLACEMENT_READ_COLUMNS}, fork_key FROM session_meta WHERE session_id = $1 FOR UPDATE`, [sessionId]);
|
|
562
|
+
const prior = placedRes.rows[0];
|
|
563
|
+
if (prior?.placement_kind != null) {
|
|
534
564
|
await conn.query("ROLLBACK");
|
|
535
|
-
throw new SessionError("placement_refused", `session ${sessionId} belongs to the ${
|
|
565
|
+
throw new SessionError("placement_refused", `session ${sessionId} belongs to the ${String(prior.placement_kind)} placement partition — a session-sync replace cannot overwrite a delegation transcript`);
|
|
536
566
|
}
|
|
537
|
-
await conn.query("DELETE FROM
|
|
538
|
-
|
|
539
|
-
|
|
567
|
+
const purged = await conn.query("DELETE FROM session_meta WHERE session_id = $1 AND owner IS NOT DISTINCT FROM $2 AND placement_kind IS NULL", [sessionId, owner]);
|
|
568
|
+
const swapped = (purged.rowCount ?? 0) === 1 ? prior : undefined;
|
|
569
|
+
const cells = carriedRowCells(sessionId, swapped === undefined ? undefined : { incarnation: swapped.incarnation, forkKey: swapped.fork_key });
|
|
570
|
+
const priorCell = swapped === undefined ? undefined : placementCellFromRow(sessionId, swapped);
|
|
571
|
+
if (priorCell?.role === "writer") {
|
|
572
|
+
await conn.query("ROLLBACK");
|
|
573
|
+
throw new SessionWriterPresentError(sessionId, priorCell.lane);
|
|
574
|
+
}
|
|
575
|
+
await this.writeImportedLog(conn, sessionId, owner, validated, cells, "replace");
|
|
540
576
|
await conn.query("COMMIT");
|
|
541
577
|
}
|
|
542
578
|
catch (err) {
|
|
@@ -545,7 +581,7 @@ export class PgSessionStore {
|
|
|
545
581
|
}
|
|
546
582
|
catch {
|
|
547
583
|
}
|
|
548
|
-
if (err instanceof SessionError)
|
|
584
|
+
if (err instanceof SessionError || err instanceof SessionWriterPresentError || err instanceof SessionActiveRunError)
|
|
549
585
|
throw err;
|
|
550
586
|
throw new SessionError("storage", `replace of session ${sessionId} failed`, err instanceof Error ? err : undefined);
|
|
551
587
|
}
|
|
@@ -553,16 +589,29 @@ export class PgSessionStore {
|
|
|
553
589
|
conn.release();
|
|
554
590
|
}
|
|
555
591
|
}
|
|
556
|
-
async
|
|
592
|
+
async writeImportedLog(conn, sessionId, owner, validated, cells, mode) {
|
|
557
593
|
let leafId = null;
|
|
558
594
|
for (const e of validated)
|
|
559
595
|
leafId = leafIdAfterEntry(e);
|
|
560
596
|
const now = new Date();
|
|
597
|
+
const imported = importedRowCells(cells.forkKey);
|
|
598
|
+
await conn.query(`INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq, incarnation, ${imported.columns}) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)` +
|
|
599
|
+
(mode === "create" ? " ON CONFLICT (session_id) DO NOTHING" : ""), [sessionId, owner, now, now, leafId, validated.length, cells.incarnation, ...imported.values]);
|
|
600
|
+
if (mode === "create") {
|
|
601
|
+
const held = await conn.query("SELECT incarnation FROM session_meta WHERE session_id = $1 FOR UPDATE", [sessionId]);
|
|
602
|
+
if (String(held.rows[0]?.incarnation) !== cells.incarnation) {
|
|
603
|
+
throw new SessionError("storage", `import of session ${sessionId} failed`, new Error(`session ${sessionId} already exists — importEntries only creates a session (replaceEntries rewrites one); nothing was written`));
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
const claim = await lockRootAndReadClaim(rowsOnClient(conn), "pg", sessionId);
|
|
607
|
+
if (claim.activeTaskId !== null)
|
|
608
|
+
throw new SessionActiveRunError(sessionId, claim.activeTaskId);
|
|
609
|
+
if (mode === "replace")
|
|
610
|
+
await conn.query("DELETE FROM session_event WHERE session_id = $1", [sessionId]);
|
|
561
611
|
for (let seq = 0; seq < validated.length; seq++) {
|
|
562
612
|
const e = validated[seq];
|
|
563
613
|
await conn.query("INSERT INTO session_event (session_id, seq, entry_id, parent_id, type, payload, ts) VALUES ($1,$2,$3,$4,$5,$6::jsonb,$7)", [sessionId, seq, e.id, e.parentId ?? null, e.type, pgSafeJsonStringify(e), new Date(e.timestamp)]);
|
|
564
614
|
}
|
|
565
|
-
await conn.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq, incarnation) VALUES ($1,$2,$3,$4,$5,$6,$7)", [sessionId, owner, now, now, leafId, validated.length, incarnation]);
|
|
566
615
|
}
|
|
567
616
|
beginImportStaging(realSessionId, token) {
|
|
568
617
|
return new PgStagingHandle(this.pool, realSessionId, stagingIdFor(realSessionId, token));
|
|
@@ -587,6 +636,11 @@ export class PgSessionStore {
|
|
|
587
636
|
const conn = await this.pool.connect();
|
|
588
637
|
try {
|
|
589
638
|
await conn.query("BEGIN");
|
|
639
|
+
const claim = await lockRootAndReadClaim(rowsOnClient(conn), "pg", sessionId);
|
|
640
|
+
if (claim.activeTaskId !== null) {
|
|
641
|
+
await conn.query("ROLLBACK");
|
|
642
|
+
throw new SessionActiveRunError(sessionId, claim.activeTaskId);
|
|
643
|
+
}
|
|
590
644
|
const metaRes = await conn.query("DELETE FROM session_meta WHERE session_id = $1 AND owner IS NOT DISTINCT FROM $2", [sessionId, owner]);
|
|
591
645
|
if ((metaRes.rowCount ?? 0) > 0)
|
|
592
646
|
await conn.query("DELETE FROM session_event WHERE session_id = $1", [sessionId]);
|
|
@@ -666,6 +720,11 @@ class PgStagingHandle {
|
|
|
666
720
|
throw new StagedCommitOwnerChangedError(this.realSessionId);
|
|
667
721
|
}
|
|
668
722
|
}
|
|
723
|
+
const claim = await lockRootAndReadClaim(rowsOnClient(conn), "pg", this.realSessionId);
|
|
724
|
+
if (claim.activeTaskId !== null) {
|
|
725
|
+
await conn.query("ROLLBACK");
|
|
726
|
+
throw new SessionActiveRunError(this.realSessionId, claim.activeTaskId);
|
|
727
|
+
}
|
|
669
728
|
const staged = await conn.query("SELECT entry_id FROM session_event WHERE session_id = $1 ORDER BY seq ASC FOR UPDATE", [this.stagingId]);
|
|
670
729
|
const dst = await conn.query("SELECT entry_id FROM session_event WHERE session_id = $1 ORDER BY seq ASC FOR UPDATE", [this.realSessionId]);
|
|
671
730
|
const dstMeta = await conn.query("SELECT placement_kind FROM session_meta WHERE session_id = $1", [this.realSessionId]);
|
|
@@ -698,7 +757,16 @@ class PgStagingHandle {
|
|
|
698
757
|
let leafId = null;
|
|
699
758
|
if (lastRes.rows[0])
|
|
700
759
|
leafId = leafIdAfterEntry(decodeSessionEventPayload(lastRes.rows[0].payload, this.realSessionId, "last staged row"));
|
|
701
|
-
await conn.query(
|
|
760
|
+
const settle = await conn.query(`UPDATE session_meta SET owner = $1, updated_at = $2, leaf_id = $3, leaf_seq = $4, ${MIRROR_ON_COMMIT_SET} WHERE session_id = $5 AND ${MIRROR_ON_COMMIT_GUARD}`, [owner, now, leafId, srcIds.length, this.realSessionId]);
|
|
761
|
+
if ((settle.rowCount ?? 0) !== 1) {
|
|
762
|
+
const locked = readPlacementRowStatement("pg", this.realSessionId, { lock: true });
|
|
763
|
+
const lockedRow = (await conn.query(locked.text, [...locked.values])).rows[0];
|
|
764
|
+
const cell = lockedRow === undefined ? undefined : placementCellFromRow(this.realSessionId, lockedRow);
|
|
765
|
+
await conn.query("ROLLBACK");
|
|
766
|
+
if (cell?.role === "writer")
|
|
767
|
+
throw new SessionWriterPresentError(this.realSessionId, cell.lane);
|
|
768
|
+
throw new SessionError("storage", `staged commit for ${this.realSessionId}: the commit point matched no row under a held row lock and the row is not a writer — refusing to guess`);
|
|
769
|
+
}
|
|
702
770
|
const afterMeta = await conn.query("SELECT placement_kind, owner, incarnation FROM session_meta WHERE session_id = $1", [this.realSessionId]);
|
|
703
771
|
if (afterMeta.rows[0]?.placement_kind != null) {
|
|
704
772
|
await conn.query("ROLLBACK");
|
|
@@ -713,7 +781,7 @@ class PgStagingHandle {
|
|
|
713
781
|
await conn.query("ROLLBACK");
|
|
714
782
|
}
|
|
715
783
|
catch { }
|
|
716
|
-
if (err instanceof SyncConflictError || err instanceof SessionError)
|
|
784
|
+
if (err instanceof SyncConflictError || err instanceof SessionError || err instanceof SessionWriterPresentError || err instanceof SessionActiveRunError)
|
|
717
785
|
throw err;
|
|
718
786
|
throw new SessionError("storage", `staged commit for ${this.realSessionId} failed`, err instanceof Error ? err : undefined);
|
|
719
787
|
}
|
|
@@ -63,11 +63,16 @@ export declare class RemoteAdbExecutionEnv implements RemoteExecutionEnv {
|
|
|
63
63
|
* 谁该声明由闭集门看着(`test/execution-lane-caps.test.ts` 的逐 lane 对账:新 lane 不表态 = 编译红)。
|
|
64
64
|
*/
|
|
65
65
|
/** `sharesProcessTable`(core 7.57.0,必填):命令跑在设备上(adb shell),不在本进程所在的进程表。 */
|
|
66
|
+
/** 能力块(引擎 [ref],八键必填、各键只经引擎读器读):`hostLocalPaths` = 这条车道答出的路径是不是本进程所在机器上的文件;`externalContentTarget` = 产出是否来自部署信任边界之外;`io` = 文件与 shell 两面真到达目标;`keepsReadFloor` = 是否守住读拒表的字节地板(本车道不守 ⇒ 读拒表非空的 run 被告知,部署要求地板时准备期拒)。 */
|
|
66
67
|
readonly capabilities: {
|
|
67
|
-
isolation:
|
|
68
|
-
suspendable:
|
|
69
|
-
canonicalPathAuthoritative:
|
|
70
|
-
sharesProcessTable:
|
|
68
|
+
isolation: false;
|
|
69
|
+
suspendable: false;
|
|
70
|
+
canonicalPathAuthoritative: true;
|
|
71
|
+
sharesProcessTable: false;
|
|
72
|
+
hostLocalPaths: false;
|
|
73
|
+
externalContentTarget: false;
|
|
74
|
+
io: true;
|
|
75
|
+
keepsReadFloor: false;
|
|
71
76
|
};
|
|
72
77
|
cwd: string;
|
|
73
78
|
private connected;
|
|
@@ -175,8 +180,19 @@ export declare class RemoteAdbExecutionEnv implements RemoteExecutionEnv {
|
|
|
175
180
|
maxLines?: number;
|
|
176
181
|
abortSignal?: AbortSignal;
|
|
177
182
|
}): Promise<Result<string[], FileError>>;
|
|
183
|
+
/** 暂存写的两只面(规则与提交脚本的唯一属主 = `remote-shell.ts` `writeFileStaged` / `appendFileStaged`;父目录与私有暂存目录由编排的准备那一步建):
|
|
184
|
+
* 上传 = `adb push` 到私有目录里的暂存件;提交 = 文件面探针 exec。两者同一身份(push 与 `adb shell` 都由设备上的 adbd 以它的身份落盘 / 起进程)。 */
|
|
185
|
+
private stagedLane;
|
|
186
|
+
/** 整旧或整新(暂存 → 一次 `mv -f`)。修前 push 直落目标:传输中途断 / 取消时目标剩什么取决于设备侧 adbd 的落地实现,本适配器不依赖它。 */
|
|
178
187
|
writeFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
|
|
188
|
+
/** 追加(暂存要加的字节 → 一次 `cat >>`;inode 不换、已有字节不重写)。修前 = 读旧 + 拼 + 整份 push(O(文件);中途断 ⇒ 目标没了或半份)。
|
|
189
|
+
* 目录不收新名字 / 私有目录收不成私有时的退路 = 修前那一形(push 没有追加写,只能把整份文件落到一个路径上):读旧(不在 ⇒ 按空;别的读错照实答、不 push)
|
|
190
|
+
* + 拼 + 整份 push 直落目标(与 e2b 共用 `rewriteAppend`)—— 非原子,push 中途断 / 取消 ⇒ 目标可能没了或只剩半份(只有 push 那一步经 `ctx.inPlace`:读旧时取消照答 `aborted`,目标未碰)。
|
|
191
|
+
* Android 共享存储(权限位由挂载派生)上收紧那一步按 sdcardfs / MediaProvider FUSE 的权限语义**推断**不过(未上真机,真机读数候 test),追加每次走这里。 */
|
|
179
192
|
appendFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
|
|
193
|
+
/** 把整份内容 `adb push` 到设备上的 `dest`(建或覆盖)。本机这一侧先落一只本地临时件再 push(二进制安全,不经 shell 转义载荷)——
|
|
194
|
+
* 临时件同样只在私有目录里出现(`mkdtemp` 建的目录生来 0700),本机别的用户读不到;文件自己的权限位照缺省(push 把它带到设备上,与修前同)。 */
|
|
195
|
+
private push;
|
|
180
196
|
private posixFsInst?;
|
|
181
197
|
private get posixFs();
|
|
182
198
|
fileInfo(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo, FileError>>;
|