@akshar5/cohall 0.2.0 → 0.3.1
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 +20 -0
- package/README.md +6 -1
- package/bin/cohall.js +236 -38
- package/bin/cohall.js.map +10 -10
- package/package.json +2 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.3.1](https://github.com/AksharP5/cohall/compare/v0.3.0...v0.3.1) (2026-08-06)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* make Codex and OpenCode delegation reliable ([974e0d1](https://github.com/AksharP5/cohall/commit/974e0d127a56060ee9fec0210b138a0879dde9d8))
|
|
9
|
+
|
|
10
|
+
## [0.3.0](https://github.com/AksharP5/cohall/compare/v0.2.0...v0.3.0) (2026-08-06)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* add durable task tracing ([b7c2564](https://github.com/AksharP5/cohall/commit/b7c2564c770453a3c67821a9521aacac4f4a58e2))
|
|
16
|
+
|
|
17
|
+
## 0.2.0 (2026-08-05)
|
|
18
|
+
|
|
19
|
+
- Initial public release of the CLI, embedded agent skill, optional MCP server,
|
|
20
|
+
self-hosted relay, and cross-device delegation runtime.
|
package/README.md
CHANGED
|
@@ -117,12 +117,16 @@ npx -y @akshar5/cohall delegate --target @linux --no-wait \
|
|
|
117
117
|
--prompt 'Run the test suite and report failures.'
|
|
118
118
|
npx -y @akshar5/cohall wait <task-id> --timeout 1800
|
|
119
119
|
npx -y @akshar5/cohall cancel <task-id>
|
|
120
|
+
npx -y @akshar5/cohall trace <task-id> --follow
|
|
120
121
|
npx -y @akshar5/cohall thread <thread-id>
|
|
121
122
|
```
|
|
122
123
|
|
|
123
124
|
Follow-ups using the same `thread_id` resume the provider session on the target
|
|
124
125
|
device. Active cancellation remains `cancelling` until the target acknowledges
|
|
125
|
-
that its local process stopped.
|
|
126
|
+
that its local process stopped. `trace` reports the durable relay and device
|
|
127
|
+
lifecycle without prompts, results, credentials, or provider session IDs.
|
|
128
|
+
`--follow` emits changed snapshots as newline-delimited JSON until the task is
|
|
129
|
+
terminal.
|
|
126
130
|
|
|
127
131
|
## Optional MCP
|
|
128
132
|
|
|
@@ -132,6 +136,7 @@ server exposes:
|
|
|
132
136
|
- `list_devices`
|
|
133
137
|
- `delegate`
|
|
134
138
|
- `task_status`
|
|
139
|
+
- `task_trace`
|
|
135
140
|
- `wait_task`
|
|
136
141
|
- `cancel_task`
|
|
137
142
|
- `thread_context`
|
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 { Schema } from "effect";
|
|
20
|
-
var version = "0.
|
|
20
|
+
var version = "0.3.1", 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)), 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)), DeviceId, ThreadId, MessageId, TaskId, AuthSessionId, Timestamp, Platform, Provider, DeviceStatus, TaskStatus, MessageRole, ConnectionRole, AuthSession, CreatePairingInput, PairingCredential, ExchangePairingInput, SessionCredential, PairingResult, Capability, Workspace, Device, Thread, Message, Task, 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()), decodeCreatePairingInput, decodeExchangePairingInput, decodeCreateTaskInput, decodeDevice, decodeSocketEvent;
|
|
22
|
+
})).pipe(Schema.brand(name)), DeviceId, ThreadId, MessageId, TaskId, AuthSessionId, Timestamp, Platform, Provider, DeviceStatus, TaskStatus, TaskTraceEventKind, MessageRole, ConnectionRole, AuthSession, CreatePairingInput, PairingCredential, ExchangePairingInput, SessionCredential, PairingResult, Capability, Workspace, Device, Thread, Message, Task, 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()), decodeCreatePairingInput, decodeExchangePairingInput, decodeCreateTaskInput, decodeDevice, decodeSocketEvent;
|
|
23
23
|
var init_src = __esm(() => {
|
|
24
24
|
DeviceId = uuid("DeviceId");
|
|
25
25
|
ThreadId = uuid("ThreadId");
|
|
@@ -39,6 +39,7 @@ var init_src = __esm(() => {
|
|
|
39
39
|
"failed",
|
|
40
40
|
"cancelled"
|
|
41
41
|
]);
|
|
42
|
+
TaskTraceEventKind = Schema.Literals([...TaskStatus.literals, "requeued"]);
|
|
42
43
|
MessageRole = Schema.Literals(["human", "agent", "system"]);
|
|
43
44
|
ConnectionRole = Schema.Literals(["client", "device"]);
|
|
44
45
|
AuthSession = Schema.Struct({
|
|
@@ -128,6 +129,28 @@ var init_src = __esm(() => {
|
|
|
128
129
|
startedAt: Schema.optionalKey(Timestamp),
|
|
129
130
|
completedAt: Schema.optionalKey(Timestamp)
|
|
130
131
|
});
|
|
132
|
+
TaskTraceEvent = Schema.Struct({
|
|
133
|
+
kind: TaskTraceEventKind,
|
|
134
|
+
at: Timestamp,
|
|
135
|
+
detail: Schema.optionalKey(optionalText(512))
|
|
136
|
+
});
|
|
137
|
+
TaskTrace = Schema.Struct({
|
|
138
|
+
taskId: TaskId,
|
|
139
|
+
threadId: ThreadId,
|
|
140
|
+
status: TaskStatus,
|
|
141
|
+
provider: Provider,
|
|
142
|
+
sourceDeviceId: Schema.optionalKey(DeviceId),
|
|
143
|
+
targetDevice: Device,
|
|
144
|
+
parentTaskId: Schema.optionalKey(TaskId),
|
|
145
|
+
workspace: Schema.optionalKey(bounded(4096)),
|
|
146
|
+
createdAt: Timestamp,
|
|
147
|
+
updatedAt: Timestamp,
|
|
148
|
+
startedAt: Schema.optionalKey(Timestamp),
|
|
149
|
+
completedAt: Schema.optionalKey(Timestamp),
|
|
150
|
+
events: boundedArray(TaskTraceEvent, 100),
|
|
151
|
+
truncated: Schema.Boolean,
|
|
152
|
+
error: Schema.optionalKey(optionalText(16384))
|
|
153
|
+
});
|
|
131
154
|
ThreadContext = Schema.Struct({
|
|
132
155
|
thread: Thread,
|
|
133
156
|
messages: boundedArray(Message, 20),
|
|
@@ -436,6 +459,10 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
436
459
|
...row.error === null ? {} : { error: row.error },
|
|
437
460
|
...row.started_at === null ? {} : { startedAt: row.started_at },
|
|
438
461
|
...row.completed_at === null ? {} : { completedAt: row.completed_at }
|
|
462
|
+
}), taskTraceEventFromRow = (row) => decode("RelayStore.decodeTaskTraceEvent", TaskTraceEvent, {
|
|
463
|
+
kind: row.kind,
|
|
464
|
+
at: row.created_at,
|
|
465
|
+
...row.detail === null ? {} : { detail: row.detail }
|
|
439
466
|
}), rolesFromRow = (row) => Schema9.decodeUnknownSync(Schema9.Array(ConnectionRole))(JSON.parse(row.roles_json)), authSessionFromRow = (row) => {
|
|
440
467
|
const roles = rolesFromRow(row);
|
|
441
468
|
const role = roles[0];
|
|
@@ -480,7 +507,31 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
480
507
|
selected.unshift(item);
|
|
481
508
|
}
|
|
482
509
|
return { items: selected, truncated: selected.length < items.length };
|
|
510
|
+
}, derivedTraceEvents = (task) => {
|
|
511
|
+
const detail = "Derived from task timestamps; earlier trace events were not recorded";
|
|
512
|
+
const events = [
|
|
513
|
+
TaskTraceEvent.make({ kind: "queued", at: task.createdAt, detail })
|
|
514
|
+
];
|
|
515
|
+
if (task.startedAt !== undefined) {
|
|
516
|
+
events.push(TaskTraceEvent.make({ kind: "running", at: task.startedAt, detail }));
|
|
517
|
+
} else if (task.status === "assigned") {
|
|
518
|
+
events.push(TaskTraceEvent.make({ kind: "assigned", at: task.updatedAt, detail }));
|
|
519
|
+
}
|
|
520
|
+
if (task.status === "cancelling") {
|
|
521
|
+
events.push(TaskTraceEvent.make({ kind: "cancelling", at: task.updatedAt, detail }));
|
|
522
|
+
}
|
|
523
|
+
if (task.completedAt !== undefined && (task.status === "completed" || task.status === "failed" || task.status === "cancelled")) {
|
|
524
|
+
events.push(TaskTraceEvent.make({ kind: task.status, at: task.completedAt, detail }));
|
|
525
|
+
}
|
|
526
|
+
return events;
|
|
483
527
|
}, makeService = (db) => {
|
|
528
|
+
const recordTaskTraceEvent = (taskId, kind, detail, createdAt = now()) => {
|
|
529
|
+
db.query(`INSERT INTO task_trace_events (task_id, kind, created_at, detail)
|
|
530
|
+
VALUES (?, ?, ?, ?)`).run(taskId, kind, createdAt, detail);
|
|
531
|
+
db.query(`DELETE FROM task_trace_events WHERE task_id = ? AND id NOT IN (
|
|
532
|
+
SELECT id FROM task_trace_events WHERE task_id = ? ORDER BY id DESC LIMIT 1000
|
|
533
|
+
)`).run(taskId, taskId);
|
|
534
|
+
};
|
|
484
535
|
const queryTask = (taskId) => db.query("SELECT * FROM tasks WHERE id = ?").get(taskId);
|
|
485
536
|
const getTask = Effect9.fn("RelayStore.getTask")(function* (taskId) {
|
|
486
537
|
const row = yield* Effect9.try({
|
|
@@ -495,6 +546,46 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
495
546
|
}
|
|
496
547
|
return yield* taskFromRow(row);
|
|
497
548
|
});
|
|
549
|
+
const traceTask = Effect9.fn("RelayStore.traceTask")(function* (taskId) {
|
|
550
|
+
const task = yield* getTask(taskId);
|
|
551
|
+
const [deviceRow, eventRows] = yield* Effect9.try({
|
|
552
|
+
try: () => [
|
|
553
|
+
db.query("SELECT * FROM devices WHERE id = ?").get(task.targetDeviceId),
|
|
554
|
+
db.query(`SELECT * FROM (
|
|
555
|
+
SELECT id, kind, created_at, detail FROM task_trace_events
|
|
556
|
+
WHERE task_id = ? ORDER BY id DESC LIMIT 101
|
|
557
|
+
) ORDER BY id`).all(taskId)
|
|
558
|
+
],
|
|
559
|
+
catch: operationError("RelayStore.traceTask")
|
|
560
|
+
});
|
|
561
|
+
if (deviceRow === null) {
|
|
562
|
+
return yield* Effect9.fail(new PersistenceError({
|
|
563
|
+
operation: "RelayStore.traceTask.device",
|
|
564
|
+
message: `Unknown target device ${task.targetDeviceId}`
|
|
565
|
+
}));
|
|
566
|
+
}
|
|
567
|
+
const targetDevice = yield* deviceFromRow(deviceRow);
|
|
568
|
+
const selectedRows = eventRows.slice(-100);
|
|
569
|
+
const recordedEvents = yield* Effect9.forEach(selectedRows, taskTraceEventFromRow);
|
|
570
|
+
const events = recordedEvents.length === 0 ? derivedTraceEvents(task) : recordedEvents;
|
|
571
|
+
return TaskTrace.make({
|
|
572
|
+
taskId: task.id,
|
|
573
|
+
threadId: task.threadId,
|
|
574
|
+
status: task.status,
|
|
575
|
+
provider: task.provider,
|
|
576
|
+
targetDevice,
|
|
577
|
+
createdAt: task.createdAt,
|
|
578
|
+
updatedAt: task.updatedAt,
|
|
579
|
+
events,
|
|
580
|
+
truncated: eventRows.length > 100 || recordedEvents.length === 0 || recordedEvents[0]?.kind !== "queued",
|
|
581
|
+
...task.sourceDeviceId === undefined ? {} : { sourceDeviceId: task.sourceDeviceId },
|
|
582
|
+
...task.parentTaskId === undefined ? {} : { parentTaskId: task.parentTaskId },
|
|
583
|
+
...task.workspace === undefined ? {} : { workspace: task.workspace },
|
|
584
|
+
...task.startedAt === undefined ? {} : { startedAt: task.startedAt },
|
|
585
|
+
...task.completedAt === undefined ? {} : { completedAt: task.completedAt },
|
|
586
|
+
...task.error === undefined ? {} : { error: task.error }
|
|
587
|
+
});
|
|
588
|
+
});
|
|
498
589
|
const listDevices = Effect9.fn("RelayStore.listDevices")(function* () {
|
|
499
590
|
const rows = yield* Effect9.try({
|
|
500
591
|
try: () => db.query("SELECT * FROM devices ORDER BY name COLLATE NOCASE, id").all(),
|
|
@@ -506,8 +597,12 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
506
597
|
yield* Effect9.try({
|
|
507
598
|
try: () => db.transaction(() => {
|
|
508
599
|
const timestamp = now();
|
|
600
|
+
const interrupted = db.query("SELECT id FROM tasks WHERE status IN ('assigned', 'running')").all();
|
|
509
601
|
db.query("UPDATE devices SET status = 'offline'").run();
|
|
510
602
|
db.query("UPDATE tasks SET status = 'queued', updated_at = ? WHERE status IN ('assigned', 'running')").run(timestamp);
|
|
603
|
+
for (const task of interrupted) {
|
|
604
|
+
recordTaskTraceEvent(TaskId.make(task.id), "requeued", "Relay restarted before the task completed", timestamp);
|
|
605
|
+
}
|
|
511
606
|
db.query("DELETE FROM auth_pairings WHERE expires_at <= ? OR used_at IS NOT NULL").run(timestamp);
|
|
512
607
|
db.query("UPDATE auth_sessions SET revoked_at = ? WHERE revoked_at IS NULL AND roles_json LIKE '%,%'").run(timestamp);
|
|
513
608
|
})(),
|
|
@@ -602,6 +697,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
602
697
|
target_device_id, parent_task_id, workspace, provider_session_id,
|
|
603
698
|
created_at, updated_at
|
|
604
699
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(task.id, task.threadId, task.prompt, task.context ?? null, task.provider, task.status, task.sourceDeviceId ?? null, task.targetDeviceId, task.parentTaskId ?? null, task.workspace ?? null, task.providerSessionId ?? null, task.createdAt, task.updatedAt);
|
|
700
|
+
recordTaskTraceEvent(task.id, "queued", "Relay accepted the task", timestamp);
|
|
605
701
|
db.query(`INSERT INTO messages (
|
|
606
702
|
id, thread_id, role, kind, author_id, author_name, content, created_at,
|
|
607
703
|
task_id, device_id
|
|
@@ -658,16 +754,22 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
658
754
|
});
|
|
659
755
|
return yield* Effect9.forEach(rows, taskFromRow);
|
|
660
756
|
});
|
|
661
|
-
const transition = Effect9.fn("RelayStore.transition")(function* (taskId, expected, update) {
|
|
757
|
+
const transition = Effect9.fn("RelayStore.transition")(function* (taskId, expected, update, event) {
|
|
662
758
|
const current = yield* getTask(taskId);
|
|
663
759
|
if (!expected.includes(current.status)) {
|
|
664
760
|
return current;
|
|
665
761
|
}
|
|
666
762
|
const placeholders = expected.map(() => "?").join(", ");
|
|
763
|
+
const timestamp = now();
|
|
667
764
|
yield* Effect9.try({
|
|
668
|
-
try: () => db.
|
|
669
|
-
|
|
670
|
-
|
|
765
|
+
try: () => db.transaction(() => {
|
|
766
|
+
const result = db.query(`UPDATE tasks SET status = ?, provider_session_id = ?, result = ?, error = ?,
|
|
767
|
+
started_at = ?, completed_at = ?, updated_at = ?
|
|
768
|
+
WHERE id = ? AND status IN (${placeholders})`).run(update.status ?? current.status, update.providerSessionId ?? current.providerSessionId ?? null, update.result ?? current.result ?? null, update.error ?? current.error ?? null, update.startedAt ?? current.startedAt ?? null, update.completedAt ?? current.completedAt ?? null, timestamp, taskId, ...expected);
|
|
769
|
+
if (result.changes === 1 && event !== undefined) {
|
|
770
|
+
recordTaskTraceEvent(taskId, event.kind, event.detail, timestamp);
|
|
771
|
+
}
|
|
772
|
+
})(),
|
|
671
773
|
catch: operationError("RelayStore.transition")
|
|
672
774
|
});
|
|
673
775
|
return yield* getTask(taskId);
|
|
@@ -677,13 +779,19 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
677
779
|
if (current.status !== "queued") {
|
|
678
780
|
return current;
|
|
679
781
|
}
|
|
782
|
+
const timestamp = now();
|
|
680
783
|
yield* Effect9.try({
|
|
681
|
-
try: () => db.
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
784
|
+
try: () => db.transaction(() => {
|
|
785
|
+
const result = db.query(`UPDATE tasks SET status = 'assigned', updated_at = ?
|
|
786
|
+
WHERE id = ? AND status = 'queued' AND NOT EXISTS (
|
|
787
|
+
SELECT 1 FROM tasks active
|
|
788
|
+
WHERE active.target_device_id = ? AND active.id <> ?
|
|
789
|
+
AND active.status IN ('assigned', 'running', 'cancelling')
|
|
790
|
+
)`).run(timestamp, taskId, current.targetDeviceId, taskId);
|
|
791
|
+
if (result.changes === 1) {
|
|
792
|
+
recordTaskTraceEvent(taskId, "assigned", "Relay dispatched the task over the device WebSocket", timestamp);
|
|
793
|
+
}
|
|
794
|
+
})(),
|
|
687
795
|
catch: operationError("RelayStore.assignTask")
|
|
688
796
|
});
|
|
689
797
|
return yield* getTask(taskId);
|
|
@@ -712,6 +820,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
712
820
|
if (updated.changes !== 1) {
|
|
713
821
|
return;
|
|
714
822
|
}
|
|
823
|
+
recordTaskTraceEvent(taskId, status, status === "completed" ? "Target device completed the task" : status === "failed" ? "Target device reported a task failure" : "Target device acknowledged cancellation", timestamp);
|
|
715
824
|
if (providerSessionId !== undefined) {
|
|
716
825
|
db.query(`INSERT INTO provider_sessions (thread_id, device_id, provider, session_id, updated_at)
|
|
717
826
|
VALUES (?, ?, ?, ?, ?)
|
|
@@ -737,14 +846,26 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
737
846
|
if (["completed", "failed", "cancelled"].includes(current.status)) {
|
|
738
847
|
return current;
|
|
739
848
|
}
|
|
740
|
-
|
|
849
|
+
if (current.status === "cancelling") {
|
|
850
|
+
return current;
|
|
851
|
+
}
|
|
852
|
+
const timestamp = now();
|
|
853
|
+
return current.status === "queued" ? yield* transition(taskId, ["queued"], { status: "cancelled", completedAt: timestamp }, { kind: "cancelled", detail: "Client cancelled the task before dispatch" }) : yield* transition(taskId, ["assigned", "running"], {
|
|
741
854
|
status: "cancelling"
|
|
742
|
-
});
|
|
855
|
+
}, { kind: "cancelling", detail: "Client requested task cancellation" });
|
|
743
856
|
});
|
|
744
857
|
const requeueTasksFor = Effect9.fn("RelayStore.requeueTasksFor")(function* (deviceId) {
|
|
858
|
+
const timestamp = now();
|
|
745
859
|
yield* Effect9.try({
|
|
746
|
-
try: () => db.
|
|
747
|
-
|
|
860
|
+
try: () => db.transaction(() => {
|
|
861
|
+
const interrupted = db.query(`SELECT id FROM tasks WHERE target_device_id = ?
|
|
862
|
+
AND status IN ('assigned', 'running')`).all(deviceId);
|
|
863
|
+
db.query(`UPDATE tasks SET status = 'queued', updated_at = ?
|
|
864
|
+
WHERE target_device_id = ? AND status IN ('assigned', 'running')`).run(timestamp, deviceId);
|
|
865
|
+
for (const task of interrupted) {
|
|
866
|
+
recordTaskTraceEvent(TaskId.make(task.id), "requeued", "Target device disconnected before task completion", timestamp);
|
|
867
|
+
}
|
|
868
|
+
})(),
|
|
748
869
|
catch: operationError("RelayStore.requeueTasksFor")
|
|
749
870
|
});
|
|
750
871
|
});
|
|
@@ -867,11 +988,12 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
867
988
|
markDeviceOffline: (deviceId) => updateDeviceStatus(deviceId, "offline"),
|
|
868
989
|
createDelegation: createDelegation2,
|
|
869
990
|
getTask,
|
|
991
|
+
traceTask,
|
|
870
992
|
threadContext: threadContext2,
|
|
871
993
|
pendingTasksFor,
|
|
872
994
|
assignTask,
|
|
873
|
-
rollbackAssignment: (taskId) => transition(taskId, ["assigned"], { status: "queued" }),
|
|
874
|
-
acceptTask: (taskId, deviceId) => requireTarget(taskId, deviceId).pipe(Effect9.flatMap(() => transition(taskId, ["assigned"], { status: "running", startedAt: now() }))),
|
|
995
|
+
rollbackAssignment: (taskId) => transition(taskId, ["assigned"], { status: "queued" }, { kind: "requeued", detail: "Target device was unavailable during dispatch" }),
|
|
996
|
+
acceptTask: (taskId, deviceId) => requireTarget(taskId, deviceId).pipe(Effect9.flatMap(() => transition(taskId, ["assigned"], { status: "running", startedAt: now() }, { kind: "running", detail: "Target device accepted the task and started the provider" }))),
|
|
875
997
|
finishTask: (taskId, deviceId, result, providerSessionId) => terminal(taskId, deviceId, "completed", result, undefined, providerSessionId),
|
|
876
998
|
failTask: (taskId, deviceId, error) => terminal(taskId, deviceId, "failed", undefined, error),
|
|
877
999
|
acknowledgeCancellation: (taskId, deviceId) => terminal(taskId, deviceId, "cancelled"),
|
|
@@ -919,6 +1041,13 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
919
1041
|
);
|
|
920
1042
|
CREATE INDEX IF NOT EXISTS tasks_thread_created ON tasks(thread_id, created_at);
|
|
921
1043
|
CREATE INDEX IF NOT EXISTS tasks_target_status ON tasks(target_device_id, status);
|
|
1044
|
+
CREATE TABLE IF NOT EXISTS task_trace_events (
|
|
1045
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1046
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1047
|
+
kind TEXT NOT NULL, created_at TEXT NOT NULL, detail TEXT
|
|
1048
|
+
);
|
|
1049
|
+
CREATE INDEX IF NOT EXISTS task_trace_events_task_id
|
|
1050
|
+
ON task_trace_events(task_id, id);
|
|
922
1051
|
CREATE TABLE IF NOT EXISTS provider_sessions (
|
|
923
1052
|
thread_id TEXT NOT NULL REFERENCES threads(id) ON DELETE CASCADE,
|
|
924
1053
|
device_id TEXT NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
|
|
@@ -1296,6 +1425,10 @@ Content-Length: 0\r
|
|
|
1296
1425
|
if (request.method === "GET" && task?.[1] !== undefined) {
|
|
1297
1426
|
return json(yield* store.getTask(yield* pathId(TaskId, task[1])));
|
|
1298
1427
|
}
|
|
1428
|
+
const trace = url.pathname.match(/^\/api\/tasks\/([^/]+)\/trace$/);
|
|
1429
|
+
if (request.method === "GET" && trace?.[1] !== undefined) {
|
|
1430
|
+
return json(yield* store.traceTask(yield* pathId(TaskId, trace[1])));
|
|
1431
|
+
}
|
|
1299
1432
|
const cancel2 = url.pathname.match(/^\/api\/tasks\/([^/]+)\/cancel$/);
|
|
1300
1433
|
if (request.method === "POST" && cancel2?.[1] !== undefined) {
|
|
1301
1434
|
const id = yield* pathId(TaskId, cancel2[1]);
|
|
@@ -1556,6 +1689,7 @@ var make = (options) => {
|
|
|
1556
1689
|
body: JSON.stringify(input)
|
|
1557
1690
|
}),
|
|
1558
1691
|
getTask: (taskId) => request("RelayClient.getTask", `/api/tasks/${encodeURIComponent(taskId)}`, Task),
|
|
1692
|
+
traceTask: (taskId) => request("RelayClient.traceTask", `/api/tasks/${encodeURIComponent(taskId)}/trace`, TaskTrace),
|
|
1559
1693
|
cancelTask: (taskId) => request("RelayClient.cancelTask", `/api/tasks/${encodeURIComponent(taskId)}/cancel`, Task, {
|
|
1560
1694
|
method: "POST"
|
|
1561
1695
|
}),
|
|
@@ -1682,6 +1816,7 @@ var boundedText = (stream, limit, onOverflow) => new Promise((resolve, reject) =
|
|
|
1682
1816
|
var codexCommand = (options) => {
|
|
1683
1817
|
const common = [
|
|
1684
1818
|
"--json",
|
|
1819
|
+
"--skip-git-repo-check",
|
|
1685
1820
|
"-c",
|
|
1686
1821
|
'approval_policy="never"',
|
|
1687
1822
|
...options.model === undefined ? [] : ["--model", options.model],
|
|
@@ -1703,7 +1838,8 @@ var opencodeCommand = (options) => [
|
|
|
1703
1838
|
"--format",
|
|
1704
1839
|
"json",
|
|
1705
1840
|
...options.model === undefined ? [] : ["--model", options.model],
|
|
1706
|
-
...options.sessionId === undefined ? [] : ["--session", options.sessionId]
|
|
1841
|
+
...options.sessionId === undefined ? [] : ["--session", options.sessionId],
|
|
1842
|
+
options.prompt
|
|
1707
1843
|
];
|
|
1708
1844
|
var command = (options) => {
|
|
1709
1845
|
switch (options.provider) {
|
|
@@ -1759,6 +1895,18 @@ var textFromPart = (value) => {
|
|
|
1759
1895
|
}
|
|
1760
1896
|
return text(part.text) ?? text(part.content) ?? text(record(part.part)?.text);
|
|
1761
1897
|
};
|
|
1898
|
+
var openCodeError = (event) => {
|
|
1899
|
+
if (text(event.type) !== "error") {
|
|
1900
|
+
return;
|
|
1901
|
+
}
|
|
1902
|
+
const error = record(event.error);
|
|
1903
|
+
const name = text(error?.name);
|
|
1904
|
+
const message = text(record(error?.data)?.message) ?? text(error?.message) ?? text(event.message) ?? text(event.error);
|
|
1905
|
+
if (name !== undefined && message !== undefined) {
|
|
1906
|
+
return `OpenCode ${name}: ${message}`;
|
|
1907
|
+
}
|
|
1908
|
+
return message === undefined ? "OpenCode reported an error" : `OpenCode: ${message}`;
|
|
1909
|
+
};
|
|
1762
1910
|
var parseOpenCode = (stdout, existingSession) => {
|
|
1763
1911
|
let result = "";
|
|
1764
1912
|
let sessionId = existingSession;
|
|
@@ -1771,6 +1919,10 @@ var parseOpenCode = (stdout, existingSession) => {
|
|
|
1771
1919
|
if (event === undefined) {
|
|
1772
1920
|
continue;
|
|
1773
1921
|
}
|
|
1922
|
+
const failure = openCodeError(event);
|
|
1923
|
+
if (failure !== undefined) {
|
|
1924
|
+
throw new Error(failure);
|
|
1925
|
+
}
|
|
1774
1926
|
sessionId = text(event.sessionID) ?? text(event.sessionId) ?? text(event.session_id) ?? text(record(event.info)?.sessionID) ?? sessionId;
|
|
1775
1927
|
const content = textFromPart(event.part) ?? textFromPart(event) ?? text(event.result);
|
|
1776
1928
|
if (content !== undefined && content.length > 0) {
|
|
@@ -1778,7 +1930,7 @@ var parseOpenCode = (stdout, existingSession) => {
|
|
|
1778
1930
|
}
|
|
1779
1931
|
}
|
|
1780
1932
|
if (result.length === 0) {
|
|
1781
|
-
throw new Error("OpenCode
|
|
1933
|
+
throw new Error("OpenCode produced no result; verify its authentication and use a supported project workspace");
|
|
1782
1934
|
}
|
|
1783
1935
|
return { result, ...sessionId === undefined ? {} : { sessionId } };
|
|
1784
1936
|
};
|
|
@@ -1835,7 +1987,7 @@ var run = (options) => Effect2.tryPromise({
|
|
|
1835
1987
|
killTimer.unref();
|
|
1836
1988
|
};
|
|
1837
1989
|
signal.addEventListener("abort", terminate, { once: true });
|
|
1838
|
-
child.stdin.end(options.prompt);
|
|
1990
|
+
child.stdin.end(options.provider === "opencode" ? undefined : options.prompt);
|
|
1839
1991
|
const [stdout, stderr, exitCode] = await Promise.all([
|
|
1840
1992
|
boundedText(child.stdout, 1024 * 1024, terminate),
|
|
1841
1993
|
boundedText(child.stderr, 64 * 1024, terminate),
|
|
@@ -1897,12 +2049,15 @@ keeps its local files, credentials, browser state, provider login, skills, and
|
|
|
1897
2049
|
permissions. The relay carries prompts, final results, and task state; it is not
|
|
1898
2050
|
a lead agent.
|
|
1899
2051
|
|
|
2052
|
+
Use the installed \`cohall\` executable when it is available. Fall back to
|
|
2053
|
+
\`npx -y @akshar5/cohall\` only when Cohall is not installed globally.
|
|
2054
|
+
|
|
1900
2055
|
## Delegate work
|
|
1901
2056
|
|
|
1902
2057
|
1. List devices unless the target is already explicit:
|
|
1903
2058
|
|
|
1904
2059
|
\`\`\`bash
|
|
1905
|
-
|
|
2060
|
+
cohall devices
|
|
1906
2061
|
\`\`\`
|
|
1907
2062
|
|
|
1908
2063
|
2. Choose a target whose provider, workspaces, platform, and capabilities fit
|
|
@@ -1911,7 +2066,7 @@ a lead agent.
|
|
|
1911
2066
|
3. Delegate one concrete outcome. Send only the context the target needs:
|
|
1912
2067
|
|
|
1913
2068
|
\`\`\`bash
|
|
1914
|
-
|
|
2069
|
+
cohall delegate \\
|
|
1915
2070
|
--target @macbook \\
|
|
1916
2071
|
--provider codex \\
|
|
1917
2072
|
--workspace /Users/me/dev/project \\
|
|
@@ -1930,7 +2085,7 @@ a lead agent.
|
|
|
1930
2085
|
its local session:
|
|
1931
2086
|
|
|
1932
2087
|
\`\`\`bash
|
|
1933
|
-
|
|
2088
|
+
cohall delegate \\
|
|
1934
2089
|
--thread 11111111-1111-4111-8111-111111111111 \\
|
|
1935
2090
|
--target @macbook \\
|
|
1936
2091
|
--prompt 'Check whether yesterday’s deployment failed for the same reason.'
|
|
@@ -1955,17 +2110,17 @@ ordinary shell or a separate agent turn.
|
|
|
1955
2110
|
Queue work when the current agent can make independent progress:
|
|
1956
2111
|
|
|
1957
2112
|
\`\`\`bash
|
|
1958
|
-
|
|
2113
|
+
cohall delegate --target @linux --no-wait \\
|
|
1959
2114
|
--prompt 'Run the project test suite and report failures.'
|
|
1960
|
-
|
|
1961
|
-
|
|
2115
|
+
cohall status <task-id>
|
|
2116
|
+
cohall wait <task-id> --timeout 1800
|
|
1962
2117
|
\`\`\`
|
|
1963
2118
|
|
|
1964
2119
|
The timeout error includes the task ID and last known status. The task continues
|
|
1965
2120
|
unless cancelled:
|
|
1966
2121
|
|
|
1967
2122
|
\`\`\`bash
|
|
1968
|
-
|
|
2123
|
+
cohall cancel <task-id>
|
|
1969
2124
|
\`\`\`
|
|
1970
2125
|
|
|
1971
2126
|
Active cancellation is acknowledged by the target device; \`cancelling\` means
|
|
@@ -1974,7 +2129,7 @@ the provider process has not confirmed termination yet.
|
|
|
1974
2129
|
## Read shared context
|
|
1975
2130
|
|
|
1976
2131
|
\`\`\`bash
|
|
1977
|
-
|
|
2132
|
+
cohall thread <thread-id>
|
|
1978
2133
|
\`\`\`
|
|
1979
2134
|
|
|
1980
2135
|
This returns a byte-bounded recent window of prompts, final responses, and task
|
|
@@ -1985,7 +2140,7 @@ states. Check \`truncated\`; older history remains on the relay when it is true.
|
|
|
1985
2140
|
Use \`--prompt-file\` or stdin for multiline work without shell interpolation:
|
|
1986
2141
|
|
|
1987
2142
|
\`\`\`bash
|
|
1988
|
-
|
|
2143
|
+
cohall delegate --target @macbook --prompt - <<'COHALL_PROMPT'
|
|
1989
2144
|
Analyze the authenticated pages in the attached task context.
|
|
1990
2145
|
Return the shared conclusions, disagreements, and source URLs.
|
|
1991
2146
|
COHALL_PROMPT
|
|
@@ -1995,11 +2150,21 @@ Only one input may read stdin. Use \`--context-file\` when the prompt uses stdin
|
|
|
1995
2150
|
|
|
1996
2151
|
## Diagnostics
|
|
1997
2152
|
|
|
1998
|
-
|
|
2153
|
+
Trace a known task before inspecting machine-local service logs:
|
|
1999
2154
|
|
|
2000
2155
|
\`\`\`bash
|
|
2001
|
-
|
|
2002
|
-
|
|
2156
|
+
cohall trace <task-id> --follow
|
|
2157
|
+
\`\`\`
|
|
2158
|
+
|
|
2159
|
+
The trace is redacted and reports relay dispatch, device execution, retries,
|
|
2160
|
+
and terminal state. Use \`cohall thread <thread-id>\` when prompt and result
|
|
2161
|
+
history is relevant.
|
|
2162
|
+
|
|
2163
|
+
If Cohall cannot connect or no target is available, run:
|
|
2164
|
+
|
|
2165
|
+
\`\`\`bash
|
|
2166
|
+
cohall doctor
|
|
2167
|
+
cohall devices
|
|
2003
2168
|
\`\`\`
|
|
2004
2169
|
|
|
2005
2170
|
Return the diagnostic failure to the user rather than claiming remote work ran.
|
|
@@ -2197,7 +2362,7 @@ var knownProviders = Provider.literals;
|
|
|
2197
2362
|
|
|
2198
2363
|
// apps/device/src/delegation.ts
|
|
2199
2364
|
init_src();
|
|
2200
|
-
import { Effect as Effect4, Schema as Schema5 } from "effect";
|
|
2365
|
+
import { Effect as Effect4, Schedule, Schema as Schema5 } from "effect";
|
|
2201
2366
|
|
|
2202
2367
|
class DeviceSelectionError extends Schema5.TaggedErrorClass()("Cohall.DeviceSelectionError", { message: Schema5.String, devices: Schema5.Array(Schema5.String) }) {
|
|
2203
2368
|
}
|
|
@@ -2269,6 +2434,22 @@ var waitForTask = Effect4.fn("Cohall.waitForTask")(function* (client, initial, t
|
|
|
2269
2434
|
}
|
|
2270
2435
|
return task;
|
|
2271
2436
|
});
|
|
2437
|
+
var followTaskTrace = Effect4.fn("Cohall.followTaskTrace")(function* (client, taskId, onTrace) {
|
|
2438
|
+
let revision;
|
|
2439
|
+
const poll = client.traceTask(taskId).pipe(Effect4.tap((trace) => {
|
|
2440
|
+
const latest = trace.events.at(-1);
|
|
2441
|
+
const nextRevision = `${trace.status}:${latest?.kind ?? "none"}:${latest?.at ?? "none"}`;
|
|
2442
|
+
if (nextRevision === revision) {
|
|
2443
|
+
return Effect4.succeed(undefined);
|
|
2444
|
+
}
|
|
2445
|
+
revision = nextRevision;
|
|
2446
|
+
return Effect4.sync(() => onTrace(trace));
|
|
2447
|
+
}));
|
|
2448
|
+
return yield* poll.pipe(Effect4.repeat({
|
|
2449
|
+
until: (trace) => terminalTaskStatuses.has(trace.status),
|
|
2450
|
+
schedule: Schedule.spaced("1 second")
|
|
2451
|
+
}));
|
|
2452
|
+
});
|
|
2272
2453
|
var threadContext = Effect4.fn("Cohall.threadContext")(function* (client, threadId) {
|
|
2273
2454
|
return yield* client.threadContext(threadId);
|
|
2274
2455
|
});
|
|
@@ -2291,7 +2472,7 @@ var valueOptions = new Set([
|
|
|
2291
2472
|
"token-file",
|
|
2292
2473
|
"workspace"
|
|
2293
2474
|
]);
|
|
2294
|
-
var flagOptions = new Set(["client-only", "no-wait"]);
|
|
2475
|
+
var flagOptions = new Set(["client-only", "follow", "no-wait"]);
|
|
2295
2476
|
var aliases = new Map([
|
|
2296
2477
|
["-c", "context"],
|
|
2297
2478
|
["-p", "prompt"],
|
|
@@ -2310,6 +2491,7 @@ Usage:
|
|
|
2310
2491
|
[--context text] [--thread uuid] [--workspace path]
|
|
2311
2492
|
[--timeout seconds] [--no-wait]
|
|
2312
2493
|
cohall status <task-id>
|
|
2494
|
+
cohall trace <task-id> [--follow]
|
|
2313
2495
|
cohall wait <task-id> [--timeout seconds]
|
|
2314
2496
|
cohall cancel <task-id>
|
|
2315
2497
|
cohall thread <thread-id>
|
|
@@ -2405,6 +2587,7 @@ var identifier = (arguments_, label) => {
|
|
|
2405
2587
|
return value;
|
|
2406
2588
|
};
|
|
2407
2589
|
var print = (value) => console.log(JSON.stringify(value, null, 2));
|
|
2590
|
+
var printLine = (value) => console.log(JSON.stringify(value));
|
|
2408
2591
|
var readInput = async (arguments_, name) => {
|
|
2409
2592
|
const direct = option(arguments_, name);
|
|
2410
2593
|
const path = option(arguments_, `${name}-file`);
|
|
@@ -2706,6 +2889,16 @@ var runCli = async (command2, raw) => {
|
|
|
2706
2889
|
print(taskResult(await Effect5.runPromise(relay.getTask(id))));
|
|
2707
2890
|
return;
|
|
2708
2891
|
}
|
|
2892
|
+
if (command2 === "trace") {
|
|
2893
|
+
allowOptions(arguments_, ["follow"]);
|
|
2894
|
+
const id = Schema6.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
|
|
2895
|
+
if (arguments_.options.has("follow")) {
|
|
2896
|
+
await Effect5.runPromise(followTaskTrace(relay, id, printLine));
|
|
2897
|
+
return;
|
|
2898
|
+
}
|
|
2899
|
+
print(await Effect5.runPromise(relay.traceTask(id)));
|
|
2900
|
+
return;
|
|
2901
|
+
}
|
|
2709
2902
|
if (command2 === "wait") {
|
|
2710
2903
|
allowOptions(arguments_, ["timeout"]);
|
|
2711
2904
|
const id = Schema6.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
|
|
@@ -2730,7 +2923,7 @@ var runCli = async (command2, raw) => {
|
|
|
2730
2923
|
|
|
2731
2924
|
// apps/device/src/daemon.ts
|
|
2732
2925
|
init_src();
|
|
2733
|
-
import { Effect as Effect6, Schedule, Schema as Schema7 } from "effect";
|
|
2926
|
+
import { Effect as Effect6, Schedule as Schedule2, Schema as Schema7 } from "effect";
|
|
2734
2927
|
import { realpath as realpath2 } from "node:fs/promises";
|
|
2735
2928
|
import { arch, hostname as hostname2, platform as platform3 } from "node:os";
|
|
2736
2929
|
import { basename, isAbsolute as isAbsolute2, relative } from "node:path";
|
|
@@ -3012,7 +3205,7 @@ var runDaemon = (configuration) => {
|
|
|
3012
3205
|
tasks: new Map,
|
|
3013
3206
|
completed: new Set
|
|
3014
3207
|
};
|
|
3015
|
-
return connect(configuration, state).pipe(Effect6.repeat({ schedule:
|
|
3208
|
+
return connect(configuration, state).pipe(Effect6.repeat({ schedule: Schedule2.spaced("2 seconds") }), Effect6.ensuring(Effect6.sync(() => {
|
|
3016
3209
|
state.socket?.close();
|
|
3017
3210
|
for (const controller of state.tasks.values()) {
|
|
3018
3211
|
controller.abort();
|
|
@@ -3069,6 +3262,11 @@ var runMcp = async (configuration) => {
|
|
|
3069
3262
|
description: "Read the current state and result of a Cohall task.",
|
|
3070
3263
|
inputSchema: { task_id: z.string().uuid() }
|
|
3071
3264
|
}, async ({ task_id }) => output(taskResult(await Effect7.runPromise(client2.getTask(TaskId.make(task_id))))));
|
|
3265
|
+
server.registerTool("task_trace", {
|
|
3266
|
+
title: "Trace a delegated task",
|
|
3267
|
+
description: "Read the redacted relay and device lifecycle for troubleshooting a Cohall task.",
|
|
3268
|
+
inputSchema: { task_id: z.string().uuid() }
|
|
3269
|
+
}, async ({ task_id }) => output(await Effect7.runPromise(client2.traceTask(TaskId.make(task_id)))));
|
|
3072
3270
|
server.registerTool("wait_task", {
|
|
3073
3271
|
title: "Wait for delegated work",
|
|
3074
3272
|
description: "Wait for a Cohall task to finish and return its final result.",
|
|
@@ -3126,4 +3324,4 @@ await main().catch((cause) => {
|
|
|
3126
3324
|
process.exitCode = 1;
|
|
3127
3325
|
});
|
|
3128
3326
|
|
|
3129
|
-
//# debugId=
|
|
3327
|
+
//# debugId=266CBA4746A41C1964756E2164756E21
|