@mandipadk7/kavi 0.1.1 → 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/main.js CHANGED
@@ -12,6 +12,7 @@ import { writeJson } from "./fs.js";
12
12
  import { createGitignoreEntries, detectRepoRoot, ensureWorktrees, findOverlappingWorktreePaths, getHeadCommit, landBranches, resolveTargetBranch } from "./git.js";
13
13
  import { buildSessionId, resolveAppPaths } from "./paths.js";
14
14
  import { isProcessAlive, spawnDetachedNode } from "./process.js";
15
+ import { pingRpc, readSnapshot, rpcEnqueueTask, rpcNotifyExternalUpdate, rpcKickoff, rpcRecentEvents, rpcResolveApproval, rpcShutdown, rpcTaskArtifact } from "./rpc.js";
15
16
  import { resolveSessionRuntime } from "./runtime.js";
16
17
  import { buildAdHocTask, extractPromptPathHints, routeTask } from "./router.js";
17
18
  import { createSessionRecord, loadSessionRecord, readRecentEvents, recordEvent, sessionExists, sessionHeartbeatAgeMs } from "./session.js";
@@ -68,6 +69,8 @@ function renderUsage() {
68
69
  " kavi task [--agent codex|claude|auto] <prompt>",
69
70
  " kavi tasks [--json]",
70
71
  " kavi task-output <task-id|latest> [--json]",
72
+ " kavi decisions [--json] [--limit N]",
73
+ " kavi claims [--json] [--all]",
71
74
  " kavi approvals [--json] [--all]",
72
75
  " kavi approve <request-id|latest> [--remember]",
73
76
  " kavi deny <request-id|latest> [--remember]",
@@ -96,7 +99,7 @@ async function waitForSession(paths, expectedState = "running") {
96
99
  try {
97
100
  if (await sessionExists(paths)) {
98
101
  const session = await loadSessionRecord(paths);
99
- if (expectedState === "running" && isSessionLive(session)) {
102
+ if (expectedState === "running" && isSessionLive(session) && await pingRpc(paths)) {
100
103
  return;
101
104
  }
102
105
  if (expectedState === "stopped" && session.status === "stopped") {
@@ -148,11 +151,9 @@ async function startOrAttachSession(cwd, goal) {
148
151
  if (await sessionExists(paths)) {
149
152
  try {
150
153
  const session = await loadSessionRecord(paths);
151
- if (isSessionLive(session)) {
154
+ if (isSessionLive(session) && await pingRpc(paths)) {
152
155
  if (goal) {
153
- await appendCommand(paths, "kickoff", {
154
- prompt: goal
155
- });
156
+ await rpcKickoff(paths, goal);
156
157
  }
157
158
  return session.socketPath;
158
159
  }
@@ -166,7 +167,7 @@ async function startOrAttachSession(cwd, goal) {
166
167
  const runtime = await resolveSessionRuntime(paths);
167
168
  const baseCommit = await getHeadCommit(repoRoot);
168
169
  const sessionId = buildSessionId();
169
- const rpcEndpoint = "file://session-state";
170
+ const rpcEndpoint = paths.socketPath;
170
171
  await fs.writeFile(paths.commandsFile, "", "utf8");
171
172
  const worktrees = await ensureWorktrees(repoRoot, paths, sessionId, config, baseCommit);
172
173
  await createSessionRecord(paths, config, runtime, sessionId, baseCommit, worktrees, goal, rpcEndpoint);
@@ -210,6 +211,20 @@ async function requireSession(cwd) {
210
211
  paths
211
212
  };
212
213
  }
214
+ async function tryRpcSnapshot(paths) {
215
+ if (!await pingRpc(paths)) {
216
+ return null;
217
+ }
218
+ return await readSnapshot(paths);
219
+ }
220
+ async function notifyOperatorSurface(paths, reason) {
221
+ if (!await pingRpc(paths)) {
222
+ return;
223
+ }
224
+ try {
225
+ await rpcNotifyExternalUpdate(paths, reason);
226
+ } catch {}
227
+ }
213
228
  async function commandOpen(cwd, args) {
214
229
  const goal = getGoal(args);
215
230
  await startOrAttachSession(cwd, goal);
@@ -238,17 +253,20 @@ async function commandStart(cwd, args) {
238
253
  }
239
254
  async function commandStatus(cwd, args) {
240
255
  const { paths } = await requireSession(cwd);
241
- const session = await loadSessionRecord(paths);
242
- const pendingApprovals = await listApprovalRequests(paths);
256
+ const rpcSnapshot = await tryRpcSnapshot(paths);
257
+ const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
258
+ const pendingApprovals = rpcSnapshot?.approvals.filter((item)=>item.status === "pending") ?? await listApprovalRequests(paths);
243
259
  const heartbeatAgeMs = sessionHeartbeatAgeMs(session);
244
260
  const payload = {
245
261
  id: session.id,
246
262
  status: session.status,
247
263
  repoRoot: session.repoRoot,
264
+ socketPath: session.socketPath,
248
265
  goal: session.goal,
249
266
  daemonPid: session.daemonPid,
250
267
  daemonHeartbeatAt: session.daemonHeartbeatAt,
251
268
  daemonHealthy: isSessionLive(session),
269
+ rpcConnected: rpcSnapshot !== null,
252
270
  heartbeatAgeMs,
253
271
  runtime: session.runtime,
254
272
  taskCounts: {
@@ -277,6 +295,7 @@ async function commandStatus(cwd, args) {
277
295
  console.log(`Session: ${payload.id}`);
278
296
  console.log(`Status: ${payload.status}${payload.daemonHealthy ? " (healthy)" : " (stale or stopped)"}`);
279
297
  console.log(`Repo: ${payload.repoRoot}`);
298
+ console.log(`Control: ${payload.socketPath}${payload.rpcConnected ? " (connected)" : " (disconnected)"}`);
280
299
  console.log(`Goal: ${payload.goal ?? "-"}`);
281
300
  console.log(`Daemon PID: ${payload.daemonPid ?? "-"}`);
282
301
  console.log(`Heartbeat: ${payload.daemonHeartbeatAt ?? "-"}${heartbeatAgeMs === null ? "" : ` (${heartbeatAgeMs} ms ago)`}`);
@@ -305,6 +324,7 @@ async function commandPaths(cwd, args) {
305
324
  eventsFile: paths.eventsFile,
306
325
  approvalsFile: paths.approvalsFile,
307
326
  commandsFile: paths.commandsFile,
327
+ socketPath: paths.socketPath,
308
328
  runsDir: paths.runsDir,
309
329
  claudeSettingsFile: paths.claudeSettingsFile,
310
330
  homeApprovalRulesFile: paths.homeApprovalRulesFile,
@@ -325,6 +345,7 @@ async function commandPaths(cwd, args) {
325
345
  console.log(`Events file: ${payload.eventsFile}`);
326
346
  console.log(`Approvals file: ${payload.approvalsFile}`);
327
347
  console.log(`Command queue: ${payload.commandsFile}`);
348
+ console.log(`Control socket: ${payload.socketPath}`);
328
349
  console.log(`Task artifacts: ${payload.runsDir}`);
329
350
  console.log(`Claude settings: ${payload.claudeSettingsFile}`);
330
351
  console.log(`Approval rules: ${payload.homeApprovalRulesFile}`);
@@ -332,7 +353,8 @@ async function commandPaths(cwd, args) {
332
353
  }
333
354
  async function commandTask(cwd, args) {
334
355
  const { paths } = await requireSession(cwd);
335
- const session = await loadSessionRecord(paths);
356
+ const rpcSnapshot = await tryRpcSnapshot(paths);
357
+ const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
336
358
  const requestedAgent = getFlag(args, "--agent");
337
359
  const prompt = getGoal(args.filter((arg, index)=>arg !== "--agent" && args[index - 1] !== "--agent"));
338
360
  if (!prompt) {
@@ -345,14 +367,25 @@ async function commandTask(cwd, args) {
345
367
  reason: `User explicitly assigned the task to ${requestedAgent}.`,
346
368
  claimedPaths: extractPromptPathHints(prompt)
347
369
  } : await routeTask(prompt, session, paths);
348
- await appendCommand(paths, "enqueue", {
349
- owner: routeDecision.owner,
350
- prompt,
351
- routeReason: routeDecision.reason,
352
- claimedPaths: routeDecision.claimedPaths,
353
- routeStrategy: routeDecision.strategy,
354
- routeConfidence: routeDecision.confidence
355
- });
370
+ if (rpcSnapshot) {
371
+ await rpcEnqueueTask(paths, {
372
+ owner: routeDecision.owner,
373
+ prompt,
374
+ routeReason: routeDecision.reason,
375
+ claimedPaths: routeDecision.claimedPaths,
376
+ routeStrategy: routeDecision.strategy,
377
+ routeConfidence: routeDecision.confidence
378
+ });
379
+ } else {
380
+ await appendCommand(paths, "enqueue", {
381
+ owner: routeDecision.owner,
382
+ prompt,
383
+ routeReason: routeDecision.reason,
384
+ claimedPaths: routeDecision.claimedPaths,
385
+ routeStrategy: routeDecision.strategy,
386
+ routeConfidence: routeDecision.confidence
387
+ });
388
+ }
356
389
  await recordEvent(paths, session.id, "task.cli_enqueued", {
357
390
  owner: routeDecision.owner,
358
391
  prompt,
@@ -364,7 +397,8 @@ async function commandTask(cwd, args) {
364
397
  }
365
398
  async function commandTasks(cwd, args) {
366
399
  const { paths } = await requireSession(cwd);
367
- const session = await loadSessionRecord(paths);
400
+ const rpcSnapshot = await tryRpcSnapshot(paths);
401
+ const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
368
402
  const artifacts = await listTaskArtifacts(paths);
369
403
  const artifactMap = new Map(artifacts.map((artifact)=>[
370
404
  artifact.taskId,
@@ -411,12 +445,13 @@ function resolveRequestedTaskId(args, knownTaskIds) {
411
445
  }
412
446
  async function commandTaskOutput(cwd, args) {
413
447
  const { paths } = await requireSession(cwd);
414
- const session = await loadSessionRecord(paths);
448
+ const rpcSnapshot = await tryRpcSnapshot(paths);
449
+ const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
415
450
  const sortedTasks = [
416
451
  ...session.tasks
417
452
  ].sort((left, right)=>left.updatedAt.localeCompare(right.updatedAt));
418
453
  const taskId = resolveRequestedTaskId(args, sortedTasks.map((task)=>task.id));
419
- const artifact = await loadTaskArtifact(paths, taskId);
454
+ const artifact = rpcSnapshot ? await rpcTaskArtifact(paths, taskId) : await loadTaskArtifact(paths, taskId);
420
455
  if (!artifact) {
421
456
  throw new Error(`No task artifact found for ${taskId}.`);
422
457
  }
@@ -430,15 +465,65 @@ async function commandTaskOutput(cwd, args) {
430
465
  console.log(`Started: ${artifact.startedAt}`);
431
466
  console.log(`Finished: ${artifact.finishedAt}`);
432
467
  console.log(`Summary: ${artifact.summary ?? "-"}`);
468
+ console.log(`Route: ${artifact.routeReason ?? "-"}`);
469
+ console.log(`Claimed paths: ${artifact.claimedPaths.join(", ") || "-"}`);
433
470
  console.log(`Error: ${artifact.error ?? "-"}`);
471
+ console.log("Decision Replay:");
472
+ for (const line of artifact.decisionReplay){
473
+ console.log(line);
474
+ }
434
475
  console.log("Envelope:");
435
476
  console.log(JSON.stringify(artifact.envelope, null, 2));
436
477
  console.log("Raw Output:");
437
478
  console.log(artifact.rawOutput ?? "");
438
479
  }
480
+ async function commandDecisions(cwd, args) {
481
+ const { paths } = await requireSession(cwd);
482
+ const rpcSnapshot = await tryRpcSnapshot(paths);
483
+ const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
484
+ const limitArg = getFlag(args, "--limit");
485
+ const limit = limitArg ? Number(limitArg) : 20;
486
+ const decisions = [
487
+ ...session.decisions
488
+ ].sort((left, right)=>left.createdAt.localeCompare(right.createdAt)).slice(-Math.max(1, Number.isFinite(limit) ? limit : 20));
489
+ if (args.includes("--json")) {
490
+ console.log(JSON.stringify(decisions, null, 2));
491
+ return;
492
+ }
493
+ if (decisions.length === 0) {
494
+ console.log("No decisions recorded.");
495
+ return;
496
+ }
497
+ for (const decision of decisions){
498
+ console.log(`${decision.createdAt} | ${decision.kind} | ${decision.agent ?? "-"} | ${decision.summary}`);
499
+ console.log(` task: ${decision.taskId ?? "-"}`);
500
+ console.log(` detail: ${decision.detail}`);
501
+ }
502
+ }
503
+ async function commandClaims(cwd, args) {
504
+ const { paths } = await requireSession(cwd);
505
+ const rpcSnapshot = await tryRpcSnapshot(paths);
506
+ const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
507
+ const claims = args.includes("--all") ? session.pathClaims : session.pathClaims.filter((claim)=>claim.status === "active");
508
+ if (args.includes("--json")) {
509
+ console.log(JSON.stringify(claims, null, 2));
510
+ return;
511
+ }
512
+ if (claims.length === 0) {
513
+ console.log("No path claims recorded.");
514
+ return;
515
+ }
516
+ for (const claim of claims){
517
+ console.log(`${claim.id} | ${claim.agent} | ${claim.status} | ${claim.source} | ${claim.paths.join(", ") || "-"}`);
518
+ console.log(` task: ${claim.taskId}`);
519
+ console.log(` updated: ${claim.updatedAt}`);
520
+ console.log(` note: ${claim.note ?? "-"}`);
521
+ }
522
+ }
439
523
  async function commandApprovals(cwd, args) {
440
524
  const { paths } = await requireSession(cwd);
441
- const requests = await listApprovalRequests(paths, {
525
+ const rpcSnapshot = await tryRpcSnapshot(paths);
526
+ const requests = rpcSnapshot ? rpcSnapshot.approvals.filter((request)=>args.includes("--all") || request.status === "pending") : await listApprovalRequests(paths, {
442
527
  includeResolved: args.includes("--all")
443
528
  });
444
529
  if (args.includes("--json")) {
@@ -469,40 +554,54 @@ function resolveApprovalRequestId(requests, requested) {
469
554
  }
470
555
  async function commandResolveApproval(cwd, args, decision) {
471
556
  const { paths } = await requireSession(cwd);
472
- const requests = await listApprovalRequests(paths, {
557
+ const rpcSnapshot = await tryRpcSnapshot(paths);
558
+ const requests = rpcSnapshot?.approvals ?? await listApprovalRequests(paths, {
473
559
  includeResolved: true
474
560
  });
475
561
  const requestedId = args.find((arg)=>!arg.startsWith("--")) ?? "latest";
476
562
  const requestId = resolveApprovalRequestId(requests, requestedId);
477
563
  const remember = args.includes("--remember");
478
- const request = await resolveApprovalRequest(paths, requestId, decision, remember);
479
- const session = await loadSessionRecord(paths);
480
- addDecisionRecord(session, {
481
- kind: "approval",
482
- agent: request.agent,
483
- summary: `${decision === "allow" ? "Approved" : "Denied"} ${request.toolName}`,
484
- detail: request.summary,
485
- metadata: {
486
- requestId: request.id,
564
+ const request = requests.find((item)=>item.id === requestId);
565
+ if (!request) {
566
+ throw new Error(`Approval request ${requestId} not found.`);
567
+ }
568
+ if (rpcSnapshot) {
569
+ await rpcResolveApproval(paths, {
570
+ requestId,
571
+ decision,
572
+ remember
573
+ });
574
+ } else {
575
+ const resolved = await resolveApprovalRequest(paths, requestId, decision, remember);
576
+ const session = await loadSessionRecord(paths);
577
+ addDecisionRecord(session, {
578
+ kind: "approval",
579
+ agent: resolved.agent,
580
+ summary: `${decision === "allow" ? "Approved" : "Denied"} ${resolved.toolName}`,
581
+ detail: resolved.summary,
582
+ metadata: {
583
+ requestId: resolved.id,
584
+ remember,
585
+ toolName: resolved.toolName
586
+ }
587
+ });
588
+ await saveSessionRecord(paths, session);
589
+ await recordEvent(paths, session.id, "approval.resolved", {
590
+ requestId: resolved.id,
591
+ decision,
487
592
  remember,
488
- toolName: request.toolName
489
- }
490
- });
491
- await saveSessionRecord(paths, session);
492
- await recordEvent(paths, session.id, "approval.resolved", {
493
- requestId: request.id,
494
- decision,
495
- remember,
496
- agent: request.agent,
497
- toolName: request.toolName
498
- });
593
+ agent: resolved.agent,
594
+ toolName: resolved.toolName
595
+ });
596
+ }
499
597
  console.log(`${decision === "allow" ? "Approved" : "Denied"} ${request.id}: ${request.summary}${remember ? " (remembered)" : ""}`);
500
598
  }
501
599
  async function commandEvents(cwd, args) {
502
600
  const { paths } = await requireSession(cwd);
503
601
  const limitArg = getFlag(args, "--limit");
504
602
  const limit = limitArg ? Number(limitArg) : 20;
505
- const events = await readRecentEvents(paths, Number.isFinite(limit) ? limit : 20);
603
+ const rpcSnapshot = await tryRpcSnapshot(paths);
604
+ const events = rpcSnapshot ? await rpcRecentEvents(paths, Number.isFinite(limit) ? limit : 20) : await readRecentEvents(paths, Number.isFinite(limit) ? limit : 20);
506
605
  for (const event of events){
507
606
  console.log(`${event.timestamp} ${event.type} ${JSON.stringify(event.payload)}`);
508
607
  }
@@ -517,8 +616,12 @@ async function commandStop(cwd) {
517
616
  console.log(`Marked stale Kavi session ${session.id} as stopped`);
518
617
  return;
519
618
  }
520
- await appendCommand(paths, "shutdown", {});
521
- await recordEvent(paths, session.id, "daemon.stop_requested", {});
619
+ if (await pingRpc(paths)) {
620
+ await rpcShutdown(paths);
621
+ } else {
622
+ await appendCommand(paths, "shutdown", {});
623
+ await recordEvent(paths, session.id, "daemon.stop_requested", {});
624
+ }
522
625
  await waitForSession(paths, "stopped");
523
626
  console.log(`Stopped Kavi session ${session.id}`);
524
627
  }
@@ -618,6 +721,7 @@ async function commandHook(args) {
618
721
  toolName: descriptor.toolName,
619
722
  summary: descriptor.summary
620
723
  });
724
+ await notifyOperatorSurface(paths, "approval.auto_allowed");
621
725
  console.log(JSON.stringify({
622
726
  continue: true,
623
727
  suppressOutput: true,
@@ -642,6 +746,7 @@ async function commandHook(args) {
642
746
  decision: rule.decision,
643
747
  summary: descriptor.summary
644
748
  });
749
+ await notifyOperatorSurface(paths, "approval.auto_decided");
645
750
  console.log(JSON.stringify({
646
751
  continue: true,
647
752
  suppressOutput: true,
@@ -666,6 +771,7 @@ async function commandHook(args) {
666
771
  toolName: request.toolName,
667
772
  summary: request.summary
668
773
  });
774
+ await notifyOperatorSurface(paths, "approval.requested");
669
775
  const resolved = await waitForApprovalDecision(paths, request.id);
670
776
  const approved = resolved?.status === "approved";
671
777
  const denied = resolved?.status === "denied";
@@ -674,6 +780,7 @@ async function commandHook(args) {
674
780
  requestId: request.id,
675
781
  outcome: approved ? "approved" : denied ? "denied" : "expired"
676
782
  });
783
+ await notifyOperatorSurface(paths, "approval.completed");
677
784
  console.log(JSON.stringify({
678
785
  continue: true,
679
786
  suppressOutput: true,
@@ -687,6 +794,7 @@ async function commandHook(args) {
687
794
  }
688
795
  if (session) {
689
796
  await recordEvent(paths, session.id, "claude.hook", hookPayload);
797
+ await notifyOperatorSurface(paths, "claude.hook");
690
798
  }
691
799
  console.log(JSON.stringify({
692
800
  continue: true
@@ -731,6 +839,12 @@ async function main() {
731
839
  case "task-output":
732
840
  await commandTaskOutput(cwd, args);
733
841
  break;
842
+ case "decisions":
843
+ await commandDecisions(cwd, args);
844
+ break;
845
+ case "claims":
846
+ await commandClaims(cwd, args);
847
+ break;
734
848
  case "approvals":
735
849
  await commandApprovals(cwd, args);
736
850
  break;
package/dist/paths.js CHANGED
@@ -23,7 +23,7 @@ export function resolveAppPaths(repoRoot) {
23
23
  approvalsFile: path.join(stateDir, "approvals.json"),
24
24
  commandsFile: path.join(runtimeDir, "commands.jsonl"),
25
25
  claudeSettingsFile: path.join(runtimeDir, "claude.settings.json"),
26
- socketPath: path.join(runtimeDir, "kavid.sock"),
26
+ socketPath: path.join(homeStateDir, "sockets", `${safeRepoId}.sock`),
27
27
  homeConfigDir,
28
28
  homeConfigFile: path.join(homeConfigDir, "config.toml"),
29
29
  homeApprovalRulesFile: path.join(homeConfigDir, "approval-rules.json"),
package/dist/rpc.js ADDED
@@ -0,0 +1,226 @@
1
+ import net from "node:net";
2
+ const RPC_TIMEOUT_MS = 4_000;
3
+ function randomId() {
4
+ return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
5
+ }
6
+ function asObject(value) {
7
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
8
+ }
9
+ export async function sendRpcRequest(paths, method, params = {}) {
10
+ return await new Promise((resolve, reject)=>{
11
+ const socket = net.createConnection(paths.socketPath);
12
+ const requestId = randomId();
13
+ let buffer = "";
14
+ let settled = false;
15
+ const finish = (callback)=>{
16
+ if (settled) {
17
+ return;
18
+ }
19
+ settled = true;
20
+ socket.setTimeout(0);
21
+ socket.end();
22
+ callback();
23
+ };
24
+ socket.setEncoding("utf8");
25
+ socket.setTimeout(RPC_TIMEOUT_MS, ()=>{
26
+ finish(()=>reject(new Error(`RPC ${method} timed out after ${RPC_TIMEOUT_MS}ms.`)));
27
+ });
28
+ socket.on("connect", ()=>{
29
+ const payload = {
30
+ id: requestId,
31
+ method,
32
+ params
33
+ };
34
+ socket.write(`${JSON.stringify(payload)}\n`);
35
+ });
36
+ socket.on("data", (chunk)=>{
37
+ buffer += chunk;
38
+ while(true){
39
+ const newlineIndex = buffer.indexOf("\n");
40
+ if (newlineIndex === -1) {
41
+ return;
42
+ }
43
+ const line = buffer.slice(0, newlineIndex).trim();
44
+ buffer = buffer.slice(newlineIndex + 1);
45
+ if (!line) {
46
+ continue;
47
+ }
48
+ let response;
49
+ try {
50
+ response = JSON.parse(line);
51
+ } catch (error) {
52
+ finish(()=>{
53
+ reject(new Error(error instanceof Error ? error.message : `Unable to parse RPC response for ${method}.`));
54
+ });
55
+ return;
56
+ }
57
+ if (response.id !== requestId) {
58
+ continue;
59
+ }
60
+ if (response.error) {
61
+ finish(()=>reject(new Error(response.error?.message ?? `${method} failed.`)));
62
+ return;
63
+ }
64
+ finish(()=>resolve(response.result));
65
+ return;
66
+ }
67
+ });
68
+ socket.on("error", (error)=>{
69
+ finish(()=>reject(error));
70
+ });
71
+ socket.on("end", ()=>{
72
+ if (!settled) {
73
+ finish(()=>reject(new Error(`Socket closed before completing ${method}.`)));
74
+ }
75
+ });
76
+ });
77
+ }
78
+ export async function pingRpc(paths) {
79
+ try {
80
+ await sendRpcRequest(paths, "ping");
81
+ return true;
82
+ } catch {
83
+ return false;
84
+ }
85
+ }
86
+ export async function readSnapshot(paths) {
87
+ return await sendRpcRequest(paths, "snapshot");
88
+ }
89
+ export async function rpcKickoff(paths, prompt) {
90
+ await sendRpcRequest(paths, "kickoff", {
91
+ prompt
92
+ });
93
+ }
94
+ export async function rpcEnqueueTask(paths, params) {
95
+ await sendRpcRequest(paths, "enqueueTask", {
96
+ owner: params.owner,
97
+ prompt: params.prompt,
98
+ routeReason: params.routeReason,
99
+ claimedPaths: params.claimedPaths,
100
+ routeStrategy: params.routeStrategy,
101
+ routeConfidence: params.routeConfidence
102
+ });
103
+ }
104
+ export async function rpcResolveApproval(paths, params) {
105
+ await sendRpcRequest(paths, "resolveApproval", params);
106
+ }
107
+ export async function rpcShutdown(paths) {
108
+ await sendRpcRequest(paths, "shutdown");
109
+ }
110
+ export async function rpcNotifyExternalUpdate(paths, reason) {
111
+ await sendRpcRequest(paths, "notifyExternalUpdate", {
112
+ reason
113
+ });
114
+ }
115
+ export async function rpcTaskArtifact(paths, taskId) {
116
+ return await sendRpcRequest(paths, "taskArtifact", {
117
+ taskId
118
+ });
119
+ }
120
+ export async function rpcRecentEvents(paths, limit) {
121
+ return await sendRpcRequest(paths, "events", {
122
+ limit
123
+ });
124
+ }
125
+ export async function rpcWorktreeDiff(paths, agent, filePath) {
126
+ return await sendRpcRequest(paths, "worktreeDiff", {
127
+ agent,
128
+ filePath
129
+ });
130
+ }
131
+ export function subscribeSnapshotRpc(paths, handlers) {
132
+ const socket = net.createConnection(paths.socketPath);
133
+ const requestId = randomId();
134
+ let buffer = "";
135
+ let closed = false;
136
+ let connectedResolver = null;
137
+ let connectedRejecter = null;
138
+ const connected = new Promise((resolve, reject)=>{
139
+ connectedResolver = resolve;
140
+ connectedRejecter = reject;
141
+ });
142
+ const finishWithError = (error)=>{
143
+ handlers.onError?.(error);
144
+ if (connectedRejecter) {
145
+ connectedRejecter(error);
146
+ connectedRejecter = null;
147
+ connectedResolver = null;
148
+ }
149
+ };
150
+ socket.setEncoding("utf8");
151
+ socket.setTimeout(RPC_TIMEOUT_MS, ()=>{
152
+ finishWithError(new Error(`Subscription timed out after ${RPC_TIMEOUT_MS}ms.`));
153
+ socket.end();
154
+ });
155
+ socket.on("connect", ()=>{
156
+ const payload = {
157
+ id: requestId,
158
+ method: "subscribe"
159
+ };
160
+ socket.write(`${JSON.stringify(payload)}\n`);
161
+ });
162
+ socket.on("data", (chunk)=>{
163
+ buffer += chunk;
164
+ while(true){
165
+ const newlineIndex = buffer.indexOf("\n");
166
+ if (newlineIndex === -1) {
167
+ return;
168
+ }
169
+ const line = buffer.slice(0, newlineIndex).trim();
170
+ buffer = buffer.slice(newlineIndex + 1);
171
+ if (!line) {
172
+ continue;
173
+ }
174
+ let message;
175
+ try {
176
+ message = JSON.parse(line);
177
+ } catch (error) {
178
+ finishWithError(new Error(error instanceof Error ? error.message : "Unable to parse snapshot subscription payload."));
179
+ socket.end();
180
+ return;
181
+ }
182
+ if ("id" in message && message.id === requestId) {
183
+ if (message.error) {
184
+ finishWithError(new Error(message.error.message));
185
+ socket.end();
186
+ return;
187
+ }
188
+ socket.setTimeout(0);
189
+ connectedResolver?.();
190
+ connectedResolver = null;
191
+ connectedRejecter = null;
192
+ handlers.onSnapshot({
193
+ reason: "subscribe",
194
+ snapshot: message.result
195
+ });
196
+ continue;
197
+ }
198
+ if ("method" in message && message.method === "snapshot.updated") {
199
+ handlers.onSnapshot(message.params);
200
+ }
201
+ }
202
+ });
203
+ socket.on("error", (error)=>{
204
+ if (!closed) {
205
+ finishWithError(error instanceof Error ? error : new Error(String(error)));
206
+ }
207
+ });
208
+ socket.on("close", ()=>{
209
+ if (!closed) {
210
+ handlers.onDisconnect?.();
211
+ }
212
+ });
213
+ return {
214
+ connected,
215
+ close: ()=>{
216
+ closed = true;
217
+ socket.end();
218
+ }
219
+ };
220
+ }
221
+ export function parseRpcParams(value) {
222
+ return asObject(value);
223
+ }
224
+
225
+
226
+ //# sourceURL=rpc.ts
@@ -4,6 +4,14 @@ import { ensureDir, fileExists, readJson, writeJson } from "./fs.js";
4
4
  function artifactPath(paths, taskId) {
5
5
  return path.join(paths.runsDir, `${taskId}.json`);
6
6
  }
7
+ function normalizeArtifact(artifact) {
8
+ return {
9
+ ...artifact,
10
+ routeReason: typeof artifact.routeReason === "string" ? artifact.routeReason : null,
11
+ claimedPaths: Array.isArray(artifact.claimedPaths) ? artifact.claimedPaths.map((item)=>String(item)) : [],
12
+ decisionReplay: Array.isArray(artifact.decisionReplay) ? artifact.decisionReplay.map((item)=>String(item)) : []
13
+ };
14
+ }
7
15
  export async function saveTaskArtifact(paths, artifact) {
8
16
  await ensureDir(paths.runsDir);
9
17
  await writeJson(artifactPath(paths, artifact.taskId), artifact);
@@ -13,7 +21,7 @@ export async function loadTaskArtifact(paths, taskId) {
13
21
  if (!await fileExists(filePath)) {
14
22
  return null;
15
23
  }
16
- return readJson(filePath);
24
+ return normalizeArtifact(await readJson(filePath));
17
25
  }
18
26
  export async function listTaskArtifacts(paths) {
19
27
  if (!await fileExists(paths.runsDir)) {
@@ -28,7 +36,7 @@ export async function listTaskArtifacts(paths) {
28
36
  continue;
29
37
  }
30
38
  const artifact = await readJson(path.join(paths.runsDir, entry.name));
31
- artifacts.push(artifact);
39
+ artifacts.push(normalizeArtifact(artifact));
32
40
  }
33
41
  return artifacts.sort((left, right)=>left.finishedAt.localeCompare(right.finishedAt));
34
42
  }