@sema-agent/server 7.133.0 → 7.134.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/MIGRATION.md +87 -0
  2. package/README.md +2 -2
  3. package/README.zh-CN.md +2 -2
  4. package/USAGE.md +129 -11
  5. package/deploy/sema-up/README.md +16 -0
  6. package/deploy/sema-up/chart/Chart.yaml +1 -1
  7. package/deploy/sema-up/chart/templates/NOTES.txt +15 -0
  8. package/deploy/sema-up/chart/templates/_helpers.tpl +129 -0
  9. package/deploy/sema-up/chart/templates/sealed-key.yaml +24 -0
  10. package/deploy/sema-up/chart/templates/server-v4pro.yaml +14 -0
  11. package/deploy/sema-up/chart/templates/server.yaml +14 -0
  12. package/deploy/sema-up/chart/values-full-single.yaml +5 -1
  13. package/deploy/sema-up/chart/values.yaml +34 -0
  14. package/dist/approval-ask-audit-store.d.ts +1 -1
  15. package/dist/approval-card.d.ts +4 -4
  16. package/dist/approval-card.js +1 -2
  17. package/dist/approval-content-kind.d.ts +5 -2
  18. package/dist/approval-content-kind.js +16 -3
  19. package/dist/approval-gate-currency.d.ts +72 -0
  20. package/dist/approval-gate-currency.js +94 -0
  21. package/dist/approval.d.ts +6 -0
  22. package/dist/approval.js +31 -14
  23. package/dist/boot/coordinators.d.ts +6 -1
  24. package/dist/boot/coordinators.js +6 -3
  25. package/dist/boot/leader.d.ts +2 -0
  26. package/dist/boot/leader.js +2 -1
  27. package/dist/boot/read-deny-anchor-audit.js +2 -2
  28. package/dist/boot/runner-deps.d.ts +18 -5
  29. package/dist/boot/runner-deps.js +9 -0
  30. package/dist/boot/stage-06-runners.d.ts +4 -3
  31. package/dist/boot/stage-06-runners.js +15 -2
  32. package/dist/boot/stage-07-capability-layer.d.ts +2 -1
  33. package/dist/boot/stage-08-reapers.d.ts +2 -1
  34. package/dist/boot/stage-09-leader.d.ts +2 -1
  35. package/dist/boot/stage-09-leader.js +2 -2
  36. package/dist/boot/stage-10-http-server.d.ts +2 -1
  37. package/dist/boot/stage-10-http-server.js +2 -1
  38. package/dist/boot/stores.d.ts +8 -0
  39. package/dist/boot/stores.js +8 -12
  40. package/dist/boot/web-admin.d.ts +6 -0
  41. package/dist/boot/web-admin.js +11 -2
  42. package/dist/budget.js +5 -1
  43. package/dist/caller-text-echo.d.ts +2 -0
  44. package/dist/caller-text-echo.js +2 -1
  45. package/dist/canonical-json.d.ts +2 -41
  46. package/dist/canonical-json.js +2 -13
  47. package/dist/capabilities/composite-members.d.ts +1 -1
  48. package/dist/capabilities/composite-members.js +1 -1
  49. package/dist/capabilities/mcp-probe.js +2 -2
  50. package/dist/capability-facts.d.ts +2 -0
  51. package/dist/capability-facts.js +4 -4
  52. package/dist/checkpoint-ctx-seal-m28.d.ts +42 -0
  53. package/dist/checkpoint-ctx-seal-m28.js +19 -0
  54. package/dist/field-conflict.d.ts +2 -1
  55. package/dist/field-conflict.js +1 -0
  56. package/dist/file-history-disclosure.d.ts +34 -5
  57. package/dist/file-history-disclosure.js +2 -2
  58. package/dist/fleet/fleet-bus.d.ts +4 -0
  59. package/dist/fleet/fleet-bus.js +1 -0
  60. package/dist/hooks/hook-runner.js +2 -2
  61. package/dist/http/admission.js +8 -0
  62. package/dist/http/approval-sweeps.js +3 -1
  63. package/dist/http/checkpoint-ctx.d.ts +157 -2
  64. package/dist/http/checkpoint-ctx.js +158 -2
  65. package/dist/http/dispatch.js +3 -3
  66. package/dist/http/durable-run-leg.js +3 -2
  67. package/dist/http/idempotency.d.ts +2 -2
  68. package/dist/http/idempotency.js +2 -2
  69. package/dist/http/principal-gate.d.ts +32 -6
  70. package/dist/http/principal-gate.js +27 -4
  71. package/dist/http/refusal-body.d.ts +12 -0
  72. package/dist/http/refusal-body.js +11 -0
  73. package/dist/http/request-guards.d.ts +0 -9
  74. package/dist/http/request-guards.js +2 -16
  75. package/dist/http/resume-ctx-read.d.ts +48 -1
  76. package/dist/http/resume-ctx-read.js +26 -3
  77. package/dist/http/resume-legs.d.ts +2 -1
  78. package/dist/http/resume-legs.js +37 -22
  79. package/dist/http/route-ctx.d.ts +0 -9
  80. package/dist/http/routes/agents-roster.js +2 -3
  81. package/dist/http/routes/approvals-assistant.js +43 -9
  82. package/dist/http/routes/capabilities.js +1 -1
  83. package/dist/http/routes/diagnostics.d.ts +3 -3
  84. package/dist/http/routes/diagnostics.js +1 -1
  85. package/dist/http/routes/fleet.js +4 -4
  86. package/dist/http/routes/memory-origin.d.ts +1 -1
  87. package/dist/http/routes/rules.js +4 -4
  88. package/dist/http/routes/runs.js +30 -9
  89. package/dist/http/routes/session-sync.d.ts +3 -2
  90. package/dist/http/routes/session-sync.js +38 -20
  91. package/dist/http/routes/sessions-list.d.ts +1 -1
  92. package/dist/http/routes/sessions-list.js +3 -4
  93. package/dist/http/routes/sessions.js +4 -4
  94. package/dist/http/routes/tasks.js +21 -16
  95. package/dist/http/routes/trace-usage.js +7 -9
  96. package/dist/http/run-declared-families.d.ts +16 -0
  97. package/dist/http/run-declared-families.js +12 -0
  98. package/dist/http/server.d.ts +6 -0
  99. package/dist/http/server.js +5 -2
  100. package/dist/http/submit-ctx.d.ts +4 -3
  101. package/dist/http/submit-ctx.js +19 -3
  102. package/dist/http/wire-types.d.ts +7 -3
  103. package/dist/index.d.ts +2 -0
  104. package/dist/index.js +1 -0
  105. package/dist/leader/wire.d.ts +4 -0
  106. package/dist/leader/wire.js +8 -7
  107. package/dist/main.js +1 -1
  108. package/dist/memory-origin-fence.js +2 -2
  109. package/dist/model-compat.d.ts +1 -1
  110. package/dist/observability/prompt-manifest.d.ts +19 -8
  111. package/dist/observability/prompt-manifest.js +9 -8
  112. package/dist/observability/tool-trace.js +8 -9
  113. package/dist/orchestration/workflow-park-decision.d.ts +1 -1
  114. package/dist/orchestration/workflow-park-decision.js +1 -0
  115. package/dist/parked-decide.d.ts +4 -5
  116. package/dist/parked-decide.js +3 -1
  117. package/dist/permission-rule-vocab.d.ts +1 -23
  118. package/dist/permission-rule-vocab.js +0 -13
  119. package/dist/plugins/approval-payload-fingerprint.d.ts +14 -6
  120. package/dist/plugins/approval-payload-fingerprint.js +1 -1
  121. package/dist/plugins/caching-session-store.d.ts +1 -1
  122. package/dist/plugins/caching-session-store.js +4 -3
  123. package/dist/plugins/checkpoint-store-sql.d.ts +22 -1
  124. package/dist/plugins/checkpoint-store-sql.js +45 -1
  125. package/dist/plugins/local-checkpoint-store.d.ts +13 -2
  126. package/dist/plugins/local-checkpoint-store.js +26 -0
  127. package/dist/plugins/local-session-store.d.ts +12 -9
  128. package/dist/plugins/local-session-store.js +23 -14
  129. package/dist/plugins/local-session-window.d.ts +12 -6
  130. package/dist/plugins/local-session-window.js +1 -17
  131. package/dist/plugins/one-time-migrations.d.ts +11 -0
  132. package/dist/plugins/one-time-migrations.js +12 -0
  133. package/dist/plugins/permission-rule-store-file.js +10 -6
  134. package/dist/plugins/permission-rule-store-sql.js +8 -8
  135. package/dist/plugins/pg-session-storage.d.ts +1 -1
  136. package/dist/plugins/pg-session-storage.js +39 -17
  137. package/dist/plugins/remote-env-device.d.ts +14 -7
  138. package/dist/plugins/remote-env-device.js +23 -15
  139. package/dist/plugins/session-ownership.d.ts +11 -0
  140. package/dist/plugins/session-ownership.js +11 -0
  141. package/dist/plugins/session-placement.d.ts +12 -14
  142. package/dist/plugins/session-placement.js +2 -15
  143. package/dist/plugins/tidb-session-store.d.ts +1 -1
  144. package/dist/plugins/tidb-session-store.js +39 -17
  145. package/dist/request-tool-axes.d.ts +12 -0
  146. package/dist/request-tool-axes.js +20 -0
  147. package/dist/run-local.js +1 -0
  148. package/dist/runs.d.ts +2 -1
  149. package/dist/runs.js +4 -3
  150. package/dist/sealed-key.d.ts +32 -0
  151. package/dist/sealed-key.js +55 -1
  152. package/dist/security.d.ts +9 -7
  153. package/dist/session-error-wire.d.ts +12 -5
  154. package/dist/session-error-wire.js +4 -1
  155. package/dist/session-sync-kernel.d.ts +37 -10
  156. package/dist/session-sync-kernel.js +10 -0
  157. package/dist/task-a2a.js +4 -3
  158. package/dist/task-mcp.d.ts +2 -2
  159. package/dist/task-mcp.js +20 -8
  160. package/dist/task-settings.d.ts +5 -1
  161. package/dist/task-settings.js +1 -1
  162. package/dist/tool-approval.d.ts +151 -52
  163. package/dist/tool-approval.js +113 -20
  164. package/dist/tool-rule-names.d.ts +6 -0
  165. package/dist/trace/core-keyset-guard.d.ts +4 -3
  166. package/dist/trace/exit-seat.d.ts +13 -1
  167. package/dist/trace/exit-seat.js +55 -1
  168. package/dist/trace/ledger-events.d.ts +3 -1
  169. package/dist/trace/project.d.ts +18 -1
  170. package/dist/trace/project.js +16 -4
  171. package/dist/trace/redact.d.ts +6 -3
  172. package/dist/trace/redact.js +191 -23
  173. package/dist/trace/self-minted-wire.d.ts +4 -0
  174. package/dist/trace/self-minted-wire.js +1 -0
  175. package/dist/trace/wire-projection-faces.d.ts +6 -0
  176. package/dist/trace/wire-projection-faces.js +6 -0
  177. package/dist/web-admin-client/skills-mcp.d.ts +1 -1
  178. package/dist/web-admin-client/skills-mcp.js +2 -3
  179. package/dist/web-admin-client/types.d.ts +1 -1
  180. package/package.json +2 -2
  181. package/scripts/drain-check-local.mjs +34 -2
  182. package/scripts/drain-check.mysql.sql +20 -2
  183. package/scripts/drain-check.pg.sql +20 -2
  184. package/dist/mcp-tool-faces.d.ts +0 -69
  185. package/dist/mcp-tool-faces.js +0 -104
package/dist/main.js CHANGED
@@ -17,7 +17,7 @@ async function main() {
17
17
  const s04 = await runStage04(s03);
18
18
  const s05 = await runStage05(s04);
19
19
  await settleHostLaneShell(s05.config, s05.logger);
20
- const s06 = runStage06(s05);
20
+ const s06 = await runStage06(s05);
21
21
  const s07 = await runStage07(s06);
22
22
  const s08 = runStage08(s07);
23
23
  const s09 = runStage09(s08);
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { committedOriginOf } from "@sema-agent/core";
3
- import { canonicalJson } from "./canonical-json.js";
3
+ import { canonicalJsonStringify } from "@sema-agent/core";
4
4
  export const ORIGIN_AWARE_QUERY = "originAware";
5
5
  export function readOriginAware(params) {
6
6
  return params.get(ORIGIN_AWARE_QUERY) === "1";
@@ -47,7 +47,7 @@ export function buildOriginFencedBundle(bundle, originAware) {
47
47
  integrity: {
48
48
  entryCount: kept.length,
49
49
  revs: Object.fromEntries(kept.map((e) => [e.id, e.rev])),
50
- sectionHashes: { ...bundle.integrity.sectionHashes, entries: sha256Line(canonicalJson(kept)) },
50
+ sectionHashes: { ...bundle.integrity.sectionHashes, entries: sha256Line(canonicalJsonStringify(kept)) },
51
51
  },
52
52
  withheldOriginTagged: withheld,
53
53
  };
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `Model.compat` —— 一只模型对 **openai-completions 线路**的兼容声明(core `OpenAICompletionsCompat`;
3
3
  * 声明面 settings-schema 1.10.0 `ModelEntry.compat`)。本模块是**两条腿共用的唯一判据点 + 唯一铸点**,
4
- * 与 {@link import("./mcp-tool-faces.js").readMcpToolFaces} 同一个「判据单点、多腿一致」的形。
4
+ * 与 `toolFaces` 的读法(引擎根导出的 `readToolFaces`,`task-mcp.ts` / `skills-mcp.ts` 两腿同问)同一个「判据单点、多腿一致」的形。
5
5
  *
6
6
  * ## 它治的病(1.10.0 CHANGELOG + core 型面顶注双向亲读)
7
7
  * 引擎的 openai brain 一直按这五键发请求,但声明写不进来:
@@ -53,12 +53,23 @@ export declare function configAssembledRecordOf(e: {
53
53
  overrideReasons?: unknown;
54
54
  ts?: unknown;
55
55
  }): Record<string, unknown> | undefined;
56
+ /**
57
+ * 一条 run 级的**伴随记录**:trace 帧在白名单读法之后要落成的那一行耐久事件(与清单行同生命周期:register 同门、drain 同拍、lease clear 同清)。
58
+ * 种类是一张闭表 —— 帧与账本行一一对应,加一种伴随记录 = 加一个词(`ledger-events.ts` 的账本行名闭集同批加),不另开一对 record / drain。
59
+ * · `config_assembled` —— [ref]③ `config.assembled` 帧(生效配置逐字段来源);
60
+ * · `cross_boundary_taint` —— core 7.54.0 [ref] O9b S1 `permission.cross_boundary_taint` 帧(设备车道发射闸:树的污染位与 governed;
61
+ * 只在执行于目标设备的树上发,宿主车道零帧;投影器 `trace/project.ts` `crossBoundaryTaintEventData`)。
62
+ */
63
+ export type RunCompanionRecordType = "config_assembled" | "cross_boundary_taint";
64
+ export interface RunCompanionRecord {
65
+ readonly type: RunCompanionRecordType;
66
+ readonly data: Record<string, unknown>;
67
+ }
56
68
  export declare class PromptManifestTracker {
57
69
  private readonly byTask;
58
- /** [ref]③:config.assembled 的同生命周期伴随记录(register 同门、drainConfig 同拍)。队列形
59
- * (codex F2):cascade 每 rung 独立解析配置,后到覆盖会把先前已计费 rung 的生效配置抹掉——与
60
- * prompt manifests 同姿势逐条保序。 */
61
- private readonly configByTask;
70
+ /** 伴随记录({@link RunCompanionRecord}):清单之外、与清单同生命周期的 run 级耐久行。**一只**按发射序的队列(codex F2 当年给 config 一族立的保序律 ——
71
+ * cascade 每 rung 各自成帧,后到覆盖会抹掉先前 rung 的那一份 —— 对每一种伴随记录同样成立,所以是一只队列,不是每种一只)。 */
72
+ private readonly companionsByTask;
62
73
  private readonly active;
63
74
  private leaseCounter;
64
75
  /** Mark a TOP-LEVEL run active (same fence as `ModelUsageTracker.register`, budget.ts). Returns this leg's
@@ -66,10 +77,10 @@ export declare class PromptManifestTracker {
66
77
  register(taskId: string): number;
67
78
  /** Queue this prepare's manifest — ONLY for a registered top-level run (else a no-op). Sync, never throws. */
68
79
  record(taskId: string, m: PromptManifestRecord): void;
69
- /** [ref]③:队一条 config.assembled 投影(同 register 门;逐条保序——cascade 每 rung 各留其形)。 */
70
- recordConfig(taskId: string, rec: Record<string, unknown>): void;
71
- /** Return-and-clear ALL pending config records in emission order; `undefined` when none. */
72
- drainConfig(taskId: string): Array<Record<string, unknown>> | undefined;
80
+ /** 队一条伴随记录(同 register 门;按发射序保序)。Sync, never throws. */
81
+ recordCompanion(taskId: string, type: RunCompanionRecordType, data: Record<string, unknown>): void;
82
+ /** Return-and-clear ALL pending companion records in emission order; `undefined` when none. */
83
+ drainCompanions(taskId: string): RunCompanionRecord[] | undefined;
73
84
  /** Return-and-clear ALL pending manifests in emission order; `undefined` when none. */
74
85
  drain(taskId: string): PromptManifestRecord[] | undefined;
75
86
  /** Drop pending + unregister (run end / abandoned leg) — but ONLY while `lease` still owns the slot;
@@ -140,7 +140,7 @@ export function configAssembledRecordOf(e) {
140
140
  }
141
141
  export class PromptManifestTracker {
142
142
  byTask = new Map();
143
- configByTask = new Map();
143
+ companionsByTask = new Map();
144
144
  active = new Map();
145
145
  leaseCounter = 0;
146
146
  register(taskId) {
@@ -159,20 +159,21 @@ export class PromptManifestTracker {
159
159
  else
160
160
  this.byTask.set(taskId, [m]);
161
161
  }
162
- recordConfig(taskId, rec) {
162
+ recordCompanion(taskId, type, data) {
163
163
  if (!taskId || !this.active.has(taskId))
164
164
  return;
165
- const q = this.configByTask.get(taskId);
165
+ const q = this.companionsByTask.get(taskId);
166
+ const rec = { type, data };
166
167
  if (q)
167
168
  q.push(rec);
168
169
  else
169
- this.configByTask.set(taskId, [rec]);
170
+ this.companionsByTask.set(taskId, [rec]);
170
171
  }
171
- drainConfig(taskId) {
172
- const q = this.configByTask.get(taskId);
172
+ drainCompanions(taskId) {
173
+ const q = this.companionsByTask.get(taskId);
173
174
  if (!q || q.length === 0)
174
175
  return undefined;
175
- this.configByTask.delete(taskId);
176
+ this.companionsByTask.delete(taskId);
176
177
  return q;
177
178
  }
178
179
  drain(taskId) {
@@ -187,7 +188,7 @@ export class PromptManifestTracker {
187
188
  return;
188
189
  this.byTask.delete(taskId);
189
190
  this.active.delete(taskId);
190
- this.configByTask.delete(taskId);
191
+ this.companionsByTask.delete(taskId);
191
192
  }
192
193
  }
193
194
  //# sourceMappingURL=prompt-manifest.js.map
@@ -1,6 +1,6 @@
1
1
  import { isClassifierDenyCause, isDeniedBy } from "@sema-agent/core";
2
2
  import { observeClassic } from "../hooks/classic-stations.js";
3
- import { redactSecrets } from "../trace/redact.js";
3
+ import { seatText } from "../trace/exit-seat.js";
4
4
  import { echoCallerText } from "../caller-text-echo.js";
5
5
  export const PERMISSION_DENIED_METER_REGISTRATION = "server.permission-denied-meter";
6
6
  export const TOOL_TRACE_REGISTRATION = "server.tool-trace";
@@ -51,7 +51,7 @@ export function createToolTracer(logger) {
51
51
  isError: true,
52
52
  isInterrupt: e.failure.isInterrupt,
53
53
  ...(e.failure.durationMs !== undefined ? { durationMs: e.failure.durationMs } : {}),
54
- out: echoCallerText(redactSecrets(e.failure.error), { maxChars: TRACE_OUT_ECHO_CHARS }),
54
+ out: seatText("operator-log-text", e.failure.error),
55
55
  });
56
56
  });
57
57
  observeClassic(on, "classic.PermissionDenied", (payload) => {
@@ -62,33 +62,32 @@ export function createToolTracer(logger) {
62
62
  deniedBy: deniedByLabel(payload.gate),
63
63
  ...(payload.gate.origin === undefined ? {} : { origin: payload.gate.origin }),
64
64
  ...(payload.gate.settlement === undefined ? {} : { settlement: payload.gate.settlement.kind }),
65
- reason: echoCallerText(redactSecrets(payload.reason), { maxChars: TRACE_OUT_ECHO_CHARS }),
65
+ reason: seatText("operator-log-text", payload.reason),
66
66
  arg: summarize(payload.input),
67
67
  });
68
68
  });
69
69
  },
70
70
  };
71
71
  }
72
- const TRACE_ARG_ECHO_CHARS = 120;
73
- const TRACE_OUT_ECHO_CHARS = 200;
72
+ const TRACE_KEY_ECHO_CHARS = 120;
74
73
  function summarize(input) {
75
74
  if (!input || typeof input !== "object")
76
- return { v: echoCallerText(redactSecrets(String(input)), { maxChars: TRACE_ARG_ECHO_CHARS }) };
75
+ return { v: seatText("operator-log-arg", String(input)) };
77
76
  const o = input;
78
77
  const s = {};
79
78
  for (const k of ["path", "file_path", "command", "cmd", "pattern", "glob", "old_string", "new_string"]) {
80
79
  if (typeof o[k] === "string")
81
- s[k] = echoCallerText(redactSecrets(o[k]), { maxChars: TRACE_ARG_ECHO_CHARS });
80
+ s[k] = seatText("operator-log-arg", o[k]);
82
81
  }
83
82
  if (typeof o.content === "string")
84
83
  s.contentLen = o.content.length;
85
- return Object.keys(s).length ? s : { keys: Object.keys(o).map((k) => echoCallerText(k, { maxChars: TRACE_ARG_ECHO_CHARS })) };
84
+ return Object.keys(s).length ? s : { keys: Object.keys(o).map((k) => echoCallerText(k, { maxChars: TRACE_KEY_ECHO_CHARS })) };
86
85
  }
87
86
  function textOf(content) {
88
87
  const text = content
89
88
  .map((c) => (c && typeof c === "object" && typeof c.text === "string" ? c.text : ""))
90
89
  .join("")
91
90
  .trim();
92
- return echoCallerText(redactSecrets(text), { maxChars: TRACE_OUT_ECHO_CHARS });
91
+ return seatText("operator-log-text", text);
93
92
  }
94
93
  //# sourceMappingURL=tool-trace.js.map
@@ -52,7 +52,7 @@ export interface ParkDecisionLaneSeats {
52
52
  */
53
53
  export declare function parkDecisionStoreRefusal(seats: ParkDecisionLaneSeats): string | undefined;
54
54
  /**
55
- * 决定的同一性判据:判词 + 绑定两位 + 出处词(`decidedBy`)+ 载荷指纹(`updatedInput` / `answer` / `reason`,与 live 腿幂等回放的门⑦
55
+ * 决定的同一性判据:判词 + 绑定两位 + 出处词(`decidedBy`)+ 载荷指纹(`updatedInput` / `answer` / `reason` / `feedback`,与 live 腿幂等回放的门⑦
56
56
  * 同一只 {@link approvalPayloadFingerprint})。通道自报的 `approver` 不进 —— 它是归属标签,不改引擎铸的结算词。
57
57
  * 载荷序列化不了 ⇒ 抛(两侧都源自 JSON,走到这里是结构异常;那只函数自己已经响亮 warn)。
58
58
  */
@@ -36,6 +36,7 @@ export function parkDecisionFingerprint(outcome) {
36
36
  ...(outcome.updatedInput !== undefined ? { updatedInput: outcome.updatedInput } : {}),
37
37
  ...(outcome.answer !== undefined ? { answer: outcome.answer } : {}),
38
38
  ...(outcome.reason !== undefined ? { reason: outcome.reason } : {}),
39
+ ...(outcome.feedback !== undefined ? { feedback: outcome.feedback } : {}),
39
40
  });
40
41
  if (payload === null)
41
42
  throw new Error("workflow park decision: the decision payload cannot be serialized — refusing to record a decision whose identity cannot be stated");
@@ -9,6 +9,7 @@
9
9
  * 行永 parked、生命周期分叉)。
10
10
  */
11
11
  import type { HostDecision } from "./host-decision.js";
12
+ import type { DecideBinding } from "./http/wire-types.js";
12
13
  import type { BackgroundAgentRecord, BackgroundAgentStore, CheckpointStore, CheckpointToken, QuestionAnswer, RunInternals, TaskRegistry, ToolExecuteContext, WorkflowRun, WorkflowRunStore } from "@sema-agent/core";
13
14
  import type { ParkedReviveSurfaceFactory } from "./parked-revive-surface.js";
14
15
  import type { RebuiltInheritedGate } from "./boot/parked-revive-gate.js";
@@ -204,11 +205,7 @@ export interface ParkedDecideRequest {
204
205
  reason?: string;
205
206
  /** D-1 decision-binding echo(操作员在 GET 看到的 boundCallId/boundInputHash 原样透传;server
206
207
  * 绝不重算,core resume 侧做 fail-closed 等值校验——与 legacy 腿同姿势)。 */
207
- binding?: {
208
- boundCallId?: string;
209
- boundInputHash?: string;
210
- updatedInput?: unknown;
211
- };
208
+ binding?: Omit<DecideBinding, "checkpointToken">;
212
209
  /** body 带了 `remember:"session"` 时路由传入的 grant 执行器([ref] 候裁② 的 server 侧修,经两轮
213
210
  * 行为探针实证):豁免必须锚在**行的 root/host 会话 id**——部署侧三个豁免探针(policy 层/fs-write
214
211
  * 门/workflow 门)的闭包统一捕获 host 会话键且签名不携身份(1.393 seam 亲读:`sourceTaskId` 只在
@@ -247,6 +244,8 @@ export interface ParkedDecideResult {
247
244
  * · `hostDecision` = 结算**事实**逐字上 outcome;`gateOutcome` 一格 server **永不**填(core 对宿主
248
245
  * 自铸的结算词是 pre-CAS 响亮拒)。
249
246
  * · `answer` 原样透传([ref]:header 逐字匹配等语义校验全在 core,server 不预判)。
247
+ * · `feedback`(core 7.54.0):绑定里有就原样上 outcome —— 决定口只在人的放行上收它(拒决 / 策略的决定上带它是门口 400),本铸点不再按判词剪
248
+ * (剪 = 静默丢一段人写的话;门口已拒的形到不了这里,引擎在 claim 前再判一次);什么算写了字(空白读作没写)是引擎的读法,本铸点不 trim。
250
249
  */
251
250
  export declare function buildParkedResumeOutcome(req: ParkedDecideRequest): WorkflowParkOutcome;
252
251
  /**
@@ -5,7 +5,7 @@ import { defaultSubagentTailBus } from "./fleet/subagent-tail-bus.js";
5
5
  import { backgroundScopesForCheckpointScope, decodeCheckpointScope, ownerOfCheckpointScope } from "./security.js";
6
6
  import { ASK_USER_QUESTION_TOOL_NAME } from "./approval-content-kind.js";
7
7
  import { sameSessionOccupant } from "./session-incarnation.js";
8
- import { refusalBody } from "./http/refusal-body.js";
8
+ import { refusalBody, resumeRefusalFacts } from "./http/refusal-body.js";
9
9
  export async function findParkOrigin(sources, cp) {
10
10
  if (sources.agentStore !== undefined) {
11
11
  let match;
@@ -125,6 +125,7 @@ export function buildParkedResumeOutcome(req) {
125
125
  boundInputHash: req.binding?.boundInputHash ?? persistedHash,
126
126
  decision: req.decision === "approve" ? "allow" : "deny",
127
127
  ...(req.decision === "approve" && req.binding?.updatedInput !== undefined ? { updatedInput: req.binding.updatedInput } : {}),
128
+ ...(req.binding?.feedback !== undefined ? { feedback: req.binding.feedback } : {}),
128
129
  ...(req.answer !== undefined ? { answer: req.answer } : {}),
129
130
  ...(req.reason !== undefined ? { reason: req.reason } : {}),
130
131
  hostDecision: req.hostDecision,
@@ -369,6 +370,7 @@ export async function decideParkedAgent(deps, req, match) {
369
370
  taskId: match.handle,
370
371
  rollback: disp,
371
372
  ...(cause !== undefined ? { cause } : {}),
373
+ ...resumeRefusalFacts(receiptDetails.detail),
372
374
  },
373
375
  }),
374
376
  };
@@ -1,4 +1,4 @@
1
- import type { RuleApprovalRecord, RuleBehavior } from "@sema-agent/core";
1
+ import type { RuleBehavior } from "@sema-agent/core";
2
2
  /**
3
3
  * `PersistedRuleTool` **不再是闭词表**(core 7.9.0 / [ref]):工具名自 core 的**目录**派生
4
4
  * (`pathTarget` 声明 ⇒ 路径文法;`Bash` ⇒ 命令文法),型是 `string`。于是本文件对它的处置从
@@ -9,15 +9,6 @@ import type { RuleApprovalRecord, RuleBehavior } from "@sema-agent/core";
9
9
  * (既非命令文法也非路径文法 ⇒ 这条行永远匹配不上任何调用),两只店与票口一律拒。
10
10
  */
11
11
  export declare function isPersistedRuleTool(name: string): boolean;
12
- /**
13
- * `PersistedRuleMatch` 七员(core 顶注逐字):`exact` / `prefix`(历史 colon-star,永久兼容读、复合前缀形的
14
- * 唯一拼写)/ `wildcard`([ref] 尾 space-star,单命令体上与 prefix **同一谓词**,建议面自此铸它)/
15
- * `subpath`(Read 目录形,永不容纳任何命令)/ `path`(core 7.9.0 [ref]:路径族 deny/ask/allow 的模式文法
16
- * —— `//abs`、`~/`、`/root-relative`、cwd 相对,段内 `*`、跨段 `**`)/ `name` / `name_prefix`(core 7.43.0 #1318:`Skill` 的名字文法 ——
17
- * `Skill(<名>)` 精确、`Skill(<前缀>:*)` 名字前缀、无词界;技能卡的建议行 `match: "name"`)/ `host` / `subdomain`(core 7.49.0 #1365:
18
- * `WebFetch(domain:<host>)` 精确主机、`WebFetch(domain:*.<域>)` 域下每个主机 —— 只有 allow 形)。
19
- */
20
- export declare const PERSISTED_RULE_MATCHES: readonly ["exact", "prefix", "wildcard", "subpath", "path", "name", "name_prefix", "host", "subdomain"];
21
12
  /**
22
13
  * S-511 —— `RuleBehavior` 每一员在**无同意仪式的直接写口**(`POST /v1/rules`)上的逐员表态。
23
14
  *
@@ -44,19 +35,6 @@ export type DirectWritableRuleBehavior = {
44
35
  }[RuleBehavior];
45
36
  /** 判别式谓词(收窄到 {@link DirectWritableRuleBehavior}):端点判过之后,`allow` 在**类型上**到不了写家。 */
46
37
  export declare function isDirectWritableRuleBehavior(behavior: RuleBehavior): behavior is DirectWritableRuleBehavior;
47
- /** [ref] §2.3 B3:batch offer 成员的判别位(`command` = 历史逐段 Bash 规则;`directoryRead` = cd 段铸的目录只读授权)。 */
48
- export declare const RULE_OFFER_BATCH_MEMBER_KINDS: readonly ["command", "directoryRead"];
49
- /** [ref] §3.5:一段「为什么仍未被覆盖」的闭三词集(`redirection` 永久逐次 / `no_rule_form` 无可覆盖形 / `cap_overflow` 去重+帽挤出)。 */
50
- export declare const UNCOVERED_SEGMENT_REASONS: readonly ["redirection", "no_rule_form", "cap_overflow"];
51
- /** [ref] §3.3-5:卡编辑面的 breadth 警告码(记录 `edited.warnings` 存的就是这两个字面)。 */
52
- export declare const EDITED_RULE_BREADTH_WARNING_CODES: readonly ["broad_prefix", "compound_prefix"];
53
- /**
54
- * [ref] §4.5 / [ref] §3.5 —— 审批记录的**形版本号**(core `RuleApprovalRecord.schema`,本批 3→4;[ref] 三态 behavior 进候选行)。
55
- * 类型绑 core 的字面型:core 再升号 ⇒ 这里编译红,两只 durable 店(SQL 双方言 + File)的 stale 判别与写侧钉
56
- * 同批随之;判别是**等式**(`!==`)不是下限 —— 旧号的行按 stale 信封交还(不迁移、不 widen、不丢行),
57
- * pending 卡短命,重触发即取新卡(B7/B8 既诺)。
58
- */
59
- export declare const RULE_APPROVAL_RECORD_SCHEMA: RuleApprovalRecord["schema"];
60
38
  /**
61
39
  * 一次撤销之后「这条规则还活着吗」的**三词闭集**(core 7.25.0 [ref]:`RemovalLiveness`)。
62
40
  * `"yes"` = 店说它还在(add-wins:撤销期间又落了一次新批准);`"no"` = 店说它不在了;
@@ -2,9 +2,6 @@ import { ruleToolGrammarOf } from "@sema-agent/core";
2
2
  export function isPersistedRuleTool(name) {
3
3
  return ruleToolGrammarOf(name) !== undefined;
4
4
  }
5
- export const PERSISTED_RULE_MATCHES = ["exact", "prefix", "wildcard", "subpath", "path", "name", "name_prefix", "host", "subdomain"];
6
- const MATCHES_PIN = true;
7
- void MATCHES_PIN;
8
5
  export const DIRECT_RULE_WRITE_ADMITS = {
9
6
  allow: false,
10
7
  deny: true,
@@ -13,14 +10,4 @@ export const DIRECT_RULE_WRITE_ADMITS = {
13
10
  export function isDirectWritableRuleBehavior(behavior) {
14
11
  return DIRECT_RULE_WRITE_ADMITS[behavior];
15
12
  }
16
- export const RULE_OFFER_BATCH_MEMBER_KINDS = ["command", "directoryRead"];
17
- const MEMBER_KINDS_PIN = true;
18
- void MEMBER_KINDS_PIN;
19
- export const UNCOVERED_SEGMENT_REASONS = ["redirection", "no_rule_form", "cap_overflow"];
20
- const REASONS_PIN = true;
21
- void REASONS_PIN;
22
- export const EDITED_RULE_BREADTH_WARNING_CODES = ["broad_prefix", "compound_prefix"];
23
- const WARNING_CODES_PIN = true;
24
- void WARNING_CODES_PIN;
25
- export const RULE_APPROVAL_RECORD_SCHEMA = 4;
26
13
  //# sourceMappingURL=permission-rule-vocab.js.map
@@ -1,5 +1,17 @@
1
+ /** 决议载荷 —— 指纹的输入形(两条车道 + workflow 停驻决定共用一份;成员顺序见 {@link approvalPayloadFingerprint})。 */
2
+ export interface ApprovalPayload {
3
+ updatedInput?: unknown;
4
+ answer?: unknown;
5
+ reason?: string;
6
+ /** core 7.54.0:人写在卡上的话(引擎逐字记在赢家上,空白也记)。 */
7
+ feedback?: string;
8
+ }
1
9
  /**
2
- * [ref] 件1(codex R2-[high] 一)—— 决议载荷的比对指纹:`[updatedInput, answer, reason]` 三件的规范串。
10
+ * [ref] 件1(codex R2-[high] 一)—— 决议载荷的比对指纹:`[updatedInput, answer, reason]` 三件的规范串,其后**按在场追加**后来的成员(今天一员:`feedback`)。
11
+ *
12
+ * 🔴 成员律(一条):首发的三件恒占位(缺席折 `null`);之后加进来的成员**只在在场时**追加在尾部 —— workflow 停驻决定的同一性指纹
13
+ * ({@link parkDecisionFingerprint} 包它)是**落了库**的,新成员若恒占位,存量决定的指纹全部换字节、一次合法的重放被判成「另一个决定」;
14
+ * 按在场追加 ⇒ 不带新成员的决定字节不变,带了的与不带的不可能撞(串的元素个数不同)。
3
15
  *
4
16
  * 为什么是指纹而不是把三件原样交出来:`updatedInput` 可能携人写的命令原文/密钥形字节,而本读口的
5
17
  * 契约是「窄读、非凭据面」—— 交指纹够比对,也不给任何调用点把它误投上 wire 的机会。
@@ -15,9 +27,5 @@
15
27
  * 铸点丢掉的字节行上就没有,请求侧多喂一件会把一次合法重试误判成「不同的决议」。
16
28
  * 序列化失败(循环引用/BigInt 等)⇒ `null` = 判别不出,调用方据此拒绝回放(fail-closed)。
17
29
  */
18
- export declare function approvalPayloadFingerprint(v: {
19
- updatedInput?: unknown;
20
- answer?: unknown;
21
- reason?: string;
22
- }): string | null;
30
+ export declare function approvalPayloadFingerprint(v: ApprovalPayload): string | null;
23
31
  //# sourceMappingURL=approval-payload-fingerprint.d.ts.map
@@ -12,7 +12,7 @@ function canonicalFingerprintJson(v) {
12
12
  }
13
13
  export function approvalPayloadFingerprint(v) {
14
14
  try {
15
- const fp = canonicalFingerprintJson([v.updatedInput ?? null, v.answer ?? null, v.reason ?? null]);
15
+ const fp = canonicalFingerprintJson([v.updatedInput ?? null, v.answer ?? null, v.reason ?? null, ...(v.feedback !== undefined ? [v.feedback] : [])]);
16
16
  return fp;
17
17
  }
18
18
  catch (e) {
@@ -1,4 +1,4 @@
1
- import type { AcquiredSession, PlacedSessionRow, RetentionDeclaration, SessionPlacementRecord, SessionStore, SessionTreeEntry } from "@sema-agent/core";
1
+ import { type AcquiredSession, type PlacedSessionRow, type RetentionDeclaration, type SessionPlacementRecord, type SessionStore, type SessionTreeEntry } from "@sema-agent/core";
2
2
  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";
@@ -1,4 +1,5 @@
1
- import { assertPlacementAdmission } from "./session-placement.js";
1
+ import { admitSessionPlacement } from "@sema-agent/core";
2
+ import { placementCapabilityOf } from "./session-placement.js";
2
3
  export class CachingSessionStore {
3
4
  inner;
4
5
  cache = new Map();
@@ -144,7 +145,7 @@ export class CachingSessionStore {
144
145
  }
145
146
  const hit = await this.freshHit(sessionId);
146
147
  if (hit && opts?.requireExisting !== true && this.placements !== undefined && this.placementOf !== undefined) {
147
- assertPlacementAdmission(sessionId, await this.placementOf(sessionId), opts);
148
+ admitSessionPlacement(sessionId, await this.placementOf(sessionId), opts ?? {}, placementCapabilityOf(this));
148
149
  }
149
150
  if (hit) {
150
151
  hit.expiresAt = Date.now() + this.ttlMs;
@@ -159,7 +160,7 @@ export class CachingSessionStore {
159
160
  if (inflight) {
160
161
  const acquired = await inflight;
161
162
  if (this.placements !== undefined && this.placementOf !== undefined) {
162
- assertPlacementAdmission(sessionId, await this.placementOf(sessionId), opts);
163
+ admitSessionPlacement(sessionId, await this.placementOf(sessionId), opts ?? {}, placementCapabilityOf(this));
163
164
  }
164
165
  return acquired;
165
166
  }
@@ -3,6 +3,15 @@ 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 } from "../approval-card.js";
5
5
  import { type SqlDialect, type SqlDriver } from "./sql-driver.js";
6
+ import type { SealedCheckpointCtx } from "../http/checkpoint-ctx.js";
7
+ /** M28 店腿的读数(两孪生同形;随 M28 删):扫到的可读行 · 回写成的行 · 回写成的行里被改写的值 · 读不出的行 · 封好之后一行装不下的行。 */
8
+ export interface CtxRowsRewriteM28 {
9
+ readonly scanned: number;
10
+ readonly rewritten: number;
11
+ readonly valuesRewritten: number;
12
+ readonly unreadable: number;
13
+ readonly tooLarge: number;
14
+ }
6
15
  export { approvalPayloadFingerprint } from "./approval-payload-fingerprint.js";
7
16
  /**
8
17
  * [ref] D-1 (§3 invariant #3 — crash-safe reaper backstop): an ABSOLUTE upper bound on a pending
@@ -818,8 +827,20 @@ export declare class SqlCheckpointStore implements CheckpointStore {
818
827
  * SUCCEED and replace a readable durable row with an unreadable one (caller sees 2xx, the next resume
819
828
  * 409s forever). So the serialized value is judged by {@link assertJsonColumnWritable} — the SAME shape
820
829
  * reader the read leg uses — BEFORE the statement runs: refuse loudly, leave the row on disk untouched. */
821
- putCtx(sessionId: string, ctx: object): Promise<void>;
830
+ putCtx(sessionId: string, ctx: SealedCheckpointCtx): Promise<void>;
822
831
  getCtx<T extends object = object>(sessionId: string): Promise<T | null>;
832
+ /**
833
+ * 🔴 **一次性迁移 M28 的店腿**(窗与删法见 `plugins/one-time-migrations.ts` 登记表 M28;编排 = `checkpoint-ctx-seal-m28.ts`,行改写 =
834
+ * `http/checkpoint-ctx.ts` 的 `sealStoredCheckpointCtxM28`;本店不知道「封」是什么,只负责扫与回写):按 `session_id` 键集分页扫 `checkpoint_ctx`
835
+ * (每页 {@link CTX_REWRITE_PAGE} 行,只取会话 id 与 ctx 文本),行体经读腿**同一只**判形({@link decodeCtxColumn})—— 读不出 ⇒ 计数、跳过、不动;
836
+ * `rewrite` 交回新体 ⇒ 经写腿同一只编码与形闸,**按原文 CAS** 回写(MySQL 协议两边 `CAST(… AS BINARY)`,与 M17 / M18 同一条理由:连接排序规则大小写
837
+ * 不敏感;PG `text` 等值)。`updated_at_ms` **不动**(收割按它判,迁移不许延长或缩短一行的寿命)。CAS 打空(扫描与回写之间这一行被续跑腿 / 别的副本写过)
838
+ * ⇒ 不计、不重试(新写者是本版就已封好;是旧版副本就由之后的起服再扫到)。一行装不下(封签比明文长)⇒ 计 `tooLarge`、那一行留原样,其余照扫。
839
+ */
840
+ rewriteCtxRowsM28(rewrite: (sessionId: string, ctx: object) => {
841
+ readonly ctx: SealedCheckpointCtx;
842
+ readonly values: number;
843
+ } | undefined): Promise<CtxRowsRewriteM28>;
823
844
  /**
824
845
  * GC ctx rows whose lifecycle is over (bound to the checkpoint, don't leak):
825
846
  * a session with NO pending checkpoint, NO active run, AND not touched since `cutoff`.
@@ -8,10 +8,11 @@ import { assertJsonColumnWritable, createObjectColumnReader, decodeJsonColumn, d
8
8
  import { recordFailOpen } from "../observability/fail-open.js";
9
9
  import { durableRowIdentity } from "../checkpoint-identity.js";
10
10
  import { mysqlDriver, pgDriver, dialectProtocolJsonEncoder } from "./sql-driver.js";
11
- import { isDupKeyError, isMissingColumnError, rowTooLargeOrSelf } from "./sql-errors.js";
11
+ import { isDupKeyError, isMissingColumnError, rowTooLargeOrSelf, StoreRowTooLargeError } from "./sql-errors.js";
12
12
  import { MANAGED_RETENTION } from "./retention-store-sql.js";
13
13
  import { approvalPayloadFingerprint } from "./approval-payload-fingerprint.js";
14
14
  const REAP_CLASSIFY_BATCH = 200;
15
+ const CTX_REWRITE_PAGE = 500;
15
16
  export { approvalPayloadFingerprint } from "./approval-payload-fingerprint.js";
16
17
  const MAX_TOOL_INPUT_CHARS = 8192;
17
18
  export const TERMINAL_BACKSTOP_MS = Math.max(60_000, (Number.isFinite(Number(process.env.APPROVAL_TERMINAL_BACKSTOP_MS)) ? Number(process.env.APPROVAL_TERMINAL_BACKSTOP_MS) : 0) || 30 * 86_400_000);
@@ -755,6 +756,49 @@ export class SqlCheckpointStore {
755
756
  const { rows } = await this.db.query(this.q(`SELECT ${jsonColumnText("tidb", "ctx")} FROM checkpoint_ctx WHERE session_id=?`, `SELECT ${jsonColumnText("pg", "ctx")} FROM checkpoint_ctx WHERE session_id=$1`), [sessionId]);
756
757
  return rows[0] ? refusing("a resume context nobody can read", () => decodeCtxColumn(rows[0].ctx, `for session ${JSON.stringify(sessionId)}`)) : null;
757
758
  }
759
+ async rewriteCtxRowsM28(rewrite) {
760
+ const out = { scanned: 0, rewritten: 0, valuesRewritten: 0, unreadable: 0, tooLarge: 0 };
761
+ let after = "";
762
+ for (;;) {
763
+ const { rows } = await this.db.query(this.q(`SELECT session_id, ${jsonColumnText("tidb", "ctx")} FROM checkpoint_ctx WHERE session_id > ? ORDER BY session_id LIMIT ${CTX_REWRITE_PAGE}`, `SELECT session_id, ${jsonColumnText("pg", "ctx")} FROM checkpoint_ctx WHERE session_id > $1 ORDER BY session_id LIMIT ${CTX_REWRITE_PAGE}`), [after]);
764
+ for (const row of rows) {
765
+ const sessionId = String(row.session_id);
766
+ after = sessionId;
767
+ let ctx;
768
+ try {
769
+ ctx = decodeCtxColumn(row.ctx, `for session ${JSON.stringify(sessionId)} (M28 scan)`);
770
+ }
771
+ catch {
772
+ out.unreadable += 1;
773
+ continue;
774
+ }
775
+ out.scanned += 1;
776
+ const next = rewrite(sessionId, ctx);
777
+ if (next === undefined)
778
+ continue;
779
+ const text = this.json(next.ctx, "checkpoint ctx");
780
+ assertJsonColumnWritable(text, createObjectColumnReader(), `checkpoint_ctx.ctx for session ${JSON.stringify(sessionId)} (M28 write)`);
781
+ let affected;
782
+ try {
783
+ ({ affected } = await this.db.query(this.q("UPDATE checkpoint_ctx SET ctx = ? WHERE session_id = ? AND CAST(CAST(ctx AS CHAR) AS BINARY) = CAST(? AS BINARY)", "UPDATE checkpoint_ctx SET ctx = $1::jsonb WHERE session_id = $2 AND ctx::text = $3"), [text, sessionId, String(row.ctx)]));
784
+ }
785
+ catch (e) {
786
+ const folded = rowTooLargeOrSelf(this.db.dialect, e, `checkpoint_ctx.ctx for session ${JSON.stringify(sessionId)} (M28 write)`);
787
+ if (!(folded instanceof StoreRowTooLargeError))
788
+ throw folded;
789
+ out.tooLarge += 1;
790
+ continue;
791
+ }
792
+ if (affected > 0) {
793
+ out.rewritten += 1;
794
+ out.valuesRewritten += next.values;
795
+ }
796
+ }
797
+ if (rows.length < CTX_REWRITE_PAGE)
798
+ break;
799
+ }
800
+ return out;
801
+ }
758
802
  async reapCtx(cutoff) {
759
803
  const res = await this.db.query(this.q("DELETE FROM checkpoint_ctx WHERE updated_at_ms < ? " +
760
804
  "AND session_id NOT IN (SELECT session_id FROM checkpoint WHERE status='pending') " +
@@ -1,5 +1,6 @@
1
1
  import { type Checkpoint, type CheckpointSummary, type CheckpointToken, type PendingSteerInput, type ReopenReason, type ResolveExpectation, type ExecutionOutcomeRecordWord, type GateOutcome, type ResumeOutcome, type StoreDurability, type StoreFidelity } from "@sema-agent/core";
2
- import { type CheckpointAskCandidate, type DecidedApprovalRecord, type PendingCheckpoint, type PendingRowBinding } from "./checkpoint-store-sql.js";
2
+ import { type CheckpointAskCandidate, type CtxRowsRewriteM28, type DecidedApprovalRecord, type PendingCheckpoint, type PendingRowBinding } from "./checkpoint-store-sql.js";
3
+ import type { SealedCheckpointCtx } from "../http/checkpoint-ctx.js";
3
4
  export interface LocalCheckpointStoreOptions {
4
5
  /** taskId join for the pending card (the local FileRunStore's `getActiveTaskId`). Absent ⇒ `taskId: null`. */
5
6
  getActiveTaskId?: (sessionId: string) => Promise<string | undefined>;
@@ -199,7 +200,7 @@ export declare class LocalCheckpointStore {
199
200
  * 一个 `toJSON(k){ return k === "ctx" ? null : {…} }` 的对象验的是 `{…}`、写的是 `{"ctx":null}`
200
201
  * (实测两串逐字不同)。有状态的序列化器同理。⇒ 整条记录**只序列化一次**,闸判那一串,写也写那一串。
201
202
  */
202
- putCtx(sessionId: string, ctx: object): Promise<void>;
203
+ putCtx(sessionId: string, ctx: SealedCheckpointCtx): Promise<void>;
203
204
  /**
204
205
  * 🔴 **坏体 ⇒ 拒,绝不读成缺席**(S-577,与 SQL 孪生同一条判据)。修前这里是
205
206
  * `catch { return null }` + `(rec.ctx as T) ?? null`,于是一个撕裂 / 手改 / 权限读不动的 ctx 文件与
@@ -211,6 +212,16 @@ export declare class LocalCheckpointStore {
211
212
  * 只收对象,读腿只认对象,两条腿一个类型。
212
213
  */
213
214
  getCtx<T extends object = object>(sessionId: string): Promise<T | null>;
215
+ /**
216
+ * 🔴 **一次性迁移 M28 的店腿**(local 孪生;窗与删法见 `plugins/one-time-migrations.ts` 登记表 M28,判词与 SQL 孪生 `rewriteCtxRowsM28` 逐条同):
217
+ * 逐档读(信封经读腿**同一只**判形 —— 读不出 ⇒ 计数、跳过、**不删**),`rewrite` 交回新体 ⇒ 信封原样(`sessionId` / `updatedAt` 不动:收割按 `updatedAt` 判)
218
+ * 只换 `ctx`,经写腿同一只形闸、同一只原子写落回原档。封签绑的是信封里的 `sessionId`(与 `getCtx` 认行的那一格同一个)。
219
+ * 数据根独占 + 起服在任何读写者之前跑 ⇒ 本车道没有并发写者,不需要 CAS;没有一行装不下这回事(`tooLarge` 恒 0)。
220
+ */
221
+ rewriteCtxRowsM28(rewrite: (sessionId: string, ctx: object) => {
222
+ readonly ctx: SealedCheckpointCtx;
223
+ readonly values: number;
224
+ } | undefined): Promise<CtxRowsRewriteM28>;
214
225
  /** GC ctx files whose lifecycle is over — same three guards as the SQL twins (stale ∧ no pending ∧ no active run). */
215
226
  reapCtx(cutoff: number): Promise<number>;
216
227
  /**
@@ -327,6 +327,32 @@ export class LocalCheckpointStore {
327
327
  const rec = refusingCtx(sessionId, () => decodeJsonColumn(readFileSync(p, "utf8"), createCtxRecordReader(), `checkpoint-ctx ${ctxFileOf(sessionId)} for session ${JSON.stringify(sessionId)}`));
328
328
  return rec.sessionId === sessionId ? rec.ctx : null;
329
329
  }
330
+ async rewriteCtxRowsM28(rewrite) {
331
+ const out = { scanned: 0, rewritten: 0, valuesRewritten: 0, unreadable: 0, tooLarge: 0 };
332
+ for (const f of readdirSync(this.ctxDir)) {
333
+ if (!f.endsWith(".json"))
334
+ continue;
335
+ const p = join(this.ctxDir, f);
336
+ let rec;
337
+ try {
338
+ rec = decodeJsonColumn(readFileSync(p, "utf8"), createCtxRecordReader(), `checkpoint-ctx ${f} (M28 scan)`);
339
+ }
340
+ catch {
341
+ out.unreadable += 1;
342
+ continue;
343
+ }
344
+ out.scanned += 1;
345
+ const next = rewrite(rec.sessionId, rec.ctx);
346
+ if (next === undefined)
347
+ continue;
348
+ const text = JSON.stringify({ sessionId: rec.sessionId, updatedAt: rec.updatedAt, ctx: next.ctx });
349
+ assertJsonColumnWritable(text, createCtxRecordReader(), `checkpoint ctx for session ${JSON.stringify(rec.sessionId)} (local M28 write)`);
350
+ atomicWriteFile(this.tmpDir, p, text);
351
+ out.rewritten += 1;
352
+ out.valuesRewritten += next.values;
353
+ }
354
+ return out;
355
+ }
330
356
  async reapCtx(cutoff) {
331
357
  let n = 0;
332
358
  for (const f of readdirSync(this.ctxDir)) {
@@ -4,7 +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 StagingHandle, type SyncRelation } from "../session-sync-kernel.js";
7
+ import { type StagedCommitOptions, type StagingHandle, type SyncRelation } from "../session-sync-kernel.js";
8
8
  /**
9
9
  * local 车道会话存档的**唯一 repo 出口**:本仓里每一只读 `<root>/sessions/*.jsonl` 的 `FileSessionRepo` 都从这里取(`LocalSessionStore` 的底座与会话视图口
10
10
  * 窗口 `local-session-window.ts#LocalSessionWindow.wake` 的读旁路),所以坏记录的披露座只有这一份接线 —— 修前视图口自建一只不带座的 repo,同一份坏档在这里读到有计数 / 探针,
@@ -299,9 +299,13 @@ export declare class LocalSessionStore implements SessionStore {
299
299
  /** Synthesize a session's objective preview = the most-recent USER text message (the objective the run last saw).
300
300
  * Reads the session's entries through the repo; any read error degrades to null (never fails the list). */
301
301
  private previewOf;
302
- /** E17 — fork a session's WHOLE history to a NEW id owned by `owner` (core SessionRepo.fork). Returns the new id,
303
- * or null if the source does not exist (the route maps that to 404). */
304
- fork(sourceId: string, owner: string | null): Promise<string | null>;
302
+ /** E17 — fork a session's WHOLE history to a NEW id (core SessionRepo.fork). Returns the new id, or null if the source does not exist.
303
+ * 属主按 core `SessionStore.fork` 契约(core 7.54.0 `sessionStoreContract` 的 [ownership] 格,唯一读法 {@link forkOwnerOf}):显式串 ⇒ 该串;
304
+ * 无参 / 显式 `null` ⇒ **源会话记下的属主**(一份有主历史的拷贝永不降成匿名 —— 匿名的拷贝谁都能认领);源会话没记属主 ⇒ 拷贝也不记(读作无主)。
305
+ * 修前把参数原样写进新格:`fork(owned, null)` 铸出一份匿名的有主历史。
306
+ * 🚧 已知分歧(不在本版收):拷的是整份日志,含在飞的 assistant 轮(契约:只拷**已提交**的叶)—— 剪在飞轮的判据只能是引擎的 `settledForkOptions`
307
+ * (core 7.54.0 未导出,#2098 排在 7.55.0),本仓不写第二份;钉 `test/session-store-contract-kit.test.ts` 的 `it.fails` 格。 */
308
+ fork(sourceId: string, owner?: string | null): Promise<string | null>;
305
309
  /** 2c session-sync — EXPORT a session's full durable entry log (core `SessionRepo.exportEntries`; the
306
310
  * in-memory/file repo holds the whole log so there is no floor to bypass). null if the source is unknown / the
307
311
  * underlying repo lacks the optional seam. The owner is NOT exported — import re-stamps it. */
@@ -352,14 +356,13 @@ export declare class LocalSessionStore implements SessionStore {
352
356
  /** Drop a finished/aborted local staging handle from the registry (called by the handle on commit/abort). */
353
357
  forgetStaging(stagingId: string): void;
354
358
  /**
355
- * {@link LocalStagingHandle.commit} 的整段 —— **一次持锁**(这只会话的写锁,{@link withSessionWriteLock}):提交时重新分类(目的端此刻的日志 vs staged)
359
+ * {@link LocalStagingHandle.commit} 的整段 —— **一次持锁**(这只会话的写锁,{@link withSessionWriteLock}):属主 CAS(此刻的属主格 = 这次导入的门判过的那一格,
360
+ * 否则抛 {@link StagedCommitOwnerChangedError},一个字节不写;在判目的端日志之前判,SQL 孪生同序)→ 提交时重新分类(目的端此刻的日志 vs staged)
356
361
  * → 拒未经同意的 fork / stale / 载荷分叉 → 换装(validate-first → 删 → 导入,化身按换装律沿用)或 identical 且内容相同时只重戳属主 → **在同一次持锁里**
357
362
  * 读出落定的会话面交回。锁放下之前谁也换不掉这一位住客,所以交回的面就是这次提交落下的那一位(SQL 孪生在提交事务里读同一行,同义);
358
- * 提交之后按住客条件写的重放(会话行两位)只认这只面,不再重读。
363
+ * 提交之后按住客条件写的重放(会话行两位)只认这只面,不再重读。改属主的每一条写腿(登记 / 删除 / 导入 / 换装)都在同一把锁里,所以锁内这一读到锁放下都成立。
359
364
  */
360
- commitStagedImport(sessionId: string, owner: string | null, staged: SessionTreeEntry[], opts?: {
361
- resolution?: "overwrite-dst";
362
- }): Promise<{
365
+ commitStagedImport(sessionId: string, owner: string | null, staged: SessionTreeEntry[], opts: StagedCommitOptions): Promise<{
363
366
  relation: SyncRelation["relation"];
364
367
  face: SessionFace;
365
368
  }>;