@lincoln504/pi-research 0.1.12 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +64 -115
- package/dist/cli.mjs +23211 -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 +102 -0
- package/docs/ARCHITECTURE.md +306 -80
- package/docs/CONFIGURATION.md +249 -0
- package/docs/KNOWLEDGE-STORE.md +213 -0
- package/docs/PI-EXTENSION.md +91 -0
- package/docs/SDK.md +113 -0
- package/docs/THIRD-PARTY-NOTICES.md +64 -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 +1044 -0
- package/src/config.ts +1036 -147
- 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 +264 -7
- 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 -174
- package/src/infrastructure/searxng-lifecycle.ts +0 -577
- 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,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State Migration
|
|
3
|
+
*
|
|
4
|
+
* Forward migration of persisted state across schema versions, plus detection
|
|
5
|
+
* of state written by a NEWER build than the current process understands.
|
|
6
|
+
*
|
|
7
|
+
* The state file is shared by every co-installed front end (pi extension, SDK,
|
|
8
|
+
* CLI / agent skill). When those builds differ in schema version, an older
|
|
9
|
+
* reader must NOT treat a newer file as corruption and overwrite it — that would
|
|
10
|
+
* destroy the newer build's live sessions, browser-server lease, and authSecret.
|
|
11
|
+
* Instead:
|
|
12
|
+
* - version < CURRENT → migrate forward, stepwise, to CURRENT.
|
|
13
|
+
* - version === CURRENT → pass through unchanged.
|
|
14
|
+
* - version > CURRENT → throw StateVersionTooNewError (caller quarantines and
|
|
15
|
+
* runs degraded rather than clobbering the file).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { CURRENT_STATE_VERSION } from '../types/state-types.ts';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Thrown when the on-disk state was written by a build with a higher schema
|
|
22
|
+
* version than this process supports. Carries the versions for logging.
|
|
23
|
+
*/
|
|
24
|
+
export class StateVersionTooNewError extends Error {
|
|
25
|
+
readonly fileVersion: number;
|
|
26
|
+
readonly currentVersion: number;
|
|
27
|
+
constructor(fileVersion: number, currentVersion: number) {
|
|
28
|
+
super(
|
|
29
|
+
`State file version ${fileVersion} is newer than the supported version ${currentVersion}. ` +
|
|
30
|
+
`It was written by a newer build; this process will not modify it.`,
|
|
31
|
+
);
|
|
32
|
+
this.name = 'StateVersionTooNewError';
|
|
33
|
+
this.fileVersion = fileVersion;
|
|
34
|
+
this.currentVersion = currentVersion;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A single forward migration step. Receives a plain parsed object at version
|
|
40
|
+
* `from` and must return an object at version `from + 1`. Steps run in order
|
|
41
|
+
* until the object reaches CURRENT_STATE_VERSION.
|
|
42
|
+
*
|
|
43
|
+
* Keyed by the source version. When CURRENT_STATE_VERSION is bumped to N, add a
|
|
44
|
+
* MIGRATIONS[N - 1] entry that upgrades an (N-1)-shaped object to N.
|
|
45
|
+
*/
|
|
46
|
+
type MigrationStep = (raw: Record<string, unknown>) => Record<string, unknown>;
|
|
47
|
+
|
|
48
|
+
const MIGRATIONS: Record<number, MigrationStep> = {
|
|
49
|
+
// No migrations yet — CURRENT_STATE_VERSION is 1, the initial schema.
|
|
50
|
+
// Example for a future bump to 2:
|
|
51
|
+
// 1: (raw) => ({ ...raw, version: 2, /* fill new required fields */ }),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Read a numeric `version` from a parsed object, or null if it is absent or not
|
|
56
|
+
* a finite number (in which case the object is malformed and the validator will
|
|
57
|
+
* reject it as corruption).
|
|
58
|
+
*/
|
|
59
|
+
function readVersion(raw: unknown): number | null {
|
|
60
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
61
|
+
const v = (raw as Record<string, unknown>)['version'];
|
|
62
|
+
return typeof v === 'number' && Number.isFinite(v) ? v : null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Bring a parsed state object up to CURRENT_STATE_VERSION.
|
|
67
|
+
*
|
|
68
|
+
* - Objects without a usable numeric version are returned unchanged so the
|
|
69
|
+
* schema validator can reject them as corruption (the existing recovery path).
|
|
70
|
+
* - Future versions throw StateVersionTooNewError.
|
|
71
|
+
* - Older versions are migrated forward step by step; a missing step for some
|
|
72
|
+
* intermediate version is an unrecoverable gap and throws.
|
|
73
|
+
*/
|
|
74
|
+
export function migrateRawState(raw: unknown): unknown {
|
|
75
|
+
const version = readVersion(raw);
|
|
76
|
+
if (version === null) return raw;
|
|
77
|
+
|
|
78
|
+
if (version > CURRENT_STATE_VERSION) {
|
|
79
|
+
throw new StateVersionTooNewError(version, CURRENT_STATE_VERSION);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (version === CURRENT_STATE_VERSION) return raw;
|
|
83
|
+
|
|
84
|
+
let current = raw as Record<string, unknown>;
|
|
85
|
+
let v = version;
|
|
86
|
+
while (v < CURRENT_STATE_VERSION) {
|
|
87
|
+
const step = MIGRATIONS[v];
|
|
88
|
+
if (!step) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`No migration registered from state version ${v} to ${v + 1}; cannot upgrade.`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
current = step(current);
|
|
94
|
+
const next = readVersion(current);
|
|
95
|
+
if (next !== v + 1) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`Migration from state version ${v} produced version ${String(next)}, expected ${v + 1}.`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
v = next;
|
|
101
|
+
}
|
|
102
|
+
return current;
|
|
103
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State Path Configuration Service
|
|
3
|
+
*
|
|
4
|
+
* Computes and provides file paths for state management components.
|
|
5
|
+
* This service is registered first so that FileLockService and StateBackupManager
|
|
6
|
+
* can depend on it for their path configuration.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as path from 'node:path';
|
|
10
|
+
import { getGlobalConfigDir } from '../../config.ts';
|
|
11
|
+
import type { IService } from '../../core/service-registry.ts';
|
|
12
|
+
import { ServiceLifecycle } from '../../core/service-registry.ts';
|
|
13
|
+
import { ServiceNames } from '../../core/interfaces/service-names.ts';
|
|
14
|
+
|
|
15
|
+
export interface StatePaths {
|
|
16
|
+
stateFilePath: string;
|
|
17
|
+
lockDirPath: string;
|
|
18
|
+
backupDirPath: string;
|
|
19
|
+
lockFilePath: string;
|
|
20
|
+
projectSettingsPath: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* State Path Configuration Service
|
|
25
|
+
*
|
|
26
|
+
* Provides centralized path configuration for state management.
|
|
27
|
+
*/
|
|
28
|
+
export class StatePathConfiguration implements IService {
|
|
29
|
+
readonly name = ServiceNames.STATE_PATH_CONFIGURATION;
|
|
30
|
+
lifecycle = ServiceLifecycle.UNINITIALIZED;
|
|
31
|
+
private _initialized = false;
|
|
32
|
+
|
|
33
|
+
private readonly stateDir: string;
|
|
34
|
+
private readonly stateFilePath: string;
|
|
35
|
+
private readonly lockDirPath: string;
|
|
36
|
+
private readonly backupDirPath: string;
|
|
37
|
+
private readonly lockFilePath: string;
|
|
38
|
+
private readonly projectSettingsPath: string;
|
|
39
|
+
|
|
40
|
+
constructor(stateDir?: string) {
|
|
41
|
+
// pi-research's own namespace (~/.pi/research/state), beside config.env and
|
|
42
|
+
// knowledge_db — not the host pi config root (~/.pi/state).
|
|
43
|
+
const resolvedStateDir = stateDir || path.join(getGlobalConfigDir(), 'state');
|
|
44
|
+
this.stateDir = resolvedStateDir;
|
|
45
|
+
this.stateFilePath = path.join(resolvedStateDir, 'research-state.json');
|
|
46
|
+
this.lockDirPath = path.join(resolvedStateDir, '.locks');
|
|
47
|
+
this.backupDirPath = path.join(resolvedStateDir, 'backups');
|
|
48
|
+
this.lockFilePath = path.join(this.lockDirPath, 'research-state.lock');
|
|
49
|
+
this.projectSettingsPath = path.join(resolvedStateDir, 'project-settings.json');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async initialize(): Promise<void> {
|
|
53
|
+
if (this._initialized) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
this._initialized = true;
|
|
57
|
+
this.lifecycle = ServiceLifecycle.INITIALIZED;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async dispose(): Promise<void> {
|
|
61
|
+
// Nothing to dispose
|
|
62
|
+
this.lifecycle = ServiceLifecycle.DISPOSED;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get all state paths
|
|
67
|
+
*/
|
|
68
|
+
getPaths(): StatePaths {
|
|
69
|
+
return {
|
|
70
|
+
stateFilePath: this.stateFilePath,
|
|
71
|
+
lockDirPath: this.lockDirPath,
|
|
72
|
+
backupDirPath: this.backupDirPath,
|
|
73
|
+
lockFilePath: this.lockFilePath,
|
|
74
|
+
projectSettingsPath: this.projectSettingsPath,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get the state directory
|
|
80
|
+
*/
|
|
81
|
+
getStateDir(): string {
|
|
82
|
+
return this.stateDir;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get the state file path
|
|
87
|
+
*/
|
|
88
|
+
getStateFilePath(): string {
|
|
89
|
+
return this.stateFilePath;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get the lock directory path
|
|
94
|
+
*/
|
|
95
|
+
getLockDirPath(): string {
|
|
96
|
+
return this.lockDirPath;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get the backup directory path
|
|
101
|
+
*/
|
|
102
|
+
getBackupDirPath(): string {
|
|
103
|
+
return this.backupDirPath;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get the lock file path
|
|
108
|
+
*/
|
|
109
|
+
getLockFilePath(): string {
|
|
110
|
+
return this.lockFilePath;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get the project settings file path
|
|
115
|
+
*/
|
|
116
|
+
getProjectSettingsPath(): string {
|
|
117
|
+
return this.projectSettingsPath;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State Session API
|
|
3
|
+
*
|
|
4
|
+
* Session management API that delegates to StateSessionManager.
|
|
5
|
+
* This separates the public API from the internal session manager.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { SingletonState } from '../types/state-types.ts';
|
|
9
|
+
import type { StateSessionManager } from './state-session-manager.ts';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Provides session management operations on top of StateSessionManager
|
|
13
|
+
*/
|
|
14
|
+
export class StateSessionApi {
|
|
15
|
+
constructor(private readonly sessionManager: StateSessionManager) {}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Add a new session to the state
|
|
19
|
+
* @param sessionId The session ID to add
|
|
20
|
+
* @param param PID (number) or container name (string)
|
|
21
|
+
* @param updateState Function to update state atomically
|
|
22
|
+
* @param processPid PID of the current process
|
|
23
|
+
* @param getStartTime Function to get process start time
|
|
24
|
+
* @throws Error if unable to update state
|
|
25
|
+
*/
|
|
26
|
+
async addSession(
|
|
27
|
+
sessionId: string,
|
|
28
|
+
param: number | string,
|
|
29
|
+
updateState: (updater: (state: SingletonState) => SingletonState | Promise<SingletonState>) => Promise<void>,
|
|
30
|
+
processPid: number,
|
|
31
|
+
getStartTime: (pid: number) => Promise<number | null>
|
|
32
|
+
): Promise<void> {
|
|
33
|
+
const pid = typeof param === 'number' ? param : processPid;
|
|
34
|
+
const startTime = await getStartTime(pid);
|
|
35
|
+
await updateState(async (state): Promise<SingletonState> => {
|
|
36
|
+
return this.sessionManager.addSession(state, sessionId, pid, startTime);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Remove a session from the state
|
|
42
|
+
* @param sessionId The session ID to remove
|
|
43
|
+
* @param updateState Function to update state atomically
|
|
44
|
+
*/
|
|
45
|
+
async removeSession(
|
|
46
|
+
sessionId: string,
|
|
47
|
+
updateState: (updater: (state: SingletonState) => SingletonState | Promise<SingletonState>) => Promise<void>
|
|
48
|
+
): Promise<void> {
|
|
49
|
+
await updateState((state): SingletonState => {
|
|
50
|
+
return this.sessionManager.removeSession(state, sessionId);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Update the heartbeat timestamp for a session
|
|
56
|
+
* @param sessionId The session ID to update
|
|
57
|
+
* @param updateState Function to update state atomically
|
|
58
|
+
*/
|
|
59
|
+
async updateHeartbeat(
|
|
60
|
+
sessionId: string,
|
|
61
|
+
updateState: (updater: (state: SingletonState) => SingletonState | Promise<SingletonState>) => Promise<void>
|
|
62
|
+
): Promise<void> {
|
|
63
|
+
await updateState((state): SingletonState => {
|
|
64
|
+
return this.sessionManager.updateHeartbeat(state, sessionId);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Clean up stale sessions based on timeout and process liveness
|
|
70
|
+
* @param timeoutMs Timeout in milliseconds for session staleness
|
|
71
|
+
* @param readState Function to read state
|
|
72
|
+
* @param updateState Function to update state atomically
|
|
73
|
+
* @returns Number of sessions removed
|
|
74
|
+
*/
|
|
75
|
+
async cleanupStaleSessions(
|
|
76
|
+
timeoutMs: number,
|
|
77
|
+
readState: () => Promise<SingletonState>,
|
|
78
|
+
updateState: (updater: (state: SingletonState) => SingletonState | Promise<SingletonState>) => Promise<void>
|
|
79
|
+
): Promise<number> {
|
|
80
|
+
const state = await readState();
|
|
81
|
+
const sessionsToRemove = await this.sessionManager.classifyStaleSessions(state, timeoutMs);
|
|
82
|
+
|
|
83
|
+
// Remove stale sessions under a fresh lock. Re-validate lastSeen so a
|
|
84
|
+
// session that received a heartbeat between the two lock acquisitions is
|
|
85
|
+
// not accidentally deleted (TOCTOU guard).
|
|
86
|
+
if (sessionsToRemove.size > 0) {
|
|
87
|
+
await updateState((state): SingletonState => {
|
|
88
|
+
return this.sessionManager.removeStaleSessions(state, sessionsToRemove);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return sessionsToRemove.size;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State Session Manager
|
|
3
|
+
*
|
|
4
|
+
* Handles session lifecycle and cleanup operations for the StateManager.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ProcessLifecycleService } from '../process-lifecycle-service.ts';
|
|
8
|
+
import type { SingletonState } from '../types/state-types.ts';
|
|
9
|
+
import { ServiceLifecycle, type IService } from '../../core/service-registry.ts';
|
|
10
|
+
import { ServiceNames } from '../../core/interfaces/service-names.ts';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Manages research sessions within the state
|
|
14
|
+
*/
|
|
15
|
+
export class StateSessionManager implements IService {
|
|
16
|
+
readonly name = ServiceNames.STATE_SESSION_MANAGER;
|
|
17
|
+
|
|
18
|
+
lifecycle = ServiceLifecycle.UNINITIALIZED;
|
|
19
|
+
constructor(private readonly processLifecycle: ProcessLifecycleService) {}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Add a new session to the state
|
|
23
|
+
* @param state The current state
|
|
24
|
+
* @param sessionId The session ID to add
|
|
25
|
+
* @param pid The process ID
|
|
26
|
+
* @param startTime Optional process start time (Linux)
|
|
27
|
+
* @returns Updated state with new session
|
|
28
|
+
*/
|
|
29
|
+
addSession(state: SingletonState, sessionId: string, pid: number, startTime?: number | null): SingletonState {
|
|
30
|
+
state.sessions[sessionId] = {
|
|
31
|
+
pid,
|
|
32
|
+
startTime: startTime ?? undefined,
|
|
33
|
+
lastSeen: Date.now(),
|
|
34
|
+
connectedAt: Date.now(),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return state;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Remove a session from the state
|
|
42
|
+
* @param state The current state
|
|
43
|
+
* @param sessionId The session ID to remove
|
|
44
|
+
* @returns Updated state without the session
|
|
45
|
+
*/
|
|
46
|
+
removeSession(state: SingletonState, sessionId: string): SingletonState {
|
|
47
|
+
if (state.sessions[sessionId] !== undefined) {
|
|
48
|
+
delete state.sessions[sessionId];
|
|
49
|
+
}
|
|
50
|
+
return state;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Update the heartbeat timestamp for a session
|
|
55
|
+
* @param state The current state
|
|
56
|
+
* @param sessionId The session ID to update
|
|
57
|
+
* @returns Updated state with updated heartbeat
|
|
58
|
+
*/
|
|
59
|
+
updateHeartbeat(state: SingletonState, sessionId: string): SingletonState {
|
|
60
|
+
const session = state.sessions[sessionId];
|
|
61
|
+
if (session !== undefined) {
|
|
62
|
+
session.lastSeen = Date.now();
|
|
63
|
+
}
|
|
64
|
+
return state;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Clean up stale sessions based on timeout and process liveness
|
|
69
|
+
* @param state The current state
|
|
70
|
+
* @param timeoutMs Timeout in milliseconds for session staleness
|
|
71
|
+
* @returns Number of sessions to remove and their lastSeen timestamps
|
|
72
|
+
*/
|
|
73
|
+
async classifyStaleSessions(state: SingletonState, timeoutMs: number): Promise<Map<string, number>> {
|
|
74
|
+
const now = Date.now();
|
|
75
|
+
const sessionsToRemove = new Map<string, number>();
|
|
76
|
+
|
|
77
|
+
for (const [sessionId, sessionInfo] of Object.entries(state.sessions)) {
|
|
78
|
+
// Primary check: lastSeen timeout
|
|
79
|
+
const lastSeenAge = now - sessionInfo.lastSeen;
|
|
80
|
+
|
|
81
|
+
if (lastSeenAge > timeoutMs) {
|
|
82
|
+
sessionsToRemove.set(sessionId, sessionInfo.lastSeen);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Secondary check: is process still alive?
|
|
87
|
+
// Passing startTime prevents PID reuse races on Linux.
|
|
88
|
+
const isAlive = await this.processLifecycle.isProcessAlive(sessionInfo.pid, sessionInfo.startTime);
|
|
89
|
+
|
|
90
|
+
if (!isAlive) {
|
|
91
|
+
sessionsToRemove.set(sessionId, sessionInfo.lastSeen);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return sessionsToRemove;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Remove stale sessions from state
|
|
100
|
+
* @param state The current state
|
|
101
|
+
* @param sessionsToRemove Map of session IDs to their lastSeen timestamps
|
|
102
|
+
* @returns Updated state with stale sessions removed
|
|
103
|
+
*/
|
|
104
|
+
removeStaleSessions(state: SingletonState, sessionsToRemove: Map<string, number>): SingletonState {
|
|
105
|
+
for (const [sessionId, lastSeenAtClassify] of sessionsToRemove) {
|
|
106
|
+
const current = state.sessions[sessionId];
|
|
107
|
+
if (current && current.lastSeen === lastSeenAtClassify) {
|
|
108
|
+
delete state.sessions[sessionId];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return state;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async initialize(): Promise<void> {
|
|
115
|
+
if (this.lifecycle === ServiceLifecycle.INITIALIZED) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
this.lifecycle = ServiceLifecycle.INITIALIZED;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async dispose(): Promise<void> {
|
|
122
|
+
if (this.lifecycle === ServiceLifecycle.DISPOSED) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
this.lifecycle = ServiceLifecycle.DISPOSED;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State Validator
|
|
3
|
+
*
|
|
4
|
+
* Validates state structure and values.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { SingletonState } from '../types/state-types.ts';
|
|
8
|
+
import { SingletonStateSchema } from '../types/state-types.ts';
|
|
9
|
+
import { migrateRawState } from './state-migration.ts';
|
|
10
|
+
import { Value } from 'typebox/value';
|
|
11
|
+
import { ServiceLifecycle, type IService } from '../../core/service-registry.ts';
|
|
12
|
+
import { ServiceNames } from '../../core/interfaces/service-names.ts';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Validates state objects using TypeBox schemas
|
|
16
|
+
*/
|
|
17
|
+
export class StateValidator implements IService {
|
|
18
|
+
readonly name = ServiceNames.STATE_VALIDATOR;
|
|
19
|
+
lifecycle = ServiceLifecycle.UNINITIALIZED;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Validate the structure and version of a state object
|
|
23
|
+
* @param state The state object to validate
|
|
24
|
+
* @returns The validated and potentially coerced SingletonState
|
|
25
|
+
* @throws Error if state structure or version is invalid
|
|
26
|
+
*/
|
|
27
|
+
validateState(state: unknown): SingletonState {
|
|
28
|
+
if (!state || typeof state !== 'object') {
|
|
29
|
+
throw new Error('Invalid state: not an object');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Coerce values if possible
|
|
33
|
+
const coerced = Value.Convert(SingletonStateSchema, state);
|
|
34
|
+
|
|
35
|
+
// Validate against schema
|
|
36
|
+
if (!Value.Check(SingletonStateSchema, coerced)) {
|
|
37
|
+
// Use coerced state for error reporting to get better paths
|
|
38
|
+
const errors = [...Value.Errors(SingletonStateSchema, coerced)];
|
|
39
|
+
const errorMsg = errors.map((e: any) => {
|
|
40
|
+
const path = String(e.path || 'root');
|
|
41
|
+
return `${path}: ${e.message} (${JSON.stringify(e.value)})`;
|
|
42
|
+
}).join(', ');
|
|
43
|
+
throw new Error(`Invalid state: ${errorMsg}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Additional value range checks not easily covered by basic schemas
|
|
47
|
+
if (coerced.port < 0 || coerced.port > 65535) {
|
|
48
|
+
throw new Error(`Invalid state: port must be 0-65535, got ${coerced.port}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (coerced.browserServer && (coerced.browserServer.port < 0 || coerced.browserServer.port > 65535)) {
|
|
52
|
+
throw new Error(`Invalid state: browserServer.port must be 0-65535, got ${coerced.browserServer.port}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// embeddingServer.port allows the sentinel -1 ("a process is initializing"),
|
|
56
|
+
// otherwise must be a valid TCP port. This rejects garbage (e.g. -5, 99999)
|
|
57
|
+
// that would otherwise produce confusing socket errors in EmbeddingClient.
|
|
58
|
+
if (coerced.embeddingServer) {
|
|
59
|
+
const ep = coerced.embeddingServer.port;
|
|
60
|
+
if (ep < -1 || ep > 65535) {
|
|
61
|
+
throw new Error(`Invalid state: embeddingServer.port must be -1 or 0-65535, got ${ep}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return coerced;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Migrate a freshly-parsed state object up to the current schema version, then
|
|
70
|
+
* validate it. Use this for state read from disk (where the version may be
|
|
71
|
+
* older, or newer than this process understands). Throws
|
|
72
|
+
* StateVersionTooNewError for future versions so the caller can quarantine
|
|
73
|
+
* rather than overwrite; throws "Invalid state: ..." for everything the schema
|
|
74
|
+
* rejects, matching the corruption-recovery contract in _readState.
|
|
75
|
+
*/
|
|
76
|
+
migrateAndValidate(raw: unknown): SingletonState {
|
|
77
|
+
return this.validateState(migrateRawState(raw));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async initialize(): Promise<void> {
|
|
81
|
+
if (this.lifecycle === ServiceLifecycle.INITIALIZED) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
this.lifecycle = ServiceLifecycle.INITIALIZED;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async dispose(): Promise<void> {
|
|
88
|
+
if (this.lifecycle === ServiceLifecycle.DISPOSED) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
this.lifecycle = ServiceLifecycle.DISPOSED;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State Types
|
|
3
|
+
*
|
|
4
|
+
* Shared types for state management.
|
|
5
|
+
* Extracted to separate file to avoid circular dependencies.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Type, type Static } from 'typebox';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Current on-disk state schema version. Bump this (and add a migration step in
|
|
12
|
+
* state-migration.ts) whenever the persisted shape changes incompatibly. The
|
|
13
|
+
* schema's `version` literal is pinned to this value so a reader only accepts a
|
|
14
|
+
* state it actually understands; older/newer files are routed through migration
|
|
15
|
+
* or future-version quarantine rather than being silently overwritten.
|
|
16
|
+
*/
|
|
17
|
+
export const CURRENT_STATE_VERSION = 1 as const;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* State metrics interface
|
|
21
|
+
*/
|
|
22
|
+
export interface StateMetrics {
|
|
23
|
+
totalSessions: number;
|
|
24
|
+
activeSessions: number;
|
|
25
|
+
oldestSession: number | null;
|
|
26
|
+
newestSession: number | null;
|
|
27
|
+
containerUptime: number | null;
|
|
28
|
+
lastHeartbeatAge: number | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Session information interface
|
|
33
|
+
*/
|
|
34
|
+
export const SessionInfoSchema = Type.Object({
|
|
35
|
+
pid: Type.Number(),
|
|
36
|
+
startTime: Type.Optional(Type.Number()),
|
|
37
|
+
lastSeen: Type.Number(),
|
|
38
|
+
connectedAt: Type.Number(),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Main state structure interface
|
|
43
|
+
*/
|
|
44
|
+
export const SingletonStateSchema = Type.Object({
|
|
45
|
+
version: Type.Literal(CURRENT_STATE_VERSION),
|
|
46
|
+
containerId: Type.String(),
|
|
47
|
+
containerName: Type.String(),
|
|
48
|
+
port: Type.Number(),
|
|
49
|
+
sessions: Type.Record(Type.String(), SessionInfoSchema),
|
|
50
|
+
lastUpdated: Type.Number(),
|
|
51
|
+
browserServer: Type.Optional(Type.Object({
|
|
52
|
+
port: Type.Number(),
|
|
53
|
+
pid: Type.Number(),
|
|
54
|
+
startTime: Type.Optional(Type.Number()),
|
|
55
|
+
schedulerId: Type.Optional(Type.String()),
|
|
56
|
+
authSecret: Type.Optional(Type.String()),
|
|
57
|
+
})),
|
|
58
|
+
schedulerVersion: Type.Optional(Type.String()),
|
|
59
|
+
gpuOwner: Type.Optional(Type.Object({
|
|
60
|
+
pid: Type.Number(),
|
|
61
|
+
startTime: Type.Optional(Type.Number()),
|
|
62
|
+
startedAt: Type.Number(),
|
|
63
|
+
sessionId: Type.Optional(Type.String()),
|
|
64
|
+
// Re-entrant hold count for the owning process. The lock is shared by N
|
|
65
|
+
// concurrent in-process embedding calls (activeEmbeddings is designed > 1);
|
|
66
|
+
// each re-entrant acquire increments and each release decrements, so the
|
|
67
|
+
// cross-process owner record is only cleared when the last in-process hold
|
|
68
|
+
// is released. Absent on state written before refcounting — treated as 1.
|
|
69
|
+
holds: Type.Optional(Type.Number()),
|
|
70
|
+
})),
|
|
71
|
+
embeddingServer: Type.Optional(Type.Object({
|
|
72
|
+
port: Type.Number(),
|
|
73
|
+
pid: Type.Number(),
|
|
74
|
+
startTime: Type.Optional(Type.Number()),
|
|
75
|
+
serverId: Type.String(),
|
|
76
|
+
// EMBEDDING_MODEL the leader serves. A follower configured with a DIFFERENT
|
|
77
|
+
// model must not adopt this leader (same-dimension models would silently
|
|
78
|
+
// cross-embed and corrupt the vector space) — it falls back to a local
|
|
79
|
+
// in-process embedder instead. Optional for entries written by older
|
|
80
|
+
// processes, which are tolerated as matching.
|
|
81
|
+
model: Type.Optional(Type.String()),
|
|
82
|
+
})),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
export type SingletonState = Static<typeof SingletonStateSchema>;
|