@bivy/bivy 0.15.0-staging.36 → 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 {
@@ -2845,7 +2854,7 @@ const RELAY_COMMANDS = {
2845
2854
  // this the relay client (PWA) is stranded on "Working…" forever with
2846
2855
  // only a session.error toast. Clear working so a terminal state reaches it.
2847
2856
  clearSessionWorking(record);
2848
- broadcast({ type: "session.error", sessionId: record.id, error: actionableAgentError(record.runtimeId, error) });
2857
+ broadcast({ type: "session.error", sessionId: record.id, error: sessionAgentError(record, error) });
2849
2858
  });
2850
2859
  },
2851
2860
  async "session.new"(msg) {
@@ -5962,6 +5971,7 @@ async function sessionListRows() {
5962
5971
  agent: meta?.runtimeId ?? s.agent,
5963
5972
  agentName: meta?.agentName ?? s.agentName,
5964
5973
  source: rec?.source ?? meta?.source,
5974
+ bivyCreated: Boolean(meta),
5965
5975
  forkedFrom: rec?.forkedFrom ?? meta?.forkedFrom,
5966
5976
  branch: rec?.worktree?.branch ?? meta?.branch,
5967
5977
  sandbox: rec?.sandbox ?? normalizeSandboxTier(meta?.sandbox),
@@ -7178,6 +7188,11 @@ function actionableAgentError(runtimeId, error) {
7178
7188
  }
7179
7189
  return raw;
7180
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
+ }
7181
7196
  /**
7182
7197
  * A turn that ended in a *terminal* model/provider failure the runtime would
7183
7198
  * otherwise swallow. `agent_end` carries the turn's messages and whether the
@@ -7234,7 +7249,7 @@ function attachSessionListeners(record) {
7234
7249
  policy: sessionRunPolicy,
7235
7250
  onNotice: (n) => broadcast({ type: "session.notice", sessionId: record.id, level: n.level, message: n.message }),
7236
7251
  onModelChanged: () => broadcast({ type: "model.updated", sessionId: record.id, model: publicModel(record.session.getCurrentModel(), record.session.getCurrentModel()) }),
7237
- 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) }),
7238
7253
  });
7239
7254
  }
7240
7255
  record.unsubscribe = record.session.subscribe((event) => {
@@ -7331,9 +7346,14 @@ function attachSessionListeners(record) {
7331
7346
  transcripts.resolveInlineImages(record);
7332
7347
  }
7333
7348
  // Durably persist the throttled sidecars at the turn boundary so a crash
7334
- // loses at most the in-flight turn's UI detail, not the whole turn.
7335
- 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") {
7336
7353
  eventLog.flush(record.id);
7354
+ clearSessionWorking(record);
7355
+ finishHarnessTurn(record);
7356
+ }
7337
7357
  // AskUserQuestion is intercepted and answered by the daemon's guardian /
7338
7358
  // QuestionManager (see guardianInterceptor), which broadcasts
7339
7359
  // session.question(.resolved) from its own listeners — runtimes no longer
@@ -7368,14 +7388,9 @@ function attachSessionListeners(record) {
7368
7388
  transcripts.clearLiveIntermediate(record.id);
7369
7389
  clearSessionWorking(record);
7370
7390
  void refreshSessionUsage(record);
7371
- // Snapshot the worktree and broadcast the structured diff this turn made —
7372
- // universal edit review + rewind target, for every runtime.
7373
- // Warm-replicate this turn to the standby AFTER the checkpoint is committed
7374
- // (so the shipped frame carries this turn's transcript AND its checkpoint).
7375
- // Gated on session sync — inert by default.
7376
- void harnessEndTurn(record).finally(() => {
7377
- void replication.onTurnComplete(record.id);
7378
- });
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);
7379
7394
  // A turn that ended in a terminal model/provider error (e.g. an expired
7380
7395
  // credential or a 4xx from the API) otherwise vanished: working cleared,
7381
7396
  // no reply, no signal. Surface it as a session-scoped error so the client
@@ -7437,7 +7452,7 @@ function attachSessionListeners(record) {
7437
7452
  scheduleAdvertise();
7438
7453
  broadcast({ type: "session.failed", sessionId: record.id, failedAt: record.lastFailureAt });
7439
7454
  if (messageError)
7440
- broadcast({ type: "session.error", sessionId: record.id, error: actionableAgentError(record.runtimeId, messageError) });
7455
+ broadcast({ type: "session.error", sessionId: record.id, error: sessionAgentError(record, messageError) });
7441
7456
  // If the terminal error is an auth failure (expired key/token → 4xx),
7442
7457
  // also raise the sign-in sheet for the failing provider.
7443
7458
  maybeSignalAuthRequired(record, turnError);
@@ -8061,7 +8076,7 @@ async function createSession(workspace = defaultWorkspace, sessionFile, opts = {
8061
8076
  void refreshSessionUsage(record);
8062
8077
  if (makeActive)
8063
8078
  active = record;
8064
- 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) });
8065
8080
  void maybeNotifyBivyUpdate();
8066
8081
  scheduleAdvertise();
8067
8082
  return record;
@@ -9856,6 +9871,7 @@ app.get("/api/sessions", async (_req, res, next) => {
9856
9871
  agent: meta?.runtimeId ?? s.agent,
9857
9872
  agentName: meta?.agentName ?? s.agentName,
9858
9873
  source: rec?.source ?? meta?.source,
9874
+ bivyCreated: Boolean(meta),
9859
9875
  forkedFrom: rec?.forkedFrom ?? meta?.forkedFrom,
9860
9876
  branch: rec?.worktree?.branch ?? meta?.branch,
9861
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.36",
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.",