@akshar5/cohall 0.6.2 → 0.8.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.2", maxSocketPayloadBytes, bounded = (maxLength) => Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(maxLength)), optionalText = (maxLength) => Schema.String.check(Schema.isMaxLength(maxLength)), boundedArray = (schema, maxLength) => Schema.Array(schema).check(Schema.isMaxLength(maxLength)), count, uuid = (name) => Schema.String.check(Schema.isUUID(4)).pipe(Schema.brand(name)), isoTimestamp = (name) => Schema.String.check(Schema.isPattern(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/, {
20
+ var version = "0.8.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.2", 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
  });
@@ -564,12 +600,12 @@ __export(exports_store, {
564
600
  RelayStore: () => exports_store,
565
601
  PersistenceError: () => PersistenceError
566
602
  });
567
- 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";
568
604
  import { createHash as createHash2, randomBytes } from "node:crypto";
569
605
  var PersistenceError, Service2, operationError = (operation) => (cause) => new PersistenceError({
570
606
  operation,
571
607
  message: cause instanceof Error ? cause.message : String(cause)
572
- }), 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, {
573
609
  id: row.id,
574
610
  title: row.title,
575
611
  createdAt: row.created_at,
@@ -598,7 +634,10 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
598
634
  version: row.version,
599
635
  lastSeenAt: row.last_seen_at,
600
636
  ...row.connected_at === null ? {} : { connectedAt: row.connected_at }
601
- }), 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, {
602
641
  id: row.id,
603
642
  threadId: row.thread_id,
604
643
  prompt: row.prompt,
@@ -614,9 +653,21 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
614
653
  ...row.workspace === null ? {} : { workspace: row.workspace },
615
654
  ...row.provider_session_id === null ? {} : { providerSessionId: row.provider_session_id },
616
655
  ...row.result === null ? {} : { result: row.result },
656
+ ...attachments2.length === 0 ? {} : { inputAttachmentNames: attachments2.map((item) => item.name) },
617
657
  ...row.error === null ? {} : { error: row.error },
618
658
  ...row.started_at === null ? {} : { startedAt: row.started_at },
619
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 }
620
671
  }), operationFromRow = (row) => decode2("RelayStore.decodeOperation", DeviceOperation, {
621
672
  id: row.id,
622
673
  kind: row.kind,
@@ -641,10 +692,10 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
641
692
  kind: row.kind,
642
693
  at: row.created_at,
643
694
  ...row.detail === null ? {} : { detail: row.detail }
644
- }), 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) => {
645
696
  const roles = rolesFromRow(row);
646
697
  const role = roles[0];
647
- return role === undefined ? Effect13.fail(new PersistenceError({
698
+ return role === undefined ? Effect14.fail(new PersistenceError({
648
699
  operation: "RelayStore.decodeAuthSession",
649
700
  message: `Session ${row.id} has no role`
650
701
  })) : decode2("RelayStore.decodeAuthSession", AuthSession, {
@@ -740,23 +791,82 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
740
791
  )`).run(...parameters);
741
792
  };
742
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`;
743
799
  const queryOperation = (operationId) => db.query("SELECT * FROM device_operations WHERE id = ?").get(operationId);
744
- const getTask = Effect13.fn("RelayStore.getTask")(function* (taskId) {
745
- const row = yield* Effect13.try({
800
+ const getTask = Effect14.fn("RelayStore.getTask")(function* (taskId) {
801
+ const row = yield* Effect14.try({
746
802
  try: () => queryTask(taskId),
747
803
  catch: operationError("RelayStore.getTask")
748
804
  });
749
805
  if (row === null) {
750
- return yield* Effect13.fail(new PersistenceError({
806
+ return yield* Effect14.fail(new PersistenceError({
751
807
  operation: "RelayStore.getTask",
752
808
  message: `Unknown task ${taskId}`
753
809
  }));
754
810
  }
755
- return yield* taskFromRow(row);
811
+ return yield* taskFromRow(db, row);
756
812
  });
757
- const traceTask = Effect13.fn("RelayStore.traceTask")(function* (taskId) {
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);
866
+ });
867
+ const traceTask = Effect14.fn("RelayStore.traceTask")(function* (taskId) {
758
868
  const task = yield* getTask(taskId);
759
- const [deviceRow, eventRows] = yield* Effect13.try({
869
+ const [deviceRow, eventRows] = yield* Effect14.try({
760
870
  try: () => [
761
871
  db.query("SELECT * FROM devices WHERE id = ?").get(task.targetDeviceId),
762
872
  db.query(`SELECT * FROM (
@@ -767,14 +877,14 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
767
877
  catch: operationError("RelayStore.traceTask")
768
878
  });
769
879
  if (deviceRow === null) {
770
- return yield* Effect13.fail(new PersistenceError({
880
+ return yield* Effect14.fail(new PersistenceError({
771
881
  operation: "RelayStore.traceTask.device",
772
882
  message: `Unknown target device ${task.targetDeviceId}`
773
883
  }));
774
884
  }
775
885
  const targetDevice = yield* deviceFromRow(deviceRow);
776
886
  const selectedRows = eventRows.slice(-100);
777
- const recordedEvents = yield* Effect13.forEach(selectedRows, taskTraceEventFromRow);
887
+ const recordedEvents = yield* Effect14.forEach(selectedRows, taskTraceEventFromRow);
778
888
  const events = recordedEvents.length === 0 ? derivedTraceEvents(task) : recordedEvents;
779
889
  return TaskTrace.make({
780
890
  taskId: task.id,
@@ -795,15 +905,15 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
795
905
  ...task.error === undefined ? {} : { error: task.error }
796
906
  });
797
907
  });
798
- const listDevices = Effect13.fn("RelayStore.listDevices")(function* () {
799
- const rows = yield* Effect13.try({
908
+ const listDevices = Effect14.fn("RelayStore.listDevices")(function* () {
909
+ const rows = yield* Effect14.try({
800
910
  try: () => db.query("SELECT * FROM devices WHERE forgotten_at IS NULL ORDER BY name COLLATE NOCASE, id").all(),
801
911
  catch: operationError("RelayStore.listDevices")
802
912
  });
803
- return yield* Effect13.forEach(rows, deviceFromRow);
913
+ return yield* Effect14.forEach(rows, deviceFromRow);
804
914
  });
805
- const usage = Effect13.fn("RelayStore.usage")(function* () {
806
- const rows = yield* Effect13.try({
915
+ const usage = Effect14.fn("RelayStore.usage")(function* () {
916
+ const rows = yield* Effect14.try({
807
917
  try: () => db.query(`SELECT tasks.target_device_id AS device_id, devices.name AS device_name,
808
918
  tasks.status, tasks.provider, COUNT(*) AS tasks
809
919
  FROM tasks
@@ -817,9 +927,9 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
817
927
  const devices = new Map;
818
928
  let retainedTasks = 0;
819
929
  for (const row of rows) {
820
- const status = Schema13.decodeUnknownSync(TaskStatus)(row.status);
821
- const provider = Schema13.decodeUnknownSync(Provider)(row.provider);
822
- 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);
823
933
  retainedTasks += row.tasks;
824
934
  totalStatus[status] += row.tasks;
825
935
  totalProviders.set(provider, (totalProviders.get(provider) ?? 0) + row.tasks);
@@ -849,29 +959,29 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
849
959
  }))
850
960
  });
851
961
  });
852
- const getOperation = Effect13.fn("RelayStore.getOperation")(function* (operationId) {
853
- const row = yield* Effect13.try({
962
+ const getOperation = Effect14.fn("RelayStore.getOperation")(function* (operationId) {
963
+ const row = yield* Effect14.try({
854
964
  try: () => queryOperation(operationId),
855
965
  catch: operationError("RelayStore.getOperation")
856
966
  });
857
967
  if (row === null) {
858
- return yield* Effect13.fail(new PersistenceError({
968
+ return yield* Effect14.fail(new PersistenceError({
859
969
  operation: "RelayStore.getOperation",
860
970
  message: `Unknown device operation ${operationId}`
861
971
  }));
862
972
  }
863
973
  return yield* operationFromRow(row);
864
974
  });
865
- const listOperations = Effect13.fn("RelayStore.listOperations")(function* () {
866
- const rows = yield* Effect13.try({
975
+ const listOperations = Effect14.fn("RelayStore.listOperations")(function* () {
976
+ const rows = yield* Effect14.try({
867
977
  try: () => db.query("SELECT * FROM device_operations ORDER BY created_at DESC, id DESC LIMIT 50").all(),
868
978
  catch: operationError("RelayStore.listOperations")
869
979
  });
870
- return yield* Effect13.forEach(rows, operationFromRow);
980
+ return yield* Effect14.forEach(rows, operationFromRow);
871
981
  });
872
- const createUpgradeOperations = Effect13.fn("RelayStore.createUpgradeOperations")(function* (input) {
982
+ const createUpgradeOperations = Effect14.fn("RelayStore.createUpgradeOperations")(function* (input) {
873
983
  const timestamp = now();
874
- const operations = yield* Effect13.try({
984
+ const operations = yield* Effect14.try({
875
985
  try: () => db.transaction(() => {
876
986
  const devices = db.query("SELECT * FROM devices WHERE forgotten_at IS NULL ORDER BY name COLLATE NOCASE, id").all();
877
987
  if (devices.length === 0) {
@@ -911,15 +1021,15 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
911
1021
  });
912
1022
  return operations;
913
1023
  });
914
- const pendingOperationsFor = Effect13.fn("RelayStore.pendingOperationsFor")(function* (deviceId) {
915
- const rows = yield* Effect13.try({
1024
+ const pendingOperationsFor = Effect14.fn("RelayStore.pendingOperationsFor")(function* (deviceId) {
1025
+ const rows = yield* Effect14.try({
916
1026
  try: () => db.query(`SELECT * FROM device_operations WHERE target_device_id = ? AND status = 'queued'
917
1027
  ORDER BY created_at, id LIMIT 10`).all(deviceId),
918
1028
  catch: operationError("RelayStore.pendingOperationsFor")
919
1029
  });
920
- return yield* Effect13.forEach(rows, operationFromRow);
1030
+ return yield* Effect14.forEach(rows, operationFromRow);
921
1031
  });
922
- 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) {
923
1033
  const current = yield* getOperation(operationId);
924
1034
  if (!expected.includes(current.status)) {
925
1035
  return current;
@@ -927,7 +1037,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
927
1037
  const timestamp = now();
928
1038
  const completed = status === "completed" || status === "failed" ? timestamp : undefined;
929
1039
  const placeholders = expected.map(() => "?").join(", ");
930
- yield* Effect13.try({
1040
+ yield* Effect14.try({
931
1041
  try: () => db.transaction(() => {
932
1042
  const updated = db.query(`UPDATE device_operations
933
1043
  SET status = ?, result = ?, error = ?, updated_at = ?, completed_at = ?
@@ -940,23 +1050,23 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
940
1050
  });
941
1051
  return yield* getOperation(operationId);
942
1052
  });
943
- const requireOperationTarget = Effect13.fn("RelayStore.requireOperationTarget")(function* (operationId, deviceId) {
1053
+ const requireOperationTarget = Effect14.fn("RelayStore.requireOperationTarget")(function* (operationId, deviceId) {
944
1054
  const operation = yield* getOperation(operationId);
945
1055
  if (operation.targetDeviceId !== deviceId) {
946
- return yield* Effect13.fail(new PersistenceError({
1056
+ return yield* Effect14.fail(new PersistenceError({
947
1057
  operation: "RelayStore.requireOperationTarget",
948
1058
  message: `Device ${deviceId} cannot update operation ${operationId}`
949
1059
  }));
950
1060
  }
951
1061
  return operation;
952
1062
  });
953
- const assignOperation = Effect13.fn("RelayStore.assignOperation")(function* (operationId) {
1063
+ const assignOperation = Effect14.fn("RelayStore.assignOperation")(function* (operationId) {
954
1064
  const current = yield* getOperation(operationId);
955
1065
  if (current.status !== "queued") {
956
1066
  return current;
957
1067
  }
958
1068
  const timestamp = now();
959
- yield* Effect13.try({
1069
+ yield* Effect14.try({
960
1070
  try: () => db.query(`UPDATE device_operations SET status = 'assigned', updated_at = ?
961
1071
  WHERE id = ? AND status = 'queued'
962
1072
  AND NOT EXISTS (
@@ -972,19 +1082,19 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
972
1082
  });
973
1083
  return yield* getOperation(operationId);
974
1084
  });
975
- const acceptOperation = (operationId, deviceId) => requireOperationTarget(operationId, deviceId).pipe(Effect13.flatMap(() => transitionOperation(operationId, ["assigned"], "running")));
976
- const finishOperation = (operationId, deviceId, result) => requireOperationTarget(operationId, deviceId).pipe(Effect13.flatMap(() => transitionOperation(operationId, ["queued", "assigned", "running"], "completed", result)));
977
- 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)));
978
1088
  const abandonOperation = (operationId) => transitionOperation(operationId, ["queued", "assigned", "running"], "failed", undefined, "Abandoned by the relay owner");
979
- const requeueOperationsFor = Effect13.fn("RelayStore.requeueOperationsFor")(function* (deviceId) {
980
- yield* Effect13.try({
1089
+ const requeueOperationsFor = Effect14.fn("RelayStore.requeueOperationsFor")(function* (deviceId) {
1090
+ yield* Effect14.try({
981
1091
  try: () => db.query(`UPDATE device_operations SET status = 'queued', updated_at = ?
982
1092
  WHERE target_device_id = ? AND status IN ('assigned', 'running')`).run(now(), deviceId),
983
1093
  catch: operationError("RelayStore.requeueOperationsFor")
984
1094
  });
985
1095
  });
986
- const forgetDevice = Effect13.fn("RelayStore.forgetDevice")(function* (deviceId) {
987
- const row = yield* Effect13.try({
1096
+ const forgetDevice = Effect14.fn("RelayStore.forgetDevice")(function* (deviceId) {
1097
+ const row = yield* Effect14.try({
988
1098
  try: () => db.transaction(() => {
989
1099
  const device = db.query("SELECT * FROM devices WHERE id = ? AND forgotten_at IS NULL").get(deviceId);
990
1100
  if (device === null) {
@@ -1011,8 +1121,8 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1011
1121
  });
1012
1122
  return yield* deviceFromRow(row);
1013
1123
  });
1014
- const recover = Effect13.fn("RelayStore.recover")(function* () {
1015
- yield* Effect13.try({
1124
+ const recover = Effect14.fn("RelayStore.recover")(function* () {
1125
+ yield* Effect14.try({
1016
1126
  try: () => db.transaction(() => {
1017
1127
  const timestamp = now();
1018
1128
  const interrupted = db.query("SELECT id FROM tasks WHERE status IN ('assigned', 'running')").all();
@@ -1030,8 +1140,8 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1030
1140
  catch: operationError("RelayStore.recover")
1031
1141
  });
1032
1142
  });
1033
- const upsertDevice = Effect13.fn("RelayStore.upsertDevice")(function* (device) {
1034
- yield* Effect13.try({
1143
+ const upsertDevice = Effect14.fn("RelayStore.upsertDevice")(function* (device) {
1144
+ yield* Effect14.try({
1035
1145
  try: () => db.query(`INSERT INTO devices (
1036
1146
  id, name, hostname, platform, architecture, status, providers_json,
1037
1147
  capabilities_json, workspaces_json, version, last_seen_at, connected_at, bots_json
@@ -1048,29 +1158,31 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1048
1158
  });
1049
1159
  return device;
1050
1160
  });
1051
- const updateDeviceStatus = Effect13.fn("RelayStore.updateDeviceStatus")(function* (deviceId, status, bots) {
1161
+ const updateDeviceStatus = Effect14.fn("RelayStore.updateDeviceStatus")(function* (deviceId, status, bots) {
1052
1162
  const timestamp = now();
1053
- const result = yield* Effect13.try({
1163
+ const result = yield* Effect14.try({
1054
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),
1055
1165
  catch: operationError("RelayStore.updateDeviceStatus")
1056
1166
  });
1057
1167
  if (result.changes !== 1) {
1058
- return yield* Effect13.fail(new PersistenceError({
1168
+ return yield* Effect14.fail(new PersistenceError({
1059
1169
  operation: "RelayStore.updateDeviceStatus",
1060
1170
  message: `Unknown device ${deviceId}`
1061
1171
  }));
1062
1172
  }
1063
1173
  const row = db.query("SELECT * FROM devices WHERE id = ?").get(deviceId);
1064
1174
  if (row === null) {
1065
- return yield* Effect13.fail(new PersistenceError({
1175
+ return yield* Effect14.fail(new PersistenceError({
1066
1176
  operation: "RelayStore.updateDeviceStatus.read",
1067
1177
  message: `Device ${deviceId} disappeared after update`
1068
1178
  }));
1069
1179
  }
1070
1180
  return yield* deviceFromRow(row);
1071
1181
  });
1072
- const createDelegation2 = Effect13.fn("RelayStore.createDelegation")(function* (input, targetDeviceId, boundDeviceId, providerSessionId) {
1073
- const sourceDeviceId = yield* Effect13.try({
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({
1074
1186
  try: () => boundDeviceId === undefined || db.query("SELECT id FROM devices WHERE id = ? AND forgotten_at IS NULL").get(boundDeviceId) === null ? undefined : boundDeviceId,
1075
1187
  catch: operationError("RelayStore.createDelegation.source")
1076
1188
  });
@@ -1090,6 +1202,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1090
1202
  ...sourceDeviceId === undefined ? {} : { sourceDeviceId },
1091
1203
  ...input.parentTaskId === undefined ? {} : { parentTaskId: input.parentTaskId },
1092
1204
  ...input.workspace === undefined ? {} : { workspace: input.workspace },
1205
+ ...input.attachments === undefined || input.attachments.length === 0 ? {} : { inputAttachmentNames: input.attachments.map((attachment) => attachment.name) },
1093
1206
  ...providerSessionId === undefined ? {} : { providerSessionId }
1094
1207
  });
1095
1208
  const thread = input.threadId === undefined ? Thread.make({
@@ -1108,7 +1221,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1108
1221
  taskId: task.id,
1109
1222
  ...sourceDeviceId === undefined ? {} : { deviceId: sourceDeviceId }
1110
1223
  });
1111
- yield* Effect13.try({
1224
+ yield* Effect14.try({
1112
1225
  try: () => db.transaction(() => {
1113
1226
  const outstanding = db.query(`SELECT COUNT(*) AS count FROM tasks WHERE target_device_id = ?
1114
1227
  AND status IN ('queued', 'assigned', 'running', 'cancelling')`).get(targetDeviceId);
@@ -1122,9 +1235,16 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1122
1235
  }
1123
1236
  db.query(`INSERT INTO tasks (
1124
1237
  id, thread_id, prompt, context, provider, status, source_device_id,
1125
- target_device_id, parent_task_id, workspace, provider_session_id, bot_id,
1126
- created_at, updated_at
1127
- ) 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
+ }
1128
1248
  recordTaskTraceEvent(task.id, "queued", "Relay accepted the task", timestamp);
1129
1249
  db.query(`INSERT INTO messages (
1130
1250
  id, thread_id, role, kind, author_id, author_name, content, created_at,
@@ -1136,18 +1256,18 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1136
1256
  });
1137
1257
  return task;
1138
1258
  });
1139
- const threadContext2 = Effect13.fn("RelayStore.threadContext")(function* (threadId) {
1140
- const threadRow = yield* Effect13.try({
1259
+ const threadContext2 = Effect14.fn("RelayStore.threadContext")(function* (threadId) {
1260
+ const threadRow = yield* Effect14.try({
1141
1261
  try: () => db.query("SELECT * FROM threads WHERE id = ?").get(threadId),
1142
1262
  catch: operationError("RelayStore.threadContext.thread")
1143
1263
  });
1144
1264
  if (threadRow === null) {
1145
- return yield* Effect13.fail(new PersistenceError({
1265
+ return yield* Effect14.fail(new PersistenceError({
1146
1266
  operation: "RelayStore.threadContext",
1147
1267
  message: `Unknown thread ${threadId}`
1148
1268
  }));
1149
1269
  }
1150
- const [messageRows, taskRows] = yield* Effect13.try({
1270
+ const [messageRows, taskRows] = yield* Effect14.try({
1151
1271
  try: () => [
1152
1272
  db.query(`SELECT * FROM (
1153
1273
  SELECT * FROM messages WHERE thread_id = ?
@@ -1160,10 +1280,10 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1160
1280
  ],
1161
1281
  catch: operationError("RelayStore.threadContext.items")
1162
1282
  });
1163
- const [thread, messages, tasks] = yield* Effect13.all([
1283
+ const [thread, messages, tasks] = yield* Effect14.all([
1164
1284
  threadFromRow(threadRow),
1165
- Effect13.forEach(messageRows.slice(-20), messageFromRow),
1166
- Effect13.forEach(taskRows.slice(-20), taskFromRow)
1285
+ Effect14.forEach(messageRows.slice(-20), messageFromRow),
1286
+ Effect14.forEach(taskRows.slice(-20), (row) => taskFromRow(db, row))
1167
1287
  ]);
1168
1288
  const recentMessages = recentWithin(messages, 256 * 1024);
1169
1289
  const recentTasks = recentWithin(tasks, 768 * 1024);
@@ -1174,22 +1294,22 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1174
1294
  truncated: messageRows.length > 20 || taskRows.length > 20 || recentMessages.truncated || recentTasks.truncated
1175
1295
  });
1176
1296
  });
1177
- const pendingTasksFor = Effect13.fn("RelayStore.pendingTasksFor")(function* (deviceId) {
1178
- const rows = yield* Effect13.try({
1297
+ const pendingTasksFor = Effect14.fn("RelayStore.pendingTasksFor")(function* (deviceId) {
1298
+ const rows = yield* Effect14.try({
1179
1299
  try: () => db.query(`SELECT * FROM tasks WHERE target_device_id = ?
1180
1300
  AND status IN ('queued', 'cancelling') ORDER BY created_at, id LIMIT 100`).all(deviceId),
1181
1301
  catch: operationError("RelayStore.pendingTasksFor")
1182
1302
  });
1183
- return yield* Effect13.forEach(rows, taskFromRow);
1303
+ return yield* Effect14.forEach(rows, (row) => taskFromRow(db, row));
1184
1304
  });
1185
- const transition = Effect13.fn("RelayStore.transition")(function* (taskId, expected, update, event) {
1305
+ const transition = Effect14.fn("RelayStore.transition")(function* (taskId, expected, update, event) {
1186
1306
  const current = yield* getTask(taskId);
1187
1307
  if (!expected.includes(current.status)) {
1188
1308
  return current;
1189
1309
  }
1190
1310
  const placeholders = expected.map(() => "?").join(", ");
1191
1311
  const timestamp = now();
1192
- yield* Effect13.try({
1312
+ yield* Effect14.try({
1193
1313
  try: () => db.transaction(() => {
1194
1314
  const result = db.query(`UPDATE tasks SET status = ?, provider_session_id = ?, result = ?, error = ?,
1195
1315
  started_at = ?, completed_at = ?, updated_at = ?
@@ -1205,13 +1325,13 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1205
1325
  });
1206
1326
  return yield* getTask(taskId);
1207
1327
  });
1208
- const assignTask = Effect13.fn("RelayStore.assignTask")(function* (taskId) {
1328
+ const assignTask = Effect14.fn("RelayStore.assignTask")(function* (taskId) {
1209
1329
  const current = yield* getTask(taskId);
1210
1330
  if (current.status !== "queued") {
1211
1331
  return current;
1212
1332
  }
1213
1333
  const timestamp = now();
1214
- yield* Effect13.try({
1334
+ yield* Effect14.try({
1215
1335
  try: () => db.transaction(() => {
1216
1336
  const result = db.query(`UPDATE tasks SET status = 'assigned', updated_at = ?,
1217
1337
  provider_session_id = CASE
@@ -1241,23 +1361,23 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1241
1361
  });
1242
1362
  return yield* getTask(taskId);
1243
1363
  });
1244
- const requireTarget = Effect13.fn("RelayStore.requireTarget")(function* (taskId, deviceId) {
1364
+ const requireTarget = Effect14.fn("RelayStore.requireTarget")(function* (taskId, deviceId) {
1245
1365
  const task = yield* getTask(taskId);
1246
1366
  if (task.targetDeviceId !== deviceId) {
1247
- return yield* Effect13.fail(new PersistenceError({
1367
+ return yield* Effect14.fail(new PersistenceError({
1248
1368
  operation: "RelayStore.requireTarget",
1249
1369
  message: `Task ${taskId} belongs to another device`
1250
1370
  }));
1251
1371
  }
1252
1372
  return task;
1253
1373
  });
1254
- 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) {
1255
1375
  const current = yield* requireTarget(taskId, deviceId);
1256
1376
  if (["completed", "failed", "cancelled"].includes(current.status)) {
1257
1377
  return current;
1258
1378
  }
1259
1379
  const timestamp = now();
1260
- yield* Effect13.try({
1380
+ yield* Effect14.try({
1261
1381
  try: () => db.transaction(() => {
1262
1382
  const updated = db.query(`UPDATE tasks SET status = ?, result = ?, error = ?, provider_session_id = ?,
1263
1383
  completed_at = ?, updated_at = ? WHERE id = ?
@@ -1265,6 +1385,19 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1265
1385
  if (updated.changes !== 1) {
1266
1386
  return;
1267
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
+ }
1268
1401
  recordTaskTraceEvent(taskId, status, status === "completed" ? "Target device completed the task" : status === "failed" ? "Target device reported a task failure" : "Target device acknowledged cancellation", timestamp);
1269
1402
  if (current.provider !== "grok-bot" && providerSessionId !== undefined) {
1270
1403
  db.query(`INSERT INTO provider_sessions (thread_id, device_id, provider, session_id, updated_at)
@@ -1287,12 +1420,12 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1287
1420
  });
1288
1421
  return yield* getTask(taskId);
1289
1422
  });
1290
- const requestCancellation = Effect13.fn("RelayStore.requestCancellation")(function* (taskId) {
1423
+ const requestCancellation = Effect14.fn("RelayStore.requestCancellation")(function* (taskId) {
1291
1424
  const current = yield* getTask(taskId);
1292
1425
  if (["completed", "failed", "cancelled"].includes(current.status)) {
1293
1426
  return current;
1294
1427
  }
1295
- const dispatched = current.provider === "grok-bot" && (yield* Effect13.try({
1428
+ const dispatched = current.provider === "grok-bot" && (yield* Effect14.try({
1296
1429
  try: () => db.query("SELECT 1 FROM tasks WHERE id = ? AND dispatched_at IS NOT NULL").get(taskId) !== null,
1297
1430
  catch: operationError("RelayStore.requestCancellation")
1298
1431
  }));
@@ -1310,9 +1443,9 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1310
1443
  status: "cancelling"
1311
1444
  }, { kind: "cancelling", detail: "Client requested task cancellation" });
1312
1445
  });
1313
- const requeueTasksFor = Effect13.fn("RelayStore.requeueTasksFor")(function* (deviceId) {
1446
+ const requeueTasksFor = Effect14.fn("RelayStore.requeueTasksFor")(function* (deviceId) {
1314
1447
  const timestamp = now();
1315
- yield* Effect13.try({
1448
+ yield* Effect14.try({
1316
1449
  try: () => db.transaction(() => {
1317
1450
  const interrupted = db.query(`SELECT id FROM tasks WHERE target_device_id = ?
1318
1451
  AND status IN ('assigned', 'running')`).all(deviceId);
@@ -1325,42 +1458,42 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1325
1458
  catch: operationError("RelayStore.requeueTasksFor")
1326
1459
  });
1327
1460
  });
1328
- const sessionFor = Effect13.fn("RelayStore.sessionFor")(function* (threadId, deviceId, provider) {
1329
- const row = yield* Effect13.try({
1461
+ const sessionFor = Effect14.fn("RelayStore.sessionFor")(function* (threadId, deviceId, provider) {
1462
+ const row = yield* Effect14.try({
1330
1463
  try: () => db.query(`SELECT session_id FROM provider_sessions
1331
1464
  WHERE thread_id = ? AND device_id = ? AND provider = ?`).get(threadId, deviceId, provider),
1332
1465
  catch: operationError("RelayStore.sessionFor")
1333
1466
  });
1334
1467
  return row?.session_id;
1335
1468
  });
1336
- const createPairing = Effect13.fn("RelayStore.createPairing")(function* (input) {
1469
+ const createPairing = Effect14.fn("RelayStore.createPairing")(function* (input) {
1337
1470
  const roles = [...new Set(input.roles)];
1338
1471
  if (roles.length === 0) {
1339
- return yield* Effect13.fail(new PersistenceError({
1472
+ return yield* Effect14.fail(new PersistenceError({
1340
1473
  operation: "RelayStore.createPairing",
1341
1474
  message: "A pairing must allow at least one role"
1342
1475
  }));
1343
1476
  }
1344
1477
  const token = secret("pair");
1345
1478
  const expiresAt = new Date(Date.now() + 10 * 60 * 1000).toISOString();
1346
- yield* Effect13.try({
1479
+ yield* Effect14.try({
1347
1480
  try: () => db.query(`INSERT INTO auth_pairings (token_hash, label, roles_json, expires_at, created_at)
1348
1481
  VALUES (?, ?, ?, ?, ?)`).run(tokenHash(token), input.label, JSON.stringify(roles), expiresAt, now()),
1349
1482
  catch: operationError("RelayStore.createPairing")
1350
1483
  });
1351
1484
  return PairingCredential.make({ token, expiresAt: Timestamp.make(expiresAt) });
1352
1485
  });
1353
- const exchangePairing2 = Effect13.fn("RelayStore.exchangePairing")(function* (token) {
1486
+ const exchangePairing2 = Effect14.fn("RelayStore.exchangePairing")(function* (token) {
1354
1487
  const timestamp = now();
1355
1488
  const expiresAt = new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString();
1356
- const created = yield* Effect13.try({
1489
+ const created = yield* Effect14.try({
1357
1490
  try: () => db.transaction(() => {
1358
1491
  const pairing = db.query(`SELECT * FROM auth_pairings
1359
1492
  WHERE token_hash = ? AND used_at IS NULL AND expires_at > ?`).get(tokenHash(token), timestamp);
1360
1493
  if (pairing === null) {
1361
1494
  throw new Error("Pairing credential is invalid, expired, or already used");
1362
1495
  }
1363
- 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));
1364
1497
  const deviceId = roles.includes("device") ? makeDeviceId() : undefined;
1365
1498
  const consumed = db.query("UPDATE auth_pairings SET used_at = ? WHERE token_hash = ? AND used_at IS NULL").run(timestamp, pairing.token_hash);
1366
1499
  if (consumed.changes !== 1) {
@@ -1388,11 +1521,11 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1388
1521
  })(),
1389
1522
  catch: operationError("RelayStore.exchangePairing")
1390
1523
  });
1391
- 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 }))));
1392
1525
  return PairingResult.make({ credentials });
1393
1526
  });
1394
- const authenticateSession = Effect13.fn("RelayStore.authenticateSession")(function* (token, role) {
1395
- const row = yield* Effect13.try({
1527
+ const authenticateSession = Effect14.fn("RelayStore.authenticateSession")(function* (token, role) {
1528
+ const row = yield* Effect14.try({
1396
1529
  try: () => db.query(`SELECT * FROM auth_sessions
1397
1530
  WHERE token_hash = ? AND revoked_at IS NULL AND expires_at > ?`).get(tokenHash(token), now()),
1398
1531
  catch: operationError("RelayStore.authenticateSession")
@@ -1401,35 +1534,35 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1401
1534
  return;
1402
1535
  }
1403
1536
  const lastSeenAt = now();
1404
- yield* Effect13.try({
1537
+ yield* Effect14.try({
1405
1538
  try: () => db.query("UPDATE auth_sessions SET last_seen_at = ? WHERE id = ?").run(lastSeenAt, row.id),
1406
1539
  catch: operationError("RelayStore.authenticateSession.touch")
1407
1540
  });
1408
1541
  const session = yield* authSessionFromRow(row);
1409
1542
  return AuthSession.make({ ...session, lastSeenAt });
1410
1543
  });
1411
- const listAuthSessions = Effect13.fn("RelayStore.listAuthSessions")(function* () {
1412
- const rows = yield* Effect13.try({
1544
+ const listAuthSessions = Effect14.fn("RelayStore.listAuthSessions")(function* () {
1545
+ const rows = yield* Effect14.try({
1413
1546
  try: () => db.query("SELECT * FROM auth_sessions ORDER BY created_at DESC, id LIMIT 1000").all(),
1414
1547
  catch: operationError("RelayStore.listAuthSessions")
1415
1548
  });
1416
- return yield* Effect13.forEach(rows, authSessionFromRow);
1549
+ return yield* Effect14.forEach(rows, authSessionFromRow);
1417
1550
  });
1418
- const revokeAuthSession = Effect13.fn("RelayStore.revokeAuthSession")(function* (sessionId) {
1551
+ const revokeAuthSession = Effect14.fn("RelayStore.revokeAuthSession")(function* (sessionId) {
1419
1552
  const revokedAt = now();
1420
- const result = yield* Effect13.try({
1553
+ const result = yield* Effect14.try({
1421
1554
  try: () => db.query("UPDATE auth_sessions SET revoked_at = ? WHERE id = ? AND revoked_at IS NULL").run(revokedAt, sessionId),
1422
1555
  catch: operationError("RelayStore.revokeAuthSession")
1423
1556
  });
1424
1557
  if (result.changes !== 1) {
1425
- return yield* Effect13.fail(new PersistenceError({
1558
+ return yield* Effect14.fail(new PersistenceError({
1426
1559
  operation: "RelayStore.revokeAuthSession",
1427
1560
  message: `Unknown or already revoked session ${sessionId}`
1428
1561
  }));
1429
1562
  }
1430
1563
  const row = db.query("SELECT * FROM auth_sessions WHERE id = ?").get(sessionId);
1431
1564
  if (row === null) {
1432
- return yield* Effect13.fail(new PersistenceError({
1565
+ return yield* Effect14.fail(new PersistenceError({
1433
1566
  operation: "RelayStore.revokeAuthSession.read",
1434
1567
  message: `Session ${sessionId} disappeared after revocation`
1435
1568
  }));
@@ -1446,19 +1579,23 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1446
1579
  markDeviceOffline: (deviceId) => updateDeviceStatus(deviceId, "offline"),
1447
1580
  createDelegation: createDelegation2,
1448
1581
  getTask,
1582
+ listAttachments,
1583
+ readAttachment,
1584
+ inboxFor,
1585
+ acknowledgeCompletion,
1449
1586
  traceTask,
1450
1587
  threadContext: threadContext2,
1451
1588
  pendingTasksFor,
1452
1589
  assignTask,
1453
- markTaskDispatched: (taskId) => Effect13.try({
1590
+ markTaskDispatched: (taskId) => Effect14.try({
1454
1591
  try: () => {
1455
1592
  db.query("UPDATE tasks SET dispatched_at = COALESCE(dispatched_at, ?) WHERE id = ? AND status = 'assigned'").run(now(), taskId);
1456
1593
  },
1457
1594
  catch: operationError("RelayStore.markTaskDispatched")
1458
1595
  }),
1459
1596
  rollbackAssignment: (taskId) => transition(taskId, ["assigned"], { status: "queued" }, { kind: "requeued", detail: "Target device was unavailable during dispatch" }),
1460
- acceptTask: (taskId, deviceId) => requireTarget(taskId, deviceId).pipe(Effect13.flatMap(() => transition(taskId, ["assigned"], { status: "running", startedAt: now() }, { kind: "running", detail: "Target device accepted the task and started the provider" }))),
1461
- 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),
1462
1599
  failTask: (taskId, deviceId, error) => terminal(taskId, deviceId, "failed", undefined, error),
1463
1600
  acknowledgeCancellation: (taskId, deviceId) => terminal(taskId, deviceId, "cancelled"),
1464
1601
  requestCancellation,
@@ -1480,7 +1617,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1480
1617
  listAuthSessions,
1481
1618
  revokeAuthSession
1482
1619
  });
1483
- }, migrate = (db) => Effect13.try({
1620
+ }, migrate = (db) => Effect14.try({
1484
1621
  try: () => {
1485
1622
  db.exec("PRAGMA journal_mode = WAL");
1486
1623
  db.exec("PRAGMA foreign_keys = ON");
@@ -1509,6 +1646,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1509
1646
  id TEXT PRIMARY KEY, thread_id TEXT NOT NULL REFERENCES threads(id) ON DELETE CASCADE,
1510
1647
  prompt TEXT NOT NULL, context TEXT, provider TEXT NOT NULL, bot_id TEXT, status TEXT NOT NULL,
1511
1648
  source_device_id TEXT REFERENCES devices(id),
1649
+ requester_id TEXT, completion_seen_at TEXT,
1512
1650
  target_device_id TEXT NOT NULL REFERENCES devices(id), parent_task_id TEXT,
1513
1651
  workspace TEXT, provider_session_id TEXT, result TEXT, error TEXT,
1514
1652
  created_at TEXT NOT NULL, updated_at TEXT NOT NULL, started_at TEXT, completed_at TEXT,
@@ -1516,6 +1654,12 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1516
1654
  );
1517
1655
  CREATE INDEX IF NOT EXISTS tasks_thread_created ON tasks(thread_id, created_at);
1518
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
+ );
1519
1663
  CREATE TABLE IF NOT EXISTS task_trace_events (
1520
1664
  id INTEGER PRIMARY KEY AUTOINCREMENT,
1521
1665
  task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
@@ -1560,6 +1704,14 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1560
1704
  if (!taskColumns.some((column) => column.name === "bot_id")) {
1561
1705
  db.exec("ALTER TABLE tasks ADD COLUMN bot_id TEXT");
1562
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)`);
1563
1715
  if (!taskColumns.some((column) => column.name === "dispatched_at")) {
1564
1716
  db.transaction(() => {
1565
1717
  db.exec("ALTER TABLE tasks ADD COLUMN dispatched_at TEXT");
@@ -1569,15 +1721,15 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
1569
1721
  }
1570
1722
  },
1571
1723
  catch: operationError("RelayStore.migrate")
1572
- }), layer2 = (path, retainedTerminalTasks = 1000) => Layer2.effect(Service2, Effect13.acquireRelease(Effect13.try({
1724
+ }), layer2 = (path, retainedTerminalTasks = 1000) => Layer2.effect(Service2, Effect14.acquireRelease(Effect14.try({
1573
1725
  try: () => new Database(path),
1574
1726
  catch: operationError("RelayStore.open")
1575
- }).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))));
1576
1728
  var init_store = __esm(() => {
1577
1729
  init_src();
1578
1730
  init_database();
1579
1731
  init_store();
1580
- 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 }) {
1581
1733
  };
1582
1734
  Service2 = class Service2 extends Context2.Service()("@cohall/RelayStore") {
1583
1735
  };
@@ -1588,9 +1740,12 @@ var exports_main = {};
1588
1740
  __export(exports_main, {
1589
1741
  runRelay: () => runRelay,
1590
1742
  resolveDelegation: () => resolveDelegation,
1591
- relayListenOptions: () => relayListenOptions
1743
+ relayListenOptions: () => relayListenOptions,
1744
+ maxTaskRequestBodyBytes: () => maxTaskRequestBodyBytes,
1745
+ canReadTaskAttachments: () => canReadTaskAttachments,
1746
+ canDispatchTaskToDevice: () => canDispatchTaskToDevice
1592
1747
  });
1593
- import { Effect as Effect14, ManagedRuntime, Schema as Schema14 } from "effect";
1748
+ import { Effect as Effect15, ManagedRuntime, Schema as Schema16 } from "effect";
1594
1749
  import { createHash as createHash3, timingSafeEqual } from "node:crypto";
1595
1750
  import { access as access6, chmod as chmod8 } from "node:fs/promises";
1596
1751
  import { createServer } from "node:http";
@@ -1636,7 +1791,7 @@ var RequestError, relayListenOptions = (configuration, environment = process.env
1636
1791
  return;
1637
1792
  }
1638
1793
  cleanup();
1639
- reject(new Error("Request body exceeded 256 KiB"));
1794
+ reject(new Error(`Request body exceeded ${Math.floor(limit / 1024)} KiB`));
1640
1795
  };
1641
1796
  const onEnd = () => {
1642
1797
  cleanup();
@@ -1650,7 +1805,7 @@ var RequestError, relayListenOptions = (configuration, environment = process.env
1650
1805
  request.once("end", onEnd);
1651
1806
  request.once("error", onError);
1652
1807
  });
1653
- }, webRequest = async (request) => {
1808
+ }, maxTaskRequestBodyBytes, webRequest = async (request) => {
1654
1809
  const headers = new Headers;
1655
1810
  for (let index = 0;index < request.rawHeaders.length; index += 2) {
1656
1811
  const name = request.rawHeaders[index];
@@ -1659,7 +1814,8 @@ var RequestError, relayListenOptions = (configuration, environment = process.env
1659
1814
  headers.append(name, value);
1660
1815
  }
1661
1816
  }
1662
- 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);
1663
1819
  return new Request(new URL(request.url ?? "/", "http://cohall.local"), {
1664
1820
  method: request.method ?? "GET",
1665
1821
  headers,
@@ -1689,15 +1845,15 @@ Content-Length: 0\r
1689
1845
  if ("destroy" in socket && typeof socket.destroy === "function") {
1690
1846
  socket.destroy();
1691
1847
  }
1692
- }, body = (request, decode3) => Effect14.tryPromise({
1848
+ }, body = (request, decode3) => Effect15.tryPromise({
1693
1849
  try: () => request.json(),
1694
1850
  catch: () => new RequestError({ status: 400, message: "Expected a JSON request body" })
1695
- }).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) => {
1696
1852
  const prompt = `${input.prompt} ${input.context ?? ""}`.toLowerCase();
1697
1853
  const needsXcode = /\b(xcode|ios|macos|swiftui|app store)\b/.test(prompt);
1698
1854
  const needsBrowser = /\b(browser|chrome|youtube|twitter|logged[- ]?in|signed[- ]?in|x\.com)\b/.test(prompt);
1699
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);
1700
- }, resolveDelegation = (input, sourceDeviceId) => Effect14.gen(function* () {
1856
+ }, resolveDelegation = (input, sourceDeviceId) => Effect15.gen(function* () {
1701
1857
  const store = yield* exports_store.Service;
1702
1858
  const ancestors = [];
1703
1859
  const seen = new Set;
@@ -1705,11 +1861,11 @@ Content-Length: 0\r
1705
1861
  while (nextParentId !== undefined && !seen.has(nextParentId)) {
1706
1862
  const parentId = nextParentId;
1707
1863
  seen.add(parentId);
1708
- const parent2 = yield* store.getTask(parentId).pipe(Effect14.catch((cause) => {
1864
+ const parent2 = yield* store.getTask(parentId).pipe(Effect15.catch((cause) => {
1709
1865
  const missing = cause.message === `Unknown task ${parentId}`;
1710
1866
  if (missing && ancestors.length > 0)
1711
- return Effect14.succeed(undefined);
1712
- return Effect14.fail(new RequestError({ status: missing ? 404 : 500, message: cause.message }));
1867
+ return Effect15.succeed(undefined);
1868
+ return Effect15.fail(new RequestError({ status: missing ? 404 : 500, message: cause.message }));
1713
1869
  }));
1714
1870
  if (parent2 === undefined)
1715
1871
  break;
@@ -1724,8 +1880,15 @@ Content-Length: 0\r
1724
1880
  });
1725
1881
  }
1726
1882
  const resolvedInput = parent === undefined ? input : { ...input, threadId: parent.threadId };
1727
- const devices = yield* store.listDevices().pipe(Effect14.mapError((cause) => new RequestError({ status: 500, message: cause.message })));
1883
+ const devices = yield* store.listDevices().pipe(Effect15.mapError((cause) => new RequestError({ status: 500, message: cause.message })));
1728
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
+ }
1729
1892
  const slot = taskSlot({
1730
1893
  provider,
1731
1894
  ...input.botId === undefined ? {} : { botId: input.botId }
@@ -1745,6 +1908,12 @@ Content-Length: 0\r
1745
1908
  message: `${target.name} does not advertise the ${provider} provider`
1746
1909
  });
1747
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
+ }
1748
1917
  if (input.botId !== undefined && !target.bots?.some((bot) => bot.id === input.botId)) {
1749
1918
  return yield* new RequestError({
1750
1919
  status: 404,
@@ -1759,7 +1928,8 @@ Content-Length: 0\r
1759
1928
  }
1760
1929
  return { input: resolvedInput, targetDeviceId: target.id };
1761
1930
  }
1762
- 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;
1763
1933
  if (input.botId !== undefined && candidates.length > 1) {
1764
1934
  return yield* new RequestError({
1765
1935
  status: 409,
@@ -1770,16 +1940,16 @@ Content-Length: 0\r
1770
1940
  if (selected === undefined) {
1771
1941
  return yield* new RequestError({
1772
1942
  status: 409,
1773
- message: candidates.length > 0 ? "No available delegation target avoids an unfinished parent's worker slot; select another device or bot" : input.botId === undefined ? `No Cohall device advertises the ${provider} provider` : `No Cohall device advertises bot ${input.botId}; refresh the bot list`
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`
1774
1944
  });
1775
1945
  }
1776
1946
  return { input: resolvedInput, targetDeviceId: selected.id };
1777
- }), runRelay = async () => {
1778
- const configuration = await Effect14.runPromise(loadEnvironmentConfiguration);
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);
1779
1949
  process.umask(63);
1780
1950
  const runtime = ManagedRuntime.make(exports_store.layer(configuration.databasePath, configuration.historyTaskLimit));
1781
1951
  const run2 = (effect) => runtime.runPromise(effect);
1782
- await run2(Effect14.gen(function* () {
1952
+ await run2(Effect15.gen(function* () {
1783
1953
  const store = yield* exports_store.Service;
1784
1954
  yield* store.recover();
1785
1955
  }));
@@ -1794,33 +1964,41 @@ Content-Length: 0\r
1794
1964
  if (ownerToken2(token)) {
1795
1965
  return "owner";
1796
1966
  }
1797
- return run2(Effect14.gen(function* () {
1967
+ return run2(Effect15.gen(function* () {
1798
1968
  const store = yield* exports_store.Service;
1799
1969
  return yield* store.authenticateSession(token, role);
1800
1970
  })).catch(() => {
1801
1971
  return;
1802
1972
  });
1803
1973
  };
1804
- const principalFor = async (request) => {
1974
+ const principalFor = async (request, allowDevice = false) => {
1805
1975
  const authorization = request.headers.get("authorization");
1806
1976
  if (authorization?.startsWith("Bearer ") !== true) {
1807
1977
  return;
1808
1978
  }
1809
- 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);
1810
1981
  };
1811
1982
  const dispatch = async (task) => {
1812
1983
  const store = await run2(exports_store.Service);
1813
- const assigned = await Effect14.runPromise(store.assignTask(task.id));
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));
1814
1992
  if (assigned.status !== "assigned") {
1815
1993
  return assigned;
1816
1994
  }
1817
- if (hub.sendToDevice(assigned.targetDeviceId, SocketEvent.make({ _tag: "TaskAssigned", task: assigned }), assigned.provider === "grok-bot" ? () => Effect14.runSync(store.markTaskDispatched(assigned.id)) : undefined)) {
1995
+ if (hub.sendToDevice(assigned.targetDeviceId, SocketEvent.make({ _tag: "TaskAssigned", task: assigned }), assigned.provider === "grok-bot" ? () => Effect15.runSync(store.markTaskDispatched(assigned.id)) : undefined)) {
1818
1996
  return assigned;
1819
1997
  }
1820
- return Effect14.runPromise(store.rollbackAssignment(task.id));
1998
+ return Effect15.runPromise(store.rollbackAssignment(task.id));
1821
1999
  };
1822
2000
  const dispatchOperation = async (operation) => {
1823
- const assigned = await run2(Effect14.gen(function* () {
2001
+ const assigned = await run2(Effect15.gen(function* () {
1824
2002
  const store = yield* exports_store.Service;
1825
2003
  return yield* store.assignOperation(operation.id);
1826
2004
  }));
@@ -1830,13 +2008,13 @@ Content-Length: 0\r
1830
2008
  if (hub.sendToDevice(assigned.targetDeviceId, SocketEvent.make({ _tag: "OperationAssigned", operation: assigned }))) {
1831
2009
  return assigned;
1832
2010
  }
1833
- return run2(Effect14.gen(function* () {
2011
+ return run2(Effect15.gen(function* () {
1834
2012
  const store = yield* exports_store.Service;
1835
2013
  return yield* store.rollbackOperation(operation.id);
1836
2014
  }));
1837
2015
  };
1838
2016
  const dispatchPendingOperations = async (deviceId) => {
1839
- const operations = await run2(Effect14.gen(function* () {
2017
+ const operations = await run2(Effect15.gen(function* () {
1840
2018
  const store = yield* exports_store.Service;
1841
2019
  return yield* store.pendingOperationsFor(deviceId);
1842
2020
  }));
@@ -1845,7 +2023,7 @@ Content-Length: 0\r
1845
2023
  }
1846
2024
  };
1847
2025
  const dispatchPendingTasks = async (deviceId) => {
1848
- const tasks = await run2(Effect14.gen(function* () {
2026
+ const tasks = await run2(Effect15.gen(function* () {
1849
2027
  const store = yield* exports_store.Service;
1850
2028
  return yield* store.pendingTasksFor(deviceId);
1851
2029
  }));
@@ -1869,10 +2047,10 @@ Content-Length: 0\r
1869
2047
  })));
1870
2048
  };
1871
2049
  const handleSocketEvent = async (socket, input) => {
1872
- const event = await Effect14.runPromise(Effect14.try({
2050
+ const event = await Effect15.runPromise(Effect15.try({
1873
2051
  try: () => JSON.parse(socketText(input)),
1874
2052
  catch: () => new RequestError({ status: 400, message: "Invalid socket JSON" })
1875
- }).pipe(Effect14.flatMap(decodeSocketEvent))).catch((cause) => {
2053
+ }).pipe(Effect15.flatMap(decodeSocketEvent))).catch((cause) => {
1876
2054
  sendError(socket, "invalid_event", cause instanceof Error ? cause.message : String(cause));
1877
2055
  return;
1878
2056
  });
@@ -1907,7 +2085,12 @@ Content-Length: 0\r
1907
2085
  socket.close(4003, "Device registration timeout");
1908
2086
  }
1909
2087
  }, 5000);
1910
- 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
+ })));
1911
2094
  return;
1912
2095
  }
1913
2096
  if (event._tag === "Authenticate") {
@@ -1929,7 +2112,7 @@ Content-Length: 0\r
1929
2112
  socket.close(4009, "Device is already connected");
1930
2113
  return;
1931
2114
  }
1932
- await run2(Effect14.gen(function* () {
2115
+ await run2(Effect15.gen(function* () {
1933
2116
  const store = yield* exports_store.Service;
1934
2117
  yield* store.upsertDevice(device);
1935
2118
  }));
@@ -1948,7 +2131,7 @@ Content-Length: 0\r
1948
2131
  const settleOperation = (operationId) => {
1949
2132
  socket.send(JSON.stringify(SocketEvent.make({ _tag: "OperationSettled", operationId })));
1950
2133
  };
1951
- const processed = await run2(Effect14.gen(function* () {
2134
+ const processed = await run2(Effect15.gen(function* () {
1952
2135
  const store = yield* exports_store.Service;
1953
2136
  switch (event._tag) {
1954
2137
  case "DeviceHeartbeat":
@@ -1960,7 +2143,7 @@ Content-Length: 0\r
1960
2143
  yield* store.acceptTask(event.taskId, deviceId);
1961
2144
  return;
1962
2145
  case "TaskFinished":
1963
- yield* store.finishTask(event.taskId, deviceId, event.result, event.providerSessionId);
2146
+ yield* store.finishTask(event.taskId, deviceId, event.result, event.providerSessionId, event.attachments);
1964
2147
  return;
1965
2148
  case "TaskFailed":
1966
2149
  yield* store.failTask(event.taskId, deviceId, event.error);
@@ -2001,16 +2184,17 @@ Content-Length: 0\r
2001
2184
  };
2002
2185
  const api = async (request, url) => {
2003
2186
  if (url.pathname === "/api/health" && request.method === "GET") {
2004
- return json({ ok: true, version });
2187
+ return json({ ok: true, version, taskAttachments: true });
2005
2188
  }
2006
2189
  if (url.pathname === "/api/auth/pair" && request.method === "POST") {
2007
- return run2(Effect14.gen(function* () {
2190
+ return run2(Effect15.gen(function* () {
2008
2191
  const store = yield* exports_store.Service;
2009
2192
  const input = yield* body(request, decodeExchangePairingInput);
2010
2193
  return yield* store.exchangePairing(input.token);
2011
2194
  })).then((value) => json(value, 201)).catch(() => json({ error: "Pairing credential is invalid, expired, or already used" }, 401));
2012
2195
  }
2013
- const principal = await principalFor(request);
2196
+ const attachmentRoute = request.method === "GET" && /^\/api\/tasks\/[^/]+\/attachments(?:\/[^/]+)?$/.test(url.pathname);
2197
+ const principal = await principalFor(request, attachmentRoute);
2014
2198
  if (principal === undefined) {
2015
2199
  return json({ error: "Unauthorized" }, 401);
2016
2200
  }
@@ -2019,7 +2203,7 @@ Content-Length: 0\r
2019
2203
  return json({ error: "Owner token required" }, 403);
2020
2204
  }
2021
2205
  const sourceDeviceId = principal === "owner" ? undefined : principal.deviceId;
2022
- const effect = Effect14.gen(function* () {
2206
+ const effect = Effect15.gen(function* () {
2023
2207
  const store = yield* exports_store.Service;
2024
2208
  if (url.pathname === "/api/auth/pairings" && request.method === "POST") {
2025
2209
  const input = yield* body(request, decodeCreatePairingInput);
@@ -2047,7 +2231,7 @@ Content-Length: 0\r
2047
2231
  if (url.pathname === "/api/operations/upgrades" && request.method === "POST") {
2048
2232
  const input = yield* body(request, decodeCreateUpgradeOperationsInput);
2049
2233
  const operations = yield* store.createUpgradeOperations(input);
2050
- return json(yield* Effect14.tryPromise(() => Promise.all(operations.map(dispatchOperation))), 201);
2234
+ return json(yield* Effect15.tryPromise(() => Promise.all(operations.map(dispatchOperation))), 201);
2051
2235
  }
2052
2236
  const abandonOperation = url.pathname.match(/^\/api\/operations\/([^/]+)\/abandon$/);
2053
2237
  if (request.method === "POST" && abandonOperation?.[1] !== undefined) {
@@ -2074,8 +2258,52 @@ Content-Length: 0\r
2074
2258
  if (url.pathname === "/api/tasks" && request.method === "POST") {
2075
2259
  const { input, targetDeviceId: target } = yield* resolveDelegation(yield* body(request, decodeCreateTaskInput), sourceDeviceId);
2076
2260
  const providerSessionId = input.threadId === undefined || input.provider === "grok-bot" ? undefined : yield* store.sessionFor(input.threadId, target, input.provider ?? "codex");
2077
- const task2 = yield* store.createDelegation(input, target, sourceDeviceId, providerSessionId);
2078
- 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
+ });
2079
2307
  }
2080
2308
  const task = url.pathname.match(/^\/api\/tasks\/([^/]+)$/);
2081
2309
  if (request.method === "GET" && task?.[1] !== undefined) {
@@ -2099,7 +2327,7 @@ Content-Length: 0\r
2099
2327
  return json(yield* store.threadContext(yield* pathId(ThreadId, thread[1])));
2100
2328
  }
2101
2329
  return yield* new RequestError({ status: 404, message: "Route not found" });
2102
- }).pipe(Effect14.mapError((cause) => cause instanceof RequestError ? cause : new RequestError({
2330
+ }).pipe(Effect15.mapError((cause) => cause instanceof RequestError ? cause : new RequestError({
2103
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,
2104
2332
  message: cause.message
2105
2333
  })));
@@ -2175,7 +2403,7 @@ Content-Length: 0\r
2175
2403
  if (deviceId === undefined) {
2176
2404
  return;
2177
2405
  }
2178
- run2(Effect14.gen(function* () {
2406
+ run2(Effect15.gen(function* () {
2179
2407
  const store = yield* exports_store.Service;
2180
2408
  yield* store.requeueTasksFor(deviceId);
2181
2409
  yield* store.requeueOperationsFor(deviceId);
@@ -2187,7 +2415,7 @@ Content-Length: 0\r
2187
2415
  webRequest(incoming).then((request) => {
2188
2416
  const url = new URL(request.url);
2189
2417
  return url.pathname.startsWith("/api/") ? api(request, url) : json({ name: "Cohall relay", version, status: "running" });
2190
- }).then((response) => sendResponse(outgoing, response)).catch((cause) => sendResponse(outgoing, json({ error: cause instanceof Error ? cause.message : "Invalid request" }, cause instanceof Error && cause.message.includes("256 KiB") ? 413 : 400)));
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)));
2191
2419
  });
2192
2420
  server.on("upgrade", (request, socket, head) => {
2193
2421
  const url = URL.parse(request.url ?? "/", "http://cohall.local");
@@ -2238,17 +2466,18 @@ var init_main = __esm(async () => {
2238
2466
  init_config();
2239
2467
  init_hub();
2240
2468
  init_store();
2241
- RequestError = class RequestError extends Schema14.TaggedErrorClass()("Relay.RequestError", {
2242
- status: Schema14.Int,
2243
- message: Schema14.String
2469
+ RequestError = class RequestError extends Schema16.TaggedErrorClass()("Relay.RequestError", {
2470
+ status: Schema16.Int,
2471
+ message: Schema16.String
2244
2472
  }) {
2245
2473
  };
2474
+ maxTaskRequestBodyBytes = 3 * 1024 * 1024;
2246
2475
  if (false) {}
2247
2476
  });
2248
2477
 
2249
2478
  // apps/device/src/main.ts
2250
2479
  init_src();
2251
- import { Effect as Effect15 } from "effect";
2480
+ import { Effect as Effect16 } from "effect";
2252
2481
 
2253
2482
  // packages/client/src/index.ts
2254
2483
  init_src();
@@ -2353,11 +2582,72 @@ var make = (options) => {
2353
2582
  return Service.of({
2354
2583
  devices: () => request("RelayClient.devices", "/api/devices", Schema2.Array(Device)),
2355
2584
  forgetDevice: (deviceId) => request("RelayClient.forgetDevice", `/api/devices/${encodeURIComponent(deviceId)}/forget`, Device, { method: "POST" }),
2356
- createTask: (input) => request("RelayClient.createTask", "/api/tasks", Task, {
2357
- method: "POST",
2358
- 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
+ });
2359
2599
  }),
2360
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" }),
2361
2651
  traceTask: (taskId) => request("RelayClient.traceTask", `/api/tasks/${encodeURIComponent(taskId)}/trace`, TaskTrace),
2362
2652
  cancelTask: (taskId) => request("RelayClient.cancelTask", `/api/tasks/${encodeURIComponent(taskId)}/cancel`, Task, {
2363
2653
  method: "POST"
@@ -2888,10 +3178,10 @@ var run = (options) => Effect3.callback((resume, signal) => {
2888
3178
  });
2889
3179
 
2890
3180
  // apps/device/src/cli.ts
2891
- import { Effect as Effect9, Schema as Schema10 } from "effect";
2892
- import { access as access4, mkdir as mkdir6, readFile as readFile4, stat as stat4, 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";
2893
3183
  import { homedir as homedir4 } from "node:os";
2894
- 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";
2895
3185
 
2896
3186
  // skills/cohall/SKILL.md
2897
3187
  var SKILL_default = `---
@@ -2903,7 +3193,7 @@ description: Delegate through Cohall to another user-owned device or an existing
2903
3193
 
2904
3194
  Cohall sends a focused task to a coding agent or an existing named Grok Bot. The target
2905
3195
  keeps its local files, credentials, browser state, provider login, skills, and
2906
- permissions. The relay carries prompts, final results, and task state.
3196
+ permissions. The relay carries prompts, final results, task state, and explicitly attached files.
2907
3197
 
2908
3198
  Use the installed \`cohall\` executable when it is available. Fall back to
2909
3199
  \`npx -y @akshar5/cohall\` only when Cohall is not installed globally.
@@ -2964,6 +3254,16 @@ Do not delegate ordinary local work when the other device provides no advantage.
2964
3254
  when \`status\` is \`completed\`; use \`result\` in the current task. Report a
2965
3255
  \`failed\`, \`cancelled\`, or \`cancelling\` state accurately.
2966
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
+
2967
3267
  6. Reuse \`thread_id\` for related follow-ups so the target provider can resume
2968
3268
  its local session:
2969
3269
 
@@ -2978,6 +3278,11 @@ When a coding agent running as a Cohall task delegates again, \`COHALL_THREAD_ID
2978
3278
  and \`COHALL_TASK_ID\` carry its thread and parent task. Pass \`--thread\` and
2979
3279
  \`--parent\` explicitly from a Grok Bot using the IDs in its handoff.
2980
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
+
2981
3286
  ## Message named bots
2982
3287
 
2983
3288
  1. Run \`cohall bots\` and choose a listed target. Use its qualified target when
@@ -3359,7 +3664,8 @@ var TaskResult = Schema5.Struct({
3359
3664
  target_device_id: DeviceId,
3360
3665
  bot_id: Schema5.optionalKey(BotId),
3361
3666
  result: Schema5.optionalKey(Schema5.String),
3362
- error: Schema5.optionalKey(Schema5.String)
3667
+ error: Schema5.optionalKey(Schema5.String),
3668
+ inbox_warning: Schema5.optionalKey(Schema5.String)
3363
3669
  });
3364
3670
  var matchingDevices = (devices, target) => {
3365
3671
  const value = target.replace(/^@/, "");
@@ -3413,6 +3719,20 @@ var taskResult = (task) => TaskResult.make({
3413
3719
  ...task.result === undefined ? {} : { result: task.result },
3414
3720
  ...task.error === undefined ? {} : { error: task.error }
3415
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
+ };
3416
3736
  var createDelegation = Effect5.fn("Cohall.createDelegation")(function* (client, configuration, options) {
3417
3737
  const devices = yield* client.devices();
3418
3738
  if (devices.length === 0) {
@@ -3459,7 +3779,8 @@ var createDelegation = Effect5.fn("Cohall.createDelegation")(function* (client,
3459
3779
  ...options.context === undefined ? {} : { context: options.context },
3460
3780
  ...options.workspace === undefined ? {} : { workspace: options.workspace },
3461
3781
  ...provider === undefined ? {} : { provider },
3462
- ...parentTaskId === undefined ? {} : { parentTaskId }
3782
+ ...parentTaskId === undefined ? {} : { parentTaskId },
3783
+ ...options.attachments === undefined ? {} : { attachments: options.attachments }
3463
3784
  });
3464
3785
  });
3465
3786
  var waitForTask = Effect5.fn("Cohall.waitForTask")(function* (client, initial, timeoutSeconds) {
@@ -5111,10 +5432,143 @@ var switchRelay = async (options) => {
5111
5432
  };
5112
5433
  };
5113
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
+
5533
+ // apps/device/src/mcp-diagnostics.ts
5534
+ init_src();
5535
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
5536
+ import {
5537
+ getDefaultEnvironment,
5538
+ StdioClientTransport
5539
+ } from "@modelcontextprotocol/sdk/client/stdio.js";
5540
+ var checkMcp = async (entrypoint, relayUrl, token) => {
5541
+ const client = new Client({ name: "cohall-doctor", version });
5542
+ const transport = new StdioClientTransport({
5543
+ command: process.execPath,
5544
+ args: [entrypoint, "mcp"],
5545
+ env: {
5546
+ ...getDefaultEnvironment(),
5547
+ COHALL_CONFIG: configurationPath(),
5548
+ COHALL_RELAY_URL: relayUrl,
5549
+ COHALL_CLIENT_TOKEN: token
5550
+ },
5551
+ stderr: "pipe"
5552
+ });
5553
+ try {
5554
+ await client.connect(transport, { timeout: 5000 });
5555
+ const { tools } = await client.listTools(undefined, { timeout: 5000 });
5556
+ if (tools.length === 0)
5557
+ return { status: "error", error: "MCP server exposed no tools" };
5558
+ return { status: "ok", tool_count: tools.length };
5559
+ } catch (cause) {
5560
+ return { status: "error", error: cause instanceof Error ? cause.message : String(cause) };
5561
+ } finally {
5562
+ await client.close();
5563
+ }
5564
+ };
5565
+
5114
5566
  // apps/device/src/cli.ts
5115
5567
  var valueOptions = new Set([
5568
+ "attach",
5116
5569
  "context",
5117
5570
  "context-file",
5571
+ "direction",
5118
5572
  "grok-gateway",
5119
5573
  "error",
5120
5574
  "label",
@@ -5122,6 +5576,7 @@ var valueOptions = new Set([
5122
5576
  "message",
5123
5577
  "message-file",
5124
5578
  "name",
5579
+ "output",
5125
5580
  "parent",
5126
5581
  "prompt",
5127
5582
  "prompt-file",
@@ -5167,10 +5622,13 @@ Usage:
5167
5622
  cohall send [@device-or-bot] [prompt] [delegate options]
5168
5623
  cohall delegate [prompt] [--target @device-or-bot] [--provider provider]
5169
5624
  [--context text] [--thread uuid] [--parent task-id] [--workspace path]
5170
- [--timeout seconds] [--no-wait]
5625
+ [--attach path] [--timeout seconds] [--no-wait]
5171
5626
  cohall status <task-id>
5627
+ cohall inbox [ack <task-id>]
5172
5628
  cohall trace <task-id> [--follow]
5173
5629
  cohall wait <task-id> [--timeout seconds]
5630
+ cohall attachments <task-id>
5631
+ cohall download <task-id> <name> --output path [--direction input|output]
5174
5632
  cohall cancel <task-id>
5175
5633
  cohall reply <task-id> --message-file path | --message text | --message - | --error text
5176
5634
  cohall thread <thread-id>
@@ -5333,14 +5791,14 @@ var pairingToken = async (arguments_) => {
5333
5791
  return token;
5334
5792
  };
5335
5793
  var client = async () => {
5336
- const configuration = await Effect9.runPromise(loadClientConfiguration);
5794
+ const configuration = await Effect10.runPromise(loadClientConfiguration);
5337
5795
  return {
5338
5796
  configuration,
5339
5797
  relay: exports_src.make({ baseUrl: configuration.relayUrl, token: configuration.token })
5340
5798
  };
5341
5799
  };
5342
5800
  var ownerClient = async () => {
5343
- const configuration = await Effect9.runPromise(loadOwnerConfiguration);
5801
+ const configuration = await Effect10.runPromise(loadOwnerConfiguration);
5344
5802
  return {
5345
5803
  configuration,
5346
5804
  relay: exports_src.make({ baseUrl: configuration.relayUrl, token: configuration.token })
@@ -5348,17 +5806,17 @@ var ownerClient = async () => {
5348
5806
  };
5349
5807
  var installSkill = async (target) => {
5350
5808
  const roots = {
5351
- agents: join7(homedir4(), ".agents", "skills", "cohall", "SKILL.md"),
5352
- claude: join7(homedir4(), ".claude", "skills", "cohall", "SKILL.md"),
5353
- opencode: join7(homedir4(), ".config", "opencode", "skills", "cohall", "SKILL.md")
5809
+ agents: join8(homedir4(), ".agents", "skills", "cohall", "SKILL.md"),
5810
+ claude: join8(homedir4(), ".claude", "skills", "cohall", "SKILL.md"),
5811
+ opencode: join8(homedir4(), ".config", "opencode", "skills", "cohall", "SKILL.md")
5354
5812
  };
5355
5813
  const selected = target === "all" ? Object.values(roots) : (target in roots) ? [roots[target]] : undefined;
5356
5814
  if (selected === undefined) {
5357
5815
  throw new Error("Skill target must be agents, claude, opencode, or all");
5358
5816
  }
5359
5817
  for (const path of selected) {
5360
- await mkdir6(dirname6(path), { recursive: true });
5361
- await writeFile6(path, SKILL_default);
5818
+ await mkdir7(dirname6(path), { recursive: true });
5819
+ await writeFile7(path, SKILL_default);
5362
5820
  }
5363
5821
  return selected;
5364
5822
  };
@@ -5368,7 +5826,7 @@ var runCli = async (command, raw) => {
5368
5826
  const arguments_ = parseArguments(raw);
5369
5827
  if (command === "reply") {
5370
5828
  allowOptions(arguments_, ["message", "message-file", "error"]);
5371
- const id = Schema10.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
5829
+ const id = Schema11.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
5372
5830
  const error = option(arguments_, "error");
5373
5831
  if (error !== undefined && (option(arguments_, "message") !== undefined || option(arguments_, "message-file") !== undefined)) {
5374
5832
  throw new Error("Use either --error or --message/--message-file, not both");
@@ -5506,7 +5964,7 @@ var runCli = async (command, raw) => {
5506
5964
  const { relay: relay2 } = await ownerClient();
5507
5965
  const restart = !arguments_.options.has("no-restart");
5508
5966
  if (arguments_.options.has("dry-run")) {
5509
- const devices = await Effect9.runPromise(relay2.devices());
5967
+ const devices = await Effect10.runPromise(relay2.devices());
5510
5968
  assertDeviceOperationSupport(devices);
5511
5969
  print({
5512
5970
  dry_run: true,
@@ -5516,7 +5974,7 @@ var runCli = async (command, raw) => {
5516
5974
  });
5517
5975
  return;
5518
5976
  }
5519
- const operations = await Effect9.runPromise(relay2.createUpgradeOperations({ target: normalizedTarget, restart }));
5977
+ const operations = await Effect10.runPromise(relay2.createUpgradeOperations({ target: normalizedTarget, restart }));
5520
5978
  print({
5521
5979
  queued: operations.length,
5522
5980
  requested_version: normalizedTarget,
@@ -5542,10 +6000,10 @@ var runCli = async (command, raw) => {
5542
6000
  if (arguments_.positionals.length !== 2 || arguments_.positionals[0] !== "abandon") {
5543
6001
  throw new Error("Usage: cohall upgrades [abandon <operation-id>]");
5544
6002
  }
5545
- print(await Effect9.runPromise(relay2.abandonOperation(Schema10.decodeUnknownSync(OperationId)(arguments_.positionals[1]))));
6003
+ print(await Effect10.runPromise(relay2.abandonOperation(Schema11.decodeUnknownSync(OperationId)(arguments_.positionals[1]))));
5546
6004
  return;
5547
6005
  }
5548
- print(await Effect9.runPromise(relay2.operations()));
6006
+ print(await Effect10.runPromise(relay2.operations()));
5549
6007
  return;
5550
6008
  }
5551
6009
  if (command === "join") {
@@ -5624,7 +6082,7 @@ var runCli = async (command, raw) => {
5624
6082
  const existing = await readStoredConfiguration();
5625
6083
  const relayUrl = normalizeRelayUrl(option(arguments_, "relay") ?? existing?.relayUrl ?? "http://127.0.0.1:8787");
5626
6084
  const supplied = values(arguments_, "workspace");
5627
- const workspaces = supplied.length === 0 ? existing?.workspaces ?? [] : await Effect9.runPromise(parseWorkspaces("", JSON.stringify(supplied)));
6085
+ const workspaces = supplied.length === 0 ? existing?.workspaces ?? [] : await Effect10.runPromise(parseWorkspaces("", JSON.stringify(supplied)));
5628
6086
  const sandbox = option(arguments_, "sandbox");
5629
6087
  const model = option(arguments_, "model") ?? existing?.model;
5630
6088
  const gatewayInput = option(arguments_, "grok-gateway") ?? existing?.grokGateway;
@@ -5642,7 +6100,7 @@ var runCli = async (command, raw) => {
5642
6100
  ...model === undefined ? {} : { model },
5643
6101
  ...grokGateway === undefined ? {} : { grokGateway },
5644
6102
  ...(sandbox ?? existing?.sandbox) === undefined ? {} : {
5645
- sandbox: Schema10.decodeUnknownSync(Schema10.Literals(["read-only", "workspace-write", "danger-full-access"]))(sandbox ?? existing?.sandbox)
6103
+ sandbox: Schema11.decodeUnknownSync(Schema11.Literals(["read-only", "workspace-write", "danger-full-access"]))(sandbox ?? existing?.sandbox)
5646
6104
  }
5647
6105
  });
5648
6106
  await writeStoredConfiguration(configuration2);
@@ -5685,7 +6143,7 @@ var runCli = async (command, raw) => {
5685
6143
  noPositionals(arguments_, command);
5686
6144
  if (arguments_.options.has("all")) {
5687
6145
  const { relay: relay2 } = await client();
5688
- print(allDeviceHealth(await Effect9.runPromise(relay2.devices()), version));
6146
+ print(allDeviceHealth(await Effect10.runPromise(relay2.devices()), version));
5689
6147
  return;
5690
6148
  }
5691
6149
  const configuration2 = await readStoredConfiguration();
@@ -5707,15 +6165,17 @@ var runCli = async (command, raw) => {
5707
6165
  return;
5708
6166
  });
5709
6167
  const clientToken = process.env.COHALL_CLIENT_TOKEN ?? storedCredentials.clientToken;
6168
+ const mcp = clientToken === undefined ? { status: "not configured" } : await checkMcp(process.argv[1] ?? "", relayUrl, clientToken);
5710
6169
  const hasDeviceCredential = process.env.COHALL_DEVICE_TOKEN !== undefined || storedCredentials.deviceToken !== undefined;
5711
6170
  const deviceId = process.env.COHALL_DEVICE_ID ?? configuration2?.deviceId;
5712
- 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(() => {
6171
+ 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(() => {
5713
6172
  return;
5714
6173
  });
5715
6174
  const versionWarning = deviceVersionWarning(version, currentDevice?.version);
5716
6175
  const warnings = [
5717
6176
  ...response?.ok === true ? [] : ["Relay is unreachable"],
5718
6177
  ...versionWarning === undefined ? [] : [versionWarning],
6178
+ ...mcp.status === "error" ? [`MCP check failed: ${mcp.error}`] : [],
5719
6179
  ...hasDeviceCredential && response?.ok === true && currentDevice?.status !== "online" && currentDevice?.status !== "busy" ? [
5720
6180
  clientToken === undefined ? "Device status cannot be checked without a client credential" : "Device daemon is not connected or is not registered with the relay"
5721
6181
  ] : [],
@@ -5729,6 +6189,7 @@ var runCli = async (command, raw) => {
5729
6189
  relay_url: relayUrl,
5730
6190
  config_path: configurationPath(),
5731
6191
  client_credential: process.env.COHALL_CLIENT_TOKEN !== undefined || storedCredentials.clientToken !== undefined,
6192
+ mcp,
5732
6193
  device_credential: hasDeviceCredential,
5733
6194
  workspaces: configuration2?.workspaces ?? [],
5734
6195
  providers: providerExecutables,
@@ -5751,7 +6212,7 @@ var runCli = async (command, raw) => {
5751
6212
  allowOptions(arguments_, []);
5752
6213
  noPositionals(arguments_, command);
5753
6214
  const { relay: relay2 } = await client();
5754
- print(deviceVersions(await Effect9.runPromise(relay2.devices()), version));
6215
+ print(deviceVersions(await Effect10.runPromise(relay2.devices()), version));
5755
6216
  return;
5756
6217
  }
5757
6218
  if (command === "usage") {
@@ -5759,7 +6220,7 @@ var runCli = async (command, raw) => {
5759
6220
  noPositionals(arguments_, command);
5760
6221
  const { relay: relay2 } = await client();
5761
6222
  print({
5762
- ...await Effect9.runPromise(relay2.usage()),
6223
+ ...await Effect10.runPromise(relay2.usage()),
5763
6224
  scope: "Retained Cohall task activity; provider token and billing usage are not available."
5764
6225
  });
5765
6226
  return;
@@ -5770,7 +6231,7 @@ var runCli = async (command, raw) => {
5770
6231
  allowOptions(arguments_, ["client-only", "label"]);
5771
6232
  noPositionals(arguments_, command);
5772
6233
  const roles = arguments_.options.has("client-only") ? ["client"] : ["client", "device"];
5773
- const credential = await Effect9.runPromise(relay2.createPairing({ label: option(arguments_, "label") ?? "Cohall device", roles }));
6234
+ const credential = await Effect10.runPromise(relay2.createPairing({ label: option(arguments_, "label") ?? "Cohall device", roles }));
5774
6235
  print({
5775
6236
  relay_url: configuration2.relayUrl,
5776
6237
  pairing_token: credential.token,
@@ -5782,35 +6243,36 @@ var runCli = async (command, raw) => {
5782
6243
  if (command === "sessions") {
5783
6244
  allowOptions(arguments_, []);
5784
6245
  noPositionals(arguments_, command);
5785
- print(await Effect9.runPromise(relay2.authSessions()));
6246
+ print(await Effect10.runPromise(relay2.authSessions()));
5786
6247
  return;
5787
6248
  }
5788
6249
  if (command === "forget") {
5789
6250
  allowOptions(arguments_, []);
5790
- print(await Effect9.runPromise(relay2.forgetDevice(Schema10.decodeUnknownSync(DeviceId)(identifier(arguments_, "device id")))));
6251
+ print(await Effect10.runPromise(relay2.forgetDevice(Schema11.decodeUnknownSync(DeviceId)(identifier(arguments_, "device id")))));
5791
6252
  return;
5792
6253
  }
5793
6254
  allowOptions(arguments_, []);
5794
- print(await Effect9.runPromise(relay2.revokeAuthSession(Schema10.decodeUnknownSync(AuthSessionId)(identifier(arguments_, "session id")))));
6255
+ print(await Effect10.runPromise(relay2.revokeAuthSession(Schema11.decodeUnknownSync(AuthSessionId)(identifier(arguments_, "session id")))));
5795
6256
  return;
5796
6257
  }
5797
6258
  const { configuration, relay } = await client();
5798
6259
  if (command === "devices") {
5799
6260
  allowOptions(arguments_, []);
5800
6261
  noPositionals(arguments_, command);
5801
- print(await Effect9.runPromise(relay.devices()));
6262
+ print(await Effect10.runPromise(relay.devices()));
5802
6263
  return;
5803
6264
  }
5804
6265
  if (command === "bots") {
5805
6266
  allowOptions(arguments_, []);
5806
6267
  noPositionals(arguments_, command);
5807
- print(listBots(await Effect9.runPromise(relay.devices())));
6268
+ print(listBots(await Effect10.runPromise(relay.devices())));
5808
6269
  return;
5809
6270
  }
5810
6271
  if (command === "delegate" || command === "send") {
5811
6272
  allowOptions(arguments_, [
5812
6273
  "context",
5813
6274
  "context-file",
6275
+ "attach",
5814
6276
  "no-wait",
5815
6277
  "parent",
5816
6278
  "prompt",
@@ -5839,56 +6301,96 @@ var runCli = async (command, raw) => {
5839
6301
  throw new Error("Use either a positional prompt or --prompt, not both");
5840
6302
  }
5841
6303
  const context = await readInput(arguments_, "context");
6304
+ const attachments2 = await readInputAttachments(values(arguments_, "attach"));
5842
6305
  const target = positionalTarget ?? option(arguments_, "target");
5843
6306
  const thread = option(arguments_, "thread");
5844
6307
  const workspace = option(arguments_, "workspace");
5845
6308
  const provider = option(arguments_, "provider");
5846
6309
  const parent = option(arguments_, "parent");
5847
- const task = await Effect9.runPromise(createDelegation(relay, configuration, {
6310
+ const task = await Effect10.runPromise(createDelegation(relay, configuration, {
5848
6311
  prompt,
5849
6312
  ...target === undefined ? {} : { target },
5850
6313
  ...context === undefined ? {} : { context },
5851
- ...thread === undefined ? {} : { threadId: Schema10.decodeUnknownSync(ThreadId)(thread) },
5852
- ...parent === undefined ? {} : { parentTaskId: Schema10.decodeUnknownSync(TaskId)(parent) },
6314
+ ...thread === undefined ? {} : { threadId: Schema11.decodeUnknownSync(ThreadId)(thread) },
6315
+ ...parent === undefined ? {} : { parentTaskId: Schema11.decodeUnknownSync(TaskId)(parent) },
5853
6316
  ...workspace === undefined ? {} : { workspace },
5854
- ...provider === undefined ? {} : { provider: Schema10.decodeUnknownSync(Provider)(provider) }
6317
+ ...provider === undefined ? {} : { provider: Schema11.decodeUnknownSync(Provider)(provider) },
6318
+ ...attachments2.length === 0 ? {} : { attachments: attachments2 }
5855
6319
  }));
5856
- print(taskResult(arguments_.options.has("no-wait") ? task : await Effect9.runPromise(waitForTask(relay, task, waitSeconds))));
6320
+ const completed = arguments_.options.has("no-wait") ? task : await Effect10.runPromise(waitForTask(relay, task, waitSeconds));
6321
+ print(arguments_.options.has("no-wait") ? taskResult(completed) : await acknowledgedTaskResult(relay, completed));
6322
+ return;
6323
+ }
6324
+ if (command === "inbox") {
6325
+ allowOptions(arguments_, []);
6326
+ if (arguments_.positionals[0] === "ack") {
6327
+ if (arguments_.positionals.length !== 2)
6328
+ throw new Error("Usage: cohall inbox ack <task-id>");
6329
+ const id = Schema11.decodeUnknownSync(TaskId)(arguments_.positionals[1]);
6330
+ print(await Effect10.runPromise(relay.acknowledgeCompletion(id)));
6331
+ return;
6332
+ }
6333
+ noPositionals(arguments_, command);
6334
+ print(await Effect10.runPromise(relay.inbox()));
5857
6335
  return;
5858
6336
  }
5859
6337
  if (command === "status") {
5860
6338
  allowOptions(arguments_, []);
5861
- const id = Schema10.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
5862
- print(taskResult(await Effect9.runPromise(relay.getTask(id))));
6339
+ const id = Schema11.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
6340
+ print(taskResult(await Effect10.runPromise(relay.getTask(id))));
6341
+ return;
6342
+ }
6343
+ if (command === "attachments") {
6344
+ allowOptions(arguments_, []);
6345
+ const id = Schema11.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
6346
+ print(await Effect10.runPromise(relay.listAttachments(id)));
6347
+ return;
6348
+ }
6349
+ if (command === "download") {
6350
+ allowOptions(arguments_, ["output", "direction"]);
6351
+ if (arguments_.positionals.length !== 2) {
6352
+ throw new Error("Usage: cohall download <task-id> <name> --output path [--direction input|output]");
6353
+ }
6354
+ const destination = option(arguments_, "output");
6355
+ if (destination === undefined) {
6356
+ throw new Error("--output is required");
6357
+ }
6358
+ const id = Schema11.decodeUnknownSync(TaskId)(arguments_.positionals[0]);
6359
+ const name = Schema11.decodeUnknownSync(AttachmentName)(arguments_.positionals[1]);
6360
+ const directionInput = option(arguments_, "direction");
6361
+ const direction = directionInput === undefined ? undefined : Schema11.decodeUnknownSync(AttachmentDirection)(directionInput);
6362
+ const data = await Effect10.runPromise(relay.readAttachment(id, name, direction));
6363
+ await writeFile7(destination, data, { flag: "wx", mode: 384 });
6364
+ print({ task_id: id, name, path: resolve3(destination), bytes: data.length });
5863
6365
  return;
5864
6366
  }
5865
6367
  if (command === "trace") {
5866
6368
  allowOptions(arguments_, ["follow"]);
5867
- const id = Schema10.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
6369
+ const id = Schema11.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
5868
6370
  if (arguments_.options.has("follow")) {
5869
- await Effect9.runPromise(followTaskTrace(relay, id, printLine));
6371
+ await Effect10.runPromise(followTaskTrace(relay, id, printLine));
5870
6372
  return;
5871
6373
  }
5872
- print(await Effect9.runPromise(relay.traceTask(id)));
6374
+ print(await Effect10.runPromise(relay.traceTask(id)));
5873
6375
  return;
5874
6376
  }
5875
6377
  if (command === "wait") {
5876
6378
  allowOptions(arguments_, ["timeout"]);
5877
- const id = Schema10.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
5878
- const task = await Effect9.runPromise(relay.getTask(id));
5879
- print(taskResult(await Effect9.runPromise(waitForTask(relay, task, timeout(arguments_)))));
6379
+ const id = Schema11.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
6380
+ const task = await Effect10.runPromise(relay.getTask(id));
6381
+ print(taskResult(await Effect10.runPromise(waitForTask(relay, task, timeout(arguments_)))));
5880
6382
  return;
5881
6383
  }
5882
6384
  if (command === "cancel") {
5883
6385
  allowOptions(arguments_, []);
5884
- const id = Schema10.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
5885
- print(taskResult(await Effect9.runPromise(relay.cancelTask(id))));
6386
+ const id = Schema11.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
6387
+ print(taskResult(await Effect10.runPromise(relay.cancelTask(id))));
5886
6388
  return;
5887
6389
  }
5888
6390
  if (command === "thread") {
5889
6391
  allowOptions(arguments_, []);
5890
- const id = Schema10.decodeUnknownSync(ThreadId)(identifier(arguments_, "thread id"));
5891
- print(await Effect9.runPromise(threadContext(relay, id)));
6392
+ const id = Schema11.decodeUnknownSync(ThreadId)(identifier(arguments_, "thread id"));
6393
+ print(await Effect10.runPromise(threadContext(relay, id)));
5892
6394
  return;
5893
6395
  }
5894
6396
  throw new Error(`Unknown Cohall command: ${command}`);
@@ -5896,15 +6398,15 @@ var runCli = async (command, raw) => {
5896
6398
 
5897
6399
  // apps/device/src/daemon.ts
5898
6400
  init_src();
5899
- import { Effect as Effect10, Schedule as Schedule3, Schema as Schema11 } from "effect";
5900
- import { constants as constants3 } from "node:fs";
5901
- import { open as open3, realpath as realpath5, stat as stat5 } from "node:fs/promises";
6401
+ import { Effect as Effect11, Schedule as Schedule3, Schema as Schema12 } from "effect";
6402
+ import { constants as constants4 } from "node:fs";
6403
+ import { open as open4, realpath as realpath5, stat as stat5 } from "node:fs/promises";
5902
6404
  import { arch, hostname as hostname3, platform as platform4 } from "node:os";
5903
- import { basename as basename2, isAbsolute as isAbsolute3, relative as relative2 } from "node:path";
6405
+ import { basename as basename3, isAbsolute as isAbsolute3, relative as relative2 } from "node:path";
5904
6406
  import { WebSocket as WebSocket2 } from "ws";
5905
6407
  var maxQueuedRelayMessages = 8;
5906
6408
 
5907
- class DeviceConnectionError extends Schema11.TaggedErrorClass()("Device.ConnectionError", { message: Schema11.String }) {
6409
+ class DeviceConnectionError extends Schema12.TaggedErrorClass()("Device.ConnectionError", { message: Schema12.String }) {
5908
6410
  }
5909
6411
  var socketUrl = (configuration) => {
5910
6412
  const url = new URL(configuration.relayUrl);
@@ -5930,6 +6432,9 @@ var capabilities = (providers) => {
5930
6432
  label: providerLabel(provider),
5931
6433
  detail: provider === "grok-bot" ? "Named bots through this computer's local Grok Bot gateway" : `${providerLabel(provider)} executable detected; authentication is checked when work starts`
5932
6434
  }));
6435
+ if (providers.some((provider) => provider !== "grok-bot")) {
6436
+ values2.push({ id: "task-attachments", label: "Task file attachments" });
6437
+ }
5933
6438
  if (findExecutable("google-chrome") !== undefined || findExecutable("chromium") !== undefined || platform4() === "darwin") {
5934
6439
  values2.push({ id: "browser-session", label: "Signed-in browser" });
5935
6440
  }
@@ -5962,7 +6467,7 @@ var describeDevice = (configuration, status = "online", bots = []) => {
5962
6467
  capabilities: capabilities(providers),
5963
6468
  workspaces: configuration.workspaces.map((path) => ({
5964
6469
  path,
5965
- label: basename2(path) || path
6470
+ label: basename3(path) || path
5966
6471
  })),
5967
6472
  version,
5968
6473
  lastSeenAt: now()
@@ -5986,8 +6491,8 @@ var allowedWorkspace = async (configuration, requested) => {
5986
6491
  var openAllowedWorkspace = async (configuration, requested) => {
5987
6492
  const path = await allowedWorkspace(configuration, requested);
5988
6493
  const operatingSystem2 = platform4();
5989
- const flags = constants3.O_RDONLY | (operatingSystem2 === "win32" ? 0 : constants3.O_DIRECTORY | constants3.O_NOFOLLOW);
5990
- const handle = await open3(path, flags);
6494
+ const flags = constants4.O_RDONLY | (operatingSystem2 === "win32" ? 0 : constants4.O_DIRECTORY | constants4.O_NOFOLLOW);
6495
+ const handle = await open4(path, flags);
5991
6496
  const identity = await handle.stat();
5992
6497
  if (!identity.isDirectory()) {
5993
6498
  await handle.close();
@@ -6012,7 +6517,7 @@ var openAllowedWorkspace = async (configuration, requested) => {
6012
6517
  close: () => handle.close()
6013
6518
  };
6014
6519
  };
6015
- var promptFor = (task, deviceName) => {
6520
+ var promptFor = (task, deviceName, files) => {
6016
6521
  const context = task.context === undefined ? "" : `
6017
6522
 
6018
6523
  Relevant context supplied by the sending agent:
@@ -6022,6 +6527,14 @@ ${task.context}`;
6022
6527
  "Complete the delegated task using this device's local workspace, tools, credentials, and signed-in services.",
6023
6528
  "Never read, reveal, copy, or use Cohall configuration files or Cohall authentication tokens.",
6024
6529
  "Return a concise, complete result with the evidence the sending agent needs.",
6530
+ ...files === undefined ? [] : [
6531
+ ...files.inputNames.length === 0 ? [] : [
6532
+ `Input directory: ${files.input}`,
6533
+ `Input file names: ${files.inputNames.join(", ")}`
6534
+ ],
6535
+ `Output directory: ${files.output}`,
6536
+ "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."
6537
+ ],
6025
6538
  `
6026
6539
  Task:
6027
6540
  ${task.prompt}${context}`
@@ -6033,11 +6546,25 @@ var send = (state, event) => {
6033
6546
  state.socket.send(JSON.stringify(event));
6034
6547
  }
6035
6548
  };
6549
+ var omitOutputFiles = (event, note) => {
6550
+ if (event._tag !== "TaskFinished" || (event.attachments?.length ?? 0) === 0) {
6551
+ return event;
6552
+ }
6553
+ const message = `
6554
+
6555
+ [Output files omitted: ${note}]`;
6556
+ return SocketEvent.make({
6557
+ _tag: "TaskFinished",
6558
+ taskId: event.taskId,
6559
+ result: `${event.result.slice(0, 131072 - message.length)}${message}`,
6560
+ ...event.providerSessionId === undefined ? {} : { providerSessionId: event.providerSessionId }
6561
+ });
6562
+ };
6036
6563
  var sendTerminal = (state, taskId, event) => {
6037
- const payload = JSON.stringify(event);
6038
- state.terminal.set(taskId, payload);
6564
+ const safeEvent = state.supportsAttachments ? event : omitOutputFiles(event, "the relay does not support attachments. Upgrade the relay and retry.");
6565
+ state.terminal.set(taskId, safeEvent);
6039
6566
  if (state.socket?.readyState === WebSocket2.OPEN) {
6040
- state.socket.send(payload);
6567
+ state.socket.send(JSON.stringify(safeEvent));
6041
6568
  }
6042
6569
  };
6043
6570
  var sendOperationTerminal = (state, operationId, event) => {
@@ -6100,9 +6627,9 @@ var execute = (configuration, state, task) => {
6100
6627
  const controller = new AbortController;
6101
6628
  state.tasks.set(task.id, { task, controller });
6102
6629
  send(state, SocketEvent.make({ _tag: "TaskAccepted", taskId: task.id }));
6103
- const workflow = Effect10.gen(function* () {
6630
+ const workflow = Effect11.gen(function* () {
6104
6631
  if (task.provider === "grok-bot") {
6105
- return yield* Effect10.tryPromise({
6632
+ return yield* Effect11.tryPromise({
6106
6633
  try: (signal) => runGrokBot(configuration.grokGateway, task, signal),
6107
6634
  catch: (cause) => new ProviderRunError({
6108
6635
  provider: task.provider,
@@ -6110,39 +6637,64 @@ var execute = (configuration, state, task) => {
6110
6637
  })
6111
6638
  });
6112
6639
  }
6113
- const workspace = yield* Effect10.tryPromise({
6640
+ const provider = task.provider;
6641
+ if (!state.supportsAttachments && (task.inputAttachmentNames?.length ?? 0) > 0) {
6642
+ return yield* new ProviderRunError({
6643
+ provider,
6644
+ message: "The relay does not support task file attachments"
6645
+ });
6646
+ }
6647
+ const workspace = yield* Effect11.tryPromise({
6114
6648
  try: () => openAllowedWorkspace(configuration, task.workspace),
6115
6649
  catch: (cause) => new ProviderRunError({
6116
- provider: task.provider,
6650
+ provider,
6117
6651
  message: cause instanceof Error ? cause.message : String(cause)
6118
6652
  })
6119
6653
  });
6120
- const sessionId = task.providerSessionId ?? state.sessions.get(sessionKey(task));
6121
- return yield* run({
6122
- provider: task.provider,
6123
- threadId: task.threadId,
6124
- taskId: task.id,
6125
- prompt: promptFor(task, configuration.name),
6126
- cwd: workspace.cwd,
6127
- beforeSpawn: workspace.validate,
6128
- ...sessionId === undefined ? {} : { sessionId },
6129
- ...configuration.model === undefined ? {} : { model: configuration.model },
6130
- ...configuration.sandbox === undefined ? {} : { sandbox: configuration.sandbox }
6131
- }).pipe(Effect10.ensuring(Effect10.promise(() => workspace.close().catch(() => {
6654
+ return yield* Effect11.gen(function* () {
6655
+ const files = state.supportsAttachments ? yield* Effect11.tryPromise({
6656
+ try: (signal) => prepareTaskFiles(configuration, task.id, task.inputAttachmentNames ?? [], signal),
6657
+ catch: (cause) => new ProviderRunError({
6658
+ provider,
6659
+ message: cause instanceof Error ? cause.message : String(cause)
6660
+ })
6661
+ }) : undefined;
6662
+ const sessionId = task.providerSessionId ?? state.sessions.get(sessionKey(task));
6663
+ return yield* run({
6664
+ provider,
6665
+ threadId: task.threadId,
6666
+ taskId: task.id,
6667
+ prompt: promptFor(task, configuration.name, files),
6668
+ cwd: workspace.cwd,
6669
+ beforeSpawn: workspace.validate,
6670
+ ...sessionId === undefined ? {} : { sessionId },
6671
+ ...configuration.model === undefined ? {} : { model: configuration.model },
6672
+ ...configuration.sandbox === undefined ? {} : { sandbox: configuration.sandbox }
6673
+ }).pipe(Effect11.flatMap((result) => files === undefined ? Effect11.succeed(result) : Effect11.tryPromise({
6674
+ try: async () => ({ ...result, attachments: await files.collectOutputs() }),
6675
+ catch: (cause) => new ProviderRunError({
6676
+ provider,
6677
+ message: cause instanceof Error ? cause.message : String(cause)
6678
+ })
6679
+ })), Effect11.ensuring(files === undefined ? Effect11.void : Effect11.promise(() => files.cleanup())));
6680
+ }).pipe(Effect11.ensuring(Effect11.promise(() => workspace.close().catch(() => {
6132
6681
  return;
6133
6682
  }))));
6134
6683
  });
6135
- Effect10.runPromise(workflow, { signal: controller.signal }).then((result) => {
6684
+ Effect11.runPromise(workflow, { signal: controller.signal }).then((result) => {
6685
+ const finished = SocketEvent.make({
6686
+ _tag: "TaskFinished",
6687
+ taskId: task.id,
6688
+ result: result.result,
6689
+ ...result.sessionId === undefined ? {} : { providerSessionId: result.sessionId },
6690
+ ...result.attachments === undefined || result.attachments.length === 0 ? {} : { attachments: result.attachments }
6691
+ });
6692
+ 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;
6136
6693
  if (result.sessionId !== undefined) {
6137
6694
  state.sessions.set(sessionKey(task), result.sessionId);
6138
6695
  }
6139
6696
  remember(state, task.id);
6140
- sendTerminal(state, task.id, SocketEvent.make({
6141
- _tag: "TaskFinished",
6142
- taskId: task.id,
6143
- result: result.result,
6144
- ...result.sessionId === undefined ? {} : { providerSessionId: result.sessionId }
6145
- }));
6697
+ sendTerminal(state, task.id, bounded2);
6146
6698
  }).catch((cause) => {
6147
6699
  remember(state, task.id);
6148
6700
  sendTerminal(state, task.id, controller.signal.aborted ? SocketEvent.make({ _tag: "TaskCancelled", taskId: task.id }) : SocketEvent.make({
@@ -6233,7 +6785,7 @@ var cancel = (state, taskId) => {
6233
6785
  remember(state, taskId);
6234
6786
  sendTerminal(state, taskId, SocketEvent.make({ _tag: "TaskCancelled", taskId }));
6235
6787
  };
6236
- var connect = (configuration, state) => Effect10.tryPromise({
6788
+ var connect = (configuration, state) => Effect11.tryPromise({
6237
6789
  try: (signal) => new Promise((complete) => {
6238
6790
  const socket = new WebSocket2(socketUrl(configuration), {
6239
6791
  maxPayload: maxSocketPayloadBytes,
@@ -6298,18 +6850,21 @@ var connect = (configuration, state) => Effect10.tryPromise({
6298
6850
  return;
6299
6851
  }
6300
6852
  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");
6301
- const event = await Effect10.runPromise(Effect10.try({
6853
+ const event = await Effect11.runPromise(Effect11.try({
6302
6854
  try: () => JSON.parse(text3),
6303
6855
  catch: () => new DeviceConnectionError({ message: "Relay sent invalid JSON" })
6304
- }).pipe(Effect10.flatMap(decodeSocketEvent)));
6856
+ }).pipe(Effect11.flatMap(decodeSocketEvent)));
6305
6857
  if (event._tag === "Connected") {
6858
+ state.supportsAttachments = event.taskAttachments === true;
6306
6859
  state.socket = socket;
6307
6860
  socket.send(JSON.stringify(SocketEvent.make({
6308
6861
  _tag: "DeviceHello",
6309
6862
  device: describeDevice(configuration, state.tasks.size > 0 || state.operation !== undefined ? "busy" : "online", bots)
6310
6863
  })));
6311
- for (const payload of state.terminal.values()) {
6312
- socket.send(payload);
6864
+ for (const [taskId, pending] of state.terminal) {
6865
+ const safeEvent = state.supportsAttachments ? pending : omitOutputFiles(pending, "the relay does not support attachments. Upgrade the relay and retry.");
6866
+ state.terminal.set(taskId, safeEvent);
6867
+ socket.send(JSON.stringify(safeEvent));
6313
6868
  }
6314
6869
  for (const payload of state.operationTerminal.values()) {
6315
6870
  socket.send(payload);
@@ -6359,6 +6914,7 @@ var connect = (configuration, state) => Effect10.tryPromise({
6359
6914
  var runDaemon = (configuration) => {
6360
6915
  const state = {
6361
6916
  socket: undefined,
6917
+ supportsAttachments: false,
6362
6918
  processing: Promise.resolve(),
6363
6919
  terminal: new Map,
6364
6920
  queue: [],
@@ -6370,7 +6926,7 @@ var runDaemon = (configuration) => {
6370
6926
  operationTerminal: new Map,
6371
6927
  completedOperations: new Set
6372
6928
  };
6373
- return connect(configuration, state).pipe(Effect10.repeat({ schedule: Schedule3.spaced("2 seconds") }), Effect10.ensuring(Effect10.sync(() => {
6929
+ return connect(configuration, state).pipe(Effect11.repeat({ schedule: Schedule3.spaced("2 seconds") }), Effect11.ensuring(Effect11.sync(() => {
6374
6930
  state.socket?.close();
6375
6931
  for (const { controller } of state.tasks.values()) {
6376
6932
  controller.abort();
@@ -6382,8 +6938,9 @@ var runDaemon = (configuration) => {
6382
6938
  init_src();
6383
6939
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6384
6940
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6385
- import { Effect as Effect11 } from "effect";
6941
+ import { Effect as Effect12, Schema as Schema13 } from "effect";
6386
6942
  import * as z from "zod/v4";
6943
+ import { writeFile as writeFile8 } from "node:fs/promises";
6387
6944
  var output = (value) => ({
6388
6945
  content: [{ type: "text", text: JSON.stringify(value, null, 2) }]
6389
6946
  });
@@ -6397,12 +6954,12 @@ var runMcp = async (configuration) => {
6397
6954
  title: "List Cohall devices",
6398
6955
  description: "List devices, their availability, local providers, capabilities, and allowed workspaces.",
6399
6956
  inputSchema: {}
6400
- }, async () => output(await Effect11.runPromise(client2.devices())));
6957
+ }, async () => output(await Effect12.runPromise(client2.devices())));
6401
6958
  server.registerTool("list_bots", {
6402
6959
  title: "List Cohall bots",
6403
6960
  description: "List named Grok Bots on paired devices, their host availability, and unambiguous targets for delegate. Bot names can also be used when unique.",
6404
6961
  inputSchema: {}
6405
- }, async () => output(listBots(await Effect11.runPromise(client2.devices()))));
6962
+ }, async () => output(listBots(await Effect12.runPromise(client2.devices()))));
6406
6963
  server.registerTool("delegate", {
6407
6964
  title: "Send work to a device or bot",
6408
6965
  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.",
@@ -6414,6 +6971,7 @@ var runMcp = async (configuration) => {
6414
6971
  thread_id: z.string().uuid().optional(),
6415
6972
  parent_task_id: z.string().uuid().optional().describe("The task delegating this work. Inherited from COHALL_TASK_ID when present."),
6416
6973
  workspace: z.string().max(4096).optional().describe("Coding provider workspace. Omit for named Grok Bots."),
6974
+ 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."),
6417
6975
  wait: z.boolean().default(true),
6418
6976
  timeout_seconds: z.number().int().min(5).max(86400).default(900)
6419
6977
  }
@@ -6425,30 +6983,65 @@ var runMcp = async (configuration) => {
6425
6983
  thread_id,
6426
6984
  parent_task_id,
6427
6985
  workspace,
6986
+ attachment_paths,
6428
6987
  wait,
6429
6988
  timeout_seconds
6430
6989
  }) => {
6431
- const task = await Effect11.runPromise(createDelegation(client2, configuration, {
6990
+ const attachments2 = await readInputAttachments(attachment_paths ?? []);
6991
+ const task = await Effect12.runPromise(createDelegation(client2, configuration, {
6432
6992
  prompt,
6433
6993
  ...provider === undefined ? {} : { provider },
6434
6994
  ...target === undefined ? {} : { target },
6435
6995
  ...context === undefined ? {} : { context },
6436
6996
  ...thread_id === undefined ? {} : { threadId: ThreadId.make(thread_id) },
6437
6997
  ...parent_task_id === undefined ? {} : { parentTaskId: TaskId.make(parent_task_id) },
6438
- ...workspace === undefined ? {} : { workspace }
6998
+ ...workspace === undefined ? {} : { workspace },
6999
+ ...attachments2.length === 0 ? {} : { attachments: attachments2 }
6439
7000
  }));
6440
- return output(taskResult(wait ? await Effect11.runPromise(waitForTask(client2, task, timeout_seconds)) : task));
7001
+ const completed = wait ? await Effect12.runPromise(waitForTask(client2, task, timeout_seconds)) : task;
7002
+ return output(wait ? await acknowledgedTaskResult(client2, completed) : taskResult(completed));
6441
7003
  });
7004
+ server.registerTool("completion_inbox", {
7005
+ title: "List completed work",
7006
+ 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.",
7007
+ inputSchema: {}
7008
+ }, async () => output(await Effect12.runPromise(client2.inbox())));
7009
+ server.registerTool("acknowledge_completion", {
7010
+ title: "Acknowledge completed work",
7011
+ description: "Remove a handled task from this client's completion inbox.",
7012
+ inputSchema: { task_id: z.string().uuid() }
7013
+ }, async ({ task_id }) => output(await Effect12.runPromise(client2.acknowledgeCompletion(TaskId.make(task_id)))));
6442
7014
  server.registerTool("task_status", {
6443
7015
  title: "Get delegated task status",
6444
7016
  description: "Read the current state and result of a Cohall task.",
6445
7017
  inputSchema: { task_id: z.string().uuid() }
6446
- }, async ({ task_id }) => output(taskResult(await Effect11.runPromise(client2.getTask(TaskId.make(task_id))))));
7018
+ }, async ({ task_id }) => output(taskResult(await Effect12.runPromise(client2.getTask(TaskId.make(task_id))))));
7019
+ server.registerTool("list_task_attachments", {
7020
+ title: "List task files",
7021
+ description: "List input and output files retained with a delegated task.",
7022
+ inputSchema: { task_id: z.string().uuid() }
7023
+ }, async ({ task_id }) => output(await Effect12.runPromise(client2.listAttachments(TaskId.make(task_id)))));
7024
+ server.registerTool("download_task_attachment", {
7025
+ title: "Download a task file",
7026
+ description: "Save one task file to a new local path. Existing files are never overwritten.",
7027
+ inputSchema: {
7028
+ task_id: z.string().uuid(),
7029
+ name: z.string(),
7030
+ output_path: z.string(),
7031
+ direction: z.enum(["input", "output"]).optional()
7032
+ }
7033
+ }, async ({ task_id, name, output_path, direction }) => {
7034
+ const safeName = Schema13.decodeUnknownSync(AttachmentName)(name);
7035
+ const safeDirection = direction === undefined ? undefined : Schema13.decodeUnknownSync(AttachmentDirection)(direction);
7036
+ const data = await Effect12.runPromise(client2.readAttachment(TaskId.make(task_id), safeName, safeDirection));
7037
+ await writeFile8(output_path, data, { flag: "wx", mode: 384 });
7038
+ return output({ task_id, name: safeName, output_path, bytes: data.length });
7039
+ });
6447
7040
  server.registerTool("task_trace", {
6448
7041
  title: "Trace a delegated task",
6449
7042
  description: "Read the redacted relay and device lifecycle for troubleshooting a Cohall task.",
6450
7043
  inputSchema: { task_id: z.string().uuid() }
6451
- }, async ({ task_id }) => output(await Effect11.runPromise(client2.traceTask(TaskId.make(task_id)))));
7044
+ }, async ({ task_id }) => output(await Effect12.runPromise(client2.traceTask(TaskId.make(task_id)))));
6452
7045
  server.registerTool("wait_task", {
6453
7046
  title: "Wait for delegated work",
6454
7047
  description: "Wait for a Cohall task to finish and return its final result.",
@@ -6457,19 +7050,19 @@ var runMcp = async (configuration) => {
6457
7050
  timeout_seconds: z.number().int().min(5).max(86400).default(900)
6458
7051
  }
6459
7052
  }, async ({ task_id, timeout_seconds }) => {
6460
- const task = await Effect11.runPromise(client2.getTask(TaskId.make(task_id)));
6461
- return output(taskResult(await Effect11.runPromise(waitForTask(client2, task, timeout_seconds))));
7053
+ const task = await Effect12.runPromise(client2.getTask(TaskId.make(task_id)));
7054
+ return output(taskResult(await Effect12.runPromise(waitForTask(client2, task, timeout_seconds))));
6462
7055
  });
6463
7056
  server.registerTool("cancel_task", {
6464
7057
  title: "Cancel delegated work",
6465
7058
  description: "Request cancellation of coding work or a queued Bot task. Active Grok Bot tasks must be stopped in Grok Bot.",
6466
7059
  inputSchema: { task_id: z.string().uuid() }
6467
- }, async ({ task_id }) => output(taskResult(await Effect11.runPromise(client2.cancelTask(TaskId.make(task_id))))));
7060
+ }, async ({ task_id }) => output(taskResult(await Effect12.runPromise(client2.cancelTask(TaskId.make(task_id))))));
6468
7061
  server.registerTool("thread_context", {
6469
7062
  title: "Read a Cohall thread",
6470
7063
  description: "Read shared task prompts and final results for a cross-device thread.",
6471
7064
  inputSchema: { thread_id: z.string().uuid() }
6472
- }, async ({ thread_id }) => output(await Effect11.runPromise(threadContext(client2, ThreadId.make(thread_id)))));
7065
+ }, async ({ thread_id }) => output(await Effect12.runPromise(threadContext(client2, ThreadId.make(thread_id)))));
6473
7066
  await server.connect(new StdioServerTransport);
6474
7067
  };
6475
7068
 
@@ -6498,12 +7091,12 @@ var main = async () => {
6498
7091
  return;
6499
7092
  }
6500
7093
  if (command === "device") {
6501
- const configuration = await Effect15.runPromise(loadDeviceConfiguration);
6502
- await Effect15.runPromise(runDaemon(configuration));
7094
+ const configuration = await Effect16.runPromise(loadDeviceConfiguration);
7095
+ await Effect16.runPromise(runDaemon(configuration));
6503
7096
  return;
6504
7097
  }
6505
7098
  if (command === "mcp") {
6506
- const configuration = await Effect15.runPromise(loadClientConfiguration);
7099
+ const configuration = await Effect16.runPromise(loadClientConfiguration);
6507
7100
  await runMcp(configuration);
6508
7101
  return;
6509
7102
  }
@@ -6514,4 +7107,4 @@ await main().catch((cause) => {
6514
7107
  process.exitCode = 1;
6515
7108
  });
6516
7109
 
6517
- //# debugId=B9C02E5590F539C264756E2164756E21
7110
+ //# debugId=9F11AB66C376A14E64756E2164756E21