@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 +2 -2
- package/src/api.test.ts +3 -3
- package/src/api.ts +45 -52
- package/src/capabilities.ts +1 -0
- package/src/client.ts +82 -79
- package/src/herdr-pane.test.ts +2 -2
- package/src/herdr-pane.ts +2 -2
- package/src/herdr-tab-store.ts +1 -1
- package/src/retention.test.ts +6 -6
- package/src/retention.ts +2 -2
- package/src/review.test.ts +2 -2
- package/src/review.ts +5 -5
- package/src/server.ts +5 -0
- package/src/share-blob.ts +6 -6
- package/src/store.conformance.test.ts +9 -9
- package/src/store.jsonl.test.ts +20 -20
- package/src/store.ts +35 -24
- package/src/testing/store-conformance.ts +18 -18
- package/src/validate.test.ts +11 -11
- package/src/validate.ts +17 -6
package/src/client.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
Artifact,
|
|
12
12
|
DiffFileStatus,
|
|
13
13
|
HunkRejection,
|
|
14
|
-
|
|
14
|
+
Thread,
|
|
15
15
|
VerdictKind,
|
|
16
16
|
WorkspaceKey,
|
|
17
17
|
} from "@cueloop/schema";
|
|
@@ -29,7 +29,7 @@ import type { SharedMerge } from "./api";
|
|
|
29
29
|
|
|
30
30
|
export type { SharedMerge } from "./api";
|
|
31
31
|
import { cueloopHome, ownerTokenPath, socketPath } from "./paths";
|
|
32
|
-
import { Params,
|
|
32
|
+
import { Params, ThreadRecordSchema, DiffFileContentsSchema } from "./validate";
|
|
33
33
|
import type { WorkingTreeDiff } from "./working-tree";
|
|
34
34
|
import { DAEMON_VERSION } from "./version";
|
|
35
35
|
|
|
@@ -66,25 +66,31 @@ const HerdrTabResultSchema = v.nullable(v.object({ tabId: v.string(), paneId: v.
|
|
|
66
66
|
export interface SessionClient {
|
|
67
67
|
onEvent(listener: (event: EventFrame) => void): () => void;
|
|
68
68
|
subscribe(): Promise<void>;
|
|
69
|
-
sessionGet(id: string): Promise<
|
|
70
|
-
sessionList(filter?: { status?: "pending" | "resolved" }): Promise<
|
|
69
|
+
sessionGet(id: string): Promise<Thread>;
|
|
70
|
+
sessionList(filter?: { status?: "pending" | "resolved" }): Promise<Thread[]>;
|
|
71
|
+
/** Add a comment; the primary annotate method. `sessionAnnotate` is the retained alias. */
|
|
72
|
+
sessionComment(
|
|
73
|
+
id: string,
|
|
74
|
+
annotation: Omit<Annotation, "createdAt">,
|
|
75
|
+
authorName?: string,
|
|
76
|
+
): Promise<Thread>;
|
|
71
77
|
sessionAnnotate(
|
|
72
78
|
id: string,
|
|
73
79
|
annotation: Omit<Annotation, "createdAt">,
|
|
74
80
|
authorName?: string,
|
|
75
|
-
): Promise<
|
|
81
|
+
): Promise<Thread>;
|
|
76
82
|
/** Remove a comment; a non-owner connection removes only the comments of the author it is bound to. */
|
|
77
|
-
sessionRemoveAnnotation(id: string, annotationId: string): Promise<
|
|
78
|
-
sessionSetWorkingCopy(id: string, workingCopy: string | undefined): Promise<
|
|
83
|
+
sessionRemoveAnnotation(id: string, annotationId: string): Promise<Thread>;
|
|
84
|
+
sessionSetWorkingCopy(id: string, workingCopy: string | undefined): Promise<Thread>;
|
|
79
85
|
/** Cut the `blockIndex`-th block of the working copy. */
|
|
80
|
-
sessionCutBlock(id: string, blockIndex: number): Promise<
|
|
86
|
+
sessionCutBlock(id: string, blockIndex: number): Promise<Thread>;
|
|
81
87
|
/** Re-insert the `baseBlockIndex`-th block of the submitted revision before `line` (default: the end). */
|
|
82
|
-
sessionRestoreBlock(id: string, baseBlockIndex: number, line?: number): Promise<
|
|
88
|
+
sessionRestoreBlock(id: string, baseBlockIndex: number, line?: number): Promise<Thread>;
|
|
83
89
|
/** Replace a diff review's reject decisions; the working copy follows. */
|
|
84
|
-
sessionCurate(id: string, rejections: HunkRejection[]): Promise<
|
|
85
|
-
sessionSetViewed(id: string, viewedPaths: string[]): Promise<
|
|
90
|
+
sessionCurate(id: string, rejections: HunkRejection[]): Promise<Thread>;
|
|
91
|
+
sessionSetViewed(id: string, viewedPaths: string[]): Promise<Thread>;
|
|
86
92
|
/** Rename a session's display title; an empty title restores the derived default. */
|
|
87
|
-
sessionSetTitle(id: string, title: string): Promise<
|
|
93
|
+
sessionSetTitle(id: string, title: string): Promise<Thread>;
|
|
88
94
|
/** Tracked, repo-relative file paths for the session's workspace; a client with no local repo (a share) omits it. */
|
|
89
95
|
projectFiles?(sessionId: string): Promise<string[]>;
|
|
90
96
|
/** UTF-8 contents of a repo-relative file, or null when it cannot be read safely; omitted by a client with no local repo. */
|
|
@@ -98,32 +104,27 @@ export interface SessionClient {
|
|
|
98
104
|
/** The live working-tree diff (patch plus per-file contents) at `cwd`. */
|
|
99
105
|
repoDiff?(cwd: string): Promise<WorkingTreeDiff>;
|
|
100
106
|
/** Find-or-create the per-repo workbench thread for `cwd`, so a bare launch's first comment persists. */
|
|
101
|
-
sessionWorkbench?(cwd: string): Promise<
|
|
107
|
+
sessionWorkbench?(cwd: string): Promise<Thread>;
|
|
102
108
|
/** Move a branch's tip (the current one, or `branch` after switching to it) back to an entry on its path; a summary records the abandoned segment. */
|
|
103
|
-
sessionNavigate(
|
|
104
|
-
id: string,
|
|
105
|
-
entryId: string,
|
|
106
|
-
summary?: string,
|
|
107
|
-
branch?: string,
|
|
108
|
-
): Promise<ReviewSession>;
|
|
109
|
+
sessionNavigate(id: string, entryId: string, summary?: string, branch?: string): Promise<Thread>;
|
|
109
110
|
/** Start a branch at the current tip and switch to it. */
|
|
110
|
-
sessionBranch(id: string, name: string): Promise<
|
|
111
|
-
sessionSwitch(id: string, branch: string): Promise<
|
|
111
|
+
sessionBranch(id: string, name: string): Promise<Thread>;
|
|
112
|
+
sessionSwitch(id: string, branch: string): Promise<Thread>;
|
|
112
113
|
/** Name the current tip as a checkpoint. */
|
|
113
|
-
sessionLabel(id: string, label: string): Promise<
|
|
114
|
+
sessionLabel(id: string, label: string): Promise<Thread>;
|
|
114
115
|
/** Copy the current path into a new session; returns the fork. */
|
|
115
|
-
sessionFork(id: string): Promise<
|
|
116
|
-
sessionSetShareId(id: string, shareId: string): Promise<
|
|
117
|
-
sessionMergeShared(id: string, incoming: SharedMerge): Promise<
|
|
116
|
+
sessionFork(id: string): Promise<Thread>;
|
|
117
|
+
sessionSetShareId(id: string, shareId: string): Promise<Thread>;
|
|
118
|
+
sessionMergeShared(id: string, incoming: SharedMerge): Promise<Thread>;
|
|
118
119
|
sessionDelete(id: string): Promise<void>;
|
|
119
120
|
/** Record the caller's own identity name (collaborator self-naming on a share). */
|
|
120
|
-
sessionSetSelfName(id: string, name: string): Promise<
|
|
121
|
+
sessionSetSelfName(id: string, name: string): Promise<Thread>;
|
|
121
122
|
sessionResolve(
|
|
122
123
|
id: string,
|
|
123
124
|
verdictKind: VerdictKind,
|
|
124
125
|
summary: string,
|
|
125
126
|
actionBodies?: Record<string, string>,
|
|
126
|
-
): Promise<
|
|
127
|
+
): Promise<Thread>;
|
|
127
128
|
close(): void;
|
|
128
129
|
}
|
|
129
130
|
|
|
@@ -374,84 +375,86 @@ export class DaemonClient implements SessionClient {
|
|
|
374
375
|
subscribe(): Promise<void> {
|
|
375
376
|
return this.request("events.subscribe", {}, EmptyResultSchema).then(() => undefined);
|
|
376
377
|
}
|
|
377
|
-
sessionCreate(workspace: WorkspaceKey, artifact: Artifact): Promise<
|
|
378
|
-
return this.request("session.create", { workspace, artifact },
|
|
378
|
+
sessionCreate(workspace: WorkspaceKey, artifact: Artifact): Promise<Thread> {
|
|
379
|
+
return this.request("session.create", { workspace, artifact }, ThreadRecordSchema);
|
|
379
380
|
}
|
|
380
|
-
sessionGet(id: string): Promise<
|
|
381
|
-
return this.request("session.get", { id },
|
|
381
|
+
sessionGet(id: string): Promise<Thread> {
|
|
382
|
+
return this.request("session.get", { id }, ThreadRecordSchema);
|
|
382
383
|
}
|
|
383
|
-
sessionList(filter?: { status?: "pending" | "resolved" }): Promise<
|
|
384
|
-
return this.request("session.list", { filter }, v.array(
|
|
384
|
+
sessionList(filter?: { status?: "pending" | "resolved" }): Promise<Thread[]> {
|
|
385
|
+
return this.request("session.list", { filter }, v.array(ThreadRecordSchema));
|
|
385
386
|
}
|
|
386
387
|
/** Long-poll; null = still pending after timeoutMs (re-poll to collect). */
|
|
387
|
-
sessionWait(id: string, timeoutMs: number): Promise<
|
|
388
|
+
sessionWait(id: string, timeoutMs: number): Promise<Thread | null> {
|
|
388
389
|
return this.request(
|
|
389
390
|
"session.wait",
|
|
390
391
|
{ id, timeoutMs },
|
|
391
|
-
v.nullable(
|
|
392
|
+
v.nullable(ThreadRecordSchema),
|
|
392
393
|
timeoutMs + 10_000,
|
|
393
394
|
);
|
|
394
395
|
}
|
|
396
|
+
sessionComment(
|
|
397
|
+
id: string,
|
|
398
|
+
annotation: Omit<Annotation, "createdAt">,
|
|
399
|
+
authorName?: string,
|
|
400
|
+
): Promise<Thread> {
|
|
401
|
+
return this.request("session.comment", { id, annotation, authorName }, ThreadRecordSchema);
|
|
402
|
+
}
|
|
395
403
|
sessionAnnotate(
|
|
396
404
|
id: string,
|
|
397
405
|
annotation: Omit<Annotation, "createdAt">,
|
|
398
406
|
authorName?: string,
|
|
399
|
-
): Promise<
|
|
400
|
-
return this.request("session.annotate", { id, annotation, authorName },
|
|
407
|
+
): Promise<Thread> {
|
|
408
|
+
return this.request("session.annotate", { id, annotation, authorName }, ThreadRecordSchema);
|
|
401
409
|
}
|
|
402
|
-
sessionRemoveAnnotation(id: string, annotationId: string): Promise<
|
|
403
|
-
return this.request("session.removeAnnotation", { id, annotationId },
|
|
410
|
+
sessionRemoveAnnotation(id: string, annotationId: string): Promise<Thread> {
|
|
411
|
+
return this.request("session.removeAnnotation", { id, annotationId }, ThreadRecordSchema);
|
|
404
412
|
}
|
|
405
413
|
/** Register a display name for a participant - a collaborator's or an agent's own, on a share or locally. */
|
|
406
|
-
sessionSetParticipantName(id: string, author: string, name: string): Promise<
|
|
407
|
-
return this.request("session.setParticipantName", { id, author, name },
|
|
414
|
+
sessionSetParticipantName(id: string, author: string, name: string): Promise<Thread> {
|
|
415
|
+
return this.request("session.setParticipantName", { id, author, name }, ThreadRecordSchema);
|
|
408
416
|
}
|
|
409
|
-
sessionSetWorkingCopy(id: string, workingCopy: string | undefined): Promise<
|
|
410
|
-
return this.request("session.setWorkingCopy", { id, workingCopy },
|
|
417
|
+
sessionSetWorkingCopy(id: string, workingCopy: string | undefined): Promise<Thread> {
|
|
418
|
+
return this.request("session.setWorkingCopy", { id, workingCopy }, ThreadRecordSchema);
|
|
411
419
|
}
|
|
412
|
-
sessionCutBlock(id: string, blockIndex: number): Promise<
|
|
413
|
-
return this.request("session.cutBlock", { id, blockIndex },
|
|
420
|
+
sessionCutBlock(id: string, blockIndex: number): Promise<Thread> {
|
|
421
|
+
return this.request("session.cutBlock", { id, blockIndex }, ThreadRecordSchema);
|
|
414
422
|
}
|
|
415
|
-
sessionNavigate(
|
|
416
|
-
id: string,
|
|
417
|
-
entryId: string,
|
|
418
|
-
summary?: string,
|
|
419
|
-
branch?: string,
|
|
420
|
-
): Promise<ReviewSession> {
|
|
423
|
+
sessionNavigate(id: string, entryId: string, summary?: string, branch?: string): Promise<Thread> {
|
|
421
424
|
const params: v.InferInput<(typeof Params)["session.navigate"]> = { id, entryId };
|
|
422
425
|
|
|
423
426
|
if (summary !== undefined) params.summary = summary;
|
|
424
427
|
if (branch !== undefined) params.branch = branch;
|
|
425
428
|
|
|
426
|
-
return this.request("session.navigate", params,
|
|
429
|
+
return this.request("session.navigate", params, ThreadRecordSchema);
|
|
427
430
|
}
|
|
428
|
-
sessionBranch(id: string, name: string): Promise<
|
|
429
|
-
return this.request("session.branch", { id, name },
|
|
431
|
+
sessionBranch(id: string, name: string): Promise<Thread> {
|
|
432
|
+
return this.request("session.branch", { id, name }, ThreadRecordSchema);
|
|
430
433
|
}
|
|
431
|
-
sessionSwitch(id: string, branch: string): Promise<
|
|
432
|
-
return this.request("session.switch", { id, branch },
|
|
434
|
+
sessionSwitch(id: string, branch: string): Promise<Thread> {
|
|
435
|
+
return this.request("session.switch", { id, branch }, ThreadRecordSchema);
|
|
433
436
|
}
|
|
434
|
-
sessionLabel(id: string, label: string): Promise<
|
|
435
|
-
return this.request("session.label", { id, label },
|
|
437
|
+
sessionLabel(id: string, label: string): Promise<Thread> {
|
|
438
|
+
return this.request("session.label", { id, label }, ThreadRecordSchema);
|
|
436
439
|
}
|
|
437
|
-
sessionFork(id: string): Promise<
|
|
438
|
-
return this.request("session.fork", { id },
|
|
440
|
+
sessionFork(id: string): Promise<Thread> {
|
|
441
|
+
return this.request("session.fork", { id }, ThreadRecordSchema);
|
|
439
442
|
}
|
|
440
|
-
sessionRestoreBlock(id: string, baseBlockIndex: number, line?: number): Promise<
|
|
443
|
+
sessionRestoreBlock(id: string, baseBlockIndex: number, line?: number): Promise<Thread> {
|
|
441
444
|
return this.request(
|
|
442
445
|
"session.restoreBlock",
|
|
443
446
|
line === undefined ? { id, baseBlockIndex } : { id, baseBlockIndex, line },
|
|
444
|
-
|
|
447
|
+
ThreadRecordSchema,
|
|
445
448
|
);
|
|
446
449
|
}
|
|
447
|
-
sessionCurate(id: string, rejections: HunkRejection[]): Promise<
|
|
448
|
-
return this.request("session.curate", { id, rejections },
|
|
450
|
+
sessionCurate(id: string, rejections: HunkRejection[]): Promise<Thread> {
|
|
451
|
+
return this.request("session.curate", { id, rejections }, ThreadRecordSchema);
|
|
449
452
|
}
|
|
450
|
-
sessionSetViewed(id: string, viewedPaths: string[]): Promise<
|
|
451
|
-
return this.request("session.setViewed", { id, viewedPaths },
|
|
453
|
+
sessionSetViewed(id: string, viewedPaths: string[]): Promise<Thread> {
|
|
454
|
+
return this.request("session.setViewed", { id, viewedPaths }, ThreadRecordSchema);
|
|
452
455
|
}
|
|
453
|
-
sessionSetTitle(id: string, title: string): Promise<
|
|
454
|
-
return this.request("session.setTitle", { id, title },
|
|
456
|
+
sessionSetTitle(id: string, title: string): Promise<Thread> {
|
|
457
|
+
return this.request("session.setTitle", { id, title }, ThreadRecordSchema);
|
|
455
458
|
}
|
|
456
459
|
projectFiles(sessionId: string): Promise<string[]> {
|
|
457
460
|
return this.request("session.projectFiles", { id: sessionId }, v.array(v.string()));
|
|
@@ -479,24 +482,24 @@ export class DaemonClient implements SessionClient {
|
|
|
479
482
|
v.object({ patch: v.string(), files: v.array(DiffFileContentsSchema) }),
|
|
480
483
|
);
|
|
481
484
|
}
|
|
482
|
-
sessionWorkbench(cwd: string): Promise<
|
|
483
|
-
return this.request("session.workbench", { cwd },
|
|
485
|
+
sessionWorkbench(cwd: string): Promise<Thread> {
|
|
486
|
+
return this.request("session.workbench", { cwd }, ThreadRecordSchema);
|
|
484
487
|
}
|
|
485
488
|
/** Re-capture a diff session's working tree; changed=true when the patch moved and an event fired. */
|
|
486
489
|
sessionRefreshDiff(id: string): Promise<{ changed: boolean }> {
|
|
487
490
|
return this.request("session.refreshDiff", { id }, RefreshDiffResultSchema);
|
|
488
491
|
}
|
|
489
|
-
sessionSetShareId(id: string, shareId: string): Promise<
|
|
490
|
-
return this.request("session.setShareId", { id, shareId },
|
|
492
|
+
sessionSetShareId(id: string, shareId: string): Promise<Thread> {
|
|
493
|
+
return this.request("session.setShareId", { id, shareId }, ThreadRecordSchema);
|
|
491
494
|
}
|
|
492
|
-
sessionMergeShared(id: string, incoming: SharedMerge): Promise<
|
|
493
|
-
return this.request("session.mergeShared", { id, ...incoming },
|
|
495
|
+
sessionMergeShared(id: string, incoming: SharedMerge): Promise<Thread> {
|
|
496
|
+
return this.request("session.mergeShared", { id, ...incoming }, ThreadRecordSchema);
|
|
494
497
|
}
|
|
495
498
|
sessionDelete(id: string): Promise<void> {
|
|
496
499
|
return this.request("session.delete", { id }, EmptyResultSchema).then(() => undefined);
|
|
497
500
|
}
|
|
498
501
|
/** Local sessions have no collaborator self-name; the share client owns this. */
|
|
499
|
-
sessionSetSelfName(id: string, _name: string): Promise<
|
|
502
|
+
sessionSetSelfName(id: string, _name: string): Promise<Thread> {
|
|
500
503
|
return this.sessionGet(id);
|
|
501
504
|
}
|
|
502
505
|
sessionResolve(
|
|
@@ -504,22 +507,22 @@ export class DaemonClient implements SessionClient {
|
|
|
504
507
|
verdictKind: VerdictKind,
|
|
505
508
|
summary: string,
|
|
506
509
|
actionBodies?: Record<string, string>,
|
|
507
|
-
): Promise<
|
|
510
|
+
): Promise<Thread> {
|
|
508
511
|
return this.request(
|
|
509
512
|
"session.resolve",
|
|
510
513
|
{ id, verdictKind, summary, actionBodies },
|
|
511
|
-
|
|
514
|
+
ThreadRecordSchema,
|
|
512
515
|
);
|
|
513
516
|
}
|
|
514
517
|
sessionSubmitRevision(
|
|
515
518
|
id: string,
|
|
516
519
|
content: string,
|
|
517
520
|
addressedAnnotationIds: string[] = [],
|
|
518
|
-
): Promise<
|
|
521
|
+
): Promise<Thread> {
|
|
519
522
|
return this.request(
|
|
520
523
|
"session.submitRevision",
|
|
521
524
|
{ id, content, addressedAnnotationIds },
|
|
522
|
-
|
|
525
|
+
ThreadRecordSchema,
|
|
523
526
|
);
|
|
524
527
|
}
|
|
525
528
|
/** herdr adapter scratch: the tab opened for a review; local-only, off the SessionClient contract. */
|
package/src/herdr-pane.test.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { afterAll, describe, expect, test } from "bun:test";
|
|
|
4
4
|
import { chmodSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { join } from "node:path";
|
|
7
|
-
import type {
|
|
7
|
+
import type { Thread } from "@cueloop/schema";
|
|
8
8
|
import { openHerdrPane, openHerdrPaneForReview } from "./herdr-pane";
|
|
9
9
|
import type { HerdrTabPersistence } from "./herdr-pane";
|
|
10
10
|
import type { HerdrTabHandle } from "./herdr-tab-store";
|
|
@@ -58,7 +58,7 @@ function fakePersistence(initial: HerdrTabHandle | null = null) {
|
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
function newSession(overrides: Partial<
|
|
61
|
+
function newSession(overrides: Partial<Thread> = {}): Thread {
|
|
62
62
|
return {
|
|
63
63
|
schemaVersion: "1",
|
|
64
64
|
id: "ses_new1",
|
package/src/herdr-pane.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* and never blocks review creation; each spawn is bounded by a timeout.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { detectHerdr, type HerdrEnv, type
|
|
9
|
+
import { detectHerdr, type HerdrEnv, type Thread } from "@cueloop/schema";
|
|
10
10
|
import * as v from "valibot";
|
|
11
11
|
import type { HerdrTabHandle } from "./herdr-tab-store";
|
|
12
12
|
|
|
@@ -117,7 +117,7 @@ function focusHerdrTab(binPath: string, tabId: string): void {
|
|
|
117
117
|
* its handle. No-op outside herdr.
|
|
118
118
|
*/
|
|
119
119
|
export async function openHerdrPaneForReview(
|
|
120
|
-
session:
|
|
120
|
+
session: Thread,
|
|
121
121
|
persistence: HerdrTabPersistence,
|
|
122
122
|
env: HerdrEnv = process.env,
|
|
123
123
|
): Promise<void> {
|
package/src/herdr-tab-store.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* herdr adapter scratch, kept out of the core session record: the tab cueloop
|
|
3
3
|
* opened to render each review, keyed by session id. One JSON map, atomic write.
|
|
4
|
-
* The core `
|
|
4
|
+
* The core `Thread` never mentions herdr; this side-store is the whole
|
|
5
5
|
* leak boundary.
|
|
6
6
|
*/
|
|
7
7
|
|
package/src/retention.test.ts
CHANGED
|
@@ -4,8 +4,8 @@ import { describe, expect, test } from "bun:test";
|
|
|
4
4
|
import { mkdirSync, mkdtempSync, utimesSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { join } from "node:path";
|
|
7
|
-
import type {
|
|
8
|
-
import {
|
|
7
|
+
import type { Thread, SessionStatus } from "@cueloop/schema";
|
|
8
|
+
import { ThreadStore } from "./store";
|
|
9
9
|
import { reportsDir } from "./paths";
|
|
10
10
|
import {
|
|
11
11
|
DEFAULT_CLEANUP_PERIOD_DAYS,
|
|
@@ -28,7 +28,7 @@ function daysAgo(days: number): string {
|
|
|
28
28
|
return new Date(NOW_MS - days * DAY_MS).toISOString();
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
function session(id: string, createdAt: string, status: SessionStatus = "resolved"):
|
|
31
|
+
function session(id: string, createdAt: string, status: SessionStatus = "resolved"): Thread {
|
|
32
32
|
return {
|
|
33
33
|
schemaVersion: "1",
|
|
34
34
|
id,
|
|
@@ -112,7 +112,7 @@ describe("pruneExpiredSessions", () => {
|
|
|
112
112
|
test("deletes resolved sessions past the window and keeps recent ones", () => {
|
|
113
113
|
// Arrange
|
|
114
114
|
const home = tempDir("cueloop-retention-home-");
|
|
115
|
-
const store = new
|
|
115
|
+
const store = new ThreadStore(home);
|
|
116
116
|
|
|
117
117
|
store.upsert(session("ses_old", daysAgo(40)));
|
|
118
118
|
store.upsert(session("ses_new", daysAgo(2)));
|
|
@@ -129,7 +129,7 @@ describe("pruneExpiredSessions", () => {
|
|
|
129
129
|
test("never deletes an active pending session however old", () => {
|
|
130
130
|
// Arrange
|
|
131
131
|
const home = tempDir("cueloop-retention-home-");
|
|
132
|
-
const store = new
|
|
132
|
+
const store = new ThreadStore(home);
|
|
133
133
|
|
|
134
134
|
store.upsert(session("ses_active", daysAgo(400), "pending"));
|
|
135
135
|
|
|
@@ -144,7 +144,7 @@ describe("pruneExpiredSessions", () => {
|
|
|
144
144
|
test("a period of zero prunes nothing", () => {
|
|
145
145
|
// Arrange
|
|
146
146
|
const home = tempDir("cueloop-retention-home-");
|
|
147
|
-
const store = new
|
|
147
|
+
const store = new ThreadStore(home);
|
|
148
148
|
|
|
149
149
|
store.upsert(session("ses_old", daysAgo(999)));
|
|
150
150
|
|
package/src/retention.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { readdirSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import * as v from "valibot";
|
|
5
|
-
import type {
|
|
5
|
+
import type { ThreadStore } from "./store";
|
|
6
6
|
|
|
7
7
|
export const DEFAULT_CLEANUP_PERIOD_DAYS = 30;
|
|
8
8
|
export const LATEST_REPORT_FILENAME = "report.md";
|
|
@@ -68,7 +68,7 @@ export function isExpired(createdAt: string, periodDays: number, nowMs: number):
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
export function pruneExpiredSessions(
|
|
71
|
-
store:
|
|
71
|
+
store: ThreadStore,
|
|
72
72
|
periodDays: number,
|
|
73
73
|
nowMs: number,
|
|
74
74
|
): string[] {
|
package/src/review.test.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
|
4
4
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { join } from "node:path";
|
|
7
|
-
import type {
|
|
7
|
+
import type { Thread } from "@cueloop/schema";
|
|
8
8
|
import { DaemonServer } from "./server";
|
|
9
9
|
import { DaemonClient } from "./client";
|
|
10
10
|
import { awaitResolve, openReview, resolveWorkspace } from "./review";
|
|
@@ -246,7 +246,7 @@ describe("awaitVerdict: chunked loop (the pi shape)", () => {
|
|
|
246
246
|
test("onProgress sees fresh sessions between chunks; the verdict ends the loop", async () => {
|
|
247
247
|
// Arrange
|
|
248
248
|
const review = await openReview(client, { type: "plan", content: PLAN, cwd: home });
|
|
249
|
-
const seen:
|
|
249
|
+
const seen: Thread[] = [];
|
|
250
250
|
const waiting = review.awaitVerdict({
|
|
251
251
|
timeoutMs: Infinity,
|
|
252
252
|
pollMs: 100,
|
package/src/review.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
newAnnotationId,
|
|
13
13
|
type ArtifactType,
|
|
14
14
|
type DiffFileContents,
|
|
15
|
-
type
|
|
15
|
+
type Thread,
|
|
16
16
|
type WorkspaceKey,
|
|
17
17
|
} from "@cueloop/schema";
|
|
18
18
|
import { verdictResponse } from "./api";
|
|
@@ -129,7 +129,7 @@ export interface AwaitVerdictOptions {
|
|
|
129
129
|
/** Chunk length for the poll loop; between chunks the session is re-read for progress. */
|
|
130
130
|
pollMs?: number;
|
|
131
131
|
/** Called with the fresh session after each chunk that is still pending. */
|
|
132
|
-
onProgress?: (session:
|
|
132
|
+
onProgress?: (session: Thread) => void;
|
|
133
133
|
signal?: AbortSignal;
|
|
134
134
|
}
|
|
135
135
|
|
|
@@ -137,13 +137,13 @@ export interface AwaitVerdictOptions {
|
|
|
137
137
|
export interface VerdictOutcome {
|
|
138
138
|
allow: boolean;
|
|
139
139
|
feedback: string;
|
|
140
|
-
session:
|
|
140
|
+
session: Thread;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
export class ReviewHandle {
|
|
144
144
|
constructor(
|
|
145
145
|
private readonly client: DaemonClient,
|
|
146
|
-
readonly session:
|
|
146
|
+
readonly session: Thread,
|
|
147
147
|
) {}
|
|
148
148
|
|
|
149
149
|
get id(): string {
|
|
@@ -184,7 +184,7 @@ export class ReviewHandle {
|
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
function outcome(session:
|
|
187
|
+
function outcome(session: Thread): VerdictOutcome {
|
|
188
188
|
return { ...verdictResponse(session), session };
|
|
189
189
|
}
|
|
190
190
|
|
package/src/server.ts
CHANGED
|
@@ -308,6 +308,11 @@ export class DaemonServer {
|
|
|
308
308
|
|
|
309
309
|
return this.core.sessionAnnotate(params.id, params.annotation, params.authorName);
|
|
310
310
|
},
|
|
311
|
+
"session.comment": (_connection, request) => {
|
|
312
|
+
const params = parseParams("session.comment", request.params);
|
|
313
|
+
|
|
314
|
+
return this.core.sessionAnnotate(params.id, params.annotation, params.authorName);
|
|
315
|
+
},
|
|
311
316
|
"session.removeAnnotation": (connection, request) => {
|
|
312
317
|
const params = parseParams("session.removeAnnotation", request.params);
|
|
313
318
|
|
package/src/share-blob.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The share-blob wire format: how a
|
|
2
|
+
* The share-blob wire format: how a Thread travels from `cueloop share`
|
|
3
3
|
* to the gateway. The planner's CLI packs (this is the only thing it can do -
|
|
4
4
|
* it holds no encryption key); the gateway unpacks, then encrypts for R2.
|
|
5
5
|
*
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { gzipSync, gunzipSync } from "node:zlib";
|
|
13
|
-
import type {
|
|
14
|
-
import {
|
|
13
|
+
import type { Thread } from "@cueloop/schema";
|
|
14
|
+
import { validateThreadRecord } from "./validate";
|
|
15
15
|
|
|
16
16
|
/** Decompressed ceiling for one shared session (ADR 0004's payload cap). */
|
|
17
17
|
export const MAX_BLOB_BYTES = 1024 * 1024;
|
|
@@ -24,12 +24,12 @@ export const DEFAULT_SHARE_HOST = "cueloop.dev";
|
|
|
24
24
|
export const DEFAULT_SHARE_PORT = 22;
|
|
25
25
|
|
|
26
26
|
/** Serialise + compress a session for upload. */
|
|
27
|
-
export function packSessionBlob(session:
|
|
27
|
+
export function packSessionBlob(session: Thread): Buffer {
|
|
28
28
|
return gzipSync(Buffer.from(JSON.stringify(session), "utf8"));
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/** Decompress + validate an uploaded blob, or throw a precise reason. */
|
|
32
|
-
export function unpackSessionBlob(bytes: Uint8Array):
|
|
32
|
+
export function unpackSessionBlob(bytes: Uint8Array): Thread {
|
|
33
33
|
let json: string;
|
|
34
34
|
|
|
35
35
|
try {
|
|
@@ -47,7 +47,7 @@ export function unpackSessionBlob(bytes: Uint8Array): ReviewSession {
|
|
|
47
47
|
} catch {
|
|
48
48
|
throw new Error("blob is not valid JSON");
|
|
49
49
|
}
|
|
50
|
-
const parsed =
|
|
50
|
+
const parsed = validateThreadRecord(raw);
|
|
51
51
|
|
|
52
52
|
if (!parsed.ok) throw new Error(`blob is not a valid session: ${parsed.error}`);
|
|
53
53
|
|
|
@@ -2,16 +2,16 @@ import { mkdtempSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import * as v from "valibot";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import { MemoryThreadStore, ThreadStore, type ThreadRepository } from "./store";
|
|
6
6
|
import { sessionsDir } from "./paths";
|
|
7
|
-
import {
|
|
7
|
+
import { runThreadStoreConformance } from "./testing/store-conformance";
|
|
8
8
|
|
|
9
|
-
const homes = new WeakMap<
|
|
9
|
+
const homes = new WeakMap<ThreadRepository, string>();
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
runThreadStoreConformance("file store", {
|
|
12
12
|
open: (records) => {
|
|
13
13
|
const home = mkdtempSync(join(tmpdir(), "cueloop-store-"));
|
|
14
|
-
const store = new
|
|
14
|
+
const store = new ThreadStore(home);
|
|
15
15
|
|
|
16
16
|
// records land on disk the way an earlier daemon would have written them;
|
|
17
17
|
// the file name comes from the record's id when it has one
|
|
@@ -26,7 +26,7 @@ runSessionStoreConformance("file store", {
|
|
|
26
26
|
return store;
|
|
27
27
|
},
|
|
28
28
|
restart: (store) => {
|
|
29
|
-
const reopened = new
|
|
29
|
+
const reopened = new ThreadStore(homes.get(store)!);
|
|
30
30
|
|
|
31
31
|
homes.set(reopened, homes.get(store)!);
|
|
32
32
|
|
|
@@ -34,8 +34,8 @@ runSessionStoreConformance("file store", {
|
|
|
34
34
|
},
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
open: (records) => new
|
|
37
|
+
runThreadStoreConformance("memory store", {
|
|
38
|
+
open: (records) => new MemoryThreadStore(records),
|
|
39
39
|
// nothing persists past the instance: a restart recovers what it was seeded with
|
|
40
|
-
restart: (store) => new
|
|
40
|
+
restart: (store) => new MemoryThreadStore(store.list()),
|
|
41
41
|
});
|