@tea-agent/loop-agent 0.25.3 → 0.25.5
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/AGENTS.md +6 -0
- package/CHANGELOG.md +55 -0
- package/dist/application/dag/args.js +21 -1
- package/dist/application/dag/run-dag.js +1 -0
- package/dist/cli/command-definitions.js +1 -1
- package/dist/cli/program.js +82 -63
- package/dist/commands/client-recovery.js +209 -62
- package/dist/commands/init.js +206 -82
- package/dist/commands/run-dag-progress.js +109 -0
- package/dist/commands/run-dag.js +16 -5
- package/dist/executors/dag-pi-executor.js +80 -15
- package/dist/executors/model-routing.js +1 -1
- package/dist/executors/shell-executor.js +159 -0
- package/dist/executors/shell-write-guard.js +21 -7
- package/dist/worker/console/repo-fingerprint.js +7 -1
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +964 -0
- package/dist/workflows/dag/backend-test-case-manifest.js +39 -1
- package/dist/workflows/dag/backend-test-markdown-workflow.js +306 -30
- package/dist/workflows/dag/backend-test-result-contract.js +35 -9
- package/dist/workflows/dag/convergence/controller.js +134 -9
- package/dist/workflows/dag/frontend-test-case-checklist.js +71 -0
- package/dist/workflows/dag/frontend-test-html-report.js +77 -0
- package/dist/workflows/dag/frontend-test-l5-report.js +138 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +44 -1
- package/dist/workflows/dag/init-hybrid.js +267 -80
- package/dist/workflows/dag/node-execution.js +64 -11
- package/dist/workflows/dag/prompt.js +118 -4
- package/dist/workflows/dag/retry-policy.js +5 -4
- package/dist/workflows/dag/scheduler.js +32 -5
- package/dist/workflows/dag/types.js +10 -3
- package/dist/workflows/dag/validate.js +6 -3
- package/docs/architecture/dag-execution.md +7 -4
- package/docs/architecture/runtime-boundaries.md +1 -1
- package/docs/templates/agent-dag.base.json +1 -1
- package/docs/templates/agent-dag.final-verification.json +1 -1
- package/docs/templates/agent-dag.schema.json +6 -0
- package/docs/templates/agent-dag.supervised-implementation.json +1 -1
- package/docs/templates/backend-test-dag.json +40 -13
- package/docs/templates/backend-test-dag.review-cases.prompt.md +1 -1
- package/docs/templates/frontend-test-dag.json +62 -5
- package/docs/templates/hybrid-dag.json +1 -1
- package/examples/decision-gate-agent-dag.json +1 -1
- package/examples/example-dag.json +1 -1
- package/examples/hybrid-loop-agent-dag.json +1 -1
- package/harness.json +3 -2
- package/package.json +1 -1
- package/skills/loop-agent/references/command-reference.md +2 -1
- package/skills/loop-agent/references/hybrid-dag.md +2 -2
- package/skills/loop-agent/references/model-routing.md +1 -1
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, readFile, readdir, rename, rm, writeFile, } from "node:fs/promises";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
export const CLIENT_RECOVERY_MODES = [
|
|
4
|
+
export const CLIENT_RECOVERY_MODES = [
|
|
5
|
+
"auto",
|
|
6
|
+
"project",
|
|
7
|
+
"user",
|
|
8
|
+
"off",
|
|
9
|
+
];
|
|
5
10
|
export const OPENCODE_TRANSIENT_RETRY_PLUGIN_PATH = ".opencode/plugins/loop-agent-transient-retry.js";
|
|
6
11
|
export const PERMANENT_ERROR_INTERACTION = "plugin-ignore-permanent-error";
|
|
7
12
|
export const BACKOFF_MS = [2000, 4000, 8000, 16000, 30000];
|
|
@@ -16,14 +21,26 @@ export const PI_RECOMMENDED_RETRY = {
|
|
|
16
21
|
},
|
|
17
22
|
};
|
|
18
23
|
const PERMANENT_MESSAGE_PATTERNS = [
|
|
19
|
-
{
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
{
|
|
25
|
+
reason: "auth",
|
|
26
|
+
pattern: /\b(401|unauthorized|invalid api key|authentication)\b/i,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
reason: "permission",
|
|
30
|
+
pattern: /\b(403|forbidden|permission denied|not allowed)\b/i,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
reason: "quota",
|
|
34
|
+
pattern: /\b(quota|rate.?limit|billing|insufficient.?credit)\b/i,
|
|
35
|
+
},
|
|
22
36
|
{
|
|
23
37
|
reason: "context-overflow",
|
|
24
38
|
pattern: /\b(context (length )?overflow|too many tokens|maximum context|context window)\b/i,
|
|
25
39
|
},
|
|
26
|
-
{
|
|
40
|
+
{
|
|
41
|
+
reason: "cancelled",
|
|
42
|
+
pattern: /\b(cancelled|canceled|aborted by user|user cancel)\b/i,
|
|
43
|
+
},
|
|
27
44
|
{
|
|
28
45
|
reason: "business-validation",
|
|
29
46
|
pattern: /\b(business validation|invalid task|schema validation)\b/i,
|
|
@@ -33,7 +50,6 @@ const TRANSIENT_MESSAGE_PATTERNS = [
|
|
|
33
50
|
/\bcode[:\s]*502\b/i,
|
|
34
51
|
/\b502\b/,
|
|
35
52
|
/\bLLMRequestError\b/i,
|
|
36
|
-
/\bTypeValidationError\b/i,
|
|
37
53
|
/\bnetwork fluctuation\b/i,
|
|
38
54
|
/\btimeout\b/i,
|
|
39
55
|
/\btransient provider failure\b/i,
|
|
@@ -90,13 +106,34 @@ function hasUnknownErrorShape(error) {
|
|
|
90
106
|
}
|
|
91
107
|
return false;
|
|
92
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* TypeValidation bypass shape: bare TypeValidationError (zod invalid_union
|
|
111
|
+
* surfaced directly by the provider/SDK instead of wrapped as UnknownError).
|
|
112
|
+
* Bypass only fires when combined with a transient signal (see classifyTransientUnknownError).
|
|
113
|
+
*/
|
|
114
|
+
function hasTypeValidationShape(error) {
|
|
115
|
+
const text = collectErrorText(error);
|
|
116
|
+
if (/\bTypeValidationError\b/i.test(text))
|
|
117
|
+
return true;
|
|
118
|
+
if (/Type validation failed/i.test(text))
|
|
119
|
+
return true;
|
|
120
|
+
if (error && typeof error === "object") {
|
|
121
|
+
const record = error;
|
|
122
|
+
if (typeof record.name === "string" &&
|
|
123
|
+
/TypeValidationError/i.test(record.name)) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
93
129
|
function isStandardApiError(error) {
|
|
94
130
|
if (!error || typeof error !== "object")
|
|
95
131
|
return false;
|
|
96
132
|
const record = error;
|
|
97
133
|
if (typeof record.name === "string" && /^APIError$/i.test(record.name))
|
|
98
134
|
return true;
|
|
99
|
-
if (typeof record.name === "string" &&
|
|
135
|
+
if (typeof record.name === "string" &&
|
|
136
|
+
/^(AuthError|PermissionError)$/i.test(record.name)) {
|
|
100
137
|
return true;
|
|
101
138
|
}
|
|
102
139
|
return false;
|
|
@@ -106,7 +143,8 @@ function permanentReason(error) {
|
|
|
106
143
|
const record = error;
|
|
107
144
|
if (typeof record.name === "string" && /AuthError/i.test(record.name))
|
|
108
145
|
return "auth";
|
|
109
|
-
if (typeof record.name === "string" &&
|
|
146
|
+
if (typeof record.name === "string" &&
|
|
147
|
+
/PermissionError/i.test(record.name)) {
|
|
110
148
|
return "permission";
|
|
111
149
|
}
|
|
112
150
|
return "api-error";
|
|
@@ -137,6 +175,12 @@ function looksTransient(error) {
|
|
|
137
175
|
/**
|
|
138
176
|
* Decide whether an OpenCode error should be resumed by the project plugin.
|
|
139
177
|
* Only compensates transient UnknownError paths that built-in APIError retry misses.
|
|
178
|
+
*
|
|
179
|
+
* TypeValidation bypass: a bare TypeValidationError (provider/SDK surfaces
|
|
180
|
+
* zod invalid_union directly, without the UnknownError wrapper) is treated as
|
|
181
|
+
* a retryable transient-unknown-error ONLY when it also carries a transient
|
|
182
|
+
* signal (502 / LLMRequestError / timeout / etc.). A pure schema mismatch
|
|
183
|
+
* without a transient signal is still rejected (resumable: false).
|
|
140
184
|
*/
|
|
141
185
|
export function classifyTransientUnknownError(error) {
|
|
142
186
|
const permanent = permanentReason(error);
|
|
@@ -147,7 +191,9 @@ export function classifyTransientUnknownError(error) {
|
|
|
147
191
|
interaction: PERMANENT_ERROR_INTERACTION,
|
|
148
192
|
};
|
|
149
193
|
}
|
|
150
|
-
|
|
194
|
+
const isUnknownShape = hasUnknownErrorShape(error);
|
|
195
|
+
const isTypeValidationShape = hasTypeValidationShape(error);
|
|
196
|
+
if (!isUnknownShape && !isTypeValidationShape) {
|
|
151
197
|
return {
|
|
152
198
|
resumable: false,
|
|
153
199
|
reason: "not-unknown-error",
|
|
@@ -164,7 +210,9 @@ export function classifyTransientUnknownError(error) {
|
|
|
164
210
|
return { resumable: true, reason: "transient-unknown-error" };
|
|
165
211
|
}
|
|
166
212
|
export function nextBackoffMs(attempt) {
|
|
167
|
-
if (!Number.isInteger(attempt) ||
|
|
213
|
+
if (!Number.isInteger(attempt) ||
|
|
214
|
+
attempt < 0 ||
|
|
215
|
+
attempt >= BACKOFF_MS.length) {
|
|
168
216
|
return undefined;
|
|
169
217
|
}
|
|
170
218
|
return BACKOFF_MS[attempt];
|
|
@@ -173,10 +221,15 @@ export function canResumeSession(input) {
|
|
|
173
221
|
if (input.sessionStatus === "retry") {
|
|
174
222
|
return { allowed: false, reason: "builtin-retry-active" };
|
|
175
223
|
}
|
|
224
|
+
if (!input.sessionStatus) {
|
|
225
|
+
return { allowed: false, reason: "session-status-unknown" };
|
|
226
|
+
}
|
|
176
227
|
if (input.locked) {
|
|
177
228
|
return { allowed: false, reason: "session-locked" };
|
|
178
229
|
}
|
|
179
|
-
if (!input.idle ||
|
|
230
|
+
if (!input.idle ||
|
|
231
|
+
input.sessionStatus === "busy" ||
|
|
232
|
+
input.sessionStatus === "running") {
|
|
180
233
|
return { allowed: false, reason: "session-busy" };
|
|
181
234
|
}
|
|
182
235
|
if (input.attempt >= MAX_SESSION_RETRIES) {
|
|
@@ -253,7 +306,12 @@ export function planPiRetryMerge(existing) {
|
|
|
253
306
|
action: "write",
|
|
254
307
|
reason: "missing-file",
|
|
255
308
|
path: settingsPath,
|
|
256
|
-
next: {
|
|
309
|
+
next: {
|
|
310
|
+
retry: {
|
|
311
|
+
...PI_RECOMMENDED_RETRY,
|
|
312
|
+
provider: { ...PI_RECOMMENDED_RETRY.provider },
|
|
313
|
+
},
|
|
314
|
+
},
|
|
257
315
|
};
|
|
258
316
|
}
|
|
259
317
|
if (!isRecord(existing)) {
|
|
@@ -276,7 +334,8 @@ export function planPiRetryMerge(existing) {
|
|
|
276
334
|
}
|
|
277
335
|
const retry = isRecord(existing.retry) ? { ...existing.retry } : {};
|
|
278
336
|
const provider = isRecord(retry.provider) ? { ...retry.provider } : {};
|
|
279
|
-
let changed = !isRecord(existing.retry) ||
|
|
337
|
+
let changed = !isRecord(existing.retry) ||
|
|
338
|
+
!isRecord(existing.retry.provider);
|
|
280
339
|
if (retry.enabled === undefined) {
|
|
281
340
|
retry.enabled = PI_RECOMMENDED_RETRY.enabled;
|
|
282
341
|
changed = true;
|
|
@@ -383,7 +442,8 @@ export async function applyPiRetryMerge(input) {
|
|
|
383
442
|
const dir = path.dirname(settingsPath);
|
|
384
443
|
const entries = await readdir(dir);
|
|
385
444
|
await Promise.all(entries
|
|
386
|
-
.filter((name) => name.startsWith(`.${path.basename(settingsPath)}.`) &&
|
|
445
|
+
.filter((name) => name.startsWith(`.${path.basename(settingsPath)}.`) &&
|
|
446
|
+
name.endsWith(".tmp"))
|
|
387
447
|
.map((name) => rm(path.join(dir, name), { force: true })));
|
|
388
448
|
}
|
|
389
449
|
catch {
|
|
@@ -414,11 +474,13 @@ const MAX_RETRIES = ${MAX_SESSION_RETRIES};
|
|
|
414
474
|
|
|
415
475
|
const sessionState = new Map();
|
|
416
476
|
const sessionStatus = new Map();
|
|
477
|
+
const pendingErrors = new Map();
|
|
478
|
+
const recoveryWorkers = new Map();
|
|
417
479
|
|
|
418
480
|
function getState(sessionId) {
|
|
419
481
|
let state = sessionState.get(sessionId);
|
|
420
482
|
if (!state) {
|
|
421
|
-
state = { attempt: 0, locked: false };
|
|
483
|
+
state = { attempt: 0, locked: false, rerunRequested: false };
|
|
422
484
|
sessionState.set(sessionId, state);
|
|
423
485
|
}
|
|
424
486
|
return state;
|
|
@@ -449,9 +511,13 @@ function isTransientUnknown(error) {
|
|
|
449
511
|
if (isPermanent(error)) return false;
|
|
450
512
|
const text = textOf(error);
|
|
451
513
|
const hasUnknown = /UnknownError/i.test(text) || (error && /UnknownError/i.test(String(error.name || "")));
|
|
452
|
-
|
|
514
|
+
const hasTypeValidation =
|
|
515
|
+
/\\bTypeValidationError\\b/i.test(text) ||
|
|
516
|
+
/Type validation failed/i.test(text) ||
|
|
517
|
+
(error && /TypeValidationError/i.test(String(error.name || "")));
|
|
518
|
+
if (!hasUnknown && !hasTypeValidation) return false;
|
|
453
519
|
return (
|
|
454
|
-
/\\b(code[:\\s]*502|502|LLMRequestError|
|
|
520
|
+
/\\b(code[:\\s]*502|502|LLMRequestError|network fluctuation|timeout|transient provider failure|non-standard)\\b/i.test(
|
|
455
521
|
text,
|
|
456
522
|
) ||
|
|
457
523
|
(error && (error.code === 502 || error.status === 502))
|
|
@@ -474,68 +540,121 @@ async function wait(ms) {
|
|
|
474
540
|
}
|
|
475
541
|
|
|
476
542
|
function statusType(status) {
|
|
477
|
-
|
|
543
|
+
const type = status && typeof status.type === "string" ? status.type : undefined;
|
|
544
|
+
return type === "idle" || type === "retry" || type === "busy" ? type : undefined;
|
|
478
545
|
}
|
|
479
546
|
|
|
480
547
|
async function readSessionStatus(client, sessionId) {
|
|
481
548
|
const cached = sessionStatus.get(sessionId);
|
|
482
|
-
if (cached === "retry"
|
|
549
|
+
if (cached === "retry" || cached === "busy") {
|
|
550
|
+
return { ok: true, type: cached };
|
|
551
|
+
}
|
|
483
552
|
try {
|
|
484
|
-
const response = await client.session.status();
|
|
553
|
+
const response = await client.session.status({ throwOnError: true });
|
|
554
|
+
if (!response || typeof response !== "object" || response.error != null) {
|
|
555
|
+
return { ok: false };
|
|
556
|
+
}
|
|
485
557
|
const current = statusType(response?.data?.[sessionId]);
|
|
486
|
-
if (current)
|
|
487
|
-
|
|
558
|
+
if (!current) return { ok: false };
|
|
559
|
+
sessionStatus.set(sessionId, current);
|
|
560
|
+
return { ok: true, type: current };
|
|
488
561
|
} catch {
|
|
489
|
-
return
|
|
562
|
+
return { ok: false };
|
|
490
563
|
}
|
|
491
564
|
}
|
|
492
565
|
|
|
566
|
+
function promptWasAccepted(response) {
|
|
567
|
+
return Boolean(
|
|
568
|
+
response &&
|
|
569
|
+
typeof response === "object" &&
|
|
570
|
+
response.error == null &&
|
|
571
|
+
Object.prototype.hasOwnProperty.call(response, "data") &&
|
|
572
|
+
response.data !== null &&
|
|
573
|
+
typeof response.data === "object",
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
|
|
493
577
|
export default async function loopAgentTransientRetryPlugin({ client, $ }) {
|
|
494
578
|
void $;
|
|
495
579
|
void PLUGIN_PATH;
|
|
496
580
|
|
|
497
|
-
async function
|
|
498
|
-
if (!sessionId) return { interaction: PERMANENT_INTERACTION, reason: "missing-session" };
|
|
499
|
-
if (!isTransientUnknown(error)) {
|
|
500
|
-
return { interaction: PERMANENT_INTERACTION, reason: "plugin-ignore-permanent-error" };
|
|
501
|
-
}
|
|
502
|
-
|
|
581
|
+
async function drainPendingRecovery(sessionId) {
|
|
503
582
|
const state = getState(sessionId);
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
}
|
|
583
|
+
const pending = pendingErrors.get(sessionId);
|
|
584
|
+
if (!pending) return;
|
|
507
585
|
if (state.attempt >= MAX_RETRIES) {
|
|
508
|
-
|
|
586
|
+
pendingErrors.delete(sessionId);
|
|
587
|
+
return;
|
|
509
588
|
}
|
|
510
589
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
return { interaction: PERMANENT_INTERACTION, reason: "builtin-retry-active" };
|
|
517
|
-
}
|
|
518
|
-
if (status === "busy" || status === "running") {
|
|
519
|
-
return { interaction: PERMANENT_INTERACTION, reason: "session-busy" };
|
|
520
|
-
}
|
|
590
|
+
// A status event may be stale by the time this worker runs. Confirm idle via
|
|
591
|
+
// the SDK before and after backoff; every other outcome keeps the error pending.
|
|
592
|
+
const beforeDelay = await readSessionStatus(client, sessionId);
|
|
593
|
+
if (pendingErrors.get(sessionId) !== pending) return;
|
|
594
|
+
if (!beforeDelay.ok || beforeDelay.type !== "idle") return;
|
|
521
595
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
596
|
+
const delay = BACKOFF_MS[state.attempt];
|
|
597
|
+
if (typeof delay !== "number") {
|
|
598
|
+
pendingErrors.delete(sessionId);
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
await wait(delay);
|
|
602
|
+
if (pendingErrors.get(sessionId) !== pending) return;
|
|
603
|
+
|
|
604
|
+
const beforePrompt = await readSessionStatus(client, sessionId);
|
|
605
|
+
if (pendingErrors.get(sessionId) !== pending) return;
|
|
606
|
+
if (!beforePrompt.ok || beforePrompt.type !== "idle") return;
|
|
607
|
+
|
|
608
|
+
const nextAttempt = state.attempt + 1;
|
|
609
|
+
const prompt = buildResumePrompt(sessionId, pending.error, nextAttempt);
|
|
610
|
+
let response;
|
|
611
|
+
try {
|
|
612
|
+
response = await client.session.promptAsync({
|
|
533
613
|
path: { id: sessionId },
|
|
534
614
|
body: { parts: [{ type: "text", text: prompt }] },
|
|
615
|
+
throwOnError: true,
|
|
535
616
|
});
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
617
|
+
} catch {
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
if (!promptWasAccepted(response)) return;
|
|
621
|
+
|
|
622
|
+
state.attempt = nextAttempt;
|
|
623
|
+
if (pendingErrors.get(sessionId) === pending) {
|
|
624
|
+
pendingErrors.delete(sessionId);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function startRecoveryWorker(sessionId) {
|
|
629
|
+
const state = getState(sessionId);
|
|
630
|
+
if (recoveryWorkers.has(sessionId)) {
|
|
631
|
+
state.rerunRequested = true;
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
state.locked = true;
|
|
635
|
+
state.rerunRequested = false;
|
|
636
|
+
const worker = drainPendingRecovery(sessionId)
|
|
637
|
+
.catch(() => {})
|
|
638
|
+
.finally(() => {
|
|
639
|
+
recoveryWorkers.delete(sessionId);
|
|
640
|
+
const rerun = state.rerunRequested;
|
|
641
|
+
state.rerunRequested = false;
|
|
642
|
+
state.locked = false;
|
|
643
|
+
if (rerun && pendingErrors.has(sessionId)) {
|
|
644
|
+
startRecoveryWorker(sessionId);
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
recoveryWorkers.set(sessionId, worker);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function clearSession(sessionId) {
|
|
651
|
+
pendingErrors.delete(sessionId);
|
|
652
|
+
sessionStatus.delete(sessionId);
|
|
653
|
+
const state = sessionState.get(sessionId);
|
|
654
|
+
if (state) {
|
|
655
|
+
state.attempt = 0;
|
|
656
|
+
state.rerunRequested = false;
|
|
657
|
+
if (!recoveryWorkers.has(sessionId)) state.locked = false;
|
|
539
658
|
}
|
|
540
659
|
}
|
|
541
660
|
|
|
@@ -544,11 +663,27 @@ export default async function loopAgentTransientRetryPlugin({ client, $ }) {
|
|
|
544
663
|
if (!event || typeof event !== "object") return;
|
|
545
664
|
const properties = event.properties || {};
|
|
546
665
|
const sessionId =
|
|
547
|
-
properties.sessionID || properties.sessionId || properties.id;
|
|
666
|
+
properties.sessionID || properties.sessionId || properties.id || properties.info?.id;
|
|
548
667
|
|
|
549
668
|
if (event.type === "session.status") {
|
|
550
669
|
const current = statusType(properties.status);
|
|
551
670
|
if (sessionId && current) sessionStatus.set(sessionId, current);
|
|
671
|
+
if (sessionId && current === "idle" && pendingErrors.has(sessionId)) {
|
|
672
|
+
startRecoveryWorker(sessionId);
|
|
673
|
+
}
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (event.type === "session.idle") {
|
|
678
|
+
if (sessionId) {
|
|
679
|
+
sessionStatus.set(sessionId, "idle");
|
|
680
|
+
if (pendingErrors.has(sessionId)) startRecoveryWorker(sessionId);
|
|
681
|
+
}
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (event.type === "session.deleted") {
|
|
686
|
+
if (sessionId) clearSession(sessionId);
|
|
552
687
|
return;
|
|
553
688
|
}
|
|
554
689
|
|
|
@@ -557,16 +692,26 @@ export default async function loopAgentTransientRetryPlugin({ client, $ }) {
|
|
|
557
692
|
if (
|
|
558
693
|
info?.role === "assistant" &&
|
|
559
694
|
info.sessionID &&
|
|
695
|
+
(info.status === undefined || info.status === "completed") &&
|
|
560
696
|
info.time?.completed != null &&
|
|
561
697
|
info.error == null
|
|
562
698
|
) {
|
|
563
|
-
|
|
699
|
+
clearSession(info.sessionID);
|
|
564
700
|
}
|
|
565
701
|
return;
|
|
566
702
|
}
|
|
567
703
|
|
|
568
704
|
if (event.type === "session.error") {
|
|
569
|
-
|
|
705
|
+
if (!sessionId) return;
|
|
706
|
+
const error = properties.error || properties;
|
|
707
|
+
if (!isTransientUnknown(error)) {
|
|
708
|
+
pendingErrors.delete(sessionId);
|
|
709
|
+
getState(sessionId).rerunRequested = false;
|
|
710
|
+
return { interaction: PERMANENT_INTERACTION, reason: "plugin-ignore-permanent-error" };
|
|
711
|
+
}
|
|
712
|
+
pendingErrors.set(sessionId, { error });
|
|
713
|
+
startRecoveryWorker(sessionId);
|
|
714
|
+
return { pending: true };
|
|
570
715
|
}
|
|
571
716
|
},
|
|
572
717
|
};
|
|
@@ -647,7 +792,9 @@ export async function runClientRecovery(input) {
|
|
|
647
792
|
},
|
|
648
793
|
};
|
|
649
794
|
}
|
|
650
|
-
const plugin = await installProjectOpenCodePlugin({
|
|
795
|
+
const plugin = await installProjectOpenCodePlugin({
|
|
796
|
+
repoRoot: input.repoRoot,
|
|
797
|
+
});
|
|
651
798
|
if (mode !== "user") {
|
|
652
799
|
return { mode, plugin };
|
|
653
800
|
}
|