@songsid/agend 2.1.4-beta.31 → 2.1.4-beta.32

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
@@ -1037,7 +1037,7 @@ export class Daemon extends EventEmitter {
1037
1037
  }
1038
1038
  else if (msg.type === "raw_paste") {
1039
1039
  // Paste raw text directly to CLI without [user:] wrapping.
1040
- this.queueRawPaste(msg.content, this.captureDeliveryEpoch(msg.delivery_epoch));
1040
+ this.queueRawPaste(msg.content, this.captureDeliveryEpoch(msg.delivery_epoch), msg.confirm_clear === true);
1041
1041
  }
1042
1042
  else if (msg.type === "config_update") {
1043
1043
  this.applyConfigUpdate(msg.config);
@@ -1961,7 +1961,7 @@ export class Daemon extends EventEmitter {
1961
1961
  isDeliveryEpochCurrent(epoch) {
1962
1962
  return epoch === this.deliveryEpoch;
1963
1963
  }
1964
- queueRawPaste(rawText, deliveryEpoch = this.deliveryEpoch) {
1964
+ queueRawPaste(rawText, deliveryEpoch = this.deliveryEpoch, confirmClear = false) {
1965
1965
  if (!this.tmux || !this.isDeliveryEpochCurrent(deliveryEpoch))
1966
1966
  return;
1967
1967
  this.pasteLock = this.pasteLock.then(async () => {
@@ -1973,11 +1973,62 @@ export class Daemon extends EventEmitter {
1973
1973
  if (!this.isDeliveryEpochCurrent(deliveryEpoch))
1974
1974
  return;
1975
1975
  await this.deliverMessage(rawText, undefined, { deliveryEpoch });
1976
+ if (confirmClear)
1977
+ await this.confirmBackendClearDialog();
1976
1978
  this.logger.debug({ text: rawText.slice(0, 100) }, "Raw paste delivered");
1977
1979
  }).catch(err => {
1978
1980
  this.logger.warn({ err: err.message }, "raw_paste delivery error");
1979
1981
  });
1980
1982
  }
1983
+ /**
1984
+ * Confirm a backend's terminal-level /clear prompt after the channel-level
1985
+ * admin confirmation has already succeeded. Poll instead of pasting `/clear\ny`
1986
+ * as one buffer: Kiro versions that clear immediately interpret that queued
1987
+ * `y` as a brand-new user message.
1988
+ */
1989
+ async confirmBackendClearDialog() {
1990
+ const dialog = this.backend?.getClearConfirmationDialog?.();
1991
+ if (!dialog || !this.tmux)
1992
+ return;
1993
+ const matches = (pane) => {
1994
+ dialog.pattern.lastIndex = 0;
1995
+ return dialog.pattern.test(pane);
1996
+ };
1997
+ const deadline = Date.now() + 3_000;
1998
+ while (Date.now() <= deadline) {
1999
+ const pane = await this.tmux.capturePane().catch(() => "");
2000
+ if (matches(pane)) {
2001
+ const sent = await this.paneWriteLock.run(async () => {
2002
+ // Re-check under the write lock so a prompt that disappeared between
2003
+ // capture and lock acquisition never receives a stray literal `y`.
2004
+ const currentPane = await this.tmux.capturePane().catch(() => "");
2005
+ if (!matches(currentPane))
2006
+ return false;
2007
+ const specialKeys = new Set(["Up", "Down", "Enter", "Escape", "Right", "Left"]);
2008
+ for (const key of dialog.keys) {
2009
+ const ok = specialKeys.has(key)
2010
+ ? await this.tmux.sendSpecialKey(key)
2011
+ : await this.tmux.sendKeys(key);
2012
+ if (!ok)
2013
+ return false;
2014
+ await new Promise(resolve => setTimeout(resolve, 100));
2015
+ }
2016
+ return true;
2017
+ });
2018
+ if (sent) {
2019
+ this.logger.info({ dialog: dialog.description }, "Auto-confirmed backend clear dialog");
2020
+ }
2021
+ else {
2022
+ this.logger.warn({ dialog: dialog.description }, "Backend clear dialog disappeared or confirmation keys failed");
2023
+ }
2024
+ return;
2025
+ }
2026
+ await new Promise(resolve => setTimeout(resolve, 100));
2027
+ }
2028
+ // Recent Kiro releases may clear immediately without a second prompt. That
2029
+ // is a successful no-op here, not an error.
2030
+ this.logger.debug({ dialog: dialog.description }, "Backend clear dialog did not appear");
2031
+ }
1981
2032
  /** Send the backend-specific graceful quit command/key sequence. */
1982
2033
  async sendQuitSequence() {
1983
2034
  if (!this.tmux || !this.backend)
@@ -1985,18 +2036,20 @@ export class Daemon extends EventEmitter {
1985
2036
  const quitCmd = this.backend.getQuitCommand();
1986
2037
  const quitKey = this.backend.getQuitKey?.();
1987
2038
  if (quitCmd) {
1988
- await this.tmux.sendKeys(quitCmd);
2039
+ if (!await this.tmux.sendKeys(quitCmd))
2040
+ return false;
1989
2041
  // Delay before Enter to prevent tmux server races when instances stop in
1990
2042
  // parallel (same pattern as pasteText).
1991
2043
  await new Promise(r => setTimeout(r, 150));
1992
- await this.tmux.sendSpecialKey("Enter");
1993
- return true;
2044
+ return this.tmux.sendSpecialKey("Enter");
1994
2045
  }
1995
2046
  if (!quitKey)
1996
2047
  return false;
1997
2048
  const presses = Math.max(1, Math.floor(this.backend.getQuitKeyPresses?.() ?? 1));
1998
2049
  for (let i = 0; i < presses; i++) {
1999
- await this.tmux.sendSpecialKey(quitKey);
2050
+ const sent = await this.tmux.sendSpecialKey(quitKey);
2051
+ if (!sent)
2052
+ return false;
2000
2053
  if (i + 1 < presses)
2001
2054
  await new Promise(r => setTimeout(r, 250));
2002
2055
  }
@@ -2029,8 +2082,23 @@ export class Daemon extends EventEmitter {
2029
2082
  }
2030
2083
  }
2031
2084
  }
2032
- if (!killed)
2033
- this.logger.warn("CLI did not exit gracefully within 3s, force killing window");
2085
+ if (!killed) {
2086
+ this.logger.warn({ quitSent }, "CLI did not exit gracefully within 3s — falling back to SIGTERM");
2087
+ await this.killProcessTree("SIGTERM");
2088
+ for (let i = 0; i < 5; i++) {
2089
+ await new Promise(r => setTimeout(r, 200));
2090
+ const status = await this.tmux.getPaneStatus();
2091
+ if (!status || !status.alive) {
2092
+ killed = true;
2093
+ break;
2094
+ }
2095
+ }
2096
+ }
2097
+ if (!killed) {
2098
+ this.logger.warn("CLI process tree survived SIGTERM — falling back to SIGKILL");
2099
+ await this.killProcessTree("SIGKILL");
2100
+ await new Promise(r => setTimeout(r, 200));
2101
+ }
2034
2102
  // Always kill window — remain-on-exit keeps dead panes around after CLI exits
2035
2103
  const stoppedWindowId = this.tmux.getWindowId();
2036
2104
  await this.tmux.killWindow();