@bivy/bivy 0.5.1-staging.63 → 0.5.1-staging.65

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.
Files changed (2) hide show
  1. package/dist/server.js +65 -3
  2. package/package.json +1 -1
package/dist/server.js CHANGED
@@ -409,13 +409,14 @@ const approvals = new ApprovalManager();
409
409
  const questionManager = new QuestionManager();
410
410
  questionManager.onRequest((request) => {
411
411
  scheduleAdvertise();
412
- broadcast({ type: "session.question", sessionId: request.sessionId, requestId: request.id, questions: request.questions });
412
+ broadcast({ type: "session.question", sessionId: request.sessionId, requestId: request.id, questions: request.questions, createdAt: request.createdAt });
413
413
  // Notify unconditionally: a clarifying question always fires mid-turn (the
414
414
  // session is "working"), and it's a hard blocker the user must see to unblock
415
415
  // — matching the pre-refactor behavior.
416
416
  void sendNotificationHint({
417
417
  kind: "question_asked",
418
418
  sessionId: request.sessionId,
419
+ attentionId: request.id,
419
420
  title: "Bivy needs your input",
420
421
  body: `${sessionNotifyLabel(resolveSession(request.sessionId))} is asking a question — tap to answer.`,
421
422
  });
@@ -3618,6 +3619,18 @@ function startRelayIfConfigured() {
3618
3619
  clearInterval(advertiseResyncTimer);
3619
3620
  advertiseResyncTimer = setInterval(() => scheduleAdvertise(), 60_000);
3620
3621
  advertiseResyncTimer.unref?.();
3622
+ // Periodic online heartbeat. The relay flips the node's `online` flag
3623
+ // fire-and-forget on socket connect/close with no ordering guard, so a
3624
+ // late/racing `false` can pin a genuinely-connected node offline in the
3625
+ // registry until some later reconnect wins. Re-affirming online on a steady
3626
+ // interval keeps `last_seen_at` fresh and self-heals a lost race (the control
3627
+ // plane treats a recent heartbeat as online — see NODE_ONLINE_TTL_MS). Fire one
3628
+ // immediately so a reconnect corrects a stale `false` without waiting a full tick.
3629
+ if (nodeHeartbeatTimer)
3630
+ clearInterval(nodeHeartbeatTimer);
3631
+ void sendNodeHeartbeat();
3632
+ nodeHeartbeatTimer = setInterval(() => void sendNodeHeartbeat(), NODE_HEARTBEAT_MS);
3633
+ nodeHeartbeatTimer.unref?.();
3621
3634
  }
3622
3635
  console.log("[relay] connector enabled");
3623
3636
  return true;
@@ -4100,6 +4113,28 @@ function startModelAuthWatcher() {
4100
4113
  let sessionAdvertiseTarget;
4101
4114
  let advertiseTimer;
4102
4115
  let advertiseResyncTimer;
4116
+ // How often the node re-affirms it's online to the control plane. Kept well
4117
+ // under the control plane's NODE_ONLINE_TTL_MS (90s) so a missed beat or two
4118
+ // doesn't flap a healthy node's status.
4119
+ const NODE_HEARTBEAT_MS = 30_000;
4120
+ let nodeHeartbeatTimer;
4121
+ /** Re-affirm this node's online status so the registry self-heals a lost
4122
+ * relay connect/close race (see the heartbeat wiring in the relay connector
4123
+ * and NODE_ONLINE_TTL_MS in the control plane). Best-effort: a missed beat is
4124
+ * covered by the next tick and the TTL window. */
4125
+ async function sendNodeHeartbeat() {
4126
+ if (!sessionAdvertiseTarget)
4127
+ return;
4128
+ try {
4129
+ await fetch(`${sessionAdvertiseTarget.controlPlaneUrl.replace(/\/$/, "")}/node/heartbeat`, {
4130
+ method: "POST",
4131
+ headers: { authorization: `Bearer ${sessionAdvertiseTarget.enrollmentToken}` },
4132
+ });
4133
+ }
4134
+ catch {
4135
+ // best effort; the next tick retries
4136
+ }
4137
+ }
4103
4138
  async function advertiseNodeName(name, prevName) {
4104
4139
  if (!sessionAdvertiseTarget)
4105
4140
  return;
@@ -4286,16 +4321,37 @@ async function advertiseSessions() {
4286
4321
  // dropped; and with the remote flag off the map is empty, so the payload is
4287
4322
  // byte-identical to before.
4288
4323
  const agentServiceAddress = sessionAgentServiceAddress(record) ?? (record ? undefined : (await inMemorySessionLocations.lookup(s.id).catch(() => undefined))?.agentServiceAddress);
4324
+ const approvalAttention = approvals.list()
4325
+ .filter((a) => a.sessionId === s.id && a.status === "pending")
4326
+ .map((a) => ({
4327
+ id: a.id,
4328
+ kind: "approval",
4329
+ severity: a.risk === "critical" ? "critical" : a.risk === "high" ? "error" : "warning",
4330
+ createdAt: new Date(a.createdAt).toISOString(),
4331
+ }));
4332
+ const questionAttention = questionManager.list()
4333
+ .filter((q) => q.sessionId === s.id && q.status === "pending")
4334
+ .map((q) => ({ id: q.id, kind: "question", severity: "warning", createdAt: new Date(q.createdAt).toISOString() }));
4335
+ const failureAt = record?.lastFailureAt || (meta?.status === "failed" ? Date.parse(meta.updatedAt) : 0);
4336
+ const failureAttention = failureAt
4337
+ ? [{
4338
+ id: "last-failure",
4339
+ kind: (record?.source || meta?.source ? "automation" : "session"),
4340
+ severity: "error",
4341
+ createdAt: new Date(failureAt).toISOString(),
4342
+ }]
4343
+ : [];
4289
4344
  return {
4290
4345
  sessionId: s.id,
4291
- status: pendingApproval ? "needs_action" : (record ? (sessionBusy(record) ? "working" : "idle") : "saved"),
4292
- needsAction: pendingApproval,
4346
+ status: pendingApproval || failureAttention.length ? "needs_action" : (record ? (sessionBusy(record) ? "working" : "idle") : "saved"),
4347
+ needsAction: pendingApproval || failureAttention.length > 0,
4293
4348
  source: record?.source || meta?.source,
4294
4349
  titleEnc: name ? relay.sealString(name) : undefined,
4295
4350
  branch: record?.worktree?.branch || meta?.branch,
4296
4351
  agentServiceAddress,
4297
4352
  githubIssueUrl: record?.githubIssueUrl,
4298
4353
  prUrl: record?.prUrl,
4354
+ attention: [...approvalAttention, ...questionAttention, ...failureAttention],
4299
4355
  };
4300
4356
  }));
4301
4357
  try {
@@ -6578,6 +6634,8 @@ function markSessionWorking(record, activity) {
6578
6634
  record.isWorking = true;
6579
6635
  record.lastActivity = activity;
6580
6636
  record.workingStartedAt ||= Date.now();
6637
+ // A new attempt resolves the prior turn's failure condition at its source.
6638
+ record.lastFailureAt = undefined;
6581
6639
  metadata.touchSession(record.id, "working");
6582
6640
  if (!wasWorking)
6583
6641
  scheduleAdvertise(); // idle → working transition
@@ -6789,6 +6847,9 @@ function attachSessionListeners(record) {
6789
6847
  });
6790
6848
  }
6791
6849
  else if (turnError) {
6850
+ record.lastFailureAt = Date.now();
6851
+ metadata.touchSession(record.id, "failed");
6852
+ scheduleAdvertise();
6792
6853
  broadcast({ type: "session.error", sessionId: record.id, error: turnError });
6793
6854
  void sendNotificationHint({
6794
6855
  kind: "session_error",
@@ -7949,6 +8010,7 @@ approvals.onRequest((request) => {
7949
8010
  void sendNotificationHint({
7950
8011
  kind: "approval_requested",
7951
8012
  sessionId: request.sessionId,
8013
+ attentionId: request.id,
7952
8014
  title: "Approval needed",
7953
8015
  body: `${sessionNotifyLabel(rec)} wants to run something — tap to approve or deny.`,
7954
8016
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bivy/bivy",
3
- "version": "0.5.1-staging.63",
3
+ "version": "0.5.1-staging.65",
4
4
  "type": "module",
5
5
  "license": "FSL-1.1-ALv2",
6
6
  "description": "Run coding agents on machines you own. Source-available, self-hostable agent workspace.",