@agentdeck/bridge 1.0.5 → 1.0.7
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/assets/fm-helper/agentdeck-fm-helper +0 -0
- package/dist/apme/collector.d.ts +10 -0
- package/dist/apme/collector.d.ts.map +1 -1
- package/dist/apme/collector.js +39 -10
- package/dist/apme/collector.js.map +1 -1
- package/dist/apme/dashboard-html.d.ts.map +1 -1
- package/dist/apme/dashboard-html.js +289 -0
- package/dist/apme/dashboard-html.js.map +1 -1
- package/dist/apme/graph.d.ts +48 -0
- package/dist/apme/graph.d.ts.map +1 -0
- package/dist/apme/graph.js +226 -0
- package/dist/apme/graph.js.map +1 -0
- package/dist/apme/http.d.ts.map +1 -1
- package/dist/apme/http.js +52 -0
- package/dist/apme/http.js.map +1 -1
- package/dist/apme/judge-detect.d.ts.map +1 -1
- package/dist/apme/judge-detect.js +26 -2
- package/dist/apme/judge-detect.js.map +1 -1
- package/dist/apme/store.d.ts +69 -1
- package/dist/apme/store.d.ts.map +1 -1
- package/dist/apme/store.js +238 -4
- package/dist/apme/store.js.map +1 -1
- package/dist/apme/types.d.ts +1 -1
- package/dist/apme/types.d.ts.map +1 -1
- package/dist/apme/types.js.map +1 -1
- package/dist/ble-sync-spawn.d.ts +25 -2
- package/dist/ble-sync-spawn.d.ts.map +1 -1
- package/dist/ble-sync-spawn.js +38 -2
- package/dist/ble-sync-spawn.js.map +1 -1
- package/dist/ble-sync-status.d.ts +77 -0
- package/dist/ble-sync-status.d.ts.map +1 -0
- package/dist/ble-sync-status.js +157 -0
- package/dist/ble-sync-status.js.map +1 -0
- package/dist/bridge-core.d.ts.map +1 -1
- package/dist/bridge-core.js +7 -1
- package/dist/bridge-core.js.map +1 -1
- package/dist/cli.js +0 -0
- package/dist/codex-rate-limits-live.d.ts +60 -0
- package/dist/codex-rate-limits-live.d.ts.map +1 -0
- package/dist/codex-rate-limits-live.js +282 -0
- package/dist/codex-rate-limits-live.js.map +1 -0
- package/dist/d200h/hid-protocol.d.ts +62 -0
- package/dist/d200h/hid-protocol.d.ts.map +1 -0
- package/dist/d200h/hid-protocol.js +124 -0
- package/dist/d200h/hid-protocol.js.map +1 -0
- package/dist/d200h/image-renderer.d.ts +24 -0
- package/dist/d200h/image-renderer.d.ts.map +1 -0
- package/dist/d200h/image-renderer.js +321 -0
- package/dist/d200h/image-renderer.js.map +1 -0
- package/dist/daemon-server.d.ts.map +1 -1
- package/dist/daemon-server.js +163 -10
- package/dist/daemon-server.js.map +1 -1
- package/dist/daemon.js +1 -1
- package/dist/device-voice-reply.d.ts +1 -10
- package/dist/device-voice-reply.d.ts.map +1 -1
- package/dist/device-voice-reply.js +4 -33
- package/dist/device-voice-reply.js.map +1 -1
- package/dist/foundation-models-helper.d.ts.map +1 -1
- package/dist/foundation-models-helper.js +59 -22
- package/dist/foundation-models-helper.js.map +1 -1
- package/dist/hook-migration.d.ts +2 -0
- package/dist/hook-migration.d.ts.map +1 -0
- package/dist/hook-migration.js +116 -0
- package/dist/hook-migration.js.map +1 -0
- package/dist/idotmatrix/idotmatrix-daemon-sync.d.ts +8 -0
- package/dist/idotmatrix/idotmatrix-daemon-sync.d.ts.map +1 -1
- package/dist/idotmatrix/idotmatrix-daemon-sync.js +19 -3
- package/dist/idotmatrix/idotmatrix-daemon-sync.js.map +1 -1
- package/dist/modules/d200h-module.d.ts +65 -0
- package/dist/modules/d200h-module.d.ts.map +1 -0
- package/dist/modules/d200h-module.js +495 -0
- package/dist/modules/d200h-module.js.map +1 -0
- package/dist/modules/d200h-renderer.d.ts +7 -0
- package/dist/modules/d200h-renderer.d.ts.map +1 -0
- package/dist/modules/d200h-renderer.js +321 -0
- package/dist/modules/d200h-renderer.js.map +1 -0
- package/dist/modules/idotmatrix-module.d.ts.map +1 -1
- package/dist/modules/idotmatrix-module.js +12 -1
- package/dist/modules/idotmatrix-module.js.map +1 -1
- package/dist/modules/timebox-module.d.ts.map +1 -1
- package/dist/modules/timebox-module.js +12 -1
- package/dist/modules/timebox-module.js.map +1 -1
- package/dist/modules/trmnl-module.d.ts +38 -0
- package/dist/modules/trmnl-module.d.ts.map +1 -0
- package/dist/modules/trmnl-module.js +111 -0
- package/dist/modules/trmnl-module.js.map +1 -0
- package/dist/openclaw-session.d.ts +6 -0
- package/dist/openclaw-session.d.ts.map +1 -1
- package/dist/openclaw-session.js +7 -1
- package/dist/openclaw-session.js.map +1 -1
- package/dist/timebox/timebox-daemon-sync.d.ts +8 -0
- package/dist/timebox/timebox-daemon-sync.d.ts.map +1 -1
- package/dist/timebox/timebox-daemon-sync.js +29 -1
- package/dist/timebox/timebox-daemon-sync.js.map +1 -1
- package/dist/timeline-summarizer.d.ts +12 -2
- package/dist/timeline-summarizer.d.ts.map +1 -1
- package/dist/timeline-summarizer.js +66 -4
- package/dist/timeline-summarizer.js.map +1 -1
- package/dist/trmnl/byos-server.d.ts +39 -0
- package/dist/trmnl/byos-server.d.ts.map +1 -0
- package/dist/trmnl/byos-server.js +301 -0
- package/dist/trmnl/byos-server.js.map +1 -0
- package/dist/trmnl/frame-cache.d.ts +37 -0
- package/dist/trmnl/frame-cache.d.ts.map +1 -0
- package/dist/trmnl/frame-cache.js +157 -0
- package/dist/trmnl/frame-cache.js.map +1 -0
- package/dist/trmnl/image-renderer.d.ts +27 -0
- package/dist/trmnl/image-renderer.d.ts.map +1 -0
- package/dist/trmnl/image-renderer.js +235 -0
- package/dist/trmnl/image-renderer.js.map +1 -0
- package/dist/trmnl/trmnl-settings.d.ts +96 -0
- package/dist/trmnl/trmnl-settings.d.ts.map +1 -0
- package/dist/trmnl/trmnl-settings.js +177 -0
- package/dist/trmnl/trmnl-settings.js.map +1 -0
- package/dist/trmnl/trmnl-telemetry.d.ts +47 -0
- package/dist/trmnl/trmnl-telemetry.d.ts.map +1 -0
- package/dist/trmnl/trmnl-telemetry.js +56 -0
- package/dist/trmnl/trmnl-telemetry.js.map +1 -0
- package/dist/whisper-server-manager.d.ts +19 -0
- package/dist/whisper-server-manager.d.ts.map +1 -0
- package/dist/whisper-server-manager.js +171 -0
- package/dist/whisper-server-manager.js.map +1 -0
- package/fm-helper/Info.plist +27 -0
- package/package.json +13 -14
- package/scripts/build-fm-helper.mjs +35 -3
- package/src/idotmatrix/sync.py +16 -1
- package/src/pysync/matrix_sync_common.py +83 -0
- package/src/timebox/sync_ble.py +16 -0
package/dist/daemon-server.js
CHANGED
|
@@ -46,6 +46,13 @@ const OBSERVED_APPROVAL_ENABLED = process.env.AGENTDECK_OBSERVED_APPROVAL !== '0
|
|
|
46
46
|
* call to Claude's own permission flow. Must stay well under the hook curl's
|
|
47
47
|
* --max-time 60 so the release reaches Claude before curl gives up. */
|
|
48
48
|
const OBSERVED_APPROVAL_HOLD_MS = Math.min(50_000, Math.max(5_000, Number(process.env.AGENTDECK_APPROVAL_HOLD_MS) || 25_000));
|
|
49
|
+
/** Inactivity after which an unclosed APME run is treated as abandoned and
|
|
50
|
+
* finalized. Deliberately generous: an orphan stays reapable forever, so
|
|
51
|
+
* closing it late costs nothing, while reaping a live run corrupts the unit
|
|
52
|
+
* being measured. Session bridges own their own collector state and are
|
|
53
|
+
* invisible to this daemon, so only elapsed silence protects them (runs this
|
|
54
|
+
* daemon still holds are skipped via `collector.isLiveRun`). */
|
|
55
|
+
const APME_ABANDONED_RUN_STALE_SEC = Math.max(600, Number(process.env.AGENTDECK_APME_ABANDON_SEC) || 7200);
|
|
49
56
|
import { VoiceManager } from './voice.js';
|
|
50
57
|
import { VoiceAssistantManager } from './voice-assistant.js';
|
|
51
58
|
import { listActive as listActiveSessions, findAvailablePort, findExistingDaemon, DAEMON_DEFAULT_PORT, probeDaemonHealth, requestDaemonShutdown, scanDaemonPortWindow, shouldConcedePortToOccupant, waitForDaemonExit, writeDaemonInfo, ensureDaemonInfo, removeDaemonInfo, readDaemonInfo, removeDaemonSession, getCandidateDataDirs, getOwnTimelineFile, } from './session-registry.js';
|
|
@@ -64,7 +71,7 @@ import { FallbackTaskTimeline } from './fallback-task-timeline.js';
|
|
|
64
71
|
import { handleApmeRequest } from './apme/http.js';
|
|
65
72
|
import { readModelFromTranscript } from './apme/claude-transcript-reader.js';
|
|
66
73
|
import { transcriptTimelineForSession, lastAssistantTextFromTranscript, lastAssistantTextForSession, } from './session-transcript-timeline.js';
|
|
67
|
-
import { DeviceVoiceReplyRouter, speakableReply, pcmFromWav, } from './device-voice-reply.js';
|
|
74
|
+
import { DeviceVoiceReplyRouter, speakableReply, spokenDigest, pcmFromWav, } from './device-voice-reply.js';
|
|
68
75
|
import { rawSessionId } from '@agentdeck/shared';
|
|
69
76
|
import { lastAgentMessageFromCodexRollout } from './codex-rollout-response.js';
|
|
70
77
|
import { callFoundationModelsHelper } from './foundation-models-helper.js';
|
|
@@ -75,7 +82,7 @@ import { getConnectedAdbDevices, hasAdb, getAdbDeviceCount } from './adb-reverse
|
|
|
75
82
|
import { getPixooDeviceDetails, pixooDeviceCount } from './pixoo/pixoo-bridge.js';
|
|
76
83
|
import { loadTimeboxDevices } from './timebox/timebox-settings.js';
|
|
77
84
|
import { getLanIp, stripUnsafeText, cleanRawText, prepareMarkdownDetail, normalizeCommandPrompt, formatDurationSec } from '@agentdeck/shared';
|
|
78
|
-
import { injectOpenClawSession } from './openclaw-session.js';
|
|
85
|
+
import { injectOpenClawSession, OPENCLAW_SESSION_ID } from './openclaw-session.js';
|
|
79
86
|
import { buildCardFeed, buildGlance, applyOutboxDecisions, FeedPullTracker, formatFeedPull, normalizeClientIp, parsePullTelemetry, } from './card-feed.js';
|
|
80
87
|
import { threadModule } from './card-modules.js';
|
|
81
88
|
import { AutonomousPocketEngine, createAutonomousPocketModules, parsePocketAutonomyConfig, } from './pocket-autonomy.js';
|
|
@@ -788,8 +795,16 @@ function buildNodeModuleHealth(startedModules) {
|
|
|
788
795
|
modules.timebox = timebox.statusSnapshot();
|
|
789
796
|
}
|
|
790
797
|
else if (configuredTimebox.length > 0) {
|
|
798
|
+
// Configured but the module never started — say so explicitly. An omitted
|
|
799
|
+
// `connected` reads as "no information" to retain-on-absent clients, and a
|
|
800
|
+
// panel nobody is driving must not inherit a stale connected:true.
|
|
791
801
|
modules.timebox = {
|
|
792
802
|
configuredDeviceCount: configuredTimebox.length,
|
|
803
|
+
connected: false,
|
|
804
|
+
statusReason: 'sync module not started',
|
|
805
|
+
displayDimmed: false,
|
|
806
|
+
hasFrame: false,
|
|
807
|
+
lastError: null,
|
|
793
808
|
devices: configuredTimebox.map((d) => ({
|
|
794
809
|
address: d.address,
|
|
795
810
|
name: d.name ?? 'Timebox Mini',
|
|
@@ -803,8 +818,14 @@ function buildNodeModuleHealth(startedModules) {
|
|
|
803
818
|
modules.idotmatrix = idotmatrix.statusSnapshot();
|
|
804
819
|
}
|
|
805
820
|
else if (configuredIDotMatrix.length > 0) {
|
|
821
|
+
// Configured but the module never started — see the Timebox note above.
|
|
806
822
|
modules.idotmatrix = {
|
|
807
823
|
configuredDeviceCount: configuredIDotMatrix.length,
|
|
824
|
+
connected: false,
|
|
825
|
+
statusReason: 'sync module not started',
|
|
826
|
+
displayDimmed: false,
|
|
827
|
+
hasFrame: false,
|
|
828
|
+
lastError: null,
|
|
808
829
|
devices: configuredIDotMatrix.map((d) => ({
|
|
809
830
|
address: d.address,
|
|
810
831
|
name: d.name ?? 'iDotMatrix',
|
|
@@ -983,6 +1004,22 @@ export async function startDaemon(opts) {
|
|
|
983
1004
|
// `gatewaySessionState` / `gatewayModelName` dedicated fields.
|
|
984
1005
|
let gatewaySessionState = 'idle';
|
|
985
1006
|
let gatewayModelName;
|
|
1007
|
+
/**
|
|
1008
|
+
* Snapshot for an `openclaw` state event: the global machine with the
|
|
1009
|
+
* Gateway-local model pinned back on.
|
|
1010
|
+
*
|
|
1011
|
+
* The Gateway's `model_info` no longer writes `core.stateMachine` (see the
|
|
1012
|
+
* parser branch): that machine is fed by every observed Claude/Codex/OpenCode
|
|
1013
|
+
* hook, and the hub stamps its broadcasts with the user's focused session —
|
|
1014
|
+
* so a model written there by a *different* agent is read as the focused
|
|
1015
|
+
* session's own. That is how `GLM-5.2 (1M)` appeared as the model of a Claude
|
|
1016
|
+
* session on both decks. Keeping the model out of the shared slot means the
|
|
1017
|
+
* Gateway's own events have to carry it explicitly, which is what this does.
|
|
1018
|
+
*/
|
|
1019
|
+
const gatewaySnapshot = (base) => ({
|
|
1020
|
+
...(base ?? core.stateMachine.getSnapshot()),
|
|
1021
|
+
modelName: gatewayModelName ?? null,
|
|
1022
|
+
});
|
|
986
1023
|
// Wi-Fi WebSocket e-ink panels (XTeink X3/X4 CrossPoint fork) self-register
|
|
987
1024
|
// their device roster via `client_register{clientType:"eink-device"}` — the
|
|
988
1025
|
// same volunteer model as the Stream Deck plugin. This handler lived ONLY in
|
|
@@ -1982,6 +2019,27 @@ export async function startDaemon(opts) {
|
|
|
1982
2019
|
const respRaw = responseText.length > 0
|
|
1983
2020
|
? cleanRawText(responseText.length > 200 ? responseText.slice(0, 197) + '...' : responseText)
|
|
1984
2021
|
: '';
|
|
2022
|
+
// APME: the text below is the assistant half of the turn — hand it to
|
|
2023
|
+
// the collector on the SAME branch that decides the timeline row.
|
|
2024
|
+
// `setTurnResponse` was reachable only from the PTY session bridge
|
|
2025
|
+
// (bridge/src/index.ts), which hook-observed sessions (direct
|
|
2026
|
+
// `claude` / `codex` / standalone `opencode`) never go through, so
|
|
2027
|
+
// every such turn archived a prompt and its tool calls but no reply:
|
|
2028
|
+
// `turns.response` stayed NULL and no `assistant_message` trajectory
|
|
2029
|
+
// event was ever written. The dashboard therefore could not replay a
|
|
2030
|
+
// conversation the timeline shows in full, and the judge scored those
|
|
2031
|
+
// turns against silence. Empty text is still worth recording — it
|
|
2032
|
+
// tags `response_kind` tool_only/empty so the runner skips judging.
|
|
2033
|
+
if (apme) {
|
|
2034
|
+
if (turnOpen)
|
|
2035
|
+
apme.collector.setTurnResponse(hookSid, responseText);
|
|
2036
|
+
else if (responseText.length > 0) {
|
|
2037
|
+
// No open turn: a missed prompt hook or a late/duplicate stop.
|
|
2038
|
+
// The last-closed fallback refuses to overwrite an existing
|
|
2039
|
+
// response, so a duplicate stop is a no-op instead of a clobber.
|
|
2040
|
+
apme.collector.setLastClosedTurnResponse(hookSid, responseText);
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
1985
2043
|
if (turnOpen && lastStart) {
|
|
1986
2044
|
const now = Date.now();
|
|
1987
2045
|
const taskId = (apme
|
|
@@ -2475,6 +2533,24 @@ export async function startDaemon(opts) {
|
|
|
2475
2533
|
core.wireDisplayMonitor();
|
|
2476
2534
|
let lastStateEvent = null;
|
|
2477
2535
|
let userFocusedSessionId = null;
|
|
2536
|
+
/**
|
|
2537
|
+
* Stamp the focused session onto a hub `state_update`.
|
|
2538
|
+
*
|
|
2539
|
+
* This field carries two jobs, and both are load-bearing, which is why it
|
|
2540
|
+
* cannot simply be narrowed:
|
|
2541
|
+
* - attribution: `buildStateEvent` sets no `sessionId` at all, so a
|
|
2542
|
+
* hook-observed session's AWAITING_PERMISSION options reach the decks
|
|
2543
|
+
* attributed only by this field (see session-deck-awaiting tests).
|
|
2544
|
+
* - focus ack: the Apple app toggles creature focus against it and needs it
|
|
2545
|
+
* even for a session that emits nothing of its own.
|
|
2546
|
+
*
|
|
2547
|
+
* The consequence is a standing invariant on the payload: whatever rides along
|
|
2548
|
+
* here is read as the focused session's own. So nothing may write a *different*
|
|
2549
|
+
* agent's per-session field into `core.stateMachine` — the Gateway's model went
|
|
2550
|
+
* in and came out as a Claude session's model. Gateway-specific state lives in
|
|
2551
|
+
* `gatewaySessionState` / `gatewayModelName` and is re-attached by
|
|
2552
|
+
* `gatewaySnapshot()`.
|
|
2553
|
+
*/
|
|
2478
2554
|
const attachFocusedSessionId = (event) => {
|
|
2479
2555
|
if (event.type !== 'state_update')
|
|
2480
2556
|
return event;
|
|
@@ -2483,6 +2559,28 @@ export async function startDaemon(opts) {
|
|
|
2483
2559
|
focusedSessionId: userFocusedSessionId ?? '',
|
|
2484
2560
|
};
|
|
2485
2561
|
};
|
|
2562
|
+
/**
|
|
2563
|
+
* Stamp a *Gateway-specific* state event — one whose payload really is the
|
|
2564
|
+
* Gateway's (its parser events, its connection transitions), as opposed to the
|
|
2565
|
+
* hub broadcasts above that merely get labelled `openclaw` while the Gateway is
|
|
2566
|
+
* alive.
|
|
2567
|
+
*
|
|
2568
|
+
* These name their own row (`sessionId`), which is the honest attribution and
|
|
2569
|
+
* the reason a client never has to guess: an event carrying the Gateway's model
|
|
2570
|
+
* now says so. The focus ack rides only when the Gateway IS the focused row —
|
|
2571
|
+
* otherwise this event would claim to describe a Claude session, which is
|
|
2572
|
+
* precisely how `GLM-5.2 (1M)` became that session's model on both decks. The
|
|
2573
|
+
* ack still reaches the Apple app through every hub broadcast.
|
|
2574
|
+
*/
|
|
2575
|
+
const attachGatewayEvent = (event) => {
|
|
2576
|
+
if (event.type !== 'state_update')
|
|
2577
|
+
return event;
|
|
2578
|
+
return {
|
|
2579
|
+
...event,
|
|
2580
|
+
sessionId: OPENCLAW_SESSION_ID,
|
|
2581
|
+
focusedSessionId: userFocusedSessionId === OPENCLAW_SESSION_ID ? OPENCLAW_SESSION_ID : '',
|
|
2582
|
+
};
|
|
2583
|
+
};
|
|
2486
2584
|
const broadcastFocusedState = () => {
|
|
2487
2585
|
const gwAlive = gatewayAdapter?.isAlive() ?? false;
|
|
2488
2586
|
const stateEvent = attachFocusedSessionId(core.buildStateEvent({
|
|
@@ -2932,7 +3030,14 @@ export async function startDaemon(opts) {
|
|
|
2932
3030
|
core.stateMachine.handleHookEvent('SessionEnd', {});
|
|
2933
3031
|
break;
|
|
2934
3032
|
case 'parser':
|
|
2935
|
-
|
|
3033
|
+
// `model_info` is deliberately NOT forwarded: the global machine holds
|
|
3034
|
+
// one modelName for the whole hub, fed by every observed hook session,
|
|
3035
|
+
// and its broadcasts are stamped with the user's focused session — so a
|
|
3036
|
+
// model written here by the Gateway is read as that session's own
|
|
3037
|
+
// (GLM on a Claude row). It lives in `gatewayModelName` instead, and
|
|
3038
|
+
// `gatewaySnapshot()` puts it back on the Gateway's own events.
|
|
3039
|
+
if (evt.event !== 'model_info')
|
|
3040
|
+
core.stateMachine.handleParserEvent(evt.event, evt.data);
|
|
2936
3041
|
// Gateway-local activity state for the virtual session row (mirror of
|
|
2937
3042
|
// Swift `gatewaySessionState`). Kept separate from the global
|
|
2938
3043
|
// `core.stateMachine` above so the OpenClaw row reflects only the
|
|
@@ -2961,10 +3066,10 @@ export async function startDaemon(opts) {
|
|
|
2961
3066
|
if (models) {
|
|
2962
3067
|
core.cachedModelCatalog = models;
|
|
2963
3068
|
const snap = core.stateMachine.getSnapshot();
|
|
2964
|
-
const stateEvent =
|
|
3069
|
+
const stateEvent = attachGatewayEvent(core.buildStateEvent({
|
|
2965
3070
|
agentType: 'openclaw',
|
|
2966
3071
|
agentCapabilities: OPENCLAW_CAPABILITIES,
|
|
2967
|
-
snapshot: snap,
|
|
3072
|
+
snapshot: gatewaySnapshot(snap),
|
|
2968
3073
|
}));
|
|
2969
3074
|
lastStateEvent = stateEvent;
|
|
2970
3075
|
core.broadcast(stateEvent);
|
|
@@ -3017,10 +3122,10 @@ export async function startDaemon(opts) {
|
|
|
3017
3122
|
}
|
|
3018
3123
|
// Force full state broadcast
|
|
3019
3124
|
const snap = core.stateMachine.getSnapshot();
|
|
3020
|
-
const gwStateEvent =
|
|
3125
|
+
const gwStateEvent = attachGatewayEvent(core.buildStateEvent({
|
|
3021
3126
|
agentType: 'openclaw',
|
|
3022
3127
|
agentCapabilities: OPENCLAW_CAPABILITIES,
|
|
3023
|
-
snapshot: snap,
|
|
3128
|
+
snapshot: gatewaySnapshot(snap),
|
|
3024
3129
|
}));
|
|
3025
3130
|
lastStateEvent = gwStateEvent;
|
|
3026
3131
|
core.wsServer.broadcast(gwStateEvent);
|
|
@@ -3434,10 +3539,10 @@ export async function startDaemon(opts) {
|
|
|
3434
3539
|
if (target === 'openclaw' && gatewayAdapter?.isAlive()) {
|
|
3435
3540
|
// Force broadcast OpenClaw state to all clients
|
|
3436
3541
|
const snap = core.stateMachine.getSnapshot();
|
|
3437
|
-
const gwStateEvent =
|
|
3542
|
+
const gwStateEvent = attachGatewayEvent(core.buildStateEvent({
|
|
3438
3543
|
agentType: 'openclaw',
|
|
3439
3544
|
agentCapabilities: OPENCLAW_CAPABILITIES,
|
|
3440
|
-
snapshot: snap,
|
|
3545
|
+
snapshot: gatewaySnapshot(snap),
|
|
3441
3546
|
}));
|
|
3442
3547
|
lastStateEvent = gwStateEvent;
|
|
3443
3548
|
core.wsServer.broadcast(gwStateEvent);
|
|
@@ -3799,7 +3904,18 @@ export async function startDaemon(opts) {
|
|
|
3799
3904
|
const voiceCfg = loadDaemonSettings().voice;
|
|
3800
3905
|
if (voiceCfg?.speakReplies === false)
|
|
3801
3906
|
return; // opt-out, default on
|
|
3802
|
-
|
|
3907
|
+
// Speech gets the lead, not the transcript: an explicit summary line if the
|
|
3908
|
+
// answer has one, else its first sentence. The full answer is already on the
|
|
3909
|
+
// screen the user is sitting at, where it can be skimmed — read aloud it is
|
|
3910
|
+
// a minute of monologue nobody can skip. `voice.speakFullReply` opts back in.
|
|
3911
|
+
const full = voiceCfg?.speakFullReply === true;
|
|
3912
|
+
const spoken = spokenDigest(text, { full });
|
|
3913
|
+
if (!full) {
|
|
3914
|
+
const readable = speakableReply(text, Number.MAX_SAFE_INTEGER);
|
|
3915
|
+
if (spoken && readable.length > spoken.length) {
|
|
3916
|
+
log(`[agentdeck] voice: reply digested ${readable.length} -> ${spoken.length} chars`);
|
|
3917
|
+
}
|
|
3918
|
+
}
|
|
3803
3919
|
if (!spoken) {
|
|
3804
3920
|
// A code-only answer has nothing worth reading aloud; say that instead of
|
|
3805
3921
|
// spelling out punctuation, so the user still knows the turn finished.
|
|
@@ -4649,6 +4765,43 @@ export async function startDaemon(opts) {
|
|
|
4649
4765
|
for (const id of orphans) {
|
|
4650
4766
|
apme.store.updateRun(id, { endedAt: Date.now(), taskCategory: '_empty' });
|
|
4651
4767
|
}
|
|
4768
|
+
// 5. Reap ABANDONED runs — the ones step 4 cannot see. A daemon restart
|
|
4769
|
+
// (or crash) drops the in-memory session→run map, so every run that
|
|
4770
|
+
// was mid-session is left with ended_at NULL forever. Those carry real
|
|
4771
|
+
// prompts and turns, so they fail step 4's empty-shell predicate, and
|
|
4772
|
+
// because their TASK also never closes they are never evaluated: the
|
|
4773
|
+
// store had accumulated 65 open tasks against 9 closed ones.
|
|
4774
|
+
// Closing the run makes step 1 pick it up next tick; the task eval has
|
|
4775
|
+
// to be enqueued here, since its normal trigger (`onTaskClosed`) fires
|
|
4776
|
+
// from the collector's in-memory close path, which is exactly what
|
|
4777
|
+
// went missing.
|
|
4778
|
+
// Batched small: `enqueueTask` dispatches immediately with no
|
|
4779
|
+
// concurrency cap, so a backlog is drained a few per tick rather than
|
|
4780
|
+
// fired at the judge all at once.
|
|
4781
|
+
const abandoned = apme.store.listAbandonedRuns(APME_ABANDONED_RUN_STALE_SEC, 5);
|
|
4782
|
+
for (const run of abandoned) {
|
|
4783
|
+
if (apme.collector.isLiveRun(run.id))
|
|
4784
|
+
continue; // still owned by us
|
|
4785
|
+
const closedTasks = apme.store.reapAbandonedRun(run.id, run.lastActivity);
|
|
4786
|
+
for (const task of closedTasks) {
|
|
4787
|
+
// Judge only what there is something to judge. The backlog this
|
|
4788
|
+
// reaper drains predates the response-capture fix, so most of those
|
|
4789
|
+
// tasks hold prompts and tool calls but no reply — scoring them would
|
|
4790
|
+
// push hundreds of "judged against silence" rows into the scorecard,
|
|
4791
|
+
// the same noise `response_kind` exists to keep out. Closing the row
|
|
4792
|
+
// is the data-hygiene win; the eval is a bonus when it can be earned.
|
|
4793
|
+
const hasReply = apme.store.listTurnsForTask(task.id)
|
|
4794
|
+
.some((t) => typeof t.response === 'string' && t.response.trim().length > 0);
|
|
4795
|
+
if (!hasReply)
|
|
4796
|
+
continue;
|
|
4797
|
+
apme.runner.enqueueTask({
|
|
4798
|
+
runId: run.id, taskId: task.id,
|
|
4799
|
+
...(task.category ? { category: task.category } : {}),
|
|
4800
|
+
boundarySignal: 'orphaned',
|
|
4801
|
+
});
|
|
4802
|
+
}
|
|
4803
|
+
debug('APME', `reaped abandoned run ${run.id.slice(0, 8)} — ${closedTasks.length} task(s) closed`);
|
|
4804
|
+
}
|
|
4652
4805
|
}, 30_000); // every 30s
|
|
4653
4806
|
core.addInterval(apmeEvalTimer);
|
|
4654
4807
|
}
|