@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 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.2.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$/, {
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.query(`UPDATE tasks SET status = ?, provider_session_id = ?, result = ?, error = ?,
669
- started_at = ?, completed_at = ?, updated_at = ?
670
- 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, now(), taskId, ...expected),
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.query(`UPDATE tasks SET status = 'assigned', updated_at = ?
682
- WHERE id = ? AND status = 'queued' AND NOT EXISTS (
683
- SELECT 1 FROM tasks active
684
- WHERE active.target_device_id = ? AND active.id <> ?
685
- AND active.status IN ('assigned', 'running', 'cancelling')
686
- )`).run(now(), taskId, current.targetDeviceId, taskId),
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
- return current.status === "queued" ? yield* transition(taskId, ["queued"], { status: "cancelled", completedAt: now() }) : yield* transition(taskId, ["assigned", "running", "cancelling"], {
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.query(`UPDATE tasks SET status = 'queued', updated_at = ?
747
- WHERE target_device_id = ? AND status IN ('assigned', 'running')`).run(now(), deviceId),
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 completed without a text response");
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
- npx -y @akshar5/cohall devices
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
- npx -y @akshar5/cohall delegate \\
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
- npx -y @akshar5/cohall delegate \\
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
- npx -y @akshar5/cohall delegate --target @linux --no-wait \\
2113
+ cohall delegate --target @linux --no-wait \\
1959
2114
  --prompt 'Run the project test suite and report failures.'
1960
- npx -y @akshar5/cohall status <task-id>
1961
- npx -y @akshar5/cohall wait <task-id> --timeout 1800
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
- npx -y @akshar5/cohall cancel <task-id>
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
- npx -y @akshar5/cohall thread <thread-id>
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
- npx -y @akshar5/cohall delegate --target @macbook --prompt - <<'COHALL_PROMPT'
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
- If a command cannot connect or no target is available, run:
2153
+ Trace a known task before inspecting machine-local service logs:
1999
2154
 
2000
2155
  \`\`\`bash
2001
- npx -y @akshar5/cohall doctor
2002
- npx -y @akshar5/cohall devices
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: Schedule.spaced("2 seconds") }), Effect6.ensuring(Effect6.sync(() => {
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=7CC47D186DE52B1264756E2164756E21
3327
+ //# debugId=266CBA4746A41C1964756E2164756E21