@akshar5/cohall 0.5.6 → 0.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.
package/bin/cohall.js CHANGED
@@ -16,10 +16,10 @@ var __export = (target, all) => {
16
16
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
17
 
18
18
  // packages/protocol/src/index.ts
19
- import { Schema } from "effect";
20
- var version = "0.5.6", 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$/, {
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$/, {
21
21
  expected: "an ISO-8601 UTC timestamp"
22
- })).pipe(Schema.brand(name)), 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) => {
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);
24
24
  if (match === null) {
25
25
  return;
@@ -50,10 +50,25 @@ var version = "0.5.6", maxSocketPayloadBytes, bounded = (maxLength) => Schema.St
50
50
  return;
51
51
  }
52
52
  throw new Error(`All-device upgrades require Cohall ${minimumDeviceOperationVersion} or newer on every device. Upgrade individually first: ${unsupported.map((device) => `${device.name} (${device.version})`).join(", ")}`);
53
- }, Thread, Message, Task, upgradeVersion, CreateUpgradeOperationsInput, DeviceOperation, TaskStatusCounts, ProviderUsage, DeviceUsage, UsageSummary, 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()), makeOperationId = () => OperationId.make(crypto.randomUUID()), decodeCreatePairingInput, decodeExchangePairingInput, decodeCreateTaskInput, decodeCreateUpgradeOperationsInput, decodeDevice, decodeSocketEvent;
53
+ }, Thread, Message, validBotTarget = (input) => {
54
+ const provider = input.provider ?? (input.botId === undefined ? "codex" : "grok-bot");
55
+ if (provider === "grok-bot" && input.workspace !== undefined) {
56
+ return "Grok Bots use their own computer context; omit workspace";
57
+ }
58
+ return provider === "grok-bot" === (input.botId !== undefined) ? undefined : "The grok-bot provider requires botId; other providers cannot select a bot";
59
+ }, Task, taskSlot = (task) => task.provider === "grok-bot" ? `grok-bot:${task.botId}` : "cli", upgradeVersion, CreateUpgradeOperationsInput, DeviceOperation, TaskStatusCounts, ProviderUsage, DeviceUsage, UsageSummary, 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()), makeOperationId = () => OperationId.make(crypto.randomUUID()), decodeCreatePairingInput, decodeExchangePairingInput, decodeCreateTaskInput = (input) => Schema.decodeUnknownEffect(CreateTaskInput)(input).pipe(Effect.map((task) => ({
60
+ ...task,
61
+ provider: task.provider ?? (task.botId === undefined ? "codex" : "grok-bot")
62
+ }))), decodeCreateUpgradeOperationsInput, decodeDevice, decodeSocketEvent;
54
63
  var init_src = __esm(() => {
55
64
  maxSocketPayloadBytes = 1024 * 1024;
56
65
  count = Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER }));
66
+ BotId = bounded(256).pipe(Schema.brand("BotId"));
67
+ Bot = Schema.Struct({
68
+ id: BotId,
69
+ name: bounded(128),
70
+ description: Schema.optionalKey(optionalText(512))
71
+ });
57
72
  DeviceId = uuid("DeviceId");
58
73
  ThreadId = uuid("ThreadId");
59
74
  MessageId = uuid("MessageId");
@@ -62,7 +77,7 @@ var init_src = __esm(() => {
62
77
  OperationId = uuid("OperationId");
63
78
  Timestamp = isoTimestamp("Timestamp");
64
79
  Platform = Schema.Literals(["darwin", "linux", "windows", "unknown"]);
65
- Provider = Schema.Literals(["codex", "claude-code", "opencode"]);
80
+ Provider = Schema.Literals(["codex", "claude-code", "opencode", "grok-bot"]);
66
81
  DeviceStatus = Schema.Literals(["online", "busy", "offline"]);
67
82
  TaskStatus = Schema.Literals([
68
83
  "queued",
@@ -127,7 +142,8 @@ var init_src = __esm(() => {
127
142
  platform: Platform,
128
143
  architecture: bounded(64),
129
144
  status: DeviceStatus,
130
- providers: boundedArray(Provider, 3),
145
+ providers: boundedArray(Provider, Provider.literals.length),
146
+ bots: Schema.optionalKey(boundedArray(Bot, 256)),
131
147
  capabilities: boundedArray(Capability, 64),
132
148
  workspaces: boundedArray(Workspace, 64),
133
149
  version: bounded(32),
@@ -157,6 +173,7 @@ var init_src = __esm(() => {
157
173
  prompt: bounded(131072),
158
174
  context: Schema.optionalKey(optionalText(131072)),
159
175
  provider: Provider,
176
+ botId: Schema.optionalKey(BotId),
160
177
  status: TaskStatus,
161
178
  sourceDeviceId: Schema.optionalKey(DeviceId),
162
179
  targetDeviceId: DeviceId,
@@ -169,7 +186,7 @@ var init_src = __esm(() => {
169
186
  updatedAt: Timestamp,
170
187
  startedAt: Schema.optionalKey(Timestamp),
171
188
  completedAt: Schema.optionalKey(Timestamp)
172
- });
189
+ }).check(Schema.makeFilter(validBotTarget));
173
190
  upgradeVersion = Schema.String.check(Schema.isPattern(/^(?:latest|v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)$/, {
174
191
  expected: "latest or an exact semantic version"
175
192
  }));
@@ -208,12 +225,12 @@ var init_src = __esm(() => {
208
225
  deviceName: bounded(128),
209
226
  tasks: count,
210
227
  byStatus: TaskStatusCounts,
211
- byProvider: boundedArray(ProviderUsage, 3)
228
+ byProvider: boundedArray(ProviderUsage, Provider.literals.length)
212
229
  });
213
230
  UsageSummary = Schema.Struct({
214
231
  retainedTasks: count,
215
232
  byStatus: TaskStatusCounts,
216
- byProvider: boundedArray(ProviderUsage, 3),
233
+ byProvider: boundedArray(ProviderUsage, Provider.literals.length),
217
234
  devices: boundedArray(DeviceUsage, 256)
218
235
  });
219
236
  TaskTraceEvent = Schema.Struct({
@@ -226,6 +243,7 @@ var init_src = __esm(() => {
226
243
  threadId: ThreadId,
227
244
  status: TaskStatus,
228
245
  provider: Provider,
246
+ botId: Schema.optionalKey(BotId),
229
247
  sourceDeviceId: Schema.optionalKey(DeviceId),
230
248
  targetDevice: Device,
231
249
  parentTaskId: Schema.optionalKey(TaskId),
@@ -237,7 +255,7 @@ var init_src = __esm(() => {
237
255
  events: boundedArray(TaskTraceEvent, 100),
238
256
  truncated: Schema.Boolean,
239
257
  error: Schema.optionalKey(optionalText(16384))
240
- });
258
+ }).check(Schema.makeFilter(validBotTarget));
241
259
  ThreadContext = Schema.Struct({
242
260
  thread: Thread,
243
261
  messages: boundedArray(Message, 20),
@@ -250,15 +268,20 @@ var init_src = __esm(() => {
250
268
  prompt: bounded(131072),
251
269
  context: Schema.optionalKey(optionalText(131072)),
252
270
  provider: Schema.optionalKey(Provider),
271
+ botId: Schema.optionalKey(BotId),
253
272
  targetDeviceId: Schema.optionalKey(DeviceId),
254
273
  parentTaskId: Schema.optionalKey(TaskId),
255
274
  workspace: Schema.optionalKey(bounded(4096))
256
- });
275
+ }).check(Schema.makeFilter(validBotTarget));
257
276
  SocketEvent = Schema.TaggedUnion({
258
277
  Authenticate: { token: bounded(256) },
259
278
  Connected: { serverVersion: bounded(32), connectedAt: Timestamp },
260
279
  DeviceHello: { device: Device },
261
- DeviceHeartbeat: { deviceId: DeviceId, status: DeviceStatus },
280
+ DeviceHeartbeat: {
281
+ deviceId: DeviceId,
282
+ status: DeviceStatus,
283
+ bots: Schema.optionalKey(boundedArray(Bot, 256))
284
+ },
262
285
  TaskAssigned: { task: Task },
263
286
  TaskAccepted: { taskId: TaskId },
264
287
  TaskFinished: {
@@ -287,25 +310,24 @@ var init_src = __esm(() => {
287
310
  ]);
288
311
  decodeCreatePairingInput = Schema.decodeUnknownEffect(CreatePairingInput);
289
312
  decodeExchangePairingInput = Schema.decodeUnknownEffect(ExchangePairingInput);
290
- decodeCreateTaskInput = Schema.decodeUnknownEffect(CreateTaskInput);
291
313
  decodeCreateUpgradeOperationsInput = Schema.decodeUnknownEffect(CreateUpgradeOperationsInput);
292
314
  decodeDevice = Schema.decodeUnknownEffect(Device);
293
315
  decodeSocketEvent = Schema.decodeUnknownEffect(SocketEvent);
294
316
  });
295
317
 
296
318
  // apps/relay/src/config.ts
297
- import { Effect as Effect10, Schema as Schema10 } from "effect";
298
- import { access as access5, chmod as chmod6, mkdir as mkdir6, readFile as readFile5, writeFile as writeFile7 } from "node:fs/promises";
299
- import { homedir as homedir5, platform as platform4 } from "node:os";
300
- import { join as join7, resolve as resolve3 } from "node:path";
319
+ import { Effect as Effect12, Schema as Schema12 } from "effect";
320
+ import { access as access5, chmod as chmod7, mkdir as mkdir7, readFile as readFile5, writeFile as writeFile7 } from "node:fs/promises";
321
+ import { homedir as homedir5, platform as platform5 } from "node:os";
322
+ import { join as join8, resolve as resolve4 } from "node:path";
301
323
  var RelayConfiguration, defaultDataDirectory = () => {
302
- if (platform4() === "win32") {
303
- return join7(process.env.LOCALAPPDATA ?? join7(homedir5(), "AppData", "Local"), "Cohall");
324
+ if (platform5() === "win32") {
325
+ return join8(process.env.LOCALAPPDATA ?? join8(homedir5(), "AppData", "Local"), "Cohall");
304
326
  }
305
- if (platform4() === "darwin") {
306
- return join7(homedir5(), "Library", "Application Support", "Cohall", "relay");
327
+ if (platform5() === "darwin") {
328
+ return join8(homedir5(), "Library", "Application Support", "Cohall", "relay");
307
329
  }
308
- return join7(process.env.XDG_DATA_HOME ?? join7(homedir5(), ".local", "share"), "cohall");
330
+ return join8(process.env.XDG_DATA_HOME ?? join8(homedir5(), ".local", "share"), "cohall");
309
331
  }, generatedToken = () => Array.from(crypto.getRandomValues(new Uint8Array(32)), (value) => value.toString(16).padStart(2, "0")).join(""), tokenFrom = async (path) => {
310
332
  if (await access5(path).then(() => true).catch(() => false)) {
311
333
  return (await readFile5(path, "utf8")).trim();
@@ -313,8 +335,8 @@ var RelayConfiguration, defaultDataDirectory = () => {
313
335
  const token = generatedToken();
314
336
  await writeFile7(path, `${token}
315
337
  `, { mode: 384 });
316
- if (platform4() !== "win32") {
317
- await chmod6(path, 384);
338
+ if (platform5() !== "win32") {
339
+ await chmod7(path, 384);
318
340
  }
319
341
  return token;
320
342
  }, booleanEnvironment = (name) => {
@@ -328,16 +350,16 @@ var RelayConfiguration, defaultDataDirectory = () => {
328
350
  throw new Error(`${name} must be true or false`);
329
351
  }, isLoopback2 = (host) => host === "127.0.0.1" || host === "localhost" || host === "::1", loadEnvironmentConfiguration;
330
352
  var init_config = __esm(() => {
331
- RelayConfiguration = Schema10.Struct({
332
- host: Schema10.NonEmptyString,
333
- port: Schema10.Int.check(Schema10.isBetween({ minimum: 1, maximum: 65535 })),
334
- token: Schema10.String.check(Schema10.isMinLength(32)),
335
- dataDirectory: Schema10.NonEmptyString,
336
- databasePath: Schema10.NonEmptyString,
337
- allowRemote: Schema10.Boolean,
338
- historyTaskLimit: Schema10.Int.check(Schema10.isBetween({ minimum: 100, maximum: 1e5 }))
353
+ RelayConfiguration = Schema12.Struct({
354
+ host: Schema12.NonEmptyString,
355
+ port: Schema12.Int.check(Schema12.isBetween({ minimum: 1, maximum: 65535 })),
356
+ token: Schema12.String.check(Schema12.isMinLength(32)),
357
+ dataDirectory: Schema12.NonEmptyString,
358
+ databasePath: Schema12.NonEmptyString,
359
+ allowRemote: Schema12.Boolean,
360
+ historyTaskLimit: Schema12.Int.check(Schema12.isBetween({ minimum: 100, maximum: 1e5 }))
339
361
  });
340
- loadEnvironmentConfiguration = Effect10.tryPromise({
362
+ loadEnvironmentConfiguration = Effect12.tryPromise({
341
363
  try: async () => {
342
364
  const host = process.env.COHALL_RELAY_HOST ?? "127.0.0.1";
343
365
  const port = Number(process.env.COHALL_RELAY_PORT ?? "8787");
@@ -345,22 +367,22 @@ var init_config = __esm(() => {
345
367
  if (!isLoopback2(host) && !allowRemote) {
346
368
  throw new Error("Remote relay binding is disabled. Use a private network or TLS reverse proxy, then set COHALL_RELAY_ALLOW_REMOTE=true.");
347
369
  }
348
- const dataDirectory = resolve3(process.env.COHALL_DATA_DIR ?? defaultDataDirectory());
349
- await mkdir6(dataDirectory, { recursive: true, mode: 448 });
350
- if (platform4() !== "win32") {
351
- await chmod6(dataDirectory, 448);
370
+ const dataDirectory = resolve4(process.env.COHALL_DATA_DIR ?? defaultDataDirectory());
371
+ await mkdir7(dataDirectory, { recursive: true, mode: 448 });
372
+ if (platform5() !== "win32") {
373
+ await chmod7(dataDirectory, 448);
352
374
  }
353
- const token = process.env.COHALL_TOKEN ?? await tokenFrom(join7(dataDirectory, "owner-token"));
375
+ const token = process.env.COHALL_TOKEN ?? await tokenFrom(join8(dataDirectory, "owner-token"));
354
376
  if (token.length < 32) {
355
377
  throw new Error("The Cohall relay owner token must be at least 32 characters");
356
378
  }
357
379
  const historyTaskLimit = Number(process.env.COHALL_HISTORY_TASK_LIMIT ?? "1000");
358
- return Schema10.decodeUnknownSync(RelayConfiguration)({
380
+ return Schema12.decodeUnknownSync(RelayConfiguration)({
359
381
  host,
360
382
  port,
361
383
  token,
362
384
  dataDirectory,
363
- databasePath: join7(dataDirectory, "cohall.db"),
385
+ databasePath: join8(dataDirectory, "cohall.db"),
364
386
  allowRemote,
365
387
  historyTaskLimit
366
388
  });
@@ -540,18 +562,18 @@ __export(exports_store, {
540
562
  RelayStore: () => exports_store,
541
563
  PersistenceError: () => PersistenceError
542
564
  });
543
- import { Context as Context2, Effect as Effect11, Layer as Layer2, Schema as Schema11 } from "effect";
565
+ import { Context as Context2, Effect as Effect13, Layer as Layer2, Schema as Schema13 } from "effect";
544
566
  import { createHash as createHash2, randomBytes } from "node:crypto";
545
567
  var PersistenceError, Service2, operationError = (operation) => (cause) => new PersistenceError({
546
568
  operation,
547
569
  message: cause instanceof Error ? cause.message : String(cause)
548
- }), decode = (operation, schema, value) => Schema11.decodeUnknownEffect(schema)(value).pipe(Effect11.mapError(operationError(operation))), threadFromRow = (row) => decode("RelayStore.decodeThread", Thread, {
570
+ }), decode2 = (operation, schema, value) => Schema13.decodeUnknownEffect(schema)(value).pipe(Effect13.mapError(operationError(operation))), threadFromRow = (row) => decode2("RelayStore.decodeThread", Thread, {
549
571
  id: row.id,
550
572
  title: row.title,
551
573
  createdAt: row.created_at,
552
574
  updatedAt: row.updated_at,
553
575
  ...row.default_device_id === null ? {} : { defaultDeviceId: row.default_device_id }
554
- }), messageFromRow = (row) => decode("RelayStore.decodeMessage", Message, {
576
+ }), messageFromRow = (row) => decode2("RelayStore.decodeMessage", Message, {
555
577
  id: row.id,
556
578
  threadId: row.thread_id,
557
579
  role: row.role === "tool" ? "agent" : row.role,
@@ -560,7 +582,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
560
582
  createdAt: row.created_at,
561
583
  ...row.task_id === null ? {} : { taskId: row.task_id },
562
584
  ...row.device_id === null ? {} : { deviceId: row.device_id }
563
- }), deviceFromRow = (row) => decode("RelayStore.decodeDevice", Device, {
585
+ }), deviceFromRow = (row) => decode2("RelayStore.decodeDevice", Device, {
564
586
  id: row.id,
565
587
  name: row.name,
566
588
  hostname: row.hostname,
@@ -568,16 +590,18 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
568
590
  architecture: row.architecture,
569
591
  status: row.status,
570
592
  providers: JSON.parse(row.providers_json),
593
+ ...row.bots_json === null ? {} : { bots: JSON.parse(row.bots_json) },
571
594
  capabilities: JSON.parse(row.capabilities_json),
572
595
  workspaces: JSON.parse(row.workspaces_json),
573
596
  version: row.version,
574
597
  lastSeenAt: row.last_seen_at,
575
598
  ...row.connected_at === null ? {} : { connectedAt: row.connected_at }
576
- }), taskFromRow = (row) => decode("RelayStore.decodeTask", Task, {
599
+ }), taskFromRow = (row) => decode2("RelayStore.decodeTask", Task, {
577
600
  id: row.id,
578
601
  threadId: row.thread_id,
579
602
  prompt: row.prompt,
580
603
  provider: row.provider,
604
+ ...row.bot_id === null ? {} : { botId: row.bot_id },
581
605
  status: row.status,
582
606
  targetDeviceId: row.target_device_id,
583
607
  createdAt: row.created_at,
@@ -591,7 +615,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
591
615
  ...row.error === null ? {} : { error: row.error },
592
616
  ...row.started_at === null ? {} : { startedAt: row.started_at },
593
617
  ...row.completed_at === null ? {} : { completedAt: row.completed_at }
594
- }), operationFromRow = (row) => decode("RelayStore.decodeOperation", DeviceOperation, {
618
+ }), operationFromRow = (row) => decode2("RelayStore.decodeOperation", DeviceOperation, {
595
619
  id: row.id,
596
620
  kind: row.kind,
597
621
  status: row.status,
@@ -611,17 +635,17 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
611
635
  completed: 0,
612
636
  failed: 0,
613
637
  cancelled: 0
614
- }), taskTraceEventFromRow = (row) => decode("RelayStore.decodeTaskTraceEvent", TaskTraceEvent, {
638
+ }), taskTraceEventFromRow = (row) => decode2("RelayStore.decodeTaskTraceEvent", TaskTraceEvent, {
615
639
  kind: row.kind,
616
640
  at: row.created_at,
617
641
  ...row.detail === null ? {} : { detail: row.detail }
618
- }), rolesFromRow = (row) => Schema11.decodeUnknownSync(Schema11.Array(ConnectionRole))(JSON.parse(row.roles_json)), authSessionFromRow = (row) => {
642
+ }), rolesFromRow = (row) => Schema13.decodeUnknownSync(Schema13.Array(ConnectionRole))(JSON.parse(row.roles_json)), authSessionFromRow = (row) => {
619
643
  const roles = rolesFromRow(row);
620
644
  const role = roles[0];
621
- return role === undefined ? Effect11.fail(new PersistenceError({
645
+ return role === undefined ? Effect13.fail(new PersistenceError({
622
646
  operation: "RelayStore.decodeAuthSession",
623
647
  message: `Session ${row.id} has no role`
624
- })) : decode("RelayStore.decodeAuthSession", AuthSession, {
648
+ })) : decode2("RelayStore.decodeAuthSession", AuthSession, {
625
649
  id: row.id,
626
650
  label: row.label,
627
651
  role,
@@ -641,6 +665,8 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
641
665
  return "Claude Code";
642
666
  case "opencode":
643
667
  return "OpenCode";
668
+ case "grok-bot":
669
+ return "Grok Bot";
644
670
  }
645
671
  }, recentWithin = (items, byteBudget) => {
646
672
  const encoder = new TextEncoder;
@@ -713,22 +739,22 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
713
739
  };
714
740
  const queryTask = (taskId) => db.query("SELECT * FROM tasks WHERE id = ?").get(taskId);
715
741
  const queryOperation = (operationId) => db.query("SELECT * FROM device_operations WHERE id = ?").get(operationId);
716
- const getTask = Effect11.fn("RelayStore.getTask")(function* (taskId) {
717
- const row = yield* Effect11.try({
742
+ const getTask = Effect13.fn("RelayStore.getTask")(function* (taskId) {
743
+ const row = yield* Effect13.try({
718
744
  try: () => queryTask(taskId),
719
745
  catch: operationError("RelayStore.getTask")
720
746
  });
721
747
  if (row === null) {
722
- return yield* Effect11.fail(new PersistenceError({
748
+ return yield* Effect13.fail(new PersistenceError({
723
749
  operation: "RelayStore.getTask",
724
750
  message: `Unknown task ${taskId}`
725
751
  }));
726
752
  }
727
753
  return yield* taskFromRow(row);
728
754
  });
729
- const traceTask = Effect11.fn("RelayStore.traceTask")(function* (taskId) {
755
+ const traceTask = Effect13.fn("RelayStore.traceTask")(function* (taskId) {
730
756
  const task = yield* getTask(taskId);
731
- const [deviceRow, eventRows] = yield* Effect11.try({
757
+ const [deviceRow, eventRows] = yield* Effect13.try({
732
758
  try: () => [
733
759
  db.query("SELECT * FROM devices WHERE id = ?").get(task.targetDeviceId),
734
760
  db.query(`SELECT * FROM (
@@ -739,20 +765,21 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
739
765
  catch: operationError("RelayStore.traceTask")
740
766
  });
741
767
  if (deviceRow === null) {
742
- return yield* Effect11.fail(new PersistenceError({
768
+ return yield* Effect13.fail(new PersistenceError({
743
769
  operation: "RelayStore.traceTask.device",
744
770
  message: `Unknown target device ${task.targetDeviceId}`
745
771
  }));
746
772
  }
747
773
  const targetDevice = yield* deviceFromRow(deviceRow);
748
774
  const selectedRows = eventRows.slice(-100);
749
- const recordedEvents = yield* Effect11.forEach(selectedRows, taskTraceEventFromRow);
775
+ const recordedEvents = yield* Effect13.forEach(selectedRows, taskTraceEventFromRow);
750
776
  const events = recordedEvents.length === 0 ? derivedTraceEvents(task) : recordedEvents;
751
777
  return TaskTrace.make({
752
778
  taskId: task.id,
753
779
  threadId: task.threadId,
754
780
  status: task.status,
755
781
  provider: task.provider,
782
+ ...task.botId === undefined ? {} : { botId: task.botId },
756
783
  targetDevice,
757
784
  createdAt: task.createdAt,
758
785
  updatedAt: task.updatedAt,
@@ -766,15 +793,15 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
766
793
  ...task.error === undefined ? {} : { error: task.error }
767
794
  });
768
795
  });
769
- const listDevices = Effect11.fn("RelayStore.listDevices")(function* () {
770
- const rows = yield* Effect11.try({
796
+ const listDevices = Effect13.fn("RelayStore.listDevices")(function* () {
797
+ const rows = yield* Effect13.try({
771
798
  try: () => db.query("SELECT * FROM devices WHERE forgotten_at IS NULL ORDER BY name COLLATE NOCASE, id").all(),
772
799
  catch: operationError("RelayStore.listDevices")
773
800
  });
774
- return yield* Effect11.forEach(rows, deviceFromRow);
801
+ return yield* Effect13.forEach(rows, deviceFromRow);
775
802
  });
776
- const usage = Effect11.fn("RelayStore.usage")(function* () {
777
- const rows = yield* Effect11.try({
803
+ const usage = Effect13.fn("RelayStore.usage")(function* () {
804
+ const rows = yield* Effect13.try({
778
805
  try: () => db.query(`SELECT tasks.target_device_id AS device_id, devices.name AS device_name,
779
806
  tasks.status, tasks.provider, COUNT(*) AS tasks
780
807
  FROM tasks
@@ -788,9 +815,9 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
788
815
  const devices = new Map;
789
816
  let retainedTasks = 0;
790
817
  for (const row of rows) {
791
- const status = Schema11.decodeUnknownSync(TaskStatus)(row.status);
792
- const provider = Schema11.decodeUnknownSync(Provider)(row.provider);
793
- const deviceId = Schema11.decodeUnknownSync(DeviceId)(row.device_id);
818
+ const status = Schema13.decodeUnknownSync(TaskStatus)(row.status);
819
+ const provider = Schema13.decodeUnknownSync(Provider)(row.provider);
820
+ const deviceId = Schema13.decodeUnknownSync(DeviceId)(row.device_id);
794
821
  retainedTasks += row.tasks;
795
822
  totalStatus[status] += row.tasks;
796
823
  totalProviders.set(provider, (totalProviders.get(provider) ?? 0) + row.tasks);
@@ -820,29 +847,29 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
820
847
  }))
821
848
  });
822
849
  });
823
- const getOperation = Effect11.fn("RelayStore.getOperation")(function* (operationId) {
824
- const row = yield* Effect11.try({
850
+ const getOperation = Effect13.fn("RelayStore.getOperation")(function* (operationId) {
851
+ const row = yield* Effect13.try({
825
852
  try: () => queryOperation(operationId),
826
853
  catch: operationError("RelayStore.getOperation")
827
854
  });
828
855
  if (row === null) {
829
- return yield* Effect11.fail(new PersistenceError({
856
+ return yield* Effect13.fail(new PersistenceError({
830
857
  operation: "RelayStore.getOperation",
831
858
  message: `Unknown device operation ${operationId}`
832
859
  }));
833
860
  }
834
861
  return yield* operationFromRow(row);
835
862
  });
836
- const listOperations = Effect11.fn("RelayStore.listOperations")(function* () {
837
- const rows = yield* Effect11.try({
863
+ const listOperations = Effect13.fn("RelayStore.listOperations")(function* () {
864
+ const rows = yield* Effect13.try({
838
865
  try: () => db.query("SELECT * FROM device_operations ORDER BY created_at DESC, id DESC LIMIT 50").all(),
839
866
  catch: operationError("RelayStore.listOperations")
840
867
  });
841
- return yield* Effect11.forEach(rows, operationFromRow);
868
+ return yield* Effect13.forEach(rows, operationFromRow);
842
869
  });
843
- const createUpgradeOperations = Effect11.fn("RelayStore.createUpgradeOperations")(function* (input) {
870
+ const createUpgradeOperations = Effect13.fn("RelayStore.createUpgradeOperations")(function* (input) {
844
871
  const timestamp = now();
845
- const operations = yield* Effect11.try({
872
+ const operations = yield* Effect13.try({
846
873
  try: () => db.transaction(() => {
847
874
  const devices = db.query("SELECT * FROM devices WHERE forgotten_at IS NULL ORDER BY name COLLATE NOCASE, id").all();
848
875
  if (devices.length === 0) {
@@ -882,15 +909,15 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
882
909
  });
883
910
  return operations;
884
911
  });
885
- const pendingOperationsFor = Effect11.fn("RelayStore.pendingOperationsFor")(function* (deviceId) {
886
- const rows = yield* Effect11.try({
912
+ const pendingOperationsFor = Effect13.fn("RelayStore.pendingOperationsFor")(function* (deviceId) {
913
+ const rows = yield* Effect13.try({
887
914
  try: () => db.query(`SELECT * FROM device_operations WHERE target_device_id = ? AND status = 'queued'
888
915
  ORDER BY created_at, id LIMIT 10`).all(deviceId),
889
916
  catch: operationError("RelayStore.pendingOperationsFor")
890
917
  });
891
- return yield* Effect11.forEach(rows, operationFromRow);
918
+ return yield* Effect13.forEach(rows, operationFromRow);
892
919
  });
893
- const transitionOperation = Effect11.fn("RelayStore.transitionOperation")(function* (operationId, expected, status, result, error) {
920
+ const transitionOperation = Effect13.fn("RelayStore.transitionOperation")(function* (operationId, expected, status, result, error) {
894
921
  const current = yield* getOperation(operationId);
895
922
  if (!expected.includes(current.status)) {
896
923
  return current;
@@ -898,7 +925,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
898
925
  const timestamp = now();
899
926
  const completed = status === "completed" || status === "failed" ? timestamp : undefined;
900
927
  const placeholders = expected.map(() => "?").join(", ");
901
- yield* Effect11.try({
928
+ yield* Effect13.try({
902
929
  try: () => db.transaction(() => {
903
930
  const updated = db.query(`UPDATE device_operations
904
931
  SET status = ?, result = ?, error = ?, updated_at = ?, completed_at = ?
@@ -911,23 +938,23 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
911
938
  });
912
939
  return yield* getOperation(operationId);
913
940
  });
914
- const requireOperationTarget = Effect11.fn("RelayStore.requireOperationTarget")(function* (operationId, deviceId) {
941
+ const requireOperationTarget = Effect13.fn("RelayStore.requireOperationTarget")(function* (operationId, deviceId) {
915
942
  const operation = yield* getOperation(operationId);
916
943
  if (operation.targetDeviceId !== deviceId) {
917
- return yield* Effect11.fail(new PersistenceError({
944
+ return yield* Effect13.fail(new PersistenceError({
918
945
  operation: "RelayStore.requireOperationTarget",
919
946
  message: `Device ${deviceId} cannot update operation ${operationId}`
920
947
  }));
921
948
  }
922
949
  return operation;
923
950
  });
924
- const assignOperation = Effect11.fn("RelayStore.assignOperation")(function* (operationId) {
951
+ const assignOperation = Effect13.fn("RelayStore.assignOperation")(function* (operationId) {
925
952
  const current = yield* getOperation(operationId);
926
953
  if (current.status !== "queued") {
927
954
  return current;
928
955
  }
929
956
  const timestamp = now();
930
- yield* Effect11.try({
957
+ yield* Effect13.try({
931
958
  try: () => db.query(`UPDATE device_operations SET status = 'assigned', updated_at = ?
932
959
  WHERE id = ? AND status = 'queued'
933
960
  AND NOT EXISTS (
@@ -943,19 +970,19 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
943
970
  });
944
971
  return yield* getOperation(operationId);
945
972
  });
946
- const acceptOperation = (operationId, deviceId) => requireOperationTarget(operationId, deviceId).pipe(Effect11.flatMap(() => transitionOperation(operationId, ["assigned"], "running")));
947
- const finishOperation = (operationId, deviceId, result) => requireOperationTarget(operationId, deviceId).pipe(Effect11.flatMap(() => transitionOperation(operationId, ["queued", "assigned", "running"], "completed", result)));
948
- const failOperation = (operationId, deviceId, error) => requireOperationTarget(operationId, deviceId).pipe(Effect11.flatMap(() => transitionOperation(operationId, ["queued", "assigned", "running"], "failed", undefined, error)));
973
+ const acceptOperation = (operationId, deviceId) => requireOperationTarget(operationId, deviceId).pipe(Effect13.flatMap(() => transitionOperation(operationId, ["assigned"], "running")));
974
+ const finishOperation = (operationId, deviceId, result) => requireOperationTarget(operationId, deviceId).pipe(Effect13.flatMap(() => transitionOperation(operationId, ["queued", "assigned", "running"], "completed", result)));
975
+ const failOperation = (operationId, deviceId, error) => requireOperationTarget(operationId, deviceId).pipe(Effect13.flatMap(() => transitionOperation(operationId, ["queued", "assigned", "running"], "failed", undefined, error)));
949
976
  const abandonOperation = (operationId) => transitionOperation(operationId, ["queued", "assigned", "running"], "failed", undefined, "Abandoned by the relay owner");
950
- const requeueOperationsFor = Effect11.fn("RelayStore.requeueOperationsFor")(function* (deviceId) {
951
- yield* Effect11.try({
977
+ const requeueOperationsFor = Effect13.fn("RelayStore.requeueOperationsFor")(function* (deviceId) {
978
+ yield* Effect13.try({
952
979
  try: () => db.query(`UPDATE device_operations SET status = 'queued', updated_at = ?
953
980
  WHERE target_device_id = ? AND status IN ('assigned', 'running')`).run(now(), deviceId),
954
981
  catch: operationError("RelayStore.requeueOperationsFor")
955
982
  });
956
983
  });
957
- const forgetDevice = Effect11.fn("RelayStore.forgetDevice")(function* (deviceId) {
958
- const row = yield* Effect11.try({
984
+ const forgetDevice = Effect13.fn("RelayStore.forgetDevice")(function* (deviceId) {
985
+ const row = yield* Effect13.try({
959
986
  try: () => db.transaction(() => {
960
987
  const device = db.query("SELECT * FROM devices WHERE id = ? AND forgotten_at IS NULL").get(deviceId);
961
988
  if (device === null) {
@@ -982,8 +1009,8 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
982
1009
  });
983
1010
  return yield* deviceFromRow(row);
984
1011
  });
985
- const recover = Effect11.fn("RelayStore.recover")(function* () {
986
- yield* Effect11.try({
1012
+ const recover = Effect13.fn("RelayStore.recover")(function* () {
1013
+ yield* Effect13.try({
987
1014
  try: () => db.transaction(() => {
988
1015
  const timestamp = now();
989
1016
  const interrupted = db.query("SELECT id FROM tasks WHERE status IN ('assigned', 'running')").all();
@@ -1001,52 +1028,54 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1001
1028
  catch: operationError("RelayStore.recover")
1002
1029
  });
1003
1030
  });
1004
- const upsertDevice = Effect11.fn("RelayStore.upsertDevice")(function* (device) {
1005
- yield* Effect11.try({
1031
+ const upsertDevice = Effect13.fn("RelayStore.upsertDevice")(function* (device) {
1032
+ yield* Effect13.try({
1006
1033
  try: () => db.query(`INSERT INTO devices (
1007
1034
  id, name, hostname, platform, architecture, status, providers_json,
1008
- capabilities_json, workspaces_json, version, last_seen_at, connected_at
1009
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1035
+ capabilities_json, workspaces_json, version, last_seen_at, connected_at, bots_json
1036
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1010
1037
  ON CONFLICT(id) DO UPDATE SET
1011
1038
  name = excluded.name, hostname = excluded.hostname, platform = excluded.platform,
1012
1039
  architecture = excluded.architecture, status = excluded.status,
1013
- providers_json = excluded.providers_json, capabilities_json = excluded.capabilities_json,
1040
+ providers_json = excluded.providers_json, bots_json = excluded.bots_json,
1041
+ capabilities_json = excluded.capabilities_json,
1014
1042
  workspaces_json = excluded.workspaces_json, version = excluded.version,
1015
1043
  last_seen_at = excluded.last_seen_at, connected_at = excluded.connected_at,
1016
- forgotten_at = NULL`).run(device.id, device.name, device.hostname, device.platform, device.architecture, device.status, JSON.stringify(device.providers), JSON.stringify(device.capabilities), JSON.stringify(device.workspaces), device.version, device.lastSeenAt, device.connectedAt ?? null),
1044
+ forgotten_at = NULL`).run(device.id, device.name, device.hostname, device.platform, device.architecture, device.status, JSON.stringify(device.providers), JSON.stringify(device.capabilities), JSON.stringify(device.workspaces), device.version, device.lastSeenAt, device.connectedAt ?? null, device.bots === undefined ? null : JSON.stringify(device.bots)),
1017
1045
  catch: operationError("RelayStore.upsertDevice")
1018
1046
  });
1019
1047
  return device;
1020
1048
  });
1021
- const updateDeviceStatus = Effect11.fn("RelayStore.updateDeviceStatus")(function* (deviceId, status) {
1049
+ const updateDeviceStatus = Effect13.fn("RelayStore.updateDeviceStatus")(function* (deviceId, status, bots) {
1022
1050
  const timestamp = now();
1023
- const result = yield* Effect11.try({
1024
- try: () => db.query("UPDATE devices SET status = ?, last_seen_at = ? WHERE id = ?").run(status, timestamp, deviceId),
1051
+ const result = yield* Effect13.try({
1052
+ try: () => db.query("UPDATE devices SET status = ?, last_seen_at = ?, bots_json = COALESCE(?, bots_json) WHERE id = ?").run(status, timestamp, bots === undefined ? null : JSON.stringify(bots), deviceId),
1025
1053
  catch: operationError("RelayStore.updateDeviceStatus")
1026
1054
  });
1027
1055
  if (result.changes !== 1) {
1028
- return yield* Effect11.fail(new PersistenceError({
1056
+ return yield* Effect13.fail(new PersistenceError({
1029
1057
  operation: "RelayStore.updateDeviceStatus",
1030
1058
  message: `Unknown device ${deviceId}`
1031
1059
  }));
1032
1060
  }
1033
1061
  const row = db.query("SELECT * FROM devices WHERE id = ?").get(deviceId);
1034
1062
  if (row === null) {
1035
- return yield* Effect11.fail(new PersistenceError({
1063
+ return yield* Effect13.fail(new PersistenceError({
1036
1064
  operation: "RelayStore.updateDeviceStatus.read",
1037
1065
  message: `Device ${deviceId} disappeared after update`
1038
1066
  }));
1039
1067
  }
1040
1068
  return yield* deviceFromRow(row);
1041
1069
  });
1042
- const createDelegation2 = Effect11.fn("RelayStore.createDelegation")(function* (input, targetDeviceId, sourceDeviceId, providerSessionId) {
1070
+ const createDelegation2 = Effect13.fn("RelayStore.createDelegation")(function* (input, targetDeviceId, sourceDeviceId, providerSessionId) {
1043
1071
  const timestamp = now();
1044
1072
  const threadId = input.threadId ?? makeThreadId();
1045
1073
  const task = Task.make({
1046
1074
  id: makeTaskId(),
1047
1075
  threadId,
1048
1076
  prompt: input.prompt,
1049
- provider: input.provider ?? "codex",
1077
+ provider: input.provider ?? (input.botId === undefined ? "codex" : "grok-bot"),
1078
+ ...input.botId === undefined ? {} : { botId: input.botId },
1050
1079
  status: "queued",
1051
1080
  targetDeviceId,
1052
1081
  createdAt: timestamp,
@@ -1073,7 +1102,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1073
1102
  taskId: task.id,
1074
1103
  ...sourceDeviceId === undefined ? {} : { deviceId: sourceDeviceId }
1075
1104
  });
1076
- yield* Effect11.try({
1105
+ yield* Effect13.try({
1077
1106
  try: () => db.transaction(() => {
1078
1107
  const outstanding = db.query(`SELECT COUNT(*) AS count FROM tasks WHERE target_device_id = ?
1079
1108
  AND status IN ('queued', 'assigned', 'running', 'cancelling')`).get(targetDeviceId);
@@ -1087,9 +1116,9 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1087
1116
  }
1088
1117
  db.query(`INSERT INTO tasks (
1089
1118
  id, thread_id, prompt, context, provider, status, source_device_id,
1090
- target_device_id, parent_task_id, workspace, provider_session_id,
1119
+ target_device_id, parent_task_id, workspace, provider_session_id, bot_id,
1091
1120
  created_at, updated_at
1092
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(task.id, task.threadId, task.prompt, task.context ?? null, task.provider, task.status, task.sourceDeviceId ?? null, task.targetDeviceId, task.parentTaskId ?? null, task.workspace ?? null, task.providerSessionId ?? null, task.createdAt, task.updatedAt);
1121
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(task.id, task.threadId, task.prompt, task.context ?? null, task.provider, task.status, task.sourceDeviceId ?? null, task.targetDeviceId, task.parentTaskId ?? null, task.workspace ?? null, task.providerSessionId ?? null, task.botId ?? null, task.createdAt, task.updatedAt);
1093
1122
  recordTaskTraceEvent(task.id, "queued", "Relay accepted the task", timestamp);
1094
1123
  db.query(`INSERT INTO messages (
1095
1124
  id, thread_id, role, kind, author_id, author_name, content, created_at,
@@ -1101,18 +1130,18 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1101
1130
  });
1102
1131
  return task;
1103
1132
  });
1104
- const threadContext2 = Effect11.fn("RelayStore.threadContext")(function* (threadId) {
1105
- const threadRow = yield* Effect11.try({
1133
+ const threadContext2 = Effect13.fn("RelayStore.threadContext")(function* (threadId) {
1134
+ const threadRow = yield* Effect13.try({
1106
1135
  try: () => db.query("SELECT * FROM threads WHERE id = ?").get(threadId),
1107
1136
  catch: operationError("RelayStore.threadContext.thread")
1108
1137
  });
1109
1138
  if (threadRow === null) {
1110
- return yield* Effect11.fail(new PersistenceError({
1139
+ return yield* Effect13.fail(new PersistenceError({
1111
1140
  operation: "RelayStore.threadContext",
1112
1141
  message: `Unknown thread ${threadId}`
1113
1142
  }));
1114
1143
  }
1115
- const [messageRows, taskRows] = yield* Effect11.try({
1144
+ const [messageRows, taskRows] = yield* Effect13.try({
1116
1145
  try: () => [
1117
1146
  db.query(`SELECT * FROM (
1118
1147
  SELECT * FROM messages WHERE thread_id = ?
@@ -1125,10 +1154,10 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1125
1154
  ],
1126
1155
  catch: operationError("RelayStore.threadContext.items")
1127
1156
  });
1128
- const [thread, messages, tasks] = yield* Effect11.all([
1157
+ const [thread, messages, tasks] = yield* Effect13.all([
1129
1158
  threadFromRow(threadRow),
1130
- Effect11.forEach(messageRows.slice(-20), messageFromRow),
1131
- Effect11.forEach(taskRows.slice(-20), taskFromRow)
1159
+ Effect13.forEach(messageRows.slice(-20), messageFromRow),
1160
+ Effect13.forEach(taskRows.slice(-20), taskFromRow)
1132
1161
  ]);
1133
1162
  const recentMessages = recentWithin(messages, 256 * 1024);
1134
1163
  const recentTasks = recentWithin(tasks, 768 * 1024);
@@ -1139,22 +1168,22 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1139
1168
  truncated: messageRows.length > 20 || taskRows.length > 20 || recentMessages.truncated || recentTasks.truncated
1140
1169
  });
1141
1170
  });
1142
- const pendingTasksFor = Effect11.fn("RelayStore.pendingTasksFor")(function* (deviceId) {
1143
- const rows = yield* Effect11.try({
1171
+ const pendingTasksFor = Effect13.fn("RelayStore.pendingTasksFor")(function* (deviceId) {
1172
+ const rows = yield* Effect13.try({
1144
1173
  try: () => db.query(`SELECT * FROM tasks WHERE target_device_id = ?
1145
1174
  AND status IN ('queued', 'cancelling') ORDER BY created_at, id LIMIT 100`).all(deviceId),
1146
1175
  catch: operationError("RelayStore.pendingTasksFor")
1147
1176
  });
1148
- return yield* Effect11.forEach(rows, taskFromRow);
1177
+ return yield* Effect13.forEach(rows, taskFromRow);
1149
1178
  });
1150
- const transition = Effect11.fn("RelayStore.transition")(function* (taskId, expected, update, event) {
1179
+ const transition = Effect13.fn("RelayStore.transition")(function* (taskId, expected, update, event) {
1151
1180
  const current = yield* getTask(taskId);
1152
1181
  if (!expected.includes(current.status)) {
1153
1182
  return current;
1154
1183
  }
1155
1184
  const placeholders = expected.map(() => "?").join(", ");
1156
1185
  const timestamp = now();
1157
- yield* Effect11.try({
1186
+ yield* Effect13.try({
1158
1187
  try: () => db.transaction(() => {
1159
1188
  const result = db.query(`UPDATE tasks SET status = ?, provider_session_id = ?, result = ?, error = ?,
1160
1189
  started_at = ?, completed_at = ?, updated_at = ?
@@ -1170,24 +1199,26 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1170
1199
  });
1171
1200
  return yield* getTask(taskId);
1172
1201
  });
1173
- const assignTask = Effect11.fn("RelayStore.assignTask")(function* (taskId) {
1202
+ const assignTask = Effect13.fn("RelayStore.assignTask")(function* (taskId) {
1174
1203
  const current = yield* getTask(taskId);
1175
1204
  if (current.status !== "queued") {
1176
1205
  return current;
1177
1206
  }
1178
1207
  const timestamp = now();
1179
- yield* Effect11.try({
1208
+ yield* Effect13.try({
1180
1209
  try: () => db.transaction(() => {
1181
1210
  const result = db.query(`UPDATE tasks SET status = 'assigned', updated_at = ?
1182
1211
  WHERE id = ? AND status = 'queued' AND NOT EXISTS (
1183
1212
  SELECT 1 FROM tasks active
1184
1213
  WHERE active.target_device_id = ? AND active.id <> ?
1214
+ AND CASE WHEN active.provider = 'grok-bot'
1215
+ THEN 'grok-bot:' || active.bot_id ELSE 'cli' END = ?
1185
1216
  AND active.status IN ('assigned', 'running', 'cancelling')
1186
1217
  ) AND NOT EXISTS (
1187
1218
  SELECT 1 FROM device_operations active
1188
1219
  WHERE active.target_device_id = ?
1189
1220
  AND active.status IN ('assigned', 'running')
1190
- )`).run(timestamp, taskId, current.targetDeviceId, taskId, current.targetDeviceId);
1221
+ )`).run(timestamp, taskId, current.targetDeviceId, taskId, taskSlot(current), current.targetDeviceId);
1191
1222
  if (result.changes === 1) {
1192
1223
  recordTaskTraceEvent(taskId, "assigned", "Relay dispatched the task over the device WebSocket", timestamp);
1193
1224
  }
@@ -1196,32 +1227,32 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1196
1227
  });
1197
1228
  return yield* getTask(taskId);
1198
1229
  });
1199
- const requireTarget = Effect11.fn("RelayStore.requireTarget")(function* (taskId, deviceId) {
1230
+ const requireTarget = Effect13.fn("RelayStore.requireTarget")(function* (taskId, deviceId) {
1200
1231
  const task = yield* getTask(taskId);
1201
1232
  if (task.targetDeviceId !== deviceId) {
1202
- return yield* Effect11.fail(new PersistenceError({
1233
+ return yield* Effect13.fail(new PersistenceError({
1203
1234
  operation: "RelayStore.requireTarget",
1204
1235
  message: `Task ${taskId} belongs to another device`
1205
1236
  }));
1206
1237
  }
1207
1238
  return task;
1208
1239
  });
1209
- const terminal = Effect11.fn("RelayStore.terminal")(function* (taskId, deviceId, status, result, error, providerSessionId) {
1240
+ const terminal = Effect13.fn("RelayStore.terminal")(function* (taskId, deviceId, status, result, error, providerSessionId) {
1210
1241
  const current = yield* requireTarget(taskId, deviceId);
1211
1242
  if (["completed", "failed", "cancelled"].includes(current.status)) {
1212
1243
  return current;
1213
1244
  }
1214
1245
  const timestamp = now();
1215
- yield* Effect11.try({
1246
+ yield* Effect13.try({
1216
1247
  try: () => db.transaction(() => {
1217
1248
  const updated = db.query(`UPDATE tasks SET status = ?, result = ?, error = ?, provider_session_id = ?,
1218
1249
  completed_at = ?, updated_at = ? WHERE id = ?
1219
- AND status NOT IN ('completed', 'failed', 'cancelled')`).run(status, result ?? null, error ?? null, providerSessionId ?? current.providerSessionId ?? null, timestamp, timestamp, taskId);
1250
+ AND status NOT IN ('completed', 'failed', 'cancelled')`).run(status, result ?? null, error ?? null, current.provider === "grok-bot" ? null : providerSessionId ?? current.providerSessionId ?? null, timestamp, timestamp, taskId);
1220
1251
  if (updated.changes !== 1) {
1221
1252
  return;
1222
1253
  }
1223
1254
  recordTaskTraceEvent(taskId, status, status === "completed" ? "Target device completed the task" : status === "failed" ? "Target device reported a task failure" : "Target device acknowledged cancellation", timestamp);
1224
- if (providerSessionId !== undefined) {
1255
+ if (current.provider !== "grok-bot" && providerSessionId !== undefined) {
1225
1256
  db.query(`INSERT INTO provider_sessions (thread_id, device_id, provider, session_id, updated_at)
1226
1257
  VALUES (?, ?, ?, ?, ?)
1227
1258
  ON CONFLICT(thread_id, device_id, provider) DO UPDATE SET
@@ -1242,11 +1273,17 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1242
1273
  });
1243
1274
  return yield* getTask(taskId);
1244
1275
  });
1245
- const requestCancellation = Effect11.fn("RelayStore.requestCancellation")(function* (taskId) {
1276
+ const requestCancellation = Effect13.fn("RelayStore.requestCancellation")(function* (taskId) {
1246
1277
  const current = yield* getTask(taskId);
1247
1278
  if (["completed", "failed", "cancelled"].includes(current.status)) {
1248
1279
  return current;
1249
1280
  }
1281
+ if (current.provider === "grok-bot" && current.status !== "queued") {
1282
+ return yield* new PersistenceError({
1283
+ operation: "RelayStore.requestCancellation",
1284
+ message: "Stop this bot in Grok Bot; its gateway cannot cancel an individual Cohall request"
1285
+ });
1286
+ }
1250
1287
  if (current.status === "cancelling") {
1251
1288
  return current;
1252
1289
  }
@@ -1255,9 +1292,9 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1255
1292
  status: "cancelling"
1256
1293
  }, { kind: "cancelling", detail: "Client requested task cancellation" });
1257
1294
  });
1258
- const requeueTasksFor = Effect11.fn("RelayStore.requeueTasksFor")(function* (deviceId) {
1295
+ const requeueTasksFor = Effect13.fn("RelayStore.requeueTasksFor")(function* (deviceId) {
1259
1296
  const timestamp = now();
1260
- yield* Effect11.try({
1297
+ yield* Effect13.try({
1261
1298
  try: () => db.transaction(() => {
1262
1299
  const interrupted = db.query(`SELECT id FROM tasks WHERE target_device_id = ?
1263
1300
  AND status IN ('assigned', 'running')`).all(deviceId);
@@ -1270,42 +1307,42 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1270
1307
  catch: operationError("RelayStore.requeueTasksFor")
1271
1308
  });
1272
1309
  });
1273
- const sessionFor = Effect11.fn("RelayStore.sessionFor")(function* (threadId, deviceId, provider) {
1274
- const row = yield* Effect11.try({
1310
+ const sessionFor = Effect13.fn("RelayStore.sessionFor")(function* (threadId, deviceId, provider) {
1311
+ const row = yield* Effect13.try({
1275
1312
  try: () => db.query(`SELECT session_id FROM provider_sessions
1276
1313
  WHERE thread_id = ? AND device_id = ? AND provider = ?`).get(threadId, deviceId, provider),
1277
1314
  catch: operationError("RelayStore.sessionFor")
1278
1315
  });
1279
1316
  return row?.session_id;
1280
1317
  });
1281
- const createPairing = Effect11.fn("RelayStore.createPairing")(function* (input) {
1318
+ const createPairing = Effect13.fn("RelayStore.createPairing")(function* (input) {
1282
1319
  const roles = [...new Set(input.roles)];
1283
1320
  if (roles.length === 0) {
1284
- return yield* Effect11.fail(new PersistenceError({
1321
+ return yield* Effect13.fail(new PersistenceError({
1285
1322
  operation: "RelayStore.createPairing",
1286
1323
  message: "A pairing must allow at least one role"
1287
1324
  }));
1288
1325
  }
1289
1326
  const token = secret("pair");
1290
1327
  const expiresAt = new Date(Date.now() + 10 * 60 * 1000).toISOString();
1291
- yield* Effect11.try({
1328
+ yield* Effect13.try({
1292
1329
  try: () => db.query(`INSERT INTO auth_pairings (token_hash, label, roles_json, expires_at, created_at)
1293
1330
  VALUES (?, ?, ?, ?, ?)`).run(tokenHash(token), input.label, JSON.stringify(roles), expiresAt, now()),
1294
1331
  catch: operationError("RelayStore.createPairing")
1295
1332
  });
1296
1333
  return PairingCredential.make({ token, expiresAt: Timestamp.make(expiresAt) });
1297
1334
  });
1298
- const exchangePairing2 = Effect11.fn("RelayStore.exchangePairing")(function* (token) {
1335
+ const exchangePairing2 = Effect13.fn("RelayStore.exchangePairing")(function* (token) {
1299
1336
  const timestamp = now();
1300
1337
  const expiresAt = new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString();
1301
- const created = yield* Effect11.try({
1338
+ const created = yield* Effect13.try({
1302
1339
  try: () => db.transaction(() => {
1303
1340
  const pairing = db.query(`SELECT * FROM auth_pairings
1304
1341
  WHERE token_hash = ? AND used_at IS NULL AND expires_at > ?`).get(tokenHash(token), timestamp);
1305
1342
  if (pairing === null) {
1306
1343
  throw new Error("Pairing credential is invalid, expired, or already used");
1307
1344
  }
1308
- const roles = Schema11.decodeUnknownSync(Schema11.Array(ConnectionRole))(JSON.parse(pairing.roles_json));
1345
+ const roles = Schema13.decodeUnknownSync(Schema13.Array(ConnectionRole))(JSON.parse(pairing.roles_json));
1309
1346
  const deviceId = roles.includes("device") ? makeDeviceId() : undefined;
1310
1347
  const consumed = db.query("UPDATE auth_pairings SET used_at = ? WHERE token_hash = ? AND used_at IS NULL").run(timestamp, pairing.token_hash);
1311
1348
  if (consumed.changes !== 1) {
@@ -1333,11 +1370,11 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1333
1370
  })(),
1334
1371
  catch: operationError("RelayStore.exchangePairing")
1335
1372
  });
1336
- const credentials = yield* Effect11.forEach(created, ({ token: sessionToken, row }) => authSessionFromRow(row).pipe(Effect11.map((session) => ({ token: sessionToken, session }))));
1373
+ const credentials = yield* Effect13.forEach(created, ({ token: sessionToken, row }) => authSessionFromRow(row).pipe(Effect13.map((session) => ({ token: sessionToken, session }))));
1337
1374
  return PairingResult.make({ credentials });
1338
1375
  });
1339
- const authenticateSession = Effect11.fn("RelayStore.authenticateSession")(function* (token, role) {
1340
- const row = yield* Effect11.try({
1376
+ const authenticateSession = Effect13.fn("RelayStore.authenticateSession")(function* (token, role) {
1377
+ const row = yield* Effect13.try({
1341
1378
  try: () => db.query(`SELECT * FROM auth_sessions
1342
1379
  WHERE token_hash = ? AND revoked_at IS NULL AND expires_at > ?`).get(tokenHash(token), now()),
1343
1380
  catch: operationError("RelayStore.authenticateSession")
@@ -1346,35 +1383,35 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1346
1383
  return;
1347
1384
  }
1348
1385
  const lastSeenAt = now();
1349
- yield* Effect11.try({
1386
+ yield* Effect13.try({
1350
1387
  try: () => db.query("UPDATE auth_sessions SET last_seen_at = ? WHERE id = ?").run(lastSeenAt, row.id),
1351
1388
  catch: operationError("RelayStore.authenticateSession.touch")
1352
1389
  });
1353
1390
  const session = yield* authSessionFromRow(row);
1354
1391
  return AuthSession.make({ ...session, lastSeenAt });
1355
1392
  });
1356
- const listAuthSessions = Effect11.fn("RelayStore.listAuthSessions")(function* () {
1357
- const rows = yield* Effect11.try({
1393
+ const listAuthSessions = Effect13.fn("RelayStore.listAuthSessions")(function* () {
1394
+ const rows = yield* Effect13.try({
1358
1395
  try: () => db.query("SELECT * FROM auth_sessions ORDER BY created_at DESC, id LIMIT 1000").all(),
1359
1396
  catch: operationError("RelayStore.listAuthSessions")
1360
1397
  });
1361
- return yield* Effect11.forEach(rows, authSessionFromRow);
1398
+ return yield* Effect13.forEach(rows, authSessionFromRow);
1362
1399
  });
1363
- const revokeAuthSession = Effect11.fn("RelayStore.revokeAuthSession")(function* (sessionId) {
1400
+ const revokeAuthSession = Effect13.fn("RelayStore.revokeAuthSession")(function* (sessionId) {
1364
1401
  const revokedAt = now();
1365
- const result = yield* Effect11.try({
1402
+ const result = yield* Effect13.try({
1366
1403
  try: () => db.query("UPDATE auth_sessions SET revoked_at = ? WHERE id = ? AND revoked_at IS NULL").run(revokedAt, sessionId),
1367
1404
  catch: operationError("RelayStore.revokeAuthSession")
1368
1405
  });
1369
1406
  if (result.changes !== 1) {
1370
- return yield* Effect11.fail(new PersistenceError({
1407
+ return yield* Effect13.fail(new PersistenceError({
1371
1408
  operation: "RelayStore.revokeAuthSession",
1372
1409
  message: `Unknown or already revoked session ${sessionId}`
1373
1410
  }));
1374
1411
  }
1375
1412
  const row = db.query("SELECT * FROM auth_sessions WHERE id = ?").get(sessionId);
1376
1413
  if (row === null) {
1377
- return yield* Effect11.fail(new PersistenceError({
1414
+ return yield* Effect13.fail(new PersistenceError({
1378
1415
  operation: "RelayStore.revokeAuthSession.read",
1379
1416
  message: `Session ${sessionId} disappeared after revocation`
1380
1417
  }));
@@ -1387,7 +1424,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1387
1424
  usage,
1388
1425
  forgetDevice,
1389
1426
  upsertDevice,
1390
- heartbeat: (deviceId, status) => updateDeviceStatus(deviceId, status),
1427
+ heartbeat: (deviceId, status, bots) => updateDeviceStatus(deviceId, status, bots),
1391
1428
  markDeviceOffline: (deviceId) => updateDeviceStatus(deviceId, "offline"),
1392
1429
  createDelegation: createDelegation2,
1393
1430
  getTask,
@@ -1396,7 +1433,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1396
1433
  pendingTasksFor,
1397
1434
  assignTask,
1398
1435
  rollbackAssignment: (taskId) => transition(taskId, ["assigned"], { status: "queued" }, { kind: "requeued", detail: "Target device was unavailable during dispatch" }),
1399
- acceptTask: (taskId, deviceId) => requireTarget(taskId, deviceId).pipe(Effect11.flatMap(() => transition(taskId, ["assigned"], { status: "running", startedAt: now() }, { kind: "running", detail: "Target device accepted the task and started the provider" }))),
1436
+ 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" }))),
1400
1437
  finishTask: (taskId, deviceId, result, providerSessionId) => terminal(taskId, deviceId, "completed", result, undefined, providerSessionId),
1401
1438
  failTask: (taskId, deviceId, error) => terminal(taskId, deviceId, "failed", undefined, error),
1402
1439
  acknowledgeCancellation: (taskId, deviceId) => terminal(taskId, deviceId, "cancelled"),
@@ -1419,7 +1456,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1419
1456
  listAuthSessions,
1420
1457
  revokeAuthSession
1421
1458
  });
1422
- }, migrate = (db) => Effect11.try({
1459
+ }, migrate = (db) => Effect13.try({
1423
1460
  try: () => {
1424
1461
  db.exec("PRAGMA journal_mode = WAL");
1425
1462
  db.exec("PRAGMA foreign_keys = ON");
@@ -1428,7 +1465,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1428
1465
  CREATE TABLE IF NOT EXISTS devices (
1429
1466
  id TEXT PRIMARY KEY, name TEXT NOT NULL, hostname TEXT NOT NULL,
1430
1467
  platform TEXT NOT NULL, architecture TEXT NOT NULL, status TEXT NOT NULL,
1431
- providers_json TEXT NOT NULL, capabilities_json TEXT NOT NULL,
1468
+ providers_json TEXT NOT NULL, bots_json TEXT, capabilities_json TEXT NOT NULL,
1432
1469
  workspaces_json TEXT NOT NULL, version TEXT NOT NULL, last_seen_at TEXT NOT NULL,
1433
1470
  connected_at TEXT, forgotten_at TEXT
1434
1471
  );
@@ -1446,7 +1483,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1446
1483
  CREATE INDEX IF NOT EXISTS messages_thread_created ON messages(thread_id, created_at);
1447
1484
  CREATE TABLE IF NOT EXISTS tasks (
1448
1485
  id TEXT PRIMARY KEY, thread_id TEXT NOT NULL REFERENCES threads(id) ON DELETE CASCADE,
1449
- prompt TEXT NOT NULL, context TEXT, provider TEXT NOT NULL, status TEXT NOT NULL,
1486
+ prompt TEXT NOT NULL, context TEXT, provider TEXT NOT NULL, bot_id TEXT, status TEXT NOT NULL,
1450
1487
  source_device_id TEXT REFERENCES devices(id),
1451
1488
  target_device_id TEXT NOT NULL REFERENCES devices(id), parent_task_id TEXT,
1452
1489
  workspace TEXT, provider_session_id TEXT, result TEXT, error TEXT,
@@ -1491,17 +1528,24 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1491
1528
  if (!deviceColumns.some((column) => column.name === "forgotten_at")) {
1492
1529
  db.exec("ALTER TABLE devices ADD COLUMN forgotten_at TEXT");
1493
1530
  }
1531
+ if (!deviceColumns.some((column) => column.name === "bots_json")) {
1532
+ db.exec("ALTER TABLE devices ADD COLUMN bots_json TEXT");
1533
+ }
1534
+ const taskColumns = db.query("PRAGMA table_info(tasks)").all();
1535
+ if (!taskColumns.some((column) => column.name === "bot_id")) {
1536
+ db.exec("ALTER TABLE tasks ADD COLUMN bot_id TEXT");
1537
+ }
1494
1538
  },
1495
1539
  catch: operationError("RelayStore.migrate")
1496
- }), layer2 = (path, retainedTerminalTasks = 1000) => Layer2.effect(Service2, Effect11.acquireRelease(Effect11.try({
1540
+ }), layer2 = (path, retainedTerminalTasks = 1000) => Layer2.effect(Service2, Effect13.acquireRelease(Effect13.try({
1497
1541
  try: () => new Database(path),
1498
1542
  catch: operationError("RelayStore.open")
1499
- }).pipe(Effect11.tap(migrate)), (db) => Effect11.sync(() => db.close())).pipe(Effect11.map((db) => makeService(db, retainedTerminalTasks)))), layerFromDatabase = (db) => Layer2.effect(Service2, migrate(db).pipe(Effect11.map(() => makeService(db))));
1543
+ }).pipe(Effect13.tap(migrate)), (db) => Effect13.sync(() => db.close())).pipe(Effect13.map((db) => makeService(db, retainedTerminalTasks)))), layerFromDatabase = (db) => Layer2.effect(Service2, migrate(db).pipe(Effect13.map(() => makeService(db))));
1500
1544
  var init_store = __esm(() => {
1501
1545
  init_src();
1502
1546
  init_database();
1503
1547
  init_store();
1504
- PersistenceError = class PersistenceError extends Schema11.TaggedErrorClass()("RelayStore.PersistenceError", { operation: Schema11.String, message: Schema11.String }) {
1548
+ PersistenceError = class PersistenceError extends Schema13.TaggedErrorClass()("RelayStore.PersistenceError", { operation: Schema13.String, message: Schema13.String }) {
1505
1549
  };
1506
1550
  Service2 = class Service2 extends Context2.Service()("@cohall/RelayStore") {
1507
1551
  };
@@ -1511,13 +1555,14 @@ var init_store = __esm(() => {
1511
1555
  var exports_main = {};
1512
1556
  __export(exports_main, {
1513
1557
  runRelay: () => runRelay,
1514
- relayListenOptions: () => relayListenOptions
1558
+ relayListenOptions: () => relayListenOptions,
1559
+ chooseDevice: () => chooseDevice
1515
1560
  });
1516
- import { Effect as Effect12, ManagedRuntime, Schema as Schema12 } from "effect";
1561
+ import { Effect as Effect14, ManagedRuntime, Schema as Schema14 } from "effect";
1517
1562
  import { createHash as createHash3, timingSafeEqual } from "node:crypto";
1518
- import { access as access6, chmod as chmod7 } from "node:fs/promises";
1563
+ import { access as access6, chmod as chmod8 } from "node:fs/promises";
1519
1564
  import { createServer } from "node:http";
1520
- import { platform as platform5 } from "node:os";
1565
+ import { platform as platform6 } from "node:os";
1521
1566
  import { WebSocketServer } from "ws";
1522
1567
  var RequestError, relayListenOptions = (configuration, environment = process.env, pid = process.pid) => {
1523
1568
  const listenPid = Number(environment.LISTEN_PID);
@@ -1544,7 +1589,7 @@ var RequestError, relayListenOptions = (configuration, environment = process.env
1544
1589
  if (request.method === "GET" || request.method === "HEAD") {
1545
1590
  return Promise.resolve(undefined);
1546
1591
  }
1547
- return new Promise((resolve4, reject) => {
1592
+ return new Promise((resolve5, reject) => {
1548
1593
  const chunks = [];
1549
1594
  let size = 0;
1550
1595
  const cleanup = () => {
@@ -1563,7 +1608,7 @@ var RequestError, relayListenOptions = (configuration, environment = process.env
1563
1608
  };
1564
1609
  const onEnd = () => {
1565
1610
  cleanup();
1566
- resolve4(Buffer.concat(chunks, size));
1611
+ resolve5(Buffer.concat(chunks, size));
1567
1612
  };
1568
1613
  const onError = (cause) => {
1569
1614
  cleanup();
@@ -1612,18 +1657,18 @@ Content-Length: 0\r
1612
1657
  if ("destroy" in socket && typeof socket.destroy === "function") {
1613
1658
  socket.destroy();
1614
1659
  }
1615
- }, body = (request, decode2) => Effect12.tryPromise({
1660
+ }, body = (request, decode3) => Effect14.tryPromise({
1616
1661
  try: () => request.json(),
1617
1662
  catch: () => new RequestError({ status: 400, message: "Expected a JSON request body" })
1618
- }).pipe(Effect12.flatMap(decode2), Effect12.mapError((cause) => cause instanceof RequestError ? cause : new RequestError({ status: 400, message: String(cause).slice(0, 2048) }))), pathId = (schema, value) => Schema12.decodeUnknownEffect(schema)(value).pipe(Effect12.mapError(() => new RequestError({ status: 400, message: `Invalid identifier: ${value}` }))), scoreDevice = (device, input, sourceDeviceId) => {
1663
+ }).pipe(Effect14.flatMap(decode3), Effect14.mapError((cause) => cause instanceof RequestError ? cause : new RequestError({ status: 400, message: String(cause).slice(0, 2048) }))), pathId = (schema, value) => Schema14.decodeUnknownEffect(schema)(value).pipe(Effect14.mapError(() => new RequestError({ status: 400, message: `Invalid identifier: ${value}` }))), scoreDevice = (device, input, sourceDeviceId) => {
1619
1664
  const prompt = `${input.prompt} ${input.context ?? ""}`.toLowerCase();
1620
1665
  const needsXcode = /\b(xcode|ios|macos|swiftui|app store)\b/.test(prompt);
1621
1666
  const needsBrowser = /\b(browser|chrome|youtube|twitter|logged[- ]?in|signed[- ]?in|x\.com)\b/.test(prompt);
1622
1667
  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);
1623
- }, chooseDevice = (input, sourceDeviceId) => Effect12.gen(function* () {
1668
+ }, chooseDevice = (input, sourceDeviceId) => Effect14.gen(function* () {
1624
1669
  const store = yield* exports_store.Service;
1625
- const devices = yield* store.listDevices().pipe(Effect12.mapError((cause) => new RequestError({ status: 500, message: cause.message })));
1626
- const provider = input.provider ?? "codex";
1670
+ const devices = yield* store.listDevices().pipe(Effect14.mapError((cause) => new RequestError({ status: 500, message: cause.message })));
1671
+ const provider = input.provider ?? (input.botId === undefined ? "codex" : "grok-bot");
1627
1672
  if (input.targetDeviceId !== undefined) {
1628
1673
  const target = devices.find((device) => device.id === input.targetDeviceId);
1629
1674
  if (target === undefined) {
@@ -1638,27 +1683,40 @@ Content-Length: 0\r
1638
1683
  message: `${target.name} does not advertise the ${provider} provider`
1639
1684
  });
1640
1685
  }
1686
+ if (input.botId !== undefined && !target.bots?.some((bot) => bot.id === input.botId)) {
1687
+ return yield* new RequestError({
1688
+ status: 404,
1689
+ message: `${target.name} does not advertise bot ${input.botId}; refresh the bot list`
1690
+ });
1691
+ }
1641
1692
  return target.id;
1642
1693
  }
1643
- const selected = devices.filter((device) => device.providers.includes(provider)).sort((left, right) => scoreDevice(right, input, sourceDeviceId) - scoreDevice(left, input, sourceDeviceId))[0];
1694
+ const candidates = devices.filter((device) => device.providers.includes(provider) && (input.botId === undefined || device.bots?.some((bot) => bot.id === input.botId)));
1695
+ if (input.botId !== undefined && candidates.length > 1) {
1696
+ return yield* new RequestError({
1697
+ status: 409,
1698
+ message: `Bot ${input.botId} is advertised by more than one device; select a device`
1699
+ });
1700
+ }
1701
+ const selected = candidates.sort((left, right) => scoreDevice(right, input, sourceDeviceId) - scoreDevice(left, input, sourceDeviceId))[0];
1644
1702
  if (selected === undefined) {
1645
1703
  return yield* new RequestError({
1646
1704
  status: 409,
1647
- message: `No Cohall device advertises the ${provider} provider`
1705
+ message: input.botId === undefined ? `No Cohall device advertises the ${provider} provider` : `No Cohall device advertises bot ${input.botId}; refresh the bot list`
1648
1706
  });
1649
1707
  }
1650
1708
  return selected.id;
1651
1709
  }), runRelay = async () => {
1652
- const configuration = await Effect12.runPromise(loadEnvironmentConfiguration);
1710
+ const configuration = await Effect14.runPromise(loadEnvironmentConfiguration);
1653
1711
  process.umask(63);
1654
1712
  const runtime = ManagedRuntime.make(exports_store.layer(configuration.databasePath, configuration.historyTaskLimit));
1655
1713
  const run2 = (effect) => runtime.runPromise(effect);
1656
- await run2(Effect12.gen(function* () {
1714
+ await run2(Effect14.gen(function* () {
1657
1715
  const store = yield* exports_store.Service;
1658
1716
  yield* store.recover();
1659
1717
  }));
1660
- if (platform5() !== "win32" && await access6(configuration.databasePath).then(() => true).catch(() => false)) {
1661
- await chmod7(configuration.databasePath, 384);
1718
+ if (platform6() !== "win32" && await access6(configuration.databasePath).then(() => true).catch(() => false)) {
1719
+ await chmod8(configuration.databasePath, 384);
1662
1720
  }
1663
1721
  const hub = new Hub;
1664
1722
  let connections = 0;
@@ -1668,7 +1726,7 @@ Content-Length: 0\r
1668
1726
  if (ownerToken2(token)) {
1669
1727
  return "owner";
1670
1728
  }
1671
- return run2(Effect12.gen(function* () {
1729
+ return run2(Effect14.gen(function* () {
1672
1730
  const store = yield* exports_store.Service;
1673
1731
  return yield* store.authenticateSession(token, role);
1674
1732
  })).catch(() => {
@@ -1683,7 +1741,7 @@ Content-Length: 0\r
1683
1741
  return authenticate(authorization.slice("Bearer ".length), "client");
1684
1742
  };
1685
1743
  const dispatch = async (task) => {
1686
- const assigned = await run2(Effect12.gen(function* () {
1744
+ const assigned = await run2(Effect14.gen(function* () {
1687
1745
  const store = yield* exports_store.Service;
1688
1746
  return yield* store.assignTask(task.id);
1689
1747
  }));
@@ -1693,13 +1751,13 @@ Content-Length: 0\r
1693
1751
  if (hub.sendToDevice(assigned.targetDeviceId, SocketEvent.make({ _tag: "TaskAssigned", task: assigned }))) {
1694
1752
  return assigned;
1695
1753
  }
1696
- return run2(Effect12.gen(function* () {
1754
+ return run2(Effect14.gen(function* () {
1697
1755
  const store = yield* exports_store.Service;
1698
1756
  return yield* store.rollbackAssignment(task.id);
1699
1757
  }));
1700
1758
  };
1701
1759
  const dispatchOperation = async (operation) => {
1702
- const assigned = await run2(Effect12.gen(function* () {
1760
+ const assigned = await run2(Effect14.gen(function* () {
1703
1761
  const store = yield* exports_store.Service;
1704
1762
  return yield* store.assignOperation(operation.id);
1705
1763
  }));
@@ -1709,13 +1767,22 @@ Content-Length: 0\r
1709
1767
  if (hub.sendToDevice(assigned.targetDeviceId, SocketEvent.make({ _tag: "OperationAssigned", operation: assigned }))) {
1710
1768
  return assigned;
1711
1769
  }
1712
- return run2(Effect12.gen(function* () {
1770
+ return run2(Effect14.gen(function* () {
1713
1771
  const store = yield* exports_store.Service;
1714
1772
  return yield* store.rollbackOperation(operation.id);
1715
1773
  }));
1716
1774
  };
1717
- const dispatchPending = async (deviceId) => {
1718
- const tasks = await run2(Effect12.gen(function* () {
1775
+ const dispatchPendingOperations = async (deviceId) => {
1776
+ const operations = await run2(Effect14.gen(function* () {
1777
+ const store = yield* exports_store.Service;
1778
+ return yield* store.pendingOperationsFor(deviceId);
1779
+ }));
1780
+ for (const operation of operations) {
1781
+ await dispatchOperation(operation);
1782
+ }
1783
+ };
1784
+ const dispatchPendingTasks = async (deviceId) => {
1785
+ const tasks = await run2(Effect14.gen(function* () {
1719
1786
  const store = yield* exports_store.Service;
1720
1787
  return yield* store.pendingTasksFor(deviceId);
1721
1788
  }));
@@ -1726,13 +1793,10 @@ Content-Length: 0\r
1726
1793
  await dispatch(task);
1727
1794
  }
1728
1795
  }
1729
- const operations = await run2(Effect12.gen(function* () {
1730
- const store = yield* exports_store.Service;
1731
- return yield* store.pendingOperationsFor(deviceId);
1732
- }));
1733
- for (const operation of operations) {
1734
- await dispatchOperation(operation);
1735
- }
1796
+ };
1797
+ const dispatchPending = async (deviceId) => {
1798
+ await dispatchPendingOperations(deviceId);
1799
+ await dispatchPendingTasks(deviceId);
1736
1800
  };
1737
1801
  const sendError = (socket, code, message) => {
1738
1802
  socket.send(JSON.stringify(SocketEvent.make({
@@ -1742,10 +1806,10 @@ Content-Length: 0\r
1742
1806
  })));
1743
1807
  };
1744
1808
  const handleSocketEvent = async (socket, input) => {
1745
- const event = await Effect12.runPromise(Effect12.try({
1809
+ const event = await Effect14.runPromise(Effect14.try({
1746
1810
  try: () => JSON.parse(socketText(input)),
1747
1811
  catch: () => new RequestError({ status: 400, message: "Invalid socket JSON" })
1748
- }).pipe(Effect12.flatMap(decodeSocketEvent))).catch((cause) => {
1812
+ }).pipe(Effect14.flatMap(decodeSocketEvent))).catch((cause) => {
1749
1813
  sendError(socket, "invalid_event", cause instanceof Error ? cause.message : String(cause));
1750
1814
  return;
1751
1815
  });
@@ -1802,11 +1866,12 @@ Content-Length: 0\r
1802
1866
  socket.close(4009, "Device is already connected");
1803
1867
  return;
1804
1868
  }
1805
- await run2(Effect12.gen(function* () {
1869
+ await run2(Effect14.gen(function* () {
1806
1870
  const store = yield* exports_store.Service;
1807
1871
  yield* store.upsertDevice(device);
1808
1872
  }));
1809
- await dispatchPending(device.id);
1873
+ await dispatchPendingTasks(device.id);
1874
+ await dispatchPendingOperations(device.id);
1810
1875
  return;
1811
1876
  }
1812
1877
  const deviceId = hub.deviceId(socket);
@@ -1820,12 +1885,12 @@ Content-Length: 0\r
1820
1885
  const settleOperation = (operationId) => {
1821
1886
  socket.send(JSON.stringify(SocketEvent.make({ _tag: "OperationSettled", operationId })));
1822
1887
  };
1823
- const processed = await run2(Effect12.gen(function* () {
1888
+ const processed = await run2(Effect14.gen(function* () {
1824
1889
  const store = yield* exports_store.Service;
1825
1890
  switch (event._tag) {
1826
1891
  case "DeviceHeartbeat":
1827
1892
  if (event.deviceId === deviceId) {
1828
- yield* store.heartbeat(deviceId, event.status === "busy" ? "busy" : "online");
1893
+ yield* store.heartbeat(deviceId, event.status === "busy" ? "busy" : "online", event.bots);
1829
1894
  }
1830
1895
  return;
1831
1896
  case "TaskAccepted":
@@ -1876,7 +1941,7 @@ Content-Length: 0\r
1876
1941
  return json({ ok: true, version });
1877
1942
  }
1878
1943
  if (url.pathname === "/api/auth/pair" && request.method === "POST") {
1879
- return run2(Effect12.gen(function* () {
1944
+ return run2(Effect14.gen(function* () {
1880
1945
  const store = yield* exports_store.Service;
1881
1946
  const input = yield* body(request, decodeExchangePairingInput);
1882
1947
  return yield* store.exchangePairing(input.token);
@@ -1891,7 +1956,7 @@ Content-Length: 0\r
1891
1956
  return json({ error: "Owner token required" }, 403);
1892
1957
  }
1893
1958
  const sourceDeviceId = principal === "owner" ? undefined : principal.deviceId;
1894
- const effect = Effect12.gen(function* () {
1959
+ const effect = Effect14.gen(function* () {
1895
1960
  const store = yield* exports_store.Service;
1896
1961
  if (url.pathname === "/api/auth/pairings" && request.method === "POST") {
1897
1962
  const input = yield* body(request, decodeCreatePairingInput);
@@ -1919,7 +1984,7 @@ Content-Length: 0\r
1919
1984
  if (url.pathname === "/api/operations/upgrades" && request.method === "POST") {
1920
1985
  const input = yield* body(request, decodeCreateUpgradeOperationsInput);
1921
1986
  const operations = yield* store.createUpgradeOperations(input);
1922
- return json(yield* Effect12.tryPromise(() => Promise.all(operations.map(dispatchOperation))), 201);
1987
+ return json(yield* Effect14.tryPromise(() => Promise.all(operations.map(dispatchOperation))), 201);
1923
1988
  }
1924
1989
  const abandonOperation = url.pathname.match(/^\/api\/operations\/([^/]+)\/abandon$/);
1925
1990
  if (request.method === "POST" && abandonOperation?.[1] !== undefined) {
@@ -1946,9 +2011,9 @@ Content-Length: 0\r
1946
2011
  if (url.pathname === "/api/tasks" && request.method === "POST") {
1947
2012
  const input = yield* body(request, decodeCreateTaskInput);
1948
2013
  const target = yield* chooseDevice(input, sourceDeviceId);
1949
- const providerSessionId = input.threadId === undefined ? undefined : yield* store.sessionFor(input.threadId, target, input.provider ?? "codex");
2014
+ const providerSessionId = input.threadId === undefined || input.provider === "grok-bot" ? undefined : yield* store.sessionFor(input.threadId, target, input.provider ?? "codex");
1950
2015
  const task2 = yield* store.createDelegation(input, target, sourceDeviceId, providerSessionId);
1951
- return json(yield* Effect12.tryPromise(() => dispatch(task2)), 201);
2016
+ return json(yield* Effect14.tryPromise(() => dispatch(task2)), 201);
1952
2017
  }
1953
2018
  const task = url.pathname.match(/^\/api\/tasks\/([^/]+)$/);
1954
2019
  if (request.method === "GET" && task?.[1] !== undefined) {
@@ -1972,8 +2037,8 @@ Content-Length: 0\r
1972
2037
  return json(yield* store.threadContext(yield* pathId(ThreadId, thread[1])));
1973
2038
  }
1974
2039
  return yield* new RequestError({ status: 404, message: "Route not found" });
1975
- }).pipe(Effect12.mapError((cause) => cause instanceof RequestError ? cause : new RequestError({
1976
- status: cause._tag !== "RelayStore.PersistenceError" ? 500 : cause.message.startsWith("Unknown") ? 404 : cause.message.includes("outstanding task limit") ? 429 : cause.message.includes("must be offline") || cause.message.includes("still has outstanding tasks") || cause.message.includes("upgrade operation in progress") || cause.message === "No Cohall devices are registered" || cause.message.startsWith("All-device operations support") ? 409 : 500,
2040
+ }).pipe(Effect14.mapError((cause) => cause instanceof RequestError ? cause : new RequestError({
2041
+ status: cause._tag !== "RelayStore.PersistenceError" ? 500 : cause.message.startsWith("Unknown") ? 404 : cause.message.includes("cannot cancel an individual Cohall request") ? 409 : cause.message.includes("outstanding task limit") ? 429 : cause.message.includes("must be offline") || cause.message.includes("still has outstanding tasks") || cause.message.includes("upgrade operation in progress") || cause.message === "No Cohall devices are registered" || cause.message.startsWith("All-device operations support") ? 409 : 500,
1977
2042
  message: cause.message
1978
2043
  })));
1979
2044
  return run2(effect).catch((cause) => cause instanceof RequestError ? json({ error: cause.status === 500 ? "Relay request failed" : cause.message }, cause.status) : json({ error: "Relay request failed" }, 500));
@@ -1981,7 +2046,8 @@ Content-Length: 0\r
1981
2046
  const websocketServer = new WebSocketServer({
1982
2047
  noServer: true,
1983
2048
  maxPayload: maxSocketPayloadBytes,
1984
- perMessageDeflate: false
2049
+ perMessageDeflate: false,
2050
+ allowSynchronousEvents: false
1985
2051
  });
1986
2052
  websocketServer.on("connection", (rawSocket) => {
1987
2053
  const socket = rawSocket;
@@ -2046,7 +2112,7 @@ Content-Length: 0\r
2046
2112
  if (deviceId === undefined) {
2047
2113
  return;
2048
2114
  }
2049
- run2(Effect12.gen(function* () {
2115
+ run2(Effect14.gen(function* () {
2050
2116
  const store = yield* exports_store.Service;
2051
2117
  yield* store.requeueTasksFor(deviceId);
2052
2118
  yield* store.requeueOperationsFor(deviceId);
@@ -2074,11 +2140,11 @@ Content-Length: 0\r
2074
2140
  websocketServer.emit("connection", websocket, request);
2075
2141
  });
2076
2142
  });
2077
- await new Promise((resolve4, reject) => {
2143
+ await new Promise((resolve5, reject) => {
2078
2144
  server.once("error", reject);
2079
2145
  server.listen(relayListenOptions(configuration), () => {
2080
2146
  server.off("error", reject);
2081
- resolve4();
2147
+ resolve5();
2082
2148
  });
2083
2149
  });
2084
2150
  const address = server.address();
@@ -2093,7 +2159,7 @@ Content-Length: 0\r
2093
2159
  for (const socket of websocketServer.clients) {
2094
2160
  socket.close(1001, "Relay shutting down");
2095
2161
  }
2096
- await new Promise((resolve4) => server.close(() => resolve4()));
2162
+ await new Promise((resolve5) => server.close(() => resolve5()));
2097
2163
  websocketServer.close();
2098
2164
  await runtime.dispose();
2099
2165
  };
@@ -2105,9 +2171,9 @@ var init_main = __esm(async () => {
2105
2171
  init_config();
2106
2172
  init_hub();
2107
2173
  init_store();
2108
- RequestError = class RequestError extends Schema12.TaggedErrorClass()("Relay.RequestError", {
2109
- status: Schema12.Int,
2110
- message: Schema12.String
2174
+ RequestError = class RequestError extends Schema14.TaggedErrorClass()("Relay.RequestError", {
2175
+ status: Schema14.Int,
2176
+ message: Schema14.String
2111
2177
  }) {
2112
2178
  };
2113
2179
  if (false) {}
@@ -2115,7 +2181,7 @@ var init_main = __esm(async () => {
2115
2181
 
2116
2182
  // apps/device/src/main.ts
2117
2183
  init_src();
2118
- import { Effect as Effect13 } from "effect";
2184
+ import { Effect as Effect15 } from "effect";
2119
2185
 
2120
2186
  // packages/client/src/index.ts
2121
2187
  init_src();
@@ -2129,7 +2195,7 @@ __export(exports_src, {
2129
2195
  RelayDecodeError: () => RelayDecodeError,
2130
2196
  RelayClient: () => exports_src
2131
2197
  });
2132
- import { Context, Effect, Layer, Schema as Schema2 } from "effect";
2198
+ import { Context, Effect as Effect2, Layer, Schema as Schema2 } from "effect";
2133
2199
 
2134
2200
  class RelayRequestError extends Schema2.TaggedErrorClass()("RelayClient.RequestError", {
2135
2201
  operation: Schema2.String,
@@ -2188,8 +2254,8 @@ var responseMessage = async (response, operation) => {
2188
2254
  };
2189
2255
  var make = (options) => {
2190
2256
  const baseUrl = normalizeBaseUrl(options.baseUrl);
2191
- const request = (operation, path, schema, init) => Effect.gen(function* () {
2192
- const response = yield* Effect.tryPromise({
2257
+ const request = (operation, path, schema, init) => Effect2.gen(function* () {
2258
+ const response = yield* Effect2.tryPromise({
2193
2259
  try: (signal) => fetch(`${baseUrl}${path}`, {
2194
2260
  ...init,
2195
2261
  signal: AbortSignal.any([signal, AbortSignal.timeout(30000)]),
@@ -2205,17 +2271,17 @@ var make = (options) => {
2205
2271
  })
2206
2272
  });
2207
2273
  if (!response.ok) {
2208
- return yield* Effect.fail(new RelayRequestError({
2274
+ return yield* Effect2.fail(new RelayRequestError({
2209
2275
  operation,
2210
- message: yield* Effect.promise(() => responseMessage(response, operation)),
2276
+ message: yield* Effect2.promise(() => responseMessage(response, operation)),
2211
2277
  status: response.status
2212
2278
  }));
2213
2279
  }
2214
- const json = yield* Effect.tryPromise({
2280
+ const json = yield* Effect2.tryPromise({
2215
2281
  try: () => jsonBody(response, operation),
2216
2282
  catch: (cause) => cause instanceof RelayDecodeError ? cause : new RelayDecodeError({ operation, message: String(cause) })
2217
2283
  });
2218
- return yield* Schema2.decodeUnknownEffect(schema)(json).pipe(Effect.mapError((cause) => new RelayDecodeError({ operation, message: String(cause) })));
2284
+ return yield* Schema2.decodeUnknownEffect(schema)(json).pipe(Effect2.mapError((cause) => new RelayDecodeError({ operation, message: String(cause) })));
2219
2285
  });
2220
2286
  return Service.of({
2221
2287
  devices: () => request("RelayClient.devices", "/api/devices", Schema2.Array(Device)),
@@ -2242,9 +2308,9 @@ var make = (options) => {
2242
2308
  abandonOperation: (operationId) => request("RelayClient.abandonOperation", `/api/operations/${encodeURIComponent(operationId)}/abandon`, DeviceOperation, { method: "POST" })
2243
2309
  });
2244
2310
  };
2245
- var exchangePairing = (baseUrl, input) => Effect.gen(function* () {
2311
+ var exchangePairing = (baseUrl, input) => Effect2.gen(function* () {
2246
2312
  const operation = "RelayClient.exchangePairing";
2247
- const response = yield* Effect.tryPromise({
2313
+ const response = yield* Effect2.tryPromise({
2248
2314
  try: (signal) => fetch(`${normalizeBaseUrl(baseUrl)}/api/auth/pair`, {
2249
2315
  method: "POST",
2250
2316
  signal: AbortSignal.any([signal, AbortSignal.timeout(30000)]),
@@ -2257,17 +2323,17 @@ var exchangePairing = (baseUrl, input) => Effect.gen(function* () {
2257
2323
  })
2258
2324
  });
2259
2325
  if (!response.ok) {
2260
- return yield* Effect.fail(new RelayRequestError({
2326
+ return yield* Effect2.fail(new RelayRequestError({
2261
2327
  operation,
2262
- message: yield* Effect.promise(() => responseMessage(response, operation)),
2328
+ message: yield* Effect2.promise(() => responseMessage(response, operation)),
2263
2329
  status: response.status
2264
2330
  }));
2265
2331
  }
2266
- const json = yield* Effect.tryPromise({
2332
+ const json = yield* Effect2.tryPromise({
2267
2333
  try: () => jsonBody(response, operation),
2268
2334
  catch: (cause) => cause instanceof RelayDecodeError ? cause : new RelayDecodeError({ operation, message: String(cause) })
2269
2335
  });
2270
- return yield* Schema2.decodeUnknownEffect(PairingResult)(json).pipe(Effect.mapError((cause) => new RelayDecodeError({ operation, message: String(cause) })));
2336
+ return yield* Schema2.decodeUnknownEffect(PairingResult)(json).pipe(Effect2.mapError((cause) => new RelayDecodeError({ operation, message: String(cause) })));
2271
2337
  });
2272
2338
  var layer = (options) => Layer.succeed(Service, make(options));
2273
2339
 
@@ -2276,7 +2342,7 @@ init_src();
2276
2342
 
2277
2343
  // packages/providers/src/index.ts
2278
2344
  init_src();
2279
- import { Effect as Effect2, Schema as Schema3 } from "effect";
2345
+ import { Effect as Effect3, Schema as Schema3 } from "effect";
2280
2346
  import { accessSync, constants } from "node:fs";
2281
2347
  import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises";
2282
2348
  import { homedir, platform, tmpdir } from "node:os";
@@ -2341,8 +2407,8 @@ var findExecutable = (command) => {
2341
2407
  }
2342
2408
  return;
2343
2409
  };
2344
- var available = (provider) => findExecutable(executables[provider]) !== undefined;
2345
- var availableProviders = () => Provider.literals.filter(available);
2410
+ var available = (provider) => provider !== "grok-bot" && findExecutable(executables[provider]) !== undefined;
2411
+ var availableProviders = () => Provider.literals.filter((provider) => provider !== "grok-bot").filter(available);
2346
2412
  var boundedText = (stream, limit, onOverflow) => new Promise((resolve, reject) => {
2347
2413
  const chunks = [];
2348
2414
  let size = 0;
@@ -2642,7 +2708,7 @@ var terminateProcessTree = async (pid) => {
2642
2708
  process.kill(-pid, "SIGKILL");
2643
2709
  await waitForProcessGroup(pid, 1000);
2644
2710
  };
2645
- var run = (options) => Effect2.tryPromise({
2711
+ var run = (options) => Effect3.tryPromise({
2646
2712
  try: async (signal) => {
2647
2713
  const executable = findExecutable(executables[options.provider]);
2648
2714
  if (executable === undefined) {
@@ -2660,7 +2726,8 @@ var run = (options) => Effect2.tryPromise({
2660
2726
  env: {
2661
2727
  ...providerEnvironment(),
2662
2728
  COHALL_PROVIDER: options.provider,
2663
- COHALL_THREAD_ID: options.threadId
2729
+ COHALL_THREAD_ID: options.threadId,
2730
+ ...options.taskId === undefined ? {} : { COHALL_TASK_ID: options.taskId }
2664
2731
  },
2665
2732
  detached: platform() !== "win32",
2666
2733
  stdio: ["pipe", "pipe", "pipe"]
@@ -2735,23 +2802,22 @@ var run = (options) => Effect2.tryPromise({
2735
2802
  });
2736
2803
 
2737
2804
  // apps/device/src/cli.ts
2738
- import { Effect as Effect7, Schema as Schema8 } from "effect";
2739
- import { access as access4, mkdir as mkdir5, readFile as readFile4, stat as stat3, writeFile as writeFile6 } from "node:fs/promises";
2805
+ 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";
2740
2807
  import { homedir as homedir4 } from "node:os";
2741
- import { dirname as dirname5, join as join6 } from "node:path";
2808
+ import { dirname as dirname6, join as join7, resolve as resolve3 } from "node:path";
2742
2809
 
2743
2810
  // skills/cohall/SKILL.md
2744
2811
  var SKILL_default = `---
2745
2812
  name: cohall
2746
- description: Delegate work to an agent on another user-owned device through the Cohall CLI. Use when the user asks to run, research, or check something on another device, names a Cohall target such as @macbook or @server, or needs machine-local state or capabilities such as a signed-in browser, Xcode, Docker, deployment access, a repository checkout, or unavailable tools. Carry the relevant current-conversation context into the handoff automatically.
2813
+ description: Delegate through Cohall to another user-owned device or an existing Grok Bot. Use when the user names a Cohall target, asks to message one of their bots, or needs another machine's tools, files, or signed-in services. Carry relevant conversation context into the handoff.
2747
2814
  ---
2748
2815
 
2749
2816
  # Cohall
2750
2817
 
2751
- Cohall sends a focused task to an agent running on another device. The target
2818
+ Cohall sends a focused task to a coding agent or an existing named Grok Bot. The target
2752
2819
  keeps its local files, credentials, browser state, provider login, skills, and
2753
- permissions. The relay carries prompts, final results, and task state; it is not
2754
- a lead agent.
2820
+ permissions. The relay carries prompts, final results, and task state.
2755
2821
 
2756
2822
  Use the installed \`cohall\` executable when it is available. Fall back to
2757
2823
  \`npx -y @akshar5/cohall\` only when Cohall is not installed globally.
@@ -2759,9 +2825,8 @@ Use the installed \`cohall\` executable when it is available. Fall back to
2759
2825
  ## Recognize cross-device requests
2760
2826
 
2761
2827
  Treat phrases such as “run this on my Mac,” “ask \`@server\`,” or “have the Linux
2762
- machine check this” as target intent. The \`@name\` form is a Cohall device
2763
- selector, not chat syntax supplied by the harness. Resolve it with \`cohall
2764
- devices\`, then delegate the smallest useful outcome.
2828
+ machine check this” as target intent. Resolve computers with \`cohall devices\`
2829
+ and named bots with \`cohall bots\`, then delegate the smallest useful outcome.
2765
2830
 
2766
2831
  Good reasons to delegate include:
2767
2832
 
@@ -2805,8 +2870,9 @@ Do not delegate ordinary local work when the other device provides no advantage.
2805
2870
  --context 'Why: deployment 184 failed after 15:00 UTC. Known: local checks passed and the provider status page reported elevated errors. Need: determine whether the outage explains our failure, with primary-source links and contrary evidence.'
2806
2871
  \`\`\`
2807
2872
 
2808
- Providers are \`codex\`, \`claude-code\`, and \`opencode\`. Omit \`--provider\` to
2809
- use Codex. Omit \`--target\` only when Cohall may choose a matching device.
2873
+ Coding providers are \`codex\`, \`claude-code\`, and \`opencode\`. Omit \`--provider\`
2874
+ to use Codex on a computer; selecting a named bot infers \`grok-bot\`.
2875
+ Omit \`--target\` only when Cohall may choose a matching computer.
2810
2876
 
2811
2877
  5. The command waits by default and returns JSON. Treat work as successful only
2812
2878
  when \`status\` is \`completed\`; use \`result\` in the current task. Report a
@@ -2822,9 +2888,43 @@ Do not delegate ordinary local work when the other device provides no advantage.
2822
2888
  --prompt 'Check whether yesterday’s deployment failed for the same reason.'
2823
2889
  \`\`\`
2824
2890
 
2825
- When an agent running as a Cohall task delegates again, \`COHALL_THREAD_ID\`
2826
- automatically carries the current thread. Pass \`--thread\` explicitly from an
2827
- ordinary shell or a separate agent turn.
2891
+ When a coding agent running as a Cohall task delegates again, \`COHALL_THREAD_ID\`
2892
+ and \`COHALL_TASK_ID\` carry its thread and parent task. Pass \`--thread\` and
2893
+ \`--parent\` explicitly from a Grok Bot using the IDs in its handoff.
2894
+
2895
+ ## Message named bots
2896
+
2897
+ 1. Run \`cohall bots\` and choose a listed target. Use its qualified target when
2898
+ names collide; discovery includes every available bot on configured hosts.
2899
+ 2. Send the prompt and relevant context:
2900
+
2901
+ \`\`\`bash
2902
+ cohall send @Research --prompt 'Find three project ideas worth exploring.'
2903
+ cohall send '@cloud/Video ideas' --prompt 'Suggest an opening for the video.'
2904
+ \`\`\`
2905
+
2906
+ Named bots use their existing Grok conversation and permissions. Omit
2907
+ \`--workspace\`. A Cohall thread records the exchange; it does not create an
2908
+ isolated Grok conversation.
2909
+
2910
+ 3. Reuse \`--thread <thread-id>\` for follow-ups. With an explicit thread and no
2911
+ target, Cohall resumes its most recent root bot task. Select a computer
2912
+ explicitly when delegating a child coding task:
2913
+
2914
+ \`\`\`bash
2915
+ cohall delegate --target @cloud --provider codex \\
2916
+ --thread <thread-id> --parent <task-id> --prompt 'Implement the selected idea.'
2917
+ \`\`\`
2918
+
2919
+ Different bots and the computer's coding agent can run concurrently. Each
2920
+ individual bot processes one Cohall request at a time.
2921
+
2922
+ When receiving a Cohall request inside a Grok Bot, finish with the \`cohall reply\`
2923
+ command supplied in the handoff. Pass the final answer through \`--message-file\`
2924
+ or \`--message -\` on stdin, then reply normally in chat. Use \`--error\` if unable
2925
+ to finish. This local receipt is what returns the result to the sending device;
2926
+ a chat message alone does not complete the Cohall task. The receipt survives
2927
+ worker restarts and works while the relay is temporarily unreachable.
2828
2928
 
2829
2929
  ## Context and safety
2830
2930
 
@@ -2860,8 +2960,10 @@ unless cancelled:
2860
2960
  cohall cancel <task-id>
2861
2961
  \`\`\`
2862
2962
 
2863
- Active cancellation is acknowledged by the target device; \`cancelling\` means
2864
- the provider process has not confirmed termination yet.
2963
+ For coding agents, active cancellation is acknowledged by the target device;
2964
+ \`cancelling\` means the provider process has not confirmed termination yet.
2965
+ Queued bot tasks can be cancelled. Stop an active bot in Grok Bot; its gateway
2966
+ cannot safely cancel an individual Cohall request.
2865
2967
 
2866
2968
  ## Read shared context
2867
2969
 
@@ -2910,12 +3012,12 @@ Return the diagnostic failure to the user rather than claiming remote work ran.
2910
3012
 
2911
3013
  // apps/device/src/config.ts
2912
3014
  init_src();
2913
- import { Effect as Effect3, Schema as Schema4 } from "effect";
3015
+ import { Effect as Effect4, Schema as Schema4 } from "effect";
2914
3016
  import { access, chmod as chmod2, mkdir, readFile, realpath, rename, writeFile as writeFile2 } from "node:fs/promises";
2915
3017
  import { homedir as homedir2, hostname, platform as platform2 } from "node:os";
2916
3018
  import { dirname, join as join2, resolve } from "node:path";
2917
3019
  var WorkspaceList = Schema4.Array(Schema4.NonEmptyString).check(Schema4.isMaxLength(64));
2918
- var ProviderList = Schema4.Array(Provider).check(Schema4.isMinLength(1)).check(Schema4.isMaxLength(3));
3020
+ var ProviderList = Schema4.Array(Provider).check(Schema4.isMinLength(1)).check(Schema4.isMaxLength(Provider.literals.length));
2919
3021
  var Sandbox = Schema4.Literals(["read-only", "workspace-write", "danger-full-access"]);
2920
3022
  var StoredConfiguration = Schema4.Struct({
2921
3023
  version: Schema4.Literal(1),
@@ -2927,12 +3029,14 @@ var StoredConfiguration = Schema4.Struct({
2927
3029
  deviceToken: Schema4.optionalKey(Schema4.NonEmptyString),
2928
3030
  providers: Schema4.optionalKey(ProviderList),
2929
3031
  model: Schema4.optionalKey(Schema4.NonEmptyString),
2930
- sandbox: Schema4.optionalKey(Sandbox)
3032
+ sandbox: Schema4.optionalKey(Sandbox),
3033
+ grokGateway: Schema4.optionalKey(Schema4.NonEmptyString)
2931
3034
  });
2932
3035
  var ClientConfiguration = Schema4.Struct({
2933
3036
  relayUrl: Schema4.NonEmptyString,
2934
3037
  token: Schema4.NonEmptyString,
2935
- mcpThreadId: Schema4.optionalKey(Schema4.String)
3038
+ mcpThreadId: Schema4.optionalKey(Schema4.String),
3039
+ mcpTaskId: Schema4.optionalKey(Schema4.String)
2936
3040
  });
2937
3041
  var DeviceConfiguration = Schema4.Struct({
2938
3042
  relayUrl: Schema4.NonEmptyString,
@@ -2942,7 +3046,8 @@ var DeviceConfiguration = Schema4.Struct({
2942
3046
  workspaces: WorkspaceList,
2943
3047
  providers: Schema4.optionalKey(ProviderList),
2944
3048
  model: Schema4.optionalKey(Schema4.NonEmptyString),
2945
- sandbox: Schema4.optionalKey(Sandbox)
3049
+ sandbox: Schema4.optionalKey(Sandbox),
3050
+ grokGateway: Schema4.optionalKey(Schema4.NonEmptyString)
2946
3051
  });
2947
3052
  var defaultConfigDirectory = () => {
2948
3053
  if (platform2() === "win32") {
@@ -2978,7 +3083,7 @@ var normalizeRelayUrl = (input) => {
2978
3083
  url.pathname = "/";
2979
3084
  return url.toString().replace(/\/$/, "");
2980
3085
  };
2981
- var parseWorkspaces = (workspaceList, workspaceJson) => Effect3.tryPromise({
3086
+ var parseWorkspaces = (workspaceList, workspaceJson) => Effect4.tryPromise({
2982
3087
  try: async () => {
2983
3088
  const values = workspaceJson === undefined ? workspaceList.split(",").map((workspace) => workspace.trim()).filter((workspace) => workspace.length > 0) : Schema4.decodeUnknownSync(WorkspaceList)(JSON.parse(workspaceJson));
2984
3089
  const canonical = await Promise.all(values.map((workspace) => realpath(resolve(workspace))));
@@ -3031,7 +3136,7 @@ var makeStoredConfiguration = async (input) => {
3031
3136
  const existing = await readStoredConfiguration();
3032
3137
  const relayUrl = normalizeRelayUrl(input.relayUrl);
3033
3138
  const retainedCredentials = credentialsForRelay(existing, relayUrl);
3034
- const canonicalWorkspaces = input.workspaces === undefined ? existing?.workspaces ?? [] : await Effect3.runPromise(parseWorkspaces("", JSON.stringify(input.workspaces)));
3139
+ const canonicalWorkspaces = input.workspaces === undefined ? existing?.workspaces ?? [] : await Effect4.runPromise(parseWorkspaces("", JSON.stringify(input.workspaces)));
3035
3140
  const clientToken = input.clientToken ?? retainedCredentials.clientToken;
3036
3141
  const deviceToken = input.deviceToken ?? retainedCredentials.deviceToken;
3037
3142
  const providers = input.providers === "auto" ? undefined : input.providers ?? existing?.providers;
@@ -3045,7 +3150,8 @@ var makeStoredConfiguration = async (input) => {
3045
3150
  ...deviceToken === undefined ? {} : { deviceToken },
3046
3151
  ...providers === undefined ? {} : { providers },
3047
3152
  ...existing?.model === undefined ? {} : { model: existing.model },
3048
- ...existing?.sandbox === undefined ? {} : { sandbox: existing.sandbox }
3153
+ ...existing?.sandbox === undefined ? {} : { sandbox: existing.sandbox },
3154
+ ...existing?.grokGateway === undefined ? {} : { grokGateway: existing.grokGateway }
3049
3155
  });
3050
3156
  };
3051
3157
  var environmentWorkspaces = async (stored) => {
@@ -3054,7 +3160,7 @@ var environmentWorkspaces = async (stored) => {
3054
3160
  if (list === undefined && json === undefined) {
3055
3161
  return stored?.workspaces ?? [];
3056
3162
  }
3057
- return Effect3.runPromise(parseWorkspaces(list ?? "", json));
3163
+ return Effect4.runPromise(parseWorkspaces(list ?? "", json));
3058
3164
  };
3059
3165
  var environmentProvider = () => process.env.COHALL_MODEL ?? process.env.COHALL_CODEX_MODEL;
3060
3166
  var environmentSandbox = () => {
@@ -3075,7 +3181,7 @@ var storedOrDefaults = async () => {
3075
3181
  workspaces: []
3076
3182
  });
3077
3183
  };
3078
- var loadClientConfiguration = Effect3.tryPromise({
3184
+ var loadClientConfiguration = Effect4.tryPromise({
3079
3185
  try: async () => {
3080
3186
  const stored = await storedOrDefaults();
3081
3187
  const relayUrl = normalizeRelayUrl(process.env.COHALL_RELAY_URL ?? stored.relayUrl);
@@ -3086,12 +3192,13 @@ var loadClientConfiguration = Effect3.tryPromise({
3086
3192
  return ClientConfiguration.make({
3087
3193
  relayUrl,
3088
3194
  token,
3089
- ...process.env.COHALL_THREAD_ID === undefined ? {} : { mcpThreadId: process.env.COHALL_THREAD_ID }
3195
+ ...process.env.COHALL_THREAD_ID === undefined ? {} : { mcpThreadId: process.env.COHALL_THREAD_ID },
3196
+ ...process.env.COHALL_TASK_ID === undefined ? {} : { mcpTaskId: process.env.COHALL_TASK_ID }
3090
3197
  });
3091
3198
  },
3092
3199
  catch: (cause) => cause instanceof Error ? cause : new Error(String(cause))
3093
3200
  });
3094
- var loadOwnerConfiguration = Effect3.tryPromise({
3201
+ var loadOwnerConfiguration = Effect4.tryPromise({
3095
3202
  try: async () => {
3096
3203
  const stored = await storedOrDefaults();
3097
3204
  const ownerTokenPath = join2(relayDataDirectory(), "owner-token");
@@ -3111,7 +3218,7 @@ var loadOwnerConfiguration = Effect3.tryPromise({
3111
3218
  },
3112
3219
  catch: (cause) => cause instanceof Error ? cause : new Error(String(cause))
3113
3220
  });
3114
- var loadDeviceConfiguration = Effect3.tryPromise({
3221
+ var loadDeviceConfiguration = Effect4.tryPromise({
3115
3222
  try: async () => {
3116
3223
  const stored = await storedOrDefaults();
3117
3224
  const relayUrl = normalizeRelayUrl(process.env.COHALL_RELAY_URL ?? stored.relayUrl);
@@ -3126,6 +3233,7 @@ var loadDeviceConfiguration = Effect3.tryPromise({
3126
3233
  const model = environmentProvider() ?? stored.model;
3127
3234
  const sandbox = environmentSandbox() ?? stored.sandbox;
3128
3235
  const providers = environmentProviders(stored);
3236
+ const grokGateway = process.env.COHALL_GROK_GATEWAY ?? stored.grokGateway;
3129
3237
  return DeviceConfiguration.make({
3130
3238
  relayUrl,
3131
3239
  token,
@@ -3134,7 +3242,8 @@ var loadDeviceConfiguration = Effect3.tryPromise({
3134
3242
  workspaces,
3135
3243
  ...providers === undefined ? {} : { providers },
3136
3244
  ...model === undefined ? {} : { model },
3137
- ...sandbox === undefined ? {} : { sandbox }
3245
+ ...sandbox === undefined ? {} : { sandbox },
3246
+ ...grokGateway === undefined ? {} : { grokGateway: resolve(grokGateway) }
3138
3247
  });
3139
3248
  },
3140
3249
  catch: (cause) => cause instanceof Error ? cause : new Error(String(cause))
@@ -3143,7 +3252,7 @@ var knownProviders = Provider.literals;
3143
3252
 
3144
3253
  // apps/device/src/delegation.ts
3145
3254
  init_src();
3146
- import { Effect as Effect4, Schedule, Schema as Schema5 } from "effect";
3255
+ import { Effect as Effect5, Schedule, Schema as Schema5 } from "effect";
3147
3256
 
3148
3257
  class DeviceSelectionError extends Schema5.TaggedErrorClass()("Cohall.DeviceSelectionError", { message: Schema5.String, devices: Schema5.Array(Schema5.String) }) {
3149
3258
  }
@@ -3156,50 +3265,112 @@ var TaskResult = Schema5.Struct({
3156
3265
  status: TaskStatus,
3157
3266
  provider: Provider,
3158
3267
  target_device_id: DeviceId,
3268
+ bot_id: Schema5.optionalKey(BotId),
3159
3269
  result: Schema5.optionalKey(Schema5.String),
3160
3270
  error: Schema5.optionalKey(Schema5.String)
3161
3271
  });
3162
3272
  var matchingDevices = (devices, target) => {
3163
- const normalized = target.replace(/^@/, "").toLowerCase();
3164
- const byId = devices.find((device) => device.id === target);
3273
+ const value = target.replace(/^@/, "");
3274
+ const normalized = value.toLowerCase();
3275
+ const byId = devices.find((device) => device.id === value);
3165
3276
  return byId === undefined ? devices.filter((device) => device.name.toLowerCase() === normalized || device.hostname.toLowerCase() === normalized) : [byId];
3166
3277
  };
3278
+ var listBots = (devices) => devices.flatMap((device) => (device.bots ?? []).map((bot) => ({
3279
+ id: bot.id,
3280
+ name: bot.name,
3281
+ ...bot.description === undefined ? {} : { description: bot.description },
3282
+ device_id: device.id,
3283
+ device_name: device.name,
3284
+ status: device.status,
3285
+ target: `@${device.id}/${bot.id}`
3286
+ })));
3287
+ var selectTarget = Effect5.fn("Cohall.selectTarget")(function* (devices, target) {
3288
+ const value = target.replace(/^@/, "");
3289
+ const separator = value.indexOf("/");
3290
+ const qualified = separator !== -1;
3291
+ const deviceMatches = matchingDevices(devices, qualified ? value.slice(0, separator) : value);
3292
+ const botValue = qualified ? value.slice(separator + 1) : value;
3293
+ const bots = listBots(qualified ? deviceMatches : devices);
3294
+ const botsById = bots.filter((bot) => bot.id === botValue);
3295
+ const botMatches = botsById.length > 0 ? botsById : bots.filter((bot) => bot.name.toLowerCase() === botValue.toLowerCase());
3296
+ const exactDevice = qualified ? undefined : deviceMatches.find((device) => device.id === value);
3297
+ if (exactDevice !== undefined)
3298
+ return { deviceId: exactDevice.id, botId: undefined };
3299
+ const matches = [
3300
+ ...qualified ? [] : deviceMatches.map((device) => ({ deviceId: device.id, botId: undefined })),
3301
+ ...botMatches.map((bot) => ({ deviceId: bot.device_id, botId: bot.id }))
3302
+ ];
3303
+ const selected = matches[0];
3304
+ if (matches.length === 1 && selected !== undefined)
3305
+ return selected;
3306
+ return yield* new DeviceSelectionError({
3307
+ message: matches.length === 0 ? `No Cohall device or bot matches ${target}; use cohall devices or cohall bots` : `${target} is ambiguous; use a device ID or @device-id/bot-id from cohall bots`,
3308
+ devices: [
3309
+ ...deviceMatches.map((device) => `${device.name} (@${device.id})`),
3310
+ ...botMatches.map((bot) => `${bot.name} on ${bot.device_name} (${bot.target})`)
3311
+ ]
3312
+ });
3313
+ });
3167
3314
  var taskResult = (task) => TaskResult.make({
3168
3315
  task_id: task.id,
3169
3316
  thread_id: task.threadId,
3170
3317
  status: task.status,
3171
3318
  provider: task.provider,
3172
3319
  target_device_id: task.targetDeviceId,
3320
+ ...task.botId === undefined ? {} : { bot_id: task.botId },
3173
3321
  ...task.result === undefined ? {} : { result: task.result },
3174
3322
  ...task.error === undefined ? {} : { error: task.error }
3175
3323
  });
3176
- var createDelegation = Effect4.fn("Cohall.createDelegation")(function* (client, configuration, options) {
3324
+ var createDelegation = Effect5.fn("Cohall.createDelegation")(function* (client, configuration, options) {
3177
3325
  const devices = yield* client.devices();
3178
- const matches = options.target === undefined ? [] : matchingDevices(devices, options.target);
3179
- if (options.target !== undefined && matches.length !== 1) {
3326
+ if (devices.length === 0) {
3180
3327
  return yield* new DeviceSelectionError({
3181
- message: matches.length === 0 ? `No Cohall device matches ${options.target}` : `${options.target} is ambiguous; use a device ID`,
3182
- devices: matches.length === 0 ? devices.map((device) => `${device.name} (${device.id})`) : matches.map((device) => `${device.name} (${device.id})`)
3328
+ message: "No Cohall devices are registered",
3329
+ devices: []
3183
3330
  });
3184
3331
  }
3185
- if (devices.length === 0) {
3332
+ let target = options.target === undefined ? undefined : yield* selectTarget(devices, options.target);
3333
+ if (target === undefined && options.threadId !== undefined && options.provider === undefined && options.parentTaskId === undefined && configuration.mcpTaskId === undefined) {
3334
+ const context = yield* client.threadContext(options.threadId);
3335
+ const previous = context.tasks.findLast((task) => task.parentTaskId === undefined);
3336
+ if (previous === undefined && context.truncated) {
3337
+ return yield* new DeviceSelectionError({
3338
+ message: "Earlier thread targets are no longer in the retained context; specify --target",
3339
+ devices: []
3340
+ });
3341
+ }
3342
+ if (previous?.provider === "grok-bot" && previous.botId !== undefined) {
3343
+ target = yield* selectTarget(devices, `@${previous.targetDeviceId}/${previous.botId}`);
3344
+ }
3345
+ }
3346
+ const botId = target?.botId;
3347
+ if (botId !== undefined && options.provider !== undefined && options.provider !== "grok-bot") {
3186
3348
  return yield* new DeviceSelectionError({
3187
- message: "No Cohall devices are registered",
3349
+ message: `A bot target requires provider grok-bot, not ${options.provider}`,
3350
+ devices: []
3351
+ });
3352
+ }
3353
+ if (botId === undefined && options.provider === "grok-bot") {
3354
+ return yield* new DeviceSelectionError({
3355
+ message: "Select a bot with --target; use cohall bots to list available bots",
3188
3356
  devices: []
3189
3357
  });
3190
3358
  }
3191
- const target = matches[0];
3359
+ const provider = botId === undefined ? options.provider : "grok-bot";
3192
3360
  const inheritedThread = configuration.mcpThreadId;
3361
+ const parentTaskId = options.parentTaskId ?? (configuration.mcpTaskId === undefined ? undefined : yield* Schema5.decodeUnknownEffect(TaskId)(configuration.mcpTaskId));
3193
3362
  return yield* client.createTask({
3194
3363
  prompt: options.prompt,
3195
- ...target === undefined ? {} : { targetDeviceId: target.id },
3364
+ ...target === undefined ? {} : { targetDeviceId: target.deviceId },
3365
+ ...botId === undefined ? {} : { botId },
3196
3366
  ...options.threadId !== undefined ? { threadId: options.threadId } : inheritedThread === undefined ? {} : { threadId: ThreadId.make(inheritedThread) },
3197
3367
  ...options.context === undefined ? {} : { context: options.context },
3198
3368
  ...options.workspace === undefined ? {} : { workspace: options.workspace },
3199
- ...options.provider === undefined ? {} : { provider: options.provider }
3369
+ ...provider === undefined ? {} : { provider },
3370
+ ...parentTaskId === undefined ? {} : { parentTaskId }
3200
3371
  });
3201
3372
  });
3202
- var waitForTask = Effect4.fn("Cohall.waitForTask")(function* (client, initial, timeoutSeconds) {
3373
+ var waitForTask = Effect5.fn("Cohall.waitForTask")(function* (client, initial, timeoutSeconds) {
3203
3374
  const deadline = Date.now() + timeoutSeconds * 1000;
3204
3375
  let task = initial;
3205
3376
  while (!isTerminalTask(task)) {
@@ -3210,28 +3381,28 @@ var waitForTask = Effect4.fn("Cohall.waitForTask")(function* (client, initial, t
3210
3381
  status: task.status
3211
3382
  });
3212
3383
  }
3213
- yield* Effect4.sleep("1 second");
3384
+ yield* Effect5.sleep("1 second");
3214
3385
  task = yield* client.getTask(task.id);
3215
3386
  }
3216
3387
  return task;
3217
3388
  });
3218
- var followTaskTrace = Effect4.fn("Cohall.followTaskTrace")(function* (client, taskId, onTrace) {
3389
+ var followTaskTrace = Effect5.fn("Cohall.followTaskTrace")(function* (client, taskId, onTrace) {
3219
3390
  let revision;
3220
- const poll = client.traceTask(taskId).pipe(Effect4.tap((trace) => {
3391
+ const poll = client.traceTask(taskId).pipe(Effect5.tap((trace) => {
3221
3392
  const latest = trace.events.at(-1);
3222
3393
  const nextRevision = `${trace.status}:${latest?.kind ?? "none"}:${latest?.at ?? "none"}`;
3223
3394
  if (nextRevision === revision) {
3224
- return Effect4.succeed(undefined);
3395
+ return Effect5.succeed(undefined);
3225
3396
  }
3226
3397
  revision = nextRevision;
3227
- return Effect4.sync(() => onTrace(trace));
3398
+ return Effect5.sync(() => onTrace(trace));
3228
3399
  }));
3229
- return yield* poll.pipe(Effect4.repeat({
3400
+ return yield* poll.pipe(Effect5.repeat({
3230
3401
  until: (trace) => terminalTaskStatuses.has(trace.status),
3231
3402
  schedule: Schedule.spaced("1 second")
3232
3403
  }));
3233
3404
  });
3234
- var threadContext = Effect4.fn("Cohall.threadContext")(function* (client, threadId) {
3405
+ var threadContext = Effect5.fn("Cohall.threadContext")(function* (client, threadId) {
3235
3406
  return yield* client.threadContext(threadId);
3236
3407
  });
3237
3408
 
@@ -3280,8 +3451,458 @@ var allDeviceHealth = (devices, localVersion) => {
3280
3451
  };
3281
3452
  };
3282
3453
 
3454
+ // apps/device/src/grok-bot.ts
3455
+ init_src();
3456
+ import { Effect as Effect6, Schedule as Schedule2, Schema as Schema7 } from "effect";
3457
+ import { open as open2 } from "node:fs/promises";
3458
+ import { isIP } from "node:net";
3459
+
3460
+ // apps/device/src/bot-replies.ts
3461
+ init_src();
3462
+ import { Schema as Schema6 } from "effect";
3463
+ import { randomUUID } from "node:crypto";
3464
+ import { chmod as chmod3, link, mkdir as mkdir2, open, unlink } from "node:fs/promises";
3465
+ import { dirname as dirname2, join as join3 } from "node:path";
3466
+ import { platform as platform3 } from "node:os";
3467
+ var replyLifetimeMs = 6 * 60 * 60 * 1000;
3468
+ var Pending = Schema6.Struct({
3469
+ taskId: TaskId,
3470
+ botId: BotId,
3471
+ threadId: ThreadId,
3472
+ deadline: Schema6.Int.check(Schema6.isGreaterThanOrEqualTo(0))
3473
+ });
3474
+ var Dispatch = Schema6.Struct({ taskId: TaskId });
3475
+ var Reply = Schema6.Union([
3476
+ Schema6.Struct({
3477
+ result: Schema6.String.check(Schema6.isMaxLength(131072)),
3478
+ error: Schema6.optionalKey(Schema6.Never)
3479
+ }),
3480
+ Schema6.Struct({
3481
+ error: Schema6.NonEmptyString.check(Schema6.isMaxLength(16384)),
3482
+ result: Schema6.optionalKey(Schema6.Never)
3483
+ })
3484
+ ]);
3485
+ var Receipt = Schema6.Struct({
3486
+ taskId: TaskId,
3487
+ submittedAt: Schema6.Int.check(Schema6.isGreaterThanOrEqualTo(0)),
3488
+ reply: Reply
3489
+ });
3490
+ var isMissing = (cause) => cause instanceof Error && ("code" in cause) && cause.code === "ENOENT";
3491
+ var alreadyExists = (cause) => cause instanceof Error && ("code" in cause) && cause.code === "EEXIST";
3492
+ var paths = (taskId) => {
3493
+ const id = Schema6.decodeUnknownSync(TaskId)(taskId);
3494
+ const directory = join3(dirname2(configurationPath()), "bot-replies");
3495
+ return {
3496
+ directory,
3497
+ pending: join3(directory, `${id}.pending.json`),
3498
+ receipt: join3(directory, `${id}.reply.json`),
3499
+ dispatch: join3(directory, `${id}.dispatch.json`)
3500
+ };
3501
+ };
3502
+ var readJson = async (path, limit) => {
3503
+ const file = await open(path, "r").catch((cause) => {
3504
+ if (isMissing(cause))
3505
+ return;
3506
+ throw cause;
3507
+ });
3508
+ if (file === undefined)
3509
+ return;
3510
+ try {
3511
+ const buffer = Buffer.alloc(limit + 1);
3512
+ let size = 0;
3513
+ while (size < buffer.length) {
3514
+ const { bytesRead } = await file.read(buffer, size, buffer.length - size, size);
3515
+ if (bytesRead === 0)
3516
+ break;
3517
+ size += bytesRead;
3518
+ }
3519
+ if (size > limit)
3520
+ throw new Error("Bot reply record exceeds its size limit");
3521
+ return JSON.parse(buffer.subarray(0, size).toString("utf8"));
3522
+ } catch {
3523
+ throw new Error("Invalid local bot reply record");
3524
+ } finally {
3525
+ await file.close();
3526
+ }
3527
+ };
3528
+ var pendingReply = async (taskId) => {
3529
+ const value = await readJson(paths(taskId).pending, 4096);
3530
+ if (value === undefined)
3531
+ return;
3532
+ const decoded = Schema6.decodeUnknownResult(Pending)(value);
3533
+ if (decoded._tag === "Failure" || decoded.success.taskId !== taskId) {
3534
+ throw new Error("Invalid local bot reply manifest");
3535
+ }
3536
+ return decoded.success;
3537
+ };
3538
+ var publish = async (path, value) => {
3539
+ const temporary = `${path}.${randomUUID()}.tmp`;
3540
+ const file = await open(temporary, "wx", 384);
3541
+ try {
3542
+ try {
3543
+ await file.writeFile(`${JSON.stringify(value)}
3544
+ `, "utf8");
3545
+ await file.sync();
3546
+ } finally {
3547
+ await file.close();
3548
+ }
3549
+ await link(temporary, path);
3550
+ if (platform3() !== "win32") {
3551
+ const directory = await open(dirname2(path), "r");
3552
+ try {
3553
+ await directory.sync();
3554
+ } finally {
3555
+ await directory.close();
3556
+ }
3557
+ }
3558
+ return true;
3559
+ } catch (cause) {
3560
+ if (alreadyExists(cause))
3561
+ return false;
3562
+ throw cause;
3563
+ } finally {
3564
+ await unlink(temporary);
3565
+ }
3566
+ };
3567
+ var shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
3568
+ var prepareBotReply = async (task) => {
3569
+ if (task.provider !== "grok-bot" || task.botId === undefined) {
3570
+ throw new Error("Only a named Grok Bot task can receive a bot reply");
3571
+ }
3572
+ const files = paths(task.id);
3573
+ await mkdir2(files.directory, { recursive: true, mode: 448 });
3574
+ await chmod3(files.directory, 448);
3575
+ const record2 = Pending.make({
3576
+ taskId: task.id,
3577
+ botId: task.botId,
3578
+ threadId: task.threadId,
3579
+ deadline: Date.now() + replyLifetimeMs
3580
+ });
3581
+ await publish(files.pending, record2);
3582
+ const pending = await pendingReply(task.id);
3583
+ if (pending === undefined || pending.botId !== task.botId || pending.threadId !== task.threadId) {
3584
+ throw new Error("Bot reply manifest does not match this task");
3585
+ }
3586
+ const dispatch = await readJson(files.dispatch, 4096);
3587
+ if (dispatch !== undefined) {
3588
+ const decoded = Schema6.decodeUnknownResult(Dispatch)(dispatch);
3589
+ if (decoded._tag === "Failure" || decoded.success.taskId !== task.id) {
3590
+ throw new Error("Invalid local bot dispatch record");
3591
+ }
3592
+ }
3593
+ return {
3594
+ deadline: pending.deadline,
3595
+ command: `COHALL_CONFIG=${shellQuote(configurationPath())} cohall`,
3596
+ dispatched: dispatch !== undefined
3597
+ };
3598
+ };
3599
+ var claimBotDispatch = async (taskId) => {
3600
+ const pending = await pendingReply(taskId);
3601
+ if (pending === undefined)
3602
+ throw new Error("No pending bot reply for this task");
3603
+ if (Date.now() >= pending.deadline)
3604
+ throw new Error("Bot reply deadline has expired");
3605
+ return publish(paths(taskId).dispatch, { taskId });
3606
+ };
3607
+ var readBotReply = async (taskId) => {
3608
+ const pending = await pendingReply(taskId);
3609
+ if (pending === undefined)
3610
+ return;
3611
+ const value = await readJson(paths(taskId).receipt, 1024 * 1024);
3612
+ if (value === undefined)
3613
+ return;
3614
+ const decoded = Schema6.decodeUnknownResult(Receipt)(value);
3615
+ if (decoded._tag === "Failure" || decoded.success.taskId !== taskId) {
3616
+ throw new Error("Invalid local bot reply receipt");
3617
+ }
3618
+ if (decoded.success.submittedAt >= pending.deadline) {
3619
+ throw new Error("Bot reply was submitted after its deadline");
3620
+ }
3621
+ return decoded.success.reply;
3622
+ };
3623
+ var writeBotReply = async (taskId, reply) => {
3624
+ const decoded = Schema6.decodeUnknownResult(Reply)(reply);
3625
+ if (decoded._tag === "Failure") {
3626
+ throw new Error("Provide a result of at most 131072 characters or a nonempty error of at most 16384 characters");
3627
+ }
3628
+ const pending = await pendingReply(taskId);
3629
+ if (pending === undefined)
3630
+ throw new Error("No pending bot reply for this task");
3631
+ const existing = await readBotReply(taskId);
3632
+ if (existing !== undefined) {
3633
+ if (JSON.stringify(existing) === JSON.stringify(decoded.success))
3634
+ return;
3635
+ throw new Error("A different reply has already been submitted for this task");
3636
+ }
3637
+ const submittedAt = Date.now();
3638
+ if (submittedAt >= pending.deadline)
3639
+ throw new Error("Bot reply deadline has expired");
3640
+ if (await publish(paths(taskId).receipt, { taskId, submittedAt, reply: decoded.success }))
3641
+ return;
3642
+ const winner = await readBotReply(taskId);
3643
+ if (JSON.stringify(winner) !== JSON.stringify(decoded.success)) {
3644
+ throw new Error("A different reply has already been submitted for this task");
3645
+ }
3646
+ };
3647
+ var cleanupBotReply = async (taskId) => {
3648
+ const files = paths(taskId);
3649
+ for (const path of [files.pending, files.receipt, files.dispatch]) {
3650
+ await unlink(path).catch((cause) => {
3651
+ if (!isMissing(cause))
3652
+ throw cause;
3653
+ });
3654
+ }
3655
+ };
3656
+
3657
+ // apps/device/src/grok-bot.ts
3658
+ var maxResponseBytes = 4 * 1024 * 1024;
3659
+ var maxDiscoveryBytes = 64 * 1024;
3660
+ var text2 = Schema7.NonEmptyString;
3661
+ var Discovery = Schema7.Struct({
3662
+ port: Schema7.Int.check(Schema7.isBetween({ minimum: 1, maximum: 65535 })),
3663
+ host: Schema7.optionalKey(text2),
3664
+ scheme: Schema7.optionalKey(Schema7.Literals(["http", "https"])),
3665
+ token: Schema7.optionalKey(Schema7.String.check(Schema7.isMaxLength(16384)))
3666
+ });
3667
+ var Agent = Schema7.Struct({
3668
+ id: Bot.fields.id,
3669
+ name: Bot.fields.name,
3670
+ description: Schema7.optionalKey(Schema7.String),
3671
+ isGroup: Schema7.optionalKey(Schema7.Boolean)
3672
+ });
3673
+ var Roster = Schema7.Array(Agent).check(Schema7.isMaxLength(1024));
3674
+ var Acceptance = Schema7.Union([
3675
+ Schema7.Struct({ outcome: Schema7.Literals(["not-found", "unknown-durability"]) }),
3676
+ Schema7.Struct({
3677
+ outcome: Schema7.Literal("found"),
3678
+ record: Schema7.Struct({
3679
+ status: Schema7.Literals(["accepted", "pending", "rejected"])
3680
+ })
3681
+ })
3682
+ ]);
3683
+ var SendResult = Schema7.Struct({ accepted: Schema7.Literal(true) });
3684
+
3685
+ class GatewayError extends Error {
3686
+ }
3687
+ var decode = (schema, value, label) => {
3688
+ const result = Schema7.decodeUnknownResult(schema)(value);
3689
+ if (result._tag === "Failure")
3690
+ throw new GatewayError(`Grok Bot returned an invalid ${label}`);
3691
+ return result.success;
3692
+ };
3693
+ var readConnection = async (path) => {
3694
+ const value = await (async () => {
3695
+ const file = await open2(path, "r");
3696
+ try {
3697
+ const buffer = Buffer.alloc(maxDiscoveryBytes + 1);
3698
+ const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
3699
+ if (bytesRead > maxDiscoveryBytes)
3700
+ throw new Error("Discovery file exceeds its size limit");
3701
+ return decode(Discovery, JSON.parse(buffer.subarray(0, bytesRead).toString("utf8")), "discovery file");
3702
+ } finally {
3703
+ await file.close();
3704
+ }
3705
+ })().catch(() => {
3706
+ throw new Error("Cannot read Grok Bot gateway discovery; check its configured file and running gateway");
3707
+ });
3708
+ const host = value.host ?? "127.0.0.1";
3709
+ const normalized = host === "0.0.0.0" || host === "localhost" ? "127.0.0.1" : host === "::" || host === "::1" || host === "[::1]" ? "[::1]" : host;
3710
+ if (!(normalized === "[::1]" || isIP(normalized) === 4 && normalized.startsWith("127."))) {
3711
+ throw new Error("Grok Bot gateway must use a loopback address on this device");
3712
+ }
3713
+ return { url: `${value.scheme ?? "http"}://${normalized}:${value.port}`, token: value.token };
3714
+ };
3715
+ var rpc = async (connection, method, args, schema, signal) => {
3716
+ const controller = new AbortController;
3717
+ const deadline = setTimeout(() => controller.abort(), 20000);
3718
+ const combined = signal === undefined ? controller.signal : AbortSignal.any([signal, controller.signal]);
3719
+ try {
3720
+ const response = await fetch(`${connection.url}/api/${method}`, {
3721
+ method: "POST",
3722
+ redirect: "error",
3723
+ headers: {
3724
+ "content-type": "application/json",
3725
+ "x-sand-slim-avatars": "1",
3726
+ ...connection.token === undefined || connection.token.length === 0 ? {} : { authorization: `Bearer ${connection.token}` }
3727
+ },
3728
+ body: JSON.stringify(args),
3729
+ signal: combined
3730
+ });
3731
+ if (!response.ok) {
3732
+ await response.body?.cancel();
3733
+ throw new GatewayError(`Grok Bot ${method} failed (HTTP ${response.status})`);
3734
+ }
3735
+ if (response.body === null)
3736
+ throw new GatewayError("Grok Bot returned an empty response");
3737
+ const reader = response.body.getReader();
3738
+ const chunks = [];
3739
+ let size = 0;
3740
+ try {
3741
+ for (;; ) {
3742
+ const chunk = await reader.read();
3743
+ if (chunk.done)
3744
+ break;
3745
+ size += chunk.value.byteLength;
3746
+ if (size > maxResponseBytes)
3747
+ throw new GatewayError("Grok Bot response exceeded 4 MiB");
3748
+ chunks.push(chunk.value);
3749
+ }
3750
+ } finally {
3751
+ await reader.cancel().catch(() => {
3752
+ return;
3753
+ });
3754
+ reader.releaseLock();
3755
+ }
3756
+ let json;
3757
+ try {
3758
+ json = JSON.parse(Buffer.concat(chunks, size).toString("utf8"));
3759
+ } catch {
3760
+ throw new GatewayError(`Grok Bot ${method} returned invalid JSON`);
3761
+ }
3762
+ return decode(schema, json, `${method} response`);
3763
+ } catch (cause) {
3764
+ if (signal?.aborted)
3765
+ throw new Error("Stopped waiting for Grok Bot; its run may still be active");
3766
+ if (cause instanceof GatewayError)
3767
+ throw cause;
3768
+ throw new Error(`Grok Bot ${method} failed; check gateway availability and compatibility`);
3769
+ } finally {
3770
+ clearTimeout(deadline);
3771
+ }
3772
+ };
3773
+ var discoverGrokBots = async (path) => {
3774
+ const connection = await readConnection(path);
3775
+ const roster = await rpc(connection, "listAgents", {}, Roster);
3776
+ const bots = roster.filter((agent) => agent.isGroup !== true);
3777
+ if (bots.length > 256)
3778
+ throw new Error("Grok Bot discovery exceeds Cohall's limit of 256 bots");
3779
+ if (new Set(bots.map((bot) => bot.id)).size !== bots.length) {
3780
+ throw new Error("Grok Bot returned duplicate bot identities");
3781
+ }
3782
+ return bots.map(({ id, name, description }) => ({
3783
+ id,
3784
+ name,
3785
+ ...description === undefined ? {} : { description: description.slice(0, 512) }
3786
+ }));
3787
+ };
3788
+ var poll = (pass, intervalMs = 1000) => Effect6.tryPromise({ try: pass, catch: (cause) => cause }).pipe(Effect6.repeat({
3789
+ schedule: Schedule2.spaced(intervalMs),
3790
+ until: (value) => value !== undefined
3791
+ }));
3792
+ var timeoutMessage = "Timed out waiting for the bot to reply through Cohall; the bot may still be running.";
3793
+ var requireTime = (deadline) => {
3794
+ if (Date.now() >= deadline)
3795
+ throw new Error(timeoutMessage);
3796
+ };
3797
+ var finishReply = (reply) => {
3798
+ if (reply.error !== undefined)
3799
+ throw new Error(reply.error);
3800
+ if (reply.result === undefined)
3801
+ throw new Error("Grok Bot returned an invalid Cohall reply");
3802
+ return { result: reply.result };
3803
+ };
3804
+ var taskPrompt = (task, command) => [
3805
+ task.prompt,
3806
+ task.context === undefined ? "" : `Context:
3807
+ ${task.context}`,
3808
+ `This request came through Cohall. Thread: ${task.threadId}. Task: ${task.id}.`,
3809
+ `You may use your own tools and delegate work to another Cohall device when the request calls for it. Discover devices with: ${command} devices`,
3810
+ `Delegation example: ${command} delegate --thread ${task.threadId} --parent ${task.id} --target @device --provider codex --prompt 'Concrete task' --context 'Relevant context'`,
3811
+ "When finished, hand the final answer back to Cohall so it reaches the requesting device. Write it to a UTF-8 file on this computer, then run:",
3812
+ `${command} reply ${task.id} --message-file /absolute/path/to/your-answer.txt`,
3813
+ `Alternatively pipe your answer into: ${command} reply ${task.id} --message -`,
3814
+ `If you cannot complete this task, report the reason with: ${command} reply ${task.id} --error 'Reason'`,
3815
+ "The reply command must succeed. Then send the same answer in this chat. A normal chat message alone does not complete the Cohall task."
3816
+ ].filter(Boolean).join(`
3817
+
3818
+ `);
3819
+ var runGrokBot = async (path, task, signal) => {
3820
+ const existing = await readBotReply(task.id);
3821
+ if (existing !== undefined)
3822
+ return finishReply(existing);
3823
+ const { deadline, command, dispatched } = await prepareBotReply(task);
3824
+ requireTime(deadline);
3825
+ const agentId = task.botId;
3826
+ if (agentId === undefined)
3827
+ throw new Error("Select a Grok Bot for this task");
3828
+ const lookup = async (lookupSignal = signal) => {
3829
+ if (path === undefined)
3830
+ throw new Error("Configure this device's Grok Bot gateway discovery file first");
3831
+ return rpc(await readConnection(path), "promptAcceptanceStatus", { accountSlot: "host", agentId, clientNonce: task.id }, Acceptance, lookupSignal);
3832
+ };
3833
+ const waitForReply = (reconcile = false) => {
3834
+ const receipt = poll(async () => {
3835
+ const receipt2 = await readBotReply(task.id);
3836
+ if (receipt2 !== undefined)
3837
+ return finishReply(receipt2);
3838
+ requireTime(deadline);
3839
+ return;
3840
+ });
3841
+ if (!reconcile || path === undefined)
3842
+ return Effect6.runPromise(receipt, { signal });
3843
+ const acceptance2 = poll(async (lookupSignal) => {
3844
+ const current = await lookup(lookupSignal).catch(() => {
3845
+ return;
3846
+ });
3847
+ if (current?.outcome !== "found")
3848
+ return;
3849
+ if (current.record.status === "rejected")
3850
+ throw new Error("Grok Bot rejected this task; check its conversation");
3851
+ return current.record.status === "accepted" ? true : undefined;
3852
+ }, 5000).pipe(Effect6.andThen(Effect6.never));
3853
+ return Effect6.runPromise(Effect6.raceFirst(receipt, acceptance2), { signal });
3854
+ };
3855
+ if (dispatched)
3856
+ return waitForReply(true);
3857
+ if (path === undefined)
3858
+ throw new Error("Configure this device's Grok Bot gateway discovery file first");
3859
+ const connection = await readConnection(path);
3860
+ let acceptance = await lookup();
3861
+ if (acceptance.outcome === "unknown-durability")
3862
+ throw new Error("Grok Bot cannot confirm whether this task was accepted; check its conversation before retrying");
3863
+ if (acceptance.outcome === "not-found") {
3864
+ const receipt = await readBotReply(task.id);
3865
+ if (receipt !== undefined)
3866
+ return finishReply(receipt);
3867
+ requireTime(deadline);
3868
+ const roster = await rpc(connection, "listAgents", {}, Roster, signal);
3869
+ if (!roster.some((row) => row.id === agentId && row.isGroup !== true)) {
3870
+ throw new Error("The selected Grok Bot no longer exists on this device");
3871
+ }
3872
+ requireTime(deadline);
3873
+ acceptance = await lookup();
3874
+ if (acceptance.outcome === "not-found") {
3875
+ requireTime(deadline);
3876
+ if (!await claimBotDispatch(task.id)) {
3877
+ return waitForReply(true);
3878
+ }
3879
+ try {
3880
+ await rpc(connection, "sendPrompt", {
3881
+ agentId,
3882
+ prompt: taskPrompt(task, command),
3883
+ clientNonce: task.id,
3884
+ directAddressedAcceptance: true
3885
+ }, SendResult, signal);
3886
+ } catch {
3887
+ if (signal.aborted)
3888
+ throw new Error("Stopped waiting for Grok Bot; its run may still be active");
3889
+ const receipt2 = await readBotReply(task.id);
3890
+ if (receipt2 !== undefined)
3891
+ return finishReply(receipt2);
3892
+ return waitForReply(true);
3893
+ }
3894
+ return waitForReply();
3895
+ }
3896
+ }
3897
+ if (acceptance.outcome === "unknown-durability")
3898
+ throw new Error("Grok Bot cannot confirm task acceptance; the prompt was not resent");
3899
+ if (acceptance.outcome === "found" && acceptance.record.status === "rejected")
3900
+ throw new Error("Grok Bot rejected this task; check its conversation");
3901
+ return waitForReply(acceptance.outcome === "found" && acceptance.record.status === "pending");
3902
+ };
3903
+
3283
3904
  // apps/device/src/setup.ts
3284
- import { Effect as Effect5 } from "effect";
3905
+ import { Effect as Effect7 } from "effect";
3285
3906
  import { hostname as hostname2 } from "node:os";
3286
3907
  import { Writable } from "node:stream";
3287
3908
  import { createInterface } from "node:readline/promises";
@@ -3295,7 +3916,7 @@ var joinRelay = async (options) => {
3295
3916
  if (!options.clientOnly && draft.workspaces.length === 0) {
3296
3917
  throw new Error("At least one workspace is required when joining a device");
3297
3918
  }
3298
- const paired = await Effect5.runPromise(exchangePairing(draft.relayUrl, { token: options.token }));
3919
+ const paired = await Effect7.runPromise(exchangePairing(draft.relayUrl, { token: options.token }));
3299
3920
  const clientToken = paired.credentials.find((credential) => credential.session.role === "client")?.token;
3300
3921
  const deviceCredential = paired.credentials.find((credential) => credential.session.role === "device");
3301
3922
  if (options.clientOnly && clientToken === undefined) {
@@ -3378,15 +3999,15 @@ var guidedSetupInput = async (options, prompter) => {
3378
3999
 
3379
4000
  // apps/device/src/relay-migration.ts
3380
4001
  init_src();
3381
- import { Effect as Effect6, Schema as Schema7 } from "effect";
4002
+ import { Effect as Effect8, Schema as Schema9 } from "effect";
3382
4003
  import { createHash } from "node:crypto";
3383
4004
  import { createReadStream } from "node:fs";
3384
4005
  import {
3385
4006
  access as access3,
3386
- chmod as chmod5,
4007
+ chmod as chmod6,
3387
4008
  copyFile,
3388
4009
  lstat,
3389
- mkdir as mkdir4,
4010
+ mkdir as mkdir5,
3390
4011
  mkdtemp as mkdtemp2,
3391
4012
  readFile as readFile3,
3392
4013
  realpath as realpath4,
@@ -3395,24 +4016,24 @@ import {
3395
4016
  stat as stat2,
3396
4017
  writeFile as writeFile5
3397
4018
  } from "node:fs/promises";
3398
- import { basename, dirname as dirname4, join as join5, resolve as resolve2 } from "node:path";
4019
+ import { basename, dirname as dirname5, join as join6, resolve as resolve2 } from "node:path";
3399
4020
  import { backup as sqliteBackup, DatabaseSync } from "node:sqlite";
3400
4021
  import { WebSocket } from "ws";
3401
4022
 
3402
4023
  // apps/device/src/service.ts
3403
4024
  import { execFile as execFile3 } from "node:child_process";
3404
- import { chmod as chmod4, mkdir as mkdir3, realpath as realpath3, writeFile as writeFile4 } from "node:fs/promises";
4025
+ import { chmod as chmod5, mkdir as mkdir4, realpath as realpath3, writeFile as writeFile4 } from "node:fs/promises";
3405
4026
  import { homedir as homedir3 } from "node:os";
3406
- import { dirname as dirname3, join as join4 } from "node:path";
4027
+ import { dirname as dirname4, join as join5 } from "node:path";
3407
4028
 
3408
4029
  // apps/device/src/upgrade.ts
3409
- import { Schema as Schema6 } from "effect";
4030
+ import { Schema as Schema8 } from "effect";
3410
4031
  import { execFile as execFile2 } from "node:child_process";
3411
4032
  import { constants as constants2 } from "node:fs";
3412
4033
  import {
3413
4034
  access as access2,
3414
- chmod as chmod3,
3415
- mkdir as mkdir2,
4035
+ chmod as chmod4,
4036
+ mkdir as mkdir3,
3416
4037
  readFile as readFile2,
3417
4038
  realpath as realpath2,
3418
4039
  rename as rename2,
@@ -3420,18 +4041,18 @@ import {
3420
4041
  stat,
3421
4042
  writeFile as writeFile3
3422
4043
  } from "node:fs/promises";
3423
- import { delimiter as delimiter2, dirname as dirname2, extname as extname2, isAbsolute as isAbsolute2, join as join3, relative } from "node:path";
4044
+ import { delimiter as delimiter2, dirname as dirname3, extname as extname2, isAbsolute as isAbsolute2, join as join4, relative } from "node:path";
3424
4045
  import { platform as operatingSystem } from "node:os";
3425
4046
  var packageName = "@akshar5/cohall";
3426
4047
  var semanticVersion = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
3427
- var PackageManager = Schema6.Literals(["npm", "bun", "pnpm"]);
3428
- var RestartReceipt = Schema6.Struct({
3429
- version: Schema6.String,
3430
- fromVersion: Schema6.String,
4048
+ var PackageManager = Schema8.Literals(["npm", "bun", "pnpm"]);
4049
+ var RestartReceipt = Schema8.Struct({
4050
+ version: Schema8.String,
4051
+ fromVersion: Schema8.String,
3431
4052
  packageManager: PackageManager,
3432
- pendingServices: Schema6.Array(Schema6.String),
3433
- restartedServices: Schema6.Array(Schema6.String),
3434
- restartingService: Schema6.optionalKey(Schema6.String)
4053
+ pendingServices: Schema8.Array(Schema8.String),
4054
+ restartedServices: Schema8.Array(Schema8.String),
4055
+ restartingService: Schema8.optionalKey(Schema8.String)
3435
4056
  });
3436
4057
  var normalizePath = (path) => path.replaceAll("\\", "/");
3437
4058
  var normalizeUpgradeTarget = (target) => {
@@ -3505,32 +4126,19 @@ var linuxSystemExecutableRoots = [
3505
4126
  ];
3506
4127
  var macosHomebrewRoots = ["/opt/homebrew", "/usr/local"];
3507
4128
  var macosAdminGroup = 80;
3508
- var isTrustedSystemExecutablePath = (platform3, canonical) => platform3 === "linux" && linuxSystemExecutableRoots.some((root) => containsPath(root, canonical));
4129
+ var isTrustedSystemPath = (platform4, path) => platform4 === "linux" && (path === "/home" || linuxSystemExecutableRoots.some((root) => containsPath(root, path) || containsPath(path, root)));
3509
4130
  var isTrustedGroupWritablePath = (options) => {
3510
4131
  const writableRoot = options.writableRoot;
3511
4132
  return options.platform === "darwin" && writableRoot !== undefined && options.uid !== undefined && options.ownerUid === options.uid && options.ownerGid === macosAdminGroup && macosHomebrewRoots.some((root) => containsPath(root, writableRoot)) && containsPath(writableRoot, options.canonical) && containsPath(writableRoot, options.path);
3512
4133
  };
3513
- var trustedExecutable = async (command, options = {}) => {
3514
- const candidates = isAbsolute2(command) ? [command] : (process.env.PATH ?? "").split(delimiter2).filter(Boolean).flatMap((directory) => executableNames(command).map((name) => join3(directory, name)));
3515
- let selected;
3516
- for (const path of candidates) {
3517
- const available2 = await access2(path, operatingSystem() === "win32" ? constants2.F_OK : constants2.X_OK).then(() => true).catch(() => false);
3518
- if (available2) {
3519
- selected = path;
3520
- break;
3521
- }
3522
- }
3523
- if (selected === undefined) {
3524
- throw new Error(`Could not find ${command} on PATH`);
3525
- }
3526
- const canonical = await realpath2(selected);
4134
+ var validateExecutable = async (candidate, installationRoot) => {
4135
+ const canonical = await realpath2(candidate);
3527
4136
  if (operatingSystem() === "win32") {
3528
4137
  return canonical;
3529
4138
  }
3530
4139
  const uid = process.getuid?.();
3531
- const writableRoot = options.writableRoot === undefined ? undefined : await realpath2(options.writableRoot);
3532
- const systemExecutable = isTrustedSystemExecutablePath(operatingSystem(), canonical);
3533
- for (let path = canonical;; path = dirname2(path)) {
4140
+ const writableRoot = installationRoot === undefined ? undefined : await realpath2(installationRoot);
4141
+ for (let path = canonical;; path = dirname3(path)) {
3534
4142
  const metadata = await stat(path);
3535
4143
  const trustedGroupWritablePath = isTrustedGroupWritablePath({
3536
4144
  platform: operatingSystem(),
@@ -3544,16 +4152,31 @@ var trustedExecutable = async (command, options = {}) => {
3544
4152
  if ((metadata.mode & 2) !== 0 || (metadata.mode & 16) !== 0 && !trustedGroupWritablePath) {
3545
4153
  throw new Error(`Refusing executable beneath group- or world-writable path ${path}`);
3546
4154
  }
3547
- if (uid !== undefined && metadata.uid !== 0 && metadata.uid !== uid && !systemExecutable) {
4155
+ if (uid !== undefined && metadata.uid !== 0 && metadata.uid !== uid && !isTrustedSystemPath(operatingSystem(), path)) {
3548
4156
  throw new Error(`Refusing executable owned by another user at ${path}`);
3549
4157
  }
3550
- const parent = dirname2(path);
4158
+ const parent = dirname3(path);
3551
4159
  if (parent === path) {
3552
4160
  break;
3553
4161
  }
3554
4162
  }
3555
4163
  return canonical;
3556
4164
  };
4165
+ var trustedExecutable = async (command, options = {}) => {
4166
+ const candidates = isAbsolute2(command) ? [command] : (process.env.PATH ?? "").split(delimiter2).filter(Boolean).flatMap((directory) => executableNames(command).map((name) => join4(directory, name)));
4167
+ let failure;
4168
+ for (const path of candidates) {
4169
+ const available2 = await access2(path, operatingSystem() === "win32" ? constants2.F_OK : constants2.X_OK).then(() => true).catch(() => false);
4170
+ if (!available2)
4171
+ continue;
4172
+ try {
4173
+ return await validateExecutable(path, options.writableRoot);
4174
+ } catch (cause) {
4175
+ failure ??= cause;
4176
+ }
4177
+ }
4178
+ throw failure ?? new Error(`Could not find ${command} on PATH`);
4179
+ };
3557
4180
  var trustedServices = async (services, resolveExecutable) => {
3558
4181
  const resolved = new Map;
3559
4182
  const invocation = async (value) => {
@@ -3712,15 +4335,15 @@ ${result.stderr}`);
3712
4335
  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.`);
3713
4336
  }
3714
4337
  };
3715
- var readReceipt = async (path) => readFile2(path, "utf8").then((content) => Schema6.decodeUnknownOption(RestartReceipt)(JSON.parse(content))).then((decoded) => decoded._tag === "Some" ? decoded.value : undefined).catch(() => {
4338
+ var readReceipt = async (path) => readFile2(path, "utf8").then((content) => Schema8.decodeUnknownOption(RestartReceipt)(JSON.parse(content))).then((decoded) => decoded._tag === "Some" ? decoded.value : undefined).catch(() => {
3716
4339
  return;
3717
4340
  });
3718
4341
  var writeReceipt = async (path, value) => {
3719
- const directory = dirname2(path);
4342
+ const directory = dirname3(path);
3720
4343
  const temporary = `${path}.${process.pid}.tmp`;
3721
- await mkdir2(directory, { recursive: true, mode: 448 });
4344
+ await mkdir3(directory, { recursive: true, mode: 448 });
3722
4345
  if (operatingSystem() !== "win32") {
3723
- await chmod3(directory, 448);
4346
+ await chmod4(directory, 448);
3724
4347
  }
3725
4348
  await writeFile3(temporary, `${JSON.stringify(value, null, 2)}
3726
4349
  `, { mode: 384 });
@@ -3728,13 +4351,13 @@ var writeReceipt = async (path, value) => {
3728
4351
  };
3729
4352
  var installedVersion = async (entrypoint) => {
3730
4353
  const canonicalEntrypoint = await realpath2(entrypoint);
3731
- const metadata = JSON.parse(await readFile2(join3(dirname2(dirname2(canonicalEntrypoint)), "package.json"), "utf8"));
4354
+ const metadata = JSON.parse(await readFile2(join4(dirname3(dirname3(canonicalEntrypoint)), "package.json"), "utf8"));
3732
4355
  if (typeof metadata !== "object" || metadata === null || !("name" in metadata) || metadata.name !== packageName || !("version" in metadata) || typeof metadata.version !== "string") {
3733
4356
  throw new Error(`Could not verify the installed ${packageName} version`);
3734
4357
  }
3735
4358
  return metadata.version;
3736
4359
  };
3737
- var restartReceiptPath = () => join3(dirname2(configurationPath()), "upgrade-restart.json");
4360
+ var restartReceiptPath = () => join4(dirname3(configurationPath()), "upgrade-restart.json");
3738
4361
  var restartServices = async (runner, services, statePath, state, preserveDeviceReceipt) => {
3739
4362
  let current = state;
3740
4363
  for (const service of services) {
@@ -3771,6 +4394,7 @@ var restartServices = async (runner, services, statePath, state, preserveDeviceR
3771
4394
  };
3772
4395
  var reportedRestartServices = (receipt) => receipt.restartingService === undefined || receipt.restartedServices.includes(receipt.restartingService) ? receipt.restartedServices : [...receipt.restartedServices, receipt.restartingService];
3773
4396
  var upgrade = async (options) => {
4397
+ const target = normalizeUpgradeTarget(options.target);
3774
4398
  const runner = options.runner ?? defaultRunner;
3775
4399
  const resolveExecutable = options.resolveExecutable ?? trustedExecutable;
3776
4400
  const runtimePlatform = options.platform ?? process.platform;
@@ -3778,7 +4402,7 @@ var upgrade = async (options) => {
3778
4402
  const candidates = await trustedServices(serviceCandidates(runtimePlatform, uid), resolveExecutable);
3779
4403
  const statePath = options.statePath ?? restartReceiptPath();
3780
4404
  const previous = await readReceipt(statePath);
3781
- if (previous?.version === options.currentVersion) {
4405
+ if (previous?.version === options.currentVersion && (target === "latest" || target === previous.version)) {
3782
4406
  const byId = new Map(candidates.map((service) => [service.id, service]));
3783
4407
  const pending = previous.pendingServices.flatMap((id) => {
3784
4408
  const service = byId.get(id);
@@ -3831,15 +4455,12 @@ var upgrade = async (options) => {
3831
4455
  dry_run: false
3832
4456
  };
3833
4457
  }
3834
- await rm2(statePath, { force: true });
3835
- const target = normalizeUpgradeTarget(options.target);
3836
4458
  const entrypoint = options.entrypoint ?? process.argv[1];
3837
4459
  if (entrypoint === undefined) {
3838
4460
  throw new Error("Could not resolve the Cohall executable path");
3839
4461
  }
3840
4462
  const canonicalEntrypoint = await realpath2(entrypoint);
3841
4463
  const installation = packageInstallation(canonicalEntrypoint, entrypoint);
3842
- const install = packageInstallCommand(installation, target);
3843
4464
  const services = await activeServices(runner, candidates);
3844
4465
  await assertServiceInstallations(runner, services, canonicalEntrypoint, entrypoint);
3845
4466
  if (options.dryRun) {
@@ -3855,14 +4476,21 @@ var upgrade = async (options) => {
3855
4476
  dry_run: true
3856
4477
  };
3857
4478
  }
3858
- const installExecutable = options.resolveExecutable === undefined ? await trustedExecutable(install.command, installation.prefix === undefined ? {} : { writableRoot: installation.prefix }) : await resolveExecutable(install.command);
3859
- await checked(runner, { ...install, command: installExecutable });
3860
- const nextVersion = await installedVersion(entrypoint);
4479
+ let nextVersion = await installedVersion(entrypoint).catch(() => {
4480
+ return;
4481
+ });
4482
+ if (nextVersion === undefined || target !== nextVersion) {
4483
+ const install = packageInstallCommand(installation, target);
4484
+ const installExecutable = options.resolveExecutable === undefined ? await trustedExecutable(install.command, installation.prefix === undefined ? {} : { writableRoot: installation.prefix }) : await resolveExecutable(install.command);
4485
+ await checked(runner, { ...install, command: installExecutable });
4486
+ nextVersion = await installedVersion(entrypoint);
4487
+ }
3861
4488
  if (target !== "latest" && nextVersion !== target) {
3862
4489
  throw new Error(`Installed Cohall ${nextVersion}, expected ${target}`);
3863
4490
  }
3864
4491
  const upgraded = nextVersion !== options.currentVersion;
3865
4492
  if (!options.restart || services.length === 0) {
4493
+ await rm2(statePath, { force: true });
3866
4494
  return {
3867
4495
  upgraded,
3868
4496
  from_version: options.currentVersion,
@@ -3905,7 +4533,7 @@ var xml = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").repl
3905
4533
  var systemdArgument = (value) => `"${value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"")}"`;
3906
4534
  var deviceServicePlan = (options) => {
3907
4535
  if (options.platform === "linux") {
3908
- const path = join4(options.home, ".config", "systemd", "user", "cohall-device.service");
4536
+ const path = join5(options.home, ".config", "systemd", "user", "cohall-device.service");
3909
4537
  return {
3910
4538
  file: {
3911
4539
  path,
@@ -3938,7 +4566,7 @@ WantedBy=default.target
3938
4566
  }
3939
4567
  if (options.platform === "darwin") {
3940
4568
  const label = "com.cohall.device";
3941
- const path = join4(options.home, "Library", "LaunchAgents", `${label}.plist`);
4569
+ const path = join5(options.home, "Library", "LaunchAgents", `${label}.plist`);
3942
4570
  const domain = `gui/${options.uid ?? 0}`;
3943
4571
  return {
3944
4572
  file: {
@@ -4009,10 +4637,10 @@ var checked2 = async (runner, command, arguments_, allowFailure = false) => {
4009
4637
  var installDeviceService = async (options = {}) => {
4010
4638
  const entrypoint = await realpath3(options.entrypoint ?? process.argv[1] ?? "");
4011
4639
  packageInstallation(entrypoint);
4012
- const platform3 = options.platform ?? process.platform;
4640
+ const platform4 = options.platform ?? process.platform;
4013
4641
  const runner = options.runner ?? defaultRunner2;
4014
- if (platform3 === "win32") {
4015
- const script = join4(dirname3(dirname3(entrypoint)), "deploy", "windows", "install-device.ps1");
4642
+ if (platform4 === "win32") {
4643
+ const script = join5(dirname4(dirname4(entrypoint)), "deploy", "windows", "install-device.ps1");
4016
4644
  await checked2(runner, "powershell.exe", [
4017
4645
  "-NoProfile",
4018
4646
  "-ExecutionPolicy",
@@ -4024,16 +4652,16 @@ var installDeviceService = async (options = {}) => {
4024
4652
  }
4025
4653
  const uid = options.uid ?? process.getuid?.();
4026
4654
  const plan = deviceServicePlan({
4027
- platform: platform3,
4655
+ platform: platform4,
4028
4656
  entrypoint,
4029
4657
  home: options.home ?? homedir3(),
4030
4658
  ...uid === undefined ? {} : { uid }
4031
4659
  });
4032
- await mkdir3(dirname3(plan.file.path), { recursive: true, mode: 448 });
4660
+ await mkdir4(dirname4(plan.file.path), { recursive: true, mode: 448 });
4033
4661
  await writeFile4(plan.file.path, plan.file.content, { mode: plan.file.mode });
4034
- await chmod4(plan.file.path, plan.file.mode);
4662
+ await chmod5(plan.file.path, plan.file.mode);
4035
4663
  for (const [index, command] of plan.commands.entries()) {
4036
- await checked2(runner, command.command, command.arguments, platform3 === "darwin" && index === 0);
4664
+ await checked2(runner, command.command, command.arguments, platform4 === "darwin" && index === 0);
4037
4665
  }
4038
4666
  return {
4039
4667
  installed: plan.file.path,
@@ -4041,9 +4669,9 @@ var installDeviceService = async (options = {}) => {
4041
4669
  };
4042
4670
  };
4043
4671
  var restartDeviceService = async (options = {}) => {
4044
- const platform3 = options.platform ?? process.platform;
4672
+ const platform4 = options.platform ?? process.platform;
4045
4673
  const runner = options.runner ?? defaultRunner2;
4046
- if (platform3 === "linux") {
4674
+ if (platform4 === "linux") {
4047
4675
  const service = "cohall-device.service";
4048
4676
  const check = await runner.run("systemctl", ["--user", "is-active", "--quiet", service], 1e4);
4049
4677
  if (check.exitCode !== 0) {
@@ -4052,7 +4680,7 @@ var restartDeviceService = async (options = {}) => {
4052
4680
  await checked2(runner, "systemctl", ["--user", "restart", service]);
4053
4681
  return { running: true, restarted: true, service };
4054
4682
  }
4055
- if (platform3 === "darwin") {
4683
+ if (platform4 === "darwin") {
4056
4684
  const service = "com.cohall.device";
4057
4685
  const target = `gui/${options.uid ?? process.getuid?.() ?? 0}/${service}`;
4058
4686
  const check = await runner.run("launchctl", ["print", target], 1e4);
@@ -4062,7 +4690,7 @@ var restartDeviceService = async (options = {}) => {
4062
4690
  await checked2(runner, "launchctl", ["kickstart", "-k", target]);
4063
4691
  return { running: true, restarted: true, service };
4064
4692
  }
4065
- if (platform3 === "win32") {
4693
+ if (platform4 === "win32") {
4066
4694
  const service = "Cohall Device";
4067
4695
  const check = await runner.run("schtasks.exe", ["/Query", "/TN", service], 1e4);
4068
4696
  if (check.exitCode !== 0) {
@@ -4076,10 +4704,10 @@ var restartDeviceService = async (options = {}) => {
4076
4704
  };
4077
4705
 
4078
4706
  // apps/device/src/relay-migration.ts
4079
- var sha256 = Schema7.String.check(Schema7.isPattern(/^[a-f0-9]{64}$/, { expected: "a lowercase SHA-256 digest" }));
4080
- var RelayBackupManifest = Schema7.Struct({
4081
- formatVersion: Schema7.Literal(1),
4082
- cohallVersion: Schema7.NonEmptyString,
4707
+ var sha256 = Schema9.String.check(Schema9.isPattern(/^[a-f0-9]{64}$/, { expected: "a lowercase SHA-256 digest" }));
4708
+ var RelayBackupManifest = Schema9.Struct({
4709
+ formatVersion: Schema9.Literal(1),
4710
+ cohallVersion: Schema9.NonEmptyString,
4083
4711
  createdAt: Timestamp,
4084
4712
  databaseSha256: sha256,
4085
4713
  ownerTokenSha256: sha256
@@ -4103,27 +4731,27 @@ var verifyBackupMember = async (path, maximumBytes) => {
4103
4731
  };
4104
4732
  var readManifest = async (path) => {
4105
4733
  await verifyBackupMember(path, 16384);
4106
- return Schema7.decodeUnknownSync(RelayBackupManifest)(JSON.parse(await readFile3(path, "utf8")));
4734
+ return Schema9.decodeUnknownSync(RelayBackupManifest)(JSON.parse(await readFile3(path, "utf8")));
4107
4735
  };
4108
4736
  var verifyBackupDestinationParent = async (path) => {
4109
4737
  if (process.platform === "win32") {
4110
4738
  return;
4111
4739
  }
4112
- for (let current = await realpath4(path);; current = dirname4(current)) {
4740
+ for (let current = await realpath4(path);; current = dirname5(current)) {
4113
4741
  const metadata = await stat2(current);
4114
4742
  const writableByOthers = (metadata.mode & 18) !== 0;
4115
4743
  const sticky = (metadata.mode & 512) !== 0;
4116
4744
  if (writableByOthers && !sticky) {
4117
4745
  throw new Error(`Backup destination ancestors must not be group- or world-writable unless they use the sticky bit: ${current}`);
4118
4746
  }
4119
- const parent = dirname4(current);
4747
+ const parent = dirname5(current);
4120
4748
  if (parent === current) {
4121
4749
  return;
4122
4750
  }
4123
4751
  }
4124
4752
  };
4125
4753
  var ownerToken = async (dataDirectory) => {
4126
- const path = join5(dataDirectory, "owner-token");
4754
+ const path = join6(dataDirectory, "owner-token");
4127
4755
  const configured = process.env.COHALL_TOKEN;
4128
4756
  if (configured !== undefined) {
4129
4757
  return configured;
@@ -4149,7 +4777,7 @@ var verifyDatabase = (path) => {
4149
4777
  };
4150
4778
  var backupRelay = async (destinationInput) => {
4151
4779
  const sourceDirectory = relayDataDirectory();
4152
- const sourceDatabase = join5(sourceDirectory, "cohall.db");
4780
+ const sourceDatabase = join6(sourceDirectory, "cohall.db");
4153
4781
  if (!await exists(sourceDatabase)) {
4154
4782
  throw new Error(`Relay database was not found at ${sourceDatabase}`);
4155
4783
  }
@@ -4157,11 +4785,11 @@ var backupRelay = async (destinationInput) => {
4157
4785
  if (await exists(destination)) {
4158
4786
  throw new Error(`Backup destination already exists: ${destination}`);
4159
4787
  }
4160
- await verifyBackupDestinationParent(dirname4(destination));
4161
- await mkdir4(destination, { mode: 448 });
4788
+ await verifyBackupDestinationParent(dirname5(destination));
4789
+ await mkdir5(destination, { mode: 448 });
4162
4790
  try {
4163
- const databasePath = join5(destination, "cohall.db");
4164
- const ownerTokenPath = join5(destination, "owner-token");
4791
+ const databasePath = join6(destination, "cohall.db");
4792
+ const ownerTokenPath = join6(destination, "owner-token");
4165
4793
  const database = new DatabaseSync(sourceDatabase, { readOnly: true });
4166
4794
  try {
4167
4795
  await sqliteBackup(database, databasePath);
@@ -4175,7 +4803,7 @@ var backupRelay = async (destinationInput) => {
4175
4803
  }
4176
4804
  await writeFile5(ownerTokenPath, `${token}
4177
4805
  `, { mode: 384 });
4178
- await Promise.all([chmod5(databasePath, 384), chmod5(ownerTokenPath, 384)]);
4806
+ await Promise.all([chmod6(databasePath, 384), chmod6(ownerTokenPath, 384)]);
4179
4807
  const createdAt = Timestamp.make(new Date().toISOString());
4180
4808
  const manifest = RelayBackupManifest.make({
4181
4809
  formatVersion: 1,
@@ -4184,7 +4812,7 @@ var backupRelay = async (destinationInput) => {
4184
4812
  databaseSha256: await digest(databasePath),
4185
4813
  ownerTokenSha256: await digest(ownerTokenPath)
4186
4814
  });
4187
- await writeFile5(join5(destination, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
4815
+ await writeFile5(join6(destination, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
4188
4816
  `, {
4189
4817
  mode: 384
4190
4818
  });
@@ -4201,9 +4829,9 @@ var backupRelay = async (destinationInput) => {
4201
4829
  };
4202
4830
  var restoreRelay = async (sourceInput) => {
4203
4831
  const source = resolve2(sourceInput);
4204
- const sourceDatabase = join5(source, "cohall.db");
4205
- const sourceOwnerToken = join5(source, "owner-token");
4206
- const sourceManifest = join5(source, "manifest.json");
4832
+ const sourceDatabase = join6(source, "cohall.db");
4833
+ const sourceOwnerToken = join6(source, "owner-token");
4834
+ const sourceManifest = join6(source, "manifest.json");
4207
4835
  for (const path of [sourceDatabase, sourceOwnerToken, sourceManifest]) {
4208
4836
  if (!await exists(path)) {
4209
4837
  throw new Error(`Relay backup is missing ${basename(path)}`);
@@ -4218,24 +4846,24 @@ var restoreRelay = async (sourceInput) => {
4218
4846
  if (await exists(target)) {
4219
4847
  throw new Error(`Relay data directory already exists: ${target}. Restore into a new COHALL_DATA_DIR.`);
4220
4848
  }
4221
- const parent = dirname4(target);
4222
- await mkdir4(parent, { recursive: true, mode: 448 });
4223
- const staging = await mkdtemp2(join5(parent, `.${basename(target)}.restore-`));
4849
+ const parent = dirname5(target);
4850
+ await mkdir5(parent, { recursive: true, mode: 448 });
4851
+ const staging = await mkdtemp2(join6(parent, `.${basename(target)}.restore-`));
4224
4852
  let backupVersion = "";
4225
4853
  try {
4226
- await chmod5(staging, 448);
4227
- const databasePath = join5(staging, "cohall.db");
4228
- const ownerTokenPath = join5(staging, "owner-token");
4229
- const manifestPath = join5(staging, "migration-manifest.json");
4854
+ await chmod6(staging, 448);
4855
+ const databasePath = join6(staging, "cohall.db");
4856
+ const ownerTokenPath = join6(staging, "owner-token");
4857
+ const manifestPath = join6(staging, "migration-manifest.json");
4230
4858
  await Promise.all([
4231
4859
  copyFile(sourceDatabase, databasePath),
4232
4860
  copyFile(sourceOwnerToken, ownerTokenPath),
4233
4861
  copyFile(sourceManifest, manifestPath)
4234
4862
  ]);
4235
4863
  await Promise.all([
4236
- chmod5(databasePath, 384),
4237
- chmod5(ownerTokenPath, 384),
4238
- chmod5(manifestPath, 384)
4864
+ chmod6(databasePath, 384),
4865
+ chmod6(ownerTokenPath, 384),
4866
+ chmod6(manifestPath, 384)
4239
4867
  ]);
4240
4868
  const manifest = await readManifest(manifestPath);
4241
4869
  if (await digest(databasePath) !== manifest.databaseSha256 || await digest(ownerTokenPath) !== manifest.ownerTokenSha256) {
@@ -4267,7 +4895,7 @@ var websocketUrl = (relayUrl) => {
4267
4895
  };
4268
4896
  var isLoopback = (url) => url.hostname === "localhost" || url.hostname === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(url.hostname);
4269
4897
  var verifyClientCredential = async (relayUrl, token) => {
4270
- await Effect6.runPromise(exports_src.make({ baseUrl: relayUrl, token }).devices());
4898
+ await Effect8.runPromise(exports_src.make({ baseUrl: relayUrl, token }).devices());
4271
4899
  };
4272
4900
  var verifyDeviceCredential = (relayUrl, token) => new Promise((resolvePromise, reject) => {
4273
4901
  const socket = new WebSocket(websocketUrl(relayUrl), {
@@ -4295,7 +4923,7 @@ var verifyDeviceCredential = (relayUrl, token) => new Promise((resolvePromise, r
4295
4923
  });
4296
4924
  socket.on("message", (data) => {
4297
4925
  try {
4298
- const event = Schema7.decodeUnknownSync(SocketEvent)(JSON.parse(data.toString()));
4926
+ const event = Schema9.decodeUnknownSync(SocketEvent)(JSON.parse(data.toString()));
4299
4927
  if (event._tag === "Connected") {
4300
4928
  finish();
4301
4929
  } else if (event._tag === "Error") {
@@ -4364,9 +4992,14 @@ var switchRelay = async (options) => {
4364
4992
  var valueOptions = new Set([
4365
4993
  "context",
4366
4994
  "context-file",
4995
+ "grok-gateway",
4996
+ "error",
4367
4997
  "label",
4368
4998
  "model",
4999
+ "message",
5000
+ "message-file",
4369
5001
  "name",
5002
+ "parent",
4370
5003
  "prompt",
4371
5004
  "prompt-file",
4372
5005
  "provider",
@@ -4404,15 +5037,19 @@ Usage:
4404
5037
  cohall join --relay <url> --workspace <path> [--providers list] [--token-file <path>]
4405
5038
  cohall configure [--relay url] [--name name] [--workspace path]
4406
5039
  [--providers codex,opencode|auto] [--model id]
5040
+ [--grok-gateway path]
4407
5041
  cohall config
4408
5042
  cohall devices
4409
- cohall delegate [prompt] [--target @device] [--provider provider]
4410
- [--context text] [--thread uuid] [--workspace path]
5043
+ cohall bots
5044
+ cohall send [@device-or-bot] [prompt] [delegate options]
5045
+ cohall delegate [prompt] [--target @device-or-bot] [--provider provider]
5046
+ [--context text] [--thread uuid] [--parent task-id] [--workspace path]
4411
5047
  [--timeout seconds] [--no-wait]
4412
5048
  cohall status <task-id>
4413
5049
  cohall trace <task-id> [--follow]
4414
5050
  cohall wait <task-id> [--timeout seconds]
4415
5051
  cohall cancel <task-id>
5052
+ cohall reply <task-id> --message-file path | --message text | --message - | --error text
4416
5053
  cohall thread <thread-id>
4417
5054
  cohall pair [--label name] [--client-only]
4418
5055
  cohall sessions
@@ -4434,7 +5071,7 @@ Usage:
4434
5071
  cohall skill [install [agents|claude|opencode|all]]
4435
5072
  cohall integrations
4436
5073
 
4437
- Providers: codex, claude-code, opencode.
5074
+ Providers: codex, claude-code, opencode, grok-bot.
4438
5075
  Configuration is stored per user; environment variables override it.`;
4439
5076
  var parseArguments = (values) => {
4440
5077
  const options = new Map;
@@ -4520,6 +5157,7 @@ var printLine = (value) => console.log(JSON.stringify(value));
4520
5157
  var readInput = async (arguments_, name) => {
4521
5158
  const direct = option(arguments_, name);
4522
5159
  const path = option(arguments_, `${name}-file`);
5160
+ const byteLimit = name === "message" ? 4 * 131072 : 131072;
4523
5161
  if (direct !== undefined && path !== undefined) {
4524
5162
  throw new Error(`Use either --${name} or --${name}-file, not both`);
4525
5163
  }
@@ -4527,14 +5165,13 @@ var readInput = async (arguments_, name) => {
4527
5165
  if (!await access4(path).then(() => true).catch(() => false)) {
4528
5166
  throw new Error(`File does not exist: ${path}`);
4529
5167
  }
4530
- if ((await stat3(path)).size > 131072) {
4531
- throw new Error(`${name} file exceeds 128 KiB`);
5168
+ if ((await stat3(path)).size > byteLimit) {
5169
+ throw new Error(`${name} file exceeds ${byteLimit / 1024} KiB`);
4532
5170
  }
4533
- return readFile4(path, "utf8");
4534
5171
  }
4535
- const result = direct === "-" ? await readStdin(131072, name) : direct;
5172
+ const result = path === undefined ? direct === "-" ? await readStdin(byteLimit, name) : direct : await readFile4(path, "utf8");
4536
5173
  if (result !== undefined && result.length > 131072) {
4537
- throw new Error(`${name} exceeds 128 KiB`);
5174
+ throw new Error(`${name} exceeds 131072 characters`);
4538
5175
  }
4539
5176
  return result;
4540
5177
  };
@@ -4573,14 +5210,14 @@ var pairingToken = async (arguments_) => {
4573
5210
  return token;
4574
5211
  };
4575
5212
  var client = async () => {
4576
- const configuration = await Effect7.runPromise(loadClientConfiguration);
5213
+ const configuration = await Effect9.runPromise(loadClientConfiguration);
4577
5214
  return {
4578
5215
  configuration,
4579
5216
  relay: exports_src.make({ baseUrl: configuration.relayUrl, token: configuration.token })
4580
5217
  };
4581
5218
  };
4582
5219
  var ownerClient = async () => {
4583
- const configuration = await Effect7.runPromise(loadOwnerConfiguration);
5220
+ const configuration = await Effect9.runPromise(loadOwnerConfiguration);
4584
5221
  return {
4585
5222
  configuration,
4586
5223
  relay: exports_src.make({ baseUrl: configuration.relayUrl, token: configuration.token })
@@ -4588,16 +5225,16 @@ var ownerClient = async () => {
4588
5225
  };
4589
5226
  var installSkill = async (target) => {
4590
5227
  const roots = {
4591
- agents: join6(homedir4(), ".agents", "skills", "cohall", "SKILL.md"),
4592
- claude: join6(homedir4(), ".claude", "skills", "cohall", "SKILL.md"),
4593
- opencode: join6(homedir4(), ".config", "opencode", "skills", "cohall", "SKILL.md")
5228
+ agents: join7(homedir4(), ".agents", "skills", "cohall", "SKILL.md"),
5229
+ claude: join7(homedir4(), ".claude", "skills", "cohall", "SKILL.md"),
5230
+ opencode: join7(homedir4(), ".config", "opencode", "skills", "cohall", "SKILL.md")
4594
5231
  };
4595
5232
  const selected = target === "all" ? Object.values(roots) : (target in roots) ? [roots[target]] : undefined;
4596
5233
  if (selected === undefined) {
4597
5234
  throw new Error("Skill target must be agents, claude, opencode, or all");
4598
5235
  }
4599
5236
  for (const path of selected) {
4600
- await mkdir5(dirname5(path), { recursive: true });
5237
+ await mkdir6(dirname6(path), { recursive: true });
4601
5238
  await writeFile6(path, SKILL_default);
4602
5239
  }
4603
5240
  return selected;
@@ -4606,6 +5243,21 @@ var printHelp = () => console.log(help);
4606
5243
  var printSkill = () => console.log(SKILL_default.trimEnd());
4607
5244
  var runCli = async (command, raw) => {
4608
5245
  const arguments_ = parseArguments(raw);
5246
+ if (command === "reply") {
5247
+ allowOptions(arguments_, ["message", "message-file", "error"]);
5248
+ const id = Schema10.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
5249
+ const error = option(arguments_, "error");
5250
+ if (error !== undefined && (option(arguments_, "message") !== undefined || option(arguments_, "message-file") !== undefined)) {
5251
+ throw new Error("Use either --error or --message/--message-file, not both");
5252
+ }
5253
+ const message = error === undefined ? await readInput(arguments_, "message") : undefined;
5254
+ if (error === undefined && message === undefined) {
5255
+ throw new Error("A bot reply requires --message, --message-file, or --error");
5256
+ }
5257
+ await writeBotReply(id, error === undefined ? { result: message ?? "" } : { error });
5258
+ print({ submitted: true, task_id: id });
5259
+ return;
5260
+ }
4609
5261
  if (command === "init") {
4610
5262
  allowOptions(arguments_, [
4611
5263
  "client-only",
@@ -4731,7 +5383,7 @@ var runCli = async (command, raw) => {
4731
5383
  const { relay: relay2 } = await ownerClient();
4732
5384
  const restart = !arguments_.options.has("no-restart");
4733
5385
  if (arguments_.options.has("dry-run")) {
4734
- const devices = await Effect7.runPromise(relay2.devices());
5386
+ const devices = await Effect9.runPromise(relay2.devices());
4735
5387
  assertDeviceOperationSupport(devices);
4736
5388
  print({
4737
5389
  dry_run: true,
@@ -4741,7 +5393,7 @@ var runCli = async (command, raw) => {
4741
5393
  });
4742
5394
  return;
4743
5395
  }
4744
- const operations = await Effect7.runPromise(relay2.createUpgradeOperations({ target: normalizedTarget, restart }));
5396
+ const operations = await Effect9.runPromise(relay2.createUpgradeOperations({ target: normalizedTarget, restart }));
4745
5397
  print({
4746
5398
  queued: operations.length,
4747
5399
  requested_version: normalizedTarget,
@@ -4767,10 +5419,10 @@ var runCli = async (command, raw) => {
4767
5419
  if (arguments_.positionals.length !== 2 || arguments_.positionals[0] !== "abandon") {
4768
5420
  throw new Error("Usage: cohall upgrades [abandon <operation-id>]");
4769
5421
  }
4770
- print(await Effect7.runPromise(relay2.abandonOperation(Schema8.decodeUnknownSync(OperationId)(arguments_.positionals[1]))));
5422
+ print(await Effect9.runPromise(relay2.abandonOperation(Schema10.decodeUnknownSync(OperationId)(arguments_.positionals[1]))));
4771
5423
  return;
4772
5424
  }
4773
- print(await Effect7.runPromise(relay2.operations()));
5425
+ print(await Effect9.runPromise(relay2.operations()));
4774
5426
  return;
4775
5427
  }
4776
5428
  if (command === "join") {
@@ -4829,20 +5481,31 @@ var runCli = async (command, raw) => {
4829
5481
  device_credential: configuration2.deviceToken !== undefined,
4830
5482
  providers: configuration2.providers ?? "auto",
4831
5483
  model: configuration2.model,
4832
- sandbox: configuration2.sandbox
5484
+ sandbox: configuration2.sandbox,
5485
+ grok_gateway: configuration2.grokGateway
4833
5486
  }
4834
5487
  });
4835
5488
  return;
4836
5489
  }
4837
5490
  if (command === "configure") {
4838
- allowOptions(arguments_, ["model", "name", "providers", "relay", "sandbox", "workspace"]);
5491
+ allowOptions(arguments_, [
5492
+ "grok-gateway",
5493
+ "model",
5494
+ "name",
5495
+ "providers",
5496
+ "relay",
5497
+ "sandbox",
5498
+ "workspace"
5499
+ ]);
4839
5500
  noPositionals(arguments_, command);
4840
5501
  const existing = await readStoredConfiguration();
4841
5502
  const relayUrl = normalizeRelayUrl(option(arguments_, "relay") ?? existing?.relayUrl ?? "http://127.0.0.1:8787");
4842
5503
  const supplied = values(arguments_, "workspace");
4843
- const workspaces = supplied.length === 0 ? existing?.workspaces ?? [] : await Effect7.runPromise(parseWorkspaces("", JSON.stringify(supplied)));
5504
+ const workspaces = supplied.length === 0 ? existing?.workspaces ?? [] : await Effect9.runPromise(parseWorkspaces("", JSON.stringify(supplied)));
4844
5505
  const sandbox = option(arguments_, "sandbox");
4845
5506
  const model = option(arguments_, "model") ?? existing?.model;
5507
+ const gatewayInput = option(arguments_, "grok-gateway") ?? existing?.grokGateway;
5508
+ const grokGateway = gatewayInput === undefined ? undefined : resolve3(gatewayInput);
4846
5509
  const providerInput = option(arguments_, "providers");
4847
5510
  const providers = providerInput === undefined ? existing?.providers : providerInput === "auto" ? undefined : parseProviders(providerInput);
4848
5511
  const configuration2 = StoredConfiguration.make({
@@ -4854,8 +5517,9 @@ var runCli = async (command, raw) => {
4854
5517
  ...credentialsForRelay(existing, relayUrl),
4855
5518
  ...providers === undefined ? {} : { providers },
4856
5519
  ...model === undefined ? {} : { model },
5520
+ ...grokGateway === undefined ? {} : { grokGateway },
4857
5521
  ...(sandbox ?? existing?.sandbox) === undefined ? {} : {
4858
- sandbox: Schema8.decodeUnknownSync(Schema8.Literals(["read-only", "workspace-write", "danger-full-access"]))(sandbox ?? existing?.sandbox)
5522
+ sandbox: Schema10.decodeUnknownSync(Schema10.Literals(["read-only", "workspace-write", "danger-full-access"]))(sandbox ?? existing?.sandbox)
4859
5523
  }
4860
5524
  });
4861
5525
  await writeStoredConfiguration(configuration2);
@@ -4898,7 +5562,7 @@ var runCli = async (command, raw) => {
4898
5562
  noPositionals(arguments_, command);
4899
5563
  if (arguments_.options.has("all")) {
4900
5564
  const { relay: relay2 } = await client();
4901
- print(allDeviceHealth(await Effect7.runPromise(relay2.devices()), version));
5565
+ print(allDeviceHealth(await Effect9.runPromise(relay2.devices()), version));
4902
5566
  return;
4903
5567
  }
4904
5568
  const configuration2 = await readStoredConfiguration();
@@ -4911,14 +5575,18 @@ var runCli = async (command, raw) => {
4911
5575
  });
4912
5576
  const providerInput = process.env.COHALL_DEVICE_PROVIDERS;
4913
5577
  const selectedProviders = providerInput === undefined ? configuration2?.providers : providerInput === "auto" ? undefined : parseProviders(providerInput);
4914
- const providerExecutables = Object.fromEntries(Provider.literals.map((provider) => [
5578
+ const providerExecutables = Object.fromEntries(Provider.literals.filter((provider) => provider !== "grok-bot").map((provider) => [
4915
5579
  provider,
4916
5580
  findExecutable(provider === "claude-code" ? "claude" : provider) ?? "not found"
4917
5581
  ]));
5582
+ const grokGateway = process.env.COHALL_GROK_GATEWAY ?? configuration2?.grokGateway;
5583
+ const grokBots = grokGateway === undefined ? undefined : await discoverGrokBots(grokGateway).catch(() => {
5584
+ return;
5585
+ });
4918
5586
  const clientToken = process.env.COHALL_CLIENT_TOKEN ?? storedCredentials.clientToken;
4919
5587
  const hasDeviceCredential = process.env.COHALL_DEVICE_TOKEN !== undefined || storedCredentials.deviceToken !== undefined;
4920
5588
  const deviceId = process.env.COHALL_DEVICE_ID ?? configuration2?.deviceId;
4921
- const currentDevice = response?.ok !== true || clientToken === undefined || deviceId === undefined ? undefined : await Effect7.runPromise(exports_src.make({ baseUrl: relayUrl, token: clientToken }).devices()).then((devices) => devices.find((device) => device.id === deviceId)).catch(() => {
5589
+ const currentDevice = response?.ok !== true || clientToken === undefined || deviceId === undefined ? undefined : await Effect9.runPromise(exports_src.make({ baseUrl: relayUrl, token: clientToken }).devices()).then((devices) => devices.find((device) => device.id === deviceId)).catch(() => {
4922
5590
  return;
4923
5591
  });
4924
5592
  const versionWarning = deviceVersionWarning(version, currentDevice?.version);
@@ -4928,7 +5596,9 @@ var runCli = async (command, raw) => {
4928
5596
  ...hasDeviceCredential && response?.ok === true && currentDevice?.status !== "online" && currentDevice?.status !== "busy" ? [
4929
5597
  clientToken === undefined ? "Device status cannot be checked without a client credential" : "Device daemon is not connected or is not registered with the relay"
4930
5598
  ] : [],
4931
- ...selectedProviders?.flatMap((provider) => providerExecutables[provider] === "not found" ? [`Configured provider ${provider} is not installed or not on PATH`] : []) ?? []
5599
+ ...selectedProviders?.flatMap((provider) => provider === "grok-bot" ? grokBots === undefined ? [
5600
+ "Configured Grok Bot gateway is unavailable; check its discovery file and host process"
5601
+ ] : [] : providerExecutables[provider] === "not found" ? [`Configured provider ${provider} is not installed or not on PATH`] : []) ?? []
4932
5602
  ];
4933
5603
  print({
4934
5604
  version,
@@ -4939,6 +5609,13 @@ var runCli = async (command, raw) => {
4939
5609
  device_credential: hasDeviceCredential,
4940
5610
  workspaces: configuration2?.workspaces ?? [],
4941
5611
  providers: providerExecutables,
5612
+ ...grokGateway === undefined ? {} : {
5613
+ grok_gateway: {
5614
+ path: grokGateway,
5615
+ reachable: grokBots !== undefined,
5616
+ bots: grokBots?.length ?? 0
5617
+ }
5618
+ },
4942
5619
  provider_selection: selectedProviders ?? "auto",
4943
5620
  provider_authentication: "checked when delegated work starts",
4944
5621
  device_status: hasDeviceCredential === false ? "not configured" : currentDevice?.status ?? "unknown",
@@ -4951,7 +5628,7 @@ var runCli = async (command, raw) => {
4951
5628
  allowOptions(arguments_, []);
4952
5629
  noPositionals(arguments_, command);
4953
5630
  const { relay: relay2 } = await client();
4954
- print(deviceVersions(await Effect7.runPromise(relay2.devices()), version));
5631
+ print(deviceVersions(await Effect9.runPromise(relay2.devices()), version));
4955
5632
  return;
4956
5633
  }
4957
5634
  if (command === "usage") {
@@ -4959,7 +5636,7 @@ var runCli = async (command, raw) => {
4959
5636
  noPositionals(arguments_, command);
4960
5637
  const { relay: relay2 } = await client();
4961
5638
  print({
4962
- ...await Effect7.runPromise(relay2.usage()),
5639
+ ...await Effect9.runPromise(relay2.usage()),
4963
5640
  scope: "Retained Cohall task activity; provider token and billing usage are not available."
4964
5641
  });
4965
5642
  return;
@@ -4970,7 +5647,7 @@ var runCli = async (command, raw) => {
4970
5647
  allowOptions(arguments_, ["client-only", "label"]);
4971
5648
  noPositionals(arguments_, command);
4972
5649
  const roles = arguments_.options.has("client-only") ? ["client"] : ["client", "device"];
4973
- const credential = await Effect7.runPromise(relay2.createPairing({ label: option(arguments_, "label") ?? "Cohall device", roles }));
5650
+ const credential = await Effect9.runPromise(relay2.createPairing({ label: option(arguments_, "label") ?? "Cohall device", roles }));
4974
5651
  print({
4975
5652
  relay_url: configuration2.relayUrl,
4976
5653
  pairing_token: credential.token,
@@ -4982,30 +5659,37 @@ var runCli = async (command, raw) => {
4982
5659
  if (command === "sessions") {
4983
5660
  allowOptions(arguments_, []);
4984
5661
  noPositionals(arguments_, command);
4985
- print(await Effect7.runPromise(relay2.authSessions()));
5662
+ print(await Effect9.runPromise(relay2.authSessions()));
4986
5663
  return;
4987
5664
  }
4988
5665
  if (command === "forget") {
4989
5666
  allowOptions(arguments_, []);
4990
- print(await Effect7.runPromise(relay2.forgetDevice(Schema8.decodeUnknownSync(DeviceId)(identifier(arguments_, "device id")))));
5667
+ print(await Effect9.runPromise(relay2.forgetDevice(Schema10.decodeUnknownSync(DeviceId)(identifier(arguments_, "device id")))));
4991
5668
  return;
4992
5669
  }
4993
5670
  allowOptions(arguments_, []);
4994
- print(await Effect7.runPromise(relay2.revokeAuthSession(Schema8.decodeUnknownSync(AuthSessionId)(identifier(arguments_, "session id")))));
5671
+ print(await Effect9.runPromise(relay2.revokeAuthSession(Schema10.decodeUnknownSync(AuthSessionId)(identifier(arguments_, "session id")))));
4995
5672
  return;
4996
5673
  }
4997
5674
  const { configuration, relay } = await client();
4998
5675
  if (command === "devices") {
4999
5676
  allowOptions(arguments_, []);
5000
5677
  noPositionals(arguments_, command);
5001
- print(await Effect7.runPromise(relay.devices()));
5678
+ print(await Effect9.runPromise(relay.devices()));
5679
+ return;
5680
+ }
5681
+ if (command === "bots") {
5682
+ allowOptions(arguments_, []);
5683
+ noPositionals(arguments_, command);
5684
+ print(listBots(await Effect9.runPromise(relay.devices())));
5002
5685
  return;
5003
5686
  }
5004
- if (command === "delegate") {
5687
+ if (command === "delegate" || command === "send") {
5005
5688
  allowOptions(arguments_, [
5006
5689
  "context",
5007
5690
  "context-file",
5008
5691
  "no-wait",
5692
+ "parent",
5009
5693
  "prompt",
5010
5694
  "prompt-file",
5011
5695
  "provider",
@@ -5014,66 +5698,73 @@ var runCli = async (command, raw) => {
5014
5698
  "timeout",
5015
5699
  "workspace"
5016
5700
  ]);
5701
+ const positionalTarget = command === "send" && arguments_.positionals[0]?.startsWith("@") ? arguments_.positionals[0] : undefined;
5702
+ const positionals = positionalTarget === undefined ? arguments_.positionals : arguments_.positionals.slice(1);
5703
+ if (positionalTarget !== undefined && option(arguments_, "target") !== undefined) {
5704
+ throw new Error("Use either a positional target or --target, not both");
5705
+ }
5017
5706
  if (option(arguments_, "prompt") === "-" && option(arguments_, "context") === "-") {
5018
5707
  throw new Error("Only one of --prompt and --context may read from stdin");
5019
5708
  }
5020
5709
  const suppliedPrompt = await readInput(arguments_, "prompt");
5021
- const prompt = suppliedPrompt ?? arguments_.positionals.join(" ");
5710
+ const prompt = suppliedPrompt ?? positionals.join(" ");
5022
5711
  if (prompt.trim().length === 0) {
5023
5712
  throw new Error("A prompt is required");
5024
5713
  }
5025
- if (suppliedPrompt !== undefined && arguments_.positionals.length > 0) {
5714
+ if (suppliedPrompt !== undefined && positionals.length > 0) {
5026
5715
  throw new Error("Use either a positional prompt or --prompt, not both");
5027
5716
  }
5028
5717
  const context = await readInput(arguments_, "context");
5029
- const target = option(arguments_, "target");
5718
+ const target = positionalTarget ?? option(arguments_, "target");
5030
5719
  const thread = option(arguments_, "thread");
5031
5720
  const workspace = option(arguments_, "workspace");
5032
5721
  const provider = option(arguments_, "provider");
5033
- const task = await Effect7.runPromise(createDelegation(relay, configuration, {
5722
+ const parent = option(arguments_, "parent");
5723
+ const task = await Effect9.runPromise(createDelegation(relay, configuration, {
5034
5724
  prompt,
5035
5725
  ...target === undefined ? {} : { target },
5036
5726
  ...context === undefined ? {} : { context },
5037
- ...thread === undefined ? {} : { threadId: Schema8.decodeUnknownSync(ThreadId)(thread) },
5727
+ ...thread === undefined ? {} : { threadId: Schema10.decodeUnknownSync(ThreadId)(thread) },
5728
+ ...parent === undefined ? {} : { parentTaskId: Schema10.decodeUnknownSync(TaskId)(parent) },
5038
5729
  ...workspace === undefined ? {} : { workspace },
5039
- ...provider === undefined ? {} : { provider: Schema8.decodeUnknownSync(Provider)(provider) }
5730
+ ...provider === undefined ? {} : { provider: Schema10.decodeUnknownSync(Provider)(provider) }
5040
5731
  }));
5041
- print(taskResult(arguments_.options.has("no-wait") ? task : await Effect7.runPromise(waitForTask(relay, task, timeout(arguments_)))));
5732
+ print(taskResult(arguments_.options.has("no-wait") ? task : await Effect9.runPromise(waitForTask(relay, task, timeout(arguments_)))));
5042
5733
  return;
5043
5734
  }
5044
5735
  if (command === "status") {
5045
5736
  allowOptions(arguments_, []);
5046
- const id = Schema8.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
5047
- print(taskResult(await Effect7.runPromise(relay.getTask(id))));
5737
+ const id = Schema10.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
5738
+ print(taskResult(await Effect9.runPromise(relay.getTask(id))));
5048
5739
  return;
5049
5740
  }
5050
5741
  if (command === "trace") {
5051
5742
  allowOptions(arguments_, ["follow"]);
5052
- const id = Schema8.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
5743
+ const id = Schema10.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
5053
5744
  if (arguments_.options.has("follow")) {
5054
- await Effect7.runPromise(followTaskTrace(relay, id, printLine));
5745
+ await Effect9.runPromise(followTaskTrace(relay, id, printLine));
5055
5746
  return;
5056
5747
  }
5057
- print(await Effect7.runPromise(relay.traceTask(id)));
5748
+ print(await Effect9.runPromise(relay.traceTask(id)));
5058
5749
  return;
5059
5750
  }
5060
5751
  if (command === "wait") {
5061
5752
  allowOptions(arguments_, ["timeout"]);
5062
- const id = Schema8.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
5063
- const task = await Effect7.runPromise(relay.getTask(id));
5064
- print(taskResult(await Effect7.runPromise(waitForTask(relay, task, timeout(arguments_)))));
5753
+ const id = Schema10.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
5754
+ const task = await Effect9.runPromise(relay.getTask(id));
5755
+ print(taskResult(await Effect9.runPromise(waitForTask(relay, task, timeout(arguments_)))));
5065
5756
  return;
5066
5757
  }
5067
5758
  if (command === "cancel") {
5068
5759
  allowOptions(arguments_, []);
5069
- const id = Schema8.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
5070
- print(taskResult(await Effect7.runPromise(relay.cancelTask(id))));
5760
+ const id = Schema10.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
5761
+ print(taskResult(await Effect9.runPromise(relay.cancelTask(id))));
5071
5762
  return;
5072
5763
  }
5073
5764
  if (command === "thread") {
5074
5765
  allowOptions(arguments_, []);
5075
- const id = Schema8.decodeUnknownSync(ThreadId)(identifier(arguments_, "thread id"));
5076
- print(await Effect7.runPromise(threadContext(relay, id)));
5766
+ const id = Schema10.decodeUnknownSync(ThreadId)(identifier(arguments_, "thread id"));
5767
+ print(await Effect9.runPromise(threadContext(relay, id)));
5077
5768
  return;
5078
5769
  }
5079
5770
  throw new Error(`Unknown Cohall command: ${command}`);
@@ -5081,15 +5772,15 @@ var runCli = async (command, raw) => {
5081
5772
 
5082
5773
  // apps/device/src/daemon.ts
5083
5774
  init_src();
5084
- import { Effect as Effect8, Schedule as Schedule2, Schema as Schema9 } from "effect";
5775
+ import { Effect as Effect10, Schedule as Schedule3, Schema as Schema11 } from "effect";
5085
5776
  import { constants as constants3 } from "node:fs";
5086
- import { open, realpath as realpath5, stat as stat4 } from "node:fs/promises";
5087
- import { arch, hostname as hostname3, platform as platform3 } from "node:os";
5777
+ import { open as open3, realpath as realpath5, stat as stat4 } from "node:fs/promises";
5778
+ import { arch, hostname as hostname3, platform as platform4 } from "node:os";
5088
5779
  import { basename as basename2, isAbsolute as isAbsolute3, relative as relative2 } from "node:path";
5089
5780
  import { WebSocket as WebSocket2 } from "ws";
5090
5781
  var maxQueuedRelayMessages = 8;
5091
5782
 
5092
- class DeviceConnectionError extends Schema9.TaggedErrorClass()("Device.ConnectionError", { message: Schema9.String }) {
5783
+ class DeviceConnectionError extends Schema11.TaggedErrorClass()("Device.ConnectionError", { message: Schema11.String }) {
5093
5784
  }
5094
5785
  var socketUrl = (configuration) => {
5095
5786
  const url = new URL(configuration.relayUrl);
@@ -5105,15 +5796,17 @@ var providerLabel = (provider) => {
5105
5796
  return "Claude Code";
5106
5797
  case "opencode":
5107
5798
  return "OpenCode";
5799
+ case "grok-bot":
5800
+ return "Grok Bot";
5108
5801
  }
5109
5802
  };
5110
5803
  var capabilities = (providers) => {
5111
5804
  const values2 = providers.map((provider) => ({
5112
5805
  id: provider,
5113
5806
  label: providerLabel(provider),
5114
- detail: `${providerLabel(provider)} executable detected; authentication is checked when work starts`
5807
+ detail: provider === "grok-bot" ? "Named bots through this computer's local Grok Bot gateway" : `${providerLabel(provider)} executable detected; authentication is checked when work starts`
5115
5808
  }));
5116
- if (findExecutable("google-chrome") !== undefined || findExecutable("chromium") !== undefined || platform3() === "darwin") {
5809
+ if (findExecutable("google-chrome") !== undefined || findExecutable("chromium") !== undefined || platform4() === "darwin") {
5117
5810
  values2.push({ id: "browser-session", label: "Signed-in browser" });
5118
5811
  }
5119
5812
  if (findExecutable("xcodebuild") !== undefined) {
@@ -5125,10 +5818,13 @@ var capabilities = (providers) => {
5125
5818
  return values2;
5126
5819
  };
5127
5820
  var selectProviders = (installed, configured) => configured === undefined ? installed : configured.filter((provider) => installed.includes(provider));
5128
- var describeDevice = (configuration, status = "online") => {
5129
- const operatingSystem2 = platform3();
5821
+ var describeDevice = (configuration, status = "online", bots = []) => {
5822
+ const operatingSystem2 = platform4();
5130
5823
  const platformName = operatingSystem2 === "darwin" || operatingSystem2 === "linux" ? operatingSystem2 : operatingSystem2 === "win32" ? "windows" : "unknown";
5131
- const installed = availableProviders();
5824
+ const installed = [
5825
+ ...availableProviders(),
5826
+ ...configuration.grokGateway === undefined ? [] : ["grok-bot"]
5827
+ ];
5132
5828
  const providers = selectProviders(installed, configuration.providers);
5133
5829
  return Device.make({
5134
5830
  id: configuration.id,
@@ -5138,6 +5834,7 @@ var describeDevice = (configuration, status = "online") => {
5138
5834
  architecture: arch(),
5139
5835
  status,
5140
5836
  providers,
5837
+ ...providers.includes("grok-bot") ? { bots } : {},
5141
5838
  capabilities: capabilities(providers),
5142
5839
  workspaces: configuration.workspaces.map((path) => ({
5143
5840
  path,
@@ -5164,9 +5861,9 @@ var allowedWorkspace = async (configuration, requested) => {
5164
5861
  };
5165
5862
  var openAllowedWorkspace = async (configuration, requested) => {
5166
5863
  const path = await allowedWorkspace(configuration, requested);
5167
- const operatingSystem2 = platform3();
5864
+ const operatingSystem2 = platform4();
5168
5865
  const flags = constants3.O_RDONLY | (operatingSystem2 === "win32" ? 0 : constants3.O_DIRECTORY | constants3.O_NOFOLLOW);
5169
- const handle = await open(path, flags);
5866
+ const handle = await open3(path, flags);
5170
5867
  const identity = await handle.stat();
5171
5868
  if (!identity.isDirectory()) {
5172
5869
  await handle.close();
@@ -5249,15 +5946,47 @@ var rememberOperation = (state, operationId) => {
5249
5946
  }
5250
5947
  };
5251
5948
  var sessionKey = (task) => `${task.threadId}:${task.provider}`;
5949
+ var drain = (configuration, state) => {
5950
+ if (state.operation !== undefined) {
5951
+ return;
5952
+ }
5953
+ if (state.tasks.size === 0) {
5954
+ const operation = state.operationQueue.shift();
5955
+ if (operation !== undefined) {
5956
+ executeOperation(configuration, state, operation);
5957
+ return;
5958
+ }
5959
+ }
5960
+ const occupied = new Set([...state.tasks.values()].map(({ task }) => taskSlot(task)));
5961
+ for (let index = 0;index < state.queue.length; ) {
5962
+ const next = state.queue[index];
5963
+ if (next === undefined || occupied.has(taskSlot(next))) {
5964
+ index += 1;
5965
+ continue;
5966
+ }
5967
+ state.queue.splice(index, 1);
5968
+ occupied.add(taskSlot(next));
5969
+ execute(configuration, state, next);
5970
+ }
5971
+ };
5252
5972
  var execute = (configuration, state, task) => {
5253
5973
  if (state.tasks.has(task.id) || state.completed.has(task.id)) {
5254
5974
  return;
5255
5975
  }
5256
5976
  const controller = new AbortController;
5257
- state.tasks.set(task.id, controller);
5977
+ state.tasks.set(task.id, { task, controller });
5258
5978
  send(state, SocketEvent.make({ _tag: "TaskAccepted", taskId: task.id }));
5259
- const workflow = Effect8.gen(function* () {
5260
- const workspace = yield* Effect8.tryPromise({
5979
+ const workflow = Effect10.gen(function* () {
5980
+ if (task.provider === "grok-bot") {
5981
+ return yield* Effect10.tryPromise({
5982
+ try: (signal) => runGrokBot(configuration.grokGateway, task, signal),
5983
+ catch: (cause) => new ProviderRunError({
5984
+ provider: task.provider,
5985
+ message: cause instanceof Error ? cause.message : String(cause)
5986
+ })
5987
+ });
5988
+ }
5989
+ const workspace = yield* Effect10.tryPromise({
5261
5990
  try: () => openAllowedWorkspace(configuration, task.workspace),
5262
5991
  catch: (cause) => new ProviderRunError({
5263
5992
  provider: task.provider,
@@ -5268,17 +5997,18 @@ var execute = (configuration, state, task) => {
5268
5997
  return yield* run({
5269
5998
  provider: task.provider,
5270
5999
  threadId: task.threadId,
6000
+ taskId: task.id,
5271
6001
  prompt: promptFor(task, configuration.name),
5272
6002
  cwd: workspace.cwd,
5273
6003
  beforeSpawn: workspace.validate,
5274
6004
  ...sessionId === undefined ? {} : { sessionId },
5275
6005
  ...configuration.model === undefined ? {} : { model: configuration.model },
5276
6006
  ...configuration.sandbox === undefined ? {} : { sandbox: configuration.sandbox }
5277
- }).pipe(Effect8.ensuring(Effect8.promise(() => workspace.close().catch(() => {
6007
+ }).pipe(Effect10.ensuring(Effect10.promise(() => workspace.close().catch(() => {
5278
6008
  return;
5279
6009
  }))));
5280
6010
  });
5281
- Effect8.runPromise(workflow, { signal: controller.signal }).then((result) => {
6011
+ Effect10.runPromise(workflow, { signal: controller.signal }).then((result) => {
5282
6012
  if (result.sessionId !== undefined) {
5283
6013
  state.sessions.set(sessionKey(task), result.sessionId);
5284
6014
  }
@@ -5298,15 +6028,7 @@ var execute = (configuration, state, task) => {
5298
6028
  }));
5299
6029
  }).finally(() => {
5300
6030
  state.tasks.delete(task.id);
5301
- const operation = state.operationQueue.shift();
5302
- if (operation !== undefined) {
5303
- executeOperation(configuration, state, operation);
5304
- return;
5305
- }
5306
- const next = state.queue.shift();
5307
- if (next !== undefined && state.operation === undefined) {
5308
- execute(configuration, state, next);
5309
- }
6031
+ drain(configuration, state);
5310
6032
  });
5311
6033
  };
5312
6034
  var executeOperation = (configuration, state, operation) => {
@@ -5331,15 +6053,7 @@ var executeOperation = (configuration, state, operation) => {
5331
6053
  }));
5332
6054
  }).finally(() => {
5333
6055
  state.operation = undefined;
5334
- const pending = state.operationQueue.shift();
5335
- if (pending !== undefined) {
5336
- executeOperation(configuration, state, pending);
5337
- return;
5338
- }
5339
- const next = state.queue.shift();
5340
- if (next !== undefined && state.tasks.size === 0) {
5341
- execute(configuration, state, next);
5342
- }
6056
+ drain(configuration, state);
5343
6057
  });
5344
6058
  };
5345
6059
  var scheduleOperation = (configuration, state, operation) => {
@@ -5370,7 +6084,7 @@ var schedule = (configuration, state, task) => {
5370
6084
  if (task.providerSessionId !== undefined) {
5371
6085
  state.sessions.set(sessionKey(task), task.providerSessionId);
5372
6086
  }
5373
- if (state.tasks.size > 0 || state.operation !== undefined) {
6087
+ if ([...state.tasks.values()].some(({ task: active }) => taskSlot(active) === taskSlot(task)) || state.operation !== undefined) {
5374
6088
  if (state.queue.length >= 100) {
5375
6089
  state.socket?.close(4008, "Task queue limit reached");
5376
6090
  return;
@@ -5383,7 +6097,9 @@ var schedule = (configuration, state, task) => {
5383
6097
  var cancel = (state, taskId) => {
5384
6098
  const running = state.tasks.get(taskId);
5385
6099
  if (running !== undefined) {
5386
- running.abort();
6100
+ if (running.task.provider !== "grok-bot") {
6101
+ running.controller.abort();
6102
+ }
5387
6103
  return;
5388
6104
  }
5389
6105
  const index = state.queue.findIndex((task) => task.id === taskId);
@@ -5393,18 +6109,39 @@ var cancel = (state, taskId) => {
5393
6109
  remember(state, taskId);
5394
6110
  sendTerminal(state, taskId, SocketEvent.make({ _tag: "TaskCancelled", taskId }));
5395
6111
  };
5396
- var connect = (configuration, state) => Effect8.tryPromise({
6112
+ var connect = (configuration, state) => Effect10.tryPromise({
5397
6113
  try: (signal) => new Promise((complete) => {
5398
6114
  const socket = new WebSocket2(socketUrl(configuration), {
5399
6115
  maxPayload: maxSocketPayloadBytes,
5400
- perMessageDeflate: false
6116
+ perMessageDeflate: false,
6117
+ allowSynchronousEvents: false
5401
6118
  });
6119
+ let bots = [];
6120
+ let refreshing = false;
6121
+ const refreshBots = async () => {
6122
+ if (refreshing || configuration.grokGateway === undefined || configuration.providers !== undefined && !configuration.providers.includes("grok-bot")) {
6123
+ return;
6124
+ }
6125
+ refreshing = true;
6126
+ bots = await discoverGrokBots(configuration.grokGateway).catch(() => []);
6127
+ refreshing = false;
6128
+ if (!closed) {
6129
+ send(state, SocketEvent.make({
6130
+ _tag: "DeviceHeartbeat",
6131
+ deviceId: configuration.id,
6132
+ status: state.tasks.size > 0 || state.operation !== undefined ? "busy" : "online",
6133
+ bots
6134
+ }));
6135
+ }
6136
+ };
5402
6137
  const heartbeat = setInterval(() => {
5403
6138
  send(state, SocketEvent.make({
5404
6139
  _tag: "DeviceHeartbeat",
5405
6140
  deviceId: configuration.id,
5406
- status: state.tasks.size > 0 || state.operation !== undefined ? "busy" : "online"
6141
+ status: state.tasks.size > 0 || state.operation !== undefined ? "busy" : "online",
6142
+ ...configuration.grokGateway === undefined ? {} : { bots }
5407
6143
  }));
6144
+ refreshBots();
5408
6145
  }, 15000);
5409
6146
  let closed = false;
5410
6147
  let queuedMessages = 0;
@@ -5436,16 +6173,16 @@ var connect = (configuration, state) => Effect8.tryPromise({
5436
6173
  if (closed) {
5437
6174
  return;
5438
6175
  }
5439
- 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");
5440
- const event = await Effect8.runPromise(Effect8.try({
5441
- try: () => JSON.parse(text2),
6176
+ const text3 = 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");
6177
+ const event = await Effect10.runPromise(Effect10.try({
6178
+ try: () => JSON.parse(text3),
5442
6179
  catch: () => new DeviceConnectionError({ message: "Relay sent invalid JSON" })
5443
- }).pipe(Effect8.flatMap(decodeSocketEvent)));
6180
+ }).pipe(Effect10.flatMap(decodeSocketEvent)));
5444
6181
  if (event._tag === "Connected") {
5445
6182
  state.socket = socket;
5446
6183
  socket.send(JSON.stringify(SocketEvent.make({
5447
6184
  _tag: "DeviceHello",
5448
- device: describeDevice(configuration, state.tasks.size > 0 || state.operation !== undefined ? "busy" : "online")
6185
+ device: describeDevice(configuration, state.tasks.size > 0 || state.operation !== undefined ? "busy" : "online", bots)
5449
6186
  })));
5450
6187
  for (const payload of state.terminal.values()) {
5451
6188
  socket.send(payload);
@@ -5453,6 +6190,7 @@ var connect = (configuration, state) => Effect8.tryPromise({
5453
6190
  for (const payload of state.operationTerminal.values()) {
5454
6191
  socket.send(payload);
5455
6192
  }
6193
+ refreshBots();
5456
6194
  return;
5457
6195
  }
5458
6196
  if (event._tag === "TaskAssigned") {
@@ -5465,6 +6203,9 @@ var connect = (configuration, state) => Effect8.tryPromise({
5465
6203
  }
5466
6204
  if (event._tag === "TaskSettled") {
5467
6205
  state.terminal.delete(event.taskId);
6206
+ cleanupBotReply(event.taskId).catch(() => {
6207
+ console.error(`Could not remove local bot reply for settled task ${event.taskId}`);
6208
+ });
5468
6209
  return;
5469
6210
  }
5470
6211
  if (event._tag === "OperationAssigned") {
@@ -5505,9 +6246,9 @@ var runDaemon = (configuration) => {
5505
6246
  operationTerminal: new Map,
5506
6247
  completedOperations: new Set
5507
6248
  };
5508
- return connect(configuration, state).pipe(Effect8.repeat({ schedule: Schedule2.spaced("2 seconds") }), Effect8.ensuring(Effect8.sync(() => {
6249
+ return connect(configuration, state).pipe(Effect10.repeat({ schedule: Schedule3.spaced("2 seconds") }), Effect10.ensuring(Effect10.sync(() => {
5509
6250
  state.socket?.close();
5510
- for (const controller of state.tasks.values()) {
6251
+ for (const { controller } of state.tasks.values()) {
5511
6252
  controller.abort();
5512
6253
  }
5513
6254
  })));
@@ -5517,7 +6258,7 @@ var runDaemon = (configuration) => {
5517
6258
  init_src();
5518
6259
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5519
6260
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5520
- import { Effect as Effect9 } from "effect";
6261
+ import { Effect as Effect11 } from "effect";
5521
6262
  import * as z from "zod/v4";
5522
6263
  var output = (value) => ({
5523
6264
  content: [{ type: "text", text: JSON.stringify(value, null, 2) }]
@@ -5532,41 +6273,58 @@ var runMcp = async (configuration) => {
5532
6273
  title: "List Cohall devices",
5533
6274
  description: "List devices, their availability, local providers, capabilities, and allowed workspaces.",
5534
6275
  inputSchema: {}
5535
- }, async () => output(await Effect9.runPromise(client2.devices())));
6276
+ }, async () => output(await Effect11.runPromise(client2.devices())));
6277
+ server.registerTool("list_bots", {
6278
+ title: "List Cohall bots",
6279
+ description: "List named Grok Bots on paired devices, their host availability, and unambiguous targets for delegate. Bot names can also be used when unique.",
6280
+ inputSchema: {}
6281
+ }, async () => output(listBots(await Effect11.runPromise(client2.devices()))));
5536
6282
  server.registerTool("delegate", {
5537
- title: "Delegate work to another device",
5538
- description: "Send focused work to another Cohall device. When the request depends on the current conversation, distill its motivation, relevant facts, prior findings, constraints, and desired decision into context; Cohall cannot read the host transcript. Never forward unrelated transcript content. Reuse thread_id for related follow-ups.",
6283
+ title: "Send work to a device or bot",
6284
+ description: "Send focused work to a Cohall device or named Grok Bot. When the request depends on the current conversation, distill its motivation, relevant facts, prior findings, constraints, and desired decision into context; Cohall cannot read the host transcript. Never forward unrelated transcript content. Reuse thread_id for related follow-ups. Grok Bots use their existing conversation; a Cohall thread does not isolate it.",
5539
6285
  inputSchema: {
5540
6286
  prompt: z.string().min(1).max(131072),
5541
- target: z.string().optional().describe("Device name, @name, hostname, or ID"),
5542
- provider: z.enum(["codex", "claude-code", "opencode"]).default("codex"),
6287
+ target: z.string().optional().describe("Device name, hostname or ID; @BotName; or @device/BotName. Use list_bots targets to disambiguate."),
6288
+ provider: z.enum(Provider.literals).optional().describe("Inferred as grok-bot for a bot target; otherwise defaults to codex."),
5543
6289
  context: z.string().max(131072).optional().describe("Distilled context the target needs: why the user is asking, relevant facts and prior findings, constraints, and the intended decision. The calling agent must supply this when the prompt depends on its conversation; omit it only for a self-contained task."),
5544
6290
  thread_id: z.string().uuid().optional(),
5545
- workspace: z.string().max(4096).optional(),
6291
+ parent_task_id: z.string().uuid().optional().describe("The task delegating this work. Inherited from COHALL_TASK_ID when present."),
6292
+ workspace: z.string().max(4096).optional().describe("Coding provider workspace. Omit for named Grok Bots."),
5546
6293
  wait: z.boolean().default(true),
5547
6294
  timeout_seconds: z.number().int().min(5).max(86400).default(900)
5548
6295
  }
5549
- }, async ({ prompt, target, provider, context, thread_id, workspace, wait, timeout_seconds }) => {
5550
- const task = await Effect9.runPromise(createDelegation(client2, configuration, {
6296
+ }, async ({
6297
+ prompt,
6298
+ target,
6299
+ provider,
6300
+ context,
6301
+ thread_id,
6302
+ parent_task_id,
6303
+ workspace,
6304
+ wait,
6305
+ timeout_seconds
6306
+ }) => {
6307
+ const task = await Effect11.runPromise(createDelegation(client2, configuration, {
5551
6308
  prompt,
5552
- provider,
6309
+ ...provider === undefined ? {} : { provider },
5553
6310
  ...target === undefined ? {} : { target },
5554
6311
  ...context === undefined ? {} : { context },
5555
6312
  ...thread_id === undefined ? {} : { threadId: ThreadId.make(thread_id) },
6313
+ ...parent_task_id === undefined ? {} : { parentTaskId: TaskId.make(parent_task_id) },
5556
6314
  ...workspace === undefined ? {} : { workspace }
5557
6315
  }));
5558
- return output(taskResult(wait ? await Effect9.runPromise(waitForTask(client2, task, timeout_seconds)) : task));
6316
+ return output(taskResult(wait ? await Effect11.runPromise(waitForTask(client2, task, timeout_seconds)) : task));
5559
6317
  });
5560
6318
  server.registerTool("task_status", {
5561
6319
  title: "Get delegated task status",
5562
6320
  description: "Read the current state and result of a Cohall task.",
5563
6321
  inputSchema: { task_id: z.string().uuid() }
5564
- }, async ({ task_id }) => output(taskResult(await Effect9.runPromise(client2.getTask(TaskId.make(task_id))))));
6322
+ }, async ({ task_id }) => output(taskResult(await Effect11.runPromise(client2.getTask(TaskId.make(task_id))))));
5565
6323
  server.registerTool("task_trace", {
5566
6324
  title: "Trace a delegated task",
5567
6325
  description: "Read the redacted relay and device lifecycle for troubleshooting a Cohall task.",
5568
6326
  inputSchema: { task_id: z.string().uuid() }
5569
- }, async ({ task_id }) => output(await Effect9.runPromise(client2.traceTask(TaskId.make(task_id)))));
6327
+ }, async ({ task_id }) => output(await Effect11.runPromise(client2.traceTask(TaskId.make(task_id)))));
5570
6328
  server.registerTool("wait_task", {
5571
6329
  title: "Wait for delegated work",
5572
6330
  description: "Wait for a Cohall task to finish and return its final result.",
@@ -5575,19 +6333,19 @@ var runMcp = async (configuration) => {
5575
6333
  timeout_seconds: z.number().int().min(5).max(86400).default(900)
5576
6334
  }
5577
6335
  }, async ({ task_id, timeout_seconds }) => {
5578
- const task = await Effect9.runPromise(client2.getTask(TaskId.make(task_id)));
5579
- return output(taskResult(await Effect9.runPromise(waitForTask(client2, task, timeout_seconds))));
6336
+ const task = await Effect11.runPromise(client2.getTask(TaskId.make(task_id)));
6337
+ return output(taskResult(await Effect11.runPromise(waitForTask(client2, task, timeout_seconds))));
5580
6338
  });
5581
6339
  server.registerTool("cancel_task", {
5582
6340
  title: "Cancel delegated work",
5583
- description: "Request cancellation and return its acknowledged or pending state.",
6341
+ description: "Request cancellation of coding work or a queued Bot task. Active Grok Bot tasks must be stopped in Grok Bot.",
5584
6342
  inputSchema: { task_id: z.string().uuid() }
5585
- }, async ({ task_id }) => output(taskResult(await Effect9.runPromise(client2.cancelTask(TaskId.make(task_id))))));
6343
+ }, async ({ task_id }) => output(taskResult(await Effect11.runPromise(client2.cancelTask(TaskId.make(task_id))))));
5586
6344
  server.registerTool("thread_context", {
5587
6345
  title: "Read a Cohall thread",
5588
6346
  description: "Read shared task prompts and final results for a cross-device thread.",
5589
6347
  inputSchema: { thread_id: z.string().uuid() }
5590
- }, async ({ thread_id }) => output(await Effect9.runPromise(threadContext(client2, ThreadId.make(thread_id)))));
6348
+ }, async ({ thread_id }) => output(await Effect11.runPromise(threadContext(client2, ThreadId.make(thread_id)))));
5591
6349
  await server.connect(new StdioServerTransport);
5592
6350
  };
5593
6351
 
@@ -5616,12 +6374,12 @@ var main = async () => {
5616
6374
  return;
5617
6375
  }
5618
6376
  if (command === "device") {
5619
- const configuration = await Effect13.runPromise(loadDeviceConfiguration);
5620
- await Effect13.runPromise(runDaemon(configuration));
6377
+ const configuration = await Effect15.runPromise(loadDeviceConfiguration);
6378
+ await Effect15.runPromise(runDaemon(configuration));
5621
6379
  return;
5622
6380
  }
5623
6381
  if (command === "mcp") {
5624
- const configuration = await Effect13.runPromise(loadClientConfiguration);
6382
+ const configuration = await Effect15.runPromise(loadClientConfiguration);
5625
6383
  await runMcp(configuration);
5626
6384
  return;
5627
6385
  }
@@ -5632,4 +6390,4 @@ await main().catch((cause) => {
5632
6390
  process.exitCode = 1;
5633
6391
  });
5634
6392
 
5635
- //# debugId=28F1D5ED59BE527664756E2164756E21
6393
+ //# debugId=78963F23872A4D6564756E2164756E21