@akshar5/cohall 0.6.1 → 0.6.2

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.
package/bin/cohall.js CHANGED
@@ -17,7 +17,7 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
17
 
18
18
  // packages/protocol/src/index.ts
19
19
  import { Effect, Schema } from "effect";
20
- var version = "0.6.1", maxSocketPayloadBytes, bounded = (maxLength) => Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(maxLength)), optionalText = (maxLength) => Schema.String.check(Schema.isMaxLength(maxLength)), boundedArray = (schema, maxLength) => Schema.Array(schema).check(Schema.isMaxLength(maxLength)), count, uuid = (name) => Schema.String.check(Schema.isUUID(4)).pipe(Schema.brand(name)), isoTimestamp = (name) => Schema.String.check(Schema.isPattern(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/, {
20
+ var version = "0.6.2", maxSocketPayloadBytes, bounded = (maxLength) => Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(maxLength)), optionalText = (maxLength) => Schema.String.check(Schema.isMaxLength(maxLength)), boundedArray = (schema, maxLength) => Schema.Array(schema).check(Schema.isMaxLength(maxLength)), count, uuid = (name) => Schema.String.check(Schema.isUUID(4)).pipe(Schema.brand(name)), isoTimestamp = (name) => Schema.String.check(Schema.isPattern(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/, {
21
21
  expected: "an ISO-8601 UTC timestamp"
22
22
  })).pipe(Schema.brand(name)), BotId, Bot, DeviceId, ThreadId, MessageId, TaskId, AuthSessionId, OperationId, Timestamp, Platform, Provider, DeviceStatus, TaskStatus, OperationStatus, TaskTraceEventKind, MessageRole, ConnectionRole, AuthSession, CreatePairingInput, PairingCredential, ExchangePairingInput, SessionCredential, PairingResult, Capability, Workspace, Device, minimumDeviceOperationVersion = "0.5.0", parsedVersion = (value) => {
23
23
  const match = /^v?(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.exec(value);
@@ -487,13 +487,15 @@ class Hub {
487
487
  hasDevice(deviceId) {
488
488
  return this.#devices.get(deviceId)?.readyState === WebSocket3.OPEN;
489
489
  }
490
- sendToDevice(deviceId, event) {
490
+ sendToDevice(deviceId, event, beforeSend) {
491
491
  const socket = this.#devices.get(deviceId);
492
492
  if (socket?.readyState !== WebSocket3.OPEN || !this.isAuthorized(socket)) {
493
493
  return false;
494
494
  }
495
+ const payload = JSON.stringify(event);
496
+ beforeSend?.();
495
497
  try {
496
- socket.send(JSON.stringify(event));
498
+ socket.send(payload);
497
499
  return true;
498
500
  } catch {
499
501
  return false;
@@ -1067,7 +1069,11 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1067
1069
  }
1068
1070
  return yield* deviceFromRow(row);
1069
1071
  });
1070
- const createDelegation2 = Effect13.fn("RelayStore.createDelegation")(function* (input, targetDeviceId, sourceDeviceId, providerSessionId) {
1072
+ const createDelegation2 = Effect13.fn("RelayStore.createDelegation")(function* (input, targetDeviceId, boundDeviceId, providerSessionId) {
1073
+ const sourceDeviceId = yield* Effect13.try({
1074
+ try: () => boundDeviceId === undefined || db.query("SELECT id FROM devices WHERE id = ? AND forgotten_at IS NULL").get(boundDeviceId) === null ? undefined : boundDeviceId,
1075
+ catch: operationError("RelayStore.createDelegation.source")
1076
+ });
1071
1077
  const timestamp = now();
1072
1078
  const threadId = input.threadId ?? makeThreadId();
1073
1079
  const task = Task.make({
@@ -1207,7 +1213,15 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1207
1213
  const timestamp = now();
1208
1214
  yield* Effect13.try({
1209
1215
  try: () => db.transaction(() => {
1210
- const result = db.query(`UPDATE tasks SET status = 'assigned', updated_at = ?
1216
+ const result = db.query(`UPDATE tasks SET status = 'assigned', updated_at = ?,
1217
+ provider_session_id = CASE
1218
+ WHEN provider = 'grok-bot' THEN NULL
1219
+ WHEN started_at IS NULL THEN COALESCE((
1220
+ SELECT session_id FROM provider_sessions
1221
+ WHERE thread_id = tasks.thread_id AND device_id = tasks.target_device_id
1222
+ AND provider = tasks.provider
1223
+ ), provider_session_id)
1224
+ ELSE provider_session_id END
1211
1225
  WHERE id = ? AND status = 'queued' AND NOT EXISTS (
1212
1226
  SELECT 1 FROM tasks active
1213
1227
  WHERE active.target_device_id = ? AND active.id <> ?
@@ -1278,7 +1292,11 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1278
1292
  if (["completed", "failed", "cancelled"].includes(current.status)) {
1279
1293
  return current;
1280
1294
  }
1281
- if (current.provider === "grok-bot" && current.status !== "queued") {
1295
+ const dispatched = current.provider === "grok-bot" && (yield* Effect13.try({
1296
+ try: () => db.query("SELECT 1 FROM tasks WHERE id = ? AND dispatched_at IS NOT NULL").get(taskId) !== null,
1297
+ catch: operationError("RelayStore.requestCancellation")
1298
+ }));
1299
+ if (current.provider === "grok-bot" && (current.status !== "queued" || current.startedAt !== undefined || dispatched)) {
1282
1300
  return yield* new PersistenceError({
1283
1301
  operation: "RelayStore.requestCancellation",
1284
1302
  message: "Stop this bot in Grok Bot; its gateway cannot cancel an individual Cohall request"
@@ -1358,7 +1376,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1358
1376
  created_at: timestamp,
1359
1377
  expires_at: expiresAt,
1360
1378
  last_seen_at: timestamp,
1361
- bound_device_id: role === "device" ? deviceId ?? null : null,
1379
+ bound_device_id: deviceId ?? null,
1362
1380
  revoked_at: null
1363
1381
  };
1364
1382
  db.query(`INSERT INTO auth_sessions (
@@ -1432,6 +1450,12 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1432
1450
  threadContext: threadContext2,
1433
1451
  pendingTasksFor,
1434
1452
  assignTask,
1453
+ markTaskDispatched: (taskId) => Effect13.try({
1454
+ try: () => {
1455
+ db.query("UPDATE tasks SET dispatched_at = COALESCE(dispatched_at, ?) WHERE id = ? AND status = 'assigned'").run(now(), taskId);
1456
+ },
1457
+ catch: operationError("RelayStore.markTaskDispatched")
1458
+ }),
1435
1459
  rollbackAssignment: (taskId) => transition(taskId, ["assigned"], { status: "queued" }, { kind: "requeued", detail: "Target device was unavailable during dispatch" }),
1436
1460
  acceptTask: (taskId, deviceId) => requireTarget(taskId, deviceId).pipe(Effect13.flatMap(() => transition(taskId, ["assigned"], { status: "running", startedAt: now() }, { kind: "running", detail: "Target device accepted the task and started the provider" }))),
1437
1461
  finishTask: (taskId, deviceId, result, providerSessionId) => terminal(taskId, deviceId, "completed", result, undefined, providerSessionId),
@@ -1487,7 +1511,8 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1487
1511
  source_device_id TEXT REFERENCES devices(id),
1488
1512
  target_device_id TEXT NOT NULL REFERENCES devices(id), parent_task_id TEXT,
1489
1513
  workspace TEXT, provider_session_id TEXT, result TEXT, error TEXT,
1490
- created_at TEXT NOT NULL, updated_at TEXT NOT NULL, started_at TEXT, completed_at TEXT
1514
+ created_at TEXT NOT NULL, updated_at TEXT NOT NULL, started_at TEXT, completed_at TEXT,
1515
+ dispatched_at TEXT
1491
1516
  );
1492
1517
  CREATE INDEX IF NOT EXISTS tasks_thread_created ON tasks(thread_id, created_at);
1493
1518
  CREATE INDEX IF NOT EXISTS tasks_target_status ON tasks(target_device_id, status);
@@ -1535,6 +1560,13 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1535
1560
  if (!taskColumns.some((column) => column.name === "bot_id")) {
1536
1561
  db.exec("ALTER TABLE tasks ADD COLUMN bot_id TEXT");
1537
1562
  }
1563
+ if (!taskColumns.some((column) => column.name === "dispatched_at")) {
1564
+ db.transaction(() => {
1565
+ db.exec("ALTER TABLE tasks ADD COLUMN dispatched_at TEXT");
1566
+ db.exec(`UPDATE tasks SET dispatched_at = COALESCE(started_at, updated_at)
1567
+ WHERE provider = 'grok-bot' AND status NOT IN ('completed', 'failed', 'cancelled')`);
1568
+ })();
1569
+ }
1538
1570
  },
1539
1571
  catch: operationError("RelayStore.migrate")
1540
1572
  }), layer2 = (path, retainedTerminalTasks = 1000) => Layer2.effect(Service2, Effect13.acquireRelease(Effect13.try({
@@ -1555,8 +1587,8 @@ var init_store = __esm(() => {
1555
1587
  var exports_main = {};
1556
1588
  __export(exports_main, {
1557
1589
  runRelay: () => runRelay,
1558
- relayListenOptions: () => relayListenOptions,
1559
- chooseDevice: () => chooseDevice
1590
+ resolveDelegation: () => resolveDelegation,
1591
+ relayListenOptions: () => relayListenOptions
1560
1592
  });
1561
1593
  import { Effect as Effect14, ManagedRuntime, Schema as Schema14 } from "effect";
1562
1594
  import { createHash as createHash3, timingSafeEqual } from "node:crypto";
@@ -1665,10 +1697,40 @@ Content-Length: 0\r
1665
1697
  const needsXcode = /\b(xcode|ios|macos|swiftui|app store)\b/.test(prompt);
1666
1698
  const needsBrowser = /\b(browser|chrome|youtube|twitter|logged[- ]?in|signed[- ]?in|x\.com)\b/.test(prompt);
1667
1699
  return (device.status === "offline" ? 0 : 100) + (input.workspace !== undefined && device.workspaces.some((workspace) => input.workspace === workspace.path || input.workspace?.startsWith(`${workspace.path}/`)) ? 80 : 0) + (needsXcode && device.capabilities.some((capability) => capability.id === "xcode") ? 60 : 0) + (needsBrowser && device.capabilities.some((capability) => capability.id === "browser-session") ? 40 : 0) + (sourceDeviceId === device.id ? -5 : 0);
1668
- }, chooseDevice = (input, sourceDeviceId) => Effect14.gen(function* () {
1700
+ }, resolveDelegation = (input, sourceDeviceId) => Effect14.gen(function* () {
1669
1701
  const store = yield* exports_store.Service;
1702
+ const ancestors = [];
1703
+ const seen = new Set;
1704
+ let nextParentId = input.parentTaskId;
1705
+ while (nextParentId !== undefined && !seen.has(nextParentId)) {
1706
+ const parentId = nextParentId;
1707
+ seen.add(parentId);
1708
+ const parent2 = yield* store.getTask(parentId).pipe(Effect14.catch((cause) => {
1709
+ const missing = cause.message === `Unknown task ${parentId}`;
1710
+ if (missing && ancestors.length > 0)
1711
+ return Effect14.succeed(undefined);
1712
+ return Effect14.fail(new RequestError({ status: missing ? 404 : 500, message: cause.message }));
1713
+ }));
1714
+ if (parent2 === undefined)
1715
+ break;
1716
+ ancestors.push(parent2);
1717
+ nextParentId = parent2.parentTaskId;
1718
+ }
1719
+ const parent = ancestors[0];
1720
+ if (parent !== undefined && input.threadId !== undefined && input.threadId !== parent.threadId) {
1721
+ return yield* new RequestError({
1722
+ status: 400,
1723
+ message: "A child task must use its parent's thread"
1724
+ });
1725
+ }
1726
+ const resolvedInput = parent === undefined ? input : { ...input, threadId: parent.threadId };
1670
1727
  const devices = yield* store.listDevices().pipe(Effect14.mapError((cause) => new RequestError({ status: 500, message: cause.message })));
1671
1728
  const provider = input.provider ?? (input.botId === undefined ? "codex" : "grok-bot");
1729
+ const slot = taskSlot({
1730
+ provider,
1731
+ ...input.botId === undefined ? {} : { botId: input.botId }
1732
+ });
1733
+ const blocked = new Set(ancestors.filter((task) => !isTerminalTask(task) && taskSlot(task) === slot).map((task) => task.targetDeviceId));
1672
1734
  if (input.targetDeviceId !== undefined) {
1673
1735
  const target = devices.find((device) => device.id === input.targetDeviceId);
1674
1736
  if (target === undefined) {
@@ -1689,7 +1751,13 @@ Content-Length: 0\r
1689
1751
  message: `${target.name} does not advertise bot ${input.botId}; refresh the bot list`
1690
1752
  });
1691
1753
  }
1692
- return target.id;
1754
+ if (blocked.has(target.id)) {
1755
+ return yield* new RequestError({
1756
+ status: 409,
1757
+ message: `${target.name} cannot run this task while an unfinished parent occupies the same worker slot; select another device or bot`
1758
+ });
1759
+ }
1760
+ return { input: resolvedInput, targetDeviceId: target.id };
1693
1761
  }
1694
1762
  const candidates = devices.filter((device) => device.providers.includes(provider) && (input.botId === undefined || device.bots?.some((bot) => bot.id === input.botId)));
1695
1763
  if (input.botId !== undefined && candidates.length > 1) {
@@ -1698,14 +1766,14 @@ Content-Length: 0\r
1698
1766
  message: `Bot ${input.botId} is advertised by more than one device; select a device`
1699
1767
  });
1700
1768
  }
1701
- const selected = candidates.sort((left, right) => scoreDevice(right, input, sourceDeviceId) - scoreDevice(left, input, sourceDeviceId))[0];
1769
+ const selected = candidates.filter((device) => !blocked.has(device.id)).sort((left, right) => scoreDevice(right, input, sourceDeviceId) - scoreDevice(left, input, sourceDeviceId))[0];
1702
1770
  if (selected === undefined) {
1703
1771
  return yield* new RequestError({
1704
1772
  status: 409,
1705
- message: input.botId === undefined ? `No Cohall device advertises the ${provider} provider` : `No Cohall device advertises bot ${input.botId}; refresh the bot list`
1773
+ message: candidates.length > 0 ? "No available delegation target avoids an unfinished parent's worker slot; select another device or bot" : input.botId === undefined ? `No Cohall device advertises the ${provider} provider` : `No Cohall device advertises bot ${input.botId}; refresh the bot list`
1706
1774
  });
1707
1775
  }
1708
- return selected.id;
1776
+ return { input: resolvedInput, targetDeviceId: selected.id };
1709
1777
  }), runRelay = async () => {
1710
1778
  const configuration = await Effect14.runPromise(loadEnvironmentConfiguration);
1711
1779
  process.umask(63);
@@ -1741,20 +1809,15 @@ Content-Length: 0\r
1741
1809
  return authenticate(authorization.slice("Bearer ".length), "client");
1742
1810
  };
1743
1811
  const dispatch = async (task) => {
1744
- const assigned = await run2(Effect14.gen(function* () {
1745
- const store = yield* exports_store.Service;
1746
- return yield* store.assignTask(task.id);
1747
- }));
1812
+ const store = await run2(exports_store.Service);
1813
+ const assigned = await Effect14.runPromise(store.assignTask(task.id));
1748
1814
  if (assigned.status !== "assigned") {
1749
1815
  return assigned;
1750
1816
  }
1751
- if (hub.sendToDevice(assigned.targetDeviceId, SocketEvent.make({ _tag: "TaskAssigned", task: assigned }))) {
1817
+ if (hub.sendToDevice(assigned.targetDeviceId, SocketEvent.make({ _tag: "TaskAssigned", task: assigned }), assigned.provider === "grok-bot" ? () => Effect14.runSync(store.markTaskDispatched(assigned.id)) : undefined)) {
1752
1818
  return assigned;
1753
1819
  }
1754
- return run2(Effect14.gen(function* () {
1755
- const store = yield* exports_store.Service;
1756
- return yield* store.rollbackAssignment(task.id);
1757
- }));
1820
+ return Effect14.runPromise(store.rollbackAssignment(task.id));
1758
1821
  };
1759
1822
  const dispatchOperation = async (operation) => {
1760
1823
  const assigned = await run2(Effect14.gen(function* () {
@@ -2009,8 +2072,7 @@ Content-Length: 0\r
2009
2072
  return json(forgotten);
2010
2073
  }
2011
2074
  if (url.pathname === "/api/tasks" && request.method === "POST") {
2012
- const input = yield* body(request, decodeCreateTaskInput);
2013
- const target = yield* chooseDevice(input, sourceDeviceId);
2075
+ const { input, targetDeviceId: target } = yield* resolveDelegation(yield* body(request, decodeCreateTaskInput), sourceDeviceId);
2014
2076
  const providerSessionId = input.threadId === undefined || input.provider === "grok-bot" ? undefined : yield* store.sessionFor(input.threadId, target, input.provider ?? "codex");
2015
2077
  const task2 = yield* store.createDelegation(input, target, sourceDeviceId, providerSessionId);
2016
2078
  return json(yield* Effect14.tryPromise(() => dispatch(task2)), 201);
@@ -2051,6 +2113,7 @@ Content-Length: 0\r
2051
2113
  });
2052
2114
  websocketServer.on("connection", (rawSocket) => {
2053
2115
  const socket = rawSocket;
2116
+ socket.on("error", () => socket.terminate());
2054
2117
  socket.data = {
2055
2118
  processing: Promise.resolve(),
2056
2119
  stageDeadline: undefined,
@@ -2127,7 +2190,11 @@ Content-Length: 0\r
2127
2190
  }).then((response) => sendResponse(outgoing, response)).catch((cause) => sendResponse(outgoing, json({ error: cause instanceof Error ? cause.message : "Invalid request" }, cause instanceof Error && cause.message.includes("256 KiB") ? 413 : 400)));
2128
2191
  });
2129
2192
  server.on("upgrade", (request, socket, head) => {
2130
- const url = new URL(request.url ?? "/", "http://cohall.local");
2193
+ const url = URL.parse(request.url ?? "/", "http://cohall.local");
2194
+ if (url === null) {
2195
+ rejectUpgrade(socket, 400, "Bad Request");
2196
+ return;
2197
+ }
2131
2198
  if (url.pathname !== "/ws/device") {
2132
2199
  rejectUpgrade(socket, 404, "Not Found");
2133
2200
  return;
@@ -2343,11 +2410,12 @@ init_src();
2343
2410
  // packages/providers/src/index.ts
2344
2411
  init_src();
2345
2412
  import { Effect as Effect3, Schema as Schema3 } from "effect";
2413
+ import { execa } from "execa";
2346
2414
  import { accessSync, constants } from "node:fs";
2347
2415
  import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises";
2348
2416
  import { homedir, platform, tmpdir } from "node:os";
2349
2417
  import { delimiter, extname, isAbsolute, join } from "node:path";
2350
- import { execFile, spawn } from "node:child_process";
2418
+ import { execFile } from "node:child_process";
2351
2419
 
2352
2420
  class ProviderUnavailableError extends Schema3.TaggedErrorClass()("CohallProvider.Unavailable", { provider: Provider, message: Schema3.String }) {
2353
2421
  }
@@ -2364,7 +2432,10 @@ var executables = {
2364
2432
  opencode: "opencode"
2365
2433
  };
2366
2434
  var providerEnvironment = () => ({
2367
- ...Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.startsWith("COHALL_") || name === "COHALL_CONFIG")),
2435
+ ...Object.fromEntries(Object.entries(process.env).filter(([name]) => {
2436
+ const key = platform() === "win32" ? name.toUpperCase() : name;
2437
+ return !key.startsWith("COHALL_") || key === "COHALL_CONFIG";
2438
+ })),
2368
2439
  PATH: executableDirectories().join(delimiter)
2369
2440
  });
2370
2441
  var record = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? Object.fromEntries(Object.entries(value)) : undefined;
@@ -2708,8 +2779,8 @@ var terminateProcessTree = async (pid) => {
2708
2779
  process.kill(-pid, "SIGKILL");
2709
2780
  await waitForProcessGroup(pid, 1000);
2710
2781
  };
2711
- var run = (options) => Effect3.tryPromise({
2712
- try: async (signal) => {
2782
+ var run = (options) => Effect3.callback((resume, signal) => {
2783
+ const running = (async () => {
2713
2784
  const executable = findExecutable(executables[options.provider]);
2714
2785
  if (executable === undefined) {
2715
2786
  throw new ProviderUnavailableError({
@@ -2720,8 +2791,9 @@ var run = (options) => Effect3.tryPromise({
2720
2791
  const prepared = await prepareCommand(options);
2721
2792
  try {
2722
2793
  await options.beforeSpawn?.();
2794
+ signal.throwIfAborted();
2723
2795
  const [, ...arguments_] = prepared.command;
2724
- const child = spawn(executable, arguments_, {
2796
+ const child = execa(executable, arguments_, {
2725
2797
  cwd: options.cwd,
2726
2798
  env: {
2727
2799
  ...providerEnvironment(),
@@ -2729,12 +2801,22 @@ var run = (options) => Effect3.tryPromise({
2729
2801
  COHALL_THREAD_ID: options.threadId,
2730
2802
  ...options.taskId === undefined ? {} : { COHALL_TASK_ID: options.taskId }
2731
2803
  },
2804
+ extendEnv: false,
2732
2805
  detached: platform() !== "win32",
2733
- stdio: ["pipe", "pipe", "pipe"]
2806
+ stdin: "pipe",
2807
+ stdout: "pipe",
2808
+ stderr: "pipe",
2809
+ buffer: false,
2810
+ reject: false,
2811
+ cleanup: false
2734
2812
  });
2735
- const exited = new Promise((resolve, reject) => {
2736
- child.once("error", reject);
2737
- child.once("exit", (code) => resolve(code ?? 1));
2813
+ let finished = false;
2814
+ const exited = child.then(({ exitCode, failed, originalMessage, shortMessage }) => {
2815
+ finished = true;
2816
+ if (failed && exitCode === undefined) {
2817
+ throw new Error(originalMessage ?? shortMessage ?? "Provider failed to start");
2818
+ }
2819
+ return exitCode ?? 1;
2738
2820
  });
2739
2821
  let termination;
2740
2822
  const terminate = () => {
@@ -2749,12 +2831,16 @@ var run = (options) => Effect3.tryPromise({
2749
2831
  if (signal.aborted) {
2750
2832
  terminate();
2751
2833
  }
2752
- child.stdin.end(prepared.input);
2753
2834
  try {
2835
+ const written = new Promise((resolve, reject) => {
2836
+ child.stdin.once("error", reject);
2837
+ child.stdin.end(prepared.input, (cause) => cause === undefined || cause === null ? resolve() : reject(cause));
2838
+ });
2754
2839
  const [result, stderr, exitCode] = await Promise.all([
2755
2840
  parse(options, child.stdout, terminate),
2756
2841
  captureText(child.stderr, 64 * 1024),
2757
- exited
2842
+ exited,
2843
+ written
2758
2844
  ]);
2759
2845
  if (exitCode !== 0) {
2760
2846
  throw new ProviderRunError({
@@ -2778,7 +2864,7 @@ var run = (options) => Effect3.tryPromise({
2778
2864
  return result;
2779
2865
  } finally {
2780
2866
  signal.removeEventListener("abort", terminate);
2781
- if (child.exitCode === null) {
2867
+ if (!finished) {
2782
2868
  terminate();
2783
2869
  await exited.catch(() => {
2784
2870
  return;
@@ -2789,21 +2875,21 @@ var run = (options) => Effect3.tryPromise({
2789
2875
  } finally {
2790
2876
  await prepared.cleanup();
2791
2877
  }
2792
- },
2793
- catch: (cause) => {
2794
- if (cause instanceof ProviderUnavailableError || cause instanceof ProviderRunError) {
2795
- return cause;
2796
- }
2797
- return new ProviderRunError({
2798
- provider: options.provider,
2799
- message: (cause instanceof Error ? cause.message : String(cause)).slice(0, 16384)
2800
- });
2801
- }
2878
+ })();
2879
+ running.then((result) => resume(Effect3.succeed(result)), (cause) => resume(Effect3.fail(cause instanceof ProviderUnavailableError || cause instanceof ProviderRunError ? cause : new ProviderRunError({
2880
+ provider: options.provider,
2881
+ message: (cause instanceof Error ? cause.message : String(cause)).slice(0, 16384)
2882
+ }))));
2883
+ return Effect3.promise(() => running.then(() => {
2884
+ return;
2885
+ }, () => {
2886
+ return;
2887
+ }));
2802
2888
  });
2803
2889
 
2804
2890
  // apps/device/src/cli.ts
2805
2891
  import { Effect as Effect9, Schema as Schema10 } from "effect";
2806
- import { access as access4, mkdir as mkdir6, readFile as readFile4, stat as stat3, writeFile as writeFile6 } from "node:fs/promises";
2892
+ import { access as access4, mkdir as mkdir6, readFile as readFile4, stat as stat4, writeFile as writeFile6 } from "node:fs/promises";
2807
2893
  import { homedir as homedir4 } from "node:os";
2808
2894
  import { dirname as dirname6, join as join7, resolve as resolve3 } from "node:path";
2809
2895
 
@@ -3013,7 +3099,7 @@ Return the diagnostic failure to the user rather than claiming remote work ran.
3013
3099
  // apps/device/src/config.ts
3014
3100
  init_src();
3015
3101
  import { Effect as Effect4, Schema as Schema4 } from "effect";
3016
- import { access, chmod as chmod2, mkdir, readFile, realpath, rename, writeFile as writeFile2 } from "node:fs/promises";
3102
+ import { access, chmod as chmod2, mkdir, readFile, realpath, rename, stat, writeFile as writeFile2 } from "node:fs/promises";
3017
3103
  import { homedir as homedir2, hostname, platform as platform2 } from "node:os";
3018
3104
  import { dirname, join as join2, resolve } from "node:path";
3019
3105
  var WorkspaceList = Schema4.Array(Schema4.NonEmptyString).check(Schema4.isMaxLength(64));
@@ -3086,10 +3172,16 @@ var normalizeRelayUrl = (input) => {
3086
3172
  var parseWorkspaces = (workspaceList, workspaceJson) => Effect4.tryPromise({
3087
3173
  try: async () => {
3088
3174
  const values = workspaceJson === undefined ? workspaceList.split(",").map((workspace) => workspace.trim()).filter((workspace) => workspace.length > 0) : Schema4.decodeUnknownSync(WorkspaceList)(JSON.parse(workspaceJson));
3089
- const canonical = await Promise.all(values.map((workspace) => realpath(resolve(workspace))));
3175
+ const canonical = await Promise.all(values.map(async (workspace) => {
3176
+ const path = await realpath(resolve(workspace));
3177
+ if (!(await stat(path)).isDirectory()) {
3178
+ throw new Error(`Not a directory: ${workspace}`);
3179
+ }
3180
+ return path;
3181
+ }));
3090
3182
  return [...new Set(canonical)];
3091
3183
  },
3092
- catch: (cause) => new Error(`Workspace roots must be existing paths; JSON input must be an array of paths: ${cause instanceof Error ? cause.message : String(cause)}`)
3184
+ catch: (cause) => new Error(`Workspace roots must be existing directories; JSON input must be an array of paths: ${cause instanceof Error ? cause.message : String(cause)}`)
3093
3185
  });
3094
3186
  var parseProviders = (input) => Schema4.decodeUnknownSync(ProviderList)([
3095
3187
  ...new Set(input.split(",").map((provider) => provider.trim()).filter(Boolean))
@@ -4013,7 +4105,7 @@ import {
4013
4105
  realpath as realpath4,
4014
4106
  rename as rename3,
4015
4107
  rm as rm3,
4016
- stat as stat2,
4108
+ stat as stat3,
4017
4109
  writeFile as writeFile5
4018
4110
  } from "node:fs/promises";
4019
4111
  import { basename, dirname as dirname5, join as join6, resolve as resolve2 } from "node:path";
@@ -4021,14 +4113,14 @@ import { backup as sqliteBackup, DatabaseSync } from "node:sqlite";
4021
4113
  import { WebSocket } from "ws";
4022
4114
 
4023
4115
  // apps/device/src/service.ts
4024
- import { execFile as execFile3 } from "node:child_process";
4116
+ import { execFile as execFile2 } from "node:child_process";
4025
4117
  import { chmod as chmod5, mkdir as mkdir4, realpath as realpath3, writeFile as writeFile4 } from "node:fs/promises";
4026
4118
  import { homedir as homedir3 } from "node:os";
4027
4119
  import { dirname as dirname4, join as join5 } from "node:path";
4028
4120
 
4029
4121
  // apps/device/src/upgrade.ts
4030
4122
  import { Schema as Schema8 } from "effect";
4031
- import { execFile as execFile2 } from "node:child_process";
4123
+ import { execa as execa2 } from "execa";
4032
4124
  import { constants as constants2 } from "node:fs";
4033
4125
  import {
4034
4126
  access as access2,
@@ -4038,7 +4130,7 @@ import {
4038
4130
  realpath as realpath2,
4039
4131
  rename as rename2,
4040
4132
  rm as rm2,
4041
- stat,
4133
+ stat as stat2,
4042
4134
  writeFile as writeFile3
4043
4135
  } from "node:fs/promises";
4044
4136
  import { delimiter as delimiter2, dirname as dirname3, extname as extname2, isAbsolute as isAbsolute2, join as join4, relative } from "node:path";
@@ -4139,7 +4231,7 @@ var validateExecutable = async (candidate, installationRoot) => {
4139
4231
  const uid = process.getuid?.();
4140
4232
  const writableRoot = installationRoot === undefined ? undefined : await realpath2(installationRoot);
4141
4233
  for (let path = canonical;; path = dirname3(path)) {
4142
- const metadata = await stat(path);
4234
+ const metadata = await stat2(path);
4143
4235
  const trustedGroupWritablePath = isTrustedGroupWritablePath({
4144
4236
  platform: operatingSystem(),
4145
4237
  canonical,
@@ -4285,16 +4377,23 @@ var serviceCandidates = (runtimePlatform, uid) => {
4285
4377
  return [];
4286
4378
  };
4287
4379
  var defaultRunner = {
4288
- run: (command, arguments_, timeoutMs = 300000) => new Promise((resolve2) => {
4289
- execFile2(command, [...arguments_], { encoding: "utf8", maxBuffer: 1024 * 1024, timeout: timeoutMs, windowsHide: true }, (error, stdout, stderr) => {
4290
- resolve2({
4291
- exitCode: typeof error?.code === "number" ? error.code : error === null ? 0 : 1,
4292
- stdout,
4293
- stderr,
4294
- ...error === null ? {} : { error: error.message }
4295
- });
4380
+ run: async (command, arguments_, timeoutMs = 300000) => {
4381
+ const result = await execa2(command, arguments_, {
4382
+ encoding: "utf8",
4383
+ maxBuffer: 1024 * 1024,
4384
+ timeout: timeoutMs,
4385
+ windowsHide: true,
4386
+ stdin: "ignore",
4387
+ reject: false,
4388
+ stripFinalNewline: false
4296
4389
  });
4297
- })
4390
+ return {
4391
+ exitCode: result.failed ? result.exitCode || 1 : 0,
4392
+ stdout: result.stdout,
4393
+ stderr: result.stderr,
4394
+ ...result.shortMessage === undefined ? {} : { error: result.shortMessage.slice(0, 16384) }
4395
+ };
4396
+ }
4298
4397
  };
4299
4398
  var checked = async (runner, invocation, timeoutMs) => {
4300
4399
  const result = await runner.run(invocation.command, invocation.arguments, timeoutMs);
@@ -4530,8 +4629,20 @@ var upgrade = async (options) => {
4530
4629
 
4531
4630
  // apps/device/src/service.ts
4532
4631
  var xml = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
4533
- var systemdArgument = (value) => `"${value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"")}"`;
4632
+ var systemdArgument = (value) => `"${value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"").replaceAll("%", "%%").replaceAll(`
4633
+ `, "\\n").replaceAll("\r", "\\r")}"`;
4534
4634
  var deviceServicePlan = (options) => {
4635
+ const servicePath = [
4636
+ dirname4(options.nodeExecutable),
4637
+ join5(options.home, ".local", "bin"),
4638
+ join5(options.home, ".npm-global", "bin"),
4639
+ join5(options.home, ".bun", "bin"),
4640
+ options.platform === "darwin" ? join5(options.home, "Library", "pnpm") : join5(options.home, ".local", "share", "pnpm"),
4641
+ ...options.platform === "darwin" ? ["/opt/homebrew/bin"] : [],
4642
+ "/usr/local/bin",
4643
+ "/usr/bin",
4644
+ "/bin"
4645
+ ].join(":");
4535
4646
  if (options.platform === "linux") {
4536
4647
  const path = join5(options.home, ".config", "systemd", "user", "cohall-device.service");
4537
4648
  return {
@@ -4545,7 +4656,8 @@ Wants=network-online.target
4545
4656
 
4546
4657
  [Service]
4547
4658
  Type=simple
4548
- Environment=PATH=%h/.local/bin:%h/.npm-global/bin:%h/.bun/bin:%h/.local/share/pnpm:/usr/local/bin:/usr/bin:/bin
4659
+ Environment=${systemdArgument(`PATH=${servicePath}`)}
4660
+ Environment=${systemdArgument(`COHALL_CONFIG=${options.configPath}`)}
4549
4661
  ExecStart=${systemdArgument(options.entrypoint)} device
4550
4662
  Restart=always
4551
4663
  RestartSec=3
@@ -4559,7 +4671,8 @@ WantedBy=default.target
4559
4671
  },
4560
4672
  commands: [
4561
4673
  { command: "systemctl", arguments: ["--user", "daemon-reload"] },
4562
- { command: "systemctl", arguments: ["--user", "enable", "--now", "cohall-device.service"] }
4674
+ { command: "systemctl", arguments: ["--user", "enable", "cohall-device.service"] },
4675
+ { command: "systemctl", arguments: ["--user", "restart", "cohall-device.service"] }
4563
4676
  ],
4564
4677
  note: "The device starts after login. Run loginctl enable-linger $USER if it must run before login."
4565
4678
  };
@@ -4586,7 +4699,9 @@ WantedBy=default.target
4586
4699
  <key>EnvironmentVariables</key>
4587
4700
  <dict>
4588
4701
  <key>PATH</key>
4589
- <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
4702
+ <string>${xml(servicePath)}</string>
4703
+ <key>COHALL_CONFIG</key>
4704
+ <string>${xml(options.configPath)}</string>
4590
4705
  </dict>
4591
4706
  <key>RunAtLoad</key>
4592
4707
  <true/>
@@ -4618,7 +4733,7 @@ var defaultRunner2 = {
4618
4733
  run: async (command, arguments_, timeoutMs = 60000) => {
4619
4734
  const executable = await trustedExecutable(command);
4620
4735
  return new Promise((resolve2) => {
4621
- execFile3(executable, [...arguments_], { encoding: "utf8", maxBuffer: 1024 * 1024, timeout: timeoutMs, windowsHide: true }, (error, stdout, stderr) => resolve2({
4736
+ execFile2(executable, [...arguments_], { encoding: "utf8", maxBuffer: 1024 * 1024, timeout: timeoutMs, windowsHide: true }, (error, stdout, stderr) => resolve2({
4622
4737
  exitCode: typeof error?.code === "number" ? error.code : error === null ? 0 : 1,
4623
4738
  stdout,
4624
4739
  stderr,
@@ -4646,7 +4761,13 @@ var installDeviceService = async (options = {}) => {
4646
4761
  "-ExecutionPolicy",
4647
4762
  "Bypass",
4648
4763
  "-File",
4649
- script
4764
+ script,
4765
+ "-NodeExecutable",
4766
+ process.execPath,
4767
+ "-Entrypoint",
4768
+ entrypoint,
4769
+ "-ConfigurationPath",
4770
+ configurationPath()
4650
4771
  ]);
4651
4772
  return { installed: "scheduled-task:Cohall Device" };
4652
4773
  }
@@ -4655,6 +4776,8 @@ var installDeviceService = async (options = {}) => {
4655
4776
  platform: platform4,
4656
4777
  entrypoint,
4657
4778
  home: options.home ?? homedir3(),
4779
+ nodeExecutable: process.execPath,
4780
+ configPath: configurationPath(),
4658
4781
  ...uid === undefined ? {} : { uid }
4659
4782
  });
4660
4783
  await mkdir4(dirname4(plan.file.path), { recursive: true, mode: 448 });
@@ -4738,7 +4861,7 @@ var verifyBackupDestinationParent = async (path) => {
4738
4861
  return;
4739
4862
  }
4740
4863
  for (let current = await realpath4(path);; current = dirname5(current)) {
4741
- const metadata = await stat2(current);
4864
+ const metadata = await stat3(current);
4742
4865
  const writableByOthers = (metadata.mode & 18) !== 0;
4743
4866
  const sticky = (metadata.mode & 512) !== 0;
4744
4867
  if (writableByOthers && !sticky) {
@@ -4759,7 +4882,7 @@ var ownerToken = async (dataDirectory) => {
4759
4882
  if (!await exists(path)) {
4760
4883
  throw new Error(`Relay owner token was not found at ${path}`);
4761
4884
  }
4762
- if ((await stat2(path)).size > 512) {
4885
+ if ((await stat3(path)).size > 512) {
4763
4886
  throw new Error("Relay owner token file exceeds 512 bytes");
4764
4887
  }
4765
4888
  return (await readFile3(path, "utf8")).trim();
@@ -5165,7 +5288,7 @@ var readInput = async (arguments_, name) => {
5165
5288
  if (!await access4(path).then(() => true).catch(() => false)) {
5166
5289
  throw new Error(`File does not exist: ${path}`);
5167
5290
  }
5168
- if ((await stat3(path)).size > byteLimit) {
5291
+ if ((await stat4(path)).size > byteLimit) {
5169
5292
  throw new Error(`${name} file exceeds ${byteLimit / 1024} KiB`);
5170
5293
  }
5171
5294
  }
@@ -5200,7 +5323,7 @@ var pairingToken = async (arguments_) => {
5200
5323
  if (path !== undefined && !await access4(path).then(() => true).catch(() => false)) {
5201
5324
  throw new Error(`Pairing token file does not exist: ${path}`);
5202
5325
  }
5203
- if (path !== undefined && (await stat3(path)).size > 512) {
5326
+ if (path !== undefined && (await stat4(path)).size > 512) {
5204
5327
  throw new Error("Pairing token file exceeds 512 bytes");
5205
5328
  }
5206
5329
  const token = (path === undefined ? await readStdin(512, "Pairing token") : await readFile4(path, "utf8")).trim();
@@ -5698,6 +5821,7 @@ var runCli = async (command, raw) => {
5698
5821
  "timeout",
5699
5822
  "workspace"
5700
5823
  ]);
5824
+ const waitSeconds = timeout(arguments_);
5701
5825
  const positionalTarget = command === "send" && arguments_.positionals[0]?.startsWith("@") ? arguments_.positionals[0] : undefined;
5702
5826
  const positionals = positionalTarget === undefined ? arguments_.positionals : arguments_.positionals.slice(1);
5703
5827
  if (positionalTarget !== undefined && option(arguments_, "target") !== undefined) {
@@ -5729,7 +5853,7 @@ var runCli = async (command, raw) => {
5729
5853
  ...workspace === undefined ? {} : { workspace },
5730
5854
  ...provider === undefined ? {} : { provider: Schema10.decodeUnknownSync(Provider)(provider) }
5731
5855
  }));
5732
- print(taskResult(arguments_.options.has("no-wait") ? task : await Effect9.runPromise(waitForTask(relay, task, timeout(arguments_)))));
5856
+ print(taskResult(arguments_.options.has("no-wait") ? task : await Effect9.runPromise(waitForTask(relay, task, waitSeconds))));
5733
5857
  return;
5734
5858
  }
5735
5859
  if (command === "status") {
@@ -5774,7 +5898,7 @@ var runCli = async (command, raw) => {
5774
5898
  init_src();
5775
5899
  import { Effect as Effect10, Schedule as Schedule3, Schema as Schema11 } from "effect";
5776
5900
  import { constants as constants3 } from "node:fs";
5777
- import { open as open3, realpath as realpath5, stat as stat4 } from "node:fs/promises";
5901
+ import { open as open3, realpath as realpath5, stat as stat5 } from "node:fs/promises";
5778
5902
  import { arch, hostname as hostname3, platform as platform4 } from "node:os";
5779
5903
  import { basename as basename2, isAbsolute as isAbsolute3, relative as relative2 } from "node:path";
5780
5904
  import { WebSocket as WebSocket2 } from "ws";
@@ -5879,7 +6003,7 @@ var openAllowedWorkspace = async (configuration, requested) => {
5879
6003
  }
5880
6004
  if (operatingSystem2 !== "linux") {
5881
6005
  const currentPath = await realpath5(path);
5882
- const currentPathIdentity = await stat4(currentPath);
6006
+ const currentPathIdentity = await stat5(currentPath);
5883
6007
  if (currentPathIdentity.dev !== identity.dev || currentPathIdentity.ino !== identity.ino) {
5884
6008
  throw new Error(`Workspace ${path} changed before provider startup`);
5885
6009
  }
@@ -6390,4 +6514,4 @@ await main().catch((cause) => {
6390
6514
  process.exitCode = 1;
6391
6515
  });
6392
6516
 
6393
- //# debugId=78963F23872A4D6564756E2164756E21
6517
+ //# debugId=B9C02E5590F539C264756E2164756E21