@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/CHANGELOG.md +15 -0
- package/README.md +54 -3
- package/bin/cohall.js +880 -287
- package/bin/cohall.js.map +13 -11
- package/docs/grok-bot.md +196 -0
- package/docs/install.md +8 -6
- package/docs/integrations.md +12 -1
- package/docs/services.md +4 -0
- package/package.json +1 -1
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.
|
|
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: {
|
|
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
|
|
320
|
-
import { access as access5, chmod as chmod7, mkdir as
|
|
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
|
|
358
|
+
import { join as join9, resolve as resolve4 } from "node:path";
|
|
323
359
|
var RelayConfiguration, defaultDataDirectory = () => {
|
|
324
360
|
if (platform5() === "win32") {
|
|
325
|
-
return
|
|
361
|
+
return join9(process.env.LOCALAPPDATA ?? join9(homedir5(), "AppData", "Local"), "Cohall");
|
|
326
362
|
}
|
|
327
363
|
if (platform5() === "darwin") {
|
|
328
|
-
return
|
|
364
|
+
return join9(homedir5(), "Library", "Application Support", "Cohall", "relay");
|
|
329
365
|
}
|
|
330
|
-
return
|
|
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
|
|
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 =
|
|
354
|
-
host:
|
|
355
|
-
port:
|
|
356
|
-
token:
|
|
357
|
-
dataDirectory:
|
|
358
|
-
databasePath:
|
|
359
|
-
allowRemote:
|
|
360
|
-
historyTaskLimit:
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
|
416
|
+
return Schema14.decodeUnknownSync(RelayConfiguration)({
|
|
381
417
|
host,
|
|
382
418
|
port,
|
|
383
419
|
token,
|
|
384
420
|
dataDirectory,
|
|
385
|
-
databasePath:
|
|
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
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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 ?
|
|
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 =
|
|
745
|
-
const row = yield*
|
|
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*
|
|
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
|
|
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*
|
|
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*
|
|
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*
|
|
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 =
|
|
799
|
-
const rows = yield*
|
|
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*
|
|
913
|
+
return yield* Effect14.forEach(rows, deviceFromRow);
|
|
804
914
|
});
|
|
805
|
-
const usage =
|
|
806
|
-
const rows = yield*
|
|
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 =
|
|
821
|
-
const provider =
|
|
822
|
-
const deviceId =
|
|
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 =
|
|
853
|
-
const row = yield*
|
|
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*
|
|
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 =
|
|
866
|
-
const rows = yield*
|
|
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*
|
|
980
|
+
return yield* Effect14.forEach(rows, operationFromRow);
|
|
871
981
|
});
|
|
872
|
-
const createUpgradeOperations =
|
|
982
|
+
const createUpgradeOperations = Effect14.fn("RelayStore.createUpgradeOperations")(function* (input) {
|
|
873
983
|
const timestamp = now();
|
|
874
|
-
const operations = yield*
|
|
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 =
|
|
915
|
-
const rows = yield*
|
|
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*
|
|
1030
|
+
return yield* Effect14.forEach(rows, operationFromRow);
|
|
921
1031
|
});
|
|
922
|
-
const transitionOperation =
|
|
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*
|
|
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 =
|
|
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*
|
|
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 =
|
|
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*
|
|
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(
|
|
976
|
-
const finishOperation = (operationId, deviceId, result) => requireOperationTarget(operationId, deviceId).pipe(
|
|
977
|
-
const failOperation = (operationId, deviceId, error) => requireOperationTarget(operationId, deviceId).pipe(
|
|
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 =
|
|
980
|
-
yield*
|
|
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 =
|
|
987
|
-
const row = yield*
|
|
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 =
|
|
1015
|
-
yield*
|
|
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 =
|
|
1034
|
-
yield*
|
|
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 =
|
|
1161
|
+
const updateDeviceStatus = Effect14.fn("RelayStore.updateDeviceStatus")(function* (deviceId, status, bots) {
|
|
1052
1162
|
const timestamp = now();
|
|
1053
|
-
const result = yield*
|
|
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*
|
|
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*
|
|
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 =
|
|
1073
|
-
const
|
|
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*
|
|
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,
|
|
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 =
|
|
1140
|
-
const threadRow = yield*
|
|
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*
|
|
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*
|
|
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*
|
|
1283
|
+
const [thread, messages, tasks] = yield* Effect14.all([
|
|
1164
1284
|
threadFromRow(threadRow),
|
|
1165
|
-
|
|
1166
|
-
|
|
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 =
|
|
1178
|
-
const rows = yield*
|
|
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*
|
|
1303
|
+
return yield* Effect14.forEach(rows, (row) => taskFromRow(db, row));
|
|
1184
1304
|
});
|
|
1185
|
-
const transition =
|
|
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*
|
|
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 =
|
|
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*
|
|
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 =
|
|
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*
|
|
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 =
|
|
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*
|
|
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 =
|
|
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*
|
|
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 =
|
|
1446
|
+
const requeueTasksFor = Effect14.fn("RelayStore.requeueTasksFor")(function* (deviceId) {
|
|
1314
1447
|
const timestamp = now();
|
|
1315
|
-
yield*
|
|
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 =
|
|
1329
|
-
const row = yield*
|
|
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 =
|
|
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*
|
|
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*
|
|
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 =
|
|
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*
|
|
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 =
|
|
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*
|
|
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 =
|
|
1395
|
-
const row = yield*
|
|
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*
|
|
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 =
|
|
1412
|
-
const rows = yield*
|
|
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*
|
|
1549
|
+
return yield* Effect14.forEach(rows, authSessionFromRow);
|
|
1417
1550
|
});
|
|
1418
|
-
const revokeAuthSession =
|
|
1551
|
+
const revokeAuthSession = Effect14.fn("RelayStore.revokeAuthSession")(function* (sessionId) {
|
|
1419
1552
|
const revokedAt = now();
|
|
1420
|
-
const result = yield*
|
|
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*
|
|
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*
|
|
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) =>
|
|
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(
|
|
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) =>
|
|
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,
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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) =>
|
|
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(
|
|
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) =>
|
|
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(
|
|
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
|
|
1712
|
-
return
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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" ? () =>
|
|
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
|
|
1998
|
+
return Effect15.runPromise(store.rollbackAssignment(task.id));
|
|
1821
1999
|
};
|
|
1822
2000
|
const dispatchOperation = async (operation) => {
|
|
1823
|
-
const assigned = await run2(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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({
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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 =
|
|
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*
|
|
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,
|
|
2078
|
-
return json(yield*
|
|
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(
|
|
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(
|
|
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("
|
|
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
|
|
2242
|
-
status:
|
|
2243
|
-
message:
|
|
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
|
|
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) =>
|
|
2357
|
-
|
|
2358
|
-
|
|
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
|
|
2892
|
-
import { access as access4, mkdir as
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
5352
|
-
claude:
|
|
5353
|
-
opencode:
|
|
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
|
|
5361
|
-
await
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
6003
|
+
print(await Effect10.runPromise(relay2.abandonOperation(Schema11.decodeUnknownSync(OperationId)(arguments_.positionals[1]))));
|
|
5546
6004
|
return;
|
|
5547
6005
|
}
|
|
5548
|
-
print(await
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
6246
|
+
print(await Effect10.runPromise(relay2.authSessions()));
|
|
5786
6247
|
return;
|
|
5787
6248
|
}
|
|
5788
6249
|
if (command === "forget") {
|
|
5789
6250
|
allowOptions(arguments_, []);
|
|
5790
|
-
print(await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
5852
|
-
...parent === undefined ? {} : { parentTaskId:
|
|
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:
|
|
6317
|
+
...provider === undefined ? {} : { provider: Schema11.decodeUnknownSync(Provider)(provider) },
|
|
6318
|
+
...attachments2.length === 0 ? {} : { attachments: attachments2 }
|
|
5855
6319
|
}));
|
|
5856
|
-
|
|
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 =
|
|
5862
|
-
print(taskResult(await
|
|
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 =
|
|
6369
|
+
const id = Schema11.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
|
|
5868
6370
|
if (arguments_.options.has("follow")) {
|
|
5869
|
-
await
|
|
6371
|
+
await Effect10.runPromise(followTaskTrace(relay, id, printLine));
|
|
5870
6372
|
return;
|
|
5871
6373
|
}
|
|
5872
|
-
print(await
|
|
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 =
|
|
5878
|
-
const task = await
|
|
5879
|
-
print(taskResult(await
|
|
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 =
|
|
5885
|
-
print(taskResult(await
|
|
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 =
|
|
5891
|
-
print(await
|
|
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
|
|
5900
|
-
import { constants as
|
|
5901
|
-
import { open as
|
|
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
|
|
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
|
|
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:
|
|
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 =
|
|
5990
|
-
const handle = await
|
|
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
|
|
6038
|
-
state.terminal.set(taskId,
|
|
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(
|
|
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 =
|
|
6630
|
+
const workflow = Effect11.gen(function* () {
|
|
6104
6631
|
if (task.provider === "grok-bot") {
|
|
6105
|
-
return yield*
|
|
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
|
|
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
|
|
6650
|
+
provider,
|
|
6117
6651
|
message: cause instanceof Error ? cause.message : String(cause)
|
|
6118
6652
|
})
|
|
6119
6653
|
});
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
|
|
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
|
-
|
|
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,
|
|
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) =>
|
|
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
|
|
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(
|
|
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
|
|
6312
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
6461
|
-
return output(taskResult(await
|
|
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
|
|
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
|
|
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
|
|
6502
|
-
await
|
|
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
|
|
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=
|
|
7110
|
+
//# debugId=9F11AB66C376A14E64756E2164756E21
|