@agentdeck/bridge 1.0.24 → 1.2.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/README.md +14 -6
- package/assets/fonts/NOTICE-PocketSansWorld.md +10 -0
- package/assets/fonts/OFL-PocketSansWorld.txt +93 -0
- package/assets/fonts/PocketSansWorld_12.cpfont +0 -0
- package/assets/fonts/pocket-sans-world.manifest.json +49 -0
- package/assets/learning/LICENSE-CC-BY-SA-4.0.txt +10 -0
- package/assets/learning/NOTICE.md +33 -0
- package/assets/learning/jp-n3-ko.manifest.json +27 -0
- package/assets/learning/jp-n3-ko.pdl +0 -0
- 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/agent-cli-diagnostics.d.ts +20 -0
- package/dist/agent-cli-diagnostics.d.ts.map +1 -0
- package/dist/agent-cli-diagnostics.js +100 -0
- package/dist/agent-cli-diagnostics.js.map +1 -0
- 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 +55 -2
- package/dist/apme/collector.d.ts.map +1 -1
- package/dist/apme/collector.js +324 -37
- package/dist/apme/collector.js.map +1 -1
- package/dist/apme/dashboard-html.d.ts.map +1 -1
- package/dist/apme/dashboard-html.js +275 -35
- package/dist/apme/dashboard-html.js.map +1 -1
- package/dist/apme/http.d.ts.map +1 -1
- package/dist/apme/http.js +33 -5
- package/dist/apme/http.js.map +1 -1
- package/dist/apme/index.d.ts.map +1 -1
- package/dist/apme/index.js +6 -2
- package/dist/apme/index.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 +47 -1
- package/dist/apme/store.d.ts.map +1 -1
- package/dist/apme/store.js +397 -99
- package/dist/apme/store.js.map +1 -1
- package/dist/bridge-core.d.ts +25 -1
- package/dist/bridge-core.d.ts.map +1 -1
- package/dist/bridge-core.js +50 -9
- 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 +56 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +442 -34
- package/dist/cli.js.map +1 -1
- package/dist/codex-rate-limits-live.d.ts +180 -10
- package/dist/codex-rate-limits-live.d.ts.map +1 -1
- package/dist/codex-rate-limits-live.js +475 -22
- package/dist/codex-rate-limits-live.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 +876 -109
- 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-ws-client.d.ts +1 -0
- package/dist/daemon-ws-client.d.ts.map +1 -1
- package/dist/daemon-ws-client.js +25 -16
- package/dist/daemon-ws-client.js.map +1 -1
- package/dist/daemon.js +1 -1
- package/dist/daemon.js.map +1 -1
- package/dist/esp32-serial.d.ts +18 -0
- package/dist/esp32-serial.d.ts.map +1 -1
- package/dist/esp32-serial.js +23 -1
- package/dist/esp32-serial.js.map +1 -1
- package/dist/font-pack.d.ts +15 -0
- package/dist/font-pack.d.ts.map +1 -0
- package/dist/font-pack.js +75 -0
- package/dist/font-pack.js.map +1 -0
- package/dist/glance-frame.d.ts.map +1 -1
- package/dist/glance-frame.js +5 -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/http-auth-gate.d.ts.map +1 -1
- package/dist/http-auth-gate.js +9 -1
- package/dist/http-auth-gate.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/learning-pack.d.ts +24 -0
- package/dist/learning-pack.d.ts.map +1 -0
- package/dist/learning-pack.js +125 -0
- package/dist/learning-pack.js.map +1 -0
- 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 +92 -3
- 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/pairing-knocks.d.ts +80 -0
- package/dist/pairing-knocks.d.ts.map +1 -0
- package/dist/pairing-knocks.js +312 -0
- package/dist/pairing-knocks.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/pixoo/micro-glyphs.d.ts +4 -0
- package/dist/pixoo/micro-glyphs.d.ts.map +1 -1
- package/dist/pixoo/micro-glyphs.js +29 -0
- package/dist/pixoo/micro-glyphs.js.map +1 -1
- package/dist/pixoo/pixoo-font.d.ts.map +1 -1
- package/dist/pixoo/pixoo-font.js +3 -1
- package/dist/pixoo/pixoo-font.js.map +1 -1
- package/dist/pixoo/pixoo-renderer.d.ts +1 -1
- package/dist/pixoo/pixoo-renderer.d.ts.map +1 -1
- package/dist/pixoo/pixoo-renderer.js +17 -2
- package/dist/pixoo/pixoo-renderer.js.map +1 -1
- package/dist/pty-manager.d.ts +9 -0
- package/dist/pty-manager.d.ts.map +1 -1
- package/dist/pty-manager.js +63 -4
- package/dist/pty-manager.js.map +1 -1
- package/dist/relayed-usage.d.ts +98 -0
- package/dist/relayed-usage.d.ts.map +1 -0
- package/dist/relayed-usage.js +103 -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 +237 -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/dashboard.d.ts.map +1 -1
- package/dist/tui/dashboard.js +8 -1
- package/dist/tui/dashboard.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 +18 -0
- package/dist/ws-server.d.ts.map +1 -1
- package/dist/ws-server.js +95 -7
- package/dist/ws-server.js.map +1 -1
- package/fm-helper/AgentDeckFMHelper.swift +51 -5
- package/package.json +4 -3
- package/src/timebox/sync_ble.py +35 -16
package/dist/apme/collector.js
CHANGED
|
@@ -18,7 +18,7 @@ import { mkdirSync, writeFileSync } from 'fs';
|
|
|
18
18
|
import { homedir } from 'os';
|
|
19
19
|
import { debug } from '../logger.js';
|
|
20
20
|
import { resolveProjectName } from '../utils/project-name.js';
|
|
21
|
-
import {
|
|
21
|
+
import { AGENT_IDLE_GAP_MS, deriveTaskTitle, isPricedModel, normalizeModelProvider, priceUsd } from '@agentdeck/shared';
|
|
22
22
|
import { classifyRunSmart, computeSignals, classify } from './classifier.js';
|
|
23
23
|
import { readOpenTurnEvidence } from './claude-transcript-reader.js';
|
|
24
24
|
/** A turn_start with the same prompt landing on a fresh, still-empty turn
|
|
@@ -32,10 +32,42 @@ const DUPLICATE_TURN_OPEN_WINDOW_MS = 15_000;
|
|
|
32
32
|
* a cancel milliseconds into a turn reads as belonging to the previous one.
|
|
33
33
|
* Matches the watchdog's `TURN_OPEN_SLACK_MS` — same comparison, same risk. */
|
|
34
34
|
const TURN_INTERRUPT_SLACK_MS = 2_000;
|
|
35
|
+
/**
|
|
36
|
+
* Is `echo` the same user prompt as `original`, arriving a second time?
|
|
37
|
+
*
|
|
38
|
+
* Two producers open a turn for one prompt: our own `chat.send` span, which
|
|
39
|
+
* carries the text verbatim, and the Gateway's `session.message`(user) echo,
|
|
40
|
+
* which has been trimmed, envelope-stripped and capped at 8,000 characters
|
|
41
|
+
* before it reaches us. Exact equality therefore misses on inputs as ordinary
|
|
42
|
+
* as a trailing newline — and a miss is not benign: the duplicate then closes
|
|
43
|
+
* the open turn with `resolveDisplacedTurnSource`, which for a non-`claude-code`
|
|
44
|
+
* agent returns `next_prompt`, i.e. the unrecovered-Stop-loss bucket. A stray
|
|
45
|
+
* "\n" would invent a Stop loss for an agent that has no Stop hook at all,
|
|
46
|
+
* strand an empty turn, and shift every later `turn_index`.
|
|
47
|
+
*
|
|
48
|
+
* Comparing TRIMMED is the whole fix, and deliberately all of it. A first pass
|
|
49
|
+
* here also allowed a prefix match when the shorter side sat at the Gateway's
|
|
50
|
+
* 8,000-character cap — that branch could never fire: both operands reach this
|
|
51
|
+
* function through the caller's own `slice(0, 8_000)`, so "the shorter one is
|
|
52
|
+
* at 8,000" forces both to be exactly 8,000 characters, and two equal-length
|
|
53
|
+
* strings where one is a prefix of the other are identical, which the equality
|
|
54
|
+
* check above already answered. Reintroducing truncation tolerance means
|
|
55
|
+
* comparing BEFORE that slice, not adding a branch after it.
|
|
56
|
+
*
|
|
57
|
+
* Known and NOT closed by this: because the caller slices first, two genuinely
|
|
58
|
+
* different prompts that share their first 8,000 characters compare equal and
|
|
59
|
+
* collapse into one turn — an eval harness sending a long fixed preamble with a
|
|
60
|
+
* differing tail is exactly that shape. That predates this comparison and is
|
|
61
|
+
* fixed by keying on a hash of the unsliced prompt, not here.
|
|
62
|
+
*/
|
|
63
|
+
function samePromptEcho(original, echo) {
|
|
64
|
+
return original.trim() === echo.trim();
|
|
65
|
+
}
|
|
35
66
|
export class ApmeCollector {
|
|
36
67
|
store;
|
|
37
68
|
hwSampler;
|
|
38
69
|
openTurnProbe;
|
|
70
|
+
idleGapMs;
|
|
39
71
|
sessionToRun = new Map(); // sessionId → runId
|
|
40
72
|
sessionToAgentType = new Map(); // sessionId → agentType (survives closeRun for late-attributed timeline rows)
|
|
41
73
|
sessionToTurn = new Map(); // sessionId → current turn
|
|
@@ -43,6 +75,13 @@ export class ApmeCollector {
|
|
|
43
75
|
sessionToTask = new Map(); // sessionId → current task
|
|
44
76
|
runTaskCount = new Map(); // runId → next task_index
|
|
45
77
|
sessionToUsage = new Map(); // last cumulative usage
|
|
78
|
+
/** Running cost total for producers that report PER-MESSAGE cost.
|
|
79
|
+
* Separate from `sessionToUsage` because `updateUsage` writes the cost
|
|
80
|
+
* column absolutely and would otherwise stamp one message's cost as the
|
|
81
|
+
* run's. `seen` keeps a reported ZERO distinct from nothing reported —
|
|
82
|
+
* glm-5.2 answers `usage.cost.total = 0` on every message, so folding the
|
|
83
|
+
* two would record "free" as "unknown" for a whole class of run. */
|
|
84
|
+
sessionToCostTotal = new Map();
|
|
46
85
|
/** Optional listener fired after `closeTask` persists the row. The runner
|
|
47
86
|
* wires this to enqueue a task-level judge call. */
|
|
48
87
|
onTaskClosed = null;
|
|
@@ -58,14 +97,21 @@ export class ApmeCollector {
|
|
|
58
97
|
/** Last milestone key (`taskId:turnIndex`) per session — a turn can carry
|
|
59
98
|
* several all-completed TodoWrite calls; surface only the first. */
|
|
60
99
|
sessionToLastMilestone = new Map();
|
|
100
|
+
/** Per-session idle-gap timers — armed in `closeTurn`, cleared on the next
|
|
101
|
+
* turn open / task close. See `AGENT_IDLE_GAP_MS` for the measurement the
|
|
102
|
+
* constant rests on. */
|
|
103
|
+
sessionToIdleGapTimer = new Map();
|
|
61
104
|
constructor(store, hwSampler,
|
|
62
105
|
/** What the transcript proves about a turn open since a timestamp — cancel,
|
|
63
106
|
* client abort, or never having run at all. Injectable for tests; the
|
|
64
107
|
* default reads the Claude transcript tail once for all three. */
|
|
65
|
-
openTurnProbe = readOpenTurnEvidence
|
|
108
|
+
openTurnProbe = readOpenTurnEvidence,
|
|
109
|
+
/** Injectable for tests only — production call sites take the default. */
|
|
110
|
+
idleGapMs = AGENT_IDLE_GAP_MS) {
|
|
66
111
|
this.store = store;
|
|
67
112
|
this.hwSampler = hwSampler;
|
|
68
113
|
this.openTurnProbe = openTurnProbe;
|
|
114
|
+
this.idleGapMs = idleGapMs;
|
|
69
115
|
}
|
|
70
116
|
/** Start a new run and return its id. Safe to call if store disabled (returns ''). */
|
|
71
117
|
openRun(input) {
|
|
@@ -78,6 +124,7 @@ export class ApmeCollector {
|
|
|
78
124
|
sessionId: input.sessionId,
|
|
79
125
|
agentType: input.agentType,
|
|
80
126
|
modelId: input.modelId ?? null,
|
|
127
|
+
provider: input.modelId ? normalizeModelProvider(undefined, input.modelId) : null,
|
|
81
128
|
projectName: input.projectName ?? (input.projectPath ? resolveProjectName({ cwd: input.projectPath }) : null),
|
|
82
129
|
projectPath: input.projectPath ?? null,
|
|
83
130
|
taskPrompt: input.taskPrompt ?? null,
|
|
@@ -126,7 +173,8 @@ export class ApmeCollector {
|
|
|
126
173
|
// empty phantom turn and shift every later turn_index, so an identical
|
|
127
174
|
// prompt landing on a fresh, still-empty turn is a no-op.
|
|
128
175
|
const openTurn = this.sessionToTurn.get(sessionId);
|
|
129
|
-
if (openTurn && prompt !== null && openTurn.prompt
|
|
176
|
+
if (openTurn && prompt !== null && openTurn.prompt !== null &&
|
|
177
|
+
samePromptEcho(openTurn.prompt, prompt) &&
|
|
130
178
|
openTurn.toolCalls === 0 && !openTurn.hasResponse &&
|
|
131
179
|
Date.now() - openTurn.startedAt < DUPLICATE_TURN_OPEN_WINDOW_MS) {
|
|
132
180
|
debug('APME', `duplicate turn_start ignored (echo) turn=${openTurn.id.slice(0, 8)}`);
|
|
@@ -164,14 +212,18 @@ export class ApmeCollector {
|
|
|
164
212
|
prompt, hasResponse: false,
|
|
165
213
|
};
|
|
166
214
|
this.sessionToTurn.set(sessionId, turn);
|
|
215
|
+
// A new turn is the "still working" signal the idle-gap timer waits on.
|
|
216
|
+
this.clearIdleGapTimer(sessionId);
|
|
167
217
|
// Ensure an active task exists so this turn can attach to it. Tasks group
|
|
168
218
|
// consecutive turns between boundary signals (TodoWrite all-completed,
|
|
169
219
|
// /clear, session_end). First turn in a run opens task 0.
|
|
170
220
|
const task = this.openTaskIfNone(sessionId, runId);
|
|
171
221
|
if (task) {
|
|
172
222
|
const wasFirstTurn = task.firstTurnIndex === null;
|
|
173
|
-
if (wasFirstTurn)
|
|
223
|
+
if (wasFirstTurn) {
|
|
174
224
|
task.firstTurnIndex = turnIndex;
|
|
225
|
+
task.firstPrompt = prompt ?? null;
|
|
226
|
+
}
|
|
175
227
|
task.lastTurnIndex = turnIndex;
|
|
176
228
|
// Second (or later) turn on the same task proves it's a real,
|
|
177
229
|
// multi-turn work unit → promote the deferred TASK header so the
|
|
@@ -415,6 +467,9 @@ export class ApmeCollector {
|
|
|
415
467
|
catch (err) {
|
|
416
468
|
debug('APME', `closeTurn failed: ${String(err)}`);
|
|
417
469
|
}
|
|
470
|
+
// Idle starts when the turn ends. The timer is cleared by the next turn
|
|
471
|
+
// open (still working) or by closeTask (boundary reached another way).
|
|
472
|
+
this.armIdleGapTimer(sessionId);
|
|
418
473
|
}
|
|
419
474
|
/** Get the current active turn ID for a session (if any). */
|
|
420
475
|
getActiveTurnId(sessionId) {
|
|
@@ -479,6 +534,7 @@ export class ApmeCollector {
|
|
|
479
534
|
startedAt: Date.now(),
|
|
480
535
|
firstTurnIndex: null,
|
|
481
536
|
lastTurnIndex: null,
|
|
537
|
+
firstPrompt: null,
|
|
482
538
|
timelineEmitted: false,
|
|
483
539
|
};
|
|
484
540
|
this.sessionToTask.set(sessionId, task);
|
|
@@ -519,12 +575,40 @@ export class ApmeCollector {
|
|
|
519
575
|
projectName: run?.projectName ?? null,
|
|
520
576
|
taskIndex: task.index,
|
|
521
577
|
startedAt: task.startedAt,
|
|
578
|
+
title: deriveTaskTitle(task.firstPrompt),
|
|
522
579
|
});
|
|
523
580
|
}
|
|
524
581
|
catch (err) {
|
|
525
582
|
debug('APME', `onTaskOpened listener threw: ${String(err)}`);
|
|
526
583
|
}
|
|
527
584
|
}
|
|
585
|
+
/** Arm the per-session idle-gap timer. Called from `closeTurn` — the turn
|
|
586
|
+
* just finished, so the gap being measured is genuine idle time, never the
|
|
587
|
+
* agent's own working time. Re-arming replaces any previous timer. */
|
|
588
|
+
armIdleGapTimer(sessionId) {
|
|
589
|
+
this.clearIdleGapTimer(sessionId);
|
|
590
|
+
const timer = setTimeout(() => {
|
|
591
|
+
this.sessionToIdleGapTimer.delete(sessionId);
|
|
592
|
+
// A turn opened since (and its clear lost a race), or work is mid-turn:
|
|
593
|
+
// never split under an active turn.
|
|
594
|
+
if (this.sessionToTurn.has(sessionId))
|
|
595
|
+
return;
|
|
596
|
+
if (!this.sessionToTask.has(sessionId))
|
|
597
|
+
return;
|
|
598
|
+
debug('APME', `idle gap (${Math.round(this.idleGapMs / 1000)}s) → closing task for ${sessionId.slice(0, 8)}`);
|
|
599
|
+
this.closeTask(sessionId, 'idle_gap');
|
|
600
|
+
}, this.idleGapMs);
|
|
601
|
+
// Never hold the process open for an idle bookkeeping timer.
|
|
602
|
+
timer.unref?.();
|
|
603
|
+
this.sessionToIdleGapTimer.set(sessionId, timer);
|
|
604
|
+
}
|
|
605
|
+
clearIdleGapTimer(sessionId) {
|
|
606
|
+
const timer = this.sessionToIdleGapTimer.get(sessionId);
|
|
607
|
+
if (timer) {
|
|
608
|
+
clearTimeout(timer);
|
|
609
|
+
this.sessionToIdleGapTimer.delete(sessionId);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
528
612
|
/** Public wrapper for closeTask. Used by the manual-boundary CLI /
|
|
529
613
|
* HTTP route + the macOS detail-pane button — lets the user declare
|
|
530
614
|
* "this task is done" without `/clear` (which would also split the
|
|
@@ -557,6 +641,7 @@ export class ApmeCollector {
|
|
|
557
641
|
return;
|
|
558
642
|
this.sessionToTask.delete(sessionId);
|
|
559
643
|
this.sessionToLastMilestone.delete(sessionId);
|
|
644
|
+
this.clearIdleGapTimer(sessionId);
|
|
560
645
|
// Empty task: no turns ever attached. Drop the row so the dashboard
|
|
561
646
|
// doesn't show phantom entries from back-to-back boundary signals.
|
|
562
647
|
if (task.firstTurnIndex === null) {
|
|
@@ -599,6 +684,14 @@ export class ApmeCollector {
|
|
|
599
684
|
try {
|
|
600
685
|
this.store.updateTask(task.id, {
|
|
601
686
|
endedAt: Date.now(),
|
|
687
|
+
// `first_turn_index` too, not just the last. The task row is INSERTed
|
|
688
|
+
// from inside the turn_start handler, i.e. before the turn exists, so
|
|
689
|
+
// it is written NULL and the real index only ever landed in memory —
|
|
690
|
+
// 294 of 1080 closed claude-code tasks carry NULL here, and the task
|
|
691
|
+
// rollup the layer-2 judge reads is keyed on these two columns.
|
|
692
|
+
// `reapAbandonedRun` already COALESCEs a value in at reap time, which
|
|
693
|
+
// is this defect's workaround rather than its fix.
|
|
694
|
+
firstTurnIndex: task.firstTurnIndex,
|
|
602
695
|
lastTurnIndex: task.lastTurnIndex ?? task.firstTurnIndex,
|
|
603
696
|
boundarySignal,
|
|
604
697
|
taskCategory,
|
|
@@ -700,6 +793,7 @@ export class ApmeCollector {
|
|
|
700
793
|
inputTokens: ev.inputTokens ?? null,
|
|
701
794
|
outputTokens: ev.outputTokens ?? null,
|
|
702
795
|
costUsd: ev.costUsd ?? null,
|
|
796
|
+
costKnown: ev.costKnown ?? null,
|
|
703
797
|
latencyMs: ev.latencyMs ?? null,
|
|
704
798
|
toolName: ev.toolName ?? null,
|
|
705
799
|
toolStatus: ev.toolStatus ?? null,
|
|
@@ -742,7 +836,7 @@ export class ApmeCollector {
|
|
|
742
836
|
* Falls back to the last closed turn if closeTurn() already ran (race with session exit).
|
|
743
837
|
* Tags turns.efficiency_json.response_kind so the runner can skip tool-only / empty
|
|
744
838
|
* turns — judging silence produces noise scores. */
|
|
745
|
-
setTurnResponse(sessionId, response) {
|
|
839
|
+
setTurnResponse(sessionId, response, source = 'direct') {
|
|
746
840
|
if (!this.store.enabled)
|
|
747
841
|
return;
|
|
748
842
|
const turn = this.sessionToTurn.get(sessionId);
|
|
@@ -750,15 +844,27 @@ export class ApmeCollector {
|
|
|
750
844
|
debug('APME', `setTurnResponse session=${sessionId.slice(0, 8)} turnId=${turnId?.slice(0, 8) ?? 'null'} respLen=${response.length}`);
|
|
751
845
|
if (!turnId)
|
|
752
846
|
return;
|
|
847
|
+
const existingTurn = this.store.getTurn(turnId);
|
|
848
|
+
const existingMeta = parseEfficiencyJson(existingTurn);
|
|
849
|
+
const existingSource = typeof existingMeta.response_source === 'string'
|
|
850
|
+
? existingMeta.response_source
|
|
851
|
+
: (existingTurn?.response ? 'direct' : undefined);
|
|
852
|
+
if (responseSourcePriority(source) < responseSourcePriority(existingSource)) {
|
|
853
|
+
debug('APME', `setTurnResponse ignored lower-priority source=${source} existing=${existingSource}`);
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
753
856
|
const trimmedLen = response.trim().length;
|
|
754
857
|
// Active-turn toolCalls is authoritative; after closeTurn() the counter is
|
|
755
858
|
// already flushed to the DB row, so we fetch from there instead.
|
|
756
859
|
const toolCalls = turn?.toolCalls
|
|
757
|
-
?? (
|
|
860
|
+
?? (existingTurn?.tool_calls ?? 0);
|
|
758
861
|
const kind = trimmedLen >= 1
|
|
759
862
|
? 'text'
|
|
760
863
|
: (toolCalls > 0 ? 'tool_only' : 'empty');
|
|
761
|
-
const efficiencyJson = mergeEfficiencyJson(
|
|
864
|
+
const efficiencyJson = mergeEfficiencyJson(existingTurn, {
|
|
865
|
+
response_kind: kind,
|
|
866
|
+
response_source: source,
|
|
867
|
+
});
|
|
762
868
|
try {
|
|
763
869
|
this.store.updateTurn(turnId, {
|
|
764
870
|
response: response.slice(0, 10_000),
|
|
@@ -773,11 +879,18 @@ export class ApmeCollector {
|
|
|
773
879
|
// Sample trajectory: the assistant response closes the turn's event arc.
|
|
774
880
|
const ctx = this.sampleCtxForTurn(sessionId, turnId);
|
|
775
881
|
if (ctx) {
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
}
|
|
882
|
+
const payload = safeStringify({ text: response.slice(0, 10_000), responseKind: kind });
|
|
883
|
+
const existingEvent = this.store.findAssistantMessageEvent(ctx.taskId, turnId);
|
|
884
|
+
if (existingEvent?.id != null) {
|
|
885
|
+
this.store.updateSampleEvent(existingEvent.id, { payload, ts: Date.now() });
|
|
886
|
+
}
|
|
887
|
+
else {
|
|
888
|
+
this.appendSampleEvent(ctx, {
|
|
889
|
+
kind: 'assistant_message',
|
|
890
|
+
dedupCore: `turn:${turnId}`,
|
|
891
|
+
payloadObj: { text: response.slice(0, 10_000), responseKind: kind },
|
|
892
|
+
});
|
|
893
|
+
}
|
|
781
894
|
}
|
|
782
895
|
}
|
|
783
896
|
/** Apply response to the last closed turn if it has no response yet.
|
|
@@ -835,16 +948,27 @@ export class ApmeCollector {
|
|
|
835
948
|
}
|
|
836
949
|
case 'turn_response': {
|
|
837
950
|
const text = a['agentdeck.response_text'] ?? '';
|
|
951
|
+
const source = a['agentdeck.response_source'] ?? 'direct';
|
|
838
952
|
const fallback = a['agentdeck.fallback_to_last_closed'] === true;
|
|
839
953
|
if (fallback)
|
|
840
954
|
this.setLastClosedTurnResponse(sessionId, text);
|
|
841
955
|
else
|
|
842
|
-
this.setTurnResponse(sessionId, text);
|
|
956
|
+
this.setTurnResponse(sessionId, text, source);
|
|
843
957
|
return;
|
|
844
958
|
}
|
|
845
|
-
case 'turn_end':
|
|
846
|
-
//
|
|
959
|
+
case 'turn_end': {
|
|
960
|
+
// An explicit close, from a source that HAS a stop signal of its own.
|
|
961
|
+
// This used to be a no-op on the theory that turns auto-close on the
|
|
962
|
+
// next `turn_start` — which is true, and is exactly the shape the
|
|
963
|
+
// `end_source` column exists to avoid: closing at the next prompt puts
|
|
964
|
+
// the user's thinking time inside the turn's duration, and a
|
|
965
|
+
// conversation with one turn never closes it at all. OpenClaw is the
|
|
966
|
+
// first emitter (its `chat.final` is the stop signal); every other
|
|
967
|
+
// adapter still auto-closes, unchanged.
|
|
968
|
+
const source = a['agentdeck.turn_end_source'] ?? 'stop';
|
|
969
|
+
this.closeTurn(sessionId, source);
|
|
847
970
|
return;
|
|
971
|
+
}
|
|
848
972
|
case 'tool_call': {
|
|
849
973
|
const toolName = (a['gen_ai.tool.name'] ?? a['agentdeck.tool_name']);
|
|
850
974
|
const raw = a['agentdeck.raw_payload'] ?? {};
|
|
@@ -891,17 +1015,18 @@ export class ApmeCollector {
|
|
|
891
1015
|
}
|
|
892
1016
|
case 'session_meta': {
|
|
893
1017
|
const model = a['gen_ai.request.model'];
|
|
894
|
-
|
|
895
|
-
|
|
1018
|
+
const rawProvider = a['gen_ai.system'];
|
|
1019
|
+
if (model || rawProvider)
|
|
1020
|
+
this.updateTurnIdentity(sessionId, model, rawProvider);
|
|
896
1021
|
const inputTokens = a['agentdeck.usage.input_tokens'];
|
|
897
1022
|
const outputTokens = a['agentdeck.usage.output_tokens'];
|
|
898
1023
|
const costUsd = a['agentdeck.usage.cost_usd'];
|
|
899
1024
|
if (inputTokens !== undefined || outputTokens !== undefined || costUsd !== undefined) {
|
|
900
|
-
//
|
|
901
|
-
this.
|
|
1025
|
+
// PER-MESSAGE numbers, not a running total — see `addUsageIncrement`.
|
|
1026
|
+
this.addUsageIncrement(sessionId, {
|
|
902
1027
|
inputTokens: inputTokens ?? 0,
|
|
903
1028
|
outputTokens: outputTokens ?? 0,
|
|
904
|
-
|
|
1029
|
+
costUsd: costUsd ?? null,
|
|
905
1030
|
});
|
|
906
1031
|
}
|
|
907
1032
|
return;
|
|
@@ -957,45 +1082,128 @@ export class ApmeCollector {
|
|
|
957
1082
|
* Snapshots carry CUMULATIVE session totals, so we emit a priced ModelEvent
|
|
958
1083
|
* for the delta and attribute it to the active task (the SessionSample cost
|
|
959
1084
|
* is the sum of its ModelEvents). */
|
|
1085
|
+
/**
|
|
1086
|
+
* Record usage a producer reports for ONE message.
|
|
1087
|
+
*
|
|
1088
|
+
* `updateUsage` is cumulative by contract: it assigns `inputTokens` to the run
|
|
1089
|
+
* row absolutely and derives its per-task ModelEvent as `max(0, cur - prev)`.
|
|
1090
|
+
* Its original and only other caller is the PTY poller's `UsageTracker`
|
|
1091
|
+
* snapshot, which really is a running total.
|
|
1092
|
+
*
|
|
1093
|
+
* A `session.message`(assistant) frame reports THIS message's tokens, and those
|
|
1094
|
+
* are not monotonic — measured on a real OpenClaw session: 30302, 21375, 335,
|
|
1095
|
+
* 96, 114. Fed to `updateUsage` directly, the run row ends at the LAST
|
|
1096
|
+
* message's count (114 for a session that consumed 52,222), every decrease
|
|
1097
|
+
* clamps its delta to 0 so most messages append no ModelEvent at all, and
|
|
1098
|
+
* `recomputeSampleCost` then prices the run from what survives. Two messages
|
|
1099
|
+
* sharing an `(in,out)` pair are additionally deduped away, because the dedup
|
|
1100
|
+
* key assumes cumulative values are unique.
|
|
1101
|
+
*
|
|
1102
|
+
* So convert here rather than at each producer: keep the running total and
|
|
1103
|
+
* hand `updateUsage` the cumulative series its arithmetic is written for.
|
|
1104
|
+
*/
|
|
1105
|
+
addUsageIncrement(sessionId, delta) {
|
|
1106
|
+
if (!this.store.enabled)
|
|
1107
|
+
return;
|
|
1108
|
+
// ONE guard for both accumulators. `updateUsage` drops everything when the
|
|
1109
|
+
// session has no open run, so advancing the cost total before that check
|
|
1110
|
+
// let cost count messages the token total did not: a span arriving before
|
|
1111
|
+
// the run opened, then one after, recorded 7 tokens at $10.50. Two
|
|
1112
|
+
// accumulators with different failure modes is the same defect shape as a
|
|
1113
|
+
// guard cleared only by a `defer` it shares a failure mode with.
|
|
1114
|
+
if (!this.sessionToRun.get(sessionId))
|
|
1115
|
+
return;
|
|
1116
|
+
const prev = this.sessionToUsage.get(sessionId) ?? { in: 0, out: 0, cost: null };
|
|
1117
|
+
const prevCost = this.sessionToCostTotal.get(sessionId) ?? { total: 0, seen: false };
|
|
1118
|
+
const reported = typeof delta.costUsd === 'number';
|
|
1119
|
+
const cost = { total: prevCost.total + (reported ? delta.costUsd : 0),
|
|
1120
|
+
seen: prevCost.seen || reported };
|
|
1121
|
+
this.sessionToCostTotal.set(sessionId, cost);
|
|
1122
|
+
this.updateUsage(sessionId, {
|
|
1123
|
+
// The clamp is deliberate, not defensive: `updateUsage` derives its
|
|
1124
|
+
// ModelEvent as `max(0, cur - prev)`, so the series handed to it must be
|
|
1125
|
+
// monotonic. No producer here reports a negative correction; if one ever
|
|
1126
|
+
// does, it needs its own path rather than silently shrinking the total.
|
|
1127
|
+
inputTokens: prev.in + Math.max(0, delta.inputTokens),
|
|
1128
|
+
outputTokens: prev.out + Math.max(0, delta.outputTokens),
|
|
1129
|
+
estimatedCostUsd: cost.seen ? cost.total : null,
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
960
1132
|
updateUsage(sessionId, snapshot) {
|
|
961
1133
|
if (!this.store.enabled)
|
|
962
1134
|
return;
|
|
963
1135
|
const runId = this.sessionToRun.get(sessionId);
|
|
964
1136
|
if (!runId)
|
|
965
1137
|
return;
|
|
1138
|
+
const curIn = snapshot.inputTokens ?? 0;
|
|
1139
|
+
const curOut = snapshot.outputTokens ?? 0;
|
|
1140
|
+
const prev = this.sessionToUsage.get(sessionId) ?? { in: 0, out: 0, cost: null };
|
|
1141
|
+
const hadPreviousUsage = prev.in > 0 || prev.out > 0;
|
|
1142
|
+
const dIn = Math.max(0, curIn - prev.in);
|
|
1143
|
+
const dOut = Math.max(0, curOut - prev.out);
|
|
1144
|
+
const run = this.store.getRun(runId);
|
|
1145
|
+
const turnId = this.turnIdFor(sessionId);
|
|
1146
|
+
const turn = turnId ? this.store.getTurn(turnId) : null;
|
|
1147
|
+
const model = turn?.model_id ?? run?.modelId ?? null;
|
|
1148
|
+
const reportedTotal = snapshot.estimatedCostUsd ?? snapshot.costSpent ?? null;
|
|
1149
|
+
const reportedDelta = reportedTotal == null
|
|
1150
|
+
? null
|
|
1151
|
+
: Math.max(0, reportedTotal - (prev.cost ?? 0));
|
|
1152
|
+
const priced = isPricedModel(model);
|
|
1153
|
+
const eventCostKnown = priced || (reportedDelta != null && reportedDelta > 0);
|
|
1154
|
+
const eventCost = reportedDelta ?? priceUsd(model, dIn, dOut);
|
|
1155
|
+
const hasUsageDelta = dIn > 0 || dOut > 0;
|
|
1156
|
+
const hasAccountingDelta = hasUsageDelta || (reportedDelta ?? 0) > 0;
|
|
1157
|
+
const runCostKnown = !hasAccountingDelta
|
|
1158
|
+
? Boolean(run?.costKnown)
|
|
1159
|
+
: hadPreviousUsage
|
|
1160
|
+
? Boolean(run?.costKnown) && eventCostKnown
|
|
1161
|
+
: eventCostKnown;
|
|
1162
|
+
const runCost = reportedTotal ?? (priced ? priceUsd(model, curIn, curOut) : null);
|
|
1163
|
+
this.sessionToUsage.set(sessionId, { in: curIn, out: curOut, cost: reportedTotal });
|
|
966
1164
|
try {
|
|
967
1165
|
this.store.updateRun(runId, {
|
|
968
1166
|
inputTokens: snapshot.inputTokens,
|
|
969
1167
|
outputTokens: snapshot.outputTokens,
|
|
970
|
-
costUsd:
|
|
1168
|
+
costUsd: runCost,
|
|
1169
|
+
costKnown: runCostKnown,
|
|
971
1170
|
});
|
|
972
1171
|
}
|
|
973
1172
|
catch { /* ignore */ }
|
|
974
1173
|
// ── Per-task ModelEvent from the cumulative delta ──
|
|
975
|
-
const curIn = snapshot.inputTokens ?? 0;
|
|
976
|
-
const curOut = snapshot.outputTokens ?? 0;
|
|
977
|
-
const prev = this.sessionToUsage.get(sessionId) ?? { in: 0, out: 0 };
|
|
978
|
-
const dIn = Math.max(0, curIn - prev.in);
|
|
979
|
-
const dOut = Math.max(0, curOut - prev.out);
|
|
980
|
-
this.sessionToUsage.set(sessionId, { in: curIn, out: curOut });
|
|
981
1174
|
if (dIn === 0 && dOut === 0)
|
|
982
1175
|
return;
|
|
983
1176
|
const task = this.sessionToTask.get(sessionId);
|
|
984
1177
|
if (!task)
|
|
985
1178
|
return;
|
|
986
|
-
const
|
|
987
|
-
|
|
1179
|
+
const provider = turn?.provider
|
|
1180
|
+
?? run?.provider
|
|
1181
|
+
?? normalizeModelProvider(undefined, model);
|
|
1182
|
+
const taskRow = this.store.getTask(task.id);
|
|
1183
|
+
const taskModel = !model
|
|
1184
|
+
? taskRow?.modelId
|
|
1185
|
+
: !taskRow?.modelId || taskRow.modelId === model
|
|
1186
|
+
? model
|
|
1187
|
+
: 'mixed';
|
|
1188
|
+
const taskProvider = !provider || provider === 'unknown'
|
|
1189
|
+
? taskRow?.provider
|
|
1190
|
+
: !taskRow?.provider || taskRow.provider === provider
|
|
1191
|
+
? provider
|
|
1192
|
+
: 'mixed';
|
|
988
1193
|
const turnIndex = this.sessionToTurn.get(sessionId)?.index ?? task.lastTurnIndex ?? 0;
|
|
989
|
-
|
|
990
|
-
const cost = priceUsd(model, dIn, dOut);
|
|
991
|
-
this.appendSampleEvent({ taskId: task.id, runId, turnIndex, turnId: this.turnIdFor(sessionId) }, {
|
|
1194
|
+
this.appendSampleEvent({ taskId: task.id, runId, turnIndex, turnId }, {
|
|
992
1195
|
kind: 'model', model: model ?? undefined, inputTokens: dIn, outputTokens: dOut,
|
|
993
|
-
costUsd:
|
|
1196
|
+
costUsd: eventCost, costKnown: eventCostKnown,
|
|
1197
|
+
latencyMs: 0, dedupCore: `${curIn}:${curOut}`,
|
|
994
1198
|
});
|
|
995
1199
|
try {
|
|
996
1200
|
this.store.updateTask(task.id, {
|
|
997
|
-
modelId:
|
|
998
|
-
|
|
1201
|
+
modelId: taskModel,
|
|
1202
|
+
provider: taskProvider,
|
|
1203
|
+
modelConfig: JSON.stringify({
|
|
1204
|
+
modelId: taskModel ?? 'unknown',
|
|
1205
|
+
provider: taskProvider ?? null,
|
|
1206
|
+
}),
|
|
999
1207
|
});
|
|
1000
1208
|
this.store.recomputeSampleCost(task.id);
|
|
1001
1209
|
}
|
|
@@ -1046,7 +1254,71 @@ export class ApmeCollector {
|
|
|
1046
1254
|
if (!runId)
|
|
1047
1255
|
return;
|
|
1048
1256
|
try {
|
|
1049
|
-
this.store.updateRun(runId, {
|
|
1257
|
+
this.store.updateRun(runId, {
|
|
1258
|
+
modelId,
|
|
1259
|
+
provider: normalizeModelProvider(undefined, modelId),
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
catch { /* ignore */ }
|
|
1263
|
+
}
|
|
1264
|
+
/** Persist the model/provider on the assistant message's own turn.
|
|
1265
|
+
* `session_meta` can arrive after `chat.final` closed that turn, so the same
|
|
1266
|
+
* current→last fallback used by setTurnResponse is required here too. */
|
|
1267
|
+
updateTurnIdentity(sessionId, reportedModel, reportedProvider) {
|
|
1268
|
+
const runId = this.sessionToRun.get(sessionId);
|
|
1269
|
+
if (!runId)
|
|
1270
|
+
return;
|
|
1271
|
+
const turnId = this.turnIdFor(sessionId);
|
|
1272
|
+
const turn = turnId ? this.store.getTurn(turnId) : null;
|
|
1273
|
+
const run = this.store.getRun(runId);
|
|
1274
|
+
const model = reportedModel ?? turn?.model_id ?? run?.modelId ?? undefined;
|
|
1275
|
+
if (!model && !reportedProvider)
|
|
1276
|
+
return;
|
|
1277
|
+
const normalized = normalizeModelProvider(reportedProvider, model);
|
|
1278
|
+
const provider = normalized === 'unknown' ? null : normalized;
|
|
1279
|
+
if (turnId) {
|
|
1280
|
+
try {
|
|
1281
|
+
this.store.updateTurn(turnId, {
|
|
1282
|
+
...(model ? { modelId: model } : {}),
|
|
1283
|
+
provider,
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
catch { /* ignore */ }
|
|
1287
|
+
}
|
|
1288
|
+
// Preserve the established run-level "latest observed model" contract for
|
|
1289
|
+
// legacy consumers. Scorecards no longer depend on it when a turn has its
|
|
1290
|
+
// own identity, so updating this compatibility fallback cannot reassign
|
|
1291
|
+
// earlier turns.
|
|
1292
|
+
if (run) {
|
|
1293
|
+
const patch = {
|
|
1294
|
+
...(model ? { modelId: model } : {}),
|
|
1295
|
+
provider,
|
|
1296
|
+
};
|
|
1297
|
+
if (Object.keys(patch).length > 0) {
|
|
1298
|
+
try {
|
|
1299
|
+
this.store.updateRun(runId, patch);
|
|
1300
|
+
}
|
|
1301
|
+
catch { /* ignore */ }
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
const taskId = turn?.task_id ?? this.sessionToTask.get(sessionId)?.id;
|
|
1305
|
+
if (!taskId || !model)
|
|
1306
|
+
return;
|
|
1307
|
+
const task = this.store.getTask(taskId);
|
|
1308
|
+
if (!task)
|
|
1309
|
+
return;
|
|
1310
|
+
const taskModel = !task.modelId || task.modelId === model ? model : 'mixed';
|
|
1311
|
+
const taskProvider = !provider
|
|
1312
|
+
? task.provider
|
|
1313
|
+
: task.provider == null || task.provider === provider
|
|
1314
|
+
? provider
|
|
1315
|
+
: 'mixed';
|
|
1316
|
+
try {
|
|
1317
|
+
this.store.updateTask(taskId, {
|
|
1318
|
+
modelId: taskModel,
|
|
1319
|
+
provider: taskProvider,
|
|
1320
|
+
modelConfig: JSON.stringify({ modelId: taskModel, provider: taskProvider }),
|
|
1321
|
+
});
|
|
1050
1322
|
}
|
|
1051
1323
|
catch { /* ignore */ }
|
|
1052
1324
|
}
|
|
@@ -1068,6 +1340,7 @@ export class ApmeCollector {
|
|
|
1068
1340
|
this.sessionToLastTurnId.delete(sessionId);
|
|
1069
1341
|
this.runTaskCount.delete(runId);
|
|
1070
1342
|
this.sessionToUsage.delete(sessionId);
|
|
1343
|
+
this.sessionToCostTotal.delete(sessionId);
|
|
1071
1344
|
// Mark empty runs so the dashboard can filter them out.
|
|
1072
1345
|
// Don't delete — FK constraints and concurrent access make deletion risky.
|
|
1073
1346
|
const run = this.store.getRun(runId);
|
|
@@ -1260,6 +1533,10 @@ function safeStringify(v) {
|
|
|
1260
1533
|
* sibling keys. Returns a JSON string suitable for the column. Unparseable
|
|
1261
1534
|
* existing values are replaced outright. */
|
|
1262
1535
|
function mergeEfficiencyJson(turn, patch) {
|
|
1536
|
+
const base = parseEfficiencyJson(turn);
|
|
1537
|
+
return JSON.stringify({ ...base, ...patch });
|
|
1538
|
+
}
|
|
1539
|
+
function parseEfficiencyJson(turn) {
|
|
1263
1540
|
let base = {};
|
|
1264
1541
|
const raw = turn?.efficiency_json;
|
|
1265
1542
|
if (typeof raw === 'string' && raw.length > 0) {
|
|
@@ -1271,6 +1548,16 @@ function mergeEfficiencyJson(turn, patch) {
|
|
|
1271
1548
|
}
|
|
1272
1549
|
catch { /* replace */ }
|
|
1273
1550
|
}
|
|
1274
|
-
return
|
|
1551
|
+
return base;
|
|
1552
|
+
}
|
|
1553
|
+
/** Full/final responses outrank lossy session-message projections. Unknown
|
|
1554
|
+
* historical provenance is treated as direct so a new projection cannot
|
|
1555
|
+
* downgrade data written before source tracking existed. */
|
|
1556
|
+
function responseSourcePriority(source) {
|
|
1557
|
+
if (source === 'session_message_projection')
|
|
1558
|
+
return 1;
|
|
1559
|
+
if (source === 'chat_final')
|
|
1560
|
+
return 3;
|
|
1561
|
+
return source ? 2 : 0;
|
|
1275
1562
|
}
|
|
1276
1563
|
//# sourceMappingURL=collector.js.map
|