chatroom-cli 1.6.4 → 1.7.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 (2) hide show
  1. package/dist/index.js +732 -23
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -10678,7 +10678,54 @@ var CURSOR_COMMAND = "agent", CURSOR_MODELS, CursorAgentService;
10678
10678
  var init_cursor_agent_service = __esm(() => {
10679
10679
  init_base_cli_agent_service();
10680
10680
  init_cursor_stream_reader();
10681
- CURSOR_MODELS = ["opus-4.6", "sonnet-4.6"];
10681
+ CURSOR_MODELS = [
10682
+ "opus-4.6",
10683
+ "opus-4.6-thinking",
10684
+ "opus-4.5",
10685
+ "opus-4.5-thinking",
10686
+ "sonnet-4.6",
10687
+ "sonnet-4.6-thinking",
10688
+ "sonnet-4.5",
10689
+ "sonnet-4.5-thinking",
10690
+ "gpt-5.4-low",
10691
+ "gpt-5.4-medium",
10692
+ "gpt-5.4-medium-fast",
10693
+ "gpt-5.4-high",
10694
+ "gpt-5.4-high-fast",
10695
+ "gpt-5.4-xhigh",
10696
+ "gpt-5.4-xhigh-fast",
10697
+ "gpt-5.3-codex-low",
10698
+ "gpt-5.3-codex-low-fast",
10699
+ "gpt-5.3-codex",
10700
+ "gpt-5.3-codex-fast",
10701
+ "gpt-5.3-codex-high",
10702
+ "gpt-5.3-codex-high-fast",
10703
+ "gpt-5.3-codex-xhigh",
10704
+ "gpt-5.3-codex-xhigh-fast",
10705
+ "gpt-5.3-codex-spark-preview",
10706
+ "gpt-5.2",
10707
+ "gpt-5.2-high",
10708
+ "gpt-5.2-codex-low",
10709
+ "gpt-5.2-codex-low-fast",
10710
+ "gpt-5.2-codex",
10711
+ "gpt-5.2-codex-fast",
10712
+ "gpt-5.2-codex-high",
10713
+ "gpt-5.2-codex-high-fast",
10714
+ "gpt-5.2-codex-xhigh",
10715
+ "gpt-5.2-codex-xhigh-fast",
10716
+ "gpt-5.1-high",
10717
+ "gpt-5.1-codex-max",
10718
+ "gpt-5.1-codex-max-high",
10719
+ "gpt-5.1-codex-mini",
10720
+ "gemini-3.1-pro",
10721
+ "gemini-3-pro",
10722
+ "gemini-3-flash",
10723
+ "grok",
10724
+ "kimi-k2.5",
10725
+ "auto",
10726
+ "composer-1.5",
10727
+ "composer-1"
10728
+ ];
10682
10729
  CursorAgentService = class CursorAgentService extends BaseCLIAgentService {
10683
10730
  id = "cursor";
10684
10731
  displayName = "Cursor";
@@ -13975,29 +14022,149 @@ async function recoverAgentState(ctx) {
13975
14022
  const entries = ctx.deps.machine.listAgentEntries(ctx.machineId);
13976
14023
  if (entries.length === 0) {
13977
14024
  console.log(` No agent entries found — nothing to recover`);
13978
- return;
13979
- }
13980
- let recovered = 0;
13981
- let cleared = 0;
13982
- for (const { chatroomId, role, entry } of entries) {
13983
- const { pid, harness } = entry;
13984
- const service = ctx.agentServices.get(harness) ?? ctx.agentServices.values().next().value;
13985
- const alive = service ? service.isAlive(pid) : false;
13986
- if (alive) {
13987
- console.log(` ✅ Recovered: ${role} (PID ${pid}, harness: ${harness})`);
13988
- recovered++;
13989
- } else {
13990
- console.log(` \uD83E\uDDF9 Stale PID ${pid} for ${role} — clearing`);
13991
- await clearAgentPidEverywhere(ctx, chatroomId, role);
13992
- cleared++;
14025
+ } else {
14026
+ let recovered = 0;
14027
+ let cleared = 0;
14028
+ const chatroomIds = new Set;
14029
+ for (const { chatroomId, role, entry } of entries) {
14030
+ const { pid, harness } = entry;
14031
+ const service = ctx.agentServices.get(harness) ?? ctx.agentServices.values().next().value;
14032
+ const alive = service ? service.isAlive(pid) : false;
14033
+ if (alive) {
14034
+ console.log(` ✅ Recovered: ${role} (PID ${pid}, harness: ${harness})`);
14035
+ recovered++;
14036
+ chatroomIds.add(chatroomId);
14037
+ } else {
14038
+ console.log(` \uD83E\uDDF9 Stale PID ${pid} for ${role} — clearing`);
14039
+ await clearAgentPidEverywhere(ctx, chatroomId, role);
14040
+ cleared++;
14041
+ }
14042
+ }
14043
+ console.log(` Recovery complete: ${recovered} alive, ${cleared} stale cleared`);
14044
+ for (const chatroomId of chatroomIds) {
14045
+ try {
14046
+ const configsResult = await ctx.deps.backend.query(api.machines.getMachineAgentConfigs, {
14047
+ sessionId: ctx.sessionId,
14048
+ chatroomId
14049
+ });
14050
+ for (const config3 of configsResult.configs) {
14051
+ if (config3.machineId === ctx.machineId && config3.workingDir) {
14052
+ ctx.activeWorkingDirs.add(config3.workingDir);
14053
+ }
14054
+ }
14055
+ } catch {}
14056
+ }
14057
+ if (ctx.activeWorkingDirs.size > 0) {
14058
+ console.log(` \uD83D\uDD00 Recovered ${ctx.activeWorkingDirs.size} active working dir(s) for git tracking`);
13993
14059
  }
13994
14060
  }
13995
- console.log(` Recovery complete: ${recovered} alive, ${cleared} stale cleared`);
13996
14061
  }
13997
14062
  var init_state_recovery = __esm(() => {
14063
+ init_api3();
13998
14064
  init_shared();
13999
14065
  });
14000
14066
 
14067
+ // src/infrastructure/services/harness-spawning/rate-limiter.ts
14068
+ class SpawnRateLimiter {
14069
+ config;
14070
+ buckets = new Map;
14071
+ constructor(config3 = {}) {
14072
+ this.config = { ...DEFAULT_CONFIG, ...config3 };
14073
+ }
14074
+ tryConsume(chatroomId, reason) {
14075
+ if (reason.startsWith("user.")) {
14076
+ return { allowed: true };
14077
+ }
14078
+ const bucket = this._getOrCreateBucket(chatroomId);
14079
+ this._refill(bucket);
14080
+ if (bucket.tokens < 1) {
14081
+ const elapsed = Date.now() - bucket.lastRefillAt;
14082
+ const retryAfterMs = this.config.refillRateMs - elapsed;
14083
+ console.warn(`⚠️ [RateLimiter] Agent spawn rate-limited for chatroom ${chatroomId} (reason: ${reason}). Retry after ${retryAfterMs}ms`);
14084
+ return { allowed: false, retryAfterMs };
14085
+ }
14086
+ bucket.tokens -= 1;
14087
+ const remaining = Math.floor(bucket.tokens);
14088
+ if (remaining <= LOW_TOKEN_THRESHOLD) {
14089
+ console.warn(`⚠️ [RateLimiter] Agent spawn tokens running low for chatroom ${chatroomId} (${remaining}/${this.config.maxTokens} remaining)`);
14090
+ }
14091
+ return { allowed: true };
14092
+ }
14093
+ getStatus(chatroomId) {
14094
+ const bucket = this._getOrCreateBucket(chatroomId);
14095
+ this._refill(bucket);
14096
+ return {
14097
+ remaining: Math.floor(bucket.tokens),
14098
+ total: this.config.maxTokens
14099
+ };
14100
+ }
14101
+ _getOrCreateBucket(chatroomId) {
14102
+ if (!this.buckets.has(chatroomId)) {
14103
+ this.buckets.set(chatroomId, {
14104
+ tokens: this.config.initialTokens,
14105
+ lastRefillAt: Date.now()
14106
+ });
14107
+ }
14108
+ return this.buckets.get(chatroomId);
14109
+ }
14110
+ _refill(bucket) {
14111
+ const now = Date.now();
14112
+ const elapsed = now - bucket.lastRefillAt;
14113
+ if (elapsed >= this.config.refillRateMs) {
14114
+ const tokensToAdd = Math.floor(elapsed / this.config.refillRateMs);
14115
+ bucket.tokens = Math.min(this.config.maxTokens, bucket.tokens + tokensToAdd);
14116
+ bucket.lastRefillAt += tokensToAdd * this.config.refillRateMs;
14117
+ }
14118
+ }
14119
+ }
14120
+ var DEFAULT_CONFIG, LOW_TOKEN_THRESHOLD = 1;
14121
+ var init_rate_limiter = __esm(() => {
14122
+ DEFAULT_CONFIG = {
14123
+ maxTokens: 5,
14124
+ refillRateMs: 60000,
14125
+ initialTokens: 5
14126
+ };
14127
+ });
14128
+
14129
+ // src/infrastructure/services/harness-spawning/harness-spawning-service.ts
14130
+ class HarnessSpawningService {
14131
+ rateLimiter;
14132
+ concurrentAgents = new Map;
14133
+ constructor({ rateLimiter }) {
14134
+ this.rateLimiter = rateLimiter;
14135
+ }
14136
+ shouldAllowSpawn(chatroomId, reason) {
14137
+ const current = this.concurrentAgents.get(chatroomId) ?? 0;
14138
+ if (current >= MAX_CONCURRENT_AGENTS_PER_CHATROOM) {
14139
+ console.warn(`⚠️ [HarnessSpawningService] Concurrent agent limit reached for chatroom ${chatroomId} ` + `(${current}/${MAX_CONCURRENT_AGENTS_PER_CHATROOM} active agents). Spawn rejected.`);
14140
+ return { allowed: false };
14141
+ }
14142
+ const result = this.rateLimiter.tryConsume(chatroomId, reason);
14143
+ if (!result.allowed) {
14144
+ console.warn(`⚠️ [HarnessSpawningService] Spawn blocked by rate limiter for chatroom ${chatroomId} ` + `(reason: ${reason}).`);
14145
+ }
14146
+ return result;
14147
+ }
14148
+ recordSpawn(chatroomId) {
14149
+ const current = this.concurrentAgents.get(chatroomId) ?? 0;
14150
+ this.concurrentAgents.set(chatroomId, current + 1);
14151
+ }
14152
+ recordExit(chatroomId) {
14153
+ const current = this.concurrentAgents.get(chatroomId) ?? 0;
14154
+ const next = Math.max(0, current - 1);
14155
+ this.concurrentAgents.set(chatroomId, next);
14156
+ }
14157
+ getConcurrentCount(chatroomId) {
14158
+ return this.concurrentAgents.get(chatroomId) ?? 0;
14159
+ }
14160
+ }
14161
+ var MAX_CONCURRENT_AGENTS_PER_CHATROOM = 10;
14162
+
14163
+ // src/infrastructure/services/harness-spawning/index.ts
14164
+ var init_harness_spawning = __esm(() => {
14165
+ init_rate_limiter();
14166
+ });
14167
+
14001
14168
  // src/commands/machine/pid.ts
14002
14169
  import { createHash } from "node:crypto";
14003
14170
  import { existsSync as existsSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4, unlinkSync as unlinkSync2, mkdirSync as mkdirSync4 } from "node:fs";
@@ -14228,7 +14395,8 @@ function createDefaultDeps16() {
14228
14395
  clock: {
14229
14396
  now: () => Date.now(),
14230
14397
  delay: (ms) => new Promise((resolve2) => setTimeout(resolve2, ms))
14231
- }
14398
+ },
14399
+ spawning: new HarnessSpawningService({ rateLimiter: new SpawnRateLimiter })
14232
14400
  };
14233
14401
  }
14234
14402
  function validateAuthentication(convexUrl) {
@@ -14345,7 +14513,9 @@ async function initDaemon() {
14345
14513
  config: config3,
14346
14514
  deps,
14347
14515
  events,
14348
- agentServices
14516
+ agentServices,
14517
+ activeWorkingDirs: new Set,
14518
+ lastPushedGitState: new Map
14349
14519
  };
14350
14520
  registerEventListeners(ctx);
14351
14521
  logStartup(ctx, availableModels);
@@ -14360,6 +14530,7 @@ var init_init2 = __esm(() => {
14360
14530
  init_machine();
14361
14531
  init_intentional_stops();
14362
14532
  init_remote_agents();
14533
+ init_harness_spawning();
14363
14534
  init_error_formatting();
14364
14535
  init_version();
14365
14536
  init_pid();
@@ -14468,6 +14639,7 @@ async function executeStartAgent(ctx, args) {
14468
14639
  const { pid } = spawnResult;
14469
14640
  const msg = `Agent spawned (PID: ${pid})`;
14470
14641
  console.log(` ✅ ${msg}`);
14642
+ ctx.deps.spawning.recordSpawn(chatroomId);
14471
14643
  try {
14472
14644
  await ctx.deps.backend.mutation(api.machines.updateSpawnedAgent, {
14473
14645
  sessionId: ctx.sessionId,
@@ -14490,7 +14662,9 @@ async function executeStartAgent(ctx, args) {
14490
14662
  harness: agentHarness,
14491
14663
  model
14492
14664
  });
14665
+ ctx.activeWorkingDirs.add(workingDir);
14493
14666
  spawnResult.onExit(({ code: code2, signal }) => {
14667
+ ctx.deps.spawning.recordExit(chatroomId);
14494
14668
  const pendingReason = ctx.deps.stops.consume(chatroomId, role);
14495
14669
  const stopReason = pendingReason ?? resolveStopReason(code2, signal, false);
14496
14670
  ctx.events.emit("agent:exited", {
@@ -14535,6 +14709,12 @@ async function onRequestStartAgent(ctx, event) {
14535
14709
  console.log(`[daemon] ⏰ Skipping expired agent.requestStart for role=${event.role} (deadline passed)`);
14536
14710
  return;
14537
14711
  }
14712
+ const spawnCheck = ctx.deps.spawning.shouldAllowSpawn(event.chatroomId, event.reason);
14713
+ if (!spawnCheck.allowed) {
14714
+ const retryMsg = spawnCheck.retryAfterMs ? ` Retry after ${spawnCheck.retryAfterMs}ms.` : "";
14715
+ console.warn(`[daemon] ⚠️ Spawn suppressed for chatroom=${event.chatroomId} role=${event.role} reason=${event.reason}.${retryMsg}`);
14716
+ return;
14717
+ }
14538
14718
  await executeStartAgent(ctx, {
14539
14719
  chatroomId: event.chatroomId,
14540
14720
  role: event.role,
@@ -14648,6 +14828,514 @@ function handlePing() {
14648
14828
  return { result: "pong", failed: false };
14649
14829
  }
14650
14830
 
14831
+ // src/infrastructure/git/types.ts
14832
+ function makeGitStateKey(machineId, workingDir) {
14833
+ return `${machineId}::${workingDir}`;
14834
+ }
14835
+ var FULL_DIFF_MAX_BYTES = 500000, COMMITS_PER_PAGE = 20;
14836
+
14837
+ // src/infrastructure/git/git-reader.ts
14838
+ import { exec as exec2 } from "node:child_process";
14839
+ import { promisify as promisify2 } from "node:util";
14840
+ async function runGit(args, cwd) {
14841
+ try {
14842
+ const result = await execAsync2(`git ${args}`, {
14843
+ cwd,
14844
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_PAGER: "cat", NO_COLOR: "1" },
14845
+ maxBuffer: FULL_DIFF_MAX_BYTES + 64 * 1024
14846
+ });
14847
+ return result;
14848
+ } catch (err) {
14849
+ return { error: err };
14850
+ }
14851
+ }
14852
+ function isGitNotInstalled(message) {
14853
+ return message.includes("command not found") || message.includes("ENOENT") || message.includes("not found") || message.includes("'git' is not recognized");
14854
+ }
14855
+ function isNotAGitRepo(message) {
14856
+ return message.includes("not a git repository") || message.includes("Not a git repository");
14857
+ }
14858
+ function isPermissionDenied(message) {
14859
+ return message.includes("Permission denied") || message.includes("EACCES");
14860
+ }
14861
+ function isEmptyRepo(stderr) {
14862
+ return stderr.includes("does not have any commits yet") || stderr.includes("no commits yet") || stderr.includes("ambiguous argument 'HEAD'") || stderr.includes("unknown revision or path");
14863
+ }
14864
+ function classifyError(errMessage) {
14865
+ if (isGitNotInstalled(errMessage)) {
14866
+ return { status: "error", message: "git is not installed or not in PATH" };
14867
+ }
14868
+ if (isNotAGitRepo(errMessage)) {
14869
+ return { status: "not_found" };
14870
+ }
14871
+ if (isPermissionDenied(errMessage)) {
14872
+ return { status: "error", message: `Permission denied: ${errMessage}` };
14873
+ }
14874
+ return { status: "error", message: errMessage.trim() };
14875
+ }
14876
+ async function isGitRepo(workingDir) {
14877
+ const result = await runGit("rev-parse --git-dir", workingDir);
14878
+ if ("error" in result)
14879
+ return false;
14880
+ return result.stdout.trim().length > 0;
14881
+ }
14882
+ async function getBranch(workingDir) {
14883
+ const result = await runGit("rev-parse --abbrev-ref HEAD", workingDir);
14884
+ if ("error" in result) {
14885
+ const errMsg = result.error.message;
14886
+ if (errMsg.includes("unknown revision") || errMsg.includes("No such file or directory") || errMsg.includes("does not have any commits")) {
14887
+ return { status: "available", branch: "HEAD" };
14888
+ }
14889
+ return classifyError(errMsg);
14890
+ }
14891
+ const branch = result.stdout.trim();
14892
+ if (!branch) {
14893
+ return { status: "error", message: "git rev-parse returned empty output" };
14894
+ }
14895
+ return { status: "available", branch };
14896
+ }
14897
+ async function isDirty(workingDir) {
14898
+ const result = await runGit("status --porcelain", workingDir);
14899
+ if ("error" in result)
14900
+ return false;
14901
+ return result.stdout.trim().length > 0;
14902
+ }
14903
+ function parseDiffStatLine(statLine) {
14904
+ const filesMatch = statLine.match(/(\d+)\s+file/);
14905
+ const insertMatch = statLine.match(/(\d+)\s+insertion/);
14906
+ const deleteMatch = statLine.match(/(\d+)\s+deletion/);
14907
+ return {
14908
+ filesChanged: filesMatch ? parseInt(filesMatch[1], 10) : 0,
14909
+ insertions: insertMatch ? parseInt(insertMatch[1], 10) : 0,
14910
+ deletions: deleteMatch ? parseInt(deleteMatch[1], 10) : 0
14911
+ };
14912
+ }
14913
+ async function getDiffStat(workingDir) {
14914
+ const result = await runGit("diff HEAD --stat", workingDir);
14915
+ if ("error" in result) {
14916
+ const errMsg = result.error.message;
14917
+ if (isEmptyRepo(result.error.message)) {
14918
+ return { status: "no_commits" };
14919
+ }
14920
+ const classified = classifyError(errMsg);
14921
+ if (classified.status === "not_found")
14922
+ return { status: "not_found" };
14923
+ return classified;
14924
+ }
14925
+ const output = result.stdout;
14926
+ const stderr = result.stderr;
14927
+ if (isEmptyRepo(stderr)) {
14928
+ return { status: "no_commits" };
14929
+ }
14930
+ if (!output.trim()) {
14931
+ return {
14932
+ status: "available",
14933
+ diffStat: { filesChanged: 0, insertions: 0, deletions: 0 }
14934
+ };
14935
+ }
14936
+ const lines = output.trim().split(`
14937
+ `);
14938
+ const summaryLine = lines[lines.length - 1] ?? "";
14939
+ const diffStat = parseDiffStatLine(summaryLine);
14940
+ return { status: "available", diffStat };
14941
+ }
14942
+ async function getFullDiff(workingDir) {
14943
+ const result = await runGit("diff HEAD", workingDir);
14944
+ if ("error" in result) {
14945
+ const errMsg = result.error.message;
14946
+ if (isEmptyRepo(errMsg)) {
14947
+ return { status: "no_commits" };
14948
+ }
14949
+ const classified = classifyError(errMsg);
14950
+ if (classified.status === "not_found")
14951
+ return { status: "not_found" };
14952
+ return classified;
14953
+ }
14954
+ const stderr = result.stderr;
14955
+ if (isEmptyRepo(stderr)) {
14956
+ return { status: "no_commits" };
14957
+ }
14958
+ const raw = result.stdout;
14959
+ const byteLength2 = Buffer.byteLength(raw, "utf8");
14960
+ if (byteLength2 > FULL_DIFF_MAX_BYTES) {
14961
+ const truncated = Buffer.from(raw, "utf8").subarray(0, FULL_DIFF_MAX_BYTES).toString("utf8");
14962
+ return { status: "truncated", content: truncated, truncated: true };
14963
+ }
14964
+ return { status: "available", content: raw, truncated: false };
14965
+ }
14966
+ async function getRecentCommits(workingDir, count = 20, skip = 0) {
14967
+ const format = "%H%x00%h%x00%s%x00%an%x00%aI";
14968
+ const skipArg = skip > 0 ? ` --skip=${skip}` : "";
14969
+ const result = await runGit(`log -${count}${skipArg} --format=${format}`, workingDir);
14970
+ if ("error" in result) {
14971
+ return [];
14972
+ }
14973
+ const output = result.stdout.trim();
14974
+ if (!output)
14975
+ return [];
14976
+ const commits = [];
14977
+ for (const line of output.split(`
14978
+ `)) {
14979
+ const trimmed = line.trim();
14980
+ if (!trimmed)
14981
+ continue;
14982
+ const parts = trimmed.split("\x00");
14983
+ if (parts.length !== 5)
14984
+ continue;
14985
+ const [sha, shortSha, message, author, date] = parts;
14986
+ commits.push({ sha, shortSha, message, author, date });
14987
+ }
14988
+ return commits;
14989
+ }
14990
+ async function getCommitDetail(workingDir, sha) {
14991
+ const result = await runGit(`show ${sha} --format="" --stat -p`, workingDir);
14992
+ if ("error" in result) {
14993
+ const errMsg = result.error.message;
14994
+ const classified = classifyError(errMsg);
14995
+ if (classified.status === "not_found")
14996
+ return { status: "not_found" };
14997
+ if (isEmptyRepo(errMsg) || errMsg.includes("unknown revision") || errMsg.includes("bad object") || errMsg.includes("does not exist")) {
14998
+ return { status: "not_found" };
14999
+ }
15000
+ return classified;
15001
+ }
15002
+ const raw = result.stdout;
15003
+ const byteLength2 = Buffer.byteLength(raw, "utf8");
15004
+ if (byteLength2 > FULL_DIFF_MAX_BYTES) {
15005
+ const truncated = Buffer.from(raw, "utf8").subarray(0, FULL_DIFF_MAX_BYTES).toString("utf8");
15006
+ return { status: "truncated", content: truncated, truncated: true };
15007
+ }
15008
+ return { status: "available", content: raw, truncated: false };
15009
+ }
15010
+ async function getCommitMetadata(workingDir, sha) {
15011
+ const format = "%s%x00%an%x00%aI";
15012
+ const result = await runGit(`log -1 --format=${format} ${sha}`, workingDir);
15013
+ if ("error" in result)
15014
+ return null;
15015
+ const output = result.stdout.trim();
15016
+ if (!output)
15017
+ return null;
15018
+ const parts = output.split("\x00");
15019
+ if (parts.length !== 3)
15020
+ return null;
15021
+ return { message: parts[0], author: parts[1], date: parts[2] };
15022
+ }
15023
+ var execAsync2;
15024
+ var init_git_reader = __esm(() => {
15025
+ execAsync2 = promisify2(exec2);
15026
+ });
15027
+
15028
+ // src/commands/machine/daemon-start/git-polling.ts
15029
+ function startGitPollingLoop(ctx) {
15030
+ const timer = setInterval(() => {
15031
+ runPollingTick(ctx).catch((err) => {
15032
+ console.warn(`[${formatTimestamp()}] ⚠️ Git polling tick failed: ${err.message}`);
15033
+ });
15034
+ }, GIT_POLLING_INTERVAL_MS);
15035
+ timer.unref();
15036
+ console.log(`[${formatTimestamp()}] \uD83D\uDD00 Git polling loop started (interval: ${GIT_POLLING_INTERVAL_MS}ms)`);
15037
+ return {
15038
+ stop: () => {
15039
+ clearInterval(timer);
15040
+ console.log(`[${formatTimestamp()}] \uD83D\uDD00 Git polling loop stopped`);
15041
+ }
15042
+ };
15043
+ }
15044
+ function extractDiffStatFromShowOutput(content) {
15045
+ for (const line of content.split(`
15046
+ `)) {
15047
+ if (/\d+\s+file.*changed/.test(line)) {
15048
+ return parseDiffStatLine(line);
15049
+ }
15050
+ }
15051
+ return { filesChanged: 0, insertions: 0, deletions: 0 };
15052
+ }
15053
+ async function processFullDiff(ctx, req) {
15054
+ const result = await getFullDiff(req.workingDir);
15055
+ if (result.status === "available" || result.status === "truncated") {
15056
+ const diffStatResult = await getDiffStat(req.workingDir);
15057
+ const diffStat = diffStatResult.status === "available" ? diffStatResult.diffStat : { filesChanged: 0, insertions: 0, deletions: 0 };
15058
+ await ctx.deps.backend.mutation(api.workspaces.upsertFullDiff, {
15059
+ sessionId: ctx.sessionId,
15060
+ machineId: ctx.machineId,
15061
+ workingDir: req.workingDir,
15062
+ diffContent: result.content,
15063
+ truncated: result.truncated,
15064
+ diffStat
15065
+ });
15066
+ console.log(`[${formatTimestamp()}] \uD83D\uDCC4 Full diff pushed: ${req.workingDir} (${diffStat.filesChanged} files, ${result.truncated ? "truncated" : "complete"})`);
15067
+ } else {
15068
+ await ctx.deps.backend.mutation(api.workspaces.upsertFullDiff, {
15069
+ sessionId: ctx.sessionId,
15070
+ machineId: ctx.machineId,
15071
+ workingDir: req.workingDir,
15072
+ diffContent: "",
15073
+ truncated: false,
15074
+ diffStat: { filesChanged: 0, insertions: 0, deletions: 0 }
15075
+ });
15076
+ console.log(`[${formatTimestamp()}] \uD83D\uDCC4 Full diff pushed (empty): ${req.workingDir} (${result.status})`);
15077
+ }
15078
+ }
15079
+ async function processCommitDetail(ctx, req) {
15080
+ if (!req.sha) {
15081
+ throw new Error("commit_detail request missing sha");
15082
+ }
15083
+ const [result, metadata] = await Promise.all([
15084
+ getCommitDetail(req.workingDir, req.sha),
15085
+ getCommitMetadata(req.workingDir, req.sha)
15086
+ ]);
15087
+ if (result.status === "not_found") {
15088
+ await ctx.deps.backend.mutation(api.workspaces.upsertCommitDetail, {
15089
+ sessionId: ctx.sessionId,
15090
+ machineId: ctx.machineId,
15091
+ workingDir: req.workingDir,
15092
+ sha: req.sha,
15093
+ status: "not_found",
15094
+ message: metadata?.message,
15095
+ author: metadata?.author,
15096
+ date: metadata?.date
15097
+ });
15098
+ return;
15099
+ }
15100
+ if (result.status === "error") {
15101
+ await ctx.deps.backend.mutation(api.workspaces.upsertCommitDetail, {
15102
+ sessionId: ctx.sessionId,
15103
+ machineId: ctx.machineId,
15104
+ workingDir: req.workingDir,
15105
+ sha: req.sha,
15106
+ status: "error",
15107
+ errorMessage: result.message,
15108
+ message: metadata?.message,
15109
+ author: metadata?.author,
15110
+ date: metadata?.date
15111
+ });
15112
+ return;
15113
+ }
15114
+ const diffStat = extractDiffStatFromShowOutput(result.content);
15115
+ await ctx.deps.backend.mutation(api.workspaces.upsertCommitDetail, {
15116
+ sessionId: ctx.sessionId,
15117
+ machineId: ctx.machineId,
15118
+ workingDir: req.workingDir,
15119
+ sha: req.sha,
15120
+ status: "available",
15121
+ diffContent: result.content,
15122
+ truncated: result.truncated,
15123
+ message: metadata?.message,
15124
+ author: metadata?.author,
15125
+ date: metadata?.date,
15126
+ diffStat
15127
+ });
15128
+ console.log(`[${formatTimestamp()}] \uD83D\uDD0D Commit detail pushed: ${req.sha.slice(0, 7)} in ${req.workingDir}`);
15129
+ }
15130
+ async function processMoreCommits(ctx, req) {
15131
+ const offset = req.offset ?? 0;
15132
+ const commits = await getRecentCommits(req.workingDir, COMMITS_PER_PAGE, offset);
15133
+ const hasMoreCommits = commits.length >= COMMITS_PER_PAGE;
15134
+ await ctx.deps.backend.mutation(api.workspaces.appendMoreCommits, {
15135
+ sessionId: ctx.sessionId,
15136
+ machineId: ctx.machineId,
15137
+ workingDir: req.workingDir,
15138
+ commits,
15139
+ hasMoreCommits
15140
+ });
15141
+ console.log(`[${formatTimestamp()}] \uD83D\uDCDC More commits appended: ${req.workingDir} (+${commits.length} commits, offset=${offset})`);
15142
+ }
15143
+ async function runPollingTick(ctx) {
15144
+ const requests = await ctx.deps.backend.query(api.workspaces.getPendingRequests, {
15145
+ sessionId: ctx.sessionId,
15146
+ machineId: ctx.machineId
15147
+ });
15148
+ if (requests.length === 0)
15149
+ return;
15150
+ for (const req of requests) {
15151
+ try {
15152
+ await ctx.deps.backend.mutation(api.workspaces.updateRequestStatus, {
15153
+ sessionId: ctx.sessionId,
15154
+ requestId: req._id,
15155
+ status: "processing"
15156
+ });
15157
+ switch (req.requestType) {
15158
+ case "full_diff":
15159
+ await processFullDiff(ctx, req);
15160
+ break;
15161
+ case "commit_detail":
15162
+ await processCommitDetail(ctx, req);
15163
+ break;
15164
+ case "more_commits":
15165
+ await processMoreCommits(ctx, req);
15166
+ break;
15167
+ }
15168
+ await ctx.deps.backend.mutation(api.workspaces.updateRequestStatus, {
15169
+ sessionId: ctx.sessionId,
15170
+ requestId: req._id,
15171
+ status: "done"
15172
+ });
15173
+ } catch (err) {
15174
+ console.warn(`[${formatTimestamp()}] ⚠️ Failed to process ${req.requestType} request: ${err.message}`);
15175
+ await ctx.deps.backend.mutation(api.workspaces.updateRequestStatus, {
15176
+ sessionId: ctx.sessionId,
15177
+ requestId: req._id,
15178
+ status: "error"
15179
+ }).catch(() => {});
15180
+ }
15181
+ }
15182
+ }
15183
+ var GIT_POLLING_INTERVAL_MS = 5000;
15184
+ var init_git_polling = __esm(() => {
15185
+ init_api3();
15186
+ init_git_reader();
15187
+ });
15188
+
15189
+ // src/commands/machine/daemon-start/git-heartbeat.ts
15190
+ import { createHash as createHash2 } from "node:crypto";
15191
+ async function pushGitState(ctx) {
15192
+ if (ctx.activeWorkingDirs.size === 0)
15193
+ return;
15194
+ for (const workingDir of ctx.activeWorkingDirs) {
15195
+ try {
15196
+ await pushSingleWorkspaceGitState(ctx, workingDir);
15197
+ } catch (err) {
15198
+ console.warn(`[${formatTimestamp()}] ⚠️ Git state push failed for ${workingDir}: ${err.message}`);
15199
+ }
15200
+ }
15201
+ }
15202
+ async function pushSingleWorkspaceGitState(ctx, workingDir) {
15203
+ const stateKey = makeGitStateKey(ctx.machineId, workingDir);
15204
+ const isRepo = await isGitRepo(workingDir);
15205
+ if (!isRepo) {
15206
+ const stateHash2 = "not_found";
15207
+ if (ctx.lastPushedGitState.get(stateKey) === stateHash2)
15208
+ return;
15209
+ await ctx.deps.backend.mutation(api.workspaces.upsertWorkspaceGitState, {
15210
+ sessionId: ctx.sessionId,
15211
+ machineId: ctx.machineId,
15212
+ workingDir,
15213
+ status: "not_found"
15214
+ });
15215
+ ctx.lastPushedGitState.set(stateKey, stateHash2);
15216
+ return;
15217
+ }
15218
+ const [branchResult, dirtyResult, diffStatResult, commits] = await Promise.all([
15219
+ getBranch(workingDir),
15220
+ isDirty(workingDir),
15221
+ getDiffStat(workingDir),
15222
+ getRecentCommits(workingDir, COMMITS_PER_PAGE)
15223
+ ]);
15224
+ if (branchResult.status === "error") {
15225
+ const stateHash2 = `error:${branchResult.message}`;
15226
+ if (ctx.lastPushedGitState.get(stateKey) === stateHash2)
15227
+ return;
15228
+ await ctx.deps.backend.mutation(api.workspaces.upsertWorkspaceGitState, {
15229
+ sessionId: ctx.sessionId,
15230
+ machineId: ctx.machineId,
15231
+ workingDir,
15232
+ status: "error",
15233
+ errorMessage: branchResult.message
15234
+ });
15235
+ ctx.lastPushedGitState.set(stateKey, stateHash2);
15236
+ return;
15237
+ }
15238
+ if (branchResult.status === "not_found") {
15239
+ return;
15240
+ }
15241
+ const branch = branchResult.branch;
15242
+ const isDirty2 = dirtyResult;
15243
+ const diffStat = diffStatResult.status === "available" ? diffStatResult.diffStat : { filesChanged: 0, insertions: 0, deletions: 0 };
15244
+ const hasMoreCommits = commits.length >= COMMITS_PER_PAGE;
15245
+ const stateHash = createHash2("md5").update(JSON.stringify({ branch, isDirty: isDirty2, diffStat, shas: commits.map((c) => c.sha) })).digest("hex");
15246
+ if (ctx.lastPushedGitState.get(stateKey) === stateHash) {
15247
+ return;
15248
+ }
15249
+ await ctx.deps.backend.mutation(api.workspaces.upsertWorkspaceGitState, {
15250
+ sessionId: ctx.sessionId,
15251
+ machineId: ctx.machineId,
15252
+ workingDir,
15253
+ status: "available",
15254
+ branch,
15255
+ isDirty: isDirty2,
15256
+ diffStat,
15257
+ recentCommits: commits,
15258
+ hasMoreCommits
15259
+ });
15260
+ ctx.lastPushedGitState.set(stateKey, stateHash);
15261
+ console.log(`[${formatTimestamp()}] \uD83D\uDD00 Git state pushed: ${workingDir} (${branch}${isDirty2 ? ", dirty" : ", clean"})`);
15262
+ prefetchMissingCommitDetails(ctx, workingDir, commits).catch((err) => {
15263
+ console.warn(`[${formatTimestamp()}] ⚠️ Commit pre-fetch failed for ${workingDir}: ${err.message}`);
15264
+ });
15265
+ }
15266
+ async function prefetchMissingCommitDetails(ctx, workingDir, commits) {
15267
+ if (commits.length === 0)
15268
+ return;
15269
+ const shas = commits.map((c) => c.sha);
15270
+ const missingShas = await ctx.deps.backend.query(api.workspaces.getMissingCommitShas, {
15271
+ sessionId: ctx.sessionId,
15272
+ machineId: ctx.machineId,
15273
+ workingDir,
15274
+ shas
15275
+ });
15276
+ if (missingShas.length === 0)
15277
+ return;
15278
+ console.log(`[${formatTimestamp()}] \uD83D\uDD0D Pre-fetching ${missingShas.length} commit(s) for ${workingDir}`);
15279
+ for (const sha of missingShas) {
15280
+ try {
15281
+ await prefetchSingleCommit(ctx, workingDir, sha, commits);
15282
+ } catch (err) {
15283
+ console.warn(`[${formatTimestamp()}] ⚠️ Pre-fetch failed for ${sha.slice(0, 7)}: ${err.message}`);
15284
+ }
15285
+ }
15286
+ }
15287
+ async function prefetchSingleCommit(ctx, workingDir, sha, commits) {
15288
+ const metadata = commits.find((c) => c.sha === sha);
15289
+ const result = await getCommitDetail(workingDir, sha);
15290
+ if (result.status === "not_found") {
15291
+ await ctx.deps.backend.mutation(api.workspaces.upsertCommitDetail, {
15292
+ sessionId: ctx.sessionId,
15293
+ machineId: ctx.machineId,
15294
+ workingDir,
15295
+ sha,
15296
+ status: "not_found",
15297
+ message: metadata?.message,
15298
+ author: metadata?.author,
15299
+ date: metadata?.date
15300
+ });
15301
+ return;
15302
+ }
15303
+ if (result.status === "error") {
15304
+ await ctx.deps.backend.mutation(api.workspaces.upsertCommitDetail, {
15305
+ sessionId: ctx.sessionId,
15306
+ machineId: ctx.machineId,
15307
+ workingDir,
15308
+ sha,
15309
+ status: "error",
15310
+ errorMessage: result.message,
15311
+ message: metadata?.message,
15312
+ author: metadata?.author,
15313
+ date: metadata?.date
15314
+ });
15315
+ return;
15316
+ }
15317
+ const diffStat = extractDiffStatFromShowOutput(result.content);
15318
+ await ctx.deps.backend.mutation(api.workspaces.upsertCommitDetail, {
15319
+ sessionId: ctx.sessionId,
15320
+ machineId: ctx.machineId,
15321
+ workingDir,
15322
+ sha,
15323
+ status: "available",
15324
+ diffContent: result.content,
15325
+ truncated: result.truncated,
15326
+ message: metadata?.message,
15327
+ author: metadata?.author,
15328
+ date: metadata?.date,
15329
+ diffStat
15330
+ });
15331
+ console.log(`[${formatTimestamp()}] ✅ Pre-fetched: ${sha.slice(0, 7)} in ${workingDir}`);
15332
+ }
15333
+ var init_git_heartbeat = __esm(() => {
15334
+ init_api3();
15335
+ init_git_reader();
15336
+ init_git_polling();
15337
+ });
15338
+
14651
15339
  // src/commands/machine/daemon-start/command-loop.ts
14652
15340
  async function refreshModels(ctx) {
14653
15341
  if (!ctx.config)
@@ -14670,7 +15358,7 @@ async function refreshModels(ctx) {
14670
15358
  console.warn(`[${formatTimestamp()}] ⚠️ Model refresh failed: ${error.message}`);
14671
15359
  }
14672
15360
  }
14673
- function evictStaleDedupEntries(processedCommandIds, processedPingIds) {
15361
+ function evictStaleDedupEntries(processedCommandIds, processedPingIds, processedGitRefreshIds) {
14674
15362
  const evictBefore = Date.now() - AGENT_REQUEST_DEADLINE_MS;
14675
15363
  for (const [id, ts] of processedCommandIds) {
14676
15364
  if (ts < evictBefore)
@@ -14680,8 +15368,12 @@ function evictStaleDedupEntries(processedCommandIds, processedPingIds) {
14680
15368
  if (ts < evictBefore)
14681
15369
  processedPingIds.delete(id);
14682
15370
  }
15371
+ for (const [id, ts] of processedGitRefreshIds) {
15372
+ if (ts < evictBefore)
15373
+ processedGitRefreshIds.delete(id);
15374
+ }
14683
15375
  }
14684
- async function dispatchCommandEvent(ctx, event, processedCommandIds, processedPingIds) {
15376
+ async function dispatchCommandEvent(ctx, event, processedCommandIds, processedPingIds, processedGitRefreshIds) {
14685
15377
  const eventId = event._id.toString();
14686
15378
  if (event.type === "agent.requestStart") {
14687
15379
  if (processedCommandIds.has(eventId))
@@ -14703,6 +15395,14 @@ async function dispatchCommandEvent(ctx, event, processedCommandIds, processedPi
14703
15395
  machineId: ctx.machineId,
14704
15396
  pingEventId: event._id
14705
15397
  });
15398
+ } else if (event.type === "daemon.gitRefresh") {
15399
+ if (processedGitRefreshIds.has(eventId))
15400
+ return;
15401
+ processedGitRefreshIds.set(eventId, Date.now());
15402
+ const stateKey = makeGitStateKey(ctx.machineId, event.workingDir);
15403
+ ctx.lastPushedGitState.delete(stateKey);
15404
+ console.log(`[${formatTimestamp()}] \uD83D\uDD04 Git refresh requested for ${event.workingDir}`);
15405
+ await pushGitState(ctx);
14706
15406
  }
14707
15407
  }
14708
15408
  async function startCommandLoop(ctx) {
@@ -14714,15 +15414,21 @@ async function startCommandLoop(ctx) {
14714
15414
  }).then(() => {
14715
15415
  heartbeatCount++;
14716
15416
  console.log(`[${formatTimestamp()}] \uD83D\uDC93 Daemon heartbeat #${heartbeatCount} OK`);
15417
+ pushGitState(ctx).catch((err) => {
15418
+ console.warn(`[${formatTimestamp()}] ⚠️ Git state push failed: ${err.message}`);
15419
+ });
14717
15420
  }).catch((err) => {
14718
15421
  console.warn(`[${formatTimestamp()}] ⚠️ Daemon heartbeat failed: ${err.message}`);
14719
15422
  });
14720
15423
  }, DAEMON_HEARTBEAT_INTERVAL_MS);
14721
15424
  heartbeatTimer.unref();
15425
+ const gitPollingHandle = startGitPollingLoop(ctx);
15426
+ pushGitState(ctx).catch(() => {});
14722
15427
  const shutdown = async () => {
14723
15428
  console.log(`
14724
15429
  [${formatTimestamp()}] Shutting down...`);
14725
15430
  clearInterval(heartbeatTimer);
15431
+ gitPollingHandle.stop();
14726
15432
  await onDaemonShutdown(ctx);
14727
15433
  releaseLock();
14728
15434
  process.exit(0);
@@ -14737,17 +15443,18 @@ Listening for commands...`);
14737
15443
  `);
14738
15444
  const processedCommandIds = new Map;
14739
15445
  const processedPingIds = new Map;
15446
+ const processedGitRefreshIds = new Map;
14740
15447
  wsClient2.onUpdate(api.machines.getCommandEvents, {
14741
15448
  sessionId: ctx.sessionId,
14742
15449
  machineId: ctx.machineId
14743
15450
  }, async (result) => {
14744
15451
  if (!result.events || result.events.length === 0)
14745
15452
  return;
14746
- evictStaleDedupEntries(processedCommandIds, processedPingIds);
15453
+ evictStaleDedupEntries(processedCommandIds, processedPingIds, processedGitRefreshIds);
14747
15454
  for (const event of result.events) {
14748
15455
  try {
14749
15456
  console.log(`[${formatTimestamp()}] \uD83D\uDCE1 Stream command event: ${event.type}`);
14750
- await dispatchCommandEvent(ctx, event, processedCommandIds, processedPingIds);
15457
+ await dispatchCommandEvent(ctx, event, processedCommandIds, processedPingIds, processedGitRefreshIds);
14751
15458
  } catch (err) {
14752
15459
  console.error(`[${formatTimestamp()}] ❌ Stream command event failed: ${err.message}`);
14753
15460
  }
@@ -14771,6 +15478,8 @@ var init_command_loop = __esm(() => {
14771
15478
  init_on_request_start_agent();
14772
15479
  init_on_request_stop_agent();
14773
15480
  init_pid();
15481
+ init_git_polling();
15482
+ init_git_heartbeat();
14774
15483
  MODEL_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
14775
15484
  });
14776
15485
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatroom-cli",
3
- "version": "1.6.4",
3
+ "version": "1.7.0",
4
4
  "description": "CLI for multi-agent chatroom collaboration",
5
5
  "type": "module",
6
6
  "bin": {