@lincoln504/pi-research 0.1.13 → 1.0.1
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/LICENSE +1 -1
- package/README.md +55 -138
- package/dist/cli.mjs +23375 -0
- package/dist/prompts/research-knowledge-relevance-triage.md +35 -0
- package/dist/prompts/research-knowledge-search-extractor.md +41 -0
- package/dist/prompts/research-tool-usage.md +92 -0
- package/dist/prompts/researcher.md +89 -0
- package/dist/prompts/system-coordinator.md +75 -0
- package/dist/prompts/system-lead-evaluator.md +118 -0
- package/dist/thread-worker.mjs +2611 -0
- package/dist/webgpu-probe.mjs +80 -0
- package/docs/AGENT-SKILL.md +130 -0
- package/docs/ARCHITECTURE.md +306 -80
- package/docs/CONFIGURATION.md +249 -0
- package/docs/KNOWLEDGE-STORE.md +211 -0
- package/docs/PI-EXTENSION.md +91 -0
- package/docs/SDK.md +122 -0
- package/docs/THIRD-PARTY-NOTICES.md +66 -0
- package/package.json +102 -36
- package/scripts/build.cjs +140 -0
- package/scripts/cleanup.cjs +149 -0
- package/scripts/prepare.cjs +28 -0
- package/scripts/setup.cjs +134 -0
- package/skills/pi-research/README.md +90 -0
- package/skills/pi-research/SKILL.md +102 -0
- package/skills/pi-research/references/configuration.md +77 -0
- package/skills/pi-research/scripts/run.mjs +237 -0
- package/src/cleanup/research-cleanup.ts +140 -0
- package/src/cli.ts +1195 -0
- package/src/config.ts +1032 -155
- package/src/constants.ts +115 -84
- package/src/core/health-check-service.ts +265 -0
- package/src/core/interfaces/health-check-interfaces.ts +50 -0
- package/src/core/interfaces/knowledge-interfaces.ts +190 -0
- package/src/core/interfaces/observer-interfaces.ts +67 -0
- package/src/core/interfaces/orchestration-interfaces.ts +86 -0
- package/src/core/interfaces/planning-interfaces.ts +100 -0
- package/src/core/interfaces/process-interfaces.ts +37 -0
- package/src/core/interfaces/research-plan-types.ts +52 -0
- package/src/core/interfaces/scheduler-interfaces.ts +91 -0
- package/src/core/interfaces/service-names.ts +39 -0
- package/src/core/interfaces/state-manager-interfaces.ts +29 -0
- package/src/core/interfaces/steering-interfaces.ts +31 -0
- package/src/core/llm/agentic-repair.ts +169 -0
- package/src/core/llm/inject-date.ts +52 -0
- package/src/core/llm/llm-timeout.ts +64 -0
- package/src/core/llm/llm-utils.ts +86 -0
- package/src/core/llm/model-registry-factory.ts +287 -0
- package/src/core/llm/pi-ai-completion.ts +29 -0
- package/src/core/llm/prompts.ts +50 -0
- package/src/core/llm/research-model-resolver.ts +91 -0
- package/src/core/planning-constants.ts +13 -0
- package/src/core/planning-service.ts +649 -0
- package/src/core/planning-utils.ts +299 -0
- package/src/core/scheduler-factory.ts +48 -0
- package/src/core/scheduler-service.ts +123 -0
- package/src/core/service-initialization.ts +194 -0
- package/src/core/service-interfaces.ts +48 -0
- package/src/core/service-registry.ts +659 -0
- package/src/healthcheck/index.ts +271 -265
- package/src/healthcheck/registry.ts +130 -0
- package/src/index.ts +639 -50
- package/src/infrastructure/browser/browser-cleanup.ts +309 -0
- package/src/infrastructure/browser/browser-client.ts +199 -0
- package/src/infrastructure/browser/browser-error-utils.ts +160 -0
- package/src/infrastructure/browser/browser-lifecycle.ts +120 -0
- package/src/infrastructure/browser/browser-server.ts +174 -0
- package/src/infrastructure/browser/browser-task-scheduler.ts +461 -0
- package/src/infrastructure/browser/cleanup-utils.ts +155 -0
- package/src/infrastructure/browser/client-agent.ts +31 -0
- package/src/infrastructure/browser/config.ts +288 -0
- package/src/infrastructure/browser/ensure-browser.ts +205 -0
- package/src/infrastructure/browser/index.ts +37 -0
- package/src/infrastructure/browser/priority-task-queue.ts +226 -0
- package/src/infrastructure/browser/scheduler-factory.ts +354 -0
- package/src/infrastructure/browser/task-execution-service.ts +209 -0
- package/src/infrastructure/browser/thread-worker-browser.ts +282 -0
- package/src/infrastructure/browser/thread-worker-lifecycle.ts +323 -0
- package/src/infrastructure/browser/thread-worker-messaging.ts +662 -0
- package/src/infrastructure/browser/thread-worker-types.ts +25 -0
- package/src/infrastructure/browser/thread-worker.mjs +2611 -0
- package/src/infrastructure/browser/thread-worker.ts +168 -0
- package/src/infrastructure/browser/worker-pool-manager.ts +449 -0
- package/src/infrastructure/embedding/embedding-client.ts +205 -0
- package/src/infrastructure/embedding/embedding-factory.ts +324 -0
- package/src/infrastructure/embedding/embedding-server.ts +535 -0
- package/src/infrastructure/file-lock-service.ts +592 -0
- package/src/infrastructure/gpu-resource-service.ts +191 -0
- package/src/infrastructure/knowledge-store-service.ts +475 -0
- package/src/infrastructure/metrics-service.ts +129 -0
- package/src/infrastructure/process-lifecycle-service.ts +253 -0
- package/src/infrastructure/scheduler-factory-service.ts +58 -0
- package/src/infrastructure/service-initialization.ts +292 -0
- package/src/infrastructure/state/state-backup-manager.ts +290 -0
- package/src/infrastructure/state/state-browser-api.ts +62 -0
- package/src/infrastructure/state/state-browser-manager.ts +70 -0
- package/src/infrastructure/state/state-manager-service.ts +264 -0
- package/src/infrastructure/state/state-manager.ts +547 -0
- package/src/infrastructure/state/state-metrics.ts +92 -0
- package/src/infrastructure/state/state-migration.ts +103 -0
- package/src/infrastructure/state/state-path-configuration.ts +119 -0
- package/src/infrastructure/state/state-session-api.ts +94 -0
- package/src/infrastructure/state/state-session-manager.ts +127 -0
- package/src/infrastructure/state/state-validator.ts +93 -0
- package/src/infrastructure/types/state-types.ts +85 -0
- package/src/knowledge/chunker.ts +157 -0
- package/src/knowledge/embedder-init.ts +413 -0
- package/src/knowledge/embedder-utils.ts +296 -0
- package/src/knowledge/embedder.ts +592 -0
- package/src/knowledge/index.ts +126 -0
- package/src/knowledge/lancedb-loader.ts +35 -0
- package/src/knowledge/migration.ts +17 -0
- package/src/knowledge/model-config.ts +132 -0
- package/src/knowledge/onnx-env.ts +48 -0
- package/src/knowledge/store-operations.ts +291 -0
- package/src/knowledge/store-schema.ts +79 -0
- package/src/knowledge/store.ts +1251 -0
- package/src/knowledge/transformers-loader.ts +31 -0
- package/src/knowledge/webgpu-probe.mjs +80 -0
- package/src/knowledge/webgpu-viability.ts +255 -0
- package/src/knowledge/writer-queue.ts +252 -0
- package/src/logger.ts +316 -122
- package/src/observers/research-observer-impl.ts +459 -0
- package/src/orchestration/deep-research-orchestrator.ts +524 -773
- package/src/orchestration/deep-research-types.ts +47 -35
- package/src/orchestration/headless-observer.ts +114 -0
- package/src/orchestration/orchestration-types.ts +33 -0
- package/src/orchestration/quick-research-orchestrator.ts +463 -0
- package/src/orchestration/research-orchestration-service.ts +549 -0
- package/src/orchestration/research-session-service.ts +178 -0
- package/src/orchestration/research-synthesis-service.ts +348 -0
- package/src/orchestration/researcher-executor.ts +394 -0
- package/src/orchestration/researcher.ts +115 -42
- package/src/orchestration/service-initialization.ts +109 -0
- package/src/orchestration/session-context.ts +7 -3
- package/src/orchestration/session-state.ts +672 -0
- package/src/prompts/research-knowledge-relevance-triage.md +35 -0
- package/src/prompts/research-knowledge-search-extractor.md +41 -0
- package/src/prompts/research-tool-usage.md +63 -16
- package/src/prompts/researcher.md +59 -136
- package/src/prompts/system-coordinator.md +64 -13
- package/src/prompts/system-lead-evaluator.md +82 -189
- package/src/research-config.ts +872 -0
- package/src/sdk.ts +847 -0
- package/src/security/cisa-kev.ts +72 -9
- package/src/security/cvss.ts +92 -0
- package/src/security/ecosystem.ts +45 -0
- package/src/security/github-advisories.ts +168 -428
- package/src/security/github-advisory-types.ts +282 -0
- package/src/security/index.ts +63 -177
- package/src/security/nvd-client.ts +550 -0
- package/src/security/nvd-types.ts +179 -0
- package/src/security/osv-client.ts +246 -0
- package/src/security/osv-types.ts +285 -0
- package/src/security/types.ts +67 -53
- package/src/skill-install/skill-installer.ts +519 -0
- package/src/stackexchange/index.ts +81 -27
- package/src/stackexchange/output/compact.ts +7 -6
- package/src/stackexchange/output/table.ts +11 -7
- package/src/stackexchange/queries.ts +0 -10
- package/src/stackexchange/rest-client.ts +141 -66
- package/src/stackexchange/types.ts +0 -17
- package/src/tool.ts +15 -423
- package/src/tools/grep.ts +32 -310
- package/src/tools/health-tool-definition.ts +101 -0
- package/src/tools/index.ts +38 -9
- package/src/tools/research-knowledge-search.ts +974 -0
- package/src/tools/research-knowledge-types.ts +77 -0
- package/src/tools/research-tool-definition.ts +494 -0
- package/src/tools/scrape.ts +286 -388
- package/src/tools/search.ts +98 -153
- package/src/tools/security.ts +107 -80
- package/src/tools/stackexchange.ts +58 -35
- package/src/tools/youtube-transcript.ts +163 -0
- package/src/tui/knowledge-search-panel.ts +86 -0
- package/src/tui/pulse.ts +86 -0
- package/src/tui/research-health.ts +124 -0
- package/src/tui/research-panel-color-utils.ts +340 -0
- package/src/tui/research-panel-format.ts +54 -0
- package/src/tui/research-panel-state.ts +404 -0
- package/src/tui/research-panel-wave.ts +280 -0
- package/src/tui/research-panel.ts +339 -435
- package/src/tui/research-tui-manager.ts +233 -0
- package/src/tui/tui-controller.ts +137 -0
- package/src/types/extension-context.ts +18 -49
- package/src/types/index.ts +61 -0
- package/src/types/llm.ts +68 -29
- package/src/types/research-panel-types.ts +61 -0
- package/src/utils/circuit-breaker.ts +146 -0
- package/src/utils/citation-utils.ts +97 -0
- package/src/utils/disk-space-checker.ts +55 -0
- package/src/utils/error-tracker.ts +265 -0
- package/src/utils/host-config.ts +45 -0
- package/src/utils/input-validation.ts +8 -10
- package/src/utils/json-utils.ts +150 -16
- package/src/utils/llm-usage.ts +54 -0
- package/src/utils/log-rotation.ts +125 -0
- package/src/utils/log-utils.ts +159 -0
- package/src/utils/make-resource-loader.ts +3 -1
- package/src/utils/metrics-summary.ts +459 -0
- package/src/utils/metrics.ts +302 -0
- package/src/utils/research-export.ts +28 -12
- package/src/utils/safe-unref.ts +31 -0
- package/src/utils/shared-links.ts +195 -90
- package/src/utils/shutdown-manager.ts +65 -0
- package/src/utils/stdio-capture.ts +407 -0
- package/src/utils/text-utils.ts +258 -11
- package/src/utils/tool-usage-tracker.ts +83 -16
- package/src/utils/url-utils.ts +255 -0
- package/src/utils/user-agent.ts +28 -0
- package/src/web-research/browser-search.ts +179 -0
- package/src/web-research/retry-utils.ts +173 -77
- package/src/web-research/scraper-types.ts +72 -0
- package/src/web-research/scraper-utils.ts +434 -0
- package/src/web-research/search.ts +78 -218
- package/src/web-research/types.ts +9 -24
- package/src/web-research/utils.ts +2 -143
- package/src/web-research/web-scraper.ts +477 -0
- package/src/youtube/index.ts +154 -0
- package/src/youtube/potoken.ts +259 -0
- package/src/youtube/transcript-client.ts +390 -0
- package/src/youtube/video-id.ts +118 -0
- package/config/searxng/default-settings.yml +0 -189
- package/config/searxng/limiter.toml +0 -34
- package/config/tooling/eslint.config.js +0 -73
- package/config/tooling/vitest.config.integration.ts +0 -17
- package/config/tooling/vitest.config.ts +0 -40
- package/config/tooling/vitest.config.unit.ts +0 -12
- package/docs/assets/README-banner.jpg +0 -0
- package/scripts/setup.js +0 -212
- package/src/infrastructure/searxng-lifecycle.ts +0 -644
- package/src/infrastructure/searxng-manager.ts +0 -1183
- package/src/infrastructure/state-manager.ts +0 -918
- package/src/orchestration/deep-research-reducer.ts +0 -129
- package/src/orchestration/id-utils.ts +0 -63
- package/src/security/nvd.ts +0 -532
- package/src/security/osv.ts +0 -487
- package/src/stackexchange/cache.ts +0 -111
- package/src/stackexchange/output/index.ts +0 -7
- package/src/stackexchange/output/json.ts +0 -21
- package/src/tui/searxng-status.ts +0 -80
- package/src/utils/inject-date.ts +0 -29
- package/src/utils/research-error.ts +0 -205
- package/src/utils/searxng-config.ts +0 -94
- package/src/utils/session-state.ts +0 -295
- package/src/web-research/dependencies.ts +0 -20
- package/src/web-research/scrapers.ts +0 -935
- package/tsconfig.json +0 -40
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared State Manager
|
|
3
|
+
*
|
|
4
|
+
* Core implementation for cross-process state management.
|
|
5
|
+
* Manages the state file, locking, backups, and provides APIs for
|
|
6
|
+
* session and browser management.
|
|
7
|
+
*
|
|
8
|
+
* This implementation is decomposed into several sub-managers:
|
|
9
|
+
* - FileLockService for file locking
|
|
10
|
+
* - GPUResourceService for GPU lock management
|
|
11
|
+
* - StateSessionManager for session operations
|
|
12
|
+
* - StateBrowserManager for browser operations
|
|
13
|
+
* - StateBackupManager for backup/recovery
|
|
14
|
+
* - StateMetricsCollector for metrics
|
|
15
|
+
* - StateValidator for validation
|
|
16
|
+
* - StateSessionApi for session API
|
|
17
|
+
* - StateBrowserApi for browser API
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import * as fs from 'node:fs/promises';
|
|
21
|
+
import * as path from 'node:path';
|
|
22
|
+
import * as crypto from 'node:crypto';
|
|
23
|
+
import process from 'node:process';
|
|
24
|
+
import { getGlobalConfigDir } from '../../config.ts';
|
|
25
|
+
import { logger } from '../../logger.ts';
|
|
26
|
+
import { metrics } from '../../utils/metrics.ts';
|
|
27
|
+
import type { IProcessLifecycle } from '../../core/interfaces/process-interfaces.ts';
|
|
28
|
+
import type {
|
|
29
|
+
StateMetrics,
|
|
30
|
+
SingletonState,
|
|
31
|
+
} from '../types/state-types.ts';
|
|
32
|
+
import { CURRENT_STATE_VERSION } from '../types/state-types.ts';
|
|
33
|
+
import { FileLockService } from '../file-lock-service.ts';
|
|
34
|
+
import type { GPUResourceService } from '../gpu-resource-service.ts';
|
|
35
|
+
import { StateBackupManager } from './state-backup-manager.ts';
|
|
36
|
+
import { StateVersionTooNewError } from './state-migration.ts';
|
|
37
|
+
import type { StateBrowserManager } from './state-browser-manager.ts';
|
|
38
|
+
import type { StateSessionManager } from './state-session-manager.ts';
|
|
39
|
+
import type { StateMetricsCollector } from './state-metrics.ts';
|
|
40
|
+
import type { StateValidator } from './state-validator.ts';
|
|
41
|
+
import { StateSessionApi } from './state-session-api.ts';
|
|
42
|
+
import { StateBrowserApi } from './state-browser-api.ts';
|
|
43
|
+
|
|
44
|
+
// Re-export commonly used types
|
|
45
|
+
export type { StateMetrics, SingletonState } from '../types/state-types.ts';
|
|
46
|
+
|
|
47
|
+
export interface StateManagerOptions {
|
|
48
|
+
stateDir?: string;
|
|
49
|
+
processLifecycle: IProcessLifecycle;
|
|
50
|
+
gpuResourceService: GPUResourceService;
|
|
51
|
+
sessionManager: StateSessionManager;
|
|
52
|
+
browserManager: StateBrowserManager;
|
|
53
|
+
metricsCollector: StateMetricsCollector;
|
|
54
|
+
validator: StateValidator;
|
|
55
|
+
fileLockService: FileLockService;
|
|
56
|
+
backupManager: StateBackupManager;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Shared State Manager Implementation
|
|
61
|
+
*/
|
|
62
|
+
export class StateManager {
|
|
63
|
+
private stateFilePath: string;
|
|
64
|
+
private lockDirPath: string;
|
|
65
|
+
private backupDirPath: string;
|
|
66
|
+
private lockFilePath: string;
|
|
67
|
+
|
|
68
|
+
// Sub-services (injected via constructor)
|
|
69
|
+
private readonly fileLockService: FileLockService;
|
|
70
|
+
private readonly processLifecycle: IProcessLifecycle;
|
|
71
|
+
private readonly gpuResourceService: GPUResourceService;
|
|
72
|
+
private readonly sessionApi: StateSessionApi;
|
|
73
|
+
private readonly browserApi: StateBrowserApi;
|
|
74
|
+
private readonly backupManager: StateBackupManager;
|
|
75
|
+
private readonly metricsCollector: StateMetricsCollector;
|
|
76
|
+
private readonly validator: StateValidator;
|
|
77
|
+
|
|
78
|
+
// Set when a read finds the state file was written by a newer build than this
|
|
79
|
+
// process supports. While set, writes are suppressed so we never overwrite the
|
|
80
|
+
// newer file (which holds that build's sessions, browser lease, authSecret).
|
|
81
|
+
private stateTooNew = false;
|
|
82
|
+
|
|
83
|
+
// One-time guard for the orphaned-temp sweep below.
|
|
84
|
+
private orphanTmpSwept = false;
|
|
85
|
+
|
|
86
|
+
constructor(options: StateManagerOptions) {
|
|
87
|
+
const {
|
|
88
|
+
stateDir: providedStateDir,
|
|
89
|
+
processLifecycle,
|
|
90
|
+
fileLockService,
|
|
91
|
+
gpuResourceService,
|
|
92
|
+
sessionManager,
|
|
93
|
+
browserManager,
|
|
94
|
+
backupManager,
|
|
95
|
+
metricsCollector,
|
|
96
|
+
validator,
|
|
97
|
+
} = options;
|
|
98
|
+
|
|
99
|
+
let stateDir = providedStateDir;
|
|
100
|
+
|
|
101
|
+
if (!stateDir) {
|
|
102
|
+
// pi-research's own namespace (~/.pi/research/state), not the host pi root.
|
|
103
|
+
stateDir = path.join(getGlobalConfigDir(), 'state');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
this.stateFilePath = path.join(stateDir, 'research-state.json');
|
|
107
|
+
this.lockDirPath = path.join(stateDir, '.locks');
|
|
108
|
+
this.backupDirPath = path.join(stateDir, 'backups');
|
|
109
|
+
this.lockFilePath = path.join(this.lockDirPath, 'research-state.lock');
|
|
110
|
+
|
|
111
|
+
// Use injected services
|
|
112
|
+
this.fileLockService = fileLockService;
|
|
113
|
+
this.processLifecycle = processLifecycle;
|
|
114
|
+
this.gpuResourceService = gpuResourceService;
|
|
115
|
+
this.sessionApi = new StateSessionApi(sessionManager);
|
|
116
|
+
this.browserApi = new StateBrowserApi(browserManager);
|
|
117
|
+
this.backupManager = backupManager;
|
|
118
|
+
this.metricsCollector = metricsCollector;
|
|
119
|
+
this.validator = validator;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Initialize directories with proper permissions
|
|
124
|
+
*/
|
|
125
|
+
private async ensureDirectories(): Promise<void> {
|
|
126
|
+
const dirs = [
|
|
127
|
+
path.dirname(this.stateFilePath),
|
|
128
|
+
this.lockDirPath,
|
|
129
|
+
this.backupDirPath,
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
for (const dir of dirs) {
|
|
133
|
+
try {
|
|
134
|
+
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
135
|
+
} catch (err) {
|
|
136
|
+
logger.error(`[StateManager] Failed to create directory ${dir}:`, err);
|
|
137
|
+
throw err;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
await this.sweepOrphanedTempFiles();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Reclaim orphaned `research-state-*.tmp` files left in the state dir.
|
|
146
|
+
*
|
|
147
|
+
* Both the primary writer (_writeState) and the recovery writer
|
|
148
|
+
* (StateBackupManager.atomicWriteStateFile) write via temp+fsync+rename. The
|
|
149
|
+
* in-process `finally` cleans the temp on a failed write, but a process killed
|
|
150
|
+
* mid-write (SIGKILL, crash, OOM) between open and rename leaks the temp, and
|
|
151
|
+
* nothing else ever removes it — only backups/ is pruned. Sweep them here, once,
|
|
152
|
+
* on the first directory ensure.
|
|
153
|
+
*
|
|
154
|
+
* Only temps older than a safety window are removed: a live write holds the
|
|
155
|
+
* state lock and a temp exists for milliseconds, so anything older than the
|
|
156
|
+
* window cannot belong to a concurrent in-flight write (in this or another
|
|
157
|
+
* process). This makes the sweep safe without taking the state lock.
|
|
158
|
+
*/
|
|
159
|
+
private async sweepOrphanedTempFiles(): Promise<void> {
|
|
160
|
+
if (this.orphanTmpSwept) return;
|
|
161
|
+
this.orphanTmpSwept = true;
|
|
162
|
+
|
|
163
|
+
const STALE_TMP_MS = 5 * 60 * 1000; // 5 minutes — far longer than any real write
|
|
164
|
+
const stateDir = path.dirname(this.stateFilePath);
|
|
165
|
+
const cutoff = Date.now() - STALE_TMP_MS;
|
|
166
|
+
try {
|
|
167
|
+
const entries = await fs.readdir(stateDir);
|
|
168
|
+
for (const entry of entries) {
|
|
169
|
+
if (!entry.startsWith('research-state-') || !entry.endsWith('.tmp')) continue;
|
|
170
|
+
const filePath = path.join(stateDir, entry);
|
|
171
|
+
try {
|
|
172
|
+
const stats = await fs.stat(filePath);
|
|
173
|
+
if (!stats.isFile() || stats.mtimeMs >= cutoff) continue;
|
|
174
|
+
await fs.unlink(filePath);
|
|
175
|
+
logger.debug(`[StateManager] Removed orphaned state temp file: ${entry}`);
|
|
176
|
+
} catch {
|
|
177
|
+
// Raced with another sweeper or the file vanished — ignore.
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
} catch (err) {
|
|
181
|
+
// Never let cleanup hygiene fail directory setup.
|
|
182
|
+
logger.debug('[StateManager] Orphaned temp sweep failed (non-fatal):', err);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Get the default state
|
|
188
|
+
*/
|
|
189
|
+
private getDefaultState(): SingletonState {
|
|
190
|
+
return {
|
|
191
|
+
version: CURRENT_STATE_VERSION,
|
|
192
|
+
containerId: crypto.randomBytes(16).toString('hex'),
|
|
193
|
+
containerName: 'pi-research-shared-state',
|
|
194
|
+
port: 0,
|
|
195
|
+
sessions: {},
|
|
196
|
+
lastUpdated: Date.now(),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ==================== Core State Operations ====================
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Internal read without lock acquisition (caller must hold lock)
|
|
204
|
+
*/
|
|
205
|
+
private async _readState(): Promise<SingletonState> {
|
|
206
|
+
// Once this process has seen a newer-build state file, it runs read-only on an in-memory
|
|
207
|
+
// default for the rest of its life (writes are suppressed in _writeState). Short-circuit
|
|
208
|
+
// here so every subsequent read does NOT re-open the file, re-throw StateVersionTooNewError,
|
|
209
|
+
// and re-quarantine a fresh copy on each call — which, on a hot path (heartbeats, GPU lock,
|
|
210
|
+
// metrics), produced an unbounded storm of `*.quarantine` copies under the state lock.
|
|
211
|
+
if (this.stateTooNew) {
|
|
212
|
+
return this.getDefaultState();
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
const content = await fs.readFile(this.stateFilePath, 'utf-8');
|
|
216
|
+
const state = JSON.parse(content) as unknown;
|
|
217
|
+
return this.validator.migrateAndValidate(state);
|
|
218
|
+
} catch (error: unknown) {
|
|
219
|
+
if (typeof error === 'object' && error !== null && 'code' in error) {
|
|
220
|
+
const errnoError = error as NodeJS.ErrnoException;
|
|
221
|
+
if (errnoError.code === 'ENOENT') {
|
|
222
|
+
return this.getDefaultState();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// A file written by a NEWER build is not corruption. Recovering/overwriting
|
|
227
|
+
// it would destroy that build's live sessions, browser lease, and
|
|
228
|
+
// authSecret. Instead: quarantine a copy for forensics, mark this process
|
|
229
|
+
// read-only so no write clobbers the newer file, and run on an in-memory
|
|
230
|
+
// default. Normal service resumes once this build is upgraded.
|
|
231
|
+
if (error instanceof StateVersionTooNewError) {
|
|
232
|
+
logger.error(
|
|
233
|
+
`[StateManager] ${error.message} Running in read-only mode on an in-memory default; writes are suppressed to protect the newer file.`,
|
|
234
|
+
);
|
|
235
|
+
this.stateTooNew = true;
|
|
236
|
+
await this.backupManager
|
|
237
|
+
.quarantineFutureState(error.fileVersion)
|
|
238
|
+
.catch((qErr: unknown) =>
|
|
239
|
+
logger.warn(
|
|
240
|
+
`[StateManager] Failed to quarantine newer state file: ${qErr instanceof Error ? qErr.message : String(qErr)}`,
|
|
241
|
+
),
|
|
242
|
+
);
|
|
243
|
+
return this.getDefaultState();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Corruption is not only truncated/garbage JSON — a well-formed file can
|
|
247
|
+
// still fail schema validation (missing field, out-of-range port). These
|
|
248
|
+
// throw from JSON.parse (SyntaxError) or validateState ("Invalid state:
|
|
249
|
+
// ..."). Treat every such case as recoverable so a single bad file can
|
|
250
|
+
// never brick every reader; genuine I/O errors (EACCES/EPERM/EISDIR) are
|
|
251
|
+
// NOT corruption and still surface via the throw below. A future `version`
|
|
252
|
+
// is handled separately above (quarantine, not overwrite).
|
|
253
|
+
// A missing migration step ("No migration registered from state version
|
|
254
|
+
// N…") means an old, un-upgradeable file — recover to a fresh default like
|
|
255
|
+
// any other corruption. A buggy migration ("Migration … produced version
|
|
256
|
+
// …") is a programming error and is intentionally left to surface.
|
|
257
|
+
const isCorruption =
|
|
258
|
+
error instanceof SyntaxError ||
|
|
259
|
+
(error instanceof Error &&
|
|
260
|
+
(error.message.includes('parse') ||
|
|
261
|
+
error.message.startsWith('Invalid state') ||
|
|
262
|
+
error.message.startsWith('No migration registered')));
|
|
263
|
+
if (isCorruption) {
|
|
264
|
+
logger.error(
|
|
265
|
+
`[StateManager] State file corrupt/invalid (${error instanceof Error ? error.message : String(error)}); attempting recovery...`,
|
|
266
|
+
);
|
|
267
|
+
try {
|
|
268
|
+
await this.recoverFromCorruptionDirect();
|
|
269
|
+
const recovered = await fs.readFile(this.stateFilePath, 'utf-8');
|
|
270
|
+
return this.validator.migrateAndValidate(JSON.parse(recovered) as unknown);
|
|
271
|
+
} catch (recoveryError) {
|
|
272
|
+
logger.error(
|
|
273
|
+
`[StateManager] Recovery failed (${recoveryError instanceof Error ? recoveryError.message : String(recoveryError)}); using in-memory default state.`,
|
|
274
|
+
);
|
|
275
|
+
return this.getDefaultState();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
throw new Error(`Failed to read state: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Read the state from the file system
|
|
285
|
+
* @param skipLock If true, skip acquiring the file lock (caller must hold it)
|
|
286
|
+
*/
|
|
287
|
+
public async readState(skipLock?: boolean): Promise<SingletonState> {
|
|
288
|
+
await this.ensureDirectories();
|
|
289
|
+
if (skipLock) {
|
|
290
|
+
return await this._readState();
|
|
291
|
+
}
|
|
292
|
+
return await this.fileLockService.withLock(async () => {
|
|
293
|
+
return await this._readState();
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Write the state to the file system atomically with backup creation
|
|
299
|
+
*/
|
|
300
|
+
public async writeState(state: SingletonState): Promise<void> {
|
|
301
|
+
await this.ensureDirectories();
|
|
302
|
+
const startTime = Date.now();
|
|
303
|
+
try {
|
|
304
|
+
await this.fileLockService.withLock(() => this._writeState(state));
|
|
305
|
+
const duration = Date.now() - startTime;
|
|
306
|
+
metrics.observe('state_operation_duration_ms', duration, { operation: 'write', status: 'success' });
|
|
307
|
+
metrics.increment('state_operations_total', 1, { operation: 'write', status: 'success' });
|
|
308
|
+
} catch (error) {
|
|
309
|
+
const duration = Date.now() - startTime;
|
|
310
|
+
metrics.observe('state_operation_duration_ms', duration, { operation: 'write', status: 'error' });
|
|
311
|
+
metrics.increment('state_operations_total', 1, { operation: 'write', status: 'error' });
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Internal write without lock acquisition (caller must hold lock)
|
|
318
|
+
*/
|
|
319
|
+
private async _writeState(state: SingletonState): Promise<void> {
|
|
320
|
+
// A prior read found a newer-build state file on disk. Suppress the write so
|
|
321
|
+
// we do not clobber that build's live state with our older-schema view. The
|
|
322
|
+
// in-memory updates are lost, which is the intended degradation: data loss
|
|
323
|
+
// of the newer file is the far worse outcome. (All mutations flow through
|
|
324
|
+
// updateState, which reads — and so sets this flag — before writing.)
|
|
325
|
+
if (this.stateTooNew) {
|
|
326
|
+
logger.warn('[StateManager] Skipping state write: on-disk state was written by a newer build (read-only mode).');
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
this.validator.validateState(state);
|
|
330
|
+
state.lastUpdated = Date.now();
|
|
331
|
+
|
|
332
|
+
let tempFilePath: string | null = null;
|
|
333
|
+
try {
|
|
334
|
+
// Backups are a safety net, not a prerequisite for writing. The atomic
|
|
335
|
+
// temp+fsync+rename below already protects the primary file, so a failed
|
|
336
|
+
// backup (e.g. unwritable backups/ dir) must NOT fail-close every state
|
|
337
|
+
// write — log and continue. This also keeps backup I/O from extending the
|
|
338
|
+
// time the state lock is held on a degraded disk.
|
|
339
|
+
await this.backupManager.createBackup().catch((err: unknown) => {
|
|
340
|
+
logger.warn(`[StateManager] Backup before write failed (continuing): ${err instanceof Error ? err.message : String(err)}`);
|
|
341
|
+
});
|
|
342
|
+
const tempFileName = `research-state-${crypto.randomBytes(16).toString('hex')}.tmp`;
|
|
343
|
+
tempFilePath = path.join(path.dirname(this.stateFilePath), tempFileName);
|
|
344
|
+
const content = JSON.stringify(state, null, 2);
|
|
345
|
+
// Open, write, fsync, close — ensures data is on disk before rename is visible.
|
|
346
|
+
const fh = await fs.open(tempFilePath, 'w', 0o600);
|
|
347
|
+
try {
|
|
348
|
+
await fh.writeFile(content, { encoding: 'utf-8' });
|
|
349
|
+
await fh.sync();
|
|
350
|
+
} finally {
|
|
351
|
+
await fh.close();
|
|
352
|
+
}
|
|
353
|
+
try {
|
|
354
|
+
await fs.rename(tempFilePath, this.stateFilePath);
|
|
355
|
+
} catch (renameErr) {
|
|
356
|
+
// fs.rename fails on Windows if target exists (NTFS). Fall back to copy+delete.
|
|
357
|
+
if (process.platform === 'win32') {
|
|
358
|
+
await fs.copyFile(tempFilePath, this.stateFilePath);
|
|
359
|
+
// copyFile succeeded → the state IS persisted. Make the temp-file removal
|
|
360
|
+
// best-effort: a transient AV lock on the temp would otherwise throw here and
|
|
361
|
+
// fall through to the catch below, reporting "Failed to write state" (status:
|
|
362
|
+
// error) for a write that actually succeeded — a caller that retries/aborts on
|
|
363
|
+
// the false failure would act wrongly.
|
|
364
|
+
const tmp = tempFilePath;
|
|
365
|
+
tempFilePath = null;
|
|
366
|
+
await fs.unlink(tmp).catch((err) => {
|
|
367
|
+
logger.warn('[StateManager] Failed to remove temp file after copy fallback (write succeeded):', err);
|
|
368
|
+
});
|
|
369
|
+
} else {
|
|
370
|
+
throw renameErr;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
tempFilePath = null;
|
|
374
|
+
await this.backupManager.cleanupOldBackups();
|
|
375
|
+
} catch (error: unknown) {
|
|
376
|
+
throw new Error(`Failed to write state: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
377
|
+
} finally {
|
|
378
|
+
if (tempFilePath) {
|
|
379
|
+
await fs.unlink(tempFilePath).catch((err) => {
|
|
380
|
+
logger.warn('[StateManager] Failed to clean up temp file:', err);
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Update state atomically using an updater function
|
|
388
|
+
*/
|
|
389
|
+
public async updateState(updater: (state: SingletonState) => SingletonState | Promise<SingletonState>): Promise<void> {
|
|
390
|
+
await this.ensureDirectories();
|
|
391
|
+
const startTime = Date.now();
|
|
392
|
+
try {
|
|
393
|
+
await this.fileLockService.withLock(async () => {
|
|
394
|
+
const currentState = await this._readState();
|
|
395
|
+
const newState = await updater(currentState);
|
|
396
|
+
await this._writeState(newState);
|
|
397
|
+
});
|
|
398
|
+
const duration = Date.now() - startTime;
|
|
399
|
+
metrics.observe('state_operation_duration_ms', duration, { operation: 'update', status: 'success' });
|
|
400
|
+
metrics.increment('state_operations_total', 1, { operation: 'update', status: 'success' });
|
|
401
|
+
} catch (error) {
|
|
402
|
+
const duration = Date.now() - startTime;
|
|
403
|
+
metrics.observe('state_operation_duration_ms', duration, { operation: 'update', status: 'error' });
|
|
404
|
+
metrics.increment('state_operations_total', 1, { operation: 'update', status: 'error' });
|
|
405
|
+
throw error;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ==================== Session API ====================
|
|
410
|
+
|
|
411
|
+
public async addSession(sessionId: string, param: number | string): Promise<void> {
|
|
412
|
+
await this.sessionApi.addSession(
|
|
413
|
+
sessionId,
|
|
414
|
+
param,
|
|
415
|
+
this.updateState.bind(this),
|
|
416
|
+
process.pid,
|
|
417
|
+
this.processLifecycle.getProcessStartTime.bind(this.processLifecycle)
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
public async removeSession(sessionId: string): Promise<void> {
|
|
422
|
+
await this.sessionApi.removeSession(sessionId, this.updateState.bind(this));
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
public async updateHeartbeat(sessionId: string): Promise<void> {
|
|
426
|
+
await this.sessionApi.updateHeartbeat(sessionId, this.updateState.bind(this));
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
public async cleanupStaleSessions(timeoutMs: number): Promise<number> {
|
|
430
|
+
return this.sessionApi.cleanupStaleSessions(timeoutMs, this.readState.bind(this), this.updateState.bind(this));
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// ==================== Browser API ====================
|
|
434
|
+
|
|
435
|
+
public async getBrowserServer(): Promise<{ port: number; pid: number; schedulerId?: string; startTime?: number | null; authSecret?: string } | null> {
|
|
436
|
+
return this.browserApi.getBrowserServer(this.readState.bind(this));
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
public async setBrowserServer(port: number, pid: number, schedulerId?: string, authSecret?: string): Promise<void> {
|
|
440
|
+
await this.browserApi.setBrowserServer(
|
|
441
|
+
port,
|
|
442
|
+
pid,
|
|
443
|
+
schedulerId,
|
|
444
|
+
this.updateState.bind(this),
|
|
445
|
+
this.processLifecycle.getProcessStartTime.bind(this.processLifecycle),
|
|
446
|
+
authSecret
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
public async clearBrowserServer(): Promise<void> {
|
|
451
|
+
await this.browserApi.clearBrowserServer(this.updateState.bind(this));
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ==================== Embedding Server API ====================
|
|
455
|
+
|
|
456
|
+
public async getEmbeddingServer(): Promise<{ port: number; pid: number; startTime?: number; serverId: string; model?: string } | null> {
|
|
457
|
+
const state = await this.readState();
|
|
458
|
+
return state.embeddingServer ?? null;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
public async clearEmbeddingServer(): Promise<void> {
|
|
462
|
+
await this.updateState((state) => {
|
|
463
|
+
delete state.embeddingServer;
|
|
464
|
+
return state;
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ==================== Process API ====================
|
|
469
|
+
|
|
470
|
+
public async isPidAlive(pid: number, expectedSchedulerId?: string, skipLock?: boolean): Promise<boolean> {
|
|
471
|
+
const state = await this.readState(skipLock);
|
|
472
|
+
const expectedStartTime = state.browserServer?.pid === pid ? state.browserServer.startTime : undefined;
|
|
473
|
+
|
|
474
|
+
return this.processLifecycle.isPidAlive(pid, expectedSchedulerId, {
|
|
475
|
+
getState: (s?: boolean) => this.readState(s ?? skipLock),
|
|
476
|
+
skipLock,
|
|
477
|
+
getSchedulerIdFromState: (state: SingletonState) => state.browserServer?.schedulerId,
|
|
478
|
+
expectedStartTime,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// ==================== GPU Lock API ====================
|
|
483
|
+
|
|
484
|
+
public async acquireGpuLock(sessionId?: string, timeoutMs?: number): Promise<boolean> {
|
|
485
|
+
return this.gpuResourceService.acquireGpuLock(this.updateState.bind(this) as any, sessionId, timeoutMs);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
public async releaseGpuLock(pid?: number): Promise<void> {
|
|
489
|
+
await this.gpuResourceService.releaseGpuLock(this.updateState.bind(this) as any, pid);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
public async getGpuOwner(): Promise<SingletonState['gpuOwner'] | null> {
|
|
493
|
+
const state = await this.readState();
|
|
494
|
+
return state.gpuOwner || null;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ==================== Metrics ====================
|
|
498
|
+
|
|
499
|
+
public async getMetrics(): Promise<StateMetrics> {
|
|
500
|
+
const state = await this.readState();
|
|
501
|
+
return this.metricsCollector.getMetrics(state);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// ==================== Recovery ====================
|
|
505
|
+
|
|
506
|
+
private async recoverFromCorruptionDirect(): Promise<void> {
|
|
507
|
+
try {
|
|
508
|
+
await this.backupManager.recoverFromCorruption();
|
|
509
|
+
} catch (error: unknown) {
|
|
510
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
511
|
+
logger.error(`[StateManager] Failed to recover from corruption: ${message}`);
|
|
512
|
+
throw error;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// ==================== Cleanup ====================
|
|
517
|
+
|
|
518
|
+
public async cleanup(): Promise<void> {
|
|
519
|
+
await this.fileLockService.cleanup();
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// ==================== Public Getters ====================
|
|
523
|
+
|
|
524
|
+
public getStateFilePath(): string {
|
|
525
|
+
return this.stateFilePath;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
public getLockFilePath(): string {
|
|
529
|
+
return this.lockFilePath;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
public getBackupDirPath(): string {
|
|
533
|
+
return this.backupDirPath;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
getFileLockService(): FileLockService {
|
|
537
|
+
return this.fileLockService;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
getProcessLifecycleService(): IProcessLifecycle {
|
|
541
|
+
return this.processLifecycle;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
getGpuResourceService(): GPUResourceService {
|
|
545
|
+
return this.gpuResourceService;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State Metrics
|
|
3
|
+
*
|
|
4
|
+
* Handles metrics collection for the StateManager.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { StateMetrics, SingletonState } from '../types/state-types.ts';
|
|
8
|
+
import { metrics } from '../../utils/metrics.ts';
|
|
9
|
+
import { ServiceLifecycle, type IService } from '../../core/service-registry.ts';
|
|
10
|
+
import { ServiceNames } from '../../core/interfaces/service-names.ts';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Collects metrics from state
|
|
14
|
+
*/
|
|
15
|
+
export class StateMetricsCollector implements IService {
|
|
16
|
+
readonly name = ServiceNames.STATE_METRICS_COLLECTOR;
|
|
17
|
+
lifecycle = ServiceLifecycle.UNINITIALIZED;
|
|
18
|
+
/**
|
|
19
|
+
* Get metrics about the current state
|
|
20
|
+
* @param state The current state
|
|
21
|
+
* @returns StateMetrics object with various statistics
|
|
22
|
+
*/
|
|
23
|
+
getMetrics(state: SingletonState): StateMetrics {
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
|
|
26
|
+
const sessionEntries = Object.entries(state.sessions);
|
|
27
|
+
const totalSessions = sessionEntries.length;
|
|
28
|
+
|
|
29
|
+
let activeSessions = 0;
|
|
30
|
+
let oldestSession: number | null = null;
|
|
31
|
+
let newestSession: number | null = null;
|
|
32
|
+
let lastHeartbeatAge: number | null = null;
|
|
33
|
+
|
|
34
|
+
for (const [, sessionInfo] of sessionEntries) {
|
|
35
|
+
// Check if session is active (lastSeen within last 5 minutes)
|
|
36
|
+
if (now - sessionInfo.lastSeen < 300000) {
|
|
37
|
+
activeSessions++;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Track oldest and newest connection times
|
|
41
|
+
if (oldestSession === null || sessionInfo.connectedAt < oldestSession) {
|
|
42
|
+
oldestSession = sessionInfo.connectedAt;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (newestSession === null || sessionInfo.connectedAt > newestSession) {
|
|
46
|
+
newestSession = sessionInfo.connectedAt;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Track last heartbeat age
|
|
50
|
+
const heartbeatAge = now - sessionInfo.lastSeen;
|
|
51
|
+
if (lastHeartbeatAge === null || heartbeatAge > lastHeartbeatAge) {
|
|
52
|
+
lastHeartbeatAge = heartbeatAge;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Calculate container uptime if container exists
|
|
57
|
+
let containerUptime: number | null = null;
|
|
58
|
+
if (state.containerId !== '') {
|
|
59
|
+
// Use lastUpdated as approximation for container uptime
|
|
60
|
+
containerUptime = now - state.lastUpdated;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Export to metrics system
|
|
64
|
+
metrics.setGauge('state_sessions_total', totalSessions);
|
|
65
|
+
metrics.setGauge('state_sessions_active', activeSessions);
|
|
66
|
+
metrics.setGauge('state_browser_server_exists', state.browserServer ? 1 : 0);
|
|
67
|
+
metrics.setGauge('state_gpu_lock_owner_exists', state.gpuOwner ? 1 : 0);
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
totalSessions,
|
|
71
|
+
activeSessions,
|
|
72
|
+
oldestSession,
|
|
73
|
+
newestSession,
|
|
74
|
+
containerUptime,
|
|
75
|
+
lastHeartbeatAge,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async initialize(): Promise<void> {
|
|
80
|
+
if (this.lifecycle === ServiceLifecycle.INITIALIZED) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
this.lifecycle = ServiceLifecycle.INITIALIZED;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async dispose(): Promise<void> {
|
|
87
|
+
if (this.lifecycle === ServiceLifecycle.DISPOSED) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
this.lifecycle = ServiceLifecycle.DISPOSED;
|
|
91
|
+
}
|
|
92
|
+
}
|