@gethmy/agent 1.11.1 → 1.12.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.
Files changed (3) hide show
  1. package/dist/cli.js +142 -47
  2. package/dist/index.js +142 -47
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -390,7 +390,7 @@ var init_types = __esm(() => {
390
390
  pickupColumns: ["To Do"],
391
391
  priorityLabels: { urgent: 100, critical: 90, bug: 50 },
392
392
  columnBoost: true,
393
- runner: "cli",
393
+ runner: "sdk",
394
394
  completion: {
395
395
  createPR: false,
396
396
  moveToColumn: "Review",
@@ -565,7 +565,7 @@ function loadDaemonConfig() {
565
565
  }
566
566
  };
567
567
  if (agent.runner !== "cli" && agent.runner !== "sdk") {
568
- agent.runner = "cli";
568
+ agent.runner = DEFAULT_AGENT_CONFIG.runner;
569
569
  }
570
570
  return {
571
571
  apiKey,
@@ -1228,7 +1228,8 @@ function fetchBaseBranch(repoRoot, baseBranch, attempts = 3, fetchImpl = (root,
1228
1228
  log.warn(TAG5, `fetch origin ${baseBranch} failed (attempt ${attempt}/${attempts})`);
1229
1229
  }
1230
1230
  }
1231
- const detail = lastErr instanceof Error ? lastErr.message : String(lastErr);
1231
+ const e = lastErr;
1232
+ const detail = e?.stderr?.toString?.().trim() || (lastErr instanceof Error ? lastErr.message : String(lastErr));
1232
1233
  throw new WorktreeBaseError(`Could not fetch origin/${baseBranch} after ${attempts} attempts — ` + `refusing to build on a stale base. ${detail}`);
1233
1234
  }
1234
1235
  function createWorktree(basePath, baseBranch, branchName) {
@@ -1350,6 +1351,16 @@ var init_worktree = __esm(() => {
1350
1351
  import { execFileSync as execFileSync4, execSync as execSync3 } from "node:child_process";
1351
1352
  import { existsSync as existsSync3 } from "node:fs";
1352
1353
  import { resolve as resolve2 } from "node:path";
1354
+ function gitErrorDetail(err) {
1355
+ const e = err;
1356
+ const stderr = e?.stderr?.toString?.().trim();
1357
+ if (stderr)
1358
+ return stderr;
1359
+ const stdout = e?.stdout?.toString?.().trim();
1360
+ if (stdout)
1361
+ return stdout;
1362
+ return err instanceof Error ? err.message : String(err);
1363
+ }
1353
1364
  function checkoutExistingBranch(basePath, branchName) {
1354
1365
  const repoRoot = execFileSync4("git", ["rev-parse", "--show-toplevel"], {
1355
1366
  encoding: "utf-8"
@@ -1370,8 +1381,8 @@ function checkoutExistingBranch(basePath, branchName) {
1370
1381
  cwd: repoRoot,
1371
1382
  stdio: "pipe"
1372
1383
  });
1373
- } catch {
1374
- throw new Error(`Failed to fetch remote branch: ${branchName}`);
1384
+ } catch (err) {
1385
+ throw new Error(`Failed to fetch remote branch ${branchName}: ${gitErrorDetail(err)}`);
1375
1386
  }
1376
1387
  try {
1377
1388
  execFileSync4("git", ["branch", "-D", branchName], {
@@ -1380,15 +1391,19 @@ function checkoutExistingBranch(basePath, branchName) {
1380
1391
  });
1381
1392
  } catch {}
1382
1393
  log.info(TAG6, `Creating review worktree: ${worktreeDir} (branch: ${branchName})`);
1383
- execFileSync4("git", [
1384
- "worktree",
1385
- "add",
1386
- "--track",
1387
- "-b",
1388
- branchName,
1389
- worktreeDir,
1390
- `origin/${branchName}`
1391
- ], { cwd: repoRoot, stdio: "pipe" });
1394
+ try {
1395
+ execFileSync4("git", [
1396
+ "worktree",
1397
+ "add",
1398
+ "--track",
1399
+ "-b",
1400
+ branchName,
1401
+ worktreeDir,
1402
+ `origin/${branchName}`
1403
+ ], { cwd: repoRoot, stdio: "pipe" });
1404
+ } catch (err) {
1405
+ throw new Error(`Failed to create review worktree for ${branchName}: ${gitErrorDetail(err)}`);
1406
+ }
1392
1407
  log.info(TAG6, "Installing dependencies in review worktree...");
1393
1408
  try {
1394
1409
  execSync3(installCommand(), {
@@ -2613,6 +2628,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
2613
2628
  reviewFindings: [],
2614
2629
  revertWarnings: []
2615
2630
  };
2631
+ commitUncommittedChanges(worktreePath, card);
2616
2632
  const hasCommits = checkHasCommits(worktreePath, config.worktree.baseBranch);
2617
2633
  if (!hasCommits) {
2618
2634
  const { maxTurnsExhausted, failureSummary } = describeNoCommitFailure(sessionStats?.cost?.numTurns ?? 0, config.claude.maxTurns);
@@ -2787,6 +2803,37 @@ function readHeadSha(worktreePath) {
2787
2803
  return null;
2788
2804
  }
2789
2805
  }
2806
+ function commitUncommittedChanges(worktreePath, card) {
2807
+ let status = "";
2808
+ try {
2809
+ status = execFileSync9("git", ["status", "--porcelain"], {
2810
+ cwd: worktreePath,
2811
+ encoding: "utf-8"
2812
+ }).trim();
2813
+ } catch (err) {
2814
+ log.warn(TAG14, `git status failed in ${worktreePath}: ${err instanceof Error ? err.message : err}`);
2815
+ return false;
2816
+ }
2817
+ if (status.length === 0)
2818
+ return false;
2819
+ const title = card.title?.trim() || "agent changes";
2820
+ const message = `#${card.short_id} ${title}`;
2821
+ try {
2822
+ execFileSync9("git", ["add", "-A"], {
2823
+ cwd: worktreePath,
2824
+ encoding: "utf-8"
2825
+ });
2826
+ execFileSync9("git", ["commit", "-m", message], {
2827
+ cwd: worktreePath,
2828
+ encoding: "utf-8"
2829
+ });
2830
+ log.warn(TAG14, `Auto-committed uncommitted worktree changes for #${card.short_id} — agent ended without committing`);
2831
+ return true;
2832
+ } catch (err) {
2833
+ log.error(TAG14, `auto-commit failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
2834
+ return false;
2835
+ }
2836
+ }
2790
2837
  function checkHasCommits(worktreePath, baseBranch) {
2791
2838
  try {
2792
2839
  const count = execFileSync9("git", ["rev-list", "--count", `origin/${baseBranch}..HEAD`], { cwd: worktreePath, encoding: "utf-8" }).trim();
@@ -6180,6 +6227,15 @@ class Worker {
6180
6227
  ...this.runLedger()
6181
6228
  });
6182
6229
  } catch {}
6230
+ const stopCost = this.lastSessionStats?.cost;
6231
+ if (stopCost) {
6232
+ const cents = Math.round(stopCost.totalCostUsd * 100);
6233
+ if (cents > 0) {
6234
+ try {
6235
+ await this.stateStore.addCost(card.id, cents);
6236
+ } catch {}
6237
+ }
6238
+ }
6183
6239
  } else if (this.runId && this.verificationFailed) {
6184
6240
  try {
6185
6241
  await this.stateStore.endRun(this.runId, "paused", {
@@ -6662,7 +6718,7 @@ class Worker {
6662
6718
  clearTimeout(this.timeoutTimer);
6663
6719
  this.timeoutTimer = null;
6664
6720
  }
6665
- if (this.worktreePath && (this.state === "error" || this.timedOut)) {
6721
+ if (this.worktreePath && (this.state === "error" || this.timedOut || this.aborted)) {
6666
6722
  try {
6667
6723
  cleanupWorktree(this.worktreePath, this.branchName ?? undefined);
6668
6724
  } catch {
@@ -7634,6 +7690,10 @@ class Watcher {
7634
7690
  readyPromise = new Promise((resolve3) => {
7635
7691
  this.readyResolve = resolve3;
7636
7692
  });
7693
+ stopping = false;
7694
+ reconnectTimer = null;
7695
+ reconnectAttempts = 0;
7696
+ broadcastGen = 0;
7637
7697
  get isConnected() {
7638
7698
  return this.connected;
7639
7699
  }
@@ -7662,7 +7722,34 @@ class Watcher {
7662
7722
  }
7663
7723
  this.supabase = createClient(this.credentials.supabaseUrl, this.credentials.supabaseAnonKey);
7664
7724
  const presenceChannel = this.supabase.channel(`board-presence-${this.projectId}`);
7665
- const channel = this.supabase.channel(`board-${this.projectId}`).on("broadcast", { event: "card_update" }, (msg) => {
7725
+ this.subscribeBroadcast();
7726
+ presenceChannel.on("presence", { event: "sync" }, () => {
7727
+ log.debug(TAG32, "Presence sync");
7728
+ }).subscribe(async (status) => {
7729
+ if (status === "SUBSCRIBED") {
7730
+ await presenceChannel.track({
7731
+ daemonId: this.daemonId,
7732
+ startedAt: new Date().toISOString(),
7733
+ userId: this.identity.userId,
7734
+ agentId: this.identity.agentId,
7735
+ userEmail: this.identity.userEmail,
7736
+ agentIdentifier: this.identity.agentIdentifier,
7737
+ agentName: this.identity.agentName
7738
+ });
7739
+ if (!isPretty() || !this.suppressStartupLogs) {
7740
+ log.info(TAG32, "Presence tracked on board-presence channel");
7741
+ }
7742
+ this.presenceTracked = true;
7743
+ this.maybeResolveReady();
7744
+ }
7745
+ });
7746
+ this.presenceChannel = presenceChannel;
7747
+ }
7748
+ subscribeBroadcast() {
7749
+ if (!this.supabase)
7750
+ return;
7751
+ const gen = ++this.broadcastGen;
7752
+ this.channel = this.supabase.channel(`board-${this.projectId}`).on("broadcast", { event: "card_update" }, (msg) => {
7666
7753
  log.debug(TAG32, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
7667
7754
  this.onCardBroadcast({
7668
7755
  event: "card_update",
@@ -7683,46 +7770,54 @@ class Watcher {
7683
7770
  this.onAgentCommand?.({ cardId, command });
7684
7771
  }
7685
7772
  }).subscribe((status) => {
7773
+ if (gen !== this.broadcastGen)
7774
+ return;
7686
7775
  if (status === "SUBSCRIBED") {
7687
7776
  this.connected = true;
7777
+ this.reconnectAttempts = 0;
7688
7778
  if (!isPretty() || !this.suppressStartupLogs) {
7689
7779
  log.info(TAG32, "Broadcast subscription active");
7690
7780
  }
7691
7781
  this.maybeResolveReady();
7692
- } else if (status === "CHANNEL_ERROR") {
7693
- this.connected = false;
7694
- log.error(TAG32, "Broadcast channel error — will rely on reconciliation");
7695
- } else if (status === "TIMED_OUT") {
7782
+ } else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT" || status === "CLOSED") {
7696
7783
  this.connected = false;
7697
- log.warn(TAG32, "Broadcast subscription timed out — retrying...");
7698
- } else if (status === "CLOSED") {
7699
- this.connected = false;
7700
- }
7701
- });
7702
- this.channel = channel;
7703
- presenceChannel.on("presence", { event: "sync" }, () => {
7704
- log.debug(TAG32, "Presence sync");
7705
- }).subscribe(async (status) => {
7706
- if (status === "SUBSCRIBED") {
7707
- await presenceChannel.track({
7708
- daemonId: this.daemonId,
7709
- startedAt: new Date().toISOString(),
7710
- userId: this.identity.userId,
7711
- agentId: this.identity.agentId,
7712
- userEmail: this.identity.userEmail,
7713
- agentIdentifier: this.identity.agentIdentifier,
7714
- agentName: this.identity.agentName
7715
- });
7716
- if (!isPretty() || !this.suppressStartupLogs) {
7717
- log.info(TAG32, "Presence tracked on board-presence channel");
7784
+ if (!this.stopping) {
7785
+ log.warn(TAG32, `Broadcast subscription ${status} scheduling reconnect`);
7786
+ this.scheduleReconnect();
7718
7787
  }
7719
- this.presenceTracked = true;
7720
- this.maybeResolveReady();
7721
7788
  }
7722
7789
  });
7723
- this.presenceChannel = presenceChannel;
7790
+ }
7791
+ scheduleReconnect() {
7792
+ if (this.stopping || this.reconnectTimer)
7793
+ return;
7794
+ const delay = Math.min(30000, 1000 * 2 ** this.reconnectAttempts);
7795
+ this.reconnectAttempts++;
7796
+ this.reconnectTimer = setTimeout(() => {
7797
+ this.reconnectTimer = null;
7798
+ this.reconnectBroadcast();
7799
+ }, delay);
7800
+ }
7801
+ async reconnectBroadcast() {
7802
+ if (this.stopping || !this.supabase)
7803
+ return;
7804
+ log.warn(TAG32, `Reconnecting broadcast subscription (attempt ${this.reconnectAttempts})`);
7805
+ if (this.channel) {
7806
+ const old = this.channel;
7807
+ this.channel = null;
7808
+ this.broadcastGen++;
7809
+ try {
7810
+ await this.supabase.removeChannel(old);
7811
+ } catch {}
7812
+ }
7813
+ this.subscribeBroadcast();
7724
7814
  }
7725
7815
  async stop() {
7816
+ this.stopping = true;
7817
+ if (this.reconnectTimer) {
7818
+ clearTimeout(this.reconnectTimer);
7819
+ this.reconnectTimer = null;
7820
+ }
7726
7821
  if (this.presenceChannel) {
7727
7822
  await this.supabase?.removeChannel(this.presenceChannel);
7728
7823
  this.presenceChannel = null;
@@ -7758,7 +7853,7 @@ import { resolve as resolve3 } from "node:path";
7758
7853
  function isTransientGitNetworkError(message) {
7759
7854
  return TRANSIENT_GIT_NETWORK_ERROR.test(message);
7760
7855
  }
7761
- function gitErrorDetail(err) {
7856
+ function gitErrorDetail2(err) {
7762
7857
  if (err && typeof err === "object" && "stderr" in err) {
7763
7858
  const stderr = String(err.stderr ?? "").trim();
7764
7859
  if (stderr)
@@ -7857,7 +7952,7 @@ function pruneFailedRemoteBranches(opts) {
7857
7952
  ...GIT_NETWORK_EXEC
7858
7953
  });
7859
7954
  } catch (err) {
7860
- const detail = gitErrorDetail(err);
7955
+ const detail = gitErrorDetail2(err);
7861
7956
  if (isTransientGitNetworkError(detail)) {
7862
7957
  log.debug(TAG33, `Remote branch GC skipped — remote unreachable: ${detail}`);
7863
7958
  return result;
@@ -7909,7 +8004,7 @@ function pruneFailedRemoteBranches(opts) {
7909
8004
  });
7910
8005
  result.removed.push(ref);
7911
8006
  } catch (err) {
7912
- const detail = gitErrorDetail(err);
8007
+ const detail = gitErrorDetail2(err);
7913
8008
  if (isTransientGitNetworkError(detail)) {
7914
8009
  log.debug(TAG33, `Remote branch GC interrupted — remote unreachable: ${detail}`);
7915
8010
  break;
package/dist/index.js CHANGED
@@ -389,7 +389,7 @@ var init_types = __esm(() => {
389
389
  pickupColumns: ["To Do"],
390
390
  priorityLabels: { urgent: 100, critical: 90, bug: 50 },
391
391
  columnBoost: true,
392
- runner: "cli",
392
+ runner: "sdk",
393
393
  completion: {
394
394
  createPR: false,
395
395
  moveToColumn: "Review",
@@ -564,7 +564,7 @@ function loadDaemonConfig() {
564
564
  }
565
565
  };
566
566
  if (agent.runner !== "cli" && agent.runner !== "sdk") {
567
- agent.runner = "cli";
567
+ agent.runner = DEFAULT_AGENT_CONFIG.runner;
568
568
  }
569
569
  return {
570
570
  apiKey,
@@ -1227,7 +1227,8 @@ function fetchBaseBranch(repoRoot, baseBranch, attempts = 3, fetchImpl = (root,
1227
1227
  log.warn(TAG5, `fetch origin ${baseBranch} failed (attempt ${attempt}/${attempts})`);
1228
1228
  }
1229
1229
  }
1230
- const detail = lastErr instanceof Error ? lastErr.message : String(lastErr);
1230
+ const e = lastErr;
1231
+ const detail = e?.stderr?.toString?.().trim() || (lastErr instanceof Error ? lastErr.message : String(lastErr));
1231
1232
  throw new WorktreeBaseError(`Could not fetch origin/${baseBranch} after ${attempts} attempts — ` + `refusing to build on a stale base. ${detail}`);
1232
1233
  }
1233
1234
  function createWorktree(basePath, baseBranch, branchName) {
@@ -1349,6 +1350,16 @@ var init_worktree = __esm(() => {
1349
1350
  import { execFileSync as execFileSync4, execSync as execSync3 } from "node:child_process";
1350
1351
  import { existsSync as existsSync3 } from "node:fs";
1351
1352
  import { resolve as resolve2 } from "node:path";
1353
+ function gitErrorDetail(err) {
1354
+ const e = err;
1355
+ const stderr = e?.stderr?.toString?.().trim();
1356
+ if (stderr)
1357
+ return stderr;
1358
+ const stdout = e?.stdout?.toString?.().trim();
1359
+ if (stdout)
1360
+ return stdout;
1361
+ return err instanceof Error ? err.message : String(err);
1362
+ }
1352
1363
  function checkoutExistingBranch(basePath, branchName) {
1353
1364
  const repoRoot = execFileSync4("git", ["rev-parse", "--show-toplevel"], {
1354
1365
  encoding: "utf-8"
@@ -1369,8 +1380,8 @@ function checkoutExistingBranch(basePath, branchName) {
1369
1380
  cwd: repoRoot,
1370
1381
  stdio: "pipe"
1371
1382
  });
1372
- } catch {
1373
- throw new Error(`Failed to fetch remote branch: ${branchName}`);
1383
+ } catch (err) {
1384
+ throw new Error(`Failed to fetch remote branch ${branchName}: ${gitErrorDetail(err)}`);
1374
1385
  }
1375
1386
  try {
1376
1387
  execFileSync4("git", ["branch", "-D", branchName], {
@@ -1379,15 +1390,19 @@ function checkoutExistingBranch(basePath, branchName) {
1379
1390
  });
1380
1391
  } catch {}
1381
1392
  log.info(TAG6, `Creating review worktree: ${worktreeDir} (branch: ${branchName})`);
1382
- execFileSync4("git", [
1383
- "worktree",
1384
- "add",
1385
- "--track",
1386
- "-b",
1387
- branchName,
1388
- worktreeDir,
1389
- `origin/${branchName}`
1390
- ], { cwd: repoRoot, stdio: "pipe" });
1393
+ try {
1394
+ execFileSync4("git", [
1395
+ "worktree",
1396
+ "add",
1397
+ "--track",
1398
+ "-b",
1399
+ branchName,
1400
+ worktreeDir,
1401
+ `origin/${branchName}`
1402
+ ], { cwd: repoRoot, stdio: "pipe" });
1403
+ } catch (err) {
1404
+ throw new Error(`Failed to create review worktree for ${branchName}: ${gitErrorDetail(err)}`);
1405
+ }
1391
1406
  log.info(TAG6, "Installing dependencies in review worktree...");
1392
1407
  try {
1393
1408
  execSync3(installCommand(), {
@@ -2612,6 +2627,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
2612
2627
  reviewFindings: [],
2613
2628
  revertWarnings: []
2614
2629
  };
2630
+ commitUncommittedChanges(worktreePath, card);
2615
2631
  const hasCommits = checkHasCommits(worktreePath, config.worktree.baseBranch);
2616
2632
  if (!hasCommits) {
2617
2633
  const { maxTurnsExhausted, failureSummary } = describeNoCommitFailure(sessionStats?.cost?.numTurns ?? 0, config.claude.maxTurns);
@@ -2786,6 +2802,37 @@ function readHeadSha(worktreePath) {
2786
2802
  return null;
2787
2803
  }
2788
2804
  }
2805
+ function commitUncommittedChanges(worktreePath, card) {
2806
+ let status = "";
2807
+ try {
2808
+ status = execFileSync9("git", ["status", "--porcelain"], {
2809
+ cwd: worktreePath,
2810
+ encoding: "utf-8"
2811
+ }).trim();
2812
+ } catch (err) {
2813
+ log.warn(TAG14, `git status failed in ${worktreePath}: ${err instanceof Error ? err.message : err}`);
2814
+ return false;
2815
+ }
2816
+ if (status.length === 0)
2817
+ return false;
2818
+ const title = card.title?.trim() || "agent changes";
2819
+ const message = `#${card.short_id} ${title}`;
2820
+ try {
2821
+ execFileSync9("git", ["add", "-A"], {
2822
+ cwd: worktreePath,
2823
+ encoding: "utf-8"
2824
+ });
2825
+ execFileSync9("git", ["commit", "-m", message], {
2826
+ cwd: worktreePath,
2827
+ encoding: "utf-8"
2828
+ });
2829
+ log.warn(TAG14, `Auto-committed uncommitted worktree changes for #${card.short_id} — agent ended without committing`);
2830
+ return true;
2831
+ } catch (err) {
2832
+ log.error(TAG14, `auto-commit failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
2833
+ return false;
2834
+ }
2835
+ }
2789
2836
  function checkHasCommits(worktreePath, baseBranch) {
2790
2837
  try {
2791
2838
  const count = execFileSync9("git", ["rev-list", "--count", `origin/${baseBranch}..HEAD`], { cwd: worktreePath, encoding: "utf-8" }).trim();
@@ -6179,6 +6226,15 @@ class Worker {
6179
6226
  ...this.runLedger()
6180
6227
  });
6181
6228
  } catch {}
6229
+ const stopCost = this.lastSessionStats?.cost;
6230
+ if (stopCost) {
6231
+ const cents = Math.round(stopCost.totalCostUsd * 100);
6232
+ if (cents > 0) {
6233
+ try {
6234
+ await this.stateStore.addCost(card.id, cents);
6235
+ } catch {}
6236
+ }
6237
+ }
6182
6238
  } else if (this.runId && this.verificationFailed) {
6183
6239
  try {
6184
6240
  await this.stateStore.endRun(this.runId, "paused", {
@@ -6661,7 +6717,7 @@ class Worker {
6661
6717
  clearTimeout(this.timeoutTimer);
6662
6718
  this.timeoutTimer = null;
6663
6719
  }
6664
- if (this.worktreePath && (this.state === "error" || this.timedOut)) {
6720
+ if (this.worktreePath && (this.state === "error" || this.timedOut || this.aborted)) {
6665
6721
  try {
6666
6722
  cleanupWorktree(this.worktreePath, this.branchName ?? undefined);
6667
6723
  } catch {
@@ -7633,6 +7689,10 @@ class Watcher {
7633
7689
  readyPromise = new Promise((resolve3) => {
7634
7690
  this.readyResolve = resolve3;
7635
7691
  });
7692
+ stopping = false;
7693
+ reconnectTimer = null;
7694
+ reconnectAttempts = 0;
7695
+ broadcastGen = 0;
7636
7696
  get isConnected() {
7637
7697
  return this.connected;
7638
7698
  }
@@ -7661,7 +7721,34 @@ class Watcher {
7661
7721
  }
7662
7722
  this.supabase = createClient(this.credentials.supabaseUrl, this.credentials.supabaseAnonKey);
7663
7723
  const presenceChannel = this.supabase.channel(`board-presence-${this.projectId}`);
7664
- const channel = this.supabase.channel(`board-${this.projectId}`).on("broadcast", { event: "card_update" }, (msg) => {
7724
+ this.subscribeBroadcast();
7725
+ presenceChannel.on("presence", { event: "sync" }, () => {
7726
+ log.debug(TAG32, "Presence sync");
7727
+ }).subscribe(async (status) => {
7728
+ if (status === "SUBSCRIBED") {
7729
+ await presenceChannel.track({
7730
+ daemonId: this.daemonId,
7731
+ startedAt: new Date().toISOString(),
7732
+ userId: this.identity.userId,
7733
+ agentId: this.identity.agentId,
7734
+ userEmail: this.identity.userEmail,
7735
+ agentIdentifier: this.identity.agentIdentifier,
7736
+ agentName: this.identity.agentName
7737
+ });
7738
+ if (!isPretty() || !this.suppressStartupLogs) {
7739
+ log.info(TAG32, "Presence tracked on board-presence channel");
7740
+ }
7741
+ this.presenceTracked = true;
7742
+ this.maybeResolveReady();
7743
+ }
7744
+ });
7745
+ this.presenceChannel = presenceChannel;
7746
+ }
7747
+ subscribeBroadcast() {
7748
+ if (!this.supabase)
7749
+ return;
7750
+ const gen = ++this.broadcastGen;
7751
+ this.channel = this.supabase.channel(`board-${this.projectId}`).on("broadcast", { event: "card_update" }, (msg) => {
7665
7752
  log.debug(TAG32, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
7666
7753
  this.onCardBroadcast({
7667
7754
  event: "card_update",
@@ -7682,46 +7769,54 @@ class Watcher {
7682
7769
  this.onAgentCommand?.({ cardId, command });
7683
7770
  }
7684
7771
  }).subscribe((status) => {
7772
+ if (gen !== this.broadcastGen)
7773
+ return;
7685
7774
  if (status === "SUBSCRIBED") {
7686
7775
  this.connected = true;
7776
+ this.reconnectAttempts = 0;
7687
7777
  if (!isPretty() || !this.suppressStartupLogs) {
7688
7778
  log.info(TAG32, "Broadcast subscription active");
7689
7779
  }
7690
7780
  this.maybeResolveReady();
7691
- } else if (status === "CHANNEL_ERROR") {
7692
- this.connected = false;
7693
- log.error(TAG32, "Broadcast channel error — will rely on reconciliation");
7694
- } else if (status === "TIMED_OUT") {
7781
+ } else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT" || status === "CLOSED") {
7695
7782
  this.connected = false;
7696
- log.warn(TAG32, "Broadcast subscription timed out — retrying...");
7697
- } else if (status === "CLOSED") {
7698
- this.connected = false;
7699
- }
7700
- });
7701
- this.channel = channel;
7702
- presenceChannel.on("presence", { event: "sync" }, () => {
7703
- log.debug(TAG32, "Presence sync");
7704
- }).subscribe(async (status) => {
7705
- if (status === "SUBSCRIBED") {
7706
- await presenceChannel.track({
7707
- daemonId: this.daemonId,
7708
- startedAt: new Date().toISOString(),
7709
- userId: this.identity.userId,
7710
- agentId: this.identity.agentId,
7711
- userEmail: this.identity.userEmail,
7712
- agentIdentifier: this.identity.agentIdentifier,
7713
- agentName: this.identity.agentName
7714
- });
7715
- if (!isPretty() || !this.suppressStartupLogs) {
7716
- log.info(TAG32, "Presence tracked on board-presence channel");
7783
+ if (!this.stopping) {
7784
+ log.warn(TAG32, `Broadcast subscription ${status} scheduling reconnect`);
7785
+ this.scheduleReconnect();
7717
7786
  }
7718
- this.presenceTracked = true;
7719
- this.maybeResolveReady();
7720
7787
  }
7721
7788
  });
7722
- this.presenceChannel = presenceChannel;
7789
+ }
7790
+ scheduleReconnect() {
7791
+ if (this.stopping || this.reconnectTimer)
7792
+ return;
7793
+ const delay = Math.min(30000, 1000 * 2 ** this.reconnectAttempts);
7794
+ this.reconnectAttempts++;
7795
+ this.reconnectTimer = setTimeout(() => {
7796
+ this.reconnectTimer = null;
7797
+ this.reconnectBroadcast();
7798
+ }, delay);
7799
+ }
7800
+ async reconnectBroadcast() {
7801
+ if (this.stopping || !this.supabase)
7802
+ return;
7803
+ log.warn(TAG32, `Reconnecting broadcast subscription (attempt ${this.reconnectAttempts})`);
7804
+ if (this.channel) {
7805
+ const old = this.channel;
7806
+ this.channel = null;
7807
+ this.broadcastGen++;
7808
+ try {
7809
+ await this.supabase.removeChannel(old);
7810
+ } catch {}
7811
+ }
7812
+ this.subscribeBroadcast();
7723
7813
  }
7724
7814
  async stop() {
7815
+ this.stopping = true;
7816
+ if (this.reconnectTimer) {
7817
+ clearTimeout(this.reconnectTimer);
7818
+ this.reconnectTimer = null;
7819
+ }
7725
7820
  if (this.presenceChannel) {
7726
7821
  await this.supabase?.removeChannel(this.presenceChannel);
7727
7822
  this.presenceChannel = null;
@@ -7757,7 +7852,7 @@ import { resolve as resolve3 } from "node:path";
7757
7852
  function isTransientGitNetworkError(message) {
7758
7853
  return TRANSIENT_GIT_NETWORK_ERROR.test(message);
7759
7854
  }
7760
- function gitErrorDetail(err) {
7855
+ function gitErrorDetail2(err) {
7761
7856
  if (err && typeof err === "object" && "stderr" in err) {
7762
7857
  const stderr = String(err.stderr ?? "").trim();
7763
7858
  if (stderr)
@@ -7856,7 +7951,7 @@ function pruneFailedRemoteBranches(opts) {
7856
7951
  ...GIT_NETWORK_EXEC
7857
7952
  });
7858
7953
  } catch (err) {
7859
- const detail = gitErrorDetail(err);
7954
+ const detail = gitErrorDetail2(err);
7860
7955
  if (isTransientGitNetworkError(detail)) {
7861
7956
  log.debug(TAG33, `Remote branch GC skipped — remote unreachable: ${detail}`);
7862
7957
  return result;
@@ -7908,7 +8003,7 @@ function pruneFailedRemoteBranches(opts) {
7908
8003
  });
7909
8004
  result.removed.push(ref);
7910
8005
  } catch (err) {
7911
- const detail = gitErrorDetail(err);
8006
+ const detail = gitErrorDetail2(err);
7912
8007
  if (isTransientGitNetworkError(detail)) {
7913
8008
  log.debug(TAG33, `Remote branch GC interrupted — remote unreachable: ${detail}`);
7914
8009
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/agent",
3
- "version": "1.11.1",
3
+ "version": "1.12.0",
4
4
  "description": "Push-based agent daemon for Harmony — watches board assignments and spawns Claude CLI workers",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",