@agentrific/server 0.2.0 → 0.4.0

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/index.js CHANGED
@@ -455,6 +455,7 @@ var RunPreparationSchema = z2.object({
455
455
  var SessionSchema = z2.object({
456
456
  id: z2.string(),
457
457
  taskId: z2.string().nullable().optional(),
458
+ kind: z2.enum(["task", "repo_chat"]).optional(),
458
459
  projectId: z2.string(),
459
460
  title: z2.string(),
460
461
  status: SessionStatusSchema,
@@ -465,6 +466,11 @@ var SessionSchema = z2.object({
465
466
  gitFlow: GitFlowSchema.optional(),
466
467
  gitDelivery: GitDeliverySchema.optional(),
467
468
  preparation: RunPreparationSchema.optional(),
469
+ usage: z2.object({
470
+ tokens: z2.number().nonnegative(),
471
+ turns: z2.number().int().nonnegative(),
472
+ tools: z2.number().int().nonnegative()
473
+ }).optional(),
468
474
  createdAt: z2.string().datetime(),
469
475
  updatedAt: z2.string().datetime()
470
476
  });
@@ -528,6 +534,7 @@ var SessionEventSchema = z2.discriminatedUnion("type", [
528
534
  }),
529
535
  z2.object({
530
536
  type: z2.literal("message.completed"),
537
+ tokens: z2.number().nonnegative().optional(),
531
538
  model: ModelIdentitySchema.optional(),
532
539
  sessionId: z2.string(),
533
540
  messageId: z2.string().optional(),
@@ -863,7 +870,7 @@ var Attachments = class extends Context.Service()(
863
870
  };
864
871
 
865
872
  // src/index.ts
866
- import { randomUUID as randomUUID8 } from "node:crypto";
873
+ import { randomUUID as randomUUID9 } from "node:crypto";
867
874
  import { execFile as execFile2 } from "node:child_process";
868
875
  import { promisify as promisify2 } from "node:util";
869
876
  import { realpath as realpath2 } from "node:fs/promises";
@@ -1090,7 +1097,7 @@ async function startControl(directory, security2, status, maintenance2) {
1090
1097
  // package.json
1091
1098
  var package_default = {
1092
1099
  name: "@agentrific/server",
1093
- version: "0.2.0",
1100
+ version: "0.4.0",
1094
1101
  description: "Self-hosted control plane for long-running coding agents",
1095
1102
  keywords: [
1096
1103
  "agentrific",
@@ -1476,7 +1483,7 @@ var UpdateDiscovery = class {
1476
1483
  };
1477
1484
 
1478
1485
  // src/index.ts
1479
- import { Effect as Effect16, ManagedRuntime } from "effect";
1486
+ import { Effect as Effect17, ManagedRuntime } from "effect";
1480
1487
  import { z as z17 } from "zod";
1481
1488
 
1482
1489
  // src/automation.ts
@@ -1564,7 +1571,9 @@ async function boardTasks(store2, projectId) {
1564
1571
  status: taskStatusForSession(session.status, session)
1565
1572
  };
1566
1573
  });
1567
- const legacyTasks = sessions.filter((session) => !session.taskId && !linkedSessionIds.has(session.id)).filter((session) => !projectId || session.projectId === projectId).map(sessionTask);
1574
+ const legacyTasks = sessions.filter(
1575
+ (session) => session.kind !== "repo_chat" && !session.taskId && !linkedSessionIds.has(session.id)
1576
+ ).filter((session) => !projectId || session.projectId === projectId).map(sessionTask);
1568
1577
  return [...linkedTasks, ...legacyTasks].map((task) => TaskSchema.parse(task)).sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
1569
1578
  }
1570
1579
  async function findTask(store2, id) {
@@ -1783,13 +1792,29 @@ var AgentController = class {
1783
1792
  const entry = {
1784
1793
  session,
1785
1794
  agent,
1786
- unsubscribe: agent.subscribe(this.emit),
1795
+ unsubscribe: () => {
1796
+ },
1787
1797
  running: false,
1788
1798
  stopping: false,
1789
1799
  pending: []
1790
1800
  };
1801
+ entry.unsubscribe = agent.subscribe((event) => {
1802
+ this.emit(event);
1803
+ if (event.type !== "message.completed" && event.type !== "tool.started") return;
1804
+ const usage = entry.session.usage ?? { tokens: 0, turns: 0, tools: 0 };
1805
+ entry.session = {
1806
+ ...entry.session,
1807
+ usage: {
1808
+ tokens: usage.tokens + (event.type === "message.completed" ? event.tokens ?? 0 : 0),
1809
+ turns: usage.turns + (event.type === "message.completed" ? 1 : 0),
1810
+ tools: usage.tools + (event.type === "tool.started" ? 1 : 0)
1811
+ },
1812
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1813
+ };
1814
+ this.emit({ type: "session.updated", session: entry.session });
1815
+ });
1791
1816
  if (agent.model) {
1792
- entry.session = { ...session, model: agent.model };
1817
+ entry.session = { ...entry.session, model: agent.model };
1793
1818
  this.emit({ type: "session.updated", session: entry.session });
1794
1819
  }
1795
1820
  this.active.set(session.id, entry);
@@ -2981,6 +3006,7 @@ function createPiFactory(files, models2, delivery2, createSession = createAgentS
2981
3006
  sessionId,
2982
3007
  messageId: assistantMessageId,
2983
3008
  text,
3009
+ tokens: event.message.usage.totalTokens,
2984
3010
  model: models2.identity(event.message.provider, event.message.model)
2985
3011
  });
2986
3012
  assistantMessageId = void 0;
@@ -3004,7 +3030,8 @@ function createPiFactory(files, models2, delivery2, createSession = createAgentS
3004
3030
  sessionId,
3005
3031
  toolCallId: event.toolCallId,
3006
3032
  tool: event.toolName,
3007
- input: boundedJson(event.args, 1200),
3033
+ // Preserve complete Bash arguments so multiline commands can be inspected.
3034
+ input: event.toolName === "bash" ? JSON.stringify(event.args) : boundedJson(event.args, 1200),
3008
3035
  filePath: filePaths.get(event.toolCallId)
3009
3036
  });
3010
3037
  return true;
@@ -3711,6 +3738,38 @@ async function removeRunFiles(dataRoot2, sessionId) {
3711
3738
  await rm2(path8.join(dataRoot2, "pi", sessionId), { recursive: true, force: true });
3712
3739
  }
3713
3740
 
3741
+ // src/repo-chat.ts
3742
+ import { randomUUID as randomUUID8 } from "node:crypto";
3743
+ import { Effect as Effect16 } from "effect";
3744
+ function openRepoChat(store2, projectId, workspacePath, defaultModel, reset = false) {
3745
+ return withTaskLock(`repo-chat:${projectId}`, async () => {
3746
+ const existing = (await Effect16.runPromise(store2.list())).filter((session2) => session2.projectId === projectId && session2.kind === "repo_chat").sort((a, b) => b.createdAt.localeCompare(a.createdAt))[0];
3747
+ if (existing && !reset) return existing;
3748
+ if (existing?.status === "running")
3749
+ throw new Error("Wait for the current reply before resetting chat.");
3750
+ const model = await defaultModel();
3751
+ if (!model) throw new Error("Choose an available default model in Settings before chatting.");
3752
+ const now = new Date(
3753
+ Math.max(Date.now(), existing ? Date.parse(existing.createdAt) + 1 : 0)
3754
+ ).toISOString();
3755
+ const session = {
3756
+ id: randomUUID8(),
3757
+ projectId,
3758
+ kind: "repo_chat",
3759
+ title: workspacePath.split("/").at(-1) ?? projectId,
3760
+ status: "waiting_for_input",
3761
+ agent: "pi",
3762
+ model,
3763
+ branch: null,
3764
+ workspacePath,
3765
+ createdAt: now,
3766
+ updatedAt: now
3767
+ };
3768
+ await Effect16.runPromise(store2.append({ type: "session.updated", session }));
3769
+ return session;
3770
+ });
3771
+ }
3772
+
3714
3773
  // src/index.ts
3715
3774
  var host = process.env.AGENTRIFIC_HOST ?? "127.0.0.1";
3716
3775
  var port = Number(process.env.AGENTRIFIC_PORT ?? 4318);
@@ -3743,7 +3802,7 @@ var dataRoot = path9.resolve(process.env.AGENTRIFIC_DATA_DIR ?? "./data");
3743
3802
  var releaseLock = await acquireServerLock(dataRoot);
3744
3803
  var effects = ManagedRuntime.make(SessionStore.layer(path9.join(dataRoot, "sessions.sqlite")));
3745
3804
  var store = await effects.runPromise(SessionStore);
3746
- var security = new ServerSecurity(store, await Effect16.runPromise(store.serverIdentity()));
3805
+ var security = new ServerSecurity(store, await Effect17.runPromise(store.serverIdentity()));
3747
3806
  var testPushUrl = process.env.AGENTRIFIC_PUSH_TEST_URL;
3748
3807
  if (testPushUrl && (process.env.AGENTRIFIC_TEST_MODE !== "1" || !new URL(testPushUrl).hostname.endsWith(".ts.net") || new URL(testPushUrl).protocol !== "https:"))
3749
3808
  throw new Error("Test relay requires explicit test mode and Tailscale HTTPS.");
@@ -3766,10 +3825,10 @@ var attachmentEffects = ManagedRuntime.make(
3766
3825
  Attachments.layer(path9.join(dataRoot, "attachments"), store)
3767
3826
  );
3768
3827
  var attachments = await attachmentEffects.runPromise(Attachments);
3769
- await Effect16.runPromise(attachments.cleanup());
3828
+ await Effect17.runPromise(attachments.cleanup());
3770
3829
  var attachmentCleanup = setInterval(
3771
3830
  () => {
3772
- void Effect16.runPromise(attachments.cleanup()).catch((error) => app.log.error(error));
3831
+ void Effect17.runPromise(attachments.cleanup()).catch((error) => app.log.error(error));
3773
3832
  },
3774
3833
  60 * 60 * 1e3
3775
3834
  );
@@ -3780,14 +3839,14 @@ await app.register(multipart, {
3780
3839
  app.post(
3781
3840
  "/api/projects/:projectId/attachments",
3782
3841
  async (request, reply) => {
3783
- const root = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
3784
- const projects = await Effect16.runPromise(discoverProjects(root, store));
3842
+ const root = await Effect17.runPromise(store.projectsRoot(defaultProjectsRoot));
3843
+ const projects = await Effect17.runPromise(discoverProjects(root, store));
3785
3844
  if (!projects.some((project) => project.id === request.params.projectId && project.available))
3786
3845
  return reply.code(404).send({ error: "Project not found." });
3787
3846
  try {
3788
3847
  const file = await request.file();
3789
3848
  if (!file) return reply.code(400).send({ error: "Choose a file to upload." });
3790
- return reply.code(201).send(await Effect16.runPromise(attachments.upload(request.params.projectId, file)));
3849
+ return reply.code(201).send(await Effect17.runPromise(attachments.upload(request.params.projectId, file)));
3791
3850
  } catch (error) {
3792
3851
  return reply.code(400).send({ error: error instanceof Error ? error.message : "Upload failed." });
3793
3852
  }
@@ -3796,7 +3855,7 @@ app.post(
3796
3855
  app.get(
3797
3856
  "/api/projects/:projectId/attachments/:id/content",
3798
3857
  async (request, reply) => {
3799
- const attachment = await Effect16.runPromise(store.getAttachment(request.params.id));
3858
+ const attachment = await Effect17.runPromise(store.getAttachment(request.params.id));
3800
3859
  if (!attachment || attachment.projectId !== request.params.projectId)
3801
3860
  return reply.code(404).send({ error: "Attachment not found." });
3802
3861
  const preview = request.query.preview === "1" && attachment.kind === "image";
@@ -3810,7 +3869,7 @@ app.get(
3810
3869
  }
3811
3870
  );
3812
3871
  function publish(event, workflowUpdate = false) {
3813
- const previous = event.type === "session.updated" ? Effect16.runSync(store.get(event.session.id)) : void 0;
3872
+ const previous = event.type === "session.updated" ? Effect17.runSync(store.get(event.session.id)) : void 0;
3814
3873
  const emittedEvent = event.type === "session.updated" && previous && !workflowUpdate ? {
3815
3874
  type: "session.updated",
3816
3875
  session: {
@@ -3820,7 +3879,7 @@ function publish(event, workflowUpdate = false) {
3820
3879
  }
3821
3880
  } : event;
3822
3881
  const notification = emittedEvent.type === "session.updated" ? push.pending(emittedEvent.session, previous) : void 0;
3823
- const record = Effect16.runSync(store.append(emittedEvent, notification));
3882
+ const record = Effect17.runSync(store.append(emittedEvent, notification));
3824
3883
  const message = JSON.stringify({ type: "event", record });
3825
3884
  for (const client of security.sockets.keys()) {
3826
3885
  if (!(client instanceof WebSocket) || client.readyState !== WebSocket.OPEN) continue;
@@ -3833,8 +3892,8 @@ var delivery = new DeliveryCoordinator(store, {
3833
3892
  publish: (event) => publish(event, true),
3834
3893
  draft: deliveryWriter(models),
3835
3894
  projectPath: async (session) => {
3836
- const projectsRoot = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
3837
- const project = (await Effect16.runPromise(discoverProjects(projectsRoot, store))).find(
3895
+ const projectsRoot = await Effect17.runPromise(store.projectsRoot(defaultProjectsRoot));
3896
+ const project = (await Effect17.runPromise(discoverProjects(projectsRoot, store))).find(
3838
3897
  (item) => item.id === session.projectId && item.available
3839
3898
  );
3840
3899
  if (!project) throw new Error("Reconnect the project folder before delivering changes.");
@@ -3848,7 +3907,7 @@ var agentEffects = ManagedRuntime.make(
3848
3907
  publish,
3849
3908
  worktreesRoot,
3850
3909
  path9.join(dataRoot, "pi"),
3851
- (sessionId, messageId) => Effect16.runSync(store.hasMessage(sessionId, messageId)),
3910
+ (sessionId, messageId) => Effect17.runSync(store.hasMessage(sessionId, messageId)),
3852
3911
  (session) => delivery.settled(session.id)
3853
3912
  )
3854
3913
  );
@@ -3883,7 +3942,7 @@ var stopUpdateDiscovery = updates.start(
3883
3942
  () => maintenance.ready(),
3884
3943
  (error) => app.log.error(error)
3885
3944
  );
3886
- for (const session of await Effect16.runPromise(store.list())) {
3945
+ for (const session of await Effect17.runPromise(store.list())) {
3887
3946
  if (session.gitDelivery?.status === "pending") {
3888
3947
  publish(
3889
3948
  {
@@ -3927,7 +3986,7 @@ async function githubCliStatus() {
3927
3986
  }
3928
3987
  app.get("/api/connections", async () => githubCliStatus());
3929
3988
  app.get("/api/settings", async () => ({
3930
- projectsRoot: await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot)),
3989
+ projectsRoot: await Effect17.runPromise(store.projectsRoot(defaultProjectsRoot)),
3931
3990
  defaultModel: await models.defaultIdentity()
3932
3991
  }));
3933
3992
  app.get("/api/models", async (_request, reply) => {
@@ -3955,7 +4014,7 @@ app.post("/api/settings/model", async (request, reply) => {
3955
4014
  try {
3956
4015
  const defaultModel = await models.setDefault(parsed.data);
3957
4016
  return {
3958
- projectsRoot: await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot)),
4017
+ projectsRoot: await Effect17.runPromise(store.projectsRoot(defaultProjectsRoot)),
3959
4018
  defaultModel
3960
4019
  };
3961
4020
  } catch {
@@ -3968,13 +4027,13 @@ app.post(
3968
4027
  "/api/sessions/:id/model",
3969
4028
  async (request, reply) => withSessionTaskLock(request.params.id, async () => {
3970
4029
  const parsed = ModelReferenceSchema.safeParse(request.body);
3971
- const session = await Effect16.runPromise(store.get(request.params.id));
4030
+ const session = await Effect17.runPromise(store.get(request.params.id));
3972
4031
  if (!session) return reply.code(404).send({ error: "Session not found." });
3973
4032
  if (!parsed.success)
3974
4033
  return reply.code(400).send({ error: "Choose a model from the server catalogue." });
3975
4034
  try {
3976
4035
  await models.resolve(parsed.data);
3977
- return await Effect16.runPromise(runtime.setModel(session, parsed.data));
4036
+ return await Effect17.runPromise(runtime.setModel(session, parsed.data));
3978
4037
  } catch {
3979
4038
  return reply.code(409).send({
3980
4039
  error: "Could not change the model. Wait for the current reply to finish and choose an available model."
@@ -3985,21 +4044,21 @@ app.post(
3985
4044
  app.post("/api/settings", async (request, reply) => {
3986
4045
  const parsed = UpdateSettingsSchema.safeParse(request.body);
3987
4046
  if (!parsed.success) return reply.code(400).send({ error: "Enter a projects folder." });
3988
- const result = await Effect16.runPromise(
3989
- resolveProjectsRoot(parsed.data.projectsRoot).pipe(Effect16.result)
4047
+ const result = await Effect17.runPromise(
4048
+ resolveProjectsRoot(parsed.data.projectsRoot).pipe(Effect17.result)
3990
4049
  );
3991
4050
  if (result._tag === "Failure") return reply.code(400).send({ error: result.failure.message });
3992
- return Effect16.runPromise(store.setProjectsRoot(result.success));
4051
+ return Effect17.runPromise(store.setProjectsRoot(result.success));
3993
4052
  });
3994
4053
  app.get("/api/projects", async (_request, reply) => {
3995
- const root = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
3996
- const result = await Effect16.runPromise(discoverProjects(root, store).pipe(Effect16.result));
4054
+ const root = await Effect17.runPromise(store.projectsRoot(defaultProjectsRoot));
4055
+ const result = await Effect17.runPromise(discoverProjects(root, store).pipe(Effect17.result));
3997
4056
  if (result._tag === "Failure") return reply.code(400).send({ error: result.failure.message });
3998
4057
  return result.success;
3999
4058
  });
4000
4059
  app.get("/api/projects/:id", async (request, reply) => {
4001
- const root = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
4002
- const result = await Effect16.runPromise(discoverProjects(root, store).pipe(Effect16.result));
4060
+ const root = await Effect17.runPromise(store.projectsRoot(defaultProjectsRoot));
4061
+ const result = await Effect17.runPromise(discoverProjects(root, store).pipe(Effect17.result));
4003
4062
  if (result._tag === "Failure") return reply.code(400).send({ error: result.failure.message });
4004
4063
  const project = result.success.find((item) => item.id === request.params.id);
4005
4064
  return project ?? reply.code(404).send({ error: "Project not found on this server." });
@@ -4008,13 +4067,13 @@ app.post("/api/projects/:id/reconnect", async (request, reply) => {
4008
4067
  const parsed = z17.object({ path: z17.string().trim().min(1).max(4096) }).safeParse(request.body);
4009
4068
  if (!parsed.success)
4010
4069
  return reply.code(400).send({ error: "Enter the project's new folder path." });
4011
- const root = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
4012
- const result = await Effect16.runPromise(
4013
- reconnectProject(root, request.params.id, parsed.data.path, store).pipe(Effect16.result)
4070
+ const root = await Effect17.runPromise(store.projectsRoot(defaultProjectsRoot));
4071
+ const result = await Effect17.runPromise(
4072
+ reconnectProject(root, request.params.id, parsed.data.path, store).pipe(Effect17.result)
4014
4073
  );
4015
4074
  return result._tag === "Failure" ? reply.code(400).send({ error: result.failure.message }) : result.success;
4016
4075
  });
4017
- app.get("/api/sessions", () => Effect16.runPromise(store.list()));
4076
+ app.get("/api/sessions", () => Effect17.runPromise(store.list()));
4018
4077
  app.get(
4019
4078
  "/api/tasks",
4020
4079
  (request) => boardTasks(store, request.query.projectId)
@@ -4027,15 +4086,15 @@ app.post("/api/tasks", async (request, reply) => {
4027
4086
  if (!github.githubCli.authenticated)
4028
4087
  return reply.code(400).send({ error: "GitHub CLI must be authenticated on the server to open a PR." });
4029
4088
  }
4030
- const projectsRoot = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
4031
- const projects = await Effect16.runPromise(
4032
- discoverProjects(projectsRoot, store).pipe(Effect16.result)
4089
+ const projectsRoot = await Effect17.runPromise(store.projectsRoot(defaultProjectsRoot));
4090
+ const projects = await Effect17.runPromise(
4091
+ discoverProjects(projectsRoot, store).pipe(Effect17.result)
4033
4092
  );
4034
4093
  if (projects._tag === "Failure" || !projects.success.some((project) => project.id === parsed.data.projectId && project.available))
4035
4094
  return reply.code(404).send({ error: "Project not found in the server's projects folder." });
4036
4095
  let files;
4037
4096
  try {
4038
- files = await Effect16.runPromise(
4097
+ files = await Effect17.runPromise(
4039
4098
  attachments.resolve(parsed.data.projectId, parsed.data.attachmentIds ?? [])
4040
4099
  );
4041
4100
  } catch (error) {
@@ -4043,7 +4102,7 @@ app.post("/api/tasks", async (request, reply) => {
4043
4102
  }
4044
4103
  const now = (/* @__PURE__ */ new Date()).toISOString();
4045
4104
  const task = TaskSchema.parse({
4046
- id: randomUUID8(),
4105
+ id: randomUUID9(),
4047
4106
  ...parsed.data,
4048
4107
  attachments: files,
4049
4108
  status: "backlog",
@@ -4051,7 +4110,7 @@ app.post("/api/tasks", async (request, reply) => {
4051
4110
  createdAt: now,
4052
4111
  updatedAt: now
4053
4112
  });
4054
- return reply.code(201).send(await Effect16.runPromise(store.saveTask(task)));
4113
+ return reply.code(201).send(await Effect17.runPromise(store.saveTask(task)));
4055
4114
  });
4056
4115
  app.patch("/api/tasks/:id", async (request, reply) => {
4057
4116
  const parsed = UpdateTaskSchema.safeParse(request.body);
@@ -4066,8 +4125,8 @@ app.patch("/api/tasks/:id", async (request, reply) => {
4066
4125
  return reply.code(400).send({ error: "GitHub CLI must be authenticated on the server to open a PR." });
4067
4126
  }
4068
4127
  return await editTask(store, request.params.id, parsed.data, async (projectId, ids) => {
4069
- const result = await Effect16.runPromise(
4070
- attachments.resolve(projectId, ids).pipe(Effect16.result)
4128
+ const result = await Effect17.runPromise(
4129
+ attachments.resolve(projectId, ids).pipe(Effect17.result)
4071
4130
  );
4072
4131
  if (result._tag === "Failure") throw new TaskActionError(result.failure.message, 400);
4073
4132
  return result.success;
@@ -4081,11 +4140,11 @@ app.patch("/api/tasks/:id", async (request, reply) => {
4081
4140
  app.delete("/api/tasks/:id", async (request, reply) => {
4082
4141
  try {
4083
4142
  await deleteTask(store, request.params.id, async (session) => {
4084
- await Effect16.runPromise(runtime.forget(session.id));
4143
+ await Effect17.runPromise(runtime.forget(session.id));
4085
4144
  await worktreeCleanup.remove(session);
4086
4145
  await removeRunFiles(dataRoot, session.id);
4087
4146
  });
4088
- await Effect16.runPromise(attachments.cleanup());
4147
+ await Effect17.runPromise(attachments.cleanup());
4089
4148
  return reply.code(204).send();
4090
4149
  } catch (error) {
4091
4150
  if (error instanceof TaskActionError)
@@ -4094,14 +4153,14 @@ app.delete("/api/tasks/:id", async (request, reply) => {
4094
4153
  }
4095
4154
  });
4096
4155
  app.get("/api/sessions/:id", async (request, reply) => {
4097
- const session = await Effect16.runPromise(store.get(request.params.id));
4156
+ const session = await Effect17.runPromise(store.get(request.params.id));
4098
4157
  if (!session) return reply.code(404).send({ error: "Session not found." });
4099
4158
  return session;
4100
4159
  });
4101
4160
  app.post(
4102
4161
  "/api/sessions/:id/delivery",
4103
4162
  async (request, reply) => withSessionTaskLock(request.params.id, async () => {
4104
- const session = await Effect16.runPromise(store.get(request.params.id));
4163
+ const session = await Effect17.runPromise(store.get(request.params.id));
4105
4164
  if (!session) return reply.code(404).send({ error: "Session not found." });
4106
4165
  try {
4107
4166
  const result = await delivery.retry(session.id);
@@ -4118,10 +4177,10 @@ app.post(
4118
4177
  try {
4119
4178
  const prepared = await preparation.retry(request.params.id);
4120
4179
  preparedSession = prepared.session;
4121
- const files = await Effect16.runPromise(
4180
+ const files = await Effect17.runPromise(
4122
4181
  attachments.resolve(prepared.session.projectId, prepared.input.attachmentIds ?? [])
4123
4182
  );
4124
- await Effect16.runPromise(runtime.send(prepared.session, prepared.input.prompt, files));
4183
+ await Effect17.runPromise(runtime.send(prepared.session, prepared.input.prompt, files));
4125
4184
  return reply.code(202).send(prepared.session);
4126
4185
  } catch (error) {
4127
4186
  if (preparedSession) {
@@ -4144,9 +4203,9 @@ var HistoryQuery = z17.object({
4144
4203
  app.get("/api/sessions/:id/events", async (request, reply) => {
4145
4204
  const query = HistoryQuery.safeParse(request.query);
4146
4205
  if (!query.success) return reply.code(400).send({ error: "Invalid history cursor or limit." });
4147
- const session = await Effect16.runPromise(store.get(request.params.id));
4206
+ const session = await Effect17.runPromise(store.get(request.params.id));
4148
4207
  if (!session) return reply.code(404).send({ error: "Session not found." });
4149
- return Effect16.runPromise(store.history(session.id, query.data.after, query.data.limit));
4208
+ return Effect17.runPromise(store.history(session.id, query.data.after, query.data.limit));
4150
4209
  });
4151
4210
  async function selectSessionModel(input, hasImages) {
4152
4211
  try {
@@ -4166,8 +4225,8 @@ async function selectSessionModel(input, hasImages) {
4166
4225
  }
4167
4226
  }
4168
4227
  async function resolveSessionProject(projectId) {
4169
- const projectsRoot = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
4170
- const project = (await Effect16.runPromise(discoverProjects(projectsRoot, store))).find(
4228
+ const projectsRoot = await Effect17.runPromise(store.projectsRoot(defaultProjectsRoot));
4229
+ const project = (await Effect17.runPromise(discoverProjects(projectsRoot, store))).find(
4171
4230
  (item) => item.id === projectId && item.available
4172
4231
  );
4173
4232
  if (!project)
@@ -4209,7 +4268,7 @@ async function createAndStartSession(input) {
4209
4268
  const projectPath = location.projectPath;
4210
4269
  let files;
4211
4270
  try {
4212
- files = await Effect16.runPromise(
4271
+ files = await Effect17.runPromise(
4213
4272
  attachments.resolve(input.projectId, input.attachmentIds ?? [])
4214
4273
  );
4215
4274
  } catch (error) {
@@ -4222,17 +4281,18 @@ async function createAndStartSession(input) {
4222
4281
  if (selection.error !== void 0) return { error: selection.error, status: selection.status };
4223
4282
  const selectedModel = selection.model;
4224
4283
  const now = (/* @__PURE__ */ new Date()).toISOString();
4225
- const task = input.taskId ? await Effect16.runPromise(store.getTask(input.taskId)) : void 0;
4284
+ const task = input.taskId ? await Effect17.runPromise(store.getTask(input.taskId)) : void 0;
4226
4285
  if (input.taskId && sessionTaskUnavailable(task, input.projectId))
4227
4286
  return { error: "Task not found.", status: 404 };
4228
4287
  const session = SessionSchema.parse({
4229
- id: randomUUID8(),
4288
+ id: randomUUID9(),
4230
4289
  taskId: input.taskId ?? null,
4231
4290
  projectId: input.projectId,
4232
4291
  title: task?.title ?? input.prompt.slice(0, 80),
4233
4292
  status: "starting",
4234
4293
  agent: input.agent,
4235
4294
  model: selectedModel,
4295
+ usage: { tokens: 0, turns: 0, tools: 0 },
4236
4296
  gitFlow: input.gitFlow,
4237
4297
  branch: null,
4238
4298
  createdAt: now,
@@ -4251,7 +4311,7 @@ async function createAndStartSession(input) {
4251
4311
  );
4252
4312
  } catch (error) {
4253
4313
  if (task)
4254
- await Effect16.runPromise(
4314
+ await Effect17.runPromise(
4255
4315
  store.saveTask({ ...task, sessionId: session.id, updatedAt: (/* @__PURE__ */ new Date()).toISOString() })
4256
4316
  );
4257
4317
  return {
@@ -4260,7 +4320,7 @@ async function createAndStartSession(input) {
4260
4320
  };
4261
4321
  }
4262
4322
  try {
4263
- await Effect16.runPromise(runtime.send(running, input.prompt, files));
4323
+ await Effect17.runPromise(runtime.send(running, input.prompt, files));
4264
4324
  } catch (error) {
4265
4325
  publish({
4266
4326
  type: "session.updated",
@@ -4269,8 +4329,27 @@ async function createAndStartSession(input) {
4269
4329
  publish({ type: "error", sessionId: session.id, message: String(error) });
4270
4330
  return { error: "Could not start the agent session.", status: 500 };
4271
4331
  }
4272
- return { session: await Effect16.runPromise(store.get(session.id)), status: 201 };
4332
+ return { session: await Effect17.runPromise(store.get(session.id)), status: 201 };
4273
4333
  }
4334
+ app.post("/api/projects/:id/chat", async (request, reply) => {
4335
+ maintenance.assertReady();
4336
+ const location = await resolveSessionProject(request.params.id);
4337
+ if (location.error !== void 0)
4338
+ return reply.code(location.status).send({ error: location.error });
4339
+ const parsed = z17.object({ reset: z17.boolean().optional() }).safeParse(request.body ?? {});
4340
+ if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
4341
+ try {
4342
+ return await openRepoChat(
4343
+ store,
4344
+ request.params.id,
4345
+ location.projectPath,
4346
+ () => models.defaultIdentity(),
4347
+ parsed.data.reset
4348
+ );
4349
+ } catch (error) {
4350
+ return reply.code(409).send({ error: String(error) });
4351
+ }
4352
+ });
4274
4353
  app.post("/api/sessions", async (request, reply) => {
4275
4354
  const parsed = CreateSessionSchema.safeParse(request.body);
4276
4355
  if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
@@ -4287,11 +4366,11 @@ function startTask(id, instructionsOverride, automated = false) {
4287
4366
  "agent-dispatch",
4288
4367
  () => withTaskLock(id, async () => {
4289
4368
  maintenance.assertReady();
4290
- const task = await Effect16.runPromise(store.getTask(id));
4369
+ const task = await Effect17.runPromise(store.getTask(id));
4291
4370
  if (!task || task.deletedAt) throw new TaskActionError("Task not found.", 404);
4292
4371
  if (automated) {
4293
- const state = await Effect16.runPromise(store.automation());
4294
- if (activeAgents(await Effect16.runPromise(store.list())) >= state.config.parallelism) return;
4372
+ const state = await Effect17.runPromise(store.automation());
4373
+ if (activeAgents(await Effect17.runPromise(store.list())) >= state.config.parallelism) return;
4295
4374
  const backlog = await boardTasks(store);
4296
4375
  if (!backlog.some((item) => item.id === id && item.status === "backlog")) return;
4297
4376
  }
@@ -4299,7 +4378,7 @@ function startTask(id, instructionsOverride, automated = false) {
4299
4378
  const prompt = instructions ? `${task.title}
4300
4379
 
4301
4380
  ${instructions}` : task.title;
4302
- const linkedSession = task.sessionId ? await Effect16.runPromise(store.get(task.sessionId)) : void 0;
4381
+ const linkedSession = task.sessionId ? await Effect17.runPromise(store.get(task.sessionId)) : void 0;
4303
4382
  if (linkedSession && (["starting", "running", "queued"].includes(linkedSession.status) || linkedSession.gitDelivery?.status === "pending"))
4304
4383
  throw new TaskActionError("This task already has a running agent.", 409);
4305
4384
  const result = await createAndStartSession({
@@ -4313,7 +4392,7 @@ ${instructions}` : task.title;
4313
4392
  });
4314
4393
  if (!result.session)
4315
4394
  throw new TaskActionError(result.error ?? "Could not start task.", result.status);
4316
- const updatedTask = await Effect16.runPromise(
4395
+ const updatedTask = await Effect17.runPromise(
4317
4396
  store.saveTask({
4318
4397
  ...task,
4319
4398
  status: taskStatusForSession(result.session.status, result.session),
@@ -4348,8 +4427,8 @@ app.patch("/api/automation", async (request, reply) => {
4348
4427
  const parsed = AutomationConfigSchema.safeParse(request.body);
4349
4428
  if (!parsed.success)
4350
4429
  return reply.code(400).send({ error: parsed.error.issues.map((issue) => issue.message).join(" ") });
4351
- const root = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
4352
- const projects = await Effect16.runPromise(discoverProjects(root, store));
4430
+ const root = await Effect17.runPromise(store.projectsRoot(defaultProjectsRoot));
4431
+ const projects = await Effect17.runPromise(discoverProjects(root, store));
4353
4432
  if (parsed.data.projectIds.some(
4354
4433
  (id) => !projects.some((project) => project.id === id && project.available)
4355
4434
  ))
@@ -4377,7 +4456,7 @@ automationTimer.unref();
4377
4456
  app.post(
4378
4457
  "/api/sessions/:id/messages",
4379
4458
  async (request, reply) => withSessionTaskLock(request.params.id, async () => {
4380
- const session = await Effect16.runPromise(store.get(request.params.id));
4459
+ const session = await Effect17.runPromise(store.get(request.params.id));
4381
4460
  if (!session) return reply.code(404).send({ error: "Session not found." });
4382
4461
  const parsed = SendMessageSchema.safeParse(request.body);
4383
4462
  if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
@@ -4385,10 +4464,10 @@ app.post(
4385
4464
  await delivery.assertCanWork(session.id);
4386
4465
  if (session.preparation && session.preparation.status !== "ready")
4387
4466
  throw new Error("Retry workspace preparation before sending a message.");
4388
- const files = await Effect16.runPromise(
4467
+ const files = await Effect17.runPromise(
4389
4468
  attachments.resolve(session.projectId, parsed.data.attachmentIds ?? [])
4390
4469
  );
4391
- await Effect16.runPromise(
4470
+ await Effect17.runPromise(
4392
4471
  runtime.send(
4393
4472
  session,
4394
4473
  parsed.data.message,
@@ -4404,22 +4483,22 @@ app.post(
4404
4483
  })
4405
4484
  );
4406
4485
  async function withSessionTaskLock(id, operation) {
4407
- const session = await Effect16.runPromise(store.get(id));
4486
+ const session = await Effect17.runPromise(store.get(id));
4408
4487
  const lockId = session ? await sessionTaskLockId(store, session) : `session-${id}`;
4409
4488
  return withTaskLock(lockId, operation);
4410
4489
  }
4411
4490
  app.post(
4412
4491
  "/api/sessions/:id/stop",
4413
4492
  async (request, reply) => withSessionTaskLock(request.params.id, async () => {
4414
- const session = await Effect16.runPromise(store.get(request.params.id));
4493
+ const session = await Effect17.runPromise(store.get(request.params.id));
4415
4494
  if (!session) return reply.code(404).send({ error: "Session not found." });
4416
- await Effect16.runPromise(runtime.stop(session));
4495
+ await Effect17.runPromise(runtime.stop(session));
4417
4496
  return reply.code(202).send({ accepted: true });
4418
4497
  })
4419
4498
  );
4420
4499
  attachEventServer(app, security, allowedOrigins, () => ({
4421
4500
  type: "snapshot",
4422
- sessions: Effect16.runSync(store.list())
4501
+ sessions: Effect17.runSync(store.list())
4423
4502
  }));
4424
4503
  var closeControl = await startControl(
4425
4504
  dataRoot,
@@ -4431,7 +4510,7 @@ var closeControl = await startControl(
4431
4510
  connectedDevices: new Set(security.sockets.values()).size,
4432
4511
  connectedSockets: security.sockets.size,
4433
4512
  memory: process.memoryUsage().rss,
4434
- activeRuns: activeAgents(Effect16.runSync(store.list())),
4513
+ activeRuns: activeAgents(Effect17.runSync(store.list())),
4435
4514
  maintenance: maintenance.status()
4436
4515
  }),
4437
4516
  maintenance
@@ -4449,7 +4528,7 @@ app.addHook("onClose", async () => {
4449
4528
  clearInterval(automationTimer);
4450
4529
  await withTaskLock("automation", async () => {
4451
4530
  });
4452
- await Effect16.runPromise(runtime.close());
4531
+ await Effect17.runPromise(runtime.close());
4453
4532
  await agentEffects.dispose();
4454
4533
  await attachmentEffects.dispose();
4455
4534
  await effects.dispose();