@agentrific/server 0.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @agentrific/server
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - d5dd726: Add persistent project chat with native Board and Chat tabs, project titles and a Reset action that starts a fresh conversation while preserving history. Correct native route parameters, header spacing and composer placement above the keyboard and Android tab bar.
8
+
3
9
  ## 0.3.0
4
10
 
5
11
  ### Minor Changes
package/cli.js CHANGED
@@ -30,7 +30,7 @@ var init_package = __esm({
30
30
  "package.json"() {
31
31
  package_default = {
32
32
  name: "@agentrific/server",
33
- version: "0.3.0",
33
+ version: "0.4.0",
34
34
  description: "Self-hosted control plane for long-running coding agents",
35
35
  keywords: [
36
36
  "agentrific",
@@ -345,6 +345,7 @@ var init_src = __esm({
345
345
  SessionSchema = z2.object({
346
346
  id: z2.string(),
347
347
  taskId: z2.string().nullable().optional(),
348
+ kind: z2.enum(["task", "repo_chat"]).optional(),
348
349
  projectId: z2.string(),
349
350
  title: z2.string(),
350
351
  status: SessionStatusSchema,
@@ -1833,7 +1834,9 @@ async function boardTasks(store2, projectId) {
1833
1834
  status: taskStatusForSession(session.status, session)
1834
1835
  };
1835
1836
  });
1836
- const legacyTasks = sessions.filter((session) => !session.taskId && !linkedSessionIds.has(session.id)).filter((session) => !projectId || session.projectId === projectId).map(sessionTask);
1837
+ const legacyTasks = sessions.filter(
1838
+ (session) => session.kind !== "repo_chat" && !session.taskId && !linkedSessionIds.has(session.id)
1839
+ ).filter((session) => !projectId || session.projectId === projectId).map(sessionTask);
1837
1840
  return [...linkedTasks, ...legacyTasks].map((task) => TaskSchema.parse(task)).sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
1838
1841
  }
1839
1842
  async function findTask(store2, id) {
@@ -4121,6 +4124,44 @@ var init_worktree_cleanup = __esm({
4121
4124
  }
4122
4125
  });
4123
4126
 
4127
+ // src/repo-chat.ts
4128
+ import { randomUUID as randomUUID8 } from "node:crypto";
4129
+ import { Effect as Effect16 } from "effect";
4130
+ function openRepoChat(store2, projectId, workspacePath, defaultModel, reset = false) {
4131
+ return withTaskLock(`repo-chat:${projectId}`, async () => {
4132
+ 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];
4133
+ if (existing && !reset) return existing;
4134
+ if (existing?.status === "running")
4135
+ throw new Error("Wait for the current reply before resetting chat.");
4136
+ const model = await defaultModel();
4137
+ if (!model) throw new Error("Choose an available default model in Settings before chatting.");
4138
+ const now = new Date(
4139
+ Math.max(Date.now(), existing ? Date.parse(existing.createdAt) + 1 : 0)
4140
+ ).toISOString();
4141
+ const session = {
4142
+ id: randomUUID8(),
4143
+ projectId,
4144
+ kind: "repo_chat",
4145
+ title: workspacePath.split("/").at(-1) ?? projectId,
4146
+ status: "waiting_for_input",
4147
+ agent: "pi",
4148
+ model,
4149
+ branch: null,
4150
+ workspacePath,
4151
+ createdAt: now,
4152
+ updatedAt: now
4153
+ };
4154
+ await Effect16.runPromise(store2.append({ type: "session.updated", session }));
4155
+ return session;
4156
+ });
4157
+ }
4158
+ var init_repo_chat = __esm({
4159
+ "src/repo-chat.ts"() {
4160
+ "use strict";
4161
+ init_tasks();
4162
+ }
4163
+ });
4164
+
4124
4165
  // src/index.ts
4125
4166
  var index_exports = {};
4126
4167
  import Fastify from "fastify";
@@ -4128,18 +4169,18 @@ import pretty from "pino-pretty";
4128
4169
  import { styleText as styleText4 } from "node:util";
4129
4170
  import cors from "@fastify/cors";
4130
4171
  import multipart from "@fastify/multipart";
4131
- import { randomUUID as randomUUID8 } from "node:crypto";
4172
+ import { randomUUID as randomUUID9 } from "node:crypto";
4132
4173
  import { execFile as execFile3 } from "node:child_process";
4133
4174
  import { promisify as promisify3 } from "node:util";
4134
4175
  import { realpath as realpath2 } from "node:fs/promises";
4135
4176
  import { homedir as homedir4 } from "node:os";
4136
4177
  import path11 from "node:path";
4137
4178
  import { WebSocket } from "ws";
4138
- import { Effect as Effect16, ManagedRuntime } from "effect";
4179
+ import { Effect as Effect17, ManagedRuntime } from "effect";
4139
4180
  import { z as z18 } from "zod";
4140
4181
  import { ModelRuntime } from "@earendil-works/pi-coding-agent";
4141
4182
  function publish(event, workflowUpdate = false) {
4142
- const previous = event.type === "session.updated" ? Effect16.runSync(store.get(event.session.id)) : void 0;
4183
+ const previous = event.type === "session.updated" ? Effect17.runSync(store.get(event.session.id)) : void 0;
4143
4184
  const emittedEvent = event.type === "session.updated" && previous && !workflowUpdate ? {
4144
4185
  type: "session.updated",
4145
4186
  session: {
@@ -4149,7 +4190,7 @@ function publish(event, workflowUpdate = false) {
4149
4190
  }
4150
4191
  } : event;
4151
4192
  const notification = emittedEvent.type === "session.updated" ? push.pending(emittedEvent.session, previous) : void 0;
4152
- const record = Effect16.runSync(store.append(emittedEvent, notification));
4193
+ const record = Effect17.runSync(store.append(emittedEvent, notification));
4153
4194
  const message = JSON.stringify({ type: "event", record });
4154
4195
  for (const client of security.sockets.keys()) {
4155
4196
  if (!(client instanceof WebSocket) || client.readyState !== WebSocket.OPEN) continue;
@@ -4189,8 +4230,8 @@ async function selectSessionModel(input, hasImages) {
4189
4230
  }
4190
4231
  }
4191
4232
  async function resolveSessionProject(projectId) {
4192
- const projectsRoot = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
4193
- const project = (await Effect16.runPromise(discoverProjects(projectsRoot, store))).find(
4233
+ const projectsRoot = await Effect17.runPromise(store.projectsRoot(defaultProjectsRoot));
4234
+ const project = (await Effect17.runPromise(discoverProjects(projectsRoot, store))).find(
4194
4235
  (item) => item.id === projectId && item.available
4195
4236
  );
4196
4237
  if (!project)
@@ -4232,7 +4273,7 @@ async function createAndStartSession(input) {
4232
4273
  const projectPath = location.projectPath;
4233
4274
  let files;
4234
4275
  try {
4235
- files = await Effect16.runPromise(
4276
+ files = await Effect17.runPromise(
4236
4277
  attachments.resolve(input.projectId, input.attachmentIds ?? [])
4237
4278
  );
4238
4279
  } catch (error) {
@@ -4245,11 +4286,11 @@ async function createAndStartSession(input) {
4245
4286
  if (selection.error !== void 0) return { error: selection.error, status: selection.status };
4246
4287
  const selectedModel = selection.model;
4247
4288
  const now = (/* @__PURE__ */ new Date()).toISOString();
4248
- const task = input.taskId ? await Effect16.runPromise(store.getTask(input.taskId)) : void 0;
4289
+ const task = input.taskId ? await Effect17.runPromise(store.getTask(input.taskId)) : void 0;
4249
4290
  if (input.taskId && sessionTaskUnavailable(task, input.projectId))
4250
4291
  return { error: "Task not found.", status: 404 };
4251
4292
  const session = SessionSchema.parse({
4252
- id: randomUUID8(),
4293
+ id: randomUUID9(),
4253
4294
  taskId: input.taskId ?? null,
4254
4295
  projectId: input.projectId,
4255
4296
  title: task?.title ?? input.prompt.slice(0, 80),
@@ -4275,7 +4316,7 @@ async function createAndStartSession(input) {
4275
4316
  );
4276
4317
  } catch (error) {
4277
4318
  if (task)
4278
- await Effect16.runPromise(
4319
+ await Effect17.runPromise(
4279
4320
  store.saveTask({ ...task, sessionId: session.id, updatedAt: (/* @__PURE__ */ new Date()).toISOString() })
4280
4321
  );
4281
4322
  return {
@@ -4284,7 +4325,7 @@ async function createAndStartSession(input) {
4284
4325
  };
4285
4326
  }
4286
4327
  try {
4287
- await Effect16.runPromise(runtime.send(running, input.prompt, files));
4328
+ await Effect17.runPromise(runtime.send(running, input.prompt, files));
4288
4329
  } catch (error) {
4289
4330
  publish({
4290
4331
  type: "session.updated",
@@ -4293,18 +4334,18 @@ async function createAndStartSession(input) {
4293
4334
  publish({ type: "error", sessionId: session.id, message: String(error) });
4294
4335
  return { error: "Could not start the agent session.", status: 500 };
4295
4336
  }
4296
- return { session: await Effect16.runPromise(store.get(session.id)), status: 201 };
4337
+ return { session: await Effect17.runPromise(store.get(session.id)), status: 201 };
4297
4338
  }
4298
4339
  function startTask(id, instructionsOverride, automated = false) {
4299
4340
  return withTaskLock(
4300
4341
  "agent-dispatch",
4301
4342
  () => withTaskLock(id, async () => {
4302
4343
  maintenance.assertReady();
4303
- const task = await Effect16.runPromise(store.getTask(id));
4344
+ const task = await Effect17.runPromise(store.getTask(id));
4304
4345
  if (!task || task.deletedAt) throw new TaskActionError("Task not found.", 404);
4305
4346
  if (automated) {
4306
- const state = await Effect16.runPromise(store.automation());
4307
- if (activeAgents(await Effect16.runPromise(store.list())) >= state.config.parallelism) return;
4347
+ const state = await Effect17.runPromise(store.automation());
4348
+ if (activeAgents(await Effect17.runPromise(store.list())) >= state.config.parallelism) return;
4308
4349
  const backlog = await boardTasks(store);
4309
4350
  if (!backlog.some((item) => item.id === id && item.status === "backlog")) return;
4310
4351
  }
@@ -4312,7 +4353,7 @@ function startTask(id, instructionsOverride, automated = false) {
4312
4353
  const prompt = instructions ? `${task.title}
4313
4354
 
4314
4355
  ${instructions}` : task.title;
4315
- const linkedSession = task.sessionId ? await Effect16.runPromise(store.get(task.sessionId)) : void 0;
4356
+ const linkedSession = task.sessionId ? await Effect17.runPromise(store.get(task.sessionId)) : void 0;
4316
4357
  if (linkedSession && (["starting", "running", "queued"].includes(linkedSession.status) || linkedSession.gitDelivery?.status === "pending"))
4317
4358
  throw new TaskActionError("This task already has a running agent.", 409);
4318
4359
  const result = await createAndStartSession({
@@ -4326,7 +4367,7 @@ ${instructions}` : task.title;
4326
4367
  });
4327
4368
  if (!result.session)
4328
4369
  throw new TaskActionError(result.error ?? "Could not start task.", result.status);
4329
- const updatedTask = await Effect16.runPromise(
4370
+ const updatedTask = await Effect17.runPromise(
4330
4371
  store.saveTask({
4331
4372
  ...task,
4332
4373
  status: taskStatusForSession(result.session.status, result.session),
@@ -4339,7 +4380,7 @@ ${instructions}` : task.title;
4339
4380
  );
4340
4381
  }
4341
4382
  async function withSessionTaskLock(id, operation) {
4342
- const session = await Effect16.runPromise(store.get(id));
4383
+ const session = await Effect17.runPromise(store.get(id));
4343
4384
  const lockId = session ? await sessionTaskLockId(store, session) : `session-${id}`;
4344
4385
  return withTaskLock(lockId, operation);
4345
4386
  }
@@ -4368,6 +4409,7 @@ var init_index = __esm({
4368
4409
  init_store();
4369
4410
  init_tasks();
4370
4411
  init_worktree_cleanup();
4412
+ init_repo_chat();
4371
4413
  host = process.env.AGENTRIFIC_HOST ?? "127.0.0.1";
4372
4414
  port = Number(process.env.AGENTRIFIC_PORT ?? 4318);
4373
4415
  defaultProjectsRoot = path11.resolve(process.env.AGENTRIFIC_PROJECTS_DIR ?? "./projects");
@@ -4399,7 +4441,7 @@ var init_index = __esm({
4399
4441
  releaseLock = await acquireServerLock(dataRoot);
4400
4442
  effects = ManagedRuntime.make(SessionStore.layer(path11.join(dataRoot, "sessions.sqlite")));
4401
4443
  store = await effects.runPromise(SessionStore);
4402
- security = new ServerSecurity(store, await Effect16.runPromise(store.serverIdentity()));
4444
+ security = new ServerSecurity(store, await Effect17.runPromise(store.serverIdentity()));
4403
4445
  testPushUrl = process.env.AGENTRIFIC_PUSH_TEST_URL;
4404
4446
  if (testPushUrl && (process.env.AGENTRIFIC_TEST_MODE !== "1" || !new URL(testPushUrl).hostname.endsWith(".ts.net") || new URL(testPushUrl).protocol !== "https:"))
4405
4447
  throw new Error("Test relay requires explicit test mode and Tailscale HTTPS.");
@@ -4422,10 +4464,10 @@ var init_index = __esm({
4422
4464
  Attachments.layer(path11.join(dataRoot, "attachments"), store)
4423
4465
  );
4424
4466
  attachments = await attachmentEffects.runPromise(Attachments);
4425
- await Effect16.runPromise(attachments.cleanup());
4467
+ await Effect17.runPromise(attachments.cleanup());
4426
4468
  attachmentCleanup = setInterval(
4427
4469
  () => {
4428
- void Effect16.runPromise(attachments.cleanup()).catch((error) => app.log.error(error));
4470
+ void Effect17.runPromise(attachments.cleanup()).catch((error) => app.log.error(error));
4429
4471
  },
4430
4472
  60 * 60 * 1e3
4431
4473
  );
@@ -4436,14 +4478,14 @@ var init_index = __esm({
4436
4478
  app.post(
4437
4479
  "/api/projects/:projectId/attachments",
4438
4480
  async (request, reply) => {
4439
- const root = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
4440
- const projects = await Effect16.runPromise(discoverProjects(root, store));
4481
+ const root = await Effect17.runPromise(store.projectsRoot(defaultProjectsRoot));
4482
+ const projects = await Effect17.runPromise(discoverProjects(root, store));
4441
4483
  if (!projects.some((project) => project.id === request.params.projectId && project.available))
4442
4484
  return reply.code(404).send({ error: "Project not found." });
4443
4485
  try {
4444
4486
  const file = await request.file();
4445
4487
  if (!file) return reply.code(400).send({ error: "Choose a file to upload." });
4446
- return reply.code(201).send(await Effect16.runPromise(attachments.upload(request.params.projectId, file)));
4488
+ return reply.code(201).send(await Effect17.runPromise(attachments.upload(request.params.projectId, file)));
4447
4489
  } catch (error) {
4448
4490
  return reply.code(400).send({ error: error instanceof Error ? error.message : "Upload failed." });
4449
4491
  }
@@ -4452,7 +4494,7 @@ var init_index = __esm({
4452
4494
  app.get(
4453
4495
  "/api/projects/:projectId/attachments/:id/content",
4454
4496
  async (request, reply) => {
4455
- const attachment = await Effect16.runPromise(store.getAttachment(request.params.id));
4497
+ const attachment = await Effect17.runPromise(store.getAttachment(request.params.id));
4456
4498
  if (!attachment || attachment.projectId !== request.params.projectId)
4457
4499
  return reply.code(404).send({ error: "Attachment not found." });
4458
4500
  const preview = request.query.preview === "1" && attachment.kind === "image";
@@ -4470,8 +4512,8 @@ var init_index = __esm({
4470
4512
  publish: (event) => publish(event, true),
4471
4513
  draft: deliveryWriter(models),
4472
4514
  projectPath: async (session) => {
4473
- const projectsRoot = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
4474
- const project = (await Effect16.runPromise(discoverProjects(projectsRoot, store))).find(
4515
+ const projectsRoot = await Effect17.runPromise(store.projectsRoot(defaultProjectsRoot));
4516
+ const project = (await Effect17.runPromise(discoverProjects(projectsRoot, store))).find(
4475
4517
  (item) => item.id === session.projectId && item.available
4476
4518
  );
4477
4519
  if (!project) throw new Error("Reconnect the project folder before delivering changes.");
@@ -4485,7 +4527,7 @@ var init_index = __esm({
4485
4527
  publish,
4486
4528
  worktreesRoot,
4487
4529
  path11.join(dataRoot, "pi"),
4488
- (sessionId, messageId) => Effect16.runSync(store.hasMessage(sessionId, messageId)),
4530
+ (sessionId, messageId) => Effect17.runSync(store.hasMessage(sessionId, messageId)),
4489
4531
  (session) => delivery.settled(session.id)
4490
4532
  )
4491
4533
  );
@@ -4520,7 +4562,7 @@ var init_index = __esm({
4520
4562
  () => maintenance.ready(),
4521
4563
  (error) => app.log.error(error)
4522
4564
  );
4523
- for (const session of await Effect16.runPromise(store.list())) {
4565
+ for (const session of await Effect17.runPromise(store.list())) {
4524
4566
  if (session.gitDelivery?.status === "pending") {
4525
4567
  publish(
4526
4568
  {
@@ -4550,7 +4592,7 @@ var init_index = __esm({
4550
4592
  await delivery.recover();
4551
4593
  app.get("/api/connections", async () => githubCliStatus());
4552
4594
  app.get("/api/settings", async () => ({
4553
- projectsRoot: await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot)),
4595
+ projectsRoot: await Effect17.runPromise(store.projectsRoot(defaultProjectsRoot)),
4554
4596
  defaultModel: await models.defaultIdentity()
4555
4597
  }));
4556
4598
  app.get("/api/models", async (_request, reply) => {
@@ -4578,7 +4620,7 @@ var init_index = __esm({
4578
4620
  try {
4579
4621
  const defaultModel = await models.setDefault(parsed.data);
4580
4622
  return {
4581
- projectsRoot: await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot)),
4623
+ projectsRoot: await Effect17.runPromise(store.projectsRoot(defaultProjectsRoot)),
4582
4624
  defaultModel
4583
4625
  };
4584
4626
  } catch {
@@ -4591,13 +4633,13 @@ var init_index = __esm({
4591
4633
  "/api/sessions/:id/model",
4592
4634
  async (request, reply) => withSessionTaskLock(request.params.id, async () => {
4593
4635
  const parsed = ModelReferenceSchema.safeParse(request.body);
4594
- const session = await Effect16.runPromise(store.get(request.params.id));
4636
+ const session = await Effect17.runPromise(store.get(request.params.id));
4595
4637
  if (!session) return reply.code(404).send({ error: "Session not found." });
4596
4638
  if (!parsed.success)
4597
4639
  return reply.code(400).send({ error: "Choose a model from the server catalogue." });
4598
4640
  try {
4599
4641
  await models.resolve(parsed.data);
4600
- return await Effect16.runPromise(runtime.setModel(session, parsed.data));
4642
+ return await Effect17.runPromise(runtime.setModel(session, parsed.data));
4601
4643
  } catch {
4602
4644
  return reply.code(409).send({
4603
4645
  error: "Could not change the model. Wait for the current reply to finish and choose an available model."
@@ -4608,21 +4650,21 @@ var init_index = __esm({
4608
4650
  app.post("/api/settings", async (request, reply) => {
4609
4651
  const parsed = UpdateSettingsSchema.safeParse(request.body);
4610
4652
  if (!parsed.success) return reply.code(400).send({ error: "Enter a projects folder." });
4611
- const result = await Effect16.runPromise(
4612
- resolveProjectsRoot(parsed.data.projectsRoot).pipe(Effect16.result)
4653
+ const result = await Effect17.runPromise(
4654
+ resolveProjectsRoot(parsed.data.projectsRoot).pipe(Effect17.result)
4613
4655
  );
4614
4656
  if (result._tag === "Failure") return reply.code(400).send({ error: result.failure.message });
4615
- return Effect16.runPromise(store.setProjectsRoot(result.success));
4657
+ return Effect17.runPromise(store.setProjectsRoot(result.success));
4616
4658
  });
4617
4659
  app.get("/api/projects", async (_request, reply) => {
4618
- const root = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
4619
- const result = await Effect16.runPromise(discoverProjects(root, store).pipe(Effect16.result));
4660
+ const root = await Effect17.runPromise(store.projectsRoot(defaultProjectsRoot));
4661
+ const result = await Effect17.runPromise(discoverProjects(root, store).pipe(Effect17.result));
4620
4662
  if (result._tag === "Failure") return reply.code(400).send({ error: result.failure.message });
4621
4663
  return result.success;
4622
4664
  });
4623
4665
  app.get("/api/projects/:id", async (request, reply) => {
4624
- const root = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
4625
- const result = await Effect16.runPromise(discoverProjects(root, store).pipe(Effect16.result));
4666
+ const root = await Effect17.runPromise(store.projectsRoot(defaultProjectsRoot));
4667
+ const result = await Effect17.runPromise(discoverProjects(root, store).pipe(Effect17.result));
4626
4668
  if (result._tag === "Failure") return reply.code(400).send({ error: result.failure.message });
4627
4669
  const project = result.success.find((item) => item.id === request.params.id);
4628
4670
  return project ?? reply.code(404).send({ error: "Project not found on this server." });
@@ -4631,13 +4673,13 @@ var init_index = __esm({
4631
4673
  const parsed = z18.object({ path: z18.string().trim().min(1).max(4096) }).safeParse(request.body);
4632
4674
  if (!parsed.success)
4633
4675
  return reply.code(400).send({ error: "Enter the project's new folder path." });
4634
- const root = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
4635
- const result = await Effect16.runPromise(
4636
- reconnectProject(root, request.params.id, parsed.data.path, store).pipe(Effect16.result)
4676
+ const root = await Effect17.runPromise(store.projectsRoot(defaultProjectsRoot));
4677
+ const result = await Effect17.runPromise(
4678
+ reconnectProject(root, request.params.id, parsed.data.path, store).pipe(Effect17.result)
4637
4679
  );
4638
4680
  return result._tag === "Failure" ? reply.code(400).send({ error: result.failure.message }) : result.success;
4639
4681
  });
4640
- app.get("/api/sessions", () => Effect16.runPromise(store.list()));
4682
+ app.get("/api/sessions", () => Effect17.runPromise(store.list()));
4641
4683
  app.get(
4642
4684
  "/api/tasks",
4643
4685
  (request) => boardTasks(store, request.query.projectId)
@@ -4650,15 +4692,15 @@ var init_index = __esm({
4650
4692
  if (!github.githubCli.authenticated)
4651
4693
  return reply.code(400).send({ error: "GitHub CLI must be authenticated on the server to open a PR." });
4652
4694
  }
4653
- const projectsRoot = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
4654
- const projects = await Effect16.runPromise(
4655
- discoverProjects(projectsRoot, store).pipe(Effect16.result)
4695
+ const projectsRoot = await Effect17.runPromise(store.projectsRoot(defaultProjectsRoot));
4696
+ const projects = await Effect17.runPromise(
4697
+ discoverProjects(projectsRoot, store).pipe(Effect17.result)
4656
4698
  );
4657
4699
  if (projects._tag === "Failure" || !projects.success.some((project) => project.id === parsed.data.projectId && project.available))
4658
4700
  return reply.code(404).send({ error: "Project not found in the server's projects folder." });
4659
4701
  let files;
4660
4702
  try {
4661
- files = await Effect16.runPromise(
4703
+ files = await Effect17.runPromise(
4662
4704
  attachments.resolve(parsed.data.projectId, parsed.data.attachmentIds ?? [])
4663
4705
  );
4664
4706
  } catch (error) {
@@ -4666,7 +4708,7 @@ var init_index = __esm({
4666
4708
  }
4667
4709
  const now = (/* @__PURE__ */ new Date()).toISOString();
4668
4710
  const task = TaskSchema.parse({
4669
- id: randomUUID8(),
4711
+ id: randomUUID9(),
4670
4712
  ...parsed.data,
4671
4713
  attachments: files,
4672
4714
  status: "backlog",
@@ -4674,7 +4716,7 @@ var init_index = __esm({
4674
4716
  createdAt: now,
4675
4717
  updatedAt: now
4676
4718
  });
4677
- return reply.code(201).send(await Effect16.runPromise(store.saveTask(task)));
4719
+ return reply.code(201).send(await Effect17.runPromise(store.saveTask(task)));
4678
4720
  });
4679
4721
  app.patch("/api/tasks/:id", async (request, reply) => {
4680
4722
  const parsed = UpdateTaskSchema.safeParse(request.body);
@@ -4689,8 +4731,8 @@ var init_index = __esm({
4689
4731
  return reply.code(400).send({ error: "GitHub CLI must be authenticated on the server to open a PR." });
4690
4732
  }
4691
4733
  return await editTask(store, request.params.id, parsed.data, async (projectId, ids) => {
4692
- const result = await Effect16.runPromise(
4693
- attachments.resolve(projectId, ids).pipe(Effect16.result)
4734
+ const result = await Effect17.runPromise(
4735
+ attachments.resolve(projectId, ids).pipe(Effect17.result)
4694
4736
  );
4695
4737
  if (result._tag === "Failure") throw new TaskActionError(result.failure.message, 400);
4696
4738
  return result.success;
@@ -4704,11 +4746,11 @@ var init_index = __esm({
4704
4746
  app.delete("/api/tasks/:id", async (request, reply) => {
4705
4747
  try {
4706
4748
  await deleteTask(store, request.params.id, async (session) => {
4707
- await Effect16.runPromise(runtime.forget(session.id));
4749
+ await Effect17.runPromise(runtime.forget(session.id));
4708
4750
  await worktreeCleanup.remove(session);
4709
4751
  await removeRunFiles(dataRoot, session.id);
4710
4752
  });
4711
- await Effect16.runPromise(attachments.cleanup());
4753
+ await Effect17.runPromise(attachments.cleanup());
4712
4754
  return reply.code(204).send();
4713
4755
  } catch (error) {
4714
4756
  if (error instanceof TaskActionError)
@@ -4717,14 +4759,14 @@ var init_index = __esm({
4717
4759
  }
4718
4760
  });
4719
4761
  app.get("/api/sessions/:id", async (request, reply) => {
4720
- const session = await Effect16.runPromise(store.get(request.params.id));
4762
+ const session = await Effect17.runPromise(store.get(request.params.id));
4721
4763
  if (!session) return reply.code(404).send({ error: "Session not found." });
4722
4764
  return session;
4723
4765
  });
4724
4766
  app.post(
4725
4767
  "/api/sessions/:id/delivery",
4726
4768
  async (request, reply) => withSessionTaskLock(request.params.id, async () => {
4727
- const session = await Effect16.runPromise(store.get(request.params.id));
4769
+ const session = await Effect17.runPromise(store.get(request.params.id));
4728
4770
  if (!session) return reply.code(404).send({ error: "Session not found." });
4729
4771
  try {
4730
4772
  const result = await delivery.retry(session.id);
@@ -4741,10 +4783,10 @@ var init_index = __esm({
4741
4783
  try {
4742
4784
  const prepared = await preparation.retry(request.params.id);
4743
4785
  preparedSession = prepared.session;
4744
- const files = await Effect16.runPromise(
4786
+ const files = await Effect17.runPromise(
4745
4787
  attachments.resolve(prepared.session.projectId, prepared.input.attachmentIds ?? [])
4746
4788
  );
4747
- await Effect16.runPromise(runtime.send(prepared.session, prepared.input.prompt, files));
4789
+ await Effect17.runPromise(runtime.send(prepared.session, prepared.input.prompt, files));
4748
4790
  return reply.code(202).send(prepared.session);
4749
4791
  } catch (error) {
4750
4792
  if (preparedSession) {
@@ -4767,9 +4809,28 @@ var init_index = __esm({
4767
4809
  app.get("/api/sessions/:id/events", async (request, reply) => {
4768
4810
  const query = HistoryQuery.safeParse(request.query);
4769
4811
  if (!query.success) return reply.code(400).send({ error: "Invalid history cursor or limit." });
4770
- const session = await Effect16.runPromise(store.get(request.params.id));
4812
+ const session = await Effect17.runPromise(store.get(request.params.id));
4771
4813
  if (!session) return reply.code(404).send({ error: "Session not found." });
4772
- return Effect16.runPromise(store.history(session.id, query.data.after, query.data.limit));
4814
+ return Effect17.runPromise(store.history(session.id, query.data.after, query.data.limit));
4815
+ });
4816
+ app.post("/api/projects/:id/chat", async (request, reply) => {
4817
+ maintenance.assertReady();
4818
+ const location = await resolveSessionProject(request.params.id);
4819
+ if (location.error !== void 0)
4820
+ return reply.code(location.status).send({ error: location.error });
4821
+ const parsed = z18.object({ reset: z18.boolean().optional() }).safeParse(request.body ?? {});
4822
+ if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
4823
+ try {
4824
+ return await openRepoChat(
4825
+ store,
4826
+ request.params.id,
4827
+ location.projectPath,
4828
+ () => models.defaultIdentity(),
4829
+ parsed.data.reset
4830
+ );
4831
+ } catch (error) {
4832
+ return reply.code(409).send({ error: String(error) });
4833
+ }
4773
4834
  });
4774
4835
  app.post("/api/sessions", async (request, reply) => {
4775
4836
  const parsed = CreateSessionSchema.safeParse(request.body);
@@ -4805,8 +4866,8 @@ var init_index = __esm({
4805
4866
  const parsed = AutomationConfigSchema.safeParse(request.body);
4806
4867
  if (!parsed.success)
4807
4868
  return reply.code(400).send({ error: parsed.error.issues.map((issue) => issue.message).join(" ") });
4808
- const root = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
4809
- const projects = await Effect16.runPromise(discoverProjects(root, store));
4869
+ const root = await Effect17.runPromise(store.projectsRoot(defaultProjectsRoot));
4870
+ const projects = await Effect17.runPromise(discoverProjects(root, store));
4810
4871
  if (parsed.data.projectIds.some(
4811
4872
  (id) => !projects.some((project) => project.id === id && project.available)
4812
4873
  ))
@@ -4834,7 +4895,7 @@ var init_index = __esm({
4834
4895
  app.post(
4835
4896
  "/api/sessions/:id/messages",
4836
4897
  async (request, reply) => withSessionTaskLock(request.params.id, async () => {
4837
- const session = await Effect16.runPromise(store.get(request.params.id));
4898
+ const session = await Effect17.runPromise(store.get(request.params.id));
4838
4899
  if (!session) return reply.code(404).send({ error: "Session not found." });
4839
4900
  const parsed = SendMessageSchema.safeParse(request.body);
4840
4901
  if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
@@ -4842,10 +4903,10 @@ var init_index = __esm({
4842
4903
  await delivery.assertCanWork(session.id);
4843
4904
  if (session.preparation && session.preparation.status !== "ready")
4844
4905
  throw new Error("Retry workspace preparation before sending a message.");
4845
- const files = await Effect16.runPromise(
4906
+ const files = await Effect17.runPromise(
4846
4907
  attachments.resolve(session.projectId, parsed.data.attachmentIds ?? [])
4847
4908
  );
4848
- await Effect16.runPromise(
4909
+ await Effect17.runPromise(
4849
4910
  runtime.send(
4850
4911
  session,
4851
4912
  parsed.data.message,
@@ -4863,15 +4924,15 @@ var init_index = __esm({
4863
4924
  app.post(
4864
4925
  "/api/sessions/:id/stop",
4865
4926
  async (request, reply) => withSessionTaskLock(request.params.id, async () => {
4866
- const session = await Effect16.runPromise(store.get(request.params.id));
4927
+ const session = await Effect17.runPromise(store.get(request.params.id));
4867
4928
  if (!session) return reply.code(404).send({ error: "Session not found." });
4868
- await Effect16.runPromise(runtime.stop(session));
4929
+ await Effect17.runPromise(runtime.stop(session));
4869
4930
  return reply.code(202).send({ accepted: true });
4870
4931
  })
4871
4932
  );
4872
4933
  attachEventServer(app, security, allowedOrigins, () => ({
4873
4934
  type: "snapshot",
4874
- sessions: Effect16.runSync(store.list())
4935
+ sessions: Effect17.runSync(store.list())
4875
4936
  }));
4876
4937
  closeControl = await startControl(
4877
4938
  dataRoot,
@@ -4883,7 +4944,7 @@ var init_index = __esm({
4883
4944
  connectedDevices: new Set(security.sockets.values()).size,
4884
4945
  connectedSockets: security.sockets.size,
4885
4946
  memory: process.memoryUsage().rss,
4886
- activeRuns: activeAgents(Effect16.runSync(store.list())),
4947
+ activeRuns: activeAgents(Effect17.runSync(store.list())),
4887
4948
  maintenance: maintenance.status()
4888
4949
  }),
4889
4950
  maintenance
@@ -4901,7 +4962,7 @@ var init_index = __esm({
4901
4962
  clearInterval(automationTimer);
4902
4963
  await withTaskLock("automation", async () => {
4903
4964
  });
4904
- await Effect16.runPromise(runtime.close());
4965
+ await Effect17.runPromise(runtime.close());
4905
4966
  await agentEffects.dispose();
4906
4967
  await attachmentEffects.dispose();
4907
4968
  await effects.dispose();