@cueloop/daemon 0.1.0-alpha.69 → 0.1.0-alpha.70

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.69",
3
+ "version": "0.1.0-alpha.70",
4
4
  "description": "The cueloop session daemon and its client: socket protocol, atomic store, resumable waits",
5
5
  "homepage": "https://github.com/mmurakaru/cueloop#readme",
6
6
  "bugs": {
@@ -35,7 +35,7 @@
35
35
  "access": "public"
36
36
  },
37
37
  "dependencies": {
38
- "@cueloop/schema": "0.1.0-alpha.69",
38
+ "@cueloop/schema": "0.1.0-alpha.70",
39
39
  "@pierre/diffs": "^1.3.6",
40
40
  "effect": "^4.0.0-rc.115",
41
41
  "valibot": "^1.4.2"
package/src/api.test.ts CHANGED
@@ -3,7 +3,7 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { DaemonCore, verdictResponse } from "./api";
6
- import { SessionStore } from "./store";
6
+ import { ThreadStore } from "./store";
7
7
  import { derivePath, tipOf, type Artifact, type WorkspaceKey } from "@cueloop/schema";
8
8
  import { MAX_BLOB_BYTES, packSessionBlob, unpackSessionBlob } from "./share-blob";
9
9
 
@@ -420,7 +420,7 @@ describe("persistence and recovery", () => {
420
420
  // Arrange
421
421
  core.sessionCreate({ workspace: WS, artifact: PLAN });
422
422
  await Bun.write(join(home, "sessions", "broken.json"), "{ not json");
423
- const store = new SessionStore(home);
423
+ const store = new ThreadStore(home);
424
424
 
425
425
  // Act
426
426
  const report = store.recover();
@@ -558,7 +558,7 @@ describe("the session history records every write as an entry", () => {
558
558
  const legacy = { ...created };
559
559
 
560
560
  delete legacy.history;
561
- new SessionStore(home).upsert(legacy);
561
+ new ThreadStore(home).upsert(legacy);
562
562
 
563
563
  // Act
564
564
  const rebooted = new DaemonCore(home);
package/src/api.ts CHANGED
@@ -37,14 +37,14 @@ import {
37
37
  type NewEntry,
38
38
  type Artifact,
39
39
  type Identity,
40
- type ReviewSession,
40
+ type Thread,
41
41
  type SessionHistory,
42
42
  type Verdict,
43
43
  type VerdictKind,
44
44
  type WorkspaceKey,
45
45
  } from "@cueloop/schema";
46
46
  import { curateDiff } from "./curate";
47
- import { SessionStore, withHistory } from "./store";
47
+ import { ThreadStore, withHistory } from "./store";
48
48
  import { pruneExpiredSessions, resolveCleanupPeriodDays } from "./retention";
49
49
  import { HerdrTabStore, type HerdrTabHandle } from "./herdr-tab-store";
50
50
  import { DiffWatcher } from "./diff-watcher";
@@ -78,9 +78,9 @@ export interface DaemonEvent {
78
78
  type EventListener = (event: DaemonEvent) => void;
79
79
 
80
80
  export class DaemonCore {
81
- readonly store: SessionStore;
81
+ readonly store: ThreadStore;
82
82
  readonly herdrTabs: HerdrTabStore;
83
- private waiters = new Map<string, ((session: ReviewSession) => void)[]>();
83
+ private waiters = new Map<string, ((session: Thread) => void)[]>();
84
84
  private listeners = new Set<EventListener>();
85
85
  private seq = 0;
86
86
  /** Drives diff hot-reload: watches each live diff session's repo for working-tree changes. */
@@ -92,10 +92,10 @@ export class DaemonCore {
92
92
  */
93
93
  private readonly diffRefreshGenerations = new Map<string, number>();
94
94
  /** In-flight workbench creations keyed by project, so concurrent bare launches share one thread. */
95
- private readonly workbenchCreation = new Map<string, Promise<ReviewSession>>();
95
+ private readonly workbenchCreation = new Map<string, Promise<Thread>>();
96
96
 
97
97
  constructor(home: string) {
98
- this.store = new SessionStore(home);
98
+ this.store = new ThreadStore(home);
99
99
  this.store.recover();
100
100
  pruneExpiredSessions(this.store, resolveCleanupPeriodDays(), Date.now());
101
101
  this.herdrTabs = new HerdrTabStore(home);
@@ -136,9 +136,9 @@ export class DaemonCore {
136
136
  return this.store.list().some((session) => session.status === "pending");
137
137
  }
138
138
 
139
- sessionCreate(params: { workspace: WorkspaceKey; artifact: Artifact }): ReviewSession {
139
+ sessionCreate(params: { workspace: WorkspaceKey; artifact: Artifact }): Thread {
140
140
  const now = new Date().toISOString();
141
- const session: ReviewSession = {
141
+ const session: Thread = {
142
142
  schemaVersion: SCHEMA_VERSION,
143
143
  id: this.newSessionId(now),
144
144
  workspace: params.workspace,
@@ -163,7 +163,7 @@ export class DaemonCore {
163
163
  return `ses_${now.replace(/\D/g, "").slice(0, 14)}_${(++this.seq).toString(36)}${Math.random().toString(36).slice(2, 6)}`;
164
164
  }
165
165
 
166
- sessionGet(id: string): ReviewSession {
166
+ sessionGet(id: string): Thread {
167
167
  const session = this.store.get(id);
168
168
 
169
169
  if (!session) throw new DaemonError("not_found", `no session ${id}`);
@@ -174,7 +174,7 @@ export class DaemonCore {
174
174
  sessionList(filter?: {
175
175
  status?: "pending" | "resolved";
176
176
  workspace?: Partial<WorkspaceKey>;
177
- }): ReviewSession[] {
177
+ }): Thread[] {
178
178
  return this.store.list().filter((session) => {
179
179
  if (filter?.status && session.status !== filter.status) return false;
180
180
  if (filter?.workspace?.repoRoot && session.workspace.repoRoot !== filter.workspace.repoRoot)
@@ -191,7 +191,7 @@ export class DaemonCore {
191
191
  * otherwise parks until sessionResolve fires or timeoutMs elapses (null =
192
192
  * still pending - the caller re-polls later; the verdict is never lost).
193
193
  */
194
- sessionWait(id: string, timeoutMs: number): Promise<ReviewSession | null> {
194
+ sessionWait(id: string, timeoutMs: number): Promise<Thread | null> {
195
195
  const current = this.sessionGet(id);
196
196
 
197
197
  if (current.status === "resolved") return Promise.resolve(current);
@@ -207,7 +207,7 @@ export class DaemonCore {
207
207
  if (waiterIndex !== -1) list.splice(waiterIndex, 1);
208
208
  resolve(null);
209
209
  }, timeoutMs);
210
- const waiter = (session: ReviewSession) => {
210
+ const waiter = (session: Thread) => {
211
211
  if (done) return;
212
212
  done = true;
213
213
  clearTimeout(timer);
@@ -227,7 +227,7 @@ export class DaemonCore {
227
227
  id: string,
228
228
  annotation: Omit<Annotation, "createdAt">,
229
229
  authorName?: string,
230
- ): ReviewSession {
230
+ ): Thread {
231
231
  const session = this.mutable(id);
232
232
  // a welcome-playground note is ephemeral by design: never persisted, so never fed back
233
233
  if (annotationTarget(annotation).kind === "welcome") return session;
@@ -265,7 +265,7 @@ export class DaemonCore {
265
265
  * agent acting as that author and may remove only that author's comments;
266
266
  * the owner (no `onBehalfOf`) may remove any.
267
267
  */
268
- sessionRemoveAnnotation(id: string, annotationId: string, onBehalfOf?: string): ReviewSession {
268
+ sessionRemoveAnnotation(id: string, annotationId: string, onBehalfOf?: string): Thread {
269
269
  const session = this.mutable(id);
270
270
  const target = session.annotations.find((candidate) => candidate.id === annotationId);
271
271
 
@@ -292,7 +292,7 @@ export class DaemonCore {
292
292
  }
293
293
 
294
294
  /** Register a display name for a participant; how a collaborator or an agent names itself. */
295
- sessionSetParticipantName(id: string, author: string, name: string): ReviewSession {
295
+ sessionSetParticipantName(id: string, author: string, name: string): Thread {
296
296
  const session = this.mutable(id);
297
297
 
298
298
  session.participants = registerParticipant(session, author, name).participants;
@@ -303,7 +303,7 @@ export class DaemonCore {
303
303
  }
304
304
 
305
305
  /** The reviewer's working copy; undefined clears it (revert all edits). */
306
- sessionSetWorkingCopy(id: string, workingCopy: string | undefined): ReviewSession {
306
+ sessionSetWorkingCopy(id: string, workingCopy: string | undefined): Thread {
307
307
  const session = this.mutable(id);
308
308
  const entryId = this.applyWorkingCopy(session, workingCopy);
309
309
 
@@ -318,7 +318,7 @@ export class DaemonCore {
318
318
  * marks, so concurrent or stale clients converge instead of overwriting
319
319
  * each other. An empty array is the explicit reset.
320
320
  */
321
- sessionSetViewed(id: string, viewedPaths: string[]): ReviewSession {
321
+ sessionSetViewed(id: string, viewedPaths: string[]): Thread {
322
322
  const session = this.mutable(id);
323
323
 
324
324
  if (viewedPaths.length === 0) delete session.viewedPaths;
@@ -330,7 +330,7 @@ export class DaemonCore {
330
330
  }
331
331
 
332
332
  /** Rename a session's display title; an empty title clears it back to the derived default. */
333
- sessionSetTitle(id: string, title: string): ReviewSession {
333
+ sessionSetTitle(id: string, title: string): Thread {
334
334
  const session = this.mutable(id);
335
335
  const trimmed = title.trim();
336
336
 
@@ -383,7 +383,7 @@ export class DaemonCore {
383
383
  * or lazily creates it with the working-tree diff as its artifact, so a bare launch persists its
384
384
  * first comment without an agent submission. Owner-only.
385
385
  */
386
- async workbenchSession(cwd: string): Promise<ReviewSession> {
386
+ async workbenchSession(cwd: string): Promise<Thread> {
387
387
  const workspace = await resolveWorkspace(cwd);
388
388
  const key = workspace.rootCommit ?? "_standalone";
389
389
  // an open workbench is reused; a resolved one is immutable and would reject the note, so a fresh
@@ -422,7 +422,7 @@ export class DaemonCore {
422
422
  }
423
423
  }
424
424
 
425
- sessionSetShareId(id: string, shareId: string): ReviewSession {
425
+ sessionSetShareId(id: string, shareId: string): Thread {
426
426
  const session = this.mutable(id);
427
427
 
428
428
  session.shareId = shareId;
@@ -447,7 +447,7 @@ export class DaemonCore {
447
447
  * Cut one block of the reviewer's working copy - the `blockIndex`-th block
448
448
  * of the working text - so it serializes into the diff the agent receives.
449
449
  */
450
- sessionCutBlock(id: string, blockIndex: number): ReviewSession {
450
+ sessionCutBlock(id: string, blockIndex: number): Thread {
451
451
  const session = this.mutable(id);
452
452
  const working = session.workingCopy ?? session.artifact.content;
453
453
  const block = parseBlocks(working)[blockIndex];
@@ -467,7 +467,7 @@ export class DaemonCore {
467
467
  * revision - into the working copy before `line` (default: the end). A copy
468
468
  * that reads as the submitted revision again is dropped, not stored.
469
469
  */
470
- sessionRestoreBlock(id: string, baseBlockIndex: number, line?: number): ReviewSession {
470
+ sessionRestoreBlock(id: string, baseBlockIndex: number, line?: number): Thread {
471
471
  const session = this.mutable(id);
472
472
  const base = session.artifact.content;
473
473
  const block = parseBlocks(base)[baseBlockIndex];
@@ -500,7 +500,7 @@ export class DaemonCore {
500
500
  * patch they leave, or clears when nothing is rejected. Needs the full file
501
501
  * contents a working-tree diff carries; a PR diff cannot be curated.
502
502
  */
503
- sessionCurate(id: string, rejections: HunkRejection[]): ReviewSession {
503
+ sessionCurate(id: string, rejections: HunkRejection[]): Thread {
504
504
  const session = this.mutable(id);
505
505
 
506
506
  if (session.artifact.type !== "diff") {
@@ -530,7 +530,7 @@ export class DaemonCore {
530
530
  * on their own name). Everything lands on the branch the share follows, and
531
531
  * the record then shows its current path again, wherever the owner stands.
532
532
  */
533
- sessionMergeShared(id: string, incoming: SharedMerge): ReviewSession {
533
+ sessionMergeShared(id: string, incoming: SharedMerge): Thread {
534
534
  const session = this.mutable(id);
535
535
  const branch = this.shareBranchOf(session);
536
536
  const known = new Set(
@@ -583,7 +583,7 @@ export class DaemonCore {
583
583
  verdictKind: VerdictKind,
584
584
  summary: string,
585
585
  actionBodies?: Record<string, string>,
586
- ): ReviewSession {
586
+ ): Thread {
587
587
  const session = this.mutable(id);
588
588
  const verdict: Verdict = {
589
589
  kind: verdictKind,
@@ -626,7 +626,7 @@ export class DaemonCore {
626
626
  id: string,
627
627
  content: string,
628
628
  addressedAnnotationIds: string[] = [],
629
- ): ReviewSession {
629
+ ): Thread {
630
630
  const session = this.sessionGet(id);
631
631
  const now = new Date().toISOString();
632
632
  const revisionNumber = session.revisions.length + 1;
@@ -688,7 +688,7 @@ export class DaemonCore {
688
688
  * summary records them as a branch-summary entry at the target. The view
689
689
  * follows the path: on `main`, the agent's next revision lands there.
690
690
  */
691
- sessionNavigate(id: string, entryId: string, summary?: string, branch?: string): ReviewSession {
691
+ sessionNavigate(id: string, entryId: string, summary?: string, branch?: string): Thread {
692
692
  const session = this.mutable(id);
693
693
  const moved = this.tree(id, () => {
694
694
  const history = this.historyOf(session);
@@ -706,7 +706,7 @@ export class DaemonCore {
706
706
  }
707
707
 
708
708
  /** Start a branch at the current tip and switch to it; `main` stays where it is. */
709
- sessionBranch(id: string, name: string): ReviewSession {
709
+ sessionBranch(id: string, name: string): Thread {
710
710
  const session = this.mutable(id);
711
711
 
712
712
  this.refreshView(
@@ -718,7 +718,7 @@ export class DaemonCore {
718
718
  return session;
719
719
  }
720
720
 
721
- sessionSwitch(id: string, branch: string): ReviewSession {
721
+ sessionSwitch(id: string, branch: string): Thread {
722
722
  const session = this.mutable(id);
723
723
 
724
724
  this.refreshView(
@@ -731,7 +731,7 @@ export class DaemonCore {
731
731
  }
732
732
 
733
733
  /** Name the current tip as a checkpoint to navigate back to. */
734
- sessionLabel(id: string, label: string): ReviewSession {
734
+ sessionLabel(id: string, label: string): Thread {
735
735
  const session = this.mutable(id);
736
736
 
737
737
  session.history = labelTip(this.historyOf(session), label);
@@ -746,13 +746,13 @@ export class DaemonCore {
746
746
  * comments, labels, and participant names travel; verdicts, edits, and the
747
747
  * share do not. A resolved session can be forked.
748
748
  */
749
- sessionFork(id: string): ReviewSession {
749
+ sessionFork(id: string): Thread {
750
750
  const source = this.sessionGet(id);
751
751
  const history = this.tree(id, () => forkHistory(this.historyOf(source)));
752
752
  const now = new Date().toISOString();
753
753
  const known = [...source.annotations, ...(source.shelvedAnnotations ?? [])];
754
754
  const view = viewOfPath(history, known);
755
- const fork: ReviewSession = {
755
+ const fork: Thread = {
756
756
  schemaVersion: SCHEMA_VERSION,
757
757
  id: this.newSessionId(now),
758
758
  workspace: source.workspace,
@@ -832,12 +832,12 @@ export class DaemonCore {
832
832
  }
833
833
  }
834
834
 
835
- private watchIfDiffSession(session: ReviewSession): void {
835
+ private watchIfDiffSession(session: Thread): void {
836
836
  if (isLiveDiffSession(session))
837
837
  this.diffWatcher.trackDiffRepo(session.workspace.repoRoot, session.id);
838
838
  }
839
839
 
840
- private unwatchIfDiffSession(session: ReviewSession): void {
840
+ private unwatchIfDiffSession(session: Thread): void {
841
841
  if (session.artifact.type === "diff")
842
842
  this.diffWatcher.untrackDiffRepo(session.workspace.repoRoot, session.id);
843
843
  }
@@ -846,10 +846,7 @@ export class DaemonCore {
846
846
  * Set or clear the working copy and, when the reviewer's text changed, record
847
847
  * it as a reviewer revision on the current branch. Returns that entry's id.
848
848
  */
849
- private applyWorkingCopy(
850
- session: ReviewSession,
851
- workingCopy: string | undefined,
852
- ): string | undefined {
849
+ private applyWorkingCopy(session: Thread, workingCopy: string | undefined): string | undefined {
853
850
  const before = session.workingCopy ?? session.artifact.content;
854
851
  const next =
855
852
  workingCopy === undefined || workingCopy === session.artifact.content
@@ -871,7 +868,7 @@ export class DaemonCore {
871
868
  }
872
869
 
873
870
  /** The session's history; a record without a revision has none and cannot be moved through. */
874
- private historyOf(session: ReviewSession): SessionHistory {
871
+ private historyOf(session: Thread): SessionHistory {
875
872
  const history = withHistory(session).history;
876
873
 
877
874
  if (!history) throw new DaemonError("invalid_params", `session ${session.id} has no history`);
@@ -890,7 +887,7 @@ export class DaemonCore {
890
887
  }
891
888
 
892
889
  /** Store a moved history and make the record show its active path. */
893
- private refreshView(session: ReviewSession, history: SessionHistory): void {
890
+ private refreshView(session: Thread, history: SessionHistory): void {
894
891
  session.history = history;
895
892
  applyPathView(
896
893
  session,
@@ -900,7 +897,7 @@ export class DaemonCore {
900
897
  }
901
898
 
902
899
  /** Append an entry on the session's current branch; a session without a head has no history to extend. */
903
- private record(session: ReviewSession, entry: NewEntry): string | undefined {
900
+ private record(session: Thread, entry: NewEntry): string | undefined {
904
901
  const history = withHistory(session).history;
905
902
 
906
903
  if (!history) return undefined;
@@ -912,12 +909,12 @@ export class DaemonCore {
912
909
  }
913
910
 
914
911
  /** Append an entry on main, leaving the reviewer's current branch where it is. */
915
- private recordOnMain(session: ReviewSession, entry: NewEntry): string | undefined {
912
+ private recordOnMain(session: Thread, entry: NewEntry): string | undefined {
916
913
  return this.recordOn(session, MAIN_BRANCH, entry);
917
914
  }
918
915
 
919
916
  /** Append an entry on a named branch, leaving the reviewer's current branch where it is. */
920
- private recordOn(session: ReviewSession, branch: string, entry: NewEntry): string | undefined {
917
+ private recordOn(session: Thread, branch: string, entry: NewEntry): string | undefined {
921
918
  const history = withHistory(session).history;
922
919
 
923
920
  if (!history) return undefined;
@@ -929,13 +926,13 @@ export class DaemonCore {
929
926
  }
930
927
 
931
928
  /** The branch a share follows; a share made before branches existed follows main. */
932
- private shareBranchOf(session: ReviewSession): string {
929
+ private shareBranchOf(session: Thread): string {
933
930
  const branch = session.shareBranch ?? MAIN_BRANCH;
934
931
 
935
932
  return session.history?.tips[branch] === undefined ? MAIN_BRANCH : branch;
936
933
  }
937
934
 
938
- private mutable(id: string): ReviewSession {
935
+ private mutable(id: string): Thread {
939
936
  const session = this.sessionGet(id);
940
937
 
941
938
  if (session.status === "resolved")
@@ -953,11 +950,7 @@ export { DaemonError };
953
950
  * revision's text, and a comment addressed by a revision off the fork's path
954
951
  * is open again.
955
952
  */
956
- function forkedAnnotation(
957
- annotation: Annotation,
958
- source: ReviewSession,
959
- fork: ReviewSession,
960
- ): Annotation {
953
+ function forkedAnnotation(annotation: Annotation, source: Thread, fork: Thread): Annotation {
961
954
  const { resolution, ...open } = annotation;
962
955
 
963
956
  if (resolution === undefined) return { ...annotation };
@@ -972,12 +965,12 @@ function forkedAnnotation(
972
965
  }
973
966
 
974
967
  /** A pending diff session: the state that warrants hot-reload watching of its working tree. */
975
- function isLiveDiffSession(session: ReviewSession): boolean {
968
+ function isLiveDiffSession(session: Thread): boolean {
976
969
  return session.status === "pending" && session.artifact.type === "diff";
977
970
  }
978
971
 
979
972
  /** Convenience for adapters: map a resolved session to the agent contract. */
980
- export function verdictResponse(session: ReviewSession) {
973
+ export function verdictResponse(session: Thread) {
981
974
  if (!session.verdict) throw new DaemonError("pending", "session has no verdict");
982
975
 
983
976
  return { allow: verdictAllows(session.verdict.kind), feedback: session.verdict.feedback };
@@ -27,6 +27,7 @@ const PRIMITIVE_ROLES = {
27
27
  "session.list": "any",
28
28
  "session.wait": "any",
29
29
  "session.annotate": "any",
30
+ "session.comment": "any",
30
31
  // any role may call it; a non-owner must act on behalf of an author and stays scoped to it
31
32
  "session.removeAnnotation": "any",
32
33
  "session.setParticipantName": "any",