chatroom-cli 1.2.4 → 1.3.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 +285 -389
  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");
@@ -11314,7 +11328,7 @@ var init_register_agent = __esm(() => {
11314
11328
  init_opencode();
11315
11329
  init_pi();
11316
11330
  });
11317
- // ../../services/backend/prompts/base/cli/task-started/command.ts
11331
+ // ../../services/backend/prompts/cli/task-started/command.ts
11318
11332
  function taskStartedCommand(params) {
11319
11333
  const prefix = params.cliEnvPrefix || "";
11320
11334
  const chatroomId = params.chatroomId || "<chatroom-id>";
@@ -11338,22 +11352,22 @@ EOF`;
11338
11352
  return baseCmd;
11339
11353
  }
11340
11354
 
11341
- // ../../services/backend/prompts/base/cli/task-started/classification/new-feature.ts
11355
+ // ../../services/backend/prompts/cli/task-started/classification/new-feature.ts
11342
11356
  var init_new_feature = () => {};
11343
- // ../../services/backend/prompts/base/cli/task-started/classification/index.ts
11357
+ // ../../services/backend/prompts/cli/task-started/classification/index.ts
11344
11358
  var init_classification = __esm(() => {
11345
11359
  init_new_feature();
11346
11360
  });
11347
- // ../../services/backend/prompts/base/cli/task-started/main-prompt.ts
11361
+ // ../../services/backend/prompts/cli/task-started/main-prompt.ts
11348
11362
  var init_main_prompt = () => {};
11349
11363
 
11350
- // ../../services/backend/prompts/base/cli/task-started/index.ts
11364
+ // ../../services/backend/prompts/cli/task-started/index.ts
11351
11365
  var init_task_started = __esm(() => {
11352
11366
  init_classification();
11353
11367
  init_main_prompt();
11354
11368
  });
11355
11369
 
11356
- // ../../services/backend/prompts/base/cli/get-next-task/reminder.ts
11370
+ // ../../services/backend/prompts/cli/get-next-task/reminder.ts
11357
11371
  function getNextTaskGuidance() {
11358
11372
  return `\uD83D\uDD17 STAYING CONNECTED TO YOUR TEAM
11359
11373
 
@@ -11373,24 +11387,23 @@ Two requirements:
11373
11387
 
11374
11388
  ⚠️ WHEN THE PROCESS IS TERMINATED OR TIMED OUT
11375
11389
 
11376
- \`\`\`
11377
- @startuml
11378
- start
11379
- :Command terminated unexpectedly;
11380
- if (Urgent pending work?) then (yes)
11381
- :Finish urgent work;
11382
- :Reconnect with get-next-task;
11383
- else (no)
11384
- :Reconnect immediately;
11385
- note right: Team cannot reach you without it
11386
- endif
11387
- stop
11388
- @enduml
11390
+ \`\`\`mermaid
11391
+ flowchart TD
11392
+ A([Start]) --> B[Command terminated unexpectedly]
11393
+ B --> C{Urgent pending work?}
11394
+ C -->|yes| D[Finish urgent work]
11395
+ D --> E[Reconnect with get-next-task]
11396
+ C -->|no| E
11397
+ E --> F([Stop])
11389
11398
  \`\`\`
11390
11399
 
11391
11400
  \uD83D\uDCCB BACKLOG TASKS
11392
11401
  chatroom backlog list --chatroom-id=<chatroomId> --role=<role> --status=backlog
11393
- chatroom backlog --help`;
11402
+ chatroom backlog --help
11403
+
11404
+ \uD83D\uDCCB CONTEXT RECOVERY (after compaction/summarization)
11405
+ If your context was compacted, run: chatroom get-system-prompt --chatroom-id=<id> --role=<role>
11406
+ to reload your full system and role prompt.`;
11394
11407
  }
11395
11408
  var init_reminder = () => {};
11396
11409
  // ../../services/backend/prompts/config/index.ts
@@ -11440,9 +11453,10 @@ var init_utils = __esm(() => {
11440
11453
  // ../../services/backend/prompts/base/shared/getting-started-content.ts
11441
11454
  var init_getting_started_content = __esm(() => {
11442
11455
  init_utils();
11456
+ init_reminder();
11443
11457
  });
11444
11458
 
11445
- // ../../services/backend/prompts/base/cli/index.ts
11459
+ // ../../services/backend/prompts/cli/index.ts
11446
11460
  var init_cli = __esm(() => {
11447
11461
  init_task_started();
11448
11462
  init_reminder();
@@ -11468,7 +11482,7 @@ var init_errorCodes = __esm(() => {
11468
11482
  ];
11469
11483
  });
11470
11484
 
11471
- // ../../services/backend/prompts/base/cli/get-next-task/command.ts
11485
+ // ../../services/backend/prompts/cli/get-next-task/command.ts
11472
11486
  function getNextTaskCommand(params) {
11473
11487
  const prefix = params.cliEnvPrefix || "";
11474
11488
  const chatroomId = params.chatroomId || "<chatroom-id>";
@@ -11777,18 +11791,6 @@ async function getNextTask(chatroomId, options) {
11777
11791
  harnessVersions: machineInfo.harnessVersions,
11778
11792
  availableModels
11779
11793
  });
11780
- const agentType = options.agentType ?? (machineInfo.availableHarnesses.length > 0 ? machineInfo.availableHarnesses[0] : undefined);
11781
- if (agentType) {
11782
- const workingDir = process.cwd();
11783
- await client2.mutation(api.machines.updateAgentConfig, {
11784
- sessionId,
11785
- machineId: machineInfo.machineId,
11786
- chatroomId,
11787
- role,
11788
- agentType,
11789
- workingDir
11790
- });
11791
- }
11792
11794
  } catch (machineError) {
11793
11795
  if (!silent) {
11794
11796
  console.warn(`⚠️ Machine registration failed: ${sanitizeUnknownForTerminal(machineError.message)}`);
@@ -12169,7 +12171,7 @@ function formatDecodeError(error) {
12169
12171
  return message;
12170
12172
  }
12171
12173
 
12172
- // ../../services/backend/prompts/base/cli/handoff/command.ts
12174
+ // ../../services/backend/prompts/cli/handoff/command.ts
12173
12175
  function handoffCommand(params) {
12174
12176
  const prefix = params.cliEnvPrefix || "";
12175
12177
  const chatroomId = params.chatroomId || "<chatroom-id>";
@@ -12470,7 +12472,6 @@ async function listBacklog(chatroomId, options, deps) {
12470
12472
  "pending",
12471
12473
  "acknowledged",
12472
12474
  "in_progress",
12473
- "queued",
12474
12475
  "backlog",
12475
12476
  "backlog_acknowledged",
12476
12477
  "completed",
@@ -12831,8 +12832,6 @@ function getStatusEmoji(status) {
12831
12832
  return "\uD83D\uDCEC";
12832
12833
  case "in_progress":
12833
12834
  return "\uD83D\uDD35";
12834
- case "queued":
12835
- return "\uD83D\uDFE1";
12836
12835
  case "backlog":
12837
12836
  return "⚪";
12838
12837
  case "backlog_acknowledged":
@@ -13643,6 +13642,7 @@ async function getSystemPrompt(chatroomId, options, deps) {
13643
13642
  const prompt = await d.backend.query(api.prompts.webapp.getAgentPrompt, {
13644
13643
  chatroomId,
13645
13644
  role,
13645
+ teamId: chatroom.teamId,
13646
13646
  teamName: chatroom.teamName,
13647
13647
  teamRoles: chatroom.teamRoles,
13648
13648
  teamEntryPoint: chatroom.teamEntryPoint,
@@ -13663,66 +13663,14 @@ var init_get_system_prompt = __esm(() => {
13663
13663
  });
13664
13664
 
13665
13665
  // ../../services/backend/config/reliability.ts
13666
- var DAEMON_HEARTBEAT_INTERVAL_MS = 30000;
13666
+ var DAEMON_HEARTBEAT_INTERVAL_MS = 30000, AGENT_REQUEST_DEADLINE_MS = 120000;
13667
13667
 
13668
13668
  // src/commands/machine/daemon-start/utils.ts
13669
13669
  function formatTimestamp() {
13670
13670
  return new Date().toISOString().replace("T", " ").substring(0, 19);
13671
13671
  }
13672
- function parseMachineCommand(raw) {
13673
- switch (raw.type) {
13674
- case "ping":
13675
- return { _id: raw._id, type: "ping", payload: {}, createdAt: raw.createdAt };
13676
- case "status":
13677
- return { _id: raw._id, type: "status", payload: {}, createdAt: raw.createdAt };
13678
- case "start-agent": {
13679
- const { chatroomId, role, agentHarness } = raw.payload;
13680
- if (!chatroomId || !role || !agentHarness) {
13681
- console.error(` ⚠️ Invalid start-agent command: missing chatroomId, role, or agentHarness`);
13682
- return null;
13683
- }
13684
- if (!raw.reason) {
13685
- console.error(` ⚠️ Invalid start-agent command: missing required reason field`);
13686
- return null;
13687
- }
13688
- return {
13689
- _id: raw._id,
13690
- type: "start-agent",
13691
- reason: raw.reason,
13692
- payload: {
13693
- chatroomId,
13694
- role,
13695
- agentHarness,
13696
- model: raw.payload.model,
13697
- workingDir: raw.payload.workingDir
13698
- },
13699
- createdAt: raw.createdAt
13700
- };
13701
- }
13702
- case "stop-agent": {
13703
- const { chatroomId, role } = raw.payload;
13704
- if (!chatroomId || !role) {
13705
- console.error(` ⚠️ Invalid stop-agent command: missing chatroomId or role`);
13706
- return null;
13707
- }
13708
- if (!raw.reason) {
13709
- console.error(` ⚠️ Invalid stop-agent command: missing required reason field`);
13710
- return null;
13711
- }
13712
- return {
13713
- _id: raw._id,
13714
- type: "stop-agent",
13715
- reason: raw.reason,
13716
- payload: { chatroomId, role },
13717
- createdAt: raw.createdAt
13718
- };
13719
- }
13720
- default:
13721
- return null;
13722
- }
13723
- }
13724
13672
 
13725
- // src/commands/machine/events/on-agent-shutdown/index.ts
13673
+ // src/events/lifecycle/on-agent-shutdown.ts
13726
13674
  async function onAgentShutdown(ctx, options) {
13727
13675
  const { chatroomId, role, pid, skipKill } = options;
13728
13676
  try {
@@ -13781,42 +13729,13 @@ async function onAgentShutdown(ctx, options) {
13781
13729
  console.log(` ⚠️ Failed to clear local PID for ${role}: ${e.message}`);
13782
13730
  }
13783
13731
  }
13784
- let spawnedAgentCleared = false;
13785
- if (killed || skipKill) {
13786
- try {
13787
- await ctx.deps.backend.mutation(api.machines.updateSpawnedAgent, {
13788
- sessionId: ctx.sessionId,
13789
- machineId: ctx.machineId,
13790
- chatroomId,
13791
- role,
13792
- pid: undefined
13793
- });
13794
- spawnedAgentCleared = true;
13795
- } catch (e) {
13796
- console.log(` ⚠️ Failed to clear spawnedAgent for ${role}: ${e.message}`);
13797
- }
13798
- }
13799
- let participantRemoved = false;
13800
- try {
13801
- await ctx.deps.backend.mutation(api.participants.leave, {
13802
- sessionId: ctx.sessionId,
13803
- chatroomId,
13804
- role
13805
- });
13806
- participantRemoved = true;
13807
- } catch (e) {
13808
- console.log(` ⚠️ Failed to remove participant for ${role}: ${e.message}`);
13809
- }
13810
13732
  return {
13811
13733
  killed: killed || (skipKill ?? false),
13812
- cleaned: spawnedAgentCleared && participantRemoved
13734
+ cleaned: killed || (skipKill ?? false)
13813
13735
  };
13814
13736
  }
13815
- var init_on_agent_shutdown = __esm(() => {
13816
- init_api3();
13817
- });
13818
13737
 
13819
- // src/commands/machine/events/on-daemon-shutdown/index.ts
13738
+ // src/events/lifecycle/on-daemon-shutdown.ts
13820
13739
  async function onDaemonShutdown(ctx) {
13821
13740
  const agents = ctx.deps.machine.listAgentEntries(ctx.machineId);
13822
13741
  if (agents.length > 0) {
@@ -13855,110 +13774,28 @@ async function onDaemonShutdown(ctx) {
13855
13774
  var AGENT_SHUTDOWN_TIMEOUT_MS = 5000;
13856
13775
  var init_on_daemon_shutdown = __esm(() => {
13857
13776
  init_api3();
13858
- init_on_agent_shutdown();
13859
13777
  });
13860
13778
 
13861
- // src/commands/machine/pid.ts
13862
- import { createHash } from "node:crypto";
13863
- import { existsSync as existsSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4, unlinkSync as unlinkSync2, mkdirSync as mkdirSync4 } from "node:fs";
13864
- import { homedir as homedir4 } from "node:os";
13865
- import { join as join5 } from "node:path";
13866
- function getUrlHash() {
13867
- const url = getConvexUrl();
13868
- return createHash("sha256").update(url).digest("hex").substring(0, 8);
13869
- }
13870
- function getPidFileName() {
13871
- return `daemon-${getUrlHash()}.pid`;
13872
- }
13873
- function ensureChatroomDir() {
13874
- if (!existsSync4(CHATROOM_DIR4)) {
13875
- mkdirSync4(CHATROOM_DIR4, { recursive: true, mode: 448 });
13876
- }
13877
- }
13878
- function getPidFilePath() {
13879
- return join5(CHATROOM_DIR4, getPidFileName());
13880
- }
13881
- function isProcessRunning(pid) {
13882
- try {
13883
- process.kill(pid, 0);
13884
- return true;
13885
- } catch {
13886
- return false;
13887
- }
13888
- }
13889
- function readPid() {
13890
- const pidPath = getPidFilePath();
13891
- if (!existsSync4(pidPath)) {
13892
- return null;
13893
- }
13894
- try {
13895
- const content = readFileSync6(pidPath, "utf-8").trim();
13896
- const pid = parseInt(content, 10);
13897
- if (isNaN(pid) || pid <= 0) {
13898
- return null;
13899
- }
13900
- return pid;
13901
- } catch {
13902
- return null;
13903
- }
13904
- }
13905
- function writePid() {
13906
- ensureChatroomDir();
13907
- const pidPath = getPidFilePath();
13908
- writeFileSync4(pidPath, process.pid.toString(), "utf-8");
13909
- }
13910
- function removePid() {
13911
- const pidPath = getPidFilePath();
13912
- try {
13913
- if (existsSync4(pidPath)) {
13914
- unlinkSync2(pidPath);
13915
- }
13916
- } catch {}
13917
- }
13918
- function isDaemonRunning() {
13919
- const pid = readPid();
13920
- if (pid === null) {
13921
- return { running: false, pid: null };
13922
- }
13923
- if (isProcessRunning(pid)) {
13924
- return { running: true, pid };
13925
- }
13926
- removePid();
13927
- return { running: false, pid: null };
13928
- }
13929
- function acquireLock() {
13930
- const { running, pid } = isDaemonRunning();
13931
- if (running) {
13932
- console.error(`❌ Daemon already running for ${getConvexUrl()} (PID: ${pid})`);
13933
- return false;
13934
- }
13935
- writePid();
13936
- return true;
13937
- }
13938
- function releaseLock() {
13939
- removePid();
13940
- }
13941
- var CHATROOM_DIR4;
13942
- var init_pid = __esm(() => {
13943
- init_client2();
13944
- CHATROOM_DIR4 = join5(homedir4(), ".chatroom");
13945
- });
13946
-
13947
- // src/commands/machine/daemon-start/handlers/ping.ts
13948
- function handlePing() {
13949
- console.log(` ↪ Responding: pong`);
13950
- return { result: "pong", failed: false };
13779
+ // src/infrastructure/machine/stop-reason.ts
13780
+ function resolveStopReason(code2, signal, wasIntentional) {
13781
+ if (wasIntentional)
13782
+ return "intentional_stop";
13783
+ if (signal !== null)
13784
+ return "process_terminated_with_signal";
13785
+ if (code2 === 0)
13786
+ return "process_exited_with_success";
13787
+ return "process_terminated_unexpectedly";
13951
13788
  }
13952
13789
 
13953
13790
  // src/commands/machine/daemon-start/handlers/start-agent.ts
13954
- async function handleStartAgent(ctx, command) {
13955
- const { chatroomId, role, agentHarness, model, workingDir } = command.payload;
13791
+ async function executeStartAgent(ctx, args) {
13792
+ const { chatroomId, role, agentHarness, model, workingDir, reason } = args;
13956
13793
  console.log(` ↪ start-agent command received`);
13957
13794
  console.log(` Chatroom: ${chatroomId}`);
13958
13795
  console.log(` Role: ${role}`);
13959
13796
  console.log(` Harness: ${agentHarness}`);
13960
- if (command.reason) {
13961
- console.log(` Reason: ${command.reason}`);
13797
+ if (reason) {
13798
+ console.log(` Reason: ${reason}`);
13962
13799
  }
13963
13800
  if (model) {
13964
13801
  console.log(` Model: ${model}`);
@@ -14064,12 +13901,14 @@ async function handleStartAgent(ctx, command) {
14064
13901
  });
14065
13902
  spawnResult.onExit(({ code: code2, signal }) => {
14066
13903
  const wasIntentional = ctx.deps.stops.consume(chatroomId, role);
13904
+ const stopReason = resolveStopReason(code2, signal, wasIntentional);
14067
13905
  ctx.events.emit("agent:exited", {
14068
13906
  chatroomId,
14069
13907
  role,
14070
13908
  pid,
14071
13909
  code: code2,
14072
13910
  signal,
13911
+ stopReason,
14073
13912
  intentional: wasIntentional
14074
13913
  });
14075
13914
  });
@@ -14090,19 +13929,26 @@ async function handleStartAgent(ctx, command) {
14090
13929
  var init_start_agent = __esm(() => {
14091
13930
  init_api3();
14092
13931
  init_client2();
14093
- init_on_agent_shutdown();
14094
13932
  });
14095
13933
 
14096
- // src/commands/machine/daemon-start/handlers/status.ts
14097
- function handleStatus(ctx) {
14098
- const result = JSON.stringify({
14099
- hostname: ctx.config?.hostname,
14100
- os: ctx.config?.os,
14101
- availableHarnesses: ctx.config?.availableHarnesses
13934
+ // src/events/daemon/agent/on-request-start-agent.ts
13935
+ async function onRequestStartAgent(ctx, event) {
13936
+ if (Date.now() > event.deadline) {
13937
+ console.log(`[daemon] ⏰ Skipping expired agent.requestStart for role=${event.role} (deadline passed)`);
13938
+ return;
13939
+ }
13940
+ await executeStartAgent(ctx, {
13941
+ chatroomId: event.chatroomId,
13942
+ role: event.role,
13943
+ agentHarness: event.agentHarness,
13944
+ model: event.model,
13945
+ workingDir: event.workingDir,
13946
+ reason: event.reason
14102
13947
  });
14103
- console.log(` ↪ Responding with status`);
14104
- return { result, failed: false };
14105
13948
  }
13949
+ var init_on_request_start_agent = __esm(() => {
13950
+ init_start_agent();
13951
+ });
14106
13952
 
14107
13953
  // src/commands/machine/daemon-start/handlers/shared.ts
14108
13954
  async function clearAgentPidEverywhere(ctx, chatroomId, role) {
@@ -14124,8 +13970,8 @@ var init_shared = __esm(() => {
14124
13970
  });
14125
13971
 
14126
13972
  // src/commands/machine/daemon-start/handlers/stop-agent.ts
14127
- async function handleStopAgent(ctx, command) {
14128
- const { chatroomId, role } = command.payload;
13973
+ async function executeStopAgent(ctx, args) {
13974
+ const { chatroomId, role } = args;
14129
13975
  console.log(` ↪ stop-agent command received`);
14130
13976
  console.log(` Chatroom: ${chatroomId}`);
14131
13977
  console.log(` Role: ${role}`);
@@ -14150,7 +13996,7 @@ async function handleStopAgent(ctx, command) {
14150
13996
  console.log(` Stopping agent with PID: ${pid}`);
14151
13997
  const isAlive = anyService ? anyService.isAlive(pid) : false;
14152
13998
  if (!isAlive) {
14153
- console.log(` ⚠️ PID ${pid} does not appear to belong to the expected agent`);
13999
+ console.log(` ⚠️ PID ${pid} not found process already exited or was never started`);
14154
14000
  await clearAgentPidEverywhere(ctx, chatroomId, role);
14155
14001
  console.log(` Cleared stale PID`);
14156
14002
  try {
@@ -14195,10 +14041,117 @@ async function handleStopAgent(ctx, command) {
14195
14041
  }
14196
14042
  var init_stop_agent = __esm(() => {
14197
14043
  init_api3();
14198
- init_on_agent_shutdown();
14199
14044
  init_shared();
14200
14045
  });
14201
14046
 
14047
+ // src/events/daemon/agent/on-request-stop-agent.ts
14048
+ async function onRequestStopAgent(ctx, event) {
14049
+ if (Date.now() > event.deadline) {
14050
+ console.log(`[daemon] ⏰ Skipping expired agent.requestStop for role=${event.role} (deadline passed)`);
14051
+ return;
14052
+ }
14053
+ await executeStopAgent(ctx, {
14054
+ chatroomId: event.chatroomId,
14055
+ role: event.role,
14056
+ reason: event.reason
14057
+ });
14058
+ }
14059
+ var init_on_request_stop_agent = __esm(() => {
14060
+ init_stop_agent();
14061
+ });
14062
+
14063
+ // src/commands/machine/pid.ts
14064
+ import { createHash } from "node:crypto";
14065
+ import { existsSync as existsSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4, unlinkSync as unlinkSync2, mkdirSync as mkdirSync4 } from "node:fs";
14066
+ import { homedir as homedir4 } from "node:os";
14067
+ import { join as join5 } from "node:path";
14068
+ function getUrlHash() {
14069
+ const url = getConvexUrl();
14070
+ return createHash("sha256").update(url).digest("hex").substring(0, 8);
14071
+ }
14072
+ function getPidFileName() {
14073
+ return `daemon-${getUrlHash()}.pid`;
14074
+ }
14075
+ function ensureChatroomDir() {
14076
+ if (!existsSync4(CHATROOM_DIR4)) {
14077
+ mkdirSync4(CHATROOM_DIR4, { recursive: true, mode: 448 });
14078
+ }
14079
+ }
14080
+ function getPidFilePath() {
14081
+ return join5(CHATROOM_DIR4, getPidFileName());
14082
+ }
14083
+ function isProcessRunning(pid) {
14084
+ try {
14085
+ process.kill(pid, 0);
14086
+ return true;
14087
+ } catch {
14088
+ return false;
14089
+ }
14090
+ }
14091
+ function readPid() {
14092
+ const pidPath = getPidFilePath();
14093
+ if (!existsSync4(pidPath)) {
14094
+ return null;
14095
+ }
14096
+ try {
14097
+ const content = readFileSync6(pidPath, "utf-8").trim();
14098
+ const pid = parseInt(content, 10);
14099
+ if (isNaN(pid) || pid <= 0) {
14100
+ return null;
14101
+ }
14102
+ return pid;
14103
+ } catch {
14104
+ return null;
14105
+ }
14106
+ }
14107
+ function writePid() {
14108
+ ensureChatroomDir();
14109
+ const pidPath = getPidFilePath();
14110
+ writeFileSync4(pidPath, process.pid.toString(), "utf-8");
14111
+ }
14112
+ function removePid() {
14113
+ const pidPath = getPidFilePath();
14114
+ try {
14115
+ if (existsSync4(pidPath)) {
14116
+ unlinkSync2(pidPath);
14117
+ }
14118
+ } catch {}
14119
+ }
14120
+ function isDaemonRunning() {
14121
+ const pid = readPid();
14122
+ if (pid === null) {
14123
+ return { running: false, pid: null };
14124
+ }
14125
+ if (isProcessRunning(pid)) {
14126
+ return { running: true, pid };
14127
+ }
14128
+ removePid();
14129
+ return { running: false, pid: null };
14130
+ }
14131
+ function acquireLock() {
14132
+ const { running, pid } = isDaemonRunning();
14133
+ if (running) {
14134
+ console.error(`❌ Daemon already running for ${getConvexUrl()} (PID: ${pid})`);
14135
+ return false;
14136
+ }
14137
+ writePid();
14138
+ return true;
14139
+ }
14140
+ function releaseLock() {
14141
+ removePid();
14142
+ }
14143
+ var CHATROOM_DIR4;
14144
+ var init_pid = __esm(() => {
14145
+ init_client2();
14146
+ CHATROOM_DIR4 = join5(homedir4(), ".chatroom");
14147
+ });
14148
+
14149
+ // src/commands/machine/daemon-start/handlers/ping.ts
14150
+ function handlePing() {
14151
+ console.log(` ↪ Responding: pong`);
14152
+ return { result: "pong", failed: false };
14153
+ }
14154
+
14202
14155
  // src/commands/machine/daemon-start/command-loop.ts
14203
14156
  async function refreshModels(ctx) {
14204
14157
  const models = {};
@@ -14227,70 +14180,6 @@ async function refreshModels(ctx) {
14227
14180
  console.warn(`[${formatTimestamp()}] ⚠️ Model refresh failed: ${error.message}`);
14228
14181
  }
14229
14182
  }
14230
- async function processCommand(ctx, command) {
14231
- console.log(`[${formatTimestamp()}] \uD83D\uDCE8 Command received: ${command.type}`);
14232
- try {
14233
- await ctx.deps.backend.mutation(api.machines.ackCommand, {
14234
- sessionId: ctx.sessionId,
14235
- commandId: command._id,
14236
- status: "processing"
14237
- });
14238
- ctx.events.emit("command:processing", {
14239
- commandId: command._id.toString(),
14240
- type: command.type
14241
- });
14242
- let commandResult;
14243
- switch (command.type) {
14244
- case "ping":
14245
- commandResult = handlePing();
14246
- break;
14247
- case "status":
14248
- commandResult = handleStatus(ctx);
14249
- break;
14250
- case "start-agent":
14251
- commandResult = await handleStartAgent(ctx, command);
14252
- break;
14253
- case "stop-agent":
14254
- commandResult = await handleStopAgent(ctx, command);
14255
- break;
14256
- default: {
14257
- const _exhaustive = command;
14258
- commandResult = {
14259
- result: `Unknown command type: ${_exhaustive.type}`,
14260
- failed: true
14261
- };
14262
- }
14263
- }
14264
- const finalStatus = commandResult.failed ? "failed" : "completed";
14265
- await ctx.deps.backend.mutation(api.machines.ackCommand, {
14266
- sessionId: ctx.sessionId,
14267
- commandId: command._id,
14268
- status: finalStatus,
14269
- result: commandResult.result
14270
- });
14271
- ctx.events.emit("command:completed", {
14272
- commandId: command._id.toString(),
14273
- type: command.type,
14274
- failed: commandResult.failed,
14275
- result: commandResult.result
14276
- });
14277
- if (commandResult.failed) {
14278
- console.log(` ❌ Command failed: ${commandResult.result}`);
14279
- } else {
14280
- console.log(` ✅ Command completed`);
14281
- }
14282
- } catch (error) {
14283
- console.error(` ❌ Command failed: ${error.message}`);
14284
- try {
14285
- await ctx.deps.backend.mutation(api.machines.ackCommand, {
14286
- sessionId: ctx.sessionId,
14287
- commandId: command._id,
14288
- status: "failed",
14289
- result: error.message
14290
- });
14291
- } catch {}
14292
- }
14293
- }
14294
14183
  async function startCommandLoop(ctx) {
14295
14184
  let heartbeatCount = 0;
14296
14185
  const heartbeatTimer = setInterval(() => {
@@ -14317,66 +14206,56 @@ async function startCommandLoop(ctx) {
14317
14206
  process.on("SIGTERM", shutdown);
14318
14207
  process.on("SIGHUP", shutdown);
14319
14208
  const wsClient2 = await getConvexWsClient();
14320
- const commandQueue = [];
14321
- const queuedCommandIds = new Set;
14322
- let drainingQueue = false;
14323
- const enqueueCommands = (commands) => {
14324
- for (const command of commands) {
14325
- const commandId = command._id.toString();
14326
- if (queuedCommandIds.has(commandId))
14327
- continue;
14328
- queuedCommandIds.add(commandId);
14329
- commandQueue.push(command);
14330
- }
14331
- };
14332
- const drainQueue = async () => {
14333
- if (drainingQueue)
14334
- return;
14335
- drainingQueue = true;
14336
- try {
14337
- while (commandQueue.length > 0) {
14338
- const command = commandQueue.shift();
14339
- const commandId = command._id.toString();
14340
- queuedCommandIds.delete(commandId);
14341
- try {
14342
- await processCommand(ctx, command);
14343
- } catch (error) {
14344
- console.error(` ❌ Command processing failed: ${error.message}`);
14345
- }
14346
- }
14347
- } finally {
14348
- drainingQueue = false;
14349
- }
14350
- };
14351
14209
  console.log(`
14352
14210
  Listening for commands...`);
14353
14211
  console.log(`Press Ctrl+C to stop
14354
14212
  `);
14355
- wsClient2.onUpdate(api.machines.getPendingCommands, {
14213
+ const processedCommandIds = new Map;
14214
+ const processedPingIds = new Map;
14215
+ wsClient2.onUpdate(api.machines.getCommandEvents, {
14356
14216
  sessionId: ctx.sessionId,
14357
14217
  machineId: ctx.machineId
14358
14218
  }, async (result) => {
14359
- if (!result.commands || result.commands.length === 0)
14219
+ if (!result.events || result.events.length === 0)
14360
14220
  return;
14361
- const parsed = [];
14362
- for (const raw of result.commands) {
14363
- const command = parseMachineCommand(raw);
14364
- if (command !== null) {
14365
- parsed.push(command);
14366
- } else {
14367
- try {
14368
- await ctx.deps.backend.mutation(api.machines.ackCommand, {
14221
+ const evictBefore = Date.now() - AGENT_REQUEST_DEADLINE_MS;
14222
+ for (const [id, ts] of processedCommandIds) {
14223
+ if (ts < evictBefore)
14224
+ processedCommandIds.delete(id);
14225
+ }
14226
+ for (const [id, ts] of processedPingIds) {
14227
+ if (ts < evictBefore)
14228
+ processedPingIds.delete(id);
14229
+ }
14230
+ for (const event of result.events) {
14231
+ const eventId = event._id.toString();
14232
+ try {
14233
+ console.log(`[${formatTimestamp()}] \uD83D\uDCE1 Stream command event: ${event.type}`);
14234
+ if (event.type === "agent.requestStart") {
14235
+ if (processedCommandIds.has(eventId))
14236
+ continue;
14237
+ processedCommandIds.set(eventId, Date.now());
14238
+ await onRequestStartAgent(ctx, event);
14239
+ } else if (event.type === "agent.requestStop") {
14240
+ if (processedCommandIds.has(eventId))
14241
+ continue;
14242
+ processedCommandIds.set(eventId, Date.now());
14243
+ await onRequestStopAgent(ctx, event);
14244
+ } else if (event.type === "daemon.ping") {
14245
+ if (processedPingIds.has(eventId))
14246
+ continue;
14247
+ processedPingIds.set(eventId, Date.now());
14248
+ handlePing();
14249
+ await ctx.deps.backend.mutation(api.machines.ackPing, {
14369
14250
  sessionId: ctx.sessionId,
14370
- commandId: raw._id,
14371
- status: "failed",
14372
- result: `Invalid command: type="${raw.type}" missing required payload fields`
14251
+ machineId: ctx.machineId,
14252
+ pingEventId: event._id
14373
14253
  });
14374
- console.warn(`[${formatTimestamp()}] ⚠️ Acked invalid command ${raw._id} (type=${raw.type}) as failed`);
14375
- } catch {}
14254
+ }
14255
+ } catch (err) {
14256
+ console.error(`[${formatTimestamp()}] ❌ Stream command event failed: ${err.message}`);
14376
14257
  }
14377
14258
  }
14378
- enqueueCommands(parsed);
14379
- await drainQueue();
14380
14259
  });
14381
14260
  const modelRefreshTimer = setInterval(() => {
14382
14261
  refreshModels(ctx).catch((err) => {
@@ -14391,9 +14270,9 @@ var init_command_loop = __esm(() => {
14391
14270
  init_api3();
14392
14271
  init_client2();
14393
14272
  init_on_daemon_shutdown();
14273
+ init_on_request_start_agent();
14274
+ init_on_request_stop_agent();
14394
14275
  init_pid();
14395
- init_start_agent();
14396
- init_stop_agent();
14397
14276
  MODEL_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
14398
14277
  });
14399
14278
 
@@ -14425,7 +14304,7 @@ var init_state_recovery = __esm(() => {
14425
14304
  init_shared();
14426
14305
  });
14427
14306
 
14428
- // src/commands/machine/daemon-start/event-bus.ts
14307
+ // src/events/daemon/event-bus.ts
14429
14308
  class DaemonEventBus {
14430
14309
  listeners = new Map;
14431
14310
  on(event, listener) {
@@ -14454,54 +14333,69 @@ class DaemonEventBus {
14454
14333
  }
14455
14334
  }
14456
14335
 
14457
- // src/commands/machine/daemon-start/event-listeners.ts
14336
+ // src/events/daemon/agent/on-agent-exited.ts
14337
+ function onAgentExited(ctx, payload) {
14338
+ const { chatroomId, role, pid, code: code2, signal, stopReason, intentional } = payload;
14339
+ const ts = formatTimestamp();
14340
+ console.log(`[${ts}] Agent stopped: ${stopReason} (${role})`);
14341
+ if (intentional) {
14342
+ console.log(`[${ts}] ℹ️ Agent process exited after intentional stop ` + `(PID: ${pid}, role: ${role}, code: ${code2}, signal: ${signal})`);
14343
+ } else {
14344
+ console.log(`[${ts}] ⚠️ Agent process exited ` + `(PID: ${pid}, role: ${role}, code: ${code2}, signal: ${signal})`);
14345
+ }
14346
+ ctx.deps.backend.mutation(api.machines.recordAgentExited, {
14347
+ sessionId: ctx.sessionId,
14348
+ machineId: ctx.machineId,
14349
+ chatroomId,
14350
+ role,
14351
+ pid,
14352
+ intentional,
14353
+ stopReason,
14354
+ stopSignal: stopReason === "process_terminated_with_signal" ? signal ?? undefined : undefined,
14355
+ exitCode: code2 ?? undefined,
14356
+ signal: signal ?? undefined
14357
+ }).catch((err) => {
14358
+ console.log(` ⚠️ Failed to record agent exit event: ${err.message}`);
14359
+ });
14360
+ ctx.deps.machine.clearAgentPid(ctx.machineId, chatroomId, role);
14361
+ for (const service of ctx.agentServices.values()) {
14362
+ service.untrack(pid);
14363
+ }
14364
+ }
14365
+ var init_on_agent_exited = __esm(() => {
14366
+ init_api3();
14367
+ });
14368
+
14369
+ // src/events/daemon/agent/on-agent-started.ts
14370
+ function onAgentStarted(ctx, payload) {
14371
+ const ts = formatTimestamp();
14372
+ console.log(`[${ts}] \uD83D\uDFE2 Agent started: ${payload.role} (PID: ${payload.pid}, harness: ${payload.harness})`);
14373
+ }
14374
+ var init_on_agent_started = () => {};
14375
+
14376
+ // src/events/daemon/agent/on-agent-stopped.ts
14377
+ function onAgentStopped(ctx, payload) {
14378
+ const ts = formatTimestamp();
14379
+ console.log(`[${ts}] \uD83D\uDD34 Agent stopped: ${payload.role} (PID: ${payload.pid})`);
14380
+ }
14381
+ var init_on_agent_stopped = () => {};
14382
+
14383
+ // src/events/daemon/register-listeners.ts
14458
14384
  function registerEventListeners(ctx) {
14459
14385
  const unsubs = [];
14460
- unsubs.push(ctx.events.on("agent:exited", (payload) => {
14461
- const { chatroomId, role, pid, code: code2, signal, intentional } = payload;
14462
- const ts = formatTimestamp();
14463
- if (intentional) {
14464
- console.log(`[${ts}] ℹ️ Agent process exited after intentional stop ` + `(PID: ${pid}, role: ${role}, code: ${code2}, signal: ${signal})`);
14465
- } else {
14466
- console.log(`[${ts}] ⚠️ Agent process exited ` + `(PID: ${pid}, role: ${role}, code: ${code2}, signal: ${signal})`);
14467
- }
14468
- ctx.deps.backend.mutation(api.machines.updateSpawnedAgent, {
14469
- sessionId: ctx.sessionId,
14470
- machineId: ctx.machineId,
14471
- chatroomId,
14472
- role,
14473
- pid: undefined
14474
- }).catch((err) => {
14475
- console.log(` ⚠️ Failed to clear PID in backend: ${err.message}`);
14476
- });
14477
- ctx.deps.machine.clearAgentPid(ctx.machineId, chatroomId, role);
14478
- for (const service of ctx.agentServices.values()) {
14479
- service.untrack(pid);
14480
- }
14481
- ctx.deps.backend.mutation(api.participants.leave, {
14482
- sessionId: ctx.sessionId,
14483
- chatroomId,
14484
- role
14485
- }).catch((err) => {
14486
- console.log(` ⚠️ Could not remove participant: ${err.message}`);
14487
- });
14488
- }));
14489
- unsubs.push(ctx.events.on("agent:started", (payload) => {
14490
- const ts = formatTimestamp();
14491
- console.log(`[${ts}] \uD83D\uDFE2 Agent started: ${payload.role} (PID: ${payload.pid}, harness: ${payload.harness})`);
14492
- }));
14493
- unsubs.push(ctx.events.on("agent:stopped", (payload) => {
14494
- const ts = formatTimestamp();
14495
- console.log(`[${ts}] \uD83D\uDD34 Agent stopped: ${payload.role} (PID: ${payload.pid})`);
14496
- }));
14386
+ unsubs.push(ctx.events.on("agent:exited", (payload) => onAgentExited(ctx, payload)));
14387
+ unsubs.push(ctx.events.on("agent:started", (payload) => onAgentStarted(ctx, payload)));
14388
+ unsubs.push(ctx.events.on("agent:stopped", (payload) => onAgentStopped(ctx, payload)));
14497
14389
  return () => {
14498
14390
  for (const unsub of unsubs) {
14499
14391
  unsub();
14500
14392
  }
14501
14393
  };
14502
14394
  }
14503
- var init_event_listeners = __esm(() => {
14504
- init_api3();
14395
+ var init_register_listeners = __esm(() => {
14396
+ init_on_agent_exited();
14397
+ init_on_agent_started();
14398
+ init_on_agent_stopped();
14505
14399
  });
14506
14400
 
14507
14401
  // src/commands/machine/daemon-start/init.ts
@@ -14543,7 +14437,9 @@ function createDefaultDeps16() {
14543
14437
  machine: {
14544
14438
  clearAgentPid,
14545
14439
  persistAgentPid,
14546
- listAgentEntries
14440
+ listAgentEntries,
14441
+ persistEventCursor,
14442
+ loadEventCursor
14547
14443
  },
14548
14444
  clock: {
14549
14445
  now: () => Date.now(),
@@ -14672,7 +14568,7 @@ var init_init2 = __esm(() => {
14672
14568
  init_error_formatting();
14673
14569
  init_version();
14674
14570
  init_pid();
14675
- init_event_listeners();
14571
+ init_register_listeners();
14676
14572
  });
14677
14573
 
14678
14574
  // src/commands/machine/daemon-start/index.ts
@@ -15397,7 +15293,7 @@ program2.command("report-progress").description("Report progress on current task
15397
15293
  });
15398
15294
  });
15399
15295
  var backlogCommand = program2.command("backlog").description("Manage task queue and backlog");
15400
- backlogCommand.command("list").description("List tasks in a chatroom").requiredOption("--chatroom-id <id>", "Chatroom identifier").requiredOption("--role <role>", "Your role").requiredOption("--status <status>", "Filter by status (pending|in_progress|queued|backlog|completed|cancelled|active|pending_review|archived|all)").option("--limit <n>", "Maximum number of tasks to show (required for --status=all)").option("--full", "Show full task content without truncation").action(async (options) => {
15296
+ backlogCommand.command("list").description("List tasks in a chatroom").requiredOption("--chatroom-id <id>", "Chatroom identifier").requiredOption("--role <role>", "Your role").requiredOption("--status <status>", "Filter by status (pending|in_progress|backlog|completed|cancelled|active|pending_review|archived|all)").option("--limit <n>", "Maximum number of tasks to show (required for --status=all)").option("--full", "Show full task content without truncation").action(async (options) => {
15401
15297
  if (options.status === "all" && !options.limit) {
15402
15298
  console.error("❌ When using --status=all, you must specify --limit=<n>");
15403
15299
  console.error(" Example: chatroom backlog list --chatroom-id=<id> --role=builder --status=all --limit=50");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatroom-cli",
3
- "version": "1.2.4",
3
+ "version": "1.3.0",
4
4
  "description": "CLI for multi-agent chatroom collaboration",
5
5
  "type": "module",
6
6
  "bin": {