@rynx-ai/runtime 0.1.11-beta.33 → 0.1.11-beta.35

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 (36) hide show
  1. package/dist/claude/native-bridge.d.ts +16 -1
  2. package/dist/claude/native-bridge.js +91 -17
  3. package/dist/claude/native-hook-main.js +1 -0
  4. package/dist/claude/native-integration.d.ts +10 -0
  5. package/dist/claude/native-integration.js +100 -6
  6. package/dist/codex-app-server/forwarder.js +23 -1
  7. package/dist/codex-app-server/mapping.js +34 -10
  8. package/dist/codex-app-server/process-registry.d.ts +36 -0
  9. package/dist/codex-app-server/process-registry.js +320 -0
  10. package/dist/codex-app-server/ws-channel.d.ts +7 -0
  11. package/dist/codex-app-server/ws-channel.js +85 -9
  12. package/dist/codex-home.d.ts +35 -3
  13. package/dist/codex-home.js +321 -14
  14. package/dist/host.d.ts +11 -25
  15. package/dist/host.js +147 -132
  16. package/dist/index.d.ts +2 -2
  17. package/dist/index.js +1 -1
  18. package/dist/runner/child.d.ts +25 -3
  19. package/dist/runner/child.js +359 -64
  20. package/dist/runner/manager.d.ts +33 -18
  21. package/dist/runner/manager.js +229 -49
  22. package/dist/runner/protocol.d.ts +79 -7
  23. package/dist/runner/transport.d.ts +9 -2
  24. package/dist/runner/transport.js +55 -3
  25. package/dist/runner-main.js +8 -3
  26. package/dist/terminal/codex-tui.d.ts +4 -0
  27. package/dist/terminal/codex-tui.js +5 -0
  28. package/dist/terminal/control-parser.d.ts +39 -0
  29. package/dist/terminal/control-parser.js +172 -0
  30. package/dist/terminal/registry.d.ts +18 -15
  31. package/dist/terminal/registry.js +43 -23
  32. package/dist/terminal/spool.d.ts +47 -0
  33. package/dist/terminal/spool.js +231 -0
  34. package/dist/terminal/tmux.d.ts +93 -84
  35. package/dist/terminal/tmux.js +675 -200
  36. package/package.json +3 -4
@@ -4,6 +4,7 @@ export declare const STATE_FILE = "state.json";
4
4
  export declare const DELTAS_FILE = "message_deltas.jsonl";
5
5
  export declare const STATUS_FILE = "status.json";
6
6
  export declare const FORWARDER_STATE_FILE = "transcript_forwarder.json";
7
+ export declare const HOOK_FORWARDER_STATE_FILE = "hook_forwarder.json";
7
8
  export declare const INTERACTIONS_FILE = "interactions.jsonl";
8
9
  export declare const INTERACTION_ACKS_FILE = "interaction-acks.jsonl";
9
10
  export declare const INTERACTION_RESULTS_DIR = "interaction-results";
@@ -104,6 +105,10 @@ export interface JsonlReadResult<T> {
104
105
  export declare function readJsonlFrom<T = unknown>(path: string, byteOffset: number): JsonlReadResult<T>;
105
106
  /** One parsed Claude hook event (the fields the forwarder acts on + the raw payload). */
106
107
  export interface HookEvent {
108
+ /** One-based record position in the append-only hook stream. */
109
+ eventCursor: number;
110
+ /** Byte offset immediately after this complete JSONL record. */
111
+ byteOffset: number;
107
112
  eventName?: string;
108
113
  transcriptPath?: string;
109
114
  /** Claude's session uuid — subagent hooks report a `subagents/…` transcript path. */
@@ -114,10 +119,20 @@ export interface HookEvent {
114
119
  payload: Record<string, unknown>;
115
120
  }
116
121
  /** Tail `hooks.jsonl` from a byte offset into parsed {@link HookEvent}s. */
117
- export declare function readHookEventsFrom(bridgeDir: string, byteOffset: number): {
122
+ export declare function readHookEventsFrom(bridgeDir: string, byteOffset: number, startEventCursor?: number): {
118
123
  events: HookEvent[];
119
124
  nextOffset: number;
120
125
  };
126
+ export interface HookForwardState {
127
+ eventCursor: number;
128
+ byteOffset: number;
129
+ cursorFingerprint?: string;
130
+ }
131
+ /** Read and validate the durable hook cursor. A replaced/truncated hook stream
132
+ * restarts at zero; event handlers provide their own stable deduplication. */
133
+ export declare function readHookForwardState(bridgeDir: string): HookForwardState | undefined;
134
+ /** Persist one consumed hook record before another poll can observe it. */
135
+ export declare function writeHookForwardState(bridgeDir: string, state: HookForwardState): void;
121
136
  /** One streamed assistant-text chunk from `message_deltas.jsonl` (MessageDisplay). */
122
137
  export interface MessageDelta {
123
138
  messageId: string;
@@ -20,6 +20,7 @@ export const STATE_FILE = "state.json";
20
20
  export const DELTAS_FILE = "message_deltas.jsonl";
21
21
  export const STATUS_FILE = "status.json";
22
22
  export const FORWARDER_STATE_FILE = "transcript_forwarder.json";
23
+ export const HOOK_FORWARDER_STATE_FILE = "hook_forwarder.json";
23
24
  export const INTERACTIONS_FILE = "interactions.jsonl";
24
25
  export const INTERACTION_ACKS_FILE = "interaction-acks.jsonl";
25
26
  export const INTERACTION_RESULTS_DIR = "interaction-results";
@@ -47,6 +48,7 @@ export function prepareClaudeBridgeDir(sessionId) {
47
48
  INTERACTIONS_FILE,
48
49
  INTERACTION_ACKS_FILE,
49
50
  MANAGED_SETTINGS_FILE,
51
+ HOOK_FORWARDER_STATE_FILE,
50
52
  ]) {
51
53
  try {
52
54
  rmSync(join(dir, file));
@@ -360,27 +362,99 @@ export function readJsonlFrom(path, byteOffset) {
360
362
  }
361
363
  }
362
364
  /** Tail `hooks.jsonl` from a byte offset into parsed {@link HookEvent}s. */
363
- export function readHookEventsFrom(bridgeDir, byteOffset) {
364
- const { records, nextOffset } = readJsonlFrom(join(bridgeDir, HOOKS_FILE), byteOffset);
365
+ export function readHookEventsFrom(bridgeDir, byteOffset, startEventCursor = 0) {
366
+ const path = join(bridgeDir, HOOKS_FILE);
367
+ let info;
368
+ try {
369
+ info = statSync(path);
370
+ }
371
+ catch {
372
+ return { events: [], nextOffset: byteOffset };
373
+ }
374
+ if (info.size <= byteOffset)
375
+ return { events: [], nextOffset: byteOffset };
376
+ const fd = openSync(path, "r");
365
377
  const events = [];
366
- for (const rec of records) {
367
- const payload = rec.payload;
368
- if (!payload || typeof payload !== "object")
369
- continue;
370
- const liveBackgroundTasks = backgroundTaskCount(payload);
371
- events.push({
372
- eventName: asString(payload.hook_event_name),
373
- transcriptPath: asString(payload.transcript_path),
374
- sessionId: asString(payload.session_id),
375
- source: asString(payload.source),
376
- ...(liveBackgroundTasks === undefined
377
- ? {}
378
- : { backgroundTaskCount: liveBackgroundTasks }),
379
- payload,
380
- });
378
+ let nextOffset = byteOffset;
379
+ let eventCursor = startEventCursor;
380
+ try {
381
+ const length = info.size - byteOffset;
382
+ const buf = Buffer.alloc(length);
383
+ readSync(fd, buf, 0, length, byteOffset);
384
+ const text = buf.toString("utf8");
385
+ const lines = text.split("\n");
386
+ lines.pop(); // retain a partial trailing record for the next poll
387
+ for (const line of lines) {
388
+ const lineBytes = Buffer.byteLength(`${line}\n`, "utf8");
389
+ nextOffset += lineBytes;
390
+ if (!line.trim())
391
+ continue;
392
+ eventCursor += 1;
393
+ let rec;
394
+ try {
395
+ rec = JSON.parse(line);
396
+ }
397
+ catch {
398
+ events.push({ eventCursor, byteOffset: nextOffset, payload: {} });
399
+ continue;
400
+ }
401
+ const payload = rec.payload;
402
+ if (!payload || typeof payload !== "object") {
403
+ events.push({ eventCursor, byteOffset: nextOffset, payload: {} });
404
+ continue;
405
+ }
406
+ const liveBackgroundTasks = backgroundTaskCount(payload);
407
+ events.push({
408
+ eventCursor,
409
+ byteOffset: nextOffset,
410
+ eventName: asString(payload.hook_event_name),
411
+ transcriptPath: asString(payload.transcript_path),
412
+ sessionId: asString(payload.session_id),
413
+ source: asString(payload.source),
414
+ ...(liveBackgroundTasks === undefined
415
+ ? {}
416
+ : { backgroundTaskCount: liveBackgroundTasks }),
417
+ payload,
418
+ });
419
+ }
420
+ }
421
+ finally {
422
+ closeSync(fd);
381
423
  }
382
424
  return { events, nextOffset };
383
425
  }
426
+ /** Read and validate the durable hook cursor. A replaced/truncated hook stream
427
+ * restarts at zero; event handlers provide their own stable deduplication. */
428
+ export function readHookForwardState(bridgeDir) {
429
+ const raw = readJsonFile(join(bridgeDir, HOOK_FORWARDER_STATE_FILE));
430
+ if (!raw || typeof raw !== "object")
431
+ return undefined;
432
+ const state = raw;
433
+ const eventCursor = asNumber(state.eventCursor);
434
+ const byteOffset = asNumber(state.byteOffset);
435
+ const cursorFingerprint = asString(state.cursorFingerprint);
436
+ if (eventCursor === undefined || eventCursor < 0 || !Number.isInteger(eventCursor) ||
437
+ byteOffset === undefined || byteOffset < 0 || !Number.isInteger(byteOffset))
438
+ return undefined;
439
+ const current = jsonlCursorFingerprint(join(bridgeDir, HOOKS_FILE), byteOffset);
440
+ if (!cursorFingerprint || current !== cursorFingerprint) {
441
+ return {
442
+ eventCursor: 0,
443
+ byteOffset: 0,
444
+ ...(jsonlCursorFingerprint(join(bridgeDir, HOOKS_FILE), 0)
445
+ ? { cursorFingerprint: jsonlCursorFingerprint(join(bridgeDir, HOOKS_FILE), 0) }
446
+ : {}),
447
+ };
448
+ }
449
+ return { eventCursor, byteOffset, cursorFingerprint };
450
+ }
451
+ /** Persist one consumed hook record before another poll can observe it. */
452
+ export function writeHookForwardState(bridgeDir, state) {
453
+ mkdirSync(bridgeDir, { recursive: true, mode: 0o700 });
454
+ const tmp = join(bridgeDir, `${HOOK_FORWARDER_STATE_FILE}.tmp`);
455
+ writeFileSync(tmp, JSON.stringify(state));
456
+ renameSync(tmp, join(bridgeDir, HOOK_FORWARDER_STATE_FILE));
457
+ }
384
458
  /** Tail `message_deltas.jsonl` from a byte offset into {@link MessageDelta}s. */
385
459
  export function readMessageDeltasFrom(bridgeDir, byteOffset) {
386
460
  const { records, nextOffset } = readJsonlFrom(join(bridgeDir, DELTAS_FILE), byteOffset);
@@ -312,6 +312,7 @@ function nativeVerdict(hookKind, payload, result, suggestions) {
312
312
  ...(behavior === "deny" && feedback ? { message: feedback } : {}),
313
313
  ...(behavior === "allow"
314
314
  ? {
315
+ updatedInput: toolInput,
315
316
  updatedPermissions: [{
316
317
  type: "setMode",
317
318
  mode: resolution.actionId === "allow_auto" ? "auto" : "default",
@@ -88,7 +88,10 @@ export declare class ClaudeLiveSession {
88
88
  private readonly leaseUpdatedAt;
89
89
  private started;
90
90
  private stopped;
91
+ private supervisorTask?;
92
+ private releaseSleep?;
91
93
  private hooksOffset;
94
+ private hookEventCursor;
92
95
  private interactionsOffset;
93
96
  private interactionAcksOffset;
94
97
  private transcriptOffset;
@@ -202,6 +205,10 @@ export declare class ClaudeLiveSession {
202
205
  private consumeQueuedPromotion;
203
206
  start(): void;
204
207
  stop(): void;
208
+ /** Wait for the supervised polling task to retire. Terminal teardown starts
209
+ * synchronously in {@link stop}; callers use this bounded join before their
210
+ * runner process exits. */
211
+ waitForStop(timeoutMs?: number): Promise<void>;
205
212
  /** Phase two of runner shutdown. The caller must stop the Claude terminal
206
213
  * first so no hook can still be opening an atomically claimed answer. This is
207
214
  * synchronous because the runner process exits immediately afterwards. */
@@ -214,8 +221,11 @@ export declare class ClaudeLiveSession {
214
221
  panePid: () => number | undefined;
215
222
  configDir?: string;
216
223
  }): void;
224
+ private supervise;
217
225
  private loop;
226
+ private sleepUntilWake;
218
227
  private pollHooks;
228
+ private persistHookCursor;
219
229
  private handleHook;
220
230
  /** Process a provider failure only after this tick has discovered every
221
231
  * blocking request already appended by its hook subprocess. */
@@ -20,7 +20,7 @@
20
20
  */
21
21
  import { statSync } from "node:fs";
22
22
  import { parseTerminalCommand, parseTranscriptRecord, readSubagentEvents, subagentTranscriptPath, transcriptHasForkedFrom, } from "./transcript.js";
23
- import { jsonlCursorFingerprint, interactionLeaseUpdatedAt, readClaimedInteractionResult, readClaudeStatus, readForwardState, readInteractionAcksFrom, readHookEventsFrom, readInteractionRequestsFrom, readJsonlFrom, readMessageDeltasFrom, resetForwardState, removeClaimedInteractionResult, removeInteractionLease, removeInteractionResult, scrubClaudeInteractionArtifacts, writeInteractionResult, writeForwardState, } from "./native-bridge.js";
23
+ import { HOOKS_FILE, jsonlCursorFingerprint, interactionLeaseUpdatedAt, readClaimedInteractionResult, readClaudeStatus, readForwardState, readHookForwardState, readInteractionAcksFrom, readHookEventsFrom, readInteractionRequestsFrom, readJsonlFrom, readMessageDeltasFrom, resetForwardState, removeClaimedInteractionResult, removeInteractionLease, removeInteractionResult, scrubClaudeInteractionArtifacts, writeInteractionResult, writeForwardState, writeHookForwardState, } from "./native-bridge.js";
24
24
  import { boundInteractionRequest, redactInteractionResolution, validateInteractionResolution, } from "../interactions.js";
25
25
  import { ClaudeSessionStatusPoller, } from "./session-status.js";
26
26
  const MAX_SETTLED_INTERACTIONS = 512;
@@ -32,6 +32,9 @@ const INTERACTION_ACK_TIMEOUT_MS = 5_000;
32
32
  // reference implementation's `_CLAUDE_INTERRUPT_RECORD_RE`.
33
33
  const CLAUDE_INTERRUPT_RECORD_RE = /^\[Request interrupted by user(?: for tool use)?\]$/;
34
34
  const INTERACTION_LEASE_TIMEOUT_MS = 30_000;
35
+ const FORWARDER_RESTART_INITIAL_MS = 1_000;
36
+ const FORWARDER_RESTART_MAX_MS = 30_000;
37
+ const FORWARDER_HEALTHY_UPTIME_MS = 60_000;
35
38
  function processIsAlive(pid) {
36
39
  try {
37
40
  process.kill(pid, 0);
@@ -149,7 +152,10 @@ export class ClaudeLiveSession {
149
152
  leaseUpdatedAt;
150
153
  started = false;
151
154
  stopped = false;
155
+ supervisorTask;
156
+ releaseSleep;
152
157
  hooksOffset = 0;
158
+ hookEventCursor = 0;
153
159
  interactionsOffset = 0;
154
160
  interactionAcksOffset = 0;
155
161
  transcriptOffset = 0;
@@ -249,6 +255,11 @@ export class ClaudeLiveSession {
249
255
  this.expectedClaudeSessionId = opts.claudeSessionId;
250
256
  this.resumeAtEndOnDiscovery = opts.resumeAtEndOnDiscovery ?? false;
251
257
  this.transcriptPath = opts.transcriptPath;
258
+ const hookState = readHookForwardState(this.bridgeDir);
259
+ if (hookState) {
260
+ this.hooksOffset = hookState.byteOffset;
261
+ this.hookEventCursor = hookState.eventCursor;
262
+ }
252
263
  // Resume path: bind the session id + restore the persisted forwarder cursor so
253
264
  // we continue from where a prior forwarder left off (no re-mirror on relaunch).
254
265
  if (opts.transcriptPath) {
@@ -342,12 +353,13 @@ export class ClaudeLiveSession {
342
353
  if (this.started)
343
354
  return;
344
355
  this.started = true;
345
- void this.loop();
356
+ this.supervisorTask = this.supervise();
346
357
  }
347
358
  stop() {
348
359
  if (this.stopped)
349
360
  return;
350
361
  this.stopped = true;
362
+ this.releaseSleep?.();
351
363
  this.statusPoller?.retire();
352
364
  // A hook may have appended its request just before shutdown but not yet
353
365
  // reached a scheduled poll. Drain it so the subprocess receives cancellation
@@ -360,6 +372,24 @@ export class ClaudeLiveSession {
360
372
  this.closeTurn();
361
373
  this.scheduleClaimedInteractionScrubs();
362
374
  }
375
+ /** Wait for the supervised polling task to retire. Terminal teardown starts
376
+ * synchronously in {@link stop}; callers use this bounded join before their
377
+ * runner process exits. */
378
+ async waitForStop(timeoutMs = 5_000) {
379
+ const task = this.supervisorTask;
380
+ if (!task)
381
+ return;
382
+ let timer;
383
+ await Promise.race([
384
+ task,
385
+ new Promise((resolve) => {
386
+ timer = setTimeout(resolve, timeoutMs);
387
+ timer.unref?.();
388
+ }),
389
+ ]);
390
+ if (timer)
391
+ clearTimeout(timer);
392
+ }
363
393
  /** Phase two of runner shutdown. The caller must stop the Claude terminal
364
394
  * first so no hook can still be opening an atomically claimed answer. This is
365
395
  * synchronous because the runner process exits immediately afterwards. */
@@ -400,6 +430,32 @@ export class ClaudeLiveSession {
400
430
  ...(configDir ? { configDir } : {}),
401
431
  });
402
432
  }
433
+ async supervise() {
434
+ let backoffMs = FORWARDER_RESTART_INITIAL_MS;
435
+ while (!this.stopped) {
436
+ const startedAt = this.now();
437
+ let crashed;
438
+ try {
439
+ await this.loop();
440
+ if (!this.stopped) {
441
+ console.warn("[claude-forwarder] polling loop returned unexpectedly; restarting");
442
+ }
443
+ }
444
+ catch (error) {
445
+ crashed = error;
446
+ }
447
+ if (this.stopped)
448
+ break;
449
+ if (this.now() - startedAt >= FORWARDER_HEALTHY_UPTIME_MS) {
450
+ backoffMs = FORWARDER_RESTART_INITIAL_MS;
451
+ }
452
+ if (crashed !== undefined) {
453
+ console.error(`[claude-forwarder] polling loop crashed; restarting in ${backoffMs}ms:`, crashed);
454
+ }
455
+ await this.sleepUntilWake(backoffMs);
456
+ backoffMs = Math.min(backoffMs * 2, FORWARDER_RESTART_MAX_MS);
457
+ }
458
+ }
403
459
  async loop() {
404
460
  while (!this.stopped) {
405
461
  try {
@@ -411,7 +467,7 @@ export class ClaudeLiveSession {
411
467
  // per-iteration `except Exception` in the transcript forwarder loop.
412
468
  console.error("[claude-forwarder] tick failed; continuing:", err);
413
469
  }
414
- await new Promise((r) => setTimeout(r, this.pollMs));
470
+ await this.sleepUntilWake(this.pollMs);
415
471
  }
416
472
  // Catch a request appended while the loop was waking up to observe stopped.
417
473
  this.pollInteractions();
@@ -420,11 +476,49 @@ export class ClaudeLiveSession {
420
476
  this.closeTurn();
421
477
  this.scheduleClaimedInteractionScrubs();
422
478
  }
479
+ sleepUntilWake(delayMs) {
480
+ if (this.stopped)
481
+ return Promise.resolve();
482
+ return new Promise((resolve) => {
483
+ const timer = setTimeout(done, delayMs);
484
+ timer.unref?.();
485
+ const self = this;
486
+ function done() {
487
+ if (self.releaseSleep === done)
488
+ self.releaseSleep = undefined;
489
+ clearTimeout(timer);
490
+ resolve();
491
+ }
492
+ this.releaseSleep = done;
493
+ });
494
+ }
423
495
  pollHooks() {
424
- const { events, nextOffset } = readHookEventsFrom(this.bridgeDir, this.hooksOffset);
425
- this.hooksOffset = nextOffset;
426
- for (const ev of events)
496
+ const { events } = readHookEventsFrom(this.bridgeDir, this.hooksOffset, this.hookEventCursor);
497
+ for (const ev of events) {
498
+ // Rotation is an exactly-once edge. Persist it before the callback so a
499
+ // partial publication failure cannot create another replacement session
500
+ // when this forwarder (or its supervisor) restarts.
501
+ const rotation = ev.eventName === "SessionStart" && (ev.source === "clear" ||
502
+ (ev.source === "resume" && Boolean(ev.sessionId &&
503
+ ev.transcriptPath &&
504
+ !this.seenClaudeSessionIds.has(ev.sessionId) &&
505
+ transcriptHasForkedFrom(ev.transcriptPath, ev.sessionId))));
506
+ if (rotation)
507
+ this.persistHookCursor(ev);
427
508
  this.handleHook(ev);
509
+ if (!rotation)
510
+ this.persistHookCursor(ev);
511
+ }
512
+ }
513
+ persistHookCursor(ev) {
514
+ this.hooksOffset = ev.byteOffset;
515
+ this.hookEventCursor = ev.eventCursor;
516
+ const fingerprint = jsonlCursorFingerprint(`${this.bridgeDir}/${HOOKS_FILE}`, ev.byteOffset);
517
+ writeHookForwardState(this.bridgeDir, {
518
+ eventCursor: ev.eventCursor,
519
+ byteOffset: ev.byteOffset,
520
+ ...(fingerprint ? { cursorFingerprint: fingerprint } : {}),
521
+ });
428
522
  }
429
523
  handleHook(ev) {
430
524
  if (ev.eventName === "SessionStart") {
@@ -6,6 +6,18 @@ function threadIdFrom(params) {
6
6
  const p = params;
7
7
  return p?.threadId ?? p?.thread?.id;
8
8
  }
9
+ /** Agent-control children announce their own thread on the shared app-server.
10
+ * They do not replace the parent TUI thread: only a top-level thread switch is
11
+ * a Session rotation boundary. */
12
+ function isSubagentThreadStarted(params) {
13
+ const source = params?.thread?.source?.subAgent?.thread_spawn;
14
+ return source !== null && typeof source === "object" && !Array.isArray(source);
15
+ }
16
+ /** Internal system threads are non-persistable and never replace the parent
17
+ * TUI thread, even though the app-server broadcasts `thread/started`. */
18
+ function isEphemeralThreadStarted(params) {
19
+ return params?.thread?.ephemeral === true;
20
+ }
9
21
  function turnIdFrom(params) {
10
22
  const p = params;
11
23
  return p?.turnId ?? p?.turn?.id;
@@ -108,7 +120,14 @@ export class CodexSessionForwarder {
108
120
  if (this.unsubscribe)
109
121
  return;
110
122
  this.unsubscribe = this.client.onNotification((method, params) => {
111
- this.handle(method, params);
123
+ try {
124
+ this.handle(method, params);
125
+ }
126
+ catch (error) {
127
+ // One malformed or unsupported notification must not detach the
128
+ // long-lived observer from every later event in this session.
129
+ console.error(`[codex-forwarder] notification handler failed for ${method}; continuing:`, error);
130
+ }
112
131
  });
113
132
  const startup = this.options.mcpStartup;
114
133
  if (startup?.servers.length) {
@@ -247,6 +266,9 @@ export class CodexSessionForwarder {
247
266
  }
248
267
  handle(method, params) {
249
268
  if (method === "thread/started" || method === "thread.started") {
269
+ if (method === "thread/started" &&
270
+ (isSubagentThreadStarted(params) || isEphemeralThreadStarted(params)))
271
+ return;
250
272
  const tid = threadIdFrom(params);
251
273
  if (tid && tid !== this.currentThreadIdValue) {
252
274
  const forkedFromId = params?.thread?.forkedFromId;
@@ -127,10 +127,12 @@ function fileChangeSummary(changes) {
127
127
  for (const change of changes) {
128
128
  if (!isRecord(change))
129
129
  continue;
130
+ if (typeof change.path !== "string")
131
+ continue;
130
132
  const kind = isRecord(change.kind) && typeof change.kind.type === "string" && change.kind.type
131
133
  ? change.kind.type
132
134
  : "change";
133
- lines.push(`${kind} ${String(change.path)}`);
135
+ lines.push(`${kind} ${change.path}`);
134
136
  }
135
137
  return lines.join("\n");
136
138
  }
@@ -147,20 +149,37 @@ export function mapCodexItem(method, item) {
147
149
  switch (item.type) {
148
150
  case "commandExecution": {
149
151
  const commandItem = item;
152
+ const id = typeof commandItem.id === "string" ? commandItem.id : "";
153
+ const command = typeof commandItem.command === "string" ? commandItem.command : "";
154
+ if (!id || !command)
155
+ return { events };
156
+ const exitCode = typeof commandItem.exitCode === "number"
157
+ ? commandItem.exitCode
158
+ : null;
159
+ const rawOutput = commandItem.aggregatedOutput ?? "";
160
+ const aggregatedOutput = exitCode !== null && exitCode !== 0
161
+ ? `${rawOutput}${rawOutput ? "\n" : ""}[exit code: ${exitCode}]`
162
+ : rawOutput;
150
163
  events.push({
151
164
  type: "tool",
152
165
  event: isStart ? "on_tool_start" : "on_tool_end",
153
- name: "command_execution",
154
- input: isStart
155
- ? { id: commandItem.id, command: commandItem.command, cwd: commandItem.cwd }
166
+ name: "shell",
167
+ input: isStart || isEnd
168
+ ? {
169
+ id,
170
+ command,
171
+ ...(typeof commandItem.cwd === "string" && commandItem.cwd
172
+ ? { cwd: commandItem.cwd }
173
+ : {}),
174
+ }
156
175
  : undefined,
157
176
  output: isEnd
158
177
  ? {
159
- id: commandItem.id,
160
- command: commandItem.command,
178
+ id,
179
+ command,
161
180
  status: commandItem.status,
162
- aggregatedOutput: commandItem.aggregatedOutput ?? null,
163
- exitCode: commandItem.exitCode ?? null,
181
+ aggregatedOutput,
182
+ exitCode,
164
183
  }
165
184
  : undefined,
166
185
  data: { method, item },
@@ -170,14 +189,19 @@ export function mapCodexItem(method, item) {
170
189
  case "fileChange": {
171
190
  const fileChange = item;
172
191
  const changes = Array.isArray(fileChange.changes) ? fileChange.changes : [];
192
+ const id = typeof item.id === "string" ? item.id : "";
193
+ if (!id ||
194
+ changes.length === 0 ||
195
+ !changes.some((change) => isRecord(change) && typeof change.path === "string"))
196
+ return { events };
173
197
  events.push({
174
198
  type: "tool",
175
199
  event: isStart ? "on_tool_start" : "on_tool_end",
176
200
  name: "apply_patch",
177
- input: isStart || isEnd ? { id: item.id, ...(changes.length ? { changes } : {}) } : undefined,
201
+ input: isStart || isEnd ? { id, changes } : undefined,
178
202
  output: isEnd
179
203
  ? {
180
- id: item.id,
204
+ id,
181
205
  status: fileChange.status,
182
206
  aggregatedOutput: fileChangeSummary(changes),
183
207
  }
@@ -0,0 +1,36 @@
1
+ export interface RuntimeProcessEntry {
2
+ pid: number;
3
+ pgid: number;
4
+ ownerPid: number;
5
+ /** OS process-start identity. Unlike a PID, this cannot silently refer to a
6
+ * later process after the launcher crashes and the PID is recycled. */
7
+ ownerIdentity?: string;
8
+ sessionTag: string;
9
+ stateDir: string;
10
+ }
11
+ export declare function runtimeProcessRegistryPath(): string;
12
+ export declare function runtimeProcessTagArg(sessionTag: string): string;
13
+ export declare function runtimeProcessArgv0(cliPath: string, sessionTag: string): string;
14
+ export declare function withRuntimeProcessStateArg(baseArgs: string[], stateDir: string): string[];
15
+ export declare function registerRuntimeProcess(entry: RuntimeProcessEntry, registryPath?: string): void;
16
+ export declare function unregisterRuntimeProcess(sessionTag: string, registryPath?: string): void;
17
+ export interface RuntimeProcessReconcileOptions {
18
+ registryPath?: string;
19
+ ownerAlive?: (pid: number) => boolean;
20
+ ownerIdentity?: (pid: number) => string;
21
+ childAlive?: (pid: number) => boolean;
22
+ commandLine?: (pid: number) => string;
23
+ terminateGroup?: (pgid: number) => boolean;
24
+ }
25
+ export declare function reconcileRuntimeProcesses(opts?: RuntimeProcessReconcileOptions): number;
26
+ export declare function reapRuntimeProcessesForStateDir(stateDir: string, opts?: RuntimeProcessStateReapOptions): number;
27
+ export interface RuntimeProcessStateReapOptions {
28
+ processListing?: () => string;
29
+ currentPgid?: () => number;
30
+ childAlive?: (pid: number) => boolean;
31
+ signalGroup?: (pgid: number, signal: NodeJS.Signals) => boolean;
32
+ graceMs?: number;
33
+ }
34
+ /** Return an OS process-start identity used to distinguish PID reuse. */
35
+ export declare function runtimeProcessOwnerIdentity(pid: number): string;
36
+ export declare function newRuntimeProcessTag(): string;