chatroom-cli 1.2.3 → 1.2.7

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 +314 -396
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -10371,6 +10371,20 @@ function listAgentEntries(machineId) {
10371
10371
  }
10372
10372
  return results;
10373
10373
  }
10374
+ function persistEventCursor(machineId, lastSeenEventId) {
10375
+ try {
10376
+ const state = loadOrCreate(machineId);
10377
+ state.lastSeenEventId = lastSeenEventId;
10378
+ state.updatedAt = new Date().toISOString();
10379
+ saveDaemonState(state);
10380
+ } catch (err) {
10381
+ console.warn(`⚠️ Failed to persist event cursor: ${err.message}`);
10382
+ }
10383
+ }
10384
+ function loadEventCursor(machineId) {
10385
+ const state = loadDaemonState(machineId);
10386
+ return state?.lastSeenEventId ?? null;
10387
+ }
10374
10388
  var CHATROOM_DIR3, STATE_DIR, STATE_VERSION = "1";
10375
10389
  var init_daemon_state = __esm(() => {
10376
10390
  CHATROOM_DIR3 = join4(homedir3(), ".chatroom");
@@ -11284,22 +11298,9 @@ async function registerAgent(chatroomId, options, deps) {
11284
11298
  harnessVersions: machineInfo.harnessVersions,
11285
11299
  availableModels
11286
11300
  });
11287
- const agentHarness = machineInfo.availableHarnesses.length > 0 ? machineInfo.availableHarnesses[0] : undefined;
11288
- await d.backend.mutation(api.machines.saveTeamAgentConfig, {
11289
- sessionId,
11290
- chatroomId,
11291
- role,
11292
- type: "remote",
11293
- machineId: machineInfo.machineId,
11294
- agentHarness,
11295
- workingDir: process.cwd()
11296
- });
11297
11301
  console.log(`✅ Registered as remote agent for role "${role}"`);
11298
11302
  console.log(` Machine: ${machineInfo.hostname} (${machineInfo.machineId})`);
11299
11303
  console.log(` Working directory: ${process.cwd()}`);
11300
- if (agentHarness) {
11301
- console.log(` Agent harness: ${agentHarness}`);
11302
- }
11303
11304
  } catch (error) {
11304
11305
  console.error(`❌ Registration failed: ${error.message}`);
11305
11306
  process.exit(1);
@@ -11327,7 +11328,7 @@ var init_register_agent = __esm(() => {
11327
11328
  init_opencode();
11328
11329
  init_pi();
11329
11330
  });
11330
- // ../../services/backend/prompts/base/cli/task-started/command.ts
11331
+ // ../../services/backend/prompts/cli/task-started/command.ts
11331
11332
  function taskStartedCommand(params) {
11332
11333
  const prefix = params.cliEnvPrefix || "";
11333
11334
  const chatroomId = params.chatroomId || "<chatroom-id>";
@@ -11351,22 +11352,22 @@ EOF`;
11351
11352
  return baseCmd;
11352
11353
  }
11353
11354
 
11354
- // ../../services/backend/prompts/base/cli/task-started/classification/new-feature.ts
11355
+ // ../../services/backend/prompts/cli/task-started/classification/new-feature.ts
11355
11356
  var init_new_feature = () => {};
11356
- // ../../services/backend/prompts/base/cli/task-started/classification/index.ts
11357
+ // ../../services/backend/prompts/cli/task-started/classification/index.ts
11357
11358
  var init_classification = __esm(() => {
11358
11359
  init_new_feature();
11359
11360
  });
11360
- // ../../services/backend/prompts/base/cli/task-started/main-prompt.ts
11361
+ // ../../services/backend/prompts/cli/task-started/main-prompt.ts
11361
11362
  var init_main_prompt = () => {};
11362
11363
 
11363
- // ../../services/backend/prompts/base/cli/task-started/index.ts
11364
+ // ../../services/backend/prompts/cli/task-started/index.ts
11364
11365
  var init_task_started = __esm(() => {
11365
11366
  init_classification();
11366
11367
  init_main_prompt();
11367
11368
  });
11368
11369
 
11369
- // ../../services/backend/prompts/base/cli/get-next-task/reminder.ts
11370
+ // ../../services/backend/prompts/cli/get-next-task/reminder.ts
11370
11371
  function getNextTaskGuidance() {
11371
11372
  return `\uD83D\uDD17 STAYING CONNECTED TO YOUR TEAM
11372
11373
 
@@ -11455,7 +11456,7 @@ var init_getting_started_content = __esm(() => {
11455
11456
  init_utils();
11456
11457
  });
11457
11458
 
11458
- // ../../services/backend/prompts/base/cli/index.ts
11459
+ // ../../services/backend/prompts/cli/index.ts
11459
11460
  var init_cli = __esm(() => {
11460
11461
  init_task_started();
11461
11462
  init_reminder();
@@ -11481,7 +11482,7 @@ var init_errorCodes = __esm(() => {
11481
11482
  ];
11482
11483
  });
11483
11484
 
11484
- // ../../services/backend/prompts/base/cli/get-next-task/command.ts
11485
+ // ../../services/backend/prompts/cli/get-next-task/command.ts
11485
11486
  function getNextTaskCommand(params) {
11486
11487
  const prefix = params.cliEnvPrefix || "";
11487
11488
  const chatroomId = params.chatroomId || "<chatroom-id>";
@@ -12182,7 +12183,7 @@ function formatDecodeError(error) {
12182
12183
  return message;
12183
12184
  }
12184
12185
 
12185
- // ../../services/backend/prompts/base/cli/handoff/command.ts
12186
+ // ../../services/backend/prompts/cli/handoff/command.ts
12186
12187
  function handoffCommand(params) {
12187
12188
  const prefix = params.cliEnvPrefix || "";
12188
12189
  const chatroomId = params.chatroomId || "<chatroom-id>";
@@ -13656,6 +13657,7 @@ async function getSystemPrompt(chatroomId, options, deps) {
13656
13657
  const prompt = await d.backend.query(api.prompts.webapp.getAgentPrompt, {
13657
13658
  chatroomId,
13658
13659
  role,
13660
+ teamId: chatroom.teamId,
13659
13661
  teamName: chatroom.teamName,
13660
13662
  teamRoles: chatroom.teamRoles,
13661
13663
  teamEntryPoint: chatroom.teamEntryPoint,
@@ -13676,56 +13678,14 @@ var init_get_system_prompt = __esm(() => {
13676
13678
  });
13677
13679
 
13678
13680
  // ../../services/backend/config/reliability.ts
13679
- var DAEMON_HEARTBEAT_INTERVAL_MS = 30000;
13681
+ var DAEMON_HEARTBEAT_INTERVAL_MS = 30000, AGENT_REQUEST_DEADLINE_MS = 120000;
13680
13682
 
13681
13683
  // src/commands/machine/daemon-start/utils.ts
13682
13684
  function formatTimestamp() {
13683
13685
  return new Date().toISOString().replace("T", " ").substring(0, 19);
13684
13686
  }
13685
- function parseMachineCommand(raw) {
13686
- switch (raw.type) {
13687
- case "ping":
13688
- return { _id: raw._id, type: "ping", payload: {}, createdAt: raw.createdAt };
13689
- case "status":
13690
- return { _id: raw._id, type: "status", payload: {}, createdAt: raw.createdAt };
13691
- case "start-agent": {
13692
- const { chatroomId, role, agentHarness } = raw.payload;
13693
- if (!chatroomId || !role || !agentHarness) {
13694
- console.error(` ⚠️ Invalid start-agent command: missing chatroomId, role, or agentHarness`);
13695
- return null;
13696
- }
13697
- return {
13698
- _id: raw._id,
13699
- type: "start-agent",
13700
- payload: {
13701
- chatroomId,
13702
- role,
13703
- agentHarness,
13704
- model: raw.payload.model,
13705
- workingDir: raw.payload.workingDir
13706
- },
13707
- createdAt: raw.createdAt
13708
- };
13709
- }
13710
- case "stop-agent": {
13711
- const { chatroomId, role } = raw.payload;
13712
- if (!chatroomId || !role) {
13713
- console.error(` ⚠️ Invalid stop-agent command: missing chatroomId or role`);
13714
- return null;
13715
- }
13716
- return {
13717
- _id: raw._id,
13718
- type: "stop-agent",
13719
- payload: { chatroomId, role },
13720
- createdAt: raw.createdAt
13721
- };
13722
- }
13723
- default:
13724
- return null;
13725
- }
13726
- }
13727
13687
 
13728
- // src/commands/machine/events/on-agent-shutdown/index.ts
13688
+ // src/events/lifecycle/on-agent-shutdown.ts
13729
13689
  async function onAgentShutdown(ctx, options) {
13730
13690
  const { chatroomId, role, pid, skipKill } = options;
13731
13691
  try {
@@ -13784,42 +13744,13 @@ async function onAgentShutdown(ctx, options) {
13784
13744
  console.log(` ⚠️ Failed to clear local PID for ${role}: ${e.message}`);
13785
13745
  }
13786
13746
  }
13787
- let spawnedAgentCleared = false;
13788
- if (killed || skipKill) {
13789
- try {
13790
- await ctx.deps.backend.mutation(api.machines.updateSpawnedAgent, {
13791
- sessionId: ctx.sessionId,
13792
- machineId: ctx.machineId,
13793
- chatroomId,
13794
- role,
13795
- pid: undefined
13796
- });
13797
- spawnedAgentCleared = true;
13798
- } catch (e) {
13799
- console.log(` ⚠️ Failed to clear spawnedAgent for ${role}: ${e.message}`);
13800
- }
13801
- }
13802
- let participantRemoved = false;
13803
- try {
13804
- await ctx.deps.backend.mutation(api.participants.leave, {
13805
- sessionId: ctx.sessionId,
13806
- chatroomId,
13807
- role
13808
- });
13809
- participantRemoved = true;
13810
- } catch (e) {
13811
- console.log(` ⚠️ Failed to remove participant for ${role}: ${e.message}`);
13812
- }
13813
13747
  return {
13814
13748
  killed: killed || (skipKill ?? false),
13815
- cleaned: spawnedAgentCleared && participantRemoved
13749
+ cleaned: killed || (skipKill ?? false)
13816
13750
  };
13817
13751
  }
13818
- var init_on_agent_shutdown = __esm(() => {
13819
- init_api3();
13820
- });
13821
13752
 
13822
- // src/commands/machine/events/on-daemon-shutdown/index.ts
13753
+ // src/events/lifecycle/on-daemon-shutdown.ts
13823
13754
  async function onDaemonShutdown(ctx) {
13824
13755
  const agents = ctx.deps.machine.listAgentEntries(ctx.machineId);
13825
13756
  if (agents.length > 0) {
@@ -13858,108 +13789,18 @@ async function onDaemonShutdown(ctx) {
13858
13789
  var AGENT_SHUTDOWN_TIMEOUT_MS = 5000;
13859
13790
  var init_on_daemon_shutdown = __esm(() => {
13860
13791
  init_api3();
13861
- init_on_agent_shutdown();
13862
13792
  });
13863
13793
 
13864
- // src/commands/machine/pid.ts
13865
- import { createHash } from "node:crypto";
13866
- import { existsSync as existsSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4, unlinkSync as unlinkSync2, mkdirSync as mkdirSync4 } from "node:fs";
13867
- import { homedir as homedir4 } from "node:os";
13868
- import { join as join5 } from "node:path";
13869
- function getUrlHash() {
13870
- const url = getConvexUrl();
13871
- return createHash("sha256").update(url).digest("hex").substring(0, 8);
13872
- }
13873
- function getPidFileName() {
13874
- return `daemon-${getUrlHash()}.pid`;
13875
- }
13876
- function ensureChatroomDir() {
13877
- if (!existsSync4(CHATROOM_DIR4)) {
13878
- mkdirSync4(CHATROOM_DIR4, { recursive: true, mode: 448 });
13879
- }
13880
- }
13881
- function getPidFilePath() {
13882
- return join5(CHATROOM_DIR4, getPidFileName());
13883
- }
13884
- function isProcessRunning(pid) {
13885
- try {
13886
- process.kill(pid, 0);
13887
- return true;
13888
- } catch {
13889
- return false;
13890
- }
13891
- }
13892
- function readPid() {
13893
- const pidPath = getPidFilePath();
13894
- if (!existsSync4(pidPath)) {
13895
- return null;
13896
- }
13897
- try {
13898
- const content = readFileSync6(pidPath, "utf-8").trim();
13899
- const pid = parseInt(content, 10);
13900
- if (isNaN(pid) || pid <= 0) {
13901
- return null;
13902
- }
13903
- return pid;
13904
- } catch {
13905
- return null;
13906
- }
13907
- }
13908
- function writePid() {
13909
- ensureChatroomDir();
13910
- const pidPath = getPidFilePath();
13911
- writeFileSync4(pidPath, process.pid.toString(), "utf-8");
13912
- }
13913
- function removePid() {
13914
- const pidPath = getPidFilePath();
13915
- try {
13916
- if (existsSync4(pidPath)) {
13917
- unlinkSync2(pidPath);
13918
- }
13919
- } catch {}
13920
- }
13921
- function isDaemonRunning() {
13922
- const pid = readPid();
13923
- if (pid === null) {
13924
- return { running: false, pid: null };
13925
- }
13926
- if (isProcessRunning(pid)) {
13927
- return { running: true, pid };
13928
- }
13929
- removePid();
13930
- return { running: false, pid: null };
13931
- }
13932
- function acquireLock() {
13933
- const { running, pid } = isDaemonRunning();
13934
- if (running) {
13935
- console.error(`❌ Daemon already running for ${getConvexUrl()} (PID: ${pid})`);
13936
- return false;
13937
- }
13938
- writePid();
13939
- return true;
13940
- }
13941
- function releaseLock() {
13942
- removePid();
13943
- }
13944
- var CHATROOM_DIR4;
13945
- var init_pid = __esm(() => {
13946
- init_client2();
13947
- CHATROOM_DIR4 = join5(homedir4(), ".chatroom");
13948
- });
13949
-
13950
- // src/commands/machine/daemon-start/handlers/ping.ts
13951
- function handlePing() {
13952
- console.log(` ↪ Responding: pong`);
13953
- return { result: "pong", failed: false };
13954
- }
13955
-
13956
13794
  // src/commands/machine/daemon-start/handlers/start-agent.ts
13957
- async function handleStartAgent(ctx, command) {
13958
- const { chatroomId, role, agentHarness, model, workingDir } = command.payload;
13795
+ async function executeStartAgent(ctx, args) {
13796
+ const { chatroomId, role, agentHarness, model, workingDir, reason } = args;
13959
13797
  console.log(` ↪ start-agent command received`);
13960
13798
  console.log(` Chatroom: ${chatroomId}`);
13961
13799
  console.log(` Role: ${role}`);
13962
13800
  console.log(` Harness: ${agentHarness}`);
13801
+ if (reason) {
13802
+ console.log(` Reason: ${reason}`);
13803
+ }
13963
13804
  if (model) {
13964
13805
  console.log(` Model: ${model}`);
13965
13806
  }
@@ -13987,14 +13828,19 @@ async function handleStartAgent(ctx, command) {
13987
13828
  chatroomId
13988
13829
  });
13989
13830
  const existingConfig = existingConfigs.configs.find((c) => c.machineId === ctx.machineId && c.role.toLowerCase() === role.toLowerCase());
13990
- if (existingConfig?.spawnedAgentPid) {
13991
- const existingPid = existingConfig.spawnedAgentPid;
13992
- const anyService = ctx.agentServices.values().next().value;
13993
- const isAlive = anyService ? anyService.isAlive(existingPid) : false;
13831
+ const backendPid = existingConfig?.spawnedAgentPid;
13832
+ const localEntry = ctx.deps.machine.listAgentEntries(ctx.machineId).find((e) => e.chatroomId === chatroomId && e.role.toLowerCase() === role.toLowerCase());
13833
+ const localPid = localEntry?.entry.pid;
13834
+ const pidsToKill = [
13835
+ ...new Set([backendPid, localPid].filter((p) => p !== undefined))
13836
+ ];
13837
+ const anyService = ctx.agentServices.values().next().value;
13838
+ for (const pid2 of pidsToKill) {
13839
+ const isAlive = anyService ? anyService.isAlive(pid2) : false;
13994
13840
  if (isAlive) {
13995
- console.log(` ⚠️ Existing agent detected (PID: ${existingPid}) — stopping before respawn`);
13996
- await onAgentShutdown(ctx, { chatroomId, role, pid: existingPid });
13997
- console.log(` ✅ Existing agent stopped`);
13841
+ console.log(` ⚠️ Existing agent detected (PID: ${pid2}) — stopping before respawn`);
13842
+ await onAgentShutdown(ctx, { chatroomId, role, pid: pid2 });
13843
+ console.log(` ✅ Existing agent stopped (PID: ${pid2})`);
13998
13844
  }
13999
13845
  }
14000
13846
  } catch (e) {
@@ -14085,19 +13931,26 @@ async function handleStartAgent(ctx, command) {
14085
13931
  var init_start_agent = __esm(() => {
14086
13932
  init_api3();
14087
13933
  init_client2();
14088
- init_on_agent_shutdown();
14089
13934
  });
14090
13935
 
14091
- // src/commands/machine/daemon-start/handlers/status.ts
14092
- function handleStatus(ctx) {
14093
- const result = JSON.stringify({
14094
- hostname: ctx.config?.hostname,
14095
- os: ctx.config?.os,
14096
- availableHarnesses: ctx.config?.availableHarnesses
13936
+ // src/events/daemon/agent/on-request-start-agent.ts
13937
+ async function onRequestStartAgent(ctx, event) {
13938
+ if (Date.now() > event.deadline) {
13939
+ console.log(`[daemon] ⏰ Skipping expired agent.requestStart for role=${event.role} (deadline passed)`);
13940
+ return;
13941
+ }
13942
+ await executeStartAgent(ctx, {
13943
+ chatroomId: event.chatroomId,
13944
+ role: event.role,
13945
+ agentHarness: event.agentHarness,
13946
+ model: event.model,
13947
+ workingDir: event.workingDir,
13948
+ reason: event.reason
14097
13949
  });
14098
- console.log(` ↪ Responding with status`);
14099
- return { result, failed: false };
14100
13950
  }
13951
+ var init_on_request_start_agent = __esm(() => {
13952
+ init_start_agent();
13953
+ });
14101
13954
 
14102
13955
  // src/commands/machine/daemon-start/handlers/shared.ts
14103
13956
  async function clearAgentPidEverywhere(ctx, chatroomId, role) {
@@ -14119,8 +13972,8 @@ var init_shared = __esm(() => {
14119
13972
  });
14120
13973
 
14121
13974
  // src/commands/machine/daemon-start/handlers/stop-agent.ts
14122
- async function handleStopAgent(ctx, command) {
14123
- const { chatroomId, role } = command.payload;
13975
+ async function executeStopAgent(ctx, args) {
13976
+ const { chatroomId, role } = args;
14124
13977
  console.log(` ↪ stop-agent command received`);
14125
13978
  console.log(` Chatroom: ${chatroomId}`);
14126
13979
  console.log(` Role: ${role}`);
@@ -14129,53 +13982,178 @@ async function handleStopAgent(ctx, command) {
14129
13982
  chatroomId
14130
13983
  });
14131
13984
  const targetConfig = configsResult.configs.find((c) => c.machineId === ctx.machineId && c.role.toLowerCase() === role.toLowerCase());
14132
- if (!targetConfig?.spawnedAgentPid) {
13985
+ const backendPid = targetConfig?.spawnedAgentPid;
13986
+ const localEntry = ctx.deps.machine.listAgentEntries(ctx.machineId).find((e) => e.chatroomId === chatroomId && e.role.toLowerCase() === role.toLowerCase());
13987
+ const localPid = localEntry?.entry.pid;
13988
+ const allPids = [...new Set([backendPid, localPid].filter((p) => p !== undefined))];
13989
+ if (allPids.length === 0) {
14133
13990
  const msg = "No running agent found (no PID recorded)";
14134
13991
  console.log(` ⚠️ ${msg}`);
14135
13992
  return { result: msg, failed: true };
14136
13993
  }
14137
- const pidToKill = targetConfig.spawnedAgentPid;
14138
- console.log(` Stopping agent with PID: ${pidToKill}`);
14139
13994
  const anyService = ctx.agentServices.values().next().value;
14140
- const isAlive = anyService ? anyService.isAlive(pidToKill) : false;
14141
- if (!isAlive) {
14142
- console.log(` ⚠️ PID ${pidToKill} does not appear to belong to the expected agent`);
14143
- await clearAgentPidEverywhere(ctx, chatroomId, role);
14144
- console.log(` Cleared stale PID`);
13995
+ let anyKilled = false;
13996
+ let lastError = null;
13997
+ for (const pid of allPids) {
13998
+ console.log(` Stopping agent with PID: ${pid}`);
13999
+ const isAlive = anyService ? anyService.isAlive(pid) : false;
14000
+ if (!isAlive) {
14001
+ console.log(` ⚠️ PID ${pid} not found — process already exited or was never started`);
14002
+ await clearAgentPidEverywhere(ctx, chatroomId, role);
14003
+ console.log(` Cleared stale PID`);
14004
+ try {
14005
+ await ctx.deps.backend.mutation(api.participants.leave, {
14006
+ sessionId: ctx.sessionId,
14007
+ chatroomId,
14008
+ role
14009
+ });
14010
+ console.log(` Removed participant record`);
14011
+ } catch {}
14012
+ continue;
14013
+ }
14145
14014
  try {
14146
- await ctx.deps.backend.mutation(api.participants.leave, {
14147
- sessionId: ctx.sessionId,
14015
+ const shutdownResult = await onAgentShutdown(ctx, {
14148
14016
  chatroomId,
14149
- role
14017
+ role,
14018
+ pid
14150
14019
  });
14151
- console.log(` Removed participant record`);
14152
- } catch {}
14153
- return {
14154
- result: `PID ${pidToKill} appears stale (process not found or belongs to different program)`,
14155
- failed: true
14156
- };
14020
+ const msg = shutdownResult.killed ? `Agent stopped (PID: ${pid})` : `Agent stop attempted (PID: ${pid}) — process may still be running`;
14021
+ console.log(` ${shutdownResult.killed ? "✅" : "⚠️ "} ${msg}`);
14022
+ if (shutdownResult.killed) {
14023
+ anyKilled = true;
14024
+ }
14025
+ } catch (e) {
14026
+ lastError = e;
14027
+ console.log(` ⚠️ Failed to stop agent (PID: ${pid}): ${e.message}`);
14028
+ }
14157
14029
  }
14158
- try {
14159
- const shutdownResult = await onAgentShutdown(ctx, {
14160
- chatroomId,
14161
- role,
14162
- pid: pidToKill
14163
- });
14164
- const msg = shutdownResult.killed ? `Agent stopped (PID: ${pidToKill})` : `Agent stop attempted (PID: ${pidToKill}) — process may still be running`;
14165
- console.log(` ${shutdownResult.killed ? "✅" : "⚠️ "} ${msg}`);
14166
- return { result: msg, failed: !shutdownResult.killed };
14167
- } catch (e) {
14168
- const msg = `Failed to stop agent: ${e.message}`;
14030
+ if (lastError && !anyKilled) {
14031
+ const msg = `Failed to stop agent: ${lastError.message}`;
14169
14032
  console.log(` ⚠️ ${msg}`);
14170
14033
  return { result: msg, failed: true };
14171
14034
  }
14035
+ if (!anyKilled) {
14036
+ return {
14037
+ result: `All recorded PIDs appear stale (processes not found or belong to different programs)`,
14038
+ failed: true
14039
+ };
14040
+ }
14041
+ const killedCount = allPids.length > 1 ? ` (${allPids.length} PIDs)` : ``;
14042
+ return { result: `Agent stopped${killedCount}`, failed: false };
14172
14043
  }
14173
14044
  var init_stop_agent = __esm(() => {
14174
14045
  init_api3();
14175
- init_on_agent_shutdown();
14176
14046
  init_shared();
14177
14047
  });
14178
14048
 
14049
+ // src/events/daemon/agent/on-request-stop-agent.ts
14050
+ async function onRequestStopAgent(ctx, event) {
14051
+ if (Date.now() > event.deadline) {
14052
+ console.log(`[daemon] ⏰ Skipping expired agent.requestStop for role=${event.role} (deadline passed)`);
14053
+ return;
14054
+ }
14055
+ await executeStopAgent(ctx, {
14056
+ chatroomId: event.chatroomId,
14057
+ role: event.role,
14058
+ reason: event.reason
14059
+ });
14060
+ }
14061
+ var init_on_request_stop_agent = __esm(() => {
14062
+ init_stop_agent();
14063
+ });
14064
+
14065
+ // src/commands/machine/pid.ts
14066
+ import { createHash } from "node:crypto";
14067
+ import { existsSync as existsSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4, unlinkSync as unlinkSync2, mkdirSync as mkdirSync4 } from "node:fs";
14068
+ import { homedir as homedir4 } from "node:os";
14069
+ import { join as join5 } from "node:path";
14070
+ function getUrlHash() {
14071
+ const url = getConvexUrl();
14072
+ return createHash("sha256").update(url).digest("hex").substring(0, 8);
14073
+ }
14074
+ function getPidFileName() {
14075
+ return `daemon-${getUrlHash()}.pid`;
14076
+ }
14077
+ function ensureChatroomDir() {
14078
+ if (!existsSync4(CHATROOM_DIR4)) {
14079
+ mkdirSync4(CHATROOM_DIR4, { recursive: true, mode: 448 });
14080
+ }
14081
+ }
14082
+ function getPidFilePath() {
14083
+ return join5(CHATROOM_DIR4, getPidFileName());
14084
+ }
14085
+ function isProcessRunning(pid) {
14086
+ try {
14087
+ process.kill(pid, 0);
14088
+ return true;
14089
+ } catch {
14090
+ return false;
14091
+ }
14092
+ }
14093
+ function readPid() {
14094
+ const pidPath = getPidFilePath();
14095
+ if (!existsSync4(pidPath)) {
14096
+ return null;
14097
+ }
14098
+ try {
14099
+ const content = readFileSync6(pidPath, "utf-8").trim();
14100
+ const pid = parseInt(content, 10);
14101
+ if (isNaN(pid) || pid <= 0) {
14102
+ return null;
14103
+ }
14104
+ return pid;
14105
+ } catch {
14106
+ return null;
14107
+ }
14108
+ }
14109
+ function writePid() {
14110
+ ensureChatroomDir();
14111
+ const pidPath = getPidFilePath();
14112
+ writeFileSync4(pidPath, process.pid.toString(), "utf-8");
14113
+ }
14114
+ function removePid() {
14115
+ const pidPath = getPidFilePath();
14116
+ try {
14117
+ if (existsSync4(pidPath)) {
14118
+ unlinkSync2(pidPath);
14119
+ }
14120
+ } catch {}
14121
+ }
14122
+ function isDaemonRunning() {
14123
+ const pid = readPid();
14124
+ if (pid === null) {
14125
+ return { running: false, pid: null };
14126
+ }
14127
+ if (isProcessRunning(pid)) {
14128
+ return { running: true, pid };
14129
+ }
14130
+ removePid();
14131
+ return { running: false, pid: null };
14132
+ }
14133
+ function acquireLock() {
14134
+ const { running, pid } = isDaemonRunning();
14135
+ if (running) {
14136
+ console.error(`❌ Daemon already running for ${getConvexUrl()} (PID: ${pid})`);
14137
+ return false;
14138
+ }
14139
+ writePid();
14140
+ return true;
14141
+ }
14142
+ function releaseLock() {
14143
+ removePid();
14144
+ }
14145
+ var CHATROOM_DIR4;
14146
+ var init_pid = __esm(() => {
14147
+ init_client2();
14148
+ CHATROOM_DIR4 = join5(homedir4(), ".chatroom");
14149
+ });
14150
+
14151
+ // src/commands/machine/daemon-start/handlers/ping.ts
14152
+ function handlePing() {
14153
+ console.log(` ↪ Responding: pong`);
14154
+ return { result: "pong", failed: false };
14155
+ }
14156
+
14179
14157
  // src/commands/machine/daemon-start/command-loop.ts
14180
14158
  async function refreshModels(ctx) {
14181
14159
  const models = {};
@@ -14204,70 +14182,6 @@ async function refreshModels(ctx) {
14204
14182
  console.warn(`[${formatTimestamp()}] ⚠️ Model refresh failed: ${error.message}`);
14205
14183
  }
14206
14184
  }
14207
- async function processCommand(ctx, command) {
14208
- console.log(`[${formatTimestamp()}] \uD83D\uDCE8 Command received: ${command.type}`);
14209
- try {
14210
- await ctx.deps.backend.mutation(api.machines.ackCommand, {
14211
- sessionId: ctx.sessionId,
14212
- commandId: command._id,
14213
- status: "processing"
14214
- });
14215
- ctx.events.emit("command:processing", {
14216
- commandId: command._id.toString(),
14217
- type: command.type
14218
- });
14219
- let commandResult;
14220
- switch (command.type) {
14221
- case "ping":
14222
- commandResult = handlePing();
14223
- break;
14224
- case "status":
14225
- commandResult = handleStatus(ctx);
14226
- break;
14227
- case "start-agent":
14228
- commandResult = await handleStartAgent(ctx, command);
14229
- break;
14230
- case "stop-agent":
14231
- commandResult = await handleStopAgent(ctx, command);
14232
- break;
14233
- default: {
14234
- const _exhaustive = command;
14235
- commandResult = {
14236
- result: `Unknown command type: ${_exhaustive.type}`,
14237
- failed: true
14238
- };
14239
- }
14240
- }
14241
- const finalStatus = commandResult.failed ? "failed" : "completed";
14242
- await ctx.deps.backend.mutation(api.machines.ackCommand, {
14243
- sessionId: ctx.sessionId,
14244
- commandId: command._id,
14245
- status: finalStatus,
14246
- result: commandResult.result
14247
- });
14248
- ctx.events.emit("command:completed", {
14249
- commandId: command._id.toString(),
14250
- type: command.type,
14251
- failed: commandResult.failed,
14252
- result: commandResult.result
14253
- });
14254
- if (commandResult.failed) {
14255
- console.log(` ❌ Command failed: ${commandResult.result}`);
14256
- } else {
14257
- console.log(` ✅ Command completed`);
14258
- }
14259
- } catch (error) {
14260
- console.error(` ❌ Command failed: ${error.message}`);
14261
- try {
14262
- await ctx.deps.backend.mutation(api.machines.ackCommand, {
14263
- sessionId: ctx.sessionId,
14264
- commandId: command._id,
14265
- status: "failed",
14266
- result: error.message
14267
- });
14268
- } catch {}
14269
- }
14270
- }
14271
14185
  async function startCommandLoop(ctx) {
14272
14186
  let heartbeatCount = 0;
14273
14187
  const heartbeatTimer = setInterval(() => {
@@ -14294,66 +14208,56 @@ async function startCommandLoop(ctx) {
14294
14208
  process.on("SIGTERM", shutdown);
14295
14209
  process.on("SIGHUP", shutdown);
14296
14210
  const wsClient2 = await getConvexWsClient();
14297
- const commandQueue = [];
14298
- const queuedCommandIds = new Set;
14299
- let drainingQueue = false;
14300
- const enqueueCommands = (commands) => {
14301
- for (const command of commands) {
14302
- const commandId = command._id.toString();
14303
- if (queuedCommandIds.has(commandId))
14304
- continue;
14305
- queuedCommandIds.add(commandId);
14306
- commandQueue.push(command);
14307
- }
14308
- };
14309
- const drainQueue = async () => {
14310
- if (drainingQueue)
14311
- return;
14312
- drainingQueue = true;
14313
- try {
14314
- while (commandQueue.length > 0) {
14315
- const command = commandQueue.shift();
14316
- const commandId = command._id.toString();
14317
- queuedCommandIds.delete(commandId);
14318
- try {
14319
- await processCommand(ctx, command);
14320
- } catch (error) {
14321
- console.error(` ❌ Command processing failed: ${error.message}`);
14322
- }
14323
- }
14324
- } finally {
14325
- drainingQueue = false;
14326
- }
14327
- };
14328
14211
  console.log(`
14329
14212
  Listening for commands...`);
14330
14213
  console.log(`Press Ctrl+C to stop
14331
14214
  `);
14332
- wsClient2.onUpdate(api.machines.getPendingCommands, {
14215
+ const processedCommandIds = new Map;
14216
+ const processedPingIds = new Map;
14217
+ wsClient2.onUpdate(api.machines.getCommandEvents, {
14333
14218
  sessionId: ctx.sessionId,
14334
14219
  machineId: ctx.machineId
14335
14220
  }, async (result) => {
14336
- if (!result.commands || result.commands.length === 0)
14221
+ if (!result.events || result.events.length === 0)
14337
14222
  return;
14338
- const parsed = [];
14339
- for (const raw of result.commands) {
14340
- const command = parseMachineCommand(raw);
14341
- if (command !== null) {
14342
- parsed.push(command);
14343
- } else {
14344
- try {
14345
- await ctx.deps.backend.mutation(api.machines.ackCommand, {
14223
+ const evictBefore = Date.now() - AGENT_REQUEST_DEADLINE_MS;
14224
+ for (const [id, ts] of processedCommandIds) {
14225
+ if (ts < evictBefore)
14226
+ processedCommandIds.delete(id);
14227
+ }
14228
+ for (const [id, ts] of processedPingIds) {
14229
+ if (ts < evictBefore)
14230
+ processedPingIds.delete(id);
14231
+ }
14232
+ for (const event of result.events) {
14233
+ const eventId = event._id.toString();
14234
+ try {
14235
+ console.log(`[${formatTimestamp()}] \uD83D\uDCE1 Stream command event: ${event.type}`);
14236
+ if (event.type === "agent.requestStart") {
14237
+ if (processedCommandIds.has(eventId))
14238
+ continue;
14239
+ processedCommandIds.set(eventId, Date.now());
14240
+ await onRequestStartAgent(ctx, event);
14241
+ } else if (event.type === "agent.requestStop") {
14242
+ if (processedCommandIds.has(eventId))
14243
+ continue;
14244
+ processedCommandIds.set(eventId, Date.now());
14245
+ await onRequestStopAgent(ctx, event);
14246
+ } else if (event.type === "daemon.ping") {
14247
+ if (processedPingIds.has(eventId))
14248
+ continue;
14249
+ processedPingIds.set(eventId, Date.now());
14250
+ handlePing();
14251
+ await ctx.deps.backend.mutation(api.machines.ackPing, {
14346
14252
  sessionId: ctx.sessionId,
14347
- commandId: raw._id,
14348
- status: "failed",
14349
- result: `Invalid command: type="${raw.type}" missing required payload fields`
14253
+ machineId: ctx.machineId,
14254
+ pingEventId: event._id
14350
14255
  });
14351
- console.warn(`[${formatTimestamp()}] ⚠️ Acked invalid command ${raw._id} (type=${raw.type}) as failed`);
14352
- } catch {}
14256
+ }
14257
+ } catch (err) {
14258
+ console.error(`[${formatTimestamp()}] ❌ Stream command event failed: ${err.message}`);
14353
14259
  }
14354
14260
  }
14355
- enqueueCommands(parsed);
14356
- await drainQueue();
14357
14261
  });
14358
14262
  const modelRefreshTimer = setInterval(() => {
14359
14263
  refreshModels(ctx).catch((err) => {
@@ -14368,9 +14272,9 @@ var init_command_loop = __esm(() => {
14368
14272
  init_api3();
14369
14273
  init_client2();
14370
14274
  init_on_daemon_shutdown();
14275
+ init_on_request_start_agent();
14276
+ init_on_request_stop_agent();
14371
14277
  init_pid();
14372
- init_start_agent();
14373
- init_stop_agent();
14374
14278
  MODEL_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
14375
14279
  });
14376
14280
 
@@ -14402,7 +14306,7 @@ var init_state_recovery = __esm(() => {
14402
14306
  init_shared();
14403
14307
  });
14404
14308
 
14405
- // src/commands/machine/daemon-start/event-bus.ts
14309
+ // src/events/daemon/event-bus.ts
14406
14310
  class DaemonEventBus {
14407
14311
  listeners = new Map;
14408
14312
  on(event, listener) {
@@ -14431,54 +14335,66 @@ class DaemonEventBus {
14431
14335
  }
14432
14336
  }
14433
14337
 
14434
- // src/commands/machine/daemon-start/event-listeners.ts
14338
+ // src/events/daemon/agent/on-agent-exited.ts
14339
+ function onAgentExited(ctx, payload) {
14340
+ const { chatroomId, role, pid, code: code2, signal, intentional } = payload;
14341
+ const ts = formatTimestamp();
14342
+ if (intentional) {
14343
+ console.log(`[${ts}] ℹ️ Agent process exited after intentional stop ` + `(PID: ${pid}, role: ${role}, code: ${code2}, signal: ${signal})`);
14344
+ } else {
14345
+ console.log(`[${ts}] ⚠️ Agent process exited ` + `(PID: ${pid}, role: ${role}, code: ${code2}, signal: ${signal})`);
14346
+ }
14347
+ ctx.deps.backend.mutation(api.machines.recordAgentExited, {
14348
+ sessionId: ctx.sessionId,
14349
+ machineId: ctx.machineId,
14350
+ chatroomId,
14351
+ role,
14352
+ pid,
14353
+ intentional,
14354
+ exitCode: code2 ?? undefined,
14355
+ signal: signal ?? undefined
14356
+ }).catch((err) => {
14357
+ console.log(` ⚠️ Failed to record agent exit event: ${err.message}`);
14358
+ });
14359
+ ctx.deps.machine.clearAgentPid(ctx.machineId, chatroomId, role);
14360
+ for (const service of ctx.agentServices.values()) {
14361
+ service.untrack(pid);
14362
+ }
14363
+ }
14364
+ var init_on_agent_exited = __esm(() => {
14365
+ init_api3();
14366
+ });
14367
+
14368
+ // src/events/daemon/agent/on-agent-started.ts
14369
+ function onAgentStarted(ctx, payload) {
14370
+ const ts = formatTimestamp();
14371
+ console.log(`[${ts}] \uD83D\uDFE2 Agent started: ${payload.role} (PID: ${payload.pid}, harness: ${payload.harness})`);
14372
+ }
14373
+ var init_on_agent_started = () => {};
14374
+
14375
+ // src/events/daemon/agent/on-agent-stopped.ts
14376
+ function onAgentStopped(ctx, payload) {
14377
+ const ts = formatTimestamp();
14378
+ console.log(`[${ts}] \uD83D\uDD34 Agent stopped: ${payload.role} (PID: ${payload.pid})`);
14379
+ }
14380
+ var init_on_agent_stopped = () => {};
14381
+
14382
+ // src/events/daemon/register-listeners.ts
14435
14383
  function registerEventListeners(ctx) {
14436
14384
  const unsubs = [];
14437
- unsubs.push(ctx.events.on("agent:exited", (payload) => {
14438
- const { chatroomId, role, pid, code: code2, signal, intentional } = payload;
14439
- const ts = formatTimestamp();
14440
- if (intentional) {
14441
- console.log(`[${ts}] ℹ️ Agent process exited after intentional stop ` + `(PID: ${pid}, role: ${role}, code: ${code2}, signal: ${signal})`);
14442
- } else {
14443
- console.log(`[${ts}] ⚠️ Agent process exited ` + `(PID: ${pid}, role: ${role}, code: ${code2}, signal: ${signal})`);
14444
- }
14445
- ctx.deps.backend.mutation(api.machines.updateSpawnedAgent, {
14446
- sessionId: ctx.sessionId,
14447
- machineId: ctx.machineId,
14448
- chatroomId,
14449
- role,
14450
- pid: undefined
14451
- }).catch((err) => {
14452
- console.log(` ⚠️ Failed to clear PID in backend: ${err.message}`);
14453
- });
14454
- ctx.deps.machine.clearAgentPid(ctx.machineId, chatroomId, role);
14455
- for (const service of ctx.agentServices.values()) {
14456
- service.untrack(pid);
14457
- }
14458
- ctx.deps.backend.mutation(api.participants.leave, {
14459
- sessionId: ctx.sessionId,
14460
- chatroomId,
14461
- role
14462
- }).catch((err) => {
14463
- console.log(` ⚠️ Could not remove participant: ${err.message}`);
14464
- });
14465
- }));
14466
- unsubs.push(ctx.events.on("agent:started", (payload) => {
14467
- const ts = formatTimestamp();
14468
- console.log(`[${ts}] \uD83D\uDFE2 Agent started: ${payload.role} (PID: ${payload.pid}, harness: ${payload.harness})`);
14469
- }));
14470
- unsubs.push(ctx.events.on("agent:stopped", (payload) => {
14471
- const ts = formatTimestamp();
14472
- console.log(`[${ts}] \uD83D\uDD34 Agent stopped: ${payload.role} (PID: ${payload.pid})`);
14473
- }));
14385
+ unsubs.push(ctx.events.on("agent:exited", (payload) => onAgentExited(ctx, payload)));
14386
+ unsubs.push(ctx.events.on("agent:started", (payload) => onAgentStarted(ctx, payload)));
14387
+ unsubs.push(ctx.events.on("agent:stopped", (payload) => onAgentStopped(ctx, payload)));
14474
14388
  return () => {
14475
14389
  for (const unsub of unsubs) {
14476
14390
  unsub();
14477
14391
  }
14478
14392
  };
14479
14393
  }
14480
- var init_event_listeners = __esm(() => {
14481
- init_api3();
14394
+ var init_register_listeners = __esm(() => {
14395
+ init_on_agent_exited();
14396
+ init_on_agent_started();
14397
+ init_on_agent_stopped();
14482
14398
  });
14483
14399
 
14484
14400
  // src/commands/machine/daemon-start/init.ts
@@ -14520,7 +14436,9 @@ function createDefaultDeps16() {
14520
14436
  machine: {
14521
14437
  clearAgentPid,
14522
14438
  persistAgentPid,
14523
- listAgentEntries
14439
+ listAgentEntries,
14440
+ persistEventCursor,
14441
+ loadEventCursor
14524
14442
  },
14525
14443
  clock: {
14526
14444
  now: () => Date.now(),
@@ -14649,7 +14567,7 @@ var init_init2 = __esm(() => {
14649
14567
  init_error_formatting();
14650
14568
  init_version();
14651
14569
  init_pid();
14652
- init_event_listeners();
14570
+ init_register_listeners();
14653
14571
  });
14654
14572
 
14655
14573
  // src/commands/machine/daemon-start/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatroom-cli",
3
- "version": "1.2.3",
3
+ "version": "1.2.7",
4
4
  "description": "CLI for multi-agent chatroom collaboration",
5
5
  "type": "module",
6
6
  "bin": {