@agentdeck/bridge 1.0.20 → 1.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/dist/adapters/openclaw.d.ts +55 -1
  2. package/dist/adapters/openclaw.d.ts.map +1 -1
  3. package/dist/adapters/openclaw.js +278 -45
  4. package/dist/adapters/openclaw.js.map +1 -1
  5. package/dist/adb-reverse.d.ts +26 -3
  6. package/dist/adb-reverse.d.ts.map +1 -1
  7. package/dist/adb-reverse.js +26 -5
  8. package/dist/adb-reverse.js.map +1 -1
  9. package/dist/apme/adapters/openclaw-hook.d.ts +45 -1
  10. package/dist/apme/adapters/openclaw-hook.d.ts.map +1 -1
  11. package/dist/apme/adapters/openclaw-hook.js +84 -4
  12. package/dist/apme/adapters/openclaw-hook.js.map +1 -1
  13. package/dist/apme/claude-transcript-reader.d.ts +54 -0
  14. package/dist/apme/claude-transcript-reader.d.ts.map +1 -1
  15. package/dist/apme/claude-transcript-reader.js +77 -0
  16. package/dist/apme/claude-transcript-reader.js.map +1 -1
  17. package/dist/apme/collector.d.ts +63 -5
  18. package/dist/apme/collector.d.ts.map +1 -1
  19. package/dist/apme/collector.js +147 -10
  20. package/dist/apme/collector.js.map +1 -1
  21. package/dist/apme/store.d.ts +23 -1
  22. package/dist/apme/store.d.ts.map +1 -1
  23. package/dist/apme/store.js +81 -2
  24. package/dist/apme/store.js.map +1 -1
  25. package/dist/apme/types.d.ts +1 -1
  26. package/dist/apme/types.d.ts.map +1 -1
  27. package/dist/apme/types.js.map +1 -1
  28. package/dist/auth.d.ts +37 -0
  29. package/dist/auth.d.ts.map +1 -1
  30. package/dist/auth.js +23 -0
  31. package/dist/auth.js.map +1 -1
  32. package/dist/claude-interrupt-marker.d.ts +31 -0
  33. package/dist/claude-interrupt-marker.d.ts.map +1 -0
  34. package/dist/claude-interrupt-marker.js +58 -0
  35. package/dist/claude-interrupt-marker.js.map +1 -0
  36. package/dist/claude-turn-watchdog.d.ts +23 -0
  37. package/dist/claude-turn-watchdog.d.ts.map +1 -1
  38. package/dist/claude-turn-watchdog.js +30 -4
  39. package/dist/claude-turn-watchdog.js.map +1 -1
  40. package/dist/cli.d.ts +6 -0
  41. package/dist/cli.d.ts.map +1 -1
  42. package/dist/cli.js +398 -115
  43. package/dist/cli.js.map +1 -1
  44. package/dist/daemon-port.d.ts +90 -0
  45. package/dist/daemon-port.d.ts.map +1 -0
  46. package/dist/daemon-port.js +119 -0
  47. package/dist/daemon-port.js.map +1 -0
  48. package/dist/daemon-server.d.ts +14 -1
  49. package/dist/daemon-server.d.ts.map +1 -1
  50. package/dist/daemon-server.js +297 -69
  51. package/dist/daemon-server.js.map +1 -1
  52. package/dist/daemon-settings.d.ts +46 -0
  53. package/dist/daemon-settings.d.ts.map +1 -0
  54. package/dist/daemon-settings.js +85 -0
  55. package/dist/daemon-settings.js.map +1 -0
  56. package/dist/daemon-takeover.d.ts +75 -0
  57. package/dist/daemon-takeover.d.ts.map +1 -0
  58. package/dist/daemon-takeover.js +137 -0
  59. package/dist/daemon-takeover.js.map +1 -0
  60. package/dist/daemon.js +1 -1
  61. package/dist/index.d.ts +1 -1
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +30 -11
  64. package/dist/index.js.map +1 -1
  65. package/dist/kiro-diagnostics.d.ts +90 -0
  66. package/dist/kiro-diagnostics.d.ts.map +1 -0
  67. package/dist/kiro-diagnostics.js +278 -0
  68. package/dist/kiro-diagnostics.js.map +1 -0
  69. package/dist/kiro-session.d.ts +113 -0
  70. package/dist/kiro-session.d.ts.map +1 -0
  71. package/dist/kiro-session.js +688 -0
  72. package/dist/kiro-session.js.map +1 -0
  73. package/dist/kiro-timeline-feed.d.ts +45 -0
  74. package/dist/kiro-timeline-feed.d.ts.map +1 -0
  75. package/dist/kiro-timeline-feed.js +116 -0
  76. package/dist/kiro-timeline-feed.js.map +1 -0
  77. package/dist/kiro-transcript-timeline.d.ts +57 -0
  78. package/dist/kiro-transcript-timeline.d.ts.map +1 -0
  79. package/dist/kiro-transcript-timeline.js +183 -0
  80. package/dist/kiro-transcript-timeline.js.map +1 -0
  81. package/dist/linux-service.d.ts +2 -1
  82. package/dist/linux-service.d.ts.map +1 -1
  83. package/dist/linux-service.js +8 -3
  84. package/dist/linux-service.js.map +1 -1
  85. package/dist/mdns-discover.d.ts +27 -0
  86. package/dist/mdns-discover.d.ts.map +1 -1
  87. package/dist/mdns-discover.js +60 -7
  88. package/dist/mdns-discover.js.map +1 -1
  89. package/dist/mdns.d.ts.map +1 -1
  90. package/dist/mdns.js +16 -3
  91. package/dist/mdns.js.map +1 -1
  92. package/dist/modules/adb-module.d.ts.map +1 -1
  93. package/dist/modules/adb-module.js +5 -2
  94. package/dist/modules/adb-module.js.map +1 -1
  95. package/dist/modules/index.d.ts +2 -1
  96. package/dist/modules/index.d.ts.map +1 -1
  97. package/dist/modules/index.js +1 -0
  98. package/dist/modules/index.js.map +1 -1
  99. package/dist/modules/types.d.ts +34 -9
  100. package/dist/modules/types.d.ts.map +1 -1
  101. package/dist/modules/types.js +34 -1
  102. package/dist/modules/types.js.map +1 -1
  103. package/dist/network-posture.d.ts +67 -0
  104. package/dist/network-posture.d.ts.map +1 -0
  105. package/dist/network-posture.js +100 -0
  106. package/dist/network-posture.js.map +1 -0
  107. package/dist/observed-steering.d.ts +14 -0
  108. package/dist/observed-steering.d.ts.map +1 -1
  109. package/dist/observed-steering.js +18 -0
  110. package/dist/observed-steering.js.map +1 -1
  111. package/dist/observed-turn-watchdogs.d.ts +84 -0
  112. package/dist/observed-turn-watchdogs.d.ts.map +1 -0
  113. package/dist/observed-turn-watchdogs.js +134 -0
  114. package/dist/observed-turn-watchdogs.js.map +1 -0
  115. package/dist/openclaw-session.d.ts +10 -0
  116. package/dist/openclaw-session.d.ts.map +1 -1
  117. package/dist/openclaw-session.js +12 -0
  118. package/dist/openclaw-session.js.map +1 -1
  119. package/dist/passive-observer.d.ts +19 -0
  120. package/dist/passive-observer.d.ts.map +1 -1
  121. package/dist/passive-observer.js +208 -24
  122. package/dist/passive-observer.js.map +1 -1
  123. package/dist/pixoo/micro-glyphs.d.ts +1 -1
  124. package/dist/pixoo/micro-glyphs.d.ts.map +1 -1
  125. package/dist/pixoo/micro-glyphs.js +2 -0
  126. package/dist/pixoo/micro-glyphs.js.map +1 -1
  127. package/dist/pixoo/official-dot-glyphs.generated.d.ts +1 -1
  128. package/dist/pixoo/official-dot-glyphs.generated.d.ts.map +1 -1
  129. package/dist/pixoo/official-dot-glyphs.generated.js +47 -0
  130. package/dist/pixoo/official-dot-glyphs.generated.js.map +1 -1
  131. package/dist/pixoo/pixoo-discover.d.ts +8 -1
  132. package/dist/pixoo/pixoo-discover.d.ts.map +1 -1
  133. package/dist/pixoo/pixoo-discover.js +15 -8
  134. package/dist/pixoo/pixoo-discover.js.map +1 -1
  135. package/dist/pixoo/pixoo-renderer.d.ts +4 -2
  136. package/dist/pixoo/pixoo-renderer.d.ts.map +1 -1
  137. package/dist/pixoo/pixoo-renderer.js +105 -36
  138. package/dist/pixoo/pixoo-renderer.js.map +1 -1
  139. package/dist/pixoo/pixoo-settings.d.ts +19 -2
  140. package/dist/pixoo/pixoo-settings.d.ts.map +1 -1
  141. package/dist/pixoo/pixoo-settings.js +22 -4
  142. package/dist/pixoo/pixoo-settings.js.map +1 -1
  143. package/dist/pixoo/pixoo-sprites.d.ts.map +1 -1
  144. package/dist/pixoo/pixoo-sprites.js +2 -0
  145. package/dist/pixoo/pixoo-sprites.js.map +1 -1
  146. package/dist/session-aggregator.d.ts +9 -1
  147. package/dist/session-aggregator.d.ts.map +1 -1
  148. package/dist/session-aggregator.js +1 -1
  149. package/dist/session-aggregator.js.map +1 -1
  150. package/dist/session-registry.d.ts +71 -3
  151. package/dist/session-registry.d.ts.map +1 -1
  152. package/dist/session-registry.js +126 -19
  153. package/dist/session-registry.js.map +1 -1
  154. package/dist/subagent-timeline.d.ts +38 -7
  155. package/dist/subagent-timeline.d.ts.map +1 -1
  156. package/dist/subagent-timeline.js +154 -23
  157. package/dist/subagent-timeline.js.map +1 -1
  158. package/dist/timeline-store.d.ts.map +1 -1
  159. package/dist/timeline-store.js +26 -1
  160. package/dist/timeline-store.js.map +1 -1
  161. package/dist/tui/renderer.d.ts.map +1 -1
  162. package/dist/tui/renderer.js +13 -1
  163. package/dist/tui/renderer.js.map +1 -1
  164. package/dist/tui/terrarium.d.ts.map +1 -1
  165. package/dist/tui/terrarium.js +10 -5
  166. package/dist/tui/terrarium.js.map +1 -1
  167. package/dist/utils/redact-secrets.d.ts +3 -0
  168. package/dist/utils/redact-secrets.d.ts.map +1 -0
  169. package/dist/utils/redact-secrets.js +29 -0
  170. package/dist/utils/redact-secrets.js.map +1 -0
  171. package/dist/windows-service.d.ts +2 -1
  172. package/dist/windows-service.d.ts.map +1 -1
  173. package/dist/windows-service.js +6 -3
  174. package/dist/windows-service.js.map +1 -1
  175. package/package.json +3 -3
@@ -1,6 +1,7 @@
1
1
  import { EventEmitter } from 'events';
2
2
  import { type Server } from 'http';
3
3
  import type { AgentAdapter, AgentCapabilities, AdapterStartOptions, PluginCommand } from '../types.js';
4
+ import { type OpenClawApprovalPrompt } from '@agentdeck/shared';
4
5
  /**
5
6
  * OpenClaw adapter — connects to OpenClaw Gateway via WebSocket.
6
7
  *
@@ -34,7 +35,14 @@ export declare class OpenClawAdapter extends EventEmitter implements AgentAdapte
34
35
  private autoReconnect;
35
36
  private currentSessionKey;
36
37
  private currentRunId;
37
- private pendingApprovalId;
38
+ /** The approval the Gateway is currently blocked on, normalized for display
39
+ * AND for answering — the options carry their own decision so an index press
40
+ * is never re-derived at the answer site. `null` ⇒ nothing to approve. */
41
+ private pendingApproval;
42
+ /** Fires at `expiresAtMs`. The Gateway drops an expired approval without
43
+ * emitting `exec.approval.resolved`, so without this the deck would keep
44
+ * offering buttons that resolve to "unknown or expired approval id". */
45
+ private approvalExpiryTimer;
38
46
  private chatStarted;
39
47
  private chatStartTime;
40
48
  private chatToolCount;
@@ -76,6 +84,52 @@ export declare class OpenClawAdapter extends EventEmitter implements AgentAdapte
76
84
  start(options: AdapterStartOptions & {
77
85
  externalServer?: Server;
78
86
  }): Promise<void>;
87
+ /**
88
+ * The approval the Gateway is blocked on, or `null`. Read by the daemon so
89
+ * the virtual `openclaw-gateway` session row can carry the real question and
90
+ * option list — without it every deck falls back to "PERMIT? / answer in
91
+ * terminal", which is not answerable anywhere.
92
+ */
93
+ getPendingApproval(): OpenClawApprovalPrompt | null;
94
+ /**
95
+ * Read the approvals the Gateway is already blocked on and surface the oldest
96
+ * one. Called once per handshake.
97
+ *
98
+ * Only one is adopted: the deck renders a single question at a time, and the
99
+ * Gateway blocks the agent on each in turn, so the oldest is the one actually
100
+ * holding work up. The rest arrive as `requested` events (or on the next
101
+ * catch-up) once it is answered.
102
+ */
103
+ private adoptPendingApprovals;
104
+ private setPendingApproval;
105
+ private clearPendingApproval;
106
+ /**
107
+ * The approval went away without a decision — it expired, its run was
108
+ * cancelled, or the Gateway link dropped. The Gateway emits no
109
+ * `exec.approval.resolved` for any of those, so nothing else would ever close
110
+ * the `tool_request` row or move the session off `awaiting_permission`: the
111
+ * deck kept showing PERMIT? for a request that no longer existed.
112
+ */
113
+ private abandonPendingApproval;
114
+ /** Called from the turn-end paths (`final` / `aborted` / `error`). */
115
+ private abandonPendingApprovalForTurnEnd;
116
+ /**
117
+ * Send the decision the Gateway actually accepts. The pending pointer is held
118
+ * until the RPC succeeds: clearing it optimistically (as this used to) meant a
119
+ * rejected call left the user with no prompt on screen AND an approval still
120
+ * blocking the agent, with the failure swallowed into a debug line that is off
121
+ * by default.
122
+ */
123
+ private resolveApproval;
124
+ /** Re-emit the pending prompt so devices re-sync after a dropped press. */
125
+ private rebroadcastPendingApproval;
126
+ /**
127
+ * Truncation-tolerant echo check. Devices cap the question they display
128
+ * (ESP32 buffers, 16-char key labels), so an exact compare would refuse every
129
+ * legitimate press from a small surface. Compare on UTF-16 units to match the
130
+ * TS producers; the Swift mirror compares `.utf16` for the same reason.
131
+ */
132
+ private approvalEchoMatches;
79
133
  handleCommand(cmd: PluginCommand): boolean;
80
134
  writeInput(data: string): void;
81
135
  isAlive(): boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"openclaw.d.ts","sourceRoot":"","sources":["../../src/adapters/openclaw.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"openclaw.d.ts","sourceRoot":"","sources":["../../src/adapters/openclaw.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,MAAM,CAAC;AAgBjD,OAAO,KAAK,EACV,YAAY,EACZ,iBAAiB,EACjB,mBAAmB,EAEnB,aAAa,EAQd,MAAM,aAAa,CAAC;AAErB,OAAO,EAKL,KAAK,sBAAsB,EAE5B,MAAM,mBAAmB,CAAC;AAuD3B;;;;;;;;;;GAUG;AACH,MAAM,WAAW,sBAAsB;IACrC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qFAAqF;IACrF,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,qBAAa,eAAgB,SAAQ,YAAa,YAAW,YAAY;IACvE,QAAQ,CAAC,YAAY,EAAE,iBAAiB,CAAyB;IAEjE,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,EAAE,CAA0B;IACpC,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,KAAK,CAAK;IAClB,OAAO,CAAC,UAAU,CAIb;IACL,OAAO,CAAC,cAAc,CAA8C;IACpE,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,kBAAkB,CAAS;IACnC,OAAO,CAAC,WAAW,CAA6C;IAChE,OAAO,CAAC,eAAe,CAAyC;IAChE,OAAO,CAAC,aAAa,CAAU;IAG/B,OAAO,CAAC,iBAAiB,CAAuB;IAChD,OAAO,CAAC,YAAY,CAAuB;IAC3C;;+EAE2E;IAC3E,OAAO,CAAC,eAAe,CAAuC;IAC9D;;6EAEyE;IACzE,OAAO,CAAC,mBAAmB,CAA8C;IAGzE,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,mBAAmB,CAAM;IACjC,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,eAAe,CAAS;IAGhC,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,eAAe,CAAgC;IACvD,OAAO,CAAC,kBAAkB,CAAuB;IACjD,OAAO,CAAC,+BAA+B,CAAS;IAOhD,OAAO,CAAC,aAAa,CAAuB;IAC5C,OAAO,CAAC,WAAW,CAAgB;IACnC,OAAO,CAAC,WAAW,CAAqB;IACxC;;8EAE0E;IAC1E,OAAO,CAAC,aAAa,CAA8C;IAEnE,yDAAyD;IACzD,OAAO,CAAC,cAAc,CAAQ;IAC9B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAU;IACrD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAU;gBAEjC,OAAO,CAAC,EAAE,MAAM,GAAG,sBAAsB;IAiDrD;yEACqE;IACrE,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI;IAKrD;;gEAE4D;IAC5D,sBAAsB,IAAI,OAAO;IAIjC,OAAO,CAAC,YAAY;IAWpB,OAAO,CAAC,eAAe;IASvB;;gEAE4D;IAC5D,OAAO,CAAC,eAAe;IAiBvB,OAAO,CAAC,iBAAiB;IAOnB,KAAK,CAAC,OAAO,EAAE,mBAAmB,GAAG;QAAE,cAAc,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAoCtF;;;;;OAKG;IACH,kBAAkB,IAAI,sBAAsB,GAAG,IAAI;IAInD;;;;;;;;OAQG;YACW,qBAAqB;IAqBnC,OAAO,CAAC,kBAAkB;IAe1B,OAAO,CAAC,oBAAoB;IAQ5B;;;;;;OAMG;IACH,OAAO,CAAC,sBAAsB;IAU9B,sEAAsE;IACtE,OAAO,CAAC,gCAAgC;IAKxC;;;;;;OAMG;IACH,OAAO,CAAC,eAAe;IAqBvB,2EAA2E;IAC3E,OAAO,CAAC,0BAA0B;IAiBlC;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;IAO3B,aAAa,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO;IAiJ1C,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAc9B,OAAO,IAAI,OAAO;IAIlB,cAAc,CAAC,MAAM,EAAE,MAAM,CAAC,cAAc,EAAE,OAAO,EAAE,MAAM,CAAC,cAAc,GAAG,IAAI;IAInF,UAAU,IAAI,MAAM,GAAG,SAAS;IAIhC,cAAc,IAAI,MAAM,GAAG,IAAI;IAI/B,aAAa,IAAI,MAAM;IAIvB,MAAM,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,GAAG,IAAI;IAIjD,SAAS,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI;IAI3C,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAgC/B,OAAO,CAAC,kBAAkB;IAgC1B,OAAO,CAAC,sBAAsB;IAW9B,kEAAkE;IAClE,OAAO,CAAC,qBAAqB;IAQ7B,mDAAmD;IACnD,OAAO,CAAC,eAAe;IA2CvB,OAAO,CAAC,cAAc;IA4DtB,OAAO,CAAC,iBAAiB;IAiBzB;;;;;OAKG;IACH,OAAO,CAAC,OAAO;IAsCf,OAAO,CAAC,oBAAoB;IA2B5B,OAAO,CAAC,kBAAkB;IAuc1B;;;OAGG;IACH,OAAO,CAAC,kBAAkB;IAmH1B,sEAAsE;YACxD,aAAa;IAmC3B,4EAA4E;YAC9D,gBAAgB;IAuC9B;;;OAGG;IACH,OAAO,CAAC,kBAAkB;IAW1B,uDAAuD;IACvD,OAAO,CAAC,gBAAgB;IAMxB,6DAA6D;IAC7D,OAAO,CAAC,iBAAiB;IAIzB,mFAAmF;IACnF,OAAO,CAAC,kBAAkB;IAI1B,OAAO,CAAC,gBAAgB;CAGzB"}
@@ -9,10 +9,11 @@ import { debug, logError } from '../logger.js';
9
9
  import { summarizeResponse } from '../timeline-summarizer.js';
10
10
  import { extractTopicHint, extractTopicHintWithKind, promptSnippetFallback, prepareMarkdownDetail } from '@agentdeck/shared';
11
11
  import { cleanRawText, cleanNopMarkers, isOpenClawCronPrompt, ED25519_SPKI_PREFIX_LEN, GATEWAY_PROTOCOL_VERSION, } from '@agentdeck/shared';
12
+ import { parseExecApprovalRequest, decisionForOptionIndex, decisionForRespondValue, execApprovalAllows, } from '@agentdeck/shared';
12
13
  import { OPENCLAW_CAPABILITIES, OPENCLAW_GATEWAY_PORT } from '../types.js';
13
14
  import { fetchModelCatalog, getDefaultModelName, invalidateModelCache } from '../model-catalog.js';
14
15
  import { getApme } from '../apme/index.js';
15
- import { openclawChatEventToSpans, openclawChatSendToSpan, openclawIdleGapTaskBoundary, openclawSessionToolToSpans, openclawSessionMessageToSpans, OPENCLAW_IDLE_GAP_MS, } from '../apme/adapters/openclaw-hook.js';
16
+ import { openclawChatEventToSpans, openclawChatErrorLabel, openclawChatErrorDetail, openclawChatSendToSpan, openclawIdleGapTaskBoundary, openclawSessionToolToSpans, openclawSessionMessageToSpans, OPENCLAW_IDLE_GAP_MS, } from '../apme/adapters/openclaw-hook.js';
16
17
  function extractGatewayTokenFromJson(json) {
17
18
  if (!json || typeof json !== 'object')
18
19
  return null;
@@ -63,7 +64,14 @@ export class OpenClawAdapter extends EventEmitter {
63
64
  // Session tracking
64
65
  currentSessionKey = null;
65
66
  currentRunId = null;
66
- pendingApprovalId = null;
67
+ /** The approval the Gateway is currently blocked on, normalized for display
68
+ * AND for answering — the options carry their own decision so an index press
69
+ * is never re-derived at the answer site. `null` ⇒ nothing to approve. */
70
+ pendingApproval = null;
71
+ /** Fires at `expiresAtMs`. The Gateway drops an expired approval without
72
+ * emitting `exec.approval.resolved`, so without this the deck would keep
73
+ * offering buttons that resolve to "unknown or expired approval id". */
74
+ approvalExpiryTimer = null;
67
75
  // Chat tracking for timeline events
68
76
  chatStarted = false;
69
77
  chatStartTime = 0;
@@ -240,34 +248,182 @@ export class OpenClawAdapter extends EventEmitter {
240
248
  // Connect to OpenClaw Gateway
241
249
  this.connectGateway();
242
250
  }
251
+ // ===== Pending exec approval =====
252
+ /**
253
+ * The approval the Gateway is blocked on, or `null`. Read by the daemon so
254
+ * the virtual `openclaw-gateway` session row can carry the real question and
255
+ * option list — without it every deck falls back to "PERMIT? / answer in
256
+ * terminal", which is not answerable anywhere.
257
+ */
258
+ getPendingApproval() {
259
+ return this.pendingApproval;
260
+ }
261
+ /**
262
+ * Read the approvals the Gateway is already blocked on and surface the oldest
263
+ * one. Called once per handshake.
264
+ *
265
+ * Only one is adopted: the deck renders a single question at a time, and the
266
+ * Gateway blocks the agent on each in turn, so the oldest is the one actually
267
+ * holding work up. The rest arrive as `requested` events (or on the next
268
+ * catch-up) once it is answered.
269
+ */
270
+ async adoptPendingApprovals() {
271
+ const pending = await this.rpcCall('exec.approval.list', {});
272
+ if (!Array.isArray(pending) || pending.length === 0)
273
+ return;
274
+ const oldest = [...pending].sort((a, b) => (a?.createdAtMs ?? 0) - (b?.createdAtMs ?? 0))[0];
275
+ const prompt = parseExecApprovalRequest(oldest, Date.now());
276
+ if (!prompt)
277
+ return;
278
+ // Already tracking it (a reconnect that raced the event) — don't re-emit.
279
+ if (this.pendingApproval?.id === prompt.id)
280
+ return;
281
+ debug('adapter:openclaw', `adopted pending approval ${prompt.id} on connect`);
282
+ this.setPendingApproval(prompt);
283
+ this.emitTimelineEntry({
284
+ ts: Date.now(), type: 'tool_request',
285
+ raw: prompt.question.length > 500 ? prompt.question.slice(0, 497) + '...' : prompt.question,
286
+ ...(prompt.detail ? { detail: prompt.detail } : {}),
287
+ approvalId: prompt.id, status: 'pending',
288
+ });
289
+ this.rebroadcastPendingApproval();
290
+ }
291
+ setPendingApproval(prompt) {
292
+ this.clearPendingApproval();
293
+ this.pendingApproval = prompt;
294
+ if (typeof prompt.expiresAtMs === 'number') {
295
+ const delay = prompt.expiresAtMs - Date.now();
296
+ // An already-expired stamp still needs the sweep to run (on the next
297
+ // tick, not inline) so the row and the state settle the same way.
298
+ this.approvalExpiryTimer = setTimeout(() => this.abandonPendingApproval(prompt.id, 'Expired'), Math.max(0, delay));
299
+ this.approvalExpiryTimer.unref?.();
300
+ }
301
+ }
302
+ clearPendingApproval() {
303
+ if (this.approvalExpiryTimer) {
304
+ clearTimeout(this.approvalExpiryTimer);
305
+ this.approvalExpiryTimer = null;
306
+ }
307
+ this.pendingApproval = null;
308
+ }
309
+ /**
310
+ * The approval went away without a decision — it expired, its run was
311
+ * cancelled, or the Gateway link dropped. The Gateway emits no
312
+ * `exec.approval.resolved` for any of those, so nothing else would ever close
313
+ * the `tool_request` row or move the session off `awaiting_permission`: the
314
+ * deck kept showing PERMIT? for a request that no longer existed.
315
+ */
316
+ abandonPendingApproval(approvalId, reason) {
317
+ if (!this.pendingApproval || this.pendingApproval.id !== approvalId)
318
+ return;
319
+ this.clearPendingApproval();
320
+ this.emitTimelineEntry({
321
+ ts: Date.now(), type: 'tool_resolved', raw: `Not approved · ${reason}`,
322
+ approvalId, status: 'denied',
323
+ });
324
+ this.emitAdapterEvent({ source: 'parser', event: 'idle' });
325
+ }
326
+ /** Called from the turn-end paths (`final` / `aborted` / `error`). */
327
+ abandonPendingApprovalForTurnEnd(reason) {
328
+ const pending = this.pendingApproval;
329
+ if (pending)
330
+ this.abandonPendingApproval(pending.id, reason);
331
+ }
332
+ /**
333
+ * Send the decision the Gateway actually accepts. The pending pointer is held
334
+ * until the RPC succeeds: clearing it optimistically (as this used to) meant a
335
+ * rejected call left the user with no prompt on screen AND an approval still
336
+ * blocking the agent, with the failure swallowed into a debug line that is off
337
+ * by default.
338
+ */
339
+ resolveApproval(prompt, decision) {
340
+ debug('adapter:openclaw', `exec.approval.resolve ${prompt.id} → ${decision}`);
341
+ this.rpcCall('exec.approval.resolve', { id: prompt.id, decision })
342
+ .then(() => {
343
+ // The `exec.approval.resolved` event is the authoritative close (it also
344
+ // arrives when another operator answers). Clear here too so a dropped
345
+ // event cannot strand the prompt.
346
+ if (this.pendingApproval?.id === prompt.id)
347
+ this.clearPendingApproval();
348
+ })
349
+ .catch((err) => {
350
+ logError(`[adapter:openclaw] exec.approval.resolve failed (${decision}): ${String(err)}`);
351
+ this.emitTimelineEntry({
352
+ ts: Date.now(), type: 'error',
353
+ raw: `Approval failed: ${err instanceof Error ? err.message : String(err)}`,
354
+ });
355
+ // Put the prompt back on every surface so the user can retry rather
356
+ // than staring at a deck that silently did nothing.
357
+ this.rebroadcastPendingApproval();
358
+ });
359
+ }
360
+ /** Re-emit the pending prompt so devices re-sync after a dropped press. */
361
+ rebroadcastPendingApproval() {
362
+ const prompt = this.pendingApproval;
363
+ if (!prompt)
364
+ return;
365
+ this.emitAdapterEvent({
366
+ source: 'parser',
367
+ event: 'permission_prompt',
368
+ data: {
369
+ question: prompt.question,
370
+ options: prompt.options.map((o) => ({
371
+ index: o.index, label: o.label, shortcut: o.shortcut,
372
+ })),
373
+ navigable: false,
374
+ cursorIndex: 0,
375
+ },
376
+ });
377
+ }
378
+ /**
379
+ * Truncation-tolerant echo check. Devices cap the question they display
380
+ * (ESP32 buffers, 16-char key labels), so an exact compare would refuse every
381
+ * legitimate press from a small surface. Compare on UTF-16 units to match the
382
+ * TS producers; the Swift mirror compares `.utf16` for the same reason.
383
+ */
384
+ approvalEchoMatches(prompt, echo) {
385
+ const a = prompt.question.trim();
386
+ const b = echo.trim();
387
+ if (!b)
388
+ return true;
389
+ return a.startsWith(b) || b.startsWith(a);
390
+ }
243
391
  handleCommand(cmd) {
244
392
  switch (cmd.type) {
245
393
  case 'respond': {
246
394
  debug('adapter:openclaw', `respond: "${cmd.value}"`);
247
- if (this.pendingApprovalId) {
248
- const decision = cmd.value === 'y' ? 'allow' : 'deny';
249
- this.rpcCall('exec.approval.resolve', {
250
- id: this.pendingApprovalId,
251
- decision,
252
- }).catch((err) => {
253
- debug('adapter:openclaw', `exec.approval.resolve failed: ${err}`);
254
- });
255
- this.pendingApprovalId = null;
395
+ const prompt = this.pendingApproval;
396
+ if (prompt) {
397
+ const decision = decisionForRespondValue(prompt, cmd.value);
398
+ if (!decision) {
399
+ logError(`[adapter:openclaw] respond "${cmd.value}" does not name a decision this approval allows` +
400
+ ` (${prompt.options.map((o) => o.decision).join('/')}) — ignored`);
401
+ return true;
402
+ }
403
+ this.resolveApproval(prompt, decision);
256
404
  }
257
405
  return true;
258
406
  }
259
407
  case 'select_option': {
260
408
  debug('adapter:openclaw', `select_option: idx=${cmd.index}`);
261
- // Permission prompts: index 0 = Allow, index 1 = Deny
262
- if (this.pendingApprovalId) {
263
- const decision = cmd.index === 0 ? 'allow' : 'deny';
264
- this.rpcCall('exec.approval.resolve', {
265
- id: this.pendingApprovalId,
266
- decision,
267
- }).catch((err) => {
268
- debug('adapter:openclaw', `exec.approval.resolve failed: ${err}`);
269
- });
270
- this.pendingApprovalId = null;
409
+ const prompt = this.pendingApproval;
410
+ if (prompt) {
411
+ // The index names an option in the list the device was DISPLAYING.
412
+ // `question` echoes that list's headline; a press aimed at an
413
+ // approval the Gateway has already moved past must be dropped rather
414
+ // than applied to the new one — approving the wrong command is the
415
+ // single worst outcome this path can produce.
416
+ if (cmd.question && !this.approvalEchoMatches(prompt, cmd.question)) {
417
+ logError('[adapter:openclaw] select_option dropped: question echo does not match the pending approval');
418
+ this.rebroadcastPendingApproval();
419
+ return true;
420
+ }
421
+ const decision = decisionForOptionIndex(prompt, cmd.index);
422
+ if (!decision) {
423
+ logError(`[adapter:openclaw] select_option index ${cmd.index} out of range — ignored`);
424
+ return true;
425
+ }
426
+ this.resolveApproval(prompt, decision);
271
427
  }
272
428
  return true;
273
429
  }
@@ -530,6 +686,10 @@ export class OpenClawAdapter extends EventEmitter {
530
686
  debug('adapter:openclaw', 'Gateway disconnected');
531
687
  const wasAlive = this.alive;
532
688
  this.alive = false;
689
+ // Pending approvals are Gateway-process state: a reconnect issues new
690
+ // ids, so holding the old one would offer buttons that resolve to
691
+ // "unknown or expired approval id".
692
+ this.abandonPendingApprovalForTurnEnd('gateway disconnected');
533
693
  if (wasAlive) {
534
694
  this.emitAdapterEvent({ source: 'connection', status: 'disconnected' });
535
695
  this.emitAdapterEvent({ source: 'hook', event: 'SessionEnd', data: {} });
@@ -841,6 +1001,7 @@ export class OpenClawAdapter extends EventEmitter {
841
1001
  this.lastPrompt = null;
842
1002
  this.accumulatedResponse = '';
843
1003
  this.currentRunId = null;
1004
+ this.abandonPendingApprovalForTurnEnd('turn ended');
844
1005
  this.emitAdapterEvent({ source: 'parser', event: 'idle' });
845
1006
  break;
846
1007
  }
@@ -874,19 +1035,64 @@ export class OpenClawAdapter extends EventEmitter {
874
1035
  this.lastPrompt = null;
875
1036
  this.accumulatedResponse = '';
876
1037
  this.currentRunId = null;
1038
+ // A run cancelled while an approval was outstanding is the shape
1039
+ // that stranded the deck on PERMIT?: the Gateway drops the approval
1040
+ // silently, so this is the only place that can close it.
1041
+ this.abandonPendingApprovalForTurnEnd('run cancelled');
877
1042
  this.emitAdapterEvent({ source: 'parser', event: 'idle' });
878
1043
  break;
879
1044
  }
880
1045
  case 'error': {
881
- const errMsg = payload.errorMessage || 'unknown';
1046
+ // The Gateway's error frame reports only the user-facing message
1047
+ // plus `errorKind`/`stopReason`/`runId` — never the provider or
1048
+ // model that failed (OpenClaw's failover means the failing model
1049
+ // isn't the one the turn ends on anyway). So the row is built from
1050
+ // the frame PLUS this adapter's own turn counters; anything not
1051
+ // reported stays absent rather than getting guessed. `runId` is
1052
+ // the join key into the gateway log, where the provider/model and
1053
+ // the HTTP status do live.
1054
+ //
1055
+ // The frame spells the sentence `errorMessage` while
1056
+ // `ChatEventPayload` declares `error` — accept either rather than
1057
+ // silently losing it to a name mismatch.
1058
+ const errText = [payload.errorMessage, payload.error]
1059
+ .find((v) => typeof v === 'string' && v.trim().length > 0)?.trim();
1060
+ const errInput = {
1061
+ state: 'error',
1062
+ ...(errText ? { error: errText } : {}),
1063
+ ...(typeof payload.errorKind === 'string' ? { errorKind: payload.errorKind } : {}),
1064
+ ...(typeof payload.stopReason === 'string' ? { stopReason: payload.stopReason } : {}),
1065
+ ...(runId ? { runId } : {}),
1066
+ ...(sessionKey ? { sessionKey } : {}),
1067
+ durationSec: this.chatStarted ? Math.round((Date.now() - this.chatStartTime) / 1000) : 0,
1068
+ toolNames: [...this.chatToolNames],
1069
+ };
1070
+ const errLabel = openclawChatErrorLabel(errInput);
1071
+ const errDetail = openclawChatErrorDetail(errInput);
1072
+ const errTs = Date.now();
882
1073
  this.emitTimelineEntry({
883
- ts: Date.now(), type: 'error', raw: errMsg,
1074
+ ts: errTs, type: 'error', raw: errLabel,
1075
+ ...(errDetail ? { detail: errDetail } : {}),
1076
+ // Parity with the `final`/`aborted` rows: without this an
1077
+ // errored cron turn is indistinguishable from a user's.
1078
+ ...(this.chatIsAutomated ? { automated: true } : {}),
1079
+ ...(this.chatStarted ? { startedAt: this.chatStartTime, endedAt: errTs } : {}),
884
1080
  });
1081
+ // APME: record the failure in the trajectory, then re-arm the
1082
+ // idle-gap timer. `chat.send` cleared that timer and only `final`
1083
+ // used to re-arm it, so a prompt that errored and was then
1084
+ // abandoned left its task open indefinitely.
1085
+ const errCtx = this.buildApmeCtx();
1086
+ if (errCtx) {
1087
+ this.ingestApmeSpans(openclawChatEventToSpans(errCtx, errInput));
1088
+ this.armIdleGapTimer();
1089
+ }
885
1090
  this.chatStarted = false;
886
1091
  this.lastPrompt = null;
887
1092
  this.accumulatedResponse = '';
888
1093
  this.currentRunId = null;
889
- debug('adapter:openclaw', `Chat error: ${errMsg}`);
1094
+ debug('adapter:openclaw', `Chat error: ${errLabel}`);
1095
+ this.abandonPendingApprovalForTurnEnd('turn failed');
890
1096
  this.emitAdapterEvent({ source: 'parser', event: 'idle' });
891
1097
  break;
892
1098
  }
@@ -895,33 +1101,41 @@ export class OpenClawAdapter extends EventEmitter {
895
1101
  }
896
1102
  // ===== Tool approval =====
897
1103
  case 'exec.approval.requested': {
898
- const approvalId = payload.id;
899
- const command = payload.command;
900
- const ask = payload.ask;
901
- this.pendingApprovalId = approvalId;
1104
+ // The Gateway nests everything under `request` and describes the
1105
+ // permitted decisions per-request. Both facts are handled in the shared
1106
+ // parser reading this payload flat here is what left every approval
1107
+ // rendered as a bare "Approve tool execution?" with no command on it.
1108
+ const prompt = parseExecApprovalRequest(payload, Date.now());
1109
+ if (!prompt) {
1110
+ logError('[adapter:openclaw] exec.approval.requested without a usable id — ignored');
1111
+ break;
1112
+ }
1113
+ this.setPendingApproval(prompt);
902
1114
  // Track tool for chat summary
903
- const toolName = command?.split(' ')[0] || 'tool';
1115
+ const toolName = prompt.command.split(/\s+/)[0] || 'tool';
904
1116
  this.chatToolCount++;
905
1117
  if (!this.chatToolNames.includes(toolName)) {
906
1118
  this.chatToolNames.push(toolName);
907
1119
  }
908
- const toolRaw = ask ? `${command}: ${ask}` : (command || 'Approve tool execution?');
909
- const toolRequestRaw = toolRaw.length > 500 ? toolRaw.slice(0, 497) + '...' : toolRaw;
910
- const toolRequestDetail = ask && ask.length > 100 ? (ask.length > 1000 ? ask.slice(0, 997) + '...' : ask) : undefined;
1120
+ const toolRequestRaw = prompt.question.length > 500
1121
+ ? prompt.question.slice(0, 497) + '...'
1122
+ : prompt.question;
1123
+ const toolRequestDetail = prompt.detail && prompt.detail.length > 1000
1124
+ ? prompt.detail.slice(0, 997) + '...'
1125
+ : prompt.detail;
911
1126
  this.emitTimelineEntry({
912
1127
  ts: Date.now(), type: 'tool_request', raw: toolRequestRaw,
913
1128
  ...(toolRequestDetail ? { detail: toolRequestDetail } : {}),
914
- approvalId, status: 'pending',
1129
+ approvalId: prompt.id, status: 'pending',
915
1130
  });
916
1131
  this.emitAdapterEvent({
917
1132
  source: 'parser',
918
1133
  event: 'permission_prompt',
919
1134
  data: {
920
- question: ask || command || 'Approve tool execution?',
921
- options: [
922
- { index: 0, label: 'Allow', shortcut: 'y' },
923
- { index: 1, label: 'Deny', shortcut: 'n' },
924
- ],
1135
+ question: prompt.question,
1136
+ options: prompt.options.map((o) => ({
1137
+ index: o.index, label: o.label, shortcut: o.shortcut,
1138
+ })),
925
1139
  navigable: false,
926
1140
  cursorIndex: 0,
927
1141
  },
@@ -929,19 +1143,32 @@ export class OpenClawAdapter extends EventEmitter {
929
1143
  break;
930
1144
  }
931
1145
  case 'exec.approval.resolved': {
932
- // Approval resolved (by us or another client) processing continues
933
- const resolvedId = this.pendingApprovalId;
934
- const decision = payload.decision || 'allow';
935
- this.pendingApprovalId = null;
1146
+ // Resolved by us or by another operator client. Key off the payload's
1147
+ // own id, not our pending pointer: a resolution can arrive for an
1148
+ // approval we already dropped (expiry, abort), and treating it as
1149
+ // "whatever we last saw" mislabels an unrelated row.
1150
+ const resolvedId = typeof payload.id === 'string' && payload.id
1151
+ ? payload.id
1152
+ : this.pendingApproval?.id;
1153
+ const decision = typeof payload.decision === 'string' ? payload.decision : '';
1154
+ const allowed = execApprovalAllows(decision);
1155
+ if (this.pendingApproval && resolvedId === this.pendingApproval.id) {
1156
+ this.clearPendingApproval();
1157
+ }
936
1158
  if (resolvedId) {
937
1159
  this.emitTimelineEntry({
938
1160
  ts: Date.now(), type: 'tool_resolved',
939
- raw: decision === 'allow' ? 'Approved' : 'Denied',
1161
+ // `decision` is allow-once / allow-always / deny. Comparing it to
1162
+ // 'allow' (as this did) labeled every real approval "Denied".
1163
+ raw: decision === 'allow-always' ? 'Approved (always)'
1164
+ : allowed ? 'Approved' : 'Denied',
940
1165
  approvalId: resolvedId,
941
- status: decision === 'allow' ? 'approved' : 'denied',
1166
+ status: allowed ? 'approved' : 'denied',
942
1167
  });
943
1168
  }
944
- this.emitAdapterEvent({ source: 'parser', event: 'spinner_start' });
1169
+ // Denial does not resume the turn — the tool call fails and the agent
1170
+ // either recovers or ends. Only an allow means work continues.
1171
+ this.emitAdapterEvent({ source: 'parser', event: allowed ? 'spinner_start' : 'idle' });
945
1172
  break;
946
1173
  }
947
1174
  // ===== Gateway health =====
@@ -1062,6 +1289,12 @@ export class OpenClawAdapter extends EventEmitter {
1062
1289
  this.fetchSessions().catch(err => debug('adapter:openclaw', `fetchSessions error: ${err}`));
1063
1290
  // Fetch model catalog (async, non-blocking)
1064
1291
  this.emitModelCatalog().catch(err => debug('adapter:openclaw', `emitModelCatalog error: ${err}`));
1292
+ // Catch up on approvals that were already waiting. `exec.approval.requested`
1293
+ // is a broadcast and is never replayed, so without this read a daemon
1294
+ // restart (or any Gateway reconnect) leaves the agent blocked on an
1295
+ // approval that exists on exactly one side: the user sees an idle deck
1296
+ // and the agent never moves.
1297
+ this.adoptPendingApprovals().catch(err => debug('adapter:openclaw', `adoptPendingApprovals error: ${err}`));
1065
1298
  },
1066
1299
  reject: (err) => {
1067
1300
  debug('adapter:openclaw', `Handshake failed: ${err.message}`);