@scotthuang/agent-knock-knock 0.10.2 → 0.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/cli.js CHANGED
@@ -8,14 +8,14 @@ import { fileURLToPath } from "node:url";
8
8
  import { createCodexTerminalAgentAdapter, detectCodexDurableCompletion } from "./codex-terminal-agent-adapter.js";
9
9
  import { createClaudeTerminalAgentAdapter } from "./claude-terminal-agent-adapter.js";
10
10
  import { captureClaudeTranscriptAnchor, defaultClaudeHome, detectClaudeTranscriptAcceptance, detectClaudeTranscriptCompletion, detectClaudeTranscriptPendingApproval, listClaudeThreadLifecycleCandidates, revalidateClaudeThreadLifecycleCandidate } from "./claude-local-transcript-provider.js";
11
- import { captureCodexRolloutAcceptanceAnchor, detectCodexRolloutAcceptance, terminalSubmissionReplayReceipt, validateTerminalSubmissionAcceptanceEvidence } from "./terminal-submission-acceptance.js";
11
+ import { captureCodexRolloutAcceptanceAnchor, detectCodexBoundRolloutCompletion, detectCodexRolloutAcceptance, terminalSubmissionReplayReceipt, validateTerminalSubmissionAcceptanceEvidence } from "./terminal-submission-acceptance.js";
12
12
  import { CodexLocalSessionProvider } from "./codex-local-session-provider.js";
13
13
  import { CodexStoreAdapter } from "./codex-store-adapter.js";
14
14
  import { applyMessageToConversation, budgetAction, createConversation, createMessage, effectiveTurnStatus, executorForConversation, extractStructuredMessage, isTurnPhaseStatus, normalizeLegacyCallbackStatus, parseMessageJson, resolveExecutor, sessionIdForConversation, turnIdForConversation } from "./protocol.js";
15
15
  import { EXECUTOR_KINDS, executorDefinitionForKind, isExecutorKind } from "./executors.js";
16
16
  import { redactString, writeRuntimeLog } from "./runtime-log.js";
17
17
  import { formatTranscript, readNdjsonLog } from "./transcript.js";
18
- import { appendEvent, assertStoreWriterCompatible, defaultStoreDir, ensureDir, ensureStoreWritable, inspectStoreCompatibility, listConversations, logPathForStatePath, loadConversationById, loadState, messageEvent, pathsForConversation, pathsForConversationDir, saveState, statePathForConversationId, withStoreWriterLease, withStoreWriterLeaseAsync } from "./store.js";
18
+ import { appendEvent, assertStoreWriterCompatible, defaultStoreDir, ensureDir, ensureStoreWritable, inspectStoreCompatibility, listConversations, logPathForStatePath, loadConversationById, loadState, messageEvent, pathsForConversation, pathsForConversationDir, saveState, StoreLockTimeoutError, statePathForConversationId, withStoreWriterLease, withStoreWriterLeaseAsync } from "./store.js";
19
19
  import { createManagedSessionId, createNativeThreadTransitionId, isExactNativeThreadId, managedSessionBindingToken, nativeThreadCommandFingerprint, terminalBindingFrom, unmanagedTerminalBindingToken } from "./managed-session.js";
20
20
  import { classifyCodexLifecyclePostcondition, evaluateResumeCandidateAvailability, hasStrongCodexLifecycleIdentity, isFreshCodexPostProbeScreen } from "./native-thread-lifecycle-policy.js";
21
21
  import { listManagedSessions, loadManagedSession, loadNativeThreadTransition, saveManagedSession, saveNativeThreadTransition, tryLoadManagedSession } from "./session-store.js";
@@ -125,6 +125,13 @@ const STORE_MUTATION_COMMANDS = new Set([
125
125
  "clear-thread",
126
126
  "resume-thread"
127
127
  ]);
128
+ class TurnBindingSupersededError extends Error {
129
+ code = "AKK_TURN_BINDING_SUPERSEDED";
130
+ constructor(message) {
131
+ super(message);
132
+ this.name = "TurnBindingSupersededError";
133
+ }
134
+ }
128
135
  class InlineCodexSessionAdapter {
129
136
  threads;
130
137
  processes;
@@ -711,6 +718,16 @@ function createRuntimeTerminalAgentRegistry(options) {
711
718
  if (!isRecord(conversation)) {
712
719
  return undefined;
713
720
  }
721
+ const exactCompletion = detectExactBoundCodexCompletion({
722
+ conversation,
723
+ nativeTakeover,
724
+ request,
725
+ runtime,
726
+ options
727
+ });
728
+ if (exactCompletion.handled) {
729
+ return exactCompletion.completion;
730
+ }
714
731
  const contextMatches = await loadCodexTerminalContexts({
715
732
  nativeTakeover,
716
733
  options
@@ -766,6 +783,89 @@ function createRuntimeTerminalAgentRegistry(options) {
766
783
  ]
767
784
  });
768
785
  }
786
+ function detectExactBoundCodexCompletion({ conversation, nativeTakeover, request, runtime, options }) {
787
+ const submission = terminalBridgeSubmission(conversation);
788
+ const acceptanceEvidence = isRecord(submission?.acceptance_evidence)
789
+ ? submission.acceptance_evidence
790
+ : undefined;
791
+ const anchor = isRecord(nativeTakeover?.codex_rollout_acceptance_anchor)
792
+ ? nativeTakeover.codex_rollout_acceptance_anchor
793
+ : undefined;
794
+ if (submission?.status !== "agent_accepted") {
795
+ return { handled: false };
796
+ }
797
+ const exactRequired = requiresExactBoundCodexCompletion(conversation, options);
798
+ if (!exactRequired) {
799
+ return { handled: false };
800
+ }
801
+ if (acceptanceEvidence?.source !== "codex_rollout") {
802
+ throw new Error("[codex_exact_bound_rollout:invalid_acceptance_evidence] " +
803
+ "the accepted modern Codex Turn has no exact rollout acceptance evidence");
804
+ }
805
+ if (!anchor) {
806
+ throw new Error("[codex_exact_bound_rollout:invalid_anchor] " +
807
+ "the accepted modern Codex Turn has no exact rollout byte anchor");
808
+ }
809
+ const nativeRollout = isRecord(runtime?.nativeRollout)
810
+ ? runtime.nativeRollout
811
+ : undefined;
812
+ const result = detectCodexBoundRolloutCompletion({
813
+ anchor: anchor,
814
+ acceptanceEvidence: acceptanceEvidence,
815
+ currentIdentity: {
816
+ sessionId: stringValue(runtime?.nativeSessionId) ??
817
+ stringValue(runtime?.sessionId) ??
818
+ "",
819
+ processUuid: stringValue(runtime?.nativeProcessUuid),
820
+ processBirth: stringValue(runtime?.nativeProcessBirth),
821
+ ...(nativeRollout
822
+ ? {
823
+ rollout: {
824
+ fd: String(nativeRollout.fd ?? ""),
825
+ device: String(nativeRollout.device ?? ""),
826
+ inode: String(nativeRollout.inode ?? ""),
827
+ path: String(nativeRollout.path ?? "")
828
+ }
829
+ }
830
+ : {})
831
+ },
832
+ requestHash: stringValue(request.requestHash) ??
833
+ stringValue(nativeTakeover?.terminal_bridge_request_hash) ??
834
+ ""
835
+ });
836
+ if (result.status === "failure") {
837
+ throw new Error(`[codex_exact_bound_rollout:${result.diagnostics.code}] ${result.diagnostics.detail ?? "the exact bound rollout is not safely inspectable"}`);
838
+ }
839
+ if (result.status === "pending") {
840
+ return { handled: true };
841
+ }
842
+ return {
843
+ handled: true,
844
+ completion: {
845
+ ...result.completion,
846
+ metadata: {
847
+ ...result.completion.metadata,
848
+ context_match: "exact_bound_rollout",
849
+ detector_code: result.diagnostics.code
850
+ }
851
+ }
852
+ };
853
+ }
854
+ function requiresExactBoundCodexCompletion(conversation, options) {
855
+ const nativeTakeover = isRecord(conversation.native_session_takeover)
856
+ ? conversation.native_session_takeover
857
+ : undefined;
858
+ const submission = terminalBridgeSubmission(conversation);
859
+ if (submission?.status !== "agent_accepted") {
860
+ return false;
861
+ }
862
+ const hasExactArtifacts = isRecord(nativeTakeover?.codex_rollout_acceptance_anchor) &&
863
+ isRecord(submission.acceptance_evidence) &&
864
+ submission.acceptance_evidence.source === "codex_rollout";
865
+ const modernProductionTurn = Number(nativeTakeover?.terminal_agent_identity_protocol) === 1 &&
866
+ !allowsSyntheticTerminalAcceptance(options);
867
+ return hasExactArtifacts || modernProductionTurn;
868
+ }
769
869
  function createTerminalAgentBridge(options, terminalProvider = createTerminalControlProvider(options), registry = createRuntimeTerminalAgentRegistry(options)) {
770
870
  const processSource = createTerminalProcessSource(options);
771
871
  return new TerminalAgentBridge({
@@ -1566,6 +1666,7 @@ function withTerminalBridgeState({ conversation, message, requestText, startedAt
1566
1666
  claude_home: claudeHome,
1567
1667
  terminal_bridge_completion_claim: undefined,
1568
1668
  terminal_bridge_approval_dispatch: undefined,
1669
+ terminal_bridge_detector_diagnostic: undefined,
1569
1670
  terminal_bridge_monitor_lock_version: TERMINAL_BRIDGE_MONITOR_LOCK_VERSION,
1570
1671
  terminal_bridge_monitor_started_at: startedAt,
1571
1672
  terminal_bridge_last_activity_at: startedAt,
@@ -9860,7 +9961,7 @@ function assertTurnBindingCurrent(conversation, operation) {
9860
9961
  turnNativeThreadId
9861
9962
  }));
9862
9963
  if (!exactModernBinding && !compatibleMigratedBinding) {
9863
- throw new Error(`cannot ${operation} Turn ${turnIdForConversation(conversation)}: its ` +
9964
+ throw new TurnBindingSupersededError(`cannot ${operation} Turn ${turnIdForConversation(conversation)}: its ` +
9864
9965
  `Session binding generation is no longer current`);
9865
9966
  }
9866
9967
  }
@@ -10429,9 +10530,11 @@ async function runRenew(options) {
10429
10530
  });
10430
10531
  }
10431
10532
  async function runReconcileMonitors(options) {
10533
+ const reason = stringValue(options.reason) ?? "startup_reconciliation";
10432
10534
  printJson(await reconcileMonitors(options, {
10433
- includeCallbackRecovery: true,
10434
- reason: "startup_reconciliation",
10535
+ includeCallbackRecovery: options.terminalMonitorsOnly !== true &&
10536
+ reason !== "monitor_supervision",
10537
+ reason,
10435
10538
  conversationId: undefined
10436
10539
  }));
10437
10540
  }
@@ -10517,6 +10620,9 @@ async function reconcileMonitors(options, { includeCallbackRecovery, reason, con
10517
10620
  });
10518
10621
  continue;
10519
10622
  }
10623
+ const previousMonitorPid = latestTerminalBridgeMonitorLaunchPid(logPath);
10624
+ const unexpectedMonitorExit = previousMonitorPid !== undefined &&
10625
+ !isProcessAlive(previousMonitorPid);
10520
10626
  const activeOwner = activeTerminalBridgeMonitorOwner(statePath, initialEligibility.terminalMessageId);
10521
10627
  if (activeOwner) {
10522
10628
  alreadyRunning += 1;
@@ -10601,13 +10707,33 @@ async function reconcileMonitors(options, { includeCallbackRecovery, reason, con
10601
10707
  continue;
10602
10708
  }
10603
10709
  const launchedAt = new Date().toISOString();
10710
+ const launchReason = unexpectedMonitorExit
10711
+ ? "unexpected_exit_recovery"
10712
+ : reason;
10713
+ if (unexpectedMonitorExit) {
10714
+ appendEvent(logPath, {
10715
+ ts: launchedAt,
10716
+ conversation_id: prepared.conversation.conversation_id,
10717
+ event: "terminal_bridge_monitor_exit_observed",
10718
+ previous_monitor_pid: previousMonitorPid,
10719
+ terminal_control: prepared.terminalControl,
10720
+ reason: "monitor_owner_process_missing",
10721
+ observed_by: reason
10722
+ });
10723
+ runtimeLog("warn", "terminal_bridge_monitor_exit_observed", {
10724
+ conversation_id: prepared.conversation.conversation_id,
10725
+ previous_monitor_pid: previousMonitorPid,
10726
+ terminal_target: prepared.terminalControl.target,
10727
+ observed_by: reason
10728
+ });
10729
+ }
10604
10730
  appendEvent(logPath, {
10605
10731
  ts: launchedAt,
10606
10732
  conversation_id: prepared.conversation.conversation_id,
10607
10733
  event: "terminal_bridge_monitor_launch",
10608
10734
  pid: monitor.pid ?? null,
10609
10735
  terminal_control: prepared.terminalControl,
10610
- reason,
10736
+ reason: launchReason,
10611
10737
  agent_timeout_minutes: prepared.inactivityTimeoutMinutes,
10612
10738
  agent_hard_timeout_minutes: prepared.hardTimeoutMinutes
10613
10739
  });
@@ -10620,11 +10746,23 @@ async function reconcileMonitors(options, { includeCallbackRecovery, reason, con
10620
10746
  items.push({
10621
10747
  conversation_id: prepared.conversation.conversation_id,
10622
10748
  status: "launched",
10623
- reason,
10624
- monitor_pid: monitor.pid ?? null
10749
+ reason: launchReason,
10750
+ monitor_pid: monitor.pid ?? null,
10751
+ ...(unexpectedMonitorExit
10752
+ ? { previous_monitor_pid: previousMonitorPid }
10753
+ : {})
10625
10754
  });
10626
10755
  }
10627
10756
  catch (error) {
10757
+ if (error instanceof TurnBindingSupersededError) {
10758
+ skipped += 1;
10759
+ items.push({
10760
+ conversation_id: listedConversation.conversation_id,
10761
+ status: "skipped",
10762
+ reason: "session_binding_superseded"
10763
+ });
10764
+ continue;
10765
+ }
10628
10766
  errors += 1;
10629
10767
  items.push({
10630
10768
  conversation_id: listedConversation.conversation_id,
@@ -10854,8 +10992,11 @@ function latestTerminalBridgeMonitorLaunchPid(logPath) {
10854
10992
  catch {
10855
10993
  return undefined;
10856
10994
  }
10857
- const launch = [...events].reverse().find((event) => event.event === "terminal_bridge_monitor_launch");
10858
- const pid = Number(launch?.pid);
10995
+ const ownership = [...events].reverse().find((event) => event.event === "terminal_bridge_monitor_launch" ||
10996
+ event.event === "terminal_bridge_monitor_started");
10997
+ const pid = Number(ownership?.event === "terminal_bridge_monitor_started"
10998
+ ? ownership.monitor_pid
10999
+ : ownership?.pid);
10859
11000
  return Number.isSafeInteger(pid) && pid > 1 ? pid : undefined;
10860
11001
  }
10861
11002
  function prepareTerminalBridgeMonitorReconciliation({ statePath, expectedMessageId, requireWaitingForAgentStatus = false }) {
@@ -11578,6 +11719,7 @@ function runTerminalBridgeMonitorHandoff(options) {
11578
11719
  }
11579
11720
  async function runTerminalBridgeMonitor(options) {
11580
11721
  const statePath = expandHome(required(options.state, "--state is required"));
11722
+ const logPath = expandHome(options.log ?? logPathForStatePath(statePath));
11581
11723
  const conversation = loadState(statePath);
11582
11724
  const nativeTakeover = isRecord(conversation.native_session_takeover)
11583
11725
  ? conversation.native_session_takeover
@@ -11600,14 +11742,81 @@ async function runTerminalBridgeMonitor(options) {
11600
11742
  });
11601
11743
  return;
11602
11744
  }
11745
+ const lifecycle = { startedRecorded: false };
11746
+ let storeDeferredAttempts = 0;
11747
+ let storeFirstDeferredAt;
11603
11748
  try {
11604
- await runTerminalBridgeMonitorWithLock(options);
11749
+ while (true) {
11750
+ try {
11751
+ if (storeDeferredAttempts > 0) {
11752
+ const resumedAt = new Date().toISOString();
11753
+ const resumedConversation = loadState(statePath);
11754
+ const resumedTakeover = isRecord(resumedConversation.native_session_takeover)
11755
+ ? resumedConversation.native_session_takeover
11756
+ : undefined;
11757
+ if (stringValue(resumedTakeover?.terminal_bridge_message_id) !==
11758
+ terminalMessageId) {
11759
+ runtimeLog("info", "terminal_bridge_monitor_finished", {
11760
+ conversation_id: resumedConversation.conversation_id,
11761
+ terminal_bridge_message_id: terminalMessageId,
11762
+ reason: "terminal_bridge_generation_replaced_during_store_deferral"
11763
+ });
11764
+ printJson({
11765
+ conversation: resumedConversation,
11766
+ monitored: true,
11767
+ terminal_bridge: true,
11768
+ completed: false,
11769
+ reason: "terminal_bridge_generation_replaced"
11770
+ });
11771
+ return;
11772
+ }
11773
+ appendEvent(logPath, {
11774
+ ts: resumedAt,
11775
+ conversation_id: resumedConversation.conversation_id,
11776
+ event: "terminal_bridge_monitor_store_operation_deferred",
11777
+ terminal_bridge_message_id: terminalMessageId,
11778
+ error_code: "AKK_STORE_LOCK_TIMEOUT",
11779
+ first_deferred_at: storeFirstDeferredAt,
11780
+ resumed_at: resumedAt,
11781
+ attempts: storeDeferredAttempts,
11782
+ outcome: "resumed"
11783
+ });
11784
+ runtimeLog("info", "terminal_bridge_monitor_store_operation_resumed", {
11785
+ conversation_id: resumedConversation.conversation_id,
11786
+ terminal_bridge_message_id: terminalMessageId,
11787
+ attempts: storeDeferredAttempts,
11788
+ first_deferred_at: storeFirstDeferredAt
11789
+ });
11790
+ storeDeferredAttempts = 0;
11791
+ storeFirstDeferredAt = undefined;
11792
+ }
11793
+ await runTerminalBridgeMonitorWithLock(options, lifecycle, terminalMessageId);
11794
+ return;
11795
+ }
11796
+ catch (error) {
11797
+ if (!(error instanceof StoreLockTimeoutError)) {
11798
+ throw error;
11799
+ }
11800
+ storeDeferredAttempts += 1;
11801
+ storeFirstDeferredAt ??= new Date().toISOString();
11802
+ const retryInMs = Math.min(5_000, 250 * (2 ** Math.min(5, storeDeferredAttempts - 1)));
11803
+ runtimeLog("warn", "terminal_bridge_monitor_store_operation_deferred", {
11804
+ conversation_id: conversation.conversation_id,
11805
+ terminal_bridge_message_id: terminalMessageId,
11806
+ error_code: error.code,
11807
+ lock_kind: error.lockKind,
11808
+ attempt: storeDeferredAttempts,
11809
+ retry_in_ms: retryInMs
11810
+ });
11811
+ sleepSync(retryInMs);
11812
+ }
11813
+ }
11605
11814
  }
11606
11815
  finally {
11607
11816
  monitorLock.release();
11608
11817
  }
11609
11818
  }
11610
- async function runTerminalBridgeMonitorWithLock(options) {
11819
+ async function runTerminalBridgeMonitorWithLock(options, lifecycle, expectedTerminalMessageId) {
11611
11820
  const statePath = expandHome(required(options.state, "--state is required"));
11612
11821
  const logPath = expandHome(options.log ?? logPathForStatePath(statePath));
11613
11822
  const pollIntervalMs = Math.max(50, Number(options.pollIntervalMs ?? DEFAULT_MONITOR_POLL_INTERVAL_MS));
@@ -11620,6 +11829,22 @@ async function runTerminalBridgeMonitorWithLock(options) {
11620
11829
  const initialNativeTakeover = isRecord(conversation.native_session_takeover)
11621
11830
  ? conversation.native_session_takeover
11622
11831
  : undefined;
11832
+ if (stringValue(initialNativeTakeover?.terminal_bridge_message_id) !==
11833
+ expectedTerminalMessageId) {
11834
+ runtimeLog("info", "terminal_bridge_monitor_finished", {
11835
+ conversation_id: conversation.conversation_id,
11836
+ terminal_bridge_message_id: expectedTerminalMessageId,
11837
+ reason: "terminal_bridge_generation_replaced_before_monitor_restart"
11838
+ });
11839
+ printJson({
11840
+ conversation,
11841
+ monitored: true,
11842
+ terminal_bridge: true,
11843
+ completed: false,
11844
+ reason: "terminal_bridge_generation_replaced"
11845
+ });
11846
+ return;
11847
+ }
11623
11848
  const timeoutMinutes = Number(options.agentTimeoutMinutes ??
11624
11849
  initialNativeTakeover?.["terminal_bridge_inactivity_timeout_minutes"] ??
11625
11850
  DEFAULT_AGENT_TIMEOUT_MINUTES);
@@ -11627,7 +11852,7 @@ async function runTerminalBridgeMonitorWithLock(options) {
11627
11852
  initialNativeTakeover?.["terminal_bridge_hard_timeout_minutes"] ??
11628
11853
  DEFAULT_AGENT_HARD_TIMEOUT_MINUTES, "--agent-hard-timeout-minutes");
11629
11854
  const monitorStartedAtMs = Date.now();
11630
- const monitorMessageId = stringValue(initialNativeTakeover?.["terminal_bridge_message_id"]);
11855
+ const monitorMessageId = expectedTerminalMessageId;
11631
11856
  const taskStartedAtMs = validTimestampMs(initialNativeTakeover?.["terminal_bridge_started_at"]) ?? monitorStartedAtMs;
11632
11857
  let lastActivityAtMs = validTimestampMs(initialNativeTakeover?.["terminal_bridge_last_activity_at"]) ?? taskStartedAtMs;
11633
11858
  let lastPersistedActivityAtMs = lastActivityAtMs;
@@ -11636,33 +11861,45 @@ async function runTerminalBridgeMonitorWithLock(options) {
11636
11861
  let previousScreenFingerprint = preSendScreenFingerprint;
11637
11862
  let previousDurableFingerprint;
11638
11863
  let persistedActivityReason = stringValue(initialNativeTakeover?.["terminal_bridge_last_activity_reason"]);
11864
+ const initialDetectorDiagnostic = isRecord(initialNativeTakeover?.["terminal_bridge_detector_diagnostic"])
11865
+ ? initialNativeTakeover.terminal_bridge_detector_diagnostic
11866
+ : undefined;
11867
+ let persistedDetectorDiagnosticFingerprint = stringValue(initialDetectorDiagnostic?.fingerprint);
11868
+ let persistedDetectorDiagnosticStatus = stringValue(initialDetectorDiagnostic?.status);
11639
11869
  const executor = executorForConversation(conversation);
11640
11870
  const terminalBridge = createTerminalAgentBridge(options);
11641
- appendEvent(logPath, {
11642
- ts: new Date().toISOString(),
11643
- conversation_id: conversation.conversation_id,
11644
- event: "terminal_bridge_monitor_started",
11645
- executor,
11646
- agent_timeout_minutes: timeoutMinutes,
11647
- agent_hard_timeout_minutes: hardTimeoutMinutes,
11648
- poll_interval_ms: pollIntervalMs,
11649
- task_started_at: new Date(taskStartedAtMs).toISOString(),
11650
- last_activity_at: new Date(lastActivityAtMs).toISOString(),
11651
- inactivity_deadline_at: timeoutMinutes > 0
11652
- ? new Date(lastActivityAtMs + timeoutMinutes * 60 * 1000).toISOString()
11653
- : null,
11654
- hard_deadline_at: hardTimeoutMinutes > 0
11655
- ? new Date(taskStartedAtMs + hardTimeoutMinutes * 60 * 1000).toISOString()
11656
- : null
11657
- });
11658
- runtimeLog("info", "terminal_bridge_monitor_started", {
11659
- conversation_id: conversation.conversation_id,
11660
- agent: executor.kind,
11661
- executor_session: executor.session,
11662
- agent_timeout_minutes: timeoutMinutes,
11663
- agent_hard_timeout_minutes: hardTimeoutMinutes
11664
- });
11871
+ if (!lifecycle.startedRecorded) {
11872
+ appendEvent(logPath, {
11873
+ ts: new Date().toISOString(),
11874
+ conversation_id: conversation.conversation_id,
11875
+ event: "terminal_bridge_monitor_started",
11876
+ monitor_pid: process.pid,
11877
+ executor,
11878
+ agent_timeout_minutes: timeoutMinutes,
11879
+ agent_hard_timeout_minutes: hardTimeoutMinutes,
11880
+ poll_interval_ms: pollIntervalMs,
11881
+ task_started_at: new Date(taskStartedAtMs).toISOString(),
11882
+ last_activity_at: new Date(lastActivityAtMs).toISOString(),
11883
+ inactivity_deadline_at: timeoutMinutes > 0
11884
+ ? new Date(lastActivityAtMs + timeoutMinutes * 60 * 1000).toISOString()
11885
+ : null,
11886
+ hard_deadline_at: hardTimeoutMinutes > 0
11887
+ ? new Date(taskStartedAtMs + hardTimeoutMinutes * 60 * 1000).toISOString()
11888
+ : null
11889
+ });
11890
+ runtimeLog("info", "terminal_bridge_monitor_started", {
11891
+ conversation_id: conversation.conversation_id,
11892
+ monitor_pid: process.pid,
11893
+ agent: executor.kind,
11894
+ executor_session: executor.session,
11895
+ agent_timeout_minutes: timeoutMinutes,
11896
+ agent_hard_timeout_minutes: hardTimeoutMinutes
11897
+ });
11898
+ lifecycle.startedRecorded = true;
11899
+ }
11665
11900
  let idleCompletionFingerprint;
11901
+ let bindingCheckDeferredAttempts = 0;
11902
+ let bindingCheckFirstDeferredAt;
11666
11903
  while (true) {
11667
11904
  conversation = loadState(statePath);
11668
11905
  if (!isWaitingForAgent(conversation.status)) {
@@ -11995,11 +12232,55 @@ async function runTerminalBridgeMonitorWithLock(options) {
11995
12232
  }
11996
12233
  catch (error) {
11997
12234
  const reason = error instanceof Error ? error.message : String(error);
12235
+ if (error instanceof StoreLockTimeoutError) {
12236
+ bindingCheckDeferredAttempts += 1;
12237
+ bindingCheckFirstDeferredAt ??= new Date().toISOString();
12238
+ const backoffMs = Math.min(5_000, 250 * (2 ** Math.min(5, bindingCheckDeferredAttempts - 1)));
12239
+ runtimeLog("warn", "terminal_bridge_monitor_binding_check_deferred", {
12240
+ conversation_id: conversation.conversation_id,
12241
+ terminal_target: terminalControl.target,
12242
+ error_code: error.code,
12243
+ lock_kind: error.lockKind,
12244
+ attempt: bindingCheckDeferredAttempts,
12245
+ retry_in_ms: backoffMs
12246
+ });
12247
+ sleepSync(backoffMs);
12248
+ continue;
12249
+ }
12250
+ if (!(error instanceof TurnBindingSupersededError)) {
12251
+ runtimeLog("error", "terminal_bridge_monitor_binding_check_failed", {
12252
+ conversation_id: conversation.conversation_id,
12253
+ terminal_target: terminalControl.target,
12254
+ error_code: isRecord(error) ? stringValue(error.code) : undefined,
12255
+ reason
12256
+ });
12257
+ throw error;
12258
+ }
11998
12259
  runtimeLog("warn", "terminal_bridge_monitor_binding_superseded", {
11999
12260
  conversation_id: conversation.conversation_id,
12000
12261
  terminal_target: terminalControl.target,
12001
12262
  reason
12002
12263
  });
12264
+ try {
12265
+ appendEvent(logPath, {
12266
+ ts: new Date().toISOString(),
12267
+ conversation_id: conversation.conversation_id,
12268
+ event: "terminal_bridge_monitor_binding_superseded",
12269
+ terminal_control: terminalControl,
12270
+ error_code: error.code,
12271
+ reason
12272
+ });
12273
+ }
12274
+ catch (diagnosticError) {
12275
+ runtimeLog("warn", "terminal_bridge_monitor_diagnostic_write_failed", {
12276
+ conversation_id: conversation.conversation_id,
12277
+ terminal_target: terminalControl.target,
12278
+ diagnostic_event: "terminal_bridge_monitor_binding_superseded",
12279
+ reason: diagnosticError instanceof Error
12280
+ ? diagnosticError.message
12281
+ : String(diagnosticError)
12282
+ });
12283
+ }
12003
12284
  printJson({
12004
12285
  conversation,
12005
12286
  monitored: true,
@@ -12010,6 +12291,44 @@ async function runTerminalBridgeMonitorWithLock(options) {
12010
12291
  });
12011
12292
  return;
12012
12293
  }
12294
+ if (bindingCheckDeferredAttempts > 0) {
12295
+ const resumedAt = new Date().toISOString();
12296
+ try {
12297
+ appendEvent(logPath, {
12298
+ ts: resumedAt,
12299
+ conversation_id: conversation.conversation_id,
12300
+ event: "terminal_bridge_monitor_binding_check_deferred",
12301
+ terminal_control: terminalControl,
12302
+ error_code: "AKK_STORE_LOCK_TIMEOUT",
12303
+ lock_kind: "writer",
12304
+ first_deferred_at: bindingCheckFirstDeferredAt,
12305
+ resumed_at: resumedAt,
12306
+ attempts: bindingCheckDeferredAttempts,
12307
+ outcome: "resumed"
12308
+ });
12309
+ runtimeLog("info", "terminal_bridge_monitor_binding_check_resumed", {
12310
+ conversation_id: conversation.conversation_id,
12311
+ terminal_target: terminalControl.target,
12312
+ attempts: bindingCheckDeferredAttempts,
12313
+ first_deferred_at: bindingCheckFirstDeferredAt
12314
+ });
12315
+ bindingCheckDeferredAttempts = 0;
12316
+ bindingCheckFirstDeferredAt = undefined;
12317
+ }
12318
+ catch (error) {
12319
+ if (!(error instanceof StoreLockTimeoutError)) {
12320
+ throw error;
12321
+ }
12322
+ runtimeLog("warn", "terminal_bridge_monitor_diagnostic_write_deferred", {
12323
+ conversation_id: conversation.conversation_id,
12324
+ terminal_target: terminalControl.target,
12325
+ error_code: error.code,
12326
+ lock_kind: error.lockKind
12327
+ });
12328
+ sleepSync(Math.max(250, pollIntervalMs));
12329
+ continue;
12330
+ }
12331
+ }
12013
12332
  const screenChangedSinceSend = preSendScreenFingerprint !== undefined &&
12014
12333
  previousScreenFingerprint !== undefined &&
12015
12334
  previousScreenFingerprint !== preSendScreenFingerprint;
@@ -12107,6 +12426,47 @@ async function runTerminalBridgeMonitorWithLock(options) {
12107
12426
  releaseTerminalPollLock();
12108
12427
  }
12109
12428
  const terminalStatus = poll.status;
12429
+ const detectorLimitation = stringValue(terminalStatus.capability_limitation);
12430
+ const detectorDiagnosticFingerprint = detectorLimitation
12431
+ ? terminalBridgeActivityFingerprint(detectorLimitation)
12432
+ : undefined;
12433
+ const detectorDiagnosticChanged = detectorLimitation
12434
+ ? detectorDiagnosticFingerprint !== persistedDetectorDiagnosticFingerprint ||
12435
+ persistedDetectorDiagnosticStatus !== "limited"
12436
+ : persistedDetectorDiagnosticStatus === "limited";
12437
+ if (detectorDiagnosticChanged) {
12438
+ try {
12439
+ const diagnostic = persistTerminalBridgeDetectorDiagnostic({
12440
+ statePath,
12441
+ logPath,
12442
+ expectedConversationId: conversation.conversation_id,
12443
+ expectedMessageId: currentMessageId,
12444
+ limitation: detectorLimitation,
12445
+ fingerprint: detectorDiagnosticFingerprint
12446
+ });
12447
+ if (diagnostic.diagnostic) {
12448
+ conversation = diagnostic.conversation;
12449
+ nativeTakeover = isRecord(conversation.native_session_takeover)
12450
+ ? conversation.native_session_takeover
12451
+ : undefined;
12452
+ persistedDetectorDiagnosticFingerprint = stringValue(diagnostic.diagnostic?.fingerprint);
12453
+ persistedDetectorDiagnosticStatus = stringValue(diagnostic.diagnostic?.status);
12454
+ }
12455
+ }
12456
+ catch (error) {
12457
+ if (!(error instanceof StoreLockTimeoutError)) {
12458
+ throw error;
12459
+ }
12460
+ runtimeLog("warn", "terminal_bridge_detector_diagnostic_deferred", {
12461
+ conversation_id: conversation.conversation_id,
12462
+ terminal_target: terminalControl.target,
12463
+ error_code: error.code,
12464
+ lock_kind: error.lockKind
12465
+ });
12466
+ sleepSync(Math.max(250, pollIntervalMs));
12467
+ continue;
12468
+ }
12469
+ }
12110
12470
  const approval = terminalStatus.approval_state;
12111
12471
  const currentScreenFingerprint = stringValue(terminalStatus?.screen?.digest) ??
12112
12472
  terminalBridgeScreenFingerprint(terminalStatus?.screen?.excerpt);
@@ -14986,6 +15346,101 @@ function persistTerminalBridgeActivity({ conversation, statePath, logPath, obser
14986
15346
  releaseLock();
14987
15347
  }
14988
15348
  }
15349
+ function persistTerminalBridgeDetectorDiagnostic({ statePath, logPath, expectedConversationId, expectedMessageId, limitation, fingerprint }) {
15350
+ const storeDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
15351
+ const releaseLock = acquireFileLock(`${statePath}.lock`);
15352
+ try {
15353
+ return withStoreWriterLease(storeDir, () => {
15354
+ const conversation = loadState(statePath);
15355
+ const nativeTakeover = isRecord(conversation.native_session_takeover)
15356
+ ? conversation.native_session_takeover
15357
+ : {};
15358
+ if (conversation.conversation_id !== expectedConversationId ||
15359
+ stringValue(nativeTakeover.terminal_bridge_message_id) !==
15360
+ expectedMessageId) {
15361
+ return {
15362
+ persisted: false,
15363
+ conversation,
15364
+ reason: "terminal_bridge_task_replaced"
15365
+ };
15366
+ }
15367
+ const existing = isRecord(nativeTakeover.terminal_bridge_detector_diagnostic)
15368
+ ? nativeTakeover.terminal_bridge_detector_diagnostic
15369
+ : undefined;
15370
+ const now = new Date().toISOString();
15371
+ const nextDiagnostic = limitation && fingerprint
15372
+ ? {
15373
+ status: "limited",
15374
+ source: "terminal_completion_detector",
15375
+ fingerprint,
15376
+ detail: truncateText(redactString(limitation), 1000),
15377
+ observed_at: now
15378
+ }
15379
+ : existing && stringValue(existing.status) === "limited"
15380
+ ? {
15381
+ ...existing,
15382
+ status: "recovered",
15383
+ recovered_at: now
15384
+ }
15385
+ : undefined;
15386
+ if (!nextDiagnostic) {
15387
+ return {
15388
+ persisted: false,
15389
+ conversation,
15390
+ diagnostic: existing,
15391
+ reason: "detector_diagnostic_unchanged"
15392
+ };
15393
+ }
15394
+ if (stringValue(existing?.status) === stringValue(nextDiagnostic.status) &&
15395
+ stringValue(existing?.fingerprint) ===
15396
+ stringValue(nextDiagnostic.fingerprint)) {
15397
+ return {
15398
+ persisted: false,
15399
+ conversation,
15400
+ diagnostic: existing,
15401
+ reason: "detector_diagnostic_unchanged"
15402
+ };
15403
+ }
15404
+ const nextConversation = {
15405
+ ...conversation,
15406
+ native_session_takeover: {
15407
+ ...nativeTakeover,
15408
+ terminal_bridge_detector_diagnostic: nextDiagnostic
15409
+ },
15410
+ updated_at: now
15411
+ };
15412
+ saveState(statePath, nextConversation);
15413
+ const event = nextDiagnostic.status === "limited"
15414
+ ? "terminal_bridge_completion_detector_limited"
15415
+ : "terminal_bridge_completion_detector_recovered";
15416
+ appendEvent(logPath, {
15417
+ ts: now,
15418
+ conversation_id: conversation.conversation_id,
15419
+ event,
15420
+ terminal_bridge_message_id: expectedMessageId,
15421
+ detector_source: nextDiagnostic.source,
15422
+ diagnostic_fingerprint: nextDiagnostic.fingerprint,
15423
+ detail: nextDiagnostic.status === "limited"
15424
+ ? nextDiagnostic.detail
15425
+ : undefined
15426
+ });
15427
+ runtimeLog(nextDiagnostic.status === "limited" ? "warn" : "info", event, {
15428
+ conversation_id: conversation.conversation_id,
15429
+ terminal_bridge_message_id: expectedMessageId,
15430
+ detector_source: nextDiagnostic.source,
15431
+ diagnostic_fingerprint: nextDiagnostic.fingerprint
15432
+ });
15433
+ return {
15434
+ persisted: true,
15435
+ conversation: nextConversation,
15436
+ diagnostic: nextDiagnostic
15437
+ };
15438
+ });
15439
+ }
15440
+ finally {
15441
+ releaseLock();
15442
+ }
15443
+ }
14989
15444
  function terminalBridgeApprovalCandidate({ executor, terminalControl, terminalStatus, fingerprint }) {
14990
15445
  const approval = isRecord(terminalStatus?.approval_state) ? terminalStatus.approval_state : {};
14991
15446
  if (approval.approvable !== true) {