@rynx-ai/runtime 0.1.11-beta.5 → 0.1.11-beta.50
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 +460 -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 +1571 -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 +183 -46
- 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 +96 -3
- package/dist/codex-app-server/forwarder.js +542 -57
- package/dist/codex-app-server/mapping.d.ts +3 -6
- package/dist/codex-app-server/mapping.js +228 -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 +68 -7
- package/dist/codex-app-server/transport.d.ts +2 -2
- package/dist/codex-app-server/transport.js +4 -0
- package/dist/codex-app-server/ws-channel.d.ts +8 -1
- package/dist/codex-app-server/ws-channel.js +113 -28
- package/dist/codex-child-env.js +3 -32
- 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 +106 -48
- package/dist/host.js +2024 -640
- 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.js +12 -3
- package/dist/runner/child.d.ts +105 -28
- package/dist/runner/child.js +1625 -100
- package/dist/runner/manager.d.ts +114 -46
- package/dist/runner/manager.js +1523 -446
- 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/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, copyFileSync, 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");
|
|
@@ -39,6 +69,21 @@ export function prepareClaudeBridgeDir(sessionId) {
|
|
|
39
69
|
const dir = claudeBridgeDir(sessionId);
|
|
40
70
|
adoptLegacyRuntimeDirectory(join(legacyRuntimeStateRoot(), "claude-native", runtimeSessionDigest(sessionId)), dir);
|
|
41
71
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
72
|
+
// Preserve original bridge logs before the normal runtime refresh clears its
|
|
73
|
+
// rendezvous. Collection itself never calls this function or changes files.
|
|
74
|
+
const history = join(dir, "history", randomUUID());
|
|
75
|
+
for (const file of [HOOKS_FILE, STATE_FILE, DELTAS_FILE, INTERACTIONS_FILE, INTERACTION_ACKS_FILE]) {
|
|
76
|
+
try {
|
|
77
|
+
if (!statSync(join(dir, file)).isFile())
|
|
78
|
+
continue;
|
|
79
|
+
mkdirSync(history, { recursive: true, mode: 0o700 });
|
|
80
|
+
copyFileSync(join(dir, file), join(history, file));
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (error.code !== "ENOENT")
|
|
84
|
+
process.stderr.write(`Claude bridge log preservation failed: ${String(error)}\n`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
42
87
|
for (const file of [
|
|
43
88
|
HOOKS_FILE,
|
|
44
89
|
STATE_FILE,
|
|
@@ -47,6 +92,7 @@ export function prepareClaudeBridgeDir(sessionId) {
|
|
|
47
92
|
INTERACTIONS_FILE,
|
|
48
93
|
INTERACTION_ACKS_FILE,
|
|
49
94
|
MANAGED_SETTINGS_FILE,
|
|
95
|
+
HOOK_FORWARDER_STATE_FILE,
|
|
50
96
|
]) {
|
|
51
97
|
try {
|
|
52
98
|
rmSync(join(dir, file));
|
|
@@ -267,17 +313,59 @@ function readJsonFile(path) {
|
|
|
267
313
|
function asString(value) {
|
|
268
314
|
return typeof value === "string" && value ? value : undefined;
|
|
269
315
|
}
|
|
316
|
+
const TERMINAL_BACKGROUND_TASK_STATUSES = new Set([
|
|
317
|
+
"completed",
|
|
318
|
+
"failed",
|
|
319
|
+
"stopped",
|
|
320
|
+
"killed",
|
|
321
|
+
]);
|
|
322
|
+
function backgroundTaskCount(payload) {
|
|
323
|
+
if (payload.hook_event_name !== "Stop")
|
|
324
|
+
return undefined;
|
|
325
|
+
const tasks = payload.background_tasks;
|
|
326
|
+
if (!Array.isArray(tasks))
|
|
327
|
+
return 0;
|
|
328
|
+
return tasks.filter((task) => {
|
|
329
|
+
if (!task || typeof task !== "object" || Array.isArray(task))
|
|
330
|
+
return true;
|
|
331
|
+
const status = task.status;
|
|
332
|
+
return typeof status !== "string" || !TERMINAL_BACKGROUND_TASK_STATUSES.has(status);
|
|
333
|
+
}).length;
|
|
334
|
+
}
|
|
270
335
|
export function readClaudeState(bridgeDir) {
|
|
271
336
|
const raw = readJsonFile(join(bridgeDir, STATE_FILE));
|
|
272
337
|
if (!raw || typeof raw !== "object")
|
|
273
338
|
return {};
|
|
274
339
|
const s = raw;
|
|
340
|
+
const seen = Array.isArray(s.seenClaudeSessionIds)
|
|
341
|
+
? s.seenClaudeSessionIds.filter((value) => typeof value === "string" && value.length > 0)
|
|
342
|
+
: [];
|
|
343
|
+
const current = asString(s.claudeSessionId);
|
|
344
|
+
if (current && !seen.includes(current))
|
|
345
|
+
seen.push(current);
|
|
275
346
|
return {
|
|
276
347
|
transcriptPath: asString(s.transcriptPath),
|
|
277
|
-
claudeSessionId:
|
|
348
|
+
claudeSessionId: current,
|
|
349
|
+
...(seen.length > 0 ? { seenClaudeSessionIds: seen } : {}),
|
|
278
350
|
lastHookEventName: asString(s.lastHookEventName),
|
|
279
351
|
};
|
|
280
352
|
}
|
|
353
|
+
/** Capture the pre-SessionStart identity before state.json is advanced. The
|
|
354
|
+
* observer hook and forwarder use these stable facts to distinguish a new
|
|
355
|
+
* branch from a later resume into an already-seen native Session. */
|
|
356
|
+
export function annotateClaudeResumeContext(bridgeDir, payload) {
|
|
357
|
+
if (payload.hook_event_name !== "SessionStart" || payload.source !== "resume")
|
|
358
|
+
return;
|
|
359
|
+
const next = asString(payload.session_id);
|
|
360
|
+
if (!next)
|
|
361
|
+
return;
|
|
362
|
+
const state = readClaudeState(bridgeDir);
|
|
363
|
+
if (state.claudeSessionId && state.claudeSessionId !== next) {
|
|
364
|
+
payload.rynx_previous_claude_session_id = state.claudeSessionId;
|
|
365
|
+
}
|
|
366
|
+
payload.rynx_claude_session_was_seen =
|
|
367
|
+
(state.seenClaudeSessionIds ?? []).includes(next);
|
|
368
|
+
}
|
|
281
369
|
/**
|
|
282
370
|
* Append one Claude hook payload to `hooks.jsonl` and fold its key fields into
|
|
283
371
|
* `state.json` (transcript path + claude session id + last event). The payload
|
|
@@ -288,31 +376,58 @@ export function readClaudeState(bridgeDir) {
|
|
|
288
376
|
*/
|
|
289
377
|
export function recordHookEvent(bridgeDir, payload) {
|
|
290
378
|
mkdirSync(bridgeDir, { recursive: true, mode: 0o700 });
|
|
291
|
-
|
|
292
|
-
|
|
379
|
+
if (payload.rynx_claude_session_was_seen === undefined) {
|
|
380
|
+
annotateClaudeResumeContext(bridgeDir, payload);
|
|
381
|
+
}
|
|
293
382
|
const state = readClaudeState(bridgeDir);
|
|
294
383
|
const eventName = asString(payload.hook_event_name);
|
|
295
384
|
if (eventName)
|
|
296
385
|
state.lastHookEventName = eventName;
|
|
297
|
-
const transcriptPath = asString(payload.transcript_path);
|
|
298
|
-
if (transcriptPath)
|
|
299
|
-
state.transcriptPath = transcriptPath;
|
|
300
386
|
const claudeSessionId = asString(payload.session_id);
|
|
301
|
-
|
|
302
|
-
state.claudeSessionId
|
|
303
|
-
|
|
387
|
+
const identityAllowed = eventName === "SessionStart" || (claudeSessionId !== undefined &&
|
|
388
|
+
(!state.claudeSessionId || claudeSessionId === state.claudeSessionId));
|
|
389
|
+
if (identityAllowed) {
|
|
390
|
+
const transcriptPath = asString(payload.transcript_path);
|
|
391
|
+
if (transcriptPath)
|
|
392
|
+
state.transcriptPath = transcriptPath;
|
|
393
|
+
if (claudeSessionId) {
|
|
394
|
+
state.claudeSessionId = claudeSessionId;
|
|
395
|
+
const seen = new Set(state.seenClaudeSessionIds ?? []);
|
|
396
|
+
seen.add(claudeSessionId);
|
|
397
|
+
state.seenClaudeSessionIds = [...seen].sort();
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
const envelope = { recordedAt: Date.now(), payload };
|
|
401
|
+
appendFileSync(join(bridgeDir, HOOKS_FILE), `${JSON.stringify(envelope)}\n`);
|
|
402
|
+
writeJsonFileAtomic(join(bridgeDir, STATE_FILE), state);
|
|
304
403
|
}
|
|
305
404
|
/** Read complete newline-terminated JSON records appended after `byteOffset`. */
|
|
306
405
|
export function readJsonlFrom(path, byteOffset) {
|
|
406
|
+
const result = readJsonlEntriesFrom(path, byteOffset);
|
|
407
|
+
return { records: result.records, nextOffset: result.nextOffset };
|
|
408
|
+
}
|
|
409
|
+
/** Read complete records with their stable byte/line source positions. */
|
|
410
|
+
export function readJsonlEntriesFrom(path, byteOffset, startLineCursor = 0) {
|
|
307
411
|
let info;
|
|
308
412
|
try {
|
|
309
413
|
info = statSync(path);
|
|
310
414
|
}
|
|
311
415
|
catch {
|
|
312
|
-
return {
|
|
416
|
+
return {
|
|
417
|
+
records: [],
|
|
418
|
+
entries: [],
|
|
419
|
+
nextOffset: byteOffset,
|
|
420
|
+
nextLineCursor: startLineCursor,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
if (info.size <= byteOffset) {
|
|
424
|
+
return {
|
|
425
|
+
records: [],
|
|
426
|
+
entries: [],
|
|
427
|
+
nextOffset: byteOffset,
|
|
428
|
+
nextLineCursor: startLineCursor,
|
|
429
|
+
};
|
|
313
430
|
}
|
|
314
|
-
if (info.size <= byteOffset)
|
|
315
|
-
return { records: [], nextOffset: byteOffset };
|
|
316
431
|
const fd = openSync(path, "r");
|
|
317
432
|
try {
|
|
318
433
|
const length = info.size - byteOffset;
|
|
@@ -323,41 +438,245 @@ export function readJsonlFrom(path, byteOffset) {
|
|
|
323
438
|
const trailing = lines.pop() ?? ""; // partial (no trailing newline yet)
|
|
324
439
|
const consumed = length - Buffer.byteLength(trailing, "utf8");
|
|
325
440
|
const records = [];
|
|
441
|
+
const entries = [];
|
|
442
|
+
let recordOffset = byteOffset;
|
|
443
|
+
let lineNumber = startLineCursor;
|
|
326
444
|
for (const line of lines) {
|
|
445
|
+
const lineOffset = recordOffset;
|
|
446
|
+
recordOffset += Buffer.byteLength(`${line}\n`, "utf8");
|
|
447
|
+
lineNumber += 1;
|
|
327
448
|
const trimmed = line.trim();
|
|
328
449
|
if (!trimmed)
|
|
329
450
|
continue;
|
|
330
451
|
try {
|
|
331
|
-
|
|
452
|
+
const record = JSON.parse(trimmed);
|
|
453
|
+
records.push(record);
|
|
454
|
+
entries.push({ record, byteOffset: lineOffset, lineNumber });
|
|
332
455
|
}
|
|
333
456
|
catch {
|
|
334
457
|
// Skip a malformed complete line; the offset still advances past it.
|
|
335
458
|
}
|
|
336
459
|
}
|
|
337
|
-
return {
|
|
460
|
+
return {
|
|
461
|
+
records,
|
|
462
|
+
entries,
|
|
463
|
+
nextOffset: byteOffset + consumed,
|
|
464
|
+
nextLineCursor: lineNumber,
|
|
465
|
+
};
|
|
338
466
|
}
|
|
339
467
|
finally {
|
|
340
468
|
closeSync(fd);
|
|
341
469
|
}
|
|
342
470
|
}
|
|
343
471
|
/** Tail `hooks.jsonl` from a byte offset into parsed {@link HookEvent}s. */
|
|
344
|
-
export function readHookEventsFrom(bridgeDir, byteOffset) {
|
|
345
|
-
const
|
|
472
|
+
export function readHookEventsFrom(bridgeDir, byteOffset, startEventCursor = 0) {
|
|
473
|
+
const path = join(bridgeDir, HOOKS_FILE);
|
|
474
|
+
let info;
|
|
475
|
+
try {
|
|
476
|
+
info = statSync(path);
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
return { events: [], nextOffset: byteOffset };
|
|
480
|
+
}
|
|
481
|
+
if (info.size <= byteOffset)
|
|
482
|
+
return { events: [], nextOffset: byteOffset };
|
|
483
|
+
const fd = openSync(path, "r");
|
|
346
484
|
const events = [];
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
485
|
+
let nextOffset = byteOffset;
|
|
486
|
+
let eventCursor = startEventCursor;
|
|
487
|
+
try {
|
|
488
|
+
const length = info.size - byteOffset;
|
|
489
|
+
const buf = Buffer.alloc(length);
|
|
490
|
+
readSync(fd, buf, 0, length, byteOffset);
|
|
491
|
+
const text = buf.toString("utf8");
|
|
492
|
+
const lines = text.split("\n");
|
|
493
|
+
lines.pop(); // retain a partial trailing record for the next poll
|
|
494
|
+
for (const line of lines) {
|
|
495
|
+
const lineBytes = Buffer.byteLength(`${line}\n`, "utf8");
|
|
496
|
+
nextOffset += lineBytes;
|
|
497
|
+
if (!line.trim())
|
|
498
|
+
continue;
|
|
499
|
+
eventCursor += 1;
|
|
500
|
+
let rec;
|
|
501
|
+
try {
|
|
502
|
+
rec = JSON.parse(line);
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
events.push({ eventCursor, byteOffset: nextOffset, payload: {} });
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
const payload = rec.payload;
|
|
509
|
+
if (!payload || typeof payload !== "object") {
|
|
510
|
+
events.push({ eventCursor, byteOffset: nextOffset, payload: {} });
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
const liveBackgroundTasks = backgroundTaskCount(payload);
|
|
514
|
+
events.push({
|
|
515
|
+
eventCursor,
|
|
516
|
+
byteOffset: nextOffset,
|
|
517
|
+
...(typeof rec.recordedAt === "number" && Number.isFinite(rec.recordedAt)
|
|
518
|
+
? { recordedAt: rec.recordedAt }
|
|
519
|
+
: {}),
|
|
520
|
+
eventName: asString(payload.hook_event_name),
|
|
521
|
+
transcriptPath: asString(payload.transcript_path),
|
|
522
|
+
sessionId: asString(payload.session_id),
|
|
523
|
+
source: asString(payload.source),
|
|
524
|
+
...(liveBackgroundTasks === undefined
|
|
525
|
+
? {}
|
|
526
|
+
: { backgroundTaskCount: liveBackgroundTasks }),
|
|
527
|
+
payload,
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
finally {
|
|
532
|
+
closeSync(fd);
|
|
358
533
|
}
|
|
359
534
|
return { events, nextOffset };
|
|
360
535
|
}
|
|
536
|
+
/** Read and validate the durable hook cursor. A replaced/truncated hook stream
|
|
537
|
+
* restarts at zero; event handlers provide their own stable deduplication. */
|
|
538
|
+
export function readHookForwardState(bridgeDir) {
|
|
539
|
+
const raw = readJsonFile(join(bridgeDir, HOOK_FORWARDER_STATE_FILE));
|
|
540
|
+
if (!raw || typeof raw !== "object")
|
|
541
|
+
return undefined;
|
|
542
|
+
const state = raw;
|
|
543
|
+
const eventCursor = asNumber(state.eventCursor);
|
|
544
|
+
const byteOffset = asNumber(state.byteOffset);
|
|
545
|
+
const cursorFingerprint = asString(state.cursorFingerprint);
|
|
546
|
+
if (eventCursor === undefined || eventCursor < 0 || !Number.isInteger(eventCursor) ||
|
|
547
|
+
byteOffset === undefined || byteOffset < 0 || !Number.isInteger(byteOffset))
|
|
548
|
+
return undefined;
|
|
549
|
+
const current = jsonlCursorFingerprint(join(bridgeDir, HOOKS_FILE), byteOffset);
|
|
550
|
+
if (!cursorFingerprint || current !== cursorFingerprint) {
|
|
551
|
+
return {
|
|
552
|
+
eventCursor: 0,
|
|
553
|
+
byteOffset: 0,
|
|
554
|
+
...(jsonlCursorFingerprint(join(bridgeDir, HOOKS_FILE), 0)
|
|
555
|
+
? { cursorFingerprint: jsonlCursorFingerprint(join(bridgeDir, HOOKS_FILE), 0) }
|
|
556
|
+
: {}),
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
return { eventCursor, byteOffset, cursorFingerprint };
|
|
560
|
+
}
|
|
561
|
+
/** Persist one consumed hook record before another poll can observe it. */
|
|
562
|
+
export function writeHookForwardState(bridgeDir, state) {
|
|
563
|
+
mkdirSync(bridgeDir, { recursive: true, mode: 0o700 });
|
|
564
|
+
const tmp = join(bridgeDir, `${HOOK_FORWARDER_STATE_FILE}.tmp`);
|
|
565
|
+
writeFileSync(tmp, JSON.stringify(state));
|
|
566
|
+
renameSync(tmp, join(bridgeDir, HOOK_FORWARDER_STATE_FILE));
|
|
567
|
+
}
|
|
568
|
+
export function readCompactionForwardState(bridgeDir, generation) {
|
|
569
|
+
let raw;
|
|
570
|
+
try {
|
|
571
|
+
raw = JSON.parse(readFileSync(join(bridgeDir, COMPACTION_FORWARDER_STATE_FILE), "utf8"));
|
|
572
|
+
}
|
|
573
|
+
catch {
|
|
574
|
+
return {
|
|
575
|
+
generation,
|
|
576
|
+
lastSequence: 0,
|
|
577
|
+
persistedSequences: [],
|
|
578
|
+
lastPrecompactCursor: 0,
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
if (!raw || typeof raw !== "object") {
|
|
582
|
+
return {
|
|
583
|
+
generation,
|
|
584
|
+
lastSequence: 0,
|
|
585
|
+
persistedSequences: [],
|
|
586
|
+
lastPrecompactCursor: 0,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
const value = raw;
|
|
590
|
+
if (asString(value.generation) !== generation) {
|
|
591
|
+
return {
|
|
592
|
+
generation,
|
|
593
|
+
lastSequence: 0,
|
|
594
|
+
persistedSequences: [],
|
|
595
|
+
lastPrecompactCursor: 0,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
const lastSequence = asNumber(value.lastSequence);
|
|
599
|
+
const lastPrecompactCursor = asNumber(value.lastPrecompactCursor);
|
|
600
|
+
const persistedSequences = Array.isArray(value.persistedSequences)
|
|
601
|
+
? value.persistedSequences.filter((entry) => typeof entry === "number" && Number.isInteger(entry) && entry > 0).slice(-16)
|
|
602
|
+
: [];
|
|
603
|
+
const pendingRaw = value.pending;
|
|
604
|
+
let pending;
|
|
605
|
+
if (pendingRaw && typeof pendingRaw === "object") {
|
|
606
|
+
const candidate = pendingRaw;
|
|
607
|
+
const sequence = asNumber(candidate.sequence);
|
|
608
|
+
if (sequence !== undefined && Number.isInteger(sequence) && sequence > 0) {
|
|
609
|
+
pending = {
|
|
610
|
+
sequence,
|
|
611
|
+
...(asString(candidate.claudeSessionId)
|
|
612
|
+
? { claudeSessionId: asString(candidate.claudeSessionId) }
|
|
613
|
+
: {}),
|
|
614
|
+
...(asString(candidate.transcriptPath)
|
|
615
|
+
? { transcriptPath: asString(candidate.transcriptPath) }
|
|
616
|
+
: {}),
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
const expectCompletionAckSequence = asNumber(value.expectCompletionAckSequence);
|
|
621
|
+
return {
|
|
622
|
+
generation,
|
|
623
|
+
lastSequence: lastSequence !== undefined && Number.isInteger(lastSequence) && lastSequence >= 0
|
|
624
|
+
? lastSequence
|
|
625
|
+
: 0,
|
|
626
|
+
persistedSequences,
|
|
627
|
+
lastPrecompactCursor: lastPrecompactCursor !== undefined && Number.isInteger(lastPrecompactCursor) &&
|
|
628
|
+
lastPrecompactCursor >= 0
|
|
629
|
+
? lastPrecompactCursor
|
|
630
|
+
: 0,
|
|
631
|
+
...(pending ? { pending } : {}),
|
|
632
|
+
...(expectCompletionAckSequence !== undefined &&
|
|
633
|
+
Number.isInteger(expectCompletionAckSequence) && expectCompletionAckSequence > 0
|
|
634
|
+
? { expectCompletionAckSequence }
|
|
635
|
+
: {}),
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
export function writeCompactionForwardState(bridgeDir, state) {
|
|
639
|
+
mkdirSync(bridgeDir, { recursive: true, mode: 0o700 });
|
|
640
|
+
const tmp = join(bridgeDir, `${COMPACTION_FORWARDER_STATE_FILE}.tmp`);
|
|
641
|
+
writeFileSync(tmp, JSON.stringify({
|
|
642
|
+
...state,
|
|
643
|
+
persistedSequences: state.persistedSequences.slice(-16),
|
|
644
|
+
}));
|
|
645
|
+
renameSync(tmp, join(bridgeDir, COMPACTION_FORWARDER_STATE_FILE));
|
|
646
|
+
}
|
|
647
|
+
/** Read the process-wide MessageDisplay cursor. It deliberately survives
|
|
648
|
+
* logical Session rotation; if the append log was truncated by a fresh Claude
|
|
649
|
+
* process, restart at zero. */
|
|
650
|
+
export function readDeltaForwardState(bridgeDir) {
|
|
651
|
+
let raw;
|
|
652
|
+
try {
|
|
653
|
+
raw = JSON.parse(readFileSync(join(bridgeDir, DELTA_FORWARDER_STATE_FILE), "utf8"));
|
|
654
|
+
}
|
|
655
|
+
catch {
|
|
656
|
+
return { byteOffset: 0 };
|
|
657
|
+
}
|
|
658
|
+
const byteOffset = raw && typeof raw === "object"
|
|
659
|
+
? asNumber(raw.byteOffset)
|
|
660
|
+
: undefined;
|
|
661
|
+
if (byteOffset === undefined || byteOffset < 0 || !Number.isInteger(byteOffset)) {
|
|
662
|
+
return { byteOffset: 0 };
|
|
663
|
+
}
|
|
664
|
+
try {
|
|
665
|
+
if (statSync(join(bridgeDir, DELTAS_FILE)).size < byteOffset)
|
|
666
|
+
return { byteOffset: 0 };
|
|
667
|
+
}
|
|
668
|
+
catch {
|
|
669
|
+
if (byteOffset > 0)
|
|
670
|
+
return { byteOffset: 0 };
|
|
671
|
+
}
|
|
672
|
+
return { byteOffset };
|
|
673
|
+
}
|
|
674
|
+
export function writeDeltaForwardState(bridgeDir, state) {
|
|
675
|
+
mkdirSync(bridgeDir, { recursive: true, mode: 0o700 });
|
|
676
|
+
const tmp = join(bridgeDir, `${DELTA_FORWARDER_STATE_FILE}.tmp`);
|
|
677
|
+
writeFileSync(tmp, JSON.stringify({ byteOffset: state.byteOffset }));
|
|
678
|
+
renameSync(tmp, join(bridgeDir, DELTA_FORWARDER_STATE_FILE));
|
|
679
|
+
}
|
|
361
680
|
/** Tail `message_deltas.jsonl` from a byte offset into {@link MessageDelta}s. */
|
|
362
681
|
export function readMessageDeltasFrom(bridgeDir, byteOffset) {
|
|
363
682
|
const { records, nextOffset } = readJsonlFrom(join(bridgeDir, DELTAS_FILE), byteOffset);
|
|
@@ -451,7 +770,36 @@ export function readForwardState(bridgeDir) {
|
|
|
451
770
|
? s.seenSourceIds.filter((x) => typeof x === "string")
|
|
452
771
|
: [];
|
|
453
772
|
const cursorFingerprint = asString(s.cursorFingerprint);
|
|
454
|
-
|
|
773
|
+
const lineCursor = asNumber(s.lineCursor);
|
|
774
|
+
const currentTurnId = asString(s.currentTurnId);
|
|
775
|
+
const currentResponseId = asString(s.currentResponseId);
|
|
776
|
+
const activeTerminalCommandRaw = s.activeTerminalCommand;
|
|
777
|
+
const activeTerminalCommandRecord = activeTerminalCommandRaw &&
|
|
778
|
+
typeof activeTerminalCommandRaw === "object" &&
|
|
779
|
+
!Array.isArray(activeTerminalCommandRaw)
|
|
780
|
+
? activeTerminalCommandRaw
|
|
781
|
+
: undefined;
|
|
782
|
+
const activeTerminalCommandText = asString(activeTerminalCommandRecord?.command);
|
|
783
|
+
const activeTerminalCommandTurnId = asString(activeTerminalCommandRecord?.turnId);
|
|
784
|
+
const activeTerminalCommand = activeTerminalCommandText
|
|
785
|
+
? {
|
|
786
|
+
command: activeTerminalCommandText,
|
|
787
|
+
...(activeTerminalCommandTurnId ? { turnId: activeTerminalCommandTurnId } : {}),
|
|
788
|
+
}
|
|
789
|
+
: undefined;
|
|
790
|
+
return {
|
|
791
|
+
transcriptPath,
|
|
792
|
+
byteOffset,
|
|
793
|
+
seenSourceIds,
|
|
794
|
+
...(lineCursor !== undefined && Number.isInteger(lineCursor) && lineCursor >= 0
|
|
795
|
+
? { lineCursor }
|
|
796
|
+
: {}),
|
|
797
|
+
...(cursorFingerprint ? { cursorFingerprint } : {}),
|
|
798
|
+
...(currentTurnId ? { currentTurnId } : {}),
|
|
799
|
+
...(currentResponseId ? { currentResponseId } : {}),
|
|
800
|
+
...(s.turnOpen === true ? { turnOpen: true } : {}),
|
|
801
|
+
...(activeTerminalCommand ? { activeTerminalCommand } : {}),
|
|
802
|
+
};
|
|
455
803
|
}
|
|
456
804
|
/** Drop the forwarder cursor (a `/clear` starts fresh). */
|
|
457
805
|
export function resetForwardState(bridgeDir) {
|
|
@@ -462,6 +810,88 @@ export function resetForwardState(bridgeDir) {
|
|
|
462
810
|
// absent — fine
|
|
463
811
|
}
|
|
464
812
|
}
|
|
813
|
+
/** Parent Task/tool ids already consumed by the main transcript cursor. Kept
|
|
814
|
+
* independently of concrete child agents because Claude may create the child
|
|
815
|
+
* meta file only after the parent record was durably acknowledged. */
|
|
816
|
+
export function readSubagentParentResponses(bridgeDir, parentTranscriptPath) {
|
|
817
|
+
const raw = readJsonFile(join(bridgeDir, SUBAGENT_FORWARDER_STATE_FILE));
|
|
818
|
+
if (!raw || typeof raw !== "object")
|
|
819
|
+
return new Map();
|
|
820
|
+
const file = raw;
|
|
821
|
+
if (file.parentTranscriptPath !== parentTranscriptPath)
|
|
822
|
+
return new Map();
|
|
823
|
+
const responses = file.parentResponses;
|
|
824
|
+
if (!responses || typeof responses !== "object" || Array.isArray(responses))
|
|
825
|
+
return new Map();
|
|
826
|
+
const result = new Map();
|
|
827
|
+
for (const [parentToolCallId, responseId] of Object.entries(responses)) {
|
|
828
|
+
if (!parentToolCallId || typeof responseId !== "string" || !responseId)
|
|
829
|
+
continue;
|
|
830
|
+
result.set(parentToolCallId, responseId);
|
|
831
|
+
}
|
|
832
|
+
return result;
|
|
833
|
+
}
|
|
834
|
+
/** Read independent Claude native sub-agent cursors for one parent transcript. */
|
|
835
|
+
export function readSubagentForwardStates(bridgeDir, parentTranscriptPath) {
|
|
836
|
+
const raw = readJsonFile(join(bridgeDir, SUBAGENT_FORWARDER_STATE_FILE));
|
|
837
|
+
if (!raw || typeof raw !== "object")
|
|
838
|
+
return [];
|
|
839
|
+
const file = raw;
|
|
840
|
+
if (file.parentTranscriptPath !== parentTranscriptPath || !Array.isArray(file.agents)) {
|
|
841
|
+
return [];
|
|
842
|
+
}
|
|
843
|
+
const states = [];
|
|
844
|
+
for (const value of file.agents) {
|
|
845
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
846
|
+
continue;
|
|
847
|
+
const state = value;
|
|
848
|
+
const agentId = asString(state.agentId);
|
|
849
|
+
const parentToolCallId = asString(state.parentToolCallId);
|
|
850
|
+
const parentResponseId = asString(state.parentResponseId);
|
|
851
|
+
const transcriptPath = asString(state.transcriptPath);
|
|
852
|
+
const byteOffset = asNumber(state.byteOffset);
|
|
853
|
+
const lineCursor = asNumber(state.lineCursor);
|
|
854
|
+
const terminalCommand = asString(state.terminalCommand);
|
|
855
|
+
if (!agentId || !parentToolCallId || !transcriptPath || byteOffset === undefined)
|
|
856
|
+
continue;
|
|
857
|
+
states.push({
|
|
858
|
+
agentId,
|
|
859
|
+
parentToolCallId,
|
|
860
|
+
...(parentResponseId ? { parentResponseId } : {}),
|
|
861
|
+
transcriptPath,
|
|
862
|
+
byteOffset,
|
|
863
|
+
...(lineCursor !== undefined && Number.isInteger(lineCursor) && lineCursor >= 0
|
|
864
|
+
? { lineCursor }
|
|
865
|
+
: {}),
|
|
866
|
+
...(terminalCommand ? { terminalCommand } : {}),
|
|
867
|
+
seenSourceIds: Array.isArray(state.seenSourceIds)
|
|
868
|
+
? state.seenSourceIds
|
|
869
|
+
.filter((id) => typeof id === "string")
|
|
870
|
+
.slice(-MAX_SEEN_SOURCE_IDS)
|
|
871
|
+
: [],
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
return states;
|
|
875
|
+
}
|
|
876
|
+
/** Atomically persist every native sub-agent's independent delivery cursor. */
|
|
877
|
+
export function writeSubagentForwardStates(bridgeDir, parentTranscriptPath, states, parentResponses = new Map()) {
|
|
878
|
+
mkdirSync(bridgeDir, { recursive: true, mode: 0o700 });
|
|
879
|
+
const file = {
|
|
880
|
+
parentTranscriptPath,
|
|
881
|
+
parentResponses: Object.fromEntries([...parentResponses].slice(-MAX_SEEN_SOURCE_IDS)),
|
|
882
|
+
agents: states.map((state) => ({
|
|
883
|
+
...state,
|
|
884
|
+
seenSourceIds: state.seenSourceIds.slice(-MAX_SEEN_SOURCE_IDS),
|
|
885
|
+
})),
|
|
886
|
+
};
|
|
887
|
+
const target = join(bridgeDir, SUBAGENT_FORWARDER_STATE_FILE);
|
|
888
|
+
const tmp = `${target}.tmp`;
|
|
889
|
+
writeFileSync(tmp, JSON.stringify(file));
|
|
890
|
+
renameSync(tmp, target);
|
|
891
|
+
}
|
|
892
|
+
export function resetSubagentForwardStates(bridgeDir) {
|
|
893
|
+
rmSync(join(bridgeDir, SUBAGENT_FORWARDER_STATE_FILE), { force: true });
|
|
894
|
+
}
|
|
465
895
|
/** Read the latest `status.json` snapshot, or undefined if absent/malformed. */
|
|
466
896
|
export function readClaudeStatus(bridgeDir) {
|
|
467
897
|
const raw = readJsonFile(join(bridgeDir, STATUS_FILE));
|