chatroom-cli 1.11.2 → 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 +1613 -1423
  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
@@ -11685,8 +11685,8 @@ var init_utils = __esm(() => {
11685
11685
 
11686
11686
  // ../../services/backend/prompts/base/shared/getting-started-content.ts
11687
11687
  var init_getting_started_content = __esm(() => {
11688
- init_utils();
11689
11688
  init_reminder();
11689
+ init_utils();
11690
11690
  });
11691
11691
 
11692
11692
  // ../../services/backend/prompts/cli/index.ts
@@ -12320,8 +12320,7 @@ async function classify(chatroomId, options, deps) {
12320
12320
  console.error(`❌ \`classify\` is only available to the entry point role (${entryPoint}). Your role is ${role}.`);
12321
12321
  console.error("");
12322
12322
  console.error(" Entry point roles receive user messages and must classify them.");
12323
- console.error(" Other roles receive handoffs and should use:");
12324
- 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.");
12325
12324
  process.exit(1);
12326
12325
  }
12327
12326
  if (originMessageClassification === "new_feature") {
@@ -12815,12 +12814,19 @@ __export(exports_backlog, {
12815
12814
  patchBacklog: () => patchBacklog,
12816
12815
  markForReviewBacklog: () => markForReviewBacklog,
12817
12816
  listBacklog: () => listBacklog,
12817
+ importBacklog: () => importBacklog,
12818
12818
  historyBacklog: () => historyBacklog,
12819
+ exportBacklog: () => exportBacklog,
12820
+ computeContentHash: () => computeContentHash,
12819
12821
  completeBacklog: () => completeBacklog,
12822
+ closeBacklog: () => closeBacklog,
12820
12823
  addBacklog: () => addBacklog
12821
12824
  });
12825
+ import { createHash } from "node:crypto";
12826
+ import * as nodePath from "node:path";
12822
12827
  async function createDefaultDeps11() {
12823
12828
  const client2 = await getConvexClient();
12829
+ const fs = await import("node:fs/promises");
12824
12830
  return {
12825
12831
  backend: {
12826
12832
  mutation: (endpoint, args) => client2.mutation(endpoint, args),
@@ -12830,6 +12836,11 @@ async function createDefaultDeps11() {
12830
12836
  getSessionId,
12831
12837
  getConvexUrl,
12832
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)
12833
12844
  }
12834
12845
  };
12835
12846
  }
@@ -12856,17 +12867,19 @@ async function listBacklog(chatroomId, options, deps) {
12856
12867
  const backlogItems = await d.backend.query(api.backlog.listBacklogItems, {
12857
12868
  sessionId,
12858
12869
  chatroomId,
12859
- statusFilter: "active",
12870
+ statusFilter: "backlog",
12871
+ sort: options.sort,
12872
+ filter: options.filter,
12860
12873
  limit
12861
12874
  });
12862
12875
  console.log("");
12863
12876
  console.log("══════════════════════════════════════════════════");
12864
- console.log("\uD83D\uDCCB ACTIVE BACKLOG");
12877
+ console.log("\uD83D\uDCCB BACKLOG");
12865
12878
  console.log("══════════════════════════════════════════════════");
12866
12879
  console.log(`Chatroom: ${chatroomId}`);
12867
12880
  console.log("");
12868
12881
  if (backlogItems.length === 0) {
12869
- console.log("No active backlog items.");
12882
+ console.log("No backlog items.");
12870
12883
  } else {
12871
12884
  console.log("──────────────────────────────────────────────────");
12872
12885
  for (let i2 = 0;i2 < backlogItems.length; i2++) {
@@ -13149,7 +13162,7 @@ async function historyBacklog(chatroomId, options, deps) {
13149
13162
  const sessionId = requireAuth2(d);
13150
13163
  validateChatroomId(chatroomId);
13151
13164
  const now = Date.now();
13152
- const defaultFrom = now - 30 * 24 * 60 * 60 * 1000;
13165
+ const defaultFrom = now - 2592000000;
13153
13166
  let fromMs;
13154
13167
  let toMs;
13155
13168
  if (options.from) {
@@ -13239,6 +13252,38 @@ async function historyBacklog(chatroomId, options, deps) {
13239
13252
  return;
13240
13253
  }
13241
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
+ }
13242
13287
  function getStatusEmoji(status) {
13243
13288
  switch (status) {
13244
13289
  case "pending":
@@ -13259,10 +13304,121 @@ function getStatusEmoji(status) {
13259
13304
  return "⚫";
13260
13305
  }
13261
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";
13262
13417
  var init_backlog = __esm(() => {
13263
13418
  init_api3();
13264
13419
  init_storage();
13265
13420
  init_client2();
13421
+ STALENESS_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1000;
13266
13422
  });
13267
13423
 
13268
13424
  // src/utils/file-content.ts
@@ -13354,6 +13510,32 @@ async function taskRead(chatroomId, options, deps) {
13354
13510
  console.log(`✅ Task content:`);
13355
13511
  console.log(` Task ID: ${result.taskId}`);
13356
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
+ }
13357
13539
  console.log(`
13358
13540
  ${result.content}`);
13359
13541
  } catch (error) {
@@ -14256,317 +14438,98 @@ var init_get_system_prompt = __esm(() => {
14256
14438
  // ../../services/backend/config/reliability.ts
14257
14439
  var DAEMON_HEARTBEAT_INTERVAL_MS = 30000, AGENT_REQUEST_DEADLINE_MS = 120000;
14258
14440
 
14259
- // src/commands/machine/daemon-start/utils.ts
14260
- function formatTimestamp() {
14261
- return new Date().toISOString().replace("T", " ").substring(0, 19);
14262
- }
14263
-
14264
- // src/commands/machine/daemon-start/types.ts
14265
- function agentKey2(chatroomId, role) {
14266
- return `${chatroomId}:${role.toLowerCase()}`;
14267
- }
14268
-
14269
- // src/events/lifecycle/on-agent-shutdown.ts
14270
- async function onAgentShutdown(ctx, options) {
14271
- const { chatroomId, role, pid, skipKill } = options;
14272
- try {
14273
- ctx.pendingStops.set(agentKey2(chatroomId, role), options.stopReason ?? "user.stop");
14274
- } catch (e) {
14275
- console.log(` ⚠️ Failed to mark intentional stop for ${role}: ${e.message}`);
14276
- }
14277
- let killed = false;
14278
- if (!skipKill) {
14279
- try {
14280
- ctx.deps.processes.kill(-pid, "SIGTERM");
14281
- } catch (e) {
14282
- const isEsrch = e.code === "ESRCH" || e.message?.includes("ESRCH");
14283
- if (isEsrch) {
14284
- killed = true;
14285
- }
14286
- if (!isEsrch) {
14287
- console.log(` ⚠️ Failed to send SIGTERM to ${role}: ${e.message}`);
14288
- }
14289
- }
14290
- if (!killed) {
14291
- const SIGTERM_TIMEOUT_MS = 1e4;
14292
- const POLL_INTERVAL_MS2 = 500;
14293
- const deadline = Date.now() + SIGTERM_TIMEOUT_MS;
14294
- while (Date.now() < deadline) {
14295
- await ctx.deps.clock.delay(POLL_INTERVAL_MS2);
14296
- try {
14297
- ctx.deps.processes.kill(pid, 0);
14298
- } catch {
14299
- killed = true;
14300
- break;
14301
- }
14302
- }
14303
- }
14304
- if (!killed) {
14305
- try {
14306
- ctx.deps.processes.kill(-pid, "SIGKILL");
14307
- } catch {
14308
- killed = true;
14309
- }
14310
- }
14311
- if (!killed) {
14312
- await ctx.deps.clock.delay(5000);
14313
- try {
14314
- ctx.deps.processes.kill(pid, 0);
14315
- console.log(` ⚠️ Process ${pid} (${role}) still alive after SIGKILL — possible zombie`);
14316
- } catch {
14317
- killed = true;
14318
- }
14319
- }
14320
- }
14321
- if (killed || skipKill) {
14322
- try {
14323
- ctx.deps.machine.clearAgentPid(ctx.machineId, chatroomId, role);
14324
- } catch (e) {
14325
- console.log(` ⚠️ Failed to clear local PID for ${role}: ${e.message}`);
14326
- }
14327
- }
14328
- return {
14329
- killed: killed || (skipKill ?? false),
14330
- cleaned: killed || (skipKill ?? false)
14331
- };
14332
- }
14333
- var init_on_agent_shutdown = () => {};
14334
-
14335
- // src/events/lifecycle/on-daemon-shutdown.ts
14336
- async function onDaemonShutdown(ctx) {
14337
- const agents = ctx.deps.machine.listAgentEntries(ctx.machineId);
14338
- if (agents.length > 0) {
14339
- console.log(`[${formatTimestamp()}] Stopping ${agents.length} agent(s)...`);
14340
- await Promise.allSettled(agents.map(async ({ chatroomId, role, entry }) => {
14341
- const result = await onAgentShutdown(ctx, {
14342
- chatroomId,
14343
- role,
14344
- pid: entry.pid
14345
- });
14346
- if (result.killed) {
14347
- console.log(` Sent SIGTERM to ${role} (PID ${entry.pid})`);
14348
- } else {
14349
- console.log(` ${role} (PID ${entry.pid}) already exited`);
14350
- }
14351
- return result;
14352
- }));
14353
- await ctx.deps.clock.delay(AGENT_SHUTDOWN_TIMEOUT_MS);
14354
- for (const { role, entry } of agents) {
14355
- try {
14356
- ctx.deps.processes.kill(entry.pid, 0);
14357
- ctx.deps.processes.kill(entry.pid, "SIGKILL");
14358
- console.log(` Force-killed ${role} (PID ${entry.pid})`);
14359
- } catch {}
14360
- }
14361
- 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;
14362
14447
  }
14363
- try {
14364
- 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, {
14365
14461
  sessionId: ctx.sessionId,
14462
+ chatroomId: event.chatroomId,
14366
14463
  machineId: ctx.machineId,
14367
- 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}`);
14368
14469
  });
14369
- } catch {}
14470
+ }
14370
14471
  }
14371
- var AGENT_SHUTDOWN_TIMEOUT_MS = 5000;
14372
- var init_on_daemon_shutdown = __esm(() => {
14472
+ var init_on_request_start_agent = __esm(() => {
14373
14473
  init_api3();
14374
- init_on_agent_shutdown();
14375
14474
  });
14376
14475
 
14377
- // src/commands/machine/daemon-start/handlers/shared.ts
14378
- async function clearAgentPidEverywhere(ctx, chatroomId, role) {
14379
- try {
14380
- await ctx.deps.backend.mutation(api.machines.updateSpawnedAgent, {
14381
- sessionId: ctx.sessionId,
14382
- machineId: ctx.machineId,
14383
- chatroomId,
14384
- role,
14385
- pid: undefined
14386
- });
14387
- } catch (e) {
14388
- console.log(` ⚠️ Failed to clear PID in backend: ${e.message}`);
14389
- }
14390
- 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 };
14391
14491
  }
14392
- var init_shared = __esm(() => {
14393
- init_api3();
14394
- });
14395
14492
 
14396
- // src/commands/machine/daemon-start/handlers/state-recovery.ts
14397
- async function recoverAgentState(ctx) {
14398
- const entries = ctx.deps.machine.listAgentEntries(ctx.machineId);
14399
- if (entries.length === 0) {
14400
- console.log(` No agent entries found — nothing to recover`);
14401
- } else {
14402
- let recovered = 0;
14403
- let cleared = 0;
14404
- const chatroomIds = new Set;
14405
- for (const { chatroomId, role, entry } of entries) {
14406
- const { pid, harness } = entry;
14407
- const service = ctx.agentServices.get(harness) ?? ctx.agentServices.values().next().value;
14408
- const alive = service ? service.isAlive(pid) : false;
14409
- if (alive) {
14410
- console.log(` ✅ Recovered: ${role} (PID ${pid}, harness: ${harness})`);
14411
- recovered++;
14412
- chatroomIds.add(chatroomId);
14413
- } else {
14414
- console.log(` \uD83E\uDDF9 Stale PID ${pid} for ${role} — clearing`);
14415
- await clearAgentPidEverywhere(ctx, chatroomId, role);
14416
- cleared++;
14417
- }
14418
- }
14419
- console.log(` Recovery complete: ${recovered} alive, ${cleared} stale cleared`);
14420
- for (const chatroomId of chatroomIds) {
14421
- try {
14422
- const configsResult = await ctx.deps.backend.query(api.machines.getMachineAgentConfigs, {
14423
- sessionId: ctx.sessionId,
14424
- chatroomId
14425
- });
14426
- for (const config3 of configsResult.configs) {
14427
- if (config3.machineId === ctx.machineId && config3.workingDir) {
14428
- ctx.activeWorkingDirs.add(config3.workingDir);
14429
- }
14430
- }
14431
- } catch {}
14432
- }
14433
- if (ctx.activeWorkingDirs.size > 0) {
14434
- console.log(` \uD83D\uDD00 Recovered ${ctx.activeWorkingDirs.size} active working dir(s) for git tracking`);
14435
- }
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;
14436
14498
  }
14499
+ await executeStopAgent(ctx, {
14500
+ chatroomId: event.chatroomId,
14501
+ role: event.role,
14502
+ reason: event.reason
14503
+ });
14437
14504
  }
14438
- var init_state_recovery = __esm(() => {
14439
- init_api3();
14440
- init_shared();
14441
- });
14505
+ var init_on_request_stop_agent = () => {};
14442
14506
 
14443
- // src/infrastructure/services/harness-spawning/rate-limiter.ts
14444
- class SpawnRateLimiter {
14445
- config;
14446
- buckets = new Map;
14447
- constructor(config3 = {}) {
14448
- 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 });
14449
14522
  }
14450
- tryConsume(chatroomId, reason) {
14451
- if (reason.startsWith("user.")) {
14452
- return { allowed: true };
14453
- }
14454
- const bucket = this._getOrCreateBucket(chatroomId);
14455
- this._refill(bucket);
14456
- if (bucket.tokens < 1) {
14457
- const elapsed = Date.now() - bucket.lastRefillAt;
14458
- const retryAfterMs = this.config.refillRateMs - elapsed;
14459
- console.warn(`⚠️ [RateLimiter] Agent spawn rate-limited for chatroom ${chatroomId} (reason: ${reason}). Retry after ${retryAfterMs}ms`);
14460
- return { allowed: false, retryAfterMs };
14461
- }
14462
- bucket.tokens -= 1;
14463
- const remaining = Math.floor(bucket.tokens);
14464
- if (remaining <= LOW_TOKEN_THRESHOLD) {
14465
- console.warn(`⚠️ [RateLimiter] Agent spawn tokens running low for chatroom ${chatroomId} (${remaining}/${this.config.maxTokens} remaining)`);
14466
- }
14467
- return { allowed: true };
14468
- }
14469
- getStatus(chatroomId) {
14470
- const bucket = this._getOrCreateBucket(chatroomId);
14471
- this._refill(bucket);
14472
- return {
14473
- remaining: Math.floor(bucket.tokens),
14474
- total: this.config.maxTokens
14475
- };
14476
- }
14477
- _getOrCreateBucket(chatroomId) {
14478
- if (!this.buckets.has(chatroomId)) {
14479
- this.buckets.set(chatroomId, {
14480
- tokens: this.config.initialTokens,
14481
- lastRefillAt: Date.now()
14482
- });
14483
- }
14484
- return this.buckets.get(chatroomId);
14485
- }
14486
- _refill(bucket) {
14487
- const now = Date.now();
14488
- const elapsed = now - bucket.lastRefillAt;
14489
- if (elapsed >= this.config.refillRateMs) {
14490
- const tokensToAdd = Math.floor(elapsed / this.config.refillRateMs);
14491
- bucket.tokens = Math.min(this.config.maxTokens, bucket.tokens + tokensToAdd);
14492
- bucket.lastRefillAt += tokensToAdd * this.config.refillRateMs;
14493
- }
14494
- }
14495
- }
14496
- var DEFAULT_CONFIG, LOW_TOKEN_THRESHOLD = 1;
14497
- var init_rate_limiter = __esm(() => {
14498
- DEFAULT_CONFIG = {
14499
- maxTokens: 5,
14500
- refillRateMs: 60000,
14501
- initialTokens: 5
14502
- };
14503
- });
14504
-
14505
- // src/infrastructure/services/harness-spawning/harness-spawning-service.ts
14506
- class HarnessSpawningService {
14507
- rateLimiter;
14508
- concurrentAgents = new Map;
14509
- constructor({ rateLimiter }) {
14510
- this.rateLimiter = rateLimiter;
14511
- }
14512
- shouldAllowSpawn(chatroomId, reason) {
14513
- const current = this.concurrentAgents.get(chatroomId) ?? 0;
14514
- if (current >= MAX_CONCURRENT_AGENTS_PER_CHATROOM) {
14515
- console.warn(`⚠️ [HarnessSpawningService] Concurrent agent limit reached for chatroom ${chatroomId} ` + `(${current}/${MAX_CONCURRENT_AGENTS_PER_CHATROOM} active agents). Spawn rejected.`);
14516
- return { allowed: false };
14517
- }
14518
- const result = this.rateLimiter.tryConsume(chatroomId, reason);
14519
- if (!result.allowed) {
14520
- console.warn(`⚠️ [HarnessSpawningService] Spawn blocked by rate limiter for chatroom ${chatroomId} ` + `(reason: ${reason}).`);
14521
- }
14522
- return result;
14523
- }
14524
- recordSpawn(chatroomId) {
14525
- const current = this.concurrentAgents.get(chatroomId) ?? 0;
14526
- this.concurrentAgents.set(chatroomId, current + 1);
14527
- }
14528
- recordExit(chatroomId) {
14529
- const current = this.concurrentAgents.get(chatroomId) ?? 0;
14530
- const next = Math.max(0, current - 1);
14531
- this.concurrentAgents.set(chatroomId, next);
14532
- }
14533
- getConcurrentCount(chatroomId) {
14534
- return this.concurrentAgents.get(chatroomId) ?? 0;
14535
- }
14536
- }
14537
- var MAX_CONCURRENT_AGENTS_PER_CHATROOM = 10;
14538
-
14539
- // src/infrastructure/services/harness-spawning/index.ts
14540
- var init_harness_spawning = __esm(() => {
14541
- init_rate_limiter();
14542
- });
14543
-
14544
- // src/commands/machine/pid.ts
14545
- import { createHash } from "node:crypto";
14546
- import { existsSync as existsSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4, unlinkSync as unlinkSync2, mkdirSync as mkdirSync4 } from "node:fs";
14547
- import { homedir as homedir4 } from "node:os";
14548
- import { join as join5 } from "node:path";
14549
- function getUrlHash() {
14550
- const url = getConvexUrl();
14551
- return createHash("sha256").update(url).digest("hex").substring(0, 8);
14552
- }
14553
- function getPidFileName() {
14554
- return `daemon-${getUrlHash()}.pid`;
14555
- }
14556
- function ensureChatroomDir() {
14557
- if (!existsSync4(CHATROOM_DIR4)) {
14558
- mkdirSync4(CHATROOM_DIR4, { recursive: true, mode: 448 });
14559
- }
14560
- }
14561
- function getPidFilePath() {
14562
- return join5(CHATROOM_DIR4, getPidFileName());
14563
- }
14564
- function isProcessRunning(pid) {
14565
- try {
14566
- process.kill(pid, 0);
14567
- return true;
14568
- } catch {
14569
- 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;
14570
14533
  }
14571
14534
  }
14572
14535
  function readPid() {
@@ -14624,858 +14587,566 @@ function releaseLock() {
14624
14587
  var CHATROOM_DIR4;
14625
14588
  var init_pid = __esm(() => {
14626
14589
  init_client2();
14627
- CHATROOM_DIR4 = join5(homedir4(), ".chatroom");
14590
+ CHATROOM_DIR4 = join6(homedir4(), ".chatroom");
14628
14591
  });
14629
14592
 
14630
- // src/events/daemon/event-bus.ts
14631
- class DaemonEventBus {
14632
- listeners = new Map;
14633
- on(event, listener) {
14634
- if (!this.listeners.has(event)) {
14635
- this.listeners.set(event, new Set);
14636
- }
14637
- this.listeners.get(event).add(listener);
14638
- return () => {
14639
- this.listeners.get(event)?.delete(listener);
14640
- };
14641
- }
14642
- emit(event, payload) {
14643
- const set = this.listeners.get(event);
14644
- if (!set)
14645
- return;
14646
- for (const listener of set) {
14647
- try {
14648
- listener(payload);
14649
- } catch (err) {
14650
- console.warn(`[EventBus] Listener error on "${event}": ${err.message}`);
14651
- }
14652
- }
14653
- }
14654
- removeAllListeners() {
14655
- this.listeners.clear();
14656
- }
14593
+ // src/commands/machine/daemon-start/utils.ts
14594
+ function formatTimestamp() {
14595
+ return new Date().toISOString().replace("T", " ").substring(0, 19);
14657
14596
  }
14658
14597
 
14659
- // src/events/daemon/agent/on-agent-exited.ts
14660
- function onAgentExited(ctx, payload) {
14661
- const { chatroomId, role, pid, code: code2, signal, stopReason } = payload;
14662
- const ts = formatTimestamp();
14663
- console.log(`[${ts}] Agent stopped: ${stopReason} (${role})`);
14664
- const isDaemonRespawn = stopReason === "daemon.respawn";
14665
- const isIntentional = stopReason === "user.stop" || stopReason === "platform.team_switch" || stopReason === "agent_process.turn_end" || stopReason === "agent_process.turn_end_quick_fail";
14666
- if (isIntentional && !isDaemonRespawn) {
14667
- console.log(`[${ts}] ℹ️ Agent process exited after intentional stop ` + `(PID: ${pid}, role: ${role}, code: ${code2}, signal: ${signal})`);
14668
- } else if (isDaemonRespawn) {
14669
- console.log(`[${ts}] \uD83D\uDD04 Agent process stopped for respawn ` + `(PID: ${pid}, role: ${role}, code: ${code2}, signal: ${signal})`);
14670
- } else {
14671
- console.log(`[${ts}] ⚠️ Agent process exited ` + `(PID: ${pid}, role: ${role}, code: ${code2}, signal: ${signal})`);
14672
- }
14673
- ctx.deps.backend.mutation(api.machines.recordAgentExited, {
14674
- sessionId: ctx.sessionId,
14675
- machineId: ctx.machineId,
14676
- chatroomId,
14677
- role,
14678
- pid,
14679
- stopReason,
14680
- stopSignal: stopReason === "agent_process.signal" ? signal ?? undefined : undefined,
14681
- exitCode: code2 ?? undefined,
14682
- signal: signal ?? undefined,
14683
- agentHarness: payload.agentHarness
14684
- }).catch((err) => {
14685
- console.log(` ⚠️ Failed to record agent exit event: ${err.message}`);
14686
- });
14687
- ctx.deps.machine.clearAgentPid(ctx.machineId, chatroomId, role);
14688
- for (const service of ctx.agentServices.values()) {
14689
- service.untrack(pid);
14690
- }
14598
+ // src/infrastructure/git/types.ts
14599
+ function makeGitStateKey(machineId, workingDir) {
14600
+ return `${machineId}::${workingDir}`;
14691
14601
  }
14692
- var init_on_agent_exited = __esm(() => {
14693
- init_api3();
14694
- });
14602
+ var FULL_DIFF_MAX_BYTES = 500000, COMMITS_PER_PAGE = 20;
14695
14603
 
14696
- // src/events/daemon/agent/on-agent-started.ts
14697
- function onAgentStarted(ctx, payload) {
14698
- const ts = formatTimestamp();
14699
- 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
+ }
14700
14618
  }
14701
- var init_on_agent_started = () => {};
14702
-
14703
- // src/events/daemon/agent/on-agent-stopped.ts
14704
- function onAgentStopped(ctx, payload) {
14705
- const ts = formatTimestamp();
14706
- 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");
14707
14621
  }
14708
- var init_on_agent_stopped = () => {};
14709
-
14710
- // src/events/daemon/register-listeners.ts
14711
- function registerEventListeners(ctx) {
14712
- const unsubs = [];
14713
- unsubs.push(ctx.events.on("agent:exited", (payload) => onAgentExited(ctx, payload)));
14714
- unsubs.push(ctx.events.on("agent:started", (payload) => onAgentStarted(ctx, payload)));
14715
- unsubs.push(ctx.events.on("agent:stopped", (payload) => onAgentStopped(ctx, payload)));
14716
- return () => {
14717
- for (const unsub of unsubs) {
14718
- unsub();
14719
- }
14720
- };
14622
+ function isNotAGitRepo(message) {
14623
+ return message.includes("not a git repository") || message.includes("Not a git repository");
14721
14624
  }
14722
- var init_register_listeners = __esm(() => {
14723
- init_on_agent_exited();
14724
- init_on_agent_started();
14725
- init_on_agent_stopped();
14726
- });
14727
-
14728
- // src/commands/machine/daemon-start/init.ts
14729
- import { stat } from "node:fs/promises";
14730
- async function discoverModels(agentServices) {
14731
- const results = {};
14732
- for (const [harness, service] of agentServices) {
14733
- if (service.isInstalled()) {
14734
- try {
14735
- results[harness] = await service.listModels();
14736
- } catch {
14737
- results[harness] = [];
14738
- }
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" };
14739
14655
  }
14656
+ return classifyError(errMsg);
14740
14657
  }
14741
- 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 };
14742
14663
  }
14743
- 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/);
14744
14674
  return {
14745
- backend: {
14746
- mutation: async () => {
14747
- throw new Error("Backend not initialized");
14748
- },
14749
- query: async () => {
14750
- throw new Error("Backend not initialized");
14751
- }
14752
- },
14753
- processes: {
14754
- kill: (pid, signal) => process.kill(pid, signal)
14755
- },
14756
- fs: {
14757
- stat
14758
- },
14759
- machine: {
14760
- clearAgentPid,
14761
- persistAgentPid,
14762
- listAgentEntries,
14763
- persistEventCursor,
14764
- loadEventCursor
14765
- },
14766
- clock: {
14767
- now: () => Date.now(),
14768
- delay: (ms) => new Promise((resolve2) => setTimeout(resolve2, ms))
14769
- },
14770
- 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
14771
14678
  };
14772
14679
  }
14773
- function validateAuthentication(convexUrl) {
14774
- const sessionId = getSessionId();
14775
- if (!sessionId) {
14776
- const otherUrls = getOtherSessionUrls();
14777
- console.error(`❌ Not authenticated for: ${convexUrl}`);
14778
- if (otherUrls.length > 0) {
14779
- console.error(`
14780
- \uD83D\uDCA1 You have sessions for other environments:`);
14781
- for (const url of otherUrls) {
14782
- console.error(` • ${url}`);
14783
- }
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" };
14784
14686
  }
14785
- console.error(`
14786
- Run: chatroom auth login`);
14787
- releaseLock();
14788
- process.exit(1);
14687
+ const classified = classifyError(errMsg);
14688
+ if (classified.status === "not_found")
14689
+ return { status: "not_found" };
14690
+ return classified;
14789
14691
  }
14790
- 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 };
14791
14708
  }
14792
- async function validateSession(client2, sessionId, convexUrl) {
14793
- const validation = await client2.query(api.cliAuth.validateSession, { sessionId });
14794
- if (!validation.valid) {
14795
- console.error(`❌ Session invalid: ${validation.reason}`);
14796
- console.error(`
14797
- Run: chatroom auth login`);
14798
- releaseLock();
14799
- 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 };
14800
14730
  }
14731
+ return { status: "available", content: raw, truncated: false };
14801
14732
  }
14802
- function setupMachine() {
14803
- ensureMachineRegistered();
14804
- const config3 = loadMachineConfig();
14805
- 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;
14806
14756
  }
14807
- async function registerCapabilities(client2, sessionId, config3, agentServices) {
14808
- const { machineId } = config3;
14809
- const availableModels = await discoverModels(agentServices);
14810
- try {
14811
- await client2.mutation(api.machines.register, {
14812
- sessionId,
14813
- machineId,
14814
- hostname: config3.hostname,
14815
- os: config3.os,
14816
- availableHarnesses: config3.availableHarnesses,
14817
- harnessVersions: config3.harnessVersions,
14818
- availableModels
14819
- });
14820
- } catch (error) {
14821
- 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;
14822
14768
  }
14823
- 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 };
14824
14776
  }
14825
- 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) {
14826
14791
  try {
14827
- await client2.mutation(api.machines.updateDaemonStatus, {
14828
- sessionId,
14829
- machineId,
14830
- connected: true
14792
+ const result = await execAsync2(command, {
14793
+ cwd,
14794
+ env: { ...process.env, NO_COLOR: "1" },
14795
+ timeout: 15000
14831
14796
  });
14832
- } catch (error) {
14833
- if (isNetworkError(error)) {
14834
- formatConnectivityError(error, convexUrl);
14835
- } else {
14836
- console.error(`❌ Failed to update daemon status: ${error.message}`);
14837
- }
14838
- releaseLock();
14839
- process.exit(1);
14797
+ return result;
14798
+ } catch (err) {
14799
+ return { error: err };
14840
14800
  }
14841
14801
  }
14842
- function logStartup(ctx, availableModels) {
14843
- console.log(`[${formatTimestamp()}] \uD83D\uDE80 Daemon started`);
14844
- console.log(` CLI version: ${getVersion()}`);
14845
- console.log(` Machine ID: ${ctx.machineId}`);
14846
- console.log(` Hostname: ${ctx.config?.hostname ?? "unknown"}`);
14847
- console.log(` Available harnesses: ${ctx.config?.availableHarnesses.join(", ") || "none"}`);
14848
- console.log(` Available models: ${Object.keys(availableModels).length > 0 ? `${Object.values(availableModels).flat().length} models across ${Object.keys(availableModels).join(", ")}` : "none discovered"}`);
14849
- 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;
14850
14813
  }
14851
- async function recoverState(ctx) {
14852
- console.log(`
14853
- [${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 [];
14854
14833
  try {
14855
- await recoverAgentState(ctx);
14856
- } catch (e) {
14857
- console.log(` ⚠️ Recovery failed: ${e.message}`);
14858
- 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 [];
14859
14846
  }
14860
14847
  }
14861
- async function initDaemon() {
14862
- if (!acquireLock()) {
14863
- 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
+ }
14864
14889
  }
14865
- const convexUrl = getConvexUrl();
14866
- const sessionId = validateAuthentication(convexUrl);
14867
- const client2 = await getConvexClient();
14868
- const typedSessionId = sessionId;
14869
- await validateSession(client2, typedSessionId, convexUrl);
14870
- const config3 = setupMachine();
14871
- const { machineId } = config3;
14872
- initHarnessRegistry();
14873
- const agentServices = new Map(getAllHarnesses().map((s) => [s.id, s]));
14874
- const availableModels = await registerCapabilities(client2, typedSessionId, config3, agentServices);
14875
- await connectDaemon(client2, typedSessionId, machineId, convexUrl);
14876
- const deps = createDefaultDeps19();
14877
- deps.backend.mutation = (endpoint, args) => client2.mutation(endpoint, args);
14878
- deps.backend.query = (endpoint, args) => client2.query(endpoint, args);
14879
- const events = new DaemonEventBus;
14880
- const ctx = {
14881
- client: client2,
14882
- sessionId: typedSessionId,
14883
- machineId,
14884
- config: config3,
14885
- deps,
14886
- events,
14887
- agentServices,
14888
- activeWorkingDirs: new Set,
14889
- lastPushedGitState: new Map,
14890
- pendingStops: new Map
14891
- };
14892
- registerEventListeners(ctx);
14893
- logStartup(ctx, availableModels);
14894
- await recoverState(ctx);
14895
- return ctx;
14896
- }
14897
- var init_init2 = __esm(() => {
14898
- init_state_recovery();
14899
- init_api3();
14900
- init_storage();
14901
- init_client2();
14902
- init_machine();
14903
- init_remote_agents();
14904
- init_harness_spawning();
14905
- init_error_formatting();
14906
- init_version();
14907
- init_pid();
14908
- init_register_listeners();
14909
- });
14910
-
14911
- // src/infrastructure/machine/stop-reason.ts
14912
- function resolveStopReason(code2, signal, wasIntentional) {
14913
- if (wasIntentional)
14914
- return "user.stop";
14915
- if (signal !== null)
14916
- return "agent_process.signal";
14917
- if (code2 === 0)
14918
- return "agent_process.exited_clean";
14919
- return "agent_process.crashed";
14890
+ return { filesChanged: 0, insertions: 0, deletions: 0 };
14920
14891
  }
14921
-
14922
- // src/commands/machine/daemon-start/handlers/start-agent.ts
14923
- async function executeStartAgent(ctx, args) {
14924
- const { chatroomId, role, agentHarness, model, workingDir, reason } = args;
14925
- console.log(` ↪ start-agent command received`);
14926
- console.log(` Chatroom: ${chatroomId}`);
14927
- console.log(` Role: ${role}`);
14928
- console.log(` Harness: ${agentHarness}`);
14929
- if (reason) {
14930
- console.log(` Reason: ${reason}`);
14931
- }
14932
- if (model) {
14933
- console.log(` Model: ${model}`);
14934
- }
14935
- if (!workingDir) {
14936
- const msg2 = `No workingDir provided in command payload for ${chatroomId}/${role}`;
14937
- console.log(` ⚠️ ${msg2}`);
14938
- return { result: msg2, failed: true };
14939
- }
14940
- console.log(` Working dir: ${workingDir}`);
14941
- try {
14942
- const dirStat = await ctx.deps.fs.stat(workingDir);
14943
- if (!dirStat.isDirectory()) {
14944
- const msg2 = `Working directory is not a directory: ${workingDir}`;
14945
- console.log(` ⚠️ ${msg2}`);
14946
- return { result: msg2, failed: true };
14947
- }
14948
- } catch {
14949
- const msg2 = `Working directory does not exist: ${workingDir}`;
14950
- console.log(` ⚠️ ${msg2}`);
14951
- return { result: msg2, failed: true };
14952
- }
14953
- try {
14954
- 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, {
14955
14898
  sessionId: ctx.sessionId,
14956
- chatroomId
14957
- });
14958
- const existingConfig = existingConfigs.configs.find((c) => c.machineId === ctx.machineId && c.role.toLowerCase() === role.toLowerCase());
14959
- const backendPid = existingConfig?.spawnedAgentPid;
14960
- const localEntry = ctx.deps.machine.listAgentEntries(ctx.machineId).find((e) => e.chatroomId === chatroomId && e.role.toLowerCase() === role.toLowerCase());
14961
- const localPid = localEntry?.entry.pid;
14962
- const pidsToKill = [
14963
- ...new Set([backendPid, localPid].filter((p) => p !== undefined))
14964
- ];
14965
- const anyService = ctx.agentServices.values().next().value;
14966
- for (const pid2 of pidsToKill) {
14967
- const isAlive = anyService ? anyService.isAlive(pid2) : false;
14968
- if (isAlive) {
14969
- console.log(` ⚠️ Existing agent detected (PID: ${pid2}) — stopping before respawn`);
14970
- await onAgentShutdown(ctx, { chatroomId, role, pid: pid2, stopReason: "daemon.respawn" });
14971
- console.log(` ✅ Existing agent stopped (PID: ${pid2})`);
14972
- }
14973
- }
14974
- } catch (e) {
14975
- console.log(` ⚠️ Could not check for existing agent (proceeding): ${e.message}`);
14976
- }
14977
- const convexUrl = getConvexUrl();
14978
- const initPromptResult = await ctx.deps.backend.query(api.messages.getInitPrompt, {
14979
- sessionId: ctx.sessionId,
14980
- chatroomId,
14981
- role,
14982
- convexUrl
14983
- });
14984
- if (!initPromptResult?.prompt) {
14985
- const msg2 = "Failed to fetch init prompt from backend";
14986
- console.log(` ⚠️ ${msg2}`);
14987
- return { result: msg2, failed: true };
14988
- }
14989
- console.log(` Fetched split init prompt from backend`);
14990
- const service = ctx.agentServices.get(agentHarness);
14991
- if (!service) {
14992
- const msg2 = `Unknown agent harness: ${agentHarness}`;
14993
- console.log(` ⚠️ ${msg2}`);
14994
- return { result: msg2, failed: true };
14995
- }
14996
- let spawnResult;
14997
- try {
14998
- spawnResult = await service.spawn({
14999
- workingDir,
15000
- prompt: initPromptResult.initialMessage,
15001
- systemPrompt: initPromptResult.rolePrompt,
15002
- model,
15003
- context: { machineId: ctx.machineId, chatroomId, role }
14899
+ machineId: ctx.machineId,
14900
+ workingDir: req.workingDir,
14901
+ diffContent: result.content,
14902
+ truncated: result.truncated,
14903
+ diffStat
15004
14904
  });
15005
- } catch (e) {
15006
- const msg2 = `Failed to spawn agent: ${e.message}`;
15007
- console.log(` ⚠️ ${msg2}`);
15008
- return { result: msg2, failed: true };
15009
- }
15010
- const { pid } = spawnResult;
15011
- const spawnedAt = Date.now();
15012
- const msg = `Agent spawned (PID: ${pid})`;
15013
- console.log(` ✅ ${msg}`);
15014
- ctx.deps.spawning.recordSpawn(chatroomId);
15015
- try {
15016
- 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, {
15017
14908
  sessionId: ctx.sessionId,
15018
14909
  machineId: ctx.machineId,
15019
- chatroomId,
15020
- role,
15021
- pid,
15022
- model,
15023
- reason
15024
- });
15025
- console.log(` Updated backend with PID: ${pid}`);
15026
- ctx.deps.machine.persistAgentPid(ctx.machineId, chatroomId, role, pid, agentHarness);
15027
- } catch (e) {
15028
- console.log(` ⚠️ Failed to update PID in backend: ${e.message}`);
15029
- }
15030
- ctx.events.emit("agent:started", {
15031
- chatroomId,
15032
- role,
15033
- pid,
15034
- harness: agentHarness,
15035
- model
15036
- });
15037
- ctx.activeWorkingDirs.add(workingDir);
15038
- spawnResult.onExit(({ code: code2, signal }) => {
15039
- ctx.deps.spawning.recordExit(chatroomId);
15040
- const key = agentKey2(chatroomId, role);
15041
- const pendingReason = ctx.pendingStops.get(key) ?? null;
15042
- if (pendingReason) {
15043
- ctx.pendingStops.delete(key);
15044
- }
15045
- const stopReason = pendingReason ?? resolveStopReason(code2, signal, false);
15046
- ctx.events.emit("agent:exited", {
15047
- chatroomId,
15048
- role,
15049
- pid,
15050
- code: code2,
15051
- signal,
15052
- stopReason,
15053
- agentHarness
15054
- });
15055
- });
15056
- if (spawnResult.onAgentEnd) {
15057
- spawnResult.onAgentEnd(() => {
15058
- const elapsed = Date.now() - spawnedAt;
15059
- const isHealthyTurn = elapsed >= MIN_HEALTHY_TURN_MS;
15060
- const stopReason = isHealthyTurn ? "agent_process.turn_end" : "agent_process.turn_end_quick_fail";
15061
- const key = agentKey2(chatroomId, role);
15062
- ctx.pendingStops.set(key, stopReason);
15063
- try {
15064
- ctx.deps.processes.kill(-pid, "SIGTERM");
15065
- } catch {}
14910
+ workingDir: req.workingDir,
14911
+ diffContent: "",
14912
+ truncated: false,
14913
+ diffStat: { filesChanged: 0, insertions: 0, deletions: 0 }
15066
14914
  });
14915
+ console.log(`[${formatTimestamp()}] \uD83D\uDCC4 Full diff pushed (empty): ${req.workingDir} (${result.status})`);
15067
14916
  }
15068
- let lastReportedTokenAt = 0;
15069
- spawnResult.onOutput(() => {
15070
- const now = Date.now();
15071
- if (now - lastReportedTokenAt >= 30000) {
15072
- lastReportedTokenAt = now;
15073
- ctx.deps.backend.mutation(api.participants.updateTokenActivity, {
15074
- sessionId: ctx.sessionId,
15075
- chatroomId,
15076
- role
15077
- }).catch(() => {});
15078
- }
15079
- });
15080
- return { result: msg, failed: false };
15081
14917
  }
15082
- var MIN_HEALTHY_TURN_MS = 30000;
15083
- var init_start_agent = __esm(() => {
15084
- init_api3();
15085
- init_client2();
15086
- init_on_agent_shutdown();
15087
- });
15088
-
15089
- // src/events/daemon/agent/on-request-start-agent.ts
15090
- async function onRequestStartAgent(ctx, event) {
15091
- const eventId = event._id.toString();
15092
- if (Date.now() > event.deadline) {
15093
- console.log(`[daemon] ⏰ Skipping expired agent.requestStart for role=${event.role} (id: ${eventId}, 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
+ });
15094
14937
  return;
15095
14938
  }
15096
- const spawnCheck = ctx.deps.spawning.shouldAllowSpawn(event.chatroomId, event.reason);
15097
- if (!spawnCheck.allowed) {
15098
- const retryMsg = spawnCheck.retryAfterMs ? ` Retry after ${spawnCheck.retryAfterMs}ms.` : "";
15099
- console.warn(`[daemon] ⚠️ Spawn suppressed for chatroom=${event.chatroomId} role=${event.role} reason=${event.reason} (id: ${eventId}).${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
+ });
15100
14951
  return;
15101
14952
  }
15102
- console.log(`[daemon] Processing agent.requestStart (id: ${eventId})`);
15103
- await executeStartAgent(ctx, {
15104
- chatroomId: event.chatroomId,
15105
- role: event.role,
15106
- agentHarness: event.agentHarness,
15107
- model: event.model,
15108
- workingDir: event.workingDir,
15109
- 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
15110
14966
  });
14967
+ console.log(`[${formatTimestamp()}] \uD83D\uDD0D Commit detail pushed: ${req.sha.slice(0, 7)} in ${req.workingDir}`);
15111
14968
  }
15112
- var init_on_request_start_agent = __esm(() => {
15113
- init_start_agent();
15114
- });
15115
-
15116
- // src/commands/machine/daemon-start/handlers/stop-agent.ts
15117
- async function executeStopAgent(ctx, args) {
15118
- const { chatroomId, role, reason } = args;
15119
- const stopReason = reason;
15120
- console.log(` ↪ stop-agent command received`);
15121
- console.log(` Chatroom: ${chatroomId}`);
15122
- console.log(` Role: ${role}`);
15123
- console.log(` Reason: ${reason}`);
15124
- 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, {
15125
14974
  sessionId: ctx.sessionId,
15126
- chatroomId
14975
+ machineId: ctx.machineId,
14976
+ workingDir: req.workingDir,
14977
+ commits,
14978
+ hasMoreCommits
15127
14979
  });
15128
- const targetConfig = configsResult.configs.find((c) => c.machineId === ctx.machineId && c.role.toLowerCase() === role.toLowerCase());
15129
- const backendPid = targetConfig?.spawnedAgentPid;
15130
- const localEntry = ctx.deps.machine.listAgentEntries(ctx.machineId).find((e) => e.chatroomId === chatroomId && e.role.toLowerCase() === role.toLowerCase());
15131
- const localPid = localEntry?.entry.pid;
15132
- const allPids = [...new Set([backendPid, localPid].filter((p) => p !== undefined))];
15133
- if (allPids.length === 0) {
15134
- const msg = "No running agent found (no PID recorded)";
15135
- console.log(` ⚠️ ${msg}`);
15136
- return { result: msg, failed: true };
15137
- }
15138
- const anyService = ctx.agentServices.values().next().value;
15139
- let anyKilled = false;
15140
- let lastError = null;
15141
- for (const pid of allPids) {
15142
- console.log(` Stopping agent with PID: ${pid}`);
15143
- const isAlive = anyService ? anyService.isAlive(pid) : false;
15144
- if (!isAlive) {
15145
- console.log(` ⚠️ PID ${pid} not found — process already exited or was never started`);
15146
- await clearAgentPidEverywhere(ctx, chatroomId, role);
15147
- console.log(` Cleared stale PID`);
15148
- try {
15149
- await ctx.deps.backend.mutation(api.participants.leave, {
15150
- sessionId: ctx.sessionId,
15151
- chatroomId,
15152
- role
15153
- });
15154
- console.log(` Removed participant record`);
15155
- } 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))
15156
14991
  continue;
15157
- }
14992
+ processedRequestIds.set(requestId, Date.now());
15158
14993
  try {
15159
- const shutdownResult = await onAgentShutdown(ctx, {
15160
- chatroomId,
15161
- role,
15162
- pid,
15163
- stopReason
14994
+ await ctx.deps.backend.mutation(api.workspaces.updateRequestStatus, {
14995
+ sessionId: ctx.sessionId,
14996
+ requestId: req._id,
14997
+ status: "processing"
15164
14998
  });
15165
- const msg = shutdownResult.killed ? `Agent stopped (PID: ${pid})` : `Agent stop attempted (PID: ${pid}) — process may still be running`;
15166
- console.log(` ${shutdownResult.killed ? "" : "⚠️ "} ${msg}`);
15167
- if (shutdownResult.killed) {
15168
- 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;
15169
15009
  }
15170
- } catch (e) {
15171
- lastError = e;
15172
- 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(() => {});
15173
15022
  }
15174
15023
  }
15175
- if (lastError && !anyKilled) {
15176
- const msg = `Failed to stop agent: ${lastError.message}`;
15177
- console.log(` ⚠️ ${msg}`);
15178
- return { result: msg, failed: true };
15179
- }
15180
- if (!anyKilled) {
15181
- return {
15182
- result: `All recorded PIDs appear stale (processes not found or belong to different programs)`,
15183
- failed: true
15184
- };
15185
- }
15186
- const killedCount = allPids.length > 1 ? ` (${allPids.length} PIDs)` : ``;
15187
- return { result: `Agent stopped${killedCount}`, failed: false };
15188
15024
  }
15189
- var init_stop_agent = __esm(() => {
15025
+ var init_git_subscription = __esm(() => {
15190
15026
  init_api3();
15191
- init_on_agent_shutdown();
15192
- init_shared();
15027
+ init_git_reader();
15193
15028
  });
15194
15029
 
15195
- // src/events/daemon/agent/on-request-stop-agent.ts
15196
- async function onRequestStopAgent(ctx, event) {
15197
- if (Date.now() > event.deadline) {
15198
- 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}`);
15199
15041
  return;
15200
15042
  }
15201
- await executeStopAgent(ctx, {
15202
- chatroomId: event.chatroomId,
15203
- role: event.role,
15204
- reason: event.reason
15205
- });
15206
- }
15207
- var init_on_request_stop_agent = __esm(() => {
15208
- init_stop_agent();
15209
- });
15210
-
15211
- // src/commands/machine/daemon-start/handlers/ping.ts
15212
- function handlePing() {
15213
- console.log(` ↪ Responding: pong`);
15214
- return { result: "pong", failed: false };
15215
- }
15216
-
15217
- // src/infrastructure/git/types.ts
15218
- function makeGitStateKey(machineId, workingDir) {
15219
- return `${machineId}::${workingDir}`;
15220
- }
15221
- var FULL_DIFF_MAX_BYTES = 500000, COMMITS_PER_PAGE = 20;
15222
-
15223
- // src/infrastructure/git/git-reader.ts
15224
- import { exec as exec2 } from "node:child_process";
15225
- import { promisify as promisify2 } from "node:util";
15226
- async function runGit(args, cwd) {
15227
- try {
15228
- const result = await execAsync2(`git ${args}`, {
15229
- cwd,
15230
- env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_PAGER: "cat", NO_COLOR: "1" },
15231
- maxBuffer: FULL_DIFF_MAX_BYTES + 64 * 1024
15232
- });
15233
- return result;
15234
- } catch (err) {
15235
- return { error: err };
15236
- }
15237
- }
15238
- function isGitNotInstalled(message) {
15239
- return message.includes("command not found") || message.includes("ENOENT") || message.includes("not found") || message.includes("'git' is not recognized");
15240
- }
15241
- function isNotAGitRepo(message) {
15242
- return message.includes("not a git repository") || message.includes("Not a git repository");
15243
- }
15244
- function isPermissionDenied(message) {
15245
- return message.includes("Permission denied") || message.includes("EACCES");
15246
- }
15247
- function isEmptyRepo(stderr) {
15248
- 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");
15249
- }
15250
- function classifyError(errMessage) {
15251
- if (isGitNotInstalled(errMessage)) {
15252
- return { status: "error", message: "git is not installed or not in PATH" };
15253
- }
15254
- if (isNotAGitRepo(errMessage)) {
15255
- return { status: "not_found" };
15256
- }
15257
- if (isPermissionDenied(errMessage)) {
15258
- return { status: "error", message: `Permission denied: ${errMessage}` };
15259
- }
15260
- return { status: "error", message: errMessage.trim() };
15261
- }
15262
- async function isGitRepo(workingDir) {
15263
- const result = await runGit("rev-parse --git-dir", workingDir);
15264
- if ("error" in result)
15265
- return false;
15266
- return result.stdout.trim().length > 0;
15267
- }
15268
- async function getBranch(workingDir) {
15269
- const result = await runGit("rev-parse --abbrev-ref HEAD", workingDir);
15270
- if ("error" in result) {
15271
- const errMsg = result.error.message;
15272
- if (errMsg.includes("unknown revision") || errMsg.includes("No such file or directory") || errMsg.includes("does not have any commits")) {
15273
- return { status: "available", branch: "HEAD" };
15274
- }
15275
- return classifyError(errMsg);
15276
- }
15277
- const branch = result.stdout.trim();
15278
- if (!branch) {
15279
- return { status: "error", message: "git rev-parse returned empty output" };
15280
- }
15281
- return { status: "available", branch };
15282
- }
15283
- async function isDirty(workingDir) {
15284
- const result = await runGit("status --porcelain", workingDir);
15285
- if ("error" in result)
15286
- return false;
15287
- return result.stdout.trim().length > 0;
15288
- }
15289
- function parseDiffStatLine(statLine) {
15290
- const filesMatch = statLine.match(/(\d+)\s+file/);
15291
- const insertMatch = statLine.match(/(\d+)\s+insertion/);
15292
- const deleteMatch = statLine.match(/(\d+)\s+deletion/);
15293
- return {
15294
- filesChanged: filesMatch ? parseInt(filesMatch[1], 10) : 0,
15295
- insertions: insertMatch ? parseInt(insertMatch[1], 10) : 0,
15296
- deletions: deleteMatch ? parseInt(deleteMatch[1], 10) : 0
15297
- };
15298
- }
15299
- async function getDiffStat(workingDir) {
15300
- const result = await runGit("diff HEAD --stat", workingDir);
15301
- if ("error" in result) {
15302
- const errMsg = result.error.message;
15303
- if (isEmptyRepo(result.error.message)) {
15304
- return { status: "no_commits" };
15305
- }
15306
- const classified = classifyError(errMsg);
15307
- if (classified.status === "not_found")
15308
- return { status: "not_found" };
15309
- return classified;
15310
- }
15311
- const output = result.stdout;
15312
- const stderr = result.stderr;
15313
- if (isEmptyRepo(stderr)) {
15314
- return { status: "no_commits" };
15315
- }
15316
- if (!output.trim()) {
15317
- return {
15318
- status: "available",
15319
- diffStat: { filesChanged: 0, insertions: 0, deletions: 0 }
15320
- };
15321
- }
15322
- const lines = output.trim().split(`
15323
- `);
15324
- const summaryLine = lines[lines.length - 1] ?? "";
15325
- const diffStat = parseDiffStatLine(summaryLine);
15326
- return { status: "available", diffStat };
15327
- }
15328
- async function getFullDiff(workingDir) {
15329
- const result = await runGit("diff HEAD", workingDir);
15330
- if ("error" in result) {
15331
- const errMsg = result.error.message;
15332
- if (isEmptyRepo(errMsg)) {
15333
- return { status: "no_commits" };
15334
- }
15335
- const classified = classifyError(errMsg);
15336
- if (classified.status === "not_found")
15337
- return { status: "not_found" };
15338
- return classified;
15339
- }
15340
- const stderr = result.stderr;
15341
- if (isEmptyRepo(stderr)) {
15342
- return { status: "no_commits" };
15343
- }
15344
- const raw = result.stdout;
15345
- const byteLength2 = Buffer.byteLength(raw, "utf8");
15346
- if (byteLength2 > FULL_DIFF_MAX_BYTES) {
15347
- const truncated = Buffer.from(raw, "utf8").subarray(0, FULL_DIFF_MAX_BYTES).toString("utf8");
15348
- return { status: "truncated", content: truncated, truncated: true };
15349
- }
15350
- return { status: "available", content: raw, truncated: false };
15351
- }
15352
- async function getRecentCommits(workingDir, count = 20, skip = 0) {
15353
- const format = "%H%x00%h%x00%s%x00%an%x00%aI";
15354
- const skipArg = skip > 0 ? ` --skip=${skip}` : "";
15355
- const result = await runGit(`log -${count}${skipArg} --format=${format}`, workingDir);
15356
- if ("error" in result) {
15357
- return [];
15358
- }
15359
- const output = result.stdout.trim();
15360
- if (!output)
15361
- return [];
15362
- const commits = [];
15363
- for (const line of output.split(`
15364
- `)) {
15365
- const trimmed = line.trim();
15366
- if (!trimmed)
15367
- continue;
15368
- const parts = trimmed.split("\x00");
15369
- if (parts.length !== 5)
15370
- continue;
15371
- const [sha, shortSha, message, author, date] = parts;
15372
- commits.push({ sha, shortSha, message, author, date });
15373
- }
15374
- return commits;
15375
- }
15376
- async function getCommitDetail(workingDir, sha) {
15377
- const result = await runGit(`show ${sha} --format="" --stat -p`, workingDir);
15378
- if ("error" in result) {
15379
- const errMsg = result.error.message;
15380
- const classified = classifyError(errMsg);
15381
- if (classified.status === "not_found")
15382
- return { status: "not_found" };
15383
- if (isEmptyRepo(errMsg) || errMsg.includes("unknown revision") || errMsg.includes("bad object") || errMsg.includes("does not exist")) {
15384
- return { status: "not_found" };
15385
- }
15386
- return classified;
15387
- }
15388
- const raw = result.stdout;
15389
- const byteLength2 = Buffer.byteLength(raw, "utf8");
15390
- if (byteLength2 > FULL_DIFF_MAX_BYTES) {
15391
- const truncated = Buffer.from(raw, "utf8").subarray(0, FULL_DIFF_MAX_BYTES).toString("utf8");
15392
- return { status: "truncated", content: truncated, truncated: true };
15393
- }
15394
- return { status: "available", content: raw, truncated: false };
15395
- }
15396
- async function getCommitMetadata(workingDir, sha) {
15397
- const format = "%s%x00%an%x00%aI";
15398
- const result = await runGit(`log -1 --format=${format} ${sha}`, workingDir);
15399
- if ("error" in result)
15400
- return null;
15401
- const output = result.stdout.trim();
15402
- if (!output)
15403
- return null;
15404
- const parts = output.split("\x00");
15405
- if (parts.length !== 3)
15406
- return null;
15407
- return { message: parts[0], author: parts[1], date: parts[2] };
15408
- }
15409
- var execAsync2;
15410
- var init_git_reader = __esm(() => {
15411
- execAsync2 = promisify2(exec2);
15412
- });
15413
-
15414
- // src/commands/machine/daemon-start/git-polling.ts
15415
- function startGitPollingLoop(ctx) {
15416
- const timer = setInterval(() => {
15417
- runPollingTick(ctx).catch((err) => {
15418
- console.warn(`[${formatTimestamp()}] ⚠️ Git polling tick failed: ${err.message}`);
15419
- });
15420
- }, GIT_POLLING_INTERVAL_MS);
15421
- timer.unref();
15422
- console.log(`[${formatTimestamp()}] \uD83D\uDD00 Git polling loop started (interval: ${GIT_POLLING_INTERVAL_MS}ms)`);
15423
- return {
15424
- stop: () => {
15425
- clearInterval(timer);
15426
- console.log(`[${formatTimestamp()}] \uD83D\uDD00 Git polling loop stopped`);
15427
- }
15428
- };
15429
- }
15430
- function extractDiffStatFromShowOutput(content) {
15431
- for (const line of content.split(`
15432
- `)) {
15433
- if (/\d+\s+file.*changed/.test(line)) {
15434
- 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}`);
15435
15051
  }
15436
15052
  }
15437
- return { filesChanged: 0, insertions: 0, deletions: 0 };
15438
15053
  }
15439
- async function processFullDiff(ctx, req) {
15440
- const result = await getFullDiff(req.workingDir);
15441
- if (result.status === "available" || result.status === "truncated") {
15442
- const diffStatResult = await getDiffStat(req.workingDir);
15443
- const diffStat = diffStatResult.status === "available" ? diffStatResult.diffStat : { filesChanged: 0, insertions: 0, deletions: 0 };
15444
- 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, {
15445
15062
  sessionId: ctx.sessionId,
15446
15063
  machineId: ctx.machineId,
15447
- workingDir: req.workingDir,
15448
- diffContent: result.content,
15449
- truncated: result.truncated,
15450
- diffStat
15064
+ workingDir,
15065
+ status: "not_found"
15451
15066
  });
15452
- console.log(`[${formatTimestamp()}] \uD83D\uDCC4 Full diff pushed: ${req.workingDir} (${diffStat.filesChanged} files, ${result.truncated ? "truncated" : "complete"})`);
15453
- } else {
15454
- 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, {
15455
15081
  sessionId: ctx.sessionId,
15456
15082
  machineId: ctx.machineId,
15457
- workingDir: req.workingDir,
15458
- diffContent: "",
15459
- truncated: false,
15460
- diffStat: { filesChanged: 0, insertions: 0, deletions: 0 }
15083
+ workingDir,
15084
+ status: "error",
15085
+ errorMessage: branchResult.message
15461
15086
  });
15462
- 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;
15463
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
+ });
15464
15119
  }
15465
- async function processCommitDetail(ctx, req) {
15466
- if (!req.sha) {
15467
- 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
+ }
15468
15139
  }
15469
- const [result, metadata] = await Promise.all([
15470
- getCommitDetail(req.workingDir, req.sha),
15471
- getCommitMetadata(req.workingDir, req.sha)
15472
- ]);
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);
15473
15144
  if (result.status === "not_found") {
15474
15145
  await ctx.deps.backend.mutation(api.workspaces.upsertCommitDetail, {
15475
15146
  sessionId: ctx.sessionId,
15476
15147
  machineId: ctx.machineId,
15477
- workingDir: req.workingDir,
15478
- sha: req.sha,
15148
+ workingDir,
15149
+ sha,
15479
15150
  status: "not_found",
15480
15151
  message: metadata?.message,
15481
15152
  author: metadata?.author,
@@ -15487,8 +15158,8 @@ async function processCommitDetail(ctx, req) {
15487
15158
  await ctx.deps.backend.mutation(api.workspaces.upsertCommitDetail, {
15488
15159
  sessionId: ctx.sessionId,
15489
15160
  machineId: ctx.machineId,
15490
- workingDir: req.workingDir,
15491
- sha: req.sha,
15161
+ workingDir,
15162
+ sha,
15492
15163
  status: "error",
15493
15164
  errorMessage: result.message,
15494
15165
  message: metadata?.message,
@@ -15501,8 +15172,8 @@ async function processCommitDetail(ctx, req) {
15501
15172
  await ctx.deps.backend.mutation(api.workspaces.upsertCommitDetail, {
15502
15173
  sessionId: ctx.sessionId,
15503
15174
  machineId: ctx.machineId,
15504
- workingDir: req.workingDir,
15505
- sha: req.sha,
15175
+ workingDir,
15176
+ sha,
15506
15177
  status: "available",
15507
15178
  diffContent: result.content,
15508
15179
  truncated: result.truncated,
@@ -15511,371 +15182,876 @@ async function processCommitDetail(ctx, req) {
15511
15182
  date: metadata?.date,
15512
15183
  diffStat
15513
15184
  });
15514
- 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}`);
15515
15186
  }
15516
- async function processMoreCommits(ctx, req) {
15517
- const offset = req.offset ?? 0;
15518
- const commits = await getRecentCommits(req.workingDir, COMMITS_PER_PAGE, offset);
15519
- const hasMoreCommits = commits.length >= COMMITS_PER_PAGE;
15520
- await ctx.deps.backend.mutation(api.workspaces.appendMoreCommits, {
15521
- sessionId: ctx.sessionId,
15522
- machineId: ctx.machineId,
15523
- workingDir: req.workingDir,
15524
- commits,
15525
- hasMoreCommits
15526
- });
15527
- 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 };
15528
15197
  }
15529
- async function runPollingTick(ctx) {
15530
- const requests = await ctx.deps.backend.query(api.workspaces.getPendingRequests, {
15531
- sessionId: ctx.sessionId,
15532
- machineId: ctx.machineId
15533
- });
15534
- 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`);
15535
15205
  return;
15536
- 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) {
15537
15210
  try {
15538
- await ctx.deps.backend.mutation(api.workspaces.updateRequestStatus, {
15211
+ const configsResult = await ctx.deps.backend.query(api.machines.getMachineAgentConfigs, {
15539
15212
  sessionId: ctx.sessionId,
15540
- requestId: req._id,
15541
- status: "processing"
15213
+ chatroomId
15542
15214
  });
15543
- switch (req.requestType) {
15544
- case "full_diff":
15545
- await processFullDiff(ctx, req);
15546
- break;
15547
- case "commit_detail":
15548
- await processCommitDetail(ctx, req);
15549
- break;
15550
- case "more_commits":
15551
- await processMoreCommits(ctx, req);
15552
- 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
+ }
15553
15229
  }
15554
- await ctx.deps.backend.mutation(api.workspaces.updateRequestStatus, {
15555
- sessionId: ctx.sessionId,
15556
- requestId: req._id,
15557
- 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()
15558
15350
  });
15559
- } catch (err) {
15560
- console.warn(`[${formatTimestamp()}] ⚠️ Failed to process ${req.requestType} request: ${err.message}`);
15561
- await ctx.deps.backend.mutation(api.workspaces.updateRequestStatus, {
15562
- sessionId: ctx.sessionId,
15563
- requestId: req._id,
15564
- status: "error"
15565
- }).catch(() => {});
15566
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;
15567
15403
  }
15568
15404
  }
15569
- var GIT_POLLING_INTERVAL_MS = 5000;
15570
- var init_git_polling = __esm(() => {
15571
- init_api3();
15572
- 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();
15573
15410
  });
15574
15411
 
15575
- // src/commands/machine/daemon-start/git-heartbeat.ts
15576
- import { createHash as createHash2 } from "node:crypto";
15577
- async function pushGitState(ctx) {
15578
- if (ctx.activeWorkingDirs.size === 0)
15579
- return;
15580
- for (const workingDir of ctx.activeWorkingDirs) {
15581
- try {
15582
- await pushSingleWorkspaceGitState(ctx, workingDir);
15583
- } catch (err) {
15584
- console.warn(`[${formatTimestamp()}] ⚠️ Git state push failed for ${workingDir}: ${err.message}`);
15585
- }
15586
- }
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";
15587
15451
  }
15588
- async function pushSingleWorkspaceGitState(ctx, workingDir) {
15589
- const stateKey = makeGitStateKey(ctx.machineId, workingDir);
15590
- const isRepo = await isGitRepo(workingDir);
15591
- if (!isRepo) {
15592
- const stateHash2 = "not_found";
15593
- 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) {
15594
15508
  return;
15595
- await ctx.deps.backend.mutation(api.workspaces.upsertWorkspaceGitState, {
15596
- sessionId: ctx.sessionId,
15597
- machineId: ctx.machineId,
15598
- workingDir,
15599
- 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}`);
15600
15537
  });
15601
- ctx.lastPushedGitState.set(stateKey, stateHash2);
15602
- return;
15603
- }
15604
- const [branchResult, dirtyResult, diffStatResult, commits] = await Promise.all([
15605
- getBranch(workingDir),
15606
- isDirty(workingDir),
15607
- getDiffStat(workingDir),
15608
- getRecentCommits(workingDir, COMMITS_PER_PAGE)
15609
- ]);
15610
- if (branchResult.status === "error") {
15611
- const stateHash2 = `error:${branchResult.message}`;
15612
- 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);
15613
15546
  return;
15614
- await ctx.deps.backend.mutation(api.workspaces.upsertWorkspaceGitState, {
15615
- sessionId: ctx.sessionId,
15616
- 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,
15617
15560
  workingDir,
15618
- status: "error",
15619
- 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
+ });
15620
15573
  });
15621
- ctx.lastPushedGitState.set(stateKey, stateHash2);
15622
- return;
15623
15574
  }
15624
- if (branchResult.status === "not_found") {
15625
- return;
15575
+ getSlot(chatroomId, role) {
15576
+ return this.slots.get(agentKey2(chatroomId, role));
15626
15577
  }
15627
- const branch = branchResult.branch;
15628
- const isDirty2 = dirtyResult;
15629
- const diffStat = diffStatResult.status === "available" ? diffStatResult.diffStat : { filesChanged: 0, insertions: 0, deletions: 0 };
15630
- const hasMoreCommits = commits.length >= COMMITS_PER_PAGE;
15631
- const stateHash = createHash2("md5").update(JSON.stringify({ branch, isDirty: isDirty2, diffStat, shas: commits.map((c) => c.sha) })).digest("hex");
15632
- if (ctx.lastPushedGitState.get(stateKey) === stateHash) {
15633
- 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;
15634
15587
  }
15635
- await ctx.deps.backend.mutation(api.workspaces.upsertWorkspaceGitState, {
15636
- sessionId: ctx.sessionId,
15637
- machineId: ctx.machineId,
15638
- workingDir,
15639
- status: "available",
15640
- branch,
15641
- isDirty: isDirty2,
15642
- diffStat,
15643
- recentCommits: commits,
15644
- hasMoreCommits
15645
- });
15646
- ctx.lastPushedGitState.set(stateKey, stateHash);
15647
- console.log(`[${formatTimestamp()}] \uD83D\uDD00 Git state pushed: ${workingDir} (${branch}${isDirty2 ? ", dirty" : ", clean"})`);
15648
- prefetchMissingCommitDetails(ctx, workingDir, commits).catch((err) => {
15649
- console.warn(`[${formatTimestamp()}] ⚠️ Commit pre-fetch failed for ${workingDir}: ${err.message}`);
15650
- });
15651
- }
15652
- async function prefetchMissingCommitDetails(ctx, workingDir, commits) {
15653
- if (commits.length === 0)
15654
- return;
15655
- const shas = commits.map((c) => c.sha);
15656
- const missingShas = await ctx.deps.backend.query(api.workspaces.getMissingCommitShas, {
15657
- sessionId: ctx.sessionId,
15658
- machineId: ctx.machineId,
15659
- workingDir,
15660
- shas
15661
- });
15662
- if (missingShas.length === 0)
15663
- return;
15664
- console.log(`[${formatTimestamp()}] \uD83D\uDD0D Pre-fetching ${missingShas.length} commit(s) for ${workingDir}`);
15665
- for (const sha of missingShas) {
15666
- try {
15667
- await prefetchSingleCommit(ctx, workingDir, sha, commits);
15668
- } catch (err) {
15669
- 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
+ }
15670
15612
  }
15613
+ console.log(`[AgentProcessManager] Recovery: ${recovered} alive, ${cleaned} cleaned up`);
15671
15614
  }
15672
- }
15673
- async function prefetchSingleCommit(ctx, workingDir, sha, commits) {
15674
- const metadata = commits.find((c) => c.sha === sha);
15675
- const result = await getCommitDetail(workingDir, sha);
15676
- if (result.status === "not_found") {
15677
- await ctx.deps.backend.mutation(api.workspaces.upsertCommitDetail, {
15678
- sessionId: ctx.sessionId,
15679
- machineId: ctx.machineId,
15680
- workingDir,
15681
- sha,
15682
- status: "not_found",
15683
- message: metadata?.message,
15684
- author: metadata?.author,
15685
- date: metadata?.date
15686
- });
15687
- 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;
15688
15622
  }
15689
- if (result.status === "error") {
15690
- await ctx.deps.backend.mutation(api.workspaces.upsertCommitDetail, {
15691
- sessionId: ctx.sessionId,
15692
- machineId: ctx.machineId,
15693
- workingDir,
15694
- sha,
15695
- status: "error",
15696
- errorMessage: result.message,
15697
- message: metadata?.message,
15698
- author: metadata?.author,
15699
- date: metadata?.date
15700
- });
15701
- 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 };
15702
15823
  }
15703
- const diffStat = extractDiffStatFromShowOutput(result.content);
15704
- await ctx.deps.backend.mutation(api.workspaces.upsertCommitDetail, {
15705
- sessionId: ctx.sessionId,
15706
- machineId: ctx.machineId,
15707
- workingDir,
15708
- sha,
15709
- status: "available",
15710
- diffContent: result.content,
15711
- truncated: result.truncated,
15712
- message: metadata?.message,
15713
- author: metadata?.author,
15714
- date: metadata?.date,
15715
- diffStat
15716
- });
15717
- console.log(`[${formatTimestamp()}] ✅ Pre-fetched: ${sha.slice(0, 7)} in ${workingDir}`);
15718
15824
  }
15719
- var init_git_heartbeat = __esm(() => {
15825
+ var init_agent_process_manager = __esm(() => {
15720
15826
  init_api3();
15721
- init_git_reader();
15722
- init_git_polling();
15723
15827
  });
15724
15828
 
15725
- // src/infrastructure/services/remote-agents/harness-restart-policy.ts
15726
- class OpenCodeRestartPolicy {
15727
- id = "opencode";
15728
- shouldStartAgent(params) {
15729
- const { task, agentConfig } = params;
15730
- if (agentConfig.desiredState !== "running") {
15731
- return false;
15732
- }
15733
- if (agentConfig.circuitState === "open") {
15734
- return false;
15735
- }
15736
- if (task.status === "in_progress") {
15737
- return agentConfig.spawnedAgentPid == null;
15738
- }
15739
- if (task.status === "pending" || task.status === "acknowledged") {
15740
- 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
+ }
15741
15840
  }
15742
- return false;
15743
15841
  }
15842
+ return results;
15744
15843
  }
15745
-
15746
- class PiRestartPolicy {
15747
- id = "pi";
15748
- shouldStartAgent(params, context) {
15749
- const { task, agentConfig } = params;
15750
- if (agentConfig.desiredState !== "running") {
15751
- return false;
15752
- }
15753
- if (agentConfig.circuitState === "open") {
15754
- return false;
15755
- }
15756
- if (task.status === "in_progress") {
15757
- return agentConfig.spawnedAgentPid == null;
15758
- }
15759
- if (task.status === "pending" || task.status === "acknowledged") {
15760
- if (agentConfig.spawnedAgentPid == null) {
15761
- 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}`);
15762
15885
  }
15763
15886
  }
15764
- if (task.status !== "pending" && task.status !== "acknowledged") {
15765
- return false;
15766
- }
15767
- if (!context?.pendingStops) {
15768
- return false;
15769
- }
15770
- const key = `${task.chatroomId}:${agentConfig.role.toLowerCase()}`;
15771
- const pendingReason = context.pendingStops.get(key);
15772
- return pendingReason === "agent_process.turn_end";
15887
+ console.error(`
15888
+ Run: chatroom auth login`);
15889
+ releaseLock();
15890
+ process.exit(1);
15773
15891
  }
15892
+ return sessionId;
15774
15893
  }
15775
- function getRestartPolicyForHarness(harness) {
15776
- switch (harness) {
15777
- case "opencode":
15778
- return new OpenCodeRestartPolicy;
15779
- case "pi":
15780
- return new PiRestartPolicy;
15781
- default:
15782
- return {
15783
- id: harness,
15784
- shouldStartAgent: () => false
15785
- };
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);
15786
15902
  }
15787
15903
  }
15788
-
15789
- // src/commands/machine/daemon-start/task-monitor.ts
15790
- function canAttemptRestart(chatroomId, role, now) {
15791
- const key = `${chatroomId}:${role.toLowerCase()}`;
15792
- const lastAttempt = lastRestartAttempt.get(key) ?? 0;
15793
- return now - lastAttempt >= RESTART_COOLDOWN_MS;
15794
- }
15795
- function recordRestartAttempt(chatroomId, role, now) {
15796
- const key = `${chatroomId}:${role.toLowerCase()}`;
15797
- lastRestartAttempt.set(key, now);
15798
- }
15799
- function startTaskMonitor(ctx) {
15800
- let unsubscribe = null;
15801
- let isRunning = true;
15802
- const startMonitoring = async () => {
15803
- try {
15804
- const wsClient2 = await getConvexWsClient();
15805
- const agentEndContext = {
15806
- pendingStops: ctx.pendingStops
15807
- };
15808
- unsubscribe = wsClient2.onUpdate(api.machines.getAssignedTasks, {
15809
- sessionId: ctx.sessionId,
15810
- machineId: ctx.machineId
15811
- }, async (result) => {
15812
- if (!result?.tasks || result.tasks.length === 0)
15813
- return;
15814
- const now = Date.now();
15815
- for (const taskInfo of result.tasks) {
15816
- const { task, agentConfig } = {
15817
- task: {
15818
- taskId: taskInfo.taskId,
15819
- chatroomId: taskInfo.chatroomId,
15820
- status: taskInfo.status,
15821
- assignedTo: taskInfo.assignedTo,
15822
- updatedAt: taskInfo.updatedAt,
15823
- createdAt: taskInfo.createdAt
15824
- },
15825
- agentConfig: taskInfo.agentConfig
15826
- };
15827
- const policy = getRestartPolicyForHarness(agentConfig.agentHarness);
15828
- const shouldStart = policy.shouldStartAgent({ task, agentConfig }, agentEndContext);
15829
- if (!shouldStart)
15830
- continue;
15831
- if (!canAttemptRestart(task.chatroomId, agentConfig.role, now)) {
15832
- continue;
15833
- }
15834
- if (!agentConfig.workingDir) {
15835
- console.warn(`[${formatTimestamp()}] ⚠️ Missing workingDir for ${task.chatroomId}/${agentConfig.role}` + ` — skipping`);
15836
- continue;
15837
- }
15838
- console.log(`[${formatTimestamp()}] \uD83D\uDCE1 Task monitor: starting agent for ` + `${task.chatroomId}/${agentConfig.role} (harness: ${agentConfig.agentHarness})`);
15839
- recordRestartAttempt(task.chatroomId, agentConfig.role, now);
15840
- try {
15841
- await executeStartAgent(ctx, {
15842
- chatroomId: task.chatroomId,
15843
- role: agentConfig.role,
15844
- agentHarness: agentConfig.agentHarness,
15845
- model: agentConfig.model,
15846
- workingDir: agentConfig.workingDir,
15847
- reason: "daemon.task_monitor"
15848
- });
15849
- } catch (err) {
15850
- console.error(`[${formatTimestamp()}] ❌ Task monitor failed to start agent ` + `for ${task.chatroomId}/${agentConfig.role}: ${err.message}`);
15851
- }
15852
- }
15853
- });
15854
- console.log(`[${formatTimestamp()}] \uD83D\uDD0D Task monitor started`);
15855
- } catch (err) {
15856
- if (isRunning) {
15857
- console.error(`[${formatTimestamp()}] ❌ Task monitor error: ${err.message}`);
15858
- setTimeout(startMonitoring, 5000);
15859
- }
15860
- }
15861
- };
15862
- startMonitoring();
15863
- return {
15864
- stop: () => {
15865
- isRunning = false;
15866
- if (unsubscribe) {
15867
- unsubscribe();
15868
- unsubscribe = null;
15869
- }
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}`);
15870
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
15871
16004
  };
16005
+ registerEventListeners(ctx);
16006
+ logStartup(ctx, availableModels);
16007
+ await recoverState(ctx);
16008
+ return ctx;
15872
16009
  }
15873
- var RESTART_COOLDOWN_MS = 60000, lastRestartAttempt;
15874
- var init_task_monitor = __esm(() => {
16010
+ var init_init2 = __esm(() => {
16011
+ init_state_recovery();
15875
16012
  init_api3();
16013
+ init_register_listeners();
16014
+ init_storage();
15876
16015
  init_client2();
15877
- init_start_agent();
15878
- 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();
15879
16055
  });
15880
16056
 
15881
16057
  // src/commands/machine/daemon-start/command-loop.ts
@@ -15964,15 +16140,14 @@ async function startCommandLoop(ctx) {
15964
16140
  });
15965
16141
  }, DAEMON_HEARTBEAT_INTERVAL_MS);
15966
16142
  heartbeatTimer.unref();
15967
- const gitPollingHandle = startGitPollingLoop(ctx);
15968
- const taskMonitorHandle = startTaskMonitor(ctx);
16143
+ let gitSubscriptionHandle = null;
15969
16144
  pushGitState(ctx).catch(() => {});
15970
16145
  const shutdown = async () => {
15971
16146
  console.log(`
15972
16147
  [${formatTimestamp()}] Shutting down...`);
15973
16148
  clearInterval(heartbeatTimer);
15974
- gitPollingHandle.stop();
15975
- taskMonitorHandle.stop();
16149
+ if (gitSubscriptionHandle)
16150
+ gitSubscriptionHandle.stop();
15976
16151
  await onDaemonShutdown(ctx);
15977
16152
  releaseLock();
15978
16153
  process.exit(0);
@@ -15981,6 +16156,7 @@ async function startCommandLoop(ctx) {
15981
16156
  process.on("SIGTERM", shutdown);
15982
16157
  process.on("SIGHUP", shutdown);
15983
16158
  const wsClient2 = await getConvexWsClient();
16159
+ gitSubscriptionHandle = startGitRequestSubscription(ctx, wsClient2);
15984
16160
  console.log(`
15985
16161
  Listening for commands...`);
15986
16162
  console.log(`Press Ctrl+C to stop
@@ -16014,17 +16190,16 @@ Listening for commands...`);
16014
16190
  }
16015
16191
  var MODEL_REFRESH_INTERVAL_MS;
16016
16192
  var init_command_loop = __esm(() => {
16017
- init_api3();
16018
- init_client2();
16019
- init_machine();
16020
- init_on_daemon_shutdown();
16021
- init_init2();
16022
16193
  init_on_request_start_agent();
16023
16194
  init_on_request_stop_agent();
16024
16195
  init_pid();
16025
- init_git_polling();
16026
16196
  init_git_heartbeat();
16027
- init_task_monitor();
16197
+ init_git_subscription();
16198
+ init_init2();
16199
+ init_api3();
16200
+ init_on_daemon_shutdown();
16201
+ init_client2();
16202
+ init_machine();
16028
16203
  MODEL_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
16029
16204
  });
16030
16205
 
@@ -16036,8 +16211,6 @@ async function daemonStart() {
16036
16211
  var init_daemon_start = __esm(() => {
16037
16212
  init_command_loop();
16038
16213
  init_init2();
16039
- init_start_agent();
16040
- init_stop_agent();
16041
16214
  init_state_recovery();
16042
16215
  });
16043
16216
 
@@ -16695,7 +16868,7 @@ program2.command("task-started").description("[LEGACY] Acknowledge a task and op
16695
16868
  noClassify: skipClassification
16696
16869
  });
16697
16870
  });
16698
- 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) => {
16699
16872
  await maybeRequireAuth();
16700
16873
  const validClassifications = ["question", "new_feature", "follow_up"];
16701
16874
  if (!validClassifications.includes(options.originMessageClassification)) {
@@ -16781,12 +16954,14 @@ program2.command("report-progress").description("Report progress on current task
16781
16954
  });
16782
16955
  });
16783
16956
  var backlogCommand = program2.command("backlog").description("Manage task queue and backlog");
16784
- 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) => {
16785
16958
  await maybeRequireAuth();
16786
16959
  const { listBacklog: listBacklog2 } = await Promise.resolve().then(() => (init_backlog(), exports_backlog));
16787
16960
  await listBacklog2(options.chatroomId, {
16788
16961
  role: options.role,
16789
- 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
16790
16965
  });
16791
16966
  });
16792
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) => {
@@ -16836,6 +17011,21 @@ backlogCommand.command("history").description("View completed and closed backlog
16836
17011
  limit: options.limit ? parseInt(options.limit, 10) : undefined
16837
17012
  });
16838
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
+ });
16839
17029
  var taskCommand = program2.command("task").description("Manage tasks");
16840
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) => {
16841
17031
  await maybeRequireAuth();