@yolo-labs/yolobridge 0.22.0 → 0.23.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.
@@ -22,8 +22,8 @@ import { SseFrameParser } from './sse-frame-parser.js';
22
22
  import { actionForFrame } from './frame-actions.js';
23
23
  import { startHeartbeat, defaultTimers } from './heartbeat.js';
24
24
  import { nextBackoffMs, isFatalCredentialRefusal, fatalCredentialRefusalMessage, } from './reconnect.js';
25
- import { deliverPromptToLocalAgent, captureLocalAgentOutput, onLocalAgentData, takeRawSeed as takeRawSeedFromAgent, primeRawStream as primeRawStreamFromAgent, getLocalAgentGeometry, } from './local-agent.js';
26
- import { OutputStreamBuffer, DEFAULT_FLUSH_INTERVAL_MS, } from './output-stream.js';
25
+ import { deliverPromptToLocalAgent, captureLocalAgentOutput, onLocalAgentData, takeRawSeed as takeRawSeedFromAgent, primeRawStream as primeRawStreamFromAgent, getLocalAgentGeometry, writeInputToLocalAgent, } from './local-agent.js';
26
+ import { OutputStreamBuffer, DEFAULT_FLUSH_INTERVAL_MS, INTERACTIVE_ECHO_FLUSH_MS, } from './output-stream.js';
27
27
  import * as apiClient from './api-client.js';
28
28
  import { refreshAccessToken as refreshAccessTokenApi } from './device-auth.js';
29
29
  import { loadAuth, saveAuth, loadAttachment, saveAttachment, clearAttachment, } from './config-store.js';
@@ -78,6 +78,7 @@ export async function runAttachDaemon(deps) {
78
78
  const log = deps.log ?? ((line) => process.stdout.write(`${line}\n`));
79
79
  const clearScreen = deps.clearScreen ?? (() => process.stdout.write('\x1b[2J\x1b[3J\x1b[H'));
80
80
  const deliverPrompt = deps.deliverPrompt ?? deliverPromptToLocalAgent;
81
+ const writeInput = deps.writeInput ?? writeInputToLocalAgent;
81
82
  const captureOutput = deps.captureOutput ?? captureLocalAgentOutput;
82
83
  const timers = deps.timers ?? defaultTimers;
83
84
  const now = deps.now ?? Date.now;
@@ -925,6 +926,27 @@ export async function runAttachDaemon(deps) {
925
926
  case 'prompt':
926
927
  await deliverPrompt(action.prompt);
927
928
  break;
929
+ case 'input': {
930
+ // Raw keystrokes from a console client. Written verbatim —
931
+ // no Enter appended, no readiness gate — see
932
+ // `writeInputToLocalAgent`. Nothing logs the bytes.
933
+ writeInput(action.data);
934
+ // The PTY echoes within ~1ms. Without this the echo waits out
935
+ // the 80ms batch window, which buys nothing for a payload
936
+ // this small and spends ~40% of the latency budget that is
937
+ // ours rather than the network's. `flushOutputStream` re-checks
938
+ // that this session is still current, so a stale timer no-ops.
939
+ const echoSession = outputStream;
940
+ if (echoSession) {
941
+ // A plain timer, not `timers`: that seam exists so tests can
942
+ // drive the heartbeat/flush CADENCE, and widening it for a
943
+ // 5ms nudge would touch every existing double. `unref` so a
944
+ // pending echo can never hold the process open at exit.
945
+ const t = setTimeout(() => { void flushOutputStream(echoSession); }, INTERACTIVE_ECHO_FLUSH_MS);
946
+ t.unref?.();
947
+ }
948
+ break;
949
+ }
928
950
  case 'read-output': {
929
951
  const captured = await captureOutput();
930
952
  // Taken AFTER the (async) capture and synchronously, so
@@ -27,6 +27,10 @@ export function actionForFrame(frame) {
27
27
  return { kind: 'ping' };
28
28
  case 'prompt':
29
29
  return { kind: 'prompt', attachmentId: String(data.attachmentId ?? ''), prompt: String(data.prompt ?? '') };
30
+ case 'input':
31
+ // No coercion beyond String(): these are the operator's own keystrokes
32
+ // and anything clever here would corrupt a control sequence.
33
+ return { kind: 'input', attachmentId: String(data.attachmentId ?? ''), data: String(data.data ?? '') };
30
34
  case 'read-output':
31
35
  return {
32
36
  kind: 'read-output',
@@ -868,6 +868,38 @@ export function stopLocalAgent() {
868
868
  // already dead
869
869
  }
870
870
  }
871
+ /**
872
+ * Write raw bytes straight into the PTY, exactly as typed.
873
+ *
874
+ * ⚠️ DELIBERATELY NOT `deliverPromptToLocalAgent`. That function is for one
875
+ * coherent instruction: it waits on a readiness gate, writes the text, pauses,
876
+ * then writes `\r`. Every one of those is wrong for a keystroke —
877
+ * a lone `\x03` would get an Enter appended, an arrow-key escape sequence would
878
+ * be split by the pause, and the readiness gate would block on a prompt that a
879
+ * mid-session TUI never shows.
880
+ *
881
+ * So this does the minimum: if there is a live PTY, write the bytes. No
882
+ * interpretation, no framing, no Enter.
883
+ *
884
+ * ⚠️ NOTHING HERE LOGS `data`. These are the operator's keystrokes on their own
885
+ * machine — the same rule the output path already follows, and the reason a
886
+ * console session logs that it opened and closed and never what was typed.
887
+ *
888
+ * Returns false when there is no agent to write to, so a caller can say
889
+ * "nothing is attached" rather than silently swallowing input.
890
+ */
891
+ export function writeInputToLocalAgent(data) {
892
+ if (!current || !data)
893
+ return false;
894
+ try {
895
+ current.ptyProcess.write(data);
896
+ return true;
897
+ }
898
+ catch {
899
+ // The child is gone; onExit will clear `current`.
900
+ return false;
901
+ }
902
+ }
871
903
  /**
872
904
  * Best-effort readiness check before `deliverPromptToLocalAgent` writes
873
905
  * into the PTY — docs/YOLOBRIDGE_PLAN.md's "[P1] Blind prompt delivery can
@@ -42,6 +42,21 @@
42
42
  * output reads as live, long enough that a chatty PTY costs ~12 POSTs/second
43
43
  * rather than one per `onData`. */
44
44
  export const DEFAULT_FLUSH_INTERVAL_MS = 80;
45
+ /**
46
+ * Flush delay after a console keystroke, rather than waiting out the batch
47
+ * window.
48
+ *
49
+ * The 80ms above is right for a passive VIEWER: it trades a little liveness for
50
+ * ~12 POSTs/second instead of one per `onData`. It is wrong for an interactive
51
+ * session, where a single echoed keystroke IS the entire payload and the batch
52
+ * saves nothing while costing up to 80ms of a round trip already near 200ms —
53
+ * measured 2026-08-28, and roughly 40% of the budget that is ours to spend
54
+ * rather than the network's.
55
+ *
56
+ * 5ms is long enough that a burst of keystrokes still coalesces into one POST,
57
+ * short enough to be invisible next to the ~160ms the network costs.
58
+ */
59
+ export const INTERACTIVE_ECHO_FLUSH_MS = 5;
45
60
  /** ~192 KiB/s sustained. Comfortably above a fast agent's real output rate
46
61
  * (a streaming LLM response is a few KiB/s), far below what `cat`ting a
47
62
  * large file would produce. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yolo-labs/yolobridge",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "YoloBridge — local coding-agent daemon that attaches a user's own Claude Code/Codex session to a YOLO Studio workspace as a first-class tile (docs/YOLOBRIDGE_PLAN.md, build-order Phase 5).",
5
5
  "license": "MIT",
6
6
  "type": "module",