pi-long-task 0.5.0 → 0.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.
@@ -1,3 +1,9 @@
1
+ import {
2
+ NetworkRecoveryConfigError,
3
+ resolveNetworkRecoveryConfig,
4
+ type NetworkRecoveryConfigInput,
5
+ } from "./network_recovery_config.ts";
6
+
1
7
  export interface ParsedWorkerRuntimeConfig {
2
8
  modelName?: string;
3
9
  maxAttemptsPerTask?: number;
@@ -5,6 +11,7 @@ export interface ParsedWorkerRuntimeConfig {
5
11
  maxBashTimeoutMs?: number;
6
12
  workerSessionReuseEnabled?: boolean;
7
13
  workerSessionReuseContextThresholdPercent?: number;
14
+ networkRecovery?: NetworkRecoveryConfigInput;
8
15
  }
9
16
 
10
17
  type MutableWorkerRuntimeConfig = ParsedWorkerRuntimeConfig & { provider?: string; model?: string };
@@ -36,6 +43,9 @@ export function parseWorkerRuntimeConfig(text: string): ParsedWorkerRuntimeConfi
36
43
 
37
44
  parseLineDirectives(text, state);
38
45
  parseNaturalLanguageDirectives(text, state);
46
+ if (state.networkRecovery) {
47
+ resolveNetworkRecoveryConfig(state.networkRecovery);
48
+ }
39
49
 
40
50
  const modelName = combineProviderAndModel(state.provider, state.model);
41
51
  return {
@@ -49,6 +59,7 @@ export function parseWorkerRuntimeConfig(text: string): ParsedWorkerRuntimeConfi
49
59
  ...(state.workerSessionReuseContextThresholdPercent !== undefined
50
60
  ? { workerSessionReuseContextThresholdPercent: state.workerSessionReuseContextThresholdPercent }
51
61
  : {}),
62
+ ...(state.networkRecovery ? { networkRecovery: { ...state.networkRecovery } } : {}),
52
63
  };
53
64
  }
54
65
 
@@ -159,6 +170,11 @@ function parseNaturalLanguageDirectives(text: string, state: MutableWorkerRuntim
159
170
  }
160
171
 
161
172
  function applyDirective(key: string, value: string, state: MutableWorkerRuntimeConfig): void {
173
+ if (/\bnetwork\b/.test(key) && /\brecover(?:y|ies)?\b/.test(key)) {
174
+ applyNetworkRecoveryDirective(key, value, state);
175
+ return;
176
+ }
177
+
162
178
  if (/\breuse\b/.test(key) && /\b(?:threshold|context)\b/.test(key)) {
163
179
  const threshold = percentageFromText(value);
164
180
  if (threshold !== undefined) {
@@ -215,6 +231,64 @@ function applyDirective(key: string, value: string, state: MutableWorkerRuntimeC
215
231
  }
216
232
  }
217
233
 
234
+ function applyNetworkRecoveryDirective(key: string, value: string, state: MutableWorkerRuntimeConfig): void {
235
+ const recovery = (state.networkRecovery ??= {});
236
+
237
+ if (/\bbase\b/.test(key) && /\bdelay\b/.test(key)) {
238
+ recovery.baseDelayMs = requiredNetworkRecoveryDuration("base delay", value);
239
+ return;
240
+ }
241
+ if (/\bmax(?:imum)?\b/.test(key) && /\bdelay\b/.test(key)) {
242
+ recovery.maxDelayMs = requiredNetworkRecoveryDuration("maximum delay", value);
243
+ return;
244
+ }
245
+ if (/\b(?:outage|duration|wait)\b/.test(key)) {
246
+ const normalized = trimDirectiveValue(value)
247
+ .toLowerCase()
248
+ .replace(/[.!]+$/g, "")
249
+ .trim();
250
+ if (/^(?:unlimited|indefinite|indefinitely|until cancelled|until canceled)$/.test(normalized)) {
251
+ recovery.maxOutageMs = null;
252
+ return;
253
+ }
254
+ recovery.maxOutageMs = requiredNetworkRecoveryDuration("maximum outage", value);
255
+ return;
256
+ }
257
+ if (/\b(?:enabled?|enablement)\b/.test(key) || /\bnetwork recovery\b/.test(key)) {
258
+ const enabled = booleanSetting(value);
259
+ if (enabled === undefined) {
260
+ throw new NetworkRecoveryConfigError(
261
+ "Network recovery must be configured as enabled/disabled, on/off, true/false, or yes/no.",
262
+ );
263
+ }
264
+ recovery.enabled = enabled;
265
+ return;
266
+ }
267
+
268
+ throw new NetworkRecoveryConfigError(`Unknown network recovery configuration directive: ${key}.`);
269
+ }
270
+
271
+ function requiredNetworkRecoveryDuration(label: string, value: string): number {
272
+ const trimmed = trimDirectiveValue(value)
273
+ .replace(/[.!]+$/g, "")
274
+ .trim();
275
+ const match = /^(\d+(?:\.\d+)?)\s*(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h)?$/i.exec(
276
+ trimmed,
277
+ );
278
+ if (!match) {
279
+ throw new NetworkRecoveryConfigError(
280
+ `Network recovery ${label} must be a positive finite duration (for example 1000ms, 30s, or 5m).`,
281
+ );
282
+ }
283
+ const milliseconds = durationMsFromText(trimmed, { allowBareSeconds: true });
284
+ if (milliseconds === undefined) {
285
+ throw new NetworkRecoveryConfigError(
286
+ `Network recovery ${label} must be a positive finite duration (for example 1000ms, 30s, or 5m).`,
287
+ );
288
+ }
289
+ return milliseconds;
290
+ }
291
+
218
292
  function captureTokens(text: string, pattern: RegExp, apply: (token: string) => void): void {
219
293
  for (const match of text.matchAll(pattern)) {
220
294
  const token = modelToken(match[1] ?? "");
@@ -8,14 +8,25 @@ import {
8
8
  parseCompleteTaskResult,
9
9
  parseReportedStatus,
10
10
  } from "./result_writer.ts";
11
+ import type { NetworkRecoveryConfig } from "./network_recovery_config.ts";
11
12
  import type { Task } from "./todo_parser.ts";
12
13
 
14
+ export interface WorkerNetworkRecoveryContext {
15
+ /** One-based coordinator network retry count; this is not a task attempt. */
16
+ retryCount: number;
17
+ durableEvidencePath: string;
18
+ priorSessionId?: string;
19
+ failure: string;
20
+ }
21
+
13
22
  export interface WorkerTaskPromptOptions {
14
23
  todoPath: string;
15
24
  task: Pick<Task, "taskId" | "title" | "section">;
16
25
  attempt: number;
17
26
  commitRequested: boolean;
18
27
  previousAttempts?: string;
28
+ /** Continuity supplied only when a failed transport session is replaced. */
29
+ networkRecoveryContext?: WorkerNetworkRecoveryContext;
19
30
  globalInstructions?: string;
20
31
  goal?: string;
21
32
  maxBashTimeoutSeconds: number;
@@ -43,6 +54,18 @@ Previous attempts for this same assigned task are below. Use them only as contin
43
54
  \`\`\`text
44
55
  ${previousAttempts}
45
56
  \`\`\`
57
+ `
58
+ : "";
59
+
60
+ const recovery = options.networkRecoveryContext;
61
+ const recoveryText = recovery
62
+ ? `
63
+ Network recovery continuation (network retry ${recovery.retryCount}, still ordinary task attempt ${options.attempt}):
64
+ - The prior worker session ended only after Pi's bounded provider-request retries were exhausted: ${recovery.failure}
65
+ - Durable interruption evidence was recorded in \`${recovery.durableEvidencePath}\`${recovery.priorSessionId ? ` for session \`${recovery.priorSessionId}\`` : ""}.
66
+ - The prior session may already have completed tool calls and changed the working tree. Inspect the durable evidence and current files before acting.
67
+ - Continue this same TODO from its current state. Never blindly replay prior edits, commands, commits, external writes, or other side effects.
68
+ - Report one final TASK_RESULT for the assignment only after verifying what remains.
46
69
  `
47
70
  : "";
48
71
 
@@ -93,7 +116,7 @@ Rules:
93
116
  - Do not run bash commands with timeout greater than ${options.maxBashTimeoutSeconds.toFixed(0)} seconds. For long full-suite checks, run once with a bounded timeout and report any timeout/failure in TASK_RESULT instead of continuing indefinitely.
94
117
  - If TODO-file global instructions restrict scope, obey them strictly. If the task appears to require out-of-scope code changes, stop and report \`status: blocked\` instead of changing those files.
95
118
 
96
- ${globalText}Assigned task content only:
119
+ ${globalText}${recoveryText}Assigned task content only:
97
120
 
98
121
  \`\`\`markdown
99
122
  ${options.task.section.trimEnd()}
@@ -289,6 +312,8 @@ export type WorkerSessionFactory = (options: CreateWorkerSessionOptions) => Prom
289
312
  export interface RunWorkerTaskOptions extends WorkerTaskPromptOptions, CreateWorkerSessionOptions {
290
313
  taskTimeoutSeconds?: number;
291
314
  gracefulShutdownSeconds?: number;
315
+ /** Normalized coordinator policy for resuming this operation after transient transport failure. */
316
+ networkRecovery?: Readonly<NetworkRecoveryConfig>;
292
317
  abortSignal?: AbortSignal;
293
318
  sessionFactory?: WorkerSessionFactory;
294
319
  onEvent?: (event: CapturedWorkerEvent) => void;
@@ -353,6 +378,8 @@ export interface SessionOutcome {
353
378
  timedOut: boolean;
354
379
  aborted: boolean;
355
380
  error?: string;
381
+ /** Original provider/transport failure retained for coordinator classification. */
382
+ failure?: unknown;
356
383
  }
357
384
 
358
385
  export function buildMissingTaskResultMessage(): string {
@@ -512,6 +539,7 @@ export async function runWorkerTaskAssignment(
512
539
  let timedOut = false;
513
540
  let aborted = false;
514
541
  let error: string | undefined;
542
+ let failure: unknown;
515
543
  let finished = false;
516
544
  let turnCount = 0;
517
545
  let messageUsageCostTotal = 0;
@@ -637,6 +665,7 @@ export async function runWorkerTaskAssignment(
637
665
  },
638
666
  (exc: unknown) => {
639
667
  settled = true;
668
+ failure ??= exc;
640
669
  error = error ?? errorMessage(exc);
641
670
  resolvePromptWait?.();
642
671
  resolvePromptWait = undefined;
@@ -773,6 +802,7 @@ export async function runWorkerTaskAssignment(
773
802
  assistantText = latestInvocationAssistantText(session, assistantText, invocationMessageStart, !reusedAssignment);
774
803
  }
775
804
  } catch (exc) {
805
+ failure ??= exc;
776
806
  error = error ?? errorMessage(exc);
777
807
  } finally {
778
808
  finished = true;
@@ -823,6 +853,7 @@ export async function runWorkerTaskAssignment(
823
853
  timedOut,
824
854
  aborted: aborted || cancelled,
825
855
  error,
856
+ ...(failure === undefined ? {} : { failure }),
826
857
  };
827
858
  }
828
859
 
@@ -872,6 +903,7 @@ export function buildWorkerSessionCreationFailureOutcome(
872
903
  timedOut: false,
873
904
  aborted: Boolean(options.abortSignal?.aborted),
874
905
  error: message,
906
+ failure: error,
875
907
  };
876
908
  }
877
909