baxian 2.0.16 → 2.0.17

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.
@@ -19,7 +19,7 @@ import { createRunner, LocalRunner, shellQuote, resolveAgentHost, workdirHostGro
19
19
  import { isTransientNetworkFailure } from './net-exec.js';
20
20
  import { imageFilename, agentHostPath, writeImageToHost } from './image-input.js';
21
21
  import { TmuxManager, ReplNotReadyError, detectStartupDialog, detectRuntimeMenu, runtimeBusyCheck, hasRuntimeReadyView, hasReplProcTitle, isShellProcTitle, hasOscTitleWorking, hasOscTitleIdle, screenAllowsTitleIdle, PaneGoneError, } from './tmux.js';
22
- import { BranchManager, DirtyWorkdirError, ReviewHeadMismatchError, isAutoDeletableTaskBranch } from './branch.js';
22
+ import { BranchManager, DirtyWorkdirError, ReviewHeadMismatchError, isAutoDeletableTaskBranch, } from './branch.js';
23
23
  import { RepoStore, createRepoStoreCache } from './repo-store.js';
24
24
  import { PhaseSignalWatcher, } from './phase-signal-watcher.js';
25
25
  import { buildPromptInline, buildGreetingPrompt, PromptSizeError, MAX_PROMPT_BYTES_ROUTE_LIMIT, } from './prompt.js';
@@ -150,6 +150,13 @@ const DEFAULT_NEED_INPUT_RETRY_INTERVAL_MS = 5_000;
150
150
  const DEFAULT_DISPATCH_SETTLE_TIMEOUT_MS = 3_000;
151
151
  const DELETE_CLEANUP_TIMEOUT_MS = 15_000;
152
152
  const PLATFORM_HEAD_SHA_RE = new RegExp(`^${SHA_HEX_SOURCE}$`);
153
+ function branchCleanupIdentity(task) {
154
+ return {
155
+ taskId: task.id,
156
+ taskBranch: task.branch,
157
+ branchCreatedByBaxian: task.branchCreatedByBaxian,
158
+ };
159
+ }
153
160
  function createSignalTokenExcluding(...excluded) {
154
161
  let token = createSignalToken();
155
162
  while (excluded.includes(token))
@@ -1701,9 +1708,7 @@ export class AgentManager {
1701
1708
  }
1702
1709
  const pendingAt = new Date().toISOString();
1703
1710
  if (boundTask && cfg.role === 'dev') {
1704
- boundTask.branchCleanupPending = { agentId, reason, updatedAt: pendingAt };
1705
- boundTask.updatedAt = pendingAt;
1706
- await this.taskStore.set(boundTask);
1711
+ await this.persistBranchCleanupOutcome(boundTask, agentId, { status: 'pending', reason });
1707
1712
  }
1708
1713
  let wrote = false;
1709
1714
  await this.agentStore.update(agentId, (latest2) => {
@@ -1769,59 +1774,8 @@ export class AgentManager {
1769
1774
  }
1770
1775
  if (cfg.role === 'dev' && boundTask) {
1771
1776
  const branches = new BranchManager(runner);
1772
- const cleanup = await branches.cleanupTaskBranch(releaseWorkdir, {
1773
- taskId: boundTask.id,
1774
- taskBranch: boundTask.branch,
1775
- branchCreatedByBaxian: boundTask.branchCreatedByBaxian,
1776
- }, () => this.assertTaskGeneration(agentId, expectedTaskId, lockToken, releaseWorkdir).then(() => undefined));
1777
- let cleanupStateChanged = false;
1778
- if (cleanup.status === 'pending') {
1779
- if (boundTask.branchCleanupPending?.agentId !== agentId
1780
- || boundTask.branchCleanupPending.reason !== cleanup.reason
1781
- || boundTask.branchCleanupSkipped !== undefined) {
1782
- boundTask.branchCleanupPending = {
1783
- agentId,
1784
- reason: cleanup.reason,
1785
- updatedAt: new Date().toISOString(),
1786
- };
1787
- delete boundTask.branchCleanupSkipped;
1788
- cleanupStateChanged = true;
1789
- }
1790
- }
1791
- else if (cleanup.status === 'skipped') {
1792
- if (boundTask.branchCleanupSkipped?.agentId !== agentId
1793
- || boundTask.branchCleanupSkipped.reason !== cleanup.reason
1794
- || boundTask.branchCleanupPending !== undefined) {
1795
- boundTask.branchCleanupSkipped = {
1796
- agentId,
1797
- reason: cleanup.reason,
1798
- updatedAt: new Date().toISOString(),
1799
- };
1800
- delete boundTask.branchCleanupPending;
1801
- cleanupStateChanged = true;
1802
- }
1803
- }
1804
- else {
1805
- if (boundTask.branchCleanupPending?.agentId === agentId) {
1806
- delete boundTask.branchCleanupPending;
1807
- cleanupStateChanged = true;
1808
- }
1809
- if (boundTask.branchCleanupSkipped?.agentId === agentId) {
1810
- delete boundTask.branchCleanupSkipped;
1811
- cleanupStateChanged = true;
1812
- }
1813
- if (cleanup.remoteTipSha) {
1814
- boundTask.branchLocalCleaned = {
1815
- remoteTipSha: cleanup.remoteTipSha,
1816
- updatedAt: new Date().toISOString(),
1817
- };
1818
- cleanupStateChanged = true;
1819
- }
1820
- }
1821
- if (cleanupStateChanged) {
1822
- boundTask.updatedAt = new Date().toISOString();
1823
- await this.taskStore.set(boundTask);
1824
- }
1777
+ const cleanup = await branches.cleanupTaskBranch(releaseWorkdir, this.cleanupIdentityFor(boundTask), () => this.assertTaskGeneration(agentId, expectedTaskId, lockToken, releaseWorkdir).then(() => undefined));
1778
+ await this.persistBranchCleanupOutcome(boundTask, agentId, cleanup);
1825
1779
  if (cleanup.status !== 'deleted')
1826
1780
  await branches.parkOnDefaultDetached(releaseWorkdir);
1827
1781
  }
@@ -1891,6 +1845,64 @@ export class AgentManager {
1891
1845
  console.warn(`[AgentManager] clearing branchLocalCleaned(${taskId}) failed:`, err);
1892
1846
  });
1893
1847
  }
1848
+ cleanupIdentityFor(task) {
1849
+ const identity = branchCleanupIdentity(task);
1850
+ if (task.status === 'merged' && task.prNumber !== undefined) {
1851
+ identity.resolveMergedHeadSha = () => this.platformFetchPrHeadSha(task.id);
1852
+ }
1853
+ return identity;
1854
+ }
1855
+ async persistBranchCleanupOutcome(task, agentId, cleanup, guard) {
1856
+ await this.withTaskLock(async () => {
1857
+ const latest = await this.taskStore.get(task.id);
1858
+ if (!latest || latest.branch !== task.branch || (guard !== undefined && !guard(latest)))
1859
+ return;
1860
+ const effective = cleanup;
1861
+ const now = new Date().toISOString();
1862
+ let changed = false;
1863
+ if (effective.status === 'pending') {
1864
+ if (latest.branchCleanupPending?.agentId !== agentId
1865
+ || latest.branchCleanupPending.reason !== effective.reason
1866
+ || latest.branchCleanupSkipped !== undefined) {
1867
+ latest.branchCleanupPending = { agentId, reason: effective.reason, updatedAt: now };
1868
+ delete latest.branchCleanupSkipped;
1869
+ changed = true;
1870
+ }
1871
+ if (effective.remoteTipSha !== undefined
1872
+ && latest.branchLocalCleaned?.remoteTipSha !== effective.remoteTipSha) {
1873
+ latest.branchLocalCleaned = { remoteTipSha: effective.remoteTipSha, updatedAt: now };
1874
+ changed = true;
1875
+ }
1876
+ }
1877
+ else if (effective.status === 'skipped') {
1878
+ if (latest.branchCleanupSkipped?.agentId !== agentId
1879
+ || latest.branchCleanupSkipped.reason !== effective.reason
1880
+ || latest.branchCleanupPending !== undefined) {
1881
+ latest.branchCleanupSkipped = { agentId, reason: effective.reason, updatedAt: now };
1882
+ delete latest.branchCleanupPending;
1883
+ changed = true;
1884
+ }
1885
+ }
1886
+ else {
1887
+ if (latest.branchCleanupPending !== undefined) {
1888
+ delete latest.branchCleanupPending;
1889
+ changed = true;
1890
+ }
1891
+ if (latest.branchCleanupSkipped !== undefined) {
1892
+ delete latest.branchCleanupSkipped;
1893
+ changed = true;
1894
+ }
1895
+ if (effective.remoteTipSha) {
1896
+ latest.branchLocalCleaned = { remoteTipSha: effective.remoteTipSha, updatedAt: now };
1897
+ changed = true;
1898
+ }
1899
+ }
1900
+ if (!changed)
1901
+ return;
1902
+ latest.updatedAt = now;
1903
+ await this.taskStore.set(latest);
1904
+ });
1905
+ }
1894
1906
  async releaseAgentIfBound(agentId, expectedTaskId, opts) {
1895
1907
  const state = await this.agentStore.get(agentId);
1896
1908
  if (state?.taskId !== expectedTaskId)
@@ -1911,50 +1923,7 @@ export class AgentManager {
1911
1923
  && task.branchCleanupSkipped === undefined)
1912
1924
  .map(task => [task.branch, task]));
1913
1925
  const owner = 'maintenance:branch-reconcile';
1914
- const persistCleanup = async (task, agentId, cleanup) => {
1915
- await this.withTaskLock(async () => {
1916
- const latest = await this.taskStore.get(task.id);
1917
- if (!latest
1918
- || latest.branch !== task.branch
1919
- || latest.branchCreatedByBaxian !== true
1920
- || !TERMINAL_STATUSES.includes(latest.status))
1921
- return;
1922
- const now = new Date().toISOString();
1923
- let changed = false;
1924
- if (cleanup.status === 'pending') {
1925
- if (latest.branchCleanupPending?.agentId !== agentId
1926
- || latest.branchCleanupPending.reason !== cleanup.reason
1927
- || latest.branchCleanupSkipped !== undefined) {
1928
- latest.branchCleanupPending = { agentId, reason: cleanup.reason, updatedAt: now };
1929
- delete latest.branchCleanupSkipped;
1930
- changed = true;
1931
- }
1932
- }
1933
- else if (cleanup.status === 'skipped') {
1934
- if (latest.branchCleanupSkipped?.agentId !== agentId
1935
- || latest.branchCleanupSkipped.reason !== cleanup.reason
1936
- || latest.branchCleanupPending !== undefined) {
1937
- latest.branchCleanupSkipped = { agentId, reason: cleanup.reason, updatedAt: now };
1938
- delete latest.branchCleanupPending;
1939
- changed = true;
1940
- }
1941
- }
1942
- else {
1943
- if (latest.branchCleanupPending !== undefined) {
1944
- delete latest.branchCleanupPending;
1945
- changed = true;
1946
- }
1947
- if (latest.branchCleanupSkipped !== undefined) {
1948
- delete latest.branchCleanupSkipped;
1949
- changed = true;
1950
- }
1951
- }
1952
- if (!changed)
1953
- return;
1954
- latest.updatedAt = now;
1955
- await this.taskStore.set(latest);
1956
- });
1957
- };
1926
+ const persistCleanup = (task, agentId, cleanup) => this.persistBranchCleanupOutcome(task, agentId, cleanup, latest => latest.branchCreatedByBaxian === true && TERMINAL_STATUSES.includes(latest.status));
1958
1927
  for (const cfg of this.agentIndex.values()) {
1959
1928
  if (cfg.role !== 'dev')
1960
1929
  continue;
@@ -1980,7 +1949,9 @@ export class AgentManager {
1980
1949
  `${err instanceof Error ? err.message : String(err)}`);
1981
1950
  continue;
1982
1951
  }
1983
- if (!refs.some(ref => candidateByBranch.has(ref.slice('refs/heads/'.length))))
1952
+ const isTarget = (task, present) => present.has(task.branch) || task.branchCleanupPending !== undefined;
1953
+ let present = new Set(refs.map(ref => ref.slice('refs/heads/'.length)));
1954
+ if (!candidateTasks.some(task => isTarget(task, present)))
1984
1955
  continue;
1985
1956
  if (this.isDeletionInFlight(cfg.id))
1986
1957
  continue;
@@ -1988,6 +1959,7 @@ export class AgentManager {
1988
1959
  if (!token)
1989
1960
  continue;
1990
1961
  const cleanupResults = [];
1962
+ let targets = [];
1991
1963
  try {
1992
1964
  if (!this.deletionGateOpen(cfg.id, genAtEntry))
1993
1965
  continue;
@@ -1997,8 +1969,9 @@ export class AgentManager {
1997
1969
  if (!(await this.lockManager.isOwner(cfg.id, owner, token)))
1998
1970
  continue;
1999
1971
  refs = await branches.listLocalTaskRefs(workdir);
2000
- refs = refs.filter(ref => candidateByBranch.has(ref.slice('refs/heads/'.length)));
2001
- if (refs.length === 0)
1972
+ present = new Set(refs.map(ref => ref.slice('refs/heads/'.length)));
1973
+ targets = [...candidateByBranch.values()].filter(task => isTarget(task, present));
1974
+ if (targets.length === 0)
2002
1975
  continue;
2003
1976
  const tmux = new TmuxManager(runner);
2004
1977
  if (await tmux.hasSession(cfg.id)) {
@@ -2007,16 +1980,8 @@ export class AgentManager {
2007
1980
  }
2008
1981
  if (!(await this.lockManager.isOwner(cfg.id, owner, token)))
2009
1982
  continue;
2010
- for (const actualRef of refs) {
2011
- const branch = actualRef.slice('refs/heads/'.length);
2012
- const task = candidateByBranch.get(branch);
2013
- if (!task)
2014
- continue;
2015
- const cleanup = await branches.cleanupTaskBranch(workdir, {
2016
- taskId: task.id,
2017
- taskBranch: task.branch,
2018
- branchCreatedByBaxian: task.branchCreatedByBaxian,
2019
- }, () => this.assertTaskLockOwner(cfg.id, owner, token));
1983
+ for (const task of targets) {
1984
+ const cleanup = await branches.cleanupTaskBranch(workdir, this.cleanupIdentityFor(task), () => this.assertTaskLockOwner(cfg.id, owner, token));
2020
1985
  cleanupResults.push({ task, cleanup });
2021
1986
  }
2022
1987
  }
@@ -2026,10 +1991,7 @@ export class AgentManager {
2026
1991
  await this.markAwaitingHuman(cfg.id, 'dirty-workdir', message, { expectedTaskId: null });
2027
1992
  }
2028
1993
  else if (err instanceof ReplNotReadyError || message.includes('stayed busy past')) {
2029
- for (const actualRef of refs) {
2030
- const task = candidateByBranch.get(actualRef.slice('refs/heads/'.length));
2031
- if (!task)
2032
- continue;
1994
+ for (const task of targets) {
2033
1995
  cleanupResults.push({
2034
1996
  task,
2035
1997
  cleanup: {