@cjhyy/code-shell-server 0.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.
Files changed (70) hide show
  1. package/README.md +77 -0
  2. package/dist/attachment-service.d.ts +80 -0
  3. package/dist/attachment-service.js +587 -0
  4. package/dist/bin/code-shell-serve.d.ts +2 -0
  5. package/dist/bin/code-shell-serve.js +7 -0
  6. package/dist/client-message-id.d.ts +1 -0
  7. package/dist/client-message-id.js +8 -0
  8. package/dist/image-byte-probe.d.ts +17 -0
  9. package/dist/image-byte-probe.js +330 -0
  10. package/dist/index.d.ts +10 -0
  11. package/dist/index.js +10 -0
  12. package/dist/index.mobile-remote.d.ts +21 -0
  13. package/dist/index.mobile-remote.js +21 -0
  14. package/dist/index.serve.d.ts +3 -0
  15. package/dist/index.serve.js +3 -0
  16. package/dist/index.storage.d.ts +10 -0
  17. package/dist/index.storage.js +10 -0
  18. package/dist/index.worker.d.ts +2 -0
  19. package/dist/index.worker.js +2 -0
  20. package/dist/mobile-remote/access-passcode.d.ts +80 -0
  21. package/dist/mobile-remote/access-passcode.js +354 -0
  22. package/dist/mobile-remote/cloudflared-binary.d.ts +76 -0
  23. package/dist/mobile-remote/cloudflared-binary.js +242 -0
  24. package/dist/mobile-remote/codex-parse.d.ts +34 -0
  25. package/dist/mobile-remote/codex-parse.js +131 -0
  26. package/dist/mobile-remote/codex-room-agent.d.ts +94 -0
  27. package/dist/mobile-remote/codex-room-agent.js +213 -0
  28. package/dist/mobile-remote/mobile-attachments.d.ts +21 -0
  29. package/dist/mobile-remote/mobile-attachments.js +119 -0
  30. package/dist/mobile-remote/mobile-chat-turn.d.ts +32 -0
  31. package/dist/mobile-remote/mobile-chat-turn.js +67 -0
  32. package/dist/mobile-remote/mobile-history.d.ts +22 -0
  33. package/dist/mobile-remote/mobile-history.js +27 -0
  34. package/dist/mobile-remote/mobile-run-dispatch.d.ts +17 -0
  35. package/dist/mobile-remote/mobile-run-dispatch.js +56 -0
  36. package/dist/mobile-remote/mobile-static.d.ts +43 -0
  37. package/dist/mobile-remote/mobile-static.js +154 -0
  38. package/dist/mobile-remote/mobile-upload-service.d.ts +57 -0
  39. package/dist/mobile-remote/mobile-upload-service.js +370 -0
  40. package/dist/mobile-remote/pairing.d.ts +8 -0
  41. package/dist/mobile-remote/pairing.js +23 -0
  42. package/dist/mobile-remote/path-bins.d.ts +7 -0
  43. package/dist/mobile-remote/path-bins.js +16 -0
  44. package/dist/mobile-remote/pending-approvals.d.ts +14 -0
  45. package/dist/mobile-remote/pending-approvals.js +54 -0
  46. package/dist/mobile-remote/remote-host-manager.d.ts +117 -0
  47. package/dist/mobile-remote/remote-host-manager.js +345 -0
  48. package/dist/mobile-remote/resident-agent.d.ts +98 -0
  49. package/dist/mobile-remote/resident-agent.js +216 -0
  50. package/dist/mobile-remote/room-manager.d.ts +286 -0
  51. package/dist/mobile-remote/room-manager.js +765 -0
  52. package/dist/mobile-remote/trusted-device-store.d.ts +19 -0
  53. package/dist/mobile-remote/trusted-device-store.js +118 -0
  54. package/dist/mobile-remote/tunnel-manager.d.ts +105 -0
  55. package/dist/mobile-remote/tunnel-manager.js +375 -0
  56. package/dist/mobile-remote/types.d.ts +1 -0
  57. package/dist/mobile-remote/types.js +1 -0
  58. package/dist/mobile-remote/viewer-identity.d.ts +5 -0
  59. package/dist/mobile-remote/viewer-identity.js +5 -0
  60. package/dist/serve/cli.d.ts +15 -0
  61. package/dist/serve/cli.js +115 -0
  62. package/dist/serve/headless-server.d.ts +40 -0
  63. package/dist/serve/headless-server.js +276 -0
  64. package/dist/sessions-service.d.ts +73 -0
  65. package/dist/sessions-service.js +303 -0
  66. package/dist/static-files.d.ts +1 -0
  67. package/dist/static-files.js +25 -0
  68. package/dist/worker-bridge-core.d.ts +154 -0
  69. package/dist/worker-bridge-core.js +315 -0
  70. package/package.json +63 -0
@@ -0,0 +1,303 @@
1
+ /**
2
+ * List session files persisted by the agent worker.
3
+ * Uses core's canonical session root so CODE_SHELL_HOME matches the worker.
4
+ */
5
+ import { SessionManager, sessionsRoot } from "@cjhyy/code-shell-core";
6
+ import * as fs from "node:fs/promises";
7
+ import * as path from "node:path";
8
+ const SAFE_ID = /^[A-Za-z0-9_.-]+$/;
9
+ const QUICK_CHAT_SESSION_PREFIX = "qchat-";
10
+ function isQuickChatSessionId(id) {
11
+ return id.startsWith(QUICK_CHAT_SESSION_PREFIX);
12
+ }
13
+ export async function listSessions(baseDir = sessionsRoot()) {
14
+ try {
15
+ const entries = await fs.readdir(baseDir, { withFileTypes: true });
16
+ const summaries = [];
17
+ for (const e of entries) {
18
+ if (!e.isFile())
19
+ continue;
20
+ if (!e.name.endsWith(".jsonl") && !e.name.endsWith(".json"))
21
+ continue;
22
+ const id = e.name.replace(/\.jsonl?$/, "");
23
+ if (isQuickChatSessionId(id))
24
+ continue;
25
+ const full = path.join(baseDir, e.name);
26
+ try {
27
+ const st = await fs.stat(full);
28
+ summaries.push({
29
+ id,
30
+ file: full,
31
+ size: st.size,
32
+ createdAt: st.birthtimeMs || st.mtimeMs,
33
+ updatedAt: st.mtimeMs,
34
+ });
35
+ }
36
+ catch {
37
+ // Skip files we cannot stat (race with delete, permissions).
38
+ }
39
+ }
40
+ summaries.sort((a, b) => b.updatedAt - a.updatedAt);
41
+ return summaries;
42
+ }
43
+ catch (e) {
44
+ if (e.code === "ENOENT")
45
+ return [];
46
+ throw e;
47
+ }
48
+ }
49
+ /**
50
+ * Remove a session's on-disk data. Modern sessions are a directory
51
+ * `<sessionsDir>/<id>/` (transcript.jsonl + state.json — see core
52
+ * session-manager.ts:93,141). Pre-directory sessions were flat
53
+ * `<id>.jsonl`/`<id>.json` files; we remove those too for back-compat.
54
+ * `baseDir` is overridable for tests. Unsafe ids (slashes / "." / "..")
55
+ * are rejected without any filesystem access.
56
+ */
57
+ export async function deleteSessionDir(id, baseDir = sessionsRoot()) {
58
+ if (!SAFE_ID.test(id) || id === "." || id === "..")
59
+ return;
60
+ // Directory form (current).
61
+ await fs.rm(path.join(baseDir, id), { recursive: true, force: true });
62
+ // Legacy flat files.
63
+ for (const ext of [".jsonl", ".json"]) {
64
+ try {
65
+ await fs.unlink(path.join(baseDir, `${id}${ext}`));
66
+ }
67
+ catch (e) {
68
+ if (e.code !== "ENOENT")
69
+ throw e;
70
+ }
71
+ }
72
+ }
73
+ /** @deprecated retained for the existing IPC handler; delegates to deleteSessionDir. */
74
+ export async function deleteSession(id) {
75
+ await deleteSessionDir(id);
76
+ }
77
+ /**
78
+ * Reap quick-chat data left by an unclean prior desktop exit. This must run
79
+ * before a new renderer/worker can claim a qchat id; normal live cleanup is
80
+ * coordinated by QuickChatOwnershipRegistry instead.
81
+ */
82
+ export async function cleanupStaleQuickChatSessions(baseDir = sessionsRoot()) {
83
+ let entries;
84
+ try {
85
+ entries = await fs.readdir(baseDir, { withFileTypes: true });
86
+ }
87
+ catch (e) {
88
+ if (e.code === "ENOENT")
89
+ return [];
90
+ throw e;
91
+ }
92
+ const ids = new Set();
93
+ const stagingDirs = [];
94
+ for (const entry of entries) {
95
+ if (entry.isDirectory() && SAFE_ID.test(entry.name) && isQuickChatSessionId(entry.name)) {
96
+ ids.add(entry.name);
97
+ continue;
98
+ }
99
+ if (entry.isDirectory() && entry.name.startsWith(".pending-fork-")) {
100
+ try {
101
+ const state = JSON.parse(await fs.readFile(path.join(baseDir, entry.name, "state.json"), "utf8"));
102
+ if (state.ephemeral === true &&
103
+ typeof state.sessionId === "string" &&
104
+ isQuickChatSessionId(state.sessionId)) {
105
+ stagingDirs.push(entry.name);
106
+ }
107
+ }
108
+ catch {
109
+ // Missing, malformed, or unreadable state cannot prove ephemeral
110
+ // ownership, so fail closed and leave the staging directory intact.
111
+ }
112
+ continue;
113
+ }
114
+ if (!entry.isFile() || (!entry.name.endsWith(".jsonl") && !entry.name.endsWith(".json"))) {
115
+ continue;
116
+ }
117
+ const id = entry.name.replace(/\.jsonl?$/, "");
118
+ if (SAFE_ID.test(id) && isQuickChatSessionId(id))
119
+ ids.add(id);
120
+ }
121
+ for (const id of ids)
122
+ await deleteSessionDir(id, baseDir);
123
+ for (const stagingDir of stagingDirs) {
124
+ await fs.rm(path.join(baseDir, stagingDir), { recursive: true, force: true });
125
+ }
126
+ return [...ids, ...stagingDirs];
127
+ }
128
+ function encodeDiskSessionCursor(cursor) {
129
+ return `v1:${cursor.mtime}:${cursor.id}`;
130
+ }
131
+ function decodeDiskSessionCursor(value) {
132
+ if (!value)
133
+ return 0;
134
+ // Keep accepting the old numeric offset during upgrades. New responses use
135
+ // a stable tuple so deleting an item from an earlier page cannot shift and
136
+ // skip a later session.
137
+ if (/^\d+$/.test(value))
138
+ return Number(value);
139
+ const match = /^v1:([^:]+):([A-Za-z0-9._-]+)$/.exec(value);
140
+ if (!match)
141
+ return null;
142
+ const mtime = Number(match[1]);
143
+ return Number.isFinite(mtime) ? { mtime, id: match[2] } : null;
144
+ }
145
+ /**
146
+ * List top-level (non-sub-agent) sessions from disk, newest first, paginated.
147
+ * Filter on state.json `parentSessionId`:
148
+ * - key absent → legacy → skip (存量 not auto-rebuilt)
149
+ * - null / "" → top-level → show
150
+ * - non-empty → sub-agent → filter out
151
+ */
152
+ export async function listDiskSessions(opts, baseDir = sessionsRoot()) {
153
+ // Async fs throughout: this is an IPC handler on the Electron main thread
154
+ // (sessions:listDisk). The previous synchronous statSync/readFileSync loops
155
+ // never yielded the event loop and froze the UI while enumerating sessions.
156
+ let entries;
157
+ try {
158
+ entries = await fs.readdir(baseDir, { withFileTypes: true });
159
+ }
160
+ catch (e) {
161
+ if (e.code === "ENOENT")
162
+ return { sessions: [], nextCursor: null };
163
+ throw e;
164
+ }
165
+ const candidates = entries.filter((e) => e.isDirectory() && SAFE_ID.test(e.name) && !isQuickChatSessionId(e.name));
166
+ const stats = await Promise.all(candidates.map(async (e) => {
167
+ try {
168
+ return { id: e.name, mtime: (await fs.stat(path.join(baseDir, e.name))).mtimeMs };
169
+ }
170
+ catch {
171
+ return null; // skip unreadable
172
+ }
173
+ }));
174
+ const dirs = stats.filter((d) => d !== null);
175
+ dirs.sort((a, b) => b.mtime - a.mtime || a.id.localeCompare(b.id));
176
+ const decodedCursor = decodeDiskSessionCursor(opts.cursor);
177
+ if (decodedCursor === null)
178
+ return { sessions: [], nextCursor: null };
179
+ const start = typeof decodedCursor === "number"
180
+ ? Math.max(0, decodedCursor)
181
+ : (() => {
182
+ const index = dirs.findIndex((entry) => entry.mtime < decodedCursor.mtime ||
183
+ (entry.mtime === decodedCursor.mtime && entry.id > decodedCursor.id));
184
+ return index < 0 ? dirs.length : index;
185
+ })();
186
+ const sessions = [];
187
+ // Cache cwd existence across the loop — many sessions share one project root,
188
+ // and we must not re-stat it per session (nor block on a sync existsSync,
189
+ // which the async-throughout rewrite above was meant to eliminate).
190
+ const cwdExists = new Map();
191
+ const pathExists = async (cwdStr) => {
192
+ const cached = cwdExists.get(cwdStr);
193
+ if (cached !== undefined)
194
+ return cached;
195
+ const ok = await fs
196
+ .access(cwdStr)
197
+ .then(() => true)
198
+ .catch(() => false);
199
+ cwdExists.set(cwdStr, ok);
200
+ return ok;
201
+ };
202
+ let i = start;
203
+ for (; i < dirs.length && sessions.length < opts.limit; i++) {
204
+ const { id, mtime } = dirs[i];
205
+ let state;
206
+ try {
207
+ state = JSON.parse(await fs.readFile(path.join(baseDir, id, "state.json"), "utf8"));
208
+ }
209
+ catch {
210
+ continue;
211
+ }
212
+ if (state.ephemeral === true)
213
+ continue;
214
+ if (state.kind === "pet")
215
+ continue;
216
+ if (!("parentSessionId" in state))
217
+ continue; // legacy → skip
218
+ if (state.parentSessionId)
219
+ continue; // sub-agent (non-empty) → filter
220
+ // Only desktop + automation belong in the desktop sidebar. tui / missing
221
+ // origin are filtered out (tui shares ~/.code-shell; legacy has no origin).
222
+ const origin = state.origin;
223
+ if (origin !== "desktop" && origin !== "automation")
224
+ continue;
225
+ // Skip sessions whose project root has been deleted. Listing them led the
226
+ // renderer's disk-rebuild to call createRepoForCwd → the deleted project
227
+ // reappeared in the sidebar, and any subsequent write under that cwd
228
+ // resurrected it on disk as an empty shell. An empty cwd (no-repo session)
229
+ // is intentionally NOT filtered here — the renderer handles that via
230
+ // isNoRepoCwd. (deleted-project resurrection)
231
+ const cwdStr = typeof state.cwd === "string" ? state.cwd : "";
232
+ if (cwdStr && !(await pathExists(cwdStr)))
233
+ continue;
234
+ // Archived sessions are hidden from the default catalog. Filtering happens
235
+ // before the push (like every other skip above), so the mtime cursor —
236
+ // derived from the dirs[] position, not from how many rows we kept — keeps
237
+ // advancing across archived rows: a page may return fewer than `limit`
238
+ // sessions, but no live session is skipped and the cursor never stalls.
239
+ const archivedAt = typeof state.archivedAt === "number" ? state.archivedAt : undefined;
240
+ if (archivedAt !== undefined && !opts.includeArchived)
241
+ continue;
242
+ sessions.push({
243
+ id,
244
+ engineSessionId: id,
245
+ cwd: typeof state.cwd === "string" ? state.cwd : "",
246
+ // Prefer the LLM-generated title (now persisted to state); fall back to
247
+ // the raw first-message summary, then the id. This lets the disk rebuild
248
+ // surface the real title after a localStorage wipe.
249
+ title: (typeof state.title === "string" && state.title ? state.title : undefined) ??
250
+ (typeof state.summary === "string" && state.summary ? state.summary : id),
251
+ updatedAt: mtime,
252
+ origin,
253
+ status: state.status === "active" || state.status === "paused" || state.status === "completed"
254
+ ? state.status
255
+ : state.status === "aborted_streaming" || state.status === "aborted_tools"
256
+ ? "cancelled"
257
+ : typeof state.status === "string"
258
+ ? "failed"
259
+ : undefined,
260
+ ...(state.status === "completed" &&
261
+ (state.lastCompletionKind === "background_wait" ||
262
+ state.lastCompletionKind === "goal_control_stop" ||
263
+ state.lastCompletionKind === "limit_stop")
264
+ ? { completionKind: state.lastCompletionKind }
265
+ : {}),
266
+ ...(archivedAt !== undefined ? { archivedAt } : {}),
267
+ ...(typeof state.workspaceProfile === "string" && state.workspaceProfile
268
+ ? { workspaceProfile: state.workspaceProfile }
269
+ : {}),
270
+ });
271
+ }
272
+ return {
273
+ sessions,
274
+ nextCursor: i < dirs.length && i > 0 ? encodeDiskSessionCursor(dirs[i - 1]) : null,
275
+ };
276
+ }
277
+ /**
278
+ * Set (number) or clear (undefined) a session's durable archival marker on
279
+ * disk, then atomically replace `state.json`. This is the main-process writer
280
+ * counterpart to `listDiskSessions`' `archivedAt` filter: main has no live
281
+ * Engine/SessionManager handle, so it mutates the same `state.json` the worker
282
+ * persists. The rest of the state is preserved verbatim; only `archivedAt` is
283
+ * added or removed. Unsafe ids and missing sessions throw.
284
+ */
285
+ export async function archiveDiskSession(id, archivedAt, baseDir = sessionsRoot()) {
286
+ if (!SAFE_ID.test(id) || id === "." || id === "..") {
287
+ throw new Error(`archiveDiskSession: unsafe session id ${JSON.stringify(id)}`);
288
+ }
289
+ // Route through Core's SessionManager instead of hand-rolling the write.
290
+ //
291
+ // This used to read the whole state.json, set/delete `archivedAt`, and rename a
292
+ // full replacement over it — with no lock and no revision check. A live worker
293
+ // saving token usage, goal, workspace, title or status at the same instant
294
+ // meant two full snapshots racing, and only one survived: archiving could roll
295
+ // back a just-saved field, or a worker save could erase `archivedAt`.
296
+ //
297
+ // `setSessionArchived` → `updateSessionState` already does this correctly: it
298
+ // takes the per-session lock, compares `stateRevision`, re-reads and merges at
299
+ // field level on a CAS miss, and retries. Archiving is a field-level update, so
300
+ // it belongs on exactly that path.
301
+ const manager = new SessionManager(baseDir);
302
+ manager.setSessionArchived(id, archivedAt);
303
+ }
@@ -0,0 +1 @@
1
+ export declare function contentTypeFor(extname: string): string;
@@ -0,0 +1,25 @@
1
+ // packages/server/src/static-files.ts
2
+ //
3
+ // Shared static-file primitives for the two HTTP hosts (pairing host's
4
+ // /mobile static root and the headless serve SPA root). Keep this file free
5
+ // of host wiring — MIME map + helpers only.
6
+ const CONTENT_TYPES = {
7
+ ".html": "text/html; charset=utf-8",
8
+ ".js": "text/javascript; charset=utf-8",
9
+ ".mjs": "text/javascript; charset=utf-8",
10
+ ".css": "text/css; charset=utf-8",
11
+ ".json": "application/json; charset=utf-8",
12
+ ".svg": "image/svg+xml",
13
+ ".png": "image/png",
14
+ ".jpg": "image/jpeg",
15
+ ".jpeg": "image/jpeg",
16
+ ".webp": "image/webp",
17
+ ".ico": "image/x-icon",
18
+ ".map": "application/json",
19
+ ".woff": "font/woff",
20
+ ".woff2": "font/woff2",
21
+ ".ttf": "font/ttf",
22
+ };
23
+ export function contentTypeFor(extname) {
24
+ return CONTENT_TYPES[extname.toLowerCase()] ?? "application/octet-stream";
25
+ }
@@ -0,0 +1,154 @@
1
+ /** Truncate a wire line for logs. */
2
+ export declare function previewLine(s: string, max?: number): string;
3
+ export type WorkerBridgeLog = (event: string, data?: Record<string, unknown>) => void;
4
+ export interface WorkerExitInfo {
5
+ code: number | null;
6
+ signal: NodeJS.Signals | null;
7
+ /** true only for code === 0 && signal === null (normal completion). */
8
+ clean: boolean;
9
+ /** true when a crash pushed past the restart limit inside the window. */
10
+ gaveUp: boolean;
11
+ }
12
+ /** Outcome of a correlated JSON-RPC request. Never rejects — callers map. */
13
+ export type WorkerRpcOutcome = {
14
+ status: "result";
15
+ result: unknown;
16
+ } | {
17
+ status: "error";
18
+ error: {
19
+ message?: string;
20
+ code?: number;
21
+ };
22
+ } | {
23
+ status: "timeout";
24
+ }
25
+ /** Sending the frame failed (no live worker, or the write threw). */
26
+ | {
27
+ status: "sendFailed";
28
+ error?: unknown;
29
+ }
30
+ /** Worker exited while pending (only for `settleOnExit: true` requests). */
31
+ | {
32
+ status: "workerExit";
33
+ };
34
+ export interface WorkerRequestOptions {
35
+ /** Wire id for the frame. Caller mints it (must be unique while pending). */
36
+ id: string;
37
+ timeoutMs: number;
38
+ /**
39
+ * true: the matching response line is consumed here and NOT dispatched to
40
+ * line listeners (host-internal RPC, e.g. pet snapshots). false: the line
41
+ * is settled AND still dispatched, so downstream mirrors see it unchanged.
42
+ */
43
+ consume?: boolean;
44
+ /** Settle with { status: "workerExit" } when the worker dies while pending. */
45
+ settleOnExit?: boolean;
46
+ /** Settle with { status: "sendFailed" } immediately when the frame can't be
47
+ * written; otherwise a failed send leaves the request pending until the
48
+ * timeout (matches fire-and-forget inject semantics). */
49
+ failFast?: boolean;
50
+ /** Spawn the worker on demand (idempotent) before writing the frame. Set for
51
+ * requests that are meant to WAKE the worker — e.g. a pet/IM-gateway
52
+ * `agent/run` — mirroring the renderer's spawn-on-`agent/run` path. Without
53
+ * it a request to a lazily-unspawned worker is dropped and hangs to timeout.
54
+ * Optionally pass the cwd the fresh worker should use. */
55
+ ensureWorker?: boolean;
56
+ /** cwd handed to ensureWorker() when it spawns. Ignored unless ensureWorker. */
57
+ ensureWorkerCwd?: string;
58
+ }
59
+ export interface WorkerBridgeCoreOptions {
60
+ /** Absolute path of the worker entry script (agent-server-stdio). */
61
+ entryPath: string;
62
+ /** Runtime binary; defaults to process.execPath. */
63
+ execPath?: string;
64
+ /** Build the spawn env (called per spawn). Defaults to process.env. */
65
+ buildEnv?: () => NodeJS.ProcessEnv;
66
+ /** cwd used when ensureWorker() is called without one. */
67
+ fallbackCwd?: () => string;
68
+ log?: WorkerBridgeLog;
69
+ /**
70
+ * Rewrite/side-effect hook for injectWorkerMessage lines (e.g. agent/run
71
+ * trust metadata + on-demand spawn). Returns the line to write; `method`
72
+ * is only used for log context.
73
+ */
74
+ prepareInbound?: (line: string) => {
75
+ line: string;
76
+ method?: string;
77
+ };
78
+ /** A worker successfully spawned with piped stdio. */
79
+ onWorkerStarted?: (info: {
80
+ generation: number;
81
+ pid?: number;
82
+ }) => void;
83
+ /** spawn() threw synchronously, or stdio came back unpiped. No child. */
84
+ onSpawnFailed?: () => void;
85
+ /** The child emitted 'error' (ENOENT/EACCES/…) — no 'exit' will follow. */
86
+ onSpawnError?: (error: unknown) => void;
87
+ onExit?: (info: WorkerExitInfo) => void;
88
+ onStderr?: (text: string) => void;
89
+ restartWindowMs?: number;
90
+ restartLimit?: number;
91
+ }
92
+ export declare class WorkerBridgeCore {
93
+ private readonly opts;
94
+ private child;
95
+ /** Pending lines that arrived before the worker's stdin existed. */
96
+ private outbox;
97
+ private restartCount;
98
+ private restartWindowStart;
99
+ private generation;
100
+ private readonly lineListeners;
101
+ private readonly pendingRequests;
102
+ private readonly restartWindowMs;
103
+ private readonly restartLimit;
104
+ constructor(opts: WorkerBridgeCoreOptions);
105
+ private log;
106
+ /** Current worker generation (0 until the first successful spawn). */
107
+ workerGeneration(): number;
108
+ hasChild(): boolean;
109
+ /** stdin exists and is not destroyed — a line write won't be dropped. */
110
+ canSend(): boolean;
111
+ hasLiveWorker(): boolean;
112
+ /**
113
+ * Spawn the worker if none is alive. `cwd` is the working directory the
114
+ * Engine will use (i.e. the repo root). Idempotent if a child is alive.
115
+ */
116
+ ensureWorker(requestedCwd?: string): void;
117
+ /** Returns true after >= restartLimit crashes in the current window. */
118
+ private shouldDeclareGaveUp;
119
+ /**
120
+ * If `line` is a response to a pending request(), settle it. Returns true
121
+ * when the line was consumed (must NOT be dispatched to listeners).
122
+ */
123
+ private settleMatchingRequest;
124
+ private settlePendingOnExit;
125
+ /**
126
+ * Register an observer of every worker stdout line that wasn't consumed by
127
+ * a `consume: true` request. Returns an unsubscribe. A throwing listener
128
+ * never disrupts the stream.
129
+ */
130
+ subscribeLines(listener: (line: string) => void): () => void;
131
+ /**
132
+ * Write one JSON-RPC line to the worker's stdin (newline appended). Returns
133
+ * false when there is no live worker (caller decides drop/fallback). A
134
+ * stream write error propagates to the caller, matching a direct write.
135
+ */
136
+ sendLine(line: string): boolean;
137
+ /**
138
+ * Inject a JSON-RPC line into the worker exactly as a first-party front end
139
+ * would. The host's `prepareInbound` hook runs first (metadata rewriting +
140
+ * on-demand spawn side effects); the possibly-rewritten line is then
141
+ * written, or dropped with a log when no worker is alive — identical
142
+ * semantics for every front end (renderer, mobile remote, orchestrators).
143
+ */
144
+ injectWorkerMessage(rawLine: string): void;
145
+ /**
146
+ * Send a correlated JSON-RPC request and await its response by id.
147
+ * `params` is omitted from the frame when undefined. Never rejects — the
148
+ * outcome union carries error/timeout/send-failure states for the caller
149
+ * to map onto its own semantics.
150
+ */
151
+ request(method: string, params: unknown, options: WorkerRequestOptions): Promise<WorkerRpcOutcome>;
152
+ /** Graceful shutdown: SIGTERM the worker (no-op when none is alive). */
153
+ kill(): void;
154
+ }