@cueloop/daemon 0.1.0-alpha.11 → 0.1.0-alpha.111

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 (85) hide show
  1. package/README.md +7 -0
  2. package/SDK.md +102 -0
  3. package/package.json +44 -22
  4. package/src/agent-tools.ts +104 -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 +1483 -90
  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 +191 -0
  13. package/src/client.test.ts +37 -0
  14. package/src/client.ts +844 -95
  15. package/src/curate.ts +111 -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 +405 -0
  19. package/src/daemon-version.test.ts +142 -0
  20. package/src/diff-hot-reload.test.ts +423 -0
  21. package/src/diff-watcher.test.ts +165 -0
  22. package/src/diff-watcher.ts +363 -0
  23. package/src/examples/daemon-sdk.ts +57 -0
  24. package/src/gh.test.ts +59 -0
  25. package/src/gh.ts +78 -0
  26. package/src/ghostty-thread-surface-store.test.ts +49 -0
  27. package/src/ghostty-thread-surface-store.ts +104 -0
  28. package/src/ghostty-thread-surface.test.ts +400 -0
  29. package/src/ghostty-thread-surface.ts +273 -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 +31 -0
  33. package/src/harness-handoff.test.ts +725 -0
  34. package/src/harness-handoff.ts +27 -0
  35. package/src/harness-state-store.test.ts +93 -0
  36. package/src/harness-state-store.ts +218 -0
  37. package/src/herdr-split.test.ts +106 -0
  38. package/src/herdr-split.ts +90 -0
  39. package/src/herdr-thread-surface-store.test.ts +81 -0
  40. package/src/herdr-thread-surface-store.ts +66 -0
  41. package/src/herdr-thread-surface.test.ts +465 -0
  42. package/src/herdr-thread-surface.ts +392 -0
  43. package/src/index.ts +8 -9
  44. package/src/interruptible-wait.test.ts +127 -0
  45. package/src/interruptible-wait.ts +69 -0
  46. package/src/jj-working-tree.ts +140 -0
  47. package/src/main.ts +5 -1
  48. package/src/operation-receipts.test.ts +111 -0
  49. package/src/operation-receipts.ts +104 -0
  50. package/src/paths.ts +49 -0
  51. package/src/pr-poller.test.ts +102 -0
  52. package/src/pr-poller.ts +80 -0
  53. package/src/project-files.test.ts +134 -0
  54. package/src/project-files.ts +72 -0
  55. package/src/protocol.ts +82 -7
  56. package/src/retention.test.ts +191 -0
  57. package/src/retention.ts +131 -0
  58. package/src/sdk-identities.ts +35 -0
  59. package/src/server.test.ts +502 -42
  60. package/src/server.ts +660 -88
  61. package/src/share-blob.test.ts +142 -0
  62. package/src/share-blob.ts +57 -0
  63. package/src/shared-agent-protocol.ts +30 -0
  64. package/src/single-instance.test.ts +165 -13
  65. package/src/store.conformance.test.ts +41 -0
  66. package/src/store.jsonl.test.ts +173 -0
  67. package/src/store.ts +278 -34
  68. package/src/testing/store-conformance.ts +192 -0
  69. package/src/thread-agent-validation.ts +141 -0
  70. package/src/thread-agent.test.ts +732 -0
  71. package/src/thread-agent.ts +1171 -0
  72. package/src/thread-review.test.ts +487 -0
  73. package/src/thread-review.ts +394 -0
  74. package/src/thread-subscription.test.ts +177 -0
  75. package/src/thread-subscription.ts +138 -0
  76. package/src/thread-surface-config.test.ts +63 -0
  77. package/src/thread-surface-config.ts +61 -0
  78. package/src/validate.test.ts +290 -37
  79. package/src/validate.ts +427 -33
  80. package/src/vcs-source.test.ts +180 -0
  81. package/src/vcs-source.ts +235 -0
  82. package/src/version.ts +6 -0
  83. package/src/working-tree.ts +154 -0
  84. package/src/review.test.ts +0 -167
  85. package/src/review.ts +0 -174
@@ -0,0 +1,192 @@
1
+ /**
2
+ * The session repository contract as executable cases. Every adapter runs the
3
+ * same suite: what a store returns, in which order, what survives a restart,
4
+ * what a bad record does, and how a record without a history reads.
5
+ *
6
+ * `restart` gives back a fresh adapter over the same persisted state (or the
7
+ * same seed) so recovery is tested through the contract, never past it.
8
+ */
9
+
10
+ import { describe, expect, test } from "bun:test";
11
+ import { SCHEMA_VERSION, type Thread } from "@cueloop/schema";
12
+ import type { ThreadRepository } from "../store";
13
+
14
+ export interface StoreHarness {
15
+ open: (records: unknown[]) => ThreadRepository;
16
+ restart: (store: ThreadRepository) => ThreadRepository;
17
+ }
18
+
19
+ export function createTestThreadRecord(
20
+ id: string,
21
+ createdAt: string,
22
+ overrides: Partial<Thread> = {},
23
+ ): Thread {
24
+ return {
25
+ schemaVersion: SCHEMA_VERSION,
26
+ id,
27
+ workspace: { repoRoot: "/repo", branch: "main" },
28
+ artifact: { type: "plan", content: `# ${id}`, meta: {} },
29
+ revisions: [{ revision: 1, content: `# ${id}`, submittedAt: createdAt }],
30
+ annotations: [],
31
+ message: null,
32
+ status: "pending",
33
+ createdAt,
34
+ ...overrides,
35
+ };
36
+ }
37
+
38
+ export function runThreadStoreConformance(name: string, harness: StoreHarness): void {
39
+ describe(`${name} - session repository contract`, () => {
40
+ test("a stored session reads back whole and lists oldest first", () => {
41
+ // Arrange
42
+ const store = harness.open([]);
43
+
44
+ store.recover();
45
+ const newer = createTestThreadRecord("ses_b", "2026-09-02T00:00:00.000Z");
46
+ const older = createTestThreadRecord("ses_a", "2026-09-01T00:00:00.000Z");
47
+
48
+ // Act
49
+ store.upsert(newer);
50
+ store.upsert(older);
51
+
52
+ // Assert
53
+ expect(store.get("ses_a")).toEqual(older);
54
+ expect(store.list().map((session) => session.id)).toEqual(["ses_a", "ses_b"]);
55
+ expect(store.get("ses_zzz")).toBeUndefined();
56
+ });
57
+
58
+ test("upsert replaces by id and delete reports whether anything was removed", () => {
59
+ // Arrange
60
+ const store = harness.open([]);
61
+
62
+ store.recover();
63
+ store.upsert(createTestThreadRecord("ses_a", "2026-09-01T00:00:00.000Z"));
64
+
65
+ // Act
66
+ store.upsert(
67
+ createTestThreadRecord("ses_a", "2026-09-01T00:00:00.000Z", { status: "resolved" }),
68
+ );
69
+
70
+ // Assert
71
+ expect(store.list()).toHaveLength(1);
72
+ expect(store.get("ses_a")?.status).toBe("resolved");
73
+ expect(store.delete("ses_a")).toBe(true);
74
+ expect(store.delete("ses_a")).toBe(false);
75
+ expect(store.list()).toEqual([]);
76
+ });
77
+
78
+ test("what was upserted survives a restart, deletions included", () => {
79
+ // Arrange
80
+ const store = harness.open([]);
81
+
82
+ store.recover();
83
+ store.upsert(createTestThreadRecord("ses_keep", "2026-09-01T00:00:00.000Z"));
84
+ store.upsert(createTestThreadRecord("ses_gone", "2026-09-02T00:00:00.000Z"));
85
+ store.delete("ses_gone");
86
+
87
+ // Act
88
+ const reopened = harness.restart(store);
89
+ const report = reopened.recover();
90
+
91
+ // Assert
92
+ expect(report.recovered).toEqual(["ses_keep"]);
93
+ expect(reopened.get("ses_keep")?.artifact.content).toBe("# ses_keep");
94
+ expect(reopened.get("ses_gone")).toBeUndefined();
95
+ });
96
+
97
+ test("a record without a history reads as a one-branch tree whose head is the artifact", () => {
98
+ // Arrange: written before histories existed, with two revisions and a comment
99
+ const legacy = createTestThreadRecord("ses_old", "2026-09-01T00:00:00.000Z", {
100
+ artifact: { type: "plan", content: "v2", meta: {} },
101
+ revisions: [
102
+ { revision: 1, content: "v1", submittedAt: "2026-09-01T00:00:00.000Z" },
103
+ { revision: 2, content: "v2", submittedAt: "2026-09-01T02:00:00.000Z" },
104
+ ],
105
+ annotations: [
106
+ {
107
+ id: "a1",
108
+ kind: "comment",
109
+ anchor: { quote: "v1", prefix: "", suffix: "" },
110
+ body: "why",
111
+ createdAt: "2026-09-01T01:00:00.000Z",
112
+ },
113
+ ],
114
+ });
115
+ const store = harness.open([legacy]);
116
+
117
+ // Act
118
+ const report = store.recover();
119
+ const history = store.get("ses_old")?.history;
120
+
121
+ // Assert
122
+ expect(report.recovered).toEqual(["ses_old"]);
123
+ expect(history?.branch).toBe("main");
124
+ expect(history?.entries.map((entry) => entry.type)).toEqual([
125
+ "revision",
126
+ "comment",
127
+ "revision",
128
+ ]);
129
+ expect(history?.entries.at(-1)).toMatchObject({ type: "revision", content: "v2" });
130
+ expect(history?.tips.main).toBe(history?.entries.at(-1)?.id);
131
+ });
132
+
133
+ test("a record with no revision still recovers, without a history", () => {
134
+ // Arrange
135
+ const store = harness.open([
136
+ createTestThreadRecord("ses_empty", "2026-09-01T00:00:00.000Z", { revisions: [] }),
137
+ ]);
138
+
139
+ // Act
140
+ const report = store.recover();
141
+
142
+ // Assert
143
+ expect(report.recovered).toEqual(["ses_empty"]);
144
+ expect(store.get("ses_empty")?.history).toBeUndefined();
145
+ });
146
+
147
+ test("a record that already carries a history keeps it untouched", () => {
148
+ // Arrange
149
+ const withTree = createTestThreadRecord("ses_tree", "2026-09-01T00:00:00.000Z", {
150
+ history: {
151
+ entries: [
152
+ {
153
+ id: "root",
154
+ parentId: null,
155
+ type: "revision",
156
+ by: "agent",
157
+ content: "# ses_tree",
158
+ createdAt: "2026-09-01T00:00:00.000Z",
159
+ },
160
+ ],
161
+ tips: { main: "root", ramble: "root" },
162
+ branch: "ramble",
163
+ labels: { root: "start" },
164
+ },
165
+ });
166
+ const store = harness.open([withTree]);
167
+
168
+ // Act
169
+ store.recover();
170
+
171
+ // Assert
172
+ expect(store.get("ses_tree")?.history).toEqual(withTree.history);
173
+ });
174
+
175
+ test("a record that fails validation is skipped and reported, never dropped from the report", () => {
176
+ // Arrange
177
+ const store = harness.open([
178
+ createTestThreadRecord("ses_good", "2026-09-01T00:00:00.000Z"),
179
+ { schemaVersion: SCHEMA_VERSION, id: "ses_bad" },
180
+ ]);
181
+
182
+ // Act
183
+ const report = store.recover();
184
+
185
+ // Assert
186
+ expect(report.recovered).toEqual(["ses_good"]);
187
+ expect(report.skipped).toHaveLength(1);
188
+ expect(report.skipped[0]!.error).toContain("invalid record");
189
+ expect(store.get("ses_bad")).toBeUndefined();
190
+ });
191
+ });
192
+ }
@@ -0,0 +1,141 @@
1
+ import * as v from "valibot";
2
+ import type { ThreadAgentState } from "@cueloop/schema";
3
+
4
+ const AgentAnchorSchema = v.object({
5
+ quote: v.pipe(v.string(), v.minLength(1), v.maxLength(32_768)),
6
+ prefix: v.string(),
7
+ suffix: v.string(),
8
+ blockIndex: v.number(),
9
+ start: v.number(),
10
+ end: v.number(),
11
+ endBlockIndex: v.optional(v.number()),
12
+ });
13
+
14
+ /** Validate agent comments at the socket and persisted transcript boundaries. */
15
+ export const AgentCommentSchema = v.object({
16
+ id: v.string(),
17
+ messageId: v.string(),
18
+ anchor: AgentAnchorSchema,
19
+ body: v.pipe(v.string(), v.minLength(1), v.maxLength(32_768)),
20
+ sent: v.boolean(),
21
+ replyTo: v.optional(v.string()),
22
+ author: v.optional(v.string()),
23
+ });
24
+
25
+ /** Validate persisted agent transcripts independently of submitted artifacts. */
26
+ const StoredAgentSchema = v.object({
27
+ threadId: v.string(),
28
+ fxSessionId: v.optional(v.string()),
29
+ harness: v.optional(
30
+ v.object({ id: v.string(), label: v.string(), sessionId: v.optional(v.string()) }),
31
+ ),
32
+ phase: v.variant("kind", [
33
+ v.object({ kind: v.literal("idle") }),
34
+ v.object({ kind: v.literal("running") }),
35
+ v.object({ kind: v.literal("offline") }),
36
+ v.object({ kind: v.literal("failed"), error: v.string() }),
37
+ v.object({
38
+ kind: v.literal("permission"),
39
+ permission: v.object({
40
+ id: v.string(),
41
+ title: v.string(),
42
+ options: v.array(
43
+ v.object({
44
+ optionId: v.string(),
45
+ name: v.string(),
46
+ kind: v.string(),
47
+ }),
48
+ ),
49
+ }),
50
+ }),
51
+ ]),
52
+ messages: v.array(
53
+ v.object({
54
+ id: v.string(),
55
+ role: v.picklist(["user", "agent"]),
56
+ text: v.string(),
57
+ complete: v.boolean(),
58
+ revision: v.number(),
59
+ submissionId: v.optional(v.string()),
60
+ }),
61
+ ),
62
+ tools: v.array(
63
+ v.object({
64
+ id: v.string(),
65
+ turnId: v.string(),
66
+ title: v.string(),
67
+ kind: v.string(),
68
+ status: v.picklist(["pending", "in_progress", "completed", "failed", "cancelled"]),
69
+ output: v.string(),
70
+ locations: v.array(v.object({ path: v.string(), line: v.optional(v.number()) })),
71
+ }),
72
+ ),
73
+ comments: v.array(AgentCommentSchema),
74
+ submissions: v.optional(
75
+ v.array(
76
+ v.object({
77
+ id: v.string(),
78
+ commentId: v.optional(v.string()),
79
+ messageId: v.optional(v.string()),
80
+ prompt: v.string(),
81
+ harnessPrompt: v.optional(v.string()),
82
+ attemptId: v.optional(v.string()),
83
+ quote: v.optional(v.string()),
84
+ context: v.optional(v.string()),
85
+ status: v.picklist(["queued", "running", "completed", "failed"]),
86
+ cancelled: v.optional(v.boolean()),
87
+ }),
88
+ ),
89
+ ),
90
+ promptOperations: v.optional(
91
+ v.array(
92
+ v.object({
93
+ operationId: v.string(),
94
+ fingerprint: v.string(),
95
+ result: v.array(v.string()),
96
+ outcome: v.optional(v.picklist(["completed", "failed", "cancelled"])),
97
+ }),
98
+ ),
99
+ ),
100
+ continuation: v.optional(v.string()),
101
+ handoff: v.optional(
102
+ v.intersect([
103
+ v.object({
104
+ target: v.string(),
105
+ requestedTarget: v.optional(v.string()),
106
+ operationId: v.string(),
107
+ prompt: v.optional(v.string()),
108
+ source: v.optional(
109
+ v.object({ id: v.string(), label: v.string(), sessionId: v.optional(v.string()) }),
110
+ ),
111
+ }),
112
+ v.variant("status", [
113
+ v.object({ status: v.literal("queued") }),
114
+ v.object({ status: v.literal("summarizing") }),
115
+ v.object({ status: v.literal("prepared"), text: v.string() }),
116
+ ]),
117
+ ]),
118
+ ),
119
+ configOptions: v.optional(
120
+ v.array(
121
+ v.object({
122
+ id: v.string(),
123
+ name: v.string(),
124
+ category: v.optional(v.string()),
125
+ currentValue: v.string(),
126
+ options: v.array(v.object({ value: v.string(), name: v.string() })),
127
+ }),
128
+ ),
129
+ ),
130
+ });
131
+
132
+ /** Read the first fx prototype's records without discarding their session identity. */
133
+ export const ThreadAgentSchema: v.GenericSchema<ThreadAgentState> = v.pipe(
134
+ StoredAgentSchema,
135
+ v.transform(({ fxSessionId, ...state }) => ({
136
+ ...state,
137
+ harness:
138
+ state.harness ??
139
+ (fxSessionId ? { id: "fx", label: "fx", sessionId: fxSessionId } : undefined),
140
+ })),
141
+ );