@sema-agent/server 7.124.0 → 7.125.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 (152) hide show
  1. package/MIGRATION.md +138 -0
  2. package/USAGE.md +46 -8
  3. package/dist/audit.d.ts +27 -5
  4. package/dist/audit.js +42 -8
  5. package/dist/boot/execution-env.d.ts +33 -1
  6. package/dist/boot/execution-env.js +31 -2
  7. package/dist/boot/resolve-spec.js +4 -3
  8. package/dist/boot/session-faces.d.ts +14 -1
  9. package/dist/boot/session-faces.js +6 -5
  10. package/dist/boot/stage-02-web-admin-pull.js +5 -1
  11. package/dist/boot/stage-05-execution-env.js +6 -2
  12. package/dist/boot/stage-09-leader.d.ts +1 -1
  13. package/dist/boot/stage-10-http-server.d.ts +1 -1
  14. package/dist/boot/web-admin.js +130 -140
  15. package/dist/capabilities/center-wiring.d.ts +2 -1
  16. package/dist/capabilities/composite-members.d.ts +1 -0
  17. package/dist/capabilities/composite-members.js +1 -0
  18. package/dist/capability-facts.d.ts +28 -3
  19. package/dist/capability-facts.js +20 -0
  20. package/dist/config-catalog.js +4 -1
  21. package/dist/config-d-migration.js +2 -1
  22. package/dist/config-lkg.d.ts +4 -1
  23. package/dist/config-lkg.js +16 -12
  24. package/dist/config-provider.d.ts +4 -1
  25. package/dist/config-provider.js +1 -0
  26. package/dist/config-types.d.ts +10 -3
  27. package/dist/config.js +44 -14
  28. package/dist/env-facts.d.ts +2 -2
  29. package/dist/env-facts.js +3 -2
  30. package/dist/fleet/fleet-bus.d.ts +24 -12
  31. package/dist/fleet/fleet-bus.js +4 -1
  32. package/dist/fleet/workspace-snapshot-residual.d.ts +10 -0
  33. package/dist/fleet/workspace-snapshot-residual.js +35 -0
  34. package/dist/hooks/hook-runner.d.ts +15 -27
  35. package/dist/hooks/hook-runner.js +19 -59
  36. package/dist/http/active-run-conflict.d.ts +3 -0
  37. package/dist/http/active-run-conflict.js +3 -0
  38. package/dist/http/admission.js +7 -7
  39. package/dist/http/approval-sweeps.js +2 -4
  40. package/dist/http/device-placement.js +1 -0
  41. package/dist/http/dispatch.js +1 -1
  42. package/dist/http/durable-run-leg.d.ts +20 -6
  43. package/dist/http/durable-run-leg.js +4 -4
  44. package/dist/http/idempotency.d.ts +83 -38
  45. package/dist/http/idempotency.js +38 -13
  46. package/dist/http/request-guards.d.ts +18 -0
  47. package/dist/http/request-guards.js +12 -3
  48. package/dist/http/resume-legs.js +7 -1
  49. package/dist/http/route-ctx.d.ts +3 -3
  50. package/dist/http/route-ctx.js +1 -1
  51. package/dist/http/routes/a2a-serve.js +27 -11
  52. package/dist/http/routes/capabilities.js +1 -0
  53. package/dist/http/routes/fleet.js +2 -0
  54. package/dist/http/routes/images.js +2 -2
  55. package/dist/http/routes/leader.d.ts +4 -0
  56. package/dist/http/routes/leader.js +13 -3
  57. package/dist/http/routes/observability.js +2 -0
  58. package/dist/http/routes/runs.d.ts +6 -2
  59. package/dist/http/routes/runs.js +71 -101
  60. package/dist/http/routes/session-sync.d.ts +3 -0
  61. package/dist/http/routes/session-sync.js +30 -1
  62. package/dist/http/routes/sessions-list.d.ts +11 -0
  63. package/dist/http/routes/sessions-list.js +6 -6
  64. package/dist/http/routes/sessions.d.ts +6 -1
  65. package/dist/http/routes/sessions.js +41 -10
  66. package/dist/http/routes/tasks.js +63 -21
  67. package/dist/http/run-read-face.d.ts +26 -0
  68. package/dist/http/run-read-face.js +67 -0
  69. package/dist/http/server.d.ts +4 -4
  70. package/dist/http/server.js +1 -2
  71. package/dist/http/submit-replay.d.ts +40 -0
  72. package/dist/http/submit-replay.js +22 -0
  73. package/dist/leader/endpoint.d.ts +10 -5
  74. package/dist/leader/endpoint.js +27 -5
  75. package/dist/observability/cost-quota.d.ts +51 -7
  76. package/dist/observability/cost-quota.js +35 -17
  77. package/dist/observability/fail-open.d.ts +1 -1
  78. package/dist/observability/fail-open.js +1 -1
  79. package/dist/observability/run-terminal.d.ts +5 -9
  80. package/dist/observability/run-terminal.js +2 -7
  81. package/dist/plugins/approval-ask-store-sql.js +1 -1
  82. package/dist/plugins/caching-session-store.d.ts +7 -2
  83. package/dist/plugins/caching-session-store.js +6 -0
  84. package/dist/plugins/device-store-sql.js +1 -1
  85. package/dist/plugins/ensure-index.js +22 -0
  86. package/dist/plugins/file-run-store.d.ts +24 -14
  87. package/dist/plugins/file-run-store.js +52 -12
  88. package/dist/plugins/leader-run-store-sql.d.ts +1 -11
  89. package/dist/plugins/leader-run-store-sql.js +1 -5
  90. package/dist/plugins/local-session-store.d.ts +35 -6
  91. package/dist/plugins/local-session-store.js +87 -18
  92. package/dist/plugins/memory-run-store.d.ts +13 -12
  93. package/dist/plugins/memory-run-store.js +25 -0
  94. package/dist/plugins/one-time-migrations.js +1 -1
  95. package/dist/plugins/pg-cost-quota.d.ts +1 -1
  96. package/dist/plugins/pg-cost-quota.js +2 -5
  97. package/dist/plugins/pg-pool.js +3 -0
  98. package/dist/plugins/pg-session-storage.d.ts +11 -1
  99. package/dist/plugins/pg-session-storage.js +44 -8
  100. package/dist/plugins/remote-env-k8s.d.ts +127 -12
  101. package/dist/plugins/remote-env-k8s.js +270 -27
  102. package/dist/plugins/run-store-sql.d.ts +26 -12
  103. package/dist/plugins/run-store-sql.js +50 -25
  104. package/dist/plugins/schema-consistency.d.ts +1 -13
  105. package/dist/plugins/schema-consistency.js +1 -41
  106. package/dist/plugins/schema-indexes.js +1 -0
  107. package/dist/plugins/schema-live-columns.d.ts +20 -0
  108. package/dist/plugins/schema-live-columns.js +42 -0
  109. package/dist/plugins/store-contracts.d.ts +40 -2
  110. package/dist/plugins/tidb-cost-quota.d.ts +1 -1
  111. package/dist/plugins/tidb-cost-quota.js +2 -5
  112. package/dist/plugins/tidb-pool.js +14 -3
  113. package/dist/plugins/tidb-session-store.d.ts +22 -2
  114. package/dist/plugins/tidb-session-store.js +33 -6
  115. package/dist/plugins/write-behind-counter.d.ts +12 -5
  116. package/dist/plugins/write-behind-counter.js +14 -7
  117. package/dist/prompts-domain-validate.js +3 -2
  118. package/dist/rules-consent.d.ts +2 -2
  119. package/dist/rules-consent.js +2 -2
  120. package/dist/run-local.js +31 -23
  121. package/dist/runs.d.ts +9 -0
  122. package/dist/runs.js +8 -0
  123. package/dist/runtime-governance.d.ts +3 -3
  124. package/dist/runtime-governance.js +2 -4
  125. package/dist/security.d.ts +19 -2
  126. package/dist/session-row.d.ts +53 -0
  127. package/dist/session-row.js +47 -0
  128. package/dist/session-sync.d.ts +6 -0
  129. package/dist/session-sync.js +5 -1
  130. package/dist/session-titler.d.ts +9 -5
  131. package/dist/session-titler.js +3 -1
  132. package/dist/spec-fields.js +2 -2
  133. package/dist/task-cwd.d.ts +0 -20
  134. package/dist/task-cwd.js +2 -10
  135. package/dist/tool-approval.d.ts +5 -7
  136. package/dist/trace/redact.d.ts +23 -50
  137. package/dist/trace/redact.js +58 -18
  138. package/dist/trace/self-minted-wire.d.ts +25 -13
  139. package/dist/trace/self-minted-wire.js +9 -4
  140. package/dist/web-admin-client/apply-effective.d.ts +10 -12
  141. package/dist/web-admin-client/apply-effective.js +2 -3
  142. package/dist/web-admin-client/candidate-gate.d.ts +69 -0
  143. package/dist/web-admin-client/candidate-gate.js +27 -0
  144. package/dist/web-admin-client/http-client.d.ts +2 -1
  145. package/dist/web-admin-client/http-client.js +2 -1
  146. package/dist/web-admin-client/pull-state.d.ts +6 -4
  147. package/dist/web-admin-client/read-warnings.d.ts +29 -5
  148. package/dist/web-admin-client/read-warnings.js +24 -4
  149. package/dist/web-admin-client/stage-limits.js +4 -1
  150. package/dist/workspace-space.d.ts +41 -0
  151. package/dist/workspace-space.js +5 -0
  152. package/package.json +3 -3
@@ -3,6 +3,7 @@ import { resolveStreamApprovalGate } from "./tool-approval.js";
3
3
  import { selfOrchestrationDenial } from "./task-workflow.js";
4
4
  import { centerEntitlementSourceWired } from "./runtime-caps-resolver.js";
5
5
  import { overwriteWipeIncapable } from "./session-sync.js";
6
+ import { WORKSPACE_PERSIST_DEFAULT, WORKSPACE_SNAPSHOT_KEEP_DEFAULT } from "./workspace-space.js";
6
7
  export const CAPABILITY_FACT_WHY = {
7
8
  run_store_absent: "no durable run store is wired (DB_BACKEND=mysql|pg|local)",
8
9
  subagent_registry_absent: "the sub-agent steer registry is not wired",
@@ -15,6 +16,7 @@ export const CAPABILITY_FACT_WHY = {
15
16
  session_leaf_face_absent: "the session store has no leaf-read face (sessionStorage.getLeafId)",
16
17
  session_lister_absent: "no session enumerator is wired (session store or run store listSessions)",
17
18
  session_fork_face_absent: "the session store has no fork face",
19
+ session_row_faces_absent: "the session store has no session-row faces (sessionFaceOf / sessionSummaryOf / patchSessionMeta)",
18
20
  session_purge_absent: "no session purge coordinator is wired",
19
21
  session_policy_store_absent: "no session policy store is wired",
20
22
  store_backend_absent: "no store backend is wired (DB_BACKEND=memory, multi-tenant bare boot, or half-configured SQL)",
@@ -58,6 +60,8 @@ export const CAPABILITY_FACT_WHY = {
58
60
  scheduler_disabled: "the self-wake scheduler is off (SCHEDULER_ENABLED)",
59
61
  multi_tenant_deployment: "this is a multi-tenant deployment (REQUIRE_PRINCIPAL=true)",
60
62
  not_host_lane: "the execution lane is not the single-user host lane (REMOTE_EXEC=host)",
63
+ not_k8s_lane: "the execution lane is not the k8s pod sandbox (REMOTE_EXEC=k8s)",
64
+ workspace_snapshot_store_absent: "no workspace snapshot store is configured (MINIO_ENDPOINT / MINIO_ACCESS_KEY / MINIO_SECRET_KEY)",
61
65
  };
62
66
  const no = (why) => ({ available: false, why });
63
67
  const yes = (w) => ({ available: true, ...w });
@@ -123,6 +127,12 @@ export function sessionFork(d) {
123
127
  return no("session_owner_face_absent");
124
128
  return yes({ sessionStorage: s });
125
129
  }
130
+ export function sessionPatch(d) {
131
+ const s = d.sessionStorage;
132
+ if (s?.sessionFaceOf === undefined || s.patchSessionMeta === undefined)
133
+ return no("session_row_faces_absent");
134
+ return yes({ sessionStorage: s });
135
+ }
126
136
  export function sessionDelete(d) {
127
137
  if (d.purgeSession === undefined)
128
138
  return no("session_purge_absent");
@@ -151,6 +161,8 @@ export function sessionSync(d) {
151
161
  return no("session_export_absent");
152
162
  if (s.beginImportStaging === undefined)
153
163
  return no("session_import_staging_absent");
164
+ if (s.sessionFaceOf === undefined || s.sessionSummaryOf === undefined || s.patchSessionMeta === undefined)
165
+ return no("session_row_faces_absent");
154
166
  if (overwriteWipeIncapable(d.sessionPolicyStore))
155
167
  return no("policy_purge_absent");
156
168
  return yes({ backend: d.backend, sessionStorage: s, historyStore: d.fileHistoryStore });
@@ -342,6 +354,14 @@ export function workflowsJournal(d) {
342
354
  const journal = workflowJournal(d);
343
355
  return journal.available ? yes({ store: runs.store, journal: journal.journal }) : journal;
344
356
  }
357
+ export function workspaceSpace(c) {
358
+ const rx = c.remoteExec;
359
+ if (rx?.provider !== "k8s")
360
+ return no("not_k8s_lane");
361
+ if (!rx.s3Snapshot)
362
+ return no("workspace_snapshot_store_absent");
363
+ return yes({ policy: { persist: rx.workspacePersist ?? WORKSPACE_PERSIST_DEFAULT, keep: rx.workspaceSnapshotKeep ?? WORKSPACE_SNAPSHOT_KEEP_DEFAULT } });
364
+ }
345
365
  export function hostScheduler(c) {
346
366
  if (c.schedulerEnabled !== true)
347
367
  return no("scheduler_disabled");
@@ -2,6 +2,7 @@ import { CROSS_SESSION_INBOUND_SETTINGS, CROSS_SESSION_DIALOG_EXPIRY_VALUES } fr
2
2
  import { GLOBAL_SCOPE } from "@sema-agent/settings-schema/api/scopes";
3
3
  import { WEB_ADMIN_ENV, DOMAIN_SCHEMAS, buildRetiredEnvWire } from "@sema-agent/settings-schema";
4
4
  import { CENTER_CREDENTIAL_RENEW_LEAD_DEFAULT_MS, CENTER_CREDENTIAL_RENEW_LEAD_MAX_MS, CENTER_CREDENTIAL_RENEW_LEAD_MIN_MS } from "./center-credential-renewer.js";
5
+ import { WORKSPACE_PERSIST_DEFAULT, WORKSPACE_PERSIST_WORDS, WORKSPACE_SNAPSHOT_KEEP_DEFAULT, WORKSPACE_SNAPSHOT_KEEP_RANGE } from "./workspace-space.js";
5
6
  import { THINKING_FORMATS } from "./model-compat.js";
6
7
  import { AUTO_MODE_CLASSIFIER_TIMEOUT_BOUNDS, CENTER_BASE_URL_RULE, CONFIG_PROVIDER_WORDS, DRAIN_SECOND_SIGNAL_MIN_GAP_BOUNDS, DRAIN_SECOND_SIGNAL_MIN_GAP_DEFAULT_MS, FIRST_BYTE_TIMEOUT_BOUNDS, FLEET_LEASE_OUTAGE_MAX_DEFAULT_MS, RESILIENCE_DEFAULTS, TASK_WRITE_FACE_ESCALATIONS } from "./config-types.js";
7
8
  import { webSearchProviderFromEnv, WEB_SEARCH_PROVIDERS } from "./plugins/web-search.js";
@@ -193,7 +194,7 @@ export const CONFIG_CATALOG = [
193
194
  r("K8S_NAMESPACE", "orchestration", "string", "沙箱 pod namespace"),
194
195
  r("K8S_RUNTIME_CLASS", "orchestration", "string", "沙箱 pod runtimeClass(Kata 等)"),
195
196
  r("K8S_SANDBOX_IMAGE", "orchestration", "string", "k8s 车道沙箱镜像(REMOTE_EXEC=k8s 必需件)"),
196
- r("K8S_TIMEOUT_MS", "orchestration", "number", "k8s 单命令超时(ms)"),
197
+ r("K8S_TIMEOUT_MS", "orchestration", "number", "k8s 沙箱 pod 寿命(activeDeadlineSeconds,ms;整只 pod 的泄漏护栏,后台任务超时也被它钳;各条命令的超时见工具超时)"),
197
198
  r("K8S_TOKEN", "orchestration", "secret", "k8s API token(缺席走 in-cluster SA)", { danger: SEC }),
198
199
  r("KUBERNETES_SERVICE_HOST", "orchestration", "string", "平台注入的 in-cluster k8s API host(K8S_API_URL 缺席时用)"),
199
200
  r("KUBERNETES_SERVICE_PORT", "orchestration", "string", "平台注入的 in-cluster k8s API 端口(缺省 443)"),
@@ -479,7 +480,9 @@ export const CONFIG_CATALOG = [
479
480
  r("WORKFLOW_SIZE_GUIDELINE", "orchestration", "enum", "Workflow 工具卡的规模建议(advisory;typo 拒启)", { enumValues: ["small", "medium", "large", "unrestricted"], configKey: "workflowSizeGuideline" }),
480
481
  r("WORKFLOW_TOTAL_TIMEOUT_MS", "orchestration", "number", "workflow 脚本总时限(ms;0/负不是禁用 —— 无禁用臂)", { staticDefault: "3600000" }),
481
482
  r("WORKSPACE_FILE_MAX_BYTES", "limitsHttp", "number", "已随 workspace 浏览面退役(S-15):值照常校验/落值,应用于空(重启也不生效)", { envTiming: "inert", configKey: "workspaceFileMaxBytes", center: cLimits("workspaceFileMaxBytes", { timing: "inert" }) }),
483
+ r("WORKSPACE_PERSIST", "orchestration", "enum", "k8s 工作区跨轮保留(opt-in):space = 每条 run 结束把 /workspace 快照进本会话住客的空间、下一条 run 开始恢复最近一份(开着时有已知残窗:提交标记晚于拆环境上界落地 ⇒ 上一条的改动可能被覆盖,见 USAGE §9.3b);off = 每轮新建(缺省)。设了 space 却没配快照存储(MINIO_* 三键)⇒ 不生效,起服一行 workspace_persist_unavailable 点名缺键;前提:会话店读得出会话住客(local / SQL 会话店),内存会话店设 space(且配了快照存储)⇒ 拒启;词表外拒启", { enumValues: WORKSPACE_PERSIST_WORDS, staticDefault: WORKSPACE_PERSIST_DEFAULT, danger: DATA }),
482
484
  r("WORKSPACE_SNAPSHOT_BUCKET", "orchestration", "string", "k8s 工作区快照桶", { derivedDefaultNote: "缺席 ⇒ workspaces" }),
485
+ r("WORKSPACE_SNAPSHOT_KEEP", "orchestration", "number", `k8s 工作区每个空间(会话住客)保留的快照份数(整数 [${WORKSPACE_SNAPSHOT_KEEP_RANGE.min}, ${WORKSPACE_SNAPSHOT_KEEP_RANGE.max}];写新快照后删最老的;域外拒启)`, { staticDefault: String(WORKSPACE_SNAPSHOT_KEEP_DEFAULT), danger: DATA }),
483
486
  r("WORKSPACE_SNAPSHOT_PREFIX", "orchestration", "string", "k8s 工作区快照 key 前缀"),
484
487
  r("WORKSPACE_SNAPSHOT_TTL_SEC", "orchestration", "number", "k8s 工作区快照 presign TTL(秒)"),
485
488
  r("WORKTREE_ALLOWED_ROOTS", "orchestration", "csv", "worktree 隔离纵深白名单(缺省 = [repoRoot])", { danger: SEC }),
@@ -2,7 +2,8 @@ import { stat } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { RETIRED_DOMAINS, formatConfigPath, isRetiredDomainName } from "@sema-agent/settings-schema";
4
4
  import { PORTABLE_DOMAINS, migrateConfigDirectory, nodeConfigDirectoryIo } from "@sema-agent/settings-schema/node";
5
- const READ_BACK_DOCUMENTS = [...PORTABLE_DOMAINS, ...Object.keys(RETIRED_DOMAINS).filter(isRetiredDomainName)];
5
+ import { REMOTE_EXEC_DOCUMENT } from "@sema-agent/settings-schema/remote-exec";
6
+ const READ_BACK_DOCUMENTS = [...PORTABLE_DOMAINS, ...Object.keys(RETIRED_DOMAINS).filter(isRetiredDomainName), REMOTE_EXEC_DOCUMENT];
6
7
  const errnoOf = (err) => (err instanceof Error && "code" in err && typeof err.code === "string" ? err.code : undefined);
7
8
  export async function migrateLocalConfigDirectory(root) {
8
9
  const dir = join(root, "config.d");
@@ -1,4 +1,5 @@
1
1
  import { type MigrationReceipt } from "@sema-agent/settings-schema/one-time-migrations";
2
+ import type { PromptsDomainFaces } from "./prompts-domain-validate.js";
2
3
  /** 落盘信封。`effective`/`etag` 逐字=当时的 fetchEffective 返回(JSON verbatim 面)。`formatVersion` 的语义见头注(4 = 载荷是 settings-schema 11 的形)。 */
3
4
  export interface PersistedLkg {
4
5
  formatVersion: 4;
@@ -59,9 +60,11 @@ export interface LkgReadMigration {
59
60
  * 文件缺席/JSON 坏/信封形不对/worker 或凭证身份不匹配(防同名文件跨 worker / 跨账号串包;`CONFIG_LKG_PATH`
60
61
  * 单一路径时这是唯一的一道)/载荷有迁不动的旧形({@link migrateLkgPayload})/prompts 面复判失败。
61
62
  * `migrated`:读回时做过一次性迁移(信封 2 → 3 或载荷的 settings-schema 迁移)—— 调用方响亮打一行,并把交回的 4 写回原位。
63
+ * `prompts`:候选判官对这份载荷 prompts 面交出的已判形值(`undefined` = 载荷不带 prompts 键)—— 调用方采纳 prompts 直接用它。
62
64
  */
63
65
  export declare function loadLkg(path: string, worker: string | undefined, identity: string): Promise<{
64
66
  lkg: PersistedLkg;
67
+ prompts: PromptsDomainFaces | undefined;
65
68
  migrated?: LkgReadMigration;
66
69
  } | {
67
70
  lkg?: undefined;
@@ -104,7 +107,7 @@ export type LegacyLkgMigration = {
104
107
  * 2. 旧文件没有(ENOENT / ENOTDIR)或不是 formatVersion 1 信封 ⇒ `none`。旧默认路径上却读不了 / 不是 JSON ⇒
105
108
  * `not-migrated`(响亮,不当它不存在);`CONFIG_LKG_PATH` 同路径时这两形由 loadLkg 自己报(不重复打一行)。
106
109
  * 3. v1 信封只记 worker:唯一推得出身份的形是拉取令牌(身份 = worker,`pullTokenIdentity`)。此刻身份正是它、worker
107
- * 对得上、载荷过 {@link migrateLkgPayload}(迁成 11 的形;迁不动 ⇒ 响亮拒、不写箱)与 {@link lkgPayloadProblem} ⇒ **先写后退役**(修复验证 N9):先按新格式把它写进 `binPath`(原子写,
110
+ * 对得上、载荷过 {@link migrateLkgPayload}(迁成 11 的形;迁不动 ⇒ 响亮拒、不写箱)与 {@link lkgPayloadJudged} ⇒ **先写后退役**(修复验证 N9):先按新格式把它写进 `binPath`(原子写,
108
111
  * 0600),写成了才把旧文件改名 `<legacyPath>.migrated-v1`。写失败 ⇒ `not-migrated`,旧文件原样留在原位,下一次
109
112
  * 照常可迁;写成之后改名失败 ⇒ 仍是 `migrated`(`retireError` 响一行),箱已在 ⇒ 下一次按判序 1 不再看旧文件。
110
113
  * 同路径(`CONFIG_LKG_PATH`)时新箱的写入会原子替换旧文件本身,所以旧字节在写之前留一份 `.migrated-v1` 副本
@@ -3,7 +3,7 @@ import { migrateEffectiveSnapshot } from "@sema-agent/settings-schema/one-time-m
3
3
  import { formatConfigPath } from "@sema-agent/settings-schema";
4
4
  import { join, dirname, resolve } from "node:path";
5
5
  import { randomBytes } from "node:crypto";
6
- import { validatePromptsDomain } from "./prompts-domain-validate.js";
6
+ import { judgeConfigCandidate } from "./web-admin-client/candidate-gate.js";
7
7
  import { centerStateBin, pullTokenIdentity } from "./center-credential.js";
8
8
  import { configRootOf } from "./sema-config-dir.js";
9
9
  export function defaultSkillCacheDir(config) {
@@ -74,12 +74,12 @@ export async function loadLkg(path, worker, identity) {
74
74
  const payload = migrateLkgPayload(envelope.effective);
75
75
  if ("refused" in payload)
76
76
  return { reason: payload.refused };
77
- const problem = lkgPayloadProblem(payload.effective, envelope.etag);
78
- if (problem !== undefined)
79
- return { reason: problem };
77
+ const judged = lkgPayloadJudged(payload.effective, envelope.etag);
78
+ if ("problem" in judged)
79
+ return { reason: judged.problem };
80
80
  const lkg = { ...envelope, formatVersion: 4, effective: payload.effective };
81
81
  const migrated = formatVersion !== 4 || payload.receipts.length > 0;
82
- return migrated ? { lkg, migrated: { fromFormat: formatVersion, ...(note !== undefined ? { note } : {}), receipts: payload.receipts } } : { lkg };
82
+ return migrated ? { lkg, prompts: judged.prompts, migrated: { fromFormat: formatVersion, ...(note !== undefined ? { note } : {}), receipts: payload.receipts } } : { lkg, prompts: judged.prompts };
83
83
  }
84
84
  export function migrateLkgPayload(effective) {
85
85
  const m = migrateEffectiveSnapshot(effective);
@@ -122,7 +122,14 @@ function migrateV2Lkg(lkg) {
122
122
  (hasRetired ? "; the retired carrier models.droppedIdentities was moved to models.droppedCatalogEntries" : ""),
123
123
  };
124
124
  }
125
- function lkgPayloadProblem(effective, etag) {
125
+ function lkgPayloadJudged(effective, etag) {
126
+ const problem = lkgPayloadShapeProblem(effective, etag);
127
+ if (problem !== undefined)
128
+ return { problem };
129
+ const verdict = judgeConfigCandidate({ effective: effective });
130
+ return verdict.verdict === "clean" ? { prompts: verdict.prompts } : { problem: "prompts face fails re-validation" };
131
+ }
132
+ function lkgPayloadShapeProblem(effective, etag) {
126
133
  if (effective === null || typeof effective !== "object")
127
134
  return "bad effective";
128
135
  if (etag !== undefined && typeof etag !== "string")
@@ -142,9 +149,6 @@ function lkgPayloadProblem(effective, etag) {
142
149
  return `bad effective.${domain}.${arrayKey}`;
143
150
  }
144
151
  }
145
- const promptsRaw = eff.prompts;
146
- if (promptsRaw !== undefined && !validatePromptsDomain(promptsRaw).ok)
147
- return "prompts face fails re-validation";
148
152
  return undefined;
149
153
  }
150
154
  export function legacyV1LkgPath(config, worker) {
@@ -197,9 +201,9 @@ export async function migrateV1Lkg(o) {
197
201
  const payload = migrateLkgPayload(v1.effective);
198
202
  if ("refused" in payload)
199
203
  return refuse(`its payload cannot be migrated to the current settings form (${payload.refused})`);
200
- const problem = lkgPayloadProblem(payload.effective, v1.etag);
201
- if (problem !== undefined)
202
- return refuse(`its payload is unusable (${problem})`);
204
+ const judged = lkgPayloadJudged(payload.effective, v1.etag);
205
+ if ("problem" in judged)
206
+ return refuse(`its payload is unusable (${judged.problem})`);
203
207
  const retiredTo = `${o.legacyPath}.migrated-v1`;
204
208
  let retireError;
205
209
  if (sameFile) {
@@ -1,4 +1,4 @@
1
- import { type DroppedConfigEntry } from "./web-admin-client/read-warnings.js";
1
+ import { type DroppedConfigEntry, type UnreadConfigFile } from "./web-admin-client/read-warnings.js";
2
2
  import { FileConfigStore } from "@sema-agent/settings-schema/node";
3
3
  import { type EffectiveConfig as AgentConfigEffective, type EffectiveReadWarning } from "@sema-agent/settings-schema";
4
4
  import { fetchEffective as remoteFetchEffective, fetchSkillContent as remoteFetchSkillContent, type EffectiveConfig } from "./web-admin-client/facade.js";
@@ -23,6 +23,8 @@ import type { ConfigProviderWord } from "./config-types.js";
23
23
  /** `droppedEntries`(S-704 ②):schema 逐条丢掉的条目(坐标 + 字段诊断,已脱敏;S-1295 起每行带闭集词 `reasonCode`)—— **不**进候选门,
24
24
  * 调用方逐条响亮点名、其余照用;缺席 = 这次读零逐条丢弃(与 `domainErrors` 同一种缺席即无的形)。其中的目录条目同一次折叠另记进
25
25
  * `effective.models.droppedCatalogEntries`(`GET /v1/models.droppedEntries` 的源,与 `models` 同世代)。 */
26
+ /** `unreadFiles`:`config.d` 里读腿根本不读的文件(不是域 / 不是便携域)—— **不**进候选门(不是读的域判形失败),调用方一份一行
27
+ * `config_unread_file` 点名、其余照用;缺席 = 没有(本地读腿才会有)。 */
26
28
  export type FetchEffectiveResult = {
27
29
  effective: EffectiveConfig;
28
30
  etag?: string;
@@ -31,6 +33,7 @@ export type FetchEffectiveResult = {
31
33
  error: string;
32
34
  }>;
33
35
  droppedEntries?: DroppedConfigEntry[];
36
+ unreadFiles?: UnreadConfigFile[];
34
37
  readWarnings: readonly EffectiveReadWarning[];
35
38
  credentialIdentity?: string;
36
39
  } | null;
@@ -107,6 +107,7 @@ export class LocalConfigProvider {
107
107
  readWarnings,
108
108
  ...(errors.length > 0 ? { domainErrors: errors } : {}),
109
109
  ...(droppedEntries.length > 0 ? { droppedEntries } : {}),
110
+ ...(folded.unreadFiles.length > 0 ? { unreadFiles: folded.unreadFiles } : {}),
110
111
  };
111
112
  }
112
113
  async fetchSkillContent(contentHash) {
@@ -907,6 +907,12 @@ export interface ServiceConfigFlat {
907
907
  keyPrefix?: string;
908
908
  presignTtlSec?: number;
909
909
  };
910
+ /** Workspace persistence by space key (`WORKSPACE_PERSIST`) and snapshots kept per space key (`WORKSPACE_SNAPSHOT_KEEP`).
911
+ * The loader always mints both on this arm; optional only so hand-built configs may omit them. Effective policy =
912
+ * the capability fact `workspaceSpace` (`capability-facts.ts`, the one reading: defaults applied there, absent store ⇒
913
+ * unavailable). */
914
+ workspacePersist?: import("./workspace-space.js").WorkspacePersist;
915
+ workspaceSnapshotKeep?: number;
910
916
  } | {
911
917
  provider: "ssh";
912
918
  host: string;
@@ -1572,7 +1578,8 @@ export interface ServiceConfigFlat {
1572
1578
  * per-region via the registry effective-config (shell B3) later. */
1573
1579
  sandboxPkgSource?: string;
1574
1580
  /** Auto-generate a session title at first submit (one cheap-model call per
1575
- * session, write-once; the user rename layer is BFF-side). Default ON; `SESSION_AUTO_TITLE=false` opts out. */
1581
+ * session; fills a NULL title only — a title the user set via `PATCH /v1/sessions/:id` is never overwritten).
1582
+ * Default ON; `SESSION_AUTO_TITLE=false` opts out. */
1576
1583
  sessionAutoTitle: boolean;
1577
1584
  /** S-178:上面那一位的来源(同一张四词表)。本键**不进 posture**(缺省恒 true,两形同值)——
1578
1585
  * 报来源是为了让运维分辨「这台机器显式关了自动标题」与「它本来就开着」;壳此前注 `false` 的那条
@@ -2033,10 +2040,10 @@ export interface ServiceConfigFlat {
2033
2040
  * single task from burning the shared cloud key. Enforced by core (failed + errorCode budget.*). */
2034
2041
  maxTaskCostUsd: number;
2035
2042
  maxTaskTokens: number;
2036
- /** Per-principal CUMULATIVE cost ceiling (USD) within a rolling window — blocks a tenant's NEXT task
2043
+ /** Per-principal CUMULATIVE cost ceiling (USD) within a clock-aligned fixed window — blocks a tenant's NEXT task
2037
2044
  * once their accumulated spend crosses it. 0/unset = no per-principal quota. */
2038
2045
  maxPrincipalCostUsd: number;
2039
- /** Rolling window (seconds) for the per-principal cost quota. Default 86400 (1 day). */
2046
+ /** Clock-aligned fixed window (seconds, in [1, 1e10]; bucket = floor(now / window)) for the per-principal cost quota. Default 86400 (1 day). */
2040
2047
  costQuotaWindowSec: number;
2041
2048
  /** Degradation to a cheaper model `to` (a catalog name) instead of hard-failing (core 1.39/1.40):
2042
2049
  * - **prospective** (1.40, `atCostFraction`): near-budget — at `atCostFraction × maxCostUsd` switch at
package/dist/config.js CHANGED
@@ -20,11 +20,13 @@ import { normalizeSshFingerprint } from "./ssh-host-key.js";
20
20
  import { buildReadFacePostureNotice, readFaceForPosture } from "./read-face-posture.js";
21
21
  import { THINKING_LEVELS, THINKING_FORMATS, buildModelCompat, isThinkingFormat, isThinkingTier } from "./model-compat.js";
22
22
  import {} from "./posture-source.js";
23
+ import { COST_QUOTA_WINDOW_SEC_RANGE, costQuotaWindowSecInRange } from "./observability/cost-quota.js";
23
24
  import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
24
25
  import { describeUnmatchableRuleName, ruleEntryToolName, unmatchableRuleNameOf } from "./namespaced-rule-name.js";
25
26
  import { bakeOperatorPrincipalsRefusal, bakeRunnerCredentialRefusal, centerIdentityRefusal, halfOpenDirectDoorRefusal, leaderDurableStoreRefusal, operatorPrincipalsRequiredRefusal } from "./config-invariants.js";
26
27
  import { declaredSemaConfigDir } from "./sema-config-dir.js";
27
28
  import { lkgPathOverrideOf } from "./config-lkg.js";
29
+ import { WORKSPACE_PERSIST_DEFAULT, WORKSPACE_PERSIST_WORDS, WORKSPACE_SNAPSHOT_KEEP_DEFAULT, WORKSPACE_SNAPSHOT_KEEP_RANGE } from "./workspace-space.js";
28
30
  import { REMOTE_EXEC_PROVIDER_WORDS, executionLaneOf, executionLaneRequiredRefusal, isSingleMachineDefaultLane, laneRequiredEnv } from "./execution-lane-caps.js";
29
31
  import { MEMORY_ENGINE_BACKENDS, SESSION_BACKENDS, STORE_BACKENDS } from "@sema-agent/settings-schema/execution-lanes";
30
32
  import { AUTONOMY_MODES } from "./runtime-governance.js";
@@ -1853,19 +1855,7 @@ function parseOrchestrationDomain(ctx) {
1853
1855
  ...((n) => (n !== undefined ? { timeoutMs: n } : {}))(optNumEnvStrict("K8S_TIMEOUT_MS")),
1854
1856
  ...(process.env.K8S_MEMORY ? { memory: process.env.K8S_MEMORY } : {}),
1855
1857
  ...(process.env.K8S_CPU ? { cpu: process.env.K8S_CPU } : {}),
1856
- ...(process.env.MINIO_ENDPOINT && process.env.MINIO_ACCESS_KEY && process.env.MINIO_SECRET_KEY
1857
- ? {
1858
- s3Snapshot: {
1859
- endpoint: process.env.MINIO_ENDPOINT,
1860
- bucket: process.env.WORKSPACE_SNAPSHOT_BUCKET ?? "workspaces",
1861
- accessKey: process.env.MINIO_ACCESS_KEY,
1862
- secretKey: process.env.MINIO_SECRET_KEY,
1863
- ...(process.env.MINIO_REGION ? { region: process.env.MINIO_REGION } : {}),
1864
- ...(process.env.WORKSPACE_SNAPSHOT_PREFIX ? { keyPrefix: process.env.WORKSPACE_SNAPSHOT_PREFIX } : {}),
1865
- ...((n) => (n !== undefined ? { presignTtlSec: n } : {}))(optNumEnvStrict("WORKSPACE_SNAPSHOT_TTL_SEC")),
1866
- },
1867
- }
1868
- : {}),
1858
+ ...k8sWorkspaceStore(),
1869
1859
  }
1870
1860
  :
1871
1861
  process.env.REMOTE_EXEC === "ssh" && process.env.SSH_HOST && process.env.SSH_USER && process.env.SSH_PRIVATE_KEY
@@ -2039,7 +2029,14 @@ function parseLimitsHttpDomain() {
2039
2029
  maxTaskCostUsd: numEnv("MAX_TASK_COST_USD", "0"),
2040
2030
  maxTaskTokens: numEnv("MAX_TASK_TOKENS", "0"),
2041
2031
  maxPrincipalCostUsd: numEnv("MAX_PRINCIPAL_COST_USD", "0"),
2042
- costQuotaWindowSec: numEnv("COST_QUOTA_WINDOW_SEC", "86400"),
2032
+ costQuotaWindowSec: (() => {
2033
+ const sec = numEnv("COST_QUOTA_WINDOW_SEC", "86400");
2034
+ if (!costQuotaWindowSecInRange(sec)) {
2035
+ const { min, max } = COST_QUOTA_WINDOW_SEC_RANGE;
2036
+ throw new Error(`env COST_QUOTA_WINDOW_SEC=${sec} must be in [${min}, ${max}] seconds — it is the length of the clock-aligned per-principal cost-quota window, and its end must be a representable instant; there is no "off" here: MAX_PRINCIPAL_COST_USD=0 (or unset) turns the quota off (the web-admin refuses the same value)`);
2037
+ }
2038
+ return sec;
2039
+ })(),
2043
2040
  reapIntervalSec: numEnv("REAP_INTERVAL_SEC", "60"),
2044
2041
  runStaleSec: numEnv("REAP_RUN_STALE_SEC", "120"),
2045
2042
  toolResultTtlSec: (() => {
@@ -2385,6 +2382,39 @@ export function attachConfigGroups(flat) {
2385
2382
  }
2386
2383
  return flat;
2387
2384
  }
2385
+ function k8sWorkspaceStore() {
2386
+ const workspacePersist = enumEnv("WORKSPACE_PERSIST", WORKSPACE_PERSIST_DEFAULT, WORKSPACE_PERSIST_WORDS);
2387
+ const workspaceSnapshotKeep = numEnvBounded("WORKSPACE_SNAPSHOT_KEEP", String(WORKSPACE_SNAPSHOT_KEEP_DEFAULT), WORKSPACE_SNAPSHOT_KEEP_RANGE.min, WORKSPACE_SNAPSHOT_KEEP_RANGE.max);
2388
+ if (!Number.isInteger(workspaceSnapshotKeep))
2389
+ throw new Error(`env WORKSPACE_SNAPSHOT_KEEP=${workspaceSnapshotKeep} must be an integer (a count of snapshots kept per space key)`);
2390
+ const endpoint = process.env.MINIO_ENDPOINT;
2391
+ const accessKey = process.env.MINIO_ACCESS_KEY;
2392
+ const secretKey = process.env.MINIO_SECRET_KEY;
2393
+ if (!endpoint || !accessKey || !secretKey) {
2394
+ if (workspacePersist === "space") {
2395
+ const missing = [...(endpoint ? [] : ["MINIO_ENDPOINT"]), ...(accessKey ? [] : ["MINIO_ACCESS_KEY"]), ...(secretKey ? [] : ["MINIO_SECRET_KEY"])];
2396
+ CONFIG_NOTICES.push({
2397
+ level: "warn",
2398
+ event: "workspace_persist_unavailable",
2399
+ fields: { missing: missing.join(","), effect: "k8s workspace starts empty every run (no snapshot store); set the missing keys, or drop WORKSPACE_PERSIST=space (the default is off) to silence this line" },
2400
+ });
2401
+ }
2402
+ return { workspacePersist, workspaceSnapshotKeep };
2403
+ }
2404
+ return {
2405
+ workspacePersist,
2406
+ workspaceSnapshotKeep,
2407
+ s3Snapshot: {
2408
+ endpoint,
2409
+ bucket: process.env.WORKSPACE_SNAPSHOT_BUCKET ?? "workspaces",
2410
+ accessKey,
2411
+ secretKey,
2412
+ ...(process.env.MINIO_REGION ? { region: process.env.MINIO_REGION } : {}),
2413
+ ...(process.env.WORKSPACE_SNAPSHOT_PREFIX ? { keyPrefix: process.env.WORKSPACE_SNAPSHOT_PREFIX } : {}),
2414
+ ...((n) => (n !== undefined ? { presignTtlSec: n } : {}))(optNumEnvStrict("WORKSPACE_SNAPSHOT_TTL_SEC")),
2415
+ },
2416
+ };
2417
+ }
2388
2418
  export function loadConfig() {
2389
2419
  EXEC_LANE_NOTICE = undefined;
2390
2420
  rejectRetiredEnvs();
@@ -101,8 +101,8 @@ warn: (msg: string, meta?: object) => void): Promise<string | undefined>;
101
101
  * 这条豁免目录算不算绝对」——引擎自本版起按**树的家族**判(`isAbsoluteForFamily`,家族从树的 root/cwd 拼法
102
102
  * 读,不看宿主 OS),本函数用的是 Node 宿主口径。**posix 家族两者逐字同判**(2026-09-12 实测六形全同),
103
103
  * 而本闸又只在 `hostSemanticsLane` 上场(路径就是这台 worker 自己的盘)⇒ 今天零分歧,不改。**分歧只在 win
104
- * 家族的 `/`-开头无盘符那一类**(Node 说绝对、引擎说未完全限定)。判据与坐标属主见 `task-cwd.ts` 的
105
- * {@link import("./task-cwd.js").isValidCwd} 顶注(同一条接缝,那里是唯一成文点,这里不复述第二份)。 */
104
+ * 家族的 `/`-开头无盘符那一类**(Node 说绝对、引擎说未完全限定)。同一条接缝的读数钉在 `test/task-cwd.test.ts`「[ref] 路径家族接缝」格
105
+ * (cwd 那一侧的判官是契约包的 `isAbsoluteHostPath`,posix 口径;那里是唯一成文点,这里不复述第二份)。 */
106
106
  export declare function acceptShellScratchpadDir(raw: unknown, opts: {
107
107
  requirePrincipal: boolean;
108
108
  hostSemanticsLane: boolean;
package/dist/env-facts.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { isAbsolute, join, resolve, sep } from "node:path";
2
2
  import { mkdir, readdir, rm, stat } from "node:fs/promises";
3
3
  import { createHash } from "node:crypto";
4
- import { dirCoversPath, isValidCwd, normalizeAbsolutePathLexically } from "./task-cwd.js";
4
+ import { isAbsoluteHostPath } from "@sema-agent/settings-schema";
5
+ import { dirCoversPath, normalizeAbsolutePathLexically } from "./task-cwd.js";
5
6
  const MAX_STR = 64;
6
7
  const MAX_CAPS = 16;
7
8
  const MAX_PATH = 160;
@@ -118,7 +119,7 @@ export function screenShellScratchpadDir(raw, opts) {
118
119
  return canonical;
119
120
  }
120
121
  export function scratchpadCoveringTaskRoot(args) {
121
- if (!args.cwdHonored || !isValidCwd(args.cwd))
122
+ if (!args.cwdHonored || !isAbsoluteHostPath(args.cwd))
122
123
  return undefined;
123
124
  const candidates = [];
124
125
  const shell = screenShellScratchpadDir(args.shellScratchpadDir, { requirePrincipal: args.requirePrincipal, hostSemanticsLane: true, warn: () => { } });
@@ -62,7 +62,9 @@ export interface FleetTaskRow {
62
62
  /** Owning principal (= the run's scope). The `/v1/fleet/stream` route filters to the caller's scope (owner-gated)
63
63
  * unless the caller is fleet-wide (operator / trace-token). NOT rendered — a tenant-isolation field. */
64
64
  scope?: string;
65
- /** Fleet per-session scope: the run's rootTaskId (= sessionId). The
65
+ /** Fleet per-session scope: the **originating session id** — the id the caller submitted under, which is what a
66
+ * shell sends as `?session=`. It is NOT the run's tree root (`rootTaskId`, the canonical taskId that forwarded
67
+ * subagents nest under — a different quantity, see {@link fleetRunPublisher}'s `run` param). The
66
68
  * `/v1/fleet/stream` route ALSO filters to the caller's `?session=` when present, so a TUI footer shows only the
67
69
  * CURRENT session's rows, not OTHER sessions of the same principal (the "他会话 token" leak). NOT rendered — a
68
70
  * scoping field like `scope`. Absent ⇒ the row is visible to any same-principal caller (back-compat, unscoped). */
@@ -128,6 +130,11 @@ export interface FleetTaskRow {
128
130
  turns?: number;
129
131
  costMicroUsd?: number;
130
132
  };
133
+ /** 顶层 run 的终局残局键(同上「只在终态帧出现」):k8s 车道工作区跨轮保留开着时,这条 run 结束的工作区快照
134
+ * **没存上**(上传或 `latest` 指针写失败)⇒ `"failed"`;下一条 run 恢复的是上一份。成功 / 没开保留 / 不是 k8s
135
+ * 车道 / 这条 run 没碰过沙箱 / 结局晚于终态帧(只留运维行)⇒ 缺席。只承载本 run 在终态帧之前落定的结局。
136
+ * 单成员闭集;源 = `workspace-snapshot-residual.ts`。ADDITIVE / tolerate-absent。 */
137
+ workspaceSnapshot?: "failed";
131
138
  resumable?: boolean;
132
139
  editedFiles?: Array<{
133
140
  path: string;
@@ -179,7 +186,8 @@ export interface FleetWorkflowRow {
179
186
  description?: string;
180
187
  /** Owning principal (= the workflow run's scope). Same owner-gate as FleetTaskRow. NOT rendered. */
181
188
  scope?: string;
182
- /** #3 fleet per-session scope (rootTaskId = sessionId): the `/v1/fleet/stream` `?session=` filter. NOT rendered. */
189
+ /** #3 fleet per-session scope (the originating session id, same quantity as {@link FleetTaskRow.sessionId}): the
190
+ * `/v1/fleet/stream` `?session=` filter. NOT rendered. */
183
191
  sessionId?: string;
184
192
  status: string;
185
193
  doneCount?: number;
@@ -556,7 +564,13 @@ export declare function fleetRunPublisher(bus: FleetEventBus | undefined, run: {
556
564
  scope: string;
557
565
  name: string;
558
566
  description?: string;
567
+ /** 嵌子代树的根:这条腿的 canonical core taskId(`spec.taskId ?? sessionId`)。转发子代的 `parentTaskId` 与它相等
568
+ * ⇒ 挂在 run 行下,否则挂在那个子代的行下。**只管嵌套**,不是行的会话键。 */
559
569
  rootTaskId?: string;
570
+ /** 发起会话 id —— 行上 `sessionId`(`GET /v1/fleet/stream?session=` 的过滤键)的**唯一来源**。
571
+ * 与 `rootTaskId` 是两个量(交互 / bg / 续跑三条腿的树根都是 taskId,不是会话 id)。
572
+ * 缺席 = 没有会话的 run:行不打键(既有 UNTAGGED 语义:同 principal 全会话可见,内容降权)。 */
573
+ sessionId?: string;
560
574
  }): {
561
575
  onStart: () => void;
562
576
  onEvent: (ev: {
@@ -592,11 +606,7 @@ export declare function fleetRunPublisher(bus: FleetEventBus | undefined, run: {
592
606
  /** [ref]①/(a) 案:`residuals` = 顶层 run 的终局残局键(有源才传,见 FleetTaskRow 上的键注)。
593
607
  * 只有**真终态**(completed/failed/killed)帧带残局——parked(suspended/needs_review)的行还活着,
594
608
  * 没有「终局」可言,误传也不上帧。 */
595
- onTerminal: (status: RunStatusForFleet, residuals?: {
596
- usage?: FleetTaskRow["usage"];
597
- stoppedBy?: string;
598
- transcriptId?: string;
599
- }) => void;
609
+ onTerminal: (status: RunStatusForFleet, residuals?: FleetRunResiduals) => void;
600
610
  };
601
611
  /** The run-scoped publisher handle (returned by `fleetRunPublisher`). Threaded into `runInBackground` so a
602
612
  * background run TRANSITIONS its fleet row exactly like the sync POST /v1/runs leg does. */
@@ -610,6 +620,12 @@ export type FleetRunPublisher = ReturnType<typeof fleetRunPublisher>;
610
620
  * 留着旧形不会红 —— 那正是危险所在:`TaskResult` 上已经没有 `errorCode` 这个键了,可结构性可选参数照样
611
621
  * 编译通过,于是 `stoppedBy:"user"` 会**静默地永远不再铸**(取消的 run 在舰队行上从此看不出是被人杀的)。
612
622
  * 终局事实一律走因由,`failureCodeOf` 是本仓读「这次失败带的是哪个码」的唯一形。 */
623
+ export type FleetRunResiduals = {
624
+ usage?: FleetTaskRow["usage"];
625
+ stoppedBy?: string;
626
+ transcriptId?: string;
627
+ workspaceSnapshot?: "failed";
628
+ };
613
629
  export declare function fleetRunResiduals(result: {
614
630
  sessionId?: string;
615
631
  terminal?: TerminalCause;
@@ -619,11 +635,7 @@ export declare function fleetRunResiduals(result: {
619
635
  toolCalls?: number;
620
636
  costMicroUsd?: number;
621
637
  };
622
- } | null | undefined): {
623
- usage?: FleetTaskRow["usage"];
624
- stoppedBy?: string;
625
- transcriptId?: string;
626
- };
638
+ } | null | undefined): FleetRunResiduals;
627
639
  /**
628
640
  * [ref]-B (core 1.240.0) — the PROCESS-LEVEL background-child fleet publisher, wired once
629
641
  * onto `RunnerDeps.onBackgroundChildEvent`. Unlike {@link fleetRunPublisher} (per-leg: dies at leg settle —
@@ -1,6 +1,7 @@
1
1
  import { EventEmitter } from "node:events";
2
2
  import { DURABLE_AGENT_HANDLE_RE, deriveAgentDisplayStatus, isDelegatedAgentTerminal } from "@sema-agent/core";
3
3
  import { persistedPlaneOf } from "../terminal.js";
4
+ import { takeWorkspaceSnapshotFailure } from "./workspace-snapshot-residual.js";
4
5
  import { recordFailOpen } from "../observability/fail-open.js";
5
6
  import { redactSecrets } from "../trace/redact.js";
6
7
  import { buildRunObjectivePreview } from "../user-authored-text.js";
@@ -200,7 +201,7 @@ export function fleetRunPublisher(bus, run) {
200
201
  return { onStart: () => undefined, onEvent: () => undefined, onForwardEvent: () => undefined, onChildTerminal: () => false, onTerminal: () => undefined };
201
202
  let tokens = 0;
202
203
  const startedAt = Date.now();
203
- const sessionField = run.rootTaskId ? { sessionId: run.rootTaskId } : {};
204
+ const sessionField = run.sessionId ? { sessionId: run.sessionId } : {};
204
205
  const children = new Set();
205
206
  const childStartedAt = new Map();
206
207
  const childByToolCall = new Map();
@@ -332,6 +333,7 @@ export function fleetRunPublisher(bus, run) {
332
333
  ...(residuals.usage ? { usage: residuals.usage } : {}),
333
334
  ...(residuals.stoppedBy ? { stoppedBy: residuals.stoppedBy } : {}),
334
335
  ...(residuals.transcriptId ? { transcriptId: residuals.transcriptId } : {}),
336
+ ...(residuals.workspaceSnapshot ? { workspaceSnapshot: residuals.workspaceSnapshot } : {}),
335
337
  }
336
338
  : {};
337
339
  bus.publishTask({ id: run.runId, status: fleetStatus, tokens, elapsedMs: Date.now() - startedAt, ...res });
@@ -362,6 +364,7 @@ export function fleetRunResiduals(result) {
362
364
  ...(Object.keys(usage).length > 0 ? { usage } : {}),
363
365
  ...(persistedPlaneOf(result).errorCode === "cancelled" ? { stoppedBy: "user" } : {}),
364
366
  ...(result.sessionId ? { transcriptId: result.sessionId } : {}),
367
+ ...(takeWorkspaceSnapshotFailure(result.sessionId) ? { workspaceSnapshot: "failed" } : {}),
365
368
  };
366
369
  }
367
370
  export function fleetBackgroundChildPublisher(bus, log) {
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Open the outcome seat of the env being built for a run of `sessionId` (call it when the env is built, before its
3
+ * connect). Closes the session's earlier seats and drops what they left pending. The returned function is the env's
4
+ * `onWorkspaceSnapshot` seat — a closure ⇒ `create*`.
5
+ */
6
+ export declare function createRunWorkspaceSnapshotSeat(sessionId: string): (outcome: "saved" | "failed") => void;
7
+ /** Take-once, at settle: `true` when this session's run reported a failed run-end snapshot that no settle has carried yet.
8
+ * Closes the session's seat (see the header: the frame ends "before the frame"). */
9
+ export declare function takeWorkspaceSnapshotFailure(sessionId: string | undefined): boolean;
10
+ //# sourceMappingURL=workspace-snapshot-residual.d.ts.map
@@ -0,0 +1,35 @@
1
+ const pending = new Map();
2
+ const seats = new Map();
3
+ const CAP = 1024;
4
+ let generation = 0;
5
+ function bounded(map) {
6
+ while (map.size > CAP) {
7
+ const oldest = map.keys().next().value;
8
+ if (oldest === undefined)
9
+ return;
10
+ map.delete(oldest);
11
+ }
12
+ }
13
+ export function createRunWorkspaceSnapshotSeat(sessionId) {
14
+ const mine = ++generation;
15
+ seats.delete(sessionId);
16
+ seats.set(sessionId, mine);
17
+ bounded(seats);
18
+ pending.delete(sessionId);
19
+ return (outcome) => {
20
+ if (seats.get(sessionId) !== mine)
21
+ return;
22
+ pending.delete(sessionId);
23
+ if (outcome === "saved")
24
+ return;
25
+ pending.set(sessionId, true);
26
+ bounded(pending);
27
+ };
28
+ }
29
+ export function takeWorkspaceSnapshotFailure(sessionId) {
30
+ if (sessionId === undefined)
31
+ return false;
32
+ seats.delete(sessionId);
33
+ return pending.delete(sessionId);
34
+ }
35
+ //# sourceMappingURL=workspace-snapshot-residual.js.map