autodev-cli 1.4.191 → 1.4.193

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.
Files changed (45) hide show
  1. package/out/cli.js +4 -0
  2. package/out/cli.js.map +1 -1
  3. package/out/commands/grokKeepalive.d.ts +10 -0
  4. package/out/commands/grokKeepalive.js +27 -0
  5. package/out/commands/grokKeepalive.js.map +1 -0
  6. package/out/commands/leaderStop.d.ts +10 -0
  7. package/out/commands/leaderStop.js +63 -0
  8. package/out/commands/leaderStop.js.map +1 -0
  9. package/out/pendingSteers.d.ts +28 -0
  10. package/out/pendingSteers.js +145 -0
  11. package/out/pendingSteers.js.map +1 -0
  12. package/out/providers/acp/AcpSession.d.ts +31 -0
  13. package/out/providers/acp/AcpSession.js +70 -1
  14. package/out/providers/acp/AcpSession.js.map +1 -1
  15. package/out/providers/grok/GrokAcpSession.d.ts +14 -1
  16. package/out/providers/grok/GrokAcpSession.js +29 -2
  17. package/out/providers/grok/GrokAcpSession.js.map +1 -1
  18. package/out/providers/grok/keepalive.d.ts +15 -0
  19. package/out/providers/grok/keepalive.js +141 -0
  20. package/out/providers/grok/keepalive.js.map +1 -0
  21. package/out/providers/grok/keepaliveMain.d.ts +1 -0
  22. package/out/providers/grok/keepaliveMain.js +22 -0
  23. package/out/providers/grok/keepaliveMain.js.map +1 -0
  24. package/out/providers/grok/leader.d.ts +66 -0
  25. package/out/providers/grok/leader.js +390 -0
  26. package/out/providers/grok/leader.js.map +1 -0
  27. package/out/providers/grok/tui.d.ts +18 -0
  28. package/out/providers/grok/tui.js +187 -28
  29. package/out/providers/grok/tui.js.map +1 -1
  30. package/out/providers/opencode/acp.d.ts +13 -1
  31. package/out/providers/opencode/acp.js +45 -0
  32. package/out/providers/opencode/acp.js.map +1 -1
  33. package/out/providers/opencode/sdk.d.ts +1 -0
  34. package/out/providers/opencode/sdk.js +8 -0
  35. package/out/providers/opencode/sdk.js.map +1 -1
  36. package/out/taskLoop.d.ts +29 -0
  37. package/out/taskLoop.js +177 -2
  38. package/out/taskLoop.js.map +1 -1
  39. package/out/webSocketPoller.d.ts +6 -2
  40. package/out/webSocketPoller.js +8 -2
  41. package/out/webSocketPoller.js.map +1 -1
  42. package/out/webhookPoller.d.ts +4 -1
  43. package/out/webhookPoller.js +7 -0
  44. package/out/webhookPoller.js.map +1 -1
  45. package/package.json +2 -2
@@ -82,6 +82,9 @@ exports.sendGrokCliPrompt = sendGrokCliPrompt;
82
82
  exports.sendGrokAcpPrompt = sendGrokAcpPrompt;
83
83
  exports.steerGrokAcp = steerGrokAcp;
84
84
  exports.isGrokAcpBusy = isGrokAcpBusy;
85
+ exports.hasGrokAcpSession = hasGrokAcpSession;
86
+ exports.steerGrokAcpTracked = steerGrokAcpTracked;
87
+ exports.ensureGrokAcpSteer = ensureGrokAcpSteer;
85
88
  exports.decideGrokRunMode = decideGrokRunMode;
86
89
  exports.decideHeadlessGrokMode = decideHeadlessGrokMode;
87
90
  exports.resumeProbeVerdict = resumeProbeVerdict;
@@ -108,6 +111,8 @@ const grokEvents_1 = require("../../core/grokEvents");
108
111
  const sessions_1 = require("./sessions");
109
112
  const redactSecrets_1 = require("../../core/redactSecrets");
110
113
  const GrokAcpSession_1 = require("./GrokAcpSession");
114
+ const AcpSession_1 = require("../acp/AcpSession");
115
+ const leader_1 = require("./leader");
111
116
  // ---------------------------------------------------------------------------
112
117
  // Constants
113
118
  // ---------------------------------------------------------------------------
@@ -2375,20 +2380,10 @@ function sendGrokCliPrompt(root, promptFilePath, resolvedSessionId, stdoutFile,
2375
2380
  // ---------------------------------------------------------------------------
2376
2381
  // PERSISTENT-PROCESS (ACP) path — one long-lived `grok agent stdio` per root.
2377
2382
  // ---------------------------------------------------------------------------
2378
- /** Get (or lazily create) the persistent ACP session for a root. */
2379
- function getOrCreateAcpSession(root, providerId, model, log, resolvedSessionId) {
2380
- let sess = _acpSessions.get(root);
2381
- if (sess) {
2382
- return sess;
2383
- }
2384
- // Resume-on-(re)start: seed the id to session/load from the persisted id, else
2385
- // the newest grok session on disk for this cwd (works whether that session was
2386
- // created by a prior ACP run or the legacy headless `--continue`).
2387
- // Layer A.2: PREFER the persisted id (session-state.json) over the disk-latest
2388
- // fallback — disk-latest can resume the WRONG session (a stray/newer dir). Use
2389
- // disk-latest ONLY when there is no persisted id.
2390
- const seedId = (0, sessionState_1.getSessionId)(root, providerId) || resolvedSessionId || (0, sessions_1.getGrokLatestSessionId)(root) || undefined;
2391
- const hooks = {
2383
+ /** Redaction-aware AcpHostHooks that append to this root's hook feed. Shared by
2384
+ * the non-leader and leader ACP paths so hook emission stays in one place. */
2385
+ function makeAcpHooks(root, log) {
2386
+ return {
2392
2387
  emitHook: (name, extra) => {
2393
2388
  if (name === 'Notification' && typeof extra['message'] === 'string') {
2394
2389
  const msg = (0, redactSecrets_1.redactSecrets)(String(extra['message'])).slice(0, 1200);
@@ -2403,10 +2398,88 @@ function getOrCreateAcpSession(root, providerId, model, log, resolvedSessionId)
2403
2398
  },
2404
2399
  log,
2405
2400
  };
2406
- sess = new GrokAcpSession_1.GrokAcpSession({ root, model, bin: GROK_BIN, hooks, initialSessionId: seedId });
2401
+ }
2402
+ /** Get (or lazily create) the NON-LEADER persistent ACP session for a root. */
2403
+ function getOrCreateAcpSession(root, providerId, model, log, resolvedSessionId) {
2404
+ let sess = _acpSessions.get(root);
2405
+ if (sess) {
2406
+ return sess;
2407
+ }
2408
+ // Resume-on-(re)start: seed the id to session/load from the persisted id, else
2409
+ // the newest grok session on disk for this cwd (works whether that session was
2410
+ // created by a prior ACP run or the legacy headless `--continue`).
2411
+ // Layer A.2: PREFER the persisted id (session-state.json) over the disk-latest
2412
+ // fallback — disk-latest can resume the WRONG session (a stray/newer dir). Use
2413
+ // disk-latest ONLY when there is no persisted id.
2414
+ const seedId = (0, sessionState_1.getSessionId)(root, providerId) || resolvedSessionId || (0, sessions_1.getGrokLatestSessionId)(root) || undefined;
2415
+ sess = new GrokAcpSession_1.GrokAcpSession({ root, model, bin: GROK_BIN, hooks: makeAcpHooks(root, log), initialSessionId: seedId });
2407
2416
  _acpSessions.set(root, sess);
2408
2417
  return sess;
2409
2418
  }
2419
+ /**
2420
+ * LEADER-AWARE ensure (Layer B, 1.4.193). When AUTODEV_GROK_LEADER is ON, ensure a
2421
+ * per-agent leader + keepalive backend (orphan-sweeping/adopting/spawning as
2422
+ * needed) and return a LEADER-CLIENT ACP session that ATTACHES to the warm session
2423
+ * WITHOUT session/load. When the flag is OFF — or the leader backend can't be
2424
+ * established — this returns the existing non-leader session UNCHANGED. Async
2425
+ * because ensuring the leader spawns detached processes on a cold start.
2426
+ */
2427
+ async function ensureAcpSession(root, providerId, model, log, resolvedSessionId) {
2428
+ if ((0, leader_1.leaderModeEnabled)()) {
2429
+ const cached = _acpSessions.get(root);
2430
+ if (cached && cached.isAlive()) {
2431
+ return cached;
2432
+ }
2433
+ if (cached) {
2434
+ try {
2435
+ cached.close();
2436
+ }
2437
+ catch { /* ignore */ }
2438
+ _acpSessions.delete(root);
2439
+ }
2440
+ const info = await (0, leader_1.ensureLeader)({ root, bin: GROK_BIN, providerId, model, log });
2441
+ if (info) {
2442
+ const sess = new GrokAcpSession_1.GrokAcpSession({
2443
+ root, model, bin: GROK_BIN, hooks: makeAcpHooks(root, log),
2444
+ initialSessionId: info.sessionId, leaderSocket: info.socket, attachWithoutLoad: true,
2445
+ });
2446
+ _acpSessions.set(root, sess);
2447
+ return sess;
2448
+ }
2449
+ log('grok leader: backend unavailable — falling back to the non-leader ACP path');
2450
+ }
2451
+ return getOrCreateAcpSession(root, providerId, model, log, resolvedSessionId);
2452
+ }
2453
+ /**
2454
+ * Leader POISON-ESCAPE: the attached session id was rejected by the leader
2455
+ * ("unknown session id" — the keepalive died / a wrong id). Reap the leader
2456
+ * backend, clear the persisted id, drop the thin client — the caller then
2457
+ * re-ensures a FRESH leader with session/new.
2458
+ */
2459
+ function recoverLeaderPoison(root, providerId, log) {
2460
+ try {
2461
+ (0, leader_1.stopLeader)(root, log);
2462
+ }
2463
+ catch { /* ignore */ }
2464
+ try {
2465
+ (0, sessionState_1.clearSessionId)(root, providerId);
2466
+ }
2467
+ catch { /* ignore */ }
2468
+ const s = _acpSessions.get(root);
2469
+ if (s) {
2470
+ if (s.pid) {
2471
+ try {
2472
+ (0, killProcessTree_1.killProcessTree)(s.pid);
2473
+ }
2474
+ catch { /* ignore */ }
2475
+ }
2476
+ try {
2477
+ s.close();
2478
+ }
2479
+ catch { /* ignore */ }
2480
+ _acpSessions.delete(root);
2481
+ }
2482
+ }
2410
2483
  /**
2411
2484
  * Fire-and-forget: run ONE turn through the persistent ACP session. Streams
2412
2485
  * assistant text to stdoutFile and writes the exit code to exitFile exactly once
@@ -2440,7 +2513,6 @@ function sendGrokAcpPrompt(root, promptFilePath, resolvedSessionId, stdoutFile,
2440
2513
  catch { /* ignore */ }
2441
2514
  return;
2442
2515
  }
2443
- const sess = getOrCreateAcpSession(root, providerId, model, log, resolvedSessionId);
2444
2516
  _busyRoots.add(root);
2445
2517
  if (showOutput) {
2446
2518
  try {
@@ -2453,23 +2525,29 @@ function sendGrokAcpPrompt(root, promptFilePath, resolvedSessionId, stdoutFile,
2453
2525
  }
2454
2526
  catch { /* ignore */ } };
2455
2527
  void (async () => {
2528
+ let sess;
2456
2529
  try {
2530
+ sess = await ensureAcpSession(root, providerId, model, log, resolvedSessionId);
2457
2531
  await sess.ensureReady();
2458
2532
  }
2459
2533
  catch (err) {
2460
- const startupErr = `${err?.message ?? err} ${sess.stderrSnapshot}`;
2461
- log(`Grok ACP: startup failed (${err?.message ?? err}); stderr="${sess.stderrSnapshot.slice(0, 200)}" — falling back to headless one-shot`);
2462
- try {
2463
- sess.close();
2534
+ const cur = _acpSessions.get(root);
2535
+ const stderr = cur?.stderrSnapshot ?? '';
2536
+ const startupErr = `${err?.message ?? err} ${stderr}`;
2537
+ log(`Grok ACP: startup failed (${err?.message ?? err}); stderr="${stderr.slice(0, 200)}" — falling back to headless one-shot`);
2538
+ if (cur) {
2539
+ try {
2540
+ cur.close();
2541
+ }
2542
+ catch { /* ignore */ }
2464
2543
  }
2465
- catch { /* ignore */ }
2466
2544
  _acpSessions.delete(root);
2467
2545
  _busyRoots.delete(root);
2468
2546
  // AUTH failure at ACP startup (grok not signed in): the headless fallback
2469
2547
  // would just re-hit the same logged-out gate, so short-circuit to
2470
2548
  // reauth_required (non-retryable) instead of falling through.
2471
2549
  if ((0, authClassify_1.isGrokAuthFailure)(startupErr)) {
2472
- _emitGrokReauthHook(root, stdoutFile, log, 'ACP startup (not signed in)', providerId, sess.stderrSnapshot.slice(0, 160));
2550
+ _emitGrokReauthHook(root, stdoutFile, log, 'ACP startup (not signed in)', providerId, stderr.slice(0, 160));
2473
2551
  try {
2474
2552
  fs.writeFileSync(exitFile, '1\n', 'utf8');
2475
2553
  }
@@ -2488,12 +2566,34 @@ function sendGrokAcpPrompt(root, promptFilePath, resolvedSessionId, stdoutFile,
2488
2566
  }
2489
2567
  catch { /* ignore */ }
2490
2568
  }
2491
- let res;
2492
- try {
2493
- res = await sess.runTurn(promptText, sink);
2494
- }
2495
- catch (err) {
2496
- res = { clean: false, stopReason: 'exception', error: err?.message ?? String(err) };
2569
+ const runTurnSafe = async (s) => {
2570
+ try {
2571
+ return await s.runTurn(promptText, sink);
2572
+ }
2573
+ catch (err) {
2574
+ return { clean: false, stopReason: 'exception', error: err?.message ?? String(err) };
2575
+ }
2576
+ };
2577
+ let res = await runTurnSafe(sess);
2578
+ // Leader POISON-ESCAPE: the warm session was rejected (keepalive died / stale
2579
+ // id). Reap the leader + session/new on a FRESH leader, then retry ONCE.
2580
+ if ((0, leader_1.leaderModeEnabled)() && sess.attachMode && !res.clean && AcpSession_1.AcpSession.isUnknownSessionError(res.error)) {
2581
+ log('Grok leader: attach rejected (poison) — reaping leader, starting a fresh session, retrying once');
2582
+ recoverLeaderPoison(root, providerId, log);
2583
+ try {
2584
+ sess = await ensureAcpSession(root, providerId, model, log, undefined);
2585
+ await sess.ensureReady();
2586
+ if (sess.sessionId) {
2587
+ try {
2588
+ (0, sessionState_1.saveSessionId)(root, providerId, sess.sessionId);
2589
+ }
2590
+ catch { /* ignore */ }
2591
+ }
2592
+ res = await runTurnSafe(sess);
2593
+ }
2594
+ catch (err) {
2595
+ res = { clean: false, stopReason: 'poison-recover-failed', error: err?.message ?? String(err) };
2596
+ }
2497
2597
  }
2498
2598
  if (!res.clean && res.error) {
2499
2599
  try {
@@ -2535,6 +2635,61 @@ function isGrokAcpBusy(root) {
2535
2635
  const sess = _acpSessions.get(root);
2536
2636
  return !!sess && sess.isBusy();
2537
2637
  }
2638
+ /** True when a LIVE (alive) persistent grok ACP session is held for this root. */
2639
+ function hasGrokAcpSession(root) {
2640
+ const sess = _acpSessions.get(root);
2641
+ return !!sess && sess.isAlive();
2642
+ }
2643
+ /**
2644
+ * Durable-steer delivery to an EXISTING live grok ACP session. Returns a promise
2645
+ * that resolves when the steered turn COMPLETES (so the caller can ack + de-queue
2646
+ * only then), or null when no live session exists (the caller keeps it queued and
2647
+ * retries via {@link ensureGrokAcpSteer} at the next boundary).
2648
+ */
2649
+ function steerGrokAcpTracked(root, text, log) {
2650
+ const sess = _acpSessions.get(root);
2651
+ if (!sess || !sess.isAlive()) {
2652
+ return null;
2653
+ }
2654
+ const p = sess.steerTracked(text);
2655
+ if (p) {
2656
+ log(`Grok ACP: durable steer delivered to live session (${text.length} chars) — awaiting completion`);
2657
+ }
2658
+ return p;
2659
+ }
2660
+ /**
2661
+ * Restart-drain delivery: ENSURE a live grok ACP session (spawn + session/load if
2662
+ * none is held — e.g. right after a loop restart with an empty TODO), then deliver
2663
+ * the steer and await its completion. Returns the turn result, or null when the
2664
+ * session could not be brought up (caller keeps the steer queued for a later
2665
+ * boundary). Grok stays ALIVE between turns, so the common case just reuses the
2666
+ * live session.
2667
+ */
2668
+ async function ensureGrokAcpSteer(root, providerId, model, text, log) {
2669
+ if (!grokAcpEnabled()) {
2670
+ return null;
2671
+ }
2672
+ const sess = await ensureAcpSession(root, providerId, model, log, undefined);
2673
+ try {
2674
+ await sess.ensureReady();
2675
+ }
2676
+ catch (err) {
2677
+ log(`Grok ACP: durable-steer ensureReady failed (${err?.message ?? err})`);
2678
+ return null;
2679
+ }
2680
+ if (sess.sessionId) {
2681
+ try {
2682
+ (0, sessionState_1.saveSessionId)(root, providerId, sess.sessionId);
2683
+ }
2684
+ catch { /* ignore */ }
2685
+ }
2686
+ const p = sess.steerTracked(text);
2687
+ if (!p) {
2688
+ return null;
2689
+ }
2690
+ log(`Grok ACP: durable steer (re)delivered on restart-drain (${text.length} chars) — awaiting completion`);
2691
+ return p;
2692
+ }
2538
2693
  /**
2539
2694
  * grok-tui: persistent — run the turn in the per-workspace tmux session so
2540
2695
  * grok's in-process context accumulates across tasks. Falls back to the old
@@ -2777,6 +2932,10 @@ function closeGrokTuiSession(root, _log) {
2777
2932
  if (acp) {
2778
2933
  // Reap grok's whole worker subtree (grok forks a per-turn worker child), then
2779
2934
  // close — SIGKILL of just the parent would orphan the worker to init.
2935
+ // LEADER-AWARE (Layer B): acp.pid here is the THIN CLIENT. The leader + keepalive
2936
+ // are DETACHED (their own process groups, not in the client's tree), so this
2937
+ // reaps ONLY the client — the leader survives the loop restart, exactly as
2938
+ // intended. The app reaps the leader itself via `autodev leader-stop <root>`.
2780
2939
  if (acp.pid) {
2781
2940
  try {
2782
2941
  (0, killProcessTree_1.killProcessTree)(acp.pid);