agent-relay-orchestrator 0.10.26 → 0.10.27

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/spawn.ts +41 -11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay-orchestrator",
3
- "version": "0.10.26",
3
+ "version": "0.10.27",
4
4
  "description": "Agent Relay orchestrator — manages agent lifecycle across hosts",
5
5
  "type": "module",
6
6
  "bin": {
package/src/spawn.ts CHANGED
@@ -962,20 +962,14 @@ export function captureTerminal(name: string, config: OrchestratorConfig): Termi
962
962
  }
963
963
 
964
964
  const size = tmuxPaneSize(name, socketName);
965
- const cursor = tmuxCursorPos(name, socketName);
966
- const result = Bun.spawnSync(tmuxCommand(socketName, "capture-pane", "-p", "-e", "-S", "-1000", "-t", name), {
967
- stdin: "ignore",
968
- stdout: "pipe",
969
- stderr: "pipe",
970
- });
971
- if (result.exitCode !== 0) {
972
- const stderr = result.stderr.toString().trim();
973
- throw new Error(stderr || `tmux capture-pane failed with exit code ${result.exitCode}`);
974
- }
965
+ const { content, cursor } = captureConsistent(
966
+ () => captureContent(name, socketName),
967
+ () => tmuxCursorPos(name, socketName),
968
+ );
975
969
 
976
970
  return {
977
971
  session: name,
978
- content: result.stdout.toString(),
972
+ content,
979
973
  running: true,
980
974
  agentAlive,
981
975
  ...size,
@@ -984,6 +978,42 @@ export function captureTerminal(name: string, config: OrchestratorConfig): Termi
984
978
  };
985
979
  }
986
980
 
981
+ // Capture cursor and content *consistently*. They come from separate tmux invocations,
982
+ // so if the pane scrolls between them (e.g. tool output streaming while the agent
983
+ // "thinks"), cursorY ends up off-by-one against the captured grid — the parked cursor
984
+ // then lands a row off and the TUI's next relative redraw stacks a stale statusline row
985
+ // (bottom-box ghost). Read content, then cursor, then content again; accept only when the
986
+ // two content reads bracket the cursor read unchanged, which proves the cursor reflects
987
+ // that exact grid. Fall through with the latest capture if the pane never holds still.
988
+ export function captureConsistent(
989
+ readContent: () => string,
990
+ readCursor: () => { cursorX?: number; cursorY?: number },
991
+ maxAttempts = 4,
992
+ ): { content: string; cursor: { cursorX?: number; cursorY?: number } } {
993
+ let content = readContent();
994
+ let cursor = readCursor();
995
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
996
+ const recheck = readContent();
997
+ if (recheck === content) break;
998
+ content = recheck;
999
+ cursor = readCursor();
1000
+ }
1001
+ return { content, cursor };
1002
+ }
1003
+
1004
+ function captureContent(name: string, socketName?: string): string {
1005
+ const result = Bun.spawnSync(tmuxCommand(socketName, "capture-pane", "-p", "-e", "-S", "-1000", "-t", name), {
1006
+ stdin: "ignore",
1007
+ stdout: "pipe",
1008
+ stderr: "pipe",
1009
+ });
1010
+ if (result.exitCode !== 0) {
1011
+ const stderr = result.stderr.toString().trim();
1012
+ throw new Error(stderr || `tmux capture-pane failed with exit code ${result.exitCode}`);
1013
+ }
1014
+ return result.stdout.toString();
1015
+ }
1016
+
987
1017
  export function terminalInputTokens(data: string): TerminalInputToken[] {
988
1018
  const tokens: TerminalInputToken[] = [];
989
1019
  let literal = "";