@bastani/atomic 0.8.26-alpha.5 → 0.8.26-alpha.6

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 (36) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/builtin/intercom/CHANGELOG.md +6 -0
  3. package/dist/builtin/intercom/package.json +1 -1
  4. package/dist/builtin/mcp/CHANGELOG.md +6 -0
  5. package/dist/builtin/mcp/package.json +1 -1
  6. package/dist/builtin/subagents/CHANGELOG.md +6 -0
  7. package/dist/builtin/subagents/package.json +1 -1
  8. package/dist/builtin/subagents/src/runs/background/subagent-runner.ts +48 -10
  9. package/dist/builtin/subagents/src/runs/foreground/execution.ts +30 -9
  10. package/dist/builtin/subagents/src/runs/shared/final-drain.ts +34 -0
  11. package/dist/builtin/subagents/src/runs/shared/model-fallback.ts +416 -7
  12. package/dist/builtin/web-access/CHANGELOG.md +6 -0
  13. package/dist/builtin/web-access/package.json +1 -1
  14. package/dist/builtin/workflows/CHANGELOG.md +6 -0
  15. package/dist/builtin/workflows/package.json +1 -1
  16. package/dist/builtin/workflows/src/extension/index.ts +10 -2
  17. package/dist/builtin/workflows/src/extension/runtime.ts +35 -3
  18. package/dist/builtin/workflows/src/runs/background/status.ts +52 -6
  19. package/dist/builtin/workflows/src/runs/foreground/executor.ts +441 -15
  20. package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +69 -8
  21. package/dist/builtin/workflows/src/runs/shared/model-fallback.ts +402 -8
  22. package/dist/builtin/workflows/src/shared/persistence-restore.ts +182 -6
  23. package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +76 -6
  24. package/dist/builtin/workflows/src/shared/stage-prompt.ts +33 -2
  25. package/dist/builtin/workflows/src/shared/store-types.ts +31 -0
  26. package/dist/builtin/workflows/src/shared/store.ts +99 -11
  27. package/dist/builtin/workflows/src/shared/workflow-failures.ts +758 -132
  28. package/dist/core/tools/ask-user-question/tool/format-answer.d.ts +5 -5
  29. package/dist/core/tools/ask-user-question/tool/format-answer.d.ts.map +1 -1
  30. package/dist/core/tools/ask-user-question/tool/format-answer.js +5 -5
  31. package/dist/core/tools/ask-user-question/tool/format-answer.js.map +1 -1
  32. package/dist/core/tools/ask-user-question/tool/response-envelope.d.ts +16 -3
  33. package/dist/core/tools/ask-user-question/tool/response-envelope.d.ts.map +1 -1
  34. package/dist/core/tools/ask-user-question/tool/response-envelope.js +21 -3
  35. package/dist/core/tools/ask-user-question/tool/response-envelope.js.map +1 -1
  36. package/package.json +1 -1
@@ -62,7 +62,7 @@ export function currentModelFullId(model: { provider: string; id: string } | und
62
62
  return `${String(model.provider)}/${model.id}`;
63
63
  }
64
64
 
65
- const RETRYABLE_MODEL_FAILURE_PATTERNS = [
65
+ const RETRYABLE_MODEL_FAILURE_PATTERNS: readonly RegExp[] = [
66
66
  /rate\s*limit/i,
67
67
  /too many requests/i,
68
68
  /\b429\b/,
@@ -71,6 +71,7 @@ const RETRYABLE_MODEL_FAILURE_PATTERNS = [
71
71
  /credit/i,
72
72
  /auth(?:entication)?/i,
73
73
  /unauthori[sz]ed/i,
74
+ /\b40[13]\b/,
74
75
  /forbidden/i,
75
76
  /api key/i,
76
77
  /token expired/i,
@@ -90,14 +91,422 @@ const RETRYABLE_MODEL_FAILURE_PATTERNS = [
90
91
  /upstream/i,
91
92
  /timed? out/i,
92
93
  /timeout/i,
93
- /\b502\b/,
94
- /\b503\b/,
95
- /\b504\b/,
94
+ /\b50[0-4]\b/,
96
95
  ];
97
96
 
98
- export function isRetryableModelFailure(error: string | undefined): boolean {
99
- if (!error) return false;
100
- return RETRYABLE_MODEL_FAILURE_PATTERNS.some((pattern) => pattern.test(error));
97
+ const NON_RETRYABLE_FAILURE_PATTERNS: readonly RegExp[] = [
98
+ /command failed/i,
99
+ /tests? failed/i,
100
+ /shell/i,
101
+ /missing file/i,
102
+ /no such file/i,
103
+ /completion guard/i,
104
+ /cancel/i,
105
+ /abort/i,
106
+ /interrupted/i,
107
+ ];
108
+
109
+ const CANCELLED_FAILURE_PATTERNS: readonly RegExp[] = [
110
+ /cancel/i,
111
+ /abort/i,
112
+ /interrupted/i,
113
+ ];
114
+
115
+ export type ModelFallbackFailureKind =
116
+ | "auth_on_candidate_provider"
117
+ | "rate_limit"
118
+ | "provider_unavailable"
119
+ | "network_timeout"
120
+ | "model_unavailable"
121
+ | "cancelled"
122
+ | "task_failure"
123
+ | "unknown";
124
+
125
+ export type ModelFallbackFailureSource =
126
+ | "assistant_message"
127
+ | "diagnostic"
128
+ | "throw"
129
+ | "structured"
130
+ | "string_fallback";
131
+
132
+ export interface ModelFallbackFailureSignal {
133
+ readonly kind: ModelFallbackFailureKind;
134
+ readonly message: string;
135
+ readonly source: ModelFallbackFailureSource;
136
+ readonly stopReason?: string;
137
+ readonly status?: number;
138
+ readonly code?: string | number;
139
+ readonly name?: string;
140
+ }
141
+
142
+ const FALLBACKABLE_FAILURE_KINDS: ReadonlySet<ModelFallbackFailureKind> = new Set([
143
+ "auth_on_candidate_provider",
144
+ "rate_limit",
145
+ "provider_unavailable",
146
+ "network_timeout",
147
+ "model_unavailable",
148
+ ]);
149
+
150
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
151
+ return value !== null && typeof value === "object" ? value as Record<string, unknown> : undefined;
152
+ }
153
+
154
+ function field(value: unknown, key: string): unknown {
155
+ return asRecord(value)?.[key];
156
+ }
157
+
158
+ function stringField(value: unknown, key: string): string | undefined {
159
+ const raw = field(value, key);
160
+ return typeof raw === "string" && raw.trim().length > 0 ? raw : undefined;
161
+ }
162
+
163
+ function errorName(value: unknown): string | undefined {
164
+ return value instanceof Error ? value.name : stringField(value, "name");
165
+ }
166
+
167
+ function directMessageFrom(value: unknown): string | undefined {
168
+ return stringField(value, "errorMessage")
169
+ ?? stringField(value, "message")
170
+ ?? stringField(value, "statusText");
171
+ }
172
+
173
+ function integerFrom(value: unknown): number | undefined {
174
+ if (typeof value === "number" && Number.isInteger(value)) return value;
175
+ if (typeof value !== "string" || value.trim().length === 0) return undefined;
176
+ const parsed = Number(value.trim());
177
+ return Number.isInteger(parsed) ? parsed : undefined;
178
+ }
179
+
180
+ function statusFrom(value: unknown): number | undefined {
181
+ return integerFrom(field(value, "status"))
182
+ ?? integerFrom(field(value, "statusCode"))
183
+ ?? integerFrom(field(value, "httpStatus"));
184
+ }
185
+
186
+ function codeFrom(value: unknown): string | number | undefined {
187
+ const rawCode = field(value, "code");
188
+ return typeof rawCode === "string" || typeof rawCode === "number" ? rawCode : undefined;
189
+ }
190
+
191
+ function stopReasonFrom(value: unknown): string | undefined {
192
+ return stringField(value, "stopReason");
193
+ }
194
+
195
+ function finishReasonFrom(value: unknown): string | undefined {
196
+ return stringField(value, "finish_reason") ?? stringField(value, "finishReason");
197
+ }
198
+
199
+ function causeOf(value: unknown): unknown {
200
+ return value instanceof Error ? value.cause : field(value, "cause");
201
+ }
202
+
203
+ function diagnosticErrors(value: unknown): readonly unknown[] {
204
+ const diagnostics = field(value, "diagnostics");
205
+ if (!Array.isArray(diagnostics)) return [];
206
+ const errors: unknown[] = [];
207
+ for (const diagnostic of diagnostics) {
208
+ const diagnosticError = field(diagnostic, "error");
209
+ errors.push(diagnosticError ?? diagnostic);
210
+ }
211
+ return errors;
212
+ }
213
+
214
+ function normalizeCode(value: string | number | undefined): string | undefined {
215
+ if (value === undefined) return undefined;
216
+ const normalized = String(value)
217
+ .trim()
218
+ .toLowerCase()
219
+ .replace(/[^a-z0-9]+/g, "_")
220
+ .replace(/^_+|_+$/g, "");
221
+ return normalized.length > 0 ? normalized : undefined;
222
+ }
223
+
224
+ function kindFromStatus(status: number | undefined): ModelFallbackFailureKind | undefined {
225
+ switch (status) {
226
+ case 401:
227
+ case 403:
228
+ return "auth_on_candidate_provider";
229
+ case 408:
230
+ return "network_timeout";
231
+ case 404:
232
+ return "model_unavailable";
233
+ case 429:
234
+ return "rate_limit";
235
+ default:
236
+ if (status !== undefined && status >= 500 && status <= 599) return "provider_unavailable";
237
+ return undefined;
238
+ }
239
+ }
240
+
241
+ function refusalKindFromCode(code: string | number | undefined): ModelFallbackFailureKind | undefined {
242
+ const normalizedCode = normalizeCode(code);
243
+ if (normalizedCode === undefined) return undefined;
244
+ if (normalizedCode.includes("content_filter") || normalizedCode.includes("contentfilter")) return "task_failure";
245
+ if (normalizedCode.includes("safety") || normalizedCode.includes("policy")) return "task_failure";
246
+ switch (normalizedCode) {
247
+ case "blocked":
248
+ case "blocked_by_provider":
249
+ case "blocked_by_safety":
250
+ case "blocked_by_policy":
251
+ case "provider_refusal":
252
+ case "refusal":
253
+ case "tool_refusal":
254
+ case "tool_call_refusal":
255
+ case "tool_use_refusal":
256
+ return "task_failure";
257
+ default:
258
+ return undefined;
259
+ }
260
+ }
261
+
262
+ function kindFromCode(code: string | number | undefined): ModelFallbackFailureKind | undefined {
263
+ const normalizedCode = normalizeCode(code);
264
+ if (normalizedCode === undefined) return undefined;
265
+ const refusalKind = refusalKindFromCode(code);
266
+ if (refusalKind !== undefined) return refusalKind;
267
+ const httpStatusKind = kindFromStatus(integerFrom(code));
268
+ if (httpStatusKind !== undefined) return httpStatusKind;
269
+
270
+ switch (normalizedCode) {
271
+ case "auth":
272
+ case "auth_required":
273
+ case "authentication_required":
274
+ case "unauthorized":
275
+ case "forbidden":
276
+ case "invalid_api_key":
277
+ case "missing_api_key":
278
+ case "invalid_key":
279
+ return "auth_on_candidate_provider";
280
+ case "etimedout":
281
+ case "econnreset":
282
+ case "econnrefused":
283
+ case "enotfound":
284
+ case "eai_again":
285
+ case "fetch_failed":
286
+ case "network_error":
287
+ case "timeout":
288
+ case "timeout_error":
289
+ case "und_err_connect_timeout":
290
+ return "network_timeout";
291
+ case "rate_limit":
292
+ case "rate_limit_exceeded":
293
+ case "too_many_requests":
294
+ case "quota_exceeded":
295
+ return "rate_limit";
296
+ case "aborterror":
297
+ case "aborted":
298
+ case "cancelled":
299
+ case "canceled":
300
+ return "cancelled";
301
+ case "model_not_found":
302
+ case "model_unavailable":
303
+ case "model_disabled":
304
+ case "unknown_model":
305
+ return "model_unavailable";
306
+ case "provider_error":
307
+ case "api_error":
308
+ case "service_unavailable":
309
+ case "temporarily_unavailable":
310
+ case "overloaded":
311
+ return "provider_unavailable";
312
+ default:
313
+ return undefined;
314
+ }
315
+ }
316
+
317
+ const PROVIDER_REFUSAL_FAILURE_PATTERNS: readonly RegExp[] = [
318
+ /\bfinish[_\s-]?reason\b[^\n]*\bcontent[_\s-]?filter\b/i,
319
+ /\bcontent[_\s-]?filter(?:ed|ing)?\b/i,
320
+ /\b(?:safety|policy)\b[^\n]*\b(?:refus(?:e|al|ed|es|ing)?|block(?:ed|ing)?|filter(?:ed|ing)?|violat(?:e|ion|ed|ing)?|disallow(?:ed|ing)?|reject(?:ed|ion|ing)?)\b/i,
321
+ /\b(?:refus(?:e|al|ed|es|ing)?|block(?:ed|ing)?|filter(?:ed|ing)?|violat(?:e|ion|ed|ing)?|disallow(?:ed|ing)?|reject(?:ed|ion|ing)?)\b[^\n]*\b(?:safety|policy)\b/i,
322
+ /\btool[_\s-]?(?:call|use)?[_\s-]?refus(?:e|al|ed|es|ing)?\b/i,
323
+ /\btool(?:\s+call|\s+use)?\b[^\n]*\brefus(?:e|al|ed|es|ing)?\b/i,
324
+ /\brefus(?:e|al|ed|es|ing)?\b[^\n]*\btool(?:\s+call|\s+use)?\b/i,
325
+ /\bprovider[_\s-]?refus(?:e|al|ed|es|ing)?\b/i,
326
+ /\bprovider\b[^\n]*\brefus(?:e|al|ed|es|ing)?\b[^\n]*\b(?:prompt|request|content|policy|safety)\b/i,
327
+ ];
328
+
329
+ function refusalKindFromMessage(message: string): ModelFallbackFailureKind | undefined {
330
+ if (CANCELLED_FAILURE_PATTERNS.some((pattern) => pattern.test(message))) return "cancelled";
331
+ if (NON_RETRYABLE_FAILURE_PATTERNS.some((pattern) => pattern.test(message))) return "task_failure";
332
+ if (PROVIDER_REFUSAL_FAILURE_PATTERNS.some((pattern) => pattern.test(message))) return "task_failure";
333
+ return undefined;
334
+ }
335
+
336
+ function fallbackKindFromMessage(message: string, name: string | undefined): ModelFallbackFailureKind | undefined {
337
+ const refusalKind = refusalKindFromMessage(message);
338
+ if (refusalKind !== undefined) return refusalKind;
339
+ const nameKind = kindFromCode(name);
340
+ if (nameKind !== undefined) return nameKind;
341
+ if (!RETRYABLE_MODEL_FAILURE_PATTERNS.some((pattern) => pattern.test(message))) return undefined;
342
+ if (/rate\s*limit|too many requests|\b429\b|quota|billing|credit/i.test(message)) return "rate_limit";
343
+ if (/auth|unauthori[sz]ed|\b40[13]\b|api key|token expired|forbidden|invalid key/i.test(message)) return "auth_on_candidate_provider";
344
+ if (/model.*(?:unavailable|disabled|not found|unknown)|(?:unavailable|disabled|not found|unknown).*model/i.test(message)) return "model_unavailable";
345
+ if (/network|fetch failed|socket|connection refused|timeout|timed? out/i.test(message)) return "network_timeout";
346
+ return "provider_unavailable";
347
+ }
348
+
349
+ function signalSource(value: unknown, fallback: ModelFallbackFailureSource | undefined): ModelFallbackFailureSource {
350
+ if (fallback !== undefined) return fallback;
351
+ if (stopReasonFrom(value) !== undefined || diagnosticErrors(value).length > 0) return "assistant_message";
352
+ if (value instanceof Error) return "throw";
353
+ return "structured";
354
+ }
355
+
356
+ function makeSignal(
357
+ kind: ModelFallbackFailureKind,
358
+ value: unknown,
359
+ source: ModelFallbackFailureSource | undefined,
360
+ ): ModelFallbackFailureSignal {
361
+ const status = statusFrom(value);
362
+ const code = codeFrom(value);
363
+ const name = errorName(value);
364
+ const stopReason = stopReasonFrom(value);
365
+ return {
366
+ kind,
367
+ message: modelFailureMessage(value),
368
+ source: signalSource(value, source),
369
+ ...(stopReason !== undefined ? { stopReason } : {}),
370
+ ...(status !== undefined ? { status } : {}),
371
+ ...(code !== undefined ? { code } : {}),
372
+ ...(name !== undefined ? { name } : {}),
373
+ };
374
+ }
375
+
376
+ function fallbackSignalFromMessage(
377
+ value: unknown,
378
+ source: ModelFallbackFailureSource | undefined,
379
+ ): ModelFallbackFailureSignal | undefined {
380
+ const message = modelFailureMessage(value);
381
+ if (!message.trim()) return undefined;
382
+ const kind = fallbackKindFromMessage(message, errorName(value));
383
+ return kind === undefined ? undefined : makeSignal(kind, value, source);
384
+ }
385
+
386
+ function classifyAssistantRefusalSignal(
387
+ value: unknown,
388
+ source: ModelFallbackFailureSource | undefined,
389
+ ): ModelFallbackFailureSignal | undefined {
390
+ const codeRefusalKind = refusalKindFromCode(codeFrom(value))
391
+ ?? refusalKindFromCode(errorName(value))
392
+ ?? refusalKindFromCode(finishReasonFrom(value));
393
+ if (codeRefusalKind !== undefined) return makeSignal(codeRefusalKind, value, source);
394
+
395
+ const messageRefusalKind = refusalKindFromMessage(directMessageFrom(value) ?? "");
396
+ return messageRefusalKind === undefined ? undefined : makeSignal(messageRefusalKind, value, source);
397
+ }
398
+
399
+ function isRefusalSignal(signal: ModelFallbackFailureSignal): boolean {
400
+ return signal.kind === "cancelled" || signal.kind === "task_failure";
401
+ }
402
+
403
+ function structuredSignal(
404
+ value: unknown,
405
+ seen: Set<unknown>,
406
+ source?: ModelFallbackFailureSource,
407
+ ): ModelFallbackFailureSignal | undefined {
408
+ if (value === undefined || value === null || seen.has(value)) return undefined;
409
+ if (typeof value === "object") seen.add(value);
410
+
411
+ const stopReason = stopReasonFrom(value)?.toLowerCase();
412
+ if (stopReason === "aborted") return makeSignal("cancelled", value, source);
413
+
414
+ const directRefusalSignal = classifyAssistantRefusalSignal(value, source);
415
+ if (directRefusalSignal !== undefined) return directRefusalSignal;
416
+
417
+ const codeKind = kindFromCode(codeFrom(value));
418
+ const nameKind = kindFromCode(errorName(value));
419
+ if (codeKind === "cancelled" || nameKind === "cancelled") return makeSignal("cancelled", value, source);
420
+
421
+ let firstNestedFallbackSignal: ModelFallbackFailureSignal | undefined;
422
+ const nestedSeen = new Set(seen);
423
+ for (const diagnosticError of diagnosticErrors(value)) {
424
+ const diagnosticSignal = structuredSignal(diagnosticError, nestedSeen, "diagnostic")
425
+ ?? fallbackSignalFromMessage(diagnosticError, "diagnostic");
426
+ if (diagnosticSignal === undefined) continue;
427
+ if (isRefusalSignal(diagnosticSignal)) return diagnosticSignal;
428
+ firstNestedFallbackSignal ??= diagnosticSignal;
429
+ }
430
+
431
+ const cause = causeOf(value);
432
+ const causeSignal = structuredSignal(cause, nestedSeen, source)
433
+ ?? fallbackSignalFromMessage(cause, source);
434
+ if (causeSignal !== undefined) {
435
+ if (isRefusalSignal(causeSignal)) return causeSignal;
436
+ firstNestedFallbackSignal ??= causeSignal;
437
+ }
438
+
439
+ const statusKind = kindFromStatus(statusFrom(value));
440
+ if (statusKind !== undefined) return makeSignal(statusKind, value, source);
441
+ if (codeKind !== undefined) return makeSignal(codeKind, value, source);
442
+ if (nameKind !== undefined) return makeSignal(nameKind, value, source);
443
+
444
+ if (firstNestedFallbackSignal !== undefined) return firstNestedFallbackSignal;
445
+
446
+ if (stopReason === "error") return makeSignal("provider_unavailable", value, source);
447
+
448
+ return undefined;
449
+ }
450
+
451
+ function messageFromUnknown(value: unknown, seen: Set<unknown>): string | undefined {
452
+ if (value === undefined || value === null || seen.has(value)) return undefined;
453
+ if (typeof value === "string") return value.trim().length > 0 ? value : undefined;
454
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
455
+ if (typeof value === "symbol" || typeof value === "function") return undefined;
456
+ seen.add(value);
457
+
458
+ if (value instanceof Error && value.message.trim().length > 0) return value.message;
459
+ const directMessage = directMessageFrom(value);
460
+ if (directMessage !== undefined) return directMessage;
461
+
462
+ for (const diagnosticError of diagnosticErrors(value)) {
463
+ const diagnosticMessage = messageFromUnknown(diagnosticError, seen);
464
+ if (diagnosticMessage !== undefined) return diagnosticMessage;
465
+ }
466
+
467
+ const causeMessage = messageFromUnknown(causeOf(value), seen);
468
+ if (causeMessage !== undefined) return causeMessage;
469
+
470
+ const stopReason = stopReasonFrom(value);
471
+ if (stopReason !== undefined) return `Assistant message ended with stopReason:${stopReason}`;
472
+ const finishReason = finishReasonFrom(value);
473
+ if (finishReason !== undefined) return `Model request finished with finish_reason:${finishReason}`;
474
+ const status = statusFrom(value);
475
+ if (status !== undefined) return `Model request failed with status ${status}`;
476
+ const code = codeFrom(value);
477
+ if (code !== undefined) return `Model request failed with code ${String(code)}`;
478
+
479
+ return undefined;
480
+ }
481
+
482
+ export function modelFailureMessage(error: unknown): string {
483
+ const structuredMessage = messageFromUnknown(error, new Set());
484
+ if (structuredMessage !== undefined) return structuredMessage;
485
+ const rendered = String(error);
486
+ return rendered === "[object Object]" ? "Model request failed" : rendered;
487
+ }
488
+
489
+ export function normalizeModelFailureSignal(error: unknown): ModelFallbackFailureSignal {
490
+ const structured = structuredSignal(error, new Set());
491
+ if (structured !== undefined) return structured;
492
+
493
+ const message = modelFailureMessage(error);
494
+ const name = errorName(error);
495
+ const fallbackKind = message.trim().length > 0
496
+ ? fallbackKindFromMessage(message, name)
497
+ : undefined;
498
+ return {
499
+ kind: fallbackKind ?? "unknown",
500
+ message,
501
+ source: "string_fallback",
502
+ ...(name !== undefined ? { name } : {}),
503
+ };
504
+ }
505
+
506
+ export function isRetryableModelFailure(error: unknown): boolean {
507
+ if (error === undefined) return false;
508
+ const signal = normalizeModelFailureSignal(error);
509
+ return FALLBACKABLE_FAILURE_KINDS.has(signal.kind);
101
510
  }
102
511
 
103
512
  export function formatModelAttemptNote(attempt: ModelAttemptSummary, nextModel?: string): string {
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.8.26-alpha.6] - 2026-06-06
8
+
9
+ ### Changed
10
+
11
+ - Bumped package version for the Atomic 0.8.26-alpha.6 prerelease.
12
+
7
13
  ## [0.8.26-alpha.5] - 2026-06-06
8
14
 
9
15
  ### Changed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/web-access",
3
- "version": "0.8.26-alpha.5",
3
+ "version": "0.8.26-alpha.6",
4
4
  "private": true,
5
5
  "description": "Atomic extension for web search, URL fetching, GitHub repo cloning, PDF/video extraction. Fork of: https://github.com/nicobailon/pi-web-access",
6
6
  "contributors": [
@@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.8.26-alpha.6] - 2026-06-06
10
+
11
+ ### Changed
12
+
13
+ - Bumped package version for the Atomic 0.8.26-alpha.6 prerelease.
14
+
9
15
  ## [0.8.26-alpha.5] - 2026-06-06
10
16
 
11
17
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/workflows",
3
- "version": "0.8.26-alpha.5",
3
+ "version": "0.8.26-alpha.6",
4
4
  "private": true,
5
5
  "description": "Atomic extension for multi-stage workflow authoring and execution.",
6
6
  "contributors": [
@@ -1795,7 +1795,11 @@ export function makeExecuteWorkflowTool(
1795
1795
  const isPaused =
1796
1796
  run?.status === "paused" ||
1797
1797
  (run?.stages.some((s) => s.status === "paused") ?? false);
1798
- if (!isPaused && run?.status === "failed" && run.endedAt !== undefined && run.resumable !== false) {
1798
+ const isResumableContinuation = run !== undefined && !isPaused && (
1799
+ (run.status === "failed" && run.endedAt !== undefined && run.resumable !== false) ||
1800
+ (run.endedAt === undefined && run.resumable === true && run.failureRecoverability === "recoverable")
1801
+ );
1802
+ if (isResumableContinuation) {
1799
1803
  const continuation = activeRuntime.resumeFailedRun(stageRunId, stage.stageId, { policy });
1800
1804
  return {
1801
1805
  action: "resume",
@@ -3133,7 +3137,11 @@ function factory(pi: ExtensionAPI): void {
3133
3137
  const isPaused =
3134
3138
  run?.status === "paused" ||
3135
3139
  (run?.stages.some((s) => s.status === "paused") ?? false);
3136
- if (!isPaused && run?.status === "failed" && run.endedAt !== undefined && run.resumable !== false) {
3140
+ const isResumableContinuation = run !== undefined && !isPaused && (
3141
+ (run.status === "failed" && run.endedAt !== undefined && run.resumable !== false) ||
3142
+ (run.endedAt === undefined && run.resumable === true && run.failureRecoverability === "recoverable")
3143
+ );
3144
+ if (isResumableContinuation) {
3137
3145
  const continuation = runtimeForContext(ctx).resumeFailedRun(stageRunId, stageId, { policy });
3138
3146
  if (continuation.ok) {
3139
3147
  print(continuation.message);
@@ -44,6 +44,7 @@ import {
44
44
  import { validateWorkflowModels } from "../runs/shared/model-fallback.js";
45
45
  import { runDetached } from "../runs/background/runner.js";
46
46
  import type { JobTracker } from "../runs/background/job-tracker.js";
47
+ import { appendRunEnd } from "../shared/persistence-session-entries.js";
47
48
  import { classifyWorkflowFailure } from "../shared/workflow-failures.js";
48
49
 
49
50
  // ---------------------------------------------------------------------------
@@ -389,13 +390,39 @@ export function createExtensionRuntime(opts: ExtensionRuntimeOpts = {}): Extensi
389
390
  return { ok: true, stageId: failedStageId };
390
391
  }
391
392
 
393
+ function finalizeResumedActiveBlockedSourceRun(source: RunSnapshot, continuationRunId: string): void {
394
+ const errorMessage = source.error ?? source.failureMessage ?? `workflow resumed in new run ${continuationRunId}`;
395
+ const metadata = {
396
+ ...(source.failureKind !== undefined ? { failureKind: source.failureKind } : {}),
397
+ ...(source.failureCode !== undefined ? { failureCode: source.failureCode } : {}),
398
+ failureRecoverability: "non_recoverable",
399
+ failureDisposition: "terminal_killed",
400
+ ...(source.failureMessage !== undefined ? { failureMessage: source.failureMessage } : {}),
401
+ ...(source.failedStageId !== undefined ? { failedStageId: source.failedStageId } : {}),
402
+ resumable: false,
403
+ ...(source.retryAfterMs !== undefined ? { retryAfterMs: source.retryAfterMs } : {}),
404
+ } as const;
405
+ const recorded = activeStore.recordRunEnd(source.id, "killed", undefined, errorMessage, metadata);
406
+ if (recorded && persistence !== undefined) {
407
+ appendRunEnd(persistence, {
408
+ runId: source.id,
409
+ status: "killed",
410
+ error: errorMessage,
411
+ ...metadata,
412
+ ts: Date.now(),
413
+ });
414
+ }
415
+ }
416
+
392
417
  function resumeFailedRun(sourceRunId: string, stageId?: string, options?: RuntimeDispatchOptions): ResumeFailedRunResult {
393
418
  const source = activeStore.runs().find((run) => run.id === sourceRunId);
394
419
  if (source === undefined) {
395
420
  return { ok: false, reason: "run_not_found", message: `run not found: ${sourceRunId}` };
396
421
  }
397
- if (source.status !== "failed" || source.endedAt === undefined || source.resumable === false) {
398
- return { ok: false, reason: "not_resumable", message: `run ${sourceRunId} is not a failed resumable workflow run` };
422
+ const isTerminalFailedResumable = source.status === "failed" && source.endedAt !== undefined && source.resumable !== false;
423
+ const isActiveBlockedResumable = source.endedAt === undefined && source.resumable === true && source.failureRecoverability === "recoverable";
424
+ if (!isTerminalFailedResumable && !isActiveBlockedResumable) {
425
+ return { ok: false, reason: "not_resumable", message: `run ${sourceRunId} is not a resumable workflow run` };
399
426
  }
400
427
  const def = registry.get(source.name);
401
428
  if (def === undefined) {
@@ -415,12 +442,17 @@ export function createExtensionRuntime(opts: ExtensionRuntimeOpts = {}): Extensi
415
442
  ...runOptions({ workflow: def.name, inputs: sourceInputs }, options?.policy),
416
443
  continuation: { source, resumeFromStageId: resolvedStage.stageId },
417
444
  });
445
+ if (isActiveBlockedResumable) {
446
+ finalizeResumedActiveBlockedSourceRun(source, accepted.runId);
447
+ }
418
448
  return {
419
449
  ok: true,
420
450
  runId: accepted.runId,
421
451
  sourceRunId: source.id,
422
452
  resumeFromStageId: resolvedStage.stageId,
423
- message: `Resuming failed workflow "${def.name}" from run ${source.id.slice(0, 8)} at stage ${resolvedStage.stageId.slice(0, 8)} (new run ${accepted.runId}).`,
453
+ message: isActiveBlockedResumable
454
+ ? `Resuming blocked workflow "${def.name}" from run ${source.id.slice(0, 8)} at stage ${resolvedStage.stageId.slice(0, 8)} (new run ${accepted.runId}).`
455
+ : `Resuming failed workflow "${def.name}" from run ${source.id.slice(0, 8)} at stage ${resolvedStage.stageId.slice(0, 8)} (new run ${accepted.runId}).`,
424
456
  };
425
457
  }
426
458
 
@@ -82,6 +82,14 @@ export interface RunDetail {
82
82
  readonly stages: readonly RunSnapshot["stages"][number][];
83
83
  readonly result?: WorkflowOutputValues;
84
84
  readonly error?: string;
85
+ readonly failureKind?: RunSnapshot["failureKind"];
86
+ readonly failureCode?: RunSnapshot["failureCode"];
87
+ readonly failureRecoverability?: RunSnapshot["failureRecoverability"];
88
+ readonly failureDisposition?: RunSnapshot["failureDisposition"];
89
+ readonly failedStageId?: string;
90
+ readonly resumable?: boolean;
91
+ readonly retryAfterMs?: number;
92
+ readonly blockedAt?: number;
85
93
  }
86
94
 
87
95
  export type InspectRunResult =
@@ -146,11 +154,26 @@ export function killRun(
146
154
  const previousStatus = run.status;
147
155
 
148
156
  // Abort active executor (no-op if not registered)
149
- opts?.cancellation?.abort(runId, "workflow killed");
150
-
151
- const recorded = activeStore.recordRunEnd(runId, "killed", undefined, "workflow killed");
157
+ const errorMessage = "workflow killed";
158
+ opts?.cancellation?.abort(runId, errorMessage);
159
+
160
+ const metadata = {
161
+ failureKind: "cancelled",
162
+ failureCode: "cancelled",
163
+ failureRecoverability: "non_recoverable",
164
+ failureDisposition: "terminal_killed",
165
+ failureMessage: errorMessage,
166
+ resumable: false,
167
+ } as const;
168
+ const recorded = activeStore.recordRunEnd(runId, "killed", undefined, errorMessage, metadata);
152
169
  if (recorded && opts?.persistence) {
153
- appendRunEnd(opts.persistence, { runId, status: "killed", ts: Date.now() });
170
+ appendRunEnd(opts.persistence, {
171
+ runId,
172
+ status: "killed",
173
+ error: errorMessage,
174
+ ...metadata,
175
+ ts: Date.now(),
176
+ });
154
177
  }
155
178
 
156
179
  return { ok: true, runId, previousStatus };
@@ -247,14 +270,29 @@ export function resumeRun(
247
270
  // Return a deep copy of the snapshot for safe consumption
248
271
  const snapshot = structuredClone(run);
249
272
  const resumedCopy = structuredClone(resumed);
250
- if (run.status === "failed" && run.endedAt !== undefined && run.resumable === false) {
273
+ if (run.status === "killed" || run.resumable === false) {
251
274
  return {
252
275
  ok: true,
253
276
  runId,
254
277
  snapshot,
255
278
  resumed: resumedCopy,
256
279
  mode: "not_resumable",
257
- message: "This failed workflow is not resumable; inspect the snapshot and rerun the workflow when ready.",
280
+ message: "This workflow is not resumable; inspect the snapshot and start a new workflow run when ready.",
281
+ };
282
+ }
283
+ if (
284
+ run.endedAt === undefined &&
285
+ run.resumable === true &&
286
+ run.failureRecoverability === "recoverable" &&
287
+ run.failedStageId !== undefined
288
+ ) {
289
+ return {
290
+ ok: true,
291
+ runId,
292
+ snapshot,
293
+ resumed: resumedCopy,
294
+ mode: resumedCopy.length > 0 ? "paused" : "snapshot",
295
+ message: `Workflow is blocked on a recoverable ${run.failureCode ?? run.failureKind ?? "workflow"} failure at stage ${run.failedStageId}; retry/resume after the issue clears.`,
258
296
  };
259
297
  }
260
298
  return {
@@ -441,6 +479,14 @@ export function inspectRun(
441
479
  stages: expandedStages.map((stage) => structuredClone(stage)),
442
480
  result: copy.result,
443
481
  error: copy.error,
482
+ failureKind: copy.failureKind,
483
+ failureCode: copy.failureCode,
484
+ failureRecoverability: copy.failureRecoverability,
485
+ failureDisposition: copy.failureDisposition,
486
+ failedStageId: copy.failedStageId,
487
+ resumable: copy.resumable,
488
+ retryAfterMs: copy.retryAfterMs,
489
+ blockedAt: copy.blockedAt,
444
490
  };
445
491
 
446
492
  return { ok: true, runId: copy.id, detail };