@rynx-ai/runtime 0.1.11-beta.4 → 0.1.11-beta.41
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.
- package/dist/claude/executor.d.ts +19 -5
- package/dist/claude/executor.js +56 -12
- package/dist/claude/models.d.ts +0 -5
- package/dist/claude/models.js +1 -7
- package/dist/claude/native-bridge.d.ts +103 -1
- package/dist/claude/native-bridge.js +445 -30
- package/dist/claude/native-hook-main.js +81 -1
- package/dist/claude/native-hooks.js +7 -0
- package/dist/claude/native-integration.d.ts +178 -26
- package/dist/claude/native-integration.js +1528 -170
- package/dist/claude/session-status.d.ts +39 -0
- package/dist/claude/session-status.js +163 -0
- package/dist/claude/transcript-clone.d.ts +18 -0
- package/dist/claude/transcript-clone.js +497 -0
- package/dist/claude/transcript.d.ts +27 -4
- package/dist/claude/transcript.js +158 -47
- package/dist/codex-app-server/client.d.ts +10 -6
- package/dist/codex-app-server/client.js +67 -15
- package/dist/codex-app-server/forwarder.d.ts +92 -3
- package/dist/codex-app-server/forwarder.js +532 -57
- package/dist/codex-app-server/mapping.d.ts +3 -6
- package/dist/codex-app-server/mapping.js +206 -36
- package/dist/codex-app-server/mcp-startup.d.ts +13 -0
- package/dist/codex-app-server/mcp-startup.js +63 -0
- package/dist/codex-app-server/process-registry.d.ts +36 -0
- package/dist/codex-app-server/process-registry.js +320 -0
- package/dist/codex-app-server/protocol.d.ts +64 -7
- package/dist/codex-app-server/ws-channel.d.ts +7 -0
- package/dist/codex-app-server/ws-channel.js +104 -28
- package/dist/codex-home.d.ts +35 -3
- package/dist/codex-home.js +323 -18
- package/dist/codex-session-store.d.ts +23 -0
- package/dist/codex-session-store.js +21 -0
- package/dist/host.d.ts +103 -46
- package/dist/host.js +1988 -634
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/input-resources.d.ts +4 -0
- package/dist/input-resources.js +21 -5
- package/dist/models-catalog.d.ts +2 -1
- package/dist/models-catalog.js +94 -6
- package/dist/runner/child.d.ts +97 -28
- package/dist/runner/child.js +1486 -100
- package/dist/runner/manager.d.ts +110 -29
- package/dist/runner/manager.js +1481 -246
- package/dist/runner/protocol.d.ts +212 -24
- package/dist/runner/protocol.js +5 -0
- package/dist/runner/startup-policy.d.ts +7 -0
- package/dist/runner/startup-policy.js +10 -0
- package/dist/runner/transport.d.ts +18 -2
- package/dist/runner/transport.js +82 -3
- package/dist/runner-main.js +8 -3
- package/dist/terminal/claude-tui.d.ts +3 -1
- package/dist/terminal/claude-tui.js +3 -1
- package/dist/terminal/codex-tui.d.ts +4 -0
- package/dist/terminal/codex-tui.js +5 -0
- package/dist/terminal/control-parser.d.ts +39 -0
- package/dist/terminal/control-parser.js +172 -0
- package/dist/terminal/registry.d.ts +18 -15
- package/dist/terminal/registry.js +44 -23
- package/dist/terminal/spool.d.ts +47 -0
- package/dist/terminal/spool.js +231 -0
- package/dist/terminal/tmux.d.ts +126 -74
- package/dist/terminal/tmux.js +807 -211
- package/package.json +4 -4
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* Dependency-light on purpose (node builtins plus erased protocol types) so the
|
|
12
12
|
* standalone hook entrypoints do not pull the whole runtime into every process.
|
|
13
13
|
*/
|
|
14
|
-
import { appendFileSync, closeSync, mkdirSync, openSync, readFileSync, readSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
15
|
-
import { createHash } from "node:crypto";
|
|
14
|
+
import { appendFileSync, closeSync, fsyncSync, mkdirSync, openSync, readFileSync, readSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
15
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
16
16
|
import { join } from "node:path";
|
|
17
17
|
import { adoptLegacyRuntimeDirectory, legacyRuntimeStateRoot, runtimeSessionDigest, runtimeSessionStateDir, } from "../runtime-state-paths.js";
|
|
18
18
|
export const HOOKS_FILE = "hooks.jsonl";
|
|
@@ -20,12 +20,42 @@ 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";
|
|
24
|
+
export const SUBAGENT_FORWARDER_STATE_FILE = "subagent_forwarder.json";
|
|
25
|
+
export const DELTA_FORWARDER_STATE_FILE = "message_deltas_forwarder.json";
|
|
26
|
+
export const COMPACTION_FORWARDER_STATE_FILE = "compaction_forwarder.json";
|
|
23
27
|
export const INTERACTIONS_FILE = "interactions.jsonl";
|
|
24
28
|
export const INTERACTION_ACKS_FILE = "interaction-acks.jsonl";
|
|
25
29
|
export const INTERACTION_RESULTS_DIR = "interaction-results";
|
|
26
30
|
export const INTERACTION_CLAIMS_DIR = "interaction-claims";
|
|
27
31
|
export const INTERACTION_LEASES_DIR = "interaction-leases";
|
|
28
32
|
export const MANAGED_SETTINGS_FILE = "managed-settings.json";
|
|
33
|
+
/** Reference bridge state uses temp + fsync + replace so a hook crash can
|
|
34
|
+
* never leave readers with a truncated JSON object. A unique temp name also
|
|
35
|
+
* keeps concurrent native hook subprocesses from clobbering one another's
|
|
36
|
+
* staging file. */
|
|
37
|
+
function writeJsonFileAtomic(path, payload) {
|
|
38
|
+
const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
39
|
+
let fd;
|
|
40
|
+
try {
|
|
41
|
+
fd = openSync(tmp, "wx", 0o600);
|
|
42
|
+
writeFileSync(fd, JSON.stringify(payload));
|
|
43
|
+
fsyncSync(fd);
|
|
44
|
+
closeSync(fd);
|
|
45
|
+
fd = undefined;
|
|
46
|
+
renameSync(tmp, path);
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
if (fd !== undefined)
|
|
50
|
+
closeSync(fd);
|
|
51
|
+
try {
|
|
52
|
+
rmSync(tmp);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// rename succeeded, or a competing cleanup already removed the temp.
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
29
59
|
/** The deterministic bridge directory for a rynx session id. */
|
|
30
60
|
export function claudeBridgeDir(sessionId) {
|
|
31
61
|
return join(runtimeSessionStateDir(sessionId), "claude-bridge");
|
|
@@ -47,6 +77,7 @@ export function prepareClaudeBridgeDir(sessionId) {
|
|
|
47
77
|
INTERACTIONS_FILE,
|
|
48
78
|
INTERACTION_ACKS_FILE,
|
|
49
79
|
MANAGED_SETTINGS_FILE,
|
|
80
|
+
HOOK_FORWARDER_STATE_FILE,
|
|
50
81
|
]) {
|
|
51
82
|
try {
|
|
52
83
|
rmSync(join(dir, file));
|
|
@@ -267,17 +298,59 @@ function readJsonFile(path) {
|
|
|
267
298
|
function asString(value) {
|
|
268
299
|
return typeof value === "string" && value ? value : undefined;
|
|
269
300
|
}
|
|
301
|
+
const TERMINAL_BACKGROUND_TASK_STATUSES = new Set([
|
|
302
|
+
"completed",
|
|
303
|
+
"failed",
|
|
304
|
+
"stopped",
|
|
305
|
+
"killed",
|
|
306
|
+
]);
|
|
307
|
+
function backgroundTaskCount(payload) {
|
|
308
|
+
if (payload.hook_event_name !== "Stop")
|
|
309
|
+
return undefined;
|
|
310
|
+
const tasks = payload.background_tasks;
|
|
311
|
+
if (!Array.isArray(tasks))
|
|
312
|
+
return 0;
|
|
313
|
+
return tasks.filter((task) => {
|
|
314
|
+
if (!task || typeof task !== "object" || Array.isArray(task))
|
|
315
|
+
return true;
|
|
316
|
+
const status = task.status;
|
|
317
|
+
return typeof status !== "string" || !TERMINAL_BACKGROUND_TASK_STATUSES.has(status);
|
|
318
|
+
}).length;
|
|
319
|
+
}
|
|
270
320
|
export function readClaudeState(bridgeDir) {
|
|
271
321
|
const raw = readJsonFile(join(bridgeDir, STATE_FILE));
|
|
272
322
|
if (!raw || typeof raw !== "object")
|
|
273
323
|
return {};
|
|
274
324
|
const s = raw;
|
|
325
|
+
const seen = Array.isArray(s.seenClaudeSessionIds)
|
|
326
|
+
? s.seenClaudeSessionIds.filter((value) => typeof value === "string" && value.length > 0)
|
|
327
|
+
: [];
|
|
328
|
+
const current = asString(s.claudeSessionId);
|
|
329
|
+
if (current && !seen.includes(current))
|
|
330
|
+
seen.push(current);
|
|
275
331
|
return {
|
|
276
332
|
transcriptPath: asString(s.transcriptPath),
|
|
277
|
-
claudeSessionId:
|
|
333
|
+
claudeSessionId: current,
|
|
334
|
+
...(seen.length > 0 ? { seenClaudeSessionIds: seen } : {}),
|
|
278
335
|
lastHookEventName: asString(s.lastHookEventName),
|
|
279
336
|
};
|
|
280
337
|
}
|
|
338
|
+
/** Capture the pre-SessionStart identity before state.json is advanced. The
|
|
339
|
+
* observer hook and forwarder use these stable facts to distinguish a new
|
|
340
|
+
* branch from a later resume into an already-seen native Session. */
|
|
341
|
+
export function annotateClaudeResumeContext(bridgeDir, payload) {
|
|
342
|
+
if (payload.hook_event_name !== "SessionStart" || payload.source !== "resume")
|
|
343
|
+
return;
|
|
344
|
+
const next = asString(payload.session_id);
|
|
345
|
+
if (!next)
|
|
346
|
+
return;
|
|
347
|
+
const state = readClaudeState(bridgeDir);
|
|
348
|
+
if (state.claudeSessionId && state.claudeSessionId !== next) {
|
|
349
|
+
payload.rynx_previous_claude_session_id = state.claudeSessionId;
|
|
350
|
+
}
|
|
351
|
+
payload.rynx_claude_session_was_seen =
|
|
352
|
+
(state.seenClaudeSessionIds ?? []).includes(next);
|
|
353
|
+
}
|
|
281
354
|
/**
|
|
282
355
|
* Append one Claude hook payload to `hooks.jsonl` and fold its key fields into
|
|
283
356
|
* `state.json` (transcript path + claude session id + last event). The payload
|
|
@@ -288,31 +361,58 @@ export function readClaudeState(bridgeDir) {
|
|
|
288
361
|
*/
|
|
289
362
|
export function recordHookEvent(bridgeDir, payload) {
|
|
290
363
|
mkdirSync(bridgeDir, { recursive: true, mode: 0o700 });
|
|
291
|
-
|
|
292
|
-
|
|
364
|
+
if (payload.rynx_claude_session_was_seen === undefined) {
|
|
365
|
+
annotateClaudeResumeContext(bridgeDir, payload);
|
|
366
|
+
}
|
|
293
367
|
const state = readClaudeState(bridgeDir);
|
|
294
368
|
const eventName = asString(payload.hook_event_name);
|
|
295
369
|
if (eventName)
|
|
296
370
|
state.lastHookEventName = eventName;
|
|
297
|
-
const transcriptPath = asString(payload.transcript_path);
|
|
298
|
-
if (transcriptPath)
|
|
299
|
-
state.transcriptPath = transcriptPath;
|
|
300
371
|
const claudeSessionId = asString(payload.session_id);
|
|
301
|
-
|
|
302
|
-
state.claudeSessionId
|
|
303
|
-
|
|
372
|
+
const identityAllowed = eventName === "SessionStart" || (claudeSessionId !== undefined &&
|
|
373
|
+
(!state.claudeSessionId || claudeSessionId === state.claudeSessionId));
|
|
374
|
+
if (identityAllowed) {
|
|
375
|
+
const transcriptPath = asString(payload.transcript_path);
|
|
376
|
+
if (transcriptPath)
|
|
377
|
+
state.transcriptPath = transcriptPath;
|
|
378
|
+
if (claudeSessionId) {
|
|
379
|
+
state.claudeSessionId = claudeSessionId;
|
|
380
|
+
const seen = new Set(state.seenClaudeSessionIds ?? []);
|
|
381
|
+
seen.add(claudeSessionId);
|
|
382
|
+
state.seenClaudeSessionIds = [...seen].sort();
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
const envelope = { recordedAt: Date.now(), payload };
|
|
386
|
+
appendFileSync(join(bridgeDir, HOOKS_FILE), `${JSON.stringify(envelope)}\n`);
|
|
387
|
+
writeJsonFileAtomic(join(bridgeDir, STATE_FILE), state);
|
|
304
388
|
}
|
|
305
389
|
/** Read complete newline-terminated JSON records appended after `byteOffset`. */
|
|
306
390
|
export function readJsonlFrom(path, byteOffset) {
|
|
391
|
+
const result = readJsonlEntriesFrom(path, byteOffset);
|
|
392
|
+
return { records: result.records, nextOffset: result.nextOffset };
|
|
393
|
+
}
|
|
394
|
+
/** Read complete records with their stable byte/line source positions. */
|
|
395
|
+
export function readJsonlEntriesFrom(path, byteOffset, startLineCursor = 0) {
|
|
307
396
|
let info;
|
|
308
397
|
try {
|
|
309
398
|
info = statSync(path);
|
|
310
399
|
}
|
|
311
400
|
catch {
|
|
312
|
-
return {
|
|
401
|
+
return {
|
|
402
|
+
records: [],
|
|
403
|
+
entries: [],
|
|
404
|
+
nextOffset: byteOffset,
|
|
405
|
+
nextLineCursor: startLineCursor,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
if (info.size <= byteOffset) {
|
|
409
|
+
return {
|
|
410
|
+
records: [],
|
|
411
|
+
entries: [],
|
|
412
|
+
nextOffset: byteOffset,
|
|
413
|
+
nextLineCursor: startLineCursor,
|
|
414
|
+
};
|
|
313
415
|
}
|
|
314
|
-
if (info.size <= byteOffset)
|
|
315
|
-
return { records: [], nextOffset: byteOffset };
|
|
316
416
|
const fd = openSync(path, "r");
|
|
317
417
|
try {
|
|
318
418
|
const length = info.size - byteOffset;
|
|
@@ -323,41 +423,245 @@ export function readJsonlFrom(path, byteOffset) {
|
|
|
323
423
|
const trailing = lines.pop() ?? ""; // partial (no trailing newline yet)
|
|
324
424
|
const consumed = length - Buffer.byteLength(trailing, "utf8");
|
|
325
425
|
const records = [];
|
|
426
|
+
const entries = [];
|
|
427
|
+
let recordOffset = byteOffset;
|
|
428
|
+
let lineNumber = startLineCursor;
|
|
326
429
|
for (const line of lines) {
|
|
430
|
+
const lineOffset = recordOffset;
|
|
431
|
+
recordOffset += Buffer.byteLength(`${line}\n`, "utf8");
|
|
432
|
+
lineNumber += 1;
|
|
327
433
|
const trimmed = line.trim();
|
|
328
434
|
if (!trimmed)
|
|
329
435
|
continue;
|
|
330
436
|
try {
|
|
331
|
-
|
|
437
|
+
const record = JSON.parse(trimmed);
|
|
438
|
+
records.push(record);
|
|
439
|
+
entries.push({ record, byteOffset: lineOffset, lineNumber });
|
|
332
440
|
}
|
|
333
441
|
catch {
|
|
334
442
|
// Skip a malformed complete line; the offset still advances past it.
|
|
335
443
|
}
|
|
336
444
|
}
|
|
337
|
-
return {
|
|
445
|
+
return {
|
|
446
|
+
records,
|
|
447
|
+
entries,
|
|
448
|
+
nextOffset: byteOffset + consumed,
|
|
449
|
+
nextLineCursor: lineNumber,
|
|
450
|
+
};
|
|
338
451
|
}
|
|
339
452
|
finally {
|
|
340
453
|
closeSync(fd);
|
|
341
454
|
}
|
|
342
455
|
}
|
|
343
456
|
/** Tail `hooks.jsonl` from a byte offset into parsed {@link HookEvent}s. */
|
|
344
|
-
export function readHookEventsFrom(bridgeDir, byteOffset) {
|
|
345
|
-
const
|
|
457
|
+
export function readHookEventsFrom(bridgeDir, byteOffset, startEventCursor = 0) {
|
|
458
|
+
const path = join(bridgeDir, HOOKS_FILE);
|
|
459
|
+
let info;
|
|
460
|
+
try {
|
|
461
|
+
info = statSync(path);
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
return { events: [], nextOffset: byteOffset };
|
|
465
|
+
}
|
|
466
|
+
if (info.size <= byteOffset)
|
|
467
|
+
return { events: [], nextOffset: byteOffset };
|
|
468
|
+
const fd = openSync(path, "r");
|
|
346
469
|
const events = [];
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
470
|
+
let nextOffset = byteOffset;
|
|
471
|
+
let eventCursor = startEventCursor;
|
|
472
|
+
try {
|
|
473
|
+
const length = info.size - byteOffset;
|
|
474
|
+
const buf = Buffer.alloc(length);
|
|
475
|
+
readSync(fd, buf, 0, length, byteOffset);
|
|
476
|
+
const text = buf.toString("utf8");
|
|
477
|
+
const lines = text.split("\n");
|
|
478
|
+
lines.pop(); // retain a partial trailing record for the next poll
|
|
479
|
+
for (const line of lines) {
|
|
480
|
+
const lineBytes = Buffer.byteLength(`${line}\n`, "utf8");
|
|
481
|
+
nextOffset += lineBytes;
|
|
482
|
+
if (!line.trim())
|
|
483
|
+
continue;
|
|
484
|
+
eventCursor += 1;
|
|
485
|
+
let rec;
|
|
486
|
+
try {
|
|
487
|
+
rec = JSON.parse(line);
|
|
488
|
+
}
|
|
489
|
+
catch {
|
|
490
|
+
events.push({ eventCursor, byteOffset: nextOffset, payload: {} });
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
const payload = rec.payload;
|
|
494
|
+
if (!payload || typeof payload !== "object") {
|
|
495
|
+
events.push({ eventCursor, byteOffset: nextOffset, payload: {} });
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
const liveBackgroundTasks = backgroundTaskCount(payload);
|
|
499
|
+
events.push({
|
|
500
|
+
eventCursor,
|
|
501
|
+
byteOffset: nextOffset,
|
|
502
|
+
...(typeof rec.recordedAt === "number" && Number.isFinite(rec.recordedAt)
|
|
503
|
+
? { recordedAt: rec.recordedAt }
|
|
504
|
+
: {}),
|
|
505
|
+
eventName: asString(payload.hook_event_name),
|
|
506
|
+
transcriptPath: asString(payload.transcript_path),
|
|
507
|
+
sessionId: asString(payload.session_id),
|
|
508
|
+
source: asString(payload.source),
|
|
509
|
+
...(liveBackgroundTasks === undefined
|
|
510
|
+
? {}
|
|
511
|
+
: { backgroundTaskCount: liveBackgroundTasks }),
|
|
512
|
+
payload,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
finally {
|
|
517
|
+
closeSync(fd);
|
|
358
518
|
}
|
|
359
519
|
return { events, nextOffset };
|
|
360
520
|
}
|
|
521
|
+
/** Read and validate the durable hook cursor. A replaced/truncated hook stream
|
|
522
|
+
* restarts at zero; event handlers provide their own stable deduplication. */
|
|
523
|
+
export function readHookForwardState(bridgeDir) {
|
|
524
|
+
const raw = readJsonFile(join(bridgeDir, HOOK_FORWARDER_STATE_FILE));
|
|
525
|
+
if (!raw || typeof raw !== "object")
|
|
526
|
+
return undefined;
|
|
527
|
+
const state = raw;
|
|
528
|
+
const eventCursor = asNumber(state.eventCursor);
|
|
529
|
+
const byteOffset = asNumber(state.byteOffset);
|
|
530
|
+
const cursorFingerprint = asString(state.cursorFingerprint);
|
|
531
|
+
if (eventCursor === undefined || eventCursor < 0 || !Number.isInteger(eventCursor) ||
|
|
532
|
+
byteOffset === undefined || byteOffset < 0 || !Number.isInteger(byteOffset))
|
|
533
|
+
return undefined;
|
|
534
|
+
const current = jsonlCursorFingerprint(join(bridgeDir, HOOKS_FILE), byteOffset);
|
|
535
|
+
if (!cursorFingerprint || current !== cursorFingerprint) {
|
|
536
|
+
return {
|
|
537
|
+
eventCursor: 0,
|
|
538
|
+
byteOffset: 0,
|
|
539
|
+
...(jsonlCursorFingerprint(join(bridgeDir, HOOKS_FILE), 0)
|
|
540
|
+
? { cursorFingerprint: jsonlCursorFingerprint(join(bridgeDir, HOOKS_FILE), 0) }
|
|
541
|
+
: {}),
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
return { eventCursor, byteOffset, cursorFingerprint };
|
|
545
|
+
}
|
|
546
|
+
/** Persist one consumed hook record before another poll can observe it. */
|
|
547
|
+
export function writeHookForwardState(bridgeDir, state) {
|
|
548
|
+
mkdirSync(bridgeDir, { recursive: true, mode: 0o700 });
|
|
549
|
+
const tmp = join(bridgeDir, `${HOOK_FORWARDER_STATE_FILE}.tmp`);
|
|
550
|
+
writeFileSync(tmp, JSON.stringify(state));
|
|
551
|
+
renameSync(tmp, join(bridgeDir, HOOK_FORWARDER_STATE_FILE));
|
|
552
|
+
}
|
|
553
|
+
export function readCompactionForwardState(bridgeDir, generation) {
|
|
554
|
+
let raw;
|
|
555
|
+
try {
|
|
556
|
+
raw = JSON.parse(readFileSync(join(bridgeDir, COMPACTION_FORWARDER_STATE_FILE), "utf8"));
|
|
557
|
+
}
|
|
558
|
+
catch {
|
|
559
|
+
return {
|
|
560
|
+
generation,
|
|
561
|
+
lastSequence: 0,
|
|
562
|
+
persistedSequences: [],
|
|
563
|
+
lastPrecompactCursor: 0,
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
if (!raw || typeof raw !== "object") {
|
|
567
|
+
return {
|
|
568
|
+
generation,
|
|
569
|
+
lastSequence: 0,
|
|
570
|
+
persistedSequences: [],
|
|
571
|
+
lastPrecompactCursor: 0,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
const value = raw;
|
|
575
|
+
if (asString(value.generation) !== generation) {
|
|
576
|
+
return {
|
|
577
|
+
generation,
|
|
578
|
+
lastSequence: 0,
|
|
579
|
+
persistedSequences: [],
|
|
580
|
+
lastPrecompactCursor: 0,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
const lastSequence = asNumber(value.lastSequence);
|
|
584
|
+
const lastPrecompactCursor = asNumber(value.lastPrecompactCursor);
|
|
585
|
+
const persistedSequences = Array.isArray(value.persistedSequences)
|
|
586
|
+
? value.persistedSequences.filter((entry) => typeof entry === "number" && Number.isInteger(entry) && entry > 0).slice(-16)
|
|
587
|
+
: [];
|
|
588
|
+
const pendingRaw = value.pending;
|
|
589
|
+
let pending;
|
|
590
|
+
if (pendingRaw && typeof pendingRaw === "object") {
|
|
591
|
+
const candidate = pendingRaw;
|
|
592
|
+
const sequence = asNumber(candidate.sequence);
|
|
593
|
+
if (sequence !== undefined && Number.isInteger(sequence) && sequence > 0) {
|
|
594
|
+
pending = {
|
|
595
|
+
sequence,
|
|
596
|
+
...(asString(candidate.claudeSessionId)
|
|
597
|
+
? { claudeSessionId: asString(candidate.claudeSessionId) }
|
|
598
|
+
: {}),
|
|
599
|
+
...(asString(candidate.transcriptPath)
|
|
600
|
+
? { transcriptPath: asString(candidate.transcriptPath) }
|
|
601
|
+
: {}),
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
const expectCompletionAckSequence = asNumber(value.expectCompletionAckSequence);
|
|
606
|
+
return {
|
|
607
|
+
generation,
|
|
608
|
+
lastSequence: lastSequence !== undefined && Number.isInteger(lastSequence) && lastSequence >= 0
|
|
609
|
+
? lastSequence
|
|
610
|
+
: 0,
|
|
611
|
+
persistedSequences,
|
|
612
|
+
lastPrecompactCursor: lastPrecompactCursor !== undefined && Number.isInteger(lastPrecompactCursor) &&
|
|
613
|
+
lastPrecompactCursor >= 0
|
|
614
|
+
? lastPrecompactCursor
|
|
615
|
+
: 0,
|
|
616
|
+
...(pending ? { pending } : {}),
|
|
617
|
+
...(expectCompletionAckSequence !== undefined &&
|
|
618
|
+
Number.isInteger(expectCompletionAckSequence) && expectCompletionAckSequence > 0
|
|
619
|
+
? { expectCompletionAckSequence }
|
|
620
|
+
: {}),
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
export function writeCompactionForwardState(bridgeDir, state) {
|
|
624
|
+
mkdirSync(bridgeDir, { recursive: true, mode: 0o700 });
|
|
625
|
+
const tmp = join(bridgeDir, `${COMPACTION_FORWARDER_STATE_FILE}.tmp`);
|
|
626
|
+
writeFileSync(tmp, JSON.stringify({
|
|
627
|
+
...state,
|
|
628
|
+
persistedSequences: state.persistedSequences.slice(-16),
|
|
629
|
+
}));
|
|
630
|
+
renameSync(tmp, join(bridgeDir, COMPACTION_FORWARDER_STATE_FILE));
|
|
631
|
+
}
|
|
632
|
+
/** Read the process-wide MessageDisplay cursor. It deliberately survives
|
|
633
|
+
* logical Session rotation; if the append log was truncated by a fresh Claude
|
|
634
|
+
* process, restart at zero. */
|
|
635
|
+
export function readDeltaForwardState(bridgeDir) {
|
|
636
|
+
let raw;
|
|
637
|
+
try {
|
|
638
|
+
raw = JSON.parse(readFileSync(join(bridgeDir, DELTA_FORWARDER_STATE_FILE), "utf8"));
|
|
639
|
+
}
|
|
640
|
+
catch {
|
|
641
|
+
return { byteOffset: 0 };
|
|
642
|
+
}
|
|
643
|
+
const byteOffset = raw && typeof raw === "object"
|
|
644
|
+
? asNumber(raw.byteOffset)
|
|
645
|
+
: undefined;
|
|
646
|
+
if (byteOffset === undefined || byteOffset < 0 || !Number.isInteger(byteOffset)) {
|
|
647
|
+
return { byteOffset: 0 };
|
|
648
|
+
}
|
|
649
|
+
try {
|
|
650
|
+
if (statSync(join(bridgeDir, DELTAS_FILE)).size < byteOffset)
|
|
651
|
+
return { byteOffset: 0 };
|
|
652
|
+
}
|
|
653
|
+
catch {
|
|
654
|
+
if (byteOffset > 0)
|
|
655
|
+
return { byteOffset: 0 };
|
|
656
|
+
}
|
|
657
|
+
return { byteOffset };
|
|
658
|
+
}
|
|
659
|
+
export function writeDeltaForwardState(bridgeDir, state) {
|
|
660
|
+
mkdirSync(bridgeDir, { recursive: true, mode: 0o700 });
|
|
661
|
+
const tmp = join(bridgeDir, `${DELTA_FORWARDER_STATE_FILE}.tmp`);
|
|
662
|
+
writeFileSync(tmp, JSON.stringify({ byteOffset: state.byteOffset }));
|
|
663
|
+
renameSync(tmp, join(bridgeDir, DELTA_FORWARDER_STATE_FILE));
|
|
664
|
+
}
|
|
361
665
|
/** Tail `message_deltas.jsonl` from a byte offset into {@link MessageDelta}s. */
|
|
362
666
|
export function readMessageDeltasFrom(bridgeDir, byteOffset) {
|
|
363
667
|
const { records, nextOffset } = readJsonlFrom(join(bridgeDir, DELTAS_FILE), byteOffset);
|
|
@@ -451,7 +755,36 @@ export function readForwardState(bridgeDir) {
|
|
|
451
755
|
? s.seenSourceIds.filter((x) => typeof x === "string")
|
|
452
756
|
: [];
|
|
453
757
|
const cursorFingerprint = asString(s.cursorFingerprint);
|
|
454
|
-
|
|
758
|
+
const lineCursor = asNumber(s.lineCursor);
|
|
759
|
+
const currentTurnId = asString(s.currentTurnId);
|
|
760
|
+
const currentResponseId = asString(s.currentResponseId);
|
|
761
|
+
const activeTerminalCommandRaw = s.activeTerminalCommand;
|
|
762
|
+
const activeTerminalCommandRecord = activeTerminalCommandRaw &&
|
|
763
|
+
typeof activeTerminalCommandRaw === "object" &&
|
|
764
|
+
!Array.isArray(activeTerminalCommandRaw)
|
|
765
|
+
? activeTerminalCommandRaw
|
|
766
|
+
: undefined;
|
|
767
|
+
const activeTerminalCommandText = asString(activeTerminalCommandRecord?.command);
|
|
768
|
+
const activeTerminalCommandTurnId = asString(activeTerminalCommandRecord?.turnId);
|
|
769
|
+
const activeTerminalCommand = activeTerminalCommandText
|
|
770
|
+
? {
|
|
771
|
+
command: activeTerminalCommandText,
|
|
772
|
+
...(activeTerminalCommandTurnId ? { turnId: activeTerminalCommandTurnId } : {}),
|
|
773
|
+
}
|
|
774
|
+
: undefined;
|
|
775
|
+
return {
|
|
776
|
+
transcriptPath,
|
|
777
|
+
byteOffset,
|
|
778
|
+
seenSourceIds,
|
|
779
|
+
...(lineCursor !== undefined && Number.isInteger(lineCursor) && lineCursor >= 0
|
|
780
|
+
? { lineCursor }
|
|
781
|
+
: {}),
|
|
782
|
+
...(cursorFingerprint ? { cursorFingerprint } : {}),
|
|
783
|
+
...(currentTurnId ? { currentTurnId } : {}),
|
|
784
|
+
...(currentResponseId ? { currentResponseId } : {}),
|
|
785
|
+
...(s.turnOpen === true ? { turnOpen: true } : {}),
|
|
786
|
+
...(activeTerminalCommand ? { activeTerminalCommand } : {}),
|
|
787
|
+
};
|
|
455
788
|
}
|
|
456
789
|
/** Drop the forwarder cursor (a `/clear` starts fresh). */
|
|
457
790
|
export function resetForwardState(bridgeDir) {
|
|
@@ -462,6 +795,88 @@ export function resetForwardState(bridgeDir) {
|
|
|
462
795
|
// absent — fine
|
|
463
796
|
}
|
|
464
797
|
}
|
|
798
|
+
/** Parent Task/tool ids already consumed by the main transcript cursor. Kept
|
|
799
|
+
* independently of concrete child agents because Claude may create the child
|
|
800
|
+
* meta file only after the parent record was durably acknowledged. */
|
|
801
|
+
export function readSubagentParentResponses(bridgeDir, parentTranscriptPath) {
|
|
802
|
+
const raw = readJsonFile(join(bridgeDir, SUBAGENT_FORWARDER_STATE_FILE));
|
|
803
|
+
if (!raw || typeof raw !== "object")
|
|
804
|
+
return new Map();
|
|
805
|
+
const file = raw;
|
|
806
|
+
if (file.parentTranscriptPath !== parentTranscriptPath)
|
|
807
|
+
return new Map();
|
|
808
|
+
const responses = file.parentResponses;
|
|
809
|
+
if (!responses || typeof responses !== "object" || Array.isArray(responses))
|
|
810
|
+
return new Map();
|
|
811
|
+
const result = new Map();
|
|
812
|
+
for (const [parentToolCallId, responseId] of Object.entries(responses)) {
|
|
813
|
+
if (!parentToolCallId || typeof responseId !== "string" || !responseId)
|
|
814
|
+
continue;
|
|
815
|
+
result.set(parentToolCallId, responseId);
|
|
816
|
+
}
|
|
817
|
+
return result;
|
|
818
|
+
}
|
|
819
|
+
/** Read independent Claude native sub-agent cursors for one parent transcript. */
|
|
820
|
+
export function readSubagentForwardStates(bridgeDir, parentTranscriptPath) {
|
|
821
|
+
const raw = readJsonFile(join(bridgeDir, SUBAGENT_FORWARDER_STATE_FILE));
|
|
822
|
+
if (!raw || typeof raw !== "object")
|
|
823
|
+
return [];
|
|
824
|
+
const file = raw;
|
|
825
|
+
if (file.parentTranscriptPath !== parentTranscriptPath || !Array.isArray(file.agents)) {
|
|
826
|
+
return [];
|
|
827
|
+
}
|
|
828
|
+
const states = [];
|
|
829
|
+
for (const value of file.agents) {
|
|
830
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
831
|
+
continue;
|
|
832
|
+
const state = value;
|
|
833
|
+
const agentId = asString(state.agentId);
|
|
834
|
+
const parentToolCallId = asString(state.parentToolCallId);
|
|
835
|
+
const parentResponseId = asString(state.parentResponseId);
|
|
836
|
+
const transcriptPath = asString(state.transcriptPath);
|
|
837
|
+
const byteOffset = asNumber(state.byteOffset);
|
|
838
|
+
const lineCursor = asNumber(state.lineCursor);
|
|
839
|
+
const terminalCommand = asString(state.terminalCommand);
|
|
840
|
+
if (!agentId || !parentToolCallId || !transcriptPath || byteOffset === undefined)
|
|
841
|
+
continue;
|
|
842
|
+
states.push({
|
|
843
|
+
agentId,
|
|
844
|
+
parentToolCallId,
|
|
845
|
+
...(parentResponseId ? { parentResponseId } : {}),
|
|
846
|
+
transcriptPath,
|
|
847
|
+
byteOffset,
|
|
848
|
+
...(lineCursor !== undefined && Number.isInteger(lineCursor) && lineCursor >= 0
|
|
849
|
+
? { lineCursor }
|
|
850
|
+
: {}),
|
|
851
|
+
...(terminalCommand ? { terminalCommand } : {}),
|
|
852
|
+
seenSourceIds: Array.isArray(state.seenSourceIds)
|
|
853
|
+
? state.seenSourceIds
|
|
854
|
+
.filter((id) => typeof id === "string")
|
|
855
|
+
.slice(-MAX_SEEN_SOURCE_IDS)
|
|
856
|
+
: [],
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
return states;
|
|
860
|
+
}
|
|
861
|
+
/** Atomically persist every native sub-agent's independent delivery cursor. */
|
|
862
|
+
export function writeSubagentForwardStates(bridgeDir, parentTranscriptPath, states, parentResponses = new Map()) {
|
|
863
|
+
mkdirSync(bridgeDir, { recursive: true, mode: 0o700 });
|
|
864
|
+
const file = {
|
|
865
|
+
parentTranscriptPath,
|
|
866
|
+
parentResponses: Object.fromEntries([...parentResponses].slice(-MAX_SEEN_SOURCE_IDS)),
|
|
867
|
+
agents: states.map((state) => ({
|
|
868
|
+
...state,
|
|
869
|
+
seenSourceIds: state.seenSourceIds.slice(-MAX_SEEN_SOURCE_IDS),
|
|
870
|
+
})),
|
|
871
|
+
};
|
|
872
|
+
const target = join(bridgeDir, SUBAGENT_FORWARDER_STATE_FILE);
|
|
873
|
+
const tmp = `${target}.tmp`;
|
|
874
|
+
writeFileSync(tmp, JSON.stringify(file));
|
|
875
|
+
renameSync(tmp, target);
|
|
876
|
+
}
|
|
877
|
+
export function resetSubagentForwardStates(bridgeDir) {
|
|
878
|
+
rmSync(join(bridgeDir, SUBAGENT_FORWARDER_STATE_FILE), { force: true });
|
|
879
|
+
}
|
|
465
880
|
/** Read the latest `status.json` snapshot, or undefined if absent/malformed. */
|
|
466
881
|
export function readClaudeStatus(bridgeDir) {
|
|
467
882
|
const raw = readJsonFile(join(bridgeDir, STATUS_FILE));
|