@songsid/agend 2.1.4-beta.35 → 2.1.4-beta.37

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/daemon.js CHANGED
@@ -847,6 +847,17 @@ export class Daemon extends EventEmitter {
847
847
  authFailureUnresolved = false;
848
848
  /** One login-screen auth report per spawn — the screen persists across polls. */
849
849
  loginScreenReported = false;
850
+ /** A fatal startup screen (see StartupDialog.fatal) was already reported this run. */
851
+ fatalStartupReported = false;
852
+ /**
853
+ * The CLI is parked on a fatal startup screen and every pane write must be
854
+ * refused. This is deliberately separate from the pause that the reported
855
+ * pty_error triggers: the pause is applied by an async handler, and a queued
856
+ * delivery would race it into the modal (where paste+Enter can confirm
857
+ * "Exit and fix manually"). Set synchronously at dialog-match time; cleared
858
+ * when a startup scan reaches a real ready prompt, or on wake().
859
+ */
860
+ fatalStartupBlocked = false;
850
861
  /** model_error seen mid-turn: notify only if it survives to the idle screen. */
851
862
  pendingModelErrorKey = null;
852
863
  /**
@@ -2337,6 +2348,8 @@ export class Daemon extends EventEmitter {
2337
2348
  this.pausePending = false;
2338
2349
  this.authFailureUnresolved = false;
2339
2350
  this.loginScreenReported = false;
2351
+ this.fatalStartupReported = false;
2352
+ this.fatalStartupBlocked = false;
2340
2353
  if (this.pauseWakeState === "active")
2341
2354
  return;
2342
2355
  if (this.pauseWakeState === "pausing")
@@ -3332,6 +3345,8 @@ export class Daemon extends EventEmitter {
3332
3345
  await this.waitForSpawnToSettle();
3333
3346
  if (cancelled())
3334
3347
  return false;
3348
+ if (this.refuseFatalStartupDelivery(status))
3349
+ return false;
3335
3350
  let windowId = this.getWindowId();
3336
3351
  const supportsQueuedInput = this.backend?.supportsQueuedInput?.() === true;
3337
3352
  let handingOffToNativeQueue = false;
@@ -3373,9 +3388,29 @@ export class Daemon extends EventEmitter {
3373
3388
  return this.paneWriteLock.run(async () => {
3374
3389
  if (cancelled())
3375
3390
  return false;
3391
+ // Re-checked here because the flag can be set during the waits above
3392
+ // (a spawn's dialog scan runs concurrently with a queued delivery).
3393
+ if (this.refuseFatalStartupDelivery(status))
3394
+ return false;
3376
3395
  return this.writeMessageToPane(formatted, windowId, handingOffToNativeQueue, status);
3377
3396
  });
3378
3397
  }
3398
+ /**
3399
+ * While the CLI is parked on a fatal startup screen (see fatalStartupBlocked),
3400
+ * no pane write may happen: paste+Enter into the corrupt-config modal would
3401
+ * confirm "Exit and fix manually" and exit the CLI. The message is failed (❌)
3402
+ * rather than held — the accompanying config_error notification tells the user
3403
+ * what to fix, and holding would only build a queue that floods the pane the
3404
+ * moment the flag clears.
3405
+ */
3406
+ refuseFatalStartupDelivery(status) {
3407
+ if (!this.fatalStartupBlocked)
3408
+ return false;
3409
+ this.logger.error("Delivery refused — CLI is parked on a fatal startup screen");
3410
+ if (status)
3411
+ this.emit("message_failed", status); // ❌
3412
+ return true;
3413
+ }
3379
3414
  /**
3380
3415
  * A raw `%output` silence check is authoritative for ordinary TUIs. It is not
3381
3416
  * for a backend such as agy whose status-line hook periodically redraws an
@@ -4358,7 +4393,10 @@ export class Daemon extends EventEmitter {
4358
4393
  if (!await this.tmux.isWindowAlive())
4359
4394
  return false;
4360
4395
  const ready = await this.dismissDialogsUntilReady(3);
4361
- if (ready)
4396
+ // A fatal startup screen returns true (the spawn is over — respawning
4397
+ // would only re-show the same screen) but is NOT ready-for-delivery:
4398
+ // fatalStartupBlocked gates every pane write in deliverMessage.
4399
+ if (ready && !this.fatalStartupBlocked)
4362
4400
  this.firstDeliveryDelay.recordReady();
4363
4401
  return ready;
4364
4402
  }
@@ -4381,6 +4419,26 @@ export class Daemon extends EventEmitter {
4381
4419
  let matched = false;
4382
4420
  for (const dialog of startupDialogs) {
4383
4421
  if (dialog.pattern.test(pane)) {
4422
+ // A fatal startup screen cannot be dismissed by keypresses (e.g. a
4423
+ // corrupt claude.json modal offering only "exit" / "reset config").
4424
+ // It must be caught HERE and not fall through to the ready check:
4425
+ // the modal contains the `❯` selector the ready pattern matches, so
4426
+ // it would otherwise pass as ready and queued messages would be
4427
+ // typed into the modal. Report the error (pause stops delivery) and
4428
+ // stop the retry loop — like the sign-in screen case below, only a
4429
+ // human fixing the config ends this state.
4430
+ if (dialog.fatal) {
4431
+ // Block deliveries BEFORE the emit: the pty_error pause handler
4432
+ // is async, and a queued message would otherwise win the race
4433
+ // and be pasted into the modal.
4434
+ this.fatalStartupBlocked = true;
4435
+ if (!this.fatalStartupReported) {
4436
+ this.fatalStartupReported = true;
4437
+ this.logger.warn({ description: dialog.description }, "Fatal startup screen detected — reporting instead of dismissing");
4438
+ this.emit("pty_error", { name: this.name, ...dialog.fatal });
4439
+ }
4440
+ return true;
4441
+ }
4384
4442
  this.logger.debug(`Dismissing startup dialog: ${dialog.description}`);
4385
4443
  // Restart is exactly when inbound messages pile up, and nothing gates
4386
4444
  // delivery on `spawning`. Take the pane lock for the key sequence so a
@@ -4413,8 +4471,13 @@ export class Daemon extends EventEmitter {
4413
4471
  if (matched)
4414
4472
  continue;
4415
4473
  // CLI is ready (pattern defined by each backend)
4416
- if (this.backend.getReadyPattern().test(pane))
4474
+ if (this.backend.getReadyPattern().test(pane)) {
4475
+ // A real ready prompt (with no fatal dialog on screen — those are
4476
+ // matched above, before this check) means the fatal screen is gone,
4477
+ // e.g. the user fixed the config and the instance respawned.
4478
+ this.fatalStartupBlocked = false;
4417
4479
  return true;
4480
+ }
4418
4481
  // A CLI parked at its own sign-in screen is an AUTH incident: no dialog
4419
4482
  // key can dismiss it and no ready pattern will ever appear. Report it as
4420
4483
  // an auth error (the lifecycle double-checks with the token-free probe)