@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.
- package/README.md +57 -4
- package/concurrency.ts +55 -16
- package/config.ts +584 -50
- package/delegate.ts +11 -3
- package/dispatch.ts +436 -65
- package/extension.ts +16 -4
- package/format.ts +52 -6
- package/host-cache.ts +70 -0
- package/host.ts +169 -811
- package/isolated-workspace.ts +857 -0
- package/lifecycle.ts +699 -520
- package/manual.ts +4 -7
- package/package.json +1 -1
- package/pi-package-source.ts +293 -0
- package/pool.ts +23 -1
- package/provider-extensions.ts +537 -0
- package/quiescence.ts +262 -0
- package/render-branches.ts +30 -0
- package/render-result.ts +8 -5
- package/runner.ts +35 -141
- package/schema.ts +133 -202
- package/settings.ts +202 -84
- package/shared-write-safety.ts +273 -0
- package/task-resolution.ts +121 -32
- package/telemetry.ts +135 -68
- package/ticket-format.ts +331 -0
- package/tickets.ts +101 -258
- package/tools.ts +12 -0
- package/trusted-paths.ts +71 -0
- package/types.ts +83 -16
package/task-resolution.ts
CHANGED
|
@@ -6,15 +6,29 @@ import {
|
|
|
6
6
|
DEFAULT_TOOLS,
|
|
7
7
|
VALID_THINKING,
|
|
8
8
|
} from "./constants.ts";
|
|
9
|
-
import {
|
|
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 {
|
|
16
|
-
|
|
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
|
|
115
|
-
// effective workspace, so
|
|
116
|
-
// caller omits workspace.
|
|
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
|
|
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 ===
|
|
130
|
-
?
|
|
131
|
-
:
|
|
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
|
-
//
|
|
229
|
-
|
|
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
|
-
?
|
|
269
|
+
? getOwnMapValue(
|
|
270
|
+
getOwnMapValue(overridesByParentModel, parentModelKey),
|
|
271
|
+
t.agent,
|
|
272
|
+
)
|
|
236
273
|
: undefined;
|
|
237
274
|
const agentOverride =
|
|
238
|
-
t.agent && !isDefaultAgent
|
|
239
|
-
?
|
|
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
|
-
|
|
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
|
-
|
|
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 ??
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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,
|
|
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
|
|
523
|
+
function getBackendForConfig(
|
|
524
|
+
generation: number,
|
|
525
|
+
config: import("./config.ts").TelemetryConfig,
|
|
526
|
+
): TelemetryBackend | undefined {
|
|
501
527
|
if (telemetryClosed) return undefined;
|
|
502
|
-
if (generation !==
|
|
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 (
|
|
506
|
-
if (
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
601
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|