@sema-agent/server 7.136.0 → 7.138.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 +501 -139
- package/README.md +1 -0
- package/README.zh-CN.md +1 -0
- package/USAGE.md +149 -34
- package/deploy/sema-up/README.md +14 -0
- package/deploy/sema-up/chart/templates/sandbox-rbac.yaml +7 -1
- package/deploy/sema-up/stack/sema-stack-cluster.yaml +7 -1
- package/dist/approval-card.d.ts +21 -14
- package/dist/approval-card.js +1 -0
- package/dist/approval-content-kind.d.ts +6 -0
- package/dist/approval-content-kind.js +7 -0
- package/dist/approval-gate-currency.d.ts +48 -6
- package/dist/approval-gate-currency.js +16 -9
- package/dist/approval-hmac.d.ts +13 -1
- package/dist/approval-hmac.js +1 -1
- package/dist/approval.js +3 -3
- package/dist/audit.js +1 -0
- package/dist/auth-bridge.js +2 -2
- package/dist/background-agent-name.d.ts +5 -0
- package/dist/background-agent-name.js +5 -0
- 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/execution-env.d.ts +24 -1
- package/dist/boot/execution-env.js +69 -6
- package/dist/boot/governance-seams.d.ts +10 -17
- package/dist/boot/governance-seams.js +5 -17
- package/dist/boot/leader.d.ts +3 -0
- package/dist/boot/leader.js +1 -0
- package/dist/boot/org-delivery.js +15 -2
- package/dist/boot/reapers.d.ts +3 -0
- package/dist/boot/reapers.js +15 -1
- package/dist/boot/resolve-spec.d.ts +7 -0
- package/dist/boot/resolve-spec.js +36 -26
- package/dist/boot/retention-lane.d.ts +11 -5
- package/dist/boot/retention-lane.js +2 -2
- package/dist/boot/runner-deps.d.ts +7 -3
- package/dist/boot/runner-deps.js +1 -0
- package/dist/boot/stage-03-store-backend.d.ts +1 -1
- package/dist/boot/stage-04-stores.d.ts +2 -13
- package/dist/boot/stage-05-execution-env.d.ts +6 -13
- package/dist/boot/stage-05-execution-env.js +17 -5
- package/dist/boot/stage-06-runners.d.ts +6 -13
- package/dist/boot/stage-06-runners.js +10 -3
- package/dist/boot/stage-07-capability-layer.d.ts +7 -13
- package/dist/boot/stage-07-capability-layer.js +3 -3
- package/dist/boot/stage-08-reapers.d.ts +7 -13
- package/dist/boot/stage-08-reapers.js +2 -1
- package/dist/boot/stage-09-leader.d.ts +7 -13
- package/dist/boot/stage-09-leader.js +2 -2
- package/dist/boot/stage-10-http-server.d.ts +7 -13
- package/dist/boot/stage-10-http-server.js +6 -3
- package/dist/boot/stores.d.ts +1 -1
- package/dist/boot/web-admin.d.ts +5 -2
- package/dist/boot/web-admin.js +3 -3
- package/dist/brain.js +5 -5
- package/dist/broker/broker.d.ts +89 -0
- package/dist/broker/broker.js +287 -0
- package/dist/broker/constants.d.ts +39 -0
- package/dist/broker/constants.js +15 -0
- package/dist/broker/http1.d.ts +111 -0
- package/dist/broker/http1.js +256 -0
- package/dist/broker/lease-book.d.ts +87 -0
- package/dist/broker/lease-book.js +142 -0
- package/dist/broker/lease-file.d.ts +60 -0
- package/dist/broker/lease-file.js +127 -0
- package/dist/broker/main.d.ts +2 -0
- package/dist/broker/main.js +20 -0
- package/dist/budget.d.ts +17 -15
- package/dist/budget.js +2 -2
- package/dist/capabilities/composite-members.d.ts +2 -1
- package/dist/capabilities/composite-members.js +2 -1
- package/dist/capabilities/engine-capabilities.js +1 -0
- package/dist/capabilities/mcp-probe.d.ts +36 -11
- package/dist/capabilities/mcp-probe.js +44 -5
- package/dist/capabilities/scenarios.d.ts +23 -8
- package/dist/capabilities/scenarios.js +13 -8
- package/dist/capabilities/skills.d.ts +28 -5
- package/dist/capabilities/skills.js +8 -0
- package/dist/capability-facts.d.ts +6 -0
- package/dist/capability-facts.js +5 -0
- package/dist/center-credential-renewer.js +5 -1
- package/dist/checkpoint-ctx-seal-m28.d.ts +3 -10
- package/dist/checkpoint-ctx-seal-m28.js +1 -1
- package/dist/checkpoint-identity.d.ts +13 -6
- package/dist/config-catalog.js +16 -7
- package/dist/config-types.d.ts +24 -7
- package/dist/config.d.ts +1 -1
- package/dist/config.js +42 -13
- package/dist/deployment-governance.d.ts +2 -2
- package/dist/deployment-governance.js +7 -9
- package/dist/deployment-shape.d.ts +6 -6
- package/dist/device-ws-protocol.d.ts +1 -0
- package/dist/device-ws-protocol.js +2 -1
- package/dist/file-history-disclosure.d.ts +4 -2
- package/dist/file-history-disclosure.js +2 -0
- package/dist/gate-settlement.d.ts +71 -0
- package/dist/gate-settlement.js +40 -0
- package/dist/hooks/classic-stations.d.ts +12 -54
- 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/http/admission.js +14 -9
- package/dist/http/approval-sweeps.js +4 -3
- package/dist/http/caller-trust.d.ts +2 -1
- package/dist/http/caller-trust.js +1 -3
- package/dist/http/checkpoint-ctx.d.ts +48 -7
- package/dist/http/checkpoint-ctx.js +40 -0
- package/dist/http/dispatch.d.ts +4 -0
- package/dist/http/dispatch.js +18 -10
- package/dist/http/durable-run-leg.d.ts +5 -1
- package/dist/http/durable-run-leg.js +5 -3
- package/dist/http/health-self-report.d.ts +2 -2
- package/dist/http/memory-compliance-failure-reply.js +3 -0
- package/dist/http/principal-gate.d.ts +15 -10
- package/dist/http/principal-gate.js +48 -9
- package/dist/http/resume-legs.d.ts +7 -31
- package/dist/http/resume-legs.js +185 -126
- package/dist/http/route-ctx.d.ts +38 -53
- 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 +24 -10
- package/dist/http/routes/capabilities.js +23 -11
- package/dist/http/routes/mcp-reconnect.d.ts +3 -2
- package/dist/http/routes/mcp-reconnect.js +7 -7
- 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 +13 -8
- package/dist/http/routes/session-sync.js +42 -6
- package/dist/http/routes/sessions.js +6 -3
- package/dist/http/routes/side-query.js +2 -3
- package/dist/http/routes/tasks.js +794 -780
- package/dist/http/server.d.ts +35 -25
- package/dist/http/stored-body-replay.d.ts +3 -3
- package/dist/http/stored-body-replay.js +6 -1
- package/dist/http/wire-types.d.ts +66 -33
- package/dist/identity-source.d.ts +65 -0
- package/dist/identity-source.js +45 -0
- package/dist/index.d.ts +2 -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-store-refusal.d.ts +3 -3
- package/dist/memory-sync-client.js +72 -57
- package/dist/memory-sync.d.ts +1 -1
- package/dist/model-entry-refusal.d.ts +2 -0
- package/dist/model-entry-refusal.js +11 -0
- package/dist/model-route-gap.d.ts +6 -22
- package/dist/model-route-gap.js +7 -5
- package/dist/model-select.d.ts +6 -17
- package/dist/model-select.js +0 -3
- package/dist/mods-declaration.d.ts +110 -0
- package/dist/mods-declaration.js +109 -0
- package/dist/namespaced-rule-name.d.ts +1 -1
- 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/metrics.js +1 -1
- package/dist/observability/run-terminal.js +1 -0
- package/dist/observability/secret-env-scrub.d.ts +1 -1
- package/dist/observability/secret-env-scrub.js +1 -1
- package/dist/operator-ask.d.ts +2 -2
- package/dist/org-delivery-gate.js +1 -1
- package/dist/org-memory-admission.d.ts +2 -2
- package/dist/org-memory-admission.js +39 -2
- package/dist/parked-decide.d.ts +15 -2
- package/dist/parked-decide.js +17 -10
- package/dist/peer-directory.js +2 -0
- package/dist/peer-seat.d.ts +150 -0
- package/dist/peer-seat.js +239 -0
- package/dist/permission-rule-list.d.ts +2 -2
- 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/background-agent-store-sql.js +6 -3
- package/dist/plugins/caching-session-store.d.ts +5 -0
- package/dist/plugins/caching-session-store.js +6 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +34 -29
- package/dist/plugins/checkpoint-store-sql.js +14 -14
- package/dist/plugins/file-history-store-sql.js +10 -16
- package/dist/plugins/file-run-store.js +8 -2
- package/dist/plugins/k8s-exec-protocol.d.ts +69 -13
- package/dist/plugins/k8s-exec-protocol.js +116 -28
- package/dist/plugins/k8s-pod-delete-failure.d.ts +25 -0
- package/dist/plugins/k8s-pod-delete-failure.js +17 -0
- package/dist/plugins/k8s-pod-reaper.d.ts +106 -0
- package/dist/plugins/k8s-pod-reaper.js +122 -0
- package/dist/plugins/local-checkpoint-store.d.ts +4 -17
- package/dist/plugins/local-checkpoint-store.js +3 -4
- package/dist/plugins/local-session-store.d.ts +66 -27
- package/dist/plugins/local-session-store.js +60 -28
- package/dist/plugins/mailbox-store-sql.d.ts +11 -3
- package/dist/plugins/mailbox-store-sql.js +103 -15
- package/dist/plugins/memory-run-store.js +2 -0
- 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 +34 -12
- package/dist/plugins/one-time-migrations.js +51 -9
- package/dist/plugins/permission-rule-store-file.js +2 -2
- package/dist/plugins/permission-rule-store-sql.d.ts +17 -17
- package/dist/plugins/permission-rule-store-sql.js +37 -30
- package/dist/plugins/pg-pool.js +4 -0
- package/dist/plugins/pg-session-storage.d.ts +10 -2
- package/dist/plugins/pg-session-storage.js +20 -7
- package/dist/plugins/remote-env-adb.d.ts +6 -1
- package/dist/plugins/remote-env-adb.js +2 -1
- package/dist/plugins/remote-env-device.d.ts +6 -1
- package/dist/plugins/remote-env-device.js +5 -1
- package/dist/plugins/remote-env-e2b.d.ts +5 -0
- package/dist/plugins/remote-env-e2b.js +2 -1
- package/dist/plugins/remote-env-file-error.d.ts +8 -1
- package/dist/plugins/remote-env-file-error.js +7 -1
- package/dist/plugins/remote-env-host.d.ts +10 -2
- package/dist/plugins/remote-env-host.js +8 -2
- package/dist/plugins/remote-env-k8s.d.ts +140 -11
- package/dist/plugins/remote-env-k8s.js +277 -15
- package/dist/plugins/remote-env-local-docker.d.ts +5 -0
- package/dist/plugins/remote-env-local-docker.js +2 -1
- package/dist/plugins/remote-env-ssh.d.ts +6 -1
- package/dist/plugins/remote-env-ssh.js +2 -1
- package/dist/plugins/retention-store-sql.d.ts +14 -2
- package/dist/plugins/retention-store-sql.js +6 -0
- package/dist/plugins/run-store-sql.js +10 -4
- package/dist/plugins/session-fork.d.ts +10 -13
- package/dist/plugins/session-fork.js +5 -25
- package/dist/plugins/session-ownership.d.ts +24 -3
- package/dist/plugins/session-ownership.js +24 -5
- package/dist/plugins/session-workspace-cell.d.ts +17 -0
- package/dist/plugins/session-workspace-cell.js +38 -0
- package/dist/plugins/sql-errors.d.ts +23 -2
- package/dist/plugins/sql-errors.js +15 -0
- package/dist/plugins/store-backend.js +7 -7
- package/dist/plugins/store-contracts.d.ts +10 -1
- package/dist/plugins/store-fault.d.ts +2 -2
- package/dist/plugins/store-fault.js +4 -2
- package/dist/plugins/tidb-pool.js +13 -0
- package/dist/plugins/tidb-session-store.d.ts +17 -2
- package/dist/plugins/tidb-session-store.js +18 -7
- package/dist/plugins/tool-result-store-sql.d.ts +13 -6
- package/dist/plugins/tool-result-store-sql.js +8 -7
- 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/request-key-closure.js +1 -1
- package/dist/request-value-closure.d.ts +31 -2
- package/dist/request-value-closure.js +7 -1
- package/dist/rule-entry-list.d.ts +3 -2
- package/dist/rule-entry-list.js +2 -2
- package/dist/rules-consent.js +12 -7
- package/dist/run-local.d.ts +2 -2
- package/dist/run-local.js +12 -6
- package/dist/runs.d.ts +7 -1
- package/dist/runs.js +8 -2
- package/dist/runtime-governance.d.ts +5 -5
- package/dist/sandbox-git-lease.d.ts +49 -0
- package/dist/sandbox-git-lease.js +60 -0
- package/dist/sandbox-git-plane.d.ts +84 -0
- package/dist/sandbox-git-plane.js +105 -0
- package/dist/security.d.ts +16 -4
- package/dist/security.js +8 -3
- package/dist/server-secret-env.d.ts +12 -3
- package/dist/server-secret-env.js +18 -2
- package/dist/session-error-wire.d.ts +14 -6
- package/dist/session-error-wire.js +2 -1
- package/dist/session-incarnation.d.ts +4 -5
- package/dist/session-incarnation.js +2 -2
- package/dist/session-policy-wire.d.ts +1 -1
- package/dist/session-policy-wire.js +2 -2
- package/dist/session-sync.d.ts +1 -1
- package/dist/session-workspace.d.ts +130 -0
- package/dist/session-workspace.js +83 -0
- package/dist/spec-fields.d.ts +44 -14
- package/dist/spec-fields.js +33 -12
- package/dist/standing-answer-grant.d.ts +2 -13
- package/dist/standing-answer-grant.js +0 -7
- package/dist/stored-request-prompt-profile-m30.d.ts +49 -0
- package/dist/stored-request-prompt-profile-m30.js +25 -0
- package/dist/task-a2a.d.ts +44 -14
- package/dist/task-a2a.js +46 -20
- package/dist/task-mcp.d.ts +34 -21
- package/dist/task-mcp.js +46 -22
- package/dist/task-settings.d.ts +2 -2
- package/dist/tool-approval.d.ts +43 -9
- package/dist/tool-approval.js +57 -15
- package/dist/tool-rule-names.d.ts +5 -0
- package/dist/trace/core-keyset-guard.d.ts +34 -19
- package/dist/trace/engine-notice-wire.d.ts +1 -1
- package/dist/trace/engine-notice-wire.js +4 -0
- package/dist/trace/exit-seat.d.ts +20 -1
- package/dist/trace/exit-seat.js +5 -2
- package/dist/trace/extension-call-face.d.ts +33 -0
- package/dist/trace/extension-call-face.js +39 -0
- package/dist/trace/ledger-sink.d.ts +4 -5
- package/dist/trace/ledger-sink.js +3 -7
- package/dist/trace/project.d.ts +62 -10
- package/dist/trace/project.js +49 -9
- package/dist/trace/redact.d.ts +18 -6
- package/dist/trace/redact.js +154 -60
- package/dist/trace/self-minted-wire.d.ts +500 -7
- package/dist/trace/self-minted-wire.js +182 -1
- package/dist/trace/wire-projection-faces.d.ts +7 -5
- package/dist/trace/wire-projection-faces.js +9 -4
- package/dist/user-authored-text.d.ts +2 -2
- package/dist/user-authored-text.js +16 -1
- package/dist/web-admin-client/apply-effective.js +0 -1
- package/dist/web-admin-client/center-request.d.ts +23 -4
- package/dist/web-admin-client/center-request.js +42 -8
- package/dist/web-admin-client/http-client.js +6 -6
- package/dist/web-admin-client/skills-mcp.d.ts +8 -4
- package/dist/web-admin-client/skills-mcp.js +31 -18
- package/dist/web-admin-client/types.d.ts +10 -5
- package/docs/DEVICE-EXECUTOR-PROTOCOL.md +11 -5
- package/package.json +3 -3
- package/wire-changes/server@7.137.0.json +825 -0
- package/wire-changes/server@7.138.0.json +446 -0
|
@@ -4,6 +4,7 @@ import type { SessionRowPatch } from "../session-row.js";
|
|
|
4
4
|
import { type Logger } from "../observability/logger.js";
|
|
5
5
|
import { type SessionFace } from "../session-incarnation.js";
|
|
6
6
|
import { type InstructionSourceBaseline } from "../instruction-source-notice.js";
|
|
7
|
+
import type { SessionWorkspace } from "../session-workspace.js";
|
|
7
8
|
import { type StagedCommitOptions, type StagingHandle, type SyncRelation } from "../session-sync-kernel.js";
|
|
8
9
|
/**
|
|
9
10
|
* local 车道会话存档的**唯一 repo 出口**:本仓里每一只读 `<root>/sessions/*.jsonl` 的 `FileSessionRepo` 都从这里取(`LocalSessionStore` 的底座与会话视图口
|
|
@@ -64,6 +65,9 @@ export declare class LocalSessionStore implements SessionStore {
|
|
|
64
65
|
* run selected, encoded like the SQL twins' `session_meta.instruction_source` column ('' = none; missing key = never
|
|
65
66
|
* recorded). Its own file for the same reason titles have theirs: one small map per concern, no format coupling. */
|
|
66
67
|
private readonly instructionSources;
|
|
68
|
+
/** 会话行的位置与项目那一格(`session-workspaces.json`)—— SQL 孪生 `session_meta.workspace` 的 local 对位:`{ [sessionId]: 那一格的文本 }`,
|
|
69
|
+
* 文本与 SQL 列同一只编解码(`session-workspace-cell.ts`,一格读不出 ⇒ 读作没有记录并计一次)。缺键 = 没有记录。一事一档,理由同 titles。 */
|
|
70
|
+
private readonly workspaces;
|
|
67
71
|
/**
|
|
68
72
|
* 会话元数据(`session-incarnations.json`,一格的形与两种读法见 {@link SessionMetaCell}):**属主与化身的唯一真源**,SQL 孪生
|
|
69
73
|
* `session_meta` 行上 `owner` / `incarnation` 两列在本店的对位。两样都**必须落盘**:
|
|
@@ -175,6 +179,13 @@ export declare class LocalSessionStore implements SessionStore {
|
|
|
175
179
|
* 开始之前共享的那张表只含落过盘的值,判定永远不会建立在一个没落过盘的值上(codex che-NM r2 [medium])。
|
|
176
180
|
*/
|
|
177
181
|
recordInstructionSource(sessionId: string, expected: InstructionSourceBaseline, source: string | null): Promise<void>;
|
|
182
|
+
/** 会话行的位置与项目那一格(local 孪生;读法与 SQL 两孪生同一只 `decodeSessionWorkspaceCell`)。缺键 ⇒ `undefined`。 */
|
|
183
|
+
readSessionWorkspace(sessionId: string): Promise<SessionWorkspace | undefined>;
|
|
184
|
+
/**
|
|
185
|
+
* 整格覆写(最后一帧胜;唯一调用方 = 腿的记录座)。与 SQL 孪生「`UPDATE … WHERE session_id = ? AND incarnation = ?`」同义:**会话不在、或同 id
|
|
186
|
+
* 已换了住客 ⇒ 不写**,判与写排在这只会话的写锁里(删除 / 换人同锁 —— 判过的住客在这一笔落盘之前换不掉)。落不了盘 ⇒ 内存不动、抛。
|
|
187
|
+
*/
|
|
188
|
+
recordSessionWorkspace(sessionId: string, incarnation: string, workspace: SessionWorkspace): Promise<void>;
|
|
178
189
|
/** Best-effort, one-shot hydrate of the persisted lastActivity map (no-op when not persisting / no sidecar yet). A
|
|
179
190
|
* read/parse error degrades to an empty map (createdAt ordering) rather than failing the list — the picker still
|
|
180
191
|
* works, just without restart-persisted recency. Memoized so concurrent listSessions share one read. */
|
|
@@ -235,11 +246,6 @@ export declare class LocalSessionStore implements SessionStore {
|
|
|
235
246
|
* `REQUIRE_PRINCIPAL=true`,授权口也只在那时才把 principal 递过来认领(`security.ts` `createAuthorizer`)。
|
|
236
247
|
*/
|
|
237
248
|
register(sessionId: string, owner: string | null): Promise<void>;
|
|
238
|
-
/** 2c P1d-β — re-stamp the session's owner WITHOUT rewriting entries (the staged-commit `identical` path: the log is
|
|
239
|
-
* already present & equal, so only the owner needs re-stamping to the importing principal, §9). Persisted like every
|
|
240
|
-
* owner write; the incarnation is untouched (same occupant). No cell (no such session) ⇒ no-op. Only caller:
|
|
241
|
-
* {@link commitStagedImport}, inside the session's write lock. */
|
|
242
|
-
private restampOwner;
|
|
243
249
|
touch(sessionId: string): Promise<void>;
|
|
244
250
|
/** Durable history is retained by the repo; just drop the in-flight cache entry (mirrors TiDBSessionStore.release). */
|
|
245
251
|
release(sessionId: string): Promise<void>;
|
|
@@ -322,27 +328,60 @@ export declare class LocalSessionStore implements SessionStore {
|
|
|
322
328
|
afterSeq?: number;
|
|
323
329
|
batchSize?: number;
|
|
324
330
|
}): Promise<AsyncIterable<SessionTreeEntry> | null>;
|
|
325
|
-
/**
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
331
|
+
/**
|
|
332
|
+
* 2c session-sync — IMPORT a verbatim entry log into `sessionId`(引擎的 `validateEntriesForImport` 闸在引擎仓的导入里先跑:坏包一个字节不写)。`owner` = 鉴权过的导入者。
|
|
333
|
+
* 与引擎 `SessionRepo.importEntries` 的导入律同一句,只是**属主在本店的属主格上判**(会话元数据那一格是本店唯一的属主真源;本店建的存档在引擎仓里
|
|
334
|
+
* 不带属主戳 —— `register` 只 `repo.create({ id })` —— 引擎仓的属主比较替这一格答不了):落在既有行上 ⇒ 只落在导入者自己的行上(读不中 ⇒ `conflict`,
|
|
335
|
+
* 一个字节不写),存档原地换日志、行原样;落在不存在的 id 上 ⇒ 新行(属主 = 导入者,铸一只化身)。整段在这只会话的写锁里;日志 = {@link swapLogHeld},行收尾 = {@link finishSwap}。
|
|
336
|
+
*/
|
|
330
337
|
importEntries(sessionId: string, owner: string | null, entries: SessionTreeEntry[]): Promise<void>;
|
|
331
|
-
/**
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
*
|
|
335
|
-
|
|
336
|
-
* deleteSession-then-importEntries is effectively atomic for the local case (no concurrent reader can observe the
|
|
337
|
-
* gap, unlike the durable cross-instance backends which do it in ONE SQL txn). Both halves are best-effort feature-
|
|
338
|
-
* gated: a repo lacking either seam throws the same clear error importEntries does (no silent half-write). */
|
|
338
|
+
/**
|
|
339
|
+
* 2c session-sync (§7/§8) — IDEMPOTENT REPLACE,按接口句(`SessionStorage.replaceEntries`:先清这只会话既有的对话日志,再导入 `entries`)。
|
|
340
|
+
* 与 {@link importEntries} 同一个入口、同一只属主判、同一个行收尾,只差日志那一半:先清内容存档再导入(`purge_first`,见 {@link LogSwap})——
|
|
341
|
+
* 所以一只会话头读不出的存档也换得掉;存档是重建的那一只,建档时刻 = 换装时刻(SQL 孪生 replace 重插行、`created_at` 重记,同义)。
|
|
342
|
+
*/
|
|
339
343
|
replaceEntries(sessionId: string, owner: string | null, entries: SessionTreeEntry[]): Promise<void>;
|
|
340
|
-
/**
|
|
341
|
-
|
|
344
|
+
/**
|
|
345
|
+
* 换装三条写腿(公开 {@link importEntries} / {@link replaceEntries} 与暂存提交 {@link commitStagedImport})的**同一个入口**:调用方给的属主先过
|
|
346
|
+
* {@link importerOwnerOf}(主体名或 `null`;否则 `TypeError`,什么都没读、一个字节没写),再进这只会话的写锁 —— 锁里的写体只见判过的那一个值,
|
|
347
|
+
* 写进行的属主就是它。引擎导入口的属主判管不到这一格:换装交给引擎的是存档自己的戳(见 {@link swapLogHeld}),不是调用方的属主。
|
|
348
|
+
*/
|
|
349
|
+
private withImportLock;
|
|
350
|
+
/**
|
|
351
|
+
* 公开导入口({@link importEntries} / {@link replaceEntries})的锁内一形(调用方持这只会话的写锁)。属主判 —— 这条路径上**唯一**一次读那一格:
|
|
352
|
+
* 行在而它的属主格不是导入者 ⇒ `conflict`,一个字节不写;判过才交给写体(写体不读那一格、不判属主)。
|
|
353
|
+
*/
|
|
354
|
+
private importHeld;
|
|
355
|
+
/**
|
|
356
|
+
* 换装的**日志**一半(公开导入 / 换装与暂存提交三条腿共用;行那一半在 {@link finishSwap})。调用方持这只会话的写锁,并已在本店的属主格上判过 ——
|
|
357
|
+
* 这里不读那一格、不判属主。只换**内容**;会话元数据那一格、标题 / 置顶 / 指令来源 / 工作区四只行边档、新近,这里一个字节不碰。
|
|
358
|
+
* 一次引擎仓导入,落在 {@link archiveToSwap} 交回的那一份存档上(怎么对待既有存档由写腿的动词定,{@link LogSwap}):
|
|
359
|
+
* 落在既有存档上 = 整份日志换成 `entries`、存档的元数据整只留着(其中的建档时刻是列表行的 firstActivityAt,与 SQL 孪生留在行上的 `created_at` 同义);
|
|
360
|
+
* 校验(`validateEntriesForImport`)、存档判与写在它里面是一步,抛在哪一处都一个字节没写。
|
|
361
|
+
* 存档上的属主戳不是本店的事实(属主只在那一格;本店建的存档一律不带戳,存量存档可能带着此前导入者的戳),只是引擎仓原地换的条件:
|
|
362
|
+
* 按那一份存档自己的戳导入,并以它为 `expect`(戳 + 存档的令牌;没有存档 ⇒ `row: null`,导入新建一只不带戳的存档)——
|
|
363
|
+
* 换的就是这一份;读与写之间它换了一代 ⇒ 引擎仓拒 `conflict`,一个字节不写。
|
|
364
|
+
*/
|
|
365
|
+
private swapLogHeld;
|
|
366
|
+
/**
|
|
367
|
+
* 换装落在哪一份存档上(`null` = 没有存档,导入新建一只)—— {@link LogSwap} 的唯一读法。
|
|
368
|
+
* `in_place`:锁里读到的那一份(读不出 ⇒ 引擎的故障原样上抛,一个字节没写)。
|
|
369
|
+
* `purge_first`:先清掉内容存档(不带 `expect` —— 带了就得先读它的头,读不出的存档就清不掉),再交回 `null`;清不可撤,所以引擎导入里的那一只闸
|
|
370
|
+
* (`validateEntriesForImport`)在清之前先跑一遍,坏包什么都不清。清过之后导入再抛(I/O 故障),内容就没了、行与四只边档还在 —— 已发版本的 replace 同形(删后导入)。
|
|
371
|
+
*/
|
|
372
|
+
private archiveToSwap;
|
|
373
|
+
/**
|
|
374
|
+
* 推送的**行**收尾 —— 换了日志的与内容相同只重戳的,都经这**一处**(暂存提交两支之后、公开两口的写体之后;调用方持写锁,`owner` 的形已在入口
|
|
375
|
+
* {@link withImportLock} 判过、归属已在锁里判过):
|
|
376
|
+
* 会话元数据那一格属主重戳为导入者;化身经 {@link carriedIncarnation}(三只店同一句)读那一格自己 —— 行在 ⇒ 沿用(同一行 = 同一位住客),
|
|
377
|
+
* 刚建的行 ⇒ 铸一只;然后记新近 = 此刻:推送即活动,不因内容相同分叉。标题 / 置顶 / 指令来源 / 工作区四只行边档不碰。
|
|
378
|
+
* SQL 孪生暂存提交那一句 `UPDATE session_meta SET owner = ?, updated_at = now, leaf_id, leaf_seq` 的 local 对位(那一句两支同写)。
|
|
379
|
+
*/
|
|
380
|
+
private finishSwap;
|
|
342
381
|
/** 2c session-sync P1d-β (PUSH streaming) — begin a STAGED import. The LOCAL backend is single-writer with no
|
|
343
382
|
* cross-instance / cross-replica concurrency story (the durable twins carry that), so the staging handle simply
|
|
344
|
-
* ACCUMULATES the streamed batches in memory then, on commit, runs the SAME
|
|
345
|
-
*
|
|
383
|
+
* ACCUMULATES the streamed batches in memory then, on commit, runs the SAME in-place log swap ({@link swapLogHeld}: one
|
|
384
|
+
* engine-repo import onto the archive as it stands — validate, judge and write in one step) the whole-bundle local import uses. Keep it
|
|
346
385
|
* simple per §1: the durable twins are where the SHADOW-id swap + bounded-memory + concurrency matter. */
|
|
347
386
|
beginImportStaging(realSessionId: string, token: string): StagingHandle;
|
|
348
387
|
/** 2c P1d-β (staged-row inspection) — the in-memory staged entries for a local staging id. No longer the import gate
|
|
@@ -354,7 +393,7 @@ export declare class LocalSessionStore implements SessionStore {
|
|
|
354
393
|
/**
|
|
355
394
|
* {@link LocalStagingHandle.commit} 的整段 —— **一次持锁**(这只会话的写锁,{@link withSessionWriteLock}):属主 CAS(此刻的属主格 = 这次导入的门判过的那一格,
|
|
356
395
|
* 否则抛 {@link StagedCommitOwnerChangedError},一个字节不写;在判目的端日志之前判,SQL 孪生同序)→ 提交时重新分类(目的端此刻的日志 vs staged)
|
|
357
|
-
* → 拒未经同意的 fork / stale / 载荷分叉 →
|
|
396
|
+
* → 拒未经同意的 fork / stale / 载荷分叉 → 换日志({@link swapLogHeld},原地换;属主只在 CAS 那一读上判)或 identical 且内容相同时日志不动 → 两支同一收尾 {@link finishSwap}(重戳属主 + 记新近)→ **在同一次持锁里**
|
|
358
397
|
* 读出落定的会话面交回。锁放下之前谁也换不掉这一位住客,所以交回的面就是这次提交落下的那一位(SQL 孪生在提交事务里读同一行,同义);
|
|
359
398
|
* 提交之后按住客条件写的重放(会话行两位)只认这只面,不再重读。改属主的每一条写腿(登记 / 删除 / 导入 / 换装)都在同一把锁里,所以锁内这一读到锁放下都成立。
|
|
360
399
|
*/
|
|
@@ -367,10 +406,10 @@ export declare class LocalSessionStore implements SessionStore {
|
|
|
367
406
|
* tool-result rows are purged separately by the route's purgeSession coordinator. 整段在这只会话的写锁里(换人)。 */
|
|
368
407
|
deleteSession(sessionId: string, owner: string | null): Promise<boolean>;
|
|
369
408
|
/**
|
|
370
|
-
* {@link deleteSession} 的锁内一形(
|
|
371
|
-
* 删的是 SQL 孪生 `session_meta`
|
|
372
|
-
*
|
|
373
|
-
* 留在盘上的值会挂到同 id 的下一位住客身上(E21 的删除权残留)
|
|
409
|
+
* {@link deleteSession} 的锁内一形(调用方持这只会话的写锁;换装不走它 —— 换装只换内容存档,行原样,见 {@link swapLogHeld})。
|
|
410
|
+
* 删的是 SQL 孪生 `session_meta` 那一行在本店的全部对位:会话存档、四只行边档(标题 / 置顶 / 指令来源基线 / 工作区)、新近标记、属主 + 化身那一格。
|
|
411
|
+
* 四只边档的那一键**都经各自的写链删**(`commit`:草稿上删、落盘、再发布 —— 与写它们的那一笔同一条链、同一把会话锁),落不了盘 ⇒ 抛:
|
|
412
|
+
* 留在盘上的值会挂到同 id 的下一位住客身上(E21 的删除权残留)。元数据格**最后**删:四只边档任一只落不了而抛时,那一格还在 ⇒ 会话对属主门仍
|
|
374
413
|
* 「在」,重发删除照样走得进来、收敛;反过来先删格,重发就成了 no-op,残留永远留下。
|
|
375
414
|
* 🔴 hydrate FIRST (workflow audit 2026-07-13): after a restart the maps start empty — an un-hydrated delete would find nothing and
|
|
376
415
|
* the sidecar would keep the value (an E21 right-to-delete residue that would RESURRECT onto a same-id re-import). ensureLoaded is
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
|
-
import { assertFirstOwnerWritable, forkOwnerOf } from "./session-ownership.js";
|
|
3
|
+
import { assertFirstOwnerWritable, forkOwnerOf, importerOwnerOf } from "./session-ownership.js";
|
|
4
4
|
import { FileSessionRepo, SessionError, StoredSession, settledForkOptions, validateEntriesForImport } from "@sema-agent/core";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { contentForkRelation, fastForwardSharedContentDiverged, identicalIdsAlsoIdenticalContent } from "../session-sync-content.js";
|
|
@@ -11,6 +11,7 @@ import { createLogger } from "../observability/logger.js";
|
|
|
11
11
|
import { UNMANAGED_RETENTION } from "./retention-store-sql.js";
|
|
12
12
|
import { UnreadableLeaf, carriedIncarnation, headFromRow, mintSessionIncarnation, sessionFaceFromRow } from "../session-incarnation.js";
|
|
13
13
|
import { decodeInstructionSource, encodeInstructionSource, sameInstructionSourceBaseline } from "../instruction-source-notice.js";
|
|
14
|
+
import { decodeSessionWorkspaceCell, encodeSessionWorkspaceCell } from "./session-workspace-cell.js";
|
|
14
15
|
import { noteReplayInSessionOpenScope, openSessionForItself } from "../session-records-skipped-notice.js";
|
|
15
16
|
import { createCorruptReadFailOpenSeat } from "../observability/corrupt-read-seat.js";
|
|
16
17
|
import { buildObjectivePreview, isAttachmentFrame } from "../user-authored-text.js";
|
|
@@ -35,7 +36,7 @@ function userText(entry) {
|
|
|
35
36
|
}
|
|
36
37
|
return null;
|
|
37
38
|
}
|
|
38
|
-
const SIDECAR_DEGRADES = { lastActivity: "recency", title: "title", pinned: "pin state", "instruction-source": "instruction-source baseline", "session-meta": "session owner + incarnation (the write that needed it failed loudly)" };
|
|
39
|
+
const SIDECAR_DEGRADES = { lastActivity: "recency", title: "title", pinned: "pin state", "instruction-source": "instruction-source baseline", workspace: "each session's last recorded place and project (the next frame records it again)", "session-meta": "session owner + incarnation (the write that needed it failed loudly)" };
|
|
39
40
|
const readPinnedSidecar = createZodColumnReader(z.record(z.string(), z.literal(true)));
|
|
40
41
|
function readSessionMetaCell(cell) {
|
|
41
42
|
return typeof cell === "string" ? { incarnation: cell, owner: undefined } : cell;
|
|
@@ -180,6 +181,7 @@ export class LocalSessionStore {
|
|
|
180
181
|
titles;
|
|
181
182
|
pinned;
|
|
182
183
|
instructionSources;
|
|
184
|
+
workspaces;
|
|
183
185
|
meta;
|
|
184
186
|
log;
|
|
185
187
|
sessionWriteLocks;
|
|
@@ -192,6 +194,7 @@ export class LocalSessionStore {
|
|
|
192
194
|
this.titles = sidecar("session-titles.json", "title", readStringRecordColumn);
|
|
193
195
|
this.pinned = sidecar("session-pinned.json", "pinned", readPinnedSidecar);
|
|
194
196
|
this.instructionSources = sidecar("session-instruction-sources.json", "instruction-source", readStringRecordColumn);
|
|
197
|
+
this.workspaces = sidecar("session-workspaces.json", "workspace", readStringRecordColumn);
|
|
195
198
|
this.meta = sidecar("session-incarnations.json", "session-meta", readSessionMetaSidecar, true);
|
|
196
199
|
const root = dataDir ? resolve(dataDir) : undefined;
|
|
197
200
|
if (root !== undefined && !SESSION_WRITE_LOCKS_BY_ROOT.has(root))
|
|
@@ -352,6 +355,26 @@ export class LocalSessionStore {
|
|
|
352
355
|
if (!committed)
|
|
353
356
|
throw new Error("LocalSessionStore: instruction-source sidecar write failed — baseline not recorded");
|
|
354
357
|
}
|
|
358
|
+
async readSessionWorkspace(sessionId) {
|
|
359
|
+
await this.workspaces.ensureLoaded();
|
|
360
|
+
return decodeSessionWorkspaceCell(this.workspaces.state.entries.get(sessionId) ?? null, sessionId);
|
|
361
|
+
}
|
|
362
|
+
async recordSessionWorkspace(sessionId, incarnation, workspace) {
|
|
363
|
+
const text = encodeSessionWorkspaceCell(workspace, JSON.stringify, sessionId);
|
|
364
|
+
await this.withSessionWriteLock(sessionId, async () => {
|
|
365
|
+
if ((await this.metaOf(sessionId))?.incarnation !== incarnation)
|
|
366
|
+
return;
|
|
367
|
+
await this.workspaces.ensureLoaded();
|
|
368
|
+
const committed = await this.workspaces.commit((draft) => {
|
|
369
|
+
if (draft.get(sessionId) === text)
|
|
370
|
+
return false;
|
|
371
|
+
draft.set(sessionId, text);
|
|
372
|
+
return true;
|
|
373
|
+
});
|
|
374
|
+
if (!committed)
|
|
375
|
+
throw new SessionError("storage", `LocalSessionStore: workspace sidecar write failed for session ${sessionId} — its place was not recorded`);
|
|
376
|
+
});
|
|
377
|
+
}
|
|
355
378
|
async ensureLoaded() {
|
|
356
379
|
if (!this.sidecarPath)
|
|
357
380
|
return;
|
|
@@ -480,9 +503,6 @@ export class LocalSessionStore {
|
|
|
480
503
|
this.log.info("session_owner_claimed", { sessionId });
|
|
481
504
|
});
|
|
482
505
|
}
|
|
483
|
-
async restampOwner(sessionId, owner) {
|
|
484
|
-
await this.writeMeta(sessionId, (before) => (before === undefined ? undefined : { incarnation: readSessionMetaCell(before).incarnation, owner }));
|
|
485
|
-
}
|
|
486
506
|
async touch(sessionId) {
|
|
487
507
|
this.lastActivity.set(sessionId, new Date().toISOString());
|
|
488
508
|
this.scheduleFlush();
|
|
@@ -607,28 +627,42 @@ export class LocalSessionStore {
|
|
|
607
627
|
return gen();
|
|
608
628
|
}
|
|
609
629
|
async importEntries(sessionId, owner, entries) {
|
|
610
|
-
await this.
|
|
611
|
-
}
|
|
612
|
-
async importEntriesAs(sessionId, owner, entries, incarnation) {
|
|
613
|
-
if (!this.repo.importEntries)
|
|
614
|
-
throw new SessionError("storage", "session import is not supported by this local backend");
|
|
615
|
-
await this.meta.ensureLoaded();
|
|
616
|
-
await this.repo.importEntries(sessionId, owner ?? undefined, entries);
|
|
617
|
-
await this.writeMeta(sessionId, () => ({ incarnation, owner }));
|
|
630
|
+
await this.withImportLock(sessionId, owner, (importer) => this.importHeld(sessionId, importer, entries, "in_place"));
|
|
618
631
|
}
|
|
619
632
|
async replaceEntries(sessionId, owner, entries) {
|
|
620
|
-
await this.
|
|
633
|
+
await this.withImportLock(sessionId, owner, (importer) => this.importHeld(sessionId, importer, entries, "purge_first"));
|
|
634
|
+
}
|
|
635
|
+
async withImportLock(sessionId, owner, body) {
|
|
636
|
+
const importer = importerOwnerOf(owner);
|
|
637
|
+
return this.withSessionWriteLock(sessionId, () => body(importer));
|
|
621
638
|
}
|
|
622
|
-
async
|
|
639
|
+
async importHeld(sessionId, owner, entries, swap) {
|
|
640
|
+
const row = await this.metaOf(sessionId);
|
|
641
|
+
if (row !== undefined && !ownerEq(row.owner ?? null, owner)) {
|
|
642
|
+
throw new SessionError("conflict", `session ${sessionId} is not the importer's row (its owner cell differs) — nothing was written`);
|
|
643
|
+
}
|
|
644
|
+
await this.swapLogHeld(sessionId, entries, swap);
|
|
645
|
+
await this.finishSwap(sessionId, owner);
|
|
646
|
+
}
|
|
647
|
+
async swapLogHeld(sessionId, entries, swap) {
|
|
623
648
|
if (!this.repo.importEntries)
|
|
624
649
|
throw new SessionError("storage", "session import is not supported by this local backend");
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
650
|
+
const archive = await this.archiveToSwap(sessionId, entries, swap);
|
|
651
|
+
await this.repo.importEntries(sessionId, archive?.owner ?? undefined, entries, { expect: { row: archive } });
|
|
652
|
+
}
|
|
653
|
+
async archiveToSwap(sessionId, entries, swap) {
|
|
654
|
+
switch (swap) {
|
|
655
|
+
case "in_place":
|
|
656
|
+
return (await this.repo.headOf(sessionId)) ?? null;
|
|
657
|
+
case "purge_first":
|
|
658
|
+
validateEntriesForImport(entries);
|
|
659
|
+
await this.repo.delete({ id: sessionId, createdAt: "" });
|
|
660
|
+
return null;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
async finishSwap(sessionId, owner) {
|
|
664
|
+
await this.writeMeta(sessionId, (before) => ({ incarnation: carriedIncarnation(sessionId, before === undefined ? undefined : readSessionMetaCell(before)), owner }));
|
|
665
|
+
await this.touch(sessionId);
|
|
632
666
|
}
|
|
633
667
|
beginImportStaging(realSessionId, token) {
|
|
634
668
|
const h = new LocalStagingHandle(this, realSessionId, stagingIdFor(realSessionId, token));
|
|
@@ -642,7 +676,7 @@ export class LocalSessionStore {
|
|
|
642
676
|
this.stagings.delete(stagingId);
|
|
643
677
|
}
|
|
644
678
|
async commitStagedImport(sessionId, owner, staged, opts) {
|
|
645
|
-
return this.
|
|
679
|
+
return this.withImportLock(sessionId, owner, async (importer) => {
|
|
646
680
|
const current = await this.metaOf(sessionId);
|
|
647
681
|
if (!ownerCellIs(opts.expectedOwner, current === undefined ? undefined : (current.owner ?? null)))
|
|
648
682
|
throw new StagedCommitOwnerChangedError(sessionId);
|
|
@@ -659,11 +693,9 @@ export class LocalSessionStore {
|
|
|
659
693
|
}
|
|
660
694
|
const contentEqual = rel.relation === "identical" && identicalIdsAlsoIdenticalContent(staged, dstEntries);
|
|
661
695
|
if (rel.relation !== "identical" || !contentEqual) {
|
|
662
|
-
await this.
|
|
663
|
-
}
|
|
664
|
-
else {
|
|
665
|
-
await this.restampOwner(sessionId, owner);
|
|
696
|
+
await this.swapLogHeld(sessionId, staged, "in_place");
|
|
666
697
|
}
|
|
698
|
+
await this.finishSwap(sessionId, importer);
|
|
667
699
|
const meta = await this.metaOf(sessionId);
|
|
668
700
|
if (meta === undefined)
|
|
669
701
|
throw new SessionError("storage", `LocalSessionStore: staged commit for session ${sessionId} left no session face — nothing was settled`);
|
|
@@ -685,7 +717,7 @@ export class LocalSessionStore {
|
|
|
685
717
|
if (!isNotFound(e))
|
|
686
718
|
throw e;
|
|
687
719
|
}
|
|
688
|
-
for (const [map, which] of [[this.titles, "title"], [this.pinned, "pinned"], [this.instructionSources, "instruction-source"]]) {
|
|
720
|
+
for (const [map, which] of [[this.titles, "title"], [this.pinned, "pinned"], [this.instructionSources, "instruction-source"], [this.workspaces, "workspace"]]) {
|
|
689
721
|
await map.ensureLoaded();
|
|
690
722
|
if (!(await map.deleteKey(sessionId)))
|
|
691
723
|
throw new SessionError("storage", `LocalSessionStore: ${which} sidecar write failed while deleting session ${sessionId} — its ${which} is still on disk; retry the delete`);
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { Pool as MySqlPool } from "mysql2/promise";
|
|
2
2
|
import type { Pool as PgPool } from "pg";
|
|
3
|
-
import type { MailboxAckResult, MailboxAppendMessage, MailboxLease, MailboxStore } from "@sema-agent/core";
|
|
3
|
+
import type { MailboxAckResult, MailboxAppendMessage, MailboxDecision, MailboxDecisionOutcome, MailboxLease, MailboxStore } from "@sema-agent/core";
|
|
4
4
|
import { type IndexSpec } from "./ensure-index.js";
|
|
5
5
|
import type { MailboxRecipientLifecycle, MailboxRecipientTombstone } from "./mailbox-recipient-lifecycle.js";
|
|
6
6
|
export declare const MAILBOX_TABLE = "mailbox";
|
|
7
7
|
export declare const MAILBOX_MSG_TABLE = "mailbox_message";
|
|
8
8
|
/** [ref] 收件人墓碑表(预删除态的**唯一**真源;形与理由见 `mailbox-recipient-lifecycle.ts` 模块头)。
|
|
9
|
-
* 🔴 为什么是**新表**而不是给 `mailbox`
|
|
10
|
-
* (
|
|
9
|
+
* 🔴 为什么是**新表**而不是给 `mailbox` 加两列:加列走的是本仓「启动 DDL 不开 ALTER seam」那条政策
|
|
10
|
+
* (存量库缺列起服列册对账拒启、要运维先手工 `ALTER`);而 `CREATE TABLE IF NOT EXISTS` 一张新表在存量库上零破坏、零运维动作。语义上也更干净
|
|
11
11
|
* —— `mailbox` 行是**盒**事实(next_seq / lease),墓碑是**收件人**事实,两者的生命周期本来就不同
|
|
12
12
|
* (`drop` 同时终结两者 —— 墓碑随箱亡,理由全文见 `mailbox-recipient-lifecycle.ts` 模块头)。 */
|
|
13
13
|
export declare const MAILBOX_TOMBSTONE_TABLE = "mailbox_tombstone";
|
|
@@ -69,6 +69,12 @@ export declare class TiDBMailboxStore implements MailboxStore, MailboxRecipientL
|
|
|
69
69
|
append(scope: string, handle: string, msg: MailboxAppendMessage): Promise<number>;
|
|
70
70
|
claimLease(scope: string, handle: string, owner: string, ttlMs: number, now?: number): Promise<MailboxLease | null>;
|
|
71
71
|
ack(scope: string, handle: string, owner: string, upToSeq: number): Promise<MailboxAckResult>;
|
|
72
|
+
/**
|
|
73
|
+
* core 7.57.0 #972b —— 一条记录的留置决定从 `expect`(缺席 = 从未 held)移到 `next`:比较并设置,对每一次别的移动与 `ack` 原子
|
|
74
|
+
* (锁序与 ack / drop / reap 同为「盒行 → 消息行」,当前读);**不受租约围栏**(人从哪个终端答都行)。坏调用(词不在闭集 / seq 不是正整数 /
|
|
75
|
+
* 决定律不许的边)先于键判官与任何副作用以 `mailbox.invalid_decision_move` 拒(core 的唯一验证器 `assertMailboxDecisionMove`);判官拒的键没有箱 ⇒ gone。
|
|
76
|
+
*/
|
|
77
|
+
moveDecision(scope: string, handle: string, seq: number, expect: MailboxDecision | undefined, next: MailboxDecision): Promise<MailboxDecisionOutcome>;
|
|
72
78
|
releaseLease(scope: string, handle: string, owner: string): Promise<void>;
|
|
73
79
|
peekCount(scope: string, handle: string): Promise<number>;
|
|
74
80
|
drop(scope: string, handle: string): Promise<void>;
|
|
@@ -93,6 +99,8 @@ export declare class PgMailboxStore implements MailboxStore, MailboxRecipientLif
|
|
|
93
99
|
append(scope: string, handle: string, msg: MailboxAppendMessage): Promise<number>;
|
|
94
100
|
claimLease(scope: string, handle: string, owner: string, ttlMs: number, now?: number): Promise<MailboxLease | null>;
|
|
95
101
|
ack(scope: string, handle: string, owner: string, upToSeq: number): Promise<MailboxAckResult>;
|
|
102
|
+
/** core 7.57.0 #972b 留置决定的比较并设置(TiDB 孪生同注:验证先于一切、锁序盒行 → 消息行、不受租约围栏、判官拒的键 ⇒ gone)。 */
|
|
103
|
+
moveDecision(scope: string, handle: string, seq: number, expect: MailboxDecision | undefined, next: MailboxDecision): Promise<MailboxDecisionOutcome>;
|
|
96
104
|
releaseLease(scope: string, handle: string, owner: string): Promise<void>;
|
|
97
105
|
peekCount(scope: string, handle: string): Promise<number>;
|
|
98
106
|
drop(scope: string, handle: string): Promise<void>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isUtf8 } from "node:buffer";
|
|
2
|
-
import { MAILBOX_INVALID_PEER_META_CODE, MAILBOX_TOMBSTONED_RECIPIENT_CODE, MailboxStoreError, readMailboxPeerMeta } from "@sema-agent/core";
|
|
2
|
+
import { MAILBOX_INVALID_PEER_META_CODE, MAILBOX_RECORD_ID_MAX_CHARS, MAILBOX_TOMBSTONED_RECIPIENT_CODE, MailboxStoreError, assertMailboxDecisionMove, judgeMailboxAppendById, readMailboxAppend, readMailboxPeerMeta, readStoredMailboxDecision } from "@sema-agent/core";
|
|
3
3
|
import { pgHasUnstorable, pgSanitizeText, PgUnstorableError } from "./pg-safe-json.js";
|
|
4
4
|
import { isWellFormedUnicode } from "../principal-jwt.js";
|
|
5
5
|
import { ensureIndexes } from "./ensure-index.js";
|
|
@@ -25,15 +25,36 @@ export const MAILBOX_KEY_MAX_UTF8_BYTES = 190;
|
|
|
25
25
|
export function mailboxKeyProblem(what, v) {
|
|
26
26
|
if (what === "scope" && (v === undefined || v === ""))
|
|
27
27
|
return "MailboxStore: refusing a message without a scope (an empty scope addresses no box)";
|
|
28
|
+
const unaddressable = byteExactIdentityProblem(what, v);
|
|
29
|
+
if (unaddressable !== undefined)
|
|
30
|
+
return unaddressable;
|
|
31
|
+
if (Buffer.byteLength(v, "utf8") > MAILBOX_KEY_MAX_UTF8_BYTES) {
|
|
32
|
+
return `MailboxStore: ${what} exceeds the ${MAILBOX_KEY_MAX_UTF8_BYTES}-byte key column (refusing — a truncated key would desync from its byte-exact lookups)`;
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
function byteExactIdentityProblem(what, v) {
|
|
28
37
|
if (!isWellFormedUnicode(v))
|
|
29
38
|
return `MailboxStore: ${what} is not well-formed Unicode (lone surrogate) (refusing — the driver would store it as U+FFFD and alias a different key)`;
|
|
30
39
|
if ([...v].some((ch) => ch.charCodeAt(0) < 0x20 || ch.charCodeAt(0) === 0x7f))
|
|
31
40
|
return `MailboxStore: ${what} contains control characters (refusing — identity keys must be byte-exact addressable)`;
|
|
32
|
-
if (Buffer.byteLength(v, "utf8") > MAILBOX_KEY_MAX_UTF8_BYTES) {
|
|
33
|
-
return `MailboxStore: ${what} exceeds the ${MAILBOX_KEY_MAX_UTF8_BYTES}-byte key column (refusing — a truncated key would desync from its byte-exact lookups)`;
|
|
34
|
-
}
|
|
35
41
|
return undefined;
|
|
36
42
|
}
|
|
43
|
+
function appendSnapshot(msg) {
|
|
44
|
+
const rec = readMailboxAppend(msg);
|
|
45
|
+
if (rec.id !== undefined) {
|
|
46
|
+
const problem = byteExactIdentityProblem("record id", rec.id);
|
|
47
|
+
if (problem !== undefined)
|
|
48
|
+
throw new Error(problem);
|
|
49
|
+
}
|
|
50
|
+
return rec;
|
|
51
|
+
}
|
|
52
|
+
function replayedSeq(held, scope, handle, logger, asStored) {
|
|
53
|
+
return judgeMailboxAppendById(rowsToMessages(held, scope, handle, logger), asStored);
|
|
54
|
+
}
|
|
55
|
+
function asUtf8Stored(v) {
|
|
56
|
+
return Buffer.from(v, "utf8").toString("utf8");
|
|
57
|
+
}
|
|
37
58
|
function assertMailboxKey(what, v) {
|
|
38
59
|
const problem = mailboxKeyProblem(what, v);
|
|
39
60
|
if (problem !== undefined)
|
|
@@ -130,6 +151,7 @@ function rowsToMessages(rows, scope, handle, logger) {
|
|
|
130
151
|
};
|
|
131
152
|
const hopChain = read("hop_chain", () => decodeHopChain(r.hop_chain, r.seq));
|
|
132
153
|
const peerMeta = read("peer_meta", () => decodePeerMeta(r.peer_meta, r.seq));
|
|
154
|
+
const decision = decisionOfColumn(r.decision);
|
|
133
155
|
out.push({
|
|
134
156
|
seq: Number(r.seq),
|
|
135
157
|
...(r.from_id !== null && r.from_id !== undefined ? { from: bufToStr(r.from_id) } : {}),
|
|
@@ -137,10 +159,21 @@ function rowsToMessages(rows, scope, handle, logger) {
|
|
|
137
159
|
sentAt: Number(r.sent_at_ms),
|
|
138
160
|
...(hopChain !== undefined ? { hopChain } : {}),
|
|
139
161
|
...(peerMeta !== undefined ? { peerMeta } : {}),
|
|
162
|
+
...(decision !== undefined ? { decision } : {}),
|
|
163
|
+
...(r.record_id !== null && r.record_id !== undefined ? { id: String(r.record_id) } : {}),
|
|
140
164
|
});
|
|
141
165
|
}
|
|
142
166
|
return out;
|
|
143
167
|
}
|
|
168
|
+
function decisionOfColumn(raw) {
|
|
169
|
+
return raw === null || raw === undefined ? undefined : readStoredMailboxDecision(bufToStr(raw));
|
|
170
|
+
}
|
|
171
|
+
function decisionMoveVerdict(rows, expect) {
|
|
172
|
+
if (rows.length === 0)
|
|
173
|
+
return { ok: false, current: "gone" };
|
|
174
|
+
const current = decisionOfColumn(rows[0].decision);
|
|
175
|
+
return current === expect ? "write" : { ok: false, current: current ?? "unset" };
|
|
176
|
+
}
|
|
144
177
|
export async function ensureTiDBMailboxSchema(pool) {
|
|
145
178
|
await pool.query(`CREATE TABLE IF NOT EXISTS ${MAILBOX_TABLE} (
|
|
146
179
|
scope_key VARBINARY(190) NOT NULL,
|
|
@@ -166,8 +199,15 @@ export async function ensureTiDBMailboxSchema(pool) {
|
|
|
166
199
|
-- peer_meta(S-90;core fc52b4d4 design/385 片2b):drain 点权威判断读的 typed peer 记录,JSON 文本;
|
|
167
200
|
-- NULL = 缺席(**有语义**:pre-385/foreign 记录,drain 永不回溯准入;与空对象绝不互折)。写入面先过 core
|
|
168
201
|
-- readMailboxPeerMeta(坏值带码拒于任何副作用前)+ PEER_META_MAX_BYTES 守卫关掉静默截断类。
|
|
169
|
-
--
|
|
202
|
+
-- 新列直接折进 CREATE(启动 DDL 不开 ALTER seam);存量库缺列 ⇒ 起服列册对账拒启并给出可粘贴的 ALTER 语句,由运维在升级前执行。
|
|
170
203
|
peer_meta TEXT NULL,
|
|
204
|
+
-- decision(core 7.57.0 #972b):收件人设置留置的这条记录的**决定**(七词闭集 MAILBOX_DECISIONS);NULL = 从未 held。
|
|
205
|
+
-- 只经 moveDecision 的比较并设置移动,从不随 append 写;随记录生随记录亡(ack / reap / drop 删行即删它)—— 决定是记录自己的字段。
|
|
206
|
+
decision VARCHAR(16) NULL,
|
|
207
|
+
-- record_id(core 7.59.0):生产者铸的记录身份;NULL = 没说身份(每次 append 都是一条新记录)。盒持有一条这身份的记录时,append 经引擎的
|
|
208
|
+
-- judgeMailboxAppendById 判(同记录 ⇒ 答它的 seq、零写;不同 ⇒ mailbox.duplicate_record_id),判在 append 的盒行锁之内 —— 盒行 FOR UPDATE 串行了
|
|
209
|
+
-- 同一只盒的全部 append,「(scope, handle, id) 在持有行里至多一行」由它成立;ack / reap / drop 删行即还身份。宽 = 引擎的 MAILBOX_RECORD_ID_MAX_CHARS(列宽读那只常量,不抄数)。
|
|
210
|
+
record_id VARCHAR(${MAILBOX_RECORD_ID_MAX_CHARS}) NULL,
|
|
171
211
|
PRIMARY KEY (scope_key, handle, seq),
|
|
172
212
|
KEY idx_mbm_sent (scope_key, handle, sent_at_ms)
|
|
173
213
|
) COLLATE utf8mb4_bin`);
|
|
@@ -204,6 +244,8 @@ export async function ensurePgMailboxSchema(q) {
|
|
|
204
244
|
sent_at_ms BIGINT NOT NULL,
|
|
205
245
|
hop_chain TEXT COLLATE "C", -- core 5.17.0 design/176(TiDB twin 同注:NULL = 缺席且有语义)
|
|
206
246
|
peer_meta TEXT COLLATE "C", -- S-90 core fc52b4d4 design/385 片2b(TiDB twin 同注:typed peer 记录 JSON;NULL = 缺席且有语义)
|
|
247
|
+
decision VARCHAR(16) COLLATE "C", -- core 7.57.0 #972b(TiDB twin 同注:留置决定,NULL = 从未 held;只经 moveDecision 移动,随记录亡)
|
|
248
|
+
record_id VARCHAR(${MAILBOX_RECORD_ID_MAX_CHARS}) COLLATE "C", -- core 7.59.0(TiDB twin 同注:生产者铸的记录身份,NULL = 没说身份;盒行锁之内经引擎判,随记录亡)
|
|
207
249
|
PRIMARY KEY (scope_key, handle, seq)
|
|
208
250
|
)`);
|
|
209
251
|
await q(`CREATE TABLE IF NOT EXISTS ${MAILBOX_TOMBSTONE_TABLE} (
|
|
@@ -242,8 +284,9 @@ export class TiDBMailboxStore {
|
|
|
242
284
|
async append(scope, handle, msg) {
|
|
243
285
|
assertMailboxKey("scope", scope);
|
|
244
286
|
assertMailboxKey("handle", handle);
|
|
245
|
-
const
|
|
246
|
-
const
|
|
287
|
+
const rec = appendSnapshot(msg);
|
|
288
|
+
const hopChain = encodeHopChain(rec.hopChain);
|
|
289
|
+
const peerMeta = encodePeerMeta(rec.peerMeta);
|
|
247
290
|
return this.tx(async (c) => {
|
|
248
291
|
await c.query(`INSERT INTO ${MAILBOX_TABLE} (scope_key, handle, scope, next_seq) VALUES (?, ?, ?, 1) ON DUPLICATE KEY UPDATE scope_key = scope_key`, [scope, handle, scope]);
|
|
249
292
|
let [rows] = (await c.query(`SELECT next_seq FROM ${MAILBOX_TABLE} WHERE scope_key = ? AND handle = ? FOR UPDATE`, [scope, handle]));
|
|
@@ -254,9 +297,15 @@ export class TiDBMailboxStore {
|
|
|
254
297
|
const [tomb] = await c.query(`SELECT 1 FROM ${MAILBOX_TOMBSTONE_TABLE} WHERE scope_key = ? AND handle = ? FOR UPDATE`, [scope, handle]);
|
|
255
298
|
if (tomb.length > 0)
|
|
256
299
|
throw tombstonedRecipientError(scope, handle);
|
|
300
|
+
if (rec.id !== undefined) {
|
|
301
|
+
const [held] = await c.query(`SELECT seq, from_id, content, sent_at_ms, ${jsonColumnText("tidb", "hop_chain")}, ${jsonColumnText("tidb", "peer_meta")}, decision, record_id FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = ? AND handle = ? AND record_id = ? FOR UPDATE`, [scope, handle, rec.id]);
|
|
302
|
+
const replay = replayedSeq(held, scope, handle, this.logger, { ...rec, content: asUtf8Stored(rec.content), ...(rec.from !== undefined ? { from: asUtf8Stored(rec.from) } : {}) });
|
|
303
|
+
if (replay !== undefined)
|
|
304
|
+
return replay;
|
|
305
|
+
}
|
|
257
306
|
const seq = Number(rows[0].next_seq);
|
|
258
307
|
await c.query(`UPDATE ${MAILBOX_TABLE} SET next_seq = ? WHERE scope_key = ? AND handle = ?`, [seq + 1, scope, handle]);
|
|
259
|
-
await c.query(`INSERT INTO ${MAILBOX_MSG_TABLE} (scope_key, handle, seq, from_id, content, sent_at_ms, hop_chain, peer_meta) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [scope, handle, seq,
|
|
308
|
+
await c.query(`INSERT INTO ${MAILBOX_MSG_TABLE} (scope_key, handle, seq, from_id, content, sent_at_ms, hop_chain, peer_meta, record_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [scope, handle, seq, rec.from ?? null, rec.content, rec.sentAt, hopChain, peerMeta, rec.id ?? null]);
|
|
260
309
|
return seq;
|
|
261
310
|
});
|
|
262
311
|
}
|
|
@@ -274,7 +323,7 @@ export class TiDBMailboxStore {
|
|
|
274
323
|
bufToStr(box.lease_owner) !== owner) {
|
|
275
324
|
return null;
|
|
276
325
|
}
|
|
277
|
-
const [msgs] = (await c.query(`SELECT seq, from_id, content, sent_at_ms, ${jsonColumnText("tidb", "hop_chain")}, ${jsonColumnText("tidb", "peer_meta")} FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = ? AND handle = ? ORDER BY seq FOR UPDATE`, [scope, handle]));
|
|
326
|
+
const [msgs] = (await c.query(`SELECT seq, from_id, content, sent_at_ms, ${jsonColumnText("tidb", "hop_chain")}, ${jsonColumnText("tidb", "peer_meta")}, decision, record_id FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = ? AND handle = ? ORDER BY seq FOR UPDATE`, [scope, handle]));
|
|
278
327
|
if (msgs.length === 0)
|
|
279
328
|
return null;
|
|
280
329
|
const maxSeq = Number(msgs[msgs.length - 1].seq);
|
|
@@ -295,6 +344,22 @@ export class TiDBMailboxStore {
|
|
|
295
344
|
return { acked: true };
|
|
296
345
|
});
|
|
297
346
|
}
|
|
347
|
+
async moveDecision(scope, handle, seq, expect, next) {
|
|
348
|
+
assertMailboxDecisionMove(seq, expect, next);
|
|
349
|
+
if (keyless(scope, handle))
|
|
350
|
+
return { ok: false, current: "gone" };
|
|
351
|
+
return this.tx(async (c) => {
|
|
352
|
+
const [boxes] = await c.query(`SELECT 1 FROM ${MAILBOX_TABLE} WHERE scope_key = ? AND handle = ? FOR UPDATE`, [scope, handle]);
|
|
353
|
+
if (boxes.length === 0)
|
|
354
|
+
return { ok: false, current: "gone" };
|
|
355
|
+
const [rows] = await c.query(`SELECT decision FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = ? AND handle = ? AND seq = ? FOR UPDATE`, [scope, handle, seq]);
|
|
356
|
+
const verdict = decisionMoveVerdict(rows, expect);
|
|
357
|
+
if (verdict !== "write")
|
|
358
|
+
return verdict;
|
|
359
|
+
await c.query(`UPDATE ${MAILBOX_MSG_TABLE} SET decision = ? WHERE scope_key = ? AND handle = ? AND seq = ?`, [next, scope, handle, seq]);
|
|
360
|
+
return { ok: true };
|
|
361
|
+
});
|
|
362
|
+
}
|
|
298
363
|
async releaseLease(scope, handle, owner) {
|
|
299
364
|
if (keyless(scope, handle, owner))
|
|
300
365
|
return;
|
|
@@ -384,11 +449,12 @@ export class PgMailboxStore {
|
|
|
384
449
|
async append(scope, handle, msg) {
|
|
385
450
|
assertMailboxKey("scope", scope);
|
|
386
451
|
assertMailboxKey("handle", handle);
|
|
387
|
-
|
|
452
|
+
const rec = appendSnapshot(msg);
|
|
453
|
+
if (rec.from !== undefined && pgHasUnstorable(rec.from))
|
|
388
454
|
throw new PgUnstorableError("mailbox from");
|
|
389
|
-
const content = pgSanitizeText(
|
|
390
|
-
const hopChain = encodeHopChain(
|
|
391
|
-
const peerMeta = encodePeerMeta(
|
|
455
|
+
const content = pgSanitizeText(rec.content);
|
|
456
|
+
const hopChain = encodeHopChain(rec.hopChain);
|
|
457
|
+
const peerMeta = encodePeerMeta(rec.peerMeta);
|
|
392
458
|
return this.tx(async (c) => {
|
|
393
459
|
await c.query(`INSERT INTO ${MAILBOX_TABLE} (scope_key, handle, scope, next_seq) VALUES ($1, $2, $3, 1) ON CONFLICT (scope_key, handle) DO NOTHING`, [
|
|
394
460
|
scope,
|
|
@@ -407,9 +473,15 @@ export class PgMailboxStore {
|
|
|
407
473
|
const { rows: tomb } = await c.query(`SELECT 1 FROM ${MAILBOX_TOMBSTONE_TABLE} WHERE scope_key = $1 AND handle = $2 FOR UPDATE`, [scope, handle]);
|
|
408
474
|
if (tomb.length > 0)
|
|
409
475
|
throw tombstonedRecipientError(scope, handle);
|
|
476
|
+
if (rec.id !== undefined) {
|
|
477
|
+
const { rows: held } = await c.query(`SELECT seq, from_id, content, sent_at_ms, ${jsonColumnText("pg", "hop_chain")}, ${jsonColumnText("pg", "peer_meta")}, decision, record_id FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = $1 AND handle = $2 AND record_id = $3 FOR UPDATE`, [scope, handle, rec.id]);
|
|
478
|
+
const replay = replayedSeq(held, scope, handle, this.logger, { ...rec, content });
|
|
479
|
+
if (replay !== undefined)
|
|
480
|
+
return replay;
|
|
481
|
+
}
|
|
410
482
|
const seq = Number(rows[0].next_seq);
|
|
411
483
|
await c.query(`UPDATE ${MAILBOX_TABLE} SET next_seq = $1 WHERE scope_key = $2 AND handle = $3`, [seq + 1, scope, handle]);
|
|
412
|
-
await c.query(`INSERT INTO ${MAILBOX_MSG_TABLE} (scope_key, handle, seq, from_id, content, sent_at_ms, hop_chain, peer_meta) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`, [scope, handle, seq,
|
|
484
|
+
await c.query(`INSERT INTO ${MAILBOX_MSG_TABLE} (scope_key, handle, seq, from_id, content, sent_at_ms, hop_chain, peer_meta, record_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, [scope, handle, seq, rec.from ?? null, content, rec.sentAt, hopChain, peerMeta, rec.id ?? null]);
|
|
413
485
|
return seq;
|
|
414
486
|
});
|
|
415
487
|
}
|
|
@@ -424,7 +496,7 @@ export class PgMailboxStore {
|
|
|
424
496
|
const box = boxes[0];
|
|
425
497
|
if (box.lease_owner !== null && Number(box.lease_expires_at_ms) > now && String(box.lease_owner) !== owner)
|
|
426
498
|
return null;
|
|
427
|
-
const { rows: msgs } = await c.query(`SELECT seq, from_id, content, sent_at_ms, ${jsonColumnText("pg", "hop_chain")}, ${jsonColumnText("pg", "peer_meta")} FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = $1 AND handle = $2 ORDER BY seq FOR UPDATE`, [scope, handle]);
|
|
499
|
+
const { rows: msgs } = await c.query(`SELECT seq, from_id, content, sent_at_ms, ${jsonColumnText("pg", "hop_chain")}, ${jsonColumnText("pg", "peer_meta")}, decision, record_id FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = $1 AND handle = $2 ORDER BY seq FOR UPDATE`, [scope, handle]);
|
|
428
500
|
if (msgs.length === 0)
|
|
429
501
|
return null;
|
|
430
502
|
const maxSeq = Number(msgs[msgs.length - 1].seq);
|
|
@@ -445,6 +517,22 @@ export class PgMailboxStore {
|
|
|
445
517
|
return { acked: true };
|
|
446
518
|
});
|
|
447
519
|
}
|
|
520
|
+
async moveDecision(scope, handle, seq, expect, next) {
|
|
521
|
+
assertMailboxDecisionMove(seq, expect, next);
|
|
522
|
+
if (keyless(scope, handle))
|
|
523
|
+
return { ok: false, current: "gone" };
|
|
524
|
+
return this.tx(async (c) => {
|
|
525
|
+
const { rows: boxes } = await c.query(`SELECT 1 FROM ${MAILBOX_TABLE} WHERE scope_key = $1 AND handle = $2 FOR UPDATE`, [scope, handle]);
|
|
526
|
+
if (boxes.length === 0)
|
|
527
|
+
return { ok: false, current: "gone" };
|
|
528
|
+
const { rows } = await c.query(`SELECT decision FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = $1 AND handle = $2 AND seq = $3 FOR UPDATE`, [scope, handle, seq]);
|
|
529
|
+
const verdict = decisionMoveVerdict(rows, expect);
|
|
530
|
+
if (verdict !== "write")
|
|
531
|
+
return verdict;
|
|
532
|
+
await c.query(`UPDATE ${MAILBOX_MSG_TABLE} SET decision = $1 WHERE scope_key = $2 AND handle = $3 AND seq = $4`, [next, scope, handle, seq]);
|
|
533
|
+
return { ok: true };
|
|
534
|
+
});
|
|
535
|
+
}
|
|
448
536
|
async releaseLease(scope, handle, owner) {
|
|
449
537
|
if (keyless(scope, handle, owner))
|
|
450
538
|
return;
|
|
@@ -43,6 +43,7 @@ export class MemoryRunStore {
|
|
|
43
43
|
errorCode: r.errorCode,
|
|
44
44
|
jobId: r.jobId,
|
|
45
45
|
source: r.source,
|
|
46
|
+
identity: r.identity,
|
|
46
47
|
instanceId: r.instanceId,
|
|
47
48
|
cancelRequested: r.cancelRequested,
|
|
48
49
|
objectivePreview: r.objectivePreview,
|
|
@@ -75,6 +76,7 @@ export class MemoryRunStore {
|
|
|
75
76
|
objectivePreview: meta.objectivePreview ?? null,
|
|
76
77
|
idemKey,
|
|
77
78
|
idemBodyFingerprint: meta.idem?.bodyFingerprint ?? null,
|
|
79
|
+
identity: meta.identity ?? null,
|
|
78
80
|
createdAt: now,
|
|
79
81
|
updatedAt: now,
|
|
80
82
|
cancelRequested: false,
|