@sema-agent/server 7.137.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 +600 -142
- package/README.md +7 -3
- package/README.zh-CN.md +6 -3
- package/USAGE.md +132 -41
- 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 +23 -16
- package/dist/approval-card.js +1 -0
- package/dist/audit.js +1 -0
- package/dist/background-agent-name.d.ts +5 -0
- package/dist/background-agent-name.js +5 -0
- 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/budget-tracing.d.ts +1 -12
- package/dist/boot/deployment-mods.d.ts +46 -16
- package/dist/boot/deployment-mods.js +82 -6
- 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 +12 -18
- package/dist/boot/governance-seams.js +10 -24
- package/dist/boot/leader.d.ts +3 -0
- package/dist/boot/leader.js +1 -0
- package/dist/boot/memory-consolidation.js +2 -2
- package/dist/boot/org-delivery.js +15 -2
- package/dist/boot/resolve-spec.d.ts +4 -1
- package/dist/boot/resolve-spec.js +29 -19
- package/dist/boot/retention-lane.d.ts +14 -8
- package/dist/boot/retention-lane.js +2 -8
- package/dist/boot/runner-deps.d.ts +10 -4
- package/dist/boot/runner-deps.js +2 -0
- 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-04-stores.d.ts +1 -12
- package/dist/boot/stage-05-execution-env.d.ts +3 -12
- package/dist/boot/stage-05-execution-env.js +8 -5
- package/dist/boot/stage-06-runners.d.ts +3 -12
- package/dist/boot/stage-06-runners.js +5 -3
- package/dist/boot/stage-07-capability-layer.d.ts +3 -12
- package/dist/boot/stage-07-capability-layer.js +2 -2
- package/dist/boot/stage-08-reapers.d.ts +3 -12
- package/dist/boot/stage-09-leader.d.ts +3 -12
- package/dist/boot/stage-09-leader.js +2 -3
- package/dist/boot/stage-10-http-server.d.ts +3 -12
- package/dist/boot/stage-10-http-server.js +5 -3
- package/dist/boot/stores.js +4 -4
- package/dist/boot/web-admin.d.ts +5 -2
- package/dist/boot/web-admin.js +5 -5
- package/dist/brain.js +5 -5
- package/dist/budget.d.ts +17 -15
- package/dist/budget.js +2 -2
- package/dist/capabilities/mcp-probe.d.ts +36 -11
- package/dist/capabilities/mcp-probe.js +44 -5
- package/dist/capabilities/scenarios.d.ts +1 -1
- package/dist/capability-facts.d.ts +21 -7
- package/dist/capability-facts.js +13 -3
- package/dist/checkpoint-identity.d.ts +13 -6
- package/dist/config-catalog.js +14 -8
- 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 +25 -12
- package/dist/config.d.ts +33 -0
- package/dist/config.js +52 -20
- package/dist/delegated-session-owner.d.ts +2 -2
- package/dist/deployment-shape.d.ts +7 -7
- 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/gate-settlement.d.ts +71 -0
- package/dist/gate-settlement.js +40 -0
- package/dist/hooks/classic-stations.d.ts +1 -0
- package/dist/hooks/classic-stations.js +1 -0
- package/dist/hooks/command-spawn.d.ts +2 -2
- package/dist/hooks/hook-runner.js +14 -14
- package/dist/hosted-posture.d.ts +2 -2
- package/dist/http/admission.js +10 -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/dispatch.d.ts +4 -0
- package/dist/http/dispatch.js +3 -0
- package/dist/http/durable-run-leg.js +12 -6
- package/dist/http/execution-context.d.ts +218 -0
- package/dist/http/execution-context.js +330 -0
- package/dist/http/memory-compliance-failure-reply.js +3 -0
- package/dist/http/resume-legs.d.ts +5 -11
- package/dist/http/resume-legs.js +179 -104
- package/dist/http/route-ctx.d.ts +44 -59
- package/dist/http/route-ctx.js +2 -5
- package/dist/http/routes/a2a-serve.js +10 -4
- package/dist/http/routes/agents-roster.d.ts +1 -1
- package/dist/http/routes/agents-roster.js +5 -1
- package/dist/http/routes/approvals-assistant.js +6 -6
- package/dist/http/routes/background.d.ts +29 -1
- package/dist/http/routes/background.js +29 -2
- package/dist/http/routes/capabilities.js +12 -10
- package/dist/http/routes/devices.d.ts +13 -19
- package/dist/http/routes/devices.js +21 -26
- package/dist/http/routes/mcp-reconnect.d.ts +3 -2
- package/dist/http/routes/mcp-reconnect.js +7 -5
- package/dist/http/routes/memory-consolidation.d.ts +1 -1
- package/dist/http/routes/memory-origin.d.ts +1 -1
- package/dist/http/routes/rules.js +3 -3
- package/dist/http/routes/runs.js +11 -7
- package/dist/http/routes/session-sync.js +110 -15
- package/dist/http/routes/sessions.d.ts +6 -0
- package/dist/http/routes/sessions.js +119 -13
- package/dist/http/routes/side-query.d.ts +12 -4
- package/dist/http/routes/side-query.js +6 -8
- package/dist/http/routes/tasks.js +800 -782
- package/dist/http/server.d.ts +61 -39
- 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 +22 -18
- package/dist/identity-source.d.ts +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/leader/wire.d.ts +7 -0
- package/dist/leader/wire.js +5 -4
- package/dist/mcp-assertion-header.d.ts +51 -40
- package/dist/mcp-assertion-header.js +10 -11
- package/dist/mcp-authorization-face.d.ts +27 -0
- package/dist/mcp-authorization-face.js +15 -0
- package/dist/memory-operator-faces.d.ts +13 -4
- package/dist/memory-operator-faces.js +1 -17
- package/dist/memory-sync-client.d.ts +53 -14
- package/dist/memory-sync-client.js +187 -92
- package/dist/memory-sync.d.ts +1 -1
- package/dist/memory-sync.js +3 -2
- package/dist/model-route-gap.d.ts +6 -22
- package/dist/model-route-gap.js +7 -5
- package/dist/mods-declaration.d.ts +110 -0
- package/dist/mods-declaration.js +109 -0
- package/dist/observability/cost-taxonomy.d.ts +2 -2
- package/dist/observability/err-text.d.ts +5 -0
- package/dist/observability/err-text.js +9 -0
- package/dist/observability/fail-open.d.ts +17 -1
- package/dist/observability/fail-open.js +17 -1
- package/dist/observability/run-terminal.js +0 -1
- package/dist/observability/secret-env-scrub.d.ts +1 -1
- package/dist/observability/secret-env-scrub.js +1 -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 +49 -11
- package/dist/parked-decide.js +25 -26
- package/dist/parked-revive-surface.d.ts +12 -15
- package/dist/parked-revive-surface.js +3 -6
- package/dist/peer-seat.d.ts +156 -0
- package/dist/peer-seat.js +273 -0
- package/dist/permission-rule-vocab.d.ts +7 -7
- package/dist/permission-rule-vocab.js +3 -3
- package/dist/plain-string-record.d.ts +38 -0
- package/dist/plain-string-record.js +38 -0
- 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/background-agent-store-sql.js +6 -3
- package/dist/plugins/caching-session-store.d.ts +11 -2
- package/dist/plugins/caching-session-store.js +15 -3
- package/dist/plugins/checkpoint-store-sql.d.ts +21 -25
- package/dist/plugins/checkpoint-store-sql.js +13 -15
- 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 +8 -7
- 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/k8s-pod-reaper.d.ts +2 -8
- package/dist/plugins/k8s-pod-reaper.js +3 -3
- package/dist/plugins/local-checkpoint-store.d.ts +1 -11
- package/dist/plugins/local-checkpoint-store.js +0 -1
- package/dist/plugins/local-session-store.d.ts +183 -45
- package/dist/plugins/local-session-store.js +412 -105
- package/dist/plugins/mailbox-store-sql.d.ts +11 -3
- package/dist/plugins/mailbox-store-sql.js +103 -15
- 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/memory-sync-store-pg.d.ts +16 -5
- package/dist/plugins/memory-sync-store-tidb.d.ts +3 -3
- package/dist/plugins/one-time-migrations.d.ts +117 -6
- package/dist/plugins/one-time-migrations.js +383 -14
- 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 +34 -29
- package/dist/plugins/pg-pool.d.ts +7 -3
- package/dist/plugins/pg-session-storage.d.ts +41 -22
- package/dist/plugins/pg-session-storage.js +136 -55
- package/dist/plugins/remote-env-adb.d.ts +22 -4
- package/dist/plugins/remote-env-adb.js +70 -62
- package/dist/plugins/remote-env-device.d.ts +32 -12
- package/dist/plugins/remote-env-device.js +18 -17
- package/dist/plugins/remote-env-e2b.d.ts +19 -3
- 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 +23 -8
- package/dist/plugins/remote-env-host.js +66 -70
- package/dist/plugins/remote-env-k8s.d.ts +13 -16
- package/dist/plugins/remote-env-k8s.js +61 -54
- package/dist/plugins/remote-env-local-docker.d.ts +30 -5
- package/dist/plugins/remote-env-local-docker.js +46 -59
- package/dist/plugins/remote-env-ssh.d.ts +18 -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 +23 -8
- package/dist/plugins/retention-store-sql.js +13 -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 +63 -12
- package/dist/plugins/session-fork.js +38 -21
- package/dist/plugins/session-ownership.d.ts +7 -0
- package/dist/plugins/session-ownership.js +5 -0
- 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/session-workspace-cell.d.ts +17 -0
- package/dist/plugins/session-workspace-cell.js +38 -0
- package/dist/plugins/store-backend.d.ts +1 -1
- package/dist/plugins/store-backend.js +26 -14
- package/dist/plugins/store-contracts.d.ts +25 -5
- package/dist/plugins/tidb-pool.d.ts +12 -7
- package/dist/plugins/tidb-pool.js +15 -3
- package/dist/plugins/tidb-session-store.d.ts +74 -28
- package/dist/plugins/tidb-session-store.js +132 -53
- package/dist/plugins/tool-result-store-sql.d.ts +16 -8
- package/dist/plugins/tool-result-store-sql.js +26 -17
- package/dist/plugins/worktree-isolation.d.ts +3 -2
- package/dist/plugins/worktree-isolation.js +1 -7
- package/dist/project-identity.d.ts +1 -14
- package/dist/project-identity.js +0 -4
- 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/rules-consent.js +6 -4
- package/dist/run-local.d.ts +6 -0
- package/dist/run-local.js +36 -26
- package/dist/runs.d.ts +9 -2
- package/dist/runs.js +9 -2
- package/dist/sandbox-git-plane.d.ts +2 -2
- package/dist/security.d.ts +38 -12
- package/dist/security.js +8 -2
- package/dist/server-secret-env.d.ts +15 -8
- package/dist/server-secret-env.js +4 -2
- package/dist/session-children-retire.d.ts +27 -9
- package/dist/session-children-retire.js +9 -5
- package/dist/session-incarnation.d.ts +19 -10
- package/dist/session-incarnation.js +5 -3
- 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/session-workspace.d.ts +130 -0
- package/dist/session-workspace.js +83 -0
- package/dist/spec-fields.d.ts +1 -1
- package/dist/standing-answer-grant.d.ts +2 -13
- package/dist/standing-answer-grant.js +0 -7
- package/dist/task-a2a.d.ts +44 -14
- package/dist/task-a2a.js +46 -20
- package/dist/task-cwd.d.ts +6 -1
- package/dist/task-mcp.d.ts +33 -20
- package/dist/task-mcp.js +46 -22
- package/dist/tool-approval.d.ts +38 -8
- package/dist/tool-approval.js +57 -13
- package/dist/trace/core-keyset-guard.d.ts +33 -18
- package/dist/trace/engine-notice-wire.d.ts +1 -1
- package/dist/trace/engine-notice-wire.js +2 -0
- package/dist/trace/exit-seat.d.ts +20 -1
- package/dist/trace/exit-seat.js +3 -0
- package/dist/trace/extension-call-face.d.ts +33 -0
- package/dist/trace/extension-call-face.js +39 -0
- package/dist/trace/project.d.ts +26 -7
- package/dist/trace/project.js +30 -3
- package/dist/trace/redact.js +42 -13
- package/dist/trace/self-minted-wire.d.ts +240 -12
- package/dist/trace/self-minted-wire.js +100 -4
- package/dist/trace/wire-projection-faces.d.ts +6 -6
- package/dist/trace/wire-projection-faces.js +6 -5
- package/dist/url-refusal.d.ts +1 -1
- package/dist/user-authored-text.d.ts +2 -2
- package/dist/user-authored-text.js +16 -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/center-request.d.ts +3 -2
- package/dist/web-admin-client/center-request.js +2 -3
- package/dist/web-admin-client/skills-mcp.d.ts +8 -4
- package/dist/web-admin-client/skills-mcp.js +32 -18
- package/dist/web-admin-client/types.d.ts +11 -0
- package/package.json +3 -3
- package/wire-changes/server@7.138.0.json +446 -0
- 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
- package/dist/http/mcp-remote-text.d.ts +0 -5
- package/dist/http/mcp-remote-text.js +0 -7
|
@@ -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";
|
|
@@ -14,11 +15,17 @@ import { createLogger } from "../observability/logger.js";
|
|
|
14
15
|
import { classifySyncRelationshipByIds, SyncConflictError, StagedCommitOwnerChangedError, stagingIdFor, STAGING_ID_MARKER, STAGING_GC_GRACE_MS, } from "../session-sync-kernel.js";
|
|
15
16
|
import { isPgUniqueViolation } from "./sql-errors.js";
|
|
16
17
|
import { BASELINE_IO_DEADLINE_MS, decodeInstructionSource, encodeInstructionSource } from "../instruction-source-notice.js";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
18
|
+
import { SESSION_META_WORKSPACE_COLUMN, decodeSessionWorkspaceCell, encodeSessionWorkspaceCell } from "./session-workspace-cell.js";
|
|
19
|
+
import { MANAGED_RETENTION, UNMANAGED_RETENTION } from "./retention-store-sql.js";
|
|
20
|
+
import { SESSION_META_INCARNATION_COLUMN, carriedRowCells, headFromRow, incarnationFromCell, mintSessionIncarnation, sessionFaceFromRow } from "../session-incarnation.js";
|
|
19
21
|
import { ensureIndexes } from "./ensure-index.js";
|
|
20
22
|
import { assertFirstOwnerWritable, forkOwnerOf, isSessionOwnerValue } from "./session-ownership.js";
|
|
21
|
-
import {
|
|
23
|
+
import { selectForkRows } from "@sema-agent/core";
|
|
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;
|
|
22
29
|
const isDupKey = isPgUniqueViolation;
|
|
23
30
|
function baselineQuery(text, values) {
|
|
24
31
|
return { text, values, query_timeout: BASELINE_IO_DEADLINE_MS };
|
|
@@ -32,13 +39,15 @@ export const PG_SESSION_SCHEMA = [
|
|
|
32
39
|
leaf_id VARCHAR(64) COLLATE "C",
|
|
33
40
|
leaf_seq BIGINT NOT NULL DEFAULT 0,
|
|
34
41
|
-- title: one column, two writers (TiDB twin, same name / width; law in src/session-row.ts): the user's write
|
|
35
|
-
-- (
|
|
42
|
+
-- (writeSessionRowCells, unconditional unless the PATCH carries expectedRowTag; null = cleared back to automatic) and the auto-titler (setTitleIfNull / probeTitle
|
|
36
43
|
-- — fills NULL only; NULL = "untitled", the three-state probe distinguishes it from "no such session").
|
|
37
44
|
title VARCHAR(${SESSION_TITLE_MAX_CHARS}) COLLATE "C",
|
|
38
45
|
-- pinned: the user's pin (TiDB twin); every list row carries it, false by default.
|
|
39
46
|
pinned BOOLEAN NOT NULL DEFAULT FALSE,
|
|
40
47
|
-- instruction_source(S-690):换源通告的基线 —— TiDB 孪生同名同宽,三态读法(NULL / '' / 链成员相对路径)见那里的列注。
|
|
41
48
|
instruction_source VARCHAR(64) COLLATE "C",
|
|
49
|
+
-- workspace:本会话上一条腿自己的最后一帧 workspace_changed —— TiDB 孪生同名同义(列注与唯一属主见那里:src/plugins/session-workspace-cell.ts)。
|
|
50
|
+
${SESSION_META_WORKSPACE_COLUMN.pg},
|
|
42
51
|
-- #292 C18(core 5.48.0 design/323)subagent 转录持久化 —— placement 元组(TiDB 孪生同名同宽,
|
|
43
52
|
-- 语义与六条义务见 src/plugins/session-placement.ts 顶注):创建时一次落行、first-write 不可变;
|
|
44
53
|
-- placement_kind 在场 ⇒ 住在 placement 分区(不上宿主枚举 / claim 形 acquire 恒拒 / release=真删除 /
|
|
@@ -52,6 +61,8 @@ export const PG_SESSION_SCHEMA = [
|
|
|
52
61
|
placed_at TIMESTAMPTZ(3),
|
|
53
62
|
-- S-790 / S-506:会话化身(TiDB 孪生同名同义;列定义唯一属主 = src/session-incarnation.ts 的 SESSION_META_INCARNATION_COLUMN)。
|
|
54
63
|
${SESSION_META_INCARNATION_COLUMN.pg},
|
|
64
|
+
-- 执行放置(根会话行上这一位住客的一格:execution_* 七列)+ fork 键(fork_key)—— TiDB 孪生同名同义;列定义与语句的唯一属主 = src/plugins/execution-placement-sql.ts。
|
|
65
|
+
${sessionMetaPlacementColumnsDdl("pg")},
|
|
55
66
|
PRIMARY KEY (session_id)
|
|
56
67
|
)`,
|
|
57
68
|
`CREATE TABLE IF NOT EXISTS session_event (
|
|
@@ -176,13 +187,18 @@ export class PgSessionStorage extends BaseSessionStorage {
|
|
|
176
187
|
}
|
|
177
188
|
export class PgSessionStore {
|
|
178
189
|
pool;
|
|
179
|
-
retention
|
|
190
|
+
retention;
|
|
180
191
|
placements = SQL_SESSION_PLACEMENTS;
|
|
181
192
|
pending = new Map();
|
|
182
193
|
log;
|
|
194
|
+
executionPlacement;
|
|
195
|
+
db;
|
|
183
196
|
constructor(pool, opts = {}) {
|
|
184
197
|
this.pool = pool;
|
|
185
198
|
this.log = opts.log ?? createLogger();
|
|
199
|
+
this.retention = opts.retention ?? UNMANAGED_RETENTION;
|
|
200
|
+
this.db = pgDriver(pool);
|
|
201
|
+
this.executionPlacement = createSqlExecutionPlacementSeat(this.db);
|
|
186
202
|
}
|
|
187
203
|
async acquire(sessionId, opts) {
|
|
188
204
|
if (!sessionId) {
|
|
@@ -336,38 +352,25 @@ export class PgSessionStore {
|
|
|
336
352
|
const sid = ph(opts.cursor.sessionId);
|
|
337
353
|
where.push(`(sm.updated_at < ${a} OR (sm.updated_at = ${b} AND sm.session_id < ${sid}))`);
|
|
338
354
|
}
|
|
339
|
-
return this.
|
|
355
|
+
return (await this.selectListRows({ where, params, limit: opts.limit })).map((a) => a.row);
|
|
340
356
|
}
|
|
341
357
|
async sessionSummaryOf(sessionId) {
|
|
342
|
-
|
|
343
|
-
return rows[0];
|
|
358
|
+
return (await this.listRowOf(sessionId))?.row;
|
|
344
359
|
}
|
|
345
|
-
async
|
|
346
|
-
|
|
347
|
-
const params = [];
|
|
348
|
-
if (patch.title !== undefined) {
|
|
349
|
-
params.push(patch.title);
|
|
350
|
-
sets.push(`title = $${params.length}`);
|
|
351
|
-
}
|
|
352
|
-
if (patch.pinned !== undefined) {
|
|
353
|
-
params.push(patch.pinned);
|
|
354
|
-
sets.push(`pinned = $${params.length}`);
|
|
355
|
-
}
|
|
356
|
-
if (sets.length > 0) {
|
|
357
|
-
params.push(sessionId, face.incarnation);
|
|
358
|
-
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);
|
|
359
|
-
}
|
|
360
|
-
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 });
|
|
361
|
-
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];
|
|
362
362
|
}
|
|
363
|
-
async
|
|
363
|
+
async writeSessionRowCells(sessionId, cells, how) {
|
|
364
|
+
return writeSessionRowCellsSql(this.db, sessionId, cells, how, (id) => this.listRowOf(id));
|
|
365
|
+
}
|
|
366
|
+
async selectListRows(q) {
|
|
364
367
|
const params = [...q.params];
|
|
365
368
|
const ph = (v) => {
|
|
366
369
|
params.push(v);
|
|
367
370
|
return `$${params.length}`;
|
|
368
371
|
};
|
|
369
372
|
const runWhere = q.runSessionId !== undefined ? ` WHERE session_id = ${ph(q.runSessionId)}` : "";
|
|
370
|
-
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, " +
|
|
371
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 " +
|
|
372
375
|
"FROM session_meta sm " +
|
|
373
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 " +
|
|
@@ -378,9 +381,14 @@ export class PgSessionStore {
|
|
|
378
381
|
const res = await this.pool.query(sql, params);
|
|
379
382
|
return res.rows.map((r) => {
|
|
380
383
|
const runId = r.last_task_run_id;
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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,
|
|
384
392
|
lastActivityAt: r.last_activity instanceof Date ? r.last_activity.toISOString() : String(r.last_activity),
|
|
385
393
|
firstActivityAt: r.first_activity instanceof Date ? r.first_activity.toISOString() : String(r.first_activity),
|
|
386
394
|
runCount: Number(r.run_count),
|
|
@@ -388,9 +396,11 @@ export class PgSessionStore {
|
|
|
388
396
|
lastStatus: r.status == null ? "" : String(r.status),
|
|
389
397
|
lastRunId: r.last_run_id ?? null,
|
|
390
398
|
...(typeof runId === "string" && runId !== "" ? { lastTaskRunId: runId } : {}),
|
|
391
|
-
title
|
|
392
|
-
pinned
|
|
399
|
+
title,
|
|
400
|
+
pinned,
|
|
401
|
+
...sessionRowTagKey({ sessionId, owner, incarnation, cells: { title, pinned } }),
|
|
393
402
|
};
|
|
403
|
+
return { row, occupant: { owner, incarnation } };
|
|
394
404
|
});
|
|
395
405
|
}
|
|
396
406
|
async probeTitle(sessionId) {
|
|
@@ -417,7 +427,26 @@ export class PgSessionStore {
|
|
|
417
427
|
await this.pool.query(baselineQuery("UPDATE session_meta SET instruction_source = $1 WHERE session_id = $2 AND instruction_source IS NULL", [next, sessionId]));
|
|
418
428
|
}
|
|
419
429
|
}
|
|
430
|
+
async readSessionWorkspace(sessionId) {
|
|
431
|
+
const res = await this.pool.query(`SELECT ${jsonColumnText("pg", "workspace")} FROM session_meta WHERE session_id = $1`, [sessionId]);
|
|
432
|
+
const row = res.rows[0];
|
|
433
|
+
return row === undefined ? undefined : decodeSessionWorkspaceCell(row.workspace, sessionId);
|
|
434
|
+
}
|
|
435
|
+
async recordSessionWorkspace(sessionId, incarnation, workspace) {
|
|
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]);
|
|
437
|
+
}
|
|
420
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) {
|
|
421
450
|
const metaRes = await this.pool.query("SELECT leaf_id, owner FROM session_meta WHERE session_id = $1", [sourceId]);
|
|
422
451
|
if (!metaRes.rows[0])
|
|
423
452
|
return null;
|
|
@@ -428,20 +457,30 @@ export class PgSessionStore {
|
|
|
428
457
|
return null;
|
|
429
458
|
const point = await forkPointOf(storage);
|
|
430
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]);
|
|
431
|
-
const { rows, leafId: forkLeafId } =
|
|
432
|
-
const newId = uuidv7();
|
|
460
|
+
const { rows, leafId: forkLeafId } = selectForkRows(eventRes.rows, leafId, point, FORK_LOG_ROW_SHAPE);
|
|
433
461
|
const now = new Date();
|
|
434
462
|
const conn = await this.pool.connect();
|
|
463
|
+
let collided = false;
|
|
435
464
|
try {
|
|
436
465
|
await conn.query("BEGIN");
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
const payload = String(r.payload);
|
|
440
|
-
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]);
|
|
441
468
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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");
|
|
445
484
|
}
|
|
446
485
|
catch (err) {
|
|
447
486
|
try {
|
|
@@ -454,6 +493,9 @@ export class PgSessionStore {
|
|
|
454
493
|
finally {
|
|
455
494
|
conn.release();
|
|
456
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);
|
|
457
499
|
}
|
|
458
500
|
async exportEntries(sessionId) {
|
|
459
501
|
const metaRes = await this.pool.query("SELECT 1 FROM session_meta WHERE session_id = $1", [sessionId]);
|
|
@@ -493,7 +535,7 @@ export class PgSessionStore {
|
|
|
493
535
|
const conn = await this.pool.connect();
|
|
494
536
|
try {
|
|
495
537
|
await conn.query("BEGIN");
|
|
496
|
-
await this.
|
|
538
|
+
await this.writeImportedLog(conn, sessionId, owner, validated, { incarnation: mintSessionIncarnation(), forkKey: null }, "create");
|
|
497
539
|
await conn.query("COMMIT");
|
|
498
540
|
}
|
|
499
541
|
catch (err) {
|
|
@@ -502,7 +544,7 @@ export class PgSessionStore {
|
|
|
502
544
|
}
|
|
503
545
|
catch {
|
|
504
546
|
}
|
|
505
|
-
if (err instanceof SessionError)
|
|
547
|
+
if (err instanceof SessionError || err instanceof SessionActiveRunError)
|
|
506
548
|
throw err;
|
|
507
549
|
throw new SessionError("storage", `import of session ${sessionId} failed`, err instanceof Error ? err : undefined);
|
|
508
550
|
}
|
|
@@ -516,14 +558,21 @@ export class PgSessionStore {
|
|
|
516
558
|
const conn = await this.pool.connect();
|
|
517
559
|
try {
|
|
518
560
|
await conn.query("BEGIN");
|
|
519
|
-
const placedRes = await conn.query(
|
|
520
|
-
|
|
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) {
|
|
521
564
|
await conn.query("ROLLBACK");
|
|
522
|
-
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`);
|
|
523
566
|
}
|
|
524
|
-
await conn.query("DELETE FROM
|
|
525
|
-
|
|
526
|
-
|
|
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");
|
|
527
576
|
await conn.query("COMMIT");
|
|
528
577
|
}
|
|
529
578
|
catch (err) {
|
|
@@ -532,7 +581,7 @@ export class PgSessionStore {
|
|
|
532
581
|
}
|
|
533
582
|
catch {
|
|
534
583
|
}
|
|
535
|
-
if (err instanceof SessionError)
|
|
584
|
+
if (err instanceof SessionError || err instanceof SessionWriterPresentError || err instanceof SessionActiveRunError)
|
|
536
585
|
throw err;
|
|
537
586
|
throw new SessionError("storage", `replace of session ${sessionId} failed`, err instanceof Error ? err : undefined);
|
|
538
587
|
}
|
|
@@ -540,16 +589,29 @@ export class PgSessionStore {
|
|
|
540
589
|
conn.release();
|
|
541
590
|
}
|
|
542
591
|
}
|
|
543
|
-
async
|
|
592
|
+
async writeImportedLog(conn, sessionId, owner, validated, cells, mode) {
|
|
544
593
|
let leafId = null;
|
|
545
594
|
for (const e of validated)
|
|
546
595
|
leafId = leafIdAfterEntry(e);
|
|
547
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]);
|
|
548
611
|
for (let seq = 0; seq < validated.length; seq++) {
|
|
549
612
|
const e = validated[seq];
|
|
550
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)]);
|
|
551
614
|
}
|
|
552
|
-
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]);
|
|
553
615
|
}
|
|
554
616
|
beginImportStaging(realSessionId, token) {
|
|
555
617
|
return new PgStagingHandle(this.pool, realSessionId, stagingIdFor(realSessionId, token));
|
|
@@ -574,6 +636,11 @@ export class PgSessionStore {
|
|
|
574
636
|
const conn = await this.pool.connect();
|
|
575
637
|
try {
|
|
576
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
|
+
}
|
|
577
644
|
const metaRes = await conn.query("DELETE FROM session_meta WHERE session_id = $1 AND owner IS NOT DISTINCT FROM $2", [sessionId, owner]);
|
|
578
645
|
if ((metaRes.rowCount ?? 0) > 0)
|
|
579
646
|
await conn.query("DELETE FROM session_event WHERE session_id = $1", [sessionId]);
|
|
@@ -653,6 +720,11 @@ class PgStagingHandle {
|
|
|
653
720
|
throw new StagedCommitOwnerChangedError(this.realSessionId);
|
|
654
721
|
}
|
|
655
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
|
+
}
|
|
656
728
|
const staged = await conn.query("SELECT entry_id FROM session_event WHERE session_id = $1 ORDER BY seq ASC FOR UPDATE", [this.stagingId]);
|
|
657
729
|
const dst = await conn.query("SELECT entry_id FROM session_event WHERE session_id = $1 ORDER BY seq ASC FOR UPDATE", [this.realSessionId]);
|
|
658
730
|
const dstMeta = await conn.query("SELECT placement_kind FROM session_meta WHERE session_id = $1", [this.realSessionId]);
|
|
@@ -685,7 +757,16 @@ class PgStagingHandle {
|
|
|
685
757
|
let leafId = null;
|
|
686
758
|
if (lastRes.rows[0])
|
|
687
759
|
leafId = leafIdAfterEntry(decodeSessionEventPayload(lastRes.rows[0].payload, this.realSessionId, "last staged row"));
|
|
688
|
-
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
|
+
}
|
|
689
770
|
const afterMeta = await conn.query("SELECT placement_kind, owner, incarnation FROM session_meta WHERE session_id = $1", [this.realSessionId]);
|
|
690
771
|
if (afterMeta.rows[0]?.placement_kind != null) {
|
|
691
772
|
await conn.query("ROLLBACK");
|
|
@@ -700,7 +781,7 @@ class PgStagingHandle {
|
|
|
700
781
|
await conn.query("ROLLBACK");
|
|
701
782
|
}
|
|
702
783
|
catch { }
|
|
703
|
-
if (err instanceof SyncConflictError || err instanceof SessionError)
|
|
784
|
+
if (err instanceof SyncConflictError || err instanceof SessionError || err instanceof SessionWriterPresentError || err instanceof SessionActiveRunError)
|
|
704
785
|
throw err;
|
|
705
786
|
throw new SessionError("storage", `staged commit for ${this.realSessionId} failed`, err instanceof Error ? err : undefined);
|
|
706
787
|
}
|
|
@@ -58,14 +58,21 @@ export declare class RemoteAdbExecutionEnv implements RemoteExecutionEnv {
|
|
|
58
58
|
* (RPC 断、传输掉),不是「一个它看不见的命名空间」⇒ 读边界的执行期复核**拒**,而不是让词法判决站着。
|
|
59
59
|
* 缺席是今天每个适配器的读法(整链沉默 ⇒ 词法判决成立、命令照跑),所以这一声明是**收紧**方向。
|
|
60
60
|
* 严格读法 `=== true`(core `canonicalPathIsAuthoritative`);`isSuspendable` 一族同样严格,对本仓的
|
|
61
|
-
* 布尔字面量零影响(
|
|
61
|
+
* 布尔字面量零影响(四键都写死字面 `true`/`false`,不存在「非布尔值被读成缺席」那一形 —— 那一形由
|
|
62
62
|
* core 的 `config.execution_env_capability_invalid` 普查响亮播报,本仓的受众表已登记)。
|
|
63
63
|
* 谁该声明由闭集门看着(`test/execution-lane-caps.test.ts` 的逐 lane 对账:新 lane 不表态 = 编译红)。
|
|
64
64
|
*/
|
|
65
|
+
/** `sharesProcessTable`(core 7.57.0,必填):命令跑在设备上(adb shell),不在本进程所在的进程表。 */
|
|
66
|
+
/** 能力块(引擎 [ref],八键必填、各键只经引擎读器读):`hostLocalPaths` = 这条车道答出的路径是不是本进程所在机器上的文件;`externalContentTarget` = 产出是否来自部署信任边界之外;`io` = 文件与 shell 两面真到达目标;`keepsReadFloor` = 是否守住读拒表的字节地板(本车道不守 ⇒ 读拒表非空的 run 被告知,部署要求地板时准备期拒)。 */
|
|
65
67
|
readonly capabilities: {
|
|
66
|
-
isolation:
|
|
67
|
-
suspendable:
|
|
68
|
-
canonicalPathAuthoritative:
|
|
68
|
+
isolation: false;
|
|
69
|
+
suspendable: false;
|
|
70
|
+
canonicalPathAuthoritative: true;
|
|
71
|
+
sharesProcessTable: false;
|
|
72
|
+
hostLocalPaths: false;
|
|
73
|
+
externalContentTarget: false;
|
|
74
|
+
io: true;
|
|
75
|
+
keepsReadFloor: false;
|
|
69
76
|
};
|
|
70
77
|
cwd: string;
|
|
71
78
|
private connected;
|
|
@@ -173,8 +180,19 @@ export declare class RemoteAdbExecutionEnv implements RemoteExecutionEnv {
|
|
|
173
180
|
maxLines?: number;
|
|
174
181
|
abortSignal?: AbortSignal;
|
|
175
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 的落地实现,本适配器不依赖它。 */
|
|
176
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),追加每次走这里。 */
|
|
177
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;
|
|
178
196
|
private posixFsInst?;
|
|
179
197
|
private get posixFs();
|
|
180
198
|
fileInfo(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo, FileError>>;
|
|
@@ -4,9 +4,10 @@ import fs from "node:fs/promises";
|
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
5
|
import { hostSubprocessEnv } from "../server-secret-env.js";
|
|
6
6
|
import { LANE_HOME_PROBE_COMMAND, LaneHomeSeat } from "../lane-home-probe.js";
|
|
7
|
-
import { buildCwdEnvCommand, createCwdGuardedCall, shellQuote, armPipeDestroyGrace } from "./remote-shell.js";
|
|
7
|
+
import { appendFileStaged, rewriteAppend, buildCwdEnvCommand, createCwdGuardedCall, shellQuote, armPipeDestroyGrace, writeFileStaged } from "./remote-shell.js";
|
|
8
8
|
import { signalNumber } from "./host-platform.js";
|
|
9
9
|
import { buildLivenessCut, commandBoundsOf } from "../exec-bounds.js";
|
|
10
|
+
import { buildCapturedOutput, createCutError } from "../exec-cut.js";
|
|
10
11
|
import { FileError, ExecutionError, RemoteExecutionError, withRetry, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
|
|
11
12
|
import { classifyFsStderr, fileErrorFromExec } from "./remote-env-file-error.js";
|
|
12
13
|
import { createPosixShellFs } from "./posix-shell-fs.js";
|
|
@@ -27,7 +28,7 @@ export function isAdbTransportLost(stderr) {
|
|
|
27
28
|
return /device offline|device '.*' not found|no devices\/emulators found|connection reset|protocol fault|device disconnected/i.test(stderr);
|
|
28
29
|
}
|
|
29
30
|
export class RemoteAdbExecutionEnv {
|
|
30
|
-
capabilities = { isolation: false, suspendable: false, canonicalPathAuthoritative: true };
|
|
31
|
+
capabilities = { isolation: false, suspendable: false, canonicalPathAuthoritative: true, sharesProcessTable: false, hostLocalPaths: false, externalContentTarget: false, io: true, keepsReadFloor: false };
|
|
31
32
|
cwd;
|
|
32
33
|
connected = false;
|
|
33
34
|
destroyed = false;
|
|
@@ -341,57 +342,46 @@ export class RemoteAdbExecutionEnv {
|
|
|
341
342
|
lines = lines.slice(0, options.maxLines);
|
|
342
343
|
return ok(lines);
|
|
343
344
|
}
|
|
345
|
+
stagedLane(content) {
|
|
346
|
+
return { upload: (dest, signal) => this.push(content, dest, signal), run: (cmd, signal) => this.fsExec(cmd, signal) };
|
|
347
|
+
}
|
|
344
348
|
async writeFile(p, content, abortSignal) {
|
|
349
|
+
const gated = await this.fsGate(p);
|
|
350
|
+
if (gated)
|
|
351
|
+
return gated;
|
|
352
|
+
return writeFileStaged(this.stagedLane(content), this.resolve(p), abortSignal);
|
|
353
|
+
}
|
|
354
|
+
async appendFile(p, content, abortSignal) {
|
|
345
355
|
const gated = await this.fsGate(p);
|
|
346
356
|
if (gated)
|
|
347
357
|
return gated;
|
|
348
358
|
const abs = this.resolve(p);
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
return { ok: false, error: fileErrorFromExec(mk.error, dir) };
|
|
354
|
-
if (mk.value.exitCode !== 0)
|
|
355
|
-
return { ok: false, error: new FileError("unknown", mk.value.stderr.trim() || `mkdir -p failed for ${dir}`, dir) };
|
|
356
|
-
}
|
|
357
|
-
let local;
|
|
359
|
+
return appendFileStaged(this.stagedLane(content), abs, abortSignal, (ctx) => rewriteAppend((signal) => this.readBinaryFile(abs, signal), (bytes, signal) => this.push(bytes, abs, signal), content, ctx));
|
|
360
|
+
}
|
|
361
|
+
async push(content, dest, abortSignal) {
|
|
362
|
+
let localDir;
|
|
358
363
|
try {
|
|
359
|
-
|
|
364
|
+
localDir = await fs.mkdtemp(path.join(os.tmpdir(), "sema-adb-push-"));
|
|
365
|
+
const local = path.join(localDir, "data");
|
|
360
366
|
await fs.writeFile(local, typeof content === "string" ? Buffer.from(content, "utf8") : Buffer.from(content));
|
|
361
|
-
const r = await this.adb(["-s", this.cfg.serial, "push", local,
|
|
367
|
+
const r = await this.adb(["-s", this.cfg.serial, "push", local, dest], { signal: abortSignal, timeoutMs: this.cfg.dataTimeoutMs });
|
|
362
368
|
if (!r.ok)
|
|
363
|
-
return { ok: false, error: fileErrorFromExec(r.error,
|
|
369
|
+
return { ok: false, error: fileErrorFromExec(r.error, dest) };
|
|
364
370
|
const out = `${r.value.stdout} ${r.value.stderr}`;
|
|
365
371
|
if (/permission denied/i.test(out))
|
|
366
|
-
return { ok: false, error: new FileError("permission_denied", out.trim(),
|
|
372
|
+
return { ok: false, error: new FileError("permission_denied", out.trim(), dest) };
|
|
367
373
|
if (r.value.exitCode !== 0)
|
|
368
|
-
return { ok: false, error: new FileError("unknown", r.value.stderr.trim() || `adb push failed for ${
|
|
374
|
+
return { ok: false, error: new FileError("unknown", r.value.stderr.trim() || `adb push failed for ${dest}`, dest) };
|
|
369
375
|
return ok(undefined);
|
|
370
376
|
}
|
|
371
377
|
catch (e) {
|
|
372
|
-
return { ok: false, error: new FileError("unknown", e instanceof Error ? e.message : String(e),
|
|
378
|
+
return { ok: false, error: new FileError("unknown", e instanceof Error ? e.message : String(e), dest) };
|
|
373
379
|
}
|
|
374
380
|
finally {
|
|
375
|
-
if (
|
|
376
|
-
await fs.rm(
|
|
381
|
+
if (localDir)
|
|
382
|
+
await fs.rm(localDir, { recursive: true, force: true }).catch(() => { });
|
|
377
383
|
}
|
|
378
384
|
}
|
|
379
|
-
async appendFile(p, content, abortSignal) {
|
|
380
|
-
const abs = this.resolve(p);
|
|
381
|
-
const existing = await this.readBinaryFile(abs, abortSignal);
|
|
382
|
-
let base;
|
|
383
|
-
if (existing.ok)
|
|
384
|
-
base = existing.value;
|
|
385
|
-
else if (existing.error.code === "not_found")
|
|
386
|
-
base = new Uint8Array(0);
|
|
387
|
-
else
|
|
388
|
-
return existing;
|
|
389
|
-
const add = typeof content === "string" ? new TextEncoder().encode(content) : content;
|
|
390
|
-
const merged = new Uint8Array(base.length + add.length);
|
|
391
|
-
merged.set(base, 0);
|
|
392
|
-
merged.set(add, base.length);
|
|
393
|
-
return this.writeFile(abs, merged, abortSignal);
|
|
394
|
-
}
|
|
395
385
|
posixFsInst;
|
|
396
386
|
get posixFs() {
|
|
397
387
|
return (this.posixFsInst ??= createPosixShellFs({
|
|
@@ -451,23 +441,44 @@ export class RemoteAdbExecutionEnv {
|
|
|
451
441
|
const outBufs = [];
|
|
452
442
|
const outTail = binary ? undefined : new RollingTailBuffer();
|
|
453
443
|
const errBuf = new RollingTailBuffer();
|
|
454
|
-
const
|
|
444
|
+
const recordStderr = (bytes) => {
|
|
445
|
+
if (bytes.length > 0)
|
|
446
|
+
errBuf.push(bytes);
|
|
447
|
+
};
|
|
448
|
+
const emitStderr = (bytes) => {
|
|
449
|
+
if (bytes.length > 0)
|
|
450
|
+
opts?.onStderr?.(bytes.toString());
|
|
451
|
+
};
|
|
452
|
+
const takeStderr = (bytes) => {
|
|
453
|
+
recordStderr(bytes);
|
|
454
|
+
emitStderr(bytes);
|
|
455
|
+
};
|
|
456
|
+
const heldStderr = () => opts?.stderrFilter?.endStderr();
|
|
457
|
+
const captured = () => {
|
|
458
|
+
const held = heldStderr();
|
|
459
|
+
if (held)
|
|
460
|
+
recordStderr(held);
|
|
461
|
+
return buildCapturedOutput(outTail, errBuf);
|
|
462
|
+
};
|
|
463
|
+
const finish = (outcome) => {
|
|
455
464
|
if (settled)
|
|
456
|
-
return;
|
|
465
|
+
return false;
|
|
457
466
|
settled = true;
|
|
467
|
+
const r = outcome();
|
|
458
468
|
clearTimeout(timer);
|
|
459
469
|
opts?.signal?.removeEventListener("abort", onAbort);
|
|
460
470
|
resolve(r);
|
|
471
|
+
return true;
|
|
461
472
|
};
|
|
462
473
|
const timeoutMs = opts?.timeoutMs ?? this.cfg.commandTimeoutMs;
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
killGrace();
|
|
469
|
-
finish({ ok: false, error: new ExecutionError("aborted", "adb command aborted") });
|
|
474
|
+
const cut = (c) => {
|
|
475
|
+
finish(() => {
|
|
476
|
+
killGrace();
|
|
477
|
+
return { ok: false, error: createCutError(c, captured()) };
|
|
478
|
+
});
|
|
470
479
|
};
|
|
480
|
+
const timer = setTimeout(() => cut({ code: "timeout", message: `adb ${args[0]} exceeded ${timeoutMs}ms` }), timeoutMs);
|
|
481
|
+
const onAbort = () => cut({ code: "aborted", message: "adb command aborted" });
|
|
471
482
|
if (opts?.signal) {
|
|
472
483
|
if (opts.signal.aborted)
|
|
473
484
|
return onAbort();
|
|
@@ -480,30 +491,27 @@ export class RemoteAdbExecutionEnv {
|
|
|
480
491
|
outTail.push(d);
|
|
481
492
|
opts?.onStdout?.(d.toString());
|
|
482
493
|
});
|
|
483
|
-
const takeStderr = (bytes) => {
|
|
484
|
-
if (bytes.length === 0)
|
|
485
|
-
return;
|
|
486
|
-
errBuf.push(bytes);
|
|
487
|
-
opts?.onStderr?.(bytes.toString());
|
|
488
|
-
};
|
|
489
494
|
child.stderr.on("data", (d) => takeStderr(opts?.stderrFilter ? opts.stderrFilter.filterStderr(d) : d));
|
|
490
|
-
child.on("error", (e) => finish({ ok: false, error: new ExecutionError("spawn_error", `adb spawn failed (is '${this.cfg.adbPath}' installed?): ${e.message}`, e) }));
|
|
495
|
+
child.on("error", (e) => finish(() => ({ ok: false, error: new ExecutionError("spawn_error", `adb spawn failed (is '${this.cfg.adbPath}' installed?): ${e.message}`, e) })));
|
|
491
496
|
child.on("close", (code, signal) => {
|
|
492
497
|
if (forceSettleTimer)
|
|
493
498
|
clearTimeout(forceSettleTimer);
|
|
494
|
-
if (opts?.stderrFilter)
|
|
495
|
-
takeStderr(opts.stderrFilter.endStderr());
|
|
496
|
-
const err = errBuf.result();
|
|
497
|
-
const stderr = markTruncated(err.text, err.droppedBytes);
|
|
498
499
|
const exitCode = code ?? (signal ? 128 + (signalNumber(signal) ?? 9) : 0);
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
500
|
+
const held = heldStderr();
|
|
501
|
+
const won = finish(() => {
|
|
502
|
+
if (held)
|
|
503
|
+
recordStderr(held);
|
|
504
|
+
const err = errBuf.result();
|
|
505
|
+
const stderr = markTruncated(err.text, err.droppedBytes);
|
|
506
|
+
if (binary) {
|
|
507
|
+
const stdoutBytes = Buffer.concat(outBufs);
|
|
508
|
+
return ok({ stdout: stdoutBytes.toString("utf8"), stderr, exitCode, stdoutBytes: new Uint8Array(stdoutBytes) });
|
|
509
|
+
}
|
|
504
510
|
const out = outTail.result();
|
|
505
|
-
|
|
506
|
-
}
|
|
511
|
+
return ok({ stdout: markTruncated(out.text, out.droppedBytes), stderr, exitCode });
|
|
512
|
+
});
|
|
513
|
+
if (won && held)
|
|
514
|
+
emitStderr(held);
|
|
507
515
|
});
|
|
508
516
|
});
|
|
509
517
|
}
|