@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.
- package/CHANGELOG.md +22 -0
- package/README.md +39 -26
- package/dist/src/approval-policy.d.ts +11 -2
- package/dist/src/approval-policy.js +33 -7
- package/dist/src/approval-policy.js.map +1 -1
- package/dist/src/claude-local-transcript-provider.d.ts +81 -0
- package/dist/src/claude-local-transcript-provider.js +854 -18
- package/dist/src/claude-local-transcript-provider.js.map +1 -1
- package/dist/src/cli-core.js +85 -0
- package/dist/src/cli-core.js.map +1 -1
- package/dist/src/deferred-foreground-authority-cli-adapter.js +90 -19
- package/dist/src/deferred-foreground-authority-cli-adapter.js.map +1 -1
- package/dist/src/deferred-foreground-transfer.js +3 -4
- package/dist/src/deferred-foreground-transfer.js.map +1 -1
- package/dist/src/openclaw-callback-transport.d.ts +5 -0
- package/dist/src/openclaw-callback-transport.js +1 -1
- package/dist/src/openclaw-callback-transport.js.map +1 -1
- package/dist/src/openclaw-plugin-callback-adapter.js +96 -14
- package/dist/src/openclaw-plugin-callback-adapter.js.map +1 -1
- package/dist/src/openclaw-plugin-command-adapter.js +548 -107
- package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
- package/dist/src/openclaw-plugin-helpers.d.ts +27 -4
- package/dist/src/openclaw-plugin-helpers.js +309 -44
- package/dist/src/openclaw-plugin-helpers.js.map +1 -1
- package/dist/src/openclaw-plugin-schemas.d.ts +44 -68
- package/dist/src/openclaw-plugin-schemas.js +63 -91
- package/dist/src/openclaw-plugin-schemas.js.map +1 -1
- package/dist/src/openclaw-plugin-supervisor.js +48 -10
- package/dist/src/openclaw-plugin-supervisor.js.map +1 -1
- package/dist/src/openclaw-private-authority-offers.d.ts +22 -0
- package/dist/src/openclaw-private-authority-offers.js +177 -0
- package/dist/src/openclaw-private-authority-offers.js.map +1 -0
- package/dist/src/store.d.ts +1 -0
- package/dist/src/store.js +3 -1
- package/dist/src/store.js.map +1 -1
- package/dist/src/terminal-action-projection.d.ts +19 -1
- package/dist/src/terminal-action-projection.js +23 -0
- package/dist/src/terminal-action-projection.js.map +1 -1
- package/dist/src/terminal-command-cli-adapter.d.ts +5 -0
- package/dist/src/terminal-command-cli-adapter.js +249 -89
- package/dist/src/terminal-command-cli-adapter.js.map +1 -1
- package/dist/src/terminal-dispatch-repository-cli-adapter.js +305 -79
- package/dist/src/terminal-dispatch-repository-cli-adapter.js.map +1 -1
- package/dist/src/terminal-list-cli-adapter.d.ts +31 -7
- package/dist/src/terminal-list-cli-adapter.js +285 -67
- package/dist/src/terminal-list-cli-adapter.js.map +1 -1
- package/dist/src/terminal-list-renderer.d.ts +2 -0
- package/dist/src/terminal-list-renderer.js +116 -41
- package/dist/src/terminal-list-renderer.js.map +1 -1
- package/dist/src/terminal-monitor-cli-adapter.js +1 -1
- package/dist/src/terminal-monitor-cli-adapter.js.map +1 -1
- package/dist/src/terminal-monitor-state-cli-adapter.js +6 -15
- package/dist/src/terminal-monitor-state-cli-adapter.js.map +1 -1
- package/dist/src/terminal-status-cli-adapter.d.ts +10 -0
- package/dist/src/terminal-status-cli-adapter.js +89 -7
- package/dist/src/terminal-status-cli-adapter.js.map +1 -1
- package/dist/src/terminal-submission-acceptance.d.ts +60 -1
- package/dist/src/terminal-submission-acceptance.js +560 -0
- package/dist/src/terminal-submission-acceptance.js.map +1 -1
- package/dist/src/terminal-watch-callback-cli-adapter.d.ts +20 -0
- package/dist/src/terminal-watch-callback-cli-adapter.js +55 -0
- package/dist/src/terminal-watch-callback-cli-adapter.js.map +1 -0
- package/dist/src/terminal-watch-cli-adapter.d.ts +64 -0
- package/dist/src/terminal-watch-cli-adapter.js +508 -0
- package/dist/src/terminal-watch-cli-adapter.js.map +1 -0
- package/dist/src/terminal-watch-service.d.ts +97 -0
- package/dist/src/terminal-watch-service.js +714 -0
- package/dist/src/terminal-watch-service.js.map +1 -0
- package/dist/src/terminal-watch-store.d.ts +149 -0
- package/dist/src/terminal-watch-store.js +861 -0
- package/dist/src/terminal-watch-store.js.map +1 -0
- package/docs/quickstart-herdr.md +8 -0
- package/docs/quickstart-tmux.md +33 -6
- package/openclaw.plugin.json +11 -3
- package/package.json +1 -1
- 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
|
-
|
|
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) =>
|
|
932
|
-
|
|
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
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
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
|
|
2021
|
+
if (buffer.length === 0) {
|
|
1224
2022
|
return [];
|
|
1225
2023
|
}
|
|
1226
|
-
|
|
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
|
-
|
|
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);
|