chatroom-cli 1.36.0 → 1.37.1

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.
package/dist/index.js CHANGED
@@ -13426,6 +13426,10 @@ function startSessionEventForwarder(client4, options) {
13426
13426
  const errorTarget = options.errorTarget ?? process.stderr;
13427
13427
  let cancelled = false;
13428
13428
  let doneResolve;
13429
+ let sessionStarted = false;
13430
+ const seenToolStates = new Map;
13431
+ let lastStatus;
13432
+ const agentEndCallbacks = [];
13429
13433
  const donePromise = new Promise((resolve) => {
13430
13434
  doneResolve = resolve;
13431
13435
  });
@@ -13445,31 +13449,76 @@ function startSessionEventForwarder(client4, options) {
13445
13449
  const eventSession = eventSessionId(event);
13446
13450
  if (eventSession && eventSession !== options.sessionId)
13447
13451
  continue;
13448
- if (eventSession === undefined && event.type !== "session.idle" && event.type !== "session.compacted" && event.type !== "session.error" && event.type !== "session.status" && event.type !== "file.edited")
13452
+ if (eventSession === undefined && event.type !== "message.part.updated" && event.type !== "session.idle" && event.type !== "session.compacted" && event.type !== "session.error" && event.type !== "session.status" && event.type !== "file.edited")
13449
13453
  continue;
13454
+ if (!sessionStarted) {
13455
+ sessionStarted = true;
13456
+ target.write(formatLogLine(options, "session] Started", `role: ${options.role}`) + `
13457
+ `);
13458
+ }
13450
13459
  switch (event.type) {
13451
13460
  case "message.part.updated": {
13452
13461
  const props = event.properties;
13453
13462
  const part = props?.part;
13454
- if (part?.type === "text" && part.delta) {
13455
- target.write(formatLogLine(options, "text", part.delta) + `
13463
+ if (part?.type === "text") {
13464
+ const chunk = props?.delta !== undefined && props.delta !== "" ? props.delta : part.text;
13465
+ if (chunk) {
13466
+ target.write(formatLogLine(options, "text", chunk) + `
13456
13467
  `);
13468
+ }
13469
+ } else if (part?.type === "reasoning") {
13470
+ const chunk = props?.delta !== undefined && props.delta !== "" ? props.delta : part.text;
13471
+ if (chunk) {
13472
+ target.write(formatLogLine(options, "thinking", chunk) + `
13473
+ `);
13474
+ }
13457
13475
  } else if (part?.type === "tool" && part.tool) {
13458
- const state = props?.state ?? "started";
13459
- target.write(formatLogLine(options, "tool: " + part.tool, state) + `
13476
+ let appendInput = function(base, input, tool) {
13477
+ if (!input || typeof input === "object" && Object.keys(input).length === 0) {
13478
+ return base;
13479
+ }
13480
+ const inp = input;
13481
+ if (tool === "bash" && typeof inp.command === "string") {
13482
+ return `${base}: ${inp.command}`;
13483
+ }
13484
+ const inputStr = typeof inp === "string" ? inp : JSON.stringify(inp);
13485
+ return `${base}: ${inputStr}`;
13486
+ };
13487
+ const state = typeof props?.state === "string" ? props.state : typeof part.state?.status === "string" ? part.state.status : "started";
13488
+ let payload = state;
13489
+ if (part.state?.input) {
13490
+ payload = appendInput(payload, part.state.input, part.tool);
13491
+ }
13492
+ if (state === "completed" && part.state?.time?.start !== undefined && part.state?.time?.end !== undefined) {
13493
+ const duration = ((part.state.time.end - part.state.time.start) / 1000).toFixed(1);
13494
+ payload = appendInput(`${state} (${duration}s)`, part.state.input, part.tool);
13495
+ }
13496
+ const callID = part.callID ?? "unknown";
13497
+ const seenKey = `${callID}:${state}`;
13498
+ if (!seenToolStates.has(seenKey)) {
13499
+ seenToolStates.set(seenKey, payload);
13500
+ target.write(formatLogLine(options, "tool: " + part.tool, payload) + `
13460
13501
  `);
13502
+ }
13503
+ if (state === "completed" || state === "error") {
13504
+ seenToolStates.delete(seenKey);
13505
+ }
13461
13506
  }
13462
13507
  break;
13463
13508
  }
13464
13509
  case "file.edited": {
13465
13510
  const props = event.properties;
13466
- target.write(formatLogLine(options, "file", props?.file) + `
13511
+ const kind = props?.action ?? props?.kind;
13512
+ const filePayload = kind ? `${props?.file} (${kind})` : props?.file;
13513
+ target.write(formatLogLine(options, "file", filePayload) + `
13467
13514
  `);
13468
13515
  break;
13469
13516
  }
13470
13517
  case "session.idle": {
13471
13518
  target.write(formatLogLine(options, "agent_end") + `
13472
13519
  `);
13520
+ for (const cb of agentEndCallbacks)
13521
+ cb();
13473
13522
  break;
13474
13523
  }
13475
13524
  case "session.compacted": {
@@ -13479,15 +13528,25 @@ function startSessionEventForwarder(client4, options) {
13479
13528
  }
13480
13529
  case "session.status": {
13481
13530
  const props = event.properties;
13482
- target.write(formatLogLine(options, "status", props?.status?.type) + `
13531
+ const currentStatus = props?.status?.type;
13532
+ if (currentStatus !== lastStatus) {
13533
+ lastStatus = currentStatus;
13534
+ target.write(formatLogLine(options, "status", currentStatus) + `
13483
13535
  `);
13536
+ }
13484
13537
  break;
13485
13538
  }
13486
13539
  case "session.error": {
13487
13540
  const props = event.properties;
13488
13541
  const err = props?.error;
13489
13542
  const errMsg = err?.name ? `${err.name}${err?.data?.message ? ": " + err.data.message : ""}` : String(err ?? "unknown");
13490
- errorTarget.write(formatLogLine(options, "error", errMsg) + `
13543
+ let payload = errMsg;
13544
+ if (props?.tool) {
13545
+ payload += ` [tool: ${props.tool}]`;
13546
+ } else if (props?.command) {
13547
+ payload += ` [command: ${props.command}]`;
13548
+ }
13549
+ errorTarget.write(formatLogLine(options, "error", payload) + `
13491
13550
  `);
13492
13551
  break;
13493
13552
  }
@@ -13508,7 +13567,10 @@ function startSessionEventForwarder(client4, options) {
13508
13567
  stop: () => {
13509
13568
  cancelled = true;
13510
13569
  },
13511
- done: donePromise
13570
+ done: donePromise,
13571
+ onAgentEnd: (cb) => {
13572
+ agentEndCallbacks.push(cb);
13573
+ }
13512
13574
  };
13513
13575
  }
13514
13576
 
@@ -13629,7 +13691,12 @@ var init_opencode_sdk_agent_service = __esm(() => {
13629
13691
  agent: selected.name,
13630
13692
  ...composedSystem ? { system: composedSystem } : {},
13631
13693
  parts: [{ type: "text", text: prompt }],
13632
- ...modelParts ? { model: modelParts } : {}
13694
+ ...modelParts ? { model: modelParts } : {},
13695
+ tools: {
13696
+ task: false,
13697
+ question: false,
13698
+ external_directory: false
13699
+ }
13633
13700
  }
13634
13701
  }), PROMPT_ASYNC_TIMEOUT_MS, "session.promptAsync");
13635
13702
  } catch (err) {
@@ -13688,6 +13755,9 @@ var init_opencode_sdk_agent_service = __esm(() => {
13688
13755
  },
13689
13756
  onOutput: (cb) => {
13690
13757
  outputCallbacks.push(cb);
13758
+ },
13759
+ onAgentEnd: (cb) => {
13760
+ forwarder?.onAgentEnd(cb);
13691
13761
  }
13692
13762
  };
13693
13763
  }
@@ -56809,7 +56879,7 @@ var init_telegram = __esm(() => {
56809
56879
  });
56810
56880
 
56811
56881
  // ../../services/backend/config/reliability.ts
56812
- var DAEMON_HEARTBEAT_INTERVAL_MS = 30000, AGENT_REQUEST_DEADLINE_MS = 120000;
56882
+ var DAEMON_HEARTBEAT_INTERVAL_MS = 30000, AGENT_REQUEST_DEADLINE_MS = 120000, OBSERVATION_TTL_MS = 60000, OBSERVED_SAFETY_POLL_MS = 30000;
56813
56883
 
56814
56884
  // src/events/daemon/agent/on-request-start-agent.ts
56815
56885
  async function onRequestStartAgent(ctx, event) {
@@ -56974,6 +57044,55 @@ var init_pid = __esm(() => {
56974
57044
  CHATROOM_DIR4 = join8(homedir5(), ".chatroom");
56975
57045
  });
56976
57046
 
57047
+ // src/infrastructure/git/git-state-pipeline.ts
57048
+ import { createHash as createHash3 } from "node:crypto";
57049
+
57050
+ class GitStatePipeline {
57051
+ fields;
57052
+ constructor(fields) {
57053
+ this.fields = fields;
57054
+ }
57055
+ async collect(workingDir, preCollected) {
57056
+ const results = new Map(preCollected);
57057
+ if (this.fields.length === 0)
57058
+ return results;
57059
+ const entries = await Promise.all(this.fields.filter((f) => !results.has(f.key)).map(async (field) => {
57060
+ try {
57061
+ const raw = await field.collect(workingDir);
57062
+ return { key: field.key, raw };
57063
+ } catch {
57064
+ return { key: field.key, raw: field.defaultValue };
57065
+ }
57066
+ }));
57067
+ for (const { key, raw } of entries) {
57068
+ results.set(key, raw);
57069
+ }
57070
+ return results;
57071
+ }
57072
+ computeHash(values, slim) {
57073
+ const hashInput = {};
57074
+ for (const field of this.fields) {
57075
+ if (slim && !field.includeInSlim)
57076
+ continue;
57077
+ const raw = values.get(field.key) ?? field.defaultValue;
57078
+ hashInput[field.key] = field.toHashable(raw);
57079
+ }
57080
+ return createHash3("md5").update(JSON.stringify(hashInput)).digest("hex");
57081
+ }
57082
+ toMutationArgs(values, slim) {
57083
+ const args = {};
57084
+ for (const field of this.fields) {
57085
+ if (slim && !field.includeInSlim)
57086
+ continue;
57087
+ const raw = values.get(field.key) ?? field.defaultValue;
57088
+ Object.assign(args, field.toMutationPartial(raw));
57089
+ }
57090
+ args.pipelineMode = slim ? "slim" : "full";
57091
+ return args;
57092
+ }
57093
+ }
57094
+ var init_git_state_pipeline = () => {};
57095
+
56977
57096
  // src/commands/machine/daemon-start/utils.ts
56978
57097
  function formatTimestamp() {
56979
57098
  return new Date().toISOString().replace("T", " ").substring(0, 19);
@@ -57613,6 +57732,28 @@ async function processMoreCommits(ctx, req) {
57613
57732
  });
57614
57733
  console.log(`[${formatTimestamp()}] \uD83D\uDCDC More commits appended: ${req.workingDir} (+${commits.length} commits, offset=${offset})`);
57615
57734
  }
57735
+ async function processAllPullRequests(ctx, req) {
57736
+ const pullRequests = await getAllPRs(req.workingDir);
57737
+ await ctx.deps.backend.mutation(api.workspaces.upsertAllPullRequests, {
57738
+ sessionId: ctx.sessionId,
57739
+ machineId: ctx.machineId,
57740
+ workingDir: req.workingDir,
57741
+ pullRequests
57742
+ });
57743
+ console.log(`[${formatTimestamp()}] \uD83D\uDCCB All pull requests pushed: ${req.workingDir} (${pullRequests.length} PRs)`);
57744
+ }
57745
+ async function processRecentCommits(ctx, req) {
57746
+ const commits = await getRecentCommits(req.workingDir, COMMITS_PER_PAGE, 0);
57747
+ const hasMoreCommits = commits.length >= COMMITS_PER_PAGE;
57748
+ await ctx.deps.backend.mutation(api.workspaces.upsertRecentCommits, {
57749
+ sessionId: ctx.sessionId,
57750
+ machineId: ctx.machineId,
57751
+ workingDir: req.workingDir,
57752
+ commits,
57753
+ hasMoreCommits
57754
+ });
57755
+ console.log(`[${formatTimestamp()}] \uD83D\uDCDC Recent commits pushed: ${req.workingDir} (${commits.length} commits)`);
57756
+ }
57616
57757
  async function processRequests(ctx, requests, processedRequestIds, dedupTtlMs) {
57617
57758
  const evictBefore = Date.now() - dedupTtlMs;
57618
57759
  for (const [id, ts] of processedRequestIds) {
@@ -57649,6 +57790,12 @@ async function processRequests(ctx, requests, processedRequestIds, dedupTtlMs) {
57649
57790
  case "pr_commits":
57650
57791
  await processPRCommits(ctx, req);
57651
57792
  break;
57793
+ case "all_pull_requests":
57794
+ await processAllPullRequests(ctx, req);
57795
+ break;
57796
+ case "recent_commits":
57797
+ await processRecentCommits(ctx, req);
57798
+ break;
57652
57799
  }
57653
57800
  await ctx.deps.backend.mutation(api.workspaces.updateRequestStatus, {
57654
57801
  sessionId: ctx.sessionId,
@@ -57673,7 +57820,26 @@ var init_git_subscription = __esm(() => {
57673
57820
  });
57674
57821
 
57675
57822
  // src/commands/machine/daemon-start/git-heartbeat.ts
57676
- import { createHash as createHash3 } from "node:crypto";
57823
+ function makeBranchDependentFields(branch) {
57824
+ return [
57825
+ {
57826
+ key: "openPullRequests",
57827
+ includeInSlim: true,
57828
+ collect: (wd) => getOpenPRsForBranch(wd, branch),
57829
+ toHashable: (raw) => raw.map((pr) => pr.prNumber),
57830
+ toMutationPartial: (raw) => ({ openPullRequests: raw }),
57831
+ defaultValue: []
57832
+ },
57833
+ {
57834
+ key: "headCommitStatus",
57835
+ includeInSlim: true,
57836
+ collect: (wd) => getCommitStatusChecks(wd, branch),
57837
+ toHashable: (raw) => raw,
57838
+ toMutationPartial: (raw) => ({ headCommitStatus: raw }),
57839
+ defaultValue: null
57840
+ }
57841
+ ];
57842
+ }
57677
57843
  async function pushGitState(ctx) {
57678
57844
  let workspaces;
57679
57845
  try {
@@ -57700,8 +57866,8 @@ async function pushSingleWorkspaceGitState(ctx, workingDir) {
57700
57866
  const stateKey = makeGitStateKey(ctx.machineId, workingDir);
57701
57867
  const isRepo = await isGitRepo(workingDir);
57702
57868
  if (!isRepo) {
57703
- const stateHash2 = "not_found";
57704
- if (ctx.lastPushedGitState.get(stateKey) === stateHash2)
57869
+ const stateHash = "not_found";
57870
+ if (ctx.lastPushedGitState.get(stateKey) === stateHash)
57705
57871
  return;
57706
57872
  await ctx.deps.backend.mutation(api.workspaces.upsertWorkspaceGitState, {
57707
57873
  sessionId: ctx.sessionId,
@@ -57709,20 +57875,13 @@ async function pushSingleWorkspaceGitState(ctx, workingDir) {
57709
57875
  workingDir,
57710
57876
  status: "not_found"
57711
57877
  });
57712
- ctx.lastPushedGitState.set(stateKey, stateHash2);
57878
+ ctx.lastPushedGitState.set(stateKey, stateHash);
57713
57879
  return;
57714
57880
  }
57715
- const [branchResult, dirtyResult, diffStatResult, commits, commitsAhead] = await Promise.all([
57716
- getBranch(workingDir),
57717
- isDirty(workingDir),
57718
- getDiffStat(workingDir),
57719
- getRecentCommits(workingDir, COMMITS_PER_PAGE),
57720
- getCommitsAhead(workingDir)
57721
- ]);
57722
- const remotes = await getRemotes(workingDir);
57881
+ const branchResult = await getBranch(workingDir);
57723
57882
  if (branchResult.status === "error") {
57724
- const stateHash2 = `error:${branchResult.message}`;
57725
- if (ctx.lastPushedGitState.get(stateKey) === stateHash2)
57883
+ const stateHash = `error:${branchResult.message}`;
57884
+ if (ctx.lastPushedGitState.get(stateKey) === stateHash)
57726
57885
  return;
57727
57886
  await ctx.deps.backend.mutation(api.workspaces.upsertWorkspaceGitState, {
57728
57887
  sessionId: ctx.sessionId,
@@ -57731,21 +57890,21 @@ async function pushSingleWorkspaceGitState(ctx, workingDir) {
57731
57890
  status: "error",
57732
57891
  errorMessage: branchResult.message
57733
57892
  });
57734
- ctx.lastPushedGitState.set(stateKey, stateHash2);
57893
+ ctx.lastPushedGitState.set(stateKey, stateHash);
57735
57894
  return;
57736
57895
  }
57737
57896
  if (branchResult.status === "not_found") {
57738
57897
  return;
57739
57898
  }
57740
- const openPRs = await getOpenPRsForBranch(workingDir, branchResult.branch);
57741
- const allPRs = await getAllPRs(workingDir);
57742
- const headCommitStatus = await getCommitStatusChecks(workingDir, branchResult.branch);
57743
57899
  const branch = branchResult.branch;
57744
- const isDirty2 = dirtyResult;
57745
- const diffStat = diffStatResult.status === "available" ? diffStatResult.diffStat : { filesChanged: 0, insertions: 0, deletions: 0 };
57900
+ const allFields = [branchField, ...GIT_STATE_FIELDS, ...makeBranchDependentFields(branch)];
57901
+ const pipeline2 = new GitStatePipeline(allFields);
57902
+ const preCollected = new Map([["branch", branchResult]]);
57903
+ const values = await pipeline2.collect(workingDir, preCollected);
57904
+ const commits = values.get("commits");
57746
57905
  const hasMoreCommits = commits.length >= COMMITS_PER_PAGE;
57747
- const stateHash = createHash3("md5").update(JSON.stringify({ branch, isDirty: isDirty2, diffStat, commitsAhead, shas: commits.map((c) => c.sha), prs: openPRs.map((pr) => pr.prNumber), allPrs: allPRs.map((pr) => `${pr.prNumber}:${pr.state}`), remotes: remotes.map((r) => `${r.name}:${r.url}`), headCommitStatus })).digest("hex");
57748
- if (ctx.lastPushedGitState.get(stateKey) === stateHash) {
57906
+ const hash = pipeline2.computeHash(values, false);
57907
+ if (ctx.lastPushedGitState.get(stateKey) === hash) {
57749
57908
  return;
57750
57909
  }
57751
57910
  await ctx.deps.backend.mutation(api.workspaces.upsertWorkspaceGitState, {
@@ -57753,23 +57912,73 @@ async function pushSingleWorkspaceGitState(ctx, workingDir) {
57753
57912
  machineId: ctx.machineId,
57754
57913
  workingDir,
57755
57914
  status: "available",
57756
- branch,
57757
- isDirty: isDirty2,
57758
- diffStat,
57915
+ ...pipeline2.toMutationArgs(values, false),
57759
57916
  recentCommits: commits,
57760
- hasMoreCommits,
57761
- openPullRequests: openPRs,
57762
- allPullRequests: allPRs,
57763
- remotes,
57764
- commitsAhead,
57765
- headCommitStatus
57917
+ hasMoreCommits
57766
57918
  });
57767
- ctx.lastPushedGitState.set(stateKey, stateHash);
57768
- console.log(`[${formatTimestamp()}] \uD83D\uDD00 Git state pushed: ${workingDir} (${branch}${isDirty2 ? ", dirty" : ", clean"})`);
57919
+ ctx.lastPushedGitState.set(stateKey, hash);
57920
+ console.log(`[${formatTimestamp()}] \uD83D\uDD00 Git state pushed: ${workingDir} (${branch}${values.get("isDirty") ? ", dirty" : ", clean"})`);
57769
57921
  prefetchMissingCommitDetails(ctx, workingDir, commits).catch((err) => {
57770
57922
  console.warn(`[${formatTimestamp()}] ⚠️ Commit pre-fetch failed for ${workingDir}: ${getErrorMessage(err)}`);
57771
57923
  });
57772
57924
  }
57925
+ async function pushSingleWorkspaceGitSummaryForObserved(ctx, workingDir, reason = "safety-poll") {
57926
+ const stateKey = makeGitStateKey(ctx.machineId, workingDir);
57927
+ const isRepo = await isGitRepo(workingDir);
57928
+ if (!isRepo) {
57929
+ const stateHash = "not_found";
57930
+ if (reason !== "refresh" && ctx.lastPushedGitState.get(stateKey) === stateHash)
57931
+ return;
57932
+ await ctx.deps.backend.mutation(api.workspaces.upsertWorkspaceGitState, {
57933
+ sessionId: ctx.sessionId,
57934
+ machineId: ctx.machineId,
57935
+ workingDir,
57936
+ status: "not_found"
57937
+ });
57938
+ ctx.lastPushedGitState.set(stateKey, stateHash);
57939
+ return;
57940
+ }
57941
+ const branchResult = await getBranch(workingDir);
57942
+ if (branchResult.status === "error") {
57943
+ const stateHash = `error:${branchResult.message}`;
57944
+ if (reason !== "refresh" && ctx.lastPushedGitState.get(stateKey) === stateHash)
57945
+ return;
57946
+ await ctx.deps.backend.mutation(api.workspaces.upsertWorkspaceGitState, {
57947
+ sessionId: ctx.sessionId,
57948
+ machineId: ctx.machineId,
57949
+ workingDir,
57950
+ status: "error",
57951
+ errorMessage: branchResult.message
57952
+ });
57953
+ ctx.lastPushedGitState.set(stateKey, stateHash);
57954
+ return;
57955
+ }
57956
+ if (branchResult.status === "not_found") {
57957
+ return;
57958
+ }
57959
+ const branch = branchResult.branch;
57960
+ const slimFields = [
57961
+ branchField,
57962
+ ...GIT_STATE_FIELDS.filter((f) => f.includeInSlim),
57963
+ ...makeBranchDependentFields(branch)
57964
+ ];
57965
+ const pipeline2 = new GitStatePipeline(slimFields);
57966
+ const preCollected = new Map([["branch", branchResult]]);
57967
+ const values = await pipeline2.collect(workingDir, preCollected);
57968
+ const hash = pipeline2.computeHash(values, true);
57969
+ if (reason !== "refresh" && ctx.lastPushedGitState.get(stateKey) === hash) {
57970
+ return;
57971
+ }
57972
+ await ctx.deps.backend.mutation(api.workspaces.upsertWorkspaceGitState, {
57973
+ sessionId: ctx.sessionId,
57974
+ machineId: ctx.machineId,
57975
+ workingDir,
57976
+ status: "available",
57977
+ ...pipeline2.toMutationArgs(values, true)
57978
+ });
57979
+ ctx.lastPushedGitState.set(stateKey, hash);
57980
+ console.log(`[${formatTimestamp()}] \uD83D\uDC41️ Observed git summary pushed: ${workingDir} (${branch}${values.get("isDirty") ? ", dirty" : ", clean"})${reason === "refresh" ? " [refresh]" : ""}`);
57981
+ }
57773
57982
  async function prefetchMissingCommitDetails(ctx, workingDir, commits) {
57774
57983
  if (commits.length === 0)
57775
57984
  return;
@@ -57840,11 +58049,87 @@ async function prefetchSingleCommit(ctx, workingDir, sha, commits) {
57840
58049
  });
57841
58050
  console.log(`[${formatTimestamp()}] ✅ Pre-fetched: ${sha.slice(0, 7)} in ${workingDir}`);
57842
58051
  }
58052
+ var branchField, GIT_STATE_FIELDS;
57843
58053
  var init_git_heartbeat = __esm(() => {
58054
+ init_git_state_pipeline();
57844
58055
  init_git_subscription();
57845
58056
  init_api3();
57846
58057
  init_git_reader();
57847
58058
  init_convex_error();
58059
+ branchField = {
58060
+ key: "branch",
58061
+ includeInSlim: true,
58062
+ collect: () => {
58063
+ throw new Error("branch must be pre-collected");
58064
+ },
58065
+ toHashable: (raw) => {
58066
+ const r = raw;
58067
+ return r.status === "available" ? r.branch : "unknown";
58068
+ },
58069
+ toMutationPartial: (raw) => {
58070
+ const r = raw;
58071
+ return r.status === "available" ? { branch: r.branch } : {};
58072
+ },
58073
+ defaultValue: { status: "not_found" }
58074
+ };
58075
+ GIT_STATE_FIELDS = [
58076
+ {
58077
+ key: "isDirty",
58078
+ includeInSlim: true,
58079
+ collect: (wd) => isDirty(wd),
58080
+ toHashable: (raw) => raw,
58081
+ toMutationPartial: (raw) => ({ isDirty: raw }),
58082
+ defaultValue: false
58083
+ },
58084
+ {
58085
+ key: "diffStat",
58086
+ includeInSlim: false,
58087
+ collect: (wd) => getDiffStat(wd),
58088
+ toHashable: (raw) => {
58089
+ const r = raw;
58090
+ return r.status === "available" ? r.diffStat : { filesChanged: 0, insertions: 0, deletions: 0 };
58091
+ },
58092
+ toMutationPartial: (raw) => {
58093
+ const r = raw;
58094
+ return {
58095
+ diffStat: r.status === "available" ? r.diffStat : { filesChanged: 0, insertions: 0, deletions: 0 }
58096
+ };
58097
+ },
58098
+ defaultValue: { status: "not_found" }
58099
+ },
58100
+ {
58101
+ key: "commits",
58102
+ includeInSlim: false,
58103
+ collect: (wd) => getRecentCommits(wd, COMMITS_PER_PAGE),
58104
+ toHashable: (raw) => raw.map((c) => c.sha),
58105
+ toMutationPartial: () => ({}),
58106
+ defaultValue: []
58107
+ },
58108
+ {
58109
+ key: "commitsAhead",
58110
+ includeInSlim: false,
58111
+ collect: (wd) => getCommitsAhead(wd),
58112
+ toHashable: (raw) => raw,
58113
+ toMutationPartial: (raw) => ({ commitsAhead: raw }),
58114
+ defaultValue: 0
58115
+ },
58116
+ {
58117
+ key: "remotes",
58118
+ includeInSlim: false,
58119
+ collect: (wd) => getRemotes(wd),
58120
+ toHashable: (raw) => raw.map((r) => `${r.name}:${r.url}`),
58121
+ toMutationPartial: (raw) => ({ remotes: raw }),
58122
+ defaultValue: []
58123
+ },
58124
+ {
58125
+ key: "allPullRequests",
58126
+ includeInSlim: false,
58127
+ collect: (wd) => getAllPRs(wd),
58128
+ toHashable: (raw) => raw.map((pr) => `${pr.prNumber}:${pr.state}`),
58129
+ toMutationPartial: (raw) => ({ allPullRequests: raw }),
58130
+ defaultValue: []
58131
+ }
58132
+ ];
57848
58133
  });
57849
58134
 
57850
58135
  // src/infrastructure/services/workspace/workspace-resolver.ts
@@ -58485,6 +58770,176 @@ var init_file_tree_subscription = __esm(() => {
58485
58770
  init_convex_error();
58486
58771
  });
58487
58772
 
58773
+ // src/commands/machine/daemon-start/observed-sync.ts
58774
+ function startObservedSyncSubscription(ctx, wsClient2) {
58775
+ console.log(`[${formatTimestamp()}] \uD83D\uDC41️ Starting observed-sync subscription (reactive)`);
58776
+ const observedWorkingDirs = new Map;
58777
+ const chatroomRefreshState = new Map;
58778
+ const skippedPushCount = new Map;
58779
+ const pendingRefresh = new Map;
58780
+ let stopped = false;
58781
+ let reconcileInFlight = false;
58782
+ const unsubscribe = wsClient2.onUpdate(api.machines.getObservedChatroomsForMachine, {
58783
+ sessionId: ctx.sessionId,
58784
+ machineId: ctx.machineId
58785
+ }, (observed) => {
58786
+ if (stopped)
58787
+ return;
58788
+ handleObservedChange(observed ?? []);
58789
+ }, (err) => {
58790
+ console.warn(`[${formatTimestamp()}] ⚠️ Observed-sync subscription error: ${getErrorMessage(err)}`);
58791
+ });
58792
+ const reconcileIntervalMs = Math.max(OBSERVATION_TTL_MS / 2, OBSERVED_SAFETY_POLL_MS);
58793
+ const reconcileTimer = setInterval(() => {
58794
+ if (stopped || reconcileInFlight)
58795
+ return;
58796
+ reconcileInFlight = true;
58797
+ ctx.deps.backend.query(api.machines.getObservedChatroomsForMachine, {
58798
+ sessionId: ctx.sessionId,
58799
+ machineId: ctx.machineId
58800
+ }).then((observed) => {
58801
+ if (!stopped)
58802
+ handleObservedChange(observed ?? []);
58803
+ }).catch((err) => {
58804
+ console.warn(`[${formatTimestamp()}] ⚠️ Observed-sync reconcile query failed: ${getErrorMessage(err)}`);
58805
+ }).finally(() => {
58806
+ reconcileInFlight = false;
58807
+ });
58808
+ }, reconcileIntervalMs);
58809
+ console.log(`[${formatTimestamp()}] \uD83D\uDC41️ Observed-sync subscription started`);
58810
+ return {
58811
+ stop: () => {
58812
+ stopped = true;
58813
+ unsubscribe();
58814
+ clearInterval(reconcileTimer);
58815
+ for (const [wd, state] of observedWorkingDirs) {
58816
+ clearInterval(state.intervalHandle);
58817
+ const skips = skippedPushCount.get(wd) ?? 0;
58818
+ if (skips > 0) {
58819
+ console.log(`[${formatTimestamp()}] \uD83D\uDC41️ Stopped observing ${wd} (skipped ${skips} overlapping pushes)`);
58820
+ } else {
58821
+ console.log(`[${formatTimestamp()}] \uD83D\uDC41️ Stopped observing ${wd}`);
58822
+ }
58823
+ }
58824
+ observedWorkingDirs.clear();
58825
+ skippedPushCount.clear();
58826
+ pendingRefresh.clear();
58827
+ console.log(`[${formatTimestamp()}] \uD83D\uDC41️ Observed-sync subscription stopped`);
58828
+ }
58829
+ };
58830
+ function handleObservedChange(observed) {
58831
+ const newWorkingDirs = new Set;
58832
+ const refreshedWorkingDirs = new Set;
58833
+ for (const chatroom of observed) {
58834
+ const chatroomId = chatroom.chatroomId;
58835
+ const currentRefresh = chatroom.lastRefreshedAt;
58836
+ const previous = chatroomRefreshState.get(chatroomId);
58837
+ const wasRefreshed = currentRefresh !== null && currentRefresh !== undefined && (previous === undefined || (previous.lastRefreshedAt ?? 0) < currentRefresh);
58838
+ if (wasRefreshed) {
58839
+ chatroomRefreshState.set(chatroomId, { lastRefreshedAt: currentRefresh });
58840
+ for (const wd of chatroom.workingDirs) {
58841
+ refreshedWorkingDirs.add(wd);
58842
+ }
58843
+ }
58844
+ for (const wd of chatroom.workingDirs) {
58845
+ newWorkingDirs.add(wd);
58846
+ }
58847
+ }
58848
+ for (const [chatroomId] of chatroomRefreshState) {
58849
+ const stillObserved = observed.some((c) => c.chatroomId === chatroomId);
58850
+ if (!stillObserved) {
58851
+ chatroomRefreshState.delete(chatroomId);
58852
+ }
58853
+ }
58854
+ const currentWorkingDirs = new Set(observedWorkingDirs.keys());
58855
+ let addedCount = 0;
58856
+ let removedCount = 0;
58857
+ for (const wd of currentWorkingDirs) {
58858
+ if (!newWorkingDirs.has(wd)) {
58859
+ const state = observedWorkingDirs.get(wd);
58860
+ if (state) {
58861
+ clearInterval(state.intervalHandle);
58862
+ observedWorkingDirs.delete(wd);
58863
+ const skips = skippedPushCount.get(wd) ?? 0;
58864
+ if (skips > 0) {
58865
+ console.log(`[${formatTimestamp()}] \uD83D\uDC41️ Stopped observing ${wd} (skipped ${skips} overlapping pushes)`);
58866
+ } else {
58867
+ console.log(`[${formatTimestamp()}] \uD83D\uDC41️ Stopped observing ${wd}`);
58868
+ }
58869
+ skippedPushCount.delete(wd);
58870
+ pendingRefresh.delete(wd);
58871
+ removedCount++;
58872
+ }
58873
+ }
58874
+ }
58875
+ for (const wd of newWorkingDirs) {
58876
+ if (!observedWorkingDirs.has(wd)) {
58877
+ observedWorkingDirs.set(wd, {
58878
+ intervalHandle: setInterval(() => {
58879
+ schedulePushForWorkingDir(wd, "safety-poll");
58880
+ }, OBSERVED_SAFETY_POLL_MS),
58881
+ pushInFlight: false
58882
+ });
58883
+ console.log(`[${formatTimestamp()}] \uD83D\uDC41️ Started observing ${wd}`);
58884
+ schedulePushForWorkingDir(wd, "safety-poll");
58885
+ addedCount++;
58886
+ }
58887
+ }
58888
+ if (addedCount > 0 || removedCount > 0) {
58889
+ console.log(`[${formatTimestamp()}] \uD83D\uDC41️ Observing ${observedWorkingDirs.size} working dir(s)`);
58890
+ }
58891
+ for (const wd of refreshedWorkingDirs) {
58892
+ if (observedWorkingDirs.has(wd)) {
58893
+ console.log(`[${formatTimestamp()}] \uD83D\uDD04 Refresh triggered for ${wd}`);
58894
+ schedulePushForWorkingDir(wd, "refresh");
58895
+ }
58896
+ }
58897
+ }
58898
+ function schedulePushForWorkingDir(workingDir, reason = "safety-poll") {
58899
+ if (stopped)
58900
+ return;
58901
+ const state = observedWorkingDirs.get(workingDir);
58902
+ if (!state)
58903
+ return;
58904
+ if (state.pushInFlight) {
58905
+ if (reason === "refresh") {
58906
+ pendingRefresh.set(workingDir, true);
58907
+ }
58908
+ const current = skippedPushCount.get(workingDir) ?? 0;
58909
+ skippedPushCount.set(workingDir, current + 1);
58910
+ console.log(`[${formatTimestamp()}] \uD83D\uDC41️ Skipping observed push for ${workingDir} (${reason}, in flight)`);
58911
+ return;
58912
+ }
58913
+ state.pushInFlight = true;
58914
+ pushForWorkingDir(workingDir, reason).catch((err) => {
58915
+ console.warn(`[${formatTimestamp()}] ⚠️ Push failed for ${workingDir}: ${getErrorMessage(err)}`);
58916
+ }).finally(() => {
58917
+ const s = observedWorkingDirs.get(workingDir);
58918
+ if (s) {
58919
+ s.pushInFlight = false;
58920
+ if (pendingRefresh.get(workingDir)) {
58921
+ pendingRefresh.delete(workingDir);
58922
+ schedulePushForWorkingDir(workingDir, "refresh");
58923
+ }
58924
+ }
58925
+ });
58926
+ }
58927
+ async function pushForWorkingDir(workingDir, reason = "safety-poll") {
58928
+ await pushSingleWorkspaceGitSummaryForObserved(ctx, workingDir, reason).catch((err) => {
58929
+ console.warn(`[${formatTimestamp()}] ⚠️ Observed git summary push failed for ${workingDir}: ${getErrorMessage(err)}`);
58930
+ });
58931
+ await pushSingleWorkspaceCommands(ctx, workingDir).catch((err) => {
58932
+ console.warn(`[${formatTimestamp()}] ⚠️ Command sync failed for ${workingDir}: ${getErrorMessage(err)}`);
58933
+ });
58934
+ }
58935
+ }
58936
+ var init_observed_sync = __esm(() => {
58937
+ init_api3();
58938
+ init_git_heartbeat();
58939
+ init_command_sync_heartbeat();
58940
+ init_convex_error();
58941
+ });
58942
+
58488
58943
  // src/commands/machine/daemon-start/handlers/ping.ts
58489
58944
  function handlePing() {
58490
58945
  console.log(` ↪ Responding: pong`);
@@ -58494,6 +58949,13 @@ function handlePing() {
58494
58949
  // src/commands/machine/daemon-start/handlers/command-runner.ts
58495
58950
  import { spawn as spawn3 } from "node:child_process";
58496
58951
  import { access as access3 } from "node:fs/promises";
58952
+ function evictStalePendingStops() {
58953
+ const evictBefore = Date.now() - PENDING_STOP_TTL_MS;
58954
+ for (const [runId, ts] of pendingStops) {
58955
+ if (ts < evictBefore)
58956
+ pendingStops.delete(runId);
58957
+ }
58958
+ }
58497
58959
  async function reportRunFailed(ctx, runId, reason) {
58498
58960
  try {
58499
58961
  await ctx.deps.backend.mutation(api.commands.updateRunStatus, {
@@ -58538,6 +59000,21 @@ async function onCommandRun(ctx, event) {
58538
59000
  console.log(`[${formatTimestamp()}] ⚠️ Command already running: ${runIdStr}`);
58539
59001
  return;
58540
59002
  }
59003
+ if (pendingStops.has(runIdStr)) {
59004
+ pendingStops.delete(runIdStr);
59005
+ console.log(`[${formatTimestamp()}] ⏭️ Skipping command run due to pending stop: ${commandName} (${runIdStr})`);
59006
+ try {
59007
+ await ctx.deps.backend.mutation(api.commands.updateRunStatus, {
59008
+ sessionId: ctx.sessionId,
59009
+ machineId: ctx.machineId,
59010
+ runId,
59011
+ status: "stopped"
59012
+ });
59013
+ } catch (err) {
59014
+ console.warn(`[${formatTimestamp()}] ⚠️ Failed to update status to stopped for pending-stop skip: ${getErrorMessage(err)}`);
59015
+ }
59016
+ return;
59017
+ }
58541
59018
  console.log(`[${formatTimestamp()}] \uD83D\uDE80 Running command: ${commandName} → ${script}`);
58542
59019
  if (!workingDir.startsWith("/")) {
58543
59020
  console.error(`[${formatTimestamp()}] ❌ Rejected command: workingDir is not absolute: ${workingDir}`);
@@ -58557,6 +59034,34 @@ async function onCommandRun(ctx, event) {
58557
59034
  stdio: ["ignore", "pipe", "pipe"],
58558
59035
  detached: true
58559
59036
  });
59037
+ const timeoutTimer = setTimeout(() => {
59038
+ console.log(`[${formatTimestamp()}] ⏰ Command timed out after ${DEFAULT_COMMAND_TIMEOUT_MS / 60000} minutes: ${commandName} (runId: ${runIdStr})`);
59039
+ const pid = child.pid;
59040
+ if (pid) {
59041
+ try {
59042
+ process.kill(-pid, "SIGTERM");
59043
+ } catch {
59044
+ child.kill("SIGTERM");
59045
+ }
59046
+ } else {
59047
+ child.kill("SIGTERM");
59048
+ }
59049
+ setTimeout(() => {
59050
+ if (!runningProcesses.has(runIdStr))
59051
+ return;
59052
+ console.log(`[${formatTimestamp()}] \uD83D\uDD2A Force-killing timed-out process: ${runIdStr}`);
59053
+ if (pid) {
59054
+ try {
59055
+ process.kill(-pid, "SIGKILL");
59056
+ } catch {
59057
+ child.kill("SIGKILL");
59058
+ }
59059
+ } else {
59060
+ child.kill("SIGKILL");
59061
+ }
59062
+ }, FORCE_KILL_DELAY_MS);
59063
+ }, DEFAULT_COMMAND_TIMEOUT_MS);
59064
+ timeoutTimer.unref?.();
58560
59065
  const tracked = {
58561
59066
  process: child,
58562
59067
  runId: runIdStr,
@@ -58564,7 +59069,8 @@ async function onCommandRun(ctx, event) {
58564
59069
  chunkIndex: 0,
58565
59070
  flushTimer: setInterval(() => {
58566
59071
  flushOutput(ctx, tracked).catch(() => {});
58567
- }, OUTPUT_FLUSH_INTERVAL_MS)
59072
+ }, OUTPUT_FLUSH_INTERVAL_MS),
59073
+ timeoutTimer
58568
59074
  };
58569
59075
  tracked.flushTimer.unref?.();
58570
59076
  runningProcesses.set(runIdStr, tracked);
@@ -58589,6 +59095,8 @@ async function onCommandRun(ctx, event) {
58589
59095
  console.log(`[${formatTimestamp()}] \uD83C\uDFC1 Command exited: ${commandName} (code=${code2}, signal=${signal})`);
58590
59096
  await flushOutput(ctx, tracked).catch(() => {});
58591
59097
  clearInterval(tracked.flushTimer);
59098
+ if (tracked.timeoutTimer)
59099
+ clearTimeout(tracked.timeoutTimer);
58592
59100
  runningProcesses.delete(runIdStr);
58593
59101
  const status = code2 === 0 ? "completed" : signal ? "stopped" : "failed";
58594
59102
  try {
@@ -58606,6 +59114,8 @@ async function onCommandRun(ctx, event) {
58606
59114
  child.on("error", async (err) => {
58607
59115
  console.error(`[${formatTimestamp()}] ❌ Command spawn failed: ${commandName}: ${err.message}`);
58608
59116
  clearInterval(tracked.flushTimer);
59117
+ if (tracked.timeoutTimer)
59118
+ clearTimeout(tracked.timeoutTimer);
58609
59119
  runningProcesses.delete(runIdStr);
58610
59120
  try {
58611
59121
  await ctx.deps.backend.mutation(api.commands.updateRunStatus, {
@@ -58624,6 +59134,8 @@ async function onCommandStop(ctx, event) {
58624
59134
  const tracked = runningProcesses.get(runIdStr);
58625
59135
  if (!tracked) {
58626
59136
  console.log(`[${formatTimestamp()}] ⚠️ No running process found for run: ${runIdStr}`);
59137
+ pendingStops.set(runIdStr, Date.now());
59138
+ console.log(`[${formatTimestamp()}] \uD83D\uDCDD Registered pending stop for run: ${runIdStr}`);
58627
59139
  try {
58628
59140
  await ctx.deps.backend.mutation(api.commands.updateRunStatus, {
58629
59141
  sessionId: ctx.sessionId,
@@ -58633,11 +59145,16 @@ async function onCommandStop(ctx, event) {
58633
59145
  });
58634
59146
  console.log(`[${formatTimestamp()}] \uD83D\uDCDD Marked orphaned run as stopped: ${runIdStr}`);
58635
59147
  } catch (err) {
58636
- console.warn(`[${formatTimestamp()}] ⚠️ Failed to mark orphaned run as stopped: ${getErrorMessage(err)}`);
59148
+ console.warn(`[${formatTimestamp()}] ⚠️ Failed to mark orphaned run as stopped (will retry): ${getErrorMessage(err)}`);
59149
+ throw err;
58637
59150
  }
58638
59151
  return;
58639
59152
  }
58640
59153
  console.log(`[${formatTimestamp()}] \uD83D\uDED1 Stopping command run: ${runIdStr}`);
59154
+ if (tracked.timeoutTimer) {
59155
+ clearTimeout(tracked.timeoutTimer);
59156
+ tracked.timeoutTimer = null;
59157
+ }
58641
59158
  const pid = tracked.process.pid;
58642
59159
  if (pid) {
58643
59160
  try {
@@ -58649,7 +59166,7 @@ async function onCommandStop(ctx, event) {
58649
59166
  tracked.process.kill("SIGTERM");
58650
59167
  }
58651
59168
  setTimeout(() => {
58652
- if (tracked.process.killed)
59169
+ if (!runningProcesses.has(runIdStr))
58653
59170
  return;
58654
59171
  console.log(`[${formatTimestamp()}] \uD83D\uDD2A Force-killing process: ${runIdStr}`);
58655
59172
  if (pid) {
@@ -58661,14 +59178,55 @@ async function onCommandStop(ctx, event) {
58661
59178
  } else {
58662
59179
  tracked.process.kill("SIGKILL");
58663
59180
  }
58664
- }, 5000);
59181
+ }, FORCE_KILL_DELAY_MS);
59182
+ }
59183
+ async function shutdownAllCommands(ctx) {
59184
+ if (runningProcesses.size === 0)
59185
+ return;
59186
+ console.log(`[${formatTimestamp()}] Shutting down ${runningProcesses.size} running command(s)...`);
59187
+ for (const [, tracked] of runningProcesses) {
59188
+ clearInterval(tracked.flushTimer);
59189
+ if (tracked.timeoutTimer)
59190
+ clearTimeout(tracked.timeoutTimer);
59191
+ await flushOutput(ctx, tracked).catch(() => {});
59192
+ const pid = tracked.process.pid;
59193
+ if (pid) {
59194
+ try {
59195
+ process.kill(-pid, "SIGTERM");
59196
+ } catch {
59197
+ tracked.process.kill("SIGTERM");
59198
+ }
59199
+ } else {
59200
+ tracked.process.kill("SIGTERM");
59201
+ }
59202
+ }
59203
+ await new Promise((resolve5) => {
59204
+ const t = setTimeout(resolve5, 3000);
59205
+ t.unref?.();
59206
+ });
59207
+ for (const [, tracked] of runningProcesses) {
59208
+ const pid = tracked.process.pid;
59209
+ if (pid) {
59210
+ try {
59211
+ process.kill(-pid, "SIGKILL");
59212
+ } catch {}
59213
+ } else {
59214
+ try {
59215
+ tracked.process.kill("SIGKILL");
59216
+ } catch {}
59217
+ }
59218
+ }
59219
+ runningProcesses.clear();
59220
+ console.log(`[${formatTimestamp()}] All commands stopped`);
58665
59221
  }
58666
- var runningProcesses, OUTPUT_FLUSH_INTERVAL_MS = 3000, MAX_BUFFER_SIZE;
59222
+ var runningProcesses, pendingStops, PENDING_STOP_TTL_MS = 60000, OUTPUT_FLUSH_INTERVAL_MS = 3000, MAX_BUFFER_SIZE, DEFAULT_COMMAND_TIMEOUT_MS, FORCE_KILL_DELAY_MS = 5000;
58667
59223
  var init_command_runner = __esm(() => {
58668
59224
  init_api3();
58669
59225
  init_convex_error();
58670
59226
  runningProcesses = new Map;
59227
+ pendingStops = new Map;
58671
59228
  MAX_BUFFER_SIZE = 100 * 1024;
59229
+ DEFAULT_COMMAND_TIMEOUT_MS = 30 * 60 * 1000;
58672
59230
  });
58673
59231
 
58674
59232
  // src/commands/machine/daemon-start/handlers/state-recovery.ts
@@ -58712,6 +59270,14 @@ var init_state_recovery = __esm(() => {
58712
59270
  init_api3();
58713
59271
  });
58714
59272
 
59273
+ // src/commands/machine/daemon-start/capabilities-snapshot.ts
59274
+ function harnessCapabilitiesFingerprint(harnesses, versions) {
59275
+ const h = [...harnesses].sort().join("\x01");
59276
+ const keys = Object.keys(versions).sort();
59277
+ const v3 = keys.map((k) => `${k}:${JSON.stringify(versions[k] ?? null)}`).join("\x02");
59278
+ return `${h}::${v3}`;
59279
+ }
59280
+
58715
59281
  // src/infrastructure/local-api/routes/identity.ts
58716
59282
  async function handleIdentity(_req, ctx) {
58717
59283
  const identity = {
@@ -59727,6 +60293,15 @@ var init_agent_process_manager = __esm(() => {
59727
60293
  init_api3();
59728
60294
  });
59729
60295
 
60296
+ // ../../services/backend/config/featureFlags.ts
60297
+ var featureFlags;
60298
+ var init_featureFlags = __esm(() => {
60299
+ featureFlags = {
60300
+ observedSyncEnabled: false,
60301
+ disableLogin: false
60302
+ };
60303
+ });
60304
+
59730
60305
  // src/commands/machine/daemon-start/init.ts
59731
60306
  import { stat as stat2 } from "node:fs/promises";
59732
60307
  async function discoverModels(agentServices) {
@@ -59900,6 +60475,17 @@ async function recoverState(ctx) {
59900
60475
  } catch (e) {
59901
60476
  console.log(` ⚠️ Failed to clear stale PIDs: ${getErrorMessage(e)}`);
59902
60477
  }
60478
+ try {
60479
+ const runResult = await ctx.deps.backend.mutation(api.commands.clearStaleCommandRuns, {
60480
+ sessionId: ctx.sessionId,
60481
+ machineId: ctx.machineId
60482
+ });
60483
+ if (runResult.clearedCount > 0) {
60484
+ console.log(` \uD83E\uDDF9 Cleared ${runResult.clearedCount} stale command run(s) from backend`);
60485
+ }
60486
+ } catch (e) {
60487
+ console.log(` ⚠️ Failed to clear stale command runs: ${getErrorMessage(e)}`);
60488
+ }
59903
60489
  }
59904
60490
  async function initDaemon() {
59905
60491
  if (!acquireLock()) {
@@ -59949,7 +60535,9 @@ async function initDaemon() {
59949
60535
  events,
59950
60536
  agentServices,
59951
60537
  lastPushedGitState: new Map,
59952
- lastPushedModels: availableModels
60538
+ lastPushedModels: availableModels,
60539
+ lastPushedHarnessFingerprint: harnessCapabilitiesFingerprint(config3.availableHarnesses, config3.harnessVersions),
60540
+ observedSyncEnabled: featureFlags.observedSyncEnabled ?? false
59953
60541
  };
59954
60542
  registerEventListeners(ctx);
59955
60543
  logStartup(ctx, availableModels);
@@ -59991,11 +60579,13 @@ var init_init2 = __esm(() => {
59991
60579
  init_convex_error();
59992
60580
  init_version();
59993
60581
  init_pid();
60582
+ init_featureFlags();
59994
60583
  AUTH_WAIT_TIMEOUT_MS = 5 * 60 * 1000;
59995
60584
  });
59996
60585
 
59997
60586
  // src/events/lifecycle/on-daemon-shutdown.ts
59998
60587
  async function onDaemonShutdown(ctx) {
60588
+ await shutdownAllCommands(ctx);
59999
60589
  const activeAgents = ctx.deps.agentProcessManager.listActive();
60000
60590
  if (activeAgents.length > 0) {
60001
60591
  console.log(`[${formatTimestamp()}] Stopping ${activeAgents.length} agent(s)...`);
@@ -60024,6 +60614,7 @@ async function onDaemonShutdown(ctx) {
60024
60614
  }
60025
60615
  var init_on_daemon_shutdown = __esm(() => {
60026
60616
  init_api3();
60617
+ init_command_runner();
60027
60618
  });
60028
60619
 
60029
60620
  // src/infrastructure/git/git-writer.ts
@@ -60215,15 +60806,18 @@ function formatModelMap(map) {
60215
60806
  return Object.entries(map).map(([harness, models]) => `${harness}: ${models.join(", ")}`).join("; ");
60216
60807
  }
60217
60808
  async function refreshModels(ctx) {
60218
- if (!ctx.config)
60219
- return;
60809
+ if (!ctx.config) {
60810
+ return { kind: "noop" };
60811
+ }
60220
60812
  const models = await discoverModels(ctx.agentServices);
60221
60813
  const freshConfig = ensureMachineRegistered();
60222
60814
  ctx.config.availableHarnesses = freshConfig.availableHarnesses;
60223
60815
  ctx.config.harnessVersions = freshConfig.harnessVersions;
60224
- const diff = diffModels(ctx.lastPushedModels, models);
60225
- if (!diff.hasChanges) {
60226
- return;
60816
+ const modelDiff = diffModels(ctx.lastPushedModels, models);
60817
+ const nextHarnessFingerprint = harnessCapabilitiesFingerprint(ctx.config.availableHarnesses, ctx.config.harnessVersions);
60818
+ const harnessFingerprintChanged = ctx.lastPushedHarnessFingerprint !== null && nextHarnessFingerprint !== ctx.lastPushedHarnessFingerprint;
60819
+ if (!modelDiff.hasChanges && !harnessFingerprintChanged) {
60820
+ return { kind: "skipped_no_changes" };
60227
60821
  }
60228
60822
  const totalCount = Object.values(models).flat().length;
60229
60823
  try {
@@ -60235,15 +60829,19 @@ async function refreshModels(ctx) {
60235
60829
  availableModels: models
60236
60830
  });
60237
60831
  ctx.lastPushedModels = models;
60238
- if (Object.keys(diff.added).length > 0) {
60239
- console.log(`[${formatTimestamp()}] New models detected — ${formatModelMap(diff.added)}`);
60832
+ ctx.lastPushedHarnessFingerprint = nextHarnessFingerprint;
60833
+ if (Object.keys(modelDiff.added).length > 0) {
60834
+ console.log(`[${formatTimestamp()}] ➕ New models detected — ${formatModelMap(modelDiff.added)}`);
60240
60835
  }
60241
- if (Object.keys(diff.removed).length > 0) {
60242
- console.log(`[${formatTimestamp()}] ➖ Models no longer available — ${formatModelMap(diff.removed)}`);
60836
+ if (Object.keys(modelDiff.removed).length > 0) {
60837
+ console.log(`[${formatTimestamp()}] ➖ Models no longer available — ${formatModelMap(modelDiff.removed)}`);
60243
60838
  }
60244
60839
  console.log(`[${formatTimestamp()}] \uD83D\uDD04 Model refresh pushed: ${totalCount > 0 ? `${totalCount} models` : "none discovered"}`);
60840
+ return { kind: "pushed" };
60245
60841
  } catch (error) {
60246
- console.warn(`[${formatTimestamp()}] ⚠️ Model refresh failed: ${getErrorMessage(error)}`);
60842
+ const message = getErrorMessage(error);
60843
+ console.warn(`[${formatTimestamp()}] ⚠️ Model refresh failed: ${message}`);
60844
+ return { kind: "failed", message };
60247
60845
  }
60248
60846
  }
60249
60847
  function evictStaleDedupEntries(tracker) {
@@ -60260,6 +60858,10 @@ function evictStaleDedupEntries(tracker) {
60260
60858
  if (ts2 < evictBefore)
60261
60859
  tracker.gitRefreshIds.delete(id);
60262
60860
  }
60861
+ for (const [id, ts2] of tracker.capabilitiesRefreshIds) {
60862
+ if (ts2 < evictBefore)
60863
+ tracker.capabilitiesRefreshIds.delete(id);
60864
+ }
60263
60865
  for (const [id, ts2] of tracker.localActionIds) {
60264
60866
  if (ts2 < evictBefore)
60265
60867
  tracker.localActionIds.delete(id);
@@ -60272,6 +60874,7 @@ function evictStaleDedupEntries(tracker) {
60272
60874
  if (ts2 < evictBefore)
60273
60875
  tracker.commandStopIds.delete(id);
60274
60876
  }
60877
+ evictStalePendingStops();
60275
60878
  }
60276
60879
  async function dispatchCommandEvent(ctx, event, tracker) {
60277
60880
  const eventId = event._id.toString();
@@ -60279,40 +60882,40 @@ async function dispatchCommandEvent(ctx, event, tracker) {
60279
60882
  if (event.type === "agent.requestStart") {
60280
60883
  if (tracker.commandIds.has(eventId))
60281
60884
  return;
60282
- tracker.commandIds.set(eventId, Date.now());
60283
60885
  await onRequestStartAgent(ctx, event);
60886
+ tracker.commandIds.set(eventId, Date.now());
60284
60887
  } else if (event.type === "agent.requestStop") {
60285
60888
  if (tracker.commandIds.has(eventId))
60286
60889
  return;
60287
- tracker.commandIds.set(eventId, Date.now());
60288
60890
  await onRequestStopAgent(ctx, event);
60891
+ tracker.commandIds.set(eventId, Date.now());
60289
60892
  } else if (event.type === "daemon.ping") {
60290
60893
  if (tracker.pingIds.has(eventId))
60291
60894
  return;
60292
- tracker.pingIds.set(eventId, Date.now());
60293
60895
  handlePing();
60294
60896
  await ctx.deps.backend.mutation(api.machines.ackPing, {
60295
60897
  sessionId: ctx.sessionId,
60296
60898
  machineId: ctx.machineId,
60297
60899
  pingEventId: event._id
60298
60900
  });
60901
+ tracker.pingIds.set(eventId, Date.now());
60299
60902
  } else if (event.type === "daemon.gitRefresh") {
60300
60903
  if (tracker.gitRefreshIds.has(eventId))
60301
60904
  return;
60302
- tracker.gitRefreshIds.set(eventId, Date.now());
60303
60905
  const stateKey = makeGitStateKey(ctx.machineId, event.workingDir);
60304
60906
  ctx.lastPushedGitState.delete(stateKey);
60305
60907
  console.log(`[${formatTimestamp()}] \uD83D\uDD04 Git refresh requested for ${event.workingDir}`);
60306
60908
  await pushGitState(ctx);
60909
+ tracker.gitRefreshIds.set(eventId, Date.now());
60307
60910
  } else if (event.type === "daemon.localAction") {
60308
60911
  if (tracker.localActionIds.has(eventId))
60309
60912
  return;
60310
- tracker.localActionIds.set(eventId, Date.now());
60311
60913
  console.log(`[${formatTimestamp()}] \uD83D\uDDA5️ Local action: ${event.action} → ${event.workingDir}`);
60312
60914
  const result = await executeLocalAction(event.action, event.workingDir);
60313
60915
  if (!result.success) {
60314
60916
  console.warn(`[${formatTimestamp()}] ⚠️ Local action failed: ${result.error}`);
60315
60917
  }
60918
+ tracker.localActionIds.set(eventId, Date.now());
60316
60919
  } else if (eventType === "command.run") {
60317
60920
  if (tracker.commandRunIds.has(eventId))
60318
60921
  return;
@@ -60321,8 +60924,42 @@ async function dispatchCommandEvent(ctx, event, tracker) {
60321
60924
  } else if (eventType === "command.stop") {
60322
60925
  if (tracker.commandStopIds.has(eventId))
60323
60926
  return;
60324
- tracker.commandStopIds.set(eventId, Date.now());
60325
60927
  await onCommandStop(ctx, event);
60928
+ tracker.commandStopIds.set(eventId, Date.now());
60929
+ } else if (event.type === "daemon.refreshCapabilities") {
60930
+ if (tracker.capabilitiesRefreshIds.has(eventId))
60931
+ return;
60932
+ console.log(`[${formatTimestamp()}] \uD83D\uDD04 Manual capabilities refresh requested`);
60933
+ const outcome = await refreshModels(ctx);
60934
+ tracker.capabilitiesRefreshIds.set(eventId, Date.now());
60935
+ const batchId = "batchId" in event && event.batchId !== undefined ? event.batchId : undefined;
60936
+ if (!batchId) {
60937
+ return;
60938
+ }
60939
+ let status;
60940
+ let errorMessage;
60941
+ if (outcome.kind === "pushed") {
60942
+ status = "completed";
60943
+ } else if (outcome.kind === "skipped_no_changes") {
60944
+ status = "skipped_no_changes";
60945
+ } else if (outcome.kind === "failed") {
60946
+ status = "failed";
60947
+ errorMessage = outcome.message;
60948
+ } else {
60949
+ status = "failed";
60950
+ errorMessage = "Daemon configuration unavailable";
60951
+ }
60952
+ try {
60953
+ await ctx.deps.backend.mutation(api.machines.reportCapabilitiesRefreshResult, {
60954
+ sessionId: ctx.sessionId,
60955
+ batchId,
60956
+ machineId: ctx.machineId,
60957
+ status,
60958
+ errorMessage
60959
+ });
60960
+ } catch (error) {
60961
+ console.warn(`[${formatTimestamp()}] ⚠️ Capabilities refresh report failed: ${getErrorMessage(error)}`);
60962
+ }
60326
60963
  }
60327
60964
  }
60328
60965
  async function startCommandLoop(ctx) {
@@ -60334,12 +60971,14 @@ async function startCommandLoop(ctx) {
60334
60971
  }).then(() => {
60335
60972
  heartbeatCount++;
60336
60973
  console.log(`[${formatTimestamp()}] \uD83D\uDC93 Daemon heartbeat #${heartbeatCount} OK`);
60337
- pushGitState(ctx).catch((err) => {
60338
- console.warn(`[${formatTimestamp()}] ⚠️ Git state push failed: ${getErrorMessage(err)}`);
60339
- });
60340
- pushCommands(ctx).catch((err) => {
60341
- console.warn(`[${formatTimestamp()}] ⚠️ Command sync failed: ${getErrorMessage(err)}`);
60342
- });
60974
+ if (!ctx.observedSyncEnabled) {
60975
+ pushGitState(ctx).catch((err) => {
60976
+ console.warn(`[${formatTimestamp()}] ⚠️ Git state push failed: ${getErrorMessage(err)}`);
60977
+ });
60978
+ pushCommands(ctx).catch((err) => {
60979
+ console.warn(`[${formatTimestamp()}] ⚠️ Command sync failed: ${getErrorMessage(err)}`);
60980
+ });
60981
+ }
60343
60982
  }).catch((err) => {
60344
60983
  console.warn(`[${formatTimestamp()}] ⚠️ Daemon heartbeat failed: ${getErrorMessage(err)}`);
60345
60984
  });
@@ -60348,8 +60987,13 @@ async function startCommandLoop(ctx) {
60348
60987
  let gitSubscriptionHandle = null;
60349
60988
  let fileContentSubscriptionHandle = null;
60350
60989
  let fileTreeSubscriptionHandle = null;
60351
- pushGitState(ctx).catch(() => {});
60352
- pushCommands(ctx).catch(() => {});
60990
+ let observedSyncSubscriptionHandle = null;
60991
+ if (ctx.observedSyncEnabled) {
60992
+ console.log(`[${formatTimestamp()}] \uD83D\uDC41️ Observed-sync enabled, skipping immediate push`);
60993
+ } else {
60994
+ pushGitState(ctx).catch(() => {});
60995
+ pushCommands(ctx).catch(() => {});
60996
+ }
60353
60997
  const shutdown = async () => {
60354
60998
  console.log(`
60355
60999
  [${formatTimestamp()}] Shutting down...`);
@@ -60360,6 +61004,8 @@ async function startCommandLoop(ctx) {
60360
61004
  fileContentSubscriptionHandle.stop();
60361
61005
  if (fileTreeSubscriptionHandle)
60362
61006
  fileTreeSubscriptionHandle.stop();
61007
+ if (observedSyncSubscriptionHandle)
61008
+ observedSyncSubscriptionHandle.stop();
60363
61009
  await onDaemonShutdown(ctx);
60364
61010
  if (ctx.stopLocalApi) {
60365
61011
  await ctx.stopLocalApi().catch(() => {});
@@ -60374,6 +61020,9 @@ async function startCommandLoop(ctx) {
60374
61020
  gitSubscriptionHandle = startGitRequestSubscription(ctx, wsClient2);
60375
61021
  fileContentSubscriptionHandle = startFileContentSubscription(ctx, wsClient2);
60376
61022
  fileTreeSubscriptionHandle = startFileTreeSubscription(ctx, wsClient2);
61023
+ if (ctx.observedSyncEnabled) {
61024
+ observedSyncSubscriptionHandle = startObservedSyncSubscription(ctx, wsClient2);
61025
+ }
60377
61026
  console.log(`
60378
61027
  Listening for commands...`);
60379
61028
  console.log(`Press Ctrl+C to stop
@@ -60382,6 +61031,7 @@ Listening for commands...`);
60382
61031
  commandIds: new Map,
60383
61032
  pingIds: new Map,
60384
61033
  gitRefreshIds: new Map,
61034
+ capabilitiesRefreshIds: new Map,
60385
61035
  localActionIds: new Map,
60386
61036
  commandRunIds: new Map,
60387
61037
  commandStopIds: new Map
@@ -60402,15 +61052,8 @@ Listening for commands...`);
60402
61052
  }
60403
61053
  }
60404
61054
  });
60405
- const modelRefreshTimer = setInterval(() => {
60406
- refreshModels(ctx).catch((err) => {
60407
- console.warn(`[${formatTimestamp()}] ⚠️ Model refresh error: ${getErrorMessage(err)}`);
60408
- });
60409
- }, MODEL_REFRESH_INTERVAL_MS);
60410
- modelRefreshTimer.unref();
60411
61055
  return await new Promise(() => {});
60412
61056
  }
60413
- var MODEL_REFRESH_INTERVAL_MS;
60414
61057
  var init_command_loop = __esm(() => {
60415
61058
  init_on_request_start_agent();
60416
61059
  init_on_request_stop_agent();
@@ -60420,6 +61063,7 @@ var init_command_loop = __esm(() => {
60420
61063
  init_file_content_subscription();
60421
61064
  init_file_tree_subscription();
60422
61065
  init_git_subscription();
61066
+ init_observed_sync();
60423
61067
  init_command_runner();
60424
61068
  init_init2();
60425
61069
  init_api3();
@@ -60428,7 +61072,6 @@ var init_command_loop = __esm(() => {
60428
61072
  init_local_actions();
60429
61073
  init_machine();
60430
61074
  init_convex_error();
60431
- MODEL_REFRESH_INTERVAL_MS = 10 * 1000;
60432
61075
  });
60433
61076
 
60434
61077
  // src/commands/machine/daemon-start/index.ts
@@ -61525,5 +62168,5 @@ program2.hook("preAction", async (_thisCommand, actionCommand) => {
61525
62168
  });
61526
62169
  program2.parse();
61527
62170
 
61528
- //# debugId=29314BC73A4B833B64756E2164756E21
62171
+ //# debugId=1C43E3169710F16B64756E2164756E21
61529
62172
  //# sourceMappingURL=index.js.map