@songsid/agend 2.1.7 → 2.1.8-beta.2
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/backend/codex.d.ts +4 -0
- package/dist/backend/codex.js +91 -25
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/types.d.ts +8 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/daemon.d.ts +50 -0
- package/dist/daemon.js +148 -10
- package/dist/daemon.js.map +1 -1
- package/dist/ui/settings.html +120 -38
- package/package.json +1 -1
package/dist/daemon.js
CHANGED
|
@@ -979,6 +979,25 @@ export class Daemon extends EventEmitter {
|
|
|
979
979
|
* after the startup scan can still swallow that message's Enter.
|
|
980
980
|
*/
|
|
981
981
|
inputTransientGuardGeneration = null;
|
|
982
|
+
/**
|
|
983
|
+
* The spawn whose passive transients are over, independently of the
|
|
984
|
+
* startup-proof tail above. A transient screen can be quoted verbatim by the
|
|
985
|
+
* transcript (a cropped quote of the resume screen is byte-identical to the
|
|
986
|
+
* real one), so the pane alone must not be able to hold input. AgEnD's own
|
|
987
|
+
* launch state retires the check, per launch attempt: a launch that cannot show one, the real
|
|
988
|
+
* transient seen and then gone, or a "transient" that never changed for
|
|
989
|
+
* INPUT_TRANSIENT_STALL_MS. (The first submitted message retires the whole
|
|
990
|
+
* guard, as before.)
|
|
991
|
+
*/
|
|
992
|
+
inputTransientRetiredAttempt = null;
|
|
993
|
+
/** The launch attempt in which a passive transient was actually observed on screen. */
|
|
994
|
+
inputTransientSeenAttempt = null;
|
|
995
|
+
/**
|
|
996
|
+
* One per command actually launched. A spawn can launch more than once
|
|
997
|
+
* (spawnClaudeWindow retries a failed resume without a new beginSpawn), and
|
|
998
|
+
* each launch paints its own transient, so retirement is per attempt.
|
|
999
|
+
*/
|
|
1000
|
+
launchAttempt = 0;
|
|
982
1001
|
skipResume = false;
|
|
983
1002
|
startupAborted = false;
|
|
984
1003
|
/** First time the current on-screen blocking dialog was seen (0 = none); drives the parked report. */
|
|
@@ -4945,19 +4964,39 @@ export class Daemon extends EventEmitter {
|
|
|
4945
4964
|
if (this.inputTransientGuardGeneration === null
|
|
4946
4965
|
|| this.inputTransientGuardGeneration !== this.spawnGeneration)
|
|
4947
4966
|
return [];
|
|
4967
|
+
if (this.inputTransientRetiredAttempt === this.launchAttempt)
|
|
4968
|
+
return [];
|
|
4948
4969
|
return this.backend?.getInputUnavailableTransients?.() ?? [];
|
|
4949
4970
|
}
|
|
4971
|
+
/** Stop honouring passive transients for the current spawn. */
|
|
4972
|
+
retireInputTransients(reason) {
|
|
4973
|
+
if (this.inputTransientRetiredAttempt === this.launchAttempt)
|
|
4974
|
+
return;
|
|
4975
|
+
this.inputTransientRetiredAttempt = this.launchAttempt;
|
|
4976
|
+
this.logger.debug({ reason, generation: this.spawnGeneration, attempt: this.launchAttempt }, "Startup input transient retired for this launch");
|
|
4977
|
+
}
|
|
4950
4978
|
/** Match a passive transient against the current interactive screen. */
|
|
4951
4979
|
inputTransientInPane(pane) {
|
|
4952
|
-
|
|
4980
|
+
const transients = this.guardedInputTransients();
|
|
4981
|
+
if (transients.length === 0)
|
|
4982
|
+
return null;
|
|
4983
|
+
for (const transient of transients) {
|
|
4953
4984
|
// `pattern` is only a cheap pre-filter. `isActive` is deliberately the
|
|
4954
4985
|
// authority because users and agents routinely quote diagnostic text.
|
|
4955
4986
|
transient.pattern.lastIndex = 0;
|
|
4956
4987
|
if (!transient.pattern.test(pane))
|
|
4957
4988
|
continue;
|
|
4958
|
-
if (transient.isActive(pane))
|
|
4989
|
+
if (transient.isActive(pane)) {
|
|
4990
|
+
this.inputTransientSeenAttempt = this.launchAttempt;
|
|
4959
4991
|
return transient;
|
|
4992
|
+
}
|
|
4960
4993
|
}
|
|
4994
|
+
// The phase is over once it has been seen to end; from then on the same
|
|
4995
|
+
// screen is a quote. A settled-looking pane before that proves nothing:
|
|
4996
|
+
// codex 0.154 painted the prompt and footer first and "Resuming session…"
|
|
4997
|
+
// a moment later — the reason this guard outlives the startup scan.
|
|
4998
|
+
if (this.inputTransientSeenAttempt === this.launchAttempt)
|
|
4999
|
+
this.retireInputTransients("transient ended");
|
|
4961
5000
|
return null;
|
|
4962
5001
|
}
|
|
4963
5002
|
/** Capture failure is unknown, never evidence that input is available. */
|
|
@@ -4993,7 +5032,12 @@ export class Daemon extends EventEmitter {
|
|
|
4993
5032
|
return budget;
|
|
4994
5033
|
}
|
|
4995
5034
|
async probeInputTransient() {
|
|
4996
|
-
|
|
5035
|
+
// A retired transient still leaves the startup guard armed: an unreadable
|
|
5036
|
+
// pane keeps answering "unknown", so it stalls out on the same budget
|
|
5037
|
+
// instead of letting a delivery poll a dark pane for ever.
|
|
5038
|
+
const armed = this.inputTransientGuardGeneration !== null && this.inputTransientGuardGeneration === this.spawnGeneration
|
|
5039
|
+
&& (this.backend?.getInputUnavailableTransients?.().length ?? 0) > 0;
|
|
5040
|
+
if (!armed || !this.tmux)
|
|
4997
5041
|
return { state: "clear" };
|
|
4998
5042
|
try {
|
|
4999
5043
|
const pane = await this.tmux.capturePane();
|
|
@@ -5001,10 +5045,13 @@ export class Daemon extends EventEmitter {
|
|
|
5001
5045
|
// The pane text comes back with the verdict: a transient that is still
|
|
5002
5046
|
// repainting is still working, and that is the only thing separating a
|
|
5003
5047
|
// slow resume from a wedged one.
|
|
5048
|
+
if (transient)
|
|
5049
|
+
this.resetFooterFallback();
|
|
5004
5050
|
return transient ? { state: "active", transient, pane } : { state: "clear" };
|
|
5005
5051
|
}
|
|
5006
5052
|
catch (err) {
|
|
5007
5053
|
this.logger.debug({ err }, "capture-pane failed during the input-transient probe — pane state unknown");
|
|
5054
|
+
this.resetFooterFallback();
|
|
5008
5055
|
return { state: "unknown" };
|
|
5009
5056
|
}
|
|
5010
5057
|
}
|
|
@@ -5059,6 +5106,11 @@ export class Daemon extends EventEmitter {
|
|
|
5059
5106
|
stalledForMs: budget.stalled ? INPUT_TRANSIENT_STALL_MS : undefined,
|
|
5060
5107
|
ceilingMs: timeoutMs,
|
|
5061
5108
|
}, "CLI input stayed unavailable — refusing to send Enter");
|
|
5109
|
+
// A screen that did not change for the whole stall window is not a
|
|
5110
|
+
// load in progress. This delivery still fails, but the same screen
|
|
5111
|
+
// (possibly a quote) must not fail every later one too.
|
|
5112
|
+
if (budget.stalled && observedDescription !== null)
|
|
5113
|
+
this.retireInputTransients("transient stalled");
|
|
5062
5114
|
return false;
|
|
5063
5115
|
}
|
|
5064
5116
|
await new Promise(r => setTimeout(r, INPUT_TRANSIENT_POLL_MS));
|
|
@@ -5210,14 +5262,78 @@ export class Daemon extends EventEmitter {
|
|
|
5210
5262
|
return !check;
|
|
5211
5263
|
try {
|
|
5212
5264
|
const mode = await this.tmux.getPaneInputMode?.();
|
|
5213
|
-
if (mode !== "raw")
|
|
5265
|
+
if (mode !== "raw") {
|
|
5266
|
+
this.resetFooterFallback();
|
|
5214
5267
|
return false;
|
|
5215
|
-
|
|
5268
|
+
}
|
|
5269
|
+
return this.deliveryInputReadyPane(await this.tmux.capturePane());
|
|
5216
5270
|
}
|
|
5217
5271
|
catch {
|
|
5272
|
+
this.resetFooterFallback();
|
|
5218
5273
|
return false;
|
|
5219
5274
|
}
|
|
5220
5275
|
}
|
|
5276
|
+
/** Pane text of the current footer-fallback candidate, and since when it has been unchanged. */
|
|
5277
|
+
footerFallbackPaneKey = null;
|
|
5278
|
+
footerFallbackStableSince = 0;
|
|
5279
|
+
/** The launch the candidate was seen in: a new spawn or launch attempt starts over. */
|
|
5280
|
+
footerFallbackEpoch = null;
|
|
5281
|
+
/**
|
|
5282
|
+
* Forget the footer-fallback candidate. Called whenever the pane is seen
|
|
5283
|
+
* outside the candidate state (busy, a dialog, a transient, unreadable) and
|
|
5284
|
+
* on every spawn: "stable for UNKNOWN_LAYOUT_STABLE_MS" must mean this screen,
|
|
5285
|
+
* continuously, not time accumulated across an interruption.
|
|
5286
|
+
*/
|
|
5287
|
+
resetFooterFallback() {
|
|
5288
|
+
this.footerFallbackPaneKey = null;
|
|
5289
|
+
this.footerFallbackEpoch = null;
|
|
5290
|
+
}
|
|
5291
|
+
footerFallbackWarnedGeneration = null;
|
|
5292
|
+
/**
|
|
5293
|
+
* The input-row proof used before a paste (#931/#947), with one fallback
|
|
5294
|
+
* (#1031). Codex sometimes paints an idle composer without its Context
|
|
5295
|
+
* status item even though tui.status_line asks for it: after four fleet
|
|
5296
|
+
* restarts, sol's resumed session showed only `⚠ 2 warnings · f2 to view`.
|
|
5297
|
+
* The proof needs a recognised footer, so the first delivery after each
|
|
5298
|
+
* restart waited 30 minutes, failed, and waited again, for seven hours.
|
|
5299
|
+
*
|
|
5300
|
+
* When the footer is the only thing missing, the #978 structural evidence
|
|
5301
|
+
* stands in for it, all of it required: the backend's own stable-unknown
|
|
5302
|
+
* idle check (an empty live composer; no busy row, esc-to-interrupt row,
|
|
5303
|
+
* queued ↳ input or known picker), no input transient such as the resume
|
|
5304
|
+
* load, and the same screen for UNKNOWN_LAYOUT_STABLE_MS. The raw-tty
|
|
5305
|
+
* requirement stays with the caller.
|
|
5306
|
+
*/
|
|
5307
|
+
deliveryInputReadyPane(pane) {
|
|
5308
|
+
const check = this.backend?.isDeliveryInputReadyPane;
|
|
5309
|
+
if (!check)
|
|
5310
|
+
return true;
|
|
5311
|
+
if (check.call(this.backend, pane)) {
|
|
5312
|
+
this.footerFallbackPaneKey = null;
|
|
5313
|
+
return true;
|
|
5314
|
+
}
|
|
5315
|
+
const stableUnknown = this.backend?.isStableUnknownLayoutIdlePane;
|
|
5316
|
+
if (!stableUnknown?.call(this.backend, pane) || this.inputTransientInPane(pane)) {
|
|
5317
|
+
this.footerFallbackPaneKey = null;
|
|
5318
|
+
return false;
|
|
5319
|
+
}
|
|
5320
|
+
const key = pane.replace(/⋆/gu, "").replace(/[ \t]+$/gmu, "");
|
|
5321
|
+
const epoch = `${this.spawnGeneration}:${this.launchAttempt}`;
|
|
5322
|
+
const now = Date.now();
|
|
5323
|
+
if (key !== this.footerFallbackPaneKey || epoch !== this.footerFallbackEpoch) {
|
|
5324
|
+
this.footerFallbackPaneKey = key;
|
|
5325
|
+
this.footerFallbackEpoch = epoch;
|
|
5326
|
+
this.footerFallbackStableSince = now;
|
|
5327
|
+
return false;
|
|
5328
|
+
}
|
|
5329
|
+
if (now - this.footerFallbackStableSince < UNKNOWN_LAYOUT_STABLE_MS)
|
|
5330
|
+
return false;
|
|
5331
|
+
if (this.footerFallbackWarnedGeneration !== this.spawnGeneration) {
|
|
5332
|
+
this.footerFallbackWarnedGeneration = this.spawnGeneration;
|
|
5333
|
+
this.logger.warn({ backend: this.backend?.binaryName, stableForMs: now - this.footerFallbackStableSince }, "Idle footer not recognised — delivering on the #978 fallback (stable empty composer, no busy/queued/dialog, raw tty, no transient)");
|
|
5334
|
+
}
|
|
5335
|
+
return true;
|
|
5336
|
+
}
|
|
5221
5337
|
/**
|
|
5222
5338
|
* Why a pane is not deliverable matters. "busy" may be handed to a native
|
|
5223
5339
|
* input queue or steered (after its own dialog probe); "dialog", "transient"
|
|
@@ -5226,16 +5342,26 @@ export class Daemon extends EventEmitter {
|
|
|
5226
5342
|
* pane proves nothing. The silence gate comes first and costs no capture.
|
|
5227
5343
|
*/
|
|
5228
5344
|
async paneReadinessForDelivery(windowId) {
|
|
5229
|
-
|
|
5345
|
+
// Each early return below is the pane seen outside the footer-fallback
|
|
5346
|
+
// candidate state, and never reaches deliveryInputReadyPane: reset here.
|
|
5347
|
+
if (!this.isPaneIdleForDelivery(windowId)) {
|
|
5348
|
+
this.resetFooterFallback();
|
|
5230
5349
|
return "busy";
|
|
5350
|
+
}
|
|
5231
5351
|
const probe = await this.probeBlockingDialog();
|
|
5232
|
-
if (probe.state !== "clear")
|
|
5352
|
+
if (probe.state !== "clear") {
|
|
5353
|
+
this.resetFooterFallback();
|
|
5233
5354
|
return probe.state;
|
|
5355
|
+
}
|
|
5234
5356
|
const transient = await this.probeInputTransient();
|
|
5235
|
-
if (transient.state === "active")
|
|
5357
|
+
if (transient.state === "active") {
|
|
5358
|
+
this.resetFooterFallback();
|
|
5236
5359
|
return "transient";
|
|
5237
|
-
|
|
5360
|
+
}
|
|
5361
|
+
if (transient.state === "unknown") {
|
|
5362
|
+
this.resetFooterFallback();
|
|
5238
5363
|
return "unknown";
|
|
5364
|
+
}
|
|
5239
5365
|
if (this.needsStartupInputProof()) {
|
|
5240
5366
|
return await this.hasPositiveDeliveryInput() ? "ready" : "transient";
|
|
5241
5367
|
}
|
|
@@ -5306,6 +5432,11 @@ export class Daemon extends EventEmitter {
|
|
|
5306
5432
|
stalledForMs: transientBudget.stalled ? INPUT_TRANSIENT_STALL_MS : undefined,
|
|
5307
5433
|
ceilingMs: INPUT_TRANSIENT_WAIT_MS,
|
|
5308
5434
|
}, "CLI input stayed unavailable during the delivery-readiness wait");
|
|
5435
|
+
// Same rule as the pre-Enter wait: a screen frozen for the whole
|
|
5436
|
+
// stall window is not a load in progress, so it holds this delivery
|
|
5437
|
+
// only.
|
|
5438
|
+
if (transientBudget.stalled)
|
|
5439
|
+
this.retireInputTransients("transient stalled");
|
|
5309
5440
|
return false;
|
|
5310
5441
|
}
|
|
5311
5442
|
await new Promise(r => setTimeout(r, BOTTOM_READY_POLL_MS));
|
|
@@ -5407,7 +5538,7 @@ export class Daemon extends EventEmitter {
|
|
|
5407
5538
|
return "busy";
|
|
5408
5539
|
if (this.backend?.getBusyPattern?.()?.test(pane))
|
|
5409
5540
|
return "busy";
|
|
5410
|
-
if (
|
|
5541
|
+
if (!this.deliveryInputReadyPane(pane))
|
|
5411
5542
|
return "busy";
|
|
5412
5543
|
if (strandedAgendMessageInInput(pane, prompt))
|
|
5413
5544
|
return "stranded";
|
|
@@ -7570,6 +7701,13 @@ export class Daemon extends EventEmitter {
|
|
|
7570
7701
|
}
|
|
7571
7702
|
}
|
|
7572
7703
|
const cmd = `${envPrefix} ` + this.backend.buildCommand(launchConfig);
|
|
7704
|
+
// Every launched command re-arms the passive-transient check, including a
|
|
7705
|
+
// retry inside the same spawn: its load is a new one.
|
|
7706
|
+
this.launchAttempt++;
|
|
7707
|
+
// A launch that cannot paint a passive transient (Codex: a fresh start
|
|
7708
|
+
// never shows "Resuming session…") must not honour one quoted on screen.
|
|
7709
|
+
if (this.backend.launchMayShowInputTransient?.() === false)
|
|
7710
|
+
this.retireInputTransients("launch shows none");
|
|
7573
7711
|
// e.g. Codex could not read its session DB and chose a fallback (#984):
|
|
7574
7712
|
// the operator should know which conversation this launch continues.
|
|
7575
7713
|
const launchWarning = this.backend.consumeLaunchWarning?.() ?? null;
|