/** * [ref] — shared support for the remote (TOB) `BackgroundShellCapability` implementations. * * core defines the seam (`@sema-agent/core` `BackgroundShellCapability` + `hasBackgroundShell`) and ships the TOC * reference (`NodeExecutionEnv`). This module centralises the **security + lifecycle red-lines** that EVERY remote * adapter (E2B, kata/k8s, …) must enforce identically, so a transport author only writes the 4 transport * primitives and cannot accidentally re-implement (or skip) an invariant: * * - **opaque shellId** — a fresh `bg__` brand, NEVER derived from the provider's job/pid (超范围红线, * [ref] §3.8). The provider id lives only inside the driver state. * - **owned-id isolation** — `poll`/`kill` look the shellId up in THIS manager's registry; an id from another env * (or a forged one) is `not_found`, never resolved against the provider ([ref] §3.8 / interface 超范围契约). * - **maxConcurrent** — counts only `running` shells (terminal entries are kept for residual polling but free their * slot, so "TaskStop one first" is actionable — matches the TOC `maxConcurrent 终态占槽` review fix). * - **timeout hard wall** — a control-plane timer kills the shell and flips it to `killed` at the bounded BG * timeout ([ref] §3.6); the driver may set an additional provider-level backstop ≥ this wall. * - **dispose** — kills + cleans up EVERY shell, best-effort, MUST NOT throw, idempotent ([ref] §3.7). * * The two transports legitimately take different "按-id-可重读" paths ([ref] §5.2): E2B holds a resident * `CommandHandle` reader feeding a control-plane tail buffer (path ②); kata/k8s buffers into pod-side files * re-read by byte cursor on each poll (path ①). Both are expressed through {@link BackgroundShellDriver}. */ import { BackgroundShellError } from "@sema-agent/core"; import type { BackgroundShellId, BackgroundPoll, BackgroundSpawnOptions, Result } from "@sema-agent/core"; import { StringDecoder } from "node:string_decoder"; /** Per-端 background capability bounds, surfaced as `backgroundCapabilities` on the env ([ref] §3.1/§3.6). * * ⚠️ 有据不接:core 契约的 `retainBackgroundProcesses` 能力字段【有意】不在此声明(2026-07-08 定谳): * TB 的声明与兑现全在 core 自己 runtime 内闭环(sema-tb bin 进程内 TOC local env 声明,core task-registry * settle/reap 双门控读取)——service env 全程不在链上;k8s/e2b 隔离 env 拆机进程必然消亡,声明只是名义。 * 接单信号=「远端 TB / 常驻服务题落到 service host env」,届时切片很薄(dispose `{except}` 管道已通, * retain 只是 env 级常开白名单)。 */ export interface BackgroundCaps { readonly supported: boolean; readonly maxConcurrent: number; readonly defaultBgTimeoutSec: number; readonly maxBgTimeoutSec: number; } /** One stream's adopt-mode drain state. `pendingBytes` = queued-undrained (≤ cap); `totalBytes` = cursor metadata. */ export interface BgMemStream { chunks: string[]; pendingBytes: number; totalBytes: number; droppedBytes: number; /** Per-stream stateful UTF-8 decoder for Buffer inputs (WS frames / pipe chunks split at arbitrary byte offsets — * a multibyte codepoint straddling two chunks must NOT decode to U+FFFD; the incomplete tail rides to the next * write). Mirrors the fg-path `StringDecoder` (remote-env-k8s.ts runExec, council BUG#1). Undefined until first * Buffer feed. STRING inputs (E2B-style, already decoded by the SDK) bypass it. */ decoder?: StringDecoder; } /** Fresh drain buffer pre-seeded with the output captured before adoption (rides the first poll). `initialDropped` = * bytes the fg segment's rolling-tail already evicted (surfaced as `truncated` on the first drain — the seed text is * RAW, so the truncation marker is NOT baked into the stream content: review LOW, avoids double-marking + accounting * pollution). */ export declare function seedMemStream(text: string, initialDropped?: number): BgMemStream; /** Append a live chunk, evicting the OLDEST queued chunks beyond `cap` (counted → surfaced as `truncated`). A Buffer * goes through the per-stream StringDecoder (multibyte-safe across chunk boundaries); a string passes through. */ export declare function feedMemStream(s: BgMemStream, d: Buffer | string, cap: number): void; /** Drain everything queued since the last read (a driver `read`); resets the pending queue + dropped counter. */ export declare function drainMemStream(s: BgMemStream): { text: string; dropped: number; }; /** Context handed to {@link BackgroundShellDriver.launch}. */ export interface LaunchCtx { /** * Push-model terminal notification (E2B `CommandHandle.wait()` resolves/rejects): the driver invokes this exactly * once when the process ends. Pull-model drivers (k8s, which discovers termination during {@link * BackgroundShellDriver.read}) simply never call it and return `terminal` from `read` instead. The manager * ignores either source once a shell has left `running` (kill/timeout win). */ onTerminal(failed: boolean, exitCode?: number): void; /** Bounded effective BG timeout (sec) the driver MAY use as a provider-level backstop. ≥ the manager hard wall. */ readonly bgTimeoutSec: number; } /** One poll's worth of driver output: raw increment since the driver's own cursor + head-evict accounting. */ export interface DriverPoll { /** New stdout since last read (RAW — core's `BashOutput` tool applies `filter` + display truncation). */ stdout: string; /** New stderr since last read. */ stderr: string; /** Total output bytes the process has produced from start (cursor metadata; [ref] §3.4). */ bytesFromStart: number; /** Bytes permanently lost to head-eviction before this read's cursor (0 if none). */ bytesDroppedBeforeCursor: number; /** * Pull-model termination signal (k8s): the process is no longer running. `undefined` ⇒ still running / unknown. * Push-model drivers (E2B) leave this `undefined` and use {@link LaunchCtx.onTerminal} instead. */ terminal?: { failed: boolean; exitCode?: number; }; } /** * Transport primitives a remote adapter implements. The driver owns its per-shell state `S` (handles, tail buffers, * pod-side cursors) and the transport mechanics; the {@link BackgroundShellManager} owns all the shared invariants. */ export interface BackgroundShellDriver { /** Launch a detached process. Return per-shell transport state. MUST NOT block on exit. Throw `BackgroundShellError` on failure. */ launch(command: string, options: BackgroundSpawnOptions | undefined, ctx: LaunchCtx): Promise; /** Read incremental output (+ maybe terminal). Safe to call repeatedly across independent poll calls. */ read(state: S): Promise; /** Transport-level kill (best-effort, idempotent — killing an already-dead process is a no-op). */ kill(state: S): Promise; /** Best-effort cleanup of ONE shell's transport resources (kill + remove temp dirs/handles). Should not throw. */ disposeOne(state: S): Promise; } export declare class BackgroundShellManager { private readonly driver; readonly caps: BackgroundCaps; private readonly shells; private counter; constructor(driver: BackgroundShellDriver, caps: BackgroundCaps); /** [ref] 六轮复审:destroy 端判「还有 shell 依赖 spool 文件吗」——running(驻留 keep-alive)与 * terminal 残余(dispose 前仍可轮询)都算;有=env 不得整树删 spool 根(逐 shell 清理归 dispose 面)。 */ hasShells(): boolean; /** Count live shells; KillShell/timeout flip a shell out of `running` and free its slot. */ private liveCount; /** Bound heap: evict the OLDEST terminal entries (never a running one) once the registry exceeds the retention cap. */ private evictOldTerminal; spawn(command: string, options?: BackgroundSpawnOptions): Promise>; /** * [ref] detach (clay 排,1.81): adopt an EXTERNALLY-created RUNNING process as a background shell — the * mid-flight ctrl+b path, where the process was spawned by a FOREGROUND `exec` and must move under this * manager's ownership instead of being killed. The `builder` receives the same {@link LaunchCtx} a driver * `launch` would (terminal notification + the bounded BG timeout it may use as a backstop) and returns the * driver state the normal `read`/`kill`/`disposeOne` primitives operate on — after adoption the shell is * indistinguishable from a spawned one (poll/kill/dispose/timeout wall all apply). Returns `undefined` when * the live-shell limit is hit OR the builder throws: detach REFUSED, the caller keeps the process running in * the foreground (core NodeExecutionEnv semantics — never a silently dropped process). * * 🔴 SYNCHRONOUS by contract (double-review of the first async cut, 3 confirmed findings): the builder must * attach the live child's pipe listeners in the SAME synchronous tick it snapshots the seed, with NO `await` * before registration — otherwise (a) output produced during an async open/mkdir window is lost, (b) file fds * leak on natural exit, and (c) an exec-settle can race the async adoption and orphan a shell. A sync builder * (memory-buffered, no files) closes all three: no Node data/close event can interleave a synchronous block. * * 🔴 builder side-effect contract (Fable-5 review #5): the catch below returns undefined WITHOUT undoing any * side effects the builder already performed (listener attach, stream reroute). A builder must therefore be * all-or-nothing: put every operation that can throw BEFORE the first side effect, or clean up on the throw * path itself. Today's three lane builders are entirely non-throwing (listener attach + object construction). */ /** [ref]-T0:两条注册腿共用的 BG 超时钳制。非有限值(NaN/±Infinity)当缺席回退 default—— * Math.max/min 对 NaN 全塌 NaN,setTimeout(NaN) 被 Node 折成 1ms = 后台 shell 秒杀 * (方向反转:想给超时变成即杀;同族判例 TASK_TIMEOUT_SEC / leader 旋钮非法值)。 */ private boundedBgTimeoutSec; adoptSync(builder: (ctx: LaunchCtx) => S, timeoutSec?: number): { shellId: BackgroundShellId; } | undefined; /** The shared spawn/adopt body: limit check, opaque id, terminal-latch buffering, BG-timeout hard wall. */ private register; poll(shellId: BackgroundShellId): Promise>; kill(shellId: BackgroundShellId): Promise>; /** Kill + clean up EVERY shell. Best-effort, MUST NOT throw, idempotent ([ref] §3.7). * * core `except` 契约(background-shell.d.ts:core runtask 尾调 `{except: keepAlive}`): * 这些 shellId **留活**——session 驻留 persistent Monitor 的进程,其全部意义就是跨 turn 存活;run-end * 全灭会留下「registry handle 活着、进程死了」的孤儿 watch。留活条目连注册表一起保留(poll/kill 继续 * 可用,timeout 墙钟 timer 不清=到点照杀)。无参=既有全灭行为(suspend/review 前的 dispose 不带 except)。 */ dispose(opts?: { except?: readonly BackgroundShellId[]; }): Promise; } //# sourceMappingURL=background-shell-support.d.ts.map