@akshar5/cohall 0.9.0 → 0.10.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 +39 -0
- package/README.md +65 -17
- package/bin/cohall.js +1735 -534
- package/bin/cohall.js.map +20 -20
- package/deploy/windows/install-device.ps1 +25 -2
- package/docs/grok-bot.md +7 -0
- package/docs/install.md +32 -5
- package/docs/integrations.md +160 -8
- package/docs/muse.md +162 -0
- package/docs/releasing.md +2 -2
- package/docs/services.md +68 -12
- package/package.json +1 -1
package/bin/cohall.js
CHANGED
|
@@ -17,9 +17,9 @@ 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.10.0", maxSocketPayloadBytes, maxAttachmentBytes, maxTaskAttachments = 2, maxProgressNoteBytes = 1024, bounded = (maxLength) => Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(maxLength)), optionalText = (maxLength) => Schema.String.check(Schema.isMaxLength(maxLength)), boundedArray = (schema, maxLength) => Schema.Array(schema).check(Schema.isMaxLength(maxLength)), count, uuid = (name) => Schema.String.check(Schema.isUUID(4)).pipe(Schema.brand(name)), isoTimestamp = (name) => Schema.String.check(Schema.isPattern(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/, {
|
|
21
21
|
expected: "an ISO-8601 UTC timestamp"
|
|
22
|
-
})).pipe(Schema.brand(name)), 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) => {
|
|
22
|
+
})).pipe(Schema.brand(name)), BotId, Bot, DeviceId, ThreadId, MessageId, TaskId, TaskRequestId, TaskRunId, ClarificationId, AuthSessionId, OperationId, Timestamp, TaskDeadline, taskDeadlineError = "Task deadline exceeded", taskDeadlinePassed = (task, time = Date.now()) => task.expiresAt !== undefined && Date.parse(task.expiresAt) <= time, Platform, Provider, DeviceStatus, TaskStatus, OperationStatus, TaskTraceEventKind, MessageRole, ConnectionRole, AuthSession, CreatePairingInput, PairingCredential, ExchangePairingInput, SessionCredential, PairingResult, Capability, Workspace, Device, maxDevicePageDevices = 16, maxDevicePageResponseBytes, DevicePage, minimumDeviceOperationVersion = "0.5.0", parsedVersion = (value) => {
|
|
23
23
|
const match = /^v?(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.exec(value);
|
|
24
24
|
if (match === null) {
|
|
25
25
|
return;
|
|
@@ -56,7 +56,7 @@ var version = "0.9.0", maxSocketPayloadBytes, maxAttachmentBytes, maxTaskAttachm
|
|
|
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
|
-
}, TaskProgressInput, TaskProgress, 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, decodeTaskProgressInput, decodeExchangePairingInput, decodeCreateTaskInput = (input) => Schema.decodeUnknownEffect(CreateTaskInput)(input).pipe(Effect.map((task) => ({
|
|
59
|
+
}, TaskProgressInput, TaskProgress, maxClarificationBytes = 4096, maxTaskClarifications = 10, clarificationText, RequestTaskInput, AnswerTaskInput, TaskClarification, clarifications, validClarificationState = (task) => task.status !== "needs_input" || task.clarifications?.at(-1) !== undefined && task.clarifications.at(-1)?.answer === undefined ? undefined : "A task awaiting input must have an unanswered question", 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, decodeTaskProgressInput, decodeRequestTaskInput, decodeAnswerTaskInput, 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;
|
|
@@ -74,9 +74,16 @@ var init_src = __esm(() => {
|
|
|
74
74
|
ThreadId = uuid("ThreadId");
|
|
75
75
|
MessageId = uuid("MessageId");
|
|
76
76
|
TaskId = uuid("TaskId");
|
|
77
|
+
TaskRequestId = uuid("TaskRequestId");
|
|
78
|
+
TaskRunId = uuid("TaskRunId");
|
|
79
|
+
ClarificationId = uuid("ClarificationId");
|
|
77
80
|
AuthSessionId = uuid("AuthSessionId");
|
|
78
81
|
OperationId = uuid("OperationId");
|
|
79
82
|
Timestamp = isoTimestamp("Timestamp");
|
|
83
|
+
TaskDeadline = Timestamp.check(Schema.makeFilter((value) => {
|
|
84
|
+
const time = Date.parse(value);
|
|
85
|
+
return Number.isFinite(time) && new Date(time).toISOString().slice(0, 19) === value.slice(0, 19) ? undefined : "Expected a valid ISO-8601 UTC deadline";
|
|
86
|
+
}));
|
|
80
87
|
Platform = Schema.Literals(["darwin", "linux", "windows", "unknown"]);
|
|
81
88
|
Provider = Schema.Literals(["codex", "claude-code", "opencode", "grok-bot"]);
|
|
82
89
|
DeviceStatus = Schema.Literals(["online", "busy", "offline"]);
|
|
@@ -84,6 +91,7 @@ var init_src = __esm(() => {
|
|
|
84
91
|
"queued",
|
|
85
92
|
"assigned",
|
|
86
93
|
"running",
|
|
94
|
+
"needs_input",
|
|
87
95
|
"cancelling",
|
|
88
96
|
"completed",
|
|
89
97
|
"failed",
|
|
@@ -96,7 +104,12 @@ var init_src = __esm(() => {
|
|
|
96
104
|
"completed",
|
|
97
105
|
"failed"
|
|
98
106
|
]);
|
|
99
|
-
TaskTraceEventKind = Schema.Literals([
|
|
107
|
+
TaskTraceEventKind = Schema.Literals([
|
|
108
|
+
...TaskStatus.literals,
|
|
109
|
+
"requeued",
|
|
110
|
+
"input_requested",
|
|
111
|
+
"input_answered"
|
|
112
|
+
]);
|
|
100
113
|
MessageRole = Schema.Literals(["human", "agent", "system"]);
|
|
101
114
|
ConnectionRole = Schema.Literals(["client", "device"]);
|
|
102
115
|
AuthSession = Schema.Struct({
|
|
@@ -155,6 +168,19 @@ var init_src = __esm(() => {
|
|
|
155
168
|
oldestQueuedAt: Schema.optionalKey(Timestamp)
|
|
156
169
|
}))
|
|
157
170
|
});
|
|
171
|
+
maxDevicePageResponseBytes = 3 * 1024 * 1024;
|
|
172
|
+
DevicePage = Schema.Struct({
|
|
173
|
+
devices: boundedArray(Device, maxDevicePageDevices),
|
|
174
|
+
nextCursor: Schema.optionalKey(DeviceId)
|
|
175
|
+
}).check(Schema.makeFilter((page) => {
|
|
176
|
+
let previous;
|
|
177
|
+
for (const device of page.devices) {
|
|
178
|
+
if (previous !== undefined && device.id <= previous)
|
|
179
|
+
return "Device pages must be ordered by ID";
|
|
180
|
+
previous = device.id;
|
|
181
|
+
}
|
|
182
|
+
return page.nextCursor === undefined || page.nextCursor === previous ? undefined : "The next cursor must be the last device ID";
|
|
183
|
+
}));
|
|
158
184
|
Thread = Schema.Struct({
|
|
159
185
|
id: ThreadId,
|
|
160
186
|
title: bounded(256),
|
|
@@ -188,8 +214,22 @@ var init_src = __esm(() => {
|
|
|
188
214
|
note: bounded(maxProgressNoteBytes).check(Schema.makeFilter((value) => value.trim().length > 0 && new TextEncoder().encode(value).byteLength <= maxProgressNoteBytes ? undefined : `Expected a nonblank progress note of at most ${maxProgressNoteBytes} UTF-8 bytes`)).pipe(Schema.brand("TaskProgressNote"))
|
|
189
215
|
});
|
|
190
216
|
TaskProgress = Schema.Struct({ ...TaskProgressInput.fields, at: Timestamp });
|
|
217
|
+
clarificationText = bounded(maxClarificationBytes).check(Schema.makeFilter((value) => value.trim().length > 0 && new TextEncoder().encode(value).byteLength <= maxClarificationBytes ? undefined : `Expected nonblank text of at most ${maxClarificationBytes} UTF-8 bytes`)).pipe(Schema.brand("ClarificationText"));
|
|
218
|
+
RequestTaskInput = Schema.Struct({ runId: TaskRunId, question: clarificationText });
|
|
219
|
+
AnswerTaskInput = Schema.Struct({
|
|
220
|
+
requestId: ClarificationId,
|
|
221
|
+
answer: clarificationText
|
|
222
|
+
});
|
|
223
|
+
TaskClarification = Schema.Struct({
|
|
224
|
+
id: ClarificationId,
|
|
225
|
+
question: RequestTaskInput.fields.question,
|
|
226
|
+
at: Timestamp,
|
|
227
|
+
answer: Schema.optionalKey(Schema.Struct({ text: AnswerTaskInput.fields.answer, at: Timestamp }))
|
|
228
|
+
});
|
|
229
|
+
clarifications = boundedArray(TaskClarification, maxTaskClarifications);
|
|
191
230
|
Task = Schema.Struct({
|
|
192
231
|
id: TaskId,
|
|
232
|
+
runId: Schema.optionalKey(TaskRunId),
|
|
193
233
|
threadId: ThreadId,
|
|
194
234
|
prompt: bounded(131072),
|
|
195
235
|
context: Schema.optionalKey(optionalText(131072)),
|
|
@@ -205,23 +245,26 @@ var init_src = __esm(() => {
|
|
|
205
245
|
inputAttachmentNames: Schema.optionalKey(boundedArray(AttachmentName, maxTaskAttachments)),
|
|
206
246
|
error: Schema.optionalKey(optionalText(16384)),
|
|
207
247
|
progress: Schema.optionalKey(TaskProgress),
|
|
248
|
+
clarifications: Schema.optionalKey(clarifications),
|
|
208
249
|
createdAt: Timestamp,
|
|
209
250
|
updatedAt: Timestamp,
|
|
210
251
|
startedAt: Schema.optionalKey(Timestamp),
|
|
211
|
-
completedAt: Schema.optionalKey(Timestamp)
|
|
212
|
-
|
|
252
|
+
completedAt: Schema.optionalKey(Timestamp),
|
|
253
|
+
expiresAt: Schema.optionalKey(TaskDeadline)
|
|
254
|
+
}).check(Schema.makeFilter(validBotTarget), Schema.makeFilter(validClarificationState));
|
|
213
255
|
TaskInboxItem = Schema.Struct({
|
|
214
256
|
id: TaskId,
|
|
215
257
|
threadId: ThreadId,
|
|
216
258
|
targetDeviceId: DeviceId,
|
|
217
259
|
provider: Provider,
|
|
218
260
|
botId: Schema.optionalKey(BotId),
|
|
219
|
-
status: Schema.Literals(["completed", "failed", "cancelled"]),
|
|
261
|
+
status: Schema.Literals(["needs_input", "completed", "failed", "cancelled"]),
|
|
220
262
|
promptPreview: optionalText(320),
|
|
221
263
|
resultPreview: Schema.optionalKey(optionalText(1024)),
|
|
222
264
|
errorPreview: Schema.optionalKey(optionalText(1024)),
|
|
223
|
-
completedAt: Timestamp
|
|
224
|
-
|
|
265
|
+
completedAt: Schema.optionalKey(Timestamp),
|
|
266
|
+
inputRequest: Schema.optionalKey(TaskClarification)
|
|
267
|
+
}).check(Schema.makeFilter((item) => item.status === "needs_input" ? item.inputRequest !== undefined && item.inputRequest.answer === undefined && item.completedAt === undefined ? undefined : "An input request must have an unanswered question and no completion time" : item.completedAt !== undefined && item.inputRequest === undefined ? undefined : "A completion must have a completion time and no input request"));
|
|
225
268
|
TaskInbox = Schema.Struct({
|
|
226
269
|
items: boundedArray(TaskInboxItem, 20),
|
|
227
270
|
hasMore: Schema.Boolean
|
|
@@ -250,6 +293,7 @@ var init_src = __esm(() => {
|
|
|
250
293
|
queued: count,
|
|
251
294
|
assigned: count,
|
|
252
295
|
running: count,
|
|
296
|
+
needs_input: Schema.optionalKey(count),
|
|
253
297
|
cancelling: count,
|
|
254
298
|
completed: count,
|
|
255
299
|
failed: count,
|
|
@@ -291,11 +335,13 @@ var init_src = __esm(() => {
|
|
|
291
335
|
updatedAt: Timestamp,
|
|
292
336
|
startedAt: Schema.optionalKey(Timestamp),
|
|
293
337
|
completedAt: Schema.optionalKey(Timestamp),
|
|
338
|
+
expiresAt: Schema.optionalKey(TaskDeadline),
|
|
294
339
|
events: boundedArray(TaskTraceEvent, 100),
|
|
295
340
|
truncated: Schema.Boolean,
|
|
296
341
|
error: Schema.optionalKey(optionalText(16384)),
|
|
297
|
-
progress: Schema.optionalKey(TaskProgress)
|
|
298
|
-
|
|
342
|
+
progress: Schema.optionalKey(TaskProgress),
|
|
343
|
+
clarifications: Schema.optionalKey(clarifications)
|
|
344
|
+
}).check(Schema.makeFilter(validBotTarget), Schema.makeFilter(validClarificationState));
|
|
299
345
|
ThreadContext = Schema.Struct({
|
|
300
346
|
thread: Thread,
|
|
301
347
|
messages: boundedArray(Message, 20),
|
|
@@ -303,6 +349,7 @@ var init_src = __esm(() => {
|
|
|
303
349
|
truncated: Schema.Boolean
|
|
304
350
|
});
|
|
305
351
|
CreateTaskInput = Schema.Struct({
|
|
352
|
+
requestId: Schema.optionalKey(TaskRequestId),
|
|
306
353
|
threadId: Schema.optionalKey(ThreadId),
|
|
307
354
|
title: Schema.optionalKey(bounded(256)),
|
|
308
355
|
prompt: bounded(131072),
|
|
@@ -311,6 +358,7 @@ var init_src = __esm(() => {
|
|
|
311
358
|
botId: Schema.optionalKey(BotId),
|
|
312
359
|
targetDeviceId: Schema.optionalKey(DeviceId),
|
|
313
360
|
parentTaskId: Schema.optionalKey(TaskId),
|
|
361
|
+
expiresAt: Schema.optionalKey(TaskDeadline),
|
|
314
362
|
workspace: Schema.optionalKey(bounded(4096)),
|
|
315
363
|
attachments: Schema.optionalKey(attachments)
|
|
316
364
|
}).check(Schema.makeFilter(validBotTarget));
|
|
@@ -319,7 +367,9 @@ var init_src = __esm(() => {
|
|
|
319
367
|
Connected: {
|
|
320
368
|
serverVersion: bounded(32),
|
|
321
369
|
connectedAt: Timestamp,
|
|
322
|
-
taskAttachments: Schema.optionalKey(Schema.Boolean)
|
|
370
|
+
taskAttachments: Schema.optionalKey(Schema.Boolean),
|
|
371
|
+
taskClarification: Schema.optionalKey(Schema.Boolean),
|
|
372
|
+
taskDeadlines: Schema.optionalKey(Schema.Boolean)
|
|
323
373
|
},
|
|
324
374
|
DeviceHello: { device: Device },
|
|
325
375
|
DeviceHeartbeat: {
|
|
@@ -328,17 +378,22 @@ var init_src = __esm(() => {
|
|
|
328
378
|
bots: Schema.optionalKey(boundedArray(Bot, 256))
|
|
329
379
|
},
|
|
330
380
|
TaskAssigned: { task: Task },
|
|
331
|
-
TaskAccepted: { taskId: TaskId },
|
|
381
|
+
TaskAccepted: { taskId: TaskId, runId: Schema.optionalKey(TaskRunId) },
|
|
332
382
|
TaskFinished: {
|
|
333
383
|
taskId: TaskId,
|
|
384
|
+
runId: Schema.optionalKey(TaskRunId),
|
|
334
385
|
result: optionalText(131072),
|
|
335
386
|
attachments: Schema.optionalKey(attachments),
|
|
336
387
|
providerSessionId: Schema.optionalKey(bounded(4096))
|
|
337
388
|
},
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
389
|
+
TaskInputRequested: {
|
|
390
|
+
taskId: TaskId,
|
|
391
|
+
...RequestTaskInput.fields
|
|
392
|
+
},
|
|
393
|
+
TaskFailed: { taskId: TaskId, runId: Schema.optionalKey(TaskRunId), error: bounded(16384) },
|
|
394
|
+
TaskCancelled: { taskId: TaskId, runId: Schema.optionalKey(TaskRunId) },
|
|
395
|
+
TaskSettled: { taskId: TaskId, runId: Schema.optionalKey(TaskRunId) },
|
|
396
|
+
CancelTask: { taskId: TaskId, runId: Schema.optionalKey(TaskRunId) },
|
|
342
397
|
OperationAssigned: { operation: DeviceOperation },
|
|
343
398
|
OperationAccepted: { operationId: OperationId },
|
|
344
399
|
OperationFinished: { operationId: OperationId, result: optionalText(16384) },
|
|
@@ -356,6 +411,8 @@ var init_src = __esm(() => {
|
|
|
356
411
|
]);
|
|
357
412
|
decodeCreatePairingInput = Schema.decodeUnknownEffect(CreatePairingInput);
|
|
358
413
|
decodeTaskProgressInput = Schema.decodeUnknownEffect(TaskProgressInput);
|
|
414
|
+
decodeRequestTaskInput = Schema.decodeUnknownEffect(RequestTaskInput);
|
|
415
|
+
decodeAnswerTaskInput = Schema.decodeUnknownEffect(AnswerTaskInput);
|
|
359
416
|
decodeExchangePairingInput = Schema.decodeUnknownEffect(ExchangePairingInput);
|
|
360
417
|
decodeCreateUpgradeOperationsInput = Schema.decodeUnknownEffect(CreateUpgradeOperationsInput);
|
|
361
418
|
decodeDevice = Schema.decodeUnknownEffect(Device);
|
|
@@ -366,7 +423,7 @@ var init_src = __esm(() => {
|
|
|
366
423
|
import { Effect as Effect13, Schema as Schema14 } from "effect";
|
|
367
424
|
import { access as access5, chmod as chmod7, mkdir as mkdir8, readFile as readFile5, writeFile as writeFile9 } from "node:fs/promises";
|
|
368
425
|
import { homedir as homedir5, platform as platform5 } from "node:os";
|
|
369
|
-
import { join as join9, resolve as
|
|
426
|
+
import { join as join9, resolve as resolve5 } from "node:path";
|
|
370
427
|
var RelayConfiguration, defaultDataDirectory = () => {
|
|
371
428
|
if (platform5() === "win32") {
|
|
372
429
|
return join9(process.env.LOCALAPPDATA ?? join9(homedir5(), "AppData", "Local"), "Cohall");
|
|
@@ -414,7 +471,7 @@ var init_config = __esm(() => {
|
|
|
414
471
|
if (!isLoopback2(host) && !allowRemote) {
|
|
415
472
|
throw new Error("Remote relay binding is disabled. Use a private network or TLS reverse proxy, then set COHALL_RELAY_ALLOW_REMOTE=true.");
|
|
416
473
|
}
|
|
417
|
-
const dataDirectory =
|
|
474
|
+
const dataDirectory = resolve5(process.env.COHALL_DATA_DIR ?? defaultDataDirectory());
|
|
418
475
|
await mkdir8(dataDirectory, { recursive: true, mode: 448 });
|
|
419
476
|
if (platform5() !== "win32") {
|
|
420
477
|
await chmod7(dataDirectory, 448);
|
|
@@ -607,14 +664,17 @@ var exports_store = {};
|
|
|
607
664
|
__export(exports_store, {
|
|
608
665
|
layerFromDatabase: () => layerFromDatabase,
|
|
609
666
|
layer: () => layer2,
|
|
667
|
+
TaskRequestError: () => TaskRequestError,
|
|
610
668
|
TaskProgressError: () => TaskProgressError,
|
|
669
|
+
TaskInputError: () => TaskInputError,
|
|
670
|
+
TaskDeadlineError: () => TaskDeadlineError,
|
|
611
671
|
Service: () => Service2,
|
|
612
672
|
RelayStore: () => exports_store,
|
|
613
673
|
PersistenceError: () => PersistenceError
|
|
614
674
|
});
|
|
615
675
|
import { Context as Context2, Effect as Effect14, Layer as Layer2, Schema as Schema15 } from "effect";
|
|
616
676
|
import { createHash as createHash2, randomBytes } from "node:crypto";
|
|
617
|
-
var PersistenceError, TaskProgressError, Service2, operationError = (operation) => (cause) => new PersistenceError({
|
|
677
|
+
var PersistenceError, TaskProgressError, TaskInputError, TaskDeadlineError, TaskRequestError, Service2, operationError = (operation) => (cause) => new PersistenceError({
|
|
618
678
|
operation,
|
|
619
679
|
message: cause instanceof Error ? cause.message : String(cause)
|
|
620
680
|
}), decode2 = (operation, schema, value) => Schema15.decodeUnknownEffect(schema)(value).pipe(Effect14.mapError(operationError(operation))), threadFromRow = (row) => decode2("RelayStore.decodeThread", Thread, {
|
|
@@ -657,6 +717,8 @@ var PersistenceError, TaskProgressError, Service2, operationError = (operation)
|
|
|
657
717
|
catch: operationError("RelayStore.taskAttachments")
|
|
658
718
|
}).pipe(Effect14.flatMap((attachments2) => decode2("RelayStore.decodeTask", Task, {
|
|
659
719
|
id: row.id,
|
|
720
|
+
...row.run_id === null ? {} : { runId: row.run_id },
|
|
721
|
+
...row.clarifications_json === null ? {} : { clarifications: JSON.parse(row.clarifications_json) },
|
|
660
722
|
threadId: row.thread_id,
|
|
661
723
|
prompt: row.prompt,
|
|
662
724
|
provider: row.provider,
|
|
@@ -665,6 +727,7 @@ var PersistenceError, TaskProgressError, Service2, operationError = (operation)
|
|
|
665
727
|
targetDeviceId: row.target_device_id,
|
|
666
728
|
createdAt: row.created_at,
|
|
667
729
|
updatedAt: row.updated_at,
|
|
730
|
+
...row.expires_at === null ? {} : { expiresAt: row.expires_at },
|
|
668
731
|
...row.context === null ? {} : { context: row.context },
|
|
669
732
|
...row.source_device_id === null ? {} : { sourceDeviceId: row.source_device_id },
|
|
670
733
|
...row.parent_task_id === null ? {} : { parentTaskId: row.parent_task_id },
|
|
@@ -684,7 +747,8 @@ var PersistenceError, TaskProgressError, Service2, operationError = (operation)
|
|
|
684
747
|
...row.bot_id === null ? {} : { botId: row.bot_id },
|
|
685
748
|
status: row.status,
|
|
686
749
|
promptPreview: row.prompt_preview,
|
|
687
|
-
completedAt: row.completed_at,
|
|
750
|
+
...row.completed_at === null ? {} : { completedAt: row.completed_at },
|
|
751
|
+
...row.input_request_json === null ? {} : { inputRequest: JSON.parse(row.input_request_json) },
|
|
688
752
|
...row.result_preview === null ? {} : { resultPreview: row.result_preview },
|
|
689
753
|
...row.error_preview === null ? {} : { errorPreview: row.error_preview }
|
|
690
754
|
}), operationFromRow = (row) => decode2("RelayStore.decodeOperation", DeviceOperation, {
|
|
@@ -703,6 +767,7 @@ var PersistenceError, TaskProgressError, Service2, operationError = (operation)
|
|
|
703
767
|
queued: 0,
|
|
704
768
|
assigned: 0,
|
|
705
769
|
running: 0,
|
|
770
|
+
needs_input: 0,
|
|
706
771
|
cancelling: 0,
|
|
707
772
|
completed: 0,
|
|
708
773
|
failed: 0,
|
|
@@ -774,7 +839,52 @@ var PersistenceError, TaskProgressError, Service2, operationError = (operation)
|
|
|
774
839
|
events.push(TaskTraceEvent.make({ kind: task.status, at: task.completedAt, detail }));
|
|
775
840
|
}
|
|
776
841
|
return events;
|
|
777
|
-
},
|
|
842
|
+
}, interruptedTaskStatus = `CASE
|
|
843
|
+
WHEN json_array_length(clarifications_json) > 0
|
|
844
|
+
AND json_extract(clarifications_json, '$[#-1].answer') IS NULL THEN 'needs_input'
|
|
845
|
+
ELSE 'queued' END`, makeService = (db, retainedTerminalTasks = 1000) => {
|
|
846
|
+
const requestHash = (input) => {
|
|
847
|
+
const { requestId: _requestId, attachments: attachments2, ...fields } = input;
|
|
848
|
+
const normalized = {
|
|
849
|
+
...fields,
|
|
850
|
+
provider: input.provider ?? (input.botId === undefined ? "codex" : "grok-bot"),
|
|
851
|
+
...attachments2 === undefined || attachments2.length === 0 ? {} : {
|
|
852
|
+
attachments: attachments2.map(({ name, data }) => [
|
|
853
|
+
name,
|
|
854
|
+
Buffer.from(data, "base64").toString("base64")
|
|
855
|
+
])
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
return createHash2("sha256").update(JSON.stringify(Object.entries(normalized).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0))).digest("hex");
|
|
859
|
+
};
|
|
860
|
+
const requestedTask = (input, principal) => {
|
|
861
|
+
if (input.requestId === undefined)
|
|
862
|
+
return null;
|
|
863
|
+
const saved = db.query("SELECT input_hash, task_id FROM task_requests WHERE requester_id = ? AND request_id = ?").get(principal === "owner" ? "owner" : principal.id, input.requestId.toLowerCase());
|
|
864
|
+
if (saved === null)
|
|
865
|
+
return null;
|
|
866
|
+
if (saved.input_hash !== requestHash(input))
|
|
867
|
+
throw new TaskRequestError({
|
|
868
|
+
status: 409,
|
|
869
|
+
message: "Request ID was already used with different task input"
|
|
870
|
+
});
|
|
871
|
+
if (saved.task_id === null)
|
|
872
|
+
throw new TaskRequestError({
|
|
873
|
+
status: 410,
|
|
874
|
+
message: "The original task is no longer retained; this request ID cannot be reused"
|
|
875
|
+
});
|
|
876
|
+
const task = db.query("SELECT * FROM tasks WHERE id = ?").get(saved.task_id);
|
|
877
|
+
if (task === null)
|
|
878
|
+
throw new Error(`Missing task ${saved.task_id} for retained request`);
|
|
879
|
+
return task;
|
|
880
|
+
};
|
|
881
|
+
const findDelegation = Effect14.fn("RelayStore.findDelegation")(function* (input, principal) {
|
|
882
|
+
const row = yield* Effect14.try({
|
|
883
|
+
try: () => requestedTask(input, principal),
|
|
884
|
+
catch: (cause) => cause instanceof TaskRequestError ? cause : operationError("RelayStore.findDelegation")(cause)
|
|
885
|
+
});
|
|
886
|
+
return row === null ? undefined : yield* taskFromRow(db, row);
|
|
887
|
+
});
|
|
778
888
|
const recordTaskTraceEvent = (taskId, kind, detail, createdAt = now()) => {
|
|
779
889
|
db.query(`INSERT INTO task_trace_events (task_id, kind, created_at, detail)
|
|
780
890
|
VALUES (?, ?, ?, ?)`).run(taskId, kind, createdAt, detail);
|
|
@@ -814,7 +924,8 @@ var PersistenceError, TaskProgressError, Service2, operationError = (operation)
|
|
|
814
924
|
substr(prompt, 1, 160) AS prompt_preview,
|
|
815
925
|
substr(result, 1, 512) AS result_preview,
|
|
816
926
|
substr(error, 1, 512) AS error_preview,
|
|
817
|
-
COALESCE(completed_at, updated_at) AS completed_at
|
|
927
|
+
CASE WHEN status = 'needs_input' THEN NULL ELSE COALESCE(completed_at, updated_at) END AS completed_at,
|
|
928
|
+
CASE WHEN status = 'needs_input' THEN json_extract(clarifications_json, '$[#-1]') END AS input_request_json`;
|
|
818
929
|
const queryOperation = (operationId) => db.query("SELECT * FROM device_operations WHERE id = ?").get(operationId);
|
|
819
930
|
const getTask = Effect14.fn("RelayStore.getTask")(function* (taskId) {
|
|
820
931
|
const row = yield* Effect14.try({
|
|
@@ -824,7 +935,8 @@ var PersistenceError, TaskProgressError, Service2, operationError = (operation)
|
|
|
824
935
|
if (row === null) {
|
|
825
936
|
return yield* Effect14.fail(new PersistenceError({
|
|
826
937
|
operation: "RelayStore.getTask",
|
|
827
|
-
message: `Unknown task ${taskId}
|
|
938
|
+
message: `Unknown task ${taskId}`,
|
|
939
|
+
missingRecord: "task"
|
|
828
940
|
}));
|
|
829
941
|
}
|
|
830
942
|
return yield* taskFromRow(db, row);
|
|
@@ -855,8 +967,8 @@ var PersistenceError, TaskProgressError, Service2, operationError = (operation)
|
|
|
855
967
|
const requesterId = principal === "owner" ? "owner" : principal.id;
|
|
856
968
|
const rows = yield* Effect14.try({
|
|
857
969
|
try: () => db.query(`SELECT ${inboxColumns} FROM tasks WHERE requester_id = ? AND completion_seen_at IS NULL
|
|
858
|
-
AND status IN ('completed', 'failed', 'cancelled')
|
|
859
|
-
ORDER BY completed_at ASC, id ASC LIMIT 21`).all(requesterId),
|
|
970
|
+
AND status IN ('needs_input', 'completed', 'failed', 'cancelled')
|
|
971
|
+
ORDER BY COALESCE(completed_at, updated_at) ASC, id ASC LIMIT 21`).all(requesterId),
|
|
860
972
|
catch: operationError("RelayStore.inboxFor")
|
|
861
973
|
});
|
|
862
974
|
return TaskInbox.make({
|
|
@@ -921,8 +1033,10 @@ var PersistenceError, TaskProgressError, Service2, operationError = (operation)
|
|
|
921
1033
|
...task.workspace === undefined ? {} : { workspace: task.workspace },
|
|
922
1034
|
...task.startedAt === undefined ? {} : { startedAt: task.startedAt },
|
|
923
1035
|
...task.completedAt === undefined ? {} : { completedAt: task.completedAt },
|
|
1036
|
+
...task.expiresAt === undefined ? {} : { expiresAt: task.expiresAt },
|
|
924
1037
|
...task.error === undefined ? {} : { error: task.error },
|
|
925
|
-
...task.progress === undefined ? {} : { progress: task.progress }
|
|
1038
|
+
...task.progress === undefined ? {} : { progress: task.progress },
|
|
1039
|
+
...task.clarifications === undefined ? {} : { clarifications: task.clarifications }
|
|
926
1040
|
});
|
|
927
1041
|
});
|
|
928
1042
|
const listDevices = Effect14.fn("RelayStore.listDevices")(function* () {
|
|
@@ -937,6 +1051,54 @@ var PersistenceError, TaskProgressError, Service2, operationError = (operation)
|
|
|
937
1051
|
});
|
|
938
1052
|
return yield* Effect14.forEach(rows, deviceFromRow);
|
|
939
1053
|
});
|
|
1054
|
+
const listDevicePage = Effect14.fn("RelayStore.listDevicePage")(function* (after) {
|
|
1055
|
+
const nextRow = yield* Effect14.try({
|
|
1056
|
+
try: () => db.query(`SELECT devices.*,
|
|
1057
|
+
(SELECT COUNT(*) FROM tasks
|
|
1058
|
+
WHERE target_device_id = devices.id AND status = 'queued') AS queued_tasks,
|
|
1059
|
+
(SELECT MIN(created_at) FROM tasks
|
|
1060
|
+
WHERE target_device_id = devices.id AND status = 'queued') AS oldest_queued_at
|
|
1061
|
+
FROM devices WHERE forgotten_at IS NULL AND devices.id > ?
|
|
1062
|
+
ORDER BY devices.id LIMIT 1`),
|
|
1063
|
+
catch: operationError("RelayStore.listDevicePage.query")
|
|
1064
|
+
});
|
|
1065
|
+
const devices = [];
|
|
1066
|
+
const targetBytes = 2 * 1024 * 1024 - 1024;
|
|
1067
|
+
let bytes = 0;
|
|
1068
|
+
let cursor = after ?? "";
|
|
1069
|
+
while (devices.length < maxDevicePageDevices) {
|
|
1070
|
+
const row = yield* Effect14.try({
|
|
1071
|
+
try: () => nextRow.get(cursor),
|
|
1072
|
+
catch: operationError("RelayStore.listDevicePage.read")
|
|
1073
|
+
});
|
|
1074
|
+
if (row === null)
|
|
1075
|
+
break;
|
|
1076
|
+
const device = yield* deviceFromRow(row);
|
|
1077
|
+
const size = Buffer.byteLength(JSON.stringify(device)) + (devices.length === 0 ? 0 : 1);
|
|
1078
|
+
if (devices.length > 0 && bytes + size > targetBytes)
|
|
1079
|
+
break;
|
|
1080
|
+
devices.push(device);
|
|
1081
|
+
bytes += size;
|
|
1082
|
+
cursor = device.id;
|
|
1083
|
+
if (bytes > targetBytes)
|
|
1084
|
+
break;
|
|
1085
|
+
}
|
|
1086
|
+
const last = devices.at(-1);
|
|
1087
|
+
const hasMore = last !== undefined && (yield* Effect14.try({
|
|
1088
|
+
try: () => db.query("SELECT 1 AS found FROM devices WHERE forgotten_at IS NULL AND id > ? ORDER BY id LIMIT 1").get(last.id) !== null,
|
|
1089
|
+
catch: operationError("RelayStore.listDevicePage.hasMore")
|
|
1090
|
+
}));
|
|
1091
|
+
const page = DevicePage.make({
|
|
1092
|
+
devices,
|
|
1093
|
+
...hasMore && last !== undefined ? { nextCursor: last.id } : {}
|
|
1094
|
+
});
|
|
1095
|
+
if (Buffer.byteLength(JSON.stringify(page)) > maxDevicePageResponseBytes)
|
|
1096
|
+
return yield* new PersistenceError({
|
|
1097
|
+
operation: "RelayStore.listDevicePage",
|
|
1098
|
+
message: "A device page exceeds the transfer limit"
|
|
1099
|
+
});
|
|
1100
|
+
return page;
|
|
1101
|
+
});
|
|
940
1102
|
const usage = Effect14.fn("RelayStore.usage")(function* () {
|
|
941
1103
|
const rows = yield* Effect14.try({
|
|
942
1104
|
try: () => db.query(`SELECT tasks.target_device_id AS device_id, devices.name AS device_name,
|
|
@@ -992,7 +1154,8 @@ var PersistenceError, TaskProgressError, Service2, operationError = (operation)
|
|
|
992
1154
|
if (row === null) {
|
|
993
1155
|
return yield* Effect14.fail(new PersistenceError({
|
|
994
1156
|
operation: "RelayStore.getOperation",
|
|
995
|
-
message: `Unknown device operation ${operationId}
|
|
1157
|
+
message: `Unknown device operation ${operationId}`,
|
|
1158
|
+
missingRecord: "operation"
|
|
996
1159
|
}));
|
|
997
1160
|
}
|
|
998
1161
|
return yield* operationFromRow(row);
|
|
@@ -1150,13 +1313,15 @@ var PersistenceError, TaskProgressError, Service2, operationError = (operation)
|
|
|
1150
1313
|
yield* Effect14.try({
|
|
1151
1314
|
try: () => db.transaction(() => {
|
|
1152
1315
|
const timestamp = now();
|
|
1153
|
-
const interrupted = db.query(
|
|
1316
|
+
const interrupted = db.query(`SELECT id, ${interruptedTaskStatus} AS status FROM tasks WHERE status IN ('assigned', 'running')`).all();
|
|
1154
1317
|
db.query("UPDATE devices SET status = 'offline'").run();
|
|
1155
|
-
db.query(`UPDATE tasks SET status =
|
|
1318
|
+
db.query(`UPDATE tasks SET status = ${interruptedTaskStatus}, updated_at = ?, progress_note = NULL, progress_at = NULL,
|
|
1319
|
+
dispatched_at = CASE WHEN provider = 'grok-bot' THEN dispatched_at
|
|
1320
|
+
ELSE COALESCE(dispatched_at, started_at, updated_at) END
|
|
1156
1321
|
WHERE status IN ('assigned', 'running')`).run(timestamp);
|
|
1157
1322
|
db.query("UPDATE device_operations SET status = 'queued', updated_at = ? WHERE status IN ('assigned', 'running')").run(timestamp);
|
|
1158
1323
|
for (const task of interrupted) {
|
|
1159
|
-
recordTaskTraceEvent(TaskId.make(task.id), "requeued", "Relay restarted before the task completed", timestamp);
|
|
1324
|
+
recordTaskTraceEvent(TaskId.make(task.id), task.status === "needs_input" ? "needs_input" : "requeued", "Relay restarted before the task completed", timestamp);
|
|
1160
1325
|
}
|
|
1161
1326
|
db.query("DELETE FROM auth_pairings WHERE expires_at <= ? OR used_at IS NOT NULL").run(timestamp);
|
|
1162
1327
|
db.query("UPDATE auth_sessions SET revoked_at = ? WHERE revoked_at IS NULL AND roles_json LIKE '%,%'").run(timestamp);
|
|
@@ -1205,52 +1370,68 @@ var PersistenceError, TaskProgressError, Service2, operationError = (operation)
|
|
|
1205
1370
|
}
|
|
1206
1371
|
return yield* deviceFromRow(row);
|
|
1207
1372
|
});
|
|
1208
|
-
const createDelegation2 = Effect14.fn("RelayStore.createDelegation")(function* (input, targetDeviceId, principal, providerSessionId) {
|
|
1373
|
+
const createDelegation2 = Effect14.fn("RelayStore.createDelegation")(function* (input, targetDeviceId, principal, providerSessionId, requestedInput = input) {
|
|
1209
1374
|
const requesterId = principal === "owner" ? "owner" : principal.id;
|
|
1210
1375
|
const boundDeviceId = principal === "owner" ? undefined : principal.deviceId;
|
|
1211
1376
|
const sourceDeviceId = yield* Effect14.try({
|
|
1212
1377
|
try: () => boundDeviceId === undefined || db.query("SELECT id FROM devices WHERE id = ? AND forgotten_at IS NULL").get(boundDeviceId) === null ? undefined : boundDeviceId,
|
|
1213
1378
|
catch: operationError("RelayStore.createDelegation.source")
|
|
1214
1379
|
});
|
|
1215
|
-
const
|
|
1216
|
-
const threadId = input.threadId ?? makeThreadId();
|
|
1217
|
-
const task = Task.make({
|
|
1218
|
-
id: makeTaskId(),
|
|
1219
|
-
threadId,
|
|
1220
|
-
prompt: input.prompt,
|
|
1221
|
-
provider: input.provider ?? (input.botId === undefined ? "codex" : "grok-bot"),
|
|
1222
|
-
...input.botId === undefined ? {} : { botId: input.botId },
|
|
1223
|
-
status: "queued",
|
|
1224
|
-
targetDeviceId,
|
|
1225
|
-
createdAt: timestamp,
|
|
1226
|
-
updatedAt: timestamp,
|
|
1227
|
-
...input.context === undefined ? {} : { context: input.context },
|
|
1228
|
-
...sourceDeviceId === undefined ? {} : { sourceDeviceId },
|
|
1229
|
-
...input.parentTaskId === undefined ? {} : { parentTaskId: input.parentTaskId },
|
|
1230
|
-
...input.workspace === undefined ? {} : { workspace: input.workspace },
|
|
1231
|
-
...input.attachments === undefined || input.attachments.length === 0 ? {} : { inputAttachmentNames: input.attachments.map((attachment) => attachment.name) },
|
|
1232
|
-
...providerSessionId === undefined ? {} : { providerSessionId }
|
|
1233
|
-
});
|
|
1234
|
-
const thread = input.threadId === undefined ? Thread.make({
|
|
1235
|
-
id: threadId,
|
|
1236
|
-
title: input.title ?? input.prompt.slice(0, 72),
|
|
1237
|
-
createdAt: timestamp,
|
|
1238
|
-
updatedAt: timestamp
|
|
1239
|
-
}) : undefined;
|
|
1240
|
-
const message = Message.make({
|
|
1241
|
-
id: makeMessageId(),
|
|
1242
|
-
threadId,
|
|
1243
|
-
role: "human",
|
|
1244
|
-
authorName: sourceDeviceId === undefined ? "User" : "Remote agent",
|
|
1245
|
-
content: input.prompt,
|
|
1246
|
-
createdAt: timestamp,
|
|
1247
|
-
taskId: task.id,
|
|
1248
|
-
...sourceDeviceId === undefined ? {} : { deviceId: sourceDeviceId }
|
|
1249
|
-
});
|
|
1250
|
-
yield* Effect14.try({
|
|
1380
|
+
const submission = yield* Effect14.try({
|
|
1251
1381
|
try: () => db.transaction(() => {
|
|
1382
|
+
const existing = requestedTask(requestedInput, principal);
|
|
1383
|
+
if (existing !== null)
|
|
1384
|
+
return { existing };
|
|
1385
|
+
if (input.expiresAt !== undefined) {
|
|
1386
|
+
if (input.provider === "grok-bot" || input.botId !== undefined)
|
|
1387
|
+
throw new TaskDeadlineError({
|
|
1388
|
+
status: 400,
|
|
1389
|
+
message: "Task deadlines require a coding provider"
|
|
1390
|
+
});
|
|
1391
|
+
if (taskDeadlinePassed(input))
|
|
1392
|
+
throw new TaskDeadlineError({
|
|
1393
|
+
status: 400,
|
|
1394
|
+
message: "Task deadline must be in the future"
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
const timestamp = now();
|
|
1398
|
+
const threadId = input.threadId ?? makeThreadId();
|
|
1399
|
+
const task = Task.make({
|
|
1400
|
+
id: makeTaskId(),
|
|
1401
|
+
threadId,
|
|
1402
|
+
prompt: input.prompt,
|
|
1403
|
+
provider: input.provider ?? (input.botId === undefined ? "codex" : "grok-bot"),
|
|
1404
|
+
...input.botId === undefined ? {} : { botId: input.botId },
|
|
1405
|
+
status: "queued",
|
|
1406
|
+
targetDeviceId,
|
|
1407
|
+
createdAt: timestamp,
|
|
1408
|
+
updatedAt: timestamp,
|
|
1409
|
+
...input.expiresAt === undefined ? {} : { expiresAt: Timestamp.make(new Date(input.expiresAt).toISOString()) },
|
|
1410
|
+
...input.context === undefined ? {} : { context: input.context },
|
|
1411
|
+
...sourceDeviceId === undefined ? {} : { sourceDeviceId },
|
|
1412
|
+
...input.parentTaskId === undefined ? {} : { parentTaskId: input.parentTaskId },
|
|
1413
|
+
...input.workspace === undefined ? {} : { workspace: input.workspace },
|
|
1414
|
+
...input.attachments === undefined || input.attachments.length === 0 ? {} : { inputAttachmentNames: input.attachments.map((attachment) => attachment.name) },
|
|
1415
|
+
...providerSessionId === undefined ? {} : { providerSessionId }
|
|
1416
|
+
});
|
|
1417
|
+
const thread = input.threadId === undefined ? Thread.make({
|
|
1418
|
+
id: threadId,
|
|
1419
|
+
title: input.title ?? input.prompt.slice(0, 72),
|
|
1420
|
+
createdAt: timestamp,
|
|
1421
|
+
updatedAt: timestamp
|
|
1422
|
+
}) : undefined;
|
|
1423
|
+
const message = Message.make({
|
|
1424
|
+
id: makeMessageId(),
|
|
1425
|
+
threadId,
|
|
1426
|
+
role: "human",
|
|
1427
|
+
authorName: sourceDeviceId === undefined ? "User" : "Remote agent",
|
|
1428
|
+
content: input.prompt,
|
|
1429
|
+
createdAt: timestamp,
|
|
1430
|
+
taskId: task.id,
|
|
1431
|
+
...sourceDeviceId === undefined ? {} : { deviceId: sourceDeviceId }
|
|
1432
|
+
});
|
|
1252
1433
|
const outstanding = db.query(`SELECT COUNT(*) AS count FROM tasks WHERE target_device_id = ?
|
|
1253
|
-
AND status IN ('queued', 'assigned', 'running', 'cancelling')`).get(targetDeviceId);
|
|
1434
|
+
AND status IN ('queued', 'assigned', 'running', 'needs_input', 'cancelling')`).get(targetDeviceId);
|
|
1254
1435
|
if ((outstanding?.count ?? 0) >= 100) {
|
|
1255
1436
|
throw new Error(`Device ${targetDeviceId} has reached its outstanding task limit`);
|
|
1256
1437
|
}
|
|
@@ -1262,8 +1443,8 @@ var PersistenceError, TaskProgressError, Service2, operationError = (operation)
|
|
|
1262
1443
|
db.query(`INSERT INTO tasks (
|
|
1263
1444
|
id, thread_id, prompt, context, provider, status, source_device_id,
|
|
1264
1445
|
requester_id, target_device_id, parent_task_id, workspace,
|
|
1265
|
-
provider_session_id, bot_id, created_at, updated_at
|
|
1266
|
-
) 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);
|
|
1446
|
+
provider_session_id, bot_id, created_at, updated_at, expires_at
|
|
1447
|
+
) 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, task.expiresAt ?? null);
|
|
1267
1448
|
for (const attachment of input.attachments ?? []) {
|
|
1268
1449
|
const data = Buffer.from(attachment.data, "base64");
|
|
1269
1450
|
if (data.length < 1 || data.length > maxAttachmentBytes) {
|
|
@@ -1271,16 +1452,20 @@ var PersistenceError, TaskProgressError, Service2, operationError = (operation)
|
|
|
1271
1452
|
}
|
|
1272
1453
|
db.query("INSERT INTO task_attachments (task_id, name, direction, data) VALUES (?, ?, 'input', ?)").run(task.id, attachment.name, data);
|
|
1273
1454
|
}
|
|
1455
|
+
if (requestedInput.requestId !== undefined) {
|
|
1456
|
+
db.query("INSERT INTO task_requests (requester_id, request_id, input_hash, task_id) VALUES (?, ?, ?, ?)").run(requesterId, requestedInput.requestId.toLowerCase(), requestHash(requestedInput), task.id);
|
|
1457
|
+
}
|
|
1274
1458
|
recordTaskTraceEvent(task.id, "queued", "Relay accepted the task", timestamp);
|
|
1275
1459
|
db.query(`INSERT INTO messages (
|
|
1276
1460
|
id, thread_id, role, kind, author_id, author_name, content, created_at,
|
|
1277
1461
|
task_id, device_id
|
|
1278
1462
|
) VALUES (?, ?, ?, 'chat', ?, ?, ?, ?, ?, ?)`).run(message.id, message.threadId, message.role, sourceDeviceId ?? "user", message.authorName, message.content, message.createdAt, message.taskId ?? null, message.deviceId ?? null);
|
|
1279
1463
|
db.query("UPDATE threads SET updated_at = ? WHERE id = ?").run(timestamp, threadId);
|
|
1464
|
+
return { created: task };
|
|
1280
1465
|
})(),
|
|
1281
|
-
catch: operationError("RelayStore.createDelegation")
|
|
1466
|
+
catch: (cause) => cause instanceof TaskRequestError || cause instanceof TaskDeadlineError ? cause : operationError("RelayStore.createDelegation")(cause)
|
|
1282
1467
|
});
|
|
1283
|
-
return
|
|
1468
|
+
return "existing" in submission ? yield* taskFromRow(db, submission.existing) : submission.created;
|
|
1284
1469
|
});
|
|
1285
1470
|
const threadContext2 = Effect14.fn("RelayStore.threadContext")(function* (threadId) {
|
|
1286
1471
|
const threadRow = yield* Effect14.try({
|
|
@@ -1351,15 +1536,45 @@ var PersistenceError, TaskProgressError, Service2, operationError = (operation)
|
|
|
1351
1536
|
});
|
|
1352
1537
|
return yield* getTask(taskId);
|
|
1353
1538
|
});
|
|
1539
|
+
const hasDispatchEvidence = Effect14.fn("RelayStore.hasDispatchEvidence")((taskId) => Effect14.try({
|
|
1540
|
+
try: () => db.query("SELECT 1 FROM tasks WHERE id = ? AND dispatched_at IS NOT NULL").get(taskId) !== null,
|
|
1541
|
+
catch: operationError("RelayStore.hasDispatchEvidence")
|
|
1542
|
+
}));
|
|
1543
|
+
const expireTask = Effect14.fn("RelayStore.expireTask")(function* (task) {
|
|
1544
|
+
if (!taskDeadlinePassed(task) || ["cancelling", "completed", "failed", "cancelled"].includes(task.status))
|
|
1545
|
+
return task;
|
|
1546
|
+
const neverDispatched = task.status === "queued" && task.runId === undefined && task.startedAt === undefined && !(yield* hasDispatchEvidence(task.id));
|
|
1547
|
+
return yield* transition(task.id, [task.status], {
|
|
1548
|
+
status: neverDispatched ? "failed" : "cancelling",
|
|
1549
|
+
error: taskDeadlineError,
|
|
1550
|
+
...neverDispatched ? { completedAt: now() } : {}
|
|
1551
|
+
}, { kind: neverDispatched ? "failed" : "cancelling", detail: taskDeadlineError });
|
|
1552
|
+
});
|
|
1553
|
+
const expireTasks = Effect14.fn("RelayStore.expireTasks")(function* () {
|
|
1554
|
+
const rows = yield* Effect14.try({
|
|
1555
|
+
try: () => db.query(`SELECT * FROM tasks WHERE expires_at <= ? AND status IN ('queued', 'assigned', 'running', 'needs_input')
|
|
1556
|
+
ORDER BY expires_at, id LIMIT 100`).all(now()),
|
|
1557
|
+
catch: operationError("RelayStore.expireTasks")
|
|
1558
|
+
});
|
|
1559
|
+
return yield* Effect14.forEach(rows, (row) => taskFromRow(db, row).pipe(Effect14.flatMap(expireTask)));
|
|
1560
|
+
});
|
|
1354
1561
|
const assignTask = Effect14.fn("RelayStore.assignTask")(function* (taskId) {
|
|
1355
|
-
const current = yield* getTask(taskId);
|
|
1562
|
+
const current = yield* getTask(taskId).pipe(Effect14.flatMap(expireTask));
|
|
1356
1563
|
if (current.status !== "queued") {
|
|
1357
1564
|
return current;
|
|
1358
1565
|
}
|
|
1359
1566
|
const timestamp = now();
|
|
1360
1567
|
yield* Effect14.try({
|
|
1361
1568
|
try: () => db.transaction(() => {
|
|
1362
|
-
const result = db.query(`UPDATE tasks SET status = 'assigned', updated_at = ?,
|
|
1569
|
+
const result = db.query(`UPDATE tasks SET status = 'assigned', updated_at = ?, run_id = CASE
|
|
1570
|
+
WHEN dispatched_at IS NOT NULL OR started_at IS NOT NULL OR (
|
|
1571
|
+
provider = 'grok-bot' AND NOT EXISTS (
|
|
1572
|
+
SELECT 1 FROM devices, json_each(devices.capabilities_json) capability
|
|
1573
|
+
WHERE devices.id = tasks.target_device_id
|
|
1574
|
+
AND json_extract(capability.value, '$.id') = 'task-clarification'
|
|
1575
|
+
)
|
|
1576
|
+
)
|
|
1577
|
+
THEN run_id ELSE COALESCE(run_id, ?) END,
|
|
1363
1578
|
provider_session_id = CASE
|
|
1364
1579
|
WHEN provider = 'grok-bot' THEN NULL
|
|
1365
1580
|
WHEN started_at IS NULL THEN COALESCE((
|
|
@@ -1378,7 +1593,7 @@ var PersistenceError, TaskProgressError, Service2, operationError = (operation)
|
|
|
1378
1593
|
SELECT 1 FROM device_operations active
|
|
1379
1594
|
WHERE active.target_device_id = ?
|
|
1380
1595
|
AND active.status IN ('assigned', 'running')
|
|
1381
|
-
)`).run(timestamp, taskId, current.targetDeviceId, taskId, taskSlot(current), current.targetDeviceId);
|
|
1596
|
+
)`).run(timestamp, crypto.randomUUID(), taskId, current.targetDeviceId, taskId, taskSlot(current), current.targetDeviceId);
|
|
1382
1597
|
if (result.changes === 1) {
|
|
1383
1598
|
recordTaskTraceEvent(taskId, "assigned", "Relay dispatched the task over the device WebSocket", timestamp);
|
|
1384
1599
|
}
|
|
@@ -1419,22 +1634,173 @@ var PersistenceError, TaskProgressError, Service2, operationError = (operation)
|
|
|
1419
1634
|
}
|
|
1420
1635
|
return progress;
|
|
1421
1636
|
});
|
|
1422
|
-
const
|
|
1637
|
+
const requestTaskInput = Effect14.fn("RelayStore.requestTaskInput")(function* (taskId, deviceId, input, turnFinished = false) {
|
|
1638
|
+
const task = yield* getTask(taskId);
|
|
1639
|
+
if (task.targetDeviceId !== deviceId) {
|
|
1640
|
+
return yield* new TaskInputError({
|
|
1641
|
+
status: 403,
|
|
1642
|
+
message: "Only the target device can request input"
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
if (taskDeadlinePassed(task))
|
|
1646
|
+
return yield* new TaskInputError({ status: 409, message: taskDeadlineError });
|
|
1647
|
+
const active = task.status === "running" || turnFinished && (task.status === "queued" || task.status === "assigned");
|
|
1648
|
+
if (!active || task.runId !== input.runId) {
|
|
1649
|
+
return yield* new TaskInputError({
|
|
1650
|
+
status: 409,
|
|
1651
|
+
message: "Input can only be requested by the current running turn"
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1654
|
+
const row = yield* Effect14.try({
|
|
1655
|
+
try: () => db.query("SELECT * FROM devices WHERE id = ?").get(deviceId),
|
|
1656
|
+
catch: operationError("RelayStore.requestTaskInput.device")
|
|
1657
|
+
});
|
|
1658
|
+
const device = row === null ? undefined : yield* deviceFromRow(row);
|
|
1659
|
+
if (!device?.capabilities.some((capability) => capability.id === "task-clarification")) {
|
|
1660
|
+
return yield* new TaskInputError({
|
|
1661
|
+
status: 409,
|
|
1662
|
+
message: "Upgrade the target worker before requesting clarification"
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
const history = task.clarifications ?? [];
|
|
1666
|
+
const pending = history.at(-1);
|
|
1667
|
+
if (pending !== undefined && pending.answer === undefined) {
|
|
1668
|
+
if (pending.question === input.question)
|
|
1669
|
+
return pending;
|
|
1670
|
+
return yield* new TaskInputError({
|
|
1671
|
+
status: 409,
|
|
1672
|
+
message: "This task already has an unanswered question"
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
if (history.length >= maxTaskClarifications) {
|
|
1676
|
+
return yield* new TaskInputError({
|
|
1677
|
+
status: 409,
|
|
1678
|
+
message: `A task supports at most ${maxTaskClarifications} clarifications`
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
const question = TaskClarification.make({
|
|
1682
|
+
id: ClarificationId.make(crypto.randomUUID()),
|
|
1683
|
+
question: input.question,
|
|
1684
|
+
at: now()
|
|
1685
|
+
});
|
|
1686
|
+
const changed = yield* Effect14.try({
|
|
1687
|
+
try: () => db.transaction(() => {
|
|
1688
|
+
const result = db.query(`UPDATE tasks SET clarifications_json = ?, updated_at = ?
|
|
1689
|
+
WHERE id = ? AND status = ? AND run_id = ?
|
|
1690
|
+
AND COALESCE(clarifications_json, '[]') = ?`).run(JSON.stringify([...history, question]), question.at, taskId, task.status, input.runId, JSON.stringify(history));
|
|
1691
|
+
if (result.changes === 1)
|
|
1692
|
+
recordTaskTraceEvent(taskId, "input_requested", "Worker requested clarification and is ending its turn", question.at);
|
|
1693
|
+
return result.changes;
|
|
1694
|
+
})(),
|
|
1695
|
+
catch: operationError("RelayStore.requestTaskInput")
|
|
1696
|
+
});
|
|
1697
|
+
if (changed !== 1)
|
|
1698
|
+
return yield* new TaskInputError({
|
|
1699
|
+
status: 409,
|
|
1700
|
+
message: "Task changed while requesting input; check its status"
|
|
1701
|
+
});
|
|
1702
|
+
return question;
|
|
1703
|
+
});
|
|
1704
|
+
const answerTaskInput = Effect14.fn("RelayStore.answerTaskInput")(function* (taskId, principal, input) {
|
|
1705
|
+
const task = yield* getTask(taskId);
|
|
1706
|
+
const requester = yield* Effect14.try({
|
|
1707
|
+
try: () => queryTask(taskId)?.requester_id,
|
|
1708
|
+
catch: operationError("RelayStore.answerTaskInput.requester")
|
|
1709
|
+
});
|
|
1710
|
+
if (principal !== "owner" && (principal.role !== "client" || requester !== principal.id)) {
|
|
1711
|
+
return yield* new TaskInputError({
|
|
1712
|
+
status: 403,
|
|
1713
|
+
message: "Only the original requester or relay owner can answer"
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
if (taskDeadlinePassed(task))
|
|
1717
|
+
return yield* new TaskInputError({ status: 409, message: taskDeadlineError });
|
|
1718
|
+
const history = task.clarifications ?? [];
|
|
1719
|
+
const question = history.at(-1);
|
|
1720
|
+
if (task.status !== "needs_input" || question?.id !== input.requestId || question.answer !== undefined) {
|
|
1721
|
+
return yield* new TaskInputError({
|
|
1722
|
+
status: 409,
|
|
1723
|
+
message: "This question is no longer awaiting an answer; check task status"
|
|
1724
|
+
});
|
|
1725
|
+
}
|
|
1726
|
+
const timestamp = now();
|
|
1727
|
+
const answered = TaskClarification.make({
|
|
1728
|
+
...question,
|
|
1729
|
+
answer: { text: input.answer, at: timestamp }
|
|
1730
|
+
});
|
|
1731
|
+
const changed = yield* Effect14.try({
|
|
1732
|
+
try: () => db.transaction(() => {
|
|
1733
|
+
const result = db.query(`UPDATE tasks SET status = 'queued', run_id = ?, clarifications_json = ?, updated_at = ?,
|
|
1734
|
+
result = NULL, error = NULL, completed_at = NULL, progress_note = NULL, progress_at = NULL,
|
|
1735
|
+
started_at = CASE WHEN provider = 'grok-bot' THEN NULL ELSE started_at END,
|
|
1736
|
+
dispatched_at = NULL
|
|
1737
|
+
WHERE id = ? AND status = 'needs_input' AND clarifications_json = ?`).run(crypto.randomUUID(), JSON.stringify([...history.slice(0, -1), answered]), timestamp, taskId, JSON.stringify(history));
|
|
1738
|
+
if (result.changes === 1) {
|
|
1739
|
+
recordTaskTraceEvent(taskId, "input_answered", "Requester answered the worker's question", timestamp);
|
|
1740
|
+
recordTaskTraceEvent(taskId, "queued", "Task queued to resume with the answer", timestamp);
|
|
1741
|
+
}
|
|
1742
|
+
return result.changes;
|
|
1743
|
+
})(),
|
|
1744
|
+
catch: operationError("RelayStore.answerTaskInput")
|
|
1745
|
+
});
|
|
1746
|
+
if (changed !== 1)
|
|
1747
|
+
return yield* new TaskInputError({
|
|
1748
|
+
status: 409,
|
|
1749
|
+
message: "Task changed while answering; check its status"
|
|
1750
|
+
});
|
|
1751
|
+
return yield* getTask(taskId);
|
|
1752
|
+
});
|
|
1753
|
+
const matchesTaskRun = (task, runId) => task.runId === undefined || task.runId === runId || runId === undefined && (task.clarifications?.length ?? 0) === 0;
|
|
1754
|
+
const terminal = Effect14.fn("RelayStore.terminal")(function* (taskId, deviceId, status, result, error, providerSessionId, attachments2, runId) {
|
|
1423
1755
|
const current = yield* requireTarget(taskId, deviceId);
|
|
1424
|
-
if (["completed", "failed", "cancelled"].includes(current.status)) {
|
|
1756
|
+
if (!matchesTaskRun(current, runId) || current.status === "cancelling" && status !== "cancelled" || ["completed", "failed", "cancelled"].includes(current.status)) {
|
|
1425
1757
|
return current;
|
|
1426
1758
|
}
|
|
1759
|
+
if (taskDeadlinePassed(current) && status !== "cancelled")
|
|
1760
|
+
return yield* expireTask(current);
|
|
1761
|
+
const deadlineExpired = taskDeadlinePassed(current) && (current.status !== "cancelling" || current.error === taskDeadlineError);
|
|
1762
|
+
const finalStatus = status === "cancelled" && deadlineExpired ? "failed" : status;
|
|
1763
|
+
const finalError = finalStatus === "failed" && deadlineExpired ? taskDeadlineError : error;
|
|
1427
1764
|
const timestamp = now();
|
|
1428
1765
|
yield* Effect14.try({
|
|
1429
1766
|
try: () => db.transaction(() => {
|
|
1767
|
+
const saveProviderSession = () => {
|
|
1768
|
+
if (current.provider === "grok-bot" || providerSessionId === undefined)
|
|
1769
|
+
return;
|
|
1770
|
+
db.query(`INSERT INTO provider_sessions (thread_id, device_id, provider, session_id, updated_at)
|
|
1771
|
+
VALUES (?, ?, ?, ?, ?)
|
|
1772
|
+
ON CONFLICT(thread_id, device_id, provider) DO UPDATE SET
|
|
1773
|
+
session_id = excluded.session_id, updated_at = excluded.updated_at`).run(current.threadId, deviceId, current.provider, providerSessionId, timestamp);
|
|
1774
|
+
};
|
|
1775
|
+
if (current.status === "needs_input" && status !== "cancelled") {
|
|
1776
|
+
if (current.provider === "grok-bot" || providerSessionId === undefined)
|
|
1777
|
+
return;
|
|
1778
|
+
const updated2 = db.query(`UPDATE tasks SET provider_session_id = ?, updated_at = ?
|
|
1779
|
+
WHERE id = ? AND status = 'needs_input' AND run_id IS ?`).run(providerSessionId, timestamp, taskId, current.runId ?? null);
|
|
1780
|
+
if (updated2.changes === 1)
|
|
1781
|
+
saveProviderSession();
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
if (status !== "cancelled" && current.clarifications?.at(-1)?.answer === undefined && (current.clarifications?.length ?? 0) > 0) {
|
|
1785
|
+
const changed = db.query(`UPDATE tasks SET status = 'needs_input', provider_session_id = ?, updated_at = ?,
|
|
1786
|
+
progress_note = NULL, progress_at = NULL
|
|
1787
|
+
WHERE id = ? AND status = ? AND run_id IS ?`).run(current.provider === "grok-bot" ? null : providerSessionId ?? current.providerSessionId ?? null, timestamp, taskId, current.status, current.runId ?? null);
|
|
1788
|
+
if (changed.changes === 1) {
|
|
1789
|
+
saveProviderSession();
|
|
1790
|
+
recordTaskTraceEvent(taskId, "needs_input", "Worker paused for clarification", timestamp);
|
|
1791
|
+
}
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1430
1794
|
const updated = db.query(`UPDATE tasks SET status = ?, result = ?, error = ?, provider_session_id = ?,
|
|
1431
1795
|
completed_at = ?, updated_at = ?, progress_note = NULL, progress_at = NULL WHERE id = ?
|
|
1432
|
-
AND status NOT IN ('completed', 'failed', 'cancelled')
|
|
1796
|
+
AND status NOT IN ('completed', 'failed', 'cancelled')
|
|
1797
|
+
AND (status <> 'needs_input' OR ? = 'cancelled')
|
|
1798
|
+
AND (status <> 'cancelling' OR ? = 'cancelled') AND run_id IS ?`).run(finalStatus, result ?? null, finalError ?? null, current.provider === "grok-bot" ? null : providerSessionId ?? current.providerSessionId ?? null, timestamp, timestamp, taskId, status, status, current.runId ?? null);
|
|
1433
1799
|
if (updated.changes !== 1) {
|
|
1434
1800
|
return;
|
|
1435
1801
|
}
|
|
1436
1802
|
db.query("DELETE FROM task_attachments WHERE task_id = ? AND direction = 'output'").run(taskId);
|
|
1437
|
-
if (
|
|
1803
|
+
if (finalStatus === "completed") {
|
|
1438
1804
|
if ((attachments2?.length ?? 0) > maxTaskAttachments) {
|
|
1439
1805
|
throw new Error(`A task supports at most ${maxTaskAttachments} output files`);
|
|
1440
1806
|
}
|
|
@@ -1446,20 +1812,15 @@ var PersistenceError, TaskProgressError, Service2, operationError = (operation)
|
|
|
1446
1812
|
db.query("INSERT INTO task_attachments (task_id, name, direction, data) VALUES (?, ?, 'output', ?)").run(taskId, attachment.name, data);
|
|
1447
1813
|
}
|
|
1448
1814
|
}
|
|
1449
|
-
recordTaskTraceEvent(taskId,
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
VALUES (?, ?, ?, ?, ?)
|
|
1453
|
-
ON CONFLICT(thread_id, device_id, provider) DO UPDATE SET
|
|
1454
|
-
session_id = excluded.session_id, updated_at = excluded.updated_at`).run(current.threadId, deviceId, current.provider, providerSessionId, timestamp);
|
|
1455
|
-
}
|
|
1456
|
-
const content = result ?? error;
|
|
1815
|
+
recordTaskTraceEvent(taskId, finalStatus, finalStatus === "completed" ? "Target device completed the task" : finalStatus === "failed" ? "Target device reported a task failure" : "Target device acknowledged cancellation", timestamp);
|
|
1816
|
+
saveProviderSession();
|
|
1817
|
+
const content = result ?? finalError;
|
|
1457
1818
|
if (content !== undefined) {
|
|
1458
|
-
const role =
|
|
1819
|
+
const role = finalStatus === "completed" ? "agent" : "system";
|
|
1459
1820
|
db.query(`INSERT INTO messages (
|
|
1460
1821
|
id, thread_id, role, kind, author_id, author_name, content, created_at,
|
|
1461
1822
|
task_id, device_id
|
|
1462
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(makeMessageId(), current.threadId, role,
|
|
1823
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(makeMessageId(), current.threadId, role, finalStatus === "failed" ? "error" : "chat", deviceId, finalStatus === "completed" ? providerName(current.provider) : "Cohall", content, timestamp, taskId, deviceId);
|
|
1463
1824
|
db.query("UPDATE threads SET updated_at = ? WHERE id = ?").run(timestamp, current.threadId);
|
|
1464
1825
|
}
|
|
1465
1826
|
pruneTerminalHistory(taskId);
|
|
@@ -1468,16 +1829,19 @@ var PersistenceError, TaskProgressError, Service2, operationError = (operation)
|
|
|
1468
1829
|
});
|
|
1469
1830
|
return yield* getTask(taskId);
|
|
1470
1831
|
});
|
|
1832
|
+
const pauseTaskForInput = Effect14.fn("RelayStore.pauseTaskForInput")(function* (taskId, deviceId, input) {
|
|
1833
|
+
const task = yield* requireTarget(taskId, deviceId);
|
|
1834
|
+
if (task.runId !== input.runId || task.status !== "queued" && task.status !== "assigned" && task.status !== "running")
|
|
1835
|
+
return task;
|
|
1836
|
+
return yield* requestTaskInput(taskId, deviceId, input, true).pipe(Effect14.andThen(terminal(taskId, deviceId, "completed", "", undefined, undefined, undefined, input.runId)), Effect14.catchTag("RelayStore.TaskInputError", (cause) => terminal(taskId, deviceId, "failed", undefined, cause.message, undefined, undefined, input.runId)));
|
|
1837
|
+
});
|
|
1471
1838
|
const requestCancellation = Effect14.fn("RelayStore.requestCancellation")(function* (taskId) {
|
|
1472
|
-
const current = yield* getTask(taskId);
|
|
1839
|
+
const current = yield* getTask(taskId).pipe(Effect14.flatMap(expireTask));
|
|
1473
1840
|
if (["completed", "failed", "cancelled"].includes(current.status)) {
|
|
1474
1841
|
return current;
|
|
1475
1842
|
}
|
|
1476
|
-
const dispatched =
|
|
1477
|
-
|
|
1478
|
-
catch: operationError("RelayStore.requestCancellation")
|
|
1479
|
-
}));
|
|
1480
|
-
if (current.provider === "grok-bot" && (current.status !== "queued" || current.startedAt !== undefined || dispatched)) {
|
|
1843
|
+
const dispatched = yield* hasDispatchEvidence(taskId);
|
|
1844
|
+
if (current.provider === "grok-bot" && current.status !== "needs_input" && (current.status !== "queued" || current.startedAt !== undefined || dispatched)) {
|
|
1481
1845
|
return yield* new PersistenceError({
|
|
1482
1846
|
operation: "RelayStore.requestCancellation",
|
|
1483
1847
|
message: "Stop this bot in Grok Bot; its gateway cannot cancel an individual Cohall request"
|
|
@@ -1487,7 +1851,8 @@ var PersistenceError, TaskProgressError, Service2, operationError = (operation)
|
|
|
1487
1851
|
return current;
|
|
1488
1852
|
}
|
|
1489
1853
|
const timestamp = now();
|
|
1490
|
-
|
|
1854
|
+
const neverDispatched = current.status === "queued" && !dispatched && (current.provider === "grok-bot" || current.runId === undefined && current.startedAt === undefined);
|
|
1855
|
+
return neverDispatched || current.status === "needs_input" && current.provider === "grok-bot" ? yield* transition(taskId, ["queued", "needs_input"], { status: "cancelled", completedAt: timestamp }, { kind: "cancelled", detail: "Client cancelled the task before dispatch" }) : yield* transition(taskId, ["queued", "assigned", "running", "needs_input"], {
|
|
1491
1856
|
status: "cancelling"
|
|
1492
1857
|
}, { kind: "cancelling", detail: "Client requested task cancellation" });
|
|
1493
1858
|
});
|
|
@@ -1495,12 +1860,14 @@ var PersistenceError, TaskProgressError, Service2, operationError = (operation)
|
|
|
1495
1860
|
const timestamp = now();
|
|
1496
1861
|
yield* Effect14.try({
|
|
1497
1862
|
try: () => db.transaction(() => {
|
|
1498
|
-
const interrupted = db.query(`SELECT id FROM tasks WHERE target_device_id = ?
|
|
1863
|
+
const interrupted = db.query(`SELECT id, ${interruptedTaskStatus} AS status FROM tasks WHERE target_device_id = ?
|
|
1499
1864
|
AND status IN ('assigned', 'running')`).all(deviceId);
|
|
1500
|
-
db.query(`UPDATE tasks SET status =
|
|
1865
|
+
db.query(`UPDATE tasks SET status = ${interruptedTaskStatus}, updated_at = ?, progress_note = NULL, progress_at = NULL,
|
|
1866
|
+
dispatched_at = CASE WHEN provider = 'grok-bot' THEN dispatched_at
|
|
1867
|
+
ELSE COALESCE(dispatched_at, started_at, updated_at) END
|
|
1501
1868
|
WHERE target_device_id = ? AND status IN ('assigned', 'running')`).run(timestamp, deviceId);
|
|
1502
1869
|
for (const task of interrupted) {
|
|
1503
|
-
recordTaskTraceEvent(TaskId.make(task.id), "requeued", "Target device disconnected before task completion", timestamp);
|
|
1870
|
+
recordTaskTraceEvent(TaskId.make(task.id), task.status === "needs_input" ? "needs_input" : "requeued", "Target device disconnected before task completion", timestamp);
|
|
1504
1871
|
}
|
|
1505
1872
|
})(),
|
|
1506
1873
|
catch: operationError("RelayStore.requeueTasksFor")
|
|
@@ -1620,14 +1987,20 @@ var PersistenceError, TaskProgressError, Service2, operationError = (operation)
|
|
|
1620
1987
|
return Service2.of({
|
|
1621
1988
|
recover,
|
|
1622
1989
|
listDevices,
|
|
1990
|
+
listDevicePage,
|
|
1623
1991
|
usage,
|
|
1624
1992
|
forgetDevice,
|
|
1625
1993
|
upsertDevice,
|
|
1626
1994
|
heartbeat: (deviceId, status, bots) => updateDeviceStatus(deviceId, status, bots),
|
|
1627
1995
|
markDeviceOffline: (deviceId) => updateDeviceStatus(deviceId, "offline"),
|
|
1628
1996
|
createDelegation: createDelegation2,
|
|
1997
|
+
expireTasks,
|
|
1998
|
+
findDelegation,
|
|
1629
1999
|
getTask,
|
|
1630
2000
|
reportTaskProgress,
|
|
2001
|
+
requestTaskInput,
|
|
2002
|
+
pauseTaskForInput,
|
|
2003
|
+
answerTaskInput,
|
|
1631
2004
|
listAttachments,
|
|
1632
2005
|
readAttachment,
|
|
1633
2006
|
inboxFor,
|
|
@@ -1643,10 +2016,13 @@ var PersistenceError, TaskProgressError, Service2, operationError = (operation)
|
|
|
1643
2016
|
catch: operationError("RelayStore.markTaskDispatched")
|
|
1644
2017
|
}),
|
|
1645
2018
|
rollbackAssignment: (taskId) => transition(taskId, ["assigned"], { status: "queued" }, { kind: "requeued", detail: "Target device was unavailable during dispatch" }),
|
|
1646
|
-
acceptTask: (taskId, deviceId) => requireTarget(taskId, deviceId).pipe(Effect14.flatMap(() => transition(taskId, ["assigned"], { status: "running", startedAt: now() }, {
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
2019
|
+
acceptTask: (taskId, deviceId, runId) => requireTarget(taskId, deviceId).pipe(Effect14.flatMap((task) => matchesTaskRun(task, runId) ? transition(taskId, ["assigned"], { status: "running", startedAt: task.startedAt ?? now() }, {
|
|
2020
|
+
kind: "running",
|
|
2021
|
+
detail: task.startedAt === undefined ? "Target device accepted the task and started the provider" : "Target device accepted the task again"
|
|
2022
|
+
}) : Effect14.succeed(task))),
|
|
2023
|
+
finishTask: (taskId, deviceId, result, providerSessionId, attachments2, runId) => terminal(taskId, deviceId, "completed", result, undefined, providerSessionId, attachments2, runId),
|
|
2024
|
+
failTask: (taskId, deviceId, error, runId) => terminal(taskId, deviceId, "failed", undefined, error, undefined, undefined, runId),
|
|
2025
|
+
acknowledgeCancellation: (taskId, deviceId, runId) => terminal(taskId, deviceId, "cancelled", undefined, undefined, undefined, undefined, runId),
|
|
1650
2026
|
requestCancellation,
|
|
1651
2027
|
requeueTasksFor,
|
|
1652
2028
|
createUpgradeOperations,
|
|
@@ -1699,10 +2075,18 @@ var PersistenceError, TaskProgressError, Service2, operationError = (operation)
|
|
|
1699
2075
|
target_device_id TEXT NOT NULL REFERENCES devices(id), parent_task_id TEXT,
|
|
1700
2076
|
workspace TEXT, provider_session_id TEXT, result TEXT, error TEXT,
|
|
1701
2077
|
created_at TEXT NOT NULL, updated_at TEXT NOT NULL, started_at TEXT, completed_at TEXT,
|
|
1702
|
-
dispatched_at TEXT, progress_note TEXT, progress_at TEXT
|
|
2078
|
+
dispatched_at TEXT, progress_note TEXT, progress_at TEXT,
|
|
2079
|
+
run_id TEXT, clarifications_json TEXT, expires_at TEXT
|
|
1703
2080
|
);
|
|
1704
2081
|
CREATE INDEX IF NOT EXISTS tasks_thread_created ON tasks(thread_id, created_at);
|
|
1705
2082
|
CREATE INDEX IF NOT EXISTS tasks_target_status ON tasks(target_device_id, status);
|
|
2083
|
+
CREATE TABLE IF NOT EXISTS task_requests (
|
|
2084
|
+
requester_id TEXT NOT NULL, request_id TEXT NOT NULL, input_hash TEXT NOT NULL,
|
|
2085
|
+
task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
|
|
2086
|
+
PRIMARY KEY(requester_id, request_id)
|
|
2087
|
+
);
|
|
2088
|
+
CREATE INDEX IF NOT EXISTS task_requests_retained_task
|
|
2089
|
+
ON task_requests(task_id) WHERE task_id IS NOT NULL;
|
|
1706
2090
|
CREATE TABLE IF NOT EXISTS task_attachments (
|
|
1707
2091
|
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1708
2092
|
name TEXT NOT NULL, direction TEXT NOT NULL CHECK(direction IN ('input', 'output')),
|
|
@@ -1750,6 +2134,13 @@ var PersistenceError, TaskProgressError, Service2, operationError = (operation)
|
|
|
1750
2134
|
db.exec("ALTER TABLE devices ADD COLUMN bots_json TEXT");
|
|
1751
2135
|
}
|
|
1752
2136
|
const taskColumns = db.query("PRAGMA table_info(tasks)").all();
|
|
2137
|
+
if (!taskColumns.some((column) => column.name === "expires_at"))
|
|
2138
|
+
db.exec("ALTER TABLE tasks ADD COLUMN expires_at TEXT");
|
|
2139
|
+
db.exec("CREATE INDEX IF NOT EXISTS tasks_pending_deadline ON tasks(expires_at) WHERE status IN ('queued', 'assigned', 'running', 'needs_input')");
|
|
2140
|
+
if (!taskColumns.some((column) => column.name === "run_id"))
|
|
2141
|
+
db.exec("ALTER TABLE tasks ADD COLUMN run_id TEXT");
|
|
2142
|
+
if (!taskColumns.some((column) => column.name === "clarifications_json"))
|
|
2143
|
+
db.exec("ALTER TABLE tasks ADD COLUMN clarifications_json TEXT");
|
|
1753
2144
|
if (!taskColumns.some((column) => column.name === "progress_note")) {
|
|
1754
2145
|
db.exec("ALTER TABLE tasks ADD COLUMN progress_note TEXT");
|
|
1755
2146
|
}
|
|
@@ -1774,6 +2165,13 @@ var PersistenceError, TaskProgressError, Service2, operationError = (operation)
|
|
|
1774
2165
|
WHERE provider = 'grok-bot' AND status NOT IN ('completed', 'failed', 'cancelled')`);
|
|
1775
2166
|
})();
|
|
1776
2167
|
}
|
|
2168
|
+
db.exec(`UPDATE tasks SET dispatched_at = COALESCE(started_at, updated_at)
|
|
2169
|
+
WHERE provider <> 'grok-bot' AND dispatched_at IS NULL AND run_id IS NULL
|
|
2170
|
+
AND status NOT IN ('completed', 'failed', 'cancelled')
|
|
2171
|
+
AND (status IN ('assigned', 'running') OR started_at IS NOT NULL OR EXISTS (
|
|
2172
|
+
SELECT 1 FROM task_trace_events
|
|
2173
|
+
WHERE task_id = tasks.id AND kind IN ('assigned', 'running')
|
|
2174
|
+
))`);
|
|
1777
2175
|
},
|
|
1778
2176
|
catch: operationError("RelayStore.migrate")
|
|
1779
2177
|
}), layer2 = (path, retainedTerminalTasks = 1000) => Layer2.effect(Service2, Effect14.acquireRelease(Effect14.try({
|
|
@@ -1784,10 +2182,20 @@ var init_store = __esm(() => {
|
|
|
1784
2182
|
init_src();
|
|
1785
2183
|
init_database();
|
|
1786
2184
|
init_store();
|
|
1787
|
-
PersistenceError = class PersistenceError extends Schema15.TaggedErrorClass()("RelayStore.PersistenceError", {
|
|
2185
|
+
PersistenceError = class PersistenceError extends Schema15.TaggedErrorClass()("RelayStore.PersistenceError", {
|
|
2186
|
+
operation: Schema15.String,
|
|
2187
|
+
message: Schema15.String,
|
|
2188
|
+
missingRecord: Schema15.optionalKey(Schema15.Literals(["task", "operation"]))
|
|
2189
|
+
}) {
|
|
1788
2190
|
};
|
|
1789
2191
|
TaskProgressError = class TaskProgressError extends Schema15.TaggedErrorClass()("RelayStore.TaskProgressError", { status: Schema15.Literals([403, 409]), message: Schema15.String }) {
|
|
1790
2192
|
};
|
|
2193
|
+
TaskInputError = class TaskInputError extends Schema15.TaggedErrorClass()("RelayStore.TaskInputError", { status: Schema15.Literals([403, 409]), message: Schema15.String }) {
|
|
2194
|
+
};
|
|
2195
|
+
TaskDeadlineError = class TaskDeadlineError extends Schema15.TaggedErrorClass()("RelayStore.TaskDeadlineError", { status: Schema15.Literals([400, 409]), message: Schema15.String }) {
|
|
2196
|
+
};
|
|
2197
|
+
TaskRequestError = class TaskRequestError extends Schema15.TaggedErrorClass()("RelayStore.TaskRequestError", { status: Schema15.Literals([409, 410]), message: Schema15.String }) {
|
|
2198
|
+
};
|
|
1791
2199
|
Service2 = class Service2 extends Context2.Service()("@cohall/RelayStore") {
|
|
1792
2200
|
};
|
|
1793
2201
|
});
|
|
@@ -1802,7 +2210,7 @@ __export(exports_main, {
|
|
|
1802
2210
|
canReadTaskAttachments: () => canReadTaskAttachments,
|
|
1803
2211
|
canDispatchTaskToDevice: () => canDispatchTaskToDevice
|
|
1804
2212
|
});
|
|
1805
|
-
import { Effect as Effect15, ManagedRuntime, Schema as Schema16 } from "effect";
|
|
2213
|
+
import { Effect as Effect15, ManagedRuntime, Schema as Schema16, Schedule as Schedule4 } from "effect";
|
|
1806
2214
|
import { createHash as createHash3, timingSafeEqual } from "node:crypto";
|
|
1807
2215
|
import { access as access6, chmod as chmod8 } from "node:fs/promises";
|
|
1808
2216
|
import { createServer } from "node:http";
|
|
@@ -1833,7 +2241,7 @@ var RequestError, relayListenOptions = (configuration, environment = process.env
|
|
|
1833
2241
|
if (request.method === "GET" || request.method === "HEAD") {
|
|
1834
2242
|
return Promise.resolve(undefined);
|
|
1835
2243
|
}
|
|
1836
|
-
return new Promise((
|
|
2244
|
+
return new Promise((resolve6, reject) => {
|
|
1837
2245
|
const chunks = [];
|
|
1838
2246
|
let size = 0;
|
|
1839
2247
|
const cleanup = () => {
|
|
@@ -1852,7 +2260,7 @@ var RequestError, relayListenOptions = (configuration, environment = process.env
|
|
|
1852
2260
|
};
|
|
1853
2261
|
const onEnd = () => {
|
|
1854
2262
|
cleanup();
|
|
1855
|
-
|
|
2263
|
+
resolve6(Buffer.concat(chunks, size));
|
|
1856
2264
|
};
|
|
1857
2265
|
const onError = (cause) => {
|
|
1858
2266
|
cleanup();
|
|
@@ -1939,6 +2347,11 @@ Content-Length: 0\r
|
|
|
1939
2347
|
const resolvedInput = parent === undefined ? input : { ...input, threadId: parent.threadId };
|
|
1940
2348
|
const devices = yield* store.listDevices().pipe(Effect15.mapError((cause) => new RequestError({ status: 500, message: cause.message })));
|
|
1941
2349
|
const provider = input.provider ?? (input.botId === undefined ? "codex" : "grok-bot");
|
|
2350
|
+
if (input.expiresAt !== undefined && provider === "grok-bot")
|
|
2351
|
+
return yield* new RequestError({
|
|
2352
|
+
status: 400,
|
|
2353
|
+
message: "Task deadlines require a coding provider"
|
|
2354
|
+
});
|
|
1942
2355
|
const hasAttachments = (input.attachments?.length ?? 0) > 0;
|
|
1943
2356
|
if (hasAttachments && provider === "grok-bot") {
|
|
1944
2357
|
return yield* new RequestError({
|
|
@@ -1950,7 +2363,7 @@ Content-Length: 0\r
|
|
|
1950
2363
|
provider,
|
|
1951
2364
|
...input.botId === undefined ? {} : { botId: input.botId }
|
|
1952
2365
|
});
|
|
1953
|
-
const blocked = new Set(ancestors.filter((task) => !isTerminalTask(task) && taskSlot(task) === slot).map((task) => task.targetDeviceId));
|
|
2366
|
+
const blocked = new Set(ancestors.filter((task) => !isTerminalTask(task) && task.status !== "needs_input" && taskSlot(task) === slot).map((task) => task.targetDeviceId));
|
|
1954
2367
|
if (input.targetDeviceId !== undefined) {
|
|
1955
2368
|
const target = devices.find((device) => device.id === input.targetDeviceId);
|
|
1956
2369
|
if (target === undefined) {
|
|
@@ -1965,6 +2378,11 @@ Content-Length: 0\r
|
|
|
1965
2378
|
message: `${target.name} does not advertise the ${provider} provider`
|
|
1966
2379
|
});
|
|
1967
2380
|
}
|
|
2381
|
+
if (input.expiresAt !== undefined && !target.capabilities.some((capability) => capability.id === "task-deadlines"))
|
|
2382
|
+
return yield* new RequestError({
|
|
2383
|
+
status: 409,
|
|
2384
|
+
message: `${target.name} does not support task deadlines; upgrade its Cohall worker`
|
|
2385
|
+
});
|
|
1968
2386
|
if (hasAttachments && !target.capabilities.some((capability) => capability.id === "task-attachments")) {
|
|
1969
2387
|
return yield* new RequestError({
|
|
1970
2388
|
status: 409,
|
|
@@ -1985,7 +2403,7 @@ Content-Length: 0\r
|
|
|
1985
2403
|
}
|
|
1986
2404
|
return { input: resolvedInput, targetDeviceId: target.id };
|
|
1987
2405
|
}
|
|
1988
|
-
const providerDevices = devices.filter((device) => device.providers.includes(provider) && (input.botId === undefined || device.bots?.some((bot) => bot.id === input.botId)));
|
|
2406
|
+
const providerDevices = devices.filter((device) => device.providers.includes(provider) && (input.expiresAt === undefined || device.capabilities.some((capability) => capability.id === "task-deadlines")) && (input.botId === undefined || device.bots?.some((bot) => bot.id === input.botId)));
|
|
1989
2407
|
const candidates = hasAttachments ? providerDevices.filter((device) => device.capabilities.some((capability) => capability.id === "task-attachments")) : providerDevices;
|
|
1990
2408
|
if (input.botId !== undefined && candidates.length > 1) {
|
|
1991
2409
|
return yield* new RequestError({
|
|
@@ -1997,11 +2415,11 @@ Content-Length: 0\r
|
|
|
1997
2415
|
if (selected === undefined) {
|
|
1998
2416
|
return yield* new RequestError({
|
|
1999
2417
|
status: 409,
|
|
2000
|
-
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`
|
|
2418
|
+
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.expiresAt !== undefined ? `No Cohall device advertising the ${provider} provider supports task deadlines; 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`
|
|
2001
2419
|
});
|
|
2002
2420
|
}
|
|
2003
2421
|
return { input: resolvedInput, targetDeviceId: selected.id };
|
|
2004
|
-
}), 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 () => {
|
|
2422
|
+
}), canReadTaskAttachments = (principal, task) => principal === "owner" || principal.role === "client" || principal.deviceId === task.targetDeviceId, requiresClarificationSupport = (task) => (task.clarifications?.length ?? 0) > 0 || task.provider === "grok-bot" && task.runId !== undefined, canDispatchTaskToDevice = (task, device) => (task.expiresAt === undefined || device?.capabilities.some((capability) => capability.id === "task-deadlines") === true) && ((task.inputAttachmentNames?.length ?? 0) === 0 || device?.capabilities.some((capability) => capability.id === "task-attachments") === true) && (!requiresClarificationSupport(task) || device?.capabilities.some((capability) => capability.id === "task-clarification") === true), runRelay = async () => {
|
|
2005
2423
|
const configuration = await Effect15.runPromise(loadEnvironmentConfiguration);
|
|
2006
2424
|
process.umask(63);
|
|
2007
2425
|
const runtime = ManagedRuntime.make(exports_store.layer(configuration.databasePath, configuration.historyTaskLimit));
|
|
@@ -2036,20 +2454,39 @@ Content-Length: 0\r
|
|
|
2036
2454
|
const token = authorization.slice("Bearer ".length);
|
|
2037
2455
|
return await authenticate(token, "client") ?? (allowDevice ? authenticate(token, "device") : undefined);
|
|
2038
2456
|
};
|
|
2457
|
+
const sendCancellation = (task) => {
|
|
2458
|
+
hub.sendToDevice(task.targetDeviceId, SocketEvent.make({
|
|
2459
|
+
_tag: "CancelTask",
|
|
2460
|
+
taskId: task.id,
|
|
2461
|
+
...task.runId === undefined ? {} : { runId: task.runId }
|
|
2462
|
+
}));
|
|
2463
|
+
};
|
|
2039
2464
|
const dispatch = async (task) => {
|
|
2040
2465
|
const store = await run2(exports_store.Service);
|
|
2041
|
-
if ((task.inputAttachmentNames?.length ?? 0) > 0) {
|
|
2466
|
+
if (task.expiresAt !== undefined || (task.inputAttachmentNames?.length ?? 0) > 0 || requiresClarificationSupport(task)) {
|
|
2042
2467
|
const devices = await Effect15.runPromise(store.listDevices());
|
|
2043
2468
|
const target = devices.find((device) => device.id === task.targetDeviceId);
|
|
2469
|
+
if (task.expiresAt !== undefined && !target?.capabilities.some((capability) => capability.id === "task-deadlines"))
|
|
2470
|
+
return task;
|
|
2471
|
+
if (requiresClarificationSupport(task) && !target?.capabilities.some((capability) => capability.id === "task-clarification"))
|
|
2472
|
+
return task;
|
|
2044
2473
|
if (!canDispatchTaskToDevice(task, target)) {
|
|
2045
|
-
return Effect15.runPromise(store.failTask(task.id, task.targetDeviceId, "Target worker no longer supports task file attachments. Upgrade the worker and retry."));
|
|
2474
|
+
return Effect15.runPromise(store.failTask(task.id, task.targetDeviceId, "Target worker no longer supports task file attachments. Upgrade the worker and retry.", task.runId));
|
|
2046
2475
|
}
|
|
2047
2476
|
}
|
|
2477
|
+
if (!hub.hasDevice(task.targetDeviceId))
|
|
2478
|
+
return task;
|
|
2048
2479
|
const assigned = await Effect15.runPromise(store.assignTask(task.id));
|
|
2049
2480
|
if (assigned.status !== "assigned") {
|
|
2481
|
+
if (assigned.status === "cancelling")
|
|
2482
|
+
sendCancellation(assigned);
|
|
2050
2483
|
return assigned;
|
|
2051
2484
|
}
|
|
2052
|
-
|
|
2485
|
+
const assignment = SocketEvent.make({ _tag: "TaskAssigned", task: assigned });
|
|
2486
|
+
if (Buffer.byteLength(JSON.stringify(assignment)) > maxSocketPayloadBytes) {
|
|
2487
|
+
return Effect15.runPromise(store.failTask(assigned.id, assigned.targetDeviceId, "Task prompt, context, and clarification history exceed the 1 MiB transfer limit. Retry with a shorter prompt or context.", assigned.runId));
|
|
2488
|
+
}
|
|
2489
|
+
if (hub.sendToDevice(assigned.targetDeviceId, assignment, assigned.provider === "grok-bot" ? () => Effect15.runSync(store.markTaskDispatched(assigned.id)) : undefined)) {
|
|
2053
2490
|
return assigned;
|
|
2054
2491
|
}
|
|
2055
2492
|
return Effect15.runPromise(store.rollbackAssignment(task.id));
|
|
@@ -2086,7 +2523,7 @@ Content-Length: 0\r
|
|
|
2086
2523
|
}));
|
|
2087
2524
|
for (const task of tasks) {
|
|
2088
2525
|
if (task.status === "cancelling") {
|
|
2089
|
-
|
|
2526
|
+
sendCancellation(task);
|
|
2090
2527
|
} else {
|
|
2091
2528
|
await dispatch(task);
|
|
2092
2529
|
}
|
|
@@ -2146,7 +2583,9 @@ Content-Length: 0\r
|
|
|
2146
2583
|
_tag: "Connected",
|
|
2147
2584
|
serverVersion: version,
|
|
2148
2585
|
connectedAt: now(),
|
|
2149
|
-
taskAttachments: true
|
|
2586
|
+
taskAttachments: true,
|
|
2587
|
+
taskClarification: true,
|
|
2588
|
+
taskDeadlines: true
|
|
2150
2589
|
})));
|
|
2151
2590
|
return;
|
|
2152
2591
|
}
|
|
@@ -2182,12 +2621,18 @@ Content-Length: 0\r
|
|
|
2182
2621
|
sendError(socket, "device_hello_required", "Send DeviceHello before device events");
|
|
2183
2622
|
return;
|
|
2184
2623
|
}
|
|
2185
|
-
const settle = (taskId) => {
|
|
2186
|
-
socket.send(JSON.stringify(SocketEvent.make({
|
|
2624
|
+
const settle = (taskId, runId) => {
|
|
2625
|
+
socket.send(JSON.stringify(SocketEvent.make({
|
|
2626
|
+
_tag: "TaskSettled",
|
|
2627
|
+
taskId,
|
|
2628
|
+
...runId === undefined ? {} : { runId }
|
|
2629
|
+
})));
|
|
2187
2630
|
};
|
|
2188
2631
|
const settleOperation = (operationId) => {
|
|
2189
2632
|
socket.send(JSON.stringify(SocketEvent.make({ _tag: "OperationSettled", operationId })));
|
|
2190
2633
|
};
|
|
2634
|
+
const taskTerminal = event._tag === "TaskFinished" || event._tag === "TaskInputRequested" || event._tag === "TaskFailed" || event._tag === "TaskCancelled";
|
|
2635
|
+
const operationTerminal = event._tag === "OperationFinished" || event._tag === "OperationFailed";
|
|
2191
2636
|
const processed = await run2(Effect15.gen(function* () {
|
|
2192
2637
|
const store = yield* exports_store.Service;
|
|
2193
2638
|
switch (event._tag) {
|
|
@@ -2197,16 +2642,23 @@ Content-Length: 0\r
|
|
|
2197
2642
|
}
|
|
2198
2643
|
return;
|
|
2199
2644
|
case "TaskAccepted":
|
|
2200
|
-
yield* store.acceptTask(event.taskId, deviceId);
|
|
2645
|
+
yield* store.acceptTask(event.taskId, deviceId, event.runId);
|
|
2201
2646
|
return;
|
|
2202
2647
|
case "TaskFinished":
|
|
2203
|
-
yield* store.finishTask(event.taskId, deviceId, event.result, event.providerSessionId, event.attachments);
|
|
2648
|
+
yield* store.finishTask(event.taskId, deviceId, event.result, event.providerSessionId, event.attachments, event.runId);
|
|
2649
|
+
return;
|
|
2650
|
+
case "TaskInputRequested": {
|
|
2651
|
+
yield* store.pauseTaskForInput(event.taskId, deviceId, {
|
|
2652
|
+
runId: event.runId,
|
|
2653
|
+
question: event.question
|
|
2654
|
+
});
|
|
2204
2655
|
return;
|
|
2656
|
+
}
|
|
2205
2657
|
case "TaskFailed":
|
|
2206
|
-
yield* store.failTask(event.taskId, deviceId, event.error);
|
|
2658
|
+
yield* store.failTask(event.taskId, deviceId, event.error, event.runId);
|
|
2207
2659
|
return;
|
|
2208
2660
|
case "TaskCancelled":
|
|
2209
|
-
yield* store.acknowledgeCancellation(event.taskId, deviceId);
|
|
2661
|
+
yield* store.acknowledgeCancellation(event.taskId, deviceId, event.runId);
|
|
2210
2662
|
return;
|
|
2211
2663
|
case "OperationAccepted":
|
|
2212
2664
|
yield* store.acceptOperation(event.operationId, deviceId);
|
|
@@ -2227,21 +2679,31 @@ Content-Length: 0\r
|
|
|
2227
2679
|
return;
|
|
2228
2680
|
}
|
|
2229
2681
|
})).then(() => true).catch((cause) => {
|
|
2682
|
+
if (cause instanceof exports_store.PersistenceError && (taskTerminal && cause.missingRecord === "task" || operationTerminal && cause.missingRecord === "operation"))
|
|
2683
|
+
return true;
|
|
2230
2684
|
sendError(socket, "event_failed", cause instanceof Error ? cause.message : String(cause));
|
|
2231
2685
|
return false;
|
|
2232
2686
|
});
|
|
2233
|
-
if (processed &&
|
|
2234
|
-
settle(event.taskId);
|
|
2687
|
+
if (processed && taskTerminal) {
|
|
2688
|
+
settle(event.taskId, event.runId);
|
|
2235
2689
|
await dispatchPending(deviceId);
|
|
2236
2690
|
}
|
|
2237
|
-
if (processed &&
|
|
2691
|
+
if (processed && operationTerminal) {
|
|
2238
2692
|
settleOperation(event.operationId);
|
|
2239
2693
|
await dispatchPending(deviceId);
|
|
2240
2694
|
}
|
|
2241
2695
|
};
|
|
2242
2696
|
const api = async (request, url) => {
|
|
2243
2697
|
if (url.pathname === "/api/health" && request.method === "GET") {
|
|
2244
|
-
return json({
|
|
2698
|
+
return json({
|
|
2699
|
+
ok: true,
|
|
2700
|
+
version,
|
|
2701
|
+
taskAttachments: true,
|
|
2702
|
+
taskProgress: true,
|
|
2703
|
+
taskClarification: true,
|
|
2704
|
+
taskDeadlines: true,
|
|
2705
|
+
taskIdempotency: true
|
|
2706
|
+
});
|
|
2245
2707
|
}
|
|
2246
2708
|
if (url.pathname === "/api/auth/pair" && request.method === "POST") {
|
|
2247
2709
|
return run2(Effect15.gen(function* () {
|
|
@@ -2252,7 +2714,8 @@ Content-Length: 0\r
|
|
|
2252
2714
|
}
|
|
2253
2715
|
const attachmentRoute = request.method === "GET" && /^\/api\/tasks\/[^/]+\/attachments(?:\/[^/]+)?$/.test(url.pathname);
|
|
2254
2716
|
const progressRoute = request.method === "POST" && /^\/api\/tasks\/[^/]+\/progress$/.test(url.pathname);
|
|
2255
|
-
const
|
|
2717
|
+
const inputRoute = request.method === "POST" && /^\/api\/tasks\/[^/]+\/input$/.test(url.pathname);
|
|
2718
|
+
const principal = await principalFor(request, attachmentRoute || progressRoute || inputRoute);
|
|
2256
2719
|
if (principal === undefined) {
|
|
2257
2720
|
return json({ error: "Unauthorized" }, 401);
|
|
2258
2721
|
}
|
|
@@ -2277,6 +2740,10 @@ Content-Length: 0\r
|
|
|
2277
2740
|
hub.closeSession(id);
|
|
2278
2741
|
return json(session);
|
|
2279
2742
|
}
|
|
2743
|
+
if (url.pathname === "/api/devices/page" && request.method === "GET") {
|
|
2744
|
+
const after = url.searchParams.get("after");
|
|
2745
|
+
return json(yield* store.listDevicePage(after === null ? undefined : yield* pathId(DeviceId, after)));
|
|
2746
|
+
}
|
|
2280
2747
|
if (url.pathname === "/api/devices" && request.method === "GET") {
|
|
2281
2748
|
return json(yield* store.listDevices());
|
|
2282
2749
|
}
|
|
@@ -2314,9 +2781,13 @@ Content-Length: 0\r
|
|
|
2314
2781
|
return json(forgotten);
|
|
2315
2782
|
}
|
|
2316
2783
|
if (url.pathname === "/api/tasks" && request.method === "POST") {
|
|
2317
|
-
const
|
|
2784
|
+
const requestedInput = yield* body(request, decodeCreateTaskInput);
|
|
2785
|
+
const existing = yield* store.findDelegation(requestedInput, principal);
|
|
2786
|
+
if (existing !== undefined)
|
|
2787
|
+
return json(existing);
|
|
2788
|
+
const { input, targetDeviceId: target } = yield* resolveDelegation(requestedInput, sourceDeviceId);
|
|
2318
2789
|
const providerSessionId = input.threadId === undefined || input.provider === "grok-bot" ? undefined : yield* store.sessionFor(input.threadId, target, input.provider ?? "codex");
|
|
2319
|
-
const task2 = yield* store.createDelegation(input, target, principal, providerSessionId);
|
|
2790
|
+
const task2 = yield* store.createDelegation(input, target, principal, providerSessionId, requestedInput);
|
|
2320
2791
|
return json(yield* Effect15.tryPromise(() => dispatch(task2)), 201);
|
|
2321
2792
|
}
|
|
2322
2793
|
if (url.pathname === "/api/inbox" && request.method === "GET") {
|
|
@@ -2386,11 +2857,35 @@ Content-Length: 0\r
|
|
|
2386
2857
|
return json(yield* store.reportTaskProgress(id, deviceId, input.note));
|
|
2387
2858
|
}
|
|
2388
2859
|
const cancel2 = url.pathname.match(/^\/api\/tasks\/([^/]+)\/cancel$/);
|
|
2860
|
+
const inputRequest = url.pathname.match(/^\/api\/tasks\/([^/]+)\/input$/);
|
|
2861
|
+
if (request.method === "POST" && inputRequest?.[1] !== undefined) {
|
|
2862
|
+
const id = yield* pathId(TaskId, inputRequest[1]);
|
|
2863
|
+
const input = yield* body(request, decodeRequestTaskInput);
|
|
2864
|
+
const task2 = yield* store.getTask(id);
|
|
2865
|
+
const deviceId = principal === "owner" ? task2.targetDeviceId : principal.deviceId;
|
|
2866
|
+
if (deviceId === undefined)
|
|
2867
|
+
return yield* new RequestError({
|
|
2868
|
+
status: 403,
|
|
2869
|
+
message: "Only the target device can request input"
|
|
2870
|
+
});
|
|
2871
|
+
return json(yield* store.requestTaskInput(id, deviceId, input));
|
|
2872
|
+
}
|
|
2873
|
+
const answer = url.pathname.match(/^\/api\/tasks\/([^/]+)\/answer$/);
|
|
2874
|
+
if (request.method === "POST" && answer?.[1] !== undefined) {
|
|
2875
|
+
const task2 = yield* store.answerTaskInput(yield* pathId(TaskId, answer[1]), principal, yield* body(request, decodeAnswerTaskInput));
|
|
2876
|
+
return json(yield* Effect15.tryPromise({
|
|
2877
|
+
try: () => dispatch(task2),
|
|
2878
|
+
catch: (cause) => new RequestError({
|
|
2879
|
+
status: 500,
|
|
2880
|
+
message: cause instanceof Error ? cause.message : String(cause)
|
|
2881
|
+
})
|
|
2882
|
+
}));
|
|
2883
|
+
}
|
|
2389
2884
|
if (request.method === "POST" && cancel2?.[1] !== undefined) {
|
|
2390
2885
|
const id = yield* pathId(TaskId, cancel2[1]);
|
|
2391
2886
|
const updated = yield* store.requestCancellation(id);
|
|
2392
2887
|
if (updated.status === "cancelling") {
|
|
2393
|
-
|
|
2888
|
+
sendCancellation(updated);
|
|
2394
2889
|
}
|
|
2395
2890
|
return json(updated);
|
|
2396
2891
|
}
|
|
@@ -2399,7 +2894,7 @@ Content-Length: 0\r
|
|
|
2399
2894
|
return json(yield* store.threadContext(yield* pathId(ThreadId, thread[1])));
|
|
2400
2895
|
}
|
|
2401
2896
|
return yield* new RequestError({ status: 404, message: "Route not found" });
|
|
2402
|
-
}).pipe(Effect15.mapError((cause) => cause instanceof RequestError ? cause : cause._tag === "RelayStore.TaskProgressError" ? new RequestError({ status: cause.status, message: cause.message }) : new RequestError({
|
|
2897
|
+
}).pipe(Effect15.mapError((cause) => cause instanceof RequestError ? cause : cause._tag === "RelayStore.TaskProgressError" || cause._tag === "RelayStore.TaskInputError" || cause._tag === "RelayStore.TaskDeadlineError" || cause._tag === "RelayStore.TaskRequestError" ? new RequestError({ status: cause.status, message: cause.message }) : new RequestError({
|
|
2403
2898
|
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,
|
|
2404
2899
|
message: cause.message
|
|
2405
2900
|
})));
|
|
@@ -2507,26 +3002,40 @@ Content-Length: 0\r
|
|
|
2507
3002
|
websocketServer.emit("connection", websocket, request);
|
|
2508
3003
|
});
|
|
2509
3004
|
});
|
|
2510
|
-
await new Promise((
|
|
3005
|
+
await new Promise((resolve6, reject) => {
|
|
2511
3006
|
server.once("error", reject);
|
|
2512
3007
|
server.listen(relayListenOptions(configuration), () => {
|
|
2513
3008
|
server.off("error", reject);
|
|
2514
|
-
|
|
3009
|
+
resolve6();
|
|
2515
3010
|
});
|
|
2516
3011
|
});
|
|
2517
3012
|
const address = server.address();
|
|
2518
3013
|
const listener = typeof address === "string" ? address : address?.family === "IPv6" ? `[${address.address}]:${address.port}` : `${address?.address ?? configuration.host}:${address?.port ?? configuration.port}`;
|
|
2519
3014
|
console.log(`Cohall relay listening on http://${listener}`);
|
|
3015
|
+
const deadlineController = new AbortController;
|
|
3016
|
+
const enforceDeadlines = Effect15.gen(function* () {
|
|
3017
|
+
const store = yield* exports_store.Service;
|
|
3018
|
+
const overdue = yield* store.expireTasks();
|
|
3019
|
+
for (const task of overdue)
|
|
3020
|
+
if (task.status === "cancelling")
|
|
3021
|
+
sendCancellation(task);
|
|
3022
|
+
}).pipe(Effect15.catch((cause) => Effect15.logError(cause)), Effect15.repeat({ schedule: Schedule4.spaced("1 second") }));
|
|
3023
|
+
const deadlineWork = runtime.runPromise(enforceDeadlines, { signal: deadlineController.signal }).catch((cause) => {
|
|
3024
|
+
if (!deadlineController.signal.aborted)
|
|
3025
|
+
console.error(`Task deadline enforcement failed: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
3026
|
+
});
|
|
2520
3027
|
let stopping = false;
|
|
2521
3028
|
const shutdown = async () => {
|
|
2522
3029
|
if (stopping) {
|
|
2523
3030
|
return;
|
|
2524
3031
|
}
|
|
2525
3032
|
stopping = true;
|
|
3033
|
+
deadlineController.abort();
|
|
3034
|
+
await deadlineWork;
|
|
2526
3035
|
for (const socket of websocketServer.clients) {
|
|
2527
3036
|
socket.close(1001, "Relay shutting down");
|
|
2528
3037
|
}
|
|
2529
|
-
await new Promise((
|
|
3038
|
+
await new Promise((resolve6) => server.close(() => resolve6()));
|
|
2530
3039
|
websocketServer.close();
|
|
2531
3040
|
await runtime.dispose();
|
|
2532
3041
|
};
|
|
@@ -2578,7 +3087,14 @@ class RelayDecodeError extends Schema2.TaggedErrorClass()("RelayClient.DecodeErr
|
|
|
2578
3087
|
class Service extends Context.Service()("@cohall/RelayClient") {
|
|
2579
3088
|
}
|
|
2580
3089
|
var normalizeBaseUrl = (url) => url.replace(/\/+$/, "");
|
|
2581
|
-
var
|
|
3090
|
+
var compareDevices = (left, right) => {
|
|
3091
|
+
const fold = (name) => name.replace(/[A-Z]/g, (letter) => letter.toLowerCase());
|
|
3092
|
+
const byName = Buffer.compare(Buffer.from(fold(left.name)), Buffer.from(fold(right.name)));
|
|
3093
|
+
return byName || (left.id < right.id ? -1 : left.id > right.id ? 1 : 0);
|
|
3094
|
+
};
|
|
3095
|
+
var maxJsonResponseBytes = 2 * 1024 * 1024;
|
|
3096
|
+
var maxTraceResponseBytes = 4 * 1024 * 1024;
|
|
3097
|
+
var jsonBody = async (response, operation, maxBytes = maxJsonResponseBytes) => {
|
|
2582
3098
|
if (response.body === null) {
|
|
2583
3099
|
throw new RelayDecodeError({ operation, message: "Relay returned an empty response" });
|
|
2584
3100
|
}
|
|
@@ -2591,9 +3107,12 @@ var jsonBody = async (response, operation) => {
|
|
|
2591
3107
|
break;
|
|
2592
3108
|
}
|
|
2593
3109
|
size += chunk.value.byteLength;
|
|
2594
|
-
if (size >
|
|
3110
|
+
if (size > maxBytes) {
|
|
2595
3111
|
await reader.cancel();
|
|
2596
|
-
throw new RelayDecodeError({
|
|
3112
|
+
throw new RelayDecodeError({
|
|
3113
|
+
operation,
|
|
3114
|
+
message: `Relay response exceeded ${maxBytes / (1024 * 1024)} MiB`
|
|
3115
|
+
});
|
|
2597
3116
|
}
|
|
2598
3117
|
chunks.push(chunk.value);
|
|
2599
3118
|
}
|
|
@@ -2622,42 +3141,80 @@ var responseMessage = async (response, operation) => {
|
|
|
2622
3141
|
};
|
|
2623
3142
|
var make = (options) => {
|
|
2624
3143
|
const baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
2625
|
-
const request = (operation, path, schema, init) => Effect2.gen(function* () {
|
|
2626
|
-
const
|
|
2627
|
-
try: (signal) =>
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
3144
|
+
const request = (operation, path, schema, init, maxResponseBytes = maxJsonResponseBytes) => Effect2.gen(function* () {
|
|
3145
|
+
const json = yield* Effect2.tryPromise({
|
|
3146
|
+
try: async (signal) => {
|
|
3147
|
+
const response = await fetch(`${baseUrl}${path}`, {
|
|
3148
|
+
...init,
|
|
3149
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(30000)]),
|
|
3150
|
+
headers: {
|
|
3151
|
+
authorization: `Bearer ${options.token}`,
|
|
3152
|
+
"content-type": "application/json",
|
|
3153
|
+
...init?.headers
|
|
3154
|
+
}
|
|
3155
|
+
});
|
|
3156
|
+
if (!response.ok) {
|
|
3157
|
+
throw new RelayRequestError({
|
|
3158
|
+
operation,
|
|
3159
|
+
message: await responseMessage(response, operation),
|
|
3160
|
+
status: response.status
|
|
3161
|
+
});
|
|
2634
3162
|
}
|
|
2635
|
-
|
|
2636
|
-
|
|
3163
|
+
return jsonBody(response, operation, maxResponseBytes).catch((cause) => {
|
|
3164
|
+
throw cause instanceof RelayDecodeError ? cause : new RelayDecodeError({ operation, message: String(cause) });
|
|
3165
|
+
});
|
|
3166
|
+
},
|
|
3167
|
+
catch: (cause) => cause instanceof RelayRequestError || cause instanceof RelayDecodeError ? cause : new RelayRequestError({
|
|
2637
3168
|
operation,
|
|
2638
3169
|
message: cause instanceof Error ? cause.message : String(cause)
|
|
2639
3170
|
})
|
|
2640
3171
|
});
|
|
2641
|
-
if (!response.ok) {
|
|
2642
|
-
return yield* Effect2.fail(new RelayRequestError({
|
|
2643
|
-
operation,
|
|
2644
|
-
message: yield* Effect2.promise(() => responseMessage(response, operation)),
|
|
2645
|
-
status: response.status
|
|
2646
|
-
}));
|
|
2647
|
-
}
|
|
2648
|
-
const json = yield* Effect2.tryPromise({
|
|
2649
|
-
try: () => jsonBody(response, operation),
|
|
2650
|
-
catch: (cause) => cause instanceof RelayDecodeError ? cause : new RelayDecodeError({ operation, message: String(cause) })
|
|
2651
|
-
});
|
|
2652
3172
|
return yield* Schema2.decodeUnknownEffect(schema)(json).pipe(Effect2.mapError((cause) => new RelayDecodeError({ operation, message: String(cause) })));
|
|
2653
3173
|
});
|
|
3174
|
+
const devicePage = (after) => request("RelayClient.devices", `/api/devices/page${after === undefined ? "" : `?after=${encodeURIComponent(after)}`}`, DevicePage, undefined, maxDevicePageResponseBytes);
|
|
2654
3175
|
return Service.of({
|
|
2655
|
-
devices: () =>
|
|
3176
|
+
devices: () => Effect2.gen(function* () {
|
|
3177
|
+
const first = yield* devicePage().pipe(Effect2.catch((cause) => cause instanceof RelayRequestError && cause.status === 404 ? request("RelayClient.devices", "/api/devices", Schema2.Array(Device)) : Effect2.fail(cause)));
|
|
3178
|
+
if (!("devices" in first))
|
|
3179
|
+
return first;
|
|
3180
|
+
const devices = [...first.devices];
|
|
3181
|
+
let cursor = first.nextCursor;
|
|
3182
|
+
while (cursor !== undefined) {
|
|
3183
|
+
const page = yield* devicePage(cursor);
|
|
3184
|
+
const next = page.devices[0];
|
|
3185
|
+
if (next === undefined)
|
|
3186
|
+
break;
|
|
3187
|
+
if (next.id <= cursor)
|
|
3188
|
+
return yield* new RelayDecodeError({
|
|
3189
|
+
operation: "RelayClient.devices",
|
|
3190
|
+
message: "Relay device page did not advance"
|
|
3191
|
+
});
|
|
3192
|
+
devices.push(...page.devices);
|
|
3193
|
+
cursor = page.nextCursor;
|
|
3194
|
+
}
|
|
3195
|
+
return devices.sort(compareDevices);
|
|
3196
|
+
}),
|
|
2656
3197
|
forgetDevice: (deviceId) => request("RelayClient.forgetDevice", `/api/devices/${encodeURIComponent(deviceId)}/forget`, Device, { method: "POST" }),
|
|
2657
3198
|
createTask: (input) => Effect2.gen(function* () {
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
3199
|
+
const hasAttachments = (input.attachments?.length ?? 0) > 0;
|
|
3200
|
+
if (input.requestId !== undefined || input.expiresAt !== undefined || hasAttachments) {
|
|
3201
|
+
const health = yield* request("RelayClient.taskSupport", "/api/health", Schema2.Struct({
|
|
3202
|
+
taskIdempotency: Schema2.optionalKey(Schema2.Boolean),
|
|
3203
|
+
taskDeadlines: Schema2.optionalKey(Schema2.Boolean),
|
|
3204
|
+
taskAttachments: Schema2.optionalKey(Schema2.Boolean)
|
|
3205
|
+
}));
|
|
3206
|
+
if (input.requestId !== undefined && health.taskIdempotency !== true) {
|
|
3207
|
+
return yield* new RelayRequestError({
|
|
3208
|
+
operation: "RelayClient.createTask",
|
|
3209
|
+
message: "Upgrade the Cohall relay before using task request IDs"
|
|
3210
|
+
});
|
|
3211
|
+
}
|
|
3212
|
+
if (input.expiresAt !== undefined && health.taskDeadlines !== true)
|
|
3213
|
+
return yield* new RelayRequestError({
|
|
3214
|
+
operation: "RelayClient.createTask",
|
|
3215
|
+
message: "Upgrade the Cohall relay before using task deadlines"
|
|
3216
|
+
});
|
|
3217
|
+
if (hasAttachments && health.taskAttachments !== true) {
|
|
2661
3218
|
return yield* new RelayRequestError({
|
|
2662
3219
|
operation: "RelayClient.createTask",
|
|
2663
3220
|
message: "Upgrade the Cohall relay before sending file attachments"
|
|
@@ -2669,6 +3226,16 @@ var make = (options) => {
|
|
|
2669
3226
|
body: JSON.stringify(input)
|
|
2670
3227
|
});
|
|
2671
3228
|
}),
|
|
3229
|
+
requestTaskInput: (taskId, input) => Effect2.gen(function* () {
|
|
3230
|
+
const health = yield* request("RelayClient.clarificationSupport", "/api/health", Schema2.Struct({ taskClarification: Schema2.optionalKey(Schema2.Boolean) }));
|
|
3231
|
+
if (health.taskClarification !== true)
|
|
3232
|
+
return yield* new RelayRequestError({
|
|
3233
|
+
operation: "RelayClient.requestTaskInput",
|
|
3234
|
+
message: "Upgrade the Cohall relay before requesting clarification"
|
|
3235
|
+
});
|
|
3236
|
+
return yield* request("RelayClient.requestTaskInput", `/api/tasks/${encodeURIComponent(taskId)}/input`, TaskClarification, { method: "POST", body: JSON.stringify(input) });
|
|
3237
|
+
}),
|
|
3238
|
+
answerTaskInput: (taskId, input) => request("RelayClient.answerTaskInput", `/api/tasks/${encodeURIComponent(taskId)}/answer`, Task, { method: "POST", body: JSON.stringify(input) }),
|
|
2672
3239
|
getTask: (taskId) => request("RelayClient.getTask", `/api/tasks/${encodeURIComponent(taskId)}`, Task),
|
|
2673
3240
|
reportTaskProgress: (taskId, input) => Effect2.gen(function* () {
|
|
2674
3241
|
const health = yield* request("RelayClient.progressSupport", "/api/health", Schema2.Struct({ taskProgress: Schema2.optionalKey(Schema2.Boolean) }));
|
|
@@ -2730,7 +3297,7 @@ var make = (options) => {
|
|
|
2730
3297
|
}),
|
|
2731
3298
|
inbox: () => request("RelayClient.inbox", "/api/inbox", TaskInbox),
|
|
2732
3299
|
acknowledgeCompletion: (taskId) => request("RelayClient.acknowledgeCompletion", `/api/inbox/${encodeURIComponent(taskId)}/ack`, TaskInboxItem, { method: "POST" }),
|
|
2733
|
-
traceTask: (taskId) => request("RelayClient.traceTask", `/api/tasks/${encodeURIComponent(taskId)}/trace`, TaskTrace),
|
|
3300
|
+
traceTask: (taskId) => request("RelayClient.traceTask", `/api/tasks/${encodeURIComponent(taskId)}/trace`, TaskTrace, undefined, maxTraceResponseBytes),
|
|
2734
3301
|
cancelTask: (taskId) => request("RelayClient.cancelTask", `/api/tasks/${encodeURIComponent(taskId)}/cancel`, Task, {
|
|
2735
3302
|
method: "POST"
|
|
2736
3303
|
}),
|
|
@@ -2973,7 +3540,7 @@ var prepareCommand = async (options) => {
|
|
|
2973
3540
|
}
|
|
2974
3541
|
}
|
|
2975
3542
|
};
|
|
2976
|
-
var forEachJsonEvent = async (stdout, onEvent) => {
|
|
3543
|
+
var forEachJsonEvent = async (stdout, onEvent, onOversized) => {
|
|
2977
3544
|
const limit = 1024 * 1024;
|
|
2978
3545
|
let chunks = [];
|
|
2979
3546
|
let size = 0;
|
|
@@ -2996,6 +3563,7 @@ var forEachJsonEvent = async (stdout, onEvent) => {
|
|
|
2996
3563
|
const emit = () => {
|
|
2997
3564
|
if (oversized) {
|
|
2998
3565
|
oversized = false;
|
|
3566
|
+
onOversized();
|
|
2999
3567
|
return;
|
|
3000
3568
|
}
|
|
3001
3569
|
const line = Buffer.concat(chunks, size).toString("utf8");
|
|
@@ -3027,6 +3595,7 @@ var forEachJsonEvent = async (stdout, onEvent) => {
|
|
|
3027
3595
|
var parseCodex = async (stdout, existingSession) => {
|
|
3028
3596
|
let result = "";
|
|
3029
3597
|
let sessionId = existingSession;
|
|
3598
|
+
let discardedEvent = false;
|
|
3030
3599
|
await forEachJsonEvent(stdout, (event) => {
|
|
3031
3600
|
if (text(event.type) === "thread.started") {
|
|
3032
3601
|
sessionId = text(event.thread_id) ?? sessionId;
|
|
@@ -3036,9 +3605,17 @@ var parseCodex = async (stdout, existingSession) => {
|
|
|
3036
3605
|
}
|
|
3037
3606
|
const item = record(event.item);
|
|
3038
3607
|
if (text(item?.type) === "agent_message") {
|
|
3039
|
-
|
|
3608
|
+
const content = text(item?.text) ?? text(item?.content);
|
|
3609
|
+
result = content ?? result;
|
|
3610
|
+
if (content !== undefined && content.length > 0)
|
|
3611
|
+
discardedEvent = false;
|
|
3040
3612
|
}
|
|
3613
|
+
}, () => {
|
|
3614
|
+
discardedEvent = true;
|
|
3041
3615
|
});
|
|
3616
|
+
if (discardedEvent) {
|
|
3617
|
+
throw new Error("Provider JSON event exceeded 1 MiB without a later text response");
|
|
3618
|
+
}
|
|
3042
3619
|
return {
|
|
3043
3620
|
result: result || "Codex completed the task without a text response.",
|
|
3044
3621
|
...sessionId === undefined ? {} : { sessionId }
|
|
@@ -3075,17 +3652,26 @@ var openCodeError = (event) => {
|
|
|
3075
3652
|
var parseOpenCode = async (stdout, existingSession) => {
|
|
3076
3653
|
let result = "";
|
|
3077
3654
|
let sessionId = existingSession;
|
|
3655
|
+
let discardedEvent = false;
|
|
3078
3656
|
await forEachJsonEvent(stdout, (event) => {
|
|
3079
3657
|
const failure = openCodeError(event);
|
|
3080
3658
|
if (failure !== undefined) {
|
|
3081
3659
|
throw new Error(failure);
|
|
3082
3660
|
}
|
|
3083
3661
|
sessionId = text(event.sessionID) ?? text(event.sessionId) ?? text(event.session_id) ?? text(record(event.info)?.sessionID) ?? sessionId;
|
|
3662
|
+
if (text(event.type) !== "text")
|
|
3663
|
+
return;
|
|
3084
3664
|
const content = textFromPart(event.part) ?? textFromPart(event) ?? text(event.result);
|
|
3085
3665
|
if (content !== undefined && content.length > 0) {
|
|
3086
3666
|
result = content;
|
|
3667
|
+
discardedEvent = false;
|
|
3087
3668
|
}
|
|
3669
|
+
}, () => {
|
|
3670
|
+
discardedEvent = true;
|
|
3088
3671
|
});
|
|
3672
|
+
if (discardedEvent) {
|
|
3673
|
+
throw new Error("Provider JSON event exceeded 1 MiB without a later text response");
|
|
3674
|
+
}
|
|
3089
3675
|
if (result.length === 0) {
|
|
3090
3676
|
throw new Error("OpenCode produced no result; verify its authentication and use a supported project workspace");
|
|
3091
3677
|
}
|
|
@@ -3171,7 +3757,8 @@ var run = (options) => Effect3.callback((resume, signal) => {
|
|
|
3171
3757
|
...providerEnvironment(),
|
|
3172
3758
|
COHALL_PROVIDER: options.provider,
|
|
3173
3759
|
COHALL_THREAD_ID: options.threadId,
|
|
3174
|
-
...options.taskId === undefined ? {} : { COHALL_TASK_ID: options.taskId }
|
|
3760
|
+
...options.taskId === undefined ? {} : { COHALL_TASK_ID: options.taskId },
|
|
3761
|
+
...options.runId === undefined ? {} : { COHALL_TASK_RUN_ID: options.runId }
|
|
3175
3762
|
},
|
|
3176
3763
|
extendEnv: false,
|
|
3177
3764
|
detached: platform() !== "win32",
|
|
@@ -3263,7 +3850,7 @@ var run = (options) => Effect3.callback((resume, signal) => {
|
|
|
3263
3850
|
import { Effect as Effect10, Schema as Schema11 } from "effect";
|
|
3264
3851
|
import { access as access4, mkdir as mkdir7, readFile as readFile4, stat as stat4, writeFile as writeFile7 } from "node:fs/promises";
|
|
3265
3852
|
import { homedir as homedir4 } from "node:os";
|
|
3266
|
-
import { dirname as dirname6, join as join8, resolve as
|
|
3853
|
+
import { dirname as dirname6, join as join8, resolve as resolve4 } from "node:path";
|
|
3267
3854
|
|
|
3268
3855
|
// skills/cohall/SKILL.md
|
|
3269
3856
|
var SKILL_default = `---
|
|
@@ -3275,7 +3862,8 @@ description: Delegate through Cohall to another user-owned device or an existing
|
|
|
3275
3862
|
|
|
3276
3863
|
Cohall sends a focused task to a coding agent or an existing named Grok Bot. The target
|
|
3277
3864
|
keeps its local files, credentials, browser state, provider login, skills, and
|
|
3278
|
-
permissions. The relay carries prompts,
|
|
3865
|
+
permissions. The relay carries prompts, questions and answers, final results,
|
|
3866
|
+
task state, and explicitly attached files.
|
|
3279
3867
|
|
|
3280
3868
|
Use the installed \`cohall\` executable when it is available. Fall back to
|
|
3281
3869
|
\`npx -y @akshar5/cohall\` only when Cohall is not installed globally.
|
|
@@ -3332,9 +3920,19 @@ Do not delegate ordinary local work when the other device provides no advantage.
|
|
|
3332
3920
|
to use Codex on a computer; selecting a named bot infers \`grok-bot\`.
|
|
3333
3921
|
Omit \`--target\` only when Cohall may choose a matching computer.
|
|
3334
3922
|
|
|
3923
|
+
For a submission you may need to retry, generate and save a UUID v4 before
|
|
3924
|
+
sending, then pass \`--request-id <uuid>\` or MCP \`delegate.request_id\`. Use a
|
|
3925
|
+
device ID or \`@device-id/bot-id\` from discovery. After a lost response,
|
|
3926
|
+
repeat the same input and ID with the same client credential to recover
|
|
3927
|
+
the original task. Changed input returns 409; pruned task history returns
|
|
3928
|
+
410 and the ID remains consumed. Check the original work before using a
|
|
3929
|
+
new ID. With request IDs, specify a target or provider for thread follow-ups,
|
|
3930
|
+
and explicitly select a Bot for Bot follow-ups.
|
|
3931
|
+
|
|
3335
3932
|
5. The command waits by default and returns JSON. Treat work as successful only
|
|
3336
3933
|
when \`status\` is \`completed\`; use \`result\` in the current task. Report a
|
|
3337
|
-
\`failed\`, \`cancelled\`, or \`cancelling\` state accurately.
|
|
3934
|
+
\`failed\`, \`cancelled\`, or \`cancelling\` state accurately. Handle \`needs_input\`
|
|
3935
|
+
through the clarification workflow below.
|
|
3338
3936
|
|
|
3339
3937
|
Pass an explicit local file with \`--attach path\` when the coding task needs
|
|
3340
3938
|
it. The MCP \`delegate\` tool takes \`attachment_paths\`. A task accepts up to
|
|
@@ -3361,9 +3959,32 @@ and \`COHALL_TASK_ID\` carry its thread and parent task. Pass \`--thread\` and
|
|
|
3361
3959
|
\`--parent\` explicitly from a Grok Bot using the IDs in its handoff.
|
|
3362
3960
|
|
|
3363
3961
|
For work sent with \`--no-wait\`, check \`cohall inbox\` when returning to the task.
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3962
|
+
Answer pending questions using the workflow below. Read completed results with
|
|
3963
|
+
\`cohall status <task-id>\`, use them in the current work, then run
|
|
3964
|
+
\`cohall inbox ack <task-id>\`. Results that arrive after a wait times out appear
|
|
3965
|
+
in the same inbox.
|
|
3966
|
+
|
|
3967
|
+
## Clarification
|
|
3968
|
+
|
|
3969
|
+
When \`delegate\`, \`send\`, \`status\`, or \`wait\` returns \`needs_input\`, read
|
|
3970
|
+
\`input_request.question\` and \`input_request.id\`. Inbox entries use \`inputRequest\`.
|
|
3971
|
+
Answer from known conversation facts; ask the user if the answer is missing.
|
|
3972
|
+
Resume the existing task with the original requester credential:
|
|
3973
|
+
|
|
3974
|
+
\`\`\`bash
|
|
3975
|
+
cohall answer <task-id> --request-id <question-id> --message 'Use main.'
|
|
3976
|
+
cohall wait <task-id> --timeout 1800
|
|
3977
|
+
\`\`\`
|
|
3978
|
+
|
|
3979
|
+
MCP uses \`task_answer\` with \`task_id\`, \`request_id\`, and \`answer\`, then \`wait_task\`.
|
|
3980
|
+
Only a terminal result completes the handoff. Questions require an answer or
|
|
3981
|
+
cancellation, not an inbox acknowledgement. A stale question ID is rejected;
|
|
3982
|
+
read status again before answering.
|
|
3983
|
+
|
|
3984
|
+
When receiving a delegated coding task and essential information is missing,
|
|
3985
|
+
run \`cohall request-input --question 'Your question'\` or \`task_request_input\`.
|
|
3986
|
+
The task and turn IDs are inherited. After the request succeeds, end the worker
|
|
3987
|
+
turn immediately; Cohall resumes the same task after the sender answers.
|
|
3367
3988
|
|
|
3368
3989
|
## Message named bots
|
|
3369
3990
|
|
|
@@ -3393,11 +4014,13 @@ Different bots and the computer's coding agent can run concurrently. Each
|
|
|
3393
4014
|
individual bot processes one Cohall request at a time.
|
|
3394
4015
|
|
|
3395
4016
|
When receiving a Cohall request inside a Grok Bot, finish with the \`cohall reply\`
|
|
3396
|
-
command supplied in the handoff
|
|
3397
|
-
or \`--message -\` on stdin, then reply normally in chat. Use \`--error\` if unable
|
|
4017
|
+
command supplied in the handoff, including its \`--run-id\`. Pass the final answer
|
|
4018
|
+
through \`--message-file\` or \`--message -\` on stdin, then reply normally in chat. Use \`--error\` if unable
|
|
3398
4019
|
to finish. This local receipt is what returns the result to the sending device;
|
|
3399
4020
|
a chat message alone does not complete the Cohall task. The receipt survives
|
|
3400
|
-
worker restarts and works while the relay is temporarily unreachable.
|
|
4021
|
+
worker restarts and works while the relay is temporarily unreachable. For
|
|
4022
|
+
essential missing information, use that callback with \`--question\`, then end
|
|
4023
|
+
the Bot turn. Its resumed handoff supplies a new run ID and the sender's answer.
|
|
3401
4024
|
|
|
3402
4025
|
## Context and safety
|
|
3403
4026
|
|
|
@@ -3405,7 +4028,7 @@ worker restarts and works while the relay is temporarily unreachable.
|
|
|
3405
4028
|
- Add new relevant developments to \`--context\` when following up from another chat.
|
|
3406
4029
|
- Never send provider credentials, Cohall tokens, cookies, or browser-profile data.
|
|
3407
4030
|
- Request a path only when the target advertises a matching workspace root.
|
|
3408
|
-
-
|
|
4031
|
+
- Resume a clarification on its existing task instead of creating duplicate work.
|
|
3409
4032
|
- Do not submit the same work through both CLI and MCP.
|
|
3410
4033
|
- Respect user confirmation requirements for consequential actions on the target.
|
|
3411
4034
|
|
|
@@ -3426,16 +4049,25 @@ The relay itself must be reachable to accept a new task. Interrupted execution
|
|
|
3426
4049
|
uses at-least-once delivery and may run again, so make consequential prompts safe
|
|
3427
4050
|
to retry.
|
|
3428
4051
|
|
|
3429
|
-
The timeout error includes the task ID and last known status.
|
|
3430
|
-
|
|
4052
|
+
The timeout error includes the task ID and last known status. A wait timeout
|
|
4053
|
+
leaves the task running. When the user requires coding work to stop by a fixed
|
|
4054
|
+
time, pass \`--deadline <future-UTC-ISO-timestamp>\` to \`delegate\`, or \`deadline\`
|
|
4055
|
+
to MCP. The task's deadline includes queueing and clarification and remains
|
|
4056
|
+
unchanged on resume. Upgrade the requester, relay, and worker first. Named bots
|
|
4057
|
+
cannot use deadlines. An expired task fails with \`Task deadline exceeded\`.
|
|
4058
|
+
Tasks awaiting their first dispatch fail on the relay. Tasks that may have
|
|
4059
|
+
reached a worker stay \`cancelling\` until it acknowledges termination, including
|
|
4060
|
+
while it is offline. Without a deadline, stop work explicitly:
|
|
3431
4061
|
|
|
3432
4062
|
\`\`\`bash
|
|
3433
4063
|
cohall cancel <task-id>
|
|
3434
4064
|
\`\`\`
|
|
3435
4065
|
|
|
3436
|
-
For coding agents,
|
|
3437
|
-
|
|
3438
|
-
|
|
4066
|
+
For coding agents, cancellation after possible dispatch stays \`cancelling\` until
|
|
4067
|
+
the target confirms termination, including paused tasks and tasks requeued after
|
|
4068
|
+
a disconnect. Tasks awaiting their first dispatch cancel immediately. Paused
|
|
4069
|
+
bot tasks and bot tasks awaiting their first dispatch can be cancelled.
|
|
4070
|
+
Stop an active bot in Grok Bot; its gateway
|
|
3439
4071
|
cannot safely cancel an individual Cohall request.
|
|
3440
4072
|
|
|
3441
4073
|
## Read shared context
|
|
@@ -3469,9 +4101,10 @@ Trace a known task before inspecting machine-local service logs:
|
|
|
3469
4101
|
cohall trace <task-id> --follow
|
|
3470
4102
|
\`\`\`
|
|
3471
4103
|
|
|
3472
|
-
The trace
|
|
3473
|
-
and terminal state.
|
|
3474
|
-
|
|
4104
|
+
The trace reports relay dispatch, device execution, retries, progress, questions,
|
|
4105
|
+
and terminal state. It omits prompts, final results, credentials, and provider
|
|
4106
|
+
session IDs; inspect progress and clarification text before sharing it.
|
|
4107
|
+
Use \`cohall thread <thread-id>\` when prompt and result history is relevant.
|
|
3475
4108
|
|
|
3476
4109
|
If Cohall cannot connect or no target is available, run:
|
|
3477
4110
|
|
|
@@ -3509,7 +4142,8 @@ var ClientConfiguration = Schema4.Struct({
|
|
|
3509
4142
|
relayUrl: Schema4.NonEmptyString,
|
|
3510
4143
|
token: Schema4.NonEmptyString,
|
|
3511
4144
|
mcpThreadId: Schema4.optionalKey(Schema4.String),
|
|
3512
|
-
mcpTaskId: Schema4.optionalKey(Schema4.String)
|
|
4145
|
+
mcpTaskId: Schema4.optionalKey(Schema4.String),
|
|
4146
|
+
mcpTaskRunId: Schema4.optionalKey(TaskRunId)
|
|
3513
4147
|
});
|
|
3514
4148
|
var DeviceConfiguration = Schema4.Struct({
|
|
3515
4149
|
relayUrl: Schema4.NonEmptyString,
|
|
@@ -3672,7 +4306,8 @@ var loadClientConfiguration = Effect4.tryPromise({
|
|
|
3672
4306
|
relayUrl,
|
|
3673
4307
|
token,
|
|
3674
4308
|
...process.env.COHALL_THREAD_ID === undefined ? {} : { mcpThreadId: process.env.COHALL_THREAD_ID },
|
|
3675
|
-
...process.env.COHALL_TASK_ID === undefined ? {} : { mcpTaskId: process.env.COHALL_TASK_ID }
|
|
4309
|
+
...process.env.COHALL_TASK_ID === undefined ? {} : { mcpTaskId: process.env.COHALL_TASK_ID },
|
|
4310
|
+
...process.env.COHALL_TASK_RUN_ID === undefined ? {} : { mcpTaskRunId: Schema4.decodeUnknownSync(TaskRunId)(process.env.COHALL_TASK_RUN_ID) }
|
|
3676
4311
|
});
|
|
3677
4312
|
},
|
|
3678
4313
|
catch: (cause) => cause instanceof Error ? cause : new Error(String(cause))
|
|
@@ -3736,7 +4371,7 @@ import { Effect as Effect5, Schedule, Schema as Schema5 } from "effect";
|
|
|
3736
4371
|
class DeviceSelectionError extends Schema5.TaggedErrorClass()("Cohall.DeviceSelectionError", { message: Schema5.String, devices: Schema5.Array(Schema5.String) }) {
|
|
3737
4372
|
}
|
|
3738
4373
|
|
|
3739
|
-
class TaskWaitTimeoutError extends Schema5.TaggedErrorClass()("Cohall.TaskWaitTimeoutError", { message: Schema5.String, taskId: TaskId, status: TaskStatus }) {
|
|
4374
|
+
class TaskWaitTimeoutError extends Schema5.TaggedErrorClass()("Cohall.TaskWaitTimeoutError", { message: Schema5.String, taskId: TaskId, status: Schema5.optionalKey(TaskStatus) }) {
|
|
3740
4375
|
}
|
|
3741
4376
|
var TaskResult = Schema5.Struct({
|
|
3742
4377
|
task_id: TaskId,
|
|
@@ -3748,7 +4383,9 @@ var TaskResult = Schema5.Struct({
|
|
|
3748
4383
|
result: Schema5.optionalKey(Schema5.String),
|
|
3749
4384
|
error: Schema5.optionalKey(Schema5.String),
|
|
3750
4385
|
inbox_warning: Schema5.optionalKey(Schema5.String),
|
|
3751
|
-
progress: Schema5.optionalKey(TaskProgress)
|
|
4386
|
+
progress: Schema5.optionalKey(TaskProgress),
|
|
4387
|
+
input_request: Schema5.optionalKey(TaskClarification),
|
|
4388
|
+
expires_at: Schema5.optionalKey(TaskDeadline)
|
|
3752
4389
|
});
|
|
3753
4390
|
var matchingDevices = (devices, target) => {
|
|
3754
4391
|
const value = target.replace(/^@/, "");
|
|
@@ -3792,22 +4429,33 @@ var selectTarget = Effect5.fn("Cohall.selectTarget")(function* (devices, target)
|
|
|
3792
4429
|
]
|
|
3793
4430
|
});
|
|
3794
4431
|
});
|
|
3795
|
-
var taskResult = (task) =>
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
}
|
|
3806
|
-
|
|
4432
|
+
var taskResult = (task) => {
|
|
4433
|
+
const question = task.clarifications?.at(-1);
|
|
4434
|
+
return TaskResult.make({
|
|
4435
|
+
task_id: task.id,
|
|
4436
|
+
thread_id: task.threadId,
|
|
4437
|
+
status: task.status,
|
|
4438
|
+
provider: task.provider,
|
|
4439
|
+
target_device_id: task.targetDeviceId,
|
|
4440
|
+
...task.botId === undefined ? {} : { bot_id: task.botId },
|
|
4441
|
+
...task.result === undefined ? {} : { result: task.result },
|
|
4442
|
+
...task.error === undefined ? {} : { error: task.error },
|
|
4443
|
+
...task.expiresAt === undefined ? {} : { expires_at: task.expiresAt },
|
|
4444
|
+
...task.progress === undefined ? {} : { progress: task.progress },
|
|
4445
|
+
...task.status === "needs_input" && question !== undefined ? { input_request: question } : {}
|
|
4446
|
+
});
|
|
4447
|
+
};
|
|
4448
|
+
var acknowledgedTaskResult = async (client, task, signal) => {
|
|
4449
|
+
signal?.throwIfAborted();
|
|
3807
4450
|
const result = taskResult(task);
|
|
3808
|
-
|
|
4451
|
+
if (!isTerminalTask(task))
|
|
4452
|
+
return result;
|
|
4453
|
+
const acknowledgementError = await Effect5.runPromise(client.acknowledgeCompletion(task.id), {
|
|
4454
|
+
signal
|
|
4455
|
+
}).then(() => {
|
|
3809
4456
|
return;
|
|
3810
4457
|
}).catch((cause) => {
|
|
4458
|
+
signal?.throwIfAborted();
|
|
3811
4459
|
if (cause instanceof RelayRequestError && cause.status === 404)
|
|
3812
4460
|
return;
|
|
3813
4461
|
return cause instanceof Error ? cause.message : String(cause);
|
|
@@ -3817,16 +4465,51 @@ var acknowledgedTaskResult = async (client, task) => {
|
|
|
3817
4465
|
inbox_warning: `Result received, but the inbox could not be cleared: ${acknowledgementError}`
|
|
3818
4466
|
});
|
|
3819
4467
|
};
|
|
4468
|
+
var requestInput = Effect5.fn("Cohall.requestInput")(function* (client, configuration, question, taskId, runId) {
|
|
4469
|
+
const id = taskId ?? (configuration.mcpTaskId === undefined ? undefined : yield* Schema5.decodeUnknownEffect(TaskId)(configuration.mcpTaskId));
|
|
4470
|
+
if (id === undefined)
|
|
4471
|
+
return yield* new RelayRequestError({
|
|
4472
|
+
operation: "Cohall.requestInput",
|
|
4473
|
+
message: "Task id is required outside delegated work"
|
|
4474
|
+
});
|
|
4475
|
+
const inheritedRun = id === configuration.mcpTaskId ? configuration.mcpTaskRunId : undefined;
|
|
4476
|
+
const turn = runId ?? inheritedRun ?? (yield* client.getTask(id)).runId;
|
|
4477
|
+
if (turn === undefined)
|
|
4478
|
+
return yield* new RelayRequestError({
|
|
4479
|
+
operation: "Cohall.requestInput",
|
|
4480
|
+
message: "Task has no current worker turn; upgrade the relay and worker"
|
|
4481
|
+
});
|
|
4482
|
+
return yield* client.requestTaskInput(id, { runId: turn, question });
|
|
4483
|
+
});
|
|
3820
4484
|
var createDelegation = Effect5.fn("Cohall.createDelegation")(function* (client, configuration, options) {
|
|
3821
|
-
|
|
3822
|
-
|
|
4485
|
+
if (options.requestId !== undefined && options.threadId !== undefined && options.target === undefined && options.provider === undefined && options.parentTaskId === undefined && configuration.mcpTaskId === undefined)
|
|
4486
|
+
return yield* new DeviceSelectionError({
|
|
4487
|
+
message: "With a request ID, select a target or provider explicitly for thread follow-ups",
|
|
4488
|
+
devices: []
|
|
4489
|
+
});
|
|
4490
|
+
const directTarget = (() => {
|
|
4491
|
+
if (options.requestId === undefined || options.target === undefined)
|
|
4492
|
+
return;
|
|
4493
|
+
const value = options.target.replace(/^@/, "");
|
|
4494
|
+
const separator = value.indexOf("/");
|
|
4495
|
+
const id = Schema5.decodeUnknownOption(DeviceId)(separator < 0 ? value : value.slice(0, separator));
|
|
4496
|
+
if (id._tag === "None")
|
|
4497
|
+
return;
|
|
4498
|
+
return {
|
|
4499
|
+
deviceId: id.value,
|
|
4500
|
+
botId: separator < 0 ? undefined : Schema5.decodeUnknownSync(BotId)(value.slice(separator + 1))
|
|
4501
|
+
};
|
|
4502
|
+
})();
|
|
4503
|
+
const needsDiscovery = directTarget === undefined && (options.requestId === undefined || options.target !== undefined);
|
|
4504
|
+
const devices = needsDiscovery ? yield* client.devices() : [];
|
|
4505
|
+
if (needsDiscovery && devices.length === 0) {
|
|
3823
4506
|
return yield* new DeviceSelectionError({
|
|
3824
4507
|
message: "No Cohall devices are registered",
|
|
3825
4508
|
devices: []
|
|
3826
4509
|
});
|
|
3827
4510
|
}
|
|
3828
|
-
let target = options.target === undefined ? undefined : yield* selectTarget(devices, options.target);
|
|
3829
|
-
if (target === undefined && options.threadId !== undefined && options.provider === undefined && options.parentTaskId === undefined && configuration.mcpTaskId === undefined) {
|
|
4511
|
+
let target = directTarget ?? (options.target === undefined ? undefined : yield* selectTarget(devices, options.target));
|
|
4512
|
+
if (target === undefined && options.requestId === undefined && options.threadId !== undefined && options.provider === undefined && options.parentTaskId === undefined && configuration.mcpTaskId === undefined) {
|
|
3830
4513
|
const context = yield* client.threadContext(options.threadId);
|
|
3831
4514
|
const previous = context.tasks.findLast((task) => task.parentTaskId === undefined);
|
|
3832
4515
|
if (previous === undefined && context.truncated) {
|
|
@@ -3857,6 +4540,7 @@ var createDelegation = Effect5.fn("Cohall.createDelegation")(function* (client,
|
|
|
3857
4540
|
const parentTaskId = options.parentTaskId ?? (configuration.mcpTaskId === undefined ? undefined : yield* Schema5.decodeUnknownEffect(TaskId)(configuration.mcpTaskId));
|
|
3858
4541
|
return yield* client.createTask({
|
|
3859
4542
|
prompt: options.prompt,
|
|
4543
|
+
...options.requestId === undefined ? {} : { requestId: options.requestId },
|
|
3860
4544
|
...target === undefined ? {} : { targetDeviceId: target.deviceId },
|
|
3861
4545
|
...botId === undefined ? {} : { botId },
|
|
3862
4546
|
...options.threadId !== undefined ? { threadId: options.threadId } : inheritedThread === undefined ? {} : { threadId: ThreadId.make(inheritedThread) },
|
|
@@ -3864,30 +4548,35 @@ var createDelegation = Effect5.fn("Cohall.createDelegation")(function* (client,
|
|
|
3864
4548
|
...options.workspace === undefined ? {} : { workspace: options.workspace },
|
|
3865
4549
|
...provider === undefined ? {} : { provider },
|
|
3866
4550
|
...parentTaskId === undefined ? {} : { parentTaskId },
|
|
3867
|
-
...options.attachments === undefined ? {} : { attachments: options.attachments }
|
|
4551
|
+
...options.attachments === undefined ? {} : { attachments: options.attachments },
|
|
4552
|
+
...options.expiresAt === undefined ? {} : { expiresAt: options.expiresAt }
|
|
3868
4553
|
});
|
|
3869
4554
|
});
|
|
3870
4555
|
var waitForTask = Effect5.fn("Cohall.waitForTask")(function* (client, initial, timeoutSeconds) {
|
|
3871
|
-
const
|
|
3872
|
-
let task = initial;
|
|
3873
|
-
|
|
3874
|
-
if (
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
});
|
|
4556
|
+
const id = typeof initial === "string" ? initial : initial.id;
|
|
4557
|
+
let task = typeof initial === "string" ? undefined : initial;
|
|
4558
|
+
return yield* Effect5.gen(function* () {
|
|
4559
|
+
if (task === undefined)
|
|
4560
|
+
task = yield* client.getTask(id);
|
|
4561
|
+
while (!isTerminalTask(task) && task.status !== "needs_input") {
|
|
4562
|
+
yield* Effect5.sleep("1 second");
|
|
4563
|
+
task = yield* client.getTask(id);
|
|
3880
4564
|
}
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
4565
|
+
return task;
|
|
4566
|
+
}).pipe(Effect5.timeoutOrElse({
|
|
4567
|
+
duration: timeoutSeconds * 1000,
|
|
4568
|
+
orElse: () => new TaskWaitTimeoutError({
|
|
4569
|
+
message: task === undefined ? `Task ${id} did not return its status after ${timeoutSeconds} seconds` : `Task ${id} is still ${task.status} after ${timeoutSeconds} seconds`,
|
|
4570
|
+
taskId: id,
|
|
4571
|
+
...task === undefined ? {} : { status: task.status }
|
|
4572
|
+
})
|
|
4573
|
+
}));
|
|
3885
4574
|
});
|
|
3886
4575
|
var followTaskTrace = Effect5.fn("Cohall.followTaskTrace")(function* (client, taskId, onTrace) {
|
|
3887
4576
|
let revision;
|
|
3888
4577
|
const poll = client.traceTask(taskId).pipe(Effect5.tap((trace) => {
|
|
3889
4578
|
const latest = trace.events.at(-1);
|
|
3890
|
-
const nextRevision = `${trace.status}:${latest?.kind ?? "none"}:${latest?.at ?? "none"}:${trace.progress?.at ?? ""}:${trace.progress?.note ?? ""}`;
|
|
4579
|
+
const nextRevision = `${trace.status}:${latest?.kind ?? "none"}:${latest?.at ?? "none"}:${trace.progress?.at ?? ""}:${trace.progress?.note ?? ""}:${JSON.stringify(trace.clarifications)}`;
|
|
3891
4580
|
if (nextRevision === revision) {
|
|
3892
4581
|
return Effect5.succeed(undefined);
|
|
3893
4582
|
}
|
|
@@ -3973,11 +4662,18 @@ var Dispatch = Schema6.Struct({ taskId: TaskId });
|
|
|
3973
4662
|
var Reply = Schema6.Union([
|
|
3974
4663
|
Schema6.Struct({
|
|
3975
4664
|
result: Schema6.String.check(Schema6.isMaxLength(131072)),
|
|
3976
|
-
error: Schema6.optionalKey(Schema6.Never)
|
|
4665
|
+
error: Schema6.optionalKey(Schema6.Never),
|
|
4666
|
+
question: Schema6.optionalKey(Schema6.Never)
|
|
3977
4667
|
}),
|
|
3978
4668
|
Schema6.Struct({
|
|
3979
4669
|
error: Schema6.NonEmptyString.check(Schema6.isMaxLength(16384)),
|
|
3980
|
-
result: Schema6.optionalKey(Schema6.Never)
|
|
4670
|
+
result: Schema6.optionalKey(Schema6.Never),
|
|
4671
|
+
question: Schema6.optionalKey(Schema6.Never)
|
|
4672
|
+
}),
|
|
4673
|
+
Schema6.Struct({
|
|
4674
|
+
question: RequestTaskInput.fields.question,
|
|
4675
|
+
result: Schema6.optionalKey(Schema6.Never),
|
|
4676
|
+
error: Schema6.optionalKey(Schema6.Never)
|
|
3981
4677
|
})
|
|
3982
4678
|
]);
|
|
3983
4679
|
var Receipt = Schema6.Struct({
|
|
@@ -3987,14 +4683,15 @@ var Receipt = Schema6.Struct({
|
|
|
3987
4683
|
});
|
|
3988
4684
|
var isMissing = (cause) => cause instanceof Error && ("code" in cause) && cause.code === "ENOENT";
|
|
3989
4685
|
var alreadyExists = (cause) => cause instanceof Error && ("code" in cause) && cause.code === "EEXIST";
|
|
3990
|
-
var paths = (taskId) => {
|
|
4686
|
+
var paths = (taskId, runId) => {
|
|
3991
4687
|
const id = Schema6.decodeUnknownSync(TaskId)(taskId);
|
|
3992
4688
|
const directory = join3(dirname2(configurationPath()), "bot-replies");
|
|
4689
|
+
const key = runId === undefined ? id : `${id}.${Schema6.decodeUnknownSync(TaskRunId)(runId)}`;
|
|
3993
4690
|
return {
|
|
3994
4691
|
directory,
|
|
3995
|
-
pending: join3(directory, `${
|
|
3996
|
-
receipt: join3(directory, `${
|
|
3997
|
-
dispatch: join3(directory, `${
|
|
4692
|
+
pending: join3(directory, `${key}.pending.json`),
|
|
4693
|
+
receipt: join3(directory, `${key}.reply.json`),
|
|
4694
|
+
dispatch: join3(directory, `${key}.dispatch.json`)
|
|
3998
4695
|
};
|
|
3999
4696
|
};
|
|
4000
4697
|
var readJson = async (path, limit) => {
|
|
@@ -4023,8 +4720,8 @@ var readJson = async (path, limit) => {
|
|
|
4023
4720
|
await file.close();
|
|
4024
4721
|
}
|
|
4025
4722
|
};
|
|
4026
|
-
var pendingReply = async (taskId) => {
|
|
4027
|
-
const value = await readJson(paths(taskId).pending, 4096);
|
|
4723
|
+
var pendingReply = async (taskId, runId) => {
|
|
4724
|
+
const value = await readJson(paths(taskId, runId).pending, 4096);
|
|
4028
4725
|
if (value === undefined)
|
|
4029
4726
|
return;
|
|
4030
4727
|
const decoded = Schema6.decodeUnknownResult(Pending)(value);
|
|
@@ -4067,7 +4764,7 @@ var prepareBotReply = async (task) => {
|
|
|
4067
4764
|
if (task.provider !== "grok-bot" || task.botId === undefined) {
|
|
4068
4765
|
throw new Error("Only a named Grok Bot task can receive a bot reply");
|
|
4069
4766
|
}
|
|
4070
|
-
const files = paths(task.id);
|
|
4767
|
+
const files = paths(task.id, task.runId);
|
|
4071
4768
|
await mkdir2(files.directory, { recursive: true, mode: 448 });
|
|
4072
4769
|
await chmod3(files.directory, 448);
|
|
4073
4770
|
const record2 = Pending.make({
|
|
@@ -4077,7 +4774,7 @@ var prepareBotReply = async (task) => {
|
|
|
4077
4774
|
deadline: Date.now() + replyLifetimeMs
|
|
4078
4775
|
});
|
|
4079
4776
|
await publish(files.pending, record2);
|
|
4080
|
-
const pending = await pendingReply(task.id);
|
|
4777
|
+
const pending = await pendingReply(task.id, task.runId);
|
|
4081
4778
|
if (pending === undefined || pending.botId !== task.botId || pending.threadId !== task.threadId) {
|
|
4082
4779
|
throw new Error("Bot reply manifest does not match this task");
|
|
4083
4780
|
}
|
|
@@ -4094,19 +4791,19 @@ var prepareBotReply = async (task) => {
|
|
|
4094
4791
|
dispatched: dispatch !== undefined
|
|
4095
4792
|
};
|
|
4096
4793
|
};
|
|
4097
|
-
var claimBotDispatch = async (taskId) => {
|
|
4098
|
-
const pending = await pendingReply(taskId);
|
|
4794
|
+
var claimBotDispatch = async (taskId, runId) => {
|
|
4795
|
+
const pending = await pendingReply(taskId, runId);
|
|
4099
4796
|
if (pending === undefined)
|
|
4100
4797
|
throw new Error("No pending bot reply for this task");
|
|
4101
4798
|
if (Date.now() >= pending.deadline)
|
|
4102
4799
|
throw new Error("Bot reply deadline has expired");
|
|
4103
|
-
return publish(paths(taskId).dispatch, { taskId });
|
|
4800
|
+
return publish(paths(taskId, runId).dispatch, { taskId });
|
|
4104
4801
|
};
|
|
4105
|
-
var readBotReply = async (taskId) => {
|
|
4106
|
-
const pending = await pendingReply(taskId);
|
|
4802
|
+
var readBotReply = async (taskId, runId) => {
|
|
4803
|
+
const pending = await pendingReply(taskId, runId);
|
|
4107
4804
|
if (pending === undefined)
|
|
4108
4805
|
return;
|
|
4109
|
-
const value = await readJson(paths(taskId).receipt, 1024 * 1024);
|
|
4806
|
+
const value = await readJson(paths(taskId, runId).receipt, 1024 * 1024);
|
|
4110
4807
|
if (value === undefined)
|
|
4111
4808
|
return;
|
|
4112
4809
|
const decoded = Schema6.decodeUnknownResult(Receipt)(value);
|
|
@@ -4118,15 +4815,15 @@ var readBotReply = async (taskId) => {
|
|
|
4118
4815
|
}
|
|
4119
4816
|
return decoded.success.reply;
|
|
4120
4817
|
};
|
|
4121
|
-
var writeBotReply = async (taskId, reply) => {
|
|
4818
|
+
var writeBotReply = async (taskId, reply, runId) => {
|
|
4122
4819
|
const decoded = Schema6.decodeUnknownResult(Reply)(reply);
|
|
4123
4820
|
if (decoded._tag === "Failure") {
|
|
4124
|
-
throw new Error("Provide a result of at most 131072 characters
|
|
4821
|
+
throw new Error("Provide a result of at most 131072 characters, a nonempty error of at most 16384 characters, or a clarification question of at most 4096 UTF-8 bytes");
|
|
4125
4822
|
}
|
|
4126
|
-
const pending = await pendingReply(taskId);
|
|
4823
|
+
const pending = await pendingReply(taskId, runId);
|
|
4127
4824
|
if (pending === undefined)
|
|
4128
4825
|
throw new Error("No pending bot reply for this task");
|
|
4129
|
-
const existing = await readBotReply(taskId);
|
|
4826
|
+
const existing = await readBotReply(taskId, runId);
|
|
4130
4827
|
if (existing !== undefined) {
|
|
4131
4828
|
if (JSON.stringify(existing) === JSON.stringify(decoded.success))
|
|
4132
4829
|
return;
|
|
@@ -4135,15 +4832,15 @@ var writeBotReply = async (taskId, reply) => {
|
|
|
4135
4832
|
const submittedAt = Date.now();
|
|
4136
4833
|
if (submittedAt >= pending.deadline)
|
|
4137
4834
|
throw new Error("Bot reply deadline has expired");
|
|
4138
|
-
if (await publish(paths(taskId).receipt, { taskId, submittedAt, reply: decoded.success }))
|
|
4835
|
+
if (await publish(paths(taskId, runId).receipt, { taskId, submittedAt, reply: decoded.success }))
|
|
4139
4836
|
return;
|
|
4140
|
-
const winner = await readBotReply(taskId);
|
|
4837
|
+
const winner = await readBotReply(taskId, runId);
|
|
4141
4838
|
if (JSON.stringify(winner) !== JSON.stringify(decoded.success)) {
|
|
4142
4839
|
throw new Error("A different reply has already been submitted for this task");
|
|
4143
4840
|
}
|
|
4144
4841
|
};
|
|
4145
|
-
var cleanupBotReply = async (taskId) => {
|
|
4146
|
-
const files = paths(taskId);
|
|
4842
|
+
var cleanupBotReply = async (taskId, runId) => {
|
|
4843
|
+
const files = paths(taskId, runId);
|
|
4147
4844
|
for (const path of [files.pending, files.receipt, files.dispatch]) {
|
|
4148
4845
|
await unlink(path).catch((cause) => {
|
|
4149
4846
|
if (!isMissing(cause))
|
|
@@ -4293,29 +4990,43 @@ var requireTime = (deadline) => {
|
|
|
4293
4990
|
throw new Error(timeoutMessage);
|
|
4294
4991
|
};
|
|
4295
4992
|
var finishReply = (reply) => {
|
|
4993
|
+
if (reply.question !== undefined)
|
|
4994
|
+
return { result: "", question: reply.question };
|
|
4296
4995
|
if (reply.error !== undefined)
|
|
4297
4996
|
throw new Error(reply.error);
|
|
4298
4997
|
if (reply.result === undefined)
|
|
4299
4998
|
throw new Error("Grok Bot returned an invalid Cohall reply");
|
|
4300
4999
|
return { result: reply.result };
|
|
4301
5000
|
};
|
|
4302
|
-
var taskPrompt = (task, command) =>
|
|
4303
|
-
task.
|
|
4304
|
-
|
|
5001
|
+
var taskPrompt = (task, command, supportsClarification) => {
|
|
5002
|
+
const reply = `${command} reply ${task.id}${task.runId === undefined ? "" : ` --run-id ${task.runId}`}`;
|
|
5003
|
+
return [
|
|
5004
|
+
task.prompt,
|
|
5005
|
+
task.context === undefined ? "" : `Context:
|
|
4305
5006
|
${task.context}`,
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
5007
|
+
`This request came through Cohall. Thread: ${task.threadId}. Task: ${task.id}.`,
|
|
5008
|
+
`You may use your own tools and delegate work to another Cohall device when the request calls for it. Discover devices with: ${command} devices`,
|
|
5009
|
+
`Delegation example: ${command} delegate --thread ${task.threadId} --parent ${task.id} --target @device --provider codex --prompt 'Concrete task' --context 'Relevant context'`,
|
|
5010
|
+
"When finished, hand the final answer back to Cohall so it reaches the requesting device. Write it to a UTF-8 file on this computer, then run:",
|
|
5011
|
+
`${reply} --message-file /absolute/path/to/your-answer.txt`,
|
|
5012
|
+
`Alternatively pipe your answer into: ${reply} --message -`,
|
|
5013
|
+
`If you cannot complete this task, report the reason with: ${reply} --error 'Reason'`,
|
|
5014
|
+
...supportsClarification && task.runId !== undefined ? [
|
|
5015
|
+
`If essential information is missing, run ${reply} --question 'Your question' and end this turn. Cohall will pause the task and send a new turn with the answer. Do not send a final reply after asking.`,
|
|
5016
|
+
...(task.clarifications ?? []).flatMap((question) => question.answer === undefined ? [] : [
|
|
5017
|
+
`Clarification question:
|
|
5018
|
+
${question.question}
|
|
5019
|
+
Sender's answer:
|
|
5020
|
+
${question.answer.text}`
|
|
5021
|
+
])
|
|
5022
|
+
] : [],
|
|
5023
|
+
"The callback command must succeed. After submitting a final result, send the same answer in this chat. After submitting a question, end the turn. A normal chat message alone does not complete the Cohall task."
|
|
5024
|
+
].filter(Boolean).join(`
|
|
4315
5025
|
|
|
4316
5026
|
`);
|
|
4317
|
-
|
|
4318
|
-
|
|
5027
|
+
};
|
|
5028
|
+
var runGrokBot = async (path, task, signal, supportsClarification = false) => {
|
|
5029
|
+
const existing = await readBotReply(task.id, task.runId);
|
|
4319
5030
|
if (existing !== undefined)
|
|
4320
5031
|
return finishReply(existing);
|
|
4321
5032
|
const { deadline, command, dispatched } = await prepareBotReply(task);
|
|
@@ -4326,11 +5037,11 @@ var runGrokBot = async (path, task, signal) => {
|
|
|
4326
5037
|
const lookup = async (lookupSignal = signal) => {
|
|
4327
5038
|
if (path === undefined)
|
|
4328
5039
|
throw new Error("Configure this device's Grok Bot gateway discovery file first");
|
|
4329
|
-
return rpc(await readConnection(path), "promptAcceptanceStatus", { accountSlot: "host", agentId, clientNonce: task.id }, Acceptance, lookupSignal);
|
|
5040
|
+
return rpc(await readConnection(path), "promptAcceptanceStatus", { accountSlot: "host", agentId, clientNonce: task.runId ?? task.id }, Acceptance, lookupSignal);
|
|
4330
5041
|
};
|
|
4331
5042
|
const waitForReply = (reconcile = false) => {
|
|
4332
5043
|
const receipt = poll(async () => {
|
|
4333
|
-
const receipt2 = await readBotReply(task.id);
|
|
5044
|
+
const receipt2 = await readBotReply(task.id, task.runId);
|
|
4334
5045
|
if (receipt2 !== undefined)
|
|
4335
5046
|
return finishReply(receipt2);
|
|
4336
5047
|
requireTime(deadline);
|
|
@@ -4359,7 +5070,7 @@ var runGrokBot = async (path, task, signal) => {
|
|
|
4359
5070
|
if (acceptance.outcome === "unknown-durability")
|
|
4360
5071
|
throw new Error("Grok Bot cannot confirm whether this task was accepted; check its conversation before retrying");
|
|
4361
5072
|
if (acceptance.outcome === "not-found") {
|
|
4362
|
-
const receipt = await readBotReply(task.id);
|
|
5073
|
+
const receipt = await readBotReply(task.id, task.runId);
|
|
4363
5074
|
if (receipt !== undefined)
|
|
4364
5075
|
return finishReply(receipt);
|
|
4365
5076
|
requireTime(deadline);
|
|
@@ -4371,20 +5082,20 @@ var runGrokBot = async (path, task, signal) => {
|
|
|
4371
5082
|
acceptance = await lookup();
|
|
4372
5083
|
if (acceptance.outcome === "not-found") {
|
|
4373
5084
|
requireTime(deadline);
|
|
4374
|
-
if (!await claimBotDispatch(task.id)) {
|
|
5085
|
+
if (!await claimBotDispatch(task.id, task.runId)) {
|
|
4375
5086
|
return waitForReply(true);
|
|
4376
5087
|
}
|
|
4377
5088
|
try {
|
|
4378
5089
|
await rpc(connection, "sendPrompt", {
|
|
4379
5090
|
agentId,
|
|
4380
|
-
prompt: taskPrompt(task, command),
|
|
4381
|
-
clientNonce: task.id,
|
|
5091
|
+
prompt: taskPrompt(task, command, supportsClarification),
|
|
5092
|
+
clientNonce: task.runId ?? task.id,
|
|
4382
5093
|
directAddressedAcceptance: true
|
|
4383
5094
|
}, SendResult, signal);
|
|
4384
5095
|
} catch {
|
|
4385
5096
|
if (signal.aborted)
|
|
4386
5097
|
throw new Error("Stopped waiting for Grok Bot; its run may still be active");
|
|
4387
|
-
const receipt2 = await readBotReply(task.id);
|
|
5098
|
+
const receipt2 = await readBotReply(task.id, task.runId);
|
|
4388
5099
|
if (receipt2 !== undefined)
|
|
4389
5100
|
return finishReply(receipt2);
|
|
4390
5101
|
return waitForReply(true);
|
|
@@ -4475,9 +5186,13 @@ var guidedSetupInput = async (options, prompter) => {
|
|
|
4475
5186
|
const relayUrl = normalizeRelayUrl(options.relayUrl ?? await prompter.answer("Relay URL", existing?.relayUrl ?? "http://127.0.0.1:8787"));
|
|
4476
5187
|
const credentials = credentialsForRelay(existing, relayUrl);
|
|
4477
5188
|
const hasRequiredCredentials = credentials.clientToken !== undefined && (options.clientOnly || credentials.deviceToken !== undefined);
|
|
4478
|
-
|
|
5189
|
+
let workspaces = options.clientOnly ? existing?.workspaces ?? [] : options.workspaces;
|
|
5190
|
+
if (!options.clientOnly && workspaces.length === 0) {
|
|
5191
|
+
const root = await prompter.answer("Workspace root", existing?.workspaces[0] ?? options.cwd);
|
|
5192
|
+
workspaces = existing !== undefined && root === existing.workspaces[0] ? existing.workspaces : [root];
|
|
5193
|
+
}
|
|
4479
5194
|
const deviceName = options.clientOnly ? undefined : options.deviceName ?? await prompter.answer("Device name", existing?.deviceName ?? hostname2());
|
|
4480
|
-
const providers = options.providers ?? (options.clientOnly ? "auto" : await prompter.answer("Providers", existing?.providers?.join(",") ?? "auto"));
|
|
5195
|
+
const providers = options.providers ?? (options.clientOnly ? existing?.providers?.join(",") ?? "auto" : await prompter.answer("Providers", existing?.providers?.join(",") ?? "auto"));
|
|
4481
5196
|
const token = options.token ?? (hasRequiredCredentials ? undefined : await prompter.secret("Pairing token"));
|
|
4482
5197
|
if (!hasRequiredCredentials && (token === undefined || token.length === 0)) {
|
|
4483
5198
|
throw new Error("A pairing token is required");
|
|
@@ -4508,21 +5223,21 @@ import {
|
|
|
4508
5223
|
mkdir as mkdir5,
|
|
4509
5224
|
mkdtemp as mkdtemp2,
|
|
4510
5225
|
readFile as readFile3,
|
|
4511
|
-
realpath as
|
|
5226
|
+
realpath as realpath3,
|
|
4512
5227
|
rename as rename3,
|
|
4513
5228
|
rm as rm3,
|
|
4514
5229
|
stat as stat3,
|
|
4515
5230
|
writeFile as writeFile5
|
|
4516
5231
|
} from "node:fs/promises";
|
|
4517
|
-
import { basename, dirname as dirname5, join as join6, resolve as
|
|
5232
|
+
import { basename, dirname as dirname5, join as join6, resolve as resolve3 } from "node:path";
|
|
4518
5233
|
import { backup as sqliteBackup, DatabaseSync } from "node:sqlite";
|
|
4519
5234
|
import { WebSocket } from "ws";
|
|
4520
5235
|
|
|
4521
5236
|
// apps/device/src/service.ts
|
|
4522
5237
|
import { execFile as execFile2 } from "node:child_process";
|
|
4523
|
-
import { chmod as chmod5, mkdir as mkdir4,
|
|
5238
|
+
import { chmod as chmod5, mkdir as mkdir4, writeFile as writeFile4 } from "node:fs/promises";
|
|
4524
5239
|
import { homedir as homedir3 } from "node:os";
|
|
4525
|
-
import { dirname as dirname4, join as join5 } from "node:path";
|
|
5240
|
+
import { dirname as dirname4, isAbsolute as isAbsolute3, join as join5 } from "node:path";
|
|
4526
5241
|
|
|
4527
5242
|
// apps/device/src/upgrade.ts
|
|
4528
5243
|
import { Schema as Schema8 } from "effect";
|
|
@@ -4533,16 +5248,61 @@ import {
|
|
|
4533
5248
|
chmod as chmod4,
|
|
4534
5249
|
mkdir as mkdir3,
|
|
4535
5250
|
readFile as readFile2,
|
|
5251
|
+
readdir,
|
|
4536
5252
|
realpath as realpath2,
|
|
4537
5253
|
rename as rename2,
|
|
4538
5254
|
rm as rm2,
|
|
4539
5255
|
stat as stat2,
|
|
4540
5256
|
writeFile as writeFile3
|
|
4541
5257
|
} from "node:fs/promises";
|
|
4542
|
-
import { delimiter as delimiter2, dirname as dirname3, extname as extname2, isAbsolute as isAbsolute2, join as join4, relative } from "node:path";
|
|
5258
|
+
import { delimiter as delimiter2, dirname as dirname3, extname as extname2, isAbsolute as isAbsolute2, join as join4, relative, resolve as resolve2 } from "node:path";
|
|
4543
5259
|
import { platform as operatingSystem } from "node:os";
|
|
4544
5260
|
var packageName = "@akshar5/cohall";
|
|
4545
|
-
var
|
|
5261
|
+
var minimumBunLookupVersion = "1.2.15";
|
|
5262
|
+
var semanticVersion = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
5263
|
+
var parseVersion = (value) => {
|
|
5264
|
+
const [, major, minor, patch, prerelease] = semanticVersion.exec(value) ?? [];
|
|
5265
|
+
if (major === undefined || minor === undefined || patch === undefined)
|
|
5266
|
+
return;
|
|
5267
|
+
const identifiers = prerelease?.split(".") ?? [];
|
|
5268
|
+
if (identifiers.some((part) => /^0\d+$/.test(part)))
|
|
5269
|
+
return;
|
|
5270
|
+
return {
|
|
5271
|
+
core: [BigInt(major), BigInt(minor), BigInt(patch)],
|
|
5272
|
+
prerelease: identifiers
|
|
5273
|
+
};
|
|
5274
|
+
};
|
|
5275
|
+
var compareVersions = (left, right) => {
|
|
5276
|
+
const a = parseVersion(left);
|
|
5277
|
+
const b = parseVersion(right);
|
|
5278
|
+
if (a === undefined || b === undefined) {
|
|
5279
|
+
throw new Error(`Could not safely compare Cohall versions ${left} and ${right}`);
|
|
5280
|
+
}
|
|
5281
|
+
for (const [index, part] of a.core.entries()) {
|
|
5282
|
+
const other = b.core[index];
|
|
5283
|
+
if (other !== undefined && part !== other)
|
|
5284
|
+
return part > other ? 1 : -1;
|
|
5285
|
+
}
|
|
5286
|
+
if (a.prerelease.length === 0 || b.prerelease.length === 0) {
|
|
5287
|
+
return Number(b.prerelease.length > 0) - Number(a.prerelease.length > 0);
|
|
5288
|
+
}
|
|
5289
|
+
for (let index = 0;index < Math.max(a.prerelease.length, b.prerelease.length); index += 1) {
|
|
5290
|
+
const part = a.prerelease[index];
|
|
5291
|
+
const other = b.prerelease[index];
|
|
5292
|
+
if (part === undefined)
|
|
5293
|
+
return -1;
|
|
5294
|
+
if (other === undefined)
|
|
5295
|
+
return 1;
|
|
5296
|
+
if (part === other)
|
|
5297
|
+
continue;
|
|
5298
|
+
const numeric = /^\d+$/.test(part);
|
|
5299
|
+
const otherNumeric = /^\d+$/.test(other);
|
|
5300
|
+
if (numeric !== otherNumeric)
|
|
5301
|
+
return numeric ? -1 : 1;
|
|
5302
|
+
return numeric ? BigInt(part) > BigInt(other) ? 1 : -1 : part > other ? 1 : -1;
|
|
5303
|
+
}
|
|
5304
|
+
return 0;
|
|
5305
|
+
};
|
|
4546
5306
|
var PackageManager = Schema8.Literals(["npm", "bun", "pnpm"]);
|
|
4547
5307
|
var RestartReceipt = Schema8.Struct({
|
|
4548
5308
|
version: Schema8.String,
|
|
@@ -4552,45 +5312,165 @@ var RestartReceipt = Schema8.Struct({
|
|
|
4552
5312
|
restartedServices: Schema8.Array(Schema8.String),
|
|
4553
5313
|
restartingService: Schema8.optionalKey(Schema8.String)
|
|
4554
5314
|
});
|
|
5315
|
+
var pnpmEnvironment = () => Object.fromEntries([
|
|
5316
|
+
["COREPACK_ENABLE_PROJECT_SPEC", "0"],
|
|
5317
|
+
...["GLOBAL_DIR", "GLOBAL_BIN_DIR", "STORE_DIR"].flatMap((name) => {
|
|
5318
|
+
const value = process.env[`COHALL_PNPM_${name}`];
|
|
5319
|
+
return value === undefined ? [] : [
|
|
5320
|
+
[`npm_config_${name.toLowerCase()}`, value],
|
|
5321
|
+
[`pnpm_config_${name.toLowerCase()}`, value]
|
|
5322
|
+
];
|
|
5323
|
+
})
|
|
5324
|
+
]);
|
|
4555
5325
|
var normalizePath = (path) => path.replaceAll("\\", "/");
|
|
5326
|
+
var pnpmGlobalDirectory = /^(.*\/global\/v?\d+)\//;
|
|
4556
5327
|
var normalizeUpgradeTarget = (target) => {
|
|
4557
5328
|
if (target === undefined || target === "latest") {
|
|
4558
5329
|
return "latest";
|
|
4559
5330
|
}
|
|
4560
5331
|
const normalized = target.startsWith("v") ? target.slice(1) : target;
|
|
4561
|
-
if (
|
|
5332
|
+
if (parseVersion(normalized) === undefined) {
|
|
4562
5333
|
throw new Error("--to must be latest or an exact semantic version");
|
|
4563
5334
|
}
|
|
4564
5335
|
return normalized;
|
|
4565
5336
|
};
|
|
4566
5337
|
var deviceVersionWarning = (cliVersion, deviceVersion) => deviceVersion === undefined || deviceVersion === cliVersion ? undefined : `Running device daemon uses Cohall ${deviceVersion}, while this CLI is ${cliVersion}. Run upgrade through the executable configured by the device service.`;
|
|
4567
|
-
var packageInstallation = (canonicalEntrypoint, entrypoint = canonicalEntrypoint) => {
|
|
5338
|
+
var packageInstallation = (canonicalEntrypoint, entrypoint = canonicalEntrypoint, runtimePlatform = process.platform) => {
|
|
4568
5339
|
const path = normalizePath(canonicalEntrypoint);
|
|
4569
5340
|
const marker = `/node_modules/${packageName}/`;
|
|
4570
5341
|
const packageIndex = path.lastIndexOf(marker);
|
|
4571
5342
|
if (packageIndex === -1) {
|
|
4572
5343
|
throw new Error("cohall upgrade requires a global npm, Bun, or pnpm installation; package-runner and source-checkout executions cannot upgrade themselves");
|
|
4573
5344
|
}
|
|
4574
|
-
if (path.includes("/.npm/_npx/") ||
|
|
5345
|
+
if ([path, normalizePath(entrypoint)].some((candidate) => candidate.includes("/.npm/_npx/") || candidate.includes("/.bunx/") || candidate.includes("/dlx/"))) {
|
|
4575
5346
|
throw new Error("This Cohall process is running from a temporary package-runner cache; install it globally before using cohall upgrade");
|
|
4576
5347
|
}
|
|
4577
5348
|
if (path.includes("/.bun/install/global/node_modules/")) {
|
|
4578
5349
|
return { manager: "bun", entrypoint };
|
|
4579
5350
|
}
|
|
4580
|
-
if (path.includes("/pnpm/
|
|
5351
|
+
if (pnpmGlobalDirectory.test(path) || /\/v\d+\/links\//.test(path) || path.includes("/node_modules/.pnpm/") || pnpmGlobalDirectory.test(normalizePath(entrypoint))) {
|
|
4581
5352
|
return { manager: "pnpm", entrypoint };
|
|
4582
5353
|
}
|
|
4583
5354
|
const nodeModulesParent = path.slice(0, packageIndex);
|
|
4584
|
-
const prefix = nodeModulesParent.endsWith("/lib") ? nodeModulesParent.slice(0, -"/lib".length) : nodeModulesParent;
|
|
5355
|
+
const prefix = runtimePlatform !== "win32" && nodeModulesParent.endsWith("/lib") ? nodeModulesParent.slice(0, -"/lib".length) : nodeModulesParent;
|
|
4585
5356
|
return { manager: "npm", prefix, entrypoint };
|
|
4586
5357
|
};
|
|
5358
|
+
var resolvePackageInstallation = async (entrypoint, options = {}) => {
|
|
5359
|
+
const canonicalEntrypoint = await realpath2(entrypoint);
|
|
5360
|
+
const runtimePlatform = options.platform ?? process.platform;
|
|
5361
|
+
const installation = packageInstallation(canonicalEntrypoint, resolve2(entrypoint), runtimePlatform);
|
|
5362
|
+
if (installation.manager !== "pnpm") {
|
|
5363
|
+
const prefix = installation.prefix;
|
|
5364
|
+
if (installation.manager === "npm" && prefix !== undefined) {
|
|
5365
|
+
const packageEntry2 = join4(prefix, ...runtimePlatform === "win32" ? [] : ["lib"], "node_modules", packageName, "bin", "cohall.js");
|
|
5366
|
+
const command = join4(prefix, ...runtimePlatform === "win32" ? [] : ["bin"], "cohall");
|
|
5367
|
+
const globalCommand = runtimePlatform === "win32" ? await readFile2(`${command}.cmd`, "utf8").then((shim) => /"%dp0%\\node_modules\\@akshar5\\cohall\\bin\\cohall\.js"\s+%\*/i.test(shim)).catch(() => false) : await realpath2(command).catch(() => {
|
|
5368
|
+
return;
|
|
5369
|
+
}) === canonicalEntrypoint;
|
|
5370
|
+
if (globalCommand && await realpath2(packageEntry2).catch(() => {
|
|
5371
|
+
return;
|
|
5372
|
+
}) === canonicalEntrypoint)
|
|
5373
|
+
return {
|
|
5374
|
+
...installation,
|
|
5375
|
+
canonicalEntrypoint,
|
|
5376
|
+
pnpmExecutable: undefined,
|
|
5377
|
+
globalRoot: undefined
|
|
5378
|
+
};
|
|
5379
|
+
}
|
|
5380
|
+
const bun = await (options.resolveExecutable ?? trustedExecutable)("bun").catch(() => {
|
|
5381
|
+
return;
|
|
5382
|
+
});
|
|
5383
|
+
if (bun !== undefined) {
|
|
5384
|
+
const result2 = await (options.runner ?? defaultRunner).run(bun, ["pm", "ls", "--global"], 1e4);
|
|
5385
|
+
const root2 = result2.stdout.split(/\r?\n/, 1)[0]?.match(/^(.*?) node_modules \(\d+(?: installed)?\)$/)?.[1];
|
|
5386
|
+
if (result2.exitCode === 0 && root2 !== undefined && isAbsolute2(root2) && await realpath2(join4(root2, "node_modules", packageName, "bin", "cohall.js")).catch(() => {
|
|
5387
|
+
return;
|
|
5388
|
+
}) === canonicalEntrypoint)
|
|
5389
|
+
return {
|
|
5390
|
+
manager: "bun",
|
|
5391
|
+
entrypoint: resolve2(entrypoint),
|
|
5392
|
+
canonicalEntrypoint,
|
|
5393
|
+
globalDir: root2,
|
|
5394
|
+
pnpmExecutable: undefined,
|
|
5395
|
+
globalRoot: undefined
|
|
5396
|
+
};
|
|
5397
|
+
}
|
|
5398
|
+
throw new Error("This operation requires a verified global npm, Bun, or pnpm installation. Project-local packages cannot upgrade or install a service; run the global cohall command with its package manager's configuration.");
|
|
5399
|
+
}
|
|
5400
|
+
const lexicalPath = normalizePath(installation.entrypoint);
|
|
5401
|
+
const globalRoot = lexicalPath.match(pnpmGlobalDirectory)?.[1] ?? lexicalPath.match(/^(.*\/v?\d+)\//)?.[1];
|
|
5402
|
+
const globalEntrypointError = () => new Error("This operation requires a global pnpm entrypoint; project-local and shared-store executions cannot select a global installation. Run the global cohall command.");
|
|
5403
|
+
if (globalRoot === undefined)
|
|
5404
|
+
throw globalEntrypointError();
|
|
5405
|
+
const selectedManager = process.env.COHALL_PNPM_EXECUTABLE ?? "pnpm";
|
|
5406
|
+
const pnpmExecutable = options.resolveExecutable === undefined ? await trustedExecutable(selectedManager, { preserveSymlink: true }) : await options.resolveExecutable(selectedManager);
|
|
5407
|
+
const result = await checked(options.runner ?? defaultRunner, {
|
|
5408
|
+
command: pnpmExecutable,
|
|
5409
|
+
arguments: ["root", "--global"],
|
|
5410
|
+
environment: pnpmEnvironment()
|
|
5411
|
+
}, 1e4);
|
|
5412
|
+
const reported = normalizePath(result.stdout.trim().split(/\r?\n/).at(-1)?.trim() ?? "");
|
|
5413
|
+
const root = reported.endsWith("/node_modules") ? dirname3(reported) : reported;
|
|
5414
|
+
const selected = resolve2(globalRoot);
|
|
5415
|
+
const configured = isAbsolute2(root) ? resolve2(root) : undefined;
|
|
5416
|
+
if (configured === undefined || (process.platform === "win32" ? selected.toLowerCase() !== configured.toLowerCase() : selected !== configured))
|
|
5417
|
+
throw globalEntrypointError();
|
|
5418
|
+
const packageEntry = (root2) => join4(root2, "node_modules", packageName, "bin", "cohall.js");
|
|
5419
|
+
const entries = await readdir(globalRoot, { withFileTypes: true });
|
|
5420
|
+
const candidates = [
|
|
5421
|
+
packageEntry(globalRoot),
|
|
5422
|
+
...entries.filter((entry) => entry.isSymbolicLink()).map((entry) => packageEntry(join4(globalRoot, entry.name)))
|
|
5423
|
+
];
|
|
5424
|
+
for (const candidate of candidates) {
|
|
5425
|
+
if (await realpath2(candidate).catch(() => {
|
|
5426
|
+
return;
|
|
5427
|
+
}) === canonicalEntrypoint)
|
|
5428
|
+
return {
|
|
5429
|
+
...installation,
|
|
5430
|
+
entrypoint: candidate,
|
|
5431
|
+
canonicalEntrypoint,
|
|
5432
|
+
globalDir: dirname3(globalRoot),
|
|
5433
|
+
globalRoot,
|
|
5434
|
+
pnpmExecutable
|
|
5435
|
+
};
|
|
5436
|
+
}
|
|
5437
|
+
throw new Error("Could not find a stable pnpm global executable for this installation. Run the global cohall command, then use cohall service install before upgrading.");
|
|
5438
|
+
};
|
|
5439
|
+
var pnpmServiceDirectories = async (executable, globalRoot, runner = defaultRunner) => {
|
|
5440
|
+
const query = async (arguments_) => {
|
|
5441
|
+
const result = await checked(runner, { command: executable, arguments: arguments_, environment: pnpmEnvironment() }, 1e4);
|
|
5442
|
+
const directory = result.stdout.trim().split(/\r?\n/).at(-1)?.trim() ?? "";
|
|
5443
|
+
if (!isAbsolute2(directory))
|
|
5444
|
+
throw new Error(`pnpm ${arguments_.join(" ")} did not report an absolute directory`);
|
|
5445
|
+
return directory;
|
|
5446
|
+
};
|
|
5447
|
+
const [bin, store] = await Promise.all([
|
|
5448
|
+
query(["bin", "--global"]),
|
|
5449
|
+
query(["--dir", globalRoot, "--ignore-workspace", "store", "path"])
|
|
5450
|
+
]);
|
|
5451
|
+
return { bin, store };
|
|
5452
|
+
};
|
|
4587
5453
|
var packageInstallCommand = (installation, target) => {
|
|
4588
5454
|
const specification = `${packageName}@${target}`;
|
|
4589
5455
|
switch (installation.manager) {
|
|
4590
5456
|
case "bun":
|
|
4591
|
-
return {
|
|
5457
|
+
return {
|
|
5458
|
+
command: "bun",
|
|
5459
|
+
arguments: ["add", "--global", specification],
|
|
5460
|
+
...installation.globalDir === undefined ? {} : { environment: { BUN_INSTALL_GLOBAL_DIR: installation.globalDir } }
|
|
5461
|
+
};
|
|
4592
5462
|
case "pnpm":
|
|
4593
|
-
return {
|
|
5463
|
+
return {
|
|
5464
|
+
command: "pnpm",
|
|
5465
|
+
arguments: [
|
|
5466
|
+
"add",
|
|
5467
|
+
"--global",
|
|
5468
|
+
...installation.globalDir === undefined ? [] : ["--global-dir", installation.globalDir],
|
|
5469
|
+
...process.env.COHALL_PNPM_GLOBAL_BIN_DIR === undefined ? [] : ["--global-bin-dir", process.env.COHALL_PNPM_GLOBAL_BIN_DIR],
|
|
5470
|
+
specification
|
|
5471
|
+
],
|
|
5472
|
+
environment: pnpmEnvironment()
|
|
5473
|
+
};
|
|
4594
5474
|
case "npm":
|
|
4595
5475
|
return {
|
|
4596
5476
|
command: "npm",
|
|
@@ -4629,36 +5509,38 @@ var isTrustedGroupWritablePath = (options) => {
|
|
|
4629
5509
|
const writableRoot = options.writableRoot;
|
|
4630
5510
|
return options.platform === "darwin" && writableRoot !== undefined && options.uid !== undefined && options.ownerUid === options.uid && options.ownerGid === macosAdminGroup && macosHomebrewRoots.some((root) => containsPath(root, writableRoot)) && containsPath(writableRoot, options.canonical) && containsPath(writableRoot, options.path);
|
|
4631
5511
|
};
|
|
4632
|
-
var validateExecutable = async (candidate, installationRoot) => {
|
|
5512
|
+
var validateExecutable = async (candidate, installationRoot, preserveSymlink = false) => {
|
|
4633
5513
|
const canonical = await realpath2(candidate);
|
|
4634
5514
|
if (operatingSystem() === "win32") {
|
|
4635
|
-
return canonical;
|
|
5515
|
+
return preserveSymlink ? resolve2(candidate) : canonical;
|
|
4636
5516
|
}
|
|
4637
5517
|
const uid = process.getuid?.();
|
|
4638
5518
|
const writableRoot = installationRoot === undefined ? undefined : await realpath2(installationRoot);
|
|
4639
|
-
for (
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
5519
|
+
for (const executablePath of preserveSymlink ? [canonical, resolve2(candidate)] : [canonical]) {
|
|
5520
|
+
for (let path = executablePath;; path = dirname3(path)) {
|
|
5521
|
+
const metadata = await stat2(path);
|
|
5522
|
+
const trustedGroupWritablePath = isTrustedGroupWritablePath({
|
|
5523
|
+
platform: operatingSystem(),
|
|
5524
|
+
canonical,
|
|
5525
|
+
writableRoot,
|
|
5526
|
+
path,
|
|
5527
|
+
uid,
|
|
5528
|
+
ownerUid: metadata.uid,
|
|
5529
|
+
ownerGid: metadata.gid
|
|
5530
|
+
});
|
|
5531
|
+
if ((metadata.mode & 2) !== 0 || (metadata.mode & 16) !== 0 && !trustedGroupWritablePath) {
|
|
5532
|
+
throw new Error(`Refusing executable beneath group- or world-writable path ${path}`);
|
|
5533
|
+
}
|
|
5534
|
+
if (uid !== undefined && metadata.uid !== 0 && metadata.uid !== uid && !isTrustedSystemPath(operatingSystem(), path)) {
|
|
5535
|
+
throw new Error(`Refusing executable owned by another user at ${path}`);
|
|
5536
|
+
}
|
|
5537
|
+
const parent = dirname3(path);
|
|
5538
|
+
if (parent === path) {
|
|
5539
|
+
break;
|
|
5540
|
+
}
|
|
4659
5541
|
}
|
|
4660
5542
|
}
|
|
4661
|
-
return canonical;
|
|
5543
|
+
return preserveSymlink ? resolve2(candidate) : canonical;
|
|
4662
5544
|
};
|
|
4663
5545
|
var trustedExecutable = async (command, options = {}) => {
|
|
4664
5546
|
const candidates = isAbsolute2(command) ? [command] : (process.env.PATH ?? "").split(delimiter2).filter(Boolean).flatMap((directory) => executableNames(command).map((name) => join4(directory, name)));
|
|
@@ -4668,7 +5550,7 @@ var trustedExecutable = async (command, options = {}) => {
|
|
|
4668
5550
|
if (!available2)
|
|
4669
5551
|
continue;
|
|
4670
5552
|
try {
|
|
4671
|
-
return await validateExecutable(path, options.writableRoot);
|
|
5553
|
+
return await validateExecutable(path, options.writableRoot, options.preserveSymlink);
|
|
4672
5554
|
} catch (cause) {
|
|
4673
5555
|
failure ??= cause;
|
|
4674
5556
|
}
|
|
@@ -4710,6 +5592,43 @@ var trustedServices = async (services, resolveExecutable) => {
|
|
|
4710
5592
|
}
|
|
4711
5593
|
return secured;
|
|
4712
5594
|
};
|
|
5595
|
+
var ScheduledTaskAction = Schema8.Struct({
|
|
5596
|
+
Execute: Schema8.NonEmptyString,
|
|
5597
|
+
Arguments: Schema8.NonEmptyString
|
|
5598
|
+
});
|
|
5599
|
+
var powershellLiteral = "(?:[^'\\r\\n]|'')+";
|
|
5600
|
+
var scheduledTaskBootstrap = new RegExp([
|
|
5601
|
+
"^\\$ErrorActionPreference = 'Stop'",
|
|
5602
|
+
`\\$env:COHALL_CONFIG = '${powershellLiteral}'`,
|
|
5603
|
+
`(?:\\$env:PNPM_HOME = '${powershellLiteral}'\\r?\\n\\$env:PATH = '${powershellLiteral}' \\+ ';' \\+ \\$env:PATH\\r?\\n)?(?:\\$env:COHALL_PNPM_EXECUTABLE = '${powershellLiteral}'\\r?\\n)?(?:\\$env:COHALL_PNPM_STORE_DIR = '${powershellLiteral}'\\r?\\n)?(?:\\$env:COHALL_PNPM_GLOBAL_DIR = '${powershellLiteral}'\\r?\\n)?(?:\\$env:COHALL_PNPM_GLOBAL_BIN_DIR = '${powershellLiteral}'\\r?\\n\\$env:PATH = '${powershellLiteral}' \\+ ';' \\+ \\$env:PATH\\r?\\n)?\\$env:PATH = '${powershellLiteral}' \\+ ';' \\+ \\$env:PATH`,
|
|
5604
|
+
`& '(${powershellLiteral})' '(${powershellLiteral})' device`,
|
|
5605
|
+
"exit \\$LASTEXITCODE(?:\\r?\\n)?$"
|
|
5606
|
+
].join("\\r?\\n"));
|
|
5607
|
+
var scheduledTaskEntrypoint = (output) => {
|
|
5608
|
+
let value;
|
|
5609
|
+
try {
|
|
5610
|
+
value = JSON.parse(output);
|
|
5611
|
+
} catch {
|
|
5612
|
+
return;
|
|
5613
|
+
}
|
|
5614
|
+
const decoded = Schema8.decodeUnknownOption(ScheduledTaskAction)(value);
|
|
5615
|
+
if (decoded._tag === "None")
|
|
5616
|
+
return;
|
|
5617
|
+
const action = decoded.value;
|
|
5618
|
+
if (!/(?:^|[/\\])(?:powershell|pwsh)\.exe$/i.test(action.Execute))
|
|
5619
|
+
return;
|
|
5620
|
+
const encoded = /^-NoLogo\s+-NoProfile\s+-NonInteractive\s+-EncodedCommand\s+([A-Za-z0-9+/]+={0,2})\s*$/i.exec(action.Arguments)?.[1];
|
|
5621
|
+
if (encoded === undefined)
|
|
5622
|
+
return;
|
|
5623
|
+
const bytes = Buffer.from(encoded, "base64");
|
|
5624
|
+
if (bytes.length % 2 !== 0 || bytes.toString("base64") !== encoded)
|
|
5625
|
+
return;
|
|
5626
|
+
const bootstrap = scheduledTaskBootstrap.exec(bytes.toString("utf16le"));
|
|
5627
|
+
const node = bootstrap?.[1]?.replaceAll("''", "'");
|
|
5628
|
+
if (node === undefined || !/(?:^|[/\\])node\.exe$/i.test(node))
|
|
5629
|
+
return;
|
|
5630
|
+
return bootstrap?.[2]?.replaceAll("''", "'");
|
|
5631
|
+
};
|
|
4713
5632
|
var serviceCandidates = (runtimePlatform, uid) => {
|
|
4714
5633
|
if (runtimePlatform === "linux") {
|
|
4715
5634
|
const systemd = (manager, unit, device) => {
|
|
@@ -4769,6 +5688,18 @@ var serviceCandidates = (runtimePlatform, uid) => {
|
|
|
4769
5688
|
"if ((Get-ScheduledTask -TaskName 'Cohall Device' -ErrorAction SilentlyContinue).State -eq 'Running') { exit 0 } else { exit 1 }"
|
|
4770
5689
|
]
|
|
4771
5690
|
},
|
|
5691
|
+
entrypoint: {
|
|
5692
|
+
inspect: {
|
|
5693
|
+
command: "powershell.exe",
|
|
5694
|
+
arguments: [
|
|
5695
|
+
"-NoProfile",
|
|
5696
|
+
"-NonInteractive",
|
|
5697
|
+
"-Command",
|
|
5698
|
+
"@((Get-ScheduledTask -TaskName 'Cohall Device' -TaskPath '\\' -ErrorAction Stop).Actions) | Select-Object Execute, Arguments | ConvertTo-Json -Compress"
|
|
5699
|
+
]
|
|
5700
|
+
},
|
|
5701
|
+
parse: scheduledTaskEntrypoint
|
|
5702
|
+
},
|
|
4772
5703
|
restart: [
|
|
4773
5704
|
{
|
|
4774
5705
|
command: "schtasks.exe",
|
|
@@ -4783,13 +5714,14 @@ var serviceCandidates = (runtimePlatform, uid) => {
|
|
|
4783
5714
|
return [];
|
|
4784
5715
|
};
|
|
4785
5716
|
var defaultRunner = {
|
|
4786
|
-
run: async (command, arguments_, timeoutMs = 300000) => {
|
|
5717
|
+
run: async (command, arguments_, timeoutMs = 300000, environment) => {
|
|
4787
5718
|
const result = await execa2(command, arguments_, {
|
|
4788
5719
|
encoding: "utf8",
|
|
4789
5720
|
maxBuffer: 1024 * 1024,
|
|
4790
5721
|
timeout: timeoutMs,
|
|
4791
5722
|
windowsHide: true,
|
|
4792
5723
|
stdin: "ignore",
|
|
5724
|
+
...environment === undefined ? {} : { env: environment },
|
|
4793
5725
|
reject: false,
|
|
4794
5726
|
stripFinalNewline: false
|
|
4795
5727
|
});
|
|
@@ -4802,7 +5734,7 @@ var defaultRunner = {
|
|
|
4802
5734
|
}
|
|
4803
5735
|
};
|
|
4804
5736
|
var checked = async (runner, invocation, timeoutMs) => {
|
|
4805
|
-
const result = await runner.run(invocation.command, invocation.arguments, timeoutMs);
|
|
5737
|
+
const result = await runner.run(invocation.command, invocation.arguments, timeoutMs, invocation.environment);
|
|
4806
5738
|
if (result.exitCode === 0 || invocation.allowFailure === true) {
|
|
4807
5739
|
return result;
|
|
4808
5740
|
}
|
|
@@ -4820,7 +5752,7 @@ ${result.stderr}`))) {
|
|
|
4820
5752
|
}
|
|
4821
5753
|
return active;
|
|
4822
5754
|
};
|
|
4823
|
-
var assertServiceInstallations = async (runner, services,
|
|
5755
|
+
var assertServiceInstallations = async (runner, services, installation) => {
|
|
4824
5756
|
for (const service of services) {
|
|
4825
5757
|
if (service.entrypoint === undefined) {
|
|
4826
5758
|
continue;
|
|
@@ -4829,15 +5761,19 @@ var assertServiceInstallations = async (runner, services, canonicalEntrypoint, e
|
|
|
4829
5761
|
const serviceEntrypoint = service.entrypoint.parse(`${result.stdout}
|
|
4830
5762
|
${result.stderr}`);
|
|
4831
5763
|
if (serviceEntrypoint === undefined) {
|
|
4832
|
-
throw new Error(`Could not determine the executable used by active ${service.label}`);
|
|
5764
|
+
throw new Error(`Could not determine the executable used by active ${service.label}${service.manager === "scheduled-task" ? ". Reinstall the task with cohall service install." : ""}`);
|
|
4833
5765
|
}
|
|
4834
5766
|
const canonicalServiceEntrypoint = await realpath2(serviceEntrypoint).catch((cause) => {
|
|
4835
5767
|
throw new Error(`Could not resolve the executable used by active ${service.label}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
4836
5768
|
});
|
|
4837
|
-
if (canonicalServiceEntrypoint === canonicalEntrypoint) {
|
|
5769
|
+
if (canonicalServiceEntrypoint === installation.canonicalEntrypoint) {
|
|
5770
|
+
const servicePath = resolve2(serviceEntrypoint);
|
|
5771
|
+
const sameEntrypoint = service.manager === "scheduled-task" ? servicePath.toLowerCase() === installation.entrypoint.toLowerCase() : servicePath === installation.entrypoint;
|
|
5772
|
+
if (installation.manager === "pnpm" && !sameEntrypoint)
|
|
5773
|
+
throw new Error(`Active ${service.label} is pinned to a pnpm package directory that changes during upgrades. ${service.device ? "Run cohall service install through the global cohall command before upgrading." : `Update the relay service executable to ${installation.entrypoint} before upgrading.`}`);
|
|
4838
5774
|
continue;
|
|
4839
5775
|
}
|
|
4840
|
-
throw new Error(`Active ${service.label} uses ${serviceEntrypoint}, but this Cohall CLI uses ${entrypoint}. Run the service executable's upgrade command so its installation is updated before restart.`);
|
|
5776
|
+
throw new Error(`Active ${service.label} uses ${serviceEntrypoint}, but this Cohall CLI uses ${installation.entrypoint}. Run the service executable's upgrade command so its installation is updated before restart.`);
|
|
4841
5777
|
}
|
|
4842
5778
|
};
|
|
4843
5779
|
var readReceipt = async (path) => readFile2(path, "utf8").then((content) => Schema8.decodeUnknownOption(RestartReceipt)(JSON.parse(content))).then((decoded) => decoded._tag === "Some" ? decoded.value : undefined).catch(() => {
|
|
@@ -4907,13 +5843,19 @@ var upgrade = async (options) => {
|
|
|
4907
5843
|
const candidates = await trustedServices(serviceCandidates(runtimePlatform, uid), resolveExecutable);
|
|
4908
5844
|
const statePath = options.statePath ?? restartReceiptPath();
|
|
4909
5845
|
const previous = await readReceipt(statePath);
|
|
5846
|
+
const entrypoint = options.entrypoint ?? process.argv[1];
|
|
5847
|
+
if (entrypoint === undefined) {
|
|
5848
|
+
throw new Error("Could not resolve the Cohall executable path");
|
|
5849
|
+
}
|
|
5850
|
+
const installation = await resolvePackageInstallation(entrypoint, {
|
|
5851
|
+
runner,
|
|
5852
|
+
platform: runtimePlatform,
|
|
5853
|
+
...options.resolveExecutable === undefined ? {} : { resolveExecutable }
|
|
5854
|
+
});
|
|
4910
5855
|
if (previous?.version === options.currentVersion && (target === "latest" || target === previous.version)) {
|
|
4911
|
-
const
|
|
4912
|
-
const pending = previous.pendingServices.flatMap((id) => {
|
|
4913
|
-
const service = byId.get(id);
|
|
4914
|
-
return service === undefined ? [] : [service];
|
|
4915
|
-
});
|
|
5856
|
+
const pending = candidates.filter((service) => previous.pendingServices.includes(service.id));
|
|
4916
5857
|
const active = await activeServices(runner, pending);
|
|
5858
|
+
await assertServiceInstallations(runner, active, installation);
|
|
4917
5859
|
if (options.dryRun || !options.restart) {
|
|
4918
5860
|
return {
|
|
4919
5861
|
upgraded: previous.fromVersion !== previous.version,
|
|
@@ -4922,13 +5864,13 @@ var upgrade = async (options) => {
|
|
|
4922
5864
|
requested_version: previous.version,
|
|
4923
5865
|
package_manager: previous.packageManager,
|
|
4924
5866
|
services_restarted: previous.restartedServices,
|
|
4925
|
-
services_pending_restart:
|
|
5867
|
+
services_pending_restart: previous.pendingServices,
|
|
4926
5868
|
resumed_after_restart: false,
|
|
4927
5869
|
dry_run: options.dryRun
|
|
4928
5870
|
};
|
|
4929
5871
|
}
|
|
4930
5872
|
const activeIds = new Set(active.map((service) => service.id));
|
|
4931
|
-
const completedByRestart = pending.filter((service) => service.device && service.id === previous.restartingService && activeIds.has(service.id));
|
|
5873
|
+
const completedByRestart = pending.filter((service) => options.delegated === true && service.device && service.id === previous.restartingService && activeIds.has(service.id));
|
|
4932
5874
|
const completedIds = new Set(completedByRestart.map((service) => service.id));
|
|
4933
5875
|
const remaining = active.filter((service) => !completedIds.has(service.id));
|
|
4934
5876
|
let resumed = {
|
|
@@ -4944,8 +5886,10 @@ var upgrade = async (options) => {
|
|
|
4944
5886
|
if (remaining.length > 0) {
|
|
4945
5887
|
await writeReceipt(statePath, resumed);
|
|
4946
5888
|
resumed = await restartServices(runner, remaining, statePath, resumed, options.delegated === true);
|
|
5889
|
+
} else if (completedIds.size > 0) {
|
|
5890
|
+
await writeReceipt(statePath, resumed);
|
|
4947
5891
|
}
|
|
4948
|
-
if (resumed.
|
|
5892
|
+
if (resumed.pendingServices.length === 0) {
|
|
4949
5893
|
await rm2(statePath, { force: true });
|
|
4950
5894
|
}
|
|
4951
5895
|
return {
|
|
@@ -4955,69 +5899,104 @@ var upgrade = async (options) => {
|
|
|
4955
5899
|
requested_version: previous.version,
|
|
4956
5900
|
package_manager: previous.packageManager,
|
|
4957
5901
|
services_restarted: reportedRestartServices(resumed),
|
|
4958
|
-
services_pending_restart:
|
|
5902
|
+
services_pending_restart: resumed.pendingServices.filter((id) => id !== resumed.restartingService),
|
|
4959
5903
|
resumed_after_restart: true,
|
|
4960
5904
|
dry_run: false
|
|
4961
5905
|
};
|
|
4962
5906
|
}
|
|
4963
|
-
const entrypoint = options.entrypoint ?? process.argv[1];
|
|
4964
|
-
if (entrypoint === undefined) {
|
|
4965
|
-
throw new Error("Could not resolve the Cohall executable path");
|
|
4966
|
-
}
|
|
4967
|
-
const canonicalEntrypoint = await realpath2(entrypoint);
|
|
4968
|
-
const installation = packageInstallation(canonicalEntrypoint, entrypoint);
|
|
4969
5907
|
const services = await activeServices(runner, candidates);
|
|
4970
|
-
await assertServiceInstallations(runner, services,
|
|
4971
|
-
|
|
5908
|
+
await assertServiceInstallations(runner, services, installation);
|
|
5909
|
+
const pendingServices = [
|
|
5910
|
+
...new Set([...previous?.pendingServices ?? [], ...services.map((service) => service.id)])
|
|
5911
|
+
];
|
|
5912
|
+
const resolvePackageManager = () => {
|
|
5913
|
+
if (installation.pnpmExecutable !== undefined)
|
|
5914
|
+
return Promise.resolve(installation.pnpmExecutable);
|
|
5915
|
+
return options.resolveExecutable === undefined ? trustedExecutable(installation.manager, installation.prefix === undefined ? {} : { writableRoot: installation.prefix }) : resolveExecutable(installation.manager);
|
|
5916
|
+
};
|
|
5917
|
+
let packageManagerExecutable;
|
|
5918
|
+
let resolvedTarget = target;
|
|
5919
|
+
if (target === "latest") {
|
|
5920
|
+
packageManagerExecutable = await resolvePackageManager();
|
|
5921
|
+
if (installation.manager === "bun") {
|
|
5922
|
+
const information = await checked(runner, { command: packageManagerExecutable, arguments: ["--version"] }, 1e4);
|
|
5923
|
+
if (compareVersions(information.stdout.trim(), minimumBunLookupVersion) < 0) {
|
|
5924
|
+
throw new Error(`Latest upgrades require Bun ${minimumBunLookupVersion} or newer; upgrade Bun or use --to <exact-version>`);
|
|
5925
|
+
}
|
|
5926
|
+
}
|
|
5927
|
+
const lookup = await checked(runner, {
|
|
5928
|
+
command: packageManagerExecutable,
|
|
5929
|
+
arguments: [
|
|
5930
|
+
...installation.manager === "bun" ? ["pm"] : [],
|
|
5931
|
+
"view",
|
|
5932
|
+
...installation.manager === "pnpm" ? [] : ["--global"],
|
|
5933
|
+
...installation.prefix === undefined ? [] : ["--prefix", installation.prefix],
|
|
5934
|
+
`${packageName}@latest`,
|
|
5935
|
+
"version",
|
|
5936
|
+
"--json"
|
|
5937
|
+
],
|
|
5938
|
+
...installation.manager === "pnpm" ? { environment: pnpmEnvironment() } : {}
|
|
5939
|
+
}, 30000);
|
|
5940
|
+
const latest = JSON.parse(lookup.stdout);
|
|
5941
|
+
if (typeof latest !== "string" || parseVersion(latest) === undefined) {
|
|
5942
|
+
throw new Error(`Could not resolve ${packageName}@latest to an exact semantic version`);
|
|
5943
|
+
}
|
|
5944
|
+
resolvedTarget = latest;
|
|
5945
|
+
}
|
|
5946
|
+
let nextVersion = await installedVersion(installation.entrypoint).catch((cause) => {
|
|
5947
|
+
if (target === "latest") {
|
|
5948
|
+
throw new Error("Could not verify the installed version before upgrading latest; use --to <version> to repair this installation", { cause });
|
|
5949
|
+
}
|
|
5950
|
+
return;
|
|
5951
|
+
});
|
|
5952
|
+
if (target === "latest" && (compareVersions(resolvedTarget, options.currentVersion) < 0 || compareVersions(resolvedTarget, nextVersion ?? options.currentVersion) < 0)) {
|
|
4972
5953
|
return {
|
|
4973
5954
|
upgraded: false,
|
|
4974
5955
|
from_version: options.currentVersion,
|
|
4975
|
-
installed_version: options.currentVersion,
|
|
5956
|
+
installed_version: nextVersion ?? options.currentVersion,
|
|
4976
5957
|
requested_version: target,
|
|
4977
5958
|
package_manager: installation.manager,
|
|
4978
5959
|
services_restarted: [],
|
|
4979
|
-
services_pending_restart:
|
|
5960
|
+
services_pending_restart: [],
|
|
4980
5961
|
resumed_after_restart: false,
|
|
4981
|
-
dry_run:
|
|
5962
|
+
dry_run: options.dryRun
|
|
4982
5963
|
};
|
|
4983
5964
|
}
|
|
4984
|
-
|
|
4985
|
-
return;
|
|
4986
|
-
});
|
|
4987
|
-
if (nextVersion === undefined || target !== nextVersion) {
|
|
4988
|
-
const install = packageInstallCommand(installation, target);
|
|
4989
|
-
const installExecutable = options.resolveExecutable === undefined ? await trustedExecutable(install.command, installation.prefix === undefined ? {} : { writableRoot: installation.prefix }) : await resolveExecutable(install.command);
|
|
4990
|
-
await checked(runner, { ...install, command: installExecutable });
|
|
4991
|
-
nextVersion = await installedVersion(entrypoint);
|
|
4992
|
-
}
|
|
4993
|
-
if (target !== "latest" && nextVersion !== target) {
|
|
4994
|
-
throw new Error(`Installed Cohall ${nextVersion}, expected ${target}`);
|
|
4995
|
-
}
|
|
4996
|
-
const upgraded = nextVersion !== options.currentVersion;
|
|
4997
|
-
if (!options.restart || services.length === 0) {
|
|
4998
|
-
await rm2(statePath, { force: true });
|
|
5965
|
+
if (options.dryRun) {
|
|
4999
5966
|
return {
|
|
5000
|
-
upgraded,
|
|
5967
|
+
upgraded: false,
|
|
5001
5968
|
from_version: options.currentVersion,
|
|
5002
|
-
installed_version:
|
|
5969
|
+
installed_version: options.currentVersion,
|
|
5003
5970
|
requested_version: target,
|
|
5004
5971
|
package_manager: installation.manager,
|
|
5005
5972
|
services_restarted: [],
|
|
5006
|
-
services_pending_restart:
|
|
5973
|
+
services_pending_restart: pendingServices,
|
|
5007
5974
|
resumed_after_restart: false,
|
|
5008
|
-
dry_run:
|
|
5975
|
+
dry_run: true
|
|
5009
5976
|
};
|
|
5010
5977
|
}
|
|
5978
|
+
if (nextVersion === undefined || resolvedTarget !== nextVersion) {
|
|
5979
|
+
const install = packageInstallCommand(installation, resolvedTarget);
|
|
5980
|
+
const installExecutable = packageManagerExecutable ?? await resolvePackageManager();
|
|
5981
|
+
await checked(runner, { ...install, command: installExecutable });
|
|
5982
|
+
nextVersion = await installedVersion(installation.entrypoint);
|
|
5983
|
+
}
|
|
5984
|
+
if (nextVersion !== resolvedTarget) {
|
|
5985
|
+
throw new Error(`Installed Cohall ${nextVersion}, expected ${resolvedTarget}`);
|
|
5986
|
+
}
|
|
5987
|
+
const upgraded = nextVersion !== options.currentVersion;
|
|
5011
5988
|
const initial = {
|
|
5012
5989
|
version: nextVersion,
|
|
5013
5990
|
fromVersion: options.currentVersion,
|
|
5014
5991
|
packageManager: installation.manager,
|
|
5015
|
-
pendingServices
|
|
5992
|
+
pendingServices,
|
|
5016
5993
|
restartedServices: []
|
|
5017
5994
|
};
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5995
|
+
if (initial.pendingServices.length > 0) {
|
|
5996
|
+
await writeReceipt(statePath, initial);
|
|
5997
|
+
}
|
|
5998
|
+
const completed = options.restart ? await restartServices(runner, services, statePath, initial, options.delegated === true) : initial;
|
|
5999
|
+
if (completed.pendingServices.length === 0) {
|
|
5021
6000
|
await rm2(statePath, { force: true });
|
|
5022
6001
|
}
|
|
5023
6002
|
return {
|
|
@@ -5027,7 +6006,7 @@ var upgrade = async (options) => {
|
|
|
5027
6006
|
requested_version: target,
|
|
5028
6007
|
package_manager: installation.manager,
|
|
5029
6008
|
services_restarted: reportedRestartServices(completed),
|
|
5030
|
-
services_pending_restart:
|
|
6009
|
+
services_pending_restart: completed.pendingServices.filter((id) => id !== completed.restartingService),
|
|
5031
6010
|
resumed_after_restart: false,
|
|
5032
6011
|
dry_run: false
|
|
5033
6012
|
};
|
|
@@ -5037,20 +6016,36 @@ var upgrade = async (options) => {
|
|
|
5037
6016
|
var xml = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
5038
6017
|
var systemdArgument = (value) => `"${value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"").replaceAll("%", "%%").replaceAll(`
|
|
5039
6018
|
`, "\\n").replaceAll("\r", "\\r")}"`;
|
|
6019
|
+
var pnpmHomeDirectory = (platform4, home) => {
|
|
6020
|
+
if (process.env.PNPM_HOME)
|
|
6021
|
+
return process.env.PNPM_HOME;
|
|
6022
|
+
if (process.env.XDG_DATA_HOME)
|
|
6023
|
+
return join5(process.env.XDG_DATA_HOME, "pnpm");
|
|
6024
|
+
if (platform4 === "darwin")
|
|
6025
|
+
return join5(home, "Library", "pnpm");
|
|
6026
|
+
if (platform4 !== "win32")
|
|
6027
|
+
return join5(home, ".local", "share", "pnpm");
|
|
6028
|
+
return process.env.LOCALAPPDATA ? join5(process.env.LOCALAPPDATA, "pnpm") : join5(home, ".pnpm");
|
|
6029
|
+
};
|
|
5040
6030
|
var deviceServicePlan = (options) => {
|
|
6031
|
+
const pnpmHome = options.pnpm?.home ?? (options.platform === "darwin" ? join5(options.home, "Library", "pnpm") : join5(options.home, ".local", "share", "pnpm"));
|
|
5041
6032
|
const servicePath = [
|
|
5042
6033
|
dirname4(options.nodeExecutable),
|
|
5043
6034
|
join5(options.home, ".local", "bin"),
|
|
5044
6035
|
join5(options.home, ".npm-global", "bin"),
|
|
5045
6036
|
join5(options.home, ".bun", "bin"),
|
|
5046
|
-
options.
|
|
6037
|
+
...options.pnpm === undefined ? [] : [options.pnpm.bin],
|
|
6038
|
+
join5(pnpmHome, "bin"),
|
|
6039
|
+
pnpmHome,
|
|
5047
6040
|
...options.platform === "darwin" ? ["/opt/homebrew/bin"] : [],
|
|
5048
6041
|
"/usr/local/bin",
|
|
5049
6042
|
"/usr/bin",
|
|
5050
6043
|
"/bin"
|
|
5051
6044
|
].join(":");
|
|
5052
6045
|
if (options.platform === "linux") {
|
|
5053
|
-
const
|
|
6046
|
+
const configured = process.env.XDG_CONFIG_HOME;
|
|
6047
|
+
const configHome = configured !== undefined && isAbsolute3(configured) ? configured : join5(options.home, ".config");
|
|
6048
|
+
const path = join5(configHome, "systemd", "user", "cohall-device.service");
|
|
5054
6049
|
return {
|
|
5055
6050
|
file: {
|
|
5056
6051
|
path,
|
|
@@ -5064,7 +6059,12 @@ Wants=network-online.target
|
|
|
5064
6059
|
Type=simple
|
|
5065
6060
|
Environment=${systemdArgument(`PATH=${servicePath}`)}
|
|
5066
6061
|
Environment=${systemdArgument(`COHALL_CONFIG=${options.configPath}`)}
|
|
5067
|
-
|
|
6062
|
+
${options.pnpm === undefined ? "" : `Environment=${systemdArgument(`PNPM_HOME=${options.pnpm.home}`)}
|
|
6063
|
+
Environment=${systemdArgument(`COHALL_PNPM_EXECUTABLE=${options.pnpm.executable}`)}
|
|
6064
|
+
Environment=${systemdArgument(`COHALL_PNPM_GLOBAL_DIR=${options.pnpm.globalDir}`)}
|
|
6065
|
+
Environment=${systemdArgument(`COHALL_PNPM_STORE_DIR=${options.pnpm.store}`)}
|
|
6066
|
+
Environment=${systemdArgument(`COHALL_PNPM_GLOBAL_BIN_DIR=${options.pnpm.bin}`)}
|
|
6067
|
+
`}ExecStart=${systemdArgument(options.entrypoint)} device
|
|
5068
6068
|
Restart=always
|
|
5069
6069
|
RestartSec=3
|
|
5070
6070
|
UMask=0077
|
|
@@ -5108,7 +6108,17 @@ WantedBy=default.target
|
|
|
5108
6108
|
<string>${xml(servicePath)}</string>
|
|
5109
6109
|
<key>COHALL_CONFIG</key>
|
|
5110
6110
|
<string>${xml(options.configPath)}</string>
|
|
5111
|
-
|
|
6111
|
+
${options.pnpm === undefined ? "" : ` <key>PNPM_HOME</key>
|
|
6112
|
+
<string>${xml(options.pnpm.home)}</string>
|
|
6113
|
+
<key>COHALL_PNPM_EXECUTABLE</key>
|
|
6114
|
+
<string>${xml(options.pnpm.executable)}</string>
|
|
6115
|
+
<key>COHALL_PNPM_STORE_DIR</key>
|
|
6116
|
+
<string>${xml(options.pnpm.store)}</string>
|
|
6117
|
+
<key>COHALL_PNPM_GLOBAL_DIR</key>
|
|
6118
|
+
<string>${xml(options.pnpm.globalDir)}</string>
|
|
6119
|
+
<key>COHALL_PNPM_GLOBAL_BIN_DIR</key>
|
|
6120
|
+
<string>${xml(options.pnpm.bin)}</string>
|
|
6121
|
+
`} </dict>
|
|
5112
6122
|
<key>RunAtLoad</key>
|
|
5113
6123
|
<true/>
|
|
5114
6124
|
<key>KeepAlive</key>
|
|
@@ -5138,8 +6148,8 @@ WantedBy=default.target
|
|
|
5138
6148
|
var defaultRunner2 = {
|
|
5139
6149
|
run: async (command, arguments_, timeoutMs = 60000) => {
|
|
5140
6150
|
const executable = await trustedExecutable(command);
|
|
5141
|
-
return new Promise((
|
|
5142
|
-
execFile2(executable, [...arguments_], { encoding: "utf8", maxBuffer: 1024 * 1024, timeout: timeoutMs, windowsHide: true }, (error, stdout, stderr) =>
|
|
6151
|
+
return new Promise((resolve3) => {
|
|
6152
|
+
execFile2(executable, [...arguments_], { encoding: "utf8", maxBuffer: 1024 * 1024, timeout: timeoutMs, windowsHide: true }, (error, stdout, stderr) => resolve3({
|
|
5143
6153
|
exitCode: typeof error?.code === "number" ? error.code : error === null ? 0 : 1,
|
|
5144
6154
|
stdout,
|
|
5145
6155
|
stderr,
|
|
@@ -5156,12 +6166,22 @@ var checked2 = async (runner, command, arguments_, allowFailure = false) => {
|
|
|
5156
6166
|
throw new Error(`${command} failed: ${result.stderr.trim() || result.stdout.trim() || result.error || `status ${result.exitCode}`}`);
|
|
5157
6167
|
};
|
|
5158
6168
|
var installDeviceService = async (options = {}) => {
|
|
5159
|
-
const entrypoint = await
|
|
5160
|
-
|
|
6169
|
+
const { entrypoint, canonicalEntrypoint, pnpmExecutable, globalDir, globalRoot } = await resolvePackageInstallation(options.entrypoint ?? process.argv[1] ?? "", {
|
|
6170
|
+
...options.platform === undefined ? {} : { platform: options.platform },
|
|
6171
|
+
...options.runner === undefined ? {} : { runner: options.runner },
|
|
6172
|
+
...options.resolveExecutable === undefined ? {} : { resolveExecutable: options.resolveExecutable }
|
|
6173
|
+
});
|
|
5161
6174
|
const platform4 = options.platform ?? process.platform;
|
|
6175
|
+
const home = options.home ?? homedir3();
|
|
6176
|
+
const pnpm = pnpmExecutable === undefined || globalDir === undefined || globalRoot === undefined ? undefined : {
|
|
6177
|
+
home: pnpmHomeDirectory(platform4, home),
|
|
6178
|
+
executable: pnpmExecutable,
|
|
6179
|
+
globalDir,
|
|
6180
|
+
...await pnpmServiceDirectories(pnpmExecutable, globalRoot, options.runner)
|
|
6181
|
+
};
|
|
5162
6182
|
const runner = options.runner ?? defaultRunner2;
|
|
5163
6183
|
if (platform4 === "win32") {
|
|
5164
|
-
const script = join5(dirname4(dirname4(
|
|
6184
|
+
const script = join5(dirname4(dirname4(canonicalEntrypoint)), "deploy", "windows", "install-device.ps1");
|
|
5165
6185
|
await checked2(runner, "powershell.exe", [
|
|
5166
6186
|
"-NoProfile",
|
|
5167
6187
|
"-ExecutionPolicy",
|
|
@@ -5173,7 +6193,19 @@ var installDeviceService = async (options = {}) => {
|
|
|
5173
6193
|
"-Entrypoint",
|
|
5174
6194
|
entrypoint,
|
|
5175
6195
|
"-ConfigurationPath",
|
|
5176
|
-
configurationPath()
|
|
6196
|
+
configurationPath(),
|
|
6197
|
+
...pnpm === undefined ? [] : [
|
|
6198
|
+
"-PnpmHome",
|
|
6199
|
+
pnpm.home,
|
|
6200
|
+
"-PnpmExecutable",
|
|
6201
|
+
pnpm.executable,
|
|
6202
|
+
"-PnpmStoreDir",
|
|
6203
|
+
pnpm.store,
|
|
6204
|
+
"-PnpmGlobalDir",
|
|
6205
|
+
pnpm.globalDir,
|
|
6206
|
+
"-PnpmGlobalBinDir",
|
|
6207
|
+
pnpm.bin
|
|
6208
|
+
]
|
|
5177
6209
|
]);
|
|
5178
6210
|
return { installed: "scheduled-task:Cohall Device" };
|
|
5179
6211
|
}
|
|
@@ -5181,9 +6213,10 @@ var installDeviceService = async (options = {}) => {
|
|
|
5181
6213
|
const plan = deviceServicePlan({
|
|
5182
6214
|
platform: platform4,
|
|
5183
6215
|
entrypoint,
|
|
5184
|
-
home
|
|
6216
|
+
home,
|
|
5185
6217
|
nodeExecutable: process.execPath,
|
|
5186
6218
|
configPath: configurationPath(),
|
|
6219
|
+
...pnpm === undefined ? {} : { pnpm },
|
|
5187
6220
|
...uid === undefined ? {} : { uid }
|
|
5188
6221
|
});
|
|
5189
6222
|
await mkdir4(dirname4(plan.file.path), { recursive: true, mode: 448 });
|
|
@@ -5200,36 +6233,18 @@ var installDeviceService = async (options = {}) => {
|
|
|
5200
6233
|
var restartDeviceService = async (options = {}) => {
|
|
5201
6234
|
const platform4 = options.platform ?? process.platform;
|
|
5202
6235
|
const runner = options.runner ?? defaultRunner2;
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
return { running: true, restarted: true, service };
|
|
5211
|
-
}
|
|
5212
|
-
if (platform4 === "darwin") {
|
|
5213
|
-
const service = "com.cohall.device";
|
|
5214
|
-
const target = `gui/${options.uid ?? process.getuid?.() ?? 0}/${service}`;
|
|
5215
|
-
const check = await runner.run("launchctl", ["print", target], 1e4);
|
|
5216
|
-
if (check.exitCode !== 0) {
|
|
5217
|
-
return { running: false, restarted: false };
|
|
5218
|
-
}
|
|
5219
|
-
await checked2(runner, "launchctl", ["kickstart", "-k", target]);
|
|
5220
|
-
return { running: true, restarted: true, service };
|
|
6236
|
+
const service = serviceCandidates(platform4, options.uid ?? process.getuid?.()).find((candidate) => candidate.device);
|
|
6237
|
+
if (service === undefined)
|
|
6238
|
+
return { running: false, restarted: false };
|
|
6239
|
+
const check = await runner.run(service.check.command, service.check.arguments, 1e4);
|
|
6240
|
+
if (check.exitCode !== 0 || service.activeOutput !== undefined && !service.activeOutput.test(`${check.stdout}
|
|
6241
|
+
${check.stderr}`)) {
|
|
6242
|
+
return { running: false, restarted: false };
|
|
5221
6243
|
}
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
const check = await runner.run("schtasks.exe", ["/Query", "/TN", service], 1e4);
|
|
5225
|
-
if (check.exitCode !== 0) {
|
|
5226
|
-
return { running: false, restarted: false };
|
|
5227
|
-
}
|
|
5228
|
-
await checked2(runner, "schtasks.exe", ["/End", "/TN", service], true);
|
|
5229
|
-
await checked2(runner, "schtasks.exe", ["/Run", "/TN", service]);
|
|
5230
|
-
return { running: true, restarted: true, service };
|
|
6244
|
+
for (const command of service.restart) {
|
|
6245
|
+
await checked2(runner, command.command, command.arguments, command.allowFailure);
|
|
5231
6246
|
}
|
|
5232
|
-
return { running:
|
|
6247
|
+
return { running: true, restarted: true, service: service.label };
|
|
5233
6248
|
};
|
|
5234
6249
|
|
|
5235
6250
|
// apps/device/src/relay-migration.ts
|
|
@@ -5266,7 +6281,7 @@ var verifyBackupDestinationParent = async (path) => {
|
|
|
5266
6281
|
if (process.platform === "win32") {
|
|
5267
6282
|
return;
|
|
5268
6283
|
}
|
|
5269
|
-
for (let current = await
|
|
6284
|
+
for (let current = await realpath3(path);; current = dirname5(current)) {
|
|
5270
6285
|
const metadata = await stat3(current);
|
|
5271
6286
|
const writableByOthers = (metadata.mode & 18) !== 0;
|
|
5272
6287
|
const sticky = (metadata.mode & 512) !== 0;
|
|
@@ -5310,7 +6325,7 @@ var backupRelay = async (destinationInput) => {
|
|
|
5310
6325
|
if (!await exists(sourceDatabase)) {
|
|
5311
6326
|
throw new Error(`Relay database was not found at ${sourceDatabase}`);
|
|
5312
6327
|
}
|
|
5313
|
-
const destination =
|
|
6328
|
+
const destination = resolve3(destinationInput);
|
|
5314
6329
|
if (await exists(destination)) {
|
|
5315
6330
|
throw new Error(`Backup destination already exists: ${destination}`);
|
|
5316
6331
|
}
|
|
@@ -5357,7 +6372,7 @@ var backupRelay = async (destinationInput) => {
|
|
|
5357
6372
|
}
|
|
5358
6373
|
};
|
|
5359
6374
|
var restoreRelay = async (sourceInput) => {
|
|
5360
|
-
const source =
|
|
6375
|
+
const source = resolve3(sourceInput);
|
|
5361
6376
|
const sourceDatabase = join6(source, "cohall.db");
|
|
5362
6377
|
const sourceOwnerToken = join6(source, "owner-token");
|
|
5363
6378
|
const sourceManifest = join6(source, "manifest.json");
|
|
@@ -5502,9 +6517,9 @@ var switchRelay = async (options) => {
|
|
|
5502
6517
|
}
|
|
5503
6518
|
let service = { running: false, restarted: false };
|
|
5504
6519
|
let warning;
|
|
5505
|
-
if (
|
|
6520
|
+
if (options.restart && deviceTokens.length > 0) {
|
|
5506
6521
|
service = await (options.restartService ?? restartDeviceService)().catch((cause) => {
|
|
5507
|
-
warning = `Relay URL
|
|
6522
|
+
warning = `Relay URL is saved, but the device service could not restart: ${cause instanceof Error ? cause.message : String(cause)}. Retry cohall relay use ${relayUrl} to restart an active worker.`;
|
|
5508
6523
|
return { running: true, restarted: false };
|
|
5509
6524
|
});
|
|
5510
6525
|
}
|
|
@@ -5521,9 +6536,33 @@ var switchRelay = async (options) => {
|
|
|
5521
6536
|
init_src();
|
|
5522
6537
|
import { Effect as Effect9, Schema as Schema10 } from "effect";
|
|
5523
6538
|
import { constants as constants3 } from "node:fs";
|
|
5524
|
-
import {
|
|
6539
|
+
import {
|
|
6540
|
+
lstat as lstat2,
|
|
6541
|
+
mkdtemp as mkdtemp3,
|
|
6542
|
+
mkdir as mkdir6,
|
|
6543
|
+
open as open3,
|
|
6544
|
+
readdir as readdir2,
|
|
6545
|
+
rm as rm4,
|
|
6546
|
+
writeFile as writeFile6
|
|
6547
|
+
} from "node:fs/promises";
|
|
5525
6548
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
5526
6549
|
import { basename as basename2, join as join7 } from "node:path";
|
|
6550
|
+
var readBoundedFile = async (handle) => {
|
|
6551
|
+
const buffer = Buffer.allocUnsafe(maxAttachmentBytes + 1);
|
|
6552
|
+
let offset = 0;
|
|
6553
|
+
while (offset < buffer.length) {
|
|
6554
|
+
const { bytesRead } = await handle.read({
|
|
6555
|
+
buffer,
|
|
6556
|
+
offset,
|
|
6557
|
+
length: buffer.length - offset,
|
|
6558
|
+
position: null
|
|
6559
|
+
});
|
|
6560
|
+
if (bytesRead === 0)
|
|
6561
|
+
break;
|
|
6562
|
+
offset += bytesRead;
|
|
6563
|
+
}
|
|
6564
|
+
return buffer.subarray(0, offset);
|
|
6565
|
+
};
|
|
5527
6566
|
var readInputAttachments = async (paths2) => {
|
|
5528
6567
|
if (paths2.length > maxTaskAttachments) {
|
|
5529
6568
|
throw new Error(`A task supports at most ${maxTaskAttachments} input files`);
|
|
@@ -5546,7 +6585,7 @@ var readInputAttachments = async (paths2) => {
|
|
|
5546
6585
|
if (!metadata.isFile() || metadata.dev !== source.dev || metadata.ino !== source.ino || metadata.size < 1 || metadata.size > maxAttachmentBytes) {
|
|
5547
6586
|
throw new Error(`Attachment ${name} must be a regular file of 1 to ${maxAttachmentBytes} bytes`);
|
|
5548
6587
|
}
|
|
5549
|
-
const data = await handle
|
|
6588
|
+
const data = await readBoundedFile(handle);
|
|
5550
6589
|
if (data.length < 1 || data.length > maxAttachmentBytes) {
|
|
5551
6590
|
throw new Error(`Attachment ${name} changed size while reading`);
|
|
5552
6591
|
}
|
|
@@ -5556,31 +6595,32 @@ var readInputAttachments = async (paths2) => {
|
|
|
5556
6595
|
}
|
|
5557
6596
|
}));
|
|
5558
6597
|
};
|
|
5559
|
-
var prepareTaskFiles =
|
|
6598
|
+
var prepareTaskFiles = Effect9.fn("TaskFiles.prepare")(function* (configuration, taskId, inputNames) {
|
|
5560
6599
|
const client = exports_src.make({ baseUrl: configuration.relayUrl, token: configuration.token });
|
|
5561
|
-
const root =
|
|
6600
|
+
const root = yield* Effect9.acquireRelease(Effect9.tryPromise({
|
|
6601
|
+
try: () => mkdtemp3(join7(tmpdir2(), "cohall-task-")),
|
|
6602
|
+
catch: (cause) => cause
|
|
6603
|
+
}), (root2) => Effect9.promise(() => rm4(root2, { recursive: true, force: true })));
|
|
5562
6604
|
const input = join7(root, "input");
|
|
5563
6605
|
const output = join7(root, "output");
|
|
5564
|
-
try
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
output,
|
|
5573
|
-
inputNames,
|
|
5574
|
-
collectOutputs: () => collectOutputs(output),
|
|
5575
|
-
cleanup: () => rm4(root, { recursive: true, force: true })
|
|
5576
|
-
};
|
|
5577
|
-
} catch (cause) {
|
|
5578
|
-
await rm4(root, { recursive: true, force: true });
|
|
5579
|
-
throw cause;
|
|
6606
|
+
yield* Effect9.tryPromise({ try: () => mkdir6(input), catch: (cause) => cause }).pipe(Effect9.uninterruptible);
|
|
6607
|
+
yield* Effect9.tryPromise({ try: () => mkdir6(output), catch: (cause) => cause }).pipe(Effect9.uninterruptible);
|
|
6608
|
+
for (const name of inputNames) {
|
|
6609
|
+
const data = yield* client.readAttachment(taskId, name);
|
|
6610
|
+
yield* Effect9.tryPromise({
|
|
6611
|
+
try: () => writeFile6(join7(input, name), data, { flag: "wx", mode: 384 }),
|
|
6612
|
+
catch: (cause) => cause
|
|
6613
|
+
}).pipe(Effect9.uninterruptible);
|
|
5580
6614
|
}
|
|
5581
|
-
|
|
6615
|
+
return {
|
|
6616
|
+
input,
|
|
6617
|
+
output,
|
|
6618
|
+
inputNames,
|
|
6619
|
+
collectOutputs: () => collectOutputs(output)
|
|
6620
|
+
};
|
|
6621
|
+
});
|
|
5582
6622
|
var collectOutputs = async (directory) => {
|
|
5583
|
-
const entries = await
|
|
6623
|
+
const entries = await readdir2(directory, { withFileTypes: true });
|
|
5584
6624
|
if (entries.length > maxTaskAttachments) {
|
|
5585
6625
|
throw new Error(`A task can return at most ${maxTaskAttachments} files`);
|
|
5586
6626
|
}
|
|
@@ -5603,7 +6643,7 @@ var collectOutputs = async (directory) => {
|
|
|
5603
6643
|
if (!metadata.isFile() || !pathMetadata.isFile() || metadata.dev !== pathMetadata.dev || metadata.ino !== pathMetadata.ino || metadata.size < 1 || metadata.size > maxAttachmentBytes) {
|
|
5604
6644
|
throw new Error(`Output ${name} must be a regular file of 1 to ${maxAttachmentBytes} bytes`);
|
|
5605
6645
|
}
|
|
5606
|
-
const data = await handle
|
|
6646
|
+
const data = await readBoundedFile(handle);
|
|
5607
6647
|
if (data.length < 1 || data.length > maxAttachmentBytes) {
|
|
5608
6648
|
throw new Error(`Output ${name} changed size while reading`);
|
|
5609
6649
|
}
|
|
@@ -5653,6 +6693,7 @@ var valueOptions = new Set([
|
|
|
5653
6693
|
"attach",
|
|
5654
6694
|
"context",
|
|
5655
6695
|
"context-file",
|
|
6696
|
+
"deadline",
|
|
5656
6697
|
"direction",
|
|
5657
6698
|
"grok-gateway",
|
|
5658
6699
|
"error",
|
|
@@ -5665,9 +6706,12 @@ var valueOptions = new Set([
|
|
|
5665
6706
|
"parent",
|
|
5666
6707
|
"prompt",
|
|
5667
6708
|
"prompt-file",
|
|
6709
|
+
"question",
|
|
6710
|
+
"request-id",
|
|
5668
6711
|
"provider",
|
|
5669
6712
|
"providers",
|
|
5670
6713
|
"relay",
|
|
6714
|
+
"run-id",
|
|
5671
6715
|
"sandbox",
|
|
5672
6716
|
"target",
|
|
5673
6717
|
"thread",
|
|
@@ -5704,11 +6748,13 @@ Usage:
|
|
|
5704
6748
|
cohall config
|
|
5705
6749
|
cohall devices
|
|
5706
6750
|
cohall progress [task-id] --message <note>
|
|
6751
|
+
cohall request-input [task-id] --question <text> [--run-id uuid]
|
|
6752
|
+
cohall answer <task-id> --request-id uuid --message <text>
|
|
5707
6753
|
cohall bots
|
|
5708
6754
|
cohall send [@device-or-bot] [prompt] [delegate options]
|
|
5709
6755
|
cohall delegate [prompt] [--target @device-or-bot] [--provider provider]
|
|
5710
6756
|
[--context text] [--thread uuid] [--parent task-id] [--workspace path]
|
|
5711
|
-
[--attach path] [--timeout seconds] [--no-wait]
|
|
6757
|
+
[--attach path] [--request-id uuid] [--timeout seconds] [--deadline UTC-timestamp] [--no-wait]
|
|
5712
6758
|
cohall status <task-id>
|
|
5713
6759
|
cohall inbox [ack <task-id>]
|
|
5714
6760
|
cohall trace <task-id> [--follow]
|
|
@@ -5716,7 +6762,7 @@ Usage:
|
|
|
5716
6762
|
cohall attachments <task-id>
|
|
5717
6763
|
cohall download <task-id> <name> --output path [--direction input|output]
|
|
5718
6764
|
cohall cancel <task-id>
|
|
5719
|
-
cohall reply <task-id> --message-file path | --message text | --message - | --error text
|
|
6765
|
+
cohall reply <task-id> [--run-id uuid] --message-file path | --message text | --message - | --error text | --question text
|
|
5720
6766
|
cohall thread <thread-id>
|
|
5721
6767
|
cohall pair [--label name] [--client-only]
|
|
5722
6768
|
cohall sessions
|
|
@@ -5911,17 +6957,21 @@ var printSkill = () => console.log(SKILL_default.trimEnd());
|
|
|
5911
6957
|
var runCli = async (command, raw) => {
|
|
5912
6958
|
const arguments_ = parseArguments(raw);
|
|
5913
6959
|
if (command === "reply") {
|
|
5914
|
-
allowOptions(arguments_, ["message", "message-file", "error"]);
|
|
6960
|
+
allowOptions(arguments_, ["message", "message-file", "error", "question", "run-id"]);
|
|
5915
6961
|
const id = Schema11.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
|
|
5916
6962
|
const error = option(arguments_, "error");
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
6963
|
+
const question = option(arguments_, "question");
|
|
6964
|
+
const rawRunId = option(arguments_, "run-id");
|
|
6965
|
+
const runId = rawRunId === undefined ? undefined : Schema11.decodeUnknownSync(TaskRunId)(rawRunId);
|
|
6966
|
+
if ((error !== undefined || question !== undefined) && (option(arguments_, "message") !== undefined || option(arguments_, "message-file") !== undefined || error !== undefined && question !== undefined)) {
|
|
6967
|
+
throw new Error("Use one of --question, --error, or --message/--message-file");
|
|
6968
|
+
}
|
|
6969
|
+
const message = error === undefined && question === undefined ? await readInput(arguments_, "message") : undefined;
|
|
6970
|
+
if (error === undefined && question === undefined && message === undefined) {
|
|
6971
|
+
throw new Error("A bot reply requires --message, --message-file, --error, or --question");
|
|
6972
|
+
}
|
|
6973
|
+
const reply = question !== undefined ? { question: Schema11.decodeUnknownSync(RequestTaskInput.fields.question)(question) } : error === undefined ? { result: message ?? "" } : { error };
|
|
6974
|
+
await writeBotReply(id, reply, runId);
|
|
5925
6975
|
print({ submitted: true, task_id: id });
|
|
5926
6976
|
return;
|
|
5927
6977
|
}
|
|
@@ -6172,7 +7222,7 @@ var runCli = async (command, raw) => {
|
|
|
6172
7222
|
const sandbox = option(arguments_, "sandbox");
|
|
6173
7223
|
const model = option(arguments_, "model") ?? existing?.model;
|
|
6174
7224
|
const gatewayInput = option(arguments_, "grok-gateway") ?? existing?.grokGateway;
|
|
6175
|
-
const grokGateway = gatewayInput === undefined ? undefined :
|
|
7225
|
+
const grokGateway = gatewayInput === undefined ? undefined : resolve4(gatewayInput);
|
|
6176
7226
|
const providerInput = option(arguments_, "providers");
|
|
6177
7227
|
const providers = providerInput === undefined ? existing?.providers : providerInput === "auto" ? undefined : parseProviders(providerInput);
|
|
6178
7228
|
const configuration2 = StoredConfiguration.make({
|
|
@@ -6276,9 +7326,9 @@ var runCli = async (command, raw) => {
|
|
|
6276
7326
|
...hasDeviceCredential && response?.ok === true && clientAuthentication.status !== "error" && currentDevice?.status !== "online" && currentDevice?.status !== "busy" ? [
|
|
6277
7327
|
clientToken === undefined ? "Device status cannot be checked without a client credential" : "Device daemon is not connected or is not registered with the relay"
|
|
6278
7328
|
] : [],
|
|
6279
|
-
...selectedProviders
|
|
7329
|
+
...(selectedProviders ?? (grokGateway === undefined ? [] : ["grok-bot"])).flatMap((provider) => provider === "grok-bot" ? grokBots === undefined ? [
|
|
6280
7330
|
"Configured Grok Bot gateway is unavailable; check its discovery file and host process"
|
|
6281
|
-
] : [] : providerExecutables[provider] === "not found" ? [`Configured provider ${provider} is not installed or not on PATH`] : [])
|
|
7331
|
+
] : [] : providerExecutables[provider] === "not found" ? [`Configured provider ${provider} is not installed or not on PATH`] : [])
|
|
6282
7332
|
];
|
|
6283
7333
|
print({
|
|
6284
7334
|
version,
|
|
@@ -6354,6 +7404,31 @@ var runCli = async (command, raw) => {
|
|
|
6354
7404
|
return;
|
|
6355
7405
|
}
|
|
6356
7406
|
const { configuration, relay } = await client();
|
|
7407
|
+
if (command === "request-input") {
|
|
7408
|
+
allowOptions(arguments_, ["question", "run-id"]);
|
|
7409
|
+
const rawQuestion = option(arguments_, "question");
|
|
7410
|
+
if (rawQuestion === undefined)
|
|
7411
|
+
throw new Error("--question is required");
|
|
7412
|
+
const question = Schema11.decodeUnknownSync(RequestTaskInput.fields.question)(rawQuestion);
|
|
7413
|
+
const id = arguments_.positionals.length === 0 ? undefined : Schema11.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
|
|
7414
|
+
const rawRunId = option(arguments_, "run-id");
|
|
7415
|
+
const runId = rawRunId === undefined ? undefined : Schema11.decodeUnknownSync(TaskRunId)(rawRunId);
|
|
7416
|
+
print(await Effect10.runPromise(requestInput(relay, configuration, question, id, runId)));
|
|
7417
|
+
return;
|
|
7418
|
+
}
|
|
7419
|
+
if (command === "answer") {
|
|
7420
|
+
allowOptions(arguments_, ["request-id", "message", "message-file"]);
|
|
7421
|
+
const id = Schema11.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
|
|
7422
|
+
const requestId = option(arguments_, "request-id");
|
|
7423
|
+
if (requestId === undefined)
|
|
7424
|
+
throw new Error("--request-id is required; use the input request id from status or inbox");
|
|
7425
|
+
const input = Schema11.decodeUnknownSync(AnswerTaskInput)({
|
|
7426
|
+
requestId,
|
|
7427
|
+
answer: await readInput(arguments_, "message")
|
|
7428
|
+
});
|
|
7429
|
+
print(taskResult(await Effect10.runPromise(relay.answerTaskInput(id, input))));
|
|
7430
|
+
return;
|
|
7431
|
+
}
|
|
6357
7432
|
if (command === "progress") {
|
|
6358
7433
|
allowOptions(arguments_, ["message"]);
|
|
6359
7434
|
const rawId = arguments_.positionals.length === 0 ? configuration.mcpTaskId : identifier(arguments_, "task id");
|
|
@@ -6382,12 +7457,14 @@ var runCli = async (command, raw) => {
|
|
|
6382
7457
|
allowOptions(arguments_, [
|
|
6383
7458
|
"context",
|
|
6384
7459
|
"context-file",
|
|
7460
|
+
"deadline",
|
|
6385
7461
|
"attach",
|
|
6386
7462
|
"no-wait",
|
|
6387
7463
|
"parent",
|
|
6388
7464
|
"prompt",
|
|
6389
7465
|
"prompt-file",
|
|
6390
7466
|
"provider",
|
|
7467
|
+
"request-id",
|
|
6391
7468
|
"target",
|
|
6392
7469
|
"thread",
|
|
6393
7470
|
"timeout",
|
|
@@ -6417,15 +7494,19 @@ var runCli = async (command, raw) => {
|
|
|
6417
7494
|
const workspace = option(arguments_, "workspace");
|
|
6418
7495
|
const provider = option(arguments_, "provider");
|
|
6419
7496
|
const parent = option(arguments_, "parent");
|
|
7497
|
+
const deadline = option(arguments_, "deadline");
|
|
7498
|
+
const requestId = option(arguments_, "request-id");
|
|
6420
7499
|
const task = await Effect10.runPromise(createDelegation(relay, configuration, {
|
|
6421
7500
|
prompt,
|
|
7501
|
+
...requestId === undefined ? {} : { requestId: Schema11.decodeUnknownSync(TaskRequestId)(requestId) },
|
|
6422
7502
|
...target === undefined ? {} : { target },
|
|
6423
7503
|
...context === undefined ? {} : { context },
|
|
6424
7504
|
...thread === undefined ? {} : { threadId: Schema11.decodeUnknownSync(ThreadId)(thread) },
|
|
6425
7505
|
...parent === undefined ? {} : { parentTaskId: Schema11.decodeUnknownSync(TaskId)(parent) },
|
|
6426
7506
|
...workspace === undefined ? {} : { workspace },
|
|
6427
7507
|
...provider === undefined ? {} : { provider: Schema11.decodeUnknownSync(Provider)(provider) },
|
|
6428
|
-
...attachments2.length === 0 ? {} : { attachments: attachments2 }
|
|
7508
|
+
...attachments2.length === 0 ? {} : { attachments: attachments2 },
|
|
7509
|
+
...deadline === undefined ? {} : { expiresAt: Schema11.decodeUnknownSync(TaskDeadline)(deadline) }
|
|
6429
7510
|
}));
|
|
6430
7511
|
const completed = arguments_.options.has("no-wait") ? task : await Effect10.runPromise(waitForTask(relay, task, waitSeconds));
|
|
6431
7512
|
print(arguments_.options.has("no-wait") ? taskResult(completed) : await acknowledgedTaskResult(relay, completed));
|
|
@@ -6471,7 +7552,7 @@ var runCli = async (command, raw) => {
|
|
|
6471
7552
|
const direction = directionInput === undefined ? undefined : Schema11.decodeUnknownSync(AttachmentDirection)(directionInput);
|
|
6472
7553
|
const data = await Effect10.runPromise(relay.readAttachment(id, name, direction));
|
|
6473
7554
|
await writeFile7(destination, data, { flag: "wx", mode: 384 });
|
|
6474
|
-
print({ task_id: id, name, path:
|
|
7555
|
+
print({ task_id: id, name, path: resolve4(destination), bytes: data.length });
|
|
6475
7556
|
return;
|
|
6476
7557
|
}
|
|
6477
7558
|
if (command === "trace") {
|
|
@@ -6487,8 +7568,7 @@ var runCli = async (command, raw) => {
|
|
|
6487
7568
|
if (command === "wait") {
|
|
6488
7569
|
allowOptions(arguments_, ["timeout"]);
|
|
6489
7570
|
const id = Schema11.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
|
|
6490
|
-
|
|
6491
|
-
print(taskResult(await Effect10.runPromise(waitForTask(relay, task, timeout(arguments_)))));
|
|
7571
|
+
print(taskResult(await Effect10.runPromise(waitForTask(relay, id, timeout(arguments_)))));
|
|
6492
7572
|
return;
|
|
6493
7573
|
}
|
|
6494
7574
|
if (command === "cancel") {
|
|
@@ -6510,9 +7590,9 @@ var runCli = async (command, raw) => {
|
|
|
6510
7590
|
init_src();
|
|
6511
7591
|
import { Effect as Effect11, Schedule as Schedule3, Schema as Schema12 } from "effect";
|
|
6512
7592
|
import { constants as constants4 } from "node:fs";
|
|
6513
|
-
import { open as open4, realpath as
|
|
7593
|
+
import { open as open4, realpath as realpath4, stat as stat5 } from "node:fs/promises";
|
|
6514
7594
|
import { arch, hostname as hostname3, platform as platform4 } from "node:os";
|
|
6515
|
-
import { basename as basename3, isAbsolute as
|
|
7595
|
+
import { basename as basename3, isAbsolute as isAbsolute4, relative as relative2 } from "node:path";
|
|
6516
7596
|
import { WebSocket as WebSocket2 } from "ws";
|
|
6517
7597
|
var maxQueuedRelayMessages = 8;
|
|
6518
7598
|
|
|
@@ -6545,6 +7625,8 @@ var capabilities = (providers) => {
|
|
|
6545
7625
|
if (providers.some((provider) => provider !== "grok-bot")) {
|
|
6546
7626
|
values2.push({ id: "task-attachments", label: "Task file attachments" });
|
|
6547
7627
|
}
|
|
7628
|
+
values2.push({ id: "task-clarification", label: "Task clarification and resume" });
|
|
7629
|
+
values2.push({ id: "task-deadlines", label: "Task deadlines" });
|
|
6548
7630
|
if (findExecutable("google-chrome") !== undefined || findExecutable("chromium") !== undefined || platform4() === "darwin") {
|
|
6549
7631
|
values2.push({ id: "browser-session", label: "Signed-in browser" });
|
|
6550
7632
|
}
|
|
@@ -6588,10 +7670,10 @@ var allowedWorkspace = async (configuration, requested) => {
|
|
|
6588
7670
|
if (selected === undefined) {
|
|
6589
7671
|
throw new Error("No workspace is configured on this device");
|
|
6590
7672
|
}
|
|
6591
|
-
const candidate = await
|
|
7673
|
+
const candidate = await realpath4(selected);
|
|
6592
7674
|
const allowed = configuration.workspaces.some((root) => {
|
|
6593
7675
|
const child = relative2(root, candidate);
|
|
6594
|
-
return child === "" || !child.startsWith("..") && !
|
|
7676
|
+
return child === "" || !child.startsWith("..") && !isAbsolute4(child);
|
|
6595
7677
|
});
|
|
6596
7678
|
if (!allowed) {
|
|
6597
7679
|
throw new Error(`Workspace ${candidate} is outside this device's configured workspace roots`);
|
|
@@ -6617,7 +7699,7 @@ var openAllowedWorkspace = async (configuration, requested) => {
|
|
|
6617
7699
|
throw new Error(`Workspace ${path} changed before provider startup`);
|
|
6618
7700
|
}
|
|
6619
7701
|
if (operatingSystem2 !== "linux") {
|
|
6620
|
-
const currentPath = await
|
|
7702
|
+
const currentPath = await realpath4(path);
|
|
6621
7703
|
const currentPathIdentity = await stat5(currentPath);
|
|
6622
7704
|
if (currentPathIdentity.dev !== identity.dev || currentPathIdentity.ino !== identity.ino) {
|
|
6623
7705
|
throw new Error(`Workspace ${path} changed before provider startup`);
|
|
@@ -6627,7 +7709,7 @@ var openAllowedWorkspace = async (configuration, requested) => {
|
|
|
6627
7709
|
close: () => handle.close()
|
|
6628
7710
|
};
|
|
6629
7711
|
};
|
|
6630
|
-
var promptFor = (task, deviceName, files) => {
|
|
7712
|
+
var promptFor = (task, deviceName, supportsClarification, files) => {
|
|
6631
7713
|
const context = task.context === undefined ? "" : `
|
|
6632
7714
|
|
|
6633
7715
|
Relevant context supplied by the sending agent:
|
|
@@ -6638,6 +7720,9 @@ ${task.context}`;
|
|
|
6638
7720
|
"Never read, reveal, copy, or use Cohall configuration files or Cohall authentication tokens.",
|
|
6639
7721
|
"Return a concise, complete result with the evidence the sending agent needs.",
|
|
6640
7722
|
"For long tasks, optionally report a brief milestone with `cohall progress --message 'Running tests'`. Do not include logs or secrets in progress notes.",
|
|
7723
|
+
...supportsClarification ? [
|
|
7724
|
+
"If essential information is missing, run `cohall request-input --question 'Your question'` or the task_request_input MCP tool, then end this turn immediately. Cohall will pause the task and resume it when the sender answers. Do not guess, keep working, or report a final result after requesting input."
|
|
7725
|
+
] : [],
|
|
6641
7726
|
...files === undefined ? [] : [
|
|
6642
7727
|
...files.inputNames.length === 0 ? [] : [
|
|
6643
7728
|
`Input directory: ${files.input}`,
|
|
@@ -6648,7 +7733,13 @@ ${task.context}`;
|
|
|
6648
7733
|
],
|
|
6649
7734
|
`
|
|
6650
7735
|
Task:
|
|
6651
|
-
${task.prompt}${context}
|
|
7736
|
+
${task.prompt}${context}`,
|
|
7737
|
+
...(task.clarifications ?? []).flatMap((question) => question.answer === undefined ? [] : [
|
|
7738
|
+
`Clarification question:
|
|
7739
|
+
${question.question}
|
|
7740
|
+
Sender's answer:
|
|
7741
|
+
${question.answer.text}`
|
|
7742
|
+
])
|
|
6652
7743
|
].join(`
|
|
6653
7744
|
`);
|
|
6654
7745
|
};
|
|
@@ -6657,6 +7748,7 @@ var send = (state, event) => {
|
|
|
6657
7748
|
state.socket.send(JSON.stringify(event));
|
|
6658
7749
|
}
|
|
6659
7750
|
};
|
|
7751
|
+
var taskRunKey = (taskId, runId) => `${taskId}:${runId ?? ""}`;
|
|
6660
7752
|
var omitOutputFiles = (event, note) => {
|
|
6661
7753
|
if (event._tag !== "TaskFinished" || (event.attachments?.length ?? 0) === 0) {
|
|
6662
7754
|
return event;
|
|
@@ -6664,16 +7756,17 @@ var omitOutputFiles = (event, note) => {
|
|
|
6664
7756
|
const message = `
|
|
6665
7757
|
|
|
6666
7758
|
[Output files omitted: ${note}]`;
|
|
6667
|
-
return SocketEvent.make({
|
|
7759
|
+
return SocketEvent.cases.TaskFinished.make({
|
|
6668
7760
|
_tag: "TaskFinished",
|
|
6669
7761
|
taskId: event.taskId,
|
|
7762
|
+
...event.runId === undefined ? {} : { runId: event.runId },
|
|
6670
7763
|
result: `${event.result.slice(0, 131072 - message.length)}${message}`,
|
|
6671
7764
|
...event.providerSessionId === undefined ? {} : { providerSessionId: event.providerSessionId }
|
|
6672
7765
|
});
|
|
6673
7766
|
};
|
|
6674
|
-
var sendTerminal = (state,
|
|
7767
|
+
var sendTerminal = (state, event) => {
|
|
6675
7768
|
const safeEvent = state.supportsAttachments ? event : omitOutputFiles(event, "the relay does not support attachments. Upgrade the relay and retry.");
|
|
6676
|
-
state.terminal.set(taskId, safeEvent);
|
|
7769
|
+
state.terminal.set(taskRunKey(event.taskId, event.runId), safeEvent);
|
|
6677
7770
|
if (state.socket?.readyState === WebSocket2.OPEN) {
|
|
6678
7771
|
state.socket.send(JSON.stringify(safeEvent));
|
|
6679
7772
|
}
|
|
@@ -6685,12 +7778,12 @@ var sendOperationTerminal = (state, operationId, event) => {
|
|
|
6685
7778
|
state.socket.send(payload);
|
|
6686
7779
|
}
|
|
6687
7780
|
};
|
|
6688
|
-
var remember = (state,
|
|
6689
|
-
state.completed.add(
|
|
7781
|
+
var remember = (state, task) => {
|
|
7782
|
+
state.completed.add(taskRunKey(task.id, task.runId));
|
|
6690
7783
|
if (state.completed.size <= 1000) {
|
|
6691
7784
|
return;
|
|
6692
7785
|
}
|
|
6693
|
-
const oldest = state.completed.
|
|
7786
|
+
const oldest = state.completed.keys().next();
|
|
6694
7787
|
if (!oldest.done) {
|
|
6695
7788
|
state.completed.delete(oldest.value);
|
|
6696
7789
|
state.terminal.delete(oldest.value);
|
|
@@ -6708,8 +7801,9 @@ var rememberOperation = (state, operationId) => {
|
|
|
6708
7801
|
}
|
|
6709
7802
|
};
|
|
6710
7803
|
var sessionKey = (task) => `${task.threadId}:${task.provider}`;
|
|
7804
|
+
var hasCompletedRun = (state, task) => state.completed.has(taskRunKey(task.id, task.runId));
|
|
6711
7805
|
var drain = (configuration, state) => {
|
|
6712
|
-
if (state.operation !== undefined) {
|
|
7806
|
+
if (state.stopping || state.operation !== undefined) {
|
|
6713
7807
|
return;
|
|
6714
7808
|
}
|
|
6715
7809
|
if (state.tasks.size === 0) {
|
|
@@ -6732,16 +7826,37 @@ var drain = (configuration, state) => {
|
|
|
6732
7826
|
}
|
|
6733
7827
|
};
|
|
6734
7828
|
var execute = (configuration, state, task) => {
|
|
6735
|
-
if (state.tasks.has(task.id) || state
|
|
7829
|
+
if (state.tasks.has(task.id) || hasCompletedRun(state, task)) {
|
|
6736
7830
|
return;
|
|
6737
7831
|
}
|
|
6738
7832
|
const controller = new AbortController;
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
7833
|
+
let deadlineTimer;
|
|
7834
|
+
const enforceDeadline = () => {
|
|
7835
|
+
if (task.expiresAt === undefined)
|
|
7836
|
+
return;
|
|
7837
|
+
const remaining = Date.parse(task.expiresAt) - Date.now();
|
|
7838
|
+
if (remaining <= 0) {
|
|
7839
|
+
controller.abort();
|
|
7840
|
+
return;
|
|
7841
|
+
}
|
|
7842
|
+
deadlineTimer = setTimeout(enforceDeadline, Math.min(remaining, 2147483647));
|
|
7843
|
+
};
|
|
7844
|
+
enforceDeadline();
|
|
7845
|
+
send(state, SocketEvent.make({
|
|
7846
|
+
_tag: "TaskAccepted",
|
|
7847
|
+
taskId: task.id,
|
|
7848
|
+
...task.runId === undefined ? {} : { runId: task.runId }
|
|
7849
|
+
}));
|
|
7850
|
+
const workflow = Effect11.scoped(Effect11.gen(function* () {
|
|
7851
|
+
if (configuration.providers !== undefined && !configuration.providers.includes(task.provider)) {
|
|
7852
|
+
return yield* new ProviderRunError({
|
|
7853
|
+
provider: task.provider,
|
|
7854
|
+
message: `${task.provider} is not enabled on this device. Update its configured providers and restart the worker before retrying.`
|
|
7855
|
+
});
|
|
7856
|
+
}
|
|
6742
7857
|
if (task.provider === "grok-bot") {
|
|
6743
7858
|
return yield* Effect11.tryPromise({
|
|
6744
|
-
try: (signal) => runGrokBot(configuration.grokGateway, task, signal),
|
|
7859
|
+
try: (signal) => runGrokBot(configuration.grokGateway, task, signal, state.supportsClarification),
|
|
6745
7860
|
catch: (cause) => new ProviderRunError({
|
|
6746
7861
|
provider: task.provider,
|
|
6747
7862
|
message: cause instanceof Error ? cause.message : String(cause)
|
|
@@ -6755,68 +7870,84 @@ var execute = (configuration, state, task) => {
|
|
|
6755
7870
|
message: "The relay does not support task file attachments"
|
|
6756
7871
|
});
|
|
6757
7872
|
}
|
|
6758
|
-
const workspace = yield* Effect11.tryPromise({
|
|
7873
|
+
const workspace = yield* Effect11.acquireRelease(Effect11.tryPromise({
|
|
6759
7874
|
try: () => openAllowedWorkspace(configuration, task.workspace),
|
|
6760
7875
|
catch: (cause) => new ProviderRunError({
|
|
6761
7876
|
provider,
|
|
6762
7877
|
message: cause instanceof Error ? cause.message : String(cause)
|
|
6763
7878
|
})
|
|
6764
|
-
})
|
|
6765
|
-
return yield* Effect11.gen(function* () {
|
|
6766
|
-
const files = state.supportsAttachments ? yield* Effect11.tryPromise({
|
|
6767
|
-
try: (signal) => prepareTaskFiles(configuration, task.id, task.inputAttachmentNames ?? [], signal),
|
|
6768
|
-
catch: (cause) => new ProviderRunError({
|
|
6769
|
-
provider,
|
|
6770
|
-
message: cause instanceof Error ? cause.message : String(cause)
|
|
6771
|
-
})
|
|
6772
|
-
}) : undefined;
|
|
6773
|
-
const sessionId = task.providerSessionId ?? state.sessions.get(sessionKey(task));
|
|
6774
|
-
return yield* run({
|
|
6775
|
-
provider,
|
|
6776
|
-
threadId: task.threadId,
|
|
6777
|
-
taskId: task.id,
|
|
6778
|
-
prompt: promptFor(task, configuration.name, files),
|
|
6779
|
-
cwd: workspace.cwd,
|
|
6780
|
-
beforeSpawn: workspace.validate,
|
|
6781
|
-
...sessionId === undefined ? {} : { sessionId },
|
|
6782
|
-
...configuration.model === undefined ? {} : { model: configuration.model },
|
|
6783
|
-
...configuration.sandbox === undefined ? {} : { sandbox: configuration.sandbox }
|
|
6784
|
-
}).pipe(Effect11.flatMap((result) => files === undefined ? Effect11.succeed(result) : Effect11.tryPromise({
|
|
6785
|
-
try: async () => ({ ...result, attachments: await files.collectOutputs() }),
|
|
6786
|
-
catch: (cause) => new ProviderRunError({
|
|
6787
|
-
provider,
|
|
6788
|
-
message: cause instanceof Error ? cause.message : String(cause)
|
|
6789
|
-
})
|
|
6790
|
-
})), Effect11.ensuring(files === undefined ? Effect11.void : Effect11.promise(() => files.cleanup())));
|
|
6791
|
-
}).pipe(Effect11.ensuring(Effect11.promise(() => workspace.close().catch(() => {
|
|
7879
|
+
}), (workspace2) => Effect11.promise(() => workspace2.close().catch(() => {
|
|
6792
7880
|
return;
|
|
6793
|
-
})))
|
|
6794
|
-
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
|
|
7881
|
+
})));
|
|
7882
|
+
const files = state.supportsAttachments ? yield* prepareTaskFiles(configuration, task.id, task.inputAttachmentNames ?? []).pipe(Effect11.mapError((cause) => new ProviderRunError({
|
|
7883
|
+
provider,
|
|
7884
|
+
message: cause instanceof Error ? cause.message : String(cause)
|
|
7885
|
+
}))) : undefined;
|
|
7886
|
+
const sessionId = task.providerSessionId ?? state.sessions.get(sessionKey(task));
|
|
7887
|
+
return yield* run({
|
|
7888
|
+
provider,
|
|
7889
|
+
threadId: task.threadId,
|
|
6798
7890
|
taskId: task.id,
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
6802
|
-
|
|
7891
|
+
prompt: promptFor(task, configuration.name, state.supportsClarification, files),
|
|
7892
|
+
cwd: workspace.cwd,
|
|
7893
|
+
beforeSpawn: workspace.validate,
|
|
7894
|
+
...sessionId === undefined ? {} : { sessionId },
|
|
7895
|
+
...task.runId === undefined ? {} : { runId: task.runId },
|
|
7896
|
+
...configuration.model === undefined ? {} : { model: configuration.model },
|
|
7897
|
+
...configuration.sandbox === undefined ? {} : { sandbox: configuration.sandbox }
|
|
7898
|
+
}).pipe(Effect11.flatMap((result) => files === undefined ? Effect11.succeed(result) : Effect11.tryPromise({
|
|
7899
|
+
try: async () => ({ ...result, attachments: await files.collectOutputs() }),
|
|
7900
|
+
catch: (cause) => new ProviderRunError({
|
|
7901
|
+
provider,
|
|
7902
|
+
message: cause instanceof Error ? cause.message : String(cause)
|
|
7903
|
+
})
|
|
7904
|
+
}).pipe(Effect11.uninterruptible)));
|
|
7905
|
+
}));
|
|
7906
|
+
const done = Effect11.runPromise(workflow, { signal: controller.signal }).then((result) => {
|
|
7907
|
+
let finished;
|
|
7908
|
+
if (result.question !== undefined) {
|
|
7909
|
+
if (!state.supportsClarification || task.runId === undefined)
|
|
7910
|
+
throw new Error("Upgrade the relay before requesting clarification");
|
|
7911
|
+
finished = SocketEvent.cases.TaskInputRequested.make({
|
|
7912
|
+
_tag: "TaskInputRequested",
|
|
7913
|
+
taskId: task.id,
|
|
7914
|
+
runId: task.runId,
|
|
7915
|
+
question: result.question
|
|
7916
|
+
});
|
|
7917
|
+
} else {
|
|
7918
|
+
finished = SocketEvent.cases.TaskFinished.make({
|
|
7919
|
+
_tag: "TaskFinished",
|
|
7920
|
+
taskId: task.id,
|
|
7921
|
+
...task.runId === undefined ? {} : { runId: task.runId },
|
|
7922
|
+
result: result.result,
|
|
7923
|
+
...result.sessionId === undefined ? {} : { providerSessionId: result.sessionId },
|
|
7924
|
+
...result.attachments === undefined || result.attachments.length === 0 ? {} : { attachments: result.attachments }
|
|
7925
|
+
});
|
|
7926
|
+
}
|
|
6803
7927
|
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;
|
|
6804
7928
|
if (result.sessionId !== undefined) {
|
|
6805
7929
|
state.sessions.set(sessionKey(task), result.sessionId);
|
|
6806
7930
|
}
|
|
6807
|
-
remember(state, task
|
|
6808
|
-
sendTerminal(state,
|
|
7931
|
+
remember(state, task);
|
|
7932
|
+
sendTerminal(state, bounded2);
|
|
6809
7933
|
}).catch((cause) => {
|
|
6810
|
-
remember(state, task
|
|
6811
|
-
sendTerminal(state,
|
|
7934
|
+
remember(state, task);
|
|
7935
|
+
sendTerminal(state, controller.signal.aborted ? SocketEvent.cases.TaskCancelled.make({
|
|
7936
|
+
_tag: "TaskCancelled",
|
|
7937
|
+
taskId: task.id,
|
|
7938
|
+
...task.runId === undefined ? {} : { runId: task.runId }
|
|
7939
|
+
}) : SocketEvent.cases.TaskFailed.make({
|
|
6812
7940
|
_tag: "TaskFailed",
|
|
6813
7941
|
taskId: task.id,
|
|
7942
|
+
...task.runId === undefined ? {} : { runId: task.runId },
|
|
6814
7943
|
error: (cause instanceof Error ? cause.message : String(cause)).slice(0, 16384)
|
|
6815
7944
|
}));
|
|
6816
7945
|
}).finally(() => {
|
|
7946
|
+
clearTimeout(deadlineTimer);
|
|
6817
7947
|
state.tasks.delete(task.id);
|
|
6818
7948
|
drain(configuration, state);
|
|
6819
7949
|
});
|
|
7950
|
+
state.tasks.set(task.id, { task, controller, done });
|
|
6820
7951
|
};
|
|
6821
7952
|
var executeOperation = (configuration, state, operation) => {
|
|
6822
7953
|
if (state.operation !== undefined || state.completedOperations.has(operation.id)) {
|
|
@@ -6844,6 +7975,8 @@ var executeOperation = (configuration, state, operation) => {
|
|
|
6844
7975
|
});
|
|
6845
7976
|
};
|
|
6846
7977
|
var scheduleOperation = (configuration, state, operation) => {
|
|
7978
|
+
if (state.stopping)
|
|
7979
|
+
return;
|
|
6847
7980
|
if (state.completedOperations.has(operation.id) || state.operation?.id === operation.id || state.operationQueue.some((pending) => pending.id === operation.id)) {
|
|
6848
7981
|
return;
|
|
6849
7982
|
}
|
|
@@ -6865,7 +7998,18 @@ var performDeviceOperation = (operation, currentVersion, runUpgrade = upgrade) =
|
|
|
6865
7998
|
delegated: true
|
|
6866
7999
|
}).then((result) => JSON.stringify(result));
|
|
6867
8000
|
var schedule = (configuration, state, task) => {
|
|
6868
|
-
if (state.
|
|
8001
|
+
if (state.stopping)
|
|
8002
|
+
return;
|
|
8003
|
+
if (hasCompletedRun(state, task) || state.queue.some((queued) => queued.id === task.id && queued.runId === task.runId)) {
|
|
8004
|
+
return;
|
|
8005
|
+
}
|
|
8006
|
+
const running = state.tasks.get(task.id);
|
|
8007
|
+
if (running !== undefined && running.task.runId === task.runId) {
|
|
8008
|
+
send(state, SocketEvent.make({
|
|
8009
|
+
_tag: "TaskAccepted",
|
|
8010
|
+
taskId: task.id,
|
|
8011
|
+
...task.runId === undefined ? {} : { runId: task.runId }
|
|
8012
|
+
}));
|
|
6869
8013
|
return;
|
|
6870
8014
|
}
|
|
6871
8015
|
if (task.providerSessionId !== undefined) {
|
|
@@ -6881,20 +8025,24 @@ var schedule = (configuration, state, task) => {
|
|
|
6881
8025
|
}
|
|
6882
8026
|
execute(configuration, state, task);
|
|
6883
8027
|
};
|
|
6884
|
-
var cancel = (state, taskId) => {
|
|
8028
|
+
var cancel = (state, taskId, runId) => {
|
|
6885
8029
|
const running = state.tasks.get(taskId);
|
|
6886
|
-
if (running !== undefined) {
|
|
8030
|
+
if (running !== undefined && (runId === undefined || running.task.runId === runId)) {
|
|
6887
8031
|
if (running.task.provider !== "grok-bot") {
|
|
6888
8032
|
running.controller.abort();
|
|
6889
8033
|
}
|
|
6890
8034
|
return;
|
|
6891
8035
|
}
|
|
6892
|
-
const index = state.queue.findIndex((task) => task.id === taskId);
|
|
8036
|
+
const index = state.queue.findIndex((task) => task.id === taskId && (runId === undefined || task.runId === runId));
|
|
6893
8037
|
if (index !== -1) {
|
|
6894
8038
|
state.queue.splice(index, 1);
|
|
6895
8039
|
}
|
|
6896
|
-
remember(state, taskId);
|
|
6897
|
-
sendTerminal(state,
|
|
8040
|
+
remember(state, { id: taskId, ...runId === undefined ? {} : { runId } });
|
|
8041
|
+
sendTerminal(state, SocketEvent.cases.TaskCancelled.make({
|
|
8042
|
+
_tag: "TaskCancelled",
|
|
8043
|
+
taskId,
|
|
8044
|
+
...runId === undefined ? {} : { runId }
|
|
8045
|
+
}));
|
|
6898
8046
|
};
|
|
6899
8047
|
var connect = (configuration, state) => Effect11.tryPromise({
|
|
6900
8048
|
try: (signal) => new Promise((complete) => {
|
|
@@ -6903,6 +8051,15 @@ var connect = (configuration, state) => Effect11.tryPromise({
|
|
|
6903
8051
|
perMessageDeflate: false,
|
|
6904
8052
|
allowSynchronousEvents: false
|
|
6905
8053
|
});
|
|
8054
|
+
let closed = false;
|
|
8055
|
+
let connectionDeadline = setTimeout(() => socket.terminate(), 1e4);
|
|
8056
|
+
const resetConnectionDeadline = () => {
|
|
8057
|
+
if (closed || state.socket !== socket)
|
|
8058
|
+
return;
|
|
8059
|
+
clearTimeout(connectionDeadline);
|
|
8060
|
+
connectionDeadline = setTimeout(() => socket.terminate(), 45000);
|
|
8061
|
+
};
|
|
8062
|
+
socket.on("ping", resetConnectionDeadline);
|
|
6906
8063
|
let bots = [];
|
|
6907
8064
|
let refreshing = false;
|
|
6908
8065
|
const refreshBots = async () => {
|
|
@@ -6930,13 +8087,13 @@ var connect = (configuration, state) => Effect11.tryPromise({
|
|
|
6930
8087
|
}));
|
|
6931
8088
|
refreshBots();
|
|
6932
8089
|
}, 15000);
|
|
6933
|
-
let closed = false;
|
|
6934
8090
|
let queuedMessages = 0;
|
|
6935
8091
|
const close = () => {
|
|
6936
8092
|
if (closed) {
|
|
6937
8093
|
return;
|
|
6938
8094
|
}
|
|
6939
8095
|
closed = true;
|
|
8096
|
+
clearTimeout(connectionDeadline);
|
|
6940
8097
|
clearInterval(heartbeat);
|
|
6941
8098
|
if (state.socket === socket) {
|
|
6942
8099
|
state.socket = undefined;
|
|
@@ -6944,7 +8101,7 @@ var connect = (configuration, state) => Effect11.tryPromise({
|
|
|
6944
8101
|
complete();
|
|
6945
8102
|
};
|
|
6946
8103
|
signal.addEventListener("abort", () => {
|
|
6947
|
-
socket.
|
|
8104
|
+
socket.terminate();
|
|
6948
8105
|
close();
|
|
6949
8106
|
}, { once: true });
|
|
6950
8107
|
socket.once("open", () => {
|
|
@@ -6967,14 +8124,16 @@ var connect = (configuration, state) => Effect11.tryPromise({
|
|
|
6967
8124
|
}).pipe(Effect11.flatMap(decodeSocketEvent)));
|
|
6968
8125
|
if (event._tag === "Connected") {
|
|
6969
8126
|
state.supportsAttachments = event.taskAttachments === true;
|
|
8127
|
+
state.supportsClarification = event.taskClarification === true;
|
|
6970
8128
|
state.socket = socket;
|
|
8129
|
+
resetConnectionDeadline();
|
|
6971
8130
|
socket.send(JSON.stringify(SocketEvent.make({
|
|
6972
8131
|
_tag: "DeviceHello",
|
|
6973
8132
|
device: describeDevice(configuration, state.tasks.size > 0 || state.operation !== undefined ? "busy" : "online", bots)
|
|
6974
8133
|
})));
|
|
6975
|
-
for (const [
|
|
8134
|
+
for (const [key, pending] of state.terminal) {
|
|
6976
8135
|
const safeEvent = state.supportsAttachments ? pending : omitOutputFiles(pending, "the relay does not support attachments. Upgrade the relay and retry.");
|
|
6977
|
-
state.terminal.set(
|
|
8136
|
+
state.terminal.set(key, safeEvent);
|
|
6978
8137
|
socket.send(JSON.stringify(safeEvent));
|
|
6979
8138
|
}
|
|
6980
8139
|
for (const payload of state.operationTerminal.values()) {
|
|
@@ -6988,12 +8147,12 @@ var connect = (configuration, state) => Effect11.tryPromise({
|
|
|
6988
8147
|
return;
|
|
6989
8148
|
}
|
|
6990
8149
|
if (event._tag === "CancelTask") {
|
|
6991
|
-
cancel(state, event.taskId);
|
|
8150
|
+
cancel(state, event.taskId, event.runId);
|
|
6992
8151
|
return;
|
|
6993
8152
|
}
|
|
6994
8153
|
if (event._tag === "TaskSettled") {
|
|
6995
|
-
state.terminal.delete(event.taskId);
|
|
6996
|
-
cleanupBotReply(event.taskId).catch(() => {
|
|
8154
|
+
state.terminal.delete(taskRunKey(event.taskId, event.runId));
|
|
8155
|
+
cleanupBotReply(event.taskId, event.runId).catch(() => {
|
|
6997
8156
|
console.error(`Could not remove local bot reply for settled task ${event.taskId}`);
|
|
6998
8157
|
});
|
|
6999
8158
|
return;
|
|
@@ -7025,7 +8184,9 @@ var connect = (configuration, state) => Effect11.tryPromise({
|
|
|
7025
8184
|
var runDaemon = (configuration) => {
|
|
7026
8185
|
const state = {
|
|
7027
8186
|
socket: undefined,
|
|
8187
|
+
stopping: false,
|
|
7028
8188
|
supportsAttachments: false,
|
|
8189
|
+
supportsClarification: false,
|
|
7029
8190
|
processing: Promise.resolve(),
|
|
7030
8191
|
terminal: new Map,
|
|
7031
8192
|
queue: [],
|
|
@@ -7037,11 +8198,14 @@ var runDaemon = (configuration) => {
|
|
|
7037
8198
|
operationTerminal: new Map,
|
|
7038
8199
|
completedOperations: new Set
|
|
7039
8200
|
};
|
|
7040
|
-
return connect(configuration, state).pipe(Effect11.repeat({ schedule: Schedule3.spaced("2 seconds") }), Effect11.ensuring(Effect11.
|
|
8201
|
+
return connect(configuration, state).pipe(Effect11.repeat({ schedule: Schedule3.spaced("2 seconds") }), Effect11.ensuring(Effect11.promise(async () => {
|
|
8202
|
+
state.stopping = true;
|
|
7041
8203
|
state.socket?.close();
|
|
7042
|
-
|
|
8204
|
+
const running = [...state.tasks.values()];
|
|
8205
|
+
for (const { controller } of running) {
|
|
7043
8206
|
controller.abort();
|
|
7044
8207
|
}
|
|
8208
|
+
await Promise.allSettled(running.map(({ done }) => done));
|
|
7045
8209
|
})));
|
|
7046
8210
|
};
|
|
7047
8211
|
|
|
@@ -7113,6 +8277,24 @@ var runMcp = async (configuration) => {
|
|
|
7113
8277
|
token: configuration.token
|
|
7114
8278
|
});
|
|
7115
8279
|
const server = new McpServer({ name: "cohall", version });
|
|
8280
|
+
server.registerTool("task_request_input", {
|
|
8281
|
+
title: "Ask for task clarification",
|
|
8282
|
+
description: "Ask the original sender for essential missing information. After this succeeds, end the current worker turn immediately. Cohall pauses when the turn exits and resumes the same task after an answer. Inherits the delegated task and turn when omitted.",
|
|
8283
|
+
inputSchema: {
|
|
8284
|
+
task_id: z.string().uuid().optional(),
|
|
8285
|
+
run_id: z.string().uuid().optional(),
|
|
8286
|
+
question: z.string().min(1).max(maxClarificationBytes)
|
|
8287
|
+
}
|
|
8288
|
+
}, async ({ task_id, run_id, question }) => output(await Effect12.runPromise(requestInput(client2, configuration, Schema13.decodeUnknownSync(RequestTaskInput.fields.question)(question), task_id === undefined ? undefined : Schema13.decodeUnknownSync(TaskId)(task_id), run_id === undefined ? undefined : Schema13.decodeUnknownSync(TaskRunId)(run_id)))));
|
|
8289
|
+
server.registerTool("task_answer", {
|
|
8290
|
+
title: "Answer a worker's question",
|
|
8291
|
+
description: "Answer the current clarification and resume the same task. Use input_request.id from task_status or delegate as request_id, or inputRequest.id from completion_inbox. Only the original requester or relay owner can answer. Use wait_task to collect the resumed result.",
|
|
8292
|
+
inputSchema: {
|
|
8293
|
+
task_id: z.string().uuid(),
|
|
8294
|
+
request_id: z.string().uuid(),
|
|
8295
|
+
answer: z.string().min(1).max(maxClarificationBytes)
|
|
8296
|
+
}
|
|
8297
|
+
}, async ({ task_id, request_id, answer }) => output(taskResult(await Effect12.runPromise(client2.answerTaskInput(Schema13.decodeUnknownSync(TaskId)(task_id), Schema13.decodeUnknownSync(AnswerTaskInput)({ requestId: request_id, answer }))))));
|
|
7116
8298
|
server.registerTool("task_progress", {
|
|
7117
8299
|
title: "Report task progress",
|
|
7118
8300
|
description: `Replace the latest progress note for a running task on this device. Use brief milestones, up to ${maxProgressNoteBytes} UTF-8 bytes. Inherits the delegated task ID when omitted.`,
|
|
@@ -7142,6 +8324,7 @@ var runMcp = async (configuration) => {
|
|
|
7142
8324
|
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.",
|
|
7143
8325
|
inputSchema: {
|
|
7144
8326
|
prompt: z.string().min(1).max(131072),
|
|
8327
|
+
request_id: z.string().uuid().optional().describe("A UUID v4 generated before submitting. Reuse with identical input and the same client credential after a lost response to recover the original task. Use stable device IDs or device/bot IDs for retries. For thread follow-ups, select a target or provider explicitly; select Bots explicitly."),
|
|
7145
8328
|
target: z.string().optional().describe("Device name, hostname or ID; @BotName; or @device/BotName. Use list_bots targets to disambiguate."),
|
|
7146
8329
|
provider: z.enum(Provider.literals).optional().describe("Inferred as grok-bot for a bot target; otherwise defaults to codex."),
|
|
7147
8330
|
context: z.string().max(131072).optional().describe("Distilled context the target needs: why the user is asking, relevant facts and prior findings, constraints, and the intended decision. The calling agent must supply this when the prompt depends on its conversation; omit it only for a self-contained task."),
|
|
@@ -7150,10 +8333,12 @@ var runMcp = async (configuration) => {
|
|
|
7150
8333
|
workspace: z.string().max(4096).optional().describe("Coding provider workspace. Omit for named Grok Bots."),
|
|
7151
8334
|
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."),
|
|
7152
8335
|
wait: z.boolean().default(true),
|
|
7153
|
-
timeout_seconds: z.number().int().min(5).max(86400).default(900)
|
|
8336
|
+
timeout_seconds: z.number().int().min(5).max(86400).default(900),
|
|
8337
|
+
deadline: z.string().optional().describe("Absolute ISO-8601 UTC deadline for coding work, preserved through clarification. Overdue work stops; timeout_seconds only limits waiting.")
|
|
7154
8338
|
}
|
|
7155
8339
|
}, async ({
|
|
7156
8340
|
prompt,
|
|
8341
|
+
request_id,
|
|
7157
8342
|
target,
|
|
7158
8343
|
provider,
|
|
7159
8344
|
context,
|
|
@@ -7162,25 +8347,28 @@ var runMcp = async (configuration) => {
|
|
|
7162
8347
|
workspace,
|
|
7163
8348
|
attachment_paths,
|
|
7164
8349
|
wait,
|
|
7165
|
-
timeout_seconds
|
|
7166
|
-
|
|
8350
|
+
timeout_seconds,
|
|
8351
|
+
deadline
|
|
8352
|
+
}, { signal }) => {
|
|
7167
8353
|
const attachments2 = await readInputAttachments(attachment_paths ?? []);
|
|
7168
8354
|
const task = await Effect12.runPromise(createDelegation(client2, configuration, {
|
|
7169
8355
|
prompt,
|
|
8356
|
+
...request_id === undefined ? {} : { requestId: Schema13.decodeUnknownSync(TaskRequestId)(request_id) },
|
|
7170
8357
|
...provider === undefined ? {} : { provider },
|
|
7171
8358
|
...target === undefined ? {} : { target },
|
|
7172
8359
|
...context === undefined ? {} : { context },
|
|
7173
8360
|
...thread_id === undefined ? {} : { threadId: ThreadId.make(thread_id) },
|
|
7174
8361
|
...parent_task_id === undefined ? {} : { parentTaskId: TaskId.make(parent_task_id) },
|
|
7175
8362
|
...workspace === undefined ? {} : { workspace },
|
|
7176
|
-
...attachments2.length === 0 ? {} : { attachments: attachments2 }
|
|
7177
|
-
|
|
7178
|
-
|
|
7179
|
-
|
|
8363
|
+
...attachments2.length === 0 ? {} : { attachments: attachments2 },
|
|
8364
|
+
...deadline === undefined ? {} : { expiresAt: Schema13.decodeUnknownSync(TaskDeadline)(deadline) }
|
|
8365
|
+
}), { signal });
|
|
8366
|
+
const completed = wait ? await Effect12.runPromise(waitForTask(client2, task, timeout_seconds), { signal }) : task;
|
|
8367
|
+
return output(wait ? await acknowledgedTaskResult(client2, completed, signal) : taskResult(completed));
|
|
7180
8368
|
});
|
|
7181
8369
|
server.registerTool("completion_inbox", {
|
|
7182
|
-
title: "List completed work",
|
|
7183
|
-
description: "List up to 20 unacknowledged completed tasks sent by this client. hasMore signals additional entries.
|
|
8370
|
+
title: "List pending questions and completed work",
|
|
8371
|
+
description: "List up to 20 unanswered clarification requests and unacknowledged completed tasks sent by this client. hasMore signals additional entries. Answer questions with task_answer; use task_status for full results and acknowledge only handled completions.",
|
|
7184
8372
|
inputSchema: {}
|
|
7185
8373
|
}, async () => output(await Effect12.runPromise(client2.inbox())));
|
|
7186
8374
|
server.registerTool("acknowledge_completion", {
|
|
@@ -7221,18 +8409,19 @@ var runMcp = async (configuration) => {
|
|
|
7221
8409
|
}, async ({ task_id }) => output(await Effect12.runPromise(client2.traceTask(TaskId.make(task_id)))));
|
|
7222
8410
|
server.registerTool("wait_task", {
|
|
7223
8411
|
title: "Wait for delegated work",
|
|
7224
|
-
description: "Wait for a Cohall task to finish
|
|
8412
|
+
description: "Wait for a Cohall task to finish or need input. Answer a needs_input result with task_answer, then wait again.",
|
|
7225
8413
|
inputSchema: {
|
|
7226
8414
|
task_id: z.string().uuid(),
|
|
7227
8415
|
timeout_seconds: z.number().int().min(5).max(86400).default(900)
|
|
7228
8416
|
}
|
|
7229
|
-
}, async ({ task_id, timeout_seconds }) => {
|
|
7230
|
-
|
|
7231
|
-
|
|
8417
|
+
}, async ({ task_id, timeout_seconds }, { signal }) => {
|
|
8418
|
+
return output(taskResult(await Effect12.runPromise(waitForTask(client2, TaskId.make(task_id), timeout_seconds), {
|
|
8419
|
+
signal
|
|
8420
|
+
})));
|
|
7232
8421
|
});
|
|
7233
8422
|
server.registerTool("cancel_task", {
|
|
7234
8423
|
title: "Cancel delegated work",
|
|
7235
|
-
description: "
|
|
8424
|
+
description: "Cancel coding work, a paused task, or a Bot task that has never been dispatched. Active Grok Bot tasks must be stopped in Grok Bot.",
|
|
7236
8425
|
inputSchema: { task_id: z.string().uuid() }
|
|
7237
8426
|
}, async ({ task_id }) => output(taskResult(await Effect12.runPromise(client2.cancelTask(TaskId.make(task_id))))));
|
|
7238
8427
|
server.registerTool("thread_context", {
|
|
@@ -7269,7 +8458,19 @@ var main = async () => {
|
|
|
7269
8458
|
}
|
|
7270
8459
|
if (command === "device") {
|
|
7271
8460
|
const configuration = await Effect16.runPromise(loadDeviceConfiguration);
|
|
7272
|
-
|
|
8461
|
+
const controller = new AbortController;
|
|
8462
|
+
const stop = () => controller.abort();
|
|
8463
|
+
process.on("SIGINT", stop);
|
|
8464
|
+
process.on("SIGTERM", stop);
|
|
8465
|
+
try {
|
|
8466
|
+
await Effect16.runPromise(runDaemon(configuration), { signal: controller.signal }).catch((cause) => {
|
|
8467
|
+
if (!controller.signal.aborted)
|
|
8468
|
+
throw cause;
|
|
8469
|
+
});
|
|
8470
|
+
} finally {
|
|
8471
|
+
process.off("SIGINT", stop);
|
|
8472
|
+
process.off("SIGTERM", stop);
|
|
8473
|
+
}
|
|
7273
8474
|
return;
|
|
7274
8475
|
}
|
|
7275
8476
|
if (command === "mcp") {
|
|
@@ -7284,4 +8485,4 @@ await main().catch((cause) => {
|
|
|
7284
8485
|
process.exitCode = 1;
|
|
7285
8486
|
});
|
|
7286
8487
|
|
|
7287
|
-
//# debugId=
|
|
8488
|
+
//# debugId=4195FFD0F10A67E564756E2164756E21
|