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