@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.
@@ -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 { resolveModelSpec } from "./config.ts";
20
- 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";
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 the
119
- // effective workspace, so reviewer gets the same protection even when the
120
- // caller omits workspace. Explicit scratch is never silently promoted to
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 === "scratch"
134
- ? "defaults to workspace `scratch`"
135
- : "uses workspace `scratch`";
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
- // Load settings-based overrides for this agent
233
- 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).
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
- ? settings?.agentOverridesByParentModel?.[parentModelKey]?.[t.agent]
269
+ ? getOwnMapValue(
270
+ getOwnMapValue(overridesByParentModel, parentModelKey),
271
+ t.agent,
272
+ )
240
273
  : undefined;
241
274
  const agentOverride =
242
- t.agent && !isDefaultAgent && settings?.agentOverrides?.[t.agent]
243
- ? 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)
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 ?? parentModelOverride?.model ?? agentOverride?.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
- 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
+ }
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
- }${overlapWarning ? `\n\n${overlapWarning}` : ""}`,
249
+ }${dispatchWarning ? `\n\n${dispatchWarning}` : ""}${
250
+ overlapWarning ? `\n\n${overlapWarning}` : ""
251
+ }`,
244
252
  completedResults,
245
253
  overlapWarning,
246
254
  };