pi-background-tasks 2.5.0 → 2.6.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 (203) hide show
  1. package/BACKGROUND-TASKS-INSTRUCTIONS.md +1 -1
  2. package/README.md +70 -25
  3. package/TESTING.md +42 -2
  4. package/TEST_PLAN.md +7 -4
  5. package/dist/extensions/anthropic-attribution-child.js +4 -0
  6. package/dist/extensions/anthropic-attribution-child.js.map +1 -0
  7. package/dist/extensions/anthropic-attribution.js +107 -0
  8. package/dist/extensions/anthropic-attribution.js.map +1 -0
  9. package/dist/extensions/background-tasks.js +2 -0
  10. package/dist/extensions/background-tasks.js.map +1 -0
  11. package/dist/extensions/delegate-child.js +2 -0
  12. package/dist/extensions/delegate-child.js.map +1 -0
  13. package/dist/extensions/fusion-child.js +2 -0
  14. package/dist/extensions/fusion-child.js.map +1 -0
  15. package/dist/package.json +5 -0
  16. package/dist/src/core/anthropic-attribution-path.js +24 -0
  17. package/dist/src/core/anthropic-attribution-path.js.map +1 -0
  18. package/dist/src/core/anthropic-attribution.js +2247 -0
  19. package/dist/src/core/anthropic-attribution.js.map +1 -0
  20. package/dist/src/core/attested-pi-contract.js +5 -0
  21. package/dist/src/core/attested-pi-contract.js.map +1 -0
  22. package/dist/src/core/attested-pi-run.js +749 -0
  23. package/dist/src/core/attested-pi-run.js.map +1 -0
  24. package/dist/src/core/canonical-json.js +19 -0
  25. package/dist/src/core/canonical-json.js.map +1 -0
  26. package/dist/src/core/common.js +787 -0
  27. package/dist/src/core/common.js.map +1 -0
  28. package/dist/src/core/config.js +78 -0
  29. package/dist/src/core/config.js.map +1 -0
  30. package/dist/src/core/context/parent-snapshot.js +75 -0
  31. package/dist/src/core/context/parent-snapshot.js.map +1 -0
  32. package/dist/src/core/context/token-budget.js +631 -0
  33. package/dist/src/core/context/token-budget.js.map +1 -0
  34. package/dist/src/core/context/visible-conversation-v2.js +390 -0
  35. package/dist/src/core/context/visible-conversation-v2.js.map +1 -0
  36. package/dist/src/core/delegate/artifacts.js +357 -0
  37. package/dist/src/core/delegate/artifacts.js.map +1 -0
  38. package/dist/src/core/delegate/budget.js +266 -0
  39. package/dist/src/core/delegate/budget.js.map +1 -0
  40. package/dist/src/core/delegate/facade-contract.js +7 -0
  41. package/dist/src/core/delegate/facade-contract.js.map +1 -0
  42. package/dist/src/core/delegate/hook-contract-evidence.json +18 -0
  43. package/dist/src/core/delegate/hook-contract.js +124 -0
  44. package/dist/src/core/delegate/hook-contract.js.map +1 -0
  45. package/dist/src/core/delegate/launch.js +366 -0
  46. package/dist/src/core/delegate/launch.js.map +1 -0
  47. package/dist/src/core/delegate/result-package.js +293 -0
  48. package/dist/src/core/delegate/result-package.js.map +1 -0
  49. package/dist/src/core/delegate/runner.js +398 -0
  50. package/dist/src/core/delegate/runner.js.map +1 -0
  51. package/dist/src/core/delegate/seed.js +334 -0
  52. package/dist/src/core/delegate/seed.js.map +1 -0
  53. package/dist/src/core/delegate/types.js +103 -0
  54. package/dist/src/core/delegate/types.js.map +1 -0
  55. package/dist/src/core/durable-fs.js +372 -0
  56. package/dist/src/core/durable-fs.js.map +1 -0
  57. package/dist/src/core/extension-api.js +382 -0
  58. package/dist/src/core/extension-api.js.map +1 -0
  59. package/dist/src/core/fusion/artifacts.js +654 -0
  60. package/dist/src/core/fusion/artifacts.js.map +1 -0
  61. package/dist/src/core/fusion/budget.js +949 -0
  62. package/dist/src/core/fusion/budget.js.map +1 -0
  63. package/dist/src/core/fusion/child-protocol.js +176 -0
  64. package/dist/src/core/fusion/child-protocol.js.map +1 -0
  65. package/dist/src/core/fusion/claude-cache.js +156 -0
  66. package/dist/src/core/fusion/claude-cache.js.map +1 -0
  67. package/dist/src/core/fusion/clean-context.js +64 -0
  68. package/dist/src/core/fusion/clean-context.js.map +1 -0
  69. package/dist/src/core/fusion/config.js +349 -0
  70. package/dist/src/core/fusion/config.js.map +1 -0
  71. package/dist/src/core/fusion/context.js +184 -0
  72. package/dist/src/core/fusion/context.js.map +1 -0
  73. package/dist/src/core/fusion/evaluation.js +696 -0
  74. package/dist/src/core/fusion/evaluation.js.map +1 -0
  75. package/dist/src/core/fusion/facade-contract.js +3 -0
  76. package/dist/src/core/fusion/facade-contract.js.map +1 -0
  77. package/dist/src/core/fusion/orchestrator.js +867 -0
  78. package/dist/src/core/fusion/orchestrator.js.map +1 -0
  79. package/dist/src/core/fusion/output-contract.js +27 -0
  80. package/dist/src/core/fusion/output-contract.js.map +1 -0
  81. package/dist/src/core/fusion/pi-child.js +1741 -0
  82. package/dist/src/core/fusion/pi-child.js.map +1 -0
  83. package/dist/src/core/fusion/prompts.js +260 -0
  84. package/dist/src/core/fusion/prompts.js.map +1 -0
  85. package/dist/src/core/fusion/result-package.js +857 -0
  86. package/dist/src/core/fusion/result-package.js.map +1 -0
  87. package/dist/src/core/fusion/source-policy.js +259 -0
  88. package/dist/src/core/fusion/source-policy.js.map +1 -0
  89. package/dist/src/core/fusion/types.js +207 -0
  90. package/dist/src/core/fusion/types.js.map +1 -0
  91. package/dist/src/core/fusion/web-fetch.js +777 -0
  92. package/dist/src/core/fusion/web-fetch.js.map +1 -0
  93. package/dist/src/core/fusion/workflows.js +126 -0
  94. package/dist/src/core/fusion/workflows.js.map +1 -0
  95. package/dist/src/core/lazy-module.js +181 -0
  96. package/dist/src/core/lazy-module.js.map +1 -0
  97. package/dist/src/core/pi-launch.js +431 -0
  98. package/dist/src/core/pi-launch.js.map +1 -0
  99. package/dist/src/core/registry.js +3331 -0
  100. package/dist/src/core/registry.js.map +1 -0
  101. package/dist/src/core/reload-shell-owner.js +1396 -0
  102. package/dist/src/core/reload-shell-owner.js.map +1 -0
  103. package/dist/src/core/shell-policy.js +80 -0
  104. package/dist/src/core/shell-policy.js.map +1 -0
  105. package/dist/src/core/task-durable.js +51 -0
  106. package/dist/src/core/task-durable.js.map +1 -0
  107. package/dist/src/core/update-check.js +92 -0
  108. package/dist/src/core/update-check.js.map +1 -0
  109. package/dist/src/core/windows-taskkill.js +185 -0
  110. package/dist/src/core/windows-taskkill.js.map +1 -0
  111. package/dist/src/delegate-child-extension.js +776 -0
  112. package/dist/src/delegate-child-extension.js.map +1 -0
  113. package/dist/src/delegate-extension.js +746 -0
  114. package/dist/src/delegate-extension.js.map +1 -0
  115. package/dist/src/extension.js +953 -0
  116. package/dist/src/extension.js.map +1 -0
  117. package/dist/src/fusion-child-extension.js +760 -0
  118. package/dist/src/fusion-child-extension.js.map +1 -0
  119. package/dist/src/fusion-extension.js +1030 -0
  120. package/dist/src/fusion-extension.js.map +1 -0
  121. package/dist/src/ui/background-tasks-manager.js +689 -0
  122. package/dist/src/ui/background-tasks-manager.js.map +1 -0
  123. package/dist/src/ui/fusion-model-selector.js +277 -0
  124. package/dist/src/ui/fusion-model-selector.js.map +1 -0
  125. package/docs/INDEX.md +35 -33
  126. package/docs/api/eventbus-v1.md +21 -5
  127. package/docs/choose-a-workflow.md +2 -0
  128. package/docs/commands/bg-clear.md +3 -3
  129. package/docs/commands/bg-update.md +3 -3
  130. package/docs/commands/bg.md +18 -6
  131. package/docs/commands/claude-cache.md +4 -4
  132. package/docs/commands/fusion-models.md +49 -4
  133. package/docs/commands/fusion.md +3 -3
  134. package/docs/commands/jobs.md +3 -3
  135. package/docs/commands/kill.md +3 -3
  136. package/docs/commands/logs.md +3 -3
  137. package/docs/commands/task-manager.md +5 -5
  138. package/docs/concepts/completion-delivery.md +6 -2
  139. package/docs/getting-started.md +7 -1
  140. package/docs/manifest.json +228 -42
  141. package/docs/operations/configuration.md +58 -5
  142. package/docs/operations/releasing.md +4 -2
  143. package/docs/operations/testing.md +22 -0
  144. package/docs/operations/troubleshooting.md +6 -2
  145. package/docs/read-before-edit.md +44 -31
  146. package/docs/reference/runtime-contracts.md +87 -51
  147. package/docs/reference/shortcuts-and-dock.md +28 -14
  148. package/docs/subsystems/anthropic-attribution.md +23 -11
  149. package/docs/subsystems/attested-pi-runs.md +12 -7
  150. package/docs/subsystems/background-task-runtime.md +53 -11
  151. package/docs/subsystems/child-launch-durability-and-safety.md +28 -9
  152. package/docs/subsystems/delegation.md +16 -3
  153. package/docs/subsystems/docs-freshness-gate.md +14 -7
  154. package/docs/subsystems/fusion.md +36 -11
  155. package/docs/subsystems/host-ui-and-telemetry.md +22 -9
  156. package/docs/tools/bg_delegate.md +3 -1
  157. package/docs/tools/bg_kill.md +3 -1
  158. package/docs/tools/bg_logs.md +3 -1
  159. package/docs/tools/bg_result.md +5 -3
  160. package/docs/tools/bg_run.md +31 -5
  161. package/docs/tools/bg_run_pi_attested.md +3 -1
  162. package/docs/tools/bg_status.md +3 -1
  163. package/docs/tools/fusion_investigate.md +3 -1
  164. package/docs/tools/fusion_reason.md +3 -1
  165. package/docs/tools/fusion_research.md +3 -1
  166. package/docs/tools/fusion_validate.md +3 -1
  167. package/extensions/anthropic-attribution-child.ts +3 -0
  168. package/extensions/anthropic-attribution.ts +166 -1
  169. package/package.json +12 -10
  170. package/src/core/anthropic-attribution-path.ts +5 -4
  171. package/src/core/anthropic-attribution.ts +293 -113
  172. package/src/core/attested-pi-contract.ts +4 -0
  173. package/src/core/attested-pi-run.ts +468 -81
  174. package/src/core/canonical-json.ts +21 -0
  175. package/src/core/common.ts +450 -40
  176. package/src/core/config.ts +121 -0
  177. package/src/core/context/visible-conversation-v2.ts +3 -6
  178. package/src/core/delegate/artifacts.ts +13 -9
  179. package/src/core/delegate/budget.ts +13 -6
  180. package/src/core/delegate/facade-contract.ts +6 -0
  181. package/src/core/delegate/result-package.ts +32 -14
  182. package/src/core/delegate/runner.ts +129 -58
  183. package/src/core/delegate/seed.ts +5 -7
  184. package/src/core/durable-fs.ts +178 -30
  185. package/src/core/extension-api.ts +36 -8
  186. package/src/core/fusion/artifacts.ts +17 -16
  187. package/src/core/fusion/clean-context.ts +8 -5
  188. package/src/core/fusion/config.ts +2 -1
  189. package/src/core/fusion/context.ts +2 -6
  190. package/src/core/fusion/facade-contract.ts +2 -0
  191. package/src/core/fusion/orchestrator.ts +3 -3
  192. package/src/core/fusion/prompts.ts +1 -1
  193. package/src/core/fusion/result-package.ts +259 -72
  194. package/src/core/fusion/source-policy.ts +43 -17
  195. package/src/core/lazy-module.ts +215 -0
  196. package/src/core/pi-launch.ts +390 -65
  197. package/src/core/registry.ts +2010 -381
  198. package/src/core/reload-shell-owner.ts +1662 -0
  199. package/src/core/shell-policy.ts +134 -0
  200. package/src/core/task-durable.ts +67 -0
  201. package/src/delegate-extension.ts +404 -144
  202. package/src/extension.ts +387 -114
  203. package/src/fusion-extension.ts +202 -96
@@ -1,6 +1,6 @@
1
- import { statSync, type WriteStream } from 'node:fs';
1
+ import { accessSync, constants, statSync, type WriteStream } from 'node:fs';
2
2
  import { open } from 'node:fs/promises';
3
- import { extname, isAbsolute, join, win32 } from 'node:path';
3
+ import { basename, delimiter, extname, isAbsolute, join, resolve, win32 } from 'node:path';
4
4
  import { DEFAULT_MAX_BYTES } from '@earendil-works/pi-coding-agent';
5
5
  import type { BackgroundTaskChildProcess } from './registry.js';
6
6
  import type { DelegateBudgetRouteSource, DelegateExtensionMode } from './delegate/types.js';
@@ -12,6 +12,44 @@ export const TERMINAL_TASK_STATUS_VALUES = ['completed', 'failed', 'killed'] as
12
12
  export type TaskStatus = (typeof TASK_STATUS_VALUES)[number];
13
13
  export type TerminalTaskStatus = (typeof TERMINAL_TASK_STATUS_VALUES)[number];
14
14
  export type KillKind = 'user' | 'timeout' | 'output_cap' | 'shutdown';
15
+ export type ReloadShellStopKind = KillKind | 'handoff_expired';
16
+
17
+ export type TerminalPublicationState = 'pending' | 'delivered' | 'abandoned';
18
+ export type TerminalPublicationAbandonReason =
19
+ | 'registry_shutdown'
20
+ | 'publisher_closed'
21
+ | 'gate_rejected'
22
+ | 'retry_exhausted'
23
+ | 'retention_limit'
24
+ | 'reload_handoff_expired';
25
+
26
+ export type ReloadSurvivalErrorCode =
27
+ | 'pi_bg_survive_reload_invalid'
28
+ | 'pi_bg_survive_reload_requires_non_agent'
29
+ | 'pi_bg_survive_reload_unsupported_task_kind'
30
+ | 'pi_bg_reload_owner_unavailable'
31
+ | 'pi_bg_reload_owner_protocol_incompatible'
32
+ | 'pi_bg_reload_owner_activation_conflict'
33
+ | 'pi_bg_reload_owner_stale_claim'
34
+ | 'pi_bg_reload_handoff_expired';
35
+
36
+ export class ReloadSurvivalError extends Error {
37
+ constructor(
38
+ readonly code: ReloadSurvivalErrorCode,
39
+ message: string,
40
+ ) {
41
+ super(`${code}: ${message}`);
42
+ this.name = 'ReloadSurvivalError';
43
+ }
44
+ }
45
+
46
+ export function rejectSurvivalForTaskKind(value: object, kind: string): void {
47
+ if (!Object.prototype.hasOwnProperty.call(value, 'surviveReload')) return;
48
+ throw new ReloadSurvivalError(
49
+ 'pi_bg_survive_reload_unsupported_task_kind',
50
+ `${kind} does not support surviveReload; only ordinary isAgent:false shell tasks may survive reload`,
51
+ );
52
+ }
15
53
 
16
54
  export type JsonObject = Readonly<Record<PropertyKey, unknown>>;
17
55
 
@@ -36,6 +74,150 @@ export interface TaskToolUsage {
36
74
  byName: Record<string, number>;
37
75
  }
38
76
 
77
+ export interface ReloadSurvivalSnapshotV1 {
78
+ schemaVersion: 'pi-background-tasks.reload-shell.v1';
79
+ authority: 'same-process-live-owner';
80
+ hostPid: number;
81
+ sessionId: string;
82
+ cwdRealpath: string;
83
+ launchNonce: string;
84
+ completionId: string;
85
+ spawnedAt: number;
86
+ childPid: number;
87
+ timeoutDeadlineAt?: number | undefined;
88
+ outputCapBytes: number;
89
+ posixProcessGroupId?: number | undefined;
90
+ windowsTreeRootPid?: number | undefined;
91
+ leaseGeneration: number;
92
+ handoffCount: number;
93
+ }
94
+
95
+ export interface ReloadShellIdentityV1 {
96
+ readonly hostPid: number;
97
+ readonly sessionId: string;
98
+ readonly cwdRealpath: string;
99
+ }
100
+
101
+ export interface ReloadShellActivationLeaseV1 {
102
+ readonly protocol: 'pi-background-tasks.reload-shell-owner.v1';
103
+ readonly hubNonce: string;
104
+ readonly identityKey: string;
105
+ readonly generation: number;
106
+ readonly activationNonce: string;
107
+ }
108
+
109
+ export interface ReloadShellActivationClaimV1 {
110
+ readonly protocol: 'pi-background-tasks.reload-shell-owner.v1';
111
+ readonly hubNonce: string;
112
+ readonly claimNonce: string;
113
+ readonly identity: ReloadShellIdentityV1;
114
+ readonly identityKey: string;
115
+ readonly generation: number;
116
+ readonly activationNonce: string;
117
+ readonly expiresAt?: number | undefined;
118
+ readonly executions: readonly ReloadableShellExecutionV1[];
119
+ }
120
+
121
+ export interface ReloadShellProcessV1 {
122
+ readonly pid?: number | undefined;
123
+ stdout?: {
124
+ on(event: 'data', listener: (data: Buffer | string) => void): unknown;
125
+ off?(event: 'data', listener: (data: Buffer | string) => void): unknown;
126
+ } | null | undefined;
127
+ stderr?: {
128
+ on(event: 'data', listener: (data: Buffer | string) => void): unknown;
129
+ off?(event: 'data', listener: (data: Buffer | string) => void): unknown;
130
+ } | null | undefined;
131
+ kill(signal?: NodeJS.Signals): boolean;
132
+ on(event: 'error', listener: (error: Error) => void): unknown;
133
+ on(
134
+ event: 'close',
135
+ listener: (code: number | null, signal: NodeJS.Signals | null) => void,
136
+ ): unknown;
137
+ off?(event: 'error', listener: (error: Error) => void): unknown;
138
+ off?(
139
+ event: 'close',
140
+ listener: (code: number | null, signal: NodeJS.Signals | null) => void,
141
+ ): unknown;
142
+ }
143
+
144
+ export interface ReloadShellOwnerEventSinkV1 {
145
+ readonly onChanged: (execution: ReloadableShellExecutionV1) => void;
146
+ readonly onTerminal: (execution: ReloadableShellExecutionV1) => void;
147
+ }
148
+
149
+ export type ReloadShellNotificationState = 'disabled' | 'pending' | 'sending' | 'delivered';
150
+
151
+ export interface ReloadableShellExecutionV1 {
152
+ readonly protocol: 'pi-background-tasks.reload-shell-owner.v1';
153
+ readonly launchNonce: string;
154
+ readonly completionId: string;
155
+ readonly task: BgTask;
156
+ child: ReloadShellProcessV1 | undefined;
157
+ outputStream: WriteStream | undefined;
158
+ readonly spawnedAt: number;
159
+ readonly timeoutDeadlineAt?: number | undefined;
160
+ readonly outputCapBytes: number;
161
+ readonly terminal: Promise<BgTask>;
162
+ readonly requestStop: (kind: ReloadShellStopKind, reason?: string) => Promise<BgTask>;
163
+ phase: 'starting' | 'running' | 'stop_requested' | 'finalizing' | 'terminal' | 'released';
164
+ closeObservation?: {
165
+ code: number | null;
166
+ signal: NodeJS.Signals | null;
167
+ observedAt: number;
168
+ } | undefined;
169
+ admissionCommitted: boolean;
170
+ notificationState: ReloadShellNotificationState;
171
+ readonly commitInitialMetadata: (signal?: AbortSignal) => Promise<void>;
172
+ readonly failAdmission: (error: Error) => void;
173
+ readonly setOwnerEventSink: (sink: ReloadShellOwnerEventSinkV1 | undefined) => void;
174
+ readonly markAdmissionCommitted: (generation: number, handoffCount: number) => void;
175
+ readonly updateLeaseAudit: (generation: number, handoffCount: number) => void;
176
+ readonly abandonReloadHandoff: () => void;
177
+ readonly beginNotification: (lease: ReloadShellActivationLeaseV1) => string | undefined;
178
+ readonly finishNotification: (token: string, delivered: boolean) => void;
179
+ readonly releaseResources: () => void;
180
+ }
181
+
182
+ export interface ReloadShellHostAdapterV1 {
183
+ readonly activationNonce: string;
184
+ readonly onBound: (lease: ReloadShellActivationLeaseV1) => void;
185
+ readonly onChanged: (execution: ReloadableShellExecutionV1) => void;
186
+ readonly onTerminal: (execution: ReloadableShellExecutionV1) => void;
187
+ }
188
+
189
+ export interface ReloadShellOwnerHubV1 {
190
+ readonly protocol: 'pi-background-tasks.reload-shell-owner.v1';
191
+ readonly hubNonce: string;
192
+ beginActivation(
193
+ identity: ReloadShellIdentityV1,
194
+ startReason: string,
195
+ activationNonce: string,
196
+ ): ReloadShellActivationClaimV1;
197
+ commitActivation(
198
+ claim: ReloadShellActivationClaimV1,
199
+ adapter: ReloadShellHostAdapterV1,
200
+ ): ReloadShellActivationLeaseV1;
201
+ abortActivation(claim: ReloadShellActivationClaimV1, error: unknown): void;
202
+ beginReloadHandoff(
203
+ lease: ReloadShellActivationLeaseV1,
204
+ ): readonly ReloadableShellExecutionV1[];
205
+ releaseActivation(lease: ReloadShellActivationLeaseV1): void;
206
+ registerExecution(
207
+ lease: ReloadShellActivationLeaseV1,
208
+ execution: ReloadableShellExecutionV1,
209
+ ): void;
210
+ markAdmissionCommitted(
211
+ lease: ReloadShellActivationLeaseV1,
212
+ execution: ReloadableShellExecutionV1,
213
+ ): void;
214
+ releaseExecution(
215
+ leaseOrClaim: ReloadShellActivationLeaseV1 | ReloadShellActivationClaimV1,
216
+ execution: ReloadableShellExecutionV1,
217
+ ): void;
218
+ isCurrentLease(lease: ReloadShellActivationLeaseV1): boolean;
219
+ }
220
+
39
221
  export interface BgTaskSnapshot {
40
222
  id: string;
41
223
  name?: string | undefined;
@@ -51,6 +233,8 @@ export interface BgTaskSnapshot {
51
233
  pid?: number | undefined;
52
234
  bytesWritten: number;
53
235
  isAgent: boolean;
236
+ surviveReload: boolean;
237
+ reloadSurvival?: ReloadSurvivalSnapshotV1 | undefined;
54
238
  error?: string | undefined;
55
239
  notified: boolean;
56
240
  notifyOnCompletion: boolean;
@@ -61,6 +245,8 @@ export interface BgTaskSnapshot {
61
245
  toolUsage?: TaskToolUsage | undefined;
62
246
  model?: string | undefined;
63
247
  telemetryUnavailableReason?: string | undefined;
248
+ /** Immutable non-secret shell selection for ordinary shell tasks. */
249
+ shellPolicy?: ShellPolicySnapshot | undefined;
64
250
  attestationPath?: string | undefined;
65
251
  delegate?: DelegateTaskFacts | undefined;
66
252
  fusion?: FusionTaskFacts | undefined;
@@ -130,6 +316,10 @@ export interface BgTask extends Omit<BgTaskSnapshot, 'name'> {
130
316
  wrapperAbsPath?: string | undefined;
131
317
  attestationAbsPath?: string | undefined;
132
318
  child?: BackgroundTaskChildProcess | undefined;
319
+ /** Immutable in-memory ownership captured from a detached POSIX spawn; never restored from metadata. */
320
+ ownedPosixProcessGroupId?: number | undefined;
321
+ /** One-way latch preventing any later signal after group authority is released. */
322
+ posixProcessGroupSignalAuthorityReleased?: boolean | undefined;
133
323
  stream?: WriteStream | undefined;
134
324
  timeoutHandle?: NodeJS.Timeout | undefined;
135
325
  killKind?: KillKind | undefined;
@@ -137,8 +327,14 @@ export interface BgTask extends Omit<BgTaskSnapshot, 'name'> {
137
327
  killEscalationTimer?: NodeJS.Timeout | undefined;
138
328
  capExceeded?: boolean | undefined;
139
329
  finalized?: boolean | undefined;
140
- terminalPublished?: boolean | undefined;
330
+ /** True only after the terminal EventBus emitter returns successfully; abandonment is never delivery. */
331
+ terminalPublished: boolean;
332
+ terminalPublicationState: TerminalPublicationState;
333
+ terminalPublicationAbandonReason?: TerminalPublicationAbandonReason | undefined;
334
+ terminalPublishAttempts: number;
141
335
  terminalPublishInFlight?: boolean | undefined;
336
+ /** True only while the synchronous terminal emitter itself is on the stack. */
337
+ terminalEmitInFlight?: boolean | undefined;
142
338
  terminalPublishRetryHandle?: NodeJS.Timeout | undefined;
143
339
  /** Optional protocol barrier used by EventBus run requests so early child exits cannot publish before the run response is observable. */
144
340
  terminalPublicationGate?: Promise<void> | undefined;
@@ -152,6 +348,12 @@ export interface BgTask extends Omit<BgTaskSnapshot, 'name'> {
152
348
  attestedPi?: AttestedPiTaskFiles | undefined;
153
349
  delegate?: DelegateTaskFacts | undefined;
154
350
  fusion?: FusionTaskFacts | undefined;
351
+ /** In-memory same-process authority for an opted ordinary shell task; never serialized. */
352
+ reloadExecution?: ReloadableShellExecutionV1 | undefined;
353
+ /** Fresh-registry terminal host delivery state for an imported owner execution. */
354
+ reloadHostDeliveryInFlight?: boolean | undefined;
355
+ reloadHostDeliverySettled?: boolean | undefined;
356
+ reloadHostNotificationSettled?: boolean | undefined;
155
357
  /** Cancellation hook for an in-process managed task such as Fusion. */
156
358
  managedCancel?: (() => void) | undefined;
157
359
  managedCancelRequested?: boolean | undefined;
@@ -248,6 +450,7 @@ export interface StartTaskOptions {
248
450
  timeoutSeconds?: number | undefined;
249
451
  notifyOnCompletion?: boolean | undefined;
250
452
  triggerOnCompletion?: boolean | undefined;
453
+ surviveReload?: boolean | undefined;
251
454
  /** @internal EventBus protocol barrier; callers should not set this outside the extension service. */
252
455
  terminalPublicationGate?: Promise<void> | undefined;
253
456
  }
@@ -407,10 +610,12 @@ export function parseBgCommandArgs(args: string): {
407
610
  name?: string;
408
611
  command: string;
409
612
  isAgent: boolean;
613
+ surviveReload: boolean;
410
614
  } {
411
615
  let input = args.trim();
412
616
  let name: string | undefined;
413
617
  let isAgent = false;
618
+ let surviveReload = false;
414
619
 
415
620
  while (input) {
416
621
  let consumed = false;
@@ -458,6 +663,28 @@ export function parseBgCommandArgs(args: string): {
458
663
  }
459
664
  if (consumed) continue;
460
665
 
666
+ if (
667
+ input === '--survive-reload' ||
668
+ input.startsWith('--survive-reload ') ||
669
+ input.startsWith('--survive-reload\t')
670
+ ) {
671
+ if (surviveReload) {
672
+ throw new ReloadSurvivalError(
673
+ 'pi_bg_survive_reload_invalid',
674
+ '/bg accepts --survive-reload at most once',
675
+ );
676
+ }
677
+ surviveReload = true;
678
+ input = input.slice('--survive-reload'.length).trimStart();
679
+ continue;
680
+ }
681
+ if (input.startsWith('--survive-reload=')) {
682
+ throw new ReloadSurvivalError(
683
+ 'pi_bg_survive_reload_invalid',
684
+ '/bg accepts only the bare --survive-reload flag',
685
+ );
686
+ }
687
+
461
688
  if (input === '--') {
462
689
  input = '';
463
690
  break;
@@ -469,7 +696,15 @@ export function parseBgCommandArgs(args: string): {
469
696
  break;
470
697
  }
471
698
 
472
- return name ? { name, command: input, isAgent } : { command: input, isAgent };
699
+ if (surviveReload && isAgent) {
700
+ throw new ReloadSurvivalError(
701
+ 'pi_bg_survive_reload_requires_non_agent',
702
+ 'surviveReload requires isAgent:false',
703
+ );
704
+ }
705
+ return name
706
+ ? { name, command: input, isAgent, surviveReload }
707
+ : { command: input, isAgent, surviveReload };
473
708
  }
474
709
 
475
710
  export function formatDuration(ms: number): string {
@@ -611,7 +846,23 @@ export function shellQuote(value: string): string {
611
846
  return `'${value.replace(/'/g, `'"'"'`)}'`;
612
847
  }
613
848
 
614
- export type ShellDialect = 'cmd' | 'posix';
849
+ export type ShellDialect = 'cmd' | 'posix' | 'user-non-posix';
850
+ export type ShellPolicyDialect = ShellDialect | 'bash';
851
+ export type ShellPolicyName = 'inherit' | 'bash' | 'sh' | 'cmd';
852
+
853
+ /** Non-secret launch facts persisted for ordinary shell tasks. */
854
+ export interface ShellPolicySnapshot {
855
+ readonly policy: ShellPolicyName;
856
+ readonly executable: string;
857
+ readonly argvPrefix: readonly string[];
858
+ readonly dialect: ShellPolicyDialect;
859
+ }
860
+
861
+ /** One immutable shell selection shared by guidance and every ordinary spawn in an activation. */
862
+ export interface ResolvedShellPolicy extends ShellPolicySnapshot {
863
+ readonly supportsPosixFunctionWrapper: boolean;
864
+ readonly windowsVerbatimArguments: boolean;
865
+ }
615
866
 
616
867
  export interface ShellInvocation {
617
868
  shell: string;
@@ -633,6 +884,19 @@ type ShellCandidateResult =
633
884
  | { readonly found: true }
634
885
  | { readonly found: false; readonly diagnostic: string };
635
886
 
887
+ const POSIX_FUNCTION_SHELLS = new Set([
888
+ 'sh',
889
+ 'dash',
890
+ 'ash',
891
+ 'ksh',
892
+ 'ksh93',
893
+ 'mksh',
894
+ 'pdksh',
895
+ 'zsh',
896
+ 'yash',
897
+ 'posh',
898
+ ]);
899
+
636
900
  function failShellInvocation(message: string): never {
637
901
  throw new ShellInvocationError(message);
638
902
  }
@@ -641,6 +905,24 @@ function shellErrorMessage(error: unknown): string {
641
905
  return error instanceof Error ? error.message : String(error);
642
906
  }
643
907
 
908
+ function freezeShellPolicy(
909
+ policy: Omit<ResolvedShellPolicy, 'argvPrefix'> & { argvPrefix: readonly string[] },
910
+ ): ResolvedShellPolicy {
911
+ return Object.freeze({
912
+ ...policy,
913
+ argvPrefix: Object.freeze([...policy.argvPrefix]),
914
+ });
915
+ }
916
+
917
+ export function shellPolicySnapshot(policy: ResolvedShellPolicy): ShellPolicySnapshot {
918
+ return Object.freeze({
919
+ policy: policy.policy,
920
+ executable: policy.executable,
921
+ argvPrefix: Object.freeze([...policy.argvPrefix]),
922
+ dialect: policy.dialect,
923
+ });
924
+ }
925
+
644
926
  function isWindowsExecutablePath(path: string): boolean {
645
927
  const extension = extname(path).toLowerCase();
646
928
  return extension === '.exe' || extension === '.com';
@@ -696,51 +978,176 @@ function resolveWindowsBash(env: NodeJS.ProcessEnv): string {
696
978
  failShellInvocation(`PI_BG_SHELL=bash could not resolve bash.exe or bash.com on PATH${suffix}`);
697
979
  }
698
980
 
699
- function cmdShellInvocation(command: string, shell: string): ShellInvocation {
700
- return {
701
- shell,
702
- args: ['/d', '/s', '/c', `"${command}"`],
703
- dialect: 'cmd',
704
- windowsVerbatimArguments: true,
705
- };
981
+ function validatePosixShellPath(path: string, label: string): string {
982
+ if (path.length === 0) failShellInvocation(`${label} is empty`);
983
+ if (!isAbsolute(path)) failShellInvocation(`${label} must be an absolute path`);
984
+ let stats: ReturnType<typeof statSync>;
985
+ try {
986
+ stats = statSync(path);
987
+ } catch (error) {
988
+ failShellInvocation(`${label} stat failed: ${shellErrorMessage(error)}`);
989
+ }
990
+ if (!stats.isFile()) failShellInvocation(`${label} must point to a regular file`);
991
+ try {
992
+ accessSync(path, constants.X_OK);
993
+ } catch (error) {
994
+ failShellInvocation(`${label} must be executable: ${shellErrorMessage(error)}`);
995
+ }
996
+ return path;
706
997
  }
707
998
 
708
- function posixShellInvocation(command: string, shell: string): ShellInvocation {
709
- return { shell, args: ['-c', command], dialect: 'posix', windowsVerbatimArguments: false };
999
+ function inspectPosixShellCandidate(path: string): boolean {
1000
+ try {
1001
+ const stats = statSync(path);
1002
+ if (!stats.isFile()) return false;
1003
+ accessSync(path, constants.X_OK);
1004
+ return true;
1005
+ } catch {
1006
+ return false;
1007
+ }
710
1008
  }
711
1009
 
712
- export function shellInvocation(
713
- command: string,
1010
+ function resolvePosixExecutable(
1011
+ name: 'bash' | 'sh',
1012
+ env: NodeJS.ProcessEnv,
1013
+ activationCwd: string,
1014
+ ): string {
1015
+ const binCandidate = `/bin/${name}`;
1016
+ if (inspectPosixShellCandidate(binCandidate)) return binCandidate;
1017
+ const pathValue = env['PATH'] ?? '';
1018
+ for (const entry of pathValue.split(delimiter)) {
1019
+ if (entry.length === 0) continue;
1020
+ const directory = isAbsolute(entry) ? entry : resolve(activationCwd, entry);
1021
+ const candidate = join(directory, name);
1022
+ if (inspectPosixShellCandidate(candidate)) return candidate;
1023
+ }
1024
+ failShellInvocation(
1025
+ `PI_BG_POSIX_SHELL=${name} could not resolve executable ${binCandidate} or ${name} on PATH`,
1026
+ );
1027
+ }
1028
+
1029
+ function inheritedPosixDialect(executable: string): ShellPolicyDialect {
1030
+ const name = basename(executable).toLowerCase();
1031
+ if (name === 'bash') return 'bash';
1032
+ return POSIX_FUNCTION_SHELLS.has(name) ? 'posix' : 'user-non-posix';
1033
+ }
1034
+
1035
+ /** Resolve one activation-stable policy. New POSIX variables are intentionally ignored on Windows. */
1036
+ export function resolveShellPolicy(
714
1037
  platform: NodeJS.Platform = process.platform,
715
1038
  env: NodeJS.ProcessEnv = process.env,
716
- ): ShellInvocation {
717
- if (platform !== 'win32') {
718
- const shell = env['SHELL'];
719
- return posixShellInvocation(command, shell && shell.length > 0 ? shell : '/bin/sh');
1039
+ activationCwd: string = process.cwd(),
1040
+ ): ResolvedShellPolicy {
1041
+ if (platform === 'win32') {
1042
+ const requestedShell = env['PI_BG_SHELL'];
1043
+ const requestedPath = env['PI_BG_SHELL_PATH'];
1044
+ if (requestedShell === undefined) {
1045
+ if (requestedPath !== undefined)
1046
+ failShellInvocation('PI_BG_SHELL_PATH requires PI_BG_SHELL');
1047
+ const comSpec = env['ComSpec'];
1048
+ return freezeShellPolicy({
1049
+ policy: 'cmd',
1050
+ executable: comSpec && comSpec.length > 0 ? comSpec : 'cmd.exe',
1051
+ argvPrefix: ['/d', '/s', '/c'],
1052
+ dialect: 'cmd',
1053
+ supportsPosixFunctionWrapper: false,
1054
+ windowsVerbatimArguments: true,
1055
+ });
1056
+ }
1057
+ if (requestedShell !== 'cmd' && requestedShell !== 'bash') {
1058
+ failShellInvocation('PI_BG_SHELL must be exactly cmd or bash');
1059
+ }
1060
+ const explicitPath =
1061
+ requestedPath !== undefined
1062
+ ? validateWindowsShellPath(requestedPath, 'PI_BG_SHELL_PATH')
1063
+ : undefined;
1064
+ if (requestedShell === 'cmd') {
1065
+ const comSpec = env['ComSpec'];
1066
+ return freezeShellPolicy({
1067
+ policy: 'cmd',
1068
+ executable: explicitPath ?? (comSpec && comSpec.length > 0 ? comSpec : 'cmd.exe'),
1069
+ argvPrefix: ['/d', '/s', '/c'],
1070
+ dialect: 'cmd',
1071
+ supportsPosixFunctionWrapper: false,
1072
+ windowsVerbatimArguments: true,
1073
+ });
1074
+ }
1075
+ return freezeShellPolicy({
1076
+ policy: 'bash',
1077
+ executable: explicitPath ?? resolveWindowsBash(env),
1078
+ argvPrefix: ['-c'],
1079
+ dialect: 'bash',
1080
+ supportsPosixFunctionWrapper: true,
1081
+ windowsVerbatimArguments: false,
1082
+ });
720
1083
  }
721
1084
 
722
- const requestedShell = env['PI_BG_SHELL'];
723
- const requestedPath = env['PI_BG_SHELL_PATH'];
724
- if (requestedShell === undefined) {
725
- if (requestedPath !== undefined) failShellInvocation('PI_BG_SHELL_PATH requires PI_BG_SHELL');
726
- const comSpec = env['ComSpec'];
727
- return cmdShellInvocation(command, comSpec && comSpec.length > 0 ? comSpec : 'cmd.exe');
728
- }
729
- if (requestedShell !== 'cmd' && requestedShell !== 'bash') {
730
- failShellInvocation('PI_BG_SHELL must be exactly cmd or bash');
1085
+ const configuredPolicy = env['PI_BG_POSIX_SHELL'];
1086
+ if (
1087
+ configuredPolicy !== undefined &&
1088
+ configuredPolicy !== 'inherit' &&
1089
+ configuredPolicy !== 'bash' &&
1090
+ configuredPolicy !== 'sh'
1091
+ ) {
1092
+ failShellInvocation('PI_BG_POSIX_SHELL must be exactly inherit, bash, or sh');
731
1093
  }
732
- const explicitPath =
733
- requestedPath !== undefined
734
- ? validateWindowsShellPath(requestedPath, 'PI_BG_SHELL_PATH')
735
- : undefined;
736
- if (requestedShell === 'cmd') {
737
- const comSpec = env['ComSpec'];
738
- return cmdShellInvocation(
739
- command,
740
- explicitPath ?? (comSpec && comSpec.length > 0 ? comSpec : 'cmd.exe'),
741
- );
1094
+ const policy = configuredPolicy ?? 'inherit';
1095
+ const configuredPath = env['PI_BG_POSIX_SHELL_PATH'];
1096
+ if (policy === 'inherit') {
1097
+ if (configuredPath !== undefined) {
1098
+ failShellInvocation(
1099
+ 'PI_BG_POSIX_SHELL_PATH requires PI_BG_POSIX_SHELL=bash or PI_BG_POSIX_SHELL=sh',
1100
+ );
1101
+ }
1102
+ const inherited = env['SHELL'];
1103
+ const executable = inherited && inherited.length > 0 ? inherited : '/bin/sh';
1104
+ const dialect = inheritedPosixDialect(executable);
1105
+ return freezeShellPolicy({
1106
+ policy,
1107
+ executable,
1108
+ argvPrefix: ['-c'],
1109
+ dialect,
1110
+ supportsPosixFunctionWrapper: dialect === 'bash' || dialect === 'posix',
1111
+ windowsVerbatimArguments: false,
1112
+ });
742
1113
  }
743
- return posixShellInvocation(command, explicitPath ?? resolveWindowsBash(env));
1114
+
1115
+ const executable =
1116
+ configuredPath !== undefined
1117
+ ? validatePosixShellPath(configuredPath, 'PI_BG_POSIX_SHELL_PATH')
1118
+ : resolvePosixExecutable(policy, env, activationCwd);
1119
+ return freezeShellPolicy({
1120
+ policy,
1121
+ executable,
1122
+ argvPrefix: ['-c'],
1123
+ dialect: policy === 'bash' ? 'bash' : 'posix',
1124
+ supportsPosixFunctionWrapper: true,
1125
+ windowsVerbatimArguments: false,
1126
+ });
1127
+ }
1128
+
1129
+ export function shellInvocationForPolicy(
1130
+ command: string,
1131
+ policy: ResolvedShellPolicy,
1132
+ ): ShellInvocation {
1133
+ const dialect: ShellDialect = policy.dialect === 'bash' ? 'posix' : policy.dialect;
1134
+ return {
1135
+ shell: policy.executable,
1136
+ args:
1137
+ policy.dialect === 'cmd'
1138
+ ? [...policy.argvPrefix, `"${command}"`]
1139
+ : [...policy.argvPrefix, command],
1140
+ dialect,
1141
+ windowsVerbatimArguments: policy.windowsVerbatimArguments,
1142
+ };
1143
+ }
1144
+
1145
+ export function shellInvocation(
1146
+ command: string,
1147
+ platform: NodeJS.Platform = process.platform,
1148
+ env: NodeJS.ProcessEnv = process.env,
1149
+ ): ShellInvocation {
1150
+ return shellInvocationForPolicy(command, resolveShellPolicy(platform, env));
744
1151
  }
745
1152
 
746
1153
  export function normalizeMaxBytes(value: unknown, fallback = DEFAULT_LOG_BYTES): number {
@@ -764,6 +1171,8 @@ export function snapshot(task: BgTask): BgTaskSnapshot {
764
1171
  pid: task.pid,
765
1172
  bytesWritten: task.bytesWritten,
766
1173
  isAgent: task.isAgent,
1174
+ surviveReload: task.surviveReload === true,
1175
+ reloadSurvival: task.reloadSurvival,
767
1176
  error: task.error,
768
1177
  notified: task.notified,
769
1178
  notifyOnCompletion: task.notifyOnCompletion,
@@ -774,6 +1183,7 @@ export function snapshot(task: BgTask): BgTaskSnapshot {
774
1183
  toolUsage: task.toolUsage,
775
1184
  model: task.model,
776
1185
  telemetryUnavailableReason: task.telemetryUnavailableReason,
1186
+ shellPolicy: task.shellPolicy,
777
1187
  attestationPath: task.attestationPath,
778
1188
  delegate: task.delegate,
779
1189
  fusion: task.fusion,