@akshar5/cohall 0.2.0 → 0.3.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 +13 -0
- package/README.md +6 -1
- package/bin/cohall.js +215 -35
- package/bin/cohall.js.map +9 -9
- package/package.json +2 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.3.0](https://github.com/AksharP5/cohall/compare/v0.2.0...v0.3.0) (2026-08-06)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add durable task tracing ([b7c2564](https://github.com/AksharP5/cohall/commit/b7c2564c770453a3c67821a9521aacac4f4a58e2))
|
|
9
|
+
|
|
10
|
+
## 0.2.0 (2026-08-05)
|
|
11
|
+
|
|
12
|
+
- Initial public release of the CLI, embedded agent skill, optional MCP server,
|
|
13
|
+
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.0", 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
|
}),
|
|
@@ -1897,12 +2031,15 @@ keeps its local files, credentials, browser state, provider login, skills, and
|
|
|
1897
2031
|
permissions. The relay carries prompts, final results, and task state; it is not
|
|
1898
2032
|
a lead agent.
|
|
1899
2033
|
|
|
2034
|
+
Use the installed \`cohall\` executable when it is available. Fall back to
|
|
2035
|
+
\`npx -y @akshar5/cohall\` only when Cohall is not installed globally.
|
|
2036
|
+
|
|
1900
2037
|
## Delegate work
|
|
1901
2038
|
|
|
1902
2039
|
1. List devices unless the target is already explicit:
|
|
1903
2040
|
|
|
1904
2041
|
\`\`\`bash
|
|
1905
|
-
|
|
2042
|
+
cohall devices
|
|
1906
2043
|
\`\`\`
|
|
1907
2044
|
|
|
1908
2045
|
2. Choose a target whose provider, workspaces, platform, and capabilities fit
|
|
@@ -1911,7 +2048,7 @@ a lead agent.
|
|
|
1911
2048
|
3. Delegate one concrete outcome. Send only the context the target needs:
|
|
1912
2049
|
|
|
1913
2050
|
\`\`\`bash
|
|
1914
|
-
|
|
2051
|
+
cohall delegate \\
|
|
1915
2052
|
--target @macbook \\
|
|
1916
2053
|
--provider codex \\
|
|
1917
2054
|
--workspace /Users/me/dev/project \\
|
|
@@ -1930,7 +2067,7 @@ a lead agent.
|
|
|
1930
2067
|
its local session:
|
|
1931
2068
|
|
|
1932
2069
|
\`\`\`bash
|
|
1933
|
-
|
|
2070
|
+
cohall delegate \\
|
|
1934
2071
|
--thread 11111111-1111-4111-8111-111111111111 \\
|
|
1935
2072
|
--target @macbook \\
|
|
1936
2073
|
--prompt 'Check whether yesterday’s deployment failed for the same reason.'
|
|
@@ -1955,17 +2092,17 @@ ordinary shell or a separate agent turn.
|
|
|
1955
2092
|
Queue work when the current agent can make independent progress:
|
|
1956
2093
|
|
|
1957
2094
|
\`\`\`bash
|
|
1958
|
-
|
|
2095
|
+
cohall delegate --target @linux --no-wait \\
|
|
1959
2096
|
--prompt 'Run the project test suite and report failures.'
|
|
1960
|
-
|
|
1961
|
-
|
|
2097
|
+
cohall status <task-id>
|
|
2098
|
+
cohall wait <task-id> --timeout 1800
|
|
1962
2099
|
\`\`\`
|
|
1963
2100
|
|
|
1964
2101
|
The timeout error includes the task ID and last known status. The task continues
|
|
1965
2102
|
unless cancelled:
|
|
1966
2103
|
|
|
1967
2104
|
\`\`\`bash
|
|
1968
|
-
|
|
2105
|
+
cohall cancel <task-id>
|
|
1969
2106
|
\`\`\`
|
|
1970
2107
|
|
|
1971
2108
|
Active cancellation is acknowledged by the target device; \`cancelling\` means
|
|
@@ -1974,7 +2111,7 @@ the provider process has not confirmed termination yet.
|
|
|
1974
2111
|
## Read shared context
|
|
1975
2112
|
|
|
1976
2113
|
\`\`\`bash
|
|
1977
|
-
|
|
2114
|
+
cohall thread <thread-id>
|
|
1978
2115
|
\`\`\`
|
|
1979
2116
|
|
|
1980
2117
|
This returns a byte-bounded recent window of prompts, final responses, and task
|
|
@@ -1985,7 +2122,7 @@ states. Check \`truncated\`; older history remains on the relay when it is true.
|
|
|
1985
2122
|
Use \`--prompt-file\` or stdin for multiline work without shell interpolation:
|
|
1986
2123
|
|
|
1987
2124
|
\`\`\`bash
|
|
1988
|
-
|
|
2125
|
+
cohall delegate --target @macbook --prompt - <<'COHALL_PROMPT'
|
|
1989
2126
|
Analyze the authenticated pages in the attached task context.
|
|
1990
2127
|
Return the shared conclusions, disagreements, and source URLs.
|
|
1991
2128
|
COHALL_PROMPT
|
|
@@ -1995,11 +2132,21 @@ Only one input may read stdin. Use \`--context-file\` when the prompt uses stdin
|
|
|
1995
2132
|
|
|
1996
2133
|
## Diagnostics
|
|
1997
2134
|
|
|
1998
|
-
|
|
2135
|
+
Trace a known task before inspecting machine-local service logs:
|
|
2136
|
+
|
|
2137
|
+
\`\`\`bash
|
|
2138
|
+
cohall trace <task-id> --follow
|
|
2139
|
+
\`\`\`
|
|
2140
|
+
|
|
2141
|
+
The trace is redacted and reports relay dispatch, device execution, retries,
|
|
2142
|
+
and terminal state. Use \`cohall thread <thread-id>\` when prompt and result
|
|
2143
|
+
history is relevant.
|
|
2144
|
+
|
|
2145
|
+
If Cohall cannot connect or no target is available, run:
|
|
1999
2146
|
|
|
2000
2147
|
\`\`\`bash
|
|
2001
|
-
|
|
2002
|
-
|
|
2148
|
+
cohall doctor
|
|
2149
|
+
cohall devices
|
|
2003
2150
|
\`\`\`
|
|
2004
2151
|
|
|
2005
2152
|
Return the diagnostic failure to the user rather than claiming remote work ran.
|
|
@@ -2197,7 +2344,7 @@ var knownProviders = Provider.literals;
|
|
|
2197
2344
|
|
|
2198
2345
|
// apps/device/src/delegation.ts
|
|
2199
2346
|
init_src();
|
|
2200
|
-
import { Effect as Effect4, Schema as Schema5 } from "effect";
|
|
2347
|
+
import { Effect as Effect4, Schedule, Schema as Schema5 } from "effect";
|
|
2201
2348
|
|
|
2202
2349
|
class DeviceSelectionError extends Schema5.TaggedErrorClass()("Cohall.DeviceSelectionError", { message: Schema5.String, devices: Schema5.Array(Schema5.String) }) {
|
|
2203
2350
|
}
|
|
@@ -2269,6 +2416,22 @@ var waitForTask = Effect4.fn("Cohall.waitForTask")(function* (client, initial, t
|
|
|
2269
2416
|
}
|
|
2270
2417
|
return task;
|
|
2271
2418
|
});
|
|
2419
|
+
var followTaskTrace = Effect4.fn("Cohall.followTaskTrace")(function* (client, taskId, onTrace) {
|
|
2420
|
+
let revision;
|
|
2421
|
+
const poll = client.traceTask(taskId).pipe(Effect4.tap((trace) => {
|
|
2422
|
+
const latest = trace.events.at(-1);
|
|
2423
|
+
const nextRevision = `${trace.status}:${latest?.kind ?? "none"}:${latest?.at ?? "none"}`;
|
|
2424
|
+
if (nextRevision === revision) {
|
|
2425
|
+
return Effect4.succeed(undefined);
|
|
2426
|
+
}
|
|
2427
|
+
revision = nextRevision;
|
|
2428
|
+
return Effect4.sync(() => onTrace(trace));
|
|
2429
|
+
}));
|
|
2430
|
+
return yield* poll.pipe(Effect4.repeat({
|
|
2431
|
+
until: (trace) => terminalTaskStatuses.has(trace.status),
|
|
2432
|
+
schedule: Schedule.spaced("1 second")
|
|
2433
|
+
}));
|
|
2434
|
+
});
|
|
2272
2435
|
var threadContext = Effect4.fn("Cohall.threadContext")(function* (client, threadId) {
|
|
2273
2436
|
return yield* client.threadContext(threadId);
|
|
2274
2437
|
});
|
|
@@ -2291,7 +2454,7 @@ var valueOptions = new Set([
|
|
|
2291
2454
|
"token-file",
|
|
2292
2455
|
"workspace"
|
|
2293
2456
|
]);
|
|
2294
|
-
var flagOptions = new Set(["client-only", "no-wait"]);
|
|
2457
|
+
var flagOptions = new Set(["client-only", "follow", "no-wait"]);
|
|
2295
2458
|
var aliases = new Map([
|
|
2296
2459
|
["-c", "context"],
|
|
2297
2460
|
["-p", "prompt"],
|
|
@@ -2310,6 +2473,7 @@ Usage:
|
|
|
2310
2473
|
[--context text] [--thread uuid] [--workspace path]
|
|
2311
2474
|
[--timeout seconds] [--no-wait]
|
|
2312
2475
|
cohall status <task-id>
|
|
2476
|
+
cohall trace <task-id> [--follow]
|
|
2313
2477
|
cohall wait <task-id> [--timeout seconds]
|
|
2314
2478
|
cohall cancel <task-id>
|
|
2315
2479
|
cohall thread <thread-id>
|
|
@@ -2405,6 +2569,7 @@ var identifier = (arguments_, label) => {
|
|
|
2405
2569
|
return value;
|
|
2406
2570
|
};
|
|
2407
2571
|
var print = (value) => console.log(JSON.stringify(value, null, 2));
|
|
2572
|
+
var printLine = (value) => console.log(JSON.stringify(value));
|
|
2408
2573
|
var readInput = async (arguments_, name) => {
|
|
2409
2574
|
const direct = option(arguments_, name);
|
|
2410
2575
|
const path = option(arguments_, `${name}-file`);
|
|
@@ -2706,6 +2871,16 @@ var runCli = async (command2, raw) => {
|
|
|
2706
2871
|
print(taskResult(await Effect5.runPromise(relay.getTask(id))));
|
|
2707
2872
|
return;
|
|
2708
2873
|
}
|
|
2874
|
+
if (command2 === "trace") {
|
|
2875
|
+
allowOptions(arguments_, ["follow"]);
|
|
2876
|
+
const id = Schema6.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
|
|
2877
|
+
if (arguments_.options.has("follow")) {
|
|
2878
|
+
await Effect5.runPromise(followTaskTrace(relay, id, printLine));
|
|
2879
|
+
return;
|
|
2880
|
+
}
|
|
2881
|
+
print(await Effect5.runPromise(relay.traceTask(id)));
|
|
2882
|
+
return;
|
|
2883
|
+
}
|
|
2709
2884
|
if (command2 === "wait") {
|
|
2710
2885
|
allowOptions(arguments_, ["timeout"]);
|
|
2711
2886
|
const id = Schema6.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
|
|
@@ -2730,7 +2905,7 @@ var runCli = async (command2, raw) => {
|
|
|
2730
2905
|
|
|
2731
2906
|
// apps/device/src/daemon.ts
|
|
2732
2907
|
init_src();
|
|
2733
|
-
import { Effect as Effect6, Schedule, Schema as Schema7 } from "effect";
|
|
2908
|
+
import { Effect as Effect6, Schedule as Schedule2, Schema as Schema7 } from "effect";
|
|
2734
2909
|
import { realpath as realpath2 } from "node:fs/promises";
|
|
2735
2910
|
import { arch, hostname as hostname2, platform as platform3 } from "node:os";
|
|
2736
2911
|
import { basename, isAbsolute as isAbsolute2, relative } from "node:path";
|
|
@@ -3012,7 +3187,7 @@ var runDaemon = (configuration) => {
|
|
|
3012
3187
|
tasks: new Map,
|
|
3013
3188
|
completed: new Set
|
|
3014
3189
|
};
|
|
3015
|
-
return connect(configuration, state).pipe(Effect6.repeat({ schedule:
|
|
3190
|
+
return connect(configuration, state).pipe(Effect6.repeat({ schedule: Schedule2.spaced("2 seconds") }), Effect6.ensuring(Effect6.sync(() => {
|
|
3016
3191
|
state.socket?.close();
|
|
3017
3192
|
for (const controller of state.tasks.values()) {
|
|
3018
3193
|
controller.abort();
|
|
@@ -3069,6 +3244,11 @@ var runMcp = async (configuration) => {
|
|
|
3069
3244
|
description: "Read the current state and result of a Cohall task.",
|
|
3070
3245
|
inputSchema: { task_id: z.string().uuid() }
|
|
3071
3246
|
}, async ({ task_id }) => output(taskResult(await Effect7.runPromise(client2.getTask(TaskId.make(task_id))))));
|
|
3247
|
+
server.registerTool("task_trace", {
|
|
3248
|
+
title: "Trace a delegated task",
|
|
3249
|
+
description: "Read the redacted relay and device lifecycle for troubleshooting a Cohall task.",
|
|
3250
|
+
inputSchema: { task_id: z.string().uuid() }
|
|
3251
|
+
}, async ({ task_id }) => output(await Effect7.runPromise(client2.traceTask(TaskId.make(task_id)))));
|
|
3072
3252
|
server.registerTool("wait_task", {
|
|
3073
3253
|
title: "Wait for delegated work",
|
|
3074
3254
|
description: "Wait for a Cohall task to finish and return its final result.",
|
|
@@ -3126,4 +3306,4 @@ await main().catch((cause) => {
|
|
|
3126
3306
|
process.exitCode = 1;
|
|
3127
3307
|
});
|
|
3128
3308
|
|
|
3129
|
-
//# debugId=
|
|
3309
|
+
//# debugId=AB53FFE376B97AD664756E2164756E21
|