@scotthuang/agent-knock-knock 0.12.11 → 0.12.13
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 +40 -0
- package/README.md +43 -30
- package/dist/src/approval-policy.d.ts +11 -2
- package/dist/src/approval-policy.js +33 -7
- package/dist/src/approval-policy.js.map +1 -1
- package/dist/src/callback-cli-adapter.d.ts +47 -0
- package/dist/src/callback-cli-adapter.js +156 -0
- package/dist/src/callback-cli-adapter.js.map +1 -0
- package/dist/src/callback-outbox-policy.d.ts +95 -0
- package/dist/src/callback-outbox-policy.js +149 -0
- package/dist/src/callback-outbox-policy.js.map +1 -0
- package/dist/src/callback-outbox-service.d.ts +247 -0
- package/dist/src/callback-outbox-service.js +1053 -0
- package/dist/src/callback-outbox-service.js.map +1 -0
- package/dist/src/callback-outbox-settlement.d.ts +54 -0
- package/dist/src/callback-outbox-settlement.js +241 -0
- package/dist/src/callback-outbox-settlement.js.map +1 -0
- package/dist/src/canonical-json.d.ts +1 -0
- package/dist/src/canonical-json.js +14 -0
- package/dist/src/canonical-json.js.map +1 -0
- package/dist/src/claude-lifecycle-compatibility.js +15 -0
- package/dist/src/claude-lifecycle-compatibility.js.map +1 -1
- package/dist/src/claude-local-transcript-provider.d.ts +82 -1
- package/dist/src/claude-local-transcript-provider.js +873 -23
- package/dist/src/claude-local-transcript-provider.js.map +1 -1
- package/dist/src/cli-command-runtime.d.ts +14 -0
- package/dist/src/cli-command-runtime.js +164 -0
- package/dist/src/cli-command-runtime.js.map +1 -0
- package/dist/src/cli-core.d.ts +4 -42
- package/dist/src/cli-core.js +958 -28684
- package/dist/src/cli-core.js.map +1 -1
- package/dist/src/cli-runtime-context.d.ts +57 -0
- package/dist/src/cli-runtime-context.js +118 -0
- package/dist/src/cli-runtime-context.js.map +1 -0
- package/dist/src/codex-lifecycle-compatibility.js +2 -1
- package/dist/src/codex-lifecycle-compatibility.js.map +1 -1
- package/dist/src/codex-local-session-provider.d.ts +18 -0
- package/dist/src/codex-local-session-provider.js +67 -0
- package/dist/src/codex-local-session-provider.js.map +1 -1
- package/dist/src/codex-session-provider.js +1 -3
- package/dist/src/codex-session-provider.js.map +1 -1
- package/dist/src/conversation-trace.d.ts +30 -0
- package/dist/src/conversation-trace.js +185 -0
- package/dist/src/conversation-trace.js.map +1 -0
- package/dist/src/deferred-foreground-application-service.d.ts +84 -0
- package/dist/src/deferred-foreground-application-service.js +742 -0
- package/dist/src/deferred-foreground-application-service.js.map +1 -0
- package/dist/src/deferred-foreground-authority-cli-adapter.d.ts +78 -0
- package/dist/src/deferred-foreground-authority-cli-adapter.js +795 -0
- package/dist/src/deferred-foreground-authority-cli-adapter.js.map +1 -0
- package/dist/src/deferred-foreground-boundary.d.ts +61 -0
- package/dist/src/deferred-foreground-boundary.js +2 -0
- package/dist/src/deferred-foreground-boundary.js.map +1 -0
- package/dist/src/deferred-foreground-capability.d.ts +4 -0
- package/dist/src/deferred-foreground-capability.js +184 -0
- package/dist/src/deferred-foreground-capability.js.map +1 -0
- package/dist/src/deferred-foreground-preparation-cli-adapter.d.ts +53 -0
- package/dist/src/deferred-foreground-preparation-cli-adapter.js +210 -0
- package/dist/src/deferred-foreground-preparation-cli-adapter.js.map +1 -0
- package/dist/src/deferred-foreground-preparation-service.d.ts +72 -0
- package/dist/src/deferred-foreground-preparation-service.js +179 -0
- package/dist/src/deferred-foreground-preparation-service.js.map +1 -0
- package/dist/src/deferred-foreground-recovery-cli-adapter.d.ts +139 -0
- package/dist/src/deferred-foreground-recovery-cli-adapter.js +1125 -0
- package/dist/src/deferred-foreground-recovery-cli-adapter.js.map +1 -0
- package/dist/src/deferred-foreground-recovery-service.d.ts +48 -0
- package/dist/src/deferred-foreground-recovery-service.js +91 -0
- package/dist/src/deferred-foreground-recovery-service.js.map +1 -0
- package/dist/src/deferred-foreground-transfer-policy.d.ts +2 -0
- package/dist/src/deferred-foreground-transfer-policy.js +10 -0
- package/dist/src/deferred-foreground-transfer-policy.js.map +1 -0
- package/dist/src/deferred-foreground-transfer.js +46 -105
- package/dist/src/deferred-foreground-transfer.js.map +1 -1
- package/dist/src/doctor-capabilities.d.ts +9 -0
- package/dist/src/doctor-capabilities.js +22 -0
- package/dist/src/doctor-capabilities.js.map +1 -1
- package/dist/src/durable-json-file.d.ts +22 -0
- package/dist/src/durable-json-file.js +127 -0
- package/dist/src/durable-json-file.js.map +1 -0
- package/dist/src/file-lock-cli-adapter.d.ts +43 -0
- package/dist/src/file-lock-cli-adapter.js +150 -0
- package/dist/src/file-lock-cli-adapter.js.map +1 -0
- package/dist/src/herdr-terminal-control-provider.js +1 -3
- package/dist/src/herdr-terminal-control-provider.js.map +1 -1
- package/dist/src/install-doctor-command-adapter.d.ts +17 -0
- package/dist/src/install-doctor-command-adapter.js +225 -0
- package/dist/src/install-doctor-command-adapter.js.map +1 -0
- package/dist/src/live-lifecycle-evidence.js +76 -46
- package/dist/src/live-lifecycle-evidence.js.map +1 -1
- package/dist/src/live-lifecycle-smoke.js +74 -50
- package/dist/src/live-lifecycle-smoke.js.map +1 -1
- package/dist/src/managed-session.d.ts +2 -0
- package/dist/src/managed-session.js +24 -0
- package/dist/src/managed-session.js.map +1 -1
- package/dist/src/managed-turn-recovery-service.d.ts +86 -0
- package/dist/src/managed-turn-recovery-service.js +145 -0
- package/dist/src/managed-turn-recovery-service.js.map +1 -0
- package/dist/src/mutation-transaction.d.ts +48 -0
- package/dist/src/mutation-transaction.js +73 -0
- package/dist/src/mutation-transaction.js.map +1 -0
- package/dist/src/native-thread-lifecycle-cli-adapter.d.ts +154 -0
- package/dist/src/native-thread-lifecycle-cli-adapter.js +786 -0
- package/dist/src/native-thread-lifecycle-cli-adapter.js.map +1 -0
- package/dist/src/native-thread-lifecycle-ledger-cli-adapter.d.ts +25 -0
- package/dist/src/native-thread-lifecycle-ledger-cli-adapter.js +50 -0
- package/dist/src/native-thread-lifecycle-ledger-cli-adapter.js.map +1 -0
- package/dist/src/native-thread-lifecycle-query-service.d.ts +75 -0
- package/dist/src/native-thread-lifecycle-query-service.js +297 -0
- package/dist/src/native-thread-lifecycle-query-service.js.map +1 -0
- package/dist/src/native-thread-lifecycle-recovery-adapter.d.ts +45 -0
- package/dist/src/native-thread-lifecycle-recovery-adapter.js +154 -0
- package/dist/src/native-thread-lifecycle-recovery-adapter.js.map +1 -0
- package/dist/src/native-thread-lifecycle-recovery-service.d.ts +211 -0
- package/dist/src/native-thread-lifecycle-recovery-service.js +1093 -0
- package/dist/src/native-thread-lifecycle-recovery-service.js.map +1 -0
- package/dist/src/native-thread-resume-snapshot-policy.d.ts +73 -0
- package/dist/src/native-thread-resume-snapshot-policy.js +218 -0
- package/dist/src/native-thread-resume-snapshot-policy.js.map +1 -0
- package/dist/src/native-thread-resume-snapshot.d.ts +3 -61
- package/dist/src/native-thread-resume-snapshot.js +2 -143
- package/dist/src/native-thread-resume-snapshot.js.map +1 -1
- package/dist/src/native-thread-transition-application.d.ts +147 -0
- package/dist/src/native-thread-transition-application.js +974 -0
- package/dist/src/native-thread-transition-application.js.map +1 -0
- package/dist/src/native-thread-transition-policy.d.ts +127 -0
- package/dist/src/native-thread-transition-policy.js +154 -0
- package/dist/src/native-thread-transition-policy.js.map +1 -0
- package/dist/src/native-thread-transition-resource-adapter.d.ts +12 -0
- package/dist/src/native-thread-transition-resource-adapter.js +44 -0
- package/dist/src/native-thread-transition-resource-adapter.js.map +1 -0
- package/dist/src/native-thread-transition-settlement-service.d.ts +94 -0
- package/dist/src/native-thread-transition-settlement-service.js +134 -0
- package/dist/src/native-thread-transition-settlement-service.js.map +1 -0
- package/dist/src/native-thread-transition-verification-adapter.d.ts +62 -0
- package/dist/src/native-thread-transition-verification-adapter.js +451 -0
- package/dist/src/native-thread-transition-verification-adapter.js.map +1 -0
- package/dist/src/openclaw-callback-transport.d.ts +69 -0
- package/dist/src/openclaw-callback-transport.js +583 -0
- package/dist/src/openclaw-callback-transport.js.map +1 -0
- package/dist/src/openclaw-plugin-callback-adapter.d.ts +1 -0
- package/dist/src/openclaw-plugin-callback-adapter.js +423 -0
- package/dist/src/openclaw-plugin-callback-adapter.js.map +1 -0
- package/dist/src/openclaw-plugin-command-adapter.d.ts +16 -0
- package/dist/src/openclaw-plugin-command-adapter.js +1868 -0
- package/dist/src/openclaw-plugin-command-adapter.js.map +1 -0
- package/dist/src/openclaw-plugin-helpers.d.ts +27 -4
- package/dist/src/openclaw-plugin-helpers.js +356 -81
- package/dist/src/openclaw-plugin-helpers.js.map +1 -1
- package/dist/src/openclaw-plugin-schemas.d.ts +358 -0
- package/dist/src/openclaw-plugin-schemas.js +366 -0
- package/dist/src/openclaw-plugin-schemas.js.map +1 -0
- package/dist/src/openclaw-plugin-supervisor.d.ts +6 -0
- package/dist/src/openclaw-plugin-supervisor.js +123 -0
- package/dist/src/openclaw-plugin-supervisor.js.map +1 -0
- package/dist/src/openclaw-plugin.js +7 -2212
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/dist/src/openclaw-private-authority-offers.d.ts +22 -0
- package/dist/src/openclaw-private-authority-offers.js +177 -0
- package/dist/src/openclaw-private-authority-offers.js.map +1 -0
- package/dist/src/protocol.d.ts +5 -0
- package/dist/src/protocol.js +59 -3
- package/dist/src/protocol.js.map +1 -1
- package/dist/src/session-store.d.ts +1 -0
- package/dist/src/session-store.js +84 -106
- package/dist/src/session-store.js.map +1 -1
- package/dist/src/store.d.ts +1 -0
- package/dist/src/store.js +5 -2
- package/dist/src/store.js.map +1 -1
- package/dist/src/terminal-acceptance-application-service.d.ts +65 -0
- package/dist/src/terminal-acceptance-application-service.js +63 -0
- package/dist/src/terminal-acceptance-application-service.js.map +1 -0
- package/dist/src/terminal-acceptance-cli-adapter.d.ts +210 -0
- package/dist/src/terminal-acceptance-cli-adapter.js +957 -0
- package/dist/src/terminal-acceptance-cli-adapter.js.map +1 -0
- package/dist/src/terminal-action-projection.d.ts +309 -0
- package/dist/src/terminal-action-projection.js +376 -0
- package/dist/src/terminal-action-projection.js.map +1 -0
- package/dist/src/terminal-agent-bridge.d.ts +3 -0
- package/dist/src/terminal-agent-bridge.js +29 -8
- package/dist/src/terminal-agent-bridge.js.map +1 -1
- package/dist/src/terminal-authority-policy.d.ts +141 -0
- package/dist/src/terminal-authority-policy.js +367 -0
- package/dist/src/terminal-authority-policy.js.map +1 -0
- package/dist/src/terminal-binding-authority.d.ts +118 -0
- package/dist/src/terminal-binding-authority.js +245 -0
- package/dist/src/terminal-binding-authority.js.map +1 -0
- package/dist/src/terminal-binding-reconciliation-service.d.ts +75 -0
- package/dist/src/terminal-binding-reconciliation-service.js +73 -0
- package/dist/src/terminal-binding-reconciliation-service.js.map +1 -0
- package/dist/src/terminal-command-cli-adapter.d.ts +380 -0
- package/dist/src/terminal-command-cli-adapter.js +3288 -0
- package/dist/src/terminal-command-cli-adapter.js.map +1 -0
- package/dist/src/terminal-control-ref.d.ts +4 -0
- package/dist/src/terminal-control-ref.js +11 -8
- package/dist/src/terminal-control-ref.js.map +1 -1
- package/dist/src/terminal-delegate-cli-adapter.d.ts +41 -0
- package/dist/src/terminal-delegate-cli-adapter.js +242 -0
- package/dist/src/terminal-delegate-cli-adapter.js.map +1 -0
- package/dist/src/terminal-dispatch-abort.d.ts +22 -0
- package/dist/src/terminal-dispatch-abort.js +30 -0
- package/dist/src/terminal-dispatch-abort.js.map +1 -0
- package/dist/src/terminal-dispatch-application.d.ts +115 -0
- package/dist/src/terminal-dispatch-application.js +478 -0
- package/dist/src/terminal-dispatch-application.js.map +1 -0
- package/dist/src/terminal-dispatch-capability.d.ts +63 -0
- package/dist/src/terminal-dispatch-capability.js +133 -0
- package/dist/src/terminal-dispatch-capability.js.map +1 -0
- package/dist/src/terminal-dispatch-completion-cli-adapter.d.ts +22 -0
- package/dist/src/terminal-dispatch-completion-cli-adapter.js +96 -0
- package/dist/src/terminal-dispatch-completion-cli-adapter.js.map +1 -0
- package/dist/src/terminal-dispatch-composition.d.ts +79 -0
- package/dist/src/terminal-dispatch-composition.js +2 -0
- package/dist/src/terminal-dispatch-composition.js.map +1 -0
- package/dist/src/terminal-dispatch-execution.d.ts +290 -0
- package/dist/src/terminal-dispatch-execution.js +916 -0
- package/dist/src/terminal-dispatch-execution.js.map +1 -0
- package/dist/src/terminal-dispatch-ledger-codec.d.ts +110 -0
- package/dist/src/terminal-dispatch-ledger-codec.js +399 -0
- package/dist/src/terminal-dispatch-ledger-codec.js.map +1 -0
- package/dist/src/terminal-dispatch-policy.d.ts +93 -0
- package/dist/src/terminal-dispatch-policy.js +122 -0
- package/dist/src/terminal-dispatch-policy.js.map +1 -0
- package/dist/src/terminal-dispatch-presenter.d.ts +65 -0
- package/dist/src/terminal-dispatch-presenter.js +258 -0
- package/dist/src/terminal-dispatch-presenter.js.map +1 -0
- package/dist/src/terminal-dispatch-receipt.d.ts +79 -0
- package/dist/src/terminal-dispatch-receipt.js +314 -0
- package/dist/src/terminal-dispatch-receipt.js.map +1 -0
- package/dist/src/terminal-dispatch-recovery-cli-adapter.d.ts +132 -0
- package/dist/src/terminal-dispatch-recovery-cli-adapter.js +1330 -0
- package/dist/src/terminal-dispatch-recovery-cli-adapter.js.map +1 -0
- package/dist/src/terminal-dispatch-recovery-service.d.ts +271 -0
- package/dist/src/terminal-dispatch-recovery-service.js +374 -0
- package/dist/src/terminal-dispatch-recovery-service.js.map +1 -0
- package/dist/src/terminal-dispatch-repository-cli-adapter.d.ts +35 -0
- package/dist/src/terminal-dispatch-repository-cli-adapter.js +483 -0
- package/dist/src/terminal-dispatch-repository-cli-adapter.js.map +1 -0
- package/dist/src/terminal-handoff-application-service.d.ts +7 -0
- package/dist/src/terminal-handoff-application-service.js +18 -0
- package/dist/src/terminal-handoff-application-service.js.map +1 -0
- package/dist/src/terminal-handoff-cli-adapter.d.ts +229 -0
- package/dist/src/terminal-handoff-cli-adapter.js +1389 -0
- package/dist/src/terminal-handoff-cli-adapter.js.map +1 -0
- package/dist/src/terminal-handoff-facts.d.ts +12 -0
- package/dist/src/terminal-handoff-facts.js +16 -0
- package/dist/src/terminal-handoff-facts.js.map +1 -0
- package/dist/src/terminal-identity-authority-cli-adapter.d.ts +318 -0
- package/dist/src/terminal-identity-authority-cli-adapter.js +1072 -0
- package/dist/src/terminal-identity-authority-cli-adapter.js.map +1 -0
- package/dist/src/terminal-identity-authority-service.d.ts +48 -0
- package/dist/src/terminal-identity-authority-service.js +45 -0
- package/dist/src/terminal-identity-authority-service.js.map +1 -0
- package/dist/src/terminal-list-cli-adapter.d.ts +274 -0
- package/dist/src/terminal-list-cli-adapter.js +2444 -0
- package/dist/src/terminal-list-cli-adapter.js.map +1 -0
- package/dist/src/terminal-list-renderer.d.ts +34 -0
- package/dist/src/terminal-list-renderer.js +643 -0
- package/dist/src/terminal-list-renderer.js.map +1 -0
- package/dist/src/terminal-maintenance-cli-adapter.d.ts +102 -0
- package/dist/src/terminal-maintenance-cli-adapter.js +1059 -0
- package/dist/src/terminal-maintenance-cli-adapter.js.map +1 -0
- package/dist/src/terminal-monitor-application-service.d.ts +275 -0
- package/dist/src/terminal-monitor-application-service.js +982 -0
- package/dist/src/terminal-monitor-application-service.js.map +1 -0
- package/dist/src/terminal-monitor-cli-adapter.d.ts +130 -0
- package/dist/src/terminal-monitor-cli-adapter.js +442 -0
- package/dist/src/terminal-monitor-cli-adapter.js.map +1 -0
- package/dist/src/terminal-monitor-decision-policy.d.ts +98 -0
- package/dist/src/terminal-monitor-decision-policy.js +246 -0
- package/dist/src/terminal-monitor-decision-policy.js.map +1 -0
- package/dist/src/terminal-monitor-launch-plan.d.ts +45 -0
- package/dist/src/terminal-monitor-launch-plan.js +94 -0
- package/dist/src/terminal-monitor-launch-plan.js.map +1 -0
- package/dist/src/terminal-monitor-ownership-policy.d.ts +31 -0
- package/dist/src/terminal-monitor-ownership-policy.js +64 -0
- package/dist/src/terminal-monitor-ownership-policy.js.map +1 -0
- package/dist/src/terminal-monitor-poll-policy.d.ts +48 -0
- package/dist/src/terminal-monitor-poll-policy.js +67 -0
- package/dist/src/terminal-monitor-poll-policy.js.map +1 -0
- package/dist/src/terminal-monitor-reconciliation-eligibility.d.ts +53 -0
- package/dist/src/terminal-monitor-reconciliation-eligibility.js +114 -0
- package/dist/src/terminal-monitor-reconciliation-eligibility.js.map +1 -0
- package/dist/src/terminal-monitor-state-cli-adapter.d.ts +106 -0
- package/dist/src/terminal-monitor-state-cli-adapter.js +1492 -0
- package/dist/src/terminal-monitor-state-cli-adapter.js.map +1 -0
- package/dist/src/terminal-monitor-state-reconciliation-service.d.ts +87 -0
- package/dist/src/terminal-monitor-state-reconciliation-service.js +106 -0
- package/dist/src/terminal-monitor-state-reconciliation-service.js.map +1 -0
- package/dist/src/terminal-monitor-supervision-cli-adapter.d.ts +122 -0
- package/dist/src/terminal-monitor-supervision-cli-adapter.js +685 -0
- package/dist/src/terminal-monitor-supervision-cli-adapter.js.map +1 -0
- package/dist/src/terminal-process-facts.d.ts +2 -0
- package/dist/src/terminal-process-facts.js +19 -0
- package/dist/src/terminal-process-facts.js.map +1 -0
- package/dist/src/terminal-process-source.d.ts +6 -4
- package/dist/src/terminal-process-source.js +25 -20
- package/dist/src/terminal-process-source.js.map +1 -1
- package/dist/src/terminal-runtime-cli-adapter.d.ts +80 -0
- package/dist/src/terminal-runtime-cli-adapter.js +488 -0
- package/dist/src/terminal-runtime-cli-adapter.js.map +1 -0
- package/dist/src/terminal-scoped-approval-authority.d.ts +62 -0
- package/dist/src/terminal-scoped-approval-authority.js +281 -0
- package/dist/src/terminal-scoped-approval-authority.js.map +1 -0
- package/dist/src/terminal-selector-projection-service.d.ts +26 -0
- package/dist/src/terminal-selector-projection-service.js +81 -0
- package/dist/src/terminal-selector-projection-service.js.map +1 -0
- package/dist/src/terminal-status-cli-adapter.d.ts +126 -0
- package/dist/src/terminal-status-cli-adapter.js +577 -0
- package/dist/src/terminal-status-cli-adapter.js.map +1 -0
- package/dist/src/terminal-status-facts.d.ts +56 -0
- package/dist/src/terminal-status-facts.js +199 -0
- package/dist/src/terminal-status-facts.js.map +1 -0
- package/dist/src/terminal-submission-acceptance.d.ts +62 -145
- package/dist/src/terminal-submission-acceptance.js +555 -222
- package/dist/src/terminal-submission-acceptance.js.map +1 -1
- package/dist/src/terminal-submission-facts.d.ts +123 -0
- package/dist/src/terminal-submission-facts.js +205 -0
- package/dist/src/terminal-submission-facts.js.map +1 -0
- package/dist/src/terminal-turn-binding-authority-cli-adapter.d.ts +14 -0
- package/dist/src/terminal-turn-binding-authority-cli-adapter.js +90 -0
- package/dist/src/terminal-turn-binding-authority-cli-adapter.js.map +1 -0
- package/dist/src/terminal-watch-callback-cli-adapter.d.ts +20 -0
- package/dist/src/terminal-watch-callback-cli-adapter.js +55 -0
- package/dist/src/terminal-watch-callback-cli-adapter.js.map +1 -0
- package/dist/src/terminal-watch-cli-adapter.d.ts +64 -0
- package/dist/src/terminal-watch-cli-adapter.js +508 -0
- package/dist/src/terminal-watch-cli-adapter.js.map +1 -0
- package/dist/src/terminal-watch-service.d.ts +97 -0
- package/dist/src/terminal-watch-service.js +714 -0
- package/dist/src/terminal-watch-service.js.map +1 -0
- package/dist/src/terminal-watch-store.d.ts +149 -0
- package/dist/src/terminal-watch-store.js +861 -0
- package/dist/src/terminal-watch-store.js.map +1 -0
- package/dist/src/value-guards.d.ts +4 -0
- package/dist/src/value-guards.js +12 -0
- package/dist/src/value-guards.js.map +1 -0
- package/dist/src/verified-dead-agent-policy.d.ts +192 -0
- package/dist/src/verified-dead-agent-policy.js +262 -0
- package/dist/src/verified-dead-agent-policy.js.map +1 -0
- package/docs/quickstart-herdr.md +8 -0
- package/docs/quickstart-tmux.md +33 -6
- package/openclaw.plugin.json +11 -3
- package/package.json +3 -1
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +58 -40
|
@@ -0,0 +1,1868 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { executorDefinitionForKind } from "./executors.js";
|
|
5
|
+
import { isRecord, nonBlankString as stringValue } from "./value-guards.js";
|
|
6
|
+
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";
|
|
7
|
+
import { approveParameters, cancelParameters, closeParameters, listParameters, listResumableThreadsParameters, nativeInspectParameters, newThreadParameters, reconcileBindingParameters, renewParameters, respondParameters, resumeThreadParameters, retryCallbackParameters, sendParameters, statusParameters, unwatchParameters, watchParameters } from "./openclaw-plugin-schemas.js";
|
|
8
|
+
import { consumeOpenClawPrivateAuthorityOffer, openClawApprovalAuthorityOfferKey, rememberOpenClawPrivateAuthorityOffer } from "./openclaw-private-authority-offers.js";
|
|
9
|
+
const MAX_DISPLAYED_RESUME_SNAPSHOTS = 512;
|
|
10
|
+
const OPENCLAW_HANDOFF_AUTHORITY_KIND = "handoff";
|
|
11
|
+
const OPENCLAW_RECONCILE_BINDING_AUTHORITY_KIND = "reconcile_binding";
|
|
12
|
+
const CALLBACK_METHOD = AKK_CALLBACK_METHOD;
|
|
13
|
+
export const defaultOpenClawRelayPath = fileURLToPath(new URL("./cli.js", import.meta.url));
|
|
14
|
+
const relayPathByApi = new WeakMap();
|
|
15
|
+
export function bindOpenClawRelayPath(api, relayPath) {
|
|
16
|
+
relayPathByApi.set(api, relayPath);
|
|
17
|
+
}
|
|
18
|
+
export function registerOpenClawCommands(api, displayedResumeSnapshots) {
|
|
19
|
+
api.registerCommand?.({
|
|
20
|
+
name: "akk",
|
|
21
|
+
description: "Send coding work through existing Codex or Claude Code shared terminals, inspect managed Turns, observe human-started work with durable Terminal Watch, and explicitly start, clear, list, or resume native threads.",
|
|
22
|
+
acceptsArgs: true,
|
|
23
|
+
requireAuth: true,
|
|
24
|
+
nativeProgressMessages: {
|
|
25
|
+
default: "AKK is handling the request..."
|
|
26
|
+
},
|
|
27
|
+
agentPromptGuidance: [
|
|
28
|
+
"Use /akk <task> when exactly one eligible idle coding-agent terminal pane should receive new work. Structured tools use only semantic identifiers returned by AKK: session_id for an exact managed context, terminal_id for the currently verified pane, turn_id for one managed Turn, watch_id for one Terminal Watch, and native_thread_id for one resumable native thread. AKK keeps all opaque freshness authority private and revalidates it under its locks. Use /akk watch only from a currently advertised watch action to observe one human-started active task; it sends no input and creates no Session or Turn. New/clear/resume, approval, reconciliation, handoff, and recovery still require the documented user intent or explicit confirmation. AKK never starts a coding-agent process."
|
|
29
|
+
],
|
|
30
|
+
handler: async (ctx) => handleAkkCommand(api, ctx, displayedResumeSnapshots)
|
|
31
|
+
});
|
|
32
|
+
registerCliTool(api, {
|
|
33
|
+
name: "agent_knock_knock_list",
|
|
34
|
+
description: "List existing Codex and Claude Code tmux or Herdr panes as the primary terminals[] resources, plus durable terminal_watches[] records. Use only advertised actions and their semantic IDs. AKK keeps opaque freshness authority private and revalidates terminal, binding, candidate, approval, and handoff state before every side effect. A session_exact send uses session_id; terminal_follow_current and Watch use terminal_id; managed controls use turn_id; lifecycle resume additionally uses the complete native_thread_id. Recovery actions remain explicit-confirmation operations. AKK never starts a coding-agent process.",
|
|
35
|
+
parameters: listParameters,
|
|
36
|
+
buildArgs: (params) => {
|
|
37
|
+
const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
|
|
38
|
+
const args = ["list", "--reconcile"];
|
|
39
|
+
pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
|
|
40
|
+
pushOptional(args, "--idle-timeout-minutes", numberString(params.idleTimeoutMinutes) ?? numberString(api.pluginConfig?.idleTimeoutMinutes));
|
|
41
|
+
pushOptional(args, "--agent", stringValue(params.agent));
|
|
42
|
+
pushOptional(args, "--status", stringValue(params.status));
|
|
43
|
+
if (params.all === true) {
|
|
44
|
+
args.push("--all");
|
|
45
|
+
}
|
|
46
|
+
if (params.noApprovalScan === true) {
|
|
47
|
+
args.push("--no-approval-scan");
|
|
48
|
+
}
|
|
49
|
+
if (params.terminalDebug === true) {
|
|
50
|
+
args.push("--terminal-debug");
|
|
51
|
+
}
|
|
52
|
+
return args;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
registerCliTool(api, {
|
|
56
|
+
name: "agent_knock_knock_watch",
|
|
57
|
+
description: "Start one durable Terminal Watch for the exact supported human-started task already active in a listed Codex or Claude Code terminal. Call only from that terminal row's current watch action and pass its exact terminal_id. AKK refreshes and revalidates current observation authority internally. This observes external work, creates no AKK Session or Turn, sends no terminal input, and never adopts or blocks the human's terminal task.",
|
|
58
|
+
parameters: watchParameters,
|
|
59
|
+
normalizeTurnIdentity: false,
|
|
60
|
+
buildArgs: (params, toolContext) => {
|
|
61
|
+
const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
|
|
62
|
+
const openclawSession = stringValue(toolContext?.sessionKey) ??
|
|
63
|
+
"agent:main:main";
|
|
64
|
+
const args = [
|
|
65
|
+
"watch-terminal",
|
|
66
|
+
"--terminal",
|
|
67
|
+
requiredString(params.terminal_id, "terminal_id")
|
|
68
|
+
];
|
|
69
|
+
pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
|
|
70
|
+
pushOptional(args, "--hard-timeout-minutes", numberString(params.hardTimeoutMinutes) ??
|
|
71
|
+
numberString(config.agentHardTimeoutMinutes));
|
|
72
|
+
pushOptional(args, "--openclaw-session", openclawSession);
|
|
73
|
+
pushOptional(args, "--openclaw-bin", stringValue(config.openclawBin));
|
|
74
|
+
return args;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
registerCliTool(api, {
|
|
78
|
+
name: "agent_knock_knock_unwatch",
|
|
79
|
+
description: "Stop one exact durable Terminal Watch by its authoritative watch_id. This cancels observation only; it sends no terminal input and does not interrupt, adopt, or otherwise mutate the human's coding-agent task.",
|
|
80
|
+
parameters: unwatchParameters,
|
|
81
|
+
normalizeTurnIdentity: false,
|
|
82
|
+
buildArgs: (params) => {
|
|
83
|
+
const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
|
|
84
|
+
const args = [
|
|
85
|
+
"unwatch-terminal",
|
|
86
|
+
"--watch",
|
|
87
|
+
requiredString(params.watch_id, "watch_id")
|
|
88
|
+
];
|
|
89
|
+
pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
|
|
90
|
+
return args;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
registerCliTool(api, {
|
|
94
|
+
name: "agent_knock_knock_list_resumable_threads",
|
|
95
|
+
description: "List verified native Codex or Claude Code threads for one exact terminal. Resume only a row with resumable=true by passing this terminal_id and that row's complete native_thread_id. AKK retains candidate and binding freshness evidence privately. Number and short-id fields are slash-command display aids, never tool arguments. This is read-only for Session/Turn state and creates no AKK Turn.",
|
|
96
|
+
parameters: listResumableThreadsParameters,
|
|
97
|
+
normalizeTurnIdentity: false,
|
|
98
|
+
buildArgs: (params) => {
|
|
99
|
+
const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
|
|
100
|
+
const args = [
|
|
101
|
+
"list-resumable-threads",
|
|
102
|
+
"--terminal",
|
|
103
|
+
requiredString(params.terminal_id, "terminal_id")
|
|
104
|
+
];
|
|
105
|
+
pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
|
|
106
|
+
pushOptional(args, "--codex-home", stringValue(config.codexHome));
|
|
107
|
+
return args;
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
registerCliTool(api, {
|
|
111
|
+
name: "agent_knock_knock_native_inspect",
|
|
112
|
+
description: "Execute one closed, exact-version native status inspection in an exact Codex or Claude Code terminal. Pass only terminal_id and inspection=status; AKK refreshes binding authority privately. Arbitrary slash commands remain unavailable. This creates no AKK Session, Turn, receipt, monitor, or callback.",
|
|
113
|
+
parameters: nativeInspectParameters,
|
|
114
|
+
normalizeTurnIdentity: false,
|
|
115
|
+
buildArgs: (params) => {
|
|
116
|
+
const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
|
|
117
|
+
const inspection = requiredString(params.inspection, "inspection");
|
|
118
|
+
if (inspection !== "status") {
|
|
119
|
+
throw new Error("inspection must be status");
|
|
120
|
+
}
|
|
121
|
+
const terminalId = requiredString(params.terminal_id, "terminal_id");
|
|
122
|
+
const action = privateTerminalActionArguments(api, terminalId, "agent_knock_knock_native_inspect");
|
|
123
|
+
const args = [
|
|
124
|
+
"native-inspect",
|
|
125
|
+
"--terminal",
|
|
126
|
+
terminalId,
|
|
127
|
+
"--inspection",
|
|
128
|
+
inspection,
|
|
129
|
+
"--expected-binding-token",
|
|
130
|
+
requiredString(action.expected_binding_token, "current internal native-inspect binding authority")
|
|
131
|
+
];
|
|
132
|
+
pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
|
|
133
|
+
pushOptional(args, "--codex-home", stringValue(config.codexHome));
|
|
134
|
+
return args;
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
registerCliTool(api, {
|
|
138
|
+
name: "agent_knock_knock_new_thread",
|
|
139
|
+
description: "Start and verify a clean native coding-agent thread in the exact terminal_id after explicit user intent. AKK refreshes lifecycle authority privately. Never send /clear as ordinary task text. This creates a new AKK Session but no Turn.",
|
|
140
|
+
parameters: newThreadParameters,
|
|
141
|
+
normalizeTurnIdentity: false,
|
|
142
|
+
isErrorResult: (result) => !isAkkThreadTransitionSuccess(result),
|
|
143
|
+
buildArgs: (params) => {
|
|
144
|
+
const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
|
|
145
|
+
const terminalId = requiredString(params.terminal_id, "terminal_id");
|
|
146
|
+
const action = privateTerminalActionArguments(api, terminalId, "agent_knock_knock_new_thread");
|
|
147
|
+
const args = [
|
|
148
|
+
"new-thread",
|
|
149
|
+
"--terminal",
|
|
150
|
+
terminalId,
|
|
151
|
+
"--expected-binding-token",
|
|
152
|
+
requiredString(action.expected_binding_token, "current internal new-thread binding authority")
|
|
153
|
+
];
|
|
154
|
+
pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
|
|
155
|
+
pushOptional(args, "--codex-home", stringValue(config.codexHome));
|
|
156
|
+
return args;
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
registerCliTool(api, {
|
|
160
|
+
name: "agent_knock_knock_reconcile_binding",
|
|
161
|
+
description: "Detach one exact conflicting managed Session binding without adopting the live replacement thread. Pass only the advertised terminal_id and conflicting_session_id after explicit user confirmation; AKK refreshes all revision and binding authority privately. This sends no coding-agent input and creates no Turn.",
|
|
162
|
+
parameters: reconcileBindingParameters,
|
|
163
|
+
normalizeTurnIdentity: false,
|
|
164
|
+
buildArgs: (params, toolContext) => {
|
|
165
|
+
const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
|
|
166
|
+
const terminalId = requiredString(params.terminal_id, "terminal_id");
|
|
167
|
+
const conflictingSessionId = requiredString(params.conflicting_session_id, "conflicting_session_id");
|
|
168
|
+
const action = consumeDisplayedPrivateAction(api, {
|
|
169
|
+
sessionKey: requiredOpenClawSessionKey(toolContext?.sessionKey),
|
|
170
|
+
sessionId: requiredOpenClawSessionId(toolContext?.sessionId),
|
|
171
|
+
kind: OPENCLAW_RECONCILE_BINDING_AUTHORITY_KIND,
|
|
172
|
+
target: { type: "terminal_id", id: terminalId },
|
|
173
|
+
tool: "agent_knock_knock_reconcile_binding",
|
|
174
|
+
terminalId,
|
|
175
|
+
matches: (argumentsValue) => stringValue(argumentsValue.terminal_id) === terminalId &&
|
|
176
|
+
stringValue(argumentsValue.conflicting_session_id) ===
|
|
177
|
+
conflictingSessionId
|
|
178
|
+
});
|
|
179
|
+
const revision = Number(action.expected_session_revision);
|
|
180
|
+
if (!Number.isSafeInteger(revision) || revision < 1) {
|
|
181
|
+
throw new Error("expected_session_revision must be a positive safe integer");
|
|
182
|
+
}
|
|
183
|
+
const args = [
|
|
184
|
+
"reconcile-binding",
|
|
185
|
+
"--terminal",
|
|
186
|
+
terminalId,
|
|
187
|
+
"--conflicting-session",
|
|
188
|
+
requiredString(conflictingSessionId, "conflicting_session_id"),
|
|
189
|
+
"--expected-session-revision",
|
|
190
|
+
String(revision),
|
|
191
|
+
"--expected-binding-token",
|
|
192
|
+
requiredString(action.expected_binding_token, "current internal conflicting binding authority"),
|
|
193
|
+
"--expected-terminal-token",
|
|
194
|
+
requiredString(action.expected_terminal_token, "current internal terminal authority")
|
|
195
|
+
];
|
|
196
|
+
pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
|
|
197
|
+
pushOptional(args, "--codex-home", stringValue(config.codexHome));
|
|
198
|
+
return args;
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
registerCliTool(api, {
|
|
202
|
+
name: "agent_knock_knock_resume_thread",
|
|
203
|
+
description: "Resume one exact verified historical native thread after explicit user intent. Pass only terminal_id and one complete native_thread_id from a resumable=true row; AKK refreshes binding and candidate evidence privately. This creates or reactivates an AKK Session but no Turn.",
|
|
204
|
+
parameters: resumeThreadParameters,
|
|
205
|
+
normalizeTurnIdentity: false,
|
|
206
|
+
isErrorResult: (result) => !isAkkThreadTransitionSuccess(result),
|
|
207
|
+
buildArgs: (params) => {
|
|
208
|
+
const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
|
|
209
|
+
const terminalId = requiredString(params.terminal_id, "terminal_id");
|
|
210
|
+
const nativeThreadId = requiredString(params.native_thread_id, "native_thread_id");
|
|
211
|
+
const discovery = privateThreadDiscovery(api, terminalId);
|
|
212
|
+
const candidate = Array.isArray(discovery.threads)
|
|
213
|
+
? discovery.threads.find((thread) => isRecord(thread) &&
|
|
214
|
+
thread.resumable === true &&
|
|
215
|
+
stringValue(thread.native_thread_id) === nativeThreadId)
|
|
216
|
+
: undefined;
|
|
217
|
+
if (!isRecord(candidate)) {
|
|
218
|
+
throw new Error(`native thread ${nativeThreadId} is not resumable in the current lifecycle snapshot`);
|
|
219
|
+
}
|
|
220
|
+
const args = [
|
|
221
|
+
"resume-thread",
|
|
222
|
+
"--terminal",
|
|
223
|
+
terminalId,
|
|
224
|
+
"--native-thread",
|
|
225
|
+
nativeThreadId,
|
|
226
|
+
"--expected-binding-token",
|
|
227
|
+
requiredString(discovery.expected_binding_token, "current internal lifecycle binding authority"),
|
|
228
|
+
"--candidate-token",
|
|
229
|
+
requiredString(candidate.candidate_token, "current internal resume candidate authority")
|
|
230
|
+
];
|
|
231
|
+
pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
|
|
232
|
+
pushOptional(args, "--codex-home", stringValue(config.codexHome));
|
|
233
|
+
return args;
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
api.registerTool((toolContext) => ({
|
|
237
|
+
label: "AKK Status",
|
|
238
|
+
name: "agent_knock_knock_status",
|
|
239
|
+
description: "Inspect one exact AKK-managed Turn by its authoritative turn_id, one durable Terminal Watch by its authoritative watch_id, or use only a raw terminal row's own prefilled compatibility selector. These targets are mutually exclusive. The deprecated conversation_id remains a legacy Turn alias and the list-prefilled raw-terminal input; never construct it. Watch status describes observed external work and never claims AKK sent or adopted the task. AKK never starts a coding agent.",
|
|
240
|
+
parameters: statusParameters,
|
|
241
|
+
async execute(_toolCallId, params) {
|
|
242
|
+
try {
|
|
243
|
+
const result = runCli(api, buildStatusCliArgs(api, isRecord(params) ? params : {}));
|
|
244
|
+
const rendered = toolResult(result);
|
|
245
|
+
rememberDisplayedApprovalOffer(api, toolContext?.sessionKey, toolContext?.sessionId, result);
|
|
246
|
+
return rendered;
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
throw modelFacingToolError(error);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}), { name: "agent_knock_knock_status", optional: true });
|
|
253
|
+
api.registerTool((toolContext) => ({
|
|
254
|
+
label: "AKK Send",
|
|
255
|
+
name: "agent_knock_knock_send",
|
|
256
|
+
description: "Start a new AKK Turn. Use session_id only for an advertised session_exact action, or terminal_id for an advertised terminal_follow_current action; omit both only when AKK should require one unique eligible idle pane. AKK derives all terminal freshness authority privately, revalidates the exact pane/process/context under lock, sends once, and binds only the unique accepting native thread. Never pass a Turn id as a destination. This is asynchronous: after acceptance, yield and wait for the callback or an explicit status request.",
|
|
257
|
+
parameters: sendParameters,
|
|
258
|
+
async execute(toolCallId, params) {
|
|
259
|
+
try {
|
|
260
|
+
const result = await runSendRequest(api, isRecord(params) ? params : {}, toolContext, terminalMessageIdForToolCall({
|
|
261
|
+
toolCallId,
|
|
262
|
+
sessionKey: toolContext?.sessionKey,
|
|
263
|
+
sessionId: toolContext?.sessionId,
|
|
264
|
+
toolName: "agent_knock_knock_send"
|
|
265
|
+
}));
|
|
266
|
+
return toolResult(result, { submissionErrors: true });
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
throw modelFacingToolError(error);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}), { name: "agent_knock_knock_send", optional: true });
|
|
273
|
+
registerCliTool(api, {
|
|
274
|
+
name: "agent_knock_knock_respond",
|
|
275
|
+
description: "Respond to a question or blocked callback in one exact in-flight AKK turn. This continues that turn and does not create a new turn; for later ordinary work refresh agent_knock_knock_list and use that terminal row's currently advertised send action.",
|
|
276
|
+
parameters: respondParameters,
|
|
277
|
+
buildArgs: (params, toolContext, toolCallId) => {
|
|
278
|
+
const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
|
|
279
|
+
const openclawSession = stringValue(toolContext?.sessionKey) ?? "agent:main:main";
|
|
280
|
+
const args = [
|
|
281
|
+
"respond",
|
|
282
|
+
"--turn",
|
|
283
|
+
authoritativeManagedId(params.turn_id, "turn_id"),
|
|
284
|
+
"--message",
|
|
285
|
+
requiredString(params.request, "request")
|
|
286
|
+
];
|
|
287
|
+
pushOptional(args, "--message-id", terminalMessageIdForToolCall({
|
|
288
|
+
toolCallId,
|
|
289
|
+
sessionKey: openclawSession,
|
|
290
|
+
sessionId: toolContext?.sessionId,
|
|
291
|
+
toolName: "agent_knock_knock_respond"
|
|
292
|
+
}));
|
|
293
|
+
pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
|
|
294
|
+
pushOptional(args, "--openclaw-session", openclawSession);
|
|
295
|
+
return args;
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
registerCliTool(api, {
|
|
299
|
+
name: "agent_knock_knock_approve",
|
|
300
|
+
description: "Manually approve the current exact permission request only after the user reviews and explicitly confirms it. Use turn_id for a managed Turn or terminal_id for a separately advertised terminal-scoped approval. AKK privately refreshes the approval prompt and terminal authority, then the CLI recaptures the same prompt under lock before sending keys. Never retry an interrupted approval blindly. This tool cannot enable automatic approval.",
|
|
301
|
+
parameters: approveParameters,
|
|
302
|
+
buildArgs: (params, toolContext) => buildPrivateApprovalArgs(api, params, {
|
|
303
|
+
sessionKey: requiredOpenClawSessionKey(toolContext?.sessionKey),
|
|
304
|
+
sessionId: requiredOpenClawSessionId(toolContext?.sessionId)
|
|
305
|
+
})
|
|
306
|
+
});
|
|
307
|
+
registerCliTool(api, {
|
|
308
|
+
name: "agent_knock_knock_renew",
|
|
309
|
+
description: "Renew monitoring for one exact stalled turn_id without sending text or keys to the coding agent. Use this when the user wants a still-live long-running terminal task to keep monitoring after an inactivity stall.",
|
|
310
|
+
parameters: renewParameters,
|
|
311
|
+
buildArgs: (params) => {
|
|
312
|
+
const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
|
|
313
|
+
const args = ["renew"];
|
|
314
|
+
pushTurnTarget(args, params);
|
|
315
|
+
pushOptional(args, "--minutes", numberString(params.minutes) ?? numberString(config.agentTimeoutMinutes));
|
|
316
|
+
pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
|
|
317
|
+
return args;
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
registerCliTool(api, {
|
|
321
|
+
name: "agent_knock_knock_retry_callback",
|
|
322
|
+
description: "Retry a persisted AKK callback for an exact turn that failed before reaching OpenClaw. The original callback message id and turn identity are reused for idempotent delivery.",
|
|
323
|
+
parameters: retryCallbackParameters,
|
|
324
|
+
buildArgs: (params) => {
|
|
325
|
+
const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
|
|
326
|
+
const args = ["retry-callback"];
|
|
327
|
+
pushTurnTarget(args, params);
|
|
328
|
+
pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
|
|
329
|
+
return args;
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
registerCliTool(api, {
|
|
333
|
+
name: "agent_knock_knock_cancel",
|
|
334
|
+
description: "Interrupt one exact AKK turn_id, or use only an unmanaged raw terminal row's own prefilled cancel action. Claude sends Escape; Codex uses its declared interrupt key. The shared terminal pane remains open for human takeover.",
|
|
335
|
+
parameters: cancelParameters,
|
|
336
|
+
buildArgs: (params) => {
|
|
337
|
+
const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
|
|
338
|
+
const args = ["cancel"];
|
|
339
|
+
pushTurnTarget(args, params);
|
|
340
|
+
pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
|
|
341
|
+
pushOptional(args, "--idle-timeout-minutes", numberString(params.idleTimeoutMinutes) ?? numberString(config.idleTimeoutMinutes));
|
|
342
|
+
return args;
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
registerCliTool(api, {
|
|
346
|
+
name: "agent_knock_knock_close",
|
|
347
|
+
description: "Close one managed Turn record without terminating the shared pane. Ordinary close uses turn_id. Orphan dispatch/lifecycle recovery may additionally use its exact expected_message_id or expected_transition_id. Human-context handoff uses the advertised turn_id plus reason=superseded_by_human_context_switch after explicit confirmation; AKK resolves the current handoff authority privately. Refresh list afterward.",
|
|
348
|
+
parameters: closeParameters,
|
|
349
|
+
isErrorResult: isBlockedTerminalDispatchResult,
|
|
350
|
+
buildArgs: (params, toolContext) => {
|
|
351
|
+
const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
|
|
352
|
+
const args = ["close"];
|
|
353
|
+
assertExclusiveRecoveryFence(params);
|
|
354
|
+
const handoffReason = stringValue(params.reason) ===
|
|
355
|
+
"superseded_by_human_context_switch";
|
|
356
|
+
const handoffAction = handoffReason
|
|
357
|
+
? consumeDisplayedPrivateAction(api, {
|
|
358
|
+
sessionKey: requiredOpenClawSessionKey(toolContext?.sessionKey),
|
|
359
|
+
sessionId: requiredOpenClawSessionId(toolContext?.sessionId),
|
|
360
|
+
kind: OPENCLAW_HANDOFF_AUTHORITY_KIND,
|
|
361
|
+
target: {
|
|
362
|
+
type: "turn_id",
|
|
363
|
+
id: requiredString(params.turn_id, "turn_id")
|
|
364
|
+
},
|
|
365
|
+
tool: "agent_knock_knock_close",
|
|
366
|
+
matches: (argumentsValue) => stringValue(argumentsValue.turn_id) ===
|
|
367
|
+
stringValue(params.turn_id) &&
|
|
368
|
+
stringValue(argumentsValue.reason) ===
|
|
369
|
+
"superseded_by_human_context_switch"
|
|
370
|
+
})
|
|
371
|
+
: undefined;
|
|
372
|
+
pushTurnTarget(args, params);
|
|
373
|
+
pushOptional(args, "--reason", stringValue(params.reason));
|
|
374
|
+
pushOptional(args, "--expected-message-id", stringValue(params.expected_message_id));
|
|
375
|
+
pushOptional(args, "--expected-transition-id", stringValue(params.expected_transition_id));
|
|
376
|
+
pushOptional(args, "--expected-handoff-token", handoffAction
|
|
377
|
+
? requiredString(handoffAction.expected_handoff_token, "current internal handoff authority")
|
|
378
|
+
: undefined);
|
|
379
|
+
pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
|
|
380
|
+
return args;
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
function resumeSelectionScope(sessionKey, sessionId) {
|
|
385
|
+
const key = requiredString(sessionKey, "OpenClaw session identity is required for snapshot-bound Resume");
|
|
386
|
+
const incarnation = stringValue(sessionId) ?? null;
|
|
387
|
+
return `openclaw:${createHash("sha256")
|
|
388
|
+
.update(JSON.stringify([key, incarnation]))
|
|
389
|
+
.digest("hex")}`;
|
|
390
|
+
}
|
|
391
|
+
function resumeSnapshotCacheKey(sessionKey, sessionId, terminalId) {
|
|
392
|
+
const key = requiredString(sessionKey, "OpenClaw session identity is required for snapshot-bound Resume");
|
|
393
|
+
return JSON.stringify([key, stringValue(sessionId) ?? null, terminalId]);
|
|
394
|
+
}
|
|
395
|
+
function rememberDisplayedResumeSnapshot(snapshots, key, snapshotId, expiresAtMs) {
|
|
396
|
+
const now = Date.now();
|
|
397
|
+
for (const [candidateKey, value] of snapshots) {
|
|
398
|
+
if (value.expiresAtMs <= now) {
|
|
399
|
+
snapshots.delete(candidateKey);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
snapshots.delete(key);
|
|
403
|
+
while (snapshots.size >= MAX_DISPLAYED_RESUME_SNAPSHOTS) {
|
|
404
|
+
const oldestKey = snapshots.keys().next().value;
|
|
405
|
+
if (typeof oldestKey !== "string") {
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
snapshots.delete(oldestKey);
|
|
409
|
+
}
|
|
410
|
+
snapshots.set(key, { snapshotId, expiresAtMs });
|
|
411
|
+
}
|
|
412
|
+
function currentDisplayedResumeSnapshotId(snapshots, key) {
|
|
413
|
+
const value = snapshots.get(key);
|
|
414
|
+
if (!value) {
|
|
415
|
+
return undefined;
|
|
416
|
+
}
|
|
417
|
+
if (value.expiresAtMs <= Date.now()) {
|
|
418
|
+
snapshots.delete(key);
|
|
419
|
+
return undefined;
|
|
420
|
+
}
|
|
421
|
+
return value.snapshotId;
|
|
422
|
+
}
|
|
423
|
+
async function handleAkkCommand(api, ctx, displayedResumeSnapshots) {
|
|
424
|
+
try {
|
|
425
|
+
const parsed = parseAkkCommand(ctx.args);
|
|
426
|
+
if (parsed.action === "help") {
|
|
427
|
+
return { text: akkUsageText() };
|
|
428
|
+
}
|
|
429
|
+
if (parsed.action === "delegate") {
|
|
430
|
+
const result = await runDelegate(api, {
|
|
431
|
+
request: parsed.request
|
|
432
|
+
}, {
|
|
433
|
+
sessionKey: ctx.sessionKey
|
|
434
|
+
});
|
|
435
|
+
return {
|
|
436
|
+
text: formatDelegateCommandResult(result),
|
|
437
|
+
isError: result.status !== "async_pending"
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
|
|
441
|
+
if (parsed.action === "list-resumable-threads" ||
|
|
442
|
+
parsed.action === "new-thread" ||
|
|
443
|
+
parsed.action === "resume-thread") {
|
|
444
|
+
return await handleAkkLifecycleCommand(api, ctx, parsed, config, displayedResumeSnapshots);
|
|
445
|
+
}
|
|
446
|
+
const args = parsed.action === "approve"
|
|
447
|
+
? buildPrivateApprovalArgs(api, { turn_id: parsed.turnId }, {
|
|
448
|
+
sessionKey: requiredOpenClawSessionKey(ctx.sessionKey),
|
|
449
|
+
sessionId: requiredOpenClawSessionId(ctx.sessionId)
|
|
450
|
+
})
|
|
451
|
+
: buildAkkCommandCliArgs(parsed, config, {
|
|
452
|
+
sessionKey: ctx.sessionKey
|
|
453
|
+
});
|
|
454
|
+
if (!args) {
|
|
455
|
+
return { text: akkUsageText(), isError: true };
|
|
456
|
+
}
|
|
457
|
+
// Doctor calls back into the running Gateway for its independent health
|
|
458
|
+
// check. Keep the Gateway event loop free while that child CLI runs.
|
|
459
|
+
const result = parsed.action === "doctor"
|
|
460
|
+
? await runCliAsync(api, args, { allowNonzeroJson: true })
|
|
461
|
+
: runCli(api, args);
|
|
462
|
+
switch (parsed.action) {
|
|
463
|
+
case "doctor":
|
|
464
|
+
return {
|
|
465
|
+
text: formatDoctorCommandResult(result),
|
|
466
|
+
isError: result.ok !== true
|
|
467
|
+
};
|
|
468
|
+
case "list":
|
|
469
|
+
return { text: formatAkkListCommandResult(result) };
|
|
470
|
+
case "watch":
|
|
471
|
+
return { text: formatAkkWatchCommandResult(result) };
|
|
472
|
+
case "unwatch":
|
|
473
|
+
return { text: formatAkkUnwatchCommandResult(result) };
|
|
474
|
+
case "status":
|
|
475
|
+
return { text: formatStatusCommandResult(result) };
|
|
476
|
+
case "send":
|
|
477
|
+
return {
|
|
478
|
+
text: formatSendCommandResult(result),
|
|
479
|
+
isError: terminalSubmissionReported(result)
|
|
480
|
+
? !isAkkNativeSubmissionAccepted(result)
|
|
481
|
+
: result.status === "delivered_unfenced"
|
|
482
|
+
};
|
|
483
|
+
case "respond":
|
|
484
|
+
return formatAkkRespondCommandResult(result);
|
|
485
|
+
case "approve":
|
|
486
|
+
return { text: formatApproveCommandResult(result) };
|
|
487
|
+
case "renew":
|
|
488
|
+
return { text: formatRenewCommandResult(result) };
|
|
489
|
+
case "retry-callback":
|
|
490
|
+
return { text: formatRetryCallbackCommandResult(result) };
|
|
491
|
+
case "cancel":
|
|
492
|
+
return { text: formatCancelCommandResult(result) };
|
|
493
|
+
case "close":
|
|
494
|
+
return {
|
|
495
|
+
text: formatCloseCommandResult(result),
|
|
496
|
+
isError: isBlockedTerminalDispatchResult(result)
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
const message = modelFacingErrorMessage(error);
|
|
502
|
+
return {
|
|
503
|
+
text: `AKK command failed: ${message}`,
|
|
504
|
+
isError: true
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
async function handleAkkLifecycleCommand(api, ctx, parsed, config, displayedResumeSnapshots) {
|
|
509
|
+
const selectionScope = resumeSelectionScope(ctx.sessionKey, ctx.sessionId);
|
|
510
|
+
const snapshotCacheKey = resumeSnapshotCacheKey(ctx.sessionKey, ctx.sessionId, parsed.terminalId);
|
|
511
|
+
if (parsed.action === "resume-thread" &&
|
|
512
|
+
parsed.selection &&
|
|
513
|
+
["number", "short-id"].includes(parsed.selection.kind)) {
|
|
514
|
+
requiredString(ctx.sessionId, "OpenClaw conversation incarnation is required for number or short-id Resume; run /akk threads again in the current conversation or use the complete UUID");
|
|
515
|
+
const mutationArgs = buildAkkCommandCliArgs(parsed, config, {
|
|
516
|
+
sessionKey: ctx.sessionKey,
|
|
517
|
+
selectionScope,
|
|
518
|
+
selectionSnapshotId: currentDisplayedResumeSnapshotId(displayedResumeSnapshots, snapshotCacheKey)
|
|
519
|
+
});
|
|
520
|
+
if (!mutationArgs) {
|
|
521
|
+
throw new Error("could not build snapshot-bound resume command");
|
|
522
|
+
}
|
|
523
|
+
const transitionResult = runCli(api, mutationArgs);
|
|
524
|
+
if (isAkkThreadTransitionSuccess(transitionResult)) {
|
|
525
|
+
displayedResumeSnapshots.delete(snapshotCacheKey);
|
|
526
|
+
}
|
|
527
|
+
return {
|
|
528
|
+
text: formatAkkThreadTransitionCommandResult(transitionResult),
|
|
529
|
+
isError: !isAkkThreadTransitionSuccess(transitionResult)
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
const discoveryArgs = buildAkkCommandCliArgs({
|
|
533
|
+
action: "list-resumable-threads",
|
|
534
|
+
terminalId: parsed.terminalId
|
|
535
|
+
}, config, { selectionScope });
|
|
536
|
+
if (!discoveryArgs) {
|
|
537
|
+
throw new Error("could not build native-thread discovery command");
|
|
538
|
+
}
|
|
539
|
+
const discovery = runCli(api, discoveryArgs);
|
|
540
|
+
if (parsed.action === "list-resumable-threads" ||
|
|
541
|
+
(parsed.action === "resume-thread" &&
|
|
542
|
+
!parsed.selection)) {
|
|
543
|
+
const snapshotId = isRecord(discovery.selection_snapshot)
|
|
544
|
+
? stringValue(discovery.selection_snapshot.snapshot_id)
|
|
545
|
+
: undefined;
|
|
546
|
+
const expiresAt = isRecord(discovery.selection_snapshot)
|
|
547
|
+
? Date.parse(String(discovery.selection_snapshot.expires_at ?? ""))
|
|
548
|
+
: Number.NaN;
|
|
549
|
+
if (snapshotId && Number.isFinite(expiresAt) && expiresAt > Date.now()) {
|
|
550
|
+
rememberDisplayedResumeSnapshot(displayedResumeSnapshots, snapshotCacheKey, snapshotId, expiresAt);
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
displayedResumeSnapshots.delete(snapshotCacheKey);
|
|
554
|
+
}
|
|
555
|
+
return { text: formatAkkThreadsCommandResult(discovery) };
|
|
556
|
+
}
|
|
557
|
+
let expectedBindingToken = requiredString(discovery.expected_binding_token, "expected_binding_token from lifecycle discovery");
|
|
558
|
+
let candidateToken;
|
|
559
|
+
let mutationCommand = parsed;
|
|
560
|
+
if (parsed.action === "resume-thread") {
|
|
561
|
+
let nativeThreadId;
|
|
562
|
+
if (parsed.selection?.kind === "previous") {
|
|
563
|
+
const previous = isRecord(discovery.previous)
|
|
564
|
+
? discovery.previous
|
|
565
|
+
: undefined;
|
|
566
|
+
const action = isRecord(previous?.available_actions) &&
|
|
567
|
+
isRecord(previous.available_actions.resume_thread)
|
|
568
|
+
? previous.available_actions.resume_thread
|
|
569
|
+
: undefined;
|
|
570
|
+
const actionArguments = isRecord(action?.arguments)
|
|
571
|
+
? action.arguments
|
|
572
|
+
: undefined;
|
|
573
|
+
nativeThreadId = requiredString(actionArguments?.native_thread_id, "no verified previous native thread is available; run /akk threads");
|
|
574
|
+
if (stringValue(actionArguments?.terminal_id) !== parsed.terminalId) {
|
|
575
|
+
throw new Error("previous resume action belongs to another terminal");
|
|
576
|
+
}
|
|
577
|
+
expectedBindingToken = requiredString(actionArguments?.expected_binding_token, "expected_binding_token from the previous resume action");
|
|
578
|
+
candidateToken = requiredString(actionArguments?.candidate_token, "candidate_token from the previous resume action");
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
if (parsed.selection?.kind !== "exact") {
|
|
582
|
+
throw new Error("resume selection could not be resolved");
|
|
583
|
+
}
|
|
584
|
+
nativeThreadId = parsed.selection.nativeThreadId;
|
|
585
|
+
const candidate = Array.isArray(discovery.threads)
|
|
586
|
+
? discovery.threads.find((thread) => isRecord(thread) &&
|
|
587
|
+
stringValue(thread.native_thread_id) === nativeThreadId)
|
|
588
|
+
: undefined;
|
|
589
|
+
if (!isRecord(candidate) || candidate.resumable !== true) {
|
|
590
|
+
throw new Error(`native thread ${nativeThreadId} is not resumable in the current lifecycle snapshot`);
|
|
591
|
+
}
|
|
592
|
+
candidateToken = requiredString(candidate.candidate_token, "candidate_token from the selected resumable thread");
|
|
593
|
+
}
|
|
594
|
+
mutationCommand = {
|
|
595
|
+
action: "resume-thread",
|
|
596
|
+
terminalId: parsed.terminalId,
|
|
597
|
+
selection: { kind: "exact", nativeThreadId }
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
const mutationArgs = buildAkkCommandCliArgs(mutationCommand, config, {
|
|
601
|
+
sessionKey: ctx.sessionKey,
|
|
602
|
+
expectedBindingToken,
|
|
603
|
+
candidateToken,
|
|
604
|
+
selectionScope
|
|
605
|
+
});
|
|
606
|
+
if (!mutationArgs) {
|
|
607
|
+
throw new Error("could not build native-thread lifecycle command");
|
|
608
|
+
}
|
|
609
|
+
const transitionResult = runCli(api, mutationArgs);
|
|
610
|
+
if (isAkkThreadTransitionSuccess(transitionResult)) {
|
|
611
|
+
displayedResumeSnapshots.delete(snapshotCacheKey);
|
|
612
|
+
}
|
|
613
|
+
return {
|
|
614
|
+
text: formatAkkThreadTransitionCommandResult(transitionResult),
|
|
615
|
+
isError: !isAkkThreadTransitionSuccess(transitionResult)
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
function formatDelegateCommandResult(result) {
|
|
619
|
+
const agent = executorDisplayName(result.agent);
|
|
620
|
+
const { sessionId, turnId } = publicTurnIdentity(result);
|
|
621
|
+
if (result.status === "submission_unfenced") {
|
|
622
|
+
return [
|
|
623
|
+
`AKK sent the terminal input to ${agent}, but could not bind later side effects to an exact native session.`,
|
|
624
|
+
`session: ${sessionId}`,
|
|
625
|
+
`turn: ${turnId}`,
|
|
626
|
+
"next: do not retry or continue automatically; inspect the named pane and close this Turn before sending more work."
|
|
627
|
+
].join("\n");
|
|
628
|
+
}
|
|
629
|
+
if (result.status === "submission_uncertain") {
|
|
630
|
+
return [
|
|
631
|
+
`AKK could not prove whether ${agent} received the terminal task.`,
|
|
632
|
+
`session: ${sessionId}`,
|
|
633
|
+
`turn: ${turnId}`,
|
|
634
|
+
"next: do not retry automatically; inspect AKK status and the named terminal pane."
|
|
635
|
+
].join("\n");
|
|
636
|
+
}
|
|
637
|
+
if (result.status === "submission_aborted") {
|
|
638
|
+
const safeToRetry = result.safe_to_retry === true && result.do_not_retry !== true;
|
|
639
|
+
return [
|
|
640
|
+
`AKK stopped before sending the terminal task to ${agent}.`,
|
|
641
|
+
`session: ${sessionId}`,
|
|
642
|
+
`turn: ${turnId}`,
|
|
643
|
+
safeToRetry
|
|
644
|
+
? "next: no terminal input was sent and the aborted receipt is durable, so this request may be retried."
|
|
645
|
+
: "next: do not retry automatically; AKK could not prove a durable safe abort, so inspect this Turn and its terminal dispatch ledger."
|
|
646
|
+
].join("\n");
|
|
647
|
+
}
|
|
648
|
+
if (result.status === "submission_pending_acceptance") {
|
|
649
|
+
return [
|
|
650
|
+
`AKK dispatched terminal input to ${agent}, but native acceptance is still pending.`,
|
|
651
|
+
`session: ${sessionId}`,
|
|
652
|
+
`turn: ${turnId}`,
|
|
653
|
+
"next: do not retry or report success; wait for acceptance or inspect the shared terminal pane."
|
|
654
|
+
].join("\n");
|
|
655
|
+
}
|
|
656
|
+
if (result.status === "submission_not_accepted") {
|
|
657
|
+
return [
|
|
658
|
+
`AKK proved that ${agent} did not accept the terminal draft.`,
|
|
659
|
+
`session: ${sessionId}`,
|
|
660
|
+
`turn: ${turnId}`,
|
|
661
|
+
"next: do not retry automatically; inspect the exact draft in the shared terminal pane."
|
|
662
|
+
].join("\n");
|
|
663
|
+
}
|
|
664
|
+
return [
|
|
665
|
+
`AKK sent the task to ${agent} in the shared terminal.`,
|
|
666
|
+
`session: ${sessionId}`,
|
|
667
|
+
`turn: ${turnId}`,
|
|
668
|
+
`status: ${result.conversation_status ?? result.status ?? "unknown"}`,
|
|
669
|
+
"The result will return to this OpenClaw session through the callback."
|
|
670
|
+
].join("\n");
|
|
671
|
+
}
|
|
672
|
+
function executorDisplayName(kind) {
|
|
673
|
+
try {
|
|
674
|
+
return executorDefinitionForKind(String(kind ?? "codex")).displayName;
|
|
675
|
+
}
|
|
676
|
+
catch {
|
|
677
|
+
return String(kind ?? "agent");
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
function formatStatusCommandResult(result) {
|
|
681
|
+
if (stringValue(result.watch_id) ||
|
|
682
|
+
(isRecord(result.watch) && stringValue(result.watch.watch_id)) ||
|
|
683
|
+
(isRecord(result.terminal_watch) &&
|
|
684
|
+
stringValue(result.terminal_watch.watch_id))) {
|
|
685
|
+
return formatAkkWatchStatusCommandResult(result);
|
|
686
|
+
}
|
|
687
|
+
const summary = result.summary ?? result.conversation ?? result ?? {};
|
|
688
|
+
const terminalStatus = isRecord(result.terminal_status) ? result.terminal_status : {};
|
|
689
|
+
const rawCallbackDelivery = isRecord(result.conversation?.callback_delivery)
|
|
690
|
+
? result.conversation.callback_delivery
|
|
691
|
+
: undefined;
|
|
692
|
+
const summarizedCallbackDelivery = isRecord(summary.callback_delivery)
|
|
693
|
+
? summary.callback_delivery
|
|
694
|
+
: undefined;
|
|
695
|
+
const callbackDelivery = rawCallbackDelivery || summarizedCallbackDelivery
|
|
696
|
+
? {
|
|
697
|
+
...(rawCallbackDelivery ?? {}),
|
|
698
|
+
...(summarizedCallbackDelivery ?? {})
|
|
699
|
+
}
|
|
700
|
+
: undefined;
|
|
701
|
+
const { sessionId, turnId } = publicTurnIdentity(result);
|
|
702
|
+
const lines = [
|
|
703
|
+
"AKK status:",
|
|
704
|
+
`session: ${sessionId}`,
|
|
705
|
+
`turn: ${turnId}`,
|
|
706
|
+
`agent: ${summary.agent ?? summary.executor?.kind ?? terminalStatus.agent ?? "unknown"}`,
|
|
707
|
+
`turn status: ${summary.status ?? result.conversation_status ?? result.status ?? "not managed"}`,
|
|
708
|
+
`terminal activity: ${terminalStatus.activity_state ?? "unavailable"}`,
|
|
709
|
+
...formatAkkTerminalWatchHint(result)
|
|
710
|
+
];
|
|
711
|
+
if (callbackDelivery) {
|
|
712
|
+
const callbackParts = [
|
|
713
|
+
String(callbackDelivery.status ?? "unknown"),
|
|
714
|
+
Number.isSafeInteger(Number(callbackDelivery.attempts))
|
|
715
|
+
? `attempt ${Number(callbackDelivery.attempts)}`
|
|
716
|
+
: undefined,
|
|
717
|
+
callbackDelivery.attempt_state === "in_flight"
|
|
718
|
+
? "in flight"
|
|
719
|
+
: undefined,
|
|
720
|
+
stringValue(callbackDelivery.next_attempt_at)
|
|
721
|
+
? `next retry ${stringValue(callbackDelivery.next_attempt_at)}`
|
|
722
|
+
: undefined
|
|
723
|
+
].filter(Boolean);
|
|
724
|
+
lines.push(`callback: ${callbackParts.join(", ")}`);
|
|
725
|
+
}
|
|
726
|
+
if (summary.request) {
|
|
727
|
+
lines.push(`request: ${truncateText(summary.request, 180)}`);
|
|
728
|
+
}
|
|
729
|
+
if (result.about) {
|
|
730
|
+
lines.push(`about: ${truncateText(result.about, 500)}`);
|
|
731
|
+
}
|
|
732
|
+
if (result.confidence) {
|
|
733
|
+
lines.push(`confidence: ${result.confidence}`);
|
|
734
|
+
}
|
|
735
|
+
const limitations = Array.isArray(result.limitations)
|
|
736
|
+
? result.limitations.filter(Boolean)
|
|
737
|
+
: [];
|
|
738
|
+
if (limitations.length > 0) {
|
|
739
|
+
lines.push(`limitations: ${limitations.slice(0, 3).join("; ")}`);
|
|
740
|
+
}
|
|
741
|
+
const screen = terminalScreenExcerpt(result);
|
|
742
|
+
if (screen) {
|
|
743
|
+
lines.push(`terminal screen:\n${screen}`);
|
|
744
|
+
}
|
|
745
|
+
return lines.join("\n");
|
|
746
|
+
}
|
|
747
|
+
function formatDoctorCommandResult(result) {
|
|
748
|
+
const capabilities = isRecord(result.capabilities) ? result.capabilities : {};
|
|
749
|
+
const tmux = isRecord(capabilities.tmux) ? capabilities.tmux : {};
|
|
750
|
+
const herdr = isRecord(capabilities.herdr) ? capabilities.herdr : {};
|
|
751
|
+
const dependencyChecks = Array.isArray(result.checks)
|
|
752
|
+
? result.checks.filter(isRecord)
|
|
753
|
+
: [];
|
|
754
|
+
const codingAgentLines = [
|
|
755
|
+
["Codex", "codex"],
|
|
756
|
+
["Claude Code", "claude"]
|
|
757
|
+
].map(([label, command]) => {
|
|
758
|
+
const check = dependencyChecks.find((entry) => entry.command === command);
|
|
759
|
+
const version = typeof check?.version === "string"
|
|
760
|
+
? check.version
|
|
761
|
+
: "unavailable";
|
|
762
|
+
const nativeProfile = check?.native_profile_supported === true &&
|
|
763
|
+
typeof check.native_profile === "string"
|
|
764
|
+
? `native profile ${check.native_profile}`
|
|
765
|
+
: check?.available === true
|
|
766
|
+
? "native lifecycle/status unverified"
|
|
767
|
+
: "unavailable";
|
|
768
|
+
return `${label}: ${version} (${nativeProfile})`;
|
|
769
|
+
});
|
|
770
|
+
const openclaw = isRecord(result.openclaw) ? result.openclaw : {};
|
|
771
|
+
const checks = Array.isArray(openclaw.checks) ? openclaw.checks : [];
|
|
772
|
+
const failures = checks
|
|
773
|
+
.filter((check) => isRecord(check) && check.ok !== true)
|
|
774
|
+
.map((check) => String(check.name ?? "unknown"));
|
|
775
|
+
const remediation = [...new Set(checks.flatMap((check) => isRecord(check) && Array.isArray(check.remediation)
|
|
776
|
+
? check.remediation.filter((command) => typeof command === "string" && command.trim().length > 0)
|
|
777
|
+
: []))].slice(0, 3);
|
|
778
|
+
return [
|
|
779
|
+
`AKK doctor: ${result.ok === true ? "ready" : "needs attention"}`,
|
|
780
|
+
`tmux: ${tmux.status ?? "unknown"}`,
|
|
781
|
+
`Herdr: ${herdr.status ?? "unknown"}`,
|
|
782
|
+
...codingAgentLines,
|
|
783
|
+
`OpenClaw package: ${openclaw.package_ready === true ? "ready" : "not ready"}`,
|
|
784
|
+
`Gateway: ${openclaw.gateway_ready === true ? "healthy" : "unavailable"}`,
|
|
785
|
+
...(failures.length > 0 ? [`check: ${failures.join(", ")}`] : []),
|
|
786
|
+
...remediation.map((command) => `next: ${command}`)
|
|
787
|
+
].join("\n");
|
|
788
|
+
}
|
|
789
|
+
function formatRenewCommandResult(result) {
|
|
790
|
+
const { sessionId, turnId } = publicTurnIdentity(result);
|
|
791
|
+
return [
|
|
792
|
+
"AKK monitoring renewed.",
|
|
793
|
+
`session: ${sessionId}`,
|
|
794
|
+
`turn: ${turnId}`,
|
|
795
|
+
`inactivity timeout: ${result.agent_timeout_minutes ?? "unknown"} minutes`,
|
|
796
|
+
`hard lifetime: ${result.agent_hard_timeout_minutes ?? "unknown"} minutes`,
|
|
797
|
+
"No message or key was sent to the coding agent."
|
|
798
|
+
].join("\n");
|
|
799
|
+
}
|
|
800
|
+
function formatRetryCallbackCommandResult(result) {
|
|
801
|
+
const { sessionId, turnId } = publicTurnIdentity(result);
|
|
802
|
+
return [
|
|
803
|
+
"AKK callback delivered.",
|
|
804
|
+
`session: ${sessionId}`,
|
|
805
|
+
`turn: ${turnId}`,
|
|
806
|
+
`status: ${result.conversation?.status ?? "unknown"}`,
|
|
807
|
+
`attempts: ${result.conversation?.callback_delivery?.attempts ?? "unknown"}`
|
|
808
|
+
].join("\n");
|
|
809
|
+
}
|
|
810
|
+
function formatSendCommandResult(result) {
|
|
811
|
+
const conversation = result.conversation ?? {};
|
|
812
|
+
const conversationId = conversation.conversation_id ?? result.conversation_id ?? "unknown";
|
|
813
|
+
const sessionId = conversation.session_id ?? result.session_id ?? conversationId;
|
|
814
|
+
const turnId = conversation.turn_id ?? result.turn_id ?? conversationId;
|
|
815
|
+
const status = conversation.status ?? result.status ?? "unknown";
|
|
816
|
+
const nextAction = isRecord(result.openclaw_next_action) ? result.openclaw_next_action : undefined;
|
|
817
|
+
if (result.status === "delivered_unfenced") {
|
|
818
|
+
return [
|
|
819
|
+
"AKK sent the terminal input but could not bind an exact native session.",
|
|
820
|
+
`session: ${sessionId}`,
|
|
821
|
+
`turn: ${turnId}`,
|
|
822
|
+
`status: ${status}`,
|
|
823
|
+
"next: do not retry or continue automatically; inspect the shared terminal pane and close this Turn before sending more work."
|
|
824
|
+
].join("\n");
|
|
825
|
+
}
|
|
826
|
+
if (result.submission_outcome === "uncertain") {
|
|
827
|
+
return [
|
|
828
|
+
"AKK terminal submission outcome is uncertain.",
|
|
829
|
+
`session: ${sessionId}`,
|
|
830
|
+
`turn: ${turnId}`,
|
|
831
|
+
`status: ${result.status ?? status}`,
|
|
832
|
+
"next: do not retry automatically; inspect AKK status and the named terminal pane."
|
|
833
|
+
].join("\n");
|
|
834
|
+
}
|
|
835
|
+
if (result.submission_outcome === "aborted") {
|
|
836
|
+
const safeToRetry = result.safe_to_retry === true && result.do_not_retry !== true;
|
|
837
|
+
return [
|
|
838
|
+
"AKK terminal submission was aborted before terminal input.",
|
|
839
|
+
`session: ${sessionId}`,
|
|
840
|
+
`turn: ${turnId}`,
|
|
841
|
+
`status: ${result.status ?? status}`,
|
|
842
|
+
safeToRetry
|
|
843
|
+
? "next: this request was not sent, the aborted receipt is durable, and it may be retried."
|
|
844
|
+
: "next: do not retry automatically; inspect the Turn because a durable safe abort was not proven."
|
|
845
|
+
].join("\n");
|
|
846
|
+
}
|
|
847
|
+
if (result.submission_outcome === "pending_acceptance") {
|
|
848
|
+
return [
|
|
849
|
+
"AKK dispatched the terminal input but native acceptance is still pending.",
|
|
850
|
+
`session: ${sessionId}`,
|
|
851
|
+
`turn: ${turnId}`,
|
|
852
|
+
`status: ${result.status ?? status}`,
|
|
853
|
+
"next: do not retry or report success; wait for acceptance or inspect the shared terminal pane."
|
|
854
|
+
].join("\n");
|
|
855
|
+
}
|
|
856
|
+
if (result.submission_outcome === "not_accepted") {
|
|
857
|
+
return [
|
|
858
|
+
"AKK proved that the agent did not accept the terminal draft.",
|
|
859
|
+
`session: ${sessionId}`,
|
|
860
|
+
`turn: ${turnId}`,
|
|
861
|
+
`status: ${result.status ?? status}`,
|
|
862
|
+
"next: do not retry automatically; inspect the exact draft in the shared terminal pane."
|
|
863
|
+
].join("\n");
|
|
864
|
+
}
|
|
865
|
+
if (terminalSubmissionReported(result) &&
|
|
866
|
+
!isAkkNativeSubmissionAccepted(result)) {
|
|
867
|
+
return [
|
|
868
|
+
"AKK could not verify native agent acceptance for this terminal input.",
|
|
869
|
+
`session: ${sessionId}`,
|
|
870
|
+
`turn: ${turnId}`,
|
|
871
|
+
`status: ${result.status ?? status}`,
|
|
872
|
+
"next: do not retry or report success; inspect the exact Turn receipt and shared terminal pane."
|
|
873
|
+
].join("\n");
|
|
874
|
+
}
|
|
875
|
+
const lines = [
|
|
876
|
+
"AKK turn sent.",
|
|
877
|
+
`session: ${sessionId}`,
|
|
878
|
+
`turn: ${turnId}`,
|
|
879
|
+
`status: ${status}`
|
|
880
|
+
];
|
|
881
|
+
if (result.source) {
|
|
882
|
+
lines.push(`source: ${result.source}`);
|
|
883
|
+
}
|
|
884
|
+
return [
|
|
885
|
+
...lines,
|
|
886
|
+
nextAction?.action === "yield"
|
|
887
|
+
? "next: yield now and wait for the AKK callback or an explicit status request."
|
|
888
|
+
: `launched: ${result.launched === true ? "yes" : "no"}`
|
|
889
|
+
].join("\n");
|
|
890
|
+
}
|
|
891
|
+
function formatCancelCommandResult(result) {
|
|
892
|
+
const conversation = result.conversation ?? {};
|
|
893
|
+
const { sessionId, turnId } = publicTurnIdentity(result);
|
|
894
|
+
return [
|
|
895
|
+
"AKK cancel requested.",
|
|
896
|
+
`session: ${sessionId}`,
|
|
897
|
+
`turn: ${turnId}`,
|
|
898
|
+
`agent: ${result.executor?.kind ?? conversation.executor?.kind ?? "unknown"}`,
|
|
899
|
+
`status: ${conversation.status ?? (result.cancel_requested === true ? "cancel requested" : "not cancelled")}`
|
|
900
|
+
].join("\n");
|
|
901
|
+
}
|
|
902
|
+
function formatApproveCommandResult(result) {
|
|
903
|
+
const conversation = result.conversation ?? {};
|
|
904
|
+
const { sessionId, turnId } = publicTurnIdentity(result);
|
|
905
|
+
return [
|
|
906
|
+
result.approved === true
|
|
907
|
+
? "AKK approved the current terminal request."
|
|
908
|
+
: "AKK did not approve the terminal request.",
|
|
909
|
+
`session: ${sessionId}`,
|
|
910
|
+
`turn: ${turnId}`,
|
|
911
|
+
`status: ${conversation.status ?? "unknown"}`,
|
|
912
|
+
...(result.reason
|
|
913
|
+
? [`reason: ${sanitizeAkkModelFacingDiagnosticText(String(result.reason))}`]
|
|
914
|
+
: [])
|
|
915
|
+
].join("\n");
|
|
916
|
+
}
|
|
917
|
+
function formatCloseCommandResult(result) {
|
|
918
|
+
if (result.source === "terminal_control") {
|
|
919
|
+
const terminalControl = isRecord(result.terminal_control)
|
|
920
|
+
? result.terminal_control
|
|
921
|
+
: {};
|
|
922
|
+
if (result.terminal_dispatch_resolved !== true) {
|
|
923
|
+
return [
|
|
924
|
+
"AKK did not clear the unresolved terminal dispatch fence.",
|
|
925
|
+
`terminal: ${terminalControl.target ?? "unknown"}`,
|
|
926
|
+
...(stringValue(result.transition_id)
|
|
927
|
+
? [`transition: ${stringValue(result.transition_id)}`]
|
|
928
|
+
: []),
|
|
929
|
+
`reason: ${sanitizeAkkModelFacingDiagnosticText(stringValue(result.reason) ??
|
|
930
|
+
"the recorded lifecycle outcome could not be verified")}`,
|
|
931
|
+
"This terminal remains blocked. Do not retry or continue automatically; inspect the pane and use the fresh recovery action from /akk list."
|
|
932
|
+
].join("\n");
|
|
933
|
+
}
|
|
934
|
+
return [
|
|
935
|
+
"AKK cleared the unresolved terminal dispatch fence.",
|
|
936
|
+
`terminal: ${terminalControl.target ?? "unknown"}`,
|
|
937
|
+
...(stringValue(result.transition_id)
|
|
938
|
+
? [`transition: ${stringValue(result.transition_id)}`]
|
|
939
|
+
: [
|
|
940
|
+
`previous turn: ${result.owner_turn_id ?? result.owner_conversation_id ?? "unknown"}`
|
|
941
|
+
]),
|
|
942
|
+
"The coding agent and terminal pane remain open."
|
|
943
|
+
].join("\n");
|
|
944
|
+
}
|
|
945
|
+
const conversation = result.conversation ?? {};
|
|
946
|
+
const { sessionId, turnId } = publicTurnIdentity(result);
|
|
947
|
+
return [
|
|
948
|
+
"AKK Turn record closed.",
|
|
949
|
+
`session: ${sessionId}`,
|
|
950
|
+
`turn: ${turnId}`,
|
|
951
|
+
`status: ${conversation.status ?? "unknown"}`
|
|
952
|
+
].join("\n");
|
|
953
|
+
}
|
|
954
|
+
function publicTurnIdentity(result) {
|
|
955
|
+
const conversation = isRecord(result.conversation) ? result.conversation : {};
|
|
956
|
+
const summary = isRecord(result.summary) ? result.summary : {};
|
|
957
|
+
const compatibilityId = stringValue(conversation.conversation_id) ??
|
|
958
|
+
stringValue(summary.conversation_id) ??
|
|
959
|
+
stringValue(result.conversation_id);
|
|
960
|
+
return {
|
|
961
|
+
sessionId: stringValue(conversation.session_id) ??
|
|
962
|
+
stringValue(summary.session_id) ??
|
|
963
|
+
stringValue(result.session_id) ??
|
|
964
|
+
compatibilityId ??
|
|
965
|
+
"unknown",
|
|
966
|
+
turnId: stringValue(conversation.turn_id) ??
|
|
967
|
+
stringValue(summary.turn_id) ??
|
|
968
|
+
stringValue(result.turn_id) ??
|
|
969
|
+
compatibilityId ??
|
|
970
|
+
"unknown"
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
function buildStatusCliArgs(api, params) {
|
|
974
|
+
const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
|
|
975
|
+
if (Object.hasOwn(params, "watch_id")) {
|
|
976
|
+
if (Object.hasOwn(params, "turn_id") ||
|
|
977
|
+
Object.hasOwn(params, "conversation_id")) {
|
|
978
|
+
throw new Error("status accepts exactly one of turn_id, conversation_id, or watch_id");
|
|
979
|
+
}
|
|
980
|
+
const watchId = requiredString(params.watch_id, "watch_id");
|
|
981
|
+
const watchArgs = [
|
|
982
|
+
"watch-status",
|
|
983
|
+
"--watch",
|
|
984
|
+
watchId
|
|
985
|
+
];
|
|
986
|
+
pushOptional(watchArgs, "--store-dir", resolvePluginStoreDir(config));
|
|
987
|
+
return watchArgs;
|
|
988
|
+
}
|
|
989
|
+
const args = [
|
|
990
|
+
"status",
|
|
991
|
+
"--reconcile"
|
|
992
|
+
];
|
|
993
|
+
pushTurnTarget(args, params);
|
|
994
|
+
pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
|
|
995
|
+
pushOptional(args, "--idle-timeout-minutes", numberString(params.idleTimeoutMinutes) ??
|
|
996
|
+
numberString(api.pluginConfig?.idleTimeoutMinutes));
|
|
997
|
+
if (params.trace === true) {
|
|
998
|
+
args.push("--trace");
|
|
999
|
+
}
|
|
1000
|
+
return args;
|
|
1001
|
+
}
|
|
1002
|
+
function terminalScreenExcerpt(result) {
|
|
1003
|
+
const screen = result.terminal_screen;
|
|
1004
|
+
const text = typeof screen === "string"
|
|
1005
|
+
? screen
|
|
1006
|
+
: isRecord(screen)
|
|
1007
|
+
? stringValue(screen.excerpt) ??
|
|
1008
|
+
stringValue(screen.text) ??
|
|
1009
|
+
stringValue(screen.content)
|
|
1010
|
+
: undefined;
|
|
1011
|
+
if (!text) {
|
|
1012
|
+
return undefined;
|
|
1013
|
+
}
|
|
1014
|
+
return text.length <= 1600
|
|
1015
|
+
? text
|
|
1016
|
+
: `…${text.slice(-1599)}`;
|
|
1017
|
+
}
|
|
1018
|
+
async function runSendRequest(api, params, toolContext, messageId) {
|
|
1019
|
+
const requestedType = Object.hasOwn(params, "type")
|
|
1020
|
+
? stringValue(params.type)
|
|
1021
|
+
: "task";
|
|
1022
|
+
if (requestedType !== "task") {
|
|
1023
|
+
throw new Error("ordinary send type must be task; use agent_knock_knock_respond for an in-flight response");
|
|
1024
|
+
}
|
|
1025
|
+
if (Object.hasOwn(params, "session_id") && Object.hasOwn(params, "terminal_id")) {
|
|
1026
|
+
throw new Error("ordinary send accepts only one of session_id or terminal_id");
|
|
1027
|
+
}
|
|
1028
|
+
const sessionId = Object.hasOwn(params, "session_id")
|
|
1029
|
+
? authoritativeManagedId(params.session_id, "session_id")
|
|
1030
|
+
: undefined;
|
|
1031
|
+
const terminalId = Object.hasOwn(params, "terminal_id")
|
|
1032
|
+
? requiredString(params.terminal_id, "terminal_id")
|
|
1033
|
+
: undefined;
|
|
1034
|
+
if (terminalId && !/^terminal:v[0-9]+:\S+$/u.test(terminalId)) {
|
|
1035
|
+
throw new Error("terminal_id must be the exact full terminal identifier returned by AKK list");
|
|
1036
|
+
}
|
|
1037
|
+
if (!sessionId && !terminalId) {
|
|
1038
|
+
return runDelegate(api, { ...params, messageId }, toolContext);
|
|
1039
|
+
}
|
|
1040
|
+
const terminalAction = terminalId
|
|
1041
|
+
? privateActionArguments(api, {
|
|
1042
|
+
tool: "agent_knock_knock_send",
|
|
1043
|
+
terminalId,
|
|
1044
|
+
matches: (argumentsValue) => stringValue(argumentsValue.selector) === terminalId ||
|
|
1045
|
+
stringValue(argumentsValue.terminal_id) === terminalId
|
|
1046
|
+
})
|
|
1047
|
+
: undefined;
|
|
1048
|
+
const expectedTerminalToken = terminalAction
|
|
1049
|
+
? requiredString(terminalAction.expected_terminal_token, "current internal terminal send authority")
|
|
1050
|
+
: undefined;
|
|
1051
|
+
const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
|
|
1052
|
+
const openclawSession = stringValue(toolContext?.sessionKey) ??
|
|
1053
|
+
"agent:main:main";
|
|
1054
|
+
const args = [
|
|
1055
|
+
"send"
|
|
1056
|
+
];
|
|
1057
|
+
if (sessionId) {
|
|
1058
|
+
args.push("--session", sessionId);
|
|
1059
|
+
}
|
|
1060
|
+
else {
|
|
1061
|
+
args.push("--conversation", requiredString(terminalId, "terminal_id"));
|
|
1062
|
+
}
|
|
1063
|
+
pushOptional(args, "--expected-terminal-token", expectedTerminalToken);
|
|
1064
|
+
args.push("--message", requiredString(params.request, "request"), "--background");
|
|
1065
|
+
pushOptional(args, "--type", stringValue(params.type));
|
|
1066
|
+
pushOptional(args, "--message-id", messageId);
|
|
1067
|
+
pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
|
|
1068
|
+
pushOptional(args, "--idle-timeout-minutes", numberString(params.idleTimeoutMinutes) ??
|
|
1069
|
+
numberString(config.idleTimeoutMinutes));
|
|
1070
|
+
pushOptional(args, "--agent-timeout-minutes", numberString(params.agentTimeoutMinutes) ??
|
|
1071
|
+
numberString(config.agentTimeoutMinutes));
|
|
1072
|
+
pushOptional(args, "--agent-hard-timeout-minutes", numberString(params.agentHardTimeoutMinutes) ??
|
|
1073
|
+
numberString(config.agentHardTimeoutMinutes));
|
|
1074
|
+
pushOptional(args, "--openclaw-session", openclawSession);
|
|
1075
|
+
pushOptional(args, "--gateway-method", CALLBACK_METHOD);
|
|
1076
|
+
pushOptional(args, "--gateway-session", openclawSession);
|
|
1077
|
+
pushOptional(args, "--openclaw-bin", stringValue(config.openclawBin));
|
|
1078
|
+
return runCli(api, args);
|
|
1079
|
+
}
|
|
1080
|
+
function toolResult(result, { submissionErrors = false, normalizeTurnIdentity = true, forceError = false } = {}) {
|
|
1081
|
+
const normalized = normalizeTurnIdentity ? withTurnIdentity(result) : result;
|
|
1082
|
+
const modelFacing = sanitizeModelFacingValue(normalized);
|
|
1083
|
+
const submissionError = submissionErrors && isSubmissionError(normalized);
|
|
1084
|
+
return {
|
|
1085
|
+
content: [
|
|
1086
|
+
{
|
|
1087
|
+
type: "text",
|
|
1088
|
+
text: JSON.stringify(modelFacing, null, 2)
|
|
1089
|
+
}
|
|
1090
|
+
],
|
|
1091
|
+
details: modelFacing,
|
|
1092
|
+
...(submissionError || forceError ? { isError: true } : {})
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
function privateList(api) {
|
|
1096
|
+
const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
|
|
1097
|
+
const args = ["list", "--reconcile"];
|
|
1098
|
+
pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
|
|
1099
|
+
pushOptional(args, "--idle-timeout-minutes", numberString(config.idleTimeoutMinutes));
|
|
1100
|
+
return runCli(api, args);
|
|
1101
|
+
}
|
|
1102
|
+
function privateThreadDiscovery(api, terminalId) {
|
|
1103
|
+
const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
|
|
1104
|
+
const args = [
|
|
1105
|
+
"list-resumable-threads",
|
|
1106
|
+
"--terminal",
|
|
1107
|
+
terminalId
|
|
1108
|
+
];
|
|
1109
|
+
pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
|
|
1110
|
+
pushOptional(args, "--codex-home", stringValue(config.codexHome));
|
|
1111
|
+
return runCli(api, args);
|
|
1112
|
+
}
|
|
1113
|
+
function privateTerminalActionArguments(api, terminalId, tool) {
|
|
1114
|
+
return privateActionArguments(api, {
|
|
1115
|
+
tool,
|
|
1116
|
+
terminalId,
|
|
1117
|
+
matches: (argumentsValue) => stringValue(argumentsValue.terminal_id) === terminalId
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
function privateActionArguments(api, input) {
|
|
1121
|
+
const result = privateList(api);
|
|
1122
|
+
const terminals = input.terminalId
|
|
1123
|
+
? terminalRows(result).filter((terminal) => stringValue(terminal.id) === input.terminalId)
|
|
1124
|
+
: terminalRows(result);
|
|
1125
|
+
if (input.terminalId && terminals.length !== 1) {
|
|
1126
|
+
throw new Error(`terminal ${input.terminalId} is not uniquely present in the current AKK list`);
|
|
1127
|
+
}
|
|
1128
|
+
const candidates = (input.tool === "agent_knock_knock_close"
|
|
1129
|
+
? authoritativeHandoffActionArguments(terminals)
|
|
1130
|
+
: terminals.flatMap((terminal) => authoritativeTerminalActionArguments(terminal, input.tool))).filter(input.matches);
|
|
1131
|
+
const uniqueCandidates = [...new Map(candidates.map((candidate) => [
|
|
1132
|
+
JSON.stringify(candidate),
|
|
1133
|
+
candidate
|
|
1134
|
+
])).values()];
|
|
1135
|
+
if (uniqueCandidates.length !== 1) {
|
|
1136
|
+
throw new Error(`the current AKK list does not advertise one exact ${input.tool} action for the requested semantic target`);
|
|
1137
|
+
}
|
|
1138
|
+
return uniqueCandidates[0];
|
|
1139
|
+
}
|
|
1140
|
+
function rememberDisplayedPrivateAuthorityOffers(api, sessionKeyValue, sessionIdValue, result) {
|
|
1141
|
+
const sessionKey = stringValue(sessionKeyValue);
|
|
1142
|
+
const sessionId = stringValue(sessionIdValue);
|
|
1143
|
+
if (!sessionKey || !sessionId || !isRecord(result))
|
|
1144
|
+
return;
|
|
1145
|
+
rememberDisplayedHandoffActions(api, sessionKey, sessionId, result);
|
|
1146
|
+
rememberDisplayedReconcileActions(api, sessionKey, sessionId, result);
|
|
1147
|
+
}
|
|
1148
|
+
function rememberDisplayedHandoffActions(api, sessionKey, sessionId, result) {
|
|
1149
|
+
for (const args of uniqueArgumentObjects(authoritativeHandoffActionArguments(terminalRows(result)))) {
|
|
1150
|
+
const turnId = stringValue(args.turn_id ?? args.conversation_id);
|
|
1151
|
+
if (!turnId ||
|
|
1152
|
+
stringValue(args.reason) !== "superseded_by_human_context_switch" ||
|
|
1153
|
+
!stringValue(args.expected_handoff_token)) {
|
|
1154
|
+
continue;
|
|
1155
|
+
}
|
|
1156
|
+
rememberOpenClawPrivateAuthorityOffer(api, privateAuthorityOfferKey(sessionKey, sessionId, OPENCLAW_HANDOFF_AUTHORITY_KIND, { type: "turn_id", id: turnId }), { args });
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
function rememberDisplayedReconcileActions(api, sessionKey, sessionId, result) {
|
|
1160
|
+
for (const args of uniqueArgumentObjects(terminalRows(result).flatMap((terminal) => authoritativeTerminalActionArguments(terminal, "agent_knock_knock_reconcile_binding")))) {
|
|
1161
|
+
const terminalId = stringValue(args.terminal_id);
|
|
1162
|
+
const conflictingSessionId = stringValue(args.conflicting_session_id);
|
|
1163
|
+
if (!terminalId || !conflictingSessionId)
|
|
1164
|
+
continue;
|
|
1165
|
+
rememberOpenClawPrivateAuthorityOffer(api, privateAuthorityOfferKey(sessionKey, sessionId, OPENCLAW_RECONCILE_BINDING_AUTHORITY_KIND, { type: "terminal_id", id: terminalId }), { args });
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
function rememberDisplayedApprovalOffer(api, sessionKeyValue, sessionIdValue, result) {
|
|
1169
|
+
const sessionKey = stringValue(sessionKeyValue);
|
|
1170
|
+
const sessionId = stringValue(sessionIdValue);
|
|
1171
|
+
if (!sessionKey || !sessionId || !isRecord(result))
|
|
1172
|
+
return;
|
|
1173
|
+
const fingerprints = currentApprovalFingerprints(result);
|
|
1174
|
+
if (fingerprints.length !== 1)
|
|
1175
|
+
return;
|
|
1176
|
+
const target = approvalTargetFromStatus(result);
|
|
1177
|
+
if (!target)
|
|
1178
|
+
return;
|
|
1179
|
+
rememberOpenClawPrivateAuthorityOffer(api, openClawApprovalAuthorityOfferKey(sessionKey, sessionId, target), { fingerprint: fingerprints[0] });
|
|
1180
|
+
}
|
|
1181
|
+
function approvalTargetFromStatus(result) {
|
|
1182
|
+
const conversationId = stringValue(result.conversation_id);
|
|
1183
|
+
if (stringValue(result.source) === "terminal_control" ||
|
|
1184
|
+
conversationId?.startsWith("terminal:")) {
|
|
1185
|
+
return conversationId
|
|
1186
|
+
? { type: "terminal_id", id: conversationId }
|
|
1187
|
+
: undefined;
|
|
1188
|
+
}
|
|
1189
|
+
const { turnId } = publicTurnIdentity(result);
|
|
1190
|
+
return turnId && turnId !== "unknown"
|
|
1191
|
+
? { type: "turn_id", id: turnId }
|
|
1192
|
+
: undefined;
|
|
1193
|
+
}
|
|
1194
|
+
function uniqueArgumentObjects(candidates) {
|
|
1195
|
+
return [...new Map(candidates.map((args) => [
|
|
1196
|
+
JSON.stringify(args),
|
|
1197
|
+
args
|
|
1198
|
+
])).values()];
|
|
1199
|
+
}
|
|
1200
|
+
function privateAuthorityOfferKey(sessionKey, sessionId, kind, target) {
|
|
1201
|
+
return { sessionKey, sessionId, kind, target };
|
|
1202
|
+
}
|
|
1203
|
+
function consumeDisplayedPrivateAction(api, input) {
|
|
1204
|
+
const offered = consumeOpenClawPrivateAuthorityOffer(api, {
|
|
1205
|
+
sessionKey: input.sessionKey,
|
|
1206
|
+
sessionId: input.sessionId,
|
|
1207
|
+
kind: input.kind,
|
|
1208
|
+
target: input.target
|
|
1209
|
+
});
|
|
1210
|
+
if (!offered || !isRecord(offered.args)) {
|
|
1211
|
+
throw new Error(`${input.tool} requires a current action shown by AKK list in this OpenClaw session; refresh list, review it, and explicitly confirm again`);
|
|
1212
|
+
}
|
|
1213
|
+
const current = privateActionArguments(api, input);
|
|
1214
|
+
if (JSON.stringify(current) !== JSON.stringify(offered.args)) {
|
|
1215
|
+
throw new Error(`${input.tool} authority changed after it was shown; refresh AKK list, review the current action, and explicitly confirm again`);
|
|
1216
|
+
}
|
|
1217
|
+
return current;
|
|
1218
|
+
}
|
|
1219
|
+
function terminalRows(result) {
|
|
1220
|
+
return Array.isArray(result.terminals)
|
|
1221
|
+
? result.terminals.filter(isRecord)
|
|
1222
|
+
: [];
|
|
1223
|
+
}
|
|
1224
|
+
function authoritativeTerminalActionArguments(terminal, tool) {
|
|
1225
|
+
const availableActions = isRecord(terminal.available_actions)
|
|
1226
|
+
? terminal.available_actions
|
|
1227
|
+
: undefined;
|
|
1228
|
+
if (!availableActions)
|
|
1229
|
+
return [];
|
|
1230
|
+
return Object.values(availableActions).flatMap((action) => isRecord(action) && action.tool === tool && isRecord(action.arguments)
|
|
1231
|
+
? [action.arguments]
|
|
1232
|
+
: []);
|
|
1233
|
+
}
|
|
1234
|
+
function authoritativeHandoffActionArguments(terminals) {
|
|
1235
|
+
return terminals.flatMap((terminal) => {
|
|
1236
|
+
const decision = isRecord(terminal.handoff_decision)
|
|
1237
|
+
? terminal.handoff_decision
|
|
1238
|
+
: undefined;
|
|
1239
|
+
const choices = isRecord(decision?.choices) ? decision.choices : undefined;
|
|
1240
|
+
const takeOver = isRecord(choices?.take_over_current)
|
|
1241
|
+
? choices.take_over_current
|
|
1242
|
+
: undefined;
|
|
1243
|
+
const action = isRecord(takeOver?.action) ? takeOver.action : undefined;
|
|
1244
|
+
return decision?.kind === "active_turn_requires_decision" &&
|
|
1245
|
+
action?.tool === "agent_knock_knock_close" &&
|
|
1246
|
+
action.requires_explicit_user_confirmation === true &&
|
|
1247
|
+
isRecord(action.arguments)
|
|
1248
|
+
? [action.arguments]
|
|
1249
|
+
: [];
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
function currentApprovalFingerprint(result) {
|
|
1253
|
+
const unique = isRecord(result) ? currentApprovalFingerprints(result) : [];
|
|
1254
|
+
if (unique.length !== 1) {
|
|
1255
|
+
throw new Error("the current status does not contain one exact approvable prompt; refresh status and ask the user to review it again");
|
|
1256
|
+
}
|
|
1257
|
+
return unique[0];
|
|
1258
|
+
}
|
|
1259
|
+
function buildPrivateApprovalArgs(api, params, { sessionKey, sessionId }) {
|
|
1260
|
+
const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
|
|
1261
|
+
const turnId = stringValue(params.turn_id);
|
|
1262
|
+
const terminalId = stringValue(params.terminal_id);
|
|
1263
|
+
if (Boolean(turnId) === Boolean(terminalId)) {
|
|
1264
|
+
throw new Error("approve requires exactly one of turn_id or terminal_id");
|
|
1265
|
+
}
|
|
1266
|
+
const target = terminalId
|
|
1267
|
+
? { type: "terminal_id", id: terminalId }
|
|
1268
|
+
: { type: "turn_id", id: requiredString(turnId, "turn_id") };
|
|
1269
|
+
const offered = consumeOpenClawPrivateAuthorityOffer(api, openClawApprovalAuthorityOfferKey(sessionKey, sessionId, target));
|
|
1270
|
+
const offeredFingerprint = stringValue(offered?.fingerprint) ??
|
|
1271
|
+
(isRecord(offered?.args)
|
|
1272
|
+
? stringValue(offered.args.expected_approval_fingerprint)
|
|
1273
|
+
: undefined);
|
|
1274
|
+
if (!isExactApprovalFingerprint(offeredFingerprint)) {
|
|
1275
|
+
throw new Error("approve requires a current approval request shown by agent_knock_knock_status in this OpenClaw conversation; refresh status, ask the user to review it, and explicitly confirm again");
|
|
1276
|
+
}
|
|
1277
|
+
const action = terminalId
|
|
1278
|
+
? privateActionArguments(api, {
|
|
1279
|
+
tool: "agent_knock_knock_approve",
|
|
1280
|
+
terminalId,
|
|
1281
|
+
matches: (argumentsValue) => stringValue(argumentsValue.terminal_id ?? argumentsValue.conversation_id) === terminalId
|
|
1282
|
+
})
|
|
1283
|
+
: undefined;
|
|
1284
|
+
const statusArgs = ["status", "--reconcile"];
|
|
1285
|
+
if (terminalId) {
|
|
1286
|
+
statusArgs.push("--conversation", terminalId);
|
|
1287
|
+
}
|
|
1288
|
+
else {
|
|
1289
|
+
statusArgs.push("--turn", requiredString(turnId, "turn_id"));
|
|
1290
|
+
}
|
|
1291
|
+
pushOptional(statusArgs, "--store-dir", resolvePluginStoreDir(config));
|
|
1292
|
+
const args = ["approve"];
|
|
1293
|
+
if (terminalId) {
|
|
1294
|
+
args.push("--conversation", terminalId);
|
|
1295
|
+
}
|
|
1296
|
+
else {
|
|
1297
|
+
args.push("--turn", requiredString(turnId, "turn_id"));
|
|
1298
|
+
}
|
|
1299
|
+
const currentFingerprint = currentApprovalFingerprint(runCli(api, statusArgs));
|
|
1300
|
+
if (currentFingerprint !== offeredFingerprint) {
|
|
1301
|
+
throw new Error("the approval request changed after it was shown; refresh AKK status, ask the user to review the current request, and explicitly confirm again");
|
|
1302
|
+
}
|
|
1303
|
+
args.push("--expected-approval-fingerprint", currentFingerprint);
|
|
1304
|
+
if (terminalId) {
|
|
1305
|
+
args.push("--expected-terminal-token", requiredString(action?.expected_terminal_token, "current internal terminal approval authority"));
|
|
1306
|
+
}
|
|
1307
|
+
pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
|
|
1308
|
+
return args;
|
|
1309
|
+
}
|
|
1310
|
+
function isExactApprovalFingerprint(value) {
|
|
1311
|
+
return typeof value === "string" && /^[a-f0-9]{64}$/u.test(value);
|
|
1312
|
+
}
|
|
1313
|
+
function currentApprovalFingerprints(result) {
|
|
1314
|
+
const terminalStatus = isRecord(result.terminal_status)
|
|
1315
|
+
? result.terminal_status
|
|
1316
|
+
: undefined;
|
|
1317
|
+
const states = [
|
|
1318
|
+
isRecord(result.approval_state) ? result.approval_state : undefined,
|
|
1319
|
+
isRecord(terminalStatus?.approval_state)
|
|
1320
|
+
? terminalStatus.approval_state
|
|
1321
|
+
: undefined
|
|
1322
|
+
];
|
|
1323
|
+
return [...new Set(states.flatMap((state) => {
|
|
1324
|
+
const fingerprint = state?.approvable === true
|
|
1325
|
+
? stringValue(state.fingerprint)
|
|
1326
|
+
: undefined;
|
|
1327
|
+
return fingerprint && /^[a-f0-9]{64}$/u.test(fingerprint)
|
|
1328
|
+
? [fingerprint]
|
|
1329
|
+
: [];
|
|
1330
|
+
}))];
|
|
1331
|
+
}
|
|
1332
|
+
function sanitizeModelFacingValue(value, parentKey, path = []) {
|
|
1333
|
+
if (Array.isArray(value)) {
|
|
1334
|
+
if (parentKey === "missing_required") {
|
|
1335
|
+
return value.filter((item) => typeof item !== "string" ||
|
|
1336
|
+
!isAkkModelFacingPrivateAuthorityField(item));
|
|
1337
|
+
}
|
|
1338
|
+
return value.map((item, index) => sanitizeModelFacingValue(item, parentKey, [...path, String(index)]));
|
|
1339
|
+
}
|
|
1340
|
+
if (!isRecord(value)) {
|
|
1341
|
+
if (typeof value !== "string") {
|
|
1342
|
+
return value;
|
|
1343
|
+
}
|
|
1344
|
+
if (isAkkModelFacingDiagnosticField(parentKey)) {
|
|
1345
|
+
return sanitizeAkkModelFacingDiagnosticText(value);
|
|
1346
|
+
}
|
|
1347
|
+
return isLegacyAuthorityInstructionPath(path)
|
|
1348
|
+
? sanitizeAkkModelFacingLegacyAuthorityInstructionText(value)
|
|
1349
|
+
: value;
|
|
1350
|
+
}
|
|
1351
|
+
const output = {};
|
|
1352
|
+
for (const [key, item] of Object.entries(value)) {
|
|
1353
|
+
if (isAkkModelFacingPrivateAuthorityField(key) ||
|
|
1354
|
+
key === "selection_snapshot" ||
|
|
1355
|
+
key === "live_native_thread_id" ||
|
|
1356
|
+
(key === "fingerprint" && parentKey === "approval_state")) {
|
|
1357
|
+
continue;
|
|
1358
|
+
}
|
|
1359
|
+
output[key] = sanitizeModelFacingValue(item, key, [...path, key]);
|
|
1360
|
+
}
|
|
1361
|
+
if (output.tool === "agent_knock_knock_send" && isRecord(output.arguments)) {
|
|
1362
|
+
const selector = stringValue(output.arguments.selector);
|
|
1363
|
+
if (selector && /^terminal:v[0-9]+:\S+$/u.test(selector)) {
|
|
1364
|
+
const { selector: _selector, ...argumentsValue } = output.arguments;
|
|
1365
|
+
output.arguments = { ...argumentsValue, terminal_id: selector };
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
if (output.tool === "agent_knock_knock_approve" && isRecord(output.arguments)) {
|
|
1369
|
+
const conversationId = stringValue(output.arguments.conversation_id);
|
|
1370
|
+
if (conversationId && /^terminal:v[0-9]+:\S+$/u.test(conversationId)) {
|
|
1371
|
+
const { conversation_id: _conversationId, ...argumentsValue } = output.arguments;
|
|
1372
|
+
output.arguments = { ...argumentsValue, terminal_id: conversationId };
|
|
1373
|
+
}
|
|
1374
|
+
const approvalArguments = isRecord(output.arguments)
|
|
1375
|
+
? output.arguments
|
|
1376
|
+
: {};
|
|
1377
|
+
const terminalId = stringValue(approvalArguments.terminal_id);
|
|
1378
|
+
output.before_call = {
|
|
1379
|
+
tool: "agent_knock_knock_status",
|
|
1380
|
+
arguments: terminalId
|
|
1381
|
+
? { conversation_id: terminalId }
|
|
1382
|
+
: { ...approvalArguments },
|
|
1383
|
+
use: "Show the current approval request to the user. After explicit confirmation, call approve with only this semantic target; AKK revalidates the prompt privately."
|
|
1384
|
+
};
|
|
1385
|
+
delete output.missing_required;
|
|
1386
|
+
}
|
|
1387
|
+
return output;
|
|
1388
|
+
}
|
|
1389
|
+
function isLegacyAuthorityInstructionPath(path) {
|
|
1390
|
+
const normalized = path.map(normalizeAkkModelFacingFieldName);
|
|
1391
|
+
if (normalized.at(-1) !== "body")
|
|
1392
|
+
return false;
|
|
1393
|
+
const callbackMessageBody = normalized.slice(-3).join(".") ===
|
|
1394
|
+
"callbackdelivery.message.body";
|
|
1395
|
+
return callbackMessageBody || normalized.includes("recentevents");
|
|
1396
|
+
}
|
|
1397
|
+
function modelFacingToolError(error) {
|
|
1398
|
+
return new Error(modelFacingErrorMessage(error));
|
|
1399
|
+
}
|
|
1400
|
+
function modelFacingErrorMessage(error) {
|
|
1401
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1402
|
+
return sanitizeAkkModelFacingDiagnosticText(message);
|
|
1403
|
+
}
|
|
1404
|
+
function isBlockedTerminalDispatchResult(result) {
|
|
1405
|
+
return Boolean(isRecord(result) &&
|
|
1406
|
+
result.source === "terminal_control" &&
|
|
1407
|
+
result.terminal_dispatch_resolved !== true);
|
|
1408
|
+
}
|
|
1409
|
+
function isSubmissionError(result) {
|
|
1410
|
+
if (!isRecord(result)) {
|
|
1411
|
+
return false;
|
|
1412
|
+
}
|
|
1413
|
+
if (terminalSubmissionReported(result)) {
|
|
1414
|
+
return !isAkkNativeSubmissionAccepted(result);
|
|
1415
|
+
}
|
|
1416
|
+
return [
|
|
1417
|
+
"submission_unfenced",
|
|
1418
|
+
"submission_uncertain",
|
|
1419
|
+
"submission_aborted",
|
|
1420
|
+
"submission_pending_acceptance",
|
|
1421
|
+
"submission_not_accepted"
|
|
1422
|
+
].includes(String(result.status ?? "")) ||
|
|
1423
|
+
["uncertain", "aborted", "pending_acceptance", "not_accepted"].includes(String(result.submission_outcome ?? "")) ||
|
|
1424
|
+
result.status === "delivered_unfenced";
|
|
1425
|
+
}
|
|
1426
|
+
function terminalSubmissionReported(result) {
|
|
1427
|
+
return result.submission_outcome !== undefined ||
|
|
1428
|
+
result.delivery_receipt !== undefined ||
|
|
1429
|
+
result.delivered !== undefined;
|
|
1430
|
+
}
|
|
1431
|
+
function withTurnIdentity(result) {
|
|
1432
|
+
if (!isRecord(result)) {
|
|
1433
|
+
return result;
|
|
1434
|
+
}
|
|
1435
|
+
const sources = [
|
|
1436
|
+
{ label: "result", value: result },
|
|
1437
|
+
{ label: "result.conversation", value: result.conversation },
|
|
1438
|
+
{ label: "result.summary", value: result.summary },
|
|
1439
|
+
{ label: "result.message", value: result.message }
|
|
1440
|
+
];
|
|
1441
|
+
const compatibilityId = consistentResultIdentity("conversation_id", sources);
|
|
1442
|
+
const explicitSessionId = consistentResultIdentity("session_id", sources);
|
|
1443
|
+
const explicitTurnId = consistentResultIdentity("turn_id", sources);
|
|
1444
|
+
const hasModernIdentity = Boolean(explicitSessionId || explicitTurnId);
|
|
1445
|
+
if (hasModernIdentity && (!explicitSessionId || !explicitTurnId)) {
|
|
1446
|
+
throw new Error("agent-knock-knock CLI returned a partial session_id/turn_id identity");
|
|
1447
|
+
}
|
|
1448
|
+
if (hasModernIdentity && !compatibilityId) {
|
|
1449
|
+
throw new Error("agent-knock-knock CLI returned modern identity without conversation_id");
|
|
1450
|
+
}
|
|
1451
|
+
if (hasModernIdentity && compatibilityId !== explicitTurnId) {
|
|
1452
|
+
throw new Error("agent-knock-knock CLI returned conversation_id that differs from turn_id");
|
|
1453
|
+
}
|
|
1454
|
+
if (!hasModernIdentity && !compatibilityId) {
|
|
1455
|
+
return result;
|
|
1456
|
+
}
|
|
1457
|
+
if (!hasModernIdentity &&
|
|
1458
|
+
(compatibilityId?.startsWith("terminal:") ||
|
|
1459
|
+
stringValue(result.source) === "terminal" ||
|
|
1460
|
+
(isRecord(result.summary) &&
|
|
1461
|
+
stringValue(result.summary.source) === "terminal"))) {
|
|
1462
|
+
return result;
|
|
1463
|
+
}
|
|
1464
|
+
const sessionId = explicitSessionId ?? compatibilityId;
|
|
1465
|
+
const turnId = explicitTurnId ?? compatibilityId;
|
|
1466
|
+
return {
|
|
1467
|
+
...result,
|
|
1468
|
+
session_id: sessionId,
|
|
1469
|
+
turn_id: turnId
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
function consistentResultIdentity(field, sources) {
|
|
1473
|
+
const values = sources.flatMap(({ label, value }) => {
|
|
1474
|
+
if (!isRecord(value) || !Object.hasOwn(value, field)) {
|
|
1475
|
+
return [];
|
|
1476
|
+
}
|
|
1477
|
+
const identity = stringValue(value[field]);
|
|
1478
|
+
if (!identity) {
|
|
1479
|
+
throw new Error(`agent-knock-knock CLI returned invalid ${label}.${field}`);
|
|
1480
|
+
}
|
|
1481
|
+
return [{ label, identity }];
|
|
1482
|
+
});
|
|
1483
|
+
const expected = values[0];
|
|
1484
|
+
for (const candidate of values.slice(1)) {
|
|
1485
|
+
if (candidate.identity !== expected.identity) {
|
|
1486
|
+
throw new Error(`agent-knock-knock CLI returned conflicting ${field} between ${expected.label} and ${candidate.label}`);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
return expected?.identity;
|
|
1490
|
+
}
|
|
1491
|
+
async function runDelegate(api, params, toolContext) {
|
|
1492
|
+
const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
|
|
1493
|
+
const request = requiredString(params.request, "request");
|
|
1494
|
+
const openclawSession = stringValue(toolContext?.sessionKey) ??
|
|
1495
|
+
"agent:main:main";
|
|
1496
|
+
const args = [
|
|
1497
|
+
"delegate",
|
|
1498
|
+
"--request",
|
|
1499
|
+
request,
|
|
1500
|
+
"--background"
|
|
1501
|
+
];
|
|
1502
|
+
pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
|
|
1503
|
+
pushOptional(args, "--message-id", stringValue(params.messageId));
|
|
1504
|
+
pushOptional(args, "--openclaw-session", openclawSession);
|
|
1505
|
+
pushOptional(args, "--gateway-method", CALLBACK_METHOD);
|
|
1506
|
+
pushOptional(args, "--gateway-session", openclawSession);
|
|
1507
|
+
pushOptional(args, "--openclaw-bin", stringValue(config.openclawBin));
|
|
1508
|
+
pushOptional(args, "--idle-timeout-minutes", numberString(params.idleTimeoutMinutes) ?? numberString(config.idleTimeoutMinutes));
|
|
1509
|
+
pushOptional(args, "--agent-timeout-minutes", numberString(params.agentTimeoutMinutes) ?? numberString(config.agentTimeoutMinutes));
|
|
1510
|
+
pushOptional(args, "--agent-hard-timeout-minutes", numberString(params.agentHardTimeoutMinutes) ?? numberString(config.agentHardTimeoutMinutes));
|
|
1511
|
+
const parsed = withTurnIdentity(await runCliAsync(api, args));
|
|
1512
|
+
if (!isRecord(parsed)) {
|
|
1513
|
+
throw new Error("agent-knock-knock delegate returned a non-object result");
|
|
1514
|
+
}
|
|
1515
|
+
const conversationId = stringValue(parsed.conversation_id) ??
|
|
1516
|
+
(isRecord(parsed.conversation)
|
|
1517
|
+
? stringValue(parsed.conversation.conversation_id)
|
|
1518
|
+
: undefined);
|
|
1519
|
+
const sessionId = stringValue(parsed.session_id);
|
|
1520
|
+
const turnId = stringValue(parsed.turn_id);
|
|
1521
|
+
if (!conversationId || !sessionId || !turnId) {
|
|
1522
|
+
throw new Error("agent-knock-knock delegate returned incomplete Turn identity");
|
|
1523
|
+
}
|
|
1524
|
+
const parsedConversation = isRecord(parsed.conversation)
|
|
1525
|
+
? parsed.conversation
|
|
1526
|
+
: undefined;
|
|
1527
|
+
const parsedPaths = isRecord(parsed.paths) ? parsed.paths : undefined;
|
|
1528
|
+
const parsedTerminalControl = isRecord(parsed.terminal_control)
|
|
1529
|
+
? parsed.terminal_control
|
|
1530
|
+
: undefined;
|
|
1531
|
+
const statePath = parsedConversation?.state_path ??
|
|
1532
|
+
parsedPaths?.statePath;
|
|
1533
|
+
const logPath = parsedConversation?.event_log_path ??
|
|
1534
|
+
parsedPaths?.logPath;
|
|
1535
|
+
const submissionUncertain = parsed.submission_outcome === "uncertain";
|
|
1536
|
+
const submissionAborted = parsed.submission_outcome === "aborted";
|
|
1537
|
+
const submissionAccepted = parsed.submission_outcome === "agent_accepted" &&
|
|
1538
|
+
parsed.delivery_receipt === "agent_accepted" &&
|
|
1539
|
+
parsed.delivered === true;
|
|
1540
|
+
const submissionNotAccepted = parsed.submission_outcome === "not_accepted";
|
|
1541
|
+
const submissionPending = !submissionAccepted &&
|
|
1542
|
+
!submissionUncertain &&
|
|
1543
|
+
!submissionAborted &&
|
|
1544
|
+
!submissionNotAccepted;
|
|
1545
|
+
const submissionUnfenced = parsed.status === "delivered_unfenced";
|
|
1546
|
+
const agent = (isRecord(parsedConversation?.executor)
|
|
1547
|
+
? stringValue(parsedConversation.executor.kind)
|
|
1548
|
+
: undefined) ??
|
|
1549
|
+
stringValue(parsed.agent);
|
|
1550
|
+
return {
|
|
1551
|
+
status: submissionUnfenced
|
|
1552
|
+
? "submission_unfenced"
|
|
1553
|
+
: submissionUncertain
|
|
1554
|
+
? "submission_uncertain"
|
|
1555
|
+
: submissionAborted
|
|
1556
|
+
? "submission_aborted"
|
|
1557
|
+
: submissionNotAccepted
|
|
1558
|
+
? "submission_not_accepted"
|
|
1559
|
+
: submissionPending
|
|
1560
|
+
? "submission_pending_acceptance"
|
|
1561
|
+
: "async_pending",
|
|
1562
|
+
submission_status: submissionUnfenced
|
|
1563
|
+
? "submitted_unfenced"
|
|
1564
|
+
: submissionUncertain
|
|
1565
|
+
? "uncertain"
|
|
1566
|
+
: submissionAborted
|
|
1567
|
+
? "aborted"
|
|
1568
|
+
: submissionNotAccepted
|
|
1569
|
+
? "not_accepted"
|
|
1570
|
+
: submissionPending
|
|
1571
|
+
? "pending_acceptance"
|
|
1572
|
+
: "accepted",
|
|
1573
|
+
conversation_id: conversationId,
|
|
1574
|
+
session_id: sessionId,
|
|
1575
|
+
turn_id: turnId,
|
|
1576
|
+
conversation_status: parsedConversation?.status,
|
|
1577
|
+
state_path: statePath,
|
|
1578
|
+
event_log_path: logPath,
|
|
1579
|
+
agent,
|
|
1580
|
+
executor: parsedConversation?.executor,
|
|
1581
|
+
session: (isRecord(parsedConversation?.executor)
|
|
1582
|
+
? parsedConversation.executor.session
|
|
1583
|
+
: undefined) ??
|
|
1584
|
+
parsedTerminalControl?.target,
|
|
1585
|
+
openclaw_session: openclawSession,
|
|
1586
|
+
launched: parsed.launched === true,
|
|
1587
|
+
replayed: parsed.replayed === true,
|
|
1588
|
+
background: parsed.background === true,
|
|
1589
|
+
pid: parsed.pid ?? parsedTerminalControl?.panePid ?? null,
|
|
1590
|
+
callback_method: CALLBACK_METHOD,
|
|
1591
|
+
...(submissionUnfenced
|
|
1592
|
+
? {
|
|
1593
|
+
submission_outcome: "submitted",
|
|
1594
|
+
do_not_retry: true,
|
|
1595
|
+
reason: parsed.reason,
|
|
1596
|
+
openclaw_next_action: parsed.openclaw_next_action,
|
|
1597
|
+
note: "AKK sent the terminal input but could not fence later side effects to an exact native session. Do not retry or continue automatically; inspect the pane and close this Turn."
|
|
1598
|
+
}
|
|
1599
|
+
: submissionUncertain
|
|
1600
|
+
? {
|
|
1601
|
+
submission_outcome: "uncertain",
|
|
1602
|
+
do_not_retry: true,
|
|
1603
|
+
reason: parsed.reason,
|
|
1604
|
+
openclaw_next_action: parsed.openclaw_next_action,
|
|
1605
|
+
note: "AKK could not prove whether the terminal accepted Enter. Do not retry automatically; inspect the exact AKK Turn record and shared terminal."
|
|
1606
|
+
}
|
|
1607
|
+
: submissionAborted
|
|
1608
|
+
? parsed.safe_to_retry === true && parsed.do_not_retry !== true
|
|
1609
|
+
? {
|
|
1610
|
+
submission_outcome: "aborted",
|
|
1611
|
+
safe_to_retry: true,
|
|
1612
|
+
do_not_retry: false,
|
|
1613
|
+
reason: parsed.reason,
|
|
1614
|
+
openclaw_next_action: parsed.openclaw_next_action,
|
|
1615
|
+
note: "AKK stopped before sending terminal input and durably proved a safe abort. The request may be retried."
|
|
1616
|
+
}
|
|
1617
|
+
: {
|
|
1618
|
+
submission_outcome: "aborted",
|
|
1619
|
+
safe_to_retry: false,
|
|
1620
|
+
do_not_retry: true,
|
|
1621
|
+
reason: parsed.reason,
|
|
1622
|
+
openclaw_next_action: parsed.openclaw_next_action,
|
|
1623
|
+
note: "AKK could not prove a durable safe abort. Do not retry automatically; inspect the exact Turn and terminal dispatch ledger."
|
|
1624
|
+
}
|
|
1625
|
+
: submissionNotAccepted
|
|
1626
|
+
? {
|
|
1627
|
+
submission_outcome: "not_accepted",
|
|
1628
|
+
do_not_retry: true,
|
|
1629
|
+
reason: parsed.reason,
|
|
1630
|
+
openclaw_next_action: parsed.openclaw_next_action,
|
|
1631
|
+
note: "AKK proved terminal transport but the exact draft is still present in the agent composer. Do not retry automatically; inspect the shared pane."
|
|
1632
|
+
}
|
|
1633
|
+
: submissionPending
|
|
1634
|
+
? {
|
|
1635
|
+
submission_outcome: "pending_acceptance",
|
|
1636
|
+
do_not_retry: true,
|
|
1637
|
+
reason: parsed.reason,
|
|
1638
|
+
openclaw_next_action: parsed.openclaw_next_action,
|
|
1639
|
+
note: "AKK proved only terminal transport and is still waiting for native agent acceptance. Do not retry or report the task as accepted."
|
|
1640
|
+
}
|
|
1641
|
+
: {
|
|
1642
|
+
openclaw_next_action: {
|
|
1643
|
+
action: "yield",
|
|
1644
|
+
reason: "The coding agent is working in the shared terminal. End this OpenClaw turn now and wait for an Agent Knock Knock callback.",
|
|
1645
|
+
do_not: "Do not poll terminal internals while waiting. Further communication must use Agent Knock Knock tools so the same shared terminal remains authoritative.",
|
|
1646
|
+
expected_callback: "The callback will be injected and scheduled into this OpenClaw session by the agent-knock-knock.callback Gateway method."
|
|
1647
|
+
},
|
|
1648
|
+
note: "The task was sent to the shared terminal. OpenClaw should yield now and wait for the scheduled callback turn."
|
|
1649
|
+
})
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
function terminalMessageIdForToolCall({ toolCallId: toolCallIdValue, sessionKey: sessionKeyValue, sessionId: sessionIdValue, toolName }) {
|
|
1653
|
+
const toolCallId = stringValue(toolCallIdValue);
|
|
1654
|
+
if (!toolCallId) {
|
|
1655
|
+
return undefined;
|
|
1656
|
+
}
|
|
1657
|
+
const sessionKey = stringValue(sessionKeyValue) ?? "agent:main:main";
|
|
1658
|
+
// sessionId is the OpenClaw conversation incarnation. It changes across
|
|
1659
|
+
// /new and /reset even when sessionKey remains stable. Keep a literal null
|
|
1660
|
+
// fallback so retries with the same legacy context remain deterministic.
|
|
1661
|
+
const sessionId = stringValue(sessionIdValue) ?? null;
|
|
1662
|
+
const digest = createHash("sha256")
|
|
1663
|
+
.update(JSON.stringify([sessionKey, sessionId, toolName, toolCallId]))
|
|
1664
|
+
.digest("hex");
|
|
1665
|
+
return `msg-openclaw-${digest}`;
|
|
1666
|
+
}
|
|
1667
|
+
function registerCliTool(api, { name, description, parameters, buildArgs, normalizeTurnIdentity = true, isErrorResult = (_result) => false }) {
|
|
1668
|
+
api.registerTool((toolContext) => ({
|
|
1669
|
+
label: toolLabel(name),
|
|
1670
|
+
name,
|
|
1671
|
+
description,
|
|
1672
|
+
parameters,
|
|
1673
|
+
async execute(toolCallId, params) {
|
|
1674
|
+
try {
|
|
1675
|
+
const result = runCli(api, buildArgs(isRecord(params) ? params : {}, toolContext, toolCallId));
|
|
1676
|
+
const rendered = toolResult(result, {
|
|
1677
|
+
submissionErrors: name === "agent_knock_knock_respond",
|
|
1678
|
+
normalizeTurnIdentity,
|
|
1679
|
+
forceError: typeof isErrorResult === "function" && isErrorResult(result) === true
|
|
1680
|
+
});
|
|
1681
|
+
if (name === "agent_knock_knock_list") {
|
|
1682
|
+
rememberDisplayedPrivateAuthorityOffers(api, toolContext?.sessionKey, toolContext?.sessionId, result);
|
|
1683
|
+
}
|
|
1684
|
+
return rendered;
|
|
1685
|
+
}
|
|
1686
|
+
catch (error) {
|
|
1687
|
+
throw modelFacingToolError(error);
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
}), { name, optional: true });
|
|
1691
|
+
}
|
|
1692
|
+
function toolLabel(name) {
|
|
1693
|
+
const action = String(name)
|
|
1694
|
+
.replace(/^agent_knock_knock_/, "")
|
|
1695
|
+
.split("_")
|
|
1696
|
+
.filter(Boolean)
|
|
1697
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
1698
|
+
.join(" ");
|
|
1699
|
+
return `AKK ${action || "Tool"}`;
|
|
1700
|
+
}
|
|
1701
|
+
export function runCli(api, cliArgs, { cwd = process.cwd(), allowNonzeroJson = false } = {}) {
|
|
1702
|
+
const binPath = relayPathForApi(api);
|
|
1703
|
+
const spawned = spawnSync(process.execPath, [binPath, ...cliArgs], {
|
|
1704
|
+
encoding: "utf8",
|
|
1705
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
1706
|
+
cwd
|
|
1707
|
+
});
|
|
1708
|
+
if (spawned.error) {
|
|
1709
|
+
throw new Error(`agent-knock-knock ${cliArgs[0]} failed to start: ${spawned.error.message}`);
|
|
1710
|
+
}
|
|
1711
|
+
if (spawned.status !== 0) {
|
|
1712
|
+
if (allowNonzeroJson && spawned.stdout.trim()) {
|
|
1713
|
+
return parseJson(spawned.stdout);
|
|
1714
|
+
}
|
|
1715
|
+
throw new Error(cleanError(spawned.stderr || spawned.stdout || `agent-knock-knock ${cliArgs[0]} exited with status ${spawned.status}`));
|
|
1716
|
+
}
|
|
1717
|
+
return parseJson(spawned.stdout);
|
|
1718
|
+
}
|
|
1719
|
+
export function runCliAsync(api, cliArgs, { cwd = process.cwd(), allowNonzeroJson = false, timeoutMs = 90_000 } = {}) {
|
|
1720
|
+
const binPath = relayPathForApi(api);
|
|
1721
|
+
const maxBuffer = 10 * 1024 * 1024;
|
|
1722
|
+
return new Promise((resolve, reject) => {
|
|
1723
|
+
const child = spawn(process.execPath, [binPath, ...cliArgs], {
|
|
1724
|
+
cwd,
|
|
1725
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1726
|
+
windowsHide: true
|
|
1727
|
+
});
|
|
1728
|
+
child.stdout.setEncoding("utf8");
|
|
1729
|
+
child.stderr.setEncoding("utf8");
|
|
1730
|
+
let stdout = "";
|
|
1731
|
+
let stderr = "";
|
|
1732
|
+
let overflow = false;
|
|
1733
|
+
let timedOut = false;
|
|
1734
|
+
const append = (current, chunk) => {
|
|
1735
|
+
const next = current + chunk;
|
|
1736
|
+
if (Buffer.byteLength(next, "utf8") > maxBuffer) {
|
|
1737
|
+
overflow = true;
|
|
1738
|
+
child.kill("SIGKILL");
|
|
1739
|
+
}
|
|
1740
|
+
return next;
|
|
1741
|
+
};
|
|
1742
|
+
child.stdout.on("data", (chunk) => {
|
|
1743
|
+
stdout = append(stdout, chunk);
|
|
1744
|
+
});
|
|
1745
|
+
child.stderr.on("data", (chunk) => {
|
|
1746
|
+
stderr = append(stderr, chunk);
|
|
1747
|
+
});
|
|
1748
|
+
const timeout = setTimeout(() => {
|
|
1749
|
+
timedOut = true;
|
|
1750
|
+
child.kill("SIGKILL");
|
|
1751
|
+
}, timeoutMs);
|
|
1752
|
+
timeout.unref();
|
|
1753
|
+
child.once("error", (error) => {
|
|
1754
|
+
clearTimeout(timeout);
|
|
1755
|
+
reject(new Error(`agent-knock-knock ${cliArgs[0]} failed to start: ${error.message}`));
|
|
1756
|
+
});
|
|
1757
|
+
child.once("close", (status) => {
|
|
1758
|
+
clearTimeout(timeout);
|
|
1759
|
+
if (timedOut) {
|
|
1760
|
+
reject(new Error(`agent-knock-knock ${cliArgs[0]} timed out`));
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
if (overflow) {
|
|
1764
|
+
reject(new Error(`agent-knock-knock ${cliArgs[0]} output exceeded 10 MiB`));
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1767
|
+
if (status !== 0) {
|
|
1768
|
+
if (allowNonzeroJson && stdout.trim()) {
|
|
1769
|
+
try {
|
|
1770
|
+
resolve(parseJson(stdout));
|
|
1771
|
+
}
|
|
1772
|
+
catch (error) {
|
|
1773
|
+
reject(error);
|
|
1774
|
+
}
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
reject(new Error(cleanError(stderr ||
|
|
1778
|
+
stdout ||
|
|
1779
|
+
`agent-knock-knock ${cliArgs[0]} exited with status ${status}`)));
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
try {
|
|
1783
|
+
resolve(parseJson(stdout));
|
|
1784
|
+
}
|
|
1785
|
+
catch (error) {
|
|
1786
|
+
reject(error);
|
|
1787
|
+
}
|
|
1788
|
+
});
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
function relayPathForApi(api) {
|
|
1792
|
+
return relayPathByApi.get(api) ?? defaultOpenClawRelayPath;
|
|
1793
|
+
}
|
|
1794
|
+
function pushTurnTarget(args, params) {
|
|
1795
|
+
if (Object.hasOwn(params, "turn_id") && Object.hasOwn(params, "conversation_id")) {
|
|
1796
|
+
throw new Error("turn-target tools accept only one of turn_id or conversation_id");
|
|
1797
|
+
}
|
|
1798
|
+
const turnId = stringValue(params.turn_id);
|
|
1799
|
+
if (turnId) {
|
|
1800
|
+
args.push("--turn", authoritativeManagedId(turnId, "turn_id"));
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1803
|
+
args.push("--conversation", requiredString(params.conversation_id, "turn_id"));
|
|
1804
|
+
}
|
|
1805
|
+
function authoritativeManagedId(value, name) {
|
|
1806
|
+
const id = requiredString(value, name).trim();
|
|
1807
|
+
if (/^(?:only|latest|codex|claude|(?:codex|claude):latest)$/iu.test(id) ||
|
|
1808
|
+
/^@[0-9a-f]+$/iu.test(id) ||
|
|
1809
|
+
/^terminal:/iu.test(id)) {
|
|
1810
|
+
throw new Error(`${name} must be an authoritative managed id, not a discovery selector or terminal id`);
|
|
1811
|
+
}
|
|
1812
|
+
return id;
|
|
1813
|
+
}
|
|
1814
|
+
function assertExclusiveRecoveryFence(params) {
|
|
1815
|
+
const expectedMessageId = stringValue(params.expected_message_id);
|
|
1816
|
+
const expectedTransitionId = stringValue(params.expected_transition_id);
|
|
1817
|
+
const closeFenceCount = [expectedMessageId, expectedTransitionId]
|
|
1818
|
+
.filter(Boolean).length;
|
|
1819
|
+
if (closeFenceCount > 1) {
|
|
1820
|
+
throw new Error("close accepts only one of expected_message_id or expected_transition_id");
|
|
1821
|
+
}
|
|
1822
|
+
const handoffReason = stringValue(params.reason) ===
|
|
1823
|
+
"superseded_by_human_context_switch";
|
|
1824
|
+
if (handoffReason && (!stringValue(params.turn_id) ||
|
|
1825
|
+
stringValue(params.conversation_id) ||
|
|
1826
|
+
closeFenceCount > 0)) {
|
|
1827
|
+
throw new Error("human-context handoff requires only the exact managed turn_id and supersede reason");
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
export function pushOptional(args, flag, value) {
|
|
1831
|
+
if (value !== undefined && value !== "") {
|
|
1832
|
+
args.push(flag, value);
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
function numberString(value) {
|
|
1836
|
+
return typeof value === "number" && Number.isFinite(value) ? String(value) : undefined;
|
|
1837
|
+
}
|
|
1838
|
+
function truncateText(value, maxLength) {
|
|
1839
|
+
const text = String(value ?? "").replace(/\s+/g, " ").trim();
|
|
1840
|
+
if (text.length <= maxLength) {
|
|
1841
|
+
return text;
|
|
1842
|
+
}
|
|
1843
|
+
return `${text.slice(0, Math.max(0, maxLength - 1))}...`;
|
|
1844
|
+
}
|
|
1845
|
+
function requiredString(value, name) {
|
|
1846
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
1847
|
+
throw new Error(`${name} is required`);
|
|
1848
|
+
}
|
|
1849
|
+
return value;
|
|
1850
|
+
}
|
|
1851
|
+
function requiredOpenClawSessionKey(value) {
|
|
1852
|
+
return requiredString(value, "OpenClaw session identity for this confirmed action");
|
|
1853
|
+
}
|
|
1854
|
+
function requiredOpenClawSessionId(value) {
|
|
1855
|
+
return requiredString(value, "OpenClaw conversation incarnation for this confirmed action");
|
|
1856
|
+
}
|
|
1857
|
+
function parseJson(text) {
|
|
1858
|
+
try {
|
|
1859
|
+
return JSON.parse(text);
|
|
1860
|
+
}
|
|
1861
|
+
catch (error) {
|
|
1862
|
+
throw new Error(`agent-knock-knock CLI returned invalid JSON: ${error.message}`);
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
function cleanError(text) {
|
|
1866
|
+
return String(text).trim().slice(0, 2000);
|
|
1867
|
+
}
|
|
1868
|
+
//# sourceMappingURL=openclaw-plugin-command-adapter.js.map
|