@scotthuang/agent-knock-knock 0.2.42 → 0.2.43
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 +13 -0
- package/README.md +9 -2
- package/dist/src/cli.js +652 -46
- package/dist/src/cli.js.map +1 -1
- package/dist/src/codex-local-session-provider.js +2 -0
- package/dist/src/codex-local-session-provider.js.map +1 -1
- package/dist/src/codex-session-provider.d.ts +11 -0
- package/dist/src/codex-session-provider.js +52 -0
- package/dist/src/codex-session-provider.js.map +1 -1
- package/dist/src/openclaw-plugin.js +70 -2
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/openclaw.plugin.json +14 -1
- package/package.json +1 -1
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +2 -0
package/dist/src/cli.js
CHANGED
|
@@ -17,6 +17,7 @@ import { planFork, planTakeover } from "./session-takeover-planner.js";
|
|
|
17
17
|
import { StaticTerminalControlProvider, TmuxTerminalControlProvider, enrichActiveProcessesWithTerminalControl, terminalRefFromPane } from "./terminal-control-provider.js";
|
|
18
18
|
const DEFAULT_IDLE_TIMEOUT_MINUTES = 10080;
|
|
19
19
|
const DEFAULT_AGENT_TIMEOUT_MINUTES = 60;
|
|
20
|
+
const DEFAULT_AGENT_HARD_TIMEOUT_MINUTES = 720;
|
|
20
21
|
const DEFAULT_MONITOR_POLL_INTERVAL_MS = 5000;
|
|
21
22
|
const DEFAULT_CODEX_ACPX_AGENT_COMMAND = "npx -y @agentclientprotocol/codex-acp@^1.1.0";
|
|
22
23
|
class InlineCodexSessionAdapter {
|
|
@@ -105,6 +106,9 @@ async function runCommand(commandName, options) {
|
|
|
105
106
|
else if (commandName === "cancel") {
|
|
106
107
|
await runCancel(options);
|
|
107
108
|
}
|
|
109
|
+
else if (commandName === "renew") {
|
|
110
|
+
await runRenew(options);
|
|
111
|
+
}
|
|
108
112
|
else if (commandName === "recover") {
|
|
109
113
|
runRecover(options);
|
|
110
114
|
}
|
|
@@ -772,13 +776,13 @@ function isCodexWorkingLine(line) {
|
|
|
772
776
|
if (!trimmed) {
|
|
773
777
|
return false;
|
|
774
778
|
}
|
|
775
|
-
if (/^•\s+Working\b/u.test(trimmed)) {
|
|
779
|
+
if (/^•\s+Working\b/u.test(trimmed) && (/\besc to interrupt\b/u.test(trimmed) || trimmed === "• Working")) {
|
|
776
780
|
return true;
|
|
777
781
|
}
|
|
778
|
-
if (
|
|
782
|
+
if (/^•\s+Waiting for background terminals?\b(?:\s*·|$)/u.test(trimmed)) {
|
|
779
783
|
return true;
|
|
780
784
|
}
|
|
781
|
-
return
|
|
785
|
+
return /^\d+\s+background terminals? running\b/u.test(trimmed) && /\/(?:ps|stop)\b/u.test(trimmed);
|
|
782
786
|
}
|
|
783
787
|
function isCodexIdlePromptLine(line) {
|
|
784
788
|
const trimmed = line.trim();
|
|
@@ -1330,7 +1334,7 @@ function startExecutorMonitor({ statePath, logPath, pid, outputPath, agentTimeou
|
|
|
1330
1334
|
child.unref();
|
|
1331
1335
|
return child;
|
|
1332
1336
|
}
|
|
1333
|
-
function startTerminalBridgeMonitor({ statePath, logPath, agentTimeoutMinutes, pollIntervalMs, codexHome }) {
|
|
1337
|
+
function startTerminalBridgeMonitor({ statePath, logPath, agentTimeoutMinutes, agentHardTimeoutMinutes, pollIntervalMs, codexHome }) {
|
|
1334
1338
|
const args = [
|
|
1335
1339
|
new URL(import.meta.url).pathname,
|
|
1336
1340
|
"monitor",
|
|
@@ -1341,6 +1345,8 @@ function startTerminalBridgeMonitor({ statePath, logPath, agentTimeoutMinutes, p
|
|
|
1341
1345
|
logPath,
|
|
1342
1346
|
"--agent-timeout-minutes",
|
|
1343
1347
|
String(agentTimeoutMinutes),
|
|
1348
|
+
"--agent-hard-timeout-minutes",
|
|
1349
|
+
String(agentHardTimeoutMinutes),
|
|
1344
1350
|
"--poll-interval-ms",
|
|
1345
1351
|
String(pollIntervalMs)
|
|
1346
1352
|
];
|
|
@@ -1360,10 +1366,18 @@ function startTerminalBridgeMonitorForConversation({ conversation, statePath, lo
|
|
|
1360
1366
|
if (!terminalBridgeEnabled(conversation) || !conversation.gateway_method || options.disableTerminalBridgeMonitor === true) {
|
|
1361
1367
|
return undefined;
|
|
1362
1368
|
}
|
|
1369
|
+
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
1370
|
+
? conversation.native_session_takeover
|
|
1371
|
+
: undefined;
|
|
1363
1372
|
return startTerminalBridgeMonitor({
|
|
1364
1373
|
statePath,
|
|
1365
1374
|
logPath,
|
|
1366
|
-
agentTimeoutMinutes: Number(options.agentTimeoutMinutes ??
|
|
1375
|
+
agentTimeoutMinutes: Number(options.agentTimeoutMinutes ??
|
|
1376
|
+
nativeTakeover?.["terminal_bridge_inactivity_timeout_minutes"] ??
|
|
1377
|
+
DEFAULT_AGENT_TIMEOUT_MINUTES),
|
|
1378
|
+
agentHardTimeoutMinutes: Number(options.agentHardTimeoutMinutes ??
|
|
1379
|
+
nativeTakeover?.["terminal_bridge_hard_timeout_minutes"] ??
|
|
1380
|
+
DEFAULT_AGENT_HARD_TIMEOUT_MINUTES),
|
|
1367
1381
|
pollIntervalMs: Number(options.monitorPollIntervalMs ?? DEFAULT_MONITOR_POLL_INTERVAL_MS),
|
|
1368
1382
|
codexHome: options.codexHome
|
|
1369
1383
|
});
|
|
@@ -1374,7 +1388,7 @@ function terminalBridgeEnabled(conversation) {
|
|
|
1374
1388
|
: undefined;
|
|
1375
1389
|
return nativeTakeover?.["terminal_bridge"] === true;
|
|
1376
1390
|
}
|
|
1377
|
-
function withTerminalBridgeState({ conversation, message, startedAt }) {
|
|
1391
|
+
function withTerminalBridgeState({ conversation, message, startedAt, agentTimeoutMinutes, agentHardTimeoutMinutes, preSendScreenFingerprint }) {
|
|
1378
1392
|
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
1379
1393
|
? conversation.native_session_takeover
|
|
1380
1394
|
: {};
|
|
@@ -1384,7 +1398,16 @@ function withTerminalBridgeState({ conversation, message, startedAt }) {
|
|
|
1384
1398
|
...nativeTakeover,
|
|
1385
1399
|
terminal_bridge: true,
|
|
1386
1400
|
terminal_bridge_started_at: startedAt,
|
|
1387
|
-
terminal_bridge_message_id: message.id
|
|
1401
|
+
terminal_bridge_message_id: message.id,
|
|
1402
|
+
terminal_bridge_request_text: String(message.body ?? ""),
|
|
1403
|
+
terminal_bridge_request_hash: terminalBridgeRequestFingerprint(message.body),
|
|
1404
|
+
terminal_bridge_pre_send_screen_fingerprint: preSendScreenFingerprint,
|
|
1405
|
+
terminal_bridge_monitor_started_at: startedAt,
|
|
1406
|
+
terminal_bridge_last_activity_at: startedAt,
|
|
1407
|
+
terminal_bridge_inactivity_timeout_minutes: agentTimeoutMinutes,
|
|
1408
|
+
terminal_bridge_hard_timeout_minutes: agentHardTimeoutMinutes,
|
|
1409
|
+
terminal_bridge_inactivity_deadline_at: deadlineAt(startedAt, agentTimeoutMinutes),
|
|
1410
|
+
terminal_bridge_hard_deadline_at: deadlineAt(startedAt, agentHardTimeoutMinutes)
|
|
1388
1411
|
},
|
|
1389
1412
|
updated_at: startedAt
|
|
1390
1413
|
};
|
|
@@ -2033,6 +2056,9 @@ function recordTerminalBridgeApprovalNotification({ statePath, logPath, terminal
|
|
|
2033
2056
|
}
|
|
2034
2057
|
async function runSend(options) {
|
|
2035
2058
|
const messageBody = required(options.message ?? options.request, "--message is required");
|
|
2059
|
+
if (options.agentHardTimeoutMinutes !== undefined) {
|
|
2060
|
+
positiveMinutes(options.agentHardTimeoutMinutes, "--agent-hard-timeout-minutes");
|
|
2061
|
+
}
|
|
2036
2062
|
cleanupIdleConversations(storeDirFromOptions(options), options);
|
|
2037
2063
|
const terminalConversation = await resolveTerminalConversationFromOptions(options);
|
|
2038
2064
|
if (terminalConversation) {
|
|
@@ -2523,17 +2549,31 @@ async function runApprove(options) {
|
|
|
2523
2549
|
const nativeTakeoverForUpdate = isRecord(conversation.native_session_takeover)
|
|
2524
2550
|
? { ...conversation.native_session_takeover }
|
|
2525
2551
|
: {};
|
|
2552
|
+
const approvalResolvedAt = new Date().toISOString();
|
|
2553
|
+
const agentTimeoutMinutes = Number(options.agentTimeoutMinutes ??
|
|
2554
|
+
nativeTakeoverForUpdate.terminal_bridge_inactivity_timeout_minutes ??
|
|
2555
|
+
DEFAULT_AGENT_TIMEOUT_MINUTES);
|
|
2556
|
+
const agentHardTimeoutMinutes = positiveMinutes(options.agentHardTimeoutMinutes ??
|
|
2557
|
+
nativeTakeoverForUpdate.terminal_bridge_hard_timeout_minutes ??
|
|
2558
|
+
DEFAULT_AGENT_HARD_TIMEOUT_MINUTES, "--agent-hard-timeout-minutes");
|
|
2526
2559
|
const nextNativeTakeover = {
|
|
2527
2560
|
...nativeTakeoverForUpdate,
|
|
2528
2561
|
terminal_bridge_approval: undefined,
|
|
2529
|
-
terminal_bridge_approval_resolved_at:
|
|
2562
|
+
terminal_bridge_approval_resolved_at: approvalResolvedAt,
|
|
2563
|
+
terminal_bridge_monitor_started_at: approvalResolvedAt,
|
|
2564
|
+
terminal_bridge_last_activity_at: approvalResolvedAt,
|
|
2565
|
+
terminal_bridge_last_activity_reason: "approval resolved",
|
|
2566
|
+
terminal_bridge_inactivity_timeout_minutes: agentTimeoutMinutes,
|
|
2567
|
+
terminal_bridge_hard_timeout_minutes: agentHardTimeoutMinutes,
|
|
2568
|
+
terminal_bridge_inactivity_deadline_at: deadlineAt(approvalResolvedAt, agentTimeoutMinutes),
|
|
2569
|
+
terminal_bridge_hard_deadline_at: deadlineAt(stringValue(nativeTakeoverForUpdate.terminal_bridge_started_at) ?? approvalResolvedAt, agentHardTimeoutMinutes)
|
|
2530
2570
|
};
|
|
2531
2571
|
delete nextNativeTakeover.terminal_bridge_approval;
|
|
2532
2572
|
const nextConversation = {
|
|
2533
2573
|
...conversation,
|
|
2534
2574
|
status: terminalBridgeEnabled(conversation) ? "waiting_for_agent" : conversation.status,
|
|
2535
2575
|
native_session_takeover: nextNativeTakeover,
|
|
2536
|
-
updated_at:
|
|
2576
|
+
updated_at: approvalResolvedAt
|
|
2537
2577
|
};
|
|
2538
2578
|
saveState(statePath, nextConversation);
|
|
2539
2579
|
const bridgeMonitor = startTerminalBridgeMonitorForConversation({
|
|
@@ -2550,7 +2590,8 @@ async function runApprove(options) {
|
|
|
2550
2590
|
pid: bridgeMonitor.pid ?? null,
|
|
2551
2591
|
terminal_control: terminalControl,
|
|
2552
2592
|
reason: "approval_resolved",
|
|
2553
|
-
agent_timeout_minutes:
|
|
2593
|
+
agent_timeout_minutes: agentTimeoutMinutes,
|
|
2594
|
+
agent_hard_timeout_minutes: agentHardTimeoutMinutes
|
|
2554
2595
|
});
|
|
2555
2596
|
runtimeLog("info", "terminal_bridge_monitor_launch", {
|
|
2556
2597
|
conversation_id: conversation.conversation_id,
|
|
@@ -2640,10 +2681,32 @@ async function runTerminalConversationSend({ options, conversationId, messageBod
|
|
|
2640
2681
|
})
|
|
2641
2682
|
});
|
|
2642
2683
|
}
|
|
2643
|
-
async function runTerminalControlSend({ options, conversation, nextConversation, statePath, logPath, executor, message, terminalControl, needsNativeTakeoverBootstrap }) {
|
|
2644
|
-
const provider = createTerminalControlProvider(options);
|
|
2684
|
+
async function runTerminalControlSend({ options, conversation, nextConversation, statePath, logPath, executor, message, terminalControl, needsNativeTakeoverBootstrap, terminalSendLockHeld = false }) {
|
|
2645
2685
|
const bridge = terminalBridgeEnabled(conversation);
|
|
2686
|
+
if (bridge && !terminalSendLockHeld) {
|
|
2687
|
+
const releaseTerminalLock = acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), terminalControl), { timeoutMs: 30000 });
|
|
2688
|
+
try {
|
|
2689
|
+
return await runTerminalControlSend({
|
|
2690
|
+
options,
|
|
2691
|
+
conversation,
|
|
2692
|
+
nextConversation,
|
|
2693
|
+
statePath,
|
|
2694
|
+
logPath,
|
|
2695
|
+
executor,
|
|
2696
|
+
message,
|
|
2697
|
+
terminalControl,
|
|
2698
|
+
needsNativeTakeoverBootstrap,
|
|
2699
|
+
terminalSendLockHeld: true
|
|
2700
|
+
});
|
|
2701
|
+
}
|
|
2702
|
+
finally {
|
|
2703
|
+
releaseTerminalLock();
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
const provider = createTerminalControlProvider(options);
|
|
2646
2707
|
const bridgeStartedAt = new Date().toISOString();
|
|
2708
|
+
const agentTimeoutMinutes = Number(options.agentTimeoutMinutes ?? DEFAULT_AGENT_TIMEOUT_MINUTES);
|
|
2709
|
+
const agentHardTimeoutMinutes = positiveMinutes(options.agentHardTimeoutMinutes ?? DEFAULT_AGENT_HARD_TIMEOUT_MINUTES, "--agent-hard-timeout-minutes");
|
|
2647
2710
|
const terminalPayload = needsNativeTakeoverBootstrap && !bridge
|
|
2648
2711
|
? terminalSubmissionPayload(buildAgentSendPayload({
|
|
2649
2712
|
conversation,
|
|
@@ -2654,12 +2717,32 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
2654
2717
|
forkTakeover: undefined
|
|
2655
2718
|
}))
|
|
2656
2719
|
: terminalSubmissionPayload(String(message.body ?? ""));
|
|
2720
|
+
let preSendScreenFingerprint;
|
|
2721
|
+
if (bridge) {
|
|
2722
|
+
try {
|
|
2723
|
+
const screen = await provider.capture(terminalControl.target, {
|
|
2724
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
2725
|
+
socketPath: terminalControl.socketPath
|
|
2726
|
+
});
|
|
2727
|
+
preSendScreenFingerprint = terminalBridgeScreenFingerprint(screenExcerpt(screen));
|
|
2728
|
+
}
|
|
2729
|
+
catch {
|
|
2730
|
+
// Delivery can still succeed when capture is unavailable; prompt matching remains the fallback boundary.
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2657
2733
|
await provider.sendText(terminalControl.target, terminalPayload, {
|
|
2658
2734
|
socketPath: terminalControl.socketPath
|
|
2659
2735
|
});
|
|
2660
2736
|
await provider.sendKeys(terminalControl.target, ["C-m"], {
|
|
2661
2737
|
socketPath: terminalControl.socketPath
|
|
2662
2738
|
});
|
|
2739
|
+
const supersededConversationIds = bridge
|
|
2740
|
+
? supersedeTerminalBridgeConversations({
|
|
2741
|
+
storeDir: storeDirFromOptions(options),
|
|
2742
|
+
terminalControl,
|
|
2743
|
+
replacementConversationId: conversation.conversation_id
|
|
2744
|
+
})
|
|
2745
|
+
: [];
|
|
2663
2746
|
appendEvent(logPath, {
|
|
2664
2747
|
ts: new Date().toISOString(),
|
|
2665
2748
|
conversation_id: conversation.conversation_id,
|
|
@@ -2667,20 +2750,25 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
2667
2750
|
executor,
|
|
2668
2751
|
terminal_control: terminalControl,
|
|
2669
2752
|
message: textSummary(message.body),
|
|
2670
|
-
payload: textSummary(terminalPayload)
|
|
2753
|
+
payload: textSummary(terminalPayload),
|
|
2754
|
+
superseded_conversation_ids: supersededConversationIds
|
|
2671
2755
|
});
|
|
2672
2756
|
runtimeLog("info", "terminal_message_send", {
|
|
2673
2757
|
conversation_id: conversation.conversation_id,
|
|
2674
2758
|
agent: executor.kind,
|
|
2675
2759
|
terminal_target: terminalControl.target,
|
|
2676
2760
|
message: textSummary(message.body),
|
|
2677
|
-
payload: textSummary(terminalPayload)
|
|
2761
|
+
payload: textSummary(terminalPayload),
|
|
2762
|
+
superseded_conversation_ids: supersededConversationIds
|
|
2678
2763
|
});
|
|
2679
2764
|
const bridgeConversation = bridge
|
|
2680
2765
|
? withTerminalBridgeState({
|
|
2681
2766
|
conversation: nextConversation,
|
|
2682
2767
|
message,
|
|
2683
|
-
startedAt: bridgeStartedAt
|
|
2768
|
+
startedAt: bridgeStartedAt,
|
|
2769
|
+
agentTimeoutMinutes,
|
|
2770
|
+
agentHardTimeoutMinutes,
|
|
2771
|
+
preSendScreenFingerprint
|
|
2684
2772
|
})
|
|
2685
2773
|
: nextConversation;
|
|
2686
2774
|
const deliveredConversation = markTakeoverBootstrapped({
|
|
@@ -2707,7 +2795,8 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
2707
2795
|
event: "terminal_bridge_monitor_launch",
|
|
2708
2796
|
pid: bridgeMonitor.pid ?? null,
|
|
2709
2797
|
terminal_control: terminalControl,
|
|
2710
|
-
agent_timeout_minutes:
|
|
2798
|
+
agent_timeout_minutes: agentTimeoutMinutes,
|
|
2799
|
+
agent_hard_timeout_minutes: agentHardTimeoutMinutes
|
|
2711
2800
|
});
|
|
2712
2801
|
runtimeLog("info", "terminal_bridge_monitor_launch", {
|
|
2713
2802
|
conversation_id: deliveredConversation.conversation_id,
|
|
@@ -2844,6 +2933,72 @@ function createManagedTerminalConversationFromRawId({ options, conversationId, m
|
|
|
2844
2933
|
message
|
|
2845
2934
|
};
|
|
2846
2935
|
}
|
|
2936
|
+
function supersedeTerminalBridgeConversations({ storeDir, terminalControl, replacementConversationId }) {
|
|
2937
|
+
const activeStatuses = new Set([
|
|
2938
|
+
"created",
|
|
2939
|
+
"running",
|
|
2940
|
+
"waiting_for_agent",
|
|
2941
|
+
"waiting_for_openclaw",
|
|
2942
|
+
"stalled",
|
|
2943
|
+
"cancelling"
|
|
2944
|
+
]);
|
|
2945
|
+
const superseded = [];
|
|
2946
|
+
for (const candidate of listConversations(storeDir)) {
|
|
2947
|
+
if (candidate.conversation_id === replacementConversationId || !activeStatuses.has(candidate.status)) {
|
|
2948
|
+
continue;
|
|
2949
|
+
}
|
|
2950
|
+
const candidateTakeover = isRecord(candidate.native_session_takeover)
|
|
2951
|
+
? candidate.native_session_takeover
|
|
2952
|
+
: undefined;
|
|
2953
|
+
const candidateControl = terminalControlFromTakeover(candidateTakeover);
|
|
2954
|
+
if (candidateTakeover?.["terminal_bridge"] !== true ||
|
|
2955
|
+
candidateControl?.target !== terminalControl.target ||
|
|
2956
|
+
candidateControl?.socketPath !== terminalControl.socketPath) {
|
|
2957
|
+
continue;
|
|
2958
|
+
}
|
|
2959
|
+
const candidateStatePath = stringValue(candidate.state_path);
|
|
2960
|
+
if (!candidateStatePath) {
|
|
2961
|
+
continue;
|
|
2962
|
+
}
|
|
2963
|
+
const releaseLock = acquireFileLock(`${candidateStatePath}.lock`);
|
|
2964
|
+
try {
|
|
2965
|
+
const current = loadState(candidateStatePath);
|
|
2966
|
+
if (!activeStatuses.has(current.status)) {
|
|
2967
|
+
continue;
|
|
2968
|
+
}
|
|
2969
|
+
const currentTakeover = isRecord(current.native_session_takeover)
|
|
2970
|
+
? current.native_session_takeover
|
|
2971
|
+
: undefined;
|
|
2972
|
+
const currentControl = terminalControlFromTakeover(currentTakeover);
|
|
2973
|
+
if (currentTakeover?.["terminal_bridge"] !== true ||
|
|
2974
|
+
currentControl?.target !== terminalControl.target ||
|
|
2975
|
+
currentControl?.socketPath !== terminalControl.socketPath) {
|
|
2976
|
+
continue;
|
|
2977
|
+
}
|
|
2978
|
+
const now = new Date().toISOString();
|
|
2979
|
+
saveState(candidateStatePath, {
|
|
2980
|
+
...current,
|
|
2981
|
+
status: "closed",
|
|
2982
|
+
closed_at: now,
|
|
2983
|
+
close_reason: "terminal bridge superseded by a newer task on the same terminal",
|
|
2984
|
+
superseded_by_conversation_id: replacementConversationId,
|
|
2985
|
+
updated_at: now
|
|
2986
|
+
});
|
|
2987
|
+
appendEvent(logPathForStatePath(candidateStatePath), {
|
|
2988
|
+
ts: now,
|
|
2989
|
+
conversation_id: current.conversation_id,
|
|
2990
|
+
event: "terminal_bridge_superseded",
|
|
2991
|
+
terminal_control: terminalControl,
|
|
2992
|
+
replacement_conversation_id: replacementConversationId
|
|
2993
|
+
});
|
|
2994
|
+
superseded.push(current.conversation_id);
|
|
2995
|
+
}
|
|
2996
|
+
finally {
|
|
2997
|
+
releaseLock();
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
return superseded;
|
|
3001
|
+
}
|
|
2847
3002
|
function runNativeCodexResumeSend({ options, conversation, nextConversation, statePath, logPath, executor, executorEnv, message, payload, nativeTakeover, needsNativeTakeoverBootstrap }) {
|
|
2848
3003
|
const codexPath = resolveExecutable("codex");
|
|
2849
3004
|
const nativeSessionId = String(nativeTakeover["native_session_id"]);
|
|
@@ -3225,6 +3380,113 @@ function markConversationNeedsRecovery({ conversation, statePath, logPath, execu
|
|
|
3225
3380
|
budget: budgetAction(nextConversation)
|
|
3226
3381
|
};
|
|
3227
3382
|
}
|
|
3383
|
+
async function runRenew(options) {
|
|
3384
|
+
const { conversation, statePath, logPath } = loadConversationFromOptions(options);
|
|
3385
|
+
if (conversation.status === "closed") {
|
|
3386
|
+
throw new Error(`cannot renew ${conversation.conversation_id}; conversation is closed`);
|
|
3387
|
+
}
|
|
3388
|
+
if (conversation.status !== "stalled") {
|
|
3389
|
+
throw new Error(`cannot renew ${conversation.conversation_id}; conversation is ${conversation.status}, not stalled`);
|
|
3390
|
+
}
|
|
3391
|
+
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
3392
|
+
? conversation.native_session_takeover
|
|
3393
|
+
: undefined;
|
|
3394
|
+
const terminalControl = terminalControlFromTakeover(nativeTakeover);
|
|
3395
|
+
if (!terminalControl || nativeTakeover?.["terminal_bridge"] !== true) {
|
|
3396
|
+
throw new Error(`cannot renew ${conversation.conversation_id}; conversation is not a terminal bridge task`);
|
|
3397
|
+
}
|
|
3398
|
+
const panes = await createTerminalControlProvider(options).listPanes();
|
|
3399
|
+
const terminalExists = panes.some((pane) => pane.target === terminalControl.target &&
|
|
3400
|
+
(terminalControl.socketPath === undefined || pane.socketPath === terminalControl.socketPath));
|
|
3401
|
+
if (!terminalExists) {
|
|
3402
|
+
throw new Error(`cannot renew ${conversation.conversation_id}; terminal ${terminalControl.target} is no longer available`);
|
|
3403
|
+
}
|
|
3404
|
+
const inactivityTimeoutMinutes = positiveMinutes(options.minutes ??
|
|
3405
|
+
options.agentTimeoutMinutes ??
|
|
3406
|
+
nativeTakeover?.["terminal_bridge_inactivity_timeout_minutes"] ??
|
|
3407
|
+
DEFAULT_AGENT_TIMEOUT_MINUTES, "--minutes");
|
|
3408
|
+
const hardTimeoutMinutes = positiveMinutes(nativeTakeover?.["terminal_bridge_hard_timeout_minutes"] ??
|
|
3409
|
+
DEFAULT_AGENT_HARD_TIMEOUT_MINUTES, "--agent-hard-timeout-minutes");
|
|
3410
|
+
const startedAt = stringValue(nativeTakeover?.["terminal_bridge_started_at"]);
|
|
3411
|
+
const startedAtMs = startedAt ? Date.parse(startedAt) : NaN;
|
|
3412
|
+
if (Number.isFinite(startedAtMs) && Date.now() - startedAtMs >= hardTimeoutMinutes * 60 * 1000) {
|
|
3413
|
+
throw new Error(`cannot renew ${conversation.conversation_id}; terminal bridge hard lifetime of ${hardTimeoutMinutes} minutes has elapsed`);
|
|
3414
|
+
}
|
|
3415
|
+
const now = new Date().toISOString();
|
|
3416
|
+
const renewed = {
|
|
3417
|
+
...conversation,
|
|
3418
|
+
status: "waiting_for_agent",
|
|
3419
|
+
native_session_takeover: {
|
|
3420
|
+
...nativeTakeover,
|
|
3421
|
+
terminal_bridge_monitor_started_at: now,
|
|
3422
|
+
terminal_bridge_last_activity_at: now,
|
|
3423
|
+
terminal_bridge_inactivity_timeout_minutes: inactivityTimeoutMinutes,
|
|
3424
|
+
terminal_bridge_hard_timeout_minutes: hardTimeoutMinutes,
|
|
3425
|
+
terminal_bridge_inactivity_deadline_at: deadlineAt(now, inactivityTimeoutMinutes),
|
|
3426
|
+
terminal_bridge_hard_deadline_at: deadlineAt(startedAt ?? now, hardTimeoutMinutes),
|
|
3427
|
+
terminal_bridge_renewed_at: now
|
|
3428
|
+
},
|
|
3429
|
+
updated_at: now
|
|
3430
|
+
};
|
|
3431
|
+
Reflect.deleteProperty(renewed, "stalled_at");
|
|
3432
|
+
Reflect.deleteProperty(renewed, "stalled_reason");
|
|
3433
|
+
Reflect.deleteProperty(renewed, "stalled_notification_sent_at");
|
|
3434
|
+
Reflect.deleteProperty(renewed, "stalled_notification_message_id");
|
|
3435
|
+
saveState(statePath, renewed);
|
|
3436
|
+
appendEvent(logPath, {
|
|
3437
|
+
ts: now,
|
|
3438
|
+
conversation_id: conversation.conversation_id,
|
|
3439
|
+
event: "terminal_bridge_renewed",
|
|
3440
|
+
previous_status: conversation.status,
|
|
3441
|
+
terminal_control: terminalControl,
|
|
3442
|
+
agent_timeout_minutes: inactivityTimeoutMinutes,
|
|
3443
|
+
agent_hard_timeout_minutes: hardTimeoutMinutes,
|
|
3444
|
+
last_activity_at: now
|
|
3445
|
+
});
|
|
3446
|
+
runtimeLog("info", "terminal_bridge_renewed", {
|
|
3447
|
+
conversation_id: conversation.conversation_id,
|
|
3448
|
+
terminal_target: terminalControl.target,
|
|
3449
|
+
agent_timeout_minutes: inactivityTimeoutMinutes,
|
|
3450
|
+
agent_hard_timeout_minutes: hardTimeoutMinutes
|
|
3451
|
+
});
|
|
3452
|
+
const monitor = startTerminalBridgeMonitorForConversation({
|
|
3453
|
+
conversation: renewed,
|
|
3454
|
+
statePath,
|
|
3455
|
+
logPath,
|
|
3456
|
+
options: {
|
|
3457
|
+
...options,
|
|
3458
|
+
agentTimeoutMinutes: inactivityTimeoutMinutes,
|
|
3459
|
+
agentHardTimeoutMinutes: hardTimeoutMinutes
|
|
3460
|
+
}
|
|
3461
|
+
});
|
|
3462
|
+
if (monitor) {
|
|
3463
|
+
appendEvent(logPath, {
|
|
3464
|
+
ts: new Date().toISOString(),
|
|
3465
|
+
conversation_id: conversation.conversation_id,
|
|
3466
|
+
event: "terminal_bridge_monitor_launch",
|
|
3467
|
+
pid: monitor.pid ?? null,
|
|
3468
|
+
terminal_control: terminalControl,
|
|
3469
|
+
reason: "renewal",
|
|
3470
|
+
agent_timeout_minutes: inactivityTimeoutMinutes,
|
|
3471
|
+
agent_hard_timeout_minutes: hardTimeoutMinutes
|
|
3472
|
+
});
|
|
3473
|
+
}
|
|
3474
|
+
printJson({
|
|
3475
|
+
conversation: renewed,
|
|
3476
|
+
renewed: true,
|
|
3477
|
+
terminal_control: terminalControl,
|
|
3478
|
+
agent_timeout_minutes: inactivityTimeoutMinutes,
|
|
3479
|
+
agent_hard_timeout_minutes: hardTimeoutMinutes,
|
|
3480
|
+
monitor_pid: monitor?.pid ?? null
|
|
3481
|
+
});
|
|
3482
|
+
}
|
|
3483
|
+
function positiveMinutes(value, optionName) {
|
|
3484
|
+
const parsed = Number(value);
|
|
3485
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
3486
|
+
throw new Error(`${optionName} must be a positive number`);
|
|
3487
|
+
}
|
|
3488
|
+
return parsed;
|
|
3489
|
+
}
|
|
3228
3490
|
async function runCancel(options) {
|
|
3229
3491
|
cleanupIdleConversations(storeDirFromOptions(options), options);
|
|
3230
3492
|
const terminalConversation = await resolveTerminalConversationFromOptions(options);
|
|
@@ -3694,9 +3956,27 @@ async function runMonitor(options) {
|
|
|
3694
3956
|
async function runTerminalBridgeMonitor(options) {
|
|
3695
3957
|
const statePath = expandHome(required(options.state, "--state is required"));
|
|
3696
3958
|
const logPath = expandHome(options.log ?? logPathForStatePath(statePath));
|
|
3697
|
-
const timeoutMinutes = Number(options.agentTimeoutMinutes ?? DEFAULT_AGENT_TIMEOUT_MINUTES);
|
|
3698
3959
|
const pollIntervalMs = Math.max(50, Number(options.pollIntervalMs ?? DEFAULT_MONITOR_POLL_INTERVAL_MS));
|
|
3699
3960
|
let conversation = loadState(statePath);
|
|
3961
|
+
const initialNativeTakeover = isRecord(conversation.native_session_takeover)
|
|
3962
|
+
? conversation.native_session_takeover
|
|
3963
|
+
: undefined;
|
|
3964
|
+
const timeoutMinutes = Number(options.agentTimeoutMinutes ??
|
|
3965
|
+
initialNativeTakeover?.["terminal_bridge_inactivity_timeout_minutes"] ??
|
|
3966
|
+
DEFAULT_AGENT_TIMEOUT_MINUTES);
|
|
3967
|
+
const hardTimeoutMinutes = positiveMinutes(options.agentHardTimeoutMinutes ??
|
|
3968
|
+
initialNativeTakeover?.["terminal_bridge_hard_timeout_minutes"] ??
|
|
3969
|
+
DEFAULT_AGENT_HARD_TIMEOUT_MINUTES, "--agent-hard-timeout-minutes");
|
|
3970
|
+
const monitorStartedAtMs = Date.now();
|
|
3971
|
+
const monitorMessageId = stringValue(initialNativeTakeover?.["terminal_bridge_message_id"]);
|
|
3972
|
+
const taskStartedAtMs = validTimestampMs(initialNativeTakeover?.["terminal_bridge_started_at"]) ?? monitorStartedAtMs;
|
|
3973
|
+
let lastActivityAtMs = validTimestampMs(initialNativeTakeover?.["terminal_bridge_last_activity_at"]) ?? taskStartedAtMs;
|
|
3974
|
+
let lastPersistedActivityAtMs = lastActivityAtMs;
|
|
3975
|
+
const activityPersistIntervalMs = terminalBridgeActivityPersistIntervalMs(timeoutMinutes, pollIntervalMs);
|
|
3976
|
+
const preSendScreenFingerprint = stringValue(initialNativeTakeover?.["terminal_bridge_pre_send_screen_fingerprint"]);
|
|
3977
|
+
let previousScreenFingerprint = preSendScreenFingerprint;
|
|
3978
|
+
let previousRolloutFingerprint;
|
|
3979
|
+
let persistedActivityReason = stringValue(initialNativeTakeover?.["terminal_bridge_last_activity_reason"]);
|
|
3700
3980
|
const executor = executorForConversation(conversation);
|
|
3701
3981
|
appendEvent(logPath, {
|
|
3702
3982
|
ts: new Date().toISOString(),
|
|
@@ -3704,13 +3984,23 @@ async function runTerminalBridgeMonitor(options) {
|
|
|
3704
3984
|
event: "terminal_bridge_monitor_started",
|
|
3705
3985
|
executor,
|
|
3706
3986
|
agent_timeout_minutes: timeoutMinutes,
|
|
3707
|
-
|
|
3987
|
+
agent_hard_timeout_minutes: hardTimeoutMinutes,
|
|
3988
|
+
poll_interval_ms: pollIntervalMs,
|
|
3989
|
+
task_started_at: new Date(taskStartedAtMs).toISOString(),
|
|
3990
|
+
last_activity_at: new Date(lastActivityAtMs).toISOString(),
|
|
3991
|
+
inactivity_deadline_at: timeoutMinutes > 0
|
|
3992
|
+
? new Date(lastActivityAtMs + timeoutMinutes * 60 * 1000).toISOString()
|
|
3993
|
+
: null,
|
|
3994
|
+
hard_deadline_at: hardTimeoutMinutes > 0
|
|
3995
|
+
? new Date(taskStartedAtMs + hardTimeoutMinutes * 60 * 1000).toISOString()
|
|
3996
|
+
: null
|
|
3708
3997
|
});
|
|
3709
3998
|
runtimeLog("info", "terminal_bridge_monitor_started", {
|
|
3710
3999
|
conversation_id: conversation.conversation_id,
|
|
3711
4000
|
agent: executor.kind,
|
|
3712
4001
|
executor_session: executor.session,
|
|
3713
|
-
agent_timeout_minutes: timeoutMinutes
|
|
4002
|
+
agent_timeout_minutes: timeoutMinutes,
|
|
4003
|
+
agent_hard_timeout_minutes: hardTimeoutMinutes
|
|
3714
4004
|
});
|
|
3715
4005
|
let idleCompletionFingerprint;
|
|
3716
4006
|
while (true) {
|
|
@@ -3733,6 +4023,24 @@ async function runTerminalBridgeMonitor(options) {
|
|
|
3733
4023
|
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
3734
4024
|
? conversation.native_session_takeover
|
|
3735
4025
|
: undefined;
|
|
4026
|
+
const currentMessageId = stringValue(nativeTakeover?.["terminal_bridge_message_id"]);
|
|
4027
|
+
if (monitorMessageId && currentMessageId !== monitorMessageId) {
|
|
4028
|
+
appendEvent(logPath, {
|
|
4029
|
+
ts: new Date().toISOString(),
|
|
4030
|
+
conversation_id: conversation.conversation_id,
|
|
4031
|
+
event: "terminal_bridge_monitor_superseded",
|
|
4032
|
+
monitor_message_id: monitorMessageId,
|
|
4033
|
+
current_message_id: currentMessageId
|
|
4034
|
+
});
|
|
4035
|
+
printJson({
|
|
4036
|
+
conversation,
|
|
4037
|
+
monitored: true,
|
|
4038
|
+
terminal_bridge: true,
|
|
4039
|
+
completed: false,
|
|
4040
|
+
reason: "terminal_bridge_task_replaced"
|
|
4041
|
+
});
|
|
4042
|
+
return;
|
|
4043
|
+
}
|
|
3736
4044
|
const terminalControl = terminalControlFromTakeover(nativeTakeover);
|
|
3737
4045
|
if (!terminalControl || nativeTakeover?.["terminal_bridge"] !== true) {
|
|
3738
4046
|
const stalledConversation = markConversationStalled({
|
|
@@ -3841,26 +4149,91 @@ async function runTerminalBridgeMonitor(options) {
|
|
|
3841
4149
|
});
|
|
3842
4150
|
return;
|
|
3843
4151
|
}
|
|
4152
|
+
const screenFingerprint = terminalBridgeScreenFingerprint(terminalStatus?.screen?.excerpt);
|
|
4153
|
+
const screenChanged = previousScreenFingerprint !== undefined &&
|
|
4154
|
+
screenFingerprint !== undefined &&
|
|
4155
|
+
screenFingerprint !== previousScreenFingerprint;
|
|
4156
|
+
const screenChangedSinceSend = preSendScreenFingerprint !== undefined &&
|
|
4157
|
+
screenFingerprint !== undefined &&
|
|
4158
|
+
screenFingerprint !== preSendScreenFingerprint;
|
|
4159
|
+
previousScreenFingerprint = screenFingerprint;
|
|
3844
4160
|
const contextMatch = await terminalBridgeCodexContext({
|
|
3845
4161
|
conversation,
|
|
3846
4162
|
nativeTakeover,
|
|
3847
4163
|
options
|
|
3848
4164
|
});
|
|
3849
4165
|
const startedAt = stringValue(nativeTakeover?.["terminal_bridge_started_at"]);
|
|
4166
|
+
const rolloutCompletion = contextMatch?.context
|
|
4167
|
+
? latestCompletedRolloutTurn({
|
|
4168
|
+
context: contextMatch.context,
|
|
4169
|
+
conversation,
|
|
4170
|
+
nativeTakeover,
|
|
4171
|
+
startedAt
|
|
4172
|
+
})
|
|
4173
|
+
: undefined;
|
|
3850
4174
|
const assistantMessage = contextMatch?.context
|
|
3851
4175
|
? latestAssistantAfter(contextMatch.context, startedAt)
|
|
3852
4176
|
: undefined;
|
|
4177
|
+
const rolloutFingerprint = rolloutCompletion || assistantMessage
|
|
4178
|
+
? terminalBridgeActivityFingerprint(JSON.stringify({
|
|
4179
|
+
text: rolloutCompletion?.text ?? assistantMessage?.text,
|
|
4180
|
+
timestamp: rolloutCompletion?.timestamp ?? assistantMessage?.timestamp,
|
|
4181
|
+
turn_id: rolloutCompletion?.turnId
|
|
4182
|
+
}))
|
|
4183
|
+
: undefined;
|
|
4184
|
+
const rolloutChanged = rolloutFingerprint !== undefined && rolloutFingerprint !== previousRolloutFingerprint;
|
|
4185
|
+
previousRolloutFingerprint = rolloutFingerprint;
|
|
4186
|
+
const activityReasons = [
|
|
4187
|
+
terminalStatus.activity_state === "working" ? terminalStatus.activity_reason : undefined,
|
|
4188
|
+
screenChanged ? "terminal screen changed" : undefined,
|
|
4189
|
+
rolloutChanged ? "Codex rollout assistant output changed" : undefined
|
|
4190
|
+
].filter((value) => Boolean(value));
|
|
4191
|
+
if (activityReasons.length > 0) {
|
|
4192
|
+
const observedAtMs = Date.now();
|
|
4193
|
+
lastActivityAtMs = observedAtMs;
|
|
4194
|
+
const activityReason = activityReasons.join("; ");
|
|
4195
|
+
if (persistedActivityReason === undefined ||
|
|
4196
|
+
observedAtMs - lastPersistedActivityAtMs >= activityPersistIntervalMs) {
|
|
4197
|
+
conversation = persistTerminalBridgeActivity({
|
|
4198
|
+
conversation,
|
|
4199
|
+
statePath,
|
|
4200
|
+
logPath,
|
|
4201
|
+
observedAtMs,
|
|
4202
|
+
reason: activityReason,
|
|
4203
|
+
activityState: terminalStatus.activity_state,
|
|
4204
|
+
timeoutMinutes,
|
|
4205
|
+
hardTimeoutMinutes
|
|
4206
|
+
});
|
|
4207
|
+
lastPersistedActivityAtMs = observedAtMs;
|
|
4208
|
+
persistedActivityReason = activityReason;
|
|
4209
|
+
if (!isWaitingForAgent(conversation.status)) {
|
|
4210
|
+
continue;
|
|
4211
|
+
}
|
|
4212
|
+
}
|
|
4213
|
+
}
|
|
3853
4214
|
const terminalIdle = terminalStatus.activity_state === "idle";
|
|
3854
|
-
const screenMessage = !assistantMessage && terminalIdle
|
|
3855
|
-
? terminalBridgeScreenMessage({
|
|
4215
|
+
const screenMessage = !rolloutCompletion && !assistantMessage && terminalIdle
|
|
4216
|
+
? terminalBridgeScreenMessage({
|
|
4217
|
+
conversation,
|
|
4218
|
+
nativeTakeover,
|
|
4219
|
+
terminalStatus,
|
|
4220
|
+
screenChangedSinceSend
|
|
4221
|
+
})
|
|
3856
4222
|
: undefined;
|
|
3857
|
-
const completion = terminalIdle ? assistantMessage ?? screenMessage : undefined;
|
|
4223
|
+
const completion = rolloutCompletion ?? (terminalIdle ? assistantMessage ?? screenMessage : undefined);
|
|
4224
|
+
const completionMatch = rolloutCompletion
|
|
4225
|
+
? "rollout_task_complete"
|
|
4226
|
+
: assistantMessage
|
|
4227
|
+
? contextMatch?.match
|
|
4228
|
+
: "terminal_screen";
|
|
3858
4229
|
const completionFingerprint = completion
|
|
3859
4230
|
? createHash("sha256")
|
|
3860
4231
|
.update(JSON.stringify({
|
|
3861
4232
|
text: completion.text,
|
|
3862
4233
|
timestamp: completion.timestamp,
|
|
3863
|
-
match:
|
|
4234
|
+
match: completionMatch,
|
|
4235
|
+
turn_id: rolloutCompletion?.turnId,
|
|
4236
|
+
message_id: currentMessageId
|
|
3864
4237
|
}))
|
|
3865
4238
|
.digest("hex")
|
|
3866
4239
|
: undefined;
|
|
@@ -3872,9 +4245,11 @@ async function runTerminalBridgeMonitor(options) {
|
|
|
3872
4245
|
conversation_id: conversation.conversation_id,
|
|
3873
4246
|
event: "terminal_bridge_completion_detected",
|
|
3874
4247
|
terminal_control: terminalControl,
|
|
3875
|
-
match:
|
|
4248
|
+
match: completionMatch,
|
|
3876
4249
|
codex_session: contextMatch?.context.source,
|
|
3877
|
-
assistant_timestamp: completion?.timestamp
|
|
4250
|
+
assistant_timestamp: completion?.timestamp,
|
|
4251
|
+
rollout_turn_id: rolloutCompletion?.turnId,
|
|
4252
|
+
terminal_bridge_message_id: currentMessageId
|
|
3878
4253
|
});
|
|
3879
4254
|
const callbackMessage = createMessage({
|
|
3880
4255
|
conversation,
|
|
@@ -3887,9 +4262,11 @@ async function runTerminalBridgeMonitor(options) {
|
|
|
3887
4262
|
source: "terminal_bridge",
|
|
3888
4263
|
terminal_control: terminalControl,
|
|
3889
4264
|
codex_session: contextMatch?.context.source,
|
|
3890
|
-
confidence: assistantMessage ? contextMatch?.confidence : "screen_only",
|
|
3891
|
-
match:
|
|
3892
|
-
assistant_timestamp: completion?.timestamp
|
|
4265
|
+
confidence: rolloutCompletion || assistantMessage ? contextMatch?.confidence : "screen_only",
|
|
4266
|
+
match: completionMatch,
|
|
4267
|
+
assistant_timestamp: completion?.timestamp,
|
|
4268
|
+
rollout_turn_id: rolloutCompletion?.turnId,
|
|
4269
|
+
terminal_bridge_message_id: currentMessageId
|
|
3893
4270
|
}
|
|
3894
4271
|
});
|
|
3895
4272
|
runLockedCallback({
|
|
@@ -3907,18 +4284,59 @@ async function runTerminalBridgeMonitor(options) {
|
|
|
3907
4284
|
});
|
|
3908
4285
|
return;
|
|
3909
4286
|
}
|
|
4287
|
+
// A concrete approval or completion observed on this poll wins over a timeout boundary.
|
|
4288
|
+
const nowMs = Date.now();
|
|
4289
|
+
if (Number.isFinite(hardTimeoutMinutes) &&
|
|
4290
|
+
hardTimeoutMinutes > 0 &&
|
|
4291
|
+
nowMs - taskStartedAtMs >= hardTimeoutMinutes * 60 * 1000) {
|
|
4292
|
+
appendEvent(logPath, {
|
|
4293
|
+
ts: new Date(nowMs).toISOString(),
|
|
4294
|
+
conversation_id: conversation.conversation_id,
|
|
4295
|
+
event: "terminal_bridge_hard_timeout_reached",
|
|
4296
|
+
terminal_control: terminalControl,
|
|
4297
|
+
task_started_at: new Date(taskStartedAtMs).toISOString(),
|
|
4298
|
+
hard_deadline_at: new Date(taskStartedAtMs + hardTimeoutMinutes * 60 * 1000).toISOString(),
|
|
4299
|
+
agent_hard_timeout_minutes: hardTimeoutMinutes,
|
|
4300
|
+
last_activity_at: new Date(lastActivityAtMs).toISOString(),
|
|
4301
|
+
terminal_activity_state: terminalStatus.activity_state
|
|
4302
|
+
});
|
|
4303
|
+
const stalledConversation = markConversationStalled({
|
|
4304
|
+
statePath,
|
|
4305
|
+
logPath,
|
|
4306
|
+
reason: `terminal bridge reached its hard lifetime of ${hardTimeoutMinutes} minutes`,
|
|
4307
|
+
detail: {
|
|
4308
|
+
terminal_bridge: true,
|
|
4309
|
+
terminal_control: terminalControl,
|
|
4310
|
+
task_started_at: new Date(taskStartedAtMs).toISOString(),
|
|
4311
|
+
last_activity_at: new Date(lastActivityAtMs).toISOString(),
|
|
4312
|
+
agent_hard_timeout_minutes: hardTimeoutMinutes,
|
|
4313
|
+
terminal_activity_state: terminalStatus.activity_state
|
|
4314
|
+
}
|
|
4315
|
+
});
|
|
4316
|
+
printJson({
|
|
4317
|
+
conversation: stalledConversation,
|
|
4318
|
+
monitored: true,
|
|
4319
|
+
terminal_bridge: true,
|
|
4320
|
+
stalled: true,
|
|
4321
|
+
hard_timeout: true,
|
|
4322
|
+
reason: stalledConversation?.stalled_reason
|
|
4323
|
+
});
|
|
4324
|
+
return;
|
|
4325
|
+
}
|
|
3910
4326
|
if (Number.isFinite(timeoutMinutes) && timeoutMinutes > 0) {
|
|
3911
|
-
|
|
3912
|
-
if (Number.isFinite(updatedAtMs) && Date.now() - updatedAtMs >= timeoutMinutes * 60 * 1000) {
|
|
4327
|
+
if (nowMs - lastActivityAtMs >= timeoutMinutes * 60 * 1000) {
|
|
3913
4328
|
const stalledConversation = markConversationStalled({
|
|
3914
4329
|
statePath,
|
|
3915
4330
|
logPath,
|
|
3916
|
-
reason: `terminal bridge
|
|
4331
|
+
reason: `terminal bridge observed no activity for ${timeoutMinutes} minutes`,
|
|
3917
4332
|
detail: {
|
|
3918
4333
|
terminal_bridge: true,
|
|
3919
4334
|
terminal_control: terminalControl,
|
|
3920
4335
|
match: contextMatch?.match,
|
|
3921
|
-
terminal_activity_state: terminalStatus.activity_state
|
|
4336
|
+
terminal_activity_state: terminalStatus.activity_state,
|
|
4337
|
+
last_activity_at: new Date(lastActivityAtMs).toISOString(),
|
|
4338
|
+
inactivity_deadline_at: new Date(lastActivityAtMs + timeoutMinutes * 60 * 1000).toISOString(),
|
|
4339
|
+
agent_timeout_minutes: timeoutMinutes
|
|
3922
4340
|
}
|
|
3923
4341
|
});
|
|
3924
4342
|
printJson({
|
|
@@ -3934,6 +4352,108 @@ async function runTerminalBridgeMonitor(options) {
|
|
|
3934
4352
|
sleepSync(pollIntervalMs);
|
|
3935
4353
|
}
|
|
3936
4354
|
}
|
|
4355
|
+
function validTimestampMs(value) {
|
|
4356
|
+
const parsed = Date.parse(String(value ?? ""));
|
|
4357
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
4358
|
+
}
|
|
4359
|
+
function deadlineAt(startedAt, timeoutMinutes) {
|
|
4360
|
+
const startedAtMs = validTimestampMs(startedAt);
|
|
4361
|
+
return startedAtMs !== undefined && Number.isFinite(timeoutMinutes) && timeoutMinutes > 0
|
|
4362
|
+
? new Date(startedAtMs + timeoutMinutes * 60 * 1000).toISOString()
|
|
4363
|
+
: undefined;
|
|
4364
|
+
}
|
|
4365
|
+
function terminalBridgeActivityFingerprint(value) {
|
|
4366
|
+
const text = stringValue(value);
|
|
4367
|
+
return text ? createHash("sha256").update(text).digest("hex") : undefined;
|
|
4368
|
+
}
|
|
4369
|
+
function terminalBridgeScreenFingerprint(value) {
|
|
4370
|
+
return typeof value === "string"
|
|
4371
|
+
? createHash("sha256").update(value).digest("hex")
|
|
4372
|
+
: undefined;
|
|
4373
|
+
}
|
|
4374
|
+
function terminalBridgeSendLockPath(storeDir, terminalControl) {
|
|
4375
|
+
const terminalKey = createHash("sha256")
|
|
4376
|
+
.update(JSON.stringify({
|
|
4377
|
+
target: terminalControl.target,
|
|
4378
|
+
socket_path: terminalControl.socketPath
|
|
4379
|
+
}))
|
|
4380
|
+
.digest("hex")
|
|
4381
|
+
.slice(0, 20);
|
|
4382
|
+
return path.join(storeDir, `.terminal-bridge-send-${terminalKey}.lock`);
|
|
4383
|
+
}
|
|
4384
|
+
function terminalBridgeRequestFingerprint(value) {
|
|
4385
|
+
const text = String(value ?? "").replace(/\s+/gu, " ").trim();
|
|
4386
|
+
return text ? createHash("sha256").update(text).digest("hex") : undefined;
|
|
4387
|
+
}
|
|
4388
|
+
function terminalBridgeActivityPersistIntervalMs(timeoutMinutes, pollIntervalMs) {
|
|
4389
|
+
if (!Number.isFinite(timeoutMinutes) || timeoutMinutes <= 0) {
|
|
4390
|
+
return 5 * 60 * 1000;
|
|
4391
|
+
}
|
|
4392
|
+
return Math.max(pollIntervalMs, Math.min(timeoutMinutes * 30 * 1000, 5 * 60 * 1000));
|
|
4393
|
+
}
|
|
4394
|
+
function persistTerminalBridgeActivity({ conversation, statePath, logPath, observedAtMs, reason, activityState, timeoutMinutes, hardTimeoutMinutes }) {
|
|
4395
|
+
const releaseLock = acquireFileLock(`${statePath}.lock`);
|
|
4396
|
+
try {
|
|
4397
|
+
const currentConversation = loadState(statePath);
|
|
4398
|
+
if (!isWaitingForAgent(currentConversation.status)) {
|
|
4399
|
+
return currentConversation;
|
|
4400
|
+
}
|
|
4401
|
+
const expectedNativeTakeover = isRecord(conversation.native_session_takeover)
|
|
4402
|
+
? conversation.native_session_takeover
|
|
4403
|
+
: {};
|
|
4404
|
+
const nativeTakeover = isRecord(currentConversation.native_session_takeover)
|
|
4405
|
+
? currentConversation.native_session_takeover
|
|
4406
|
+
: {};
|
|
4407
|
+
if (nativeTakeover["terminal_bridge_message_id"] !==
|
|
4408
|
+
expectedNativeTakeover["terminal_bridge_message_id"]) {
|
|
4409
|
+
return currentConversation;
|
|
4410
|
+
}
|
|
4411
|
+
const previousActivityAtMs = validTimestampMs(nativeTakeover["terminal_bridge_last_activity_at"]);
|
|
4412
|
+
const observedAt = new Date(observedAtMs).toISOString();
|
|
4413
|
+
const inactivityDeadlineAt = Number.isFinite(timeoutMinutes) && timeoutMinutes > 0
|
|
4414
|
+
? new Date(observedAtMs + timeoutMinutes * 60 * 1000).toISOString()
|
|
4415
|
+
: undefined;
|
|
4416
|
+
const nextConversation = {
|
|
4417
|
+
...currentConversation,
|
|
4418
|
+
native_session_takeover: {
|
|
4419
|
+
...nativeTakeover,
|
|
4420
|
+
terminal_bridge_last_activity_at: observedAt,
|
|
4421
|
+
terminal_bridge_last_activity_reason: reason,
|
|
4422
|
+
terminal_bridge_inactivity_deadline_at: inactivityDeadlineAt,
|
|
4423
|
+
terminal_bridge_inactivity_timeout_minutes: timeoutMinutes,
|
|
4424
|
+
terminal_bridge_hard_timeout_minutes: hardTimeoutMinutes
|
|
4425
|
+
},
|
|
4426
|
+
updated_at: observedAt
|
|
4427
|
+
};
|
|
4428
|
+
saveState(statePath, nextConversation);
|
|
4429
|
+
appendEvent(logPath, {
|
|
4430
|
+
ts: observedAt,
|
|
4431
|
+
conversation_id: currentConversation.conversation_id,
|
|
4432
|
+
event: "terminal_bridge_activity_observed",
|
|
4433
|
+
reason,
|
|
4434
|
+
last_activity_at: observedAt,
|
|
4435
|
+
terminal_activity_state: activityState
|
|
4436
|
+
});
|
|
4437
|
+
if (inactivityDeadlineAt) {
|
|
4438
|
+
appendEvent(logPath, {
|
|
4439
|
+
ts: observedAt,
|
|
4440
|
+
conversation_id: currentConversation.conversation_id,
|
|
4441
|
+
event: "terminal_bridge_inactivity_deadline_extended",
|
|
4442
|
+
reason,
|
|
4443
|
+
previous_last_activity_at: previousActivityAtMs === undefined
|
|
4444
|
+
? null
|
|
4445
|
+
: new Date(previousActivityAtMs).toISOString(),
|
|
4446
|
+
last_activity_at: observedAt,
|
|
4447
|
+
inactivity_deadline_at: inactivityDeadlineAt,
|
|
4448
|
+
agent_timeout_minutes: timeoutMinutes
|
|
4449
|
+
});
|
|
4450
|
+
}
|
|
4451
|
+
return nextConversation;
|
|
4452
|
+
}
|
|
4453
|
+
finally {
|
|
4454
|
+
releaseLock();
|
|
4455
|
+
}
|
|
4456
|
+
}
|
|
3937
4457
|
function terminalBridgeApprovalCandidate({ executor, terminalControl, terminalStatus, fingerprint }) {
|
|
3938
4458
|
const approval = isRecord(terminalStatus?.approval_state) ? terminalStatus.approval_state : {};
|
|
3939
4459
|
if (approval.approvable !== true) {
|
|
@@ -4019,22 +4539,59 @@ function latestAssistantAfter(context, startedAt) {
|
|
|
4019
4539
|
return Number.isFinite(messageTime) && messageTime >= Number(threshold);
|
|
4020
4540
|
});
|
|
4021
4541
|
}
|
|
4022
|
-
function
|
|
4542
|
+
function latestCompletedRolloutTurn({ context, conversation, nativeTakeover, startedAt }) {
|
|
4543
|
+
const threshold = validTimestampMs(startedAt);
|
|
4544
|
+
const expectedRequestHash = stringValue(nativeTakeover?.["terminal_bridge_request_hash"]) ??
|
|
4545
|
+
terminalBridgeRequestFingerprint(nativeTakeover?.["terminal_bridge_request_text"] ?? conversation.user_request);
|
|
4546
|
+
if (threshold === undefined || !expectedRequestHash) {
|
|
4547
|
+
return undefined;
|
|
4548
|
+
}
|
|
4549
|
+
const turn = [...(context.turns ?? [])]
|
|
4550
|
+
.reverse()
|
|
4551
|
+
.find((candidate) => {
|
|
4552
|
+
const userTimestamp = validTimestampMs(candidate.userTimestamp);
|
|
4553
|
+
const completedAt = validTimestampMs(candidate.completedAt);
|
|
4554
|
+
return candidate.userTextHash === expectedRequestHash &&
|
|
4555
|
+
userTimestamp !== undefined &&
|
|
4556
|
+
completedAt !== undefined &&
|
|
4557
|
+
userTimestamp >= threshold &&
|
|
4558
|
+
completedAt >= userTimestamp &&
|
|
4559
|
+
Boolean(candidate.lastAssistantMessage);
|
|
4560
|
+
});
|
|
4561
|
+
if (!turn?.lastAssistantMessage) {
|
|
4562
|
+
return undefined;
|
|
4563
|
+
}
|
|
4564
|
+
return {
|
|
4565
|
+
role: "assistant",
|
|
4566
|
+
text: turn.lastAssistantMessage,
|
|
4567
|
+
timestamp: turn.completedAt,
|
|
4568
|
+
turnId: turn.turnId,
|
|
4569
|
+
userTimestamp: turn.userTimestamp
|
|
4570
|
+
};
|
|
4571
|
+
}
|
|
4572
|
+
function terminalBridgeScreenMessage({ conversation, nativeTakeover, terminalStatus, screenChangedSinceSend }) {
|
|
4023
4573
|
const excerpt = stringValue(terminalStatus?.screen?.excerpt);
|
|
4024
4574
|
if (!excerpt) {
|
|
4025
4575
|
return undefined;
|
|
4026
4576
|
}
|
|
4027
|
-
const request = String(conversation.user_request ?? "").trim();
|
|
4028
|
-
const
|
|
4029
|
-
const afterPrompt =
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
const completionText =
|
|
4034
|
-
?
|
|
4035
|
-
|
|
4577
|
+
const request = String(nativeTakeover?.["terminal_bridge_request_text"] ?? conversation.user_request ?? "").trim();
|
|
4578
|
+
const promptEnd = request ? whitespaceInsensitiveMatchEnd(excerpt, request) : undefined;
|
|
4579
|
+
const afterPrompt = promptEnd === undefined ? undefined : excerpt.slice(promptEnd);
|
|
4580
|
+
const completionBoundary = afterPrompt === undefined
|
|
4581
|
+
? undefined
|
|
4582
|
+
: terminalBridgeCompletionBoundary(afterPrompt);
|
|
4583
|
+
const completionText = afterPrompt === undefined
|
|
4584
|
+
? screenChangedSinceSend
|
|
4585
|
+
? terminalBridgeLatestCompletedSegment(excerpt)
|
|
4586
|
+
: undefined
|
|
4587
|
+
: completionBoundary === undefined
|
|
4588
|
+
? afterPrompt
|
|
4589
|
+
: afterPrompt.slice(0, completionBoundary);
|
|
4590
|
+
if (completionText === undefined) {
|
|
4591
|
+
return undefined;
|
|
4592
|
+
}
|
|
4036
4593
|
const cleaned = cleanTerminalBridgeScreenText(completionText);
|
|
4037
|
-
const hasCompletionEvidence = completionBoundary !== undefined || /[•└]/u.test(cleaned ?? "");
|
|
4594
|
+
const hasCompletionEvidence = promptEnd === undefined || completionBoundary !== undefined || /[•└]/u.test(cleaned ?? "");
|
|
4038
4595
|
if (!cleaned || cleaned.length < 40 || !hasCompletionEvidence) {
|
|
4039
4596
|
return undefined;
|
|
4040
4597
|
}
|
|
@@ -4044,6 +4601,51 @@ function terminalBridgeScreenMessage({ conversation, terminalStatus }) {
|
|
|
4044
4601
|
timestamp: undefined
|
|
4045
4602
|
};
|
|
4046
4603
|
}
|
|
4604
|
+
function whitespaceInsensitiveMatchEnd(text, expected) {
|
|
4605
|
+
const normalizedExpected = expected.replace(/\s/gu, "");
|
|
4606
|
+
if (!normalizedExpected) {
|
|
4607
|
+
return undefined;
|
|
4608
|
+
}
|
|
4609
|
+
let normalizedText = "";
|
|
4610
|
+
const sourceEnds = [];
|
|
4611
|
+
for (let index = 0; index < text.length;) {
|
|
4612
|
+
const codePoint = text.codePointAt(index);
|
|
4613
|
+
if (codePoint === undefined) {
|
|
4614
|
+
break;
|
|
4615
|
+
}
|
|
4616
|
+
const character = String.fromCodePoint(codePoint);
|
|
4617
|
+
if (!/\s/u.test(character)) {
|
|
4618
|
+
normalizedText += character;
|
|
4619
|
+
for (let codeUnit = 0; codeUnit < character.length; codeUnit += 1) {
|
|
4620
|
+
sourceEnds.push(index + character.length);
|
|
4621
|
+
}
|
|
4622
|
+
}
|
|
4623
|
+
index += character.length;
|
|
4624
|
+
}
|
|
4625
|
+
const matchIndex = normalizedText.lastIndexOf(normalizedExpected);
|
|
4626
|
+
if (matchIndex < 0) {
|
|
4627
|
+
return undefined;
|
|
4628
|
+
}
|
|
4629
|
+
return sourceEnds[matchIndex + normalizedExpected.length - 1];
|
|
4630
|
+
}
|
|
4631
|
+
function terminalBridgeLatestCompletedSegment(text) {
|
|
4632
|
+
const matches = [...text.matchAll(/^[ \t]*[─━-]+\s+Worked for\b.*$/gmu)];
|
|
4633
|
+
const completion = matches.at(-1);
|
|
4634
|
+
if (completion?.index === undefined) {
|
|
4635
|
+
return undefined;
|
|
4636
|
+
}
|
|
4637
|
+
const previousCompletion = matches.at(-2);
|
|
4638
|
+
let start = previousCompletion?.index === undefined
|
|
4639
|
+
? 0
|
|
4640
|
+
: previousCompletion.index + previousCompletion[0].length;
|
|
4641
|
+
const beforeCompletion = text.slice(0, completion.index);
|
|
4642
|
+
const prompts = [...beforeCompletion.matchAll(/^[ \t]*›(?:\s|$).*$/gmu)];
|
|
4643
|
+
const latestPrompt = prompts.at(-1);
|
|
4644
|
+
if (latestPrompt?.index !== undefined && latestPrompt.index >= start) {
|
|
4645
|
+
start = latestPrompt.index + latestPrompt[0].length;
|
|
4646
|
+
}
|
|
4647
|
+
return text.slice(start, completion.index);
|
|
4648
|
+
}
|
|
4047
4649
|
function terminalBridgeCompletionBoundary(text) {
|
|
4048
4650
|
const matches = [...text.matchAll(/^[ \t]*[─━-]+\s+Worked for\b.*$/gmu)];
|
|
4049
4651
|
return matches.at(-1)?.index;
|
|
@@ -4417,7 +5019,7 @@ function acquireFileLock(lockPath, { timeoutMs = 5000, retryMs = 50 } = {}) {
|
|
|
4417
5019
|
throw error;
|
|
4418
5020
|
}
|
|
4419
5021
|
if (Date.now() - started >= timeoutMs) {
|
|
4420
|
-
throw new Error(`timed out waiting for
|
|
5022
|
+
throw new Error(`timed out waiting for file lock: ${lockPath}`);
|
|
4421
5023
|
}
|
|
4422
5024
|
sleepSync(retryMs);
|
|
4423
5025
|
}
|
|
@@ -4955,6 +5557,7 @@ function markConversationStalled({ statePath, logPath, reason, detail = {} }) {
|
|
|
4955
5557
|
}
|
|
4956
5558
|
const now = new Date().toISOString();
|
|
4957
5559
|
const executor = executorForConversation(conversation);
|
|
5560
|
+
const terminalBridge = terminalBridgeEnabled(conversation);
|
|
4958
5561
|
const shouldNotify = Boolean(conversation.gateway_method && !conversation.stalled_notification_sent_at);
|
|
4959
5562
|
stalledMessage = shouldNotify
|
|
4960
5563
|
? createMessage({
|
|
@@ -4968,7 +5571,9 @@ function markConversationStalled({ statePath, logPath, reason, detail = {} }) {
|
|
|
4968
5571
|
"",
|
|
4969
5572
|
`Conversation: ${conversation.conversation_id}`,
|
|
4970
5573
|
`Session: ${executor.session}`,
|
|
4971
|
-
|
|
5574
|
+
terminalBridge
|
|
5575
|
+
? `Use \`AKK status ${conversation.conversation_id}\` for details, \`AKK renew ${conversation.conversation_id}\` to resume monitoring without sending another task, or \`AKK close ${conversation.conversation_id}\` to close it.`
|
|
5576
|
+
: "Use `AKK status` for details, `AKK send` to retry/follow up, or `AKK close` to close it."
|
|
4972
5577
|
].join("\n")
|
|
4973
5578
|
})
|
|
4974
5579
|
: undefined;
|
|
@@ -5686,9 +6291,10 @@ function usage() {
|
|
|
5686
6291
|
agent-knock-knock list [--store-dir <dir>] [--agent ${agentList}] [--status <status>] [--all] [--managed-only] [--no-approval-scan] [--terminal-debug]
|
|
5687
6292
|
agent-knock-knock status --conversation <id> [--store-dir <dir>] [--trace]
|
|
5688
6293
|
agent-knock-knock describe --conversation <id> [--store-dir <dir>]
|
|
5689
|
-
agent-knock-knock send --conversation <id> --message <text> [--type answer|task|control] [--all-proxy <url>] [--agent-timeout-minutes <minutes>]
|
|
6294
|
+
agent-knock-knock send --conversation <id> --message <text> [--type answer|task|control] [--all-proxy <url>] [--agent-timeout-minutes <minutes>] [--agent-hard-timeout-minutes <minutes>]
|
|
5690
6295
|
agent-knock-knock approve --conversation <id>
|
|
5691
6296
|
agent-knock-knock cancel --conversation <id> [--all-proxy <url>]
|
|
6297
|
+
agent-knock-knock renew --conversation <id> [--minutes <inactivity-minutes>]
|
|
5692
6298
|
agent-knock-knock recover --conversation <id> [--session <name>] [--all-proxy <url>]
|
|
5693
6299
|
agent-knock-knock close --conversation <id> [--reason <text>]
|
|
5694
6300
|
agent-knock-knock install-openclaw [--openclaw-bin <path>] [--skill-path <path>] [--skill-only] [--no-restart]
|