@scotthuang/agent-knock-knock 0.12.18 → 0.12.19

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 (35) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +9 -9
  3. package/dist/src/claude-local-transcript-provider.d.ts +32 -0
  4. package/dist/src/claude-local-transcript-provider.js +106 -20
  5. package/dist/src/claude-local-transcript-provider.js.map +1 -1
  6. package/dist/src/cli-core.js +3 -0
  7. package/dist/src/cli-core.js.map +1 -1
  8. package/dist/src/openclaw-plugin-command-adapter.js +15 -5
  9. package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
  10. package/dist/src/openclaw-plugin-helpers.js +5 -1
  11. package/dist/src/openclaw-plugin-helpers.js.map +1 -1
  12. package/dist/src/openclaw-plugin-schemas.js +1 -1
  13. package/dist/src/openclaw-plugin-schemas.js.map +1 -1
  14. package/dist/src/store.d.ts +1 -1
  15. package/dist/src/store.js +3 -3
  16. package/dist/src/store.js.map +1 -1
  17. package/dist/src/terminal-command-cli-adapter.d.ts +16 -0
  18. package/dist/src/terminal-command-cli-adapter.js +67 -6
  19. package/dist/src/terminal-command-cli-adapter.js.map +1 -1
  20. package/dist/src/terminal-list-cli-adapter.js +4 -3
  21. package/dist/src/terminal-list-cli-adapter.js.map +1 -1
  22. package/dist/src/terminal-list-renderer.js +4 -4
  23. package/dist/src/terminal-list-renderer.js.map +1 -1
  24. package/dist/src/terminal-watch-cli-adapter.d.ts +44 -1
  25. package/dist/src/terminal-watch-cli-adapter.js +616 -21
  26. package/dist/src/terminal-watch-cli-adapter.js.map +1 -1
  27. package/dist/src/terminal-watch-service.js +2 -0
  28. package/dist/src/terminal-watch-service.js.map +1 -1
  29. package/dist/src/terminal-watch-store.d.ts +61 -4
  30. package/dist/src/terminal-watch-store.js +312 -11
  31. package/dist/src/terminal-watch-store.js.map +1 -1
  32. package/docs/quickstart-herdr.md +3 -3
  33. package/docs/quickstart-tmux.md +4 -4
  34. package/package.json +1 -1
  35. package/templates/openclaw-skills/agent-knock-knock/SKILL.md +7 -7
@@ -1,17 +1,18 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import path from "node:path";
3
+ import { callbackRouteForConversation } from "./callback-route-authority.js";
3
4
  import { parseCallbackRoute } from "./callback-transport.js";
4
- import { captureClaudeHumanStartedActiveTaskAnchor, observeClaudeHumanStartedActiveTask } from "./claude-local-transcript-provider.js";
5
+ import { captureClaudeTranscriptAnchor, captureClaudeHumanStartedActiveTaskAnchor, observeClaudeUserExplicitFallbackTranscript, observeClaudeHumanStartedActiveTask } from "./claude-local-transcript-provider.js";
5
6
  import { claudeRuntimeCompatibilityWarning } from "./claude-lifecycle-compatibility.js";
6
7
  import { codexRuntimeCompatibilityProfile } from "./codex-lifecycle-compatibility.js";
7
8
  import { turnIdForConversation } from "./protocol.js";
8
9
  import { decideTerminalWatchExternalTaskAuthority } from "./terminal-action-projection.js";
9
10
  import { exactTerminalWatchAction } from "./terminal-list-renderer.js";
10
- import { captureCodexHumanStartedActiveTaskAnchor, observeCodexHumanStartedActiveTask } from "./terminal-submission-acceptance.js";
11
+ import { captureCodexCandidateSetRolloutAcceptanceAnchor, captureCodexRolloutAcceptanceAnchor, detectCodexBoundRolloutCompletion, detectCodexCandidateSetRolloutAcceptance, detectCodexRolloutAcceptance, captureCodexHumanStartedActiveTaskAnchor, observeCodexHumanStartedActiveTask } from "./terminal-submission-acceptance.js";
11
12
  import { sameTerminalControlEvidenceIncarnation, terminalControlEvidence } from "./terminal-control-ref.js";
12
13
  import { createTerminalWatchCallbackCliAdapter, resolveTerminalWatchOpenClawCallback, resolveTerminalWatchOpenClawCallbackContext } from "./terminal-watch-callback-cli-adapter.js";
13
14
  import { createTerminalWatchService, terminalWatchObservationFence } from "./terminal-watch-service.js";
14
- import { createTerminalWatchStore, terminalWatchIdentityFingerprint } from "./terminal-watch-store.js";
15
+ import { createClaudeUserExplicitFallbackWatchAnchor, createCodexUserExplicitFallbackWatchAnchor, createTerminalWatchStore, isUserExplicitFallbackWatch, terminalUserExplicitFallbackWatchId, terminalWatchIdentityFingerprint } from "./terminal-watch-store.js";
15
16
  import { isRecord, nonBlankString as stringValue } from "./value-guards.js";
16
17
  const DEFAULT_TERMINAL_WATCH_HARD_TIMEOUT_MINUTES = 720;
17
18
  export function createTerminalWatchCliAdapter(dependencies) {
@@ -81,6 +82,162 @@ export function createTerminalWatchCliAdapter(dependencies) {
81
82
  }
82
83
  });
83
84
  }
85
+ async function prepareUserExplicitFallbackWatch(input) {
86
+ const callbackRoute = callbackRouteForUserExplicitFallback(input.options);
87
+ if (!callbackRoute)
88
+ return undefined;
89
+ await bestEffortStabilizePriorFallbackWatch(input.options, input.terminal.conversationId, input.requestHash);
90
+ const observed = await exactTerminalForWatch(input.terminal.conversationId, input.options, dependencies);
91
+ const rawTerminal = observed.rawTerminal;
92
+ assertSameUserExplicitFallbackTerminal(input.terminal, rawTerminal);
93
+ const agentVersion = requiredString(rawTerminal.agent_version, "running coding-agent version");
94
+ let anchor;
95
+ if (input.terminal.agent === "codex") {
96
+ const inventory = rawTerminal._codex_open_root_rollout_inventory;
97
+ const acceptanceAnchor = isRecord(inventory)
98
+ ? captureCodexCandidateSetRolloutAcceptanceAnchor({
99
+ inventory: inventory,
100
+ now: dependencies.now()
101
+ })
102
+ : captureCodexRolloutAcceptanceAnchor({
103
+ nativeThreadId: requiredString(rawTerminal.native_agent_session_id, "Codex native thread id"),
104
+ processUuid: requiredString(rawTerminal.native_agent_process_uuid, "Codex process UUID"),
105
+ processBirth: requiredString(rawTerminal.native_agent_process_birth, "Codex process birth"),
106
+ mode: "existing",
107
+ rollout: codexIdentity(rawTerminal).rollout,
108
+ now: dependencies.now()
109
+ });
110
+ anchor = createCodexUserExplicitFallbackWatchAnchor({
111
+ acceptanceAnchor,
112
+ requestHash: input.requestHash,
113
+ codexVersion: agentVersion
114
+ });
115
+ }
116
+ else {
117
+ const transcriptAnchor = captureClaudeTranscriptAnchor({
118
+ sessionId: requiredString(rawTerminal.native_agent_session_id, "Claude native session id"),
119
+ cwd: terminalWorkspace(rawTerminal),
120
+ pid: positiveInteger(rawTerminal.pid, "Claude PID"),
121
+ claudeHome: stringValue(input.options.claudeHome),
122
+ agentRows: dependencies.loadClaudeAgentRows(input.options, { required: true }),
123
+ now: dependencies.now()
124
+ });
125
+ if (!transcriptAnchor) {
126
+ throw new Error("Claude transcript anchor is unavailable before terminal input");
127
+ }
128
+ anchor = createClaudeUserExplicitFallbackWatchAnchor({
129
+ transcriptAnchor,
130
+ requestHash: input.requestHash,
131
+ claudeVersion: agentVersion
132
+ });
133
+ }
134
+ return {
135
+ watchId: terminalUserExplicitFallbackWatchId({
136
+ messageId: input.messageId,
137
+ physicalToken: input.physicalToken,
138
+ requestHash: input.requestHash
139
+ }),
140
+ terminalId: input.terminal.conversationId,
141
+ agent: input.terminal.agent,
142
+ pid: input.terminal.pid,
143
+ terminalEndpoint: terminalControlEvidence(input.terminal.terminalControl),
144
+ terminalIdentity: terminalWatchIdentity(rawTerminal, input.physicalToken),
145
+ physicalToken: input.physicalToken,
146
+ requestHash: input.requestHash,
147
+ callbackRoute,
148
+ openclawSession: callbackRoute.controller_session_id,
149
+ openclawBin: stringValue(input.options.openclawBin) ?? "openclaw",
150
+ timeoutMs: positiveMinutes(input.options.hardTimeoutMinutes ??
151
+ input.options.agentHardTimeoutMinutes, DEFAULT_TERMINAL_WATCH_HARD_TIMEOUT_MINUTES) * 60_000,
152
+ anchor
153
+ };
154
+ }
155
+ async function attachUserExplicitFallbackWatch(input) {
156
+ let observed;
157
+ try {
158
+ observed = await dependencies.observeExactTerminal({
159
+ options: input.options,
160
+ terminalId: input.prepared.terminalId
161
+ });
162
+ }
163
+ catch {
164
+ // Observation itself is best effort after the user's physical Send.
165
+ // Persist the exact pre-Send identity and let the durable provider
166
+ // artifact settle or invalidate the Watch.
167
+ }
168
+ if (observed?.state === "available") {
169
+ assertPreparedFallbackTerminal(input.prepared, observed.rawTerminal);
170
+ }
171
+ // An absent or temporarily unavailable terminal after Enter is not a
172
+ // callback veto. The immutable pre-Send provider anchor and terminal
173
+ // identity remain sufficient to recover a completion already on disk.
174
+ const service = serviceFor(input.options);
175
+ let watch;
176
+ try {
177
+ watch = service.create({
178
+ watch_id: input.prepared.watchId,
179
+ agent: input.prepared.agent,
180
+ terminal: input.prepared.terminalIdentity,
181
+ anchor: input.prepared.anchor,
182
+ callback_route: input.prepared.callbackRoute,
183
+ openclaw_session: input.prepared.openclawSession,
184
+ openclaw_bin: input.prepared.openclawBin,
185
+ timeout_ms: input.prepared.timeoutMs
186
+ });
187
+ }
188
+ catch (error) {
189
+ const existing = service.list().find((candidate) => candidate.watch_id === input.prepared.watchId);
190
+ if (!existing ||
191
+ !isUserExplicitFallbackWatch(existing) ||
192
+ existing.anchor.anchor_fingerprint !==
193
+ input.prepared.anchor.anchor_fingerprint) {
194
+ throw error;
195
+ }
196
+ watch = existing;
197
+ }
198
+ return {
199
+ callback_expected: true,
200
+ callback_mode: "terminal_watch",
201
+ watch_id: watch.watch_id
202
+ };
203
+ }
204
+ async function bestEffortStabilizePriorFallbackWatch(options, terminalId, requestHash) {
205
+ try {
206
+ const service = serviceFor(options);
207
+ const prior = service.list().filter((watch) => watch.status === "active" &&
208
+ isUserExplicitFallbackWatch(watch) &&
209
+ watch.terminal.terminal_id === terminalId &&
210
+ watch.anchor.request_hash === requestHash);
211
+ for (const watch of prior) {
212
+ try {
213
+ await service.reconcile(watch.watch_id);
214
+ }
215
+ catch {
216
+ // A prior callback must never delay or veto the new user Send.
217
+ }
218
+ }
219
+ }
220
+ catch {
221
+ // Store or observer failure is callback-only degradation. The caller
222
+ // still captures the new pre-Send anchor and physical Send proceeds.
223
+ }
224
+ }
225
+ function userExplicitFallbackWatchReceipt(input) {
226
+ try {
227
+ const watch = serviceFor(input.options).get(input.watchId);
228
+ if (!isUserExplicitFallbackWatch(watch) || !watch.callback_route) {
229
+ return undefined;
230
+ }
231
+ return {
232
+ callback_expected: true,
233
+ callback_mode: "terminal_watch",
234
+ watch_id: watch.watch_id
235
+ };
236
+ }
237
+ catch {
238
+ return undefined;
239
+ }
240
+ }
84
241
  async function runWatch(options) {
85
242
  const terminalId = requiredString(options.terminal, "--terminal");
86
243
  const callbackRoute = Object.hasOwn(options, "callbackRoute")
@@ -172,6 +329,9 @@ export function createTerminalWatchCliAdapter(dependencies) {
172
329
  .map(publicTerminalWatch);
173
330
  }
174
331
  return Object.freeze({
332
+ prepareUserExplicitFallbackWatch,
333
+ attachUserExplicitFallbackWatch,
334
+ userExplicitFallbackWatchReceipt,
175
335
  runWatch,
176
336
  runUnwatch,
177
337
  runWatchStatus,
@@ -185,7 +345,13 @@ function assertWatchAnchorVersion(anchor, terminal) {
185
345
  const artifactVersion = anchor.schema ===
186
346
  "agent-knock-knock/codex-human-started-active-task-anchor"
187
347
  ? anchor.codex_version
188
- : anchor.claude_version;
348
+ : anchor.schema ===
349
+ "agent-knock-knock/claude-human-started-active-task-anchor"
350
+ ? anchor.claude_version
351
+ : undefined;
352
+ if (!artifactVersion) {
353
+ throw new Error("manual Terminal Watch requires a human-started task anchor");
354
+ }
189
355
  if (artifactVersion !== runningVersion) {
190
356
  throw new Error(`the active task artifact reports ${artifactVersion}, not the running ` +
191
357
  `coding-agent version ${runningVersion}; no Terminal Watch was created`);
@@ -202,6 +368,37 @@ function routeBoundWatchCallbackRoute(template, watch) {
202
368
  controller_session_id: watch.openclaw_session
203
369
  });
204
370
  }
371
+ function callbackRouteForUserExplicitFallback(options) {
372
+ if (Object.hasOwn(options, "callbackRoute")) {
373
+ return parseCallbackRoute(options.callbackRoute);
374
+ }
375
+ const openclawSession = stringValue(options.openclawSession);
376
+ return callbackRouteForConversation({
377
+ gateway_method: options.gatewayMethod,
378
+ gateway_session: options.gatewaySession ?? openclawSession,
379
+ openclaw_session: openclawSession,
380
+ openclaw_bin: options.openclawBin,
381
+ gateway_url: options.gatewayUrl
382
+ });
383
+ }
384
+ function assertSameUserExplicitFallbackTerminal(expected, observed) {
385
+ if (requiredString(observed.id, "terminal id") !== expected.conversationId ||
386
+ terminalAgent(observed) !== expected.agent ||
387
+ positiveInteger(observed.pid, "terminal PID") !== expected.pid ||
388
+ !isRecord(observed.terminal_control) ||
389
+ !sameTerminalControlEvidenceIncarnation(expected.terminalControl, observed.terminal_control)) {
390
+ throw new Error("terminal changed before the user-explicit fallback Watch anchor was captured");
391
+ }
392
+ }
393
+ function assertPreparedFallbackTerminal(prepared, observed) {
394
+ if (requiredString(observed.id, "terminal id") !== prepared.terminalId ||
395
+ terminalAgent(observed) !== prepared.agent ||
396
+ positiveInteger(observed.pid, "terminal PID") !== prepared.pid ||
397
+ !isRecord(observed.terminal_control) ||
398
+ !sameTerminalControlEvidenceIncarnation(prepared.terminalEndpoint, observed.terminal_control)) {
399
+ throw new Error("terminal changed before the user-explicit fallback Watch was attached");
400
+ }
401
+ }
205
402
  async function exactTerminalForWatch(terminalId, options, dependencies) {
206
403
  const observation = await dependencies.observeExactTerminal({
207
404
  options,
@@ -280,8 +477,22 @@ async function observeTerminalWatch(watch, options, dependencies) {
280
477
  ? exactTerminal.terminal
281
478
  : undefined;
282
479
  const terminalMatches = rawTerminal
283
- ? terminalMatchesWatch(rawTerminal, watch)
480
+ ? isUserExplicitFallbackWatch(watch)
481
+ ? terminalMatchesUserExplicitFallbackWatch(rawTerminal, watch)
482
+ : terminalMatchesWatch(rawTerminal, watch)
284
483
  : false;
484
+ if (isUserExplicitFallbackWatch(watch)) {
485
+ return observeUserExplicitFallbackTerminalWatch({
486
+ watch,
487
+ exactTerminal,
488
+ rawTerminal,
489
+ projectedTerminal,
490
+ terminalMatches,
491
+ observedAt,
492
+ options,
493
+ dependencies
494
+ });
495
+ }
285
496
  const observation = watch.anchor.schema ===
286
497
  "agent-knock-knock/codex-human-started-active-task-anchor"
287
498
  ? observeCodexHumanStartedActiveTask({
@@ -291,13 +502,18 @@ async function observeTerminalWatch(watch, options, dependencies) {
291
502
  : codexIdentityForWatch(watch),
292
503
  resumeOffsetBytes: watch.observation_checkpoint.safe_resume_offset_bytes
293
504
  })
294
- : observeClaudeHumanStartedActiveTask({
295
- anchor: watch.anchor,
296
- claudeHome: stringValue(options.claudeHome),
297
- agentRows: dependencies.loadClaudeAgentRows(options, { required: true }),
298
- resumeOffsetBytes: watch.observation_checkpoint.safe_resume_offset_bytes,
299
- checkpoint: claudeObservationCheckpoint(watch)
300
- });
505
+ : watch.anchor.schema ===
506
+ "agent-knock-knock/claude-human-started-active-task-anchor"
507
+ ? observeClaudeHumanStartedActiveTask({
508
+ anchor: watch.anchor,
509
+ claudeHome: stringValue(options.claudeHome),
510
+ agentRows: dependencies.loadClaudeAgentRows(options, { required: true }),
511
+ resumeOffsetBytes: watch.observation_checkpoint.safe_resume_offset_bytes,
512
+ checkpoint: claudeObservationCheckpoint(watch)
513
+ })
514
+ : (() => {
515
+ throw new Error("terminal Watch anchor is unsupported");
516
+ })();
301
517
  if (observation.status === "invalidated") {
302
518
  return invalidatedObservation(watch, observedAt, "task_anchor_invalidated");
303
519
  }
@@ -375,6 +591,342 @@ async function observeTerminalWatch(watch, options, dependencies) {
375
591
  observation_checkpoint: observationCheckpoint
376
592
  };
377
593
  }
594
+ async function observeUserExplicitFallbackTerminalWatch(input) {
595
+ const { watch, exactTerminal, rawTerminal, terminalMatches, observedAt, options, dependencies } = input;
596
+ const fence = terminalWatchObservationFence(watch);
597
+ if (watch.anchor.schema ===
598
+ "agent-knock-knock/codex-user-explicit-fallback-watch-anchor") {
599
+ const anchor = watch.anchor.acceptance_anchor;
600
+ const persistedCheckpoint = codexFallbackObservationCheckpoint(watch);
601
+ let acceptance;
602
+ let currentIdentity;
603
+ let acceptedCheckpoint;
604
+ if (persistedCheckpoint.acceptance_evidence &&
605
+ persistedCheckpoint.accepted_identity) {
606
+ acceptance = persistedCheckpoint.acceptance_evidence;
607
+ currentIdentity = {
608
+ sessionId: persistedCheckpoint.accepted_identity.native_thread_id,
609
+ processUuid: persistedCheckpoint.accepted_identity.process_uuid,
610
+ processBirth: persistedCheckpoint.accepted_identity.process_birth,
611
+ rollout: persistedCheckpoint.accepted_identity.rollout
612
+ };
613
+ acceptedCheckpoint = persistedCheckpoint;
614
+ }
615
+ else if (anchor.version === 3) {
616
+ const inventory = rawTerminal?._codex_open_root_rollout_inventory;
617
+ if (!isRecord(inventory)) {
618
+ return fallbackPendingOrTerminalObservation({
619
+ watch,
620
+ exactTerminal,
621
+ rawTerminal,
622
+ terminalMatches,
623
+ observedAt,
624
+ reasonCode: "codex_rollout_inventory_unavailable"
625
+ });
626
+ }
627
+ const result = detectCodexCandidateSetRolloutAcceptance({
628
+ anchor,
629
+ currentInventory: inventory,
630
+ requestHash: watch.anchor.request_hash
631
+ });
632
+ if (result.status === "uncertain") {
633
+ if (result.code === "candidate_scan_invalid") {
634
+ return {
635
+ ...fence,
636
+ kind: "unavailable",
637
+ observed_at: observedAt,
638
+ reason_code: "native_acceptance_scan_unavailable"
639
+ };
640
+ }
641
+ return invalidatedObservation(watch, observedAt, `native_acceptance_${result.code}`);
642
+ }
643
+ if (result.status === "pending") {
644
+ return fallbackPendingOrTerminalObservation({
645
+ watch,
646
+ exactTerminal,
647
+ rawTerminal,
648
+ terminalMatches,
649
+ observedAt
650
+ });
651
+ }
652
+ acceptance = result.evidence;
653
+ currentIdentity = result.identity;
654
+ acceptedCheckpoint = codexFallbackAcceptedCheckpoint(watch, acceptance, currentIdentity);
655
+ }
656
+ else {
657
+ try {
658
+ currentIdentity = anchor.version === 1
659
+ ? {
660
+ sessionId: anchor.native_thread_id,
661
+ processUuid: anchor.process_uuid,
662
+ processBirth: anchor.process_birth,
663
+ rollout: anchor.rollout
664
+ }
665
+ : rawTerminal
666
+ ? codexIdentity(rawTerminal)
667
+ : (() => {
668
+ throw new Error("Codex pre-materialization identity is unavailable");
669
+ })();
670
+ acceptance = detectCodexRolloutAcceptance({
671
+ anchor,
672
+ currentIdentity,
673
+ requestHash: watch.anchor.request_hash
674
+ });
675
+ }
676
+ catch (error) {
677
+ if (retryableProviderError(error)) {
678
+ return {
679
+ ...fence,
680
+ kind: "unavailable",
681
+ observed_at: observedAt,
682
+ reason_code: "native_acceptance_scan_unavailable"
683
+ };
684
+ }
685
+ return invalidatedObservation(watch, observedAt, "native_acceptance_identity_changed");
686
+ }
687
+ if (!acceptance) {
688
+ return fallbackPendingOrTerminalObservation({
689
+ watch,
690
+ exactTerminal,
691
+ rawTerminal,
692
+ terminalMatches,
693
+ observedAt
694
+ });
695
+ }
696
+ acceptedCheckpoint = codexFallbackAcceptedCheckpoint(watch, acceptance, currentIdentity);
697
+ }
698
+ if (!acceptance || !acceptedCheckpoint) {
699
+ throw new Error("Codex fallback Watch acceptance checkpoint is incomplete");
700
+ }
701
+ const completion = detectCodexBoundRolloutCompletion({
702
+ anchor,
703
+ acceptanceEvidence: acceptance,
704
+ currentIdentity,
705
+ requestHash: watch.anchor.request_hash
706
+ });
707
+ if (completion.status === "completed") {
708
+ return fallbackCompletionObservation(watch, observedAt, completion.completion, completion.diagnostics.observed_end_offset_bytes, codexFallbackAcceptedCheckpoint(watch, acceptance, currentIdentity));
709
+ }
710
+ if (completion.status === "failure") {
711
+ if (completion.diagnostics.code === "rollout_unreadable") {
712
+ return {
713
+ ...fence,
714
+ kind: "unavailable",
715
+ observed_at: observedAt,
716
+ safe_resume_offset_bytes: acceptedCheckpoint.safe_resume_offset_bytes,
717
+ observation_checkpoint: acceptedCheckpoint,
718
+ reason_code: "accepted_rollout_unavailable"
719
+ };
720
+ }
721
+ return invalidatedObservation(watch, observedAt, `native_completion_${completion.diagnostics.code}`);
722
+ }
723
+ return fallbackPendingOrTerminalObservation({
724
+ watch,
725
+ exactTerminal,
726
+ rawTerminal,
727
+ terminalMatches,
728
+ observedAt,
729
+ observedEndOffsetBytes: completion.diagnostics.observed_end_offset_bytes,
730
+ observationCheckpoint: codexFallbackAcceptedCheckpoint(watch, acceptance, currentIdentity)
731
+ });
732
+ }
733
+ if (watch.anchor.schema !==
734
+ "agent-knock-knock/claude-user-explicit-fallback-watch-anchor") {
735
+ throw new Error("user-explicit fallback Watch anchor is unsupported");
736
+ }
737
+ const request = {
738
+ sessionId: watch.anchor.transcript_anchor.session_id,
739
+ cwd: watch.anchor.transcript_anchor.cwd,
740
+ requestHash: watch.anchor.request_hash,
741
+ startedAt: watch.anchor.captured_at,
742
+ context: {
743
+ claudeTranscriptAnchor: watch.anchor.transcript_anchor,
744
+ pid: watch.anchor.transcript_anchor.pid
745
+ }
746
+ };
747
+ const persistedCheckpoint = claudeFallbackObservationCheckpoint(watch);
748
+ const observation = observeClaudeUserExplicitFallbackTranscript(request, {
749
+ claudeHome: stringValue(options.claudeHome),
750
+ // This observer is transcript-authoritative by design. Completion must
751
+ // remain recoverable after the exact pane/process leaves discovery.
752
+ acceptanceEvidence: persistedCheckpoint.acceptance_evidence
753
+ });
754
+ if (observation.status === "unavailable") {
755
+ return {
756
+ ...fence,
757
+ kind: "unavailable",
758
+ observed_at: observedAt,
759
+ reason_code: "native_acceptance_scan_unavailable"
760
+ };
761
+ }
762
+ const acceptance = observation.acceptance;
763
+ if (!acceptance) {
764
+ return fallbackPendingOrTerminalObservation({
765
+ watch,
766
+ exactTerminal,
767
+ rawTerminal,
768
+ terminalMatches,
769
+ observedAt
770
+ });
771
+ }
772
+ const acceptedCheckpoint = claudeFallbackAcceptedCheckpoint(watch, acceptance);
773
+ if (observation.status === "completed") {
774
+ return fallbackCompletionObservation(watch, observedAt, observation.completion, observation.observedEndOffsetBytes, acceptedCheckpoint);
775
+ }
776
+ return fallbackPendingOrTerminalObservation({
777
+ watch,
778
+ exactTerminal,
779
+ rawTerminal,
780
+ terminalMatches,
781
+ observedAt,
782
+ observedEndOffsetBytes: observation.observedEndOffsetBytes,
783
+ observationCheckpoint: acceptedCheckpoint
784
+ });
785
+ }
786
+ function fallbackPendingOrTerminalObservation(input) {
787
+ if (input.exactTerminal.state === "unavailable") {
788
+ return {
789
+ ...terminalWatchObservationFence(input.watch),
790
+ kind: "unavailable",
791
+ observed_at: input.observedAt,
792
+ reason_code: input.reasonCode ?? "terminal_observation_unavailable"
793
+ };
794
+ }
795
+ if (input.exactTerminal.state === "absent" || !input.rawTerminal) {
796
+ return invalidatedObservation(input.watch, input.observedAt, "terminal_process_unavailable");
797
+ }
798
+ if (!input.terminalMatches) {
799
+ return invalidatedObservation(input.watch, input.observedAt, "terminal_identity_changed");
800
+ }
801
+ if (input.reasonCode) {
802
+ return {
803
+ ...terminalWatchObservationFence(input.watch),
804
+ kind: "unavailable",
805
+ observed_at: input.observedAt,
806
+ reason_code: input.reasonCode
807
+ };
808
+ }
809
+ return fallbackPendingObservation(input.watch, input.observedAt, input.observedEndOffsetBytes, input.observationCheckpoint);
810
+ }
811
+ function fallbackPendingObservation(watch, observedAt, observedEndOffsetBytes, observationCheckpoint) {
812
+ const effectiveCheckpoint = observationCheckpoint ?? (watch.anchor.schema ===
813
+ "agent-knock-knock/codex-user-explicit-fallback-watch-anchor"
814
+ ? watch.observation_checkpoint
815
+ : undefined);
816
+ const safeResumeOffsetBytes = effectiveCheckpoint
817
+ ?.safe_resume_offset_bytes ?? fallbackResumeOffset(watch, observedEndOffsetBytes);
818
+ return {
819
+ ...terminalWatchObservationFence(watch),
820
+ kind: "pending",
821
+ observed_at: observedAt,
822
+ safe_resume_offset_bytes: safeResumeOffsetBytes,
823
+ ...(effectiveCheckpoint
824
+ ? { observation_checkpoint: effectiveCheckpoint }
825
+ : {})
826
+ };
827
+ }
828
+ function codexFallbackObservationCheckpoint(watch) {
829
+ const checkpoint = watch.observation_checkpoint;
830
+ if (!("schema" in checkpoint) ||
831
+ checkpoint.schema !==
832
+ "agent-knock-knock/codex-user-explicit-fallback-watch-checkpoint") {
833
+ throw new Error("Codex fallback Watch has no exact acceptance checkpoint");
834
+ }
835
+ return checkpoint;
836
+ }
837
+ function claudeFallbackObservationCheckpoint(watch) {
838
+ const checkpoint = watch.observation_checkpoint;
839
+ if (!("schema" in checkpoint) ||
840
+ checkpoint.schema !==
841
+ "agent-knock-knock/claude-user-explicit-fallback-watch-checkpoint") {
842
+ throw new Error("Claude fallback Watch has no exact acceptance checkpoint");
843
+ }
844
+ return checkpoint;
845
+ }
846
+ function codexFallbackAcceptedCheckpoint(watch, acceptanceEvidence, currentIdentity) {
847
+ const nativeThreadId = requiredString(currentIdentity.sessionId, "accepted Codex native thread id");
848
+ const processUuid = requiredString(currentIdentity.processUuid, "accepted Codex process UUID");
849
+ const processBirth = requiredString(currentIdentity.processBirth, "accepted Codex process birth");
850
+ const rollout = currentIdentity.rollout;
851
+ if (!rollout) {
852
+ throw new Error("accepted Codex rollout identity is unavailable");
853
+ }
854
+ const acceptanceOffset = numericMetadata(acceptanceEvidence.metadata, "observed_end_offset_bytes");
855
+ if (acceptanceOffset === undefined) {
856
+ throw new Error("accepted Codex rollout boundary is unavailable");
857
+ }
858
+ return {
859
+ schema: "agent-knock-knock/codex-user-explicit-fallback-watch-checkpoint",
860
+ version: 1,
861
+ safe_resume_offset_bytes: fallbackResumeOffset(watch, acceptanceOffset),
862
+ acceptance_evidence: acceptanceEvidence,
863
+ accepted_identity: {
864
+ native_thread_id: nativeThreadId,
865
+ process_uuid: processUuid,
866
+ process_birth: processBirth,
867
+ rollout
868
+ }
869
+ };
870
+ }
871
+ function claudeFallbackAcceptedCheckpoint(watch, acceptanceEvidence) {
872
+ const acceptanceOffset = numericMetadata(acceptanceEvidence.metadata, "observed_end_offset_bytes");
873
+ if (acceptanceOffset === undefined) {
874
+ throw new Error("accepted Claude transcript boundary is unavailable");
875
+ }
876
+ const promptUuid = requiredString(acceptanceEvidence.acceptanceId, "accepted Claude prompt UUID");
877
+ return {
878
+ schema: "agent-knock-knock/claude-user-explicit-fallback-watch-checkpoint",
879
+ version: 1,
880
+ safe_resume_offset_bytes: fallbackResumeOffset(watch, acceptanceOffset),
881
+ acceptance_evidence: acceptanceEvidence,
882
+ accepted_prompt_uuid: promptUuid
883
+ };
884
+ }
885
+ function fallbackCompletionObservation(watch, observedAt, completion, observedEndOffsetBytes, observationCheckpoint) {
886
+ const safeResumeOffsetBytes = observationCheckpoint
887
+ ?.safe_resume_offset_bytes ?? fallbackResumeOffset(watch, observedEndOffsetBytes);
888
+ const kind = completion.outcome === "failure" ? "failed" : "completed";
889
+ return {
890
+ ...terminalWatchObservationFence(watch),
891
+ kind,
892
+ observed_at: observedAt,
893
+ safe_resume_offset_bytes: safeResumeOffsetBytes,
894
+ ...(observationCheckpoint
895
+ ? { observation_checkpoint: observationCheckpoint }
896
+ : {}),
897
+ evidence_fingerprint: sha256({
898
+ kind,
899
+ watch_id: watch.watch_id,
900
+ completion_id: completion.id ?? null,
901
+ completion_timestamp: completion.timestamp ?? null,
902
+ anchor_fingerprint: watch.anchor.anchor_fingerprint
903
+ }),
904
+ reason_code: completion.outcome === "failure"
905
+ ? "anchored_task_failed"
906
+ : "anchored_task_completed",
907
+ completion_text: completion.text.slice(0, 4000),
908
+ completion_id: completion.id,
909
+ completion_timestamp: completion.timestamp
910
+ };
911
+ }
912
+ function fallbackResumeOffset(watch, observedEndOffsetBytes) {
913
+ return Math.max(watch.observation_checkpoint.safe_resume_offset_bytes, Number.isSafeInteger(observedEndOffsetBytes) &&
914
+ Number(observedEndOffsetBytes) >= 0
915
+ ? Number(observedEndOffsetBytes)
916
+ : 0);
917
+ }
918
+ function numericMetadata(metadata, field) {
919
+ const value = metadata?.[field];
920
+ return Number.isSafeInteger(value) && Number(value) >= 0
921
+ ? Number(value)
922
+ : undefined;
923
+ }
924
+ function retryableProviderError(error) {
925
+ return isRecord(error) && typeof error.code === "string" && new Set([
926
+ "EACCES", "EAGAIN", "EBUSY", "EIO", "EMFILE", "ENFILE", "ENOENT",
927
+ "EPERM", "ESTALE", "ETIMEDOUT"
928
+ ]).has(error.code);
929
+ }
378
930
  function claudeObservationCheckpoint(watch) {
379
931
  const checkpoint = watch.observation_checkpoint;
380
932
  if (!("schema" in checkpoint) ||
@@ -406,6 +958,31 @@ function terminalMatchesWatch(terminal, watch) {
406
958
  return false;
407
959
  }
408
960
  }
961
+ function terminalMatchesUserExplicitFallbackWatch(terminal, watch) {
962
+ try {
963
+ if (!isUserExplicitFallbackWatch(watch) ||
964
+ terminalAgent(terminal) !== watch.agent ||
965
+ requiredString(terminal.id, "terminal id") !==
966
+ watch.terminal.terminal_id ||
967
+ terminalWorkspace(terminal) !== watch.terminal.workspace ||
968
+ !isRecord(terminal.terminal_control) ||
969
+ !sameTerminalControlEvidenceIncarnation(terminal.terminal_control, watch.terminal.terminal_endpoint)) {
970
+ return false;
971
+ }
972
+ if (watch.anchor.schema ===
973
+ "agent-knock-knock/codex-user-explicit-fallback-watch-anchor") {
974
+ return requiredString(terminal.native_agent_process_uuid, "Codex process UUID") === watch.anchor.acceptance_anchor.process_uuid &&
975
+ requiredString(terminal.native_agent_process_birth, "Codex process birth") === watch.anchor.acceptance_anchor.process_birth;
976
+ }
977
+ return watch.anchor.schema ===
978
+ "agent-knock-knock/claude-user-explicit-fallback-watch-anchor" &&
979
+ positiveInteger(terminal.pid, "Claude PID") ===
980
+ watch.anchor.transcript_anchor.pid;
981
+ }
982
+ catch {
983
+ return false;
984
+ }
985
+ }
409
986
  function terminalWatchIdentity(terminal, bindingToken) {
410
987
  const terminalControl = terminal.terminal_control;
411
988
  return {
@@ -447,24 +1024,28 @@ function codexIdentityForWatch(watch) {
447
1024
  function publicTerminalWatch(watch) {
448
1025
  const pending = watch.notification_outbox.filter(({ status }) => status === "pending" || status === "delivering" || status === "failed").length;
449
1026
  const compatibilityWarning = watch.agent === "codex" &&
450
- watch.anchor.schema ===
451
- "agent-knock-knock/codex-human-started-active-task-anchor"
1027
+ (watch.anchor.schema ===
1028
+ "agent-knock-knock/codex-human-started-active-task-anchor" ||
1029
+ watch.anchor.schema ===
1030
+ "agent-knock-knock/codex-user-explicit-fallback-watch-anchor")
452
1031
  ? codexRuntimeCompatibilityProfile(watch.anchor.codex_version)
453
1032
  ?.compatibilityWarning
454
1033
  : watch.agent === "claude" &&
455
- watch.anchor.schema ===
456
- "agent-knock-knock/claude-human-started-active-task-anchor"
1034
+ (watch.anchor.schema ===
1035
+ "agent-knock-knock/claude-human-started-active-task-anchor" ||
1036
+ watch.anchor.schema ===
1037
+ "agent-knock-knock/claude-user-explicit-fallback-watch-anchor")
457
1038
  ? claudeRuntimeCompatibilityWarning(watch.anchor.claude_version)
458
1039
  : undefined;
1040
+ const userExplicitFallback = isUserExplicitFallbackWatch(watch);
459
1041
  return {
460
1042
  watch_id: watch.watch_id,
461
- source: "human_started_terminal_watch",
1043
+ source: userExplicitFallback
1044
+ ? "terminal_user_explicit_fallback_watch"
1045
+ : "human_started_terminal_watch",
462
1046
  agent: watch.agent,
463
1047
  terminal_id: watch.terminal.terminal_id,
464
- native_thread_id: watch.anchor.schema ===
465
- "agent-knock-knock/codex-human-started-active-task-anchor"
466
- ? watch.anchor.native_thread_id
467
- : watch.anchor.session_id,
1048
+ native_thread_id: terminalWatchNativeThreadId(watch),
468
1049
  workspace: watch.terminal.workspace,
469
1050
  status: watch.status,
470
1051
  activity_state: watch.status === "active" ? "watching" : "settled",
@@ -510,6 +1091,20 @@ function publicTerminalWatch(watch) {
510
1091
  }
511
1092
  };
512
1093
  }
1094
+ function terminalWatchNativeThreadId(watch) {
1095
+ switch (watch.anchor.schema) {
1096
+ case "agent-knock-knock/codex-human-started-active-task-anchor":
1097
+ return watch.anchor.native_thread_id;
1098
+ case "agent-knock-knock/claude-human-started-active-task-anchor":
1099
+ return watch.anchor.session_id;
1100
+ case "agent-knock-knock/codex-user-explicit-fallback-watch-anchor":
1101
+ return watch.anchor.acceptance_anchor.version === 1
1102
+ ? watch.anchor.acceptance_anchor.native_thread_id
1103
+ : undefined;
1104
+ case "agent-knock-knock/claude-user-explicit-fallback-watch-anchor":
1105
+ return watch.anchor.transcript_anchor.session_id;
1106
+ }
1107
+ }
513
1108
  function approvalFingerprint(terminal) {
514
1109
  if (terminal.activity_state !== "awaiting_approval")
515
1110
  return undefined;