@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.
Files changed (248) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +64 -115
  3. package/dist/cli.mjs +23211 -0
  4. package/dist/prompts/research-knowledge-relevance-triage.md +35 -0
  5. package/dist/prompts/research-knowledge-search-extractor.md +41 -0
  6. package/dist/prompts/research-tool-usage.md +92 -0
  7. package/dist/prompts/researcher.md +89 -0
  8. package/dist/prompts/system-coordinator.md +75 -0
  9. package/dist/prompts/system-lead-evaluator.md +118 -0
  10. package/dist/thread-worker.mjs +2611 -0
  11. package/dist/webgpu-probe.mjs +80 -0
  12. package/docs/AGENT-SKILL.md +102 -0
  13. package/docs/ARCHITECTURE.md +306 -80
  14. package/docs/CONFIGURATION.md +249 -0
  15. package/docs/KNOWLEDGE-STORE.md +213 -0
  16. package/docs/PI-EXTENSION.md +91 -0
  17. package/docs/SDK.md +113 -0
  18. package/docs/THIRD-PARTY-NOTICES.md +64 -0
  19. package/package.json +102 -36
  20. package/scripts/build.cjs +140 -0
  21. package/scripts/cleanup.cjs +149 -0
  22. package/scripts/prepare.cjs +28 -0
  23. package/scripts/setup.cjs +134 -0
  24. package/skills/pi-research/README.md +90 -0
  25. package/skills/pi-research/SKILL.md +102 -0
  26. package/skills/pi-research/references/configuration.md +77 -0
  27. package/skills/pi-research/scripts/run.mjs +237 -0
  28. package/src/cleanup/research-cleanup.ts +140 -0
  29. package/src/cli.ts +1044 -0
  30. package/src/config.ts +1036 -147
  31. package/src/constants.ts +115 -84
  32. package/src/core/health-check-service.ts +265 -0
  33. package/src/core/interfaces/health-check-interfaces.ts +50 -0
  34. package/src/core/interfaces/knowledge-interfaces.ts +190 -0
  35. package/src/core/interfaces/observer-interfaces.ts +67 -0
  36. package/src/core/interfaces/orchestration-interfaces.ts +86 -0
  37. package/src/core/interfaces/planning-interfaces.ts +100 -0
  38. package/src/core/interfaces/process-interfaces.ts +37 -0
  39. package/src/core/interfaces/research-plan-types.ts +52 -0
  40. package/src/core/interfaces/scheduler-interfaces.ts +91 -0
  41. package/src/core/interfaces/service-names.ts +39 -0
  42. package/src/core/interfaces/state-manager-interfaces.ts +29 -0
  43. package/src/core/interfaces/steering-interfaces.ts +31 -0
  44. package/src/core/llm/agentic-repair.ts +169 -0
  45. package/src/core/llm/inject-date.ts +52 -0
  46. package/src/core/llm/llm-timeout.ts +64 -0
  47. package/src/core/llm/llm-utils.ts +86 -0
  48. package/src/core/llm/model-registry-factory.ts +287 -0
  49. package/src/core/llm/pi-ai-completion.ts +29 -0
  50. package/src/core/llm/prompts.ts +50 -0
  51. package/src/core/llm/research-model-resolver.ts +91 -0
  52. package/src/core/planning-constants.ts +13 -0
  53. package/src/core/planning-service.ts +649 -0
  54. package/src/core/planning-utils.ts +299 -0
  55. package/src/core/scheduler-factory.ts +48 -0
  56. package/src/core/scheduler-service.ts +123 -0
  57. package/src/core/service-initialization.ts +194 -0
  58. package/src/core/service-interfaces.ts +48 -0
  59. package/src/core/service-registry.ts +659 -0
  60. package/src/healthcheck/index.ts +271 -265
  61. package/src/healthcheck/registry.ts +130 -0
  62. package/src/index.ts +639 -50
  63. package/src/infrastructure/browser/browser-cleanup.ts +309 -0
  64. package/src/infrastructure/browser/browser-client.ts +199 -0
  65. package/src/infrastructure/browser/browser-error-utils.ts +160 -0
  66. package/src/infrastructure/browser/browser-lifecycle.ts +120 -0
  67. package/src/infrastructure/browser/browser-server.ts +174 -0
  68. package/src/infrastructure/browser/browser-task-scheduler.ts +461 -0
  69. package/src/infrastructure/browser/cleanup-utils.ts +155 -0
  70. package/src/infrastructure/browser/client-agent.ts +31 -0
  71. package/src/infrastructure/browser/config.ts +288 -0
  72. package/src/infrastructure/browser/ensure-browser.ts +205 -0
  73. package/src/infrastructure/browser/index.ts +37 -0
  74. package/src/infrastructure/browser/priority-task-queue.ts +226 -0
  75. package/src/infrastructure/browser/scheduler-factory.ts +354 -0
  76. package/src/infrastructure/browser/task-execution-service.ts +209 -0
  77. package/src/infrastructure/browser/thread-worker-browser.ts +282 -0
  78. package/src/infrastructure/browser/thread-worker-lifecycle.ts +323 -0
  79. package/src/infrastructure/browser/thread-worker-messaging.ts +662 -0
  80. package/src/infrastructure/browser/thread-worker-types.ts +25 -0
  81. package/src/infrastructure/browser/thread-worker.mjs +2611 -0
  82. package/src/infrastructure/browser/thread-worker.ts +168 -0
  83. package/src/infrastructure/browser/worker-pool-manager.ts +449 -0
  84. package/src/infrastructure/embedding/embedding-client.ts +205 -0
  85. package/src/infrastructure/embedding/embedding-factory.ts +324 -0
  86. package/src/infrastructure/embedding/embedding-server.ts +535 -0
  87. package/src/infrastructure/file-lock-service.ts +592 -0
  88. package/src/infrastructure/gpu-resource-service.ts +191 -0
  89. package/src/infrastructure/knowledge-store-service.ts +475 -0
  90. package/src/infrastructure/metrics-service.ts +129 -0
  91. package/src/infrastructure/process-lifecycle-service.ts +253 -0
  92. package/src/infrastructure/scheduler-factory-service.ts +58 -0
  93. package/src/infrastructure/service-initialization.ts +292 -0
  94. package/src/infrastructure/state/state-backup-manager.ts +290 -0
  95. package/src/infrastructure/state/state-browser-api.ts +62 -0
  96. package/src/infrastructure/state/state-browser-manager.ts +70 -0
  97. package/src/infrastructure/state/state-manager-service.ts +264 -0
  98. package/src/infrastructure/state/state-manager.ts +547 -0
  99. package/src/infrastructure/state/state-metrics.ts +92 -0
  100. package/src/infrastructure/state/state-migration.ts +103 -0
  101. package/src/infrastructure/state/state-path-configuration.ts +119 -0
  102. package/src/infrastructure/state/state-session-api.ts +94 -0
  103. package/src/infrastructure/state/state-session-manager.ts +127 -0
  104. package/src/infrastructure/state/state-validator.ts +93 -0
  105. package/src/infrastructure/types/state-types.ts +85 -0
  106. package/src/knowledge/chunker.ts +157 -0
  107. package/src/knowledge/embedder-init.ts +413 -0
  108. package/src/knowledge/embedder-utils.ts +296 -0
  109. package/src/knowledge/embedder.ts +592 -0
  110. package/src/knowledge/index.ts +126 -0
  111. package/src/knowledge/lancedb-loader.ts +35 -0
  112. package/src/knowledge/migration.ts +17 -0
  113. package/src/knowledge/model-config.ts +132 -0
  114. package/src/knowledge/onnx-env.ts +48 -0
  115. package/src/knowledge/store-operations.ts +291 -0
  116. package/src/knowledge/store-schema.ts +79 -0
  117. package/src/knowledge/store.ts +1251 -0
  118. package/src/knowledge/transformers-loader.ts +31 -0
  119. package/src/knowledge/webgpu-probe.mjs +80 -0
  120. package/src/knowledge/webgpu-viability.ts +255 -0
  121. package/src/knowledge/writer-queue.ts +252 -0
  122. package/src/logger.ts +316 -122
  123. package/src/observers/research-observer-impl.ts +459 -0
  124. package/src/orchestration/deep-research-orchestrator.ts +524 -773
  125. package/src/orchestration/deep-research-types.ts +47 -35
  126. package/src/orchestration/headless-observer.ts +114 -0
  127. package/src/orchestration/orchestration-types.ts +33 -0
  128. package/src/orchestration/quick-research-orchestrator.ts +463 -0
  129. package/src/orchestration/research-orchestration-service.ts +549 -0
  130. package/src/orchestration/research-session-service.ts +178 -0
  131. package/src/orchestration/research-synthesis-service.ts +348 -0
  132. package/src/orchestration/researcher-executor.ts +394 -0
  133. package/src/orchestration/researcher.ts +115 -42
  134. package/src/orchestration/service-initialization.ts +109 -0
  135. package/src/orchestration/session-context.ts +7 -3
  136. package/src/orchestration/session-state.ts +672 -0
  137. package/src/prompts/research-knowledge-relevance-triage.md +35 -0
  138. package/src/prompts/research-knowledge-search-extractor.md +41 -0
  139. package/src/prompts/research-tool-usage.md +63 -16
  140. package/src/prompts/researcher.md +59 -136
  141. package/src/prompts/system-coordinator.md +64 -13
  142. package/src/prompts/system-lead-evaluator.md +82 -189
  143. package/src/research-config.ts +872 -0
  144. package/src/sdk.ts +847 -0
  145. package/src/security/cisa-kev.ts +72 -9
  146. package/src/security/cvss.ts +92 -0
  147. package/src/security/ecosystem.ts +45 -0
  148. package/src/security/github-advisories.ts +168 -428
  149. package/src/security/github-advisory-types.ts +282 -0
  150. package/src/security/index.ts +63 -177
  151. package/src/security/nvd-client.ts +550 -0
  152. package/src/security/nvd-types.ts +179 -0
  153. package/src/security/osv-client.ts +246 -0
  154. package/src/security/osv-types.ts +285 -0
  155. package/src/security/types.ts +67 -53
  156. package/src/skill-install/skill-installer.ts +519 -0
  157. package/src/stackexchange/index.ts +81 -27
  158. package/src/stackexchange/output/compact.ts +7 -6
  159. package/src/stackexchange/output/table.ts +11 -7
  160. package/src/stackexchange/queries.ts +0 -10
  161. package/src/stackexchange/rest-client.ts +141 -66
  162. package/src/stackexchange/types.ts +0 -17
  163. package/src/tool.ts +15 -423
  164. package/src/tools/grep.ts +32 -310
  165. package/src/tools/health-tool-definition.ts +101 -0
  166. package/src/tools/index.ts +38 -9
  167. package/src/tools/research-knowledge-search.ts +974 -0
  168. package/src/tools/research-knowledge-types.ts +77 -0
  169. package/src/tools/research-tool-definition.ts +494 -0
  170. package/src/tools/scrape.ts +286 -388
  171. package/src/tools/search.ts +98 -153
  172. package/src/tools/security.ts +107 -80
  173. package/src/tools/stackexchange.ts +58 -35
  174. package/src/tools/youtube-transcript.ts +163 -0
  175. package/src/tui/knowledge-search-panel.ts +86 -0
  176. package/src/tui/pulse.ts +86 -0
  177. package/src/tui/research-health.ts +124 -0
  178. package/src/tui/research-panel-color-utils.ts +340 -0
  179. package/src/tui/research-panel-format.ts +54 -0
  180. package/src/tui/research-panel-state.ts +404 -0
  181. package/src/tui/research-panel-wave.ts +280 -0
  182. package/src/tui/research-panel.ts +339 -435
  183. package/src/tui/research-tui-manager.ts +233 -0
  184. package/src/tui/tui-controller.ts +137 -0
  185. package/src/types/extension-context.ts +18 -49
  186. package/src/types/index.ts +61 -0
  187. package/src/types/llm.ts +68 -29
  188. package/src/types/research-panel-types.ts +61 -0
  189. package/src/utils/circuit-breaker.ts +146 -0
  190. package/src/utils/citation-utils.ts +97 -0
  191. package/src/utils/disk-space-checker.ts +55 -0
  192. package/src/utils/error-tracker.ts +265 -0
  193. package/src/utils/host-config.ts +45 -0
  194. package/src/utils/input-validation.ts +8 -10
  195. package/src/utils/json-utils.ts +150 -16
  196. package/src/utils/llm-usage.ts +54 -0
  197. package/src/utils/log-rotation.ts +125 -0
  198. package/src/utils/log-utils.ts +159 -0
  199. package/src/utils/make-resource-loader.ts +3 -1
  200. package/src/utils/metrics-summary.ts +459 -0
  201. package/src/utils/metrics.ts +302 -0
  202. package/src/utils/research-export.ts +28 -12
  203. package/src/utils/safe-unref.ts +31 -0
  204. package/src/utils/shared-links.ts +195 -90
  205. package/src/utils/shutdown-manager.ts +65 -0
  206. package/src/utils/stdio-capture.ts +407 -0
  207. package/src/utils/text-utils.ts +264 -7
  208. package/src/utils/tool-usage-tracker.ts +83 -16
  209. package/src/utils/url-utils.ts +255 -0
  210. package/src/utils/user-agent.ts +28 -0
  211. package/src/web-research/browser-search.ts +179 -0
  212. package/src/web-research/retry-utils.ts +173 -77
  213. package/src/web-research/scraper-types.ts +72 -0
  214. package/src/web-research/scraper-utils.ts +434 -0
  215. package/src/web-research/search.ts +78 -218
  216. package/src/web-research/types.ts +9 -24
  217. package/src/web-research/utils.ts +2 -143
  218. package/src/web-research/web-scraper.ts +477 -0
  219. package/src/youtube/index.ts +154 -0
  220. package/src/youtube/potoken.ts +259 -0
  221. package/src/youtube/transcript-client.ts +390 -0
  222. package/src/youtube/video-id.ts +118 -0
  223. package/config/searxng/default-settings.yml +0 -189
  224. package/config/searxng/limiter.toml +0 -34
  225. package/config/tooling/eslint.config.js +0 -73
  226. package/config/tooling/vitest.config.integration.ts +0 -17
  227. package/config/tooling/vitest.config.ts +0 -40
  228. package/config/tooling/vitest.config.unit.ts +0 -12
  229. package/docs/assets/README-banner.jpg +0 -0
  230. package/scripts/setup.js +0 -174
  231. package/src/infrastructure/searxng-lifecycle.ts +0 -577
  232. package/src/infrastructure/searxng-manager.ts +0 -1183
  233. package/src/infrastructure/state-manager.ts +0 -918
  234. package/src/orchestration/deep-research-reducer.ts +0 -129
  235. package/src/orchestration/id-utils.ts +0 -63
  236. package/src/security/nvd.ts +0 -532
  237. package/src/security/osv.ts +0 -487
  238. package/src/stackexchange/cache.ts +0 -111
  239. package/src/stackexchange/output/index.ts +0 -7
  240. package/src/stackexchange/output/json.ts +0 -21
  241. package/src/tui/searxng-status.ts +0 -80
  242. package/src/utils/inject-date.ts +0 -29
  243. package/src/utils/research-error.ts +0 -205
  244. package/src/utils/searxng-config.ts +0 -94
  245. package/src/utils/session-state.ts +0 -295
  246. package/src/web-research/dependencies.ts +0 -20
  247. package/src/web-research/scrapers.ts +0 -935
  248. 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>;