@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
|
@@ -1,123 +1,228 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared Links
|
|
2
|
+
* Shared Links Registry
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* No per-session state - everything flows through global state.
|
|
4
|
+
* Provides a global registry of links scraped across all researchers
|
|
5
|
+
* in a session to prevent redundant work.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
|
-
import
|
|
8
|
+
import { normalizeUrl } from './url-utils.ts';
|
|
9
|
+
export { normalizeUrl };
|
|
10
|
+
import { logger } from '../logger.ts';
|
|
11
|
+
import { safeUnref } from './safe-unref.ts';
|
|
12
|
+
import { truncateWithMarker } from './text-utils.ts';
|
|
13
|
+
import { MAX_SCRAPE_CONTENT_CHARS_PER_DOC } from '../constants.ts';
|
|
14
|
+
import { randomUUID } from 'node:crypto';
|
|
15
|
+
|
|
16
|
+
const sessionLinks = new Map<string, Set<string>>();
|
|
17
|
+
const sessionScrapedContent = new Map<string, Map<string, string>>();
|
|
18
|
+
// Per-researcher scrape tracking: researchId → researcherId → Set<normalizedUrl>
|
|
19
|
+
const researcherScrapes = new Map<string, Map<string, Set<string>>>();
|
|
20
|
+
|
|
21
|
+
// FIX (Issue 12): Track creation timestamps for orphaned-entry cleanup.
|
|
22
|
+
const sessionTimestamps = new Map<string, number>();
|
|
23
|
+
const SESSION_MAX_AGE_MS = 2 * 60 * 60 * 1000; // 2 hours
|
|
24
|
+
// FIX (#20): Cap per-session cached content to prevent unbounded memory growth
|
|
25
|
+
const MAX_CACHED_CONTENT_PER_SESSION = 500;
|
|
26
|
+
|
|
27
|
+
// Periodic cleanup of orphaned sessions (every 15 minutes)
|
|
28
|
+
let cleanupInterval: NodeJS.Timeout | null = null;
|
|
29
|
+
|
|
30
|
+
function startCleanupTimer(): void {
|
|
31
|
+
if (cleanupInterval) return;
|
|
32
|
+
cleanupInterval = setInterval(() => {
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
let cleaned = 0;
|
|
35
|
+
for (const [researchId, createdAt] of sessionTimestamps.entries()) {
|
|
36
|
+
if (now - createdAt > SESSION_MAX_AGE_MS) {
|
|
37
|
+
sessionLinks.delete(researchId);
|
|
38
|
+
sessionScrapedContent.delete(researchId);
|
|
39
|
+
sessionTimestamps.delete(researchId);
|
|
40
|
+
researcherScrapes.delete(researchId);
|
|
41
|
+
cleaned++;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (cleaned > 0) {
|
|
45
|
+
logger.debug(`[Shared Links] Cleaned up ${cleaned} orphaned session(s) older than ${SESSION_MAX_AGE_MS / 60000} minutes`);
|
|
46
|
+
}
|
|
47
|
+
}, 15 * 60 * 1000);
|
|
48
|
+
if (cleanupInterval) safeUnref(cleanupInterval);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Start on module load
|
|
52
|
+
startCleanupTimer();
|
|
10
53
|
|
|
11
54
|
/**
|
|
12
|
-
*
|
|
55
|
+
* Cache the raw markdown of a scraped URL during a research session.
|
|
56
|
+
* This is used later to embed the raw text alongside the agent's summary.
|
|
13
57
|
*/
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
58
|
+
export function cacheScrapedContent(researchId: string, url: string, content: string) {
|
|
59
|
+
if (!sessionScrapedContent.has(researchId)) {
|
|
60
|
+
sessionScrapedContent.set(researchId, new Map());
|
|
61
|
+
}
|
|
62
|
+
// Refresh on every write so the orphan-cleanup TTL is last-activity based, not
|
|
63
|
+
// creation based — otherwise a research session running longer than the 2h
|
|
64
|
+
// SESSION_MAX_AGE_MS has its dedup/content pools wiped mid-run.
|
|
65
|
+
sessionTimestamps.set(researchId, Date.now());
|
|
66
|
+
const cache = sessionScrapedContent.get(researchId)!;
|
|
67
|
+
const key = normalizeUrl(url);
|
|
68
|
+
// FIX (#20): Evict oldest entry only when at capacity AND inserting a NEW key.
|
|
69
|
+
// Re-caching an existing URL must not evict an unrelated entry (net size loss).
|
|
70
|
+
if (!cache.has(key) && cache.size >= MAX_CACHED_CONTENT_PER_SESSION) {
|
|
71
|
+
const firstKey = cache.keys().next().value;
|
|
72
|
+
if (firstKey !== undefined) cache.delete(firstKey);
|
|
73
|
+
}
|
|
74
|
+
// Byte-bound each entry: the cache was previously only COUNT-capped (500
|
|
75
|
+
// entries), so 500 × multi-MB documents could grow unbounded in memory —
|
|
76
|
+
// and cached entries are re-served verbatim into researcher LLM sessions
|
|
77
|
+
// (scrape duplicate path) and embedded by the knowledge-store writer.
|
|
78
|
+
cache.set(key, truncateWithMarker(content, MAX_SCRAPE_CONTENT_CHARS_PER_DOC));
|
|
27
79
|
}
|
|
28
80
|
|
|
29
81
|
/**
|
|
30
|
-
*
|
|
31
|
-
* @param baseId Base identifier (e.g., request ID)
|
|
32
|
-
* @returns Unique session ID (e.g., "abc123-x9k2")
|
|
82
|
+
* Retrieve the cached raw markdown for a scraped URL.
|
|
33
83
|
*/
|
|
34
|
-
export function
|
|
35
|
-
|
|
36
|
-
return `${baseId}-${hash}`;
|
|
84
|
+
export function getCachedScrapedContent(researchId: string, url: string): string | undefined {
|
|
85
|
+
return sessionScrapedContent.get(researchId)?.get(normalizeUrl(url));
|
|
37
86
|
}
|
|
38
87
|
|
|
39
88
|
/**
|
|
40
|
-
*
|
|
89
|
+
* Generate a unique research ID based on the parent Pi session.
|
|
41
90
|
*/
|
|
42
|
-
export function
|
|
43
|
-
|
|
44
|
-
const normalized = url.toLowerCase().trim();
|
|
45
|
-
// Remove trailing slash except for root
|
|
46
|
-
if (normalized.length > 1 && normalized.endsWith('/')) {
|
|
47
|
-
return normalized.slice(0, -1);
|
|
48
|
-
}
|
|
49
|
-
return normalized;
|
|
50
|
-
} catch {
|
|
51
|
-
return url.toLowerCase().trim();
|
|
52
|
-
}
|
|
91
|
+
export function generateSessionId(piSessionId: string): string {
|
|
92
|
+
return `${piSessionId}-${randomUUID().replace(/-/g, '').substring(0, 8)}`;
|
|
53
93
|
}
|
|
54
94
|
|
|
55
95
|
/**
|
|
56
|
-
*
|
|
57
|
-
* Uses normalized comparison for accuracy.
|
|
58
|
-
*
|
|
59
|
-
* @param urls - URLs to check
|
|
60
|
-
* @param alreadyScraped - Array of already-scraped URLs
|
|
61
|
-
* @returns Object with kept URLs and removed duplicates
|
|
96
|
+
* Register links as scraped for a specific session.
|
|
62
97
|
*/
|
|
63
|
-
export function
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
} {
|
|
67
|
-
const normalizedAlreadyScraped = new Set(
|
|
68
|
-
alreadyScraped.map(normalizeUrl)
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
const kept: string[] = [];
|
|
72
|
-
const duplicates: string[] = [];
|
|
73
|
-
|
|
74
|
-
for (const url of urls) {
|
|
75
|
-
const normalized = normalizeUrl(url);
|
|
76
|
-
if (normalizedAlreadyScraped.has(normalized)) {
|
|
77
|
-
duplicates.push(url);
|
|
78
|
-
} else {
|
|
79
|
-
kept.push(url);
|
|
98
|
+
export function registerScrapedLinks(researchId: string, links: string[]) {
|
|
99
|
+
if (!sessionLinks.has(researchId)) {
|
|
100
|
+
sessionLinks.set(researchId, new Set());
|
|
80
101
|
}
|
|
81
|
-
|
|
102
|
+
sessionTimestamps.set(researchId, Date.now()); // last-activity TTL refresh
|
|
103
|
+
const pool = sessionLinks.get(researchId)!;
|
|
104
|
+
links.forEach(l => pool.add(normalizeUrl(l)));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get all scraped links for a session.
|
|
109
|
+
*/
|
|
110
|
+
export function getScrapedLinks(researchId: string): string[] {
|
|
111
|
+
return Array.from(sessionLinks.get(researchId) || []);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Deduplicate a list of candidate URLs against already scraped links.
|
|
116
|
+
*/
|
|
117
|
+
export function deduplicateUrls(urls: string[], researchId: string): { kept: string[], duplicates: string[] } {
|
|
118
|
+
const pool = sessionLinks.get(researchId) || new Set();
|
|
119
|
+
const kept: string[] = [];
|
|
120
|
+
const duplicates: string[] = [];
|
|
121
|
+
|
|
122
|
+
urls.forEach(url => {
|
|
123
|
+
const normalized = normalizeUrl(url);
|
|
124
|
+
if (pool.has(normalized)) {
|
|
125
|
+
duplicates.push(url); // Keep original URL for logging/display
|
|
126
|
+
} else {
|
|
127
|
+
kept.push(url);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return { kept, duplicates };
|
|
132
|
+
}
|
|
82
133
|
|
|
83
|
-
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Cleanup session data.
|
|
137
|
+
*/
|
|
138
|
+
export function cleanupSharedLinks(researchId: string) {
|
|
139
|
+
sessionLinks.delete(researchId);
|
|
140
|
+
sessionScrapedContent.delete(researchId);
|
|
141
|
+
sessionTimestamps.delete(researchId);
|
|
142
|
+
researcherScrapes.delete(researchId);
|
|
143
|
+
logger.debug(`[Shared Links] Cleaned up session: ${researchId}`);
|
|
84
144
|
}
|
|
85
145
|
|
|
86
146
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
147
|
+
* Cleanup ALL session data and stop the background timer.
|
|
148
|
+
* Used during system shutdown to prevent memory leaks.
|
|
89
149
|
*/
|
|
90
|
-
export function
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
let output = '## Shared Links from Previous Research\n\n';
|
|
95
|
-
output += 'The following links have already been examined by your siblings:\n\n';
|
|
96
|
-
|
|
97
|
-
for (const aspect of completed) {
|
|
98
|
-
const response = aspect.report!;
|
|
99
|
-
output += `### Aspect: ${aspect.query} (ID: ${aspect.id})\n\n`;
|
|
100
|
-
|
|
101
|
-
// Extract CITED LINKS
|
|
102
|
-
const citedMatch = response.match(/###?\s*CITED\s*LINKS?\s*\n([\s\S]*?)(?=\n###|$)/i);
|
|
103
|
-
if (citedMatch) {
|
|
104
|
-
output += '#### CITED LINKS\n';
|
|
105
|
-
output += citedMatch[1]!.trim() + '\n\n';
|
|
150
|
+
export function clearAllSharedLinks(): void {
|
|
151
|
+
if (cleanupInterval) {
|
|
152
|
+
clearInterval(cleanupInterval);
|
|
153
|
+
cleanupInterval = null;
|
|
106
154
|
}
|
|
155
|
+
sessionLinks.clear();
|
|
156
|
+
sessionScrapedContent.clear();
|
|
157
|
+
sessionTimestamps.clear();
|
|
158
|
+
researcherScrapes.clear();
|
|
159
|
+
logger.debug('[Shared Links] All global state cleared');
|
|
160
|
+
}
|
|
107
161
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
162
|
+
/**
|
|
163
|
+
* Register that a specific researcher scraped a set of URLs.
|
|
164
|
+
*/
|
|
165
|
+
export function registerResearcherScrapes(researchId: string, researcherId: string, urls: string[]) {
|
|
166
|
+
if (!researcherScrapes.has(researchId)) {
|
|
167
|
+
researcherScrapes.set(researchId, new Map());
|
|
113
168
|
}
|
|
114
|
-
|
|
169
|
+
sessionTimestamps.set(researchId, Date.now()); // keep this session reapable + TTL fresh
|
|
170
|
+
const map = researcherScrapes.get(researchId)!;
|
|
171
|
+
if (!map.has(researcherId)) {
|
|
172
|
+
map.set(researcherId, new Set());
|
|
173
|
+
}
|
|
174
|
+
const set = map.get(researcherId)!;
|
|
175
|
+
urls.forEach(url => set.add(normalizeUrl(url)));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Check if a specific researcher scraped a given URL.
|
|
180
|
+
*/
|
|
181
|
+
export function didResearcherScrape(researchId: string, researcherId: string, url: string): boolean {
|
|
182
|
+
return researcherScrapes.get(researchId)?.get(researcherId)?.has(normalizeUrl(url)) ?? false;
|
|
183
|
+
}
|
|
115
184
|
|
|
116
|
-
|
|
117
|
-
|
|
185
|
+
/**
|
|
186
|
+
* Get all URLs scraped by a specific researcher.
|
|
187
|
+
*/
|
|
188
|
+
export function getResearcherScrapes(researchId: string, researcherId: string): string[] {
|
|
189
|
+
return Array.from(researcherScrapes.get(researchId)?.get(researcherId) ?? []);
|
|
118
190
|
}
|
|
119
191
|
|
|
120
192
|
/**
|
|
121
|
-
*
|
|
193
|
+
* Build the Session URL Pool footer for scrape responses.
|
|
194
|
+
* Shows URLs previously scraped in the session, filtered to exclude the current batch.
|
|
195
|
+
* Capped at MAX_FOOTER_URLS (20).
|
|
122
196
|
*/
|
|
123
|
-
|
|
197
|
+
const MAX_FOOTER_URLS = 20;
|
|
198
|
+
|
|
199
|
+
export function buildSessionPoolFooter(researchId: string, currentBatchUrls: string[], researcherId?: string): string {
|
|
200
|
+
const pool = sessionLinks.get(researchId);
|
|
201
|
+
if (!pool || pool.size === 0) return '';
|
|
202
|
+
|
|
203
|
+
const currentNormalized = new Set(currentBatchUrls.map(u => normalizeUrl(u)));
|
|
204
|
+
// Also exclude URLs the current researcher already scraped in earlier batches
|
|
205
|
+
if (researcherId) {
|
|
206
|
+
const ownScrapes = researcherScrapes.get(researchId)?.get(researcherId);
|
|
207
|
+
if (ownScrapes) {
|
|
208
|
+
for (const url of ownScrapes) {
|
|
209
|
+
currentNormalized.add(url);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const eligible = Array.from(pool).filter(u => !currentNormalized.has(u));
|
|
214
|
+
if (eligible.length === 0) return '';
|
|
215
|
+
|
|
216
|
+
const capped = eligible.slice(-MAX_FOOTER_URLS);
|
|
217
|
+
const overflow = eligible.length - capped.length;
|
|
218
|
+
|
|
219
|
+
let footer = '\n---\n## Session URL Pool\n';
|
|
220
|
+
footer += 'Info only — URLs scraped by other researchers in this session. Do NOT use these in your report.\n';
|
|
221
|
+
for (const url of capped) {
|
|
222
|
+
footer += `- ${url}\n`;
|
|
223
|
+
}
|
|
224
|
+
if (overflow > 0) {
|
|
225
|
+
footer += `*...and ${overflow} more*\n`;
|
|
226
|
+
}
|
|
227
|
+
return footer;
|
|
228
|
+
}
|
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
import { logger } from '../logger.ts';
|
|
2
|
+
import { safeUnref } from './safe-unref.ts';
|
|
2
3
|
|
|
3
4
|
export type CleanupTask = () => Promise<void> | void;
|
|
4
5
|
|
|
6
|
+
// Minimal interface matching Node.js EventEmitter and process
|
|
7
|
+
interface EventEmitterLike {
|
|
8
|
+
on(event: string | symbol, listener: (...args: any[]) => void): any;
|
|
9
|
+
off?(event: string | symbol, listener: (...args: any[]) => void): any;
|
|
10
|
+
removeListener?(event: string | symbol, listener: (...args: any[]) => void): any;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface EventListenerCleanup {
|
|
14
|
+
target: EventEmitterLike;
|
|
15
|
+
event: string;
|
|
16
|
+
listener: (...args: any[]) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
5
19
|
export class ShutdownManager {
|
|
6
20
|
private tasks: CleanupTask[] = [];
|
|
21
|
+
private eventListeners: EventListenerCleanup[] = [];
|
|
7
22
|
private cleanupPromise: Promise<void> | null = null;
|
|
8
23
|
|
|
9
24
|
register(task: CleanupTask) {
|
|
@@ -14,6 +29,30 @@ export class ShutdownManager {
|
|
|
14
29
|
this.tasks.push(task);
|
|
15
30
|
}
|
|
16
31
|
|
|
32
|
+
registerEventListener<T extends EventEmitterLike>(
|
|
33
|
+
target: T,
|
|
34
|
+
event: string,
|
|
35
|
+
listener: (...args: any[]) => void
|
|
36
|
+
) {
|
|
37
|
+
target.on(event, listener);
|
|
38
|
+
this.eventListeners.push({ target, event, listener });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
unregisterEventListener<T extends EventEmitterLike>(
|
|
42
|
+
target: T,
|
|
43
|
+
event: string,
|
|
44
|
+
listener: (...args: any[]) => void
|
|
45
|
+
) {
|
|
46
|
+
if (target.off) {
|
|
47
|
+
target.off(event, listener);
|
|
48
|
+
} else if (target.removeListener) {
|
|
49
|
+
target.removeListener(event, listener);
|
|
50
|
+
}
|
|
51
|
+
this.eventListeners = this.eventListeners.filter(
|
|
52
|
+
el => el.target !== target || el.event !== event || el.listener !== listener
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
17
56
|
async runCleanup(reason: string): Promise<void> {
|
|
18
57
|
if (this.cleanupPromise !== null) {
|
|
19
58
|
return this.cleanupPromise;
|
|
@@ -46,9 +85,35 @@ export class ShutdownManager {
|
|
|
46
85
|
try {
|
|
47
86
|
await this.cleanupPromise;
|
|
48
87
|
} finally {
|
|
88
|
+
// Remove all registered event listeners to allow clean process exit
|
|
89
|
+
for (const { target, event, listener } of this.eventListeners) {
|
|
90
|
+
try {
|
|
91
|
+
if (target.off) {
|
|
92
|
+
target.off(event, listener);
|
|
93
|
+
} else if (target.removeListener) {
|
|
94
|
+
target.removeListener(event, listener);
|
|
95
|
+
}
|
|
96
|
+
} catch (error) {
|
|
97
|
+
logger.error('[ShutdownManager] Error removing event listener:', error);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
this.eventListeners = [];
|
|
49
101
|
this.cleanupPromise = null;
|
|
50
102
|
}
|
|
51
103
|
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Force exit the process after a timeout if shutdown is hanging
|
|
107
|
+
* This should be called after runCleanup() with a short delay
|
|
108
|
+
*/
|
|
109
|
+
forceExitAfter(timeoutMs: number, code = 0) {
|
|
110
|
+
const timer = setTimeout(() => {
|
|
111
|
+
logger.warn(`[ShutdownManager] Forcing exit after ${timeoutMs}ms timeout`);
|
|
112
|
+
process.env['PI_PROCESS_EXITING'] = '1';
|
|
113
|
+
process.exit(code);
|
|
114
|
+
}, timeoutMs);
|
|
115
|
+
safeUnref(timer); // unref() allows Node.js to exit if this is the only timer
|
|
116
|
+
}
|
|
52
117
|
}
|
|
53
118
|
|
|
54
119
|
export const shutdownManager = new ShutdownManager();
|