@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.
- package/README.md +7 -0
- package/SDK.md +102 -0
- package/package.json +43 -22
- package/src/agent-tools.ts +98 -0
- package/src/agent-turn.test.ts +24 -0
- package/src/agent-turn.ts +34 -0
- package/src/api.test.ts +1081 -65
- package/src/api.ts +1425 -89
- package/src/capabilities.test.ts +57 -0
- package/src/capabilities.ts +95 -0
- package/src/client-errors.ts +24 -0
- package/src/client-reliability.test.ts +189 -0
- package/src/client.test.ts +37 -0
- package/src/client.ts +820 -94
- package/src/curate.ts +109 -0
- package/src/daemon-sdk-effect.ts +113 -0
- package/src/daemon-sdk.test.ts +306 -0
- package/src/daemon-sdk.ts +397 -0
- package/src/daemon-version.test.ts +142 -0
- package/src/diff-hot-reload.test.ts +419 -0
- package/src/diff-watcher.test.ts +117 -0
- package/src/diff-watcher.ts +326 -0
- package/src/examples/daemon-sdk.ts +55 -0
- package/src/gh.test.ts +59 -0
- package/src/gh.ts +74 -0
- package/src/ghostty-thread-surface-store.test.ts +49 -0
- package/src/ghostty-thread-surface-store.ts +101 -0
- package/src/ghostty-thread-surface.test.ts +398 -0
- package/src/ghostty-thread-surface.ts +267 -0
- package/src/harness-delivery.test.ts +269 -0
- package/src/harness-diagnostics.test.ts +26 -0
- package/src/harness-diagnostics.ts +30 -0
- package/src/harness-state-store.test.ts +93 -0
- package/src/harness-state-store.ts +213 -0
- package/src/herdr-split.test.ts +106 -0
- package/src/herdr-split.ts +85 -0
- package/src/herdr-thread-surface-store.test.ts +81 -0
- package/src/herdr-thread-surface-store.ts +66 -0
- package/src/herdr-thread-surface.test.ts +465 -0
- package/src/herdr-thread-surface.ts +378 -0
- package/src/index.ts +8 -9
- package/src/interruptible-wait.test.ts +127 -0
- package/src/interruptible-wait.ts +66 -0
- package/src/jj-working-tree.ts +131 -0
- package/src/main.ts +4 -1
- package/src/operation-receipts.test.ts +111 -0
- package/src/operation-receipts.ts +91 -0
- package/src/paths.ts +49 -0
- package/src/pr-poller.test.ts +102 -0
- package/src/pr-poller.ts +73 -0
- package/src/project-files.test.ts +134 -0
- package/src/project-files.ts +68 -0
- package/src/protocol.ts +78 -7
- package/src/retention.test.ts +191 -0
- package/src/retention.ts +123 -0
- package/src/sdk-identities.ts +35 -0
- package/src/server.test.ts +456 -42
- package/src/server.ts +601 -88
- package/src/share-blob.test.ts +119 -0
- package/src/share-blob.ts +57 -0
- package/src/single-instance.test.ts +162 -13
- package/src/store.conformance.test.ts +41 -0
- package/src/store.jsonl.test.ts +173 -0
- package/src/store.ts +267 -34
- package/src/testing/store-conformance.ts +192 -0
- package/src/thread-agent-validation.ts +119 -0
- package/src/thread-agent.test.ts +412 -0
- package/src/thread-agent.ts +713 -0
- package/src/thread-review.test.ts +483 -0
- package/src/thread-review.ts +382 -0
- package/src/thread-subscription.test.ts +174 -0
- package/src/thread-subscription.ts +129 -0
- package/src/thread-surface-config.test.ts +63 -0
- package/src/thread-surface-config.ts +61 -0
- package/src/validate.test.ts +279 -37
- package/src/validate.ts +418 -33
- package/src/vcs-source.test.ts +180 -0
- package/src/vcs-source.ts +229 -0
- package/src/version.ts +6 -0
- package/src/working-tree.ts +150 -0
- package/src/review.test.ts +0 -167
- package/src/review.ts +0 -174
package/src/validate.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Runtime validation of the wire boundary
|
|
2
|
+
* Runtime validation of the wire boundary. Everything arriving over the
|
|
3
3
|
* socket is untrusted JSON: adapters, the CLI, extensions, and any script a
|
|
4
4
|
* user writes all speak this protocol. Validating here means DaemonCore only
|
|
5
5
|
* ever sees well-formed input, and a malformed request gets a precise error
|
|
@@ -11,17 +11,36 @@
|
|
|
11
11
|
|
|
12
12
|
import * as v from "valibot";
|
|
13
13
|
import {
|
|
14
|
+
ARTIFACT_TYPES,
|
|
15
|
+
MESSAGE_OUTCOMES,
|
|
16
|
+
WORKFLOW_KINDS,
|
|
14
17
|
SCHEMA_VERSION,
|
|
15
18
|
type Anchor,
|
|
16
19
|
type Annotation,
|
|
20
|
+
type AnnotationTarget,
|
|
17
21
|
type Artifact,
|
|
18
22
|
type ArtifactMeta,
|
|
19
|
-
type
|
|
23
|
+
type DiffFileContents,
|
|
24
|
+
type Identity,
|
|
25
|
+
type Thread,
|
|
26
|
+
type TextCut,
|
|
27
|
+
type HunkRejection,
|
|
28
|
+
type HarnessBinding,
|
|
29
|
+
type Delivery,
|
|
30
|
+
type PendingDelivery,
|
|
20
31
|
type Revision,
|
|
21
|
-
|
|
32
|
+
REVIEW_SEVERITIES,
|
|
33
|
+
type ReviewComment,
|
|
34
|
+
type SessionHistory,
|
|
35
|
+
type ShareLink,
|
|
36
|
+
validateHistory,
|
|
37
|
+
type Message,
|
|
22
38
|
type WorkspaceKey,
|
|
39
|
+
applyTextCuts,
|
|
23
40
|
} from "@cueloop/schema";
|
|
24
41
|
import { DaemonError } from "./errors";
|
|
42
|
+
import type { Request } from "./protocol";
|
|
43
|
+
import { AgentCommentSchema } from "./thread-agent-validation";
|
|
25
44
|
|
|
26
45
|
/**
|
|
27
46
|
* Drift guard for every hand-mirrored shape below. v.object strips keys it
|
|
@@ -34,25 +53,69 @@ type EntriesOf<T> = { [K in keyof T]-?: v.GenericSchema<any, any> };
|
|
|
34
53
|
|
|
35
54
|
const NonEmpty = v.pipe(v.string(), v.minLength(1));
|
|
36
55
|
|
|
56
|
+
const TextCutSchema = v.object({
|
|
57
|
+
start: v.pipe(v.number(), v.integer(), v.minValue(0)),
|
|
58
|
+
end: v.pipe(v.number(), v.integer(), v.minValue(1)),
|
|
59
|
+
quote: NonEmpty,
|
|
60
|
+
} satisfies EntriesOf<TextCut>);
|
|
61
|
+
|
|
62
|
+
const TextCutsSchema = v.pipe(
|
|
63
|
+
v.array(TextCutSchema),
|
|
64
|
+
v.check(
|
|
65
|
+
(cuts) =>
|
|
66
|
+
cuts.every(
|
|
67
|
+
(cut, index) =>
|
|
68
|
+
cut.end - cut.start === cut.quote.length &&
|
|
69
|
+
(index === 0 || cuts[index - 1]!.end <= cut.start),
|
|
70
|
+
),
|
|
71
|
+
"text Cuts must be ordered, non-overlapping, and match their quote lengths",
|
|
72
|
+
),
|
|
73
|
+
);
|
|
74
|
+
|
|
37
75
|
export const WorkspaceSchema = v.object({
|
|
38
76
|
repoRoot: NonEmpty,
|
|
39
77
|
branch: NonEmpty,
|
|
78
|
+
rootCommit: v.optional(NonEmpty),
|
|
79
|
+
remote: v.optional(NonEmpty),
|
|
40
80
|
} satisfies EntriesOf<WorkspaceKey>);
|
|
41
81
|
|
|
42
82
|
export const ArtifactMetaSchema = v.object({
|
|
83
|
+
workflow: v.optional(v.picklist(WORKFLOW_KINDS)),
|
|
43
84
|
cwd: v.optional(v.string()),
|
|
85
|
+
vcs: v.optional(NonEmpty),
|
|
86
|
+
vcsChangeId: v.optional(NonEmpty),
|
|
87
|
+
vcsRevisionId: v.optional(NonEmpty),
|
|
44
88
|
agent: v.optional(v.string()),
|
|
45
89
|
agentSessionId: v.optional(v.string()),
|
|
46
90
|
planPath: v.optional(v.string()),
|
|
91
|
+
prototypePath: v.optional(v.string()),
|
|
47
92
|
pr: v.optional(v.string()),
|
|
93
|
+
prBrief: v.optional(v.string()),
|
|
94
|
+
prBaseSha: v.optional(NonEmpty),
|
|
95
|
+
prHeadSha: v.optional(NonEmpty),
|
|
96
|
+
prRefreshBaseSha: v.optional(NonEmpty),
|
|
97
|
+
prRefreshHeadSha: v.optional(NonEmpty),
|
|
98
|
+
prUrl: v.optional(NonEmpty),
|
|
48
99
|
herdrPane: v.optional(v.string()),
|
|
49
100
|
title: v.optional(v.string()),
|
|
101
|
+
workbench: v.optional(v.boolean()),
|
|
102
|
+
snapshot: v.optional(v.boolean()),
|
|
50
103
|
} satisfies EntriesOf<ArtifactMeta>);
|
|
51
104
|
|
|
105
|
+
export const DiffFileContentsSchema = v.object({
|
|
106
|
+
path: NonEmpty,
|
|
107
|
+
oldContents: v.string(),
|
|
108
|
+
newContents: v.string(),
|
|
109
|
+
status: v.picklist(["added", "modified", "deleted"]),
|
|
110
|
+
} satisfies EntriesOf<DiffFileContents>);
|
|
111
|
+
|
|
52
112
|
export const ArtifactSchema = v.object({
|
|
53
|
-
|
|
113
|
+
// Derived from the schema's runtime union: a new primitive extends the wire
|
|
114
|
+
// contract without touching this file.
|
|
115
|
+
type: v.picklist(ARTIFACT_TYPES),
|
|
54
116
|
content: v.string(),
|
|
55
117
|
meta: v.optional(ArtifactMetaSchema, {}),
|
|
118
|
+
files: v.optional(v.array(DiffFileContentsSchema)),
|
|
56
119
|
} satisfies EntriesOf<Artifact>);
|
|
57
120
|
|
|
58
121
|
export const AnchorSchema = v.object({
|
|
@@ -60,23 +123,115 @@ export const AnchorSchema = v.object({
|
|
|
60
123
|
prefix: v.optional(v.string(), ""),
|
|
61
124
|
suffix: v.optional(v.string(), ""),
|
|
62
125
|
blockIndex: v.optional(v.number()),
|
|
126
|
+
endBlockIndex: v.optional(v.number()),
|
|
63
127
|
start: v.optional(v.number()),
|
|
64
128
|
end: v.optional(v.number()),
|
|
129
|
+
selector: v.optional(v.string()),
|
|
65
130
|
} satisfies EntriesOf<Anchor>);
|
|
66
131
|
|
|
132
|
+
/** The surface a note was made on; absent means the reviewed artifact. */
|
|
133
|
+
export const AnnotationTargetSchema: v.GenericSchema<AnnotationTarget> = v.variant("kind", [
|
|
134
|
+
v.object({ kind: v.literal("artifact") }),
|
|
135
|
+
v.object({ kind: v.literal("file"), path: NonEmpty, rev: v.picklist(["worktree", "head"]) }),
|
|
136
|
+
v.object({ kind: v.literal("welcome") }),
|
|
137
|
+
]);
|
|
138
|
+
|
|
67
139
|
/** Wire annotations arrive without createdAt - the daemon stamps it. */
|
|
68
140
|
export const AnnotationSchema = v.object({
|
|
69
141
|
id: NonEmpty,
|
|
70
|
-
/** Open kind set
|
|
142
|
+
/** Open kind set: the built-in is comment. */
|
|
71
143
|
kind: NonEmpty,
|
|
72
144
|
anchor: AnchorSchema,
|
|
145
|
+
target: v.optional(AnnotationTargetSchema),
|
|
73
146
|
body: v.string(),
|
|
74
147
|
orphan: v.optional(v.boolean()),
|
|
148
|
+
author: v.optional(v.string()),
|
|
149
|
+
reviewComment: v.optional(
|
|
150
|
+
v.object({
|
|
151
|
+
severity: v.picklist(REVIEW_SEVERITIES),
|
|
152
|
+
title: NonEmpty,
|
|
153
|
+
path: NonEmpty,
|
|
154
|
+
line: v.pipe(v.number(), v.integer(), v.minValue(1)),
|
|
155
|
+
startLine: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1))),
|
|
156
|
+
startAnchor: v.optional(AnchorSchema),
|
|
157
|
+
side: v.picklist(["LEFT", "RIGHT"]),
|
|
158
|
+
suggestion: v.optional(v.string()),
|
|
159
|
+
prompt: v.optional(v.string()),
|
|
160
|
+
} satisfies EntriesOf<ReviewComment>),
|
|
161
|
+
),
|
|
162
|
+
replyTo: v.optional(NonEmpty),
|
|
163
|
+
resolution: v.optional(
|
|
164
|
+
v.object({
|
|
165
|
+
revision: v.number(),
|
|
166
|
+
source: v.picklist(["agent", "drift"]),
|
|
167
|
+
}),
|
|
168
|
+
),
|
|
75
169
|
} satisfies EntriesOf<Omit<Annotation, "createdAt">>);
|
|
76
170
|
|
|
77
171
|
const SessionId = NonEmpty;
|
|
78
172
|
|
|
173
|
+
/** A stored annotation: the wire shape plus the daemon-stamped createdAt. */
|
|
174
|
+
export const FullAnnotationSchema = v.object({
|
|
175
|
+
...AnnotationSchema.entries,
|
|
176
|
+
createdAt: v.string(),
|
|
177
|
+
} satisfies EntriesOf<Annotation>);
|
|
178
|
+
|
|
179
|
+
export const IdentitySchema = v.object({
|
|
180
|
+
id: NonEmpty,
|
|
181
|
+
provider: v.picklist(["ssh", "github"]),
|
|
182
|
+
name: v.optional(v.string()),
|
|
183
|
+
handle: v.optional(v.string()),
|
|
184
|
+
} satisfies EntriesOf<Identity>);
|
|
185
|
+
|
|
186
|
+
export const ShareLinkSchema = v.object({
|
|
187
|
+
id: NonEmpty,
|
|
188
|
+
name: v.optional(v.string()),
|
|
189
|
+
requireAuth: v.boolean(),
|
|
190
|
+
allowlist: v.array(NonEmpty),
|
|
191
|
+
owner: v.optional(v.string()),
|
|
192
|
+
shareBranch: v.optional(v.string()),
|
|
193
|
+
} satisfies EntriesOf<ShareLink>);
|
|
194
|
+
|
|
195
|
+
export const HarnessBindingSchema = v.object({
|
|
196
|
+
id: NonEmpty,
|
|
197
|
+
threadId: NonEmpty,
|
|
198
|
+
harness: NonEmpty,
|
|
199
|
+
harnessSessionId: NonEmpty,
|
|
200
|
+
createdAt: NonEmpty,
|
|
201
|
+
approvedRetryMessageId: v.optional(NonEmpty),
|
|
202
|
+
} satisfies EntriesOf<HarnessBinding>);
|
|
203
|
+
|
|
204
|
+
export const DeliverySchema = v.object({
|
|
205
|
+
id: NonEmpty,
|
|
206
|
+
messageId: NonEmpty,
|
|
207
|
+
bindingId: NonEmpty,
|
|
208
|
+
status: v.picklist(["pending", "acknowledged"]),
|
|
209
|
+
createdAt: NonEmpty,
|
|
210
|
+
acknowledgedAt: v.optional(NonEmpty),
|
|
211
|
+
} satisfies EntriesOf<Delivery>);
|
|
212
|
+
|
|
79
213
|
export const Params = {
|
|
214
|
+
"agent.configure": v.object({
|
|
215
|
+
id: SessionId,
|
|
216
|
+
configId: v.optional(NonEmpty),
|
|
217
|
+
value: v.optional(NonEmpty),
|
|
218
|
+
}),
|
|
219
|
+
"agent.get": v.object({ id: SessionId }),
|
|
220
|
+
"agent.prompt": v.object({
|
|
221
|
+
id: SessionId,
|
|
222
|
+
text: v.pipe(v.string(), v.maxLength(65_536)),
|
|
223
|
+
context: v.optional(v.pipe(v.string(), v.maxLength(32_768))),
|
|
224
|
+
retry: v.optional(NonEmpty),
|
|
225
|
+
operationId: v.optional(v.pipe(NonEmpty, v.maxLength(128))),
|
|
226
|
+
}),
|
|
227
|
+
"agent.reply": v.object({
|
|
228
|
+
id: SessionId,
|
|
229
|
+
commentId: NonEmpty,
|
|
230
|
+
body: v.pipe(NonEmpty, v.maxLength(32_768)),
|
|
231
|
+
}),
|
|
232
|
+
"agent.cancel": v.object({ id: SessionId }),
|
|
233
|
+
"agent.comment": v.object({ id: SessionId, comment: AgentCommentSchema }),
|
|
234
|
+
"agent.permission": v.object({ id: SessionId, requestId: NonEmpty, optionId: NonEmpty }),
|
|
80
235
|
"session.create": v.object({ workspace: WorkspaceSchema, artifact: ArtifactSchema }),
|
|
81
236
|
"session.get": v.object({ id: SessionId }),
|
|
82
237
|
"session.list": v.object({
|
|
@@ -90,20 +245,142 @@ export const Params = {
|
|
|
90
245
|
"session.wait": v.object({
|
|
91
246
|
id: SessionId,
|
|
92
247
|
// clamped: a negative or absurd timeout is a client bug, not a daemon one
|
|
93
|
-
timeoutMs: v.optional(
|
|
248
|
+
timeoutMs: v.optional(
|
|
249
|
+
v.pipe(v.number(), v.minValue(0), v.maxValue(24 * 60 * 60 * 1000)),
|
|
250
|
+
60_000,
|
|
251
|
+
),
|
|
252
|
+
}),
|
|
253
|
+
"session.annotate": v.object({
|
|
254
|
+
id: SessionId,
|
|
255
|
+
annotation: AnnotationSchema,
|
|
256
|
+
authorName: v.optional(v.string()),
|
|
257
|
+
}),
|
|
258
|
+
// "session.comment" is the primary annotate method; "session.annotate" stays an accepted alias
|
|
259
|
+
"session.comment": v.object({
|
|
260
|
+
id: SessionId,
|
|
261
|
+
annotation: AnnotationSchema,
|
|
262
|
+
authorName: v.optional(v.string()),
|
|
94
263
|
}),
|
|
95
|
-
"session.annotate": v.object({ id: SessionId, annotation: AnnotationSchema }),
|
|
96
264
|
"session.removeAnnotation": v.object({ id: SessionId, annotationId: NonEmpty }),
|
|
97
|
-
"session.
|
|
98
|
-
"session.
|
|
265
|
+
"session.setParticipantName": v.object({ id: SessionId, author: NonEmpty, name: NonEmpty }),
|
|
266
|
+
"session.setWorkingCopy": v.object({
|
|
267
|
+
id: SessionId,
|
|
268
|
+
workingCopy: v.optional(v.string()),
|
|
269
|
+
textCuts: v.optional(TextCutsSchema),
|
|
270
|
+
}),
|
|
271
|
+
"session.cutBlock": v.object({
|
|
272
|
+
id: SessionId,
|
|
273
|
+
blockIndex: v.pipe(v.number(), v.integer(), v.minValue(0)),
|
|
274
|
+
}),
|
|
275
|
+
"session.restoreBlock": v.object({
|
|
276
|
+
id: SessionId,
|
|
277
|
+
baseBlockIndex: v.pipe(v.number(), v.integer(), v.minValue(0)),
|
|
278
|
+
line: v.optional(v.pipe(v.number(), v.integer(), v.minValue(0))),
|
|
279
|
+
}),
|
|
280
|
+
"session.curate": v.object({
|
|
281
|
+
id: SessionId,
|
|
282
|
+
rejections: v.array(
|
|
283
|
+
v.object({
|
|
284
|
+
path: NonEmpty,
|
|
285
|
+
hunkIndex: v.pipe(v.number(), v.integer(), v.minValue(0)),
|
|
286
|
+
changeIndex: v.optional(v.pipe(v.number(), v.integer(), v.minValue(0))),
|
|
287
|
+
} satisfies EntriesOf<HunkRejection>),
|
|
288
|
+
),
|
|
289
|
+
}),
|
|
290
|
+
"session.setViewed": v.object({ id: SessionId, viewedPaths: v.array(v.string()) }),
|
|
291
|
+
"session.setTitle": v.object({ id: SessionId, title: v.string() }),
|
|
292
|
+
"session.projectFiles": v.object({ id: SessionId }),
|
|
293
|
+
"session.fileContents": v.object({ id: SessionId, path: NonEmpty }),
|
|
294
|
+
"repo.files": v.object({ cwd: NonEmpty }),
|
|
295
|
+
"repo.fileContents": v.object({ cwd: NonEmpty, path: NonEmpty }),
|
|
296
|
+
"repo.changes": v.object({ cwd: NonEmpty }),
|
|
297
|
+
"repo.diff": v.object({ cwd: NonEmpty, vcs: v.optional(NonEmpty) }),
|
|
298
|
+
"session.workbench": v.object({ cwd: NonEmpty }),
|
|
299
|
+
"session.workbenchReview": v.object({ cwd: NonEmpty }),
|
|
300
|
+
"session.navigate": v.object({
|
|
301
|
+
id: SessionId,
|
|
302
|
+
entryId: NonEmpty,
|
|
303
|
+
summary: v.optional(v.string()),
|
|
304
|
+
// stand on this branch first, so a move on another branch is one request
|
|
305
|
+
branch: v.optional(NonEmpty),
|
|
306
|
+
}),
|
|
307
|
+
"session.branch": v.object({ id: SessionId, name: NonEmpty }),
|
|
308
|
+
"session.switch": v.object({ id: SessionId, branch: NonEmpty }),
|
|
309
|
+
"session.label": v.object({ id: SessionId, label: NonEmpty }),
|
|
310
|
+
"session.fork": v.object({ id: SessionId }),
|
|
311
|
+
"session.refreshDiff": v.object({ id: SessionId }),
|
|
312
|
+
"session.setShares": v.object({ id: SessionId, shares: v.array(ShareLinkSchema) }),
|
|
313
|
+
"session.delete": v.object({ id: SessionId }),
|
|
314
|
+
"session.mergeShared": v.object({
|
|
99
315
|
id: SessionId,
|
|
100
|
-
|
|
316
|
+
shareId: v.optional(NonEmpty),
|
|
317
|
+
annotations: v.array(FullAnnotationSchema),
|
|
318
|
+
participants: v.optional(v.array(IdentitySchema)),
|
|
319
|
+
// the removal entries a share recorded, carried by id so a merge applies each once
|
|
320
|
+
removals: v.optional(
|
|
321
|
+
v.array(v.object({ id: NonEmpty, annotationId: NonEmpty, createdAt: v.string() })),
|
|
322
|
+
),
|
|
323
|
+
}),
|
|
324
|
+
"session.sendMessage": v.object({
|
|
325
|
+
id: SessionId,
|
|
326
|
+
outcome: v.picklist(MESSAGE_OUTCOMES),
|
|
101
327
|
summary: v.optional(v.string(), ""),
|
|
328
|
+
actionBodies: v.optional(v.record(v.string(), v.string())),
|
|
329
|
+
operationId: v.optional(v.pipe(NonEmpty, v.maxLength(128))),
|
|
330
|
+
}),
|
|
331
|
+
"harness.bind": v.object({
|
|
332
|
+
threadId: SessionId,
|
|
333
|
+
harness: NonEmpty,
|
|
334
|
+
harnessSessionId: NonEmpty,
|
|
335
|
+
}),
|
|
336
|
+
"harness.getBinding": v.object({ bindingId: NonEmpty }),
|
|
337
|
+
"harness.bindingsForSession": v.object({ harness: NonEmpty, harnessSessionId: NonEmpty }),
|
|
338
|
+
"harness.consumeApprovedRetry": v.object({
|
|
339
|
+
bindingId: NonEmpty,
|
|
340
|
+
messageId: NonEmpty,
|
|
341
|
+
content: v.string(),
|
|
342
|
+
}),
|
|
343
|
+
"delivery.pending": v.object({ bindingId: NonEmpty }),
|
|
344
|
+
"delivery.acknowledge": v.object({ deliveryId: NonEmpty }),
|
|
345
|
+
"session.submitRevision": v.object({
|
|
346
|
+
id: SessionId,
|
|
347
|
+
content: v.string(),
|
|
348
|
+
files: v.optional(v.array(DiffFileContentsSchema)),
|
|
349
|
+
source: v.optional(
|
|
350
|
+
v.object({
|
|
351
|
+
vcs: NonEmpty,
|
|
352
|
+
changeId: v.optional(NonEmpty),
|
|
353
|
+
revisionId: v.optional(NonEmpty),
|
|
354
|
+
baseRevisionId: v.optional(NonEmpty),
|
|
355
|
+
}),
|
|
356
|
+
),
|
|
357
|
+
/** Annotation ids the agent acted on; each is marked addressed. */
|
|
358
|
+
addressedAnnotationIds: v.optional(v.array(NonEmpty), []),
|
|
102
359
|
}),
|
|
103
|
-
"session.submitRevision": v.object({ id: SessionId, content: v.string() }),
|
|
104
360
|
"events.subscribe": v.object({}),
|
|
105
361
|
"daemon.ping": v.object({}),
|
|
362
|
+
// the owner token proves ownership; without it a request for owner stays a collaborator
|
|
363
|
+
"daemon.hello": v.object({
|
|
364
|
+
role: v.picklist(["owner", "collaborator", "agent"]),
|
|
365
|
+
clientVersion: v.optional(v.string()),
|
|
366
|
+
token: v.optional(v.string()),
|
|
367
|
+
// the author a non-owner acts as, bound once for the connection
|
|
368
|
+
author: v.optional(NonEmpty),
|
|
369
|
+
}),
|
|
106
370
|
"daemon.shutdown": v.object({}),
|
|
371
|
+
// Herdr terminal handles stay outside canonical Thread records.
|
|
372
|
+
"herdr.getThreadSurface": v.object({ id: SessionId }),
|
|
373
|
+
"herdr.setThreadSurface": v.object({
|
|
374
|
+
id: SessionId,
|
|
375
|
+
tabId: NonEmpty,
|
|
376
|
+
paneId: NonEmpty,
|
|
377
|
+
mode: v.optional(v.picklist(["tab", "pane"])),
|
|
378
|
+
layout: v.optional(v.literal("pair")),
|
|
379
|
+
}),
|
|
380
|
+
"ghostty.getThreadSurface": v.object({ id: SessionId }),
|
|
381
|
+
"ghostty.setThreadSurface": v.object({ id: SessionId, terminalId: NonEmpty }),
|
|
382
|
+
"ghostty.claimThreadSurface": v.object({ id: SessionId }),
|
|
383
|
+
"ghostty.releaseThreadSurface": v.object({ id: SessionId }),
|
|
107
384
|
} as const;
|
|
108
385
|
|
|
109
386
|
export type MethodName = keyof typeof Params;
|
|
@@ -113,13 +390,22 @@ export function isKnownMethod(method: string): method is MethodName {
|
|
|
113
390
|
}
|
|
114
391
|
|
|
115
392
|
/** Validate params for a method, or throw a DaemonError the client can read. */
|
|
116
|
-
export function parseParams<M extends MethodName>(
|
|
393
|
+
export function parseParams<M extends MethodName>(
|
|
394
|
+
method: M,
|
|
395
|
+
params: Request["params"],
|
|
396
|
+
): v.InferOutput<(typeof Params)[M]> {
|
|
117
397
|
const result = v.safeParse(Params[method], params ?? {});
|
|
398
|
+
|
|
118
399
|
if (!result.success) {
|
|
119
400
|
const issue = result.issues[0]!;
|
|
120
|
-
const path = issue.path?.map((
|
|
121
|
-
|
|
401
|
+
const path = issue.path?.map((pathSegment) => String(pathSegment.key)).join(".") ?? "";
|
|
402
|
+
|
|
403
|
+
throw new DaemonError(
|
|
404
|
+
"invalid_params",
|
|
405
|
+
`${method}: ${path ? path + " - " : ""}${issue.message}`,
|
|
406
|
+
);
|
|
122
407
|
}
|
|
408
|
+
|
|
123
409
|
return result.output;
|
|
124
410
|
}
|
|
125
411
|
|
|
@@ -127,33 +413,132 @@ export const RevisionSchema = v.object({
|
|
|
127
413
|
revision: v.number(),
|
|
128
414
|
content: v.string(),
|
|
129
415
|
submittedAt: v.string(),
|
|
416
|
+
files: v.optional(v.array(DiffFileContentsSchema)),
|
|
417
|
+
source: v.optional(
|
|
418
|
+
v.object({
|
|
419
|
+
vcs: NonEmpty,
|
|
420
|
+
changeId: v.optional(NonEmpty),
|
|
421
|
+
revisionId: v.optional(NonEmpty),
|
|
422
|
+
baseRevisionId: v.optional(NonEmpty),
|
|
423
|
+
}),
|
|
424
|
+
),
|
|
130
425
|
} satisfies EntriesOf<Revision>);
|
|
131
426
|
|
|
132
|
-
export const
|
|
133
|
-
|
|
427
|
+
export const MessageSchema = v.object({
|
|
428
|
+
id: NonEmpty,
|
|
429
|
+
outcome: v.picklist(MESSAGE_OUTCOMES),
|
|
134
430
|
summary: v.string(),
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
431
|
+
body: v.string(),
|
|
432
|
+
annotations: v.optional(v.array(FullAnnotationSchema)),
|
|
433
|
+
sentAt: v.string(),
|
|
434
|
+
} satisfies EntriesOf<Message>);
|
|
138
435
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
436
|
+
export const PendingDeliverySchema = v.object({
|
|
437
|
+
delivery: DeliverySchema,
|
|
438
|
+
message: MessageSchema,
|
|
439
|
+
} satisfies EntriesOf<PendingDelivery>);
|
|
440
|
+
|
|
441
|
+
const EntryBaseEntries = {
|
|
142
442
|
id: NonEmpty,
|
|
143
|
-
|
|
144
|
-
artifact: ArtifactSchema,
|
|
145
|
-
revisions: v.array(RevisionSchema),
|
|
146
|
-
annotations: v.array(v.object({ ...AnnotationSchema.entries, createdAt: v.string() } satisfies EntriesOf<Annotation>)),
|
|
147
|
-
workingCopy: v.optional(v.string()),
|
|
148
|
-
verdict: v.nullable(VerdictSchema),
|
|
149
|
-
status: v.picklist(["pending", "resolved"]),
|
|
443
|
+
parentId: v.nullable(NonEmpty),
|
|
150
444
|
createdAt: v.string(),
|
|
151
|
-
}
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
/** One record of a session's history; the tree pointers plus the entry's own fields. */
|
|
448
|
+
export const SessionEntrySchema = v.variant("type", [
|
|
449
|
+
v.object({
|
|
450
|
+
...EntryBaseEntries,
|
|
451
|
+
type: v.literal("revision"),
|
|
452
|
+
by: v.picklist(["agent", "reviewer"]),
|
|
453
|
+
content: v.string(),
|
|
454
|
+
textCuts: v.optional(TextCutsSchema),
|
|
455
|
+
}),
|
|
456
|
+
v.object({ ...EntryBaseEntries, type: v.literal("comment"), annotationId: NonEmpty }),
|
|
457
|
+
v.object({ ...EntryBaseEntries, type: v.literal("comment-removed"), annotationId: NonEmpty }),
|
|
458
|
+
v.object({ ...EntryBaseEntries, type: v.literal("message"), message: MessageSchema }),
|
|
459
|
+
v.object({
|
|
460
|
+
...EntryBaseEntries,
|
|
461
|
+
type: v.literal("branch-summary"),
|
|
462
|
+
text: v.string(),
|
|
463
|
+
abandoned: v.array(NonEmpty),
|
|
464
|
+
}),
|
|
465
|
+
]);
|
|
466
|
+
|
|
467
|
+
export const SessionHistorySchema = v.pipe(
|
|
468
|
+
v.object({
|
|
469
|
+
entries: v.array(SessionEntrySchema),
|
|
470
|
+
tips: v.record(v.string(), NonEmpty),
|
|
471
|
+
branch: NonEmpty,
|
|
472
|
+
labels: v.record(v.string(), v.string()),
|
|
473
|
+
} satisfies EntriesOf<SessionHistory>),
|
|
474
|
+
// the shape is not enough: the tree itself must hold, or a walk hangs or rewires the path
|
|
475
|
+
v.rawCheck(({ dataset, addIssue }) => {
|
|
476
|
+
if (!dataset.typed) return;
|
|
477
|
+
const problem = validateHistory(dataset.value);
|
|
478
|
+
|
|
479
|
+
if (problem !== null) addIssue({ message: `history: ${problem}` });
|
|
480
|
+
}),
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
/** Persisted records are validated on recovery: a bad file is skipped, not fatal. */
|
|
484
|
+
export const ThreadRecordSchema = v.pipe(
|
|
485
|
+
v.strictObject({
|
|
486
|
+
schemaVersion: v.literal(SCHEMA_VERSION),
|
|
487
|
+
id: NonEmpty,
|
|
488
|
+
workspace: WorkspaceSchema,
|
|
489
|
+
artifact: ArtifactSchema,
|
|
490
|
+
revisions: v.array(RevisionSchema),
|
|
491
|
+
annotations: v.array(FullAnnotationSchema),
|
|
492
|
+
history: v.optional(SessionHistorySchema),
|
|
493
|
+
curation: v.optional(
|
|
494
|
+
v.array(
|
|
495
|
+
v.object({
|
|
496
|
+
path: NonEmpty,
|
|
497
|
+
hunkIndex: v.number(),
|
|
498
|
+
changeIndex: v.optional(v.number()),
|
|
499
|
+
} satisfies EntriesOf<HunkRejection>),
|
|
500
|
+
),
|
|
501
|
+
),
|
|
502
|
+
workingCopy: v.optional(v.string()),
|
|
503
|
+
textCuts: v.optional(TextCutsSchema),
|
|
504
|
+
viewedPaths: v.optional(v.array(v.string())),
|
|
505
|
+
messageOperations: v.optional(
|
|
506
|
+
v.array(v.object({ operationId: NonEmpty, fingerprint: NonEmpty, result: MessageSchema })),
|
|
507
|
+
),
|
|
508
|
+
message: v.nullable(MessageSchema),
|
|
509
|
+
status: v.picklist(["pending", "resolved"]),
|
|
510
|
+
createdAt: v.string(),
|
|
511
|
+
shelvedAnnotations: v.optional(v.array(FullAnnotationSchema)),
|
|
512
|
+
parentSessionId: v.optional(v.string()),
|
|
513
|
+
shares: v.optional(v.array(ShareLinkSchema)),
|
|
514
|
+
participants: v.optional(v.array(IdentitySchema)),
|
|
515
|
+
} satisfies EntriesOf<Thread>),
|
|
516
|
+
v.rawCheck(({ dataset, addIssue }) => {
|
|
517
|
+
if (!dataset.typed || !dataset.value.textCuts?.length) return;
|
|
518
|
+
const { artifact, textCuts, workingCopy } = dataset.value;
|
|
519
|
+
const matchesSource = textCuts.every(
|
|
520
|
+
(cut) => artifact.content.slice(cut.start, cut.end) === cut.quote,
|
|
521
|
+
);
|
|
522
|
+
|
|
523
|
+
if (!matchesSource || workingCopy !== applyTextCuts(artifact.content, textCuts)) {
|
|
524
|
+
addIssue({ message: "text Cuts do not match the submitted artifact and working copy" });
|
|
525
|
+
}
|
|
526
|
+
}),
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
export function validateThreadRecord(
|
|
530
|
+
raw: Parameters<typeof v.safeParse>[1],
|
|
531
|
+
): { ok: true; value: v.InferOutput<typeof ThreadRecordSchema> } | { ok: false; error: string } {
|
|
532
|
+
const result = v.safeParse(ThreadRecordSchema, raw);
|
|
152
533
|
|
|
153
|
-
export function validateSessionRecord(raw: unknown): { ok: true; value: v.InferOutput<typeof SessionRecordSchema> } | { ok: false; error: string } {
|
|
154
|
-
const result = v.safeParse(SessionRecordSchema, raw);
|
|
155
534
|
if (result.success) return { ok: true, value: result.output };
|
|
156
535
|
const issue = result.issues[0]!;
|
|
157
|
-
const path = issue.path?.map((
|
|
536
|
+
const path = issue.path?.map((pathSegment) => String(pathSegment.key)).join(".") ?? "";
|
|
537
|
+
|
|
158
538
|
return { ok: false, error: `${path ? path + ": " : ""}${issue.message}` };
|
|
159
539
|
}
|
|
540
|
+
|
|
541
|
+
/** @deprecated use ThreadRecordSchema */
|
|
542
|
+
export const SessionRecordSchema = ThreadRecordSchema;
|
|
543
|
+
/** @deprecated use validateThreadRecord */
|
|
544
|
+
export const validateSessionRecord = validateThreadRecord;
|