@cueloop/daemon 0.1.0-alpha.23 → 0.1.0-alpha.25

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cueloop/daemon",
3
- "version": "0.1.0-alpha.23",
3
+ "version": "0.1.0-alpha.25",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "exports": {
@@ -11,7 +11,7 @@
11
11
  "./share-blob": "./src/share-blob.ts"
12
12
  },
13
13
  "dependencies": {
14
- "@cueloop/schema": "0.1.0-alpha.23",
14
+ "@cueloop/schema": "0.1.0-alpha.25",
15
15
  "valibot": "^1.4.2"
16
16
  },
17
17
  "publishConfig": {
package/src/api.ts CHANGED
@@ -168,6 +168,24 @@ export class DaemonCore {
168
168
  return session;
169
169
  }
170
170
 
171
+ sessionSetShareId(id: string, shareId: string): ReviewSession {
172
+ const session = this.mutable(id);
173
+ session.shareId = shareId;
174
+ this.store.upsert(session);
175
+ this.emit("session.updated", id);
176
+ return session;
177
+ }
178
+
179
+ /** Union incoming annotations in by id; existing ids (the planner's) win. */
180
+ sessionMergeAnnotations(id: string, incoming: Annotation[]): ReviewSession {
181
+ const session = this.mutable(id);
182
+ const known = new Set(session.annotations.map((annotation) => annotation.id));
183
+ for (const annotation of incoming) if (!known.has(annotation.id)) session.annotations.push(annotation);
184
+ this.store.upsert(session);
185
+ this.emit("session.updated", id);
186
+ return session;
187
+ }
188
+
171
189
  sessionResolve(id: string, verdictKind: VerdictKind, summary: string): ReviewSession {
172
190
  const session = this.mutable(id);
173
191
  const verdict: Verdict = {
package/src/client.ts CHANGED
@@ -34,6 +34,8 @@ export interface SessionClient {
34
34
  sessionRemoveAnnotation(id: string, annotationId: string): Promise<ReviewSession>;
35
35
  sessionSetWorkingCopy(id: string, workingCopy: string | undefined): Promise<ReviewSession>;
36
36
  sessionSetViewed(id: string, viewedPaths: string[]): Promise<ReviewSession>;
37
+ sessionSetShareId(id: string, shareId: string): Promise<ReviewSession>;
38
+ sessionMergeAnnotations(id: string, annotations: Annotation[]): Promise<ReviewSession>;
37
39
  sessionResolve(id: string, verdictKind: VerdictKind, summary: string): Promise<ReviewSession>;
38
40
  close(): void;
39
41
  }
@@ -183,6 +185,12 @@ export class DaemonClient implements SessionClient {
183
185
  sessionSetViewed(id: string, viewedPaths: string[]): Promise<ReviewSession> {
184
186
  return this.request("session.setViewed", { id, viewedPaths });
185
187
  }
188
+ sessionSetShareId(id: string, shareId: string): Promise<ReviewSession> {
189
+ return this.request("session.setShareId", { id, shareId });
190
+ }
191
+ sessionMergeAnnotations(id: string, annotations: Annotation[]): Promise<ReviewSession> {
192
+ return this.request("session.mergeAnnotations", { id, annotations });
193
+ }
186
194
  sessionResolve(id: string, verdictKind: VerdictKind, summary: string): Promise<ReviewSession> {
187
195
  return this.request("session.resolve", { id, verdictKind, summary });
188
196
  }
@@ -121,6 +121,41 @@ describe("socket round-trip", () => {
121
121
  expect(got.artifact.meta.agent).toBe("claude-code");
122
122
  });
123
123
 
124
+ test("share-sync verbs round-trip: setShareId persists, mergeAnnotations unions by id", async () => {
125
+ // Arrange
126
+ const session = await client.sessionCreate(WS, PLAN);
127
+ const anchor = { quote: "Body text", prefix: "", suffix: "." };
128
+
129
+ // Act - stamp the share id, then a local note
130
+ await client.sessionSetShareId(session.id, "p_abc123xy");
131
+ await client.sessionAnnotate(session.id, { id: "a1", kind: "comment", anchor, body: "mine" });
132
+
133
+ // Assert - the share id came back over the wire
134
+ expect((await client.sessionGet(session.id)).shareId).toBe("p_abc123xy");
135
+
136
+ // Act - merge an update to the known id plus a new collaborator note
137
+ const merged = await client.sessionMergeAnnotations(session.id, [
138
+ { id: "a1", kind: "comment", anchor, body: "should not overwrite", createdAt: "2026-01-01T00:00:00.000Z" },
139
+ { id: "a2", kind: "comment", anchor, body: "theirs", author: "SHA256:mate", createdAt: "2026-01-01T00:00:00.000Z" },
140
+ ]);
141
+
142
+ // Assert - union by id: existing a1 kept as-is, a2 added with its author
143
+ expect(merged.annotations.map((annotation) => annotation.id).sort()).toEqual(["a1", "a2"]);
144
+ expect(merged.annotations.find((annotation) => annotation.id === "a1")!.body).toBe("mine");
145
+ expect(merged.annotations.find((annotation) => annotation.id === "a2")!.author).toBe("SHA256:mate");
146
+ });
147
+
148
+ test("mergeAnnotations validates each annotation at the socket boundary", async () => {
149
+ const session = await client.sessionCreate(WS, PLAN);
150
+ try {
151
+ // @ts-expect-error intentionally malformed annotation (empty id, missing fields)
152
+ await client.sessionMergeAnnotations(session.id, [{ id: "" }]);
153
+ throw new Error("should have thrown");
154
+ } catch (error) {
155
+ expect((error as DaemonClientError).code).toBe("invalid_params");
156
+ }
157
+ });
158
+
124
159
  test("two clients see the same state (thin-renderer model)", async () => {
125
160
  // Arrange
126
161
  const second = await DaemonClient.connect({ home });
package/src/server.ts CHANGED
@@ -245,6 +245,14 @@ export class DaemonServer {
245
245
  const params = parseParams("session.setViewed", request.params);
246
246
  return core.sessionSetViewed(params.id, params.viewedPaths);
247
247
  }
248
+ case "session.setShareId": {
249
+ const params = parseParams("session.setShareId", request.params);
250
+ return core.sessionSetShareId(params.id, params.shareId);
251
+ }
252
+ case "session.mergeAnnotations": {
253
+ const params = parseParams("session.mergeAnnotations", request.params);
254
+ return core.sessionMergeAnnotations(params.id, params.annotations);
255
+ }
248
256
  case "session.resolve": {
249
257
  const params = parseParams("session.resolve", request.params);
250
258
  return core.sessionResolve(params.id, params.verdictKind, params.summary);
@@ -191,6 +191,8 @@ describe("wire pins", () => {
191
191
  verdict: fullVerdict,
192
192
  status: "pending",
193
193
  createdAt: "now",
194
+ shareId: "p_abc123xy",
195
+ owner: "SHA256:owner",
194
196
  };
195
197
 
196
198
  test("schema key sets match the schema types", () => {
package/src/validate.ts CHANGED
@@ -83,6 +83,9 @@ export const AnnotationSchema = v.object({
83
83
 
84
84
  const SessionId = NonEmpty;
85
85
 
86
+ /** A stored annotation: the wire shape plus the daemon-stamped createdAt. */
87
+ export const FullAnnotationSchema = v.object({ ...AnnotationSchema.entries, createdAt: v.string() } satisfies EntriesOf<Annotation>);
88
+
86
89
  export const Params = {
87
90
  "session.create": v.object({ workspace: WorkspaceSchema, artifact: ArtifactSchema }),
88
91
  "session.get": v.object({ id: SessionId }),
@@ -103,6 +106,8 @@ export const Params = {
103
106
  "session.removeAnnotation": v.object({ id: SessionId, annotationId: NonEmpty }),
104
107
  "session.setWorkingCopy": v.object({ id: SessionId, workingCopy: v.optional(v.string()) }),
105
108
  "session.setViewed": v.object({ id: SessionId, viewedPaths: v.array(v.string()) }),
109
+ "session.setShareId": v.object({ id: SessionId, shareId: NonEmpty }),
110
+ "session.mergeAnnotations": v.object({ id: SessionId, annotations: v.array(FullAnnotationSchema) }),
106
111
  "session.resolve": v.object({
107
112
  id: SessionId,
108
113
  verdictKind: v.picklist(["comment", "approve", "request_changes"]),
@@ -156,12 +161,14 @@ export const SessionRecordSchema = v.object({
156
161
  workspace: WorkspaceSchema,
157
162
  artifact: ArtifactSchema,
158
163
  revisions: v.array(RevisionSchema),
159
- annotations: v.array(v.object({ ...AnnotationSchema.entries, createdAt: v.string() } satisfies EntriesOf<Annotation>)),
164
+ annotations: v.array(FullAnnotationSchema),
160
165
  workingCopy: v.optional(v.string()),
161
166
  viewedPaths: v.optional(v.array(v.string())),
162
167
  verdict: v.nullable(VerdictSchema),
163
168
  status: v.picklist(["pending", "resolved"]),
164
169
  createdAt: v.string(),
170
+ shareId: v.optional(v.string()),
171
+ owner: v.optional(v.string()),
165
172
  } satisfies EntriesOf<ReviewSession>);
166
173
 
167
174
  export function validateSessionRecord(raw: unknown): { ok: true; value: v.InferOutput<typeof SessionRecordSchema> } | { ok: false; error: string } {