@agentdeck/bridge 1.0.24 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/openclaw.d.ts +74 -4
- package/dist/adapters/openclaw.d.ts.map +1 -1
- package/dist/adapters/openclaw.js +235 -43
- package/dist/adapters/openclaw.js.map +1 -1
- package/dist/apme/adapters/openclaw-hook.d.ts +38 -10
- package/dist/apme/adapters/openclaw-hook.d.ts.map +1 -1
- package/dist/apme/adapters/openclaw-hook.js +181 -37
- package/dist/apme/adapters/openclaw-hook.js.map +1 -1
- package/dist/apme/collector.d.ts +37 -1
- package/dist/apme/collector.d.ts.map +1 -1
- package/dist/apme/collector.js +277 -35
- package/dist/apme/collector.js.map +1 -1
- package/dist/apme/pareto.d.ts +2 -1
- package/dist/apme/pareto.d.ts.map +1 -1
- package/dist/apme/pareto.js +15 -4
- package/dist/apme/pareto.js.map +1 -1
- package/dist/apme/recommend.d.ts +24 -1
- package/dist/apme/recommend.d.ts.map +1 -1
- package/dist/apme/recommend.js +43 -7
- package/dist/apme/recommend.js.map +1 -1
- package/dist/apme/store.d.ts +6 -0
- package/dist/apme/store.d.ts.map +1 -1
- package/dist/apme/store.js +244 -72
- package/dist/apme/store.js.map +1 -1
- package/dist/bridge-core.d.ts +10 -1
- package/dist/bridge-core.d.ts.map +1 -1
- package/dist/bridge-core.js +20 -5
- package/dist/bridge-core.js.map +1 -1
- package/dist/card-feed.d.ts +46 -1
- package/dist/card-feed.d.ts.map +1 -1
- package/dist/card-feed.js +166 -6
- package/dist/card-feed.js.map +1 -1
- package/dist/cli.d.ts +45 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +388 -27
- package/dist/cli.js.map +1 -1
- package/dist/daemon-build-identity.d.ts +124 -0
- package/dist/daemon-build-identity.d.ts.map +1 -0
- package/dist/daemon-build-identity.js +310 -0
- package/dist/daemon-build-identity.js.map +1 -0
- package/dist/daemon-server.d.ts +34 -6
- package/dist/daemon-server.d.ts.map +1 -1
- package/dist/daemon-server.js +613 -108
- package/dist/daemon-server.js.map +1 -1
- package/dist/daemon-takeover.d.ts +11 -0
- package/dist/daemon-takeover.d.ts.map +1 -1
- package/dist/daemon-takeover.js +62 -17
- package/dist/daemon-takeover.js.map +1 -1
- package/dist/daemon.js +1 -1
- package/dist/daemon.js.map +1 -1
- package/dist/esp32-serial.d.ts +14 -0
- package/dist/esp32-serial.d.ts.map +1 -1
- package/dist/esp32-serial.js +19 -1
- package/dist/esp32-serial.js.map +1 -1
- package/dist/glance-frame.d.ts.map +1 -1
- package/dist/glance-frame.js +3 -1
- package/dist/glance-frame.js.map +1 -1
- package/dist/hook-codex-sessions.d.ts +3 -1
- package/dist/hook-codex-sessions.d.ts.map +1 -1
- package/dist/hook-codex-sessions.js +27 -7
- package/dist/hook-codex-sessions.js.map +1 -1
- package/dist/legacy-rearm-ledger.d.ts +56 -0
- package/dist/legacy-rearm-ledger.d.ts.map +1 -0
- package/dist/legacy-rearm-ledger.js +66 -0
- package/dist/legacy-rearm-ledger.js.map +1 -0
- package/dist/mdns.d.ts +33 -0
- package/dist/mdns.d.ts.map +1 -1
- package/dist/mdns.js +91 -2
- package/dist/mdns.js.map +1 -1
- package/dist/openclaw-session.d.ts.map +1 -1
- package/dist/openclaw-session.js +13 -0
- package/dist/openclaw-session.js.map +1 -1
- package/dist/openclaw-timeline-feed.d.ts +84 -0
- package/dist/openclaw-timeline-feed.d.ts.map +1 -0
- package/dist/openclaw-timeline-feed.js +162 -0
- package/dist/openclaw-timeline-feed.js.map +1 -0
- package/dist/openclaw-transcript-timeline.d.ts +103 -0
- package/dist/openclaw-transcript-timeline.d.ts.map +1 -0
- package/dist/openclaw-transcript-timeline.js +328 -0
- package/dist/openclaw-transcript-timeline.js.map +1 -0
- package/dist/passive-observer.d.ts +21 -1
- package/dist/passive-observer.d.ts.map +1 -1
- package/dist/passive-observer.js +37 -3
- package/dist/passive-observer.js.map +1 -1
- package/dist/relayed-usage.d.ts +68 -0
- package/dist/relayed-usage.d.ts.map +1 -0
- package/dist/relayed-usage.js +80 -0
- package/dist/relayed-usage.js.map +1 -0
- package/dist/session-aggregator.d.ts +3 -0
- package/dist/session-aggregator.d.ts.map +1 -1
- package/dist/session-aggregator.js.map +1 -1
- package/dist/session-registry.d.ts +1 -0
- package/dist/session-registry.d.ts.map +1 -1
- package/dist/surface-protocol.d.ts +84 -0
- package/dist/surface-protocol.d.ts.map +1 -0
- package/dist/surface-protocol.js +235 -0
- package/dist/surface-protocol.js.map +1 -0
- package/dist/timeline-store.d.ts +1 -1
- package/dist/timeline-store.d.ts.map +1 -1
- package/dist/timeline-store.js +16 -1
- package/dist/timeline-store.js.map +1 -1
- package/dist/tui/renderer.d.ts.map +1 -1
- package/dist/tui/renderer.js +35 -0
- package/dist/tui/renderer.js.map +1 -1
- package/dist/weather-config.d.ts +17 -0
- package/dist/weather-config.d.ts.map +1 -0
- package/dist/weather-config.js +54 -0
- package/dist/weather-config.js.map +1 -0
- package/dist/weather.d.ts +44 -4
- package/dist/weather.d.ts.map +1 -1
- package/dist/weather.js +223 -15
- package/dist/weather.js.map +1 -1
- package/dist/ws-server.d.ts.map +1 -1
- package/dist/ws-server.js +55 -5
- package/dist/ws-server.js.map +1 -1
- package/fm-helper/AgentDeckFMHelper.swift +51 -5
- package/package.json +3 -3
|
@@ -58,10 +58,29 @@ export declare class OpenClawAdapter extends EventEmitter implements AgentAdapte
|
|
|
58
58
|
private apmeSessionId;
|
|
59
59
|
private apmeTraceId;
|
|
60
60
|
private apmeCwdHint;
|
|
61
|
-
/**
|
|
62
|
-
*
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
/** Opens (or returns) the APME run for one Gateway session key. Supplied by
|
|
62
|
+
* the daemon, which owns the collector. */
|
|
63
|
+
private apmeRunFor;
|
|
64
|
+
/** Per-Gateway-session-key APME state.
|
|
65
|
+
*
|
|
66
|
+
* **Why this is keyed by session key and not by connection.** One Gateway
|
|
67
|
+
* connection sees every session the agent has: the chat the user is looking
|
|
68
|
+
* at (`agent:main:main`), each cron job (`agent:main:cron:<id>`), and one
|
|
69
|
+
* fresh key per model-eval run (`agent:main:eval-…__r2` — a suite opens six
|
|
70
|
+
* in three hours). A run scoped to the connection therefore interleaves all
|
|
71
|
+
* of them into one trajectory under one `model_id`, and the idle-gap
|
|
72
|
+
* boundary is computed across traffic from conversations that have nothing
|
|
73
|
+
* to do with each other. Turn ordering, per-task cost and every judge
|
|
74
|
+
* verdict derived from them describe a conversation that never happened.
|
|
75
|
+
*
|
|
76
|
+
* Each entry owns its own idle-gap timer for the same reason: a cron
|
|
77
|
+
* heartbeat firing every few minutes would otherwise keep resetting the
|
|
78
|
+
* timer for a user chat that ended half an hour ago, and that task would
|
|
79
|
+
* never close — a task that never closes is never evaluated. */
|
|
80
|
+
private apmeBySessionKey;
|
|
81
|
+
/** Idle-gap timer for frames that arrive without a session key (the
|
|
82
|
+
* connection-scoped fallback run). */
|
|
83
|
+
private apmeFallbackIdleTimer;
|
|
65
84
|
/** Reconnect delay (doubles on each failure, max 30s) */
|
|
66
85
|
private reconnectDelay;
|
|
67
86
|
private static readonly MAX_RECONNECT_DELAY;
|
|
@@ -70,17 +89,52 @@ export declare class OpenClawAdapter extends EventEmitter implements AgentAdapte
|
|
|
70
89
|
/** Bind the adapter to an APME session id so Gateway events can be
|
|
71
90
|
* ingested as TelemetrySpans. No-op when APME isn't initialized. */
|
|
72
91
|
setApmeSession(sessionId: string, cwd?: string): void;
|
|
92
|
+
/** Install the per-session-key run opener. The daemon owns the collector, so
|
|
93
|
+
* the adapter asks it for the APME session id belonging to a Gateway
|
|
94
|
+
* session key and caches the answer. Returning `null` (APME disabled, or
|
|
95
|
+
* `openRun` threw) makes that key fall back to the connection-scoped id,
|
|
96
|
+
* which is the pre-split behaviour rather than silence. */
|
|
97
|
+
setApmeRunResolver(resolve: (sessionKey: string) => string | null): void;
|
|
98
|
+
/** APME session ids this adapter has opened, so the daemon can close them
|
|
99
|
+
* all when the Gateway link drops. */
|
|
100
|
+
apmeSessionIds(): string[];
|
|
73
101
|
/** True when this adapter pipes Gateway events into the APME collector
|
|
74
102
|
* itself — the bridge must then NOT also convert its timeline entries to
|
|
75
103
|
* spans, or every turn and tool would be counted twice. */
|
|
76
104
|
hasDirectApmeIngestion(): boolean;
|
|
105
|
+
/** Resolve the APME context for a Gateway session key.
|
|
106
|
+
*
|
|
107
|
+
* A frame with no session key falls back to the connection-scoped id — that
|
|
108
|
+
* is what the adapter had before the split, and dropping such a frame would
|
|
109
|
+
* trade a mis-scoped record for no record at all. */
|
|
77
110
|
private buildApmeCtx;
|
|
111
|
+
/** Get-or-create the per-session-key entry — WITHOUT opening its APME run.
|
|
112
|
+
*
|
|
113
|
+
* Opening the run here is what a first version did, and it manufactured
|
|
114
|
+
* exactly the noise this whole split exists to remove: a context is built
|
|
115
|
+
* for every frame, including ones that parse to zero spans, so a real
|
|
116
|
+
* `agent:main:main:heartbeat` key opened a run holding 0 turns, 0 steps and
|
|
117
|
+
* 0 events (measured on live traffic 2026-08-23). The Gateway filters
|
|
118
|
+
* heartbeat message pairs out of `session.message`, so such a key can emit
|
|
119
|
+
* frames that carry nothing to record. A run is opened on the first span
|
|
120
|
+
* that is actually ingested. */
|
|
121
|
+
private apmeEntryFor;
|
|
122
|
+
/** The APME session id for a key, opening its run on first use. */
|
|
123
|
+
private apmeSessionIdFor;
|
|
124
|
+
/** Ingest spans against the run belonging to `sessionKey`.
|
|
125
|
+
*
|
|
126
|
+
* The target must be re-resolved from the key, never read off
|
|
127
|
+
* `this.apmeSessionId` — reading that field here is what would silently
|
|
128
|
+
* re-collapse every per-key run back into one. Returns early on an empty
|
|
129
|
+
* span list so no run is opened for a frame that records nothing. */
|
|
78
130
|
private ingestApmeSpans;
|
|
79
131
|
/** Reset (or start) the idle-gap timer. Fires
|
|
80
132
|
* `openclawIdleGapTaskBoundary` after OPENCLAW_IDLE_GAP_MS of silence
|
|
81
133
|
* (no new `chat.send`) following the last `chat.final`. */
|
|
82
134
|
private armIdleGapTimer;
|
|
83
135
|
private clearIdleGapTimer;
|
|
136
|
+
/** Clear every per-key timer (shutdown). */
|
|
137
|
+
private clearAllIdleGapTimers;
|
|
84
138
|
start(options: AdapterStartOptions & {
|
|
85
139
|
externalServer?: Server;
|
|
86
140
|
}): Promise<void>;
|
|
@@ -162,6 +216,22 @@ export declare class OpenClawAdapter extends EventEmitter implements AgentAdapte
|
|
|
162
216
|
* Called when Gateway sends `connect.challenge` event with nonce.
|
|
163
217
|
*/
|
|
164
218
|
private sendConnectRequest;
|
|
219
|
+
/** Join the Gateway's session event stream (`sessions.subscribe`, scope
|
|
220
|
+
* `operator.read`).
|
|
221
|
+
*
|
|
222
|
+
* The Gateway's recipient set for `session.message` is
|
|
223
|
+
* `sessionEventSubscribers ∪ sessionMessageSubscribers`, and the former is
|
|
224
|
+
* unioned in unconditionally — so this one no-param call delivers the
|
|
225
|
+
* messages of EVERY session, which is what we want: AgentDeck observes the
|
|
226
|
+
* whole agent, not one chat. `sessions.messages.subscribe` is the per-key
|
|
227
|
+
* alternative and would need a subscribe/unsubscribe cycle for every
|
|
228
|
+
* session key the store mints (an eval suite opens a fresh one per run), so
|
|
229
|
+
* it is the wrong shape here.
|
|
230
|
+
*
|
|
231
|
+
* Failure is non-fatal and stays visible: without the subscription the
|
|
232
|
+
* adapter degrades to exactly its previous behaviour (chat + approvals
|
|
233
|
+
* only), which is a smaller timeline, not a broken one. */
|
|
234
|
+
private subscribeSessionEvents;
|
|
165
235
|
/** Fetch sessions and set the most recently updated one as active. */
|
|
166
236
|
private fetchSessions;
|
|
167
237
|
/** Fetch model catalog via CLI and emit events. Retries once on failure. */
|
|
@@ -1 +1 @@
|
|
|
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
|
|
1
|
+
{"version":3,"file":"openclaw.d.ts","sourceRoot":"","sources":["../../src/adapters/openclaw.ts"],"names":[],"mappings":"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"}
|
|
@@ -94,10 +94,29 @@ export class OpenClawAdapter extends EventEmitter {
|
|
|
94
94
|
apmeSessionId = null;
|
|
95
95
|
apmeTraceId = randomUUID();
|
|
96
96
|
apmeCwdHint;
|
|
97
|
-
/**
|
|
98
|
-
*
|
|
99
|
-
|
|
100
|
-
|
|
97
|
+
/** Opens (or returns) the APME run for one Gateway session key. Supplied by
|
|
98
|
+
* the daemon, which owns the collector. */
|
|
99
|
+
apmeRunFor = null;
|
|
100
|
+
/** Per-Gateway-session-key APME state.
|
|
101
|
+
*
|
|
102
|
+
* **Why this is keyed by session key and not by connection.** One Gateway
|
|
103
|
+
* connection sees every session the agent has: the chat the user is looking
|
|
104
|
+
* at (`agent:main:main`), each cron job (`agent:main:cron:<id>`), and one
|
|
105
|
+
* fresh key per model-eval run (`agent:main:eval-…__r2` — a suite opens six
|
|
106
|
+
* in three hours). A run scoped to the connection therefore interleaves all
|
|
107
|
+
* of them into one trajectory under one `model_id`, and the idle-gap
|
|
108
|
+
* boundary is computed across traffic from conversations that have nothing
|
|
109
|
+
* to do with each other. Turn ordering, per-task cost and every judge
|
|
110
|
+
* verdict derived from them describe a conversation that never happened.
|
|
111
|
+
*
|
|
112
|
+
* Each entry owns its own idle-gap timer for the same reason: a cron
|
|
113
|
+
* heartbeat firing every few minutes would otherwise keep resetting the
|
|
114
|
+
* timer for a user chat that ended half an hour ago, and that task would
|
|
115
|
+
* never close — a task that never closes is never evaluated. */
|
|
116
|
+
apmeBySessionKey = new Map();
|
|
117
|
+
/** Idle-gap timer for frames that arrive without a session key (the
|
|
118
|
+
* connection-scoped fallback run). */
|
|
119
|
+
apmeFallbackIdleTimer = null;
|
|
101
120
|
/** Reconnect delay (doubles on each failure, max 30s) */
|
|
102
121
|
reconnectDelay = 1000;
|
|
103
122
|
static MAX_RECONNECT_DELAY = 30_000;
|
|
@@ -155,30 +174,103 @@ export class OpenClawAdapter extends EventEmitter {
|
|
|
155
174
|
this.apmeSessionId = sessionId;
|
|
156
175
|
this.apmeCwdHint = cwd;
|
|
157
176
|
}
|
|
177
|
+
/** Install the per-session-key run opener. The daemon owns the collector, so
|
|
178
|
+
* the adapter asks it for the APME session id belonging to a Gateway
|
|
179
|
+
* session key and caches the answer. Returning `null` (APME disabled, or
|
|
180
|
+
* `openRun` threw) makes that key fall back to the connection-scoped id,
|
|
181
|
+
* which is the pre-split behaviour rather than silence. */
|
|
182
|
+
setApmeRunResolver(resolve) {
|
|
183
|
+
this.apmeRunFor = resolve;
|
|
184
|
+
}
|
|
185
|
+
/** APME session ids this adapter has opened, so the daemon can close them
|
|
186
|
+
* all when the Gateway link drops. */
|
|
187
|
+
apmeSessionIds() {
|
|
188
|
+
return [...this.apmeBySessionKey.values()]
|
|
189
|
+
.map((e) => e.sessionId)
|
|
190
|
+
.filter((id) => id !== null);
|
|
191
|
+
}
|
|
158
192
|
/** True when this adapter pipes Gateway events into the APME collector
|
|
159
193
|
* itself — the bridge must then NOT also convert its timeline entries to
|
|
160
194
|
* spans, or every turn and tool would be counted twice. */
|
|
161
195
|
hasDirectApmeIngestion() {
|
|
162
196
|
return this.apmeSessionId != null && getApme() != null;
|
|
163
197
|
}
|
|
164
|
-
|
|
165
|
-
|
|
198
|
+
/** Resolve the APME context for a Gateway session key.
|
|
199
|
+
*
|
|
200
|
+
* A frame with no session key falls back to the connection-scoped id — that
|
|
201
|
+
* is what the adapter had before the split, and dropping such a frame would
|
|
202
|
+
* trade a mis-scoped record for no record at all. */
|
|
203
|
+
buildApmeCtx(sessionKey) {
|
|
204
|
+
if (!getApme())
|
|
205
|
+
return null;
|
|
206
|
+
const entry = sessionKey ? this.apmeEntryFor(sessionKey) : null;
|
|
207
|
+
// `sessionId` here is provisional: for a keyed context the run may not be
|
|
208
|
+
// open yet (see `apmeEntryFor`), and `ingestApmeSpans` re-resolves it
|
|
209
|
+
// against `sessionKey` before writing anything. It is never read into a
|
|
210
|
+
// span attribute — only used to route the ingest.
|
|
211
|
+
const sessionId = entry?.sessionId ?? this.apmeSessionId;
|
|
212
|
+
if (!sessionId && !entry)
|
|
166
213
|
return null;
|
|
167
214
|
return {
|
|
168
|
-
sessionId:
|
|
215
|
+
sessionId: sessionId ?? '',
|
|
169
216
|
agentType: 'openclaw',
|
|
170
217
|
cwd: this.apmeCwdHint,
|
|
171
|
-
traceId: this.apmeTraceId,
|
|
218
|
+
traceId: entry?.traceId ?? this.apmeTraceId,
|
|
172
219
|
activeTurnId: undefined,
|
|
173
220
|
};
|
|
174
221
|
}
|
|
175
|
-
|
|
222
|
+
/** Get-or-create the per-session-key entry — WITHOUT opening its APME run.
|
|
223
|
+
*
|
|
224
|
+
* Opening the run here is what a first version did, and it manufactured
|
|
225
|
+
* exactly the noise this whole split exists to remove: a context is built
|
|
226
|
+
* for every frame, including ones that parse to zero spans, so a real
|
|
227
|
+
* `agent:main:main:heartbeat` key opened a run holding 0 turns, 0 steps and
|
|
228
|
+
* 0 events (measured on live traffic 2026-08-23). The Gateway filters
|
|
229
|
+
* heartbeat message pairs out of `session.message`, so such a key can emit
|
|
230
|
+
* frames that carry nothing to record. A run is opened on the first span
|
|
231
|
+
* that is actually ingested. */
|
|
232
|
+
apmeEntryFor(sessionKey) {
|
|
233
|
+
const existing = this.apmeBySessionKey.get(sessionKey);
|
|
234
|
+
if (existing)
|
|
235
|
+
return existing;
|
|
236
|
+
if (!this.apmeRunFor)
|
|
237
|
+
return null;
|
|
238
|
+
const entry = { sessionId: null, traceId: randomUUID(), idleTimer: null };
|
|
239
|
+
this.apmeBySessionKey.set(sessionKey, entry);
|
|
240
|
+
return entry;
|
|
241
|
+
}
|
|
242
|
+
/** The APME session id for a key, opening its run on first use. */
|
|
243
|
+
apmeSessionIdFor(sessionKey) {
|
|
244
|
+
const entry = this.apmeEntryFor(sessionKey);
|
|
245
|
+
if (!entry)
|
|
246
|
+
return null;
|
|
247
|
+
if (entry.sessionId)
|
|
248
|
+
return entry.sessionId;
|
|
249
|
+
const sessionId = this.apmeRunFor?.(sessionKey) ?? null;
|
|
250
|
+
if (!sessionId)
|
|
251
|
+
return null;
|
|
252
|
+
entry.sessionId = sessionId;
|
|
253
|
+
debug('adapter:openclaw', `APME run opened for ${sessionKey} → ${sessionId.slice(0, 16)}`);
|
|
254
|
+
return sessionId;
|
|
255
|
+
}
|
|
256
|
+
/** Ingest spans against the run belonging to `sessionKey`.
|
|
257
|
+
*
|
|
258
|
+
* The target must be re-resolved from the key, never read off
|
|
259
|
+
* `this.apmeSessionId` — reading that field here is what would silently
|
|
260
|
+
* re-collapse every per-key run back into one. Returns early on an empty
|
|
261
|
+
* span list so no run is opened for a frame that records nothing. */
|
|
262
|
+
ingestApmeSpans(sessionKey, spans) {
|
|
176
263
|
const apme = getApme();
|
|
177
|
-
if (!apme ||
|
|
264
|
+
if (!apme || spans.length === 0)
|
|
265
|
+
return;
|
|
266
|
+
const target = sessionKey
|
|
267
|
+
? this.apmeSessionIdFor(sessionKey) ?? this.apmeSessionId
|
|
268
|
+
: this.apmeSessionId;
|
|
269
|
+
if (!target)
|
|
178
270
|
return;
|
|
179
271
|
for (const span of spans) {
|
|
180
272
|
try {
|
|
181
|
-
apme.collector.ingestSpan(
|
|
273
|
+
apme.collector.ingestSpan(target, span);
|
|
182
274
|
}
|
|
183
275
|
catch (err) {
|
|
184
276
|
debug('apme:openclaw', `ingestSpan failed: ${String(err)}`);
|
|
@@ -188,19 +280,29 @@ export class OpenClawAdapter extends EventEmitter {
|
|
|
188
280
|
/** Reset (or start) the idle-gap timer. Fires
|
|
189
281
|
* `openclawIdleGapTaskBoundary` after OPENCLAW_IDLE_GAP_MS of silence
|
|
190
282
|
* (no new `chat.send`) following the last `chat.final`. */
|
|
191
|
-
armIdleGapTimer() {
|
|
192
|
-
this.clearIdleGapTimer();
|
|
193
|
-
const ctx = this.buildApmeCtx();
|
|
283
|
+
armIdleGapTimer(sessionKey) {
|
|
284
|
+
this.clearIdleGapTimer(sessionKey);
|
|
285
|
+
const ctx = this.buildApmeCtx(sessionKey);
|
|
194
286
|
if (!ctx)
|
|
195
287
|
return;
|
|
196
|
-
|
|
197
|
-
|
|
288
|
+
const holder = sessionKey ? this.apmeBySessionKey.get(sessionKey) : null;
|
|
289
|
+
const timer = setTimeout(() => {
|
|
290
|
+
if (holder)
|
|
291
|
+
holder.idleTimer = null;
|
|
292
|
+
else
|
|
293
|
+
this.apmeFallbackIdleTimer = null;
|
|
198
294
|
const apme = getApme();
|
|
199
|
-
if (!apme
|
|
295
|
+
if (!apme)
|
|
296
|
+
return;
|
|
297
|
+
// Resolve at FIRE time, and never open a run here: a boundary closing a
|
|
298
|
+
// task that was never opened would mint an empty run at the exact moment
|
|
299
|
+
// the conversation went quiet.
|
|
300
|
+
const target = holder?.sessionId ?? (sessionKey ? null : this.apmeSessionId);
|
|
301
|
+
if (!target)
|
|
200
302
|
return;
|
|
201
303
|
const span = openclawIdleGapTaskBoundary(ctx);
|
|
202
304
|
try {
|
|
203
|
-
apme.collector.ingestSpan(
|
|
305
|
+
apme.collector.ingestSpan(target, span);
|
|
204
306
|
}
|
|
205
307
|
catch (err) {
|
|
206
308
|
debug('apme:openclaw', `idle_gap ingestSpan failed: ${String(err)}`);
|
|
@@ -208,14 +310,35 @@ export class OpenClawAdapter extends EventEmitter {
|
|
|
208
310
|
}, OPENCLAW_IDLE_GAP_MS);
|
|
209
311
|
// Don't keep the event loop alive just for this timer; the adapter is
|
|
210
312
|
// already pinning the loop via WebSocket.
|
|
211
|
-
|
|
313
|
+
timer.unref?.();
|
|
314
|
+
if (holder)
|
|
315
|
+
holder.idleTimer = timer;
|
|
316
|
+
else
|
|
317
|
+
this.apmeFallbackIdleTimer = timer;
|
|
212
318
|
}
|
|
213
|
-
clearIdleGapTimer() {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
319
|
+
clearIdleGapTimer(sessionKey) {
|
|
320
|
+
const holder = sessionKey ? this.apmeBySessionKey.get(sessionKey) : null;
|
|
321
|
+
if (holder) {
|
|
322
|
+
if (holder.idleTimer)
|
|
323
|
+
clearTimeout(holder.idleTimer);
|
|
324
|
+
holder.idleTimer = null;
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (this.apmeFallbackIdleTimer) {
|
|
328
|
+
clearTimeout(this.apmeFallbackIdleTimer);
|
|
329
|
+
this.apmeFallbackIdleTimer = null;
|
|
217
330
|
}
|
|
218
331
|
}
|
|
332
|
+
/** Clear every per-key timer (shutdown). */
|
|
333
|
+
clearAllIdleGapTimers() {
|
|
334
|
+
for (const entry of this.apmeBySessionKey.values()) {
|
|
335
|
+
if (entry.idleTimer)
|
|
336
|
+
clearTimeout(entry.idleTimer);
|
|
337
|
+
entry.idleTimer = null;
|
|
338
|
+
}
|
|
339
|
+
// ...and the keyless fallback timer.
|
|
340
|
+
this.clearIdleGapTimer();
|
|
341
|
+
}
|
|
219
342
|
async start(options) {
|
|
220
343
|
if (options.gatewayUrl) {
|
|
221
344
|
this.gatewayUrl = options.gatewayUrl;
|
|
@@ -290,7 +413,16 @@ export class OpenClawAdapter extends EventEmitter {
|
|
|
290
413
|
}
|
|
291
414
|
setPendingApproval(prompt) {
|
|
292
415
|
this.clearPendingApproval();
|
|
293
|
-
|
|
416
|
+
// Which OpenClaw session asked is the single most orienting fact about an
|
|
417
|
+
// approval — `agent:main:main` (the chat the user is looking at) vs
|
|
418
|
+
// `agent:main:cron:<id>` vs `agent:main:eval-…__r2` are wildly different
|
|
419
|
+
// things to approve, and the deck row is pinned to the literal "OpenClaw"
|
|
420
|
+
// for all of them. The Gateway does not always put `sessionKey` on the
|
|
421
|
+
// approval body, but the adapter has been tracking it off the chat stream
|
|
422
|
+
// all along, so fall back to that rather than surfacing nothing.
|
|
423
|
+
this.pendingApproval = prompt.sessionKey || !this.currentSessionKey
|
|
424
|
+
? prompt
|
|
425
|
+
: { ...prompt, sessionKey: this.currentSessionKey };
|
|
294
426
|
if (typeof prompt.expiresAtMs === 'number') {
|
|
295
427
|
const delay = prompt.expiresAtMs - Date.now();
|
|
296
428
|
// An already-expired stamp still needs the sweep to run (on the next
|
|
@@ -319,7 +451,12 @@ export class OpenClawAdapter extends EventEmitter {
|
|
|
319
451
|
this.clearPendingApproval();
|
|
320
452
|
this.emitTimelineEntry({
|
|
321
453
|
ts: Date.now(), type: 'tool_resolved', raw: `Not approved · ${reason}`,
|
|
322
|
-
|
|
454
|
+
// NOT `denied` — nobody refused this. It expired, its run was cancelled,
|
|
455
|
+
// its turn ended, or the link dropped. Writing `denied` here made a user's
|
|
456
|
+
// deliberate refusal byte-identical to an approval that was never
|
|
457
|
+
// answered, which is the commoner case by far and the one that means the
|
|
458
|
+
// prompt failed to reach a human in time. `raw` names which of the four.
|
|
459
|
+
approvalId, status: 'abandoned',
|
|
323
460
|
});
|
|
324
461
|
this.emitAdapterEvent({ source: 'parser', event: 'idle' });
|
|
325
462
|
}
|
|
@@ -453,10 +590,10 @@ export class OpenClawAdapter extends EventEmitter {
|
|
|
453
590
|
// APME: open a turn. The matching `turn_response` lands on
|
|
454
591
|
// `chat.final` below. Cancel any pending idle-gap timer — a new
|
|
455
592
|
// send means the conversation is still active.
|
|
456
|
-
const ctx = this.buildApmeCtx();
|
|
593
|
+
const ctx = this.buildApmeCtx(this.currentSessionKey);
|
|
457
594
|
if (ctx) {
|
|
458
|
-
this.clearIdleGapTimer();
|
|
459
|
-
this.ingestApmeSpans([openclawChatSendToSpan(ctx, cmd.text)]);
|
|
595
|
+
this.clearIdleGapTimer(this.currentSessionKey);
|
|
596
|
+
this.ingestApmeSpans(this.currentSessionKey, [openclawChatSendToSpan(ctx, cmd.text)]);
|
|
460
597
|
}
|
|
461
598
|
this.rpcCall('chat.send', {
|
|
462
599
|
sessionKey: this.currentSessionKey,
|
|
@@ -560,7 +697,7 @@ export class OpenClawAdapter extends EventEmitter {
|
|
|
560
697
|
this.reconnectTimer = null;
|
|
561
698
|
}
|
|
562
699
|
// Cancel any pending idle-gap timer so it doesn't fire after shutdown.
|
|
563
|
-
this.
|
|
700
|
+
this.clearAllIdleGapTimers();
|
|
564
701
|
for (const [id, pending] of this.pendingRpc) {
|
|
565
702
|
pending.reject(new Error('Adapter shutting down'));
|
|
566
703
|
this.pendingRpc.delete(id);
|
|
@@ -985,7 +1122,7 @@ export class OpenClawAdapter extends EventEmitter {
|
|
|
985
1122
|
// OPENCLAW_IDLE_GAP_MS the timer is cancelled (above in
|
|
986
1123
|
// `send_prompt`); otherwise it fires `task_boundary` (idle_gap)
|
|
987
1124
|
// and closes the task.
|
|
988
|
-
const apmeCtx = this.buildApmeCtx();
|
|
1125
|
+
const apmeCtx = this.buildApmeCtx(sessionKey);
|
|
989
1126
|
if (apmeCtx) {
|
|
990
1127
|
const apmePayload = {
|
|
991
1128
|
state: 'final',
|
|
@@ -994,8 +1131,8 @@ export class OpenClawAdapter extends EventEmitter {
|
|
|
994
1131
|
...(responseContent ? { response: responseContent } : {}),
|
|
995
1132
|
...(Array.isArray(payload.tools) ? { tools: payload.tools } : {}),
|
|
996
1133
|
};
|
|
997
|
-
this.ingestApmeSpans(openclawChatEventToSpans(apmeCtx, apmePayload));
|
|
998
|
-
this.armIdleGapTimer();
|
|
1134
|
+
this.ingestApmeSpans(sessionKey, openclawChatEventToSpans(apmeCtx, apmePayload));
|
|
1135
|
+
this.armIdleGapTimer(sessionKey);
|
|
999
1136
|
}
|
|
1000
1137
|
this.chatStarted = false;
|
|
1001
1138
|
this.lastPrompt = null;
|
|
@@ -1022,10 +1159,10 @@ export class OpenClawAdapter extends EventEmitter {
|
|
|
1022
1159
|
// APME: emit task_boundary so the abort itself closes the task
|
|
1023
1160
|
// immediately \u2014 don't wait for the idle timer. The user
|
|
1024
1161
|
// explicitly stopped this turn.
|
|
1025
|
-
const abortCtx = this.buildApmeCtx();
|
|
1162
|
+
const abortCtx = this.buildApmeCtx(sessionKey);
|
|
1026
1163
|
if (abortCtx) {
|
|
1027
|
-
this.clearIdleGapTimer();
|
|
1028
|
-
this.ingestApmeSpans(openclawChatEventToSpans(abortCtx, {
|
|
1164
|
+
this.clearIdleGapTimer(sessionKey);
|
|
1165
|
+
this.ingestApmeSpans(sessionKey, openclawChatEventToSpans(abortCtx, {
|
|
1029
1166
|
state: 'aborted',
|
|
1030
1167
|
...(payload.runId ? { runId: payload.runId } : {}),
|
|
1031
1168
|
...(payload.sessionKey ? { sessionKey: payload.sessionKey } : {}),
|
|
@@ -1082,10 +1219,10 @@ export class OpenClawAdapter extends EventEmitter {
|
|
|
1082
1219
|
// idle-gap timer. `chat.send` cleared that timer and only `final`
|
|
1083
1220
|
// used to re-arm it, so a prompt that errored and was then
|
|
1084
1221
|
// abandoned left its task open indefinitely.
|
|
1085
|
-
const errCtx = this.buildApmeCtx();
|
|
1222
|
+
const errCtx = this.buildApmeCtx(sessionKey);
|
|
1086
1223
|
if (errCtx) {
|
|
1087
|
-
this.ingestApmeSpans(openclawChatEventToSpans(errCtx, errInput));
|
|
1088
|
-
this.armIdleGapTimer();
|
|
1224
|
+
this.ingestApmeSpans(sessionKey, openclawChatEventToSpans(errCtx, errInput));
|
|
1225
|
+
this.armIdleGapTimer(sessionKey);
|
|
1089
1226
|
}
|
|
1090
1227
|
this.chatStarted = false;
|
|
1091
1228
|
this.lastPrompt = null;
|
|
@@ -1198,15 +1335,27 @@ export class OpenClawAdapter extends EventEmitter {
|
|
|
1198
1335
|
// trajectory empty of detail. Route them into the APME normalizer so the
|
|
1199
1336
|
// SessionSample captures the full tool-use trajectory (req #6).
|
|
1200
1337
|
case 'session.tool': {
|
|
1201
|
-
const
|
|
1338
|
+
const toolPayload = payload;
|
|
1339
|
+
const apmeCtx = this.buildApmeCtx(toolPayload.sessionKey);
|
|
1202
1340
|
if (apmeCtx)
|
|
1203
|
-
this.ingestApmeSpans(openclawSessionToolToSpans(apmeCtx,
|
|
1341
|
+
this.ingestApmeSpans(toolPayload.sessionKey, openclawSessionToolToSpans(apmeCtx, toolPayload));
|
|
1204
1342
|
break;
|
|
1205
1343
|
}
|
|
1206
1344
|
case 'session.message': {
|
|
1207
|
-
const
|
|
1208
|
-
|
|
1209
|
-
|
|
1345
|
+
const msgPayload = payload;
|
|
1346
|
+
const apmeCtx = this.buildApmeCtx(msgPayload.sessionKey);
|
|
1347
|
+
if (!apmeCtx)
|
|
1348
|
+
break;
|
|
1349
|
+
this.ingestApmeSpans(msgPayload.sessionKey, openclawSessionMessageToSpans(apmeCtx, msgPayload));
|
|
1350
|
+
// A user message re-opens the conversation: cancel the pending
|
|
1351
|
+
// idle-gap boundary for THIS key so a follow-up question stays in the
|
|
1352
|
+
// same task. An assistant message means the turn just landed, so the
|
|
1353
|
+
// gap starts counting from here.
|
|
1354
|
+
const role = (msgPayload.message?.role ?? '').toLowerCase();
|
|
1355
|
+
if (role === 'user')
|
|
1356
|
+
this.clearIdleGapTimer(msgPayload.sessionKey);
|
|
1357
|
+
else if (role === 'assistant')
|
|
1358
|
+
this.armIdleGapTimer(msgPayload.sessionKey);
|
|
1210
1359
|
break;
|
|
1211
1360
|
}
|
|
1212
1361
|
default:
|
|
@@ -1289,6 +1438,21 @@ export class OpenClawAdapter extends EventEmitter {
|
|
|
1289
1438
|
this.fetchSessions().catch(err => debug('adapter:openclaw', `fetchSessions error: ${err}`));
|
|
1290
1439
|
// Fetch model catalog (async, non-blocking)
|
|
1291
1440
|
this.emitModelCatalog().catch(err => debug('adapter:openclaw', `emitModelCatalog error: ${err}`));
|
|
1441
|
+
// Subscribe to the session event stream. Without this call the
|
|
1442
|
+
// Gateway puts us in NEITHER `sessionEventSubscribers` nor
|
|
1443
|
+
// `sessionMessageSubscribers`, and those two sets are the entire
|
|
1444
|
+
// recipient list for `session.message` and `session.tool` — so the
|
|
1445
|
+
// handlers for both events below have existed since they were written
|
|
1446
|
+
// without ever receiving one frame. That is what left APME with 657 of
|
|
1447
|
+
// 665 OpenClaw runs holding zero turns: the `chat` frame carries only
|
|
1448
|
+
// the ASSISTANT message, so nothing ever opened a turn for a
|
|
1449
|
+
// conversation the user started in the OpenClaw app, and a
|
|
1450
|
+
// `turn_response` with no open turn is dropped on the floor
|
|
1451
|
+
// (`Collector.setTurnResponse` → `if (!turnId) return`).
|
|
1452
|
+
//
|
|
1453
|
+
// `operator.read` is already in our requested scopes (it is what
|
|
1454
|
+
// `sessions.list` runs under), so this needs no new permission.
|
|
1455
|
+
this.subscribeSessionEvents().catch(err => debug('adapter:openclaw', `subscribeSessionEvents error: ${err}`));
|
|
1292
1456
|
// Catch up on approvals that were already waiting. `exec.approval.requested`
|
|
1293
1457
|
// is a broadcast and is never replayed, so without this read a daemon
|
|
1294
1458
|
// restart (or any Gateway reconnect) leaves the agent blocked on an
|
|
@@ -1318,6 +1482,34 @@ export class OpenClawAdapter extends EventEmitter {
|
|
|
1318
1482
|
debug('adapter:openclaw', `Failed to send connect request: ${err}`);
|
|
1319
1483
|
}
|
|
1320
1484
|
}
|
|
1485
|
+
/** Join the Gateway's session event stream (`sessions.subscribe`, scope
|
|
1486
|
+
* `operator.read`).
|
|
1487
|
+
*
|
|
1488
|
+
* The Gateway's recipient set for `session.message` is
|
|
1489
|
+
* `sessionEventSubscribers ∪ sessionMessageSubscribers`, and the former is
|
|
1490
|
+
* unioned in unconditionally — so this one no-param call delivers the
|
|
1491
|
+
* messages of EVERY session, which is what we want: AgentDeck observes the
|
|
1492
|
+
* whole agent, not one chat. `sessions.messages.subscribe` is the per-key
|
|
1493
|
+
* alternative and would need a subscribe/unsubscribe cycle for every
|
|
1494
|
+
* session key the store mints (an eval suite opens a fresh one per run), so
|
|
1495
|
+
* it is the wrong shape here.
|
|
1496
|
+
*
|
|
1497
|
+
* Failure is non-fatal and stays visible: without the subscription the
|
|
1498
|
+
* adapter degrades to exactly its previous behaviour (chat + approvals
|
|
1499
|
+
* only), which is a smaller timeline, not a broken one. */
|
|
1500
|
+
async subscribeSessionEvents() {
|
|
1501
|
+
const result = await this.rpcCall('sessions.subscribe', {});
|
|
1502
|
+
const subscribed = !!(result && typeof result === 'object'
|
|
1503
|
+
&& result.subscribed);
|
|
1504
|
+
if (!subscribed) {
|
|
1505
|
+
// The Gateway answers `{subscribed:false}` when the connection has no
|
|
1506
|
+
// `connId` — a real refusal, not a transport error, so it never lands in
|
|
1507
|
+
// the catch above.
|
|
1508
|
+
logError('[adapter:openclaw] sessions.subscribe returned subscribed:false — session.message/session.tool will not arrive');
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
debug('adapter:openclaw', 'sessions.subscribe ok');
|
|
1512
|
+
}
|
|
1321
1513
|
/** Fetch sessions and set the most recently updated one as active. */
|
|
1322
1514
|
async fetchSessions() {
|
|
1323
1515
|
try {
|