autodev-cli 1.4.187 → 1.4.189

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/out/taskLoop.js CHANGED
@@ -72,6 +72,27 @@ const projectSkills_1 = require("./core/projectSkills");
72
72
  const configManager_1 = require("./configManager");
73
73
  const agentBackup_1 = require("./agentBackup");
74
74
  const version_1 = require("./version");
75
+ // ---------------------------------------------------------------------------
76
+ // Persistent-CLI predicates (Phase 2). A CLI provider that runs through its
77
+ // persistent sibling's in-process client this turn must be treated by the loop
78
+ // exactly like that sibling (busy flag, mid-turn steer, session capture, close,
79
+ // compact/clear) instead of the exit-file/command path. These mirror the gate
80
+ // the dispatcher uses, so both agree per turn. When the gate is off (or the
81
+ // transport is unavailable) they return false and the legacy command handling
82
+ // applies — nothing regresses.
83
+ // ---------------------------------------------------------------------------
84
+ /** claude-cli is reusing the claude-tui StructuredClient this turn. */
85
+ function claudeCliIsPersistent(p) { return p === 'claude-cli' && (0, claudeTuiProvider_1.claudeCliPersistentEnabled)(); }
86
+ /** copilot-cli is reusing the copilot-sdk LocalSession this turn. */
87
+ function copilotCliIsPersistent(p) { return p === 'copilot-cli' && (0, copilotSdkProvider_1.copilotCliPersistentEnabled)(); }
88
+ /** opencode-cli is reusing the opencode-sdk server+session this turn. */
89
+ function opencodeCliIsPersistent(p) { return p === 'opencode-cli' && (0, opencodeSdkProvider_1.opencodeCliPersistentEnabled)(); }
90
+ /** Provider runs on the persistent claude-tui client (native or claude-cli routed). */
91
+ function usesClaudeTuiClient(p) { return p === 'claude-tui' || claudeCliIsPersistent(p); }
92
+ /** Provider runs on the persistent copilot-sdk session (native or copilot-cli routed). */
93
+ function usesCopilotSdkSession(p) { return p === 'copilot-sdk' || copilotCliIsPersistent(p); }
94
+ /** Provider runs on the persistent opencode-sdk server (native or opencode-cli routed). */
95
+ function usesOpencodeSdkServer(p) { return p === 'opencode-sdk' || opencodeCliIsPersistent(p); }
75
96
  /**
76
97
  * After `_waitForTaskCompletion` resolves, decide whether the SUCCESS/completion
77
98
  * path (`task_done` → COMPLETED, `task_progress` → WORKING, completedCount++)
@@ -373,7 +394,7 @@ class TaskLoopRunner {
373
394
  this._compacting = true;
374
395
  log(`🗜 Manual compact triggered (provider: ${provider})`);
375
396
  try {
376
- if (provider === 'claude-cli') {
397
+ if (provider === 'claude-cli' && !claudeCliIsPersistent(provider)) {
377
398
  let sid = (0, sessionState_1.getSessionId)(root, 'claude-cli');
378
399
  if (!sid) {
379
400
  sid = (0, dispatcher_1.findLatestClaudeSession)(root);
@@ -386,8 +407,10 @@ class TaskLoopRunner {
386
407
  log('⚠️ Compact: no Claude session ID found');
387
408
  }
388
409
  }
389
- else if (provider === 'claude-tui') {
390
- let sid = (0, sessionState_1.getSessionId)(root, 'claude-tui');
410
+ else if (usesClaudeTuiClient(provider)) {
411
+ // claude-tui OR a persistent claude-cli — compact the LIVE StructuredClient
412
+ // (spawning a separate `claude … /compact` would fight the held session).
413
+ let sid = (0, sessionState_1.getSessionId)(root, provider);
391
414
  if (!sid) {
392
415
  sid = (0, claudeTuiProvider_1.getClaudeTuiLatestSessionId)(root);
393
416
  }
@@ -399,7 +422,7 @@ class TaskLoopRunner {
399
422
  log('⚠️ Compact: no Claude TUI session ID found');
400
423
  }
401
424
  }
402
- else if (provider === 'opencode-cli') {
425
+ else if (provider === 'opencode-cli' && !opencodeCliIsPersistent(provider)) {
403
426
  let sid = (0, sessionState_1.getSessionId)(root, 'opencode-cli');
404
427
  if (!sid) {
405
428
  sid = await (0, opencodeCliProvider_1.getLatestOpenCodeSessionId)(root, log);
@@ -412,7 +435,7 @@ class TaskLoopRunner {
412
435
  log('⚠️ Compact: no OpenCode session ID found');
413
436
  }
414
437
  }
415
- else if (provider === 'opencode-sdk') {
438
+ else if (usesOpencodeSdkServer(provider)) {
416
439
  await (0, opencodeSdkProvider_1.runOpencodeSdkCompact)(root, log);
417
440
  log('🗜 Compact complete (opencode-sdk)');
418
441
  }
@@ -433,7 +456,7 @@ class TaskLoopRunner {
433
456
  const log = (m) => this._cb?.log(m);
434
457
  log(`🗑 Manual /clear triggered (provider: ${provider})`);
435
458
  try {
436
- if (provider === 'claude-cli') {
459
+ if (provider === 'claude-cli' && !claudeCliIsPersistent(provider)) {
437
460
  let sid = (0, sessionState_1.getSessionId)(root, 'claude-cli');
438
461
  if (!sid) {
439
462
  sid = (0, dispatcher_1.findLatestClaudeSession)(root);
@@ -447,14 +470,15 @@ class TaskLoopRunner {
447
470
  log('⚠️ /clear: no Claude session ID found');
448
471
  }
449
472
  }
450
- else if (provider === 'claude-tui') {
451
- let sid = (0, sessionState_1.getSessionId)(root, 'claude-tui');
473
+ else if (usesClaudeTuiClient(provider)) {
474
+ // claude-tui OR a persistent claude-cli — /clear the LIVE StructuredClient.
475
+ let sid = (0, sessionState_1.getSessionId)(root, provider);
452
476
  if (!sid) {
453
477
  sid = (0, claudeTuiProvider_1.getClaudeTuiLatestSessionId)(root);
454
478
  }
455
479
  if (sid) {
456
480
  await (0, claudeTuiProvider_1.runClaudeTuiClear)(root, sid, log);
457
- (0, sessionState_1.clearSessionId)(root, 'claude-tui');
481
+ (0, sessionState_1.clearSessionId)(root, provider);
458
482
  log('🗑 /clear (TUI) complete — session reset');
459
483
  }
460
484
  else {
@@ -1118,14 +1142,16 @@ class TaskLoopRunner {
1118
1142
  const root = this._workspaceRoot;
1119
1143
  const provider = this._cb?.getActiveProvider() ?? '';
1120
1144
  if (root) {
1121
- if (provider === 'claude-tui') {
1122
- let sessionId = (0, sessionState_1.getSessionId)(root, 'claude-tui');
1145
+ if (usesClaudeTuiClient(provider)) {
1146
+ // claude-tui OR a persistent claude-cli — clear the LIVE StructuredClient.
1147
+ const claudeProvider = provider;
1148
+ let sessionId = (0, sessionState_1.getSessionId)(root, claudeProvider);
1123
1149
  if (!sessionId) {
1124
1150
  sessionId = (0, claudeTuiProvider_1.getClaudeTuiLatestSessionId)(root);
1125
1151
  }
1126
1152
  if (sessionId) {
1127
1153
  (0, claudeTuiProvider_1.runClaudeTuiClear)(root, sessionId, msg => this._cb?.log(msg))
1128
- .then(() => { (0, sessionState_1.clearSessionId)(root, 'claude-tui'); this._cb?.log('🗑 /clear (TUI) complete'); })
1154
+ .then(() => { (0, sessionState_1.clearSessionId)(root, claudeProvider); this._cb?.log('🗑 /clear (TUI) complete'); })
1129
1155
  .catch(e => this._cb?.log(`⚠️ /clear (TUI) failed: ${e instanceof Error ? e.message : String(e)}`));
1130
1156
  }
1131
1157
  else {
@@ -1133,7 +1159,7 @@ class TaskLoopRunner {
1133
1159
  }
1134
1160
  }
1135
1161
  else {
1136
- // Default to claude-cli
1162
+ // Default to claude-cli (legacy per-turn command path)
1137
1163
  let sessionId = (0, sessionState_1.getSessionId)(root, 'claude-cli');
1138
1164
  if (!sessionId) {
1139
1165
  sessionId = (0, dispatcher_1.findLatestClaudeSession)(root);
@@ -1187,7 +1213,7 @@ class TaskLoopRunner {
1187
1213
  // `_currentTask` routes an idle steer to the buffer path below, which wakes the
1188
1214
  // idle sleep and drains it into a real turn immediately.
1189
1215
  const turnInFlight = !!this._currentTask;
1190
- if (root && turnInFlight && provider === 'claude-tui' && (0, claudeTuiProvider_1.isClaudeTuiBusy)(root)) {
1216
+ if (root && turnInFlight && usesClaudeTuiClient(provider) && (0, claudeTuiProvider_1.isClaudeTuiBusy)(root)) {
1191
1217
  if (await (0, claudeTuiProvider_1.steerClaudeTui)(root, clean, msg => this._cb?.log(msg))) {
1192
1218
  this._cb?.log(`⚡ Steered mid-turn: "${clean.slice(0, 80)}"`);
1193
1219
  onDelivered?.(); // durably injected into the live turn
@@ -1198,7 +1224,7 @@ class TaskLoopRunner {
1198
1224
  // injects into the running turn — true mid-turn steering, same as claude-tui.
1199
1225
  // (copilot-CLI is a one-shot subprocess with no live turn to inject into, so
1200
1226
  // it always falls through to the next-turn TODO queue below.)
1201
- if (root && turnInFlight && provider === 'copilot-sdk' && (0, copilotSdkProvider_1.isCopilotSdkBusy)(root)) {
1227
+ if (root && turnInFlight && usesCopilotSdkSession(provider) && (0, copilotSdkProvider_1.isCopilotSdkBusy)(root)) {
1202
1228
  if (await (0, copilotSdkProvider_1.steerCopilotSdk)(root, clean, msg => this._cb?.log(msg))) {
1203
1229
  this._cb?.log(`⚡ Steered mid-turn (copilot-sdk): "${clean.slice(0, 80)}"`);
1204
1230
  onDelivered?.();
@@ -1207,7 +1233,7 @@ class TaskLoopRunner {
1207
1233
  }
1208
1234
  // opencode-sdk keeps a persistent in-process server + session; a prompt sent to
1209
1235
  // the live session mid-turn is picked up by the running turn (interrupt + continue).
1210
- if (root && turnInFlight && provider === 'opencode-sdk' && (0, opencodeSdkProvider_1.isOpencodeSdkBusy)(root)) {
1236
+ if (root && turnInFlight && usesOpencodeSdkServer(provider) && (0, opencodeSdkProvider_1.isOpencodeSdkBusy)(root)) {
1211
1237
  if (await (0, opencodeSdkProvider_1.steerOpencodeSdk)(root, clean, msg => this._cb?.log(msg))) {
1212
1238
  this._cb?.log(`⚡ Steered mid-turn (opencode-sdk): "${clean.slice(0, 80)}"`);
1213
1239
  onDelivered?.();
@@ -1225,6 +1251,16 @@ class TaskLoopRunner {
1225
1251
  return;
1226
1252
  }
1227
1253
  }
1254
+ // Persistent-process grok (ACP `grok agent stdio`, grok-cli AND grok-tui):
1255
+ // a mid-run steer is a QUEUED session/prompt delivered to the LIVE process —
1256
+ // it never exits, so the steer reaches the same long-lived agent.
1257
+ if (root && turnInFlight && (provider === 'grok-cli' || provider === 'grok-tui') && (0, grokTuiProvider_1.isGrokAcpBusy)(root)) {
1258
+ if ((0, grokTuiProvider_1.steerGrokAcp)(root, clean, msg => this._cb?.log(msg))) {
1259
+ this._cb?.log(`⚡ Steered mid-turn (grok ACP): "${clean.slice(0, 80)}"`);
1260
+ onDelivered?.();
1261
+ return;
1262
+ }
1263
+ }
1228
1264
  // Fallback: the provider can't be steered mid-turn (grok-*/copilot-cli are
1229
1265
  // one-shot subprocesses). Do NOT append to TODO here — a provider that edits
1230
1266
  // TODO.md itself as it runs (grok manages its own checklist) would clobber a
@@ -1562,13 +1598,16 @@ class TaskLoopRunner {
1562
1598
  // turn takes >30 s to finish. Instead, use the in-flight turn flag which
1563
1599
  // stays true for the entire duration of the async, including after
1564
1600
  // _waitForTaskCompletion resolves but before the turn emits 'result'.
1565
- if (provider === 'claude-tui') {
1601
+ // claude-tui OR a persistent claude-cli — both track the turn via the
1602
+ // shared in-memory busy flag on the StructuredClient (honest signal even
1603
+ // for turns >30s, unlike the exit-file sentinel).
1604
+ if (usesClaudeTuiClient(provider)) {
1566
1605
  return (0, claudeTuiProvider_1.isClaudeTuiBusy)(this._workspaceRoot);
1567
1606
  }
1568
- if (provider === 'copilot-sdk') {
1607
+ if (usesCopilotSdkSession(provider)) {
1569
1608
  return (0, copilotSdkProvider_1.isCopilotSdkBusy)(this._workspaceRoot);
1570
1609
  }
1571
- if (provider === 'opencode-sdk') {
1610
+ if (usesOpencodeSdkServer(provider)) {
1572
1611
  return (0, opencodeSdkProvider_1.isOpencodeSdkBusy)(this._workspaceRoot);
1573
1612
  }
1574
1613
  // grok-tui: like claude-tui, it runs a PERSISTENT session (tmux PTY, or a
@@ -1958,7 +1997,7 @@ class TaskLoopRunner {
1958
1997
  // The exit-code file is written only after the shell command completes,
1959
1998
  // so a non-empty file guarantees the stdout file is fully flushed.
1960
1999
  const activeProvider = this._cb?.getActiveProvider();
1961
- if (this._workspaceRoot && activeProvider === 'claude-tui') {
2000
+ if (this._workspaceRoot && usesClaudeTuiClient(activeProvider)) {
1962
2001
  // claude-tui: _waitForTaskCompletion resolves as soon as the task is
1963
2002
  // marked [x] in TODO.md, but the persistent async turn may still be
1964
2003
  // running (Claude executing further tool calls, marking other tasks [~],
@@ -1989,7 +2028,7 @@ class TaskLoopRunner {
1989
2028
  (0, claudeTuiProvider_1.forceIdleClaudeTui)(this._workspaceRoot);
1990
2029
  }
1991
2030
  }
1992
- else if (this._workspaceRoot && activeProvider === 'copilot-sdk') {
2031
+ else if (this._workspaceRoot && usesCopilotSdkSession(activeProvider)) {
1993
2032
  const tuiDeadline = Date.now() + 10 * 60_000;
1994
2033
  while ((0, copilotSdkProvider_1.isCopilotSdkBusy)(this._workspaceRoot) && Date.now() < tuiDeadline) {
1995
2034
  if (this._state !== 'running') {
@@ -2001,7 +2040,7 @@ class TaskLoopRunner {
2001
2040
  this._cb?.log('⚠️ Copilot TUI turn did not complete within 10 minutes — moving on');
2002
2041
  }
2003
2042
  }
2004
- else if (this._workspaceRoot && activeProvider === 'opencode-sdk') {
2043
+ else if (this._workspaceRoot && usesOpencodeSdkServer(activeProvider)) {
2005
2044
  // opencode-sdk: same pattern as claude-tui — wait for the in-flight
2006
2045
  // fire-and-forget async to fully complete (session.idle received).
2007
2046
  // Use activity-based deadline: reset the 30-minute window on each tool
@@ -2095,7 +2134,7 @@ class TaskLoopRunner {
2095
2134
  }
2096
2135
  // Capture and persist CLI session ID so the next task can resume it
2097
2136
  if (this._workspaceRoot && activeProvider && providers_1.PROVIDERS[activeProvider]?.isCli) {
2098
- if (activeProvider === 'opencode-cli') {
2137
+ if (activeProvider === 'opencode-cli' && !opencodeCliIsPersistent(activeProvider)) {
2099
2138
  // Prefer the session ID from hooks events (fast, no subprocess);
2100
2139
  // fall back to `opencode session list` if hooks haven't fired yet.
2101
2140
  // Pass taskStartTime so we ignore stale/foreign hooks events from
@@ -2113,10 +2152,13 @@ class TaskLoopRunner {
2113
2152
  .catch(() => { });
2114
2153
  }
2115
2154
  }
2116
- else if (activeProvider === 'claude-tui') {
2155
+ else if (usesClaudeTuiClient(activeProvider)) {
2156
+ // claude-tui OR a persistent claude-cli — the StructuredClient owns the
2157
+ // real ~/.claude session uuid; persist it under the ACTIVE provider key
2158
+ // so a (re)start resumes the same session (works for command fallback too).
2117
2159
  const sid = (0, claudeTuiProvider_1.getClaudeTuiLatestSessionId)(this._workspaceRoot);
2118
2160
  if (sid) {
2119
- (0, sessionState_1.saveSessionId)(this._workspaceRoot, 'claude-tui', sid);
2161
+ (0, sessionState_1.saveSessionId)(this._workspaceRoot, activeProvider, sid);
2120
2162
  }
2121
2163
  }
2122
2164
  else if (activeProvider === 'copilot-sdk') {
@@ -2125,10 +2167,13 @@ class TaskLoopRunner {
2125
2167
  (0, sessionState_1.saveSessionId)(this._workspaceRoot, 'copilot-sdk', sid);
2126
2168
  }
2127
2169
  }
2128
- else if (activeProvider === 'opencode-sdk') {
2170
+ else if (usesOpencodeSdkServer(activeProvider)) {
2171
+ // opencode-sdk OR a persistent opencode-cli — persist the real SDK
2172
+ // session id under the ACTIVE provider key so the next turn RESUMES it
2173
+ // (never mints a fresh session — see memory opencode-session-explosion).
2129
2174
  const sid = (0, opencodeSdkProvider_1.getOpencodeSdkLatestSessionId)(this._workspaceRoot);
2130
2175
  if (sid) {
2131
- (0, sessionState_1.saveSessionId)(this._workspaceRoot, 'opencode-sdk', sid);
2176
+ (0, sessionState_1.saveSessionId)(this._workspaceRoot, activeProvider, sid);
2132
2177
  }
2133
2178
  }
2134
2179
  else {
@@ -2302,7 +2347,7 @@ class TaskLoopRunner {
2302
2347
  this._cb?.log(`🗜 Auto-compact triggered after ${compactInterval} tasks (provider: ${acProvider})`);
2303
2348
  this._notifyDiscord(`🗜 Auto-compact triggered after ${compactInterval} tasks`);
2304
2349
  try {
2305
- if (acProvider === 'claude-cli') {
2350
+ if (acProvider === 'claude-cli' && !claudeCliIsPersistent(acProvider)) {
2306
2351
  let sid = (0, sessionState_1.getSessionId)(this._workspaceRoot, 'claude-cli');
2307
2352
  if (!sid) {
2308
2353
  sid = (0, dispatcher_1.findLatestClaudeSession)(this._workspaceRoot);
@@ -2315,8 +2360,9 @@ class TaskLoopRunner {
2315
2360
  this._cb?.log('⚠️ Auto-compact: no Claude session ID found — skipping');
2316
2361
  }
2317
2362
  }
2318
- else if (acProvider === 'claude-tui') {
2319
- let sid = (0, sessionState_1.getSessionId)(this._workspaceRoot, 'claude-tui');
2363
+ else if (usesClaudeTuiClient(acProvider)) {
2364
+ // claude-tui OR a persistent claude-cli — compact the LIVE client.
2365
+ let sid = (0, sessionState_1.getSessionId)(this._workspaceRoot, acProvider);
2320
2366
  if (!sid) {
2321
2367
  sid = (0, claudeTuiProvider_1.getClaudeTuiLatestSessionId)(this._workspaceRoot);
2322
2368
  }
@@ -2328,10 +2374,10 @@ class TaskLoopRunner {
2328
2374
  this._cb?.log('⚠️ Auto-compact: no Claude TUI session ID found — skipping');
2329
2375
  }
2330
2376
  }
2331
- else if (acProvider === 'copilot-sdk') {
2377
+ else if (usesCopilotSdkSession(acProvider)) {
2332
2378
  this._cb?.log('ℹ️ Auto-compact not supported for copilot-sdk — skipping');
2333
2379
  }
2334
- else if (acProvider === 'opencode-cli') {
2380
+ else if (acProvider === 'opencode-cli' && !opencodeCliIsPersistent(acProvider)) {
2335
2381
  let sid = (0, sessionState_1.getSessionId)(this._workspaceRoot, 'opencode-cli');
2336
2382
  if (!sid) {
2337
2383
  sid = await (0, opencodeCliProvider_1.getLatestOpenCodeSessionId)(this._workspaceRoot, msg => this._cb?.log(msg));
@@ -2344,7 +2390,7 @@ class TaskLoopRunner {
2344
2390
  this._cb?.log('⚠️ Auto-compact: no OpenCode session ID found — skipping');
2345
2391
  }
2346
2392
  }
2347
- else if (acProvider === 'opencode-sdk') {
2393
+ else if (usesOpencodeSdkServer(acProvider)) {
2348
2394
  await (0, opencodeSdkProvider_1.runOpencodeSdkCompact)(this._workspaceRoot, msg => this._cb?.log(msg));
2349
2395
  this._cb?.log('🗜 Auto-compact complete (opencode-sdk)');
2350
2396
  }
@@ -2480,13 +2526,15 @@ class TaskLoopRunner {
2480
2526
  if (evictRoot) {
2481
2527
  try {
2482
2528
  const evictLog = (m) => this._cb?.log(m);
2483
- if (currentProvider === 'claude-tui') {
2529
+ // Also evict the persistent CLI variants (Phase 2) — they hold the
2530
+ // same live sibling process, which is likewise clutching the dead token.
2531
+ if (usesClaudeTuiClient(currentProvider)) {
2484
2532
  (0, claudeTuiProvider_1.closeClaudeTuiClient)(evictRoot, evictLog);
2485
2533
  }
2486
- else if (currentProvider === 'opencode-sdk') {
2534
+ else if (usesOpencodeSdkServer(currentProvider)) {
2487
2535
  (0, opencodeSdkProvider_1.closeOpencodeSdkClient)(evictRoot, evictLog);
2488
2536
  }
2489
- else if (currentProvider === 'copilot-sdk') {
2537
+ else if (usesCopilotSdkSession(currentProvider)) {
2490
2538
  (0, copilotSdkProvider_1.closeCopilotSdkSession)(evictRoot, evictLog);
2491
2539
  }
2492
2540
  else if (currentProvider === 'grok-tui') {
@@ -2614,7 +2662,7 @@ class TaskLoopRunner {
2614
2662
  const provider = this._cb?.getActiveProvider() ?? '';
2615
2663
  this._cb?.log(`🗑 Autocompact thrashing (${provider}) — running /clear to reset session…\n${rawMsg}`);
2616
2664
  this._notifyDiscord(`🗑 **Autocompact thrashing** (${provider}) — running \`/clear\` to reset session…\n\`\`\`\n${rawMsg}\n\`\`\``);
2617
- if (provider === 'claude-cli') {
2665
+ if (provider === 'claude-cli' && !claudeCliIsPersistent(provider)) {
2618
2666
  let sessionId = (0, sessionState_1.getSessionId)(this._workspaceRoot, 'claude-cli');
2619
2667
  if (!sessionId) {
2620
2668
  sessionId = (0, dispatcher_1.findLatestClaudeSession)(this._workspaceRoot);
@@ -2634,15 +2682,16 @@ class TaskLoopRunner {
2634
2682
  this._cb?.log('⚠️ No Claude session ID found for /clear — retrying anyway');
2635
2683
  }
2636
2684
  }
2637
- else if (provider === 'claude-tui') {
2638
- let sessionId = (0, sessionState_1.getSessionId)(this._workspaceRoot, 'claude-tui');
2685
+ else if (usesClaudeTuiClient(provider)) {
2686
+ const claudeProvider = provider;
2687
+ let sessionId = (0, sessionState_1.getSessionId)(this._workspaceRoot, claudeProvider);
2639
2688
  if (!sessionId) {
2640
2689
  sessionId = (0, claudeTuiProvider_1.getClaudeTuiLatestSessionId)(this._workspaceRoot);
2641
2690
  }
2642
2691
  if (sessionId) {
2643
2692
  try {
2644
2693
  await (0, claudeTuiProvider_1.runClaudeTuiClear)(this._workspaceRoot, sessionId, msg => this._cb?.log(msg));
2645
- (0, sessionState_1.clearSessionId)(this._workspaceRoot, 'claude-tui');
2694
+ (0, sessionState_1.clearSessionId)(this._workspaceRoot, claudeProvider);
2646
2695
  this._cb?.log('🗑 /clear (TUI) complete — session reset, retrying task');
2647
2696
  this._notifyDiscord('🗑 `/clear` (TUI) complete — session reset, retrying task');
2648
2697
  }
@@ -2665,7 +2714,7 @@ class TaskLoopRunner {
2665
2714
  const provider = this._cb?.getActiveProvider() ?? '';
2666
2715
  this._cb?.log(`🗜 Context length exceeded (${provider}) — running /compact: ${rawMsg}`);
2667
2716
  this._notifyDiscord(`🗜 **Context length exceeded** (${provider}) — running \`/compact\`…\n\`\`\`\n${rawMsg}\n\`\`\``);
2668
- if (provider === 'claude-cli') {
2717
+ if (provider === 'claude-cli' && !claudeCliIsPersistent(provider)) {
2669
2718
  // Resolve a Claude session ID — prefer the saved one, else scan
2670
2719
  // the .claude/projects jsonl folder for the most recent.
2671
2720
  let sessionId = (0, sessionState_1.getSessionId)(this._workspaceRoot, 'claude-cli');
@@ -2687,8 +2736,9 @@ class TaskLoopRunner {
2687
2736
  this._cb?.log('⚠️ No Claude session ID found for compact — retrying task without compact');
2688
2737
  }
2689
2738
  }
2690
- else if (provider === 'claude-tui') {
2691
- let sessionId = (0, sessionState_1.getSessionId)(this._workspaceRoot, 'claude-tui');
2739
+ else if (usesClaudeTuiClient(provider)) {
2740
+ const claudeProvider = provider;
2741
+ let sessionId = (0, sessionState_1.getSessionId)(this._workspaceRoot, claudeProvider);
2692
2742
  if (!sessionId) {
2693
2743
  sessionId = (0, claudeTuiProvider_1.getClaudeTuiLatestSessionId)(this._workspaceRoot);
2694
2744
  }
@@ -2862,6 +2912,11 @@ class TaskLoopRunner {
2862
2912
  const iscopilotSdkProvider = this._cb?.getActiveProvider() === 'copilot-sdk';
2863
2913
  const isOpenCodeCli = this._cb?.getActiveProvider() === 'opencode-cli';
2864
2914
  const isOpencodeSdk = this._cb?.getActiveProvider() === 'opencode-sdk';
2915
+ // opencode-cli running PERSISTENT (Phase 2) shares the opencode-sdk code path:
2916
+ // its in-process server writes '0' to the exit file on session.idle, so it must
2917
+ // be handled like opencode-sdk (resolve on !busy) — NOT via the CLI exit-file
2918
+ // watcher, which would re-prompt in a loop. Command-mode opencode-cli is unaffected.
2919
+ const isOpencodeSdkLike = isOpencodeSdk || (isOpenCodeCli && (0, opencodeSdkProvider_1.opencodeCliPersistentEnabled)());
2865
2920
  // grok-cli / grok-tui / copilot-cli all tee stdout too — without these flags
2866
2921
  // the rate-limit / context-length / thrash detectors never ran for them, so
2867
2922
  // a throttled or context-blown turn was silently walked to give_up and the
@@ -2878,7 +2933,12 @@ class TaskLoopRunner {
2878
2933
  // already tails. Feed that stdout (ANSI-stripped) through a narrator so each
2879
2934
  // chunk becomes a `Notification` hook event, giving line/chunk-granularity
2880
2935
  // live output. (Claude/grok/SDK providers already stream and are untouched.)
2881
- const cliNarrator = (this._workspaceRoot && (isCopilotCli || isOpenCodeCli))
2936
+ // When copilot-cli / opencode-cli run PERSISTENT (Phase 2) their SDK sibling
2937
+ // already streams assistant text as live Notification events, so the teed-stdout
2938
+ // narrator here would DOUBLE every chunk — disable it in that mode.
2939
+ const cliNarrateCopilot = isCopilotCli && !(0, copilotSdkProvider_1.copilotCliPersistentEnabled)();
2940
+ const cliNarrateOpencode = isOpenCodeCli && !(0, opencodeSdkProvider_1.opencodeCliPersistentEnabled)();
2941
+ const cliNarrator = (this._workspaceRoot && (cliNarrateCopilot || cliNarrateOpencode))
2882
2942
  ? new liveNarration_1.LiveNarrationStreamer(this._cb.getActiveProvider() ?? 'cli', this._workspaceRoot, ev => (0, liveNarration_1.appendHookEventLine)(this._workspaceRoot, ev))
2883
2943
  : null;
2884
2944
  // Independent cursor so narration doesn't disturb the claude-cli Discord
@@ -3385,7 +3445,7 @@ class TaskLoopRunner {
3385
3445
  // exit file when session.idle fires, the poller sees it, calls onCliExit(),
3386
3446
  // which re-sends the prompt, which loops). Instead, the poller resolves this
3387
3447
  // promise directly when isOpencodeSdkBusy() becomes false (see below).
3388
- if (!isOpencodeSdk && this._workspaceRoot) {
3448
+ if (!isOpencodeSdkLike && this._workspaceRoot) {
3389
3449
  attachExitWatcher((0, sessionState_1.exitFilePath)(this._workspaceRoot, activeProvider));
3390
3450
  }
3391
3451
  // Inactivity-based check-in: track Claude JSONL byte size every 3 s.
@@ -3414,14 +3474,14 @@ class TaskLoopRunner {
3414
3474
  attachStdoutWatcher(resolvedStdoutFile);
3415
3475
  }
3416
3476
  const latestExit = (0, sessionState_1.exitFilePath)(this._workspaceRoot, activeProvider);
3417
- if (!isOpencodeSdk && latestExit !== watchedExitFile) {
3477
+ if (!isOpencodeSdkLike && latestExit !== watchedExitFile) {
3418
3478
  attachExitWatcher(latestExit);
3419
3479
  }
3420
3480
  // opencode-sdk: resolve _waitForTaskCompletion as soon as the SDK
3421
3481
  // session goes idle (isOpencodeSdkBusy false). This avoids the
3422
3482
  // CLI-style onCliExit() reminder re-prompt loop that the exit-file
3423
3483
  // mechanism would otherwise trigger.
3424
- if (isOpencodeSdk && this._workspaceRoot && !(0, opencodeSdkProvider_1.isOpencodeSdkBusy)(this._workspaceRoot)) {
3484
+ if (isOpencodeSdkLike && this._workspaceRoot && !(0, opencodeSdkProvider_1.isOpencodeSdkBusy)(this._workspaceRoot)) {
3425
3485
  check(); // one last todo-file check before resolving
3426
3486
  if (!cancelled) {
3427
3487
  cleanup();
@@ -3433,7 +3493,7 @@ class TaskLoopRunner {
3433
3493
  // events (gitignored dirs, inotify limits, fast exits before re-attach).
3434
3494
  // Read the exit file directly every tick and trigger onCliExit() if it
3435
3495
  // became non-empty without the watcher firing.
3436
- if (!isOpencodeSdk && latestExit && latestExit !== handledExitFile) {
3496
+ if (!isOpencodeSdkLike && latestExit && latestExit !== handledExitFile) {
3437
3497
  try {
3438
3498
  if (fs.readFileSync(latestExit, 'utf8').trim() !== '') {
3439
3499
  handledExitFile = latestExit;