agent-relay-orchestrator 0.27.2 → 0.29.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay-orchestrator",
3
- "version": "0.27.2",
3
+ "version": "0.29.0",
4
4
  "description": "Agent Relay orchestrator — manages agent lifecycle across hosts",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,7 +16,7 @@
16
16
  "test": "bun test"
17
17
  },
18
18
  "dependencies": {
19
- "agent-relay-sdk": "0.2.16"
19
+ "agent-relay-sdk": "0.2.18"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/bun": "latest",
package/src/control.ts CHANGED
@@ -192,7 +192,7 @@ function shutdownTimeoutMs(ctrl: Record<string, any>): number | undefined {
192
192
  // input and stays defensive against junk).
193
193
  // - cwd: `source.cwd || baseDir` — an empty-string cwd now falls back to
194
194
  // baseDir on the restart path too (was a latent bug; empty cwd is invalid).
195
- export function spawnOptionsFromRecord(source: Record<string, any>, config: OrchestratorConfig): SpawnOptions {
195
+ function spawnOptionsFromRecord(source: Record<string, any>, config: OrchestratorConfig): SpawnOptions {
196
196
  return {
197
197
  provider: source.provider === "codex" ? "codex" : "claude",
198
198
  cwd: source.cwd || config.baseDir,
package/src/relay.ts CHANGED
@@ -18,7 +18,7 @@ export interface RelayClient {
18
18
  connected: boolean;
19
19
  }
20
20
 
21
- export interface RunnerTokenRemint {
21
+ interface RunnerTokenRemint {
22
22
  token: string;
23
23
  record: { jti: string; profileId?: string; expiresAt?: number };
24
24
  }
@@ -32,7 +32,7 @@ export function detectSelfSupervision(moduleUrl: string = import.meta.url): Self
32
32
  }
33
33
 
34
34
  /** Reset the cache. Test-only. */
35
- export function resetSelfSupervisionCache(): void {
35
+ function resetSelfSupervisionCache(): void {
36
36
  cached = undefined;
37
37
  }
38
38
 
@@ -16,7 +16,7 @@ const CACHE_RACE_RE = /ETARGET|No matching version found|notarget/i;
16
16
  const DEFAULT_INSTALL_RETRIES = 4;
17
17
  const DEFAULT_INSTALL_RETRY_BASE_MS = 2000;
18
18
 
19
- export interface SelfUpgradeOptions {
19
+ interface SelfUpgradeOptions {
20
20
  /** Sleep between install retries (injectable for tests). */
21
21
  sleep?: (ms: number) => Promise<void>;
22
22
  /** Max extra install attempts after the first on a cache-race error. */
@@ -55,7 +55,7 @@ const defaultRunner: SelfUpgradeRunner = {
55
55
  },
56
56
  };
57
57
 
58
- export interface SelfUpgradePlan {
58
+ interface SelfUpgradePlan {
59
59
  targetVersion: string;
60
60
  providers: string[];
61
61
  unit: string;
package/src/spawn.ts CHANGED
@@ -51,7 +51,7 @@ interface SessionInfo {
51
51
  logFile: string;
52
52
  }
53
53
 
54
- export interface TerminalGuestSession {
54
+ interface TerminalGuestSession {
55
55
  session: string;
56
56
  mode: "guest";
57
57
  provider: string;
@@ -144,7 +144,20 @@ export function decodeControlOutput(data: string): Uint8Array {
144
144
  return Uint8Array.from(out);
145
145
  }
146
146
 
147
- export type ControlLine =
147
+ // The control stream is read as latin1 so the `%output` octal path above sees faithful
148
+ // bytes. But command-reply blocks (capture-pane grid rows) arrive as raw UTF-8, so their
149
+ // lines reach us as the byte-faithful latin1 representation. Re-decode them to real UTF-8
150
+ // here, or multi-byte glyphs (box-drawing, powerline) double-encode on the next repaint
151
+ // and render as mojibake (#270). Safe to do per line: no UTF-8 continuation byte is
152
+ // 0x0A/0x0D, so the newline split never lands mid-sequence.
153
+ const REPLY_UTF8_DECODER = new TextDecoder("utf-8");
154
+ export function latin1LineToUtf8(line: string): string {
155
+ const bytes = new Uint8Array(line.length);
156
+ for (let i = 0; i < line.length; i++) bytes[i] = line.charCodeAt(i) & 0xff;
157
+ return REPLY_UTF8_DECODER.decode(bytes);
158
+ }
159
+
160
+ type ControlLine =
148
161
  | { type: "output"; pane: string; bytes: Uint8Array }
149
162
  | { type: "exit"; reason?: string }
150
163
  // Command-reply block framing. Every command written to the control client's stdin is
@@ -191,7 +204,7 @@ export function parseControlLine(line: string): ControlLine {
191
204
  // scroll-region TUIs like Claude Code emit constantly). So we track whether the outbound
192
205
  // stream is at a sequence boundary (ground) and only inject there. This is a deliberately
193
206
  // minimal VT state machine: enough to know "are we mid-sequence?", not a full parser.
194
- export type AnsiState = "ground" | "esc" | "esc-charset" | "csi" | "string" | "string-esc";
207
+ type AnsiState = "ground" | "esc" | "esc-charset" | "csi" | "string" | "string-esc";
195
208
 
196
209
  export function advanceAnsiState(state: AnsiState, byte: number): AnsiState {
197
210
  // CAN (0x18) / SUB (0x1a) abort any in-progress sequence from any state → ground.
@@ -428,7 +441,7 @@ class SessionStream {
428
441
  if (!entry) return; // unexpected extra block — drop it rather than mis-correlate
429
442
  if (entry.timer) clearTimeout(entry.timer);
430
443
  if (isError) entry.reject(new Error(block.lines.join(" ").trim() || "tmux command error"));
431
- else entry.resolve(block.lines);
444
+ else entry.resolve(block.lines.map(latin1LineToUtf8));
432
445
  }
433
446
 
434
447
  private enqueue(bytes: Uint8Array): void {
@@ -441,7 +441,7 @@ export function refreshWorkspaceDeps(repoRoot: string, worktreePath: string, opt
441
441
  * install (full isolation, no shared cache), or =none to skip entirely.
442
442
  * Never throws — provisioning failure must not block the spawn.
443
443
  */
444
- export function provisionWorkspaceDeps(repoRoot: string, worktreePath: string): WorkspaceDepsProvision {
444
+ function provisionWorkspaceDeps(repoRoot: string, worktreePath: string): WorkspaceDepsProvision {
445
445
  const requested = (process.env.AGENT_RELAY_WORKSPACE_DEPS || "symlink").toLowerCase();
446
446
  if (requested === "none") return { mode: "none" };
447
447
 
@@ -1086,7 +1086,9 @@ function workspaceId(input: WorkspaceResolutionInput): string {
1086
1086
 
1087
1087
  function branchName(input: WorkspaceResolutionInput, id: string): string {
1088
1088
  const owner = input.policyName || input.label || input.automationId || "manual";
1089
- return `agent/${safeSegment(owner, 48)}/${safeSegment(id.replace(/^sp[_-]?/, ""), 24)}`;
1089
+ // 40 fits a full UUID (36) plus the `sp_`-stripped slack. A tighter cap (was 24)
1090
+ // sliced the session UUID mid-string, leaving an unaddressable, dangling ref (#282).
1091
+ return `agent/${safeSegment(owner, 48)}/${safeSegment(id.replace(/^sp[_-]?/, ""), 40)}`;
1090
1092
  }
1091
1093
 
1092
1094
  /** Next free `<branch>-N` cycle name for a recycled worktree (#206). Strips any