opencode-auto-loop 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,30 +1,25 @@
1
1
  ---
2
- description: Start Auto Loop - auto-continues until task completion
2
+ description: "Start Auto Loop - auto-continues until task completion. Use: /auto-loop <task description>"
3
3
  ---
4
4
 
5
5
  # Auto Loop
6
6
 
7
- Start an iterative development loop that automatically continues until the task is complete.
7
+ Parse `$ARGUMENTS` for the task description and an optional `--max <number>` flag.
8
8
 
9
- ## Setup
9
+ - If `$ARGUMENTS` contains `--max <number>`, extract that number as **maxIterations** and remove it from the task string.
10
+ - Otherwise, use **maxIterations**: 25
10
11
 
11
- Create the state file in the project directory:
12
+ Invoke the `auto-loop` tool with:
12
13
 
13
- ```bash
14
- mkdir -p .opencode && cat > .opencode/auto-loop.local.md << 'EOF'
15
- ---
16
- active: true
17
- iteration: 0
18
- maxIterations: 100
19
- ---
20
-
21
- $ARGUMENTS
22
- EOF
23
- ```
14
+ - **task**: the extracted task description
15
+ - **maxIterations**: the extracted or default value
24
16
 
25
- ## Task
17
+ Examples:
18
+ - `/auto-loop Build a REST API` → task="Build a REST API", maxIterations=25
19
+ - `/auto-loop Build a REST API --max 50` → task="Build a REST API", maxIterations=50
20
+ - `/auto-loop --max 10 Fix all lint errors` → task="Fix all lint errors", maxIterations=10
26
21
 
27
- Now begin working on the task: **$ARGUMENTS**
22
+ After the tool confirms the loop is active, **immediately begin working on the task**. Do not just acknowledge — start doing the work right away.
28
23
 
29
24
  ## Progress Tracking
30
25
 
@@ -38,15 +33,11 @@ Before going idle, you MUST output structured progress so the plugin knows where
38
33
  - [ ] What needs to be done next (in priority order)
39
34
  ```
40
35
 
41
- The plugin extracts these into the state file for the next iteration's continuation prompt.
42
-
43
36
  ## Completion
44
37
 
45
- When the task is FULLY completed, signal completion by outputting:
38
+ When the task is FULLY completed, signal completion by outputting the promise-DONE XML tag on its own line:
46
39
 
47
- ```
48
40
  <promise>DONE</promise>
49
- ```
50
41
 
51
42
  **IMPORTANT:** ONLY output this when the task is COMPLETELY and VERIFIABLY finished. Do NOT output false promises to escape the loop.
52
43
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-auto-loop",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Auto-continue for OpenCode",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
@@ -27,7 +27,7 @@
27
27
  "skills/"
28
28
  ],
29
29
  "dependencies": {
30
- "@opencode-ai/plugin": "^0.15.31"
30
+ "@opencode-ai/plugin": "^1.2.27"
31
31
  },
32
32
  "engines": {
33
33
  "node": ">=18.0.0"
@@ -11,30 +11,19 @@ Start an iterative development loop that automatically continues until the task
11
11
 
12
12
  The Auto Loop creates a continuous feedback cycle for completing complex tasks:
13
13
 
14
- 1. You work on the task until you go idle
15
- 2. The plugin detects the idle state and checks for completion
16
- 3. If not complete, it extracts your progress and prompts you to continue
17
- 4. This repeats until you output the completion promise or max iterations reached
14
+ 1. You invoke the `auto-loop` tool, which creates the state file and starts the loop
15
+ 2. You work on the task until you go idle
16
+ 3. The plugin detects the idle state and checks for completion
17
+ 4. If not complete, it extracts your progress and prompts you to continue
18
+ 5. This repeats until you output the completion signal or max iterations reached
18
19
 
19
20
  Your previous work remains accessible through files, git history, and the state file's progress sections.
20
21
 
21
22
  ## Starting the Loop
22
23
 
23
- When you invoke this skill, create the state file in the project directory:
24
+ **Always use the `auto-loop` tool** to start the loop. Do NOT create the state file manually. The tool handles state file creation, session tracking, and initialization.
24
25
 
25
- ```bash
26
- mkdir -p .opencode && cat > .opencode/auto-loop.local.md << 'EOF'
27
- ---
28
- active: true
29
- iteration: 0
30
- maxIterations: 100
31
- ---
32
-
33
- [The user's task prompt goes here]
34
- EOF
35
- ```
36
-
37
- Then inform the user and begin working on the task.
26
+ After the tool confirms the loop is active, **immediately begin working on the task**. Do not just acknowledge — start doing the work.
38
27
 
39
28
  ## Progress Tracking - CRITICAL
40
29
 
@@ -61,9 +50,9 @@ Use this format in your final message of each iteration:
61
50
  - Order ## Next Steps by priority — the continuation will tell you to start from the top
62
51
  - The plugin extracts these sections and writes them into `auto-loop.local.md` for the next iteration
63
52
 
64
- ## Completion Promise - CRITICAL RULES
53
+ ## Completion Signal - CRITICAL RULES
65
54
 
66
- When you have FULLY completed the task, signal completion by outputting:
55
+ When you have FULLY completed the task, signal completion by outputting the promise-DONE XML tag on its own line:
67
56
 
68
57
  ```
69
58
  <promise>DONE</promise>
@@ -71,14 +60,14 @@ When you have FULLY completed the task, signal completion by outputting:
71
60
 
72
61
  **IMPORTANT CONSTRAINTS:**
73
62
 
74
- - ONLY output `<promise>DONE</promise>` when the task is COMPLETELY and VERIFIABLY finished
75
- - The statement MUST be completely and unequivocally TRUE
76
- - Do NOT output false promises to escape the loop, even if you think you're stuck
77
- - Do NOT lie even if you think you should exit for other reasons
63
+ - ONLY output the completion signal when the task is COMPLETELY and VERIFIABLY finished
64
+ - The completion tag MUST be on its own line (not inline with other text)
65
+ - Do NOT mention or echo the completion tag in explanatory text only output it as the actual signal
66
+ - Do NOT output false completion signals to escape the loop, even if you think you're stuck
78
67
  - If you're blocked, explain the blocker and request help instead of falsely completing
79
68
 
80
69
  The loop can only be stopped by:
81
- 1. Truthful completion promise
70
+ 1. Truthful completion signal
82
71
  2. Max iterations reached
83
72
  3. User running `/cancel-auto-loop`
84
73
 
@@ -97,7 +86,7 @@ The state file at `.opencode/auto-loop.local.md` uses YAML frontmatter with prog
97
86
  ---
98
87
  active: true
99
88
  iteration: 3
100
- maxIterations: 100
89
+ maxIterations: 25
101
90
  sessionId: ses_abc123
102
91
  ---
103
92
 
package/src/index.ts CHANGED
@@ -2,11 +2,10 @@ import { type Plugin, type PluginInput, tool } from "@opencode-ai/plugin";
2
2
  import {
3
3
  existsSync,
4
4
  readFileSync,
5
- writeFileSync,
6
5
  mkdirSync,
7
- unlinkSync,
8
6
  cpSync,
9
7
  } from "fs";
8
+ import { readFile, writeFile, unlink, mkdir } from "fs/promises";
10
9
  import { dirname, join } from "path";
11
10
  import { fileURLToPath } from "url";
12
11
  import { homedir } from "os";
@@ -16,6 +15,7 @@ interface LoopState {
16
15
  active: boolean;
17
16
  iteration: number;
18
17
  maxIterations: number;
18
+ debounceMs: number;
19
19
  sessionId?: string;
20
20
  prompt?: string;
21
21
  completed?: string;
@@ -30,8 +30,9 @@ type OpencodeClient = PluginInput["client"];
30
30
  const SERVICE_NAME = "auto-loop";
31
31
  const STATE_FILENAME = "auto-loop.local.md";
32
32
  const OPENCODE_CONFIG_DIR = join(homedir(), ".config/opencode");
33
- const COMPLETION_TAG = /<promise>\s*DONE\s*<\/promise>/is;
34
- const DEBOUNCE_MS = 2000;
33
+ const COMPLETION_TAG = /^\s*<promise>\s*DONE\s*<\/promise>\s*$/im;
34
+ const DEFAULT_DEBOUNCE_MS = 2000;
35
+ const DEFAULT_MAX_ITERATIONS = 25;
35
36
 
36
37
  // Get plugin root directory (ESM only — package is "type": "module")
37
38
  function getPluginRoot(): string {
@@ -104,13 +105,14 @@ function getStateFile(directory: string): string {
104
105
  // Parse markdown frontmatter state
105
106
  function parseState(content: string): LoopState {
106
107
  const match = content.match(/^---\n([\s\S]*?)\n---/);
107
- if (!match) return { active: false, iteration: 0, maxIterations: 100 };
108
+ if (!match) return { active: false, iteration: 0, maxIterations: DEFAULT_MAX_ITERATIONS, debounceMs: DEFAULT_DEBOUNCE_MS };
108
109
 
109
110
  const frontmatter = match[1];
110
111
  const state: LoopState = {
111
112
  active: false,
112
113
  iteration: 0,
113
- maxIterations: 100,
114
+ maxIterations: DEFAULT_MAX_ITERATIONS,
115
+ debounceMs: DEFAULT_DEBOUNCE_MS,
114
116
  };
115
117
 
116
118
  for (const line of frontmatter.split("\n")) {
@@ -118,7 +120,8 @@ function parseState(content: string): LoopState {
118
120
  const value = valueParts.join(":").trim();
119
121
  if (key === "active") state.active = value === "true";
120
122
  if (key === "iteration") state.iteration = parseInt(value) || 0;
121
- if (key === "maxIterations") state.maxIterations = parseInt(value) || 100;
123
+ if (key === "maxIterations") state.maxIterations = parseInt(value) || DEFAULT_MAX_ITERATIONS;
124
+ if (key === "debounceMs") state.debounceMs = parseInt(value) || DEFAULT_DEBOUNCE_MS;
122
125
  if (key === "sessionId") state.sessionId = value || undefined;
123
126
  }
124
127
 
@@ -152,6 +155,7 @@ function serializeState(state: LoopState): string {
152
155
  `active: ${state.active}`,
153
156
  `iteration: ${state.iteration}`,
154
157
  `maxIterations: ${state.maxIterations}`,
158
+ `debounceMs: ${state.debounceMs}`,
155
159
  ];
156
160
  if (state.sessionId) lines.push(`sessionId: ${state.sessionId}`);
157
161
  lines.push("---");
@@ -162,41 +166,49 @@ function serializeState(state: LoopState): string {
162
166
  }
163
167
 
164
168
  // Read state from project directory
165
- function readState(directory: string): LoopState {
169
+ async function readState(directory: string): Promise<LoopState> {
166
170
  const stateFile = getStateFile(directory);
167
- if (existsSync(stateFile)) {
168
- return parseState(readFileSync(stateFile, "utf-8"));
171
+ try {
172
+ const content = await readFile(stateFile, "utf-8");
173
+ return parseState(content);
174
+ } catch {
175
+ return { active: false, iteration: 0, maxIterations: DEFAULT_MAX_ITERATIONS, debounceMs: DEFAULT_DEBOUNCE_MS };
169
176
  }
170
- return { active: false, iteration: 0, maxIterations: 100 };
171
177
  }
172
178
 
173
179
  // Write state to project directory
174
- function writeState(directory: string, state: LoopState, log: LogFn): void {
180
+ async function writeState(directory: string, state: LoopState, log: LogFn): Promise<void> {
175
181
  try {
176
182
  const stateFile = getStateFile(directory);
177
- mkdirSync(dirname(stateFile), { recursive: true });
178
- writeFileSync(stateFile, serializeState(state));
183
+ await mkdir(dirname(stateFile), { recursive: true });
184
+ await writeFile(stateFile, serializeState(state));
179
185
  } catch (err) {
180
186
  log("error", `Failed to write state: ${err}`);
181
187
  }
182
188
  }
183
189
 
184
190
  // Clear state
185
- function clearState(directory: string, log: LogFn): void {
191
+ async function clearState(directory: string, log: LogFn): Promise<void> {
186
192
  try {
187
193
  const stateFile = getStateFile(directory);
188
- if (existsSync(stateFile)) unlinkSync(stateFile);
194
+ await unlink(stateFile);
189
195
  } catch (err) {
190
- log("warn", `Failed to clear state: ${err}`);
196
+ // ENOENT is fine file already gone
197
+ if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
198
+ log("warn", `Failed to clear state: ${err}`);
199
+ }
191
200
  }
192
201
  }
193
202
 
194
- // Strip markdown code fences before checking for completion tag
203
+ // Strip markdown code fences and inline code before checking for completion tag
195
204
  function stripCodeFences(text: string): string {
196
- return text.replace(/```[\s\S]*?```/g, "");
205
+ return text
206
+ .replace(/```[\s\S]*?```/g, "") // triple-backtick blocks
207
+ .replace(/`[^`]+`/g, ""); // inline backtick code
197
208
  }
198
209
 
199
- // Extract text from the last assistant message in a session
210
+ // Extract text from the last assistant message in a session.
211
+ // Fetches only the most recent messages to avoid pulling the entire history.
200
212
  async function getLastAssistantText(
201
213
  client: OpencodeClient,
202
214
  sessionId: string,
@@ -206,7 +218,7 @@ async function getLastAssistantText(
206
218
  try {
207
219
  const response = await client.session.messages({
208
220
  path: { id: sessionId },
209
- query: { directory },
221
+ query: { directory, limit: 10 },
210
222
  });
211
223
 
212
224
  const messages = response.data ?? [];
@@ -348,6 +360,27 @@ Before going idle, list your progress using ## Completed and ## Next Steps secti
348
360
  Do NOT output false completion promises. If blocked, explain the blocker.`;
349
361
  }
350
362
 
363
+ // Check if session is currently busy (not idle)
364
+ async function isSessionBusy(
365
+ client: OpencodeClient,
366
+ sessionId: string,
367
+ log: LogFn
368
+ ): Promise<boolean> {
369
+ try {
370
+ const response = await client.session.status({});
371
+ const statuses = response.data ?? {};
372
+ const status = statuses[sessionId];
373
+ if (status && status.type !== "idle") {
374
+ log("debug", `Session ${sessionId} is ${status.type}, skipping continuation`);
375
+ return true;
376
+ }
377
+ return false;
378
+ } catch (err) {
379
+ log("warn", `Failed to check session status: ${err}`);
380
+ return false; // Assume not busy if we can't check
381
+ }
382
+ }
383
+
351
384
  // Main plugin
352
385
  export const AutoLoopPlugin: Plugin = async (ctx) => {
353
386
  const directory = ctx.directory || process.cwd();
@@ -384,6 +417,11 @@ export const AutoLoopPlugin: Plugin = async (ctx) => {
384
417
 
385
418
  // Debounce tracking for idle events
386
419
  let lastContinuation = 0;
420
+ // Guard: prevent sending while a continuation is already in-flight.
421
+ // Set to true when we send promptAsync, cleared when we receive a
422
+ // session.idle or session.status(idle) event — NOT in the finally block,
423
+ // which fires too early (~50ms after the 204, while AI is still busy).
424
+ let continuationInFlight = false;
387
425
 
388
426
  return {
389
427
  tool: {
@@ -397,17 +435,27 @@ export const AutoLoopPlugin: Plugin = async (ctx) => {
397
435
  maxIterations: tool.schema
398
436
  .number()
399
437
  .optional()
400
- .describe("Maximum iterations (default: 100)"),
438
+ .describe("Maximum iterations (default: 25)"),
439
+ debounceMs: tool.schema
440
+ .number()
441
+ .optional()
442
+ .describe("Debounce delay between iterations in ms (default: 2000)"),
401
443
  },
402
- async execute({ task, maxIterations = 100 }, context) {
444
+ async execute({ task, maxIterations = DEFAULT_MAX_ITERATIONS, debounceMs = DEFAULT_DEBOUNCE_MS }, context) {
445
+ if (context.abort.aborted) return "Auto Loop start was cancelled.";
446
+
403
447
  const state: LoopState = {
404
448
  active: true,
405
449
  iteration: 0,
406
450
  maxIterations,
451
+ debounceMs,
407
452
  sessionId: context.sessionID,
408
453
  prompt: task,
409
454
  };
410
- writeState(directory, state, log);
455
+ await writeState(directory, state, log);
456
+ // Reset guards so the first idle event is not blocked
457
+ continuationInFlight = false;
458
+ lastContinuation = 0;
411
459
 
412
460
  log("info", `Loop started for session ${context.sessionID}`);
413
461
  toast(`Auto Loop started (max ${maxIterations} iterations)`, "success");
@@ -416,7 +464,9 @@ export const AutoLoopPlugin: Plugin = async (ctx) => {
416
464
 
417
465
  Task: ${task}
418
466
 
419
- I will auto-continue until the task is complete. Before going idle each iteration, I will output structured progress:
467
+ **Begin working on the task now.** The loop will auto-continue until you signal completion.
468
+
469
+ Before going idle each iteration, output structured progress:
420
470
 
421
471
  \`\`\`
422
472
  ## Completed
@@ -426,7 +476,7 @@ I will auto-continue until the task is complete. Before going idle each iteratio
426
476
  - [ ] What remains (in priority order)
427
477
  \`\`\`
428
478
 
429
- When fully done, I will output \`<promise>DONE</promise>\` to signal completion.
479
+ When the task is FULLY and VERIFIABLY complete, output the completion signal on its own line (the promise-DONE XML tag). Do NOT mention or echo the completion tag until you are truly done.
430
480
 
431
481
  Use /cancel-auto-loop to stop early.`;
432
482
  },
@@ -435,13 +485,15 @@ Use /cancel-auto-loop to stop early.`;
435
485
  "cancel-auto-loop": tool({
436
486
  description: "Cancel active Auto Loop",
437
487
  args: {},
438
- async execute() {
439
- const state = readState(directory);
488
+ async execute(_args, context) {
489
+ if (context.abort.aborted) return "Cancel was aborted.";
490
+ const state = await readState(directory);
440
491
  if (!state.active) {
441
492
  return "No active Auto Loop to cancel.";
442
493
  }
443
494
  const iterations = state.iteration;
444
- clearState(directory, log);
495
+ await clearState(directory, log);
496
+ continuationInFlight = false;
445
497
 
446
498
  log("info", `Loop cancelled after ${iterations} iteration(s)`);
447
499
  toast(`Auto Loop cancelled after ${iterations} iteration(s)`, "warning");
@@ -458,10 +510,16 @@ Use /cancel-auto-loop to stop early.`;
458
510
 
459
511
  ## Available Commands
460
512
 
461
- - \`/auto-loop <task>\` - Start an auto-continuation loop
513
+ - \`/auto-loop <task>\` - Start an auto-continuation loop (default: 25 iterations)
514
+ - \`/auto-loop <task> --max <n>\` - Start with a custom iteration limit
462
515
  - \`/cancel-auto-loop\` - Stop an active loop
463
516
  - \`/auto-loop-help\` - Show this help
464
517
 
518
+ ## Examples
519
+
520
+ - \`/auto-loop Build a REST API\` — runs up to 25 iterations
521
+ - \`/auto-loop Fix all lint errors --max 10\` — runs up to 10 iterations
522
+
465
523
  ## How It Works
466
524
 
467
525
  1. Start with: /auto-loop "Build a REST API"
@@ -480,28 +538,37 @@ Located at: .opencode/auto-loop.local.md`;
480
538
  event: async ({ event }) => {
481
539
  // --- session.idle: core auto-continuation logic ---
482
540
  if (event.type === "session.idle") {
483
- const now = Date.now();
484
- if (now - lastContinuation < DEBOUNCE_MS) return;
485
-
486
541
  const sessionId = event.properties.sessionID;
487
- const state = readState(directory);
542
+
543
+ // Session confirmed idle — safe to clear in-flight guard
544
+ continuationInFlight = false;
545
+
546
+ const state = await readState(directory);
488
547
 
489
548
  if (!state.active) return;
490
549
  if (!sessionId) return;
491
550
  if (state.sessionId && state.sessionId !== sessionId) return;
492
551
 
552
+ const now = Date.now();
553
+ if (now - lastContinuation < state.debounceMs) return;
554
+
555
+ // Double-check the session is truly idle before sending
556
+ if (await isSessionBusy(client, sessionId, log)) return;
557
+
493
558
  // Fetch last assistant message (used for completion check + progress extraction)
494
559
  const lastText = await getLastAssistantText(client, sessionId, directory, log);
495
560
 
496
- if (lastText && checkCompletion(lastText)) {
497
- clearState(directory, log);
561
+ // Skip completion check on iteration 0 (first idle after loop start)
562
+ // to avoid false positives from the tool's initial response text
563
+ if (state.iteration > 0 && lastText && checkCompletion(lastText)) {
564
+ await clearState(directory, log);
498
565
  log("info", `Loop completed at iteration ${state.iteration}`);
499
566
  toast(`Auto Loop completed after ${state.iteration} iteration(s)`, "success");
500
567
  return;
501
568
  }
502
569
 
503
570
  if (state.iteration >= state.maxIterations) {
504
- clearState(directory, log);
571
+ await clearState(directory, log);
505
572
  log("warn", `Loop hit max iterations (${state.maxIterations})`);
506
573
  toast(`Auto Loop stopped — max iterations (${state.maxIterations}) reached`, "warning");
507
574
  return;
@@ -515,12 +582,10 @@ Located at: .opencode/auto-loop.local.md`;
515
582
  ...state,
516
583
  iteration: state.iteration + 1,
517
584
  sessionId,
518
- // Update next steps if we found new ones, otherwise keep previous
519
585
  nextSteps: newNextSteps || state.nextSteps,
520
- // Merge completed: append new completed items to existing
521
586
  completed: mergeCompleted(state.completed, newCompleted),
522
587
  };
523
- writeState(directory, newState, log);
588
+ await writeState(directory, newState, log);
524
589
  lastContinuation = Date.now();
525
590
 
526
591
  // Build continuation prompt with progress context
@@ -540,7 +605,13 @@ Original task:
540
605
  ${state.prompt || "(no task specified)"}`;
541
606
 
542
607
  try {
543
- await client.session.prompt({
608
+ // Use promptAsync (fire-and-forget) so the event handler returns
609
+ // immediately. This allows the next session.idle event to fire
610
+ // naturally when the AI finishes, enabling the loop to continue.
611
+ // The synchronous prompt() blocks until the AI response completes,
612
+ // which prevents subsequent idle events from being processed.
613
+ continuationInFlight = true;
614
+ await client.session.promptAsync({
544
615
  path: { id: sessionId },
545
616
  body: {
546
617
  parts: [{ type: "text", text: continuationPrompt }],
@@ -549,21 +620,31 @@ ${state.prompt || "(no task specified)"}`;
549
620
  log("info", `Sent continuation ${newState.iteration}/${newState.maxIterations}`);
550
621
  toast(`Auto Loop: iteration ${newState.iteration}/${newState.maxIterations}`);
551
622
  } catch (err) {
623
+ // On failure, clear the guard so the next idle event can retry
624
+ continuationInFlight = false;
552
625
  log("error", `Failed to send continuation prompt: ${err}`);
553
626
  }
554
627
  }
555
628
 
629
+ // --- session.status: clear in-flight guard when session returns to idle ---
630
+ if (event.type === "session.status") {
631
+ if (event.properties.status?.type === "idle") {
632
+ continuationInFlight = false;
633
+ }
634
+ }
635
+
556
636
  // --- session.compacted: re-inject loop context after compaction ---
557
637
  if (event.type === "session.compacted") {
558
638
  const sessionId = event.properties.sessionID;
559
- const state = readState(directory);
639
+ const state = await readState(directory);
560
640
 
561
641
  if (!state.active) return;
562
642
  if (state.sessionId && state.sessionId !== sessionId) return;
563
643
 
564
644
  // After compaction, the AI loses loop context — send a reminder
645
+ // Use promptAsync so we don't block event processing
565
646
  try {
566
- await client.session.prompt({
647
+ await client.session.promptAsync({
567
648
  path: { id: sessionId },
568
649
  body: {
569
650
  parts: [{ type: "text", text: buildLoopContextReminder(state) }],
@@ -578,7 +659,11 @@ ${state.prompt || "(no task specified)"}`;
578
659
  // --- session.error: pause the loop on error ---
579
660
  if (event.type === "session.error") {
580
661
  const sessionId = event.properties.sessionID;
581
- const state = readState(directory);
662
+ // sessionID is optional in the SDK types — if missing, we can't
663
+ // reliably attribute the error to our session, so skip.
664
+ if (!sessionId) return;
665
+
666
+ const state = await readState(directory);
582
667
 
583
668
  if (
584
669
  state.active &&
@@ -586,20 +671,22 @@ ${state.prompt || "(no task specified)"}`;
586
671
  ) {
587
672
  log("warn", `Session error detected, pausing loop at iteration ${state.iteration}`);
588
673
  toast("Auto Loop paused — session error", "error");
674
+ continuationInFlight = false;
589
675
  // Mark inactive but keep state so user can inspect/resume
590
- writeState(directory, { ...state, active: false }, log);
676
+ await writeState(directory, { ...state, active: false }, log);
591
677
  }
592
678
  }
593
679
 
594
680
  // --- session.deleted: clean up if it's our session ---
595
681
  if (event.type === "session.deleted") {
596
- const state = readState(directory);
682
+ const state = await readState(directory);
597
683
  if (!state.active) return;
598
684
 
599
685
  const deletedSessionId = event.properties.info?.id;
600
686
  if (state.sessionId && deletedSessionId && state.sessionId !== deletedSessionId) return;
601
687
 
602
- clearState(directory, log);
688
+ await clearState(directory, log);
689
+ continuationInFlight = false;
603
690
  log("info", "Session deleted, cleaning up loop state");
604
691
  }
605
692
  },