@scotthuang/agent-knock-knock 0.12.12 → 0.12.13

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 (76) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +39 -26
  3. package/dist/src/approval-policy.d.ts +11 -2
  4. package/dist/src/approval-policy.js +33 -7
  5. package/dist/src/approval-policy.js.map +1 -1
  6. package/dist/src/claude-local-transcript-provider.d.ts +81 -0
  7. package/dist/src/claude-local-transcript-provider.js +854 -18
  8. package/dist/src/claude-local-transcript-provider.js.map +1 -1
  9. package/dist/src/cli-core.js +85 -0
  10. package/dist/src/cli-core.js.map +1 -1
  11. package/dist/src/deferred-foreground-authority-cli-adapter.js +90 -19
  12. package/dist/src/deferred-foreground-authority-cli-adapter.js.map +1 -1
  13. package/dist/src/deferred-foreground-transfer.js +3 -4
  14. package/dist/src/deferred-foreground-transfer.js.map +1 -1
  15. package/dist/src/openclaw-callback-transport.d.ts +5 -0
  16. package/dist/src/openclaw-callback-transport.js +1 -1
  17. package/dist/src/openclaw-callback-transport.js.map +1 -1
  18. package/dist/src/openclaw-plugin-callback-adapter.js +96 -14
  19. package/dist/src/openclaw-plugin-callback-adapter.js.map +1 -1
  20. package/dist/src/openclaw-plugin-command-adapter.js +548 -107
  21. package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
  22. package/dist/src/openclaw-plugin-helpers.d.ts +27 -4
  23. package/dist/src/openclaw-plugin-helpers.js +309 -44
  24. package/dist/src/openclaw-plugin-helpers.js.map +1 -1
  25. package/dist/src/openclaw-plugin-schemas.d.ts +44 -68
  26. package/dist/src/openclaw-plugin-schemas.js +63 -91
  27. package/dist/src/openclaw-plugin-schemas.js.map +1 -1
  28. package/dist/src/openclaw-plugin-supervisor.js +48 -10
  29. package/dist/src/openclaw-plugin-supervisor.js.map +1 -1
  30. package/dist/src/openclaw-private-authority-offers.d.ts +22 -0
  31. package/dist/src/openclaw-private-authority-offers.js +177 -0
  32. package/dist/src/openclaw-private-authority-offers.js.map +1 -0
  33. package/dist/src/store.d.ts +1 -0
  34. package/dist/src/store.js +3 -1
  35. package/dist/src/store.js.map +1 -1
  36. package/dist/src/terminal-action-projection.d.ts +19 -1
  37. package/dist/src/terminal-action-projection.js +23 -0
  38. package/dist/src/terminal-action-projection.js.map +1 -1
  39. package/dist/src/terminal-command-cli-adapter.d.ts +5 -0
  40. package/dist/src/terminal-command-cli-adapter.js +249 -89
  41. package/dist/src/terminal-command-cli-adapter.js.map +1 -1
  42. package/dist/src/terminal-dispatch-repository-cli-adapter.js +305 -79
  43. package/dist/src/terminal-dispatch-repository-cli-adapter.js.map +1 -1
  44. package/dist/src/terminal-list-cli-adapter.d.ts +31 -7
  45. package/dist/src/terminal-list-cli-adapter.js +285 -67
  46. package/dist/src/terminal-list-cli-adapter.js.map +1 -1
  47. package/dist/src/terminal-list-renderer.d.ts +2 -0
  48. package/dist/src/terminal-list-renderer.js +116 -41
  49. package/dist/src/terminal-list-renderer.js.map +1 -1
  50. package/dist/src/terminal-monitor-cli-adapter.js +1 -1
  51. package/dist/src/terminal-monitor-cli-adapter.js.map +1 -1
  52. package/dist/src/terminal-monitor-state-cli-adapter.js +6 -15
  53. package/dist/src/terminal-monitor-state-cli-adapter.js.map +1 -1
  54. package/dist/src/terminal-status-cli-adapter.d.ts +10 -0
  55. package/dist/src/terminal-status-cli-adapter.js +89 -7
  56. package/dist/src/terminal-status-cli-adapter.js.map +1 -1
  57. package/dist/src/terminal-submission-acceptance.d.ts +60 -1
  58. package/dist/src/terminal-submission-acceptance.js +560 -0
  59. package/dist/src/terminal-submission-acceptance.js.map +1 -1
  60. package/dist/src/terminal-watch-callback-cli-adapter.d.ts +20 -0
  61. package/dist/src/terminal-watch-callback-cli-adapter.js +55 -0
  62. package/dist/src/terminal-watch-callback-cli-adapter.js.map +1 -0
  63. package/dist/src/terminal-watch-cli-adapter.d.ts +64 -0
  64. package/dist/src/terminal-watch-cli-adapter.js +508 -0
  65. package/dist/src/terminal-watch-cli-adapter.js.map +1 -0
  66. package/dist/src/terminal-watch-service.d.ts +97 -0
  67. package/dist/src/terminal-watch-service.js +714 -0
  68. package/dist/src/terminal-watch-service.js.map +1 -0
  69. package/dist/src/terminal-watch-store.d.ts +149 -0
  70. package/dist/src/terminal-watch-store.js +861 -0
  71. package/dist/src/terminal-watch-store.js.map +1 -0
  72. package/docs/quickstart-herdr.md +8 -0
  73. package/docs/quickstart-tmux.md +33 -6
  74. package/openclaw.plugin.json +11 -3
  75. package/package.json +1 -1
  76. package/templates/openclaw-skills/agent-knock-knock/SKILL.md +57 -39
@@ -430,6 +430,770 @@ export function captureClaudeTranscriptAnchor(options) {
430
430
  fs.closeSync(located.fd);
431
431
  }
432
432
  }
433
+ /** Capture the latest exact, unfinished root prompt in a live Claude process. */
434
+ export function captureClaudeHumanStartedActiveTaskAnchor(options) {
435
+ const sessionId = nonEmptyString(options.sessionId);
436
+ const cwd = nonEmptyString(options.cwd);
437
+ const pid = positiveInteger(options.pid);
438
+ if (!sessionId || !cwd || pid === undefined || !CLAUDE_SESSION_ID_PATTERN.test(sessionId)) {
439
+ return undefined;
440
+ }
441
+ const agent = exactInteractiveAgent(options.agentRows, pid);
442
+ const agentStartedAtMs = positiveInteger(agent?.startedAt);
443
+ if (!agent ||
444
+ agentStartedAtMs === undefined ||
445
+ agent.sessionId !== sessionId ||
446
+ normalizePath(agent.cwd) !== normalizePath(cwd) ||
447
+ !isActiveClaudeAgentState(agent)) {
448
+ return undefined;
449
+ }
450
+ const projectsRoot = projectsRootPath(path.resolve(options.claudeHome ?? defaultClaudeHome()));
451
+ if (!isRealDirectory(projectsRoot)) {
452
+ return undefined;
453
+ }
454
+ const opened = locateTranscript(projectsRoot, sessionId);
455
+ if (!opened) {
456
+ return undefined;
457
+ }
458
+ try {
459
+ const before = opened.stat;
460
+ const maxBytes = positiveClaudeByteLimit(options.maxTurnBytes);
461
+ if (before.size === 0) {
462
+ return undefined;
463
+ }
464
+ if (!fileEndsWithNewline(opened.fd, before.size)) {
465
+ throw new Error("Claude active-task transcript has an incomplete JSONL tail; retry");
466
+ }
467
+ const recordEntries = readBoundedCompleteJsonlTail(opened.fd, before.size, maxBytes);
468
+ const records = recordEntries.map((entry) => entry.record);
469
+ const after = fs.fstatSync(opened.fd);
470
+ if (!sameStableTranscriptFile(before, after)) {
471
+ throw new Error("Claude active-task transcript changed while it was captured; retry");
472
+ }
473
+ const promptEntry = latestClaudeRootHumanPromptEntry(recordEntries);
474
+ if (!promptEntry) {
475
+ if (before.size > maxBytes) {
476
+ throw new Error("Claude active task exceeded the bounded local read limit");
477
+ }
478
+ return undefined;
479
+ }
480
+ const prompt = promptEntry.record;
481
+ assertSupportedRecord(prompt, sessionId, cwd);
482
+ const promptUuid = uuidValue(prompt.uuid);
483
+ const promptText = userPromptText(prompt);
484
+ const requestHash = exactRequestFingerprint(promptText);
485
+ const claudeVersion = nonEmptyString(prompt.version);
486
+ if (!promptUuid || !promptText || !requestHash || !claudeVersion) {
487
+ throw new Error("Claude active root prompt has no exact supported identity");
488
+ }
489
+ const device = String(before.dev);
490
+ const inode = String(before.ino);
491
+ const fileId = transcriptFileId(sessionId, `${device}:${inode}`);
492
+ const transcriptAnchor = {
493
+ schema_version: CLAUDE_TRANSCRIPT_ANCHOR_VERSION,
494
+ session_id: sessionId,
495
+ cwd: path.resolve(cwd),
496
+ pid,
497
+ agent_started_at_ms: agentStartedAtMs,
498
+ captured_at: (options.now ?? new Date()).toISOString(),
499
+ relative_path: opened.relativePath,
500
+ offset_bytes: 0,
501
+ file_existed: true,
502
+ device,
503
+ inode
504
+ };
505
+ const snapshot = {
506
+ anchor: transcriptAnchor,
507
+ sessionId,
508
+ cwd,
509
+ expectedRequestHash: requestHash,
510
+ expectedPromptUuid: promptUuid,
511
+ records,
512
+ transcriptFileId: fileId,
513
+ observedEndOffsetBytes: before.size
514
+ };
515
+ if (completionFromRecords(snapshot, {
516
+ requireVerifiableCompletionSignal: false
517
+ })) {
518
+ return undefined;
519
+ }
520
+ if (!pendingApprovalTurn(snapshot)) {
521
+ throw new Error("Claude active root prompt has no unique unfinished foreground chain");
522
+ }
523
+ const base = {
524
+ schema: "agent-knock-knock/claude-human-started-active-task-anchor",
525
+ version: 1,
526
+ session_id: sessionId,
527
+ cwd: path.resolve(cwd),
528
+ pid,
529
+ agent_started_at_ms: agentStartedAtMs,
530
+ captured_at: transcriptAnchor.captured_at,
531
+ relative_path: opened.relativePath,
532
+ device,
533
+ inode,
534
+ prompt_uuid: promptUuid,
535
+ request_hash: requestHash,
536
+ claude_version: claudeVersion,
537
+ transcript_file_id: fileId,
538
+ turn_start_offset_bytes: promptEntry.offsetBytes,
539
+ observed_end_offset_bytes: before.size
540
+ };
541
+ return {
542
+ ...base,
543
+ anchor_fingerprint: sha256Hex(JSON.stringify(base))
544
+ };
545
+ }
546
+ finally {
547
+ fs.closeSync(opened.fd);
548
+ }
549
+ }
550
+ /** Observe only the exact root prompt named by a human-started task anchor. */
551
+ export function observeClaudeHumanStartedActiveTask(options) {
552
+ try {
553
+ const anchor = validateClaudeHumanStartedActiveTaskAnchor(options.anchor);
554
+ const checkpoint = claudeActiveTaskObservationCheckpoint(anchor, options);
555
+ const resumeOffsetBytes = checkpoint.safe_resume_offset_bytes;
556
+ const projectsRoot = projectsRootPath(path.resolve(options.claudeHome ?? defaultClaudeHome()));
557
+ if (!isRealDirectory(projectsRoot)) {
558
+ return claudeActiveTaskUnavailable("Claude transcript projects directory is unavailable");
559
+ }
560
+ let opened;
561
+ try {
562
+ opened = openAnchoredTranscript(projectsRoot, claudeTranscriptAnchorForHumanStartedTask(anchor));
563
+ }
564
+ catch (error) {
565
+ if (isRetryableClaudeProviderIoError(error) ||
566
+ (error instanceof Error &&
567
+ error.message === "Claude transcript disappeared after the managed send")) {
568
+ return claudeActiveTaskUnavailable(error instanceof Error ? error.message : String(error));
569
+ }
570
+ throw error;
571
+ }
572
+ if (!opened) {
573
+ return claudeActiveTaskUnavailable("the anchored Claude transcript is unavailable");
574
+ }
575
+ try {
576
+ const before = opened.stat;
577
+ if (String(before.dev) !== anchor.device ||
578
+ String(before.ino) !== anchor.inode) {
579
+ throw new Error("Claude active-task transcript identity changed after capture");
580
+ }
581
+ if (before.size < anchor.observed_end_offset_bytes ||
582
+ before.size <= anchor.turn_start_offset_bytes) {
583
+ throw new Error("Claude active-task transcript was truncated after capture");
584
+ }
585
+ if (before.size < resumeOffsetBytes) {
586
+ throw new Error("Claude active-task transcript was truncated after observation");
587
+ }
588
+ assertClaudeActiveTaskResumeBoundary(opened.fd, resumeOffsetBytes);
589
+ const maxTurnBytes = positiveClaudeByteLimit(options.maxTurnBytes);
590
+ const availableBytes = before.size - resumeOffsetBytes;
591
+ const bytesToRead = Math.min(availableBytes, maxTurnBytes);
592
+ if (bytesToRead === 0) {
593
+ assertObservableClaudeActiveTaskAgent(options.agentRows, anchor);
594
+ return {
595
+ status: "pending",
596
+ observedEndOffsetBytes: before.size,
597
+ safeResumeOffsetBytes: resumeOffsetBytes,
598
+ checkpoint
599
+ };
600
+ }
601
+ const suffixBuffer = readExactTranscriptBytes(opened.fd, resumeOffsetBytes, bytesToRead);
602
+ const completeLength = completeTranscriptJsonlPrefixLength(suffixBuffer);
603
+ if (completeLength === 0 && availableBytes > maxTurnBytes) {
604
+ throw new Error("Claude active-task JSONL record exceeded the bounded local read limit");
605
+ }
606
+ const safeResumeOffsetBytes = resumeOffsetBytes + completeLength;
607
+ const suffixRecords = readCompleteJsonlRecordEntries(opened.fd, resumeOffsetBytes, completeLength, { preReadBuffer: suffixBuffer.subarray(0, completeLength) }).map((entry) => entry.record);
608
+ const after = fs.fstatSync(opened.fd);
609
+ if (!sameStableTranscriptFile(before, after)) {
610
+ assertObservableClaudeActiveTaskAgent(options.agentRows, anchor);
611
+ return {
612
+ status: "pending",
613
+ observedEndOffsetBytes: after.size,
614
+ safeResumeOffsetBytes: resumeOffsetBytes,
615
+ checkpoint
616
+ };
617
+ }
618
+ const advanced = advanceClaudeHumanStartedActiveTaskCheckpoint(anchor, checkpoint, suffixRecords, safeResumeOffsetBytes);
619
+ if (advanced.completion) {
620
+ return { status: "completed", completion: advanced.completion };
621
+ }
622
+ assertObservableClaudeActiveTaskAgent(options.agentRows, anchor);
623
+ return {
624
+ status: "pending",
625
+ observedEndOffsetBytes: before.size,
626
+ safeResumeOffsetBytes,
627
+ checkpoint: advanced.checkpoint
628
+ };
629
+ }
630
+ finally {
631
+ fs.closeSync(opened.fd);
632
+ }
633
+ }
634
+ catch (error) {
635
+ if (isRetryableClaudeProviderIoError(error)) {
636
+ return claudeActiveTaskUnavailable(error instanceof Error ? error.message : String(error));
637
+ }
638
+ return {
639
+ status: "invalidated",
640
+ reason: error instanceof Error ? error.message : String(error)
641
+ };
642
+ }
643
+ }
644
+ export function validateClaudeHumanStartedActiveTaskAnchor(value) {
645
+ if (!isRecord(value) ||
646
+ value.schema !==
647
+ "agent-knock-knock/claude-human-started-active-task-anchor" ||
648
+ value.version !== 1) {
649
+ throw new Error("Claude human-started active-task anchor is invalid");
650
+ }
651
+ const allowedKeys = new Set([
652
+ "schema", "version", "session_id", "cwd", "pid", "agent_started_at_ms",
653
+ "captured_at", "relative_path", "device", "inode", "prompt_uuid",
654
+ "request_hash", "claude_version", "transcript_file_id",
655
+ "turn_start_offset_bytes", "observed_end_offset_bytes", "anchor_fingerprint"
656
+ ]);
657
+ if (Object.keys(value).some((key) => !allowedKeys.has(key))) {
658
+ throw new Error("Claude human-started active-task anchor has unsupported fields");
659
+ }
660
+ const sessionId = uuidValue(value.session_id);
661
+ const promptUuid = uuidValue(value.prompt_uuid);
662
+ const cwd = nonEmptyString(value.cwd);
663
+ const pid = positiveInteger(value.pid);
664
+ const startedAt = positiveInteger(value.agent_started_at_ms);
665
+ const relativePath = nonEmptyString(value.relative_path);
666
+ const device = nonEmptyString(value.device);
667
+ const inode = nonEmptyString(value.inode);
668
+ const turnStartOffset = nonNegativeInteger(value.turn_start_offset_bytes);
669
+ const observedEndOffset = positiveInteger(value.observed_end_offset_bytes);
670
+ const capturedAt = typeof value.captured_at === "string"
671
+ ? value.captured_at
672
+ : undefined;
673
+ if (!sessionId ||
674
+ sessionId !== value.session_id ||
675
+ !promptUuid ||
676
+ promptUuid !== value.prompt_uuid ||
677
+ !cwd ||
678
+ !path.isAbsolute(cwd) ||
679
+ path.resolve(cwd) !== cwd ||
680
+ typeof value.pid !== "number" ||
681
+ pid === undefined ||
682
+ typeof value.agent_started_at_ms !== "number" ||
683
+ startedAt === undefined ||
684
+ !capturedAt ||
685
+ validTimestampMs(capturedAt) === undefined ||
686
+ new Date(capturedAt).toISOString() !== capturedAt ||
687
+ !relativePath ||
688
+ relativePath.includes("\0") ||
689
+ path.posix.isAbsolute(relativePath) ||
690
+ path.win32.isAbsolute(relativePath) ||
691
+ relativePath.split(/[\\/]/u).length !== 2 ||
692
+ relativePath.split(/[\\/]/u).some((segment) => !segment || segment === "." || segment === "..") ||
693
+ path.basename(relativePath) !== `${sessionId}.jsonl` ||
694
+ typeof value.turn_start_offset_bytes !== "number" ||
695
+ turnStartOffset === undefined ||
696
+ typeof value.observed_end_offset_bytes !== "number" ||
697
+ observedEndOffset === undefined ||
698
+ turnStartOffset >= observedEndOffset ||
699
+ !decimalFileIdentity(device) ||
700
+ !decimalFileIdentity(inode) ||
701
+ !/^[0-9a-f]{64}$/u.test(String(value.request_hash)) ||
702
+ !isCompatibleClaudeVersion(value.claude_version) ||
703
+ !/^[0-9a-f]{24}$/u.test(String(value.transcript_file_id)) ||
704
+ !/^[0-9a-f]{64}$/u.test(String(value.anchor_fingerprint))) {
705
+ throw new Error("Claude human-started active-task anchor identity is invalid");
706
+ }
707
+ const { anchor_fingerprint: _anchorFingerprint, ...base } = value;
708
+ if (sha256Hex(JSON.stringify(base)) !== value.anchor_fingerprint) {
709
+ throw new Error("Claude human-started active-task anchor fingerprint does not match");
710
+ }
711
+ const expectedFileId = transcriptFileId(sessionId, `${device}:${inode}`);
712
+ if (expectedFileId !== value.transcript_file_id) {
713
+ throw new Error("Claude human-started active-task transcript identity is invalid");
714
+ }
715
+ return value;
716
+ }
717
+ function claudeTranscriptAnchorForHumanStartedTask(anchor) {
718
+ return {
719
+ schema_version: CLAUDE_TRANSCRIPT_ANCHOR_VERSION,
720
+ session_id: anchor.session_id,
721
+ cwd: anchor.cwd,
722
+ pid: anchor.pid,
723
+ agent_started_at_ms: anchor.agent_started_at_ms,
724
+ captured_at: anchor.captured_at,
725
+ relative_path: anchor.relative_path,
726
+ offset_bytes: 0,
727
+ file_existed: true,
728
+ device: anchor.device,
729
+ inode: anchor.inode
730
+ };
731
+ }
732
+ function latestClaudeRootHumanPromptEntry(entries) {
733
+ return [...entries].reverse().find((entry) => isClaudeRootHumanPrompt(entry.record));
734
+ }
735
+ function isClaudeRootHumanPrompt(record) {
736
+ return record.type === "user" &&
737
+ isRecord(record.message) &&
738
+ record.message.role === "user" &&
739
+ record.isSidechain !== true &&
740
+ nonEmptyString(record.agentId) === undefined &&
741
+ userPromptText(record) !== undefined;
742
+ }
743
+ function isActiveClaudeAgentState(agent) {
744
+ return agent.status === "working" ||
745
+ agent.status === "busy" ||
746
+ (agent.status === "waiting" && agent.waitingFor === "dialog open");
747
+ }
748
+ function isObservableClaudeAgentState(agent) {
749
+ return agent.status === "idle" || isActiveClaudeAgentState(agent);
750
+ }
751
+ function assertObservableClaudeActiveTaskAgent(rows, anchor) {
752
+ const agent = exactInteractiveAgent(rows, anchor.pid);
753
+ if (!agent ||
754
+ agent.startedAt !== anchor.agent_started_at_ms ||
755
+ agent.sessionId !== anchor.session_id ||
756
+ normalizePath(agent.cwd) !== normalizePath(anchor.cwd) ||
757
+ !isObservableClaudeAgentState(agent)) {
758
+ throw new Error("Claude process identity changed after active-task capture");
759
+ }
760
+ }
761
+ function positiveClaudeByteLimit(value) {
762
+ const result = value ?? CLAUDE_TRANSCRIPT_MAX_TURN_BYTES;
763
+ if (!Number.isSafeInteger(result) || result <= 0) {
764
+ throw new Error("Claude active-task byte limit is invalid");
765
+ }
766
+ return result;
767
+ }
768
+ const CLAUDE_ACTIVE_TASK_CHECKPOINT_SCHEMA = "agent-knock-knock/claude-human-started-active-task-checkpoint";
769
+ const CLAUDE_ACTIVE_TASK_MAX_UNRESOLVED_TOOLS = 64;
770
+ const CLAUDE_ACTIVE_TASK_MAX_OPAQUE_ID_LENGTH = 256;
771
+ export function initialClaudeHumanStartedActiveTaskCheckpoint(value) {
772
+ const anchor = validateClaudeHumanStartedActiveTaskAnchor(value);
773
+ return signedClaudeActiveTaskCheckpoint({
774
+ schema: CLAUDE_ACTIVE_TASK_CHECKPOINT_SCHEMA,
775
+ version: 1,
776
+ anchor_fingerprint: anchor.anchor_fingerprint,
777
+ safe_resume_offset_bytes: anchor.turn_start_offset_bytes,
778
+ last_record_uuid: anchor.prompt_uuid,
779
+ record_count: 0,
780
+ chain_fingerprint: sha256Hex(`claude-active-task-checkpoint\0${anchor.anchor_fingerprint}`),
781
+ unresolved_tool_uses: []
782
+ });
783
+ }
784
+ export function validateClaudeHumanStartedActiveTaskCheckpoint(value, anchorValue) {
785
+ const anchor = validateClaudeHumanStartedActiveTaskAnchor(anchorValue);
786
+ if (!isRecord(value)) {
787
+ throw new Error("Claude active-task checkpoint is invalid");
788
+ }
789
+ const allowedKeys = new Set([
790
+ "schema", "version", "anchor_fingerprint", "safe_resume_offset_bytes",
791
+ "last_record_uuid", "record_count", "chain_fingerprint",
792
+ "unresolved_tool_uses", "assistant_message_id", "assistant_text",
793
+ "end_turn_message_id", "end_turn_assistant_uuid",
794
+ "checkpoint_fingerprint"
795
+ ]);
796
+ if (Object.keys(value).some((key) => !allowedKeys.has(key))) {
797
+ throw new Error("Claude active-task checkpoint has unsupported fields");
798
+ }
799
+ const safeOffset = nonNegativeInteger(value.safe_resume_offset_bytes);
800
+ const recordCount = nonNegativeInteger(value.record_count);
801
+ const lastRecordUuid = uuidValue(value.last_record_uuid);
802
+ const assistantMessageId = value.assistant_message_id === undefined
803
+ ? undefined
804
+ : uuidValue(value.assistant_message_id);
805
+ const endTurnMessageId = value.end_turn_message_id === undefined
806
+ ? undefined
807
+ : uuidValue(value.end_turn_message_id);
808
+ const endTurnAssistantUuid = value.end_turn_assistant_uuid === undefined
809
+ ? undefined
810
+ : uuidValue(value.end_turn_assistant_uuid);
811
+ const assistantText = value.assistant_text;
812
+ if (value.schema !== CLAUDE_ACTIVE_TASK_CHECKPOINT_SCHEMA ||
813
+ value.version !== 1 ||
814
+ value.anchor_fingerprint !== anchor.anchor_fingerprint ||
815
+ typeof value.safe_resume_offset_bytes !== "number" ||
816
+ safeOffset === undefined ||
817
+ safeOffset < anchor.turn_start_offset_bytes ||
818
+ typeof value.record_count !== "number" ||
819
+ recordCount === undefined ||
820
+ !lastRecordUuid ||
821
+ lastRecordUuid !== value.last_record_uuid ||
822
+ (recordCount === 0 && lastRecordUuid !== anchor.prompt_uuid) ||
823
+ !sha256String(value.chain_fingerprint) ||
824
+ !sha256String(value.checkpoint_fingerprint) ||
825
+ (value.assistant_message_id !== undefined && !assistantMessageId) ||
826
+ (value.end_turn_message_id !== undefined && !endTurnMessageId) ||
827
+ (value.end_turn_assistant_uuid !== undefined && !endTurnAssistantUuid) ||
828
+ (endTurnMessageId === undefined) !== (endTurnAssistantUuid === undefined) ||
829
+ (endTurnMessageId !== undefined &&
830
+ assistantMessageId !== endTurnMessageId) ||
831
+ (assistantText !== undefined &&
832
+ (typeof assistantText !== "string" ||
833
+ !assistantText ||
834
+ assistantText.length > 4000 ||
835
+ boundedRedactedText(assistantText) !== assistantText ||
836
+ assistantMessageId === undefined))) {
837
+ throw new Error("Claude active-task checkpoint identity is invalid");
838
+ }
839
+ if (!Array.isArray(value.unresolved_tool_uses) ||
840
+ value.unresolved_tool_uses.length >
841
+ CLAUDE_ACTIVE_TASK_MAX_UNRESOLVED_TOOLS) {
842
+ throw new Error("Claude active-task checkpoint tool state is invalid");
843
+ }
844
+ const seenToolIds = new Set();
845
+ for (const entry of value.unresolved_tool_uses) {
846
+ if (!isRecord(entry) ||
847
+ Object.keys(entry).some((key) => key !== "tool_use_id" && key !== "owner_uuid") ||
848
+ !checkpointOpaqueId(entry.tool_use_id) ||
849
+ !uuidValue(entry.owner_uuid) ||
850
+ uuidValue(entry.owner_uuid) !== entry.owner_uuid ||
851
+ seenToolIds.has(String(entry.tool_use_id))) {
852
+ throw new Error("Claude active-task checkpoint tool state is invalid");
853
+ }
854
+ seenToolIds.add(String(entry.tool_use_id));
855
+ }
856
+ const candidate = value;
857
+ if (claudeActiveTaskCheckpointFingerprint(candidate) !==
858
+ candidate.checkpoint_fingerprint) {
859
+ throw new Error("Claude active-task checkpoint fingerprint does not match");
860
+ }
861
+ return candidate;
862
+ }
863
+ function claudeActiveTaskObservationCheckpoint(anchor, options) {
864
+ const checkpoint = options.checkpoint === undefined
865
+ ? initialClaudeHumanStartedActiveTaskCheckpoint(anchor)
866
+ : validateClaudeHumanStartedActiveTaskCheckpoint(options.checkpoint, anchor);
867
+ if (options.resumeOffsetBytes !== undefined &&
868
+ (!Number.isSafeInteger(options.resumeOffsetBytes) ||
869
+ options.resumeOffsetBytes !== checkpoint.safe_resume_offset_bytes)) {
870
+ throw new Error("Claude active-task resume offset does not match its continuation checkpoint");
871
+ }
872
+ return checkpoint;
873
+ }
874
+ function signedClaudeActiveTaskCheckpoint(base) {
875
+ const checkpoint = {
876
+ ...base,
877
+ checkpoint_fingerprint: ""
878
+ };
879
+ return {
880
+ ...checkpoint,
881
+ checkpoint_fingerprint: claudeActiveTaskCheckpointFingerprint(checkpoint)
882
+ };
883
+ }
884
+ function claudeActiveTaskCheckpointFingerprint(value) {
885
+ return sha256Hex(JSON.stringify({
886
+ schema: value.schema,
887
+ version: value.version,
888
+ anchor_fingerprint: value.anchor_fingerprint,
889
+ safe_resume_offset_bytes: value.safe_resume_offset_bytes,
890
+ last_record_uuid: value.last_record_uuid,
891
+ record_count: value.record_count,
892
+ chain_fingerprint: value.chain_fingerprint,
893
+ unresolved_tool_uses: value.unresolved_tool_uses,
894
+ ...(value.assistant_message_id === undefined
895
+ ? {}
896
+ : { assistant_message_id: value.assistant_message_id }),
897
+ ...(value.assistant_text === undefined
898
+ ? {}
899
+ : { assistant_text: value.assistant_text }),
900
+ ...(value.end_turn_message_id === undefined
901
+ ? {}
902
+ : { end_turn_message_id: value.end_turn_message_id }),
903
+ ...(value.end_turn_assistant_uuid === undefined
904
+ ? {}
905
+ : { end_turn_assistant_uuid: value.end_turn_assistant_uuid })
906
+ }));
907
+ }
908
+ function sha256String(value) {
909
+ return typeof value === "string" && /^[0-9a-f]{64}$/u.test(value);
910
+ }
911
+ function checkpointOpaqueId(value) {
912
+ return typeof value === "string" &&
913
+ value.length > 0 &&
914
+ value.length <= CLAUDE_ACTIVE_TASK_MAX_OPAQUE_ID_LENGTH &&
915
+ /^[A-Za-z0-9_.:-]+$/u.test(value);
916
+ }
917
+ function advanceClaudeHumanStartedActiveTaskCheckpoint(anchor, checkpoint, records, safeResumeOffsetBytes) {
918
+ let lastRecordUuid = checkpoint.last_record_uuid;
919
+ let recordCount = checkpoint.record_count;
920
+ let chainFingerprint = checkpoint.chain_fingerprint;
921
+ const unresolvedToolUses = new Map(checkpoint.unresolved_tool_uses.map((entry) => [entry.tool_use_id, {
922
+ tool_use_id: entry.tool_use_id,
923
+ owner_uuid: entry.owner_uuid
924
+ }]));
925
+ let assistantMessageId = checkpoint.assistant_message_id;
926
+ let assistantText = checkpoint.assistant_text;
927
+ let endTurnMessageId = checkpoint.end_turn_message_id;
928
+ let endTurnAssistantUuid = checkpoint.end_turn_assistant_uuid;
929
+ let completion;
930
+ let durationSeen = false;
931
+ for (const record of records) {
932
+ if (recordCount === 0) {
933
+ assertClaudeActiveTaskCheckpointPrompt(record, anchor);
934
+ lastRecordUuid = anchor.prompt_uuid;
935
+ ({ recordCount, chainFingerprint } = advanceClaudeCheckpointDigest(recordCount, chainFingerprint, record));
936
+ continue;
937
+ }
938
+ if (isClaudeRootHumanPrompt(record)) {
939
+ if (completion) {
940
+ break;
941
+ }
942
+ throw new Error("a later Claude human prompt appeared before the anchored task completed");
943
+ }
944
+ const recordUuid = claudeCheckpointRecordUuid(record, anchor, lastRecordUuid);
945
+ if (recordUuid) {
946
+ lastRecordUuid = recordUuid;
947
+ }
948
+ if (hasUnresolvedBackgroundWork(record) ||
949
+ record.subtype === "stop_hook_summary") {
950
+ throw new Error("Claude anchored task acquired unsupported background or stop-hook work");
951
+ }
952
+ advanceClaudeCheckpointToolState(record, recordUuid, unresolvedToolUses);
953
+ if (record.type === "assistant") {
954
+ const message = isRecord(record.message) ? record.message : undefined;
955
+ if (message?.role !== "assistant") {
956
+ throw new Error("Claude assistant record has an unsupported message role");
957
+ }
958
+ const rawMessageId = nonEmptyString(message.id);
959
+ const messageId = rawMessageId === undefined
960
+ ? undefined
961
+ : uuidValue(rawMessageId);
962
+ if (rawMessageId !== undefined && !messageId) {
963
+ throw new Error("Claude assistant message has no stable UUID");
964
+ }
965
+ if (messageId && messageId !== assistantMessageId) {
966
+ if (endTurnMessageId !== undefined) {
967
+ throw new Error("Claude assistant message changed after an end-turn signal");
968
+ }
969
+ assistantMessageId = messageId;
970
+ assistantText = undefined;
971
+ }
972
+ const recordText = textFromAssistantRecord(record);
973
+ if (recordText) {
974
+ if (!messageId || messageId !== assistantMessageId) {
975
+ throw new Error("Claude assistant text has no stable message identity");
976
+ }
977
+ assistantText = appendBoundedRedactedClaudeText(assistantText, recordText);
978
+ }
979
+ if (record.isApiErrorMessage === true) {
980
+ if (!recordUuid || validTimestampMs(record.timestamp) === undefined) {
981
+ throw new Error("Claude API-error completion identity is invalid");
982
+ }
983
+ if (unresolvedToolUses.size > 0) {
984
+ throw new Error("Claude API error left unresolved tool use state");
985
+ }
986
+ const error = safeErrorCode(record.error);
987
+ completion = {
988
+ source: "durable",
989
+ outcome: "failure",
990
+ text: assistantText || `Claude Code stopped with ${error}.`,
991
+ timestamp: nonEmptyString(record.timestamp),
992
+ id: recordUuid,
993
+ confidence: "high",
994
+ metadata: {
995
+ match: "claude_transcript_api_error",
996
+ session_id: anchor.session_id,
997
+ prompt_uuid: anchor.prompt_uuid,
998
+ error,
999
+ transcript_schema: "claude_code_jsonl_v2",
1000
+ transcript_file_id: anchor.transcript_file_id
1001
+ }
1002
+ };
1003
+ }
1004
+ else {
1005
+ const stopReason = nonEmptyString(message.stop_reason);
1006
+ if (stopReason === "end_turn") {
1007
+ if (!recordUuid || !messageId) {
1008
+ throw new Error("Claude end-turn signal has no stable identity");
1009
+ }
1010
+ if (endTurnMessageId !== undefined &&
1011
+ endTurnMessageId !== messageId) {
1012
+ throw new Error("Claude active task contains multiple end-turn message identities");
1013
+ }
1014
+ endTurnMessageId = messageId;
1015
+ endTurnAssistantUuid = recordUuid;
1016
+ }
1017
+ else if (stopReason !== undefined && stopReason !== "tool_use") {
1018
+ throw new Error("Claude active task contains an unsupported terminal stop reason");
1019
+ }
1020
+ }
1021
+ }
1022
+ if (record.type === "system" && record.subtype === "turn_duration") {
1023
+ if (durationSeen) {
1024
+ throw new Error("Claude transcript turn contains multiple turn_duration records");
1025
+ }
1026
+ durationSeen = true;
1027
+ if (!recordUuid ||
1028
+ validTimestampMs(record.timestamp) === undefined ||
1029
+ !endTurnMessageId ||
1030
+ !endTurnAssistantUuid ||
1031
+ assistantMessageId !== endTurnMessageId ||
1032
+ !assistantText ||
1033
+ unresolvedToolUses.size > 0) {
1034
+ throw new Error("Claude turn_duration has no complete incremental completion chain");
1035
+ }
1036
+ completion = {
1037
+ source: "durable",
1038
+ outcome: "success",
1039
+ text: assistantText,
1040
+ timestamp: nonEmptyString(record.timestamp),
1041
+ id: recordUuid,
1042
+ confidence: "high",
1043
+ metadata: {
1044
+ match: "claude_transcript_turn_duration",
1045
+ session_id: anchor.session_id,
1046
+ prompt_uuid: anchor.prompt_uuid,
1047
+ assistant_message_id: endTurnMessageId,
1048
+ claude_version: anchor.claude_version,
1049
+ transcript_schema: "claude_code_jsonl_v2",
1050
+ transcript_file_id: anchor.transcript_file_id
1051
+ }
1052
+ };
1053
+ }
1054
+ ({ recordCount, chainFingerprint } = advanceClaudeCheckpointDigest(recordCount, chainFingerprint, record));
1055
+ }
1056
+ const nextCheckpoint = signedClaudeActiveTaskCheckpoint({
1057
+ schema: CLAUDE_ACTIVE_TASK_CHECKPOINT_SCHEMA,
1058
+ version: 1,
1059
+ anchor_fingerprint: anchor.anchor_fingerprint,
1060
+ safe_resume_offset_bytes: safeResumeOffsetBytes,
1061
+ last_record_uuid: lastRecordUuid,
1062
+ record_count: recordCount,
1063
+ chain_fingerprint: chainFingerprint,
1064
+ unresolved_tool_uses: [...unresolvedToolUses.values()],
1065
+ ...(assistantMessageId === undefined
1066
+ ? {}
1067
+ : { assistant_message_id: assistantMessageId }),
1068
+ ...(assistantText === undefined ? {} : { assistant_text: assistantText }),
1069
+ ...(endTurnMessageId === undefined
1070
+ ? {}
1071
+ : { end_turn_message_id: endTurnMessageId }),
1072
+ ...(endTurnAssistantUuid === undefined
1073
+ ? {}
1074
+ : { end_turn_assistant_uuid: endTurnAssistantUuid })
1075
+ });
1076
+ return { checkpoint: nextCheckpoint, ...(completion ? { completion } : {}) };
1077
+ }
1078
+ function assertClaudeActiveTaskCheckpointPrompt(record, anchor) {
1079
+ assertSupportedRecord(record, anchor.session_id, anchor.cwd);
1080
+ const promptText = userPromptText(record);
1081
+ if (!isClaudeRootHumanPrompt(record) ||
1082
+ uuidValue(record.uuid) !== anchor.prompt_uuid ||
1083
+ exactRequestFingerprint(promptText) !== anchor.request_hash ||
1084
+ nonEmptyString(record.version) !== anchor.claude_version) {
1085
+ throw new Error("Claude anchored root prompt identity changed");
1086
+ }
1087
+ }
1088
+ function claudeCheckpointRecordUuid(record, anchor, expectedParentUuid) {
1089
+ assertSupportedRecord(record, anchor.session_id, anchor.cwd);
1090
+ const recordUuid = uuidValue(record.uuid);
1091
+ const requiresIdentity = record.type === "user" ||
1092
+ record.type === "assistant" ||
1093
+ record.type === "system" ||
1094
+ record.type === "attachment" ||
1095
+ hasTurnCompletionSignal(record);
1096
+ if (!recordUuid) {
1097
+ if (requiresIdentity) {
1098
+ throw new Error("Claude active-task record has no stable UUID");
1099
+ }
1100
+ return undefined;
1101
+ }
1102
+ if (nonEmptyString(record.version) !== anchor.claude_version) {
1103
+ throw new Error("Claude transcript turn changed schema versions while it was running");
1104
+ }
1105
+ if (uuidValue(record.parentUuid) !== expectedParentUuid) {
1106
+ throw new Error("Claude active-task record is not the next exact descendant");
1107
+ }
1108
+ return recordUuid;
1109
+ }
1110
+ function advanceClaudeCheckpointToolState(record, recordUuid, unresolved) {
1111
+ const message = isRecord(record.message) ? record.message : undefined;
1112
+ const content = Array.isArray(message?.content) ? message.content : [];
1113
+ for (const block of content) {
1114
+ if (!isRecord(block)) {
1115
+ continue;
1116
+ }
1117
+ if (block.type === "tool_use") {
1118
+ const id = checkpointOpaqueId(block.id) ? block.id : undefined;
1119
+ if (!id || !recordUuid || unresolved.has(id)) {
1120
+ throw new Error("Claude active task contains invalid tool-use identity");
1121
+ }
1122
+ if (unresolved.size >= CLAUDE_ACTIVE_TASK_MAX_UNRESOLVED_TOOLS) {
1123
+ throw new Error("Claude active task has too many unresolved tool uses");
1124
+ }
1125
+ unresolved.set(id, { tool_use_id: id, owner_uuid: recordUuid });
1126
+ }
1127
+ if (block.type === "tool_result") {
1128
+ const id = checkpointOpaqueId(block.tool_use_id)
1129
+ ? block.tool_use_id
1130
+ : undefined;
1131
+ const pending = id ? unresolved.get(id) : undefined;
1132
+ if (!id ||
1133
+ !pending ||
1134
+ uuidValue(record.parentUuid) !== pending.owner_uuid ||
1135
+ uuidValue(record.sourceToolAssistantUUID) !== pending.owner_uuid) {
1136
+ throw new Error("Claude active task contains invalid tool-result identity");
1137
+ }
1138
+ unresolved.delete(id);
1139
+ }
1140
+ }
1141
+ }
1142
+ function advanceClaudeCheckpointDigest(recordCount, chainFingerprint, record) {
1143
+ if (recordCount >= Number.MAX_SAFE_INTEGER) {
1144
+ throw new Error("Claude active-task checkpoint record count overflowed");
1145
+ }
1146
+ return {
1147
+ recordCount: recordCount + 1,
1148
+ chainFingerprint: sha256Hex(`${chainFingerprint}\0${sha256Hex(JSON.stringify(record))}`)
1149
+ };
1150
+ }
1151
+ function appendBoundedRedactedClaudeText(current, value) {
1152
+ const redacted = boundedRedactedText(value);
1153
+ if (!redacted) {
1154
+ return current;
1155
+ }
1156
+ return boundedRedactedText(current ? `${current}\n${redacted}` : redacted);
1157
+ }
1158
+ function assertClaudeActiveTaskResumeBoundary(fd, offset) {
1159
+ if (offset > 0 && byteAt(fd, offset - 1) !== 0x0a) {
1160
+ throw new Error("Claude active-task resume offset is not a JSONL boundary");
1161
+ }
1162
+ }
1163
+ function readExactTranscriptBytes(fd, offset, length) {
1164
+ const buffer = Buffer.allocUnsafe(length);
1165
+ let readTotal = 0;
1166
+ while (readTotal < length) {
1167
+ const bytesRead = fs.readSync(fd, buffer, readTotal, length - readTotal, offset + readTotal);
1168
+ if (bytesRead === 0) {
1169
+ break;
1170
+ }
1171
+ readTotal += bytesRead;
1172
+ }
1173
+ if (readTotal !== length) {
1174
+ throw new Error("Claude transcript changed while it was being read");
1175
+ }
1176
+ return buffer;
1177
+ }
1178
+ function completeTranscriptJsonlPrefixLength(buffer) {
1179
+ if (buffer.length === 0) {
1180
+ return 0;
1181
+ }
1182
+ const newline = buffer.lastIndexOf(0x0a);
1183
+ return newline < 0 ? 0 : newline + 1;
1184
+ }
1185
+ function claudeActiveTaskUnavailable(reason) {
1186
+ return { status: "unavailable", reason, retryable: true };
1187
+ }
1188
+ function isRetryableClaudeProviderIoError(error) {
1189
+ if (!isRecord(error) || typeof error.code !== "string") {
1190
+ return false;
1191
+ }
1192
+ return new Set([
1193
+ "EACCES", "EAGAIN", "EBUSY", "EIO", "EMFILE", "ENFILE", "ENOENT",
1194
+ "EPERM", "ESTALE", "ETIMEDOUT"
1195
+ ]).has(error.code);
1196
+ }
433
1197
  /**
434
1198
  * Detects one completed Claude Code turn from the append-only local transcript.
435
1199
  * The detector intentionally fails closed on identity, schema, rotation, prompt,
@@ -676,6 +1440,10 @@ function readClaudeTranscriptTurnSnapshot(request, options, requiredAgentStatus,
676
1440
  function matchingManagedPrompt(snapshot) {
677
1441
  const promptCandidates = snapshot.records.filter((record) => {
678
1442
  const promptText = userPromptText(record);
1443
+ const exactPromptIdentity = snapshot.expectedPromptUuid
1444
+ ? uuidValue(record.uuid) === snapshot.expectedPromptUuid
1445
+ : snapshot.expectedPromptText !== undefined &&
1446
+ exactPromptText(promptText) === snapshot.expectedPromptText;
679
1447
  return record.type === "user" &&
680
1448
  isRecord(record.message) &&
681
1449
  record.message.role === "user" &&
@@ -685,7 +1453,7 @@ function matchingManagedPrompt(snapshot) {
685
1453
  normalizePath(record.cwd) === normalizePath(snapshot.cwd) &&
686
1454
  validTimestampMs(record.timestamp) !== undefined &&
687
1455
  promptText !== undefined &&
688
- exactPromptText(promptText) === snapshot.expectedPromptText &&
1456
+ exactPromptIdentity &&
689
1457
  exactRequestFingerprint(promptText) === snapshot.expectedRequestHash;
690
1458
  });
691
1459
  if (promptCandidates.length === 0) {
@@ -928,8 +1696,12 @@ function pendingApprovalTurn(snapshot) {
928
1696
  hasUnresolvedBackgroundWork(record))) {
929
1697
  return undefined;
930
1698
  }
931
- if (descendants.some((record) => hasTurnCompletionSignal(record) ||
932
- record.subtype === "stop_hook_summary")) {
1699
+ if (descendants.some((record) => record.subtype === "stop_hook_summary")) {
1700
+ return undefined;
1701
+ }
1702
+ const completionSignals = descendants.filter(hasTurnCompletionSignal);
1703
+ if (completionSignals.length > 0 &&
1704
+ !isPendingClaudeTurnDurationAppend(descendants, completionSignals)) {
933
1705
  return undefined;
934
1706
  }
935
1707
  const lastDescendant = descendants.at(-1);
@@ -942,6 +1714,29 @@ function pendingApprovalTurn(snapshot) {
942
1714
  }
943
1715
  return { promptUuid, descendants, lastDescendant };
944
1716
  }
1717
+ function isPendingClaudeTurnDurationAppend(descendants, completionSignals) {
1718
+ if (completionSignals.length === 0 || hasUnresolvedToolUse(descendants)) {
1719
+ return false;
1720
+ }
1721
+ const messageIds = new Set();
1722
+ for (const candidate of completionSignals) {
1723
+ const message = isRecord(candidate.message) ? candidate.message : undefined;
1724
+ const messageId = uuidValue(message?.id);
1725
+ if (candidate.type !== "assistant" ||
1726
+ candidate.isApiErrorMessage === true ||
1727
+ message?.role !== "assistant" ||
1728
+ message.stop_reason !== "end_turn" ||
1729
+ uuidValue(candidate.uuid) === undefined ||
1730
+ !messageId) {
1731
+ return false;
1732
+ }
1733
+ messageIds.add(messageId);
1734
+ }
1735
+ const finalSignal = completionSignals.at(-1);
1736
+ return messageIds.size === 1 &&
1737
+ finalSignal === descendants.at(-1) &&
1738
+ finalSignal !== undefined;
1739
+ }
945
1740
  function pendingApprovalToolUse(descendants) {
946
1741
  const toolUses = new Map();
947
1742
  const toolResults = new Map();
@@ -1208,24 +2003,59 @@ function openRelativeTranscript(projectsRoot, relativePath) {
1208
2003
  }
1209
2004
  }
1210
2005
  function readCompleteJsonlRecords(fd, offset, length) {
1211
- const buffer = Buffer.allocUnsafe(length);
1212
- let readTotal = 0;
1213
- while (readTotal < length) {
1214
- const bytesRead = fs.readSync(fd, buffer, readTotal, length - readTotal, offset + readTotal);
1215
- if (bytesRead === 0) {
1216
- break;
1217
- }
1218
- readTotal += bytesRead;
1219
- }
1220
- if (readTotal !== length) {
1221
- throw new Error("Claude transcript changed while it was being read");
2006
+ return readCompleteJsonlRecordEntries(fd, offset, length)
2007
+ .map((entry) => entry.record);
2008
+ }
2009
+ function readBoundedCompleteJsonlTail(fd, endOffset, maxBytes) {
2010
+ const offset = Math.max(0, endOffset - maxBytes);
2011
+ return readCompleteJsonlRecordEntries(fd, offset, endOffset - offset, {
2012
+ allowLeadingPartial: offset > 0
2013
+ });
2014
+ }
2015
+ function readCompleteJsonlRecordEntries(fd, offset, length, options = {}) {
2016
+ const buffer = options.preReadBuffer ??
2017
+ readExactTranscriptBytes(fd, offset, length);
2018
+ if (buffer.length !== length) {
2019
+ throw new Error("Claude transcript pre-read boundary is invalid");
1222
2020
  }
1223
- if (buffer.length === 0 || buffer[buffer.length - 1] !== 0x0a) {
2021
+ if (buffer.length === 0) {
1224
2022
  return [];
1225
2023
  }
1226
- const text = buffer.subarray(0, buffer.length - 1).toString("utf8");
2024
+ let start = 0;
2025
+ if (options.allowLeadingPartial === true &&
2026
+ offset > 0 &&
2027
+ byteAt(fd, offset - 1) !== 0x0a) {
2028
+ const firstNewline = buffer.indexOf(0x0a);
2029
+ if (firstNewline < 0) {
2030
+ return [];
2031
+ }
2032
+ start = firstNewline + 1;
2033
+ }
2034
+ let end = buffer.length;
2035
+ if (buffer[end - 1] !== 0x0a) {
2036
+ if (options.allowTrailingPartial !== true) {
2037
+ return [];
2038
+ }
2039
+ const lastNewline = buffer.lastIndexOf(0x0a);
2040
+ if (lastNewline < start) {
2041
+ return [];
2042
+ }
2043
+ end = lastNewline + 1;
2044
+ }
1227
2045
  const records = [];
1228
- for (const line of text.split("\n")) {
2046
+ let lineStart = start;
2047
+ while (lineStart < end) {
2048
+ const newline = buffer.indexOf(0x0a, lineStart);
2049
+ if (newline < 0 || newline >= end) {
2050
+ break;
2051
+ }
2052
+ let lineBuffer = buffer.subarray(lineStart, newline);
2053
+ const recordOffset = offset + lineStart;
2054
+ lineStart = newline + 1;
2055
+ if (lineBuffer.at(-1) === 0x0d) {
2056
+ lineBuffer = lineBuffer.subarray(0, lineBuffer.length - 1);
2057
+ }
2058
+ const line = lineBuffer.toString("utf8");
1229
2059
  if (!line.trim()) {
1230
2060
  continue;
1231
2061
  }
@@ -1239,10 +2069,16 @@ function readCompleteJsonlRecords(fd, offset, length) {
1239
2069
  if (!isRecord(parsed)) {
1240
2070
  throw new Error("Claude transcript contains a non-object JSONL record");
1241
2071
  }
1242
- records.push(parsed);
2072
+ records.push({ record: parsed, offsetBytes: recordOffset });
1243
2073
  }
1244
2074
  return records;
1245
2075
  }
2076
+ function byteAt(fd, offset) {
2077
+ const buffer = Buffer.allocUnsafe(1);
2078
+ return fs.readSync(fd, buffer, 0, 1, offset) === 1
2079
+ ? buffer[0]
2080
+ : undefined;
2081
+ }
1246
2082
  function descendantRecords(records, recordsByUuid, ancestorUuid, predicate) {
1247
2083
  return records.filter((record) => predicate(record) &&
1248
2084
  descendantChain(recordsByUuid, ancestorUuid, record) !== undefined);