opencode-auto-loop 0.1.6 → 0.1.7
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/package.json +1 -1
- package/skills/auto-loop/SKILL.md +3 -1
- package/skills/auto-loop-help/SKILL.md +1 -1
- package/skills/cancel-auto-loop/SKILL.md +6 -20
- package/src/index.ts +186 -105
package/package.json
CHANGED
|
@@ -21,7 +21,9 @@ Your previous work remains accessible through files, git history, and the state
|
|
|
21
21
|
|
|
22
22
|
## Starting the Loop
|
|
23
23
|
|
|
24
|
-
**
|
|
24
|
+
**Use the `auto-loop` tool ONCE to start the loop.** Do NOT create the state file manually. The tool handles state file creation, session tracking, and initialization.
|
|
25
|
+
|
|
26
|
+
**CRITICAL: Do NOT call the `auto-loop` tool more than once per loop.** The plugin handles all subsequent iterations automatically via idle detection. Re-invoking the tool would be rejected to prevent accidental state resets. If you need to restart with different parameters, use `/cancel-auto-loop` first.
|
|
25
27
|
|
|
26
28
|
After the tool confirms the loop is active, **immediately begin working on the task**. Do not just acknowledge — start doing the work.
|
|
27
29
|
|
|
@@ -44,7 +44,7 @@ Show plugin help and available commands.
|
|
|
44
44
|
1. **Start**: `/auto-loop` creates a state file at `.opencode/auto-loop.local.md`
|
|
45
45
|
2. **Loop**: When the AI goes idle, the plugin checks if `<promise>DONE</promise>` was output
|
|
46
46
|
3. **Continue**: If not found, it injects "Continue from where you left off"
|
|
47
|
-
4. **Stop**: Loop continues until DONE is found or max iterations (
|
|
47
|
+
4. **Stop**: Loop continues until DONE is found or max iterations (default: 25) reached
|
|
48
48
|
5. **Cleanup**: State file is deleted when complete
|
|
49
49
|
6. **Compaction**: Loop context survives session compaction — task and iteration info is preserved
|
|
50
50
|
|
|
@@ -9,33 +9,19 @@ Stop an active Auto Loop before completion.
|
|
|
9
9
|
|
|
10
10
|
## How to Use
|
|
11
11
|
|
|
12
|
-
When you invoke
|
|
12
|
+
When you need to cancel the loop, invoke the `cancel-auto-loop` tool. The tool will:
|
|
13
13
|
|
|
14
|
-
1.
|
|
14
|
+
1. Check if a loop is currently active
|
|
15
|
+
2. Report how many iterations were completed
|
|
16
|
+
3. Clean up the state file
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
test -f .opencode/auto-loop.local.md && echo "Loop is active" || echo "No active loop"
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
2. If active, read the current iteration count:
|
|
21
|
-
|
|
22
|
-
```bash
|
|
23
|
-
grep '^iteration:' .opencode/auto-loop.local.md
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
3. Delete the state file to stop the loop:
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
rm -f .opencode/auto-loop.local.md
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
4. Inform the user of the cancellation and which iteration was reached.
|
|
18
|
+
**That's it.** Just call the `cancel-auto-loop` tool. Do NOT manually delete the state file.
|
|
33
19
|
|
|
34
20
|
## When to Use
|
|
35
21
|
|
|
36
22
|
Use this command when:
|
|
37
23
|
- The task requirements have changed
|
|
38
|
-
- You want to restart with different parameters
|
|
24
|
+
- You want to restart with different parameters (cancel first, then `/auto-loop` again)
|
|
39
25
|
- The loop appears stuck and you want manual control
|
|
40
26
|
- You need to work on something else
|
|
41
27
|
|
package/src/index.ts
CHANGED
|
@@ -107,7 +107,7 @@ function getStateFile(directory: string): string {
|
|
|
107
107
|
|
|
108
108
|
// Parse markdown frontmatter state
|
|
109
109
|
function parseState(content: string): LoopState {
|
|
110
|
-
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
110
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
111
111
|
if (!match) return { active: false, iteration: 0, maxIterations: DEFAULT_MAX_ITERATIONS, debounceMs: DEFAULT_DEBOUNCE_MS };
|
|
112
112
|
|
|
113
113
|
const frontmatter = match[1];
|
|
@@ -118,13 +118,22 @@ function parseState(content: string): LoopState {
|
|
|
118
118
|
debounceMs: DEFAULT_DEBOUNCE_MS,
|
|
119
119
|
};
|
|
120
120
|
|
|
121
|
-
for (const line of frontmatter.split(
|
|
121
|
+
for (const line of frontmatter.split(/\r?\n/)) {
|
|
122
122
|
const [key, ...valueParts] = line.split(":");
|
|
123
123
|
const value = valueParts.join(":").trim();
|
|
124
124
|
if (key === "active") state.active = value === "true";
|
|
125
|
-
if (key === "iteration")
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
if (key === "iteration") {
|
|
126
|
+
const parsed = parseInt(value, 10);
|
|
127
|
+
state.iteration = Number.isNaN(parsed) ? 0 : Math.max(0, parsed);
|
|
128
|
+
}
|
|
129
|
+
if (key === "maxIterations") {
|
|
130
|
+
const parsed = parseInt(value, 10);
|
|
131
|
+
state.maxIterations = (Number.isFinite(parsed) && parsed > 0) ? parsed : DEFAULT_MAX_ITERATIONS;
|
|
132
|
+
}
|
|
133
|
+
if (key === "debounceMs") {
|
|
134
|
+
const parsed = parseInt(value, 10);
|
|
135
|
+
state.debounceMs = (Number.isFinite(parsed) && parsed >= 0) ? parsed : DEFAULT_DEBOUNCE_MS;
|
|
136
|
+
}
|
|
128
137
|
if (key === "forceLoop") state.forceLoop = value === "true";
|
|
129
138
|
if (key === "sessionId") state.sessionId = value || undefined;
|
|
130
139
|
}
|
|
@@ -247,9 +256,12 @@ async function getLastAssistantText(
|
|
|
247
256
|
}
|
|
248
257
|
}
|
|
249
258
|
|
|
250
|
-
// Check completion by looking for <promise>DONE</promise>
|
|
259
|
+
// Check completion by looking for <promise>DONE</promise> OR STATUS: COMPLETE
|
|
260
|
+
// in the last assistant text. Either signal indicates the AI believes the task
|
|
261
|
+
// is finished. The validateCompletion() function further validates legitimacy.
|
|
251
262
|
function checkCompletion(text: string): boolean {
|
|
252
|
-
|
|
263
|
+
const cleaned = stripCodeFences(text);
|
|
264
|
+
return COMPLETION_TAG.test(cleaned) || STATUS_COMPLETE_TAG.test(cleaned);
|
|
253
265
|
}
|
|
254
266
|
|
|
255
267
|
// Extract the STATUS signal presence from text.
|
|
@@ -418,10 +430,12 @@ function buildLoopContextReminder(state: LoopState): string {
|
|
|
418
430
|
const forceLabel = state.forceLoop ? " [FORCE MODE]" : "";
|
|
419
431
|
const rules = state.forceLoop
|
|
420
432
|
? `IMPORTANT RULES:
|
|
433
|
+
- Do NOT call the auto-loop tool — the plugin handles continuation automatically
|
|
421
434
|
- Before going idle, output ## Completed and ## Next Steps sections
|
|
422
435
|
- FORCE MODE is active — the loop will continue for all ${state.maxIterations} iterations regardless of completion signals
|
|
423
436
|
- Focus on making steady progress each iteration`
|
|
424
437
|
: `IMPORTANT RULES:
|
|
438
|
+
- Do NOT call the auto-loop tool — the plugin handles continuation automatically
|
|
425
439
|
- Before going idle, output ## Completed and ## Next Steps sections
|
|
426
440
|
- You MUST include a STATUS line: either \`STATUS: IN_PROGRESS\` or \`STATUS: COMPLETE\` on its own line
|
|
427
441
|
- Do NOT output <promise>DONE</promise> if there are ANY unchecked items (\`- [ ]\`) in your Next Steps — the plugin WILL reject it
|
|
@@ -492,11 +506,10 @@ export const AutoLoopPlugin: Plugin = async (ctx) => {
|
|
|
492
506
|
|
|
493
507
|
// Debounce tracking for idle events
|
|
494
508
|
let lastContinuation = 0;
|
|
495
|
-
//
|
|
496
|
-
//
|
|
497
|
-
//
|
|
498
|
-
|
|
499
|
-
let continuationInFlight = false;
|
|
509
|
+
// Reentrant guard: prevent concurrent idle event processing.
|
|
510
|
+
// Two idle events can interleave at await points, causing duplicate
|
|
511
|
+
// state reads/writes. This ensures only one handler runs at a time.
|
|
512
|
+
let handlingIdle = false;
|
|
500
513
|
|
|
501
514
|
return {
|
|
502
515
|
tool: {
|
|
@@ -523,33 +536,58 @@ export const AutoLoopPlugin: Plugin = async (ctx) => {
|
|
|
523
536
|
async execute({ task, maxIterations = DEFAULT_MAX_ITERATIONS, debounceMs = DEFAULT_DEBOUNCE_MS, forceLoop = false }, context) {
|
|
524
537
|
if (context.abort.aborted) return "Auto Loop start was cancelled.";
|
|
525
538
|
|
|
539
|
+
// Coerce parameters to correct types (AI may pass strings)
|
|
540
|
+
const parsedMax = parseInt(String(maxIterations), 10);
|
|
541
|
+
const safeMaxIterations = (typeof maxIterations === "number" && maxIterations > 0)
|
|
542
|
+
? maxIterations
|
|
543
|
+
: (Number.isFinite(parsedMax) && parsedMax > 0 ? parsedMax : DEFAULT_MAX_ITERATIONS);
|
|
544
|
+
const parsedDebounce = parseInt(String(debounceMs), 10);
|
|
545
|
+
const safeDebounceMs = (typeof debounceMs === "number" && debounceMs >= 0)
|
|
546
|
+
? debounceMs
|
|
547
|
+
: (Number.isFinite(parsedDebounce) && parsedDebounce >= 0 ? parsedDebounce : DEFAULT_DEBOUNCE_MS);
|
|
548
|
+
|
|
549
|
+
// Guard: if a loop is already active for this session, do NOT reset state.
|
|
550
|
+
// This prevents the AI from accidentally re-invoking the tool each iteration
|
|
551
|
+
// (which would reset the iteration counter back to 0 every time).
|
|
552
|
+
const existingState = await readState(directory);
|
|
553
|
+
if (existingState.active && existingState.sessionId === context.sessionID) {
|
|
554
|
+
log("warn", `Tool re-invoked while loop already active (iteration ${existingState.iteration}/${existingState.maxIterations}). Ignoring to prevent state reset.`);
|
|
555
|
+
return `Auto Loop is ALREADY ACTIVE (iteration ${existingState.iteration}/${existingState.maxIterations}). Do NOT call the auto-loop tool again — the plugin handles continuation automatically. Just keep working on the task. Use /cancel-auto-loop first if you need to restart with different parameters.`;
|
|
556
|
+
}
|
|
557
|
+
if (existingState.active && existingState.sessionId && existingState.sessionId !== context.sessionID) {
|
|
558
|
+
log("warn", `Starting new loop in session ${context.sessionID}, overwriting active loop from session ${existingState.sessionId} (was at iteration ${existingState.iteration}/${existingState.maxIterations})`);
|
|
559
|
+
toast(`Auto Loop: replacing active loop from another session`, "warning");
|
|
560
|
+
}
|
|
561
|
+
|
|
526
562
|
const state: LoopState = {
|
|
527
563
|
active: true,
|
|
528
564
|
iteration: 0,
|
|
529
|
-
maxIterations,
|
|
530
|
-
debounceMs,
|
|
565
|
+
maxIterations: safeMaxIterations,
|
|
566
|
+
debounceMs: safeDebounceMs,
|
|
531
567
|
forceLoop: forceLoop ? true : undefined,
|
|
532
568
|
sessionId: context.sessionID,
|
|
533
569
|
prompt: task,
|
|
534
570
|
};
|
|
535
571
|
await writeState(directory, state, log);
|
|
536
572
|
// Reset guards so the first idle event is not blocked
|
|
537
|
-
|
|
573
|
+
handlingIdle = false;
|
|
538
574
|
lastContinuation = 0;
|
|
539
575
|
|
|
540
576
|
const modeLabel = forceLoop ? " [FORCE MODE]" : "";
|
|
541
577
|
log("info", `Loop started${modeLabel} for session ${context.sessionID}`);
|
|
542
|
-
toast(`Auto Loop started${modeLabel} (max ${
|
|
578
|
+
toast(`Auto Loop started${modeLabel} (max ${safeMaxIterations} iterations)`, "success");
|
|
543
579
|
|
|
544
580
|
const forceNote = forceLoop
|
|
545
|
-
? `\n\n**FORCE MODE (--ralph):** Completion signals are IGNORED. The loop will run for all ${
|
|
581
|
+
? `\n\n**FORCE MODE (--ralph):** Completion signals are IGNORED. The loop will run for all ${safeMaxIterations} iterations regardless. Focus on making steady progress each iteration — you do NOT need to output STATUS or DONE signals.`
|
|
546
582
|
: "";
|
|
547
583
|
|
|
548
|
-
return `Auto Loop started (max ${
|
|
584
|
+
return `Auto Loop started (max ${safeMaxIterations} iterations).${forceNote}
|
|
549
585
|
|
|
550
586
|
Task: ${task}
|
|
551
587
|
|
|
552
|
-
**Begin working on the task now.** The loop will auto-continue until ${forceLoop ? `all ${
|
|
588
|
+
**Begin working on the task now.** The loop will auto-continue until ${forceLoop ? `all ${safeMaxIterations} iterations are used` : "you signal completion"}.
|
|
589
|
+
|
|
590
|
+
**CRITICAL: Do NOT call the auto-loop tool again.** The plugin handles auto-continuation automatically via idle detection. Re-invoking the tool would reset your progress. Just work on the task — the plugin will prompt you to continue when you go idle.
|
|
553
591
|
|
|
554
592
|
Before going idle each iteration, output structured progress${forceLoop ? "" : " AND a status line"}:
|
|
555
593
|
|
|
@@ -586,7 +624,8 @@ Use /cancel-auto-loop to stop early.`;
|
|
|
586
624
|
}
|
|
587
625
|
const iterations = state.iteration;
|
|
588
626
|
await clearState(directory, log);
|
|
589
|
-
|
|
627
|
+
handlingIdle = false;
|
|
628
|
+
lastContinuation = 0;
|
|
590
629
|
|
|
591
630
|
log("info", `Loop cancelled after ${iterations} iteration(s)`);
|
|
592
631
|
toast(`Auto Loop cancelled after ${iterations} iteration(s)`, "warning");
|
|
@@ -644,76 +683,121 @@ Located at: .opencode/auto-loop.local.md`;
|
|
|
644
683
|
if (event.type === "session.idle") {
|
|
645
684
|
const sessionId = event.properties.sessionID;
|
|
646
685
|
|
|
647
|
-
//
|
|
648
|
-
|
|
686
|
+
// Reentrant guard: if we're already processing an idle event,
|
|
687
|
+
// skip this one. Two idle events can interleave at await points,
|
|
688
|
+
// causing duplicate state reads/writes and double continuations.
|
|
689
|
+
if (handlingIdle) {
|
|
690
|
+
log("debug", "Idle handler already running, skipping duplicate event");
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
handlingIdle = true;
|
|
649
694
|
|
|
650
|
-
|
|
695
|
+
try {
|
|
696
|
+
const state = await readState(directory);
|
|
651
697
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
698
|
+
if (!state.active) return;
|
|
699
|
+
if (!sessionId) return;
|
|
700
|
+
if (state.sessionId && state.sessionId !== sessionId) return;
|
|
701
|
+
|
|
702
|
+
const now = Date.now();
|
|
703
|
+
if (now - lastContinuation < state.debounceMs) return;
|
|
704
|
+
|
|
705
|
+
// Double-check the session is truly idle before sending
|
|
706
|
+
if (await isSessionBusy(client, sessionId, log)) return;
|
|
707
|
+
|
|
708
|
+
// Fetch last assistant message (used for completion check + progress extraction)
|
|
709
|
+
const lastText = await getLastAssistantText(client, sessionId, directory, log);
|
|
710
|
+
|
|
711
|
+
// Skip completion check on iteration 0 (first idle after loop start)
|
|
712
|
+
// to avoid false positives from the tool's initial response text.
|
|
713
|
+
// Also skip entirely when forceLoop is true — force mode ignores
|
|
714
|
+
// all completion signals and runs until max iterations.
|
|
715
|
+
if (!state.forceLoop && state.iteration > 0 && lastText && checkCompletion(lastText)) {
|
|
716
|
+
// Validate the DONE signal — reject if there are unchecked steps
|
|
717
|
+
// or if the STATUS signal contradicts completion
|
|
718
|
+
const validation = validateCompletion(lastText);
|
|
719
|
+
if (validation.valid) {
|
|
720
|
+
await clearState(directory, log);
|
|
721
|
+
log("info", `Loop completed at iteration ${state.iteration}`);
|
|
722
|
+
toast(`Auto Loop completed after ${state.iteration} iteration(s)`, "success");
|
|
723
|
+
return;
|
|
724
|
+
} else {
|
|
725
|
+
log("warn", `Rejected premature DONE signal: ${validation.reason}`);
|
|
726
|
+
toast(`Auto Loop: DONE rejected — ${validation.reason}`, "warning");
|
|
727
|
+
// Fall through to send another continuation prompt
|
|
728
|
+
}
|
|
729
|
+
}
|
|
655
730
|
|
|
656
|
-
|
|
657
|
-
|
|
731
|
+
// Heuristic: if the AI has no remaining work, stop the loop even
|
|
732
|
+
// without an explicit DONE/STATUS signal. This catches cases where
|
|
733
|
+
// the AI says "all done" naturally without the structured tag.
|
|
734
|
+
// Conditions: not force mode, at least 1 iteration done, the last
|
|
735
|
+
// response has no unchecked steps, AND the persisted state also has
|
|
736
|
+
// no unchecked next steps.
|
|
737
|
+
if (!state.forceLoop && state.iteration > 0 && lastText) {
|
|
738
|
+
const responseHasWork = hasIncompleteSteps(lastText);
|
|
739
|
+
const stateHasWork = state.nextSteps
|
|
740
|
+
? /^\s*-\s*\[ \]/m.test(state.nextSteps)
|
|
741
|
+
: false;
|
|
742
|
+
const { hasInProgress } = getStatusSignals(lastText);
|
|
743
|
+
|
|
744
|
+
// No unchecked steps anywhere AND no explicit IN_PROGRESS signal
|
|
745
|
+
if (!responseHasWork && !stateHasWork && !hasInProgress) {
|
|
746
|
+
// Extra guard: only trigger if the response doesn't introduce
|
|
747
|
+
// new next-steps content (even without checkboxes)
|
|
748
|
+
const newSteps = extractNextSteps(lastText);
|
|
749
|
+
const hasNewWork = newSteps && newSteps.trim().length > 0;
|
|
750
|
+
if (!hasNewWork) {
|
|
751
|
+
await clearState(directory, log);
|
|
752
|
+
log("info", `Loop completed at iteration ${state.iteration} (no remaining work detected)`);
|
|
753
|
+
toast(`Auto Loop completed after ${state.iteration} iteration(s)`, "success");
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
658
758
|
|
|
659
|
-
|
|
660
|
-
|
|
759
|
+
if (state.iteration >= state.maxIterations) {
|
|
760
|
+
await clearState(directory, log);
|
|
761
|
+
log("warn", `Loop hit max iterations (${state.maxIterations})`);
|
|
762
|
+
toast(`Auto Loop stopped — max iterations (${state.maxIterations}) reached`, "warning");
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
661
765
|
|
|
662
|
-
|
|
663
|
-
|
|
766
|
+
// Extract progress from last message and merge with existing state
|
|
767
|
+
const newNextSteps = lastText ? extractNextSteps(lastText) : undefined;
|
|
768
|
+
const newCompleted = lastText ? extractCompleted(lastText) : undefined;
|
|
664
769
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
770
|
+
const newState: LoopState = {
|
|
771
|
+
...state,
|
|
772
|
+
iteration: state.iteration + 1,
|
|
773
|
+
sessionId,
|
|
774
|
+
nextSteps: newNextSteps || state.nextSteps,
|
|
775
|
+
completed: mergeCompleted(state.completed, newCompleted),
|
|
776
|
+
};
|
|
777
|
+
await writeState(directory, newState, log);
|
|
778
|
+
lastContinuation = Date.now();
|
|
779
|
+
|
|
780
|
+
// Verify the state write persisted correctly
|
|
781
|
+
const verifyState = await readState(directory);
|
|
782
|
+
if (verifyState.iteration !== newState.iteration) {
|
|
783
|
+
log("error", `State file write verification FAILED: expected iteration ${newState.iteration}, read back ${verifyState.iteration}`);
|
|
784
|
+
toast(`Auto Loop: state write failed! Check .opencode/ permissions.`, "error");
|
|
677
785
|
return;
|
|
678
|
-
} else {
|
|
679
|
-
log("warn", `Rejected premature DONE signal: ${validation.reason}`);
|
|
680
|
-
toast(`Auto Loop: DONE rejected — ${validation.reason}`, "warning");
|
|
681
|
-
// Fall through to send another continuation prompt
|
|
682
786
|
}
|
|
683
|
-
}
|
|
684
787
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
log("warn", `Loop hit max iterations (${state.maxIterations})`);
|
|
688
|
-
toast(`Auto Loop stopped — max iterations (${state.maxIterations}) reached`, "warning");
|
|
689
|
-
return;
|
|
690
|
-
}
|
|
788
|
+
// Build continuation prompt with progress context
|
|
789
|
+
const progressSection = buildProgressSection(newState);
|
|
691
790
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
const newState: LoopState = {
|
|
697
|
-
...state,
|
|
698
|
-
iteration: state.iteration + 1,
|
|
699
|
-
sessionId,
|
|
700
|
-
nextSteps: newNextSteps || state.nextSteps,
|
|
701
|
-
completed: mergeCompleted(state.completed, newCompleted),
|
|
702
|
-
};
|
|
703
|
-
await writeState(directory, newState, log);
|
|
704
|
-
lastContinuation = Date.now();
|
|
705
|
-
|
|
706
|
-
// Build continuation prompt with progress context
|
|
707
|
-
const progressSection = buildProgressSection(newState);
|
|
708
|
-
|
|
709
|
-
const forceLabel = state.forceLoop ? " [FORCE MODE]" : "";
|
|
710
|
-
const importantRules = state.forceLoop
|
|
711
|
-
? `IMPORTANT:
|
|
791
|
+
const forceLabel = state.forceLoop ? " [FORCE MODE]" : "";
|
|
792
|
+
const importantRules = state.forceLoop
|
|
793
|
+
? `IMPORTANT:
|
|
794
|
+
- Do NOT call the auto-loop tool — the plugin handles continuation automatically
|
|
712
795
|
- Pick up from the next incomplete step below
|
|
713
796
|
- Before going idle, list your progress using ## Completed and ## Next Steps sections
|
|
714
797
|
- FORCE MODE is active — the loop will continue for all ${newState.maxIterations} iterations regardless of completion signals
|
|
715
798
|
- Focus on making steady progress each iteration`
|
|
716
|
-
|
|
799
|
+
: `IMPORTANT:
|
|
800
|
+
- Do NOT call the auto-loop tool — the plugin handles continuation automatically
|
|
717
801
|
- Pick up from the next incomplete step below
|
|
718
802
|
- Before going idle, list your progress using ## Completed and ## Next Steps sections
|
|
719
803
|
- You MUST include a STATUS line: either \`STATUS: IN_PROGRESS\` or \`STATUS: COMPLETE\` on its own line
|
|
@@ -721,41 +805,36 @@ Located at: .opencode/auto-loop.local.md`;
|
|
|
721
805
|
- Only output \`STATUS: COMPLETE\` and the DONE signal when ALL steps are truly finished and Next Steps is empty
|
|
722
806
|
- Do not stop until the task is truly done`;
|
|
723
807
|
|
|
724
|
-
|
|
808
|
+
const continuationPrompt = `[AUTO LOOP${forceLabel} — ITERATION ${newState.iteration}/${newState.maxIterations}]
|
|
725
809
|
|
|
726
810
|
Continue working on the task. Do NOT repeat work that is already done.
|
|
811
|
+
Do NOT call the auto-loop tool again — the plugin handles auto-continuation.
|
|
727
812
|
${progressSection}
|
|
728
813
|
${importantRules}
|
|
729
814
|
|
|
730
815
|
Original task:
|
|
731
816
|
${state.prompt || "(no task specified)"}`;
|
|
732
817
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
// --- session.status: clear in-flight guard when session returns to idle ---
|
|
756
|
-
if (event.type === "session.status") {
|
|
757
|
-
if (event.properties.status?.type === "idle") {
|
|
758
|
-
continuationInFlight = false;
|
|
818
|
+
try {
|
|
819
|
+
// Use promptAsync (fire-and-forget) so the event handler returns
|
|
820
|
+
// immediately. This allows the next session.idle event to fire
|
|
821
|
+
// naturally when the AI finishes, enabling the loop to continue.
|
|
822
|
+
// The synchronous prompt() blocks until the AI response completes,
|
|
823
|
+
// which prevents subsequent idle events from being processed.
|
|
824
|
+
await client.session.promptAsync({
|
|
825
|
+
path: { id: sessionId },
|
|
826
|
+
body: {
|
|
827
|
+
parts: [{ type: "text", text: continuationPrompt }],
|
|
828
|
+
},
|
|
829
|
+
});
|
|
830
|
+
log("info", `Sent continuation ${newState.iteration}/${newState.maxIterations}`);
|
|
831
|
+
toast(`Auto Loop: iteration ${newState.iteration}/${newState.maxIterations}`);
|
|
832
|
+
} catch (err) {
|
|
833
|
+
log("error", `Failed to send continuation prompt: ${err}`);
|
|
834
|
+
toast(`Auto Loop: failed to send continuation — ${err}`, "error");
|
|
835
|
+
}
|
|
836
|
+
} finally {
|
|
837
|
+
handlingIdle = false;
|
|
759
838
|
}
|
|
760
839
|
}
|
|
761
840
|
|
|
@@ -797,7 +876,8 @@ ${state.prompt || "(no task specified)"}`;
|
|
|
797
876
|
) {
|
|
798
877
|
log("warn", `Session error detected, pausing loop at iteration ${state.iteration}`);
|
|
799
878
|
toast("Auto Loop paused — session error", "error");
|
|
800
|
-
|
|
879
|
+
handlingIdle = false;
|
|
880
|
+
lastContinuation = 0;
|
|
801
881
|
// Mark inactive but keep state so user can inspect/resume
|
|
802
882
|
await writeState(directory, { ...state, active: false }, log);
|
|
803
883
|
}
|
|
@@ -812,7 +892,8 @@ ${state.prompt || "(no task specified)"}`;
|
|
|
812
892
|
if (state.sessionId && deletedSessionId && state.sessionId !== deletedSessionId) return;
|
|
813
893
|
|
|
814
894
|
await clearState(directory, log);
|
|
815
|
-
|
|
895
|
+
handlingIdle = false;
|
|
896
|
+
lastContinuation = 0;
|
|
816
897
|
log("info", "Session deleted, cleaning up loop state");
|
|
817
898
|
}
|
|
818
899
|
},
|