@vibelet/cli 0.1.34 → 0.1.36
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/app.json +5 -0
- package/dist/advertised-hosts.d.ts +34 -0
- package/dist/advertised-hosts.d.ts.map +1 -0
- package/dist/advertised-hosts.js +176 -0
- package/dist/advertised-hosts.js.map +1 -0
- package/dist/advertised-hosts.test.d.ts +2 -0
- package/dist/advertised-hosts.test.d.ts.map +1 -0
- package/dist/advertised-hosts.test.js +96 -0
- package/dist/advertised-hosts.test.js.map +1 -0
- package/dist/audit.d.ts +30 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +73 -0
- package/dist/audit.js.map +1 -0
- package/dist/audit.test.d.ts +2 -0
- package/dist/audit.test.d.ts.map +1 -0
- package/dist/audit.test.js +33 -0
- package/dist/audit.test.js.map +1 -0
- package/dist/auth.d.ts +6 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +27 -0
- package/dist/auth.js.map +1 -0
- package/dist/claude-hooks.d.ts +58 -0
- package/dist/claude-hooks.d.ts.map +1 -0
- package/dist/claude-hooks.js +129 -0
- package/dist/claude-hooks.js.map +1 -0
- package/dist/cli-version.d.ts +3 -0
- package/dist/cli-version.d.ts.map +1 -0
- package/dist/cli-version.js +35 -0
- package/dist/cli-version.js.map +1 -0
- package/dist/cli-version.test.d.ts +2 -0
- package/dist/cli-version.test.d.ts.map +1 -0
- package/dist/cli-version.test.js +38 -0
- package/dist/cli-version.test.js.map +1 -0
- package/dist/config.d.ts +30 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +327 -0
- package/dist/config.js.map +1 -0
- package/dist/config.test.d.ts +2 -0
- package/dist/config.test.d.ts.map +1 -0
- package/dist/config.test.js +184 -0
- package/dist/config.test.js.map +1 -0
- package/dist/dev-auth.test.d.ts +2 -0
- package/dist/dev-auth.test.d.ts.map +1 -0
- package/dist/dev-auth.test.js +154 -0
- package/dist/dev-auth.test.js.map +1 -0
- package/dist/dev-script.test.d.ts +2 -0
- package/dist/dev-script.test.d.ts.map +1 -0
- package/dist/dev-script.test.js +412 -0
- package/dist/dev-script.test.js.map +1 -0
- package/dist/drivers/claude.d.ts +34 -0
- package/dist/drivers/claude.d.ts.map +1 -0
- package/dist/drivers/claude.js +413 -0
- package/dist/drivers/claude.js.map +1 -0
- package/dist/drivers/claude.test.d.ts +2 -0
- package/dist/drivers/claude.test.d.ts.map +1 -0
- package/dist/drivers/claude.test.js +951 -0
- package/dist/drivers/claude.test.js.map +1 -0
- package/dist/drivers/codex.d.ts +38 -0
- package/dist/drivers/codex.d.ts.map +1 -0
- package/dist/drivers/codex.js +771 -0
- package/dist/drivers/codex.js.map +1 -0
- package/dist/drivers/codex.test.d.ts +2 -0
- package/dist/drivers/codex.test.d.ts.map +1 -0
- package/dist/drivers/codex.test.js +939 -0
- package/dist/drivers/codex.test.js.map +1 -0
- package/dist/drivers/types.d.ts +14 -0
- package/dist/drivers/types.d.ts.map +1 -0
- package/dist/drivers/types.js +2 -0
- package/dist/drivers/types.js.map +1 -0
- package/dist/e2e.test.d.ts +2 -0
- package/dist/e2e.test.d.ts.map +1 -0
- package/dist/e2e.test.js +111 -0
- package/dist/e2e.test.js.map +1 -0
- package/dist/identity.d.ts +10 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +66 -0
- package/dist/identity.js.map +1 -0
- package/dist/identity.test.d.ts +2 -0
- package/dist/identity.test.d.ts.map +1 -0
- package/dist/identity.test.js +25 -0
- package/dist/identity.test.js.map +1 -0
- package/dist/index-entry.test.d.ts +2 -0
- package/dist/index-entry.test.d.ts.map +1 -0
- package/dist/index-entry.test.js +272 -0
- package/dist/index-entry.test.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +707 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +31 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +75 -0
- package/dist/logger.js.map +1 -0
- package/dist/metrics.d.ts +52 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +89 -0
- package/dist/metrics.js.map +1 -0
- package/dist/pairing-store.d.ts +29 -0
- package/dist/pairing-store.d.ts.map +1 -0
- package/dist/pairing-store.js +131 -0
- package/dist/pairing-store.js.map +1 -0
- package/dist/pairing-store.test.d.ts +2 -0
- package/dist/pairing-store.test.d.ts.map +1 -0
- package/dist/pairing-store.test.js +47 -0
- package/dist/pairing-store.test.js.map +1 -0
- package/dist/paths.d.ts +16 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +18 -0
- package/dist/paths.js.map +1 -0
- package/dist/perf-compare.d.ts +13 -0
- package/dist/perf-compare.d.ts.map +1 -0
- package/dist/perf-compare.js +125 -0
- package/dist/perf-compare.js.map +1 -0
- package/dist/port-conflict.d.ts +9 -0
- package/dist/port-conflict.d.ts.map +1 -0
- package/dist/port-conflict.js +33 -0
- package/dist/port-conflict.js.map +1 -0
- package/dist/port-conflict.test.d.ts +2 -0
- package/dist/port-conflict.test.d.ts.map +1 -0
- package/dist/port-conflict.test.js +38 -0
- package/dist/port-conflict.test.js.map +1 -0
- package/dist/process-scanner.d.ts +43 -0
- package/dist/process-scanner.d.ts.map +1 -0
- package/dist/process-scanner.js +453 -0
- package/dist/process-scanner.js.map +1 -0
- package/dist/process-scanner.perf.test.d.ts +2 -0
- package/dist/process-scanner.perf.test.d.ts.map +1 -0
- package/dist/process-scanner.perf.test.js +186 -0
- package/dist/process-scanner.perf.test.js.map +1 -0
- package/dist/process-scanner.test.d.ts +2 -0
- package/dist/process-scanner.test.d.ts.map +1 -0
- package/dist/process-scanner.test.js +399 -0
- package/dist/process-scanner.test.js.map +1 -0
- package/dist/push-protocol.d.ts +15 -0
- package/dist/push-protocol.d.ts.map +1 -0
- package/dist/push-protocol.js +23 -0
- package/dist/push-protocol.js.map +1 -0
- package/dist/push-protocol.test.d.ts +2 -0
- package/dist/push-protocol.test.d.ts.map +1 -0
- package/dist/push-protocol.test.js +57 -0
- package/dist/push-protocol.test.js.map +1 -0
- package/dist/push-store.d.ts +22 -0
- package/dist/push-store.d.ts.map +1 -0
- package/dist/push-store.js +103 -0
- package/dist/push-store.js.map +1 -0
- package/dist/push-store.test.d.ts +2 -0
- package/dist/push-store.test.d.ts.map +1 -0
- package/dist/push-store.test.js +79 -0
- package/dist/push-store.test.js.map +1 -0
- package/dist/push.d.ts +65 -0
- package/dist/push.d.ts.map +1 -0
- package/dist/push.js +202 -0
- package/dist/push.js.map +1 -0
- package/dist/push.test.d.ts +2 -0
- package/dist/push.test.d.ts.map +1 -0
- package/dist/push.test.js +199 -0
- package/dist/push.test.js.map +1 -0
- package/dist/safe-stdio.d.ts +3 -0
- package/dist/safe-stdio.d.ts.map +1 -0
- package/dist/safe-stdio.js +46 -0
- package/dist/safe-stdio.js.map +1 -0
- package/dist/scanner.d.ts +30 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +859 -0
- package/dist/scanner.js.map +1 -0
- package/dist/scanner.perf.test.d.ts +2 -0
- package/dist/scanner.perf.test.d.ts.map +1 -0
- package/dist/scanner.perf.test.js +320 -0
- package/dist/scanner.perf.test.js.map +1 -0
- package/dist/scanner.test.d.ts +2 -0
- package/dist/scanner.test.d.ts.map +1 -0
- package/dist/scanner.test.js +948 -0
- package/dist/scanner.test.js.map +1 -0
- package/dist/session-inventory.d.ts +63 -0
- package/dist/session-inventory.d.ts.map +1 -0
- package/dist/session-inventory.js +525 -0
- package/dist/session-inventory.js.map +1 -0
- package/dist/session-inventory.perf.test.d.ts +2 -0
- package/dist/session-inventory.perf.test.d.ts.map +1 -0
- package/dist/session-inventory.perf.test.js +220 -0
- package/dist/session-inventory.perf.test.js.map +1 -0
- package/dist/session-inventory.test.d.ts +2 -0
- package/dist/session-inventory.test.d.ts.map +1 -0
- package/dist/session-inventory.test.js +712 -0
- package/dist/session-inventory.test.js.map +1 -0
- package/dist/session-manager.d.ts +75 -0
- package/dist/session-manager.d.ts.map +1 -0
- package/dist/session-manager.js +1515 -0
- package/dist/session-manager.js.map +1 -0
- package/dist/session-manager.test.d.ts +2 -0
- package/dist/session-manager.test.d.ts.map +1 -0
- package/dist/session-manager.test.js +2861 -0
- package/dist/session-manager.test.js.map +1 -0
- package/dist/session-store.d.ts +42 -0
- package/dist/session-store.d.ts.map +1 -0
- package/dist/session-store.js +163 -0
- package/dist/session-store.js.map +1 -0
- package/dist/session-store.test.d.ts +2 -0
- package/dist/session-store.test.d.ts.map +1 -0
- package/dist/session-store.test.js +236 -0
- package/dist/session-store.test.js.map +1 -0
- package/dist/session-title.d.ts +6 -0
- package/dist/session-title.d.ts.map +1 -0
- package/dist/session-title.js +105 -0
- package/dist/session-title.js.map +1 -0
- package/dist/session-title.perf.test.d.ts +2 -0
- package/dist/session-title.perf.test.d.ts.map +1 -0
- package/dist/session-title.perf.test.js +99 -0
- package/dist/session-title.perf.test.js.map +1 -0
- package/dist/session-title.test.d.ts +2 -0
- package/dist/session-title.test.d.ts.map +1 -0
- package/dist/session-title.test.js +199 -0
- package/dist/session-title.test.js.map +1 -0
- package/dist/shutdown-endpoint.test.d.ts +2 -0
- package/dist/shutdown-endpoint.test.d.ts.map +1 -0
- package/dist/shutdown-endpoint.test.js +93 -0
- package/dist/shutdown-endpoint.test.js.map +1 -0
- package/dist/storage-housekeeping.d.ts +28 -0
- package/dist/storage-housekeeping.d.ts.map +1 -0
- package/dist/storage-housekeeping.js +76 -0
- package/dist/storage-housekeeping.js.map +1 -0
- package/dist/storage-housekeeping.test.d.ts +2 -0
- package/dist/storage-housekeeping.test.d.ts.map +1 -0
- package/dist/storage-housekeeping.test.js +65 -0
- package/dist/storage-housekeeping.test.js.map +1 -0
- package/dist/test-daemon-harness.d.ts +31 -0
- package/dist/test-daemon-harness.d.ts.map +1 -0
- package/dist/test-daemon-harness.js +337 -0
- package/dist/test-daemon-harness.js.map +1 -0
- package/dist/token-auth.test.d.ts +2 -0
- package/dist/token-auth.test.d.ts.map +1 -0
- package/dist/token-auth.test.js +52 -0
- package/dist/token-auth.test.js.map +1 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +40 -0
- package/dist/utils.js.map +1 -0
- package/dist/utils.test.d.ts +2 -0
- package/dist/utils.test.d.ts.map +1 -0
- package/dist/utils.test.js +54 -0
- package/dist/utils.test.js.map +1 -0
- package/dist/ws-data.d.ts +4 -0
- package/dist/ws-data.d.ts.map +1 -0
- package/dist/ws-data.js +20 -0
- package/dist/ws-data.js.map +1 -0
- package/dist/ws-data.test.d.ts +2 -0
- package/dist/ws-data.test.d.ts.map +1 -0
- package/dist/ws-data.test.js +17 -0
- package/dist/ws-data.test.js.map +1 -0
- package/package.json +24 -27
- package/perf-reporter.mjs +138 -0
- package/scripts/build-release.mjs +41 -0
- package/scripts/dev.mjs +537 -0
- package/src/advertised-hosts.test.ts +125 -0
- package/src/advertised-hosts.ts +225 -0
- package/src/audit.test.ts +38 -0
- package/src/audit.ts +117 -0
- package/src/auth.ts +31 -0
- package/src/claude-hooks.ts +195 -0
- package/src/cli-version.test.ts +36 -0
- package/src/cli-version.ts +46 -0
- package/src/config.test.ts +254 -0
- package/src/config.ts +324 -0
- package/src/dev-auth.test.ts +183 -0
- package/src/dev-script.test.ts +511 -0
- package/src/drivers/claude.test.ts +1186 -0
- package/src/drivers/claude.ts +443 -0
- package/src/drivers/codex.test.ts +1096 -0
- package/src/drivers/codex.ts +879 -0
- package/src/drivers/types.ts +15 -0
- package/src/e2e.test.ts +139 -0
- package/src/identity.test.ts +26 -0
- package/src/identity.ts +82 -0
- package/src/index-entry.test.ts +336 -0
- package/src/index.ts +781 -0
- package/src/logger.ts +112 -0
- package/src/metrics.ts +117 -0
- package/src/pairing-store.test.ts +53 -0
- package/src/pairing-store.ts +154 -0
- package/src/paths.ts +19 -0
- package/src/perf-compare.ts +164 -0
- package/src/port-conflict.test.ts +45 -0
- package/src/port-conflict.ts +44 -0
- package/src/process-scanner.perf.test.ts +222 -0
- package/src/process-scanner.test.ts +575 -0
- package/src/process-scanner.ts +514 -0
- package/src/push-protocol.test.ts +74 -0
- package/src/push-protocol.ts +36 -0
- package/src/push-store.test.ts +89 -0
- package/src/push-store.ts +126 -0
- package/src/push.test.ts +234 -0
- package/src/push.ts +318 -0
- package/src/safe-stdio.ts +51 -0
- package/src/scanner.perf.test.ts +359 -0
- package/src/scanner.test.ts +1045 -0
- package/src/scanner.ts +924 -0
- package/src/session-inventory.perf.test.ts +250 -0
- package/src/session-inventory.test.ts +1002 -0
- package/src/session-inventory.ts +721 -0
- package/src/session-manager.test.ts +3430 -0
- package/src/session-manager.ts +1775 -0
- package/src/session-store.test.ts +276 -0
- package/src/session-store.ts +202 -0
- package/src/session-title.perf.test.ts +118 -0
- package/src/session-title.test.ts +286 -0
- package/src/session-title.ts +108 -0
- package/src/shutdown-endpoint.test.ts +95 -0
- package/src/storage-housekeeping.test.ts +78 -0
- package/src/storage-housekeeping.ts +111 -0
- package/src/test-daemon-harness.ts +410 -0
- package/src/token-auth.test.ts +67 -0
- package/src/utils.test.ts +65 -0
- package/src/utils.ts +47 -0
- package/src/ws-data.test.ts +20 -0
- package/src/ws-data.ts +26 -0
- package/tsconfig.json +12 -0
- package/README.md +0 -80
- package/bin/cloudflared-quick-tunnel.mjs +0 -11
- package/bin/cloudflared-resolver.mjs +0 -68
- package/bin/vibelet-runtime-policy.mjs +0 -36
- package/bin/vibelet.cjs +0 -12
- package/bin/vibelet.mjs +0 -1019
- package/dist/index.cjs +0 -123
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentType,
|
|
3
|
+
SessionConfidence,
|
|
4
|
+
SessionInfo,
|
|
5
|
+
SessionListCursor,
|
|
6
|
+
SessionListPage,
|
|
7
|
+
SessionResumeMode,
|
|
8
|
+
SessionRuntimeInfo,
|
|
9
|
+
SessionRuntimeState,
|
|
10
|
+
} from '@vibelet/shared';
|
|
11
|
+
import { scanRunningSessions, type RunningSessionCandidate } from './process-scanner.js';
|
|
12
|
+
import { listSessions as listScannedSessions, searchSessionContent } from './scanner.js';
|
|
13
|
+
import { isFallbackSessionTitle } from './session-title.js';
|
|
14
|
+
import type { SessionRecord } from './session-store.js';
|
|
15
|
+
import { logger as rootLogger } from './logger.js';
|
|
16
|
+
import { metrics } from './metrics.js';
|
|
17
|
+
|
|
18
|
+
const log = rootLogger.child({ module: 'inventory' });
|
|
19
|
+
const DEFAULT_INVENTORY_SOURCE_TIMEOUT_MS = 6_000;
|
|
20
|
+
const EXTERNAL_INVENTORY_BACKFILL_TTL_MS = 10_000;
|
|
21
|
+
|
|
22
|
+
export type { SessionRecord } from './session-store.js';
|
|
23
|
+
|
|
24
|
+
export interface ActiveSessionSnapshot {
|
|
25
|
+
sessionId: string;
|
|
26
|
+
agent: AgentType;
|
|
27
|
+
cwd: string;
|
|
28
|
+
approvalMode?: import('@vibelet/shared').ApprovalMode;
|
|
29
|
+
title: string;
|
|
30
|
+
createdAt: string;
|
|
31
|
+
lastActivityAt: string;
|
|
32
|
+
needsAttention?: boolean;
|
|
33
|
+
isResponding?: boolean;
|
|
34
|
+
managed?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface InventorySources {
|
|
38
|
+
activeSessions: ActiveSessionSnapshot[];
|
|
39
|
+
sessionRecords: SessionRecord[];
|
|
40
|
+
scannedSessions: SessionInfo[];
|
|
41
|
+
runningSessions: RunningSessionCandidate[];
|
|
42
|
+
scannedAt: string;
|
|
43
|
+
deletedSessionIds?: ReadonlySet<string>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ListSessionPageOptions {
|
|
47
|
+
agent?: AgentType;
|
|
48
|
+
cwd?: string;
|
|
49
|
+
search?: string;
|
|
50
|
+
limit: number;
|
|
51
|
+
cursor?: SessionListCursor;
|
|
52
|
+
activeSessions: ActiveSessionSnapshot[];
|
|
53
|
+
sessionRecords: SessionRecord[];
|
|
54
|
+
deletedSessionIds?: ReadonlySet<string>;
|
|
55
|
+
/** @internal override inventory loaders in tests */
|
|
56
|
+
loaders?: Partial<InventoryLoaders>;
|
|
57
|
+
/** @internal per-source timeout for slow inventory probes */
|
|
58
|
+
sourceTimeoutMs?: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface InventoryLoaders {
|
|
62
|
+
listScannedSessions: (agent?: AgentType, cwd?: string, partial?: SessionInfo[]) => Promise<SessionInfo[]>;
|
|
63
|
+
scanRunningSessions: typeof scanRunningSessions;
|
|
64
|
+
searchSessionContent: typeof searchSessionContent;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const defaultInventoryLoaders: InventoryLoaders = {
|
|
68
|
+
listScannedSessions,
|
|
69
|
+
scanRunningSessions,
|
|
70
|
+
searchSessionContent,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
interface ExternalInventorySnapshot {
|
|
74
|
+
scannedSessions: SessionInfo[];
|
|
75
|
+
runningSessions: RunningSessionCandidate[];
|
|
76
|
+
scannedAt: string;
|
|
77
|
+
refreshedAt: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface ExternalInventoryHealthSnapshot {
|
|
81
|
+
cachedSessions: number;
|
|
82
|
+
runningSessions: number;
|
|
83
|
+
cacheAgeMs?: number;
|
|
84
|
+
backfillInFlight: boolean;
|
|
85
|
+
lastBackfillDurationMs?: number;
|
|
86
|
+
lastBackfillCompletedAt?: string;
|
|
87
|
+
lastBackfillAppliedAt?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
type ExternalInventoryBackfillListener = () => void;
|
|
91
|
+
|
|
92
|
+
let externalInventorySnapshot: ExternalInventorySnapshot = createEmptyExternalInventorySnapshot();
|
|
93
|
+
let externalInventoryRefreshInflight: Promise<void> | null = null;
|
|
94
|
+
let externalInventoryGeneration = 0;
|
|
95
|
+
let externalInventoryLastBackfillCompletedAt = 0;
|
|
96
|
+
let externalInventoryLastBackfillDurationMs = 0;
|
|
97
|
+
let externalInventoryLastBackfillAppliedAt = 0;
|
|
98
|
+
const externalInventoryBackfillListeners = new Set<ExternalInventoryBackfillListener>();
|
|
99
|
+
|
|
100
|
+
function createEmptyExternalInventorySnapshot(): ExternalInventorySnapshot {
|
|
101
|
+
return {
|
|
102
|
+
scannedSessions: [],
|
|
103
|
+
runningSessions: [],
|
|
104
|
+
scannedAt: '',
|
|
105
|
+
refreshedAt: 0,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function serializeSessionInfo(session: SessionInfo): string {
|
|
110
|
+
return JSON.stringify({
|
|
111
|
+
sessionId: session.sessionId,
|
|
112
|
+
agent: session.agent,
|
|
113
|
+
cwd: session.cwd,
|
|
114
|
+
title: session.title,
|
|
115
|
+
createdAt: session.createdAt,
|
|
116
|
+
lastActivityAt: session.lastActivityAt,
|
|
117
|
+
sources: [...session.sources].sort(),
|
|
118
|
+
runtime: {
|
|
119
|
+
state: session.runtime.state,
|
|
120
|
+
confidence: session.runtime.confidence,
|
|
121
|
+
resumeMode: session.runtime.resumeMode,
|
|
122
|
+
pid: session.runtime.pid,
|
|
123
|
+
command: session.runtime.command,
|
|
124
|
+
isResponding: session.runtime.isResponding,
|
|
125
|
+
needsAttention: session.runtime.needsAttention,
|
|
126
|
+
},
|
|
127
|
+
managed: session.managed,
|
|
128
|
+
approvalMode: session.approvalMode,
|
|
129
|
+
observedApprovalMode: session.observedApprovalMode,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function serializeRunningSession(candidate: RunningSessionCandidate): string {
|
|
134
|
+
return JSON.stringify({
|
|
135
|
+
sessionId: candidate.sessionId,
|
|
136
|
+
agent: candidate.agent,
|
|
137
|
+
cwd: candidate.cwd,
|
|
138
|
+
title: candidate.title,
|
|
139
|
+
pid: candidate.pid,
|
|
140
|
+
command: candidate.command,
|
|
141
|
+
confidence: candidate.confidence,
|
|
142
|
+
isResponding: candidate.isResponding,
|
|
143
|
+
approvalMode: candidate.approvalMode,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function externalInventorySignature(snapshot: ExternalInventorySnapshot): string {
|
|
148
|
+
return JSON.stringify({
|
|
149
|
+
scannedSessions: snapshot.scannedSessions.map(serializeSessionInfo).sort(),
|
|
150
|
+
runningSessions: snapshot.runningSessions.map(serializeRunningSession).sort(),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function notifyExternalInventoryBackfillListeners(): void {
|
|
155
|
+
for (const listener of externalInventoryBackfillListeners) {
|
|
156
|
+
try {
|
|
157
|
+
listener();
|
|
158
|
+
} catch (error) {
|
|
159
|
+
log.warn({ error: String(error) }, 'session inventory backfill listener failed');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function updateExternalInventoryGauges(): void {
|
|
165
|
+
metrics.gauge('session.inventory.external_cached_sessions', externalInventorySnapshot.scannedSessions.length);
|
|
166
|
+
metrics.gauge('session.inventory.external_running_sessions', externalInventorySnapshot.runningSessions.length);
|
|
167
|
+
metrics.gauge('session.inventory.backfill_inflight', externalInventoryRefreshInflight ? 1 : 0);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function onExternalInventoryBackfill(listener: ExternalInventoryBackfillListener): () => void {
|
|
171
|
+
externalInventoryBackfillListeners.add(listener);
|
|
172
|
+
return () => {
|
|
173
|
+
externalInventoryBackfillListeners.delete(listener);
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function getExternalInventoryHealth(): ExternalInventoryHealthSnapshot {
|
|
178
|
+
return {
|
|
179
|
+
cachedSessions: externalInventorySnapshot.scannedSessions.length,
|
|
180
|
+
runningSessions: externalInventorySnapshot.runningSessions.length,
|
|
181
|
+
cacheAgeMs: externalInventorySnapshot.refreshedAt
|
|
182
|
+
? Math.max(0, Date.now() - externalInventorySnapshot.refreshedAt)
|
|
183
|
+
: undefined,
|
|
184
|
+
backfillInFlight: Boolean(externalInventoryRefreshInflight),
|
|
185
|
+
lastBackfillDurationMs: externalInventoryLastBackfillDurationMs || undefined,
|
|
186
|
+
lastBackfillCompletedAt: externalInventoryLastBackfillCompletedAt
|
|
187
|
+
? new Date(externalInventoryLastBackfillCompletedAt).toISOString()
|
|
188
|
+
: undefined,
|
|
189
|
+
lastBackfillAppliedAt: externalInventoryLastBackfillAppliedAt
|
|
190
|
+
? new Date(externalInventoryLastBackfillAppliedAt).toISOString()
|
|
191
|
+
: undefined,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
updateExternalInventoryGauges();
|
|
196
|
+
|
|
197
|
+
const runtimeStateRank: Record<SessionRuntimeState, number> = {
|
|
198
|
+
daemonActive: 3,
|
|
199
|
+
externalRunning: 2,
|
|
200
|
+
idle: 1,
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const confidenceRank: Record<SessionConfidence, number> = {
|
|
204
|
+
high: 3,
|
|
205
|
+
medium: 2,
|
|
206
|
+
low: 1,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
function compareSessions(a: SessionInfo, b: SessionInfo): number {
|
|
210
|
+
return (
|
|
211
|
+
b.lastActivityAt.localeCompare(a.lastActivityAt) ||
|
|
212
|
+
a.sessionId.localeCompare(b.sessionId)
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function compareActiveSessions(a: SessionInfo, b: SessionInfo): number {
|
|
217
|
+
// Sessions needing attention always come first
|
|
218
|
+
const aAttention = a.runtime.needsAttention ? 1 : 0;
|
|
219
|
+
const bAttention = b.runtime.needsAttention ? 1 : 0;
|
|
220
|
+
// Responding sessions stay pinned near top so the list doesn't shift mid-reply
|
|
221
|
+
const aResponding = a.runtime.isResponding ? 1 : 0;
|
|
222
|
+
const bResponding = b.runtime.isResponding ? 1 : 0;
|
|
223
|
+
return (
|
|
224
|
+
bAttention - aAttention ||
|
|
225
|
+
bResponding - aResponding ||
|
|
226
|
+
runtimeStateRank[b.runtime.state] - runtimeStateRank[a.runtime.state] ||
|
|
227
|
+
compareSessions(a, b)
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function pickRuntime(current: SessionRuntimeInfo, next: SessionRuntimeInfo): SessionRuntimeInfo {
|
|
232
|
+
if (runtimeStateRank[next.state] > runtimeStateRank[current.state]) return next;
|
|
233
|
+
if (runtimeStateRank[next.state] < runtimeStateRank[current.state]) return current;
|
|
234
|
+
if (confidenceRank[next.confidence] > confidenceRank[current.confidence]) return next;
|
|
235
|
+
return current;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function minIso(a: string, b: string): string {
|
|
239
|
+
if (!a) return b;
|
|
240
|
+
if (!b) return a;
|
|
241
|
+
return a.localeCompare(b) <= 0 ? a : b;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function maxIso(a: string, b: string): string {
|
|
245
|
+
if (!a) return b;
|
|
246
|
+
if (!b) return a;
|
|
247
|
+
return a.localeCompare(b) >= 0 ? a : b;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function shouldOverwriteTitle(current: string | undefined, next: string | undefined): boolean {
|
|
251
|
+
if (!next) return false;
|
|
252
|
+
if (!current) return true;
|
|
253
|
+
if (isFallbackSessionTitle(next) && !isFallbackSessionTitle(current)) return false;
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function createIdleRuntime(confidence: SessionConfidence = 'high', resumeMode: SessionResumeMode = 'resumeSession'): SessionRuntimeInfo {
|
|
258
|
+
return {
|
|
259
|
+
state: 'idle',
|
|
260
|
+
confidence,
|
|
261
|
+
resumeMode,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function toRecordSession(record: SessionRecord): SessionInfo {
|
|
266
|
+
return {
|
|
267
|
+
sessionId: record.sessionId,
|
|
268
|
+
agent: record.agent,
|
|
269
|
+
cwd: record.cwd,
|
|
270
|
+
title: record.title,
|
|
271
|
+
createdAt: record.createdAt,
|
|
272
|
+
lastActivityAt: record.lastActivityAt,
|
|
273
|
+
sources: ['record'],
|
|
274
|
+
runtime: {
|
|
275
|
+
...createIdleRuntime(),
|
|
276
|
+
// Note: record.isResponding is intentionally NOT restored here.
|
|
277
|
+
// If a session only exists in the store (no active driver), it
|
|
278
|
+
// cannot still be responding — the daemon was restarted mid-reply.
|
|
279
|
+
},
|
|
280
|
+
managed: record.managed,
|
|
281
|
+
approvalMode: record.approvalMode,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function toRunningSession(candidate: RunningSessionCandidate, scannedAt: string): SessionInfo {
|
|
286
|
+
return {
|
|
287
|
+
sessionId: candidate.sessionId,
|
|
288
|
+
agent: candidate.agent,
|
|
289
|
+
cwd: candidate.cwd,
|
|
290
|
+
title: candidate.title,
|
|
291
|
+
createdAt: scannedAt,
|
|
292
|
+
lastActivityAt: scannedAt,
|
|
293
|
+
sources: ['process'],
|
|
294
|
+
runtime: {
|
|
295
|
+
state: 'externalRunning',
|
|
296
|
+
confidence: candidate.confidence,
|
|
297
|
+
resumeMode: 'resumeSession',
|
|
298
|
+
pid: candidate.pid,
|
|
299
|
+
command: candidate.command,
|
|
300
|
+
isResponding: candidate.isResponding,
|
|
301
|
+
},
|
|
302
|
+
observedApprovalMode: candidate.approvalMode,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function toActiveSession(session: ActiveSessionSnapshot): SessionInfo {
|
|
307
|
+
return {
|
|
308
|
+
sessionId: session.sessionId,
|
|
309
|
+
agent: session.agent,
|
|
310
|
+
cwd: session.cwd,
|
|
311
|
+
title: session.title,
|
|
312
|
+
createdAt: session.createdAt,
|
|
313
|
+
lastActivityAt: session.lastActivityAt,
|
|
314
|
+
sources: ['daemon'],
|
|
315
|
+
runtime: {
|
|
316
|
+
state: 'daemonActive',
|
|
317
|
+
confidence: 'high',
|
|
318
|
+
resumeMode: 'reuseDriver',
|
|
319
|
+
needsAttention: session.needsAttention || undefined,
|
|
320
|
+
isResponding: session.isResponding || undefined,
|
|
321
|
+
},
|
|
322
|
+
managed: session.managed,
|
|
323
|
+
approvalMode: session.approvalMode,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function mergeInto(merged: Map<string, SessionInfo>, sessions: SessionInfo[]): void {
|
|
328
|
+
for (const session of sessions) {
|
|
329
|
+
const key = `${session.agent}:${session.sessionId}`;
|
|
330
|
+
const current = merged.get(key);
|
|
331
|
+
if (!current) {
|
|
332
|
+
merged.set(key, {
|
|
333
|
+
...session,
|
|
334
|
+
sources: [...session.sources],
|
|
335
|
+
runtime: { ...session.runtime },
|
|
336
|
+
});
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const nextTitle = shouldOverwriteTitle(current.title, session.title) ? session.title : current.title;
|
|
341
|
+
const nextCwd = session.cwd || current.cwd;
|
|
342
|
+
const nextRuntime = pickRuntime(current.runtime, session.runtime);
|
|
343
|
+
|
|
344
|
+
// Deduplicate sources without Set (at most 4 elements)
|
|
345
|
+
const nextSources = current.sources.slice();
|
|
346
|
+
for (const s of session.sources) {
|
|
347
|
+
if (!nextSources.includes(s)) nextSources.push(s);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Process-scanner uses scannedAt (current time) as lastActivityAt which is inaccurate.
|
|
351
|
+
// Only use it if we have no better timestamp.
|
|
352
|
+
const incomingIsProcessScan = session.sources.includes('process');
|
|
353
|
+
const currentIsProcessScan = current.sources.includes('process') && !current.sources.includes('daemon');
|
|
354
|
+
const nextLastActivityAt =
|
|
355
|
+
incomingIsProcessScan ? current.lastActivityAt
|
|
356
|
+
: currentIsProcessScan ? session.lastActivityAt
|
|
357
|
+
: maxIso(current.lastActivityAt, session.lastActivityAt);
|
|
358
|
+
|
|
359
|
+
merged.set(key, {
|
|
360
|
+
...current,
|
|
361
|
+
cwd: nextCwd,
|
|
362
|
+
title: nextTitle,
|
|
363
|
+
createdAt: minIso(current.createdAt, session.createdAt),
|
|
364
|
+
lastActivityAt: nextLastActivityAt,
|
|
365
|
+
sources: nextSources,
|
|
366
|
+
runtime: nextRuntime,
|
|
367
|
+
managed: current.managed || session.managed,
|
|
368
|
+
approvalMode: session.approvalMode ?? current.approvalMode,
|
|
369
|
+
observedApprovalMode: session.observedApprovalMode ?? current.observedApprovalMode,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function mergeAllSessions(...lists: SessionInfo[][]): SessionInfo[] {
|
|
375
|
+
const merged = new Map<string, SessionInfo>();
|
|
376
|
+
for (const list of lists) mergeInto(merged, list);
|
|
377
|
+
return [...merged.values()];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function applyFilters(
|
|
381
|
+
sessions: SessionInfo[],
|
|
382
|
+
agent?: AgentType,
|
|
383
|
+
cwd?: string,
|
|
384
|
+
search?: string,
|
|
385
|
+
contentMatchIds?: Set<string>,
|
|
386
|
+
): SessionInfo[] {
|
|
387
|
+
const q = search?.toLowerCase();
|
|
388
|
+
const result = sessions.filter((session) => {
|
|
389
|
+
if (agent && session.agent !== agent) return false;
|
|
390
|
+
if (cwd && session.cwd !== cwd) return false;
|
|
391
|
+
if (q) {
|
|
392
|
+
const title = (session.title ?? '').toLowerCase();
|
|
393
|
+
const sessionCwd = (session.cwd ?? '').toLowerCase();
|
|
394
|
+
const matchesMeta = title.includes(q) || sessionCwd.includes(q);
|
|
395
|
+
const matchesContent = contentMatchIds?.has(session.sessionId) ?? false;
|
|
396
|
+
if (!matchesMeta && !matchesContent) return false;
|
|
397
|
+
}
|
|
398
|
+
return true;
|
|
399
|
+
});
|
|
400
|
+
if (search) {
|
|
401
|
+
log.info({ total: sessions.length, filtered: result.length, search, contentMatchIds: contentMatchIds?.size ?? 0 }, 'filter applied');
|
|
402
|
+
}
|
|
403
|
+
return result;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function cursorIndex(sessions: SessionInfo[], cursor?: SessionListCursor): number {
|
|
407
|
+
if (!cursor) return 0;
|
|
408
|
+
|
|
409
|
+
// Binary search on the sorted (desc lastActivityAt, asc sessionId) array
|
|
410
|
+
let lo = 0;
|
|
411
|
+
let hi = sessions.length;
|
|
412
|
+
while (lo < hi) {
|
|
413
|
+
const mid = (lo + hi) >> 1;
|
|
414
|
+
const s = sessions[mid];
|
|
415
|
+
const cmp = cursor.lastActivityAt.localeCompare(s.lastActivityAt)
|
|
416
|
+
|| s.sessionId.localeCompare(cursor.sessionId);
|
|
417
|
+
if (cmp > 0) hi = mid;
|
|
418
|
+
else lo = mid + 1;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Verify exact match at lo-1
|
|
422
|
+
if (lo > 0) {
|
|
423
|
+
const prev = sessions[lo - 1];
|
|
424
|
+
if (prev.lastActivityAt === cursor.lastActivityAt && prev.sessionId === cursor.sessionId) {
|
|
425
|
+
return lo;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return 0;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export function buildSessionInventoryPage(
|
|
432
|
+
sources: InventorySources,
|
|
433
|
+
limit: number,
|
|
434
|
+
cursor?: SessionListCursor,
|
|
435
|
+
agent?: AgentType,
|
|
436
|
+
cwd?: string,
|
|
437
|
+
search?: string,
|
|
438
|
+
contentMatchIds?: Set<string>,
|
|
439
|
+
): SessionListPage {
|
|
440
|
+
const pageSize = search ? Infinity : Math.max(1, limit || 50);
|
|
441
|
+
const merged = mergeAllSessions(
|
|
442
|
+
sources.sessionRecords.map(toRecordSession),
|
|
443
|
+
sources.scannedSessions,
|
|
444
|
+
sources.runningSessions.map((session) => toRunningSession(session, sources.scannedAt)),
|
|
445
|
+
sources.activeSessions.map(toActiveSession),
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
const visible = sources.deletedSessionIds?.size
|
|
449
|
+
? merged.filter((session) => !sources.deletedSessionIds?.has(session.sessionId))
|
|
450
|
+
: merged;
|
|
451
|
+
const filtered = applyFilters(visible, agent, cwd, search, contentMatchIds);
|
|
452
|
+
const activeSessions = filtered
|
|
453
|
+
.filter((session) => session.runtime.state !== 'idle' || session.managed)
|
|
454
|
+
.sort(compareActiveSessions);
|
|
455
|
+
const idleSessions = filtered
|
|
456
|
+
.filter((session) => session.runtime.state === 'idle' && !session.managed)
|
|
457
|
+
.sort(compareSessions);
|
|
458
|
+
|
|
459
|
+
const idleStart = cursorIndex(idleSessions, cursor);
|
|
460
|
+
const idleSlice = idleSessions.slice(idleStart, idleStart + pageSize);
|
|
461
|
+
const hasMoreIdle = idleStart + idleSlice.length < idleSessions.length;
|
|
462
|
+
const nextCursor = hasMoreIdle && idleSlice.length > 0
|
|
463
|
+
? {
|
|
464
|
+
lastActivityAt: idleSlice[idleSlice.length - 1].lastActivityAt,
|
|
465
|
+
sessionId: idleSlice[idleSlice.length - 1].sessionId,
|
|
466
|
+
}
|
|
467
|
+
: undefined;
|
|
468
|
+
|
|
469
|
+
return {
|
|
470
|
+
sessions: cursor ? idleSlice : [...activeSessions, ...idleSlice],
|
|
471
|
+
nextCursor,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function loadInventorySource<T>(
|
|
476
|
+
source: string,
|
|
477
|
+
load: () => Promise<T>,
|
|
478
|
+
fallback: T,
|
|
479
|
+
timeoutMs: number,
|
|
480
|
+
/** Shared array populated incrementally by the loader — used as partial result on timeout */
|
|
481
|
+
partial?: T,
|
|
482
|
+
): Promise<T> {
|
|
483
|
+
return new Promise((resolve) => {
|
|
484
|
+
const startedAt = Date.now();
|
|
485
|
+
let settled = false;
|
|
486
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
487
|
+
|
|
488
|
+
const finish = (value: T) => {
|
|
489
|
+
if (settled) return;
|
|
490
|
+
settled = true;
|
|
491
|
+
if (timer) {
|
|
492
|
+
clearTimeout(timer);
|
|
493
|
+
timer = null;
|
|
494
|
+
}
|
|
495
|
+
resolve(value);
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
timer = setTimeout(() => {
|
|
499
|
+
if (settled) return;
|
|
500
|
+
// Use partial results if available, otherwise fall back to empty
|
|
501
|
+
const hasPartial = Array.isArray(partial) && partial.length > 0;
|
|
502
|
+
log.warn(
|
|
503
|
+
{ source, timeoutMs, durationMs: Date.now() - startedAt, partial: hasPartial ? (partial as unknown[]).length : 0 },
|
|
504
|
+
hasPartial
|
|
505
|
+
? 'session inventory source timed out, using partial results'
|
|
506
|
+
: 'session inventory source timed out, using fallback',
|
|
507
|
+
);
|
|
508
|
+
finish(hasPartial ? partial : fallback);
|
|
509
|
+
}, timeoutMs);
|
|
510
|
+
if (typeof timer === 'object' && 'unref' in timer) {
|
|
511
|
+
timer.unref();
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
load()
|
|
515
|
+
.then((value) => {
|
|
516
|
+
finish(value);
|
|
517
|
+
})
|
|
518
|
+
.catch((error) => {
|
|
519
|
+
if (!settled) {
|
|
520
|
+
log.warn({ source, error: String(error) }, 'session inventory source failed, using fallback');
|
|
521
|
+
}
|
|
522
|
+
finish(fallback);
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function shouldRefreshExternalInventory(now = Date.now()): boolean {
|
|
528
|
+
return (
|
|
529
|
+
externalInventorySnapshot.refreshedAt === 0
|
|
530
|
+
|| now - externalInventorySnapshot.refreshedAt >= EXTERNAL_INVENTORY_BACKFILL_TTL_MS
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
async function loadExternalInventorySnapshot(
|
|
535
|
+
loaders: InventoryLoaders,
|
|
536
|
+
timeoutMs: number,
|
|
537
|
+
): Promise<ExternalInventorySnapshot> {
|
|
538
|
+
const scannedAt = new Date().toISOString();
|
|
539
|
+
const partialScanned: SessionInfo[] = [];
|
|
540
|
+
const [scannedSessions, runningSessions] = await Promise.all([
|
|
541
|
+
loadInventorySource(
|
|
542
|
+
'scanner.sessions',
|
|
543
|
+
() => loaders.listScannedSessions(undefined, undefined, partialScanned),
|
|
544
|
+
[] as SessionInfo[],
|
|
545
|
+
timeoutMs,
|
|
546
|
+
partialScanned,
|
|
547
|
+
),
|
|
548
|
+
loadInventorySource(
|
|
549
|
+
'process.running_sessions',
|
|
550
|
+
() => loaders.scanRunningSessions(),
|
|
551
|
+
[],
|
|
552
|
+
timeoutMs,
|
|
553
|
+
),
|
|
554
|
+
]);
|
|
555
|
+
|
|
556
|
+
return {
|
|
557
|
+
scannedSessions,
|
|
558
|
+
runningSessions,
|
|
559
|
+
scannedAt,
|
|
560
|
+
refreshedAt: Date.now(),
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function scheduleExternalInventoryRefresh(loaders: InventoryLoaders, timeoutMs: number): void {
|
|
565
|
+
if (externalInventoryRefreshInflight) return;
|
|
566
|
+
|
|
567
|
+
const generation = externalInventoryGeneration;
|
|
568
|
+
const endTimer = metrics.startTimer('session.inventory.backfill_latency_ms');
|
|
569
|
+
externalInventoryRefreshInflight = loadExternalInventorySnapshot(loaders, timeoutMs)
|
|
570
|
+
.then((nextSnapshot) => {
|
|
571
|
+
if (generation !== externalInventoryGeneration) return;
|
|
572
|
+
|
|
573
|
+
const previousSignature = externalInventorySignature(externalInventorySnapshot);
|
|
574
|
+
externalInventorySnapshot = nextSnapshot;
|
|
575
|
+
const nextSignature = externalInventorySignature(nextSnapshot);
|
|
576
|
+
const durationMs = endTimer();
|
|
577
|
+
externalInventoryLastBackfillDurationMs = durationMs;
|
|
578
|
+
externalInventoryLastBackfillCompletedAt = Date.now();
|
|
579
|
+
updateExternalInventoryGauges();
|
|
580
|
+
if (previousSignature === nextSignature) return;
|
|
581
|
+
|
|
582
|
+
externalInventoryLastBackfillAppliedAt = Date.now();
|
|
583
|
+
metrics.increment('session.inventory.backfill_applied');
|
|
584
|
+
log.info(
|
|
585
|
+
{
|
|
586
|
+
durationMs,
|
|
587
|
+
scanned: nextSnapshot.scannedSessions.length,
|
|
588
|
+
running: nextSnapshot.runningSessions.length,
|
|
589
|
+
},
|
|
590
|
+
'session inventory external backfill applied',
|
|
591
|
+
);
|
|
592
|
+
notifyExternalInventoryBackfillListeners();
|
|
593
|
+
})
|
|
594
|
+
.catch((error) => {
|
|
595
|
+
if (generation !== externalInventoryGeneration) return;
|
|
596
|
+
externalInventoryLastBackfillDurationMs = endTimer();
|
|
597
|
+
externalInventoryLastBackfillCompletedAt = Date.now();
|
|
598
|
+
updateExternalInventoryGauges();
|
|
599
|
+
log.warn(
|
|
600
|
+
{
|
|
601
|
+
error: String(error),
|
|
602
|
+
durationMs: externalInventoryLastBackfillDurationMs,
|
|
603
|
+
},
|
|
604
|
+
'session inventory external backfill failed',
|
|
605
|
+
);
|
|
606
|
+
})
|
|
607
|
+
.finally(() => {
|
|
608
|
+
if (generation === externalInventoryGeneration) {
|
|
609
|
+
externalInventoryRefreshInflight = null;
|
|
610
|
+
updateExternalInventoryGauges();
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
updateExternalInventoryGauges();
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
export async function __waitForExternalInventoryRefreshForTests(): Promise<void> {
|
|
617
|
+
await externalInventoryRefreshInflight;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
export function __emitExternalInventoryBackfillForTests(): void {
|
|
621
|
+
notifyExternalInventoryBackfillListeners();
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
export function __resetExternalInventoryStateForTests(): void {
|
|
625
|
+
externalInventoryGeneration += 1;
|
|
626
|
+
externalInventorySnapshot = createEmptyExternalInventorySnapshot();
|
|
627
|
+
externalInventoryRefreshInflight = null;
|
|
628
|
+
externalInventoryLastBackfillCompletedAt = 0;
|
|
629
|
+
externalInventoryLastBackfillDurationMs = 0;
|
|
630
|
+
externalInventoryLastBackfillAppliedAt = 0;
|
|
631
|
+
externalInventoryBackfillListeners.clear();
|
|
632
|
+
updateExternalInventoryGauges();
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
export async function listSessionPage(options: ListSessionPageOptions): Promise<SessionListPage> {
|
|
636
|
+
const loaders: InventoryLoaders = {
|
|
637
|
+
...defaultInventoryLoaders,
|
|
638
|
+
...(options.loaders ?? {}),
|
|
639
|
+
};
|
|
640
|
+
const timeoutMs = Math.max(250, options.sourceTimeoutMs ?? DEFAULT_INVENTORY_SOURCE_TIMEOUT_MS);
|
|
641
|
+
if (!options.search) {
|
|
642
|
+
const endFastPath = metrics.startTimer('session.inventory.fast_path_latency_ms');
|
|
643
|
+
if (shouldRefreshExternalInventory()) {
|
|
644
|
+
scheduleExternalInventoryRefresh(loaders, timeoutMs);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const snapshot = externalInventorySnapshot;
|
|
648
|
+
const page = buildSessionInventoryPage(
|
|
649
|
+
{
|
|
650
|
+
activeSessions: options.activeSessions,
|
|
651
|
+
sessionRecords: options.sessionRecords,
|
|
652
|
+
scannedSessions: snapshot.scannedSessions,
|
|
653
|
+
runningSessions: snapshot.runningSessions,
|
|
654
|
+
scannedAt: snapshot.scannedAt || new Date().toISOString(),
|
|
655
|
+
deletedSessionIds: options.deletedSessionIds,
|
|
656
|
+
},
|
|
657
|
+
options.limit,
|
|
658
|
+
options.cursor,
|
|
659
|
+
options.agent,
|
|
660
|
+
options.cwd,
|
|
661
|
+
);
|
|
662
|
+
endFastPath();
|
|
663
|
+
return page;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Shared array that the scanner populates incrementally (mtime-sorted, recent first).
|
|
667
|
+
// On timeout, loadInventorySource returns whatever has been collected so far
|
|
668
|
+
// instead of discarding all results.
|
|
669
|
+
const partialScanned: SessionInfo[] = [];
|
|
670
|
+
const scannedAt = new Date().toISOString();
|
|
671
|
+
const search = options.search;
|
|
672
|
+
const [scannedSessions, runningSessions, contentMatchIds] = await Promise.all([
|
|
673
|
+
loadInventorySource(
|
|
674
|
+
'scanner.sessions',
|
|
675
|
+
() => loaders.listScannedSessions(options.agent, options.cwd, partialScanned),
|
|
676
|
+
[] as SessionInfo[],
|
|
677
|
+
timeoutMs,
|
|
678
|
+
partialScanned,
|
|
679
|
+
),
|
|
680
|
+
loadInventorySource(
|
|
681
|
+
'process.running_sessions',
|
|
682
|
+
() => loaders.scanRunningSessions(),
|
|
683
|
+
[],
|
|
684
|
+
timeoutMs,
|
|
685
|
+
),
|
|
686
|
+
loadInventorySource(
|
|
687
|
+
'scanner.search_content',
|
|
688
|
+
() => loaders.searchSessionContent(search, options.agent),
|
|
689
|
+
undefined,
|
|
690
|
+
timeoutMs,
|
|
691
|
+
),
|
|
692
|
+
]);
|
|
693
|
+
|
|
694
|
+
log.info(
|
|
695
|
+
{
|
|
696
|
+
search: options.search,
|
|
697
|
+
contentMatches: contentMatchIds?.size ?? 0,
|
|
698
|
+
scanned: scannedSessions.length,
|
|
699
|
+
records: options.sessionRecords.length,
|
|
700
|
+
active: options.activeSessions.length,
|
|
701
|
+
},
|
|
702
|
+
'search results',
|
|
703
|
+
);
|
|
704
|
+
|
|
705
|
+
return buildSessionInventoryPage(
|
|
706
|
+
{
|
|
707
|
+
activeSessions: options.activeSessions,
|
|
708
|
+
sessionRecords: options.sessionRecords,
|
|
709
|
+
scannedSessions,
|
|
710
|
+
runningSessions,
|
|
711
|
+
scannedAt,
|
|
712
|
+
deletedSessionIds: options.deletedSessionIds,
|
|
713
|
+
},
|
|
714
|
+
options.limit,
|
|
715
|
+
options.cursor,
|
|
716
|
+
options.agent,
|
|
717
|
+
options.cwd,
|
|
718
|
+
search,
|
|
719
|
+
contentMatchIds,
|
|
720
|
+
);
|
|
721
|
+
}
|