@bermudi/pi-delegate 0.1.10 → 0.1.12

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.
@@ -6,15 +6,29 @@ import {
6
6
  DEFAULT_TOOLS,
7
7
  VALID_THINKING,
8
8
  } from "./constants.ts";
9
- import { TOOL_FACTORIES, resolveToolGroups } from "./tools.ts";
9
+ import {
10
+ TOOL_FACTORIES,
11
+ availableToolNames,
12
+ resolveToolGroups,
13
+ } from "./tools.ts";
10
14
  import { configFor } from "./pool.ts";
11
15
  import { isSessionBusy } from "./tickets.ts";
12
16
  import { BUILTIN_AGENT_CONFIGS, buildSubagentSystemPrompt } from "./agents.ts";
13
17
  import { buildParentTranscript } from "./parent-context.ts";
14
18
  import { findAvailableAlternative, resolveModelRequest } from "./model.ts";
15
- import { resolveModelSpec } from "./config.ts";
16
- import { loadDelegateSettings } from "./settings.ts";
19
+ import {
20
+ getAgentOverrides,
21
+ getAgentOverridesByParentModel,
22
+ resolveModelSpec,
23
+ getDelegateConfigSnapshot,
24
+ getProviderExtensionSignature,
25
+ } from "./config.ts";
26
+ import type { DelegateConfig } from "./config.ts";
17
27
  import { resolveCwd } from "./utils.ts";
28
+ import {
29
+ loadDelegateSettings,
30
+ warnLegacyDelegateSettingsMoved,
31
+ } from "./settings.ts";
18
32
  import type {
19
33
  AgentConfig,
20
34
  DelegateToolCtx,
@@ -28,6 +42,13 @@ const PROJECT_CONTEXT_START =
28
42
  "\n\n<project_context>\n\nProject-specific instructions and guidelines:\n\n";
29
43
  const PROJECT_CONTEXT_END = "\n</project_context>\n";
30
44
 
45
+ function getOwnMapValue<T>(
46
+ map: Record<string, T> | undefined,
47
+ key: string,
48
+ ): T | undefined {
49
+ return map && Object.hasOwn(map, key) ? map[key] : undefined;
50
+ }
51
+
31
52
  /**
32
53
  * Parent `getSystemPrompt()` is the fully assembled prompt, including the
33
54
  * parent's AGENTS.md files. A delegated session resolves resources for its own
@@ -111,24 +132,23 @@ export function validateTasks(
111
132
  );
112
133
  }
113
134
 
114
- // Scratch sessions are deliberately one-shot. This check uses the
115
- // effective workspace, so reviewer gets the same protection even when the
116
- // caller omits workspace. Explicit scratch is never silently promoted to
117
- // shared.
135
+ // Scratch and isolated sessions are deliberately one-shot. This check uses
136
+ // the effective workspace, so named agents get the same protection even
137
+ // when the caller omits workspace.
118
138
  for (const [index, task] of tasks.entries()) {
119
139
  const agent = task.agent
120
140
  ? (agents.get(task.agent) ?? BUILTIN_AGENT_CONFIGS[task.agent])
121
141
  : undefined;
122
142
  const workspace = task.workspace ?? agent?.workspace ?? "shared";
123
- const sessionAction = task.sessionAction ?? task.action;
143
+ const sessionAction = task.sessionAction;
124
144
  if (
125
- workspace === "scratch" &&
145
+ (workspace === "scratch" || workspace === "isolated") &&
126
146
  (task.sessionId || task.resumeFrom || sessionAction !== undefined)
127
147
  ) {
128
148
  const defaultText =
129
- task.workspace === undefined && agent?.workspace === "scratch"
130
- ? "defaults to workspace `scratch`"
131
- : "uses workspace `scratch`";
149
+ task.workspace === undefined && agent?.workspace === workspace
150
+ ? `defaults to workspace \`${workspace}\``
151
+ : `uses workspace \`${workspace}\``;
132
152
  const persistentAgent = task.agent ?? "agent";
133
153
  return noticeResult(
134
154
  `${formatTaskRef(index, task.id)}: Agent \`${persistentAgent}\` ${defaultText}, which is one-shot and cannot use \`sessionId\`, \`resumeFrom\`, or session actions. Set \`workspace: "shared"\` to use a persistent ${persistentAgent}.`,
@@ -195,6 +215,7 @@ export function resolveTasks(
195
215
  ctx: DelegateToolCtx,
196
216
  agents: Map<string, AgentConfig>,
197
217
  parentDefaults: ParentAgentDefaults,
218
+ dispatchConfig: DelegateConfig = getDelegateConfigSnapshot(),
198
219
  ): ResolvedTask[] {
199
220
  // Build parent transcript lazily — only computed once if any task uses with-parent-transcript
200
221
  let parentTranscript: string | null = null;
@@ -217,6 +238,12 @@ export function resolveTasks(
217
238
  ctx.getSystemPrompt?.(),
218
239
  );
219
240
 
241
+ // Modern overrides come from delegate.json. A one-release bridge below also
242
+ // reads legacy settings.json model/thinking values; modern values win
243
+ // field-by-field and legacy tools are never honored.
244
+ const agentOverrides = getAgentOverrides(dispatchConfig);
245
+ const overridesByParentModel = getAgentOverridesByParentModel(dispatchConfig);
246
+
220
247
  return tasks.map((t, i) => {
221
248
  const isDefaultAgent = t.agent === DEFAULT_AGENT_NAME;
222
249
  const agent = t.agent
@@ -225,18 +252,42 @@ export function resolveTasks(
225
252
  const isBuiltinAgent = agent?.builtin === true;
226
253
  const cwd = resolveCwd(t.cwd ?? ctx.cwd, ctx.cwd);
227
254
 
228
- // Load settings-based overrides for this agent
229
- const settings = loadDelegateSettings(cwd);
255
+ // A task can resolve to a different cwd than the parent; legacy
256
+ // `delegate` blocks in those project settings must be surfaced too.
257
+ warnLegacyDelegateSettingsMoved(cwd, (message) =>
258
+ ctx.ui?.notify(message, "warning"),
259
+ );
260
+ const legacySettings = loadDelegateSettings(cwd);
261
+
262
+ // delegate.json agent overrides for this agent. `default` bypasses them
263
+ // entirely (it mirrors the live parent by contract).
230
264
  const parentModelKey = ctx.model
231
265
  ? `${ctx.model.provider}/${ctx.model.id}`
232
266
  : undefined;
233
267
  const parentModelOverride =
234
268
  t.agent && !isDefaultAgent && parentModelKey
235
- ? settings?.agentOverridesByParentModel?.[parentModelKey]?.[t.agent]
269
+ ? getOwnMapValue(
270
+ getOwnMapValue(overridesByParentModel, parentModelKey),
271
+ t.agent,
272
+ )
236
273
  : undefined;
237
274
  const agentOverride =
238
- t.agent && !isDefaultAgent && settings?.agentOverrides?.[t.agent]
239
- ? settings.agentOverrides[t.agent]
275
+ t.agent && !isDefaultAgent
276
+ ? getOwnMapValue(agentOverrides, t.agent)
277
+ : undefined;
278
+ const legacyParentModelOverride =
279
+ t.agent && !isDefaultAgent && parentModelKey
280
+ ? getOwnMapValue(
281
+ getOwnMapValue(
282
+ legacySettings?.agentOverridesByParentModel,
283
+ parentModelKey,
284
+ ),
285
+ t.agent,
286
+ )
287
+ : undefined;
288
+ const legacyAgentOverride =
289
+ t.agent && !isDefaultAgent
290
+ ? getOwnMapValue(legacySettings?.agentOverrides, t.agent)
240
291
  : undefined;
241
292
 
242
293
  // Build system prompt. Explicit task prompts and named agent prompts
@@ -255,6 +306,10 @@ export function resolveTasks(
255
306
  warnings.push(
256
307
  "Scratch workspace: relative file changes run in a disposable CoW copy and are discarded.",
257
308
  );
309
+ } else if (workspace === "isolated") {
310
+ warnings.push(
311
+ "Isolated workspace: changes run in a detached Git worktree, then proposals are reconciled in task order. Applied changes remain unverified.",
312
+ );
258
313
  }
259
314
 
260
315
  // Prompt is required for fresh tasks. ResumeFrom provides context already.
@@ -284,9 +339,7 @@ export function resolveTasks(
284
339
  !agent?.explicitTools
285
340
  ) {
286
341
  const denied = new Set(agent.deniedTools);
287
- effectiveParentTools = parentNativeTools.filter(
288
- (t) => !denied.has(t),
289
- );
342
+ effectiveParentTools = parentNativeTools.filter((t) => !denied.has(t));
290
343
  }
291
344
  tools = resolveToolGroups(
292
345
  t.tools ??
@@ -302,15 +355,6 @@ export function resolveTasks(
302
355
  (isPoolHit ? pooledConfig?.tools : undefined) ??
303
356
  DEFAULT_TOOLS,
304
357
  );
305
- const unknownTools = tools.filter(
306
- (name) => !Object.hasOwn(TOOL_FACTORIES, name),
307
- );
308
- if (unknownTools.length) {
309
- warnings.push(
310
- `Unknown tool(s) ignored: ${unknownTools.join(", ")}. Available: ${Object.keys(TOOL_FACTORIES).join(", ")}`,
311
- );
312
- }
313
- tools = tools.filter((name) => Object.hasOwn(TOOL_FACTORIES, name));
314
358
  }
315
359
 
316
360
  // System prompt resolution. AgentSession's resource loader owns
@@ -334,14 +378,14 @@ export function resolveTasks(
334
378
  parentSystemPrompt,
335
379
  tools,
336
380
  });
337
- const requestedSystemPrompt = t.systemPrompt?.trim()
381
+ let requestedSystemPrompt = t.systemPrompt?.trim()
338
382
  ? t.systemPrompt
339
383
  : agent?.systemPrompt?.trim()
340
384
  ? agent.systemPrompt
341
385
  : isDefaultAgent
342
386
  ? resolvedBasePrompt
343
387
  : undefined;
344
- const systemPrompt = buildSubagentSystemPrompt({
388
+ let systemPrompt = buildSubagentSystemPrompt({
345
389
  taskSystemPrompt: t.systemPrompt,
346
390
  agentSystemPrompt: agent?.systemPrompt,
347
391
  parentSystemPrompt,
@@ -396,12 +440,19 @@ export function resolveTasks(
396
440
  ? (t.model ??
397
441
  parentModelOverride?.model ??
398
442
  agentOverride?.model ??
443
+ legacyParentModelOverride?.model ??
444
+ legacyAgentOverride?.model ??
399
445
  (agent?.explicitModel ? agent.model : undefined))
400
446
  : resolveModelSpec({
401
447
  taskModel:
402
- t.model ?? parentModelOverride?.model ?? agentOverride?.model,
448
+ t.model ??
449
+ parentModelOverride?.model ??
450
+ agentOverride?.model ??
451
+ legacyParentModelOverride?.model ??
452
+ legacyAgentOverride?.model,
403
453
  agentType,
404
454
  frontmatterModel: agent?.model,
455
+ config: dispatchConfig,
405
456
  });
406
457
 
407
458
  // A pool hit always runs its frozen model, but an explicitly requested
@@ -473,6 +524,8 @@ export function resolveTasks(
473
524
  ? (t.thinking ??
474
525
  parentModelOverride?.thinking ??
475
526
  agentOverride?.thinking ??
527
+ legacyParentModelOverride?.thinking ??
528
+ legacyAgentOverride?.thinking ??
476
529
  (agent?.explicitThinking ? agent.thinking : undefined) ??
477
530
  modelSuffix ??
478
531
  parentDefaults.thinking ??
@@ -481,13 +534,18 @@ export function resolveTasks(
481
534
  : (t.thinking ??
482
535
  parentModelOverride?.thinking ??
483
536
  agentOverride?.thinking ??
537
+ legacyParentModelOverride?.thinking ??
538
+ legacyAgentOverride?.thinking ??
484
539
  (agent?.explicitThinking ? agent.thinking : undefined) ??
485
540
  (isPoolHit ? pooledConfig?.thinking : undefined) ??
486
541
  modelSuffix ??
487
542
  parentDefaults.thinking ??
488
543
  "off")
489
544
  : (t.thinking ??
545
+ parentModelOverride?.thinking ??
490
546
  agentOverride?.thinking ??
547
+ legacyParentModelOverride?.thinking ??
548
+ legacyAgentOverride?.thinking ??
491
549
  agent?.thinking ??
492
550
  (isPoolHit ? pooledConfig?.thinking : undefined) ??
493
551
  modelSuffix ??
@@ -504,6 +562,36 @@ export function resolveTasks(
504
562
  );
505
563
  }
506
564
  }
565
+
566
+ const providerExtensionSources = getProviderExtensionSignature(
567
+ model?.provider,
568
+ dispatchConfig,
569
+ );
570
+
571
+ const availableTools = availableToolNames(model?.provider);
572
+ const availableToolSet = new Set(availableTools);
573
+ const unknownTools = tools.filter((name) => !availableToolSet.has(name));
574
+ if (unknownTools.length) {
575
+ warnings.push(
576
+ `Unknown tool(s) ignored: ${unknownTools.join(", ")}. Available: ${availableTools.join(", ")}`,
577
+ );
578
+ }
579
+ tools = tools.filter((name) => availableToolSet.has(name));
580
+ systemPrompt = buildSubagentSystemPrompt({
581
+ taskSystemPrompt: t.systemPrompt,
582
+ agentSystemPrompt: agent?.systemPrompt,
583
+ parentSystemPrompt,
584
+ pooledSystemPrompt: pooledConfig?.systemPrompt,
585
+ tools,
586
+ });
587
+ if (
588
+ isDefaultAgent &&
589
+ !t.systemPrompt?.trim() &&
590
+ !agent?.systemPrompt?.trim()
591
+ ) {
592
+ requestedSystemPrompt = systemPrompt;
593
+ }
594
+
507
595
  return {
508
596
  ...t,
509
597
  id: t.id,
@@ -524,6 +612,7 @@ export function resolveTasks(
524
612
  model: requestedModel,
525
613
  systemPrompt: requestedSystemPrompt,
526
614
  },
615
+ providerExtensionSources,
527
616
  };
528
617
  });
529
618
  }
package/telemetry.ts CHANGED
@@ -69,9 +69,16 @@ interface TelemetryBackend {
69
69
  close(): void;
70
70
  }
71
71
 
72
- let backend: TelemetryBackend | undefined;
73
- let backendGeneration: number | undefined;
74
- let backendFailed = false;
72
+ /** One backend follows the live config. A call captures its config, but if the
73
+ * path changes before a later row is written that row is deliberately dropped
74
+ * rather than retaining/reopening a stale database. Telemetry is best-effort;
75
+ * this small policy avoids a generation-keyed resource manager. */
76
+ let activeBackend: TelemetryBackend | undefined;
77
+ let activeBackendIdentity: string | undefined;
78
+ /** Prevent repeated open/write logs for the current live config. Reset when
79
+ * the config identity changes. */
80
+ let failedBackendIdentity: string | undefined;
81
+
75
82
  /** Monotonic runtime identity. A late worker from a previous runtime may not
76
83
  * write into the next runtime's backend after a bounded shutdown drain. */
77
84
  let telemetryGeneration = 0;
@@ -442,34 +449,51 @@ class RecorderBackend implements TelemetryBackend {
442
449
  close(): void {}
443
450
  }
444
451
 
445
- function disableBackend(operation: string, error: unknown): void {
446
- if (backendFailed) return;
447
- backendFailed = true;
448
- const failedBackend = backend;
449
- backend = undefined;
452
+ function backendIdentity(
453
+ config: import("./config.ts").TelemetryConfig,
454
+ ): string | undefined {
455
+ if (config.enabled === false) return undefined;
456
+ return config.dbPath ?? defaultDbPath();
457
+ }
458
+
459
+ function disableActiveBackend(
460
+ identity: string,
461
+ operation: string,
462
+ error: unknown,
463
+ ): void {
464
+ if (activeBackendIdentity !== identity) return;
465
+ const failedBackend = activeBackend;
466
+ activeBackend = undefined;
467
+ activeBackendIdentity = undefined;
468
+ failedBackendIdentity = identity;
450
469
  try {
451
470
  failedBackend?.close();
452
471
  } catch (closeError) {
453
472
  console.error("[delegate] telemetry backend close failed", closeError);
454
473
  }
455
474
  console.error(
456
- `[delegate] telemetry ${operation} failed; disabling telemetry`,
475
+ `[delegate] telemetry ${operation} failed; disabling telemetry until its config changes`,
457
476
  error,
458
477
  );
459
478
  }
460
479
 
461
- function openBackend(): TelemetryBackend | undefined {
462
- const config = getTelemetryConfig();
463
- if (config.enabled === false) return undefined;
464
- if (backendFailed || telemetryClosed) return undefined;
465
- if (testingRecorder) {
466
- return new RecorderBackend(testingRecorder, disableBackend);
480
+ function closeTelemetryBackend(): void {
481
+ try {
482
+ activeBackend?.close();
483
+ } catch (error) {
484
+ console.error("[delegate] telemetry backend close failed", error);
467
485
  }
486
+ activeBackend = undefined;
487
+ activeBackendIdentity = undefined;
488
+ failedBackendIdentity = undefined;
489
+ }
468
490
 
469
- if (!DatabaseSyncCtor) {
470
- backendFailed = true;
471
- return undefined;
472
- }
491
+ function openSqliteBackend(
492
+ config: import("./config.ts").TelemetryConfig,
493
+ onFailure: (operation: string, error: unknown) => void,
494
+ ): TelemetryBackend | undefined {
495
+ if (config.enabled === false || telemetryClosed) return undefined;
496
+ if (!DatabaseSyncCtor) return undefined;
473
497
 
474
498
  const dbPath = config.dbPath ?? defaultDbPath();
475
499
  let db: DatabaseSync | undefined;
@@ -484,41 +508,78 @@ function openBackend(): TelemetryBackend | undefined {
484
508
  db.exec(`PRAGMA busy_timeout = ${SQLITE_BUSY_TIMEOUT_MS};`);
485
509
  db.exec("PRAGMA journal_mode = WAL;");
486
510
  initSchema(db);
487
- return new SqliteTelemetryBackend(db, disableBackend);
511
+ return new SqliteTelemetryBackend(db, onFailure);
488
512
  } catch (error) {
489
513
  try {
490
514
  db?.close();
491
515
  } catch (closeError) {
492
516
  console.error("[delegate] telemetry database close failed", closeError);
493
517
  }
494
- backendFailed = true;
495
518
  console.error("[delegate] telemetry database failed to open", error);
496
519
  return undefined;
497
520
  }
498
521
  }
499
522
 
500
- function getBackend(generation?: number): TelemetryBackend | undefined {
523
+ function getBackendForConfig(
524
+ generation: number,
525
+ config: import("./config.ts").TelemetryConfig,
526
+ ): TelemetryBackend | undefined {
501
527
  if (telemetryClosed) return undefined;
502
- if (generation !== undefined && generation !== telemetryGeneration) {
528
+ if (generation !== telemetryGeneration) return undefined;
529
+ const capturedIdentity = backendIdentity(config);
530
+ const liveConfig = getTelemetryConfig();
531
+ const liveIdentity = backendIdentity(liveConfig);
532
+
533
+ // A hot reload changed the destination. Drop every old-identity span's
534
+ // remaining rows, including concurrent spans, and release the one live
535
+ // handle; the next call opens the new destination. Telemetry is best-effort,
536
+ // so retaining/refcounting obsolete backends is deliberately out of scope.
537
+ if (capturedIdentity !== liveIdentity) {
538
+ if (activeBackend && activeBackendIdentity !== liveIdentity) {
539
+ closeTelemetryBackend();
540
+ } else if (failedBackendIdentity !== liveIdentity) {
541
+ failedBackendIdentity = undefined;
542
+ }
503
543
  return undefined;
504
544
  }
505
- if (backend) {
506
- if (backendGeneration !== telemetryGeneration) {
507
- // A previous runtime left its handle open while its bounded shutdown
508
- // drain timed out. Dispose that stale handle before opening this one.
509
- const stale = backend;
510
- backend = undefined;
511
- backendGeneration = undefined;
512
- stale.close();
513
- } else if (generation !== undefined && backendGeneration !== generation) {
514
- return undefined;
515
- } else {
516
- return backend;
517
- }
545
+ if (liveIdentity === undefined) {
546
+ if (activeBackend || failedBackendIdentity) closeTelemetryBackend();
547
+ return undefined;
548
+ }
549
+
550
+ if (activeBackend && activeBackendIdentity !== liveIdentity) {
551
+ closeTelemetryBackend();
552
+ }
553
+ if (failedBackendIdentity !== liveIdentity) failedBackendIdentity = undefined;
554
+ if (failedBackendIdentity === liveIdentity) return undefined;
555
+ if (activeBackend) return activeBackend;
556
+
557
+ activeBackendIdentity = liveIdentity;
558
+ activeBackend = testingRecorder
559
+ ? new RecorderBackend(testingRecorder, (operation, error) =>
560
+ disableActiveBackend(liveIdentity, operation, error),
561
+ )
562
+ : openSqliteBackend(liveConfig, (operation, error) =>
563
+ disableActiveBackend(liveIdentity, operation, error),
564
+ );
565
+ if (!activeBackend) {
566
+ // This includes Bun's intentional no-node:sqlite path. No log is needed
567
+ // there; real SQLite open failures were already logged at the boundary.
568
+ activeBackendIdentity = undefined;
569
+ failedBackendIdentity = liveIdentity;
518
570
  }
519
- backend = openBackend();
520
- if (backend) backendGeneration = telemetryGeneration;
521
- return backend;
571
+ return activeBackend;
572
+ }
573
+
574
+ /** Legacy unpinned backend lookup: uses the *live* telemetry config. Callers
575
+ * that need a stable backend for the lifetime of a span/task should use
576
+ * `getBackendForConfig` with the config captured at creation. */
577
+ function getBackend(generation?: number): TelemetryBackend | undefined {
578
+ if (telemetryClosed) return undefined;
579
+ return getBackendForConfig(
580
+ generation ?? telemetryGeneration,
581
+ getTelemetryConfig(),
582
+ );
522
583
  }
523
584
 
524
585
  export interface CallSpanInput {
@@ -540,6 +601,8 @@ export interface CallSpan {
540
601
  readonly startedAt: number;
541
602
  /** Runtime generation captured at call creation; stale calls cannot write. */
542
603
  readonly generation: number;
604
+ /** Telemetry config captured at call creation; binds the span to one backend. */
605
+ readonly telemetryConfig: import("./config.ts").TelemetryConfig;
543
606
  /** Snapshot of the call row as it would be written at spawn. */
544
607
  baseRecord(): CallRecord;
545
608
  spawn(): void;
@@ -550,7 +613,9 @@ class CallSpanImpl implements CallSpan {
550
613
  readonly id: string;
551
614
  readonly startedAt: number;
552
615
  readonly generation = telemetryGeneration;
616
+ readonly telemetryConfig = getTelemetryConfig();
553
617
  private readonly input: CallSpanInput;
618
+ private finished = false;
554
619
 
555
620
  constructor(input: CallSpanInput) {
556
621
  this.id = crypto.randomUUID();
@@ -576,13 +641,15 @@ class CallSpanImpl implements CallSpan {
576
641
  }
577
642
 
578
643
  spawn(): void {
579
- const b = getBackend(this.generation);
644
+ const b = getBackendForConfig(this.generation, this.telemetryConfig);
580
645
  if (!b) return;
581
646
  b.recordCall(this.baseRecord());
582
647
  }
583
648
 
584
649
  finish(finish: CallSpanFinish): void {
585
- const b = getBackend(this.generation);
650
+ if (this.finished) return;
651
+ this.finished = true;
652
+ const b = getBackendForConfig(this.generation, this.telemetryConfig);
586
653
  if (!b) return;
587
654
  const record = this.baseRecord();
588
655
  record.wall_ms = finish.wallMs;
@@ -597,8 +664,14 @@ export function beginCall(input: CallSpanInput): CallSpan {
597
664
  return new CallSpanImpl(input);
598
665
  }
599
666
 
600
- export function recordCall(record: CallRecord, generation?: number): void {
601
- const b = getBackend(generation);
667
+ export function recordCall(
668
+ record: CallRecord,
669
+ generation?: number,
670
+ config?: import("./config.ts").TelemetryConfig,
671
+ ): void {
672
+ const b = config
673
+ ? getBackendForConfig(generation ?? telemetryGeneration, config)
674
+ : getBackend(generation);
602
675
  if (!b) return;
603
676
  b.recordCall(record);
604
677
  }
@@ -609,6 +682,9 @@ export interface TaskSpanInput {
609
682
  callId: string;
610
683
  /** Runtime generation captured by the dispatch that owns this task. */
611
684
  generation?: number;
685
+ /** Telemetry config captured at dispatch; binds the task row to the same
686
+ * backend as the call span. */
687
+ telemetryConfig?: import("./config.ts").TelemetryConfig;
612
688
  async: boolean;
613
689
  taskIndex: number;
614
690
  task: ResolvedTask;
@@ -625,7 +701,12 @@ function outcomeFromResult(result: TaskResult): string {
625
701
  }
626
702
 
627
703
  export function recordTask(input: TaskSpanInput): string | undefined {
628
- const b = getBackend(input.generation);
704
+ const b = input.telemetryConfig
705
+ ? getBackendForConfig(
706
+ input.generation ?? telemetryGeneration,
707
+ input.telemetryConfig,
708
+ )
709
+ : getBackend(input.generation);
629
710
  if (!b) return undefined;
630
711
 
631
712
  const { callId, async, taskIndex, task, progress, result, retries } = input;
@@ -671,10 +752,7 @@ export function sealTelemetryWrites(expectedGeneration?: number): boolean {
671
752
  }
672
753
  telemetryGeneration++;
673
754
  telemetryClosed = true;
674
- const current = backend;
675
- backend = undefined;
676
- backendGeneration = undefined;
677
- current?.close();
755
+ closeTelemetryBackend();
678
756
  return true;
679
757
  }
680
758
 
@@ -689,10 +767,7 @@ export function closeTelemetry(expectedGeneration?: number): void {
689
767
  return;
690
768
  }
691
769
  telemetryClosed = true;
692
- const current = backend;
693
- backend = undefined;
694
- backendGeneration = undefined;
695
- current?.close();
770
+ closeTelemetryBackend();
696
771
  }
697
772
 
698
773
  /** Current runtime identity for lifecycle owners such as session_shutdown. */
@@ -707,10 +782,7 @@ export function prepareTelemetryForSession(): void {
707
782
  // A timed-out old runtime may have left its handle open. Close it before
708
783
  // advancing the generation so the old shutdown handler cannot close the new
709
784
  // runtime's backend later.
710
- const stale = backend;
711
- backend = undefined;
712
- backendGeneration = undefined;
713
- stale?.close();
785
+ closeTelemetryBackend();
714
786
 
715
787
  telemetryGeneration++;
716
788
  telemetryClosed = false;
@@ -719,25 +791,20 @@ export function prepareTelemetryForSession(): void {
719
791
  export function _setTelemetryForTesting(
720
792
  recorder: TelemetryRecorder | undefined,
721
793
  ): void {
722
- if (backend) {
723
- backend.close();
724
- backend = undefined;
725
- backendGeneration = undefined;
726
- }
794
+ closeTelemetryBackend();
727
795
  testingRecorder = recorder;
728
- backendFailed = false;
729
796
  telemetryGeneration++;
730
797
  telemetryClosed = false;
731
798
  }
732
799
 
733
800
  export function _resetTelemetryForTesting(): void {
734
801
  testingRecorder = undefined;
735
- if (backend) {
736
- backend.close();
737
- backend = undefined;
738
- backendGeneration = undefined;
739
- }
740
- backendFailed = false;
802
+ closeTelemetryBackend();
741
803
  telemetryGeneration++;
742
804
  telemetryClosed = false;
743
805
  }
806
+
807
+ /** @internal Test seam exposing the single live backend path, never handles. */
808
+ export function _getTelemetryBackendPathsForTesting(): string[] {
809
+ return activeBackendIdentity === undefined ? [] : [activeBackendIdentity];
810
+ }