@bivy/bivy 0.15.0-staging.35 → 0.15.0-staging.37

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.
@@ -77,3 +77,12 @@ export function authProviderForSession(runtimeId, modelProvider) {
77
77
  return provider;
78
78
  return undefined;
79
79
  }
80
+ /** Convert a provider failure into the safe wire shape consumed by clients.
81
+ * Raw provider text can contain stack frames, paths, or tokens and must never
82
+ * be used as the user-facing auth error. */
83
+ export function classifyModelAuthError(raw, runtimeId, modelProvider) {
84
+ if (!isModelAuthError(raw))
85
+ return null;
86
+ const provider = authProviderForSession(runtimeId, modelProvider);
87
+ return provider ? { kind: "model_auth", provider } : null;
88
+ }
package/dist/server.js CHANGED
@@ -34,7 +34,7 @@ import { InMemoryLocationRegistry } from "./runtime/location-registry.js";
34
34
  import { ControlPlaneSessionLocationRegistry, LayeredSessionLocationRegistry } from "./runtime/control-plane-location.js";
35
35
  import { attachAdoptedSessions, classifyAttachFailure } from "./runtime/adoption.js";
36
36
  import { createCredentialStore, testProviderCredential } from "./runtime/credentials.js";
37
- import { isModelAuthError, authProviderForSession } from "./runtime/auth-errors.js";
37
+ import { isModelAuthError, authProviderForSession, classifyModelAuthError } from "./runtime/auth-errors.js";
38
38
  import { createCredentialVault, migrateVaultDir } from "./runtime/credential-store.js";
39
39
  import { probeAnthropicAccess } from "./runtime/anthropic-preflight.js";
40
40
  import { createSessionNamer, fallbackSessionName } from "./session/session-namer.js";
@@ -930,6 +930,7 @@ function harnessDirFor(record) {
930
930
  function harnessBeginTurn(record) {
931
931
  const dir = harnessDirFor(record);
932
932
  record.harnessTurnReady = undefined;
933
+ record.harnessTurnFinished = false;
933
934
  if (!dir)
934
935
  return;
935
936
  const previous = record.workspaceState === "dirty" ? "dirty" : "clean";
@@ -954,6 +955,14 @@ function harnessBeginTurn(record) {
954
955
  })();
955
956
  }
956
957
  /** After a turn, snapshot again and broadcast the structured diff it produced. */
958
+ function finishHarnessTurn(record) {
959
+ if (record.harnessTurnFinished)
960
+ return;
961
+ record.harnessTurnFinished = true;
962
+ void harnessEndTurn(record).finally(() => {
963
+ void replication.onTurnComplete(record.id);
964
+ });
965
+ }
957
966
  async function harnessEndTurn(record) {
958
967
  const previous = record.workspaceState === "dirty" ? "dirty" : "clean";
959
968
  try {
@@ -1643,11 +1652,14 @@ function stampSessionEvent(payload) {
1643
1652
  return payload;
1644
1653
  }
1645
1654
  // Collapse the burst of assistant `message_update`s (one per agent stdout line,
1646
- // each carrying the FULL text so far) into ~1 fan-out per tick. See
1647
- // session-event-coalescer.ts for the rationale (kills the O(n^2) re-serialize).
1655
+ // each carrying the FULL text so far) to a human-smooth 4 fps. A 16 ms window
1656
+ // still allowed up to 62 cumulative snapshots/second, making a long answer
1657
+ // quadratic and surprisingly expensive over a phone's relay connection. The
1658
+ // latest snapshot supersedes every skipped one, and non-update events flush it
1659
+ // immediately, so 250 ms changes neither final content nor event ordering.
1648
1660
  // The coalescer's emit stamps the surviving (latest) update, so only ~one seq is
1649
- // spent per tick — the ring holds turn-scale events, not every stdout line.
1650
- const SESSION_UPDATE_COALESCE_MS = 16;
1661
+ // spent per window — the ring holds turn-scale events, not every stdout line.
1662
+ const SESSION_UPDATE_COALESCE_MS = 250;
1651
1663
  const sessionEvents = new SessionEventCoalescer({
1652
1664
  coalesceMs: SESSION_UPDATE_COALESCE_MS,
1653
1665
  emit: (payload) => broadcastCoalesced(stampSessionEvent(payload)),
@@ -2842,7 +2854,7 @@ const RELAY_COMMANDS = {
2842
2854
  // this the relay client (PWA) is stranded on "Working…" forever with
2843
2855
  // only a session.error toast. Clear working so a terminal state reaches it.
2844
2856
  clearSessionWorking(record);
2845
- broadcast({ type: "session.error", sessionId: record.id, error: actionableAgentError(record.runtimeId, error) });
2857
+ broadcast({ type: "session.error", sessionId: record.id, error: sessionAgentError(record, error) });
2846
2858
  });
2847
2859
  },
2848
2860
  async "session.new"(msg) {
@@ -5959,6 +5971,7 @@ async function sessionListRows() {
5959
5971
  agent: meta?.runtimeId ?? s.agent,
5960
5972
  agentName: meta?.agentName ?? s.agentName,
5961
5973
  source: rec?.source ?? meta?.source,
5974
+ bivyCreated: Boolean(meta),
5962
5975
  forkedFrom: rec?.forkedFrom ?? meta?.forkedFrom,
5963
5976
  branch: rec?.worktree?.branch ?? meta?.branch,
5964
5977
  sandbox: rec?.sandbox ?? normalizeSandboxTier(meta?.sandbox),
@@ -7175,6 +7188,11 @@ function actionableAgentError(runtimeId, error) {
7175
7188
  }
7176
7189
  return raw;
7177
7190
  }
7191
+ function sessionAgentError(record, error) {
7192
+ const raw = humanizeAgentError(error instanceof Error ? error.message : String(error));
7193
+ return classifyModelAuthError(raw, record.runtimeId, record.session.getCurrentModel()?.provider)
7194
+ ?? actionableAgentError(record.runtimeId, error);
7195
+ }
7178
7196
  /**
7179
7197
  * A turn that ended in a *terminal* model/provider failure the runtime would
7180
7198
  * otherwise swallow. `agent_end` carries the turn's messages and whether the
@@ -7231,7 +7249,7 @@ function attachSessionListeners(record) {
7231
7249
  policy: sessionRunPolicy,
7232
7250
  onNotice: (n) => broadcast({ type: "session.notice", sessionId: record.id, level: n.level, message: n.message }),
7233
7251
  onModelChanged: () => broadcast({ type: "model.updated", sessionId: record.id, model: publicModel(record.session.getCurrentModel(), record.session.getCurrentModel()) }),
7234
- onFailed: (message) => broadcast({ type: "session.error", sessionId: record.id, error: message }),
7252
+ onFailed: (message) => broadcast({ type: "session.error", sessionId: record.id, error: sessionAgentError(record, message) }),
7235
7253
  });
7236
7254
  }
7237
7255
  record.unsubscribe = record.session.subscribe((event) => {
@@ -7328,9 +7346,14 @@ function attachSessionListeners(record) {
7328
7346
  transcripts.resolveInlineImages(record);
7329
7347
  }
7330
7348
  // Durably persist the throttled sidecars at the turn boundary so a crash
7331
- // loses at most the in-flight turn's UI detail, not the whole turn.
7332
- if (event.type === "turn_end")
7349
+ // loses at most the in-flight turn's UI detail, not the whole turn. A
7350
+ // runtime may keep its agent process alive and never emit agent_end (Pi),
7351
+ // so turn_end is also authoritative for the interactive working state.
7352
+ if (event.type === "turn_end") {
7333
7353
  eventLog.flush(record.id);
7354
+ clearSessionWorking(record);
7355
+ finishHarnessTurn(record);
7356
+ }
7334
7357
  // AskUserQuestion is intercepted and answered by the daemon's guardian /
7335
7358
  // QuestionManager (see guardianInterceptor), which broadcasts
7336
7359
  // session.question(.resolved) from its own listeners — runtimes no longer
@@ -7365,14 +7388,9 @@ function attachSessionListeners(record) {
7365
7388
  transcripts.clearLiveIntermediate(record.id);
7366
7389
  clearSessionWorking(record);
7367
7390
  void refreshSessionUsage(record);
7368
- // Snapshot the worktree and broadcast the structured diff this turn made —
7369
- // universal edit review + rewind target, for every runtime.
7370
- // Warm-replicate this turn to the standby AFTER the checkpoint is committed
7371
- // (so the shipped frame carries this turn's transcript AND its checkpoint).
7372
- // Gated on session sync — inert by default.
7373
- void harnessEndTurn(record).finally(() => {
7374
- void replication.onTurnComplete(record.id);
7375
- });
7391
+ // Snapshot/diff at whichever completion boundary arrived first. Pi keeps
7392
+ // its process alive after turn_end; other runtimes also emit agent_end.
7393
+ finishHarnessTurn(record);
7376
7394
  // A turn that ended in a terminal model/provider error (e.g. an expired
7377
7395
  // credential or a 4xx from the API) otherwise vanished: working cleared,
7378
7396
  // no reply, no signal. Surface it as a session-scoped error so the client
@@ -7434,7 +7452,7 @@ function attachSessionListeners(record) {
7434
7452
  scheduleAdvertise();
7435
7453
  broadcast({ type: "session.failed", sessionId: record.id, failedAt: record.lastFailureAt });
7436
7454
  if (messageError)
7437
- broadcast({ type: "session.error", sessionId: record.id, error: actionableAgentError(record.runtimeId, messageError) });
7455
+ broadcast({ type: "session.error", sessionId: record.id, error: sessionAgentError(record, messageError) });
7438
7456
  // If the terminal error is an auth failure (expired key/token → 4xx),
7439
7457
  // also raise the sign-in sheet for the failing provider.
7440
7458
  maybeSignalAuthRequired(record, turnError);
@@ -8058,7 +8076,7 @@ async function createSession(workspace = defaultWorkspace, sessionFile, opts = {
8058
8076
  void refreshSessionUsage(record);
8059
8077
  if (makeActive)
8060
8078
  active = record;
8061
- broadcast({ type: "session.created", sessionId, name: record.session.getName(), workspace: sessionWorkspace, sessionFile: record.sessionFile, source: record.source, branch: worktree?.branch, prUrl: record.prUrl, runtimeId: rt.id, agentName: rt.displayName, modelFallbackMessage, bivySession: bivySessionEnvelope(record), capabilities: capabilitiesWithCommands(rt.id, record.session) });
8079
+ broadcast({ type: "session.created", sessionId, name: record.session.getName(), workspace: sessionWorkspace, sessionFile: record.sessionFile, source: record.source, bivyCreated: true, branch: worktree?.branch, prUrl: record.prUrl, runtimeId: rt.id, agentName: rt.displayName, modelFallbackMessage, bivySession: bivySessionEnvelope(record), capabilities: capabilitiesWithCommands(rt.id, record.session) });
8062
8080
  void maybeNotifyBivyUpdate();
8063
8081
  scheduleAdvertise();
8064
8082
  return record;
@@ -9853,6 +9871,7 @@ app.get("/api/sessions", async (_req, res, next) => {
9853
9871
  agent: meta?.runtimeId ?? s.agent,
9854
9872
  agentName: meta?.agentName ?? s.agentName,
9855
9873
  source: rec?.source ?? meta?.source,
9874
+ bivyCreated: Boolean(meta),
9856
9875
  forkedFrom: rec?.forkedFrom ?? meta?.forkedFrom,
9857
9876
  branch: rec?.worktree?.branch ?? meta?.branch,
9858
9877
  prUrl: rec?.prUrl ?? meta?.prUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bivy/bivy",
3
- "version": "0.15.0-staging.35",
3
+ "version": "0.15.0-staging.37",
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.",