@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/CHANGELOG.md +12 -0
- package/README.md +12 -17
- package/dist/src/cli.js +492 -37
- package/dist/src/cli.js.map +1 -1
- package/dist/src/openclaw-plugin.d.ts +3 -1
- package/dist/src/openclaw-plugin.js +85 -22
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/dist/src/store.d.ts +6 -0
- package/dist/src/store.js +15 -1
- package/dist/src/store.js.map +1 -1
- package/dist/src/terminal-submission-acceptance.d.ts +40 -0
- package/dist/src/terminal-submission-acceptance.js +331 -0
- package/dist/src/terminal-submission-acceptance.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
|
10858
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
11642
|
-
|
|
11643
|
-
|
|
11644
|
-
|
|
11645
|
-
|
|
11646
|
-
|
|
11647
|
-
|
|
11648
|
-
|
|
11649
|
-
|
|
11650
|
-
|
|
11651
|
-
|
|
11652
|
-
|
|
11653
|
-
:
|
|
11654
|
-
|
|
11655
|
-
|
|
11656
|
-
:
|
|
11657
|
-
|
|
11658
|
-
|
|
11659
|
-
|
|
11660
|
-
|
|
11661
|
-
|
|
11662
|
-
|
|
11663
|
-
|
|
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) {
|