chatroom-cli 1.4.4 → 1.6.1

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 +1223 -1093
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -10123,306 +10123,11 @@ var init_auth_logout = __esm(() => {
10123
10123
  init_storage();
10124
10124
  });
10125
10125
 
10126
- // src/infrastructure/machine/types.ts
10127
- var AGENT_HARNESSES, AGENT_HARNESS_COMMANDS, MACHINE_CONFIG_VERSION = "1";
10128
- var init_types = __esm(() => {
10129
- AGENT_HARNESSES = ["opencode", "pi"];
10130
- AGENT_HARNESS_COMMANDS = {
10131
- opencode: "opencode",
10132
- pi: "pi"
10133
- };
10134
- });
10135
-
10136
- // src/infrastructure/machine/detection.ts
10137
- import { execSync } from "node:child_process";
10138
- function commandExists(command) {
10139
- try {
10140
- const checkCommand = process.platform === "win32" ? `where ${command}` : `which ${command}`;
10141
- execSync(checkCommand, { stdio: "ignore" });
10142
- return true;
10143
- } catch {
10144
- return false;
10145
- }
10146
- }
10147
- function parseVersion(versionStr) {
10148
- const match = versionStr.match(/v?(\d+)\.(\d+)\.(\d+)/);
10149
- if (!match)
10150
- return null;
10151
- const major = parseInt(match[1], 10);
10152
- const version2 = `${match[1]}.${match[2]}.${match[3]}`;
10153
- return { version: version2, major };
10154
- }
10155
- function detectHarnessVersion(harness) {
10156
- const versionCommand = HARNESS_VERSION_COMMANDS[harness];
10157
- if (!versionCommand)
10158
- return null;
10159
- try {
10160
- const output = execSync(versionCommand, {
10161
- stdio: ["pipe", "pipe", "pipe"],
10162
- timeout: 5000
10163
- }).toString().trim();
10164
- return parseVersion(output);
10165
- } catch {
10166
- return null;
10167
- }
10168
- }
10169
- function detectHarnessVersions(harnesses) {
10170
- const versions = {};
10171
- for (const harness of harnesses) {
10172
- const version2 = detectHarnessVersion(harness);
10173
- if (version2) {
10174
- versions[harness] = version2;
10175
- }
10176
- }
10177
- return versions;
10178
- }
10179
- function detectAvailableHarnesses() {
10180
- const available = [];
10181
- for (const harness of AGENT_HARNESSES) {
10182
- const command = AGENT_HARNESS_COMMANDS[harness];
10183
- if (commandExists(command)) {
10184
- available.push(harness);
10185
- }
10186
- }
10187
- return available;
10188
- }
10189
- var HARNESS_VERSION_COMMANDS;
10190
- var init_detection = __esm(() => {
10191
- init_types();
10192
- HARNESS_VERSION_COMMANDS = {
10193
- opencode: "opencode --version",
10194
- pi: "pi --version"
10195
- };
10196
- });
10197
-
10198
- // src/infrastructure/machine/storage.ts
10199
- import { randomUUID } from "node:crypto";
10200
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2, renameSync } from "node:fs";
10201
- import { homedir as homedir2, hostname as hostname2 } from "node:os";
10202
- import { join as join3 } from "node:path";
10203
- function ensureConfigDir2() {
10204
- if (!existsSync2(CHATROOM_DIR2)) {
10205
- mkdirSync2(CHATROOM_DIR2, { recursive: true, mode: 448 });
10206
- }
10207
- }
10208
- function getMachineConfigPath() {
10209
- return join3(CHATROOM_DIR2, MACHINE_FILE);
10210
- }
10211
- function loadConfigFile() {
10212
- const configPath = getMachineConfigPath();
10213
- if (!existsSync2(configPath)) {
10214
- return null;
10215
- }
10216
- try {
10217
- const content = readFileSync3(configPath, "utf-8");
10218
- return JSON.parse(content);
10219
- } catch (error) {
10220
- console.warn(`⚠️ Failed to read machine config at ${configPath}: ${error.message}`);
10221
- console.warn(` The machine will re-register with a new identity on next startup.`);
10222
- console.warn(` If this is unexpected, check the file for corruption.`);
10223
- return null;
10224
- }
10225
- }
10226
- function saveConfigFile(configFile) {
10227
- ensureConfigDir2();
10228
- const configPath = getMachineConfigPath();
10229
- const tempPath = `${configPath}.tmp`;
10230
- const content = JSON.stringify(configFile, null, 2);
10231
- writeFileSync2(tempPath, content, { encoding: "utf-8", mode: 384 });
10232
- renameSync(tempPath, configPath);
10233
- }
10234
- function loadMachineConfig() {
10235
- const configFile = loadConfigFile();
10236
- if (!configFile)
10237
- return null;
10238
- const convexUrl = getConvexUrl();
10239
- return configFile.machines[convexUrl] ?? null;
10240
- }
10241
- function saveMachineConfig(config) {
10242
- const configFile = loadConfigFile() ?? {
10243
- version: MACHINE_CONFIG_VERSION,
10244
- machines: {}
10245
- };
10246
- const convexUrl = getConvexUrl();
10247
- configFile.machines[convexUrl] = config;
10248
- saveConfigFile(configFile);
10249
- }
10250
- function createNewEndpointConfig() {
10251
- const now = new Date().toISOString();
10252
- const availableHarnesses = detectAvailableHarnesses();
10253
- return {
10254
- machineId: randomUUID(),
10255
- hostname: hostname2(),
10256
- os: process.platform,
10257
- registeredAt: now,
10258
- lastSyncedAt: now,
10259
- availableHarnesses,
10260
- harnessVersions: detectHarnessVersions(availableHarnesses)
10261
- };
10262
- }
10263
- function ensureMachineRegistered() {
10264
- let config = loadMachineConfig();
10265
- if (!config) {
10266
- config = createNewEndpointConfig();
10267
- saveMachineConfig(config);
10268
- } else {
10269
- const now = new Date().toISOString();
10270
- config.availableHarnesses = detectAvailableHarnesses();
10271
- config.harnessVersions = detectHarnessVersions(config.availableHarnesses);
10272
- config.lastSyncedAt = now;
10273
- saveMachineConfig(config);
10274
- }
10275
- return {
10276
- machineId: config.machineId,
10277
- hostname: config.hostname,
10278
- os: config.os,
10279
- availableHarnesses: config.availableHarnesses,
10280
- harnessVersions: config.harnessVersions
10281
- };
10282
- }
10283
- var CHATROOM_DIR2, MACHINE_FILE = "machine.json";
10284
- var init_storage2 = __esm(() => {
10285
- init_detection();
10286
- init_types();
10287
- init_client2();
10288
- CHATROOM_DIR2 = join3(homedir2(), ".chatroom");
10289
- });
10290
-
10291
- // src/infrastructure/machine/daemon-state.ts
10292
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3, renameSync as renameSync2 } from "node:fs";
10293
- import { homedir as homedir3 } from "node:os";
10294
- import { join as join4 } from "node:path";
10295
- function agentKey(chatroomId, role) {
10296
- return `${chatroomId}/${role}`;
10297
- }
10298
- function ensureStateDir() {
10299
- if (!existsSync3(STATE_DIR)) {
10300
- mkdirSync3(STATE_DIR, { recursive: true, mode: 448 });
10301
- }
10302
- }
10303
- function stateFilePath(machineId) {
10304
- return join4(STATE_DIR, `${machineId}.json`);
10305
- }
10306
- function loadDaemonState(machineId) {
10307
- const filePath = stateFilePath(machineId);
10308
- if (!existsSync3(filePath)) {
10309
- return null;
10310
- }
10311
- try {
10312
- const content = readFileSync4(filePath, "utf-8");
10313
- return JSON.parse(content);
10314
- } catch {
10315
- return null;
10316
- }
10317
- }
10318
- function saveDaemonState(state) {
10319
- ensureStateDir();
10320
- const filePath = stateFilePath(state.machineId);
10321
- const tempPath = `${filePath}.tmp`;
10322
- const content = JSON.stringify(state, null, 2);
10323
- writeFileSync3(tempPath, content, { encoding: "utf-8", mode: 384 });
10324
- renameSync2(tempPath, filePath);
10325
- }
10326
- function loadOrCreate(machineId) {
10327
- return loadDaemonState(machineId) ?? {
10328
- version: STATE_VERSION,
10329
- machineId,
10330
- updatedAt: new Date().toISOString(),
10331
- agents: {}
10332
- };
10333
- }
10334
- function persistAgentPid(machineId, chatroomId, role, pid, harness) {
10335
- const state = loadOrCreate(machineId);
10336
- state.agents[agentKey(chatroomId, role)] = {
10337
- pid,
10338
- harness,
10339
- startedAt: new Date().toISOString()
10340
- };
10341
- state.updatedAt = new Date().toISOString();
10342
- saveDaemonState(state);
10343
- }
10344
- function clearAgentPid(machineId, chatroomId, role) {
10345
- const state = loadDaemonState(machineId);
10346
- if (!state)
10347
- return;
10348
- const key = agentKey(chatroomId, role);
10349
- if (!(key in state.agents))
10350
- return;
10351
- delete state.agents[key];
10352
- state.updatedAt = new Date().toISOString();
10353
- saveDaemonState(state);
10354
- }
10355
- function listAgentEntries(machineId) {
10356
- const state = loadDaemonState(machineId);
10357
- if (!state)
10358
- return [];
10359
- const results = [];
10360
- for (const [key, entry] of Object.entries(state.agents)) {
10361
- const separatorIndex = key.lastIndexOf("/");
10362
- if (separatorIndex === -1)
10363
- continue;
10364
- const chatroomId = key.substring(0, separatorIndex);
10365
- const role = key.substring(separatorIndex + 1);
10366
- results.push({ chatroomId, role, entry });
10367
- }
10368
- return results;
10369
- }
10370
- function persistEventCursor(machineId, lastSeenEventId) {
10371
- try {
10372
- const state = loadOrCreate(machineId);
10373
- state.lastSeenEventId = lastSeenEventId;
10374
- state.updatedAt = new Date().toISOString();
10375
- saveDaemonState(state);
10376
- } catch (err) {
10377
- console.warn(`⚠️ Failed to persist event cursor: ${err.message}`);
10378
- }
10379
- }
10380
- function loadEventCursor(machineId) {
10381
- const state = loadDaemonState(machineId);
10382
- return state?.lastSeenEventId ?? null;
10383
- }
10384
- var CHATROOM_DIR3, STATE_DIR, STATE_VERSION = "1";
10385
- var init_daemon_state = __esm(() => {
10386
- CHATROOM_DIR3 = join4(homedir3(), ".chatroom");
10387
- STATE_DIR = join4(CHATROOM_DIR3, "machines", "state");
10388
- });
10389
-
10390
- // src/infrastructure/machine/intentional-stops.ts
10391
- function agentKey2(chatroomId, role) {
10392
- return `${chatroomId}:${role.toLowerCase()}`;
10393
- }
10394
- function markIntentionalStop(chatroomId, role, reason = "intentional_stop") {
10395
- pendingStops.set(agentKey2(chatroomId, role), reason);
10396
- }
10397
- function consumeIntentionalStop(chatroomId, role) {
10398
- const key = agentKey2(chatroomId, role);
10399
- const reason = pendingStops.get(key) ?? null;
10400
- if (reason !== null) {
10401
- pendingStops.delete(key);
10402
- }
10403
- return reason;
10404
- }
10405
- function clearIntentionalStop(chatroomId, role) {
10406
- pendingStops.delete(agentKey2(chatroomId, role));
10407
- }
10408
- var pendingStops;
10409
- var init_intentional_stops = __esm(() => {
10410
- pendingStops = new Map;
10411
- });
10412
-
10413
- // src/infrastructure/machine/index.ts
10414
- var init_machine = __esm(() => {
10415
- init_types();
10416
- init_storage2();
10417
- init_daemon_state();
10418
- init_intentional_stops();
10419
- });
10420
-
10421
10126
  // src/infrastructure/services/remote-agents/base-cli-agent-service.ts
10422
- import { spawn, execSync as execSync2 } from "node:child_process";
10127
+ import { spawn, execSync } from "node:child_process";
10423
10128
  function defaultDeps() {
10424
10129
  return {
10425
- execSync: execSync2,
10130
+ execSync,
10426
10131
  spawn,
10427
10132
  kill: (pid, signal) => process.kill(pid, signal)
10428
10133
  };
@@ -10514,6 +10219,9 @@ var OPENCODE_COMMAND = "opencode", OpenCodeAgentService;
10514
10219
  var init_opencode_agent_service = __esm(() => {
10515
10220
  init_base_cli_agent_service();
10516
10221
  OpenCodeAgentService = class OpenCodeAgentService extends BaseCLIAgentService {
10222
+ id = "opencode";
10223
+ displayName = "OpenCode";
10224
+ command = OPENCODE_COMMAND;
10517
10225
  constructor(deps) {
10518
10226
  super(deps);
10519
10227
  }
@@ -10597,10 +10305,6 @@ ${options.prompt}` : options.prompt;
10597
10305
  });
10598
10306
 
10599
10307
  // src/infrastructure/services/remote-agents/opencode/index.ts
10600
- var exports_opencode = {};
10601
- __export(exports_opencode, {
10602
- OpenCodeAgentService: () => OpenCodeAgentService
10603
- });
10604
10308
  var init_opencode = __esm(() => {
10605
10309
  init_opencode_agent_service();
10606
10310
  });
@@ -10700,6 +10404,9 @@ var init_pi_agent_service = __esm(() => {
10700
10404
  init_base_cli_agent_service();
10701
10405
  init_pi_rpc_reader();
10702
10406
  PiAgentService = class PiAgentService extends BaseCLIAgentService {
10407
+ id = "pi";
10408
+ displayName = "Pi";
10409
+ command = PI_COMMAND;
10703
10410
  constructor(deps) {
10704
10411
  super(deps);
10705
10412
  }
@@ -10886,13 +10593,528 @@ var init_pi_agent_service = __esm(() => {
10886
10593
  };
10887
10594
  });
10888
10595
 
10889
- // src/infrastructure/services/remote-agents/pi/index.ts
10890
- var exports_pi = {};
10891
- __export(exports_pi, {
10892
- PiAgentService: () => PiAgentService
10596
+ // src/infrastructure/services/remote-agents/pi/index.ts
10597
+ var init_pi = __esm(() => {
10598
+ init_pi_agent_service();
10599
+ });
10600
+
10601
+ // src/infrastructure/services/remote-agents/cursor/cursor-stream-reader.ts
10602
+ import { createInterface as createInterface2 } from "node:readline";
10603
+
10604
+ class CursorStreamReader {
10605
+ textCallbacks = [];
10606
+ agentEndCallbacks = [];
10607
+ toolCallCallbacks = [];
10608
+ toolResultCallbacks = [];
10609
+ anyEventCallbacks = [];
10610
+ constructor(stream) {
10611
+ const rl = createInterface2({ input: stream, crlfDelay: Infinity });
10612
+ rl.on("line", (line) => this._handleLine(line));
10613
+ }
10614
+ onText(cb) {
10615
+ this.textCallbacks.push(cb);
10616
+ }
10617
+ onAgentEnd(cb) {
10618
+ this.agentEndCallbacks.push(cb);
10619
+ }
10620
+ onToolCall(cb) {
10621
+ this.toolCallCallbacks.push(cb);
10622
+ }
10623
+ onToolResult(cb) {
10624
+ this.toolResultCallbacks.push(cb);
10625
+ }
10626
+ onAnyEvent(cb) {
10627
+ this.anyEventCallbacks.push(cb);
10628
+ }
10629
+ _handleLine(line) {
10630
+ const trimmed = line.trim();
10631
+ if (!trimmed)
10632
+ return;
10633
+ let event;
10634
+ try {
10635
+ event = JSON.parse(trimmed);
10636
+ } catch {
10637
+ return;
10638
+ }
10639
+ for (const cb of this.anyEventCallbacks)
10640
+ cb();
10641
+ const type = event["type"];
10642
+ const subtype = event["subtype"];
10643
+ if (type === "assistant") {
10644
+ const message = event["message"];
10645
+ const content = message?.["content"] ?? [];
10646
+ for (const block of content) {
10647
+ if (block["type"] === "text" && typeof block["text"] === "string") {
10648
+ for (const cb of this.textCallbacks)
10649
+ cb(block["text"]);
10650
+ }
10651
+ }
10652
+ return;
10653
+ }
10654
+ if (type === "tool_call") {
10655
+ const callId = event["call_id"] ?? "";
10656
+ const toolCall = event["tool_call"];
10657
+ if (subtype === "started") {
10658
+ for (const cb of this.toolCallCallbacks)
10659
+ cb(callId, toolCall);
10660
+ } else if (subtype === "completed") {
10661
+ for (const cb of this.toolResultCallbacks)
10662
+ cb(callId, toolCall);
10663
+ }
10664
+ return;
10665
+ }
10666
+ if (type === "result" && subtype === "success") {
10667
+ const sessionId = event["session_id"];
10668
+ for (const cb of this.agentEndCallbacks)
10669
+ cb(sessionId);
10670
+ return;
10671
+ }
10672
+ }
10673
+ }
10674
+ var init_cursor_stream_reader = () => {};
10675
+
10676
+ // src/infrastructure/services/remote-agents/cursor/cursor-agent-service.ts
10677
+ var CURSOR_COMMAND = "agent", CURSOR_MODELS, CursorAgentService;
10678
+ var init_cursor_agent_service = __esm(() => {
10679
+ init_base_cli_agent_service();
10680
+ init_cursor_stream_reader();
10681
+ CURSOR_MODELS = ["opus-4.6", "sonnet-4.6"];
10682
+ CursorAgentService = class CursorAgentService extends BaseCLIAgentService {
10683
+ id = "cursor";
10684
+ displayName = "Cursor";
10685
+ command = CURSOR_COMMAND;
10686
+ constructor(deps) {
10687
+ super(deps);
10688
+ }
10689
+ isInstalled() {
10690
+ return this.checkInstalled(CURSOR_COMMAND);
10691
+ }
10692
+ getVersion() {
10693
+ return this.checkVersion(CURSOR_COMMAND);
10694
+ }
10695
+ async listModels() {
10696
+ return CURSOR_MODELS;
10697
+ }
10698
+ async spawn(options) {
10699
+ const args = ["-p", "--force", "--output-format", "stream-json"];
10700
+ if (options.model) {
10701
+ args.push("--model", options.model);
10702
+ }
10703
+ const fullPrompt = options.systemPrompt ? `${options.systemPrompt}
10704
+
10705
+ ${options.prompt}` : options.prompt;
10706
+ const childProcess = this.deps.spawn(CURSOR_COMMAND, args, {
10707
+ cwd: options.workingDir,
10708
+ stdio: ["pipe", "pipe", "pipe"],
10709
+ shell: false,
10710
+ detached: true,
10711
+ env: { ...process.env }
10712
+ });
10713
+ childProcess.stdin?.write(fullPrompt);
10714
+ childProcess.stdin?.end();
10715
+ await new Promise((resolve) => setTimeout(resolve, 500));
10716
+ if (childProcess.killed || childProcess.exitCode !== null) {
10717
+ throw new Error(`Agent process exited immediately (exit code: ${childProcess.exitCode})`);
10718
+ }
10719
+ if (!childProcess.pid) {
10720
+ throw new Error("Agent process started but has no PID");
10721
+ }
10722
+ const pid = childProcess.pid;
10723
+ const context = options.context;
10724
+ const entry = this.registerProcess(pid, context);
10725
+ const roleTag = context.role ?? "unknown";
10726
+ const chatroomSuffix = context.chatroomId ? `@${context.chatroomId.slice(-6)}` : "";
10727
+ const logPrefix = `[cursor:${roleTag}${chatroomSuffix}`;
10728
+ const outputCallbacks = [];
10729
+ if (childProcess.stdout) {
10730
+ const reader = new CursorStreamReader(childProcess.stdout);
10731
+ let textBuffer = "";
10732
+ const flushText = () => {
10733
+ if (!textBuffer)
10734
+ return;
10735
+ for (const line of textBuffer.split(`
10736
+ `)) {
10737
+ if (line)
10738
+ process.stdout.write(`${logPrefix} text] ${line}
10739
+ `);
10740
+ }
10741
+ textBuffer = "";
10742
+ };
10743
+ reader.onText((text) => {
10744
+ textBuffer += text;
10745
+ if (textBuffer.includes(`
10746
+ `))
10747
+ flushText();
10748
+ entry.lastOutputAt = Date.now();
10749
+ for (const cb of outputCallbacks)
10750
+ cb();
10751
+ });
10752
+ reader.onAnyEvent(() => {
10753
+ entry.lastOutputAt = Date.now();
10754
+ for (const cb of outputCallbacks)
10755
+ cb();
10756
+ });
10757
+ reader.onAgentEnd(() => {
10758
+ flushText();
10759
+ process.stdout.write(`${logPrefix} agent_end]
10760
+ `);
10761
+ });
10762
+ reader.onToolCall((callId, toolCall) => {
10763
+ flushText();
10764
+ process.stdout.write(`${logPrefix} tool: ${callId} ${JSON.stringify(toolCall)}]
10765
+ `);
10766
+ });
10767
+ reader.onToolResult((callId) => {
10768
+ flushText();
10769
+ process.stdout.write(`${logPrefix} tool_result: ${callId}]
10770
+ `);
10771
+ });
10772
+ if (childProcess.stderr) {
10773
+ childProcess.stderr.pipe(process.stderr, { end: false });
10774
+ childProcess.stderr.on("data", () => {
10775
+ entry.lastOutputAt = Date.now();
10776
+ for (const cb of outputCallbacks)
10777
+ cb();
10778
+ });
10779
+ }
10780
+ return {
10781
+ pid,
10782
+ onExit: (cb) => {
10783
+ childProcess.on("exit", (code2, signal) => {
10784
+ this.deleteProcess(pid);
10785
+ cb({ code: code2, signal, context });
10786
+ });
10787
+ },
10788
+ onOutput: (cb) => {
10789
+ outputCallbacks.push(cb);
10790
+ },
10791
+ onAgentEnd: (cb) => {
10792
+ reader.onAgentEnd(cb);
10793
+ }
10794
+ };
10795
+ }
10796
+ if (childProcess.stderr) {
10797
+ childProcess.stderr.pipe(process.stderr, { end: false });
10798
+ childProcess.stderr.on("data", () => {
10799
+ entry.lastOutputAt = Date.now();
10800
+ for (const cb of outputCallbacks)
10801
+ cb();
10802
+ });
10803
+ }
10804
+ return {
10805
+ pid,
10806
+ onExit: (cb) => {
10807
+ childProcess.on("exit", (code2, signal) => {
10808
+ this.deleteProcess(pid);
10809
+ cb({ code: code2, signal, context });
10810
+ });
10811
+ },
10812
+ onOutput: (cb) => {
10813
+ outputCallbacks.push(cb);
10814
+ }
10815
+ };
10816
+ }
10817
+ };
10818
+ });
10819
+
10820
+ // src/infrastructure/services/remote-agents/cursor/index.ts
10821
+ var init_cursor = __esm(() => {
10822
+ init_cursor_agent_service();
10823
+ });
10824
+
10825
+ // src/infrastructure/services/remote-agents/registry.ts
10826
+ function registerHarness(service) {
10827
+ registry.set(service.id, service);
10828
+ }
10829
+ function getHarness(id) {
10830
+ return registry.get(id);
10831
+ }
10832
+ function getAllHarnesses() {
10833
+ return [...registry.values()];
10834
+ }
10835
+ var registry;
10836
+ var init_registry = __esm(() => {
10837
+ registry = new Map;
10838
+ });
10839
+
10840
+ // src/infrastructure/services/remote-agents/init-registry.ts
10841
+ function initHarnessRegistry() {
10842
+ if (initialized)
10843
+ return;
10844
+ registerHarness(new OpenCodeAgentService);
10845
+ registerHarness(new PiAgentService);
10846
+ registerHarness(new CursorAgentService);
10847
+ initialized = true;
10848
+ }
10849
+ var initialized = false;
10850
+ var init_init_registry = __esm(() => {
10851
+ init_registry();
10852
+ init_opencode();
10853
+ init_pi();
10854
+ init_cursor();
10855
+ });
10856
+
10857
+ // src/infrastructure/services/remote-agents/index.ts
10858
+ var init_remote_agents = __esm(() => {
10859
+ init_opencode();
10860
+ init_pi();
10861
+ init_cursor();
10862
+ init_registry();
10863
+ init_init_registry();
10864
+ });
10865
+
10866
+ // src/infrastructure/machine/detection.ts
10867
+ function detectHarnessVersion(harness) {
10868
+ initHarnessRegistry();
10869
+ const info = getHarness(harness)?.getVersion();
10870
+ if (!info)
10871
+ return null;
10872
+ return { version: info.version, major: info.major };
10873
+ }
10874
+ function detectHarnessVersions(harnesses) {
10875
+ const versions = {};
10876
+ for (const harness of harnesses) {
10877
+ const version2 = detectHarnessVersion(harness);
10878
+ if (version2) {
10879
+ versions[harness] = version2;
10880
+ }
10881
+ }
10882
+ return versions;
10883
+ }
10884
+ function detectAvailableHarnesses() {
10885
+ initHarnessRegistry();
10886
+ return getAllHarnesses().filter((s) => s.isInstalled()).map((s) => s.id);
10887
+ }
10888
+ var init_detection = __esm(() => {
10889
+ init_remote_agents();
10890
+ });
10891
+
10892
+ // src/infrastructure/machine/types.ts
10893
+ var MACHINE_CONFIG_VERSION = "1";
10894
+
10895
+ // src/infrastructure/machine/storage.ts
10896
+ import { randomUUID } from "node:crypto";
10897
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2, renameSync } from "node:fs";
10898
+ import { homedir as homedir2, hostname as hostname2 } from "node:os";
10899
+ import { join as join3 } from "node:path";
10900
+ function ensureConfigDir2() {
10901
+ if (!existsSync2(CHATROOM_DIR2)) {
10902
+ mkdirSync2(CHATROOM_DIR2, { recursive: true, mode: 448 });
10903
+ }
10904
+ }
10905
+ function getMachineConfigPath() {
10906
+ return join3(CHATROOM_DIR2, MACHINE_FILE);
10907
+ }
10908
+ function loadConfigFile() {
10909
+ const configPath = getMachineConfigPath();
10910
+ if (!existsSync2(configPath)) {
10911
+ return null;
10912
+ }
10913
+ try {
10914
+ const content = readFileSync3(configPath, "utf-8");
10915
+ return JSON.parse(content);
10916
+ } catch (error) {
10917
+ console.warn(`⚠️ Failed to read machine config at ${configPath}: ${error.message}`);
10918
+ console.warn(` The machine will re-register with a new identity on next startup.`);
10919
+ console.warn(` If this is unexpected, check the file for corruption.`);
10920
+ return null;
10921
+ }
10922
+ }
10923
+ function saveConfigFile(configFile) {
10924
+ ensureConfigDir2();
10925
+ const configPath = getMachineConfigPath();
10926
+ const tempPath = `${configPath}.tmp`;
10927
+ const content = JSON.stringify(configFile, null, 2);
10928
+ writeFileSync2(tempPath, content, { encoding: "utf-8", mode: 384 });
10929
+ renameSync(tempPath, configPath);
10930
+ }
10931
+ function loadMachineConfig() {
10932
+ const configFile = loadConfigFile();
10933
+ if (!configFile)
10934
+ return null;
10935
+ const convexUrl = getConvexUrl();
10936
+ return configFile.machines[convexUrl] ?? null;
10937
+ }
10938
+ function saveMachineConfig(config) {
10939
+ const configFile = loadConfigFile() ?? {
10940
+ version: MACHINE_CONFIG_VERSION,
10941
+ machines: {}
10942
+ };
10943
+ const convexUrl = getConvexUrl();
10944
+ configFile.machines[convexUrl] = config;
10945
+ saveConfigFile(configFile);
10946
+ }
10947
+ function createNewEndpointConfig() {
10948
+ const now = new Date().toISOString();
10949
+ const availableHarnesses = detectAvailableHarnesses();
10950
+ return {
10951
+ machineId: randomUUID(),
10952
+ hostname: hostname2(),
10953
+ os: process.platform,
10954
+ registeredAt: now,
10955
+ lastSyncedAt: now,
10956
+ availableHarnesses,
10957
+ harnessVersions: detectHarnessVersions(availableHarnesses)
10958
+ };
10959
+ }
10960
+ function ensureMachineRegistered() {
10961
+ let config = loadMachineConfig();
10962
+ if (!config) {
10963
+ config = createNewEndpointConfig();
10964
+ saveMachineConfig(config);
10965
+ } else {
10966
+ const now = new Date().toISOString();
10967
+ config.availableHarnesses = detectAvailableHarnesses();
10968
+ config.harnessVersions = detectHarnessVersions(config.availableHarnesses);
10969
+ config.lastSyncedAt = now;
10970
+ saveMachineConfig(config);
10971
+ }
10972
+ return {
10973
+ machineId: config.machineId,
10974
+ hostname: config.hostname,
10975
+ os: config.os,
10976
+ availableHarnesses: config.availableHarnesses,
10977
+ harnessVersions: config.harnessVersions
10978
+ };
10979
+ }
10980
+ function getMachineId() {
10981
+ const config = loadMachineConfig();
10982
+ return config?.machineId ?? null;
10983
+ }
10984
+ var CHATROOM_DIR2, MACHINE_FILE = "machine.json";
10985
+ var init_storage2 = __esm(() => {
10986
+ init_detection();
10987
+ init_client2();
10988
+ CHATROOM_DIR2 = join3(homedir2(), ".chatroom");
10989
+ });
10990
+
10991
+ // src/infrastructure/machine/daemon-state.ts
10992
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3, renameSync as renameSync2 } from "node:fs";
10993
+ import { homedir as homedir3 } from "node:os";
10994
+ import { join as join4 } from "node:path";
10995
+ function agentKey(chatroomId, role) {
10996
+ return `${chatroomId}/${role}`;
10997
+ }
10998
+ function ensureStateDir() {
10999
+ if (!existsSync3(STATE_DIR)) {
11000
+ mkdirSync3(STATE_DIR, { recursive: true, mode: 448 });
11001
+ }
11002
+ }
11003
+ function stateFilePath(machineId) {
11004
+ return join4(STATE_DIR, `${machineId}.json`);
11005
+ }
11006
+ function loadDaemonState(machineId) {
11007
+ const filePath = stateFilePath(machineId);
11008
+ if (!existsSync3(filePath)) {
11009
+ return null;
11010
+ }
11011
+ try {
11012
+ const content = readFileSync4(filePath, "utf-8");
11013
+ return JSON.parse(content);
11014
+ } catch {
11015
+ return null;
11016
+ }
11017
+ }
11018
+ function saveDaemonState(state) {
11019
+ ensureStateDir();
11020
+ const filePath = stateFilePath(state.machineId);
11021
+ const tempPath = `${filePath}.tmp`;
11022
+ const content = JSON.stringify(state, null, 2);
11023
+ writeFileSync3(tempPath, content, { encoding: "utf-8", mode: 384 });
11024
+ renameSync2(tempPath, filePath);
11025
+ }
11026
+ function loadOrCreate(machineId) {
11027
+ return loadDaemonState(machineId) ?? {
11028
+ version: STATE_VERSION,
11029
+ machineId,
11030
+ updatedAt: new Date().toISOString(),
11031
+ agents: {}
11032
+ };
11033
+ }
11034
+ function persistAgentPid(machineId, chatroomId, role, pid, harness) {
11035
+ const state = loadOrCreate(machineId);
11036
+ state.agents[agentKey(chatroomId, role)] = {
11037
+ pid,
11038
+ harness,
11039
+ startedAt: new Date().toISOString()
11040
+ };
11041
+ state.updatedAt = new Date().toISOString();
11042
+ saveDaemonState(state);
11043
+ }
11044
+ function clearAgentPid(machineId, chatroomId, role) {
11045
+ const state = loadDaemonState(machineId);
11046
+ if (!state)
11047
+ return;
11048
+ const key = agentKey(chatroomId, role);
11049
+ if (!(key in state.agents))
11050
+ return;
11051
+ delete state.agents[key];
11052
+ state.updatedAt = new Date().toISOString();
11053
+ saveDaemonState(state);
11054
+ }
11055
+ function listAgentEntries(machineId) {
11056
+ const state = loadDaemonState(machineId);
11057
+ if (!state)
11058
+ return [];
11059
+ const results = [];
11060
+ for (const [key, entry] of Object.entries(state.agents)) {
11061
+ const separatorIndex = key.lastIndexOf("/");
11062
+ if (separatorIndex === -1)
11063
+ continue;
11064
+ const chatroomId = key.substring(0, separatorIndex);
11065
+ const role = key.substring(separatorIndex + 1);
11066
+ results.push({ chatroomId, role, entry });
11067
+ }
11068
+ return results;
11069
+ }
11070
+ function persistEventCursor(machineId, lastSeenEventId) {
11071
+ try {
11072
+ const state = loadOrCreate(machineId);
11073
+ state.lastSeenEventId = lastSeenEventId;
11074
+ state.updatedAt = new Date().toISOString();
11075
+ saveDaemonState(state);
11076
+ } catch (err) {
11077
+ console.warn(`⚠️ Failed to persist event cursor: ${err.message}`);
11078
+ }
11079
+ }
11080
+ function loadEventCursor(machineId) {
11081
+ const state = loadDaemonState(machineId);
11082
+ return state?.lastSeenEventId ?? null;
11083
+ }
11084
+ var CHATROOM_DIR3, STATE_DIR, STATE_VERSION = "1";
11085
+ var init_daemon_state = __esm(() => {
11086
+ CHATROOM_DIR3 = join4(homedir3(), ".chatroom");
11087
+ STATE_DIR = join4(CHATROOM_DIR3, "machines", "state");
10893
11088
  });
10894
- var init_pi = __esm(() => {
10895
- init_pi_agent_service();
11089
+
11090
+ // src/infrastructure/machine/intentional-stops.ts
11091
+ function agentKey2(chatroomId, role) {
11092
+ return `${chatroomId}:${role.toLowerCase()}`;
11093
+ }
11094
+ function markIntentionalStop(chatroomId, role, reason = "user.stop") {
11095
+ pendingStops.set(agentKey2(chatroomId, role), reason);
11096
+ }
11097
+ function consumeIntentionalStop(chatroomId, role) {
11098
+ const key = agentKey2(chatroomId, role);
11099
+ const reason = pendingStops.get(key) ?? null;
11100
+ if (reason !== null) {
11101
+ pendingStops.delete(key);
11102
+ }
11103
+ return reason;
11104
+ }
11105
+ function clearIntentionalStop(chatroomId, role) {
11106
+ pendingStops.delete(agentKey2(chatroomId, role));
11107
+ }
11108
+ var pendingStops;
11109
+ var init_intentional_stops = __esm(() => {
11110
+ pendingStops = new Map;
11111
+ });
11112
+
11113
+ // src/infrastructure/machine/index.ts
11114
+ var init_machine = __esm(() => {
11115
+ init_storage2();
11116
+ init_daemon_state();
11117
+ init_intentional_stops();
10896
11118
  });
10897
11119
 
10898
11120
  // src/commands/auth-status/index.ts
@@ -10900,29 +11122,10 @@ var exports_auth_status = {};
10900
11122
  __export(exports_auth_status, {
10901
11123
  authStatus: () => authStatus
10902
11124
  });
10903
- async function listAvailableModelsDefault() {
10904
- const results = {};
10905
- try {
10906
- const { OpenCodeAgentService: OpenCodeAgentService2 } = await Promise.resolve().then(() => (init_opencode(), exports_opencode));
10907
- const agentService = new OpenCodeAgentService2;
10908
- if (agentService.isInstalled()) {
10909
- results["opencode"] = await agentService.listModels();
10910
- }
10911
- } catch {}
10912
- try {
10913
- const { PiAgentService: PiAgentService2 } = await Promise.resolve().then(() => (init_pi(), exports_pi));
10914
- const piService = new PiAgentService2;
10915
- if (piService.isInstalled()) {
10916
- results["pi"] = await piService.listModels();
10917
- }
10918
- } catch {}
10919
- return results;
10920
- }
10921
11125
  async function createDefaultDeps3() {
10922
11126
  const client2 = await getConvexClient();
10923
11127
  return {
10924
11128
  backend: {
10925
- mutation: (endpoint, args) => client2.mutation(endpoint, args),
10926
11129
  query: (endpoint, args) => client2.query(endpoint, args)
10927
11130
  },
10928
11131
  session: {
@@ -10931,8 +11134,7 @@ async function createDefaultDeps3() {
10931
11134
  isAuthenticated
10932
11135
  },
10933
11136
  getVersion,
10934
- ensureMachineRegistered,
10935
- listAvailableModels: listAvailableModelsDefault
11137
+ loadMachineConfig
10936
11138
  };
10937
11139
  }
10938
11140
  async function authStatus(deps) {
@@ -10968,32 +11170,18 @@ ${"═".repeat(50)}`);
10968
11170
  if (validation.userName) {
10969
11171
  console.log(`\uD83D\uDC64 User: ${validation.userName}`);
10970
11172
  }
10971
- try {
10972
- const machineInfo = d.ensureMachineRegistered();
10973
- const availableModels = await d.listAvailableModels();
10974
- await d.backend.mutation(api.machines.register, {
10975
- sessionId: authData.sessionId,
10976
- machineId: machineInfo.machineId,
10977
- hostname: machineInfo.hostname,
10978
- os: machineInfo.os,
10979
- availableHarnesses: machineInfo.availableHarnesses,
10980
- harnessVersions: machineInfo.harnessVersions,
10981
- availableModels
10982
- });
11173
+ const machineConfig = d.loadMachineConfig();
11174
+ if (machineConfig) {
10983
11175
  console.log(`
10984
- \uD83D\uDDA5️ Machine registered: ${machineInfo.hostname}`);
10985
- console.log(` ID: ${machineInfo.machineId}`);
10986
- if (machineInfo.availableHarnesses.length > 0) {
10987
- console.log(` Harnesses: ${machineInfo.availableHarnesses.join(", ")}`);
11176
+ \uD83D\uDDA5️ Machine: ${machineConfig.hostname}`);
11177
+ console.log(` ID: ${machineConfig.machineId}`);
11178
+ if (machineConfig.availableHarnesses.length > 0) {
11179
+ console.log(` Harnesses: ${machineConfig.availableHarnesses.join(", ")}`);
10988
11180
  }
10989
- const totalModels = Object.values(availableModels).flat().length;
10990
- if (totalModels > 0) {
10991
- console.log(` Models: ${totalModels} discovered`);
10992
- }
10993
- } catch (machineError) {
10994
- const err = machineError;
11181
+ } else {
10995
11182
  console.log(`
10996
- ⚠️ Machine registration skipped: ${err.message}`);
11183
+ \uD83D\uDDA5️ Machine: not registered`);
11184
+ console.log(` Run \`chatroom machine start\` to register this machine.`);
10997
11185
  }
10998
11186
  } else {
10999
11187
  console.log(`
@@ -11315,46 +11503,24 @@ async function registerAgent(chatroomId, options, deps) {
11315
11503
  process.exit(1);
11316
11504
  }
11317
11505
  if (type === "remote") {
11506
+ const machineId = getMachineId();
11507
+ if (!machineId) {
11508
+ console.error(`❌ Machine not registered. Run \`chatroom machine start\` first.`);
11509
+ process.exit(1);
11510
+ }
11511
+ const config = loadMachineConfig();
11318
11512
  try {
11319
- const machineInfo = ensureMachineRegistered();
11320
- const availableModels = {};
11321
- try {
11322
- const opencodeService = new OpenCodeAgentService;
11323
- if (opencodeService.isInstalled()) {
11324
- availableModels["opencode"] = await opencodeService.listModels();
11325
- }
11326
- } catch {}
11327
- try {
11328
- const piService = new PiAgentService;
11329
- if (piService.isInstalled()) {
11330
- availableModels["pi"] = await piService.listModels();
11331
- }
11332
- } catch {}
11333
- await d.backend.mutation(api.machines.register, {
11513
+ await d.backend.mutation(api.machines.recordAgentRegistered, {
11334
11514
  sessionId,
11335
- machineId: machineInfo.machineId,
11336
- hostname: machineInfo.hostname,
11337
- os: machineInfo.os,
11338
- availableHarnesses: machineInfo.availableHarnesses,
11339
- harnessVersions: machineInfo.harnessVersions,
11340
- availableModels
11515
+ chatroomId,
11516
+ role,
11517
+ agentType: "remote",
11518
+ machineId
11341
11519
  });
11342
- try {
11343
- await d.backend.mutation(api.machines.recordAgentRegistered, {
11344
- sessionId,
11345
- chatroomId,
11346
- role,
11347
- agentType: "remote",
11348
- machineId: machineInfo.machineId
11349
- });
11350
- } catch {}
11351
- console.log(`✅ Registered as remote agent for role "${role}"`);
11352
- console.log(` Machine: ${machineInfo.hostname} (${machineInfo.machineId})`);
11353
- console.log(` Working directory: ${process.cwd()}`);
11354
- } catch (error) {
11355
- console.error(`❌ Registration failed: ${error.message}`);
11356
- process.exit(1);
11357
- }
11520
+ } catch {}
11521
+ console.log(`✅ Registered as remote agent for role "${role}"`);
11522
+ console.log(` Machine: ${config?.hostname ?? "unknown"} (${machineId})`);
11523
+ console.log(` Working directory: ${process.cwd()}`);
11358
11524
  } else {
11359
11525
  try {
11360
11526
  await d.backend.mutation(api.machines.saveTeamAgentConfig, {
@@ -11375,8 +11541,6 @@ var init_register_agent = __esm(() => {
11375
11541
  init_storage();
11376
11542
  init_client2();
11377
11543
  init_machine();
11378
- init_opencode();
11379
- init_pi();
11380
11544
  });
11381
11545
  // ../../services/backend/prompts/cli/task-started/command.ts
11382
11546
  function taskStartedCommand(params) {
@@ -11737,7 +11901,6 @@ var init_session = __esm(() => {
11737
11901
  var exports_get_next_task = {};
11738
11902
  __export(exports_get_next_task, {
11739
11903
  getNextTask: () => getNextTask,
11740
- WaitForTaskSession: () => GetNextTaskSession,
11741
11904
  GetNextTaskSession: () => GetNextTaskSession
11742
11905
  });
11743
11906
  async function getNextTask(chatroomId, options) {
@@ -11789,35 +11952,6 @@ async function getNextTask(chatroomId, options) {
11789
11952
  console.error(`❌ Chatroom ${chatroomId} not found or access denied`);
11790
11953
  process.exit(1);
11791
11954
  }
11792
- try {
11793
- const machineInfo = ensureMachineRegistered();
11794
- const availableModels = {};
11795
- try {
11796
- const opencodeService = new OpenCodeAgentService;
11797
- if (opencodeService.isInstalled()) {
11798
- availableModels["opencode"] = await opencodeService.listModels();
11799
- }
11800
- } catch {}
11801
- try {
11802
- const piService = new PiAgentService;
11803
- if (piService.isInstalled()) {
11804
- availableModels["pi"] = await piService.listModels();
11805
- }
11806
- } catch {}
11807
- await client2.mutation(api.machines.register, {
11808
- sessionId,
11809
- machineId: machineInfo.machineId,
11810
- hostname: machineInfo.hostname,
11811
- os: machineInfo.os,
11812
- availableHarnesses: machineInfo.availableHarnesses,
11813
- harnessVersions: machineInfo.harnessVersions,
11814
- availableModels
11815
- });
11816
- } catch (machineError) {
11817
- if (!silent) {
11818
- console.warn(`⚠️ Machine registration failed: ${sanitizeUnknownForTerminal(machineError.message)}`);
11819
- }
11820
- }
11821
11955
  const connectionId = `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
11822
11956
  let participantAgentType;
11823
11957
  try {
@@ -11850,6 +11984,8 @@ async function getNextTask(chatroomId, options) {
11850
11984
  if (initPromptResult?.prompt) {
11851
11985
  const connectedTime = new Date().toISOString().replace("T", " ").substring(0, 19);
11852
11986
  console.log(`[${connectedTime}] ✅ Connected. Waiting for task...
11987
+ `);
11988
+ console.log(`⚠️ IMPORTANT: This command must stay in the FOREGROUND. If moved to background, terminate and restart it — background processes cannot deliver tasks.
11853
11989
  `);
11854
11990
  if (!initPromptResult.hasSystemPromptControl) {
11855
11991
  console.log("<!-- REFERENCE: Agent Initialization");
@@ -11886,14 +12022,10 @@ var init_get_next_task = __esm(() => {
11886
12022
  init_env();
11887
12023
  init_session();
11888
12024
  init_api3();
11889
- init_storage();
11890
- init_client2();
11891
- init_machine();
11892
- init_opencode();
11893
- init_pi();
12025
+ init_storage();
12026
+ init_client2();
11894
12027
  init_error_formatting();
11895
12028
  init_session();
11896
- init_session();
11897
12029
  });
11898
12030
 
11899
12031
  // src/commands/task-started/index.ts
@@ -12309,7 +12441,15 @@ ${error instanceof Error ? error.message : String(error)}`);
12309
12441
  const cliEnvPrefix2 = getCliEnvPrefix(convexUrl2);
12310
12442
  console.error(`
12311
12443
  ❌ ERROR: ${result.error.message}`);
12312
- if (result.error.suggestedTarget) {
12444
+ if (result.error.code === "INVALID_TARGET_ROLE" && result.error.suggestedTargets) {
12445
+ console.error(`
12446
+ \uD83D\uDCCB Available handoff targets for this team:`);
12447
+ for (const target of result.error.suggestedTargets) {
12448
+ console.error(` • ${target}`);
12449
+ }
12450
+ console.error(`
12451
+ \uD83D\uDCA1 Check your team's workflow in the system prompt for valid handoff paths.`);
12452
+ } else if (result.error.suggestedTarget) {
12313
12453
  console.error(`
12314
12454
  \uD83D\uDCA1 Try this instead:`);
12315
12455
  console.error("```");
@@ -13699,7 +13839,7 @@ function formatTimestamp() {
13699
13839
  async function onAgentShutdown(ctx, options) {
13700
13840
  const { chatroomId, role, pid, skipKill } = options;
13701
13841
  try {
13702
- ctx.deps.stops.mark(chatroomId, role, options.stopReason ?? "intentional_stop");
13842
+ ctx.deps.stops.mark(chatroomId, role, options.stopReason ?? "user.stop");
13703
13843
  } catch (e) {
13704
13844
  console.log(` ⚠️ Failed to mark intentional stop for ${role}: ${e.message}`);
13705
13845
  }
@@ -13801,187 +13941,6 @@ var init_on_daemon_shutdown = __esm(() => {
13801
13941
  init_api3();
13802
13942
  });
13803
13943
 
13804
- // src/infrastructure/machine/stop-reason.ts
13805
- function resolveStopReason(code2, signal, wasIntentional) {
13806
- if (wasIntentional)
13807
- return "intentional_stop";
13808
- if (signal !== null)
13809
- return "process_terminated_with_signal";
13810
- if (code2 === 0)
13811
- return "process_exited_with_success";
13812
- return "process_terminated_unexpectedly";
13813
- }
13814
-
13815
- // src/commands/machine/daemon-start/handlers/start-agent.ts
13816
- async function executeStartAgent(ctx, args) {
13817
- const { chatroomId, role, agentHarness, model, workingDir, reason } = args;
13818
- console.log(` ↪ start-agent command received`);
13819
- console.log(` Chatroom: ${chatroomId}`);
13820
- console.log(` Role: ${role}`);
13821
- console.log(` Harness: ${agentHarness}`);
13822
- if (reason) {
13823
- console.log(` Reason: ${reason}`);
13824
- }
13825
- if (model) {
13826
- console.log(` Model: ${model}`);
13827
- }
13828
- if (!workingDir) {
13829
- const msg2 = `No workingDir provided in command payload for ${chatroomId}/${role}`;
13830
- console.log(` ⚠️ ${msg2}`);
13831
- return { result: msg2, failed: true };
13832
- }
13833
- console.log(` Working dir: ${workingDir}`);
13834
- try {
13835
- const dirStat = await ctx.deps.fs.stat(workingDir);
13836
- if (!dirStat.isDirectory()) {
13837
- const msg2 = `Working directory is not a directory: ${workingDir}`;
13838
- console.log(` ⚠️ ${msg2}`);
13839
- return { result: msg2, failed: true };
13840
- }
13841
- } catch {
13842
- const msg2 = `Working directory does not exist: ${workingDir}`;
13843
- console.log(` ⚠️ ${msg2}`);
13844
- return { result: msg2, failed: true };
13845
- }
13846
- try {
13847
- const existingConfigs = await ctx.deps.backend.query(api.machines.getAgentConfigs, {
13848
- sessionId: ctx.sessionId,
13849
- chatroomId
13850
- });
13851
- const existingConfig = existingConfigs.configs.find((c) => c.machineId === ctx.machineId && c.role.toLowerCase() === role.toLowerCase());
13852
- const backendPid = existingConfig?.spawnedAgentPid;
13853
- const localEntry = ctx.deps.machine.listAgentEntries(ctx.machineId).find((e) => e.chatroomId === chatroomId && e.role.toLowerCase() === role.toLowerCase());
13854
- const localPid = localEntry?.entry.pid;
13855
- const pidsToKill = [
13856
- ...new Set([backendPid, localPid].filter((p) => p !== undefined))
13857
- ];
13858
- const anyService = ctx.agentServices.values().next().value;
13859
- for (const pid2 of pidsToKill) {
13860
- const isAlive = anyService ? anyService.isAlive(pid2) : false;
13861
- if (isAlive) {
13862
- console.log(` ⚠️ Existing agent detected (PID: ${pid2}) — stopping before respawn`);
13863
- await onAgentShutdown(ctx, { chatroomId, role, pid: pid2, stopReason: "daemon_respawn_stop" });
13864
- console.log(` ✅ Existing agent stopped (PID: ${pid2})`);
13865
- }
13866
- }
13867
- } catch (e) {
13868
- console.log(` ⚠️ Could not check for existing agent (proceeding): ${e.message}`);
13869
- }
13870
- const convexUrl = getConvexUrl();
13871
- const initPromptResult = await ctx.deps.backend.query(api.messages.getInitPrompt, {
13872
- sessionId: ctx.sessionId,
13873
- chatroomId,
13874
- role,
13875
- convexUrl
13876
- });
13877
- if (!initPromptResult?.prompt) {
13878
- const msg2 = "Failed to fetch init prompt from backend";
13879
- console.log(` ⚠️ ${msg2}`);
13880
- return { result: msg2, failed: true };
13881
- }
13882
- console.log(` Fetched split init prompt from backend`);
13883
- const service = ctx.agentServices.get(agentHarness);
13884
- if (!service) {
13885
- const msg2 = `Unknown agent harness: ${agentHarness}`;
13886
- console.log(` ⚠️ ${msg2}`);
13887
- return { result: msg2, failed: true };
13888
- }
13889
- let spawnResult;
13890
- try {
13891
- spawnResult = await service.spawn({
13892
- workingDir,
13893
- prompt: initPromptResult.initialMessage,
13894
- systemPrompt: initPromptResult.rolePrompt,
13895
- model,
13896
- context: { machineId: ctx.machineId, chatroomId, role }
13897
- });
13898
- } catch (e) {
13899
- const msg2 = `Failed to spawn agent: ${e.message}`;
13900
- console.log(` ⚠️ ${msg2}`);
13901
- return { result: msg2, failed: true };
13902
- }
13903
- const { pid } = spawnResult;
13904
- const msg = `Agent spawned (PID: ${pid})`;
13905
- console.log(` ✅ ${msg}`);
13906
- try {
13907
- await ctx.deps.backend.mutation(api.machines.updateSpawnedAgent, {
13908
- sessionId: ctx.sessionId,
13909
- machineId: ctx.machineId,
13910
- chatroomId,
13911
- role,
13912
- pid,
13913
- model
13914
- });
13915
- console.log(` Updated backend with PID: ${pid}`);
13916
- ctx.deps.machine.persistAgentPid(ctx.machineId, chatroomId, role, pid, agentHarness);
13917
- } catch (e) {
13918
- console.log(` ⚠️ Failed to update PID in backend: ${e.message}`);
13919
- }
13920
- ctx.events.emit("agent:started", {
13921
- chatroomId,
13922
- role,
13923
- pid,
13924
- harness: agentHarness,
13925
- model
13926
- });
13927
- spawnResult.onExit(({ code: code2, signal }) => {
13928
- const pendingReason = ctx.deps.stops.consume(chatroomId, role);
13929
- const stopReason = pendingReason ?? resolveStopReason(code2, signal, false);
13930
- ctx.events.emit("agent:exited", {
13931
- chatroomId,
13932
- role,
13933
- pid,
13934
- code: code2,
13935
- signal,
13936
- stopReason,
13937
- intentional: pendingReason !== null
13938
- });
13939
- });
13940
- if (spawnResult.onAgentEnd) {
13941
- spawnResult.onAgentEnd(() => {
13942
- try {
13943
- ctx.deps.processes.kill(-pid, "SIGTERM");
13944
- } catch {}
13945
- });
13946
- }
13947
- let lastReportedTokenAt = 0;
13948
- spawnResult.onOutput(() => {
13949
- const now = Date.now();
13950
- if (now - lastReportedTokenAt >= 30000) {
13951
- lastReportedTokenAt = now;
13952
- ctx.deps.backend.mutation(api.participants.updateTokenActivity, {
13953
- sessionId: ctx.sessionId,
13954
- chatroomId,
13955
- role
13956
- }).catch(() => {});
13957
- }
13958
- });
13959
- return { result: msg, failed: false };
13960
- }
13961
- var init_start_agent = __esm(() => {
13962
- init_api3();
13963
- init_client2();
13964
- });
13965
-
13966
- // src/events/daemon/agent/on-request-start-agent.ts
13967
- async function onRequestStartAgent(ctx, event) {
13968
- if (Date.now() > event.deadline) {
13969
- console.log(`[daemon] ⏰ Skipping expired agent.requestStart for role=${event.role} (deadline passed)`);
13970
- return;
13971
- }
13972
- await executeStartAgent(ctx, {
13973
- chatroomId: event.chatroomId,
13974
- role: event.role,
13975
- agentHarness: event.agentHarness,
13976
- model: event.model,
13977
- workingDir: event.workingDir,
13978
- reason: event.reason
13979
- });
13980
- }
13981
- var init_on_request_start_agent = __esm(() => {
13982
- init_start_agent();
13983
- });
13984
-
13985
13944
  // src/commands/machine/daemon-start/handlers/shared.ts
13986
13945
  async function clearAgentPidEverywhere(ctx, chatroomId, role) {
13987
13946
  try {
@@ -13989,107 +13948,44 @@ async function clearAgentPidEverywhere(ctx, chatroomId, role) {
13989
13948
  sessionId: ctx.sessionId,
13990
13949
  machineId: ctx.machineId,
13991
13950
  chatroomId,
13992
- role,
13993
- pid: undefined
13994
- });
13995
- } catch (e) {
13996
- console.log(` ⚠️ Failed to clear PID in backend: ${e.message}`);
13997
- }
13998
- ctx.deps.machine.clearAgentPid(ctx.machineId, chatroomId, role);
13999
- }
14000
- var init_shared = __esm(() => {
14001
- init_api3();
14002
- });
14003
-
14004
- // src/commands/machine/daemon-start/handlers/stop-agent.ts
14005
- async function executeStopAgent(ctx, args) {
14006
- const { chatroomId, role } = args;
14007
- console.log(` ↪ stop-agent command received`);
14008
- console.log(` Chatroom: ${chatroomId}`);
14009
- console.log(` Role: ${role}`);
14010
- const configsResult = await ctx.deps.backend.query(api.machines.getAgentConfigs, {
14011
- sessionId: ctx.sessionId,
14012
- chatroomId
14013
- });
14014
- const targetConfig = configsResult.configs.find((c) => c.machineId === ctx.machineId && c.role.toLowerCase() === role.toLowerCase());
14015
- const backendPid = targetConfig?.spawnedAgentPid;
14016
- const localEntry = ctx.deps.machine.listAgentEntries(ctx.machineId).find((e) => e.chatroomId === chatroomId && e.role.toLowerCase() === role.toLowerCase());
14017
- const localPid = localEntry?.entry.pid;
14018
- const allPids = [...new Set([backendPid, localPid].filter((p) => p !== undefined))];
14019
- if (allPids.length === 0) {
14020
- const msg = "No running agent found (no PID recorded)";
14021
- console.log(` ⚠️ ${msg}`);
14022
- return { result: msg, failed: true };
14023
- }
14024
- const anyService = ctx.agentServices.values().next().value;
14025
- let anyKilled = false;
14026
- let lastError = null;
14027
- for (const pid of allPids) {
14028
- console.log(` Stopping agent with PID: ${pid}`);
14029
- const isAlive = anyService ? anyService.isAlive(pid) : false;
14030
- if (!isAlive) {
14031
- console.log(` ⚠️ PID ${pid} not found — process already exited or was never started`);
14032
- await clearAgentPidEverywhere(ctx, chatroomId, role);
14033
- console.log(` Cleared stale PID`);
14034
- try {
14035
- await ctx.deps.backend.mutation(api.participants.leave, {
14036
- sessionId: ctx.sessionId,
14037
- chatroomId,
14038
- role
14039
- });
14040
- console.log(` Removed participant record`);
14041
- } catch {}
14042
- continue;
14043
- }
14044
- try {
14045
- const shutdownResult = await onAgentShutdown(ctx, {
14046
- chatroomId,
14047
- role,
14048
- pid
14049
- });
14050
- const msg = shutdownResult.killed ? `Agent stopped (PID: ${pid})` : `Agent stop attempted (PID: ${pid}) — process may still be running`;
14051
- console.log(` ${shutdownResult.killed ? "✅" : "⚠️ "} ${msg}`);
14052
- if (shutdownResult.killed) {
14053
- anyKilled = true;
14054
- }
14055
- } catch (e) {
14056
- lastError = e;
14057
- console.log(` ⚠️ Failed to stop agent (PID: ${pid}): ${e.message}`);
14058
- }
14059
- }
14060
- if (lastError && !anyKilled) {
14061
- const msg = `Failed to stop agent: ${lastError.message}`;
14062
- console.log(` ⚠️ ${msg}`);
14063
- return { result: msg, failed: true };
14064
- }
14065
- if (!anyKilled) {
14066
- return {
14067
- result: `All recorded PIDs appear stale (processes not found or belong to different programs)`,
14068
- failed: true
14069
- };
13951
+ role,
13952
+ pid: undefined
13953
+ });
13954
+ } catch (e) {
13955
+ console.log(` ⚠️ Failed to clear PID in backend: ${e.message}`);
14070
13956
  }
14071
- const killedCount = allPids.length > 1 ? ` (${allPids.length} PIDs)` : ``;
14072
- return { result: `Agent stopped${killedCount}`, failed: false };
13957
+ ctx.deps.machine.clearAgentPid(ctx.machineId, chatroomId, role);
14073
13958
  }
14074
- var init_stop_agent = __esm(() => {
13959
+ var init_shared = __esm(() => {
14075
13960
  init_api3();
14076
- init_shared();
14077
13961
  });
14078
13962
 
14079
- // src/events/daemon/agent/on-request-stop-agent.ts
14080
- async function onRequestStopAgent(ctx, event) {
14081
- if (Date.now() > event.deadline) {
14082
- console.log(`[daemon] Skipping expired agent.requestStop for role=${event.role} (deadline passed)`);
13963
+ // src/commands/machine/daemon-start/handlers/state-recovery.ts
13964
+ async function recoverAgentState(ctx) {
13965
+ const entries = ctx.deps.machine.listAgentEntries(ctx.machineId);
13966
+ if (entries.length === 0) {
13967
+ console.log(` No agent entries found — nothing to recover`);
14083
13968
  return;
14084
13969
  }
14085
- await executeStopAgent(ctx, {
14086
- chatroomId: event.chatroomId,
14087
- role: event.role,
14088
- reason: event.reason
14089
- });
13970
+ let recovered = 0;
13971
+ let cleared = 0;
13972
+ for (const { chatroomId, role, entry } of entries) {
13973
+ const { pid, harness } = entry;
13974
+ const service = ctx.agentServices.get(harness) ?? ctx.agentServices.values().next().value;
13975
+ const alive = service ? service.isAlive(pid) : false;
13976
+ if (alive) {
13977
+ console.log(` ✅ Recovered: ${role} (PID ${pid}, harness: ${harness})`);
13978
+ recovered++;
13979
+ } else {
13980
+ console.log(` \uD83E\uDDF9 Stale PID ${pid} for ${role} — clearing`);
13981
+ await clearAgentPidEverywhere(ctx, chatroomId, role);
13982
+ cleared++;
13983
+ }
13984
+ }
13985
+ console.log(` Recovery complete: ${recovered} alive, ${cleared} stale cleared`);
14090
13986
  }
14091
- var init_on_request_stop_agent = __esm(() => {
14092
- init_stop_agent();
13987
+ var init_state_recovery = __esm(() => {
13988
+ init_shared();
14093
13989
  });
14094
13990
 
14095
13991
  // src/commands/machine/pid.ts
@@ -14178,459 +14074,693 @@ var init_pid = __esm(() => {
14178
14074
  CHATROOM_DIR4 = join5(homedir4(), ".chatroom");
14179
14075
  });
14180
14076
 
14181
- // src/commands/machine/daemon-start/handlers/ping.ts
14182
- function handlePing() {
14183
- console.log(` ↪ Responding: pong`);
14184
- return { result: "pong", failed: false };
14077
+ // src/events/daemon/event-bus.ts
14078
+ class DaemonEventBus {
14079
+ listeners = new Map;
14080
+ on(event, listener) {
14081
+ if (!this.listeners.has(event)) {
14082
+ this.listeners.set(event, new Set);
14083
+ }
14084
+ this.listeners.get(event).add(listener);
14085
+ return () => {
14086
+ this.listeners.get(event)?.delete(listener);
14087
+ };
14088
+ }
14089
+ emit(event, payload) {
14090
+ const set = this.listeners.get(event);
14091
+ if (!set)
14092
+ return;
14093
+ for (const listener of set) {
14094
+ try {
14095
+ listener(payload);
14096
+ } catch (err) {
14097
+ console.warn(`[EventBus] Listener error on "${event}": ${err.message}`);
14098
+ }
14099
+ }
14100
+ }
14101
+ removeAllListeners() {
14102
+ this.listeners.clear();
14103
+ }
14185
14104
  }
14186
14105
 
14187
- // src/commands/machine/daemon-start/command-loop.ts
14188
- async function refreshModels(ctx) {
14189
- const models = {};
14190
- for (const [harness, service] of ctx.agentServices) {
14191
- if (!service.isInstalled())
14192
- continue;
14193
- try {
14194
- models[harness] = await service.listModels();
14195
- } catch {}
14106
+ // src/events/daemon/agent/on-agent-exited.ts
14107
+ function onAgentExited(ctx, payload) {
14108
+ const { chatroomId, role, pid, code: code2, signal, stopReason, intentional } = payload;
14109
+ const ts = formatTimestamp();
14110
+ console.log(`[${ts}] Agent stopped: ${stopReason} (${role})`);
14111
+ const isDaemonRespawn = stopReason === "daemon.respawn";
14112
+ const isIntentional = intentional && !isDaemonRespawn;
14113
+ if (isIntentional) {
14114
+ console.log(`[${ts}] ℹ️ Agent process exited after intentional stop ` + `(PID: ${pid}, role: ${role}, code: ${code2}, signal: ${signal})`);
14115
+ } else if (isDaemonRespawn) {
14116
+ console.log(`[${ts}] \uD83D\uDD04 Agent process stopped for respawn ` + `(PID: ${pid}, role: ${role}, code: ${code2}, signal: ${signal})`);
14117
+ } else {
14118
+ console.log(`[${ts}] ⚠️ Agent process exited ` + `(PID: ${pid}, role: ${role}, code: ${code2}, signal: ${signal})`);
14196
14119
  }
14197
- if (!ctx.config)
14198
- return;
14199
- const freshConfig = ensureMachineRegistered();
14200
- ctx.config.availableHarnesses = freshConfig.availableHarnesses;
14201
- ctx.config.harnessVersions = freshConfig.harnessVersions;
14202
- const totalCount = Object.values(models).flat().length;
14120
+ ctx.deps.backend.mutation(api.machines.recordAgentExited, {
14121
+ sessionId: ctx.sessionId,
14122
+ machineId: ctx.machineId,
14123
+ chatroomId,
14124
+ role,
14125
+ pid,
14126
+ intentional,
14127
+ stopReason,
14128
+ stopSignal: stopReason === "agent_process.signal" ? signal ?? undefined : undefined,
14129
+ exitCode: code2 ?? undefined,
14130
+ signal: signal ?? undefined
14131
+ }).catch((err) => {
14132
+ console.log(` ⚠️ Failed to record agent exit event: ${err.message}`);
14133
+ });
14134
+ ctx.deps.machine.clearAgentPid(ctx.machineId, chatroomId, role);
14135
+ for (const service of ctx.agentServices.values()) {
14136
+ service.untrack(pid);
14137
+ }
14138
+ }
14139
+ var init_on_agent_exited = __esm(() => {
14140
+ init_api3();
14141
+ });
14142
+
14143
+ // src/events/daemon/agent/on-agent-started.ts
14144
+ function onAgentStarted(ctx, payload) {
14145
+ const ts = formatTimestamp();
14146
+ console.log(`[${ts}] \uD83D\uDFE2 Agent started: ${payload.role} (PID: ${payload.pid}, harness: ${payload.harness})`);
14147
+ }
14148
+ var init_on_agent_started = () => {};
14149
+
14150
+ // src/events/daemon/agent/on-agent-stopped.ts
14151
+ function onAgentStopped(ctx, payload) {
14152
+ const ts = formatTimestamp();
14153
+ console.log(`[${ts}] \uD83D\uDD34 Agent stopped: ${payload.role} (PID: ${payload.pid})`);
14154
+ }
14155
+ var init_on_agent_stopped = () => {};
14156
+
14157
+ // src/events/daemon/register-listeners.ts
14158
+ function registerEventListeners(ctx) {
14159
+ const unsubs = [];
14160
+ unsubs.push(ctx.events.on("agent:exited", (payload) => onAgentExited(ctx, payload)));
14161
+ unsubs.push(ctx.events.on("agent:started", (payload) => onAgentStarted(ctx, payload)));
14162
+ unsubs.push(ctx.events.on("agent:stopped", (payload) => onAgentStopped(ctx, payload)));
14163
+ return () => {
14164
+ for (const unsub of unsubs) {
14165
+ unsub();
14166
+ }
14167
+ };
14168
+ }
14169
+ var init_register_listeners = __esm(() => {
14170
+ init_on_agent_exited();
14171
+ init_on_agent_started();
14172
+ init_on_agent_stopped();
14173
+ });
14174
+
14175
+ // src/commands/machine/daemon-start/init.ts
14176
+ import { stat } from "node:fs/promises";
14177
+ async function discoverModels(agentServices) {
14178
+ const results = {};
14179
+ for (const [harness, service] of agentServices) {
14180
+ if (service.isInstalled()) {
14181
+ try {
14182
+ results[harness] = await service.listModels();
14183
+ } catch {
14184
+ results[harness] = [];
14185
+ }
14186
+ }
14187
+ }
14188
+ return results;
14189
+ }
14190
+ function createDefaultDeps16() {
14191
+ return {
14192
+ backend: {
14193
+ mutation: async () => {
14194
+ throw new Error("Backend not initialized");
14195
+ },
14196
+ query: async () => {
14197
+ throw new Error("Backend not initialized");
14198
+ }
14199
+ },
14200
+ processes: {
14201
+ kill: (pid, signal) => process.kill(pid, signal)
14202
+ },
14203
+ fs: {
14204
+ stat
14205
+ },
14206
+ stops: {
14207
+ mark: markIntentionalStop,
14208
+ consume: consumeIntentionalStop,
14209
+ clear: clearIntentionalStop
14210
+ },
14211
+ machine: {
14212
+ clearAgentPid,
14213
+ persistAgentPid,
14214
+ listAgentEntries,
14215
+ persistEventCursor,
14216
+ loadEventCursor
14217
+ },
14218
+ clock: {
14219
+ now: () => Date.now(),
14220
+ delay: (ms) => new Promise((resolve2) => setTimeout(resolve2, ms))
14221
+ }
14222
+ };
14223
+ }
14224
+ function validateAuthentication(convexUrl) {
14225
+ const sessionId = getSessionId();
14226
+ if (!sessionId) {
14227
+ const otherUrls = getOtherSessionUrls();
14228
+ console.error(`❌ Not authenticated for: ${convexUrl}`);
14229
+ if (otherUrls.length > 0) {
14230
+ console.error(`
14231
+ \uD83D\uDCA1 You have sessions for other environments:`);
14232
+ for (const url of otherUrls) {
14233
+ console.error(` • ${url}`);
14234
+ }
14235
+ }
14236
+ console.error(`
14237
+ Run: chatroom auth login`);
14238
+ releaseLock();
14239
+ process.exit(1);
14240
+ }
14241
+ return sessionId;
14242
+ }
14243
+ async function validateSession(client2, sessionId, convexUrl) {
14244
+ const validation = await client2.query(api.cliAuth.validateSession, { sessionId });
14245
+ if (!validation.valid) {
14246
+ console.error(`❌ Session invalid: ${validation.reason}`);
14247
+ console.error(`
14248
+ Run: chatroom auth login`);
14249
+ releaseLock();
14250
+ process.exit(1);
14251
+ }
14252
+ }
14253
+ function setupMachine() {
14254
+ ensureMachineRegistered();
14255
+ const config3 = loadMachineConfig();
14256
+ return config3;
14257
+ }
14258
+ async function registerCapabilities(client2, sessionId, config3, agentServices) {
14259
+ const { machineId } = config3;
14260
+ const availableModels = await discoverModels(agentServices);
14203
14261
  try {
14204
- await ctx.deps.backend.mutation(api.machines.register, {
14205
- sessionId: ctx.sessionId,
14206
- machineId: ctx.machineId,
14207
- hostname: ctx.config.hostname,
14208
- os: ctx.config.os,
14209
- availableHarnesses: ctx.config.availableHarnesses,
14210
- harnessVersions: ctx.config.harnessVersions,
14211
- availableModels: models
14262
+ await client2.mutation(api.machines.register, {
14263
+ sessionId,
14264
+ machineId,
14265
+ hostname: config3.hostname,
14266
+ os: config3.os,
14267
+ availableHarnesses: config3.availableHarnesses,
14268
+ harnessVersions: config3.harnessVersions,
14269
+ availableModels
14270
+ });
14271
+ } catch (error) {
14272
+ console.warn(`⚠️ Machine registration update failed: ${error.message}`);
14273
+ }
14274
+ return availableModels;
14275
+ }
14276
+ async function connectDaemon(client2, sessionId, machineId, convexUrl) {
14277
+ try {
14278
+ await client2.mutation(api.machines.updateDaemonStatus, {
14279
+ sessionId,
14280
+ machineId,
14281
+ connected: true
14212
14282
  });
14213
- console.log(`[${formatTimestamp()}] \uD83D\uDD04 Model refresh: ${totalCount > 0 ? `${totalCount} models` : "none discovered"}`);
14214
14283
  } catch (error) {
14215
- console.warn(`[${formatTimestamp()}] ⚠️ Model refresh failed: ${error.message}`);
14284
+ if (isNetworkError(error)) {
14285
+ formatConnectivityError(error, convexUrl);
14286
+ } else {
14287
+ console.error(`❌ Failed to update daemon status: ${error.message}`);
14288
+ }
14289
+ releaseLock();
14290
+ process.exit(1);
14216
14291
  }
14217
14292
  }
14218
- function evictStaleDedupEntries(processedCommandIds, processedPingIds) {
14219
- const evictBefore = Date.now() - AGENT_REQUEST_DEADLINE_MS;
14220
- for (const [id, ts] of processedCommandIds) {
14221
- if (ts < evictBefore)
14222
- processedCommandIds.delete(id);
14223
- }
14224
- for (const [id, ts] of processedPingIds) {
14225
- if (ts < evictBefore)
14226
- processedPingIds.delete(id);
14227
- }
14293
+ function logStartup(ctx, availableModels) {
14294
+ console.log(`[${formatTimestamp()}] \uD83D\uDE80 Daemon started`);
14295
+ console.log(` CLI version: ${getVersion()}`);
14296
+ console.log(` Machine ID: ${ctx.machineId}`);
14297
+ console.log(` Hostname: ${ctx.config?.hostname ?? "unknown"}`);
14298
+ console.log(` Available harnesses: ${ctx.config?.availableHarnesses.join(", ") || "none"}`);
14299
+ console.log(` Available models: ${Object.keys(availableModels).length > 0 ? `${Object.values(availableModels).flat().length} models across ${Object.keys(availableModels).join(", ")}` : "none discovered"}`);
14300
+ console.log(` PID: ${process.pid}`);
14228
14301
  }
14229
- async function dispatchCommandEvent(ctx, event, processedCommandIds, processedPingIds) {
14230
- const eventId = event._id.toString();
14231
- if (event.type === "agent.requestStart") {
14232
- if (processedCommandIds.has(eventId))
14233
- return;
14234
- processedCommandIds.set(eventId, Date.now());
14235
- await onRequestStartAgent(ctx, event);
14236
- } else if (event.type === "agent.requestStop") {
14237
- if (processedCommandIds.has(eventId))
14238
- return;
14239
- processedCommandIds.set(eventId, Date.now());
14240
- await onRequestStopAgent(ctx, event);
14241
- } else if (event.type === "daemon.ping") {
14242
- if (processedPingIds.has(eventId))
14243
- return;
14244
- processedPingIds.set(eventId, Date.now());
14245
- handlePing();
14246
- await ctx.deps.backend.mutation(api.machines.ackPing, {
14247
- sessionId: ctx.sessionId,
14248
- machineId: ctx.machineId,
14249
- pingEventId: event._id
14250
- });
14302
+ async function recoverState(ctx) {
14303
+ console.log(`
14304
+ [${formatTimestamp()}] \uD83D\uDD04 Recovering agent state...`);
14305
+ try {
14306
+ await recoverAgentState(ctx);
14307
+ } catch (e) {
14308
+ console.log(` ⚠️ Recovery failed: ${e.message}`);
14309
+ console.log(` Continuing with fresh state`);
14251
14310
  }
14252
14311
  }
14253
- async function startCommandLoop(ctx) {
14254
- let heartbeatCount = 0;
14255
- const heartbeatTimer = setInterval(() => {
14256
- ctx.deps.backend.mutation(api.machines.daemonHeartbeat, {
14257
- sessionId: ctx.sessionId,
14258
- machineId: ctx.machineId
14259
- }).then(() => {
14260
- heartbeatCount++;
14261
- console.log(`[${formatTimestamp()}] \uD83D\uDC93 Daemon heartbeat #${heartbeatCount} OK`);
14262
- }).catch((err) => {
14263
- console.warn(`[${formatTimestamp()}] ⚠️ Daemon heartbeat failed: ${err.message}`);
14264
- });
14265
- }, DAEMON_HEARTBEAT_INTERVAL_MS);
14266
- heartbeatTimer.unref();
14267
- const shutdown = async () => {
14268
- console.log(`
14269
- [${formatTimestamp()}] Shutting down...`);
14270
- clearInterval(heartbeatTimer);
14271
- await onDaemonShutdown(ctx);
14272
- releaseLock();
14273
- process.exit(0);
14312
+ async function initDaemon() {
14313
+ if (!acquireLock()) {
14314
+ process.exit(1);
14315
+ }
14316
+ const convexUrl = getConvexUrl();
14317
+ const sessionId = validateAuthentication(convexUrl);
14318
+ const client2 = await getConvexClient();
14319
+ const typedSessionId = sessionId;
14320
+ await validateSession(client2, typedSessionId, convexUrl);
14321
+ const config3 = setupMachine();
14322
+ const { machineId } = config3;
14323
+ initHarnessRegistry();
14324
+ const agentServices = new Map(getAllHarnesses().map((s) => [s.id, s]));
14325
+ const availableModels = await registerCapabilities(client2, typedSessionId, config3, agentServices);
14326
+ await connectDaemon(client2, typedSessionId, machineId, convexUrl);
14327
+ const deps = createDefaultDeps16();
14328
+ deps.backend.mutation = (endpoint, args) => client2.mutation(endpoint, args);
14329
+ deps.backend.query = (endpoint, args) => client2.query(endpoint, args);
14330
+ const events = new DaemonEventBus;
14331
+ const ctx = {
14332
+ client: client2,
14333
+ sessionId: typedSessionId,
14334
+ machineId,
14335
+ config: config3,
14336
+ deps,
14337
+ events,
14338
+ agentServices
14274
14339
  };
14275
- process.on("SIGINT", shutdown);
14276
- process.on("SIGTERM", shutdown);
14277
- process.on("SIGHUP", shutdown);
14278
- const wsClient2 = await getConvexWsClient();
14279
- console.log(`
14280
- Listening for commands...`);
14281
- console.log(`Press Ctrl+C to stop
14282
- `);
14283
- const processedCommandIds = new Map;
14284
- const processedPingIds = new Map;
14285
- wsClient2.onUpdate(api.machines.getCommandEvents, {
14286
- sessionId: ctx.sessionId,
14287
- machineId: ctx.machineId
14288
- }, async (result) => {
14289
- if (!result.events || result.events.length === 0)
14290
- return;
14291
- evictStaleDedupEntries(processedCommandIds, processedPingIds);
14292
- for (const event of result.events) {
14293
- try {
14294
- console.log(`[${formatTimestamp()}] \uD83D\uDCE1 Stream command event: ${event.type}`);
14295
- await dispatchCommandEvent(ctx, event, processedCommandIds, processedPingIds);
14296
- } catch (err) {
14297
- console.error(`[${formatTimestamp()}] ❌ Stream command event failed: ${err.message}`);
14298
- }
14299
- }
14300
- });
14301
- const modelRefreshTimer = setInterval(() => {
14302
- refreshModels(ctx).catch((err) => {
14303
- console.warn(`[${formatTimestamp()}] ⚠️ Model refresh error: ${err.message}`);
14304
- });
14305
- }, MODEL_REFRESH_INTERVAL_MS);
14306
- modelRefreshTimer.unref();
14307
- return await new Promise(() => {});
14340
+ registerEventListeners(ctx);
14341
+ logStartup(ctx, availableModels);
14342
+ await recoverState(ctx);
14343
+ return ctx;
14308
14344
  }
14309
- var MODEL_REFRESH_INTERVAL_MS;
14310
- var init_command_loop = __esm(() => {
14345
+ var init_init2 = __esm(() => {
14346
+ init_state_recovery();
14311
14347
  init_api3();
14348
+ init_storage();
14312
14349
  init_client2();
14313
14350
  init_machine();
14314
- init_on_daemon_shutdown();
14315
- init_on_request_start_agent();
14316
- init_on_request_stop_agent();
14351
+ init_intentional_stops();
14352
+ init_remote_agents();
14353
+ init_error_formatting();
14354
+ init_version();
14317
14355
  init_pid();
14318
- MODEL_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
14356
+ init_register_listeners();
14319
14357
  });
14320
14358
 
14321
- // src/commands/machine/daemon-start/handlers/state-recovery.ts
14322
- async function recoverAgentState(ctx) {
14323
- const entries = ctx.deps.machine.listAgentEntries(ctx.machineId);
14324
- if (entries.length === 0) {
14325
- console.log(` No agent entries found — nothing to recover`);
14326
- return;
14327
- }
14328
- let recovered = 0;
14329
- let cleared = 0;
14330
- for (const { chatroomId, role, entry } of entries) {
14331
- const { pid, harness } = entry;
14332
- const service = ctx.agentServices.get(harness) ?? ctx.agentServices.values().next().value;
14333
- const alive = service ? service.isAlive(pid) : false;
14334
- if (alive) {
14335
- console.log(` ✅ Recovered: ${role} (PID ${pid}, harness: ${harness})`);
14336
- recovered++;
14337
- } else {
14338
- console.log(` \uD83E\uDDF9 Stale PID ${pid} for ${role} — clearing`);
14339
- await clearAgentPidEverywhere(ctx, chatroomId, role);
14340
- cleared++;
14341
- }
14342
- }
14343
- console.log(` Recovery complete: ${recovered} alive, ${cleared} stale cleared`);
14359
+ // src/infrastructure/machine/stop-reason.ts
14360
+ function resolveStopReason(code2, signal, wasIntentional) {
14361
+ if (wasIntentional)
14362
+ return "user.stop";
14363
+ if (signal !== null)
14364
+ return "agent_process.signal";
14365
+ if (code2 === 0)
14366
+ return "agent_process.exited_clean";
14367
+ return "agent_process.crashed";
14344
14368
  }
14345
- var init_state_recovery = __esm(() => {
14346
- init_shared();
14347
- });
14348
14369
 
14349
- // src/events/daemon/event-bus.ts
14350
- class DaemonEventBus {
14351
- listeners = new Map;
14352
- on(event, listener) {
14353
- if (!this.listeners.has(event)) {
14354
- this.listeners.set(event, new Set);
14370
+ // src/commands/machine/daemon-start/handlers/start-agent.ts
14371
+ async function executeStartAgent(ctx, args) {
14372
+ const { chatroomId, role, agentHarness, model, workingDir, reason } = args;
14373
+ console.log(` ↪ start-agent command received`);
14374
+ console.log(` Chatroom: ${chatroomId}`);
14375
+ console.log(` Role: ${role}`);
14376
+ console.log(` Harness: ${agentHarness}`);
14377
+ if (reason) {
14378
+ console.log(` Reason: ${reason}`);
14379
+ }
14380
+ if (model) {
14381
+ console.log(` Model: ${model}`);
14382
+ }
14383
+ if (!workingDir) {
14384
+ const msg2 = `No workingDir provided in command payload for ${chatroomId}/${role}`;
14385
+ console.log(` ⚠️ ${msg2}`);
14386
+ return { result: msg2, failed: true };
14387
+ }
14388
+ console.log(` Working dir: ${workingDir}`);
14389
+ try {
14390
+ const dirStat = await ctx.deps.fs.stat(workingDir);
14391
+ if (!dirStat.isDirectory()) {
14392
+ const msg2 = `Working directory is not a directory: ${workingDir}`;
14393
+ console.log(` ⚠️ ${msg2}`);
14394
+ return { result: msg2, failed: true };
14355
14395
  }
14356
- this.listeners.get(event).add(listener);
14357
- return () => {
14358
- this.listeners.get(event)?.delete(listener);
14359
- };
14396
+ } catch {
14397
+ const msg2 = `Working directory does not exist: ${workingDir}`;
14398
+ console.log(` ⚠️ ${msg2}`);
14399
+ return { result: msg2, failed: true };
14360
14400
  }
14361
- emit(event, payload) {
14362
- const set = this.listeners.get(event);
14363
- if (!set)
14364
- return;
14365
- for (const listener of set) {
14366
- try {
14367
- listener(payload);
14368
- } catch (err) {
14369
- console.warn(`[EventBus] Listener error on "${event}": ${err.message}`);
14401
+ try {
14402
+ const existingConfigs = await ctx.deps.backend.query(api.machines.getMachineAgentConfigs, {
14403
+ sessionId: ctx.sessionId,
14404
+ chatroomId
14405
+ });
14406
+ const existingConfig = existingConfigs.configs.find((c) => c.machineId === ctx.machineId && c.role.toLowerCase() === role.toLowerCase());
14407
+ const backendPid = existingConfig?.spawnedAgentPid;
14408
+ const localEntry = ctx.deps.machine.listAgentEntries(ctx.machineId).find((e) => e.chatroomId === chatroomId && e.role.toLowerCase() === role.toLowerCase());
14409
+ const localPid = localEntry?.entry.pid;
14410
+ const pidsToKill = [
14411
+ ...new Set([backendPid, localPid].filter((p) => p !== undefined))
14412
+ ];
14413
+ const anyService = ctx.agentServices.values().next().value;
14414
+ for (const pid2 of pidsToKill) {
14415
+ const isAlive = anyService ? anyService.isAlive(pid2) : false;
14416
+ if (isAlive) {
14417
+ console.log(` ⚠️ Existing agent detected (PID: ${pid2}) — stopping before respawn`);
14418
+ await onAgentShutdown(ctx, { chatroomId, role, pid: pid2, stopReason: "daemon.respawn" });
14419
+ console.log(` ✅ Existing agent stopped (PID: ${pid2})`);
14370
14420
  }
14371
14421
  }
14422
+ } catch (e) {
14423
+ console.log(` ⚠️ Could not check for existing agent (proceeding): ${e.message}`);
14372
14424
  }
14373
- removeAllListeners() {
14374
- this.listeners.clear();
14425
+ const convexUrl = getConvexUrl();
14426
+ const initPromptResult = await ctx.deps.backend.query(api.messages.getInitPrompt, {
14427
+ sessionId: ctx.sessionId,
14428
+ chatroomId,
14429
+ role,
14430
+ convexUrl
14431
+ });
14432
+ if (!initPromptResult?.prompt) {
14433
+ const msg2 = "Failed to fetch init prompt from backend";
14434
+ console.log(` ⚠️ ${msg2}`);
14435
+ return { result: msg2, failed: true };
14375
14436
  }
14376
- }
14377
-
14378
- // src/events/daemon/agent/on-agent-exited.ts
14379
- function onAgentExited(ctx, payload) {
14380
- const { chatroomId, role, pid, code: code2, signal, stopReason, intentional } = payload;
14381
- const ts = formatTimestamp();
14382
- console.log(`[${ts}] Agent stopped: ${stopReason} (${role})`);
14383
- const isDaemonRespawn = stopReason === "daemon_respawn_stop";
14384
- const isIntentional = intentional && !isDaemonRespawn;
14385
- if (isIntentional) {
14386
- console.log(`[${ts}] ℹ️ Agent process exited after intentional stop ` + `(PID: ${pid}, role: ${role}, code: ${code2}, signal: ${signal})`);
14387
- } else if (isDaemonRespawn) {
14388
- console.log(`[${ts}] \uD83D\uDD04 Agent process stopped for respawn ` + `(PID: ${pid}, role: ${role}, code: ${code2}, signal: ${signal})`);
14389
- } else {
14390
- console.log(`[${ts}] ⚠️ Agent process exited ` + `(PID: ${pid}, role: ${role}, code: ${code2}, signal: ${signal})`);
14437
+ console.log(` Fetched split init prompt from backend`);
14438
+ const service = ctx.agentServices.get(agentHarness);
14439
+ if (!service) {
14440
+ const msg2 = `Unknown agent harness: ${agentHarness}`;
14441
+ console.log(` ⚠️ ${msg2}`);
14442
+ return { result: msg2, failed: true };
14391
14443
  }
14392
- ctx.deps.backend.mutation(api.machines.recordAgentExited, {
14393
- sessionId: ctx.sessionId,
14394
- machineId: ctx.machineId,
14444
+ let spawnResult;
14445
+ try {
14446
+ spawnResult = await service.spawn({
14447
+ workingDir,
14448
+ prompt: initPromptResult.initialMessage,
14449
+ systemPrompt: initPromptResult.rolePrompt,
14450
+ model,
14451
+ context: { machineId: ctx.machineId, chatroomId, role }
14452
+ });
14453
+ } catch (e) {
14454
+ const msg2 = `Failed to spawn agent: ${e.message}`;
14455
+ console.log(` ⚠️ ${msg2}`);
14456
+ return { result: msg2, failed: true };
14457
+ }
14458
+ const { pid } = spawnResult;
14459
+ const msg = `Agent spawned (PID: ${pid})`;
14460
+ console.log(` ✅ ${msg}`);
14461
+ try {
14462
+ await ctx.deps.backend.mutation(api.machines.updateSpawnedAgent, {
14463
+ sessionId: ctx.sessionId,
14464
+ machineId: ctx.machineId,
14465
+ chatroomId,
14466
+ role,
14467
+ pid,
14468
+ model
14469
+ });
14470
+ console.log(` Updated backend with PID: ${pid}`);
14471
+ ctx.deps.machine.persistAgentPid(ctx.machineId, chatroomId, role, pid, agentHarness);
14472
+ } catch (e) {
14473
+ console.log(` ⚠️ Failed to update PID in backend: ${e.message}`);
14474
+ }
14475
+ ctx.events.emit("agent:started", {
14395
14476
  chatroomId,
14396
14477
  role,
14397
14478
  pid,
14398
- intentional,
14399
- stopReason,
14400
- stopSignal: stopReason === "process_terminated_with_signal" ? signal ?? undefined : undefined,
14401
- exitCode: code2 ?? undefined,
14402
- signal: signal ?? undefined
14403
- }).catch((err) => {
14404
- console.log(` ⚠️ Failed to record agent exit event: ${err.message}`);
14479
+ harness: agentHarness,
14480
+ model
14405
14481
  });
14406
- ctx.deps.machine.clearAgentPid(ctx.machineId, chatroomId, role);
14407
- for (const service of ctx.agentServices.values()) {
14408
- service.untrack(pid);
14482
+ spawnResult.onExit(({ code: code2, signal }) => {
14483
+ const pendingReason = ctx.deps.stops.consume(chatroomId, role);
14484
+ const stopReason = pendingReason ?? resolveStopReason(code2, signal, false);
14485
+ ctx.events.emit("agent:exited", {
14486
+ chatroomId,
14487
+ role,
14488
+ pid,
14489
+ code: code2,
14490
+ signal,
14491
+ stopReason,
14492
+ intentional: pendingReason !== null
14493
+ });
14494
+ });
14495
+ if (spawnResult.onAgentEnd) {
14496
+ spawnResult.onAgentEnd(() => {
14497
+ try {
14498
+ ctx.deps.processes.kill(-pid, "SIGTERM");
14499
+ } catch {}
14500
+ });
14409
14501
  }
14502
+ let lastReportedTokenAt = 0;
14503
+ spawnResult.onOutput(() => {
14504
+ const now = Date.now();
14505
+ if (now - lastReportedTokenAt >= 30000) {
14506
+ lastReportedTokenAt = now;
14507
+ ctx.deps.backend.mutation(api.participants.updateTokenActivity, {
14508
+ sessionId: ctx.sessionId,
14509
+ chatroomId,
14510
+ role
14511
+ }).catch(() => {});
14512
+ }
14513
+ });
14514
+ return { result: msg, failed: false };
14410
14515
  }
14411
- var init_on_agent_exited = __esm(() => {
14516
+ var init_start_agent = __esm(() => {
14412
14517
  init_api3();
14518
+ init_client2();
14413
14519
  });
14414
14520
 
14415
- // src/events/daemon/agent/on-agent-started.ts
14416
- function onAgentStarted(ctx, payload) {
14417
- const ts = formatTimestamp();
14418
- console.log(`[${ts}] \uD83D\uDFE2 Agent started: ${payload.role} (PID: ${payload.pid}, harness: ${payload.harness})`);
14419
- }
14420
- var init_on_agent_started = () => {};
14421
-
14422
- // src/events/daemon/agent/on-agent-stopped.ts
14423
- function onAgentStopped(ctx, payload) {
14424
- const ts = formatTimestamp();
14425
- console.log(`[${ts}] \uD83D\uDD34 Agent stopped: ${payload.role} (PID: ${payload.pid})`);
14426
- }
14427
- var init_on_agent_stopped = () => {};
14428
-
14429
- // src/events/daemon/register-listeners.ts
14430
- function registerEventListeners(ctx) {
14431
- const unsubs = [];
14432
- unsubs.push(ctx.events.on("agent:exited", (payload) => onAgentExited(ctx, payload)));
14433
- unsubs.push(ctx.events.on("agent:started", (payload) => onAgentStarted(ctx, payload)));
14434
- unsubs.push(ctx.events.on("agent:stopped", (payload) => onAgentStopped(ctx, payload)));
14435
- return () => {
14436
- for (const unsub of unsubs) {
14437
- unsub();
14438
- }
14439
- };
14521
+ // src/events/daemon/agent/on-request-start-agent.ts
14522
+ async function onRequestStartAgent(ctx, event) {
14523
+ if (Date.now() > event.deadline) {
14524
+ console.log(`[daemon] Skipping expired agent.requestStart for role=${event.role} (deadline passed)`);
14525
+ return;
14526
+ }
14527
+ await executeStartAgent(ctx, {
14528
+ chatroomId: event.chatroomId,
14529
+ role: event.role,
14530
+ agentHarness: event.agentHarness,
14531
+ model: event.model,
14532
+ workingDir: event.workingDir,
14533
+ reason: event.reason
14534
+ });
14440
14535
  }
14441
- var init_register_listeners = __esm(() => {
14442
- init_on_agent_exited();
14443
- init_on_agent_started();
14444
- init_on_agent_stopped();
14536
+ var init_on_request_start_agent = __esm(() => {
14537
+ init_start_agent();
14445
14538
  });
14446
14539
 
14447
- // src/commands/machine/daemon-start/init.ts
14448
- import { stat } from "node:fs/promises";
14449
- async function discoverModels(agentServices) {
14450
- const results = {};
14451
- for (const [harness, service] of agentServices) {
14452
- if (service.isInstalled()) {
14453
- try {
14454
- results[harness] = await service.listModels();
14455
- } catch {
14456
- results[harness] = [];
14457
- }
14458
- }
14540
+ // src/commands/machine/daemon-start/handlers/stop-agent.ts
14541
+ async function executeStopAgent(ctx, args) {
14542
+ const { chatroomId, role, reason } = args;
14543
+ const stopReason = reason;
14544
+ console.log(` ↪ stop-agent command received`);
14545
+ console.log(` Chatroom: ${chatroomId}`);
14546
+ console.log(` Role: ${role}`);
14547
+ console.log(` Reason: ${reason}`);
14548
+ const configsResult = await ctx.deps.backend.query(api.machines.getMachineAgentConfigs, {
14549
+ sessionId: ctx.sessionId,
14550
+ chatroomId
14551
+ });
14552
+ const targetConfig = configsResult.configs.find((c) => c.machineId === ctx.machineId && c.role.toLowerCase() === role.toLowerCase());
14553
+ const backendPid = targetConfig?.spawnedAgentPid;
14554
+ const localEntry = ctx.deps.machine.listAgentEntries(ctx.machineId).find((e) => e.chatroomId === chatroomId && e.role.toLowerCase() === role.toLowerCase());
14555
+ const localPid = localEntry?.entry.pid;
14556
+ const allPids = [...new Set([backendPid, localPid].filter((p) => p !== undefined))];
14557
+ if (allPids.length === 0) {
14558
+ const msg = "No running agent found (no PID recorded)";
14559
+ console.log(` ⚠️ ${msg}`);
14560
+ return { result: msg, failed: true };
14459
14561
  }
14460
- return results;
14461
- }
14462
- function createDefaultDeps16() {
14463
- return {
14464
- backend: {
14465
- mutation: async () => {
14466
- throw new Error("Backend not initialized");
14467
- },
14468
- query: async () => {
14469
- throw new Error("Backend not initialized");
14470
- }
14471
- },
14472
- processes: {
14473
- kill: (pid, signal) => process.kill(pid, signal)
14474
- },
14475
- fs: {
14476
- stat
14477
- },
14478
- stops: {
14479
- mark: markIntentionalStop,
14480
- consume: consumeIntentionalStop,
14481
- clear: clearIntentionalStop
14482
- },
14483
- machine: {
14484
- clearAgentPid,
14485
- persistAgentPid,
14486
- listAgentEntries,
14487
- persistEventCursor,
14488
- loadEventCursor
14489
- },
14490
- clock: {
14491
- now: () => Date.now(),
14492
- delay: (ms) => new Promise((resolve2) => setTimeout(resolve2, ms))
14562
+ const anyService = ctx.agentServices.values().next().value;
14563
+ let anyKilled = false;
14564
+ let lastError = null;
14565
+ for (const pid of allPids) {
14566
+ console.log(` Stopping agent with PID: ${pid}`);
14567
+ const isAlive = anyService ? anyService.isAlive(pid) : false;
14568
+ if (!isAlive) {
14569
+ console.log(` ⚠️ PID ${pid} not found — process already exited or was never started`);
14570
+ await clearAgentPidEverywhere(ctx, chatroomId, role);
14571
+ console.log(` Cleared stale PID`);
14572
+ try {
14573
+ await ctx.deps.backend.mutation(api.participants.leave, {
14574
+ sessionId: ctx.sessionId,
14575
+ chatroomId,
14576
+ role
14577
+ });
14578
+ console.log(` Removed participant record`);
14579
+ } catch {}
14580
+ continue;
14493
14581
  }
14494
- };
14495
- }
14496
- function validateAuthentication(convexUrl) {
14497
- const sessionId = getSessionId();
14498
- if (!sessionId) {
14499
- const otherUrls = getOtherSessionUrls();
14500
- console.error(`❌ Not authenticated for: ${convexUrl}`);
14501
- if (otherUrls.length > 0) {
14502
- console.error(`
14503
- \uD83D\uDCA1 You have sessions for other environments:`);
14504
- for (const url of otherUrls) {
14505
- console.error(` • ${url}`);
14582
+ try {
14583
+ const shutdownResult = await onAgentShutdown(ctx, {
14584
+ chatroomId,
14585
+ role,
14586
+ pid,
14587
+ stopReason
14588
+ });
14589
+ const msg = shutdownResult.killed ? `Agent stopped (PID: ${pid})` : `Agent stop attempted (PID: ${pid}) — process may still be running`;
14590
+ console.log(` ${shutdownResult.killed ? "✅" : "⚠️ "} ${msg}`);
14591
+ if (shutdownResult.killed) {
14592
+ anyKilled = true;
14506
14593
  }
14594
+ } catch (e) {
14595
+ lastError = e;
14596
+ console.log(` ⚠️ Failed to stop agent (PID: ${pid}): ${e.message}`);
14507
14597
  }
14508
- console.error(`
14509
- Run: chatroom auth login`);
14510
- releaseLock();
14511
- process.exit(1);
14512
14598
  }
14513
- return sessionId;
14599
+ if (lastError && !anyKilled) {
14600
+ const msg = `Failed to stop agent: ${lastError.message}`;
14601
+ console.log(` ⚠️ ${msg}`);
14602
+ return { result: msg, failed: true };
14603
+ }
14604
+ if (!anyKilled) {
14605
+ return {
14606
+ result: `All recorded PIDs appear stale (processes not found or belong to different programs)`,
14607
+ failed: true
14608
+ };
14609
+ }
14610
+ const killedCount = allPids.length > 1 ? ` (${allPids.length} PIDs)` : ``;
14611
+ return { result: `Agent stopped${killedCount}`, failed: false };
14514
14612
  }
14515
- async function validateSession(client2, sessionId, convexUrl) {
14516
- const validation = await client2.query(api.cliAuth.validateSession, { sessionId });
14517
- if (!validation.valid) {
14518
- console.error(`❌ Session invalid: ${validation.reason}`);
14519
- console.error(`
14520
- Run: chatroom auth login`);
14521
- releaseLock();
14522
- process.exit(1);
14613
+ var init_stop_agent = __esm(() => {
14614
+ init_api3();
14615
+ init_shared();
14616
+ });
14617
+
14618
+ // src/events/daemon/agent/on-request-stop-agent.ts
14619
+ async function onRequestStopAgent(ctx, event) {
14620
+ if (Date.now() > event.deadline) {
14621
+ console.log(`[daemon] ⏰ Skipping expired agent.requestStop for role=${event.role} (deadline passed)`);
14622
+ return;
14523
14623
  }
14624
+ await executeStopAgent(ctx, {
14625
+ chatroomId: event.chatroomId,
14626
+ role: event.role,
14627
+ reason: event.reason
14628
+ });
14524
14629
  }
14525
- function setupMachine() {
14526
- ensureMachineRegistered();
14527
- const config3 = loadMachineConfig();
14528
- return config3;
14630
+ var init_on_request_stop_agent = __esm(() => {
14631
+ init_stop_agent();
14632
+ });
14633
+
14634
+ // src/commands/machine/daemon-start/handlers/ping.ts
14635
+ function handlePing() {
14636
+ console.log(` ↪ Responding: pong`);
14637
+ return { result: "pong", failed: false };
14529
14638
  }
14530
- async function registerCapabilities(client2, sessionId, config3, agentServices) {
14531
- const { machineId } = config3;
14532
- const availableModels = await discoverModels(agentServices);
14639
+
14640
+ // src/commands/machine/daemon-start/command-loop.ts
14641
+ async function refreshModels(ctx) {
14642
+ if (!ctx.config)
14643
+ return;
14644
+ const models = await discoverModels(ctx.agentServices);
14645
+ const freshConfig = ensureMachineRegistered();
14646
+ ctx.config.availableHarnesses = freshConfig.availableHarnesses;
14647
+ ctx.config.harnessVersions = freshConfig.harnessVersions;
14648
+ const totalCount = Object.values(models).flat().length;
14533
14649
  try {
14534
- await client2.mutation(api.machines.register, {
14535
- sessionId,
14536
- machineId,
14537
- hostname: config3.hostname,
14538
- os: config3.os,
14539
- availableHarnesses: config3.availableHarnesses,
14540
- harnessVersions: config3.harnessVersions,
14541
- availableModels
14650
+ await ctx.deps.backend.mutation(api.machines.refreshCapabilities, {
14651
+ sessionId: ctx.sessionId,
14652
+ machineId: ctx.machineId,
14653
+ availableHarnesses: ctx.config.availableHarnesses,
14654
+ harnessVersions: ctx.config.harnessVersions,
14655
+ availableModels: models
14542
14656
  });
14657
+ console.log(`[${formatTimestamp()}] \uD83D\uDD04 Model refresh: ${totalCount > 0 ? `${totalCount} models` : "none discovered"}`);
14543
14658
  } catch (error) {
14544
- console.warn(`⚠️ Machine registration update failed: ${error.message}`);
14659
+ console.warn(`[${formatTimestamp()}] ⚠️ Model refresh failed: ${error.message}`);
14545
14660
  }
14546
- return availableModels;
14547
14661
  }
14548
- async function connectDaemon(client2, sessionId, machineId, convexUrl) {
14549
- try {
14550
- await client2.mutation(api.machines.updateDaemonStatus, {
14551
- sessionId,
14552
- machineId,
14553
- connected: true
14554
- });
14555
- } catch (error) {
14556
- if (isNetworkError(error)) {
14557
- formatConnectivityError(error, convexUrl);
14558
- } else {
14559
- console.error(`❌ Failed to update daemon status: ${error.message}`);
14560
- }
14561
- releaseLock();
14562
- process.exit(1);
14662
+ function evictStaleDedupEntries(processedCommandIds, processedPingIds) {
14663
+ const evictBefore = Date.now() - AGENT_REQUEST_DEADLINE_MS;
14664
+ for (const [id, ts] of processedCommandIds) {
14665
+ if (ts < evictBefore)
14666
+ processedCommandIds.delete(id);
14563
14667
  }
14564
- }
14565
- function logStartup(ctx, availableModels) {
14566
- console.log(`[${formatTimestamp()}] \uD83D\uDE80 Daemon started`);
14567
- console.log(` CLI version: ${getVersion()}`);
14568
- console.log(` Machine ID: ${ctx.machineId}`);
14569
- console.log(` Hostname: ${ctx.config?.hostname ?? "unknown"}`);
14570
- console.log(` Available harnesses: ${ctx.config?.availableHarnesses.join(", ") || "none"}`);
14571
- console.log(` Available models: ${Object.keys(availableModels).length > 0 ? `${Object.values(availableModels).flat().length} models across ${Object.keys(availableModels).join(", ")}` : "none discovered"}`);
14572
- console.log(` PID: ${process.pid}`);
14573
- }
14574
- async function recoverState(ctx) {
14575
- console.log(`
14576
- [${formatTimestamp()}] \uD83D\uDD04 Recovering agent state...`);
14577
- try {
14578
- await recoverAgentState(ctx);
14579
- } catch (e) {
14580
- console.log(` ⚠️ Recovery failed: ${e.message}`);
14581
- console.log(` Continuing with fresh state`);
14668
+ for (const [id, ts] of processedPingIds) {
14669
+ if (ts < evictBefore)
14670
+ processedPingIds.delete(id);
14582
14671
  }
14583
14672
  }
14584
- async function initDaemon() {
14585
- if (!acquireLock()) {
14586
- process.exit(1);
14673
+ async function dispatchCommandEvent(ctx, event, processedCommandIds, processedPingIds) {
14674
+ const eventId = event._id.toString();
14675
+ if (event.type === "agent.requestStart") {
14676
+ if (processedCommandIds.has(eventId))
14677
+ return;
14678
+ processedCommandIds.set(eventId, Date.now());
14679
+ await onRequestStartAgent(ctx, event);
14680
+ } else if (event.type === "agent.requestStop") {
14681
+ if (processedCommandIds.has(eventId))
14682
+ return;
14683
+ processedCommandIds.set(eventId, Date.now());
14684
+ await onRequestStopAgent(ctx, event);
14685
+ } else if (event.type === "daemon.ping") {
14686
+ if (processedPingIds.has(eventId))
14687
+ return;
14688
+ processedPingIds.set(eventId, Date.now());
14689
+ handlePing();
14690
+ await ctx.deps.backend.mutation(api.machines.ackPing, {
14691
+ sessionId: ctx.sessionId,
14692
+ machineId: ctx.machineId,
14693
+ pingEventId: event._id
14694
+ });
14587
14695
  }
14588
- const convexUrl = getConvexUrl();
14589
- const sessionId = validateAuthentication(convexUrl);
14590
- const client2 = await getConvexClient();
14591
- const typedSessionId = sessionId;
14592
- await validateSession(client2, typedSessionId, convexUrl);
14593
- const config3 = setupMachine();
14594
- const { machineId } = config3;
14595
- const openCodeService = new OpenCodeAgentService;
14596
- const piService = new PiAgentService;
14597
- const agentServices = new Map([
14598
- ["opencode", openCodeService],
14599
- ["pi", piService]
14600
- ]);
14601
- const availableModels = await registerCapabilities(client2, typedSessionId, config3, agentServices);
14602
- await connectDaemon(client2, typedSessionId, machineId, convexUrl);
14603
- const deps = createDefaultDeps16();
14604
- deps.backend.mutation = (endpoint, args) => client2.mutation(endpoint, args);
14605
- deps.backend.query = (endpoint, args) => client2.query(endpoint, args);
14606
- const events = new DaemonEventBus;
14607
- const ctx = {
14608
- client: client2,
14609
- sessionId: typedSessionId,
14610
- machineId,
14611
- config: config3,
14612
- deps,
14613
- events,
14614
- agentServices
14696
+ }
14697
+ async function startCommandLoop(ctx) {
14698
+ let heartbeatCount = 0;
14699
+ const heartbeatTimer = setInterval(() => {
14700
+ ctx.deps.backend.mutation(api.machines.daemonHeartbeat, {
14701
+ sessionId: ctx.sessionId,
14702
+ machineId: ctx.machineId
14703
+ }).then(() => {
14704
+ heartbeatCount++;
14705
+ console.log(`[${formatTimestamp()}] \uD83D\uDC93 Daemon heartbeat #${heartbeatCount} OK`);
14706
+ }).catch((err) => {
14707
+ console.warn(`[${formatTimestamp()}] ⚠️ Daemon heartbeat failed: ${err.message}`);
14708
+ });
14709
+ }, DAEMON_HEARTBEAT_INTERVAL_MS);
14710
+ heartbeatTimer.unref();
14711
+ const shutdown = async () => {
14712
+ console.log(`
14713
+ [${formatTimestamp()}] Shutting down...`);
14714
+ clearInterval(heartbeatTimer);
14715
+ await onDaemonShutdown(ctx);
14716
+ releaseLock();
14717
+ process.exit(0);
14615
14718
  };
14616
- registerEventListeners(ctx);
14617
- logStartup(ctx, availableModels);
14618
- await recoverState(ctx);
14619
- return ctx;
14719
+ process.on("SIGINT", shutdown);
14720
+ process.on("SIGTERM", shutdown);
14721
+ process.on("SIGHUP", shutdown);
14722
+ const wsClient2 = await getConvexWsClient();
14723
+ console.log(`
14724
+ Listening for commands...`);
14725
+ console.log(`Press Ctrl+C to stop
14726
+ `);
14727
+ const processedCommandIds = new Map;
14728
+ const processedPingIds = new Map;
14729
+ wsClient2.onUpdate(api.machines.getCommandEvents, {
14730
+ sessionId: ctx.sessionId,
14731
+ machineId: ctx.machineId
14732
+ }, async (result) => {
14733
+ if (!result.events || result.events.length === 0)
14734
+ return;
14735
+ evictStaleDedupEntries(processedCommandIds, processedPingIds);
14736
+ for (const event of result.events) {
14737
+ try {
14738
+ console.log(`[${formatTimestamp()}] \uD83D\uDCE1 Stream command event: ${event.type}`);
14739
+ await dispatchCommandEvent(ctx, event, processedCommandIds, processedPingIds);
14740
+ } catch (err) {
14741
+ console.error(`[${formatTimestamp()}] ❌ Stream command event failed: ${err.message}`);
14742
+ }
14743
+ }
14744
+ });
14745
+ const modelRefreshTimer = setInterval(() => {
14746
+ refreshModels(ctx).catch((err) => {
14747
+ console.warn(`[${formatTimestamp()}] ⚠️ Model refresh error: ${err.message}`);
14748
+ });
14749
+ }, MODEL_REFRESH_INTERVAL_MS);
14750
+ modelRefreshTimer.unref();
14751
+ return await new Promise(() => {});
14620
14752
  }
14621
- var init_init2 = __esm(() => {
14622
- init_state_recovery();
14753
+ var MODEL_REFRESH_INTERVAL_MS;
14754
+ var init_command_loop = __esm(() => {
14623
14755
  init_api3();
14624
- init_storage();
14625
14756
  init_client2();
14626
14757
  init_machine();
14627
- init_intentional_stops();
14628
- init_opencode();
14629
- init_pi();
14630
- init_error_formatting();
14631
- init_version();
14758
+ init_on_daemon_shutdown();
14759
+ init_init2();
14760
+ init_on_request_start_agent();
14761
+ init_on_request_stop_agent();
14632
14762
  init_pid();
14633
- init_register_listeners();
14763
+ MODEL_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
14634
14764
  });
14635
14765
 
14636
14766
  // src/commands/machine/daemon-start/index.ts
@@ -14711,8 +14841,8 @@ __export(exports_opencode_install, {
14711
14841
  });
14712
14842
  async function isChatroomInstalledDefault() {
14713
14843
  try {
14714
- const { execSync: execSync3 } = await import("child_process");
14715
- execSync3("chatroom --version", { stdio: "pipe" });
14844
+ const { execSync: execSync2 } = await import("child_process");
14845
+ execSync2("chatroom --version", { stdio: "pipe" });
14716
14846
  return true;
14717
14847
  } catch {
14718
14848
  return false;