autodev-cli 1.4.187 → 1.4.188

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,5 +1,12 @@
1
1
  import { RateLimitDetector } from '../../rateLimit';
2
2
  import type { ProviderId } from '../../providers';
3
+ export declare function grokAgentStdioSupported(): boolean;
4
+ /**
5
+ * Whether the persistent ACP path is active. Gated on grok supporting `agent
6
+ * stdio` AND the AUTODEV_GROK_PERSISTENT env override (default ON when supported;
7
+ * set to 'false'/'0'/'off' to force the legacy one-shot headless path).
8
+ */
9
+ export declare function grokAcpEnabled(): boolean;
3
10
  /** True while a grok turn is running for the given workspace root. */
4
11
  export declare function isGrokTuiBusy(root: string): boolean;
5
12
  /** Epoch-ms of the most recent streamed pane activity, or 0 if none. */
@@ -69,6 +76,24 @@ export declare function sendGrokPrompt(root: string, promptFilePath: string, std
69
76
  * to a stateless relaunch instead of hanging the loop.
70
77
  */
71
78
  export declare function sendGrokCliPrompt(root: string, promptFilePath: string, resolvedSessionId: string | undefined, stdoutFile: string, exitFile: string, log: (msg: string) => void, model?: string, showOutput?: () => void): void;
79
+ /**
80
+ * Fire-and-forget: run ONE turn through the persistent ACP session. Streams
81
+ * assistant text to stdoutFile and writes the exit code to exitFile exactly once
82
+ * at turn end — preserving the taskLoop contract byte-for-byte. The grok process
83
+ * STAYS ALIVE between turns; a crash/exit is detected and the NEXT turn respawns
84
+ * + session/load restores context. If the ACP handshake can't be established
85
+ * (older grok, or a transient spawn failure) it transparently FALLS BACK to the
86
+ * legacy one-shot headless path so nothing regresses.
87
+ */
88
+ export declare function sendGrokAcpPrompt(root: string, promptFilePath: string, resolvedSessionId: string | undefined, stdoutFile: string, exitFile: string, log: (msg: string) => void, providerId: 'grok-cli' | 'grok-tui', model?: string, showOutput?: () => void): void;
89
+ /**
90
+ * Deliver a mid-run steer to the LIVE persistent grok process (a queued
91
+ * session/prompt). Returns true only when a live ACP session accepted it; false
92
+ * lets the caller fall back to the durable TODO buffer (at-least-once).
93
+ */
94
+ export declare function steerGrokAcp(root: string, text: string, log: (msg: string) => void): boolean;
95
+ /** True if a persistent ACP session for this root is mid-turn. */
96
+ export declare function isGrokAcpBusy(root: string): boolean;
72
97
  /**
73
98
  * grok-tui: persistent — run the turn in the per-workspace tmux session so
74
99
  * grok's in-process context accumulates across tasks. Falls back to the old
@@ -70,6 +70,8 @@ var __importStar = (this && this.__importStar) || (function () {
70
70
  };
71
71
  })();
72
72
  Object.defineProperty(exports, "__esModule", { value: true });
73
+ exports.grokAgentStdioSupported = grokAgentStdioSupported;
74
+ exports.grokAcpEnabled = grokAcpEnabled;
73
75
  exports.isGrokTuiBusy = isGrokTuiBusy;
74
76
  exports.getGrokTuiLastActivity = getGrokTuiLastActivity;
75
77
  exports.forceIdleGrokTui = forceIdleGrokTui;
@@ -77,6 +79,9 @@ exports.tmuxAvailable = tmuxAvailable;
77
79
  exports.grokSessionArgsFor = grokSessionArgsFor;
78
80
  exports.sendGrokPrompt = sendGrokPrompt;
79
81
  exports.sendGrokCliPrompt = sendGrokCliPrompt;
82
+ exports.sendGrokAcpPrompt = sendGrokAcpPrompt;
83
+ exports.steerGrokAcp = steerGrokAcp;
84
+ exports.isGrokAcpBusy = isGrokAcpBusy;
80
85
  exports.decideGrokRunMode = decideGrokRunMode;
81
86
  exports.decideHeadlessGrokMode = decideHeadlessGrokMode;
82
87
  exports.resumeProbeVerdict = resumeProbeVerdict;
@@ -102,6 +107,7 @@ const authClassify_1 = require("./authClassify");
102
107
  const grokEvents_1 = require("../../core/grokEvents");
103
108
  const sessions_1 = require("./sessions");
104
109
  const redactSecrets_1 = require("../../core/redactSecrets");
110
+ const GrokAcpSession_1 = require("./GrokAcpSession");
105
111
  // ---------------------------------------------------------------------------
106
112
  // Constants
107
113
  // ---------------------------------------------------------------------------
@@ -156,6 +162,39 @@ const _resumeFailCount = (root) => _resumeFailures.get(root) ?? 0;
156
162
  const _noteResumeFailure = (root) => { _resumeFailures.set(root, _resumeFailCount(root) + 1); };
157
163
  const _noteResumeSuccess = (root) => { _resumeFailures.delete(root); };
158
164
  const _tmuxSessions = new Map();
165
+ /**
166
+ * Live PERSISTENT `grok agent stdio` ACP sessions by root (the persistent-process
167
+ * model). ONE long-lived grok per workspace serves many turns without exiting;
168
+ * resume happens only at (re)start via session/load. See GrokAcpSession.
169
+ */
170
+ const _acpSessions = new Map();
171
+ /** Cached probe: does this grok build expose `agent stdio` (the ACP transport)? */
172
+ let _grokAgentStdioSupported = null;
173
+ function grokAgentStdioSupported() {
174
+ if (_grokAgentStdioSupported !== null) {
175
+ return _grokAgentStdioSupported;
176
+ }
177
+ try {
178
+ const out = child_process.execFileSync(GROK_BIN, ['agent', '--help'], { encoding: 'utf8', timeout: 8_000, stdio: ['ignore', 'pipe', 'ignore'] });
179
+ _grokAgentStdioSupported = /\bstdio\b/.test(out);
180
+ }
181
+ catch {
182
+ _grokAgentStdioSupported = false;
183
+ }
184
+ return _grokAgentStdioSupported;
185
+ }
186
+ /**
187
+ * Whether the persistent ACP path is active. Gated on grok supporting `agent
188
+ * stdio` AND the AUTODEV_GROK_PERSISTENT env override (default ON when supported;
189
+ * set to 'false'/'0'/'off' to force the legacy one-shot headless path).
190
+ */
191
+ function grokAcpEnabled() {
192
+ const raw = String(process.env['AUTODEV_GROK_PERSISTENT'] ?? '').toLowerCase();
193
+ if (raw === 'false' || raw === '0' || raw === 'off' || raw === 'no') {
194
+ return false;
195
+ }
196
+ return grokAgentStdioSupported();
197
+ }
159
198
  // Per-root set of grok tool_call ids already emitted as PreToolUse, so a re-scan of
160
199
  // the transcript can't emit the SAME tool call twice → duplicate rows in the office
161
200
  // hook feed (observed ~2.4× per call). PostToolUse is already dedup'd by pending.delete;
@@ -2257,8 +2296,154 @@ function sendGrokPrompt(root, promptFilePath, stdoutFile, exitFile, log, opts =
2257
2296
  * to a stateless relaunch instead of hanging the loop.
2258
2297
  */
2259
2298
  function sendGrokCliPrompt(root, promptFilePath, resolvedSessionId, stdoutFile, exitFile, log, model, showOutput) {
2299
+ if (grokAcpEnabled()) {
2300
+ sendGrokAcpPrompt(root, promptFilePath, resolvedSessionId, stdoutFile, exitFile, log, 'grok-cli', model, showOutput);
2301
+ return;
2302
+ }
2260
2303
  sendGrokPersistentHeadless(root, promptFilePath, resolvedSessionId, stdoutFile, exitFile, log, 'grok-cli', model, showOutput);
2261
2304
  }
2305
+ // ---------------------------------------------------------------------------
2306
+ // PERSISTENT-PROCESS (ACP) path — one long-lived `grok agent stdio` per root.
2307
+ // ---------------------------------------------------------------------------
2308
+ /** Get (or lazily create) the persistent ACP session for a root. */
2309
+ function getOrCreateAcpSession(root, providerId, model, log, resolvedSessionId) {
2310
+ let sess = _acpSessions.get(root);
2311
+ if (sess) {
2312
+ return sess;
2313
+ }
2314
+ // Resume-on-(re)start: seed the id to session/load from the persisted id, else
2315
+ // the newest grok session on disk for this cwd (works whether that session was
2316
+ // created by a prior ACP run or the legacy headless `--continue`).
2317
+ const seedId = resolvedSessionId || (0, sessionState_1.getSessionId)(root, providerId) || (0, sessions_1.getGrokLatestSessionId)(root) || undefined;
2318
+ const hooks = {
2319
+ emitHook: (name, extra) => {
2320
+ if (name === 'Notification' && typeof extra['message'] === 'string') {
2321
+ const msg = (0, redactSecrets_1.redactSecrets)(String(extra['message'])).slice(0, 1200);
2322
+ if (!msg.trim()) {
2323
+ return;
2324
+ }
2325
+ _emitGrokHook(root, 'Notification', { ...extra, message: msg });
2326
+ }
2327
+ else {
2328
+ _emitGrokHook(root, name, (0, redactSecrets_1.redactDeep)(extra));
2329
+ }
2330
+ },
2331
+ log,
2332
+ };
2333
+ sess = new GrokAcpSession_1.GrokAcpSession({ root, model, bin: GROK_BIN, hooks, initialSessionId: seedId });
2334
+ _acpSessions.set(root, sess);
2335
+ return sess;
2336
+ }
2337
+ /**
2338
+ * Fire-and-forget: run ONE turn through the persistent ACP session. Streams
2339
+ * assistant text to stdoutFile and writes the exit code to exitFile exactly once
2340
+ * at turn end — preserving the taskLoop contract byte-for-byte. The grok process
2341
+ * STAYS ALIVE between turns; a crash/exit is detected and the NEXT turn respawns
2342
+ * + session/load restores context. If the ACP handshake can't be established
2343
+ * (older grok, or a transient spawn failure) it transparently FALLS BACK to the
2344
+ * legacy one-shot headless path so nothing regresses.
2345
+ */
2346
+ function sendGrokAcpPrompt(root, promptFilePath, resolvedSessionId, stdoutFile, exitFile, log, providerId, model, showOutput) {
2347
+ try {
2348
+ fs.writeFileSync(stdoutFile, '', 'utf8');
2349
+ }
2350
+ catch { /* ignore */ }
2351
+ try {
2352
+ fs.writeFileSync(exitFile, '', 'utf8');
2353
+ }
2354
+ catch { /* ignore */ }
2355
+ let promptText = '';
2356
+ try {
2357
+ promptText = fs.readFileSync(promptFilePath, 'utf8');
2358
+ }
2359
+ catch (err) {
2360
+ try {
2361
+ fs.appendFileSync(stdoutFile, `\n[Grok ACP: cannot read prompt file: ${err?.message ?? err}]\n`, 'utf8');
2362
+ }
2363
+ catch { /* ignore */ }
2364
+ try {
2365
+ fs.writeFileSync(exitFile, '1\n', 'utf8');
2366
+ }
2367
+ catch { /* ignore */ }
2368
+ return;
2369
+ }
2370
+ const sess = getOrCreateAcpSession(root, providerId, model, log, resolvedSessionId);
2371
+ _busyRoots.add(root);
2372
+ if (showOutput) {
2373
+ try {
2374
+ showOutput();
2375
+ }
2376
+ catch { /* ignore */ }
2377
+ }
2378
+ const sink = (text) => { try {
2379
+ fs.appendFileSync(stdoutFile, text, 'utf8');
2380
+ }
2381
+ catch { /* ignore */ } };
2382
+ void (async () => {
2383
+ try {
2384
+ await sess.ensureReady();
2385
+ }
2386
+ catch (err) {
2387
+ // ACP unavailable / handshake failed → clean fallback to the one-shot path
2388
+ // (which writes its own stdout/exit + hooks). Nothing regresses.
2389
+ log(`Grok ACP: startup failed (${err?.message ?? err}); stderr="${sess.stderrSnapshot.slice(0, 200)}" — falling back to headless one-shot`);
2390
+ try {
2391
+ sess.close();
2392
+ }
2393
+ catch { /* ignore */ }
2394
+ _acpSessions.delete(root);
2395
+ _busyRoots.delete(root);
2396
+ sendGrokPersistentHeadless(root, promptFilePath, resolvedSessionId, stdoutFile, exitFile, log, providerId, model, showOutput);
2397
+ return;
2398
+ }
2399
+ // Persist the session id so a restart can session/load it (resume-on-restart).
2400
+ if (sess.sessionId) {
2401
+ try {
2402
+ (0, sessionState_1.saveSessionId)(root, providerId, sess.sessionId);
2403
+ }
2404
+ catch { /* ignore */ }
2405
+ }
2406
+ let res;
2407
+ try {
2408
+ res = await sess.runTurn(promptText, sink);
2409
+ }
2410
+ catch (err) {
2411
+ res = { clean: false, stopReason: 'exception', error: err?.message ?? String(err) };
2412
+ }
2413
+ if (!res.clean && res.error) {
2414
+ try {
2415
+ fs.appendFileSync(stdoutFile, `\n[Grok ACP: turn ended ${res.stopReason} — ${res.error}]\n`, 'utf8');
2416
+ }
2417
+ catch { /* ignore */ }
2418
+ }
2419
+ try {
2420
+ fs.writeFileSync(exitFile, `${res.clean ? 0 : 1}\n`, 'utf8');
2421
+ }
2422
+ catch { /* ignore */ }
2423
+ _busyRoots.delete(root);
2424
+ })();
2425
+ }
2426
+ /**
2427
+ * Deliver a mid-run steer to the LIVE persistent grok process (a queued
2428
+ * session/prompt). Returns true only when a live ACP session accepted it; false
2429
+ * lets the caller fall back to the durable TODO buffer (at-least-once).
2430
+ */
2431
+ function steerGrokAcp(root, text, log) {
2432
+ const sess = _acpSessions.get(root);
2433
+ if (!sess || !sess.isAlive()) {
2434
+ return false;
2435
+ }
2436
+ const ok = sess.steer(text);
2437
+ if (ok) {
2438
+ log(`Grok ACP: steered live process (${text.length} chars) — queued to the running session`);
2439
+ }
2440
+ return ok;
2441
+ }
2442
+ /** True if a persistent ACP session for this root is mid-turn. */
2443
+ function isGrokAcpBusy(root) {
2444
+ const sess = _acpSessions.get(root);
2445
+ return !!sess && sess.isBusy();
2446
+ }
2262
2447
  /**
2263
2448
  * grok-tui: persistent — run the turn in the per-workspace tmux session so
2264
2449
  * grok's in-process context accumulates across tasks. Falls back to the old
@@ -2357,6 +2542,12 @@ function resumeProbeVerdict(args) {
2357
2542
  return 'wait';
2358
2543
  }
2359
2544
  function sendGrokTuiPrompt(root, promptFilePath, resolvedSessionId, stdoutFile, exitFile, log, model, showOutput) {
2545
+ // Preferred persistent model: ONE long-lived `grok agent stdio` (ACP). Cleaner
2546
+ // than the tmux PTY (no pane-scraping, real steering, unified with grok-cli).
2547
+ if (grokAcpEnabled()) {
2548
+ sendGrokAcpPrompt(root, promptFilePath, resolvedSessionId, stdoutFile, exitFile, log, 'grok-tui', model, showOutput);
2549
+ return;
2550
+ }
2360
2551
  const mode = decideGrokRunMode({ tmuxAvailable: tmuxAvailable(), providerId: 'grok-tui' });
2361
2552
  if (mode.useTmux) {
2362
2553
  runGrokTmuxTurn(root, promptFilePath, resolvedSessionId, stdoutFile, exitFile, log, model, showOutput);
@@ -2491,6 +2682,22 @@ async function steerGrokTui(root, text, log) {
2491
2682
  * persisted session id so a later task relaunches with --resume (context intact).
2492
2683
  */
2493
2684
  function closeGrokTuiSession(root, _log) {
2685
+ const acp = _acpSessions.get(root);
2686
+ if (acp) {
2687
+ // Reap grok's whole worker subtree (grok forks a per-turn worker child), then
2688
+ // close — SIGKILL of just the parent would orphan the worker to init.
2689
+ if (acp.pid) {
2690
+ try {
2691
+ (0, killProcessTree_1.killProcessTree)(acp.pid);
2692
+ }
2693
+ catch { /* ignore */ }
2694
+ }
2695
+ try {
2696
+ acp.close();
2697
+ }
2698
+ catch { /* ignore */ }
2699
+ _acpSessions.delete(root);
2700
+ }
2494
2701
  const child = _activeChildren.get(root);
2495
2702
  if (child) {
2496
2703
  if (child.pid) {
@@ -2522,6 +2729,19 @@ function closeGrokTuiSession(root, _log) {
2522
2729
  }
2523
2730
  /** Kill all live grok sessions — called on SDK/extension shutdown. */
2524
2731
  function closeAllGrokTuiSessions() {
2732
+ for (const acp of _acpSessions.values()) {
2733
+ if (acp.pid) {
2734
+ try {
2735
+ (0, killProcessTree_1.killProcessTree)(acp.pid);
2736
+ }
2737
+ catch { /* ignore */ }
2738
+ }
2739
+ try {
2740
+ acp.close();
2741
+ }
2742
+ catch { /* ignore */ }
2743
+ }
2744
+ _acpSessions.clear();
2525
2745
  for (const child of _activeChildren.values()) {
2526
2746
  if (child.pid) {
2527
2747
  (0, killProcessTree_1.killProcessTree)(child.pid);