@lincoln504/pi-research 1.3.11 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/cli.mjs +5558 -4596
- package/dist/prompts/research-tool-usage.md +1 -1
- package/dist/prompts/researcher.md +1 -0
- package/dist/prompts/system-coordinator.md +1 -1
- package/dist/prompts/system-lead-router.md +122 -0
- package/dist/prompts/system-lead-synthesizer.md +46 -0
- package/dist/thread-worker.mjs +123 -30
- package/docs/ARCHITECTURE.md +69 -24
- package/docs/CHANGELOG.md +215 -0
- package/docs/CONFIGURATION.md +108 -8
- package/package.json +19 -11
- package/scripts/build.cjs +8 -1
- package/scripts/cleanup.cjs +26 -5
- package/scripts/setup.cjs +25 -1
- package/skills/pi-research/SKILL.md +1 -1
- package/src/cli.ts +41 -2
- package/src/config.ts +12 -8
- package/src/constants.ts +1 -1
- package/src/core/interfaces/observer-interfaces.ts +3 -3
- package/src/core/interfaces/orchestration-interfaces.ts +2 -0
- package/src/core/interfaces/planning-interfaces.ts +7 -1
- package/src/core/interfaces/scheduler-interfaces.ts +18 -2
- package/src/core/llm/agentic-repair.ts +7 -2
- package/src/core/llm/inject-date.ts +4 -1
- package/src/core/llm/llm-utils.ts +7 -0
- package/src/core/planning-service.ts +719 -91
- package/src/core/planning-utils.ts +21 -5
- package/src/core/service-initialization.ts +19 -3
- package/src/core/service-registry.ts +19 -3
- package/src/healthcheck/index.ts +11 -2
- package/src/index.ts +4 -3
- package/src/infrastructure/browser/browser-client.ts +68 -34
- package/src/infrastructure/browser/browser-task-scheduler.ts +220 -34
- package/src/infrastructure/browser/config.ts +77 -6
- package/src/infrastructure/browser/priority-task-queue.ts +55 -1
- package/src/infrastructure/browser/scheduler-factory.ts +12 -1
- package/src/infrastructure/browser/task-execution-service.ts +17 -7
- package/src/infrastructure/browser/thread-worker-browser.ts +66 -20
- package/src/infrastructure/browser/thread-worker-messaging.ts +38 -6
- package/src/infrastructure/browser/thread-worker.mjs +123 -30
- package/src/infrastructure/browser/thread-worker.ts +2 -2
- package/src/infrastructure/browser/worker-pool-manager.ts +127 -12
- package/src/infrastructure/embedding/embedding-client.ts +14 -4
- package/src/infrastructure/embedding/embedding-factory.ts +83 -15
- package/src/infrastructure/file-lock-service.ts +214 -19
- package/src/infrastructure/gpu-resource-service.ts +15 -1
- package/src/infrastructure/knowledge-store-service.ts +14 -0
- package/src/infrastructure/research-run-semaphore.ts +1 -1
- package/src/infrastructure/service-initialization.ts +7 -0
- package/src/knowledge/embedder-init.ts +60 -18
- package/src/knowledge/store.ts +11 -5
- package/src/logger.ts +11 -0
- package/src/orchestration/deep-research-orchestrator.ts +141 -87
- package/src/orchestration/quick-research-orchestrator.ts +11 -2
- package/src/orchestration/research-orchestration-service.ts +30 -1
- package/src/orchestration/research-synthesis-service.ts +53 -1
- package/src/orchestration/researcher-executor.ts +53 -2
- package/src/orchestration/researcher.ts +15 -2
- package/src/prompts/research-tool-usage.md +1 -1
- package/src/prompts/researcher.md +1 -0
- package/src/prompts/system-coordinator.md +1 -1
- package/src/prompts/system-lead-router.md +122 -0
- package/src/prompts/system-lead-synthesizer.md +46 -0
- package/src/sdk.ts +10 -1
- package/src/security/nvd-client.ts +55 -4
- package/src/security/osv-client.ts +13 -1
- package/src/tools/research-knowledge-search.ts +29 -4
- package/src/tools/scrape.ts +6 -0
- package/src/tools/search.ts +7 -0
- package/src/tools/security.ts +27 -9
- package/src/tools/stackexchange.ts +13 -7
- package/src/tools/youtube-transcript.ts +26 -12
- package/src/utils/cancellation.ts +36 -0
- package/src/utils/coverage-digest.ts +204 -0
- package/src/utils/llm-usage.ts +33 -6
- package/src/utils/log-utils.ts +16 -1
- package/src/utils/metrics-summary.ts +20 -4
- package/src/utils/metrics.ts +27 -1
- package/src/web-research/browser-search.ts +93 -20
- package/src/web-research/scraper-utils.ts +55 -14
- package/dist/prompts/system-lead-evaluator.md +0 -118
- package/src/prompts/system-lead-evaluator.md +0 -118
|
@@ -61,16 +61,32 @@ Scale your team (1-${maxTeamSize}) based on topic scope — not every round need
|
|
|
61
61
|
} else {
|
|
62
62
|
return `**Complexity: Level 3 (Ultra)**. Perform an exhaustive, deep-dive research effort, leaving no stone unturned. **Plan ONLY Round 1** — make it comprehensive, deploying up to ${maxTeamSize} researchers with full query budgets (${queryBudget} each) covering all major dimensions of the topic. Round 1 should aim to cover the full scope comprehensively.
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
Follow-up rounds are purely reactive — they only happen when Round 1 findings reveal gaps that warrant deeper diving into specific dimensions or broader exploration of adjacent topics. Do NOT pre-plan multiple rounds; let the findings drive the need. Think of Round 1 as the comprehensive landscape map, with follow-up rounds as targeted expeditions into areas that need more detail.
|
|
65
65
|
|
|
66
66
|
**ULTRA-SPECIFICITY MANDATE**: Level 3 demands granular, exhaustive detail on every fact that benefits from it — exact figures, dates, names, mechanisms, edge cases, historical context, technical specifics, and primary-source precision. Plan dedicated researchers for drilling into the ultra-specific dimensions of any finding where greater detail adds value.`;
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
|
-
* Get complexity-specific guidance for the evaluator
|
|
71
|
+
* Get complexity-specific guidance for the evaluator.
|
|
72
|
+
*
|
|
73
|
+
* `researchRounds` is the number of rounds that can still DELEGATE — the caller's
|
|
74
|
+
* live, steering-extended budget minus the terminal synthesis-only iteration. It is
|
|
75
|
+
* a parameter rather than a table lookup because this text sits in the same prompt
|
|
76
|
+
* as the round-phase guidance, which is already phrased against that number: a
|
|
77
|
+
* hardcoded per-level constant here told a Level 3 router it had "3 rounds
|
|
78
|
+
* available" three lines above "Round 1 of 2", and put the two apart by a further
|
|
79
|
+
* round whenever steering extended the budget. Every round count the model is shown
|
|
80
|
+
* must come from the same source.
|
|
72
81
|
*/
|
|
73
|
-
export function getEvaluatorComplexityGuidance(complexity: 1 | 2 | 3): string {
|
|
82
|
+
export function getEvaluatorComplexityGuidance(complexity: 1 | 2 | 3, researchRounds: number): string {
|
|
83
|
+
const rounds = Math.max(1, Math.floor(researchRounds));
|
|
84
|
+
// One line, correct in both directions. At a budget of one there is no follow-up to
|
|
85
|
+
// recommend and saying otherwise invites the model to defer to a round it will never
|
|
86
|
+
// get; above one, the encouragement to use the budget is the point.
|
|
87
|
+
const budgetLine = rounds === 1
|
|
88
|
+
? 'This run has ONE research round, and it has already happened — there is no follow-up round to defer to, so decide on what is in front of you.'
|
|
89
|
+
: `This run has ${rounds} research rounds in total; lean toward using them rather than finishing early.`;
|
|
74
90
|
if (complexity === 1) {
|
|
75
91
|
return `**Level 1 (Normal)** - Thorough, well-rounded investigation with solid multi-source coverage.
|
|
76
92
|
|
|
@@ -84,14 +100,14 @@ export function getEvaluatorComplexityGuidance(complexity: 1 | 2 | 3): string {
|
|
|
84
100
|
- **SYNTHESIZE when**: You are confident the research is genuinely complete. This means multiple angles covered with substantial findings across all major topics, diverse sources cited throughout, and no significant gaps in coverage.
|
|
85
101
|
- **DELEGATE when**: ANY gaps remain in major topics, insufficient source diversity, missing details, or areas that need deeper exploration. Don't synthesize prematurely.
|
|
86
102
|
|
|
87
|
-
**DEFAULT PATH: When in doubt, DELEGATE**. It is better to conduct additional research rounds than to synthesize with incomplete findings. Level 2 is designed for multi-round research. Each round adds depth and citation diversity — but do not delegate unnecessarily. Synthesize after Round 1 ONLY if the researcher produced comprehensive coverage of the topic with good source diversity
|
|
103
|
+
**DEFAULT PATH: When in doubt, DELEGATE**. It is better to conduct additional research rounds than to synthesize with incomplete findings. Level 2 is designed for multi-round research. Each round adds depth and citation diversity — but do not delegate unnecessarily. Synthesize after Round 1 ONLY if the researcher produced comprehensive coverage of the topic with good source diversity; if it has any gaps, delegate. ${budgetLine} Scale researcher count to match the gaps — focused gaps may only need 1 researcher, while broad gaps benefit from the full team.`;
|
|
88
104
|
} else {
|
|
89
105
|
return `**Level 3 (Ultra)** - Exhaustive, comprehensive deep-dive with extensive citations.
|
|
90
106
|
|
|
91
107
|
- **SYNTHESIZE when**: You are confident the research is genuinely and exhaustively complete. This means exhaustively covered across ALL substantial avenues with multiple diverse sources per major topic, comprehensive citations throughout, and no meaningful gaps remain.
|
|
92
108
|
- **DELEGATE when**: ANY meaningful gaps, nuanced angles, insufficient source diversity, inadequate citations, or areas needing deeper investigation remain.
|
|
93
109
|
|
|
94
|
-
**DEFAULT PATH: When in doubt, DELEGATE**. It is better to conduct additional research rounds than to synthesize with incomplete findings.
|
|
110
|
+
**DEFAULT PATH: When in doubt, DELEGATE**. It is better to conduct additional research rounds than to synthesize with incomplete findings. ${budgetLine} Be generous with follow-up delegation. Each round adds breadth, depth, and citation diversity. Delegate for follow-up whenever remaining gaps or under-explored angles exist, even if progress has been good. Only synthesize when you have genuinely comprehensive coverage across all major areas with no meaningful gaps that another round would address.
|
|
95
111
|
|
|
96
112
|
**ULTRA-SPECIFICITY MANDATE**: For every fact, finding, or topic area where greater granularity adds value, delegate additional researchers to pursue it. This includes: exact figures and statistics, precise dates and timelines, technical mechanisms, named individuals and their specific contributions, primary-source verbatim data, edge cases, and any dimension where surface-level coverage would leave the reader with unanswered questions.`;
|
|
97
113
|
}
|
|
@@ -11,6 +11,7 @@ import { ServiceNames } from './service-interfaces.ts';
|
|
|
11
11
|
import type { IStateManager } from './interfaces/state-manager-interfaces.ts';
|
|
12
12
|
import { SchedulerService } from './scheduler-service.ts';
|
|
13
13
|
import { PlanningService } from './planning-service.ts';
|
|
14
|
+
import { ownsLiveEmbeddingServer } from '../infrastructure/embedding/embedding-factory.ts';
|
|
14
15
|
import { logger } from '../logger.ts';
|
|
15
16
|
|
|
16
17
|
/**
|
|
@@ -167,9 +168,24 @@ export async function disposeCoreServices(container: ServiceContainer = getServi
|
|
|
167
168
|
// process that never touched embeddings; an unconditional clear here would
|
|
168
169
|
// deregister ANOTHER process's live leader, leaving it an invisible leader
|
|
169
170
|
// (its check treats a missing entry as benign) and forcing a duplicate GPU
|
|
170
|
-
// model election on the next caller.
|
|
171
|
-
|
|
172
|
-
|
|
171
|
+
// model election on the next caller. That unconditional form produced 80
|
|
172
|
+
// orphan windows totalling 147 hours before it was pid-scoped, the worst
|
|
173
|
+
// running four days.
|
|
174
|
+
//
|
|
175
|
+
// A pid is not enough on its own, though: it cannot separate "my leftover
|
|
176
|
+
// registration" from "my server, still listening". This clear runs BEFORE
|
|
177
|
+
// disposeAllServices, so our own server is typically still up — and this is
|
|
178
|
+
// the one clear in the tree issued on behalf of an instance the caller does
|
|
179
|
+
// not hold, so it is the only one that can delete a live registration it
|
|
180
|
+
// owns. When we hold the server, leave it: its own dispose clears the entry
|
|
181
|
+
// under a serverId compare-and-set, which is the correct owner-scoped
|
|
182
|
+
// teardown and runs moments later in disposeAllServices.
|
|
183
|
+
if (ownsLiveEmbeddingServer()) {
|
|
184
|
+
logger.debug('[ServiceInitialization] Skipping embedding-server state clear — this process owns the live server; its own dispose deregisters it');
|
|
185
|
+
} else {
|
|
186
|
+
await stateManager.clearEmbeddingServer({ pid: process.pid });
|
|
187
|
+
logger.debug('[ServiceInitialization] Cleared embedding server state before disposal');
|
|
188
|
+
}
|
|
173
189
|
}
|
|
174
190
|
} catch {
|
|
175
191
|
// Non-fatal: embedding server state will be cleaned up on next startup via PID check
|
|
@@ -178,10 +178,26 @@ export class ServiceContainer {
|
|
|
178
178
|
options: ServiceContainerOptions = {}
|
|
179
179
|
): Promise<void> {
|
|
180
180
|
const mergedOptions = { ...this.defaultOptions, ...options, allowOverwrite: true };
|
|
181
|
-
|
|
181
|
+
|
|
182
182
|
// Dispose existing service if present
|
|
183
|
-
|
|
184
|
-
|
|
183
|
+
const registration = this.services.get(name);
|
|
184
|
+
if (registration) {
|
|
185
|
+
// Join an already-in-flight clear()/replace()/disposeAll() on this
|
|
186
|
+
// registration before touching its instance — the same guard those paths
|
|
187
|
+
// use on each other. Without it, a concurrent teardown and this call both
|
|
188
|
+
// dispose the SAME instance, and register() below then swaps the whole
|
|
189
|
+
// registration object out from under the in-flight one, orphaning any
|
|
190
|
+
// instance it constructs.
|
|
191
|
+
// while(), not if(): a newer disposal can begin while we are awaiting this
|
|
192
|
+
// one, exactly as get() documents for the same field.
|
|
193
|
+
while (registration.disposalPromise) {
|
|
194
|
+
await registration.disposalPromise.catch(() => { /* disposal errors are logged by clear()/replace() */ });
|
|
195
|
+
}
|
|
196
|
+
// Settle an in-flight initialization too, so we never discard a service
|
|
197
|
+
// that is still being built — mirrors replace().
|
|
198
|
+
if (registration.initializationPromise) {
|
|
199
|
+
await registration.initializationPromise.catch(() => { /* failed init cleaned up after itself */ });
|
|
200
|
+
}
|
|
185
201
|
if (registration.instance && registration.instance.dispose) {
|
|
186
202
|
try {
|
|
187
203
|
await registration.instance.dispose();
|
package/src/healthcheck/index.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { execFileSync } from 'node:child_process';
|
|
9
9
|
import { getConfig } from '../config.ts';
|
|
10
|
-
import { isBrowserAvailable, resolveHeadlessMode } from '../infrastructure/browser/config.ts';
|
|
10
|
+
import { isBrowserAvailable, resolveHeadlessMode, getHealthCheckBudgetMs } from '../infrastructure/browser/config.ts';
|
|
11
11
|
import { runBrowserHealthCheck } from '../infrastructure/browser/task-execution-service.ts';
|
|
12
12
|
import { getService, tryGetServiceContainerFromCtx, getServiceContainer, ServiceLifecycle } from '../core/service-registry.ts';
|
|
13
13
|
import type { ServiceContainer } from '../core/service-registry.ts';
|
|
@@ -78,7 +78,16 @@ export function registerHealthChecks(registry: IHealthRegistryService, container
|
|
|
78
78
|
// launch (camoufox download/spawn) can legitimately take minutes, so we never
|
|
79
79
|
// go below that, but a user who raises HEALTH_CHECK_TIMEOUT_MS past it is
|
|
80
80
|
// respected (same pattern as the KnowledgeStore check's Math.max floor).
|
|
81
|
-
|
|
81
|
+
//
|
|
82
|
+
// This must ALSO never be less than getHealthCheckBudgetMs(): that is the
|
|
83
|
+
// deadline runBrowserHealthCheck's own scheduler call arms internally (derived
|
|
84
|
+
// from SEARCH_TIMEOUT_MS/SCRAPE_TIMEOUT_MS/BROWSER_TASK_TIMEOUT_MS so it always
|
|
85
|
+
// out-waits the longest a task can legitimately hold a worker slot). A fixed
|
|
86
|
+
// 150s floor here can be smaller than that derived budget at raised timeouts,
|
|
87
|
+
// which would abort this registry-level race before the inner check ever gets
|
|
88
|
+
// to return — reporting a merely busy pool as unhealthy, the exact failure
|
|
89
|
+
// class the derived budget exists to prevent, one call-frame further out.
|
|
90
|
+
}, { timeoutMs: Math.max(healthTimeoutMs, 150000, getHealthCheckBudgetMs(config)), critical: true });
|
|
82
91
|
|
|
83
92
|
// Register Knowledge Store Check — NON-critical: the store is the optional cache
|
|
84
93
|
// research must run without (see the darwin-x64 comment below). A failure here
|
package/src/index.ts
CHANGED
|
@@ -558,9 +558,10 @@ export default async function (pi: ExtensionAPI) {
|
|
|
558
558
|
// the executor (deep) and quick orchestrator inject "ADDITIONAL USER GUIDANCE"/
|
|
559
559
|
// "ADDITIONAL CONSIDERATIONS" into the researcher system prompt themselves.
|
|
560
560
|
// Appending the same messages again here delivered every steering message
|
|
561
|
-
// TWICE to every researcher — return the prompt untouched. (The coordinator
|
|
562
|
-
//
|
|
563
|
-
// not an agent session, and get steering from planning-service
|
|
561
|
+
// TWICE to every researcher — return the prompt untouched. (The coordinator and
|
|
562
|
+
// the research lead never pass through this hook at all: they run via
|
|
563
|
+
// completeSimple, not an agent session, and get steering from planning-service
|
|
564
|
+
// directly.)
|
|
564
565
|
const isResearcher = event.systemPrompt?.includes('RESEARCHER_AGENT_MARKER');
|
|
565
566
|
if (isResearcher) {
|
|
566
567
|
return { systemPrompt: event.systemPrompt };
|
|
@@ -8,20 +8,21 @@
|
|
|
8
8
|
import * as http from 'node:http';
|
|
9
9
|
import type { Config } from '../../config.ts';
|
|
10
10
|
import { getConfig } from '../../config.ts';
|
|
11
|
+
|
|
11
12
|
import type { SearchResult } from '../../web-research/types.ts';
|
|
12
|
-
import type { IScheduler } from '../../core/interfaces/scheduler-interfaces.ts';
|
|
13
|
+
import type { IScheduler, TaskDispatchListener } from '../../core/interfaces/scheduler-interfaces.ts';
|
|
13
14
|
import { logger } from '../../logger.ts';
|
|
14
15
|
import { errorTracker } from '../../utils/error-tracker.ts';
|
|
15
16
|
import { redactSecrets } from '../../utils/log-utils.ts';
|
|
16
17
|
import { Utf8Body } from '../../utils/http-body.ts';
|
|
17
18
|
import { getClientAgent } from './client-agent.ts';
|
|
19
|
+
import { getHealthCheckBudgetMs } from './config.ts';
|
|
18
20
|
import type { NodeError } from '../../types/index.ts';
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
|
-
* Client-side patience margin on top of
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* hop and leader-side dispatch, so its own timer never answers first.
|
|
23
|
+
* Client-side patience margin on top of a leader budget that bounds the WHOLE
|
|
24
|
+
* call (only the healthcheck does, now). The follower needs headroom for the HTTP
|
|
25
|
+
* hop, so its own timer never answers first.
|
|
25
26
|
*/
|
|
26
27
|
const CLIENT_PATIENCE_MARGIN_MS = 15_000;
|
|
27
28
|
|
|
@@ -34,41 +35,65 @@ const CLIENT_PATIENCE_MARGIN_MS = 15_000;
|
|
|
34
35
|
*/
|
|
35
36
|
const CLIENT_TIMEOUT_CAP_MS = 300_000;
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
*
|
|
40
|
-
|
|
41
|
-
*/
|
|
42
|
-
const HEALTHCHECK_LEADER_BUDGET_MS = 45_000 + 60_000;
|
|
38
|
+
|
|
39
|
+
/** Fraction of the owning caller's budget at which the wedge detector fires, so the
|
|
40
|
+
* failure is reported and retriable inside the life of whatever asked for it. */
|
|
41
|
+
const OWNER_BUDGET_FRACTION = 0.8;
|
|
43
42
|
|
|
44
43
|
/**
|
|
45
|
-
* Per-operation follower timeout
|
|
46
|
-
*
|
|
44
|
+
* Per-operation follower timeout.
|
|
45
|
+
*
|
|
46
|
+
* The rule this used to state — "the follower is at least as patient as the leader" —
|
|
47
|
+
* is no longer the rule, and the change was deliberate; it is written out here because
|
|
48
|
+
* it is the obvious thing to reach for and it is wrong for two of the three cases
|
|
49
|
+
* below. What it replaced was worse still: a flat 60s that undercut the leader's
|
|
50
|
+
* healthcheck budget (105s) and its config-driven search/scrape budgets (130s+ within
|
|
51
|
+
* schema bounds), and whose message matches isTaskTimeoutError, so the retry gates
|
|
52
|
+
* skipped it — long tasks failed client-side, unretried, while the leader was still
|
|
53
|
+
* working on them.
|
|
54
|
+
*
|
|
55
|
+
* Search and scrape do not derive from the leader's TASK budget. That budget bounds
|
|
56
|
+
* EXECUTION only — it is armed when the queue dispatches, not when the request
|
|
57
|
+
* lands — so leader turnaround is queue wait plus that budget, and no value derived
|
|
58
|
+
* from it alone can out-wait it. Deriving one anyway is how a follower came to
|
|
59
|
+
* abandon requests the leader was still queueing, the same defect the leader-side
|
|
60
|
+
* deadline had. This timer is a detector for a leader that is listening but no
|
|
61
|
+
* longer answering; work waits, and is not discarded because the pool was busy.
|
|
62
|
+
*
|
|
63
|
+
* It is derived from the budget of whatever OWNS the call instead, because a
|
|
64
|
+
* detector that fires after its caller is already dead detects nothing. The flat
|
|
65
|
+
* 300s cap was exactly that: RESEARCHER_TIMEOUT_MS defaults to 300s and its schema
|
|
66
|
+
* minimum is 180s, so the researcher holding the request was always killed first
|
|
67
|
+
* and the user got a bare researcher timeout with nothing said about the browser
|
|
68
|
+
* leader — where before they at least got a request-timeout they could classify
|
|
69
|
+
* and retry. Firing at a fraction of the owner's budget leaves room for the
|
|
70
|
+
* failure to be reported and retried within the life of the thing that asked.
|
|
47
71
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
72
|
+
* The healthcheck keeps a budget-derived value: its leader side bounds the WHOLE call,
|
|
73
|
+
* wait included — the probe's wait timer rejects rather than queueing indefinitely — so
|
|
74
|
+
* a liveness probe stays as impatient as a liveness probe should be. (It used to be
|
|
75
|
+
* described as answering "healthy, saturated" instead of failing; that branch was
|
|
76
|
+
* unreachable and has been removed. The bound is what matters here, not the verdict.)
|
|
77
|
+
* Even there the old invariant is only conditional: at the schema's extreme settings
|
|
78
|
+
* the derived leader budget exceeds the absolute cap, and the cap wins. That is the
|
|
79
|
+
* intended order — five minutes is already far past the point where a liveness probe
|
|
80
|
+
* tells anyone anything, whatever the leader is still doing.
|
|
53
81
|
*/
|
|
54
82
|
export function resolveClientRequestTimeoutMs(operation: string, config?: Config): number {
|
|
55
|
-
const cfg = config ?? getConfig();
|
|
56
|
-
let leaderBudgetMs: number;
|
|
57
83
|
switch (operation) {
|
|
58
84
|
case 'search':
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
// BrowserTaskScheduler.runScrape: SCRAPE_TIMEOUT_MS + BROWSER_TASK_TIMEOUT_MS.
|
|
64
|
-
leaderBudgetMs = cfg.SCRAPE_TIMEOUT_MS + cfg.BROWSER_TASK_TIMEOUT_MS;
|
|
65
|
-
break;
|
|
85
|
+
case 'browser-task': {
|
|
86
|
+
const ownerBudgetMs = (config ?? getConfig()).RESEARCHER_TIMEOUT_MS;
|
|
87
|
+
return Math.min(CLIENT_TIMEOUT_CAP_MS, Math.floor(ownerBudgetMs * OWNER_BUDGET_FRACTION));
|
|
88
|
+
}
|
|
66
89
|
default:
|
|
67
|
-
// healthcheck, plus any future/unknown path: the
|
|
68
|
-
|
|
69
|
-
|
|
90
|
+
// healthcheck, plus any future/unknown path: the leader's own budget plus a
|
|
91
|
+
// margin. Read from the SAME function the leader reads, not from a local
|
|
92
|
+
// copy of the constant — the two were separate literals and would have
|
|
93
|
+
// drifted the moment either moved, leaving the follower less patient than
|
|
94
|
+
// the leader it is waiting on.
|
|
95
|
+
return Math.min(CLIENT_TIMEOUT_CAP_MS, getHealthCheckBudgetMs(config) + CLIENT_PATIENCE_MARGIN_MS);
|
|
70
96
|
}
|
|
71
|
-
return Math.min(CLIENT_TIMEOUT_CAP_MS, leaderBudgetMs + CLIENT_PATIENCE_MARGIN_MS);
|
|
72
97
|
}
|
|
73
98
|
|
|
74
99
|
/**
|
|
@@ -99,7 +124,7 @@ export class BrowserClient implements IScheduler {
|
|
|
99
124
|
|
|
100
125
|
return new Promise((resolve, reject) => {
|
|
101
126
|
const agent = getClientAgent();
|
|
102
|
-
//
|
|
127
|
+
// Wedge detector for search/scrape; leader budget + margin for the healthcheck.
|
|
103
128
|
const timeoutMs = resolveClientRequestTimeoutMs(operation, config);
|
|
104
129
|
let resolved = false;
|
|
105
130
|
const controller = new AbortController();
|
|
@@ -269,11 +294,20 @@ export class BrowserClient implements IScheduler {
|
|
|
269
294
|
});
|
|
270
295
|
}
|
|
271
296
|
|
|
272
|
-
|
|
297
|
+
/**
|
|
298
|
+
* `onTaskEvent` is deliberately not invoked here. A follower sees only the
|
|
299
|
+
* leader's final reply, never the moment the leader's queue hands the task to
|
|
300
|
+
* a worker, so it has nothing truthful to report. Calling it on send would
|
|
301
|
+
* arm the caller's guard against queue wait — precisely the bug being fixed —
|
|
302
|
+
* so the caller's guard stays disarmed and this class bounds the call itself
|
|
303
|
+
* (see resolveClientRequestTimeoutMs).
|
|
304
|
+
*/
|
|
305
|
+
async runSearch(query: string, config?: Config, signal?: AbortSignal, _onTaskEvent?: TaskDispatchListener): Promise<SearchResult[]> {
|
|
273
306
|
return this.request('/search', { query }, signal, config);
|
|
274
307
|
}
|
|
275
308
|
|
|
276
|
-
|
|
309
|
+
/** See runSearch on why `onTaskEvent` is not invoked. */
|
|
310
|
+
async runScrape(url: string, config?: Config, signal?: AbortSignal, _onTaskEvent?: TaskDispatchListener): Promise<any> {
|
|
277
311
|
return this.request('/scrape', { url }, signal, config);
|
|
278
312
|
}
|
|
279
313
|
|