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