@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/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
- this.session.tasks.push(buildAdHocTask(owner, command.payload.prompt, `task-${command.id}`));
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, null);
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, `${this.session.id}-claude`);
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