pi-background-tasks 2.1.3 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/BACKGROUND-TASKS-INSTRUCTIONS.md +1 -1
  2. package/PUBLISHING.md +2 -0
  3. package/README.md +16 -9
  4. package/TESTING.md +15 -10
  5. package/TEST_PLAN.md +12 -11
  6. package/THIRD_PARTY_NOTICES.md +30 -0
  7. package/docs/INDEX.md +8 -4
  8. package/docs/choose-a-workflow.md +5 -2
  9. package/docs/commands/claude-cache.md +50 -0
  10. package/docs/getting-started.md +3 -0
  11. package/docs/manifest.json +84 -19
  12. package/docs/operations/configuration.md +15 -1
  13. package/docs/operations/releasing.md +6 -3
  14. package/docs/read-before-edit.md +4 -1
  15. package/docs/reference/runtime-contracts.md +72 -71
  16. package/docs/subsystems/anthropic-attribution.md +63 -0
  17. package/docs/subsystems/attested-pi-runs.md +2 -2
  18. package/docs/subsystems/child-launch-durability-and-safety.md +3 -3
  19. package/docs/subsystems/delegation.md +12 -4
  20. package/docs/subsystems/docs-freshness-gate.md +6 -6
  21. package/docs/subsystems/fusion.md +6 -2
  22. package/docs/tools/bg_delegate.md +21 -9
  23. package/docs/tools/bg_result.md +8 -1
  24. package/docs/tools/bg_run.md +5 -0
  25. package/extensions/anthropic-attribution.ts +1 -0
  26. package/package.json +4 -2
  27. package/src/core/anthropic-attribution-path.ts +26 -0
  28. package/src/core/{fusion/anthropic-attribution.ts → anthropic-attribution.ts} +61 -8
  29. package/src/core/attested-pi-run.ts +10 -1
  30. package/src/core/common.ts +2 -1
  31. package/src/core/delegate/artifacts.ts +4 -0
  32. package/src/core/delegate/launch.ts +44 -14
  33. package/src/core/delegate/runner.ts +24 -0
  34. package/src/core/delegate/seed.ts +12 -0
  35. package/src/core/delegate/types.ts +10 -2
  36. package/src/core/fusion/artifacts.ts +265 -3
  37. package/src/core/fusion/config.ts +1 -1
  38. package/src/core/fusion/orchestrator.ts +47 -57
  39. package/src/core/fusion/pi-child.ts +7 -124
  40. package/src/core/fusion/result-package.ts +550 -3
  41. package/src/core/fusion/types.ts +87 -0
  42. package/src/core/registry.ts +4 -1
  43. package/src/delegate-child-extension.ts +9 -2
  44. package/src/delegate-extension.ts +119 -9
@@ -30,6 +30,8 @@ export const FUSION_CALIBRATION_VIOLATION_SCHEMA_VERSION =
30
30
  export const FUSION_VALIDATE_CANDIDATE_CONTRACT_EVENT_SCHEMA_VERSION =
31
31
  'pi-background-tasks.fusion-validation-candidate-contract-event.v1';
32
32
  export const FUSION_TOOL_CALL_LOG_SCHEMA_VERSION = 'pi-background-tasks.fusion-tool-call.v1';
33
+ export const FUSION_FAILURE_SUMMARY_SCHEMA_VERSION =
34
+ 'pi-background-tasks.fusion-failure-summary.v1';
33
35
 
34
36
  /**
35
37
  * Conversation-projection transform shared by every Fusion entry point.
@@ -877,6 +879,91 @@ export interface FusionArtifactRef {
877
879
  sha256: string;
878
880
  }
879
881
 
882
+ export type FusionFailureArtifactClassification =
883
+ | 'complete_stage_output'
884
+ | 'partial_stage_output'
885
+ | 'oversized_original'
886
+ | 'empty_rejected_output'
887
+ | 'evidence_only';
888
+
889
+ export interface FusionFailureEvidenceArtifact {
890
+ name: string;
891
+ classification: FusionFailureArtifactClassification;
892
+ ref: FusionArtifactRef;
893
+ }
894
+
895
+ export interface FusionFailureAttemptMetadata {
896
+ stage: FusionStage;
897
+ slot?: 1 | 2 | 3 | undefined;
898
+ attempt: number;
899
+ status: 'completed' | 'failed' | 'cancelled';
900
+ child_created: boolean;
901
+ }
902
+
903
+ export interface FusionFailureList<T> {
904
+ listed: readonly T[];
905
+ omitted_count: number;
906
+ }
907
+
908
+ export interface FusionFailureMessageMetadata {
909
+ byte_length: number;
910
+ sha256: string;
911
+ inline_message?: string | undefined;
912
+ omission_reason?: 'exceeds_inline_message_bytes_cap' | 'result_view_byte_budget' | undefined;
913
+ }
914
+
915
+ export interface FusionFailureSummaryV1 {
916
+ schema_version: typeof FUSION_FAILURE_SUMMARY_SCHEMA_VERSION;
917
+ run_id: string;
918
+ workflow: FusionWorkflowId;
919
+ source: FusionSource;
920
+ terminal_state: Exclude<FusionTerminalState, 'completed'>;
921
+ created_at: string;
922
+ answer: { present: false; reason: 'run_did_not_commit' };
923
+ failure: {
924
+ code: FusionErrorCode | null;
925
+ stage?: FusionStage | undefined;
926
+ slot?: 1 | 2 | 3 | undefined;
927
+ attempt?: number | undefined;
928
+ child_created: boolean;
929
+ message: FusionFailureMessageMetadata;
930
+ };
931
+ progress: FusionRunProgress;
932
+ usage_so_far: FusionUsage;
933
+ attempts: FusionFailureList<FusionFailureAttemptMetadata>;
934
+ evidence_artifacts: FusionFailureList<FusionFailureEvidenceArtifact>;
935
+ remediation_ids: readonly (
936
+ | 'inspect_manifest_bound_evidence'
937
+ | 'inspect_terminal_error'
938
+ | 'split_or_reduce_work'
939
+ | 'retry_same_route_after_operator_review'
940
+ )[];
941
+ }
942
+
943
+ export interface FusionFailureViewFailure {
944
+ code?: FusionErrorCode | null | undefined;
945
+ stage?: FusionStage | undefined;
946
+ slot?: 1 | 2 | 3 | undefined;
947
+ attempt?: number | undefined;
948
+ child_created?: boolean | undefined;
949
+ message: FusionFailureMessageMetadata;
950
+ }
951
+
952
+ export interface FusionFailureResultView {
953
+ schema_version: typeof FUSION_FAILURE_SUMMARY_SCHEMA_VERSION;
954
+ summary_status: 'verified' | 'legacy_manifest_only' | 'unavailable' | 'integrity_failed';
955
+ terminal_state: Exclude<FusionTerminalState, 'completed'>;
956
+ answer: { present: false; reason: 'run_did_not_commit' };
957
+ failure?: FusionFailureViewFailure | undefined;
958
+ progress?: FusionRunProgress | undefined;
959
+ usage_so_far?: FusionUsage | undefined;
960
+ attempts?: FusionFailureList<FusionFailureAttemptMetadata> | undefined;
961
+ evidence_artifacts?: FusionFailureList<FusionFailureEvidenceArtifact> | undefined;
962
+ remediation_ids?: FusionFailureSummaryV1['remediation_ids'] | undefined;
963
+ failure_summary_ref?: FusionArtifactRef | undefined;
964
+ summary_unavailable_reason?: 'no_durable_summary' | 'manifest_untrusted' | 'summary_integrity_failed' | undefined;
965
+ }
966
+
880
967
  export interface FusionArtifactManifest {
881
968
  schema_version: typeof FUSION_MANIFEST_SCHEMA_VERSION;
882
969
  run_id: string;
@@ -58,6 +58,7 @@ import {
58
58
  resolvePiLaunch,
59
59
  type PiLaunchSpec,
60
60
  } from './pi-launch.js';
61
+ import { resolveAnthropicAttributionExtensionPath } from './anthropic-attribution-path.js';
61
62
  import {
62
63
  runWindowsTaskkill,
63
64
  type TaskkillOutcome,
@@ -1234,7 +1235,9 @@ export class BackgroundTaskRegistry {
1234
1235
  if (this.shuttingDown)
1235
1236
  throw new Error('Cannot start an attested Pi task while Pi is shutting down');
1236
1237
 
1237
- const argv = buildAttestedPiArgv(request);
1238
+ const attributionExtensionPath =
1239
+ request.provider === 'anthropic' ? resolveAnthropicAttributionExtensionPath() : undefined;
1240
+ const argv = buildAttestedPiArgv(request, attributionExtensionPath);
1238
1241
  const attestedPiLaunch = resolvePiLaunch({ platform: this.platform });
1239
1242
  assertWindowsCommandLineWithinLimit(
1240
1243
  attestedPiLaunch,
@@ -7,6 +7,7 @@ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
7
7
  import type { Usage } from '@earendil-works/pi-ai';
8
8
  import {
9
9
  DELEGATE_RECEIPT_SCHEMA_VERSION,
10
+ DELEGATE_CAPABILITIES,
10
11
  type DelegateRouteAttestation,
11
12
  type DelegateSeedV1,
12
13
  type DelegateSpillReceipt,
@@ -23,8 +24,10 @@ import {
23
24
  /**
24
25
  * Package-owned delegate child extension.
25
26
  *
26
- * This runs inside the delegate child Pi process and is the only extension it
27
- * loads. It is responsible for every guarantee that cannot be enforced from the
27
+ * This runs inside every delegate child Pi process and is the package-owned
28
+ * child guard. Anthropic routes load attribution first, and ambient mode may
29
+ * also execute discovered extensions; this guard remains
30
+ * responsible for every isolation guarantee that cannot be enforced from the
28
31
  * parent:
29
32
  *
30
33
  * - verifying the frozen seed bytes before the first model call;
@@ -289,6 +292,10 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
289
292
  launchNonce: expectedNonce,
290
293
  });
291
294
 
295
+ if (!DELEGATE_CAPABILITIES.includes(seed.capability)) {
296
+ throw new Error(`delegate child cannot enforce capability ${seed.capability}`);
297
+ }
298
+
292
299
  const state: GuardState = {
293
300
  seed,
294
301
  artifactDirAbs,
@@ -11,11 +11,17 @@ import { Type, type Static } from 'typebox';
11
11
  import type { BgTask, BgTaskSnapshot, StartDelegateTaskOptions } from './core/common.js';
12
12
  import { truncateChars } from './core/common.js';
13
13
  import { sha256Buffer } from './core/attested-pi-run.js';
14
- import { readFusionCommittedResult } from './core/fusion/result-package.js';
15
- import { cloneFusionUsage, type FusionUsage, type FusionWorkflowId } from './core/fusion/types.js';
14
+ import { readFusionCommittedResult, readFusionFailureResult } from './core/fusion/result-package.js';
15
+ import {
16
+ cloneFusionUsage,
17
+ type FusionFailureResultView,
18
+ type FusionUsage,
19
+ type FusionWorkflowId,
20
+ } from './core/fusion/types.js';
16
21
  import {
17
22
  DELEGATE_AUTO_DELIVER_MODES,
18
23
  DELEGATE_CAPABILITIES,
24
+ DELEGATE_EXTENSION_MODES,
19
25
  DELEGATE_RESULT_TOOL_NAME,
20
26
  DELEGATE_TOOL_NAME,
21
27
  DelegateError,
@@ -23,6 +29,7 @@ import {
23
29
  type DelegateCapability,
24
30
  type DelegateDeliveryMode,
25
31
  type DelegateBudgetRouteSource,
32
+ type DelegateExtensionMode,
26
33
  type DelegateRoute,
27
34
  } from './core/delegate/types.js';
28
35
  import {
@@ -62,7 +69,7 @@ const HOOK_EVIDENCE_PATH = fileURLToPath(
62
69
  new URL('./core/delegate/hook-contract-evidence.json', import.meta.url),
63
70
  );
64
71
 
65
- const DelegateParams = Type.Object(
72
+ export const DelegateParams = Type.Object(
66
73
  {
67
74
  name: Type.String({
68
75
  description: 'Short human-readable task name shown in the bg footer dock. Use 2-6 words.',
@@ -88,6 +95,12 @@ const DelegateParams = Type.Object(
88
95
  description: `Capability profile. Only "inspect" (read/search/list, no shell, no writes, no network, no recursion) is supported.`,
89
96
  }),
90
97
  ),
98
+ extensionMode: Type.Optional(
99
+ Type.String({
100
+ description:
101
+ 'Extension discovery: isolated | ambient. Default isolated. Ambient is for extension-registered providers and executes arbitrary discovered extension code, weakening process isolation.',
102
+ }),
103
+ ),
91
104
  maxTurns: Type.Optional(
92
105
  Type.Number({
93
106
  description: `Maximum agent turns. Default ${String(DELEGATE_DEFAULT_MAX_TURNS)}.`,
@@ -137,6 +150,20 @@ const ResultParams = Type.Object(
137
150
  type DelegateParamsValue = Static<typeof DelegateParams>;
138
151
  type ResultParamsValue = Static<typeof ResultParams>;
139
152
 
153
+ const DELEGATE_PARAM_KEYS = new Set([
154
+ 'name',
155
+ 'prompt',
156
+ 'route',
157
+ 'capability',
158
+ 'extensionMode',
159
+ 'maxTurns',
160
+ 'maxToolCalls',
161
+ 'timeoutSeconds',
162
+ 'autoDeliver',
163
+ 'notifyOnCompletion',
164
+ 'triggerOnCompletion',
165
+ ]);
166
+
140
167
  export interface DelegateLaunchDetails {
141
168
  schema_version: 'pi-background-tasks.delegate-launch.v1';
142
169
  task: BgTaskSnapshot;
@@ -146,6 +173,7 @@ export interface DelegateLaunchDetails {
146
173
  seed_sha256: string;
147
174
  seed_utf8_bytes: number;
148
175
  budget: DelegateBudgetRouteSource;
176
+ extension_mode: DelegateExtensionMode;
149
177
  auto_deliver: DelegateAutoDeliverMode;
150
178
  notify_on_completion: boolean;
151
179
  trigger_on_completion: boolean;
@@ -161,6 +189,16 @@ export interface FusionBackgroundResultDetails {
161
189
  answer_bytes?: number | undefined;
162
190
  answer_sha256?: string | undefined;
163
191
  usage_delivered?: boolean | undefined;
192
+ answer?: { present: false; reason: 'run_did_not_commit' } | undefined;
193
+ summary_status?: FusionFailureResultView['summary_status'] | undefined;
194
+ failure_summary_ref?: FusionFailureResultView['failure_summary_ref'] | undefined;
195
+ failure?: FusionFailureResultView['failure'] | undefined;
196
+ progress?: FusionFailureResultView['progress'] | undefined;
197
+ usage_so_far?: FusionFailureResultView['usage_so_far'] | undefined;
198
+ attempts?: FusionFailureResultView['attempts'] | undefined;
199
+ evidence_artifacts?: FusionFailureResultView['evidence_artifacts'] | undefined;
200
+ remediation_ids?: FusionFailureResultView['remediation_ids'] | undefined;
201
+ summary_unavailable_reason?: FusionFailureResultView['summary_unavailable_reason'] | undefined;
164
202
  }
165
203
 
166
204
  export type BackgroundResultDetails = DelegateResultDetails | FusionBackgroundResultDetails;
@@ -172,6 +210,7 @@ export interface DelegateResultDetails {
172
210
  delivery: DelegateDeliveryMode | 'none';
173
211
  route?: { provider: string; model: string } | undefined;
174
212
  budget?: DelegateBudgetRouteSource | undefined;
213
+ extension_mode?: DelegateExtensionMode | undefined;
175
214
  answer_bytes?: number | undefined;
176
215
  answer_sha256?: string | undefined;
177
216
  turns?: number | undefined;
@@ -198,6 +237,15 @@ function requireCapability(value: unknown): DelegateCapability {
198
237
  );
199
238
  }
200
239
 
240
+ function requireExtensionMode(value: unknown): DelegateExtensionMode {
241
+ if (value === undefined) return 'isolated';
242
+ if (value === 'isolated' || value === 'ambient') return value;
243
+ throw new DelegateError(
244
+ `bg_delegate extensionMode must be one of ${DELEGATE_EXTENSION_MODES.join(', ')}`,
245
+ { code: 'invalid_arguments', childCreated: false },
246
+ );
247
+ }
248
+
201
249
  function requireAutoDeliver(value: unknown): DelegateAutoDeliverMode {
202
250
  if (value === undefined) return 'never';
203
251
  if (value === 'never' || value === 'when_small' || value === 'always') return value;
@@ -293,13 +341,15 @@ export function registerDelegateExtension(
293
341
  name: DELEGATE_TOOL_NAME,
294
342
  label: 'Background Delegate',
295
343
  description:
296
- 'Launch one background Pi agent seeded with a frozen projection of the current conversation, then return a launch receipt immediately. The child has its own session, a route pinned at launch that is never substituted, and read-only tools. Retrieve its verified answer with bg_result.',
344
+ 'Launch one background Pi agent seeded with a frozen projection of the current conversation, then return a launch receipt immediately. The child has its own session, a route pinned at launch that is never substituted, and read-only tools. Extension discovery is isolated by default; ambient mode supports extension-registered providers but executes arbitrary discovered extension code. Retrieve its verified answer with bg_result.',
297
345
  promptSnippet:
298
346
  'Delegate an investigation to a background agent that already has this conversation as context',
299
347
  promptGuidelines: [
300
348
  'Use bg_delegate when work should continue in the background and the worker needs what you already know: it is seeded with a projection of this conversation.',
301
349
  'The prompt is authoritative. State exactly what you want investigated and what the answer should contain.',
302
- 'The delegate is inspect-only: it can read, search, and list files, but cannot run shell commands, edit or write files, use the network, or delegate further.',
350
+ 'The delegate is inspect-only at the model-visible tool boundary: it can read, search, and list files, but cannot run shell commands, edit or write files, use the network, or delegate further.',
351
+ 'Extension discovery is isolated by default. Use extensionMode:"ambient" only when the pinned provider is registered by an ambient user/project extension.',
352
+ 'Ambient mode executes arbitrary discovered extension code in the child process. Tool allowlists do not sandbox extension code, so ambient mode weakens inspect-only process isolation.',
303
353
  'Facts that exist only inside omitted tool output are not available to the delegate. Restate such findings in the prompt.',
304
354
  'bg_delegate returns immediately. Do not poll; retrieve the answer with bg_result after the terminal notification arrives.',
305
355
  ],
@@ -310,6 +360,13 @@ export function registerDelegateExtension(
310
360
  code: 'invalid_arguments',
311
361
  childCreated: false,
312
362
  });
363
+ const unknownKeys = Object.keys(args).filter((key) => !DELEGATE_PARAM_KEYS.has(key));
364
+ if (unknownKeys.length > 0) {
365
+ throw new DelegateError(
366
+ `bg_delegate contains unsupported key(s): ${unknownKeys.sort().join(', ')}`,
367
+ { code: 'invalid_arguments', childCreated: false },
368
+ );
369
+ }
313
370
  const name = args['name'];
314
371
  const prompt = args['prompt'];
315
372
  if (typeof name !== 'string' || name.trim().length === 0)
@@ -326,6 +383,7 @@ export function registerDelegateExtension(
326
383
  const route = requireRoute(args['route']);
327
384
  if (route !== undefined) prepared.route = route;
328
385
  prepared.capability = requireCapability(args['capability']);
386
+ prepared.extensionMode = requireExtensionMode(args['extensionMode']);
329
387
  prepared.autoDeliver = requireAutoDeliver(args['autoDeliver']);
330
388
  const maxTurns = optionalPositiveInteger(args['maxTurns'], 'maxTurns');
331
389
  if (maxTurns !== undefined) prepared.maxTurns = maxTurns;
@@ -341,6 +399,7 @@ export function registerDelegateExtension(
341
399
  },
342
400
  async execute(toolCallId, params, _signal, _onUpdate, ctx) {
343
401
  const capability = requireCapability(params.capability);
402
+ const extensionMode = requireExtensionMode(params.extensionMode);
344
403
  const autoDeliver = requireAutoDeliver(params.autoDeliver);
345
404
  const hookEvidence = await loadEvidence();
346
405
  const route = resolveDelegateRoute({
@@ -370,6 +429,7 @@ export function registerDelegateExtension(
370
429
  toolCallId,
371
430
  prompt: params.prompt,
372
431
  capability,
432
+ extensionMode,
373
433
  route,
374
434
  limitOverrides: {
375
435
  maxTurns: params.maxTurns,
@@ -408,6 +468,7 @@ export function registerDelegateExtension(
408
468
  seed_sha256: prepared.facts.seedSha256,
409
469
  seed_utf8_bytes: prepared.preflight.plan.seed_utf8_bytes,
410
470
  budget: prepared.facts.budget,
471
+ extension_mode: extensionMode,
411
472
  auto_deliver: autoDeliver,
412
473
  notify_on_completion: launchOptions.notifyOnCompletion,
413
474
  trigger_on_completion: launchOptions.triggerOnCompletion,
@@ -422,6 +483,7 @@ export function registerDelegateExtension(
422
483
  `Seed: ${String(prepared.preflight.plan.seed_utf8_bytes)} bytes, sha256 ${prepared.facts.seedSha256}`,
423
484
  `Estimator: family ${prepared.facts.budget.family}, source ${prepared.facts.budget.rate_source.source}, rate ${String(prepared.facts.budget.rate_source.effective_rate_bytes_per_token_x100)}/100 B/tok + ${String(prepared.facts.budget.rate_source.affine_f_tokens)} tokens${prepared.facts.budget.rate_source.warning === null ? '' : `; warning: ${prepared.facts.budget.rate_source.warning}`}`,
424
485
  `Capability: ${capability} (read/search/list only)`,
486
+ `Extension mode: ${extensionMode}${extensionMode === 'ambient' ? ' — WARNING: arbitrary discovered extension code executes in the child; the tool allowlist does not sandbox it, so inspect-only process isolation is weakened.' : ' (ambient extension discovery disabled)'}`,
425
487
  `Limits: ${String(prepared.preflight.limits.max_turns)} turns, ${String(prepared.preflight.limits.max_tool_calls)} tool calls, ${String(prepared.preflight.limits.timeout_seconds)}s`,
426
488
  `Auto-deliver: ${autoDeliver}`,
427
489
  launchOptions.notifyOnCompletion
@@ -443,7 +505,7 @@ export function registerDelegateExtension(
443
505
  renderResult(result, _options, theme) {
444
506
  const details = result.details;
445
507
  return new Text(
446
- `${theme.fg('success', '✓ delegated')} ${theme.fg('accent', details.task.id)}\n${theme.fg('dim', `route ${details.route.qualified_id} · seed ${String(details.seed_utf8_bytes)}B · ${details.artifact_dir}`)}`,
508
+ `${theme.fg('success', '✓ delegated')} ${theme.fg('accent', details.task.id)}\n${theme.fg('dim', `route ${details.route.qualified_id} · extensions ${details.extension_mode} · seed ${String(details.seed_utf8_bytes)}B · ${details.artifact_dir}`)}`,
447
509
  0,
448
510
  0,
449
511
  );
@@ -508,9 +570,48 @@ export function registerDelegateExtension(
508
570
  };
509
571
  }
510
572
  if (task.status !== 'completed' || fusion.outcome?.status !== 'committed') {
511
- throw new Error(
512
- `Fusion ${task.id} did not commit a result (${task.status}): ${fusion.outcome?.error ?? task.error ?? 'no terminal detail'}`,
513
- );
573
+ const terminal = await readFusionFailureResult({
574
+ artifactDirAbs: fusion.artifactDirAbs,
575
+ artifactDir: fusion.artifactDir,
576
+ runId: fusion.runId,
577
+ workflow: fusion.workflow,
578
+ });
579
+ const state =
580
+ fusion.outcome?.status === 'cancelled' || task.status === 'killed'
581
+ ? 'cancelled'
582
+ : 'failed';
583
+ const details: FusionBackgroundResultDetails = {
584
+ schema_version: 'pi-background-tasks.fusion-result-view.v1',
585
+ task_id: task.id,
586
+ state,
587
+ delivery: 'none',
588
+ workflow: fusion.workflow,
589
+ artifact_dir: fusion.artifactDir,
590
+ answer: terminal.answer,
591
+ summary_status: terminal.summary_status,
592
+ ...(terminal.failure_summary_ref === undefined
593
+ ? {}
594
+ : { failure_summary_ref: terminal.failure_summary_ref }),
595
+ ...(terminal.failure === undefined ? {} : { failure: terminal.failure }),
596
+ ...(terminal.progress === undefined ? {} : { progress: terminal.progress }),
597
+ ...(terminal.usage_so_far === undefined ? {} : { usage_so_far: terminal.usage_so_far }),
598
+ ...(terminal.attempts === undefined ? {} : { attempts: terminal.attempts }),
599
+ ...(terminal.evidence_artifacts === undefined
600
+ ? {}
601
+ : { evidence_artifacts: terminal.evidence_artifacts }),
602
+ ...(terminal.remediation_ids === undefined
603
+ ? {}
604
+ : { remediation_ids: terminal.remediation_ids }),
605
+ ...(terminal.summary_unavailable_reason === undefined
606
+ ? {}
607
+ : { summary_unavailable_reason: terminal.summary_unavailable_reason }),
608
+ };
609
+ return {
610
+ content: textContent(
611
+ `Fusion ${task.id} ${state}; no answer was committed. Terminal evidence status: ${terminal.summary_status}. Delivery is none; use only the manifest-bound artifact references in details.`,
612
+ ),
613
+ details,
614
+ };
514
615
  }
515
616
  const verified = await readFusionCommittedResult({
516
617
  artifactDirAbs: fusion.artifactDirAbs,
@@ -579,6 +680,7 @@ export function registerDelegateExtension(
579
680
  delivery: 'none',
580
681
  artifact_dir: facts.artifactDir,
581
682
  budget: facts.budget,
683
+ extension_mode: facts.extensionMode,
582
684
  };
583
685
  return {
584
686
  content: textContent(
@@ -632,6 +734,7 @@ export function registerDelegateExtension(
632
734
  delivery: decision.mode,
633
735
  route: verified.package.route,
634
736
  budget: facts.budget,
737
+ extension_mode: facts.extensionMode,
635
738
  answer_bytes: verified.package.answer.byte_length,
636
739
  answer_sha256: verified.package.answer.sha256,
637
740
  turns: verified.package.turns,
@@ -673,6 +776,13 @@ export function registerDelegateExtension(
673
776
  0,
674
777
  0,
675
778
  );
779
+ if (fusion && (details.state === 'failed' || details.state === 'cancelled')) {
780
+ return new Text(
781
+ theme.fg('warning', `${details.state} fusion; no committed answer · ${details.summary_status ?? 'unavailable'}`),
782
+ 0,
783
+ 0,
784
+ );
785
+ }
676
786
  return new Text(
677
787
  `${theme.fg('success', fusion ? '✓ fusion answer' : '✓ delegate answer')} ${theme.fg('dim', `${String(details.answer_bytes ?? 0)}B · ${details.delivery}`)}`,
678
788
  0,