@scotthuang/agent-knock-knock 0.12.13 → 0.12.15

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.
Files changed (170) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +13 -13
  3. package/dist/src/callback-cli-adapter.d.ts +9 -6
  4. package/dist/src/callback-cli-adapter.js +119 -30
  5. package/dist/src/callback-cli-adapter.js.map +1 -1
  6. package/dist/src/callback-outbox-policy.d.ts +39 -0
  7. package/dist/src/callback-outbox-policy.js +163 -6
  8. package/dist/src/callback-outbox-policy.js.map +1 -1
  9. package/dist/src/callback-outbox-service.d.ts +28 -1
  10. package/dist/src/callback-outbox-service.js +413 -82
  11. package/dist/src/callback-outbox-service.js.map +1 -1
  12. package/dist/src/callback-outbox-settlement.d.ts +14 -1
  13. package/dist/src/callback-outbox-settlement.js +151 -30
  14. package/dist/src/callback-outbox-settlement.js.map +1 -1
  15. package/dist/src/callback-route-authority.d.ts +59 -0
  16. package/dist/src/callback-route-authority.js +123 -0
  17. package/dist/src/callback-route-authority.js.map +1 -0
  18. package/dist/src/callback-transport.d.ts +134 -0
  19. package/dist/src/callback-transport.js +256 -0
  20. package/dist/src/callback-transport.js.map +1 -0
  21. package/dist/src/cli-core.js +56 -18
  22. package/dist/src/cli-core.js.map +1 -1
  23. package/dist/src/cli-runtime-context.d.ts +2 -0
  24. package/dist/src/cli-runtime-context.js.map +1 -1
  25. package/dist/src/codex-lifecycle-compatibility.js +2 -1
  26. package/dist/src/codex-lifecycle-compatibility.js.map +1 -1
  27. package/dist/src/deferred-foreground-application-service.d.ts +18 -0
  28. package/dist/src/deferred-foreground-application-service.js +72 -0
  29. package/dist/src/deferred-foreground-application-service.js.map +1 -1
  30. package/dist/src/deferred-foreground-authority-cli-adapter.js +10 -1
  31. package/dist/src/deferred-foreground-authority-cli-adapter.js.map +1 -1
  32. package/dist/src/deferred-foreground-recovery-cli-adapter.d.ts +1 -1
  33. package/dist/src/deferred-foreground-recovery-cli-adapter.js +108 -37
  34. package/dist/src/deferred-foreground-recovery-cli-adapter.js.map +1 -1
  35. package/dist/src/deferred-foreground-recovery-service.d.ts +1 -1
  36. package/dist/src/deferred-foreground-recovery-service.js +10 -3
  37. package/dist/src/deferred-foreground-recovery-service.js.map +1 -1
  38. package/dist/src/deferred-foreground-transfer-policy.d.ts +1 -1
  39. package/dist/src/deferred-foreground-transfer-policy.js +3 -2
  40. package/dist/src/deferred-foreground-transfer-policy.js.map +1 -1
  41. package/dist/src/deferred-foreground-transfer.d.ts +17 -1
  42. package/dist/src/deferred-foreground-transfer.js +244 -35
  43. package/dist/src/deferred-foreground-transfer.js.map +1 -1
  44. package/dist/src/deferred-foreground-user-close.d.ts +18 -0
  45. package/dist/src/deferred-foreground-user-close.js +180 -0
  46. package/dist/src/deferred-foreground-user-close.js.map +1 -0
  47. package/dist/src/install-doctor-command-adapter.js +1 -1
  48. package/dist/src/install-doctor-command-adapter.js.map +1 -1
  49. package/dist/src/openclaw-callback-transport.d.ts +4 -1
  50. package/dist/src/openclaw-callback-transport.js +403 -10
  51. package/dist/src/openclaw-callback-transport.js.map +1 -1
  52. package/dist/src/openclaw-doctor.js +16 -2
  53. package/dist/src/openclaw-doctor.js.map +1 -1
  54. package/dist/src/openclaw-managed-callback-cli-adapter.d.ts +23 -0
  55. package/dist/src/openclaw-managed-callback-cli-adapter.js +77 -0
  56. package/dist/src/openclaw-managed-callback-cli-adapter.js.map +1 -0
  57. package/dist/src/openclaw-plugin-command-adapter.js +93 -39
  58. package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
  59. package/dist/src/openclaw-plugin-helpers.d.ts +1 -0
  60. package/dist/src/openclaw-plugin-helpers.js +9 -1
  61. package/dist/src/openclaw-plugin-helpers.js.map +1 -1
  62. package/dist/src/openclaw-plugin-schemas.d.ts +20 -1
  63. package/dist/src/openclaw-plugin-schemas.js +27 -3
  64. package/dist/src/openclaw-plugin-schemas.js.map +1 -1
  65. package/dist/src/protocol.d.ts +3 -0
  66. package/dist/src/protocol.js +4 -0
  67. package/dist/src/protocol.js.map +1 -1
  68. package/dist/src/store.d.ts +12 -1
  69. package/dist/src/store.js +81 -3
  70. package/dist/src/store.js.map +1 -1
  71. package/dist/src/terminal-acceptance-cli-adapter.d.ts +2 -0
  72. package/dist/src/terminal-acceptance-cli-adapter.js +62 -12
  73. package/dist/src/terminal-acceptance-cli-adapter.js.map +1 -1
  74. package/dist/src/terminal-action-projection.d.ts +31 -3
  75. package/dist/src/terminal-action-projection.js +64 -9
  76. package/dist/src/terminal-action-projection.js.map +1 -1
  77. package/dist/src/terminal-agent-bridge.d.ts +82 -0
  78. package/dist/src/terminal-agent-bridge.js +553 -109
  79. package/dist/src/terminal-agent-bridge.js.map +1 -1
  80. package/dist/src/terminal-command-cli-adapter.d.ts +26 -2
  81. package/dist/src/terminal-command-cli-adapter.js +3162 -214
  82. package/dist/src/terminal-command-cli-adapter.js.map +1 -1
  83. package/dist/src/terminal-delegate-cli-adapter.d.ts +4 -22
  84. package/dist/src/terminal-delegate-cli-adapter.js +239 -192
  85. package/dist/src/terminal-delegate-cli-adapter.js.map +1 -1
  86. package/dist/src/terminal-delegate-send-binding.d.ts +63 -0
  87. package/dist/src/terminal-delegate-send-binding.js +360 -0
  88. package/dist/src/terminal-delegate-send-binding.js.map +1 -0
  89. package/dist/src/terminal-dispatch-application.d.ts +4 -1
  90. package/dist/src/terminal-dispatch-application.js +11 -2
  91. package/dist/src/terminal-dispatch-application.js.map +1 -1
  92. package/dist/src/terminal-dispatch-composition.d.ts +5 -0
  93. package/dist/src/terminal-dispatch-execution.js +3 -1
  94. package/dist/src/terminal-dispatch-execution.js.map +1 -1
  95. package/dist/src/terminal-dispatch-ledger-codec.d.ts +3 -0
  96. package/dist/src/terminal-dispatch-ledger-codec.js +46 -19
  97. package/dist/src/terminal-dispatch-ledger-codec.js.map +1 -1
  98. package/dist/src/terminal-dispatch-presenter.js +5 -3
  99. package/dist/src/terminal-dispatch-presenter.js.map +1 -1
  100. package/dist/src/terminal-dispatch-receipt.d.ts +3 -0
  101. package/dist/src/terminal-dispatch-receipt.js +15 -1
  102. package/dist/src/terminal-dispatch-receipt.js.map +1 -1
  103. package/dist/src/terminal-dispatch-recovery-cli-adapter.js +68 -7
  104. package/dist/src/terminal-dispatch-recovery-cli-adapter.js.map +1 -1
  105. package/dist/src/terminal-dispatch-recovery-service.d.ts +2 -0
  106. package/dist/src/terminal-dispatch-recovery-service.js +9 -0
  107. package/dist/src/terminal-dispatch-recovery-service.js.map +1 -1
  108. package/dist/src/terminal-dispatch-repository-cli-adapter.d.ts +4 -2
  109. package/dist/src/terminal-dispatch-repository-cli-adapter.js +10 -3
  110. package/dist/src/terminal-dispatch-repository-cli-adapter.js.map +1 -1
  111. package/dist/src/terminal-identity-authority-cli-adapter.d.ts +2 -0
  112. package/dist/src/terminal-identity-authority-cli-adapter.js +34 -27
  113. package/dist/src/terminal-identity-authority-cli-adapter.js.map +1 -1
  114. package/dist/src/terminal-list-cli-adapter.d.ts +6 -0
  115. package/dist/src/terminal-list-cli-adapter.js +391 -177
  116. package/dist/src/terminal-list-cli-adapter.js.map +1 -1
  117. package/dist/src/terminal-list-renderer.d.ts +9 -1
  118. package/dist/src/terminal-list-renderer.js +226 -104
  119. package/dist/src/terminal-list-renderer.js.map +1 -1
  120. package/dist/src/terminal-maintenance-cli-adapter.js +253 -326
  121. package/dist/src/terminal-maintenance-cli-adapter.js.map +1 -1
  122. package/dist/src/terminal-monitor-application-service.d.ts +5 -0
  123. package/dist/src/terminal-monitor-application-service.js +5 -0
  124. package/dist/src/terminal-monitor-application-service.js.map +1 -1
  125. package/dist/src/terminal-monitor-cli-adapter.d.ts +25 -1
  126. package/dist/src/terminal-monitor-cli-adapter.js +170 -27
  127. package/dist/src/terminal-monitor-cli-adapter.js.map +1 -1
  128. package/dist/src/terminal-monitor-reconciliation-eligibility.d.ts +1 -1
  129. package/dist/src/terminal-monitor-reconciliation-eligibility.js +220 -32
  130. package/dist/src/terminal-monitor-reconciliation-eligibility.js.map +1 -1
  131. package/dist/src/terminal-monitor-state-cli-adapter.d.ts +1 -1
  132. package/dist/src/terminal-monitor-state-cli-adapter.js +808 -147
  133. package/dist/src/terminal-monitor-state-cli-adapter.js.map +1 -1
  134. package/dist/src/terminal-monitor-state-reconciliation-service.d.ts +2 -1
  135. package/dist/src/terminal-monitor-state-reconciliation-service.js +12 -0
  136. package/dist/src/terminal-monitor-state-reconciliation-service.js.map +1 -1
  137. package/dist/src/terminal-monitor-supervision-cli-adapter.d.ts +2 -0
  138. package/dist/src/terminal-monitor-supervision-cli-adapter.js +31 -11
  139. package/dist/src/terminal-monitor-supervision-cli-adapter.js.map +1 -1
  140. package/dist/src/terminal-process-source.d.ts +13 -0
  141. package/dist/src/terminal-process-source.js +37 -0
  142. package/dist/src/terminal-process-source.js.map +1 -1
  143. package/dist/src/terminal-runtime-cli-adapter.js +14 -2
  144. package/dist/src/terminal-runtime-cli-adapter.js.map +1 -1
  145. package/dist/src/terminal-status-cli-adapter.d.ts +1 -0
  146. package/dist/src/terminal-status-cli-adapter.js +30 -28
  147. package/dist/src/terminal-status-cli-adapter.js.map +1 -1
  148. package/dist/src/terminal-submission-acceptance.js +78 -25
  149. package/dist/src/terminal-submission-acceptance.js.map +1 -1
  150. package/dist/src/terminal-submission-retry-service.d.ts +114 -0
  151. package/dist/src/terminal-submission-retry-service.js +589 -0
  152. package/dist/src/terminal-submission-retry-service.js.map +1 -0
  153. package/dist/src/terminal-user-send-intent.d.ts +65 -0
  154. package/dist/src/terminal-user-send-intent.js +317 -0
  155. package/dist/src/terminal-user-send-intent.js.map +1 -0
  156. package/dist/src/terminal-watch-callback-cli-adapter.d.ts +10 -2
  157. package/dist/src/terminal-watch-callback-cli-adapter.js +76 -37
  158. package/dist/src/terminal-watch-callback-cli-adapter.js.map +1 -1
  159. package/dist/src/terminal-watch-cli-adapter.js +41 -17
  160. package/dist/src/terminal-watch-cli-adapter.js.map +1 -1
  161. package/dist/src/terminal-watch-service.d.ts +11 -6
  162. package/dist/src/terminal-watch-service.js +210 -41
  163. package/dist/src/terminal-watch-service.js.map +1 -1
  164. package/dist/src/terminal-watch-store.d.ts +23 -0
  165. package/dist/src/terminal-watch-store.js +162 -4
  166. package/dist/src/terminal-watch-store.js.map +1 -1
  167. package/docs/quickstart-herdr.md +1 -1
  168. package/docs/quickstart-tmux.md +3 -3
  169. package/package.json +1 -1
  170. package/templates/openclaw-skills/agent-knock-knock/SKILL.md +14 -11
@@ -1,9 +1,10 @@
1
1
  // Raw CLI composition for renew, cancel, and close maintenance commands.
2
2
  import { AsyncLocalStorage } from "node:async_hooks";
3
3
  import path from "node:path";
4
+ import { supersedeCallbackNotificationDelivery, supersedeUnacceptedCallbackDeliveries } from "./callback-outbox-policy.js";
4
5
  import { withCanonicalMutationLocks } from "./mutation-transaction.js";
5
6
  import { budgetAction, executorForConversation, sessionIdForConversation, isSessionSendBlockingStatus, turnIdForConversation } from "./protocol.js";
6
- import { appendEvent, loadState, logPathForStatePath, pathsForConversationDir, saveState, withStoreWriterLeaseAsync } from "./store.js";
7
+ import { appendEvent, appendExplicitUserCloseEvent, loadState, logPathForStatePath, pathsForConversationDir, saveExplicitUserCloseState, saveState, withStoreWriterLeaseAsync } from "./store.js";
7
8
  import { loadManagedSession, tryLoadManagedSession } from "./session-store.js";
8
9
  import { TerminalControlUnavailableError } from "./terminal-control-provider.js";
9
10
  import { terminalControlsShareIncarnation } from "./terminal-authority-policy.js";
@@ -13,11 +14,12 @@ import { terminalBridgeSubmission } from "./terminal-dispatch-receipt.js";
13
14
  import { terminalDispatchLedgerLooksLifecycle } from "./terminal-dispatch-ledger-codec.js";
14
15
  import { loadDeferredForegroundTransfer } from "./deferred-foreground-transfer.js";
15
16
  import { isFinalDeferredForegroundTransferStatus } from "./deferred-foreground-transfer-policy.js";
17
+ import { cleanupDeferredForegroundUserClose } from "./deferred-foreground-user-close.js";
16
18
  import { isRecoverableTerminalDispatchStatus } from "./terminal-dispatch-policy.js";
17
19
  import { decideVerifiedDeadAgentProcess } from "./verified-dead-agent-policy.js";
18
20
  import { terminalMonitorDeadlineAt as deadlineAt } from "./terminal-monitor-decision-policy.js";
19
21
  import { writeCliJson as printJson } from "./cli-command-runtime.js";
20
- import { cliCwd, cliEnv, cliExit, cliNow, cliNowMs, cliRuntimeLog as runtimeLog } from "./cli-runtime-context.js";
22
+ import { cliCwd, cliNow, cliNowMs, cliRuntimeLog as runtimeLog } from "./cli-runtime-context.js";
21
23
  import { isRecord, nonBlankString as stringValue } from "./value-guards.js";
22
24
  const terminalMaintenanceContext = new AsyncLocalStorage();
23
25
  function terminalMaintenanceRuntime() {
@@ -137,94 +139,100 @@ async function runRenew(options) {
137
139
  let renewedTerminalControl = terminalControl;
138
140
  let inactivityTimeoutMinutes = 0;
139
141
  let hardTimeoutMinutes = 0;
140
- const releaseStateLock = acquireFileLock(`${statePath}.lock`);
141
- try {
142
- const current = loadState(statePath);
143
- if (current.status !== "stalled") {
144
- throw new Error(`cannot renew ${current.conversation_id}; conversation is ${current.status}, not stalled`);
145
- }
146
- if (isVerifiedDeadTerminalAgentProcess(current)) {
147
- throw new Error(`cannot renew ${current.conversation_id}; its bound terminal agent ` +
148
- "process is verified dead");
149
- }
150
- if (terminalBridgeSubmission(current)?.status === "uncertain") {
151
- throw new Error(`cannot renew ${current.conversation_id}; its terminal submission is uncertain`);
152
- }
153
- const currentTakeover = isRecord(current.native_session_takeover)
154
- ? current.native_session_takeover
155
- : undefined;
156
- const currentControl = terminalControlFromTakeover(currentTakeover);
157
- if (!currentControl || currentTakeover?.terminal_bridge !== true) {
158
- throw new Error(`cannot renew ${current.conversation_id}; conversation is not a terminal bridge task`);
159
- }
160
- if (current.conversation_id !== conversation.conversation_id ||
161
- !terminalControlsShareIncarnation(currentControl, terminalControl) ||
162
- stringValue(currentTakeover.terminal_bridge_message_id) !== expectedMessageId ||
163
- stringValue(currentTakeover.terminal_bridge_started_at) !== expectedStartedAt) {
164
- throw new Error("conversation changed while waiting to renew; refresh status and retry");
142
+ const writerStoreDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
143
+ await withStoreWriterLease(writerStoreDir, async () => {
144
+ const releaseStateLock = acquireFileLock(`${statePath}.lock`);
145
+ try {
146
+ const current = loadState(statePath);
147
+ if (current.status !== "stalled") {
148
+ throw new Error(`cannot renew ${current.conversation_id}; conversation is ${current.status}, not stalled`);
149
+ }
150
+ if (isVerifiedDeadTerminalAgentProcess(current)) {
151
+ throw new Error(`cannot renew ${current.conversation_id}; its bound terminal agent ` +
152
+ "process is verified dead");
153
+ }
154
+ if (terminalBridgeSubmission(current)?.status === "uncertain") {
155
+ throw new Error(`cannot renew ${current.conversation_id}; its terminal submission is uncertain`);
156
+ }
157
+ const currentTakeover = isRecord(current.native_session_takeover)
158
+ ? current.native_session_takeover
159
+ : undefined;
160
+ const currentControl = terminalControlFromTakeover(currentTakeover);
161
+ if (!currentControl || currentTakeover?.terminal_bridge !== true) {
162
+ throw new Error(`cannot renew ${current.conversation_id}; conversation is not a terminal bridge task`);
163
+ }
164
+ if (current.conversation_id !== conversation.conversation_id ||
165
+ !terminalControlsShareIncarnation(currentControl, terminalControl) ||
166
+ stringValue(currentTakeover.terminal_bridge_message_id) !== expectedMessageId ||
167
+ stringValue(currentTakeover.terminal_bridge_started_at) !== expectedStartedAt) {
168
+ throw new Error("conversation changed while waiting to renew; refresh status and retry");
169
+ }
170
+ assertTurnBindingCurrent(current, "renew");
171
+ renewedTerminalControl = currentControl;
172
+ inactivityTimeoutMinutes = positiveMinutes(options.minutes ??
173
+ options.agentTimeoutMinutes ??
174
+ currentTakeover.terminal_bridge_inactivity_timeout_minutes ??
175
+ terminalMaintenanceRuntime().dependencies.runtime
176
+ .defaultAgentTimeoutMinutes, "--minutes");
177
+ hardTimeoutMinutes = positiveMinutes(currentTakeover.terminal_bridge_hard_timeout_minutes ??
178
+ terminalMaintenanceRuntime().dependencies.runtime
179
+ .defaultAgentHardTimeoutMinutes, "--agent-hard-timeout-minutes");
180
+ const startedAt = stringValue(currentTakeover.terminal_bridge_started_at);
181
+ const startedAtMs = startedAt ? Date.parse(startedAt) : NaN;
182
+ if (Number.isFinite(startedAtMs) &&
183
+ cliNowMs() - startedAtMs >= hardTimeoutMinutes * 60 * 1000) {
184
+ throw new Error(`cannot renew ${current.conversation_id}; terminal bridge hard lifetime of ${hardTimeoutMinutes} minutes has elapsed`);
185
+ }
186
+ const now = cliNow().toISOString();
187
+ const hardDeadline = deadlineAt(startedAt ?? now, hardTimeoutMinutes) ??
188
+ new Date(cliNowMs() + hardTimeoutMinutes * 60 * 1000).toISOString();
189
+ const inactivityDeadline = deadlineAt(now, inactivityTimeoutMinutes) ??
190
+ new Date(cliNowMs() + inactivityTimeoutMinutes * 60 * 1000).toISOString();
191
+ renewed = {
192
+ ...supersedeCallbackNotificationDelivery(current, {
193
+ at: now,
194
+ reason: "superseded_by_terminal_monitor_renewal"
195
+ }),
196
+ status: "waiting_for_agent",
197
+ native_session_takeover: {
198
+ ...currentTakeover,
199
+ terminal_bridge_monitor_lock_version: terminalMaintenanceRuntime().dependencies.runtime.monitorLockVersion,
200
+ terminal_bridge_monitor_started_at: now,
201
+ terminal_bridge_last_activity_at: now,
202
+ terminal_bridge_inactivity_timeout_minutes: inactivityTimeoutMinutes,
203
+ terminal_bridge_hard_timeout_minutes: hardTimeoutMinutes,
204
+ terminal_bridge_inactivity_deadline_at: inactivityDeadline,
205
+ terminal_bridge_hard_deadline_at: hardDeadline,
206
+ terminal_bridge_renewed_at: now
207
+ },
208
+ updated_at: now
209
+ };
210
+ Reflect.deleteProperty(renewed, "stalled_at");
211
+ Reflect.deleteProperty(renewed, "stalled_reason");
212
+ Reflect.deleteProperty(renewed, "stalled_notification_sent_at");
213
+ Reflect.deleteProperty(renewed, "stalled_notification_message_id");
214
+ saveState(statePath, renewed);
215
+ appendEvent(logPath, {
216
+ ts: now,
217
+ conversation_id: current.conversation_id,
218
+ event: "terminal_bridge_renewed",
219
+ previous_status: current.status,
220
+ terminal_control: currentControl,
221
+ agent_timeout_minutes: inactivityTimeoutMinutes,
222
+ agent_hard_timeout_minutes: hardTimeoutMinutes,
223
+ last_activity_at: now
224
+ });
225
+ runtimeLog("info", "terminal_bridge_renewed", {
226
+ conversation_id: current.conversation_id,
227
+ terminal_target: currentControl.target,
228
+ agent_timeout_minutes: inactivityTimeoutMinutes,
229
+ agent_hard_timeout_minutes: hardTimeoutMinutes
230
+ });
165
231
  }
166
- assertTurnBindingCurrent(current, "renew");
167
- renewedTerminalControl = currentControl;
168
- inactivityTimeoutMinutes = positiveMinutes(options.minutes ??
169
- options.agentTimeoutMinutes ??
170
- currentTakeover.terminal_bridge_inactivity_timeout_minutes ??
171
- terminalMaintenanceRuntime().dependencies.runtime
172
- .defaultAgentTimeoutMinutes, "--minutes");
173
- hardTimeoutMinutes = positiveMinutes(currentTakeover.terminal_bridge_hard_timeout_minutes ??
174
- terminalMaintenanceRuntime().dependencies.runtime
175
- .defaultAgentHardTimeoutMinutes, "--agent-hard-timeout-minutes");
176
- const startedAt = stringValue(currentTakeover.terminal_bridge_started_at);
177
- const startedAtMs = startedAt ? Date.parse(startedAt) : NaN;
178
- if (Number.isFinite(startedAtMs) &&
179
- cliNowMs() - startedAtMs >= hardTimeoutMinutes * 60 * 1000) {
180
- throw new Error(`cannot renew ${current.conversation_id}; terminal bridge hard lifetime of ${hardTimeoutMinutes} minutes has elapsed`);
232
+ finally {
233
+ releaseStateLock();
181
234
  }
182
- const now = cliNow().toISOString();
183
- const hardDeadline = deadlineAt(startedAt ?? now, hardTimeoutMinutes) ??
184
- new Date(cliNowMs() + hardTimeoutMinutes * 60 * 1000).toISOString();
185
- const inactivityDeadline = deadlineAt(now, inactivityTimeoutMinutes) ??
186
- new Date(cliNowMs() + inactivityTimeoutMinutes * 60 * 1000).toISOString();
187
- renewed = {
188
- ...current,
189
- status: "waiting_for_agent",
190
- native_session_takeover: {
191
- ...currentTakeover,
192
- terminal_bridge_monitor_lock_version: terminalMaintenanceRuntime().dependencies.runtime.monitorLockVersion,
193
- terminal_bridge_monitor_started_at: now,
194
- terminal_bridge_last_activity_at: now,
195
- terminal_bridge_inactivity_timeout_minutes: inactivityTimeoutMinutes,
196
- terminal_bridge_hard_timeout_minutes: hardTimeoutMinutes,
197
- terminal_bridge_inactivity_deadline_at: inactivityDeadline,
198
- terminal_bridge_hard_deadline_at: hardDeadline,
199
- terminal_bridge_renewed_at: now
200
- },
201
- updated_at: now
202
- };
203
- Reflect.deleteProperty(renewed, "stalled_at");
204
- Reflect.deleteProperty(renewed, "stalled_reason");
205
- Reflect.deleteProperty(renewed, "stalled_notification_sent_at");
206
- Reflect.deleteProperty(renewed, "stalled_notification_message_id");
207
- saveState(statePath, renewed);
208
- appendEvent(logPath, {
209
- ts: now,
210
- conversation_id: current.conversation_id,
211
- event: "terminal_bridge_renewed",
212
- previous_status: current.status,
213
- terminal_control: currentControl,
214
- agent_timeout_minutes: inactivityTimeoutMinutes,
215
- agent_hard_timeout_minutes: hardTimeoutMinutes,
216
- last_activity_at: now
217
- });
218
- runtimeLog("info", "terminal_bridge_renewed", {
219
- conversation_id: current.conversation_id,
220
- terminal_target: currentControl.target,
221
- agent_timeout_minutes: inactivityTimeoutMinutes,
222
- agent_hard_timeout_minutes: hardTimeoutMinutes
223
- });
224
- }
225
- finally {
226
- releaseStateLock();
227
- }
235
+ });
228
236
  const monitor = startTerminalBridgeMonitorForConversation({
229
237
  conversation: renewed,
230
238
  statePath,
@@ -336,90 +344,89 @@ async function runTerminalConversationCancel({ options, conversationId, agent, t
336
344
  }
337
345
  async function runTerminalControlCancel({ options, statePath, logPath, agent, terminalControl }) {
338
346
  const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDirFromOptions(options), terminalControl, { timeoutMs: 30000 });
339
- let releaseStateLock;
340
347
  try {
341
- releaseStateLock = acquireFileLock(`${statePath}.lock`);
342
348
  const writerStoreDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
343
349
  return await withStoreWriterLease(writerStoreDir, async () => {
344
- const currentConversation = loadState(statePath);
345
- if (!["waiting_for_agent", "waiting_for_openclaw"].includes(currentConversation.status)) {
346
- throw new Error(`cannot cancel ${currentConversation.conversation_id}; conversation is ${currentConversation.status}`);
347
- }
348
- const currentTakeover = isRecord(currentConversation.native_session_takeover)
349
- ? currentConversation.native_session_takeover
350
- : undefined;
351
- const currentControl = terminalControlFromTakeover(currentTakeover);
352
- if (!currentControl ||
353
- !terminalControlsShareIncarnation(currentControl, terminalControl)) {
354
- throw new Error("terminal control changed while waiting to cancel; refresh status and retry");
355
- }
356
- assertManagedTerminalDispatchOwner({
357
- storeDir: writerStoreDir,
358
- conversation: currentConversation,
359
- terminalControl: currentControl,
360
- action: "cancel"
361
- });
362
- const cancellation = await createTerminalAgentBridge(options).cancel(agent, currentControl, {
363
- runtime: terminalRuntimeIdentityForConversation(currentConversation, currentControl),
364
- scrollbackLines: Number(options.scrollbackLines ?? 120)
365
- });
366
- if (!cancellation.cancelRequested) {
367
- printJson({
350
+ const releaseStateLock = acquireFileLock(`${statePath}.lock`);
351
+ try {
352
+ const currentConversation = loadState(statePath);
353
+ if (!["waiting_for_agent", "waiting_for_openclaw"].includes(currentConversation.status)) {
354
+ throw new Error(`cannot cancel ${currentConversation.conversation_id}; conversation is ${currentConversation.status}`);
355
+ }
356
+ const currentTakeover = isRecord(currentConversation.native_session_takeover)
357
+ ? currentConversation.native_session_takeover
358
+ : undefined;
359
+ const currentControl = terminalControlFromTakeover(currentTakeover);
360
+ if (!currentControl ||
361
+ !terminalControlsShareIncarnation(currentControl, terminalControl)) {
362
+ throw new Error("terminal control changed while waiting to cancel; refresh status and retry");
363
+ }
364
+ assertManagedTerminalDispatchOwner({
365
+ storeDir: writerStoreDir,
368
366
  conversation: currentConversation,
369
- cancel_requested: false,
370
- reason: cancellation.reason,
367
+ terminalControl: currentControl,
368
+ action: "cancel"
369
+ });
370
+ const cancellation = await createTerminalAgentBridge(options).cancel(agent, currentControl, {
371
+ runtime: terminalRuntimeIdentityForConversation(currentConversation, currentControl),
372
+ scrollbackLines: Number(options.scrollbackLines ?? 120)
373
+ });
374
+ if (!cancellation.cancelRequested) {
375
+ printJson({
376
+ conversation: currentConversation,
377
+ cancel_requested: false,
378
+ reason: cancellation.reason,
379
+ terminal_control: currentControl,
380
+ budget: budgetAction(currentConversation)
381
+ });
382
+ return;
383
+ }
384
+ const now = cliNow().toISOString();
385
+ appendEvent(logPath, {
386
+ ts: now,
387
+ conversation_id: currentConversation.conversation_id,
388
+ event: "terminal_cancel_requested",
371
389
  terminal_control: currentControl,
372
- budget: budgetAction(currentConversation)
390
+ key: cancellation.key,
391
+ keys: cancellation.keys,
392
+ denied_approval: cancellation.deniedApproval,
393
+ request_id: cancellation.requestId
394
+ });
395
+ runtimeLog("info", "terminal_cancel_requested", {
396
+ conversation_id: currentConversation.conversation_id,
397
+ agent,
398
+ terminal_target: currentControl.target,
399
+ key: cancellation.key,
400
+ keys: cancellation.keys,
401
+ denied_approval: cancellation.deniedApproval,
402
+ request_id: cancellation.requestId
403
+ });
404
+ const nextConversation = {
405
+ ...currentConversation,
406
+ status: "cancelled",
407
+ cancelled_at: now,
408
+ terminal_cancel_requested_at: now,
409
+ updated_at: now
410
+ };
411
+ saveState(statePath, nextConversation);
412
+ printJson({
413
+ conversation: nextConversation,
414
+ cancel_requested: true,
415
+ terminal_control: currentControl,
416
+ key: cancellation.key,
417
+ keys: cancellation.keys,
418
+ denied_approval: cancellation.deniedApproval,
419
+ request_id: cancellation.requestId,
420
+ budget: budgetAction(nextConversation)
373
421
  });
374
- return;
375
422
  }
376
- const now = cliNow().toISOString();
377
- appendEvent(logPath, {
378
- ts: now,
379
- conversation_id: currentConversation.conversation_id,
380
- event: "terminal_cancel_requested",
381
- terminal_control: currentControl,
382
- key: cancellation.key,
383
- keys: cancellation.keys,
384
- denied_approval: cancellation.deniedApproval,
385
- request_id: cancellation.requestId
386
- });
387
- runtimeLog("info", "terminal_cancel_requested", {
388
- conversation_id: currentConversation.conversation_id,
389
- agent,
390
- terminal_target: currentControl.target,
391
- key: cancellation.key,
392
- keys: cancellation.keys,
393
- denied_approval: cancellation.deniedApproval,
394
- request_id: cancellation.requestId
395
- });
396
- const nextConversation = {
397
- ...currentConversation,
398
- status: "cancelled",
399
- cancelled_at: now,
400
- terminal_cancel_requested_at: now,
401
- updated_at: now
402
- };
403
- saveState(statePath, nextConversation);
404
- printJson({
405
- conversation: nextConversation,
406
- cancel_requested: true,
407
- terminal_control: currentControl,
408
- key: cancellation.key,
409
- keys: cancellation.keys,
410
- denied_approval: cancellation.deniedApproval,
411
- request_id: cancellation.requestId,
412
- budget: budgetAction(nextConversation)
413
- });
423
+ finally {
424
+ releaseStateLock();
425
+ }
414
426
  });
415
427
  }
416
428
  finally {
417
- try {
418
- releaseStateLock?.();
419
- }
420
- finally {
421
- releaseTerminalLock();
422
- }
429
+ releaseTerminalLock();
423
430
  }
424
431
  }
425
432
  async function runObservedHandoffClose({ options, statePath, logPath, initialConversation }) {
@@ -538,7 +545,10 @@ async function runObservedHandoffClose({ options, statePath, logPath, initialCon
538
545
  }
539
546
  const now = cliNow().toISOString();
540
547
  const closed = {
541
- ...conversation,
548
+ ...supersedeCallbackNotificationDelivery(conversation, {
549
+ at: now,
550
+ reason: "superseded_by_human_context_switch"
551
+ }),
542
552
  status: "closed",
543
553
  closed_at: now,
544
554
  close_reason: "superseded_by_human_context_switch",
@@ -595,187 +605,99 @@ async function runClose(options) {
595
605
  return;
596
606
  }
597
607
  const loaded = loadConversationFromOptions(options);
598
- if (stringValue(options.expectedHandoffToken)) {
599
- await runObservedHandoffClose({
600
- options,
601
- statePath: loaded.statePath,
602
- logPath: loaded.logPath,
603
- initialConversation: loaded.conversation
604
- });
605
- return;
606
- }
607
- if (stringValue(options.reason) === "superseded_by_human_context_switch") {
608
- throw new Error("reason superseded_by_human_context_switch requires the fresh " +
609
- "expected_handoff_token advertised by AKK list");
610
- }
611
608
  const { statePath, logPath } = loaded;
612
609
  const closeStoreDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
613
- const nativeTakeover = isRecord(loaded.conversation.native_session_takeover)
614
- ? loaded.conversation.native_session_takeover
615
- : undefined;
616
- const terminalControl = terminalControlFromTakeover(nativeTakeover);
617
- const releaseTerminalLock = terminalControl
618
- ? acquireTerminalBridgeSendLock(closeStoreDir, terminalControl, { timeoutMs: 30000 })
619
- : () => { };
620
610
  const closeWithFreshState = async () => {
621
611
  const releaseStateLock = acquireFileLock(`${statePath}.lock`);
622
612
  try {
623
- const conversation = loadState(statePath);
624
- let verifiedDeadProcess;
625
- assertConversationHasNoNonterminalDeferredForegroundTransfer({
626
- storeDir: closeStoreDir,
627
- conversation,
628
- action: "close"
629
- });
613
+ const conversation = {
614
+ ...loadState(statePath),
615
+ store_dir: path.resolve(closeStoreDir),
616
+ conversation_dir: path.resolve(path.dirname(statePath)),
617
+ state_path: path.resolve(statePath),
618
+ event_log_path: path.resolve(logPath)
619
+ };
630
620
  const currentTakeover = isRecord(conversation.native_session_takeover)
631
621
  ? conversation.native_session_takeover
632
622
  : undefined;
633
623
  const currentTerminalControl = terminalControlFromTakeover(currentTakeover);
634
- if (terminalControl &&
635
- !terminalControlsShareIncarnation(currentTerminalControl, terminalControl)) {
636
- throw new Error("terminal control changed after the close action was listed; refresh AKK list");
637
- }
638
- if (conversation.status === "closed" && currentTerminalControl) {
639
- const recoveredAuthority = exactVerifiedDeadTerminalAgentProcessAuthority({
640
- conversation,
641
- storeDir: closeStoreDir,
642
- terminalControl: currentTerminalControl,
643
- logPath
644
- });
645
- const recoveredDecision = decideVerifiedDeadAgentProcess({
646
- persistedAuthority: recoveredAuthority
647
- });
648
- if (recoveredDecision.status === "invalid") {
649
- throw new Error(`cannot finish close recovery for ${conversation.conversation_id}; ` +
650
- recoveredDecision.reason);
651
- }
652
- if (recoveredDecision.status === "verified_dead") {
653
- const audit = ensureVerifiedDeadTerminalAgentProcessEvent({
654
- logPath,
655
- proof: recoveredDecision.proof,
656
- action: "managed_close"
657
- });
658
- const expectedMessageId = required(stringValue(currentTakeover?.terminal_bridge_message_id), "verified-dead close recovery has no terminal message id");
659
- const dispatchLedgerResolved = resolveVerifiedDeadTerminalBridgeDispatchLedger({
660
- terminalControl: currentTerminalControl,
661
- conversation,
662
- storeDir: closeStoreDir,
663
- statePath,
664
- logPath,
665
- expectedMessageId,
666
- reason: "conversation explicitly closed by request"
667
- });
668
- ensureVerifiedDeadConversationClosedEvent({
669
- logPath,
670
- conversation,
671
- evidenceId: audit.evidenceId
672
- });
673
- printJson({
674
- conversation,
675
- closed: true,
676
- recovered: true,
677
- terminal_dispatch_resolved: dispatchLedgerResolved
678
- });
679
- return;
680
- }
681
- }
682
- if (terminalControl && currentTerminalControl) {
683
- verifiedDeadProcess =
684
- await assertGenericCloseDoesNotBypassObservedHandoff({
685
- options,
686
- storeDir: closeStoreDir,
687
- conversation,
688
- terminalControl: currentTerminalControl
689
- });
690
- }
691
624
  const now = cliNow().toISOString();
692
- const closeReason = stringValue(options.reason) ?? "closed by request";
693
- const verifiedDeadAudit = verifiedDeadProcess
694
- ? ensureVerifiedDeadTerminalAgentProcessEvent({
695
- logPath,
696
- proof: verifiedDeadProcess,
697
- action: "managed_close"
698
- })
699
- : undefined;
700
- const verifiedDeadMessageId = verifiedDeadAudit
701
- ? required(stringValue(currentTakeover?.terminal_bridge_message_id), "verified-dead close has no terminal message id")
702
- : undefined;
703
- if (verifiedDeadAudit && currentTerminalControl) {
704
- assertVerifiedDeadTerminalBridgeDispatchAuthority({
705
- terminalControl: currentTerminalControl,
706
- conversation,
707
- storeDir: closeStoreDir,
708
- statePath,
709
- logPath,
710
- expectedMessageId: verifiedDeadMessageId
711
- });
712
- }
625
+ const closedAt = stringValue(conversation.closed_at) ?? now;
626
+ const closeReason = stringValue(options.reason) ??
627
+ stringValue(conversation.close_reason) ?? "closed by request";
713
628
  const closed = {
714
- ...conversation,
629
+ ...supersedeUnacceptedCallbackDeliveries(conversation, {
630
+ at: now,
631
+ reason: "superseded_by_conversation_close"
632
+ }),
715
633
  status: "closed",
716
- closed_at: now,
634
+ closed_at: closedAt,
717
635
  close_reason: closeReason,
718
- ...(verifiedDeadAudit
719
- ? {
720
- terminal_agent_process_disposition: {
721
- status: "verified_dead",
722
- proof: verifiedDeadAudit.proof,
723
- evidence_id: verifiedDeadAudit.evidenceId,
724
- recorded_at: verifiedDeadAudit.recordedAt
725
- }
726
- }
727
- : {}),
636
+ disposition: "user_abandoned_management",
637
+ callback_expected: false,
728
638
  updated_at: now
729
639
  };
730
- saveState(statePath, closed);
731
- if (verifiedDeadAudit &&
732
- cliEnv().AKK_TEST_EXIT_AFTER_VERIFIED_DEAD_CLOSE_STATE === "1") {
733
- cliExit(86);
640
+ delete closed.idle_since;
641
+ saveExplicitUserCloseState(statePath, closed);
642
+ let transferCleanup;
643
+ try {
644
+ transferCleanup = cleanupDeferredForegroundUserClose({
645
+ storeDir: closeStoreDir,
646
+ conversation: closed,
647
+ at: now
648
+ });
649
+ }
650
+ catch (error) {
651
+ transferCleanup = {
652
+ transfer_finalized: false,
653
+ detached_session_ids: [],
654
+ warnings: [
655
+ `deferred_transfer_cleanup: ${error instanceof Error ? error.message : String(error)}`
656
+ ]
657
+ };
734
658
  }
735
659
  let dispatchLedgerResolved = false;
736
660
  let dispatchLedgerWarning;
737
- if (currentTerminalControl) {
661
+ const ledgerTerminalControl = currentTerminalControl;
662
+ if (ledgerTerminalControl) {
738
663
  try {
739
- dispatchLedgerResolved = verifiedDeadAudit
740
- ? resolveVerifiedDeadTerminalBridgeDispatchLedger({
741
- terminalControl: currentTerminalControl,
742
- conversation: closed,
743
- storeDir: closeStoreDir,
744
- statePath,
745
- logPath,
746
- expectedMessageId: verifiedDeadMessageId,
747
- reason: "conversation explicitly closed by request"
748
- })
749
- : resolveTerminalBridgeDispatchLedger(currentTerminalControl, {
750
- conversation: closed,
751
- expectedMessageId: stringValue(currentTakeover?.terminal_bridge_message_id),
752
- reason: "conversation explicitly closed by request"
753
- });
664
+ dispatchLedgerResolved = resolveTerminalBridgeDispatchLedger(ledgerTerminalControl, {
665
+ conversation: closed,
666
+ expectedMessageId: stringValue(currentTakeover?.terminal_bridge_message_id),
667
+ reason: "conversation explicitly closed by request"
668
+ });
754
669
  }
755
670
  catch (error) {
756
671
  dispatchLedgerWarning =
757
672
  error instanceof Error ? error.message : String(error);
758
673
  runtimeLog("error", "terminal_dispatch_ledger_resolve_failed", {
759
674
  conversation_id: closed.conversation_id,
760
- terminal_target: currentTerminalControl.target,
675
+ terminal_target: ledgerTerminalControl.target,
761
676
  error: dispatchLedgerWarning
762
677
  });
763
678
  }
764
679
  }
765
- if (verifiedDeadAudit) {
766
- ensureVerifiedDeadConversationClosedEvent({
767
- logPath,
768
- conversation: closed,
769
- evidenceId: verifiedDeadAudit.evidenceId
770
- });
771
- }
772
- else {
773
- appendEvent(logPath, {
774
- ts: now,
680
+ let closeEventWarning;
681
+ try {
682
+ appendExplicitUserCloseEvent(logPath, {
683
+ ts: closedAt,
775
684
  conversation_id: conversation.conversation_id,
776
685
  event: "conversation_closed",
777
686
  status: "closed",
778
- reason: closed.close_reason
687
+ reason: closed.close_reason,
688
+ disposition: "user_abandoned_management",
689
+ terminal_input_sent: false,
690
+ coding_agent_stopped: false,
691
+ tmux_pane_closed: false
692
+ });
693
+ }
694
+ catch (error) {
695
+ closeEventWarning = error instanceof Error
696
+ ? error.message
697
+ : String(error);
698
+ runtimeLog("warn", "conversation_close_event_append_failed", {
699
+ conversation_id: closed.conversation_id,
700
+ error: closeEventWarning
779
701
  });
780
702
  }
781
703
  runtimeLog("info", "conversation_closed", {
@@ -788,11 +710,26 @@ async function runClose(options) {
788
710
  printJson({
789
711
  conversation: closed,
790
712
  closed: true,
713
+ management_released: true,
714
+ terminal_input_sent: false,
715
+ coding_agent_stopped: false,
716
+ tmux_pane_closed: false,
717
+ next_action: "refresh list; if the coding agent is still working, use the fresh Watch action",
718
+ deferred_transfer: transferCleanup,
791
719
  terminal_dispatch_resolved: dispatchLedgerResolved,
792
- ...(dispatchLedgerWarning
720
+ ...(dispatchLedgerWarning ||
721
+ closeEventWarning ||
722
+ transferCleanup.warnings.length > 0
793
723
  ? {
794
- terminal_dispatch_warning: textSummary(dispatchLedgerWarning),
795
- do_not_retry: true
724
+ warnings: [
725
+ ...transferCleanup.warnings,
726
+ ...(dispatchLedgerWarning
727
+ ? [`terminal_dispatch: ${dispatchLedgerWarning}`]
728
+ : []),
729
+ ...(closeEventWarning
730
+ ? [`close_event: ${closeEventWarning}`]
731
+ : [])
732
+ ].map((warning) => textSummary(warning))
796
733
  }
797
734
  : {})
798
735
  });
@@ -801,17 +738,7 @@ async function runClose(options) {
801
738
  releaseStateLock();
802
739
  }
803
740
  };
804
- try {
805
- if (terminalControl) {
806
- await withStoreWriterLeaseAsync(closeStoreDir, closeWithFreshState);
807
- }
808
- else {
809
- await closeWithFreshState();
810
- }
811
- }
812
- finally {
813
- releaseTerminalLock();
814
- }
741
+ await withStoreWriterLeaseAsync(closeStoreDir, closeWithFreshState);
815
742
  }
816
743
  async function assertGenericCloseDoesNotBypassObservedHandoff({ options, storeDir, conversation, terminalControl }) {
817
744
  if (!isSessionSendBlockingStatus(conversation.status)) {