@scotthuang/agent-knock-knock 0.11.2 → 0.11.4
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 +15 -5
- package/dist/src/cli.js +1144 -58
- package/dist/src/cli.js.map +1 -1
- package/dist/src/codex-terminal-agent-adapter.d.ts +5 -1
- package/dist/src/codex-terminal-agent-adapter.js +357 -0
- package/dist/src/codex-terminal-agent-adapter.js.map +1 -1
- package/dist/src/openclaw-plugin.js +118 -2
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/dist/src/terminal-agent-adapter.d.ts +95 -0
- package/dist/src/terminal-agent-adapter.js +8 -0
- package/dist/src/terminal-agent-adapter.js.map +1 -1
- package/dist/src/terminal-agent-bridge.d.ts +79 -1
- package/dist/src/terminal-agent-bridge.js +332 -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 +44 -2
- package/dist/src/terminal-control-provider.js.map +1 -1
- package/docs/quickstart-tmux.md +2 -0
- package/openclaw.plugin.json +9 -1
- package/package.json +8 -2
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +14 -4
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 { NativeInspectionSubmissionError, 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";
|
|
@@ -255,9 +256,15 @@ async function runCommand(commandName, options) {
|
|
|
255
256
|
else if (commandName === "list-resumable-threads" || commandName === "threads") {
|
|
256
257
|
await runListResumableThreads(options);
|
|
257
258
|
}
|
|
259
|
+
else if (commandName === "native-inspect" || commandName === "native-status") {
|
|
260
|
+
await runNativeInspect(options);
|
|
261
|
+
}
|
|
258
262
|
else if (commandName === "resume-thread") {
|
|
259
263
|
await runResumeThread(options);
|
|
260
264
|
}
|
|
265
|
+
else if (commandName === "reconcile-binding") {
|
|
266
|
+
await runReconcileBinding(options);
|
|
267
|
+
}
|
|
261
268
|
else if (commandName === "respond") {
|
|
262
269
|
await runRespond(options);
|
|
263
270
|
}
|
|
@@ -2755,6 +2762,20 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
|
|
|
2755
2762
|
});
|
|
2756
2763
|
}
|
|
2757
2764
|
}
|
|
2765
|
+
const statusCardObservation = session.agent === "codex" &&
|
|
2766
|
+
typeof terminalState.screen_excerpt === "string"
|
|
2767
|
+
? bridge.registry.require("codex").observeThreadLifecycle?.({
|
|
2768
|
+
operation: { kind: "new_thread" },
|
|
2769
|
+
phase: "before",
|
|
2770
|
+
screen: terminalState.screen_excerpt
|
|
2771
|
+
})
|
|
2772
|
+
: undefined;
|
|
2773
|
+
const statusCardNativeThreadId = statusCardObservation?.status === "observed" &&
|
|
2774
|
+
terminalState.activity_state === "idle" &&
|
|
2775
|
+
terminalState.approval_state.blocked !== true &&
|
|
2776
|
+
isExactNativeThreadId(statusCardObservation.nativeThreadId)
|
|
2777
|
+
? statusCardObservation.nativeThreadId
|
|
2778
|
+
: undefined;
|
|
2758
2779
|
const agentVersion = agentVersionForRunningProcess(session.agent, session.pid, options);
|
|
2759
2780
|
const lifecycleCapability = bridge.registry.require(session.agent)
|
|
2760
2781
|
.probeThreadLifecycle?.(agentVersion) ?? {
|
|
@@ -2764,6 +2785,13 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
|
|
|
2764
2785
|
resumeExact: false,
|
|
2765
2786
|
reason: "native thread lifecycle is unavailable"
|
|
2766
2787
|
};
|
|
2788
|
+
const nativeInspectionCapability = bridge.registry.require(session.agent)
|
|
2789
|
+
.probeNativeInspection?.(agentVersion) ?? {
|
|
2790
|
+
status: "unsupported",
|
|
2791
|
+
agentVersion,
|
|
2792
|
+
statusInspection: false,
|
|
2793
|
+
reason: "native inspection is unavailable"
|
|
2794
|
+
};
|
|
2767
2795
|
const lifecycleBindingToken = unmanagedTerminalBindingToken({
|
|
2768
2796
|
terminalId: bridge.terminalConversationId(session),
|
|
2769
2797
|
terminalControl,
|
|
@@ -2777,6 +2805,10 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
|
|
|
2777
2805
|
});
|
|
2778
2806
|
const codexLifecycleIncarnationAvailable = session.agent !== "codex" ||
|
|
2779
2807
|
Boolean(nativeProcessUuid && nativeProcessBirth);
|
|
2808
|
+
const nativeInspectionHasBlockingTurn = listConversations(storeDirFromOptions(options)).some((turn) => isDiscoverableTmuxConversation(turn) &&
|
|
2809
|
+
terminalKeyForManagedConversation(turn) ===
|
|
2810
|
+
terminalControlSelectorKey(terminalControl) &&
|
|
2811
|
+
SESSION_SEND_BLOCKING_STATUSES.has(turn.status));
|
|
2780
2812
|
const entry = {
|
|
2781
2813
|
id: bridge.terminalConversationId(session),
|
|
2782
2814
|
short_ref: sessionShortRef(bridge.terminalConversationId(session)),
|
|
@@ -2790,12 +2822,14 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
|
|
|
2790
2822
|
workspace: session.cwd,
|
|
2791
2823
|
elapsed: session.elapsed,
|
|
2792
2824
|
native_agent_session_id: nativeAgentIdentity?.sessionId,
|
|
2825
|
+
native_agent_status_card_session_id: statusCardNativeThreadId,
|
|
2793
2826
|
native_agent_process_uuid: nativeProcessUuid,
|
|
2794
2827
|
native_agent_process_birth: nativeProcessBirth,
|
|
2795
2828
|
native_agent_rollout: nativeAgentIdentity?.rollout,
|
|
2796
2829
|
native_agent_identity_evidence: nativeProcessEvidence,
|
|
2797
2830
|
agent_version: agentVersion,
|
|
2798
2831
|
native_thread_lifecycle: lifecycleCapability,
|
|
2832
|
+
native_inspection: nativeInspectionCapability,
|
|
2799
2833
|
lifecycle_binding_token: lifecycleBindingToken,
|
|
2800
2834
|
confidence: session.confidence,
|
|
2801
2835
|
reason: session.reason,
|
|
@@ -2830,7 +2864,16 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
|
|
|
2830
2864
|
codexLifecycleIncarnationAvailable,
|
|
2831
2865
|
list_resumable_threads: lifecycleCapability.status === "supported" &&
|
|
2832
2866
|
lifecycleCapability.resumeExact === true &&
|
|
2833
|
-
codexLifecycleIncarnationAvailable
|
|
2867
|
+
codexLifecycleIncarnationAvailable,
|
|
2868
|
+
native_inspect: nativeInspectionCapability.status === "supported" &&
|
|
2869
|
+
nativeInspectionCapability.statusInspection === true &&
|
|
2870
|
+
session.agent === "codex" &&
|
|
2871
|
+
codexComposerEmpty(terminalState.screen_excerpt) &&
|
|
2872
|
+
terminalControl.capabilities.includes("send_keys") &&
|
|
2873
|
+
terminalControl.capabilities.includes("screen_status") &&
|
|
2874
|
+
codexLifecycleIncarnationAvailable &&
|
|
2875
|
+
orphanedDispatch === undefined &&
|
|
2876
|
+
!nativeInspectionHasBlockingTurn
|
|
2834
2877
|
}
|
|
2835
2878
|
};
|
|
2836
2879
|
const availableActions = availableListActions(entry);
|
|
@@ -2863,6 +2906,16 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
2863
2906
|
? [...(sessionsByTerminal.get(terminalKey) ?? [])]
|
|
2864
2907
|
: [];
|
|
2865
2908
|
const matchingSessions = relatedSessions.filter((session) => managedSessionMatchesLiveTerminalEntry(session, terminal, storeDir));
|
|
2909
|
+
const conflictingBoundSessionClaims = relatedSessions.flatMap((session) => {
|
|
2910
|
+
const kind = managedBindingConflictKindForLiveTerminalEntry({
|
|
2911
|
+
storeDir,
|
|
2912
|
+
session,
|
|
2913
|
+
terminal
|
|
2914
|
+
});
|
|
2915
|
+
return kind && kind !== "stale_process_incarnation"
|
|
2916
|
+
? [{ session, kind }]
|
|
2917
|
+
: [];
|
|
2918
|
+
});
|
|
2866
2919
|
const unresolvedSessionClaims = relatedSessions.filter((session) => ["transitioning", "quarantined"].includes(session.status) &&
|
|
2867
2920
|
managedSessionClaimsLiveTerminalEntry(session, terminal));
|
|
2868
2921
|
const sessionAuthorityConflict = unresolvedSessionClaims.length > 0
|
|
@@ -2873,13 +2926,42 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
2873
2926
|
transition_ids: unresolvedSessionClaims.map((session) => session.last_transition_id ?? null),
|
|
2874
2927
|
recovery: "use only the lifecycle recovery action listed for this terminal"
|
|
2875
2928
|
}
|
|
2876
|
-
:
|
|
2929
|
+
: conflictingBoundSessionClaims.length === 1
|
|
2877
2930
|
? {
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2931
|
+
kind: conflictingBoundSessionClaims[0].kind,
|
|
2932
|
+
reason: conflictingBoundSessionClaims[0].kind === "provisional_orphan"
|
|
2933
|
+
? "a failed raw attach left a bound Session without an authoritative native-thread identity"
|
|
2934
|
+
: conflictingBoundSessionClaims[0].kind ===
|
|
2935
|
+
"live_external_thread_change"
|
|
2936
|
+
? "the live coding-agent thread changed outside AKK while its previous Session binding remained bound"
|
|
2937
|
+
: "the live terminal no longer matches a bound managed Session and the process relationship is unverifiable",
|
|
2938
|
+
session_ids: [
|
|
2939
|
+
conflictingBoundSessionClaims[0].session.session_id
|
|
2940
|
+
],
|
|
2941
|
+
binding_ids: [
|
|
2942
|
+
conflictingBoundSessionClaims[0].session.binding?.binding_id ?? null
|
|
2943
|
+
],
|
|
2944
|
+
session_revisions: [
|
|
2945
|
+
conflictingBoundSessionClaims[0].session.revision ?? null
|
|
2946
|
+
],
|
|
2947
|
+
recovery: conflictingBoundSessionClaims[0].kind === "unverifiable"
|
|
2948
|
+
? "inspect the terminal and Session identity; AKK cannot safely reconcile an unverifiable binding"
|
|
2949
|
+
: "use only the exact reconcile_binding action listed for this terminal"
|
|
2881
2950
|
}
|
|
2882
|
-
:
|
|
2951
|
+
: conflictingBoundSessionClaims.length > 1
|
|
2952
|
+
? {
|
|
2953
|
+
kind: "ambiguous_bound_claims",
|
|
2954
|
+
reason: "multiple non-exact bound managed Sessions claim the same live terminal",
|
|
2955
|
+
session_ids: conflictingBoundSessionClaims.map(({ session }) => session.session_id),
|
|
2956
|
+
recovery: "inspect Session state; AKK will not reconcile ambiguous claims"
|
|
2957
|
+
}
|
|
2958
|
+
: matchingSessions.length > 1
|
|
2959
|
+
? {
|
|
2960
|
+
reason: "multiple first-class managed Sessions claim the same live terminal binding",
|
|
2961
|
+
session_ids: matchingSessions.map((session) => session.session_id),
|
|
2962
|
+
recovery: "inspect Session state before performing a side effect"
|
|
2963
|
+
}
|
|
2964
|
+
: undefined;
|
|
2883
2965
|
const authoritativeSession = matchingSessions[0];
|
|
2884
2966
|
const discoveredOwnership = terminalControl
|
|
2885
2967
|
? terminalDispatchOwnership(terminalControl)
|
|
@@ -2915,9 +2997,47 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
2915
2997
|
const rawActions = mutationsAllowed
|
|
2916
2998
|
? discoveredRawActions
|
|
2917
2999
|
: readOnlyListActions(discoveredRawActions);
|
|
2918
|
-
const
|
|
3000
|
+
const bindingAwareRawActions = authoritativeSession
|
|
2919
3001
|
? actionsForManagedSessionBinding(rawActions, authoritativeSession)
|
|
2920
3002
|
: rawActions;
|
|
3003
|
+
const sessionAwareRawActions = authoritativeSession &&
|
|
3004
|
+
managedSessionHasUnresolvedNativeTransition(storeDir, authoritativeSession)
|
|
3005
|
+
? Object.fromEntries(Object.entries(bindingAwareRawActions).filter(([actionName]) => actionName !== "native_inspect"))
|
|
3006
|
+
: bindingAwareRawActions;
|
|
3007
|
+
const soleBindingConflict = conflictingBoundSessionClaims.length === 1
|
|
3008
|
+
? conflictingBoundSessionClaims[0]
|
|
3009
|
+
: undefined;
|
|
3010
|
+
const conflictingSessionRevision = Number(soleBindingConflict?.session.revision);
|
|
3011
|
+
const conflictingSessionTurns = soleBindingConflict
|
|
3012
|
+
? managedTurnsForSession(storeDir, soleBindingConflict.session.session_id)
|
|
3013
|
+
: [];
|
|
3014
|
+
const expectedTerminalToken = stringValue(terminal.lifecycle_binding_token);
|
|
3015
|
+
const reconcileBindingAction = mutationsAllowed &&
|
|
3016
|
+
discoveredOwnership.state === "none" &&
|
|
3017
|
+
unresolvedSessionClaims.length === 0 &&
|
|
3018
|
+
matchingSessions.length === 0 &&
|
|
3019
|
+
soleBindingConflict &&
|
|
3020
|
+
soleBindingConflict.kind !== "unverifiable" &&
|
|
3021
|
+
Number.isSafeInteger(conflictingSessionRevision) &&
|
|
3022
|
+
conflictingSessionRevision > 0 &&
|
|
3023
|
+
expectedTerminalToken &&
|
|
3024
|
+
terminal.activity_state === "idle" &&
|
|
3025
|
+
!(isRecord(terminal.approval_state) &&
|
|
3026
|
+
terminal.approval_state.blocked === true) &&
|
|
3027
|
+
!conflictingSessionTurns.some((turn) => SESSION_SEND_BLOCKING_STATUSES.has(turn.status)) &&
|
|
3028
|
+
!managedSessionHasUnresolvedNativeTransition(storeDir, soleBindingConflict.session)
|
|
3029
|
+
? {
|
|
3030
|
+
tool: "agent_knock_knock_reconcile_binding",
|
|
3031
|
+
arguments: {
|
|
3032
|
+
terminal_id: stringValue(terminal.id),
|
|
3033
|
+
conflicting_session_id: soleBindingConflict.session.session_id,
|
|
3034
|
+
expected_session_revision: conflictingSessionRevision,
|
|
3035
|
+
expected_binding_token: managedSessionBindingToken(soleBindingConflict.session),
|
|
3036
|
+
expected_terminal_token: expectedTerminalToken
|
|
3037
|
+
},
|
|
3038
|
+
requires_user_intent: true
|
|
3039
|
+
}
|
|
3040
|
+
: undefined;
|
|
2921
3041
|
const terminalCanAcceptSend = ownership.state === "none" && isRecord(sessionAwareRawActions.send);
|
|
2922
3042
|
if (ownership.state === "current" &&
|
|
2923
3043
|
!allRelated.some((conversation) => conversation.conversation_id === ownership.conversation.conversation_id)) {
|
|
@@ -3004,7 +3124,12 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3004
3124
|
const availableActions = ownership.state === "current"
|
|
3005
3125
|
? currentTerminalActions(currentTurn)
|
|
3006
3126
|
: ownership.state === "conflict"
|
|
3007
|
-
?
|
|
3127
|
+
? {
|
|
3128
|
+
...safeTerminalActionsDuringConflict(sessionAwareRawActions),
|
|
3129
|
+
...(reconcileBindingAction
|
|
3130
|
+
? { reconcile_binding: reconcileBindingAction }
|
|
3131
|
+
: {})
|
|
3132
|
+
}
|
|
3008
3133
|
: managedSessionId &&
|
|
3009
3134
|
sessionBindingMatchesLiveTerminal &&
|
|
3010
3135
|
isRecord(sessionAwareRawActions.send)
|
|
@@ -3128,6 +3253,13 @@ function managedSessionMatchesLiveTerminalEntry(session, terminal, storeDir) {
|
|
|
3128
3253
|
return false;
|
|
3129
3254
|
}
|
|
3130
3255
|
const liveThreadId = stringValue(terminal.native_agent_session_id);
|
|
3256
|
+
const statusCardThreadId = stringValue(terminal.native_agent_status_card_session_id);
|
|
3257
|
+
if (isExactNativeThreadId(binding.native_thread_id) &&
|
|
3258
|
+
isExactNativeThreadId(statusCardThreadId) &&
|
|
3259
|
+
binding.native_thread_id.toLowerCase() !==
|
|
3260
|
+
statusCardThreadId.toLowerCase()) {
|
|
3261
|
+
return false;
|
|
3262
|
+
}
|
|
3131
3263
|
if (!liveThreadId) {
|
|
3132
3264
|
return Boolean(session.agent === "codex" &&
|
|
3133
3265
|
binding.native_thread_id &&
|
|
@@ -3186,11 +3318,116 @@ function managedSessionClaimsLiveTerminalEntry(session, terminal) {
|
|
|
3186
3318
|
: undefined;
|
|
3187
3319
|
return Boolean(binding &&
|
|
3188
3320
|
session.agent === terminal.agent &&
|
|
3189
|
-
binding.terminal_id === stringValue(terminal.id) &&
|
|
3190
3321
|
binding.native_process.pid === Number(terminal.pid) &&
|
|
3191
3322
|
terminalControlSelectorKey(binding.terminal_control) ===
|
|
3192
|
-
terminalControlSelectorKey(liveControl)
|
|
3193
|
-
|
|
3323
|
+
terminalControlSelectorKey(liveControl));
|
|
3324
|
+
}
|
|
3325
|
+
function listedTerminalProcessIncarnation(terminal) {
|
|
3326
|
+
const processUuid = stringValue(terminal.native_agent_process_uuid);
|
|
3327
|
+
const processBirth = stringValue(terminal.native_agent_process_birth);
|
|
3328
|
+
if (terminal.agent !== "codex" ||
|
|
3329
|
+
(processUuid && processBirth)) {
|
|
3330
|
+
return { processUuid, processBirth };
|
|
3331
|
+
}
|
|
3332
|
+
const pid = Number(terminal.pid);
|
|
3333
|
+
if (!Number.isSafeInteger(pid) || pid <= 1) {
|
|
3334
|
+
return { processUuid, processBirth };
|
|
3335
|
+
}
|
|
3336
|
+
try {
|
|
3337
|
+
const incarnation = codexProcessIncarnationForPid(pid);
|
|
3338
|
+
return {
|
|
3339
|
+
processUuid: processUuid ?? incarnation.processUuid,
|
|
3340
|
+
processBirth: processBirth ?? incarnation.processBirth
|
|
3341
|
+
};
|
|
3342
|
+
}
|
|
3343
|
+
catch {
|
|
3344
|
+
return { processUuid, processBirth };
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
function managedBindingConflictKindForLiveTerminalEntry({ storeDir, session, terminal }) {
|
|
3348
|
+
const binding = session.binding;
|
|
3349
|
+
if (session.status !== "bound" ||
|
|
3350
|
+
!binding ||
|
|
3351
|
+
!managedSessionClaimsLiveTerminalEntry(session, terminal) ||
|
|
3352
|
+
managedSessionMatchesLiveTerminalEntry(session, terminal, storeDir)) {
|
|
3353
|
+
return undefined;
|
|
3354
|
+
}
|
|
3355
|
+
const livePid = Number(terminal.pid);
|
|
3356
|
+
const incarnation = listedTerminalProcessIncarnation(terminal);
|
|
3357
|
+
const relationship = processIncarnationRelationship({
|
|
3358
|
+
binding,
|
|
3359
|
+
livePid,
|
|
3360
|
+
liveProcessUuid: incarnation.processUuid,
|
|
3361
|
+
liveProcessBirth: incarnation.processBirth
|
|
3362
|
+
});
|
|
3363
|
+
if (relationship === "different") {
|
|
3364
|
+
return "stale_process_incarnation";
|
|
3365
|
+
}
|
|
3366
|
+
if (binding.terminal_id !== stringValue(terminal.id) ||
|
|
3367
|
+
!matchesConfiguredWorkspace(session.workspace, terminal.workspace ?? terminal.cwd)) {
|
|
3368
|
+
return "unverifiable";
|
|
3369
|
+
}
|
|
3370
|
+
const statusCardThreadId = stringValue(terminal.native_agent_status_card_session_id);
|
|
3371
|
+
const liveNativeThreadId = stringValue(terminal.native_agent_session_id);
|
|
3372
|
+
if (isExactNativeThreadId(binding.native_thread_id) &&
|
|
3373
|
+
isExactNativeThreadId(statusCardThreadId) &&
|
|
3374
|
+
binding.native_thread_id.toLowerCase() !==
|
|
3375
|
+
statusCardThreadId.toLowerCase()) {
|
|
3376
|
+
return relationship === "same" &&
|
|
3377
|
+
isExactNativeThreadId(liveNativeThreadId) &&
|
|
3378
|
+
liveNativeThreadId.toLowerCase() ===
|
|
3379
|
+
statusCardThreadId.toLowerCase()
|
|
3380
|
+
? "live_external_thread_change"
|
|
3381
|
+
: "unverifiable";
|
|
3382
|
+
}
|
|
3383
|
+
if (session.lineage.created_by === "attach" &&
|
|
3384
|
+
!session.last_transition_id &&
|
|
3385
|
+
!binding.native_thread_id &&
|
|
3386
|
+
!binding.native_process.rollout &&
|
|
3387
|
+
managedTurnsForSession(storeDir, session.session_id).length === 0) {
|
|
3388
|
+
return "provisional_orphan";
|
|
3389
|
+
}
|
|
3390
|
+
if (relationship === "same" &&
|
|
3391
|
+
isExactNativeThreadId(binding.native_thread_id) &&
|
|
3392
|
+
isExactNativeThreadId(liveNativeThreadId) &&
|
|
3393
|
+
binding.native_thread_id.toLowerCase() !==
|
|
3394
|
+
liveNativeThreadId.toLowerCase()) {
|
|
3395
|
+
return "live_external_thread_change";
|
|
3396
|
+
}
|
|
3397
|
+
return "unverifiable";
|
|
3398
|
+
}
|
|
3399
|
+
function managedSessionHasUnresolvedNativeTransition(storeDir, session) {
|
|
3400
|
+
const root = nativeThreadTransitionsDir(storeDir);
|
|
3401
|
+
if (!fs.existsSync(root)) {
|
|
3402
|
+
return false;
|
|
3403
|
+
}
|
|
3404
|
+
let entries;
|
|
3405
|
+
try {
|
|
3406
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
3407
|
+
}
|
|
3408
|
+
catch {
|
|
3409
|
+
return true;
|
|
3410
|
+
}
|
|
3411
|
+
for (const entry of entries) {
|
|
3412
|
+
if (!entry.isDirectory()) {
|
|
3413
|
+
continue;
|
|
3414
|
+
}
|
|
3415
|
+
let transition;
|
|
3416
|
+
try {
|
|
3417
|
+
transition = loadNativeThreadTransition(storeDir, entry.name);
|
|
3418
|
+
}
|
|
3419
|
+
catch {
|
|
3420
|
+
return true;
|
|
3421
|
+
}
|
|
3422
|
+
if (transition.source_session_id !== session.session_id &&
|
|
3423
|
+
transition.target_session_id !== session.session_id) {
|
|
3424
|
+
continue;
|
|
3425
|
+
}
|
|
3426
|
+
if (!["committed", "aborted"].includes(transition.status)) {
|
|
3427
|
+
return true;
|
|
3428
|
+
}
|
|
3429
|
+
}
|
|
3430
|
+
return false;
|
|
3194
3431
|
}
|
|
3195
3432
|
function terminalKeyForManagedConversation(conversation) {
|
|
3196
3433
|
return terminalControlSelectorKey(terminalControlFromTakeover(isRecord(conversation.native_session_takeover)
|
|
@@ -3386,7 +3623,7 @@ function sendActionForManagedSession(action, sessionId) {
|
|
|
3386
3623
|
function actionsForManagedSessionBinding(actions, session) {
|
|
3387
3624
|
const token = managedSessionBindingToken(session);
|
|
3388
3625
|
const next = { ...actions };
|
|
3389
|
-
for (const actionName of ["new_thread", "resume_thread"]) {
|
|
3626
|
+
for (const actionName of ["new_thread", "resume_thread", "native_inspect"]) {
|
|
3390
3627
|
const action = isRecord(next[actionName]) ? next[actionName] : undefined;
|
|
3391
3628
|
if (!action) {
|
|
3392
3629
|
continue;
|
|
@@ -3577,7 +3814,10 @@ async function listStateForTerminal(agent, terminalControl, options, bridge = cr
|
|
|
3577
3814
|
},
|
|
3578
3815
|
activity_state: status.activity_state,
|
|
3579
3816
|
activity_reason: status.activity_reason,
|
|
3580
|
-
capability_limitation: status.capability_limitation
|
|
3817
|
+
capability_limitation: status.capability_limitation,
|
|
3818
|
+
// Internal projection evidence; terminalControlledListEntry selects all
|
|
3819
|
+
// public fields explicitly and never exposes the pane excerpt itself.
|
|
3820
|
+
screen_excerpt: status.screen.excerpt
|
|
3581
3821
|
};
|
|
3582
3822
|
}
|
|
3583
3823
|
catch (error) {
|
|
@@ -3651,11 +3891,13 @@ function managedListApprovalState(conversation) {
|
|
|
3651
3891
|
}
|
|
3652
3892
|
function listActionContracts() {
|
|
3653
3893
|
return {
|
|
3654
|
-
version:
|
|
3894
|
+
version: 7,
|
|
3655
3895
|
instructions: [
|
|
3656
3896
|
"Treat terminals[] as the primary resource and use only actions present in available_actions.",
|
|
3657
3897
|
"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
3898
|
"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.",
|
|
3899
|
+
"Native inspection is a separate terminal action: use only its closed inspection enum and current exact terminal_id/token; AKK status does not execute a native slash command.",
|
|
3900
|
+
"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
3901
|
"List resumable threads before resume; use only a complete native_thread_id and the action returned for that candidate.",
|
|
3660
3902
|
"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
3903
|
"Use respond only for an in-flight turn that is explicitly waiting for OpenClaw.",
|
|
@@ -3721,6 +3963,17 @@ function listActionContracts() {
|
|
|
3721
3963
|
required: ["terminal_id"],
|
|
3722
3964
|
side_effect_free: true
|
|
3723
3965
|
},
|
|
3966
|
+
native_inspect: {
|
|
3967
|
+
tool: "agent_knock_knock_native_inspect",
|
|
3968
|
+
target_argument: "terminal_id",
|
|
3969
|
+
required: ["terminal_id", "inspection", "expected_binding_token"],
|
|
3970
|
+
supported_inspections: ["status"],
|
|
3971
|
+
creates_turn: false,
|
|
3972
|
+
creates_session: false,
|
|
3973
|
+
mutates_store: false,
|
|
3974
|
+
sends_terminal_input: true,
|
|
3975
|
+
candidate_source: "terminals[].available_actions.native_inspect"
|
|
3976
|
+
},
|
|
3724
3977
|
resume_thread: {
|
|
3725
3978
|
tool: "agent_knock_knock_resume_thread",
|
|
3726
3979
|
target_argument: "terminal_id",
|
|
@@ -3734,6 +3987,21 @@ function listActionContracts() {
|
|
|
3734
3987
|
requires_user_intent: true,
|
|
3735
3988
|
candidate_source: "list_resumable_threads"
|
|
3736
3989
|
},
|
|
3990
|
+
reconcile_binding: {
|
|
3991
|
+
tool: "agent_knock_knock_reconcile_binding",
|
|
3992
|
+
target_argument: "terminal_id",
|
|
3993
|
+
required: [
|
|
3994
|
+
"terminal_id",
|
|
3995
|
+
"conflicting_session_id",
|
|
3996
|
+
"expected_session_revision",
|
|
3997
|
+
"expected_binding_token",
|
|
3998
|
+
"expected_terminal_token"
|
|
3999
|
+
],
|
|
4000
|
+
creates_turn: false,
|
|
4001
|
+
sends_terminal_input: false,
|
|
4002
|
+
requires_user_intent: true,
|
|
4003
|
+
effect: "Detach one exactly listed conflicting Session binding without adopting the live replacement thread."
|
|
4004
|
+
},
|
|
3737
4005
|
respond: {
|
|
3738
4006
|
tool: "agent_knock_knock_respond",
|
|
3739
4007
|
target_argument: "turn_id",
|
|
@@ -3856,6 +4124,20 @@ function availableListActions(entry, { conversation } = {}) {
|
|
|
3856
4124
|
arguments: { terminal_id: id }
|
|
3857
4125
|
};
|
|
3858
4126
|
}
|
|
4127
|
+
if (terminalControlled &&
|
|
4128
|
+
commands.native_inspect === true &&
|
|
4129
|
+
entry.activity_state === "idle" &&
|
|
4130
|
+
approvalState.blocked !== true &&
|
|
4131
|
+
lifecycleBindingToken) {
|
|
4132
|
+
actions.native_inspect = {
|
|
4133
|
+
tool: "agent_knock_knock_native_inspect",
|
|
4134
|
+
arguments: {
|
|
4135
|
+
terminal_id: id,
|
|
4136
|
+
inspection: "status",
|
|
4137
|
+
expected_binding_token: lifecycleBindingToken
|
|
4138
|
+
}
|
|
4139
|
+
};
|
|
4140
|
+
}
|
|
3859
4141
|
if (managed &&
|
|
3860
4142
|
commands.respond === true &&
|
|
3861
4143
|
terminalBridgeReady &&
|
|
@@ -4266,6 +4548,83 @@ function codexProcessIncarnationForPid(pid) {
|
|
|
4266
4548
|
evidence: "codex_process_birth"
|
|
4267
4549
|
};
|
|
4268
4550
|
}
|
|
4551
|
+
function processIncarnationRelationship({ binding, livePid, liveProcessUuid, liveProcessBirth }) {
|
|
4552
|
+
if (binding.native_process.pid !== livePid) {
|
|
4553
|
+
return "different";
|
|
4554
|
+
}
|
|
4555
|
+
const comparisons = [];
|
|
4556
|
+
if (binding.native_process.process_uuid && liveProcessUuid) {
|
|
4557
|
+
comparisons.push(binding.native_process.process_uuid === liveProcessUuid);
|
|
4558
|
+
}
|
|
4559
|
+
if (binding.native_process.process_birth && liveProcessBirth) {
|
|
4560
|
+
comparisons.push(binding.native_process.process_birth === liveProcessBirth);
|
|
4561
|
+
}
|
|
4562
|
+
if (comparisons.length === 0) {
|
|
4563
|
+
return "unverifiable";
|
|
4564
|
+
}
|
|
4565
|
+
if (comparisons.every(Boolean)) {
|
|
4566
|
+
return "same";
|
|
4567
|
+
}
|
|
4568
|
+
if (comparisons.every((value) => !value)) {
|
|
4569
|
+
return "different";
|
|
4570
|
+
}
|
|
4571
|
+
return "unverifiable";
|
|
4572
|
+
}
|
|
4573
|
+
function resolvedTerminalProcessIncarnation(terminal, identity) {
|
|
4574
|
+
if (terminal.agent !== "codex" ||
|
|
4575
|
+
(identity?.processUuid && identity.processBirth)) {
|
|
4576
|
+
return {
|
|
4577
|
+
processUuid: identity?.processUuid,
|
|
4578
|
+
processBirth: identity?.processBirth
|
|
4579
|
+
};
|
|
4580
|
+
}
|
|
4581
|
+
try {
|
|
4582
|
+
const incarnation = codexProcessIncarnationForPid(terminal.pid);
|
|
4583
|
+
return {
|
|
4584
|
+
processUuid: identity?.processUuid ?? incarnation.processUuid,
|
|
4585
|
+
processBirth: identity?.processBirth ?? incarnation.processBirth
|
|
4586
|
+
};
|
|
4587
|
+
}
|
|
4588
|
+
catch {
|
|
4589
|
+
return {
|
|
4590
|
+
processUuid: identity?.processUuid,
|
|
4591
|
+
processBirth: identity?.processBirth
|
|
4592
|
+
};
|
|
4593
|
+
}
|
|
4594
|
+
}
|
|
4595
|
+
function managedSessionOwnerIsConclusivelyInactive({ session, terminal, identity }) {
|
|
4596
|
+
const binding = session.binding;
|
|
4597
|
+
if (!binding) {
|
|
4598
|
+
return false;
|
|
4599
|
+
}
|
|
4600
|
+
if (binding.native_process.pid !== terminal.pid) {
|
|
4601
|
+
if (!isProcessAlive(binding.native_process.pid)) {
|
|
4602
|
+
return true;
|
|
4603
|
+
}
|
|
4604
|
+
if (session.agent !== "codex") {
|
|
4605
|
+
return false;
|
|
4606
|
+
}
|
|
4607
|
+
try {
|
|
4608
|
+
const ownerIncarnation = codexProcessIncarnationForPid(binding.native_process.pid);
|
|
4609
|
+
return processIncarnationRelationship({
|
|
4610
|
+
binding,
|
|
4611
|
+
livePid: binding.native_process.pid,
|
|
4612
|
+
liveProcessUuid: ownerIncarnation.processUuid,
|
|
4613
|
+
liveProcessBirth: ownerIncarnation.processBirth
|
|
4614
|
+
}) === "different";
|
|
4615
|
+
}
|
|
4616
|
+
catch {
|
|
4617
|
+
return false;
|
|
4618
|
+
}
|
|
4619
|
+
}
|
|
4620
|
+
const incarnation = resolvedTerminalProcessIncarnation(terminal, identity);
|
|
4621
|
+
return processIncarnationRelationship({
|
|
4622
|
+
binding,
|
|
4623
|
+
livePid: terminal.pid,
|
|
4624
|
+
liveProcessUuid: incarnation.processUuid,
|
|
4625
|
+
liveProcessBirth: incarnation.processBirth
|
|
4626
|
+
}) === "different";
|
|
4627
|
+
}
|
|
4269
4628
|
function isCodexStatusCardEvidence(evidence) {
|
|
4270
4629
|
return evidence.split("+").includes("codex_status_card");
|
|
4271
4630
|
}
|
|
@@ -4749,9 +5108,15 @@ function boundManagedSessionForTerminal({ storeDir, terminal, identity }) {
|
|
|
4749
5108
|
}
|
|
4750
5109
|
const conflicting = sessions.filter((session) => session.status === "bound" &&
|
|
4751
5110
|
session.binding &&
|
|
5111
|
+
session.agent === terminal.agent &&
|
|
4752
5112
|
terminalControlSelectorKey(session.binding.terminal_control) ===
|
|
4753
5113
|
terminalControlSelectorKey(terminal.terminalControl) &&
|
|
4754
5114
|
session.binding.native_process.pid === terminal.pid &&
|
|
5115
|
+
!managedSessionOwnerIsConclusivelyInactive({
|
|
5116
|
+
session,
|
|
5117
|
+
terminal,
|
|
5118
|
+
identity
|
|
5119
|
+
}) &&
|
|
4755
5120
|
!exact.includes(session));
|
|
4756
5121
|
if (conflicting.length > 0) {
|
|
4757
5122
|
throw new Error(`terminal ${terminal.terminalControl.target} changed native thread outside AKK; ` +
|
|
@@ -4759,6 +5124,49 @@ function boundManagedSessionForTerminal({ storeDir, terminal, identity }) {
|
|
|
4759
5124
|
}
|
|
4760
5125
|
return exact[0];
|
|
4761
5126
|
}
|
|
5127
|
+
function managedBindingConflictKindForResolvedTerminal({ storeDir, session, terminal, identity }) {
|
|
5128
|
+
const binding = session.binding;
|
|
5129
|
+
if (session.status !== "bound" ||
|
|
5130
|
+
!binding ||
|
|
5131
|
+
session.agent !== terminal.agent ||
|
|
5132
|
+
binding.terminal_id !== terminal.conversationId ||
|
|
5133
|
+
binding.native_process.pid !== terminal.pid ||
|
|
5134
|
+
terminalControlSelectorKey(binding.terminal_control) !==
|
|
5135
|
+
terminalControlSelectorKey(terminal.terminalControl) ||
|
|
5136
|
+
!matchesConfiguredWorkspace(session.workspace, terminal.terminalControl.currentPath) ||
|
|
5137
|
+
bindingMatchesLiveTerminal(session, terminal, identity, storeDir)) {
|
|
5138
|
+
return undefined;
|
|
5139
|
+
}
|
|
5140
|
+
if (managedSessionOwnerIsConclusivelyInactive({
|
|
5141
|
+
session,
|
|
5142
|
+
terminal,
|
|
5143
|
+
identity
|
|
5144
|
+
})) {
|
|
5145
|
+
return "stale_process_incarnation";
|
|
5146
|
+
}
|
|
5147
|
+
if (session.lineage.created_by === "attach" &&
|
|
5148
|
+
!session.last_transition_id &&
|
|
5149
|
+
!binding.native_thread_id &&
|
|
5150
|
+
!binding.native_process.rollout &&
|
|
5151
|
+
managedTurnsForSession(storeDir, session.session_id).length === 0) {
|
|
5152
|
+
return "provisional_orphan";
|
|
5153
|
+
}
|
|
5154
|
+
const incarnation = resolvedTerminalProcessIncarnation(terminal, identity);
|
|
5155
|
+
const relationship = processIncarnationRelationship({
|
|
5156
|
+
binding,
|
|
5157
|
+
livePid: terminal.pid,
|
|
5158
|
+
liveProcessUuid: incarnation.processUuid,
|
|
5159
|
+
liveProcessBirth: incarnation.processBirth
|
|
5160
|
+
});
|
|
5161
|
+
if (relationship === "same" &&
|
|
5162
|
+
isExactNativeThreadId(binding.native_thread_id) &&
|
|
5163
|
+
isExactNativeThreadId(identity?.sessionId) &&
|
|
5164
|
+
binding.native_thread_id.toLowerCase() !==
|
|
5165
|
+
identity.sessionId.toLowerCase()) {
|
|
5166
|
+
return "live_external_thread_change";
|
|
5167
|
+
}
|
|
5168
|
+
return "unverifiable";
|
|
5169
|
+
}
|
|
4762
5170
|
function soleBoundManagedSessionClaimForTerminal(storeDir, terminal) {
|
|
4763
5171
|
const claims = listManagedSessions(storeDir).filter((session) => session.status === "bound" &&
|
|
4764
5172
|
session.binding &&
|
|
@@ -4767,7 +5175,11 @@ function soleBoundManagedSessionClaimForTerminal(storeDir, terminal) {
|
|
|
4767
5175
|
session.binding.native_process.pid === terminal.pid &&
|
|
4768
5176
|
terminalControlSelectorKey(session.binding.terminal_control) ===
|
|
4769
5177
|
terminalControlSelectorKey(terminal.terminalControl) &&
|
|
4770
|
-
matchesConfiguredWorkspace(session.workspace, terminal.terminalControl.currentPath)
|
|
5178
|
+
matchesConfiguredWorkspace(session.workspace, terminal.terminalControl.currentPath) &&
|
|
5179
|
+
!managedSessionOwnerIsConclusivelyInactive({
|
|
5180
|
+
session,
|
|
5181
|
+
terminal
|
|
5182
|
+
}));
|
|
4771
5183
|
if (claims.length > 1) {
|
|
4772
5184
|
throw new Error(`terminal ${terminal.terminalControl.target} has multiple bound managed Session claims`);
|
|
4773
5185
|
}
|
|
@@ -4775,15 +5187,18 @@ function soleBoundManagedSessionClaimForTerminal(storeDir, terminal) {
|
|
|
4775
5187
|
}
|
|
4776
5188
|
function createBoundManagedSession({ sessionId, terminal, identity, nativeThreadId = identity?.sessionId, evidence = identity?.evidence ?? "native_thread_boundary", generation = 1, lineage, now = new Date() }) {
|
|
4777
5189
|
const workspace = terminal.terminalControl.currentPath ?? process.cwd();
|
|
5190
|
+
const codexIncarnation = terminal.agent === "codex" && !identity
|
|
5191
|
+
? codexProcessIncarnationForPid(terminal.pid)
|
|
5192
|
+
: undefined;
|
|
4778
5193
|
const binding = terminalBindingFrom({
|
|
4779
5194
|
terminalId: terminal.conversationId,
|
|
4780
5195
|
terminalControl: terminal.terminalControl,
|
|
4781
5196
|
pid: terminal.pid,
|
|
4782
5197
|
nativeThreadId,
|
|
4783
|
-
processUuid: identity?.processUuid,
|
|
4784
|
-
processBirth: identity?.processBirth,
|
|
5198
|
+
processUuid: identity?.processUuid ?? codexIncarnation?.processUuid,
|
|
5199
|
+
processBirth: identity?.processBirth ?? codexIncarnation?.processBirth,
|
|
4785
5200
|
rollout: identity?.rollout,
|
|
4786
|
-
evidence,
|
|
5201
|
+
evidence: identity?.evidence ?? codexIncarnation?.evidence ?? evidence,
|
|
4787
5202
|
generation,
|
|
4788
5203
|
now
|
|
4789
5204
|
});
|
|
@@ -4865,8 +5280,11 @@ async function reattachManagedSessionForNativeIdentity({ options, terminal, iden
|
|
|
4865
5280
|
});
|
|
4866
5281
|
const previousPid = existing.binding.native_process.pid;
|
|
4867
5282
|
if (existing.status === "bound" &&
|
|
4868
|
-
(
|
|
4869
|
-
|
|
5283
|
+
!managedSessionOwnerIsConclusivelyInactive({
|
|
5284
|
+
session: existing,
|
|
5285
|
+
terminal,
|
|
5286
|
+
identity
|
|
5287
|
+
})) {
|
|
4870
5288
|
throw new Error(`managed Session ${existing.session_id} is still bound to process ${previousPid}`);
|
|
4871
5289
|
}
|
|
4872
5290
|
const now = new Date();
|
|
@@ -5237,7 +5655,11 @@ async function resumableThreadCandidates({ options, terminal, currentIdentity })
|
|
|
5237
5655
|
managedSessionBindingInactive: managed.length === 1 &&
|
|
5238
5656
|
managed[0].status === "bound" &&
|
|
5239
5657
|
Boolean(managed[0].binding) &&
|
|
5240
|
-
|
|
5658
|
+
managedSessionOwnerIsConclusivelyInactive({
|
|
5659
|
+
session: managed[0],
|
|
5660
|
+
terminal,
|
|
5661
|
+
identity: currentIdentity
|
|
5662
|
+
}),
|
|
5241
5663
|
managedSessionWorkspaceMatches: managed.length === 1
|
|
5242
5664
|
? path.resolve(managed[0].workspace) === workspace
|
|
5243
5665
|
: undefined,
|
|
@@ -5597,6 +6019,280 @@ async function runListResumableThreads(options) {
|
|
|
5597
6019
|
}))
|
|
5598
6020
|
});
|
|
5599
6021
|
}
|
|
6022
|
+
function assertSameNativeInspectionTerminal(expected, actual, stage) {
|
|
6023
|
+
const expectedPath = expected.terminalControl.currentPath;
|
|
6024
|
+
const actualPath = actual.terminalControl.currentPath;
|
|
6025
|
+
if (actual.conversationId !== expected.conversationId ||
|
|
6026
|
+
actual.agent !== expected.agent ||
|
|
6027
|
+
actual.pid !== expected.pid ||
|
|
6028
|
+
terminalControlSelectorKey(actual.terminalControl) !==
|
|
6029
|
+
terminalControlSelectorKey(expected.terminalControl) ||
|
|
6030
|
+
!expectedPath ||
|
|
6031
|
+
!actualPath ||
|
|
6032
|
+
path.resolve(actualPath) !== path.resolve(expectedPath)) {
|
|
6033
|
+
throw new Error(`terminal identity, pane, or cwd changed ${stage}; refresh AKK list`);
|
|
6034
|
+
}
|
|
6035
|
+
}
|
|
6036
|
+
function assertTerminalNativeInspectionReady({ options, terminal, terminalStatus, session }) {
|
|
6037
|
+
if (terminalStatus &&
|
|
6038
|
+
(terminalStatus.reachable !== true ||
|
|
6039
|
+
terminalStatus.activity_state !== "idle" ||
|
|
6040
|
+
terminalStatus.approval_state.blocked === true)) {
|
|
6041
|
+
throw new Error(`terminal ${terminal.terminalControl.target} is not at a verified idle prompt ` +
|
|
6042
|
+
`(${terminalStatus.activity_state}: ${terminalStatus.activity_reason})`);
|
|
6043
|
+
}
|
|
6044
|
+
const blocker = listConversations(storeDirFromOptions(options))
|
|
6045
|
+
.filter(isDiscoverableTmuxConversation)
|
|
6046
|
+
.find((turn) => terminalKeyForManagedConversation(turn) ===
|
|
6047
|
+
terminalControlSelectorKey(terminal.terminalControl) &&
|
|
6048
|
+
SESSION_SEND_BLOCKING_STATUSES.has(turn.status));
|
|
6049
|
+
if (blocker) {
|
|
6050
|
+
throw new Error(`terminal ${terminal.terminalControl.target} still has unresolved Turn ` +
|
|
6051
|
+
`${turnIdForConversation(blocker)} (${blocker.status})`);
|
|
6052
|
+
}
|
|
6053
|
+
if (session &&
|
|
6054
|
+
managedSessionHasUnresolvedNativeTransition(storeDirFromOptions(options), session)) {
|
|
6055
|
+
throw new Error(`managed Session ${session.session_id} has an unresolved native-thread transition`);
|
|
6056
|
+
}
|
|
6057
|
+
const ownership = terminalDispatchOwnership(terminal.terminalControl);
|
|
6058
|
+
if (ownership.state !== "none") {
|
|
6059
|
+
throw new Error(`terminal ${terminal.terminalControl.target} has unresolved dispatch ` +
|
|
6060
|
+
"ownership; resolve it before native inspection");
|
|
6061
|
+
}
|
|
6062
|
+
const orphaned = orphanedTerminalDispatchForRecovery(terminal.terminalControl);
|
|
6063
|
+
if (orphaned) {
|
|
6064
|
+
throw new Error(`terminal ${terminal.terminalControl.target} has unresolved ` +
|
|
6065
|
+
`${String(orphaned.kind ?? "terminal")} input ` +
|
|
6066
|
+
`(${String(orphaned.status ?? "unknown")})`);
|
|
6067
|
+
}
|
|
6068
|
+
}
|
|
6069
|
+
function nativeInspectionRuntime({ terminal, snapshot }) {
|
|
6070
|
+
if (terminal.agent !== "codex") {
|
|
6071
|
+
throw new Error("native status inspection currently supports only Codex 0.146.0 and 0.146.1");
|
|
6072
|
+
}
|
|
6073
|
+
const exactRuntimeIdentity = snapshot.runtimeIdentity?.sessionId === snapshot.identity?.sessionId
|
|
6074
|
+
? snapshot.runtimeIdentity
|
|
6075
|
+
: undefined;
|
|
6076
|
+
const runtime = exactRuntimeIdentity
|
|
6077
|
+
? terminalRuntimeForLiveIdentity({
|
|
6078
|
+
terminal,
|
|
6079
|
+
identity: exactRuntimeIdentity
|
|
6080
|
+
})
|
|
6081
|
+
: {
|
|
6082
|
+
...terminalRuntimeForLiveIdentity({
|
|
6083
|
+
terminal,
|
|
6084
|
+
expectedEmptyNativeSession: true
|
|
6085
|
+
}),
|
|
6086
|
+
...(snapshot.identity?.processUuid
|
|
6087
|
+
? { nativeProcessUuid: snapshot.identity.processUuid }
|
|
6088
|
+
: {}),
|
|
6089
|
+
...(snapshot.identity?.processBirth
|
|
6090
|
+
? { nativeProcessBirth: snapshot.identity.processBirth }
|
|
6091
|
+
: {}),
|
|
6092
|
+
...(snapshot.identity?.sessionId
|
|
6093
|
+
? { expectedNativeSessionId: snapshot.identity.sessionId }
|
|
6094
|
+
: {})
|
|
6095
|
+
};
|
|
6096
|
+
return withCodexCompanionFences(runtime, snapshot.codexCompanions);
|
|
6097
|
+
}
|
|
6098
|
+
function assertNativeInspectionSnapshotUnchanged({ expectedTerminal, actualTerminal, expectedBindingToken, expectedVersion, actualSnapshot, stage }) {
|
|
6099
|
+
assertSameNativeInspectionTerminal(expectedTerminal, actualTerminal, stage);
|
|
6100
|
+
if (actualSnapshot.bindingToken !== expectedBindingToken) {
|
|
6101
|
+
throw new Error(`terminal binding changed ${stage}; refresh AKK list`);
|
|
6102
|
+
}
|
|
6103
|
+
if (actualSnapshot.version !== expectedVersion) {
|
|
6104
|
+
throw new Error(`coding-agent version changed ${stage}; refresh AKK list`);
|
|
6105
|
+
}
|
|
6106
|
+
const capability = actualSnapshot.adapter.probeNativeInspection?.(actualSnapshot.version);
|
|
6107
|
+
if (capability?.status !== "supported" ||
|
|
6108
|
+
capability.statusInspection !== true) {
|
|
6109
|
+
throw new Error(capability?.reason ??
|
|
6110
|
+
"native status inspection became unsupported; refresh AKK list");
|
|
6111
|
+
}
|
|
6112
|
+
}
|
|
6113
|
+
async function runNativeInspect(options) {
|
|
6114
|
+
const inspection = required(stringValue(options.inspection), "--inspection is required");
|
|
6115
|
+
if (inspection !== "status") {
|
|
6116
|
+
throw new Error("--inspection must be the closed value status; arbitrary native slash commands are not accepted");
|
|
6117
|
+
}
|
|
6118
|
+
if (options.command !== undefined || options.message !== undefined) {
|
|
6119
|
+
throw new Error("native inspection does not accept a command or message payload");
|
|
6120
|
+
}
|
|
6121
|
+
const expectedBindingToken = required(stringValue(options.expectedBindingToken), "--expected-binding-token is required");
|
|
6122
|
+
const storeDir = storeDirFromOptions(options);
|
|
6123
|
+
const store = inspectStoreCompatibility(storeDir);
|
|
6124
|
+
if (store.writable !== true) {
|
|
6125
|
+
throw new Error("native inspection requires a compatible AKK Store so binding authority can be verified");
|
|
6126
|
+
}
|
|
6127
|
+
const initiallyResolved = await resolveLifecycleTerminal(options);
|
|
6128
|
+
const releaseTerminalLock = acquireFileLock(terminalBridgeSendLockPath(storeDir, initiallyResolved.terminalControl), { timeoutMs: 30000 });
|
|
6129
|
+
try {
|
|
6130
|
+
const terminal = await resolveLifecycleTerminal(options);
|
|
6131
|
+
assertSameNativeInspectionTerminal(initiallyResolved, terminal, "while waiting for native-inspection control");
|
|
6132
|
+
const snapshot = await currentLifecycleSnapshot(options, terminal);
|
|
6133
|
+
if (snapshot.bindingToken !== expectedBindingToken) {
|
|
6134
|
+
throw new Error("terminal binding changed after it was listed; refresh AKK list and retry");
|
|
6135
|
+
}
|
|
6136
|
+
const capability = snapshot.adapter.probeNativeInspection?.(snapshot.version);
|
|
6137
|
+
if (capability?.status !== "supported" ||
|
|
6138
|
+
capability.statusInspection !== true) {
|
|
6139
|
+
throw new Error(capability?.reason ??
|
|
6140
|
+
"native status inspection is unavailable for this agent version");
|
|
6141
|
+
}
|
|
6142
|
+
const plan = snapshot.adapter.planNativeInspection?.({ kind: "status" }, capability);
|
|
6143
|
+
if (!plan ||
|
|
6144
|
+
plan.operation.kind !== "status" ||
|
|
6145
|
+
plan.command !== "/status" ||
|
|
6146
|
+
plan.effect !== "read_only") {
|
|
6147
|
+
throw new Error("the agent adapter did not produce the closed native status inspection plan");
|
|
6148
|
+
}
|
|
6149
|
+
const runtime = nativeInspectionRuntime({ terminal, snapshot });
|
|
6150
|
+
const bridge = createTerminalAgentBridge(options);
|
|
6151
|
+
const initialStatus = await bridge.status(terminal.agent, terminal.terminalControl, { runtime });
|
|
6152
|
+
assertTerminalNativeInspectionReady({
|
|
6153
|
+
options,
|
|
6154
|
+
terminal,
|
|
6155
|
+
terminalStatus: initialStatus,
|
|
6156
|
+
session: snapshot.session
|
|
6157
|
+
});
|
|
6158
|
+
await assertCodexComposerReadyForAutomatedInput({
|
|
6159
|
+
options,
|
|
6160
|
+
terminalControl: terminal.terminalControl
|
|
6161
|
+
});
|
|
6162
|
+
let submission;
|
|
6163
|
+
try {
|
|
6164
|
+
submission = await bridge.submitNativeInspection(terminal.agent, terminal.terminalControl, plan, {
|
|
6165
|
+
runtime,
|
|
6166
|
+
beforeEnter: async () => {
|
|
6167
|
+
const finalTerminal = await resolveLifecycleTerminal(options);
|
|
6168
|
+
const finalSnapshot = await currentLifecycleSnapshot(options, finalTerminal);
|
|
6169
|
+
assertNativeInspectionSnapshotUnchanged({
|
|
6170
|
+
expectedTerminal: terminal,
|
|
6171
|
+
actualTerminal: finalTerminal,
|
|
6172
|
+
expectedBindingToken,
|
|
6173
|
+
expectedVersion: snapshot.version,
|
|
6174
|
+
actualSnapshot: finalSnapshot,
|
|
6175
|
+
stage: "immediately before native status submission"
|
|
6176
|
+
});
|
|
6177
|
+
assertTerminalNativeInspectionReady({
|
|
6178
|
+
options,
|
|
6179
|
+
terminal: finalTerminal,
|
|
6180
|
+
session: finalSnapshot.session
|
|
6181
|
+
});
|
|
6182
|
+
}
|
|
6183
|
+
});
|
|
6184
|
+
}
|
|
6185
|
+
catch (error) {
|
|
6186
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
6187
|
+
if (error instanceof NativeInspectionSubmissionError &&
|
|
6188
|
+
error.doNotRetry !== true) {
|
|
6189
|
+
throw new Error(`native status inspection did not start; refresh AKK list and retry if still desired: ${detail}`);
|
|
6190
|
+
}
|
|
6191
|
+
throw new Error("native status inspection did not cross a proven completion boundary; " +
|
|
6192
|
+
`do not retry automatically: ${detail}`);
|
|
6193
|
+
}
|
|
6194
|
+
try {
|
|
6195
|
+
const expectedNativeThreadId = snapshot.session?.binding?.native_thread_id ??
|
|
6196
|
+
snapshot.identity?.sessionId;
|
|
6197
|
+
let stableEvidenceFingerprint;
|
|
6198
|
+
let stableObservation;
|
|
6199
|
+
let stableCount = 0;
|
|
6200
|
+
for (let attempt = 0; attempt < 50; attempt += 1) {
|
|
6201
|
+
const observed = await bridge.observeNativeInspection(terminal.agent, terminal.terminalControl, {
|
|
6202
|
+
operation: plan.operation,
|
|
6203
|
+
previousScreenFingerprint: submission.preEnterScreenDigest,
|
|
6204
|
+
preEnterEvidenceInventory: submission.preEnterEvidenceInventory,
|
|
6205
|
+
expectedNativeThreadId,
|
|
6206
|
+
expectedAgentVersion: snapshot.version
|
|
6207
|
+
}, { runtime, scrollbackLines: 240 });
|
|
6208
|
+
const observation = observed.observation;
|
|
6209
|
+
if (observed.status.reachable === true &&
|
|
6210
|
+
observed.status.activity_state === "idle" &&
|
|
6211
|
+
observed.status.approval_state.blocked !== true &&
|
|
6212
|
+
observed.screenDigest !== submission.preEnterScreenDigest &&
|
|
6213
|
+
observation.status === "observed" &&
|
|
6214
|
+
observation.result?.kind === "native_status" &&
|
|
6215
|
+
isExactNativeThreadId(observation.nativeThreadId) &&
|
|
6216
|
+
observation.evidenceFingerprint) {
|
|
6217
|
+
if (stableEvidenceFingerprint === observation.evidenceFingerprint) {
|
|
6218
|
+
stableCount += 1;
|
|
6219
|
+
}
|
|
6220
|
+
else {
|
|
6221
|
+
stableEvidenceFingerprint = observation.evidenceFingerprint;
|
|
6222
|
+
stableObservation = observation;
|
|
6223
|
+
stableCount = 1;
|
|
6224
|
+
}
|
|
6225
|
+
if (stableCount >= 2) {
|
|
6226
|
+
stableObservation = observation;
|
|
6227
|
+
break;
|
|
6228
|
+
}
|
|
6229
|
+
}
|
|
6230
|
+
else {
|
|
6231
|
+
stableEvidenceFingerprint = undefined;
|
|
6232
|
+
stableObservation = undefined;
|
|
6233
|
+
stableCount = 0;
|
|
6234
|
+
}
|
|
6235
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
6236
|
+
}
|
|
6237
|
+
if (!stableObservation || stableCount < 2) {
|
|
6238
|
+
throw new Error("native status inspection Enter was dispatched exactly once, but a fresh exact idle status result was not proven; do not retry automatically");
|
|
6239
|
+
}
|
|
6240
|
+
const finalTerminal = await resolveLifecycleTerminal(options);
|
|
6241
|
+
const finalSnapshot = await currentLifecycleSnapshot(options, finalTerminal);
|
|
6242
|
+
assertNativeInspectionSnapshotUnchanged({
|
|
6243
|
+
expectedTerminal: terminal,
|
|
6244
|
+
actualTerminal: finalTerminal,
|
|
6245
|
+
expectedBindingToken,
|
|
6246
|
+
expectedVersion: snapshot.version,
|
|
6247
|
+
actualSnapshot: finalSnapshot,
|
|
6248
|
+
stage: "after native status inspection"
|
|
6249
|
+
});
|
|
6250
|
+
const finalStatus = await bridge.status(finalTerminal.agent, finalTerminal.terminalControl, { runtime });
|
|
6251
|
+
assertTerminalNativeInspectionReady({
|
|
6252
|
+
options,
|
|
6253
|
+
terminal: finalTerminal,
|
|
6254
|
+
terminalStatus: finalStatus,
|
|
6255
|
+
session: finalSnapshot.session
|
|
6256
|
+
});
|
|
6257
|
+
await assertCodexComposerReadyForAutomatedInput({
|
|
6258
|
+
options,
|
|
6259
|
+
terminalControl: finalTerminal.terminalControl
|
|
6260
|
+
});
|
|
6261
|
+
printJson({
|
|
6262
|
+
status: "observed",
|
|
6263
|
+
inspection: "status",
|
|
6264
|
+
terminal_id: terminal.conversationId,
|
|
6265
|
+
agent: terminal.agent,
|
|
6266
|
+
agent_version: snapshot.version,
|
|
6267
|
+
behavior_profile: plan.behaviorProfile,
|
|
6268
|
+
native_thread_id: stableObservation.nativeThreadId,
|
|
6269
|
+
native_status: stableObservation.result,
|
|
6270
|
+
terminal_submission: {
|
|
6271
|
+
command: plan.command,
|
|
6272
|
+
enter_count: submission.enterCount,
|
|
6273
|
+
materialization: submission.materialization
|
|
6274
|
+
},
|
|
6275
|
+
store_mutation: false,
|
|
6276
|
+
session_created: false,
|
|
6277
|
+
turn_created: false,
|
|
6278
|
+
receipt_created: false,
|
|
6279
|
+
monitor_created: false,
|
|
6280
|
+
callback_created: false
|
|
6281
|
+
});
|
|
6282
|
+
}
|
|
6283
|
+
catch (error) {
|
|
6284
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
6285
|
+
if (/do not retry automatically/iu.test(detail)) {
|
|
6286
|
+
throw error;
|
|
6287
|
+
}
|
|
6288
|
+
throw new Error("native status inspection Enter was dispatched exactly once, but its " +
|
|
6289
|
+
`postcondition became uncertain; do not retry automatically: ${detail}`);
|
|
6290
|
+
}
|
|
6291
|
+
}
|
|
6292
|
+
finally {
|
|
6293
|
+
releaseTerminalLock();
|
|
6294
|
+
}
|
|
6295
|
+
}
|
|
5600
6296
|
function previousCommittedResumeCandidate({ storeDir, terminal, currentSession, candidates }) {
|
|
5601
6297
|
if (!currentSession?.last_transition_id) {
|
|
5602
6298
|
return undefined;
|
|
@@ -5951,6 +6647,161 @@ async function runResumeThread(options) {
|
|
|
5951
6647
|
selectionSnapshot: selection.snapshot
|
|
5952
6648
|
});
|
|
5953
6649
|
}
|
|
6650
|
+
async function runReconcileBinding(options) {
|
|
6651
|
+
const initiallyResolved = await resolveLifecycleTerminal(options);
|
|
6652
|
+
const storeDir = storeDirFromOptions(options);
|
|
6653
|
+
const conflictingSessionId = required(stringValue(options.conflictingSession ?? options.conflictingSessionId), "--conflicting-session is required");
|
|
6654
|
+
const expectedRevisionValue = required(stringValue(options.expectedSessionRevision ?? options.sessionRevision), "--expected-session-revision is required");
|
|
6655
|
+
if (!/^[1-9][0-9]*$/u.test(expectedRevisionValue)) {
|
|
6656
|
+
throw new Error("--expected-session-revision must be a positive integer");
|
|
6657
|
+
}
|
|
6658
|
+
const expectedSessionRevision = Number(expectedRevisionValue);
|
|
6659
|
+
if (!Number.isSafeInteger(expectedSessionRevision)) {
|
|
6660
|
+
throw new Error("--expected-session-revision must be a positive safe integer");
|
|
6661
|
+
}
|
|
6662
|
+
const expectedBindingToken = required(stringValue(options.expectedBindingToken), "--expected-binding-token is required");
|
|
6663
|
+
const expectedTerminalToken = required(stringValue(options.expectedTerminalToken), "--expected-terminal-token is required");
|
|
6664
|
+
const releaseTerminalLock = acquireFileLock(terminalBridgeSendLockPath(storeDir, initiallyResolved.terminalControl), { timeoutMs: 30000 });
|
|
6665
|
+
try {
|
|
6666
|
+
return await withStoreWriterLeaseAsync(storeDir, async () => {
|
|
6667
|
+
const terminal = await resolveLifecycleTerminal(options);
|
|
6668
|
+
if (terminal.pid !== initiallyResolved.pid ||
|
|
6669
|
+
terminal.conversationId !== initiallyResolved.conversationId ||
|
|
6670
|
+
terminalControlSelectorKey(terminal.terminalControl) !==
|
|
6671
|
+
terminalControlSelectorKey(initiallyResolved.terminalControl)) {
|
|
6672
|
+
throw new Error("terminal identity changed while waiting to reconcile its binding; refresh AKK list");
|
|
6673
|
+
}
|
|
6674
|
+
await recoverLifecycleFenceBeforeMutation({ options, terminal });
|
|
6675
|
+
const dispatchOwnership = terminalDispatchOwnership(terminal.terminalControl);
|
|
6676
|
+
if (dispatchOwnership.state !== "none") {
|
|
6677
|
+
throw new Error("the terminal acquired an unresolved dispatch after the binding conflict was listed; refresh AKK list");
|
|
6678
|
+
}
|
|
6679
|
+
const session = loadManagedSession(storeDir, conflictingSessionId);
|
|
6680
|
+
if (session.revision !== expectedSessionRevision ||
|
|
6681
|
+
managedSessionBindingToken(session) !== expectedBindingToken) {
|
|
6682
|
+
throw new Error("managed Session binding changed after it was listed; refresh AKK list");
|
|
6683
|
+
}
|
|
6684
|
+
const binding = session.binding;
|
|
6685
|
+
if (session.status !== "bound" ||
|
|
6686
|
+
!binding ||
|
|
6687
|
+
session.agent !== terminal.agent ||
|
|
6688
|
+
binding.terminal_id !== terminal.conversationId ||
|
|
6689
|
+
binding.native_process.pid !== terminal.pid ||
|
|
6690
|
+
terminalControlSelectorKey(binding.terminal_control) !==
|
|
6691
|
+
terminalControlSelectorKey(terminal.terminalControl) ||
|
|
6692
|
+
!matchesConfiguredWorkspace(session.workspace, terminal.terminalControl.currentPath)) {
|
|
6693
|
+
throw new Error("the listed managed Session no longer claims this exact terminal");
|
|
6694
|
+
}
|
|
6695
|
+
const identity = await resolveCurrentNativeAgentSessionIdentity({
|
|
6696
|
+
options,
|
|
6697
|
+
agent: terminal.agent,
|
|
6698
|
+
pid: terminal.pid,
|
|
6699
|
+
cwd: terminal.terminalControl.currentPath
|
|
6700
|
+
});
|
|
6701
|
+
const terminalToken = lifecycleBindingToken({
|
|
6702
|
+
terminal,
|
|
6703
|
+
identity
|
|
6704
|
+
});
|
|
6705
|
+
if (terminalToken !== expectedTerminalToken) {
|
|
6706
|
+
throw new Error("live terminal identity changed after the conflict was listed; refresh AKK list");
|
|
6707
|
+
}
|
|
6708
|
+
const bridge = createTerminalAgentBridge(options);
|
|
6709
|
+
if (managedSessionHasUnresolvedNativeTransition(storeDir, session)) {
|
|
6710
|
+
throw new Error(`managed Session ${session.session_id} has an unresolved native-thread transition`);
|
|
6711
|
+
}
|
|
6712
|
+
const blockers = managedTurnsForSession(storeDir, session.session_id).filter((turn) => SESSION_SEND_BLOCKING_STATUSES.has(turn.status));
|
|
6713
|
+
if (blockers.length > 0) {
|
|
6714
|
+
throw new Error(`managed Session ${session.session_id} still has unresolved Turn ` +
|
|
6715
|
+
`${turnIdForConversation(blockers[0])} (${blockers[0].status})`);
|
|
6716
|
+
}
|
|
6717
|
+
const finalTerminal = await resolveLifecycleTerminal(options);
|
|
6718
|
+
if (finalTerminal.pid !== terminal.pid ||
|
|
6719
|
+
finalTerminal.conversationId !== terminal.conversationId ||
|
|
6720
|
+
terminalControlSelectorKey(finalTerminal.terminalControl) !==
|
|
6721
|
+
terminalControlSelectorKey(terminal.terminalControl)) {
|
|
6722
|
+
throw new Error("terminal identity changed during binding reconciliation; refresh AKK list");
|
|
6723
|
+
}
|
|
6724
|
+
const finalStatus = await bridge.status(finalTerminal.agent, finalTerminal.terminalControl, {
|
|
6725
|
+
runtime: terminalRuntimeForLiveIdentity({
|
|
6726
|
+
terminal: finalTerminal,
|
|
6727
|
+
physicalOnly: true
|
|
6728
|
+
})
|
|
6729
|
+
});
|
|
6730
|
+
assertTerminalLifecycleReady({
|
|
6731
|
+
options,
|
|
6732
|
+
terminal: finalTerminal,
|
|
6733
|
+
terminalStatus: finalStatus
|
|
6734
|
+
});
|
|
6735
|
+
const finalIdentity = await resolveCurrentNativeAgentSessionIdentity({
|
|
6736
|
+
options,
|
|
6737
|
+
agent: finalTerminal.agent,
|
|
6738
|
+
pid: finalTerminal.pid,
|
|
6739
|
+
cwd: finalTerminal.terminalControl.currentPath
|
|
6740
|
+
});
|
|
6741
|
+
if (lifecycleBindingToken({
|
|
6742
|
+
terminal: finalTerminal,
|
|
6743
|
+
identity: finalIdentity
|
|
6744
|
+
}) !== expectedTerminalToken) {
|
|
6745
|
+
throw new Error("live terminal identity changed during binding reconciliation; refresh AKK list");
|
|
6746
|
+
}
|
|
6747
|
+
const finalSession = loadManagedSession(storeDir, conflictingSessionId);
|
|
6748
|
+
if (finalSession.revision !== expectedSessionRevision ||
|
|
6749
|
+
managedSessionBindingToken(finalSession) !== expectedBindingToken) {
|
|
6750
|
+
throw new Error("managed Session binding changed during reconciliation; refresh AKK list");
|
|
6751
|
+
}
|
|
6752
|
+
const conflictKind = managedBindingConflictKindForResolvedTerminal({
|
|
6753
|
+
storeDir,
|
|
6754
|
+
session: finalSession,
|
|
6755
|
+
terminal: finalTerminal,
|
|
6756
|
+
identity: finalIdentity
|
|
6757
|
+
});
|
|
6758
|
+
if (![
|
|
6759
|
+
"provisional_orphan",
|
|
6760
|
+
"live_external_thread_change"
|
|
6761
|
+
].includes(String(conflictKind))) {
|
|
6762
|
+
throw new Error(conflictKind === "stale_process_incarnation"
|
|
6763
|
+
? "the stale process incarnation no longer requires explicit reconciliation; refresh AKK list"
|
|
6764
|
+
: conflictKind === undefined
|
|
6765
|
+
? "the managed Session now exactly matches the live terminal; no reconciliation is needed"
|
|
6766
|
+
: "the managed binding conflict is unverifiable and cannot be detached automatically");
|
|
6767
|
+
}
|
|
6768
|
+
const reconciledAt = new Date().toISOString();
|
|
6769
|
+
const detached = saveManagedSession(storeDir, {
|
|
6770
|
+
...finalSession,
|
|
6771
|
+
status: "detached",
|
|
6772
|
+
detached_at: reconciledAt,
|
|
6773
|
+
updated_at: reconciledAt
|
|
6774
|
+
}, {
|
|
6775
|
+
expectedRevision: expectedSessionRevision
|
|
6776
|
+
});
|
|
6777
|
+
runtimeLog("info", "managed_binding_reconciled", {
|
|
6778
|
+
terminal_id: terminal.conversationId,
|
|
6779
|
+
terminal_target: terminal.terminalControl.target,
|
|
6780
|
+
session_id: detached.session_id,
|
|
6781
|
+
binding_id: detached.binding?.binding_id,
|
|
6782
|
+
previous_revision: expectedSessionRevision,
|
|
6783
|
+
revision: detached.revision,
|
|
6784
|
+
conflict_kind: conflictKind,
|
|
6785
|
+
terminal_input_sent: false
|
|
6786
|
+
});
|
|
6787
|
+
printJson({
|
|
6788
|
+
status: "reconciled",
|
|
6789
|
+
outcome: "detached_conflicting_binding",
|
|
6790
|
+
conflict_kind: conflictKind,
|
|
6791
|
+
terminal_id: terminal.conversationId,
|
|
6792
|
+
session_id: detached.session_id,
|
|
6793
|
+
binding_id: detached.binding?.binding_id,
|
|
6794
|
+
session_revision: detached.revision,
|
|
6795
|
+
terminal_input_sent: false,
|
|
6796
|
+
turn_created: false,
|
|
6797
|
+
refresh_required: true
|
|
6798
|
+
});
|
|
6799
|
+
});
|
|
6800
|
+
}
|
|
6801
|
+
finally {
|
|
6802
|
+
releaseTerminalLock();
|
|
6803
|
+
}
|
|
6804
|
+
}
|
|
5954
6805
|
function assertResumeSnapshotMatchesTerminal(snapshot, terminal) {
|
|
5955
6806
|
const workspace = path.resolve(terminal.terminalControl.currentPath ?? process.cwd());
|
|
5956
6807
|
if (snapshot.terminal_id !== terminal.conversationId ||
|
|
@@ -6173,8 +7024,11 @@ async function runNativeThreadTransition(options, operation) {
|
|
|
6173
7024
|
`${turnIdForConversation(targetBlockers[0])}`);
|
|
6174
7025
|
}
|
|
6175
7026
|
if (targetSession?.status === "bound") {
|
|
6176
|
-
|
|
6177
|
-
|
|
7027
|
+
if (!managedSessionOwnerIsConclusivelyInactive({
|
|
7028
|
+
session: targetSession,
|
|
7029
|
+
terminal,
|
|
7030
|
+
identity: beforeIdentity
|
|
7031
|
+
})) {
|
|
6178
7032
|
throw new Error(`target Session ${candidate.managed_session_id} is still bound to a live or unverifiable process`);
|
|
6179
7033
|
}
|
|
6180
7034
|
const detachedAt = new Date().toISOString();
|
|
@@ -7286,6 +8140,7 @@ async function runSend(options) {
|
|
|
7286
8140
|
storeDir: rawStoreDir
|
|
7287
8141
|
});
|
|
7288
8142
|
}
|
|
8143
|
+
let pendingRawAttachSessionCreate;
|
|
7289
8144
|
if (!managedSession) {
|
|
7290
8145
|
if (currentNativeIdentity) {
|
|
7291
8146
|
await assertNativeThreadHasExclusiveOwnership({
|
|
@@ -7303,7 +8158,13 @@ async function runSend(options) {
|
|
|
7303
8158
|
identity: currentNativeIdentity,
|
|
7304
8159
|
lineage: { created_by: "attach" }
|
|
7305
8160
|
});
|
|
7306
|
-
|
|
8161
|
+
if (terminalConversation.agent === "codex" &&
|
|
8162
|
+
!currentNativeIdentity) {
|
|
8163
|
+
pendingRawAttachSessionCreate = managedSession;
|
|
8164
|
+
}
|
|
8165
|
+
else {
|
|
8166
|
+
managedSession = saveManagedSession(rawStoreDir, managedSession, { expectedRevision: null });
|
|
8167
|
+
}
|
|
7307
8168
|
}
|
|
7308
8169
|
const logicalNativeIdentity = logicalIdentityForManagedSession({
|
|
7309
8170
|
storeDir: rawStoreDir,
|
|
@@ -7378,6 +8239,31 @@ async function runSend(options) {
|
|
|
7378
8239
|
storeWriterLeaseHeld: true,
|
|
7379
8240
|
recordMessageAfterSend: true,
|
|
7380
8241
|
recordRawAttachmentAfterSend: reusableTurn === undefined,
|
|
8242
|
+
onTerminalPreflightVerified: pendingRawAttachSessionCreate
|
|
8243
|
+
? () => {
|
|
8244
|
+
const createdSession = saveManagedSession(rawStoreDir, pendingRawAttachSessionCreate, { expectedRevision: null });
|
|
8245
|
+
managedSession = createdSession;
|
|
8246
|
+
pendingRawAttachSessionCreate = undefined;
|
|
8247
|
+
return () => {
|
|
8248
|
+
const current = loadManagedSession(rawStoreDir, createdSession.session_id);
|
|
8249
|
+
if (current.status !== "bound" ||
|
|
8250
|
+
current.revision !== createdSession.revision ||
|
|
8251
|
+
managedSessionBindingToken(current) !==
|
|
8252
|
+
managedSessionBindingToken(createdSession)) {
|
|
8253
|
+
throw new Error(`new raw-attach Session ${createdSession.session_id} changed before pre-transport rollback`);
|
|
8254
|
+
}
|
|
8255
|
+
const detachedAt = new Date().toISOString();
|
|
8256
|
+
managedSession = saveManagedSession(rawStoreDir, {
|
|
8257
|
+
...current,
|
|
8258
|
+
status: "detached",
|
|
8259
|
+
detached_at: detachedAt,
|
|
8260
|
+
updated_at: detachedAt
|
|
8261
|
+
}, {
|
|
8262
|
+
expectedRevision: current.revision
|
|
8263
|
+
});
|
|
8264
|
+
};
|
|
8265
|
+
}
|
|
8266
|
+
: undefined,
|
|
7381
8267
|
allowedPreMaterializationIdentity,
|
|
7382
8268
|
allowedAdditionalIdentities
|
|
7383
8269
|
});
|
|
@@ -8306,7 +9192,7 @@ async function runTerminalConversationApprove({ options, conversationId, agent,
|
|
|
8306
9192
|
releaseTerminalLock();
|
|
8307
9193
|
}
|
|
8308
9194
|
}
|
|
8309
|
-
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 }) {
|
|
9195
|
+
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 }) {
|
|
8310
9196
|
const bridge = terminalBridgeEnabled(conversation);
|
|
8311
9197
|
if (!terminalSendLockHeld) {
|
|
8312
9198
|
const releaseTerminalLock = acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), terminalControl), { timeoutMs: 30000 });
|
|
@@ -8325,6 +9211,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
8325
9211
|
storeWriterLeaseHeld,
|
|
8326
9212
|
recordMessageAfterSend,
|
|
8327
9213
|
recordRawAttachmentAfterSend,
|
|
9214
|
+
onTerminalPreflightVerified,
|
|
8328
9215
|
allowedPreMaterializationIdentity,
|
|
8329
9216
|
allowedAdditionalIdentities,
|
|
8330
9217
|
continuingTurnResponse
|
|
@@ -8364,6 +9251,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
8364
9251
|
storeWriterLeaseHeld,
|
|
8365
9252
|
recordMessageAfterSend,
|
|
8366
9253
|
recordRawAttachmentAfterSend,
|
|
9254
|
+
onTerminalPreflightVerified,
|
|
8367
9255
|
allowedPreMaterializationIdentity,
|
|
8368
9256
|
allowedAdditionalIdentities,
|
|
8369
9257
|
continuingTurnResponse
|
|
@@ -8389,6 +9277,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
8389
9277
|
storeWriterLeaseHeld: true,
|
|
8390
9278
|
recordMessageAfterSend,
|
|
8391
9279
|
recordRawAttachmentAfterSend,
|
|
9280
|
+
onTerminalPreflightVerified,
|
|
8392
9281
|
allowedPreMaterializationIdentity,
|
|
8393
9282
|
allowedAdditionalIdentities,
|
|
8394
9283
|
continuingTurnResponse
|
|
@@ -8623,6 +9512,31 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
8623
9512
|
catch (error) {
|
|
8624
9513
|
throw new Error(`refusing to send to ${executor.display_name} without a verified idle terminal: ${error instanceof Error ? error.message : String(error)}`);
|
|
8625
9514
|
}
|
|
9515
|
+
// A newly discovered raw terminal may not have an authoritative Session
|
|
9516
|
+
// yet. Commit that Session only after every pre-input terminal and native
|
|
9517
|
+
// acceptance check has passed, but before the Turn or dispatch ledger can
|
|
9518
|
+
// become durable. This prevents a failed virgin attach from leaving a
|
|
9519
|
+
// zero-identity `bound` Session that fences every later control action.
|
|
9520
|
+
let rollbackPreTransportAttach = onTerminalPreflightVerified?.();
|
|
9521
|
+
const rollbackRawAttachBeforeTransport = () => {
|
|
9522
|
+
if (!rollbackPreTransportAttach) {
|
|
9523
|
+
return true;
|
|
9524
|
+
}
|
|
9525
|
+
const rollback = rollbackPreTransportAttach;
|
|
9526
|
+
rollbackPreTransportAttach = undefined;
|
|
9527
|
+
try {
|
|
9528
|
+
rollback();
|
|
9529
|
+
return true;
|
|
9530
|
+
}
|
|
9531
|
+
catch (error) {
|
|
9532
|
+
runtimeLog("error", "raw_attach_pre_transport_rollback_failed", {
|
|
9533
|
+
conversation_id: conversation.conversation_id,
|
|
9534
|
+
terminal_target: terminalControl.target,
|
|
9535
|
+
error: error instanceof Error ? error.message : String(error)
|
|
9536
|
+
});
|
|
9537
|
+
return false;
|
|
9538
|
+
}
|
|
9539
|
+
};
|
|
8626
9540
|
const bridgeConversation = bridge
|
|
8627
9541
|
? withTerminalBridgeState({
|
|
8628
9542
|
conversation: nextConversation,
|
|
@@ -8646,33 +9560,53 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
8646
9560
|
status: "prepared",
|
|
8647
9561
|
preparedAt: bridgeStartedAt
|
|
8648
9562
|
});
|
|
8649
|
-
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
|
|
8657
|
-
|
|
8658
|
-
|
|
8659
|
-
|
|
8660
|
-
|
|
8661
|
-
|
|
8662
|
-
|
|
8663
|
-
|
|
8664
|
-
|
|
8665
|
-
stringValue(previousDispatchLedger?.
|
|
8666
|
-
|
|
9563
|
+
try {
|
|
9564
|
+
saveTerminalBridgeDispatchLedger(terminalControl, {
|
|
9565
|
+
...terminalBindingLedgerFields(preparedConversation),
|
|
9566
|
+
status: "prepared",
|
|
9567
|
+
generation_id: message.id,
|
|
9568
|
+
conversation_id: preparedConversation.conversation_id,
|
|
9569
|
+
session_id: sessionIdForConversation(preparedConversation),
|
|
9570
|
+
turn_id: turnIdForConversation(preparedConversation),
|
|
9571
|
+
message_id: message.id,
|
|
9572
|
+
message_type: message.type,
|
|
9573
|
+
request_hash: terminalRequestHash,
|
|
9574
|
+
prepared_at: bridgeStartedAt,
|
|
9575
|
+
dispatcher_pid: process.pid,
|
|
9576
|
+
state_path: statePath,
|
|
9577
|
+
event_log_path: logPath,
|
|
9578
|
+
callback_expected: Boolean(preparedConversation.gateway_method),
|
|
9579
|
+
previous_generation_id: stringValue(previousDispatchLedger?.generation_id) ??
|
|
9580
|
+
stringValue(previousDispatchLedger?.message_id)
|
|
9581
|
+
});
|
|
9582
|
+
}
|
|
9583
|
+
catch (error) {
|
|
9584
|
+
try {
|
|
9585
|
+
restoreTerminalBridgeDispatchLedger({
|
|
9586
|
+
terminalControl,
|
|
9587
|
+
previousLedger: previousDispatchLedger,
|
|
9588
|
+
reason: "prepared ledger persistence failed before tmux input"
|
|
9589
|
+
});
|
|
9590
|
+
}
|
|
9591
|
+
finally {
|
|
9592
|
+
rollbackRawAttachBeforeTransport();
|
|
9593
|
+
}
|
|
9594
|
+
throw error;
|
|
9595
|
+
}
|
|
8667
9596
|
try {
|
|
8668
9597
|
saveState(statePath, preparedConversation);
|
|
8669
9598
|
}
|
|
8670
9599
|
catch (error) {
|
|
8671
|
-
|
|
8672
|
-
|
|
8673
|
-
|
|
8674
|
-
|
|
8675
|
-
|
|
9600
|
+
try {
|
|
9601
|
+
restoreTerminalBridgeDispatchLedger({
|
|
9602
|
+
terminalControl,
|
|
9603
|
+
previousLedger: previousDispatchLedger,
|
|
9604
|
+
reason: "prepared state persistence failed before tmux input"
|
|
9605
|
+
});
|
|
9606
|
+
}
|
|
9607
|
+
finally {
|
|
9608
|
+
rollbackRawAttachBeforeTransport();
|
|
9609
|
+
}
|
|
8676
9610
|
throw error;
|
|
8677
9611
|
}
|
|
8678
9612
|
let bridgeMonitor;
|
|
@@ -8745,6 +9679,8 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
8745
9679
|
: String(ledgerError)
|
|
8746
9680
|
});
|
|
8747
9681
|
}
|
|
9682
|
+
const rawAttachRolledBack = rollbackRawAttachBeforeTransport();
|
|
9683
|
+
const durableAbortCanBeRetryable = dispatchLedgerRestored && rawAttachRolledBack;
|
|
8748
9684
|
const failureBase = recordRawAttachmentAfterSend
|
|
8749
9685
|
? {
|
|
8750
9686
|
...preparedConversation,
|
|
@@ -8769,7 +9705,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
8769
9705
|
preparedAt: bridgeStartedAt,
|
|
8770
9706
|
abortedAt,
|
|
8771
9707
|
error: errorMessage,
|
|
8772
|
-
safeToRetry:
|
|
9708
|
+
safeToRetry: durableAbortCanBeRetryable
|
|
8773
9709
|
});
|
|
8774
9710
|
let abortedStatePersisted = false;
|
|
8775
9711
|
try {
|
|
@@ -8788,7 +9724,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
8788
9724
|
: String(persistenceError)
|
|
8789
9725
|
});
|
|
8790
9726
|
}
|
|
8791
|
-
const safeToRetry =
|
|
9727
|
+
const safeToRetry = durableAbortCanBeRetryable && abortedStatePersisted;
|
|
8792
9728
|
try {
|
|
8793
9729
|
appendEvent(logPath, {
|
|
8794
9730
|
ts: abortedAt,
|
|
@@ -8830,7 +9766,8 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
8830
9766
|
error: errorMessage,
|
|
8831
9767
|
safe_to_retry: safeToRetry,
|
|
8832
9768
|
dispatch_ledger_restored: dispatchLedgerRestored,
|
|
8833
|
-
aborted_state_persisted: abortedStatePersisted
|
|
9769
|
+
aborted_state_persisted: abortedStatePersisted,
|
|
9770
|
+
raw_attach_rolled_back: rawAttachRolledBack
|
|
8834
9771
|
});
|
|
8835
9772
|
printJson({
|
|
8836
9773
|
session_id: sessionIdForConversation(abortedConversation),
|
|
@@ -8851,7 +9788,9 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
8851
9788
|
? "AKK failed before touching tmux; this terminal submission was not sent and may be retried."
|
|
8852
9789
|
: !dispatchLedgerRestored
|
|
8853
9790
|
? "AKK failed before tmux input but could not restore the terminal dispatch ledger; inspect and close the conversation before retrying."
|
|
8854
|
-
:
|
|
9791
|
+
: !rawAttachRolledBack
|
|
9792
|
+
? "AKK failed before tmux input but could not detach the provisional raw-attach Session; inspect its exact binding before retrying."
|
|
9793
|
+
: "AKK failed before tmux input but could not persist the aborted receipt; inspect the conversation before retrying.",
|
|
8855
9794
|
openclaw_next_action: {
|
|
8856
9795
|
action: safeToRetry ? "retry" : "inspect",
|
|
8857
9796
|
conversation_id: abortedConversation.conversation_id,
|
|
@@ -8863,7 +9802,9 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
8863
9802
|
? "The failure occurred before any tmux input."
|
|
8864
9803
|
: !dispatchLedgerRestored
|
|
8865
9804
|
? "The terminal ledger could not be restored automatically."
|
|
8866
|
-
:
|
|
9805
|
+
: !rawAttachRolledBack
|
|
9806
|
+
? "The provisional raw-attach Session could not be detached automatically."
|
|
9807
|
+
: "The aborted receipt could not be made durable."
|
|
8867
9808
|
}
|
|
8868
9809
|
});
|
|
8869
9810
|
return;
|
|
@@ -9262,6 +10203,149 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
9262
10203
|
}
|
|
9263
10204
|
}
|
|
9264
10205
|
catch (error) {
|
|
10206
|
+
if (!textInjectedAt &&
|
|
10207
|
+
error instanceof TerminalInputNotStartedError) {
|
|
10208
|
+
const abortedAt = new Date().toISOString();
|
|
10209
|
+
const errorMessage = error.message;
|
|
10210
|
+
let dispatchLedgerRestored = true;
|
|
10211
|
+
try {
|
|
10212
|
+
restoreTerminalBridgeDispatchLedger({
|
|
10213
|
+
terminalControl,
|
|
10214
|
+
previousLedger: previousDispatchLedger,
|
|
10215
|
+
reason: "terminal transport was proved not to have started"
|
|
10216
|
+
});
|
|
10217
|
+
}
|
|
10218
|
+
catch (ledgerError) {
|
|
10219
|
+
dispatchLedgerRestored = false;
|
|
10220
|
+
runtimeLog("error", "terminal_dispatch_ledger_restore_failed", {
|
|
10221
|
+
conversation_id: conversation.conversation_id,
|
|
10222
|
+
terminal_target: terminalControl.target,
|
|
10223
|
+
error: ledgerError instanceof Error
|
|
10224
|
+
? ledgerError.message
|
|
10225
|
+
: String(ledgerError)
|
|
10226
|
+
});
|
|
10227
|
+
}
|
|
10228
|
+
const rawAttachRolledBack = rollbackRawAttachBeforeTransport();
|
|
10229
|
+
const durableAbortCanBeRetryable = dispatchLedgerRestored && rawAttachRolledBack;
|
|
10230
|
+
const failureBase = recordRawAttachmentAfterSend
|
|
10231
|
+
? {
|
|
10232
|
+
...preparedConversation,
|
|
10233
|
+
status: "failed",
|
|
10234
|
+
failed_at: abortedAt,
|
|
10235
|
+
failure_reason: "terminal transport failed before terminal input"
|
|
10236
|
+
}
|
|
10237
|
+
: {
|
|
10238
|
+
...preparedConversation,
|
|
10239
|
+
status: conversation.status,
|
|
10240
|
+
...(conversation.idle_since
|
|
10241
|
+
? { idle_since: conversation.idle_since }
|
|
10242
|
+
: {})
|
|
10243
|
+
};
|
|
10244
|
+
const abortedConversation = withTerminalBridgeSubmission({
|
|
10245
|
+
conversation: failureBase,
|
|
10246
|
+
messageId: message.id,
|
|
10247
|
+
messageType: message.type,
|
|
10248
|
+
messageBody: String(message.body),
|
|
10249
|
+
requestText: terminalPayload,
|
|
10250
|
+
status: "aborted",
|
|
10251
|
+
preparedAt: bridgeStartedAt,
|
|
10252
|
+
abortedAt,
|
|
10253
|
+
error: errorMessage,
|
|
10254
|
+
safeToRetry: durableAbortCanBeRetryable
|
|
10255
|
+
});
|
|
10256
|
+
let abortedStatePersisted = false;
|
|
10257
|
+
try {
|
|
10258
|
+
saveState(statePath, abortedConversation);
|
|
10259
|
+
abortedStatePersisted = true;
|
|
10260
|
+
}
|
|
10261
|
+
catch (persistenceError) {
|
|
10262
|
+
runtimeLog("error", "terminal_message_submit_aborted_persist_failed", {
|
|
10263
|
+
conversation_id: abortedConversation.conversation_id,
|
|
10264
|
+
terminal_target: terminalControl.target,
|
|
10265
|
+
error: persistenceError instanceof Error
|
|
10266
|
+
? persistenceError.message
|
|
10267
|
+
: String(persistenceError)
|
|
10268
|
+
});
|
|
10269
|
+
}
|
|
10270
|
+
const safeToRetry = durableAbortCanBeRetryable && abortedStatePersisted;
|
|
10271
|
+
const reportedConversation = safeToRetry
|
|
10272
|
+
? abortedConversation
|
|
10273
|
+
: withTerminalBridgeSubmission({
|
|
10274
|
+
conversation: abortedConversation,
|
|
10275
|
+
messageId: message.id,
|
|
10276
|
+
messageType: message.type,
|
|
10277
|
+
messageBody: String(message.body),
|
|
10278
|
+
requestText: terminalPayload,
|
|
10279
|
+
status: "aborted",
|
|
10280
|
+
preparedAt: bridgeStartedAt,
|
|
10281
|
+
abortedAt,
|
|
10282
|
+
error: errorMessage,
|
|
10283
|
+
safeToRetry: false
|
|
10284
|
+
});
|
|
10285
|
+
try {
|
|
10286
|
+
appendEvent(logPath, {
|
|
10287
|
+
ts: abortedAt,
|
|
10288
|
+
conversation_id: abortedConversation.conversation_id,
|
|
10289
|
+
event: "terminal_message_submit_aborted",
|
|
10290
|
+
message_id: message.id,
|
|
10291
|
+
executor,
|
|
10292
|
+
terminal_control: terminalControl,
|
|
10293
|
+
error: textSummary(errorMessage),
|
|
10294
|
+
safe_to_retry: safeToRetry,
|
|
10295
|
+
terminal_input_started: false
|
|
10296
|
+
});
|
|
10297
|
+
}
|
|
10298
|
+
catch (persistenceError) {
|
|
10299
|
+
runtimeLog("error", "terminal_message_submit_aborted_event_failed", {
|
|
10300
|
+
conversation_id: abortedConversation.conversation_id,
|
|
10301
|
+
terminal_target: terminalControl.target,
|
|
10302
|
+
error: persistenceError instanceof Error
|
|
10303
|
+
? persistenceError.message
|
|
10304
|
+
: String(persistenceError)
|
|
10305
|
+
});
|
|
10306
|
+
}
|
|
10307
|
+
runtimeLog("error", "terminal_message_submit_aborted", {
|
|
10308
|
+
conversation_id: abortedConversation.conversation_id,
|
|
10309
|
+
terminal_target: terminalControl.target,
|
|
10310
|
+
error: errorMessage,
|
|
10311
|
+
safe_to_retry: safeToRetry,
|
|
10312
|
+
terminal_input_started: false,
|
|
10313
|
+
dispatch_ledger_restored: dispatchLedgerRestored,
|
|
10314
|
+
aborted_state_persisted: abortedStatePersisted,
|
|
10315
|
+
raw_attach_rolled_back: rawAttachRolledBack
|
|
10316
|
+
});
|
|
10317
|
+
printJson({
|
|
10318
|
+
session_id: sessionIdForConversation(reportedConversation),
|
|
10319
|
+
turn_id: turnIdForConversation(reportedConversation),
|
|
10320
|
+
conversation: reportedConversation,
|
|
10321
|
+
message,
|
|
10322
|
+
delivered: false,
|
|
10323
|
+
status: "submission_aborted",
|
|
10324
|
+
submission_outcome: "aborted",
|
|
10325
|
+
background: true,
|
|
10326
|
+
callback_expected: false,
|
|
10327
|
+
terminal_control: terminalControl,
|
|
10328
|
+
monitor_pid: bridgeMonitor?.pid ?? null,
|
|
10329
|
+
executor,
|
|
10330
|
+
safe_to_retry: safeToRetry,
|
|
10331
|
+
do_not_retry: !safeToRetry,
|
|
10332
|
+
reason: safeToRetry
|
|
10333
|
+
? "AKK proved that terminal input never started; this submission may be retried."
|
|
10334
|
+
: "AKK proved terminal input never started but could not make every abort receipt and Session rollback durable; inspect before retrying.",
|
|
10335
|
+
openclaw_next_action: {
|
|
10336
|
+
action: safeToRetry ? "retry" : "inspect",
|
|
10337
|
+
conversation_id: reportedConversation.conversation_id,
|
|
10338
|
+
session_id: sessionIdForConversation(reportedConversation),
|
|
10339
|
+
turn_id: turnIdForConversation(reportedConversation),
|
|
10340
|
+
safe_to_retry: safeToRetry,
|
|
10341
|
+
do_not_retry: !safeToRetry,
|
|
10342
|
+
reason: safeToRetry
|
|
10343
|
+
? "The terminal transport failed before any input operation succeeded."
|
|
10344
|
+
: "The pre-input failure could not be fully reconciled in durable state."
|
|
10345
|
+
}
|
|
10346
|
+
});
|
|
10347
|
+
return;
|
|
10348
|
+
}
|
|
9265
10349
|
const uncertainAt = new Date().toISOString();
|
|
9266
10350
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
9267
10351
|
const failureBase = stagedConversation;
|
|
@@ -9876,8 +10960,8 @@ function assertOrdinaryTerminalPayloadDoesNotInvokeNativeLifecycle(payload) {
|
|
|
9876
10960
|
return;
|
|
9877
10961
|
}
|
|
9878
10962
|
throw new Error(`ordinary send/respond cannot invoke native slash command /${reserved[1].toLowerCase()}; ` +
|
|
9879
|
-
"use
|
|
9880
|
-
"
|
|
10963
|
+
"use an advertised dedicated native action when one exists, or enter " +
|
|
10964
|
+
"the unsupported native command manually in tmux");
|
|
9881
10965
|
}
|
|
9882
10966
|
function isCompleteNativeRollout(value) {
|
|
9883
10967
|
return isRecord(value) &&
|
|
@@ -18356,8 +19440,10 @@ function usage() {
|
|
|
18356
19440
|
agent-knock-knock new-thread --terminal <exact-terminal-id> --expected-binding-token <token>
|
|
18357
19441
|
agent-knock-knock clear-thread --terminal <exact-terminal-id> --expected-binding-token <token>
|
|
18358
19442
|
agent-knock-knock list-resumable-threads --terminal <exact-terminal-id> [--selection-scope <opaque-scope>]
|
|
19443
|
+
agent-knock-knock native-inspect --terminal <exact-terminal-id> --inspection status --expected-binding-token <token>
|
|
18359
19444
|
agent-knock-knock resume-thread --terminal <exact-terminal-id> --native-thread <uuid> --expected-binding-token <token> --candidate-token <token>
|
|
18360
19445
|
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>
|
|
19446
|
+
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>
|
|
18361
19447
|
agent-knock-knock respond --turn <turn-id|selector> --message <text> [--conversation <selector>]
|
|
18362
19448
|
agent-knock-knock approve [--turn <turn-id|selector>] [--conversation <selector>] --expected-approval-fingerprint <fingerprint>
|
|
18363
19449
|
agent-knock-knock cancel [--turn <turn-id|selector>] [--conversation <selector>]
|