@akshar5/cohall 0.8.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 +53 -0
- package/README.md +84 -17
- package/bin/cohall.js +1922 -544
- package/bin/cohall.js.map +23 -22
- 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 +188 -7
- 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.8.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
|
-
}, Task, TaskInboxItem, TaskInbox, taskSlot = (task) => task.provider === "grok-bot" ? `grok-bot:${task.botId}` : "cli", upgradeVersion, CreateUpgradeOperationsInput, DeviceOperation, TaskStatusCounts, ProviderUsage, DeviceUsage, UsageSummary, TaskTraceEvent, TaskTrace, ThreadContext, CreateTaskInput, SocketEvent, ErrorResponse, terminalTaskStatuses, isTerminalTask = (task) => terminalTaskStatuses.has(task.status), now = () => Timestamp.make(new Date().toISOString()), makeDeviceId = () => DeviceId.make(crypto.randomUUID()), makeThreadId = () => ThreadId.make(crypto.randomUUID()), makeMessageId = () => MessageId.make(crypto.randomUUID()), makeTaskId = () => TaskId.make(crypto.randomUUID()), makeAuthSessionId = () => AuthSessionId.make(crypto.randomUUID()), makeOperationId = () => OperationId.make(crypto.randomUUID()), decodeCreatePairingInput, decodeExchangePairingInput, decodeCreateTaskInput = (input) => Schema.decodeUnknownEffect(CreateTaskInput)(input).pipe(Effect.map((task) => ({
|
|
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({
|
|
@@ -149,8 +162,25 @@ var init_src = __esm(() => {
|
|
|
149
162
|
workspaces: boundedArray(Workspace, 64),
|
|
150
163
|
version: bounded(32),
|
|
151
164
|
lastSeenAt: Timestamp,
|
|
152
|
-
connectedAt: Schema.optionalKey(Timestamp)
|
|
165
|
+
connectedAt: Schema.optionalKey(Timestamp),
|
|
166
|
+
queue: Schema.optionalKey(Schema.Struct({
|
|
167
|
+
queued: count,
|
|
168
|
+
oldestQueuedAt: Schema.optionalKey(Timestamp)
|
|
169
|
+
}))
|
|
153
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
|
+
}));
|
|
154
184
|
Thread = Schema.Struct({
|
|
155
185
|
id: ThreadId,
|
|
156
186
|
title: bounded(256),
|
|
@@ -180,8 +210,26 @@ var init_src = __esm(() => {
|
|
|
180
210
|
data: Schema.String.check(Schema.isMaxLength(Math.ceil(maxAttachmentBytes / 3) * 4), Schema.makeFilter((value) => value.length > 0 && value.length % 4 === 0 && value.length / 4 * 3 - (value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0) <= maxAttachmentBytes && /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value) ? undefined : "Expected nonempty base64 data"))
|
|
181
211
|
});
|
|
182
212
|
attachments = boundedArray(InputAttachment, maxTaskAttachments).check(Schema.makeFilter((files) => new Set(files.map((file) => file.name.normalize("NFC").toLocaleLowerCase("en-US"))).size === files.length ? undefined : "Attachment names must be unique"));
|
|
213
|
+
TaskProgressInput = Schema.Struct({
|
|
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"))
|
|
215
|
+
});
|
|
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);
|
|
183
230
|
Task = Schema.Struct({
|
|
184
231
|
id: TaskId,
|
|
232
|
+
runId: Schema.optionalKey(TaskRunId),
|
|
185
233
|
threadId: ThreadId,
|
|
186
234
|
prompt: bounded(131072),
|
|
187
235
|
context: Schema.optionalKey(optionalText(131072)),
|
|
@@ -196,23 +244,27 @@ var init_src = __esm(() => {
|
|
|
196
244
|
result: Schema.optionalKey(optionalText(131072)),
|
|
197
245
|
inputAttachmentNames: Schema.optionalKey(boundedArray(AttachmentName, maxTaskAttachments)),
|
|
198
246
|
error: Schema.optionalKey(optionalText(16384)),
|
|
247
|
+
progress: Schema.optionalKey(TaskProgress),
|
|
248
|
+
clarifications: Schema.optionalKey(clarifications),
|
|
199
249
|
createdAt: Timestamp,
|
|
200
250
|
updatedAt: Timestamp,
|
|
201
251
|
startedAt: Schema.optionalKey(Timestamp),
|
|
202
|
-
completedAt: Schema.optionalKey(Timestamp)
|
|
203
|
-
|
|
252
|
+
completedAt: Schema.optionalKey(Timestamp),
|
|
253
|
+
expiresAt: Schema.optionalKey(TaskDeadline)
|
|
254
|
+
}).check(Schema.makeFilter(validBotTarget), Schema.makeFilter(validClarificationState));
|
|
204
255
|
TaskInboxItem = Schema.Struct({
|
|
205
256
|
id: TaskId,
|
|
206
257
|
threadId: ThreadId,
|
|
207
258
|
targetDeviceId: DeviceId,
|
|
208
259
|
provider: Provider,
|
|
209
260
|
botId: Schema.optionalKey(BotId),
|
|
210
|
-
status: Schema.Literals(["completed", "failed", "cancelled"]),
|
|
261
|
+
status: Schema.Literals(["needs_input", "completed", "failed", "cancelled"]),
|
|
211
262
|
promptPreview: optionalText(320),
|
|
212
263
|
resultPreview: Schema.optionalKey(optionalText(1024)),
|
|
213
264
|
errorPreview: Schema.optionalKey(optionalText(1024)),
|
|
214
|
-
completedAt: Timestamp
|
|
215
|
-
|
|
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"));
|
|
216
268
|
TaskInbox = Schema.Struct({
|
|
217
269
|
items: boundedArray(TaskInboxItem, 20),
|
|
218
270
|
hasMore: Schema.Boolean
|
|
@@ -241,6 +293,7 @@ var init_src = __esm(() => {
|
|
|
241
293
|
queued: count,
|
|
242
294
|
assigned: count,
|
|
243
295
|
running: count,
|
|
296
|
+
needs_input: Schema.optionalKey(count),
|
|
244
297
|
cancelling: count,
|
|
245
298
|
completed: count,
|
|
246
299
|
failed: count,
|
|
@@ -282,10 +335,13 @@ var init_src = __esm(() => {
|
|
|
282
335
|
updatedAt: Timestamp,
|
|
283
336
|
startedAt: Schema.optionalKey(Timestamp),
|
|
284
337
|
completedAt: Schema.optionalKey(Timestamp),
|
|
338
|
+
expiresAt: Schema.optionalKey(TaskDeadline),
|
|
285
339
|
events: boundedArray(TaskTraceEvent, 100),
|
|
286
340
|
truncated: Schema.Boolean,
|
|
287
|
-
error: Schema.optionalKey(optionalText(16384))
|
|
288
|
-
|
|
341
|
+
error: Schema.optionalKey(optionalText(16384)),
|
|
342
|
+
progress: Schema.optionalKey(TaskProgress),
|
|
343
|
+
clarifications: Schema.optionalKey(clarifications)
|
|
344
|
+
}).check(Schema.makeFilter(validBotTarget), Schema.makeFilter(validClarificationState));
|
|
289
345
|
ThreadContext = Schema.Struct({
|
|
290
346
|
thread: Thread,
|
|
291
347
|
messages: boundedArray(Message, 20),
|
|
@@ -293,6 +349,7 @@ var init_src = __esm(() => {
|
|
|
293
349
|
truncated: Schema.Boolean
|
|
294
350
|
});
|
|
295
351
|
CreateTaskInput = Schema.Struct({
|
|
352
|
+
requestId: Schema.optionalKey(TaskRequestId),
|
|
296
353
|
threadId: Schema.optionalKey(ThreadId),
|
|
297
354
|
title: Schema.optionalKey(bounded(256)),
|
|
298
355
|
prompt: bounded(131072),
|
|
@@ -301,6 +358,7 @@ var init_src = __esm(() => {
|
|
|
301
358
|
botId: Schema.optionalKey(BotId),
|
|
302
359
|
targetDeviceId: Schema.optionalKey(DeviceId),
|
|
303
360
|
parentTaskId: Schema.optionalKey(TaskId),
|
|
361
|
+
expiresAt: Schema.optionalKey(TaskDeadline),
|
|
304
362
|
workspace: Schema.optionalKey(bounded(4096)),
|
|
305
363
|
attachments: Schema.optionalKey(attachments)
|
|
306
364
|
}).check(Schema.makeFilter(validBotTarget));
|
|
@@ -309,7 +367,9 @@ var init_src = __esm(() => {
|
|
|
309
367
|
Connected: {
|
|
310
368
|
serverVersion: bounded(32),
|
|
311
369
|
connectedAt: Timestamp,
|
|
312
|
-
taskAttachments: Schema.optionalKey(Schema.Boolean)
|
|
370
|
+
taskAttachments: Schema.optionalKey(Schema.Boolean),
|
|
371
|
+
taskClarification: Schema.optionalKey(Schema.Boolean),
|
|
372
|
+
taskDeadlines: Schema.optionalKey(Schema.Boolean)
|
|
313
373
|
},
|
|
314
374
|
DeviceHello: { device: Device },
|
|
315
375
|
DeviceHeartbeat: {
|
|
@@ -318,17 +378,22 @@ var init_src = __esm(() => {
|
|
|
318
378
|
bots: Schema.optionalKey(boundedArray(Bot, 256))
|
|
319
379
|
},
|
|
320
380
|
TaskAssigned: { task: Task },
|
|
321
|
-
TaskAccepted: { taskId: TaskId },
|
|
381
|
+
TaskAccepted: { taskId: TaskId, runId: Schema.optionalKey(TaskRunId) },
|
|
322
382
|
TaskFinished: {
|
|
323
383
|
taskId: TaskId,
|
|
384
|
+
runId: Schema.optionalKey(TaskRunId),
|
|
324
385
|
result: optionalText(131072),
|
|
325
386
|
attachments: Schema.optionalKey(attachments),
|
|
326
387
|
providerSessionId: Schema.optionalKey(bounded(4096))
|
|
327
388
|
},
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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) },
|
|
332
397
|
OperationAssigned: { operation: DeviceOperation },
|
|
333
398
|
OperationAccepted: { operationId: OperationId },
|
|
334
399
|
OperationFinished: { operationId: OperationId, result: optionalText(16384) },
|
|
@@ -345,6 +410,9 @@ var init_src = __esm(() => {
|
|
|
345
410
|
"cancelled"
|
|
346
411
|
]);
|
|
347
412
|
decodeCreatePairingInput = Schema.decodeUnknownEffect(CreatePairingInput);
|
|
413
|
+
decodeTaskProgressInput = Schema.decodeUnknownEffect(TaskProgressInput);
|
|
414
|
+
decodeRequestTaskInput = Schema.decodeUnknownEffect(RequestTaskInput);
|
|
415
|
+
decodeAnswerTaskInput = Schema.decodeUnknownEffect(AnswerTaskInput);
|
|
348
416
|
decodeExchangePairingInput = Schema.decodeUnknownEffect(ExchangePairingInput);
|
|
349
417
|
decodeCreateUpgradeOperationsInput = Schema.decodeUnknownEffect(CreateUpgradeOperationsInput);
|
|
350
418
|
decodeDevice = Schema.decodeUnknownEffect(Device);
|
|
@@ -355,7 +423,7 @@ var init_src = __esm(() => {
|
|
|
355
423
|
import { Effect as Effect13, Schema as Schema14 } from "effect";
|
|
356
424
|
import { access as access5, chmod as chmod7, mkdir as mkdir8, readFile as readFile5, writeFile as writeFile9 } from "node:fs/promises";
|
|
357
425
|
import { homedir as homedir5, platform as platform5 } from "node:os";
|
|
358
|
-
import { join as join9, resolve as
|
|
426
|
+
import { join as join9, resolve as resolve5 } from "node:path";
|
|
359
427
|
var RelayConfiguration, defaultDataDirectory = () => {
|
|
360
428
|
if (platform5() === "win32") {
|
|
361
429
|
return join9(process.env.LOCALAPPDATA ?? join9(homedir5(), "AppData", "Local"), "Cohall");
|
|
@@ -403,7 +471,7 @@ var init_config = __esm(() => {
|
|
|
403
471
|
if (!isLoopback2(host) && !allowRemote) {
|
|
404
472
|
throw new Error("Remote relay binding is disabled. Use a private network or TLS reverse proxy, then set COHALL_RELAY_ALLOW_REMOTE=true.");
|
|
405
473
|
}
|
|
406
|
-
const dataDirectory =
|
|
474
|
+
const dataDirectory = resolve5(process.env.COHALL_DATA_DIR ?? defaultDataDirectory());
|
|
407
475
|
await mkdir8(dataDirectory, { recursive: true, mode: 448 });
|
|
408
476
|
if (platform5() !== "win32") {
|
|
409
477
|
await chmod7(dataDirectory, 448);
|
|
@@ -596,13 +664,17 @@ var exports_store = {};
|
|
|
596
664
|
__export(exports_store, {
|
|
597
665
|
layerFromDatabase: () => layerFromDatabase,
|
|
598
666
|
layer: () => layer2,
|
|
667
|
+
TaskRequestError: () => TaskRequestError,
|
|
668
|
+
TaskProgressError: () => TaskProgressError,
|
|
669
|
+
TaskInputError: () => TaskInputError,
|
|
670
|
+
TaskDeadlineError: () => TaskDeadlineError,
|
|
599
671
|
Service: () => Service2,
|
|
600
672
|
RelayStore: () => exports_store,
|
|
601
673
|
PersistenceError: () => PersistenceError
|
|
602
674
|
});
|
|
603
675
|
import { Context as Context2, Effect as Effect14, Layer as Layer2, Schema as Schema15 } from "effect";
|
|
604
676
|
import { createHash as createHash2, randomBytes } from "node:crypto";
|
|
605
|
-
var PersistenceError, Service2, operationError = (operation) => (cause) => new PersistenceError({
|
|
677
|
+
var PersistenceError, TaskProgressError, TaskInputError, TaskDeadlineError, TaskRequestError, Service2, operationError = (operation) => (cause) => new PersistenceError({
|
|
606
678
|
operation,
|
|
607
679
|
message: cause instanceof Error ? cause.message : String(cause)
|
|
608
680
|
}), decode2 = (operation, schema, value) => Schema15.decodeUnknownEffect(schema)(value).pipe(Effect14.mapError(operationError(operation))), threadFromRow = (row) => decode2("RelayStore.decodeThread", Thread, {
|
|
@@ -633,12 +705,20 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
633
705
|
workspaces: JSON.parse(row.workspaces_json),
|
|
634
706
|
version: row.version,
|
|
635
707
|
lastSeenAt: row.last_seen_at,
|
|
636
|
-
...row.connected_at === null ? {} : { connectedAt: row.connected_at }
|
|
708
|
+
...row.connected_at === null ? {} : { connectedAt: row.connected_at },
|
|
709
|
+
...row.queued_tasks === undefined ? {} : {
|
|
710
|
+
queue: {
|
|
711
|
+
queued: row.queued_tasks,
|
|
712
|
+
...row.oldest_queued_at === undefined || row.oldest_queued_at === null ? {} : { oldestQueuedAt: row.oldest_queued_at }
|
|
713
|
+
}
|
|
714
|
+
}
|
|
637
715
|
}), taskFromRow = (db, row) => Effect14.try({
|
|
638
716
|
try: () => db.query("SELECT name FROM task_attachments WHERE task_id = ? AND direction = 'input' ORDER BY name").all(row.id),
|
|
639
717
|
catch: operationError("RelayStore.taskAttachments")
|
|
640
718
|
}).pipe(Effect14.flatMap((attachments2) => decode2("RelayStore.decodeTask", Task, {
|
|
641
719
|
id: row.id,
|
|
720
|
+
...row.run_id === null ? {} : { runId: row.run_id },
|
|
721
|
+
...row.clarifications_json === null ? {} : { clarifications: JSON.parse(row.clarifications_json) },
|
|
642
722
|
threadId: row.thread_id,
|
|
643
723
|
prompt: row.prompt,
|
|
644
724
|
provider: row.provider,
|
|
@@ -647,6 +727,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
647
727
|
targetDeviceId: row.target_device_id,
|
|
648
728
|
createdAt: row.created_at,
|
|
649
729
|
updatedAt: row.updated_at,
|
|
730
|
+
...row.expires_at === null ? {} : { expiresAt: row.expires_at },
|
|
650
731
|
...row.context === null ? {} : { context: row.context },
|
|
651
732
|
...row.source_device_id === null ? {} : { sourceDeviceId: row.source_device_id },
|
|
652
733
|
...row.parent_task_id === null ? {} : { parentTaskId: row.parent_task_id },
|
|
@@ -655,6 +736,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
655
736
|
...row.result === null ? {} : { result: row.result },
|
|
656
737
|
...attachments2.length === 0 ? {} : { inputAttachmentNames: attachments2.map((item) => item.name) },
|
|
657
738
|
...row.error === null ? {} : { error: row.error },
|
|
739
|
+
...row.progress_note === null ? {} : { progress: { note: row.progress_note, at: row.progress_at } },
|
|
658
740
|
...row.started_at === null ? {} : { startedAt: row.started_at },
|
|
659
741
|
...row.completed_at === null ? {} : { completedAt: row.completed_at }
|
|
660
742
|
}))), inboxItemFromRow = (row) => decode2("RelayStore.decodeInboxItem", TaskInboxItem, {
|
|
@@ -665,7 +747,8 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
665
747
|
...row.bot_id === null ? {} : { botId: row.bot_id },
|
|
666
748
|
status: row.status,
|
|
667
749
|
promptPreview: row.prompt_preview,
|
|
668
|
-
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) },
|
|
669
752
|
...row.result_preview === null ? {} : { resultPreview: row.result_preview },
|
|
670
753
|
...row.error_preview === null ? {} : { errorPreview: row.error_preview }
|
|
671
754
|
}), operationFromRow = (row) => decode2("RelayStore.decodeOperation", DeviceOperation, {
|
|
@@ -684,6 +767,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
684
767
|
queued: 0,
|
|
685
768
|
assigned: 0,
|
|
686
769
|
running: 0,
|
|
770
|
+
needs_input: 0,
|
|
687
771
|
cancelling: 0,
|
|
688
772
|
completed: 0,
|
|
689
773
|
failed: 0,
|
|
@@ -755,7 +839,52 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
755
839
|
events.push(TaskTraceEvent.make({ kind: task.status, at: task.completedAt, detail }));
|
|
756
840
|
}
|
|
757
841
|
return events;
|
|
758
|
-
},
|
|
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
|
+
});
|
|
759
888
|
const recordTaskTraceEvent = (taskId, kind, detail, createdAt = now()) => {
|
|
760
889
|
db.query(`INSERT INTO task_trace_events (task_id, kind, created_at, detail)
|
|
761
890
|
VALUES (?, ?, ?, ?)`).run(taskId, kind, createdAt, detail);
|
|
@@ -795,7 +924,8 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
795
924
|
substr(prompt, 1, 160) AS prompt_preview,
|
|
796
925
|
substr(result, 1, 512) AS result_preview,
|
|
797
926
|
substr(error, 1, 512) AS error_preview,
|
|
798
|
-
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`;
|
|
799
929
|
const queryOperation = (operationId) => db.query("SELECT * FROM device_operations WHERE id = ?").get(operationId);
|
|
800
930
|
const getTask = Effect14.fn("RelayStore.getTask")(function* (taskId) {
|
|
801
931
|
const row = yield* Effect14.try({
|
|
@@ -805,7 +935,8 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
805
935
|
if (row === null) {
|
|
806
936
|
return yield* Effect14.fail(new PersistenceError({
|
|
807
937
|
operation: "RelayStore.getTask",
|
|
808
|
-
message: `Unknown task ${taskId}
|
|
938
|
+
message: `Unknown task ${taskId}`,
|
|
939
|
+
missingRecord: "task"
|
|
809
940
|
}));
|
|
810
941
|
}
|
|
811
942
|
return yield* taskFromRow(db, row);
|
|
@@ -836,8 +967,8 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
836
967
|
const requesterId = principal === "owner" ? "owner" : principal.id;
|
|
837
968
|
const rows = yield* Effect14.try({
|
|
838
969
|
try: () => db.query(`SELECT ${inboxColumns} FROM tasks WHERE requester_id = ? AND completion_seen_at IS NULL
|
|
839
|
-
AND status IN ('completed', 'failed', 'cancelled')
|
|
840
|
-
ORDER BY completed_at ASC, id ASC LIMIT 21`).all(requesterId),
|
|
970
|
+
AND status IN ('needs_input', 'completed', 'failed', 'cancelled')
|
|
971
|
+
ORDER BY COALESCE(completed_at, updated_at) ASC, id ASC LIMIT 21`).all(requesterId),
|
|
841
972
|
catch: operationError("RelayStore.inboxFor")
|
|
842
973
|
});
|
|
843
974
|
return TaskInbox.make({
|
|
@@ -902,16 +1033,72 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
902
1033
|
...task.workspace === undefined ? {} : { workspace: task.workspace },
|
|
903
1034
|
...task.startedAt === undefined ? {} : { startedAt: task.startedAt },
|
|
904
1035
|
...task.completedAt === undefined ? {} : { completedAt: task.completedAt },
|
|
905
|
-
...task.
|
|
1036
|
+
...task.expiresAt === undefined ? {} : { expiresAt: task.expiresAt },
|
|
1037
|
+
...task.error === undefined ? {} : { error: task.error },
|
|
1038
|
+
...task.progress === undefined ? {} : { progress: task.progress },
|
|
1039
|
+
...task.clarifications === undefined ? {} : { clarifications: task.clarifications }
|
|
906
1040
|
});
|
|
907
1041
|
});
|
|
908
1042
|
const listDevices = Effect14.fn("RelayStore.listDevices")(function* () {
|
|
909
1043
|
const rows = yield* Effect14.try({
|
|
910
|
-
try: () => db.query(
|
|
1044
|
+
try: () => db.query(`SELECT devices.*, COUNT(tasks.id) AS queued_tasks,
|
|
1045
|
+
MIN(tasks.created_at) AS oldest_queued_at
|
|
1046
|
+
FROM devices LEFT JOIN tasks
|
|
1047
|
+
ON tasks.target_device_id = devices.id AND tasks.status = 'queued'
|
|
1048
|
+
WHERE devices.forgotten_at IS NULL
|
|
1049
|
+
GROUP BY devices.id ORDER BY devices.name COLLATE NOCASE, devices.id`).all(),
|
|
911
1050
|
catch: operationError("RelayStore.listDevices")
|
|
912
1051
|
});
|
|
913
1052
|
return yield* Effect14.forEach(rows, deviceFromRow);
|
|
914
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
|
+
});
|
|
915
1102
|
const usage = Effect14.fn("RelayStore.usage")(function* () {
|
|
916
1103
|
const rows = yield* Effect14.try({
|
|
917
1104
|
try: () => db.query(`SELECT tasks.target_device_id AS device_id, devices.name AS device_name,
|
|
@@ -967,7 +1154,8 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
967
1154
|
if (row === null) {
|
|
968
1155
|
return yield* Effect14.fail(new PersistenceError({
|
|
969
1156
|
operation: "RelayStore.getOperation",
|
|
970
|
-
message: `Unknown device operation ${operationId}
|
|
1157
|
+
message: `Unknown device operation ${operationId}`,
|
|
1158
|
+
missingRecord: "operation"
|
|
971
1159
|
}));
|
|
972
1160
|
}
|
|
973
1161
|
return yield* operationFromRow(row);
|
|
@@ -1125,12 +1313,15 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1125
1313
|
yield* Effect14.try({
|
|
1126
1314
|
try: () => db.transaction(() => {
|
|
1127
1315
|
const timestamp = now();
|
|
1128
|
-
const interrupted = db.query(
|
|
1316
|
+
const interrupted = db.query(`SELECT id, ${interruptedTaskStatus} AS status FROM tasks WHERE status IN ('assigned', 'running')`).all();
|
|
1129
1317
|
db.query("UPDATE devices SET status = 'offline'").run();
|
|
1130
|
-
db.query(
|
|
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
|
|
1321
|
+
WHERE status IN ('assigned', 'running')`).run(timestamp);
|
|
1131
1322
|
db.query("UPDATE device_operations SET status = 'queued', updated_at = ? WHERE status IN ('assigned', 'running')").run(timestamp);
|
|
1132
1323
|
for (const task of interrupted) {
|
|
1133
|
-
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);
|
|
1134
1325
|
}
|
|
1135
1326
|
db.query("DELETE FROM auth_pairings WHERE expires_at <= ? OR used_at IS NOT NULL").run(timestamp);
|
|
1136
1327
|
db.query("UPDATE auth_sessions SET revoked_at = ? WHERE revoked_at IS NULL AND roles_json LIKE '%,%'").run(timestamp);
|
|
@@ -1179,52 +1370,68 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1179
1370
|
}
|
|
1180
1371
|
return yield* deviceFromRow(row);
|
|
1181
1372
|
});
|
|
1182
|
-
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) {
|
|
1183
1374
|
const requesterId = principal === "owner" ? "owner" : principal.id;
|
|
1184
1375
|
const boundDeviceId = principal === "owner" ? undefined : principal.deviceId;
|
|
1185
1376
|
const sourceDeviceId = yield* Effect14.try({
|
|
1186
1377
|
try: () => boundDeviceId === undefined || db.query("SELECT id FROM devices WHERE id = ? AND forgotten_at IS NULL").get(boundDeviceId) === null ? undefined : boundDeviceId,
|
|
1187
1378
|
catch: operationError("RelayStore.createDelegation.source")
|
|
1188
1379
|
});
|
|
1189
|
-
const
|
|
1190
|
-
const threadId = input.threadId ?? makeThreadId();
|
|
1191
|
-
const task = Task.make({
|
|
1192
|
-
id: makeTaskId(),
|
|
1193
|
-
threadId,
|
|
1194
|
-
prompt: input.prompt,
|
|
1195
|
-
provider: input.provider ?? (input.botId === undefined ? "codex" : "grok-bot"),
|
|
1196
|
-
...input.botId === undefined ? {} : { botId: input.botId },
|
|
1197
|
-
status: "queued",
|
|
1198
|
-
targetDeviceId,
|
|
1199
|
-
createdAt: timestamp,
|
|
1200
|
-
updatedAt: timestamp,
|
|
1201
|
-
...input.context === undefined ? {} : { context: input.context },
|
|
1202
|
-
...sourceDeviceId === undefined ? {} : { sourceDeviceId },
|
|
1203
|
-
...input.parentTaskId === undefined ? {} : { parentTaskId: input.parentTaskId },
|
|
1204
|
-
...input.workspace === undefined ? {} : { workspace: input.workspace },
|
|
1205
|
-
...input.attachments === undefined || input.attachments.length === 0 ? {} : { inputAttachmentNames: input.attachments.map((attachment) => attachment.name) },
|
|
1206
|
-
...providerSessionId === undefined ? {} : { providerSessionId }
|
|
1207
|
-
});
|
|
1208
|
-
const thread = input.threadId === undefined ? Thread.make({
|
|
1209
|
-
id: threadId,
|
|
1210
|
-
title: input.title ?? input.prompt.slice(0, 72),
|
|
1211
|
-
createdAt: timestamp,
|
|
1212
|
-
updatedAt: timestamp
|
|
1213
|
-
}) : undefined;
|
|
1214
|
-
const message = Message.make({
|
|
1215
|
-
id: makeMessageId(),
|
|
1216
|
-
threadId,
|
|
1217
|
-
role: "human",
|
|
1218
|
-
authorName: sourceDeviceId === undefined ? "User" : "Remote agent",
|
|
1219
|
-
content: input.prompt,
|
|
1220
|
-
createdAt: timestamp,
|
|
1221
|
-
taskId: task.id,
|
|
1222
|
-
...sourceDeviceId === undefined ? {} : { deviceId: sourceDeviceId }
|
|
1223
|
-
});
|
|
1224
|
-
yield* Effect14.try({
|
|
1380
|
+
const submission = yield* Effect14.try({
|
|
1225
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
|
+
});
|
|
1226
1433
|
const outstanding = db.query(`SELECT COUNT(*) AS count FROM tasks WHERE target_device_id = ?
|
|
1227
|
-
AND status IN ('queued', 'assigned', 'running', 'cancelling')`).get(targetDeviceId);
|
|
1434
|
+
AND status IN ('queued', 'assigned', 'running', 'needs_input', 'cancelling')`).get(targetDeviceId);
|
|
1228
1435
|
if ((outstanding?.count ?? 0) >= 100) {
|
|
1229
1436
|
throw new Error(`Device ${targetDeviceId} has reached its outstanding task limit`);
|
|
1230
1437
|
}
|
|
@@ -1236,8 +1443,8 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1236
1443
|
db.query(`INSERT INTO tasks (
|
|
1237
1444
|
id, thread_id, prompt, context, provider, status, source_device_id,
|
|
1238
1445
|
requester_id, target_device_id, parent_task_id, workspace,
|
|
1239
|
-
provider_session_id, bot_id, created_at, updated_at
|
|
1240
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(task.id, task.threadId, task.prompt, task.context ?? null, task.provider, task.status, task.sourceDeviceId ?? null, requesterId, task.targetDeviceId, task.parentTaskId ?? null, task.workspace ?? null, task.providerSessionId ?? null, task.botId ?? null, task.createdAt, task.updatedAt);
|
|
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);
|
|
1241
1448
|
for (const attachment of input.attachments ?? []) {
|
|
1242
1449
|
const data = Buffer.from(attachment.data, "base64");
|
|
1243
1450
|
if (data.length < 1 || data.length > maxAttachmentBytes) {
|
|
@@ -1245,16 +1452,20 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1245
1452
|
}
|
|
1246
1453
|
db.query("INSERT INTO task_attachments (task_id, name, direction, data) VALUES (?, ?, 'input', ?)").run(task.id, attachment.name, data);
|
|
1247
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
|
+
}
|
|
1248
1458
|
recordTaskTraceEvent(task.id, "queued", "Relay accepted the task", timestamp);
|
|
1249
1459
|
db.query(`INSERT INTO messages (
|
|
1250
1460
|
id, thread_id, role, kind, author_id, author_name, content, created_at,
|
|
1251
1461
|
task_id, device_id
|
|
1252
1462
|
) VALUES (?, ?, ?, 'chat', ?, ?, ?, ?, ?, ?)`).run(message.id, message.threadId, message.role, sourceDeviceId ?? "user", message.authorName, message.content, message.createdAt, message.taskId ?? null, message.deviceId ?? null);
|
|
1253
1463
|
db.query("UPDATE threads SET updated_at = ? WHERE id = ?").run(timestamp, threadId);
|
|
1464
|
+
return { created: task };
|
|
1254
1465
|
})(),
|
|
1255
|
-
catch: operationError("RelayStore.createDelegation")
|
|
1466
|
+
catch: (cause) => cause instanceof TaskRequestError || cause instanceof TaskDeadlineError ? cause : operationError("RelayStore.createDelegation")(cause)
|
|
1256
1467
|
});
|
|
1257
|
-
return
|
|
1468
|
+
return "existing" in submission ? yield* taskFromRow(db, submission.existing) : submission.created;
|
|
1258
1469
|
});
|
|
1259
1470
|
const threadContext2 = Effect14.fn("RelayStore.threadContext")(function* (threadId) {
|
|
1260
1471
|
const threadRow = yield* Effect14.try({
|
|
@@ -1325,15 +1536,45 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1325
1536
|
});
|
|
1326
1537
|
return yield* getTask(taskId);
|
|
1327
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
|
+
});
|
|
1328
1561
|
const assignTask = Effect14.fn("RelayStore.assignTask")(function* (taskId) {
|
|
1329
|
-
const current = yield* getTask(taskId);
|
|
1562
|
+
const current = yield* getTask(taskId).pipe(Effect14.flatMap(expireTask));
|
|
1330
1563
|
if (current.status !== "queued") {
|
|
1331
1564
|
return current;
|
|
1332
1565
|
}
|
|
1333
1566
|
const timestamp = now();
|
|
1334
1567
|
yield* Effect14.try({
|
|
1335
1568
|
try: () => db.transaction(() => {
|
|
1336
|
-
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,
|
|
1337
1578
|
provider_session_id = CASE
|
|
1338
1579
|
WHEN provider = 'grok-bot' THEN NULL
|
|
1339
1580
|
WHEN started_at IS NULL THEN COALESCE((
|
|
@@ -1352,7 +1593,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1352
1593
|
SELECT 1 FROM device_operations active
|
|
1353
1594
|
WHERE active.target_device_id = ?
|
|
1354
1595
|
AND active.status IN ('assigned', 'running')
|
|
1355
|
-
)`).run(timestamp, taskId, current.targetDeviceId, taskId, taskSlot(current), current.targetDeviceId);
|
|
1596
|
+
)`).run(timestamp, crypto.randomUUID(), taskId, current.targetDeviceId, taskId, taskSlot(current), current.targetDeviceId);
|
|
1356
1597
|
if (result.changes === 1) {
|
|
1357
1598
|
recordTaskTraceEvent(taskId, "assigned", "Relay dispatched the task over the device WebSocket", timestamp);
|
|
1358
1599
|
}
|
|
@@ -1371,22 +1612,195 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1371
1612
|
}
|
|
1372
1613
|
return task;
|
|
1373
1614
|
});
|
|
1374
|
-
const
|
|
1615
|
+
const reportTaskProgress = Effect14.fn("RelayStore.reportTaskProgress")(function* (taskId, deviceId, note) {
|
|
1616
|
+
const task = yield* getTask(taskId);
|
|
1617
|
+
if (task.targetDeviceId !== deviceId) {
|
|
1618
|
+
return yield* new TaskProgressError({
|
|
1619
|
+
status: 403,
|
|
1620
|
+
message: "Only the target device can report progress"
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
const progress = TaskProgress.make({ note, at: now() });
|
|
1624
|
+
const changed = yield* Effect14.try({
|
|
1625
|
+
try: () => db.query(`UPDATE tasks SET progress_note = ?, progress_at = ?, updated_at = ?
|
|
1626
|
+
WHERE id = ? AND target_device_id = ? AND status = 'running'`).run(note, progress.at, progress.at, taskId, deviceId).changes,
|
|
1627
|
+
catch: operationError("RelayStore.reportTaskProgress")
|
|
1628
|
+
});
|
|
1629
|
+
if (changed !== 1) {
|
|
1630
|
+
return yield* new TaskProgressError({
|
|
1631
|
+
status: 409,
|
|
1632
|
+
message: "Task must be running to report progress"
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1635
|
+
return progress;
|
|
1636
|
+
});
|
|
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) {
|
|
1375
1755
|
const current = yield* requireTarget(taskId, deviceId);
|
|
1376
|
-
if (["completed", "failed", "cancelled"].includes(current.status)) {
|
|
1756
|
+
if (!matchesTaskRun(current, runId) || current.status === "cancelling" && status !== "cancelled" || ["completed", "failed", "cancelled"].includes(current.status)) {
|
|
1377
1757
|
return current;
|
|
1378
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;
|
|
1379
1764
|
const timestamp = now();
|
|
1380
1765
|
yield* Effect14.try({
|
|
1381
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
|
+
}
|
|
1382
1794
|
const updated = db.query(`UPDATE tasks SET status = ?, result = ?, error = ?, provider_session_id = ?,
|
|
1383
|
-
completed_at = ?, updated_at =
|
|
1384
|
-
AND status NOT IN ('completed', 'failed', 'cancelled')
|
|
1795
|
+
completed_at = ?, updated_at = ?, progress_note = NULL, progress_at = NULL WHERE id = ?
|
|
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);
|
|
1385
1799
|
if (updated.changes !== 1) {
|
|
1386
1800
|
return;
|
|
1387
1801
|
}
|
|
1388
1802
|
db.query("DELETE FROM task_attachments WHERE task_id = ? AND direction = 'output'").run(taskId);
|
|
1389
|
-
if (
|
|
1803
|
+
if (finalStatus === "completed") {
|
|
1390
1804
|
if ((attachments2?.length ?? 0) > maxTaskAttachments) {
|
|
1391
1805
|
throw new Error(`A task supports at most ${maxTaskAttachments} output files`);
|
|
1392
1806
|
}
|
|
@@ -1398,20 +1812,15 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1398
1812
|
db.query("INSERT INTO task_attachments (task_id, name, direction, data) VALUES (?, ?, 'output', ?)").run(taskId, attachment.name, data);
|
|
1399
1813
|
}
|
|
1400
1814
|
}
|
|
1401
|
-
recordTaskTraceEvent(taskId,
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
VALUES (?, ?, ?, ?, ?)
|
|
1405
|
-
ON CONFLICT(thread_id, device_id, provider) DO UPDATE SET
|
|
1406
|
-
session_id = excluded.session_id, updated_at = excluded.updated_at`).run(current.threadId, deviceId, current.provider, providerSessionId, timestamp);
|
|
1407
|
-
}
|
|
1408
|
-
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;
|
|
1409
1818
|
if (content !== undefined) {
|
|
1410
|
-
const role =
|
|
1819
|
+
const role = finalStatus === "completed" ? "agent" : "system";
|
|
1411
1820
|
db.query(`INSERT INTO messages (
|
|
1412
1821
|
id, thread_id, role, kind, author_id, author_name, content, created_at,
|
|
1413
1822
|
task_id, device_id
|
|
1414
|
-
) 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);
|
|
1415
1824
|
db.query("UPDATE threads SET updated_at = ? WHERE id = ?").run(timestamp, current.threadId);
|
|
1416
1825
|
}
|
|
1417
1826
|
pruneTerminalHistory(taskId);
|
|
@@ -1420,16 +1829,19 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1420
1829
|
});
|
|
1421
1830
|
return yield* getTask(taskId);
|
|
1422
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
|
+
});
|
|
1423
1838
|
const requestCancellation = Effect14.fn("RelayStore.requestCancellation")(function* (taskId) {
|
|
1424
|
-
const current = yield* getTask(taskId);
|
|
1839
|
+
const current = yield* getTask(taskId).pipe(Effect14.flatMap(expireTask));
|
|
1425
1840
|
if (["completed", "failed", "cancelled"].includes(current.status)) {
|
|
1426
1841
|
return current;
|
|
1427
1842
|
}
|
|
1428
|
-
const dispatched =
|
|
1429
|
-
|
|
1430
|
-
catch: operationError("RelayStore.requestCancellation")
|
|
1431
|
-
}));
|
|
1432
|
-
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)) {
|
|
1433
1845
|
return yield* new PersistenceError({
|
|
1434
1846
|
operation: "RelayStore.requestCancellation",
|
|
1435
1847
|
message: "Stop this bot in Grok Bot; its gateway cannot cancel an individual Cohall request"
|
|
@@ -1439,7 +1851,8 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1439
1851
|
return current;
|
|
1440
1852
|
}
|
|
1441
1853
|
const timestamp = now();
|
|
1442
|
-
|
|
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"], {
|
|
1443
1856
|
status: "cancelling"
|
|
1444
1857
|
}, { kind: "cancelling", detail: "Client requested task cancellation" });
|
|
1445
1858
|
});
|
|
@@ -1447,12 +1860,14 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1447
1860
|
const timestamp = now();
|
|
1448
1861
|
yield* Effect14.try({
|
|
1449
1862
|
try: () => db.transaction(() => {
|
|
1450
|
-
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 = ?
|
|
1451
1864
|
AND status IN ('assigned', 'running')`).all(deviceId);
|
|
1452
|
-
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
|
|
1453
1868
|
WHERE target_device_id = ? AND status IN ('assigned', 'running')`).run(timestamp, deviceId);
|
|
1454
1869
|
for (const task of interrupted) {
|
|
1455
|
-
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);
|
|
1456
1871
|
}
|
|
1457
1872
|
})(),
|
|
1458
1873
|
catch: operationError("RelayStore.requeueTasksFor")
|
|
@@ -1572,13 +1987,20 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1572
1987
|
return Service2.of({
|
|
1573
1988
|
recover,
|
|
1574
1989
|
listDevices,
|
|
1990
|
+
listDevicePage,
|
|
1575
1991
|
usage,
|
|
1576
1992
|
forgetDevice,
|
|
1577
1993
|
upsertDevice,
|
|
1578
1994
|
heartbeat: (deviceId, status, bots) => updateDeviceStatus(deviceId, status, bots),
|
|
1579
1995
|
markDeviceOffline: (deviceId) => updateDeviceStatus(deviceId, "offline"),
|
|
1580
1996
|
createDelegation: createDelegation2,
|
|
1997
|
+
expireTasks,
|
|
1998
|
+
findDelegation,
|
|
1581
1999
|
getTask,
|
|
2000
|
+
reportTaskProgress,
|
|
2001
|
+
requestTaskInput,
|
|
2002
|
+
pauseTaskForInput,
|
|
2003
|
+
answerTaskInput,
|
|
1582
2004
|
listAttachments,
|
|
1583
2005
|
readAttachment,
|
|
1584
2006
|
inboxFor,
|
|
@@ -1594,10 +2016,13 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1594
2016
|
catch: operationError("RelayStore.markTaskDispatched")
|
|
1595
2017
|
}),
|
|
1596
2018
|
rollbackAssignment: (taskId) => transition(taskId, ["assigned"], { status: "queued" }, { kind: "requeued", detail: "Target device was unavailable during dispatch" }),
|
|
1597
|
-
acceptTask: (taskId, deviceId) => requireTarget(taskId, deviceId).pipe(Effect14.flatMap(() => transition(taskId, ["assigned"], { status: "running", startedAt: now() }, {
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
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),
|
|
1601
2026
|
requestCancellation,
|
|
1602
2027
|
requeueTasksFor,
|
|
1603
2028
|
createUpgradeOperations,
|
|
@@ -1650,10 +2075,18 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1650
2075
|
target_device_id TEXT NOT NULL REFERENCES devices(id), parent_task_id TEXT,
|
|
1651
2076
|
workspace TEXT, provider_session_id TEXT, result TEXT, error TEXT,
|
|
1652
2077
|
created_at TEXT NOT NULL, updated_at TEXT NOT NULL, started_at TEXT, completed_at TEXT,
|
|
1653
|
-
dispatched_at TEXT
|
|
2078
|
+
dispatched_at TEXT, progress_note TEXT, progress_at TEXT,
|
|
2079
|
+
run_id TEXT, clarifications_json TEXT, expires_at TEXT
|
|
1654
2080
|
);
|
|
1655
2081
|
CREATE INDEX IF NOT EXISTS tasks_thread_created ON tasks(thread_id, created_at);
|
|
1656
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;
|
|
1657
2090
|
CREATE TABLE IF NOT EXISTS task_attachments (
|
|
1658
2091
|
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1659
2092
|
name TEXT NOT NULL, direction TEXT NOT NULL CHECK(direction IN ('input', 'output')),
|
|
@@ -1701,6 +2134,19 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1701
2134
|
db.exec("ALTER TABLE devices ADD COLUMN bots_json TEXT");
|
|
1702
2135
|
}
|
|
1703
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");
|
|
2144
|
+
if (!taskColumns.some((column) => column.name === "progress_note")) {
|
|
2145
|
+
db.exec("ALTER TABLE tasks ADD COLUMN progress_note TEXT");
|
|
2146
|
+
}
|
|
2147
|
+
if (!taskColumns.some((column) => column.name === "progress_at")) {
|
|
2148
|
+
db.exec("ALTER TABLE tasks ADD COLUMN progress_at TEXT");
|
|
2149
|
+
}
|
|
1704
2150
|
if (!taskColumns.some((column) => column.name === "bot_id")) {
|
|
1705
2151
|
db.exec("ALTER TABLE tasks ADD COLUMN bot_id TEXT");
|
|
1706
2152
|
}
|
|
@@ -1719,6 +2165,13 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1719
2165
|
WHERE provider = 'grok-bot' AND status NOT IN ('completed', 'failed', 'cancelled')`);
|
|
1720
2166
|
})();
|
|
1721
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
|
+
))`);
|
|
1722
2175
|
},
|
|
1723
2176
|
catch: operationError("RelayStore.migrate")
|
|
1724
2177
|
}), layer2 = (path, retainedTerminalTasks = 1000) => Layer2.effect(Service2, Effect14.acquireRelease(Effect14.try({
|
|
@@ -1729,7 +2182,19 @@ var init_store = __esm(() => {
|
|
|
1729
2182
|
init_src();
|
|
1730
2183
|
init_database();
|
|
1731
2184
|
init_store();
|
|
1732
|
-
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
|
+
}) {
|
|
2190
|
+
};
|
|
2191
|
+
TaskProgressError = class TaskProgressError extends Schema15.TaggedErrorClass()("RelayStore.TaskProgressError", { status: Schema15.Literals([403, 409]), message: Schema15.String }) {
|
|
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 }) {
|
|
1733
2198
|
};
|
|
1734
2199
|
Service2 = class Service2 extends Context2.Service()("@cohall/RelayStore") {
|
|
1735
2200
|
};
|
|
@@ -1745,7 +2210,7 @@ __export(exports_main, {
|
|
|
1745
2210
|
canReadTaskAttachments: () => canReadTaskAttachments,
|
|
1746
2211
|
canDispatchTaskToDevice: () => canDispatchTaskToDevice
|
|
1747
2212
|
});
|
|
1748
|
-
import { Effect as Effect15, ManagedRuntime, Schema as Schema16 } from "effect";
|
|
2213
|
+
import { Effect as Effect15, ManagedRuntime, Schema as Schema16, Schedule as Schedule4 } from "effect";
|
|
1749
2214
|
import { createHash as createHash3, timingSafeEqual } from "node:crypto";
|
|
1750
2215
|
import { access as access6, chmod as chmod8 } from "node:fs/promises";
|
|
1751
2216
|
import { createServer } from "node:http";
|
|
@@ -1776,7 +2241,7 @@ var RequestError, relayListenOptions = (configuration, environment = process.env
|
|
|
1776
2241
|
if (request.method === "GET" || request.method === "HEAD") {
|
|
1777
2242
|
return Promise.resolve(undefined);
|
|
1778
2243
|
}
|
|
1779
|
-
return new Promise((
|
|
2244
|
+
return new Promise((resolve6, reject) => {
|
|
1780
2245
|
const chunks = [];
|
|
1781
2246
|
let size = 0;
|
|
1782
2247
|
const cleanup = () => {
|
|
@@ -1795,7 +2260,7 @@ var RequestError, relayListenOptions = (configuration, environment = process.env
|
|
|
1795
2260
|
};
|
|
1796
2261
|
const onEnd = () => {
|
|
1797
2262
|
cleanup();
|
|
1798
|
-
|
|
2263
|
+
resolve6(Buffer.concat(chunks, size));
|
|
1799
2264
|
};
|
|
1800
2265
|
const onError = (cause) => {
|
|
1801
2266
|
cleanup();
|
|
@@ -1882,6 +2347,11 @@ Content-Length: 0\r
|
|
|
1882
2347
|
const resolvedInput = parent === undefined ? input : { ...input, threadId: parent.threadId };
|
|
1883
2348
|
const devices = yield* store.listDevices().pipe(Effect15.mapError((cause) => new RequestError({ status: 500, message: cause.message })));
|
|
1884
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
|
+
});
|
|
1885
2355
|
const hasAttachments = (input.attachments?.length ?? 0) > 0;
|
|
1886
2356
|
if (hasAttachments && provider === "grok-bot") {
|
|
1887
2357
|
return yield* new RequestError({
|
|
@@ -1893,7 +2363,7 @@ Content-Length: 0\r
|
|
|
1893
2363
|
provider,
|
|
1894
2364
|
...input.botId === undefined ? {} : { botId: input.botId }
|
|
1895
2365
|
});
|
|
1896
|
-
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));
|
|
1897
2367
|
if (input.targetDeviceId !== undefined) {
|
|
1898
2368
|
const target = devices.find((device) => device.id === input.targetDeviceId);
|
|
1899
2369
|
if (target === undefined) {
|
|
@@ -1908,6 +2378,11 @@ Content-Length: 0\r
|
|
|
1908
2378
|
message: `${target.name} does not advertise the ${provider} provider`
|
|
1909
2379
|
});
|
|
1910
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
|
+
});
|
|
1911
2386
|
if (hasAttachments && !target.capabilities.some((capability) => capability.id === "task-attachments")) {
|
|
1912
2387
|
return yield* new RequestError({
|
|
1913
2388
|
status: 409,
|
|
@@ -1928,7 +2403,7 @@ Content-Length: 0\r
|
|
|
1928
2403
|
}
|
|
1929
2404
|
return { input: resolvedInput, targetDeviceId: target.id };
|
|
1930
2405
|
}
|
|
1931
|
-
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)));
|
|
1932
2407
|
const candidates = hasAttachments ? providerDevices.filter((device) => device.capabilities.some((capability) => capability.id === "task-attachments")) : providerDevices;
|
|
1933
2408
|
if (input.botId !== undefined && candidates.length > 1) {
|
|
1934
2409
|
return yield* new RequestError({
|
|
@@ -1940,11 +2415,11 @@ Content-Length: 0\r
|
|
|
1940
2415
|
if (selected === undefined) {
|
|
1941
2416
|
return yield* new RequestError({
|
|
1942
2417
|
status: 409,
|
|
1943
|
-
message: candidates.length > 0 ? "No available delegation target avoids an unfinished parent's worker slot; select another device or bot" : hasAttachments && providerDevices.length > 0 ? `No Cohall device advertising the ${provider} provider supports file attachments; upgrade a Cohall worker` : input.botId === undefined ? `No Cohall device advertises the ${provider} provider` : `No Cohall device advertises bot ${input.botId}; refresh the bot list`
|
|
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`
|
|
1944
2419
|
});
|
|
1945
2420
|
}
|
|
1946
2421
|
return { input: resolvedInput, targetDeviceId: selected.id };
|
|
1947
|
-
}), canReadTaskAttachments = (principal, task) => principal === "owner" || principal.role === "client" || principal.deviceId === task.targetDeviceId, canDispatchTaskToDevice = (task, device) => (task.inputAttachmentNames?.length ?? 0) === 0 || device?.capabilities.some((capability) => capability.id === "task-attachments") === true, runRelay = async () => {
|
|
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 () => {
|
|
1948
2423
|
const configuration = await Effect15.runPromise(loadEnvironmentConfiguration);
|
|
1949
2424
|
process.umask(63);
|
|
1950
2425
|
const runtime = ManagedRuntime.make(exports_store.layer(configuration.databasePath, configuration.historyTaskLimit));
|
|
@@ -1979,20 +2454,39 @@ Content-Length: 0\r
|
|
|
1979
2454
|
const token = authorization.slice("Bearer ".length);
|
|
1980
2455
|
return await authenticate(token, "client") ?? (allowDevice ? authenticate(token, "device") : undefined);
|
|
1981
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
|
+
};
|
|
1982
2464
|
const dispatch = async (task) => {
|
|
1983
2465
|
const store = await run2(exports_store.Service);
|
|
1984
|
-
if ((task.inputAttachmentNames?.length ?? 0) > 0) {
|
|
2466
|
+
if (task.expiresAt !== undefined || (task.inputAttachmentNames?.length ?? 0) > 0 || requiresClarificationSupport(task)) {
|
|
1985
2467
|
const devices = await Effect15.runPromise(store.listDevices());
|
|
1986
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;
|
|
1987
2473
|
if (!canDispatchTaskToDevice(task, target)) {
|
|
1988
|
-
return Effect15.runPromise(store.failTask(task.id, task.targetDeviceId, "Target worker no longer supports task file attachments. Upgrade the worker and retry."));
|
|
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));
|
|
1989
2475
|
}
|
|
1990
2476
|
}
|
|
2477
|
+
if (!hub.hasDevice(task.targetDeviceId))
|
|
2478
|
+
return task;
|
|
1991
2479
|
const assigned = await Effect15.runPromise(store.assignTask(task.id));
|
|
1992
2480
|
if (assigned.status !== "assigned") {
|
|
2481
|
+
if (assigned.status === "cancelling")
|
|
2482
|
+
sendCancellation(assigned);
|
|
1993
2483
|
return assigned;
|
|
1994
2484
|
}
|
|
1995
|
-
|
|
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)) {
|
|
1996
2490
|
return assigned;
|
|
1997
2491
|
}
|
|
1998
2492
|
return Effect15.runPromise(store.rollbackAssignment(task.id));
|
|
@@ -2029,7 +2523,7 @@ Content-Length: 0\r
|
|
|
2029
2523
|
}));
|
|
2030
2524
|
for (const task of tasks) {
|
|
2031
2525
|
if (task.status === "cancelling") {
|
|
2032
|
-
|
|
2526
|
+
sendCancellation(task);
|
|
2033
2527
|
} else {
|
|
2034
2528
|
await dispatch(task);
|
|
2035
2529
|
}
|
|
@@ -2089,7 +2583,9 @@ Content-Length: 0\r
|
|
|
2089
2583
|
_tag: "Connected",
|
|
2090
2584
|
serverVersion: version,
|
|
2091
2585
|
connectedAt: now(),
|
|
2092
|
-
taskAttachments: true
|
|
2586
|
+
taskAttachments: true,
|
|
2587
|
+
taskClarification: true,
|
|
2588
|
+
taskDeadlines: true
|
|
2093
2589
|
})));
|
|
2094
2590
|
return;
|
|
2095
2591
|
}
|
|
@@ -2125,12 +2621,18 @@ Content-Length: 0\r
|
|
|
2125
2621
|
sendError(socket, "device_hello_required", "Send DeviceHello before device events");
|
|
2126
2622
|
return;
|
|
2127
2623
|
}
|
|
2128
|
-
const settle = (taskId) => {
|
|
2129
|
-
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
|
+
})));
|
|
2130
2630
|
};
|
|
2131
2631
|
const settleOperation = (operationId) => {
|
|
2132
2632
|
socket.send(JSON.stringify(SocketEvent.make({ _tag: "OperationSettled", operationId })));
|
|
2133
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";
|
|
2134
2636
|
const processed = await run2(Effect15.gen(function* () {
|
|
2135
2637
|
const store = yield* exports_store.Service;
|
|
2136
2638
|
switch (event._tag) {
|
|
@@ -2140,16 +2642,23 @@ Content-Length: 0\r
|
|
|
2140
2642
|
}
|
|
2141
2643
|
return;
|
|
2142
2644
|
case "TaskAccepted":
|
|
2143
|
-
yield* store.acceptTask(event.taskId, deviceId);
|
|
2645
|
+
yield* store.acceptTask(event.taskId, deviceId, event.runId);
|
|
2144
2646
|
return;
|
|
2145
2647
|
case "TaskFinished":
|
|
2146
|
-
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);
|
|
2147
2649
|
return;
|
|
2650
|
+
case "TaskInputRequested": {
|
|
2651
|
+
yield* store.pauseTaskForInput(event.taskId, deviceId, {
|
|
2652
|
+
runId: event.runId,
|
|
2653
|
+
question: event.question
|
|
2654
|
+
});
|
|
2655
|
+
return;
|
|
2656
|
+
}
|
|
2148
2657
|
case "TaskFailed":
|
|
2149
|
-
yield* store.failTask(event.taskId, deviceId, event.error);
|
|
2658
|
+
yield* store.failTask(event.taskId, deviceId, event.error, event.runId);
|
|
2150
2659
|
return;
|
|
2151
2660
|
case "TaskCancelled":
|
|
2152
|
-
yield* store.acknowledgeCancellation(event.taskId, deviceId);
|
|
2661
|
+
yield* store.acknowledgeCancellation(event.taskId, deviceId, event.runId);
|
|
2153
2662
|
return;
|
|
2154
2663
|
case "OperationAccepted":
|
|
2155
2664
|
yield* store.acceptOperation(event.operationId, deviceId);
|
|
@@ -2170,21 +2679,31 @@ Content-Length: 0\r
|
|
|
2170
2679
|
return;
|
|
2171
2680
|
}
|
|
2172
2681
|
})).then(() => true).catch((cause) => {
|
|
2682
|
+
if (cause instanceof exports_store.PersistenceError && (taskTerminal && cause.missingRecord === "task" || operationTerminal && cause.missingRecord === "operation"))
|
|
2683
|
+
return true;
|
|
2173
2684
|
sendError(socket, "event_failed", cause instanceof Error ? cause.message : String(cause));
|
|
2174
2685
|
return false;
|
|
2175
2686
|
});
|
|
2176
|
-
if (processed &&
|
|
2177
|
-
settle(event.taskId);
|
|
2687
|
+
if (processed && taskTerminal) {
|
|
2688
|
+
settle(event.taskId, event.runId);
|
|
2178
2689
|
await dispatchPending(deviceId);
|
|
2179
2690
|
}
|
|
2180
|
-
if (processed &&
|
|
2691
|
+
if (processed && operationTerminal) {
|
|
2181
2692
|
settleOperation(event.operationId);
|
|
2182
2693
|
await dispatchPending(deviceId);
|
|
2183
2694
|
}
|
|
2184
2695
|
};
|
|
2185
2696
|
const api = async (request, url) => {
|
|
2186
2697
|
if (url.pathname === "/api/health" && request.method === "GET") {
|
|
2187
|
-
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
|
+
});
|
|
2188
2707
|
}
|
|
2189
2708
|
if (url.pathname === "/api/auth/pair" && request.method === "POST") {
|
|
2190
2709
|
return run2(Effect15.gen(function* () {
|
|
@@ -2194,7 +2713,9 @@ Content-Length: 0\r
|
|
|
2194
2713
|
})).then((value) => json(value, 201)).catch(() => json({ error: "Pairing credential is invalid, expired, or already used" }, 401));
|
|
2195
2714
|
}
|
|
2196
2715
|
const attachmentRoute = request.method === "GET" && /^\/api\/tasks\/[^/]+\/attachments(?:\/[^/]+)?$/.test(url.pathname);
|
|
2197
|
-
const
|
|
2716
|
+
const progressRoute = request.method === "POST" && /^\/api\/tasks\/[^/]+\/progress$/.test(url.pathname);
|
|
2717
|
+
const inputRoute = request.method === "POST" && /^\/api\/tasks\/[^/]+\/input$/.test(url.pathname);
|
|
2718
|
+
const principal = await principalFor(request, attachmentRoute || progressRoute || inputRoute);
|
|
2198
2719
|
if (principal === undefined) {
|
|
2199
2720
|
return json({ error: "Unauthorized" }, 401);
|
|
2200
2721
|
}
|
|
@@ -2219,6 +2740,10 @@ Content-Length: 0\r
|
|
|
2219
2740
|
hub.closeSession(id);
|
|
2220
2741
|
return json(session);
|
|
2221
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
|
+
}
|
|
2222
2747
|
if (url.pathname === "/api/devices" && request.method === "GET") {
|
|
2223
2748
|
return json(yield* store.listDevices());
|
|
2224
2749
|
}
|
|
@@ -2256,9 +2781,13 @@ Content-Length: 0\r
|
|
|
2256
2781
|
return json(forgotten);
|
|
2257
2782
|
}
|
|
2258
2783
|
if (url.pathname === "/api/tasks" && request.method === "POST") {
|
|
2259
|
-
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);
|
|
2260
2789
|
const providerSessionId = input.threadId === undefined || input.provider === "grok-bot" ? undefined : yield* store.sessionFor(input.threadId, target, input.provider ?? "codex");
|
|
2261
|
-
const task2 = yield* store.createDelegation(input, target, principal, providerSessionId);
|
|
2790
|
+
const task2 = yield* store.createDelegation(input, target, principal, providerSessionId, requestedInput);
|
|
2262
2791
|
return json(yield* Effect15.tryPromise(() => dispatch(task2)), 201);
|
|
2263
2792
|
}
|
|
2264
2793
|
if (url.pathname === "/api/inbox" && request.method === "GET") {
|
|
@@ -2313,12 +2842,50 @@ Content-Length: 0\r
|
|
|
2313
2842
|
if (request.method === "GET" && trace?.[1] !== undefined) {
|
|
2314
2843
|
return json(yield* store.traceTask(yield* pathId(TaskId, trace[1])));
|
|
2315
2844
|
}
|
|
2845
|
+
const progress = url.pathname.match(/^\/api\/tasks\/([^/]+)\/progress$/);
|
|
2846
|
+
if (request.method === "POST" && progress?.[1] !== undefined) {
|
|
2847
|
+
const id = yield* pathId(TaskId, progress[1]);
|
|
2848
|
+
const input = yield* body(request, decodeTaskProgressInput);
|
|
2849
|
+
const task2 = yield* store.getTask(id);
|
|
2850
|
+
const deviceId = principal === "owner" ? task2.targetDeviceId : principal.deviceId;
|
|
2851
|
+
if (deviceId === undefined) {
|
|
2852
|
+
return yield* new RequestError({
|
|
2853
|
+
status: 403,
|
|
2854
|
+
message: "Only the target device can report progress"
|
|
2855
|
+
});
|
|
2856
|
+
}
|
|
2857
|
+
return json(yield* store.reportTaskProgress(id, deviceId, input.note));
|
|
2858
|
+
}
|
|
2316
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
|
+
}
|
|
2317
2884
|
if (request.method === "POST" && cancel2?.[1] !== undefined) {
|
|
2318
2885
|
const id = yield* pathId(TaskId, cancel2[1]);
|
|
2319
2886
|
const updated = yield* store.requestCancellation(id);
|
|
2320
2887
|
if (updated.status === "cancelling") {
|
|
2321
|
-
|
|
2888
|
+
sendCancellation(updated);
|
|
2322
2889
|
}
|
|
2323
2890
|
return json(updated);
|
|
2324
2891
|
}
|
|
@@ -2327,7 +2894,7 @@ Content-Length: 0\r
|
|
|
2327
2894
|
return json(yield* store.threadContext(yield* pathId(ThreadId, thread[1])));
|
|
2328
2895
|
}
|
|
2329
2896
|
return yield* new RequestError({ status: 404, message: "Route not found" });
|
|
2330
|
-
}).pipe(Effect15.mapError((cause) => cause instanceof RequestError ? cause : 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({
|
|
2331
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,
|
|
2332
2899
|
message: cause.message
|
|
2333
2900
|
})));
|
|
@@ -2435,26 +3002,40 @@ Content-Length: 0\r
|
|
|
2435
3002
|
websocketServer.emit("connection", websocket, request);
|
|
2436
3003
|
});
|
|
2437
3004
|
});
|
|
2438
|
-
await new Promise((
|
|
3005
|
+
await new Promise((resolve6, reject) => {
|
|
2439
3006
|
server.once("error", reject);
|
|
2440
3007
|
server.listen(relayListenOptions(configuration), () => {
|
|
2441
3008
|
server.off("error", reject);
|
|
2442
|
-
|
|
3009
|
+
resolve6();
|
|
2443
3010
|
});
|
|
2444
3011
|
});
|
|
2445
3012
|
const address = server.address();
|
|
2446
3013
|
const listener = typeof address === "string" ? address : address?.family === "IPv6" ? `[${address.address}]:${address.port}` : `${address?.address ?? configuration.host}:${address?.port ?? configuration.port}`;
|
|
2447
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
|
+
});
|
|
2448
3027
|
let stopping = false;
|
|
2449
3028
|
const shutdown = async () => {
|
|
2450
3029
|
if (stopping) {
|
|
2451
3030
|
return;
|
|
2452
3031
|
}
|
|
2453
3032
|
stopping = true;
|
|
3033
|
+
deadlineController.abort();
|
|
3034
|
+
await deadlineWork;
|
|
2454
3035
|
for (const socket of websocketServer.clients) {
|
|
2455
3036
|
socket.close(1001, "Relay shutting down");
|
|
2456
3037
|
}
|
|
2457
|
-
await new Promise((
|
|
3038
|
+
await new Promise((resolve6) => server.close(() => resolve6()));
|
|
2458
3039
|
websocketServer.close();
|
|
2459
3040
|
await runtime.dispose();
|
|
2460
3041
|
};
|
|
@@ -2506,7 +3087,14 @@ class RelayDecodeError extends Schema2.TaggedErrorClass()("RelayClient.DecodeErr
|
|
|
2506
3087
|
class Service extends Context.Service()("@cohall/RelayClient") {
|
|
2507
3088
|
}
|
|
2508
3089
|
var normalizeBaseUrl = (url) => url.replace(/\/+$/, "");
|
|
2509
|
-
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) => {
|
|
2510
3098
|
if (response.body === null) {
|
|
2511
3099
|
throw new RelayDecodeError({ operation, message: "Relay returned an empty response" });
|
|
2512
3100
|
}
|
|
@@ -2519,9 +3107,12 @@ var jsonBody = async (response, operation) => {
|
|
|
2519
3107
|
break;
|
|
2520
3108
|
}
|
|
2521
3109
|
size += chunk.value.byteLength;
|
|
2522
|
-
if (size >
|
|
3110
|
+
if (size > maxBytes) {
|
|
2523
3111
|
await reader.cancel();
|
|
2524
|
-
throw new RelayDecodeError({
|
|
3112
|
+
throw new RelayDecodeError({
|
|
3113
|
+
operation,
|
|
3114
|
+
message: `Relay response exceeded ${maxBytes / (1024 * 1024)} MiB`
|
|
3115
|
+
});
|
|
2525
3116
|
}
|
|
2526
3117
|
chunks.push(chunk.value);
|
|
2527
3118
|
}
|
|
@@ -2550,42 +3141,80 @@ var responseMessage = async (response, operation) => {
|
|
|
2550
3141
|
};
|
|
2551
3142
|
var make = (options) => {
|
|
2552
3143
|
const baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
2553
|
-
const request = (operation, path, schema, init) => Effect2.gen(function* () {
|
|
2554
|
-
const
|
|
2555
|
-
try: (signal) =>
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
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
|
+
});
|
|
2562
3162
|
}
|
|
2563
|
-
|
|
2564
|
-
|
|
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({
|
|
2565
3168
|
operation,
|
|
2566
3169
|
message: cause instanceof Error ? cause.message : String(cause)
|
|
2567
3170
|
})
|
|
2568
3171
|
});
|
|
2569
|
-
if (!response.ok) {
|
|
2570
|
-
return yield* Effect2.fail(new RelayRequestError({
|
|
2571
|
-
operation,
|
|
2572
|
-
message: yield* Effect2.promise(() => responseMessage(response, operation)),
|
|
2573
|
-
status: response.status
|
|
2574
|
-
}));
|
|
2575
|
-
}
|
|
2576
|
-
const json = yield* Effect2.tryPromise({
|
|
2577
|
-
try: () => jsonBody(response, operation),
|
|
2578
|
-
catch: (cause) => cause instanceof RelayDecodeError ? cause : new RelayDecodeError({ operation, message: String(cause) })
|
|
2579
|
-
});
|
|
2580
3172
|
return yield* Schema2.decodeUnknownEffect(schema)(json).pipe(Effect2.mapError((cause) => new RelayDecodeError({ operation, message: String(cause) })));
|
|
2581
3173
|
});
|
|
3174
|
+
const devicePage = (after) => request("RelayClient.devices", `/api/devices/page${after === undefined ? "" : `?after=${encodeURIComponent(after)}`}`, DevicePage, undefined, maxDevicePageResponseBytes);
|
|
2582
3175
|
return Service.of({
|
|
2583
|
-
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
|
+
}),
|
|
2584
3197
|
forgetDevice: (deviceId) => request("RelayClient.forgetDevice", `/api/devices/${encodeURIComponent(deviceId)}/forget`, Device, { method: "POST" }),
|
|
2585
3198
|
createTask: (input) => Effect2.gen(function* () {
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
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) {
|
|
2589
3218
|
return yield* new RelayRequestError({
|
|
2590
3219
|
operation: "RelayClient.createTask",
|
|
2591
3220
|
message: "Upgrade the Cohall relay before sending file attachments"
|
|
@@ -2597,7 +3226,27 @@ var make = (options) => {
|
|
|
2597
3226
|
body: JSON.stringify(input)
|
|
2598
3227
|
});
|
|
2599
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) }),
|
|
2600
3239
|
getTask: (taskId) => request("RelayClient.getTask", `/api/tasks/${encodeURIComponent(taskId)}`, Task),
|
|
3240
|
+
reportTaskProgress: (taskId, input) => Effect2.gen(function* () {
|
|
3241
|
+
const health = yield* request("RelayClient.progressSupport", "/api/health", Schema2.Struct({ taskProgress: Schema2.optionalKey(Schema2.Boolean) }));
|
|
3242
|
+
if (health.taskProgress !== true) {
|
|
3243
|
+
return yield* new RelayRequestError({
|
|
3244
|
+
operation: "RelayClient.reportTaskProgress",
|
|
3245
|
+
message: "Upgrade the Cohall relay before reporting task progress"
|
|
3246
|
+
});
|
|
3247
|
+
}
|
|
3248
|
+
return yield* request("RelayClient.reportTaskProgress", `/api/tasks/${encodeURIComponent(taskId)}/progress`, TaskProgress, { method: "POST", body: JSON.stringify(input) });
|
|
3249
|
+
}),
|
|
2601
3250
|
listAttachments: (taskId) => request("RelayClient.listAttachments", `/api/tasks/${encodeURIComponent(taskId)}/attachments`, Schema2.Array(TaskAttachment)),
|
|
2602
3251
|
readAttachment: (taskId, name, direction) => Effect2.tryPromise({
|
|
2603
3252
|
try: async (signal) => {
|
|
@@ -2648,7 +3297,7 @@ var make = (options) => {
|
|
|
2648
3297
|
}),
|
|
2649
3298
|
inbox: () => request("RelayClient.inbox", "/api/inbox", TaskInbox),
|
|
2650
3299
|
acknowledgeCompletion: (taskId) => request("RelayClient.acknowledgeCompletion", `/api/inbox/${encodeURIComponent(taskId)}/ack`, TaskInboxItem, { method: "POST" }),
|
|
2651
|
-
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),
|
|
2652
3301
|
cancelTask: (taskId) => request("RelayClient.cancelTask", `/api/tasks/${encodeURIComponent(taskId)}/cancel`, Task, {
|
|
2653
3302
|
method: "POST"
|
|
2654
3303
|
}),
|
|
@@ -2891,7 +3540,7 @@ var prepareCommand = async (options) => {
|
|
|
2891
3540
|
}
|
|
2892
3541
|
}
|
|
2893
3542
|
};
|
|
2894
|
-
var forEachJsonEvent = async (stdout, onEvent) => {
|
|
3543
|
+
var forEachJsonEvent = async (stdout, onEvent, onOversized) => {
|
|
2895
3544
|
const limit = 1024 * 1024;
|
|
2896
3545
|
let chunks = [];
|
|
2897
3546
|
let size = 0;
|
|
@@ -2914,6 +3563,7 @@ var forEachJsonEvent = async (stdout, onEvent) => {
|
|
|
2914
3563
|
const emit = () => {
|
|
2915
3564
|
if (oversized) {
|
|
2916
3565
|
oversized = false;
|
|
3566
|
+
onOversized();
|
|
2917
3567
|
return;
|
|
2918
3568
|
}
|
|
2919
3569
|
const line = Buffer.concat(chunks, size).toString("utf8");
|
|
@@ -2945,6 +3595,7 @@ var forEachJsonEvent = async (stdout, onEvent) => {
|
|
|
2945
3595
|
var parseCodex = async (stdout, existingSession) => {
|
|
2946
3596
|
let result = "";
|
|
2947
3597
|
let sessionId = existingSession;
|
|
3598
|
+
let discardedEvent = false;
|
|
2948
3599
|
await forEachJsonEvent(stdout, (event) => {
|
|
2949
3600
|
if (text(event.type) === "thread.started") {
|
|
2950
3601
|
sessionId = text(event.thread_id) ?? sessionId;
|
|
@@ -2954,9 +3605,17 @@ var parseCodex = async (stdout, existingSession) => {
|
|
|
2954
3605
|
}
|
|
2955
3606
|
const item = record(event.item);
|
|
2956
3607
|
if (text(item?.type) === "agent_message") {
|
|
2957
|
-
|
|
3608
|
+
const content = text(item?.text) ?? text(item?.content);
|
|
3609
|
+
result = content ?? result;
|
|
3610
|
+
if (content !== undefined && content.length > 0)
|
|
3611
|
+
discardedEvent = false;
|
|
2958
3612
|
}
|
|
3613
|
+
}, () => {
|
|
3614
|
+
discardedEvent = true;
|
|
2959
3615
|
});
|
|
3616
|
+
if (discardedEvent) {
|
|
3617
|
+
throw new Error("Provider JSON event exceeded 1 MiB without a later text response");
|
|
3618
|
+
}
|
|
2960
3619
|
return {
|
|
2961
3620
|
result: result || "Codex completed the task without a text response.",
|
|
2962
3621
|
...sessionId === undefined ? {} : { sessionId }
|
|
@@ -2993,17 +3652,26 @@ var openCodeError = (event) => {
|
|
|
2993
3652
|
var parseOpenCode = async (stdout, existingSession) => {
|
|
2994
3653
|
let result = "";
|
|
2995
3654
|
let sessionId = existingSession;
|
|
3655
|
+
let discardedEvent = false;
|
|
2996
3656
|
await forEachJsonEvent(stdout, (event) => {
|
|
2997
3657
|
const failure = openCodeError(event);
|
|
2998
3658
|
if (failure !== undefined) {
|
|
2999
3659
|
throw new Error(failure);
|
|
3000
3660
|
}
|
|
3001
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;
|
|
3002
3664
|
const content = textFromPart(event.part) ?? textFromPart(event) ?? text(event.result);
|
|
3003
3665
|
if (content !== undefined && content.length > 0) {
|
|
3004
3666
|
result = content;
|
|
3667
|
+
discardedEvent = false;
|
|
3005
3668
|
}
|
|
3669
|
+
}, () => {
|
|
3670
|
+
discardedEvent = true;
|
|
3006
3671
|
});
|
|
3672
|
+
if (discardedEvent) {
|
|
3673
|
+
throw new Error("Provider JSON event exceeded 1 MiB without a later text response");
|
|
3674
|
+
}
|
|
3007
3675
|
if (result.length === 0) {
|
|
3008
3676
|
throw new Error("OpenCode produced no result; verify its authentication and use a supported project workspace");
|
|
3009
3677
|
}
|
|
@@ -3089,7 +3757,8 @@ var run = (options) => Effect3.callback((resume, signal) => {
|
|
|
3089
3757
|
...providerEnvironment(),
|
|
3090
3758
|
COHALL_PROVIDER: options.provider,
|
|
3091
3759
|
COHALL_THREAD_ID: options.threadId,
|
|
3092
|
-
...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 }
|
|
3093
3762
|
},
|
|
3094
3763
|
extendEnv: false,
|
|
3095
3764
|
detached: platform() !== "win32",
|
|
@@ -3181,7 +3850,7 @@ var run = (options) => Effect3.callback((resume, signal) => {
|
|
|
3181
3850
|
import { Effect as Effect10, Schema as Schema11 } from "effect";
|
|
3182
3851
|
import { access as access4, mkdir as mkdir7, readFile as readFile4, stat as stat4, writeFile as writeFile7 } from "node:fs/promises";
|
|
3183
3852
|
import { homedir as homedir4 } from "node:os";
|
|
3184
|
-
import { dirname as dirname6, join as join8, resolve as
|
|
3853
|
+
import { dirname as dirname6, join as join8, resolve as resolve4 } from "node:path";
|
|
3185
3854
|
|
|
3186
3855
|
// skills/cohall/SKILL.md
|
|
3187
3856
|
var SKILL_default = `---
|
|
@@ -3193,7 +3862,8 @@ description: Delegate through Cohall to another user-owned device or an existing
|
|
|
3193
3862
|
|
|
3194
3863
|
Cohall sends a focused task to a coding agent or an existing named Grok Bot. The target
|
|
3195
3864
|
keeps its local files, credentials, browser state, provider login, skills, and
|
|
3196
|
-
permissions. The relay carries prompts,
|
|
3865
|
+
permissions. The relay carries prompts, questions and answers, final results,
|
|
3866
|
+
task state, and explicitly attached files.
|
|
3197
3867
|
|
|
3198
3868
|
Use the installed \`cohall\` executable when it is available. Fall back to
|
|
3199
3869
|
\`npx -y @akshar5/cohall\` only when Cohall is not installed globally.
|
|
@@ -3250,9 +3920,19 @@ Do not delegate ordinary local work when the other device provides no advantage.
|
|
|
3250
3920
|
to use Codex on a computer; selecting a named bot infers \`grok-bot\`.
|
|
3251
3921
|
Omit \`--target\` only when Cohall may choose a matching computer.
|
|
3252
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
|
+
|
|
3253
3932
|
5. The command waits by default and returns JSON. Treat work as successful only
|
|
3254
3933
|
when \`status\` is \`completed\`; use \`result\` in the current task. Report a
|
|
3255
|
-
\`failed\`, \`cancelled\`, or \`cancelling\` state accurately.
|
|
3934
|
+
\`failed\`, \`cancelled\`, or \`cancelling\` state accurately. Handle \`needs_input\`
|
|
3935
|
+
through the clarification workflow below.
|
|
3256
3936
|
|
|
3257
3937
|
Pass an explicit local file with \`--attach path\` when the coding task needs
|
|
3258
3938
|
it. The MCP \`delegate\` tool takes \`attachment_paths\`. A task accepts up to
|
|
@@ -3279,9 +3959,32 @@ and \`COHALL_TASK_ID\` carry its thread and parent task. Pass \`--thread\` and
|
|
|
3279
3959
|
\`--parent\` explicitly from a Grok Bot using the IDs in its handoff.
|
|
3280
3960
|
|
|
3281
3961
|
For work sent with \`--no-wait\`, check \`cohall inbox\` when returning to the task.
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
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.
|
|
3285
3988
|
|
|
3286
3989
|
## Message named bots
|
|
3287
3990
|
|
|
@@ -3311,11 +4014,13 @@ Different bots and the computer's coding agent can run concurrently. Each
|
|
|
3311
4014
|
individual bot processes one Cohall request at a time.
|
|
3312
4015
|
|
|
3313
4016
|
When receiving a Cohall request inside a Grok Bot, finish with the \`cohall reply\`
|
|
3314
|
-
command supplied in the handoff
|
|
3315
|
-
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
|
|
3316
4019
|
to finish. This local receipt is what returns the result to the sending device;
|
|
3317
4020
|
a chat message alone does not complete the Cohall task. The receipt survives
|
|
3318
|
-
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.
|
|
3319
4024
|
|
|
3320
4025
|
## Context and safety
|
|
3321
4026
|
|
|
@@ -3323,7 +4028,7 @@ worker restarts and works while the relay is temporarily unreachable.
|
|
|
3323
4028
|
- Add new relevant developments to \`--context\` when following up from another chat.
|
|
3324
4029
|
- Never send provider credentials, Cohall tokens, cookies, or browser-profile data.
|
|
3325
4030
|
- Request a path only when the target advertises a matching workspace root.
|
|
3326
|
-
-
|
|
4031
|
+
- Resume a clarification on its existing task instead of creating duplicate work.
|
|
3327
4032
|
- Do not submit the same work through both CLI and MCP.
|
|
3328
4033
|
- Respect user confirmation requirements for consequential actions on the target.
|
|
3329
4034
|
|
|
@@ -3344,16 +4049,25 @@ The relay itself must be reachable to accept a new task. Interrupted execution
|
|
|
3344
4049
|
uses at-least-once delivery and may run again, so make consequential prompts safe
|
|
3345
4050
|
to retry.
|
|
3346
4051
|
|
|
3347
|
-
The timeout error includes the task ID and last known status.
|
|
3348
|
-
|
|
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:
|
|
3349
4061
|
|
|
3350
4062
|
\`\`\`bash
|
|
3351
4063
|
cohall cancel <task-id>
|
|
3352
4064
|
\`\`\`
|
|
3353
4065
|
|
|
3354
|
-
For coding agents,
|
|
3355
|
-
|
|
3356
|
-
|
|
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
|
|
3357
4071
|
cannot safely cancel an individual Cohall request.
|
|
3358
4072
|
|
|
3359
4073
|
## Read shared context
|
|
@@ -3387,9 +4101,10 @@ Trace a known task before inspecting machine-local service logs:
|
|
|
3387
4101
|
cohall trace <task-id> --follow
|
|
3388
4102
|
\`\`\`
|
|
3389
4103
|
|
|
3390
|
-
The trace
|
|
3391
|
-
and terminal state.
|
|
3392
|
-
|
|
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.
|
|
3393
4108
|
|
|
3394
4109
|
If Cohall cannot connect or no target is available, run:
|
|
3395
4110
|
|
|
@@ -3427,7 +4142,8 @@ var ClientConfiguration = Schema4.Struct({
|
|
|
3427
4142
|
relayUrl: Schema4.NonEmptyString,
|
|
3428
4143
|
token: Schema4.NonEmptyString,
|
|
3429
4144
|
mcpThreadId: Schema4.optionalKey(Schema4.String),
|
|
3430
|
-
mcpTaskId: Schema4.optionalKey(Schema4.String)
|
|
4145
|
+
mcpTaskId: Schema4.optionalKey(Schema4.String),
|
|
4146
|
+
mcpTaskRunId: Schema4.optionalKey(TaskRunId)
|
|
3431
4147
|
});
|
|
3432
4148
|
var DeviceConfiguration = Schema4.Struct({
|
|
3433
4149
|
relayUrl: Schema4.NonEmptyString,
|
|
@@ -3590,7 +4306,8 @@ var loadClientConfiguration = Effect4.tryPromise({
|
|
|
3590
4306
|
relayUrl,
|
|
3591
4307
|
token,
|
|
3592
4308
|
...process.env.COHALL_THREAD_ID === undefined ? {} : { mcpThreadId: process.env.COHALL_THREAD_ID },
|
|
3593
|
-
...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) }
|
|
3594
4311
|
});
|
|
3595
4312
|
},
|
|
3596
4313
|
catch: (cause) => cause instanceof Error ? cause : new Error(String(cause))
|
|
@@ -3654,7 +4371,7 @@ import { Effect as Effect5, Schedule, Schema as Schema5 } from "effect";
|
|
|
3654
4371
|
class DeviceSelectionError extends Schema5.TaggedErrorClass()("Cohall.DeviceSelectionError", { message: Schema5.String, devices: Schema5.Array(Schema5.String) }) {
|
|
3655
4372
|
}
|
|
3656
4373
|
|
|
3657
|
-
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) }) {
|
|
3658
4375
|
}
|
|
3659
4376
|
var TaskResult = Schema5.Struct({
|
|
3660
4377
|
task_id: TaskId,
|
|
@@ -3665,7 +4382,10 @@ var TaskResult = Schema5.Struct({
|
|
|
3665
4382
|
bot_id: Schema5.optionalKey(BotId),
|
|
3666
4383
|
result: Schema5.optionalKey(Schema5.String),
|
|
3667
4384
|
error: Schema5.optionalKey(Schema5.String),
|
|
3668
|
-
inbox_warning: Schema5.optionalKey(Schema5.String)
|
|
4385
|
+
inbox_warning: Schema5.optionalKey(Schema5.String),
|
|
4386
|
+
progress: Schema5.optionalKey(TaskProgress),
|
|
4387
|
+
input_request: Schema5.optionalKey(TaskClarification),
|
|
4388
|
+
expires_at: Schema5.optionalKey(TaskDeadline)
|
|
3669
4389
|
});
|
|
3670
4390
|
var matchingDevices = (devices, target) => {
|
|
3671
4391
|
const value = target.replace(/^@/, "");
|
|
@@ -3709,21 +4429,33 @@ var selectTarget = Effect5.fn("Cohall.selectTarget")(function* (devices, target)
|
|
|
3709
4429
|
]
|
|
3710
4430
|
});
|
|
3711
4431
|
});
|
|
3712
|
-
var taskResult = (task) =>
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
}
|
|
3722
|
-
|
|
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();
|
|
3723
4450
|
const result = taskResult(task);
|
|
3724
|
-
|
|
4451
|
+
if (!isTerminalTask(task))
|
|
4452
|
+
return result;
|
|
4453
|
+
const acknowledgementError = await Effect5.runPromise(client.acknowledgeCompletion(task.id), {
|
|
4454
|
+
signal
|
|
4455
|
+
}).then(() => {
|
|
3725
4456
|
return;
|
|
3726
4457
|
}).catch((cause) => {
|
|
4458
|
+
signal?.throwIfAborted();
|
|
3727
4459
|
if (cause instanceof RelayRequestError && cause.status === 404)
|
|
3728
4460
|
return;
|
|
3729
4461
|
return cause instanceof Error ? cause.message : String(cause);
|
|
@@ -3733,16 +4465,51 @@ var acknowledgedTaskResult = async (client, task) => {
|
|
|
3733
4465
|
inbox_warning: `Result received, but the inbox could not be cleared: ${acknowledgementError}`
|
|
3734
4466
|
});
|
|
3735
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
|
+
});
|
|
3736
4484
|
var createDelegation = Effect5.fn("Cohall.createDelegation")(function* (client, configuration, options) {
|
|
3737
|
-
|
|
3738
|
-
|
|
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) {
|
|
3739
4506
|
return yield* new DeviceSelectionError({
|
|
3740
4507
|
message: "No Cohall devices are registered",
|
|
3741
4508
|
devices: []
|
|
3742
4509
|
});
|
|
3743
4510
|
}
|
|
3744
|
-
let target = options.target === undefined ? undefined : yield* selectTarget(devices, options.target);
|
|
3745
|
-
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) {
|
|
3746
4513
|
const context = yield* client.threadContext(options.threadId);
|
|
3747
4514
|
const previous = context.tasks.findLast((task) => task.parentTaskId === undefined);
|
|
3748
4515
|
if (previous === undefined && context.truncated) {
|
|
@@ -3773,6 +4540,7 @@ var createDelegation = Effect5.fn("Cohall.createDelegation")(function* (client,
|
|
|
3773
4540
|
const parentTaskId = options.parentTaskId ?? (configuration.mcpTaskId === undefined ? undefined : yield* Schema5.decodeUnknownEffect(TaskId)(configuration.mcpTaskId));
|
|
3774
4541
|
return yield* client.createTask({
|
|
3775
4542
|
prompt: options.prompt,
|
|
4543
|
+
...options.requestId === undefined ? {} : { requestId: options.requestId },
|
|
3776
4544
|
...target === undefined ? {} : { targetDeviceId: target.deviceId },
|
|
3777
4545
|
...botId === undefined ? {} : { botId },
|
|
3778
4546
|
...options.threadId !== undefined ? { threadId: options.threadId } : inheritedThread === undefined ? {} : { threadId: ThreadId.make(inheritedThread) },
|
|
@@ -3780,30 +4548,35 @@ var createDelegation = Effect5.fn("Cohall.createDelegation")(function* (client,
|
|
|
3780
4548
|
...options.workspace === undefined ? {} : { workspace: options.workspace },
|
|
3781
4549
|
...provider === undefined ? {} : { provider },
|
|
3782
4550
|
...parentTaskId === undefined ? {} : { parentTaskId },
|
|
3783
|
-
...options.attachments === undefined ? {} : { attachments: options.attachments }
|
|
4551
|
+
...options.attachments === undefined ? {} : { attachments: options.attachments },
|
|
4552
|
+
...options.expiresAt === undefined ? {} : { expiresAt: options.expiresAt }
|
|
3784
4553
|
});
|
|
3785
4554
|
});
|
|
3786
4555
|
var waitForTask = Effect5.fn("Cohall.waitForTask")(function* (client, initial, timeoutSeconds) {
|
|
3787
|
-
const
|
|
3788
|
-
let task = initial;
|
|
3789
|
-
|
|
3790
|
-
if (
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
});
|
|
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);
|
|
3796
4564
|
}
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
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
|
+
}));
|
|
3801
4574
|
});
|
|
3802
4575
|
var followTaskTrace = Effect5.fn("Cohall.followTaskTrace")(function* (client, taskId, onTrace) {
|
|
3803
4576
|
let revision;
|
|
3804
4577
|
const poll = client.traceTask(taskId).pipe(Effect5.tap((trace) => {
|
|
3805
4578
|
const latest = trace.events.at(-1);
|
|
3806
|
-
const nextRevision = `${trace.status}:${latest?.kind ?? "none"}:${latest?.at ?? "none"}`;
|
|
4579
|
+
const nextRevision = `${trace.status}:${latest?.kind ?? "none"}:${latest?.at ?? "none"}:${trace.progress?.at ?? ""}:${trace.progress?.note ?? ""}:${JSON.stringify(trace.clarifications)}`;
|
|
3807
4580
|
if (nextRevision === revision) {
|
|
3808
4581
|
return Effect5.succeed(undefined);
|
|
3809
4582
|
}
|
|
@@ -3850,6 +4623,7 @@ var allDeviceHealth = (devices, localVersion) => {
|
|
|
3850
4623
|
providers: device.providers,
|
|
3851
4624
|
workspaces: device.workspaces.map((workspace) => workspace.path),
|
|
3852
4625
|
last_seen_at: device.lastSeenAt,
|
|
4626
|
+
...device.queue === undefined ? {} : { queue: device.queue },
|
|
3853
4627
|
warnings
|
|
3854
4628
|
};
|
|
3855
4629
|
});
|
|
@@ -3888,11 +4662,18 @@ var Dispatch = Schema6.Struct({ taskId: TaskId });
|
|
|
3888
4662
|
var Reply = Schema6.Union([
|
|
3889
4663
|
Schema6.Struct({
|
|
3890
4664
|
result: Schema6.String.check(Schema6.isMaxLength(131072)),
|
|
3891
|
-
error: Schema6.optionalKey(Schema6.Never)
|
|
4665
|
+
error: Schema6.optionalKey(Schema6.Never),
|
|
4666
|
+
question: Schema6.optionalKey(Schema6.Never)
|
|
3892
4667
|
}),
|
|
3893
4668
|
Schema6.Struct({
|
|
3894
4669
|
error: Schema6.NonEmptyString.check(Schema6.isMaxLength(16384)),
|
|
3895
|
-
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)
|
|
3896
4677
|
})
|
|
3897
4678
|
]);
|
|
3898
4679
|
var Receipt = Schema6.Struct({
|
|
@@ -3902,14 +4683,15 @@ var Receipt = Schema6.Struct({
|
|
|
3902
4683
|
});
|
|
3903
4684
|
var isMissing = (cause) => cause instanceof Error && ("code" in cause) && cause.code === "ENOENT";
|
|
3904
4685
|
var alreadyExists = (cause) => cause instanceof Error && ("code" in cause) && cause.code === "EEXIST";
|
|
3905
|
-
var paths = (taskId) => {
|
|
4686
|
+
var paths = (taskId, runId) => {
|
|
3906
4687
|
const id = Schema6.decodeUnknownSync(TaskId)(taskId);
|
|
3907
4688
|
const directory = join3(dirname2(configurationPath()), "bot-replies");
|
|
4689
|
+
const key = runId === undefined ? id : `${id}.${Schema6.decodeUnknownSync(TaskRunId)(runId)}`;
|
|
3908
4690
|
return {
|
|
3909
4691
|
directory,
|
|
3910
|
-
pending: join3(directory, `${
|
|
3911
|
-
receipt: join3(directory, `${
|
|
3912
|
-
dispatch: join3(directory, `${
|
|
4692
|
+
pending: join3(directory, `${key}.pending.json`),
|
|
4693
|
+
receipt: join3(directory, `${key}.reply.json`),
|
|
4694
|
+
dispatch: join3(directory, `${key}.dispatch.json`)
|
|
3913
4695
|
};
|
|
3914
4696
|
};
|
|
3915
4697
|
var readJson = async (path, limit) => {
|
|
@@ -3938,8 +4720,8 @@ var readJson = async (path, limit) => {
|
|
|
3938
4720
|
await file.close();
|
|
3939
4721
|
}
|
|
3940
4722
|
};
|
|
3941
|
-
var pendingReply = async (taskId) => {
|
|
3942
|
-
const value = await readJson(paths(taskId).pending, 4096);
|
|
4723
|
+
var pendingReply = async (taskId, runId) => {
|
|
4724
|
+
const value = await readJson(paths(taskId, runId).pending, 4096);
|
|
3943
4725
|
if (value === undefined)
|
|
3944
4726
|
return;
|
|
3945
4727
|
const decoded = Schema6.decodeUnknownResult(Pending)(value);
|
|
@@ -3982,7 +4764,7 @@ var prepareBotReply = async (task) => {
|
|
|
3982
4764
|
if (task.provider !== "grok-bot" || task.botId === undefined) {
|
|
3983
4765
|
throw new Error("Only a named Grok Bot task can receive a bot reply");
|
|
3984
4766
|
}
|
|
3985
|
-
const files = paths(task.id);
|
|
4767
|
+
const files = paths(task.id, task.runId);
|
|
3986
4768
|
await mkdir2(files.directory, { recursive: true, mode: 448 });
|
|
3987
4769
|
await chmod3(files.directory, 448);
|
|
3988
4770
|
const record2 = Pending.make({
|
|
@@ -3992,7 +4774,7 @@ var prepareBotReply = async (task) => {
|
|
|
3992
4774
|
deadline: Date.now() + replyLifetimeMs
|
|
3993
4775
|
});
|
|
3994
4776
|
await publish(files.pending, record2);
|
|
3995
|
-
const pending = await pendingReply(task.id);
|
|
4777
|
+
const pending = await pendingReply(task.id, task.runId);
|
|
3996
4778
|
if (pending === undefined || pending.botId !== task.botId || pending.threadId !== task.threadId) {
|
|
3997
4779
|
throw new Error("Bot reply manifest does not match this task");
|
|
3998
4780
|
}
|
|
@@ -4009,19 +4791,19 @@ var prepareBotReply = async (task) => {
|
|
|
4009
4791
|
dispatched: dispatch !== undefined
|
|
4010
4792
|
};
|
|
4011
4793
|
};
|
|
4012
|
-
var claimBotDispatch = async (taskId) => {
|
|
4013
|
-
const pending = await pendingReply(taskId);
|
|
4794
|
+
var claimBotDispatch = async (taskId, runId) => {
|
|
4795
|
+
const pending = await pendingReply(taskId, runId);
|
|
4014
4796
|
if (pending === undefined)
|
|
4015
4797
|
throw new Error("No pending bot reply for this task");
|
|
4016
4798
|
if (Date.now() >= pending.deadline)
|
|
4017
4799
|
throw new Error("Bot reply deadline has expired");
|
|
4018
|
-
return publish(paths(taskId).dispatch, { taskId });
|
|
4800
|
+
return publish(paths(taskId, runId).dispatch, { taskId });
|
|
4019
4801
|
};
|
|
4020
|
-
var readBotReply = async (taskId) => {
|
|
4021
|
-
const pending = await pendingReply(taskId);
|
|
4802
|
+
var readBotReply = async (taskId, runId) => {
|
|
4803
|
+
const pending = await pendingReply(taskId, runId);
|
|
4022
4804
|
if (pending === undefined)
|
|
4023
4805
|
return;
|
|
4024
|
-
const value = await readJson(paths(taskId).receipt, 1024 * 1024);
|
|
4806
|
+
const value = await readJson(paths(taskId, runId).receipt, 1024 * 1024);
|
|
4025
4807
|
if (value === undefined)
|
|
4026
4808
|
return;
|
|
4027
4809
|
const decoded = Schema6.decodeUnknownResult(Receipt)(value);
|
|
@@ -4033,15 +4815,15 @@ var readBotReply = async (taskId) => {
|
|
|
4033
4815
|
}
|
|
4034
4816
|
return decoded.success.reply;
|
|
4035
4817
|
};
|
|
4036
|
-
var writeBotReply = async (taskId, reply) => {
|
|
4818
|
+
var writeBotReply = async (taskId, reply, runId) => {
|
|
4037
4819
|
const decoded = Schema6.decodeUnknownResult(Reply)(reply);
|
|
4038
4820
|
if (decoded._tag === "Failure") {
|
|
4039
|
-
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");
|
|
4040
4822
|
}
|
|
4041
|
-
const pending = await pendingReply(taskId);
|
|
4823
|
+
const pending = await pendingReply(taskId, runId);
|
|
4042
4824
|
if (pending === undefined)
|
|
4043
4825
|
throw new Error("No pending bot reply for this task");
|
|
4044
|
-
const existing = await readBotReply(taskId);
|
|
4826
|
+
const existing = await readBotReply(taskId, runId);
|
|
4045
4827
|
if (existing !== undefined) {
|
|
4046
4828
|
if (JSON.stringify(existing) === JSON.stringify(decoded.success))
|
|
4047
4829
|
return;
|
|
@@ -4050,15 +4832,15 @@ var writeBotReply = async (taskId, reply) => {
|
|
|
4050
4832
|
const submittedAt = Date.now();
|
|
4051
4833
|
if (submittedAt >= pending.deadline)
|
|
4052
4834
|
throw new Error("Bot reply deadline has expired");
|
|
4053
|
-
if (await publish(paths(taskId).receipt, { taskId, submittedAt, reply: decoded.success }))
|
|
4835
|
+
if (await publish(paths(taskId, runId).receipt, { taskId, submittedAt, reply: decoded.success }))
|
|
4054
4836
|
return;
|
|
4055
|
-
const winner = await readBotReply(taskId);
|
|
4837
|
+
const winner = await readBotReply(taskId, runId);
|
|
4056
4838
|
if (JSON.stringify(winner) !== JSON.stringify(decoded.success)) {
|
|
4057
4839
|
throw new Error("A different reply has already been submitted for this task");
|
|
4058
4840
|
}
|
|
4059
4841
|
};
|
|
4060
|
-
var cleanupBotReply = async (taskId) => {
|
|
4061
|
-
const files = paths(taskId);
|
|
4842
|
+
var cleanupBotReply = async (taskId, runId) => {
|
|
4843
|
+
const files = paths(taskId, runId);
|
|
4062
4844
|
for (const path of [files.pending, files.receipt, files.dispatch]) {
|
|
4063
4845
|
await unlink(path).catch((cause) => {
|
|
4064
4846
|
if (!isMissing(cause))
|
|
@@ -4208,29 +4990,43 @@ var requireTime = (deadline) => {
|
|
|
4208
4990
|
throw new Error(timeoutMessage);
|
|
4209
4991
|
};
|
|
4210
4992
|
var finishReply = (reply) => {
|
|
4993
|
+
if (reply.question !== undefined)
|
|
4994
|
+
return { result: "", question: reply.question };
|
|
4211
4995
|
if (reply.error !== undefined)
|
|
4212
4996
|
throw new Error(reply.error);
|
|
4213
4997
|
if (reply.result === undefined)
|
|
4214
4998
|
throw new Error("Grok Bot returned an invalid Cohall reply");
|
|
4215
4999
|
return { result: reply.result };
|
|
4216
5000
|
};
|
|
4217
|
-
var taskPrompt = (task, command) =>
|
|
4218
|
-
task.
|
|
4219
|
-
|
|
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:
|
|
4220
5006
|
${task.context}`,
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
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(`
|
|
4230
5025
|
|
|
4231
5026
|
`);
|
|
4232
|
-
|
|
4233
|
-
|
|
5027
|
+
};
|
|
5028
|
+
var runGrokBot = async (path, task, signal, supportsClarification = false) => {
|
|
5029
|
+
const existing = await readBotReply(task.id, task.runId);
|
|
4234
5030
|
if (existing !== undefined)
|
|
4235
5031
|
return finishReply(existing);
|
|
4236
5032
|
const { deadline, command, dispatched } = await prepareBotReply(task);
|
|
@@ -4241,11 +5037,11 @@ var runGrokBot = async (path, task, signal) => {
|
|
|
4241
5037
|
const lookup = async (lookupSignal = signal) => {
|
|
4242
5038
|
if (path === undefined)
|
|
4243
5039
|
throw new Error("Configure this device's Grok Bot gateway discovery file first");
|
|
4244
|
-
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);
|
|
4245
5041
|
};
|
|
4246
5042
|
const waitForReply = (reconcile = false) => {
|
|
4247
5043
|
const receipt = poll(async () => {
|
|
4248
|
-
const receipt2 = await readBotReply(task.id);
|
|
5044
|
+
const receipt2 = await readBotReply(task.id, task.runId);
|
|
4249
5045
|
if (receipt2 !== undefined)
|
|
4250
5046
|
return finishReply(receipt2);
|
|
4251
5047
|
requireTime(deadline);
|
|
@@ -4274,7 +5070,7 @@ var runGrokBot = async (path, task, signal) => {
|
|
|
4274
5070
|
if (acceptance.outcome === "unknown-durability")
|
|
4275
5071
|
throw new Error("Grok Bot cannot confirm whether this task was accepted; check its conversation before retrying");
|
|
4276
5072
|
if (acceptance.outcome === "not-found") {
|
|
4277
|
-
const receipt = await readBotReply(task.id);
|
|
5073
|
+
const receipt = await readBotReply(task.id, task.runId);
|
|
4278
5074
|
if (receipt !== undefined)
|
|
4279
5075
|
return finishReply(receipt);
|
|
4280
5076
|
requireTime(deadline);
|
|
@@ -4286,20 +5082,20 @@ var runGrokBot = async (path, task, signal) => {
|
|
|
4286
5082
|
acceptance = await lookup();
|
|
4287
5083
|
if (acceptance.outcome === "not-found") {
|
|
4288
5084
|
requireTime(deadline);
|
|
4289
|
-
if (!await claimBotDispatch(task.id)) {
|
|
5085
|
+
if (!await claimBotDispatch(task.id, task.runId)) {
|
|
4290
5086
|
return waitForReply(true);
|
|
4291
5087
|
}
|
|
4292
5088
|
try {
|
|
4293
5089
|
await rpc(connection, "sendPrompt", {
|
|
4294
5090
|
agentId,
|
|
4295
|
-
prompt: taskPrompt(task, command),
|
|
4296
|
-
clientNonce: task.id,
|
|
5091
|
+
prompt: taskPrompt(task, command, supportsClarification),
|
|
5092
|
+
clientNonce: task.runId ?? task.id,
|
|
4297
5093
|
directAddressedAcceptance: true
|
|
4298
5094
|
}, SendResult, signal);
|
|
4299
5095
|
} catch {
|
|
4300
5096
|
if (signal.aborted)
|
|
4301
5097
|
throw new Error("Stopped waiting for Grok Bot; its run may still be active");
|
|
4302
|
-
const receipt2 = await readBotReply(task.id);
|
|
5098
|
+
const receipt2 = await readBotReply(task.id, task.runId);
|
|
4303
5099
|
if (receipt2 !== undefined)
|
|
4304
5100
|
return finishReply(receipt2);
|
|
4305
5101
|
return waitForReply(true);
|
|
@@ -4390,9 +5186,13 @@ var guidedSetupInput = async (options, prompter) => {
|
|
|
4390
5186
|
const relayUrl = normalizeRelayUrl(options.relayUrl ?? await prompter.answer("Relay URL", existing?.relayUrl ?? "http://127.0.0.1:8787"));
|
|
4391
5187
|
const credentials = credentialsForRelay(existing, relayUrl);
|
|
4392
5188
|
const hasRequiredCredentials = credentials.clientToken !== undefined && (options.clientOnly || credentials.deviceToken !== undefined);
|
|
4393
|
-
|
|
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
|
+
}
|
|
4394
5194
|
const deviceName = options.clientOnly ? undefined : options.deviceName ?? await prompter.answer("Device name", existing?.deviceName ?? hostname2());
|
|
4395
|
-
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"));
|
|
4396
5196
|
const token = options.token ?? (hasRequiredCredentials ? undefined : await prompter.secret("Pairing token"));
|
|
4397
5197
|
if (!hasRequiredCredentials && (token === undefined || token.length === 0)) {
|
|
4398
5198
|
throw new Error("A pairing token is required");
|
|
@@ -4423,21 +5223,21 @@ import {
|
|
|
4423
5223
|
mkdir as mkdir5,
|
|
4424
5224
|
mkdtemp as mkdtemp2,
|
|
4425
5225
|
readFile as readFile3,
|
|
4426
|
-
realpath as
|
|
5226
|
+
realpath as realpath3,
|
|
4427
5227
|
rename as rename3,
|
|
4428
5228
|
rm as rm3,
|
|
4429
5229
|
stat as stat3,
|
|
4430
5230
|
writeFile as writeFile5
|
|
4431
5231
|
} from "node:fs/promises";
|
|
4432
|
-
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";
|
|
4433
5233
|
import { backup as sqliteBackup, DatabaseSync } from "node:sqlite";
|
|
4434
5234
|
import { WebSocket } from "ws";
|
|
4435
5235
|
|
|
4436
5236
|
// apps/device/src/service.ts
|
|
4437
5237
|
import { execFile as execFile2 } from "node:child_process";
|
|
4438
|
-
import { chmod as chmod5, mkdir as mkdir4,
|
|
5238
|
+
import { chmod as chmod5, mkdir as mkdir4, writeFile as writeFile4 } from "node:fs/promises";
|
|
4439
5239
|
import { homedir as homedir3 } from "node:os";
|
|
4440
|
-
import { dirname as dirname4, join as join5 } from "node:path";
|
|
5240
|
+
import { dirname as dirname4, isAbsolute as isAbsolute3, join as join5 } from "node:path";
|
|
4441
5241
|
|
|
4442
5242
|
// apps/device/src/upgrade.ts
|
|
4443
5243
|
import { Schema as Schema8 } from "effect";
|
|
@@ -4448,16 +5248,61 @@ import {
|
|
|
4448
5248
|
chmod as chmod4,
|
|
4449
5249
|
mkdir as mkdir3,
|
|
4450
5250
|
readFile as readFile2,
|
|
5251
|
+
readdir,
|
|
4451
5252
|
realpath as realpath2,
|
|
4452
5253
|
rename as rename2,
|
|
4453
5254
|
rm as rm2,
|
|
4454
5255
|
stat as stat2,
|
|
4455
5256
|
writeFile as writeFile3
|
|
4456
5257
|
} from "node:fs/promises";
|
|
4457
|
-
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";
|
|
4458
5259
|
import { platform as operatingSystem } from "node:os";
|
|
4459
5260
|
var packageName = "@akshar5/cohall";
|
|
4460
|
-
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
|
+
};
|
|
4461
5306
|
var PackageManager = Schema8.Literals(["npm", "bun", "pnpm"]);
|
|
4462
5307
|
var RestartReceipt = Schema8.Struct({
|
|
4463
5308
|
version: Schema8.String,
|
|
@@ -4467,45 +5312,165 @@ var RestartReceipt = Schema8.Struct({
|
|
|
4467
5312
|
restartedServices: Schema8.Array(Schema8.String),
|
|
4468
5313
|
restartingService: Schema8.optionalKey(Schema8.String)
|
|
4469
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
|
+
]);
|
|
4470
5325
|
var normalizePath = (path) => path.replaceAll("\\", "/");
|
|
5326
|
+
var pnpmGlobalDirectory = /^(.*\/global\/v?\d+)\//;
|
|
4471
5327
|
var normalizeUpgradeTarget = (target) => {
|
|
4472
5328
|
if (target === undefined || target === "latest") {
|
|
4473
5329
|
return "latest";
|
|
4474
5330
|
}
|
|
4475
5331
|
const normalized = target.startsWith("v") ? target.slice(1) : target;
|
|
4476
|
-
if (
|
|
5332
|
+
if (parseVersion(normalized) === undefined) {
|
|
4477
5333
|
throw new Error("--to must be latest or an exact semantic version");
|
|
4478
5334
|
}
|
|
4479
5335
|
return normalized;
|
|
4480
5336
|
};
|
|
4481
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.`;
|
|
4482
|
-
var packageInstallation = (canonicalEntrypoint, entrypoint = canonicalEntrypoint) => {
|
|
5338
|
+
var packageInstallation = (canonicalEntrypoint, entrypoint = canonicalEntrypoint, runtimePlatform = process.platform) => {
|
|
4483
5339
|
const path = normalizePath(canonicalEntrypoint);
|
|
4484
5340
|
const marker = `/node_modules/${packageName}/`;
|
|
4485
5341
|
const packageIndex = path.lastIndexOf(marker);
|
|
4486
5342
|
if (packageIndex === -1) {
|
|
4487
5343
|
throw new Error("cohall upgrade requires a global npm, Bun, or pnpm installation; package-runner and source-checkout executions cannot upgrade themselves");
|
|
4488
5344
|
}
|
|
4489
|
-
if (path.includes("/.npm/_npx/") ||
|
|
5345
|
+
if ([path, normalizePath(entrypoint)].some((candidate) => candidate.includes("/.npm/_npx/") || candidate.includes("/.bunx/") || candidate.includes("/dlx/"))) {
|
|
4490
5346
|
throw new Error("This Cohall process is running from a temporary package-runner cache; install it globally before using cohall upgrade");
|
|
4491
5347
|
}
|
|
4492
5348
|
if (path.includes("/.bun/install/global/node_modules/")) {
|
|
4493
5349
|
return { manager: "bun", entrypoint };
|
|
4494
5350
|
}
|
|
4495
|
-
if (path.includes("/pnpm/
|
|
5351
|
+
if (pnpmGlobalDirectory.test(path) || /\/v\d+\/links\//.test(path) || path.includes("/node_modules/.pnpm/") || pnpmGlobalDirectory.test(normalizePath(entrypoint))) {
|
|
4496
5352
|
return { manager: "pnpm", entrypoint };
|
|
4497
5353
|
}
|
|
4498
5354
|
const nodeModulesParent = path.slice(0, packageIndex);
|
|
4499
|
-
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;
|
|
4500
5356
|
return { manager: "npm", prefix, entrypoint };
|
|
4501
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
|
+
};
|
|
4502
5453
|
var packageInstallCommand = (installation, target) => {
|
|
4503
5454
|
const specification = `${packageName}@${target}`;
|
|
4504
5455
|
switch (installation.manager) {
|
|
4505
5456
|
case "bun":
|
|
4506
|
-
return {
|
|
5457
|
+
return {
|
|
5458
|
+
command: "bun",
|
|
5459
|
+
arguments: ["add", "--global", specification],
|
|
5460
|
+
...installation.globalDir === undefined ? {} : { environment: { BUN_INSTALL_GLOBAL_DIR: installation.globalDir } }
|
|
5461
|
+
};
|
|
4507
5462
|
case "pnpm":
|
|
4508
|
-
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
|
+
};
|
|
4509
5474
|
case "npm":
|
|
4510
5475
|
return {
|
|
4511
5476
|
command: "npm",
|
|
@@ -4544,36 +5509,38 @@ var isTrustedGroupWritablePath = (options) => {
|
|
|
4544
5509
|
const writableRoot = options.writableRoot;
|
|
4545
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);
|
|
4546
5511
|
};
|
|
4547
|
-
var validateExecutable = async (candidate, installationRoot) => {
|
|
5512
|
+
var validateExecutable = async (candidate, installationRoot, preserveSymlink = false) => {
|
|
4548
5513
|
const canonical = await realpath2(candidate);
|
|
4549
5514
|
if (operatingSystem() === "win32") {
|
|
4550
|
-
return canonical;
|
|
5515
|
+
return preserveSymlink ? resolve2(candidate) : canonical;
|
|
4551
5516
|
}
|
|
4552
5517
|
const uid = process.getuid?.();
|
|
4553
5518
|
const writableRoot = installationRoot === undefined ? undefined : await realpath2(installationRoot);
|
|
4554
|
-
for (
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
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
|
+
}
|
|
4574
5541
|
}
|
|
4575
5542
|
}
|
|
4576
|
-
return canonical;
|
|
5543
|
+
return preserveSymlink ? resolve2(candidate) : canonical;
|
|
4577
5544
|
};
|
|
4578
5545
|
var trustedExecutable = async (command, options = {}) => {
|
|
4579
5546
|
const candidates = isAbsolute2(command) ? [command] : (process.env.PATH ?? "").split(delimiter2).filter(Boolean).flatMap((directory) => executableNames(command).map((name) => join4(directory, name)));
|
|
@@ -4583,7 +5550,7 @@ var trustedExecutable = async (command, options = {}) => {
|
|
|
4583
5550
|
if (!available2)
|
|
4584
5551
|
continue;
|
|
4585
5552
|
try {
|
|
4586
|
-
return await validateExecutable(path, options.writableRoot);
|
|
5553
|
+
return await validateExecutable(path, options.writableRoot, options.preserveSymlink);
|
|
4587
5554
|
} catch (cause) {
|
|
4588
5555
|
failure ??= cause;
|
|
4589
5556
|
}
|
|
@@ -4625,6 +5592,43 @@ var trustedServices = async (services, resolveExecutable) => {
|
|
|
4625
5592
|
}
|
|
4626
5593
|
return secured;
|
|
4627
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
|
+
};
|
|
4628
5632
|
var serviceCandidates = (runtimePlatform, uid) => {
|
|
4629
5633
|
if (runtimePlatform === "linux") {
|
|
4630
5634
|
const systemd = (manager, unit, device) => {
|
|
@@ -4684,6 +5688,18 @@ var serviceCandidates = (runtimePlatform, uid) => {
|
|
|
4684
5688
|
"if ((Get-ScheduledTask -TaskName 'Cohall Device' -ErrorAction SilentlyContinue).State -eq 'Running') { exit 0 } else { exit 1 }"
|
|
4685
5689
|
]
|
|
4686
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
|
+
},
|
|
4687
5703
|
restart: [
|
|
4688
5704
|
{
|
|
4689
5705
|
command: "schtasks.exe",
|
|
@@ -4698,13 +5714,14 @@ var serviceCandidates = (runtimePlatform, uid) => {
|
|
|
4698
5714
|
return [];
|
|
4699
5715
|
};
|
|
4700
5716
|
var defaultRunner = {
|
|
4701
|
-
run: async (command, arguments_, timeoutMs = 300000) => {
|
|
5717
|
+
run: async (command, arguments_, timeoutMs = 300000, environment) => {
|
|
4702
5718
|
const result = await execa2(command, arguments_, {
|
|
4703
5719
|
encoding: "utf8",
|
|
4704
5720
|
maxBuffer: 1024 * 1024,
|
|
4705
5721
|
timeout: timeoutMs,
|
|
4706
5722
|
windowsHide: true,
|
|
4707
5723
|
stdin: "ignore",
|
|
5724
|
+
...environment === undefined ? {} : { env: environment },
|
|
4708
5725
|
reject: false,
|
|
4709
5726
|
stripFinalNewline: false
|
|
4710
5727
|
});
|
|
@@ -4717,7 +5734,7 @@ var defaultRunner = {
|
|
|
4717
5734
|
}
|
|
4718
5735
|
};
|
|
4719
5736
|
var checked = async (runner, invocation, timeoutMs) => {
|
|
4720
|
-
const result = await runner.run(invocation.command, invocation.arguments, timeoutMs);
|
|
5737
|
+
const result = await runner.run(invocation.command, invocation.arguments, timeoutMs, invocation.environment);
|
|
4721
5738
|
if (result.exitCode === 0 || invocation.allowFailure === true) {
|
|
4722
5739
|
return result;
|
|
4723
5740
|
}
|
|
@@ -4735,7 +5752,7 @@ ${result.stderr}`))) {
|
|
|
4735
5752
|
}
|
|
4736
5753
|
return active;
|
|
4737
5754
|
};
|
|
4738
|
-
var assertServiceInstallations = async (runner, services,
|
|
5755
|
+
var assertServiceInstallations = async (runner, services, installation) => {
|
|
4739
5756
|
for (const service of services) {
|
|
4740
5757
|
if (service.entrypoint === undefined) {
|
|
4741
5758
|
continue;
|
|
@@ -4744,15 +5761,19 @@ var assertServiceInstallations = async (runner, services, canonicalEntrypoint, e
|
|
|
4744
5761
|
const serviceEntrypoint = service.entrypoint.parse(`${result.stdout}
|
|
4745
5762
|
${result.stderr}`);
|
|
4746
5763
|
if (serviceEntrypoint === undefined) {
|
|
4747
|
-
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." : ""}`);
|
|
4748
5765
|
}
|
|
4749
5766
|
const canonicalServiceEntrypoint = await realpath2(serviceEntrypoint).catch((cause) => {
|
|
4750
5767
|
throw new Error(`Could not resolve the executable used by active ${service.label}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
4751
5768
|
});
|
|
4752
|
-
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.`}`);
|
|
4753
5774
|
continue;
|
|
4754
5775
|
}
|
|
4755
|
-
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.`);
|
|
4756
5777
|
}
|
|
4757
5778
|
};
|
|
4758
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(() => {
|
|
@@ -4822,13 +5843,19 @@ var upgrade = async (options) => {
|
|
|
4822
5843
|
const candidates = await trustedServices(serviceCandidates(runtimePlatform, uid), resolveExecutable);
|
|
4823
5844
|
const statePath = options.statePath ?? restartReceiptPath();
|
|
4824
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
|
+
});
|
|
4825
5855
|
if (previous?.version === options.currentVersion && (target === "latest" || target === previous.version)) {
|
|
4826
|
-
const
|
|
4827
|
-
const pending = previous.pendingServices.flatMap((id) => {
|
|
4828
|
-
const service = byId.get(id);
|
|
4829
|
-
return service === undefined ? [] : [service];
|
|
4830
|
-
});
|
|
5856
|
+
const pending = candidates.filter((service) => previous.pendingServices.includes(service.id));
|
|
4831
5857
|
const active = await activeServices(runner, pending);
|
|
5858
|
+
await assertServiceInstallations(runner, active, installation);
|
|
4832
5859
|
if (options.dryRun || !options.restart) {
|
|
4833
5860
|
return {
|
|
4834
5861
|
upgraded: previous.fromVersion !== previous.version,
|
|
@@ -4837,13 +5864,13 @@ var upgrade = async (options) => {
|
|
|
4837
5864
|
requested_version: previous.version,
|
|
4838
5865
|
package_manager: previous.packageManager,
|
|
4839
5866
|
services_restarted: previous.restartedServices,
|
|
4840
|
-
services_pending_restart:
|
|
5867
|
+
services_pending_restart: previous.pendingServices,
|
|
4841
5868
|
resumed_after_restart: false,
|
|
4842
5869
|
dry_run: options.dryRun
|
|
4843
5870
|
};
|
|
4844
5871
|
}
|
|
4845
5872
|
const activeIds = new Set(active.map((service) => service.id));
|
|
4846
|
-
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));
|
|
4847
5874
|
const completedIds = new Set(completedByRestart.map((service) => service.id));
|
|
4848
5875
|
const remaining = active.filter((service) => !completedIds.has(service.id));
|
|
4849
5876
|
let resumed = {
|
|
@@ -4859,8 +5886,10 @@ var upgrade = async (options) => {
|
|
|
4859
5886
|
if (remaining.length > 0) {
|
|
4860
5887
|
await writeReceipt(statePath, resumed);
|
|
4861
5888
|
resumed = await restartServices(runner, remaining, statePath, resumed, options.delegated === true);
|
|
5889
|
+
} else if (completedIds.size > 0) {
|
|
5890
|
+
await writeReceipt(statePath, resumed);
|
|
4862
5891
|
}
|
|
4863
|
-
if (resumed.
|
|
5892
|
+
if (resumed.pendingServices.length === 0) {
|
|
4864
5893
|
await rm2(statePath, { force: true });
|
|
4865
5894
|
}
|
|
4866
5895
|
return {
|
|
@@ -4870,69 +5899,104 @@ var upgrade = async (options) => {
|
|
|
4870
5899
|
requested_version: previous.version,
|
|
4871
5900
|
package_manager: previous.packageManager,
|
|
4872
5901
|
services_restarted: reportedRestartServices(resumed),
|
|
4873
|
-
services_pending_restart:
|
|
5902
|
+
services_pending_restart: resumed.pendingServices.filter((id) => id !== resumed.restartingService),
|
|
4874
5903
|
resumed_after_restart: true,
|
|
4875
5904
|
dry_run: false
|
|
4876
5905
|
};
|
|
4877
5906
|
}
|
|
4878
|
-
const entrypoint = options.entrypoint ?? process.argv[1];
|
|
4879
|
-
if (entrypoint === undefined) {
|
|
4880
|
-
throw new Error("Could not resolve the Cohall executable path");
|
|
4881
|
-
}
|
|
4882
|
-
const canonicalEntrypoint = await realpath2(entrypoint);
|
|
4883
|
-
const installation = packageInstallation(canonicalEntrypoint, entrypoint);
|
|
4884
5907
|
const services = await activeServices(runner, candidates);
|
|
4885
|
-
await assertServiceInstallations(runner, services,
|
|
4886
|
-
|
|
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)) {
|
|
4887
5953
|
return {
|
|
4888
5954
|
upgraded: false,
|
|
4889
5955
|
from_version: options.currentVersion,
|
|
4890
|
-
installed_version: options.currentVersion,
|
|
5956
|
+
installed_version: nextVersion ?? options.currentVersion,
|
|
4891
5957
|
requested_version: target,
|
|
4892
5958
|
package_manager: installation.manager,
|
|
4893
5959
|
services_restarted: [],
|
|
4894
|
-
services_pending_restart:
|
|
5960
|
+
services_pending_restart: [],
|
|
4895
5961
|
resumed_after_restart: false,
|
|
4896
|
-
dry_run:
|
|
5962
|
+
dry_run: options.dryRun
|
|
4897
5963
|
};
|
|
4898
5964
|
}
|
|
4899
|
-
|
|
4900
|
-
return;
|
|
4901
|
-
});
|
|
4902
|
-
if (nextVersion === undefined || target !== nextVersion) {
|
|
4903
|
-
const install = packageInstallCommand(installation, target);
|
|
4904
|
-
const installExecutable = options.resolveExecutable === undefined ? await trustedExecutable(install.command, installation.prefix === undefined ? {} : { writableRoot: installation.prefix }) : await resolveExecutable(install.command);
|
|
4905
|
-
await checked(runner, { ...install, command: installExecutable });
|
|
4906
|
-
nextVersion = await installedVersion(entrypoint);
|
|
4907
|
-
}
|
|
4908
|
-
if (target !== "latest" && nextVersion !== target) {
|
|
4909
|
-
throw new Error(`Installed Cohall ${nextVersion}, expected ${target}`);
|
|
4910
|
-
}
|
|
4911
|
-
const upgraded = nextVersion !== options.currentVersion;
|
|
4912
|
-
if (!options.restart || services.length === 0) {
|
|
4913
|
-
await rm2(statePath, { force: true });
|
|
5965
|
+
if (options.dryRun) {
|
|
4914
5966
|
return {
|
|
4915
|
-
upgraded,
|
|
5967
|
+
upgraded: false,
|
|
4916
5968
|
from_version: options.currentVersion,
|
|
4917
|
-
installed_version:
|
|
5969
|
+
installed_version: options.currentVersion,
|
|
4918
5970
|
requested_version: target,
|
|
4919
5971
|
package_manager: installation.manager,
|
|
4920
5972
|
services_restarted: [],
|
|
4921
|
-
services_pending_restart:
|
|
5973
|
+
services_pending_restart: pendingServices,
|
|
4922
5974
|
resumed_after_restart: false,
|
|
4923
|
-
dry_run:
|
|
5975
|
+
dry_run: true
|
|
4924
5976
|
};
|
|
4925
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;
|
|
4926
5988
|
const initial = {
|
|
4927
5989
|
version: nextVersion,
|
|
4928
5990
|
fromVersion: options.currentVersion,
|
|
4929
5991
|
packageManager: installation.manager,
|
|
4930
|
-
pendingServices
|
|
5992
|
+
pendingServices,
|
|
4931
5993
|
restartedServices: []
|
|
4932
5994
|
};
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
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) {
|
|
4936
6000
|
await rm2(statePath, { force: true });
|
|
4937
6001
|
}
|
|
4938
6002
|
return {
|
|
@@ -4942,7 +6006,7 @@ var upgrade = async (options) => {
|
|
|
4942
6006
|
requested_version: target,
|
|
4943
6007
|
package_manager: installation.manager,
|
|
4944
6008
|
services_restarted: reportedRestartServices(completed),
|
|
4945
|
-
services_pending_restart:
|
|
6009
|
+
services_pending_restart: completed.pendingServices.filter((id) => id !== completed.restartingService),
|
|
4946
6010
|
resumed_after_restart: false,
|
|
4947
6011
|
dry_run: false
|
|
4948
6012
|
};
|
|
@@ -4952,20 +6016,36 @@ var upgrade = async (options) => {
|
|
|
4952
6016
|
var xml = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
4953
6017
|
var systemdArgument = (value) => `"${value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"").replaceAll("%", "%%").replaceAll(`
|
|
4954
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
|
+
};
|
|
4955
6030
|
var deviceServicePlan = (options) => {
|
|
6031
|
+
const pnpmHome = options.pnpm?.home ?? (options.platform === "darwin" ? join5(options.home, "Library", "pnpm") : join5(options.home, ".local", "share", "pnpm"));
|
|
4956
6032
|
const servicePath = [
|
|
4957
6033
|
dirname4(options.nodeExecutable),
|
|
4958
6034
|
join5(options.home, ".local", "bin"),
|
|
4959
6035
|
join5(options.home, ".npm-global", "bin"),
|
|
4960
6036
|
join5(options.home, ".bun", "bin"),
|
|
4961
|
-
options.
|
|
6037
|
+
...options.pnpm === undefined ? [] : [options.pnpm.bin],
|
|
6038
|
+
join5(pnpmHome, "bin"),
|
|
6039
|
+
pnpmHome,
|
|
4962
6040
|
...options.platform === "darwin" ? ["/opt/homebrew/bin"] : [],
|
|
4963
6041
|
"/usr/local/bin",
|
|
4964
6042
|
"/usr/bin",
|
|
4965
6043
|
"/bin"
|
|
4966
6044
|
].join(":");
|
|
4967
6045
|
if (options.platform === "linux") {
|
|
4968
|
-
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");
|
|
4969
6049
|
return {
|
|
4970
6050
|
file: {
|
|
4971
6051
|
path,
|
|
@@ -4979,7 +6059,12 @@ Wants=network-online.target
|
|
|
4979
6059
|
Type=simple
|
|
4980
6060
|
Environment=${systemdArgument(`PATH=${servicePath}`)}
|
|
4981
6061
|
Environment=${systemdArgument(`COHALL_CONFIG=${options.configPath}`)}
|
|
4982
|
-
|
|
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
|
|
4983
6068
|
Restart=always
|
|
4984
6069
|
RestartSec=3
|
|
4985
6070
|
UMask=0077
|
|
@@ -5023,7 +6108,17 @@ WantedBy=default.target
|
|
|
5023
6108
|
<string>${xml(servicePath)}</string>
|
|
5024
6109
|
<key>COHALL_CONFIG</key>
|
|
5025
6110
|
<string>${xml(options.configPath)}</string>
|
|
5026
|
-
|
|
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>
|
|
5027
6122
|
<key>RunAtLoad</key>
|
|
5028
6123
|
<true/>
|
|
5029
6124
|
<key>KeepAlive</key>
|
|
@@ -5053,8 +6148,8 @@ WantedBy=default.target
|
|
|
5053
6148
|
var defaultRunner2 = {
|
|
5054
6149
|
run: async (command, arguments_, timeoutMs = 60000) => {
|
|
5055
6150
|
const executable = await trustedExecutable(command);
|
|
5056
|
-
return new Promise((
|
|
5057
|
-
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({
|
|
5058
6153
|
exitCode: typeof error?.code === "number" ? error.code : error === null ? 0 : 1,
|
|
5059
6154
|
stdout,
|
|
5060
6155
|
stderr,
|
|
@@ -5071,12 +6166,22 @@ var checked2 = async (runner, command, arguments_, allowFailure = false) => {
|
|
|
5071
6166
|
throw new Error(`${command} failed: ${result.stderr.trim() || result.stdout.trim() || result.error || `status ${result.exitCode}`}`);
|
|
5072
6167
|
};
|
|
5073
6168
|
var installDeviceService = async (options = {}) => {
|
|
5074
|
-
const entrypoint = await
|
|
5075
|
-
|
|
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
|
+
});
|
|
5076
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
|
+
};
|
|
5077
6182
|
const runner = options.runner ?? defaultRunner2;
|
|
5078
6183
|
if (platform4 === "win32") {
|
|
5079
|
-
const script = join5(dirname4(dirname4(
|
|
6184
|
+
const script = join5(dirname4(dirname4(canonicalEntrypoint)), "deploy", "windows", "install-device.ps1");
|
|
5080
6185
|
await checked2(runner, "powershell.exe", [
|
|
5081
6186
|
"-NoProfile",
|
|
5082
6187
|
"-ExecutionPolicy",
|
|
@@ -5088,7 +6193,19 @@ var installDeviceService = async (options = {}) => {
|
|
|
5088
6193
|
"-Entrypoint",
|
|
5089
6194
|
entrypoint,
|
|
5090
6195
|
"-ConfigurationPath",
|
|
5091
|
-
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
|
+
]
|
|
5092
6209
|
]);
|
|
5093
6210
|
return { installed: "scheduled-task:Cohall Device" };
|
|
5094
6211
|
}
|
|
@@ -5096,9 +6213,10 @@ var installDeviceService = async (options = {}) => {
|
|
|
5096
6213
|
const plan = deviceServicePlan({
|
|
5097
6214
|
platform: platform4,
|
|
5098
6215
|
entrypoint,
|
|
5099
|
-
home
|
|
6216
|
+
home,
|
|
5100
6217
|
nodeExecutable: process.execPath,
|
|
5101
6218
|
configPath: configurationPath(),
|
|
6219
|
+
...pnpm === undefined ? {} : { pnpm },
|
|
5102
6220
|
...uid === undefined ? {} : { uid }
|
|
5103
6221
|
});
|
|
5104
6222
|
await mkdir4(dirname4(plan.file.path), { recursive: true, mode: 448 });
|
|
@@ -5115,36 +6233,18 @@ var installDeviceService = async (options = {}) => {
|
|
|
5115
6233
|
var restartDeviceService = async (options = {}) => {
|
|
5116
6234
|
const platform4 = options.platform ?? process.platform;
|
|
5117
6235
|
const runner = options.runner ?? defaultRunner2;
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
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 };
|
|
5126
6243
|
}
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
const target = `gui/${options.uid ?? process.getuid?.() ?? 0}/${service}`;
|
|
5130
|
-
const check = await runner.run("launchctl", ["print", target], 1e4);
|
|
5131
|
-
if (check.exitCode !== 0) {
|
|
5132
|
-
return { running: false, restarted: false };
|
|
5133
|
-
}
|
|
5134
|
-
await checked2(runner, "launchctl", ["kickstart", "-k", target]);
|
|
5135
|
-
return { running: true, restarted: true, service };
|
|
5136
|
-
}
|
|
5137
|
-
if (platform4 === "win32") {
|
|
5138
|
-
const service = "Cohall Device";
|
|
5139
|
-
const check = await runner.run("schtasks.exe", ["/Query", "/TN", service], 1e4);
|
|
5140
|
-
if (check.exitCode !== 0) {
|
|
5141
|
-
return { running: false, restarted: false };
|
|
5142
|
-
}
|
|
5143
|
-
await checked2(runner, "schtasks.exe", ["/End", "/TN", service], true);
|
|
5144
|
-
await checked2(runner, "schtasks.exe", ["/Run", "/TN", service]);
|
|
5145
|
-
return { running: true, restarted: true, service };
|
|
6244
|
+
for (const command of service.restart) {
|
|
6245
|
+
await checked2(runner, command.command, command.arguments, command.allowFailure);
|
|
5146
6246
|
}
|
|
5147
|
-
return { running:
|
|
6247
|
+
return { running: true, restarted: true, service: service.label };
|
|
5148
6248
|
};
|
|
5149
6249
|
|
|
5150
6250
|
// apps/device/src/relay-migration.ts
|
|
@@ -5181,7 +6281,7 @@ var verifyBackupDestinationParent = async (path) => {
|
|
|
5181
6281
|
if (process.platform === "win32") {
|
|
5182
6282
|
return;
|
|
5183
6283
|
}
|
|
5184
|
-
for (let current = await
|
|
6284
|
+
for (let current = await realpath3(path);; current = dirname5(current)) {
|
|
5185
6285
|
const metadata = await stat3(current);
|
|
5186
6286
|
const writableByOthers = (metadata.mode & 18) !== 0;
|
|
5187
6287
|
const sticky = (metadata.mode & 512) !== 0;
|
|
@@ -5225,7 +6325,7 @@ var backupRelay = async (destinationInput) => {
|
|
|
5225
6325
|
if (!await exists(sourceDatabase)) {
|
|
5226
6326
|
throw new Error(`Relay database was not found at ${sourceDatabase}`);
|
|
5227
6327
|
}
|
|
5228
|
-
const destination =
|
|
6328
|
+
const destination = resolve3(destinationInput);
|
|
5229
6329
|
if (await exists(destination)) {
|
|
5230
6330
|
throw new Error(`Backup destination already exists: ${destination}`);
|
|
5231
6331
|
}
|
|
@@ -5272,7 +6372,7 @@ var backupRelay = async (destinationInput) => {
|
|
|
5272
6372
|
}
|
|
5273
6373
|
};
|
|
5274
6374
|
var restoreRelay = async (sourceInput) => {
|
|
5275
|
-
const source =
|
|
6375
|
+
const source = resolve3(sourceInput);
|
|
5276
6376
|
const sourceDatabase = join6(source, "cohall.db");
|
|
5277
6377
|
const sourceOwnerToken = join6(source, "owner-token");
|
|
5278
6378
|
const sourceManifest = join6(source, "manifest.json");
|
|
@@ -5417,9 +6517,9 @@ var switchRelay = async (options) => {
|
|
|
5417
6517
|
}
|
|
5418
6518
|
let service = { running: false, restarted: false };
|
|
5419
6519
|
let warning;
|
|
5420
|
-
if (
|
|
6520
|
+
if (options.restart && deviceTokens.length > 0) {
|
|
5421
6521
|
service = await (options.restartService ?? restartDeviceService)().catch((cause) => {
|
|
5422
|
-
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.`;
|
|
5423
6523
|
return { running: true, restarted: false };
|
|
5424
6524
|
});
|
|
5425
6525
|
}
|
|
@@ -5436,9 +6536,33 @@ var switchRelay = async (options) => {
|
|
|
5436
6536
|
init_src();
|
|
5437
6537
|
import { Effect as Effect9, Schema as Schema10 } from "effect";
|
|
5438
6538
|
import { constants as constants3 } from "node:fs";
|
|
5439
|
-
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";
|
|
5440
6548
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
5441
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
|
+
};
|
|
5442
6566
|
var readInputAttachments = async (paths2) => {
|
|
5443
6567
|
if (paths2.length > maxTaskAttachments) {
|
|
5444
6568
|
throw new Error(`A task supports at most ${maxTaskAttachments} input files`);
|
|
@@ -5461,7 +6585,7 @@ var readInputAttachments = async (paths2) => {
|
|
|
5461
6585
|
if (!metadata.isFile() || metadata.dev !== source.dev || metadata.ino !== source.ino || metadata.size < 1 || metadata.size > maxAttachmentBytes) {
|
|
5462
6586
|
throw new Error(`Attachment ${name} must be a regular file of 1 to ${maxAttachmentBytes} bytes`);
|
|
5463
6587
|
}
|
|
5464
|
-
const data = await handle
|
|
6588
|
+
const data = await readBoundedFile(handle);
|
|
5465
6589
|
if (data.length < 1 || data.length > maxAttachmentBytes) {
|
|
5466
6590
|
throw new Error(`Attachment ${name} changed size while reading`);
|
|
5467
6591
|
}
|
|
@@ -5471,31 +6595,32 @@ var readInputAttachments = async (paths2) => {
|
|
|
5471
6595
|
}
|
|
5472
6596
|
}));
|
|
5473
6597
|
};
|
|
5474
|
-
var prepareTaskFiles =
|
|
6598
|
+
var prepareTaskFiles = Effect9.fn("TaskFiles.prepare")(function* (configuration, taskId, inputNames) {
|
|
5475
6599
|
const client = exports_src.make({ baseUrl: configuration.relayUrl, token: configuration.token });
|
|
5476
|
-
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 })));
|
|
5477
6604
|
const input = join7(root, "input");
|
|
5478
6605
|
const output = join7(root, "output");
|
|
5479
|
-
try
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
output,
|
|
5488
|
-
inputNames,
|
|
5489
|
-
collectOutputs: () => collectOutputs(output),
|
|
5490
|
-
cleanup: () => rm4(root, { recursive: true, force: true })
|
|
5491
|
-
};
|
|
5492
|
-
} catch (cause) {
|
|
5493
|
-
await rm4(root, { recursive: true, force: true });
|
|
5494
|
-
throw cause;
|
|
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);
|
|
5495
6614
|
}
|
|
5496
|
-
|
|
6615
|
+
return {
|
|
6616
|
+
input,
|
|
6617
|
+
output,
|
|
6618
|
+
inputNames,
|
|
6619
|
+
collectOutputs: () => collectOutputs(output)
|
|
6620
|
+
};
|
|
6621
|
+
});
|
|
5497
6622
|
var collectOutputs = async (directory) => {
|
|
5498
|
-
const entries = await
|
|
6623
|
+
const entries = await readdir2(directory, { withFileTypes: true });
|
|
5499
6624
|
if (entries.length > maxTaskAttachments) {
|
|
5500
6625
|
throw new Error(`A task can return at most ${maxTaskAttachments} files`);
|
|
5501
6626
|
}
|
|
@@ -5518,7 +6643,7 @@ var collectOutputs = async (directory) => {
|
|
|
5518
6643
|
if (!metadata.isFile() || !pathMetadata.isFile() || metadata.dev !== pathMetadata.dev || metadata.ino !== pathMetadata.ino || metadata.size < 1 || metadata.size > maxAttachmentBytes) {
|
|
5519
6644
|
throw new Error(`Output ${name} must be a regular file of 1 to ${maxAttachmentBytes} bytes`);
|
|
5520
6645
|
}
|
|
5521
|
-
const data = await handle
|
|
6646
|
+
const data = await readBoundedFile(handle);
|
|
5522
6647
|
if (data.length < 1 || data.length > maxAttachmentBytes) {
|
|
5523
6648
|
throw new Error(`Output ${name} changed size while reading`);
|
|
5524
6649
|
}
|
|
@@ -5568,6 +6693,7 @@ var valueOptions = new Set([
|
|
|
5568
6693
|
"attach",
|
|
5569
6694
|
"context",
|
|
5570
6695
|
"context-file",
|
|
6696
|
+
"deadline",
|
|
5571
6697
|
"direction",
|
|
5572
6698
|
"grok-gateway",
|
|
5573
6699
|
"error",
|
|
@@ -5580,9 +6706,12 @@ var valueOptions = new Set([
|
|
|
5580
6706
|
"parent",
|
|
5581
6707
|
"prompt",
|
|
5582
6708
|
"prompt-file",
|
|
6709
|
+
"question",
|
|
6710
|
+
"request-id",
|
|
5583
6711
|
"provider",
|
|
5584
6712
|
"providers",
|
|
5585
6713
|
"relay",
|
|
6714
|
+
"run-id",
|
|
5586
6715
|
"sandbox",
|
|
5587
6716
|
"target",
|
|
5588
6717
|
"thread",
|
|
@@ -5618,11 +6747,14 @@ Usage:
|
|
|
5618
6747
|
[--grok-gateway path]
|
|
5619
6748
|
cohall config
|
|
5620
6749
|
cohall devices
|
|
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>
|
|
5621
6753
|
cohall bots
|
|
5622
6754
|
cohall send [@device-or-bot] [prompt] [delegate options]
|
|
5623
6755
|
cohall delegate [prompt] [--target @device-or-bot] [--provider provider]
|
|
5624
6756
|
[--context text] [--thread uuid] [--parent task-id] [--workspace path]
|
|
5625
|
-
[--attach path] [--timeout seconds] [--no-wait]
|
|
6757
|
+
[--attach path] [--request-id uuid] [--timeout seconds] [--deadline UTC-timestamp] [--no-wait]
|
|
5626
6758
|
cohall status <task-id>
|
|
5627
6759
|
cohall inbox [ack <task-id>]
|
|
5628
6760
|
cohall trace <task-id> [--follow]
|
|
@@ -5630,7 +6762,7 @@ Usage:
|
|
|
5630
6762
|
cohall attachments <task-id>
|
|
5631
6763
|
cohall download <task-id> <name> --output path [--direction input|output]
|
|
5632
6764
|
cohall cancel <task-id>
|
|
5633
|
-
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
|
|
5634
6766
|
cohall thread <thread-id>
|
|
5635
6767
|
cohall pair [--label name] [--client-only]
|
|
5636
6768
|
cohall sessions
|
|
@@ -5825,17 +6957,21 @@ var printSkill = () => console.log(SKILL_default.trimEnd());
|
|
|
5825
6957
|
var runCli = async (command, raw) => {
|
|
5826
6958
|
const arguments_ = parseArguments(raw);
|
|
5827
6959
|
if (command === "reply") {
|
|
5828
|
-
allowOptions(arguments_, ["message", "message-file", "error"]);
|
|
6960
|
+
allowOptions(arguments_, ["message", "message-file", "error", "question", "run-id"]);
|
|
5829
6961
|
const id = Schema11.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
|
|
5830
6962
|
const error = option(arguments_, "error");
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
|
|
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);
|
|
5839
6975
|
print({ submitted: true, task_id: id });
|
|
5840
6976
|
return;
|
|
5841
6977
|
}
|
|
@@ -6086,7 +7222,7 @@ var runCli = async (command, raw) => {
|
|
|
6086
7222
|
const sandbox = option(arguments_, "sandbox");
|
|
6087
7223
|
const model = option(arguments_, "model") ?? existing?.model;
|
|
6088
7224
|
const gatewayInput = option(arguments_, "grok-gateway") ?? existing?.grokGateway;
|
|
6089
|
-
const grokGateway = gatewayInput === undefined ? undefined :
|
|
7225
|
+
const grokGateway = gatewayInput === undefined ? undefined : resolve4(gatewayInput);
|
|
6090
7226
|
const providerInput = option(arguments_, "providers");
|
|
6091
7227
|
const providers = providerInput === undefined ? existing?.providers : providerInput === "auto" ? undefined : parseProviders(providerInput);
|
|
6092
7228
|
const configuration2 = StoredConfiguration.make({
|
|
@@ -6168,20 +7304,31 @@ var runCli = async (command, raw) => {
|
|
|
6168
7304
|
const mcp = clientToken === undefined ? { status: "not configured" } : await checkMcp(process.argv[1] ?? "", relayUrl, clientToken);
|
|
6169
7305
|
const hasDeviceCredential = process.env.COHALL_DEVICE_TOKEN !== undefined || storedCredentials.deviceToken !== undefined;
|
|
6170
7306
|
const deviceId = process.env.COHALL_DEVICE_ID ?? configuration2?.deviceId;
|
|
6171
|
-
const
|
|
6172
|
-
|
|
6173
|
-
|
|
7307
|
+
const clientAuthentication = clientToken === undefined ? { status: "not configured" } : response?.ok !== true ? { status: "not checked" } : await Effect10.runPromise(exports_src.make({ baseUrl: relayUrl, token: clientToken }).devices().pipe(Effect10.match({
|
|
7308
|
+
onSuccess: (devices) => ({ status: "ok", devices }),
|
|
7309
|
+
onFailure: (error) => {
|
|
7310
|
+
const httpStatus = error instanceof RelayRequestError ? error.status : undefined;
|
|
7311
|
+
const reason = httpStatus === 401 || httpStatus === 403 ? "Client credential was rejected by the relay" : "Client relay check failed";
|
|
7312
|
+
return {
|
|
7313
|
+
status: "error",
|
|
7314
|
+
...httpStatus === undefined ? {} : { http_status: httpStatus },
|
|
7315
|
+
error: `${reason}: ${error.message}`
|
|
7316
|
+
};
|
|
7317
|
+
}
|
|
7318
|
+
})));
|
|
7319
|
+
const currentDevice = clientAuthentication.status === "ok" ? clientAuthentication.devices.find((device) => device.id === deviceId) : undefined;
|
|
6174
7320
|
const versionWarning = deviceVersionWarning(version, currentDevice?.version);
|
|
6175
7321
|
const warnings = [
|
|
6176
7322
|
...response?.ok === true ? [] : ["Relay is unreachable"],
|
|
6177
7323
|
...versionWarning === undefined ? [] : [versionWarning],
|
|
6178
7324
|
...mcp.status === "error" ? [`MCP check failed: ${mcp.error}`] : [],
|
|
6179
|
-
...
|
|
7325
|
+
...clientAuthentication.status === "error" ? [clientAuthentication.error] : [],
|
|
7326
|
+
...hasDeviceCredential && response?.ok === true && clientAuthentication.status !== "error" && currentDevice?.status !== "online" && currentDevice?.status !== "busy" ? [
|
|
6180
7327
|
clientToken === undefined ? "Device status cannot be checked without a client credential" : "Device daemon is not connected or is not registered with the relay"
|
|
6181
7328
|
] : [],
|
|
6182
|
-
...selectedProviders
|
|
7329
|
+
...(selectedProviders ?? (grokGateway === undefined ? [] : ["grok-bot"])).flatMap((provider) => provider === "grok-bot" ? grokBots === undefined ? [
|
|
6183
7330
|
"Configured Grok Bot gateway is unavailable; check its discovery file and host process"
|
|
6184
|
-
] : [] : 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`] : [])
|
|
6185
7332
|
];
|
|
6186
7333
|
print({
|
|
6187
7334
|
version,
|
|
@@ -6189,6 +7336,7 @@ var runCli = async (command, raw) => {
|
|
|
6189
7336
|
relay_url: relayUrl,
|
|
6190
7337
|
config_path: configurationPath(),
|
|
6191
7338
|
client_credential: process.env.COHALL_CLIENT_TOKEN !== undefined || storedCredentials.clientToken !== undefined,
|
|
7339
|
+
client_authentication: clientAuthentication.status === "ok" ? { status: "ok" } : clientAuthentication,
|
|
6192
7340
|
mcp,
|
|
6193
7341
|
device_credential: hasDeviceCredential,
|
|
6194
7342
|
workspaces: configuration2?.workspaces ?? [],
|
|
@@ -6256,6 +7404,43 @@ var runCli = async (command, raw) => {
|
|
|
6256
7404
|
return;
|
|
6257
7405
|
}
|
|
6258
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
|
+
}
|
|
7432
|
+
if (command === "progress") {
|
|
7433
|
+
allowOptions(arguments_, ["message"]);
|
|
7434
|
+
const rawId = arguments_.positionals.length === 0 ? configuration.mcpTaskId : identifier(arguments_, "task id");
|
|
7435
|
+
if (rawId === undefined)
|
|
7436
|
+
throw new Error("Task id is required outside delegated work");
|
|
7437
|
+
const note = option(arguments_, "message");
|
|
7438
|
+
if (note === undefined)
|
|
7439
|
+
throw new Error("--message is required");
|
|
7440
|
+
const input = Schema11.decodeUnknownSync(TaskProgressInput)({ note });
|
|
7441
|
+
print(await Effect10.runPromise(relay.reportTaskProgress(Schema11.decodeUnknownSync(TaskId)(rawId), input)));
|
|
7442
|
+
return;
|
|
7443
|
+
}
|
|
6259
7444
|
if (command === "devices") {
|
|
6260
7445
|
allowOptions(arguments_, []);
|
|
6261
7446
|
noPositionals(arguments_, command);
|
|
@@ -6272,12 +7457,14 @@ var runCli = async (command, raw) => {
|
|
|
6272
7457
|
allowOptions(arguments_, [
|
|
6273
7458
|
"context",
|
|
6274
7459
|
"context-file",
|
|
7460
|
+
"deadline",
|
|
6275
7461
|
"attach",
|
|
6276
7462
|
"no-wait",
|
|
6277
7463
|
"parent",
|
|
6278
7464
|
"prompt",
|
|
6279
7465
|
"prompt-file",
|
|
6280
7466
|
"provider",
|
|
7467
|
+
"request-id",
|
|
6281
7468
|
"target",
|
|
6282
7469
|
"thread",
|
|
6283
7470
|
"timeout",
|
|
@@ -6307,15 +7494,19 @@ var runCli = async (command, raw) => {
|
|
|
6307
7494
|
const workspace = option(arguments_, "workspace");
|
|
6308
7495
|
const provider = option(arguments_, "provider");
|
|
6309
7496
|
const parent = option(arguments_, "parent");
|
|
7497
|
+
const deadline = option(arguments_, "deadline");
|
|
7498
|
+
const requestId = option(arguments_, "request-id");
|
|
6310
7499
|
const task = await Effect10.runPromise(createDelegation(relay, configuration, {
|
|
6311
7500
|
prompt,
|
|
7501
|
+
...requestId === undefined ? {} : { requestId: Schema11.decodeUnknownSync(TaskRequestId)(requestId) },
|
|
6312
7502
|
...target === undefined ? {} : { target },
|
|
6313
7503
|
...context === undefined ? {} : { context },
|
|
6314
7504
|
...thread === undefined ? {} : { threadId: Schema11.decodeUnknownSync(ThreadId)(thread) },
|
|
6315
7505
|
...parent === undefined ? {} : { parentTaskId: Schema11.decodeUnknownSync(TaskId)(parent) },
|
|
6316
7506
|
...workspace === undefined ? {} : { workspace },
|
|
6317
7507
|
...provider === undefined ? {} : { provider: Schema11.decodeUnknownSync(Provider)(provider) },
|
|
6318
|
-
...attachments2.length === 0 ? {} : { attachments: attachments2 }
|
|
7508
|
+
...attachments2.length === 0 ? {} : { attachments: attachments2 },
|
|
7509
|
+
...deadline === undefined ? {} : { expiresAt: Schema11.decodeUnknownSync(TaskDeadline)(deadline) }
|
|
6319
7510
|
}));
|
|
6320
7511
|
const completed = arguments_.options.has("no-wait") ? task : await Effect10.runPromise(waitForTask(relay, task, waitSeconds));
|
|
6321
7512
|
print(arguments_.options.has("no-wait") ? taskResult(completed) : await acknowledgedTaskResult(relay, completed));
|
|
@@ -6361,7 +7552,7 @@ var runCli = async (command, raw) => {
|
|
|
6361
7552
|
const direction = directionInput === undefined ? undefined : Schema11.decodeUnknownSync(AttachmentDirection)(directionInput);
|
|
6362
7553
|
const data = await Effect10.runPromise(relay.readAttachment(id, name, direction));
|
|
6363
7554
|
await writeFile7(destination, data, { flag: "wx", mode: 384 });
|
|
6364
|
-
print({ task_id: id, name, path:
|
|
7555
|
+
print({ task_id: id, name, path: resolve4(destination), bytes: data.length });
|
|
6365
7556
|
return;
|
|
6366
7557
|
}
|
|
6367
7558
|
if (command === "trace") {
|
|
@@ -6377,8 +7568,7 @@ var runCli = async (command, raw) => {
|
|
|
6377
7568
|
if (command === "wait") {
|
|
6378
7569
|
allowOptions(arguments_, ["timeout"]);
|
|
6379
7570
|
const id = Schema11.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
|
|
6380
|
-
|
|
6381
|
-
print(taskResult(await Effect10.runPromise(waitForTask(relay, task, timeout(arguments_)))));
|
|
7571
|
+
print(taskResult(await Effect10.runPromise(waitForTask(relay, id, timeout(arguments_)))));
|
|
6382
7572
|
return;
|
|
6383
7573
|
}
|
|
6384
7574
|
if (command === "cancel") {
|
|
@@ -6400,9 +7590,9 @@ var runCli = async (command, raw) => {
|
|
|
6400
7590
|
init_src();
|
|
6401
7591
|
import { Effect as Effect11, Schedule as Schedule3, Schema as Schema12 } from "effect";
|
|
6402
7592
|
import { constants as constants4 } from "node:fs";
|
|
6403
|
-
import { open as open4, realpath as
|
|
7593
|
+
import { open as open4, realpath as realpath4, stat as stat5 } from "node:fs/promises";
|
|
6404
7594
|
import { arch, hostname as hostname3, platform as platform4 } from "node:os";
|
|
6405
|
-
import { basename as basename3, isAbsolute as
|
|
7595
|
+
import { basename as basename3, isAbsolute as isAbsolute4, relative as relative2 } from "node:path";
|
|
6406
7596
|
import { WebSocket as WebSocket2 } from "ws";
|
|
6407
7597
|
var maxQueuedRelayMessages = 8;
|
|
6408
7598
|
|
|
@@ -6435,6 +7625,8 @@ var capabilities = (providers) => {
|
|
|
6435
7625
|
if (providers.some((provider) => provider !== "grok-bot")) {
|
|
6436
7626
|
values2.push({ id: "task-attachments", label: "Task file attachments" });
|
|
6437
7627
|
}
|
|
7628
|
+
values2.push({ id: "task-clarification", label: "Task clarification and resume" });
|
|
7629
|
+
values2.push({ id: "task-deadlines", label: "Task deadlines" });
|
|
6438
7630
|
if (findExecutable("google-chrome") !== undefined || findExecutable("chromium") !== undefined || platform4() === "darwin") {
|
|
6439
7631
|
values2.push({ id: "browser-session", label: "Signed-in browser" });
|
|
6440
7632
|
}
|
|
@@ -6478,10 +7670,10 @@ var allowedWorkspace = async (configuration, requested) => {
|
|
|
6478
7670
|
if (selected === undefined) {
|
|
6479
7671
|
throw new Error("No workspace is configured on this device");
|
|
6480
7672
|
}
|
|
6481
|
-
const candidate = await
|
|
7673
|
+
const candidate = await realpath4(selected);
|
|
6482
7674
|
const allowed = configuration.workspaces.some((root) => {
|
|
6483
7675
|
const child = relative2(root, candidate);
|
|
6484
|
-
return child === "" || !child.startsWith("..") && !
|
|
7676
|
+
return child === "" || !child.startsWith("..") && !isAbsolute4(child);
|
|
6485
7677
|
});
|
|
6486
7678
|
if (!allowed) {
|
|
6487
7679
|
throw new Error(`Workspace ${candidate} is outside this device's configured workspace roots`);
|
|
@@ -6507,7 +7699,7 @@ var openAllowedWorkspace = async (configuration, requested) => {
|
|
|
6507
7699
|
throw new Error(`Workspace ${path} changed before provider startup`);
|
|
6508
7700
|
}
|
|
6509
7701
|
if (operatingSystem2 !== "linux") {
|
|
6510
|
-
const currentPath = await
|
|
7702
|
+
const currentPath = await realpath4(path);
|
|
6511
7703
|
const currentPathIdentity = await stat5(currentPath);
|
|
6512
7704
|
if (currentPathIdentity.dev !== identity.dev || currentPathIdentity.ino !== identity.ino) {
|
|
6513
7705
|
throw new Error(`Workspace ${path} changed before provider startup`);
|
|
@@ -6517,7 +7709,7 @@ var openAllowedWorkspace = async (configuration, requested) => {
|
|
|
6517
7709
|
close: () => handle.close()
|
|
6518
7710
|
};
|
|
6519
7711
|
};
|
|
6520
|
-
var promptFor = (task, deviceName, files) => {
|
|
7712
|
+
var promptFor = (task, deviceName, supportsClarification, files) => {
|
|
6521
7713
|
const context = task.context === undefined ? "" : `
|
|
6522
7714
|
|
|
6523
7715
|
Relevant context supplied by the sending agent:
|
|
@@ -6527,6 +7719,10 @@ ${task.context}`;
|
|
|
6527
7719
|
"Complete the delegated task using this device's local workspace, tools, credentials, and signed-in services.",
|
|
6528
7720
|
"Never read, reveal, copy, or use Cohall configuration files or Cohall authentication tokens.",
|
|
6529
7721
|
"Return a concise, complete result with the evidence the sending agent needs.",
|
|
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
|
+
] : [],
|
|
6530
7726
|
...files === undefined ? [] : [
|
|
6531
7727
|
...files.inputNames.length === 0 ? [] : [
|
|
6532
7728
|
`Input directory: ${files.input}`,
|
|
@@ -6537,7 +7733,13 @@ ${task.context}`;
|
|
|
6537
7733
|
],
|
|
6538
7734
|
`
|
|
6539
7735
|
Task:
|
|
6540
|
-
${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
|
+
])
|
|
6541
7743
|
].join(`
|
|
6542
7744
|
`);
|
|
6543
7745
|
};
|
|
@@ -6546,6 +7748,7 @@ var send = (state, event) => {
|
|
|
6546
7748
|
state.socket.send(JSON.stringify(event));
|
|
6547
7749
|
}
|
|
6548
7750
|
};
|
|
7751
|
+
var taskRunKey = (taskId, runId) => `${taskId}:${runId ?? ""}`;
|
|
6549
7752
|
var omitOutputFiles = (event, note) => {
|
|
6550
7753
|
if (event._tag !== "TaskFinished" || (event.attachments?.length ?? 0) === 0) {
|
|
6551
7754
|
return event;
|
|
@@ -6553,16 +7756,17 @@ var omitOutputFiles = (event, note) => {
|
|
|
6553
7756
|
const message = `
|
|
6554
7757
|
|
|
6555
7758
|
[Output files omitted: ${note}]`;
|
|
6556
|
-
return SocketEvent.make({
|
|
7759
|
+
return SocketEvent.cases.TaskFinished.make({
|
|
6557
7760
|
_tag: "TaskFinished",
|
|
6558
7761
|
taskId: event.taskId,
|
|
7762
|
+
...event.runId === undefined ? {} : { runId: event.runId },
|
|
6559
7763
|
result: `${event.result.slice(0, 131072 - message.length)}${message}`,
|
|
6560
7764
|
...event.providerSessionId === undefined ? {} : { providerSessionId: event.providerSessionId }
|
|
6561
7765
|
});
|
|
6562
7766
|
};
|
|
6563
|
-
var sendTerminal = (state,
|
|
7767
|
+
var sendTerminal = (state, event) => {
|
|
6564
7768
|
const safeEvent = state.supportsAttachments ? event : omitOutputFiles(event, "the relay does not support attachments. Upgrade the relay and retry.");
|
|
6565
|
-
state.terminal.set(taskId, safeEvent);
|
|
7769
|
+
state.terminal.set(taskRunKey(event.taskId, event.runId), safeEvent);
|
|
6566
7770
|
if (state.socket?.readyState === WebSocket2.OPEN) {
|
|
6567
7771
|
state.socket.send(JSON.stringify(safeEvent));
|
|
6568
7772
|
}
|
|
@@ -6574,12 +7778,12 @@ var sendOperationTerminal = (state, operationId, event) => {
|
|
|
6574
7778
|
state.socket.send(payload);
|
|
6575
7779
|
}
|
|
6576
7780
|
};
|
|
6577
|
-
var remember = (state,
|
|
6578
|
-
state.completed.add(
|
|
7781
|
+
var remember = (state, task) => {
|
|
7782
|
+
state.completed.add(taskRunKey(task.id, task.runId));
|
|
6579
7783
|
if (state.completed.size <= 1000) {
|
|
6580
7784
|
return;
|
|
6581
7785
|
}
|
|
6582
|
-
const oldest = state.completed.
|
|
7786
|
+
const oldest = state.completed.keys().next();
|
|
6583
7787
|
if (!oldest.done) {
|
|
6584
7788
|
state.completed.delete(oldest.value);
|
|
6585
7789
|
state.terminal.delete(oldest.value);
|
|
@@ -6597,8 +7801,9 @@ var rememberOperation = (state, operationId) => {
|
|
|
6597
7801
|
}
|
|
6598
7802
|
};
|
|
6599
7803
|
var sessionKey = (task) => `${task.threadId}:${task.provider}`;
|
|
7804
|
+
var hasCompletedRun = (state, task) => state.completed.has(taskRunKey(task.id, task.runId));
|
|
6600
7805
|
var drain = (configuration, state) => {
|
|
6601
|
-
if (state.operation !== undefined) {
|
|
7806
|
+
if (state.stopping || state.operation !== undefined) {
|
|
6602
7807
|
return;
|
|
6603
7808
|
}
|
|
6604
7809
|
if (state.tasks.size === 0) {
|
|
@@ -6621,16 +7826,37 @@ var drain = (configuration, state) => {
|
|
|
6621
7826
|
}
|
|
6622
7827
|
};
|
|
6623
7828
|
var execute = (configuration, state, task) => {
|
|
6624
|
-
if (state.tasks.has(task.id) || state
|
|
7829
|
+
if (state.tasks.has(task.id) || hasCompletedRun(state, task)) {
|
|
6625
7830
|
return;
|
|
6626
7831
|
}
|
|
6627
7832
|
const controller = new AbortController;
|
|
6628
|
-
|
|
6629
|
-
|
|
6630
|
-
|
|
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
|
+
}
|
|
6631
7857
|
if (task.provider === "grok-bot") {
|
|
6632
7858
|
return yield* Effect11.tryPromise({
|
|
6633
|
-
try: (signal) => runGrokBot(configuration.grokGateway, task, signal),
|
|
7859
|
+
try: (signal) => runGrokBot(configuration.grokGateway, task, signal, state.supportsClarification),
|
|
6634
7860
|
catch: (cause) => new ProviderRunError({
|
|
6635
7861
|
provider: task.provider,
|
|
6636
7862
|
message: cause instanceof Error ? cause.message : String(cause)
|
|
@@ -6644,68 +7870,84 @@ var execute = (configuration, state, task) => {
|
|
|
6644
7870
|
message: "The relay does not support task file attachments"
|
|
6645
7871
|
});
|
|
6646
7872
|
}
|
|
6647
|
-
const workspace = yield* Effect11.tryPromise({
|
|
7873
|
+
const workspace = yield* Effect11.acquireRelease(Effect11.tryPromise({
|
|
6648
7874
|
try: () => openAllowedWorkspace(configuration, task.workspace),
|
|
6649
7875
|
catch: (cause) => new ProviderRunError({
|
|
6650
7876
|
provider,
|
|
6651
7877
|
message: cause instanceof Error ? cause.message : String(cause)
|
|
6652
7878
|
})
|
|
6653
|
-
})
|
|
6654
|
-
return yield* Effect11.gen(function* () {
|
|
6655
|
-
const files = state.supportsAttachments ? yield* Effect11.tryPromise({
|
|
6656
|
-
try: (signal) => prepareTaskFiles(configuration, task.id, task.inputAttachmentNames ?? [], signal),
|
|
6657
|
-
catch: (cause) => new ProviderRunError({
|
|
6658
|
-
provider,
|
|
6659
|
-
message: cause instanceof Error ? cause.message : String(cause)
|
|
6660
|
-
})
|
|
6661
|
-
}) : undefined;
|
|
6662
|
-
const sessionId = task.providerSessionId ?? state.sessions.get(sessionKey(task));
|
|
6663
|
-
return yield* run({
|
|
6664
|
-
provider,
|
|
6665
|
-
threadId: task.threadId,
|
|
6666
|
-
taskId: task.id,
|
|
6667
|
-
prompt: promptFor(task, configuration.name, files),
|
|
6668
|
-
cwd: workspace.cwd,
|
|
6669
|
-
beforeSpawn: workspace.validate,
|
|
6670
|
-
...sessionId === undefined ? {} : { sessionId },
|
|
6671
|
-
...configuration.model === undefined ? {} : { model: configuration.model },
|
|
6672
|
-
...configuration.sandbox === undefined ? {} : { sandbox: configuration.sandbox }
|
|
6673
|
-
}).pipe(Effect11.flatMap((result) => files === undefined ? Effect11.succeed(result) : Effect11.tryPromise({
|
|
6674
|
-
try: async () => ({ ...result, attachments: await files.collectOutputs() }),
|
|
6675
|
-
catch: (cause) => new ProviderRunError({
|
|
6676
|
-
provider,
|
|
6677
|
-
message: cause instanceof Error ? cause.message : String(cause)
|
|
6678
|
-
})
|
|
6679
|
-
})), Effect11.ensuring(files === undefined ? Effect11.void : Effect11.promise(() => files.cleanup())));
|
|
6680
|
-
}).pipe(Effect11.ensuring(Effect11.promise(() => workspace.close().catch(() => {
|
|
7879
|
+
}), (workspace2) => Effect11.promise(() => workspace2.close().catch(() => {
|
|
6681
7880
|
return;
|
|
6682
|
-
})))
|
|
6683
|
-
|
|
6684
|
-
|
|
6685
|
-
|
|
6686
|
-
|
|
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,
|
|
6687
7890
|
taskId: task.id,
|
|
6688
|
-
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
|
-
|
|
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
|
+
}
|
|
6692
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;
|
|
6693
7928
|
if (result.sessionId !== undefined) {
|
|
6694
7929
|
state.sessions.set(sessionKey(task), result.sessionId);
|
|
6695
7930
|
}
|
|
6696
|
-
remember(state, task
|
|
6697
|
-
sendTerminal(state,
|
|
7931
|
+
remember(state, task);
|
|
7932
|
+
sendTerminal(state, bounded2);
|
|
6698
7933
|
}).catch((cause) => {
|
|
6699
|
-
remember(state, task
|
|
6700
|
-
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({
|
|
6701
7940
|
_tag: "TaskFailed",
|
|
6702
7941
|
taskId: task.id,
|
|
7942
|
+
...task.runId === undefined ? {} : { runId: task.runId },
|
|
6703
7943
|
error: (cause instanceof Error ? cause.message : String(cause)).slice(0, 16384)
|
|
6704
7944
|
}));
|
|
6705
7945
|
}).finally(() => {
|
|
7946
|
+
clearTimeout(deadlineTimer);
|
|
6706
7947
|
state.tasks.delete(task.id);
|
|
6707
7948
|
drain(configuration, state);
|
|
6708
7949
|
});
|
|
7950
|
+
state.tasks.set(task.id, { task, controller, done });
|
|
6709
7951
|
};
|
|
6710
7952
|
var executeOperation = (configuration, state, operation) => {
|
|
6711
7953
|
if (state.operation !== undefined || state.completedOperations.has(operation.id)) {
|
|
@@ -6733,6 +7975,8 @@ var executeOperation = (configuration, state, operation) => {
|
|
|
6733
7975
|
});
|
|
6734
7976
|
};
|
|
6735
7977
|
var scheduleOperation = (configuration, state, operation) => {
|
|
7978
|
+
if (state.stopping)
|
|
7979
|
+
return;
|
|
6736
7980
|
if (state.completedOperations.has(operation.id) || state.operation?.id === operation.id || state.operationQueue.some((pending) => pending.id === operation.id)) {
|
|
6737
7981
|
return;
|
|
6738
7982
|
}
|
|
@@ -6754,7 +7998,18 @@ var performDeviceOperation = (operation, currentVersion, runUpgrade = upgrade) =
|
|
|
6754
7998
|
delegated: true
|
|
6755
7999
|
}).then((result) => JSON.stringify(result));
|
|
6756
8000
|
var schedule = (configuration, state, task) => {
|
|
6757
|
-
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
|
+
}));
|
|
6758
8013
|
return;
|
|
6759
8014
|
}
|
|
6760
8015
|
if (task.providerSessionId !== undefined) {
|
|
@@ -6770,20 +8025,24 @@ var schedule = (configuration, state, task) => {
|
|
|
6770
8025
|
}
|
|
6771
8026
|
execute(configuration, state, task);
|
|
6772
8027
|
};
|
|
6773
|
-
var cancel = (state, taskId) => {
|
|
8028
|
+
var cancel = (state, taskId, runId) => {
|
|
6774
8029
|
const running = state.tasks.get(taskId);
|
|
6775
|
-
if (running !== undefined) {
|
|
8030
|
+
if (running !== undefined && (runId === undefined || running.task.runId === runId)) {
|
|
6776
8031
|
if (running.task.provider !== "grok-bot") {
|
|
6777
8032
|
running.controller.abort();
|
|
6778
8033
|
}
|
|
6779
8034
|
return;
|
|
6780
8035
|
}
|
|
6781
|
-
const index = state.queue.findIndex((task) => task.id === taskId);
|
|
8036
|
+
const index = state.queue.findIndex((task) => task.id === taskId && (runId === undefined || task.runId === runId));
|
|
6782
8037
|
if (index !== -1) {
|
|
6783
8038
|
state.queue.splice(index, 1);
|
|
6784
8039
|
}
|
|
6785
|
-
remember(state, taskId);
|
|
6786
|
-
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
|
+
}));
|
|
6787
8046
|
};
|
|
6788
8047
|
var connect = (configuration, state) => Effect11.tryPromise({
|
|
6789
8048
|
try: (signal) => new Promise((complete) => {
|
|
@@ -6792,6 +8051,15 @@ var connect = (configuration, state) => Effect11.tryPromise({
|
|
|
6792
8051
|
perMessageDeflate: false,
|
|
6793
8052
|
allowSynchronousEvents: false
|
|
6794
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);
|
|
6795
8063
|
let bots = [];
|
|
6796
8064
|
let refreshing = false;
|
|
6797
8065
|
const refreshBots = async () => {
|
|
@@ -6819,13 +8087,13 @@ var connect = (configuration, state) => Effect11.tryPromise({
|
|
|
6819
8087
|
}));
|
|
6820
8088
|
refreshBots();
|
|
6821
8089
|
}, 15000);
|
|
6822
|
-
let closed = false;
|
|
6823
8090
|
let queuedMessages = 0;
|
|
6824
8091
|
const close = () => {
|
|
6825
8092
|
if (closed) {
|
|
6826
8093
|
return;
|
|
6827
8094
|
}
|
|
6828
8095
|
closed = true;
|
|
8096
|
+
clearTimeout(connectionDeadline);
|
|
6829
8097
|
clearInterval(heartbeat);
|
|
6830
8098
|
if (state.socket === socket) {
|
|
6831
8099
|
state.socket = undefined;
|
|
@@ -6833,7 +8101,7 @@ var connect = (configuration, state) => Effect11.tryPromise({
|
|
|
6833
8101
|
complete();
|
|
6834
8102
|
};
|
|
6835
8103
|
signal.addEventListener("abort", () => {
|
|
6836
|
-
socket.
|
|
8104
|
+
socket.terminate();
|
|
6837
8105
|
close();
|
|
6838
8106
|
}, { once: true });
|
|
6839
8107
|
socket.once("open", () => {
|
|
@@ -6856,14 +8124,16 @@ var connect = (configuration, state) => Effect11.tryPromise({
|
|
|
6856
8124
|
}).pipe(Effect11.flatMap(decodeSocketEvent)));
|
|
6857
8125
|
if (event._tag === "Connected") {
|
|
6858
8126
|
state.supportsAttachments = event.taskAttachments === true;
|
|
8127
|
+
state.supportsClarification = event.taskClarification === true;
|
|
6859
8128
|
state.socket = socket;
|
|
8129
|
+
resetConnectionDeadline();
|
|
6860
8130
|
socket.send(JSON.stringify(SocketEvent.make({
|
|
6861
8131
|
_tag: "DeviceHello",
|
|
6862
8132
|
device: describeDevice(configuration, state.tasks.size > 0 || state.operation !== undefined ? "busy" : "online", bots)
|
|
6863
8133
|
})));
|
|
6864
|
-
for (const [
|
|
8134
|
+
for (const [key, pending] of state.terminal) {
|
|
6865
8135
|
const safeEvent = state.supportsAttachments ? pending : omitOutputFiles(pending, "the relay does not support attachments. Upgrade the relay and retry.");
|
|
6866
|
-
state.terminal.set(
|
|
8136
|
+
state.terminal.set(key, safeEvent);
|
|
6867
8137
|
socket.send(JSON.stringify(safeEvent));
|
|
6868
8138
|
}
|
|
6869
8139
|
for (const payload of state.operationTerminal.values()) {
|
|
@@ -6877,12 +8147,12 @@ var connect = (configuration, state) => Effect11.tryPromise({
|
|
|
6877
8147
|
return;
|
|
6878
8148
|
}
|
|
6879
8149
|
if (event._tag === "CancelTask") {
|
|
6880
|
-
cancel(state, event.taskId);
|
|
8150
|
+
cancel(state, event.taskId, event.runId);
|
|
6881
8151
|
return;
|
|
6882
8152
|
}
|
|
6883
8153
|
if (event._tag === "TaskSettled") {
|
|
6884
|
-
state.terminal.delete(event.taskId);
|
|
6885
|
-
cleanupBotReply(event.taskId).catch(() => {
|
|
8154
|
+
state.terminal.delete(taskRunKey(event.taskId, event.runId));
|
|
8155
|
+
cleanupBotReply(event.taskId, event.runId).catch(() => {
|
|
6886
8156
|
console.error(`Could not remove local bot reply for settled task ${event.taskId}`);
|
|
6887
8157
|
});
|
|
6888
8158
|
return;
|
|
@@ -6914,7 +8184,9 @@ var connect = (configuration, state) => Effect11.tryPromise({
|
|
|
6914
8184
|
var runDaemon = (configuration) => {
|
|
6915
8185
|
const state = {
|
|
6916
8186
|
socket: undefined,
|
|
8187
|
+
stopping: false,
|
|
6917
8188
|
supportsAttachments: false,
|
|
8189
|
+
supportsClarification: false,
|
|
6918
8190
|
processing: Promise.resolve(),
|
|
6919
8191
|
terminal: new Map,
|
|
6920
8192
|
queue: [],
|
|
@@ -6926,11 +8198,14 @@ var runDaemon = (configuration) => {
|
|
|
6926
8198
|
operationTerminal: new Map,
|
|
6927
8199
|
completedOperations: new Set
|
|
6928
8200
|
};
|
|
6929
|
-
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;
|
|
6930
8203
|
state.socket?.close();
|
|
6931
|
-
|
|
8204
|
+
const running = [...state.tasks.values()];
|
|
8205
|
+
for (const { controller } of running) {
|
|
6932
8206
|
controller.abort();
|
|
6933
8207
|
}
|
|
8208
|
+
await Promise.allSettled(running.map(({ done }) => done));
|
|
6934
8209
|
})));
|
|
6935
8210
|
};
|
|
6936
8211
|
|
|
@@ -6941,15 +8216,99 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
6941
8216
|
import { Effect as Effect12, Schema as Schema13 } from "effect";
|
|
6942
8217
|
import * as z from "zod/v4";
|
|
6943
8218
|
import { writeFile as writeFile8 } from "node:fs/promises";
|
|
6944
|
-
|
|
6945
|
-
|
|
8219
|
+
|
|
8220
|
+
// apps/device/src/mcp-build-notice.ts
|
|
8221
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
8222
|
+
import { stat as stat6 } from "node:fs/promises";
|
|
8223
|
+
import { promisify } from "node:util";
|
|
8224
|
+
var execute2 = promisify(execFile3);
|
|
8225
|
+
var fingerprint = async (entrypoint) => stat6(entrypoint, { bigint: true }).then((file) => `${file.dev}:${file.ino}:${file.size}:${file.mtimeNs}:${file.ctimeNs}`).catch(() => {
|
|
8226
|
+
return;
|
|
6946
8227
|
});
|
|
8228
|
+
var createMcpBuildNotice = async (entrypoint, runningVersion, {
|
|
8229
|
+
checkIntervalMs = 60000,
|
|
8230
|
+
probeTimeoutMs = 2000,
|
|
8231
|
+
now: now2 = Date.now
|
|
8232
|
+
} = {}) => {
|
|
8233
|
+
let previous = entrypoint === undefined ? undefined : await fingerprint(entrypoint);
|
|
8234
|
+
let nextCheckAt = 0;
|
|
8235
|
+
const reported = new Set;
|
|
8236
|
+
return async (result) => {
|
|
8237
|
+
if (entrypoint === undefined || now2() < nextCheckAt)
|
|
8238
|
+
return result;
|
|
8239
|
+
nextCheckAt = now2() + checkIntervalMs;
|
|
8240
|
+
const current = await fingerprint(entrypoint);
|
|
8241
|
+
if (current === undefined || current === previous)
|
|
8242
|
+
return result;
|
|
8243
|
+
const installedVersion2 = await execute2(process.execPath, [entrypoint, "--version"], {
|
|
8244
|
+
timeout: probeTimeoutMs,
|
|
8245
|
+
killSignal: "SIGKILL",
|
|
8246
|
+
maxBuffer: 4096,
|
|
8247
|
+
windowsHide: true
|
|
8248
|
+
}).then(({ stdout }) => stdout.trim()).catch(() => {
|
|
8249
|
+
return;
|
|
8250
|
+
});
|
|
8251
|
+
if (installedVersion2 === undefined || !/^\d+\.\d+\.\d+(?:-[\da-zA-Z.-]+)?(?:\+[\da-zA-Z.-]+)?$/.test(installedVersion2)) {
|
|
8252
|
+
return result;
|
|
8253
|
+
}
|
|
8254
|
+
previous = current;
|
|
8255
|
+
if (installedVersion2 === runningVersion || reported.has(installedVersion2))
|
|
8256
|
+
return result;
|
|
8257
|
+
reported.add(installedVersion2);
|
|
8258
|
+
return {
|
|
8259
|
+
...result,
|
|
8260
|
+
content: [
|
|
8261
|
+
...result.content,
|
|
8262
|
+
{
|
|
8263
|
+
type: "text",
|
|
8264
|
+
text: `Cohall MCP is still running ${runningVersion}; its executable is now ${installedVersion2}. Restart the Cohall MCP connection in your agent host to load it.`
|
|
8265
|
+
}
|
|
8266
|
+
]
|
|
8267
|
+
};
|
|
8268
|
+
};
|
|
8269
|
+
};
|
|
8270
|
+
|
|
8271
|
+
// apps/device/src/mcp.ts
|
|
6947
8272
|
var runMcp = async (configuration) => {
|
|
8273
|
+
const buildNotice = await createMcpBuildNotice(process.argv[1], version);
|
|
8274
|
+
const output = (value) => buildNotice({ content: [{ type: "text", text: JSON.stringify(value, null, 2) }] });
|
|
6948
8275
|
const client2 = exports_src.make({
|
|
6949
8276
|
baseUrl: configuration.relayUrl,
|
|
6950
8277
|
token: configuration.token
|
|
6951
8278
|
});
|
|
6952
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 }))))));
|
|
8298
|
+
server.registerTool("task_progress", {
|
|
8299
|
+
title: "Report task progress",
|
|
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.`,
|
|
8301
|
+
inputSchema: {
|
|
8302
|
+
task_id: z.string().uuid().optional(),
|
|
8303
|
+
note: z.string().min(1).max(maxProgressNoteBytes)
|
|
8304
|
+
}
|
|
8305
|
+
}, async ({ task_id, note }) => {
|
|
8306
|
+
const id = task_id ?? configuration.mcpTaskId;
|
|
8307
|
+
if (id === undefined)
|
|
8308
|
+
throw new Error("Task id is required outside delegated work");
|
|
8309
|
+
const input = Schema13.decodeUnknownSync(TaskProgressInput)({ note });
|
|
8310
|
+
return output(await Effect12.runPromise(client2.reportTaskProgress(Schema13.decodeUnknownSync(TaskId)(id), input)));
|
|
8311
|
+
});
|
|
6953
8312
|
server.registerTool("list_devices", {
|
|
6954
8313
|
title: "List Cohall devices",
|
|
6955
8314
|
description: "List devices, their availability, local providers, capabilities, and allowed workspaces.",
|
|
@@ -6965,6 +8324,7 @@ var runMcp = async (configuration) => {
|
|
|
6965
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.",
|
|
6966
8325
|
inputSchema: {
|
|
6967
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."),
|
|
6968
8328
|
target: z.string().optional().describe("Device name, hostname or ID; @BotName; or @device/BotName. Use list_bots targets to disambiguate."),
|
|
6969
8329
|
provider: z.enum(Provider.literals).optional().describe("Inferred as grok-bot for a bot target; otherwise defaults to codex."),
|
|
6970
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."),
|
|
@@ -6973,10 +8333,12 @@ var runMcp = async (configuration) => {
|
|
|
6973
8333
|
workspace: z.string().max(4096).optional().describe("Coding provider workspace. Omit for named Grok Bots."),
|
|
6974
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."),
|
|
6975
8335
|
wait: z.boolean().default(true),
|
|
6976
|
-
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.")
|
|
6977
8338
|
}
|
|
6978
8339
|
}, async ({
|
|
6979
8340
|
prompt,
|
|
8341
|
+
request_id,
|
|
6980
8342
|
target,
|
|
6981
8343
|
provider,
|
|
6982
8344
|
context,
|
|
@@ -6985,25 +8347,28 @@ var runMcp = async (configuration) => {
|
|
|
6985
8347
|
workspace,
|
|
6986
8348
|
attachment_paths,
|
|
6987
8349
|
wait,
|
|
6988
|
-
timeout_seconds
|
|
6989
|
-
|
|
8350
|
+
timeout_seconds,
|
|
8351
|
+
deadline
|
|
8352
|
+
}, { signal }) => {
|
|
6990
8353
|
const attachments2 = await readInputAttachments(attachment_paths ?? []);
|
|
6991
8354
|
const task = await Effect12.runPromise(createDelegation(client2, configuration, {
|
|
6992
8355
|
prompt,
|
|
8356
|
+
...request_id === undefined ? {} : { requestId: Schema13.decodeUnknownSync(TaskRequestId)(request_id) },
|
|
6993
8357
|
...provider === undefined ? {} : { provider },
|
|
6994
8358
|
...target === undefined ? {} : { target },
|
|
6995
8359
|
...context === undefined ? {} : { context },
|
|
6996
8360
|
...thread_id === undefined ? {} : { threadId: ThreadId.make(thread_id) },
|
|
6997
8361
|
...parent_task_id === undefined ? {} : { parentTaskId: TaskId.make(parent_task_id) },
|
|
6998
8362
|
...workspace === undefined ? {} : { workspace },
|
|
6999
|
-
...attachments2.length === 0 ? {} : { attachments: attachments2 }
|
|
7000
|
-
|
|
7001
|
-
|
|
7002
|
-
|
|
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));
|
|
7003
8368
|
});
|
|
7004
8369
|
server.registerTool("completion_inbox", {
|
|
7005
|
-
title: "List completed work",
|
|
7006
|
-
description: "List up to 20 unacknowledged completed tasks sent by this client. hasMore signals additional entries.
|
|
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.",
|
|
7007
8372
|
inputSchema: {}
|
|
7008
8373
|
}, async () => output(await Effect12.runPromise(client2.inbox())));
|
|
7009
8374
|
server.registerTool("acknowledge_completion", {
|
|
@@ -7039,23 +8404,24 @@ var runMcp = async (configuration) => {
|
|
|
7039
8404
|
});
|
|
7040
8405
|
server.registerTool("task_trace", {
|
|
7041
8406
|
title: "Trace a delegated task",
|
|
7042
|
-
description: "Read the
|
|
8407
|
+
description: "Read the relay and device lifecycle and latest worker progress note for troubleshooting a Cohall task.",
|
|
7043
8408
|
inputSchema: { task_id: z.string().uuid() }
|
|
7044
8409
|
}, async ({ task_id }) => output(await Effect12.runPromise(client2.traceTask(TaskId.make(task_id)))));
|
|
7045
8410
|
server.registerTool("wait_task", {
|
|
7046
8411
|
title: "Wait for delegated work",
|
|
7047
|
-
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.",
|
|
7048
8413
|
inputSchema: {
|
|
7049
8414
|
task_id: z.string().uuid(),
|
|
7050
8415
|
timeout_seconds: z.number().int().min(5).max(86400).default(900)
|
|
7051
8416
|
}
|
|
7052
|
-
}, async ({ task_id, timeout_seconds }) => {
|
|
7053
|
-
|
|
7054
|
-
|
|
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
|
+
})));
|
|
7055
8421
|
});
|
|
7056
8422
|
server.registerTool("cancel_task", {
|
|
7057
8423
|
title: "Cancel delegated work",
|
|
7058
|
-
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.",
|
|
7059
8425
|
inputSchema: { task_id: z.string().uuid() }
|
|
7060
8426
|
}, async ({ task_id }) => output(taskResult(await Effect12.runPromise(client2.cancelTask(TaskId.make(task_id))))));
|
|
7061
8427
|
server.registerTool("thread_context", {
|
|
@@ -7092,7 +8458,19 @@ var main = async () => {
|
|
|
7092
8458
|
}
|
|
7093
8459
|
if (command === "device") {
|
|
7094
8460
|
const configuration = await Effect16.runPromise(loadDeviceConfiguration);
|
|
7095
|
-
|
|
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
|
+
}
|
|
7096
8474
|
return;
|
|
7097
8475
|
}
|
|
7098
8476
|
if (command === "mcp") {
|
|
@@ -7107,4 +8485,4 @@ await main().catch((cause) => {
|
|
|
7107
8485
|
process.exitCode = 1;
|
|
7108
8486
|
});
|
|
7109
8487
|
|
|
7110
|
-
//# debugId=
|
|
8488
|
+
//# debugId=4195FFD0F10A67E564756E2164756E21
|