@lincoln504/pi-research 1.3.12 → 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.
Files changed (71) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.mjs +5495 -4645
  3. package/dist/prompts/research-tool-usage.md +1 -1
  4. package/dist/prompts/researcher.md +1 -0
  5. package/dist/prompts/system-lead-router.md +122 -0
  6. package/dist/prompts/system-lead-synthesizer.md +46 -0
  7. package/dist/thread-worker.mjs +94 -19
  8. package/docs/ARCHITECTURE.md +18 -10
  9. package/docs/CHANGELOG.md +149 -0
  10. package/docs/CONFIGURATION.md +106 -7
  11. package/package.json +2 -2
  12. package/scripts/build.cjs +8 -1
  13. package/skills/pi-research/SKILL.md +1 -1
  14. package/src/cli.ts +5 -1
  15. package/src/config.ts +12 -8
  16. package/src/constants.ts +1 -1
  17. package/src/core/interfaces/observer-interfaces.ts +3 -3
  18. package/src/core/interfaces/orchestration-interfaces.ts +2 -0
  19. package/src/core/interfaces/planning-interfaces.ts +7 -1
  20. package/src/core/interfaces/scheduler-interfaces.ts +18 -2
  21. package/src/core/llm/agentic-repair.ts +7 -2
  22. package/src/core/llm/inject-date.ts +4 -1
  23. package/src/core/llm/llm-utils.ts +7 -0
  24. package/src/core/planning-service.ts +719 -91
  25. package/src/core/planning-utils.ts +21 -5
  26. package/src/core/service-initialization.ts +19 -3
  27. package/src/healthcheck/index.ts +11 -2
  28. package/src/index.ts +4 -3
  29. package/src/infrastructure/browser/browser-client.ts +68 -34
  30. package/src/infrastructure/browser/browser-task-scheduler.ts +220 -34
  31. package/src/infrastructure/browser/config.ts +41 -1
  32. package/src/infrastructure/browser/priority-task-queue.ts +55 -1
  33. package/src/infrastructure/browser/scheduler-factory.ts +12 -1
  34. package/src/infrastructure/browser/task-execution-service.ts +5 -5
  35. package/src/infrastructure/browser/thread-worker-browser.ts +32 -2
  36. package/src/infrastructure/browser/thread-worker-messaging.ts +38 -6
  37. package/src/infrastructure/browser/thread-worker.mjs +94 -19
  38. package/src/infrastructure/browser/thread-worker.ts +2 -2
  39. package/src/infrastructure/browser/worker-pool-manager.ts +127 -12
  40. package/src/infrastructure/embedding/embedding-client.ts +14 -4
  41. package/src/infrastructure/embedding/embedding-factory.ts +83 -15
  42. package/src/infrastructure/file-lock-service.ts +214 -19
  43. package/src/infrastructure/knowledge-store-service.ts +14 -0
  44. package/src/infrastructure/research-run-semaphore.ts +1 -1
  45. package/src/infrastructure/service-initialization.ts +7 -0
  46. package/src/knowledge/embedder-init.ts +43 -17
  47. package/src/logger.ts +11 -0
  48. package/src/orchestration/deep-research-orchestrator.ts +141 -87
  49. package/src/orchestration/quick-research-orchestrator.ts +11 -2
  50. package/src/orchestration/research-orchestration-service.ts +8 -0
  51. package/src/orchestration/research-synthesis-service.ts +53 -1
  52. package/src/orchestration/researcher-executor.ts +53 -2
  53. package/src/orchestration/researcher.ts +15 -2
  54. package/src/prompts/research-tool-usage.md +1 -1
  55. package/src/prompts/researcher.md +1 -0
  56. package/src/prompts/system-lead-router.md +122 -0
  57. package/src/prompts/system-lead-synthesizer.md +46 -0
  58. package/src/tools/research-knowledge-search.ts +11 -3
  59. package/src/tools/scrape.ts +6 -0
  60. package/src/tools/search.ts +7 -0
  61. package/src/tools/security.ts +27 -9
  62. package/src/tools/stackexchange.ts +13 -7
  63. package/src/tools/youtube-transcript.ts +26 -12
  64. package/src/utils/cancellation.ts +36 -0
  65. package/src/utils/coverage-digest.ts +204 -0
  66. package/src/utils/llm-usage.ts +33 -6
  67. package/src/utils/metrics-summary.ts +20 -4
  68. package/src/utils/metrics.ts +27 -1
  69. package/src/web-research/browser-search.ts +93 -20
  70. package/dist/prompts/system-lead-evaluator.md +0 -118
  71. 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
- Rounds 2-3 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.
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. If Round 1 has any gaps, delegate Round 2. A third round is only warranted when Round 2 still leaves significant holes. Scale researcher count to match the gaps — focused gaps may only need 1 researcher, while broad gaps benefit from the full team.`;
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. Level 3 has ${MAX_ROUNDS_LEVEL_3} rounds available. Be generous with follow-up delegation — lean toward using all available rounds. 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.
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
- await stateManager.clearEmbeddingServer({ pid: process.pid });
172
- logger.debug('[ServiceInitialization] Cleared embedding server state before disposal');
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
@@ -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
- }, { timeoutMs: Math.max(healthTimeoutMs, 150000), critical: true });
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
- // evaluator never pass through this hook at all: they run via completeSimple,
563
- // not an agent session, and get steering from planning-service directly.)
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 the leader's own task budget. The leader
22
- * already bounds each task (nav budget + queue-wait margin) and answers with the
23
- * more informative in-band timeout; the follower only needs headroom for the HTTP
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
- * Mirror of the leader's fixed healthcheck budget in
39
- * BrowserTaskScheduler.runHealthCheck (45s probe deadline + 60s queue-wait
40
- * margin) — not config-derived there either.
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: the leader's own budget for that task kind
46
- * plus a patience margin, bounded by an absolute cap.
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
- * Invariant: the follower is at least as patient as the leader. The old flat 60s
49
- * undercut the leader's healthcheck budget (105s) and config-driven search/scrape
50
- * budgets (up to 130s+ within schema bounds), and the client-timeout text matches
51
- * isTaskTimeoutError, so retry gates skipped it — long tasks failed client-side,
52
- * unretried, while the leader was still working on them.
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
- // BrowserTaskScheduler.runSearch: SEARCH_TIMEOUT_MS + BROWSER_TASK_TIMEOUT_MS.
60
- leaderBudgetMs = cfg.SEARCH_TIMEOUT_MS + cfg.BROWSER_TASK_TIMEOUT_MS;
61
- break;
62
- case 'browser-task':
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 largest fixed budget.
68
- leaderBudgetMs = HEALTHCHECK_LEADER_BUDGET_MS;
69
- break;
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
- // Config-derived per task kind: leader budget + margin, capped.
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
- async runSearch(query: string, config?: Config, signal?: AbortSignal): Promise<SearchResult[]> {
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
- async runScrape(url: string, config?: Config, signal?: AbortSignal): Promise<any> {
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