@bivy/bivy 0.10.1-staging.283 → 0.10.1-staging.285

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/bin/bivy.mjs CHANGED
@@ -922,9 +922,10 @@ async function resolveRunSpec(agentId, extraArgs) {
922
922
  return { spec: { agent: id, label: agent.label, command, args: [...(agent.args ?? []), ...extraArgs] } };
923
923
  }
924
924
 
925
- // Pull bivy's own `--name`/`--model` flags (space or `=` form) out of the run
926
- // args so they aren't blindly forwarded. Only honored before a `--` separator,
927
- // past which everything is the raw command the user asked to run.
925
+ // Pull bivy's own run flags out of the args so they aren't blindly forwarded.
926
+ // Only honored before a `--` separator, past which everything is the raw command
927
+ // the user asked to run. `--chat` selects the governed app-session path instead
928
+ // of the native PTY path; `--no-open` keeps that path headless after creation.
928
929
  // Does a token look like a git remote (URL, scp-style, owner/repo, or a path)
929
930
  // rather than an agent id? Lets `--clone <remote>` disambiguate from bare
930
931
  // `--clone` (= current folder's repo) without a required `=`.
@@ -936,10 +937,14 @@ function looksLikeRemote(value) {
936
937
  function extractRunFlags(args) {
937
938
  const rest = [];
938
939
  let name, model, node, workspace;
940
+ let chat = false;
941
+ let noOpen = false;
939
942
  let clone; // undefined = no clone; true = current repo; string = explicit remote
940
943
  for (let i = 0; i < args.length; i++) {
941
944
  const a = args[i];
942
945
  if (a === "--") { rest.push(...args.slice(i)); break; }
946
+ if (a === "--chat") { chat = true; continue; }
947
+ if (a === "--no-open") { noOpen = true; continue; }
943
948
  if (a === "--name" && args[i + 1] !== undefined) { name = args[++i]; continue; }
944
949
  if (a.startsWith("--name=")) { name = a.slice("--name=".length); continue; }
945
950
  if (a === "--model" && args[i + 1] !== undefined) { model = args[++i]; continue; }
@@ -952,7 +957,7 @@ function extractRunFlags(args) {
952
957
  if (a === "--clone") { clone = looksLikeRemote(args[i + 1]) ? args[++i] : true; continue; }
953
958
  rest.push(a);
954
959
  }
955
- return { name: name?.trim() || undefined, model: model?.trim() || undefined, node: node?.trim() || undefined, workspace: workspace?.trim() || undefined, clone, rest };
960
+ return { name: name?.trim() || undefined, model: model?.trim() || undefined, node: node?.trim() || undefined, workspace: workspace?.trim() || undefined, chat, noOpen, clone, rest };
956
961
  }
957
962
 
958
963
  // A safe-ish workspace dir name from a remote or path (basename minus .git).
@@ -1793,12 +1798,67 @@ complete -c bivy -n '__fish_seen_subcommand_from run' -a '${agents.join(" ")}'`)
1793
1798
  // flag on top of it.
1794
1799
  async function cmdRun(args = []) {
1795
1800
  if (!(await ensureDeps())) process.exit(1);
1796
- const { name, model, node, workspace, clone, rest } = extractRunFlags(args);
1801
+ const { name, model, node, workspace, chat, noOpen, clone, rest } = extractRunFlags(args);
1797
1802
  // Bare `bivy` (empty rest) resolves to the configured default agent; an
1798
1803
  // explicit `bivy run <agent>` keeps that agent verbatim.
1799
1804
  const [agentIdArg, ...extraArgs] = rest;
1800
1805
  const agentId = agentIdArg || resolveDefaultAgent();
1801
1806
 
1807
+ if (chat) {
1808
+ if (node) {
1809
+ console.error(c.red("--chat currently starts a governed session on this node; it can't be combined with --node."));
1810
+ process.exit(1);
1811
+ return;
1812
+ }
1813
+ if (agentId === "--" || extraArgs.length) {
1814
+ console.error(c.red("--chat starts a governed agent integration and does not accept native agent arguments or the raw '-- <command>' form."));
1815
+ process.exit(1);
1816
+ return;
1817
+ }
1818
+ let requestedWorkspace;
1819
+ try { requestedWorkspace = resolveWorkspaceDir({ clone, workspace }); }
1820
+ catch (error) { console.error(c.red(error?.message || String(error))); process.exit(1); return; }
1821
+ const config = loadConfig();
1822
+ // Preserve `bivy run`'s cwd contract: from inside a git checkout, a chat run
1823
+ // targets that checkout; elsewhere it uses the node's configured workspace.
1824
+ const sessionWorkspace = requestedWorkspace || defaultRunWorkspace(config);
1825
+ if (!(await ensureNodeRunning(config))) {
1826
+ console.error(c.red(`Could not start the Bivy node at ${url(config)}.`));
1827
+ process.exit(1);
1828
+ return;
1829
+ }
1830
+ try {
1831
+ const created = await localApi(config, "/api/session", {
1832
+ method: "POST",
1833
+ body: JSON.stringify({ agent: agentId, ...(model ? { model: { provider: "", id: model } } : {}), ...(sessionWorkspace ? { workspace: sessionWorkspace } : {}) }),
1834
+ });
1835
+ if (name) {
1836
+ await localApi(config, "/api/sessions/rename", {
1837
+ method: "POST",
1838
+ body: JSON.stringify({ sessionId: created.id, name }),
1839
+ });
1840
+ }
1841
+ const sessionPath = `/sessions/${encodeURIComponent(created.id)}`;
1842
+ const remote = await openRemoteApp({ open: !noOpen, remotePath: sessionPath });
1843
+ console.log(c.green(`Started chat session ${created.id} (${created.agentName || created.runtimeId || agentId}).`));
1844
+ if (remote && (noOpen || !canOpenBrowser())) console.log(`Open it in the Bivy app: ${c.cyan(remote.openUrl)}`);
1845
+ else if (!remote) {
1846
+ console.log(c.yellow("Remote app access is not configured on this node."));
1847
+ console.log(`Run ${c.cyan("bivy relay:setup")}, then open session ${c.cyan(created.id)} in the app.`);
1848
+ }
1849
+ return;
1850
+ } catch (error) {
1851
+ console.error(c.red(`Could not start chat session: ${error?.message || String(error)}`));
1852
+ process.exit(1);
1853
+ return;
1854
+ }
1855
+ }
1856
+ if (noOpen) {
1857
+ console.error(c.red("--no-open is only valid together with --chat."));
1858
+ process.exit(1);
1859
+ return;
1860
+ }
1861
+
1802
1862
  // A cloned/explicit workspace lives on THIS machine, so it only applies to the
1803
1863
  // local node. For a remote --node run the checkout would need to be made there.
1804
1864
  if (node && (clone !== undefined || workspace)) {
@@ -4573,9 +4633,10 @@ async function main() {
4573
4633
  // args (including its own --help) pass straight through to it — e.g.
4574
4634
  // 'bivy run claude --help' must show Claude's help, not bivy's.
4575
4635
  if (args[0] === "-h" || args[0] === "--help") {
4576
- console.log(`Usage: bivy run <agent> [--name <label>] [--model <model>] [--node <name>] [--clone [remote]] [--workspace <dir>] | -- <command>
4636
+ console.log(`Usage: bivy run <agent> [--chat [--no-open]] [--name <label>] [--model <model>] [--node <name>] [--clone [remote]] [--workspace <dir>] | -- <command>
4577
4637
 
4578
4638
  Run a native agent (real CLI/TUI) as a relay-visible session.
4639
+ Use --chat to start the governed session used by the app instead; --no-open prints its URL without launching a browser.
4579
4640
  Agents: ${[...AGENT_INTEGRATIONS.keys()].join(", ")}, or -- <command> for anything else.
4580
4641
  An agent's own --help passes through, e.g. 'bivy run claude --help'.`);
4581
4642
  break;
@@ -372,8 +372,30 @@ function onNotification(m) {
372
372
  else turnCompletionTimer = setTimeout(finishCompletedTurn, 500);
373
373
  return;
374
374
  }
375
- case "turn/failed":
375
+ case "turn/failed": {
376
+ // A failed turn is TERMINAL in the app-server — no `turn/completed` follows
377
+ // it — so end the turn here rather than merely stashing the error for a
378
+ // completion event that never arrives. Stashing (the old behavior) left the
379
+ // turn open on the Bivy side: ProtocolRuntime never saw session.done/error,
380
+ // never emitted agent_end, and the daemon pinned the session "working" until
381
+ // the stall watchdog force-recovered it minutes later (the "Codex gets stuck,
382
+ // I have to prompt it again" symptom). session.error makes ProtocolRuntime
383
+ // emit agent_end, settling the turn immediately.
384
+ const message = params?.error?.message || params?.message || "Codex turn failed";
385
+ pendingTurnCompletion = false;
386
+ if (turnCompletionTimer) clearTimeout(turnCompletionTimer);
387
+ turnCompletionTimer = null;
388
+ activeObservedItems.clear();
389
+ bivy({ type: "session.status", status: "idle" });
390
+ bivy({ type: "session.error", error: message });
391
+ sawError = null;
392
+ return;
393
+ }
376
394
  case "error":
395
+ // A non-turn-scoped error notification (e.g. transient reconnect noise, per
396
+ // the codex exec parser's read of this event). Stash it so a following
397
+ // turn/completed surfaces it; a genuinely turn-ending failure arrives as
398
+ // turn/failed above, which never leaves the turn open.
377
399
  sawError = params?.error?.message || params?.message || "Codex turn failed";
378
400
  return;
379
401
  default:
package/dist/server.js CHANGED
@@ -43,6 +43,7 @@ import { decideOAuthLoginSweep } from "./runtime/oauth/oauth-login-sweep.js";
43
43
  import { listCodexSessions, loadCodexTranscript } from "./runtime/codex-sessions.js";
44
44
  import { dedupeSessionSummaries } from "./session-identity.js";
45
45
  import { deriveSessionState } from "./session/session-state.js";
46
+ import { resolveStreamingBehavior } from "./session/record.js";
46
47
  import { createSessionEngine } from "./session/engine.js";
47
48
  import { exportProviderAuth, exportAccountApiKeys, exportSyncableProviderAuth, exportProviderAuthTombstones, importProviderAuth, removeProvider, setProviderApiKey, listCredentialRecords, setProviderApiKeyLabeled, setProviderReferenceLabeled, removeProviderCredential, setCredentialSync, getCredentialPresets, setActiveCredentialPreset, setCredentialPresetMapping, exportSyncableRecords, exportRecordTombstones, importCredentialRecords } from "./credentials/api.js";
48
49
  import { listProviders } from "./runtime/provider-catalog.js";
@@ -888,7 +889,14 @@ function streamingBehaviorFrom(value) {
888
889
  return value === "steer" || value === "followUp" ? value : undefined;
889
890
  }
890
891
  function promptOptionsFor(record, requested, images) {
891
- const streamingBehavior = streamingBehaviorFrom(requested) ?? (record.session.isStreaming ? "steer" : undefined);
892
+ // Default to steering only when a turn is genuinely in flight — judged from
893
+ // Bivy's own `isWorking` AND the runtime's `isStreaming`, not `isStreaming`
894
+ // alone (which can be stuck-true after a turn ends and would silently steer a
895
+ // fresh message into a dead turn). See resolveStreamingBehavior.
896
+ const streamingBehavior = resolveStreamingBehavior(streamingBehaviorFrom(requested), {
897
+ isWorking: record.isWorking,
898
+ isStreaming: record.session.isStreaming,
899
+ });
892
900
  return { ...(streamingBehavior ? { streamingBehavior } : {}), ...(images?.length ? { images } : {}) };
893
901
  }
894
902
  function promptForAgent(_record, promptText) {
@@ -1 +1,18 @@
1
- export {};
1
+ /**
2
+ * Resolve the streamingBehavior for a prompt when the client didn't request one.
3
+ * A prompt sent while a turn is genuinely in flight defaults to *steering* it
4
+ * (inject guidance into the running turn); otherwise it starts a fresh turn.
5
+ *
6
+ * "In flight" is judged from BOTH Bivy's own authoritative turn flag (`isWorking`,
7
+ * set on the first turn event and cleared on `agent_end`) AND the runtime's
8
+ * `isStreaming` — never `isStreaming` alone. `isStreaming` is the SDK/runtime's
9
+ * bit and can be stuck-true after a turn has actually ended. When it is,
10
+ * defaulting to "steer" injects the user's message into a turn that no longer
11
+ * exists, so it silently vanishes and the session looks wedged. Requiring both
12
+ * flags means a message to an already-ended turn starts a fresh turn instead.
13
+ */
14
+ export function resolveStreamingBehavior(requested, turn) {
15
+ if (requested)
16
+ return requested;
17
+ return turn.isWorking && turn.isStreaming ? "steer" : undefined;
18
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bivy/bivy",
3
- "version": "0.10.1-staging.283",
3
+ "version": "0.10.1-staging.285",
4
4
  "type": "module",
5
5
  "license": "AGPL-3.0-only",
6
6
  "description": "Run coding agents on machines you own. Open-source, self-hostable agent workspace.",