clisbot 0.1.54-beta.2 → 0.1.54-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.js +473 -168
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -88771,6 +88771,11 @@ import { dirname as dirname6, join as join8, sep } from "node:path";
|
|
|
88771
88771
|
// src/control/runner/runner-exit-diagnostics.ts
|
|
88772
88772
|
import { unlink as unlink2 } from "node:fs/promises";
|
|
88773
88773
|
import { dirname as dirname5, join as join7 } from "node:path";
|
|
88774
|
+
var RUNNER_EXIT_SENTINEL_PREFIX = "[clisbot] runner exited with status";
|
|
88775
|
+
var RUNNER_EXIT_LINGER_SECONDS = 8;
|
|
88776
|
+
function paneShowsRunnerExitSentinel(snapshot) {
|
|
88777
|
+
return snapshot.includes(RUNNER_EXIT_SENTINEL_PREFIX);
|
|
88778
|
+
}
|
|
88774
88779
|
function shellQuote(value) {
|
|
88775
88780
|
if (/^[a-zA-Z0-9_./:@=-]+$/.test(value)) {
|
|
88776
88781
|
return value;
|
|
@@ -88787,6 +88792,7 @@ function getRunnerExitRecordPath(stateDir, sessionName) {
|
|
|
88787
88792
|
return join7(stateDir, "runner-exits", `${sanitizeSessionName2(sessionName)}.json`);
|
|
88788
88793
|
}
|
|
88789
88794
|
function buildRunnerLaunchCommand(params) {
|
|
88795
|
+
const exitLingerSeconds = params.exitLingerSeconds ?? RUNNER_EXIT_LINGER_SECONDS;
|
|
88790
88796
|
const runnerCommand = buildCommandString(params.command, params.args);
|
|
88791
88797
|
const exitRecordPath = getRunnerExitRecordPath(params.stateDir, params.sessionName);
|
|
88792
88798
|
const exitWriterScript = [
|
|
@@ -88809,6 +88815,7 @@ function buildRunnerLaunchCommand(params) {
|
|
|
88809
88815
|
runnerCommand,
|
|
88810
88816
|
"status=$?",
|
|
88811
88817
|
`node -e ${shellQuote(exitWriterScript)} ${shellQuote(exitRecordPath)} ${shellQuote(params.sessionName)} "$status" ${shellQuote(runnerCommand)} || true`,
|
|
88818
|
+
`if [ "$status" -ne 0 ]; then echo ${shellQuote(`${RUNNER_EXIT_SENTINEL_PREFIX} `)}"$status"; sleep ${exitLingerSeconds}; fi`,
|
|
88812
88819
|
'exit "$status"'
|
|
88813
88820
|
].join("; ");
|
|
88814
88821
|
}
|
|
@@ -92148,9 +92155,8 @@ var PASTE_CONFIRM_MAX_ATTEMPTS = 3;
|
|
|
92148
92155
|
var PASTE_CAPTURE_REVALIDATE_POLL_INTERVAL_MS = 40;
|
|
92149
92156
|
var PASTE_CAPTURE_REVALIDATE_MAX_WAIT_MS = 160;
|
|
92150
92157
|
var SUBMIT_CONFIRM_POLL_INTERVAL_MS = 40;
|
|
92151
|
-
var SUBMIT_CONFIRM_MAX_WAIT_MS =
|
|
92152
|
-
var
|
|
92153
|
-
var SUBMIT_SNAPSHOT_CONFIRM_MAX_WAIT_MS = 320;
|
|
92158
|
+
var SUBMIT_CONFIRM_MAX_WAIT_MS = 600;
|
|
92159
|
+
var SUBMIT_ENTER_MAX_ATTEMPTS = 3;
|
|
92154
92160
|
var POST_STATUS_SETTLE_POLL_INTERVAL_MS = 40;
|
|
92155
92161
|
var POST_STATUS_SETTLE_QUIET_WINDOW_MS = 80;
|
|
92156
92162
|
var POST_STATUS_SETTLE_MAX_WAIT_MS = 240;
|
|
@@ -92160,9 +92166,11 @@ var TMUX_SERVER_UNAVAILABLE_PATTERN = /(?:No such file or directory|error connec
|
|
|
92160
92166
|
|
|
92161
92167
|
class TmuxBootstrapSessionLostError extends Error {
|
|
92162
92168
|
sessionName;
|
|
92163
|
-
|
|
92169
|
+
lastSnapshot;
|
|
92170
|
+
constructor(sessionName, detail, lastSnapshot = "") {
|
|
92164
92171
|
super(`tmux bootstrap lost session "${sessionName}": ${detail}`);
|
|
92165
92172
|
this.sessionName = sessionName;
|
|
92173
|
+
this.lastSnapshot = lastSnapshot;
|
|
92166
92174
|
this.name = "TmuxBootstrapSessionLostError";
|
|
92167
92175
|
}
|
|
92168
92176
|
}
|
|
@@ -92178,7 +92186,11 @@ class TmuxPasteUnconfirmedError extends Error {
|
|
|
92178
92186
|
|
|
92179
92187
|
class TmuxSubmitUnconfirmedError extends Error {
|
|
92180
92188
|
constructor() {
|
|
92181
|
-
super(
|
|
92189
|
+
super([
|
|
92190
|
+
"tmux submit was not confirmed after Enter: the pane did not change, so clisbot does not treat the prompt as truthfully submitted.",
|
|
92191
|
+
"The runner may be busy, redrawing slowly, or showing a blocking prompt.",
|
|
92192
|
+
"Check the live pane with `clisbot watch --latest --lines 100`; if your text is sitting unsubmitted there, send /nudge, otherwise resend the message."
|
|
92193
|
+
].join(" "));
|
|
92182
92194
|
this.name = "TmuxSubmitUnconfirmedError";
|
|
92183
92195
|
}
|
|
92184
92196
|
}
|
|
@@ -92215,36 +92227,97 @@ async function submitTmuxSessionInput(params) {
|
|
|
92215
92227
|
}
|
|
92216
92228
|
const preSubmitState = pasteDelivery.state;
|
|
92217
92229
|
const preSubmitSnapshot = normalizePaneText(await params.tmux.capturePane(params.sessionName, captureLines));
|
|
92218
|
-
|
|
92219
|
-
|
|
92220
|
-
|
|
92221
|
-
|
|
92222
|
-
|
|
92223
|
-
|
|
92224
|
-
|
|
92225
|
-
|
|
92226
|
-
|
|
92227
|
-
|
|
92228
|
-
|
|
92229
|
-
|
|
92230
|
-
|
|
92231
|
-
|
|
92232
|
-
|
|
92233
|
-
|
|
92234
|
-
|
|
92235
|
-
|
|
92236
|
-
|
|
92237
|
-
baselineSnapshot: preSubmitSnapshot,
|
|
92238
|
-
captureLines
|
|
92239
|
-
});
|
|
92240
|
-
if (retrySubmitted.confirmed) {
|
|
92241
|
-
return { submittedSnapshot: preSubmitSnapshot };
|
|
92230
|
+
for (let attempt = 1;attempt <= SUBMIT_ENTER_MAX_ATTEMPTS; attempt += 1) {
|
|
92231
|
+
if (attempt > 1) {
|
|
92232
|
+
logLatencyDebug("tmux-submit-enter-retry", params.timingContext, {
|
|
92233
|
+
sessionName: params.sessionName,
|
|
92234
|
+
attempt
|
|
92235
|
+
});
|
|
92236
|
+
}
|
|
92237
|
+
await params.tmux.sendKey(params.sessionName, "Enter");
|
|
92238
|
+
const outcome = await waitForSubmitSettled({
|
|
92239
|
+
tmux: params.tmux,
|
|
92240
|
+
sessionName: params.sessionName,
|
|
92241
|
+
baseline: preSubmitState,
|
|
92242
|
+
baselineSnapshot: preSubmitSnapshot,
|
|
92243
|
+
text: params.text,
|
|
92244
|
+
captureLines
|
|
92245
|
+
});
|
|
92246
|
+
if (outcome === "submitted") {
|
|
92247
|
+
return { submittedSnapshot: preSubmitSnapshot };
|
|
92248
|
+
}
|
|
92242
92249
|
}
|
|
92243
92250
|
logLatencyDebug("tmux-submit-unconfirmed", params.timingContext, {
|
|
92244
92251
|
sessionName: params.sessionName
|
|
92245
92252
|
});
|
|
92246
92253
|
throw new TmuxSubmitUnconfirmedError;
|
|
92247
92254
|
}
|
|
92255
|
+
async function waitForSubmitSettled(params) {
|
|
92256
|
+
const deadline = Date.now() + SUBMIT_CONFIRM_MAX_WAIT_MS;
|
|
92257
|
+
let sawChange = false;
|
|
92258
|
+
while (true) {
|
|
92259
|
+
let changed = sawChange;
|
|
92260
|
+
if (!changed) {
|
|
92261
|
+
const state = await params.tmux.getPaneState(params.sessionName);
|
|
92262
|
+
changed = hasPaneStateChanged(params.baseline, state);
|
|
92263
|
+
}
|
|
92264
|
+
const snapshot = normalizePaneText(await params.tmux.capturePane(params.sessionName, params.captureLines));
|
|
92265
|
+
if (!changed) {
|
|
92266
|
+
changed = snapshot !== params.baselineSnapshot;
|
|
92267
|
+
}
|
|
92268
|
+
if (changed) {
|
|
92269
|
+
sawChange = true;
|
|
92270
|
+
if (!paneShowsPendingComposerText(snapshot, params.text)) {
|
|
92271
|
+
return "submitted";
|
|
92272
|
+
}
|
|
92273
|
+
}
|
|
92274
|
+
const remainingMs = deadline - Date.now();
|
|
92275
|
+
if (remainingMs <= 0) {
|
|
92276
|
+
return sawChange ? "pending-input" : "unchanged";
|
|
92277
|
+
}
|
|
92278
|
+
await sleep(Math.min(SUBMIT_CONFIRM_POLL_INTERVAL_MS, remainingMs));
|
|
92279
|
+
}
|
|
92280
|
+
}
|
|
92281
|
+
var COMPOSER_HINT_LINE_PATTERN = /^\?\s+for shortcuts|^Type your message or @path\/to\/file|^\d+%\s+context left|^press enter to send/i;
|
|
92282
|
+
var COMPOSER_RUNNING_INDICATOR_PATTERN = /esc to interrupt/i;
|
|
92283
|
+
var COMPOSER_BORDER_LINE_PATTERN = /^[─━═╌╍╭╮╰╯┌┐└┘+|\-_=\s]+$/;
|
|
92284
|
+
var COMPOSER_TAIL_SCAN_LINES = 12;
|
|
92285
|
+
function paneShowsPendingComposerText(snapshot, text) {
|
|
92286
|
+
const lastPromptLine = collapseWhitespace(lastNonEmptyLine(text));
|
|
92287
|
+
if (!lastPromptLine) {
|
|
92288
|
+
return false;
|
|
92289
|
+
}
|
|
92290
|
+
const lines = trimBlankLines(splitNormalizedLines(snapshot));
|
|
92291
|
+
for (let index = lines.length - 1;index >= Math.max(0, lines.length - COMPOSER_TAIL_SCAN_LINES); index -= 1) {
|
|
92292
|
+
const line = (lines[index] ?? "").trim();
|
|
92293
|
+
if (!line || COMPOSER_HINT_LINE_PATTERN.test(line) || isPromptMetadataLine(line)) {
|
|
92294
|
+
continue;
|
|
92295
|
+
}
|
|
92296
|
+
if (COMPOSER_RUNNING_INDICATOR_PATTERN.test(line)) {
|
|
92297
|
+
return false;
|
|
92298
|
+
}
|
|
92299
|
+
if (COMPOSER_BORDER_LINE_PATTERN.test(line)) {
|
|
92300
|
+
continue;
|
|
92301
|
+
}
|
|
92302
|
+
const stripped = stripComposerChrome(line);
|
|
92303
|
+
if (!stripped) {
|
|
92304
|
+
return false;
|
|
92305
|
+
}
|
|
92306
|
+
return stripped === lastPromptLine;
|
|
92307
|
+
}
|
|
92308
|
+
return false;
|
|
92309
|
+
}
|
|
92310
|
+
function stripComposerChrome(line) {
|
|
92311
|
+
return collapseWhitespace(line.replace(/^[│┃]\s*/, "").replace(/\s*[│┃]$/, "").replace(/^[›❯>]+\s*/, ""));
|
|
92312
|
+
}
|
|
92313
|
+
function lastNonEmptyLine(text) {
|
|
92314
|
+
const lines = text.split(`
|
|
92315
|
+
`).map((line) => line.trim()).filter(Boolean);
|
|
92316
|
+
return lines[lines.length - 1] ?? "";
|
|
92317
|
+
}
|
|
92318
|
+
function collapseWhitespace(value) {
|
|
92319
|
+
return value.replace(/\s+/g, " ").trim();
|
|
92320
|
+
}
|
|
92248
92321
|
async function captureTmuxSessionIdentity(params) {
|
|
92249
92322
|
await acceptTmuxStartupContinuePromptIfPresent({
|
|
92250
92323
|
tmux: params.tmux,
|
|
@@ -92389,7 +92462,7 @@ async function waitForTmuxSessionBootstrap(params) {
|
|
|
92389
92462
|
continue;
|
|
92390
92463
|
}
|
|
92391
92464
|
if (isBootstrapSessionGoneError(error)) {
|
|
92392
|
-
throw buildBootstrapSessionLostError(params.sessionName, error);
|
|
92465
|
+
throw buildBootstrapSessionLostError(params.sessionName, error, lastSnapshot);
|
|
92393
92466
|
}
|
|
92394
92467
|
throw error;
|
|
92395
92468
|
}
|
|
@@ -92407,6 +92480,18 @@ async function waitForTmuxSessionBootstrap(params) {
|
|
|
92407
92480
|
await sleep(SESSION_BOOTSTRAP_POLL_INTERVAL_MS);
|
|
92408
92481
|
continue;
|
|
92409
92482
|
}
|
|
92483
|
+
if (params.resumeRejection?.detect(snapshot)) {
|
|
92484
|
+
return {
|
|
92485
|
+
status: "resume-rejected",
|
|
92486
|
+
snapshot
|
|
92487
|
+
};
|
|
92488
|
+
}
|
|
92489
|
+
if (params.exitDetection?.detect(snapshot)) {
|
|
92490
|
+
return {
|
|
92491
|
+
status: "exited",
|
|
92492
|
+
snapshot
|
|
92493
|
+
};
|
|
92494
|
+
}
|
|
92410
92495
|
for (const blocker of blockerPatterns) {
|
|
92411
92496
|
if (blocker.regex.test(snapshot)) {
|
|
92412
92497
|
return {
|
|
@@ -92457,56 +92542,6 @@ async function acceptStartupContinuePrompt(params) {
|
|
|
92457
92542
|
return;
|
|
92458
92543
|
}
|
|
92459
92544
|
}
|
|
92460
|
-
async function waitForPaneSubmitConfirmation(params) {
|
|
92461
|
-
const deadline = Date.now() + SUBMIT_CONFIRM_MAX_WAIT_MS;
|
|
92462
|
-
while (true) {
|
|
92463
|
-
const state = await params.tmux.getPaneState(params.sessionName);
|
|
92464
|
-
if (hasPaneStateChanged(params.baseline, state)) {
|
|
92465
|
-
return {
|
|
92466
|
-
confirmed: true,
|
|
92467
|
-
snapshot: normalizePaneText(await params.tmux.capturePane(params.sessionName, params.captureLines))
|
|
92468
|
-
};
|
|
92469
|
-
}
|
|
92470
|
-
const snapshotChange = await waitForPaneSubmitSnapshotConfirmation({
|
|
92471
|
-
tmux: params.tmux,
|
|
92472
|
-
sessionName: params.sessionName,
|
|
92473
|
-
baselineSnapshot: params.baselineSnapshot,
|
|
92474
|
-
captureLines: params.captureLines,
|
|
92475
|
-
maxWaitMs: Math.min(SUBMIT_SNAPSHOT_CONFIRM_MAX_WAIT_MS, Math.max(0, deadline - Date.now()))
|
|
92476
|
-
});
|
|
92477
|
-
if (snapshotChange.confirmed) {
|
|
92478
|
-
return snapshotChange;
|
|
92479
|
-
}
|
|
92480
|
-
const remainingMs = deadline - Date.now();
|
|
92481
|
-
if (remainingMs <= 0) {
|
|
92482
|
-
return {
|
|
92483
|
-
confirmed: false,
|
|
92484
|
-
snapshot: params.baselineSnapshot
|
|
92485
|
-
};
|
|
92486
|
-
}
|
|
92487
|
-
await sleep(Math.min(SUBMIT_CONFIRM_POLL_INTERVAL_MS, remainingMs));
|
|
92488
|
-
}
|
|
92489
|
-
}
|
|
92490
|
-
async function waitForPaneSubmitSnapshotConfirmation(params) {
|
|
92491
|
-
const deadline = Date.now() + params.maxWaitMs;
|
|
92492
|
-
while (true) {
|
|
92493
|
-
const snapshot = normalizePaneText(await params.tmux.capturePane(params.sessionName, params.captureLines));
|
|
92494
|
-
if (snapshot !== params.baselineSnapshot) {
|
|
92495
|
-
return {
|
|
92496
|
-
confirmed: true,
|
|
92497
|
-
snapshot
|
|
92498
|
-
};
|
|
92499
|
-
}
|
|
92500
|
-
const remainingMs = deadline - Date.now();
|
|
92501
|
-
if (remainingMs <= 0) {
|
|
92502
|
-
return {
|
|
92503
|
-
confirmed: false,
|
|
92504
|
-
snapshot: params.baselineSnapshot
|
|
92505
|
-
};
|
|
92506
|
-
}
|
|
92507
|
-
await sleep(Math.min(SUBMIT_SNAPSHOT_CONFIRM_POLL_INTERVAL_MS, remainingMs));
|
|
92508
|
-
}
|
|
92509
|
-
}
|
|
92510
92545
|
async function deliverTmuxPasteWithConfirmation(params) {
|
|
92511
92546
|
for (let attempt = 1;attempt <= PASTE_CONFIRM_MAX_ATTEMPTS; attempt += 1) {
|
|
92512
92547
|
if (attempt > 1) {
|
|
@@ -92645,9 +92680,9 @@ function isBootstrapSessionGoneError(error) {
|
|
|
92645
92680
|
const message = error instanceof Error ? error.message : String(error);
|
|
92646
92681
|
return TMUX_MISSING_SESSION_PATTERN.test(message) || TMUX_SERVER_UNAVAILABLE_PATTERN.test(message);
|
|
92647
92682
|
}
|
|
92648
|
-
function buildBootstrapSessionLostError(sessionName, error) {
|
|
92683
|
+
function buildBootstrapSessionLostError(sessionName, error, lastSnapshot = "") {
|
|
92649
92684
|
const message = error instanceof Error ? error.message : String(error);
|
|
92650
|
-
return new TmuxBootstrapSessionLostError(sessionName, message);
|
|
92685
|
+
return new TmuxBootstrapSessionLostError(sessionName, message, lastSnapshot);
|
|
92651
92686
|
}
|
|
92652
92687
|
function arePaneStatesEqual(left, right) {
|
|
92653
92688
|
return left.cursorX === right.cursorX && left.cursorY === right.cursorY && left.historySize === right.historySize;
|
|
@@ -92863,6 +92898,132 @@ function escapeRegExp(raw) {
|
|
|
92863
92898
|
return raw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
92864
92899
|
}
|
|
92865
92900
|
|
|
92901
|
+
// src/runners/resume-rejection.ts
|
|
92902
|
+
var RESUME_REJECTED_PATTERNS = [
|
|
92903
|
+
/no saved session found with id/i,
|
|
92904
|
+
/no conversation found with session id/i,
|
|
92905
|
+
/no conversation found to resume/i,
|
|
92906
|
+
/no session found with id/i,
|
|
92907
|
+
/could not find (?:a )?session (?:with )?id/i
|
|
92908
|
+
];
|
|
92909
|
+
function paneShowsResumeRejected(snapshot) {
|
|
92910
|
+
if (!snapshot) {
|
|
92911
|
+
return false;
|
|
92912
|
+
}
|
|
92913
|
+
return RESUME_REJECTED_PATTERNS.some((pattern) => pattern.test(snapshot));
|
|
92914
|
+
}
|
|
92915
|
+
|
|
92916
|
+
class RunnerResumeRejectedError extends Error {
|
|
92917
|
+
sessionName;
|
|
92918
|
+
storedSessionId;
|
|
92919
|
+
lastPane;
|
|
92920
|
+
constructor(sessionName, storedSessionId, lastPane = "") {
|
|
92921
|
+
super(`Runner session "${sessionName}" could not resume stored session id ${storedSessionId}: the runner reported that this saved session no longer exists.`);
|
|
92922
|
+
this.sessionName = sessionName;
|
|
92923
|
+
this.storedSessionId = storedSessionId;
|
|
92924
|
+
this.lastPane = lastPane;
|
|
92925
|
+
this.name = "RunnerResumeRejectedError";
|
|
92926
|
+
}
|
|
92927
|
+
}
|
|
92928
|
+
|
|
92929
|
+
// src/runners/runner-state-failures.ts
|
|
92930
|
+
var STATE_CONTENTION_PATTERNS = [
|
|
92931
|
+
/database (?:table )?is locked/i,
|
|
92932
|
+
/SQLITE_BUSY/i,
|
|
92933
|
+
/timed out waiting for state db backfill/i,
|
|
92934
|
+
/unable to open database file/i
|
|
92935
|
+
];
|
|
92936
|
+
var STATE_CORRUPTION_PATTERNS = [
|
|
92937
|
+
/file is not a database/i,
|
|
92938
|
+
/database disk image is malformed/i,
|
|
92939
|
+
/migration \d+ was previously applied but has been modified/i,
|
|
92940
|
+
/cannot access its local database/i
|
|
92941
|
+
];
|
|
92942
|
+
function paneShowsRunnerStateContention(snapshot) {
|
|
92943
|
+
if (!snapshot) {
|
|
92944
|
+
return false;
|
|
92945
|
+
}
|
|
92946
|
+
return STATE_CONTENTION_PATTERNS.some((pattern) => pattern.test(snapshot));
|
|
92947
|
+
}
|
|
92948
|
+
function paneShowsRunnerStateCorruption(snapshot) {
|
|
92949
|
+
if (!snapshot) {
|
|
92950
|
+
return false;
|
|
92951
|
+
}
|
|
92952
|
+
return STATE_CORRUPTION_PATTERNS.some((pattern) => pattern.test(snapshot));
|
|
92953
|
+
}
|
|
92954
|
+
|
|
92955
|
+
class RunnerStateContentionError extends Error {
|
|
92956
|
+
sessionName;
|
|
92957
|
+
lastPane;
|
|
92958
|
+
constructor(sessionName, lastPane = "") {
|
|
92959
|
+
super([
|
|
92960
|
+
`Runner session "${sessionName}" could not start because the runner's local state database is locked by another running runner process (shared CODEX_HOME contention).`,
|
|
92961
|
+
"Nothing was lost: the stored session id and saved conversation were preserved.",
|
|
92962
|
+
"Resend your prompt to retry once the other run settles.",
|
|
92963
|
+
"If this keeps happening, avoid starting many runner sessions at the same time or give this agent its own CODEX_HOME."
|
|
92964
|
+
].join(" "));
|
|
92965
|
+
this.sessionName = sessionName;
|
|
92966
|
+
this.lastPane = lastPane;
|
|
92967
|
+
this.name = "RunnerStateContentionError";
|
|
92968
|
+
}
|
|
92969
|
+
}
|
|
92970
|
+
|
|
92971
|
+
class RunnerStateCorruptionError extends Error {
|
|
92972
|
+
sessionName;
|
|
92973
|
+
lastPane;
|
|
92974
|
+
constructor(sessionName, lastPane = "") {
|
|
92975
|
+
super([
|
|
92976
|
+
`Runner session "${sessionName}" cannot start: the runner reported that its local state database is corrupted or version-mismatched.`,
|
|
92977
|
+
"Retrying will not help. An operator must repair the runner state in CODEX_HOME (for codex this is usually state_5.sqlite / logs_2.sqlite; see the runner's own error output).",
|
|
92978
|
+
"The stored session id was preserved for after the repair."
|
|
92979
|
+
].join(" "));
|
|
92980
|
+
this.sessionName = sessionName;
|
|
92981
|
+
this.lastPane = lastPane;
|
|
92982
|
+
this.name = "RunnerStateCorruptionError";
|
|
92983
|
+
}
|
|
92984
|
+
}
|
|
92985
|
+
|
|
92986
|
+
// src/agents/session/run-recovery.ts
|
|
92987
|
+
var MID_RUN_RECOVERY_MAX_ATTEMPTS = 2;
|
|
92988
|
+
var MID_RUN_RECOVERY_CONTINUE_PROMPT = "continue exactly where you left off";
|
|
92989
|
+
function mergeRunSnapshot(snapshotPrefix, snapshot) {
|
|
92990
|
+
return appendInteractionText(snapshotPrefix, snapshot);
|
|
92991
|
+
}
|
|
92992
|
+
function buildRunRecoveryNote(kind, params) {
|
|
92993
|
+
const storedIdDetail = params?.storedSessionId ? ` (stored session id ${params.storedSessionId})` : "";
|
|
92994
|
+
if (kind === "resume-attempt") {
|
|
92995
|
+
const attempt = params?.attempt ?? 1;
|
|
92996
|
+
const maxAttempts = params?.maxAttempts ?? MID_RUN_RECOVERY_MAX_ATTEMPTS;
|
|
92997
|
+
return `Runner session was lost. Attempting recovery ${attempt}/${maxAttempts} by reopening the same conversation context.`;
|
|
92998
|
+
}
|
|
92999
|
+
if (kind === "resume-success") {
|
|
93000
|
+
return "Recovery succeeded. Asking the runner to continue exactly where it left off.";
|
|
93001
|
+
}
|
|
93002
|
+
if (kind === "fresh-attempt") {
|
|
93003
|
+
return "The previous runner session could not be resumed. Opening a fresh runner session 2/2 without replaying your prompt.";
|
|
93004
|
+
}
|
|
93005
|
+
if (kind === "resume-failed") {
|
|
93006
|
+
return `The previous runner session could not be resumed${storedIdDetail}. The stored session id was preserved so the conversation can still be retried.`;
|
|
93007
|
+
}
|
|
93008
|
+
if (kind === "manual-new-required") {
|
|
93009
|
+
return [
|
|
93010
|
+
`The previous runner session could not be resumed, so the interrupted run stopped${storedIdDetail}.`,
|
|
93011
|
+
"clisbot preserved the stored session id instead of silently opening a new conversation, because the run was already in progress and its context may matter.",
|
|
93012
|
+
"Next steps: resend your prompt - clisbot will retry resuming this conversation and automatically fall back to a fresh one if the runner no longer has it.",
|
|
93013
|
+
"Send `/new` first if you prefer to start a clean conversation."
|
|
93014
|
+
].join(" ");
|
|
93015
|
+
}
|
|
93016
|
+
return "The previous runner session could not be resumed. clisbot opened a new fresh session, but did not replay your prompt because the prior conversation context is no longer guaranteed. Please resend the full prompt/context to continue.";
|
|
93017
|
+
}
|
|
93018
|
+
function buildResumeRejectedFreshStartNote(params) {
|
|
93019
|
+
const cause = params.reason === "rejected" ? `the runner reported that saved session ${params.storedSessionId} no longer exists` : `the runner kept exiting while resuming saved session ${params.storedSessionId}`;
|
|
93020
|
+
const inspectHint = params.resumeCommand ? ` To inspect the old session manually, try \`${params.resumeCommand}\` in the workspace terminal.` : "";
|
|
93021
|
+
return [
|
|
93022
|
+
`The previous runner conversation could not be resumed: ${cause}.`,
|
|
93023
|
+
"clisbot opened a fresh conversation and is running your prompt there; earlier conversation context is not carried over."
|
|
93024
|
+
].join(" ") + inspectHint;
|
|
93025
|
+
}
|
|
93026
|
+
|
|
92866
93027
|
// src/agents/runtime/runner-service.ts
|
|
92867
93028
|
var TMUX_MISSING_SESSION_PATTERN2 = /(?:can't find session:|no server running on )/i;
|
|
92868
93029
|
var TMUX_SERVER_UNAVAILABLE_PATTERN2 = /(?:No such file or directory|error connecting to|failed to connect to server)/i;
|
|
@@ -92873,7 +93034,6 @@ var SESSION_READY_CAPTURE_RETRY_DELAY_MS = 100;
|
|
|
92873
93034
|
var STARTUP_SESSION_ID_CAPTURE_RETRY_COUNT = 2;
|
|
92874
93035
|
var STARTUP_SESSION_ID_CAPTURE_RETRY_DELAY_MS = 500;
|
|
92875
93036
|
var SESSION_ID_CAPTURE_FAILURE_COOLDOWN_MS = 15000;
|
|
92876
|
-
var PRESERVED_SESSION_ID_RETRY_MESSAGE = "The previous runner session could not be resumed. clisbot preserved the stored session id instead of opening a new conversation automatically. Use `/new` if you want to trigger a new runner conversation, then resend the prompt.";
|
|
92877
93037
|
function summarizeSnapshot(snapshot) {
|
|
92878
93038
|
const compact = snapshot.split(`
|
|
92879
93039
|
`).map((line) => line.trim()).filter(Boolean).join(" ").slice(0, 220);
|
|
@@ -92924,15 +93084,18 @@ class RunnerService {
|
|
|
92924
93084
|
async mapSessionError(error, sessionName, action, lastSnapshot = "") {
|
|
92925
93085
|
if (isRecoverableStartupSessionLoss(error)) {
|
|
92926
93086
|
const exitRecord = await readRunnerExitRecord(this.loadedConfig.stateDir, sessionName);
|
|
93087
|
+
const paneSnapshot = lastSnapshot || (error instanceof TmuxBootstrapSessionLostError ? error.lastSnapshot : "");
|
|
92927
93088
|
console.error("runner session disappeared", {
|
|
92928
93089
|
sessionName,
|
|
92929
93090
|
action,
|
|
92930
93091
|
exitCode: exitRecord?.exitCode,
|
|
92931
93092
|
exitedAt: exitRecord?.exitedAt,
|
|
92932
93093
|
runnerCommand: exitRecord?.command,
|
|
92933
|
-
lastVisiblePane:
|
|
93094
|
+
lastVisiblePane: paneSnapshot ? summarizeSnapshot(paneSnapshot).trim() : undefined
|
|
92934
93095
|
});
|
|
92935
|
-
|
|
93096
|
+
const exitDetail = typeof exitRecord?.exitCode === "number" ? ` The runner process exited with code ${exitRecord.exitCode}.` : "";
|
|
93097
|
+
const nextStep = action === "while the prompt was running" ? `The prompt may have partially run; check ${renderCliCommand("watch --latest --lines 100", { inline: true })} before resending.` : "Your prompt was not submitted; resend it to retry.";
|
|
93098
|
+
return new Error(`Runner session "${sessionName}" disappeared ${action}.${exitDetail} ${nextStep} If this keeps happening, verify the runner CLI starts cleanly in the workspace terminal and inspect ${renderCliCommand("logs", { inline: true })}.${summarizeSnapshot(paneSnapshot)}`);
|
|
92936
93099
|
}
|
|
92937
93100
|
if (isTransientTmuxTargetError(error)) {
|
|
92938
93101
|
return new Error(`Runner session "${sessionName}" lost its tmux target ${action}. clisbot stayed alive, but this request could not continue cleanly. Retry once. If it keeps happening, inspect ${renderCliCommand("status", { inline: true })} and ${renderCliCommand("logs", { inline: true })}.${summarizeSnapshot(lastSnapshot)}`);
|
|
@@ -93030,7 +93193,7 @@ class RunnerService {
|
|
|
93030
93193
|
pollIntervalMs: capture.pollIntervalMs
|
|
93031
93194
|
});
|
|
93032
93195
|
}
|
|
93033
|
-
async retryRunnerRestartPreservingSessionId(target, resolved, remainingFreshRetries) {
|
|
93196
|
+
async retryRunnerRestartPreservingSessionId(target, resolved, remainingFreshRetries, startupNotes) {
|
|
93034
93197
|
if (remainingFreshRetries <= 0) {
|
|
93035
93198
|
return null;
|
|
93036
93199
|
}
|
|
@@ -93039,44 +93202,113 @@ class RunnerService {
|
|
|
93039
93202
|
await sleep(resolved.runner.startupRetryDelayMs);
|
|
93040
93203
|
}
|
|
93041
93204
|
return this.ensureSessionReady(target, {
|
|
93042
|
-
remainingFreshRetries: remainingFreshRetries - 1
|
|
93205
|
+
remainingFreshRetries: remainingFreshRetries - 1,
|
|
93206
|
+
startupNotes
|
|
93043
93207
|
});
|
|
93044
93208
|
}
|
|
93045
|
-
|
|
93209
|
+
classifyRunnerStartupExit(resolved, snapshot) {
|
|
93210
|
+
if (paneShowsRunnerStateCorruption(snapshot)) {
|
|
93211
|
+
return new RunnerStateCorruptionError(resolved.sessionName, snapshot);
|
|
93212
|
+
}
|
|
93213
|
+
if (paneShowsRunnerStateContention(snapshot)) {
|
|
93214
|
+
return new RunnerStateContentionError(resolved.sessionName, snapshot);
|
|
93215
|
+
}
|
|
93216
|
+
return new TmuxBootstrapSessionLostError(resolved.sessionName, "runner exited during startup", snapshot);
|
|
93217
|
+
}
|
|
93218
|
+
async retryAfterStateContention(target, resolved, remainingFreshRetries, startupNotes) {
|
|
93219
|
+
if (remainingFreshRetries <= 0) {
|
|
93220
|
+
return null;
|
|
93221
|
+
}
|
|
93222
|
+
const attempt = resolved.runner.startupRetryCount - remainingFreshRetries + 1;
|
|
93223
|
+
const backoffMs = resolved.runner.startupRetryDelayMs * attempt + Math.floor(Math.random() * 250);
|
|
93224
|
+
console.log(`clisbot runner state database contention for ${resolved.sessionName}; retrying startup with the preserved session id in ${backoffMs}ms`);
|
|
93225
|
+
await this.killRunnerAndPreserveSessionId(resolved);
|
|
93226
|
+
if (backoffMs > 0) {
|
|
93227
|
+
await sleep(backoffMs);
|
|
93228
|
+
}
|
|
93229
|
+
return this.ensureSessionReady(target, {
|
|
93230
|
+
remainingFreshRetries: remainingFreshRetries - 1,
|
|
93231
|
+
startupNotes
|
|
93232
|
+
});
|
|
93233
|
+
}
|
|
93234
|
+
async retryAfterStartupFault(target, resolved, error, remainingFreshRetries, allowFreshResumeFallback, startupNotes) {
|
|
93235
|
+
if (error instanceof RunnerStateContentionError) {
|
|
93236
|
+
return this.retryAfterStateContention(target, resolved, remainingFreshRetries, startupNotes);
|
|
93237
|
+
}
|
|
93238
|
+
if (error instanceof RunnerStateCorruptionError) {
|
|
93239
|
+
return null;
|
|
93240
|
+
}
|
|
93046
93241
|
if (allowFreshResumeFallback) {
|
|
93047
|
-
const
|
|
93048
|
-
if (
|
|
93049
|
-
return
|
|
93242
|
+
const fallback = await this.fallBackToFreshAfterRejectedResume(target, resolved, error, remainingFreshRetries, startupNotes);
|
|
93243
|
+
if (fallback) {
|
|
93244
|
+
return fallback;
|
|
93050
93245
|
}
|
|
93051
93246
|
}
|
|
93052
93247
|
if (!isRetryableFreshStartFault(error)) {
|
|
93053
93248
|
return null;
|
|
93054
93249
|
}
|
|
93055
|
-
return this.retryRunnerRestartPreservingSessionId(target, resolved, remainingFreshRetries);
|
|
93250
|
+
return this.retryRunnerRestartPreservingSessionId(target, resolved, remainingFreshRetries, startupNotes);
|
|
93056
93251
|
}
|
|
93057
|
-
async
|
|
93252
|
+
async classifyRejectedResumeStartup(resolved, error, remainingFreshRetries) {
|
|
93253
|
+
const storedSessionId = (await this.sessionMapping.get(resolved.sessionKey))?.sessionId?.trim() || "";
|
|
93254
|
+
if (!storedSessionId) {
|
|
93255
|
+
return null;
|
|
93256
|
+
}
|
|
93257
|
+
if (error instanceof RunnerResumeRejectedError) {
|
|
93258
|
+
return { storedSessionId, reason: "rejected" };
|
|
93259
|
+
}
|
|
93260
|
+
if (error instanceof TmuxBootstrapSessionLostError && paneShowsResumeRejected(error.lastSnapshot)) {
|
|
93261
|
+
return { storedSessionId, reason: "rejected" };
|
|
93262
|
+
}
|
|
93058
93263
|
if (!isRecoverableStartupSessionLoss(error)) {
|
|
93059
93264
|
return null;
|
|
93060
93265
|
}
|
|
93061
93266
|
if (resolved.runner.sessionId.resume.mode !== "command" || resolved.runner.sessionId.create.mode !== "runner") {
|
|
93062
93267
|
return null;
|
|
93063
93268
|
}
|
|
93064
|
-
|
|
93065
|
-
if (!existing?.sessionId) {
|
|
93269
|
+
if (remainingFreshRetries > 0) {
|
|
93066
93270
|
return null;
|
|
93067
93271
|
}
|
|
93068
93272
|
const exitRecord = await readRunnerExitRecord(this.loadedConfig.stateDir, resolved.sessionName);
|
|
93069
93273
|
if (!exitRecord || exitRecord.exitCode === 0) {
|
|
93070
93274
|
return null;
|
|
93071
93275
|
}
|
|
93072
|
-
|
|
93073
|
-
|
|
93276
|
+
return { storedSessionId, reason: "exit" };
|
|
93277
|
+
}
|
|
93278
|
+
async fallBackToFreshAfterRejectedResume(target, resolved, error, remainingFreshRetries, startupNotes) {
|
|
93279
|
+
const rejection = await this.classifyRejectedResumeStartup(resolved, error, remainingFreshRetries);
|
|
93280
|
+
if (!rejection) {
|
|
93281
|
+
return null;
|
|
93282
|
+
}
|
|
93283
|
+
console.log(`clisbot resume rejected for ${resolved.sessionName}; opening a fresh runner conversation`, {
|
|
93284
|
+
storedSessionId: rejection.storedSessionId,
|
|
93285
|
+
reason: rejection.reason
|
|
93286
|
+
});
|
|
93287
|
+
await this.tmux.killSession(resolved.sessionName).catch(() => {
|
|
93288
|
+
return;
|
|
93289
|
+
});
|
|
93290
|
+
await this.sessionMapping.clearActive(resolved, {
|
|
93074
93291
|
runnerCommand: resolved.runner.command
|
|
93075
93292
|
});
|
|
93076
|
-
|
|
93293
|
+
startupNotes?.push(buildResumeRejectedFreshStartNote({
|
|
93294
|
+
storedSessionId: rejection.storedSessionId,
|
|
93295
|
+
reason: rejection.reason,
|
|
93296
|
+
resumeCommand: this.renderRunnerResumeCommand(resolved, rejection.storedSessionId)
|
|
93297
|
+
}));
|
|
93298
|
+
return this.ensureSessionReady(target, {
|
|
93299
|
+
remainingFreshRetries: resolved.runner.startupRetryCount,
|
|
93300
|
+
startupNotes
|
|
93301
|
+
});
|
|
93077
93302
|
}
|
|
93078
|
-
|
|
93079
|
-
|
|
93303
|
+
renderRunnerResumeCommand(resolved, sessionId) {
|
|
93304
|
+
if (resolved.runner.sessionId.resume.mode !== "command") {
|
|
93305
|
+
return;
|
|
93306
|
+
}
|
|
93307
|
+
const launch = this.buildRunnerArgs(resolved, { sessionId, resume: true });
|
|
93308
|
+
return [launch.command, ...launch.args].join(" ");
|
|
93309
|
+
}
|
|
93310
|
+
async retryAfterStartupTimeout(target, resolved, remainingFreshRetries, startupNotes) {
|
|
93311
|
+
return this.retryRunnerRestartPreservingSessionId(target, resolved, remainingFreshRetries, startupNotes);
|
|
93080
93312
|
}
|
|
93081
93313
|
resolveRemainingFreshRetries(resolved, options) {
|
|
93082
93314
|
if (typeof options.remainingFreshRetries === "number") {
|
|
@@ -93170,21 +93402,28 @@ class RunnerService {
|
|
|
93170
93402
|
const preparedMapping = await this.sessionMapping.prepareStartup(resolved);
|
|
93171
93403
|
const serverRunning = await this.tmux.isServerRunning();
|
|
93172
93404
|
if (serverRunning && await this.tmux.hasSession(resolved.sessionName)) {
|
|
93173
|
-
|
|
93174
|
-
|
|
93175
|
-
|
|
93176
|
-
|
|
93177
|
-
|
|
93178
|
-
|
|
93179
|
-
|
|
93180
|
-
|
|
93181
|
-
|
|
93405
|
+
const lingeringExitSnapshot = await this.captureSessionSnapshot(resolved).catch(() => "");
|
|
93406
|
+
if (paneShowsRunnerExitSentinel(lingeringExitSnapshot)) {
|
|
93407
|
+
await this.tmux.killSession(resolved.sessionName).catch(() => {
|
|
93408
|
+
return;
|
|
93409
|
+
});
|
|
93410
|
+
} else {
|
|
93411
|
+
logLatencyDebug("ensure-session-ready-existing-session", timingContext, {
|
|
93412
|
+
hasStoredSessionId: Boolean(preparedMapping.storedSessionId)
|
|
93413
|
+
});
|
|
93414
|
+
try {
|
|
93415
|
+
await clearRunnerExitRecord(this.loadedConfig.stateDir, resolved.sessionName);
|
|
93416
|
+
await this.acceptStartupContinuePromptIfPresent(resolved);
|
|
93417
|
+
await this.syncActiveSessionMappingForResolvedTarget(resolved);
|
|
93418
|
+
} catch (error) {
|
|
93419
|
+
throw await this.mapSessionError(error, resolved.sessionName, "during startup");
|
|
93420
|
+
}
|
|
93421
|
+
logLatencyDebug("ensure-session-ready-complete", timingContext, {
|
|
93422
|
+
startupDelayMs: 0,
|
|
93423
|
+
reusedSession: true
|
|
93424
|
+
});
|
|
93425
|
+
return resolved;
|
|
93182
93426
|
}
|
|
93183
|
-
logLatencyDebug("ensure-session-ready-complete", timingContext, {
|
|
93184
|
-
startupDelayMs: 0,
|
|
93185
|
-
reusedSession: true
|
|
93186
|
-
});
|
|
93187
|
-
return resolved;
|
|
93188
93427
|
}
|
|
93189
93428
|
if (!resolved.session.createIfMissing) {
|
|
93190
93429
|
throw new Error(`tmux session "${resolved.sessionName}" does not exist`);
|
|
@@ -93229,24 +93468,38 @@ class RunnerService {
|
|
|
93229
93468
|
startupDelayMs: resolved.runner.startupDelayMs,
|
|
93230
93469
|
trustWorkspace: resolved.runner.trustWorkspace,
|
|
93231
93470
|
readyPattern: resolved.runner.startupReadyPattern,
|
|
93232
|
-
blockers: resolved.runner.startupBlockers
|
|
93471
|
+
blockers: resolved.runner.startupBlockers,
|
|
93472
|
+
resumeRejection: resumingExistingSession && storedOrExplicitSessionId ? { detect: paneShowsResumeRejected } : undefined,
|
|
93473
|
+
exitDetection: { detect: paneShowsRunnerExitSentinel }
|
|
93233
93474
|
});
|
|
93475
|
+
if (bootstrapResult.status === "exited") {
|
|
93476
|
+
await this.tmux.killSession(resolved.sessionName).catch(() => {
|
|
93477
|
+
return;
|
|
93478
|
+
});
|
|
93479
|
+
throw this.classifyRunnerStartupExit(resolved, bootstrapResult.snapshot);
|
|
93480
|
+
}
|
|
93481
|
+
if (bootstrapResult.status === "resume-rejected") {
|
|
93482
|
+
await this.tmux.killSession(resolved.sessionName).catch(() => {
|
|
93483
|
+
return;
|
|
93484
|
+
});
|
|
93485
|
+
throw new RunnerResumeRejectedError(resolved.sessionName, storedOrExplicitSessionId, bootstrapResult.snapshot);
|
|
93486
|
+
}
|
|
93234
93487
|
if (bootstrapResult.status === "blocked") {
|
|
93235
93488
|
await this.abortUnreadySession(resolved, bootstrapResult.message, bootstrapResult.snapshot);
|
|
93236
93489
|
}
|
|
93237
93490
|
if (bootstrapResult.status === "timeout" && resolved.runner.startupReadyPattern) {
|
|
93238
|
-
const retried = await this.retryAfterStartupTimeout(target, resolved, remainingFreshRetries);
|
|
93491
|
+
const retried = await this.retryAfterStartupTimeout(target, resolved, remainingFreshRetries, options.startupNotes);
|
|
93239
93492
|
if (retried) {
|
|
93240
93493
|
return retried;
|
|
93241
93494
|
}
|
|
93242
|
-
await this.abortUnreadySession(resolved, `Runner session "${resolved.sessionName}" did not reach the configured ready state within ${resolved.runner.startupDelayMs}ms.`, bootstrapResult.snapshot);
|
|
93495
|
+
await this.abortUnreadySession(resolved, `Runner session "${resolved.sessionName}" did not reach the configured ready state within ${resolved.runner.startupDelayMs}ms, so your prompt was not submitted. Verify that \`${resolved.runner.command}\` starts cleanly in the workspace terminal, then resend. Inspect ${renderCliCommand("runner inspect --latest --lines 120", { inline: true })} and ${renderCliCommand("logs", { inline: true })} if it keeps happening.`, bootstrapResult.snapshot);
|
|
93243
93496
|
}
|
|
93244
93497
|
await this.finalizeSessionStartup(resolved, {
|
|
93245
93498
|
storedOrExplicitSessionId,
|
|
93246
93499
|
runnerCommand: runnerLaunch.command
|
|
93247
93500
|
});
|
|
93248
93501
|
} catch (error) {
|
|
93249
|
-
const retried = await this.retryAfterStartupFault(target, resolved, error, remainingFreshRetries, options.allowFreshRetry !== false);
|
|
93502
|
+
const retried = await this.retryAfterStartupFault(target, resolved, error, remainingFreshRetries, options.allowFreshRetry !== false, options.startupNotes);
|
|
93250
93503
|
if (retried) {
|
|
93251
93504
|
return retried;
|
|
93252
93505
|
}
|
|
@@ -93309,30 +93562,65 @@ class RunnerService {
|
|
|
93309
93562
|
return normalizePaneText(await this.tmux.capturePane(resolved.sessionName, resolved.stream.captureLines));
|
|
93310
93563
|
}
|
|
93311
93564
|
async ensureRunnerReady(target, options = {}) {
|
|
93565
|
+
const startupNotes = [];
|
|
93312
93566
|
let resolved = await this.ensureSessionReady(target, {
|
|
93313
93567
|
allowFreshRetry: options.allowFreshRetryBeforePrompt,
|
|
93314
|
-
timingContext: options.timingContext
|
|
93568
|
+
timingContext: options.timingContext,
|
|
93569
|
+
startupNotes
|
|
93315
93570
|
});
|
|
93316
93571
|
try {
|
|
93317
93572
|
return {
|
|
93318
93573
|
resolved,
|
|
93319
|
-
initialSnapshot: await this.captureSessionSnapshot(resolved)
|
|
93574
|
+
initialSnapshot: await this.captureSessionSnapshot(resolved),
|
|
93575
|
+
startupNotes
|
|
93320
93576
|
};
|
|
93321
93577
|
} catch (error) {
|
|
93322
93578
|
if (options.allowFreshRetryBeforePrompt === false || !isRecoverableStartupSessionLoss(error)) {
|
|
93323
93579
|
throw await this.mapSessionError(error, resolved.sessionName, "before prompt submission", resolved.sessionName ? await this.captureSessionSnapshot(resolved).catch(() => "") : "");
|
|
93324
93580
|
}
|
|
93325
|
-
const retried = await this.retryRunnerRestartPreservingSessionId(target, resolved, resolved.runner.startupRetryCount);
|
|
93581
|
+
const retried = await this.retryRunnerRestartPreservingSessionId(target, resolved, resolved.runner.startupRetryCount, startupNotes);
|
|
93326
93582
|
if (!retried) {
|
|
93327
93583
|
throw await this.mapSessionError(error, resolved.sessionName, "before prompt submission", resolved.sessionName ? await this.captureSessionSnapshot(resolved).catch(() => "") : "");
|
|
93328
93584
|
}
|
|
93329
93585
|
resolved = retried;
|
|
93330
93586
|
return {
|
|
93331
93587
|
resolved,
|
|
93332
|
-
initialSnapshot: await this.captureSessionSnapshot(resolved)
|
|
93588
|
+
initialSnapshot: await this.captureSessionSnapshot(resolved),
|
|
93589
|
+
startupNotes
|
|
93333
93590
|
};
|
|
93334
93591
|
}
|
|
93335
93592
|
}
|
|
93593
|
+
async recaptureSessionIdAfterRun(target) {
|
|
93594
|
+
const resolved = this.resolveTarget(target);
|
|
93595
|
+
if (resolved.runner.sessionId.capture.mode !== "status-command") {
|
|
93596
|
+
return null;
|
|
93597
|
+
}
|
|
93598
|
+
const existing = await this.sessionMapping.get(resolved.sessionKey);
|
|
93599
|
+
if (existing?.sessionId) {
|
|
93600
|
+
return existing.sessionId;
|
|
93601
|
+
}
|
|
93602
|
+
if (!await this.tmux.hasSession(resolved.sessionName)) {
|
|
93603
|
+
return null;
|
|
93604
|
+
}
|
|
93605
|
+
let sessionId = null;
|
|
93606
|
+
try {
|
|
93607
|
+
sessionId = await this.captureSessionIdFromRunner(resolved);
|
|
93608
|
+
} catch (error) {
|
|
93609
|
+
console.warn(`clisbot post-run session id recapture failed for ${resolved.sessionName}`, error);
|
|
93610
|
+
this.deferSessionIdCapture(resolved.sessionKey);
|
|
93611
|
+
return null;
|
|
93612
|
+
}
|
|
93613
|
+
if (!sessionId) {
|
|
93614
|
+
this.deferSessionIdCapture(resolved.sessionKey);
|
|
93615
|
+
return null;
|
|
93616
|
+
}
|
|
93617
|
+
const recorded = await this.recordActiveSessionIdBestEffort(resolved, sessionId);
|
|
93618
|
+
if (!recorded) {
|
|
93619
|
+
return null;
|
|
93620
|
+
}
|
|
93621
|
+
console.log(`clisbot captured durable session id after run completion for ${resolved.sessionName}`);
|
|
93622
|
+
return sessionId;
|
|
93623
|
+
}
|
|
93336
93624
|
canRecoverMidRun(error) {
|
|
93337
93625
|
return isRecoverableStartupSessionLoss(error) || isTransientTmuxTargetError(error);
|
|
93338
93626
|
}
|
|
@@ -93467,7 +93755,9 @@ class RunnerService {
|
|
|
93467
93755
|
throw new Error(`${command} completed and clisbot captured session id ${sessionId}, but could not persist it. The durable session mapping was left unchanged.${details}`);
|
|
93468
93756
|
}
|
|
93469
93757
|
async killRunnerAndPreserveSessionId(resolved) {
|
|
93470
|
-
await this.tmux.killSession(resolved.sessionName)
|
|
93758
|
+
await this.tmux.killSession(resolved.sessionName).catch(() => {
|
|
93759
|
+
return;
|
|
93760
|
+
});
|
|
93471
93761
|
await this.sessionMapping.touch(resolved, {
|
|
93472
93762
|
runnerCommand: resolved.runner.command
|
|
93473
93763
|
});
|
|
@@ -93589,33 +93879,6 @@ class RunnerService {
|
|
|
93589
93879
|
}
|
|
93590
93880
|
}
|
|
93591
93881
|
|
|
93592
|
-
// src/agents/session/run-recovery.ts
|
|
93593
|
-
var MID_RUN_RECOVERY_MAX_ATTEMPTS = 2;
|
|
93594
|
-
var MID_RUN_RECOVERY_CONTINUE_PROMPT = "continue exactly where you left off";
|
|
93595
|
-
function mergeRunSnapshot(snapshotPrefix, snapshot) {
|
|
93596
|
-
return appendInteractionText(snapshotPrefix, snapshot);
|
|
93597
|
-
}
|
|
93598
|
-
function buildRunRecoveryNote(kind, params) {
|
|
93599
|
-
if (kind === "resume-attempt") {
|
|
93600
|
-
const attempt = params?.attempt ?? 1;
|
|
93601
|
-
const maxAttempts = params?.maxAttempts ?? MID_RUN_RECOVERY_MAX_ATTEMPTS;
|
|
93602
|
-
return `Runner session was lost. Attempting recovery ${attempt}/${maxAttempts} by reopening the same conversation context.`;
|
|
93603
|
-
}
|
|
93604
|
-
if (kind === "resume-success") {
|
|
93605
|
-
return "Recovery succeeded. Asking the runner to continue exactly where it left off.";
|
|
93606
|
-
}
|
|
93607
|
-
if (kind === "fresh-attempt") {
|
|
93608
|
-
return "The previous runner session could not be resumed. Opening a fresh runner session 2/2 without replaying your prompt.";
|
|
93609
|
-
}
|
|
93610
|
-
if (kind === "resume-failed") {
|
|
93611
|
-
return "The previous runner session could not be resumed. The stored session id was preserved; use `/new` to intentionally trigger a new runner conversation.";
|
|
93612
|
-
}
|
|
93613
|
-
if (kind === "manual-new-required") {
|
|
93614
|
-
return "The previous runner session could not be resumed. clisbot preserved the stored session id instead of opening a new conversation automatically. Use `/new` if you want to trigger a new runner conversation, then resend the prompt.";
|
|
93615
|
-
}
|
|
93616
|
-
return "The previous runner session could not be resumed. clisbot opened a new fresh session, but did not replay your prompt because the prior conversation context is no longer guaranteed. Please resend the full prompt/context to continue.";
|
|
93617
|
-
}
|
|
93618
|
-
|
|
93619
93882
|
// src/runners/tmux/run-monitor.ts
|
|
93620
93883
|
var FIRST_OUTPUT_POLL_INTERVAL_MS = 250;
|
|
93621
93884
|
var RUNNING_REWRITE_PREVIEW_MAX_LINES = 8;
|
|
@@ -93801,8 +94064,10 @@ function isRetryableObserverDeliveryError(error) {
|
|
|
93801
94064
|
}
|
|
93802
94065
|
function buildMissingSessionIdStartupWarning() {
|
|
93803
94066
|
return [
|
|
93804
|
-
"Runner session started, but clisbot could not capture a durable session id yet
|
|
93805
|
-
"
|
|
94067
|
+
"Runner session started, but clisbot could not capture a durable session id yet,",
|
|
94068
|
+
"so this conversation is not resumable if the runner restarts.",
|
|
94069
|
+
"clisbot keeps retrying automatically and will persist the id once the runner reports it; no action is needed.",
|
|
94070
|
+
"If this warning keeps appearing, the runner status output may have changed; check the pane with `clisbot watch --latest`."
|
|
93806
94071
|
].join(" ");
|
|
93807
94072
|
}
|
|
93808
94073
|
|
|
@@ -93929,7 +94194,7 @@ class SessionService {
|
|
|
93929
94194
|
});
|
|
93930
94195
|
try {
|
|
93931
94196
|
await this.sessionState.markPromptAdmitted(provisionalResolved);
|
|
93932
|
-
const { resolved, initialSnapshot } = await this.runnerSessions.ensureRunnerReady(target, {
|
|
94197
|
+
const { resolved, initialSnapshot, startupNotes } = await this.runnerSessions.ensureRunnerReady(target, {
|
|
93933
94198
|
...options,
|
|
93934
94199
|
timingContext: observer.timingContext
|
|
93935
94200
|
});
|
|
@@ -93960,6 +94225,17 @@ class SessionService {
|
|
|
93960
94225
|
state: "running",
|
|
93961
94226
|
startedAt
|
|
93962
94227
|
});
|
|
94228
|
+
for (const startupNote of startupNotes ?? []) {
|
|
94229
|
+
await this.notifyRunObservers(run, this.createRunUpdate({
|
|
94230
|
+
resolved,
|
|
94231
|
+
status: "running",
|
|
94232
|
+
snapshot: "",
|
|
94233
|
+
fullSnapshot: initialSnapshot,
|
|
94234
|
+
initialSnapshot,
|
|
94235
|
+
note: startupNote,
|
|
94236
|
+
forceVisible: true
|
|
94237
|
+
}));
|
|
94238
|
+
}
|
|
93963
94239
|
if (!run.sessionId) {
|
|
93964
94240
|
await this.notifyRunObservers(run, this.createRunUpdate({
|
|
93965
94241
|
resolved,
|
|
@@ -94227,6 +94503,14 @@ class SessionService {
|
|
|
94227
94503
|
if (!run) {
|
|
94228
94504
|
return;
|
|
94229
94505
|
}
|
|
94506
|
+
if (!run.sessionId) {
|
|
94507
|
+
try {
|
|
94508
|
+
run.sessionId = await this.runnerSessions.recaptureSessionIdAfterRun({
|
|
94509
|
+
agentId: run.resolved.agentId,
|
|
94510
|
+
sessionKey: run.resolved.sessionKey
|
|
94511
|
+
}) ?? undefined;
|
|
94512
|
+
} catch {}
|
|
94513
|
+
}
|
|
94230
94514
|
await this.sessionState.setSessionRuntime(run.resolved, {
|
|
94231
94515
|
state: "idle"
|
|
94232
94516
|
});
|
|
@@ -94364,8 +94648,10 @@ class SessionService {
|
|
|
94364
94648
|
return true;
|
|
94365
94649
|
}
|
|
94366
94650
|
if (await this.requiresManualNewAfterFailedResume(currentRun)) {
|
|
94367
|
-
await this.notifyRecoveryStep(currentRun, buildRunRecoveryNote("resume-failed"));
|
|
94368
|
-
await this.failActiveRun(sessionKey, currentRun.runId, new Error(buildRunRecoveryNote("manual-new-required"
|
|
94651
|
+
await this.notifyRecoveryStep(currentRun, buildRunRecoveryNote("resume-failed", { storedSessionId: currentRun.sessionId }));
|
|
94652
|
+
await this.failActiveRun(sessionKey, currentRun.runId, new Error(buildRunRecoveryNote("manual-new-required", {
|
|
94653
|
+
storedSessionId: currentRun.sessionId
|
|
94654
|
+
})));
|
|
94369
94655
|
return true;
|
|
94370
94656
|
}
|
|
94371
94657
|
await this.notifyRecoveryStep(currentRun, buildRunRecoveryNote("fresh-attempt"));
|
|
@@ -101281,6 +101567,7 @@ import { readFile as readFile3 } from "node:fs/promises";
|
|
|
101281
101567
|
|
|
101282
101568
|
// src/channels/slack/content.ts
|
|
101283
101569
|
var SLACK_MAX_BLOCKS = 50;
|
|
101570
|
+
var SLACK_BULLET_MARKERS = ["•", "◦", "▪"];
|
|
101284
101571
|
function normalizeMarkdownLinks(text) {
|
|
101285
101572
|
return text.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_match, label, href) => {
|
|
101286
101573
|
const trimmedHref = href.trim();
|
|
@@ -101293,6 +101580,20 @@ function normalizeMarkdownLinks(text) {
|
|
|
101293
101580
|
function renderInlineMarkdownToSlackMrkdwn(text) {
|
|
101294
101581
|
return normalizeMarkdownLinks(text).replace(/~~([^~]+)~~/g, "~$1~").replace(/\*\*([^*\n][\s\S]*?[^*\n])\*\*/g, "*$1*");
|
|
101295
101582
|
}
|
|
101583
|
+
function trimOuterBlankLines(text) {
|
|
101584
|
+
return text.replace(/^\n+/, "").replace(/\n+$/, "");
|
|
101585
|
+
}
|
|
101586
|
+
function getMarkdownListDepth(rawIndent) {
|
|
101587
|
+
const width = rawIndent.replaceAll("\t", " ").length;
|
|
101588
|
+
return Math.floor(width / 2);
|
|
101589
|
+
}
|
|
101590
|
+
function renderSlackListIndent(depth) {
|
|
101591
|
+
return " ".repeat(Math.min(Math.max(depth, 0), 6));
|
|
101592
|
+
}
|
|
101593
|
+
function renderSlackBulletPrefix(depth) {
|
|
101594
|
+
const marker = SLACK_BULLET_MARKERS[Math.min(depth, SLACK_BULLET_MARKERS.length - 1)] ?? SLACK_BULLET_MARKERS[0];
|
|
101595
|
+
return `${renderSlackListIndent(depth)}${marker}`;
|
|
101596
|
+
}
|
|
101296
101597
|
function stripMarkdownInline(text) {
|
|
101297
101598
|
return text.replace(/`([^`\n]+)`/g, "$1").replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, "$1").replace(/\*\*([^*\n][\s\S]*?[^*\n])\*\*/g, "$1").replace(/\*([^*\n][\s\S]*?[^*\n])\*/g, "$1").replace(/~~([^~]+)~~/g, "$1").trim();
|
|
101298
101599
|
}
|
|
@@ -101373,10 +101674,10 @@ function parseSlackBlocksInput(text) {
|
|
|
101373
101674
|
return validateSlackBlocksArray(parsed);
|
|
101374
101675
|
}
|
|
101375
101676
|
function renderMarkdownToSlackMrkdwn(markdown) {
|
|
101376
|
-
const normalized = markdown.replaceAll(`\r
|
|
101677
|
+
const normalized = trimOuterBlankLines(markdown.replaceAll(`\r
|
|
101377
101678
|
`, `
|
|
101378
101679
|
`).replaceAll("\r", `
|
|
101379
|
-
`)
|
|
101680
|
+
`));
|
|
101380
101681
|
if (!normalized) {
|
|
101381
101682
|
return "";
|
|
101382
101683
|
}
|
|
@@ -101391,13 +101692,15 @@ function renderMarkdownToSlackMrkdwn(markdown) {
|
|
|
101391
101692
|
}
|
|
101392
101693
|
return content;
|
|
101393
101694
|
}
|
|
101394
|
-
const bulletMatch = line.match(
|
|
101695
|
+
const bulletMatch = line.match(/^(\s*)[-*+]\s+(.+)$/);
|
|
101395
101696
|
if (bulletMatch) {
|
|
101396
|
-
|
|
101697
|
+
const depth = getMarkdownListDepth(bulletMatch[1] ?? "");
|
|
101698
|
+
return `${renderSlackBulletPrefix(depth)} ${renderInlineMarkdownToSlackMrkdwn(bulletMatch[2] ?? "")}`;
|
|
101397
101699
|
}
|
|
101398
|
-
const orderedMatch = line.match(
|
|
101700
|
+
const orderedMatch = line.match(/^(\s*)(\d+)\.\s+(.+)$/);
|
|
101399
101701
|
if (orderedMatch) {
|
|
101400
|
-
|
|
101702
|
+
const depth = getMarkdownListDepth(orderedMatch[1] ?? "");
|
|
101703
|
+
return `${renderSlackListIndent(depth)}${orderedMatch[2]}. ${renderInlineMarkdownToSlackMrkdwn(orderedMatch[3] ?? "")}`;
|
|
101401
101704
|
}
|
|
101402
101705
|
return renderInlineMarkdownToSlackMrkdwn(line);
|
|
101403
101706
|
}).join(`
|
|
@@ -101462,10 +101765,10 @@ function renderMarkdownTableToFallbackSlackBlock(headers, rows) {
|
|
|
101462
101765
|
};
|
|
101463
101766
|
}
|
|
101464
101767
|
function renderMarkdownToSlackBlocks(markdown) {
|
|
101465
|
-
const normalized = markdown.replaceAll(`\r
|
|
101768
|
+
const normalized = trimOuterBlankLines(markdown.replaceAll(`\r
|
|
101466
101769
|
`, `
|
|
101467
101770
|
`).replaceAll("\r", `
|
|
101468
|
-
`)
|
|
101771
|
+
`));
|
|
101469
101772
|
if (!normalized) {
|
|
101470
101773
|
return [];
|
|
101471
101774
|
}
|
|
@@ -101499,7 +101802,7 @@ function renderMarkdownToSlackBlocks(markdown) {
|
|
|
101499
101802
|
return;
|
|
101500
101803
|
}
|
|
101501
101804
|
const text = paragraph.join(`
|
|
101502
|
-
`).
|
|
101805
|
+
`).trimEnd();
|
|
101503
101806
|
if (text) {
|
|
101504
101807
|
const shouldRenderAsPreamble = !hasSeenHeading && firstHeadingLineIndex > 0 && blocks.length === 0;
|
|
101505
101808
|
pushBlock({
|
|
@@ -101614,14 +101917,16 @@ ${codeLines.join(`
|
|
|
101614
101917
|
}
|
|
101615
101918
|
continue;
|
|
101616
101919
|
}
|
|
101617
|
-
const bulletMatch = line.match(
|
|
101920
|
+
const bulletMatch = line.match(/^(\s*)[-*+]\s+(.+)$/);
|
|
101618
101921
|
if (bulletMatch) {
|
|
101619
|
-
|
|
101922
|
+
const depth = getMarkdownListDepth(bulletMatch[1] ?? "");
|
|
101923
|
+
paragraph.push(`${renderSlackBulletPrefix(depth)} ${renderInlineMarkdownToSlackMrkdwn(bulletMatch[2] ?? "")}`);
|
|
101620
101924
|
continue;
|
|
101621
101925
|
}
|
|
101622
|
-
const orderedMatch = line.match(
|
|
101926
|
+
const orderedMatch = line.match(/^(\s*)(\d+)\.\s+(.+)$/);
|
|
101623
101927
|
if (orderedMatch) {
|
|
101624
|
-
|
|
101928
|
+
const depth = getMarkdownListDepth(orderedMatch[1] ?? "");
|
|
101929
|
+
paragraph.push(`${renderSlackListIndent(depth)}${orderedMatch[2]}. ${renderInlineMarkdownToSlackMrkdwn(orderedMatch[3] ?? "")}`);
|
|
101625
101930
|
continue;
|
|
101626
101931
|
}
|
|
101627
101932
|
paragraph.push(renderInlineMarkdownToSlackMrkdwn(line));
|