@mandipadk7/kavi 0.1.0 → 0.1.2
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 +25 -7
- package/dist/adapters/claude.js +136 -13
- package/dist/adapters/codex.js +235 -30
- package/dist/adapters/shared.js +35 -0
- package/dist/approvals.js +72 -1
- package/dist/codex-app-server.js +310 -0
- package/dist/command-queue.js +1 -0
- package/dist/daemon.js +446 -5
- package/dist/decision-ledger.js +75 -0
- package/dist/git.js +171 -0
- package/dist/main.js +251 -36
- package/dist/paths.js +1 -1
- package/dist/prompts.js +13 -0
- package/dist/router.js +190 -5
- package/dist/rpc.js +226 -0
- package/dist/runtime.js +4 -1
- package/dist/session.js +10 -1
- package/dist/task-artifacts.js +10 -2
- package/dist/tui.js +1653 -72
- package/package.json +7 -12
package/dist/daemon.js
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import net from "node:net";
|
|
3
|
+
import path from "node:path";
|
|
1
4
|
import { consumeCommands } from "./command-queue.js";
|
|
2
5
|
import { buildPeerMessages as buildClaudePeerMessages, runClaudeTask } from "./adapters/claude.js";
|
|
3
6
|
import { buildPeerMessages as buildCodexPeerMessages, runCodexTask } from "./adapters/codex.js";
|
|
7
|
+
import { buildDecisionReplay } from "./adapters/shared.js";
|
|
8
|
+
import { listApprovalRequests, resolveApprovalRequest } from "./approvals.js";
|
|
9
|
+
import { addDecisionRecord, upsertPathClaim } from "./decision-ledger.js";
|
|
10
|
+
import { getWorktreeDiffReview, listWorktreeChangedPaths } from "./git.js";
|
|
4
11
|
import { nowIso } from "./paths.js";
|
|
5
12
|
import { buildAdHocTask, buildKickoffTasks } from "./router.js";
|
|
6
|
-
import { loadSessionRecord, recordEvent, saveSessionRecord } from "./session.js";
|
|
7
|
-
import { saveTaskArtifact } from "./task-artifacts.js";
|
|
13
|
+
import { loadSessionRecord, readRecentEvents, recordEvent, saveSessionRecord } from "./session.js";
|
|
14
|
+
import { loadTaskArtifact, saveTaskArtifact } from "./task-artifacts.js";
|
|
8
15
|
export class KaviDaemon {
|
|
9
16
|
paths;
|
|
10
17
|
session;
|
|
@@ -13,6 +20,10 @@ export class KaviDaemon {
|
|
|
13
20
|
processing = false;
|
|
14
21
|
interval = null;
|
|
15
22
|
stopResolver = null;
|
|
23
|
+
rpcServer = null;
|
|
24
|
+
clients = new Set();
|
|
25
|
+
subscribers = new Set();
|
|
26
|
+
mutationQueue = Promise.resolve();
|
|
16
27
|
constructor(paths){
|
|
17
28
|
this.paths = paths;
|
|
18
29
|
}
|
|
@@ -26,6 +37,7 @@ export class KaviDaemon {
|
|
|
26
37
|
await recordEvent(this.paths, this.session.id, "daemon.started", {
|
|
27
38
|
daemonPid: process.pid
|
|
28
39
|
});
|
|
40
|
+
await this.startRpcServer();
|
|
29
41
|
this.running = true;
|
|
30
42
|
void this.tick();
|
|
31
43
|
this.interval = setInterval(()=>{
|
|
@@ -35,6 +47,343 @@ export class KaviDaemon {
|
|
|
35
47
|
this.stopResolver = resolve;
|
|
36
48
|
});
|
|
37
49
|
}
|
|
50
|
+
async startRpcServer() {
|
|
51
|
+
await fs.mkdir(path.dirname(this.paths.socketPath), {
|
|
52
|
+
recursive: true
|
|
53
|
+
});
|
|
54
|
+
await fs.rm(this.paths.socketPath, {
|
|
55
|
+
force: true
|
|
56
|
+
}).catch(()=>{});
|
|
57
|
+
this.rpcServer = net.createServer((socket)=>{
|
|
58
|
+
this.clients.add(socket);
|
|
59
|
+
socket.setEncoding("utf8");
|
|
60
|
+
let buffer = "";
|
|
61
|
+
socket.on("data", (chunk)=>{
|
|
62
|
+
buffer += chunk;
|
|
63
|
+
while(true){
|
|
64
|
+
const newlineIndex = buffer.indexOf("\n");
|
|
65
|
+
if (newlineIndex === -1) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const line = buffer.slice(0, newlineIndex).trim();
|
|
69
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
70
|
+
if (!line) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
let request;
|
|
74
|
+
try {
|
|
75
|
+
request = JSON.parse(line);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
this.writeRpc(socket, {
|
|
78
|
+
id: "parse-error",
|
|
79
|
+
error: {
|
|
80
|
+
message: error instanceof Error ? error.message : "Unable to parse RPC payload."
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
void this.handleRpcRequest(socket, request);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
const cleanup = ()=>{
|
|
89
|
+
this.clients.delete(socket);
|
|
90
|
+
this.subscribers.delete(socket);
|
|
91
|
+
};
|
|
92
|
+
socket.on("error", cleanup);
|
|
93
|
+
socket.on("close", cleanup);
|
|
94
|
+
});
|
|
95
|
+
await new Promise((resolve, reject)=>{
|
|
96
|
+
this.rpcServer?.once("error", reject);
|
|
97
|
+
this.rpcServer?.listen(this.paths.socketPath, ()=>{
|
|
98
|
+
this.rpcServer?.off("error", reject);
|
|
99
|
+
resolve();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
writeRpc(socket, response, onWritten) {
|
|
104
|
+
socket.write(`${JSON.stringify(response)}\n`, onWritten);
|
|
105
|
+
}
|
|
106
|
+
writeNotification(socket, notification) {
|
|
107
|
+
socket.write(`${JSON.stringify(notification)}\n`);
|
|
108
|
+
}
|
|
109
|
+
async handleRpcRequest(socket, request) {
|
|
110
|
+
try {
|
|
111
|
+
const dispatch = await this.dispatchRpc(socket, request.method, request.params ?? {});
|
|
112
|
+
this.writeRpc(socket, {
|
|
113
|
+
id: request.id,
|
|
114
|
+
result: dispatch.result
|
|
115
|
+
}, ()=>{
|
|
116
|
+
if (dispatch.shutdownAfterResponse) {
|
|
117
|
+
void this.stopFromRpc();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
} catch (error) {
|
|
121
|
+
this.writeRpc(socket, {
|
|
122
|
+
id: request.id,
|
|
123
|
+
error: {
|
|
124
|
+
message: error instanceof Error ? error.message : String(error)
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async dispatchRpc(socket, method, params) {
|
|
130
|
+
switch(method){
|
|
131
|
+
case "ping":
|
|
132
|
+
return {
|
|
133
|
+
result: {
|
|
134
|
+
ok: true,
|
|
135
|
+
sessionId: this.session.id
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
case "snapshot":
|
|
139
|
+
return {
|
|
140
|
+
result: await this.buildSnapshot()
|
|
141
|
+
};
|
|
142
|
+
case "subscribe":
|
|
143
|
+
this.subscribers.add(socket);
|
|
144
|
+
return {
|
|
145
|
+
result: await this.buildSnapshot()
|
|
146
|
+
};
|
|
147
|
+
case "kickoff":
|
|
148
|
+
await this.kickoffFromRpc(params);
|
|
149
|
+
return {
|
|
150
|
+
result: {
|
|
151
|
+
ok: true
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
case "enqueueTask":
|
|
155
|
+
await this.enqueueRpcTask(params);
|
|
156
|
+
return {
|
|
157
|
+
result: {
|
|
158
|
+
ok: true
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
case "shutdown":
|
|
162
|
+
return {
|
|
163
|
+
result: {
|
|
164
|
+
ok: true
|
|
165
|
+
},
|
|
166
|
+
shutdownAfterResponse: true
|
|
167
|
+
};
|
|
168
|
+
case "resolveApproval":
|
|
169
|
+
await this.resolveApprovalFromRpc(params);
|
|
170
|
+
return {
|
|
171
|
+
result: {
|
|
172
|
+
ok: true
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
case "taskArtifact":
|
|
176
|
+
return {
|
|
177
|
+
result: await this.getTaskArtifactFromRpc(params)
|
|
178
|
+
};
|
|
179
|
+
case "events":
|
|
180
|
+
return {
|
|
181
|
+
result: await this.getEventsFromRpc(params)
|
|
182
|
+
};
|
|
183
|
+
case "worktreeDiff":
|
|
184
|
+
return {
|
|
185
|
+
result: await this.getWorktreeDiffFromRpc(params)
|
|
186
|
+
};
|
|
187
|
+
case "notifyExternalUpdate":
|
|
188
|
+
await this.publishSnapshot(typeof params.reason === "string" && params.reason.trim() ? params.reason.trim() : "external.update");
|
|
189
|
+
return {
|
|
190
|
+
result: {
|
|
191
|
+
ok: true
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
default:
|
|
195
|
+
throw new Error(`Unknown RPC method: ${method}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async buildSnapshot() {
|
|
199
|
+
const session = await loadSessionRecord(this.paths);
|
|
200
|
+
const events = await readRecentEvents(this.paths, 30);
|
|
201
|
+
const approvals = await listApprovalRequests(this.paths, {
|
|
202
|
+
includeResolved: true
|
|
203
|
+
});
|
|
204
|
+
const worktreeDiffs = await Promise.all(session.worktrees.map(async (worktree)=>({
|
|
205
|
+
agent: worktree.agent,
|
|
206
|
+
paths: await listWorktreeChangedPaths(worktree.path, session.baseCommit).catch(()=>[])
|
|
207
|
+
})));
|
|
208
|
+
return {
|
|
209
|
+
session,
|
|
210
|
+
events,
|
|
211
|
+
approvals,
|
|
212
|
+
worktreeDiffs
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
async runMutation(fn) {
|
|
216
|
+
const previous = this.mutationQueue;
|
|
217
|
+
let release = ()=>{};
|
|
218
|
+
this.mutationQueue = new Promise((resolve)=>{
|
|
219
|
+
release = resolve;
|
|
220
|
+
});
|
|
221
|
+
await previous;
|
|
222
|
+
try {
|
|
223
|
+
return await fn();
|
|
224
|
+
} finally{
|
|
225
|
+
release();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
async enqueueRpcTask(params) {
|
|
229
|
+
await this.runMutation(async ()=>{
|
|
230
|
+
const prompt = typeof params.prompt === "string" ? params.prompt : "";
|
|
231
|
+
if (!prompt.trim()) {
|
|
232
|
+
throw new Error("enqueueTask requires a prompt.");
|
|
233
|
+
}
|
|
234
|
+
const owner = params.owner === "claude" ? "claude" : "codex";
|
|
235
|
+
const commandId = `rpc-${Date.now()}`;
|
|
236
|
+
const taskId = `task-${commandId}`;
|
|
237
|
+
const task = buildAdHocTask(owner, prompt, taskId, {
|
|
238
|
+
routeReason: typeof params.routeReason === "string" ? params.routeReason : null,
|
|
239
|
+
claimedPaths: Array.isArray(params.claimedPaths) ? params.claimedPaths.map((item)=>String(item)) : []
|
|
240
|
+
});
|
|
241
|
+
this.session.tasks.push(task);
|
|
242
|
+
addDecisionRecord(this.session, {
|
|
243
|
+
kind: "route",
|
|
244
|
+
agent: owner,
|
|
245
|
+
taskId,
|
|
246
|
+
summary: `Routed task to ${owner}`,
|
|
247
|
+
detail: typeof params.routeReason === "string" ? params.routeReason : `Task enqueued for ${owner}.`,
|
|
248
|
+
metadata: {
|
|
249
|
+
strategy: typeof params.routeStrategy === "string" ? params.routeStrategy : "unknown",
|
|
250
|
+
confidence: typeof params.routeConfidence === "number" ? params.routeConfidence : null,
|
|
251
|
+
claimedPaths: task.claimedPaths
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
upsertPathClaim(this.session, {
|
|
255
|
+
taskId,
|
|
256
|
+
agent: owner,
|
|
257
|
+
source: "route",
|
|
258
|
+
paths: task.claimedPaths,
|
|
259
|
+
note: task.routeReason
|
|
260
|
+
});
|
|
261
|
+
await saveSessionRecord(this.paths, this.session);
|
|
262
|
+
await recordEvent(this.paths, this.session.id, "task.enqueued", {
|
|
263
|
+
owner,
|
|
264
|
+
via: "rpc"
|
|
265
|
+
});
|
|
266
|
+
await this.publishSnapshot("task.enqueued");
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
async kickoffFromRpc(params) {
|
|
270
|
+
await this.runMutation(async ()=>{
|
|
271
|
+
const prompt = typeof params.prompt === "string" ? params.prompt : "";
|
|
272
|
+
if (!prompt.trim()) {
|
|
273
|
+
throw new Error("kickoff requires a prompt.");
|
|
274
|
+
}
|
|
275
|
+
this.session.goal = prompt;
|
|
276
|
+
this.session.tasks.push(...buildKickoffTasks(prompt));
|
|
277
|
+
await saveSessionRecord(this.paths, this.session);
|
|
278
|
+
await recordEvent(this.paths, this.session.id, "tasks.kickoff_enqueued", {
|
|
279
|
+
count: 2,
|
|
280
|
+
via: "rpc"
|
|
281
|
+
});
|
|
282
|
+
await this.publishSnapshot("tasks.kickoff_enqueued");
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
async stopFromRpc() {
|
|
286
|
+
await this.runMutation(async ()=>{
|
|
287
|
+
this.session.status = "stopped";
|
|
288
|
+
this.running = false;
|
|
289
|
+
this.session.daemonHeartbeatAt = new Date().toISOString();
|
|
290
|
+
await saveSessionRecord(this.paths, this.session);
|
|
291
|
+
await recordEvent(this.paths, this.session.id, "daemon.stopped", {
|
|
292
|
+
via: "rpc"
|
|
293
|
+
});
|
|
294
|
+
if (this.interval) {
|
|
295
|
+
clearInterval(this.interval);
|
|
296
|
+
this.interval = null;
|
|
297
|
+
}
|
|
298
|
+
await this.closeRpcServer();
|
|
299
|
+
this.stopResolver?.();
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
async resolveApprovalFromRpc(params) {
|
|
303
|
+
await this.runMutation(async ()=>{
|
|
304
|
+
const requestId = typeof params.requestId === "string" ? params.requestId : "";
|
|
305
|
+
const decision = params.decision === "deny" ? "deny" : "allow";
|
|
306
|
+
const remember = params.remember === true;
|
|
307
|
+
if (!requestId) {
|
|
308
|
+
throw new Error("resolveApproval requires a requestId.");
|
|
309
|
+
}
|
|
310
|
+
const request = await resolveApprovalRequest(this.paths, requestId, decision, remember);
|
|
311
|
+
addDecisionRecord(this.session, {
|
|
312
|
+
kind: "approval",
|
|
313
|
+
agent: request.agent,
|
|
314
|
+
summary: `${decision === "allow" ? "Approved" : "Denied"} ${request.toolName}`,
|
|
315
|
+
detail: request.summary,
|
|
316
|
+
metadata: {
|
|
317
|
+
requestId: request.id,
|
|
318
|
+
remember,
|
|
319
|
+
toolName: request.toolName
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
await saveSessionRecord(this.paths, this.session);
|
|
323
|
+
await recordEvent(this.paths, this.session.id, "approval.resolved", {
|
|
324
|
+
requestId: request.id,
|
|
325
|
+
decision,
|
|
326
|
+
remember,
|
|
327
|
+
agent: request.agent,
|
|
328
|
+
toolName: request.toolName,
|
|
329
|
+
via: "rpc"
|
|
330
|
+
});
|
|
331
|
+
await this.publishSnapshot("approval.resolved");
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
async getTaskArtifactFromRpc(params) {
|
|
335
|
+
const taskId = typeof params.taskId === "string" ? params.taskId : "";
|
|
336
|
+
if (!taskId) {
|
|
337
|
+
throw new Error("taskArtifact requires a taskId.");
|
|
338
|
+
}
|
|
339
|
+
return await loadTaskArtifact(this.paths, taskId);
|
|
340
|
+
}
|
|
341
|
+
async getEventsFromRpc(params) {
|
|
342
|
+
const limit = typeof params.limit === "number" && Number.isFinite(params.limit) ? params.limit : 20;
|
|
343
|
+
return await readRecentEvents(this.paths, limit);
|
|
344
|
+
}
|
|
345
|
+
async getWorktreeDiffFromRpc(params) {
|
|
346
|
+
const agent = params.agent === "claude" ? "claude" : "codex";
|
|
347
|
+
const filePath = typeof params.filePath === "string" ? params.filePath : null;
|
|
348
|
+
const worktree = this.session.worktrees.find((item)=>item.agent === agent);
|
|
349
|
+
if (!worktree) {
|
|
350
|
+
throw new Error(`No managed worktree found for ${agent}.`);
|
|
351
|
+
}
|
|
352
|
+
return await getWorktreeDiffReview(agent, worktree.path, this.session.baseCommit, filePath);
|
|
353
|
+
}
|
|
354
|
+
async publishSnapshot(reason) {
|
|
355
|
+
if (this.subscribers.size === 0) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const snapshot = await this.buildSnapshot();
|
|
359
|
+
const notification = {
|
|
360
|
+
method: "snapshot.updated",
|
|
361
|
+
params: {
|
|
362
|
+
reason,
|
|
363
|
+
snapshot
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
for (const subscriber of this.subscribers){
|
|
367
|
+
this.writeNotification(subscriber, notification);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
async closeRpcServer() {
|
|
371
|
+
const server = this.rpcServer;
|
|
372
|
+
this.rpcServer = null;
|
|
373
|
+
for (const client of this.clients){
|
|
374
|
+
client.end();
|
|
375
|
+
}
|
|
376
|
+
this.clients.clear();
|
|
377
|
+
this.subscribers.clear();
|
|
378
|
+
if (server) {
|
|
379
|
+
await new Promise((resolve)=>{
|
|
380
|
+
server.close(()=>resolve());
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
await fs.rm(this.paths.socketPath, {
|
|
384
|
+
force: true
|
|
385
|
+
}).catch(()=>{});
|
|
386
|
+
}
|
|
38
387
|
async tick() {
|
|
39
388
|
if (!this.running || this.processing) {
|
|
40
389
|
return;
|
|
@@ -48,6 +397,7 @@ export class KaviDaemon {
|
|
|
48
397
|
clearInterval(this.interval);
|
|
49
398
|
this.interval = null;
|
|
50
399
|
}
|
|
400
|
+
await this.closeRpcServer();
|
|
51
401
|
this.stopResolver?.();
|
|
52
402
|
return;
|
|
53
403
|
}
|
|
@@ -62,6 +412,7 @@ export class KaviDaemon {
|
|
|
62
412
|
await recordEvent(this.paths, this.session.id, "tasks.kickoff_created", {
|
|
63
413
|
count: this.session.tasks.length
|
|
64
414
|
});
|
|
415
|
+
await this.publishSnapshot("tasks.kickoff_created");
|
|
65
416
|
}
|
|
66
417
|
const pending = this.session.tasks.filter((task)=>task.status === "pending");
|
|
67
418
|
for (const task of pending){
|
|
@@ -87,6 +438,7 @@ export class KaviDaemon {
|
|
|
87
438
|
clearInterval(this.interval);
|
|
88
439
|
this.interval = null;
|
|
89
440
|
}
|
|
441
|
+
await this.closeRpcServer();
|
|
90
442
|
this.stopResolver?.();
|
|
91
443
|
return;
|
|
92
444
|
}
|
|
@@ -97,15 +449,41 @@ export class KaviDaemon {
|
|
|
97
449
|
await recordEvent(this.paths, this.session.id, "tasks.kickoff_enqueued", {
|
|
98
450
|
count: 2
|
|
99
451
|
});
|
|
452
|
+
await this.publishSnapshot("tasks.kickoff_enqueued");
|
|
100
453
|
continue;
|
|
101
454
|
}
|
|
102
455
|
if (command.type === "enqueue" && typeof command.payload.prompt === "string") {
|
|
103
456
|
const owner = command.payload.owner === "claude" ? "claude" : "codex";
|
|
104
|
-
|
|
457
|
+
const taskId = `task-${command.id}`;
|
|
458
|
+
const task = buildAdHocTask(owner, command.payload.prompt, taskId, {
|
|
459
|
+
routeReason: typeof command.payload.routeReason === "string" ? command.payload.routeReason : null,
|
|
460
|
+
claimedPaths: Array.isArray(command.payload.claimedPaths) ? command.payload.claimedPaths.map((item)=>String(item)) : []
|
|
461
|
+
});
|
|
462
|
+
this.session.tasks.push(task);
|
|
463
|
+
addDecisionRecord(this.session, {
|
|
464
|
+
kind: "route",
|
|
465
|
+
agent: owner,
|
|
466
|
+
taskId,
|
|
467
|
+
summary: `Routed task to ${owner}`,
|
|
468
|
+
detail: typeof command.payload.routeReason === "string" ? command.payload.routeReason : `Task enqueued for ${owner}.`,
|
|
469
|
+
metadata: {
|
|
470
|
+
strategy: typeof command.payload.routeStrategy === "string" ? command.payload.routeStrategy : "unknown",
|
|
471
|
+
confidence: typeof command.payload.routeConfidence === "number" ? command.payload.routeConfidence : null,
|
|
472
|
+
claimedPaths: task.claimedPaths
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
upsertPathClaim(this.session, {
|
|
476
|
+
taskId,
|
|
477
|
+
agent: owner,
|
|
478
|
+
source: "route",
|
|
479
|
+
paths: task.claimedPaths,
|
|
480
|
+
note: task.routeReason
|
|
481
|
+
});
|
|
105
482
|
await saveSessionRecord(this.paths, this.session);
|
|
106
483
|
await recordEvent(this.paths, this.session.id, "task.enqueued", {
|
|
107
484
|
owner
|
|
108
485
|
});
|
|
486
|
+
await this.publishSnapshot("task.enqueued");
|
|
109
487
|
}
|
|
110
488
|
}
|
|
111
489
|
}
|
|
@@ -118,6 +496,7 @@ export class KaviDaemon {
|
|
|
118
496
|
taskId: task.id,
|
|
119
497
|
owner: task.owner
|
|
120
498
|
});
|
|
499
|
+
await this.publishSnapshot("task.started");
|
|
121
500
|
try {
|
|
122
501
|
let envelope;
|
|
123
502
|
let peerMessages;
|
|
@@ -127,21 +506,48 @@ export class KaviDaemon {
|
|
|
127
506
|
envelope = result.envelope;
|
|
128
507
|
rawOutput = result.raw;
|
|
129
508
|
peerMessages = buildCodexPeerMessages(result.envelope, "codex", task.id);
|
|
130
|
-
await this.markAgent("codex", result.envelope.summary, 0,
|
|
509
|
+
await this.markAgent("codex", result.envelope.summary, 0, result.threadId);
|
|
131
510
|
} else if (task.owner === "claude") {
|
|
132
511
|
const result = await runClaudeTask(this.session, task, this.paths);
|
|
133
512
|
envelope = result.envelope;
|
|
134
513
|
rawOutput = result.raw;
|
|
135
514
|
peerMessages = buildClaudePeerMessages(result.envelope, "claude", task.id);
|
|
136
|
-
await this.markAgent("claude", result.envelope.summary, 0,
|
|
515
|
+
await this.markAgent("claude", result.envelope.summary, 0, result.sessionId);
|
|
137
516
|
} else {
|
|
138
517
|
throw new Error(`Unsupported task owner ${task.owner}.`);
|
|
139
518
|
}
|
|
140
519
|
task.status = envelope.status === "completed" ? "completed" : "blocked";
|
|
141
520
|
task.summary = envelope.summary;
|
|
142
521
|
task.updatedAt = new Date().toISOString();
|
|
522
|
+
if (task.owner === "codex" || task.owner === "claude") {
|
|
523
|
+
const worktree = this.session.worktrees.find((item)=>item.agent === task.owner);
|
|
524
|
+
if (worktree) {
|
|
525
|
+
const changedPaths = await listWorktreeChangedPaths(worktree.path, this.session.baseCommit);
|
|
526
|
+
task.claimedPaths = changedPaths;
|
|
527
|
+
upsertPathClaim(this.session, {
|
|
528
|
+
taskId: task.id,
|
|
529
|
+
agent: task.owner,
|
|
530
|
+
source: "diff",
|
|
531
|
+
paths: changedPaths,
|
|
532
|
+
note: task.summary
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
addDecisionRecord(this.session, {
|
|
537
|
+
kind: "task",
|
|
538
|
+
agent: task.owner === "router" ? "router" : task.owner,
|
|
539
|
+
taskId: task.id,
|
|
540
|
+
summary: `${task.owner} task ${task.status}`,
|
|
541
|
+
detail: task.summary ?? envelope.summary,
|
|
542
|
+
metadata: {
|
|
543
|
+
title: task.title,
|
|
544
|
+
status: task.status,
|
|
545
|
+
claimedPaths: task.claimedPaths
|
|
546
|
+
}
|
|
547
|
+
});
|
|
143
548
|
this.session.peerMessages.push(...peerMessages);
|
|
144
549
|
await saveSessionRecord(this.paths, this.session);
|
|
550
|
+
const decisionReplay = buildDecisionReplay(this.session, task, task.owner === "claude" ? "claude" : "codex");
|
|
145
551
|
await saveTaskArtifact(this.paths, {
|
|
146
552
|
taskId: task.id,
|
|
147
553
|
sessionId: this.session.id,
|
|
@@ -149,6 +555,9 @@ export class KaviDaemon {
|
|
|
149
555
|
owner: task.owner,
|
|
150
556
|
status: task.status,
|
|
151
557
|
summary: task.summary,
|
|
558
|
+
routeReason: task.routeReason,
|
|
559
|
+
claimedPaths: task.claimedPaths,
|
|
560
|
+
decisionReplay,
|
|
152
561
|
rawOutput,
|
|
153
562
|
error: null,
|
|
154
563
|
envelope,
|
|
@@ -161,12 +570,40 @@ export class KaviDaemon {
|
|
|
161
570
|
status: task.status,
|
|
162
571
|
peerMessages: peerMessages.length
|
|
163
572
|
});
|
|
573
|
+
await this.publishSnapshot("task.completed");
|
|
164
574
|
} catch (error) {
|
|
165
575
|
task.status = "failed";
|
|
166
576
|
task.summary = error instanceof Error ? error.message : String(error);
|
|
167
577
|
task.updatedAt = new Date().toISOString();
|
|
578
|
+
if (task.owner === "codex" || task.owner === "claude") {
|
|
579
|
+
const worktree = this.session.worktrees.find((item)=>item.agent === task.owner);
|
|
580
|
+
if (worktree) {
|
|
581
|
+
const changedPaths = await listWorktreeChangedPaths(worktree.path, this.session.baseCommit);
|
|
582
|
+
task.claimedPaths = changedPaths;
|
|
583
|
+
upsertPathClaim(this.session, {
|
|
584
|
+
taskId: task.id,
|
|
585
|
+
agent: task.owner,
|
|
586
|
+
source: "diff",
|
|
587
|
+
paths: changedPaths,
|
|
588
|
+
note: task.summary
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
addDecisionRecord(this.session, {
|
|
593
|
+
kind: "task",
|
|
594
|
+
agent: task.owner === "router" ? "router" : task.owner,
|
|
595
|
+
taskId: task.id,
|
|
596
|
+
summary: `${task.owner} task failed`,
|
|
597
|
+
detail: task.summary,
|
|
598
|
+
metadata: {
|
|
599
|
+
title: task.title,
|
|
600
|
+
status: task.status,
|
|
601
|
+
claimedPaths: task.claimedPaths
|
|
602
|
+
}
|
|
603
|
+
});
|
|
168
604
|
await this.markAgent(task.owner, task.summary, 1, task.owner === "claude" ? `${this.session.id}-claude` : null);
|
|
169
605
|
await saveSessionRecord(this.paths, this.session);
|
|
606
|
+
const decisionReplay = buildDecisionReplay(this.session, task, task.owner === "claude" ? "claude" : "codex");
|
|
170
607
|
await saveTaskArtifact(this.paths, {
|
|
171
608
|
taskId: task.id,
|
|
172
609
|
sessionId: this.session.id,
|
|
@@ -174,6 +611,9 @@ export class KaviDaemon {
|
|
|
174
611
|
owner: task.owner,
|
|
175
612
|
status: task.status,
|
|
176
613
|
summary: task.summary,
|
|
614
|
+
routeReason: task.routeReason,
|
|
615
|
+
claimedPaths: task.claimedPaths,
|
|
616
|
+
decisionReplay,
|
|
177
617
|
rawOutput: null,
|
|
178
618
|
error: task.summary,
|
|
179
619
|
envelope: null,
|
|
@@ -185,6 +625,7 @@ export class KaviDaemon {
|
|
|
185
625
|
owner: task.owner,
|
|
186
626
|
error: task.summary
|
|
187
627
|
});
|
|
628
|
+
await this.publishSnapshot("task.failed");
|
|
188
629
|
}
|
|
189
630
|
}
|
|
190
631
|
async markAgent(agent, summary, exitCode, sessionId) {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { nowIso } from "./paths.js";
|
|
3
|
+
const MAX_DECISIONS = 80;
|
|
4
|
+
function normalizePaths(paths) {
|
|
5
|
+
return [
|
|
6
|
+
...new Set(paths.map((item)=>item.trim()).filter(Boolean))
|
|
7
|
+
].sort();
|
|
8
|
+
}
|
|
9
|
+
export function addDecisionRecord(session, input) {
|
|
10
|
+
const record = {
|
|
11
|
+
id: randomUUID(),
|
|
12
|
+
kind: input.kind,
|
|
13
|
+
agent: input.agent,
|
|
14
|
+
taskId: input.taskId ?? null,
|
|
15
|
+
summary: input.summary,
|
|
16
|
+
detail: input.detail,
|
|
17
|
+
createdAt: nowIso(),
|
|
18
|
+
metadata: input.metadata ?? {}
|
|
19
|
+
};
|
|
20
|
+
session.decisions = [
|
|
21
|
+
...session.decisions,
|
|
22
|
+
record
|
|
23
|
+
].slice(-MAX_DECISIONS);
|
|
24
|
+
return record;
|
|
25
|
+
}
|
|
26
|
+
export function upsertPathClaim(session, input) {
|
|
27
|
+
const normalizedPaths = normalizePaths(input.paths);
|
|
28
|
+
const existing = session.pathClaims.find((claim)=>claim.taskId === input.taskId);
|
|
29
|
+
if (normalizedPaths.length === 0 && existing) {
|
|
30
|
+
existing.paths = [];
|
|
31
|
+
existing.status = "released";
|
|
32
|
+
existing.note = input.note ?? existing.note;
|
|
33
|
+
existing.updatedAt = nowIso();
|
|
34
|
+
return existing;
|
|
35
|
+
}
|
|
36
|
+
if (normalizedPaths.length === 0) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
if (existing) {
|
|
40
|
+
existing.agent = input.agent;
|
|
41
|
+
existing.source = input.source;
|
|
42
|
+
existing.paths = normalizedPaths;
|
|
43
|
+
existing.note = input.note ?? existing.note;
|
|
44
|
+
existing.status = input.status ?? "active";
|
|
45
|
+
existing.updatedAt = nowIso();
|
|
46
|
+
return existing;
|
|
47
|
+
}
|
|
48
|
+
const timestamp = nowIso();
|
|
49
|
+
const claim = {
|
|
50
|
+
id: randomUUID(),
|
|
51
|
+
taskId: input.taskId,
|
|
52
|
+
agent: input.agent,
|
|
53
|
+
source: input.source,
|
|
54
|
+
status: input.status ?? "active",
|
|
55
|
+
paths: normalizedPaths,
|
|
56
|
+
note: input.note ?? null,
|
|
57
|
+
createdAt: timestamp,
|
|
58
|
+
updatedAt: timestamp
|
|
59
|
+
};
|
|
60
|
+
session.pathClaims.push(claim);
|
|
61
|
+
return claim;
|
|
62
|
+
}
|
|
63
|
+
export function activePathClaims(session) {
|
|
64
|
+
return session.pathClaims.filter((claim)=>claim.status === "active" && claim.paths.length > 0);
|
|
65
|
+
}
|
|
66
|
+
export function findClaimConflicts(session, owner, claimedPaths) {
|
|
67
|
+
const normalizedPaths = normalizePaths(claimedPaths);
|
|
68
|
+
if (normalizedPaths.length === 0) {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
return activePathClaims(session).filter((claim)=>claim.agent !== owner && claim.paths.some((item)=>normalizedPaths.includes(item)));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
//# sourceURL=decision-ledger.ts
|