@cueloop/daemon 0.1.0-alpha.10 → 0.1.0-alpha.100

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.
Files changed (82) hide show
  1. package/README.md +7 -0
  2. package/SDK.md +102 -0
  3. package/package.json +43 -22
  4. package/src/agent-tools.ts +98 -0
  5. package/src/agent-turn.test.ts +24 -0
  6. package/src/agent-turn.ts +34 -0
  7. package/src/api.test.ts +1081 -65
  8. package/src/api.ts +1425 -89
  9. package/src/capabilities.test.ts +57 -0
  10. package/src/capabilities.ts +95 -0
  11. package/src/client-errors.ts +24 -0
  12. package/src/client-reliability.test.ts +189 -0
  13. package/src/client.test.ts +37 -0
  14. package/src/client.ts +820 -94
  15. package/src/curate.ts +109 -0
  16. package/src/daemon-sdk-effect.ts +113 -0
  17. package/src/daemon-sdk.test.ts +306 -0
  18. package/src/daemon-sdk.ts +397 -0
  19. package/src/daemon-version.test.ts +142 -0
  20. package/src/diff-hot-reload.test.ts +419 -0
  21. package/src/diff-watcher.test.ts +117 -0
  22. package/src/diff-watcher.ts +326 -0
  23. package/src/examples/daemon-sdk.ts +55 -0
  24. package/src/gh.test.ts +59 -0
  25. package/src/gh.ts +74 -0
  26. package/src/ghostty-thread-surface-store.test.ts +49 -0
  27. package/src/ghostty-thread-surface-store.ts +101 -0
  28. package/src/ghostty-thread-surface.test.ts +398 -0
  29. package/src/ghostty-thread-surface.ts +267 -0
  30. package/src/harness-delivery.test.ts +269 -0
  31. package/src/harness-diagnostics.test.ts +26 -0
  32. package/src/harness-diagnostics.ts +30 -0
  33. package/src/harness-state-store.test.ts +93 -0
  34. package/src/harness-state-store.ts +213 -0
  35. package/src/herdr-split.test.ts +106 -0
  36. package/src/herdr-split.ts +85 -0
  37. package/src/herdr-thread-surface-store.test.ts +81 -0
  38. package/src/herdr-thread-surface-store.ts +66 -0
  39. package/src/herdr-thread-surface.test.ts +465 -0
  40. package/src/herdr-thread-surface.ts +378 -0
  41. package/src/index.ts +8 -9
  42. package/src/interruptible-wait.test.ts +127 -0
  43. package/src/interruptible-wait.ts +66 -0
  44. package/src/jj-working-tree.ts +131 -0
  45. package/src/main.ts +4 -1
  46. package/src/operation-receipts.test.ts +111 -0
  47. package/src/operation-receipts.ts +91 -0
  48. package/src/paths.ts +49 -0
  49. package/src/pr-poller.test.ts +102 -0
  50. package/src/pr-poller.ts +73 -0
  51. package/src/project-files.test.ts +134 -0
  52. package/src/project-files.ts +68 -0
  53. package/src/protocol.ts +78 -7
  54. package/src/retention.test.ts +191 -0
  55. package/src/retention.ts +123 -0
  56. package/src/sdk-identities.ts +35 -0
  57. package/src/server.test.ts +456 -42
  58. package/src/server.ts +601 -88
  59. package/src/share-blob.test.ts +119 -0
  60. package/src/share-blob.ts +57 -0
  61. package/src/single-instance.test.ts +162 -13
  62. package/src/store.conformance.test.ts +41 -0
  63. package/src/store.jsonl.test.ts +173 -0
  64. package/src/store.ts +267 -34
  65. package/src/testing/store-conformance.ts +192 -0
  66. package/src/thread-agent-validation.ts +119 -0
  67. package/src/thread-agent.test.ts +412 -0
  68. package/src/thread-agent.ts +713 -0
  69. package/src/thread-review.test.ts +483 -0
  70. package/src/thread-review.ts +382 -0
  71. package/src/thread-subscription.test.ts +174 -0
  72. package/src/thread-subscription.ts +129 -0
  73. package/src/thread-surface-config.test.ts +63 -0
  74. package/src/thread-surface-config.ts +61 -0
  75. package/src/validate.test.ts +279 -37
  76. package/src/validate.ts +418 -33
  77. package/src/vcs-source.test.ts +180 -0
  78. package/src/vcs-source.ts +229 -0
  79. package/src/version.ts +6 -0
  80. package/src/working-tree.ts +150 -0
  81. package/src/review.test.ts +0 -167
  82. package/src/review.ts +0 -174
package/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # @cueloop/daemon
2
+
3
+ The local cueloop daemon owns Thread mutations, persisted review history, harness
4
+ bindings and socket subscriptions. Integrations use its typed NDJSON client.
5
+
6
+ See the [daemon SDK contract](SDK.md) for explicit capabilities, safe mutation
7
+ retries, reconnecting subscriptions and the optional scoped Effect service.
package/SDK.md ADDED
@@ -0,0 +1,102 @@
1
+ # Daemon SDK
2
+
3
+ The SDK is a facade over the existing NDJSON socket client. It lives at
4
+ `@cueloop/daemon/sdk`; the optional Effect service is at
5
+ `@cueloop/daemon/sdk-effect`. Both use the same daemon, Thread records and harness
6
+ adapter. Importing the Promise SDK does not load Effect.
7
+
8
+ ## Capabilities
9
+
10
+ `connectOwnerSdk({ home, autostart })` authenticates with the local owner token.
11
+ It exposes Thread creation, review comments, review messages and embedded agent
12
+ operations. Agent methods still require `[experimental] thread_agent = true` in
13
+ the daemon's workspace configuration. Shared collaborators cannot invoke them.
14
+
15
+ `connectReviewSdk({ home, role: "collaborator" | "agent", author })` exposes
16
+ Thread reads and review comments only. An `agent` role here means a review-side
17
+ agent, not the daemon-owned embedded harness. The daemon enforces authorization;
18
+ TypeScript narrows the available operations but does not grant authority.
19
+ The SDK brands Thread, operation and submission IDs separately. Thread reads
20
+ return branded identities; `sdk.ids.thread`, `sdk.ids.operation` and
21
+ `sdk.ids.submission` parse external strings for incremental adoption.
22
+
23
+ ## Acceptance, completion and retries
24
+
25
+ `sessions.sendMessage` and `agents.prompt` require a caller-chosen `operationId`
26
+ (up to 128 characters). Persist that ID with the normalized input if a workflow
27
+ needs to resume. Receipts are scoped to a Thread and method. The same ID and
28
+ payload return the original accepted message or submission batch after a lost
29
+ response, disconnect or daemon restart. Different payloads reject with
30
+ `operation_conflict`. Submission acceptance does not imply agent completion.
31
+
32
+ Receipts commit in the same persisted record as their mutation. Each method
33
+ retains up to 128 receipts for the lifetime of the Thread; it rejects new IDs with
34
+ `operation_capacity` rather than evicting receipts and risking duplicate work.
35
+ Deleting or retaining out an old Thread removes its receipts. A missing Thread
36
+ rejects retries. Private receipt metadata is omitted from share blobs.
37
+
38
+ `agents.wait(accepted)` waits for that exact batch and returns `completed`,
39
+ `failed`, or `cancelled`. Terminal outcomes stay fixed even when a failed
40
+ submission is explicitly retried with a new operation ID. Queued or running
41
+ submissions recovered after a daemon restart become failed; they are never
42
+ silently re-executed.
43
+
44
+ There is no automatic mutation replay. A transport failure after writing has
45
+ `certainty: "unknown"`; work may already have committed. Explicitly retry the two
46
+ receipt-backed methods with the same operation ID and payload. Thread creation,
47
+ comment replies and other legacy methods have no acceptance receipts: do not
48
+ retry an ambiguous write automatically. Comments accept caller-chosen annotation
49
+ IDs for upsert semantics, which are separate from mutation acceptance receipts.
50
+
51
+ ## Failures and cancellation
52
+
53
+ `DaemonClientError` carries a daemon rejection `code` (including `forbidden`,
54
+ `resolved`, `operation_conflict` and `operation_capacity`). A rejection is not a
55
+ blanket rollback guarantee: a daemon internal error can follow a commit.
56
+
57
+ `DaemonTransportError` carries `kind`: `protocol`, `connection`, `timeout` or
58
+ `cancelled`, plus `certainty`: `not_sent` or `unknown`. A malformed result rejects
59
+ its request promptly. A malformed outer frame closes the connection and rejects
60
+ all outstanding requests. Pending requests remove timers and abort listeners on
61
+ every completion or failure path.
62
+
63
+ Requests accept `{ timeoutMs, signal }`, defaulting to a 30-second local deadline.
64
+ Cancelling a request or wait stops local observation; it does not undo a mutation
65
+ or stop a harness. `agents.cancel(threadId)` explicitly requests cancellation of
66
+ the active turn and preserves partial output. It does not clear queued inputs.
67
+ Closing the owner SDK also cancels its outstanding local waits.
68
+
69
+ ## Subscriptions
70
+
71
+ `subscribeThreadState` owns an observation connection, subscribes before reading,
72
+ refreshes after matching event hints and reconnects after connection loss. Every
73
+ reconnect reads authoritative state, including changes made while disconnected.
74
+ It never replays a write or treats event notifications as a durable log.
75
+
76
+ Generation and request counters prevent late reads from overwriting newer state.
77
+ Disposal removes listeners, cancels reconnect timers, aborts reads and closes the
78
+ observation connection. Protocol errors and authorization/version rejections
79
+ stop reconnection. Transient connection failures retry at the configured delay.
80
+ The Thread-agent UI uses this shared lifecycle.
81
+
82
+ ## Effect and example
83
+
84
+ `CueloopSdk.ownerLayer` acquires the owner connection with `Effect.acquireRelease`.
85
+ Its operations have a typed `DaemonSdkError` channel. `threads.watch(id)` is a
86
+ scoped stream of refreshed snapshots with a one-item sliding buffer. It coalesces
87
+ state updates rather than promising delivery of every event. Effect interruption is passed
88
+ through an AbortSignal, including to completion waits. Scope finalization closes
89
+ the connection. The layer does not replace the daemon or rewrite UI state in
90
+ Effect.
91
+
92
+ With the experimental flag enabled and a harness configured, run:
93
+
94
+ ```sh
95
+ bun run packages/daemon/src/examples/daemon-sdk.ts
96
+ ```
97
+
98
+ The example creates and reads a Thread, submits a prompt, waits for its terminal
99
+ outcome, lists and replies to a comment, then sends approval. It creates a real
100
+ Thread and invokes the configured model. IDs are generated once per invocation;
101
+ a resumable application should save them rather than generate replacements when
102
+ retrying.
package/package.json CHANGED
@@ -1,35 +1,56 @@
1
1
  {
2
2
  "name": "@cueloop/daemon",
3
- "version": "0.1.0-alpha.10",
3
+ "version": "0.1.0-alpha.100",
4
+ "description": "The cueloop session daemon and its client: socket protocol, atomic store, resumable waits",
5
+ "homepage": "https://github.com/mmurakaru/cueloop#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/mmurakaru/cueloop/issues"
8
+ },
4
9
  "license": "Apache-2.0",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/mmurakaru/cueloop.git",
13
+ "directory": "packages/daemon"
14
+ },
15
+ "files": [
16
+ "src",
17
+ "README.md",
18
+ "SDK.md"
19
+ ],
5
20
  "type": "module",
6
21
  "exports": {
7
22
  ".": "./src/index.ts",
8
23
  "./client": "./src/client.ts",
9
- "./review": "./src/review.ts"
10
- },
11
- "dependencies": {
12
- "@cueloop/schema": "0.1.0-alpha.10",
13
- "valibot": "^1.4.2"
24
+ "./thread-review": "./src/thread-review.ts",
25
+ "./vcs-source": "./src/vcs-source.ts",
26
+ "./api": "./src/api.ts",
27
+ "./working-tree": "./src/working-tree.ts",
28
+ "./store": "./src/store.ts",
29
+ "./validate": "./src/validate.ts",
30
+ "./paths": "./src/paths.ts",
31
+ "./retention": "./src/retention.ts",
32
+ "./herdr-thread-surface": "./src/herdr-thread-surface.ts",
33
+ "./ghostty-thread-surface": "./src/ghostty-thread-surface.ts",
34
+ "./thread-surface-config": "./src/thread-surface-config.ts",
35
+ "./herdr-split": "./src/herdr-split.ts",
36
+ "./share-blob": "./src/share-blob.ts",
37
+ "./curate": "./src/curate.ts",
38
+ "./sdk": "./src/daemon-sdk.ts",
39
+ "./sdk-effect": "./src/daemon-sdk-effect.ts",
40
+ "./thread-subscription": "./src/thread-subscription.ts"
14
41
  },
15
42
  "publishConfig": {
16
43
  "access": "public"
17
44
  },
18
- "files": [
19
- "src",
20
- "README.md"
21
- ],
45
+ "dependencies": {
46
+ "@cueloop/extension-api": "0.1.0-alpha.100",
47
+ "@cueloop/schema": "0.1.0-alpha.100",
48
+ "@pierre/diffs": "^1.5.1",
49
+ "effect": "^4.0.0",
50
+ "smol-toml": "^1.9.0",
51
+ "valibot": "^1.5.0"
52
+ },
22
53
  "engines": {
23
54
  "bun": ">=1.3.0"
24
- },
25
- "repository": {
26
- "type": "git",
27
- "url": "git+https://github.com/mmurakaru/cueloop.git",
28
- "directory": "packages/daemon"
29
- },
30
- "homepage": "https://github.com/mmurakaru/cueloop#readme",
31
- "bugs": {
32
- "url": "https://github.com/mmurakaru/cueloop/issues"
33
- },
34
- "description": "The cueloop session daemon and its client: socket protocol, atomic store, resumable waits"
35
- }
55
+ }
56
+ }
@@ -0,0 +1,98 @@
1
+ import * as v from "valibot";
2
+ import { resolve } from "node:path";
3
+ import { MESSAGE_OUTCOMES, type Thread } from "@cueloop/schema";
4
+ import type { MethodName } from "./validate";
5
+
6
+ /** Tool arguments are validated before entering the owner's daemon API. */
7
+ export function parseAgentToolInput(name: string, serialized: string) {
8
+ const input: unknown = JSON.parse(serialized);
9
+
10
+ if (name === "reply_to_comment") {
11
+ const args = v.parse(
12
+ v.object({
13
+ id: v.string(),
14
+ commentId: v.string(),
15
+ body: v.pipe(v.string(), v.minLength(1), v.maxLength(32768)),
16
+ }),
17
+ input,
18
+ );
19
+
20
+ return { kind: "reply" as const, ...args };
21
+ }
22
+ if (name === "send_message") {
23
+ const args = v.parse(
24
+ v.object({
25
+ id: v.string(),
26
+ outcome: v.picklist(MESSAGE_OUTCOMES),
27
+ summary: v.optional(v.string(), ""),
28
+ }),
29
+ input,
30
+ );
31
+
32
+ return { kind: "api" as const, method: "session.sendMessage", params: args };
33
+ }
34
+ if (name !== "cueloop_api") throw new Error("Thread agent tool name is unavailable");
35
+ const args = v.parse(v.object({ method: v.string(), params: v.unknown() }), input);
36
+
37
+ return { kind: "api" as const, ...args };
38
+ }
39
+
40
+ const THREAD_AGENT_SESSION_METHODS = new Set<string>([
41
+ "session.get",
42
+ "session.wait",
43
+ "session.annotate",
44
+ "session.comment",
45
+ "session.removeAnnotation",
46
+ "session.setParticipantName",
47
+ "session.setWorkingCopy",
48
+ "session.cutBlock",
49
+ "session.navigate",
50
+ "session.branch",
51
+ "session.switch",
52
+ "session.label",
53
+ "session.restoreBlock",
54
+ "session.curate",
55
+ "session.setViewed",
56
+ "session.setTitle",
57
+ "session.projectFiles",
58
+ "session.fileContents",
59
+ "session.refreshDiff",
60
+ "session.sendMessage",
61
+ "session.submitRevision",
62
+ ] satisfies MethodName[]);
63
+ const THREAD_AGENT_REPO_METHODS = new Set<string>([
64
+ "repo.files",
65
+ "repo.fileContents",
66
+ "repo.changes",
67
+ "repo.diff",
68
+ ] satisfies MethodName[]);
69
+
70
+ /** Harness tools are bound to their originating Thread; model arguments cannot widen that scope. */
71
+ export function assertAgentToolScope(
72
+ thread: Thread,
73
+ input: ReturnType<typeof parseAgentToolInput>,
74
+ ): void {
75
+ if (input.kind === "reply") {
76
+ if (input.id !== thread.id) throw new Error("Thread agent tool cannot access another Thread");
77
+
78
+ return;
79
+ }
80
+ if (input.method === "session.list") return;
81
+ const params = v.parse(v.record(v.string(), v.unknown()), input.params);
82
+
83
+ if (THREAD_AGENT_SESSION_METHODS.has(input.method)) {
84
+ if (params.id !== thread.id) throw new Error("Thread agent tool cannot access another Thread");
85
+
86
+ return;
87
+ }
88
+ if (THREAD_AGENT_REPO_METHODS.has(input.method)) {
89
+ const { cwd } = v.parse(v.object({ cwd: v.string() }), params);
90
+
91
+ if (resolve(cwd) !== resolve(thread.workspace.repoRoot))
92
+ throw new Error("Thread agent tool cannot access another repository");
93
+
94
+ return;
95
+ }
96
+
97
+ throw new Error("Thread agent tool API method is unavailable");
98
+ }
@@ -0,0 +1,24 @@
1
+ import { expect, test } from "bun:test";
2
+ import { stepAgentTurn, type AgentTurn } from "./agent-turn";
3
+
4
+ test("an initialization stop settles before accepting a fresh turn", () => {
5
+ const starting = stepAgentTurn({ kind: "idle" }, "start");
6
+ const stopped = stepAgentTurn(starting, "stop");
7
+ const ready = stepAgentTurn(stopped, "ready");
8
+
9
+ expect(ready).toEqual({ kind: "idle" });
10
+ expect(stepAgentTurn(ready, "start")).toEqual({ kind: "starting", cancelled: false });
11
+ });
12
+
13
+ test("an active stop waits for completion before accepting another turn", () => {
14
+ const running: AgentTurn = { kind: "running" };
15
+ const stopped = stepAgentTurn(running, "stop");
16
+
17
+ expect(stopped).toEqual({ kind: "stopping" });
18
+ expect(stepAgentTurn(stopped, "start")).toEqual(stopped);
19
+ expect(stepAgentTurn(stopped, "ready")).toEqual(stopped);
20
+ expect(stepAgentTurn(stepAgentTurn(stopped, "finished"), "start")).toEqual({
21
+ kind: "starting",
22
+ cancelled: false,
23
+ });
24
+ });
@@ -0,0 +1,34 @@
1
+ /** A harness turn distinguishes initialization cancellation from a running stop request. */
2
+ export type AgentTurn =
3
+ | { kind: "idle" }
4
+ | { kind: "starting"; cancelled: boolean }
5
+ | { kind: "running" }
6
+ | { kind: "stopping" };
7
+
8
+ /** Lifecycle events are independent of the harness wire protocol. */
9
+ export type AgentTurnEvent = "start" | "ready" | "stop" | "finished";
10
+
11
+ /** Start is accepted only at idle; readiness consumes an initialization cancellation. */
12
+ export function stepAgentTurn(state: AgentTurn, event: AgentTurnEvent): AgentTurn {
13
+ switch (event) {
14
+ case "start":
15
+ return state.kind === "idle" ? { kind: "starting", cancelled: false } : state;
16
+ case "ready":
17
+ return state.kind === "starting"
18
+ ? state.cancelled
19
+ ? { kind: "idle" }
20
+ : { kind: "running" }
21
+ : state;
22
+ case "stop":
23
+ if (state.kind === "starting") return { kind: "starting", cancelled: true };
24
+
25
+ return state.kind === "running" ? { kind: "stopping" } : state;
26
+ case "finished":
27
+ return { kind: "idle" };
28
+ }
29
+ }
30
+
31
+ /** Pending stop requests suppress later tool and permission events from this turn. */
32
+ export function agentTurnCancelled(state: AgentTurn): boolean {
33
+ return state.kind === "stopping" || (state.kind === "starting" && state.cancelled);
34
+ }