@akshar5/cohall 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cohall.js CHANGED
@@ -17,7 +17,7 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
17
 
18
18
  // packages/protocol/src/index.ts
19
19
  import { Effect, Schema } from "effect";
20
- var version = "0.6.1", maxSocketPayloadBytes, bounded = (maxLength) => Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(maxLength)), optionalText = (maxLength) => Schema.String.check(Schema.isMaxLength(maxLength)), boundedArray = (schema, maxLength) => Schema.Array(schema).check(Schema.isMaxLength(maxLength)), count, uuid = (name) => Schema.String.check(Schema.isUUID(4)).pipe(Schema.brand(name)), isoTimestamp = (name) => Schema.String.check(Schema.isPattern(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/, {
20
+ var version = "0.7.0", maxSocketPayloadBytes, maxAttachmentBytes, maxTaskAttachments = 2, bounded = (maxLength) => Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(maxLength)), optionalText = (maxLength) => Schema.String.check(Schema.isMaxLength(maxLength)), boundedArray = (schema, maxLength) => Schema.Array(schema).check(Schema.isMaxLength(maxLength)), count, uuid = (name) => Schema.String.check(Schema.isUUID(4)).pipe(Schema.brand(name)), isoTimestamp = (name) => Schema.String.check(Schema.isPattern(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/, {
21
21
  expected: "an ISO-8601 UTC timestamp"
22
22
  })).pipe(Schema.brand(name)), BotId, Bot, DeviceId, ThreadId, MessageId, TaskId, AuthSessionId, OperationId, Timestamp, Platform, Provider, DeviceStatus, TaskStatus, OperationStatus, TaskTraceEventKind, MessageRole, ConnectionRole, AuthSession, CreatePairingInput, PairingCredential, ExchangePairingInput, SessionCredential, PairingResult, Capability, Workspace, Device, minimumDeviceOperationVersion = "0.5.0", parsedVersion = (value) => {
23
23
  const match = /^v?(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.exec(value);
@@ -50,18 +50,19 @@ var version = "0.6.1", 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, validBotTarget = (input) => {
53
+ }, Thread, Message, AttachmentName, AttachmentDirection, TaskAttachment, InputAttachment, attachments, validBotTarget = (input) => {
54
54
  const provider = input.provider ?? (input.botId === undefined ? "codex" : "grok-bot");
55
55
  if (provider === "grok-bot" && input.workspace !== undefined) {
56
56
  return "Grok Bots use their own computer context; omit workspace";
57
57
  }
58
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) => ({
59
+ }, Task, TaskInboxItem, TaskInbox, 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
60
  ...task,
61
61
  provider: task.provider ?? (task.botId === undefined ? "codex" : "grok-bot")
62
62
  }))), decodeCreateUpgradeOperationsInput, decodeDevice, decodeSocketEvent;
63
63
  var init_src = __esm(() => {
64
64
  maxSocketPayloadBytes = 1024 * 1024;
65
+ maxAttachmentBytes = 256 * 1024;
65
66
  count = Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER }));
66
67
  BotId = bounded(256).pipe(Schema.brand("BotId"));
67
68
  Bot = Schema.Struct({
@@ -167,6 +168,18 @@ var init_src = __esm(() => {
167
168
  taskId: Schema.optionalKey(TaskId),
168
169
  deviceId: Schema.optionalKey(DeviceId)
169
170
  });
171
+ AttachmentName = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(128), Schema.makeFilter((name) => name !== "." && name !== ".." && !/[<>:"/\\|?*]/.test(name) && !/[. ]$/.test(name) && !/^(?:CON|PRN|AUX|NUL|CONIN\$|CONOUT\$|COM[1-9¹²³]|LPT[1-9¹²³])(?:\.|$)/i.test(name) && new TextEncoder().encode(name).byteLength <= 128 && [...name].every((character) => character.charCodeAt(0) >= 32 && character.charCodeAt(0) !== 127) ? undefined : "Use a file name without path separators or control characters"));
172
+ AttachmentDirection = Schema.Literals(["input", "output"]);
173
+ TaskAttachment = Schema.Struct({
174
+ name: AttachmentName,
175
+ direction: AttachmentDirection,
176
+ bytes: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: maxAttachmentBytes }))
177
+ });
178
+ InputAttachment = Schema.Struct({
179
+ name: AttachmentName,
180
+ data: Schema.String.check(Schema.isMaxLength(Math.ceil(maxAttachmentBytes / 3) * 4), Schema.makeFilter((value) => value.length > 0 && value.length % 4 === 0 && value.length / 4 * 3 - (value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0) <= maxAttachmentBytes && /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value) ? undefined : "Expected nonempty base64 data"))
181
+ });
182
+ attachments = boundedArray(InputAttachment, maxTaskAttachments).check(Schema.makeFilter((files) => new Set(files.map((file) => file.name.normalize("NFC").toLocaleLowerCase("en-US"))).size === files.length ? undefined : "Attachment names must be unique"));
170
183
  Task = Schema.Struct({
171
184
  id: TaskId,
172
185
  threadId: ThreadId,
@@ -181,12 +194,29 @@ var init_src = __esm(() => {
181
194
  workspace: Schema.optionalKey(bounded(4096)),
182
195
  providerSessionId: Schema.optionalKey(bounded(4096)),
183
196
  result: Schema.optionalKey(optionalText(131072)),
197
+ inputAttachmentNames: Schema.optionalKey(boundedArray(AttachmentName, maxTaskAttachments)),
184
198
  error: Schema.optionalKey(optionalText(16384)),
185
199
  createdAt: Timestamp,
186
200
  updatedAt: Timestamp,
187
201
  startedAt: Schema.optionalKey(Timestamp),
188
202
  completedAt: Schema.optionalKey(Timestamp)
189
203
  }).check(Schema.makeFilter(validBotTarget));
204
+ TaskInboxItem = Schema.Struct({
205
+ id: TaskId,
206
+ threadId: ThreadId,
207
+ targetDeviceId: DeviceId,
208
+ provider: Provider,
209
+ botId: Schema.optionalKey(BotId),
210
+ status: Schema.Literals(["completed", "failed", "cancelled"]),
211
+ promptPreview: optionalText(320),
212
+ resultPreview: Schema.optionalKey(optionalText(1024)),
213
+ errorPreview: Schema.optionalKey(optionalText(1024)),
214
+ completedAt: Timestamp
215
+ });
216
+ TaskInbox = Schema.Struct({
217
+ items: boundedArray(TaskInboxItem, 20),
218
+ hasMore: Schema.Boolean
219
+ });
190
220
  upgradeVersion = Schema.String.check(Schema.isPattern(/^(?:latest|v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)$/, {
191
221
  expected: "latest or an exact semantic version"
192
222
  }));
@@ -271,11 +301,16 @@ var init_src = __esm(() => {
271
301
  botId: Schema.optionalKey(BotId),
272
302
  targetDeviceId: Schema.optionalKey(DeviceId),
273
303
  parentTaskId: Schema.optionalKey(TaskId),
274
- workspace: Schema.optionalKey(bounded(4096))
304
+ workspace: Schema.optionalKey(bounded(4096)),
305
+ attachments: Schema.optionalKey(attachments)
275
306
  }).check(Schema.makeFilter(validBotTarget));
276
307
  SocketEvent = Schema.TaggedUnion({
277
308
  Authenticate: { token: bounded(256) },
278
- Connected: { serverVersion: bounded(32), connectedAt: Timestamp },
309
+ Connected: {
310
+ serverVersion: bounded(32),
311
+ connectedAt: Timestamp,
312
+ taskAttachments: Schema.optionalKey(Schema.Boolean)
313
+ },
279
314
  DeviceHello: { device: Device },
280
315
  DeviceHeartbeat: {
281
316
  deviceId: DeviceId,
@@ -287,6 +322,7 @@ var init_src = __esm(() => {
287
322
  TaskFinished: {
288
323
  taskId: TaskId,
289
324
  result: optionalText(131072),
325
+ attachments: Schema.optionalKey(attachments),
290
326
  providerSessionId: Schema.optionalKey(bounded(4096))
291
327
  },
292
328
  TaskFailed: { taskId: TaskId, error: bounded(16384) },
@@ -316,24 +352,24 @@ var init_src = __esm(() => {
316
352
  });
317
353
 
318
354
  // apps/relay/src/config.ts
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";
355
+ import { Effect as Effect13, Schema as Schema14 } from "effect";
356
+ import { access as access5, chmod as chmod7, mkdir as mkdir8, readFile as readFile5, writeFile as writeFile9 } from "node:fs/promises";
321
357
  import { homedir as homedir5, platform as platform5 } from "node:os";
322
- import { join as join8, resolve as resolve4 } from "node:path";
358
+ import { join as join9, resolve as resolve4 } from "node:path";
323
359
  var RelayConfiguration, defaultDataDirectory = () => {
324
360
  if (platform5() === "win32") {
325
- return join8(process.env.LOCALAPPDATA ?? join8(homedir5(), "AppData", "Local"), "Cohall");
361
+ return join9(process.env.LOCALAPPDATA ?? join9(homedir5(), "AppData", "Local"), "Cohall");
326
362
  }
327
363
  if (platform5() === "darwin") {
328
- return join8(homedir5(), "Library", "Application Support", "Cohall", "relay");
364
+ return join9(homedir5(), "Library", "Application Support", "Cohall", "relay");
329
365
  }
330
- return join8(process.env.XDG_DATA_HOME ?? join8(homedir5(), ".local", "share"), "cohall");
366
+ return join9(process.env.XDG_DATA_HOME ?? join9(homedir5(), ".local", "share"), "cohall");
331
367
  }, generatedToken = () => Array.from(crypto.getRandomValues(new Uint8Array(32)), (value) => value.toString(16).padStart(2, "0")).join(""), tokenFrom = async (path) => {
332
368
  if (await access5(path).then(() => true).catch(() => false)) {
333
369
  return (await readFile5(path, "utf8")).trim();
334
370
  }
335
371
  const token = generatedToken();
336
- await writeFile7(path, `${token}
372
+ await writeFile9(path, `${token}
337
373
  `, { mode: 384 });
338
374
  if (platform5() !== "win32") {
339
375
  await chmod7(path, 384);
@@ -350,16 +386,16 @@ var RelayConfiguration, defaultDataDirectory = () => {
350
386
  throw new Error(`${name} must be true or false`);
351
387
  }, isLoopback2 = (host) => host === "127.0.0.1" || host === "localhost" || host === "::1", loadEnvironmentConfiguration;
352
388
  var init_config = __esm(() => {
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 }))
389
+ RelayConfiguration = Schema14.Struct({
390
+ host: Schema14.NonEmptyString,
391
+ port: Schema14.Int.check(Schema14.isBetween({ minimum: 1, maximum: 65535 })),
392
+ token: Schema14.String.check(Schema14.isMinLength(32)),
393
+ dataDirectory: Schema14.NonEmptyString,
394
+ databasePath: Schema14.NonEmptyString,
395
+ allowRemote: Schema14.Boolean,
396
+ historyTaskLimit: Schema14.Int.check(Schema14.isBetween({ minimum: 100, maximum: 1e5 }))
361
397
  });
362
- loadEnvironmentConfiguration = Effect12.tryPromise({
398
+ loadEnvironmentConfiguration = Effect13.tryPromise({
363
399
  try: async () => {
364
400
  const host = process.env.COHALL_RELAY_HOST ?? "127.0.0.1";
365
401
  const port = Number(process.env.COHALL_RELAY_PORT ?? "8787");
@@ -368,21 +404,21 @@ var init_config = __esm(() => {
368
404
  throw new Error("Remote relay binding is disabled. Use a private network or TLS reverse proxy, then set COHALL_RELAY_ALLOW_REMOTE=true.");
369
405
  }
370
406
  const dataDirectory = resolve4(process.env.COHALL_DATA_DIR ?? defaultDataDirectory());
371
- await mkdir7(dataDirectory, { recursive: true, mode: 448 });
407
+ await mkdir8(dataDirectory, { recursive: true, mode: 448 });
372
408
  if (platform5() !== "win32") {
373
409
  await chmod7(dataDirectory, 448);
374
410
  }
375
- const token = process.env.COHALL_TOKEN ?? await tokenFrom(join8(dataDirectory, "owner-token"));
411
+ const token = process.env.COHALL_TOKEN ?? await tokenFrom(join9(dataDirectory, "owner-token"));
376
412
  if (token.length < 32) {
377
413
  throw new Error("The Cohall relay owner token must be at least 32 characters");
378
414
  }
379
415
  const historyTaskLimit = Number(process.env.COHALL_HISTORY_TASK_LIMIT ?? "1000");
380
- return Schema12.decodeUnknownSync(RelayConfiguration)({
416
+ return Schema14.decodeUnknownSync(RelayConfiguration)({
381
417
  host,
382
418
  port,
383
419
  token,
384
420
  dataDirectory,
385
- databasePath: join8(dataDirectory, "cohall.db"),
421
+ databasePath: join9(dataDirectory, "cohall.db"),
386
422
  allowRemote,
387
423
  historyTaskLimit
388
424
  });
@@ -487,13 +523,15 @@ class Hub {
487
523
  hasDevice(deviceId) {
488
524
  return this.#devices.get(deviceId)?.readyState === WebSocket3.OPEN;
489
525
  }
490
- sendToDevice(deviceId, event) {
526
+ sendToDevice(deviceId, event, beforeSend) {
491
527
  const socket = this.#devices.get(deviceId);
492
528
  if (socket?.readyState !== WebSocket3.OPEN || !this.isAuthorized(socket)) {
493
529
  return false;
494
530
  }
531
+ const payload = JSON.stringify(event);
532
+ beforeSend?.();
495
533
  try {
496
- socket.send(JSON.stringify(event));
534
+ socket.send(payload);
497
535
  return true;
498
536
  } catch {
499
537
  return false;
@@ -562,12 +600,12 @@ __export(exports_store, {
562
600
  RelayStore: () => exports_store,
563
601
  PersistenceError: () => PersistenceError
564
602
  });
565
- import { Context as Context2, Effect as Effect13, Layer as Layer2, Schema as Schema13 } from "effect";
603
+ import { Context as Context2, Effect as Effect14, Layer as Layer2, Schema as Schema15 } from "effect";
566
604
  import { createHash as createHash2, randomBytes } from "node:crypto";
567
605
  var PersistenceError, Service2, operationError = (operation) => (cause) => new PersistenceError({
568
606
  operation,
569
607
  message: cause instanceof Error ? cause.message : String(cause)
570
- }), decode2 = (operation, schema, value) => Schema13.decodeUnknownEffect(schema)(value).pipe(Effect13.mapError(operationError(operation))), threadFromRow = (row) => decode2("RelayStore.decodeThread", Thread, {
608
+ }), decode2 = (operation, schema, value) => Schema15.decodeUnknownEffect(schema)(value).pipe(Effect14.mapError(operationError(operation))), threadFromRow = (row) => decode2("RelayStore.decodeThread", Thread, {
571
609
  id: row.id,
572
610
  title: row.title,
573
611
  createdAt: row.created_at,
@@ -596,7 +634,10 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
596
634
  version: row.version,
597
635
  lastSeenAt: row.last_seen_at,
598
636
  ...row.connected_at === null ? {} : { connectedAt: row.connected_at }
599
- }), taskFromRow = (row) => decode2("RelayStore.decodeTask", Task, {
637
+ }), taskFromRow = (db, row) => Effect14.try({
638
+ try: () => db.query("SELECT name FROM task_attachments WHERE task_id = ? AND direction = 'input' ORDER BY name").all(row.id),
639
+ catch: operationError("RelayStore.taskAttachments")
640
+ }).pipe(Effect14.flatMap((attachments2) => decode2("RelayStore.decodeTask", Task, {
600
641
  id: row.id,
601
642
  threadId: row.thread_id,
602
643
  prompt: row.prompt,
@@ -612,9 +653,21 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
612
653
  ...row.workspace === null ? {} : { workspace: row.workspace },
613
654
  ...row.provider_session_id === null ? {} : { providerSessionId: row.provider_session_id },
614
655
  ...row.result === null ? {} : { result: row.result },
656
+ ...attachments2.length === 0 ? {} : { inputAttachmentNames: attachments2.map((item) => item.name) },
615
657
  ...row.error === null ? {} : { error: row.error },
616
658
  ...row.started_at === null ? {} : { startedAt: row.started_at },
617
659
  ...row.completed_at === null ? {} : { completedAt: row.completed_at }
660
+ }))), inboxItemFromRow = (row) => decode2("RelayStore.decodeInboxItem", TaskInboxItem, {
661
+ id: row.id,
662
+ threadId: row.thread_id,
663
+ targetDeviceId: row.target_device_id,
664
+ provider: row.provider,
665
+ ...row.bot_id === null ? {} : { botId: row.bot_id },
666
+ status: row.status,
667
+ promptPreview: row.prompt_preview,
668
+ completedAt: row.completed_at,
669
+ ...row.result_preview === null ? {} : { resultPreview: row.result_preview },
670
+ ...row.error_preview === null ? {} : { errorPreview: row.error_preview }
618
671
  }), operationFromRow = (row) => decode2("RelayStore.decodeOperation", DeviceOperation, {
619
672
  id: row.id,
620
673
  kind: row.kind,
@@ -639,10 +692,10 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
639
692
  kind: row.kind,
640
693
  at: row.created_at,
641
694
  ...row.detail === null ? {} : { detail: row.detail }
642
- }), rolesFromRow = (row) => Schema13.decodeUnknownSync(Schema13.Array(ConnectionRole))(JSON.parse(row.roles_json)), authSessionFromRow = (row) => {
695
+ }), rolesFromRow = (row) => Schema15.decodeUnknownSync(Schema15.Array(ConnectionRole))(JSON.parse(row.roles_json)), authSessionFromRow = (row) => {
643
696
  const roles = rolesFromRow(row);
644
697
  const role = roles[0];
645
- return role === undefined ? Effect13.fail(new PersistenceError({
698
+ return role === undefined ? Effect14.fail(new PersistenceError({
646
699
  operation: "RelayStore.decodeAuthSession",
647
700
  message: `Session ${row.id} has no role`
648
701
  })) : decode2("RelayStore.decodeAuthSession", AuthSession, {
@@ -738,23 +791,82 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
738
791
  )`).run(...parameters);
739
792
  };
740
793
  const queryTask = (taskId) => db.query("SELECT * FROM tasks WHERE id = ?").get(taskId);
794
+ const inboxColumns = `id, thread_id, target_device_id, provider, bot_id, status,
795
+ substr(prompt, 1, 160) AS prompt_preview,
796
+ substr(result, 1, 512) AS result_preview,
797
+ substr(error, 1, 512) AS error_preview,
798
+ COALESCE(completed_at, updated_at) AS completed_at`;
741
799
  const queryOperation = (operationId) => db.query("SELECT * FROM device_operations WHERE id = ?").get(operationId);
742
- const getTask = Effect13.fn("RelayStore.getTask")(function* (taskId) {
743
- const row = yield* Effect13.try({
800
+ const getTask = Effect14.fn("RelayStore.getTask")(function* (taskId) {
801
+ const row = yield* Effect14.try({
744
802
  try: () => queryTask(taskId),
745
803
  catch: operationError("RelayStore.getTask")
746
804
  });
747
805
  if (row === null) {
748
- return yield* Effect13.fail(new PersistenceError({
806
+ return yield* Effect14.fail(new PersistenceError({
749
807
  operation: "RelayStore.getTask",
750
808
  message: `Unknown task ${taskId}`
751
809
  }));
752
810
  }
753
- return yield* taskFromRow(row);
811
+ return yield* taskFromRow(db, row);
812
+ });
813
+ const listAttachments = Effect14.fn("RelayStore.listAttachments")(function* (taskId) {
814
+ yield* getTask(taskId);
815
+ const rows = yield* Effect14.try({
816
+ try: () => db.query("SELECT name, direction, length(data) AS bytes FROM task_attachments WHERE task_id = ? ORDER BY direction, name").all(taskId),
817
+ catch: operationError("RelayStore.listAttachments")
818
+ });
819
+ return yield* Effect14.forEach(rows, (row) => decode2("RelayStore.decodeAttachment", TaskAttachment, row));
820
+ });
821
+ const readAttachment = Effect14.fn("RelayStore.readAttachment")(function* (taskId, name, direction) {
822
+ yield* getTask(taskId);
823
+ const row = yield* Effect14.try({
824
+ try: () => direction === undefined ? db.query("SELECT data FROM task_attachments WHERE task_id = ? AND name = ? ORDER BY direction DESC LIMIT 1").get(taskId, name) : db.query("SELECT data FROM task_attachments WHERE task_id = ? AND name = ? AND direction = ?").get(taskId, name, direction),
825
+ catch: operationError("RelayStore.readAttachment")
826
+ });
827
+ if (row === null) {
828
+ return yield* new PersistenceError({
829
+ operation: "RelayStore.readAttachment",
830
+ message: `Unknown attachment ${name}`
831
+ });
832
+ }
833
+ return row.data;
834
+ });
835
+ const inboxFor = Effect14.fn("RelayStore.inboxFor")(function* (principal) {
836
+ const requesterId = principal === "owner" ? "owner" : principal.id;
837
+ const rows = yield* Effect14.try({
838
+ try: () => db.query(`SELECT ${inboxColumns} FROM tasks WHERE requester_id = ? AND completion_seen_at IS NULL
839
+ AND status IN ('completed', 'failed', 'cancelled')
840
+ ORDER BY completed_at ASC, id ASC LIMIT 21`).all(requesterId),
841
+ catch: operationError("RelayStore.inboxFor")
842
+ });
843
+ return TaskInbox.make({
844
+ items: yield* Effect14.forEach(rows.slice(0, 20), inboxItemFromRow),
845
+ hasMore: rows.length > 20
846
+ });
847
+ });
848
+ const acknowledgeCompletion = Effect14.fn("RelayStore.acknowledgeCompletion")(function* (taskId, principal) {
849
+ const requesterId = principal === "owner" ? "owner" : principal.id;
850
+ const row = yield* Effect14.try({
851
+ try: () => db.query(`SELECT ${inboxColumns} FROM tasks WHERE id = ? AND requester_id = ?
852
+ AND status IN ('completed', 'failed', 'cancelled')`).get(taskId, requesterId),
853
+ catch: operationError("RelayStore.acknowledgeCompletion.read")
854
+ });
855
+ if (row === null) {
856
+ return yield* new PersistenceError({
857
+ operation: "RelayStore.acknowledgeCompletion",
858
+ message: `Unknown inbox task ${taskId}`
859
+ });
860
+ }
861
+ yield* Effect14.try({
862
+ try: () => db.query("UPDATE tasks SET completion_seen_at = COALESCE(completion_seen_at, ?) WHERE id = ?").run(now(), taskId),
863
+ catch: operationError("RelayStore.acknowledgeCompletion.write")
864
+ });
865
+ return yield* inboxItemFromRow(row);
754
866
  });
755
- const traceTask = Effect13.fn("RelayStore.traceTask")(function* (taskId) {
867
+ const traceTask = Effect14.fn("RelayStore.traceTask")(function* (taskId) {
756
868
  const task = yield* getTask(taskId);
757
- const [deviceRow, eventRows] = yield* Effect13.try({
869
+ const [deviceRow, eventRows] = yield* Effect14.try({
758
870
  try: () => [
759
871
  db.query("SELECT * FROM devices WHERE id = ?").get(task.targetDeviceId),
760
872
  db.query(`SELECT * FROM (
@@ -765,14 +877,14 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
765
877
  catch: operationError("RelayStore.traceTask")
766
878
  });
767
879
  if (deviceRow === null) {
768
- return yield* Effect13.fail(new PersistenceError({
880
+ return yield* Effect14.fail(new PersistenceError({
769
881
  operation: "RelayStore.traceTask.device",
770
882
  message: `Unknown target device ${task.targetDeviceId}`
771
883
  }));
772
884
  }
773
885
  const targetDevice = yield* deviceFromRow(deviceRow);
774
886
  const selectedRows = eventRows.slice(-100);
775
- const recordedEvents = yield* Effect13.forEach(selectedRows, taskTraceEventFromRow);
887
+ const recordedEvents = yield* Effect14.forEach(selectedRows, taskTraceEventFromRow);
776
888
  const events = recordedEvents.length === 0 ? derivedTraceEvents(task) : recordedEvents;
777
889
  return TaskTrace.make({
778
890
  taskId: task.id,
@@ -793,15 +905,15 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
793
905
  ...task.error === undefined ? {} : { error: task.error }
794
906
  });
795
907
  });
796
- const listDevices = Effect13.fn("RelayStore.listDevices")(function* () {
797
- const rows = yield* Effect13.try({
908
+ const listDevices = Effect14.fn("RelayStore.listDevices")(function* () {
909
+ const rows = yield* Effect14.try({
798
910
  try: () => db.query("SELECT * FROM devices WHERE forgotten_at IS NULL ORDER BY name COLLATE NOCASE, id").all(),
799
911
  catch: operationError("RelayStore.listDevices")
800
912
  });
801
- return yield* Effect13.forEach(rows, deviceFromRow);
913
+ return yield* Effect14.forEach(rows, deviceFromRow);
802
914
  });
803
- const usage = Effect13.fn("RelayStore.usage")(function* () {
804
- const rows = yield* Effect13.try({
915
+ const usage = Effect14.fn("RelayStore.usage")(function* () {
916
+ const rows = yield* Effect14.try({
805
917
  try: () => db.query(`SELECT tasks.target_device_id AS device_id, devices.name AS device_name,
806
918
  tasks.status, tasks.provider, COUNT(*) AS tasks
807
919
  FROM tasks
@@ -815,9 +927,9 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
815
927
  const devices = new Map;
816
928
  let retainedTasks = 0;
817
929
  for (const row of rows) {
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);
930
+ const status = Schema15.decodeUnknownSync(TaskStatus)(row.status);
931
+ const provider = Schema15.decodeUnknownSync(Provider)(row.provider);
932
+ const deviceId = Schema15.decodeUnknownSync(DeviceId)(row.device_id);
821
933
  retainedTasks += row.tasks;
822
934
  totalStatus[status] += row.tasks;
823
935
  totalProviders.set(provider, (totalProviders.get(provider) ?? 0) + row.tasks);
@@ -847,29 +959,29 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
847
959
  }))
848
960
  });
849
961
  });
850
- const getOperation = Effect13.fn("RelayStore.getOperation")(function* (operationId) {
851
- const row = yield* Effect13.try({
962
+ const getOperation = Effect14.fn("RelayStore.getOperation")(function* (operationId) {
963
+ const row = yield* Effect14.try({
852
964
  try: () => queryOperation(operationId),
853
965
  catch: operationError("RelayStore.getOperation")
854
966
  });
855
967
  if (row === null) {
856
- return yield* Effect13.fail(new PersistenceError({
968
+ return yield* Effect14.fail(new PersistenceError({
857
969
  operation: "RelayStore.getOperation",
858
970
  message: `Unknown device operation ${operationId}`
859
971
  }));
860
972
  }
861
973
  return yield* operationFromRow(row);
862
974
  });
863
- const listOperations = Effect13.fn("RelayStore.listOperations")(function* () {
864
- const rows = yield* Effect13.try({
975
+ const listOperations = Effect14.fn("RelayStore.listOperations")(function* () {
976
+ const rows = yield* Effect14.try({
865
977
  try: () => db.query("SELECT * FROM device_operations ORDER BY created_at DESC, id DESC LIMIT 50").all(),
866
978
  catch: operationError("RelayStore.listOperations")
867
979
  });
868
- return yield* Effect13.forEach(rows, operationFromRow);
980
+ return yield* Effect14.forEach(rows, operationFromRow);
869
981
  });
870
- const createUpgradeOperations = Effect13.fn("RelayStore.createUpgradeOperations")(function* (input) {
982
+ const createUpgradeOperations = Effect14.fn("RelayStore.createUpgradeOperations")(function* (input) {
871
983
  const timestamp = now();
872
- const operations = yield* Effect13.try({
984
+ const operations = yield* Effect14.try({
873
985
  try: () => db.transaction(() => {
874
986
  const devices = db.query("SELECT * FROM devices WHERE forgotten_at IS NULL ORDER BY name COLLATE NOCASE, id").all();
875
987
  if (devices.length === 0) {
@@ -909,15 +1021,15 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
909
1021
  });
910
1022
  return operations;
911
1023
  });
912
- const pendingOperationsFor = Effect13.fn("RelayStore.pendingOperationsFor")(function* (deviceId) {
913
- const rows = yield* Effect13.try({
1024
+ const pendingOperationsFor = Effect14.fn("RelayStore.pendingOperationsFor")(function* (deviceId) {
1025
+ const rows = yield* Effect14.try({
914
1026
  try: () => db.query(`SELECT * FROM device_operations WHERE target_device_id = ? AND status = 'queued'
915
1027
  ORDER BY created_at, id LIMIT 10`).all(deviceId),
916
1028
  catch: operationError("RelayStore.pendingOperationsFor")
917
1029
  });
918
- return yield* Effect13.forEach(rows, operationFromRow);
1030
+ return yield* Effect14.forEach(rows, operationFromRow);
919
1031
  });
920
- const transitionOperation = Effect13.fn("RelayStore.transitionOperation")(function* (operationId, expected, status, result, error) {
1032
+ const transitionOperation = Effect14.fn("RelayStore.transitionOperation")(function* (operationId, expected, status, result, error) {
921
1033
  const current = yield* getOperation(operationId);
922
1034
  if (!expected.includes(current.status)) {
923
1035
  return current;
@@ -925,7 +1037,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
925
1037
  const timestamp = now();
926
1038
  const completed = status === "completed" || status === "failed" ? timestamp : undefined;
927
1039
  const placeholders = expected.map(() => "?").join(", ");
928
- yield* Effect13.try({
1040
+ yield* Effect14.try({
929
1041
  try: () => db.transaction(() => {
930
1042
  const updated = db.query(`UPDATE device_operations
931
1043
  SET status = ?, result = ?, error = ?, updated_at = ?, completed_at = ?
@@ -938,23 +1050,23 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
938
1050
  });
939
1051
  return yield* getOperation(operationId);
940
1052
  });
941
- const requireOperationTarget = Effect13.fn("RelayStore.requireOperationTarget")(function* (operationId, deviceId) {
1053
+ const requireOperationTarget = Effect14.fn("RelayStore.requireOperationTarget")(function* (operationId, deviceId) {
942
1054
  const operation = yield* getOperation(operationId);
943
1055
  if (operation.targetDeviceId !== deviceId) {
944
- return yield* Effect13.fail(new PersistenceError({
1056
+ return yield* Effect14.fail(new PersistenceError({
945
1057
  operation: "RelayStore.requireOperationTarget",
946
1058
  message: `Device ${deviceId} cannot update operation ${operationId}`
947
1059
  }));
948
1060
  }
949
1061
  return operation;
950
1062
  });
951
- const assignOperation = Effect13.fn("RelayStore.assignOperation")(function* (operationId) {
1063
+ const assignOperation = Effect14.fn("RelayStore.assignOperation")(function* (operationId) {
952
1064
  const current = yield* getOperation(operationId);
953
1065
  if (current.status !== "queued") {
954
1066
  return current;
955
1067
  }
956
1068
  const timestamp = now();
957
- yield* Effect13.try({
1069
+ yield* Effect14.try({
958
1070
  try: () => db.query(`UPDATE device_operations SET status = 'assigned', updated_at = ?
959
1071
  WHERE id = ? AND status = 'queued'
960
1072
  AND NOT EXISTS (
@@ -970,19 +1082,19 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
970
1082
  });
971
1083
  return yield* getOperation(operationId);
972
1084
  });
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)));
1085
+ const acceptOperation = (operationId, deviceId) => requireOperationTarget(operationId, deviceId).pipe(Effect14.flatMap(() => transitionOperation(operationId, ["assigned"], "running")));
1086
+ const finishOperation = (operationId, deviceId, result) => requireOperationTarget(operationId, deviceId).pipe(Effect14.flatMap(() => transitionOperation(operationId, ["queued", "assigned", "running"], "completed", result)));
1087
+ const failOperation = (operationId, deviceId, error) => requireOperationTarget(operationId, deviceId).pipe(Effect14.flatMap(() => transitionOperation(operationId, ["queued", "assigned", "running"], "failed", undefined, error)));
976
1088
  const abandonOperation = (operationId) => transitionOperation(operationId, ["queued", "assigned", "running"], "failed", undefined, "Abandoned by the relay owner");
977
- const requeueOperationsFor = Effect13.fn("RelayStore.requeueOperationsFor")(function* (deviceId) {
978
- yield* Effect13.try({
1089
+ const requeueOperationsFor = Effect14.fn("RelayStore.requeueOperationsFor")(function* (deviceId) {
1090
+ yield* Effect14.try({
979
1091
  try: () => db.query(`UPDATE device_operations SET status = 'queued', updated_at = ?
980
1092
  WHERE target_device_id = ? AND status IN ('assigned', 'running')`).run(now(), deviceId),
981
1093
  catch: operationError("RelayStore.requeueOperationsFor")
982
1094
  });
983
1095
  });
984
- const forgetDevice = Effect13.fn("RelayStore.forgetDevice")(function* (deviceId) {
985
- const row = yield* Effect13.try({
1096
+ const forgetDevice = Effect14.fn("RelayStore.forgetDevice")(function* (deviceId) {
1097
+ const row = yield* Effect14.try({
986
1098
  try: () => db.transaction(() => {
987
1099
  const device = db.query("SELECT * FROM devices WHERE id = ? AND forgotten_at IS NULL").get(deviceId);
988
1100
  if (device === null) {
@@ -1009,8 +1121,8 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1009
1121
  });
1010
1122
  return yield* deviceFromRow(row);
1011
1123
  });
1012
- const recover = Effect13.fn("RelayStore.recover")(function* () {
1013
- yield* Effect13.try({
1124
+ const recover = Effect14.fn("RelayStore.recover")(function* () {
1125
+ yield* Effect14.try({
1014
1126
  try: () => db.transaction(() => {
1015
1127
  const timestamp = now();
1016
1128
  const interrupted = db.query("SELECT id FROM tasks WHERE status IN ('assigned', 'running')").all();
@@ -1028,8 +1140,8 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1028
1140
  catch: operationError("RelayStore.recover")
1029
1141
  });
1030
1142
  });
1031
- const upsertDevice = Effect13.fn("RelayStore.upsertDevice")(function* (device) {
1032
- yield* Effect13.try({
1143
+ const upsertDevice = Effect14.fn("RelayStore.upsertDevice")(function* (device) {
1144
+ yield* Effect14.try({
1033
1145
  try: () => db.query(`INSERT INTO devices (
1034
1146
  id, name, hostname, platform, architecture, status, providers_json,
1035
1147
  capabilities_json, workspaces_json, version, last_seen_at, connected_at, bots_json
@@ -1046,28 +1158,34 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1046
1158
  });
1047
1159
  return device;
1048
1160
  });
1049
- const updateDeviceStatus = Effect13.fn("RelayStore.updateDeviceStatus")(function* (deviceId, status, bots) {
1161
+ const updateDeviceStatus = Effect14.fn("RelayStore.updateDeviceStatus")(function* (deviceId, status, bots) {
1050
1162
  const timestamp = now();
1051
- const result = yield* Effect13.try({
1163
+ const result = yield* Effect14.try({
1052
1164
  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),
1053
1165
  catch: operationError("RelayStore.updateDeviceStatus")
1054
1166
  });
1055
1167
  if (result.changes !== 1) {
1056
- return yield* Effect13.fail(new PersistenceError({
1168
+ return yield* Effect14.fail(new PersistenceError({
1057
1169
  operation: "RelayStore.updateDeviceStatus",
1058
1170
  message: `Unknown device ${deviceId}`
1059
1171
  }));
1060
1172
  }
1061
1173
  const row = db.query("SELECT * FROM devices WHERE id = ?").get(deviceId);
1062
1174
  if (row === null) {
1063
- return yield* Effect13.fail(new PersistenceError({
1175
+ return yield* Effect14.fail(new PersistenceError({
1064
1176
  operation: "RelayStore.updateDeviceStatus.read",
1065
1177
  message: `Device ${deviceId} disappeared after update`
1066
1178
  }));
1067
1179
  }
1068
1180
  return yield* deviceFromRow(row);
1069
1181
  });
1070
- const createDelegation2 = Effect13.fn("RelayStore.createDelegation")(function* (input, targetDeviceId, sourceDeviceId, providerSessionId) {
1182
+ const createDelegation2 = Effect14.fn("RelayStore.createDelegation")(function* (input, targetDeviceId, principal, providerSessionId) {
1183
+ const requesterId = principal === "owner" ? "owner" : principal.id;
1184
+ const boundDeviceId = principal === "owner" ? undefined : principal.deviceId;
1185
+ const sourceDeviceId = yield* Effect14.try({
1186
+ try: () => boundDeviceId === undefined || db.query("SELECT id FROM devices WHERE id = ? AND forgotten_at IS NULL").get(boundDeviceId) === null ? undefined : boundDeviceId,
1187
+ catch: operationError("RelayStore.createDelegation.source")
1188
+ });
1071
1189
  const timestamp = now();
1072
1190
  const threadId = input.threadId ?? makeThreadId();
1073
1191
  const task = Task.make({
@@ -1084,6 +1202,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1084
1202
  ...sourceDeviceId === undefined ? {} : { sourceDeviceId },
1085
1203
  ...input.parentTaskId === undefined ? {} : { parentTaskId: input.parentTaskId },
1086
1204
  ...input.workspace === undefined ? {} : { workspace: input.workspace },
1205
+ ...input.attachments === undefined || input.attachments.length === 0 ? {} : { inputAttachmentNames: input.attachments.map((attachment) => attachment.name) },
1087
1206
  ...providerSessionId === undefined ? {} : { providerSessionId }
1088
1207
  });
1089
1208
  const thread = input.threadId === undefined ? Thread.make({
@@ -1102,7 +1221,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1102
1221
  taskId: task.id,
1103
1222
  ...sourceDeviceId === undefined ? {} : { deviceId: sourceDeviceId }
1104
1223
  });
1105
- yield* Effect13.try({
1224
+ yield* Effect14.try({
1106
1225
  try: () => db.transaction(() => {
1107
1226
  const outstanding = db.query(`SELECT COUNT(*) AS count FROM tasks WHERE target_device_id = ?
1108
1227
  AND status IN ('queued', 'assigned', 'running', 'cancelling')`).get(targetDeviceId);
@@ -1116,9 +1235,16 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1116
1235
  }
1117
1236
  db.query(`INSERT INTO tasks (
1118
1237
  id, thread_id, prompt, context, provider, status, source_device_id,
1119
- target_device_id, parent_task_id, workspace, provider_session_id, bot_id,
1120
- created_at, updated_at
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);
1238
+ requester_id, target_device_id, parent_task_id, workspace,
1239
+ provider_session_id, bot_id, created_at, updated_at
1240
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(task.id, task.threadId, task.prompt, task.context ?? null, task.provider, task.status, task.sourceDeviceId ?? null, requesterId, task.targetDeviceId, task.parentTaskId ?? null, task.workspace ?? null, task.providerSessionId ?? null, task.botId ?? null, task.createdAt, task.updatedAt);
1241
+ for (const attachment of input.attachments ?? []) {
1242
+ const data = Buffer.from(attachment.data, "base64");
1243
+ if (data.length < 1 || data.length > maxAttachmentBytes) {
1244
+ throw new Error(`Attachment ${attachment.name} must be 1 to ${maxAttachmentBytes} bytes`);
1245
+ }
1246
+ db.query("INSERT INTO task_attachments (task_id, name, direction, data) VALUES (?, ?, 'input', ?)").run(task.id, attachment.name, data);
1247
+ }
1122
1248
  recordTaskTraceEvent(task.id, "queued", "Relay accepted the task", timestamp);
1123
1249
  db.query(`INSERT INTO messages (
1124
1250
  id, thread_id, role, kind, author_id, author_name, content, created_at,
@@ -1130,18 +1256,18 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1130
1256
  });
1131
1257
  return task;
1132
1258
  });
1133
- const threadContext2 = Effect13.fn("RelayStore.threadContext")(function* (threadId) {
1134
- const threadRow = yield* Effect13.try({
1259
+ const threadContext2 = Effect14.fn("RelayStore.threadContext")(function* (threadId) {
1260
+ const threadRow = yield* Effect14.try({
1135
1261
  try: () => db.query("SELECT * FROM threads WHERE id = ?").get(threadId),
1136
1262
  catch: operationError("RelayStore.threadContext.thread")
1137
1263
  });
1138
1264
  if (threadRow === null) {
1139
- return yield* Effect13.fail(new PersistenceError({
1265
+ return yield* Effect14.fail(new PersistenceError({
1140
1266
  operation: "RelayStore.threadContext",
1141
1267
  message: `Unknown thread ${threadId}`
1142
1268
  }));
1143
1269
  }
1144
- const [messageRows, taskRows] = yield* Effect13.try({
1270
+ const [messageRows, taskRows] = yield* Effect14.try({
1145
1271
  try: () => [
1146
1272
  db.query(`SELECT * FROM (
1147
1273
  SELECT * FROM messages WHERE thread_id = ?
@@ -1154,10 +1280,10 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1154
1280
  ],
1155
1281
  catch: operationError("RelayStore.threadContext.items")
1156
1282
  });
1157
- const [thread, messages, tasks] = yield* Effect13.all([
1283
+ const [thread, messages, tasks] = yield* Effect14.all([
1158
1284
  threadFromRow(threadRow),
1159
- Effect13.forEach(messageRows.slice(-20), messageFromRow),
1160
- Effect13.forEach(taskRows.slice(-20), taskFromRow)
1285
+ Effect14.forEach(messageRows.slice(-20), messageFromRow),
1286
+ Effect14.forEach(taskRows.slice(-20), (row) => taskFromRow(db, row))
1161
1287
  ]);
1162
1288
  const recentMessages = recentWithin(messages, 256 * 1024);
1163
1289
  const recentTasks = recentWithin(tasks, 768 * 1024);
@@ -1168,22 +1294,22 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1168
1294
  truncated: messageRows.length > 20 || taskRows.length > 20 || recentMessages.truncated || recentTasks.truncated
1169
1295
  });
1170
1296
  });
1171
- const pendingTasksFor = Effect13.fn("RelayStore.pendingTasksFor")(function* (deviceId) {
1172
- const rows = yield* Effect13.try({
1297
+ const pendingTasksFor = Effect14.fn("RelayStore.pendingTasksFor")(function* (deviceId) {
1298
+ const rows = yield* Effect14.try({
1173
1299
  try: () => db.query(`SELECT * FROM tasks WHERE target_device_id = ?
1174
1300
  AND status IN ('queued', 'cancelling') ORDER BY created_at, id LIMIT 100`).all(deviceId),
1175
1301
  catch: operationError("RelayStore.pendingTasksFor")
1176
1302
  });
1177
- return yield* Effect13.forEach(rows, taskFromRow);
1303
+ return yield* Effect14.forEach(rows, (row) => taskFromRow(db, row));
1178
1304
  });
1179
- const transition = Effect13.fn("RelayStore.transition")(function* (taskId, expected, update, event) {
1305
+ const transition = Effect14.fn("RelayStore.transition")(function* (taskId, expected, update, event) {
1180
1306
  const current = yield* getTask(taskId);
1181
1307
  if (!expected.includes(current.status)) {
1182
1308
  return current;
1183
1309
  }
1184
1310
  const placeholders = expected.map(() => "?").join(", ");
1185
1311
  const timestamp = now();
1186
- yield* Effect13.try({
1312
+ yield* Effect14.try({
1187
1313
  try: () => db.transaction(() => {
1188
1314
  const result = db.query(`UPDATE tasks SET status = ?, provider_session_id = ?, result = ?, error = ?,
1189
1315
  started_at = ?, completed_at = ?, updated_at = ?
@@ -1199,15 +1325,23 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1199
1325
  });
1200
1326
  return yield* getTask(taskId);
1201
1327
  });
1202
- const assignTask = Effect13.fn("RelayStore.assignTask")(function* (taskId) {
1328
+ const assignTask = Effect14.fn("RelayStore.assignTask")(function* (taskId) {
1203
1329
  const current = yield* getTask(taskId);
1204
1330
  if (current.status !== "queued") {
1205
1331
  return current;
1206
1332
  }
1207
1333
  const timestamp = now();
1208
- yield* Effect13.try({
1334
+ yield* Effect14.try({
1209
1335
  try: () => db.transaction(() => {
1210
- const result = db.query(`UPDATE tasks SET status = 'assigned', updated_at = ?
1336
+ const result = db.query(`UPDATE tasks SET status = 'assigned', updated_at = ?,
1337
+ provider_session_id = CASE
1338
+ WHEN provider = 'grok-bot' THEN NULL
1339
+ WHEN started_at IS NULL THEN COALESCE((
1340
+ SELECT session_id FROM provider_sessions
1341
+ WHERE thread_id = tasks.thread_id AND device_id = tasks.target_device_id
1342
+ AND provider = tasks.provider
1343
+ ), provider_session_id)
1344
+ ELSE provider_session_id END
1211
1345
  WHERE id = ? AND status = 'queued' AND NOT EXISTS (
1212
1346
  SELECT 1 FROM tasks active
1213
1347
  WHERE active.target_device_id = ? AND active.id <> ?
@@ -1227,23 +1361,23 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1227
1361
  });
1228
1362
  return yield* getTask(taskId);
1229
1363
  });
1230
- const requireTarget = Effect13.fn("RelayStore.requireTarget")(function* (taskId, deviceId) {
1364
+ const requireTarget = Effect14.fn("RelayStore.requireTarget")(function* (taskId, deviceId) {
1231
1365
  const task = yield* getTask(taskId);
1232
1366
  if (task.targetDeviceId !== deviceId) {
1233
- return yield* Effect13.fail(new PersistenceError({
1367
+ return yield* Effect14.fail(new PersistenceError({
1234
1368
  operation: "RelayStore.requireTarget",
1235
1369
  message: `Task ${taskId} belongs to another device`
1236
1370
  }));
1237
1371
  }
1238
1372
  return task;
1239
1373
  });
1240
- const terminal = Effect13.fn("RelayStore.terminal")(function* (taskId, deviceId, status, result, error, providerSessionId) {
1374
+ const terminal = Effect14.fn("RelayStore.terminal")(function* (taskId, deviceId, status, result, error, providerSessionId, attachments2) {
1241
1375
  const current = yield* requireTarget(taskId, deviceId);
1242
1376
  if (["completed", "failed", "cancelled"].includes(current.status)) {
1243
1377
  return current;
1244
1378
  }
1245
1379
  const timestamp = now();
1246
- yield* Effect13.try({
1380
+ yield* Effect14.try({
1247
1381
  try: () => db.transaction(() => {
1248
1382
  const updated = db.query(`UPDATE tasks SET status = ?, result = ?, error = ?, provider_session_id = ?,
1249
1383
  completed_at = ?, updated_at = ? WHERE id = ?
@@ -1251,6 +1385,19 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1251
1385
  if (updated.changes !== 1) {
1252
1386
  return;
1253
1387
  }
1388
+ db.query("DELETE FROM task_attachments WHERE task_id = ? AND direction = 'output'").run(taskId);
1389
+ if (status === "completed") {
1390
+ if ((attachments2?.length ?? 0) > maxTaskAttachments) {
1391
+ throw new Error(`A task supports at most ${maxTaskAttachments} output files`);
1392
+ }
1393
+ for (const attachment of attachments2 ?? []) {
1394
+ const data = Buffer.from(attachment.data, "base64");
1395
+ if (data.length < 1 || data.length > maxAttachmentBytes) {
1396
+ throw new Error(`Attachment ${attachment.name} must be 1 to ${maxAttachmentBytes} bytes`);
1397
+ }
1398
+ db.query("INSERT INTO task_attachments (task_id, name, direction, data) VALUES (?, ?, 'output', ?)").run(taskId, attachment.name, data);
1399
+ }
1400
+ }
1254
1401
  recordTaskTraceEvent(taskId, status, status === "completed" ? "Target device completed the task" : status === "failed" ? "Target device reported a task failure" : "Target device acknowledged cancellation", timestamp);
1255
1402
  if (current.provider !== "grok-bot" && providerSessionId !== undefined) {
1256
1403
  db.query(`INSERT INTO provider_sessions (thread_id, device_id, provider, session_id, updated_at)
@@ -1273,12 +1420,16 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1273
1420
  });
1274
1421
  return yield* getTask(taskId);
1275
1422
  });
1276
- const requestCancellation = Effect13.fn("RelayStore.requestCancellation")(function* (taskId) {
1423
+ const requestCancellation = Effect14.fn("RelayStore.requestCancellation")(function* (taskId) {
1277
1424
  const current = yield* getTask(taskId);
1278
1425
  if (["completed", "failed", "cancelled"].includes(current.status)) {
1279
1426
  return current;
1280
1427
  }
1281
- if (current.provider === "grok-bot" && current.status !== "queued") {
1428
+ const dispatched = current.provider === "grok-bot" && (yield* Effect14.try({
1429
+ try: () => db.query("SELECT 1 FROM tasks WHERE id = ? AND dispatched_at IS NOT NULL").get(taskId) !== null,
1430
+ catch: operationError("RelayStore.requestCancellation")
1431
+ }));
1432
+ if (current.provider === "grok-bot" && (current.status !== "queued" || current.startedAt !== undefined || dispatched)) {
1282
1433
  return yield* new PersistenceError({
1283
1434
  operation: "RelayStore.requestCancellation",
1284
1435
  message: "Stop this bot in Grok Bot; its gateway cannot cancel an individual Cohall request"
@@ -1292,9 +1443,9 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1292
1443
  status: "cancelling"
1293
1444
  }, { kind: "cancelling", detail: "Client requested task cancellation" });
1294
1445
  });
1295
- const requeueTasksFor = Effect13.fn("RelayStore.requeueTasksFor")(function* (deviceId) {
1446
+ const requeueTasksFor = Effect14.fn("RelayStore.requeueTasksFor")(function* (deviceId) {
1296
1447
  const timestamp = now();
1297
- yield* Effect13.try({
1448
+ yield* Effect14.try({
1298
1449
  try: () => db.transaction(() => {
1299
1450
  const interrupted = db.query(`SELECT id FROM tasks WHERE target_device_id = ?
1300
1451
  AND status IN ('assigned', 'running')`).all(deviceId);
@@ -1307,42 +1458,42 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1307
1458
  catch: operationError("RelayStore.requeueTasksFor")
1308
1459
  });
1309
1460
  });
1310
- const sessionFor = Effect13.fn("RelayStore.sessionFor")(function* (threadId, deviceId, provider) {
1311
- const row = yield* Effect13.try({
1461
+ const sessionFor = Effect14.fn("RelayStore.sessionFor")(function* (threadId, deviceId, provider) {
1462
+ const row = yield* Effect14.try({
1312
1463
  try: () => db.query(`SELECT session_id FROM provider_sessions
1313
1464
  WHERE thread_id = ? AND device_id = ? AND provider = ?`).get(threadId, deviceId, provider),
1314
1465
  catch: operationError("RelayStore.sessionFor")
1315
1466
  });
1316
1467
  return row?.session_id;
1317
1468
  });
1318
- const createPairing = Effect13.fn("RelayStore.createPairing")(function* (input) {
1469
+ const createPairing = Effect14.fn("RelayStore.createPairing")(function* (input) {
1319
1470
  const roles = [...new Set(input.roles)];
1320
1471
  if (roles.length === 0) {
1321
- return yield* Effect13.fail(new PersistenceError({
1472
+ return yield* Effect14.fail(new PersistenceError({
1322
1473
  operation: "RelayStore.createPairing",
1323
1474
  message: "A pairing must allow at least one role"
1324
1475
  }));
1325
1476
  }
1326
1477
  const token = secret("pair");
1327
1478
  const expiresAt = new Date(Date.now() + 10 * 60 * 1000).toISOString();
1328
- yield* Effect13.try({
1479
+ yield* Effect14.try({
1329
1480
  try: () => db.query(`INSERT INTO auth_pairings (token_hash, label, roles_json, expires_at, created_at)
1330
1481
  VALUES (?, ?, ?, ?, ?)`).run(tokenHash(token), input.label, JSON.stringify(roles), expiresAt, now()),
1331
1482
  catch: operationError("RelayStore.createPairing")
1332
1483
  });
1333
1484
  return PairingCredential.make({ token, expiresAt: Timestamp.make(expiresAt) });
1334
1485
  });
1335
- const exchangePairing2 = Effect13.fn("RelayStore.exchangePairing")(function* (token) {
1486
+ const exchangePairing2 = Effect14.fn("RelayStore.exchangePairing")(function* (token) {
1336
1487
  const timestamp = now();
1337
1488
  const expiresAt = new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString();
1338
- const created = yield* Effect13.try({
1489
+ const created = yield* Effect14.try({
1339
1490
  try: () => db.transaction(() => {
1340
1491
  const pairing = db.query(`SELECT * FROM auth_pairings
1341
1492
  WHERE token_hash = ? AND used_at IS NULL AND expires_at > ?`).get(tokenHash(token), timestamp);
1342
1493
  if (pairing === null) {
1343
1494
  throw new Error("Pairing credential is invalid, expired, or already used");
1344
1495
  }
1345
- const roles = Schema13.decodeUnknownSync(Schema13.Array(ConnectionRole))(JSON.parse(pairing.roles_json));
1496
+ const roles = Schema15.decodeUnknownSync(Schema15.Array(ConnectionRole))(JSON.parse(pairing.roles_json));
1346
1497
  const deviceId = roles.includes("device") ? makeDeviceId() : undefined;
1347
1498
  const consumed = db.query("UPDATE auth_pairings SET used_at = ? WHERE token_hash = ? AND used_at IS NULL").run(timestamp, pairing.token_hash);
1348
1499
  if (consumed.changes !== 1) {
@@ -1358,7 +1509,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1358
1509
  created_at: timestamp,
1359
1510
  expires_at: expiresAt,
1360
1511
  last_seen_at: timestamp,
1361
- bound_device_id: role === "device" ? deviceId ?? null : null,
1512
+ bound_device_id: deviceId ?? null,
1362
1513
  revoked_at: null
1363
1514
  };
1364
1515
  db.query(`INSERT INTO auth_sessions (
@@ -1370,11 +1521,11 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1370
1521
  })(),
1371
1522
  catch: operationError("RelayStore.exchangePairing")
1372
1523
  });
1373
- const credentials = yield* Effect13.forEach(created, ({ token: sessionToken, row }) => authSessionFromRow(row).pipe(Effect13.map((session) => ({ token: sessionToken, session }))));
1524
+ const credentials = yield* Effect14.forEach(created, ({ token: sessionToken, row }) => authSessionFromRow(row).pipe(Effect14.map((session) => ({ token: sessionToken, session }))));
1374
1525
  return PairingResult.make({ credentials });
1375
1526
  });
1376
- const authenticateSession = Effect13.fn("RelayStore.authenticateSession")(function* (token, role) {
1377
- const row = yield* Effect13.try({
1527
+ const authenticateSession = Effect14.fn("RelayStore.authenticateSession")(function* (token, role) {
1528
+ const row = yield* Effect14.try({
1378
1529
  try: () => db.query(`SELECT * FROM auth_sessions
1379
1530
  WHERE token_hash = ? AND revoked_at IS NULL AND expires_at > ?`).get(tokenHash(token), now()),
1380
1531
  catch: operationError("RelayStore.authenticateSession")
@@ -1383,35 +1534,35 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1383
1534
  return;
1384
1535
  }
1385
1536
  const lastSeenAt = now();
1386
- yield* Effect13.try({
1537
+ yield* Effect14.try({
1387
1538
  try: () => db.query("UPDATE auth_sessions SET last_seen_at = ? WHERE id = ?").run(lastSeenAt, row.id),
1388
1539
  catch: operationError("RelayStore.authenticateSession.touch")
1389
1540
  });
1390
1541
  const session = yield* authSessionFromRow(row);
1391
1542
  return AuthSession.make({ ...session, lastSeenAt });
1392
1543
  });
1393
- const listAuthSessions = Effect13.fn("RelayStore.listAuthSessions")(function* () {
1394
- const rows = yield* Effect13.try({
1544
+ const listAuthSessions = Effect14.fn("RelayStore.listAuthSessions")(function* () {
1545
+ const rows = yield* Effect14.try({
1395
1546
  try: () => db.query("SELECT * FROM auth_sessions ORDER BY created_at DESC, id LIMIT 1000").all(),
1396
1547
  catch: operationError("RelayStore.listAuthSessions")
1397
1548
  });
1398
- return yield* Effect13.forEach(rows, authSessionFromRow);
1549
+ return yield* Effect14.forEach(rows, authSessionFromRow);
1399
1550
  });
1400
- const revokeAuthSession = Effect13.fn("RelayStore.revokeAuthSession")(function* (sessionId) {
1551
+ const revokeAuthSession = Effect14.fn("RelayStore.revokeAuthSession")(function* (sessionId) {
1401
1552
  const revokedAt = now();
1402
- const result = yield* Effect13.try({
1553
+ const result = yield* Effect14.try({
1403
1554
  try: () => db.query("UPDATE auth_sessions SET revoked_at = ? WHERE id = ? AND revoked_at IS NULL").run(revokedAt, sessionId),
1404
1555
  catch: operationError("RelayStore.revokeAuthSession")
1405
1556
  });
1406
1557
  if (result.changes !== 1) {
1407
- return yield* Effect13.fail(new PersistenceError({
1558
+ return yield* Effect14.fail(new PersistenceError({
1408
1559
  operation: "RelayStore.revokeAuthSession",
1409
1560
  message: `Unknown or already revoked session ${sessionId}`
1410
1561
  }));
1411
1562
  }
1412
1563
  const row = db.query("SELECT * FROM auth_sessions WHERE id = ?").get(sessionId);
1413
1564
  if (row === null) {
1414
- return yield* Effect13.fail(new PersistenceError({
1565
+ return yield* Effect14.fail(new PersistenceError({
1415
1566
  operation: "RelayStore.revokeAuthSession.read",
1416
1567
  message: `Session ${sessionId} disappeared after revocation`
1417
1568
  }));
@@ -1428,13 +1579,23 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1428
1579
  markDeviceOffline: (deviceId) => updateDeviceStatus(deviceId, "offline"),
1429
1580
  createDelegation: createDelegation2,
1430
1581
  getTask,
1582
+ listAttachments,
1583
+ readAttachment,
1584
+ inboxFor,
1585
+ acknowledgeCompletion,
1431
1586
  traceTask,
1432
1587
  threadContext: threadContext2,
1433
1588
  pendingTasksFor,
1434
1589
  assignTask,
1590
+ markTaskDispatched: (taskId) => Effect14.try({
1591
+ try: () => {
1592
+ db.query("UPDATE tasks SET dispatched_at = COALESCE(dispatched_at, ?) WHERE id = ? AND status = 'assigned'").run(now(), taskId);
1593
+ },
1594
+ catch: operationError("RelayStore.markTaskDispatched")
1595
+ }),
1435
1596
  rollbackAssignment: (taskId) => transition(taskId, ["assigned"], { status: "queued" }, { kind: "requeued", detail: "Target device was unavailable during dispatch" }),
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" }))),
1437
- finishTask: (taskId, deviceId, result, providerSessionId) => terminal(taskId, deviceId, "completed", result, undefined, providerSessionId),
1597
+ acceptTask: (taskId, deviceId) => requireTarget(taskId, deviceId).pipe(Effect14.flatMap(() => transition(taskId, ["assigned"], { status: "running", startedAt: now() }, { kind: "running", detail: "Target device accepted the task and started the provider" }))),
1598
+ finishTask: (taskId, deviceId, result, providerSessionId, attachments2) => terminal(taskId, deviceId, "completed", result, undefined, providerSessionId, attachments2),
1438
1599
  failTask: (taskId, deviceId, error) => terminal(taskId, deviceId, "failed", undefined, error),
1439
1600
  acknowledgeCancellation: (taskId, deviceId) => terminal(taskId, deviceId, "cancelled"),
1440
1601
  requestCancellation,
@@ -1456,7 +1617,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1456
1617
  listAuthSessions,
1457
1618
  revokeAuthSession
1458
1619
  });
1459
- }, migrate = (db) => Effect13.try({
1620
+ }, migrate = (db) => Effect14.try({
1460
1621
  try: () => {
1461
1622
  db.exec("PRAGMA journal_mode = WAL");
1462
1623
  db.exec("PRAGMA foreign_keys = ON");
@@ -1485,12 +1646,20 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1485
1646
  id TEXT PRIMARY KEY, thread_id TEXT NOT NULL REFERENCES threads(id) ON DELETE CASCADE,
1486
1647
  prompt TEXT NOT NULL, context TEXT, provider TEXT NOT NULL, bot_id TEXT, status TEXT NOT NULL,
1487
1648
  source_device_id TEXT REFERENCES devices(id),
1649
+ requester_id TEXT, completion_seen_at TEXT,
1488
1650
  target_device_id TEXT NOT NULL REFERENCES devices(id), parent_task_id TEXT,
1489
1651
  workspace TEXT, provider_session_id TEXT, result TEXT, error TEXT,
1490
- created_at TEXT NOT NULL, updated_at TEXT NOT NULL, started_at TEXT, completed_at TEXT
1652
+ created_at TEXT NOT NULL, updated_at TEXT NOT NULL, started_at TEXT, completed_at TEXT,
1653
+ dispatched_at TEXT
1491
1654
  );
1492
1655
  CREATE INDEX IF NOT EXISTS tasks_thread_created ON tasks(thread_id, created_at);
1493
1656
  CREATE INDEX IF NOT EXISTS tasks_target_status ON tasks(target_device_id, status);
1657
+ CREATE TABLE IF NOT EXISTS task_attachments (
1658
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
1659
+ name TEXT NOT NULL, direction TEXT NOT NULL CHECK(direction IN ('input', 'output')),
1660
+ data BLOB NOT NULL CHECK(length(data) BETWEEN 1 AND ${maxAttachmentBytes}),
1661
+ PRIMARY KEY(task_id, direction, name)
1662
+ );
1494
1663
  CREATE TABLE IF NOT EXISTS task_trace_events (
1495
1664
  id INTEGER PRIMARY KEY AUTOINCREMENT,
1496
1665
  task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
@@ -1535,17 +1704,32 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1535
1704
  if (!taskColumns.some((column) => column.name === "bot_id")) {
1536
1705
  db.exec("ALTER TABLE tasks ADD COLUMN bot_id TEXT");
1537
1706
  }
1707
+ if (!taskColumns.some((column) => column.name === "requester_id")) {
1708
+ db.exec("ALTER TABLE tasks ADD COLUMN requester_id TEXT");
1709
+ }
1710
+ if (!taskColumns.some((column) => column.name === "completion_seen_at")) {
1711
+ db.exec("ALTER TABLE tasks ADD COLUMN completion_seen_at TEXT");
1712
+ }
1713
+ db.exec(`CREATE INDEX IF NOT EXISTS tasks_inbox
1714
+ ON tasks(requester_id, completion_seen_at, completed_at)`);
1715
+ if (!taskColumns.some((column) => column.name === "dispatched_at")) {
1716
+ db.transaction(() => {
1717
+ db.exec("ALTER TABLE tasks ADD COLUMN dispatched_at TEXT");
1718
+ db.exec(`UPDATE tasks SET dispatched_at = COALESCE(started_at, updated_at)
1719
+ WHERE provider = 'grok-bot' AND status NOT IN ('completed', 'failed', 'cancelled')`);
1720
+ })();
1721
+ }
1538
1722
  },
1539
1723
  catch: operationError("RelayStore.migrate")
1540
- }), layer2 = (path, retainedTerminalTasks = 1000) => Layer2.effect(Service2, Effect13.acquireRelease(Effect13.try({
1724
+ }), layer2 = (path, retainedTerminalTasks = 1000) => Layer2.effect(Service2, Effect14.acquireRelease(Effect14.try({
1541
1725
  try: () => new Database(path),
1542
1726
  catch: operationError("RelayStore.open")
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))));
1727
+ }).pipe(Effect14.tap(migrate)), (db) => Effect14.sync(() => db.close())).pipe(Effect14.map((db) => makeService(db, retainedTerminalTasks)))), layerFromDatabase = (db) => Layer2.effect(Service2, migrate(db).pipe(Effect14.map(() => makeService(db))));
1544
1728
  var init_store = __esm(() => {
1545
1729
  init_src();
1546
1730
  init_database();
1547
1731
  init_store();
1548
- PersistenceError = class PersistenceError extends Schema13.TaggedErrorClass()("RelayStore.PersistenceError", { operation: Schema13.String, message: Schema13.String }) {
1732
+ PersistenceError = class PersistenceError extends Schema15.TaggedErrorClass()("RelayStore.PersistenceError", { operation: Schema15.String, message: Schema15.String }) {
1549
1733
  };
1550
1734
  Service2 = class Service2 extends Context2.Service()("@cohall/RelayStore") {
1551
1735
  };
@@ -1555,10 +1739,13 @@ var init_store = __esm(() => {
1555
1739
  var exports_main = {};
1556
1740
  __export(exports_main, {
1557
1741
  runRelay: () => runRelay,
1742
+ resolveDelegation: () => resolveDelegation,
1558
1743
  relayListenOptions: () => relayListenOptions,
1559
- chooseDevice: () => chooseDevice
1744
+ maxTaskRequestBodyBytes: () => maxTaskRequestBodyBytes,
1745
+ canReadTaskAttachments: () => canReadTaskAttachments,
1746
+ canDispatchTaskToDevice: () => canDispatchTaskToDevice
1560
1747
  });
1561
- import { Effect as Effect14, ManagedRuntime, Schema as Schema14 } from "effect";
1748
+ import { Effect as Effect15, ManagedRuntime, Schema as Schema16 } from "effect";
1562
1749
  import { createHash as createHash3, timingSafeEqual } from "node:crypto";
1563
1750
  import { access as access6, chmod as chmod8 } from "node:fs/promises";
1564
1751
  import { createServer } from "node:http";
@@ -1604,7 +1791,7 @@ var RequestError, relayListenOptions = (configuration, environment = process.env
1604
1791
  return;
1605
1792
  }
1606
1793
  cleanup();
1607
- reject(new Error("Request body exceeded 256 KiB"));
1794
+ reject(new Error(`Request body exceeded ${Math.floor(limit / 1024)} KiB`));
1608
1795
  };
1609
1796
  const onEnd = () => {
1610
1797
  cleanup();
@@ -1618,7 +1805,7 @@ var RequestError, relayListenOptions = (configuration, environment = process.env
1618
1805
  request.once("end", onEnd);
1619
1806
  request.once("error", onError);
1620
1807
  });
1621
- }, webRequest = async (request) => {
1808
+ }, maxTaskRequestBodyBytes, webRequest = async (request) => {
1622
1809
  const headers = new Headers;
1623
1810
  for (let index = 0;index < request.rawHeaders.length; index += 2) {
1624
1811
  const name = request.rawHeaders[index];
@@ -1627,7 +1814,8 @@ var RequestError, relayListenOptions = (configuration, environment = process.env
1627
1814
  headers.append(name, value);
1628
1815
  }
1629
1816
  }
1630
- const payload = await requestBody(request, 256 * 1024);
1817
+ const pathname = new URL(request.url ?? "/", "http://cohall.local").pathname;
1818
+ const payload = await requestBody(request, pathname === "/api/tasks" ? maxTaskRequestBodyBytes : maxAttachmentBytes);
1631
1819
  return new Request(new URL(request.url ?? "/", "http://cohall.local"), {
1632
1820
  method: request.method ?? "GET",
1633
1821
  headers,
@@ -1657,18 +1845,55 @@ Content-Length: 0\r
1657
1845
  if ("destroy" in socket && typeof socket.destroy === "function") {
1658
1846
  socket.destroy();
1659
1847
  }
1660
- }, body = (request, decode3) => Effect14.tryPromise({
1848
+ }, body = (request, decode3) => Effect15.tryPromise({
1661
1849
  try: () => request.json(),
1662
1850
  catch: () => new RequestError({ status: 400, message: "Expected a JSON request body" })
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) => {
1851
+ }).pipe(Effect15.flatMap(decode3), Effect15.mapError((cause) => cause instanceof RequestError ? cause : new RequestError({ status: 400, message: String(cause).slice(0, 2048) }))), pathId = (schema, value) => Schema16.decodeUnknownEffect(schema)(value).pipe(Effect15.mapError(() => new RequestError({ status: 400, message: `Invalid identifier: ${value}` }))), scoreDevice = (device, input, sourceDeviceId) => {
1664
1852
  const prompt = `${input.prompt} ${input.context ?? ""}`.toLowerCase();
1665
1853
  const needsXcode = /\b(xcode|ios|macos|swiftui|app store)\b/.test(prompt);
1666
1854
  const needsBrowser = /\b(browser|chrome|youtube|twitter|logged[- ]?in|signed[- ]?in|x\.com)\b/.test(prompt);
1667
1855
  return (device.status === "offline" ? 0 : 100) + (input.workspace !== undefined && device.workspaces.some((workspace) => input.workspace === workspace.path || input.workspace?.startsWith(`${workspace.path}/`)) ? 80 : 0) + (needsXcode && device.capabilities.some((capability) => capability.id === "xcode") ? 60 : 0) + (needsBrowser && device.capabilities.some((capability) => capability.id === "browser-session") ? 40 : 0) + (sourceDeviceId === device.id ? -5 : 0);
1668
- }, chooseDevice = (input, sourceDeviceId) => Effect14.gen(function* () {
1856
+ }, resolveDelegation = (input, sourceDeviceId) => Effect15.gen(function* () {
1669
1857
  const store = yield* exports_store.Service;
1670
- const devices = yield* store.listDevices().pipe(Effect14.mapError((cause) => new RequestError({ status: 500, message: cause.message })));
1858
+ const ancestors = [];
1859
+ const seen = new Set;
1860
+ let nextParentId = input.parentTaskId;
1861
+ while (nextParentId !== undefined && !seen.has(nextParentId)) {
1862
+ const parentId = nextParentId;
1863
+ seen.add(parentId);
1864
+ const parent2 = yield* store.getTask(parentId).pipe(Effect15.catch((cause) => {
1865
+ const missing = cause.message === `Unknown task ${parentId}`;
1866
+ if (missing && ancestors.length > 0)
1867
+ return Effect15.succeed(undefined);
1868
+ return Effect15.fail(new RequestError({ status: missing ? 404 : 500, message: cause.message }));
1869
+ }));
1870
+ if (parent2 === undefined)
1871
+ break;
1872
+ ancestors.push(parent2);
1873
+ nextParentId = parent2.parentTaskId;
1874
+ }
1875
+ const parent = ancestors[0];
1876
+ if (parent !== undefined && input.threadId !== undefined && input.threadId !== parent.threadId) {
1877
+ return yield* new RequestError({
1878
+ status: 400,
1879
+ message: "A child task must use its parent's thread"
1880
+ });
1881
+ }
1882
+ const resolvedInput = parent === undefined ? input : { ...input, threadId: parent.threadId };
1883
+ const devices = yield* store.listDevices().pipe(Effect15.mapError((cause) => new RequestError({ status: 500, message: cause.message })));
1671
1884
  const provider = input.provider ?? (input.botId === undefined ? "codex" : "grok-bot");
1885
+ const hasAttachments = (input.attachments?.length ?? 0) > 0;
1886
+ if (hasAttachments && provider === "grok-bot") {
1887
+ return yield* new RequestError({
1888
+ status: 400,
1889
+ message: "File attachments require a coding provider"
1890
+ });
1891
+ }
1892
+ const slot = taskSlot({
1893
+ provider,
1894
+ ...input.botId === undefined ? {} : { botId: input.botId }
1895
+ });
1896
+ const blocked = new Set(ancestors.filter((task) => !isTerminalTask(task) && taskSlot(task) === slot).map((task) => task.targetDeviceId));
1672
1897
  if (input.targetDeviceId !== undefined) {
1673
1898
  const target = devices.find((device) => device.id === input.targetDeviceId);
1674
1899
  if (target === undefined) {
@@ -1683,35 +1908,48 @@ Content-Length: 0\r
1683
1908
  message: `${target.name} does not advertise the ${provider} provider`
1684
1909
  });
1685
1910
  }
1911
+ if (hasAttachments && !target.capabilities.some((capability) => capability.id === "task-attachments")) {
1912
+ return yield* new RequestError({
1913
+ status: 409,
1914
+ message: `${target.name} does not support file attachments; upgrade its Cohall worker`
1915
+ });
1916
+ }
1686
1917
  if (input.botId !== undefined && !target.bots?.some((bot) => bot.id === input.botId)) {
1687
1918
  return yield* new RequestError({
1688
1919
  status: 404,
1689
1920
  message: `${target.name} does not advertise bot ${input.botId}; refresh the bot list`
1690
1921
  });
1691
1922
  }
1692
- return target.id;
1923
+ if (blocked.has(target.id)) {
1924
+ return yield* new RequestError({
1925
+ status: 409,
1926
+ message: `${target.name} cannot run this task while an unfinished parent occupies the same worker slot; select another device or bot`
1927
+ });
1928
+ }
1929
+ return { input: resolvedInput, targetDeviceId: target.id };
1693
1930
  }
1694
- const candidates = devices.filter((device) => device.providers.includes(provider) && (input.botId === undefined || device.bots?.some((bot) => bot.id === input.botId)));
1931
+ const providerDevices = devices.filter((device) => device.providers.includes(provider) && (input.botId === undefined || device.bots?.some((bot) => bot.id === input.botId)));
1932
+ const candidates = hasAttachments ? providerDevices.filter((device) => device.capabilities.some((capability) => capability.id === "task-attachments")) : providerDevices;
1695
1933
  if (input.botId !== undefined && candidates.length > 1) {
1696
1934
  return yield* new RequestError({
1697
1935
  status: 409,
1698
1936
  message: `Bot ${input.botId} is advertised by more than one device; select a device`
1699
1937
  });
1700
1938
  }
1701
- const selected = candidates.sort((left, right) => scoreDevice(right, input, sourceDeviceId) - scoreDevice(left, input, sourceDeviceId))[0];
1939
+ const selected = candidates.filter((device) => !blocked.has(device.id)).sort((left, right) => scoreDevice(right, input, sourceDeviceId) - scoreDevice(left, input, sourceDeviceId))[0];
1702
1940
  if (selected === undefined) {
1703
1941
  return yield* new RequestError({
1704
1942
  status: 409,
1705
- message: input.botId === undefined ? `No Cohall device advertises the ${provider} provider` : `No Cohall device advertises bot ${input.botId}; refresh the bot list`
1943
+ message: candidates.length > 0 ? "No available delegation target avoids an unfinished parent's worker slot; select another device or bot" : hasAttachments && providerDevices.length > 0 ? `No Cohall device advertising the ${provider} provider supports file attachments; upgrade a Cohall worker` : input.botId === undefined ? `No Cohall device advertises the ${provider} provider` : `No Cohall device advertises bot ${input.botId}; refresh the bot list`
1706
1944
  });
1707
1945
  }
1708
- return selected.id;
1709
- }), runRelay = async () => {
1710
- const configuration = await Effect14.runPromise(loadEnvironmentConfiguration);
1946
+ return { input: resolvedInput, targetDeviceId: selected.id };
1947
+ }), canReadTaskAttachments = (principal, task) => principal === "owner" || principal.role === "client" || principal.deviceId === task.targetDeviceId, canDispatchTaskToDevice = (task, device) => (task.inputAttachmentNames?.length ?? 0) === 0 || device?.capabilities.some((capability) => capability.id === "task-attachments") === true, runRelay = async () => {
1948
+ const configuration = await Effect15.runPromise(loadEnvironmentConfiguration);
1711
1949
  process.umask(63);
1712
1950
  const runtime = ManagedRuntime.make(exports_store.layer(configuration.databasePath, configuration.historyTaskLimit));
1713
1951
  const run2 = (effect) => runtime.runPromise(effect);
1714
- await run2(Effect14.gen(function* () {
1952
+ await run2(Effect15.gen(function* () {
1715
1953
  const store = yield* exports_store.Service;
1716
1954
  yield* store.recover();
1717
1955
  }));
@@ -1726,38 +1964,41 @@ Content-Length: 0\r
1726
1964
  if (ownerToken2(token)) {
1727
1965
  return "owner";
1728
1966
  }
1729
- return run2(Effect14.gen(function* () {
1967
+ return run2(Effect15.gen(function* () {
1730
1968
  const store = yield* exports_store.Service;
1731
1969
  return yield* store.authenticateSession(token, role);
1732
1970
  })).catch(() => {
1733
1971
  return;
1734
1972
  });
1735
1973
  };
1736
- const principalFor = async (request) => {
1974
+ const principalFor = async (request, allowDevice = false) => {
1737
1975
  const authorization = request.headers.get("authorization");
1738
1976
  if (authorization?.startsWith("Bearer ") !== true) {
1739
1977
  return;
1740
1978
  }
1741
- return authenticate(authorization.slice("Bearer ".length), "client");
1979
+ const token = authorization.slice("Bearer ".length);
1980
+ return await authenticate(token, "client") ?? (allowDevice ? authenticate(token, "device") : undefined);
1742
1981
  };
1743
1982
  const dispatch = async (task) => {
1744
- const assigned = await run2(Effect14.gen(function* () {
1745
- const store = yield* exports_store.Service;
1746
- return yield* store.assignTask(task.id);
1747
- }));
1983
+ const store = await run2(exports_store.Service);
1984
+ if ((task.inputAttachmentNames?.length ?? 0) > 0) {
1985
+ const devices = await Effect15.runPromise(store.listDevices());
1986
+ const target = devices.find((device) => device.id === task.targetDeviceId);
1987
+ if (!canDispatchTaskToDevice(task, target)) {
1988
+ return Effect15.runPromise(store.failTask(task.id, task.targetDeviceId, "Target worker no longer supports task file attachments. Upgrade the worker and retry."));
1989
+ }
1990
+ }
1991
+ const assigned = await Effect15.runPromise(store.assignTask(task.id));
1748
1992
  if (assigned.status !== "assigned") {
1749
1993
  return assigned;
1750
1994
  }
1751
- if (hub.sendToDevice(assigned.targetDeviceId, SocketEvent.make({ _tag: "TaskAssigned", task: assigned }))) {
1995
+ if (hub.sendToDevice(assigned.targetDeviceId, SocketEvent.make({ _tag: "TaskAssigned", task: assigned }), assigned.provider === "grok-bot" ? () => Effect15.runSync(store.markTaskDispatched(assigned.id)) : undefined)) {
1752
1996
  return assigned;
1753
1997
  }
1754
- return run2(Effect14.gen(function* () {
1755
- const store = yield* exports_store.Service;
1756
- return yield* store.rollbackAssignment(task.id);
1757
- }));
1998
+ return Effect15.runPromise(store.rollbackAssignment(task.id));
1758
1999
  };
1759
2000
  const dispatchOperation = async (operation) => {
1760
- const assigned = await run2(Effect14.gen(function* () {
2001
+ const assigned = await run2(Effect15.gen(function* () {
1761
2002
  const store = yield* exports_store.Service;
1762
2003
  return yield* store.assignOperation(operation.id);
1763
2004
  }));
@@ -1767,13 +2008,13 @@ Content-Length: 0\r
1767
2008
  if (hub.sendToDevice(assigned.targetDeviceId, SocketEvent.make({ _tag: "OperationAssigned", operation: assigned }))) {
1768
2009
  return assigned;
1769
2010
  }
1770
- return run2(Effect14.gen(function* () {
2011
+ return run2(Effect15.gen(function* () {
1771
2012
  const store = yield* exports_store.Service;
1772
2013
  return yield* store.rollbackOperation(operation.id);
1773
2014
  }));
1774
2015
  };
1775
2016
  const dispatchPendingOperations = async (deviceId) => {
1776
- const operations = await run2(Effect14.gen(function* () {
2017
+ const operations = await run2(Effect15.gen(function* () {
1777
2018
  const store = yield* exports_store.Service;
1778
2019
  return yield* store.pendingOperationsFor(deviceId);
1779
2020
  }));
@@ -1782,7 +2023,7 @@ Content-Length: 0\r
1782
2023
  }
1783
2024
  };
1784
2025
  const dispatchPendingTasks = async (deviceId) => {
1785
- const tasks = await run2(Effect14.gen(function* () {
2026
+ const tasks = await run2(Effect15.gen(function* () {
1786
2027
  const store = yield* exports_store.Service;
1787
2028
  return yield* store.pendingTasksFor(deviceId);
1788
2029
  }));
@@ -1806,10 +2047,10 @@ Content-Length: 0\r
1806
2047
  })));
1807
2048
  };
1808
2049
  const handleSocketEvent = async (socket, input) => {
1809
- const event = await Effect14.runPromise(Effect14.try({
2050
+ const event = await Effect15.runPromise(Effect15.try({
1810
2051
  try: () => JSON.parse(socketText(input)),
1811
2052
  catch: () => new RequestError({ status: 400, message: "Invalid socket JSON" })
1812
- }).pipe(Effect14.flatMap(decodeSocketEvent))).catch((cause) => {
2053
+ }).pipe(Effect15.flatMap(decodeSocketEvent))).catch((cause) => {
1813
2054
  sendError(socket, "invalid_event", cause instanceof Error ? cause.message : String(cause));
1814
2055
  return;
1815
2056
  });
@@ -1844,7 +2085,12 @@ Content-Length: 0\r
1844
2085
  socket.close(4003, "Device registration timeout");
1845
2086
  }
1846
2087
  }, 5000);
1847
- socket.send(JSON.stringify(SocketEvent.make({ _tag: "Connected", serverVersion: version, connectedAt: now() })));
2088
+ socket.send(JSON.stringify(SocketEvent.make({
2089
+ _tag: "Connected",
2090
+ serverVersion: version,
2091
+ connectedAt: now(),
2092
+ taskAttachments: true
2093
+ })));
1848
2094
  return;
1849
2095
  }
1850
2096
  if (event._tag === "Authenticate") {
@@ -1866,7 +2112,7 @@ Content-Length: 0\r
1866
2112
  socket.close(4009, "Device is already connected");
1867
2113
  return;
1868
2114
  }
1869
- await run2(Effect14.gen(function* () {
2115
+ await run2(Effect15.gen(function* () {
1870
2116
  const store = yield* exports_store.Service;
1871
2117
  yield* store.upsertDevice(device);
1872
2118
  }));
@@ -1885,7 +2131,7 @@ Content-Length: 0\r
1885
2131
  const settleOperation = (operationId) => {
1886
2132
  socket.send(JSON.stringify(SocketEvent.make({ _tag: "OperationSettled", operationId })));
1887
2133
  };
1888
- const processed = await run2(Effect14.gen(function* () {
2134
+ const processed = await run2(Effect15.gen(function* () {
1889
2135
  const store = yield* exports_store.Service;
1890
2136
  switch (event._tag) {
1891
2137
  case "DeviceHeartbeat":
@@ -1897,7 +2143,7 @@ Content-Length: 0\r
1897
2143
  yield* store.acceptTask(event.taskId, deviceId);
1898
2144
  return;
1899
2145
  case "TaskFinished":
1900
- yield* store.finishTask(event.taskId, deviceId, event.result, event.providerSessionId);
2146
+ yield* store.finishTask(event.taskId, deviceId, event.result, event.providerSessionId, event.attachments);
1901
2147
  return;
1902
2148
  case "TaskFailed":
1903
2149
  yield* store.failTask(event.taskId, deviceId, event.error);
@@ -1938,16 +2184,17 @@ Content-Length: 0\r
1938
2184
  };
1939
2185
  const api = async (request, url) => {
1940
2186
  if (url.pathname === "/api/health" && request.method === "GET") {
1941
- return json({ ok: true, version });
2187
+ return json({ ok: true, version, taskAttachments: true });
1942
2188
  }
1943
2189
  if (url.pathname === "/api/auth/pair" && request.method === "POST") {
1944
- return run2(Effect14.gen(function* () {
2190
+ return run2(Effect15.gen(function* () {
1945
2191
  const store = yield* exports_store.Service;
1946
2192
  const input = yield* body(request, decodeExchangePairingInput);
1947
2193
  return yield* store.exchangePairing(input.token);
1948
2194
  })).then((value) => json(value, 201)).catch(() => json({ error: "Pairing credential is invalid, expired, or already used" }, 401));
1949
2195
  }
1950
- const principal = await principalFor(request);
2196
+ const attachmentRoute = request.method === "GET" && /^\/api\/tasks\/[^/]+\/attachments(?:\/[^/]+)?$/.test(url.pathname);
2197
+ const principal = await principalFor(request, attachmentRoute);
1951
2198
  if (principal === undefined) {
1952
2199
  return json({ error: "Unauthorized" }, 401);
1953
2200
  }
@@ -1956,7 +2203,7 @@ Content-Length: 0\r
1956
2203
  return json({ error: "Owner token required" }, 403);
1957
2204
  }
1958
2205
  const sourceDeviceId = principal === "owner" ? undefined : principal.deviceId;
1959
- const effect = Effect14.gen(function* () {
2206
+ const effect = Effect15.gen(function* () {
1960
2207
  const store = yield* exports_store.Service;
1961
2208
  if (url.pathname === "/api/auth/pairings" && request.method === "POST") {
1962
2209
  const input = yield* body(request, decodeCreatePairingInput);
@@ -1984,7 +2231,7 @@ Content-Length: 0\r
1984
2231
  if (url.pathname === "/api/operations/upgrades" && request.method === "POST") {
1985
2232
  const input = yield* body(request, decodeCreateUpgradeOperationsInput);
1986
2233
  const operations = yield* store.createUpgradeOperations(input);
1987
- return json(yield* Effect14.tryPromise(() => Promise.all(operations.map(dispatchOperation))), 201);
2234
+ return json(yield* Effect15.tryPromise(() => Promise.all(operations.map(dispatchOperation))), 201);
1988
2235
  }
1989
2236
  const abandonOperation = url.pathname.match(/^\/api\/operations\/([^/]+)\/abandon$/);
1990
2237
  if (request.method === "POST" && abandonOperation?.[1] !== undefined) {
@@ -2009,11 +2256,54 @@ Content-Length: 0\r
2009
2256
  return json(forgotten);
2010
2257
  }
2011
2258
  if (url.pathname === "/api/tasks" && request.method === "POST") {
2012
- const input = yield* body(request, decodeCreateTaskInput);
2013
- const target = yield* chooseDevice(input, sourceDeviceId);
2259
+ const { input, targetDeviceId: target } = yield* resolveDelegation(yield* body(request, decodeCreateTaskInput), sourceDeviceId);
2014
2260
  const providerSessionId = input.threadId === undefined || input.provider === "grok-bot" ? undefined : yield* store.sessionFor(input.threadId, target, input.provider ?? "codex");
2015
- const task2 = yield* store.createDelegation(input, target, sourceDeviceId, providerSessionId);
2016
- return json(yield* Effect14.tryPromise(() => dispatch(task2)), 201);
2261
+ const task2 = yield* store.createDelegation(input, target, principal, providerSessionId);
2262
+ return json(yield* Effect15.tryPromise(() => dispatch(task2)), 201);
2263
+ }
2264
+ if (url.pathname === "/api/inbox" && request.method === "GET") {
2265
+ return json(yield* store.inboxFor(principal));
2266
+ }
2267
+ const acknowledge = url.pathname.match(/^\/api\/inbox\/([^/]+)\/ack$/);
2268
+ if (request.method === "POST" && acknowledge?.[1] !== undefined) {
2269
+ return json(yield* store.acknowledgeCompletion(yield* pathId(TaskId, acknowledge[1]), principal));
2270
+ }
2271
+ const attachments2 = url.pathname.match(/^\/api\/tasks\/([^/]+)\/attachments$/);
2272
+ if (request.method === "GET" && attachments2?.[1] !== undefined) {
2273
+ const id = yield* pathId(TaskId, attachments2[1]);
2274
+ const task2 = yield* store.getTask(id);
2275
+ if (!canReadTaskAttachments(principal, task2)) {
2276
+ return yield* new RequestError({
2277
+ status: 403,
2278
+ message: "Device cannot read this task's attachments"
2279
+ });
2280
+ }
2281
+ return json(yield* store.listAttachments(id));
2282
+ }
2283
+ const attachment = url.pathname.match(/^\/api\/tasks\/([^/]+)\/attachments\/([^/]+)$/);
2284
+ if (attachment?.[1] !== undefined && attachment[2] !== undefined && request.method === "GET") {
2285
+ const id = yield* pathId(TaskId, attachment[1]);
2286
+ const name = yield* Effect15.try({
2287
+ try: () => decodeURIComponent(attachment[2]),
2288
+ catch: () => new RequestError({ status: 400, message: "Invalid attachment name" })
2289
+ }).pipe(Effect15.flatMap((value) => pathId(AttachmentName, value)));
2290
+ const task2 = yield* store.getTask(id);
2291
+ if (!canReadTaskAttachments(principal, task2)) {
2292
+ return yield* new RequestError({
2293
+ status: 403,
2294
+ message: "Device cannot read this task's attachments"
2295
+ });
2296
+ }
2297
+ const directionInput = url.searchParams.get("direction");
2298
+ const direction = directionInput === null ? undefined : yield* pathId(AttachmentDirection, directionInput);
2299
+ const data = yield* store.readAttachment(id, name, direction);
2300
+ return new Response(new Uint8Array(data), {
2301
+ headers: {
2302
+ "content-type": "application/octet-stream",
2303
+ "cache-control": "no-store",
2304
+ "x-content-type-options": "nosniff"
2305
+ }
2306
+ });
2017
2307
  }
2018
2308
  const task = url.pathname.match(/^\/api\/tasks\/([^/]+)$/);
2019
2309
  if (request.method === "GET" && task?.[1] !== undefined) {
@@ -2037,7 +2327,7 @@ Content-Length: 0\r
2037
2327
  return json(yield* store.threadContext(yield* pathId(ThreadId, thread[1])));
2038
2328
  }
2039
2329
  return yield* new RequestError({ status: 404, message: "Route not found" });
2040
- }).pipe(Effect14.mapError((cause) => cause instanceof RequestError ? cause : new RequestError({
2330
+ }).pipe(Effect15.mapError((cause) => cause instanceof RequestError ? cause : new RequestError({
2041
2331
  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,
2042
2332
  message: cause.message
2043
2333
  })));
@@ -2051,6 +2341,7 @@ Content-Length: 0\r
2051
2341
  });
2052
2342
  websocketServer.on("connection", (rawSocket) => {
2053
2343
  const socket = rawSocket;
2344
+ socket.on("error", () => socket.terminate());
2054
2345
  socket.data = {
2055
2346
  processing: Promise.resolve(),
2056
2347
  stageDeadline: undefined,
@@ -2112,7 +2403,7 @@ Content-Length: 0\r
2112
2403
  if (deviceId === undefined) {
2113
2404
  return;
2114
2405
  }
2115
- run2(Effect14.gen(function* () {
2406
+ run2(Effect15.gen(function* () {
2116
2407
  const store = yield* exports_store.Service;
2117
2408
  yield* store.requeueTasksFor(deviceId);
2118
2409
  yield* store.requeueOperationsFor(deviceId);
@@ -2124,10 +2415,14 @@ Content-Length: 0\r
2124
2415
  webRequest(incoming).then((request) => {
2125
2416
  const url = new URL(request.url);
2126
2417
  return url.pathname.startsWith("/api/") ? api(request, url) : json({ name: "Cohall relay", version, status: "running" });
2127
- }).then((response) => sendResponse(outgoing, response)).catch((cause) => sendResponse(outgoing, json({ error: cause instanceof Error ? cause.message : "Invalid request" }, cause instanceof Error && cause.message.includes("256 KiB") ? 413 : 400)));
2418
+ }).then((response) => sendResponse(outgoing, response)).catch((cause) => sendResponse(outgoing, json({ error: cause instanceof Error ? cause.message : "Invalid request" }, cause instanceof Error && cause.message.includes("Request body exceeded") ? 413 : 400)));
2128
2419
  });
2129
2420
  server.on("upgrade", (request, socket, head) => {
2130
- const url = new URL(request.url ?? "/", "http://cohall.local");
2421
+ const url = URL.parse(request.url ?? "/", "http://cohall.local");
2422
+ if (url === null) {
2423
+ rejectUpgrade(socket, 400, "Bad Request");
2424
+ return;
2425
+ }
2131
2426
  if (url.pathname !== "/ws/device") {
2132
2427
  rejectUpgrade(socket, 404, "Not Found");
2133
2428
  return;
@@ -2171,17 +2466,18 @@ var init_main = __esm(async () => {
2171
2466
  init_config();
2172
2467
  init_hub();
2173
2468
  init_store();
2174
- RequestError = class RequestError extends Schema14.TaggedErrorClass()("Relay.RequestError", {
2175
- status: Schema14.Int,
2176
- message: Schema14.String
2469
+ RequestError = class RequestError extends Schema16.TaggedErrorClass()("Relay.RequestError", {
2470
+ status: Schema16.Int,
2471
+ message: Schema16.String
2177
2472
  }) {
2178
2473
  };
2474
+ maxTaskRequestBodyBytes = 3 * 1024 * 1024;
2179
2475
  if (false) {}
2180
2476
  });
2181
2477
 
2182
2478
  // apps/device/src/main.ts
2183
2479
  init_src();
2184
- import { Effect as Effect15 } from "effect";
2480
+ import { Effect as Effect16 } from "effect";
2185
2481
 
2186
2482
  // packages/client/src/index.ts
2187
2483
  init_src();
@@ -2286,11 +2582,72 @@ var make = (options) => {
2286
2582
  return Service.of({
2287
2583
  devices: () => request("RelayClient.devices", "/api/devices", Schema2.Array(Device)),
2288
2584
  forgetDevice: (deviceId) => request("RelayClient.forgetDevice", `/api/devices/${encodeURIComponent(deviceId)}/forget`, Device, { method: "POST" }),
2289
- createTask: (input) => request("RelayClient.createTask", "/api/tasks", Task, {
2290
- method: "POST",
2291
- body: JSON.stringify(input)
2585
+ createTask: (input) => Effect2.gen(function* () {
2586
+ if ((input.attachments?.length ?? 0) > 0) {
2587
+ const health = yield* request("RelayClient.attachmentSupport", "/api/health", Schema2.Struct({ taskAttachments: Schema2.optionalKey(Schema2.Boolean) }));
2588
+ if (health.taskAttachments !== true) {
2589
+ return yield* new RelayRequestError({
2590
+ operation: "RelayClient.createTask",
2591
+ message: "Upgrade the Cohall relay before sending file attachments"
2592
+ });
2593
+ }
2594
+ }
2595
+ return yield* request("RelayClient.createTask", "/api/tasks", Task, {
2596
+ method: "POST",
2597
+ body: JSON.stringify(input)
2598
+ });
2292
2599
  }),
2293
2600
  getTask: (taskId) => request("RelayClient.getTask", `/api/tasks/${encodeURIComponent(taskId)}`, Task),
2601
+ listAttachments: (taskId) => request("RelayClient.listAttachments", `/api/tasks/${encodeURIComponent(taskId)}/attachments`, Schema2.Array(TaskAttachment)),
2602
+ readAttachment: (taskId, name, direction) => Effect2.tryPromise({
2603
+ try: async (signal) => {
2604
+ const operation = "RelayClient.readAttachment";
2605
+ const response = await fetch(`${baseUrl}/api/tasks/${encodeURIComponent(taskId)}/attachments/${encodeURIComponent(name)}${direction === undefined ? "" : `?direction=${direction}`}`, {
2606
+ headers: { authorization: `Bearer ${options.token}` },
2607
+ signal: AbortSignal.any([signal, AbortSignal.timeout(30000)])
2608
+ });
2609
+ if (!response.ok) {
2610
+ throw new RelayRequestError({
2611
+ operation,
2612
+ message: await responseMessage(response, operation),
2613
+ status: response.status
2614
+ });
2615
+ }
2616
+ if (response.body === null) {
2617
+ throw new RelayDecodeError({
2618
+ operation,
2619
+ message: "Relay returned an empty attachment response"
2620
+ });
2621
+ }
2622
+ const reader = response.body.getReader();
2623
+ const chunks = [];
2624
+ let size = 0;
2625
+ while (true) {
2626
+ const chunk = await reader.read();
2627
+ if (chunk.done)
2628
+ break;
2629
+ size += chunk.value.byteLength;
2630
+ if (size > maxAttachmentBytes) {
2631
+ await reader.cancel();
2632
+ throw new RelayDecodeError({ operation, message: "Attachment exceeded 256 KiB" });
2633
+ }
2634
+ chunks.push(chunk.value);
2635
+ }
2636
+ const bytes = new Uint8Array(size);
2637
+ let offset = 0;
2638
+ for (const chunk of chunks) {
2639
+ bytes.set(chunk, offset);
2640
+ offset += chunk.byteLength;
2641
+ }
2642
+ return bytes;
2643
+ },
2644
+ catch: (cause) => cause instanceof RelayRequestError || cause instanceof RelayDecodeError ? cause : new RelayRequestError({
2645
+ operation: "RelayClient.readAttachment",
2646
+ message: cause instanceof Error ? cause.message : String(cause)
2647
+ })
2648
+ }),
2649
+ inbox: () => request("RelayClient.inbox", "/api/inbox", TaskInbox),
2650
+ acknowledgeCompletion: (taskId) => request("RelayClient.acknowledgeCompletion", `/api/inbox/${encodeURIComponent(taskId)}/ack`, TaskInboxItem, { method: "POST" }),
2294
2651
  traceTask: (taskId) => request("RelayClient.traceTask", `/api/tasks/${encodeURIComponent(taskId)}/trace`, TaskTrace),
2295
2652
  cancelTask: (taskId) => request("RelayClient.cancelTask", `/api/tasks/${encodeURIComponent(taskId)}/cancel`, Task, {
2296
2653
  method: "POST"
@@ -2343,11 +2700,12 @@ init_src();
2343
2700
  // packages/providers/src/index.ts
2344
2701
  init_src();
2345
2702
  import { Effect as Effect3, Schema as Schema3 } from "effect";
2703
+ import { execa } from "execa";
2346
2704
  import { accessSync, constants } from "node:fs";
2347
2705
  import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises";
2348
2706
  import { homedir, platform, tmpdir } from "node:os";
2349
2707
  import { delimiter, extname, isAbsolute, join } from "node:path";
2350
- import { execFile, spawn } from "node:child_process";
2708
+ import { execFile } from "node:child_process";
2351
2709
 
2352
2710
  class ProviderUnavailableError extends Schema3.TaggedErrorClass()("CohallProvider.Unavailable", { provider: Provider, message: Schema3.String }) {
2353
2711
  }
@@ -2364,7 +2722,10 @@ var executables = {
2364
2722
  opencode: "opencode"
2365
2723
  };
2366
2724
  var providerEnvironment = () => ({
2367
- ...Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.startsWith("COHALL_") || name === "COHALL_CONFIG")),
2725
+ ...Object.fromEntries(Object.entries(process.env).filter(([name]) => {
2726
+ const key = platform() === "win32" ? name.toUpperCase() : name;
2727
+ return !key.startsWith("COHALL_") || key === "COHALL_CONFIG";
2728
+ })),
2368
2729
  PATH: executableDirectories().join(delimiter)
2369
2730
  });
2370
2731
  var record = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? Object.fromEntries(Object.entries(value)) : undefined;
@@ -2708,8 +3069,8 @@ var terminateProcessTree = async (pid) => {
2708
3069
  process.kill(-pid, "SIGKILL");
2709
3070
  await waitForProcessGroup(pid, 1000);
2710
3071
  };
2711
- var run = (options) => Effect3.tryPromise({
2712
- try: async (signal) => {
3072
+ var run = (options) => Effect3.callback((resume, signal) => {
3073
+ const running = (async () => {
2713
3074
  const executable = findExecutable(executables[options.provider]);
2714
3075
  if (executable === undefined) {
2715
3076
  throw new ProviderUnavailableError({
@@ -2720,8 +3081,9 @@ var run = (options) => Effect3.tryPromise({
2720
3081
  const prepared = await prepareCommand(options);
2721
3082
  try {
2722
3083
  await options.beforeSpawn?.();
3084
+ signal.throwIfAborted();
2723
3085
  const [, ...arguments_] = prepared.command;
2724
- const child = spawn(executable, arguments_, {
3086
+ const child = execa(executable, arguments_, {
2725
3087
  cwd: options.cwd,
2726
3088
  env: {
2727
3089
  ...providerEnvironment(),
@@ -2729,12 +3091,22 @@ var run = (options) => Effect3.tryPromise({
2729
3091
  COHALL_THREAD_ID: options.threadId,
2730
3092
  ...options.taskId === undefined ? {} : { COHALL_TASK_ID: options.taskId }
2731
3093
  },
3094
+ extendEnv: false,
2732
3095
  detached: platform() !== "win32",
2733
- stdio: ["pipe", "pipe", "pipe"]
3096
+ stdin: "pipe",
3097
+ stdout: "pipe",
3098
+ stderr: "pipe",
3099
+ buffer: false,
3100
+ reject: false,
3101
+ cleanup: false
2734
3102
  });
2735
- const exited = new Promise((resolve, reject) => {
2736
- child.once("error", reject);
2737
- child.once("exit", (code) => resolve(code ?? 1));
3103
+ let finished = false;
3104
+ const exited = child.then(({ exitCode, failed, originalMessage, shortMessage }) => {
3105
+ finished = true;
3106
+ if (failed && exitCode === undefined) {
3107
+ throw new Error(originalMessage ?? shortMessage ?? "Provider failed to start");
3108
+ }
3109
+ return exitCode ?? 1;
2738
3110
  });
2739
3111
  let termination;
2740
3112
  const terminate = () => {
@@ -2749,12 +3121,16 @@ var run = (options) => Effect3.tryPromise({
2749
3121
  if (signal.aborted) {
2750
3122
  terminate();
2751
3123
  }
2752
- child.stdin.end(prepared.input);
2753
3124
  try {
3125
+ const written = new Promise((resolve, reject) => {
3126
+ child.stdin.once("error", reject);
3127
+ child.stdin.end(prepared.input, (cause) => cause === undefined || cause === null ? resolve() : reject(cause));
3128
+ });
2754
3129
  const [result, stderr, exitCode] = await Promise.all([
2755
3130
  parse(options, child.stdout, terminate),
2756
3131
  captureText(child.stderr, 64 * 1024),
2757
- exited
3132
+ exited,
3133
+ written
2758
3134
  ]);
2759
3135
  if (exitCode !== 0) {
2760
3136
  throw new ProviderRunError({
@@ -2778,7 +3154,7 @@ var run = (options) => Effect3.tryPromise({
2778
3154
  return result;
2779
3155
  } finally {
2780
3156
  signal.removeEventListener("abort", terminate);
2781
- if (child.exitCode === null) {
3157
+ if (!finished) {
2782
3158
  terminate();
2783
3159
  await exited.catch(() => {
2784
3160
  return;
@@ -2789,23 +3165,23 @@ var run = (options) => Effect3.tryPromise({
2789
3165
  } finally {
2790
3166
  await prepared.cleanup();
2791
3167
  }
2792
- },
2793
- catch: (cause) => {
2794
- if (cause instanceof ProviderUnavailableError || cause instanceof ProviderRunError) {
2795
- return cause;
2796
- }
2797
- return new ProviderRunError({
2798
- provider: options.provider,
2799
- message: (cause instanceof Error ? cause.message : String(cause)).slice(0, 16384)
2800
- });
2801
- }
3168
+ })();
3169
+ running.then((result) => resume(Effect3.succeed(result)), (cause) => resume(Effect3.fail(cause instanceof ProviderUnavailableError || cause instanceof ProviderRunError ? cause : new ProviderRunError({
3170
+ provider: options.provider,
3171
+ message: (cause instanceof Error ? cause.message : String(cause)).slice(0, 16384)
3172
+ }))));
3173
+ return Effect3.promise(() => running.then(() => {
3174
+ return;
3175
+ }, () => {
3176
+ return;
3177
+ }));
2802
3178
  });
2803
3179
 
2804
3180
  // apps/device/src/cli.ts
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";
3181
+ import { Effect as Effect10, Schema as Schema11 } from "effect";
3182
+ import { access as access4, mkdir as mkdir7, readFile as readFile4, stat as stat4, writeFile as writeFile7 } from "node:fs/promises";
2807
3183
  import { homedir as homedir4 } from "node:os";
2808
- import { dirname as dirname6, join as join7, resolve as resolve3 } from "node:path";
3184
+ import { dirname as dirname6, join as join8, resolve as resolve3 } from "node:path";
2809
3185
 
2810
3186
  // skills/cohall/SKILL.md
2811
3187
  var SKILL_default = `---
@@ -2817,7 +3193,7 @@ description: Delegate through Cohall to another user-owned device or an existing
2817
3193
 
2818
3194
  Cohall sends a focused task to a coding agent or an existing named Grok Bot. The target
2819
3195
  keeps its local files, credentials, browser state, provider login, skills, and
2820
- permissions. The relay carries prompts, final results, and task state.
3196
+ permissions. The relay carries prompts, final results, task state, and explicitly attached files.
2821
3197
 
2822
3198
  Use the installed \`cohall\` executable when it is available. Fall back to
2823
3199
  \`npx -y @akshar5/cohall\` only when Cohall is not installed globally.
@@ -2878,6 +3254,16 @@ Do not delegate ordinary local work when the other device provides no advantage.
2878
3254
  when \`status\` is \`completed\`; use \`result\` in the current task. Report a
2879
3255
  \`failed\`, \`cancelled\`, or \`cancelling\` state accurately.
2880
3256
 
3257
+ Pass an explicit local file with \`--attach path\` when the coding task needs
3258
+ it. The MCP \`delegate\` tool takes \`attachment_paths\`. A task accepts up to
3259
+ two files of 256 KiB each. The target can return files through the output
3260
+ directory named in its task prompt. After completion, use
3261
+ \`cohall attachments <task-id>\` and
3262
+ \`cohall download <task-id> <name> --output <new-path>\` (or the corresponding
3263
+ MCP tools). List results include input and output files. A shared name
3264
+ downloads the output by default; use \`--direction input\` or the MCP
3265
+ \`direction\` argument for the input. Named bots do not accept files.
3266
+
2881
3267
  6. Reuse \`thread_id\` for related follow-ups so the target provider can resume
2882
3268
  its local session:
2883
3269
 
@@ -2892,6 +3278,11 @@ When a coding agent running as a Cohall task delegates again, \`COHALL_THREAD_ID
2892
3278
  and \`COHALL_TASK_ID\` carry its thread and parent task. Pass \`--thread\` and
2893
3279
  \`--parent\` explicitly from a Grok Bot using the IDs in its handoff.
2894
3280
 
3281
+ For work sent with \`--no-wait\`, check \`cohall inbox\` when returning to the task.
3282
+ Read the full result with \`cohall status <task-id>\`, use it in the current work,
3283
+ then run \`cohall inbox ack <task-id>\`. Results that arrive after a wait times
3284
+ out appear in the same inbox.
3285
+
2895
3286
  ## Message named bots
2896
3287
 
2897
3288
  1. Run \`cohall bots\` and choose a listed target. Use its qualified target when
@@ -3013,7 +3404,7 @@ Return the diagnostic failure to the user rather than claiming remote work ran.
3013
3404
  // apps/device/src/config.ts
3014
3405
  init_src();
3015
3406
  import { Effect as Effect4, Schema as Schema4 } from "effect";
3016
- import { access, chmod as chmod2, mkdir, readFile, realpath, rename, writeFile as writeFile2 } from "node:fs/promises";
3407
+ import { access, chmod as chmod2, mkdir, readFile, realpath, rename, stat, writeFile as writeFile2 } from "node:fs/promises";
3017
3408
  import { homedir as homedir2, hostname, platform as platform2 } from "node:os";
3018
3409
  import { dirname, join as join2, resolve } from "node:path";
3019
3410
  var WorkspaceList = Schema4.Array(Schema4.NonEmptyString).check(Schema4.isMaxLength(64));
@@ -3086,10 +3477,16 @@ var normalizeRelayUrl = (input) => {
3086
3477
  var parseWorkspaces = (workspaceList, workspaceJson) => Effect4.tryPromise({
3087
3478
  try: async () => {
3088
3479
  const values = workspaceJson === undefined ? workspaceList.split(",").map((workspace) => workspace.trim()).filter((workspace) => workspace.length > 0) : Schema4.decodeUnknownSync(WorkspaceList)(JSON.parse(workspaceJson));
3089
- const canonical = await Promise.all(values.map((workspace) => realpath(resolve(workspace))));
3480
+ const canonical = await Promise.all(values.map(async (workspace) => {
3481
+ const path = await realpath(resolve(workspace));
3482
+ if (!(await stat(path)).isDirectory()) {
3483
+ throw new Error(`Not a directory: ${workspace}`);
3484
+ }
3485
+ return path;
3486
+ }));
3090
3487
  return [...new Set(canonical)];
3091
3488
  },
3092
- catch: (cause) => new Error(`Workspace roots must be existing paths; JSON input must be an array of paths: ${cause instanceof Error ? cause.message : String(cause)}`)
3489
+ catch: (cause) => new Error(`Workspace roots must be existing directories; JSON input must be an array of paths: ${cause instanceof Error ? cause.message : String(cause)}`)
3093
3490
  });
3094
3491
  var parseProviders = (input) => Schema4.decodeUnknownSync(ProviderList)([
3095
3492
  ...new Set(input.split(",").map((provider) => provider.trim()).filter(Boolean))
@@ -3267,7 +3664,8 @@ var TaskResult = Schema5.Struct({
3267
3664
  target_device_id: DeviceId,
3268
3665
  bot_id: Schema5.optionalKey(BotId),
3269
3666
  result: Schema5.optionalKey(Schema5.String),
3270
- error: Schema5.optionalKey(Schema5.String)
3667
+ error: Schema5.optionalKey(Schema5.String),
3668
+ inbox_warning: Schema5.optionalKey(Schema5.String)
3271
3669
  });
3272
3670
  var matchingDevices = (devices, target) => {
3273
3671
  const value = target.replace(/^@/, "");
@@ -3321,6 +3719,20 @@ var taskResult = (task) => TaskResult.make({
3321
3719
  ...task.result === undefined ? {} : { result: task.result },
3322
3720
  ...task.error === undefined ? {} : { error: task.error }
3323
3721
  });
3722
+ var acknowledgedTaskResult = async (client, task) => {
3723
+ const result = taskResult(task);
3724
+ const acknowledgementError = await Effect5.runPromise(client.acknowledgeCompletion(task.id)).then(() => {
3725
+ return;
3726
+ }).catch((cause) => {
3727
+ if (cause instanceof RelayRequestError && cause.status === 404)
3728
+ return;
3729
+ return cause instanceof Error ? cause.message : String(cause);
3730
+ });
3731
+ return acknowledgementError === undefined ? result : TaskResult.make({
3732
+ ...result,
3733
+ inbox_warning: `Result received, but the inbox could not be cleared: ${acknowledgementError}`
3734
+ });
3735
+ };
3324
3736
  var createDelegation = Effect5.fn("Cohall.createDelegation")(function* (client, configuration, options) {
3325
3737
  const devices = yield* client.devices();
3326
3738
  if (devices.length === 0) {
@@ -3367,7 +3779,8 @@ var createDelegation = Effect5.fn("Cohall.createDelegation")(function* (client,
3367
3779
  ...options.context === undefined ? {} : { context: options.context },
3368
3780
  ...options.workspace === undefined ? {} : { workspace: options.workspace },
3369
3781
  ...provider === undefined ? {} : { provider },
3370
- ...parentTaskId === undefined ? {} : { parentTaskId }
3782
+ ...parentTaskId === undefined ? {} : { parentTaskId },
3783
+ ...options.attachments === undefined ? {} : { attachments: options.attachments }
3371
3784
  });
3372
3785
  });
3373
3786
  var waitForTask = Effect5.fn("Cohall.waitForTask")(function* (client, initial, timeoutSeconds) {
@@ -4013,7 +4426,7 @@ import {
4013
4426
  realpath as realpath4,
4014
4427
  rename as rename3,
4015
4428
  rm as rm3,
4016
- stat as stat2,
4429
+ stat as stat3,
4017
4430
  writeFile as writeFile5
4018
4431
  } from "node:fs/promises";
4019
4432
  import { basename, dirname as dirname5, join as join6, resolve as resolve2 } from "node:path";
@@ -4021,14 +4434,14 @@ import { backup as sqliteBackup, DatabaseSync } from "node:sqlite";
4021
4434
  import { WebSocket } from "ws";
4022
4435
 
4023
4436
  // apps/device/src/service.ts
4024
- import { execFile as execFile3 } from "node:child_process";
4437
+ import { execFile as execFile2 } from "node:child_process";
4025
4438
  import { chmod as chmod5, mkdir as mkdir4, realpath as realpath3, writeFile as writeFile4 } from "node:fs/promises";
4026
4439
  import { homedir as homedir3 } from "node:os";
4027
4440
  import { dirname as dirname4, join as join5 } from "node:path";
4028
4441
 
4029
4442
  // apps/device/src/upgrade.ts
4030
4443
  import { Schema as Schema8 } from "effect";
4031
- import { execFile as execFile2 } from "node:child_process";
4444
+ import { execa as execa2 } from "execa";
4032
4445
  import { constants as constants2 } from "node:fs";
4033
4446
  import {
4034
4447
  access as access2,
@@ -4038,7 +4451,7 @@ import {
4038
4451
  realpath as realpath2,
4039
4452
  rename as rename2,
4040
4453
  rm as rm2,
4041
- stat,
4454
+ stat as stat2,
4042
4455
  writeFile as writeFile3
4043
4456
  } from "node:fs/promises";
4044
4457
  import { delimiter as delimiter2, dirname as dirname3, extname as extname2, isAbsolute as isAbsolute2, join as join4, relative } from "node:path";
@@ -4139,7 +4552,7 @@ var validateExecutable = async (candidate, installationRoot) => {
4139
4552
  const uid = process.getuid?.();
4140
4553
  const writableRoot = installationRoot === undefined ? undefined : await realpath2(installationRoot);
4141
4554
  for (let path = canonical;; path = dirname3(path)) {
4142
- const metadata = await stat(path);
4555
+ const metadata = await stat2(path);
4143
4556
  const trustedGroupWritablePath = isTrustedGroupWritablePath({
4144
4557
  platform: operatingSystem(),
4145
4558
  canonical,
@@ -4285,16 +4698,23 @@ var serviceCandidates = (runtimePlatform, uid) => {
4285
4698
  return [];
4286
4699
  };
4287
4700
  var defaultRunner = {
4288
- run: (command, arguments_, timeoutMs = 300000) => new Promise((resolve2) => {
4289
- execFile2(command, [...arguments_], { encoding: "utf8", maxBuffer: 1024 * 1024, timeout: timeoutMs, windowsHide: true }, (error, stdout, stderr) => {
4290
- resolve2({
4291
- exitCode: typeof error?.code === "number" ? error.code : error === null ? 0 : 1,
4292
- stdout,
4293
- stderr,
4294
- ...error === null ? {} : { error: error.message }
4295
- });
4701
+ run: async (command, arguments_, timeoutMs = 300000) => {
4702
+ const result = await execa2(command, arguments_, {
4703
+ encoding: "utf8",
4704
+ maxBuffer: 1024 * 1024,
4705
+ timeout: timeoutMs,
4706
+ windowsHide: true,
4707
+ stdin: "ignore",
4708
+ reject: false,
4709
+ stripFinalNewline: false
4296
4710
  });
4297
- })
4711
+ return {
4712
+ exitCode: result.failed ? result.exitCode || 1 : 0,
4713
+ stdout: result.stdout,
4714
+ stderr: result.stderr,
4715
+ ...result.shortMessage === undefined ? {} : { error: result.shortMessage.slice(0, 16384) }
4716
+ };
4717
+ }
4298
4718
  };
4299
4719
  var checked = async (runner, invocation, timeoutMs) => {
4300
4720
  const result = await runner.run(invocation.command, invocation.arguments, timeoutMs);
@@ -4530,8 +4950,20 @@ var upgrade = async (options) => {
4530
4950
 
4531
4951
  // apps/device/src/service.ts
4532
4952
  var xml = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
4533
- var systemdArgument = (value) => `"${value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"")}"`;
4953
+ var systemdArgument = (value) => `"${value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"").replaceAll("%", "%%").replaceAll(`
4954
+ `, "\\n").replaceAll("\r", "\\r")}"`;
4534
4955
  var deviceServicePlan = (options) => {
4956
+ const servicePath = [
4957
+ dirname4(options.nodeExecutable),
4958
+ join5(options.home, ".local", "bin"),
4959
+ join5(options.home, ".npm-global", "bin"),
4960
+ join5(options.home, ".bun", "bin"),
4961
+ options.platform === "darwin" ? join5(options.home, "Library", "pnpm") : join5(options.home, ".local", "share", "pnpm"),
4962
+ ...options.platform === "darwin" ? ["/opt/homebrew/bin"] : [],
4963
+ "/usr/local/bin",
4964
+ "/usr/bin",
4965
+ "/bin"
4966
+ ].join(":");
4535
4967
  if (options.platform === "linux") {
4536
4968
  const path = join5(options.home, ".config", "systemd", "user", "cohall-device.service");
4537
4969
  return {
@@ -4545,7 +4977,8 @@ Wants=network-online.target
4545
4977
 
4546
4978
  [Service]
4547
4979
  Type=simple
4548
- Environment=PATH=%h/.local/bin:%h/.npm-global/bin:%h/.bun/bin:%h/.local/share/pnpm:/usr/local/bin:/usr/bin:/bin
4980
+ Environment=${systemdArgument(`PATH=${servicePath}`)}
4981
+ Environment=${systemdArgument(`COHALL_CONFIG=${options.configPath}`)}
4549
4982
  ExecStart=${systemdArgument(options.entrypoint)} device
4550
4983
  Restart=always
4551
4984
  RestartSec=3
@@ -4559,7 +4992,8 @@ WantedBy=default.target
4559
4992
  },
4560
4993
  commands: [
4561
4994
  { command: "systemctl", arguments: ["--user", "daemon-reload"] },
4562
- { command: "systemctl", arguments: ["--user", "enable", "--now", "cohall-device.service"] }
4995
+ { command: "systemctl", arguments: ["--user", "enable", "cohall-device.service"] },
4996
+ { command: "systemctl", arguments: ["--user", "restart", "cohall-device.service"] }
4563
4997
  ],
4564
4998
  note: "The device starts after login. Run loginctl enable-linger $USER if it must run before login."
4565
4999
  };
@@ -4586,7 +5020,9 @@ WantedBy=default.target
4586
5020
  <key>EnvironmentVariables</key>
4587
5021
  <dict>
4588
5022
  <key>PATH</key>
4589
- <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
5023
+ <string>${xml(servicePath)}</string>
5024
+ <key>COHALL_CONFIG</key>
5025
+ <string>${xml(options.configPath)}</string>
4590
5026
  </dict>
4591
5027
  <key>RunAtLoad</key>
4592
5028
  <true/>
@@ -4618,7 +5054,7 @@ var defaultRunner2 = {
4618
5054
  run: async (command, arguments_, timeoutMs = 60000) => {
4619
5055
  const executable = await trustedExecutable(command);
4620
5056
  return new Promise((resolve2) => {
4621
- execFile3(executable, [...arguments_], { encoding: "utf8", maxBuffer: 1024 * 1024, timeout: timeoutMs, windowsHide: true }, (error, stdout, stderr) => resolve2({
5057
+ execFile2(executable, [...arguments_], { encoding: "utf8", maxBuffer: 1024 * 1024, timeout: timeoutMs, windowsHide: true }, (error, stdout, stderr) => resolve2({
4622
5058
  exitCode: typeof error?.code === "number" ? error.code : error === null ? 0 : 1,
4623
5059
  stdout,
4624
5060
  stderr,
@@ -4646,7 +5082,13 @@ var installDeviceService = async (options = {}) => {
4646
5082
  "-ExecutionPolicy",
4647
5083
  "Bypass",
4648
5084
  "-File",
4649
- script
5085
+ script,
5086
+ "-NodeExecutable",
5087
+ process.execPath,
5088
+ "-Entrypoint",
5089
+ entrypoint,
5090
+ "-ConfigurationPath",
5091
+ configurationPath()
4650
5092
  ]);
4651
5093
  return { installed: "scheduled-task:Cohall Device" };
4652
5094
  }
@@ -4655,6 +5097,8 @@ var installDeviceService = async (options = {}) => {
4655
5097
  platform: platform4,
4656
5098
  entrypoint,
4657
5099
  home: options.home ?? homedir3(),
5100
+ nodeExecutable: process.execPath,
5101
+ configPath: configurationPath(),
4658
5102
  ...uid === undefined ? {} : { uid }
4659
5103
  });
4660
5104
  await mkdir4(dirname4(plan.file.path), { recursive: true, mode: 448 });
@@ -4738,7 +5182,7 @@ var verifyBackupDestinationParent = async (path) => {
4738
5182
  return;
4739
5183
  }
4740
5184
  for (let current = await realpath4(path);; current = dirname5(current)) {
4741
- const metadata = await stat2(current);
5185
+ const metadata = await stat3(current);
4742
5186
  const writableByOthers = (metadata.mode & 18) !== 0;
4743
5187
  const sticky = (metadata.mode & 512) !== 0;
4744
5188
  if (writableByOthers && !sticky) {
@@ -4759,7 +5203,7 @@ var ownerToken = async (dataDirectory) => {
4759
5203
  if (!await exists(path)) {
4760
5204
  throw new Error(`Relay owner token was not found at ${path}`);
4761
5205
  }
4762
- if ((await stat2(path)).size > 512) {
5206
+ if ((await stat3(path)).size > 512) {
4763
5207
  throw new Error("Relay owner token file exceeds 512 bytes");
4764
5208
  }
4765
5209
  return (await readFile3(path, "utf8")).trim();
@@ -4988,10 +5432,110 @@ var switchRelay = async (options) => {
4988
5432
  };
4989
5433
  };
4990
5434
 
5435
+ // apps/device/src/task-attachments.ts
5436
+ init_src();
5437
+ import { Effect as Effect9, Schema as Schema10 } from "effect";
5438
+ import { constants as constants3 } from "node:fs";
5439
+ import { lstat as lstat2, mkdtemp as mkdtemp3, mkdir as mkdir6, open as open3, readdir, rm as rm4, writeFile as writeFile6 } from "node:fs/promises";
5440
+ import { tmpdir as tmpdir2 } from "node:os";
5441
+ import { basename as basename2, join as join7 } from "node:path";
5442
+ var readInputAttachments = async (paths2) => {
5443
+ if (paths2.length > maxTaskAttachments) {
5444
+ throw new Error(`A task supports at most ${maxTaskAttachments} input files`);
5445
+ }
5446
+ const names = new Set;
5447
+ return Promise.all(paths2.map(async (path) => {
5448
+ const name = Schema10.decodeUnknownSync(AttachmentName)(basename2(path));
5449
+ const key = name.normalize("NFC").toLocaleLowerCase("en-US");
5450
+ if (names.has(key)) {
5451
+ throw new Error(`Attachment name ${name} is repeated`);
5452
+ }
5453
+ names.add(key);
5454
+ const source = await lstat2(path);
5455
+ if (!source.isFile()) {
5456
+ throw new Error(`Attachment ${name} must be a regular file`);
5457
+ }
5458
+ const handle = await open3(path, constants3.O_RDONLY | (process.platform === "win32" ? 0 : constants3.O_NOFOLLOW));
5459
+ try {
5460
+ const metadata = await handle.stat();
5461
+ if (!metadata.isFile() || metadata.dev !== source.dev || metadata.ino !== source.ino || metadata.size < 1 || metadata.size > maxAttachmentBytes) {
5462
+ throw new Error(`Attachment ${name} must be a regular file of 1 to ${maxAttachmentBytes} bytes`);
5463
+ }
5464
+ const data = await handle.readFile();
5465
+ if (data.length < 1 || data.length > maxAttachmentBytes) {
5466
+ throw new Error(`Attachment ${name} changed size while reading`);
5467
+ }
5468
+ return { name, data: data.toString("base64") };
5469
+ } finally {
5470
+ await handle.close();
5471
+ }
5472
+ }));
5473
+ };
5474
+ var prepareTaskFiles = async (configuration, taskId, inputNames, signal) => {
5475
+ const client = exports_src.make({ baseUrl: configuration.relayUrl, token: configuration.token });
5476
+ const root = await mkdtemp3(join7(tmpdir2(), "cohall-task-"));
5477
+ const input = join7(root, "input");
5478
+ const output = join7(root, "output");
5479
+ try {
5480
+ await Promise.all([mkdir6(input), mkdir6(output)]);
5481
+ for (const name of inputNames) {
5482
+ const data = await Effect9.runPromise(client.readAttachment(taskId, name), { signal });
5483
+ await writeFile6(join7(input, name), data, { flag: "wx", mode: 384 });
5484
+ }
5485
+ return {
5486
+ input,
5487
+ output,
5488
+ inputNames,
5489
+ collectOutputs: () => collectOutputs(output),
5490
+ cleanup: () => rm4(root, { recursive: true, force: true })
5491
+ };
5492
+ } catch (cause) {
5493
+ await rm4(root, { recursive: true, force: true });
5494
+ throw cause;
5495
+ }
5496
+ };
5497
+ var collectOutputs = async (directory) => {
5498
+ const entries = await readdir(directory, { withFileTypes: true });
5499
+ if (entries.length > maxTaskAttachments) {
5500
+ throw new Error(`A task can return at most ${maxTaskAttachments} files`);
5501
+ }
5502
+ const files = [];
5503
+ const names = new Set;
5504
+ for (const entry of entries) {
5505
+ const name = Schema10.decodeUnknownSync(AttachmentName)(entry.name);
5506
+ if (!entry.isFile()) {
5507
+ throw new Error(`Output ${name} must be a regular file`);
5508
+ }
5509
+ const key = name.normalize("NFC").toLocaleLowerCase("en-US");
5510
+ if (names.has(key)) {
5511
+ throw new Error(`Output ${name} repeats another file name`);
5512
+ }
5513
+ names.add(key);
5514
+ const handle = await open3(join7(directory, name), constants3.O_RDONLY | (process.platform === "win32" ? 0 : constants3.O_NOFOLLOW));
5515
+ try {
5516
+ const metadata = await handle.stat();
5517
+ const pathMetadata = await lstat2(join7(directory, name));
5518
+ if (!metadata.isFile() || !pathMetadata.isFile() || metadata.dev !== pathMetadata.dev || metadata.ino !== pathMetadata.ino || metadata.size < 1 || metadata.size > maxAttachmentBytes) {
5519
+ throw new Error(`Output ${name} must be a regular file of 1 to ${maxAttachmentBytes} bytes`);
5520
+ }
5521
+ const data = await handle.readFile();
5522
+ if (data.length < 1 || data.length > maxAttachmentBytes) {
5523
+ throw new Error(`Output ${name} changed size while reading`);
5524
+ }
5525
+ files.push({ name, data: data.toString("base64") });
5526
+ } finally {
5527
+ await handle.close();
5528
+ }
5529
+ }
5530
+ return files;
5531
+ };
5532
+
4991
5533
  // apps/device/src/cli.ts
4992
5534
  var valueOptions = new Set([
5535
+ "attach",
4993
5536
  "context",
4994
5537
  "context-file",
5538
+ "direction",
4995
5539
  "grok-gateway",
4996
5540
  "error",
4997
5541
  "label",
@@ -4999,6 +5543,7 @@ var valueOptions = new Set([
4999
5543
  "message",
5000
5544
  "message-file",
5001
5545
  "name",
5546
+ "output",
5002
5547
  "parent",
5003
5548
  "prompt",
5004
5549
  "prompt-file",
@@ -5044,10 +5589,13 @@ Usage:
5044
5589
  cohall send [@device-or-bot] [prompt] [delegate options]
5045
5590
  cohall delegate [prompt] [--target @device-or-bot] [--provider provider]
5046
5591
  [--context text] [--thread uuid] [--parent task-id] [--workspace path]
5047
- [--timeout seconds] [--no-wait]
5592
+ [--attach path] [--timeout seconds] [--no-wait]
5048
5593
  cohall status <task-id>
5594
+ cohall inbox [ack <task-id>]
5049
5595
  cohall trace <task-id> [--follow]
5050
5596
  cohall wait <task-id> [--timeout seconds]
5597
+ cohall attachments <task-id>
5598
+ cohall download <task-id> <name> --output path [--direction input|output]
5051
5599
  cohall cancel <task-id>
5052
5600
  cohall reply <task-id> --message-file path | --message text | --message - | --error text
5053
5601
  cohall thread <thread-id>
@@ -5165,7 +5713,7 @@ var readInput = async (arguments_, name) => {
5165
5713
  if (!await access4(path).then(() => true).catch(() => false)) {
5166
5714
  throw new Error(`File does not exist: ${path}`);
5167
5715
  }
5168
- if ((await stat3(path)).size > byteLimit) {
5716
+ if ((await stat4(path)).size > byteLimit) {
5169
5717
  throw new Error(`${name} file exceeds ${byteLimit / 1024} KiB`);
5170
5718
  }
5171
5719
  }
@@ -5200,7 +5748,7 @@ var pairingToken = async (arguments_) => {
5200
5748
  if (path !== undefined && !await access4(path).then(() => true).catch(() => false)) {
5201
5749
  throw new Error(`Pairing token file does not exist: ${path}`);
5202
5750
  }
5203
- if (path !== undefined && (await stat3(path)).size > 512) {
5751
+ if (path !== undefined && (await stat4(path)).size > 512) {
5204
5752
  throw new Error("Pairing token file exceeds 512 bytes");
5205
5753
  }
5206
5754
  const token = (path === undefined ? await readStdin(512, "Pairing token") : await readFile4(path, "utf8")).trim();
@@ -5210,14 +5758,14 @@ var pairingToken = async (arguments_) => {
5210
5758
  return token;
5211
5759
  };
5212
5760
  var client = async () => {
5213
- const configuration = await Effect9.runPromise(loadClientConfiguration);
5761
+ const configuration = await Effect10.runPromise(loadClientConfiguration);
5214
5762
  return {
5215
5763
  configuration,
5216
5764
  relay: exports_src.make({ baseUrl: configuration.relayUrl, token: configuration.token })
5217
5765
  };
5218
5766
  };
5219
5767
  var ownerClient = async () => {
5220
- const configuration = await Effect9.runPromise(loadOwnerConfiguration);
5768
+ const configuration = await Effect10.runPromise(loadOwnerConfiguration);
5221
5769
  return {
5222
5770
  configuration,
5223
5771
  relay: exports_src.make({ baseUrl: configuration.relayUrl, token: configuration.token })
@@ -5225,17 +5773,17 @@ var ownerClient = async () => {
5225
5773
  };
5226
5774
  var installSkill = async (target) => {
5227
5775
  const roots = {
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")
5776
+ agents: join8(homedir4(), ".agents", "skills", "cohall", "SKILL.md"),
5777
+ claude: join8(homedir4(), ".claude", "skills", "cohall", "SKILL.md"),
5778
+ opencode: join8(homedir4(), ".config", "opencode", "skills", "cohall", "SKILL.md")
5231
5779
  };
5232
5780
  const selected = target === "all" ? Object.values(roots) : (target in roots) ? [roots[target]] : undefined;
5233
5781
  if (selected === undefined) {
5234
5782
  throw new Error("Skill target must be agents, claude, opencode, or all");
5235
5783
  }
5236
5784
  for (const path of selected) {
5237
- await mkdir6(dirname6(path), { recursive: true });
5238
- await writeFile6(path, SKILL_default);
5785
+ await mkdir7(dirname6(path), { recursive: true });
5786
+ await writeFile7(path, SKILL_default);
5239
5787
  }
5240
5788
  return selected;
5241
5789
  };
@@ -5245,7 +5793,7 @@ var runCli = async (command, raw) => {
5245
5793
  const arguments_ = parseArguments(raw);
5246
5794
  if (command === "reply") {
5247
5795
  allowOptions(arguments_, ["message", "message-file", "error"]);
5248
- const id = Schema10.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
5796
+ const id = Schema11.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
5249
5797
  const error = option(arguments_, "error");
5250
5798
  if (error !== undefined && (option(arguments_, "message") !== undefined || option(arguments_, "message-file") !== undefined)) {
5251
5799
  throw new Error("Use either --error or --message/--message-file, not both");
@@ -5383,7 +5931,7 @@ var runCli = async (command, raw) => {
5383
5931
  const { relay: relay2 } = await ownerClient();
5384
5932
  const restart = !arguments_.options.has("no-restart");
5385
5933
  if (arguments_.options.has("dry-run")) {
5386
- const devices = await Effect9.runPromise(relay2.devices());
5934
+ const devices = await Effect10.runPromise(relay2.devices());
5387
5935
  assertDeviceOperationSupport(devices);
5388
5936
  print({
5389
5937
  dry_run: true,
@@ -5393,7 +5941,7 @@ var runCli = async (command, raw) => {
5393
5941
  });
5394
5942
  return;
5395
5943
  }
5396
- const operations = await Effect9.runPromise(relay2.createUpgradeOperations({ target: normalizedTarget, restart }));
5944
+ const operations = await Effect10.runPromise(relay2.createUpgradeOperations({ target: normalizedTarget, restart }));
5397
5945
  print({
5398
5946
  queued: operations.length,
5399
5947
  requested_version: normalizedTarget,
@@ -5419,10 +5967,10 @@ var runCli = async (command, raw) => {
5419
5967
  if (arguments_.positionals.length !== 2 || arguments_.positionals[0] !== "abandon") {
5420
5968
  throw new Error("Usage: cohall upgrades [abandon <operation-id>]");
5421
5969
  }
5422
- print(await Effect9.runPromise(relay2.abandonOperation(Schema10.decodeUnknownSync(OperationId)(arguments_.positionals[1]))));
5970
+ print(await Effect10.runPromise(relay2.abandonOperation(Schema11.decodeUnknownSync(OperationId)(arguments_.positionals[1]))));
5423
5971
  return;
5424
5972
  }
5425
- print(await Effect9.runPromise(relay2.operations()));
5973
+ print(await Effect10.runPromise(relay2.operations()));
5426
5974
  return;
5427
5975
  }
5428
5976
  if (command === "join") {
@@ -5501,7 +6049,7 @@ var runCli = async (command, raw) => {
5501
6049
  const existing = await readStoredConfiguration();
5502
6050
  const relayUrl = normalizeRelayUrl(option(arguments_, "relay") ?? existing?.relayUrl ?? "http://127.0.0.1:8787");
5503
6051
  const supplied = values(arguments_, "workspace");
5504
- const workspaces = supplied.length === 0 ? existing?.workspaces ?? [] : await Effect9.runPromise(parseWorkspaces("", JSON.stringify(supplied)));
6052
+ const workspaces = supplied.length === 0 ? existing?.workspaces ?? [] : await Effect10.runPromise(parseWorkspaces("", JSON.stringify(supplied)));
5505
6053
  const sandbox = option(arguments_, "sandbox");
5506
6054
  const model = option(arguments_, "model") ?? existing?.model;
5507
6055
  const gatewayInput = option(arguments_, "grok-gateway") ?? existing?.grokGateway;
@@ -5519,7 +6067,7 @@ var runCli = async (command, raw) => {
5519
6067
  ...model === undefined ? {} : { model },
5520
6068
  ...grokGateway === undefined ? {} : { grokGateway },
5521
6069
  ...(sandbox ?? existing?.sandbox) === undefined ? {} : {
5522
- sandbox: Schema10.decodeUnknownSync(Schema10.Literals(["read-only", "workspace-write", "danger-full-access"]))(sandbox ?? existing?.sandbox)
6070
+ sandbox: Schema11.decodeUnknownSync(Schema11.Literals(["read-only", "workspace-write", "danger-full-access"]))(sandbox ?? existing?.sandbox)
5523
6071
  }
5524
6072
  });
5525
6073
  await writeStoredConfiguration(configuration2);
@@ -5562,7 +6110,7 @@ var runCli = async (command, raw) => {
5562
6110
  noPositionals(arguments_, command);
5563
6111
  if (arguments_.options.has("all")) {
5564
6112
  const { relay: relay2 } = await client();
5565
- print(allDeviceHealth(await Effect9.runPromise(relay2.devices()), version));
6113
+ print(allDeviceHealth(await Effect10.runPromise(relay2.devices()), version));
5566
6114
  return;
5567
6115
  }
5568
6116
  const configuration2 = await readStoredConfiguration();
@@ -5586,7 +6134,7 @@ var runCli = async (command, raw) => {
5586
6134
  const clientToken = process.env.COHALL_CLIENT_TOKEN ?? storedCredentials.clientToken;
5587
6135
  const hasDeviceCredential = process.env.COHALL_DEVICE_TOKEN !== undefined || storedCredentials.deviceToken !== undefined;
5588
6136
  const deviceId = process.env.COHALL_DEVICE_ID ?? configuration2?.deviceId;
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(() => {
6137
+ const currentDevice = response?.ok !== true || clientToken === undefined || deviceId === undefined ? undefined : await Effect10.runPromise(exports_src.make({ baseUrl: relayUrl, token: clientToken }).devices()).then((devices) => devices.find((device) => device.id === deviceId)).catch(() => {
5590
6138
  return;
5591
6139
  });
5592
6140
  const versionWarning = deviceVersionWarning(version, currentDevice?.version);
@@ -5628,7 +6176,7 @@ var runCli = async (command, raw) => {
5628
6176
  allowOptions(arguments_, []);
5629
6177
  noPositionals(arguments_, command);
5630
6178
  const { relay: relay2 } = await client();
5631
- print(deviceVersions(await Effect9.runPromise(relay2.devices()), version));
6179
+ print(deviceVersions(await Effect10.runPromise(relay2.devices()), version));
5632
6180
  return;
5633
6181
  }
5634
6182
  if (command === "usage") {
@@ -5636,7 +6184,7 @@ var runCli = async (command, raw) => {
5636
6184
  noPositionals(arguments_, command);
5637
6185
  const { relay: relay2 } = await client();
5638
6186
  print({
5639
- ...await Effect9.runPromise(relay2.usage()),
6187
+ ...await Effect10.runPromise(relay2.usage()),
5640
6188
  scope: "Retained Cohall task activity; provider token and billing usage are not available."
5641
6189
  });
5642
6190
  return;
@@ -5647,7 +6195,7 @@ var runCli = async (command, raw) => {
5647
6195
  allowOptions(arguments_, ["client-only", "label"]);
5648
6196
  noPositionals(arguments_, command);
5649
6197
  const roles = arguments_.options.has("client-only") ? ["client"] : ["client", "device"];
5650
- const credential = await Effect9.runPromise(relay2.createPairing({ label: option(arguments_, "label") ?? "Cohall device", roles }));
6198
+ const credential = await Effect10.runPromise(relay2.createPairing({ label: option(arguments_, "label") ?? "Cohall device", roles }));
5651
6199
  print({
5652
6200
  relay_url: configuration2.relayUrl,
5653
6201
  pairing_token: credential.token,
@@ -5659,35 +6207,36 @@ var runCli = async (command, raw) => {
5659
6207
  if (command === "sessions") {
5660
6208
  allowOptions(arguments_, []);
5661
6209
  noPositionals(arguments_, command);
5662
- print(await Effect9.runPromise(relay2.authSessions()));
6210
+ print(await Effect10.runPromise(relay2.authSessions()));
5663
6211
  return;
5664
6212
  }
5665
6213
  if (command === "forget") {
5666
6214
  allowOptions(arguments_, []);
5667
- print(await Effect9.runPromise(relay2.forgetDevice(Schema10.decodeUnknownSync(DeviceId)(identifier(arguments_, "device id")))));
6215
+ print(await Effect10.runPromise(relay2.forgetDevice(Schema11.decodeUnknownSync(DeviceId)(identifier(arguments_, "device id")))));
5668
6216
  return;
5669
6217
  }
5670
6218
  allowOptions(arguments_, []);
5671
- print(await Effect9.runPromise(relay2.revokeAuthSession(Schema10.decodeUnknownSync(AuthSessionId)(identifier(arguments_, "session id")))));
6219
+ print(await Effect10.runPromise(relay2.revokeAuthSession(Schema11.decodeUnknownSync(AuthSessionId)(identifier(arguments_, "session id")))));
5672
6220
  return;
5673
6221
  }
5674
6222
  const { configuration, relay } = await client();
5675
6223
  if (command === "devices") {
5676
6224
  allowOptions(arguments_, []);
5677
6225
  noPositionals(arguments_, command);
5678
- print(await Effect9.runPromise(relay.devices()));
6226
+ print(await Effect10.runPromise(relay.devices()));
5679
6227
  return;
5680
6228
  }
5681
6229
  if (command === "bots") {
5682
6230
  allowOptions(arguments_, []);
5683
6231
  noPositionals(arguments_, command);
5684
- print(listBots(await Effect9.runPromise(relay.devices())));
6232
+ print(listBots(await Effect10.runPromise(relay.devices())));
5685
6233
  return;
5686
6234
  }
5687
6235
  if (command === "delegate" || command === "send") {
5688
6236
  allowOptions(arguments_, [
5689
6237
  "context",
5690
6238
  "context-file",
6239
+ "attach",
5691
6240
  "no-wait",
5692
6241
  "parent",
5693
6242
  "prompt",
@@ -5698,6 +6247,7 @@ var runCli = async (command, raw) => {
5698
6247
  "timeout",
5699
6248
  "workspace"
5700
6249
  ]);
6250
+ const waitSeconds = timeout(arguments_);
5701
6251
  const positionalTarget = command === "send" && arguments_.positionals[0]?.startsWith("@") ? arguments_.positionals[0] : undefined;
5702
6252
  const positionals = positionalTarget === undefined ? arguments_.positionals : arguments_.positionals.slice(1);
5703
6253
  if (positionalTarget !== undefined && option(arguments_, "target") !== undefined) {
@@ -5715,56 +6265,96 @@ var runCli = async (command, raw) => {
5715
6265
  throw new Error("Use either a positional prompt or --prompt, not both");
5716
6266
  }
5717
6267
  const context = await readInput(arguments_, "context");
6268
+ const attachments2 = await readInputAttachments(values(arguments_, "attach"));
5718
6269
  const target = positionalTarget ?? option(arguments_, "target");
5719
6270
  const thread = option(arguments_, "thread");
5720
6271
  const workspace = option(arguments_, "workspace");
5721
6272
  const provider = option(arguments_, "provider");
5722
6273
  const parent = option(arguments_, "parent");
5723
- const task = await Effect9.runPromise(createDelegation(relay, configuration, {
6274
+ const task = await Effect10.runPromise(createDelegation(relay, configuration, {
5724
6275
  prompt,
5725
6276
  ...target === undefined ? {} : { target },
5726
6277
  ...context === undefined ? {} : { context },
5727
- ...thread === undefined ? {} : { threadId: Schema10.decodeUnknownSync(ThreadId)(thread) },
5728
- ...parent === undefined ? {} : { parentTaskId: Schema10.decodeUnknownSync(TaskId)(parent) },
6278
+ ...thread === undefined ? {} : { threadId: Schema11.decodeUnknownSync(ThreadId)(thread) },
6279
+ ...parent === undefined ? {} : { parentTaskId: Schema11.decodeUnknownSync(TaskId)(parent) },
5729
6280
  ...workspace === undefined ? {} : { workspace },
5730
- ...provider === undefined ? {} : { provider: Schema10.decodeUnknownSync(Provider)(provider) }
6281
+ ...provider === undefined ? {} : { provider: Schema11.decodeUnknownSync(Provider)(provider) },
6282
+ ...attachments2.length === 0 ? {} : { attachments: attachments2 }
5731
6283
  }));
5732
- print(taskResult(arguments_.options.has("no-wait") ? task : await Effect9.runPromise(waitForTask(relay, task, timeout(arguments_)))));
6284
+ const completed = arguments_.options.has("no-wait") ? task : await Effect10.runPromise(waitForTask(relay, task, waitSeconds));
6285
+ print(arguments_.options.has("no-wait") ? taskResult(completed) : await acknowledgedTaskResult(relay, completed));
6286
+ return;
6287
+ }
6288
+ if (command === "inbox") {
6289
+ allowOptions(arguments_, []);
6290
+ if (arguments_.positionals[0] === "ack") {
6291
+ if (arguments_.positionals.length !== 2)
6292
+ throw new Error("Usage: cohall inbox ack <task-id>");
6293
+ const id = Schema11.decodeUnknownSync(TaskId)(arguments_.positionals[1]);
6294
+ print(await Effect10.runPromise(relay.acknowledgeCompletion(id)));
6295
+ return;
6296
+ }
6297
+ noPositionals(arguments_, command);
6298
+ print(await Effect10.runPromise(relay.inbox()));
5733
6299
  return;
5734
6300
  }
5735
6301
  if (command === "status") {
5736
6302
  allowOptions(arguments_, []);
5737
- const id = Schema10.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
5738
- print(taskResult(await Effect9.runPromise(relay.getTask(id))));
6303
+ const id = Schema11.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
6304
+ print(taskResult(await Effect10.runPromise(relay.getTask(id))));
6305
+ return;
6306
+ }
6307
+ if (command === "attachments") {
6308
+ allowOptions(arguments_, []);
6309
+ const id = Schema11.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
6310
+ print(await Effect10.runPromise(relay.listAttachments(id)));
6311
+ return;
6312
+ }
6313
+ if (command === "download") {
6314
+ allowOptions(arguments_, ["output", "direction"]);
6315
+ if (arguments_.positionals.length !== 2) {
6316
+ throw new Error("Usage: cohall download <task-id> <name> --output path [--direction input|output]");
6317
+ }
6318
+ const destination = option(arguments_, "output");
6319
+ if (destination === undefined) {
6320
+ throw new Error("--output is required");
6321
+ }
6322
+ const id = Schema11.decodeUnknownSync(TaskId)(arguments_.positionals[0]);
6323
+ const name = Schema11.decodeUnknownSync(AttachmentName)(arguments_.positionals[1]);
6324
+ const directionInput = option(arguments_, "direction");
6325
+ const direction = directionInput === undefined ? undefined : Schema11.decodeUnknownSync(AttachmentDirection)(directionInput);
6326
+ const data = await Effect10.runPromise(relay.readAttachment(id, name, direction));
6327
+ await writeFile7(destination, data, { flag: "wx", mode: 384 });
6328
+ print({ task_id: id, name, path: resolve3(destination), bytes: data.length });
5739
6329
  return;
5740
6330
  }
5741
6331
  if (command === "trace") {
5742
6332
  allowOptions(arguments_, ["follow"]);
5743
- const id = Schema10.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
6333
+ const id = Schema11.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
5744
6334
  if (arguments_.options.has("follow")) {
5745
- await Effect9.runPromise(followTaskTrace(relay, id, printLine));
6335
+ await Effect10.runPromise(followTaskTrace(relay, id, printLine));
5746
6336
  return;
5747
6337
  }
5748
- print(await Effect9.runPromise(relay.traceTask(id)));
6338
+ print(await Effect10.runPromise(relay.traceTask(id)));
5749
6339
  return;
5750
6340
  }
5751
6341
  if (command === "wait") {
5752
6342
  allowOptions(arguments_, ["timeout"]);
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_)))));
6343
+ const id = Schema11.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
6344
+ const task = await Effect10.runPromise(relay.getTask(id));
6345
+ print(taskResult(await Effect10.runPromise(waitForTask(relay, task, timeout(arguments_)))));
5756
6346
  return;
5757
6347
  }
5758
6348
  if (command === "cancel") {
5759
6349
  allowOptions(arguments_, []);
5760
- const id = Schema10.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
5761
- print(taskResult(await Effect9.runPromise(relay.cancelTask(id))));
6350
+ const id = Schema11.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
6351
+ print(taskResult(await Effect10.runPromise(relay.cancelTask(id))));
5762
6352
  return;
5763
6353
  }
5764
6354
  if (command === "thread") {
5765
6355
  allowOptions(arguments_, []);
5766
- const id = Schema10.decodeUnknownSync(ThreadId)(identifier(arguments_, "thread id"));
5767
- print(await Effect9.runPromise(threadContext(relay, id)));
6356
+ const id = Schema11.decodeUnknownSync(ThreadId)(identifier(arguments_, "thread id"));
6357
+ print(await Effect10.runPromise(threadContext(relay, id)));
5768
6358
  return;
5769
6359
  }
5770
6360
  throw new Error(`Unknown Cohall command: ${command}`);
@@ -5772,15 +6362,15 @@ var runCli = async (command, raw) => {
5772
6362
 
5773
6363
  // apps/device/src/daemon.ts
5774
6364
  init_src();
5775
- import { Effect as Effect10, Schedule as Schedule3, Schema as Schema11 } from "effect";
5776
- import { constants as constants3 } from "node:fs";
5777
- import { open as open3, realpath as realpath5, stat as stat4 } from "node:fs/promises";
6365
+ import { Effect as Effect11, Schedule as Schedule3, Schema as Schema12 } from "effect";
6366
+ import { constants as constants4 } from "node:fs";
6367
+ import { open as open4, realpath as realpath5, stat as stat5 } from "node:fs/promises";
5778
6368
  import { arch, hostname as hostname3, platform as platform4 } from "node:os";
5779
- import { basename as basename2, isAbsolute as isAbsolute3, relative as relative2 } from "node:path";
6369
+ import { basename as basename3, isAbsolute as isAbsolute3, relative as relative2 } from "node:path";
5780
6370
  import { WebSocket as WebSocket2 } from "ws";
5781
6371
  var maxQueuedRelayMessages = 8;
5782
6372
 
5783
- class DeviceConnectionError extends Schema11.TaggedErrorClass()("Device.ConnectionError", { message: Schema11.String }) {
6373
+ class DeviceConnectionError extends Schema12.TaggedErrorClass()("Device.ConnectionError", { message: Schema12.String }) {
5784
6374
  }
5785
6375
  var socketUrl = (configuration) => {
5786
6376
  const url = new URL(configuration.relayUrl);
@@ -5806,6 +6396,9 @@ var capabilities = (providers) => {
5806
6396
  label: providerLabel(provider),
5807
6397
  detail: provider === "grok-bot" ? "Named bots through this computer's local Grok Bot gateway" : `${providerLabel(provider)} executable detected; authentication is checked when work starts`
5808
6398
  }));
6399
+ if (providers.some((provider) => provider !== "grok-bot")) {
6400
+ values2.push({ id: "task-attachments", label: "Task file attachments" });
6401
+ }
5809
6402
  if (findExecutable("google-chrome") !== undefined || findExecutable("chromium") !== undefined || platform4() === "darwin") {
5810
6403
  values2.push({ id: "browser-session", label: "Signed-in browser" });
5811
6404
  }
@@ -5838,7 +6431,7 @@ var describeDevice = (configuration, status = "online", bots = []) => {
5838
6431
  capabilities: capabilities(providers),
5839
6432
  workspaces: configuration.workspaces.map((path) => ({
5840
6433
  path,
5841
- label: basename2(path) || path
6434
+ label: basename3(path) || path
5842
6435
  })),
5843
6436
  version,
5844
6437
  lastSeenAt: now()
@@ -5862,8 +6455,8 @@ var allowedWorkspace = async (configuration, requested) => {
5862
6455
  var openAllowedWorkspace = async (configuration, requested) => {
5863
6456
  const path = await allowedWorkspace(configuration, requested);
5864
6457
  const operatingSystem2 = platform4();
5865
- const flags = constants3.O_RDONLY | (operatingSystem2 === "win32" ? 0 : constants3.O_DIRECTORY | constants3.O_NOFOLLOW);
5866
- const handle = await open3(path, flags);
6458
+ const flags = constants4.O_RDONLY | (operatingSystem2 === "win32" ? 0 : constants4.O_DIRECTORY | constants4.O_NOFOLLOW);
6459
+ const handle = await open4(path, flags);
5867
6460
  const identity = await handle.stat();
5868
6461
  if (!identity.isDirectory()) {
5869
6462
  await handle.close();
@@ -5879,7 +6472,7 @@ var openAllowedWorkspace = async (configuration, requested) => {
5879
6472
  }
5880
6473
  if (operatingSystem2 !== "linux") {
5881
6474
  const currentPath = await realpath5(path);
5882
- const currentPathIdentity = await stat4(currentPath);
6475
+ const currentPathIdentity = await stat5(currentPath);
5883
6476
  if (currentPathIdentity.dev !== identity.dev || currentPathIdentity.ino !== identity.ino) {
5884
6477
  throw new Error(`Workspace ${path} changed before provider startup`);
5885
6478
  }
@@ -5888,7 +6481,7 @@ var openAllowedWorkspace = async (configuration, requested) => {
5888
6481
  close: () => handle.close()
5889
6482
  };
5890
6483
  };
5891
- var promptFor = (task, deviceName) => {
6484
+ var promptFor = (task, deviceName, files) => {
5892
6485
  const context = task.context === undefined ? "" : `
5893
6486
 
5894
6487
  Relevant context supplied by the sending agent:
@@ -5898,6 +6491,14 @@ ${task.context}`;
5898
6491
  "Complete the delegated task using this device's local workspace, tools, credentials, and signed-in services.",
5899
6492
  "Never read, reveal, copy, or use Cohall configuration files or Cohall authentication tokens.",
5900
6493
  "Return a concise, complete result with the evidence the sending agent needs.",
6494
+ ...files === undefined ? [] : [
6495
+ ...files.inputNames.length === 0 ? [] : [
6496
+ `Input directory: ${files.input}`,
6497
+ `Input file names: ${files.inputNames.join(", ")}`
6498
+ ],
6499
+ `Output directory: ${files.output}`,
6500
+ "To return files, write at most 2 regular files of up to 256 KiB each in the output directory and mention them in your result."
6501
+ ],
5901
6502
  `
5902
6503
  Task:
5903
6504
  ${task.prompt}${context}`
@@ -5909,11 +6510,25 @@ var send = (state, event) => {
5909
6510
  state.socket.send(JSON.stringify(event));
5910
6511
  }
5911
6512
  };
6513
+ var omitOutputFiles = (event, note) => {
6514
+ if (event._tag !== "TaskFinished" || (event.attachments?.length ?? 0) === 0) {
6515
+ return event;
6516
+ }
6517
+ const message = `
6518
+
6519
+ [Output files omitted: ${note}]`;
6520
+ return SocketEvent.make({
6521
+ _tag: "TaskFinished",
6522
+ taskId: event.taskId,
6523
+ result: `${event.result.slice(0, 131072 - message.length)}${message}`,
6524
+ ...event.providerSessionId === undefined ? {} : { providerSessionId: event.providerSessionId }
6525
+ });
6526
+ };
5912
6527
  var sendTerminal = (state, taskId, event) => {
5913
- const payload = JSON.stringify(event);
5914
- state.terminal.set(taskId, payload);
6528
+ const safeEvent = state.supportsAttachments ? event : omitOutputFiles(event, "the relay does not support attachments. Upgrade the relay and retry.");
6529
+ state.terminal.set(taskId, safeEvent);
5915
6530
  if (state.socket?.readyState === WebSocket2.OPEN) {
5916
- state.socket.send(payload);
6531
+ state.socket.send(JSON.stringify(safeEvent));
5917
6532
  }
5918
6533
  };
5919
6534
  var sendOperationTerminal = (state, operationId, event) => {
@@ -5976,9 +6591,9 @@ var execute = (configuration, state, task) => {
5976
6591
  const controller = new AbortController;
5977
6592
  state.tasks.set(task.id, { task, controller });
5978
6593
  send(state, SocketEvent.make({ _tag: "TaskAccepted", taskId: task.id }));
5979
- const workflow = Effect10.gen(function* () {
6594
+ const workflow = Effect11.gen(function* () {
5980
6595
  if (task.provider === "grok-bot") {
5981
- return yield* Effect10.tryPromise({
6596
+ return yield* Effect11.tryPromise({
5982
6597
  try: (signal) => runGrokBot(configuration.grokGateway, task, signal),
5983
6598
  catch: (cause) => new ProviderRunError({
5984
6599
  provider: task.provider,
@@ -5986,39 +6601,64 @@ var execute = (configuration, state, task) => {
5986
6601
  })
5987
6602
  });
5988
6603
  }
5989
- const workspace = yield* Effect10.tryPromise({
6604
+ const provider = task.provider;
6605
+ if (!state.supportsAttachments && (task.inputAttachmentNames?.length ?? 0) > 0) {
6606
+ return yield* new ProviderRunError({
6607
+ provider,
6608
+ message: "The relay does not support task file attachments"
6609
+ });
6610
+ }
6611
+ const workspace = yield* Effect11.tryPromise({
5990
6612
  try: () => openAllowedWorkspace(configuration, task.workspace),
5991
6613
  catch: (cause) => new ProviderRunError({
5992
- provider: task.provider,
6614
+ provider,
5993
6615
  message: cause instanceof Error ? cause.message : String(cause)
5994
6616
  })
5995
6617
  });
5996
- const sessionId = task.providerSessionId ?? state.sessions.get(sessionKey(task));
5997
- return yield* run({
5998
- provider: task.provider,
5999
- threadId: task.threadId,
6000
- taskId: task.id,
6001
- prompt: promptFor(task, configuration.name),
6002
- cwd: workspace.cwd,
6003
- beforeSpawn: workspace.validate,
6004
- ...sessionId === undefined ? {} : { sessionId },
6005
- ...configuration.model === undefined ? {} : { model: configuration.model },
6006
- ...configuration.sandbox === undefined ? {} : { sandbox: configuration.sandbox }
6007
- }).pipe(Effect10.ensuring(Effect10.promise(() => workspace.close().catch(() => {
6618
+ return yield* Effect11.gen(function* () {
6619
+ const files = state.supportsAttachments ? yield* Effect11.tryPromise({
6620
+ try: (signal) => prepareTaskFiles(configuration, task.id, task.inputAttachmentNames ?? [], signal),
6621
+ catch: (cause) => new ProviderRunError({
6622
+ provider,
6623
+ message: cause instanceof Error ? cause.message : String(cause)
6624
+ })
6625
+ }) : undefined;
6626
+ const sessionId = task.providerSessionId ?? state.sessions.get(sessionKey(task));
6627
+ return yield* run({
6628
+ provider,
6629
+ threadId: task.threadId,
6630
+ taskId: task.id,
6631
+ prompt: promptFor(task, configuration.name, files),
6632
+ cwd: workspace.cwd,
6633
+ beforeSpawn: workspace.validate,
6634
+ ...sessionId === undefined ? {} : { sessionId },
6635
+ ...configuration.model === undefined ? {} : { model: configuration.model },
6636
+ ...configuration.sandbox === undefined ? {} : { sandbox: configuration.sandbox }
6637
+ }).pipe(Effect11.flatMap((result) => files === undefined ? Effect11.succeed(result) : Effect11.tryPromise({
6638
+ try: async () => ({ ...result, attachments: await files.collectOutputs() }),
6639
+ catch: (cause) => new ProviderRunError({
6640
+ provider,
6641
+ message: cause instanceof Error ? cause.message : String(cause)
6642
+ })
6643
+ })), Effect11.ensuring(files === undefined ? Effect11.void : Effect11.promise(() => files.cleanup())));
6644
+ }).pipe(Effect11.ensuring(Effect11.promise(() => workspace.close().catch(() => {
6008
6645
  return;
6009
6646
  }))));
6010
6647
  });
6011
- Effect10.runPromise(workflow, { signal: controller.signal }).then((result) => {
6648
+ Effect11.runPromise(workflow, { signal: controller.signal }).then((result) => {
6649
+ const finished = SocketEvent.make({
6650
+ _tag: "TaskFinished",
6651
+ taskId: task.id,
6652
+ result: result.result,
6653
+ ...result.sessionId === undefined ? {} : { providerSessionId: result.sessionId },
6654
+ ...result.attachments === undefined || result.attachments.length === 0 ? {} : { attachments: result.attachments }
6655
+ });
6656
+ const bounded2 = Buffer.byteLength(JSON.stringify(finished)) > maxSocketPayloadBytes ? omitOutputFiles(finished, "the combined result exceeded the 1 MiB transfer limit. Shorten the result or files and retry.") : finished;
6012
6657
  if (result.sessionId !== undefined) {
6013
6658
  state.sessions.set(sessionKey(task), result.sessionId);
6014
6659
  }
6015
6660
  remember(state, task.id);
6016
- sendTerminal(state, task.id, SocketEvent.make({
6017
- _tag: "TaskFinished",
6018
- taskId: task.id,
6019
- result: result.result,
6020
- ...result.sessionId === undefined ? {} : { providerSessionId: result.sessionId }
6021
- }));
6661
+ sendTerminal(state, task.id, bounded2);
6022
6662
  }).catch((cause) => {
6023
6663
  remember(state, task.id);
6024
6664
  sendTerminal(state, task.id, controller.signal.aborted ? SocketEvent.make({ _tag: "TaskCancelled", taskId: task.id }) : SocketEvent.make({
@@ -6109,7 +6749,7 @@ var cancel = (state, taskId) => {
6109
6749
  remember(state, taskId);
6110
6750
  sendTerminal(state, taskId, SocketEvent.make({ _tag: "TaskCancelled", taskId }));
6111
6751
  };
6112
- var connect = (configuration, state) => Effect10.tryPromise({
6752
+ var connect = (configuration, state) => Effect11.tryPromise({
6113
6753
  try: (signal) => new Promise((complete) => {
6114
6754
  const socket = new WebSocket2(socketUrl(configuration), {
6115
6755
  maxPayload: maxSocketPayloadBytes,
@@ -6174,18 +6814,21 @@ var connect = (configuration, state) => Effect10.tryPromise({
6174
6814
  return;
6175
6815
  }
6176
6816
  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({
6817
+ const event = await Effect11.runPromise(Effect11.try({
6178
6818
  try: () => JSON.parse(text3),
6179
6819
  catch: () => new DeviceConnectionError({ message: "Relay sent invalid JSON" })
6180
- }).pipe(Effect10.flatMap(decodeSocketEvent)));
6820
+ }).pipe(Effect11.flatMap(decodeSocketEvent)));
6181
6821
  if (event._tag === "Connected") {
6822
+ state.supportsAttachments = event.taskAttachments === true;
6182
6823
  state.socket = socket;
6183
6824
  socket.send(JSON.stringify(SocketEvent.make({
6184
6825
  _tag: "DeviceHello",
6185
6826
  device: describeDevice(configuration, state.tasks.size > 0 || state.operation !== undefined ? "busy" : "online", bots)
6186
6827
  })));
6187
- for (const payload of state.terminal.values()) {
6188
- socket.send(payload);
6828
+ for (const [taskId, pending] of state.terminal) {
6829
+ const safeEvent = state.supportsAttachments ? pending : omitOutputFiles(pending, "the relay does not support attachments. Upgrade the relay and retry.");
6830
+ state.terminal.set(taskId, safeEvent);
6831
+ socket.send(JSON.stringify(safeEvent));
6189
6832
  }
6190
6833
  for (const payload of state.operationTerminal.values()) {
6191
6834
  socket.send(payload);
@@ -6235,6 +6878,7 @@ var connect = (configuration, state) => Effect10.tryPromise({
6235
6878
  var runDaemon = (configuration) => {
6236
6879
  const state = {
6237
6880
  socket: undefined,
6881
+ supportsAttachments: false,
6238
6882
  processing: Promise.resolve(),
6239
6883
  terminal: new Map,
6240
6884
  queue: [],
@@ -6246,7 +6890,7 @@ var runDaemon = (configuration) => {
6246
6890
  operationTerminal: new Map,
6247
6891
  completedOperations: new Set
6248
6892
  };
6249
- return connect(configuration, state).pipe(Effect10.repeat({ schedule: Schedule3.spaced("2 seconds") }), Effect10.ensuring(Effect10.sync(() => {
6893
+ return connect(configuration, state).pipe(Effect11.repeat({ schedule: Schedule3.spaced("2 seconds") }), Effect11.ensuring(Effect11.sync(() => {
6250
6894
  state.socket?.close();
6251
6895
  for (const { controller } of state.tasks.values()) {
6252
6896
  controller.abort();
@@ -6258,8 +6902,9 @@ var runDaemon = (configuration) => {
6258
6902
  init_src();
6259
6903
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6260
6904
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6261
- import { Effect as Effect11 } from "effect";
6905
+ import { Effect as Effect12, Schema as Schema13 } from "effect";
6262
6906
  import * as z from "zod/v4";
6907
+ import { writeFile as writeFile8 } from "node:fs/promises";
6263
6908
  var output = (value) => ({
6264
6909
  content: [{ type: "text", text: JSON.stringify(value, null, 2) }]
6265
6910
  });
@@ -6273,12 +6918,12 @@ var runMcp = async (configuration) => {
6273
6918
  title: "List Cohall devices",
6274
6919
  description: "List devices, their availability, local providers, capabilities, and allowed workspaces.",
6275
6920
  inputSchema: {}
6276
- }, async () => output(await Effect11.runPromise(client2.devices())));
6921
+ }, async () => output(await Effect12.runPromise(client2.devices())));
6277
6922
  server.registerTool("list_bots", {
6278
6923
  title: "List Cohall bots",
6279
6924
  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
6925
  inputSchema: {}
6281
- }, async () => output(listBots(await Effect11.runPromise(client2.devices()))));
6926
+ }, async () => output(listBots(await Effect12.runPromise(client2.devices()))));
6282
6927
  server.registerTool("delegate", {
6283
6928
  title: "Send work to a device or bot",
6284
6929
  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.",
@@ -6290,6 +6935,7 @@ var runMcp = async (configuration) => {
6290
6935
  thread_id: z.string().uuid().optional(),
6291
6936
  parent_task_id: z.string().uuid().optional().describe("The task delegating this work. Inherited from COHALL_TASK_ID when present."),
6292
6937
  workspace: z.string().max(4096).optional().describe("Coding provider workspace. Omit for named Grok Bots."),
6938
+ attachment_paths: z.array(z.string()).max(2).optional().describe("Local files to send with the task, up to 2 files of 256 KiB each. Coding providers only."),
6293
6939
  wait: z.boolean().default(true),
6294
6940
  timeout_seconds: z.number().int().min(5).max(86400).default(900)
6295
6941
  }
@@ -6301,30 +6947,65 @@ var runMcp = async (configuration) => {
6301
6947
  thread_id,
6302
6948
  parent_task_id,
6303
6949
  workspace,
6950
+ attachment_paths,
6304
6951
  wait,
6305
6952
  timeout_seconds
6306
6953
  }) => {
6307
- const task = await Effect11.runPromise(createDelegation(client2, configuration, {
6954
+ const attachments2 = await readInputAttachments(attachment_paths ?? []);
6955
+ const task = await Effect12.runPromise(createDelegation(client2, configuration, {
6308
6956
  prompt,
6309
6957
  ...provider === undefined ? {} : { provider },
6310
6958
  ...target === undefined ? {} : { target },
6311
6959
  ...context === undefined ? {} : { context },
6312
6960
  ...thread_id === undefined ? {} : { threadId: ThreadId.make(thread_id) },
6313
6961
  ...parent_task_id === undefined ? {} : { parentTaskId: TaskId.make(parent_task_id) },
6314
- ...workspace === undefined ? {} : { workspace }
6962
+ ...workspace === undefined ? {} : { workspace },
6963
+ ...attachments2.length === 0 ? {} : { attachments: attachments2 }
6315
6964
  }));
6316
- return output(taskResult(wait ? await Effect11.runPromise(waitForTask(client2, task, timeout_seconds)) : task));
6965
+ const completed = wait ? await Effect12.runPromise(waitForTask(client2, task, timeout_seconds)) : task;
6966
+ return output(wait ? await acknowledgedTaskResult(client2, completed) : taskResult(completed));
6317
6967
  });
6968
+ server.registerTool("completion_inbox", {
6969
+ title: "List completed work",
6970
+ description: "List up to 20 unacknowledged completed tasks sent by this client. hasMore signals additional entries. Use task_status for a full result, then acknowledge the task after handling it.",
6971
+ inputSchema: {}
6972
+ }, async () => output(await Effect12.runPromise(client2.inbox())));
6973
+ server.registerTool("acknowledge_completion", {
6974
+ title: "Acknowledge completed work",
6975
+ description: "Remove a handled task from this client's completion inbox.",
6976
+ inputSchema: { task_id: z.string().uuid() }
6977
+ }, async ({ task_id }) => output(await Effect12.runPromise(client2.acknowledgeCompletion(TaskId.make(task_id)))));
6318
6978
  server.registerTool("task_status", {
6319
6979
  title: "Get delegated task status",
6320
6980
  description: "Read the current state and result of a Cohall task.",
6321
6981
  inputSchema: { task_id: z.string().uuid() }
6322
- }, async ({ task_id }) => output(taskResult(await Effect11.runPromise(client2.getTask(TaskId.make(task_id))))));
6982
+ }, async ({ task_id }) => output(taskResult(await Effect12.runPromise(client2.getTask(TaskId.make(task_id))))));
6983
+ server.registerTool("list_task_attachments", {
6984
+ title: "List task files",
6985
+ description: "List input and output files retained with a delegated task.",
6986
+ inputSchema: { task_id: z.string().uuid() }
6987
+ }, async ({ task_id }) => output(await Effect12.runPromise(client2.listAttachments(TaskId.make(task_id)))));
6988
+ server.registerTool("download_task_attachment", {
6989
+ title: "Download a task file",
6990
+ description: "Save one task file to a new local path. Existing files are never overwritten.",
6991
+ inputSchema: {
6992
+ task_id: z.string().uuid(),
6993
+ name: z.string(),
6994
+ output_path: z.string(),
6995
+ direction: z.enum(["input", "output"]).optional()
6996
+ }
6997
+ }, async ({ task_id, name, output_path, direction }) => {
6998
+ const safeName = Schema13.decodeUnknownSync(AttachmentName)(name);
6999
+ const safeDirection = direction === undefined ? undefined : Schema13.decodeUnknownSync(AttachmentDirection)(direction);
7000
+ const data = await Effect12.runPromise(client2.readAttachment(TaskId.make(task_id), safeName, safeDirection));
7001
+ await writeFile8(output_path, data, { flag: "wx", mode: 384 });
7002
+ return output({ task_id, name: safeName, output_path, bytes: data.length });
7003
+ });
6323
7004
  server.registerTool("task_trace", {
6324
7005
  title: "Trace a delegated task",
6325
7006
  description: "Read the redacted relay and device lifecycle for troubleshooting a Cohall task.",
6326
7007
  inputSchema: { task_id: z.string().uuid() }
6327
- }, async ({ task_id }) => output(await Effect11.runPromise(client2.traceTask(TaskId.make(task_id)))));
7008
+ }, async ({ task_id }) => output(await Effect12.runPromise(client2.traceTask(TaskId.make(task_id)))));
6328
7009
  server.registerTool("wait_task", {
6329
7010
  title: "Wait for delegated work",
6330
7011
  description: "Wait for a Cohall task to finish and return its final result.",
@@ -6333,19 +7014,19 @@ var runMcp = async (configuration) => {
6333
7014
  timeout_seconds: z.number().int().min(5).max(86400).default(900)
6334
7015
  }
6335
7016
  }, async ({ task_id, 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))));
7017
+ const task = await Effect12.runPromise(client2.getTask(TaskId.make(task_id)));
7018
+ return output(taskResult(await Effect12.runPromise(waitForTask(client2, task, timeout_seconds))));
6338
7019
  });
6339
7020
  server.registerTool("cancel_task", {
6340
7021
  title: "Cancel delegated work",
6341
7022
  description: "Request cancellation of coding work or a queued Bot task. Active Grok Bot tasks must be stopped in Grok Bot.",
6342
7023
  inputSchema: { task_id: z.string().uuid() }
6343
- }, async ({ task_id }) => output(taskResult(await Effect11.runPromise(client2.cancelTask(TaskId.make(task_id))))));
7024
+ }, async ({ task_id }) => output(taskResult(await Effect12.runPromise(client2.cancelTask(TaskId.make(task_id))))));
6344
7025
  server.registerTool("thread_context", {
6345
7026
  title: "Read a Cohall thread",
6346
7027
  description: "Read shared task prompts and final results for a cross-device thread.",
6347
7028
  inputSchema: { thread_id: z.string().uuid() }
6348
- }, async ({ thread_id }) => output(await Effect11.runPromise(threadContext(client2, ThreadId.make(thread_id)))));
7029
+ }, async ({ thread_id }) => output(await Effect12.runPromise(threadContext(client2, ThreadId.make(thread_id)))));
6349
7030
  await server.connect(new StdioServerTransport);
6350
7031
  };
6351
7032
 
@@ -6374,12 +7055,12 @@ var main = async () => {
6374
7055
  return;
6375
7056
  }
6376
7057
  if (command === "device") {
6377
- const configuration = await Effect15.runPromise(loadDeviceConfiguration);
6378
- await Effect15.runPromise(runDaemon(configuration));
7058
+ const configuration = await Effect16.runPromise(loadDeviceConfiguration);
7059
+ await Effect16.runPromise(runDaemon(configuration));
6379
7060
  return;
6380
7061
  }
6381
7062
  if (command === "mcp") {
6382
- const configuration = await Effect15.runPromise(loadClientConfiguration);
7063
+ const configuration = await Effect16.runPromise(loadClientConfiguration);
6383
7064
  await runMcp(configuration);
6384
7065
  return;
6385
7066
  }
@@ -6390,4 +7071,4 @@ await main().catch((cause) => {
6390
7071
  process.exitCode = 1;
6391
7072
  });
6392
7073
 
6393
- //# debugId=78963F23872A4D6564756E2164756E21
7074
+ //# debugId=C3F729038365DD7F64756E2164756E21