@scotthuang/agent-knock-knock 0.12.36 → 0.13.0

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 (146) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +3 -0
  3. package/dist/src/agent-session-provider.d.ts +5 -0
  4. package/dist/src/agent-session-provider.js +10 -1
  5. package/dist/src/agent-session-provider.js.map +1 -1
  6. package/dist/src/callback-outbox-service.d.ts +2 -0
  7. package/dist/src/callback-outbox-service.js +1 -1
  8. package/dist/src/callback-outbox-service.js.map +1 -1
  9. package/dist/src/callback-transport.d.ts +7 -0
  10. package/dist/src/callback-transport.js +1 -1
  11. package/dist/src/callback-transport.js.map +1 -1
  12. package/dist/src/cli-core.js +35 -32
  13. package/dist/src/cli-core.js.map +1 -1
  14. package/dist/src/codex-store-adapter.d.ts +1 -1
  15. package/dist/src/codex-store-adapter.js +35 -8
  16. package/dist/src/codex-store-adapter.js.map +1 -1
  17. package/dist/src/deferred-foreground-authority-cli-adapter.d.ts +20 -0
  18. package/dist/src/deferred-foreground-authority-cli-adapter.js +216 -8
  19. package/dist/src/deferred-foreground-authority-cli-adapter.js.map +1 -1
  20. package/dist/src/host-adapter.d.ts +1 -1
  21. package/dist/src/host-adapter.js +1 -1
  22. package/dist/src/host-bridge-tools.js +2 -2
  23. package/dist/src/managed-turn-recovery-service.d.ts +8 -1
  24. package/dist/src/managed-turn-recovery-service.js +55 -13
  25. package/dist/src/managed-turn-recovery-service.js.map +1 -1
  26. package/dist/src/mutation-transaction.d.ts +1 -0
  27. package/dist/src/mutation-transaction.js +19 -15
  28. package/dist/src/mutation-transaction.js.map +1 -1
  29. package/dist/src/native-thread-lifecycle-cli-adapter.d.ts +39 -3
  30. package/dist/src/native-thread-lifecycle-cli-adapter.js +307 -7
  31. package/dist/src/native-thread-lifecycle-cli-adapter.js.map +1 -1
  32. package/dist/src/openclaw-plugin-callback-adapter.js +17 -7
  33. package/dist/src/openclaw-plugin-callback-adapter.js.map +1 -1
  34. package/dist/src/openclaw-plugin-command-adapter.js +299 -79
  35. package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
  36. package/dist/src/openclaw-plugin-schemas.d.ts +57 -0
  37. package/dist/src/openclaw-plugin-schemas.js +59 -1
  38. package/dist/src/openclaw-plugin-schemas.js.map +1 -1
  39. package/dist/src/openclaw-private-authority-offers.d.ts +17 -0
  40. package/dist/src/openclaw-private-authority-offers.js +75 -3
  41. package/dist/src/openclaw-private-authority-offers.js.map +1 -1
  42. package/dist/src/store.d.ts +1 -1
  43. package/dist/src/store.js +6 -4
  44. package/dist/src/store.js.map +1 -1
  45. package/dist/src/terminal-acceptance-application-service.js +2 -2
  46. package/dist/src/terminal-acceptance-application-service.js.map +1 -1
  47. package/dist/src/terminal-acceptance-cli-adapter.d.ts +11 -1
  48. package/dist/src/terminal-acceptance-cli-adapter.js +290 -16
  49. package/dist/src/terminal-acceptance-cli-adapter.js.map +1 -1
  50. package/dist/src/terminal-action-projection.d.ts +1 -1
  51. package/dist/src/terminal-action-projection.js +6 -0
  52. package/dist/src/terminal-action-projection.js.map +1 -1
  53. package/dist/src/terminal-agent-adapter.d.ts +9 -0
  54. package/dist/src/terminal-agent-adapter.js.map +1 -1
  55. package/dist/src/terminal-agent-bridge.d.ts +52 -8
  56. package/dist/src/terminal-agent-bridge.js +133 -104
  57. package/dist/src/terminal-agent-bridge.js.map +1 -1
  58. package/dist/src/terminal-authority-policy.js +3 -0
  59. package/dist/src/terminal-authority-policy.js.map +1 -1
  60. package/dist/src/terminal-binding-authority.d.ts +2 -0
  61. package/dist/src/terminal-binding-authority.js +8 -0
  62. package/dist/src/terminal-binding-authority.js.map +1 -1
  63. package/dist/src/terminal-command-cli-adapter.d.ts +17 -1
  64. package/dist/src/terminal-command-cli-adapter.js +784 -286
  65. package/dist/src/terminal-command-cli-adapter.js.map +1 -1
  66. package/dist/src/terminal-delegate-cli-adapter.js +21 -7
  67. package/dist/src/terminal-delegate-cli-adapter.js.map +1 -1
  68. package/dist/src/terminal-dispatch-application.d.ts +3 -1
  69. package/dist/src/terminal-dispatch-application.js +13 -5
  70. package/dist/src/terminal-dispatch-application.js.map +1 -1
  71. package/dist/src/terminal-dispatch-composition.d.ts +43 -0
  72. package/dist/src/terminal-dispatch-execution.d.ts +2 -2
  73. package/dist/src/terminal-dispatch-execution.js +53 -3
  74. package/dist/src/terminal-dispatch-execution.js.map +1 -1
  75. package/dist/src/terminal-dispatch-policy.d.ts +20 -0
  76. package/dist/src/terminal-dispatch-policy.js +33 -1
  77. package/dist/src/terminal-dispatch-policy.js.map +1 -1
  78. package/dist/src/terminal-dispatch-presenter.d.ts +45 -0
  79. package/dist/src/terminal-dispatch-presenter.js +220 -18
  80. package/dist/src/terminal-dispatch-presenter.js.map +1 -1
  81. package/dist/src/terminal-dispatch-receipt.d.ts +5 -1
  82. package/dist/src/terminal-dispatch-receipt.js +26 -3
  83. package/dist/src/terminal-dispatch-receipt.js.map +1 -1
  84. package/dist/src/terminal-identity-authority-cli-adapter.js +103 -4
  85. package/dist/src/terminal-identity-authority-cli-adapter.js.map +1 -1
  86. package/dist/src/terminal-interaction-authority.d.ts +48 -1
  87. package/dist/src/terminal-interaction-authority.js +30 -0
  88. package/dist/src/terminal-interaction-authority.js.map +1 -1
  89. package/dist/src/terminal-interaction-cli-adapter.js +19 -9
  90. package/dist/src/terminal-interaction-cli-adapter.js.map +1 -1
  91. package/dist/src/terminal-interaction-core.d.ts +104 -0
  92. package/dist/src/terminal-interaction-core.js +407 -0
  93. package/dist/src/terminal-interaction-core.js.map +1 -0
  94. package/dist/src/terminal-interaction-protocol.d.ts +95 -0
  95. package/dist/src/terminal-interaction-protocol.js +201 -1
  96. package/dist/src/terminal-interaction-protocol.js.map +1 -1
  97. package/dist/src/terminal-list-cli-adapter.d.ts +5 -0
  98. package/dist/src/terminal-list-cli-adapter.js +190 -40
  99. package/dist/src/terminal-list-cli-adapter.js.map +1 -1
  100. package/dist/src/terminal-list-renderer.js +60 -7
  101. package/dist/src/terminal-list-renderer.js.map +1 -1
  102. package/dist/src/terminal-maintenance-cli-adapter.js +14 -3
  103. package/dist/src/terminal-maintenance-cli-adapter.js.map +1 -1
  104. package/dist/src/terminal-monitor-application-service.d.ts +1 -0
  105. package/dist/src/terminal-monitor-application-service.js +72 -10
  106. package/dist/src/terminal-monitor-application-service.js.map +1 -1
  107. package/dist/src/terminal-monitor-cli-adapter.d.ts +3 -0
  108. package/dist/src/terminal-monitor-cli-adapter.js +24 -8
  109. package/dist/src/terminal-monitor-cli-adapter.js.map +1 -1
  110. package/dist/src/terminal-monitor-state-cli-adapter.js +15 -2
  111. package/dist/src/terminal-monitor-state-cli-adapter.js.map +1 -1
  112. package/dist/src/terminal-mutation-cli-runtime.d.ts +11 -1
  113. package/dist/src/terminal-mutation-cli-runtime.js +9 -4
  114. package/dist/src/terminal-mutation-cli-runtime.js.map +1 -1
  115. package/dist/src/terminal-questionnaire-adapter.d.ts +1 -1
  116. package/dist/src/terminal-questionnaire-adapter.js +106 -39
  117. package/dist/src/terminal-questionnaire-adapter.js.map +1 -1
  118. package/dist/src/terminal-status-cli-adapter.d.ts +1 -7
  119. package/dist/src/terminal-status-cli-adapter.js +29 -3
  120. package/dist/src/terminal-status-cli-adapter.js.map +1 -1
  121. package/dist/src/terminal-status-facts.d.ts +17 -1
  122. package/dist/src/terminal-status-facts.js +59 -0
  123. package/dist/src/terminal-status-facts.js.map +1 -1
  124. package/dist/src/terminal-submission-acceptance.d.ts +37 -2
  125. package/dist/src/terminal-submission-acceptance.js +633 -30
  126. package/dist/src/terminal-submission-acceptance.js.map +1 -1
  127. package/dist/src/terminal-submission-facts.d.ts +5 -0
  128. package/dist/src/terminal-submission-facts.js.map +1 -1
  129. package/dist/src/terminal-watch-callback-cli-adapter.d.ts +2 -1
  130. package/dist/src/terminal-watch-callback-cli-adapter.js +2 -1
  131. package/dist/src/terminal-watch-callback-cli-adapter.js.map +1 -1
  132. package/dist/src/terminal-watch-cli-adapter.d.ts +6 -2
  133. package/dist/src/terminal-watch-cli-adapter.js +1051 -15
  134. package/dist/src/terminal-watch-cli-adapter.js.map +1 -1
  135. package/dist/src/terminal-watch-service.d.ts +15 -1
  136. package/dist/src/terminal-watch-service.js +169 -13
  137. package/dist/src/terminal-watch-service.js.map +1 -1
  138. package/dist/src/terminal-watch-store.d.ts +72 -3
  139. package/dist/src/terminal-watch-store.js +407 -27
  140. package/dist/src/terminal-watch-store.js.map +1 -1
  141. package/docs/host-bridge-profiles.md +2 -2
  142. package/docs/quickstart-herdr.md +5 -3
  143. package/docs/quickstart-tmux.md +7 -5
  144. package/openclaw.plugin.json +8 -0
  145. package/package.json +1 -1
  146. package/templates/openclaw-skills/agent-knock-knock/SKILL.md +15 -11
@@ -6,9 +6,10 @@ import { executorDefinitionForKind } from "./executors.js";
6
6
  import { isTerminalApprovalDecision } from "./terminal-agent-adapter.js";
7
7
  import { isRecord, nonBlankString as stringValue } from "./value-guards.js";
8
8
  import { AKK_CALLBACK_METHOD, akkUsageText, buildAkkCommandCliArgs, formatAkkListCommandResult, formatAkkRespondCommandResult, formatAkkTerminalWatchHint, formatAkkThreadsCommandResult, formatAkkThreadTransitionCommandResult, formatAkkUnwatchCommandResult, formatAkkWatchCommandResult, formatAkkWatchStatusCommandResult, isAkkModelFacingDiagnosticField, isAkkModelFacingPrivateAuthorityField, isAkkNativeSubmissionAccepted, isAkkThreadTransitionSuccess, normalizeAkkModelFacingFieldName, parseAkkCommand, resolvePluginStoreDir, sanitizeAkkModelFacingDiagnosticText, sanitizeAkkModelFacingLegacyAuthorityInstructionText } from "./openclaw-plugin-helpers.js";
9
- import { approveParameters, cancelParameters, closeParameters, listParameters, listResumableThreadsParameters, nativeInspectParameters, newThreadParameters, reconcileBindingParameters, renewParameters, respondInteractionParameters, respondParameters, resumeThreadParameters, retryCallbackParameters, sendParameters, statusParameters, unwatchParameters, watchParameters } from "./openclaw-plugin-schemas.js";
10
- import { consumeOpenClawPrivateAuthorityOffer, openClawApprovalAuthorityOfferKey, openClawInteractionAuthorityOfferKey, rememberOpenClawPrivateAuthorityOffer } from "./openclaw-private-authority-offers.js";
11
- import { TERMINAL_INTERACTION_LIMITS, validateTerminalInteractionProjection, validateTerminalInteractionResponse } from "./terminal-interaction-protocol.js";
9
+ import { approveParameters, cancelParameters, closeParameters, identifyAndSendParameters, identifyForegroundParameters, listParameters, listResumableThreadsParameters, nativeInspectParameters, newThreadParameters, reconcileBindingParameters, renewParameters, respondInteractionParameters, respondParameters, resumeThreadParameters, retryCallbackParameters, sendParameters, statusParameters, unwatchParameters, watchParameters } from "./openclaw-plugin-schemas.js";
10
+ import { consumeOpenClawPrivateAuthorityOffer, invalidateOpenClawInteractionAuthorityOffersForSubject, openClawApprovalAuthorityOfferKey, openClawInteractionAuthorityOfferKey, rememberOpenClawPrivateAuthorityOffer } from "./openclaw-private-authority-offers.js";
11
+ import { TERMINAL_INTERACTION_LIMITS, TERMINAL_INTERACTION_SUBJECT_VERSION, terminalInteractionSubjectId, validateAnyTerminalInteractionProjection, validateAnyTerminalInteractionResponse } from "./terminal-interaction-protocol.js";
12
+ import { normalizedTerminalSendResultContract, terminalSendEnterDispatched } from "./terminal-dispatch-presenter.js";
12
13
  const MAX_DISPLAYED_RESUME_SNAPSHOTS = 512;
13
14
  const OPENCLAW_HANDOFF_AUTHORITY_KIND = "handoff";
14
15
  const OPENCLAW_RECONCILE_BINDING_AUTHORITY_KIND = "reconcile_binding";
@@ -167,6 +168,7 @@ export function registerOpenClawCommands(api, displayedResumeSnapshots) {
167
168
  return args;
168
169
  }
169
170
  });
171
+ registerForegroundIdentificationTools(api);
170
172
  registerCliTool(api, {
171
173
  name: "agent_knock_knock_new_thread",
172
174
  description: "Start and verify a clean native coding-agent thread in the exact terminal_id after explicit user intent. A valid unverified agent version remains callable with a compatibility warning. AKK refreshes lifecycle authority privately. Never send /clear as ordinary task text. This creates a new AKK Session but no Turn.",
@@ -274,7 +276,7 @@ export function registerOpenClawCommands(api, displayedResumeSnapshots) {
274
276
  async execute(_toolCallId, params, signal) {
275
277
  return withHostBridgeInvocationSignal(signal, async () => {
276
278
  try {
277
- const result = await runHostAwareCli(api, buildStatusCliArgs(api, isRecord(params) ? params : {}));
279
+ const result = await runStatusRequest(api, params, toolContext);
278
280
  const rendered = toolResult(result);
279
281
  rememberDisplayedApprovalOffer(api, toolContext?.sessionKey, toolContext?.sessionId, result);
280
282
  rememberDisplayedInteractionOffer(api, toolContext?.sessionKey, toolContext?.sessionId, result);
@@ -289,7 +291,7 @@ export function registerOpenClawCommands(api, displayedResumeSnapshots) {
289
291
  api.registerTool((toolContext) => ({
290
292
  label: "AKK Send",
291
293
  name: "agent_knock_knock_send",
292
- description: "Start a new AKK Turn, use one advertised terminal_user_explicit user-priority send, or explicitly recover one current uncertain submission only through its advertised retry_submission action. Ordinary send requires request and may use session_id or terminal_id exactly as advertised. terminal_user_explicit requires one exact live physical terminal/process and a scanned, non-blocked approval state; parsed working activity and Codex Composer visibility, stability, or exactness do not veto it. Codex physical fallback sends C-u once to replace the current Composer, injects the request, waits through the paste window, and dispatches Enter exactly once without a post-text Composer veto; Claude Code remains exact-empty-only. The managed fast path may require exact empty before input, but after user-explicit Codex text injection it follows the same no-Composer-veto Enter rule. If broken internal AKK state prevents managed delivery before input, AKK delivers once as unmanaged work with no managed callback Turn, then best-effort attaches an exact Terminal Watch callback and releases stale management. Watch attachment failure never changes a successful Send and is reported in the delivery result. Once the mutation sequence begins, an uncertain result must not be automatically retried. Retry submission is the mutually exclusive exact {turn_id} form and cannot change request text or routing. Draft text, composer digests, and opaque freshness authority stay private. A Turn id is never an ordinary-send destination. Managed acceptance is asynchronous: yield and wait for its callback or an explicit status request.",
294
+ description: "Start a new AKK Turn, use one advertised terminal_user_explicit user-priority send, or explicitly recover one current uncertain submission only through its advertised retry_submission action. Ordinary send requires request and may use session_id or terminal_id exactly as advertised. terminal_user_explicit requires one exact live physical terminal/process, a scanned non-blocked approval state, and no active native questionnaire; parsed working activity, Codex rollout ambiguity, AKK management state, and Codex Composer visibility, stability, or exactness do not veto physical delivery. Codex physical fallback sends C-u once to replace the current Composer, injects the request, waits through the paste window, and dispatches Enter exactly once without a post-text Composer veto; Claude Code remains exact-empty-only. A source-less Codex terminal freezes all current rollout roots before input, then promotes a provisional Session/Turn only when exactly one anchored or newly opened rollout durably accepts the exact request hash; zero matches remain pending and ambiguity becomes uncertain without replay. If managed preparation fails before input, AKK still delivers once as unmanaged work, then best-effort attaches an exact Terminal Watch callback. After exact request acceptance and terminal attribution, a supported questionnaire on that Watch may expose owner-bound response authority through Status and its watch_id; terminal-activity observations and manual_required interactions remain notification-only. Read terminal_input_dispatched, agent_acceptance, management_mode, observation_mode, and capabilities independently. Watch attachment failure never changes a successful Send. Once the mutation sequence begins, an uncertain result must not be automatically retried. Retry submission is the mutually exclusive exact {turn_id} form and cannot change request text or routing. Draft text, composer digests, and opaque freshness authority stay private. A Turn id is never an ordinary-send destination. Managed acceptance is asynchronous: yield and wait for its callback or an explicit status request.",
293
295
  parameters: sendParameters,
294
296
  async execute(toolCallId, params, signal) {
295
297
  return withHostBridgeInvocationSignal(signal, async () => {
@@ -335,7 +337,7 @@ export function registerOpenClawCommands(api, displayedResumeSnapshots) {
335
337
  });
336
338
  registerCliTool(api, {
337
339
  name: "agent_knock_knock_respond_interaction",
338
- description: "Answer exactly one current native questionnaire step shown by agent_knock_knock_status in this controller conversation. Supply only the exact turn_id, interaction_id, and one typed semantic answer: single_select uses selected_option_ids with one advertised option_id; free_text uses text; confirm uses confirm. AKK consumes the displayed private offer and revalidates the exact discriminator-specific shape, prompt, expiry, owner, and terminal authority before any input. Raw keys, menu indexes, rendered labels, fingerprints, versions, and terminal commands are never accepted. An uncertain response must never be retried blindly.",
340
+ description: "Answer exactly one current native questionnaire step shown by agent_knock_knock_status in this controller conversation. Supply exactly one authoritative subject target: turn_id for a managed Turn or watch_id for an interactive Terminal Watch, plus interaction_id and one typed semantic answer. single_select uses selected_option_ids with one advertised option_id; free_text uses text; confirm uses confirm. AKK consumes only that subject's displayed private offer and revalidates the exact discriminator-specific shape, prompt, owner, task attribution, and terminal authority before any input. A displayed expiry is a freshness boundary: after it passes, AKK must prove the same exact live questionnaire again instead of rejecting an otherwise live prompt. Raw keys, menu indexes, rendered labels, fingerprints, versions, and terminal commands are never accepted. An uncertain response must never be retried blindly.",
339
341
  parameters: respondInteractionParameters,
340
342
  buildArgs: (params, toolContext) => buildPrivateInteractionResponseArgs(api, params, {
341
343
  sessionKey: requiredOpenClawSessionKey(toolContext?.sessionKey),
@@ -407,6 +409,70 @@ export function registerOpenClawCommands(api, displayedResumeSnapshots) {
407
409
  }
408
410
  });
409
411
  }
412
+ function registerForegroundIdentificationTools(api) {
413
+ registerCliTool(api, {
414
+ name: "agent_knock_knock_identify_foreground",
415
+ description: "Explicitly identify the foreground Codex native thread in one exact idle terminal by issuing the closed /status probe once. The result is a short-lived diagnostic bound to the current pane, process, cwd, and screen generation; it creates no Session or Turn and grants no later response, approval, lifecycle, or send authority. Ordinary list/status never runs this probe.",
416
+ parameters: identifyForegroundParameters,
417
+ normalizeTurnIdentity: false,
418
+ buildArgs: async (params) => {
419
+ const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
420
+ const terminalId = requiredString(params.terminal_id, "terminal_id");
421
+ const action = await privateTerminalActionArguments(api, terminalId, "agent_knock_knock_identify_foreground", { reconcile: false });
422
+ const args = [
423
+ "identify-foreground",
424
+ "--terminal",
425
+ terminalId,
426
+ "--expected-terminal-token",
427
+ requiredString(action.expected_terminal_token, "current internal foreground-identification terminal authority")
428
+ ];
429
+ pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
430
+ pushOptional(args, "--codex-home", stringValue(config.codexHome));
431
+ return args;
432
+ }
433
+ });
434
+ registerCliTool(api, {
435
+ name: "agent_knock_knock_identify_and_send",
436
+ description: "Explicitly identify the foreground Codex native thread with one closed /status probe, then dispatch one task while retaining the same terminal lock. This is an optional managed-attachment enhancement, not a prerequisite for ordinary human Send. The short-lived status observation never becomes durable identity: only the rollout that uniquely accepts the exact task may own the resulting Session/Turn. If the probe or boundary becomes uncertain, AKK does not send or retry the task.",
437
+ parameters: identifyAndSendParameters,
438
+ isErrorResult: isSubmissionError,
439
+ buildArgs: async (params, toolContext, toolCallId) => {
440
+ const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
441
+ const terminalId = requiredString(params.terminal_id, "terminal_id");
442
+ const action = await privateTerminalActionArguments(api, terminalId, "agent_knock_knock_identify_and_send", { reconcile: false });
443
+ const openclawSession = stringValue(toolContext?.sessionKey) ?? "agent:main:main";
444
+ const args = [
445
+ "send",
446
+ "--conversation",
447
+ terminalId,
448
+ "--expected-terminal-token",
449
+ requiredString(action.expected_terminal_token, "current internal identify-and-send terminal authority"),
450
+ "--identify-foreground",
451
+ "--message",
452
+ requiredString(params.request, "request"),
453
+ "--background"
454
+ ];
455
+ pushOptional(args, "--message-id", terminalMessageIdForToolCall({
456
+ toolCallId,
457
+ sessionKey: openclawSession,
458
+ sessionId: toolContext?.sessionId,
459
+ toolName: "agent_knock_knock_identify_and_send"
460
+ }));
461
+ pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
462
+ pushOptional(args, "--idle-timeout-minutes", numberString(params.idleTimeoutMinutes) ??
463
+ numberString(config.idleTimeoutMinutes));
464
+ pushOptional(args, "--agent-timeout-minutes", numberString(params.agentTimeoutMinutes) ??
465
+ numberString(config.agentTimeoutMinutes));
466
+ pushOptional(args, "--agent-hard-timeout-minutes", numberString(params.agentHardTimeoutMinutes) ??
467
+ numberString(config.agentHardTimeoutMinutes));
468
+ pushOptional(args, "--openclaw-session", openclawSession);
469
+ pushOptional(args, "--gateway-method", CALLBACK_METHOD);
470
+ pushOptional(args, "--gateway-session", openclawSession);
471
+ pushOptional(args, "--openclaw-bin", stringValue(config.openclawBin));
472
+ return args;
473
+ }
474
+ });
475
+ }
410
476
  function resumeSelectionScope(sessionKey, sessionId) {
411
477
  const key = requiredString(sessionKey, "Controller session identity is required for snapshot-bound Resume");
412
478
  const incarnation = stringValue(sessionId) ?? null;
@@ -459,15 +525,11 @@ async function handleAkkCommand(api, ctx, displayedResumeSnapshots) {
459
525
  }, {
460
526
  sessionKey: ctx.sessionKey
461
527
  });
462
- if (result.scope === "terminal_user_explicit") {
463
- return {
464
- text: formatSendCommandResult(result),
465
- isError: sendCommandResultIsError(result)
466
- };
467
- }
468
528
  return {
469
- text: formatDelegateCommandResult(result),
470
- isError: result.status !== "async_pending"
529
+ text: result.scope === "terminal_user_explicit"
530
+ ? formatSendCommandResult(result)
531
+ : formatDelegateCommandResult(result),
532
+ isError: sendCommandResultIsError(result)
471
533
  };
472
534
  }
473
535
  const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
@@ -546,13 +608,15 @@ async function handleAkkCommand(api, ctx, displayedResumeSnapshots) {
546
608
  }
547
609
  }
548
610
  function sendCommandResultIsError(result) {
549
- return result.delivered === true &&
550
- result.scope === "terminal_user_explicit"
611
+ return isSuccessfulTerminalDispatch(result)
551
612
  ? false
552
613
  : terminalSubmissionReported(result)
553
614
  ? !isAkkNativeSubmissionAccepted(result)
554
615
  : result.status === "delivered_unfenced";
555
616
  }
617
+ function isSuccessfulTerminalDispatch(result) {
618
+ return isRecord(result) && terminalSendEnterDispatched(result);
619
+ }
556
620
  async function handleAkkLifecycleCommand(api, ctx, parsed, config, displayedResumeSnapshots) {
557
621
  const selectionScope = resumeSelectionScope(ctx.sessionKey, ctx.sessionId);
558
622
  const snapshotCacheKey = resumeSnapshotCacheKey(ctx.sessionKey, ctx.sessionId, parsed.terminalId);
@@ -857,48 +921,66 @@ function formatRetryCallbackCommandResult(result) {
857
921
  `attempts: ${result.conversation?.callback_delivery?.attempts ?? "unknown"}`
858
922
  ].join("\n");
859
923
  }
860
- function formatSendCommandResult(result) {
861
- if (result.scope === "terminal_user_explicit") {
862
- const unmanaged = result.delivered_unmanaged === true;
863
- const replayed = result.replayed === true;
864
- const watchCallback = unmanaged &&
865
- result.callback_expected === true &&
866
- result.callback_mode === "terminal_watch" &&
867
- typeof result.watch_id === "string"
868
- ? result.watch_id
869
- : undefined;
870
- if (result.delivered !== true) {
871
- return [
872
- "AKK already dispatched this managed terminal Send; native acceptance is still pending.",
873
- `terminal: ${result.terminal_id ?? "unknown"}`,
874
- `message: ${result.message_id ?? "unknown"}`,
875
- "delivery: Enter dispatched; do not resend this message id.",
876
- "next: inspect AKK status or the terminal; wait for the existing managed Turn."
877
- ].join("\n");
878
- }
924
+ function formatUnconfirmedTerminalUserExplicitSendResult(result) {
925
+ const terminalInputDispatched = result.terminal_input_dispatched === true;
926
+ const enterDispatched = terminalSendEnterDispatched(result);
927
+ if (!enterDispatched) {
879
928
  return [
880
- unmanaged
881
- ? replayed
882
- ? "AKK confirmed this direct terminal Send was already delivered."
883
- : "AKK delivered the user's request directly to the coding agent."
884
- : replayed
885
- ? "AKK confirmed this managed terminal Send was already delivered."
886
- : "AKK delivered the user's terminal Send through managed routing.",
929
+ terminalInputDispatched
930
+ ? "AKK started this terminal Send, but could not prove that Enter was dispatched."
931
+ : "AKK stopped this terminal Send before any terminal input was proven.",
887
932
  `terminal: ${result.terminal_id ?? "unknown"}`,
888
933
  `message: ${result.message_id ?? "unknown"}`,
889
- `delivery: ${unmanaged ? "unmanaged fallback" : "managed"}`,
890
- ...(unmanaged
891
- ? [watchCallback
892
- ? `callback: Terminal Watch ${watchCallback}; no managed AKK Turn was created.`
893
- : "callback: unavailable; no managed AKK Turn was created."]
894
- : []),
895
- unmanaged
896
- ? watchCallback
897
- ? "next: wait for the Terminal Watch callback; watch-status is the recovery path."
898
- : "next: refresh AKK list and use Watch to observe the still-running coding-agent task."
899
- : "next: refresh AKK list; do not resend this message id."
934
+ terminalInputDispatched
935
+ ? "delivery: text input may be present; do not resend this message id."
936
+ : "delivery: no terminal input proven.",
937
+ "next: inspect AKK status and the terminal before deciding whether to continue."
900
938
  ].join("\n");
901
939
  }
940
+ return [
941
+ "AKK already dispatched this managed terminal Send; native acceptance is still pending.",
942
+ `terminal: ${result.terminal_id ?? "unknown"}`,
943
+ `message: ${result.message_id ?? "unknown"}`,
944
+ "delivery: Enter dispatched; do not resend this message id.",
945
+ "next: inspect AKK status or the terminal; wait for the existing managed Turn."
946
+ ].join("\n");
947
+ }
948
+ function formatTerminalUserExplicitSendResult(result) {
949
+ if (!terminalSendEnterDispatched(result)) {
950
+ return formatUnconfirmedTerminalUserExplicitSendResult(result);
951
+ }
952
+ const unmanaged = result.delivered_unmanaged === true;
953
+ const replayed = result.replayed === true;
954
+ const watchCallback = unmanaged &&
955
+ result.callback_expected === true &&
956
+ result.callback_mode === "terminal_watch" &&
957
+ typeof result.watch_id === "string"
958
+ ? result.watch_id
959
+ : undefined;
960
+ return [
961
+ unmanaged
962
+ ? replayed
963
+ ? "AKK confirmed this direct terminal Send was already delivered."
964
+ : "AKK delivered the user's request directly to the coding agent."
965
+ : replayed
966
+ ? "AKK confirmed this managed terminal Send was already delivered."
967
+ : "AKK delivered the user's terminal Send through managed routing.",
968
+ `terminal: ${result.terminal_id ?? "unknown"}`,
969
+ `message: ${result.message_id ?? "unknown"}`,
970
+ `delivery: ${unmanaged ? "unmanaged fallback" : "managed"}`,
971
+ ...(unmanaged
972
+ ? [watchCallback
973
+ ? `callback: Terminal Watch ${watchCallback}; no managed AKK Turn was created.`
974
+ : "callback: unavailable; no managed AKK Turn was created."]
975
+ : []),
976
+ unmanaged
977
+ ? watchCallback
978
+ ? "next: wait for the Terminal Watch callback; watch-status is the recovery path."
979
+ : "next: refresh AKK list and use Watch to observe the still-running coding-agent task."
980
+ : "next: refresh AKK list; do not resend this message id."
981
+ ].join("\n");
982
+ }
983
+ function formatManagedSendCommandResult(result) {
902
984
  const conversation = result.conversation ?? {};
903
985
  const conversationId = conversation.conversation_id ?? result.conversation_id ?? "unknown";
904
986
  const sessionId = conversation.session_id ?? result.session_id ?? conversationId;
@@ -979,6 +1061,11 @@ function formatSendCommandResult(result) {
979
1061
  : `launched: ${result.launched === true ? "yes" : "no"}`
980
1062
  ].join("\n");
981
1063
  }
1064
+ function formatSendCommandResult(result) {
1065
+ return result.scope === "terminal_user_explicit"
1066
+ ? formatTerminalUserExplicitSendResult(result)
1067
+ : formatManagedSendCommandResult(result);
1068
+ }
982
1069
  function formatCancelCommandResult(result) {
983
1070
  const conversation = result.conversation ?? {};
984
1071
  const { sessionId, turnId } = publicTurnIdentity(result);
@@ -1066,7 +1153,7 @@ function publicTurnIdentity(result) {
1066
1153
  "unknown"
1067
1154
  };
1068
1155
  }
1069
- function buildStatusCliArgs(api, params) {
1156
+ function buildStatusCliArgs(api, params, toolContext) {
1070
1157
  const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
1071
1158
  if (Object.hasOwn(params, "watch_id")) {
1072
1159
  if (Object.hasOwn(params, "turn_id") ||
@@ -1077,7 +1164,9 @@ function buildStatusCliArgs(api, params) {
1077
1164
  const watchArgs = [
1078
1165
  "watch-status",
1079
1166
  "--watch",
1080
- watchId
1167
+ watchId,
1168
+ "--openclaw-session",
1169
+ requiredOpenClawSessionKey(toolContext?.sessionKey)
1081
1170
  ];
1082
1171
  pushOptional(watchArgs, "--store-dir", resolvePluginStoreDir(config));
1083
1172
  return watchArgs;
@@ -1095,6 +1184,12 @@ function buildStatusCliArgs(api, params) {
1095
1184
  }
1096
1185
  return args;
1097
1186
  }
1187
+ async function runStatusRequest(api, params, toolContext) {
1188
+ const statusParams = isRecord(params) ? params : {};
1189
+ const statusArgs = buildStatusCliArgs(api, statusParams, toolContext);
1190
+ invalidateRequestedInteractionOffers(api, toolContext?.sessionKey, toolContext?.sessionId, statusParams);
1191
+ return runHostAwareCli(api, statusArgs);
1192
+ }
1098
1193
  function terminalScreenExcerpt(result) {
1099
1194
  const screen = result.terminal_screen;
1100
1195
  const text = typeof screen === "string"
@@ -1126,7 +1221,11 @@ async function runSendRequest(api, params, toolContext, messageId) {
1126
1221
  const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
1127
1222
  const args = ["send", "--turn", turnId];
1128
1223
  pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
1129
- return runHostAwareCli(api, args);
1224
+ const result = await runHostAwareCli(api, args);
1225
+ return {
1226
+ ...result,
1227
+ ...normalizedTerminalSendResultContract(result)
1228
+ };
1130
1229
  }
1131
1230
  const requestedType = Object.hasOwn(params, "type")
1132
1231
  ? stringValue(params.type)
@@ -1160,6 +1259,9 @@ async function runSendRequest(api, params, toolContext, messageId) {
1160
1259
  const expectedTerminalToken = terminalAction
1161
1260
  ? requiredString(terminalAction.expected_terminal_token, "current internal terminal send authority")
1162
1261
  : undefined;
1262
+ const expectedManagedTerminalToken = terminalAction
1263
+ ? stringValue(terminalAction.expected_managed_terminal_token)
1264
+ : undefined;
1163
1265
  const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
1164
1266
  const openclawSession = stringValue(toolContext?.sessionKey) ??
1165
1267
  "agent:main:main";
@@ -1173,6 +1275,7 @@ async function runSendRequest(api, params, toolContext, messageId) {
1173
1275
  args.push("--conversation", requiredString(terminalId, "terminal_id"));
1174
1276
  }
1175
1277
  pushOptional(args, "--expected-terminal-token", expectedTerminalToken);
1278
+ pushOptional(args, "--expected-managed-terminal-token", expectedManagedTerminalToken);
1176
1279
  args.push("--message", requiredString(params.request, "request"), "--background");
1177
1280
  pushOptional(args, "--type", stringValue(params.type));
1178
1281
  pushOptional(args, "--message-id", messageId);
@@ -1187,7 +1290,11 @@ async function runSendRequest(api, params, toolContext, messageId) {
1187
1290
  pushOptional(args, "--gateway-method", CALLBACK_METHOD);
1188
1291
  pushOptional(args, "--gateway-session", openclawSession);
1189
1292
  pushOptional(args, "--openclaw-bin", stringValue(config.openclawBin));
1190
- return runHostAwareCli(api, args);
1293
+ const result = await runHostAwareCli(api, args);
1294
+ return {
1295
+ ...result,
1296
+ ...normalizedTerminalSendResultContract(result)
1297
+ };
1191
1298
  }
1192
1299
  function toolResult(result, { submissionErrors = false, normalizeTurnIdentity = true, forceError = false } = {}) {
1193
1300
  const normalized = normalizeTurnIdentity ? withTurnIdentity(result) : result;
@@ -1204,9 +1311,11 @@ function toolResult(result, { submissionErrors = false, normalizeTurnIdentity =
1204
1311
  ...(submissionError || forceError ? { isError: true } : {})
1205
1312
  };
1206
1313
  }
1207
- async function privateList(api) {
1314
+ async function privateList(api, options = {}) {
1208
1315
  const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
1209
- const args = ["list", "--reconcile"];
1316
+ const args = ["list"];
1317
+ if (options.reconcile !== false)
1318
+ args.push("--reconcile");
1210
1319
  pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
1211
1320
  pushOptional(args, "--idle-timeout-minutes", numberString(config.idleTimeoutMinutes));
1212
1321
  return runHostAwareCli(api, args);
@@ -1222,15 +1331,16 @@ async function privateThreadDiscovery(api, terminalId) {
1222
1331
  pushOptional(args, "--codex-home", stringValue(config.codexHome));
1223
1332
  return runHostAwareCli(api, args);
1224
1333
  }
1225
- async function privateTerminalActionArguments(api, terminalId, tool) {
1334
+ async function privateTerminalActionArguments(api, terminalId, tool, options = {}) {
1226
1335
  return privateActionArguments(api, {
1227
1336
  tool,
1228
1337
  terminalId,
1338
+ reconcile: options.reconcile,
1229
1339
  matches: (argumentsValue) => stringValue(argumentsValue.terminal_id) === terminalId
1230
1340
  });
1231
1341
  }
1232
1342
  async function privateActionArguments(api, input) {
1233
- const result = await privateList(api);
1343
+ const result = await privateList(api, { reconcile: input.reconcile });
1234
1344
  const terminals = input.terminalId
1235
1345
  ? terminalRows(result).filter((terminal) => stringValue(terminal.id) === input.terminalId)
1236
1346
  : terminalRows(result);
@@ -1299,29 +1409,90 @@ function rememberDisplayedInteractionOffer(api, sessionKeyValue, sessionIdValue,
1299
1409
  const sessionId = stringValue(sessionIdValue);
1300
1410
  if (!sessionKey || !sessionId || !isRecord(result))
1301
1411
  return;
1302
- const terminalStatus = isRecord(result.terminal_status)
1303
- ? result.terminal_status
1304
- : undefined;
1305
- const fingerprint = stringValue(terminalStatus?.interaction_prompt_fingerprint);
1412
+ const displayed = displayedInteractionStatus(result);
1413
+ if (!displayed)
1414
+ return;
1415
+ const expectedSubjectId = stringValue(displayed.expectedSubjectId);
1416
+ if (!expectedSubjectId || expectedSubjectId === "unknown")
1417
+ return;
1418
+ invalidateOpenClawInteractionAuthorityOffersForSubject(api, sessionKey, sessionId, displayed.expectedSubjectKind, expectedSubjectId);
1419
+ const fingerprint = stringValue(displayed.fields.interaction_prompt_fingerprint);
1306
1420
  if (!isExactInteractionFingerprint(fingerprint))
1307
1421
  return;
1308
1422
  let interactionState;
1309
1423
  try {
1310
- interactionState = validateTerminalInteractionProjection(terminalStatus?.interaction_state);
1424
+ interactionState = validateAnyTerminalInteractionProjection(displayed.fields.interaction_state);
1311
1425
  }
1312
1426
  catch {
1313
1427
  return;
1314
1428
  }
1429
+ const subject = interactionProjectionSubject(interactionState);
1315
1430
  if (interactionState.state !== "pending" ||
1316
1431
  interactionState.capabilities.respond !== true ||
1317
- publicTurnIdentity(result).turnId !== interactionState.turn_id) {
1432
+ subject.kind !== displayed.expectedSubjectKind ||
1433
+ subject.id !== expectedSubjectId ||
1434
+ (interactionState.version === TERMINAL_INTERACTION_SUBJECT_VERSION &&
1435
+ (interactionState.response_authority !== "executable" ||
1436
+ interactionState.prompt_fingerprint !== fingerprint))) {
1318
1437
  return;
1319
1438
  }
1320
- rememberOpenClawPrivateAuthorityOffer(api, openClawInteractionAuthorityOfferKey(sessionKey, sessionId, interactionState.turn_id, interactionState.interaction_id), {
1439
+ rememberOpenClawPrivateAuthorityOffer(api, openClawInteractionAuthorityOfferKey(sessionKey, sessionId, subject.kind, subject.id, interactionState.interaction_id), {
1321
1440
  fingerprint,
1322
1441
  interaction_state: interactionState
1323
1442
  });
1324
1443
  }
1444
+ function invalidateRequestedInteractionOffers(api, sessionKeyValue, sessionIdValue, params) {
1445
+ const sessionKey = stringValue(sessionKeyValue);
1446
+ const sessionId = stringValue(sessionIdValue);
1447
+ if (!sessionKey || !sessionId)
1448
+ return;
1449
+ const watchId = stringValue(params.watch_id);
1450
+ if (watchId) {
1451
+ invalidateOpenClawInteractionAuthorityOffersForSubject(api, sessionKey, sessionId, "terminal_watch", watchId);
1452
+ return;
1453
+ }
1454
+ const turnId = stringValue(params.turn_id);
1455
+ if (!turnId)
1456
+ return;
1457
+ invalidateOpenClawInteractionAuthorityOffersForSubject(api, sessionKey, sessionId, "managed_turn", turnId);
1458
+ }
1459
+ function displayedInteractionStatus(result) {
1460
+ const watch = isRecord(result.watch) ? result.watch : undefined;
1461
+ if (watch) {
1462
+ return {
1463
+ fields: watch,
1464
+ expectedSubjectKind: "terminal_watch",
1465
+ expectedSubjectId: stringValue(watch.watch_id)
1466
+ };
1467
+ }
1468
+ const terminalStatus = isRecord(result.terminal_status)
1469
+ ? result.terminal_status
1470
+ : undefined;
1471
+ if (terminalStatus) {
1472
+ return {
1473
+ fields: terminalStatus,
1474
+ expectedSubjectKind: "managed_turn",
1475
+ expectedSubjectId: publicTurnIdentity(result).turnId
1476
+ };
1477
+ }
1478
+ if (result.interaction_state === undefined)
1479
+ return undefined;
1480
+ const watchId = stringValue(result.watch_id);
1481
+ return {
1482
+ fields: result,
1483
+ expectedSubjectKind: watchId ? "terminal_watch" : "managed_turn",
1484
+ expectedSubjectId: watchId ?? publicTurnIdentity(result).turnId
1485
+ };
1486
+ }
1487
+ function interactionProjectionSubject(projection) {
1488
+ if (projection.version === TERMINAL_INTERACTION_SUBJECT_VERSION) {
1489
+ return {
1490
+ kind: projection.subject.kind,
1491
+ id: terminalInteractionSubjectId(projection.subject)
1492
+ };
1493
+ }
1494
+ return { kind: "managed_turn", id: projection.turn_id };
1495
+ }
1325
1496
  function approvalTargetFromStatus(result) {
1326
1497
  const conversationId = stringValue(result.conversation_id);
1327
1498
  if (stringValue(result.source) === "terminal_control" ||
@@ -1470,20 +1641,28 @@ async function buildPrivateApprovalArgs(api, params, { sessionKey, sessionId })
1470
1641
  }
1471
1642
  function buildPrivateInteractionResponseArgs(api, params, { sessionKey, sessionId }) {
1472
1643
  const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
1473
- const turnId = requiredTerminalInteractionIdentifier(params.turn_id, "turn_id");
1644
+ const requestedSubject = requestedInteractionSubject(params);
1474
1645
  const interactionId = requiredTerminalInteractionIdentifier(params.interaction_id, "interaction_id");
1475
- const offered = consumeOpenClawPrivateAuthorityOffer(api, openClawInteractionAuthorityOfferKey(sessionKey, sessionId, turnId, interactionId));
1646
+ const offered = consumeOpenClawPrivateAuthorityOffer(api, openClawInteractionAuthorityOfferKey(sessionKey, sessionId, requestedSubject.kind, requestedSubject.id, interactionId));
1476
1647
  const fingerprint = stringValue(offered?.fingerprint);
1477
1648
  if (!isExactInteractionFingerprint(fingerprint) ||
1478
1649
  offered?.interaction_state === undefined) {
1479
1650
  throw new Error("respond_interaction requires a current pending interaction shown by agent_knock_knock_status in this controller conversation; refresh status, review the current questions, and respond again");
1480
1651
  }
1481
- const projection = validateTerminalInteractionProjection(offered.interaction_state);
1482
- const response = validateTerminalInteractionResponse(params, projection);
1652
+ const projection = validateAnyTerminalInteractionProjection(offered.interaction_state);
1653
+ assertInteractionSubjectMatchesProjection(requestedSubject, projection);
1654
+ const responseInput = interactionResponseForProjection(params, projection);
1655
+ const response = validateAnyTerminalInteractionResponse(responseInput, projection, {
1656
+ // This consumes a still-live, session/incarnation-bound private offer.
1657
+ // The CLI/bridge path always performs exact live terminal recaptures
1658
+ // before it can reserve or dispatch input, so projection expiry is a
1659
+ // recheck trigger rather than proof that the questionnaire disappeared.
1660
+ allowExpiredForLiveRecapture: true
1661
+ });
1483
1662
  const args = [
1484
1663
  "respond-interaction",
1485
- "--turn",
1486
- response.turn_id,
1664
+ requestedSubject.cliOption,
1665
+ requestedSubject.id,
1487
1666
  "--interaction",
1488
1667
  response.interaction_id,
1489
1668
  "--response-json",
@@ -1498,6 +1677,43 @@ function buildPrivateInteractionResponseArgs(api, params, { sessionKey, sessionI
1498
1677
  pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
1499
1678
  return args;
1500
1679
  }
1680
+ function requestedInteractionSubject(params) {
1681
+ const hasTurn = Object.hasOwn(params, "turn_id");
1682
+ const hasWatch = Object.hasOwn(params, "watch_id");
1683
+ if (hasTurn === hasWatch) {
1684
+ throw new Error("respond_interaction requires exactly one of turn_id or watch_id");
1685
+ }
1686
+ return hasTurn
1687
+ ? {
1688
+ kind: "managed_turn",
1689
+ id: requiredTerminalInteractionIdentifier(params.turn_id, "turn_id"),
1690
+ cliOption: "--turn"
1691
+ }
1692
+ : {
1693
+ kind: "terminal_watch",
1694
+ id: requiredTerminalInteractionIdentifier(params.watch_id, "watch_id"),
1695
+ cliOption: "--watch"
1696
+ };
1697
+ }
1698
+ function assertInteractionSubjectMatchesProjection(requested, projection) {
1699
+ const projected = interactionProjectionSubject(projection);
1700
+ if (requested.kind !== projected.kind || requested.id !== projected.id) {
1701
+ throw new Error("respond_interaction target does not match the displayed interaction subject; refresh status and respond to its exact turn_id or watch_id");
1702
+ }
1703
+ }
1704
+ function interactionResponseForProjection(params, projection) {
1705
+ if (projection.version !== TERMINAL_INTERACTION_SUBJECT_VERSION) {
1706
+ return params;
1707
+ }
1708
+ return {
1709
+ interaction_id: params.interaction_id,
1710
+ subject: projection.subject,
1711
+ ...(projection.subject.kind === "managed_turn"
1712
+ ? { turn_id: params.turn_id }
1713
+ : {}),
1714
+ answers: params.answers
1715
+ };
1716
+ }
1501
1717
  function isExactApprovalFingerprint(value) {
1502
1718
  return typeof value === "string" && /^[a-f0-9]{64}$/u.test(value);
1503
1719
  }
@@ -1571,7 +1787,7 @@ function sanitizeModelFacingValue(value, parentKey, path = []) {
1571
1787
  }
1572
1788
  if (key === "interaction_state") {
1573
1789
  try {
1574
- output[key] = validateTerminalInteractionProjection(item);
1790
+ output[key] = sanitizeModelFacingValue(validateAnyTerminalInteractionProjection(item), undefined, [...path, key]);
1575
1791
  }
1576
1792
  catch {
1577
1793
  continue;
@@ -1635,8 +1851,7 @@ function isSubmissionError(result) {
1635
1851
  if (!isRecord(result)) {
1636
1852
  return false;
1637
1853
  }
1638
- if (result.delivered === true &&
1639
- result.scope === "terminal_user_explicit") {
1854
+ if (isSuccessfulTerminalDispatch(result)) {
1640
1855
  return false;
1641
1856
  }
1642
1857
  if (terminalSubmissionReported(result)) {
@@ -1745,7 +1960,10 @@ async function runDelegate(api, params, toolContext) {
1745
1960
  // A uniquely delegated user-priority Send may deliberately complete
1746
1961
  // without creating a managed Turn. Preserve that terminal receipt for the
1747
1962
  // shared Send formatter instead of inventing Session/Turn identity.
1748
- return parsed;
1963
+ return {
1964
+ ...parsed,
1965
+ ...normalizedTerminalSendResultContract(parsed)
1966
+ };
1749
1967
  }
1750
1968
  const conversationId = stringValue(parsed.conversation_id) ??
1751
1969
  (isRecord(parsed.conversation)
@@ -1825,6 +2043,7 @@ async function runDelegate(api, params, toolContext) {
1825
2043
  callback_method: hostBridgePresentationApis.has(api)
1826
2044
  ? "command_json_v1"
1827
2045
  : CALLBACK_METHOD,
2046
+ ...normalizedTerminalSendResultContract(parsed),
1828
2047
  ...(submissionUnfenced
1829
2048
  ? {
1830
2049
  submission_outcome: "submitted",
@@ -1912,7 +2131,8 @@ function registerCliTool(api, { name, description, parameters, buildArgs, normal
1912
2131
  try {
1913
2132
  const result = await runHostAwareCli(api, await buildArgs(isRecord(params) ? params : {}, toolContext, toolCallId));
1914
2133
  const rendered = toolResult(result, {
1915
- submissionErrors: name === "agent_knock_knock_respond",
2134
+ submissionErrors: name === "agent_knock_knock_respond" ||
2135
+ name === "agent_knock_knock_identify_and_send",
1916
2136
  normalizeTurnIdentity,
1917
2137
  forceError: typeof isErrorResult === "function" && isErrorResult(result) === true
1918
2138
  });