@akshar5/cohall 0.4.0 → 0.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.2](https://github.com/AksharP5/cohall/compare/v0.4.1...v0.4.2) (2026-08-07)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **upgrade:** detect service installation mismatches ([19bf066](https://github.com/AksharP5/cohall/commit/19bf06674d6fac87576f804e92d863d5c673dda0))
9
+
10
+ ## [0.4.1](https://github.com/AksharP5/cohall/compare/v0.4.0...v0.4.1) (2026-08-07)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **device:** bound relay message buffering ([15b975e](https://github.com/AksharP5/cohall/commit/15b975eb658ac8b1140ed0481ab4726543265ca4))
16
+ * **providers:** bound JSON events while streaming ([7e0a4fc](https://github.com/AksharP5/cohall/commit/7e0a4fccc1b68e630b0b8bb553bbb66b84795798))
17
+ * **providers:** preserve custom config for nested delegation ([#21](https://github.com/AksharP5/cohall/issues/21)) ([63c95f8](https://github.com/AksharP5/cohall/commit/63c95f84f00ad03ad8d017696a5a87cbb8c6c05d))
18
+ * **upgrade:** restart already-current services ([a2c775f](https://github.com/AksharP5/cohall/commit/a2c775fe48a4dffda54ea7537f34b1a2b6d1e6ed))
19
+
3
20
  ## [0.4.0](https://github.com/AksharP5/cohall/compare/v0.3.4...v0.4.0) (2026-08-07)
4
21
 
5
22
 
package/README.md CHANGED
@@ -223,7 +223,10 @@ status, provider selection, executable paths, and version information.
223
223
 
224
224
  For a global installation used by services, `cohall upgrade` updates through
225
225
  the same npm, Bun, or pnpm installation and restarts only active Cohall relay
226
- and device jobs. Use `cohall upgrade --dry-run` to inspect the plan or
226
+ and device jobs. Active jobs restart even when the installed files are already
227
+ current, ensuring an older loaded process is replaced. Cohall refuses to restart
228
+ a service configured to use a different global installation; run that service's
229
+ executable directly or update its service definition first. Use `cohall upgrade --dry-run` to inspect the plan or
227
230
  `cohall upgrade --no-restart` to update files without restarting services.
228
231
  Environment variables override stored values:
229
232
 
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 { Schema } from "effect";
20
- var version = "0.4.0", 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)), 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.4.2", 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)), 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)), DeviceId, ThreadId, MessageId, TaskId, AuthSessionId, Timestamp, Platform, Provider, DeviceStatus, TaskStatus, TaskTraceEventKind, MessageRole, ConnectionRole, AuthSession, CreatePairingInput, PairingCredential, ExchangePairingInput, SessionCredential, PairingResult, Capability, Workspace, Device, Thread, Message, Task, TaskTraceEvent, TaskTrace, ThreadContext, CreateTaskInput, SocketEvent, ErrorResponse, terminalTaskStatuses, isTerminalTask = (task) => terminalTaskStatuses.has(task.status), now = () => Timestamp.make(new Date().toISOString()), makeDeviceId = () => DeviceId.make(crypto.randomUUID()), makeThreadId = () => ThreadId.make(crypto.randomUUID()), makeMessageId = () => MessageId.make(crypto.randomUUID()), makeTaskId = () => TaskId.make(crypto.randomUUID()), makeAuthSessionId = () => AuthSessionId.make(crypto.randomUUID()), decodeCreatePairingInput, decodeExchangePairingInput, decodeCreateTaskInput, decodeDevice, decodeSocketEvent;
23
23
  var init_src = __esm(() => {
@@ -1742,7 +1742,6 @@ import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises";
1742
1742
  import { homedir, platform, tmpdir } from "node:os";
1743
1743
  import { delimiter, extname, isAbsolute, join } from "node:path";
1744
1744
  import { spawn } from "node:child_process";
1745
- import { createInterface } from "node:readline";
1746
1745
 
1747
1746
  class ProviderUnavailableError extends Schema3.TaggedErrorClass()("CohallProvider.Unavailable", { provider: Provider, message: Schema3.String }) {
1748
1747
  }
@@ -1759,7 +1758,7 @@ var executables = {
1759
1758
  opencode: "opencode"
1760
1759
  };
1761
1760
  var providerEnvironment = () => ({
1762
- ...Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.startsWith("COHALL_"))),
1761
+ ...Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.startsWith("COHALL_") || name === "COHALL_CONFIG")),
1763
1762
  PATH: executableDirectories().join(delimiter)
1764
1763
  });
1765
1764
  var record = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? Object.fromEntries(Object.entries(value)) : undefined;
@@ -1927,22 +1926,43 @@ var prepareCommand = async (options) => {
1927
1926
  }
1928
1927
  };
1929
1928
  var forEachJsonEvent = async (stdout, onEvent) => {
1930
- const lines = createInterface({ input: stdout, crlfDelay: Infinity });
1931
- try {
1932
- for await (const line of lines) {
1933
- if (line.trim().length === 0) {
1934
- continue;
1935
- }
1936
- if (Buffer.byteLength(line) > 1024 * 1024) {
1937
- throw new Error("Provider event exceeded 1024 KiB");
1938
- }
1939
- const event = record(JSON.parse(line));
1940
- if (event !== undefined) {
1941
- onEvent(event);
1942
- }
1929
+ const limit = 1024 * 1024;
1930
+ let chunks = [];
1931
+ let size = 0;
1932
+ const append = (chunk) => {
1933
+ if (size + chunk.byteLength > limit) {
1934
+ throw new Error("Provider event exceeded 1024 KiB");
1935
+ }
1936
+ if (chunk.byteLength > 0) {
1937
+ chunks.push(chunk);
1938
+ size += chunk.byteLength;
1939
+ }
1940
+ };
1941
+ const emit = () => {
1942
+ const line = Buffer.concat(chunks, size).toString("utf8");
1943
+ chunks = [];
1944
+ size = 0;
1945
+ if (line.trim().length === 0) {
1946
+ return;
1947
+ }
1948
+ const event = record(JSON.parse(line));
1949
+ if (event !== undefined) {
1950
+ onEvent(event);
1951
+ }
1952
+ };
1953
+ for await (const chunk of stdout) {
1954
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
1955
+ let start = 0;
1956
+ for (let newline = bytes.indexOf(10, start);newline !== -1; ) {
1957
+ append(bytes.subarray(start, newline));
1958
+ emit();
1959
+ start = newline + 1;
1960
+ newline = bytes.indexOf(10, start);
1943
1961
  }
1944
- } finally {
1945
- lines.close();
1962
+ append(bytes.subarray(start));
1963
+ }
1964
+ if (size > 0) {
1965
+ emit();
1946
1966
  }
1947
1967
  };
1948
1968
  var parseCodex = async (stdout, existingSession) => {
@@ -2596,6 +2616,7 @@ var normalizeUpgradeTarget = (target) => {
2596
2616
  }
2597
2617
  return normalized;
2598
2618
  };
2619
+ var deviceVersionWarning = (cliVersion, deviceVersion) => deviceVersion === undefined || deviceVersion === cliVersion ? undefined : `Running device daemon uses Cohall ${deviceVersion}, while this CLI is ${cliVersion}. Run upgrade through the executable configured by the device service.`;
2599
2620
  var packageInstallation = (canonicalEntrypoint, entrypoint = canonicalEntrypoint) => {
2600
2621
  const path = normalizePath(canonicalEntrypoint);
2601
2622
  const marker = `/node_modules/${packageName}/`;
@@ -2645,6 +2666,13 @@ var serviceCandidates = (runtimePlatform, uid) => {
2645
2666
  manager,
2646
2667
  device,
2647
2668
  check: { command: "systemctl", arguments: [...user, "is-active", "--quiet", unit] },
2669
+ entrypoint: {
2670
+ inspect: {
2671
+ command: "systemctl",
2672
+ arguments: [...user, "show", "--property=ExecStart", "--value", unit]
2673
+ },
2674
+ parse: (output) => output.match(/\bpath=(.*?)\s*;/)?.[1]?.trim()
2675
+ },
2648
2676
  restart: [{ command: "systemctl", arguments: [...user, "restart", unit] }]
2649
2677
  };
2650
2678
  };
@@ -2663,6 +2691,10 @@ var serviceCandidates = (runtimePlatform, uid) => {
2663
2691
  device,
2664
2692
  check: { command: "launchctl", arguments: ["print", `${domain}/${label}`] },
2665
2693
  activeOutput: /\bstate\s*=\s*running\b/,
2694
+ entrypoint: {
2695
+ inspect: { command: "launchctl", arguments: ["print", `${domain}/${label}`] },
2696
+ parse: (output) => output.match(/^\s*program\s*=\s*(.+)$/m)?.[1]?.trim()
2697
+ },
2666
2698
  restart: [{ command: "launchctl", arguments: ["kickstart", "-k", `${domain}/${label}`] }]
2667
2699
  });
2668
2700
  return [launchd("com.cohall.relay", false), launchd("com.cohall.device", true)];
@@ -2727,6 +2759,26 @@ ${result.stderr}`))) {
2727
2759
  }
2728
2760
  return active;
2729
2761
  };
2762
+ var assertServiceInstallations = async (runner, services, canonicalEntrypoint, entrypoint) => {
2763
+ for (const service of services) {
2764
+ if (service.entrypoint === undefined) {
2765
+ continue;
2766
+ }
2767
+ const result = await checked(runner, service.entrypoint.inspect, 1e4);
2768
+ const serviceEntrypoint = service.entrypoint.parse(`${result.stdout}
2769
+ ${result.stderr}`);
2770
+ if (serviceEntrypoint === undefined) {
2771
+ throw new Error(`Could not determine the executable used by active ${service.label}`);
2772
+ }
2773
+ const canonicalServiceEntrypoint = await realpath2(serviceEntrypoint).catch((cause) => {
2774
+ throw new Error(`Could not resolve the executable used by active ${service.label}: ${cause instanceof Error ? cause.message : String(cause)}`);
2775
+ });
2776
+ if (canonicalServiceEntrypoint === canonicalEntrypoint) {
2777
+ continue;
2778
+ }
2779
+ throw new Error(`Active ${service.label} uses ${serviceEntrypoint}, but this Cohall CLI uses ${entrypoint}. Run the service executable's upgrade command so its installation is updated before restart.`);
2780
+ }
2781
+ };
2730
2782
  var readReceipt = async (path) => readFile2(path, "utf8").then((content) => Schema6.decodeUnknownOption(RestartReceipt)(JSON.parse(content))).then((decoded) => decoded._tag === "Some" ? decoded.value : undefined).catch(() => {
2731
2783
  return;
2732
2784
  });
@@ -2827,9 +2879,11 @@ var upgrade = async (options) => {
2827
2879
  if (entrypoint === undefined) {
2828
2880
  throw new Error("Could not resolve the Cohall executable path");
2829
2881
  }
2830
- const installation = packageInstallation(await realpath2(entrypoint), entrypoint);
2882
+ const canonicalEntrypoint = await realpath2(entrypoint);
2883
+ const installation = packageInstallation(canonicalEntrypoint, entrypoint);
2831
2884
  const install = packageInstallCommand(installation, target);
2832
2885
  const services = await activeServices(runner, candidates);
2886
+ await assertServiceInstallations(runner, services, canonicalEntrypoint, entrypoint);
2833
2887
  if (options.dryRun) {
2834
2888
  return {
2835
2889
  upgraded: false,
@@ -2849,7 +2903,7 @@ var upgrade = async (options) => {
2849
2903
  throw new Error(`Installed Cohall ${nextVersion}, expected ${target}`);
2850
2904
  }
2851
2905
  const upgraded = nextVersion !== options.currentVersion;
2852
- if (!upgraded || !options.restart || services.length === 0) {
2906
+ if (!options.restart || services.length === 0) {
2853
2907
  return {
2854
2908
  upgraded,
2855
2909
  from_version: options.currentVersion,
@@ -2857,7 +2911,7 @@ var upgrade = async (options) => {
2857
2911
  requested_version: target,
2858
2912
  package_manager: installation.manager,
2859
2913
  services_restarted: [],
2860
- services_pending_restart: upgraded && !options.restart ? services.map((service) => service.id) : [],
2914
+ services_pending_restart: options.restart ? [] : services.map((service) => service.id),
2861
2915
  resumed_after_restart: false,
2862
2916
  dry_run: false
2863
2917
  };
@@ -2873,7 +2927,7 @@ var upgrade = async (options) => {
2873
2927
  const completed = await restartServices(runner, services, statePath, initial);
2874
2928
  await rm2(statePath, { force: true });
2875
2929
  return {
2876
- upgraded: true,
2930
+ upgraded,
2877
2931
  from_version: options.currentVersion,
2878
2932
  installed_version: nextVersion,
2879
2933
  requested_version: target,
@@ -3279,8 +3333,10 @@ var runCli = async (command, raw) => {
3279
3333
  const currentDevice = response?.ok !== true || clientToken === undefined || deviceId === undefined ? undefined : await Effect5.runPromise(exports_src.make({ baseUrl: relayUrl, token: clientToken }).devices()).then((devices) => devices.find((device) => device.id === deviceId)).catch(() => {
3280
3334
  return;
3281
3335
  });
3336
+ const versionWarning = deviceVersionWarning(version, currentDevice?.version);
3282
3337
  const warnings = [
3283
3338
  ...response?.ok === true ? [] : ["Relay is unreachable"],
3339
+ ...versionWarning === undefined ? [] : [versionWarning],
3284
3340
  ...hasDeviceCredential && response?.ok === true && currentDevice?.status !== "online" && currentDevice?.status !== "busy" ? [
3285
3341
  clientToken === undefined ? "Device status cannot be checked without a client credential" : "Device daemon is not connected or is not registered with the relay"
3286
3342
  ] : [],
@@ -3420,6 +3476,8 @@ import { realpath as realpath3 } from "node:fs/promises";
3420
3476
  import { arch, hostname as hostname2, platform as platform3 } from "node:os";
3421
3477
  import { basename, isAbsolute as isAbsolute2, relative } from "node:path";
3422
3478
  import { WebSocket } from "ws";
3479
+ var maxRelayPayloadBytes = 256 * 1024;
3480
+ var maxQueuedRelayMessages = 8;
3423
3481
 
3424
3482
  class DeviceConnectionError extends Schema8.TaggedErrorClass()("Device.ConnectionError", { message: Schema8.String }) {
3425
3483
  }
@@ -3618,7 +3676,10 @@ var cancel = (state, taskId) => {
3618
3676
  };
3619
3677
  var connect = (configuration, state) => Effect6.tryPromise({
3620
3678
  try: (signal) => new Promise((complete) => {
3621
- const socket = new WebSocket(socketUrl(configuration));
3679
+ const socket = new WebSocket(socketUrl(configuration), {
3680
+ maxPayload: maxRelayPayloadBytes,
3681
+ perMessageDeflate: false
3682
+ });
3622
3683
  const heartbeat = setInterval(() => {
3623
3684
  send(state, SocketEvent.make({
3624
3685
  _tag: "DeviceHeartbeat",
@@ -3627,6 +3688,7 @@ var connect = (configuration, state) => Effect6.tryPromise({
3627
3688
  }));
3628
3689
  }, 15000);
3629
3690
  let closed = false;
3691
+ let queuedMessages = 0;
3630
3692
  const close = () => {
3631
3693
  if (closed) {
3632
3694
  return;
@@ -3646,8 +3708,16 @@ var connect = (configuration, state) => Effect6.tryPromise({
3646
3708
  socket.send(JSON.stringify(SocketEvent.make({ _tag: "Authenticate", token: configuration.token })));
3647
3709
  });
3648
3710
  socket.on("message", (message) => {
3649
- const text2 = Buffer.isBuffer(message) ? message.toString("utf8") : message instanceof ArrayBuffer ? Buffer.from(message).toString("utf8") : Array.isArray(message) ? Buffer.concat(message).toString("utf8") : Buffer.from(message).toString("utf8");
3711
+ if (queuedMessages >= maxQueuedRelayMessages) {
3712
+ socket.close(4008, "Message queue limit reached");
3713
+ return;
3714
+ }
3715
+ queuedMessages += 1;
3650
3716
  state.processing = state.processing.then(async () => {
3717
+ if (closed) {
3718
+ return;
3719
+ }
3720
+ const text2 = Buffer.isBuffer(message) ? message.toString("utf8") : message instanceof ArrayBuffer ? Buffer.from(message).toString("utf8") : Array.isArray(message) ? Buffer.concat(message).toString("utf8") : Buffer.from(message).toString("utf8");
3651
3721
  const event = await Effect6.runPromise(Effect6.try({
3652
3722
  try: () => JSON.parse(text2),
3653
3723
  catch: () => new DeviceConnectionError({ message: "Relay sent invalid JSON" })
@@ -3680,6 +3750,8 @@ var connect = (configuration, state) => Effect6.tryPromise({
3680
3750
  }
3681
3751
  }).catch((cause) => {
3682
3752
  console.error(`Invalid relay event: ${cause instanceof Error ? cause.message : String(cause)}`);
3753
+ }).finally(() => {
3754
+ queuedMessages = Math.max(0, queuedMessages - 1);
3683
3755
  });
3684
3756
  });
3685
3757
  socket.once("close", close);
@@ -3826,4 +3898,4 @@ await main().catch((cause) => {
3826
3898
  process.exitCode = 1;
3827
3899
  });
3828
3900
 
3829
- //# debugId=C3D1F939F21B764D64756E2164756E21
3901
+ //# debugId=D69D718E229B95B264756E2164756E21