chatroom-cli 1.11.1 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +1614 -1435
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -10895,10 +10895,10 @@ function initHarnessRegistry() {
10895
10895
  }
10896
10896
  var initialized = false;
10897
10897
  var init_init_registry = __esm(() => {
10898
- init_registry();
10898
+ init_cursor();
10899
10899
  init_opencode();
10900
10900
  init_pi();
10901
- init_cursor();
10901
+ init_registry();
10902
10902
  });
10903
10903
 
10904
10904
  // src/infrastructure/services/remote-agents/index.ts
@@ -11134,34 +11134,10 @@ var init_daemon_state = __esm(() => {
11134
11134
  STATE_DIR = join4(CHATROOM_DIR3, "machines", "state");
11135
11135
  });
11136
11136
 
11137
- // src/infrastructure/machine/intentional-stops.ts
11138
- function agentKey2(chatroomId, role) {
11139
- return `${chatroomId}:${role.toLowerCase()}`;
11140
- }
11141
- function markIntentionalStop(chatroomId, role, reason = "user.stop") {
11142
- pendingStops.set(agentKey2(chatroomId, role), reason);
11143
- }
11144
- function consumeIntentionalStop(chatroomId, role) {
11145
- const key = agentKey2(chatroomId, role);
11146
- const reason = pendingStops.get(key) ?? null;
11147
- if (reason !== null) {
11148
- pendingStops.delete(key);
11149
- }
11150
- return reason;
11151
- }
11152
- function clearIntentionalStop(chatroomId, role) {
11153
- pendingStops.delete(agentKey2(chatroomId, role));
11154
- }
11155
- var pendingStops;
11156
- var init_intentional_stops = __esm(() => {
11157
- pendingStops = new Map;
11158
- });
11159
-
11160
11137
  // src/infrastructure/machine/index.ts
11161
11138
  var init_machine = __esm(() => {
11162
11139
  init_storage2();
11163
11140
  init_daemon_state();
11164
- init_intentional_stops();
11165
11141
  });
11166
11142
 
11167
11143
  // src/commands/auth-status/index.ts
@@ -11709,8 +11685,8 @@ var init_utils = __esm(() => {
11709
11685
 
11710
11686
  // ../../services/backend/prompts/base/shared/getting-started-content.ts
11711
11687
  var init_getting_started_content = __esm(() => {
11712
- init_utils();
11713
11688
  init_reminder();
11689
+ init_utils();
11714
11690
  });
11715
11691
 
11716
11692
  // ../../services/backend/prompts/cli/index.ts
@@ -12344,8 +12320,7 @@ async function classify(chatroomId, options, deps) {
12344
12320
  console.error(`❌ \`classify\` is only available to the entry point role (${entryPoint}). Your role is ${role}.`);
12345
12321
  console.error("");
12346
12322
  console.error(" Entry point roles receive user messages and must classify them.");
12347
- console.error(" Other roles receive handoffs and should use:");
12348
- console.error(` ${cliEnvPrefix}chatroom task-started --chatroom-id=${chatroomId} --role=${role} --task-id=<task-id> --no-classify`);
12323
+ console.error(" Other roles receive handoffs use `task read` to mark in_progress.");
12349
12324
  process.exit(1);
12350
12325
  }
12351
12326
  if (originMessageClassification === "new_feature") {
@@ -12839,12 +12814,19 @@ __export(exports_backlog, {
12839
12814
  patchBacklog: () => patchBacklog,
12840
12815
  markForReviewBacklog: () => markForReviewBacklog,
12841
12816
  listBacklog: () => listBacklog,
12817
+ importBacklog: () => importBacklog,
12842
12818
  historyBacklog: () => historyBacklog,
12819
+ exportBacklog: () => exportBacklog,
12820
+ computeContentHash: () => computeContentHash,
12843
12821
  completeBacklog: () => completeBacklog,
12822
+ closeBacklog: () => closeBacklog,
12844
12823
  addBacklog: () => addBacklog
12845
12824
  });
12825
+ import { createHash } from "node:crypto";
12826
+ import * as nodePath from "node:path";
12846
12827
  async function createDefaultDeps11() {
12847
12828
  const client2 = await getConvexClient();
12829
+ const fs = await import("node:fs/promises");
12848
12830
  return {
12849
12831
  backend: {
12850
12832
  mutation: (endpoint, args) => client2.mutation(endpoint, args),
@@ -12854,6 +12836,11 @@ async function createDefaultDeps11() {
12854
12836
  getSessionId,
12855
12837
  getConvexUrl,
12856
12838
  getOtherSessionUrls
12839
+ },
12840
+ fs: {
12841
+ writeFile: (path2, data) => fs.writeFile(path2, data, "utf-8"),
12842
+ readFile: (path2, encoding) => fs.readFile(path2, { encoding }),
12843
+ mkdir: (path2, options) => fs.mkdir(path2, options)
12857
12844
  }
12858
12845
  };
12859
12846
  }
@@ -12880,17 +12867,19 @@ async function listBacklog(chatroomId, options, deps) {
12880
12867
  const backlogItems = await d.backend.query(api.backlog.listBacklogItems, {
12881
12868
  sessionId,
12882
12869
  chatroomId,
12883
- statusFilter: "active",
12870
+ statusFilter: "backlog",
12871
+ sort: options.sort,
12872
+ filter: options.filter,
12884
12873
  limit
12885
12874
  });
12886
12875
  console.log("");
12887
12876
  console.log("══════════════════════════════════════════════════");
12888
- console.log("\uD83D\uDCCB ACTIVE BACKLOG");
12877
+ console.log("\uD83D\uDCCB BACKLOG");
12889
12878
  console.log("══════════════════════════════════════════════════");
12890
12879
  console.log(`Chatroom: ${chatroomId}`);
12891
12880
  console.log("");
12892
12881
  if (backlogItems.length === 0) {
12893
- console.log("No active backlog items.");
12882
+ console.log("No backlog items.");
12894
12883
  } else {
12895
12884
  console.log("──────────────────────────────────────────────────");
12896
12885
  for (let i2 = 0;i2 < backlogItems.length; i2++) {
@@ -13173,7 +13162,7 @@ async function historyBacklog(chatroomId, options, deps) {
13173
13162
  const sessionId = requireAuth2(d);
13174
13163
  validateChatroomId(chatroomId);
13175
13164
  const now = Date.now();
13176
- const defaultFrom = now - 30 * 24 * 60 * 60 * 1000;
13165
+ const defaultFrom = now - 2592000000;
13177
13166
  let fromMs;
13178
13167
  let toMs;
13179
13168
  if (options.from) {
@@ -13263,6 +13252,38 @@ async function historyBacklog(chatroomId, options, deps) {
13263
13252
  return;
13264
13253
  }
13265
13254
  }
13255
+ async function closeBacklog(chatroomId, options, deps) {
13256
+ const d = deps ?? await createDefaultDeps11();
13257
+ const sessionId = requireAuth2(d);
13258
+ validateChatroomId(chatroomId);
13259
+ if (!options.backlogItemId || options.backlogItemId.trim().length === 0) {
13260
+ console.error(`❌ Backlog item ID is required`);
13261
+ process.exit(1);
13262
+ return;
13263
+ }
13264
+ if (!options.reason || options.reason.trim().length === 0) {
13265
+ console.error(`❌ Reason is required when closing a backlog item`);
13266
+ process.exit(1);
13267
+ return;
13268
+ }
13269
+ try {
13270
+ await d.backend.mutation(api.backlog.closeBacklogItem, {
13271
+ sessionId,
13272
+ itemId: options.backlogItemId,
13273
+ reason: options.reason
13274
+ });
13275
+ console.log("");
13276
+ console.log("✅ Backlog item closed");
13277
+ console.log(` ID: ${options.backlogItemId}`);
13278
+ console.log(` Status: closed`);
13279
+ console.log(` Reason: ${options.reason}`);
13280
+ console.log("");
13281
+ } catch (error) {
13282
+ console.error(`❌ Failed to close backlog item: ${error.message}`);
13283
+ process.exit(1);
13284
+ return;
13285
+ }
13286
+ }
13266
13287
  function getStatusEmoji(status) {
13267
13288
  switch (status) {
13268
13289
  case "pending":
@@ -13283,10 +13304,121 @@ function getStatusEmoji(status) {
13283
13304
  return "⚫";
13284
13305
  }
13285
13306
  }
13307
+ function computeContentHash(content) {
13308
+ return createHash("sha256").update(content).digest("hex");
13309
+ }
13310
+ async function exportBacklog(chatroomId, options, deps) {
13311
+ const d = deps ?? await createDefaultDeps11();
13312
+ const sessionId = requireAuth2(d);
13313
+ validateChatroomId(chatroomId);
13314
+ if (!d.fs) {
13315
+ console.error("❌ File system operations not available");
13316
+ process.exit(1);
13317
+ return;
13318
+ }
13319
+ try {
13320
+ const backlogItems = await d.backend.query(api.backlog.listBacklogItems, {
13321
+ sessionId,
13322
+ chatroomId,
13323
+ statusFilter: "backlog"
13324
+ });
13325
+ const exportData = {
13326
+ exportedAt: Date.now(),
13327
+ chatroomId,
13328
+ items: backlogItems.map((item) => {
13329
+ const exportItem = {
13330
+ contentHash: computeContentHash(item.content),
13331
+ content: item.content,
13332
+ status: item.status,
13333
+ createdBy: item.createdBy ?? "unknown",
13334
+ createdAt: item.createdAt
13335
+ };
13336
+ if (item.complexity)
13337
+ exportItem.complexity = item.complexity;
13338
+ if (item.value)
13339
+ exportItem.value = item.value;
13340
+ if (item.priority !== undefined)
13341
+ exportItem.priority = item.priority;
13342
+ return exportItem;
13343
+ })
13344
+ };
13345
+ const exportDir = options.path ?? nodePath.join(process.cwd(), DEFAULT_EXPORT_DIR);
13346
+ await d.fs.mkdir(exportDir, { recursive: true });
13347
+ const filePath = nodePath.join(exportDir, BACKLOG_EXPORT_FILENAME);
13348
+ await d.fs.writeFile(filePath, JSON.stringify(exportData, null, 2));
13349
+ console.log("");
13350
+ console.log(`✅ Exported ${exportData.items.length} backlog item(s)`);
13351
+ console.log(` File: ${filePath}`);
13352
+ console.log("");
13353
+ } catch (error) {
13354
+ console.error(`❌ Failed to export backlog items: ${error.message}`);
13355
+ process.exit(1);
13356
+ return;
13357
+ }
13358
+ }
13359
+ async function importBacklog(chatroomId, options, deps) {
13360
+ const d = deps ?? await createDefaultDeps11();
13361
+ const sessionId = requireAuth2(d);
13362
+ validateChatroomId(chatroomId);
13363
+ if (!d.fs) {
13364
+ console.error("❌ File system operations not available");
13365
+ process.exit(1);
13366
+ return;
13367
+ }
13368
+ try {
13369
+ const importDir = options.path ?? nodePath.join(process.cwd(), DEFAULT_EXPORT_DIR);
13370
+ const filePath = nodePath.join(importDir, BACKLOG_EXPORT_FILENAME);
13371
+ const raw = await d.fs.readFile(filePath, "utf-8");
13372
+ const exportData = JSON.parse(raw);
13373
+ const ageMs = Date.now() - exportData.exportedAt;
13374
+ if (ageMs > STALENESS_THRESHOLD_MS) {
13375
+ const ageDays = Math.floor(ageMs / 86400000);
13376
+ console.log(`⚠️ This export is ${ageDays} days old and may be stale.`);
13377
+ }
13378
+ const existingItems = await d.backend.query(api.backlog.listBacklogItems, {
13379
+ sessionId,
13380
+ chatroomId,
13381
+ statusFilter: "backlog"
13382
+ });
13383
+ const existingHashes = new Set(existingItems.map((item) => computeContentHash(item.content)));
13384
+ let imported = 0;
13385
+ let skipped = 0;
13386
+ for (const item of exportData.items) {
13387
+ const hash = computeContentHash(item.content);
13388
+ if (existingHashes.has(hash)) {
13389
+ skipped++;
13390
+ continue;
13391
+ }
13392
+ await d.backend.mutation(api.backlog.createBacklogItem, {
13393
+ sessionId,
13394
+ chatroomId,
13395
+ content: item.content,
13396
+ createdBy: item.createdBy,
13397
+ priority: item.priority,
13398
+ complexity: item.complexity,
13399
+ value: item.value
13400
+ });
13401
+ existingHashes.add(hash);
13402
+ imported++;
13403
+ }
13404
+ console.log("");
13405
+ console.log(`✅ Import complete`);
13406
+ console.log(` Total items in file: ${exportData.items.length}`);
13407
+ console.log(` Imported: ${imported}`);
13408
+ console.log(` Skipped (duplicate): ${skipped}`);
13409
+ console.log("");
13410
+ } catch (error) {
13411
+ console.error(`❌ Failed to import backlog items: ${error.message}`);
13412
+ process.exit(1);
13413
+ return;
13414
+ }
13415
+ }
13416
+ var BACKLOG_EXPORT_FILENAME = "backlog-export.json", STALENESS_THRESHOLD_MS, DEFAULT_EXPORT_DIR = ".chatroom/exports";
13286
13417
  var init_backlog = __esm(() => {
13287
13418
  init_api3();
13288
13419
  init_storage();
13289
13420
  init_client2();
13421
+ STALENESS_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1000;
13290
13422
  });
13291
13423
 
13292
13424
  // src/utils/file-content.ts
@@ -13378,6 +13510,32 @@ async function taskRead(chatroomId, options, deps) {
13378
13510
  console.log(`✅ Task content:`);
13379
13511
  console.log(` Task ID: ${result.taskId}`);
13380
13512
  console.log(` Status: ${result.status}`);
13513
+ if (result.context) {
13514
+ console.log("");
13515
+ console.log("PINNED CONTEXT");
13516
+ console.log("---");
13517
+ console.log("<context>");
13518
+ console.log(result.context.content);
13519
+ console.log("</context>");
13520
+ if (result.context.triggerMessageContent) {
13521
+ console.log("in response to");
13522
+ const senderTag = result.context.triggerMessageSenderRole ?? "unknown";
13523
+ console.log(`<${senderTag}-message>`);
13524
+ console.log(result.context.triggerMessageContent);
13525
+ console.log(`</${senderTag}-message>`);
13526
+ }
13527
+ const hoursAgo = Math.round(result.context.elapsedHours);
13528
+ const msgsSince = result.context.messagesSinceContext;
13529
+ const isStale = hoursAgo >= 24 || msgsSince >= 50;
13530
+ if (isStale) {
13531
+ const ageLabel = hoursAgo >= 48 ? `${Math.round(hoursAgo / 24)}d old` : hoursAgo >= 24 ? `${hoursAgo}h old` : `${msgsSince} messages old`;
13532
+ console.log(`<system-notice>`);
13533
+ console.log(`⚠️ Context is ${ageLabel}.`);
13534
+ console.log(` Entry point role will update when needed.`);
13535
+ console.log(`</system-notice>`);
13536
+ }
13537
+ console.log("---");
13538
+ }
13381
13539
  console.log(`
13382
13540
  ${result.content}`);
13383
13541
  } catch (error) {
@@ -14280,310 +14438,98 @@ var init_get_system_prompt = __esm(() => {
14280
14438
  // ../../services/backend/config/reliability.ts
14281
14439
  var DAEMON_HEARTBEAT_INTERVAL_MS = 30000, AGENT_REQUEST_DEADLINE_MS = 120000;
14282
14440
 
14283
- // src/commands/machine/daemon-start/utils.ts
14284
- function formatTimestamp() {
14285
- return new Date().toISOString().replace("T", " ").substring(0, 19);
14286
- }
14287
-
14288
- // src/events/lifecycle/on-agent-shutdown.ts
14289
- async function onAgentShutdown(ctx, options) {
14290
- const { chatroomId, role, pid, skipKill } = options;
14291
- try {
14292
- ctx.deps.stops.mark(chatroomId, role, options.stopReason ?? "user.stop");
14293
- } catch (e) {
14294
- console.log(` ⚠️ Failed to mark intentional stop for ${role}: ${e.message}`);
14295
- }
14296
- let killed = false;
14297
- if (!skipKill) {
14298
- try {
14299
- ctx.deps.processes.kill(-pid, "SIGTERM");
14300
- } catch (e) {
14301
- const isEsrch = e.code === "ESRCH" || e.message?.includes("ESRCH");
14302
- if (isEsrch) {
14303
- killed = true;
14304
- }
14305
- if (!isEsrch) {
14306
- console.log(` ⚠️ Failed to send SIGTERM to ${role}: ${e.message}`);
14307
- }
14308
- }
14309
- if (!killed) {
14310
- const SIGTERM_TIMEOUT_MS = 1e4;
14311
- const POLL_INTERVAL_MS2 = 500;
14312
- const deadline = Date.now() + SIGTERM_TIMEOUT_MS;
14313
- while (Date.now() < deadline) {
14314
- await ctx.deps.clock.delay(POLL_INTERVAL_MS2);
14315
- try {
14316
- ctx.deps.processes.kill(pid, 0);
14317
- } catch {
14318
- killed = true;
14319
- break;
14320
- }
14321
- }
14322
- }
14323
- if (!killed) {
14324
- try {
14325
- ctx.deps.processes.kill(-pid, "SIGKILL");
14326
- } catch {
14327
- killed = true;
14328
- }
14329
- }
14330
- if (!killed) {
14331
- await ctx.deps.clock.delay(5000);
14332
- try {
14333
- ctx.deps.processes.kill(pid, 0);
14334
- console.log(` ⚠️ Process ${pid} (${role}) still alive after SIGKILL — possible zombie`);
14335
- } catch {
14336
- killed = true;
14337
- }
14338
- }
14339
- }
14340
- if (killed || skipKill) {
14341
- try {
14342
- ctx.deps.machine.clearAgentPid(ctx.machineId, chatroomId, role);
14343
- } catch (e) {
14344
- console.log(` ⚠️ Failed to clear local PID for ${role}: ${e.message}`);
14345
- }
14346
- }
14347
- return {
14348
- killed: killed || (skipKill ?? false),
14349
- cleaned: killed || (skipKill ?? false)
14350
- };
14351
- }
14352
-
14353
- // src/events/lifecycle/on-daemon-shutdown.ts
14354
- async function onDaemonShutdown(ctx) {
14355
- const agents = ctx.deps.machine.listAgentEntries(ctx.machineId);
14356
- if (agents.length > 0) {
14357
- console.log(`[${formatTimestamp()}] Stopping ${agents.length} agent(s)...`);
14358
- await Promise.allSettled(agents.map(async ({ chatroomId, role, entry }) => {
14359
- const result = await onAgentShutdown(ctx, {
14360
- chatroomId,
14361
- role,
14362
- pid: entry.pid
14363
- });
14364
- if (result.killed) {
14365
- console.log(` Sent SIGTERM to ${role} (PID ${entry.pid})`);
14366
- } else {
14367
- console.log(` ${role} (PID ${entry.pid}) already exited`);
14368
- }
14369
- return result;
14370
- }));
14371
- await ctx.deps.clock.delay(AGENT_SHUTDOWN_TIMEOUT_MS);
14372
- for (const { role, entry } of agents) {
14373
- try {
14374
- ctx.deps.processes.kill(entry.pid, 0);
14375
- ctx.deps.processes.kill(entry.pid, "SIGKILL");
14376
- console.log(` Force-killed ${role} (PID ${entry.pid})`);
14377
- } catch {}
14378
- }
14379
- console.log(`[${formatTimestamp()}] All agents stopped`);
14441
+ // src/events/daemon/agent/on-request-start-agent.ts
14442
+ async function onRequestStartAgent(ctx, event) {
14443
+ const eventId = event._id.toString();
14444
+ if (Date.now() > event.deadline) {
14445
+ console.log(`[daemon] ⏰ Skipping expired agent.requestStart for role=${event.role} (id: ${eventId}, deadline passed)`);
14446
+ return;
14380
14447
  }
14381
- try {
14382
- await ctx.deps.backend.mutation(api.machines.updateDaemonStatus, {
14448
+ console.log(`[daemon] Processing agent.requestStart (id: ${eventId})`);
14449
+ const result = await ctx.deps.agentProcessManager.ensureRunning({
14450
+ chatroomId: event.chatroomId,
14451
+ role: event.role,
14452
+ agentHarness: event.agentHarness,
14453
+ model: event.model,
14454
+ workingDir: event.workingDir,
14455
+ reason: event.reason
14456
+ });
14457
+ if (!result.success) {
14458
+ console.log(`[daemon] Agent start rejected for role=${event.role}: ${result.error ?? "unknown"}`);
14459
+ } else {
14460
+ ctx.deps.backend.mutation(api.workspaces.registerWorkspace, {
14383
14461
  sessionId: ctx.sessionId,
14462
+ chatroomId: event.chatroomId,
14384
14463
  machineId: ctx.machineId,
14385
- connected: false
14464
+ workingDir: event.workingDir,
14465
+ hostname: ctx.config?.hostname ?? "unknown",
14466
+ registeredBy: event.role
14467
+ }).catch((err) => {
14468
+ console.warn(`[daemon] ⚠️ Failed to register workspace: ${err.message}`);
14386
14469
  });
14387
- } catch {}
14470
+ }
14388
14471
  }
14389
- var AGENT_SHUTDOWN_TIMEOUT_MS = 5000;
14390
- var init_on_daemon_shutdown = __esm(() => {
14472
+ var init_on_request_start_agent = __esm(() => {
14391
14473
  init_api3();
14392
14474
  });
14393
14475
 
14394
- // src/commands/machine/daemon-start/handlers/shared.ts
14395
- async function clearAgentPidEverywhere(ctx, chatroomId, role) {
14396
- try {
14397
- await ctx.deps.backend.mutation(api.machines.updateSpawnedAgent, {
14398
- sessionId: ctx.sessionId,
14399
- machineId: ctx.machineId,
14400
- chatroomId,
14401
- role,
14402
- pid: undefined
14403
- });
14404
- } catch (e) {
14405
- console.log(` ⚠️ Failed to clear PID in backend: ${e.message}`);
14406
- }
14407
- ctx.deps.machine.clearAgentPid(ctx.machineId, chatroomId, role);
14476
+ // src/commands/machine/daemon-start/handlers/stop-agent.ts
14477
+ async function executeStopAgent(ctx, args) {
14478
+ const { chatroomId, role, reason } = args;
14479
+ console.log(` ↪ stop-agent command received`);
14480
+ console.log(` Chatroom: ${chatroomId}`);
14481
+ console.log(` Role: ${role}`);
14482
+ console.log(` Reason: ${reason}`);
14483
+ const result = await ctx.deps.agentProcessManager.stop({
14484
+ chatroomId,
14485
+ role,
14486
+ reason
14487
+ });
14488
+ const msg = result.success ? `Agent stopped (${role})` : `Failed to stop agent (${role})`;
14489
+ console.log(` ${result.success ? "✅" : "⚠️ "} ${msg}`);
14490
+ return { result: msg, failed: !result.success };
14408
14491
  }
14409
- var init_shared = __esm(() => {
14410
- init_api3();
14411
- });
14412
14492
 
14413
- // src/commands/machine/daemon-start/handlers/state-recovery.ts
14414
- async function recoverAgentState(ctx) {
14415
- const entries = ctx.deps.machine.listAgentEntries(ctx.machineId);
14416
- if (entries.length === 0) {
14417
- console.log(` No agent entries found — nothing to recover`);
14418
- } else {
14419
- let recovered = 0;
14420
- let cleared = 0;
14421
- const chatroomIds = new Set;
14422
- for (const { chatroomId, role, entry } of entries) {
14423
- const { pid, harness } = entry;
14424
- const service = ctx.agentServices.get(harness) ?? ctx.agentServices.values().next().value;
14425
- const alive = service ? service.isAlive(pid) : false;
14426
- if (alive) {
14427
- console.log(` ✅ Recovered: ${role} (PID ${pid}, harness: ${harness})`);
14428
- recovered++;
14429
- chatroomIds.add(chatroomId);
14430
- } else {
14431
- console.log(` \uD83E\uDDF9 Stale PID ${pid} for ${role} — clearing`);
14432
- await clearAgentPidEverywhere(ctx, chatroomId, role);
14433
- cleared++;
14434
- }
14435
- }
14436
- console.log(` Recovery complete: ${recovered} alive, ${cleared} stale cleared`);
14437
- for (const chatroomId of chatroomIds) {
14438
- try {
14439
- const configsResult = await ctx.deps.backend.query(api.machines.getMachineAgentConfigs, {
14440
- sessionId: ctx.sessionId,
14441
- chatroomId
14442
- });
14443
- for (const config3 of configsResult.configs) {
14444
- if (config3.machineId === ctx.machineId && config3.workingDir) {
14445
- ctx.activeWorkingDirs.add(config3.workingDir);
14446
- }
14447
- }
14448
- } catch {}
14449
- }
14450
- if (ctx.activeWorkingDirs.size > 0) {
14451
- console.log(` \uD83D\uDD00 Recovered ${ctx.activeWorkingDirs.size} active working dir(s) for git tracking`);
14452
- }
14493
+ // src/events/daemon/agent/on-request-stop-agent.ts
14494
+ async function onRequestStopAgent(ctx, event) {
14495
+ if (Date.now() > event.deadline) {
14496
+ console.log(`[daemon] ⏰ Skipping expired agent.requestStop for role=${event.role} (deadline passed)`);
14497
+ return;
14453
14498
  }
14499
+ await executeStopAgent(ctx, {
14500
+ chatroomId: event.chatroomId,
14501
+ role: event.role,
14502
+ reason: event.reason
14503
+ });
14454
14504
  }
14455
- var init_state_recovery = __esm(() => {
14456
- init_api3();
14457
- init_shared();
14458
- });
14505
+ var init_on_request_stop_agent = () => {};
14459
14506
 
14460
- // src/infrastructure/services/harness-spawning/rate-limiter.ts
14461
- class SpawnRateLimiter {
14462
- config;
14463
- buckets = new Map;
14464
- constructor(config3 = {}) {
14465
- this.config = { ...DEFAULT_CONFIG, ...config3 };
14507
+ // src/commands/machine/pid.ts
14508
+ import { createHash as createHash2 } from "node:crypto";
14509
+ import { existsSync as existsSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4, unlinkSync as unlinkSync2, mkdirSync as mkdirSync4 } from "node:fs";
14510
+ import { homedir as homedir4 } from "node:os";
14511
+ import { join as join6 } from "node:path";
14512
+ function getUrlHash() {
14513
+ const url = getConvexUrl();
14514
+ return createHash2("sha256").update(url).digest("hex").substring(0, 8);
14515
+ }
14516
+ function getPidFileName() {
14517
+ return `daemon-${getUrlHash()}.pid`;
14518
+ }
14519
+ function ensureChatroomDir() {
14520
+ if (!existsSync4(CHATROOM_DIR4)) {
14521
+ mkdirSync4(CHATROOM_DIR4, { recursive: true, mode: 448 });
14466
14522
  }
14467
- tryConsume(chatroomId, reason) {
14468
- if (reason.startsWith("user.")) {
14469
- return { allowed: true };
14470
- }
14471
- const bucket = this._getOrCreateBucket(chatroomId);
14472
- this._refill(bucket);
14473
- if (bucket.tokens < 1) {
14474
- const elapsed = Date.now() - bucket.lastRefillAt;
14475
- const retryAfterMs = this.config.refillRateMs - elapsed;
14476
- console.warn(`⚠️ [RateLimiter] Agent spawn rate-limited for chatroom ${chatroomId} (reason: ${reason}). Retry after ${retryAfterMs}ms`);
14477
- return { allowed: false, retryAfterMs };
14478
- }
14479
- bucket.tokens -= 1;
14480
- const remaining = Math.floor(bucket.tokens);
14481
- if (remaining <= LOW_TOKEN_THRESHOLD) {
14482
- console.warn(`⚠️ [RateLimiter] Agent spawn tokens running low for chatroom ${chatroomId} (${remaining}/${this.config.maxTokens} remaining)`);
14483
- }
14484
- return { allowed: true };
14485
- }
14486
- getStatus(chatroomId) {
14487
- const bucket = this._getOrCreateBucket(chatroomId);
14488
- this._refill(bucket);
14489
- return {
14490
- remaining: Math.floor(bucket.tokens),
14491
- total: this.config.maxTokens
14492
- };
14493
- }
14494
- _getOrCreateBucket(chatroomId) {
14495
- if (!this.buckets.has(chatroomId)) {
14496
- this.buckets.set(chatroomId, {
14497
- tokens: this.config.initialTokens,
14498
- lastRefillAt: Date.now()
14499
- });
14500
- }
14501
- return this.buckets.get(chatroomId);
14502
- }
14503
- _refill(bucket) {
14504
- const now = Date.now();
14505
- const elapsed = now - bucket.lastRefillAt;
14506
- if (elapsed >= this.config.refillRateMs) {
14507
- const tokensToAdd = Math.floor(elapsed / this.config.refillRateMs);
14508
- bucket.tokens = Math.min(this.config.maxTokens, bucket.tokens + tokensToAdd);
14509
- bucket.lastRefillAt += tokensToAdd * this.config.refillRateMs;
14510
- }
14511
- }
14512
- }
14513
- var DEFAULT_CONFIG, LOW_TOKEN_THRESHOLD = 1;
14514
- var init_rate_limiter = __esm(() => {
14515
- DEFAULT_CONFIG = {
14516
- maxTokens: 5,
14517
- refillRateMs: 60000,
14518
- initialTokens: 5
14519
- };
14520
- });
14521
-
14522
- // src/infrastructure/services/harness-spawning/harness-spawning-service.ts
14523
- class HarnessSpawningService {
14524
- rateLimiter;
14525
- concurrentAgents = new Map;
14526
- constructor({ rateLimiter }) {
14527
- this.rateLimiter = rateLimiter;
14528
- }
14529
- shouldAllowSpawn(chatroomId, reason) {
14530
- const current = this.concurrentAgents.get(chatroomId) ?? 0;
14531
- if (current >= MAX_CONCURRENT_AGENTS_PER_CHATROOM) {
14532
- console.warn(`⚠️ [HarnessSpawningService] Concurrent agent limit reached for chatroom ${chatroomId} ` + `(${current}/${MAX_CONCURRENT_AGENTS_PER_CHATROOM} active agents). Spawn rejected.`);
14533
- return { allowed: false };
14534
- }
14535
- const result = this.rateLimiter.tryConsume(chatroomId, reason);
14536
- if (!result.allowed) {
14537
- console.warn(`⚠️ [HarnessSpawningService] Spawn blocked by rate limiter for chatroom ${chatroomId} ` + `(reason: ${reason}).`);
14538
- }
14539
- return result;
14540
- }
14541
- recordSpawn(chatroomId) {
14542
- const current = this.concurrentAgents.get(chatroomId) ?? 0;
14543
- this.concurrentAgents.set(chatroomId, current + 1);
14544
- }
14545
- recordExit(chatroomId) {
14546
- const current = this.concurrentAgents.get(chatroomId) ?? 0;
14547
- const next = Math.max(0, current - 1);
14548
- this.concurrentAgents.set(chatroomId, next);
14549
- }
14550
- getConcurrentCount(chatroomId) {
14551
- return this.concurrentAgents.get(chatroomId) ?? 0;
14552
- }
14553
- }
14554
- var MAX_CONCURRENT_AGENTS_PER_CHATROOM = 10;
14555
-
14556
- // src/infrastructure/services/harness-spawning/index.ts
14557
- var init_harness_spawning = __esm(() => {
14558
- init_rate_limiter();
14559
- });
14560
-
14561
- // src/commands/machine/pid.ts
14562
- import { createHash } from "node:crypto";
14563
- import { existsSync as existsSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4, unlinkSync as unlinkSync2, mkdirSync as mkdirSync4 } from "node:fs";
14564
- import { homedir as homedir4 } from "node:os";
14565
- import { join as join5 } from "node:path";
14566
- function getUrlHash() {
14567
- const url = getConvexUrl();
14568
- return createHash("sha256").update(url).digest("hex").substring(0, 8);
14569
- }
14570
- function getPidFileName() {
14571
- return `daemon-${getUrlHash()}.pid`;
14572
- }
14573
- function ensureChatroomDir() {
14574
- if (!existsSync4(CHATROOM_DIR4)) {
14575
- mkdirSync4(CHATROOM_DIR4, { recursive: true, mode: 448 });
14576
- }
14577
- }
14578
- function getPidFilePath() {
14579
- return join5(CHATROOM_DIR4, getPidFileName());
14580
- }
14581
- function isProcessRunning(pid) {
14582
- try {
14583
- process.kill(pid, 0);
14584
- return true;
14585
- } catch {
14586
- return false;
14523
+ }
14524
+ function getPidFilePath() {
14525
+ return join6(CHATROOM_DIR4, getPidFileName());
14526
+ }
14527
+ function isProcessRunning(pid) {
14528
+ try {
14529
+ process.kill(pid, 0);
14530
+ return true;
14531
+ } catch {
14532
+ return false;
14587
14533
  }
14588
14534
  }
14589
14535
  function readPid() {
@@ -14641,852 +14587,566 @@ function releaseLock() {
14641
14587
  var CHATROOM_DIR4;
14642
14588
  var init_pid = __esm(() => {
14643
14589
  init_client2();
14644
- CHATROOM_DIR4 = join5(homedir4(), ".chatroom");
14590
+ CHATROOM_DIR4 = join6(homedir4(), ".chatroom");
14645
14591
  });
14646
14592
 
14647
- // src/events/daemon/event-bus.ts
14648
- class DaemonEventBus {
14649
- listeners = new Map;
14650
- on(event, listener) {
14651
- if (!this.listeners.has(event)) {
14652
- this.listeners.set(event, new Set);
14653
- }
14654
- this.listeners.get(event).add(listener);
14655
- return () => {
14656
- this.listeners.get(event)?.delete(listener);
14657
- };
14658
- }
14659
- emit(event, payload) {
14660
- const set = this.listeners.get(event);
14661
- if (!set)
14662
- return;
14663
- for (const listener of set) {
14664
- try {
14665
- listener(payload);
14666
- } catch (err) {
14667
- console.warn(`[EventBus] Listener error on "${event}": ${err.message}`);
14668
- }
14669
- }
14670
- }
14671
- removeAllListeners() {
14672
- this.listeners.clear();
14673
- }
14593
+ // src/commands/machine/daemon-start/utils.ts
14594
+ function formatTimestamp() {
14595
+ return new Date().toISOString().replace("T", " ").substring(0, 19);
14674
14596
  }
14675
14597
 
14676
- // src/events/daemon/agent/on-agent-exited.ts
14677
- function onAgentExited(ctx, payload) {
14678
- const { chatroomId, role, pid, code: code2, signal, stopReason, intentional } = payload;
14679
- const ts = formatTimestamp();
14680
- console.log(`[${ts}] Agent stopped: ${stopReason} (${role})`);
14681
- const isDaemonRespawn = stopReason === "daemon.respawn";
14682
- const isIntentional = intentional && !isDaemonRespawn;
14683
- if (isIntentional) {
14684
- console.log(`[${ts}] ℹ️ Agent process exited after intentional stop ` + `(PID: ${pid}, role: ${role}, code: ${code2}, signal: ${signal})`);
14685
- } else if (isDaemonRespawn) {
14686
- console.log(`[${ts}] \uD83D\uDD04 Agent process stopped for respawn ` + `(PID: ${pid}, role: ${role}, code: ${code2}, signal: ${signal})`);
14687
- } else {
14688
- console.log(`[${ts}] ⚠️ Agent process exited ` + `(PID: ${pid}, role: ${role}, code: ${code2}, signal: ${signal})`);
14689
- }
14690
- ctx.deps.backend.mutation(api.machines.recordAgentExited, {
14691
- sessionId: ctx.sessionId,
14692
- machineId: ctx.machineId,
14693
- chatroomId,
14694
- role,
14695
- pid,
14696
- intentional,
14697
- stopReason,
14698
- stopSignal: stopReason === "agent_process.signal" ? signal ?? undefined : undefined,
14699
- exitCode: code2 ?? undefined,
14700
- signal: signal ?? undefined
14701
- }).catch((err) => {
14702
- console.log(` ⚠️ Failed to record agent exit event: ${err.message}`);
14703
- });
14704
- ctx.deps.machine.clearAgentPid(ctx.machineId, chatroomId, role);
14705
- for (const service of ctx.agentServices.values()) {
14706
- service.untrack(pid);
14707
- }
14598
+ // src/infrastructure/git/types.ts
14599
+ function makeGitStateKey(machineId, workingDir) {
14600
+ return `${machineId}::${workingDir}`;
14708
14601
  }
14709
- var init_on_agent_exited = __esm(() => {
14710
- init_api3();
14711
- });
14602
+ var FULL_DIFF_MAX_BYTES = 500000, COMMITS_PER_PAGE = 20;
14712
14603
 
14713
- // src/events/daemon/agent/on-agent-started.ts
14714
- function onAgentStarted(ctx, payload) {
14715
- const ts = formatTimestamp();
14716
- console.log(`[${ts}] \uD83D\uDFE2 Agent started: ${payload.role} (PID: ${payload.pid}, harness: ${payload.harness})`);
14604
+ // src/infrastructure/git/git-reader.ts
14605
+ import { exec as exec2 } from "node:child_process";
14606
+ import { promisify as promisify2 } from "node:util";
14607
+ async function runGit(args, cwd) {
14608
+ try {
14609
+ const result = await execAsync2(`git ${args}`, {
14610
+ cwd,
14611
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_PAGER: "cat", NO_COLOR: "1" },
14612
+ maxBuffer: FULL_DIFF_MAX_BYTES + 64 * 1024
14613
+ });
14614
+ return result;
14615
+ } catch (err) {
14616
+ return { error: err };
14617
+ }
14717
14618
  }
14718
- var init_on_agent_started = () => {};
14719
-
14720
- // src/events/daemon/agent/on-agent-stopped.ts
14721
- function onAgentStopped(ctx, payload) {
14722
- const ts = formatTimestamp();
14723
- console.log(`[${ts}] \uD83D\uDD34 Agent stopped: ${payload.role} (PID: ${payload.pid})`);
14619
+ function isGitNotInstalled(message) {
14620
+ return message.includes("command not found") || message.includes("ENOENT") || message.includes("not found") || message.includes("'git' is not recognized");
14724
14621
  }
14725
- var init_on_agent_stopped = () => {};
14726
-
14727
- // src/events/daemon/register-listeners.ts
14728
- function registerEventListeners(ctx) {
14729
- const unsubs = [];
14730
- unsubs.push(ctx.events.on("agent:exited", (payload) => onAgentExited(ctx, payload)));
14731
- unsubs.push(ctx.events.on("agent:started", (payload) => onAgentStarted(ctx, payload)));
14732
- unsubs.push(ctx.events.on("agent:stopped", (payload) => onAgentStopped(ctx, payload)));
14733
- return () => {
14734
- for (const unsub of unsubs) {
14735
- unsub();
14736
- }
14737
- };
14622
+ function isNotAGitRepo(message) {
14623
+ return message.includes("not a git repository") || message.includes("Not a git repository");
14738
14624
  }
14739
- var init_register_listeners = __esm(() => {
14740
- init_on_agent_exited();
14741
- init_on_agent_started();
14742
- init_on_agent_stopped();
14743
- });
14744
-
14745
- // src/commands/machine/daemon-start/init.ts
14746
- import { stat } from "node:fs/promises";
14747
- async function discoverModels(agentServices) {
14748
- const results = {};
14749
- for (const [harness, service] of agentServices) {
14750
- if (service.isInstalled()) {
14751
- try {
14752
- results[harness] = await service.listModels();
14753
- } catch {
14754
- results[harness] = [];
14755
- }
14625
+ function isPermissionDenied(message) {
14626
+ return message.includes("Permission denied") || message.includes("EACCES");
14627
+ }
14628
+ function isEmptyRepo(stderr) {
14629
+ 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");
14630
+ }
14631
+ function classifyError(errMessage) {
14632
+ if (isGitNotInstalled(errMessage)) {
14633
+ return { status: "error", message: "git is not installed or not in PATH" };
14634
+ }
14635
+ if (isNotAGitRepo(errMessage)) {
14636
+ return { status: "not_found" };
14637
+ }
14638
+ if (isPermissionDenied(errMessage)) {
14639
+ return { status: "error", message: `Permission denied: ${errMessage}` };
14640
+ }
14641
+ return { status: "error", message: errMessage.trim() };
14642
+ }
14643
+ async function isGitRepo(workingDir) {
14644
+ const result = await runGit("rev-parse --git-dir", workingDir);
14645
+ if ("error" in result)
14646
+ return false;
14647
+ return result.stdout.trim().length > 0;
14648
+ }
14649
+ async function getBranch(workingDir) {
14650
+ const result = await runGit("rev-parse --abbrev-ref HEAD", workingDir);
14651
+ if ("error" in result) {
14652
+ const errMsg = result.error.message;
14653
+ if (errMsg.includes("unknown revision") || errMsg.includes("No such file or directory") || errMsg.includes("does not have any commits")) {
14654
+ return { status: "available", branch: "HEAD" };
14756
14655
  }
14656
+ return classifyError(errMsg);
14757
14657
  }
14758
- return results;
14658
+ const branch = result.stdout.trim();
14659
+ if (!branch) {
14660
+ return { status: "error", message: "git rev-parse returned empty output" };
14661
+ }
14662
+ return { status: "available", branch };
14759
14663
  }
14760
- function createDefaultDeps19() {
14664
+ async function isDirty(workingDir) {
14665
+ const result = await runGit("status --porcelain", workingDir);
14666
+ if ("error" in result)
14667
+ return false;
14668
+ return result.stdout.trim().length > 0;
14669
+ }
14670
+ function parseDiffStatLine(statLine) {
14671
+ const filesMatch = statLine.match(/(\d+)\s+file/);
14672
+ const insertMatch = statLine.match(/(\d+)\s+insertion/);
14673
+ const deleteMatch = statLine.match(/(\d+)\s+deletion/);
14761
14674
  return {
14762
- backend: {
14763
- mutation: async () => {
14764
- throw new Error("Backend not initialized");
14765
- },
14766
- query: async () => {
14767
- throw new Error("Backend not initialized");
14768
- }
14769
- },
14770
- processes: {
14771
- kill: (pid, signal) => process.kill(pid, signal)
14772
- },
14773
- fs: {
14774
- stat
14775
- },
14776
- stops: {
14777
- mark: markIntentionalStop,
14778
- consume: consumeIntentionalStop,
14779
- clear: clearIntentionalStop
14780
- },
14781
- machine: {
14782
- clearAgentPid,
14783
- persistAgentPid,
14784
- listAgentEntries,
14785
- persistEventCursor,
14786
- loadEventCursor
14787
- },
14788
- clock: {
14789
- now: () => Date.now(),
14790
- delay: (ms) => new Promise((resolve2) => setTimeout(resolve2, ms))
14791
- },
14792
- spawning: new HarnessSpawningService({ rateLimiter: new SpawnRateLimiter })
14675
+ filesChanged: filesMatch ? parseInt(filesMatch[1], 10) : 0,
14676
+ insertions: insertMatch ? parseInt(insertMatch[1], 10) : 0,
14677
+ deletions: deleteMatch ? parseInt(deleteMatch[1], 10) : 0
14793
14678
  };
14794
14679
  }
14795
- function validateAuthentication(convexUrl) {
14796
- const sessionId = getSessionId();
14797
- if (!sessionId) {
14798
- const otherUrls = getOtherSessionUrls();
14799
- console.error(`❌ Not authenticated for: ${convexUrl}`);
14800
- if (otherUrls.length > 0) {
14801
- console.error(`
14802
- \uD83D\uDCA1 You have sessions for other environments:`);
14803
- for (const url of otherUrls) {
14804
- console.error(` • ${url}`);
14805
- }
14680
+ async function getDiffStat(workingDir) {
14681
+ const result = await runGit("diff HEAD --stat", workingDir);
14682
+ if ("error" in result) {
14683
+ const errMsg = result.error.message;
14684
+ if (isEmptyRepo(result.error.message)) {
14685
+ return { status: "no_commits" };
14806
14686
  }
14807
- console.error(`
14808
- Run: chatroom auth login`);
14809
- releaseLock();
14810
- process.exit(1);
14687
+ const classified = classifyError(errMsg);
14688
+ if (classified.status === "not_found")
14689
+ return { status: "not_found" };
14690
+ return classified;
14811
14691
  }
14812
- return sessionId;
14692
+ const output = result.stdout;
14693
+ const stderr = result.stderr;
14694
+ if (isEmptyRepo(stderr)) {
14695
+ return { status: "no_commits" };
14696
+ }
14697
+ if (!output.trim()) {
14698
+ return {
14699
+ status: "available",
14700
+ diffStat: { filesChanged: 0, insertions: 0, deletions: 0 }
14701
+ };
14702
+ }
14703
+ const lines = output.trim().split(`
14704
+ `);
14705
+ const summaryLine = lines[lines.length - 1] ?? "";
14706
+ const diffStat = parseDiffStatLine(summaryLine);
14707
+ return { status: "available", diffStat };
14813
14708
  }
14814
- async function validateSession(client2, sessionId, convexUrl) {
14815
- const validation = await client2.query(api.cliAuth.validateSession, { sessionId });
14816
- if (!validation.valid) {
14817
- console.error(`❌ Session invalid: ${validation.reason}`);
14818
- console.error(`
14819
- Run: chatroom auth login`);
14820
- releaseLock();
14821
- process.exit(1);
14709
+ async function getFullDiff(workingDir) {
14710
+ const result = await runGit("diff HEAD", workingDir);
14711
+ if ("error" in result) {
14712
+ const errMsg = result.error.message;
14713
+ if (isEmptyRepo(errMsg)) {
14714
+ return { status: "no_commits" };
14715
+ }
14716
+ const classified = classifyError(errMsg);
14717
+ if (classified.status === "not_found")
14718
+ return { status: "not_found" };
14719
+ return classified;
14720
+ }
14721
+ const stderr = result.stderr;
14722
+ if (isEmptyRepo(stderr)) {
14723
+ return { status: "no_commits" };
14724
+ }
14725
+ const raw = result.stdout;
14726
+ const byteLength2 = Buffer.byteLength(raw, "utf8");
14727
+ if (byteLength2 > FULL_DIFF_MAX_BYTES) {
14728
+ const truncated = Buffer.from(raw, "utf8").subarray(0, FULL_DIFF_MAX_BYTES).toString("utf8");
14729
+ return { status: "truncated", content: truncated, truncated: true };
14822
14730
  }
14731
+ return { status: "available", content: raw, truncated: false };
14823
14732
  }
14824
- function setupMachine() {
14825
- ensureMachineRegistered();
14826
- const config3 = loadMachineConfig();
14827
- return config3;
14733
+ async function getRecentCommits(workingDir, count = 20, skip = 0) {
14734
+ const format = "%H%x00%h%x00%s%x00%an%x00%aI";
14735
+ const skipArg = skip > 0 ? ` --skip=${skip}` : "";
14736
+ const result = await runGit(`log -${count}${skipArg} --format=${format}`, workingDir);
14737
+ if ("error" in result) {
14738
+ return [];
14739
+ }
14740
+ const output = result.stdout.trim();
14741
+ if (!output)
14742
+ return [];
14743
+ const commits = [];
14744
+ for (const line of output.split(`
14745
+ `)) {
14746
+ const trimmed = line.trim();
14747
+ if (!trimmed)
14748
+ continue;
14749
+ const parts = trimmed.split("\x00");
14750
+ if (parts.length !== 5)
14751
+ continue;
14752
+ const [sha, shortSha, message, author, date] = parts;
14753
+ commits.push({ sha, shortSha, message, author, date });
14754
+ }
14755
+ return commits;
14828
14756
  }
14829
- async function registerCapabilities(client2, sessionId, config3, agentServices) {
14830
- const { machineId } = config3;
14831
- const availableModels = await discoverModels(agentServices);
14832
- try {
14833
- await client2.mutation(api.machines.register, {
14834
- sessionId,
14835
- machineId,
14836
- hostname: config3.hostname,
14837
- os: config3.os,
14838
- availableHarnesses: config3.availableHarnesses,
14839
- harnessVersions: config3.harnessVersions,
14840
- availableModels
14841
- });
14842
- } catch (error) {
14843
- console.warn(`⚠️ Machine registration update failed: ${error.message}`);
14757
+ async function getCommitDetail(workingDir, sha) {
14758
+ const result = await runGit(`show ${sha} --format="" --stat -p`, workingDir);
14759
+ if ("error" in result) {
14760
+ const errMsg = result.error.message;
14761
+ const classified = classifyError(errMsg);
14762
+ if (classified.status === "not_found")
14763
+ return { status: "not_found" };
14764
+ if (isEmptyRepo(errMsg) || errMsg.includes("unknown revision") || errMsg.includes("bad object") || errMsg.includes("does not exist")) {
14765
+ return { status: "not_found" };
14766
+ }
14767
+ return classified;
14844
14768
  }
14845
- return availableModels;
14769
+ const raw = result.stdout;
14770
+ const byteLength2 = Buffer.byteLength(raw, "utf8");
14771
+ if (byteLength2 > FULL_DIFF_MAX_BYTES) {
14772
+ const truncated = Buffer.from(raw, "utf8").subarray(0, FULL_DIFF_MAX_BYTES).toString("utf8");
14773
+ return { status: "truncated", content: truncated, truncated: true };
14774
+ }
14775
+ return { status: "available", content: raw, truncated: false };
14846
14776
  }
14847
- async function connectDaemon(client2, sessionId, machineId, convexUrl) {
14777
+ async function getCommitMetadata(workingDir, sha) {
14778
+ const format = "%s%x00%an%x00%aI";
14779
+ const result = await runGit(`log -1 --format=${format} ${sha}`, workingDir);
14780
+ if ("error" in result)
14781
+ return null;
14782
+ const output = result.stdout.trim();
14783
+ if (!output)
14784
+ return null;
14785
+ const parts = output.split("\x00");
14786
+ if (parts.length !== 3)
14787
+ return null;
14788
+ return { message: parts[0], author: parts[1], date: parts[2] };
14789
+ }
14790
+ async function runCommand(command, cwd) {
14848
14791
  try {
14849
- await client2.mutation(api.machines.updateDaemonStatus, {
14850
- sessionId,
14851
- machineId,
14852
- connected: true
14792
+ const result = await execAsync2(command, {
14793
+ cwd,
14794
+ env: { ...process.env, NO_COLOR: "1" },
14795
+ timeout: 15000
14853
14796
  });
14854
- } catch (error) {
14855
- if (isNetworkError(error)) {
14856
- formatConnectivityError(error, convexUrl);
14857
- } else {
14858
- console.error(`❌ Failed to update daemon status: ${error.message}`);
14859
- }
14860
- releaseLock();
14861
- process.exit(1);
14797
+ return result;
14798
+ } catch (err) {
14799
+ return { error: err };
14862
14800
  }
14863
14801
  }
14864
- function logStartup(ctx, availableModels) {
14865
- console.log(`[${formatTimestamp()}] \uD83D\uDE80 Daemon started`);
14866
- console.log(` CLI version: ${getVersion()}`);
14867
- console.log(` Machine ID: ${ctx.machineId}`);
14868
- console.log(` Hostname: ${ctx.config?.hostname ?? "unknown"}`);
14869
- console.log(` Available harnesses: ${ctx.config?.availableHarnesses.join(", ") || "none"}`);
14870
- console.log(` Available models: ${Object.keys(availableModels).length > 0 ? `${Object.values(availableModels).flat().length} models across ${Object.keys(availableModels).join(", ")}` : "none discovered"}`);
14871
- console.log(` PID: ${process.pid}`);
14802
+ function parseRepoSlug(remoteUrl) {
14803
+ const trimmed = remoteUrl.trim();
14804
+ const httpsMatch = trimmed.match(/https?:\/\/[^/]+\/([^/]+)\/([^/]+?)(?:\.git)?$/);
14805
+ if (httpsMatch && httpsMatch[1] && httpsMatch[2]) {
14806
+ return `${httpsMatch[1]}/${httpsMatch[2]}`;
14807
+ }
14808
+ const sshMatch = trimmed.match(/git@[^:]+:([^/]+)\/([^/]+?)(?:\.git)?$/);
14809
+ if (sshMatch && sshMatch[1] && sshMatch[2]) {
14810
+ return `${sshMatch[1]}/${sshMatch[2]}`;
14811
+ }
14812
+ return null;
14872
14813
  }
14873
- async function recoverState(ctx) {
14874
- console.log(`
14875
- [${formatTimestamp()}] \uD83D\uDD04 Recovering agent state...`);
14814
+ async function getOriginRepoSlug(cwd) {
14815
+ const result = await runGit("remote get-url origin", cwd);
14816
+ if ("error" in result)
14817
+ return null;
14818
+ const url = result.stdout.trim();
14819
+ if (!url)
14820
+ return null;
14821
+ return parseRepoSlug(url);
14822
+ }
14823
+ async function getOpenPRsForBranch(cwd, branch) {
14824
+ const repoSlug = await getOriginRepoSlug(cwd);
14825
+ const repoFlag = repoSlug ? ` --repo ${JSON.stringify(repoSlug)}` : "";
14826
+ const result = await runCommand(`gh pr list --head ${JSON.stringify(branch)} --state open --json number,title,url,headRefName,state --limit 5${repoFlag}`, cwd);
14827
+ if ("error" in result) {
14828
+ return [];
14829
+ }
14830
+ const output = result.stdout.trim();
14831
+ if (!output)
14832
+ return [];
14876
14833
  try {
14877
- await recoverAgentState(ctx);
14878
- } catch (e) {
14879
- console.log(` ⚠️ Recovery failed: ${e.message}`);
14880
- console.log(` Continuing with fresh state`);
14834
+ const parsed = JSON.parse(output);
14835
+ if (!Array.isArray(parsed))
14836
+ return [];
14837
+ return parsed.filter((item) => typeof item === "object" && item !== null && typeof item.number === "number" && typeof item.title === "string" && typeof item.url === "string" && typeof item.headRefName === "string" && typeof item.state === "string").map((item) => ({
14838
+ number: item.number,
14839
+ title: item.title,
14840
+ url: item.url,
14841
+ headRefName: item.headRefName,
14842
+ state: item.state
14843
+ }));
14844
+ } catch {
14845
+ return [];
14881
14846
  }
14882
14847
  }
14883
- async function initDaemon() {
14884
- if (!acquireLock()) {
14885
- process.exit(1);
14848
+ var execAsync2;
14849
+ var init_git_reader = __esm(() => {
14850
+ execAsync2 = promisify2(exec2);
14851
+ });
14852
+
14853
+ // src/commands/machine/daemon-start/git-subscription.ts
14854
+ function startGitRequestSubscription(ctx, wsClient2) {
14855
+ const processedRequestIds = new Map;
14856
+ const DEDUP_TTL_MS = 5 * 60 * 1000;
14857
+ let processing = false;
14858
+ const unsubscribe = wsClient2.onUpdate(api.workspaces.getPendingRequests, {
14859
+ sessionId: ctx.sessionId,
14860
+ machineId: ctx.machineId
14861
+ }, (requests) => {
14862
+ if (!requests || requests.length === 0)
14863
+ return;
14864
+ if (processing)
14865
+ return;
14866
+ processing = true;
14867
+ processRequests(ctx, requests, processedRequestIds, DEDUP_TTL_MS).catch((err) => {
14868
+ console.warn(`[${formatTimestamp()}] ⚠️ Git request processing failed: ${err.message}`);
14869
+ }).finally(() => {
14870
+ processing = false;
14871
+ });
14872
+ }, (err) => {
14873
+ console.warn(`[${formatTimestamp()}] ⚠️ Git request subscription error: ${err.message}`);
14874
+ });
14875
+ console.log(`[${formatTimestamp()}] \uD83D\uDD00 Git request subscription started (reactive)`);
14876
+ return {
14877
+ stop: () => {
14878
+ unsubscribe();
14879
+ console.log(`[${formatTimestamp()}] \uD83D\uDD00 Git request subscription stopped`);
14880
+ }
14881
+ };
14882
+ }
14883
+ function extractDiffStatFromShowOutput(content) {
14884
+ for (const line of content.split(`
14885
+ `)) {
14886
+ if (/\d+\s+file.*changed/.test(line)) {
14887
+ return parseDiffStatLine(line);
14888
+ }
14886
14889
  }
14887
- const convexUrl = getConvexUrl();
14888
- const sessionId = validateAuthentication(convexUrl);
14889
- const client2 = await getConvexClient();
14890
- const typedSessionId = sessionId;
14891
- await validateSession(client2, typedSessionId, convexUrl);
14892
- const config3 = setupMachine();
14893
- const { machineId } = config3;
14894
- initHarnessRegistry();
14895
- const agentServices = new Map(getAllHarnesses().map((s) => [s.id, s]));
14896
- const availableModels = await registerCapabilities(client2, typedSessionId, config3, agentServices);
14897
- await connectDaemon(client2, typedSessionId, machineId, convexUrl);
14898
- const deps = createDefaultDeps19();
14899
- deps.backend.mutation = (endpoint, args) => client2.mutation(endpoint, args);
14900
- deps.backend.query = (endpoint, args) => client2.query(endpoint, args);
14901
- const events = new DaemonEventBus;
14902
- const ctx = {
14903
- client: client2,
14904
- sessionId: typedSessionId,
14905
- machineId,
14906
- config: config3,
14907
- deps,
14908
- events,
14909
- agentServices,
14910
- activeWorkingDirs: new Set,
14911
- lastPushedGitState: new Map,
14912
- agentEndedTurn: new Map
14913
- };
14914
- registerEventListeners(ctx);
14915
- logStartup(ctx, availableModels);
14916
- await recoverState(ctx);
14917
- return ctx;
14918
- }
14919
- var init_init2 = __esm(() => {
14920
- init_state_recovery();
14921
- init_api3();
14922
- init_storage();
14923
- init_client2();
14924
- init_machine();
14925
- init_intentional_stops();
14926
- init_remote_agents();
14927
- init_harness_spawning();
14928
- init_error_formatting();
14929
- init_version();
14930
- init_pid();
14931
- init_register_listeners();
14932
- });
14933
-
14934
- // src/infrastructure/machine/stop-reason.ts
14935
- function resolveStopReason(code2, signal, wasIntentional) {
14936
- if (wasIntentional)
14937
- return "user.stop";
14938
- if (signal !== null)
14939
- return "agent_process.signal";
14940
- if (code2 === 0)
14941
- return "agent_process.exited_clean";
14942
- return "agent_process.crashed";
14890
+ return { filesChanged: 0, insertions: 0, deletions: 0 };
14943
14891
  }
14944
-
14945
- // src/commands/machine/daemon-start/handlers/start-agent.ts
14946
- async function executeStartAgent(ctx, args) {
14947
- const { chatroomId, role, agentHarness, model, workingDir, reason } = args;
14948
- console.log(` ↪ start-agent command received`);
14949
- console.log(` Chatroom: ${chatroomId}`);
14950
- console.log(` Role: ${role}`);
14951
- console.log(` Harness: ${agentHarness}`);
14952
- if (reason) {
14953
- console.log(` Reason: ${reason}`);
14954
- }
14955
- if (model) {
14956
- console.log(` Model: ${model}`);
14957
- }
14958
- if (!workingDir) {
14959
- const msg2 = `No workingDir provided in command payload for ${chatroomId}/${role}`;
14960
- console.log(` ⚠️ ${msg2}`);
14961
- return { result: msg2, failed: true };
14962
- }
14963
- console.log(` Working dir: ${workingDir}`);
14964
- try {
14965
- const dirStat = await ctx.deps.fs.stat(workingDir);
14966
- if (!dirStat.isDirectory()) {
14967
- const msg2 = `Working directory is not a directory: ${workingDir}`;
14968
- console.log(` ⚠️ ${msg2}`);
14969
- return { result: msg2, failed: true };
14970
- }
14971
- } catch {
14972
- const msg2 = `Working directory does not exist: ${workingDir}`;
14973
- console.log(` ⚠️ ${msg2}`);
14974
- return { result: msg2, failed: true };
14975
- }
14976
- try {
14977
- const existingConfigs = await ctx.deps.backend.query(api.machines.getMachineAgentConfigs, {
14892
+ async function processFullDiff(ctx, req) {
14893
+ const result = await getFullDiff(req.workingDir);
14894
+ if (result.status === "available" || result.status === "truncated") {
14895
+ const diffStatResult = await getDiffStat(req.workingDir);
14896
+ const diffStat = diffStatResult.status === "available" ? diffStatResult.diffStat : { filesChanged: 0, insertions: 0, deletions: 0 };
14897
+ await ctx.deps.backend.mutation(api.workspaces.upsertFullDiff, {
14978
14898
  sessionId: ctx.sessionId,
14979
- chatroomId
14980
- });
14981
- const existingConfig = existingConfigs.configs.find((c) => c.machineId === ctx.machineId && c.role.toLowerCase() === role.toLowerCase());
14982
- const backendPid = existingConfig?.spawnedAgentPid;
14983
- const localEntry = ctx.deps.machine.listAgentEntries(ctx.machineId).find((e) => e.chatroomId === chatroomId && e.role.toLowerCase() === role.toLowerCase());
14984
- const localPid = localEntry?.entry.pid;
14985
- const pidsToKill = [
14986
- ...new Set([backendPid, localPid].filter((p) => p !== undefined))
14987
- ];
14988
- const anyService = ctx.agentServices.values().next().value;
14989
- for (const pid2 of pidsToKill) {
14990
- const isAlive = anyService ? anyService.isAlive(pid2) : false;
14991
- if (isAlive) {
14992
- console.log(` ⚠️ Existing agent detected (PID: ${pid2}) — stopping before respawn`);
14993
- await onAgentShutdown(ctx, { chatroomId, role, pid: pid2, stopReason: "daemon.respawn" });
14994
- console.log(` ✅ Existing agent stopped (PID: ${pid2})`);
14995
- }
14996
- }
14997
- } catch (e) {
14998
- console.log(` ⚠️ Could not check for existing agent (proceeding): ${e.message}`);
14999
- }
15000
- const convexUrl = getConvexUrl();
15001
- const initPromptResult = await ctx.deps.backend.query(api.messages.getInitPrompt, {
15002
- sessionId: ctx.sessionId,
15003
- chatroomId,
15004
- role,
15005
- convexUrl
15006
- });
15007
- if (!initPromptResult?.prompt) {
15008
- const msg2 = "Failed to fetch init prompt from backend";
15009
- console.log(` ⚠️ ${msg2}`);
15010
- return { result: msg2, failed: true };
15011
- }
15012
- console.log(` Fetched split init prompt from backend`);
15013
- const service = ctx.agentServices.get(agentHarness);
15014
- if (!service) {
15015
- const msg2 = `Unknown agent harness: ${agentHarness}`;
15016
- console.log(` ⚠️ ${msg2}`);
15017
- return { result: msg2, failed: true };
15018
- }
15019
- let spawnResult;
15020
- try {
15021
- spawnResult = await service.spawn({
15022
- workingDir,
15023
- prompt: initPromptResult.initialMessage,
15024
- systemPrompt: initPromptResult.rolePrompt,
15025
- model,
15026
- context: { machineId: ctx.machineId, chatroomId, role }
14899
+ machineId: ctx.machineId,
14900
+ workingDir: req.workingDir,
14901
+ diffContent: result.content,
14902
+ truncated: result.truncated,
14903
+ diffStat
15027
14904
  });
15028
- } catch (e) {
15029
- const msg2 = `Failed to spawn agent: ${e.message}`;
15030
- console.log(` ⚠️ ${msg2}`);
15031
- return { result: msg2, failed: true };
15032
- }
15033
- const { pid } = spawnResult;
15034
- const msg = `Agent spawned (PID: ${pid})`;
15035
- console.log(` ✅ ${msg}`);
15036
- const agentEndKey = `${chatroomId}:${role.toLowerCase()}`;
15037
- ctx.agentEndedTurn.delete(agentEndKey);
15038
- ctx.deps.spawning.recordSpawn(chatroomId);
15039
- try {
15040
- await ctx.deps.backend.mutation(api.machines.updateSpawnedAgent, {
14905
+ console.log(`[${formatTimestamp()}] \uD83D\uDCC4 Full diff pushed: ${req.workingDir} (${diffStat.filesChanged} files, ${result.truncated ? "truncated" : "complete"})`);
14906
+ } else {
14907
+ await ctx.deps.backend.mutation(api.workspaces.upsertFullDiff, {
15041
14908
  sessionId: ctx.sessionId,
15042
14909
  machineId: ctx.machineId,
15043
- chatroomId,
15044
- role,
15045
- pid,
15046
- model,
15047
- reason
15048
- });
15049
- console.log(` Updated backend with PID: ${pid}`);
15050
- ctx.deps.machine.persistAgentPid(ctx.machineId, chatroomId, role, pid, agentHarness);
15051
- } catch (e) {
15052
- console.log(` ⚠️ Failed to update PID in backend: ${e.message}`);
15053
- }
15054
- ctx.events.emit("agent:started", {
15055
- chatroomId,
15056
- role,
15057
- pid,
15058
- harness: agentHarness,
15059
- model
15060
- });
15061
- ctx.activeWorkingDirs.add(workingDir);
15062
- spawnResult.onExit(({ code: code2, signal }) => {
15063
- ctx.deps.spawning.recordExit(chatroomId);
15064
- const pendingReason = ctx.deps.stops.consume(chatroomId, role);
15065
- const stopReason = pendingReason ?? resolveStopReason(code2, signal, false);
15066
- ctx.events.emit("agent:exited", {
15067
- chatroomId,
15068
- role,
15069
- pid,
15070
- code: code2,
15071
- signal,
15072
- stopReason,
15073
- intentional: pendingReason !== null
15074
- });
15075
- });
15076
- if (spawnResult.onAgentEnd) {
15077
- spawnResult.onAgentEnd(() => {
15078
- ctx.agentEndedTurn.set(agentEndKey, true);
15079
- try {
15080
- ctx.deps.processes.kill(-pid, "SIGTERM");
15081
- } catch {}
14910
+ workingDir: req.workingDir,
14911
+ diffContent: "",
14912
+ truncated: false,
14913
+ diffStat: { filesChanged: 0, insertions: 0, deletions: 0 }
15082
14914
  });
14915
+ console.log(`[${formatTimestamp()}] \uD83D\uDCC4 Full diff pushed (empty): ${req.workingDir} (${result.status})`);
15083
14916
  }
15084
- let lastReportedTokenAt = 0;
15085
- spawnResult.onOutput(() => {
15086
- const now = Date.now();
15087
- if (now - lastReportedTokenAt >= 30000) {
15088
- lastReportedTokenAt = now;
15089
- ctx.deps.backend.mutation(api.participants.updateTokenActivity, {
15090
- sessionId: ctx.sessionId,
15091
- chatroomId,
15092
- role
15093
- }).catch(() => {});
15094
- }
15095
- });
15096
- return { result: msg, failed: false };
15097
14917
  }
15098
- var init_start_agent = __esm(() => {
15099
- init_api3();
15100
- init_client2();
15101
- });
15102
-
15103
- // src/events/daemon/agent/on-request-start-agent.ts
15104
- async function onRequestStartAgent(ctx, event) {
15105
- if (Date.now() > event.deadline) {
15106
- console.log(`[daemon] Skipping expired agent.requestStart for role=${event.role} (deadline passed)`);
14918
+ async function processCommitDetail(ctx, req) {
14919
+ if (!req.sha) {
14920
+ throw new Error("commit_detail request missing sha");
14921
+ }
14922
+ const [result, metadata] = await Promise.all([
14923
+ getCommitDetail(req.workingDir, req.sha),
14924
+ getCommitMetadata(req.workingDir, req.sha)
14925
+ ]);
14926
+ if (result.status === "not_found") {
14927
+ await ctx.deps.backend.mutation(api.workspaces.upsertCommitDetail, {
14928
+ sessionId: ctx.sessionId,
14929
+ machineId: ctx.machineId,
14930
+ workingDir: req.workingDir,
14931
+ sha: req.sha,
14932
+ status: "not_found",
14933
+ message: metadata?.message,
14934
+ author: metadata?.author,
14935
+ date: metadata?.date
14936
+ });
15107
14937
  return;
15108
14938
  }
15109
- const spawnCheck = ctx.deps.spawning.shouldAllowSpawn(event.chatroomId, event.reason);
15110
- if (!spawnCheck.allowed) {
15111
- const retryMsg = spawnCheck.retryAfterMs ? ` Retry after ${spawnCheck.retryAfterMs}ms.` : "";
15112
- console.warn(`[daemon] ⚠️ Spawn suppressed for chatroom=${event.chatroomId} role=${event.role} reason=${event.reason}.${retryMsg}`);
14939
+ if (result.status === "error") {
14940
+ await ctx.deps.backend.mutation(api.workspaces.upsertCommitDetail, {
14941
+ sessionId: ctx.sessionId,
14942
+ machineId: ctx.machineId,
14943
+ workingDir: req.workingDir,
14944
+ sha: req.sha,
14945
+ status: "error",
14946
+ errorMessage: result.message,
14947
+ message: metadata?.message,
14948
+ author: metadata?.author,
14949
+ date: metadata?.date
14950
+ });
15113
14951
  return;
15114
14952
  }
15115
- await executeStartAgent(ctx, {
15116
- chatroomId: event.chatroomId,
15117
- role: event.role,
15118
- agentHarness: event.agentHarness,
15119
- model: event.model,
15120
- workingDir: event.workingDir,
15121
- reason: event.reason
14953
+ const diffStat = extractDiffStatFromShowOutput(result.content);
14954
+ await ctx.deps.backend.mutation(api.workspaces.upsertCommitDetail, {
14955
+ sessionId: ctx.sessionId,
14956
+ machineId: ctx.machineId,
14957
+ workingDir: req.workingDir,
14958
+ sha: req.sha,
14959
+ status: "available",
14960
+ diffContent: result.content,
14961
+ truncated: result.truncated,
14962
+ message: metadata?.message,
14963
+ author: metadata?.author,
14964
+ date: metadata?.date,
14965
+ diffStat
15122
14966
  });
14967
+ console.log(`[${formatTimestamp()}] \uD83D\uDD0D Commit detail pushed: ${req.sha.slice(0, 7)} in ${req.workingDir}`);
15123
14968
  }
15124
- var init_on_request_start_agent = __esm(() => {
15125
- init_start_agent();
15126
- });
15127
-
15128
- // src/commands/machine/daemon-start/handlers/stop-agent.ts
15129
- async function executeStopAgent(ctx, args) {
15130
- const { chatroomId, role, reason } = args;
15131
- const stopReason = reason;
15132
- console.log(` ↪ stop-agent command received`);
15133
- console.log(` Chatroom: ${chatroomId}`);
15134
- console.log(` Role: ${role}`);
15135
- console.log(` Reason: ${reason}`);
15136
- const configsResult = await ctx.deps.backend.query(api.machines.getMachineAgentConfigs, {
14969
+ async function processMoreCommits(ctx, req) {
14970
+ const offset = req.offset ?? 0;
14971
+ const commits = await getRecentCommits(req.workingDir, COMMITS_PER_PAGE, offset);
14972
+ const hasMoreCommits = commits.length >= COMMITS_PER_PAGE;
14973
+ await ctx.deps.backend.mutation(api.workspaces.appendMoreCommits, {
15137
14974
  sessionId: ctx.sessionId,
15138
- chatroomId
14975
+ machineId: ctx.machineId,
14976
+ workingDir: req.workingDir,
14977
+ commits,
14978
+ hasMoreCommits
15139
14979
  });
15140
- const targetConfig = configsResult.configs.find((c) => c.machineId === ctx.machineId && c.role.toLowerCase() === role.toLowerCase());
15141
- const backendPid = targetConfig?.spawnedAgentPid;
15142
- const localEntry = ctx.deps.machine.listAgentEntries(ctx.machineId).find((e) => e.chatroomId === chatroomId && e.role.toLowerCase() === role.toLowerCase());
15143
- const localPid = localEntry?.entry.pid;
15144
- const allPids = [...new Set([backendPid, localPid].filter((p) => p !== undefined))];
15145
- if (allPids.length === 0) {
15146
- const msg = "No running agent found (no PID recorded)";
15147
- console.log(` ⚠️ ${msg}`);
15148
- return { result: msg, failed: true };
15149
- }
15150
- const anyService = ctx.agentServices.values().next().value;
15151
- let anyKilled = false;
15152
- let lastError = null;
15153
- for (const pid of allPids) {
15154
- console.log(` Stopping agent with PID: ${pid}`);
15155
- const isAlive = anyService ? anyService.isAlive(pid) : false;
15156
- if (!isAlive) {
15157
- console.log(` ⚠️ PID ${pid} not found — process already exited or was never started`);
15158
- await clearAgentPidEverywhere(ctx, chatroomId, role);
15159
- console.log(` Cleared stale PID`);
15160
- try {
15161
- await ctx.deps.backend.mutation(api.participants.leave, {
15162
- sessionId: ctx.sessionId,
15163
- chatroomId,
15164
- role
15165
- });
15166
- console.log(` Removed participant record`);
15167
- } catch {}
14980
+ console.log(`[${formatTimestamp()}] \uD83D\uDCDC More commits appended: ${req.workingDir} (+${commits.length} commits, offset=${offset})`);
14981
+ }
14982
+ async function processRequests(ctx, requests, processedRequestIds, dedupTtlMs) {
14983
+ const evictBefore = Date.now() - dedupTtlMs;
14984
+ for (const [id, ts] of processedRequestIds) {
14985
+ if (ts < evictBefore)
14986
+ processedRequestIds.delete(id);
14987
+ }
14988
+ for (const req of requests) {
14989
+ const requestId = req._id.toString();
14990
+ if (processedRequestIds.has(requestId))
15168
14991
  continue;
15169
- }
14992
+ processedRequestIds.set(requestId, Date.now());
15170
14993
  try {
15171
- const shutdownResult = await onAgentShutdown(ctx, {
15172
- chatroomId,
15173
- role,
15174
- pid,
15175
- stopReason
14994
+ await ctx.deps.backend.mutation(api.workspaces.updateRequestStatus, {
14995
+ sessionId: ctx.sessionId,
14996
+ requestId: req._id,
14997
+ status: "processing"
15176
14998
  });
15177
- const msg = shutdownResult.killed ? `Agent stopped (PID: ${pid})` : `Agent stop attempted (PID: ${pid}) — process may still be running`;
15178
- console.log(` ${shutdownResult.killed ? "" : "⚠️ "} ${msg}`);
15179
- if (shutdownResult.killed) {
15180
- anyKilled = true;
14999
+ switch (req.requestType) {
15000
+ case "full_diff":
15001
+ await processFullDiff(ctx, req);
15002
+ break;
15003
+ case "commit_detail":
15004
+ await processCommitDetail(ctx, req);
15005
+ break;
15006
+ case "more_commits":
15007
+ await processMoreCommits(ctx, req);
15008
+ break;
15181
15009
  }
15182
- } catch (e) {
15183
- lastError = e;
15184
- console.log(` ⚠️ Failed to stop agent (PID: ${pid}): ${e.message}`);
15010
+ await ctx.deps.backend.mutation(api.workspaces.updateRequestStatus, {
15011
+ sessionId: ctx.sessionId,
15012
+ requestId: req._id,
15013
+ status: "done"
15014
+ });
15015
+ } catch (err) {
15016
+ console.warn(`[${formatTimestamp()}] ⚠️ Failed to process ${req.requestType} request: ${err.message}`);
15017
+ await ctx.deps.backend.mutation(api.workspaces.updateRequestStatus, {
15018
+ sessionId: ctx.sessionId,
15019
+ requestId: req._id,
15020
+ status: "error"
15021
+ }).catch(() => {});
15185
15022
  }
15186
15023
  }
15187
- if (lastError && !anyKilled) {
15188
- const msg = `Failed to stop agent: ${lastError.message}`;
15189
- console.log(` ⚠️ ${msg}`);
15190
- return { result: msg, failed: true };
15191
- }
15192
- if (!anyKilled) {
15193
- return {
15194
- result: `All recorded PIDs appear stale (processes not found or belong to different programs)`,
15195
- failed: true
15196
- };
15197
- }
15198
- const killedCount = allPids.length > 1 ? ` (${allPids.length} PIDs)` : ``;
15199
- return { result: `Agent stopped${killedCount}`, failed: false };
15200
15024
  }
15201
- var init_stop_agent = __esm(() => {
15025
+ var init_git_subscription = __esm(() => {
15202
15026
  init_api3();
15203
- init_shared();
15027
+ init_git_reader();
15204
15028
  });
15205
15029
 
15206
- // src/events/daemon/agent/on-request-stop-agent.ts
15207
- async function onRequestStopAgent(ctx, event) {
15208
- if (Date.now() > event.deadline) {
15209
- console.log(`[daemon] ⏰ Skipping expired agent.requestStop for role=${event.role} (deadline passed)`);
15030
+ // src/commands/machine/daemon-start/git-heartbeat.ts
15031
+ import { createHash as createHash3 } from "node:crypto";
15032
+ async function pushGitState(ctx) {
15033
+ let workspaces;
15034
+ try {
15035
+ workspaces = await ctx.deps.backend.query(api.workspaces.listWorkspacesForMachine, {
15036
+ sessionId: ctx.sessionId,
15037
+ machineId: ctx.machineId
15038
+ });
15039
+ } catch (err) {
15040
+ console.warn(`[${formatTimestamp()}] ⚠️ Failed to query workspaces for git sync: ${err.message}`);
15210
15041
  return;
15211
15042
  }
15212
- await executeStopAgent(ctx, {
15213
- chatroomId: event.chatroomId,
15214
- role: event.role,
15215
- reason: event.reason
15216
- });
15217
- }
15218
- var init_on_request_stop_agent = __esm(() => {
15219
- init_stop_agent();
15220
- });
15221
-
15222
- // src/commands/machine/daemon-start/handlers/ping.ts
15223
- function handlePing() {
15224
- console.log(` ↪ Responding: pong`);
15225
- return { result: "pong", failed: false };
15226
- }
15227
-
15228
- // src/infrastructure/git/types.ts
15229
- function makeGitStateKey(machineId, workingDir) {
15230
- return `${machineId}::${workingDir}`;
15231
- }
15232
- var FULL_DIFF_MAX_BYTES = 500000, COMMITS_PER_PAGE = 20;
15233
-
15234
- // src/infrastructure/git/git-reader.ts
15235
- import { exec as exec2 } from "node:child_process";
15236
- import { promisify as promisify2 } from "node:util";
15237
- async function runGit(args, cwd) {
15238
- try {
15239
- const result = await execAsync2(`git ${args}`, {
15240
- cwd,
15241
- env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_PAGER: "cat", NO_COLOR: "1" },
15242
- maxBuffer: FULL_DIFF_MAX_BYTES + 64 * 1024
15243
- });
15244
- return result;
15245
- } catch (err) {
15246
- return { error: err };
15247
- }
15248
- }
15249
- function isGitNotInstalled(message) {
15250
- return message.includes("command not found") || message.includes("ENOENT") || message.includes("not found") || message.includes("'git' is not recognized");
15251
- }
15252
- function isNotAGitRepo(message) {
15253
- return message.includes("not a git repository") || message.includes("Not a git repository");
15254
- }
15255
- function isPermissionDenied(message) {
15256
- return message.includes("Permission denied") || message.includes("EACCES");
15257
- }
15258
- function isEmptyRepo(stderr) {
15259
- 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");
15260
- }
15261
- function classifyError(errMessage) {
15262
- if (isGitNotInstalled(errMessage)) {
15263
- return { status: "error", message: "git is not installed or not in PATH" };
15264
- }
15265
- if (isNotAGitRepo(errMessage)) {
15266
- return { status: "not_found" };
15267
- }
15268
- if (isPermissionDenied(errMessage)) {
15269
- return { status: "error", message: `Permission denied: ${errMessage}` };
15270
- }
15271
- return { status: "error", message: errMessage.trim() };
15272
- }
15273
- async function isGitRepo(workingDir) {
15274
- const result = await runGit("rev-parse --git-dir", workingDir);
15275
- if ("error" in result)
15276
- return false;
15277
- return result.stdout.trim().length > 0;
15278
- }
15279
- async function getBranch(workingDir) {
15280
- const result = await runGit("rev-parse --abbrev-ref HEAD", workingDir);
15281
- if ("error" in result) {
15282
- const errMsg = result.error.message;
15283
- if (errMsg.includes("unknown revision") || errMsg.includes("No such file or directory") || errMsg.includes("does not have any commits")) {
15284
- return { status: "available", branch: "HEAD" };
15285
- }
15286
- return classifyError(errMsg);
15287
- }
15288
- const branch = result.stdout.trim();
15289
- if (!branch) {
15290
- return { status: "error", message: "git rev-parse returned empty output" };
15291
- }
15292
- return { status: "available", branch };
15293
- }
15294
- async function isDirty(workingDir) {
15295
- const result = await runGit("status --porcelain", workingDir);
15296
- if ("error" in result)
15297
- return false;
15298
- return result.stdout.trim().length > 0;
15299
- }
15300
- function parseDiffStatLine(statLine) {
15301
- const filesMatch = statLine.match(/(\d+)\s+file/);
15302
- const insertMatch = statLine.match(/(\d+)\s+insertion/);
15303
- const deleteMatch = statLine.match(/(\d+)\s+deletion/);
15304
- return {
15305
- filesChanged: filesMatch ? parseInt(filesMatch[1], 10) : 0,
15306
- insertions: insertMatch ? parseInt(insertMatch[1], 10) : 0,
15307
- deletions: deleteMatch ? parseInt(deleteMatch[1], 10) : 0
15308
- };
15309
- }
15310
- async function getDiffStat(workingDir) {
15311
- const result = await runGit("diff HEAD --stat", workingDir);
15312
- if ("error" in result) {
15313
- const errMsg = result.error.message;
15314
- if (isEmptyRepo(result.error.message)) {
15315
- return { status: "no_commits" };
15316
- }
15317
- const classified = classifyError(errMsg);
15318
- if (classified.status === "not_found")
15319
- return { status: "not_found" };
15320
- return classified;
15321
- }
15322
- const output = result.stdout;
15323
- const stderr = result.stderr;
15324
- if (isEmptyRepo(stderr)) {
15325
- return { status: "no_commits" };
15326
- }
15327
- if (!output.trim()) {
15328
- return {
15329
- status: "available",
15330
- diffStat: { filesChanged: 0, insertions: 0, deletions: 0 }
15331
- };
15332
- }
15333
- const lines = output.trim().split(`
15334
- `);
15335
- const summaryLine = lines[lines.length - 1] ?? "";
15336
- const diffStat = parseDiffStatLine(summaryLine);
15337
- return { status: "available", diffStat };
15338
- }
15339
- async function getFullDiff(workingDir) {
15340
- const result = await runGit("diff HEAD", workingDir);
15341
- if ("error" in result) {
15342
- const errMsg = result.error.message;
15343
- if (isEmptyRepo(errMsg)) {
15344
- return { status: "no_commits" };
15345
- }
15346
- const classified = classifyError(errMsg);
15347
- if (classified.status === "not_found")
15348
- return { status: "not_found" };
15349
- return classified;
15350
- }
15351
- const stderr = result.stderr;
15352
- if (isEmptyRepo(stderr)) {
15353
- return { status: "no_commits" };
15354
- }
15355
- const raw = result.stdout;
15356
- const byteLength2 = Buffer.byteLength(raw, "utf8");
15357
- if (byteLength2 > FULL_DIFF_MAX_BYTES) {
15358
- const truncated = Buffer.from(raw, "utf8").subarray(0, FULL_DIFF_MAX_BYTES).toString("utf8");
15359
- return { status: "truncated", content: truncated, truncated: true };
15360
- }
15361
- return { status: "available", content: raw, truncated: false };
15362
- }
15363
- async function getRecentCommits(workingDir, count = 20, skip = 0) {
15364
- const format = "%H%x00%h%x00%s%x00%an%x00%aI";
15365
- const skipArg = skip > 0 ? ` --skip=${skip}` : "";
15366
- const result = await runGit(`log -${count}${skipArg} --format=${format}`, workingDir);
15367
- if ("error" in result) {
15368
- return [];
15369
- }
15370
- const output = result.stdout.trim();
15371
- if (!output)
15372
- return [];
15373
- const commits = [];
15374
- for (const line of output.split(`
15375
- `)) {
15376
- const trimmed = line.trim();
15377
- if (!trimmed)
15378
- continue;
15379
- const parts = trimmed.split("\x00");
15380
- if (parts.length !== 5)
15381
- continue;
15382
- const [sha, shortSha, message, author, date] = parts;
15383
- commits.push({ sha, shortSha, message, author, date });
15384
- }
15385
- return commits;
15386
- }
15387
- async function getCommitDetail(workingDir, sha) {
15388
- const result = await runGit(`show ${sha} --format="" --stat -p`, workingDir);
15389
- if ("error" in result) {
15390
- const errMsg = result.error.message;
15391
- const classified = classifyError(errMsg);
15392
- if (classified.status === "not_found")
15393
- return { status: "not_found" };
15394
- if (isEmptyRepo(errMsg) || errMsg.includes("unknown revision") || errMsg.includes("bad object") || errMsg.includes("does not exist")) {
15395
- return { status: "not_found" };
15396
- }
15397
- return classified;
15398
- }
15399
- const raw = result.stdout;
15400
- const byteLength2 = Buffer.byteLength(raw, "utf8");
15401
- if (byteLength2 > FULL_DIFF_MAX_BYTES) {
15402
- const truncated = Buffer.from(raw, "utf8").subarray(0, FULL_DIFF_MAX_BYTES).toString("utf8");
15403
- return { status: "truncated", content: truncated, truncated: true };
15404
- }
15405
- return { status: "available", content: raw, truncated: false };
15406
- }
15407
- async function getCommitMetadata(workingDir, sha) {
15408
- const format = "%s%x00%an%x00%aI";
15409
- const result = await runGit(`log -1 --format=${format} ${sha}`, workingDir);
15410
- if ("error" in result)
15411
- return null;
15412
- const output = result.stdout.trim();
15413
- if (!output)
15414
- return null;
15415
- const parts = output.split("\x00");
15416
- if (parts.length !== 3)
15417
- return null;
15418
- return { message: parts[0], author: parts[1], date: parts[2] };
15419
- }
15420
- var execAsync2;
15421
- var init_git_reader = __esm(() => {
15422
- execAsync2 = promisify2(exec2);
15423
- });
15424
-
15425
- // src/commands/machine/daemon-start/git-polling.ts
15426
- function startGitPollingLoop(ctx) {
15427
- const timer = setInterval(() => {
15428
- runPollingTick(ctx).catch((err) => {
15429
- console.warn(`[${formatTimestamp()}] ⚠️ Git polling tick failed: ${err.message}`);
15430
- });
15431
- }, GIT_POLLING_INTERVAL_MS);
15432
- timer.unref();
15433
- console.log(`[${formatTimestamp()}] \uD83D\uDD00 Git polling loop started (interval: ${GIT_POLLING_INTERVAL_MS}ms)`);
15434
- return {
15435
- stop: () => {
15436
- clearInterval(timer);
15437
- console.log(`[${formatTimestamp()}] \uD83D\uDD00 Git polling loop stopped`);
15438
- }
15439
- };
15440
- }
15441
- function extractDiffStatFromShowOutput(content) {
15442
- for (const line of content.split(`
15443
- `)) {
15444
- if (/\d+\s+file.*changed/.test(line)) {
15445
- return parseDiffStatLine(line);
15043
+ const uniqueWorkingDirs = new Set(workspaces.map((ws) => ws.workingDir));
15044
+ if (uniqueWorkingDirs.size === 0)
15045
+ return;
15046
+ for (const workingDir of uniqueWorkingDirs) {
15047
+ try {
15048
+ await pushSingleWorkspaceGitState(ctx, workingDir);
15049
+ } catch (err) {
15050
+ console.warn(`[${formatTimestamp()}] ⚠️ Git state push failed for ${workingDir}: ${err.message}`);
15446
15051
  }
15447
15052
  }
15448
- return { filesChanged: 0, insertions: 0, deletions: 0 };
15449
15053
  }
15450
- async function processFullDiff(ctx, req) {
15451
- const result = await getFullDiff(req.workingDir);
15452
- if (result.status === "available" || result.status === "truncated") {
15453
- const diffStatResult = await getDiffStat(req.workingDir);
15454
- const diffStat = diffStatResult.status === "available" ? diffStatResult.diffStat : { filesChanged: 0, insertions: 0, deletions: 0 };
15455
- await ctx.deps.backend.mutation(api.workspaces.upsertFullDiff, {
15054
+ async function pushSingleWorkspaceGitState(ctx, workingDir) {
15055
+ const stateKey = makeGitStateKey(ctx.machineId, workingDir);
15056
+ const isRepo = await isGitRepo(workingDir);
15057
+ if (!isRepo) {
15058
+ const stateHash2 = "not_found";
15059
+ if (ctx.lastPushedGitState.get(stateKey) === stateHash2)
15060
+ return;
15061
+ await ctx.deps.backend.mutation(api.workspaces.upsertWorkspaceGitState, {
15456
15062
  sessionId: ctx.sessionId,
15457
15063
  machineId: ctx.machineId,
15458
- workingDir: req.workingDir,
15459
- diffContent: result.content,
15460
- truncated: result.truncated,
15461
- diffStat
15064
+ workingDir,
15065
+ status: "not_found"
15462
15066
  });
15463
- console.log(`[${formatTimestamp()}] \uD83D\uDCC4 Full diff pushed: ${req.workingDir} (${diffStat.filesChanged} files, ${result.truncated ? "truncated" : "complete"})`);
15464
- } else {
15465
- await ctx.deps.backend.mutation(api.workspaces.upsertFullDiff, {
15067
+ ctx.lastPushedGitState.set(stateKey, stateHash2);
15068
+ return;
15069
+ }
15070
+ const [branchResult, dirtyResult, diffStatResult, commits] = await Promise.all([
15071
+ getBranch(workingDir),
15072
+ isDirty(workingDir),
15073
+ getDiffStat(workingDir),
15074
+ getRecentCommits(workingDir, COMMITS_PER_PAGE)
15075
+ ]);
15076
+ if (branchResult.status === "error") {
15077
+ const stateHash2 = `error:${branchResult.message}`;
15078
+ if (ctx.lastPushedGitState.get(stateKey) === stateHash2)
15079
+ return;
15080
+ await ctx.deps.backend.mutation(api.workspaces.upsertWorkspaceGitState, {
15466
15081
  sessionId: ctx.sessionId,
15467
15082
  machineId: ctx.machineId,
15468
- workingDir: req.workingDir,
15469
- diffContent: "",
15470
- truncated: false,
15471
- diffStat: { filesChanged: 0, insertions: 0, deletions: 0 }
15083
+ workingDir,
15084
+ status: "error",
15085
+ errorMessage: branchResult.message
15472
15086
  });
15473
- console.log(`[${formatTimestamp()}] \uD83D\uDCC4 Full diff pushed (empty): ${req.workingDir} (${result.status})`);
15087
+ ctx.lastPushedGitState.set(stateKey, stateHash2);
15088
+ return;
15089
+ }
15090
+ if (branchResult.status === "not_found") {
15091
+ return;
15092
+ }
15093
+ const openPRs = await getOpenPRsForBranch(workingDir, branchResult.branch);
15094
+ const branch = branchResult.branch;
15095
+ const isDirty2 = dirtyResult;
15096
+ const diffStat = diffStatResult.status === "available" ? diffStatResult.diffStat : { filesChanged: 0, insertions: 0, deletions: 0 };
15097
+ const hasMoreCommits = commits.length >= COMMITS_PER_PAGE;
15098
+ const stateHash = createHash3("md5").update(JSON.stringify({ branch, isDirty: isDirty2, diffStat, shas: commits.map((c) => c.sha), prs: openPRs.map((pr) => pr.number) })).digest("hex");
15099
+ if (ctx.lastPushedGitState.get(stateKey) === stateHash) {
15100
+ return;
15474
15101
  }
15102
+ await ctx.deps.backend.mutation(api.workspaces.upsertWorkspaceGitState, {
15103
+ sessionId: ctx.sessionId,
15104
+ machineId: ctx.machineId,
15105
+ workingDir,
15106
+ status: "available",
15107
+ branch,
15108
+ isDirty: isDirty2,
15109
+ diffStat,
15110
+ recentCommits: commits,
15111
+ hasMoreCommits,
15112
+ openPullRequests: openPRs
15113
+ });
15114
+ ctx.lastPushedGitState.set(stateKey, stateHash);
15115
+ console.log(`[${formatTimestamp()}] \uD83D\uDD00 Git state pushed: ${workingDir} (${branch}${isDirty2 ? ", dirty" : ", clean"})`);
15116
+ prefetchMissingCommitDetails(ctx, workingDir, commits).catch((err) => {
15117
+ console.warn(`[${formatTimestamp()}] ⚠️ Commit pre-fetch failed for ${workingDir}: ${err.message}`);
15118
+ });
15475
15119
  }
15476
- async function processCommitDetail(ctx, req) {
15477
- if (!req.sha) {
15478
- throw new Error("commit_detail request missing sha");
15120
+ async function prefetchMissingCommitDetails(ctx, workingDir, commits) {
15121
+ if (commits.length === 0)
15122
+ return;
15123
+ const shas = commits.map((c) => c.sha);
15124
+ const missingShas = await ctx.deps.backend.query(api.workspaces.getMissingCommitShas, {
15125
+ sessionId: ctx.sessionId,
15126
+ machineId: ctx.machineId,
15127
+ workingDir,
15128
+ shas
15129
+ });
15130
+ if (missingShas.length === 0)
15131
+ return;
15132
+ console.log(`[${formatTimestamp()}] \uD83D\uDD0D Pre-fetching ${missingShas.length} commit(s) for ${workingDir}`);
15133
+ for (const sha of missingShas) {
15134
+ try {
15135
+ await prefetchSingleCommit(ctx, workingDir, sha, commits);
15136
+ } catch (err) {
15137
+ console.warn(`[${formatTimestamp()}] ⚠️ Pre-fetch failed for ${sha.slice(0, 7)}: ${err.message}`);
15138
+ }
15479
15139
  }
15480
- const [result, metadata] = await Promise.all([
15481
- getCommitDetail(req.workingDir, req.sha),
15482
- getCommitMetadata(req.workingDir, req.sha)
15483
- ]);
15140
+ }
15141
+ async function prefetchSingleCommit(ctx, workingDir, sha, commits) {
15142
+ const metadata = commits.find((c) => c.sha === sha);
15143
+ const result = await getCommitDetail(workingDir, sha);
15484
15144
  if (result.status === "not_found") {
15485
15145
  await ctx.deps.backend.mutation(api.workspaces.upsertCommitDetail, {
15486
15146
  sessionId: ctx.sessionId,
15487
15147
  machineId: ctx.machineId,
15488
- workingDir: req.workingDir,
15489
- sha: req.sha,
15148
+ workingDir,
15149
+ sha,
15490
15150
  status: "not_found",
15491
15151
  message: metadata?.message,
15492
15152
  author: metadata?.author,
@@ -15498,8 +15158,8 @@ async function processCommitDetail(ctx, req) {
15498
15158
  await ctx.deps.backend.mutation(api.workspaces.upsertCommitDetail, {
15499
15159
  sessionId: ctx.sessionId,
15500
15160
  machineId: ctx.machineId,
15501
- workingDir: req.workingDir,
15502
- sha: req.sha,
15161
+ workingDir,
15162
+ sha,
15503
15163
  status: "error",
15504
15164
  errorMessage: result.message,
15505
15165
  message: metadata?.message,
@@ -15512,8 +15172,8 @@ async function processCommitDetail(ctx, req) {
15512
15172
  await ctx.deps.backend.mutation(api.workspaces.upsertCommitDetail, {
15513
15173
  sessionId: ctx.sessionId,
15514
15174
  machineId: ctx.machineId,
15515
- workingDir: req.workingDir,
15516
- sha: req.sha,
15175
+ workingDir,
15176
+ sha,
15517
15177
  status: "available",
15518
15178
  diffContent: result.content,
15519
15179
  truncated: result.truncated,
@@ -15522,371 +15182,876 @@ async function processCommitDetail(ctx, req) {
15522
15182
  date: metadata?.date,
15523
15183
  diffStat
15524
15184
  });
15525
- console.log(`[${formatTimestamp()}] \uD83D\uDD0D Commit detail pushed: ${req.sha.slice(0, 7)} in ${req.workingDir}`);
15185
+ console.log(`[${formatTimestamp()}] Pre-fetched: ${sha.slice(0, 7)} in ${workingDir}`);
15526
15186
  }
15527
- async function processMoreCommits(ctx, req) {
15528
- const offset = req.offset ?? 0;
15529
- const commits = await getRecentCommits(req.workingDir, COMMITS_PER_PAGE, offset);
15530
- const hasMoreCommits = commits.length >= COMMITS_PER_PAGE;
15531
- await ctx.deps.backend.mutation(api.workspaces.appendMoreCommits, {
15532
- sessionId: ctx.sessionId,
15533
- machineId: ctx.machineId,
15534
- workingDir: req.workingDir,
15535
- commits,
15536
- hasMoreCommits
15537
- });
15538
- console.log(`[${formatTimestamp()}] \uD83D\uDCDC More commits appended: ${req.workingDir} (+${commits.length} commits, offset=${offset})`);
15187
+ var init_git_heartbeat = __esm(() => {
15188
+ init_git_subscription();
15189
+ init_api3();
15190
+ init_git_reader();
15191
+ });
15192
+
15193
+ // src/commands/machine/daemon-start/handlers/ping.ts
15194
+ function handlePing() {
15195
+ console.log(` ↪ Responding: pong`);
15196
+ return { result: "pong", failed: false };
15539
15197
  }
15540
- async function runPollingTick(ctx) {
15541
- const requests = await ctx.deps.backend.query(api.workspaces.getPendingRequests, {
15542
- sessionId: ctx.sessionId,
15543
- machineId: ctx.machineId
15544
- });
15545
- if (requests.length === 0)
15198
+
15199
+ // src/commands/machine/daemon-start/handlers/state-recovery.ts
15200
+ async function recoverAgentState(ctx) {
15201
+ await ctx.deps.agentProcessManager.recover();
15202
+ const activeSlots = ctx.deps.agentProcessManager.listActive();
15203
+ if (activeSlots.length === 0) {
15204
+ console.log(` No active agents after recovery`);
15546
15205
  return;
15547
- for (const req of requests) {
15206
+ }
15207
+ const chatroomIds = new Set(activeSlots.map((s) => s.chatroomId));
15208
+ let registeredCount = 0;
15209
+ for (const chatroomId of chatroomIds) {
15548
15210
  try {
15549
- await ctx.deps.backend.mutation(api.workspaces.updateRequestStatus, {
15211
+ const configsResult = await ctx.deps.backend.query(api.machines.getMachineAgentConfigs, {
15550
15212
  sessionId: ctx.sessionId,
15551
- requestId: req._id,
15552
- status: "processing"
15213
+ chatroomId
15553
15214
  });
15554
- switch (req.requestType) {
15555
- case "full_diff":
15556
- await processFullDiff(ctx, req);
15557
- break;
15558
- case "commit_detail":
15559
- await processCommitDetail(ctx, req);
15560
- break;
15561
- case "more_commits":
15562
- await processMoreCommits(ctx, req);
15563
- break;
15215
+ for (const config3 of configsResult.configs) {
15216
+ if (config3.machineId === ctx.machineId && config3.workingDir) {
15217
+ registeredCount++;
15218
+ ctx.deps.backend.mutation(api.workspaces.registerWorkspace, {
15219
+ sessionId: ctx.sessionId,
15220
+ chatroomId,
15221
+ machineId: ctx.machineId,
15222
+ workingDir: config3.workingDir,
15223
+ hostname: ctx.config?.hostname ?? "unknown",
15224
+ registeredBy: config3.role
15225
+ }).catch((err) => {
15226
+ console.warn(`[daemon] ⚠️ Failed to register workspace on recovery: ${err.message}`);
15227
+ });
15228
+ }
15564
15229
  }
15565
- await ctx.deps.backend.mutation(api.workspaces.updateRequestStatus, {
15566
- sessionId: ctx.sessionId,
15567
- requestId: req._id,
15568
- status: "done"
15230
+ } catch {}
15231
+ }
15232
+ if (registeredCount > 0) {
15233
+ console.log(` \uD83D\uDD00 Registered ${registeredCount} workspace(s) on recovery`);
15234
+ }
15235
+ }
15236
+ var init_state_recovery = __esm(() => {
15237
+ init_api3();
15238
+ });
15239
+
15240
+ // src/events/daemon/event-bus.ts
15241
+ class DaemonEventBus {
15242
+ listeners = new Map;
15243
+ on(event, listener) {
15244
+ if (!this.listeners.has(event)) {
15245
+ this.listeners.set(event, new Set);
15246
+ }
15247
+ this.listeners.get(event).add(listener);
15248
+ return () => {
15249
+ this.listeners.get(event)?.delete(listener);
15250
+ };
15251
+ }
15252
+ emit(event, payload) {
15253
+ const set = this.listeners.get(event);
15254
+ if (!set)
15255
+ return;
15256
+ for (const listener of set) {
15257
+ try {
15258
+ listener(payload);
15259
+ } catch (err) {
15260
+ console.warn(`[EventBus] Listener error on "${event}": ${err.message}`);
15261
+ }
15262
+ }
15263
+ }
15264
+ removeAllListeners() {
15265
+ this.listeners.clear();
15266
+ }
15267
+ }
15268
+
15269
+ // src/events/daemon/agent/on-agent-exited.ts
15270
+ function onAgentExited(ctx, payload) {
15271
+ ctx.deps.agentProcessManager.handleExit({
15272
+ chatroomId: payload.chatroomId,
15273
+ role: payload.role,
15274
+ pid: payload.pid,
15275
+ code: payload.code,
15276
+ signal: payload.signal
15277
+ });
15278
+ }
15279
+
15280
+ // src/events/daemon/agent/on-agent-started.ts
15281
+ function onAgentStarted(ctx, payload) {
15282
+ const ts = formatTimestamp();
15283
+ console.log(`[${ts}] \uD83D\uDFE2 Agent started: ${payload.role} (PID: ${payload.pid}, harness: ${payload.harness})`);
15284
+ }
15285
+ var init_on_agent_started = () => {};
15286
+
15287
+ // src/events/daemon/agent/on-agent-stopped.ts
15288
+ function onAgentStopped(ctx, payload) {
15289
+ const ts = formatTimestamp();
15290
+ console.log(`[${ts}] \uD83D\uDD34 Agent stopped: ${payload.role} (PID: ${payload.pid})`);
15291
+ }
15292
+ var init_on_agent_stopped = () => {};
15293
+
15294
+ // src/events/daemon/register-listeners.ts
15295
+ function registerEventListeners(ctx) {
15296
+ const unsubs = [];
15297
+ unsubs.push(ctx.events.on("agent:exited", (payload) => onAgentExited(ctx, payload)));
15298
+ unsubs.push(ctx.events.on("agent:started", (payload) => onAgentStarted(ctx, payload)));
15299
+ unsubs.push(ctx.events.on("agent:stopped", (payload) => onAgentStopped(ctx, payload)));
15300
+ return () => {
15301
+ for (const unsub of unsubs) {
15302
+ unsub();
15303
+ }
15304
+ };
15305
+ }
15306
+ var init_register_listeners = __esm(() => {
15307
+ init_on_agent_started();
15308
+ init_on_agent_stopped();
15309
+ });
15310
+
15311
+ // src/infrastructure/services/harness-spawning/rate-limiter.ts
15312
+ class SpawnRateLimiter {
15313
+ config;
15314
+ buckets = new Map;
15315
+ constructor(config3 = {}) {
15316
+ this.config = { ...DEFAULT_CONFIG, ...config3 };
15317
+ }
15318
+ tryConsume(chatroomId, reason) {
15319
+ if (reason.startsWith("user.")) {
15320
+ return { allowed: true };
15321
+ }
15322
+ const bucket = this._getOrCreateBucket(chatroomId);
15323
+ this._refill(bucket);
15324
+ if (bucket.tokens < 1) {
15325
+ const elapsed = Date.now() - bucket.lastRefillAt;
15326
+ const retryAfterMs = this.config.refillRateMs - elapsed;
15327
+ console.warn(`⚠️ [RateLimiter] Agent spawn rate-limited for chatroom ${chatroomId} (reason: ${reason}). Retry after ${retryAfterMs}ms`);
15328
+ return { allowed: false, retryAfterMs };
15329
+ }
15330
+ bucket.tokens -= 1;
15331
+ const remaining = Math.floor(bucket.tokens);
15332
+ if (remaining <= LOW_TOKEN_THRESHOLD) {
15333
+ console.warn(`⚠️ [RateLimiter] Agent spawn tokens running low for chatroom ${chatroomId} (${remaining}/${this.config.maxTokens} remaining)`);
15334
+ }
15335
+ return { allowed: true };
15336
+ }
15337
+ getStatus(chatroomId) {
15338
+ const bucket = this._getOrCreateBucket(chatroomId);
15339
+ this._refill(bucket);
15340
+ return {
15341
+ remaining: Math.floor(bucket.tokens),
15342
+ total: this.config.maxTokens
15343
+ };
15344
+ }
15345
+ _getOrCreateBucket(chatroomId) {
15346
+ if (!this.buckets.has(chatroomId)) {
15347
+ this.buckets.set(chatroomId, {
15348
+ tokens: this.config.initialTokens,
15349
+ lastRefillAt: Date.now()
15569
15350
  });
15570
- } catch (err) {
15571
- console.warn(`[${formatTimestamp()}] ⚠️ Failed to process ${req.requestType} request: ${err.message}`);
15572
- await ctx.deps.backend.mutation(api.workspaces.updateRequestStatus, {
15573
- sessionId: ctx.sessionId,
15574
- requestId: req._id,
15575
- status: "error"
15576
- }).catch(() => {});
15577
15351
  }
15352
+ return this.buckets.get(chatroomId);
15353
+ }
15354
+ _refill(bucket) {
15355
+ const now = Date.now();
15356
+ const elapsed = now - bucket.lastRefillAt;
15357
+ if (elapsed >= this.config.refillRateMs) {
15358
+ const tokensToAdd = Math.floor(elapsed / this.config.refillRateMs);
15359
+ bucket.tokens = Math.min(this.config.maxTokens, bucket.tokens + tokensToAdd);
15360
+ bucket.lastRefillAt += tokensToAdd * this.config.refillRateMs;
15361
+ }
15362
+ }
15363
+ }
15364
+ var DEFAULT_CONFIG, LOW_TOKEN_THRESHOLD = 1;
15365
+ var init_rate_limiter = __esm(() => {
15366
+ DEFAULT_CONFIG = {
15367
+ maxTokens: 5,
15368
+ refillRateMs: 60000,
15369
+ initialTokens: 5
15370
+ };
15371
+ });
15372
+
15373
+ // src/infrastructure/services/harness-spawning/harness-spawning-service.ts
15374
+ class HarnessSpawningService {
15375
+ rateLimiter;
15376
+ concurrentAgents = new Map;
15377
+ constructor({ rateLimiter }) {
15378
+ this.rateLimiter = rateLimiter;
15379
+ }
15380
+ shouldAllowSpawn(chatroomId, reason) {
15381
+ const current = this.concurrentAgents.get(chatroomId) ?? 0;
15382
+ if (current >= MAX_CONCURRENT_AGENTS_PER_CHATROOM) {
15383
+ console.warn(`⚠️ [HarnessSpawningService] Concurrent agent limit reached for chatroom ${chatroomId} ` + `(${current}/${MAX_CONCURRENT_AGENTS_PER_CHATROOM} active agents). Spawn rejected.`);
15384
+ return { allowed: false };
15385
+ }
15386
+ const result = this.rateLimiter.tryConsume(chatroomId, reason);
15387
+ if (!result.allowed) {
15388
+ console.warn(`⚠️ [HarnessSpawningService] Spawn blocked by rate limiter for chatroom ${chatroomId} ` + `(reason: ${reason}).`);
15389
+ }
15390
+ return result;
15391
+ }
15392
+ recordSpawn(chatroomId) {
15393
+ const current = this.concurrentAgents.get(chatroomId) ?? 0;
15394
+ this.concurrentAgents.set(chatroomId, current + 1);
15395
+ }
15396
+ recordExit(chatroomId) {
15397
+ const current = this.concurrentAgents.get(chatroomId) ?? 0;
15398
+ const next = Math.max(0, current - 1);
15399
+ this.concurrentAgents.set(chatroomId, next);
15400
+ }
15401
+ getConcurrentCount(chatroomId) {
15402
+ return this.concurrentAgents.get(chatroomId) ?? 0;
15578
15403
  }
15579
15404
  }
15580
- var GIT_POLLING_INTERVAL_MS = 5000;
15581
- var init_git_polling = __esm(() => {
15582
- init_api3();
15583
- init_git_reader();
15405
+ var MAX_CONCURRENT_AGENTS_PER_CHATROOM = 10;
15406
+
15407
+ // src/infrastructure/services/harness-spawning/index.ts
15408
+ var init_harness_spawning = __esm(() => {
15409
+ init_rate_limiter();
15584
15410
  });
15585
15411
 
15586
- // src/commands/machine/daemon-start/git-heartbeat.ts
15587
- import { createHash as createHash2 } from "node:crypto";
15588
- async function pushGitState(ctx) {
15589
- if (ctx.activeWorkingDirs.size === 0)
15590
- return;
15591
- for (const workingDir of ctx.activeWorkingDirs) {
15592
- try {
15593
- await pushSingleWorkspaceGitState(ctx, workingDir);
15594
- } catch (err) {
15595
- console.warn(`[${formatTimestamp()}] ⚠️ Git state push failed for ${workingDir}: ${err.message}`);
15596
- }
15597
- }
15412
+ // src/infrastructure/machine/crash-loop-tracker.ts
15413
+ class CrashLoopTracker {
15414
+ history = new Map;
15415
+ record(chatroomId, role, now = Date.now()) {
15416
+ const key = `${chatroomId}:${role.toLowerCase()}`;
15417
+ const windowStart = now - CRASH_LOOP_WINDOW_MS;
15418
+ const raw = this.history.get(key) ?? [];
15419
+ const recent = raw.filter((ts) => ts >= windowStart);
15420
+ recent.push(now);
15421
+ this.history.set(key, recent);
15422
+ const restartCount = recent.length;
15423
+ const allowed = restartCount <= CRASH_LOOP_MAX_RESTARTS;
15424
+ return { allowed, restartCount, windowMs: CRASH_LOOP_WINDOW_MS };
15425
+ }
15426
+ clear(chatroomId, role) {
15427
+ const key = `${chatroomId}:${role.toLowerCase()}`;
15428
+ this.history.delete(key);
15429
+ }
15430
+ getCount(chatroomId, role, now = Date.now()) {
15431
+ const key = `${chatroomId}:${role.toLowerCase()}`;
15432
+ const windowStart = now - CRASH_LOOP_WINDOW_MS;
15433
+ const raw = this.history.get(key) ?? [];
15434
+ return raw.filter((ts) => ts >= windowStart).length;
15435
+ }
15436
+ }
15437
+ var CRASH_LOOP_MAX_RESTARTS = 3, CRASH_LOOP_WINDOW_MS;
15438
+ var init_crash_loop_tracker = __esm(() => {
15439
+ CRASH_LOOP_WINDOW_MS = 5 * 60 * 1000;
15440
+ });
15441
+
15442
+ // src/infrastructure/machine/stop-reason.ts
15443
+ function resolveStopReason(code2, signal, wasIntentional) {
15444
+ if (wasIntentional)
15445
+ return "user.stop";
15446
+ if (signal !== null)
15447
+ return "agent_process.signal";
15448
+ if (code2 === 0)
15449
+ return "agent_process.exited_clean";
15450
+ return "agent_process.crashed";
15598
15451
  }
15599
- async function pushSingleWorkspaceGitState(ctx, workingDir) {
15600
- const stateKey = makeGitStateKey(ctx.machineId, workingDir);
15601
- const isRepo = await isGitRepo(workingDir);
15602
- if (!isRepo) {
15603
- const stateHash2 = "not_found";
15604
- if (ctx.lastPushedGitState.get(stateKey) === stateHash2)
15452
+
15453
+ // src/infrastructure/services/agent-process-manager/agent-process-manager.ts
15454
+ function agentKey2(chatroomId, role) {
15455
+ return `${chatroomId}:${role.toLowerCase()}`;
15456
+ }
15457
+
15458
+ class AgentProcessManager {
15459
+ deps;
15460
+ slots = new Map;
15461
+ pendingStopReasons = new Map;
15462
+ constructor(deps) {
15463
+ this.deps = deps;
15464
+ }
15465
+ async ensureRunning(opts) {
15466
+ const key = agentKey2(opts.chatroomId, opts.role);
15467
+ const slot = this.getOrCreateSlot(key);
15468
+ if (slot.state === "running") {
15469
+ return { success: true, pid: slot.pid };
15470
+ }
15471
+ if (slot.state === "spawning" && slot.pendingOperation) {
15472
+ return slot.pendingOperation;
15473
+ }
15474
+ if (slot.state === "stopping" && slot.pendingOperation) {
15475
+ await slot.pendingOperation;
15476
+ }
15477
+ const operation = this.doEnsureRunning(key, slot, opts);
15478
+ slot.pendingOperation = operation;
15479
+ return operation;
15480
+ }
15481
+ async stop(opts) {
15482
+ const key = agentKey2(opts.chatroomId, opts.role);
15483
+ const slot = this.slots.get(key);
15484
+ if (!slot || slot.state === "idle") {
15485
+ return { success: true };
15486
+ }
15487
+ if (slot.state === "stopping" && slot.pendingOperation) {
15488
+ await slot.pendingOperation;
15489
+ return { success: true };
15490
+ }
15491
+ const pid = slot.pid;
15492
+ if (!pid) {
15493
+ slot.state = "idle";
15494
+ slot.pendingOperation = undefined;
15495
+ return { success: true };
15496
+ }
15497
+ this.pendingStopReasons.set(key, opts.reason);
15498
+ slot.state = "stopping";
15499
+ const operation = this.doStop(key, slot, pid, opts);
15500
+ slot.pendingOperation = operation;
15501
+ await operation;
15502
+ return { success: true };
15503
+ }
15504
+ handleExit(opts) {
15505
+ const key = agentKey2(opts.chatroomId, opts.role);
15506
+ const slot = this.slots.get(key);
15507
+ if (!slot || slot.pid !== opts.pid) {
15605
15508
  return;
15606
- await ctx.deps.backend.mutation(api.workspaces.upsertWorkspaceGitState, {
15607
- sessionId: ctx.sessionId,
15608
- machineId: ctx.machineId,
15609
- workingDir,
15610
- status: "not_found"
15509
+ }
15510
+ if (slot.state === "stopping") {
15511
+ return;
15512
+ }
15513
+ const intentionalReason = this.pendingStopReasons.get(key);
15514
+ this.pendingStopReasons.delete(key);
15515
+ const stopReason = intentionalReason ?? resolveStopReason(opts.code, opts.signal, false);
15516
+ this.deps.spawning.recordExit(opts.chatroomId);
15517
+ const harness = slot.harness;
15518
+ const model = slot.model;
15519
+ const workingDir = slot.workingDir;
15520
+ slot.state = "idle";
15521
+ slot.pid = undefined;
15522
+ slot.startedAt = undefined;
15523
+ slot.pendingOperation = undefined;
15524
+ this.deps.backend.mutation(api.machines.recordAgentExited, {
15525
+ sessionId: this.deps.sessionId,
15526
+ machineId: this.deps.machineId,
15527
+ chatroomId: opts.chatroomId,
15528
+ role: opts.role,
15529
+ pid: opts.pid,
15530
+ stopReason,
15531
+ stopSignal: stopReason === "agent_process.signal" ? opts.signal ?? undefined : undefined,
15532
+ exitCode: opts.code ?? undefined,
15533
+ signal: opts.signal ?? undefined,
15534
+ agentHarness: harness
15535
+ }).catch((err) => {
15536
+ console.log(` ⚠️ Failed to record agent exit event: ${err.message}`);
15611
15537
  });
15612
- ctx.lastPushedGitState.set(stateKey, stateHash2);
15613
- return;
15614
- }
15615
- const [branchResult, dirtyResult, diffStatResult, commits] = await Promise.all([
15616
- getBranch(workingDir),
15617
- isDirty(workingDir),
15618
- getDiffStat(workingDir),
15619
- getRecentCommits(workingDir, COMMITS_PER_PAGE)
15620
- ]);
15621
- if (branchResult.status === "error") {
15622
- const stateHash2 = `error:${branchResult.message}`;
15623
- if (ctx.lastPushedGitState.get(stateKey) === stateHash2)
15538
+ this.deps.persistence.clearAgentPid(this.deps.machineId, opts.chatroomId, opts.role);
15539
+ for (const service of this.deps.agentServices.values()) {
15540
+ service.untrack(opts.pid);
15541
+ }
15542
+ const isIntentionalStop = stopReason === "user.stop" || stopReason === "platform.team_switch";
15543
+ const isDaemonRespawn = stopReason === "daemon.respawn";
15544
+ if (isIntentionalStop) {
15545
+ this.deps.crashLoop.clear(opts.chatroomId, opts.role);
15624
15546
  return;
15625
- await ctx.deps.backend.mutation(api.workspaces.upsertWorkspaceGitState, {
15626
- sessionId: ctx.sessionId,
15627
- machineId: ctx.machineId,
15547
+ }
15548
+ if (isDaemonRespawn) {
15549
+ return;
15550
+ }
15551
+ if (!harness || !workingDir) {
15552
+ console.log(`[AgentProcessManager] ⚠️ Cannot restart — missing harness or workingDir ` + `(role: ${opts.role}, harness: ${harness ?? "none"}, workingDir: ${workingDir ?? "none"})`);
15553
+ return;
15554
+ }
15555
+ this.ensureRunning({
15556
+ chatroomId: opts.chatroomId,
15557
+ role: opts.role,
15558
+ agentHarness: harness,
15559
+ model,
15628
15560
  workingDir,
15629
- status: "error",
15630
- errorMessage: branchResult.message
15561
+ reason: "platform.crash_recovery"
15562
+ }).catch((err) => {
15563
+ console.log(` ⚠️ Failed to restart agent: ${err.message}`);
15564
+ this.deps.backend.mutation(api.machines.emitAgentStartFailed, {
15565
+ sessionId: this.deps.sessionId,
15566
+ machineId: this.deps.machineId,
15567
+ chatroomId: opts.chatroomId,
15568
+ role: opts.role,
15569
+ error: err.message
15570
+ }).catch((emitErr) => {
15571
+ console.log(` ⚠️ Failed to emit startFailed event: ${emitErr.message}`);
15572
+ });
15631
15573
  });
15632
- ctx.lastPushedGitState.set(stateKey, stateHash2);
15633
- return;
15634
15574
  }
15635
- if (branchResult.status === "not_found") {
15636
- return;
15575
+ getSlot(chatroomId, role) {
15576
+ return this.slots.get(agentKey2(chatroomId, role));
15637
15577
  }
15638
- const branch = branchResult.branch;
15639
- const isDirty2 = dirtyResult;
15640
- const diffStat = diffStatResult.status === "available" ? diffStatResult.diffStat : { filesChanged: 0, insertions: 0, deletions: 0 };
15641
- const hasMoreCommits = commits.length >= COMMITS_PER_PAGE;
15642
- const stateHash = createHash2("md5").update(JSON.stringify({ branch, isDirty: isDirty2, diffStat, shas: commits.map((c) => c.sha) })).digest("hex");
15643
- if (ctx.lastPushedGitState.get(stateKey) === stateHash) {
15644
- return;
15578
+ listActive() {
15579
+ const result = [];
15580
+ for (const [key, slot] of this.slots) {
15581
+ if (slot.state === "running" || slot.state === "spawning") {
15582
+ const [chatroomId, role] = key.split(":");
15583
+ result.push({ chatroomId, role, slot });
15584
+ }
15585
+ }
15586
+ return result;
15645
15587
  }
15646
- await ctx.deps.backend.mutation(api.workspaces.upsertWorkspaceGitState, {
15647
- sessionId: ctx.sessionId,
15648
- machineId: ctx.machineId,
15649
- workingDir,
15650
- status: "available",
15651
- branch,
15652
- isDirty: isDirty2,
15653
- diffStat,
15654
- recentCommits: commits,
15655
- hasMoreCommits
15656
- });
15657
- ctx.lastPushedGitState.set(stateKey, stateHash);
15658
- console.log(`[${formatTimestamp()}] \uD83D\uDD00 Git state pushed: ${workingDir} (${branch}${isDirty2 ? ", dirty" : ", clean"})`);
15659
- prefetchMissingCommitDetails(ctx, workingDir, commits).catch((err) => {
15660
- console.warn(`[${formatTimestamp()}] ⚠️ Commit pre-fetch failed for ${workingDir}: ${err.message}`);
15661
- });
15662
- }
15663
- async function prefetchMissingCommitDetails(ctx, workingDir, commits) {
15664
- if (commits.length === 0)
15665
- return;
15666
- const shas = commits.map((c) => c.sha);
15667
- const missingShas = await ctx.deps.backend.query(api.workspaces.getMissingCommitShas, {
15668
- sessionId: ctx.sessionId,
15669
- machineId: ctx.machineId,
15670
- workingDir,
15671
- shas
15672
- });
15673
- if (missingShas.length === 0)
15674
- return;
15675
- console.log(`[${formatTimestamp()}] \uD83D\uDD0D Pre-fetching ${missingShas.length} commit(s) for ${workingDir}`);
15676
- for (const sha of missingShas) {
15677
- try {
15678
- await prefetchSingleCommit(ctx, workingDir, sha, commits);
15679
- } catch (err) {
15680
- console.warn(`[${formatTimestamp()}] ⚠️ Pre-fetch failed for ${sha.slice(0, 7)}: ${err.message}`);
15588
+ async recover() {
15589
+ const entries = this.deps.persistence.listAgentEntries(this.deps.machineId);
15590
+ let recovered = 0;
15591
+ let cleaned = 0;
15592
+ for (const { chatroomId, role, entry } of entries) {
15593
+ const key = agentKey2(chatroomId, role);
15594
+ let alive = false;
15595
+ try {
15596
+ this.deps.processes.kill(entry.pid, 0);
15597
+ alive = true;
15598
+ } catch {
15599
+ alive = false;
15600
+ }
15601
+ if (alive) {
15602
+ this.slots.set(key, {
15603
+ state: "running",
15604
+ pid: entry.pid,
15605
+ harness: entry.harness
15606
+ });
15607
+ recovered++;
15608
+ } else {
15609
+ this.deps.persistence.clearAgentPid(this.deps.machineId, chatroomId, role);
15610
+ cleaned++;
15611
+ }
15681
15612
  }
15613
+ console.log(`[AgentProcessManager] Recovery: ${recovered} alive, ${cleaned} cleaned up`);
15682
15614
  }
15683
- }
15684
- async function prefetchSingleCommit(ctx, workingDir, sha, commits) {
15685
- const metadata = commits.find((c) => c.sha === sha);
15686
- const result = await getCommitDetail(workingDir, sha);
15687
- if (result.status === "not_found") {
15688
- await ctx.deps.backend.mutation(api.workspaces.upsertCommitDetail, {
15689
- sessionId: ctx.sessionId,
15690
- machineId: ctx.machineId,
15691
- workingDir,
15692
- sha,
15693
- status: "not_found",
15694
- message: metadata?.message,
15695
- author: metadata?.author,
15696
- date: metadata?.date
15697
- });
15698
- return;
15615
+ getOrCreateSlot(key) {
15616
+ let slot = this.slots.get(key);
15617
+ if (!slot) {
15618
+ slot = { state: "idle" };
15619
+ this.slots.set(key, slot);
15620
+ }
15621
+ return slot;
15699
15622
  }
15700
- if (result.status === "error") {
15701
- await ctx.deps.backend.mutation(api.workspaces.upsertCommitDetail, {
15702
- sessionId: ctx.sessionId,
15703
- machineId: ctx.machineId,
15704
- workingDir,
15705
- sha,
15706
- status: "error",
15707
- errorMessage: result.message,
15708
- message: metadata?.message,
15709
- author: metadata?.author,
15710
- date: metadata?.date
15711
- });
15712
- return;
15623
+ async doEnsureRunning(key, slot, opts) {
15624
+ slot.state = "spawning";
15625
+ try {
15626
+ const spawnCheck = this.deps.spawning.shouldAllowSpawn(opts.chatroomId, opts.reason);
15627
+ if (!spawnCheck.allowed) {
15628
+ slot.state = "idle";
15629
+ slot.pendingOperation = undefined;
15630
+ return { success: false, error: "rate_limited" };
15631
+ }
15632
+ if (opts.reason === "platform.crash_recovery") {
15633
+ const loopCheck = this.deps.crashLoop.record(opts.chatroomId, opts.role);
15634
+ if (!loopCheck.allowed) {
15635
+ this.deps.backend.mutation(api.machines.emitRestartLimitReached, {
15636
+ sessionId: this.deps.sessionId,
15637
+ machineId: this.deps.machineId,
15638
+ chatroomId: opts.chatroomId,
15639
+ role: opts.role,
15640
+ restartCount: loopCheck.restartCount,
15641
+ windowMs: loopCheck.windowMs
15642
+ }).catch((err) => {
15643
+ console.log(` ⚠️ Failed to emit restartLimitReached event: ${err.message}`);
15644
+ });
15645
+ slot.state = "idle";
15646
+ slot.pendingOperation = undefined;
15647
+ return { success: false, error: "crash_loop" };
15648
+ }
15649
+ }
15650
+ try {
15651
+ const dirStat = await this.deps.fs.stat(opts.workingDir);
15652
+ if (!dirStat.isDirectory()) {
15653
+ slot.state = "idle";
15654
+ slot.pendingOperation = undefined;
15655
+ return {
15656
+ success: false,
15657
+ error: `Working directory is not a directory: ${opts.workingDir}`
15658
+ };
15659
+ }
15660
+ } catch {
15661
+ slot.state = "idle";
15662
+ slot.pendingOperation = undefined;
15663
+ return { success: false, error: `Working directory does not exist: ${opts.workingDir}` };
15664
+ }
15665
+ if (slot.pid) {
15666
+ try {
15667
+ this.deps.processes.kill(-slot.pid, "SIGTERM");
15668
+ } catch {}
15669
+ slot.pid = undefined;
15670
+ }
15671
+ let initPromptResult;
15672
+ try {
15673
+ initPromptResult = await this.deps.backend.query(api.messages.getInitPrompt, {
15674
+ sessionId: this.deps.sessionId,
15675
+ chatroomId: opts.chatroomId,
15676
+ role: opts.role,
15677
+ convexUrl: this.deps.convexUrl
15678
+ });
15679
+ } catch (e) {
15680
+ slot.state = "idle";
15681
+ slot.pendingOperation = undefined;
15682
+ return { success: false, error: `Failed to fetch init prompt: ${e.message}` };
15683
+ }
15684
+ if (!initPromptResult?.prompt) {
15685
+ slot.state = "idle";
15686
+ slot.pendingOperation = undefined;
15687
+ return { success: false, error: "Failed to fetch init prompt from backend" };
15688
+ }
15689
+ const service = this.deps.agentServices.get(opts.agentHarness);
15690
+ if (!service) {
15691
+ slot.state = "idle";
15692
+ slot.pendingOperation = undefined;
15693
+ return { success: false, error: `Unknown agent harness: ${opts.agentHarness}` };
15694
+ }
15695
+ let spawnResult;
15696
+ try {
15697
+ spawnResult = await service.spawn({
15698
+ workingDir: opts.workingDir,
15699
+ prompt: initPromptResult.initialMessage,
15700
+ systemPrompt: initPromptResult.rolePrompt,
15701
+ model: opts.model,
15702
+ context: {
15703
+ machineId: this.deps.machineId,
15704
+ chatroomId: opts.chatroomId,
15705
+ role: opts.role
15706
+ }
15707
+ });
15708
+ } catch (e) {
15709
+ slot.state = "idle";
15710
+ slot.pendingOperation = undefined;
15711
+ return { success: false, error: `Failed to spawn agent: ${e.message}` };
15712
+ }
15713
+ const { pid } = spawnResult;
15714
+ this.deps.spawning.recordSpawn(opts.chatroomId);
15715
+ slot.state = "running";
15716
+ slot.pid = pid;
15717
+ slot.harness = opts.agentHarness;
15718
+ slot.model = opts.model;
15719
+ slot.workingDir = opts.workingDir;
15720
+ slot.startedAt = this.deps.clock.now();
15721
+ slot.pendingOperation = undefined;
15722
+ this.deps.backend.mutation(api.machines.updateSpawnedAgent, {
15723
+ sessionId: this.deps.sessionId,
15724
+ machineId: this.deps.machineId,
15725
+ chatroomId: opts.chatroomId,
15726
+ role: opts.role,
15727
+ pid,
15728
+ model: opts.model,
15729
+ reason: opts.reason
15730
+ }).catch((err) => {
15731
+ console.log(` ⚠️ Failed to update PID in backend: ${err.message}`);
15732
+ });
15733
+ try {
15734
+ this.deps.persistence.persistAgentPid(this.deps.machineId, opts.chatroomId, opts.role, pid, opts.agentHarness);
15735
+ } catch {}
15736
+ spawnResult.onExit(({ code: code2, signal }) => {
15737
+ this.handleExit({
15738
+ chatroomId: opts.chatroomId,
15739
+ role: opts.role,
15740
+ pid,
15741
+ code: code2,
15742
+ signal
15743
+ });
15744
+ });
15745
+ if (spawnResult.onAgentEnd) {
15746
+ spawnResult.onAgentEnd(() => {
15747
+ try {
15748
+ this.deps.processes.kill(-pid, "SIGTERM");
15749
+ } catch {}
15750
+ });
15751
+ }
15752
+ let lastReportedTokenAt = 0;
15753
+ spawnResult.onOutput(() => {
15754
+ const now = this.deps.clock.now();
15755
+ if (now - lastReportedTokenAt >= 30000) {
15756
+ lastReportedTokenAt = now;
15757
+ this.deps.backend.mutation(api.participants.updateTokenActivity, {
15758
+ sessionId: this.deps.sessionId,
15759
+ chatroomId: opts.chatroomId,
15760
+ role: opts.role
15761
+ }).catch(() => {});
15762
+ }
15763
+ });
15764
+ return { success: true, pid };
15765
+ } catch (e) {
15766
+ slot.state = "idle";
15767
+ slot.pendingOperation = undefined;
15768
+ return { success: false, error: `Unexpected error: ${e.message}` };
15769
+ }
15770
+ }
15771
+ async doStop(key, slot, pid, opts) {
15772
+ try {
15773
+ try {
15774
+ this.deps.processes.kill(-pid, "SIGTERM");
15775
+ } catch {}
15776
+ let dead = false;
15777
+ for (let i2 = 0;i2 < 20; i2++) {
15778
+ await this.deps.clock.delay(500);
15779
+ try {
15780
+ this.deps.processes.kill(pid, 0);
15781
+ } catch {
15782
+ dead = true;
15783
+ break;
15784
+ }
15785
+ }
15786
+ if (!dead) {
15787
+ try {
15788
+ this.deps.processes.kill(-pid, "SIGKILL");
15789
+ } catch {}
15790
+ for (let i2 = 0;i2 < 10; i2++) {
15791
+ await this.deps.clock.delay(500);
15792
+ try {
15793
+ this.deps.processes.kill(pid, 0);
15794
+ } catch {
15795
+ dead = true;
15796
+ break;
15797
+ }
15798
+ }
15799
+ }
15800
+ } catch {}
15801
+ slot.state = "idle";
15802
+ slot.pid = undefined;
15803
+ slot.startedAt = undefined;
15804
+ slot.pendingOperation = undefined;
15805
+ this.deps.backend.mutation(api.machines.recordAgentExited, {
15806
+ sessionId: this.deps.sessionId,
15807
+ machineId: this.deps.machineId,
15808
+ chatroomId: opts.chatroomId,
15809
+ role: opts.role,
15810
+ pid,
15811
+ stopReason: opts.reason,
15812
+ exitCode: undefined,
15813
+ signal: undefined,
15814
+ agentHarness: slot.harness
15815
+ }).catch((err) => {
15816
+ console.log(` ⚠️ Failed to record agent exit event: ${err.message}`);
15817
+ });
15818
+ this.deps.persistence.clearAgentPid(this.deps.machineId, opts.chatroomId, opts.role);
15819
+ for (const service of this.deps.agentServices.values()) {
15820
+ service.untrack(pid);
15821
+ }
15822
+ return { success: true };
15713
15823
  }
15714
- const diffStat = extractDiffStatFromShowOutput(result.content);
15715
- await ctx.deps.backend.mutation(api.workspaces.upsertCommitDetail, {
15716
- sessionId: ctx.sessionId,
15717
- machineId: ctx.machineId,
15718
- workingDir,
15719
- sha,
15720
- status: "available",
15721
- diffContent: result.content,
15722
- truncated: result.truncated,
15723
- message: metadata?.message,
15724
- author: metadata?.author,
15725
- date: metadata?.date,
15726
- diffStat
15727
- });
15728
- console.log(`[${formatTimestamp()}] ✅ Pre-fetched: ${sha.slice(0, 7)} in ${workingDir}`);
15729
15824
  }
15730
- var init_git_heartbeat = __esm(() => {
15825
+ var init_agent_process_manager = __esm(() => {
15731
15826
  init_api3();
15732
- init_git_reader();
15733
- init_git_polling();
15734
15827
  });
15735
15828
 
15736
- // src/infrastructure/services/remote-agents/harness-restart-policy.ts
15737
- class OpenCodeRestartPolicy {
15738
- id = "opencode";
15739
- shouldStartAgent(params) {
15740
- const { task, agentConfig } = params;
15741
- if (agentConfig.desiredState !== "running") {
15742
- return false;
15743
- }
15744
- if (agentConfig.circuitState === "open") {
15745
- return false;
15746
- }
15747
- if (task.status === "in_progress") {
15748
- return agentConfig.spawnedAgentPid == null;
15749
- }
15750
- if (task.status === "pending" || task.status === "acknowledged") {
15751
- return agentConfig.spawnedAgentPid == null;
15829
+ // src/commands/machine/daemon-start/init.ts
15830
+ import { stat } from "node:fs/promises";
15831
+ async function discoverModels(agentServices) {
15832
+ const results = {};
15833
+ for (const [harness, service] of agentServices) {
15834
+ if (service.isInstalled()) {
15835
+ try {
15836
+ results[harness] = await service.listModels();
15837
+ } catch {
15838
+ results[harness] = [];
15839
+ }
15752
15840
  }
15753
- return false;
15754
15841
  }
15842
+ return results;
15755
15843
  }
15756
-
15757
- class PiRestartPolicy {
15758
- id = "pi";
15759
- shouldStartAgent(params, context) {
15760
- const { task, agentConfig } = params;
15761
- if (agentConfig.desiredState !== "running") {
15762
- return false;
15763
- }
15764
- if (agentConfig.circuitState === "open") {
15765
- return false;
15766
- }
15767
- if (task.status === "in_progress") {
15768
- return agentConfig.spawnedAgentPid == null;
15769
- }
15770
- if (task.status === "pending" || task.status === "acknowledged") {
15771
- if (agentConfig.spawnedAgentPid == null) {
15772
- return true;
15844
+ function createDefaultDeps19() {
15845
+ return {
15846
+ backend: {
15847
+ mutation: async () => {
15848
+ throw new Error("Backend not initialized");
15849
+ },
15850
+ query: async () => {
15851
+ throw new Error("Backend not initialized");
15852
+ }
15853
+ },
15854
+ processes: {
15855
+ kill: (pid, signal) => process.kill(pid, signal)
15856
+ },
15857
+ fs: {
15858
+ stat
15859
+ },
15860
+ machine: {
15861
+ clearAgentPid,
15862
+ persistAgentPid,
15863
+ listAgentEntries,
15864
+ persistEventCursor,
15865
+ loadEventCursor
15866
+ },
15867
+ clock: {
15868
+ now: () => Date.now(),
15869
+ delay: (ms) => new Promise((resolve2) => setTimeout(resolve2, ms))
15870
+ },
15871
+ spawning: new HarnessSpawningService({ rateLimiter: new SpawnRateLimiter }),
15872
+ agentProcessManager: null
15873
+ };
15874
+ }
15875
+ function validateAuthentication(convexUrl) {
15876
+ const sessionId = getSessionId();
15877
+ if (!sessionId) {
15878
+ const otherUrls = getOtherSessionUrls();
15879
+ console.error(`❌ Not authenticated for: ${convexUrl}`);
15880
+ if (otherUrls.length > 0) {
15881
+ console.error(`
15882
+ \uD83D\uDCA1 You have sessions for other environments:`);
15883
+ for (const url of otherUrls) {
15884
+ console.error(` • ${url}`);
15773
15885
  }
15774
15886
  }
15775
- if (task.status !== "pending" && task.status !== "acknowledged") {
15776
- return false;
15777
- }
15778
- if (!context?.agentEndedTurn) {
15779
- return false;
15780
- }
15781
- const key = `${task.chatroomId}:${agentConfig.role}`;
15782
- const hasEndedTurn = context.agentEndedTurn.get(key);
15783
- return hasEndedTurn === true;
15887
+ console.error(`
15888
+ Run: chatroom auth login`);
15889
+ releaseLock();
15890
+ process.exit(1);
15784
15891
  }
15892
+ return sessionId;
15785
15893
  }
15786
- function getRestartPolicyForHarness(harness) {
15787
- switch (harness) {
15788
- case "opencode":
15789
- return new OpenCodeRestartPolicy;
15790
- case "pi":
15791
- return new PiRestartPolicy;
15792
- default:
15793
- return {
15794
- id: harness,
15795
- shouldStartAgent: () => false
15796
- };
15894
+ async function validateSession(client2, sessionId, convexUrl) {
15895
+ const validation = await client2.query(api.cliAuth.validateSession, { sessionId });
15896
+ if (!validation.valid) {
15897
+ console.error(`❌ Session invalid: ${validation.reason}`);
15898
+ console.error(`
15899
+ Run: chatroom auth login`);
15900
+ releaseLock();
15901
+ process.exit(1);
15797
15902
  }
15798
15903
  }
15799
-
15800
- // src/commands/machine/daemon-start/task-monitor.ts
15801
- function canAttemptRestart(chatroomId, role, now) {
15802
- const key = `${chatroomId}:${role.toLowerCase()}`;
15803
- const lastAttempt = lastRestartAttempt.get(key) ?? 0;
15804
- return now - lastAttempt >= RESTART_COOLDOWN_MS;
15805
- }
15806
- function recordRestartAttempt(chatroomId, role, now) {
15807
- const key = `${chatroomId}:${role.toLowerCase()}`;
15808
- lastRestartAttempt.set(key, now);
15809
- }
15810
- function startTaskMonitor(ctx) {
15811
- let unsubscribe = null;
15812
- let isRunning = true;
15813
- const startMonitoring = async () => {
15814
- try {
15815
- const wsClient2 = await getConvexWsClient();
15816
- const agentEndContext = {
15817
- agentEndedTurn: ctx.agentEndedTurn
15818
- };
15819
- unsubscribe = wsClient2.onUpdate(api.machines.getAssignedTasks, {
15820
- sessionId: ctx.sessionId,
15821
- machineId: ctx.machineId
15822
- }, async (result) => {
15823
- if (!result?.tasks || result.tasks.length === 0)
15824
- return;
15825
- const now = Date.now();
15826
- for (const taskInfo of result.tasks) {
15827
- const { task, agentConfig } = {
15828
- task: {
15829
- taskId: taskInfo.taskId,
15830
- chatroomId: taskInfo.chatroomId,
15831
- status: taskInfo.status,
15832
- assignedTo: taskInfo.assignedTo,
15833
- updatedAt: taskInfo.updatedAt,
15834
- createdAt: taskInfo.createdAt
15835
- },
15836
- agentConfig: taskInfo.agentConfig
15837
- };
15838
- const policy = getRestartPolicyForHarness(agentConfig.agentHarness);
15839
- const shouldStart = policy.shouldStartAgent({ task, agentConfig }, agentEndContext);
15840
- if (!shouldStart)
15841
- continue;
15842
- if (!canAttemptRestart(task.chatroomId, agentConfig.role, now)) {
15843
- continue;
15844
- }
15845
- if (!agentConfig.workingDir) {
15846
- console.warn(`[${formatTimestamp()}] ⚠️ Missing workingDir for ${task.chatroomId}/${agentConfig.role}` + ` — skipping`);
15847
- continue;
15848
- }
15849
- console.log(`[${formatTimestamp()}] \uD83D\uDCE1 Task monitor: starting agent for ` + `${task.chatroomId}/${agentConfig.role} (harness: ${agentConfig.agentHarness})`);
15850
- recordRestartAttempt(task.chatroomId, agentConfig.role, now);
15851
- try {
15852
- await executeStartAgent(ctx, {
15853
- chatroomId: task.chatroomId,
15854
- role: agentConfig.role,
15855
- agentHarness: agentConfig.agentHarness,
15856
- model: agentConfig.model,
15857
- workingDir: agentConfig.workingDir,
15858
- reason: "daemon.task_monitor"
15859
- });
15860
- } catch (err) {
15861
- console.error(`[${formatTimestamp()}] ❌ Task monitor failed to start agent ` + `for ${task.chatroomId}/${agentConfig.role}: ${err.message}`);
15862
- }
15863
- }
15864
- });
15865
- console.log(`[${formatTimestamp()}] \uD83D\uDD0D Task monitor started`);
15866
- } catch (err) {
15867
- if (isRunning) {
15868
- console.error(`[${formatTimestamp()}] ❌ Task monitor error: ${err.message}`);
15869
- setTimeout(startMonitoring, 5000);
15870
- }
15871
- }
15872
- };
15873
- startMonitoring();
15874
- return {
15875
- stop: () => {
15876
- isRunning = false;
15877
- if (unsubscribe) {
15878
- unsubscribe();
15879
- unsubscribe = null;
15880
- }
15904
+ function setupMachine() {
15905
+ ensureMachineRegistered();
15906
+ const config3 = loadMachineConfig();
15907
+ return config3;
15908
+ }
15909
+ async function registerCapabilities(client2, sessionId, config3, agentServices) {
15910
+ const { machineId } = config3;
15911
+ const availableModels = await discoverModels(agentServices);
15912
+ try {
15913
+ await client2.mutation(api.machines.register, {
15914
+ sessionId,
15915
+ machineId,
15916
+ hostname: config3.hostname,
15917
+ os: config3.os,
15918
+ availableHarnesses: config3.availableHarnesses,
15919
+ harnessVersions: config3.harnessVersions,
15920
+ availableModels
15921
+ });
15922
+ } catch (error) {
15923
+ console.warn(`⚠️ Machine registration update failed: ${error.message}`);
15924
+ }
15925
+ return availableModels;
15926
+ }
15927
+ async function connectDaemon(client2, sessionId, machineId, convexUrl) {
15928
+ try {
15929
+ await client2.mutation(api.machines.updateDaemonStatus, {
15930
+ sessionId,
15931
+ machineId,
15932
+ connected: true
15933
+ });
15934
+ } catch (error) {
15935
+ if (isNetworkError(error)) {
15936
+ formatConnectivityError(error, convexUrl);
15937
+ } else {
15938
+ console.error(`❌ Failed to update daemon status: ${error.message}`);
15881
15939
  }
15940
+ releaseLock();
15941
+ process.exit(1);
15942
+ }
15943
+ }
15944
+ function logStartup(ctx, availableModels) {
15945
+ console.log(`[${formatTimestamp()}] \uD83D\uDE80 Daemon started`);
15946
+ console.log(` CLI version: ${getVersion()}`);
15947
+ console.log(` Machine ID: ${ctx.machineId}`);
15948
+ console.log(` Hostname: ${ctx.config?.hostname ?? "unknown"}`);
15949
+ console.log(` Available harnesses: ${ctx.config?.availableHarnesses.join(", ") || "none"}`);
15950
+ console.log(` Available models: ${Object.keys(availableModels).length > 0 ? `${Object.values(availableModels).flat().length} models across ${Object.keys(availableModels).join(", ")}` : "none discovered"}`);
15951
+ console.log(` PID: ${process.pid}`);
15952
+ }
15953
+ async function recoverState(ctx) {
15954
+ console.log(`
15955
+ [${formatTimestamp()}] \uD83D\uDD04 Recovering agent state...`);
15956
+ try {
15957
+ await recoverAgentState(ctx);
15958
+ } catch (e) {
15959
+ console.log(` ⚠️ Recovery failed: ${e.message}`);
15960
+ console.log(` Continuing with fresh state`);
15961
+ }
15962
+ }
15963
+ async function initDaemon() {
15964
+ if (!acquireLock()) {
15965
+ process.exit(1);
15966
+ }
15967
+ const convexUrl = getConvexUrl();
15968
+ const sessionId = validateAuthentication(convexUrl);
15969
+ const client2 = await getConvexClient();
15970
+ const typedSessionId = sessionId;
15971
+ await validateSession(client2, typedSessionId, convexUrl);
15972
+ const config3 = setupMachine();
15973
+ const { machineId } = config3;
15974
+ initHarnessRegistry();
15975
+ const agentServices = new Map(getAllHarnesses().map((s) => [s.id, s]));
15976
+ const availableModels = await registerCapabilities(client2, typedSessionId, config3, agentServices);
15977
+ await connectDaemon(client2, typedSessionId, machineId, convexUrl);
15978
+ const deps = createDefaultDeps19();
15979
+ deps.backend.mutation = (endpoint, args) => client2.mutation(endpoint, args);
15980
+ deps.backend.query = (endpoint, args) => client2.query(endpoint, args);
15981
+ deps.agentProcessManager = new AgentProcessManager({
15982
+ agentServices,
15983
+ backend: deps.backend,
15984
+ sessionId: typedSessionId,
15985
+ machineId,
15986
+ processes: deps.processes,
15987
+ clock: deps.clock,
15988
+ fs: deps.fs,
15989
+ persistence: deps.machine,
15990
+ spawning: deps.spawning,
15991
+ crashLoop: new CrashLoopTracker,
15992
+ convexUrl
15993
+ });
15994
+ const events = new DaemonEventBus;
15995
+ const ctx = {
15996
+ client: client2,
15997
+ sessionId: typedSessionId,
15998
+ machineId,
15999
+ config: config3,
16000
+ deps,
16001
+ events,
16002
+ agentServices,
16003
+ lastPushedGitState: new Map
15882
16004
  };
16005
+ registerEventListeners(ctx);
16006
+ logStartup(ctx, availableModels);
16007
+ await recoverState(ctx);
16008
+ return ctx;
15883
16009
  }
15884
- var RESTART_COOLDOWN_MS = 60000, lastRestartAttempt;
15885
- var init_task_monitor = __esm(() => {
16010
+ var init_init2 = __esm(() => {
16011
+ init_state_recovery();
15886
16012
  init_api3();
16013
+ init_register_listeners();
16014
+ init_storage();
15887
16015
  init_client2();
15888
- init_start_agent();
15889
- lastRestartAttempt = new Map;
16016
+ init_machine();
16017
+ init_harness_spawning();
16018
+ init_crash_loop_tracker();
16019
+ init_agent_process_manager();
16020
+ init_remote_agents();
16021
+ init_error_formatting();
16022
+ init_version();
16023
+ init_pid();
16024
+ });
16025
+
16026
+ // src/events/lifecycle/on-daemon-shutdown.ts
16027
+ async function onDaemonShutdown(ctx) {
16028
+ const activeAgents = ctx.deps.agentProcessManager.listActive();
16029
+ if (activeAgents.length > 0) {
16030
+ console.log(`[${formatTimestamp()}] Stopping ${activeAgents.length} agent(s)...`);
16031
+ await Promise.allSettled(activeAgents.map(async ({ chatroomId, role, slot }) => {
16032
+ try {
16033
+ await ctx.deps.agentProcessManager.stop({
16034
+ chatroomId,
16035
+ role,
16036
+ reason: "user.stop"
16037
+ });
16038
+ console.log(` Stopped ${role} (PID ${slot.pid})`);
16039
+ } catch (e) {
16040
+ console.log(` ⚠️ Failed to stop ${role}: ${e.message}`);
16041
+ }
16042
+ }));
16043
+ console.log(`[${formatTimestamp()}] All agents stopped`);
16044
+ }
16045
+ try {
16046
+ await ctx.deps.backend.mutation(api.machines.updateDaemonStatus, {
16047
+ sessionId: ctx.sessionId,
16048
+ machineId: ctx.machineId,
16049
+ connected: false
16050
+ });
16051
+ } catch {}
16052
+ }
16053
+ var init_on_daemon_shutdown = __esm(() => {
16054
+ init_api3();
15890
16055
  });
15891
16056
 
15892
16057
  // src/commands/machine/daemon-start/command-loop.ts
@@ -15975,15 +16140,14 @@ async function startCommandLoop(ctx) {
15975
16140
  });
15976
16141
  }, DAEMON_HEARTBEAT_INTERVAL_MS);
15977
16142
  heartbeatTimer.unref();
15978
- const gitPollingHandle = startGitPollingLoop(ctx);
15979
- const taskMonitorHandle = startTaskMonitor(ctx);
16143
+ let gitSubscriptionHandle = null;
15980
16144
  pushGitState(ctx).catch(() => {});
15981
16145
  const shutdown = async () => {
15982
16146
  console.log(`
15983
16147
  [${formatTimestamp()}] Shutting down...`);
15984
16148
  clearInterval(heartbeatTimer);
15985
- gitPollingHandle.stop();
15986
- taskMonitorHandle.stop();
16149
+ if (gitSubscriptionHandle)
16150
+ gitSubscriptionHandle.stop();
15987
16151
  await onDaemonShutdown(ctx);
15988
16152
  releaseLock();
15989
16153
  process.exit(0);
@@ -15992,6 +16156,7 @@ async function startCommandLoop(ctx) {
15992
16156
  process.on("SIGTERM", shutdown);
15993
16157
  process.on("SIGHUP", shutdown);
15994
16158
  const wsClient2 = await getConvexWsClient();
16159
+ gitSubscriptionHandle = startGitRequestSubscription(ctx, wsClient2);
15995
16160
  console.log(`
15996
16161
  Listening for commands...`);
15997
16162
  console.log(`Press Ctrl+C to stop
@@ -16008,7 +16173,7 @@ Listening for commands...`);
16008
16173
  evictStaleDedupEntries(processedCommandIds, processedPingIds, processedGitRefreshIds);
16009
16174
  for (const event of result.events) {
16010
16175
  try {
16011
- console.log(`[${formatTimestamp()}] \uD83D\uDCE1 Stream command event: ${event.type}`);
16176
+ console.log(`[${formatTimestamp()}] \uD83D\uDCE1 Stream command event: ${event.type} (id: ${event._id})`);
16012
16177
  await dispatchCommandEvent(ctx, event, processedCommandIds, processedPingIds, processedGitRefreshIds);
16013
16178
  } catch (err) {
16014
16179
  console.error(`[${formatTimestamp()}] ❌ Stream command event failed: ${err.message}`);
@@ -16025,17 +16190,16 @@ Listening for commands...`);
16025
16190
  }
16026
16191
  var MODEL_REFRESH_INTERVAL_MS;
16027
16192
  var init_command_loop = __esm(() => {
16028
- init_api3();
16029
- init_client2();
16030
- init_machine();
16031
- init_on_daemon_shutdown();
16032
- init_init2();
16033
16193
  init_on_request_start_agent();
16034
16194
  init_on_request_stop_agent();
16035
16195
  init_pid();
16036
- init_git_polling();
16037
16196
  init_git_heartbeat();
16038
- init_task_monitor();
16197
+ init_git_subscription();
16198
+ init_init2();
16199
+ init_api3();
16200
+ init_on_daemon_shutdown();
16201
+ init_client2();
16202
+ init_machine();
16039
16203
  MODEL_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
16040
16204
  });
16041
16205
 
@@ -16047,8 +16211,6 @@ async function daemonStart() {
16047
16211
  var init_daemon_start = __esm(() => {
16048
16212
  init_command_loop();
16049
16213
  init_init2();
16050
- init_start_agent();
16051
- init_stop_agent();
16052
16214
  init_state_recovery();
16053
16215
  });
16054
16216
 
@@ -16706,7 +16868,7 @@ program2.command("task-started").description("[LEGACY] Acknowledge a task and op
16706
16868
  noClassify: skipClassification
16707
16869
  });
16708
16870
  });
16709
- program2.command("classify").description("Classify a task's origin message (entry-point role only). Use task-started --no-classify for handoffs.").requiredOption("--chatroom-id <id>", "Chatroom identifier").requiredOption("--role <role>", "Your role (must be entry-point role)").requiredOption("--task-id <taskId>", "Task ID to acknowledge").requiredOption("--origin-message-classification <type>", "Original message classification: question, new_feature, or follow_up").action(async (options) => {
16871
+ program2.command("classify").description("Classify a task's origin message (entry-point role only).").requiredOption("--chatroom-id <id>", "Chatroom identifier").requiredOption("--role <role>", "Your role (must be entry-point role)").requiredOption("--task-id <taskId>", "Task ID to acknowledge").requiredOption("--origin-message-classification <type>", "Original message classification: question, new_feature, or follow_up").action(async (options) => {
16710
16872
  await maybeRequireAuth();
16711
16873
  const validClassifications = ["question", "new_feature", "follow_up"];
16712
16874
  if (!validClassifications.includes(options.originMessageClassification)) {
@@ -16792,12 +16954,14 @@ program2.command("report-progress").description("Report progress on current task
16792
16954
  });
16793
16955
  });
16794
16956
  var backlogCommand = program2.command("backlog").description("Manage task queue and backlog");
16795
- backlogCommand.command("list").description("List active backlog items").requiredOption("--chatroom-id <id>", "Chatroom identifier").requiredOption("--role <role>", "Your role").option("--limit <n>", "Maximum number of items to show").action(async (options) => {
16957
+ backlogCommand.command("list").description("List backlog items").requiredOption("--chatroom-id <id>", "Chatroom identifier").requiredOption("--role <role>", "Your role").option("--limit <n>", "Maximum number of items to show").option("--sort <sort>", "Sort order: date:desc (default) | priority:desc").option("--filter <filter>", "Filter: unscored (only items without priority score)").action(async (options) => {
16796
16958
  await maybeRequireAuth();
16797
16959
  const { listBacklog: listBacklog2 } = await Promise.resolve().then(() => (init_backlog(), exports_backlog));
16798
16960
  await listBacklog2(options.chatroomId, {
16799
16961
  role: options.role,
16800
- limit: options.limit ? parseInt(options.limit, 10) : undefined
16962
+ limit: options.limit ? parseInt(options.limit, 10) : undefined,
16963
+ sort: options.sort,
16964
+ filter: options.filter
16801
16965
  });
16802
16966
  });
16803
16967
  backlogCommand.command("add").description("Add a backlog item").requiredOption("--chatroom-id <id>", "Chatroom identifier").requiredOption("--role <role>", "Your role (creator)").requiredOption("--content-file <path>", "Path to file containing task content").action(async (options) => {
@@ -16847,6 +17011,21 @@ backlogCommand.command("history").description("View completed and closed backlog
16847
17011
  limit: options.limit ? parseInt(options.limit, 10) : undefined
16848
17012
  });
16849
17013
  });
17014
+ backlogCommand.command("close").description("Close a backlog item (mark as stale/superseded)").requiredOption("--chatroom-id <id>", "Chatroom identifier").requiredOption("--role <role>", "Your role").requiredOption("--backlog-item-id <id>", "Backlog item ID to close").requiredOption("--reason <text>", "Reason for closing (required for audit trail)").action(async (options) => {
17015
+ await maybeRequireAuth();
17016
+ const { closeBacklog: closeBacklog2 } = await Promise.resolve().then(() => (init_backlog(), exports_backlog));
17017
+ await closeBacklog2(options.chatroomId, options);
17018
+ });
17019
+ backlogCommand.command("export").description("Export backlog items to a JSON file").requiredOption("--chatroom-id <id>", "Chatroom identifier").requiredOption("--role <role>", "Your role").option("--path <path>", "Directory path to export to (default: .chatroom/exports/)").action(async (options) => {
17020
+ await maybeRequireAuth();
17021
+ const { exportBacklog: exportBacklog2 } = await Promise.resolve().then(() => (init_backlog(), exports_backlog));
17022
+ await exportBacklog2(options.chatroomId, { role: options.role, path: options.path });
17023
+ });
17024
+ backlogCommand.command("import").description("Import backlog items from a JSON export file").requiredOption("--chatroom-id <id>", "Chatroom identifier").requiredOption("--role <role>", "Your role").option("--path <path>", "Directory path to import from (default: .chatroom/exports/)").action(async (options) => {
17025
+ await maybeRequireAuth();
17026
+ const { importBacklog: importBacklog2 } = await Promise.resolve().then(() => (init_backlog(), exports_backlog));
17027
+ await importBacklog2(options.chatroomId, { role: options.role, path: options.path });
17028
+ });
16850
17029
  var taskCommand = program2.command("task").description("Manage tasks");
16851
17030
  taskCommand.command("read").description("Read a task and mark it as in_progress").requiredOption("--chatroom-id <id>", "Chatroom identifier").requiredOption("--role <role>", "Your role in the chatroom").requiredOption("--task-id <taskId>", "Task ID to read").action(async (options) => {
16852
17031
  await maybeRequireAuth();