@sema-agent/server 7.133.0 → 7.134.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 +87 -0
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/USAGE.md +129 -11
- package/deploy/sema-up/README.md +16 -0
- package/deploy/sema-up/chart/Chart.yaml +1 -1
- package/deploy/sema-up/chart/templates/NOTES.txt +15 -0
- package/deploy/sema-up/chart/templates/_helpers.tpl +129 -0
- package/deploy/sema-up/chart/templates/sealed-key.yaml +24 -0
- package/deploy/sema-up/chart/templates/server-v4pro.yaml +14 -0
- package/deploy/sema-up/chart/templates/server.yaml +14 -0
- package/deploy/sema-up/chart/values-full-single.yaml +5 -1
- package/deploy/sema-up/chart/values.yaml +34 -0
- package/dist/approval-ask-audit-store.d.ts +1 -1
- package/dist/approval-card.d.ts +4 -4
- package/dist/approval-card.js +1 -2
- package/dist/approval-content-kind.d.ts +5 -2
- package/dist/approval-content-kind.js +16 -3
- package/dist/approval-gate-currency.d.ts +72 -0
- package/dist/approval-gate-currency.js +94 -0
- package/dist/approval.d.ts +6 -0
- package/dist/approval.js +31 -14
- package/dist/boot/coordinators.d.ts +6 -1
- package/dist/boot/coordinators.js +6 -3
- package/dist/boot/leader.d.ts +2 -0
- package/dist/boot/leader.js +2 -1
- package/dist/boot/read-deny-anchor-audit.js +2 -2
- package/dist/boot/runner-deps.d.ts +18 -5
- package/dist/boot/runner-deps.js +9 -0
- package/dist/boot/stage-06-runners.d.ts +4 -3
- package/dist/boot/stage-06-runners.js +15 -2
- package/dist/boot/stage-07-capability-layer.d.ts +2 -1
- package/dist/boot/stage-08-reapers.d.ts +2 -1
- package/dist/boot/stage-09-leader.d.ts +2 -1
- package/dist/boot/stage-09-leader.js +2 -2
- package/dist/boot/stage-10-http-server.d.ts +2 -1
- package/dist/boot/stage-10-http-server.js +2 -1
- package/dist/boot/stores.d.ts +8 -0
- package/dist/boot/stores.js +8 -12
- package/dist/boot/web-admin.d.ts +6 -0
- package/dist/boot/web-admin.js +11 -2
- package/dist/budget.js +5 -1
- package/dist/caller-text-echo.d.ts +2 -0
- package/dist/caller-text-echo.js +2 -1
- package/dist/canonical-json.d.ts +2 -41
- package/dist/canonical-json.js +2 -13
- package/dist/capabilities/composite-members.d.ts +1 -1
- package/dist/capabilities/composite-members.js +1 -1
- package/dist/capabilities/mcp-probe.js +2 -2
- package/dist/capability-facts.d.ts +2 -0
- package/dist/capability-facts.js +4 -4
- package/dist/checkpoint-ctx-seal-m28.d.ts +42 -0
- package/dist/checkpoint-ctx-seal-m28.js +19 -0
- package/dist/field-conflict.d.ts +2 -1
- package/dist/field-conflict.js +1 -0
- package/dist/file-history-disclosure.d.ts +34 -5
- package/dist/file-history-disclosure.js +2 -2
- package/dist/fleet/fleet-bus.d.ts +4 -0
- package/dist/fleet/fleet-bus.js +1 -0
- package/dist/hooks/hook-runner.js +2 -2
- package/dist/http/admission.js +8 -0
- package/dist/http/approval-sweeps.js +3 -1
- package/dist/http/checkpoint-ctx.d.ts +157 -2
- package/dist/http/checkpoint-ctx.js +158 -2
- package/dist/http/dispatch.js +3 -3
- package/dist/http/durable-run-leg.js +3 -2
- package/dist/http/idempotency.d.ts +2 -2
- package/dist/http/idempotency.js +2 -2
- package/dist/http/principal-gate.d.ts +32 -6
- package/dist/http/principal-gate.js +27 -4
- package/dist/http/refusal-body.d.ts +12 -0
- package/dist/http/refusal-body.js +11 -0
- package/dist/http/request-guards.d.ts +0 -9
- package/dist/http/request-guards.js +2 -16
- package/dist/http/resume-ctx-read.d.ts +48 -1
- package/dist/http/resume-ctx-read.js +26 -3
- package/dist/http/resume-legs.d.ts +2 -1
- package/dist/http/resume-legs.js +37 -22
- package/dist/http/route-ctx.d.ts +0 -9
- package/dist/http/routes/agents-roster.js +2 -3
- package/dist/http/routes/approvals-assistant.js +43 -9
- package/dist/http/routes/capabilities.js +1 -1
- package/dist/http/routes/diagnostics.d.ts +3 -3
- package/dist/http/routes/diagnostics.js +1 -1
- package/dist/http/routes/fleet.js +4 -4
- package/dist/http/routes/memory-origin.d.ts +1 -1
- package/dist/http/routes/rules.js +4 -4
- package/dist/http/routes/runs.js +30 -9
- package/dist/http/routes/session-sync.d.ts +3 -2
- package/dist/http/routes/session-sync.js +38 -20
- package/dist/http/routes/sessions-list.d.ts +1 -1
- package/dist/http/routes/sessions-list.js +3 -4
- package/dist/http/routes/sessions.js +4 -4
- package/dist/http/routes/tasks.js +21 -16
- package/dist/http/routes/trace-usage.js +7 -9
- package/dist/http/run-declared-families.d.ts +16 -0
- package/dist/http/run-declared-families.js +12 -0
- package/dist/http/server.d.ts +6 -0
- package/dist/http/server.js +5 -2
- package/dist/http/submit-ctx.d.ts +4 -3
- package/dist/http/submit-ctx.js +19 -3
- package/dist/http/wire-types.d.ts +7 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/leader/wire.d.ts +4 -0
- package/dist/leader/wire.js +8 -7
- package/dist/main.js +1 -1
- package/dist/memory-origin-fence.js +2 -2
- package/dist/model-compat.d.ts +1 -1
- package/dist/observability/prompt-manifest.d.ts +19 -8
- package/dist/observability/prompt-manifest.js +9 -8
- package/dist/observability/tool-trace.js +8 -9
- package/dist/orchestration/workflow-park-decision.d.ts +1 -1
- package/dist/orchestration/workflow-park-decision.js +1 -0
- package/dist/parked-decide.d.ts +4 -5
- package/dist/parked-decide.js +3 -1
- package/dist/permission-rule-vocab.d.ts +1 -23
- package/dist/permission-rule-vocab.js +0 -13
- package/dist/plugins/approval-payload-fingerprint.d.ts +14 -6
- package/dist/plugins/approval-payload-fingerprint.js +1 -1
- package/dist/plugins/caching-session-store.d.ts +1 -1
- package/dist/plugins/caching-session-store.js +4 -3
- package/dist/plugins/checkpoint-store-sql.d.ts +22 -1
- package/dist/plugins/checkpoint-store-sql.js +45 -1
- package/dist/plugins/local-checkpoint-store.d.ts +13 -2
- package/dist/plugins/local-checkpoint-store.js +26 -0
- package/dist/plugins/local-session-store.d.ts +12 -9
- package/dist/plugins/local-session-store.js +23 -14
- package/dist/plugins/local-session-window.d.ts +12 -6
- package/dist/plugins/local-session-window.js +1 -17
- package/dist/plugins/one-time-migrations.d.ts +11 -0
- package/dist/plugins/one-time-migrations.js +12 -0
- package/dist/plugins/permission-rule-store-file.js +10 -6
- package/dist/plugins/permission-rule-store-sql.js +8 -8
- package/dist/plugins/pg-session-storage.d.ts +1 -1
- package/dist/plugins/pg-session-storage.js +39 -17
- package/dist/plugins/remote-env-device.d.ts +14 -7
- package/dist/plugins/remote-env-device.js +23 -15
- package/dist/plugins/session-ownership.d.ts +11 -0
- package/dist/plugins/session-ownership.js +11 -0
- package/dist/plugins/session-placement.d.ts +12 -14
- package/dist/plugins/session-placement.js +2 -15
- package/dist/plugins/tidb-session-store.d.ts +1 -1
- package/dist/plugins/tidb-session-store.js +39 -17
- package/dist/request-tool-axes.d.ts +12 -0
- package/dist/request-tool-axes.js +20 -0
- package/dist/run-local.js +1 -0
- package/dist/runs.d.ts +2 -1
- package/dist/runs.js +4 -3
- package/dist/sealed-key.d.ts +32 -0
- package/dist/sealed-key.js +55 -1
- package/dist/security.d.ts +9 -7
- package/dist/session-error-wire.d.ts +12 -5
- package/dist/session-error-wire.js +4 -1
- package/dist/session-sync-kernel.d.ts +37 -10
- package/dist/session-sync-kernel.js +10 -0
- package/dist/task-a2a.js +4 -3
- package/dist/task-mcp.d.ts +2 -2
- package/dist/task-mcp.js +20 -8
- package/dist/task-settings.d.ts +5 -1
- package/dist/task-settings.js +1 -1
- package/dist/tool-approval.d.ts +151 -52
- package/dist/tool-approval.js +113 -20
- package/dist/tool-rule-names.d.ts +6 -0
- package/dist/trace/core-keyset-guard.d.ts +4 -3
- package/dist/trace/exit-seat.d.ts +13 -1
- package/dist/trace/exit-seat.js +55 -1
- package/dist/trace/ledger-events.d.ts +3 -1
- package/dist/trace/project.d.ts +18 -1
- package/dist/trace/project.js +16 -4
- package/dist/trace/redact.d.ts +6 -3
- package/dist/trace/redact.js +191 -23
- package/dist/trace/self-minted-wire.d.ts +4 -0
- package/dist/trace/self-minted-wire.js +1 -0
- package/dist/trace/wire-projection-faces.d.ts +6 -0
- package/dist/trace/wire-projection-faces.js +6 -0
- package/dist/web-admin-client/skills-mcp.d.ts +1 -1
- package/dist/web-admin-client/skills-mcp.js +2 -3
- package/dist/web-admin-client/types.d.ts +1 -1
- package/package.json +2 -2
- package/scripts/drain-check-local.mjs +34 -2
- package/scripts/drain-check.mysql.sql +20 -2
- package/scripts/drain-check.pg.sql +20 -2
- package/dist/mcp-tool-faces.d.ts +0 -69
- package/dist/mcp-tool-faces.js +0 -104
|
@@ -1,5 +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
4
|
import { FileSessionRepo, SessionError, StoredSession, validateEntriesForImport } from "@sema-agent/core";
|
|
4
5
|
import { z } from "zod";
|
|
5
6
|
import { contentForkRelation, fastForwardSharedContentDiverged, identicalIdsAlsoIdenticalContent } from "../session-sync-content.js";
|
|
@@ -13,7 +14,7 @@ import { decodeInstructionSource, encodeInstructionSource, sameInstructionSource
|
|
|
13
14
|
import { noteReplayInSessionOpenScope, openSessionForItself } from "../session-records-skipped-notice.js";
|
|
14
15
|
import { createCorruptReadFailOpenSeat } from "../observability/corrupt-read-seat.js";
|
|
15
16
|
import { buildObjectivePreview, isAttachmentFrame } from "../user-authored-text.js";
|
|
16
|
-
import { classifySyncRelationshipByIds, SyncConflictError, stagingIdFor, } from "../session-sync-kernel.js";
|
|
17
|
+
import { classifySyncRelationshipByIds, ownerCellIs, SyncConflictError, StagedCommitOwnerChangedError, stagingIdFor, } from "../session-sync-kernel.js";
|
|
17
18
|
const ownerEq = (a, b) => (a ?? null) === (b ?? null);
|
|
18
19
|
const isNotFound = (e) => e instanceof SessionError && e.code === "not_found";
|
|
19
20
|
const SESSION_OBJECTIVE_PREVIEW_CHARS = 200;
|
|
@@ -459,6 +460,7 @@ export class LocalSessionStore {
|
|
|
459
460
|
await this.meta.ensureLoaded();
|
|
460
461
|
if (ownerRecorded(this.meta.state.entries.get(sessionId)))
|
|
461
462
|
return;
|
|
463
|
+
assertFirstOwnerWritable(sessionId, owner);
|
|
462
464
|
const existed = await this.existsInRepo(sessionId);
|
|
463
465
|
const claim = existed && owner !== null && ownerClaimWindowOpen();
|
|
464
466
|
const wrote = await this.writeMeta(sessionId, (before) => {
|
|
@@ -580,17 +582,21 @@ export class LocalSessionStore {
|
|
|
580
582
|
}
|
|
581
583
|
async fork(sourceId, owner) {
|
|
582
584
|
await this.meta.ensureLoaded();
|
|
583
|
-
|
|
584
|
-
const
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
585
|
+
return this.withSessionWriteLock(sourceId, async () => {
|
|
586
|
+
const source = this.meta.state.entries.get(sourceId);
|
|
587
|
+
const forkOwner = forkOwnerOf(owner, source === undefined ? undefined : readSessionMetaCell(source).owner);
|
|
588
|
+
try {
|
|
589
|
+
const session = await this.repo.fork({ id: sourceId, createdAt: "" }, {});
|
|
590
|
+
const id = (await session.getMetadata()).id;
|
|
591
|
+
await this.writeMeta(id, () => (forkOwner === undefined ? mintSessionIncarnation() : { incarnation: mintSessionIncarnation(), owner: forkOwner }));
|
|
592
|
+
return id;
|
|
593
|
+
}
|
|
594
|
+
catch (e) {
|
|
595
|
+
if (isNotFound(e))
|
|
596
|
+
return null;
|
|
597
|
+
throw e;
|
|
598
|
+
}
|
|
599
|
+
});
|
|
594
600
|
}
|
|
595
601
|
async exportEntries(sessionId) {
|
|
596
602
|
if (!this.repo.exportEntries)
|
|
@@ -657,12 +663,15 @@ export class LocalSessionStore {
|
|
|
657
663
|
}
|
|
658
664
|
async commitStagedImport(sessionId, owner, staged, opts) {
|
|
659
665
|
return this.withSessionWriteLock(sessionId, async () => {
|
|
666
|
+
const current = await this.metaOf(sessionId);
|
|
667
|
+
if (!ownerCellIs(opts.expectedOwner, current === undefined ? undefined : (current.owner ?? null)))
|
|
668
|
+
throw new StagedCommitOwnerChangedError(sessionId);
|
|
660
669
|
const dstEntries = await this.exportEntries(sessionId);
|
|
661
670
|
const rel = classifySyncRelationshipByIds(staged.map((e) => e.id), dstEntries === null ? null : dstEntries.map((e) => e.id));
|
|
662
|
-
if ((rel.relation === "fork" || rel.relation === "stale") && opts
|
|
671
|
+
if ((rel.relation === "fork" || rel.relation === "stale") && opts.resolution !== "overwrite-dst") {
|
|
663
672
|
throw new SyncConflictError(sessionId, rel);
|
|
664
673
|
}
|
|
665
|
-
if (rel.relation === "fast_forward" && opts
|
|
674
|
+
if (rel.relation === "fast_forward" && opts.resolution !== "overwrite-dst" && dstEntries !== null && dstEntries.length > 0) {
|
|
666
675
|
const dstSet = new Set(dstEntries.map((e) => e.id));
|
|
667
676
|
const diverged = fastForwardSharedContentDiverged(staged.filter((e) => dstSet.has(e.id)), dstEntries);
|
|
668
677
|
if (diverged)
|
|
@@ -16,12 +16,18 @@
|
|
|
16
16
|
* branch below the floor — so the record the engine reads is the one the run's walk reaches.
|
|
17
17
|
* Read-only: the log is read once (`exportEntries`, pure), no append log is opened, and the window refuses writes.
|
|
18
18
|
*
|
|
19
|
-
* ## Cost: one linear walk
|
|
20
|
-
* The branch is
|
|
21
|
-
* without a parent ends it
|
|
22
|
-
* 7.
|
|
23
|
-
*
|
|
24
|
-
* the floor.
|
|
19
|
+
* ## Cost: one linear walk, the engine's
|
|
20
|
+
* The branch is core's own `getPathToRoot` over the whole log (the walk every engine read of a local session makes, with its stop
|
|
21
|
+
* rules: an entry without a parent ends it, a parent the log does not hold is `invalid_session`, an unknown leaf is `not_found`).
|
|
22
|
+
* Since core 7.54.0 (#2092) that walk is linear; before, it built the path with `unshift` (quadratic — seconds on a long session, on
|
|
23
|
+
* the event loop) and this module walked the `parentId` chain itself. The second walker is retired: the view reads the branch the
|
|
24
|
+
* engine reads, by the engine's reading. Every later read of the window goes through the same walk, bounded by the floor.
|
|
25
|
+
* A parent CYCLE (a corrupted log; the engine never writes one) is refused by that same walk: since core 7.54.1 it carries one
|
|
26
|
+
* `ParentChainGuard`, and the entry it stands on twice ends it with `invalid_session` naming that entry — `wake` refuses a looped log
|
|
27
|
+
* the way it refuses one whose parent is missing. Core 7.54.0's walk had no guard (a looped log spun it forever on the event loop,
|
|
28
|
+
* and the view's wake with it). The retired walker's own cycle refusal went with it: the engine's guard is the one refusal now, shared
|
|
29
|
+
* by `getPathToRoot` and the run's resume ancestor scan (the two walks core 7.54.1 guards; this module has no walker of its own left).
|
|
30
|
+
* Pinned in `test/session-log-parent-cycle.test.ts`.
|
|
25
31
|
*/
|
|
26
32
|
import { BaseSessionStorage } from "@sema-agent/core";
|
|
27
33
|
export declare class LocalSessionWindow extends BaseSessionStorage {
|
|
@@ -30,25 +30,9 @@ export class LocalSessionWindow extends BaseSessionStorage {
|
|
|
30
30
|
const metadata = (await repo.list()).find((m) => m.id === sessionId) ?? { id: sessionId, createdAt: new Date().toISOString() };
|
|
31
31
|
const whole = new InMemorySessionStorage({ entries: log });
|
|
32
32
|
const leafId = await whole.getLeafId();
|
|
33
|
-
const branch = await
|
|
33
|
+
const branch = await whole.getPathToRoot(leafId);
|
|
34
34
|
const { tail, floorEntryId } = boundedTail(branch);
|
|
35
35
|
return new LocalSessionWindow(metadata, tail, leafId, floorEntryId, branch.slice(0, branch.length - tail.length));
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
-
async function branchOf(whole, leafId, logLength) {
|
|
39
|
-
const fromLeaf = [];
|
|
40
|
-
let current = leafId === null ? undefined : await whole.getEntry(leafId);
|
|
41
|
-
while (current) {
|
|
42
|
-
fromLeaf.push(current);
|
|
43
|
-
if (!current.parentId)
|
|
44
|
-
break;
|
|
45
|
-
if (fromLeaf.length > logLength)
|
|
46
|
-
throw new SessionError("invalid_session", `Entry ${current.id} is on a parent cycle`);
|
|
47
|
-
const parent = await whole.getEntry(current.parentId);
|
|
48
|
-
if (!parent)
|
|
49
|
-
throw new SessionError("invalid_session", `Entry ${current.parentId} not found`);
|
|
50
|
-
current = parent;
|
|
51
|
-
}
|
|
52
|
-
return fromLeaf.reverse();
|
|
53
|
-
}
|
|
54
38
|
//# sourceMappingURL=local-session-window.js.map
|
|
@@ -256,6 +256,17 @@
|
|
|
256
256
|
* 箱的键;判官拒的 owner 持不了租(`ack` / `releaseLease` 照「不是持租者」答、`claimLease` 抛),存量那一格随租约到期失效;② MySQL 协议上带孤立代理的主体
|
|
257
257
|
* 在升级前写下的箱 —— 驱动把孤立代理换成 U+FFFD 的三字节,库里的键与字面带 U+FFFD 的那只主体的键逐字节相同,判官收这只键 ⇒ 不删(挑出来就得删掉一只律认为
|
|
258
258
|
* 存在的箱)。local 车道用引擎自己的 File 信箱(盘上格式归引擎)⇒ 恒 0。
|
|
259
|
+
* · **M28 停驻行里的明文凭据原地封好**(7.134.0)—— 7.134.0 起提交体里请求面凭据位上的每一个值(闭集 `CHECKPOINT_CTX_CREDENTIAL_SLOTS` 与规则的属主
|
|
260
|
+
* `http/checkpoint-ctx.ts` 末段「凭据位密封」;本处不重列)在 `checkpoint_ctx` 里只以封签存在,续跑上下文读口对凭据位上的明文答 409 `conflict.checkpoint_ctx_unsealable`(`reason:"plaintext"`)。升级之前写下的
|
|
261
|
+
* 行是明文 ⇒ **起服扫一遍、原地封好**:不在本文件的迁移段里(封要保管店的封签器,而本段跑在建店的 web-admin 运行时能交给它之前的店层 ——
|
|
262
|
+
* 与 M10 / M19 / M25 同为在本段之外跑的条目),编排在 `checkpoint-ctx-seal-m28.ts`(起服第 6 段,checkpoint 店建成之后、任何路由装配之前调一次),
|
|
263
|
+
* 店腿是两孪生各自的 `rewriteCtxRowsM28`(SQL:键集分页 + 按原文 CAS、`updated_at_ms` 不动;local:逐档原子重写、信封的 `updatedAt` 不动),
|
|
264
|
+
* 行改写是读写两侧同一只封口(`sealStoredCheckpointCtxM28`)。不删行、不改续跑语义;读不出的行计数不动;已封与不带凭据的行零写。读数不进
|
|
265
|
+
* {@link OneTimeMigrationReport}(`reportKeys` 空,与 M25 同形),由起服 info 一行 `checkpoint_ctx_credentials_sealed` 说(扫了几行、封了几行几个值)。
|
|
266
|
+
* 窗外不扫。窗 = 发布日 + 30 天(与 M21 / M23 同一条论证:窗关时缺省部署上升级前停驻的行已被终局兜底收走);窗关之后仍是明文的行由读口的 `plaintext` 拒接住
|
|
267
|
+
* (出路 = 重提)。**本条不覆盖混跑(如实登记)**:≤7.133.x 的续跑读者对凭据位上的封签值没有拒臂(读口、重放判据、店的形判都不看那一格,各消费者按「形不对」把它丢掉、
|
|
268
|
+
* 任务照跑),而它改写过的行与 7.134.0 副本新写的行都是封签 ⇒ 升级序是**先停光 ≤7.133.x 副本、再起第一个 7.134.0 副本**(本条在它起服时跑),回滚之前先结掉带凭据的停驻行
|
|
269
|
+
* (随包 MIGRATION 7.134.0 第 1 条)。
|
|
259
270
|
*
|
|
260
271
|
* ## 收据账(一次起服一只,每一条迁移同一只)
|
|
261
272
|
* 每一步把它**已提交**的改写 / 删除与点名读数当场记进同一只收据账({@link MigrationReceipts}):计数随每次提交累加、行随每次提交追加、形读数在那一步落定时写。走完 ⇒
|
|
@@ -450,6 +450,18 @@ export const ONE_TIME_MIGRATIONS = [
|
|
|
450
450
|
"或照 `docs/DEPLOY-PREREQS.md` 7.133.0 SQL 节的查法按键手工删",
|
|
451
451
|
reportKeys: ["mailboxRejectedKeyRowsRemoved"],
|
|
452
452
|
},
|
|
453
|
+
{
|
|
454
|
+
id: "M28",
|
|
455
|
+
introducedIn: "7.134.0",
|
|
456
|
+
releasedOn: "2026-10-21",
|
|
457
|
+
removableAfter: "2026-11-20",
|
|
458
|
+
removal: "删 `src/checkpoint-ctx-seal-m28.ts` 整个文件与起服第 6 段对它的那一处调用;两孪生的店腿 `rewriteCtxRowsM28` 与读数型 `CtxRowsRewriteM28`;" +
|
|
459
|
+
"`http/checkpoint-ctx.ts` 的 `sealStoredCheckpointCtxM28`;`test/checkpoint-ctx-seal-m28.test.ts` 与 `test/checkpoint-ctx-credential-seal-db-integration.test.ts` 的 M28 格。" +
|
|
460
|
+
"**不随本条删**:凭据位密封的常设规则(封口 / 开口 / 闭集 / 读口第四态 / 409 码 / 装配期拒启)",
|
|
461
|
+
afterRemoval: "读口对凭据位上明文的常设拒臂接住:跳过了带本条的版本直接升的库,带明文凭据的停驻行续跑答 409 `conflict.checkpoint_ctx_unsealable`(`reason:\"plaintext\"`)," +
|
|
462
|
+
"行字节不动 —— 出路 = 作为新任务重提(仍停驻的先取消),或升级时先过一次带本条的版本",
|
|
463
|
+
reportKeys: [],
|
|
464
|
+
},
|
|
453
465
|
];
|
|
454
466
|
export function migrationWindowOpen(window, today) {
|
|
455
467
|
return window.removableAfter === null || today <= window.removableAfter;
|
|
@@ -6,8 +6,7 @@ import { FileDurableRulePartitionProvider, AppendLog, ensureDir, } from "@sema-a
|
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { DurableRuleScopeSchema, buildRulePayloadHash } from "./permission-rule-store-sql.js";
|
|
8
8
|
import { replayJsonlGuarded } from "../observability/corrupt-read-seat.js";
|
|
9
|
-
import { EDITED_RULE_BREADTH_WARNING_CODES, RULE_APPROVAL_RECORD_SCHEMA, UNCOVERED_SEGMENT_REASONS } from "
|
|
10
|
-
import { RULE_BEHAVIORS } from "@sema-agent/core";
|
|
9
|
+
import { EDITED_RULE_BREADTH_WARNING_CODES, RULE_APPROVAL_KINDS, RULE_APPROVAL_RECORD_SCHEMA, RULE_APPROVAL_STATES, RULE_BEHAVIORS, RULE_OWNER_KINDS, UNCOVERED_SEGMENT_REASONS } from "@sema-agent/core";
|
|
11
10
|
const RuleDotSchema = z.object({ actor: z.string().min(1), counter: z.number().int().nonnegative().safe() }).strict();
|
|
12
11
|
const RuleCandidateSchema = z.object({ behavior: z.enum(RULE_BEHAVIORS), rule: z.string(), scope: DurableRuleScopeSchema }).strict();
|
|
13
12
|
const RuleOffer2Schema = z.union([
|
|
@@ -22,14 +21,19 @@ const RuleOffer2Schema = z.union([
|
|
|
22
21
|
})
|
|
23
22
|
.strict(),
|
|
24
23
|
]);
|
|
24
|
+
const OWNER_SHAPE_BY_KIND = {
|
|
25
|
+
principal: z.object({ kind: z.literal("principal"), principal: z.string() }).strict(),
|
|
26
|
+
"local-owner": z.object({ kind: z.literal("local-owner") }).strict(),
|
|
27
|
+
};
|
|
28
|
+
const RuleOwnerSchema = z.union([OWNER_SHAPE_BY_KIND.principal, OWNER_SHAPE_BY_KIND["local-owner"]]);
|
|
25
29
|
const ApprovalRecordSchema = z
|
|
26
30
|
.object({
|
|
27
31
|
id: z.string().min(1),
|
|
28
32
|
principal: z.string().optional(),
|
|
29
|
-
owner:
|
|
33
|
+
owner: RuleOwnerSchema.optional(),
|
|
30
34
|
schema: z.literal(RULE_APPROVAL_RECORD_SCHEMA),
|
|
31
|
-
kind: z.enum(
|
|
32
|
-
state: z.enum(
|
|
35
|
+
kind: z.enum(RULE_APPROVAL_KINDS),
|
|
36
|
+
state: z.enum(RULE_APPROVAL_STATES),
|
|
33
37
|
candidates: z.array(RuleCandidateSchema),
|
|
34
38
|
offers: z.array(RuleOffer2Schema),
|
|
35
39
|
createdAt: z.string(),
|
|
@@ -72,7 +76,7 @@ const StaleApprovalRowSchema = z
|
|
|
72
76
|
id: z.string().min(1),
|
|
73
77
|
schema: z.unknown().optional(),
|
|
74
78
|
principal: z.string().optional(),
|
|
75
|
-
owner:
|
|
79
|
+
owner: RuleOwnerSchema.optional(),
|
|
76
80
|
})
|
|
77
81
|
.loose();
|
|
78
82
|
const StaleApprovalLineSchema = z.object({ op: z.literal("put"), record: StaleApprovalRowSchema }).loose();
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { applyTombstones, assertDeleteDeltaCarriesNoAdd, assertRedemptionNotQuarantined, assertWriteDeltaScopeDurable, foldDelta, isValidDurableScope, parseRuleText, readRowBehavior, screenRuleSyncState, PERMISSION_RULE_WRITER, RULE_BEHAVIORS, RULE_SYNC_DROP_CODES, EMPTY_DURABLE_RULE_PARTITION, } from "@sema-agent/core";
|
|
3
|
+
import { applyTombstones, assertDeleteDeltaCarriesNoAdd, assertRedemptionNotQuarantined, assertWriteDeltaScopeDurable, foldDelta, isValidDurableScope, parseRuleText, readRowBehavior, screenRuleSyncState, PERMISSION_RULE_WRITER, RULE_ADD_ORIGINS, RULE_APPROVAL_KINDS, RULE_APPROVAL_RECORD_SCHEMA, RULE_APPROVAL_STATES, RULE_BEHAVIORS, RULE_SYNC_DROP_CODES, EDITED_RULE_BREADTH_WARNING_CODES, PERSISTED_RULE_MATCHES, UNCOVERED_SEGMENT_REASONS, EMPTY_DURABLE_RULE_PARTITION, } from "@sema-agent/core";
|
|
4
4
|
import { dialectProtocolJsonEncoder, sqlDbNowMs, SQL_DB_NOW_MS } from "./sql-driver.js";
|
|
5
5
|
import { isMissingColumnError } from "./sql-errors.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { canonicalJsonStringify } from "@sema-agent/core";
|
|
7
|
+
import { isPersistedRuleTool } from "../permission-rule-vocab.js";
|
|
8
8
|
import { ensureIndexes } from "./ensure-index.js";
|
|
9
9
|
import { createZodColumnReader, decodeJsonColumn, foldJsonColumn, jsonColumnText, readJsonObjectColumn, SqlJsonColumnError } from "./sql-json-column.js";
|
|
10
10
|
import { redactSecrets } from "../trace/redact.js";
|
|
@@ -25,7 +25,7 @@ export const DurableRuleScopeSchema = z.union([
|
|
|
25
25
|
]);
|
|
26
26
|
const RuleDotSchema = z.object({ actor: z.string().min(1), counter: z.number().int().nonnegative().safe() }).strict();
|
|
27
27
|
const RuleAddSchema = z
|
|
28
|
-
.object({ dot: RuleDotSchema, origin: z.enum(
|
|
28
|
+
.object({ dot: RuleDotSchema, origin: z.enum(RULE_ADD_ORIGINS), createdAt: z.string() })
|
|
29
29
|
.strict();
|
|
30
30
|
const PersistedRuleRowSchema = z
|
|
31
31
|
.object({
|
|
@@ -336,7 +336,7 @@ export function buildRuleOwnerKey(owner) {
|
|
|
336
336
|
return createHash("sha256").update(material, "utf8").digest("hex");
|
|
337
337
|
}
|
|
338
338
|
export function buildRuleBucketChecksum(body) {
|
|
339
|
-
return `sha256:${createHash("sha256").update(
|
|
339
|
+
return `sha256:${createHash("sha256").update(canonicalJsonStringify(body), "utf8").digest("hex")}`;
|
|
340
340
|
}
|
|
341
341
|
const NEXT_DOT_MAX_ATTEMPTS = 8;
|
|
342
342
|
const EMPTY_BUCKET = { actor: "", counter: 0, rev: 0, rules: [], tombstones: [], quarantined: [] };
|
|
@@ -586,8 +586,8 @@ export class SqlRuleApprovalRecordStore {
|
|
|
586
586
|
const offers = decodeJsonColumn(row.offers_json, readOffersJson, `${at}.offers_json`);
|
|
587
587
|
const dotsRaw = row.redeemed_dots_json === null || row.redeemed_dots_json === undefined ? undefined : decodeJsonColumn(row.redeemed_dots_json, readRedeemedDotsJson, `${at}.redeemed_dots_json`);
|
|
588
588
|
const edited = row.edited_json === null || row.edited_json === undefined ? undefined : decodeJsonColumn(row.edited_json, readEditedJson, `${at}.edited_json`);
|
|
589
|
-
const kind = z.enum(
|
|
590
|
-
const state = z.enum(
|
|
589
|
+
const kind = z.enum(RULE_APPROVAL_KINDS).safeParse(row.kind);
|
|
590
|
+
const state = z.enum(RULE_APPROVAL_STATES).safeParse(row.state);
|
|
591
591
|
if (!kind.success || !state.success) {
|
|
592
592
|
throw new Error(`${PERMISSION_RULE_APPROVAL_TABLE} row ${id} carries an unreadable kind/state (${String(row.kind)}/${String(row.state)})`);
|
|
593
593
|
}
|
|
@@ -662,7 +662,7 @@ export class SqlRuleApprovalRecordStore {
|
|
|
662
662
|
}
|
|
663
663
|
export const RULE_IMPORT_TICKET_GC_GRACE_MS = 10 * 60_000;
|
|
664
664
|
export function buildRulePayloadHash(candidates) {
|
|
665
|
-
return createHash("sha256").update(
|
|
665
|
+
return createHash("sha256").update(canonicalJsonStringify(candidates), "utf8").digest("hex");
|
|
666
666
|
}
|
|
667
667
|
export class SqlRuleImportTicketStore {
|
|
668
668
|
db;
|
|
@@ -149,7 +149,7 @@ export declare class PgSessionStore implements SessionStore {
|
|
|
149
149
|
recordInstructionSource(sessionId: string, expected: InstructionSourceBaseline, source: string | null): Promise<void>;
|
|
150
150
|
/** @see TiDBSessionStore.fork — copy the WHOLE durable history (full session_event log, compaction floor bypassed,
|
|
151
151
|
* NOT wake's bounded tail) to a NEW uuidv7 session, re-seq dense from 0, ids/parents verbatim, owner=caller. */
|
|
152
|
-
fork(sourceId: string, owner
|
|
152
|
+
fork(sourceId: string, owner?: string | null): Promise<string | null>;
|
|
153
153
|
/** @see TiDBSessionStore.exportEntries — 2c session-sync EXPORT: the FULL durable session_event log (compaction floor
|
|
154
154
|
* bypassed, NOT wake's tail) as `SessionTreeEntry[]`, oldest-first, verbatim. null if the source is unknown. */
|
|
155
155
|
exportEntries(sessionId: string): Promise<SessionTreeEntry[] | null>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { emitLeafAdvance } from "../session-leaf-bus.js";
|
|
2
|
-
import { BaseSessionStorage, StoredSession, SessionError, uuidv7, validateEntriesForImport, leafIdAfterEntry, } from "@sema-agent/core";
|
|
2
|
+
import { BaseSessionStorage, StoredSession, SessionError, uuidv7, validateEntriesForImport, leafIdAfterEntry, admitSessionPlacement, } from "@sema-agent/core";
|
|
3
3
|
import { SESSION_TITLE_MAX_CHARS } from "../session-row.js";
|
|
4
4
|
import { escapeLike } from "./sql-escape.js";
|
|
5
5
|
import { decodeSessionEventPayload } from "./session-event-payload.js";
|
|
@@ -7,16 +7,17 @@ import { scanEpochAnchor } from "./session-epoch-anchor.js";
|
|
|
7
7
|
import { jsonColumnText } from "./sql-json-column.js";
|
|
8
8
|
import { pgSafeJsonStringify } from "./pg-safe-json.js";
|
|
9
9
|
import { toIso } from "./sql-row-helpers.js";
|
|
10
|
-
import {
|
|
10
|
+
import { placementCapabilityOf, LIST_PLACED_BATCH, PLACEMENT_SELECT_COLUMNS, placedRowFromRow, placementFromRow, placementInsertValues, SQL_SESSION_PLACEMENTS, } from "./session-placement.js";
|
|
11
11
|
import { contentForkRelation, fastForwardSharedContentDiverged } from "../session-sync-content.js";
|
|
12
12
|
import { CLAIM_OWNERLESS_PLACED_SQL, claimOwnerlessParams, createPlacedSessionRow } from "../delegated-session-owner.js";
|
|
13
13
|
import { createLogger } from "../observability/logger.js";
|
|
14
|
-
import { classifySyncRelationshipByIds, SyncConflictError, stagingIdFor, STAGING_ID_MARKER, STAGING_GC_GRACE_MS, } from "../session-sync-kernel.js";
|
|
14
|
+
import { classifySyncRelationshipByIds, SyncConflictError, StagedCommitOwnerChangedError, stagingIdFor, STAGING_ID_MARKER, STAGING_GC_GRACE_MS, } from "../session-sync-kernel.js";
|
|
15
15
|
import { isPgUniqueViolation } from "./sql-errors.js";
|
|
16
16
|
import { BASELINE_IO_DEADLINE_MS, decodeInstructionSource, encodeInstructionSource } from "../instruction-source-notice.js";
|
|
17
17
|
import { MANAGED_RETENTION } from "./retention-store-sql.js";
|
|
18
18
|
import { SESSION_META_INCARNATION_COLUMN, carriedIncarnation, incarnationFromCell, mintSessionIncarnation, sessionFaceFromRow } from "../session-incarnation.js";
|
|
19
19
|
import { ensureIndexes } from "./ensure-index.js";
|
|
20
|
+
import { assertFirstOwnerWritable, forkOwnerOf } from "./session-ownership.js";
|
|
20
21
|
const isDupKey = isPgUniqueViolation;
|
|
21
22
|
function baselineQuery(text, values) {
|
|
22
23
|
return { text, values, query_timeout: BASELINE_IO_DEADLINE_MS };
|
|
@@ -193,7 +194,7 @@ export class PgSessionStore {
|
|
|
193
194
|
const inflight = this.pending.get(sessionId);
|
|
194
195
|
if (inflight) {
|
|
195
196
|
const acquired = await inflight;
|
|
196
|
-
|
|
197
|
+
admitSessionPlacement(sessionId, await this.placementOf(sessionId), opts ?? {}, placementCapabilityOf(this));
|
|
197
198
|
return acquired;
|
|
198
199
|
}
|
|
199
200
|
const p = this.load(sessionId, false, opts).finally(() => this.pending.delete(sessionId));
|
|
@@ -203,7 +204,7 @@ export class PgSessionStore {
|
|
|
203
204
|
async load(sessionId, requireExisting = false, opts) {
|
|
204
205
|
let storage = await PgSessionStorage.wake(this.pool, sessionId);
|
|
205
206
|
if (storage) {
|
|
206
|
-
|
|
207
|
+
admitSessionPlacement(sessionId, (await storage.getMetadata()).placement, opts ?? {}, placementCapabilityOf(this));
|
|
207
208
|
return { session: new StoredSession(storage), sessionId };
|
|
208
209
|
}
|
|
209
210
|
if (requireExisting)
|
|
@@ -213,10 +214,15 @@ export class PgSessionStore {
|
|
|
213
214
|
if (!storage) {
|
|
214
215
|
throw new SessionError("storage", `session ${sessionId} vanished between register and wake (a concurrent purge won the race) — retry`);
|
|
215
216
|
}
|
|
216
|
-
|
|
217
|
+
admitSessionPlacement(sessionId, (await storage.getMetadata()).placement, opts ?? {}, placementCapabilityOf(this));
|
|
217
218
|
return { session: new StoredSession(storage), sessionId };
|
|
218
219
|
}
|
|
219
220
|
async register(sessionId, owner) {
|
|
221
|
+
if (typeof owner !== "string" && owner !== null) {
|
|
222
|
+
if ((await this.ownerOf(sessionId)) !== undefined)
|
|
223
|
+
return;
|
|
224
|
+
assertFirstOwnerWritable(sessionId, owner);
|
|
225
|
+
}
|
|
220
226
|
await this.insertMeta(sessionId, owner, undefined);
|
|
221
227
|
}
|
|
222
228
|
async createOnAcquire(sessionId, placement) {
|
|
@@ -429,10 +435,11 @@ export class PgSessionStore {
|
|
|
429
435
|
}
|
|
430
436
|
}
|
|
431
437
|
async fork(sourceId, owner) {
|
|
432
|
-
const metaRes = await this.pool.query("SELECT leaf_id FROM session_meta WHERE session_id = $1", [sourceId]);
|
|
438
|
+
const metaRes = await this.pool.query("SELECT leaf_id, owner FROM session_meta WHERE session_id = $1", [sourceId]);
|
|
433
439
|
if (!metaRes.rows[0])
|
|
434
440
|
return null;
|
|
435
441
|
const leafId = metaRes.rows[0].leaf_id ?? null;
|
|
442
|
+
const forkOwner = forkOwnerOf(owner, metaRes.rows[0].owner ?? null) ?? null;
|
|
436
443
|
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]);
|
|
437
444
|
const rows = eventRes.rows;
|
|
438
445
|
const newId = uuidv7();
|
|
@@ -445,7 +452,7 @@ export class PgSessionStore {
|
|
|
445
452
|
const payload = String(r.payload);
|
|
446
453
|
await conn.query("INSERT INTO session_event (session_id, seq, entry_id, parent_id, type, payload, ts) VALUES ($1,$2,$3,$4,$5,$6::jsonb,$7)", [newId, seq, r.entry_id, r.parent_id ?? null, r.type, payload, r.ts]);
|
|
447
454
|
}
|
|
448
|
-
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)", [newId,
|
|
455
|
+
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)", [newId, forkOwner, now, now, leafId, rows.length, mintSessionIncarnation()]);
|
|
449
456
|
await conn.query("COMMIT");
|
|
450
457
|
return newId;
|
|
451
458
|
}
|
|
@@ -642,6 +649,23 @@ class PgStagingHandle {
|
|
|
642
649
|
const conn = await this.pool.connect();
|
|
643
650
|
try {
|
|
644
651
|
await conn.query("BEGIN");
|
|
652
|
+
const now = new Date();
|
|
653
|
+
const minted = mintSessionIncarnation();
|
|
654
|
+
if (opts.expectedOwner !== undefined) {
|
|
655
|
+
const cas = await conn.query("UPDATE session_meta SET leaf_seq = leaf_seq + 1 WHERE session_id = $1 AND owner IS NOT DISTINCT FROM $2", [this.realSessionId, opts.expectedOwner]);
|
|
656
|
+
if (cas.rowCount !== 1) {
|
|
657
|
+
await conn.query("ROLLBACK");
|
|
658
|
+
throw new StagedCommitOwnerChangedError(this.realSessionId);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
await conn.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq, incarnation) VALUES ($1,$2,$3,$4,NULL,0,$5) ON CONFLICT (session_id) DO NOTHING", [this.realSessionId, owner, now, now, minted]);
|
|
663
|
+
const claimed = await conn.query("SELECT incarnation FROM session_meta WHERE session_id = $1 FOR UPDATE", [this.realSessionId]);
|
|
664
|
+
if (String(claimed.rows[0]?.incarnation) !== minted) {
|
|
665
|
+
await conn.query("ROLLBACK");
|
|
666
|
+
throw new StagedCommitOwnerChangedError(this.realSessionId);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
645
669
|
const staged = await conn.query("SELECT entry_id FROM session_event WHERE session_id = $1 ORDER BY seq ASC FOR UPDATE", [this.stagingId]);
|
|
646
670
|
const dst = await conn.query("SELECT entry_id FROM session_event WHERE session_id = $1 ORDER BY seq ASC FOR UPDATE", [this.realSessionId]);
|
|
647
671
|
const dstMeta = await conn.query("SELECT placement_kind FROM session_meta WHERE session_id = $1", [this.realSessionId]);
|
|
@@ -650,15 +674,15 @@ class PgStagingHandle {
|
|
|
650
674
|
throw new SessionError("placement_refused", `session ${this.realSessionId} belongs to the ${dstMeta.rows[0].placement_kind} placement partition — a session-sync staged commit cannot overwrite a delegation transcript`);
|
|
651
675
|
}
|
|
652
676
|
const srcIds = staged.rows.map((r) => String(r.entry_id));
|
|
653
|
-
const dstIds =
|
|
677
|
+
const dstIds = opts.expectedOwner !== undefined ? dst.rows.map((r) => String(r.entry_id)) : null;
|
|
654
678
|
const rel = classifySyncRelationshipByIds(srcIds, dstIds);
|
|
655
|
-
if ((rel.relation === "fork" || rel.relation === "stale") && opts
|
|
679
|
+
if ((rel.relation === "fork" || rel.relation === "stale") && opts.resolution !== "overwrite-dst") {
|
|
656
680
|
await conn.query("ROLLBACK");
|
|
657
681
|
throw new SyncConflictError(this.realSessionId, rel);
|
|
658
682
|
}
|
|
659
|
-
if (rel.relation === "fast_forward" && opts
|
|
660
|
-
const dstFull = await conn.query(`SELECT seq, ${jsonColumnText("pg", "payload")} FROM session_event WHERE session_id = $1 ORDER BY seq ASC`, [this.realSessionId]);
|
|
661
|
-
const stagedFull = await conn.query(`SELECT entry_id, ${jsonColumnText("pg", "payload")} FROM session_event WHERE session_id = $1 ORDER BY seq ASC`, [this.stagingId]);
|
|
683
|
+
if (rel.relation === "fast_forward" && opts.resolution !== "overwrite-dst" && dstIds !== null && dstIds.length > 0) {
|
|
684
|
+
const dstFull = await conn.query(`SELECT seq, ${jsonColumnText("pg", "payload")} FROM session_event WHERE session_id = $1 ORDER BY seq ASC FOR UPDATE`, [this.realSessionId]);
|
|
685
|
+
const stagedFull = await conn.query(`SELECT entry_id, ${jsonColumnText("pg", "payload")} FROM session_event WHERE session_id = $1 ORDER BY seq ASC FOR UPDATE`, [this.stagingId]);
|
|
662
686
|
const dstSet = new Set(dstIds);
|
|
663
687
|
const dstEntries = dstFull.rows.map((r) => decodeSessionEventPayload(r.payload, this.realSessionId, r.seq));
|
|
664
688
|
const stagedShared = stagedFull.rows.filter((r) => dstSet.has(String(r.entry_id))).map((r) => decodeSessionEventPayload(r.payload, this.stagingId, r.entry_id));
|
|
@@ -670,13 +694,11 @@ class PgStagingHandle {
|
|
|
670
694
|
}
|
|
671
695
|
await conn.query("DELETE FROM session_event WHERE session_id = $1", [this.realSessionId]);
|
|
672
696
|
await conn.query("UPDATE session_event SET session_id = $1 WHERE session_id = $2", [this.realSessionId, this.stagingId]);
|
|
673
|
-
const lastRes = await conn.query(`SELECT ${jsonColumnText("pg", "payload")} FROM session_event WHERE session_id = $1 ORDER BY seq DESC LIMIT 1`, [this.realSessionId]);
|
|
697
|
+
const lastRes = await conn.query(`SELECT ${jsonColumnText("pg", "payload")} FROM session_event WHERE session_id = $1 ORDER BY seq DESC LIMIT 1 FOR UPDATE`, [this.realSessionId]);
|
|
674
698
|
let leafId = null;
|
|
675
699
|
if (lastRes.rows[0])
|
|
676
700
|
leafId = leafIdAfterEntry(decodeSessionEventPayload(lastRes.rows[0].payload, this.realSessionId, "last staged row"));
|
|
677
|
-
|
|
678
|
-
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) " +
|
|
679
|
-
"ON CONFLICT (session_id) DO UPDATE SET owner = EXCLUDED.owner, updated_at = EXCLUDED.updated_at, leaf_id = EXCLUDED.leaf_id, leaf_seq = EXCLUDED.leaf_seq", [this.realSessionId, owner, now, now, leafId, srcIds.length, mintSessionIncarnation()]);
|
|
701
|
+
await conn.query("UPDATE session_meta SET owner = $1, updated_at = $2, leaf_id = $3, leaf_seq = $4 WHERE session_id = $5", [owner, now, leafId, srcIds.length, this.realSessionId]);
|
|
680
702
|
const afterMeta = await conn.query("SELECT placement_kind, owner, incarnation FROM session_meta WHERE session_id = $1", [this.realSessionId]);
|
|
681
703
|
if (afterMeta.rows[0]?.placement_kind != null) {
|
|
682
704
|
await conn.query("ROLLBACK");
|
|
@@ -17,10 +17,9 @@ type ExecOpts = Parameters<ExecutionEnv["exec"]>[1];
|
|
|
17
17
|
* 🔴 三条不许折错的线:
|
|
18
18
|
* ① `outcome_unknown` **原样过境**——core 把它排除在 `RETRYABLE_REMOTE_ERROR_CODES` 之外正是为了
|
|
19
19
|
* 「可能已经执行过」这一格;折成 `timeout`/`transport_lost` 会让上层按可重试处理,重驱已提交副作用。
|
|
20
|
-
* ② `target_unavailable`(never-started 的诚实词)
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* 粒度粗)。要分得开需要 core 在 `FileErrorCode` 上补 never-started 词 —— 已作 O12 的后续请托登记。
|
|
20
|
+
* ② `target_unavailable`(never-started 的诚实词)**原样过境** —— core 7.54.0([ref] r3)给 `FileErrorCode` 补了同名词
|
|
21
|
+
* (目标在 commit 前拒、从未开始、零效果,目标回来后重试安全;写车道据它撤回首触记录、不计 edited)。修前文件面无对位词,
|
|
22
|
+
* 落 `transport_lost`(「传输中断」= 重试白名单读作「可能已执行」),file 面分不出「从未开始」与「传输中断」—— 那条 O12 后续请托随本版兑现。
|
|
24
23
|
* ③ exec 专属词(`shell_unavailable`/`spawn_error`/`callback_error`/`suspended`/`auth_failed`)出现在
|
|
25
24
|
* file 面上是**设备端协议违约**(它把一个 exec 结局贴到一次文件操作上)⇒ 落 `unknown`,不猜。
|
|
26
25
|
*/
|
|
@@ -38,9 +37,8 @@ type RejectCode = DeviceRejectCode;
|
|
|
38
37
|
/**
|
|
39
38
|
* 汇聚端的 **typed 拒**(投递门**之前**,§5.3 投递边界:设备从未可能见过这条指令)→ core 错误码。
|
|
40
39
|
*
|
|
41
|
-
* 🔴 全部落在**可安全重试**的那一侧,这是投递边界给的硬事实,不是乐观估计。exec
|
|
42
|
-
* `target_unavailable`(「目标在 commit
|
|
43
|
-
* `transport_lost`。
|
|
40
|
+
* 🔴 全部落在**可安全重试**的那一侧,这是投递边界给的硬事实,不是乐观估计。exec 面与 file 面用同一个
|
|
41
|
+
* `target_unavailable`(「目标在 commit 前拒了,指令从未启动」的诚实词;file 面自 core 7.54.0 起有它,修前落 `transport_lost`)。
|
|
44
42
|
* 例外 `identity_mismatch` / `revoked` / `not_found`:那是**永久**拒(换设备/吊销/不是你的),重试
|
|
45
43
|
* 只会同样失败 —— exec 面落 `auth_failed`(NEVER retry 的那一格),file 面无对位词落 `permission_denied`
|
|
46
44
|
* (「这条路你走不通,而且不是暂时的」是它最近的语义)。
|
|
@@ -324,6 +322,15 @@ export declare class RemoteDeviceExecutionEnv implements RemoteExecutionEnv {
|
|
|
324
322
|
* ③ 「这个 placement 根本没绑设备」是**装配/准入错误**,不是一次运行期业务拒绝 —— 响亮抛,让 spawn
|
|
325
323
|
* 失败(core 对 `isolation:"worktree"` 的同款姿势:宁可 throw,绝不静默给一个不对的 env)。
|
|
326
324
|
* 对比:「绑了但现在不在线」是运行期事实 ⇒ 构造成功,`connect()` 回 typed `device.offline`。
|
|
325
|
+
*
|
|
326
|
+
* 🔴 **工厂的每一只拒都是 typed 拒**(core 7.54.0 [ref] r3 契约:工厂抛的 `.code` 原样成失败终局的码):经 {@link deviceFactoryRefusal} 一处铸,码取
|
|
327
|
+
* `DEVICE_ERROR_CODES` 闭集(装配错误取 `config.placement_root_invalid`),句子里的 `[码]` 与 `.code` 同值。修前抛裸 `Error`,码只在句子里。
|
|
328
|
+
*
|
|
329
|
+
* 🔴 **续跑腿的投递围栏**(core 7.54.0 [ref] r3 / #2061,`ExecutionEnvFactoryContext.resumeWorkspace`):行记下了设备(`resumeWorkspace.deviceId`)⇒
|
|
330
|
+
* 本工厂**只**在那台设备上铸 —— placement 根此刻绑的不是它(换绑过)⇒ 抛 typed `device.identity_mismatch`(换绑后赎回的同一个词,零新码),
|
|
331
|
+
* 句子点名两台设备;不连另一台、不改道(新设备没有那个工作区)。读当前绑定只为判出这一拒与取属主,铸的设备恒是行记下的那一台。
|
|
332
|
+
* 「绑的就是它但现在不在线」与新跑同形:构造成功、`connect()` 回 typed `device.offline`(从不连别的设备)。
|
|
333
|
+
* 行没记设备(非设备车道 / 早于该字段的行)⇒ 不声明要兑现的设备,按普通路由走。句柄(`workspaceHandle()`)恒报连接真到达的设备,不抄 `resumeWorkspace`。
|
|
327
334
|
*/
|
|
328
335
|
export declare function deviceExecutionEnvFactory(deps: DeviceEnvFactoryDeps): ExecutionEnvFactory;
|
|
329
336
|
/**
|
|
@@ -22,7 +22,7 @@ export const DEVICE_OUTCOME_TO_FILE_ERROR = {
|
|
|
22
22
|
shell_unavailable: "unknown",
|
|
23
23
|
spawn_error: "unknown",
|
|
24
24
|
suspended: "unknown",
|
|
25
|
-
target_unavailable: "
|
|
25
|
+
target_unavailable: "target_unavailable",
|
|
26
26
|
timeout: "timeout",
|
|
27
27
|
transport_lost: "transport_lost",
|
|
28
28
|
unknown: "unknown",
|
|
@@ -66,20 +66,20 @@ export const DEVICE_REJECT_TO_EXEC_ERROR = {
|
|
|
66
66
|
"draining": "target_unavailable",
|
|
67
67
|
};
|
|
68
68
|
export const DEVICE_REJECT_TO_FILE_ERROR = {
|
|
69
|
-
"device.not_bound": "
|
|
70
|
-
"device.offline": "
|
|
71
|
-
"device.busy": "
|
|
72
|
-
"device.attached_elsewhere": "
|
|
73
|
-
"device.claim_conflict": "
|
|
69
|
+
"device.not_bound": "target_unavailable",
|
|
70
|
+
"device.offline": "target_unavailable",
|
|
71
|
+
"device.busy": "target_unavailable",
|
|
72
|
+
"device.attached_elsewhere": "target_unavailable",
|
|
73
|
+
"device.claim_conflict": "target_unavailable",
|
|
74
74
|
"device.provider_mismatch": "permission_denied",
|
|
75
75
|
"device.identity_mismatch": "permission_denied",
|
|
76
76
|
"device.revoked": "permission_denied",
|
|
77
77
|
"device.enrollment_invalid": "permission_denied",
|
|
78
78
|
"not_found.device": "permission_denied",
|
|
79
|
-
"device.rebind_rev_conflict": "
|
|
80
|
-
"device.rebind_active_run": "
|
|
81
|
-
"limit.rate_exceeded": "
|
|
82
|
-
"draining": "
|
|
79
|
+
"device.rebind_rev_conflict": "target_unavailable",
|
|
80
|
+
"device.rebind_active_run": "target_unavailable",
|
|
81
|
+
"limit.rate_exceeded": "target_unavailable",
|
|
82
|
+
"draining": "target_unavailable",
|
|
83
83
|
};
|
|
84
84
|
const DEVICE_REJECT_TO_REMOTE_ERROR = {
|
|
85
85
|
"device.not_bound": "connect_failed",
|
|
@@ -232,7 +232,7 @@ export class RemoteDeviceExecutionEnv {
|
|
|
232
232
|
failure: {
|
|
233
233
|
remote: "connect_failed",
|
|
234
234
|
exec: "target_unavailable",
|
|
235
|
-
file: "
|
|
235
|
+
file: "target_unavailable",
|
|
236
236
|
message: `[device.admission_unavailable] the device admission read failed, so this session's device could not be confirmed (fail-closed): ${err instanceof Error ? err.message : String(err)}`,
|
|
237
237
|
},
|
|
238
238
|
};
|
|
@@ -780,25 +780,33 @@ const OUTCOME_TO_REMOTE_ERROR = {
|
|
|
780
780
|
transport_lost: "transport_lost",
|
|
781
781
|
unknown: "unknown",
|
|
782
782
|
};
|
|
783
|
+
function deviceFactoryRefusal(code, message) {
|
|
784
|
+
return Object.assign(new Error(message), { code });
|
|
785
|
+
}
|
|
783
786
|
export function deviceExecutionEnvFactory(deps) {
|
|
784
787
|
const execTimeoutMs = deps.execTimeoutMs ?? 1_800_000;
|
|
785
788
|
return async (ctx) => {
|
|
786
789
|
const rootSessionId = ctx.placementRootSessionId;
|
|
787
790
|
if (typeof rootSessionId !== "string" || rootSessionId.length === 0) {
|
|
788
|
-
throw
|
|
791
|
+
throw deviceFactoryRefusal("config.placement_root_invalid", "device lane: ExecutionEnvFactoryContext.placementRootSessionId is empty [config.placement_root_invalid] — " +
|
|
789
792
|
"the device placement is keyed on the run tree's placement root; defaulting to the child sessionId would read as a NEW placement (v2 §4.3.2 / core design/380 O1①)");
|
|
790
793
|
}
|
|
791
794
|
const binding = await deps.store.getSessionBinding(rootSessionId);
|
|
792
795
|
if (!binding) {
|
|
793
|
-
throw
|
|
796
|
+
throw deviceFactoryRefusal(DEVICE_ERROR_CODES.NOT_BOUND, `device lane: no device binding for placement root ${rootSessionId} [${DEVICE_ERROR_CODES.NOT_BOUND}] — ` +
|
|
794
797
|
"the submit-time admission chain must bind the root session to a device before a run reaches the factory (v2 §4.3.2)");
|
|
795
798
|
}
|
|
799
|
+
const recordedDevice = ctx.resumeWorkspace?.deviceId;
|
|
800
|
+
if (recordedDevice !== undefined && recordedDevice !== binding.deviceId) {
|
|
801
|
+
throw deviceFactoryRefusal(DEVICE_ERROR_CODES.IDENTITY_MISMATCH, `device lane: this resume rebuilds the workspace on device ${recordedDevice}, but placement root ${rootSessionId} is now bound to ${binding.deviceId} [${DEVICE_ERROR_CODES.IDENTITY_MISMATCH}] — ` +
|
|
802
|
+
"a parked call does not follow a rebind (the new device has no copy of that workspace); finish or cancel the parked task, then start it again on the device now bound");
|
|
803
|
+
}
|
|
796
804
|
const view = await deps.store.readDeviceForAdmission(binding.deviceId, binding.owner);
|
|
797
805
|
if (!view) {
|
|
798
|
-
throw
|
|
806
|
+
throw deviceFactoryRefusal(DEVICE_ERROR_CODES.NOT_FOUND, `device lane: the bound device is gone or not owned by the binding's principal [${DEVICE_ERROR_CODES.NOT_FOUND}]`);
|
|
799
807
|
}
|
|
800
808
|
if (view.row.status === "revoked") {
|
|
801
|
-
throw
|
|
809
|
+
throw deviceFactoryRefusal(DEVICE_ERROR_CODES.REVOKED, `device lane: the bound device has been revoked [${DEVICE_ERROR_CODES.REVOKED}] — revocation is terminal; re-enrolling mints a NEW deviceId (v2 §8-R4)`);
|
|
802
810
|
}
|
|
803
811
|
assertPosixDevicePlatform(view.row.platformOs, view.row.deviceId);
|
|
804
812
|
return new RemoteDeviceExecutionEnv({
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fork 拷贝的属主 —— core `SessionStore.fork` 契约的唯一读法:
|
|
3
|
+
* 显式串 ⇒ 该串;无参 / 显式 `null` ⇒ 源会话记下的属主(`undefined` = 源会话没记,拷贝也不记)。
|
|
4
|
+
*/
|
|
5
|
+
export declare function forkOwnerOf(requested: string | null | undefined, sourceOwner: string | null | undefined): string | null | undefined;
|
|
6
|
+
/**
|
|
7
|
+
* `register` 首写的 owner 守卫 —— core `SessionStore.register` 契约的唯一读法(三只店同调):首写的 owner 既不是串也不是 `null` ⇒ 响亮拒(不留行);
|
|
8
|
+
* 「首写」由调用方判(已记下属主的会话上的调用照旧静默不改,不经这里)。
|
|
9
|
+
*/
|
|
10
|
+
export declare function assertFirstOwnerWritable(sessionId: string, owner: unknown): asserts owner is string | null;
|
|
11
|
+
//# sourceMappingURL=session-ownership.d.ts.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { SessionError } from "@sema-agent/core";
|
|
2
|
+
export function forkOwnerOf(requested, sourceOwner) {
|
|
3
|
+
return typeof requested === "string" ? requested : sourceOwner;
|
|
4
|
+
}
|
|
5
|
+
export function assertFirstOwnerWritable(sessionId, owner) {
|
|
6
|
+
if (typeof owner === "string" || owner === null)
|
|
7
|
+
return;
|
|
8
|
+
throw new SessionError("storage", `register(${sessionId}): a first ownership write must carry a principal (string) or null (anonymous), got ${owner === undefined ? "undefined" : typeof owner} — ` +
|
|
9
|
+
"a stored value that ownerOf reads back as 'no row' would leave the session claimable forever and ownable never; refusing, nothing written");
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=session-ownership.js.map
|