pi-dag-workflow 0.0.0-stage → 0.7.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.
- package/CHANGELOG.md +454 -0
- package/LICENSE +21 -0
- package/README.md +211 -2
- package/README.zh-CN.md +212 -0
- package/docs/COMMUNICATION.md +129 -0
- package/docs/RELEASING.md +66 -0
- package/package.json +70 -4
- package/src/agents/commands/child.ts +148 -0
- package/src/agents/commands/register.ts +404 -0
- package/src/agents/domain/context.ts +183 -0
- package/src/agents/domain/diagnostics.ts +32 -0
- package/src/agents/domain/owner.ts +448 -0
- package/src/agents/domain/profiles.ts +216 -0
- package/src/agents/domain/runtime.ts +603 -0
- package/src/agents/domain/schema.ts +15 -0
- package/src/agents/index.ts +5 -0
- package/src/agents/io/contracts.ts +50 -0
- package/src/agents/io/host.ts +16 -0
- package/src/agents/io/notices.ts +27 -0
- package/src/agents/io/persistence.ts +210 -0
- package/src/agents/io/process.ts +100 -0
- package/src/agents/io/profile-file.ts +16 -0
- package/src/agents/io/skills.ts +46 -0
- package/src/goal/commands/register.ts +235 -0
- package/src/goal/domain/failure.ts +14 -0
- package/src/goal/domain/owner.ts +575 -0
- package/src/goal/domain/policy.ts +8 -0
- package/src/goal/domain/prompt.ts +57 -0
- package/src/goal/domain/state.ts +176 -0
- package/src/goal/index.ts +5 -0
- package/src/goal/io/host.ts +8 -0
- package/src/plan/commands/register.ts +101 -0
- package/src/plan/domain/owner.ts +51 -0
- package/src/plan/domain/policy.ts +27 -0
- package/src/plan/domain/state.ts +15 -0
- package/src/plan/index.ts +13 -0
- package/src/plan/io/records.ts +3 -0
- package/src/retry/commands/menu.ts +158 -0
- package/src/retry/commands/register.ts +26 -0
- package/src/retry/domain/owner.ts +418 -0
- package/src/retry/domain/state.ts +71 -0
- package/src/retry/index.ts +5 -0
- package/src/retry/io/host.ts +15 -0
- package/src/shared/agent-view.ts +18 -0
- package/src/shared/completion.ts +111 -0
- package/src/shared/config-file.ts +106 -0
- package/src/shared/config.ts +112 -0
- package/src/shared/dag/cache.ts +26 -0
- package/src/shared/dag/graph.ts +170 -0
- package/src/shared/dag/layout.ts +130 -0
- package/src/shared/goal-view.ts +2 -0
- package/src/shared/i18n.ts +62 -0
- package/src/shared/message-types.ts +12 -0
- package/src/shared/messages.en.ts +1290 -0
- package/src/shared/passive-message.ts +50 -0
- package/src/shared/pi-cli.ts +29 -0
- package/src/shared/profile-launch.ts +110 -0
- package/src/shared/state-types.ts +5 -0
- package/src/shared/task-schema.ts +39 -0
- package/src/shared/task-types.ts +25 -0
- package/src/shared/terminal.ts +19 -0
- package/src/shared/tool-info.ts +8 -0
- package/src/shared/ui/filter.ts +144 -0
- package/src/shared/ui/menu.ts +447 -0
- package/src/shared/ui/panel.ts +768 -0
- package/src/shared/ui/presentation.ts +624 -0
- package/src/shared/ui/render-cache.ts +63 -0
- package/src/shared/ui/render.ts +641 -0
- package/src/shared/ui/symbols.ts +136 -0
- package/src/team/commands/bootstrap.ts +52 -0
- package/src/team/commands/controls.ts +91 -0
- package/src/team/commands/history.ts +100 -0
- package/src/team/commands/menu.ts +455 -0
- package/src/team/commands/register.ts +1321 -0
- package/src/team/domain/inbox.ts +178 -0
- package/src/team/domain/launcher.ts +355 -0
- package/src/team/domain/lifecycle.ts +34 -0
- package/src/team/domain/teams.ts +161 -0
- package/src/team/index.ts +4 -0
- package/src/team/io/inbox.ts +255 -0
- package/src/team/io/launcher.ts +270 -0
- package/src/team/io/lock.ts +19 -0
- package/src/team/io/registry.ts +477 -0
- package/src/team/io/runtime.ts +353 -0
- package/src/team/io/session-activity.ts +185 -0
- package/src/team/io/session-delete.ts +98 -0
- package/src/team/io/session-history.ts +153 -0
- package/src/team/io/session-tail.ts +132 -0
- package/src/team/io/sid-lock.ts +115 -0
- package/src/team/io/sid-session.ts +98 -0
- package/src/todos/commands/checkpoints.ts +31 -0
- package/src/todos/commands/register.ts +395 -0
- package/src/todos/domain/checkpoint-state.ts +32 -0
- package/src/todos/domain/checkpoint-summary.ts +146 -0
- package/src/todos/domain/cleanup.ts +25 -0
- package/src/todos/domain/owner.ts +109 -0
- package/src/todos/domain/presets.ts +192 -0
- package/src/todos/domain/state.ts +287 -0
- package/src/todos/index.ts +15 -0
- package/src/todos/io/records.ts +8 -0
- package/src/ui/index.ts +4 -0
- package/src/workflow/commands/connect.ts +81 -0
- package/src/workflow/commands/view.ts +121 -0
- package/src/workflow/domain/features.ts +5 -0
- package/src/workflow/domain/view-owner.ts +18 -0
- package/src/workflow/domain/view-state.ts +12 -0
- package/src/workflow/index.ts +225 -0
- package/src/workflow/io/session.ts +48 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type { Todo } from "../../shared/task-types.ts";
|
|
2
|
+
|
|
3
|
+
/** Minimal job view the context builder needs: the bound task and the report it produced. */
|
|
4
|
+
interface JobRecordView { id?: string; todoId?: number; status: string; output?: string; taskReportStale?: boolean; outputStart?: number }
|
|
5
|
+
|
|
6
|
+
const REPORT_HEAD = 200;
|
|
7
|
+
const MAX_CONTEXT = 2000;
|
|
8
|
+
/** Kept aside for the fragment list and report heads when the current step has long instructions. */
|
|
9
|
+
const REST_RESERVE = 600;
|
|
10
|
+
/** Worst case newline separators: intro, whole fragment, reports, instructions, closing. */
|
|
11
|
+
const SEPARATORS = 4;
|
|
12
|
+
|
|
13
|
+
/** Explicit path references are data to verify, not completion evidence or permissions. */
|
|
14
|
+
export function contextArtifacts(task: Todo): string[] {
|
|
15
|
+
const values = task.metadata?.artifacts;
|
|
16
|
+
if (!Array.isArray(values)) return [];
|
|
17
|
+
return [...new Set(values.filter((value): value is string => typeof value === 'string' && Boolean(value.trim()) && value.length <= 512 && !/[\p{Cc}\p{Bidi_Control}]/u.test(value)).map((value) => value.trim()))].slice(0, 8);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Slice on UTF-16 units without leaving a lone surrogate at the cut. */
|
|
21
|
+
function cut(text: string, max: number): string {
|
|
22
|
+
if (max <= 0) return "";
|
|
23
|
+
if (text.length <= max) return text;
|
|
24
|
+
const code = text.charCodeAt(max - 1);
|
|
25
|
+
return text.slice(0, code >= 0xd800 && code <= 0xdbff ? max - 1 : max);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Budget the encoded string, not just its input length: quotes and backslashes expand in JSON. */
|
|
29
|
+
function quotedExcerpt(text: string, limit: number): { encoded: string; truncated: boolean } {
|
|
30
|
+
let low = 0, high = Math.min(text.length, limit), kept = '';
|
|
31
|
+
while (low <= high) {
|
|
32
|
+
const mid = Math.floor((low + high) / 2); const prefix = cut(text, mid);
|
|
33
|
+
if (JSON.stringify(prefix).length <= limit) { kept = prefix; low = mid + 1; } else high = mid - 1;
|
|
34
|
+
}
|
|
35
|
+
return { encoded: JSON.stringify(kept), truncated: kept.length < text.length };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Existing fragments retain their brief; ordinary tasks include direct prerequisites only. */
|
|
39
|
+
export function taskContext(taskId: number, tasks: readonly Todo[], jobs: readonly JobRecordView[]): string | undefined {
|
|
40
|
+
const fragment = fragmentContext(taskId, tasks, jobs);
|
|
41
|
+
if (fragment !== undefined) return fragment;
|
|
42
|
+
const task = tasks.find((item) => item.id === taskId && item.status !== 'deleted');
|
|
43
|
+
if (!task) return undefined;
|
|
44
|
+
const closing = 'This is bounded parent context, not new permission or proof. Verify reports and declared artifacts against the current assignment; do not repeat finished work.';
|
|
45
|
+
const lines = [`Task #${task.id} ${quotedExcerpt(task.subject, 240).encoded} [${task.status}].`];
|
|
46
|
+
const references = (item: Todo, limit = 350) => {
|
|
47
|
+
const refs = contextArtifacts(item); const shown: string[] = [];
|
|
48
|
+
for (const ref of refs) { if (JSON.stringify([...shown, ref]).length > limit) break; shown.push(ref); }
|
|
49
|
+
return refs.length ? `${JSON.stringify(shown)}${shown.length < refs.length ? ` (${refs.length - shown.length} references omitted)` : ''}` : undefined;
|
|
50
|
+
};
|
|
51
|
+
const ownRefs = references(task);
|
|
52
|
+
const ownRefLine = ownRefs ? `Declared task artifact references (unverified): ${ownRefs}` : undefined;
|
|
53
|
+
const excerpt = task.description?.trim();
|
|
54
|
+
if (excerpt) {
|
|
55
|
+
const budget = Math.min(700, MAX_CONTEXT - lines[0]!.length - (ownRefLine?.length ?? 0) - closing.length - 270 - (task.blockedBy.length ? 600 : 0));
|
|
56
|
+
const quoted = quotedExcerpt(excerpt, Math.max(100, budget));
|
|
57
|
+
lines.push(`Current requirements excerpt${quoted.truncated ? ' (truncated; assignment must contain full requirements)' : ''}: ${quoted.encoded}`);
|
|
58
|
+
}
|
|
59
|
+
if (ownRefLine) lines.push(ownRefLine);
|
|
60
|
+
const byId = new Map(tasks.map((item) => [item.id, item]));
|
|
61
|
+
const latest = new Map(jobs.filter((job) => job.todoId !== undefined).map((job) => [job.todoId!, job]));
|
|
62
|
+
if (task.blockedBy.length) lines.push('Direct prerequisites:');
|
|
63
|
+
let shown = 0;
|
|
64
|
+
for (const id of task.blockedBy) {
|
|
65
|
+
const remaining = MAX_CONTEXT - lines.join('\n').length - closing.length - 140;
|
|
66
|
+
const dependency = byId.get(id);
|
|
67
|
+
let head = `- #${id} [${dependency?.status ?? 'missing'}]${dependency ? ` ${quotedExcerpt(dependency.subject, 160).encoded}` : ''}`;
|
|
68
|
+
const job = latest.get(id); const output = job?.output ?? ''; const start = job?.outputStart ?? 0;
|
|
69
|
+
const splitPair = start > 0 && /[\uD800-\uDBFF]/.test(output[start - 1] ?? '') && /[\uDC00-\uDFFF]/.test(output[start] ?? '');
|
|
70
|
+
if (dependency?.status === 'completed' && job && !job.taskReportStale && ['completed', 'running', 'waiting'].includes(job.status) && Number.isSafeInteger(start) && start >= 0 && start <= output.length && !splitPair) {
|
|
71
|
+
// Only flatten a bounded window; long retained reports do not need another full copy.
|
|
72
|
+
const window = cut(output.slice(start, start + 1601), 1600);
|
|
73
|
+
const report = window.replace(/[\p{Cc}\p{Bidi_Control}]/gu, ' ').replace(/\s+/g, ' ').trim();
|
|
74
|
+
if (report) {
|
|
75
|
+
const quoted = quotedExcerpt(report, REPORT_HEAD);
|
|
76
|
+
head += `; latest report${job.id ? ` ${quotedExcerpt(job.id, 40).encoded}` : ''} excerpt: ${quoted.encoded}${quoted.truncated || start + window.length < output.length ? ' (truncated)' : ''}`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (dependency?.status === 'completed' && remaining - head.length > 90) {
|
|
80
|
+
const refs = references(dependency, Math.min(350, remaining - head.length - 80));
|
|
81
|
+
if (refs) head += `; declared artifacts (unverified): ${refs}`;
|
|
82
|
+
}
|
|
83
|
+
if (head.length > remaining) break;
|
|
84
|
+
lines.push(head); shown++;
|
|
85
|
+
}
|
|
86
|
+
if (shown < task.blockedBy.length) lines.push(`${task.blockedBy.length - shown} direct prerequisites omitted from this brief; parent must supply required inputs in the assignment.`);
|
|
87
|
+
lines.push(closing);
|
|
88
|
+
return lines.join('\n');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Optional brief for a child that took one step of a task fragment. It answers the two questions
|
|
93
|
+
* such a child otherwise has to rediscover: where it sits in the workflow, and what the earlier
|
|
94
|
+
* and prerequisite steps already found. Only the same fragment instance contributes, so nothing
|
|
95
|
+
* else about the session leaks; every report is truncated to its head.
|
|
96
|
+
*/
|
|
97
|
+
export function fragmentContext(taskId: number, tasks: readonly Todo[], jobs: readonly JobRecordView[]): string | undefined {
|
|
98
|
+
const task = tasks.find((item) => item.id === taskId);
|
|
99
|
+
const preset = task?.metadata?.preset;
|
|
100
|
+
const run = task?.metadata?.run;
|
|
101
|
+
if (!task || typeof preset !== "string" || typeof run !== "number") return undefined;
|
|
102
|
+
const instance = tasks.filter((item) => item.metadata?.preset === preset && item.metadata.run === run && item.status !== "deleted");
|
|
103
|
+
const byId = new Map(instance.map((item) => [item.id, item]));
|
|
104
|
+
const position = typeof task.metadata?.step === "number" ? task.metadata.step : undefined;
|
|
105
|
+
const key = (item: Todo): string => cut(String(item.metadata?.key ?? `#${item.id}`), 48);
|
|
106
|
+
const label = (item: Todo): string => `${key(item)} "${cut(item.subject, 120)}"`;
|
|
107
|
+
// completed/in_progress get short tags; every other state keeps its own name instead of reading as pending.
|
|
108
|
+
const state = (item: Todo): string => item.status === "completed" ? "done" : item.status === "in_progress" ? "running" : item.status;
|
|
109
|
+
const ordinal = (item: Todo): number => typeof item.metadata?.step === "number" ? item.metadata.step : item.id;
|
|
110
|
+
|
|
111
|
+
// Real prerequisites: the transitive blockedBy closure, but only inside this fragment instance,
|
|
112
|
+
// so a forward prerequisite (id above the current step) still counts and nothing crosses a run.
|
|
113
|
+
const closure = new Set<number>();
|
|
114
|
+
const pending = [...task.blockedBy];
|
|
115
|
+
while (pending.length) {
|
|
116
|
+
const id = pending.pop()!;
|
|
117
|
+
if (closure.has(id)) continue;
|
|
118
|
+
const dependency = byId.get(id);
|
|
119
|
+
if (!dependency) continue;
|
|
120
|
+
closure.add(id);
|
|
121
|
+
pending.push(...dependency.blockedBy);
|
|
122
|
+
}
|
|
123
|
+
const mine = ordinal(task);
|
|
124
|
+
const direct = new Set(task.blockedBy);
|
|
125
|
+
const feeding = instance
|
|
126
|
+
.filter((item) => item.id !== task.id && (ordinal(item) < mine || closure.has(item.id)))
|
|
127
|
+
.sort((a, b) => Number(direct.has(b.id)) - Number(direct.has(a.id)) || Number(closure.has(b.id)) - Number(closure.has(a.id)) || ordinal(a) - ordinal(b) || a.id - b.id);
|
|
128
|
+
|
|
129
|
+
// Only the current latest job may speak for a step. A stale or failed/cancelled latest is not
|
|
130
|
+
// evidence and never falls back to an older attempt's output.
|
|
131
|
+
const latest = new Map(jobs.filter((job) => job.todoId !== undefined).map((job) => [job.todoId!, job]));
|
|
132
|
+
const report = (item: Todo): string | undefined => {
|
|
133
|
+
const job = latest.get(item.id);
|
|
134
|
+
if (!job || job.taskReportStale || !['completed', 'running', 'waiting'].includes(job.status)) return undefined;
|
|
135
|
+
const start = job.outputStart ?? 0;
|
|
136
|
+
const output = job.output ?? '';
|
|
137
|
+
if (!Number.isSafeInteger(start) || start < 0 || start > output.length || /[\uD800-\uDBFF]/.test(output[start - 1] ?? '') && /[\uDC00-\uDFFF]/.test(output[start] ?? '')) return undefined;
|
|
138
|
+
// Preserve whitespace-folding and exact ellipsis semantics without allocating a full copy.
|
|
139
|
+
let flat: string;
|
|
140
|
+
if (output.length - start <= 1600) flat = output.slice(start).replace(/\s+/g, ' ').trim();
|
|
141
|
+
else {
|
|
142
|
+
const nextCharacter = /\S/gu; nextCharacter.lastIndex = start;
|
|
143
|
+
flat = ''; let previous = start;
|
|
144
|
+
while (flat.length <= REPORT_HEAD) {
|
|
145
|
+
const match = nextCharacter.exec(output);
|
|
146
|
+
if (!match) break;
|
|
147
|
+
if (flat && match.index > previous) flat += ' ';
|
|
148
|
+
flat += match[0]; previous = nextCharacter.lastIndex;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (!flat) return undefined;
|
|
152
|
+
return `- ${key(item)} (${state(item)}): ${cut(flat, REPORT_HEAD)}${flat.length > REPORT_HEAD ? "…" : ""}`;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const intro = cut(`Task fragment ${cut(preset, 48)} run ${run}${position === undefined ? "" : `, step ${position} of ${instance.length}`}: you took ${label(task)} (${state(task)}).`, 300);
|
|
156
|
+
const whole = `Whole fragment: ${instance.map((item) => `${label(item)} [${state(item)}]`).join("; ")}.`;
|
|
157
|
+
const instructions = task.description?.trim() ? `This step's instructions: ${task.description}` : undefined;
|
|
158
|
+
const closing = "Steps that still depend on others stay for the parent to dispatch; finish what you can and report what is blocked.";
|
|
159
|
+
|
|
160
|
+
// The current step's instructions outrank the fragment list and report heads: the other sections
|
|
161
|
+
// are truncated against whatever budget is left after them.
|
|
162
|
+
const budget = Math.max(0, MAX_CONTEXT - intro.length - closing.length - SEPARATORS);
|
|
163
|
+
const instructionLine = instructions ? cut(instructions, Math.max(0, budget - REST_RESERVE)) : undefined;
|
|
164
|
+
const left = budget - (instructionLine?.length ?? 0);
|
|
165
|
+
const header = "Earlier and prerequisite steps of this fragment, report heads:";
|
|
166
|
+
const shown: string[] = [];
|
|
167
|
+
let remaining = Math.min(800, Math.max(0, left - Math.min(150, whole.length))) - header.length;
|
|
168
|
+
for (const item of feeding) {
|
|
169
|
+
const line = report(item);
|
|
170
|
+
if (line === undefined) continue;
|
|
171
|
+
if (remaining < line.length + 1) break;
|
|
172
|
+
shown.push(line); remaining -= line.length + 1;
|
|
173
|
+
}
|
|
174
|
+
const reports = shown.length ? `${header}\n${shown.join("\n")}` : '';
|
|
175
|
+
const wholeBudget = left - reports.length;
|
|
176
|
+
const wholeLine = whole.length <= wholeBudget ? whole : `${cut(whole, Math.max(0, wholeBudget - 1))}…`;
|
|
177
|
+
const lines = [intro];
|
|
178
|
+
if (wholeLine) lines.push(wholeLine);
|
|
179
|
+
if (reports) lines.push(reports);
|
|
180
|
+
if (instructionLine) lines.push(instructionLine);
|
|
181
|
+
lines.push(closing);
|
|
182
|
+
return cut(lines.join("\n"), MAX_CONTEXT);
|
|
183
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { AgentFailure } from '../io/contracts.ts';
|
|
2
|
+
export type { AgentFailure } from '../io/contracts.ts';
|
|
3
|
+
/** Diagnostics may contain headers/URLs. Never expose their common credential forms. */
|
|
4
|
+
export function safeDiagnostic(value: string, limit = 2048): string {
|
|
5
|
+
const text = value
|
|
6
|
+
.replace(/((?:["']?(?:proxy-authorization|authorization|cookie|set-cookie)["']?)\s*[:=]\s*)(?:"[^"\r\n]*"|'[^'\r\n]*'|[^\r\n]+)/gi, '$1[redacted]')
|
|
7
|
+
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '')
|
|
8
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '')
|
|
9
|
+
.replace(/[\p{Cc}\p{Bidi_Control}]/gu, ' ')
|
|
10
|
+
.replace(/https?:\/\/[^\s/@]+:[^\s/@]+@/gi, (url) => url.replace(/\/\/.*@/, '//[redacted]@'))
|
|
11
|
+
.replace(/\b(?:sk-|ghp_|github_pat_|npm_)[A-Za-z0-9_-]{8,}/g, '[redacted]')
|
|
12
|
+
.replace(/\bBearer\s+[A-Za-z0-9._~+\/-]+/gi, 'Bearer [redacted]')
|
|
13
|
+
.replace(/([?&](?:api[_-]?key|access[_-]?token|token|secret|password)=)[^&#\s]+/gi, '$1[redacted]')
|
|
14
|
+
.replace(/(["']?(?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|authorization|secret|password)["']?\s*[:=]\s*)(?:"[^"\r\n]*"|'[^'\r\n]*'|[^\s,;}]+)/gi, '$1[redacted]')
|
|
15
|
+
.replace(/\s+/g, ' ').trim();
|
|
16
|
+
if (text.length <= limit) return text;
|
|
17
|
+
const end = limit - 1;
|
|
18
|
+
return text.slice(0, /[\uD800-\uDBFF]/.test(text.charAt(end - 1)) ? end - 1 : end) + '…';
|
|
19
|
+
}
|
|
20
|
+
/** Classification informs the parent; it never schedules retries or authorizes replay. */
|
|
21
|
+
export function agentFailure(value: string): AgentFailure {
|
|
22
|
+
const reason = safeDiagnostic(value, 300);
|
|
23
|
+
let kind: AgentFailure['kind'] = 'unknown';
|
|
24
|
+
let retryable: boolean | null = null;
|
|
25
|
+
if (/insufficient[_\s-]quota|usage[_\s-]limit[_\s-]reached|usage limit (?:has been )?reached|billing[_\s-](?:hard[_\s-])?limit|billing.{0,60}(?:exhaust|exceed)|payment[_\s-]required|credits?.{0,30}(?:exhaust|insufficient)/i.test(value)) { kind = 'quota'; retryable = false; }
|
|
26
|
+
else if (/invalid[_\s-]api[_\s-]key|authentication|permission[_\s-]denied|(?:HTTP|status|error)\s*[:=]?\s*(?:401|403)\b/i.test(value)) { kind = 'auth'; retryable = false; }
|
|
27
|
+
else if (/deadline|timed?\s*out|ETIMEDOUT/i.test(value)) { kind = 'timeout'; retryable = true; }
|
|
28
|
+
else if (/ECONNRESET|ECONNREFUSED|EAI_AGAIN|network|fetch failed|stream.{0,30}(?:disconnect|closed|interrupt)|connection.{0,30}(?:reset|closed|lost)|\b429\b|rate[_\s-]limit|\b5\d\d\b/i.test(value)) { kind = 'network'; retryable = true; }
|
|
29
|
+
else if (/process|exited|RPC|startup|ENOENT|MODULE_NOT_FOUND/i.test(value)) kind = 'process';
|
|
30
|
+
const recovery = `${kind === 'quota' || kind === 'auth' ? 'Resolve the account/configuration issue or select another configured profile. ' : ''}Inspect partial results and external side effects before an explicit resumeFrom attempt on the same Todo; do not blindly replay or create a replacement Todo.`;
|
|
31
|
+
return { kind, reason, retryable, recovery };
|
|
32
|
+
}
|