@sema-agent/server 6.7.0 → 6.8.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.
@@ -15,6 +15,14 @@ import { parseNumOrFailNonNegative } from "../config.js";
15
15
  import { shellQuote as shellSafe } from "../plugins/remote-shell.js";
16
16
  import { BakeRunner, } from "./runner.js";
17
17
  const exec = promisify(execCb);
18
+ // 鲁棒性批5 A3(2026-08-05):claim/ingest/heartbeat 三个 fetch 此前零超时——bake-runner 的整条 loop()
19
+ // 是这台构建宿主机唯一的主循环,一次悬挂的 image-api 连接(网络分区/黑洞 TCP)会让 claim/heartbeat 永久
20
+ // 挂起,直到底层 socket 超时(可能几十分钟),期间既不上报心跳也不能认领新 bake——整机静默停摆。
21
+ // 上界两档:claim/ingest 不在心跳热路径,给 30s 吃满慢网 RTT + 大 body;heartbeat 必须显著小于
22
+ // BAKE_HEARTBEAT_MS 的默认量级(30s)——否则一次超时本身就吃掉一整个心跳周期,取 10s(同
23
+ // fleet-client.ts `FLEET_CLIENT_FETCH_TIMEOUT_MS` 判据的普适形)。
24
+ const BAKE_CLAIM_INGEST_FETCH_TIMEOUT_MS = 30_000;
25
+ const BAKE_HEARTBEAT_FETCH_TIMEOUT_MS = 10_000;
18
26
  function loadEnv() {
19
27
  const need = (k) => {
20
28
  const v = process.env[k];
@@ -55,6 +63,7 @@ function makeApiClient(env, log) {
55
63
  method: "POST",
56
64
  headers: auth,
57
65
  body: JSON.stringify({ runnerId: env.runnerId }),
66
+ signal: AbortSignal.timeout(BAKE_CLAIM_INGEST_FETCH_TIMEOUT_MS),
58
67
  });
59
68
  if (res.status === 204)
60
69
  return null; // empty queue — the routine "nothing to do" case, no log line
@@ -93,6 +102,7 @@ function makeApiClient(env, log) {
93
102
  method: "POST",
94
103
  headers: { ...auth, "x-bake-ingest-secret": ingestSecret },
95
104
  body: JSON.stringify(frame),
105
+ signal: AbortSignal.timeout(BAKE_CLAIM_INGEST_FETCH_TIMEOUT_MS),
96
106
  });
97
107
  const body = res.ok ? (await res.json().catch(() => ({}))) : {};
98
108
  return {
@@ -107,6 +117,7 @@ function makeApiClient(env, log) {
107
117
  method: "POST",
108
118
  headers: { ...auth, "x-bake-ingest-secret": ingestSecret },
109
119
  body: JSON.stringify({ event: "heartbeat" }),
120
+ signal: AbortSignal.timeout(BAKE_HEARTBEAT_FETCH_TIMEOUT_MS),
110
121
  });
111
122
  const body = res.ok ? (await res.json().catch(() => ({}))) : {};
112
123
  return {
@@ -23,5 +23,16 @@ export interface LeaderCtx {
23
23
  sessionStore: ReturnType<StoreBackend["session"]>;
24
24
  checkpointStore: CheckpointStoreFull | undefined;
25
25
  }
26
+ /**
27
+ * 鲁棒性批5 A6(2026-08-05):k8s 腿要求 MinIO 三件套(ENDPOINT/ACCESS_KEY/SECRET_KEY)全在——半开(一件缺)
28
+ * 就让 `leaderEndpoint` 的三元式落到 undefined,此前**零披露**:运维显式开了 LEADER_ENABLED +
29
+ * REMOTE_EXEC=k8s,却在日志里既看不到 `leader_endpoint_enabled` 也看不到任何"为什么没启用"的信号——只能
30
+ * 靠读源码才知道要去检查 MinIO 三件套。
31
+ *
32
+ * 纯谓词,抽出独立函数:只吃 leaderEnabled/leaderProvider/hasS3 三个原语 + 直接读 process.env 取缺失的
33
+ * 具体变量名,不依赖完整 `LeaderCtx`(构造一整套 brain/pricing/sessionStore 只为测一个 warn 分支不值当)。
34
+ * 返回 null = 不适用该警告(未开启/非 k8s 腿/三件套齐全);非 null = 该报警,携带具体缺的变量名列表。
35
+ */
36
+ export declare function leaderK8sMinioGap(leaderEnabled: boolean, leaderProvider: string | undefined, hasS3: boolean): string[] | null;
26
37
  export declare function createLeaderFace(ctx: LeaderCtx): ReturnType<typeof createLeaderEndpoint> | undefined;
27
38
  //# sourceMappingURL=leader.d.ts.map
@@ -1,5 +1,24 @@
1
1
  import { createLeaderEndpoint } from "../leader/endpoint.js";
2
2
  import { createLeaderRunner } from "../leader/wire.js";
3
+ /**
4
+ * 鲁棒性批5 A6(2026-08-05):k8s 腿要求 MinIO 三件套(ENDPOINT/ACCESS_KEY/SECRET_KEY)全在——半开(一件缺)
5
+ * 就让 `leaderEndpoint` 的三元式落到 undefined,此前**零披露**:运维显式开了 LEADER_ENABLED +
6
+ * REMOTE_EXEC=k8s,却在日志里既看不到 `leader_endpoint_enabled` 也看不到任何"为什么没启用"的信号——只能
7
+ * 靠读源码才知道要去检查 MinIO 三件套。
8
+ *
9
+ * 纯谓词,抽出独立函数:只吃 leaderEnabled/leaderProvider/hasS3 三个原语 + 直接读 process.env 取缺失的
10
+ * 具体变量名,不依赖完整 `LeaderCtx`(构造一整套 brain/pricing/sessionStore 只为测一个 warn 分支不值当)。
11
+ * 返回 null = 不适用该警告(未开启/非 k8s 腿/三件套齐全);非 null = 该报警,携带具体缺的变量名列表。
12
+ */
13
+ export function leaderK8sMinioGap(leaderEnabled, leaderProvider, hasS3) {
14
+ if (!(leaderEnabled && leaderProvider === "k8s" && !hasS3))
15
+ return null;
16
+ return [
17
+ !process.env.MINIO_ENDPOINT && "MINIO_ENDPOINT",
18
+ !process.env.MINIO_ACCESS_KEY && "MINIO_ACCESS_KEY",
19
+ !process.env.MINIO_SECRET_KEY && "MINIO_SECRET_KEY",
20
+ ].filter((v) => typeof v === "string");
21
+ }
3
22
  export function createLeaderFace(ctx) {
4
23
  const { config, logger, brain, pricing, executionEnvFactory, toolResultStore, sessionStore, checkpointStore } = ctx;
5
24
  // v2 leader endpoint (design/50 + design/68): wire when LEADER_ENABLED + an isolated remote-exec backend
@@ -19,6 +38,9 @@ export function createLeaderFace(ctx) {
19
38
  }
20
39
  : {};
21
40
  const leaderProvider = config.remoteExec?.provider;
41
+ const minioGap = leaderK8sMinioGap(config.leaderEnabled, leaderProvider, "s3" in leaderMinio);
42
+ if (minioGap)
43
+ logger.warn("leader_k8s_minio_incomplete", { missing: minioGap });
22
44
  // DUAL-MODE §5: orchestration is an ENGINE capability, not fleet-only — the TOC `host` lane runs leader fan-out
23
45
  // bounded by ONE box (isolation=none, NON-durable: no snapshot, so the durable sub-worker suspend block below is
24
46
  // skipped — host workers run to completion in-process-adjacent). e2b/k8s keep their isolated/suspendable posture.
package/dist/config.js CHANGED
@@ -491,9 +491,11 @@ function parseStoreDomain(ctx) {
491
491
  user: env("MYSQL_USER"),
492
492
  password: env("MYSQL_PASSWORD", ""),
493
493
  database: env("MYSQL_DATABASE"),
494
- connectionLimit: process.env.MYSQL_POOL_SIZE
495
- ? Number(process.env.MYSQL_POOL_SIZE)
496
- : undefined,
494
+ // 鲁棒性批5 A2(2026-08-05):此前裸 `Number(process.env.MYSQL_POOL_SIZE)` ——一个手滑值(如
495
+ // "20;drop") → NaN,driver 的 `connectionLimit` 直接拿到 NaN 而不是走池大小默认,行为因驱动
496
+ // 而异(未必 fail-loud)。optFinitePositiveEnv 与 dbQueryTimeoutMs 同款:非法值回默认(undefined
497
+ // = driver 默认池大小)+ S20 boot 警告,合法值照常生效。
498
+ connectionLimit: optFinitePositiveEnv("MYSQL_POOL_SIZE"),
497
499
  }
498
500
  : undefined,
499
501
  pg: needsDb && dbBackend === "pg"
@@ -503,7 +505,8 @@ function parseStoreDomain(ctx) {
503
505
  user: env("PG_USER"),
504
506
  password: env("PG_PASSWORD", ""),
505
507
  database: env("PG_DATABASE"),
506
- connectionLimit: process.env.PG_POOL_SIZE ? Number(process.env.PG_POOL_SIZE) : undefined,
508
+ // MYSQL_POOL_SIZE (鲁棒性批5 A2)
509
+ connectionLimit: optFinitePositiveEnv("PG_POOL_SIZE"),
507
510
  }
508
511
  : undefined,
509
512
  // S9 deeper fix: per-query DB timeout (see the interface doc). Soft knob — a typo degrades to the default
@@ -1177,7 +1180,12 @@ function parseOrchestrationDomain(ctx) {
1177
1180
  ...(process.env.DOCKER_HOST ? { dockerHost: process.env.DOCKER_HOST } : {}),
1178
1181
  ...(process.env.DOCKER_MEMORY ? { memory: process.env.DOCKER_MEMORY } : {}),
1179
1182
  ...(process.env.DOCKER_CPUS ? { cpus: Number(process.env.DOCKER_CPUS) } : {}),
1180
- ...(process.env.DOCKER_PIDS_LIMIT ? { pidsLimit: Number(process.env.DOCKER_PIDS_LIMIT) } : {}),
1183
+ // 鲁棒性批5 A1(2026-08-05):此前裸 `Number(process.env.DOCKER_PIDS_LIMIT)`——非数字值(如
1184
+ // 手滑的 "512;") → NaN,而消费端(local-docker 执行环境)对 `pidsLimit` 的守卫是
1185
+ // `?? 512`(只挡 null/undefined),NaN 穿透守卫;随后 `NaN > 0` 恒假,`--pids-limit` 整个
1186
+ // 从 docker run 参数里省略——运维以为设了 fork-bomb 背栓,实际背栓被静默卸掉。
1187
+ // optFinitePositiveEnv:非法值回 undefined(消费端默认 512 生效)+ S20 boot 警告。
1188
+ ...((n) => (n !== undefined ? { pidsLimit: n } : {}))(optFinitePositiveEnv("DOCKER_PIDS_LIMIT")),
1181
1189
  ...(boolEnv("DOCKER_DROP_CAPS", false) ? { dropAllCaps: true } : {}),
1182
1190
  ...(process.env.DOCKER_NETWORK
1183
1191
  ? { network: enumEnv("DOCKER_NETWORK", "bridge", ["none", "bridge", "host"]) }
@@ -420,6 +420,10 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
420
420
  }
421
421
  // §4 active-run guard (don't drop a live append) + per-session import lease (don't let two staged imports race
422
422
  // to commit the same session). Both 409.
423
+ // `runStore?.` 的可选链不是守卫豁免面(鲁棒性批5 #7 亲验定性,2026-08-06):runStore 仅在
424
+ // **backend 整体缺席**时才 undefined(tidb/pg/local 三形态的 backend.run() 全都在,main.ts:378),
425
+ // 而 backend 缺席时本路由的 Phase A 已在 `beginImportStaging` 探测处硬 501(capability.session_store_
426
+ // required)——「守卫被跳过而 sync 仍可达」的组合按构造不存在(能力面与路由解析同真值)。
423
427
  const activeTaskId = await deps.runStore?.getActiveTaskId(sessionId);
424
428
  if (activeTaskId) {
425
429
  sendError(res, 409, "session_active", "session has an active run; sync after it settles", { activeTaskId });
@@ -1,6 +1,6 @@
1
1
  import { type Brain, type Model, type ModelRoles, type ModelPricing, type TaskSpec, type ExecutionEnvFactory, type RemoteExecutionEnv, type ToolResultStore, type SessionStore } from "@sema-agent/core";
2
2
  import type { CheckpointStoreFull } from "../plugins/store-backend.js";
3
- import { type LeaderResult } from "./leader.js";
3
+ import { type LeaderDeps, type LeaderResult } from "./leader.js";
4
4
  import type { LeaderRequestBody } from "./endpoint.js";
5
5
  export interface LeaderWireConfig {
6
6
  /** Static-env compat mode (E2B only): the leader provisions/owns each env. Required when `envFactory` unset. */
@@ -162,6 +162,16 @@ export declare function leaderLoopConfig(cfg: {
162
162
  repairRounds?: number;
163
163
  conflictRounds?: number;
164
164
  }, env?: NodeJS.ProcessEnv): LeaderLoopConfig;
165
+ export declare const LEADER_PUSH_NETWORK_TIMEOUT_MS = 120000;
166
+ export declare const LEADER_PUSH_LOCAL_TIMEOUT_MS = 30000;
167
+ /** Coordinator(control plane,持有推送凭据)的 git push 闭包——从 `createLeaderRunner` 抽出为独立工厂,
168
+ * 仅依赖 durableRemote/targetRef/git 身份(不依赖 wire 内其余状态),因此可以脱离完整的
169
+ * plan→provision→fan-out→merge 管线单测(那条管线需要真 brain + E2B/k8s env)。行为与抽出前逐字相同,
170
+ * 唯一新增是六条 execFileSync 各自的 `timeout`。 */
171
+ export declare function createCoordinatorPush(durableRemote: string, targetRef: string | undefined, ident: {
172
+ name: string;
173
+ email: string;
174
+ }): LeaderDeps["push"];
165
175
  export declare function createLeaderRunner(cfg: LeaderWireConfig): (body: LeaderRequestBody) => Promise<LeaderResult>;
166
176
  export {};
167
177
  //# sourceMappingURL=wire.d.ts.map
@@ -177,6 +177,68 @@ export function leaderLoopConfig(cfg, env = process.env) {
177
177
  ...(env.LEADER_BUDGET_USD ? { replanBudgetUsd: parseNumOrFailNonNegative("LEADER_BUDGET_USD", env.LEADER_BUDGET_USD) } : {}),
178
178
  };
179
179
  }
180
+ // 鲁棒性批5 A4(2026-08-05):Coordinator 的六条 execFileSync(clone/config×2/am/ls-remote/push×2)此前零超时。
181
+ // `execFileSync` 是**同步**调用——它挂起的是整个 Node 事件循环,不只是这一次 leader 跑;`durableRemote`
182
+ // 是运维配的、可能半开/不可达的远端(DNS 黑洞、TCP SYN 丢、对端 git-upload-pack 卡死),一旦挂住,这一个
183
+ // 副本上**所有并发请求**一起冻结,直到操作系统层 TCP 超时(可能几十分钟)才会松绑。
184
+ // 上界两档:clone/push/ls-remote 是网络往返(仓可能较大,给 120s);config/am 是纯本地操作(不该合法挂起,
185
+ // 但损坏的 object store / 文件锁可能卡住 `git am`),给 30s——同 fleet-client.ts `FLEET_CLIENT_FETCH_TIMEOUT_MS`
186
+ // 判据的普适形(操作分类决定上界,不是一刀切)。
187
+ export const LEADER_PUSH_NETWORK_TIMEOUT_MS = 120_000;
188
+ export const LEADER_PUSH_LOCAL_TIMEOUT_MS = 30_000;
189
+ /** Coordinator(control plane,持有推送凭据)的 git push 闭包——从 `createLeaderRunner` 抽出为独立工厂,
190
+ * 仅依赖 durableRemote/targetRef/git 身份(不依赖 wire 内其余状态),因此可以脱离完整的
191
+ * plan→provision→fan-out→merge 管线单测(那条管线需要真 brain + E2B/k8s env)。行为与抽出前逐字相同,
192
+ * 唯一新增是六条 execFileSync 各自的 `timeout`。 */
193
+ export function createCoordinatorPush(durableRemote, targetRef, ident) {
194
+ return async (integratedPatch, baseSha) => {
195
+ // Coordinator (control plane, creds): clone durable remote, apply integrated series, force-with-lease push.
196
+ // 🔴 requires `git` on the service host PATH (deploy req).
197
+ const dir = mkdtempSync(join(tmpdir(), "leader-coord-"));
198
+ // Force C locale so the `raced` regex below matches git's (English) push-rejection messages regardless of
199
+ // the host locale — otherwise a translated message → regex miss → a real race mislabeled non-retryable (council #5).
200
+ const gitEnv = { ...process.env, LC_ALL: "C" };
201
+ try {
202
+ execFileSync("git", ["clone", "-q", durableRemote, dir], { env: gitEnv, timeout: LEADER_PUSH_NETWORK_TIMEOUT_MS });
203
+ execFileSync("git", ["-C", dir, "config", "user.name", ident.name], { timeout: LEADER_PUSH_LOCAL_TIMEOUT_MS });
204
+ execFileSync("git", ["-C", dir, "config", "user.email", ident.email], { timeout: LEADER_PUSH_LOCAL_TIMEOUT_MS });
205
+ writeFileSync(join(dir, ".leader.patch"), integratedPatch);
206
+ execFileSync("git", ["-C", dir, "am", "--3way", ".leader.patch"], { env: gitEnv, timeout: LEADER_PUSH_LOCAL_TIMEOUT_MS });
207
+ rmSync(join(dir, ".leader.patch"));
208
+ const ref = targetRef ?? "refs/heads/main";
209
+ // CAS against the TARGET ref's ACTUAL current tip (not baseSha). The old `=ref:baseSha` lease assumed
210
+ // the target ref already pointed at baseSha — but a FRESH ref (or one at any other commit) is never at
211
+ // baseSha, so the lease rejected EVERY such push and mislabeled it "raced" (found live: auto1 reached
212
+ // push after a clean 6-worker fan-out + green gradle, and failed only here). ls-remote gives the ref's
213
+ // current oid (empty = it doesn't exist yet) → lease against THAT, which still detects a real concurrent
214
+ // merge race (the ref moved since we read it) but lets a normal create/update through.
215
+ // Does the target ref already exist? (exact full-ref match; --refs drops peeled annotated-tag lines.)
216
+ const ls = execFileSync("git", ["-C", dir, "ls-remote", "--refs", "origin", ref], { encoding: "utf8", env: gitEnv, timeout: LEADER_PUSH_NETWORK_TIMEOUT_MS });
217
+ const exists = ls.split("\n").some((l) => l.split("\t")[1] === ref);
218
+ if (exists) {
219
+ // Existing ref → keep the original CAS against baseSha (detects a concurrent merge that moved the ref
220
+ // off the base this run started from — council#2). NOT against a just-read end-of-run tip: that would
221
+ // let leader B force-push over leader A's concurrent merge (Codex review #1). A ref that exists but
222
+ // isn't at baseSha → refuse (use a fresh per-run targetRef to avoid that — the proving-ground does).
223
+ execFileSync("git", ["-C", dir, "push", `--force-with-lease=${ref}:${baseSha}`, "origin", `HEAD:${ref}`], { env: gitEnv, timeout: LEADER_PUSH_NETWORK_TIMEOUT_MS });
224
+ }
225
+ else {
226
+ // Absent → create with a "must not exist" lease (empty expected oid): a concurrent create races safely
227
+ // (rejected as stale info) instead of silently fast-forwarding. The original `:baseSha` lease wrongly
228
+ // rejected a fresh ref (it's never at baseSha) — that was auto1's only failure after a green build.
229
+ execFileSync("git", ["-C", dir, "push", `--force-with-lease=${ref}:`, "origin", `HEAD:${ref}`], { env: gitEnv, timeout: LEADER_PUSH_NETWORK_TIMEOUT_MS });
230
+ }
231
+ return { ok: true, ref };
232
+ }
233
+ catch (e) {
234
+ const msg = e instanceof Error ? e.message : String(e);
235
+ return { ok: false, raced: /stale info|force-with-lease|\[rejected\]|non-fast-forward/.test(msg), error: msg };
236
+ }
237
+ finally {
238
+ rmSync(dir, { recursive: true, force: true });
239
+ }
240
+ };
241
+ }
180
242
  export function createLeaderRunner(cfg) {
181
243
  const W = cfg.workspace ?? "/home/user";
182
244
  const repo = `${W}/repo`;
@@ -563,53 +625,7 @@ export function createLeaderRunner(cfg) {
563
625
  }
564
626
  return { env: asIntegrationEnv(env), destroy: () => env.destroy().then(() => { }), ...(mkRepair(env, repo, body.oracleFiles ?? []) ? { repair: mkRepair(env, repo, body.oracleFiles ?? []) } : {}), ...(mkConflictResolver(env, repo, body.oracleFiles ?? []) ? { conflictResolver: mkConflictResolver(env, repo, body.oracleFiles ?? []) } : {}) };
565
627
  };
566
- const push = async (integratedPatch, baseSha) => {
567
- // Coordinator (control plane, creds): clone durable remote, apply integrated series, force-with-lease push.
568
- // 🔴 requires `git` on the service host PATH (deploy req).
569
- const dir = mkdtempSync(join(tmpdir(), "leader-coord-"));
570
- // Force C locale so the `raced` regex below matches git's (English) push-rejection messages regardless of
571
- // the host locale — otherwise a translated message → regex miss → a real race mislabeled non-retryable (council #5).
572
- const gitEnv = { ...process.env, LC_ALL: "C" };
573
- try {
574
- execFileSync("git", ["clone", "-q", body.durableRemote, dir], { env: gitEnv });
575
- execFileSync("git", ["-C", dir, "config", "user.name", ident.name]);
576
- execFileSync("git", ["-C", dir, "config", "user.email", ident.email]);
577
- writeFileSync(join(dir, ".leader.patch"), integratedPatch);
578
- execFileSync("git", ["-C", dir, "am", "--3way", ".leader.patch"], { env: gitEnv });
579
- rmSync(join(dir, ".leader.patch"));
580
- const ref = body.targetRef ?? "refs/heads/main";
581
- // CAS against the TARGET ref's ACTUAL current tip (not baseSha). The old `=ref:baseSha` lease assumed
582
- // the target ref already pointed at baseSha — but a FRESH ref (or one at any other commit) is never at
583
- // baseSha, so the lease rejected EVERY such push and mislabeled it "raced" (found live: auto1 reached
584
- // push after a clean 6-worker fan-out + green gradle, and failed only here). ls-remote gives the ref's
585
- // current oid (empty = it doesn't exist yet) → lease against THAT, which still detects a real concurrent
586
- // merge race (the ref moved since we read it) but lets a normal create/update through.
587
- // Does the target ref already exist? (exact full-ref match; --refs drops peeled annotated-tag lines.)
588
- const ls = execFileSync("git", ["-C", dir, "ls-remote", "--refs", "origin", ref], { encoding: "utf8", env: gitEnv });
589
- const exists = ls.split("\n").some((l) => l.split("\t")[1] === ref);
590
- if (exists) {
591
- // Existing ref → keep the original CAS against baseSha (detects a concurrent merge that moved the ref
592
- // off the base this run started from — council#2). NOT against a just-read end-of-run tip: that would
593
- // let leader B force-push over leader A's concurrent merge (Codex review #1). A ref that exists but
594
- // isn't at baseSha → refuse (use a fresh per-run targetRef to avoid that — the proving-ground does).
595
- execFileSync("git", ["-C", dir, "push", `--force-with-lease=${ref}:${baseSha}`, "origin", `HEAD:${ref}`], { env: gitEnv });
596
- }
597
- else {
598
- // Absent → create with a "must not exist" lease (empty expected oid): a concurrent create races safely
599
- // (rejected as stale info) instead of silently fast-forwarding. The original `:baseSha` lease wrongly
600
- // rejected a fresh ref (it's never at baseSha) — that was auto1's only failure after a green build.
601
- execFileSync("git", ["-C", dir, "push", `--force-with-lease=${ref}:`, "origin", `HEAD:${ref}`], { env: gitEnv });
602
- }
603
- return { ok: true, ref };
604
- }
605
- catch (e) {
606
- const msg = e instanceof Error ? e.message : String(e);
607
- return { ok: false, raced: /stale info|force-with-lease|\[rejected\]|non-fast-forward/.test(msg), error: msg };
608
- }
609
- finally {
610
- rmSync(dir, { recursive: true, force: true });
611
- }
612
- };
628
+ const push = createCoordinatorPush(body.durableRemote, body.targetRef, ident);
613
629
  const deps = {
614
630
  plan,
615
631
  provisionWorker,
package/dist/runs.js CHANGED
@@ -362,7 +362,9 @@ promptManifests) {
362
362
  // run-store mutation/poll below (the run row was created with owner = principal ?? null).
363
363
  const owner = principal ?? null;
364
364
  const heartbeat = setInterval(() => {
365
- void runStore.heartbeat(taskId, owner).catch(() => undefined);
365
+ // 鲁棒性批5 A5(2026-08-05):与下面 cancel/preempt 两条兄弟同族——此前裸吞错,store 持续故障期间本 run
366
+ // 的心跳每拍静默落空、运维零信号,直到某个副本的 reapStale 把它误判死亡(心跳失败正是那个误判的前兆)。
367
+ void runStore.heartbeat(taskId, owner).catch(() => { metrics?.inc("run_signal_poll_errors_total", { kind: "heartbeat" }); });
366
368
  // Cross-replica cancel: the cancel may have landed on another instance, which only set the durable flag.
367
369
  // Poll it here so the OWNING instance aborts its in-flight run (bounded by HEARTBEAT_MS).
368
370
  // 鲁棒性批3 A5(2026-08-04,§M 感知链路):poll 失败本身有界(下一拍重试),但此前**零披露**——
@@ -69,9 +69,10 @@ export function normalizeAttachments(v) {
69
69
  ...(o.todoReminder === true ? { todoReminder: true } : {}),
70
70
  ...(changedFiles !== undefined ? { changedFiles } : {}),
71
71
  ...(o.planModeReminder === true ? { planModeReminder: true } : {}),
72
- // core 1.253 G1: post-compaction background-task recap + deferred-tools boundary notice
73
- // same literal-true contract as the rest of the family (both are one-shot/opt-in on the core side).
74
- ...(o.backgroundTasks === true ? { backgroundTasks: true } : {}),
72
+ // core 1.253 G1 5.12.0 BREAKING-3:压缩后 bg 任务重述从 opt-in **DEFAULT-ON**(显式 false
73
+ // 恒关/缺席走默认)。literal-true 形在此翻转后就是轴B #1 的原案复刻(agentListing/skillsListing
74
+ // 当年同病):丢 false = 客户端显式关断被静默压成默认 ON,wire 上无法表达关闭 boolean 透传。
75
+ ...(typeof o.backgroundTasks === "boolean" ? { backgroundTasks: o.backgroundTasks } : {}),
75
76
  ...(o.toolsDelta === true ? { toolsDelta: true } : {}),
76
77
  ...(o.todoReminderMode === "baseline" || o.todoReminderMode === "off" ? { todoReminderMode: o.todoReminderMode } : {}),
77
78
  ...(o.budgetUsd === true ? { budgetUsd: true } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "6.7.0",
3
+ "version": "6.8.0",
4
4
  "description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -54,7 +54,7 @@
54
54
  "build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
55
55
  },
56
56
  "dependencies": {
57
- "@sema-agent/core": "^5.11.0",
57
+ "@sema-agent/core": "^5.12.0",
58
58
  "@sema-agent/registry-core": "^0.14.0",
59
59
  "e2b": "^2.28.0",
60
60
  "libsodium-wrappers": "^0.8.4",