@sema-agent/server 7.93.1 → 7.93.2

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.
@@ -342,6 +342,8 @@ async function runLeaderForTrap(rt, brain, models, roles, pricing, trap, cellId,
342
342
  "--enable=receive-pack",
343
343
  remoteDir,
344
344
  ], { stdio: "ignore" });
345
+ let daemonSpawnError;
346
+ daemon.on("error", (e) => { daemonSpawnError = e.message; });
345
347
  const waitForPort = async () => {
346
348
  for (let i = 0; i < 30; i++) {
347
349
  const up = await new Promise((resolve) => {
@@ -387,6 +389,7 @@ async function runLeaderForTrap(rt, brain, models, roles, pricing, trap, cellId,
387
389
  let res;
388
390
  try {
389
391
  if (!(await waitForPort())) {
392
+ console.error(`[s1] git daemon did not come up on 127.0.0.1:${port}${daemonSpawnError ? ` — spawn failed: ${daemonSpawnError}` : ""}`);
390
393
  return { ok: false, reports: [], infraFailed: true };
391
394
  }
392
395
  const seedDir = mkdtempSync(join(tmpdir(), "s1-team-seed-"));
@@ -1,3 +1,24 @@
1
1
  /** `err` 的 fatal 日志文本:`String(err)` 过形状律一次。纯函数,零副作用。 */
2
2
  export declare function buildFatalErrorText(err: unknown): string;
3
+ /**
4
+ * 进程级 fatal 出口的**唯一一只腿**:同一只 {@link buildFatalErrorText} 产物写两个面,然后按崩溃码退。
5
+ *
6
+ * ── 病(S-555,test [ref] P2)────────────────────────────────────────────────────────────────────
7
+ * 出口过去只有那条 JSON 行。而拒启文案是**多行**的 —— S-547 的列集对账会生成
8
+ * `ALTER TABLE <表> ADD COLUMN <列定义>;`,它存在的全部理由就是「运维直接粘进 psql」;塞进 JSON 字符串后
9
+ * 整段变成一串 `\n` 转义,**粘不走**。dist 里也没有 pretty / `isTTY` 分支去把它摊开(有也不该有:出口的
10
+ * 可读性不该取决于终端是谁)。
11
+ *
12
+ * ── 修 ─────────────────────────────────────────────────────────────────────────────────────────
13
+ * **同一只产物**再原样多行写一份到 stderr。刻意不做的三件:不铸第二份文案(两份措辞必然分叉)、不加
14
+ * pretty 旋钮(一个只有出错时才走的出口不该有配置面)、JSON 行**照旧**(机读面零回退,scraper 不动)。
15
+ * 两个面都在 stderr,次序=先机器行后人读段。**这个次序还承重**:`createLogger()` 的构造会装上 stdio 的
16
+ * EPIPE 无害化监听(见 `observability/logger.ts` 那段注 —— 宿主把 stderr 接管道又先退时,下一次写会发一个
17
+ * 异步 `'error'`,无监听即致命),所以那条裸 `process.stderr.write` **必须**排在它之后;同步抛由下面的
18
+ * `try/finally` 兜住。
19
+ *
20
+ * 全仓两个 fatal 出口(`main.ts` 的 `main().catch` 与 `run-local.ts` 的同名尾巴)都经这里 —— 后者此前
21
+ * 连形状律都没过(裸 `String(err)`),两处各写一遍正是「同类不同命」的土壤。
22
+ */
23
+ export declare function fatalExit(err: unknown): never;
3
24
  //# sourceMappingURL=fatal-exit.d.ts.map
@@ -1,5 +1,16 @@
1
1
  import { redactSecrets } from "../trace/redact.js";
2
+ import { createLogger } from "../observability/logger.js";
2
3
  export function buildFatalErrorText(err) {
3
4
  return redactSecrets(String(err));
4
5
  }
6
+ export function fatalExit(err) {
7
+ const text = buildFatalErrorText(err);
8
+ try {
9
+ createLogger().error("fatal", { err: text });
10
+ process.stderr.write(text + "\n");
11
+ }
12
+ finally {
13
+ process.exit(1);
14
+ }
15
+ }
5
16
  //# sourceMappingURL=fatal-exit.js.map
@@ -2,6 +2,7 @@ import { Runner, defaultTaskRegistry } from "@sema-agent/core";
2
2
  import { writeCrashLast, clearCrashLast, readCrashLast } from "./crash-last.js";
3
3
  import { createSighupIdleHandler } from "../sighup-idle.js";
4
4
  import { createParentWatch } from "../parent-watch.js";
5
+ import { reapHostForegroundChildren } from "../plugins/host-platform.js";
5
6
  import { createEngineLeaseTracker } from "./engine-lease.js";
6
7
  import { writeExitLast, readExitLast, clearExitLast } from "./exit-last.js";
7
8
  import { isScriptRealmRejection, describeRejectionReason } from "../orchestration/hardened-vm-runner.js";
@@ -30,8 +31,29 @@ export function installShutdownHandlers(ctx) {
30
31
  if (dataRoot)
31
32
  writeCrashLast(dataRoot, { version, inflight: drainState.inflight?.(), err });
32
33
  };
34
+ const reapOwnChildProcesses = () => {
35
+ try {
36
+ const reaped = defaultTaskRegistry.reapAllSessionBackground();
37
+ if (reaped > 0)
38
+ logger.info("shutdown_background_reaped", { reaped });
39
+ }
40
+ catch (err) {
41
+ logger.warn("shutdown_background_reap_failed", { err: String(err) });
42
+ }
43
+ try {
44
+ const fg = reapHostForegroundChildren();
45
+ if (fg.signalled > 0 || fg.alreadyDead > 0)
46
+ logger.info("shutdown_foreground_reaped", { signalled: fg.signalled, alreadyDead: fg.alreadyDead });
47
+ if (fg.failed.length > 0)
48
+ logger.warn("shutdown_foreground_reap_incomplete", { groups: fg.failed });
49
+ }
50
+ catch (err) {
51
+ logger.warn("shutdown_foreground_reap_failed", { err: String(err) });
52
+ }
53
+ };
33
54
  const onUncaughtException = (err) => {
34
55
  try {
56
+ reapOwnChildProcesses();
35
57
  writeLastWords(err);
36
58
  console.error(err);
37
59
  }
@@ -50,6 +72,7 @@ export function installShutdownHandlers(ctx) {
50
72
  return;
51
73
  }
52
74
  try {
75
+ reapOwnChildProcesses();
53
76
  writeLastWords(err);
54
77
  console.error(err);
55
78
  }
@@ -92,14 +115,7 @@ export function installShutdownHandlers(ctx) {
92
115
  subRunner.sessions.dispose();
93
116
  void lspManager?.dispose?.();
94
117
  void workflowNotifyJournal?.close?.();
95
- try {
96
- const reaped = defaultTaskRegistry.reapAllSessionBackground();
97
- if (reaped > 0)
98
- logger.info("shutdown_background_reaped", { reaped });
99
- }
100
- catch (err) {
101
- logger.warn("shutdown_background_reap_failed", { err: String(err) });
102
- }
118
+ reapOwnChildProcesses();
103
119
  void fleetClient?.stop();
104
120
  server.close(() => {
105
121
  void retentionTeardown.then(() => (backend ? backend.close() : Promise.resolve())).finally(() => process.exit(0));
package/dist/main.js CHANGED
@@ -1,5 +1,4 @@
1
- import { createLogger } from "./observability/logger.js";
2
- import { buildFatalErrorText } from "./boot/fatal-exit.js";
1
+ import { fatalExit } from "./boot/fatal-exit.js";
3
2
  import { runStage01 } from "./boot/stage-01-config.js";
4
3
  import { runStage02 } from "./boot/stage-02-registry-pull.js";
5
4
  import { runStage03 } from "./boot/stage-03-store-backend.js";
@@ -23,7 +22,6 @@ async function main() {
23
22
  await runStage10(s09);
24
23
  }
25
24
  void main().catch((err) => {
26
- createLogger().error("fatal", { err: buildFatalErrorText(err) });
27
- process.exit(1);
25
+ fatalExit(err);
28
26
  });
29
27
  //# sourceMappingURL=main.js.map
@@ -48,6 +48,45 @@ export declare function killTreeSoft(pid: number): void;
48
48
  /** Graceful tree kill where the CALLER has no escalation timer of its own (currently unused by the host lane —
49
49
  * exported for parity with core's surface; the bg driver keeps its own HOST_BG_KILL_GRACE_MS ladder). */
50
50
  export { killProcessTree };
51
+ /** 登记回执 = 注销的唯一凭据(纯数据;身份即凭据,见簿注)。 */
52
+ export interface ForegroundChildTicket {
53
+ /** 组领袖 pid = pgid(POSIX `detached:true` 的直接后果)。 */
54
+ readonly pid: number;
55
+ }
56
+ /** 登记一只自起的前台子进程,返回注销用的回执。 */
57
+ export declare function trackHostForegroundChild(pid: number): ForegroundChildTicket;
58
+ /** 注销(`exit` / settle / 领养移交)。按回执身份删 ⇒ 幂等,且**绝不**碰同号的其它登记。 */
59
+ export declare function untrackHostForegroundChild(ticket: ForegroundChildTicket): void;
60
+ /** 一次前台收割的读数。三格分开是**刻意**的:`alreadyDead` 是想要的结果(不是失败),`failed` 是**真的**
61
+ * 没收上来 —— 两者混成一个数就等于把后者静默掉([ref]:这条腿上不许有安静的兜底)。 */
62
+ export interface ForegroundReapReading {
63
+ /** 真发出了杀伤信号的组数。 */
64
+ readonly signalled: number;
65
+ /** `ESRCH` —— 组已经不在了。信号的语义就是「让它死」,它已经死了 ⇒ 记数,不报警。 */
66
+ readonly alreadyDead: number;
67
+ /** 其余拒因,逐条 `pid:code`。本模块没有 logger,所以**交回调用方响亮记下**(它有)。 */
68
+ readonly failed: readonly string[];
69
+ }
70
+ /**
71
+ * 收割簿上全部前台子进程组。返回**发出**信号的读数,不是确认死亡数 —— 确认要等,而本函数的调用点之一
72
+ * 是 uncaught 出口。
73
+ *
74
+ * **同步形、一次到位、不等宽限**:两个调用点(`hardShutdown` / uncaught 双钩)都紧挨着 `process.exit`,
75
+ * `await` 一个宽限梯就是把收割赌在一个永远等不到的续段上。上界 = 簿上条数 × 一次 `kill(-pid)` syscall
76
+ * (簿条数被 host lane 的在飞 turn 数界住),没有定时器、没有 promise。
77
+ *
78
+ * 🔴 **为什么是 `killTreeHard` 而不是 SIGTERM**(codex 对抗复审 [high],亲跑复现后改):软鞍的语义是
79
+ * 「给它一个收尾窗,到点我再升级」,而这两个调用点之后**没有任何人在场**去升级 —— 一个
80
+ * `trap '' TERM` 的组收到 SIGTERM 后 `kill` 不抛(实测 `signalled:1, failed:[]`、随后仍然在跑),于是
81
+ * 「已收割」的读数是**假的**,孤儿照旧永久存活;win32 的软鞍对普通 console tree 更是明确杀不掉任何东西
82
+ * (见 {@link killTreeSoft} 注)。且本仓对**前台 exec** 的既有杀伤姿势(abort / timeout / 流被放弃)
83
+ * 一律就是 `killTreeHard` —— 收割用**同一只**,不新造一种只能兑现一半的姿势(规则集更小,不是更大)。
84
+ * 代价如实写:一条行为良好的命令在这里拿不到收尾窗;它换来的是「引擎死了,它起的命令一定也死」这句话
85
+ * 真的成立。后台腿的宽限梯不受影响(那条腿有自己的属主,且不在本簿里)。
86
+ */
87
+ export declare function reapHostForegroundChildren(): ForegroundReapReading;
88
+ /** 测试面:簿上现存条数(产品面没有消费者 —— 收割腿只读它自己那只 Set)。 */
89
+ export declare function liveHostForegroundChildCountForTest(): number;
51
90
  /**
52
91
  * win32 env-key case collapse (design D1). Windows env keys are case-insensitive; Node hands the
53
92
  * object to CreateProcess as-is, so a case-sensitive merge that produced BOTH `Path` (inherited) and `PATH`
@@ -54,6 +54,38 @@ export function killTreeSoft(pid) {
54
54
  process.kill(-pid, "SIGTERM");
55
55
  }
56
56
  export { killProcessTree };
57
+ const liveHostForegroundGroups = new Set();
58
+ export function trackHostForegroundChild(pid) {
59
+ const ticket = { pid };
60
+ liveHostForegroundGroups.add(ticket);
61
+ return ticket;
62
+ }
63
+ export function untrackHostForegroundChild(ticket) {
64
+ liveHostForegroundGroups.delete(ticket);
65
+ }
66
+ export function reapHostForegroundChildren() {
67
+ let signalled = 0;
68
+ let alreadyDead = 0;
69
+ const failed = [];
70
+ for (const { pid } of liveHostForegroundGroups) {
71
+ try {
72
+ killTreeHard(pid);
73
+ signalled++;
74
+ }
75
+ catch (err) {
76
+ const code = err?.code;
77
+ if (code === "ESRCH")
78
+ alreadyDead++;
79
+ else
80
+ failed.push(`${pid}:${code ?? String(err)}`);
81
+ }
82
+ }
83
+ liveHostForegroundGroups.clear();
84
+ return { signalled, alreadyDead, failed };
85
+ }
86
+ export function liveHostForegroundChildCountForTest() {
87
+ return liveHostForegroundGroups.size;
88
+ }
57
89
  export function collapseWin32EnvKeys(env) {
58
90
  if (!IS_WIN32)
59
91
  return env;
@@ -7,7 +7,7 @@ import { openSync, closeSync, readSync, statSync, truncateSync, unlinkSync, mkdi
7
7
  import { StringDecoder } from "node:string_decoder";
8
8
  import { armPipeDestroyGrace, numEnvOr, SYMLINK_CHAIN_MAX_HOPS } from "./remote-shell.js";
9
9
  import { hostBackgroundShellEnabled, hostExecSpoolEnabled } from "../config.js";
10
- import { resolveHostShell, hostShell, spawnGroupOptions, killTreeHard, killTreeSoft, collapseWin32EnvKeys } from "./host-platform.js";
10
+ import { resolveHostShell, hostShell, spawnGroupOptions, killTreeHard, killTreeSoft, collapseWin32EnvKeys, trackHostForegroundChild, untrackHostForegroundChild } from "./host-platform.js";
11
11
  import { BackgroundShellManager, seedMemStream, feedMemStream, drainMemStream } from "./background-shell-support.js";
12
12
  import { FileError, ExecutionError, RemoteExecutionError, scrubSecretEnv, RollingTailBuffer, markTruncated, SchedulerError, BackgroundShellError, } from "@sema-agent/core";
13
13
  import { recordSecretEnvScrub } from "../observability/secret-env-scrub.js";
@@ -241,9 +241,9 @@ export class RemoteHostExecutionEnv {
241
241
  throw new BackgroundShellError("spawn_failed", e instanceof Error ? e.message : String(e), e);
242
242
  }
243
243
  const pid = child.pid;
244
+ child.on("error", () => ctx.onTerminal(true));
244
245
  if (pid != null) {
245
246
  child.on("close", (code, signal) => ctx.onTerminal(false, code ?? (signal ? 128 + (signalNumber(signal) ?? 9) : 0)));
246
- child.on("error", () => ctx.onTerminal(true));
247
247
  child.unref();
248
248
  }
249
249
  await outFh.close().catch(() => { });
@@ -403,10 +403,15 @@ export class RemoteHostExecutionEnv {
403
403
  env: this.mergeEnv(options?.env),
404
404
  ...spawnGroupOptions(),
405
405
  });
406
+ const ticket = child.pid != null ? trackHostForegroundChild(child.pid) : undefined;
407
+ if (ticket)
408
+ child.once("exit", () => untrackHostForegroundChild(ticket));
406
409
  const finish = (r) => {
407
410
  if (settled)
408
411
  return;
409
412
  settled = true;
413
+ if (ticket)
414
+ untrackHostForegroundChild(ticket);
410
415
  clearTimeout(timer);
411
416
  options?.abortSignal?.removeEventListener("abort", onAbort);
412
417
  removeDetachListener?.();
@@ -455,6 +460,8 @@ export class RemoteHostExecutionEnv {
455
460
  if (forceSettleTimer)
456
461
  clearTimeout(forceSettleTimer);
457
462
  options?.abortSignal?.removeEventListener("abort", onAbort);
463
+ if (ticket)
464
+ untrackHostForegroundChild(ticket);
458
465
  settled = true;
459
466
  resolve(ok({ stdout: seedOut, stderr: seedErr, exitCode: 0, detached: { shellId } }));
460
467
  };
@@ -521,6 +528,7 @@ export class RemoteHostExecutionEnv {
521
528
  const errBuf = new RollingTailBuffer();
522
529
  let settled = false;
523
530
  let handedOver = false;
531
+ let ticket;
524
532
  let pump;
525
533
  let drainTimer;
526
534
  const cursor = { out: 0, err: 0 };
@@ -613,6 +621,8 @@ export class RemoteHostExecutionEnv {
613
621
  clearInterval(pump);
614
622
  pump = undefined;
615
623
  }
624
+ if (ticket)
625
+ untrackHostForegroundChild(ticket);
616
626
  if (drainTimer) {
617
627
  clearTimeout(drainTimer);
618
628
  drainTimer = undefined;
@@ -665,6 +675,8 @@ export class RemoteHostExecutionEnv {
665
675
  cleanupSpool();
666
676
  throw e;
667
677
  }
678
+ if (child.pid != null)
679
+ ticket = trackHostForegroundChild(child.pid);
668
680
  closeSpoolFds();
669
681
  child.on("error", (e) => {
670
682
  finish({ ok: false, error: new ExecutionError("spawn_error", `host spawn failed: ${e.message}`) });
@@ -753,6 +765,8 @@ export class RemoteHostExecutionEnv {
753
765
  settleFromTermination(code, signal);
754
766
  });
755
767
  child.on("exit", (code, signal) => {
768
+ if (ticket)
769
+ untrackHostForegroundChild(ticket);
756
770
  exited = true;
757
771
  drainTimer = setTimeout(() => settleFromTermination(code, signal), EXIT_SETTLE_DRAIN_MS);
758
772
  drainTimer.unref?.();
@@ -797,6 +811,9 @@ export class RemoteHostExecutionEnv {
797
811
  const signalReady = () => { const w = wake; wake = undefined; w?.(); };
798
812
  const sh = hostShell();
799
813
  const child = spawn(sh.shell, [...sh.args, command], { cwd, env: this.mergeEnv(options?.env), ...spawnGroupOptions() });
814
+ const ticket = child.pid != null ? trackHostForegroundChild(child.pid) : undefined;
815
+ if (ticket)
816
+ child.once("exit", () => untrackHostForegroundChild(ticket));
800
817
  let pipeDestroyTimer;
801
818
  const kill = () => {
802
819
  try {
@@ -831,6 +848,8 @@ export class RemoteHostExecutionEnv {
831
848
  child.on("close", (code, signal) => {
832
849
  if (pipeDestroyTimer)
833
850
  clearTimeout(pipeDestroyTimer);
851
+ if (ticket)
852
+ untrackHostForegroundChild(ticket);
834
853
  if (finished)
835
854
  return;
836
855
  exitCode = code ?? (signal ? 128 + (signalNumber(signal) ?? 0) : 1);
@@ -906,6 +925,8 @@ export class RemoteHostExecutionEnv {
906
925
  if (wallTimer)
907
926
  clearTimeout(wallTimer);
908
927
  options?.signal?.removeEventListener("abort", onAbort);
928
+ if (ticket)
929
+ untrackHostForegroundChild(ticket);
909
930
  }
910
931
  }
911
932
  async absolutePath(p) {
package/dist/run-local.js CHANGED
@@ -42,6 +42,7 @@ import { createOrgMemoryAdmissionWiring } from "./boot/org-memory.js";
42
42
  import { createPermissionDeniedMeter } from "./observability/tool-trace.js";
43
43
  import { createKeyResolver } from "./key-resolver.js";
44
44
  import { createLogger } from "./observability/logger.js";
45
+ import { fatalExit } from "./boot/fatal-exit.js";
45
46
  import { createMetrics } from "./observability/metrics.js";
46
47
  import { installSecretEnvScrubRecorder } from "./observability/secret-env-scrub.js";
47
48
  import { buildDisplaySafeUrl } from "./trace/redact.js";
@@ -575,9 +576,6 @@ function isMainModule() {
575
576
  }
576
577
  }
577
578
  if (isMainModule()) {
578
- void main().catch((err) => {
579
- createLogger().error("fatal", { err: String(err) });
580
- process.exit(1);
581
- });
579
+ void main().catch((err) => fatalExit(err));
582
580
  }
583
581
  //# sourceMappingURL=run-local.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "7.93.1",
3
+ "version": "7.93.2",
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",