@sema-agent/server 7.137.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 +328 -139
- package/README.md +1 -0
- package/README.zh-CN.md +1 -0
- package/USAGE.md +67 -20
- package/dist/approval-card.d.ts +21 -14
- package/dist/approval-card.js +1 -0
- package/dist/audit.js +1 -0
- package/dist/background-agent-name.d.ts +5 -0
- package/dist/background-agent-name.js +5 -0
- package/dist/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/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/resolve-spec.d.ts +3 -0
- package/dist/boot/resolve-spec.js +23 -12
- 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-04-stores.d.ts +1 -12
- package/dist/boot/stage-05-execution-env.d.ts +3 -12
- package/dist/boot/stage-05-execution-env.js +7 -4
- package/dist/boot/stage-06-runners.d.ts +3 -12
- package/dist/boot/stage-06-runners.js +5 -2
- package/dist/boot/stage-07-capability-layer.d.ts +3 -12
- package/dist/boot/stage-07-capability-layer.js +2 -2
- package/dist/boot/stage-08-reapers.d.ts +3 -12
- package/dist/boot/stage-09-leader.d.ts +3 -12
- package/dist/boot/stage-09-leader.js +2 -2
- package/dist/boot/stage-10-http-server.d.ts +3 -12
- package/dist/boot/stage-10-http-server.js +4 -2
- 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/budget.d.ts +17 -15
- package/dist/budget.js +2 -2
- package/dist/capabilities/mcp-probe.d.ts +36 -11
- package/dist/capabilities/mcp-probe.js +44 -5
- package/dist/capability-facts.d.ts +6 -0
- package/dist/capability-facts.js +5 -0
- package/dist/checkpoint-identity.d.ts +13 -6
- package/dist/config-catalog.js +7 -1
- package/dist/config-types.d.ts +15 -7
- package/dist/config.js +8 -5
- package/dist/deployment-shape.d.ts +6 -6
- package/dist/gate-settlement.d.ts +71 -0
- package/dist/gate-settlement.js +40 -0
- package/dist/hooks/classic-stations.d.ts +1 -0
- package/dist/hooks/classic-stations.js +1 -0
- package/dist/hooks/command-spawn.d.ts +2 -2
- package/dist/hooks/hook-runner.js +14 -14
- package/dist/http/admission.js +4 -2
- package/dist/http/dispatch.d.ts +4 -0
- package/dist/http/dispatch.js +3 -0
- package/dist/http/durable-run-leg.js +4 -2
- package/dist/http/memory-compliance-failure-reply.js +3 -0
- package/dist/http/resume-legs.d.ts +5 -11
- package/dist/http/resume-legs.js +136 -91
- package/dist/http/route-ctx.d.ts +30 -51
- package/dist/http/route-ctx.js +2 -5
- package/dist/http/routes/a2a-serve.js +10 -4
- package/dist/http/routes/agents-roster.d.ts +1 -1
- package/dist/http/routes/agents-roster.js +5 -1
- package/dist/http/routes/approvals-assistant.js +6 -6
- package/dist/http/routes/capabilities.js +12 -10
- package/dist/http/routes/mcp-reconnect.d.ts +3 -2
- package/dist/http/routes/mcp-reconnect.js +7 -5
- package/dist/http/routes/memory-consolidation.d.ts +1 -1
- package/dist/http/routes/memory-origin.d.ts +1 -1
- package/dist/http/routes/rules.js +3 -3
- package/dist/http/routes/runs.js +11 -7
- package/dist/http/routes/sessions.js +6 -4
- package/dist/http/routes/side-query.js +2 -3
- package/dist/http/routes/tasks.js +792 -780
- package/dist/http/server.d.ts +31 -25
- package/dist/http/wire-types.d.ts +14 -6
- package/dist/identity-source.d.ts +1 -1
- 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-sync-client.js +72 -57
- package/dist/memory-sync.d.ts +1 -1
- package/dist/model-route-gap.d.ts +6 -22
- package/dist/model-route-gap.js +7 -5
- package/dist/mods-declaration.d.ts +110 -0
- package/dist/mods-declaration.js +109 -0
- package/dist/observability/err-text.d.ts +5 -0
- package/dist/observability/err-text.js +9 -0
- package/dist/observability/fail-open.d.ts +5 -1
- package/dist/observability/fail-open.js +5 -1
- package/dist/observability/secret-env-scrub.d.ts +1 -1
- package/dist/observability/secret-env-scrub.js +1 -1
- package/dist/parked-decide.d.ts +15 -2
- package/dist/parked-decide.js +14 -8
- package/dist/peer-seat.d.ts +150 -0
- package/dist/peer-seat.js +239 -0
- package/dist/permission-rule-vocab.d.ts +7 -7
- package/dist/permission-rule-vocab.js +3 -3
- package/dist/plain-string-record.d.ts +38 -0
- package/dist/plain-string-record.js +38 -0
- package/dist/plugins/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 +15 -20
- package/dist/plugins/checkpoint-store-sql.js +8 -8
- package/dist/plugins/file-history-store-sql.js +3 -2
- package/dist/plugins/k8s-pod-reaper.d.ts +2 -8
- package/dist/plugins/k8s-pod-reaper.js +3 -3
- package/dist/plugins/local-checkpoint-store.d.ts +1 -11
- package/dist/plugins/local-checkpoint-store.js +0 -1
- package/dist/plugins/local-session-store.d.ts +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-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 +6 -3
- package/dist/plugins/one-time-migrations.js +3 -4
- package/dist/plugins/permission-rule-store-sql.js +21 -13
- package/dist/plugins/pg-session-storage.d.ts +10 -2
- package/dist/plugins/pg-session-storage.js +18 -5
- package/dist/plugins/remote-env-adb.d.ts +3 -1
- package/dist/plugins/remote-env-adb.js +1 -1
- package/dist/plugins/remote-env-device.d.ts +3 -1
- package/dist/plugins/remote-env-device.js +1 -1
- package/dist/plugins/remote-env-e2b.d.ts +2 -0
- package/dist/plugins/remote-env-e2b.js +1 -1
- package/dist/plugins/remote-env-host.d.ts +3 -1
- package/dist/plugins/remote-env-host.js +1 -1
- package/dist/plugins/remote-env-k8s.d.ts +3 -1
- package/dist/plugins/remote-env-k8s.js +1 -1
- package/dist/plugins/remote-env-local-docker.d.ts +2 -0
- package/dist/plugins/remote-env-local-docker.js +1 -1
- package/dist/plugins/remote-env-ssh.d.ts +3 -1
- package/dist/plugins/remote-env-ssh.js +1 -1
- package/dist/plugins/retention-store-sql.d.ts +14 -2
- package/dist/plugins/retention-store-sql.js +6 -0
- package/dist/plugins/session-fork.d.ts +10 -13
- package/dist/plugins/session-fork.js +5 -25
- package/dist/plugins/session-ownership.d.ts +7 -0
- package/dist/plugins/session-ownership.js +5 -0
- package/dist/plugins/session-workspace-cell.d.ts +17 -0
- package/dist/plugins/session-workspace-cell.js +38 -0
- package/dist/plugins/store-backend.js +7 -7
- package/dist/plugins/tidb-pool.js +6 -0
- package/dist/plugins/tidb-session-store.d.ts +17 -2
- package/dist/plugins/tidb-session-store.js +16 -5
- 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/rules-consent.js +6 -4
- package/dist/run-local.js +12 -6
- package/dist/runs.d.ts +7 -1
- package/dist/runs.js +6 -1
- package/dist/sandbox-git-plane.d.ts +2 -2
- package/dist/server-secret-env.d.ts +6 -3
- package/dist/server-secret-env.js +4 -2
- package/dist/session-incarnation.d.ts +4 -5
- package/dist/session-incarnation.js +2 -2
- package/dist/session-workspace.d.ts +130 -0
- package/dist/session-workspace.js +83 -0
- package/dist/spec-fields.d.ts +1 -1
- package/dist/standing-answer-grant.d.ts +2 -13
- package/dist/standing-answer-grant.js +0 -7
- package/dist/task-a2a.d.ts +44 -14
- package/dist/task-a2a.js +46 -20
- package/dist/task-mcp.d.ts +33 -20
- package/dist/task-mcp.js +46 -22
- package/dist/tool-approval.d.ts +32 -8
- package/dist/tool-approval.js +49 -12
- package/dist/trace/core-keyset-guard.d.ts +33 -18
- package/dist/trace/engine-notice-wire.d.ts +1 -1
- package/dist/trace/engine-notice-wire.js +2 -0
- package/dist/trace/exit-seat.d.ts +20 -1
- package/dist/trace/exit-seat.js +3 -0
- package/dist/trace/extension-call-face.d.ts +33 -0
- package/dist/trace/extension-call-face.js +39 -0
- package/dist/trace/project.d.ts +25 -7
- package/dist/trace/project.js +29 -3
- package/dist/trace/redact.js +42 -13
- package/dist/trace/self-minted-wire.d.ts +129 -10
- package/dist/trace/self-minted-wire.js +62 -3
- package/dist/trace/wire-projection-faces.d.ts +6 -6
- package/dist/trace/wire-projection-faces.js +6 -5
- package/dist/user-authored-text.d.ts +2 -2
- package/dist/user-authored-text.js +16 -1
- package/dist/web-admin-client/center-request.d.ts +3 -2
- package/dist/web-admin-client/center-request.js +2 -3
- package/dist/web-admin-client/skills-mcp.d.ts +8 -4
- package/dist/web-admin-client/skills-mcp.js +31 -18
- package/dist/web-admin-client/types.d.ts +5 -0
- package/package.json +3 -3
- package/wire-changes/server@7.138.0.json +446 -0
- package/dist/http/mcp-remote-text.d.ts +0 -5
- package/dist/http/mcp-remote-text.js +0 -7
|
@@ -3,6 +3,7 @@ import { pgSafeJsonStringify, pgSanitizeText } from "./pg-safe-json.js";
|
|
|
3
3
|
import { isMysqlDupKeyError } from "./sql-errors.js";
|
|
4
4
|
import { ensureIndexes } from "./ensure-index.js";
|
|
5
5
|
import { createObjectColumnReader, decodeJsonColumn, decodeNullableJsonColumn, foldJsonColumn, jsonColumnText } from "./sql-json-column.js";
|
|
6
|
+
import { backgroundAgentNameOf } from "../background-agent-name.js";
|
|
6
7
|
export const BACKGROUND_AGENT_TABLE = "background_agent";
|
|
7
8
|
function summaryCols(dialect) {
|
|
8
9
|
return ("handle, owner, session_scoped, session_id, parent_session_id, root_session_id, name, agent_type, status, " +
|
|
@@ -11,12 +12,14 @@ function summaryCols(dialect) {
|
|
|
11
12
|
const readAgentRecordBody = createObjectColumnReader();
|
|
12
13
|
const readUsageBody = createObjectColumnReader();
|
|
13
14
|
function readRecordColumn(v, handle) {
|
|
15
|
+
let record;
|
|
14
16
|
try {
|
|
15
|
-
|
|
17
|
+
record = decodeJsonColumn(v, readAgentRecordBody, `${BACKGROUND_AGENT_TABLE}.record_json for handle ${handle}`);
|
|
16
18
|
}
|
|
17
19
|
catch (e) {
|
|
18
20
|
throw foldJsonColumn(e, (m) => `BackgroundAgentStore: ${m} — refusing rather than serve a hollow agent record`);
|
|
19
21
|
}
|
|
22
|
+
return { ...record, name: backgroundAgentNameOf(record) };
|
|
20
23
|
}
|
|
21
24
|
function readUsageColumn(v, handle) {
|
|
22
25
|
try {
|
|
@@ -45,7 +48,7 @@ function projParams(stored, json, text) {
|
|
|
45
48
|
stored.parentSessionId ?? null,
|
|
46
49
|
stored.rootSessionId ?? null,
|
|
47
50
|
(stored.sessionScoped ? stored.owner : stored.parentSessionId) ?? null,
|
|
48
|
-
|
|
51
|
+
text(backgroundAgentNameOf(stored)).slice(0, 255),
|
|
49
52
|
stored.agentType !== undefined ? text(stored.agentType).slice(0, 190) : null,
|
|
50
53
|
stored.status,
|
|
51
54
|
stored.spawnedAt,
|
|
@@ -68,7 +71,7 @@ function rowToSummary(r) {
|
|
|
68
71
|
...(r.session_id !== null && r.session_id !== undefined ? { sessionId: String(r.session_id) } : {}),
|
|
69
72
|
...(r.parent_session_id !== null && r.parent_session_id !== undefined ? { parentSessionId: String(r.parent_session_id) } : {}),
|
|
70
73
|
...(r.root_session_id !== null && r.root_session_id !== undefined ? { rootSessionId: String(r.root_session_id) } : {}),
|
|
71
|
-
...(r.name !== null && r.name !== undefined ? { name: String(r.name) } : {}),
|
|
74
|
+
name: backgroundAgentNameOf({ handle: String(r.handle), ...(r.name !== null && r.name !== undefined ? { name: String(r.name) } : {}) }),
|
|
72
75
|
...(r.agent_type !== null && r.agent_type !== undefined ? { agentType: String(r.agent_type) } : {}),
|
|
73
76
|
status: String(r.status),
|
|
74
77
|
spawnedAt: Number(r.spawned_at_ms),
|
|
@@ -3,6 +3,7 @@ import type { OwnerAwareSessionStore, SessionListItem } from "../security.js";
|
|
|
3
3
|
import type { StagingHandle } from "../session-sync-kernel.js";
|
|
4
4
|
import { type PlacementAcquireOpts } from "./session-placement.js";
|
|
5
5
|
import type { InstructionSourceStore } from "../instruction-source-notice.js";
|
|
6
|
+
import type { SessionWorkspaceStore } from "../session-workspace.js";
|
|
6
7
|
import type { OwnerlessClaim } from "../delegated-session-owner.js";
|
|
7
8
|
export interface CachingOptions {
|
|
8
9
|
/** Idle seconds a woken session is kept warm. 0 disables caching (pure passthrough). */
|
|
@@ -85,6 +86,10 @@ export declare class CachingSessionStore implements SessionStore {
|
|
|
85
86
|
* 而装配点按「店上两个方法都在」判席 —— 不转发 = 默认部署上换源通告**静默不发**,且没有任何一处报错。 */
|
|
86
87
|
readonly readInstructionSource?: InstructionSourceStore["readInstructionSource"];
|
|
87
88
|
readonly recordInstructionSource?: InstructionSourceStore["recordInstructionSource"];
|
|
89
|
+
/** 会话行的位置与项目那一格(`session_meta.workspace` / local sidecar;`session-workspace.ts`),纯转发、不经暖缓存。
|
|
90
|
+
* 🔴 必须转发(同上一条教训):装配点与 resolveSpec 按「店上两个方法都在」判这一面 —— 不转发 = 默认部署上不记行、续跑腿不续声明项目,且没有任何一处报错。 */
|
|
91
|
+
readonly readSessionWorkspace?: SessionWorkspaceStore["readSessionWorkspace"];
|
|
92
|
+
readonly recordSessionWorkspace?: SessionWorkspaceStore["recordSessionWorkspace"];
|
|
88
93
|
/** 2c session-sync — delegated export/import seam, DURABLE (bypasses the warm cache exactly like
|
|
89
94
|
* list/fork: an export reads the full durable log, an import writes durable state). */
|
|
90
95
|
readonly exportEntries?: (sessionId: string) => Promise<SessionTreeEntry[] | null>;
|
|
@@ -19,6 +19,8 @@ export class CachingSessionStore {
|
|
|
19
19
|
probeTitle;
|
|
20
20
|
readInstructionSource;
|
|
21
21
|
recordInstructionSource;
|
|
22
|
+
readSessionWorkspace;
|
|
23
|
+
recordSessionWorkspace;
|
|
22
24
|
exportEntries;
|
|
23
25
|
listEntryIds;
|
|
24
26
|
exportEntriesStream;
|
|
@@ -65,6 +67,10 @@ export class CachingSessionStore {
|
|
|
65
67
|
this.readInstructionSource = ownerAware.readInstructionSource.bind(inner);
|
|
66
68
|
if (ownerAware.recordInstructionSource)
|
|
67
69
|
this.recordInstructionSource = ownerAware.recordInstructionSource.bind(inner);
|
|
70
|
+
if (ownerAware.readSessionWorkspace)
|
|
71
|
+
this.readSessionWorkspace = ownerAware.readSessionWorkspace.bind(inner);
|
|
72
|
+
if (ownerAware.recordSessionWorkspace)
|
|
73
|
+
this.recordSessionWorkspace = ownerAware.recordSessionWorkspace.bind(inner);
|
|
68
74
|
if (ownerAware.fork)
|
|
69
75
|
this.fork = ownerAware.fork.bind(inner);
|
|
70
76
|
if (ownerAware.exportEntries)
|
|
@@ -3,6 +3,7 @@ import type { Pool as PgPool } from "pg";
|
|
|
3
3
|
import { type ExecutionOutcomeRecordWord, type GateOutcome, type Checkpoint, type CheckpointGate, type CheckpointState, type CheckpointStore, type CheckpointSummary, type CheckpointToken, type PendingSteerInput, type ResumeOutcome, type ResolvedOutcome, type ResolveExpectation, type ReopenReason, type RiskDescriptor, type StoreDurability, type StoreFidelity, type ToolApprovalCard } from "@sema-agent/core";
|
|
4
4
|
import { type ParkRowFacts, type RuleOfferProjection, type SkillCardFacts } from "../approval-card.js";
|
|
5
5
|
import { type SqlDialect, type SqlDriver } from "./sql-driver.js";
|
|
6
|
+
import type { RetentionDeclaration } from "@sema-agent/core";
|
|
6
7
|
import type { SealedCheckpointCtx } from "../http/checkpoint-ctx.js";
|
|
7
8
|
/** ctx 行改写店腿的读数(两孪生同形):扫到的可读行 · 回写成的行 · 回写成的行里改写函数报的改动数 · 读不出的行 · 改写之后一行装不下的行。 */
|
|
8
9
|
export interface CtxRowsRewrite {
|
|
@@ -464,9 +465,9 @@ export declare class SqlCheckpointStore implements CheckpointStore {
|
|
|
464
465
|
protected readonly logger?: {
|
|
465
466
|
info?(msg: string, meta?: unknown): void;
|
|
466
467
|
} | undefined;
|
|
467
|
-
/**
|
|
468
|
-
*
|
|
469
|
-
readonly retention:
|
|
468
|
+
/** 托管留存声明 = 装配交给它的词(后端按扫车道开不开给;不给 ⇒ `"none"`)。读法与「为什么 SQL 店答 managed 而三方法的实现体在
|
|
469
|
+
* `retention-store-sql.ts`」逐字见 {@link MANAGED_RETENTION}(扫车道开着时本店的 `checkpoint`/`checkpoint_ctx` 两表由那只聚合店按期清)。 */
|
|
470
|
+
readonly retention: RetentionDeclaration;
|
|
470
471
|
/**
|
|
471
472
|
* `CheckpointStore.durability` 声明([ref] 欠账,[ref] 件5)—— 行落在 MySQL-protocol / PostgreSQL 的
|
|
472
473
|
* `checkpoint` 表里,进程重启、副本轮换、整机重建都不丢 ⇒ `"durable"`,如实。
|
|
@@ -500,25 +501,13 @@ export declare class SqlCheckpointStore implements CheckpointStore {
|
|
|
500
501
|
* `wiring-governance-operator.test.ts` 的 [ref] 组;已上报上游求一个能表达该宽度的词。
|
|
501
502
|
*/
|
|
502
503
|
readonly fidelity: StoreFidelity;
|
|
503
|
-
/**
|
|
504
|
-
* `CheckpointStore.redecision` 声明(core 5.22.0 F-012 L2/L3;[ref] clay 裁放行)——**如实按能力判**:
|
|
505
|
-
* 本店的 {@link SqlCheckpointStore.reopen} 是真 CAS 实现(`resolved`→`pending` 原子翻回 + `reopen_reason`
|
|
506
|
-
* 权威列,[ref] D-1 的两条重开腿都走它),声明 `reopen: true` 是读数不是抬举。core 的沙箱准入
|
|
507
|
-
* 模式(pre-flight 要求 `redecision.reopen === true`,declaration never duck-typing——方法在场不算数)
|
|
508
|
-
* 由这一格武装:Kata 腿(`capabilities.isolation` 真声明)+ durable park 部署下,`sandbox_local` ask
|
|
509
|
-
* 自动放行并以 `permission.sandbox_admitted` durable 事件披露(缺席披露=core 侧变异恰红)。
|
|
510
|
-
* `validatingLease` **不声明**:本店没有 durable validating-lease 纪律,不承诺没有的东西。
|
|
511
|
-
* 翻向钉:`wiring-governance-operator.test.ts`(修前恰红,与本声明同 commit)。
|
|
512
|
-
*/
|
|
513
|
-
readonly redecision: {
|
|
514
|
-
readonly reopen: true;
|
|
515
|
-
};
|
|
516
504
|
/**
|
|
517
505
|
* `CheckpointStore.execution` 声明(core 7.7.0 [ref];[ref])——**必填**,且必须恰是 `{ outcome: true }`:
|
|
518
506
|
* 店在此承诺 {@link SqlCheckpointStore.recordExecutionOutcome} 是契约描述的那条真 CAS(谓词、四词、
|
|
519
|
-
* 冲突抛),且 {@link SqlCheckpointStore.reopen} 认这条记录。与 `durability`/`fidelity
|
|
520
|
-
*
|
|
521
|
-
*
|
|
507
|
+
* 冲突抛),且 {@link SqlCheckpointStore.reopen} 认这条记录。与 `durability`/`fidelity` 两格同一条
|
|
508
|
+
* 「声明而非鸭子判」姿势,区别只在这一格**不可缺席**:缺席的店(以及缺 `reopen` 动词的店 —— core 7.57.0 起它是必备
|
|
509
|
+
* 动词,原先的 `redecision` 声明格随之退役)会被 core 在 store 席的第一次读(`resolveCheckpointStore`)以
|
|
510
|
+
* `config.invalid_checkpoint_store` 拒启、句子点名缺的每一个成员 —— 那是契约的正确行为,
|
|
522
511
|
* 因为一台记不下「已决动作最后被怎么处置」的部署,它手里每一条 resolved 行在这条轴上都永远读不出来。
|
|
523
512
|
*/
|
|
524
513
|
readonly execution: {
|
|
@@ -526,7 +515,9 @@ export declare class SqlCheckpointStore implements CheckpointStore {
|
|
|
526
515
|
};
|
|
527
516
|
constructor(db: SqlDriver, logger?: {
|
|
528
517
|
info?(msg: string, meta?: unknown): void;
|
|
529
|
-
} | undefined
|
|
518
|
+
} | undefined, opts?: {
|
|
519
|
+
readonly retention?: RetentionDeclaration;
|
|
520
|
+
});
|
|
530
521
|
/** Pick the dialect's SQL text. Both statements stay written out at the call site ON PURPOSE. */
|
|
531
522
|
private q;
|
|
532
523
|
/** JSON column binding. TiDB: plain `JSON.stringify`, stored verbatim. PG: `pgProtocolJsonStringify` —
|
|
@@ -891,12 +882,16 @@ export declare class SqlCheckpointStore implements CheckpointStore {
|
|
|
891
882
|
export declare class TiDBCheckpointStore extends SqlCheckpointStore {
|
|
892
883
|
constructor(pool: MySqlPool, logger?: {
|
|
893
884
|
info?(msg: string, meta?: unknown): void;
|
|
885
|
+
}, opts?: {
|
|
886
|
+
readonly retention?: RetentionDeclaration;
|
|
894
887
|
});
|
|
895
888
|
}
|
|
896
889
|
/** PostgreSQL binding — historical class name + ctor shape preserved. */
|
|
897
890
|
export declare class PgCheckpointStore extends SqlCheckpointStore {
|
|
898
891
|
constructor(pool: PgPool, logger?: {
|
|
899
892
|
info?(msg: string, meta?: unknown): void;
|
|
893
|
+
}, opts?: {
|
|
894
|
+
readonly retention?: RetentionDeclaration;
|
|
900
895
|
});
|
|
901
896
|
}
|
|
902
897
|
//# sourceMappingURL=checkpoint-store-sql.d.ts.map
|
|
@@ -9,7 +9,7 @@ import { recordFailOpen } from "../observability/fail-open.js";
|
|
|
9
9
|
import { durableRowIdentity, withoutCheckpointToken } from "../checkpoint-identity.js";
|
|
10
10
|
import { mysqlDriver, pgDriver, dialectProtocolJsonEncoder } from "./sql-driver.js";
|
|
11
11
|
import { isDupKeyError, isMissingColumnError, rowTooLargeOrSelf, StoreRowTooLargeError } from "./sql-errors.js";
|
|
12
|
-
import { MANAGED_RETENTION } from "./retention-store-sql.js";
|
|
12
|
+
import { MANAGED_RETENTION, UNMANAGED_RETENTION } from "./retention-store-sql.js";
|
|
13
13
|
import { approvalPayloadFingerprint } from "./approval-payload-fingerprint.js";
|
|
14
14
|
const CTX_REWRITE_PAGE = 500;
|
|
15
15
|
export { approvalPayloadFingerprint } from "./approval-payload-fingerprint.js";
|
|
@@ -336,14 +336,14 @@ const EXECUTION_RECORD_CAS_ATTEMPTS = 8;
|
|
|
336
336
|
export class SqlCheckpointStore {
|
|
337
337
|
db;
|
|
338
338
|
logger;
|
|
339
|
-
retention
|
|
339
|
+
retention;
|
|
340
340
|
durability = "durable";
|
|
341
341
|
fidelity = "json";
|
|
342
|
-
redecision = { reopen: true };
|
|
343
342
|
execution = { outcome: true };
|
|
344
|
-
constructor(db, logger) {
|
|
343
|
+
constructor(db, logger, opts = {}) {
|
|
345
344
|
this.db = db;
|
|
346
345
|
this.logger = logger;
|
|
346
|
+
this.retention = opts.retention ?? UNMANAGED_RETENTION;
|
|
347
347
|
}
|
|
348
348
|
q(tidb, pg) {
|
|
349
349
|
return this.db.dialect === "tidb" ? tidb : pg;
|
|
@@ -798,13 +798,13 @@ export class SqlCheckpointStore {
|
|
|
798
798
|
}
|
|
799
799
|
}
|
|
800
800
|
export class TiDBCheckpointStore extends SqlCheckpointStore {
|
|
801
|
-
constructor(pool, logger) {
|
|
802
|
-
super(mysqlDriver(pool), logger);
|
|
801
|
+
constructor(pool, logger, opts) {
|
|
802
|
+
super(mysqlDriver(pool), logger, opts);
|
|
803
803
|
}
|
|
804
804
|
}
|
|
805
805
|
export class PgCheckpointStore extends SqlCheckpointStore {
|
|
806
|
-
constructor(pool, logger) {
|
|
807
|
-
super(pgDriver(pool), logger);
|
|
806
|
+
constructor(pool, logger, opts) {
|
|
807
|
+
super(pgDriver(pool), logger, opts);
|
|
808
808
|
}
|
|
809
809
|
}
|
|
810
810
|
//# sourceMappingURL=checkpoint-store-sql.js.map
|
|
@@ -721,12 +721,13 @@ export class SqlFileHistoryStore {
|
|
|
721
721
|
bytes = await srcGetBlob(hash);
|
|
722
722
|
}
|
|
723
723
|
catch (err) {
|
|
724
|
-
|
|
724
|
+
const f = foldCaughtFailure(err);
|
|
725
|
+
return { ok: false, error: { code: "adopt_failed", message: `source blob ${hash} fetch failed: ${f.text}`, ...f.fault } };
|
|
725
726
|
}
|
|
726
727
|
if (bytes === undefined)
|
|
727
728
|
return { ok: false, error: { code: "adopt_failed", message: `source blob ${hash} is missing — the envelope's graph cannot be completed, so nothing is published` } };
|
|
728
729
|
if (sha256hex(bytes) !== hash) {
|
|
729
|
-
return { ok: false, error: { code: "
|
|
730
|
+
return { ok: false, error: { code: "adopt_failed", message: `source blob ${hash} served bytes that do not hash to its address — the source cannot supply this blob, so nothing is published` } };
|
|
730
731
|
}
|
|
731
732
|
await onVerified?.(hash, bytes);
|
|
732
733
|
}
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
* 法(只删两形;判决链的缺省臂是「不动作」,删必须被显式选中 —— 销毁不可逆,拿不准恒不删,与 E2B 孤儿回收同一方向纪律):
|
|
8
8
|
* ② `status.phase ∈ {Succeeded, Failed}` —— 容器已全部结束,删它不掐断任何东西;不看 run、不看标签;
|
|
9
9
|
* ① 带 run 标签(`sema.io/run`,只有顶层 run 的 Pod 带)、run 行在 server 店里**已是终态**,且自 run 行**转终态的时刻**(行上的 `updatedAt` ——
|
|
10
|
-
* 三只 run 店的终态写当拍盖它)起超过**这只 Pod 自带的**上界:`max(租约上限 + 宽限 = 6 min, 工作区快照时限 + 引擎拆环境放行
|
|
10
|
+
* 三只 run 店的终态写当拍盖它)起超过**这只 Pod 自带的**上界:`max(租约上限 + 宽限 = 6 min, 工作区快照时限 + 引擎拆环境放行 `teardownLegBudget()`)`
|
|
11
11
|
* ({@link sandboxRunEndHoldMs};租约上限 = 凭据代理的租约律那一只 `LEASE_TTL_MAX_MS`,本模块不另立)。
|
|
12
|
-
* 第二项是 run 收尾的 `destroy()` 在终态之后还可能在 Pod 上存快照的最长时间(
|
|
12
|
+
* 第二项是 run 收尾的 `destroy()` 在终态之后还可能在 Pod 上存快照的最长时间(引擎的拆环境腿按 `teardownLegBudget()` 放行、run 随即落终态,快照按数据时限接着跑)——
|
|
13
13
|
* 那个数据时限是**建这只 Pod 的副本**的配置,所以上界**随 Pod 走、写一次读一次**:适配器建 Pod 时按自己的快照时限算一次、写进注解
|
|
14
14
|
* `sema.io/run-end-hold-ms`(`k8s-exec-protocol.ts` `SANDBOX_RUN_END_HOLD_ANNOTATION`),回收器只读 Pod 自带的这一格,**从不**读回收器所在副本的配置
|
|
15
15
|
* (列举是整个命名空间的 Pod:滚动改 `WORKSPACE_SNAPSHOT_TIMEOUT_MS` 时,一个时限短的副本不得替时限长的副本判它的 Pod)。
|
|
@@ -31,12 +31,6 @@ import type { Logger } from "../observability/logger.js";
|
|
|
31
31
|
import type { Metrics } from "../observability/metrics.js";
|
|
32
32
|
/** ① 的宽限(在租约上限之外再等的一段)。常量。 */
|
|
33
33
|
export declare const SANDBOX_POD_REAP_GRACE_MS = 60000;
|
|
34
|
-
/**
|
|
35
|
-
* 引擎在 run 收尾时给「拆执行环境」那条腿的放行时限:超过它 run 照样落终态,`destroy()` 脱离着继续跑。值 = core 的 `TEARDOWN_LEG_MAX_MS`
|
|
36
|
-
* (`dist/core/runner/teardown-bounded.js`)—— core 不从包根导出它(包的 exports 只有 `.` / `./bench` / `./fixtures`),本仓只能照抄这一个数;
|
|
37
|
-
* 收口 = 向 core 要一个包根读口后改读、删本常量。
|
|
38
|
-
*/
|
|
39
|
-
export declare const ENGINE_TEARDOWN_RELEASE_MS = 15000;
|
|
40
34
|
/** ① 的上界:`max(租约上限 + 宽限, 快照时限 + 引擎拆环境放行)`。唯一的调用方 = 适配器建 Pod 的那一拍(快照时限 = 建它的副本的 `dataTimeoutMs`),结果写进 Pod 的注解。 */
|
|
41
35
|
export declare function sandboxRunEndHoldMs(snapshotTimeoutMs: number): number;
|
|
42
36
|
/** 一个副本两轮回收之间的间隔。常量。 */
|
|
@@ -3,12 +3,12 @@ import { reportPodDeleteFailed } from "./k8s-pod-delete-failure.js";
|
|
|
3
3
|
import { runStatusClassOf } from "./store-contracts.js";
|
|
4
4
|
import { LEASE_TTL_MAX_MS } from "../broker/lease-file.js";
|
|
5
5
|
import { WORKSPACE_SNAPSHOT_TIMEOUT_MS_RANGE } from "../workspace-space.js";
|
|
6
|
+
import { teardownLegBudget } from "@sema-agent/core";
|
|
6
7
|
export const SANDBOX_POD_REAP_GRACE_MS = 60_000;
|
|
7
|
-
export const ENGINE_TEARDOWN_RELEASE_MS = 15_000;
|
|
8
8
|
export function sandboxRunEndHoldMs(snapshotTimeoutMs) {
|
|
9
|
-
return Math.max(LEASE_TTL_MAX_MS + SANDBOX_POD_REAP_GRACE_MS, snapshotTimeoutMs +
|
|
9
|
+
return Math.max(LEASE_TTL_MAX_MS + SANDBOX_POD_REAP_GRACE_MS, snapshotTimeoutMs + teardownLegBudget());
|
|
10
10
|
}
|
|
11
|
-
const RUN_END_HOLD_READABLE = { min: LEASE_TTL_MAX_MS + SANDBOX_POD_REAP_GRACE_MS, max: WORKSPACE_SNAPSHOT_TIMEOUT_MS_RANGE.max +
|
|
11
|
+
const RUN_END_HOLD_READABLE = { min: LEASE_TTL_MAX_MS + SANDBOX_POD_REAP_GRACE_MS, max: WORKSPACE_SNAPSHOT_TIMEOUT_MS_RANGE.max + teardownLegBudget() };
|
|
12
12
|
function runEndHoldOf(raw) {
|
|
13
13
|
if (raw === undefined || !/^[0-9]{1,16}$/.test(raw))
|
|
14
14
|
return undefined;
|
|
@@ -32,17 +32,7 @@ export declare class LocalCheckpointStore {
|
|
|
32
32
|
*/
|
|
33
33
|
readonly fidelity: StoreFidelity;
|
|
34
34
|
/**
|
|
35
|
-
* `CheckpointStore.
|
|
36
|
-
* 与 durability/fidelity 同一条判据:内核 core `FileCheckpointStore` **自己**声明 `{ reopen: true }`,
|
|
37
|
-
* 本类的 {@link LocalCheckpointStore.reopen} 纯委派给它 ⇒ 抄内核的读数是诚实的。包装类**不继承**被
|
|
38
|
-
* 包装者的声明(委派不是子类),所以这一格必须自己写。`validatingLease` 不声明(内核也没声明)。
|
|
39
|
-
* 翻向钉:`wiring-governance-operator.test.ts`(修前恰红,与本声明同 commit)。
|
|
40
|
-
*/
|
|
41
|
-
readonly redecision: {
|
|
42
|
-
readonly reopen: true;
|
|
43
|
-
};
|
|
44
|
-
/**
|
|
45
|
-
* `CheckpointStore.execution` 声明(core 7.7.0 [ref];[ref])——**必填**,与上面三格同一条判据:**如实
|
|
35
|
+
* `CheckpointStore.execution` 声明(core 7.7.0 [ref];[ref])——**必填**,与上面两格同一条判据:**如实
|
|
46
36
|
* 按内核判**。内核 core `FileCheckpointStore` 自己声明 `{ outcome: true }`,并把执行记录落成 ledger 的
|
|
47
37
|
* 一个新事件种类(`execution`,与 resolve 同款「先 append+fsync 再翻内存」的崩溃序),
|
|
48
38
|
* {@link LocalCheckpointStore.recordExecutionOutcome} 与 {@link LocalCheckpointStore.reopen} 都是纯委派
|
|
@@ -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`);
|