@bermudi/pi-delegate 0.1.11 → 0.1.13
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 +8 -1
- package/dispatch.ts +410 -38
- package/extension.ts +14 -0
- package/format.ts +50 -4
- package/host.ts +6 -1
- package/isolated-workspace.ts +857 -0
- package/lifecycle.ts +50 -16
- package/manual.ts +13 -9
- package/package.json +1 -1
- package/pool.ts +23 -1
- package/provider-extensions.ts +11 -2
- package/render-branches.ts +30 -0
- package/render-result.ts +8 -5
- package/runner.ts +3 -1
- package/schema.ts +58 -51
- package/settings.ts +202 -84
- package/shared-write-safety.ts +273 -0
- package/task-resolution.ts +87 -16
- package/telemetry.ts +135 -68
- package/ticket-format.ts +13 -5
- package/tickets.ts +23 -11
- package/types.ts +79 -0
package/task-resolution.ts
CHANGED
|
@@ -16,9 +16,19 @@ import { isSessionBusy } from "./tickets.ts";
|
|
|
16
16
|
import { BUILTIN_AGENT_CONFIGS, buildSubagentSystemPrompt } from "./agents.ts";
|
|
17
17
|
import { buildParentTranscript } from "./parent-context.ts";
|
|
18
18
|
import { findAvailableAlternative, resolveModelRequest } from "./model.ts";
|
|
19
|
-
import {
|
|
20
|
-
|
|
19
|
+
import {
|
|
20
|
+
getAgentOverrides,
|
|
21
|
+
getAgentOverridesByParentModel,
|
|
22
|
+
resolveModelSpec,
|
|
23
|
+
getDelegateConfigSnapshot,
|
|
24
|
+
getProviderExtensionSignature,
|
|
25
|
+
} from "./config.ts";
|
|
26
|
+
import type { DelegateConfig } from "./config.ts";
|
|
21
27
|
import { resolveCwd } from "./utils.ts";
|
|
28
|
+
import {
|
|
29
|
+
loadDelegateSettings,
|
|
30
|
+
warnLegacyDelegateSettingsMoved,
|
|
31
|
+
} from "./settings.ts";
|
|
22
32
|
import type {
|
|
23
33
|
AgentConfig,
|
|
24
34
|
DelegateToolCtx,
|
|
@@ -32,6 +42,13 @@ const PROJECT_CONTEXT_START =
|
|
|
32
42
|
"\n\n<project_context>\n\nProject-specific instructions and guidelines:\n\n";
|
|
33
43
|
const PROJECT_CONTEXT_END = "\n</project_context>\n";
|
|
34
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
|
+
|
|
35
52
|
/**
|
|
36
53
|
* Parent `getSystemPrompt()` is the fully assembled prompt, including the
|
|
37
54
|
* parent's AGENTS.md files. A delegated session resolves resources for its own
|
|
@@ -115,10 +132,9 @@ export function validateTasks(
|
|
|
115
132
|
);
|
|
116
133
|
}
|
|
117
134
|
|
|
118
|
-
// Scratch sessions are deliberately one-shot. This check uses
|
|
119
|
-
// effective workspace, so
|
|
120
|
-
// caller omits workspace.
|
|
121
|
-
// 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.
|
|
122
138
|
for (const [index, task] of tasks.entries()) {
|
|
123
139
|
const agent = task.agent
|
|
124
140
|
? (agents.get(task.agent) ?? BUILTIN_AGENT_CONFIGS[task.agent])
|
|
@@ -126,13 +142,13 @@ export function validateTasks(
|
|
|
126
142
|
const workspace = task.workspace ?? agent?.workspace ?? "shared";
|
|
127
143
|
const sessionAction = task.sessionAction;
|
|
128
144
|
if (
|
|
129
|
-
workspace === "scratch" &&
|
|
145
|
+
(workspace === "scratch" || workspace === "isolated") &&
|
|
130
146
|
(task.sessionId || task.resumeFrom || sessionAction !== undefined)
|
|
131
147
|
) {
|
|
132
148
|
const defaultText =
|
|
133
|
-
task.workspace === undefined && agent?.workspace ===
|
|
134
|
-
?
|
|
135
|
-
:
|
|
149
|
+
task.workspace === undefined && agent?.workspace === workspace
|
|
150
|
+
? `defaults to workspace \`${workspace}\``
|
|
151
|
+
: `uses workspace \`${workspace}\``;
|
|
136
152
|
const persistentAgent = task.agent ?? "agent";
|
|
137
153
|
return noticeResult(
|
|
138
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}.`,
|
|
@@ -199,6 +215,7 @@ export function resolveTasks(
|
|
|
199
215
|
ctx: DelegateToolCtx,
|
|
200
216
|
agents: Map<string, AgentConfig>,
|
|
201
217
|
parentDefaults: ParentAgentDefaults,
|
|
218
|
+
dispatchConfig: DelegateConfig = getDelegateConfigSnapshot(),
|
|
202
219
|
): ResolvedTask[] {
|
|
203
220
|
// Build parent transcript lazily — only computed once if any task uses with-parent-transcript
|
|
204
221
|
let parentTranscript: string | null = null;
|
|
@@ -221,6 +238,12 @@ export function resolveTasks(
|
|
|
221
238
|
ctx.getSystemPrompt?.(),
|
|
222
239
|
);
|
|
223
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
|
+
|
|
224
247
|
return tasks.map((t, i) => {
|
|
225
248
|
const isDefaultAgent = t.agent === DEFAULT_AGENT_NAME;
|
|
226
249
|
const agent = t.agent
|
|
@@ -229,18 +252,42 @@ export function resolveTasks(
|
|
|
229
252
|
const isBuiltinAgent = agent?.builtin === true;
|
|
230
253
|
const cwd = resolveCwd(t.cwd ?? ctx.cwd, ctx.cwd);
|
|
231
254
|
|
|
232
|
-
//
|
|
233
|
-
|
|
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).
|
|
234
264
|
const parentModelKey = ctx.model
|
|
235
265
|
? `${ctx.model.provider}/${ctx.model.id}`
|
|
236
266
|
: undefined;
|
|
237
267
|
const parentModelOverride =
|
|
238
268
|
t.agent && !isDefaultAgent && parentModelKey
|
|
239
|
-
?
|
|
269
|
+
? getOwnMapValue(
|
|
270
|
+
getOwnMapValue(overridesByParentModel, parentModelKey),
|
|
271
|
+
t.agent,
|
|
272
|
+
)
|
|
240
273
|
: undefined;
|
|
241
274
|
const agentOverride =
|
|
242
|
-
t.agent && !isDefaultAgent
|
|
243
|
-
?
|
|
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)
|
|
244
291
|
: undefined;
|
|
245
292
|
|
|
246
293
|
// Build system prompt. Explicit task prompts and named agent prompts
|
|
@@ -259,6 +306,10 @@ export function resolveTasks(
|
|
|
259
306
|
warnings.push(
|
|
260
307
|
"Scratch workspace: relative file changes run in a disposable CoW copy and are discarded.",
|
|
261
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
|
+
);
|
|
262
313
|
}
|
|
263
314
|
|
|
264
315
|
// Prompt is required for fresh tasks. ResumeFrom provides context already.
|
|
@@ -389,12 +440,19 @@ export function resolveTasks(
|
|
|
389
440
|
? (t.model ??
|
|
390
441
|
parentModelOverride?.model ??
|
|
391
442
|
agentOverride?.model ??
|
|
443
|
+
legacyParentModelOverride?.model ??
|
|
444
|
+
legacyAgentOverride?.model ??
|
|
392
445
|
(agent?.explicitModel ? agent.model : undefined))
|
|
393
446
|
: resolveModelSpec({
|
|
394
447
|
taskModel:
|
|
395
|
-
t.model ??
|
|
448
|
+
t.model ??
|
|
449
|
+
parentModelOverride?.model ??
|
|
450
|
+
agentOverride?.model ??
|
|
451
|
+
legacyParentModelOverride?.model ??
|
|
452
|
+
legacyAgentOverride?.model,
|
|
396
453
|
agentType,
|
|
397
454
|
frontmatterModel: agent?.model,
|
|
455
|
+
config: dispatchConfig,
|
|
398
456
|
});
|
|
399
457
|
|
|
400
458
|
// A pool hit always runs its frozen model, but an explicitly requested
|
|
@@ -466,6 +524,8 @@ export function resolveTasks(
|
|
|
466
524
|
? (t.thinking ??
|
|
467
525
|
parentModelOverride?.thinking ??
|
|
468
526
|
agentOverride?.thinking ??
|
|
527
|
+
legacyParentModelOverride?.thinking ??
|
|
528
|
+
legacyAgentOverride?.thinking ??
|
|
469
529
|
(agent?.explicitThinking ? agent.thinking : undefined) ??
|
|
470
530
|
modelSuffix ??
|
|
471
531
|
parentDefaults.thinking ??
|
|
@@ -474,13 +534,18 @@ export function resolveTasks(
|
|
|
474
534
|
: (t.thinking ??
|
|
475
535
|
parentModelOverride?.thinking ??
|
|
476
536
|
agentOverride?.thinking ??
|
|
537
|
+
legacyParentModelOverride?.thinking ??
|
|
538
|
+
legacyAgentOverride?.thinking ??
|
|
477
539
|
(agent?.explicitThinking ? agent.thinking : undefined) ??
|
|
478
540
|
(isPoolHit ? pooledConfig?.thinking : undefined) ??
|
|
479
541
|
modelSuffix ??
|
|
480
542
|
parentDefaults.thinking ??
|
|
481
543
|
"off")
|
|
482
544
|
: (t.thinking ??
|
|
545
|
+
parentModelOverride?.thinking ??
|
|
483
546
|
agentOverride?.thinking ??
|
|
547
|
+
legacyParentModelOverride?.thinking ??
|
|
548
|
+
legacyAgentOverride?.thinking ??
|
|
484
549
|
agent?.thinking ??
|
|
485
550
|
(isPoolHit ? pooledConfig?.thinking : undefined) ??
|
|
486
551
|
modelSuffix ??
|
|
@@ -498,6 +563,11 @@ export function resolveTasks(
|
|
|
498
563
|
}
|
|
499
564
|
}
|
|
500
565
|
|
|
566
|
+
const providerExtensionSources = getProviderExtensionSignature(
|
|
567
|
+
model?.provider,
|
|
568
|
+
dispatchConfig,
|
|
569
|
+
);
|
|
570
|
+
|
|
501
571
|
const availableTools = availableToolNames(model?.provider);
|
|
502
572
|
const availableToolSet = new Set(availableTools);
|
|
503
573
|
const unknownTools = tools.filter((name) => !availableToolSet.has(name));
|
|
@@ -542,6 +612,7 @@ export function resolveTasks(
|
|
|
542
612
|
model: requestedModel,
|
|
543
613
|
systemPrompt: requestedSystemPrompt,
|
|
544
614
|
},
|
|
615
|
+
providerExtensionSources,
|
|
545
616
|
};
|
|
546
617
|
});
|
|
547
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
|
+
}
|
package/ticket-format.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
formatTouchedOverlapWarning,
|
|
13
13
|
} from "./format.ts";
|
|
14
14
|
import { renderOutputForPoll } from "./spill.ts";
|
|
15
|
+
import { getOutputSpillTail } from "./config.ts";
|
|
15
16
|
import type {
|
|
16
17
|
AsyncTicket,
|
|
17
18
|
DelegateDetails,
|
|
@@ -111,18 +112,20 @@ function appendTouchedMeta(
|
|
|
111
112
|
}
|
|
112
113
|
|
|
113
114
|
function formatSettledPollLines(
|
|
115
|
+
ticket: AsyncTicket,
|
|
114
116
|
result: TaskResult,
|
|
115
117
|
task: ResolvedTask,
|
|
116
118
|
failed: boolean,
|
|
117
119
|
): string[] {
|
|
118
120
|
const meta = taskMetaBase(result);
|
|
119
121
|
appendTouchedMeta(meta, result, task);
|
|
122
|
+
const tailChars = getOutputSpillTail(ticket.config);
|
|
120
123
|
if (!failed) {
|
|
121
124
|
const lines = [
|
|
122
125
|
`✓ ${result.agent}${formatTaskId(result.id)} · ${meta.join(" · ")}`,
|
|
123
126
|
];
|
|
124
127
|
if (result.output && result.output !== "(no output)") {
|
|
125
|
-
lines.push(renderOutputForPoll(result.output));
|
|
128
|
+
lines.push(renderOutputForPoll(result.output, { tailChars }));
|
|
126
129
|
}
|
|
127
130
|
return lines;
|
|
128
131
|
}
|
|
@@ -133,7 +136,7 @@ function formatSettledPollLines(
|
|
|
133
136
|
if (result.sessionFile)
|
|
134
137
|
lines.push(` session: ${shortenPath(result.sessionFile)}`);
|
|
135
138
|
if (result.output && result.output !== "(no output)") {
|
|
136
|
-
lines.push(renderOutputForPoll(result.output));
|
|
139
|
+
lines.push(renderOutputForPoll(result.output, { tailChars }));
|
|
137
140
|
}
|
|
138
141
|
return lines;
|
|
139
142
|
}
|
|
@@ -146,13 +149,13 @@ function formatPollTaskLines(
|
|
|
146
149
|
const r = ticket.results[index];
|
|
147
150
|
if (p.status === "done" && r) {
|
|
148
151
|
return {
|
|
149
|
-
lines: formatSettledPollLines(r, ticket.resolved[index]!, false),
|
|
152
|
+
lines: formatSettledPollLines(ticket, r, ticket.resolved[index]!, false),
|
|
150
153
|
result: r,
|
|
151
154
|
};
|
|
152
155
|
}
|
|
153
156
|
if (p.status === "failed" && r) {
|
|
154
157
|
return {
|
|
155
|
-
lines: formatSettledPollLines(r, ticket.resolved[index]!, true),
|
|
158
|
+
lines: formatSettledPollLines(ticket, r, ticket.resolved[index]!, true),
|
|
156
159
|
result: r,
|
|
157
160
|
};
|
|
158
161
|
}
|
|
@@ -237,10 +240,15 @@ export function formatLiveTicketPoll(
|
|
|
237
240
|
findTouchedOverlaps(completedForOverlap),
|
|
238
241
|
);
|
|
239
242
|
const guidance = liveTicketGuidance(ticket);
|
|
243
|
+
const dispatchWarning = ticket.dispatchWarning
|
|
244
|
+
? `WARNING: ${ticket.dispatchWarning}`
|
|
245
|
+
: "";
|
|
240
246
|
return {
|
|
241
247
|
text: `${formatLiveTicketHeader(ticket, now)}\n${lines.join("\n")}${
|
|
242
248
|
guidance ? `\n\n${guidance}` : ""
|
|
243
|
-
}${
|
|
249
|
+
}${dispatchWarning ? `\n\n${dispatchWarning}` : ""}${
|
|
250
|
+
overlapWarning ? `\n\n${overlapWarning}` : ""
|
|
251
|
+
}`,
|
|
244
252
|
completedResults,
|
|
245
253
|
overlapWarning,
|
|
246
254
|
};
|