@cueloop/daemon 0.1.0-alpha.79 → 0.1.0-alpha.80
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 +4 -3
- package/src/api.test.ts +57 -38
- package/src/api.ts +138 -33
- package/src/capabilities.test.ts +3 -3
- package/src/capabilities.ts +6 -1
- package/src/client.ts +73 -18
- package/src/diff-hot-reload.test.ts +2 -2
- package/src/harness-delivery.test.ts +193 -0
- package/src/harness-state-store.test.ts +65 -0
- package/src/harness-state-store.ts +182 -0
- package/src/herdr-pane.test.ts +1 -1
- package/src/index.ts +1 -1
- package/src/paths.ts +5 -0
- package/src/retention.test.ts +10 -1
- package/src/retention.ts +2 -1
- package/src/server.test.ts +53 -7
- package/src/server.ts +34 -4
- package/src/single-instance.test.ts +34 -0
- package/src/testing/store-conformance.ts +1 -1
- package/src/{review.test.ts → thread-review.test.ts} +48 -48
- package/src/{review.ts → thread-review.ts} +91 -43
- package/src/validate.test.ts +16 -14
- package/src/validate.ts +53 -10
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The shared review core: one open/wait/
|
|
2
|
+
* The shared Thread review core: one open/wait/message path for every consumer -
|
|
3
3
|
* CLI commands and agent adapters. openReview resolves the workspace, shapes
|
|
4
|
-
* the artifact, and opens-or-revises by agentSessionId;
|
|
5
|
-
* wait contract onto the agent contract through
|
|
4
|
+
* the artifact, and opens-or-revises by agentSessionId; awaitMessage maps the
|
|
5
|
+
* wait contract onto the agent contract through messageResponse. An
|
|
6
6
|
* adapter keeps only two bespoke parts: parsing its host's event shape and
|
|
7
7
|
* serializing the decision in its host's contract.
|
|
8
8
|
*/
|
|
@@ -10,18 +10,20 @@
|
|
|
10
10
|
import {
|
|
11
11
|
isMarkdownArtifact,
|
|
12
12
|
newAnnotationId,
|
|
13
|
+
type Annotation,
|
|
14
|
+
type Artifact,
|
|
13
15
|
type ArtifactType,
|
|
14
16
|
type DiffFileContents,
|
|
15
17
|
type Thread,
|
|
16
18
|
type WorkspaceKey,
|
|
19
|
+
type WorkflowKind,
|
|
17
20
|
} from "@cueloop/schema";
|
|
18
|
-
import {
|
|
19
|
-
import type { DaemonClient } from "./client";
|
|
21
|
+
import { messageResponse } from "./api";
|
|
20
22
|
import { ABORTED, pollUntilResolved, raceAbort } from "./interruptible-wait";
|
|
21
23
|
|
|
22
|
-
// Adapters and CLI primitives reach the
|
|
24
|
+
// Adapters and CLI primitives reach the message mapping through this module too,
|
|
23
25
|
// so a session obtained outside a ReviewHandle maps the same way.
|
|
24
|
-
export {
|
|
26
|
+
export { messageResponse };
|
|
25
27
|
|
|
26
28
|
async function git(args: string[], cwd: string): Promise<string | null> {
|
|
27
29
|
try {
|
|
@@ -73,6 +75,8 @@ function firstHeading(markdown: string): string | undefined {
|
|
|
73
75
|
|
|
74
76
|
export interface OpenReviewOptions {
|
|
75
77
|
type: ArtifactType;
|
|
78
|
+
/** Workflow identity keeps plan/refine and diff/review Threads separate. */
|
|
79
|
+
workflow?: WorkflowKind;
|
|
76
80
|
content: string;
|
|
77
81
|
/** Workspace resolution root and meta.cwd; defaults to process.cwd(). */
|
|
78
82
|
cwd?: string;
|
|
@@ -107,9 +111,28 @@ export interface ReviewNote {
|
|
|
107
111
|
body: string;
|
|
108
112
|
}
|
|
109
113
|
|
|
114
|
+
/** The transport-neutral daemon operations used by the shared review lifecycle. */
|
|
115
|
+
export interface ThreadSessionClient {
|
|
116
|
+
sessionList(filter?: { status?: "pending" | "resolved" }): Promise<Thread[]>;
|
|
117
|
+
sessionCreate(workspace: WorkspaceKey, artifact: Artifact): Promise<Thread>;
|
|
118
|
+
sessionSubmitRevision(
|
|
119
|
+
id: string,
|
|
120
|
+
content: string,
|
|
121
|
+
addressedAnnotationIds?: string[],
|
|
122
|
+
files?: DiffFileContents[],
|
|
123
|
+
): Promise<Thread>;
|
|
124
|
+
sessionAnnotate(
|
|
125
|
+
id: string,
|
|
126
|
+
annotation: Omit<Annotation, "createdAt">,
|
|
127
|
+
authorName?: string,
|
|
128
|
+
): Promise<Thread>;
|
|
129
|
+
sessionGet(id: string): Promise<Thread>;
|
|
130
|
+
sessionWait(id: string, timeoutMs: number): Promise<Thread | null>;
|
|
131
|
+
}
|
|
132
|
+
|
|
110
133
|
/** Anchor a note at its file: quote = the path, no context selectors. */
|
|
111
134
|
async function attachNotes(
|
|
112
|
-
client:
|
|
135
|
+
client: ThreadSessionClient,
|
|
113
136
|
sessionId: string,
|
|
114
137
|
notes: ReviewNote[],
|
|
115
138
|
): Promise<void> {
|
|
@@ -123,7 +146,7 @@ async function attachNotes(
|
|
|
123
146
|
}
|
|
124
147
|
}
|
|
125
148
|
|
|
126
|
-
export interface
|
|
149
|
+
export interface AwaitMessageOptions {
|
|
127
150
|
/** Total wait budget; Infinity keeps polling until resolved or aborted. */
|
|
128
151
|
timeoutMs: number;
|
|
129
152
|
/** Chunk length for the poll loop; between chunks the session is re-read for progress. */
|
|
@@ -134,15 +157,15 @@ export interface AwaitVerdictOptions {
|
|
|
134
157
|
}
|
|
135
158
|
|
|
136
159
|
/** A resolved review mapped onto the agent contract, plus the full session. */
|
|
137
|
-
export interface
|
|
160
|
+
export interface MessageResult {
|
|
138
161
|
allow: boolean;
|
|
139
|
-
|
|
162
|
+
message: import("@cueloop/schema").Message;
|
|
140
163
|
session: Thread;
|
|
141
164
|
}
|
|
142
165
|
|
|
143
166
|
export class ReviewHandle {
|
|
144
167
|
constructor(
|
|
145
|
-
private readonly client:
|
|
168
|
+
private readonly client: Pick<ThreadSessionClient, "sessionWait" | "sessionGet">,
|
|
146
169
|
readonly session: Thread,
|
|
147
170
|
) {}
|
|
148
171
|
|
|
@@ -151,12 +174,12 @@ export class ReviewHandle {
|
|
|
151
174
|
}
|
|
152
175
|
|
|
153
176
|
/**
|
|
154
|
-
* Block on the
|
|
155
|
-
* aborted - the session stays open and the
|
|
156
|
-
* (
|
|
177
|
+
* Block on the message. "pending" means the budget ran out or the signal
|
|
178
|
+
* aborted - the session stays open and the message is collectable later
|
|
179
|
+
* (messages outlive waits). With only timeoutMs this is one long-poll;
|
|
157
180
|
* pollMs/onProgress/signal switch to the chunked loop.
|
|
158
181
|
*/
|
|
159
|
-
async
|
|
182
|
+
async awaitMessage(options: AwaitMessageOptions): Promise<MessageResult | "pending"> {
|
|
160
183
|
const { timeoutMs, pollMs, onProgress, signal } = options;
|
|
161
184
|
|
|
162
185
|
if (pollMs === undefined && onProgress === undefined && signal === undefined) {
|
|
@@ -174,18 +197,20 @@ export class ReviewHandle {
|
|
|
174
197
|
const resolved = await raceAbort(this.client.sessionWait(this.session.id, budget), signal);
|
|
175
198
|
|
|
176
199
|
if (resolved === ABORTED) return "pending";
|
|
200
|
+
|
|
177
201
|
if (resolved !== null) return outcome(resolved);
|
|
178
202
|
// Still pending after this chunk: re-read to surface reviewer progress.
|
|
179
203
|
const current = await raceAbort(this.client.sessionGet(this.session.id), signal);
|
|
180
204
|
|
|
181
205
|
if (current === ABORTED) return "pending";
|
|
206
|
+
|
|
182
207
|
onProgress?.(current);
|
|
183
208
|
}
|
|
184
209
|
}
|
|
185
210
|
}
|
|
186
211
|
|
|
187
|
-
function outcome(session: Thread):
|
|
188
|
-
return { ...
|
|
212
|
+
function outcome(session: Thread): MessageResult {
|
|
213
|
+
return { ...messageResponse(session), session };
|
|
189
214
|
}
|
|
190
215
|
|
|
191
216
|
export interface AwaitResolveOptions {
|
|
@@ -196,21 +221,21 @@ export interface AwaitResolveOptions {
|
|
|
196
221
|
}
|
|
197
222
|
|
|
198
223
|
/**
|
|
199
|
-
* Park until a thread resolves, then return the
|
|
200
|
-
* when the signal aborts first. Where ReviewHandle.
|
|
224
|
+
* Park until a thread resolves, then return the message outcome; null
|
|
225
|
+
* when the signal aborts first. Where ReviewHandle.awaitMessage needs the handle
|
|
201
226
|
* that opened the review, this needs only a session id - so a background waiter
|
|
202
227
|
* that woke on a session it did not open (a detached Claude Code / Codex waiter,
|
|
203
|
-
* or pi's session_start listener) can collect the same
|
|
228
|
+
* or pi's session_start listener) can collect the same message. This is the
|
|
204
229
|
* wake seam every non-blocking adapter builds on. Loops the daemon long-poll, so
|
|
205
|
-
* a
|
|
230
|
+
* a message that lands between chunks is never missed and a session already
|
|
206
231
|
* resolved returns on the first chunk. The held connection also keeps the daemon
|
|
207
232
|
* off its idle-exit path for the whole wait.
|
|
208
233
|
*/
|
|
209
234
|
export async function awaitResolve(
|
|
210
|
-
client:
|
|
235
|
+
client: Pick<ThreadSessionClient, "sessionWait">,
|
|
211
236
|
sessionId: string,
|
|
212
237
|
options: AwaitResolveOptions = {},
|
|
213
|
-
): Promise<
|
|
238
|
+
): Promise<MessageResult | null> {
|
|
214
239
|
const chunkMs = options.pollMs ?? 30_000;
|
|
215
240
|
const resolved = await pollUntilResolved(
|
|
216
241
|
() => client.sessionWait(sessionId, chunkMs),
|
|
@@ -220,40 +245,63 @@ export async function awaitResolve(
|
|
|
220
245
|
return resolved === null ? null : outcome(resolved);
|
|
221
246
|
}
|
|
222
247
|
|
|
248
|
+
/** Find the Thread already submitted by this agent in the same workspace and primitive. */
|
|
249
|
+
export async function findExistingReview(
|
|
250
|
+
client: ThreadSessionClient,
|
|
251
|
+
options: OpenReviewOptions,
|
|
252
|
+
): Promise<Thread | undefined> {
|
|
253
|
+
const cwd = options.cwd ?? process.cwd();
|
|
254
|
+
const workspace = options.workspace ?? (await resolveWorkspace(cwd));
|
|
255
|
+
|
|
256
|
+
if (options.agentSessionId === undefined) return undefined;
|
|
257
|
+
|
|
258
|
+
return (await client.sessionList()).find(
|
|
259
|
+
(candidate) =>
|
|
260
|
+
candidate.artifact.meta.agentSessionId === options.agentSessionId &&
|
|
261
|
+
candidate.artifact.meta.agent === options.agent &&
|
|
262
|
+
candidate.artifact.type === options.type &&
|
|
263
|
+
(candidate.artifact.meta.workflow ??
|
|
264
|
+
(candidate.artifact.meta.pr ? "review" : candidate.artifact.type)) ===
|
|
265
|
+
(options.workflow ?? (options.pr ? "review" : options.type)) &&
|
|
266
|
+
candidate.artifact.meta.pr === options.pr &&
|
|
267
|
+
candidate.workspace.repoRoot === workspace.repoRoot &&
|
|
268
|
+
candidate.workspace.branch === workspace.branch,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
223
272
|
/** Open a thread (or revise the agent session's existing one) and hand back the wait surface. */
|
|
224
273
|
export async function openReview(
|
|
225
|
-
client:
|
|
274
|
+
client: ThreadSessionClient,
|
|
226
275
|
options: OpenReviewOptions,
|
|
227
276
|
): Promise<ReviewHandle> {
|
|
228
277
|
const cwd = options.cwd ?? process.cwd();
|
|
229
278
|
const workspace = options.workspace ?? (await resolveWorkspace(cwd));
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
279
|
+
const existing = await findExistingReview(client, options);
|
|
280
|
+
|
|
281
|
+
if (existing !== undefined) {
|
|
282
|
+
// PR diffs have no full file contents. An absent local-diff capture clears
|
|
283
|
+
// old curatable files, while a PR revision must keep curation disabled.
|
|
284
|
+
const revisionFiles = options.type !== "diff" || options.pr ? undefined : (options.files ?? []);
|
|
285
|
+
let revised = await client.sessionSubmitRevision(
|
|
286
|
+
existing.id,
|
|
287
|
+
options.content,
|
|
288
|
+
[],
|
|
289
|
+
revisionFiles,
|
|
239
290
|
);
|
|
240
291
|
|
|
241
|
-
if (
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if (options.notes?.length) {
|
|
245
|
-
await attachNotes(client, revised.id, options.notes);
|
|
246
|
-
revised = await client.sessionGet(revised.id);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
return new ReviewHandle(client, revised);
|
|
292
|
+
if (options.notes?.length) {
|
|
293
|
+
await attachNotes(client, revised.id, options.notes);
|
|
294
|
+
revised = await client.sessionGet(revised.id);
|
|
250
295
|
}
|
|
296
|
+
|
|
297
|
+
return new ReviewHandle(client, revised);
|
|
251
298
|
}
|
|
252
299
|
let session = await client.sessionCreate(workspace, {
|
|
253
300
|
type: options.type,
|
|
254
301
|
content: options.content,
|
|
255
302
|
files: options.files,
|
|
256
303
|
meta: {
|
|
304
|
+
workflow: options.workflow,
|
|
257
305
|
agent: options.agent,
|
|
258
306
|
agentSessionId: options.agentSessionId,
|
|
259
307
|
planPath: options.planPath,
|
package/src/validate.test.ts
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
IdentitySchema,
|
|
14
14
|
RevisionSchema,
|
|
15
15
|
ThreadRecordSchema,
|
|
16
|
-
|
|
16
|
+
MessageSchema,
|
|
17
17
|
WorkspaceSchema,
|
|
18
18
|
isKnownMethod,
|
|
19
19
|
parseParams,
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
type Identity,
|
|
30
30
|
type Thread,
|
|
31
31
|
type Revision,
|
|
32
|
-
type
|
|
32
|
+
type Message,
|
|
33
33
|
type WorkspaceKey,
|
|
34
34
|
} from "@cueloop/schema";
|
|
35
35
|
|
|
@@ -97,10 +97,10 @@ describe("parseParams", () => {
|
|
|
97
97
|
).toThrow();
|
|
98
98
|
});
|
|
99
99
|
|
|
100
|
-
test("
|
|
101
|
-
expect(parseParams("session.
|
|
102
|
-
expect(() => parseParams("session.
|
|
103
|
-
/
|
|
100
|
+
test("message kinds are closed", () => {
|
|
101
|
+
expect(parseParams("session.sendMessage", { id: "s", outcome: "approved" }).summary).toBe("");
|
|
102
|
+
expect(() => parseParams("session.sendMessage", { id: "s", outcome: "lgtm" })).toThrow(
|
|
103
|
+
/outcome/,
|
|
104
104
|
);
|
|
105
105
|
});
|
|
106
106
|
|
|
@@ -141,7 +141,7 @@ describe("validateThreadRecord", () => {
|
|
|
141
141
|
artifact: { type: "plan", content: "# P", meta: {} },
|
|
142
142
|
revisions: [{ revision: 1, content: "# P", submittedAt: "now" }],
|
|
143
143
|
annotations: [],
|
|
144
|
-
|
|
144
|
+
message: null,
|
|
145
145
|
status: "pending",
|
|
146
146
|
createdAt: "now",
|
|
147
147
|
};
|
|
@@ -179,6 +179,7 @@ describe("wire pins", () => {
|
|
|
179
179
|
const entryKeys = (schema: { entries: object }) => Object.keys(schema.entries).sort();
|
|
180
180
|
|
|
181
181
|
const fullMeta: Required<ArtifactMeta> = {
|
|
182
|
+
workflow: "plan",
|
|
182
183
|
cwd: "/repo",
|
|
183
184
|
agent: "claude-code",
|
|
184
185
|
agentSessionId: "sess-1",
|
|
@@ -218,11 +219,12 @@ describe("wire pins", () => {
|
|
|
218
219
|
meta: fullMeta,
|
|
219
220
|
files: [{ path: "src/a.ts", oldContents: "old\n", newContents: "new\n", status: "modified" }],
|
|
220
221
|
};
|
|
221
|
-
const
|
|
222
|
-
|
|
222
|
+
const fullMessage: Required<Message> = {
|
|
223
|
+
id: "msg_1",
|
|
224
|
+
outcome: "approved",
|
|
223
225
|
summary: "",
|
|
224
|
-
|
|
225
|
-
|
|
226
|
+
body: "",
|
|
227
|
+
sentAt: "now",
|
|
226
228
|
};
|
|
227
229
|
const fullRevision: Required<Revision> = { revision: 1, content: "# P", submittedAt: "now" };
|
|
228
230
|
const fullWorkspace: Required<WorkspaceKey> = {
|
|
@@ -256,7 +258,7 @@ describe("wire pins", () => {
|
|
|
256
258
|
},
|
|
257
259
|
{ id: "e2", parentId: "e1", type: "comment", annotationId: "a1", createdAt: "now" },
|
|
258
260
|
{ id: "e3", parentId: "e2", type: "comment-removed", annotationId: "a1", createdAt: "now" },
|
|
259
|
-
{ id: "e4", parentId: "e3", type: "
|
|
261
|
+
{ id: "e4", parentId: "e3", type: "message", message: fullMessage, createdAt: "now" },
|
|
260
262
|
{
|
|
261
263
|
id: "e5",
|
|
262
264
|
parentId: "e4",
|
|
@@ -276,7 +278,7 @@ describe("wire pins", () => {
|
|
|
276
278
|
shareBranch: "main",
|
|
277
279
|
workingCopy: "# P edited",
|
|
278
280
|
viewedPaths: ["src/a.ts"],
|
|
279
|
-
|
|
281
|
+
message: fullMessage,
|
|
280
282
|
status: "pending",
|
|
281
283
|
createdAt: "now",
|
|
282
284
|
shares: [
|
|
@@ -328,7 +330,7 @@ describe("wire pins", () => {
|
|
|
328
330
|
|
|
329
331
|
expect(entryKeys(AnnotationSchema)).toEqual(keys(wireAnnotation));
|
|
330
332
|
expect(entryKeys(RevisionSchema)).toEqual(keys(fullRevision));
|
|
331
|
-
expect(entryKeys(
|
|
333
|
+
expect(entryKeys(MessageSchema)).toEqual(keys(fullMessage));
|
|
332
334
|
expect(entryKeys(IdentitySchema)).toEqual(keys(fullIdentity));
|
|
333
335
|
expect(entryKeys(ThreadRecordSchema)).toEqual(keys(fullSession));
|
|
334
336
|
// persisted annotations carry the stamped createdAt
|
package/src/validate.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import * as v from "valibot";
|
|
13
13
|
import {
|
|
14
14
|
ARTIFACT_TYPES,
|
|
15
|
+
WORKFLOW_KINDS,
|
|
15
16
|
SCHEMA_VERSION,
|
|
16
17
|
type Anchor,
|
|
17
18
|
type Annotation,
|
|
@@ -22,12 +23,15 @@ import {
|
|
|
22
23
|
type Identity,
|
|
23
24
|
type Thread,
|
|
24
25
|
type HunkRejection,
|
|
26
|
+
type HarnessBinding,
|
|
27
|
+
type Delivery,
|
|
28
|
+
type PendingDelivery,
|
|
25
29
|
type Revision,
|
|
26
30
|
type SessionHistory,
|
|
27
31
|
type ShareAccess,
|
|
28
32
|
type ShareLink,
|
|
29
33
|
validateHistory,
|
|
30
|
-
type
|
|
34
|
+
type Message,
|
|
31
35
|
type WorkspaceKey,
|
|
32
36
|
} from "@cueloop/schema";
|
|
33
37
|
import { DaemonError } from "./errors";
|
|
@@ -52,6 +56,7 @@ export const WorkspaceSchema = v.object({
|
|
|
52
56
|
} satisfies EntriesOf<WorkspaceKey>);
|
|
53
57
|
|
|
54
58
|
export const ArtifactMetaSchema = v.object({
|
|
59
|
+
workflow: v.optional(v.picklist(WORKFLOW_KINDS)),
|
|
55
60
|
cwd: v.optional(v.string()),
|
|
56
61
|
agent: v.optional(v.string()),
|
|
57
62
|
agentSessionId: v.optional(v.string()),
|
|
@@ -141,6 +146,24 @@ export const ShareLinkSchema = v.object({
|
|
|
141
146
|
shareBranch: v.optional(v.string()),
|
|
142
147
|
} satisfies EntriesOf<ShareLink>);
|
|
143
148
|
|
|
149
|
+
export const HarnessBindingSchema = v.object({
|
|
150
|
+
id: NonEmpty,
|
|
151
|
+
threadId: NonEmpty,
|
|
152
|
+
harness: NonEmpty,
|
|
153
|
+
harnessSessionId: NonEmpty,
|
|
154
|
+
createdAt: NonEmpty,
|
|
155
|
+
approvedRetryMessageId: v.optional(NonEmpty),
|
|
156
|
+
} satisfies EntriesOf<HarnessBinding>);
|
|
157
|
+
|
|
158
|
+
export const DeliverySchema = v.object({
|
|
159
|
+
id: NonEmpty,
|
|
160
|
+
messageId: NonEmpty,
|
|
161
|
+
bindingId: NonEmpty,
|
|
162
|
+
status: v.picklist(["pending", "acknowledged"]),
|
|
163
|
+
createdAt: NonEmpty,
|
|
164
|
+
acknowledgedAt: v.optional(NonEmpty),
|
|
165
|
+
} satisfies EntriesOf<Delivery>);
|
|
166
|
+
|
|
144
167
|
export const Params = {
|
|
145
168
|
"session.create": v.object({ workspace: WorkspaceSchema, artifact: ArtifactSchema }),
|
|
146
169
|
"session.get": v.object({ id: SessionId }),
|
|
@@ -227,15 +250,29 @@ export const Params = {
|
|
|
227
250
|
v.array(v.object({ id: NonEmpty, annotationId: NonEmpty, createdAt: v.string() })),
|
|
228
251
|
),
|
|
229
252
|
}),
|
|
230
|
-
"session.
|
|
253
|
+
"session.sendMessage": v.object({
|
|
231
254
|
id: SessionId,
|
|
232
|
-
|
|
255
|
+
outcome: v.picklist(["approved", "changes_requested"]),
|
|
233
256
|
summary: v.optional(v.string(), ""),
|
|
234
257
|
actionBodies: v.optional(v.record(v.string(), v.string())),
|
|
235
258
|
}),
|
|
259
|
+
"harness.bind": v.object({
|
|
260
|
+
threadId: SessionId,
|
|
261
|
+
harness: NonEmpty,
|
|
262
|
+
harnessSessionId: NonEmpty,
|
|
263
|
+
}),
|
|
264
|
+
"harness.getBinding": v.object({ bindingId: NonEmpty }),
|
|
265
|
+
"harness.consumeApprovedRetry": v.object({
|
|
266
|
+
bindingId: NonEmpty,
|
|
267
|
+
messageId: NonEmpty,
|
|
268
|
+
content: v.string(),
|
|
269
|
+
}),
|
|
270
|
+
"delivery.pending": v.object({ bindingId: NonEmpty }),
|
|
271
|
+
"delivery.acknowledge": v.object({ deliveryId: NonEmpty }),
|
|
236
272
|
"session.submitRevision": v.object({
|
|
237
273
|
id: SessionId,
|
|
238
274
|
content: v.string(),
|
|
275
|
+
files: v.optional(v.array(DiffFileContentsSchema)),
|
|
239
276
|
/** Annotation ids the agent acted on; each is marked addressed. */
|
|
240
277
|
addressedAnnotationIds: v.optional(v.array(NonEmpty), []),
|
|
241
278
|
}),
|
|
@@ -286,12 +323,18 @@ export const RevisionSchema = v.object({
|
|
|
286
323
|
submittedAt: v.string(),
|
|
287
324
|
} satisfies EntriesOf<Revision>);
|
|
288
325
|
|
|
289
|
-
export const
|
|
290
|
-
|
|
326
|
+
export const MessageSchema = v.object({
|
|
327
|
+
id: NonEmpty,
|
|
328
|
+
outcome: v.picklist(["approved", "changes_requested"]),
|
|
291
329
|
summary: v.string(),
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
} satisfies EntriesOf<
|
|
330
|
+
body: v.string(),
|
|
331
|
+
sentAt: v.string(),
|
|
332
|
+
} satisfies EntriesOf<Message>);
|
|
333
|
+
|
|
334
|
+
export const PendingDeliverySchema = v.object({
|
|
335
|
+
delivery: DeliverySchema,
|
|
336
|
+
message: MessageSchema,
|
|
337
|
+
} satisfies EntriesOf<PendingDelivery>);
|
|
295
338
|
|
|
296
339
|
const EntryBaseEntries = {
|
|
297
340
|
id: NonEmpty,
|
|
@@ -309,7 +352,7 @@ export const SessionEntrySchema = v.variant("type", [
|
|
|
309
352
|
}),
|
|
310
353
|
v.object({ ...EntryBaseEntries, type: v.literal("comment"), annotationId: NonEmpty }),
|
|
311
354
|
v.object({ ...EntryBaseEntries, type: v.literal("comment-removed"), annotationId: NonEmpty }),
|
|
312
|
-
v.object({ ...EntryBaseEntries, type: v.literal("
|
|
355
|
+
v.object({ ...EntryBaseEntries, type: v.literal("message"), message: MessageSchema }),
|
|
313
356
|
v.object({
|
|
314
357
|
...EntryBaseEntries,
|
|
315
358
|
type: v.literal("branch-summary"),
|
|
@@ -354,7 +397,7 @@ export const ThreadRecordSchema = v.object({
|
|
|
354
397
|
),
|
|
355
398
|
workingCopy: v.optional(v.string()),
|
|
356
399
|
viewedPaths: v.optional(v.array(v.string())),
|
|
357
|
-
|
|
400
|
+
message: v.nullable(MessageSchema),
|
|
358
401
|
status: v.picklist(["pending", "resolved"]),
|
|
359
402
|
createdAt: v.string(),
|
|
360
403
|
shelvedAnnotations: v.optional(v.array(FullAnnotationSchema)),
|