autodev-cli 1.4.186 → 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.
- package/out/providers/grok/GrokAcpSession.d.ts +122 -0
- package/out/providers/grok/GrokAcpSession.js +506 -0
- package/out/providers/grok/GrokAcpSession.js.map +1 -0
- package/out/providers/grok/tui.d.ts +70 -13
- package/out/providers/grok/tui.js +388 -40
- package/out/providers/grok/tui.js.map +1 -1
- package/out/taskLoop.js +10 -0
- package/out/taskLoop.js.map +1 -1
- package/package.json +2 -2
|
@@ -70,12 +70,18 @@ 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;
|
|
76
78
|
exports.tmuxAvailable = tmuxAvailable;
|
|
79
|
+
exports.grokSessionArgsFor = grokSessionArgsFor;
|
|
77
80
|
exports.sendGrokPrompt = sendGrokPrompt;
|
|
78
81
|
exports.sendGrokCliPrompt = sendGrokCliPrompt;
|
|
82
|
+
exports.sendGrokAcpPrompt = sendGrokAcpPrompt;
|
|
83
|
+
exports.steerGrokAcp = steerGrokAcp;
|
|
84
|
+
exports.isGrokAcpBusy = isGrokAcpBusy;
|
|
79
85
|
exports.decideGrokRunMode = decideGrokRunMode;
|
|
80
86
|
exports.decideHeadlessGrokMode = decideHeadlessGrokMode;
|
|
81
87
|
exports.resumeProbeVerdict = resumeProbeVerdict;
|
|
@@ -101,6 +107,7 @@ const authClassify_1 = require("./authClassify");
|
|
|
101
107
|
const grokEvents_1 = require("../../core/grokEvents");
|
|
102
108
|
const sessions_1 = require("./sessions");
|
|
103
109
|
const redactSecrets_1 = require("../../core/redactSecrets");
|
|
110
|
+
const GrokAcpSession_1 = require("./GrokAcpSession");
|
|
104
111
|
// ---------------------------------------------------------------------------
|
|
105
112
|
// Constants
|
|
106
113
|
// ---------------------------------------------------------------------------
|
|
@@ -155,6 +162,39 @@ const _resumeFailCount = (root) => _resumeFailures.get(root) ?? 0;
|
|
|
155
162
|
const _noteResumeFailure = (root) => { _resumeFailures.set(root, _resumeFailCount(root) + 1); };
|
|
156
163
|
const _noteResumeSuccess = (root) => { _resumeFailures.delete(root); };
|
|
157
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
|
+
}
|
|
158
198
|
// Per-root set of grok tool_call ids already emitted as PreToolUse, so a re-scan of
|
|
159
199
|
// the transcript can't emit the SAME tool call twice → duplicate rows in the office
|
|
160
200
|
// hook feed (observed ~2.4× per call). PostToolUse is already dedup'd by pending.delete;
|
|
@@ -1510,6 +1550,29 @@ function runGrokTmuxTurn(root, promptFilePath, resolvedSessionId, stdoutFile, ex
|
|
|
1510
1550
|
}
|
|
1511
1551
|
})();
|
|
1512
1552
|
}
|
|
1553
|
+
/**
|
|
1554
|
+
* Pure builder for the grok session CLI flags, extracted so the mutually-exclusive
|
|
1555
|
+
* invariants are unit-testable without spawning grok:
|
|
1556
|
+
* - continueSession → `['--continue']` ONLY. NEVER with `--session-id` (grok rejects
|
|
1557
|
+
* that combo unless `--fork-session`, which we never pass) and never an explicit id.
|
|
1558
|
+
* - sessionId → `['--resume', <id>]` (legacy explicit-id resume; not used by the
|
|
1559
|
+
* headless path anymore, kept for callers that pass a concrete id).
|
|
1560
|
+
* - newSessionId → `['--session-id', <uuid>]` (bootstrap a brand-new named session).
|
|
1561
|
+
* - none → `[]` (stateless: grok mints its own throwaway id).
|
|
1562
|
+
* continueSession takes precedence, so `--continue` can never be emitted alongside an id.
|
|
1563
|
+
*/
|
|
1564
|
+
function grokSessionArgsFor(args) {
|
|
1565
|
+
if (args.continueSession) {
|
|
1566
|
+
return ['--continue'];
|
|
1567
|
+
}
|
|
1568
|
+
if (args.sessionId) {
|
|
1569
|
+
return ['--resume', args.sessionId];
|
|
1570
|
+
}
|
|
1571
|
+
if (args.newSessionId) {
|
|
1572
|
+
return ['--session-id', args.newSessionId];
|
|
1573
|
+
}
|
|
1574
|
+
return [];
|
|
1575
|
+
}
|
|
1513
1576
|
function sendGrokPrompt(root, promptFilePath, stdoutFile, exitFile, log, opts = {}) {
|
|
1514
1577
|
opts.showOutput?.();
|
|
1515
1578
|
const modelArgs = opts.model ? ['-m', opts.model] : [];
|
|
@@ -1525,10 +1588,34 @@ function sendGrokPrompt(root, promptFilePath, stdoutFile, exitFile, log, opts =
|
|
|
1525
1588
|
// read from the start = an instant false failure). This is the primary fix for
|
|
1526
1589
|
// the 1.4.181 forced-resume regression.
|
|
1527
1590
|
let _launchSid;
|
|
1591
|
+
// For the `--continue` path the concrete id is not known up front — grok appends to
|
|
1592
|
+
// the cwd's most-recent session dir — so we pin the tailer to that EXISTING newest
|
|
1593
|
+
// dir (resolved at launch, before grok appends). Kept separate from _launchSid (which
|
|
1594
|
+
// means "we minted/passed this exact id"), since --continue passes no id at all.
|
|
1595
|
+
let _continueDir;
|
|
1528
1596
|
if (opts.persist) {
|
|
1529
1597
|
const providerId = opts.providerId ?? 'grok-tui';
|
|
1530
|
-
if (opts.
|
|
1531
|
-
|
|
1598
|
+
if (opts.continueSession) {
|
|
1599
|
+
// `grok --continue`: resume the folder's most-recent session. No id, and NO
|
|
1600
|
+
// --session-id (grok rejects that combo unless --fork-session, which we never
|
|
1601
|
+
// want). Robust to the stale-id / cwd-tracking-drift that made `--resume <id>`
|
|
1602
|
+
// error in ~1s on Windows and forced every turn stateless.
|
|
1603
|
+
sessionArgs.push(...grokSessionArgsFor({ continueSession: true }));
|
|
1604
|
+
_continueDir = (0, sessions_1.newestGrokSessionDir)(path.resolve(root));
|
|
1605
|
+
// Best-effort: stamp the resolved id into session-state.json so the sessions UI
|
|
1606
|
+
// / resume-aware profile reminder can still name the session. The continue
|
|
1607
|
+
// DECISION no longer depends on a stored id (it keys off folder-session presence).
|
|
1608
|
+
try {
|
|
1609
|
+
const resumedId = _continueDir ? path.basename(_continueDir) : undefined;
|
|
1610
|
+
if (resumedId) {
|
|
1611
|
+
(0, sessionState_1.saveSessionId)(root, providerId, resumedId);
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
catch { /* best effort */ }
|
|
1615
|
+
log(`Grok: continuing most-recent session for cwd (--continue) (model=${modelLabel})`);
|
|
1616
|
+
}
|
|
1617
|
+
else if (opts.sessionId) {
|
|
1618
|
+
sessionArgs.push(...grokSessionArgsFor({ sessionId: opts.sessionId }));
|
|
1532
1619
|
_launchSid = opts.sessionId;
|
|
1533
1620
|
// Persist the id we're resuming so the NEXT turn reads it from
|
|
1534
1621
|
// session-state.json directly — this also stamps a session that was only
|
|
@@ -1542,7 +1629,7 @@ function sendGrokPrompt(root, promptFilePath, stdoutFile, exitFile, log, opts =
|
|
|
1542
1629
|
}
|
|
1543
1630
|
else {
|
|
1544
1631
|
const sid = (0, crypto_1.randomUUID)();
|
|
1545
|
-
sessionArgs.push(
|
|
1632
|
+
sessionArgs.push(...grokSessionArgsFor({ newSessionId: sid }));
|
|
1546
1633
|
_launchSid = sid;
|
|
1547
1634
|
try {
|
|
1548
1635
|
(0, sessionState_1.saveSessionId)(root, providerId, sid);
|
|
@@ -1666,6 +1753,22 @@ function sendGrokPrompt(root, promptFilePath, stdoutFile, exitFile, log, opts =
|
|
|
1666
1753
|
let _endSeen = false;
|
|
1667
1754
|
let _forcedExit = null; // set by a session-log turn_ended finish
|
|
1668
1755
|
let _grokFilesActive = false; // once true, session logs own narration
|
|
1756
|
+
// Best diagnostic grok gave us THIS turn, so a fast-failing resume/continue is no
|
|
1757
|
+
// longer a blind guess (the user's Windows `--resume <id>` errors in ~1s but our log
|
|
1758
|
+
// never showed grok's real reason). Populated from three channels: an authoritative
|
|
1759
|
+
// streaming-json {type:"error"} record or an error turn_ended detail (_lastGrokErr),
|
|
1760
|
+
// and, as a fallback, the most recent stderr line (_lastGrokStderr). Surfaced right
|
|
1761
|
+
// before the stateless fallback in finishFromTurnEnd, redacted + truncated.
|
|
1762
|
+
let _lastGrokErr = ''; // authoritative: json error record / error turn_ended detail
|
|
1763
|
+
let _lastGrokStderr = ''; // fallback: latest non-empty stderr line
|
|
1764
|
+
const grokFailDiag = () => {
|
|
1765
|
+
const raw = _lastGrokErr || _lastGrokStderr;
|
|
1766
|
+
if (!raw) {
|
|
1767
|
+
return '';
|
|
1768
|
+
}
|
|
1769
|
+
const t = (0, redactSecrets_1.redactSecrets)(String(raw)).replace(/\s+/g, ' ').trim();
|
|
1770
|
+
return t.length > 300 ? t.slice(0, 297) + '…' : t;
|
|
1771
|
+
};
|
|
1669
1772
|
// Headless resume hang-probe state (see resumeProbeMs / onResumeHang below). The
|
|
1670
1773
|
// probe window is the caller-supplied value (sendGrokTuiPrompt reads the env);
|
|
1671
1774
|
// 0 (all stateless / non-probed calls) leaves the probe disarmed.
|
|
@@ -1700,7 +1803,10 @@ function sendGrokPrompt(root, promptFilePath, stdoutFile, exitFile, log, opts =
|
|
|
1700
1803
|
// the tailer pins THIS dir and never "newest-by-mtime" (which a newer unrelated
|
|
1701
1804
|
// session dir could hijack). undefined only in the stateless mode, where grok
|
|
1702
1805
|
// mints an unknown id and newest-by-mtime is the only way to find it.
|
|
1703
|
-
|
|
1806
|
+
// --continue → the existing newest dir grok appends to (resolved above); resume /
|
|
1807
|
+
// new-persistent → the fixed dir for the known id; stateless → undefined (unknown id).
|
|
1808
|
+
const _fixedDir = _continueDir
|
|
1809
|
+
?? (_launchSid ? (0, sessions_1.grokSessionDirFor)(path.resolve(root), _launchSid) : undefined);
|
|
1704
1810
|
// Snapshot the pre-turn session so the tailer forwards ONLY this turn's records:
|
|
1705
1811
|
// a RESUMED session's dir already holds backlog → seed at its current end; a
|
|
1706
1812
|
// FRESH session mints a NEW dir that appears later → read that dir from the start
|
|
@@ -1787,8 +1893,16 @@ function sendGrokPrompt(root, promptFilePath, stdoutFile, exitFile, log, opts =
|
|
|
1787
1893
|
// re-attempts `--resume <storedId>` — continuity resumes as soon as the transient
|
|
1788
1894
|
// condition clears. Only for a launched RESUME (opts.sessionId present, probe
|
|
1789
1895
|
// armed); a new-persistent/stateless error falls through to the normal report.
|
|
1790
|
-
if (!clean && !_sawFirstToken && opts.sessionId && opts.onResumeHang && !_resumeAbandoned) {
|
|
1896
|
+
if (!clean && !_sawFirstToken && (opts.sessionId || opts.continueSession) && opts.onResumeHang && !_resumeAbandoned) {
|
|
1791
1897
|
_resumeAbandoned = true; // close skips the exit file — the relaunch owns the turn
|
|
1898
|
+
const how = opts.continueSession ? '--continue' : '--resume';
|
|
1899
|
+
const diag = grokFailDiag();
|
|
1900
|
+
if (diag) {
|
|
1901
|
+
log(`Grok: ${how} failed fast — grok said: "${diag}"`);
|
|
1902
|
+
}
|
|
1903
|
+
else {
|
|
1904
|
+
log(`Grok: ${how} failed fast — grok emitted no diagnostic (no stderr / error record captured this turn)`);
|
|
1905
|
+
}
|
|
1792
1906
|
log('Grok: resume errored before any output (no first_token) — marking session non-resumable this run, falling back to stateless for this turn');
|
|
1793
1907
|
if (grokFilePoll) {
|
|
1794
1908
|
clearInterval(grokFilePoll);
|
|
@@ -1889,7 +2003,7 @@ function sendGrokPrompt(root, promptFilePath, stdoutFile, exitFile, log, opts =
|
|
|
1889
2003
|
// prior transient hiccup stops biasing later turns.
|
|
1890
2004
|
if (!_sawFirstToken && (0, grokEvents_1.scanGrokFirstToken)(ev.lines)) {
|
|
1891
2005
|
_sawFirstToken = true;
|
|
1892
|
-
if (opts.sessionId && opts.onResumeOk) {
|
|
2006
|
+
if ((opts.sessionId || opts.continueSession) && opts.onResumeOk) {
|
|
1893
2007
|
try {
|
|
1894
2008
|
opts.onResumeOk();
|
|
1895
2009
|
}
|
|
@@ -1927,6 +2041,28 @@ function sendGrokPrompt(root, promptFilePath, stdoutFile, exitFile, log, opts =
|
|
|
1927
2041
|
// instead of hanging to GROK_MAX_RUN_MS. An error/aborted outcome ends it too
|
|
1928
2042
|
// (retryable), honoring scanGrokTurnEnd's classification.
|
|
1929
2043
|
const te = (0, grokEvents_1.scanGrokTurnEnd)(ev.lines);
|
|
2044
|
+
// Capture grok's own error detail from an error turn_ended so the fast-fail
|
|
2045
|
+
// fallback can name WHY (session-not-found, in-use, cwd mismatch, auth, …). Only
|
|
2046
|
+
// when we don't already have an authoritative streaming-json error record.
|
|
2047
|
+
if (te === 'error' && !_lastGrokErr) {
|
|
2048
|
+
for (const l of ev.lines) {
|
|
2049
|
+
let o;
|
|
2050
|
+
try {
|
|
2051
|
+
o = typeof l === 'string' ? JSON.parse(l) : l;
|
|
2052
|
+
}
|
|
2053
|
+
catch {
|
|
2054
|
+
continue;
|
|
2055
|
+
}
|
|
2056
|
+
if (!o || o.type !== 'turn_ended') {
|
|
2057
|
+
continue;
|
|
2058
|
+
}
|
|
2059
|
+
const d = o.error?.message ?? o.error ?? o.detail ?? o.message ?? o.reason ?? o.outcome_detail;
|
|
2060
|
+
if (d) {
|
|
2061
|
+
_lastGrokErr = typeof d === 'string' ? d : JSON.stringify(d);
|
|
2062
|
+
break;
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
1930
2066
|
// Safety net against any residual offset skew: only honor a turn_ended once
|
|
1931
2067
|
// THIS turn has demonstrably started in the tailed window. A turn_ended with no
|
|
1932
2068
|
// preceding current-turn start is backlog (a prior errored turn read from a
|
|
@@ -2004,7 +2140,12 @@ function sendGrokPrompt(root, promptFilePath, stdoutFile, exitFile, log, opts =
|
|
|
2004
2140
|
// counts a TRANSIENT failure (bounded counter); it does NOT permanently disable
|
|
2005
2141
|
// resume — the next turn re-attempts `--resume`.
|
|
2006
2142
|
_resumeAbandoned = true;
|
|
2007
|
-
|
|
2143
|
+
const how = opts.continueSession ? '--continue' : '--resume';
|
|
2144
|
+
const diag = grokFailDiag();
|
|
2145
|
+
if (diag) {
|
|
2146
|
+
log(`Grok: ${how} failed — grok said: "${diag}"`);
|
|
2147
|
+
}
|
|
2148
|
+
log(`Grok: headless ${how} produced no output in ${Math.round(_probeWaitedMs / 1000)}s (${_probeRound} extension${_probeRound === 1 ? '' : 's'}) — stateless for this turn only (stored session id kept; resume retried next turn)`);
|
|
2008
2149
|
if (grokFilePoll) {
|
|
2009
2150
|
clearInterval(grokFilePoll);
|
|
2010
2151
|
}
|
|
@@ -2060,6 +2201,7 @@ function sendGrokPrompt(root, promptFilePath, stdoutFile, exitFile, log, opts =
|
|
|
2060
2201
|
}
|
|
2061
2202
|
else if (type === 'error') {
|
|
2062
2203
|
const errMsg = msg.message ?? JSON.stringify(msg);
|
|
2204
|
+
_lastGrokErr = errMsg; // authoritative diagnostic for a fast resume/continue failure
|
|
2063
2205
|
log(`Grok error: ${errMsg}`);
|
|
2064
2206
|
try {
|
|
2065
2207
|
fs.appendFileSync(stdoutFile, `\n[Grok error: ${errMsg}]\n`, 'utf8');
|
|
@@ -2080,6 +2222,7 @@ function sendGrokPrompt(root, promptFilePath, stdoutFile, exitFile, log, opts =
|
|
|
2080
2222
|
if (!text) {
|
|
2081
2223
|
return;
|
|
2082
2224
|
}
|
|
2225
|
+
_lastGrokStderr = text; // fallback diagnostic for a fast resume/continue failure
|
|
2083
2226
|
log(`Grok stderr: ${text}`);
|
|
2084
2227
|
if (GROK_MAX_TOOL_ERRORS > 0 && text.includes('tool_output_error')) {
|
|
2085
2228
|
toolErrorCount++;
|
|
@@ -2122,6 +2265,17 @@ function sendGrokPrompt(root, promptFilePath, stdoutFile, exitFile, log, opts =
|
|
|
2122
2265
|
// A session-log turn_ended already reported the true outcome via _forcedExit;
|
|
2123
2266
|
// don't let the SIGKILL exit code (1) override a clean finish.
|
|
2124
2267
|
const exitCode = _forcedExit ?? code ?? 1;
|
|
2268
|
+
// A resume/continue that FAILED FAST by exiting non-zero directly (grok printed to
|
|
2269
|
+
// stderr and quit, e.g. "Session <id> not found locally … 404 Not Found") never
|
|
2270
|
+
// wrote a session-log turn_ended, so it bypassed finishFromTurnEnd's diagnostic.
|
|
2271
|
+
// Surface grok's captured reason here too so this failure shape is not a blind guess.
|
|
2272
|
+
if (exitCode !== 0 && !_sawFirstToken && (opts.sessionId || opts.continueSession)) {
|
|
2273
|
+
const how = opts.continueSession ? '--continue' : '--resume';
|
|
2274
|
+
const diag = grokFailDiag();
|
|
2275
|
+
if (diag) {
|
|
2276
|
+
log(`Grok: ${how} failed fast (exit ${exitCode}) — grok said: "${diag}"`);
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2125
2279
|
log(`Grok: exited (code=${exitCode})`);
|
|
2126
2280
|
if (!_endSeen) {
|
|
2127
2281
|
_emitGrokHook(root, exitCode === 0 ? 'Stop' : 'StopFailure', { exit_code: exitCode });
|
|
@@ -2142,8 +2296,154 @@ function sendGrokPrompt(root, promptFilePath, stdoutFile, exitFile, log, opts =
|
|
|
2142
2296
|
* to a stateless relaunch instead of hanging the loop.
|
|
2143
2297
|
*/
|
|
2144
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
|
+
}
|
|
2145
2303
|
sendGrokPersistentHeadless(root, promptFilePath, resolvedSessionId, stdoutFile, exitFile, log, 'grok-cli', model, showOutput);
|
|
2146
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
|
+
}
|
|
2147
2447
|
/**
|
|
2148
2448
|
* grok-tui: persistent — run the turn in the per-workspace tmux session so
|
|
2149
2449
|
* grok's in-process context accumulates across tasks. Falls back to the old
|
|
@@ -2156,7 +2456,7 @@ function sendGrokCliPrompt(root, promptFilePath, resolvedSessionId, stdoutFile,
|
|
|
2156
2456
|
*
|
|
2157
2457
|
* - grok-tui + tmux → useTmux: true (persistent live pane; context accretes)
|
|
2158
2458
|
* - grok-tui + no tmux → useTmux: false (HEADLESS path — persistence handled there
|
|
2159
|
-
* via `grok --
|
|
2459
|
+
* via `grok --continue` behind the first_token hang-probe; see decideHeadlessGrokMode)
|
|
2160
2460
|
* - grok-cli → useTmux: false always (never uses tmux)
|
|
2161
2461
|
*/
|
|
2162
2462
|
function decideGrokRunMode(opts) {
|
|
@@ -2168,27 +2468,32 @@ function decideGrokRunMode(opts) {
|
|
|
2168
2468
|
* unit-testable. The `maxConsecutiveFailures` guard REPLACES the old permanent
|
|
2169
2469
|
* "resumeBroken" flag that doomed a whole run to statelessness after one hiccup.
|
|
2170
2470
|
*
|
|
2171
|
-
* - no
|
|
2172
|
-
*
|
|
2173
|
-
* (`--session-id`, mints + persists a UUID; NOT `--
|
|
2174
|
-
*
|
|
2175
|
-
* - a
|
|
2176
|
-
* `grok --
|
|
2177
|
-
*
|
|
2178
|
-
* -
|
|
2471
|
+
* - no folder session yet → 'new-session': there is no
|
|
2472
|
+
* session on disk for this cwd to continue, so bootstrap persistence with a fresh
|
|
2473
|
+
* named session (`--session-id`, mints + persists a UUID; NOT `--continue`, which
|
|
2474
|
+
* would have nothing to continue), behind the probe as a safety net.
|
|
2475
|
+
* - a folder session exists, failures < max → 'continue': resume the cwd's
|
|
2476
|
+
* most-recent session with `grok --continue` — robust to the stale-id / cwd-tracking
|
|
2477
|
+
* drift that made an explicit `--resume <id>` error in ~1s on Windows. Gated by the
|
|
2478
|
+
* first_token hang-probe. This is the STEADY STATE — every turn continues.
|
|
2479
|
+
* - a folder session, failures >= max (sustained hang) → 'stateless' for exactly ONE
|
|
2179
2480
|
* breather turn (skip the probe cost once); the caller resets the counter so the
|
|
2180
|
-
* NEXT turn returns to '
|
|
2181
|
-
*
|
|
2481
|
+
* NEXT turn returns to 'continue'. The folder session is untouched, so continuity is
|
|
2482
|
+
* NEVER permanently abandoned — persistence self-heals.
|
|
2483
|
+
*
|
|
2484
|
+
* NOTE: the decision keys off FOLDER-SESSION PRESENCE (newestGrokSessionDir exists),
|
|
2485
|
+
* NOT a stored id — because `--continue` needs no id, and keying off a stored id is
|
|
2486
|
+
* exactly what let a stale/mismatched id drive a doomed `--resume`.
|
|
2182
2487
|
*/
|
|
2183
2488
|
function decideHeadlessGrokMode(args) {
|
|
2184
|
-
if (!args.
|
|
2185
|
-
return 'new-
|
|
2489
|
+
if (!args.hasFolderSession) {
|
|
2490
|
+
return 'new-session';
|
|
2186
2491
|
}
|
|
2187
2492
|
const max = args.maxConsecutiveFailures ?? RESUME_MAX_CONSEC_FAILURES;
|
|
2188
2493
|
if (args.consecutiveResumeFailures >= max) {
|
|
2189
2494
|
return 'stateless';
|
|
2190
2495
|
}
|
|
2191
|
-
return '
|
|
2496
|
+
return 'continue';
|
|
2192
2497
|
}
|
|
2193
2498
|
/**
|
|
2194
2499
|
* Pure verdict for the headless resume hang-probe (multi-round exponential, 1.4.185).
|
|
@@ -2237,6 +2542,12 @@ function resumeProbeVerdict(args) {
|
|
|
2237
2542
|
return 'wait';
|
|
2238
2543
|
}
|
|
2239
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
|
+
}
|
|
2240
2551
|
const mode = decideGrokRunMode({ tmuxAvailable: tmuxAvailable(), providerId: 'grok-tui' });
|
|
2241
2552
|
if (mode.useTmux) {
|
|
2242
2553
|
runGrokTmuxTurn(root, promptFilePath, resolvedSessionId, stdoutFile, exitFile, log, model, showOutput);
|
|
@@ -2247,14 +2558,16 @@ function sendGrokTuiPrompt(root, promptFilePath, resolvedSessionId, stdoutFile,
|
|
|
2247
2558
|
}
|
|
2248
2559
|
/**
|
|
2249
2560
|
* The persistent HEADLESS grok launch, shared by grok-cli and headless grok-tui.
|
|
2250
|
-
* We want PERSISTENCE across turns, so
|
|
2251
|
-
*
|
|
2252
|
-
*
|
|
2253
|
-
*
|
|
2254
|
-
*
|
|
2255
|
-
*
|
|
2256
|
-
*
|
|
2257
|
-
*
|
|
2561
|
+
* We want PERSISTENCE across turns, so when the cwd already has a grok session on disk
|
|
2562
|
+
* we resume the MOST RECENT one with `grok --continue` (robust to the stale-id /
|
|
2563
|
+
* cwd-tracking drift that made an explicit `--resume <id>` error in ~1s on Windows and
|
|
2564
|
+
* forced every turn stateless). grok's headless resume can also hang a resumed turn
|
|
2565
|
+
* (loaded-but-never-answers) → the watchdog, so every persistent launch is gated behind
|
|
2566
|
+
* a FAST first_token hang-probe: if grok begins answering within
|
|
2567
|
+
* AUTODEV_GROK_RESUME_PROBE_MS the turn runs normally (persistent, context intact); if
|
|
2568
|
+
* not, sendGrokPrompt kills it and calls onResumeHang to relaunch THIS turn stateless. A
|
|
2569
|
+
* failure is COUNTED (bounded, NOT a permanent poison): the folder session is preserved
|
|
2570
|
+
* and the NEXT turn re-attempts `--continue`, so a
|
|
2258
2571
|
* transient hiccup can't strand the run in per-turn statelessness (the pre-1.4.184 bug).
|
|
2259
2572
|
* Only after RESUME_MAX_CONSEC_FAILURES straight failures does a single breather turn
|
|
2260
2573
|
* skip resume, after which resume is tried again. The stateless path stays intact as the
|
|
@@ -2264,6 +2577,11 @@ function sendGrokTuiPrompt(root, promptFilePath, resolvedSessionId, stdoutFile,
|
|
|
2264
2577
|
function sendGrokPersistentHeadless(root, promptFilePath, resolvedSessionId, stdoutFile, exitFile, log, providerId, model, showOutput) {
|
|
2265
2578
|
const label = providerId === 'grok-cli' ? 'Grok CLI' : 'Grok TUI';
|
|
2266
2579
|
const PROBE_MS = envNum('AUTODEV_GROK_RESUME_PROBE_MS', 90_000);
|
|
2580
|
+
// Continuity is decided by whether grok has a session on disk for THIS cwd — the same
|
|
2581
|
+
// signal `grok --continue` itself uses — NOT a stored id (a stale/mismatched id was the
|
|
2582
|
+
// root cause of the instant `--resume <id>` error). resolvedSessionId/getSessionId are
|
|
2583
|
+
// kept only for logging + best-effort id display.
|
|
2584
|
+
const hasFolderSession = !!(0, sessions_1.newestGrokSessionDir)(path.resolve(root));
|
|
2267
2585
|
const persistedSid = resolvedSessionId || (0, sessionState_1.getSessionId)(root, providerId);
|
|
2268
2586
|
// Stateless relaunch: grok mints a throwaway id for THIS turn. persist:false means
|
|
2269
2587
|
// it never overwrites the STORED session id in session-state.json → the next turn
|
|
@@ -2285,27 +2603,28 @@ function sendGrokPersistentHeadless(root, promptFilePath, resolvedSessionId, std
|
|
|
2285
2603
|
// Clear the failure counter the instant a resumed turn starts answering.
|
|
2286
2604
|
const onResumeOk = () => { _noteResumeSuccess(root); };
|
|
2287
2605
|
const decision = decideHeadlessGrokMode({
|
|
2288
|
-
|
|
2606
|
+
hasFolderSession, consecutiveResumeFailures: _resumeFailCount(root),
|
|
2289
2607
|
});
|
|
2290
2608
|
if (decision === 'stateless') {
|
|
2291
|
-
// Sustained
|
|
2292
|
-
// counter resets so the NEXT turn
|
|
2293
|
-
log(`${label}: --
|
|
2294
|
-
_noteResumeSuccess(root); // reset so the next turn returns to '
|
|
2609
|
+
// Sustained continue failures → ONE breather turn (skip the probe cost), then the
|
|
2610
|
+
// counter resets so the NEXT turn continues again. Persistence is never abandoned.
|
|
2611
|
+
log(`${label}: --continue failed ${_resumeFailCount(root)}× in a row — one stateless breather turn, will retry --continue next turn`);
|
|
2612
|
+
_noteResumeSuccess(root); // reset so the next turn returns to 'continue'
|
|
2295
2613
|
runStateless();
|
|
2296
2614
|
return;
|
|
2297
2615
|
}
|
|
2298
|
-
if (decision === '
|
|
2299
|
-
|
|
2616
|
+
if (decision === 'continue') {
|
|
2617
|
+
const idNote = persistedSid ? ` (${persistedSid.slice(0, 8)}…)` : '';
|
|
2618
|
+
log(`${label}: continuing most-recent session for cwd${idNote} (headless --continue)`);
|
|
2300
2619
|
sendGrokPrompt(root, promptFilePath, stdoutFile, exitFile, log, {
|
|
2301
|
-
model, showOutput, persist: true,
|
|
2620
|
+
model, showOutput, persist: true, continueSession: true, providerId,
|
|
2302
2621
|
resumeProbeMs: PROBE_MS, onResumeHang, onResumeOk,
|
|
2303
2622
|
});
|
|
2304
2623
|
return;
|
|
2305
2624
|
}
|
|
2306
|
-
// new-
|
|
2307
|
-
// have
|
|
2308
|
-
//
|
|
2625
|
+
// new-session — no folder session yet: bootstrap a fresh named session so later turns
|
|
2626
|
+
// have one to `--continue`. A fresh `--session-id` isn't a resume, but keep the probe
|
|
2627
|
+
// as a belt-and-braces safety net (and it can never exceed the watchdog).
|
|
2309
2628
|
log(`${label}: starting a new persistent session (headless)`);
|
|
2310
2629
|
sendGrokPrompt(root, promptFilePath, stdoutFile, exitFile, log, {
|
|
2311
2630
|
model, showOutput, persist: true, providerId,
|
|
@@ -2363,6 +2682,22 @@ async function steerGrokTui(root, text, log) {
|
|
|
2363
2682
|
* persisted session id so a later task relaunches with --resume (context intact).
|
|
2364
2683
|
*/
|
|
2365
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
|
+
}
|
|
2366
2701
|
const child = _activeChildren.get(root);
|
|
2367
2702
|
if (child) {
|
|
2368
2703
|
if (child.pid) {
|
|
@@ -2394,6 +2729,19 @@ function closeGrokTuiSession(root, _log) {
|
|
|
2394
2729
|
}
|
|
2395
2730
|
/** Kill all live grok sessions — called on SDK/extension shutdown. */
|
|
2396
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();
|
|
2397
2745
|
for (const child of _activeChildren.values()) {
|
|
2398
2746
|
if (child.pid) {
|
|
2399
2747
|
(0, killProcessTree_1.killProcessTree)(child.pid);
|