@scotthuang/agent-knock-knock 0.11.1 → 0.11.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +1 -1
- package/dist/src/cli.js +815 -54
- package/dist/src/cli.js.map +1 -1
- package/dist/src/codex-store-adapter.d.ts +8 -1
- package/dist/src/codex-store-adapter.js +108 -19
- package/dist/src/codex-store-adapter.js.map +1 -1
- package/dist/src/openclaw-plugin.js +69 -1
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/dist/src/terminal-agent-adapter.d.ts +21 -2
- package/dist/src/terminal-agent-adapter.js.map +1 -1
- package/dist/src/terminal-agent-bridge.d.ts +7 -0
- package/dist/src/terminal-agent-bridge.js +31 -7
- package/dist/src/terminal-agent-bridge.js.map +1 -1
- package/dist/src/terminal-control-provider.d.ts +8 -0
- package/dist/src/terminal-control-provider.js +30 -1
- package/dist/src/terminal-control-provider.js.map +1 -1
- package/openclaw.plugin.json +4 -0
- package/package.json +1 -1
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +2 -1
package/dist/src/cli.js
CHANGED
|
@@ -19,12 +19,12 @@ import { appendEvent, assertStoreWriterCompatible, defaultStoreDir, ensureDir, e
|
|
|
19
19
|
import { createManagedSessionId, createNativeThreadTransitionId, isExactNativeThreadId, managedSessionBindingToken, nativeThreadCommandFingerprint, terminalBindingFrom, unmanagedTerminalBindingToken } from "./managed-session.js";
|
|
20
20
|
import { classifyCodexLifecyclePostcondition, evaluateResumeCandidateAvailability, hasStrongCodexLifecycleIdentity, isFreshCodexPostProbeScreen } from "./native-thread-lifecycle-policy.js";
|
|
21
21
|
import { createNativeThreadResumeSnapshot, nativeThreadCandidateSnapshotFingerprint, nativeThreadResumeSnapshotRowsMatchCandidates, resolveNativeThreadResumeSelection, saveNativeThreadResumeSnapshot, sortNativeThreadCandidates, terminalActionFingerprint, verifiedPreviousResumeCandidate } from "./native-thread-resume-snapshot.js";
|
|
22
|
-
import { listManagedSessions, loadManagedSession, loadNativeThreadTransition, saveManagedSession, saveNativeThreadTransition, tryLoadManagedSession } from "./session-store.js";
|
|
22
|
+
import { listManagedSessions, loadManagedSession, loadNativeThreadTransition, nativeThreadTransitionsDir, saveManagedSession, saveNativeThreadTransition, tryLoadManagedSession } from "./session-store.js";
|
|
23
23
|
import { StaticTerminalControlProvider, TmuxTerminalControlProvider, terminalPaneContainsProcess } from "./terminal-control-provider.js";
|
|
24
24
|
import { parseTerminalConversationId } from "./terminal-agent-adapter.js";
|
|
25
25
|
import { createProductionTerminalAgentRegistry } from "./terminal-agent-registry.js";
|
|
26
26
|
import { parseProcessElapsedSeconds, StaticTerminalProcessSource, SystemTerminalProcessSource } from "./terminal-process-source.js";
|
|
27
|
-
import { TerminalAgentBridge } from "./terminal-agent-bridge.js";
|
|
27
|
+
import { TerminalAgentBridge, TerminalInputNotStartedError } from "./terminal-agent-bridge.js";
|
|
28
28
|
import { evaluateApprovalPolicy } from "./approval-policy.js";
|
|
29
29
|
import { evaluateDoctorCapabilities, runDoctorCapabilityProbes } from "./doctor-capabilities.js";
|
|
30
30
|
import { runOpenClawChainDiagnostics } from "./openclaw-doctor.js";
|
|
@@ -124,7 +124,8 @@ const STORE_MUTATION_COMMANDS = new Set([
|
|
|
124
124
|
"monitor",
|
|
125
125
|
"new-thread",
|
|
126
126
|
"clear-thread",
|
|
127
|
-
"resume-thread"
|
|
127
|
+
"resume-thread",
|
|
128
|
+
"reconcile-binding"
|
|
128
129
|
]);
|
|
129
130
|
class TurnBindingSupersededError extends Error {
|
|
130
131
|
code = "AKK_TURN_BINDING_SUPERSEDED";
|
|
@@ -258,6 +259,9 @@ async function runCommand(commandName, options) {
|
|
|
258
259
|
else if (commandName === "resume-thread") {
|
|
259
260
|
await runResumeThread(options);
|
|
260
261
|
}
|
|
262
|
+
else if (commandName === "reconcile-binding") {
|
|
263
|
+
await runReconcileBinding(options);
|
|
264
|
+
}
|
|
261
265
|
else if (commandName === "respond") {
|
|
262
266
|
await runRespond(options);
|
|
263
267
|
}
|
|
@@ -2755,6 +2759,20 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
|
|
|
2755
2759
|
});
|
|
2756
2760
|
}
|
|
2757
2761
|
}
|
|
2762
|
+
const statusCardObservation = session.agent === "codex" &&
|
|
2763
|
+
typeof terminalState.screen_excerpt === "string"
|
|
2764
|
+
? bridge.registry.require("codex").observeThreadLifecycle?.({
|
|
2765
|
+
operation: { kind: "new_thread" },
|
|
2766
|
+
phase: "before",
|
|
2767
|
+
screen: terminalState.screen_excerpt
|
|
2768
|
+
})
|
|
2769
|
+
: undefined;
|
|
2770
|
+
const statusCardNativeThreadId = statusCardObservation?.status === "observed" &&
|
|
2771
|
+
terminalState.activity_state === "idle" &&
|
|
2772
|
+
terminalState.approval_state.blocked !== true &&
|
|
2773
|
+
isExactNativeThreadId(statusCardObservation.nativeThreadId)
|
|
2774
|
+
? statusCardObservation.nativeThreadId
|
|
2775
|
+
: undefined;
|
|
2758
2776
|
const agentVersion = agentVersionForRunningProcess(session.agent, session.pid, options);
|
|
2759
2777
|
const lifecycleCapability = bridge.registry.require(session.agent)
|
|
2760
2778
|
.probeThreadLifecycle?.(agentVersion) ?? {
|
|
@@ -2790,6 +2808,7 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
|
|
|
2790
2808
|
workspace: session.cwd,
|
|
2791
2809
|
elapsed: session.elapsed,
|
|
2792
2810
|
native_agent_session_id: nativeAgentIdentity?.sessionId,
|
|
2811
|
+
native_agent_status_card_session_id: statusCardNativeThreadId,
|
|
2793
2812
|
native_agent_process_uuid: nativeProcessUuid,
|
|
2794
2813
|
native_agent_process_birth: nativeProcessBirth,
|
|
2795
2814
|
native_agent_rollout: nativeAgentIdentity?.rollout,
|
|
@@ -2863,6 +2882,16 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
2863
2882
|
? [...(sessionsByTerminal.get(terminalKey) ?? [])]
|
|
2864
2883
|
: [];
|
|
2865
2884
|
const matchingSessions = relatedSessions.filter((session) => managedSessionMatchesLiveTerminalEntry(session, terminal, storeDir));
|
|
2885
|
+
const conflictingBoundSessionClaims = relatedSessions.flatMap((session) => {
|
|
2886
|
+
const kind = managedBindingConflictKindForLiveTerminalEntry({
|
|
2887
|
+
storeDir,
|
|
2888
|
+
session,
|
|
2889
|
+
terminal
|
|
2890
|
+
});
|
|
2891
|
+
return kind && kind !== "stale_process_incarnation"
|
|
2892
|
+
? [{ session, kind }]
|
|
2893
|
+
: [];
|
|
2894
|
+
});
|
|
2866
2895
|
const unresolvedSessionClaims = relatedSessions.filter((session) => ["transitioning", "quarantined"].includes(session.status) &&
|
|
2867
2896
|
managedSessionClaimsLiveTerminalEntry(session, terminal));
|
|
2868
2897
|
const sessionAuthorityConflict = unresolvedSessionClaims.length > 0
|
|
@@ -2873,13 +2902,42 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
2873
2902
|
transition_ids: unresolvedSessionClaims.map((session) => session.last_transition_id ?? null),
|
|
2874
2903
|
recovery: "use only the lifecycle recovery action listed for this terminal"
|
|
2875
2904
|
}
|
|
2876
|
-
:
|
|
2905
|
+
: conflictingBoundSessionClaims.length === 1
|
|
2877
2906
|
? {
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2907
|
+
kind: conflictingBoundSessionClaims[0].kind,
|
|
2908
|
+
reason: conflictingBoundSessionClaims[0].kind === "provisional_orphan"
|
|
2909
|
+
? "a failed raw attach left a bound Session without an authoritative native-thread identity"
|
|
2910
|
+
: conflictingBoundSessionClaims[0].kind ===
|
|
2911
|
+
"live_external_thread_change"
|
|
2912
|
+
? "the live coding-agent thread changed outside AKK while its previous Session binding remained bound"
|
|
2913
|
+
: "the live terminal no longer matches a bound managed Session and the process relationship is unverifiable",
|
|
2914
|
+
session_ids: [
|
|
2915
|
+
conflictingBoundSessionClaims[0].session.session_id
|
|
2916
|
+
],
|
|
2917
|
+
binding_ids: [
|
|
2918
|
+
conflictingBoundSessionClaims[0].session.binding?.binding_id ?? null
|
|
2919
|
+
],
|
|
2920
|
+
session_revisions: [
|
|
2921
|
+
conflictingBoundSessionClaims[0].session.revision ?? null
|
|
2922
|
+
],
|
|
2923
|
+
recovery: conflictingBoundSessionClaims[0].kind === "unverifiable"
|
|
2924
|
+
? "inspect the terminal and Session identity; AKK cannot safely reconcile an unverifiable binding"
|
|
2925
|
+
: "use only the exact reconcile_binding action listed for this terminal"
|
|
2881
2926
|
}
|
|
2882
|
-
:
|
|
2927
|
+
: conflictingBoundSessionClaims.length > 1
|
|
2928
|
+
? {
|
|
2929
|
+
kind: "ambiguous_bound_claims",
|
|
2930
|
+
reason: "multiple non-exact bound managed Sessions claim the same live terminal",
|
|
2931
|
+
session_ids: conflictingBoundSessionClaims.map(({ session }) => session.session_id),
|
|
2932
|
+
recovery: "inspect Session state; AKK will not reconcile ambiguous claims"
|
|
2933
|
+
}
|
|
2934
|
+
: matchingSessions.length > 1
|
|
2935
|
+
? {
|
|
2936
|
+
reason: "multiple first-class managed Sessions claim the same live terminal binding",
|
|
2937
|
+
session_ids: matchingSessions.map((session) => session.session_id),
|
|
2938
|
+
recovery: "inspect Session state before performing a side effect"
|
|
2939
|
+
}
|
|
2940
|
+
: undefined;
|
|
2883
2941
|
const authoritativeSession = matchingSessions[0];
|
|
2884
2942
|
const discoveredOwnership = terminalControl
|
|
2885
2943
|
? terminalDispatchOwnership(terminalControl)
|
|
@@ -2918,6 +2976,40 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
2918
2976
|
const sessionAwareRawActions = authoritativeSession
|
|
2919
2977
|
? actionsForManagedSessionBinding(rawActions, authoritativeSession)
|
|
2920
2978
|
: rawActions;
|
|
2979
|
+
const soleBindingConflict = conflictingBoundSessionClaims.length === 1
|
|
2980
|
+
? conflictingBoundSessionClaims[0]
|
|
2981
|
+
: undefined;
|
|
2982
|
+
const conflictingSessionRevision = Number(soleBindingConflict?.session.revision);
|
|
2983
|
+
const conflictingSessionTurns = soleBindingConflict
|
|
2984
|
+
? managedTurnsForSession(storeDir, soleBindingConflict.session.session_id)
|
|
2985
|
+
: [];
|
|
2986
|
+
const expectedTerminalToken = stringValue(terminal.lifecycle_binding_token);
|
|
2987
|
+
const reconcileBindingAction = mutationsAllowed &&
|
|
2988
|
+
discoveredOwnership.state === "none" &&
|
|
2989
|
+
unresolvedSessionClaims.length === 0 &&
|
|
2990
|
+
matchingSessions.length === 0 &&
|
|
2991
|
+
soleBindingConflict &&
|
|
2992
|
+
soleBindingConflict.kind !== "unverifiable" &&
|
|
2993
|
+
Number.isSafeInteger(conflictingSessionRevision) &&
|
|
2994
|
+
conflictingSessionRevision > 0 &&
|
|
2995
|
+
expectedTerminalToken &&
|
|
2996
|
+
terminal.activity_state === "idle" &&
|
|
2997
|
+
!(isRecord(terminal.approval_state) &&
|
|
2998
|
+
terminal.approval_state.blocked === true) &&
|
|
2999
|
+
!conflictingSessionTurns.some((turn) => SESSION_SEND_BLOCKING_STATUSES.has(turn.status)) &&
|
|
3000
|
+
!managedSessionHasUnresolvedNativeTransition(storeDir, soleBindingConflict.session)
|
|
3001
|
+
? {
|
|
3002
|
+
tool: "agent_knock_knock_reconcile_binding",
|
|
3003
|
+
arguments: {
|
|
3004
|
+
terminal_id: stringValue(terminal.id),
|
|
3005
|
+
conflicting_session_id: soleBindingConflict.session.session_id,
|
|
3006
|
+
expected_session_revision: conflictingSessionRevision,
|
|
3007
|
+
expected_binding_token: managedSessionBindingToken(soleBindingConflict.session),
|
|
3008
|
+
expected_terminal_token: expectedTerminalToken
|
|
3009
|
+
},
|
|
3010
|
+
requires_user_intent: true
|
|
3011
|
+
}
|
|
3012
|
+
: undefined;
|
|
2921
3013
|
const terminalCanAcceptSend = ownership.state === "none" && isRecord(sessionAwareRawActions.send);
|
|
2922
3014
|
if (ownership.state === "current" &&
|
|
2923
3015
|
!allRelated.some((conversation) => conversation.conversation_id === ownership.conversation.conversation_id)) {
|
|
@@ -3004,7 +3096,12 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3004
3096
|
const availableActions = ownership.state === "current"
|
|
3005
3097
|
? currentTerminalActions(currentTurn)
|
|
3006
3098
|
: ownership.state === "conflict"
|
|
3007
|
-
?
|
|
3099
|
+
? {
|
|
3100
|
+
...safeTerminalActionsDuringConflict(sessionAwareRawActions),
|
|
3101
|
+
...(reconcileBindingAction
|
|
3102
|
+
? { reconcile_binding: reconcileBindingAction }
|
|
3103
|
+
: {})
|
|
3104
|
+
}
|
|
3008
3105
|
: managedSessionId &&
|
|
3009
3106
|
sessionBindingMatchesLiveTerminal &&
|
|
3010
3107
|
isRecord(sessionAwareRawActions.send)
|
|
@@ -3128,6 +3225,13 @@ function managedSessionMatchesLiveTerminalEntry(session, terminal, storeDir) {
|
|
|
3128
3225
|
return false;
|
|
3129
3226
|
}
|
|
3130
3227
|
const liveThreadId = stringValue(terminal.native_agent_session_id);
|
|
3228
|
+
const statusCardThreadId = stringValue(terminal.native_agent_status_card_session_id);
|
|
3229
|
+
if (isExactNativeThreadId(binding.native_thread_id) &&
|
|
3230
|
+
isExactNativeThreadId(statusCardThreadId) &&
|
|
3231
|
+
binding.native_thread_id.toLowerCase() !==
|
|
3232
|
+
statusCardThreadId.toLowerCase()) {
|
|
3233
|
+
return false;
|
|
3234
|
+
}
|
|
3131
3235
|
if (!liveThreadId) {
|
|
3132
3236
|
return Boolean(session.agent === "codex" &&
|
|
3133
3237
|
binding.native_thread_id &&
|
|
@@ -3186,11 +3290,116 @@ function managedSessionClaimsLiveTerminalEntry(session, terminal) {
|
|
|
3186
3290
|
: undefined;
|
|
3187
3291
|
return Boolean(binding &&
|
|
3188
3292
|
session.agent === terminal.agent &&
|
|
3189
|
-
binding.terminal_id === stringValue(terminal.id) &&
|
|
3190
3293
|
binding.native_process.pid === Number(terminal.pid) &&
|
|
3191
3294
|
terminalControlSelectorKey(binding.terminal_control) ===
|
|
3192
|
-
terminalControlSelectorKey(liveControl)
|
|
3193
|
-
|
|
3295
|
+
terminalControlSelectorKey(liveControl));
|
|
3296
|
+
}
|
|
3297
|
+
function listedTerminalProcessIncarnation(terminal) {
|
|
3298
|
+
const processUuid = stringValue(terminal.native_agent_process_uuid);
|
|
3299
|
+
const processBirth = stringValue(terminal.native_agent_process_birth);
|
|
3300
|
+
if (terminal.agent !== "codex" ||
|
|
3301
|
+
(processUuid && processBirth)) {
|
|
3302
|
+
return { processUuid, processBirth };
|
|
3303
|
+
}
|
|
3304
|
+
const pid = Number(terminal.pid);
|
|
3305
|
+
if (!Number.isSafeInteger(pid) || pid <= 1) {
|
|
3306
|
+
return { processUuid, processBirth };
|
|
3307
|
+
}
|
|
3308
|
+
try {
|
|
3309
|
+
const incarnation = codexProcessIncarnationForPid(pid);
|
|
3310
|
+
return {
|
|
3311
|
+
processUuid: processUuid ?? incarnation.processUuid,
|
|
3312
|
+
processBirth: processBirth ?? incarnation.processBirth
|
|
3313
|
+
};
|
|
3314
|
+
}
|
|
3315
|
+
catch {
|
|
3316
|
+
return { processUuid, processBirth };
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
function managedBindingConflictKindForLiveTerminalEntry({ storeDir, session, terminal }) {
|
|
3320
|
+
const binding = session.binding;
|
|
3321
|
+
if (session.status !== "bound" ||
|
|
3322
|
+
!binding ||
|
|
3323
|
+
!managedSessionClaimsLiveTerminalEntry(session, terminal) ||
|
|
3324
|
+
managedSessionMatchesLiveTerminalEntry(session, terminal, storeDir)) {
|
|
3325
|
+
return undefined;
|
|
3326
|
+
}
|
|
3327
|
+
const livePid = Number(terminal.pid);
|
|
3328
|
+
const incarnation = listedTerminalProcessIncarnation(terminal);
|
|
3329
|
+
const relationship = processIncarnationRelationship({
|
|
3330
|
+
binding,
|
|
3331
|
+
livePid,
|
|
3332
|
+
liveProcessUuid: incarnation.processUuid,
|
|
3333
|
+
liveProcessBirth: incarnation.processBirth
|
|
3334
|
+
});
|
|
3335
|
+
if (relationship === "different") {
|
|
3336
|
+
return "stale_process_incarnation";
|
|
3337
|
+
}
|
|
3338
|
+
if (binding.terminal_id !== stringValue(terminal.id) ||
|
|
3339
|
+
!matchesConfiguredWorkspace(session.workspace, terminal.workspace ?? terminal.cwd)) {
|
|
3340
|
+
return "unverifiable";
|
|
3341
|
+
}
|
|
3342
|
+
const statusCardThreadId = stringValue(terminal.native_agent_status_card_session_id);
|
|
3343
|
+
const liveNativeThreadId = stringValue(terminal.native_agent_session_id);
|
|
3344
|
+
if (isExactNativeThreadId(binding.native_thread_id) &&
|
|
3345
|
+
isExactNativeThreadId(statusCardThreadId) &&
|
|
3346
|
+
binding.native_thread_id.toLowerCase() !==
|
|
3347
|
+
statusCardThreadId.toLowerCase()) {
|
|
3348
|
+
return relationship === "same" &&
|
|
3349
|
+
isExactNativeThreadId(liveNativeThreadId) &&
|
|
3350
|
+
liveNativeThreadId.toLowerCase() ===
|
|
3351
|
+
statusCardThreadId.toLowerCase()
|
|
3352
|
+
? "live_external_thread_change"
|
|
3353
|
+
: "unverifiable";
|
|
3354
|
+
}
|
|
3355
|
+
if (session.lineage.created_by === "attach" &&
|
|
3356
|
+
!session.last_transition_id &&
|
|
3357
|
+
!binding.native_thread_id &&
|
|
3358
|
+
!binding.native_process.rollout &&
|
|
3359
|
+
managedTurnsForSession(storeDir, session.session_id).length === 0) {
|
|
3360
|
+
return "provisional_orphan";
|
|
3361
|
+
}
|
|
3362
|
+
if (relationship === "same" &&
|
|
3363
|
+
isExactNativeThreadId(binding.native_thread_id) &&
|
|
3364
|
+
isExactNativeThreadId(liveNativeThreadId) &&
|
|
3365
|
+
binding.native_thread_id.toLowerCase() !==
|
|
3366
|
+
liveNativeThreadId.toLowerCase()) {
|
|
3367
|
+
return "live_external_thread_change";
|
|
3368
|
+
}
|
|
3369
|
+
return "unverifiable";
|
|
3370
|
+
}
|
|
3371
|
+
function managedSessionHasUnresolvedNativeTransition(storeDir, session) {
|
|
3372
|
+
const root = nativeThreadTransitionsDir(storeDir);
|
|
3373
|
+
if (!fs.existsSync(root)) {
|
|
3374
|
+
return false;
|
|
3375
|
+
}
|
|
3376
|
+
let entries;
|
|
3377
|
+
try {
|
|
3378
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
3379
|
+
}
|
|
3380
|
+
catch {
|
|
3381
|
+
return true;
|
|
3382
|
+
}
|
|
3383
|
+
for (const entry of entries) {
|
|
3384
|
+
if (!entry.isDirectory()) {
|
|
3385
|
+
continue;
|
|
3386
|
+
}
|
|
3387
|
+
let transition;
|
|
3388
|
+
try {
|
|
3389
|
+
transition = loadNativeThreadTransition(storeDir, entry.name);
|
|
3390
|
+
}
|
|
3391
|
+
catch {
|
|
3392
|
+
return true;
|
|
3393
|
+
}
|
|
3394
|
+
if (transition.source_session_id !== session.session_id &&
|
|
3395
|
+
transition.target_session_id !== session.session_id) {
|
|
3396
|
+
continue;
|
|
3397
|
+
}
|
|
3398
|
+
if (!["committed", "aborted"].includes(transition.status)) {
|
|
3399
|
+
return true;
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
return false;
|
|
3194
3403
|
}
|
|
3195
3404
|
function terminalKeyForManagedConversation(conversation) {
|
|
3196
3405
|
return terminalControlSelectorKey(terminalControlFromTakeover(isRecord(conversation.native_session_takeover)
|
|
@@ -3577,7 +3786,10 @@ async function listStateForTerminal(agent, terminalControl, options, bridge = cr
|
|
|
3577
3786
|
},
|
|
3578
3787
|
activity_state: status.activity_state,
|
|
3579
3788
|
activity_reason: status.activity_reason,
|
|
3580
|
-
capability_limitation: status.capability_limitation
|
|
3789
|
+
capability_limitation: status.capability_limitation,
|
|
3790
|
+
// Internal projection evidence; terminalControlledListEntry selects all
|
|
3791
|
+
// public fields explicitly and never exposes the pane excerpt itself.
|
|
3792
|
+
screen_excerpt: status.screen.excerpt
|
|
3581
3793
|
};
|
|
3582
3794
|
}
|
|
3583
3795
|
catch (error) {
|
|
@@ -3651,11 +3863,12 @@ function managedListApprovalState(conversation) {
|
|
|
3651
3863
|
}
|
|
3652
3864
|
function listActionContracts() {
|
|
3653
3865
|
return {
|
|
3654
|
-
version:
|
|
3866
|
+
version: 6,
|
|
3655
3867
|
instructions: [
|
|
3656
3868
|
"Treat terminals[] as the primary resource and use only actions present in available_actions.",
|
|
3657
3869
|
"An existing managed session's ordinary send targets session_id and creates a new turn. A turn id is never an ordinary send target.",
|
|
3658
3870
|
"Read-only native-thread listing targets an exact terminal_id. Native-thread new/resume mutations also use the listed expected_binding_token and never create a Turn.",
|
|
3871
|
+
"A binding conflict may be detached only through its exact reconcile_binding action, which is snapshot-bound to the Session revision, binding, and live terminal identity and never adopts a replacement native thread.",
|
|
3659
3872
|
"List resumable threads before resume; use only a complete native_thread_id and the action returned for that candidate.",
|
|
3660
3873
|
"For first attach only, use a discovery selector explicitly named by the user or the selector prefilled by that unmanaged raw-terminal row's available send action; never infer, guess, or reuse one.",
|
|
3661
3874
|
"Use respond only for an in-flight turn that is explicitly waiting for OpenClaw.",
|
|
@@ -3734,6 +3947,21 @@ function listActionContracts() {
|
|
|
3734
3947
|
requires_user_intent: true,
|
|
3735
3948
|
candidate_source: "list_resumable_threads"
|
|
3736
3949
|
},
|
|
3950
|
+
reconcile_binding: {
|
|
3951
|
+
tool: "agent_knock_knock_reconcile_binding",
|
|
3952
|
+
target_argument: "terminal_id",
|
|
3953
|
+
required: [
|
|
3954
|
+
"terminal_id",
|
|
3955
|
+
"conflicting_session_id",
|
|
3956
|
+
"expected_session_revision",
|
|
3957
|
+
"expected_binding_token",
|
|
3958
|
+
"expected_terminal_token"
|
|
3959
|
+
],
|
|
3960
|
+
creates_turn: false,
|
|
3961
|
+
sends_terminal_input: false,
|
|
3962
|
+
requires_user_intent: true,
|
|
3963
|
+
effect: "Detach one exactly listed conflicting Session binding without adopting the live replacement thread."
|
|
3964
|
+
},
|
|
3737
3965
|
respond: {
|
|
3738
3966
|
tool: "agent_knock_knock_respond",
|
|
3739
3967
|
target_argument: "turn_id",
|
|
@@ -4266,6 +4494,83 @@ function codexProcessIncarnationForPid(pid) {
|
|
|
4266
4494
|
evidence: "codex_process_birth"
|
|
4267
4495
|
};
|
|
4268
4496
|
}
|
|
4497
|
+
function processIncarnationRelationship({ binding, livePid, liveProcessUuid, liveProcessBirth }) {
|
|
4498
|
+
if (binding.native_process.pid !== livePid) {
|
|
4499
|
+
return "different";
|
|
4500
|
+
}
|
|
4501
|
+
const comparisons = [];
|
|
4502
|
+
if (binding.native_process.process_uuid && liveProcessUuid) {
|
|
4503
|
+
comparisons.push(binding.native_process.process_uuid === liveProcessUuid);
|
|
4504
|
+
}
|
|
4505
|
+
if (binding.native_process.process_birth && liveProcessBirth) {
|
|
4506
|
+
comparisons.push(binding.native_process.process_birth === liveProcessBirth);
|
|
4507
|
+
}
|
|
4508
|
+
if (comparisons.length === 0) {
|
|
4509
|
+
return "unverifiable";
|
|
4510
|
+
}
|
|
4511
|
+
if (comparisons.every(Boolean)) {
|
|
4512
|
+
return "same";
|
|
4513
|
+
}
|
|
4514
|
+
if (comparisons.every((value) => !value)) {
|
|
4515
|
+
return "different";
|
|
4516
|
+
}
|
|
4517
|
+
return "unverifiable";
|
|
4518
|
+
}
|
|
4519
|
+
function resolvedTerminalProcessIncarnation(terminal, identity) {
|
|
4520
|
+
if (terminal.agent !== "codex" ||
|
|
4521
|
+
(identity?.processUuid && identity.processBirth)) {
|
|
4522
|
+
return {
|
|
4523
|
+
processUuid: identity?.processUuid,
|
|
4524
|
+
processBirth: identity?.processBirth
|
|
4525
|
+
};
|
|
4526
|
+
}
|
|
4527
|
+
try {
|
|
4528
|
+
const incarnation = codexProcessIncarnationForPid(terminal.pid);
|
|
4529
|
+
return {
|
|
4530
|
+
processUuid: identity?.processUuid ?? incarnation.processUuid,
|
|
4531
|
+
processBirth: identity?.processBirth ?? incarnation.processBirth
|
|
4532
|
+
};
|
|
4533
|
+
}
|
|
4534
|
+
catch {
|
|
4535
|
+
return {
|
|
4536
|
+
processUuid: identity?.processUuid,
|
|
4537
|
+
processBirth: identity?.processBirth
|
|
4538
|
+
};
|
|
4539
|
+
}
|
|
4540
|
+
}
|
|
4541
|
+
function managedSessionOwnerIsConclusivelyInactive({ session, terminal, identity }) {
|
|
4542
|
+
const binding = session.binding;
|
|
4543
|
+
if (!binding) {
|
|
4544
|
+
return false;
|
|
4545
|
+
}
|
|
4546
|
+
if (binding.native_process.pid !== terminal.pid) {
|
|
4547
|
+
if (!isProcessAlive(binding.native_process.pid)) {
|
|
4548
|
+
return true;
|
|
4549
|
+
}
|
|
4550
|
+
if (session.agent !== "codex") {
|
|
4551
|
+
return false;
|
|
4552
|
+
}
|
|
4553
|
+
try {
|
|
4554
|
+
const ownerIncarnation = codexProcessIncarnationForPid(binding.native_process.pid);
|
|
4555
|
+
return processIncarnationRelationship({
|
|
4556
|
+
binding,
|
|
4557
|
+
livePid: binding.native_process.pid,
|
|
4558
|
+
liveProcessUuid: ownerIncarnation.processUuid,
|
|
4559
|
+
liveProcessBirth: ownerIncarnation.processBirth
|
|
4560
|
+
}) === "different";
|
|
4561
|
+
}
|
|
4562
|
+
catch {
|
|
4563
|
+
return false;
|
|
4564
|
+
}
|
|
4565
|
+
}
|
|
4566
|
+
const incarnation = resolvedTerminalProcessIncarnation(terminal, identity);
|
|
4567
|
+
return processIncarnationRelationship({
|
|
4568
|
+
binding,
|
|
4569
|
+
livePid: terminal.pid,
|
|
4570
|
+
liveProcessUuid: incarnation.processUuid,
|
|
4571
|
+
liveProcessBirth: incarnation.processBirth
|
|
4572
|
+
}) === "different";
|
|
4573
|
+
}
|
|
4269
4574
|
function isCodexStatusCardEvidence(evidence) {
|
|
4270
4575
|
return evidence.split("+").includes("codex_status_card");
|
|
4271
4576
|
}
|
|
@@ -4749,9 +5054,15 @@ function boundManagedSessionForTerminal({ storeDir, terminal, identity }) {
|
|
|
4749
5054
|
}
|
|
4750
5055
|
const conflicting = sessions.filter((session) => session.status === "bound" &&
|
|
4751
5056
|
session.binding &&
|
|
5057
|
+
session.agent === terminal.agent &&
|
|
4752
5058
|
terminalControlSelectorKey(session.binding.terminal_control) ===
|
|
4753
5059
|
terminalControlSelectorKey(terminal.terminalControl) &&
|
|
4754
5060
|
session.binding.native_process.pid === terminal.pid &&
|
|
5061
|
+
!managedSessionOwnerIsConclusivelyInactive({
|
|
5062
|
+
session,
|
|
5063
|
+
terminal,
|
|
5064
|
+
identity
|
|
5065
|
+
}) &&
|
|
4755
5066
|
!exact.includes(session));
|
|
4756
5067
|
if (conflicting.length > 0) {
|
|
4757
5068
|
throw new Error(`terminal ${terminal.terminalControl.target} changed native thread outside AKK; ` +
|
|
@@ -4759,6 +5070,49 @@ function boundManagedSessionForTerminal({ storeDir, terminal, identity }) {
|
|
|
4759
5070
|
}
|
|
4760
5071
|
return exact[0];
|
|
4761
5072
|
}
|
|
5073
|
+
function managedBindingConflictKindForResolvedTerminal({ storeDir, session, terminal, identity }) {
|
|
5074
|
+
const binding = session.binding;
|
|
5075
|
+
if (session.status !== "bound" ||
|
|
5076
|
+
!binding ||
|
|
5077
|
+
session.agent !== terminal.agent ||
|
|
5078
|
+
binding.terminal_id !== terminal.conversationId ||
|
|
5079
|
+
binding.native_process.pid !== terminal.pid ||
|
|
5080
|
+
terminalControlSelectorKey(binding.terminal_control) !==
|
|
5081
|
+
terminalControlSelectorKey(terminal.terminalControl) ||
|
|
5082
|
+
!matchesConfiguredWorkspace(session.workspace, terminal.terminalControl.currentPath) ||
|
|
5083
|
+
bindingMatchesLiveTerminal(session, terminal, identity, storeDir)) {
|
|
5084
|
+
return undefined;
|
|
5085
|
+
}
|
|
5086
|
+
if (managedSessionOwnerIsConclusivelyInactive({
|
|
5087
|
+
session,
|
|
5088
|
+
terminal,
|
|
5089
|
+
identity
|
|
5090
|
+
})) {
|
|
5091
|
+
return "stale_process_incarnation";
|
|
5092
|
+
}
|
|
5093
|
+
if (session.lineage.created_by === "attach" &&
|
|
5094
|
+
!session.last_transition_id &&
|
|
5095
|
+
!binding.native_thread_id &&
|
|
5096
|
+
!binding.native_process.rollout &&
|
|
5097
|
+
managedTurnsForSession(storeDir, session.session_id).length === 0) {
|
|
5098
|
+
return "provisional_orphan";
|
|
5099
|
+
}
|
|
5100
|
+
const incarnation = resolvedTerminalProcessIncarnation(terminal, identity);
|
|
5101
|
+
const relationship = processIncarnationRelationship({
|
|
5102
|
+
binding,
|
|
5103
|
+
livePid: terminal.pid,
|
|
5104
|
+
liveProcessUuid: incarnation.processUuid,
|
|
5105
|
+
liveProcessBirth: incarnation.processBirth
|
|
5106
|
+
});
|
|
5107
|
+
if (relationship === "same" &&
|
|
5108
|
+
isExactNativeThreadId(binding.native_thread_id) &&
|
|
5109
|
+
isExactNativeThreadId(identity?.sessionId) &&
|
|
5110
|
+
binding.native_thread_id.toLowerCase() !==
|
|
5111
|
+
identity.sessionId.toLowerCase()) {
|
|
5112
|
+
return "live_external_thread_change";
|
|
5113
|
+
}
|
|
5114
|
+
return "unverifiable";
|
|
5115
|
+
}
|
|
4762
5116
|
function soleBoundManagedSessionClaimForTerminal(storeDir, terminal) {
|
|
4763
5117
|
const claims = listManagedSessions(storeDir).filter((session) => session.status === "bound" &&
|
|
4764
5118
|
session.binding &&
|
|
@@ -4767,7 +5121,11 @@ function soleBoundManagedSessionClaimForTerminal(storeDir, terminal) {
|
|
|
4767
5121
|
session.binding.native_process.pid === terminal.pid &&
|
|
4768
5122
|
terminalControlSelectorKey(session.binding.terminal_control) ===
|
|
4769
5123
|
terminalControlSelectorKey(terminal.terminalControl) &&
|
|
4770
|
-
matchesConfiguredWorkspace(session.workspace, terminal.terminalControl.currentPath)
|
|
5124
|
+
matchesConfiguredWorkspace(session.workspace, terminal.terminalControl.currentPath) &&
|
|
5125
|
+
!managedSessionOwnerIsConclusivelyInactive({
|
|
5126
|
+
session,
|
|
5127
|
+
terminal
|
|
5128
|
+
}));
|
|
4771
5129
|
if (claims.length > 1) {
|
|
4772
5130
|
throw new Error(`terminal ${terminal.terminalControl.target} has multiple bound managed Session claims`);
|
|
4773
5131
|
}
|
|
@@ -4775,15 +5133,18 @@ function soleBoundManagedSessionClaimForTerminal(storeDir, terminal) {
|
|
|
4775
5133
|
}
|
|
4776
5134
|
function createBoundManagedSession({ sessionId, terminal, identity, nativeThreadId = identity?.sessionId, evidence = identity?.evidence ?? "native_thread_boundary", generation = 1, lineage, now = new Date() }) {
|
|
4777
5135
|
const workspace = terminal.terminalControl.currentPath ?? process.cwd();
|
|
5136
|
+
const codexIncarnation = terminal.agent === "codex" && !identity
|
|
5137
|
+
? codexProcessIncarnationForPid(terminal.pid)
|
|
5138
|
+
: undefined;
|
|
4778
5139
|
const binding = terminalBindingFrom({
|
|
4779
5140
|
terminalId: terminal.conversationId,
|
|
4780
5141
|
terminalControl: terminal.terminalControl,
|
|
4781
5142
|
pid: terminal.pid,
|
|
4782
5143
|
nativeThreadId,
|
|
4783
|
-
processUuid: identity?.processUuid,
|
|
4784
|
-
processBirth: identity?.processBirth,
|
|
5144
|
+
processUuid: identity?.processUuid ?? codexIncarnation?.processUuid,
|
|
5145
|
+
processBirth: identity?.processBirth ?? codexIncarnation?.processBirth,
|
|
4785
5146
|
rollout: identity?.rollout,
|
|
4786
|
-
evidence,
|
|
5147
|
+
evidence: identity?.evidence ?? codexIncarnation?.evidence ?? evidence,
|
|
4787
5148
|
generation,
|
|
4788
5149
|
now
|
|
4789
5150
|
});
|
|
@@ -4865,8 +5226,11 @@ async function reattachManagedSessionForNativeIdentity({ options, terminal, iden
|
|
|
4865
5226
|
});
|
|
4866
5227
|
const previousPid = existing.binding.native_process.pid;
|
|
4867
5228
|
if (existing.status === "bound" &&
|
|
4868
|
-
(
|
|
4869
|
-
|
|
5229
|
+
!managedSessionOwnerIsConclusivelyInactive({
|
|
5230
|
+
session: existing,
|
|
5231
|
+
terminal,
|
|
5232
|
+
identity
|
|
5233
|
+
})) {
|
|
4870
5234
|
throw new Error(`managed Session ${existing.session_id} is still bound to process ${previousPid}`);
|
|
4871
5235
|
}
|
|
4872
5236
|
const now = new Date();
|
|
@@ -5237,7 +5601,11 @@ async function resumableThreadCandidates({ options, terminal, currentIdentity })
|
|
|
5237
5601
|
managedSessionBindingInactive: managed.length === 1 &&
|
|
5238
5602
|
managed[0].status === "bound" &&
|
|
5239
5603
|
Boolean(managed[0].binding) &&
|
|
5240
|
-
|
|
5604
|
+
managedSessionOwnerIsConclusivelyInactive({
|
|
5605
|
+
session: managed[0],
|
|
5606
|
+
terminal,
|
|
5607
|
+
identity: currentIdentity
|
|
5608
|
+
}),
|
|
5241
5609
|
managedSessionWorkspaceMatches: managed.length === 1
|
|
5242
5610
|
? path.resolve(managed[0].workspace) === workspace
|
|
5243
5611
|
: undefined,
|
|
@@ -5271,12 +5639,16 @@ function decodeThreadCandidateToken(value) {
|
|
|
5271
5639
|
}
|
|
5272
5640
|
if (!isRecord(parsed) ||
|
|
5273
5641
|
parsed.schema !== "agent-knock-knock/thread-candidate-token" ||
|
|
5274
|
-
parsed.version
|
|
5642
|
+
!(parsed.version === 1 || parsed.version === 2) ||
|
|
5275
5643
|
!["codex", "claude"].includes(String(parsed.agent)) ||
|
|
5276
5644
|
!isExactNativeThreadId(parsed.nativeThreadId) ||
|
|
5277
5645
|
!stringValue(parsed.cwd) ||
|
|
5278
5646
|
!["codex_rollout", "claude_transcript"].includes(String(parsed.source)) ||
|
|
5279
5647
|
!stringValue(parsed.agentVersion) ||
|
|
5648
|
+
(parsed.version === 1
|
|
5649
|
+
? parsed.sourceAgentVersion !== undefined
|
|
5650
|
+
: (!stringValue(parsed.sourceAgentVersion) ||
|
|
5651
|
+
parsed.sourceAgentVersion === parsed.agentVersion)) ||
|
|
5280
5652
|
!isRecord(parsed.fileToken) ||
|
|
5281
5653
|
!stringValue(parsed.fileToken.path) ||
|
|
5282
5654
|
!stringValue(parsed.fileToken.device) ||
|
|
@@ -5947,6 +6319,161 @@ async function runResumeThread(options) {
|
|
|
5947
6319
|
selectionSnapshot: selection.snapshot
|
|
5948
6320
|
});
|
|
5949
6321
|
}
|
|
6322
|
+
async function runReconcileBinding(options) {
|
|
6323
|
+
const initiallyResolved = await resolveLifecycleTerminal(options);
|
|
6324
|
+
const storeDir = storeDirFromOptions(options);
|
|
6325
|
+
const conflictingSessionId = required(stringValue(options.conflictingSession ?? options.conflictingSessionId), "--conflicting-session is required");
|
|
6326
|
+
const expectedRevisionValue = required(stringValue(options.expectedSessionRevision ?? options.sessionRevision), "--expected-session-revision is required");
|
|
6327
|
+
if (!/^[1-9][0-9]*$/u.test(expectedRevisionValue)) {
|
|
6328
|
+
throw new Error("--expected-session-revision must be a positive integer");
|
|
6329
|
+
}
|
|
6330
|
+
const expectedSessionRevision = Number(expectedRevisionValue);
|
|
6331
|
+
if (!Number.isSafeInteger(expectedSessionRevision)) {
|
|
6332
|
+
throw new Error("--expected-session-revision must be a positive safe integer");
|
|
6333
|
+
}
|
|
6334
|
+
const expectedBindingToken = required(stringValue(options.expectedBindingToken), "--expected-binding-token is required");
|
|
6335
|
+
const expectedTerminalToken = required(stringValue(options.expectedTerminalToken), "--expected-terminal-token is required");
|
|
6336
|
+
const releaseTerminalLock = acquireFileLock(terminalBridgeSendLockPath(storeDir, initiallyResolved.terminalControl), { timeoutMs: 30000 });
|
|
6337
|
+
try {
|
|
6338
|
+
return await withStoreWriterLeaseAsync(storeDir, async () => {
|
|
6339
|
+
const terminal = await resolveLifecycleTerminal(options);
|
|
6340
|
+
if (terminal.pid !== initiallyResolved.pid ||
|
|
6341
|
+
terminal.conversationId !== initiallyResolved.conversationId ||
|
|
6342
|
+
terminalControlSelectorKey(terminal.terminalControl) !==
|
|
6343
|
+
terminalControlSelectorKey(initiallyResolved.terminalControl)) {
|
|
6344
|
+
throw new Error("terminal identity changed while waiting to reconcile its binding; refresh AKK list");
|
|
6345
|
+
}
|
|
6346
|
+
await recoverLifecycleFenceBeforeMutation({ options, terminal });
|
|
6347
|
+
const dispatchOwnership = terminalDispatchOwnership(terminal.terminalControl);
|
|
6348
|
+
if (dispatchOwnership.state !== "none") {
|
|
6349
|
+
throw new Error("the terminal acquired an unresolved dispatch after the binding conflict was listed; refresh AKK list");
|
|
6350
|
+
}
|
|
6351
|
+
const session = loadManagedSession(storeDir, conflictingSessionId);
|
|
6352
|
+
if (session.revision !== expectedSessionRevision ||
|
|
6353
|
+
managedSessionBindingToken(session) !== expectedBindingToken) {
|
|
6354
|
+
throw new Error("managed Session binding changed after it was listed; refresh AKK list");
|
|
6355
|
+
}
|
|
6356
|
+
const binding = session.binding;
|
|
6357
|
+
if (session.status !== "bound" ||
|
|
6358
|
+
!binding ||
|
|
6359
|
+
session.agent !== terminal.agent ||
|
|
6360
|
+
binding.terminal_id !== terminal.conversationId ||
|
|
6361
|
+
binding.native_process.pid !== terminal.pid ||
|
|
6362
|
+
terminalControlSelectorKey(binding.terminal_control) !==
|
|
6363
|
+
terminalControlSelectorKey(terminal.terminalControl) ||
|
|
6364
|
+
!matchesConfiguredWorkspace(session.workspace, terminal.terminalControl.currentPath)) {
|
|
6365
|
+
throw new Error("the listed managed Session no longer claims this exact terminal");
|
|
6366
|
+
}
|
|
6367
|
+
const identity = await resolveCurrentNativeAgentSessionIdentity({
|
|
6368
|
+
options,
|
|
6369
|
+
agent: terminal.agent,
|
|
6370
|
+
pid: terminal.pid,
|
|
6371
|
+
cwd: terminal.terminalControl.currentPath
|
|
6372
|
+
});
|
|
6373
|
+
const terminalToken = lifecycleBindingToken({
|
|
6374
|
+
terminal,
|
|
6375
|
+
identity
|
|
6376
|
+
});
|
|
6377
|
+
if (terminalToken !== expectedTerminalToken) {
|
|
6378
|
+
throw new Error("live terminal identity changed after the conflict was listed; refresh AKK list");
|
|
6379
|
+
}
|
|
6380
|
+
const bridge = createTerminalAgentBridge(options);
|
|
6381
|
+
if (managedSessionHasUnresolvedNativeTransition(storeDir, session)) {
|
|
6382
|
+
throw new Error(`managed Session ${session.session_id} has an unresolved native-thread transition`);
|
|
6383
|
+
}
|
|
6384
|
+
const blockers = managedTurnsForSession(storeDir, session.session_id).filter((turn) => SESSION_SEND_BLOCKING_STATUSES.has(turn.status));
|
|
6385
|
+
if (blockers.length > 0) {
|
|
6386
|
+
throw new Error(`managed Session ${session.session_id} still has unresolved Turn ` +
|
|
6387
|
+
`${turnIdForConversation(blockers[0])} (${blockers[0].status})`);
|
|
6388
|
+
}
|
|
6389
|
+
const finalTerminal = await resolveLifecycleTerminal(options);
|
|
6390
|
+
if (finalTerminal.pid !== terminal.pid ||
|
|
6391
|
+
finalTerminal.conversationId !== terminal.conversationId ||
|
|
6392
|
+
terminalControlSelectorKey(finalTerminal.terminalControl) !==
|
|
6393
|
+
terminalControlSelectorKey(terminal.terminalControl)) {
|
|
6394
|
+
throw new Error("terminal identity changed during binding reconciliation; refresh AKK list");
|
|
6395
|
+
}
|
|
6396
|
+
const finalStatus = await bridge.status(finalTerminal.agent, finalTerminal.terminalControl, {
|
|
6397
|
+
runtime: terminalRuntimeForLiveIdentity({
|
|
6398
|
+
terminal: finalTerminal,
|
|
6399
|
+
physicalOnly: true
|
|
6400
|
+
})
|
|
6401
|
+
});
|
|
6402
|
+
assertTerminalLifecycleReady({
|
|
6403
|
+
options,
|
|
6404
|
+
terminal: finalTerminal,
|
|
6405
|
+
terminalStatus: finalStatus
|
|
6406
|
+
});
|
|
6407
|
+
const finalIdentity = await resolveCurrentNativeAgentSessionIdentity({
|
|
6408
|
+
options,
|
|
6409
|
+
agent: finalTerminal.agent,
|
|
6410
|
+
pid: finalTerminal.pid,
|
|
6411
|
+
cwd: finalTerminal.terminalControl.currentPath
|
|
6412
|
+
});
|
|
6413
|
+
if (lifecycleBindingToken({
|
|
6414
|
+
terminal: finalTerminal,
|
|
6415
|
+
identity: finalIdentity
|
|
6416
|
+
}) !== expectedTerminalToken) {
|
|
6417
|
+
throw new Error("live terminal identity changed during binding reconciliation; refresh AKK list");
|
|
6418
|
+
}
|
|
6419
|
+
const finalSession = loadManagedSession(storeDir, conflictingSessionId);
|
|
6420
|
+
if (finalSession.revision !== expectedSessionRevision ||
|
|
6421
|
+
managedSessionBindingToken(finalSession) !== expectedBindingToken) {
|
|
6422
|
+
throw new Error("managed Session binding changed during reconciliation; refresh AKK list");
|
|
6423
|
+
}
|
|
6424
|
+
const conflictKind = managedBindingConflictKindForResolvedTerminal({
|
|
6425
|
+
storeDir,
|
|
6426
|
+
session: finalSession,
|
|
6427
|
+
terminal: finalTerminal,
|
|
6428
|
+
identity: finalIdentity
|
|
6429
|
+
});
|
|
6430
|
+
if (![
|
|
6431
|
+
"provisional_orphan",
|
|
6432
|
+
"live_external_thread_change"
|
|
6433
|
+
].includes(String(conflictKind))) {
|
|
6434
|
+
throw new Error(conflictKind === "stale_process_incarnation"
|
|
6435
|
+
? "the stale process incarnation no longer requires explicit reconciliation; refresh AKK list"
|
|
6436
|
+
: conflictKind === undefined
|
|
6437
|
+
? "the managed Session now exactly matches the live terminal; no reconciliation is needed"
|
|
6438
|
+
: "the managed binding conflict is unverifiable and cannot be detached automatically");
|
|
6439
|
+
}
|
|
6440
|
+
const reconciledAt = new Date().toISOString();
|
|
6441
|
+
const detached = saveManagedSession(storeDir, {
|
|
6442
|
+
...finalSession,
|
|
6443
|
+
status: "detached",
|
|
6444
|
+
detached_at: reconciledAt,
|
|
6445
|
+
updated_at: reconciledAt
|
|
6446
|
+
}, {
|
|
6447
|
+
expectedRevision: expectedSessionRevision
|
|
6448
|
+
});
|
|
6449
|
+
runtimeLog("info", "managed_binding_reconciled", {
|
|
6450
|
+
terminal_id: terminal.conversationId,
|
|
6451
|
+
terminal_target: terminal.terminalControl.target,
|
|
6452
|
+
session_id: detached.session_id,
|
|
6453
|
+
binding_id: detached.binding?.binding_id,
|
|
6454
|
+
previous_revision: expectedSessionRevision,
|
|
6455
|
+
revision: detached.revision,
|
|
6456
|
+
conflict_kind: conflictKind,
|
|
6457
|
+
terminal_input_sent: false
|
|
6458
|
+
});
|
|
6459
|
+
printJson({
|
|
6460
|
+
status: "reconciled",
|
|
6461
|
+
outcome: "detached_conflicting_binding",
|
|
6462
|
+
conflict_kind: conflictKind,
|
|
6463
|
+
terminal_id: terminal.conversationId,
|
|
6464
|
+
session_id: detached.session_id,
|
|
6465
|
+
binding_id: detached.binding?.binding_id,
|
|
6466
|
+
session_revision: detached.revision,
|
|
6467
|
+
terminal_input_sent: false,
|
|
6468
|
+
turn_created: false,
|
|
6469
|
+
refresh_required: true
|
|
6470
|
+
});
|
|
6471
|
+
});
|
|
6472
|
+
}
|
|
6473
|
+
finally {
|
|
6474
|
+
releaseTerminalLock();
|
|
6475
|
+
}
|
|
6476
|
+
}
|
|
5950
6477
|
function assertResumeSnapshotMatchesTerminal(snapshot, terminal) {
|
|
5951
6478
|
const workspace = path.resolve(terminal.terminalControl.currentPath ?? process.cwd());
|
|
5952
6479
|
if (snapshot.terminal_id !== terminal.conversationId ||
|
|
@@ -6169,8 +6696,11 @@ async function runNativeThreadTransition(options, operation) {
|
|
|
6169
6696
|
`${turnIdForConversation(targetBlockers[0])}`);
|
|
6170
6697
|
}
|
|
6171
6698
|
if (targetSession?.status === "bound") {
|
|
6172
|
-
|
|
6173
|
-
|
|
6699
|
+
if (!managedSessionOwnerIsConclusivelyInactive({
|
|
6700
|
+
session: targetSession,
|
|
6701
|
+
terminal,
|
|
6702
|
+
identity: beforeIdentity
|
|
6703
|
+
})) {
|
|
6174
6704
|
throw new Error(`target Session ${candidate.managed_session_id} is still bound to a live or unverifiable process`);
|
|
6175
6705
|
}
|
|
6176
6706
|
const detachedAt = new Date().toISOString();
|
|
@@ -7282,6 +7812,7 @@ async function runSend(options) {
|
|
|
7282
7812
|
storeDir: rawStoreDir
|
|
7283
7813
|
});
|
|
7284
7814
|
}
|
|
7815
|
+
let pendingRawAttachSessionCreate;
|
|
7285
7816
|
if (!managedSession) {
|
|
7286
7817
|
if (currentNativeIdentity) {
|
|
7287
7818
|
await assertNativeThreadHasExclusiveOwnership({
|
|
@@ -7299,7 +7830,13 @@ async function runSend(options) {
|
|
|
7299
7830
|
identity: currentNativeIdentity,
|
|
7300
7831
|
lineage: { created_by: "attach" }
|
|
7301
7832
|
});
|
|
7302
|
-
|
|
7833
|
+
if (terminalConversation.agent === "codex" &&
|
|
7834
|
+
!currentNativeIdentity) {
|
|
7835
|
+
pendingRawAttachSessionCreate = managedSession;
|
|
7836
|
+
}
|
|
7837
|
+
else {
|
|
7838
|
+
managedSession = saveManagedSession(rawStoreDir, managedSession, { expectedRevision: null });
|
|
7839
|
+
}
|
|
7303
7840
|
}
|
|
7304
7841
|
const logicalNativeIdentity = logicalIdentityForManagedSession({
|
|
7305
7842
|
storeDir: rawStoreDir,
|
|
@@ -7374,6 +7911,31 @@ async function runSend(options) {
|
|
|
7374
7911
|
storeWriterLeaseHeld: true,
|
|
7375
7912
|
recordMessageAfterSend: true,
|
|
7376
7913
|
recordRawAttachmentAfterSend: reusableTurn === undefined,
|
|
7914
|
+
onTerminalPreflightVerified: pendingRawAttachSessionCreate
|
|
7915
|
+
? () => {
|
|
7916
|
+
const createdSession = saveManagedSession(rawStoreDir, pendingRawAttachSessionCreate, { expectedRevision: null });
|
|
7917
|
+
managedSession = createdSession;
|
|
7918
|
+
pendingRawAttachSessionCreate = undefined;
|
|
7919
|
+
return () => {
|
|
7920
|
+
const current = loadManagedSession(rawStoreDir, createdSession.session_id);
|
|
7921
|
+
if (current.status !== "bound" ||
|
|
7922
|
+
current.revision !== createdSession.revision ||
|
|
7923
|
+
managedSessionBindingToken(current) !==
|
|
7924
|
+
managedSessionBindingToken(createdSession)) {
|
|
7925
|
+
throw new Error(`new raw-attach Session ${createdSession.session_id} changed before pre-transport rollback`);
|
|
7926
|
+
}
|
|
7927
|
+
const detachedAt = new Date().toISOString();
|
|
7928
|
+
managedSession = saveManagedSession(rawStoreDir, {
|
|
7929
|
+
...current,
|
|
7930
|
+
status: "detached",
|
|
7931
|
+
detached_at: detachedAt,
|
|
7932
|
+
updated_at: detachedAt
|
|
7933
|
+
}, {
|
|
7934
|
+
expectedRevision: current.revision
|
|
7935
|
+
});
|
|
7936
|
+
};
|
|
7937
|
+
}
|
|
7938
|
+
: undefined,
|
|
7377
7939
|
allowedPreMaterializationIdentity,
|
|
7378
7940
|
allowedAdditionalIdentities
|
|
7379
7941
|
});
|
|
@@ -8302,7 +8864,7 @@ async function runTerminalConversationApprove({ options, conversationId, agent,
|
|
|
8302
8864
|
releaseTerminalLock();
|
|
8303
8865
|
}
|
|
8304
8866
|
}
|
|
8305
|
-
async function runTerminalControlSend({ options, conversation, nextConversation, statePath, logPath, executor, message, terminalControl, terminalSendLockHeld = false, terminalStateLockHeld = false, storeWriterLeaseHeld = false, recordMessageAfterSend = false, recordRawAttachmentAfterSend = false, allowedPreMaterializationIdentity = undefined, allowedAdditionalIdentities = [], continuingTurnResponse = false }) {
|
|
8867
|
+
async function runTerminalControlSend({ options, conversation, nextConversation, statePath, logPath, executor, message, terminalControl, terminalSendLockHeld = false, terminalStateLockHeld = false, storeWriterLeaseHeld = false, recordMessageAfterSend = false, recordRawAttachmentAfterSend = false, onTerminalPreflightVerified = undefined, allowedPreMaterializationIdentity = undefined, allowedAdditionalIdentities = [], continuingTurnResponse = false }) {
|
|
8306
8868
|
const bridge = terminalBridgeEnabled(conversation);
|
|
8307
8869
|
if (!terminalSendLockHeld) {
|
|
8308
8870
|
const releaseTerminalLock = acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), terminalControl), { timeoutMs: 30000 });
|
|
@@ -8321,6 +8883,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
8321
8883
|
storeWriterLeaseHeld,
|
|
8322
8884
|
recordMessageAfterSend,
|
|
8323
8885
|
recordRawAttachmentAfterSend,
|
|
8886
|
+
onTerminalPreflightVerified,
|
|
8324
8887
|
allowedPreMaterializationIdentity,
|
|
8325
8888
|
allowedAdditionalIdentities,
|
|
8326
8889
|
continuingTurnResponse
|
|
@@ -8360,6 +8923,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
8360
8923
|
storeWriterLeaseHeld,
|
|
8361
8924
|
recordMessageAfterSend,
|
|
8362
8925
|
recordRawAttachmentAfterSend,
|
|
8926
|
+
onTerminalPreflightVerified,
|
|
8363
8927
|
allowedPreMaterializationIdentity,
|
|
8364
8928
|
allowedAdditionalIdentities,
|
|
8365
8929
|
continuingTurnResponse
|
|
@@ -8385,6 +8949,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
8385
8949
|
storeWriterLeaseHeld: true,
|
|
8386
8950
|
recordMessageAfterSend,
|
|
8387
8951
|
recordRawAttachmentAfterSend,
|
|
8952
|
+
onTerminalPreflightVerified,
|
|
8388
8953
|
allowedPreMaterializationIdentity,
|
|
8389
8954
|
allowedAdditionalIdentities,
|
|
8390
8955
|
continuingTurnResponse
|
|
@@ -8619,6 +9184,31 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
8619
9184
|
catch (error) {
|
|
8620
9185
|
throw new Error(`refusing to send to ${executor.display_name} without a verified idle terminal: ${error instanceof Error ? error.message : String(error)}`);
|
|
8621
9186
|
}
|
|
9187
|
+
// A newly discovered raw terminal may not have an authoritative Session
|
|
9188
|
+
// yet. Commit that Session only after every pre-input terminal and native
|
|
9189
|
+
// acceptance check has passed, but before the Turn or dispatch ledger can
|
|
9190
|
+
// become durable. This prevents a failed virgin attach from leaving a
|
|
9191
|
+
// zero-identity `bound` Session that fences every later control action.
|
|
9192
|
+
let rollbackPreTransportAttach = onTerminalPreflightVerified?.();
|
|
9193
|
+
const rollbackRawAttachBeforeTransport = () => {
|
|
9194
|
+
if (!rollbackPreTransportAttach) {
|
|
9195
|
+
return true;
|
|
9196
|
+
}
|
|
9197
|
+
const rollback = rollbackPreTransportAttach;
|
|
9198
|
+
rollbackPreTransportAttach = undefined;
|
|
9199
|
+
try {
|
|
9200
|
+
rollback();
|
|
9201
|
+
return true;
|
|
9202
|
+
}
|
|
9203
|
+
catch (error) {
|
|
9204
|
+
runtimeLog("error", "raw_attach_pre_transport_rollback_failed", {
|
|
9205
|
+
conversation_id: conversation.conversation_id,
|
|
9206
|
+
terminal_target: terminalControl.target,
|
|
9207
|
+
error: error instanceof Error ? error.message : String(error)
|
|
9208
|
+
});
|
|
9209
|
+
return false;
|
|
9210
|
+
}
|
|
9211
|
+
};
|
|
8622
9212
|
const bridgeConversation = bridge
|
|
8623
9213
|
? withTerminalBridgeState({
|
|
8624
9214
|
conversation: nextConversation,
|
|
@@ -8642,33 +9232,53 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
8642
9232
|
status: "prepared",
|
|
8643
9233
|
preparedAt: bridgeStartedAt
|
|
8644
9234
|
});
|
|
8645
|
-
|
|
8646
|
-
|
|
8647
|
-
|
|
8648
|
-
|
|
8649
|
-
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
|
|
8657
|
-
|
|
8658
|
-
|
|
8659
|
-
|
|
8660
|
-
|
|
8661
|
-
stringValue(previousDispatchLedger?.
|
|
8662
|
-
|
|
9235
|
+
try {
|
|
9236
|
+
saveTerminalBridgeDispatchLedger(terminalControl, {
|
|
9237
|
+
...terminalBindingLedgerFields(preparedConversation),
|
|
9238
|
+
status: "prepared",
|
|
9239
|
+
generation_id: message.id,
|
|
9240
|
+
conversation_id: preparedConversation.conversation_id,
|
|
9241
|
+
session_id: sessionIdForConversation(preparedConversation),
|
|
9242
|
+
turn_id: turnIdForConversation(preparedConversation),
|
|
9243
|
+
message_id: message.id,
|
|
9244
|
+
message_type: message.type,
|
|
9245
|
+
request_hash: terminalRequestHash,
|
|
9246
|
+
prepared_at: bridgeStartedAt,
|
|
9247
|
+
dispatcher_pid: process.pid,
|
|
9248
|
+
state_path: statePath,
|
|
9249
|
+
event_log_path: logPath,
|
|
9250
|
+
callback_expected: Boolean(preparedConversation.gateway_method),
|
|
9251
|
+
previous_generation_id: stringValue(previousDispatchLedger?.generation_id) ??
|
|
9252
|
+
stringValue(previousDispatchLedger?.message_id)
|
|
9253
|
+
});
|
|
9254
|
+
}
|
|
9255
|
+
catch (error) {
|
|
9256
|
+
try {
|
|
9257
|
+
restoreTerminalBridgeDispatchLedger({
|
|
9258
|
+
terminalControl,
|
|
9259
|
+
previousLedger: previousDispatchLedger,
|
|
9260
|
+
reason: "prepared ledger persistence failed before tmux input"
|
|
9261
|
+
});
|
|
9262
|
+
}
|
|
9263
|
+
finally {
|
|
9264
|
+
rollbackRawAttachBeforeTransport();
|
|
9265
|
+
}
|
|
9266
|
+
throw error;
|
|
9267
|
+
}
|
|
8663
9268
|
try {
|
|
8664
9269
|
saveState(statePath, preparedConversation);
|
|
8665
9270
|
}
|
|
8666
9271
|
catch (error) {
|
|
8667
|
-
|
|
8668
|
-
|
|
8669
|
-
|
|
8670
|
-
|
|
8671
|
-
|
|
9272
|
+
try {
|
|
9273
|
+
restoreTerminalBridgeDispatchLedger({
|
|
9274
|
+
terminalControl,
|
|
9275
|
+
previousLedger: previousDispatchLedger,
|
|
9276
|
+
reason: "prepared state persistence failed before tmux input"
|
|
9277
|
+
});
|
|
9278
|
+
}
|
|
9279
|
+
finally {
|
|
9280
|
+
rollbackRawAttachBeforeTransport();
|
|
9281
|
+
}
|
|
8672
9282
|
throw error;
|
|
8673
9283
|
}
|
|
8674
9284
|
let bridgeMonitor;
|
|
@@ -8741,6 +9351,8 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
8741
9351
|
: String(ledgerError)
|
|
8742
9352
|
});
|
|
8743
9353
|
}
|
|
9354
|
+
const rawAttachRolledBack = rollbackRawAttachBeforeTransport();
|
|
9355
|
+
const durableAbortCanBeRetryable = dispatchLedgerRestored && rawAttachRolledBack;
|
|
8744
9356
|
const failureBase = recordRawAttachmentAfterSend
|
|
8745
9357
|
? {
|
|
8746
9358
|
...preparedConversation,
|
|
@@ -8765,7 +9377,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
8765
9377
|
preparedAt: bridgeStartedAt,
|
|
8766
9378
|
abortedAt,
|
|
8767
9379
|
error: errorMessage,
|
|
8768
|
-
safeToRetry:
|
|
9380
|
+
safeToRetry: durableAbortCanBeRetryable
|
|
8769
9381
|
});
|
|
8770
9382
|
let abortedStatePersisted = false;
|
|
8771
9383
|
try {
|
|
@@ -8784,7 +9396,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
8784
9396
|
: String(persistenceError)
|
|
8785
9397
|
});
|
|
8786
9398
|
}
|
|
8787
|
-
const safeToRetry =
|
|
9399
|
+
const safeToRetry = durableAbortCanBeRetryable && abortedStatePersisted;
|
|
8788
9400
|
try {
|
|
8789
9401
|
appendEvent(logPath, {
|
|
8790
9402
|
ts: abortedAt,
|
|
@@ -8826,7 +9438,8 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
8826
9438
|
error: errorMessage,
|
|
8827
9439
|
safe_to_retry: safeToRetry,
|
|
8828
9440
|
dispatch_ledger_restored: dispatchLedgerRestored,
|
|
8829
|
-
aborted_state_persisted: abortedStatePersisted
|
|
9441
|
+
aborted_state_persisted: abortedStatePersisted,
|
|
9442
|
+
raw_attach_rolled_back: rawAttachRolledBack
|
|
8830
9443
|
});
|
|
8831
9444
|
printJson({
|
|
8832
9445
|
session_id: sessionIdForConversation(abortedConversation),
|
|
@@ -8847,7 +9460,9 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
8847
9460
|
? "AKK failed before touching tmux; this terminal submission was not sent and may be retried."
|
|
8848
9461
|
: !dispatchLedgerRestored
|
|
8849
9462
|
? "AKK failed before tmux input but could not restore the terminal dispatch ledger; inspect and close the conversation before retrying."
|
|
8850
|
-
:
|
|
9463
|
+
: !rawAttachRolledBack
|
|
9464
|
+
? "AKK failed before tmux input but could not detach the provisional raw-attach Session; inspect its exact binding before retrying."
|
|
9465
|
+
: "AKK failed before tmux input but could not persist the aborted receipt; inspect the conversation before retrying.",
|
|
8851
9466
|
openclaw_next_action: {
|
|
8852
9467
|
action: safeToRetry ? "retry" : "inspect",
|
|
8853
9468
|
conversation_id: abortedConversation.conversation_id,
|
|
@@ -8859,7 +9474,9 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
8859
9474
|
? "The failure occurred before any tmux input."
|
|
8860
9475
|
: !dispatchLedgerRestored
|
|
8861
9476
|
? "The terminal ledger could not be restored automatically."
|
|
8862
|
-
:
|
|
9477
|
+
: !rawAttachRolledBack
|
|
9478
|
+
? "The provisional raw-attach Session could not be detached automatically."
|
|
9479
|
+
: "The aborted receipt could not be made durable."
|
|
8863
9480
|
}
|
|
8864
9481
|
});
|
|
8865
9482
|
return;
|
|
@@ -9258,6 +9875,149 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
9258
9875
|
}
|
|
9259
9876
|
}
|
|
9260
9877
|
catch (error) {
|
|
9878
|
+
if (!textInjectedAt &&
|
|
9879
|
+
error instanceof TerminalInputNotStartedError) {
|
|
9880
|
+
const abortedAt = new Date().toISOString();
|
|
9881
|
+
const errorMessage = error.message;
|
|
9882
|
+
let dispatchLedgerRestored = true;
|
|
9883
|
+
try {
|
|
9884
|
+
restoreTerminalBridgeDispatchLedger({
|
|
9885
|
+
terminalControl,
|
|
9886
|
+
previousLedger: previousDispatchLedger,
|
|
9887
|
+
reason: "terminal transport was proved not to have started"
|
|
9888
|
+
});
|
|
9889
|
+
}
|
|
9890
|
+
catch (ledgerError) {
|
|
9891
|
+
dispatchLedgerRestored = false;
|
|
9892
|
+
runtimeLog("error", "terminal_dispatch_ledger_restore_failed", {
|
|
9893
|
+
conversation_id: conversation.conversation_id,
|
|
9894
|
+
terminal_target: terminalControl.target,
|
|
9895
|
+
error: ledgerError instanceof Error
|
|
9896
|
+
? ledgerError.message
|
|
9897
|
+
: String(ledgerError)
|
|
9898
|
+
});
|
|
9899
|
+
}
|
|
9900
|
+
const rawAttachRolledBack = rollbackRawAttachBeforeTransport();
|
|
9901
|
+
const durableAbortCanBeRetryable = dispatchLedgerRestored && rawAttachRolledBack;
|
|
9902
|
+
const failureBase = recordRawAttachmentAfterSend
|
|
9903
|
+
? {
|
|
9904
|
+
...preparedConversation,
|
|
9905
|
+
status: "failed",
|
|
9906
|
+
failed_at: abortedAt,
|
|
9907
|
+
failure_reason: "terminal transport failed before terminal input"
|
|
9908
|
+
}
|
|
9909
|
+
: {
|
|
9910
|
+
...preparedConversation,
|
|
9911
|
+
status: conversation.status,
|
|
9912
|
+
...(conversation.idle_since
|
|
9913
|
+
? { idle_since: conversation.idle_since }
|
|
9914
|
+
: {})
|
|
9915
|
+
};
|
|
9916
|
+
const abortedConversation = withTerminalBridgeSubmission({
|
|
9917
|
+
conversation: failureBase,
|
|
9918
|
+
messageId: message.id,
|
|
9919
|
+
messageType: message.type,
|
|
9920
|
+
messageBody: String(message.body),
|
|
9921
|
+
requestText: terminalPayload,
|
|
9922
|
+
status: "aborted",
|
|
9923
|
+
preparedAt: bridgeStartedAt,
|
|
9924
|
+
abortedAt,
|
|
9925
|
+
error: errorMessage,
|
|
9926
|
+
safeToRetry: durableAbortCanBeRetryable
|
|
9927
|
+
});
|
|
9928
|
+
let abortedStatePersisted = false;
|
|
9929
|
+
try {
|
|
9930
|
+
saveState(statePath, abortedConversation);
|
|
9931
|
+
abortedStatePersisted = true;
|
|
9932
|
+
}
|
|
9933
|
+
catch (persistenceError) {
|
|
9934
|
+
runtimeLog("error", "terminal_message_submit_aborted_persist_failed", {
|
|
9935
|
+
conversation_id: abortedConversation.conversation_id,
|
|
9936
|
+
terminal_target: terminalControl.target,
|
|
9937
|
+
error: persistenceError instanceof Error
|
|
9938
|
+
? persistenceError.message
|
|
9939
|
+
: String(persistenceError)
|
|
9940
|
+
});
|
|
9941
|
+
}
|
|
9942
|
+
const safeToRetry = durableAbortCanBeRetryable && abortedStatePersisted;
|
|
9943
|
+
const reportedConversation = safeToRetry
|
|
9944
|
+
? abortedConversation
|
|
9945
|
+
: withTerminalBridgeSubmission({
|
|
9946
|
+
conversation: abortedConversation,
|
|
9947
|
+
messageId: message.id,
|
|
9948
|
+
messageType: message.type,
|
|
9949
|
+
messageBody: String(message.body),
|
|
9950
|
+
requestText: terminalPayload,
|
|
9951
|
+
status: "aborted",
|
|
9952
|
+
preparedAt: bridgeStartedAt,
|
|
9953
|
+
abortedAt,
|
|
9954
|
+
error: errorMessage,
|
|
9955
|
+
safeToRetry: false
|
|
9956
|
+
});
|
|
9957
|
+
try {
|
|
9958
|
+
appendEvent(logPath, {
|
|
9959
|
+
ts: abortedAt,
|
|
9960
|
+
conversation_id: abortedConversation.conversation_id,
|
|
9961
|
+
event: "terminal_message_submit_aborted",
|
|
9962
|
+
message_id: message.id,
|
|
9963
|
+
executor,
|
|
9964
|
+
terminal_control: terminalControl,
|
|
9965
|
+
error: textSummary(errorMessage),
|
|
9966
|
+
safe_to_retry: safeToRetry,
|
|
9967
|
+
terminal_input_started: false
|
|
9968
|
+
});
|
|
9969
|
+
}
|
|
9970
|
+
catch (persistenceError) {
|
|
9971
|
+
runtimeLog("error", "terminal_message_submit_aborted_event_failed", {
|
|
9972
|
+
conversation_id: abortedConversation.conversation_id,
|
|
9973
|
+
terminal_target: terminalControl.target,
|
|
9974
|
+
error: persistenceError instanceof Error
|
|
9975
|
+
? persistenceError.message
|
|
9976
|
+
: String(persistenceError)
|
|
9977
|
+
});
|
|
9978
|
+
}
|
|
9979
|
+
runtimeLog("error", "terminal_message_submit_aborted", {
|
|
9980
|
+
conversation_id: abortedConversation.conversation_id,
|
|
9981
|
+
terminal_target: terminalControl.target,
|
|
9982
|
+
error: errorMessage,
|
|
9983
|
+
safe_to_retry: safeToRetry,
|
|
9984
|
+
terminal_input_started: false,
|
|
9985
|
+
dispatch_ledger_restored: dispatchLedgerRestored,
|
|
9986
|
+
aborted_state_persisted: abortedStatePersisted,
|
|
9987
|
+
raw_attach_rolled_back: rawAttachRolledBack
|
|
9988
|
+
});
|
|
9989
|
+
printJson({
|
|
9990
|
+
session_id: sessionIdForConversation(reportedConversation),
|
|
9991
|
+
turn_id: turnIdForConversation(reportedConversation),
|
|
9992
|
+
conversation: reportedConversation,
|
|
9993
|
+
message,
|
|
9994
|
+
delivered: false,
|
|
9995
|
+
status: "submission_aborted",
|
|
9996
|
+
submission_outcome: "aborted",
|
|
9997
|
+
background: true,
|
|
9998
|
+
callback_expected: false,
|
|
9999
|
+
terminal_control: terminalControl,
|
|
10000
|
+
monitor_pid: bridgeMonitor?.pid ?? null,
|
|
10001
|
+
executor,
|
|
10002
|
+
safe_to_retry: safeToRetry,
|
|
10003
|
+
do_not_retry: !safeToRetry,
|
|
10004
|
+
reason: safeToRetry
|
|
10005
|
+
? "AKK proved that terminal input never started; this submission may be retried."
|
|
10006
|
+
: "AKK proved terminal input never started but could not make every abort receipt and Session rollback durable; inspect before retrying.",
|
|
10007
|
+
openclaw_next_action: {
|
|
10008
|
+
action: safeToRetry ? "retry" : "inspect",
|
|
10009
|
+
conversation_id: reportedConversation.conversation_id,
|
|
10010
|
+
session_id: sessionIdForConversation(reportedConversation),
|
|
10011
|
+
turn_id: turnIdForConversation(reportedConversation),
|
|
10012
|
+
safe_to_retry: safeToRetry,
|
|
10013
|
+
do_not_retry: !safeToRetry,
|
|
10014
|
+
reason: safeToRetry
|
|
10015
|
+
? "The terminal transport failed before any input operation succeeded."
|
|
10016
|
+
: "The pre-input failure could not be fully reconciled in durable state."
|
|
10017
|
+
}
|
|
10018
|
+
});
|
|
10019
|
+
return;
|
|
10020
|
+
}
|
|
9261
10021
|
const uncertainAt = new Date().toISOString();
|
|
9262
10022
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
9263
10023
|
const failureBase = stagedConversation;
|
|
@@ -18354,6 +19114,7 @@ function usage() {
|
|
|
18354
19114
|
agent-knock-knock list-resumable-threads --terminal <exact-terminal-id> [--selection-scope <opaque-scope>]
|
|
18355
19115
|
agent-knock-knock resume-thread --terminal <exact-terminal-id> --native-thread <uuid> --expected-binding-token <token> --candidate-token <token>
|
|
18356
19116
|
agent-knock-knock resume-thread --terminal <exact-terminal-id> (--selection-handle <handle> | --selection-snapshot <id> (--selection-number <n> | --selection-short-id <@id>)) --selection-scope <opaque-scope>
|
|
19117
|
+
agent-knock-knock reconcile-binding --terminal <exact-terminal-id> --conflicting-session <session-id> --expected-session-revision <n> --expected-binding-token <token> --expected-terminal-token <token>
|
|
18357
19118
|
agent-knock-knock respond --turn <turn-id|selector> --message <text> [--conversation <selector>]
|
|
18358
19119
|
agent-knock-knock approve [--turn <turn-id|selector>] [--conversation <selector>] --expected-approval-fingerprint <fingerprint>
|
|
18359
19120
|
agent-knock-knock cancel [--turn <turn-id|selector>] [--conversation <selector>]
|