@sema-agent/server 7.143.0 → 7.145.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.
Files changed (139) hide show
  1. package/MIGRATION.md +295 -0
  2. package/README.md +3 -0
  3. package/README.zh-CN.md +1 -0
  4. package/USAGE.md +34 -16
  5. package/dist/approval-card.d.ts +31 -14
  6. package/dist/approval-card.js +8 -8
  7. package/dist/audit.d.ts +2 -21
  8. package/dist/audit.js +0 -9
  9. package/dist/boot/execution-env.js +1 -1
  10. package/dist/boot/runner-deps.d.ts +10 -5
  11. package/dist/boot/runner-deps.js +5 -4
  12. package/dist/boot/session-faces.d.ts +1 -1
  13. package/dist/boot/session-faces.js +3 -3
  14. package/dist/boot/shutdown.d.ts +1 -1
  15. package/dist/boot/shutdown.js +4 -2
  16. package/dist/boot/stage-01-config.js +2 -0
  17. package/dist/boot/stage-07-capability-layer.js +1 -1
  18. package/dist/boot/stage-09-leader.d.ts +1 -1
  19. package/dist/boot/stage-10-http-server.d.ts +1 -1
  20. package/dist/boot/stage-10-http-server.js +4 -9
  21. package/dist/capabilities/scenarios.d.ts +14 -18
  22. package/dist/capabilities/scenarios.js +1 -2
  23. package/dist/config-d-migration.d.ts +3 -0
  24. package/dist/delegated-session-owner.d.ts +33 -151
  25. package/dist/delegated-session-owner.js +1 -83
  26. package/dist/entry-mode-clamp-notice.d.ts +1 -1
  27. package/dist/execution-placement.d.ts +7 -4
  28. package/dist/fleet/fleet-bus.d.ts +13 -3
  29. package/dist/fleet/fleet-bus.js +17 -3
  30. package/dist/hooks/classic-stations.d.ts +3 -1
  31. package/dist/hooks/classic-stations.js +2 -0
  32. package/dist/http/admission.js +2 -1
  33. package/dist/http/checkpoint-ctx.d.ts +1 -0
  34. package/dist/http/checkpoint-ctx.js +1 -0
  35. package/dist/http/durable-run-leg.js +7 -6
  36. package/dist/http/principal-gate.d.ts +1 -1
  37. package/dist/http/refusal-body.d.ts +7 -3
  38. package/dist/http/refusal-body.js +1 -0
  39. package/dist/http/resume-legs.js +16 -7
  40. package/dist/http/route-ctx.d.ts +15 -0
  41. package/dist/http/routes/approvals-assistant.d.ts +20 -1
  42. package/dist/http/routes/approvals-assistant.js +28 -6
  43. package/dist/http/routes/rules.d.ts +1 -1
  44. package/dist/http/routes/rules.js +5 -2
  45. package/dist/http/routes/session-sync.js +12 -0
  46. package/dist/http/routes/tasks.js +12 -9
  47. package/dist/http/run-meta.d.ts +14 -7
  48. package/dist/http/run-meta.js +5 -2
  49. package/dist/http/server.d.ts +13 -5
  50. package/dist/http/server.js +3 -0
  51. package/dist/http/submit-ctx.d.ts +12 -0
  52. package/dist/http/submit-ctx.js +23 -0
  53. package/dist/index.d.ts +1 -0
  54. package/dist/index.js +1 -0
  55. package/dist/lane-home-probe.d.ts +15 -0
  56. package/dist/lane-home-probe.js +4 -0
  57. package/dist/observability/corrupt-read-seat.d.ts +5 -9
  58. package/dist/observability/corrupt-read-seat.js +1 -2
  59. package/dist/observability/fail-open.d.ts +26 -10
  60. package/dist/observability/fail-open.js +26 -10
  61. package/dist/observability/run-terminal.d.ts +1 -1
  62. package/dist/observability/run-terminal.js +2 -2
  63. package/dist/parked-revive-surface.d.ts +6 -7
  64. package/dist/peer-inbound-face.d.ts +29 -34
  65. package/dist/peer-inbound-face.js +28 -63
  66. package/dist/plugins/approval-ask-store-file.d.ts +16 -2
  67. package/dist/plugins/approval-ask-store-file.js +21 -1
  68. package/dist/plugins/approval-ask-store-memory.d.ts +8 -2
  69. package/dist/plugins/approval-ask-store-memory.js +11 -8
  70. package/dist/plugins/caching-session-store.d.ts +3 -3
  71. package/dist/plugins/caching-session-store.js +3 -3
  72. package/dist/plugins/checkpoint-store-sql.d.ts +62 -37
  73. package/dist/plugins/checkpoint-store-sql.js +123 -40
  74. package/dist/plugins/file-run-store.d.ts +13 -1
  75. package/dist/plugins/file-run-store.js +11 -1
  76. package/dist/plugins/fork-routing-session-store.d.ts +8 -1
  77. package/dist/plugins/fork-routing-session-store.js +6 -0
  78. package/dist/plugins/local-checkpoint-store.d.ts +17 -8
  79. package/dist/plugins/local-checkpoint-store.js +26 -7
  80. package/dist/plugins/local-session-store.d.ts +105 -34
  81. package/dist/plugins/local-session-store.js +168 -65
  82. package/dist/plugins/local-session-window.d.ts +4 -2
  83. package/dist/plugins/memory-run-store.d.ts +2 -0
  84. package/dist/plugins/memory-run-store.js +6 -0
  85. package/dist/plugins/one-time-migrations.d.ts +12 -12
  86. package/dist/plugins/one-time-migrations.js +13 -13
  87. package/dist/plugins/pg-session-storage.d.ts +1 -9
  88. package/dist/plugins/pg-session-storage.js +2 -19
  89. package/dist/plugins/remote-env-host.d.ts +8 -4
  90. package/dist/plugins/remote-env-host.js +7 -9
  91. package/dist/plugins/remote-shell.js +12 -2
  92. package/dist/plugins/run-store-sql.d.ts +4 -0
  93. package/dist/plugins/run-store-sql.js +3 -0
  94. package/dist/plugins/schema-consistency.d.ts +5 -1
  95. package/dist/plugins/session-fork.d.ts +1 -1
  96. package/dist/plugins/session-ownership.d.ts +9 -0
  97. package/dist/plugins/session-ownership.js +6 -0
  98. package/dist/plugins/session-store.d.ts +13 -16
  99. package/dist/plugins/session-store.js +9 -10
  100. package/dist/plugins/sql-json-column.d.ts +24 -2
  101. package/dist/plugins/sql-json-column.js +38 -3
  102. package/dist/plugins/store-backend.js +3 -2
  103. package/dist/plugins/tidb-session-store.d.ts +2 -14
  104. package/dist/plugins/tidb-session-store.js +2 -20
  105. package/dist/prompt-screen.d.ts +21 -0
  106. package/dist/prompt-screen.js +8 -0
  107. package/dist/rules-consent.d.ts +63 -51
  108. package/dist/rules-consent.js +6 -10
  109. package/dist/run-acceptance.d.ts +40 -10
  110. package/dist/run-acceptance.js +38 -5
  111. package/dist/run-local.js +2 -1
  112. package/dist/runs.js +5 -1
  113. package/dist/security.d.ts +10 -7
  114. package/dist/session-agent.d.ts +39 -0
  115. package/dist/session-agent.js +16 -0
  116. package/dist/session-error-wire.d.ts +25 -26
  117. package/dist/session-error-wire.js +4 -5
  118. package/dist/session-incarnation.d.ts +2 -2
  119. package/dist/session-notify-door.d.ts +2 -2
  120. package/dist/session-notify-door.js +5 -1
  121. package/dist/tool-approval.d.ts +3 -1
  122. package/dist/tool-approval.js +47 -18
  123. package/dist/trace/core-keyset-guard.d.ts +1 -1
  124. package/dist/trace/engine-notice-wire.d.ts +1 -1
  125. package/dist/trace/engine-notice-wire.js +1 -1
  126. package/dist/trace/ledger-sink.js +12 -4
  127. package/dist/trace/model-segment.d.ts +26 -6
  128. package/dist/trace/model-segment.js +58 -9
  129. package/dist/trace/project.d.ts +8 -2
  130. package/dist/trace/project.js +6 -4
  131. package/dist/trace/redact.d.ts +77 -2
  132. package/dist/trace/redact.js +234 -99
  133. package/dist/trace/self-minted-wire.d.ts +160 -9
  134. package/dist/trace/self-minted-wire.js +52 -3
  135. package/package.json +3 -3
  136. package/wire-changes/server@7.144.0.json +291 -0
  137. package/wire-changes/server@7.145.0.json +146 -0
  138. package/dist/session-records-skipped-notice.d.ts +0 -21
  139. package/dist/session-records-skipped-notice.js +0 -27
@@ -3,7 +3,6 @@ import type { Pool, PoolClient } from "pg";
3
3
  import type { SessionSummary } from "./store-contracts.js";
4
4
  import { type ClientSessionRowWrite, type ReplaySessionRowWrite, type ReplaySessionRowWriteOutcome, type SessionRowPatch, type SessionRowWriteOutcome } from "../session-row.js";
5
5
  import { type PlacementAcquireOpts } from "./session-placement.js";
6
- import { type OrphanOwnerLog } from "../delegated-session-owner.js";
7
6
  import { type StagingHandle } from "../session-sync-kernel.js";
8
7
  import { type InstructionSourceBaseline } from "../instruction-source-notice.js";
9
8
  import type { SessionWorkspace } from "../session-workspace.js";
@@ -71,26 +70,19 @@ export declare class PgSessionStore implements SessionStore {
71
70
  readonly durability: "durable";
72
71
  /** In-flight acquisitions keyed by id, so concurrent acquire(sameId) in one process share one. */
73
72
  private readonly pending;
74
- /** 委派子代孤行那一行 warn 的去处(TiDB 孪生同格)。 */
75
- private readonly log;
76
73
  /** 执行席(TiDB 孪生同格;实现与语句的唯一属主 = `execution-placement-sql.ts`,本店只交自己的池)。 */
77
74
  readonly executionPlacement: ExecutionPlacementSeat;
78
75
  /** 本店的池包成的 SqlDriver(TiDB 孪生同格):执行席与会话行格写原语的事务都经它借连接。 */
79
76
  private readonly db;
80
77
  constructor(pool: Pool, opts?: {
81
- readonly log?: OrphanOwnerLog;
82
78
  readonly retention?: RetentionDeclaration;
83
79
  });
84
80
  acquire(sessionId?: string, opts?: PlacementAcquireOpts): Promise<AcquiredSession>;
85
81
  private load;
86
82
  /** Idempotently create a session_meta row with an owner (core `SessionStore.register`). Existing owner is never overwritten. */
87
83
  register(sessionId: string, owner: string | null): Promise<void>;
88
- /** acquire 的建行(唯一一处;[ref] 委派继承律,只住在 `createPlacedSessionRow`,本店交既有读写口 —— TiDB 孪生同名方法同注)。 */
84
+ /** acquire 的建行(唯一一处;placed 会话的属主 = `placement.owner` 原样,core 7.62.0 [ref] 盒义务 7 —— TiDB 孪生同名方法同注)。 */
89
85
  private createOnAcquire;
90
- /** [ref] 委派继承律的认领口(TiDB 孪生 {@link TiDBSessionStore.claimOwnerless} 同义;句 = `CLAIM_OWNERLESS_PLACED_SQL`,规则在 `delegated-session-owner.ts`)。 */
91
- claimOwnerless(sessionId: string, incarnation: string, owner: string): Promise<boolean>;
92
- /** 若干来源行的会话面(属主 + 化身)一次读回(TiDB 孪生同义;活住客的投影 `sessionFaceFromRow`,空化身 ⇒ 响亮拒)。 */
93
- private sourceFaces;
94
86
  /** The one session_meta INSERT. Existing owner is never overwritten.
95
87
  * [ref]:`placement` 与行同一条 INSERT;`DO NOTHING` 臂 ⇒ 已存在行的元组永不被重写(first-write 不可变)。 */
96
88
  private insertMeta;
@@ -10,8 +10,6 @@ import { pgSafeJsonStringify } from "./pg-safe-json.js";
10
10
  import { toIso } from "./sql-row-helpers.js";
11
11
  import { placementCapabilityOf, LIST_PLACED_BATCH, PLACEMENT_SELECT_COLUMNS, placedRowFromRow, placementFromRow, placementInsertValues, SESSION_DURABILITY, SESSION_PLACEMENTS, } from "./session-placement.js";
12
12
  import { contentForkRelation, fastForwardSharedContentDiverged } from "../session-sync-content.js";
13
- import { CLAIM_OWNERLESS_PLACED_SQL, claimOwnerlessParams, createPlacedSessionRow } from "../delegated-session-owner.js";
14
- import { createLogger } from "../observability/logger.js";
15
13
  import { classifySyncRelationshipByIds, SyncConflictError, StagedCommitOwnerChangedError, stagingIdFor, STAGING_ID_MARKER, STAGING_GC_GRACE_MS, } from "../session-sync-kernel.js";
16
14
  import { isPgUniqueViolation } from "./sql-errors.js";
17
15
  import { BASELINE_IO_DEADLINE_MS, decodeInstructionSource, encodeInstructionSource } from "../instruction-source-notice.js";
@@ -19,7 +17,7 @@ import { SESSION_META_WORKSPACE_COLUMN, decodeSessionWorkspaceCell, encodeSessio
19
17
  import { MANAGED_RETENTION, UNMANAGED_RETENTION } from "./retention-store-sql.js";
20
18
  import { SESSION_META_INCARNATION_COLUMN, carriedRowCells, headFromRow, incarnationFromCell, mintSessionIncarnation, sessionFaceFromRow } from "../session-incarnation.js";
21
19
  import { ensureIndexes } from "./ensure-index.js";
22
- import { assertFirstOwnerWritable, forkOwnerOf, isSessionOwnerValue } from "./session-ownership.js";
20
+ import { assertFirstOwnerWritable, forkOwnerOf, isSessionOwnerValue, placedSessionOwnerOf } from "./session-ownership.js";
23
21
  import { selectForkRows } from "@sema-agent/core";
24
22
  import { FORK_LOG_ROW_SHAPE, ForkTargetUnsettledError, classifyForkTarget, forkByFreshId, forkPointOf } from "./session-fork.js";
25
23
  import { pgDriver } from "./sql-driver.js";
@@ -191,12 +189,10 @@ export class PgSessionStore {
191
189
  placements = SESSION_PLACEMENTS;
192
190
  durability = SESSION_DURABILITY;
193
191
  pending = new Map();
194
- log;
195
192
  executionPlacement;
196
193
  db;
197
194
  constructor(pool, opts = {}) {
198
195
  this.pool = pool;
199
- this.log = opts.log ?? createLogger();
200
196
  this.retention = opts.retention ?? UNMANAGED_RETENTION;
201
197
  this.db = pgDriver(pool);
202
198
  this.executionPlacement = createSqlExecutionPlacementSeat(this.db);
@@ -244,20 +240,7 @@ export class PgSessionStore {
244
240
  await this.insertMeta(sessionId, owner, undefined);
245
241
  }
246
242
  async createOnAcquire(sessionId, placement) {
247
- if (placement === undefined)
248
- return this.insertMeta(sessionId, null, undefined);
249
- await createPlacedSessionRow(sessionId, placement, {
250
- readFaces: (ids) => this.sourceFaces(ids),
251
- insert: (owner) => this.insertMeta(sessionId, owner, placement),
252
- readOwner: async () => (await this.headOf(sessionId))?.owner,
253
- }, this.log);
254
- }
255
- async claimOwnerless(sessionId, incarnation, owner) {
256
- return ((await this.pool.query(CLAIM_OWNERLESS_PLACED_SQL.pg, [...claimOwnerlessParams(sessionId, incarnation, owner)])).rowCount ?? 0) > 0;
257
- }
258
- async sourceFaces(ids) {
259
- const res = await this.pool.query(`SELECT session_id, owner, incarnation FROM session_meta WHERE session_id IN (${ids.map((_, i) => `$${i + 1}`).join(", ")})`, [...ids]);
260
- return new Map(res.rows.map((r) => [String(r.session_id), sessionFaceFromRow(String(r.session_id), r.owner ?? null, r.incarnation)]));
243
+ await this.insertMeta(sessionId, placement === undefined ? null : placedSessionOwnerOf(sessionId, placement), placement);
261
244
  }
262
245
  async insertMeta(sessionId, owner, placement) {
263
246
  const now = new Date();
@@ -71,11 +71,8 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
71
71
  cwd: string;
72
72
  /** core `ExecutionEnv.homeDir`([ref]):本机执行用户的 home —— 主张在场 ⇒ 主张(构造时与子进程拿到的 `HOME` 对证过),否则构造时读到的那个 `HOME`,见 {@link LaneHomeSeat}。 */
73
73
  get homeDir(): string | undefined;
74
- /** 家目录的座(主张在场 ⇒ 构造时对证 · 否则构造时实测)。读数 = {@link childHome}:本类自己组合给子进程的那份 env,不 spawn。 */
74
+ /** 家目录的座(主张在场 ⇒ 构造时对证,证不了 ⇒ 构造抛、环境不铸 · 否则构造时实测)。读数 = {@link childHome}:本类自己组合给子进程的那份 env,不 spawn。 */
75
75
  private readonly homeSeat;
76
- /** 家目录:主张与子进程拿到的 `HOME` 对不上 ⇒ 这只环境的拒句;`shellReady` / `fsReady` / `connect` 三道门原样回它,一条命令、一次文件动作都不做。
77
- * 读数是本类自己组合的 env(纯函数,同一份输入同一个答案)⇒ 构造时判一次、记一辈子。 */
78
- private readonly homeRefusal;
79
76
  /**
80
77
  * core 7.49.0 [ref]:shell 子进程**真拿到的** `CDPATH` 条目(`[]` = 没有)—— 删除守卫按它解裸 `cd <dir>`。本车道的命令就是本机子进程,
81
78
  * env 是本类自己组合的(继承姿态 + `cfg.env`),所以说得出:读同一只组合({@link childEnv} 的**读面**,不披露、不钉 shell),切法与 core
@@ -84,6 +81,13 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
84
81
  * {@link hostShell}(win32 上未落定同步抛 —— 修前每条 Bash 的策略链都因此被拒)。
85
82
  */
86
83
  get cdPath(): readonly string[];
84
+ /**
85
+ * core 7.62.0 [ref]([ref]):shell 子进程**收到的**环境变量名(从不带值)—— 引擎判「git 会不会读一只文本没绑的 `GIT_*`」的唯一来源(声明且没有 `GIT_*`
86
+ * ⇒ `git show HEAD:<path>` 的对象路径只在仓库顶判;缺席 ⇒ 每层祖先照判)。与 {@link cdPath} 同一只组合的读面(继承姿态 + `cfg.env`,不 spawn、不记剥除、不碰 shell);
87
+ * 逐命令的 `options.env` 由引擎自己交,它知道(core 残余:逐命令 env 加一个名)。win32 spawn 层钉的 `SHELL` 不在这一读里(不是 `GIT_*`,不改这一判)。
88
+ * 只有本车道声明:远端车道的 shell 跑在对方自己构建的环境里,适配器说不出(缺席)。
89
+ */
90
+ get receivedEnvNames(): readonly string[];
87
91
  /**
88
92
  * core 7.56.0 [ref]:shell 子进程继承的环境里**收不到**的条目(名 + 规则,从不带值)—— 与 {@link cdPath} 同一只组合的读面
89
93
  * (`server-secret-env.ts` 的 `withheldFromHostSubprocess`:继承组合扣下的发现,减去 `cfg.env` 这条显式授予重供的键)。逐命令的 `options.env`
@@ -13,7 +13,7 @@ import { IS_WIN32, resolveHostShell, hostShell, pinHostShell, spawnGroupOptions,
13
13
  import { BackgroundShellManager, seedMemStream, feedMemStream, drainMemStream } from "./background-shell-support.js";
14
14
  import { FileError, NodeExecutionEnv, inheritEnvPolicyProblem, ExecutionError, RemoteExecutionError, RollingTailBuffer, walkSymlinkChain, markTruncated, SchedulerError, BackgroundShellError, } from "@sema-agent/core";
15
15
  import { capabilitiesFor } from "../execution-lane-caps.js";
16
- import { LaneHomeSeat } from "../lane-home-probe.js";
16
+ import { LaneHomeSeat, laneHomeRefusal } from "../lane-home-probe.js";
17
17
  import { composeHostSubprocessEnv, hostSubprocessEnv, withheldFromHostSubprocess } from "../server-secret-env.js";
18
18
  import { nameTooLongCode } from "./remote-env-file-error.js";
19
19
  import { trackEphemeralHostWorkspace, untrackEphemeralHostWorkspace } from "./host-workspace-registry.js";
@@ -64,11 +64,13 @@ export class RemoteHostExecutionEnv {
64
64
  return this.homeSeat.home;
65
65
  }
66
66
  homeSeat;
67
- homeRefusal;
68
67
  get cdPath() {
69
68
  const value = this.childEnv(undefined, undefined).CDPATH;
70
69
  return value === undefined || value === "" ? [] : value.split(":");
71
70
  }
71
+ get receivedEnvNames() {
72
+ return Object.entries(this.childEnv(undefined, undefined)).flatMap(([name, value]) => (value === undefined ? [] : [name]));
73
+ }
72
74
  get withheldEnv() {
73
75
  return withheldFromHostSubprocess({ policy: this.cfg.inheritEnv, allow: [] }, this.cfg.env);
74
76
  }
@@ -148,7 +150,9 @@ export class RemoteHostExecutionEnv {
148
150
  this.cwd = this.workspaceDir;
149
151
  this.handle = { sandboxId: this.workspaceDir, provider: PROVIDER, mountPath: this.workspaceDir };
150
152
  this.homeSeat = new LaneHomeSeat(PROVIDER, config, IS_WIN32 ? "win32" : "posix");
151
- this.homeRefusal = this.homeSeat.settle(this.childHome())?.refusal;
153
+ const homeVerdict = this.homeSeat.settle(this.childHome());
154
+ if (homeVerdict !== undefined)
155
+ throw laneHomeRefusal(homeVerdict);
152
156
  }
153
157
  workspaceHandle() {
154
158
  return this.handle;
@@ -158,8 +162,6 @@ export class RemoteHostExecutionEnv {
158
162
  return unsupported("connect(snapshotId)");
159
163
  if (this.destroyed)
160
164
  return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env already destroyed") };
161
- if (this.homeRefusal !== undefined)
162
- return { ok: false, error: new RemoteExecutionError("connect_failed", this.homeRefusal) };
163
165
  if (config?.abortSignal?.aborted)
164
166
  return { ok: false, error: new RemoteExecutionError("aborted", "connect aborted") };
165
167
  try {
@@ -1262,8 +1264,6 @@ export class RemoteHostExecutionEnv {
1262
1264
  async shellReady() {
1263
1265
  if (this.destroyed)
1264
1266
  return { ok: false, error: new ExecutionError("shell_unavailable", "execution env already destroyed") };
1265
- if (this.homeRefusal !== undefined)
1266
- return { ok: false, error: new ExecutionError("shell_unavailable", this.homeRefusal) };
1267
1267
  try {
1268
1268
  try {
1269
1269
  await resolveHostShell();
@@ -1295,8 +1295,6 @@ export class RemoteHostExecutionEnv {
1295
1295
  return (this.spoolRootDir !== undefined && inside(this.spoolRootDir)) || (this.persistent && inside(this.workspaceDir));
1296
1296
  }
1297
1297
  async fsReady(p, opts) {
1298
- if (this.homeRefusal !== undefined)
1299
- return { ok: false, error: new FileError("target_unavailable", this.homeRefusal, this.resolve(p)) };
1300
1298
  if (this.destroyed) {
1301
1299
  const lingering = opts?.lingeringBgWrite === true && this._bgManager?.hasShells() === true && this.isLingeringMirrorTarget(p);
1302
1300
  if (!lingering)
@@ -157,7 +157,7 @@ export async function runSymlinkChain(absPath, run) {
157
157
  const STAGED_FILE = "data";
158
158
  const STAGE_DIR_REFUSED_EXIT = 3;
159
159
  const STAGE_DIR_UNSEALED_EXIT = 4;
160
- const STAGE_DIR_UNSEALED_MESSAGE = "sema: the private staging directory could not be made private (chmod 700 failed or did not take effect, or the mode or owner read back differently — the filesystem ignores permission bits or the parent directory carries a default ACL); nothing was staged";
160
+ const STAGE_DIR_UNSEALED_MESSAGE = "sema: the private staging directory could not be made private (chmod 700 failed or did not take effect, the mode or owner read back differently, or an inherited ACL entry could not be removed or read on this platform — the filesystem ignores permission bits or the parent directory carries a default or inheritable ACL); nothing was staged";
161
161
  export const STAGE_DIR_LEFT_MARK = "sema: staging directory left behind:";
162
162
  export function buildStagingPaths(abs, nonce = randomBytes(6).toString("hex")) {
163
163
  const cut = abs.lastIndexOf("/");
@@ -174,7 +174,17 @@ function privateMkdir(dir) {
174
174
  }
175
175
  function hardenPrivate(dir) {
176
176
  const statOf = (gnu, bsd) => `$(stat -c ${gnu} -- ${dir} 2>/dev/null || stat -f ${bsd} -- ${dir} 2>/dev/null)`;
177
- return `( chmod -- 700 ${dir} 2>/dev/null && m=${statOf("%a", "%Lp")} && u=${statOf("%u", "%u")} && case "$m" in 700|[1-7]700) [ "$u" = "$(id -u)" ];; *) false;; esac )`;
177
+ return `( chmod -- 700 ${dir} 2>/dev/null && m=${statOf("%a", "%Lp")} && u=${statOf("%u", "%u")} && case "$m" in 700|[1-7]700) [ "$u" = "$(id -u)" ];; *) false;; esac && ${aclCleared(dir)} )`;
178
+ }
179
+ function aclCleared(dir) {
180
+ const marker = `ls_out=$(cd ${dir} && ls -ld . 2>&1) && [ -z "$(printf '%s\\n' "$ls_out" | sed -n 2p)" ] && mode_word=\${ls_out%% *} && ` +
181
+ `case "$mode_word" in d?????????*+*) echo +;; d?????????*) ;; *) false;; esac`;
182
+ return (`plat=$(uname -s 2>/dev/null) && case "$plat" in ` +
183
+ `Linux) acl_read() { :; }; acl_strip() { false; };; ` +
184
+ `Darwin) acl_read() { ls_out=$(cd ${dir} && ls -led . 2>&1) && case "$ls_out" in d?????????*) ;; *) false;; esac && printf '%s\\n' "$ls_out" | sed 1d; }; acl_strip() { chmod -N ${dir}; };; ` +
185
+ `FreeBSD) acl_read() { ${marker}; }; acl_strip() { setfacl -b ${dir}; };; ` +
186
+ `SunOS) acl_read() { ${marker}; }; acl_strip() { chmod A- ${dir}; };; ` +
187
+ `*) false;; esac && acl=$(acl_read 2>/dev/null) && { [ -z "$acl" ] || { acl_strip 2>/dev/null && acl=$(acl_read 2>/dev/null) && [ -z "$acl" ]; }; }`);
178
188
  }
179
189
  function emptyPrivate(dir) {
180
190
  return `[ ! -e ${dir}/${STAGED_FILE} ] && [ ! -L ${dir}/${STAGED_FILE} ]`;
@@ -52,6 +52,10 @@ export declare class SqlRunStore {
52
52
  * 续跑与 wake 复用 checkpoint 的会话 —— 所以零行只在「受理之后被删」这一种竞争里出现。
53
53
  */
54
54
  createRun(taskId: string, sessionId: string, owner: string | null, instanceId: string, meta?: NewRunMeta): Promise<CreateRunOutcome>;
55
+ /** run 行预览的**写一次补写**(腿上装着 prompt 屏时建行不写预览,引擎交付可写的那一份之后补;律的属主 = `run-acceptance.ts`):
56
+ * 列仍是 NULL(没建过预览)才写 —— 建行写过的、已经补过的一律不动,所以补写永远盖不掉任何一份既有预览。不碰 `updated_at`(预览不是状态变化,
57
+ * 收割与列表的时序都不该因它移动)。行不在 ⇒ 0 行,无事。PG 臂过 `pgSanitizeText`(与建行写同一条:预览是模型可控标量)。 */
58
+ fillObjectivePreview(taskId: string, preview: string): Promise<void>;
55
59
  /** Request cancel of a RUNNING run (durable, cross-replica). Sets the flag the owning instance's heartbeat
56
60
  * tick polls; a same-replica caller additionally aborts the in-flight controller for instant effect. Only
57
61
  * flags a `running` row (terminal/suspended → 0 affected = no-op). Returns whether a running row was flagged.
@@ -141,6 +141,9 @@ export class SqlRunStore {
141
141
  conn.release();
142
142
  }
143
143
  }
144
+ async fillObjectivePreview(taskId, preview) {
145
+ await this.db.query(this.q("UPDATE task_run SET objective_preview = ? WHERE task_id = ? AND objective_preview IS NULL", "UPDATE task_run SET objective_preview = $1 WHERE task_id = $2 AND objective_preview IS NULL"), [this.db.dialect === "mysql" ? preview : pgSanitizeText(preview), taskId]);
146
+ }
144
147
  async requestCancel(taskId, owner) {
145
148
  const { affected } = await this.db.query(this.q("UPDATE task_run SET cancel_requested = 1 WHERE task_id = ? AND owner <=> ? AND status = 'running'", "UPDATE task_run SET cancel_requested = 1 WHERE task_id = $1 AND owner IS NOT DISTINCT FROM $2 AND status = 'running'"), [taskId, owner]);
146
149
  return affected > 0;
@@ -73,7 +73,11 @@ export interface ParsedTable {
73
73
  export declare function parseCreateTable(stmt: string): ParsedTable | undefined;
74
74
  /** 启动时真会建出来的**全部**表形(录制 → 解析)。同名表二次出现以**先到**为准(`IF NOT EXISTS` 的真语义)。 */
75
75
  export declare function expectedTableShapes(dialect: SqlDialect): Promise<Map<string, ParsedTable>>;
76
- /** 拒启码(机读)。附录 A / 拒启码表的登记名。 */
76
+ /**
77
+ * 拒启码(机读)。附录 A / 拒启码表的登记名。含义 = 「本构建读不出的持久形,开始服务之前判」,一个码三只臂:本文件的列册对账与退役 schema 两道(SQL),
78
+ * 以及 local 数据根的严格边档对账(`local-session-store.ts` 的 `LocalDataRootSchemaMismatchError`,`LocalBackend.ensureSchema()` 调;拒句同形:
79
+ * 码开头的表头 · 逐坐标一段 · 「NEWER than this build」方向句 · 闭集出路)。
80
+ */
77
81
  export declare const SCHEMA_MISMATCH_ERROR_CODE = "config.schema_mismatch";
78
82
  /** 一张表的对账结论。 */
79
83
  export interface TableMismatch {
@@ -28,7 +28,7 @@ export interface ForkLogRow {
28
28
  readonly ts: unknown;
29
29
  }
30
30
  /**
31
- * 两只 SQL 孪生的行怎么交给引擎的 `selectForkRows`(按答案挑行 + 新会话的叶;分叉点不在日志里 ⇒ `invalid_fork_target`、路径中途悬空父 / 父链成环 ⇒ `invalid_session`,
31
+ * 两只 SQL 孪生的行怎么交给引擎的 `selectForkRows`(按答案挑行 + 新会话的叶;分叉点不在日志里 ⇒ `invalid_fork_target`、路径中途悬空父 / 父链成环 ⇒ `corrupt_session`〔core 7.61.x 及以前 `invalid_session`〕,
32
32
  * 引擎的原词原句)。本仓不再自写父链走法 —— 那曾是与引擎 `getPathToRoot` 同义的第二份。
33
33
  */
34
34
  export declare const FORK_LOG_ROW_SHAPE: ForkRowShape<ForkLogRow>;
@@ -22,6 +22,15 @@ export declare function forkOwnerOf(requested: string | null | undefined, source
22
22
  * 「首写」由调用方判(已记下属主的会话上的调用照旧静默不改,不经这里)。判官 = {@link isSessionOwnerValue}(core 7.56.0 [ref] `principalNameOf`)。
23
23
  */
24
24
  export declare function assertFirstOwnerWritable(sessionId: string, owner: unknown): asserts owner is string | null;
25
+ /**
26
+ * placed 行属主格的唯一读法(core 7.62.0 [ref] 盒义务 7;三只持久会话店的 placing write 同调):记下的属主 = 引擎交来的 `placement.owner` **原样** ——
27
+ * 委派树的属主由引擎判(没人派生的 run 读自己那一行、按化身绑一次,原样带给每一层子代;`null` = 根匿名),店只记、不判、不推断,也不进元组
28
+ * (`SessionPlacementRecord` 没有属主)。值不是主体名也不是 `null`(空串、空白、缺席 —— 绕过类型的调用)⇒ 响亮拒(`SessionError("storage")`,一个字节不写):
29
+ * 一只谁也不指的属主会让行要么谁都读得到、要么谁都读不到。判官 = {@link isSessionOwnerValue},与 `register` 首写同一只。
30
+ */
31
+ export declare function placedSessionOwnerOf(sessionId: string, placement: {
32
+ readonly owner?: unknown;
33
+ }): string | null;
25
34
  /**
26
35
  * 导入者的属主 —— 换装的写腿(公开 `importEntries` / `replaceEntries` 与暂存提交)把调用方给的属主重戳进会话行之前的唯一读法:
27
36
  * 主体名或 `null`(匿名)⇒ 原样交回;别的一切(空串、空白串、带控制 / 分隔 / 非连接符格式字符的串、非串)⇒ `TypeError` 响亮拒 ——
@@ -22,6 +22,12 @@ export function assertFirstOwnerWritable(sessionId, owner) {
22
22
  throw new SessionError("storage", `register(${sessionId}): a first ownership write must carry a principal name or null (anonymous), got ${ownerValueFault(owner)} — ` +
23
23
  "a value that names nobody would leave the session claimable by anyone or ownable by no one; refusing, nothing written");
24
24
  }
25
+ export function placedSessionOwnerOf(sessionId, placement) {
26
+ const owner = placement.owner;
27
+ if (isSessionOwnerValue(owner))
28
+ return owner;
29
+ throw new SessionError("storage", `acquire(${sessionId}): a placed session's owner (placement.owner, the delegation tree's owner the engine judged) must be a principal name or null (an anonymous tree), got ${ownerValueFault(owner)} — refusing, nothing written`);
30
+ }
25
31
  export function importerOwnerOf(owner) {
26
32
  if (isSessionOwnerValue(owner))
27
33
  return owner;
@@ -1,8 +1,6 @@
1
1
  import { type SessionStore } from "@sema-agent/core";
2
2
  import type { ServiceConfig } from "../config-types.js";
3
3
  import type { StoreBackend } from "./store-backend.js";
4
- import { type DelegationRootWitness, type OrphanOwnerLog } from "../delegated-session-owner.js";
5
- import type { OwnerAwareSessionStore } from "../security.js";
6
4
  /**
7
5
  * Session-store plugin seam.
8
6
  *
@@ -43,23 +41,22 @@ export declare function createSessionStore(config: Pick<ServiceConfig, "sessionB
43
41
  */
44
42
  export declare function ensureSessionDurable(hostStore: SessionStore, sessionId: string): Promise<void>;
45
43
  /**
46
- * core `SubagentToolOptions.background.ensureChildSessionDurable` 的实现体 —— 停驻挂点的**见证**:这只后台子代的转录此刻是 host 店里一只有真内容、
47
- * 属主按委派继承律定得下来的会话,才放行停驻;任何一步不成立 ⇒ 抛 = 否决(core 收 veto 即 expire checkpoint + settle failed,pre-153 行为,不放大)。
44
+ * core `SubagentToolOptions.background.ensureChildSessionDurable(sessionId, given)` 的实现体 —— 停驻挂点的**见证**:这只后台子代的转录此刻是 host 店里一只有真内容、
45
+ * 属主恰是引擎交来的委派树属主(`given.owner`)的会话,才放行停驻;任何一步不成立 ⇒ 抛 = 否决(core 收 veto 即 expire checkpoint + settle failed,pre-153 行为,不放大)。
48
46
  *
49
47
  * 规则(`fork-routing-session-store.ts` 头注):一只后台子代的 durable 行在,它的转录就是 host 店委派分区里的一只 placed 行。挂点只装在声明了
50
48
  * durable 委派分区的部署上(stage-07:core `resolveSubagentTranscriptTier(...) === "full"`),那里 core 给每只新起的后台子代铸 placement、
51
- * 转录生来就在 host 店 —— 所以这里**只验、不搬**(此前的「晋升」把私店的转录整树拷进 host 店,随这条规则退役)。
52
- * 0. 属主(`delegated-session-owner.ts`):按委派继承律判一次(唯一判据 `judgeDelegatedOwner`)—— 来源集 = 见证的根这一只(调用方交出的委派树根 + 调用方判断那一刻
53
- * 读到的根行住客:请求腿 = 本请求经鉴权解析的会话与受理时那一读,`/decide` 赎回腿 = 行上的 `rootSessionId` 与写口放行的那一位(认领之前解析器那一读);core 的挂点签名只给子代 id,
54
- * 场景层把见证绑进闭包交下来)。此刻根行在而住客不是见证的那一位(删了、同 id 被重新登记)⇒ **拒**(`assertRootStillWitnessed`);
55
- * 缺席(行上没有根)或根行此刻不在 ⇒ 孤行哨兵。
49
+ * 转录生来就在 host 店 —— 所以这里**只验、不搬、不判属主**:属主是引擎判的(core 7.62.0 [ref] L1:没人派生的 run 读自己那一行、按化身绑一次,原样带给每一层子代;
50
+ * 复活的子代 = 它自己那一行的属主),placed 行建行那一笔原样记下了它(盒义务 7,三只持久会话店 + `session-ownership.ts` 的 `placedSessionOwnerOf`)。
51
+ * 挂点不认领、不推断、不收委派树根见证([ref] P-5;本仓的委派继承律在这一写点退役 —— 存量无主 placed 行:SQL 两店由一次性迁移 M23 在窗内回填,本机文件店没有回填臂;本机文件店的建行自 7.145.0 起格先、存档后,崩在两笔之间不再新生这一形,见 `local-session-store.ts` 头注)。
52
+ * 0. 宿主店读不出属主(没有 `headOf`,唯一判法 `headReadOf`)⇒ 响亮拒,一次读都不发。
56
53
  * 1. 在不在、有没有真内容:{@link ensureSessionDurable}(`requireExisting` 打开 + 叶子非空 + forget 收尾)。任何失败(不在、空壳、读故障)⇒ 原样上抛 = 否决
57
- * (读故障也否决:说不准转录在不在,就不让一次审批押在它上面)。这里**不判它是不是 placed 行**:复活的是旧行时 id 取行上的会话(升级前停驻、当年晋升进来的普通行),
58
- * 它照样是这只子代的转录;判据只问「host 店里有这份真内容」。
59
- * 2. 属主定夺(`settleDelegatedOwner`):记着别的委派树 ⇒ 抛;记着这一棵 ⇒ 按它记录的根、父再判一次(与 M23 同读数),无主 ⇒ 认领(CAS 子行,写判断时的读数)、没落下 ⇒ 抛;
60
- * 此刻属主不是律给的那一只 ⇒ 那是别人的同 id 会话,不是这只子代的转录 ⇒ 抛 = 停驻否决。认领写下的是哨兵 ⇒ 写落下之后一行 warn(`site: "park"`)。
61
- * 宿主店必须读得出属主(`delegatedOwnerHostOf`:`headOf`;记录委派树的店另交 `placementOf` + `claimOwnerless`,两只同在或同缺;生产的 SQL / local 店与缓存包装层都满足),
62
- * 否则响亮拒。
54
+ * (读故障也否决:说不准转录在不在,就不让一次审批押在它上面)。不判它是不是 placed 行:复活的是旧行时 id 取行上的会话(升级前停驻、当年晋升进来的普通行),
55
+ * 它照样是这只子代的转录。
56
+ * 2. 此刻行上的属主 ≠ `given.owner` ⇒ 那是别人的同 id 会话,或这一行在引擎读之后被改写 ⇒ 抛 = 停驻否决(拒句不带任何一方的属主值 —— 它会进停驻否决的错误面)。
57
+ * 契约只承诺属主值相等(化身不比:复活腿的 `given` 只带属主)。
63
58
  */
64
- export declare function attestChildSessionDurable(hostStore: OwnerAwareSessionStore, sessionId: string, root: DelegationRootWitness | undefined, log: OrphanOwnerLog): Promise<void>;
59
+ export declare function attestChildSessionDurable(hostStore: SessionStore, sessionId: string, given: {
60
+ readonly owner: string | null;
61
+ }): Promise<void>;
65
62
  //# sourceMappingURL=session-store.d.ts.map
@@ -1,6 +1,6 @@
1
1
  import { TtlSessionStore } from "@sema-agent/core";
2
2
  import { CachingSessionStore } from "./caching-session-store.js";
3
- import { assertRootStillWitnessed, delegatedOwnerHostOf, readDelegatedOwner, reportWrittenOwner, settleDelegatedOwner, sourceFacesVia, } from "../delegated-session-owner.js";
3
+ import { headReadOf } from "../session-incarnation.js";
4
4
  export function createSessionStore(config, backend, metrics) {
5
5
  if (config.sessionBackend === "tidb") {
6
6
  if (!backend)
@@ -23,16 +23,15 @@ export async function ensureSessionDurable(hostStore, sessionId) {
23
23
  await hostStore.forget?.(acquired.sessionId);
24
24
  }
25
25
  }
26
- export async function attestChildSessionDurable(hostStore, sessionId, root, log) {
27
- const host = delegatedOwnerHostOf(hostStore);
28
- if (host === undefined) {
29
- throw new Error(`session "${sessionId}" cannot be parked: the host session store cannot read a session's owner (headOf), or records delegation trees without being able to claim under them (placementOf / claimOwnerless), so the delegated transcript's owner cannot be derived or checked`);
26
+ export async function attestChildSessionDurable(hostStore, sessionId, given) {
27
+ const headOf = headReadOf(hostStore);
28
+ if (headOf === undefined) {
29
+ throw new Error(`session "${sessionId}" cannot be parked: the host session store cannot read a session's owner (headOf), so the delegated transcript's owner cannot be checked`);
30
30
  }
31
- const judged = await readDelegatedOwner(sessionId, root === undefined ? {} : { rootSessionId: root.sessionId }, sourceFacesVia(host.headOf));
32
- assertRootStillWitnessed(sessionId, root, judged);
33
31
  await ensureSessionDurable(hostStore, sessionId);
34
- const settled = await settleDelegatedOwner(host, sessionId, judged.inherited.owner, judged.tree);
35
- if (settled.claimed)
36
- reportWrittenOwner(log, "park", sessionId, settled.judged ?? judged);
32
+ if ((await headOf(sessionId))?.owner !== given.owner) {
33
+ throw new Error(`session "${sessionId}" cannot be parked as this delegated session: the host session store holds a session with this id whose owner is not this delegation tree's owner, ` +
34
+ "so it does not attest this delegated transcript");
35
+ }
37
36
  }
38
37
  //# sourceMappingURL=session-store.js.map
@@ -68,12 +68,13 @@ export interface JsonColumnMember {
68
68
  * 同一只投影的**裸表达式**(不带别名)—— 给 `WHERE` / `UPDATE … WHERE` 这种**没有别名位**的地方用。
69
69
  *
70
70
  * 🔴 存在的唯一理由是「别名位在那里不合法」,**不是**给读腿留第二条路:读腿一律用 {@link jsonColumnText}
71
- * (别名是它的产物的一部分,理由见那只函数)。今天整仓**两处**消费方,各自的「别名位不合法」都是真的:
71
+ * (别名是它的产物的一部分,理由见那只函数)。今天整仓**三处**消费方,各自的「别名位不合法」都是真的:
72
72
  * · `one-time-migrations.ts` 的几条改写(M8 / M17 / M18 / M21 的候选扫描与按原文 CAS 回写)——
73
73
  * 它们要在 `WHERE` 里判这一列的**文本**,而 `WHERE` 没有别名位;
74
74
  * · `workflow-journal-store-sql.ts` 的 `loadPage`([ref] 车③a)—— 那一列的投影裹在一条
75
75
  * `CASE WHEN LENGTH(result) > ? THEN NULL ELSE <expr> END AS result_json` 里:**别名属于整条 CASE**
76
76
  * (`result_json`,因为超字节门的那一支要给 `NULL`),CASE 里面的表达式位上写别名是语法错。
77
+ * · 本模块的 {@link jsonColumnTextSha256} —— 投影裹在摘要函数里,**别名属于整条摘要表达式**(产物是摘要不是体)。
77
78
  * 而「怎么把一列 JSON 取成文本」这件事整仓只能有一种写法:两种承载(真 JSON 列 / LONGTEXT 别名)在这条
78
79
  * 表达式下合成同一只串,迁移的判等、分页腿的取文本因此与读腿的判形**看同一份字节**。
79
80
  *
@@ -82,6 +83,20 @@ export interface JsonColumnMember {
82
83
  * 误用的落点是真库上当场的 `SqlJsonColumnError("not-text")` 响亮抛(行上没有那个键 ⇒ `undefined`),不是静默。
83
84
  */
84
85
  export declare function jsonColumnTextExpr(dialect: SqlDialect, column: string, memberKey?: string): string;
86
+ /**
87
+ * 一列 JSON 的**投影文本的 sha256**(十六进制小写,引擎内算 —— 整只文档不出库),带别名。输入恰是 {@link jsonColumnTextExpr} 那一只投影:
88
+ * 同一行、同一次读里,本表达式的摘要就是 {@link jsonColumnText} 交给读腿的那串字节的摘要,于是「字节没变 ⇒ 摘要没变」只有一种读法。
89
+ * MySQL 协议 `SHA2(<文本>, 256)`(TiDB / MySQL / MariaDB 都认);PG `encode(sha256(convert_to(<文本>, 'UTF8')), 'hex')`(PG 11+)。
90
+ *
91
+ * 读者今天一处:`checkpoint-store-sql.ts` 的停驻行投影记忆(运维队列 / 调度视图对一份停驻行字节的判决按 (摘要, 引擎构建) 记,
92
+ * 记忆命中就不拉整只文档)。产物是摘要不是体,所以它**不是**体的第二条投影路;回读走 {@link readJsonColumnTextSha256}。
93
+ */
94
+ export declare function jsonColumnTextSha256(dialect: SqlDialect, column: string, as: string): string;
95
+ /**
96
+ * {@link jsonColumnTextSha256} 那一格的回读:恒是 64 位十六进制小写串。别的一切(键不在 = 读腿漏了投影;串形不对 = 驱动换了行为)都是**码坏**,
97
+ * 响亮抛 —— 不当作「这一行没有摘要」退成每次整行读(那会让记忆静默失效,代价无声地涨回去)。
98
+ */
99
+ export declare function readJsonColumnTextSha256(cell: unknown, where: string): string;
85
100
  /**
86
101
  * 一次读回失败的**种别**(店层码,**闭集**;运行期词表在这里,类型从它推导 —— 一张词表只有一个写者):
87
102
  * · `not-text` —— 拿到的不是串 = **读腿漏了投影**,那是**码的问题不是数据的问题** ⇒ 响亮的程序错,
@@ -114,7 +129,12 @@ export declare class SqlJsonColumnError extends Error {
114
129
  readonly problem: SqlJsonColumnProblem;
115
130
  /** 出事的坐标(`<table>.<column> for <identity>`)—— 进错误文案,让运维知道去看哪一行。 */
116
131
  readonly where: string;
117
- constructor(problem: SqlJsonColumnProblem, where: string, detail: string);
132
+ /**
133
+ * `malformed` 时形读者交出的「它不认得的键」({@link JsonColumnShapeBad.unknownKeys};别的种别 / 读者说不出 ⇒ 空)—— 体是更新的写者写下的那一形的判据
134
+ * (例:local 数据根的严格边档据它把拒句点名成「比本构建新」)。判词只有形读者一个写者,本类原样携带。
135
+ */
136
+ readonly unknownKeys: readonly string[];
137
+ constructor(problem: SqlJsonColumnProblem, where: string, detail: string, unknownKeys?: readonly string[]);
118
138
  }
119
139
  /** 形读者认了:带上收窄后的值。 */
120
140
  export interface JsonColumnShapeOk<T> {
@@ -125,6 +145,8 @@ export interface JsonColumnShapeOk<T> {
125
145
  export interface JsonColumnShapeBad {
126
146
  readonly value?: undefined;
127
147
  readonly problem: string;
148
+ /** 读者因为**不认得**而拒的键(去重、排序;没有 / 读者说不出 ⇒ 缺)—— 更新的写者给这一形加了位。{@link decodeJsonColumn} 原样交给 {@link SqlJsonColumnError.unknownKeys}。 */
149
+ readonly unknownKeys?: readonly string[];
128
150
  }
129
151
  /** {@link JsonColumnReader} 的答复。 */
130
152
  export type JsonColumnShape<T> = JsonColumnShapeOk<T> | JsonColumnShapeBad;
@@ -20,15 +20,30 @@ export function jsonColumnTextExpr(dialect, column, memberKey) {
20
20
  }
21
21
  return dialect === "pg" ? `(${column}::jsonb -> '${memberKey}')::text` : `CAST(JSON_EXTRACT(${column}, '$.${memberKey}') AS CHAR)`;
22
22
  }
23
+ export function jsonColumnTextSha256(dialect, column, as) {
24
+ if (!MEMBER_ALIAS_RE.test(as)) {
25
+ throw new Error(`jsonColumnTextSha256: alias ${JSON.stringify(as)} must be a lower-case bare identifier — PostgreSQL folds unquoted aliases to lower case, so any other spelling would give the two dialects different row keys`);
26
+ }
27
+ const text = jsonColumnTextExpr(dialect, column);
28
+ return dialect === "pg" ? `encode(sha256(convert_to(${text}, 'UTF8')), 'hex') AS ${as}` : `SHA2(${text}, 256) AS ${as}`;
29
+ }
30
+ const SHA256_HEX_RE = /^[0-9a-f]{64}$/;
31
+ export function readJsonColumnTextSha256(cell, where) {
32
+ if (typeof cell === "string" && SHA256_HEX_RE.test(cell))
33
+ return cell;
34
+ throw new Error(`${where}: expected a lower-case sha256 hex digest from the read leg, got ${cell === undefined ? "no such key (the SELECT dropped the projection)" : typeof cell} — refusing rather than guess`);
35
+ }
23
36
  export const SQL_JSON_COLUMN_PROBLEMS = ["not-text", "not-json", "malformed"];
24
37
  export class SqlJsonColumnError extends Error {
25
38
  problem;
26
39
  where;
27
- constructor(problem, where, detail) {
40
+ unknownKeys;
41
+ constructor(problem, where, detail, unknownKeys = []) {
28
42
  super(`${where}: ${detail}`);
29
43
  this.name = "SqlJsonColumnError";
30
44
  this.problem = problem;
31
45
  this.where = where;
46
+ this.unknownKeys = unknownKeys;
32
47
  }
33
48
  }
34
49
  function describeBody(body) {
@@ -53,7 +68,7 @@ export function decodeJsonColumn(text, read, where) {
53
68
  }
54
69
  const shape = read(body);
55
70
  if (shape.problem !== undefined) {
56
- throw new SqlJsonColumnError("malformed", where, `carries ${describeBody(body)} that this store cannot read (${shape.problem}) — refusing rather than serve it as an empty/partial value`);
71
+ throw new SqlJsonColumnError("malformed", where, `carries ${describeBody(body)} that this store cannot read (${shape.problem}) — refusing rather than serve it as an empty/partial value`, shape.unknownKeys);
57
72
  }
58
73
  return shape.value;
59
74
  }
@@ -85,8 +100,28 @@ export function assertJsonColumnWritable(text, read, where) {
85
100
  export function createZodColumnReader(schema) {
86
101
  return (body) => {
87
102
  const parsed = schema.safeParse(body);
88
- return parsed.success ? { value: parsed.data } : { problem: parsed.error.message };
103
+ if (parsed.success)
104
+ return { value: parsed.data };
105
+ const unknownKeys = unrecognizedKeysOf(parsed.error.issues);
106
+ return { problem: parsed.error.message, ...(unknownKeys.length > 0 ? { unknownKeys } : {}) };
107
+ };
108
+ }
109
+ function unrecognizedKeysOf(issues) {
110
+ const keys = new Set();
111
+ const walk = (list) => {
112
+ for (const issue of list) {
113
+ if (issue.code === "unrecognized_keys")
114
+ for (const key of issue.keys)
115
+ keys.add(key);
116
+ else if (issue.code === "invalid_union")
117
+ for (const branch of issue.errors)
118
+ walk(branch);
119
+ else if (issue.code === "invalid_key" || issue.code === "invalid_element")
120
+ walk(issue.issues);
121
+ }
89
122
  };
123
+ walk(issues);
124
+ return [...keys].sort();
90
125
  }
91
126
  export function createObjectColumnReader() {
92
127
  return (body) => (isJsonObject(body) ? { value: body } : { problem: "expected a JSON object" });
@@ -165,7 +165,7 @@ class TiDBBackend {
165
165
  breaker(onWriteFail) { return new TiDBBreakerState(this.pool, onWriteFail); }
166
166
  costQuota(limitMicroUsd, windowMs, onDegraded) { return new TiDBCostQuota(this.pool, limitMicroUsd, windowMs, undefined, onDegraded, this.config.dbQueryTimeoutMs); }
167
167
  rateLimiter(limit, onDegraded) { return new TiDBRateLimiter(this.pool, limit, undefined, undefined, onDegraded, this.config.dbQueryTimeoutMs); }
168
- session() { return new TiDBSessionStore(this.pool, { log: createLogger(this.config.logLevel), retention: sqlStoreRetention(this.config.retentionSweep) }); }
168
+ session() { return new TiDBSessionStore(this.pool, { retention: sqlStoreRetention(this.config.retentionSweep) }); }
169
169
  adoptionLog() { return new TiDBAdoptionLogStore(this.pool); }
170
170
  mysqlPool() { return this.pool; }
171
171
  pgPool() { return undefined; }
@@ -219,7 +219,7 @@ class PgBackend {
219
219
  breaker(onWriteFail) { return new PgBreakerState(this.pool, onWriteFail); }
220
220
  costQuota(limitMicroUsd, windowMs, onDegraded) { return new PgCostQuota(this.pool, limitMicroUsd, windowMs, undefined, onDegraded, this.config.dbQueryTimeoutMs); }
221
221
  rateLimiter(limit, onDegraded) { return new PgRateLimiter(this.pool, limit, undefined, undefined, onDegraded, this.config.dbQueryTimeoutMs); }
222
- session() { return new PgSessionStore(this.pool, { log: createLogger(this.config.logLevel), retention: sqlStoreRetention(this.config.retentionSweep) }); }
222
+ session() { return new PgSessionStore(this.pool, { retention: sqlStoreRetention(this.config.retentionSweep) }); }
223
223
  adoptionLog() { return new PgAdoptionLogStore(this.pool); }
224
224
  mysqlPool() { return undefined; }
225
225
  pgPool() { return this.pool; }
@@ -294,6 +294,7 @@ class LocalBackend {
294
294
  : (plane) => fileSessionCaptureRecordStore(plane?.controlDir ?? ownControlDir);
295
295
  }
296
296
  async ensureSchema() {
297
+ await this.sessionStore.assertStrictSidecarsReadable();
297
298
  await this.sessionStore.settleArchiveSnapshots();
298
299
  }
299
300
  async runOneTimeMigrations() {
@@ -17,7 +17,6 @@ import type { SessionWorkspace } from "../session-workspace.js";
17
17
  import type { RetentionDeclaration } from "@sema-agent/core";
18
18
  import type { SessionSummary } from "./store-contracts.js";
19
19
  import { type ClientSessionRowWrite, type ReplaySessionRowWrite, type ReplaySessionRowWriteOutcome, type SessionRowPatch, type SessionRowWriteOutcome } from "../session-row.js";
20
- import { type OrphanOwnerLog } from "../delegated-session-owner.js";
21
20
  import { type PlacementAcquireOpts } from "./session-placement.js";
22
21
  import { type StagingHandle } from "../session-sync-kernel.js";
23
22
  import { type ForkTarget, type ForkToOutcome } from "./session-fork.js";
@@ -40,8 +39,6 @@ export declare class TiDBSessionStore implements SessionStore {
40
39
  readonly durability: "durable";
41
40
  /** In-flight acquisitions keyed by id, so concurrent acquire(sameId) in one process share one. */
42
41
  private readonly pending;
43
- /** 委派子代孤行那一行 warn 的去处(`delegated-session-owner.ts`;装配给部署的 logger,缺席 ⇒ 缺省 logger,不静默)。 */
44
- private readonly log;
45
42
  /**
46
43
  * 执行席 —— 根会话行上这一位住客的那一格的唯一读口与 place / markMirror 两只写口(规则在 `src/execution-placement.ts`;
47
44
  * 实现与语句的唯一属主 = `execution-placement-sql.ts`,两孪生同一只实现,本店只交自己的池)。
@@ -50,7 +47,6 @@ export declare class TiDBSessionStore implements SessionStore {
50
47
  /** 本店的池包成的 SqlDriver:执行席与会话行格写原语的事务都经它借连接(连接初始化钉住事务读语义,`sql-driver.ts`)。 */
51
48
  private readonly db;
52
49
  constructor(pool: Pool, opts?: {
53
- readonly log?: OrphanOwnerLog;
54
50
  readonly retention?: RetentionDeclaration;
55
51
  });
56
52
  acquire(sessionId?: string, opts?: PlacementAcquireOpts): Promise<AcquiredSession>;
@@ -58,18 +54,10 @@ export declare class TiDBSessionStore implements SessionStore {
58
54
  /** Idempotently create a session_meta row with an owner (core `SessionStore.register`). Existing owner is never overwritten. */
59
55
  register(sessionId: string, owner: string | null): Promise<void>;
60
56
  /**
61
- * acquire 的建行(唯一一处):普通会话 ⇒ 无主(按 id 首建的既有读法);placed 会话 ⇒ 属主按委派继承律在**这一笔**里写入
62
- * ([ref]:根会话的属主,根行不在取父,都不在 ⇒ 孤行哨兵 + warn;属主 = 判断时见证到的,读来源 → 判 → INSERT —— 读法与顺序只住在
63
- * `delegated-session-owner.ts` 的 `createPlacedSessionRow`,本店只交既有读写口,PG 孪生同调)。
57
+ * acquire 的建行(唯一一处):普通会话 ⇒ 无主(按 id 首建的既有读法);placed 会话 ⇒ 属主 = `placement.owner` 原样,与元组同一条 INSERT 落库
58
+ * (core 7.62.0 [ref] 盒义务 7:委派树的属主是引擎判的,店只记、不判、不推断;唯一读法 `placedSessionOwnerOf`,值不是主体名也不是 null ⇒ 响亮拒、不留行。PG 孪生同调)。
64
59
  */
65
60
  private createOnAcquire;
66
- /**
67
- * [ref] 委派继承律的**认领口**:这只会话的那一位住客(判断时读到的化身)仍是无主的 placed 行 ⇒ 写 `owner`,返回是否写成
68
- * (句 = `CLAIM_OWNERLESS_PLACED_SQL`,只 CAS 子行自己,与停驻挂点 / M23 同一只;规则在 `delegated-session-owner.ts`)。普通会话行不在射程(placed 判别位)。
69
- */
70
- claimOwnerless(sessionId: string, incarnation: string, owner: string): Promise<boolean>;
71
- /** 若干来源行的会话面(属主 + 化身)一次读回(委派继承律的来源读;本店没有的 id 不在表里;活住客的投影 `sessionFaceFromRow`,空化身 ⇒ 响亮拒)。 */
72
- private sourceFaces;
73
61
  /** The one session_meta INSERT. Existing owner is never overwritten.
74
62
  * [ref]:`placement` 在场时与行同一条 INSERT 落六列;ON DUPLICATE 臂是 no-op ⇒ 已存在的行的元组
75
63
  * **永不被重写**(first-write 不可变,义务 5 的写侧半场)。