@sema-agent/server 7.149.0 → 7.150.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 (194) hide show
  1. package/MIGRATION.md +122 -9
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/USAGE.md +39 -15
  5. package/dist/approval-card.d.ts +1 -1
  6. package/dist/approval-gate-currency.d.ts +1 -1
  7. package/dist/audit.d.ts +1 -1
  8. package/dist/audit.js +2 -2
  9. package/dist/background-session-reap.d.ts +1 -1
  10. package/dist/boot/deferred-sandbox-path-env.d.ts +5 -3
  11. package/dist/boot/deferred-sandbox-path-env.js +1 -2
  12. package/dist/boot/parked-revive-gate.d.ts +5 -5
  13. package/dist/boot/parked-revive-gate.js +2 -3
  14. package/dist/boot/resolve-spec.d.ts +4 -5
  15. package/dist/boot/resolve-spec.js +61 -25
  16. package/dist/boot/retention-lane.d.ts +1 -1
  17. package/dist/boot/session-faces.js +21 -21
  18. package/dist/boot/stage-03-store-backend.js +2 -2
  19. package/dist/boot/stage-06-runners.js +1 -2
  20. package/dist/boot/stage-07-capability-layer.js +1 -1
  21. package/dist/boot/stage-10-http-server.d.ts +33 -1
  22. package/dist/boot/stage-10-http-server.js +9 -4
  23. package/dist/boot/web-admin.d.ts +1 -1
  24. package/dist/capability-facts.d.ts +65 -56
  25. package/dist/capability-facts.js +42 -35
  26. package/dist/config-catalog.js +1 -1
  27. package/dist/config-types.d.ts +12 -11
  28. package/dist/deployment-governance.d.ts +7 -28
  29. package/dist/deployment-governance.js +1 -14
  30. package/dist/device-ws-hub.d.ts +1 -0
  31. package/dist/device-ws-hub.js +64 -28
  32. package/dist/device-ws-protocol.d.ts +20 -6
  33. package/dist/device-ws-protocol.js +9 -2
  34. package/dist/engine-identity.d.ts +7 -1
  35. package/dist/engine-identity.js +5 -0
  36. package/dist/field-conflict.d.ts +2 -2
  37. package/dist/fleet/engine-signal.d.ts +14 -1
  38. package/dist/fleet/engine-signal.js +5 -1
  39. package/dist/fleet/fleet-reconciler.d.ts +1 -1
  40. package/dist/fleet/serving-lease.d.ts +18 -3
  41. package/dist/fleet/serving-lease.js +81 -35
  42. package/dist/fleet-client.d.ts +13 -11
  43. package/dist/fleet-client.js +53 -33
  44. package/dist/history-anchor.js +1 -1
  45. package/dist/hitl-delivery.d.ts +1 -3
  46. package/dist/hitl-delivery.js +1 -1
  47. package/dist/http/active-run-conflict.d.ts +3 -7
  48. package/dist/http/active-run-conflict.js +2 -2
  49. package/dist/http/admission.js +2 -2
  50. package/dist/http/approval-sweeps.js +2 -1
  51. package/dist/http/caller-trust.d.ts +2 -2
  52. package/dist/http/caller-trust.js +3 -3
  53. package/dist/http/checkpoint-ctx.d.ts +18 -63
  54. package/dist/http/checkpoint-ctx.js +1 -24
  55. package/dist/http/claim-heartbeat.d.ts +4 -4
  56. package/dist/http/claim-heartbeat.js +3 -3
  57. package/dist/http/dispatch.js +2 -2
  58. package/dist/http/durable-run-leg.js +4 -4
  59. package/dist/http/owner-access.d.ts +243 -19
  60. package/dist/http/owner-access.js +184 -5
  61. package/dist/http/readiness.d.ts +4 -2
  62. package/dist/http/resume-ctx-read.js +16 -1
  63. package/dist/http/resume-legs.js +18 -14
  64. package/dist/http/route-ctx.d.ts +23 -11
  65. package/dist/http/routes/a2a-serve.js +15 -14
  66. package/dist/http/routes/approvals-assistant.d.ts +3 -2
  67. package/dist/http/routes/approvals-assistant.js +8 -9
  68. package/dist/http/routes/capabilities.js +4 -3
  69. package/dist/http/routes/config-catalog.d.ts +1 -1
  70. package/dist/http/routes/mcp-reconnect.js +2 -2
  71. package/dist/http/routes/memory-origin.d.ts +1 -1
  72. package/dist/http/routes/notify-wake.js +2 -3
  73. package/dist/http/routes/projects.d.ts +1 -0
  74. package/dist/http/routes/projects.js +5 -3
  75. package/dist/http/routes/retention-ops.d.ts +1 -1
  76. package/dist/http/routes/run-permission-mode.d.ts +1 -1
  77. package/dist/http/routes/run-permission-mode.js +2 -2
  78. package/dist/http/routes/runs.d.ts +1 -1
  79. package/dist/http/routes/runs.js +23 -30
  80. package/dist/http/routes/session-memory-sync.js +4 -5
  81. package/dist/http/routes/session-sync.js +3 -7
  82. package/dist/http/routes/sessions.d.ts +1 -1
  83. package/dist/http/routes/sessions.js +18 -44
  84. package/dist/http/routes/tasks.js +9 -8
  85. package/dist/http/routes/trace-usage.js +14 -14
  86. package/dist/http/routes/workflows.d.ts +1 -0
  87. package/dist/http/routes/workflows.js +20 -38
  88. package/dist/http/server.d.ts +8 -4
  89. package/dist/http/server.js +5 -1
  90. package/dist/http/stored-body-replay.d.ts +3 -24
  91. package/dist/http/stored-body-replay.js +2 -12
  92. package/dist/http/submit-ctx.d.ts +1 -2
  93. package/dist/http/submit-ctx.js +3 -2
  94. package/dist/http/submit-replay.d.ts +15 -2
  95. package/dist/http/submit-replay.js +10 -4
  96. package/dist/http/wire-types.d.ts +6 -3
  97. package/dist/leader/endpoint.js +9 -8
  98. package/dist/leg-facts.d.ts +8 -2
  99. package/dist/leg-facts.js +1 -0
  100. package/dist/mcp-authorization-face.d.ts +18 -14
  101. package/dist/mcp-authorization-face.js +3 -2
  102. package/dist/memory-store-refusal.d.ts +1 -1
  103. package/dist/observability/corrupt-read-seat.d.ts +1 -1
  104. package/dist/observability/fail-open.d.ts +5 -5
  105. package/dist/observability/fail-open.js +5 -5
  106. package/dist/observability/logger.d.ts +7 -0
  107. package/dist/observability/logger.js +3 -1
  108. package/dist/observability/run-terminal.d.ts +1 -1
  109. package/dist/owner-faces.d.ts +27 -3
  110. package/dist/owner-faces.js +2 -0
  111. package/dist/peer-seat.d.ts +1 -1
  112. package/dist/peer-seat.js +52 -59
  113. package/dist/plugins/exec-path-law.d.ts +34 -0
  114. package/dist/plugins/exec-path-law.js +13 -0
  115. package/dist/plugins/file-run-store.js +1 -1
  116. package/dist/plugins/local-checkpoint-store.d.ts +4 -1
  117. package/dist/plugins/local-checkpoint-store.js +3 -0
  118. package/dist/plugins/memory-run-store.js +1 -1
  119. package/dist/plugins/one-time-migrations.d.ts +74 -49
  120. package/dist/plugins/one-time-migrations.js +134 -69
  121. package/dist/plugins/pg-pool.js +4 -0
  122. package/dist/plugins/posix-shell-fs.js +4 -2
  123. package/dist/plugins/remote-env-adb.js +3 -2
  124. package/dist/plugins/remote-env-device.js +8 -6
  125. package/dist/plugins/remote-env-e2b.d.ts +3 -1
  126. package/dist/plugins/remote-env-e2b.js +11 -8
  127. package/dist/plugins/remote-env-host.d.ts +1 -1
  128. package/dist/plugins/remote-env-host.js +20 -11
  129. package/dist/plugins/remote-env-k8s.js +3 -2
  130. package/dist/plugins/remote-env-local-docker.js +3 -2
  131. package/dist/plugins/remote-env-ssh.js +4 -3
  132. package/dist/plugins/remote-shell.d.ts +3 -1
  133. package/dist/plugins/remote-shell.js +3 -3
  134. package/dist/plugins/replica-registration.d.ts +35 -0
  135. package/dist/plugins/replica-registration.js +48 -0
  136. package/dist/plugins/retention-lane-store-sql.d.ts +1 -1
  137. package/dist/plugins/run-store-sql.d.ts +1 -1
  138. package/dist/plugins/run-store-sql.js +2 -2
  139. package/dist/plugins/scenario-prefix-switch-m42.d.ts +6 -12
  140. package/dist/plugins/scenario-prefix-switch-m42.js +7 -5
  141. package/dist/plugins/schema-consistency.d.ts +7 -5
  142. package/dist/plugins/schema-consistency.js +29 -16
  143. package/dist/plugins/schema-drift-sentence.js +1 -1
  144. package/dist/plugins/serving-requirement.d.ts +49 -0
  145. package/dist/plugins/serving-requirement.js +53 -0
  146. package/dist/plugins/stop-then-start-sentence.d.ts +42 -3
  147. package/dist/plugins/stop-then-start-sentence.js +44 -0
  148. package/dist/plugins/store-backend.d.ts +1 -1
  149. package/dist/plugins/store-contracts.d.ts +12 -2
  150. package/dist/plugins/store-contracts.js +3 -0
  151. package/dist/plugins/tidb-pool.js +4 -0
  152. package/dist/process-instance.d.ts +3 -0
  153. package/dist/process-instance.js +7 -0
  154. package/dist/project-context.d.ts +66 -0
  155. package/dist/project-context.js +70 -0
  156. package/dist/question.d.ts +9 -21
  157. package/dist/question.js +28 -19
  158. package/dist/run-cancel-card.d.ts +1 -1
  159. package/dist/run-cancel-card.js +2 -1
  160. package/dist/security.d.ts +1 -1
  161. package/dist/session-agent.d.ts +1 -10
  162. package/dist/session-agent.js +0 -6
  163. package/dist/session-incarnation.d.ts +18 -4
  164. package/dist/session-incarnation.js +4 -1
  165. package/dist/session-notify-door.d.ts +4 -4
  166. package/dist/session-notify-door.js +5 -6
  167. package/dist/session-rules-seat.d.ts +4 -3
  168. package/dist/session-rules-seat.js +3 -11
  169. package/dist/session-sync.js +3 -3
  170. package/dist/stores-sql/checkpoint-store-sql.d.ts +21 -2
  171. package/dist/stores-sql/checkpoint-store-sql.js +106 -18
  172. package/dist/stores-sql/retention-store-sql.d.ts +1 -1
  173. package/dist/task-cwd.d.ts +4 -3
  174. package/dist/task-settings.d.ts +13 -0
  175. package/dist/task-settings.js +13 -7
  176. package/dist/tool-approval.d.ts +7 -6
  177. package/dist/tool-approval.js +41 -37
  178. package/dist/trace/engine-notice-wire.d.ts +14 -0
  179. package/dist/trace/engine-notice-wire.js +18 -1
  180. package/dist/trace/project.d.ts +4 -3
  181. package/dist/trace/project.js +3 -2
  182. package/dist/trace/projection-table.d.ts +33 -2
  183. package/dist/trace/self-minted-wire.d.ts +150 -17
  184. package/dist/trace/self-minted-wire.js +54 -16
  185. package/dist/trace/tool-card.d.ts +3 -0
  186. package/dist/trace/tool-card.js +24 -0
  187. package/dist/trace/tool-output-media.d.ts +2 -1
  188. package/dist/unattended-permission-mode.d.ts +1 -1
  189. package/dist/web-admin-client/apply-effective.d.ts +1 -1
  190. package/dist/web-admin-client/effective-keys.d.ts +1 -1
  191. package/dist/web-admin-client/skills-mcp.d.ts +1 -1
  192. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +57 -29
  193. package/package.json +2 -2
  194. package/wire-changes/server@7.150.0.json +335 -0
@@ -2,15 +2,54 @@
2
2
  * 「停-迁-起」那一段的**唯一属主**。
3
3
  *
4
4
  * 一次性迁移里有几条(登记表 `stopThenStart` 一格点名的)改的是更早版本的副本还会继续写的形:改完之后它再写一笔,这一笔读不出 / 丢掉。它们没有运行期开关,
5
- * 起服时由守卫判 —— 判活窗内(`REAP_RUN_STALE_SEC`,收割器判 run 持有者还活着的同一个窗)库里还有 `running` run 在心跳 ⇒ 拒启、一字节不写。
5
+ * 起服时由守卫判 —— 库上有「不知道这条迁移的副本还在服务」的证据 ⇒ 拒启、一字节不写。证据只有一种读法(`one-time-migrations.ts` 的 `readOlderReplicaEvidence`):
6
+ * 判活窗内(`REAP_RUN_STALE_SEC`,收割器判 run 持有者还活着的同一个窗)心跳过的 `running` run,持有者没有登记为知道这条迁移(`replica-registration.ts`)。
6
7
  * 四只守卫(M40 · M41 · M42 · M51)的拒句和起服列册对账的升级清单**说同一件事**:先停光更早版本的副本、等满判活窗、再起本版。
7
- * 这件事的句子只在这里写一遍;各守卫只讲「为什么这一条要停」,不各抄一份动作段。
8
+ * 这件事的句子只在这里写一遍:证据一段({@link olderReplicaEvidenceSentence})与动作一段({@link stopThenStartAction});各守卫只讲「为什么这一条要停」。
9
+ * 起服准入(`serving-requirement.ts`:库上记着本构建没有的能力 ⇒ 拒启)的拒句同属这一族,也在这里({@link servingRequirementUnmetSentence})。
8
10
  *
9
- * 叶子模块:零 import(守卫所在的一次性迁移文件、M42 的拒句文件与列册对账都静态 import 它,不成环)。
11
+ * 叶子模块:零 import(守卫所在的一次性迁移文件、M42 的拒句文件、起服准入与列册对账都静态 import 它,不成环)。
10
12
  */
13
+ /** 证据里一只登记过、却没有这条迁移能力的持有者(归因用:点名它是谁)。 */
14
+ export interface OlderReplicaHolder {
15
+ readonly instanceId: string;
16
+ readonly releaseLine: string;
17
+ readonly hostName: string;
18
+ readonly pid: number;
19
+ /** 它持有的、判活窗内心跳过的 `running` run 数。 */
20
+ readonly runs: number;
21
+ }
22
+ /**
23
+ * 停-迁-起守卫的证据(纯数据):判活窗内心跳过、持有者没有登记为知道 {@link migration} 的 `running` run。
24
+ * 登记为知道它的持有者的 run 不在这里(它们不是旧形写者);登记只作归因,不判活。
25
+ */
26
+ export interface OlderReplicaEvidence {
27
+ readonly migration: string;
28
+ /** 算进证据的 run 总数(= {@link unregisteredRuns} + 各 {@link registeredWithout} 的 `runs`)。 */
29
+ readonly liveRuns: number;
30
+ readonly windowSec: number;
31
+ /** 全停之后可以再起的最早时刻(ISO;= 最新心跳 + 判活窗;读不出最新心跳 ⇒ 现在 + 窗,上界)。 */
32
+ readonly restartAfter: string;
33
+ /** 同一时刻的人读形(带「离现在还有几秒」)。 */
34
+ readonly after: string;
35
+ /** 持有者没有登记行(或没有持有者)的 run 数。 */
36
+ readonly unregisteredRuns: number;
37
+ /** 登记了而能力名单里没有这条迁移的持有者,逐只点名。 */
38
+ readonly registeredWithout: readonly OlderReplicaHolder[];
39
+ }
40
+ /** 证据一段(各守卫拒句与 M43 的读数共用;句子不含时刻 —— 时刻在动作段)。 */
41
+ export declare function olderReplicaEvidenceSentence(e: OlderReplicaEvidence): string;
11
42
  /**
12
43
  * 动作段:停光 → 等窗 → 再起。`after` = 全停之后可以再起的最早时刻(守卫读到的最新心跳 + 判活窗,人读形);
13
44
  * 缺席 ⇒ 读不到具体心跳(列册对账的清单:此刻没有守卫在判),只说「等满判活窗」。
14
45
  */
15
46
  export declare function stopThenStartAction(after?: string): string;
47
+ /** 起服准入里一行本构建没有的要求(`state` = 判读的态,词表外的词读作 `in_force`;`word` = 库里存的原词)。 */
48
+ export interface UnmetServingRequirement {
49
+ readonly migration: string;
50
+ readonly state: "pending" | "in_force";
51
+ readonly word: string;
52
+ }
53
+ /** 起服准入的拒句(`reason` 闭集两词;码由调用方交,本模块零 import)。 */
54
+ export declare function servingRequirementUnmetSentence(code: string, reason: "capability_missing" | "table_absent", unmet: readonly UnmetServingRequirement[], absentTables: readonly string[], releaseLine: string): string;
16
55
  //# sourceMappingURL=stop-then-start-sentence.d.ts.map
@@ -1,7 +1,51 @@
1
+ export function olderReplicaEvidenceSentence(e) {
2
+ const parts = [];
3
+ if (e.unregisteredRuns > 0)
4
+ parts.push(`${e.unregisteredRuns} by replicas with no registration on this database (a release that predates replica registration, or a process that skipped the start sequence)`);
5
+ for (const h of e.registeredWithout)
6
+ parts.push(`${h.runs} by instance ${h.instanceId} (release ${h.releaseLine}, host ${h.hostName}, pid ${h.pid}), registered without it`);
7
+ return (`${e.liveRuns} running run(s) on this database heartbeated within the last ${e.windowSec}s (REAP_RUN_STALE_SEC), held by replicas not registered as knowing one-time migration ${e.migration}` +
8
+ ` (${parts.join("; ")})`);
9
+ }
1
10
  export function stopThenStartAction(after) {
2
11
  return ("Stop every replica of the earlier version, then start this version (stop, migrate, start — this release cannot be rolled across replicas of both versions). " +
3
12
  (after === undefined
4
13
  ? "Wait out the heartbeat window (REAP_RUN_STALE_SEC) after the last replica stops, then start this version."
5
14
  : `If every replica is already stopped, those are the runs they left behind: start this version again after ${after}`));
6
15
  }
16
+ function requirementClause(r) {
17
+ switch (r.state) {
18
+ case "pending":
19
+ return `${r.migration} (pending: a newer build recorded it and has not changed any data for it yet)`;
20
+ case "in_force":
21
+ return r.word === "in_force"
22
+ ? `${r.migration} (in force: data has been changed for it)`
23
+ : `${r.migration} (state "${r.word}", which this build does not know — read as in force)`;
24
+ default: {
25
+ const unhandled = r.state;
26
+ throw new Error(`stop-then-start-sentence: unknown requirement state ${String(unhandled)}`);
27
+ }
28
+ }
29
+ }
30
+ export function servingRequirementUnmetSentence(code, reason, unmet, absentTables, releaseLine) {
31
+ switch (reason) {
32
+ case "capability_missing": {
33
+ const inForce = unmet.filter((r) => r.state === "in_force").map((r) => r.migration);
34
+ return (`${code} (reason: capability_missing): this database requires every serving replica to have ${unmet.map(requirementClause).join(", ")}; this build (release ${releaseLine}) does not. ` +
35
+ "No one-time migration ran and no stored data was changed (the section head only registered this replica and removed registrations of other replicas older than 90 days). " +
36
+ `Start a build that has ${unmet.map((r) => r.migration).join(", ")}.` +
37
+ (inForce.length === 0
38
+ ? ""
39
+ : ` Only to roll back on purpose, after handling the data of that migration: ${inForce.map((m) => `DELETE FROM serving_requirement WHERE migration_id = '${m}';`).join(" ")}`));
40
+ }
41
+ case "table_absent":
42
+ return (`${code} (reason: table_absent): this database has no ${absentTables.join(" / ")} table — the serving requirements recorded there cannot be read, ` +
43
+ 'and an unreadable requirement is not "no requirement", so this build refuses to start. Nothing was written. ' +
44
+ "Open the store with the public sequence createStoreBackend() → ensureSchema() → runOneTimeMigrations(): ensureSchema() creates these tables.");
45
+ default: {
46
+ const unhandled = reason;
47
+ throw new Error(`stop-then-start-sentence: unknown refusal reason ${String(unhandled)}`);
48
+ }
49
+ }
50
+ }
7
51
  //# sourceMappingURL=stop-then-start-sentence.js.map
@@ -467,7 +467,7 @@ export declare function createSqlSessionEraser(db: SqlDriver, opts: RetentionExe
467
467
  * ({@link SQL_BLOB_POSTURE_FACTS})。云形缺 MinIO ⇒ fail-loud(启动了才是假安全);`SNAPSHOT_BLOB_ALLOW_SQL_BYTES=true` =
468
468
  * 显式接受 SQL 店 + per-blob 帽(单机/测试台形;pg 上帽只来自 `SNAPSHOT_BLOB_SQL_MAX_BYTES`)。local 后端不经此门(file 店,无 SQL 包墙)。 */
469
469
  export declare function assertCloudSnapshotBlobPosture(kind: "mysql" | "pg" | "local", config: Pick<ServiceConfig, "snapshotBlobStore" | "snapshotBlobAllowSql">): void;
470
- /** SQL 一次性迁移段的输入(两只 SQL 后端同一份;各条目那一半的来源见 `SqlOneTimeMigrationInput`):M32 的车道表 · M40 的判活窗 = 收割器的同一个窗 · M48 的部署形读数(表里那一行)。 */
470
+ /** SQL 一次性迁移段的输入(两只 SQL 后端同一份;各条目那一半的来源见 `SqlOneTimeMigrationInput`):M32 的车道表 · M52 的判活窗(停-迁-起证据的心跳一半)= 收割器的同一个窗 · M48 的部署形读数(表里那一行)。 */
471
471
  export declare function buildSqlOneTimeMigrationInput(config: ServiceConfig): SqlOneTimeMigrationInput;
472
472
  /**
473
473
  * LOCAL backend (clay 2026-06-25 seamless local↔cloud; P0.5 variant-2 + variant-1 file-backed 2026-06-26): a DB-less StoreBackend
@@ -339,10 +339,14 @@ export type CreateRunOutcome = {
339
339
  };
340
340
  /**
341
341
  * 认领栅栏交给 `write` 的那一行 run 此刻的事实(SQL = 锁住那一行读的;进程内 = 同步段里读的)。认领与 `running` 只认 taskId,而续跑腿让同一个 taskId
342
- * 停驻之后回到 `running`、认领仍在 —— 「接下这笔写的那条腿还是不是这条 run 的腿」要按腿问的写者拿它问(今天:副本 `instanceId`,续跑的 CAS 赢点把它改成接手的副本)。
342
+ * 停驻之后回到 `running`、认领仍在 —— 「接下这笔写的那条腿还是不是这条 run 的腿」要按腿问的写者拿它问:
343
+ * · `legId` = 行此刻的持有腿({@link RunRecord.legId},认领铸、交给认领者)。持认领的腿自己的写问「是不是我」—— 判据 {@link createLegCtxClaim},
344
+ * 与停驻写的 CAS(`setSuspended` / `setNeedsReview`)、撤回(`withdrawMintedRun`)同一只身份读法;
345
+ * · `instanceId` = 行此刻的副本。切档口(`routes/run-permission-mode.ts` `switchLegHolds`)问的是「是不是我切过的那条腿」,它手上是本副本活流登记簿里的流对象。
343
346
  */
344
347
  export interface FencedRun {
345
348
  readonly instanceId: string | null;
349
+ readonly legId: string | null;
346
350
  }
347
351
  /** SQL 认领栅栏的结局:`held: false` = 认领此刻不是这条 run 的(或它已不在跑),`write` 一次都没调。 */
348
352
  export type ClaimFenced<T> = {
@@ -376,7 +380,7 @@ export type ClaimFenceStore = import("./run-store-sql.js").SqlRunStore | InProce
376
380
  /**
377
381
  * 🔴 [ref] **一笔续跑输入写所凭的那只认领**(检查点店的写口 `putCtx` 交认领的那一形与 `rewriteCtxWhileClaimed` 收它):
378
382
  * `fence` = 写者持认领的那只 run 店(栅栏的来源就是认领本身 —— 检查点店不在构造期另绑一份 run 店),`taskId` = 持认领的 run,
379
- * `legHolds` = 腿判据(在栅栏里对锁住 / 同步段里那一行同步问一次,只读、不许 await)。
383
+ * `legHolds` = 腿判据(在栅栏里对锁住 / 同步段里那一行同步问一次,只读、不许 await;持认领的腿经 {@link createLegCtxClaim} 按持有腿判)。
380
384
  * 装配契约:SQL 检查点店只收同一库上的 SQL run 店(两店出自同一后端;店按判别位 `db` 判 SQL 形且方言同,错形响亮抛),local 检查点店只收进程内栅栏。
381
385
  */
382
386
  export interface CtxWriteClaim {
@@ -384,6 +388,12 @@ export interface CtxWriteClaim {
384
388
  readonly taskId: string;
385
389
  readonly legHolds: (run: FencedRun) => boolean;
386
390
  }
391
+ /**
392
+ * 持认领的腿写续跑输入所凭的那只认领(提交腿 `http/submit-ctx.ts` 与续跑腿的写回 `http/resume-legs.ts` 同调这一只):腿判据 = 「被锁那一行此刻的持有腿就是本腿」——
393
+ * `legId` = 认领交给本腿的那一只(建行 `createRun` 的 `{ ok: true, legId }` / 续跑认领 `markResuming` 的交回值)。同一副本上同一 taskId 被后来的腿重新认领 ⇒
394
+ * 持有腿已换 ⇒ 零写(副本没变,只问副本的判据分不开这两条腿)。
395
+ */
396
+ export declare function createLegCtxClaim(fence: ClaimFenceStore, taskId: string, legId: string): CtxWriteClaim;
387
397
  /**
388
398
  * `RunRecord["status"]`(= `"running" | TaskStatus`)的**唯一分类表** —— 词表的属主 = 判据的属主,
389
399
  * 与类型定义同住一个文件。四个类:
@@ -30,6 +30,9 @@ export function mintRunLegId() {
30
30
  return randomUUID();
31
31
  }
32
32
  export const FENCE_LANDED = Symbol("in-process-claim-fence.landed");
33
+ export function createLegCtxClaim(fence, taskId, legId) {
34
+ return { fence, taskId, legHolds: (run) => run.legId === legId };
35
+ }
33
36
  export const RUN_STATUS_CLASS = {
34
37
  running: "live",
35
38
  completed: "succeeded",
@@ -31,6 +31,8 @@ import { RETENTION_LANE_INDEXES } from "./retention-lane-store-sql.js";
31
31
  import { LEADER_RUN_INDEXES } from "./leader-run-store-sql.js";
32
32
  import { acquireAdvisoryLock, releaseAdvisoryLock } from "./ensure-schema-lock-policy.js";
33
33
  import { SKILL_EXEMPTION_CUTOFF_ID, markIfAbsentSql, migrationMarkerDdl } from "./one-time-migration-marker.js";
34
+ import { replicaRegistrationDdl } from "./replica-registration.js";
35
+ import { servingRequirementDdl } from "./serving-requirement.js";
34
36
  import { createCloseAwarePool } from "./sql-driver.js";
35
37
  export function tidbPoolOptions(tidb) {
36
38
  const { host, port, user, password, database, connectionLimit } = tidb;
@@ -780,6 +782,8 @@ export const SCHEMA_STATEMENTS = [
780
782
  PRIMARY KEY (pool)
781
783
  ) COLLATE utf8mb4_bin`,
782
784
  migrationMarkerDdl("mysql"),
785
+ replicaRegistrationDdl("mysql"),
786
+ servingRequirementDdl("mysql"),
783
787
  ...TIDB_APPROVAL_ASK_STATEMENTS,
784
788
  ...TIDB_MEMORY_OPTOUT_GRANT_STATEMENTS,
785
789
  ...TIDB_SESSION_CAPTURE_OPTOUT_STATEMENTS,
@@ -0,0 +1,3 @@
1
+ /** 本进程的身份(第一次调用时铸,之后恒同值)。 */
2
+ export declare function processInstanceId(): string;
3
+ //# sourceMappingURL=process-instance.d.ts.map
@@ -0,0 +1,7 @@
1
+ import { uuidv7 } from "@sema-agent/core";
2
+ let minted;
3
+ export function processInstanceId() {
4
+ minted ??= uuidv7();
5
+ return minted;
6
+ }
7
+ //# sourceMappingURL=process-instance.js.map
@@ -0,0 +1,66 @@
1
+ import { type ProjectRegistration } from "@sema-agent/settings-schema";
2
+ import type { ListFilter } from "./owner-faces.js";
3
+ /** 进简报 / 进 `cwd` 的登记字段(闭集)。每一格的写角色不低于 `prompts` 域(`test/project-context-inheritance.test.ts` 按 settings-schema 的 `writeRoleFor` 钉);身份字段永不在表里。 */
4
+ export declare const PROJECT_BRIEF_FIELDS: readonly ["goal", "notes", "contextItems"];
5
+ /** 进简报 / 进 `cwd` 的登记字段。 */
6
+ export type ProjectBriefField = (typeof PROJECT_BRIEF_FIELDS)[number];
7
+ /** 内容证明:配置中心写口在一次按写权表判过内容字段的写之后盖的时刻(写角色同为 publisher)。本服务只读在不在。 */
8
+ export declare const PROJECT_CONTENT_ATTESTATION_FIELD: "contentAttestedAt";
9
+ /** 继承判读得到的登记成员(类型上只够得着表内字段 + 可见性的属主键 + 内容证明)。 */
10
+ export type ProjectContextSource = Pick<ProjectRegistration, ProjectBriefField | "registeredBy" | typeof PROJECT_CONTENT_ATTESTATION_FIELD>;
11
+ /** 一条 run 继承到的项目上下文(冻结记录;随 run 落进续跑重建输入,续跑腿原样重放)。 */
12
+ export interface ProjectContext {
13
+ /** 判出它的项目(登记簿键,小写)。 */
14
+ readonly projectId: string;
15
+ /** 继承的工作目录:第一条 `folder` 条目 —— 只在射程内(`callerOwnsWorkerDisk`)且请求没带 `cwd` 时在场。 */
16
+ readonly cwd?: string;
17
+ /** 项目简报(系统提示追加折叠的第二段);`projectMemory: "disabled"` 或无可列内容 ⇒ 缺席。 */
18
+ readonly brief?: string;
19
+ }
20
+ /** 继承判的输入(全部是新提交腿此刻读得到的事实)。 */
21
+ export interface ProjectContextInput {
22
+ /** 这条腿的项目(`legProjectIdOf`);缺席 ⇒ 不继承。 */
23
+ readonly projectId: string | undefined;
24
+ /** 本部署的登记簿(`capability-facts.ts` `projectRegistry` 的见证);缺席 = 本部署没有登记簿 ⇒ 不继承。 */
25
+ readonly registry: Readonly<Record<string, ProjectContextSource>> | undefined;
26
+ /** 提交者在读面上的视域(读面同一只推导);缺席 = 没有请求 ⇒ 不继承。 */
27
+ readonly viewer: ListFilter | undefined;
28
+ /** 部署形表行 `callerOwnsWorkerDisk`:路径条目(继承 `cwd`、简报的路径两段)只在它成立时兑现。 */
29
+ readonly pathsHonored: boolean;
30
+ /** 请求体带了 `cwd`(非空串)⇒ 显式值赢,不填。 */
31
+ readonly requestCwd: boolean;
32
+ /** 请求 `projectMemory: "disabled"` ⇒ 不出简报。 */
33
+ readonly projectMemoryOff: boolean;
34
+ }
35
+ /** 继承判的答案:继承到的上下文,外加射程外未列的路径条目数(server 侧 warn 只计数,不记路径)。 */
36
+ export interface ProjectContextDecision {
37
+ readonly context: ProjectContext | undefined;
38
+ readonly withheldPathItems: number;
39
+ }
40
+ /** 一只登记对这位观者可见吗(读面与继承同调的唯一判):全舰队视域看全部;否则只看 `registeredBy` 恰是自己的。看不见与不存在同答。 */
41
+ export declare function projectVisible(registration: Pick<ProjectRegistration, "registeredBy">, viewer: ListFilter): boolean;
42
+ /**
43
+ * 项目简报的唯一铸点(纯串;各段缺席不出,全缺席 ⇒ 无简报)。只读 {@link PROJECT_BRIEF_FIELDS};路径两段只在 `pathsHonored` 时出,且只列名字 ——
44
+ * `file` 条目未附上,段名写明「相关时去读」。首行点名来源与项目 id(不带 `displayName`:身份字段不进提示)。
45
+ */
46
+ export declare function buildProjectBrief(projectId: string, registration: Pick<ProjectRegistration, ProjectBriefField>, pathsHonored: boolean): string | undefined;
47
+ /**
48
+ * 继承判(唯一;调用点只有装配器头部的新提交腿)。项目缺席 / 本部署无登记簿 / 没有观者 / 未登记 / 看不见 / 无内容证明 ⇒ 不继承(不拒,与随机 id 同答)。
49
+ */
50
+ export declare function projectContextOf(input: ProjectContextInput): ProjectContextDecision;
51
+ /** 续跑重建输入里冻结的那一份的读数:缺席(没继承 / 7.150.0 之前写下的行)· 读得出 · 形不对。 */
52
+ export type FrozenProjectContextReading = {
53
+ readonly kind: "absent";
54
+ } | {
55
+ readonly kind: "ok";
56
+ readonly context: ProjectContext;
57
+ } | {
58
+ readonly kind: "malformed";
59
+ };
60
+ /**
61
+ * 冻结的那一份的**唯一读法**(读者 = `http/resume-ctx-read.ts` 的续跑上下文读口)。服务端写下的形是闭集(本文件 {@link projectContextOf} 铸、`buildCheckpointCtx` 原样落盘):
62
+ * 自有键只在 `projectId` / `cwd` / `brief` 里;`projectId` = 规范小写 UUID;`cwd` 在场 = 宿主绝对路径;`brief` 在场 = 非空串、不超过系统提示追加折叠的帽;
63
+ * `cwd` 与 `brief` 至少其一(都缺的不会被冻结)。别的形 ⇒ `malformed`(读口答坏体:续跑不重放它、不登记它的 `cwd`)。
64
+ */
65
+ export declare function frozenProjectContextOf(stored: unknown): FrozenProjectContextReading;
66
+ //# sourceMappingURL=project-context.d.ts.map
@@ -0,0 +1,70 @@
1
+ import { PROJECT_ID_REGEX } from "@sema-agent/core";
2
+ import { isAbsoluteHostPath } from "@sema-agent/settings-schema";
3
+ import { MAX_SETTINGS_OUTPUT_STYLE_CHARS } from "./task-settings.js";
4
+ export const PROJECT_BRIEF_FIELDS = ["goal", "notes", "contextItems"];
5
+ export const PROJECT_CONTENT_ATTESTATION_FIELD = "contentAttestedAt";
6
+ export function projectVisible(registration, viewer) {
7
+ return viewer.fleetWide || registration.registeredBy === viewer.gateOwner;
8
+ }
9
+ export function buildProjectBrief(projectId, registration, pathsHonored) {
10
+ const sections = [];
11
+ if (registration.goal !== undefined && registration.goal.length > 0)
12
+ sections.push(`## Goal\n${registration.goal}`);
13
+ if (registration.notes !== undefined && registration.notes.length > 0)
14
+ sections.push(`## Notes\n${registration.notes}`);
15
+ const items = registration.contextItems ?? [];
16
+ const notes = items.flatMap((item) => (item.kind === "note" && item.value.length > 0 ? [item.value] : []));
17
+ if (notes.length > 0)
18
+ sections.push(`## Project notes\n${notes.join("\n\n")}`);
19
+ if (pathsHonored) {
20
+ const folders = items.flatMap((item) => (item.kind === "folder" ? [`- ${item.value}`] : []));
21
+ if (folders.length > 0)
22
+ sections.push(`## Project folders\n${folders.join("\n")}`);
23
+ const files = items.flatMap((item) => (item.kind === "file" ? [`- ${item.value}`] : []));
24
+ if (files.length > 0)
25
+ sections.push(`## Project files (not attached — read them when relevant)\n${files.join("\n")}`);
26
+ }
27
+ if (sections.length === 0)
28
+ return undefined;
29
+ return [`# Project context (from this deployment's project registry; project ${projectId})`, ...sections].join("\n\n");
30
+ }
31
+ export function projectContextOf(input) {
32
+ const { projectId, registry, viewer } = input;
33
+ if (projectId === undefined || registry === undefined || viewer === undefined)
34
+ return { context: undefined, withheldPathItems: 0 };
35
+ const registration = Object.hasOwn(registry, projectId) ? registry[projectId] : undefined;
36
+ if (registration === undefined || !projectVisible(registration, viewer))
37
+ return { context: undefined, withheldPathItems: 0 };
38
+ if (registration[PROJECT_CONTENT_ATTESTATION_FIELD] === undefined)
39
+ return { context: undefined, withheldPathItems: 0 };
40
+ const items = registration.contextItems ?? [];
41
+ const withheldPathItems = input.pathsHonored ? 0 : items.filter((item) => item.kind !== "note").length;
42
+ const firstFolder = input.pathsHonored ? items.find((item) => item.kind === "folder")?.value : undefined;
43
+ const cwd = input.requestCwd ? undefined : firstFolder;
44
+ const brief = input.projectMemoryOff ? undefined : buildProjectBrief(projectId, registration, input.pathsHonored);
45
+ if (cwd === undefined && brief === undefined)
46
+ return { context: undefined, withheldPathItems };
47
+ return { context: { projectId, ...(cwd !== undefined ? { cwd } : {}), ...(brief !== undefined ? { brief } : {}) }, withheldPathItems };
48
+ }
49
+ export function frozenProjectContextOf(stored) {
50
+ if (stored === undefined)
51
+ return { kind: "absent" };
52
+ if (typeof stored !== "object" || stored === null || Array.isArray(stored))
53
+ return { kind: "malformed" };
54
+ if (!Object.keys(stored).every((k) => k === "projectId" || k === "cwd" || k === "brief"))
55
+ return { kind: "malformed" };
56
+ const own = (k) => (Object.hasOwn(stored, k) ? stored[k] : undefined);
57
+ const projectId = own("projectId");
58
+ const cwd = own("cwd");
59
+ const brief = own("brief");
60
+ if (typeof projectId !== "string" || !PROJECT_ID_REGEX.test(projectId) || projectId !== projectId.toLowerCase())
61
+ return { kind: "malformed" };
62
+ if (cwd !== undefined && (typeof cwd !== "string" || !isAbsoluteHostPath(cwd)))
63
+ return { kind: "malformed" };
64
+ if (brief !== undefined && (typeof brief !== "string" || brief.length === 0 || brief.length > MAX_SETTINGS_OUTPUT_STYLE_CHARS))
65
+ return { kind: "malformed" };
66
+ if (cwd === undefined && brief === undefined)
67
+ return { kind: "malformed" };
68
+ return { kind: "ok", context: { projectId, ...(cwd !== undefined ? { cwd } : {}), ...(brief !== undefined ? { brief } : {}) } };
69
+ }
70
+ //# sourceMappingURL=project-context.js.map
@@ -9,15 +9,15 @@ export interface QuestionFrame {
9
9
  /** "question" only: the model's structured questions, secret-redacted. UNTRUSTED-for-display (shell renders; never
10
10
  * re-feeds a model). Absent on an over-cap payload (the ask then reports unavailable — no frame is emitted at all). */
11
11
  questions?: AskQuestion[];
12
- /** "question_complete" only: whether the human answered (`answered`) or the ask was released unanswered
13
- * (`unanswered` — ttl/abort → core was told nobody was reachable). Cosmetic dialog-dismiss. */
12
+ /** "question_complete" only: whether the human answered (`answered`) or the ask closed without an answer
13
+ * (`unanswered` — window elapsed / abort / leg-exit sweep). Cosmetic dialog-dismiss. */
14
14
  outcome?: "answered" | "unanswered";
15
15
  /** Server-signed emit timestamp (additive, [ref]§五→[ref]). Server-minted side-frame ⇒ no core-stream
16
16
  * `eventId` to carry (absence is honest); this is the frame's own time coordinate. */
17
17
  serverNowMs?: number;
18
18
  /** "question" only ([ref]③, additive, the `tool_approval` window-trio precedent from [ref]): the
19
- * coordinator's OWN answer window (`QUESTION_TTL_MS`, default 5min) — at expiry the ask releases
20
- * UNAVAILABLE ("unanswered") and core adjudicates park-vs-continue. Minted from the SAME `ttlMs` the
19
+ * coordinator's OWN answer window (`QUESTION_TTL_MS`, default 5min) — at expiry the ask closes ("unanswered"); core hears
20
+ * a delivery failure when a live face presented the card, `unavailable` otherwise ({@link seamOutcomeOf}). Minted from the SAME `ttlMs` the
21
21
  * `setTimeout` below actually arms (single source; a shell countdown rendered from these can never
22
22
  * disagree with the release). Relative + absolute + `serverNowMs` above = clock-skew-proof trio. */
23
23
  expiresInMs?: number;
@@ -30,9 +30,7 @@ export interface QuestionFrame {
30
30
  * awaiting a human when the run aborts.
31
31
  *
32
32
  * The ledger carries the CARD, not the answer door: `POST /v1/questions/:id/respond` stays live-only (this replica's
33
- * memory, `ttlMs`), so a replayed card is answerable only until its own `expiresAtMs` and only on this replica.
34
- * The adjudication probe ({@link QuestionCoordinator.hasLiveContext}) reads the same transport's `reachable()` — one
35
- * reading of "can this leg's face show a card right now?". */
33
+ * memory, `ttlMs`), so a replayed card is answerable only until its own `expiresAtMs` and only on this replica. */
36
34
  export interface QuestionRunContext {
37
35
  taskId: string;
38
36
  owner: string | null;
@@ -73,12 +71,11 @@ export declare class QuestionCoordinator {
73
71
  private readonly throttle;
74
72
  constructor(throttle?: QuestionThrottle);
75
73
  /** Run `fn` with the per-run question context ambient. On exit, release any still-pending question for this run (a
76
- * live-only question cannot outlive its leg → unavailable) and drop the run's counters (no leak). */
74
+ * live-only question cannot outlive its leg → released) and drop the run's counters (no leak). */
77
75
  runWithContext<T>(ctx: QuestionRunContext, fn: () => Promise<T>): Promise<T>;
78
- /** `RunnerDeps.onQuestion`. core calls this when the agent's AskUserQuestion tool fires; the resolved outcome is what
79
- * core acts on — a {@link QuestionAnswer} ONLY when a human really answered, otherwise `{kind:"unavailable"}` (core
80
- * then parks on a durable leg / continues with the `declined_unavailable` card on a non-durable one; either way the
81
- * run never hangs). Arrow property so it can be passed as `onQuestion: coordinator.question` with `this` bound. */
76
+ /** `RunnerDeps.onQuestion`. core calls this when the agent's AskUserQuestion tool fires; what it settles to is what core
77
+ * acts on — a {@link QuestionAnswer} ONLY when a human really answered; a THROW when a live face presented the card and its
78
+ * window elapsed unanswered; `{kind:"unavailable"}` otherwise ({@link seamOutcomeOf}). Either way the run never hangs. Arrow property so it can be passed as `onQuestion: coordinator.question` with `this` bound. */
82
79
  question: (req: AskQuestionRequest, signal?: AbortSignal) => Promise<OnQuestionOutcome>;
83
80
  /** `POST /v1/questions/:id/respond` — resolve a parked question with the shell's answer. Owner-gated with a 404 (no
84
81
  * existence oracle): a non-owner AND an unknown id (answered / expired / wrong replica) both get 404. Returns the
@@ -89,15 +86,6 @@ export declare class QuestionCoordinator {
89
86
  };
90
87
  /** Test/observability hook: number of currently-parked questions. */
91
88
  pendingCount(): number;
92
- /** [ref] ([ref] 案二):durable 部署上的 AskUserQuestion 判决探针——本调用点是否处在某条**投递得到人的**
93
- * 活流腿上(runWithContext 包裹的 bg/SSE 腿=true;sync /v1/tasks、verify/cascade、durable resume 驱动=false)。
94
- * resolve-spec 的 durable question policy 用它在**判决时**分腿:有活流 ⇒ allow(问活人),无 ⇒
95
- * ask(durable park)。ALS 让这个判断天然 per-leg,policy 组装期不必预知腿别。
96
- * 🔴 判据是「投递面此刻可达」而不只是「ALS 在场」(复审 A1):detach 腿断连后 run 仍在本作用域里跑,
97
- * 只判 ALS 会把一条谁也收不到的问题判成 allow;park 才是它该走的腿。读的是本腿传输的 `reachable()`
98
- * (有直写面 ⇒ 直写此刻活着;只有账本 ⇒ 恒真,后台腿的 durable events tail 语义;谓词读不出 ⇒ 假,判决面 fail-closed 到 park)。
99
- * 只服务判决面:判决放行之后的开帧投递只读传输律(有账本的腿上死直写 = 卡在账本里,送到)。 */
100
- hasLiveContext(): boolean;
101
89
  private countersFor;
102
90
  }
103
91
  //# sourceMappingURL=question.d.ts.map
package/dist/question.js CHANGED
@@ -12,6 +12,20 @@ export const DEFAULT_QUESTION_THROTTLE = {
12
12
  ttlMs: 5 * 60_000,
13
13
  };
14
14
  const UNAVAILABLE = Object.freeze({ kind: "unavailable" });
15
+ const RELEASED = Object.freeze({ kind: "released" });
16
+ const WINDOW_ELAPSED = Object.freeze({ kind: "window_elapsed" });
17
+ function seamOutcomeOf(settlement, presentedLive, ttlMs) {
18
+ switch (settlement.kind) {
19
+ case "answered":
20
+ return settlement.answer;
21
+ case "released":
22
+ return UNAVAILABLE;
23
+ case "window_elapsed":
24
+ if (!presentedLive)
25
+ return UNAVAILABLE;
26
+ throw new Error(`the question was shown on this run's live stream and nobody answered it within its ${ttlMs / 1000}s answer window`);
27
+ }
28
+ }
15
29
  export function parseQuestionResponse(body) {
16
30
  if (body === null || typeof body !== "object" || Array.isArray(body))
17
31
  return { ok: false, error: "body must be an object" };
@@ -61,7 +75,7 @@ export class QuestionCoordinator {
61
75
  finally {
62
76
  for (const [, p] of [...this.pending]) {
63
77
  if (p.taskId === ctx.taskId)
64
- p.settle(UNAVAILABLE, "unanswered");
78
+ p.settle(RELEASED);
65
79
  }
66
80
  this.counters.delete(ctx.taskId);
67
81
  }
@@ -83,9 +97,9 @@ export class QuestionCoordinator {
83
97
  rc.inflight += 1;
84
98
  let done = false;
85
99
  let timer;
86
- let resolveAnswer;
87
- const answerP = new Promise((resolve) => { resolveAnswer = resolve; });
88
- const settle = (a, outcome) => {
100
+ let resolveSettled;
101
+ const settledP = new Promise((resolve) => { resolveSettled = resolve; });
102
+ const settle = (s) => {
89
103
  if (done)
90
104
  return;
91
105
  done = true;
@@ -95,12 +109,10 @@ export class QuestionCoordinator {
95
109
  ctx.abortSignal?.removeEventListener("abort", onAbort);
96
110
  this.pending.delete(id);
97
111
  rc.inflight = Math.max(0, rc.inflight - 1);
98
- lastOutcome = outcome;
99
- resolveAnswer(a);
112
+ resolveSettled(s);
100
113
  };
101
- let lastOutcome = "unanswered";
102
- const onAbort = () => settle(UNAVAILABLE, "unanswered");
103
- timer = setTimeout(() => settle(UNAVAILABLE, "unanswered"), this.throttle.ttlMs);
114
+ const onAbort = () => settle(RELEASED);
115
+ timer = setTimeout(() => settle(WINDOW_ELAPSED), this.throttle.ttlMs);
104
116
  timer.unref?.();
105
117
  signal?.addEventListener("abort", onAbort, { once: true });
106
118
  ctx.abortSignal?.addEventListener("abort", onAbort, { once: true });
@@ -108,7 +120,7 @@ export class QuestionCoordinator {
108
120
  const now = Date.now();
109
121
  const race = createHitlOpenRace({
110
122
  targets: [{ transport: ctx.transport, frame: { type: "question", questionId: id, questions, serverNowMs: now, expiresInMs: this.throttle.ttlMs, expiresAtMs: now + this.throttle.ttlMs } }],
111
- settled: answerP,
123
+ settled: settledP,
112
124
  });
113
125
  const phase = await race.phase;
114
126
  if (phase === "delivered") {
@@ -116,7 +128,7 @@ export class QuestionCoordinator {
116
128
  }
117
129
  else if (phase === "undelivered") {
118
130
  recordFailOpen("server.question.open-frame-undelivered", `question=${id}`);
119
- settle(UNAVAILABLE, "unanswered");
131
+ settle(RELEASED);
120
132
  }
121
133
  else {
122
134
  void race.landing.then((late) => {
@@ -126,9 +138,10 @@ export class QuestionCoordinator {
126
138
  recordFailOpen("server.question.open-frame-undelivered", `question=${id}`);
127
139
  });
128
140
  }
129
- const answer = await answerP;
130
- race.close({ type: "question_complete", questionId: id, outcome: lastOutcome, serverNowMs: Date.now() });
131
- return answer;
141
+ const settlement = await settledP;
142
+ race.close({ type: "question_complete", questionId: id, outcome: settlement.kind === "answered" ? "answered" : "unanswered", serverNowMs: Date.now() });
143
+ const presentedLive = settlement.kind === "window_elapsed" && phase === "delivered" && (await race.acceptedLive(0));
144
+ return seamOutcomeOf(settlement, presentedLive, this.throttle.ttlMs);
132
145
  };
133
146
  respond(id, principal, body) {
134
147
  const parsed = parseQuestionResponse(body);
@@ -138,16 +151,12 @@ export class QuestionCoordinator {
138
151
  if (!entry || !keyAdmits("ask.answer", { subject: recoveredSubject(principal), operator: false }, ownerCellOf(entry.owner))) {
139
152
  return { status: 404, body: refusalBody({ errorCode: "question.not_pending", message: "no pending question for this id (answered, expired, or not on this replica)" }) };
140
153
  }
141
- entry.settle(parsed.value, "answered");
154
+ entry.settle({ kind: "answered", answer: parsed.value });
142
155
  return { status: 200, body: { questionId: id, delivery: "applied" } };
143
156
  }
144
157
  pendingCount() {
145
158
  return this.pending.size;
146
159
  }
147
- hasLiveContext() {
148
- const ctx = this.als.getStore();
149
- return ctx !== undefined && ctx.transport.reachable();
150
- }
151
160
  countersFor(taskId) {
152
161
  let rc = this.counters.get(taskId);
153
162
  if (!rc) {
@@ -15,7 +15,7 @@ import type { CheckpointToken } from "@sema-agent/core";
15
15
  import type { CheckpointStoreFull, RunStore } from "./plugins/store-backend.js";
16
16
  /** 取消动词在 `runSessionId` 这条 run 的会话上会结算的那一张卡;没有 ⇒ `null`。 */
17
17
  export declare function runCancelCardOf(cs: Pick<CheckpointStoreFull, "findPendingTokenBySession">, runSessionId: string): Promise<CheckpointToken | null>;
18
- /** 取消动词要问的 run 行的两口:这条会话上持着认领的 run、它的行。 */
18
+ /** 取消动词要问的 run 行的两口:这条会话上持着认领的 run、它的状态(不带属主的一格事实,只经属主存取器读)。 */
19
19
  export type CancelRunReader = Pick<RunStore, "getActiveTaskId" | "getRun">;
20
20
  /**
21
21
  * 取消动词会不会结算 `token` 这一张 —— 出路投影问的就是这一问,三个条件都是取消口自己的:
@@ -1,5 +1,6 @@
1
1
  import { LIVE_RUN_GATE_KINDS } from "./plugins/checkpoint-store-face.js";
2
2
  import { isParkedRunStatus } from "./plugins/store-contracts.js";
3
+ import { runFactsOf } from "./http/owner-access.js";
3
4
  const RUN_CANCEL_CARD = { gateKinds: LIVE_RUN_GATE_KINDS, axis: "run" };
4
5
  export function runCancelCardOf(cs, runSessionId) {
5
6
  return cs.findPendingTokenBySession(runSessionId, undefined, RUN_CANCEL_CARD);
@@ -13,7 +14,7 @@ export async function cancelSettlesCard(cs, runs, token) {
13
14
  const holder = await runs.getActiveTaskId(host);
14
15
  if (holder === undefined)
15
16
  return false;
16
- const run = await runs.getRun(holder);
17
+ const run = await runFactsOf(runs, holder);
17
18
  return run !== undefined && isParkedRunStatus(run.status) && (await runCancelCardOf(cs, host)) === token;
18
19
  }
19
20
  //# sourceMappingURL=run-cancel-card.js.map
@@ -135,7 +135,7 @@ export type OwnerAwareSessionStore = SessionStore & {
135
135
  register?: (sessionId: string, owner: string | null) => Promise<void>;
136
136
  /** [ref] placed 分区的元组读口(店的既有读口,一行 PK 读;`undefined` = 普通会话或无此行)。店不交它 = 不实现 placement = 没有 placed 行(无 repo 的内存店)。两个读者:
137
137
  * ① 会话面的属主门只在属主为 NULL 时读它一次(无主的 placed 行不是公共行,`http/principal-gate.ts` 的 `sessionOwnerCellOf`);
138
- * ② `agent` 的唯一读口 `session-agent.ts` 的 `readSessionAgent`(会话视图体顶层、运维审批队列的停驻行同读):店没有这一面 ⇒ 键缺席,读面不凭别处另铸子代身份。 */
138
+ * ② `agent` 的唯一读口 `http/owner-access.ts` 的 `readSessionAgent`(会话视图体顶层、运维审批队列的停驻行同读):店没有这一面 ⇒ 键缺席,读面不凭别处另铸子代身份。 */
139
139
  placementOf?: (sessionId: string) => Promise<SessionPlacementRecord | undefined>;
140
140
  /** E16 — enumerate the caller's sessions (owner-filtered on the session store's OWN owner column).
141
141
  * `includeUnowned` = [ref] 窄互认第二臂的**第三条件**(`owner IS NULL`)的 SQL 承载位:置真时
@@ -5,7 +5,7 @@
5
5
  * 店原样记下,首写不可变。三只持久会话店都存:SQL 两孪生在 `session_meta` 的 `placement_*` 列(`plugins/session-placement.ts` 的 `placementFromRow`),
6
6
  * local 后端(7.141.0 起)在会话存档行头(core 文件 repo)。读口是店的 `placementOf`(一行 PK 探针;店不交它 = 不存元组)。
7
7
  *
8
- * 读者(同一只读口 {@link readSessionAgent}、同一只投影 {@link sessionAgentOf},逐值同答):
8
+ * 读者(同一只读口 —— 属主存取器的 `readSessionAgent`(`http/owner-access.ts`:店的行读只在存取器里)—— 同一只投影 {@link sessionAgentOf},逐值同答):
9
9
  * · 会话视图 `GET /v1/sessions/:id` 体顶层的 `agent`(视图读才付探针;探针抛 ⇒ 视图读响亮失败,`boot/session-faces.ts`);
10
10
  * · 运维审批队列 `GET /v1/approvals` 的停驻行(`pending[]` 的 `checkpoint` 族)与 `/v1/approvals/stream` 的 `pending` 帧:行的身份就是它那只会话
11
11
  * (决断口 `/v1/approvals/:sessionId/decide`),行上的 `agent` = 同一只会话在视图上的 `agent`;一行读不出 ⇒ 只这一行缺键、F 类兜底计数
@@ -26,15 +26,6 @@ export interface SessionAgent {
26
26
  parentSessionId?: string;
27
27
  rootSessionId?: string;
28
28
  }
29
- /** 交委派元组读口的会话店面(本仓三只持久会话店与缓存包装层都交;内存店不交)。 */
30
- export interface PlacementReadingSessionStore {
31
- readonly placementOf?: (sessionId: string) => Promise<SessionPlacementRecord | undefined>;
32
- }
33
29
  /** {@link SessionAgent} 的唯一投影:店答出的委派元组 → `agent` 的值;`undefined`(普通会话 / 店没有这一面)⇒ 调用方不铸键。 */
34
30
  export declare function sessionAgentOf(placement: SessionPlacementRecord | undefined): SessionAgent | undefined;
35
- /**
36
- * `agent` 的**唯一读口**:店的 `placementOf` 一读 → {@link sessionAgentOf}。店不交 `placementOf` ⇒ `undefined`(这台部署不存元组,不探)。
37
- * 探针抛 ⇒ 原样上抛 —— 读不出怎么说归读者(视图读响亮失败;队列只缺那一行的键并计数),本函数不替任何一面把故障说成「不是子代」。
38
- */
39
- export declare function readSessionAgent(store: PlacementReadingSessionStore | undefined, sessionId: string): Promise<SessionAgent | undefined>;
40
31
  //# sourceMappingURL=session-agent.d.ts.map
@@ -7,10 +7,4 @@ export function sessionAgentOf(placement) {
7
7
  ...(placement.rootSessionId !== undefined ? { rootSessionId: placement.rootSessionId } : {}),
8
8
  };
9
9
  }
10
- export async function readSessionAgent(store, sessionId) {
11
- const placementOf = store?.placementOf;
12
- if (placementOf === undefined)
13
- return undefined;
14
- return sessionAgentOf(await placementOf.call(store, sessionId));
15
- }
16
10
  //# sourceMappingURL=session-agent.js.map