@agentdeck/bridge 1.0.20 → 1.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/openclaw.d.ts +55 -1
- package/dist/adapters/openclaw.d.ts.map +1 -1
- package/dist/adapters/openclaw.js +278 -45
- package/dist/adapters/openclaw.js.map +1 -1
- package/dist/adb-reverse.d.ts +26 -3
- package/dist/adb-reverse.d.ts.map +1 -1
- package/dist/adb-reverse.js +26 -5
- package/dist/adb-reverse.js.map +1 -1
- package/dist/apme/adapters/openclaw-hook.d.ts +45 -1
- package/dist/apme/adapters/openclaw-hook.d.ts.map +1 -1
- package/dist/apme/adapters/openclaw-hook.js +84 -4
- package/dist/apme/adapters/openclaw-hook.js.map +1 -1
- package/dist/apme/claude-transcript-reader.d.ts +54 -0
- package/dist/apme/claude-transcript-reader.d.ts.map +1 -1
- package/dist/apme/claude-transcript-reader.js +77 -0
- package/dist/apme/claude-transcript-reader.js.map +1 -1
- package/dist/apme/collector.d.ts +63 -5
- package/dist/apme/collector.d.ts.map +1 -1
- package/dist/apme/collector.js +147 -10
- package/dist/apme/collector.js.map +1 -1
- package/dist/apme/store.d.ts +23 -1
- package/dist/apme/store.d.ts.map +1 -1
- package/dist/apme/store.js +81 -2
- 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/auth.d.ts +37 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +23 -0
- package/dist/auth.js.map +1 -1
- package/dist/claude-interrupt-marker.d.ts +31 -0
- package/dist/claude-interrupt-marker.d.ts.map +1 -0
- package/dist/claude-interrupt-marker.js +58 -0
- package/dist/claude-interrupt-marker.js.map +1 -0
- package/dist/claude-turn-watchdog.d.ts +23 -0
- package/dist/claude-turn-watchdog.d.ts.map +1 -1
- package/dist/claude-turn-watchdog.js +30 -4
- package/dist/claude-turn-watchdog.js.map +1 -1
- package/dist/cli.d.ts +6 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +398 -115
- package/dist/cli.js.map +1 -1
- package/dist/daemon-port.d.ts +90 -0
- package/dist/daemon-port.d.ts.map +1 -0
- package/dist/daemon-port.js +119 -0
- package/dist/daemon-port.js.map +1 -0
- package/dist/daemon-server.d.ts +14 -1
- package/dist/daemon-server.d.ts.map +1 -1
- package/dist/daemon-server.js +297 -69
- package/dist/daemon-server.js.map +1 -1
- package/dist/daemon-settings.d.ts +46 -0
- package/dist/daemon-settings.d.ts.map +1 -0
- package/dist/daemon-settings.js +85 -0
- package/dist/daemon-settings.js.map +1 -0
- package/dist/daemon-takeover.d.ts +75 -0
- package/dist/daemon-takeover.d.ts.map +1 -0
- package/dist/daemon-takeover.js +137 -0
- package/dist/daemon-takeover.js.map +1 -0
- package/dist/daemon.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +30 -11
- package/dist/index.js.map +1 -1
- package/dist/kiro-diagnostics.d.ts +90 -0
- package/dist/kiro-diagnostics.d.ts.map +1 -0
- package/dist/kiro-diagnostics.js +278 -0
- package/dist/kiro-diagnostics.js.map +1 -0
- package/dist/kiro-session.d.ts +113 -0
- package/dist/kiro-session.d.ts.map +1 -0
- package/dist/kiro-session.js +688 -0
- package/dist/kiro-session.js.map +1 -0
- package/dist/kiro-timeline-feed.d.ts +45 -0
- package/dist/kiro-timeline-feed.d.ts.map +1 -0
- package/dist/kiro-timeline-feed.js +116 -0
- package/dist/kiro-timeline-feed.js.map +1 -0
- package/dist/kiro-transcript-timeline.d.ts +57 -0
- package/dist/kiro-transcript-timeline.d.ts.map +1 -0
- package/dist/kiro-transcript-timeline.js +183 -0
- package/dist/kiro-transcript-timeline.js.map +1 -0
- package/dist/linux-service.d.ts +2 -1
- package/dist/linux-service.d.ts.map +1 -1
- package/dist/linux-service.js +8 -3
- package/dist/linux-service.js.map +1 -1
- package/dist/mdns-discover.d.ts +27 -0
- package/dist/mdns-discover.d.ts.map +1 -1
- package/dist/mdns-discover.js +60 -7
- package/dist/mdns-discover.js.map +1 -1
- package/dist/mdns.d.ts.map +1 -1
- package/dist/mdns.js +16 -3
- package/dist/mdns.js.map +1 -1
- package/dist/modules/adb-module.d.ts.map +1 -1
- package/dist/modules/adb-module.js +5 -2
- package/dist/modules/adb-module.js.map +1 -1
- package/dist/modules/index.d.ts +2 -1
- package/dist/modules/index.d.ts.map +1 -1
- package/dist/modules/index.js +1 -0
- package/dist/modules/index.js.map +1 -1
- package/dist/modules/types.d.ts +34 -9
- package/dist/modules/types.d.ts.map +1 -1
- package/dist/modules/types.js +34 -1
- package/dist/modules/types.js.map +1 -1
- package/dist/network-posture.d.ts +67 -0
- package/dist/network-posture.d.ts.map +1 -0
- package/dist/network-posture.js +100 -0
- package/dist/network-posture.js.map +1 -0
- package/dist/observed-steering.d.ts +14 -0
- package/dist/observed-steering.d.ts.map +1 -1
- package/dist/observed-steering.js +18 -0
- package/dist/observed-steering.js.map +1 -1
- package/dist/observed-turn-watchdogs.d.ts +84 -0
- package/dist/observed-turn-watchdogs.d.ts.map +1 -0
- package/dist/observed-turn-watchdogs.js +134 -0
- package/dist/observed-turn-watchdogs.js.map +1 -0
- package/dist/openclaw-session.d.ts +10 -0
- package/dist/openclaw-session.d.ts.map +1 -1
- package/dist/openclaw-session.js +12 -0
- package/dist/openclaw-session.js.map +1 -1
- package/dist/passive-observer.d.ts +19 -0
- package/dist/passive-observer.d.ts.map +1 -1
- package/dist/passive-observer.js +208 -24
- package/dist/passive-observer.js.map +1 -1
- package/dist/pixoo/micro-glyphs.d.ts +1 -1
- package/dist/pixoo/micro-glyphs.d.ts.map +1 -1
- package/dist/pixoo/micro-glyphs.js +2 -0
- package/dist/pixoo/micro-glyphs.js.map +1 -1
- package/dist/pixoo/official-dot-glyphs.generated.d.ts +1 -1
- package/dist/pixoo/official-dot-glyphs.generated.d.ts.map +1 -1
- package/dist/pixoo/official-dot-glyphs.generated.js +47 -0
- package/dist/pixoo/official-dot-glyphs.generated.js.map +1 -1
- package/dist/pixoo/pixoo-discover.d.ts +8 -1
- package/dist/pixoo/pixoo-discover.d.ts.map +1 -1
- package/dist/pixoo/pixoo-discover.js +15 -8
- package/dist/pixoo/pixoo-discover.js.map +1 -1
- package/dist/pixoo/pixoo-renderer.d.ts +4 -2
- package/dist/pixoo/pixoo-renderer.d.ts.map +1 -1
- package/dist/pixoo/pixoo-renderer.js +105 -36
- package/dist/pixoo/pixoo-renderer.js.map +1 -1
- package/dist/pixoo/pixoo-settings.d.ts +19 -2
- package/dist/pixoo/pixoo-settings.d.ts.map +1 -1
- package/dist/pixoo/pixoo-settings.js +22 -4
- package/dist/pixoo/pixoo-settings.js.map +1 -1
- package/dist/pixoo/pixoo-sprites.d.ts.map +1 -1
- package/dist/pixoo/pixoo-sprites.js +2 -0
- package/dist/pixoo/pixoo-sprites.js.map +1 -1
- package/dist/session-aggregator.d.ts +9 -1
- package/dist/session-aggregator.d.ts.map +1 -1
- package/dist/session-aggregator.js +1 -1
- package/dist/session-aggregator.js.map +1 -1
- package/dist/session-registry.d.ts +71 -3
- package/dist/session-registry.d.ts.map +1 -1
- package/dist/session-registry.js +126 -19
- package/dist/session-registry.js.map +1 -1
- package/dist/subagent-timeline.d.ts +38 -7
- package/dist/subagent-timeline.d.ts.map +1 -1
- package/dist/subagent-timeline.js +154 -23
- package/dist/subagent-timeline.js.map +1 -1
- package/dist/timeline-store.d.ts.map +1 -1
- package/dist/timeline-store.js +26 -1
- package/dist/timeline-store.js.map +1 -1
- package/dist/tui/renderer.d.ts.map +1 -1
- package/dist/tui/renderer.js +13 -1
- package/dist/tui/renderer.js.map +1 -1
- package/dist/tui/terrarium.d.ts.map +1 -1
- package/dist/tui/terrarium.js +10 -5
- package/dist/tui/terrarium.js.map +1 -1
- package/dist/utils/redact-secrets.d.ts +3 -0
- package/dist/utils/redact-secrets.d.ts.map +1 -0
- package/dist/utils/redact-secrets.js +29 -0
- package/dist/utils/redact-secrets.js.map +1 -0
- package/dist/windows-service.d.ts +2 -1
- package/dist/windows-service.d.ts.map +1 -1
- package/dist/windows-service.js +6 -3
- package/dist/windows-service.js.map +1 -1
- package/package.json +3 -3
package/dist/daemon-server.js
CHANGED
|
@@ -28,7 +28,7 @@ import { createPushChannelHandler } from './session-push-channel.js';
|
|
|
28
28
|
import { setAwaitingOverlay, setPermissionNotificationOverlay, setAskUserQuestionOverlay, advanceAskUserQuestionOverlay, getAskUserQuestionAnswers, clearAwaitingOverlay, clearAskUserQuestionOverlay, getAwaitingOverlay, isPermissionNotification, applyAwaitingOverlayToObserved, } from './awaiting-overlay.js';
|
|
29
29
|
import { askEchoMatches, askGateDecision, askPressVerdict } from './ask-gate.js';
|
|
30
30
|
import { registerPending, resolvePending, resolvePendingWithReason, sweepStalePending, drainAllPending, isPendingRequest, } from './permission-resolver.js';
|
|
31
|
-
import { shouldHoldPreToolUse, beginAskGate, gateReleased, buildGateQuestion, buildAskAnswerReason, consumeStop, requestStop, clearStop, STOP_DENY_REASON, queueDirective,
|
|
31
|
+
import { shouldHoldPreToolUse, beginAskGate, gateReleased, buildGateQuestion, buildAskAnswerReason, consumeStop, requestStop, clearStop, STOP_DENY_REASON, queueDirective, takeDirectiveForStop, clearOnUserPrompt, clearSession as clearSteeringSession, } from './observed-steering.js';
|
|
32
32
|
import { notePermissionPromptShown, noteToolEnd, steeringSnapshot, } from './observed-steering.js';
|
|
33
33
|
import { resolveSessionIdPrefix } from './session-id-resolve.js';
|
|
34
34
|
import { injectObservedSelection, injectObservedText } from './observed-inject.js';
|
|
@@ -72,7 +72,10 @@ const ASK_GATE_HOLD_MS = Math.min(50_000, Math.max(5_000, Number(process.env.AGE
|
|
|
72
72
|
const APME_ABANDONED_RUN_STALE_SEC = Math.max(600, Number(process.env.AGENTDECK_APME_ABANDON_SEC) || 7200);
|
|
73
73
|
import { VoiceManager } from './voice.js';
|
|
74
74
|
import { VoiceAssistantManager } from './voice-assistant.js';
|
|
75
|
-
import { listActive as listActiveSessions, findAvailablePort, findExistingDaemon,
|
|
75
|
+
import { listActive as listActiveSessions, findAvailablePort, findExistingDaemon, probeDaemonHealth, requestDaemonShutdown, scanDaemonPortWindow, shouldConcedePortToOccupant, waitForDaemonExit, waitForPortBindable, writeDaemonInfo, ensureDaemonInfo, removeDaemonInfo, readDaemonInfo, removeDaemonSession, getCandidateDataDirs, getOwnTimelineFile, } from './session-registry.js';
|
|
76
|
+
import { isForeignDaemon } from './daemon-takeover.js';
|
|
77
|
+
import { loadDaemonSettings } from './daemon-settings.js';
|
|
78
|
+
import { resolveDaemonPort, describeDaemonPortSource, PREFERRED_PORT_RECLAIM_MS, } from './daemon-port.js';
|
|
76
79
|
import { fetchUsageFromApi, hasOAuthToken, resetConsecutiveFailures } from './usage-api.js';
|
|
77
80
|
import { getOrCreateToken, isLocalConnection, validateToken } from './auth.js';
|
|
78
81
|
import { buildPublicHealth, gateHttpRequest, isAuthorizedHttpRequest } from './http-auth-gate.js';
|
|
@@ -85,16 +88,20 @@ import { rgbToBmp, pixooLiveHtml } from './hook-server.js';
|
|
|
85
88
|
import { enableDebugLog, debug, debugThrottled } from './logger.js';
|
|
86
89
|
import { CodexOtelTracker, CODEX_OTEL_TRACES_PATH, spanNameSummary } from './codex-otel.js';
|
|
87
90
|
import { HookCodexSessions } from './hook-codex-sessions.js';
|
|
91
|
+
import { ObservedTurnWatchdogs } from './observed-turn-watchdogs.js';
|
|
88
92
|
import { initApme, isTimelineProjectionEnabled, loadApmeConfig } from './apme/index.js';
|
|
89
93
|
import { FallbackTaskTimeline } from './fallback-task-timeline.js';
|
|
90
94
|
import { handleApmeRequest } from './apme/http.js';
|
|
91
95
|
import { readModelFromTranscript } from './apme/claude-transcript-reader.js';
|
|
92
96
|
import { transcriptTimelineForSession, lastAssistantTextFromTranscript, lastAssistantTextForSession, } from './session-transcript-timeline.js';
|
|
97
|
+
import { kiroTimelineForSession } from './kiro-transcript-timeline.js';
|
|
98
|
+
import { KiroTimelineFeed } from './kiro-timeline-feed.js';
|
|
93
99
|
import { DeviceVoiceReplyRouter, speakableReply, spokenDigest, pcmFromWav, } from './device-voice-reply.js';
|
|
94
100
|
import { rawSessionId } from '@agentdeck/shared';
|
|
95
101
|
import { codexTurnOutcomeFromRollout, lastAgentMessageFromCodexRollout } from './codex-rollout-response.js';
|
|
96
102
|
import { callFoundationModelsHelper } from './foundation-models-helper.js';
|
|
97
103
|
import { initModules, stopModules, createDefaultModules, } from './modules/index.js';
|
|
104
|
+
import { bindHostFor, daemonModuleConfigs, describeDaemonPosture, resolveDaemonPosture, } from './network-posture.js';
|
|
98
105
|
import { esp32ConnectionCount, getESP32DeviceInfo, onESP32Message, sendAuthProvisionToAll, sendWifiProvision, sendWifiProvisionToAll, handleESP32Wake, getESP32Ports, getSerialConnectionStatus, getSerialLastError, getSerialReachableBoards } from './esp32-serial.js';
|
|
99
106
|
import { loadWifiConfig } from './wifi-config.js';
|
|
100
107
|
import { getConnectedAdbDevices, hasAdb, getAdbDeviceCount } from './adb-reverse.js';
|
|
@@ -580,30 +587,6 @@ async function performWifiEsp32Ota(core, target, firmwarePath) {
|
|
|
580
587
|
throw err;
|
|
581
588
|
}
|
|
582
589
|
}
|
|
583
|
-
function loadDaemonSettings() {
|
|
584
|
-
// Newest settings.json across candidate data dirs — mirrors the
|
|
585
|
-
// daemon.json/sessions.json cross-dir discovery (and honors the
|
|
586
|
-
// AGENTDECK_DATA_DIR test override, which the old hardcoded
|
|
587
|
-
// ~/.agentdeck path ignored). The App Store sandbox container is
|
|
588
|
-
// intentionally NOT a candidate (TCC hang risk — see
|
|
589
|
-
// getCandidateDataDirs), so settings written by the sandboxed Swift
|
|
590
|
-
// app stay invisible here; that coexistence limit is documented in
|
|
591
|
-
// docs/appstore-feature-matrix.md.
|
|
592
|
-
let best = null;
|
|
593
|
-
for (const dir of getCandidateDataDirs()) {
|
|
594
|
-
try {
|
|
595
|
-
const path = join(dir, 'settings.json');
|
|
596
|
-
const mtime = statSync(path).mtimeMs;
|
|
597
|
-
if (best && best.mtime >= mtime)
|
|
598
|
-
continue;
|
|
599
|
-
best = { mtime, parsed: JSON.parse(readFileSync(path, 'utf-8')) };
|
|
600
|
-
}
|
|
601
|
-
catch {
|
|
602
|
-
// Missing or unreadable — skip this candidate.
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
return best?.parsed ?? {};
|
|
606
|
-
}
|
|
607
590
|
function latestTimelinePath() {
|
|
608
591
|
let best = null;
|
|
609
592
|
for (const dir of getCandidateDataDirs()) {
|
|
@@ -767,15 +750,19 @@ export function classifyObservedHookEvent(eventName, mapped) {
|
|
|
767
750
|
if (eventName === 'codex_subagent_start' || eventName === 'codex_subagent_stop') {
|
|
768
751
|
return { boundary: eventName, agentType: 'codex-cli' };
|
|
769
752
|
}
|
|
770
|
-
const prefixed = /^(codex|opencode|antigravity)_(session_start|session_end|user_prompt_submit|tool_start|tool_end|stop|turn_complete|notification|permission_asked|permission_replied)$/
|
|
753
|
+
const prefixed = /^(codex|opencode|antigravity|kiro|kiro_ide)_(agent_spawn|session_start|session_end|user_prompt_submit|tool_start|tool_end|stop|turn_complete|notification|permission_asked|permission_replied)$/
|
|
771
754
|
.exec(eventName);
|
|
772
755
|
if (!prefixed)
|
|
773
756
|
return { boundary: mapped, agentType: 'claude-code' };
|
|
774
757
|
return {
|
|
775
|
-
boundary: prefixed[2] === 'turn_complete' ? 'stop'
|
|
758
|
+
boundary: prefixed[2] === 'turn_complete' ? 'stop'
|
|
759
|
+
: prefixed[2] === 'agent_spawn' ? 'session_start'
|
|
760
|
+
: prefixed[2],
|
|
776
761
|
agentType: prefixed[1] === 'codex' ? 'codex-cli'
|
|
777
762
|
: prefixed[1] === 'opencode' ? 'opencode'
|
|
778
|
-
: 'antigravity'
|
|
763
|
+
: prefixed[1] === 'antigravity' ? 'antigravity'
|
|
764
|
+
: prefixed[1] === 'kiro_ide' ? 'kiro-ide'
|
|
765
|
+
: 'kiro-cli',
|
|
779
766
|
};
|
|
780
767
|
}
|
|
781
768
|
function buildNodeModuleHealth(startedModules) {
|
|
@@ -901,6 +888,10 @@ export async function startDaemon(opts) {
|
|
|
901
888
|
// CLI --wake-word flag OR settings.json wakeWord: true
|
|
902
889
|
const settings = loadDaemonSettings();
|
|
903
890
|
const wakeWordEnabled = opts.wakeWord || settings.wakeWord === true;
|
|
891
|
+
// Network posture (bind address + which device modules may run). Resolved once
|
|
892
|
+
// here so the bind, the module set, and the startup log cannot disagree about
|
|
893
|
+
// what this daemon is allowed to do.
|
|
894
|
+
const posture = resolveDaemonPosture({ local: opts.local, loopback: opts.loopback });
|
|
904
895
|
// ===== Singleton guard + port allocation =====
|
|
905
896
|
// 1. Check daemon.json and sessions.json for existing daemon
|
|
906
897
|
const existingInfo = readDaemonInfo();
|
|
@@ -948,32 +939,46 @@ export async function startDaemon(opts) {
|
|
|
948
939
|
removeDaemonSession(existingSession);
|
|
949
940
|
}
|
|
950
941
|
}
|
|
951
|
-
// 2. Determine port —
|
|
952
|
-
|
|
942
|
+
// 2. Determine the preferred port — the one this daemon intends to serve.
|
|
943
|
+
// `opts.port` is already resolved by the CLI (flag › AGENTDECK_DAEMON_PORT ›
|
|
944
|
+
// settings.json `daemonPort` › 9120); resolving again here covers the
|
|
945
|
+
// library entry point, and both paths read the same sources.
|
|
946
|
+
const preferred = opts.port !== undefined
|
|
947
|
+
? { port: opts.port, source: opts.portSource ?? 'flag' }
|
|
948
|
+
: resolveDaemonPort();
|
|
949
|
+
const requestedPort = preferred.port;
|
|
953
950
|
let port = requestedPort;
|
|
954
|
-
//
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
}
|
|
951
|
+
// Probe the preferred port before binding it. This used to run only for the
|
|
952
|
+
// built-in 9120, which made every other preferred port skip the Swift-daemon
|
|
953
|
+
// eviction and the occupied-port check — a persisted `daemonPort: 9200` would
|
|
954
|
+
// have been a second-class port on the same machine. The port a daemon
|
|
955
|
+
// intends to serve gets the same treatment wherever the intent came from.
|
|
956
|
+
const preferredOccupant = await probeDaemonHealth(requestedPort);
|
|
957
|
+
if (preferredOccupant) {
|
|
958
|
+
if (preferredOccupant.mode === 'daemon' && isForeignDaemon(preferredOccupant)) {
|
|
959
|
+
// Another OS user's daemon. Neither evict it nor concede to it — this
|
|
960
|
+
// user still gets a daemon, on a port of their own.
|
|
961
|
+
log(`[agentdeck] Port ${requestedPort} is held by another user's daemon — leaving it alone and taking a free port.`);
|
|
962
|
+
port = await findAvailablePort();
|
|
963
|
+
}
|
|
964
|
+
else if (preferredOccupant.mode === 'daemon') {
|
|
965
|
+
if (preferredOccupant.isSwift) {
|
|
966
|
+
log(`[agentdeck] Swift daemon detected on port ${requestedPort} via /health. Requesting shutdown to take over...`);
|
|
967
|
+
await requestDaemonShutdown(requestedPort);
|
|
968
|
+
await waitForDaemonExit(requestedPort);
|
|
969
|
+
await waitForPortBindable(requestedPort);
|
|
970
970
|
}
|
|
971
971
|
else {
|
|
972
|
-
//
|
|
973
|
-
log(`[agentdeck]
|
|
974
|
-
|
|
972
|
+
// Daemon alive but not in our registry — race condition or stale state
|
|
973
|
+
log(`[agentdeck] Daemon already running on port ${requestedPort} (detected via /health).`);
|
|
974
|
+
process.exit(0);
|
|
975
975
|
}
|
|
976
976
|
}
|
|
977
|
+
else {
|
|
978
|
+
// Port occupied by non-daemon (e.g. session bridge) — find alternative
|
|
979
|
+
log(`[agentdeck] Port ${requestedPort} occupied (${preferredOccupant.mode ?? 'unknown'}), finding alternative...`);
|
|
980
|
+
port = await findAvailablePort();
|
|
981
|
+
}
|
|
977
982
|
}
|
|
978
983
|
// 3. Cross-implementation sweep of the daemon port window. File discovery
|
|
979
984
|
// has two blind spots that the checks above never cover: the App Store
|
|
@@ -985,6 +990,14 @@ export async function startDaemon(opts) {
|
|
|
985
990
|
// a live Node daemon wins and we exit.
|
|
986
991
|
const strayDaemons = await scanDaemonPortWindow(new Set([requestedPort]));
|
|
987
992
|
for (const stray of strayDaemons) {
|
|
993
|
+
if (isForeignDaemon(stray.health)) {
|
|
994
|
+
// Not a stray — someone else's daemon, doing its job for its own user.
|
|
995
|
+
// This sweep runs on every start, so without this check one user
|
|
996
|
+
// starting a CLI daemon shuts down every colleague's macOS app on the
|
|
997
|
+
// box (docs/ENTERPRISE-ROADMAP.md §1.3).
|
|
998
|
+
log(`[agentdeck] Port ${stray.port} is held by another user's daemon — not touching it.`);
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
988
1001
|
if (stray.health.isSwift) {
|
|
989
1002
|
log(`[agentdeck] Swift daemon detected on fallback port ${stray.port}. Requesting shutdown to take over...`);
|
|
990
1003
|
await requestDaemonShutdown(stray.port);
|
|
@@ -1017,6 +1030,53 @@ export async function startDaemon(opts) {
|
|
|
1017
1030
|
* anything arrived SINCE this turn opened.
|
|
1018
1031
|
*/
|
|
1019
1032
|
const hookSessionLastSeenAt = new Map();
|
|
1033
|
+
/**
|
|
1034
|
+
* Missed-Stop recovery for observed Claude sessions (plain `claude` in the
|
|
1035
|
+
* user's own terminal — no PTY, hooks only).
|
|
1036
|
+
*
|
|
1037
|
+
* The Stop hook is the sole authority that closes a Claude turn, and its
|
|
1038
|
+
* curl is fire-and-forget with a tight budget, so it drops. `agentdeck
|
|
1039
|
+
* claude` has recovered from that since 1.0.20; observed sessions had
|
|
1040
|
+
* nothing, which is the majority of real usage. Recovery re-enters this
|
|
1041
|
+
* daemon's own `/hooks/Stop` over loopback rather than reimplementing that
|
|
1042
|
+
* branch — see the class doc for why the same path matters.
|
|
1043
|
+
*
|
|
1044
|
+
* Declared before the HTTP server (hooks arrive the moment the port binds)
|
|
1045
|
+
* and, like every other route, trusted purely because it is loopback: the
|
|
1046
|
+
* auth gate treats same-machine as fully authorized, so no token is
|
|
1047
|
+
* involved and none is minted.
|
|
1048
|
+
*/
|
|
1049
|
+
const observedStopWatchdogs = new ObservedTurnWatchdogs({
|
|
1050
|
+
onMissedStop: ({ sessionId, transcriptPath, reason, cwd, projectName }) => {
|
|
1051
|
+
const url = `http://127.0.0.1:${port}/hooks/Stop`;
|
|
1052
|
+
const controller = new AbortController();
|
|
1053
|
+
// Bounded like every other peer await, loopback or not: a daemon
|
|
1054
|
+
// mid-shutdown accepts the socket and never answers, and an unbounded
|
|
1055
|
+
// wait there would pin the recovery forever.
|
|
1056
|
+
const timer = setTimeout(() => controller.abort(), 2_000);
|
|
1057
|
+
void fetch(url, {
|
|
1058
|
+
method: 'POST',
|
|
1059
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1060
|
+
signal: controller.signal,
|
|
1061
|
+
body: JSON.stringify({
|
|
1062
|
+
session_id: sessionId,
|
|
1063
|
+
transcript_path: transcriptPath,
|
|
1064
|
+
// Tags the APME turn `end_source='synthetic_stop'`, which is the
|
|
1065
|
+
// standing measurement of how often the real hook drops — unless
|
|
1066
|
+
// the user cancelled, in which case no Stop was ever due and the
|
|
1067
|
+
// turn must not be charged to that rate.
|
|
1068
|
+
synthetic_stop: true,
|
|
1069
|
+
...(reason === 'interrupted' ? { interrupted: true } : {}),
|
|
1070
|
+
// Same for a client abort (usage limit, auth, API error): Claude
|
|
1071
|
+
// Code fires no Stop for it, so nothing was lost to recover.
|
|
1072
|
+
...(reason === 'aborted' ? { aborted: true } : {}),
|
|
1073
|
+
...(cwd ? { cwd } : {}),
|
|
1074
|
+
...(projectName ? { project_name: projectName } : {}),
|
|
1075
|
+
}),
|
|
1076
|
+
}).catch(() => { })
|
|
1077
|
+
.finally(() => clearTimeout(timer));
|
|
1078
|
+
},
|
|
1079
|
+
});
|
|
1020
1080
|
// Codex OTel span state. Declared before the HTTP server because the
|
|
1021
1081
|
// `/otel/v1/traces` handler closes over it — Codex's exporter can POST before
|
|
1022
1082
|
// the rest of daemon startup finishes.
|
|
@@ -1030,6 +1090,9 @@ export async function startDaemon(opts) {
|
|
|
1030
1090
|
// the rest of startup finishes. A later `const` would leave that window
|
|
1031
1091
|
// throwing on the temporal dead zone.
|
|
1032
1092
|
const passiveSessionObserver = new PassiveSessionObserver();
|
|
1093
|
+
/** Produces live timeline rows for observed Kiro sessions — see the class
|
|
1094
|
+
* doc for why Kiro needs a producer when hook agents do not. */
|
|
1095
|
+
const kiroTimelineFeed = new KiroTimelineFeed();
|
|
1033
1096
|
/** Observed Claude sessions whose AskUserQuestion PreToolUse is held open for
|
|
1034
1097
|
* a device answer, keyed by the bare Claude session UUID. Its presence is
|
|
1035
1098
|
* what makes the session's options pressable (`liveAnswerable`) and what
|
|
@@ -1255,6 +1318,11 @@ export async function startDaemon(opts) {
|
|
|
1255
1318
|
// Workers require this flag before remote-attaching; the Swift daemon
|
|
1256
1319
|
// does not advertise it and is therefore never selected as a remote hub.
|
|
1257
1320
|
sameSocketControl: true,
|
|
1321
|
+
// Network posture, so `agentdeck daemon restart` can carry the running
|
|
1322
|
+
// daemon's posture across the restart instead of silently downgrading
|
|
1323
|
+
// an enterprise install back to "advertise everything". Local-only
|
|
1324
|
+
// payload; it is diagnostic, not a credential.
|
|
1325
|
+
posture,
|
|
1258
1326
|
modules: moduleHealthProvider(),
|
|
1259
1327
|
apme: apme
|
|
1260
1328
|
? {
|
|
@@ -1887,13 +1955,22 @@ export async function startDaemon(opts) {
|
|
|
1887
1955
|
const earlyHookProject = (typeof json.project_name === 'string' && json.project_name)
|
|
1888
1956
|
? json.project_name
|
|
1889
1957
|
: (earlyHookCwd ? earlyHookCwd.split('/').filter(Boolean).pop() : undefined);
|
|
1890
|
-
const
|
|
1958
|
+
const childResult = subagentTimeline?.handle({
|
|
1891
1959
|
eventName,
|
|
1892
1960
|
payload: json,
|
|
1893
1961
|
sessionId: earlyHookSid,
|
|
1894
1962
|
agentType: hookAgentType,
|
|
1895
1963
|
projectName: earlyHookProject,
|
|
1896
|
-
})
|
|
1964
|
+
});
|
|
1965
|
+
// A child starting or stopping is the ONLY thing that moves a parent's
|
|
1966
|
+
// `subagents` block, and it moves nothing else — the parent's own state
|
|
1967
|
+
// is untouched by design. Without this rebroadcast the census would sit
|
|
1968
|
+
// stale until some unrelated event happened to refresh the list, which
|
|
1969
|
+
// for an idle parent waiting on eight children is never.
|
|
1970
|
+
if (childResult?.censusChangedFor) {
|
|
1971
|
+
core.broadcastSessionsList().catch(() => { });
|
|
1972
|
+
}
|
|
1973
|
+
const childHook = childResult?.childOnly === true;
|
|
1897
1974
|
if (childHook) {
|
|
1898
1975
|
// Child activity is observation-only. Never let child PreToolUse
|
|
1899
1976
|
// reach the parent's approval, STOP, state, or APME paths.
|
|
@@ -2044,6 +2121,15 @@ export async function startDaemon(opts) {
|
|
|
2044
2121
|
// (even one persisted before the restart) must not be force-closed.
|
|
2045
2122
|
hookSessionsSeen.add(hookSid);
|
|
2046
2123
|
hookSessionLastSeenAt.set(hookSid, Date.now());
|
|
2124
|
+
// Missed-Stop recovery for observed Claude sessions. Fed the raw
|
|
2125
|
+
// PascalCase event name — the watchdog's vocabulary is Claude's, and
|
|
2126
|
+
// its transcript probe reads Claude's JSONL, so only Claude sessions
|
|
2127
|
+
// are admitted (Codex/OpenCode carry their own turn-end signals).
|
|
2128
|
+
// A synthetic Stop re-enters here as a normal hook and disarms the
|
|
2129
|
+
// watchdog exactly like the real one would.
|
|
2130
|
+
if (hookAgentType === 'claude-code') {
|
|
2131
|
+
observedStopWatchdogs.noteHookEvent(hookSid, eventName, json);
|
|
2132
|
+
}
|
|
2047
2133
|
// Claude hooks carry `cwd` (the worktree dir), not project_name/path —
|
|
2048
2134
|
// capture it so APME runs are attributable to a specific worktree.
|
|
2049
2135
|
const hookCwd = (typeof json.cwd === 'string' ? json.cwd
|
|
@@ -2205,7 +2291,13 @@ export async function startDaemon(opts) {
|
|
|
2205
2291
|
}
|
|
2206
2292
|
const turnOpen = Boolean(lastStart && lastStart.ts > lastCompletionTs);
|
|
2207
2293
|
const tp = typeof json.transcript_path === 'string' ? json.transcript_path : '';
|
|
2208
|
-
const inlineResponse = [
|
|
2294
|
+
const inlineResponse = [
|
|
2295
|
+
json.last_assistant_message,
|
|
2296
|
+
json.assistant_response,
|
|
2297
|
+
json.response,
|
|
2298
|
+
json.output,
|
|
2299
|
+
json.result,
|
|
2300
|
+
]
|
|
2209
2301
|
.find((v) => typeof v === 'string' && v.trim().length > 0) ?? '';
|
|
2210
2302
|
// Codex's stop payload rarely carries the text inline; its rollout
|
|
2211
2303
|
// JSONL (agent_message / task_complete records) is the
|
|
@@ -2242,6 +2334,18 @@ export async function startDaemon(opts) {
|
|
|
2242
2334
|
// response, so a duplicate stop is a no-op instead of a clobber.
|
|
2243
2335
|
apme.collector.setLastClosedTurnResponse(hookSid, responseText);
|
|
2244
2336
|
}
|
|
2337
|
+
// Close the APME turn on the Stop that ended it, tagged with which
|
|
2338
|
+
// Stop that was. Runs AFTER the response capture above (the
|
|
2339
|
+
// trajectory event wants the still-active turn) and is a no-op when
|
|
2340
|
+
// no turn is open, so a duplicate/late stop cannot re-attribute an
|
|
2341
|
+
// already-closed turn. `turnOpen` above is a TIMELINE predicate —
|
|
2342
|
+
// it says nothing about the collector's turn, which is why this is
|
|
2343
|
+
// unconditional rather than nested under it.
|
|
2344
|
+
apme.collector.noteTurnStop(hookSid, {
|
|
2345
|
+
synthetic: json.synthetic_stop === true,
|
|
2346
|
+
interrupted: json.interrupted === true,
|
|
2347
|
+
aborted: json.aborted === true,
|
|
2348
|
+
});
|
|
2245
2349
|
}
|
|
2246
2350
|
if (turnOpen && lastStart) {
|
|
2247
2351
|
const now = Date.now();
|
|
@@ -2442,7 +2546,13 @@ export async function startDaemon(opts) {
|
|
|
2442
2546
|
// by blocking the stop with the directive as the continuation
|
|
2443
2547
|
// reason. Empty queue (the overwhelmingly common case) → empty body,
|
|
2444
2548
|
// Claude ends the turn normally — no stop_hook_active loop possible.
|
|
2445
|
-
|
|
2549
|
+
//
|
|
2550
|
+
// `takeDirectiveForStop` refuses a synthetic Stop (missed-Stop
|
|
2551
|
+
// watchdog): nothing is listening on that response, so draining the
|
|
2552
|
+
// queue there would silently eat the user's queued follow-up.
|
|
2553
|
+
// `clearStop` still runs either way — the transcript proved the turn
|
|
2554
|
+
// ended, so a pending soft-stop is moot however we learned it.
|
|
2555
|
+
const directive = claudeSid ? takeDirectiveForStop(claudeSid, json) : undefined;
|
|
2446
2556
|
if (claudeSid)
|
|
2447
2557
|
clearStop(claudeSid); // turn ended — a pending soft-stop is moot
|
|
2448
2558
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
@@ -2680,8 +2790,9 @@ export async function startDaemon(opts) {
|
|
|
2680
2790
|
// pull-sync e-ink clients live on the LAN — and the security boundary is the
|
|
2681
2791
|
// pairing token (http-auth-gate.ts default-denies unauthenticated LAN
|
|
2682
2792
|
// requests; the WS server has always token-gated non-local peers). Users
|
|
2683
|
-
// with no LAN devices can opt into a loopback-only
|
|
2684
|
-
|
|
2793
|
+
// with no LAN devices can opt into a loopback-only posture (issue #145),
|
|
2794
|
+
// which also silences everything the daemon emits (see network-posture.ts).
|
|
2795
|
+
const bindHost = bindHostFor(posture);
|
|
2685
2796
|
await new Promise((resolve, reject) => {
|
|
2686
2797
|
httpServer.on('error', (err) => {
|
|
2687
2798
|
if (err.code === 'EADDRINUSE') {
|
|
@@ -2716,8 +2827,48 @@ export async function startDaemon(opts) {
|
|
|
2716
2827
|
if (occupant?.mode === 'daemon') {
|
|
2717
2828
|
log(`[agentdeck] Port ${requestedPort} reports a daemon but PID ${occupant.pid} is not a live distinct process; treating as stale and falling back.`);
|
|
2718
2829
|
}
|
|
2830
|
+
// Nobody answered on the preferred port, yet it refuses to bind. That is
|
|
2831
|
+
// not a peer to yield to — it is the kernel still holding a port whose
|
|
2832
|
+
// owner has already gone: macOS keeps a NECP reservation for ~14s after
|
|
2833
|
+
// an `NWListener.cancel()` (measured 2026-08-06: bindable at ~17s, with
|
|
2834
|
+
// `lsof` showing zero sockets throughout), and a half-closed socket in
|
|
2835
|
+
// TIME_WAIT/LAST_ACK looks the same from here.
|
|
2836
|
+
//
|
|
2837
|
+
// Without this wait the daemon conceded instantly and moved to 9121 —
|
|
2838
|
+
// permanently, because nothing ever moved it back. Seconds after that
|
|
2839
|
+
// decision the canonical port was free and owned by nobody. Waiting is
|
|
2840
|
+
// the whole recovery: `waitForPortBindable` polls, so a kernel-held port
|
|
2841
|
+
// costs the ~14s it is actually held, and only a genuinely occupied port
|
|
2842
|
+
// pays the full budget before falling back.
|
|
2843
|
+
if (!occupant) {
|
|
2844
|
+
log(`[agentdeck] Port ${requestedPort} is held but nothing answers /health — usually the kernel still `
|
|
2845
|
+
+ `releasing it after the previous owner exited. Waiting up to ${Math.round(PREFERRED_PORT_RECLAIM_MS / 1000)}s…`);
|
|
2846
|
+
if (await waitForPortBindable(requestedPort, PREFERRED_PORT_RECLAIM_MS)) {
|
|
2847
|
+
const bound = await new Promise((resolve) => {
|
|
2848
|
+
const onError = () => resolve(false);
|
|
2849
|
+
httpServer.once('error', onError);
|
|
2850
|
+
httpServer.listen(requestedPort, bindHost, () => {
|
|
2851
|
+
httpServer.removeListener('error', onError);
|
|
2852
|
+
resolve(true);
|
|
2853
|
+
});
|
|
2854
|
+
});
|
|
2855
|
+
if (bound) {
|
|
2856
|
+
log(`[agentdeck] Port ${requestedPort} came free — bound it instead of falling back.`);
|
|
2857
|
+
return;
|
|
2858
|
+
}
|
|
2859
|
+
// Lost it again between the probe bind and ours. Rare, and the
|
|
2860
|
+
// fallback below is exactly the right answer for it.
|
|
2861
|
+
log(`[agentdeck] Port ${requestedPort} was taken again before this daemon could bind it.`);
|
|
2862
|
+
}
|
|
2863
|
+
else {
|
|
2864
|
+
log(`[agentdeck] Port ${requestedPort} is still held after ${Math.round(PREFERRED_PORT_RECLAIM_MS / 1000)}s — `
|
|
2865
|
+
+ `something outside AgentDeck is listening on it, or a sleeping device is holding a half-closed socket.`);
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2719
2868
|
port = await findAvailablePort();
|
|
2720
2869
|
log(`[agentdeck] Port ${requestedPort} grabbed by ${occupant?.mode ?? 'a non-daemon'}, retrying on ${port}...`);
|
|
2870
|
+
log(`[agentdeck] This daemon prefers port ${requestedPort} (${describeDaemonPortSource(preferred.source)}). `
|
|
2871
|
+
+ `Clients resolve the actual port from daemon.json; 'agentdeck daemon restart' aims at the preferred port again.`);
|
|
2721
2872
|
await new Promise((resolve, reject) => {
|
|
2722
2873
|
httpServer.on('error', (e) => reject(e));
|
|
2723
2874
|
httpServer.listen(port, bindHost, () => resolve());
|
|
@@ -2727,11 +2878,11 @@ export async function startDaemon(opts) {
|
|
|
2727
2878
|
throw err;
|
|
2728
2879
|
}
|
|
2729
2880
|
});
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
log(`[agentdeck]
|
|
2881
|
+
log(describeDaemonPosture(posture, port));
|
|
2882
|
+
if (preferred.source === 'settings' || preferred.source === 'env') {
|
|
2883
|
+
// A persisted or environmental port is invisible in the command line that
|
|
2884
|
+
// started this daemon, so the log is the only place it can be discovered.
|
|
2885
|
+
log(`[agentdeck] Preferred port ${requestedPort} comes from ${describeDaemonPortSource(preferred.source)}.`);
|
|
2735
2886
|
}
|
|
2736
2887
|
// Write daemon.json for client discovery (must be after successful bind).
|
|
2737
2888
|
// Keep the original startedAt stable when the self-heal timer rewrites it.
|
|
@@ -2744,8 +2895,14 @@ export async function startDaemon(opts) {
|
|
|
2744
2895
|
httpServer,
|
|
2745
2896
|
isDaemon: true,
|
|
2746
2897
|
});
|
|
2747
|
-
subagentTimeline = new SubagentTimelineTracker((entry) => {
|
|
2748
|
-
|
|
2898
|
+
subagentTimeline = new SubagentTimelineTracker((entry, upsert) => {
|
|
2899
|
+
// A dispatch row is upserted as its burst grows (one row per fan-out, the
|
|
2900
|
+
// `task_start` folding pattern) — emitting a fresh row per child would
|
|
2901
|
+
// spend a 200-entry buffer shared by every session on one workflow.
|
|
2902
|
+
if (upsert)
|
|
2903
|
+
core.bridgeTimeline.upsertEntry(entry, { bypassSuppression: true });
|
|
2904
|
+
else
|
|
2905
|
+
core.bridgeTimeline.addEntry(entry, { bypassSuppression: true });
|
|
2749
2906
|
});
|
|
2750
2907
|
// Hooks resolve a fallback daemon port through daemon.json. Another process
|
|
2751
2908
|
// can delete that file while this daemon remains healthy, which makes every
|
|
@@ -2828,6 +2985,11 @@ export async function startDaemon(opts) {
|
|
|
2828
2985
|
const TURN_WATCHDOG_SILENCE_MS = 3 * 60_000;
|
|
2829
2986
|
const turnWatchdog = setInterval(() => {
|
|
2830
2987
|
const now = Date.now();
|
|
2988
|
+
// Drop per-session missed-Stop watchdogs whose session has gone cold.
|
|
2989
|
+
// Rides this existing tick rather than owning a timer: an observed
|
|
2990
|
+
// session that dies mid-turn never sends SessionEnd, so its watchdog
|
|
2991
|
+
// would otherwise poll for the life of the daemon.
|
|
2992
|
+
observedStopWatchdogs.sweep();
|
|
2831
2993
|
const active = new Set();
|
|
2832
2994
|
for (const [sid, at] of hookSessionLastSeenAt) {
|
|
2833
2995
|
if (now - at <= TURN_WATCHDOG_SILENCE_MS)
|
|
@@ -3039,7 +3201,12 @@ export async function startDaemon(opts) {
|
|
|
3039
3201
|
// driven exclusively by the Ulanzi Studio plugin (`ulanzi-plugin`) over WebSocket;
|
|
3040
3202
|
// the daemon never opens it over HID. Its health is reported via the Ulanzi client
|
|
3041
3203
|
// count in moduleHealthProvider below.
|
|
3042
|
-
const startedModules = await initModules(deviceModules,
|
|
3204
|
+
const startedModules = await initModules(deviceModules, daemonModuleConfigs(posture, serialMode), {
|
|
3205
|
+
port, authToken: core.authToken, projectName: 'AgentDeck', wsServer: core.wsServer,
|
|
3206
|
+
// AdbModule restricts `adb reverse` to USB transports under loopback —
|
|
3207
|
+
// a TCP/mDNS adb device would carry the tunnel over the LAN.
|
|
3208
|
+
loopbackOnly: posture.loopbackOnly,
|
|
3209
|
+
});
|
|
3043
3210
|
moduleHealthProvider = () => {
|
|
3044
3211
|
const modules = buildNodeModuleHealth(startedModules);
|
|
3045
3212
|
const einkDevices = collectEinkDevices();
|
|
@@ -3293,7 +3460,24 @@ export async function startDaemon(opts) {
|
|
|
3293
3460
|
// The observer scans in the background now (collect() returns the cache
|
|
3294
3461
|
// immediately). When a scan lands fresh observations, push them out via
|
|
3295
3462
|
// the debounced broadcast so clients don't wait for the next 10 s poll.
|
|
3296
|
-
passiveSessionObserver.onRefreshed = () =>
|
|
3463
|
+
passiveSessionObserver.onRefreshed = () => {
|
|
3464
|
+
// Kiro pushes nothing: no `kiro_*` hook has ever reached this daemon, so
|
|
3465
|
+
// without a producer here its session shows in the HUD while the timeline
|
|
3466
|
+
// beside it stays empty. #218 taught the per-session QUERY to read Kiro's
|
|
3467
|
+
// own transcript; the main timeline is a STREAM, and this is what feeds it.
|
|
3468
|
+
try {
|
|
3469
|
+
const kiroIds = passiveSessionObserver.collect([])
|
|
3470
|
+
.filter((s) => typeof s.agentType === 'string' && s.agentType.startsWith('kiro'))
|
|
3471
|
+
.map((s) => s.id);
|
|
3472
|
+
for (const entry of kiroTimelineFeed.pump(kiroIds)) {
|
|
3473
|
+
core.bridgeTimeline.addEntry(entry);
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3476
|
+
catch (err) {
|
|
3477
|
+
debug('daemon', `kiro timeline feed failed: ${String(err)}`);
|
|
3478
|
+
}
|
|
3479
|
+
core.maybeBroadcastSessionsList();
|
|
3480
|
+
};
|
|
3297
3481
|
// OTel turn boundaries arrive in milliseconds where the observer only re-reads
|
|
3298
3482
|
// the rollout tail every scan interval, so a span that changed thread state is
|
|
3299
3483
|
// worth a broadcast of its own.
|
|
@@ -3344,15 +3528,24 @@ export async function startDaemon(opts) {
|
|
|
3344
3528
|
// Derive per-session elapsed seconds from startedAt so NTP-less devices
|
|
3345
3529
|
// (ESP32 IPS10 mosaic) render an elapsed value per cell without a wall clock.
|
|
3346
3530
|
const now = Date.now();
|
|
3531
|
+
// Live child-agent census. Hooks key children by the BARE session uuid
|
|
3532
|
+
// while these rows are `observed:<agent>:<uuid>`, so normalize before the
|
|
3533
|
+
// lookup — the two id forms are the standing trap here.
|
|
3534
|
+
const subagentCensus = subagentTimeline?.summaries() ?? new Map();
|
|
3347
3535
|
const enrichedSessions = [...sessions, ...observed, ...remote].map((s) => {
|
|
3348
3536
|
// On-demand review badge (REVIEW tile verdict / REVIEWING state) —
|
|
3349
3537
|
// applies to every session type, managed included.
|
|
3350
3538
|
const review = reviewSnapshot(s.id);
|
|
3351
3539
|
const withReview = Object.keys(review).length > 0 ? { ...s, ...review } : s;
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3540
|
+
// Emitted whenever this session has EVER had a child, zeros included: a
|
|
3541
|
+
// field that disappears when the last child exits latches "8 running" on
|
|
3542
|
+
// every client that merges retain-on-absent.
|
|
3543
|
+
const census = subagentCensus.get(rawSessionId(withReview.id));
|
|
3544
|
+
const withCensus = census ? { ...withReview, subagents: census } : withReview;
|
|
3545
|
+
if (withCensus.elapsedSec != null || !withCensus.startedAt)
|
|
3546
|
+
return withCensus;
|
|
3547
|
+
const sec = Math.round((now - Date.parse(withCensus.startedAt)) / 1000);
|
|
3548
|
+
return Number.isFinite(sec) && sec >= 0 ? { ...withCensus, elapsedSec: sec } : withCensus;
|
|
3356
3549
|
});
|
|
3357
3550
|
// SSOT: inject iff Gateway is authenticated (gatewayConnected). Reachability
|
|
3358
3551
|
// / adapter-liveness alone must not materialize a session — that kept a
|
|
@@ -3371,6 +3564,11 @@ export async function startDaemon(opts) {
|
|
|
3371
3564
|
projectName: 'OpenClaw',
|
|
3372
3565
|
modelName: gatewayModelName,
|
|
3373
3566
|
controlMode: 'managed',
|
|
3567
|
+
// Read live from the adapter rather than cached alongside
|
|
3568
|
+
// `gatewaySessionState`: the two would drift, and a row that says
|
|
3569
|
+
// "awaiting_permission" while carrying no options is exactly the
|
|
3570
|
+
// dead end this fixes.
|
|
3571
|
+
approval: gatewayAdapter?.getPendingApproval() ?? null,
|
|
3374
3572
|
});
|
|
3375
3573
|
});
|
|
3376
3574
|
// OpenClaw-specific APME session id. Distinct from `core.sessionId`
|
|
@@ -3432,6 +3630,14 @@ export async function startDaemon(opts) {
|
|
|
3432
3630
|
gatewaySessionState = 'idle';
|
|
3433
3631
|
else if (evt.event === 'permission_prompt')
|
|
3434
3632
|
gatewaySessionState = 'awaiting_permission';
|
|
3633
|
+
// The virtual Gateway row carries the approval's question/options, so
|
|
3634
|
+
// a prompt appearing or clearing changes `sessions_list` — not just
|
|
3635
|
+
// the state event. Devices read their option cells from that list; a
|
|
3636
|
+
// state-only broadcast left them showing the previous prompt (or
|
|
3637
|
+
// none) until some unrelated event forced a refresh.
|
|
3638
|
+
if (evt.event === 'spinner_start' || evt.event === 'idle' || evt.event === 'permission_prompt') {
|
|
3639
|
+
core.broadcastSessionsList().catch(() => { });
|
|
3640
|
+
}
|
|
3435
3641
|
// The OpenClaw adapter emits the real model via a model_info parser
|
|
3436
3642
|
// event, but it only ever updated the display StateMachine — the APME
|
|
3437
3643
|
// run was never told, so every openclaw run persisted model_id=NULL.
|
|
@@ -3487,6 +3693,14 @@ export async function startDaemon(opts) {
|
|
|
3487
3693
|
core.bridgeTimeline.addEntry(enriched);
|
|
3488
3694
|
if (enriched.type === 'tool_request')
|
|
3489
3695
|
bridgeLogStream.trackToolRequest(enriched.raw);
|
|
3696
|
+
// Flip the matching `tool_request` row off `pending`. The resolution
|
|
3697
|
+
// has always been emitted as its own row, so the request row kept
|
|
3698
|
+
// `status: 'pending'` forever — an approval answered minutes ago
|
|
3699
|
+
// still read as outstanding in the timeline. `updateEntryStatus`
|
|
3700
|
+
// existed for exactly this and had no production caller.
|
|
3701
|
+
if (enriched.type === 'tool_resolved' && enriched.approvalId) {
|
|
3702
|
+
core.bridgeTimeline.updateEntryStatus(enriched.approvalId, enriched.status === 'approved' ? 'approved' : 'denied');
|
|
3703
|
+
}
|
|
3490
3704
|
}
|
|
3491
3705
|
break;
|
|
3492
3706
|
case 'connection': {
|
|
@@ -3800,6 +4014,17 @@ export async function startDaemon(opts) {
|
|
|
3800
4014
|
}
|
|
3801
4015
|
catch { /* read-only best effort */ }
|
|
3802
4016
|
}
|
|
4017
|
+
// Kiro keeps its transcript in its OWN store, so the Claude reader
|
|
4018
|
+
// above can never find one — and no `kiro_*` hook has ever reached
|
|
4019
|
+
// this daemon (v2 has no global hook surface at all), so the timeline
|
|
4020
|
+
// store is empty for it too. Without this branch an observed Kiro
|
|
4021
|
+
// session's Detail view is empty whatever it does.
|
|
4022
|
+
if (entries.length === 0 && sessionId.startsWith('observed:kiro')) {
|
|
4023
|
+
try {
|
|
4024
|
+
entries = kiroTimelineForSession(sessionId, { since: sinceMs });
|
|
4025
|
+
}
|
|
4026
|
+
catch { /* read-only best effort */ }
|
|
4027
|
+
}
|
|
3803
4028
|
const historyEvent = buildCappedTimelineHistory(entries, undefined, { sessionId });
|
|
3804
4029
|
if (historyEvent) {
|
|
3805
4030
|
try {
|
|
@@ -5309,6 +5534,9 @@ export async function startDaemon(opts) {
|
|
|
5309
5534
|
removeDaemonInfo();
|
|
5310
5535
|
focusRelay.stop();
|
|
5311
5536
|
timelineRelay.stop();
|
|
5537
|
+
// Clears every per-session poll timer. A watchdog that survived teardown
|
|
5538
|
+
// would keep firing self-POSTs at a port this daemon no longer owns.
|
|
5539
|
+
observedStopWatchdogs.stop();
|
|
5312
5540
|
voiceAssistant?.stop();
|
|
5313
5541
|
bridgeLogStream.stop();
|
|
5314
5542
|
// Flush synchronously — the process exits a few lines below and a pending
|