@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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
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
+
9
+ ## 0.3.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 66aa463: Improve task-board navigation and display accumulated run usage. Add clearer Bash
14
+ command details, highlighted read-file previews and better native sheet sizing.
15
+ Server events now preserve complete Bash command inputs and report session usage.
16
+
3
17
  ## 0.2.0
4
18
 
5
19
  ### 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.2.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,
@@ -355,6 +356,11 @@ var init_src = __esm({
355
356
  gitFlow: GitFlowSchema.optional(),
356
357
  gitDelivery: GitDeliverySchema.optional(),
357
358
  preparation: RunPreparationSchema.optional(),
359
+ usage: z2.object({
360
+ tokens: z2.number().nonnegative(),
361
+ turns: z2.number().int().nonnegative(),
362
+ tools: z2.number().int().nonnegative()
363
+ }).optional(),
358
364
  createdAt: z2.string().datetime(),
359
365
  updatedAt: z2.string().datetime()
360
366
  });
@@ -418,6 +424,7 @@ var init_src = __esm({
418
424
  }),
419
425
  z2.object({
420
426
  type: z2.literal("message.completed"),
427
+ tokens: z2.number().nonnegative().optional(),
421
428
  model: ModelIdentitySchema.optional(),
422
429
  sessionId: z2.string(),
423
430
  messageId: z2.string().optional(),
@@ -1827,7 +1834,9 @@ async function boardTasks(store2, projectId) {
1827
1834
  status: taskStatusForSession(session.status, session)
1828
1835
  };
1829
1836
  });
1830
- 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);
1831
1840
  return [...linkedTasks, ...legacyTasks].map((task) => TaskSchema.parse(task)).sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
1832
1841
  }
1833
1842
  async function findTask(store2, id) {
@@ -2079,13 +2088,29 @@ var init_runtime = __esm({
2079
2088
  const entry = {
2080
2089
  session,
2081
2090
  agent,
2082
- unsubscribe: agent.subscribe(this.emit),
2091
+ unsubscribe: () => {
2092
+ },
2083
2093
  running: false,
2084
2094
  stopping: false,
2085
2095
  pending: []
2086
2096
  };
2097
+ entry.unsubscribe = agent.subscribe((event) => {
2098
+ this.emit(event);
2099
+ if (event.type !== "message.completed" && event.type !== "tool.started") return;
2100
+ const usage = entry.session.usage ?? { tokens: 0, turns: 0, tools: 0 };
2101
+ entry.session = {
2102
+ ...entry.session,
2103
+ usage: {
2104
+ tokens: usage.tokens + (event.type === "message.completed" ? event.tokens ?? 0 : 0),
2105
+ turns: usage.turns + (event.type === "message.completed" ? 1 : 0),
2106
+ tools: usage.tools + (event.type === "tool.started" ? 1 : 0)
2107
+ },
2108
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2109
+ };
2110
+ this.emit({ type: "session.updated", session: entry.session });
2111
+ });
2087
2112
  if (agent.model) {
2088
- entry.session = { ...session, model: agent.model };
2113
+ entry.session = { ...entry.session, model: agent.model };
2089
2114
  this.emit({ type: "session.updated", session: entry.session });
2090
2115
  }
2091
2116
  this.active.set(session.id, entry);
@@ -3316,6 +3341,7 @@ function createPiFactory(files, models2, delivery2, createSession = createAgentS
3316
3341
  sessionId,
3317
3342
  messageId: assistantMessageId,
3318
3343
  text,
3344
+ tokens: event.message.usage.totalTokens,
3319
3345
  model: models2.identity(event.message.provider, event.message.model)
3320
3346
  });
3321
3347
  assistantMessageId = void 0;
@@ -3339,7 +3365,8 @@ function createPiFactory(files, models2, delivery2, createSession = createAgentS
3339
3365
  sessionId,
3340
3366
  toolCallId: event.toolCallId,
3341
3367
  tool: event.toolName,
3342
- input: boundedJson(event.args, 1200),
3368
+ // Preserve complete Bash arguments so multiline commands can be inspected.
3369
+ input: event.toolName === "bash" ? JSON.stringify(event.args) : boundedJson(event.args, 1200),
3343
3370
  filePath: filePaths.get(event.toolCallId)
3344
3371
  });
3345
3372
  return true;
@@ -4097,6 +4124,44 @@ var init_worktree_cleanup = __esm({
4097
4124
  }
4098
4125
  });
4099
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
+
4100
4165
  // src/index.ts
4101
4166
  var index_exports = {};
4102
4167
  import Fastify from "fastify";
@@ -4104,18 +4169,18 @@ import pretty from "pino-pretty";
4104
4169
  import { styleText as styleText4 } from "node:util";
4105
4170
  import cors from "@fastify/cors";
4106
4171
  import multipart from "@fastify/multipart";
4107
- import { randomUUID as randomUUID8 } from "node:crypto";
4172
+ import { randomUUID as randomUUID9 } from "node:crypto";
4108
4173
  import { execFile as execFile3 } from "node:child_process";
4109
4174
  import { promisify as promisify3 } from "node:util";
4110
4175
  import { realpath as realpath2 } from "node:fs/promises";
4111
4176
  import { homedir as homedir4 } from "node:os";
4112
4177
  import path11 from "node:path";
4113
4178
  import { WebSocket } from "ws";
4114
- import { Effect as Effect16, ManagedRuntime } from "effect";
4179
+ import { Effect as Effect17, ManagedRuntime } from "effect";
4115
4180
  import { z as z18 } from "zod";
4116
4181
  import { ModelRuntime } from "@earendil-works/pi-coding-agent";
4117
4182
  function publish(event, workflowUpdate = false) {
4118
- 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;
4119
4184
  const emittedEvent = event.type === "session.updated" && previous && !workflowUpdate ? {
4120
4185
  type: "session.updated",
4121
4186
  session: {
@@ -4125,7 +4190,7 @@ function publish(event, workflowUpdate = false) {
4125
4190
  }
4126
4191
  } : event;
4127
4192
  const notification = emittedEvent.type === "session.updated" ? push.pending(emittedEvent.session, previous) : void 0;
4128
- const record = Effect16.runSync(store.append(emittedEvent, notification));
4193
+ const record = Effect17.runSync(store.append(emittedEvent, notification));
4129
4194
  const message = JSON.stringify({ type: "event", record });
4130
4195
  for (const client of security.sockets.keys()) {
4131
4196
  if (!(client instanceof WebSocket) || client.readyState !== WebSocket.OPEN) continue;
@@ -4165,8 +4230,8 @@ async function selectSessionModel(input, hasImages) {
4165
4230
  }
4166
4231
  }
4167
4232
  async function resolveSessionProject(projectId) {
4168
- const projectsRoot = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
4169
- 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(
4170
4235
  (item) => item.id === projectId && item.available
4171
4236
  );
4172
4237
  if (!project)
@@ -4208,7 +4273,7 @@ async function createAndStartSession(input) {
4208
4273
  const projectPath = location.projectPath;
4209
4274
  let files;
4210
4275
  try {
4211
- files = await Effect16.runPromise(
4276
+ files = await Effect17.runPromise(
4212
4277
  attachments.resolve(input.projectId, input.attachmentIds ?? [])
4213
4278
  );
4214
4279
  } catch (error) {
@@ -4221,17 +4286,18 @@ async function createAndStartSession(input) {
4221
4286
  if (selection.error !== void 0) return { error: selection.error, status: selection.status };
4222
4287
  const selectedModel = selection.model;
4223
4288
  const now = (/* @__PURE__ */ new Date()).toISOString();
4224
- 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;
4225
4290
  if (input.taskId && sessionTaskUnavailable(task, input.projectId))
4226
4291
  return { error: "Task not found.", status: 404 };
4227
4292
  const session = SessionSchema.parse({
4228
- id: randomUUID8(),
4293
+ id: randomUUID9(),
4229
4294
  taskId: input.taskId ?? null,
4230
4295
  projectId: input.projectId,
4231
4296
  title: task?.title ?? input.prompt.slice(0, 80),
4232
4297
  status: "starting",
4233
4298
  agent: input.agent,
4234
4299
  model: selectedModel,
4300
+ usage: { tokens: 0, turns: 0, tools: 0 },
4235
4301
  gitFlow: input.gitFlow,
4236
4302
  branch: null,
4237
4303
  createdAt: now,
@@ -4250,7 +4316,7 @@ async function createAndStartSession(input) {
4250
4316
  );
4251
4317
  } catch (error) {
4252
4318
  if (task)
4253
- await Effect16.runPromise(
4319
+ await Effect17.runPromise(
4254
4320
  store.saveTask({ ...task, sessionId: session.id, updatedAt: (/* @__PURE__ */ new Date()).toISOString() })
4255
4321
  );
4256
4322
  return {
@@ -4259,7 +4325,7 @@ async function createAndStartSession(input) {
4259
4325
  };
4260
4326
  }
4261
4327
  try {
4262
- await Effect16.runPromise(runtime.send(running, input.prompt, files));
4328
+ await Effect17.runPromise(runtime.send(running, input.prompt, files));
4263
4329
  } catch (error) {
4264
4330
  publish({
4265
4331
  type: "session.updated",
@@ -4268,18 +4334,18 @@ async function createAndStartSession(input) {
4268
4334
  publish({ type: "error", sessionId: session.id, message: String(error) });
4269
4335
  return { error: "Could not start the agent session.", status: 500 };
4270
4336
  }
4271
- return { session: await Effect16.runPromise(store.get(session.id)), status: 201 };
4337
+ return { session: await Effect17.runPromise(store.get(session.id)), status: 201 };
4272
4338
  }
4273
4339
  function startTask(id, instructionsOverride, automated = false) {
4274
4340
  return withTaskLock(
4275
4341
  "agent-dispatch",
4276
4342
  () => withTaskLock(id, async () => {
4277
4343
  maintenance.assertReady();
4278
- const task = await Effect16.runPromise(store.getTask(id));
4344
+ const task = await Effect17.runPromise(store.getTask(id));
4279
4345
  if (!task || task.deletedAt) throw new TaskActionError("Task not found.", 404);
4280
4346
  if (automated) {
4281
- const state = await Effect16.runPromise(store.automation());
4282
- 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;
4283
4349
  const backlog = await boardTasks(store);
4284
4350
  if (!backlog.some((item) => item.id === id && item.status === "backlog")) return;
4285
4351
  }
@@ -4287,7 +4353,7 @@ function startTask(id, instructionsOverride, automated = false) {
4287
4353
  const prompt = instructions ? `${task.title}
4288
4354
 
4289
4355
  ${instructions}` : task.title;
4290
- 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;
4291
4357
  if (linkedSession && (["starting", "running", "queued"].includes(linkedSession.status) || linkedSession.gitDelivery?.status === "pending"))
4292
4358
  throw new TaskActionError("This task already has a running agent.", 409);
4293
4359
  const result = await createAndStartSession({
@@ -4301,7 +4367,7 @@ ${instructions}` : task.title;
4301
4367
  });
4302
4368
  if (!result.session)
4303
4369
  throw new TaskActionError(result.error ?? "Could not start task.", result.status);
4304
- const updatedTask = await Effect16.runPromise(
4370
+ const updatedTask = await Effect17.runPromise(
4305
4371
  store.saveTask({
4306
4372
  ...task,
4307
4373
  status: taskStatusForSession(result.session.status, result.session),
@@ -4314,7 +4380,7 @@ ${instructions}` : task.title;
4314
4380
  );
4315
4381
  }
4316
4382
  async function withSessionTaskLock(id, operation) {
4317
- const session = await Effect16.runPromise(store.get(id));
4383
+ const session = await Effect17.runPromise(store.get(id));
4318
4384
  const lockId = session ? await sessionTaskLockId(store, session) : `session-${id}`;
4319
4385
  return withTaskLock(lockId, operation);
4320
4386
  }
@@ -4343,6 +4409,7 @@ var init_index = __esm({
4343
4409
  init_store();
4344
4410
  init_tasks();
4345
4411
  init_worktree_cleanup();
4412
+ init_repo_chat();
4346
4413
  host = process.env.AGENTRIFIC_HOST ?? "127.0.0.1";
4347
4414
  port = Number(process.env.AGENTRIFIC_PORT ?? 4318);
4348
4415
  defaultProjectsRoot = path11.resolve(process.env.AGENTRIFIC_PROJECTS_DIR ?? "./projects");
@@ -4374,7 +4441,7 @@ var init_index = __esm({
4374
4441
  releaseLock = await acquireServerLock(dataRoot);
4375
4442
  effects = ManagedRuntime.make(SessionStore.layer(path11.join(dataRoot, "sessions.sqlite")));
4376
4443
  store = await effects.runPromise(SessionStore);
4377
- security = new ServerSecurity(store, await Effect16.runPromise(store.serverIdentity()));
4444
+ security = new ServerSecurity(store, await Effect17.runPromise(store.serverIdentity()));
4378
4445
  testPushUrl = process.env.AGENTRIFIC_PUSH_TEST_URL;
4379
4446
  if (testPushUrl && (process.env.AGENTRIFIC_TEST_MODE !== "1" || !new URL(testPushUrl).hostname.endsWith(".ts.net") || new URL(testPushUrl).protocol !== "https:"))
4380
4447
  throw new Error("Test relay requires explicit test mode and Tailscale HTTPS.");
@@ -4397,10 +4464,10 @@ var init_index = __esm({
4397
4464
  Attachments.layer(path11.join(dataRoot, "attachments"), store)
4398
4465
  );
4399
4466
  attachments = await attachmentEffects.runPromise(Attachments);
4400
- await Effect16.runPromise(attachments.cleanup());
4467
+ await Effect17.runPromise(attachments.cleanup());
4401
4468
  attachmentCleanup = setInterval(
4402
4469
  () => {
4403
- void Effect16.runPromise(attachments.cleanup()).catch((error) => app.log.error(error));
4470
+ void Effect17.runPromise(attachments.cleanup()).catch((error) => app.log.error(error));
4404
4471
  },
4405
4472
  60 * 60 * 1e3
4406
4473
  );
@@ -4411,14 +4478,14 @@ var init_index = __esm({
4411
4478
  app.post(
4412
4479
  "/api/projects/:projectId/attachments",
4413
4480
  async (request, reply) => {
4414
- const root = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
4415
- 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));
4416
4483
  if (!projects.some((project) => project.id === request.params.projectId && project.available))
4417
4484
  return reply.code(404).send({ error: "Project not found." });
4418
4485
  try {
4419
4486
  const file = await request.file();
4420
4487
  if (!file) return reply.code(400).send({ error: "Choose a file to upload." });
4421
- 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)));
4422
4489
  } catch (error) {
4423
4490
  return reply.code(400).send({ error: error instanceof Error ? error.message : "Upload failed." });
4424
4491
  }
@@ -4427,7 +4494,7 @@ var init_index = __esm({
4427
4494
  app.get(
4428
4495
  "/api/projects/:projectId/attachments/:id/content",
4429
4496
  async (request, reply) => {
4430
- const attachment = await Effect16.runPromise(store.getAttachment(request.params.id));
4497
+ const attachment = await Effect17.runPromise(store.getAttachment(request.params.id));
4431
4498
  if (!attachment || attachment.projectId !== request.params.projectId)
4432
4499
  return reply.code(404).send({ error: "Attachment not found." });
4433
4500
  const preview = request.query.preview === "1" && attachment.kind === "image";
@@ -4445,8 +4512,8 @@ var init_index = __esm({
4445
4512
  publish: (event) => publish(event, true),
4446
4513
  draft: deliveryWriter(models),
4447
4514
  projectPath: async (session) => {
4448
- const projectsRoot = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
4449
- 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(
4450
4517
  (item) => item.id === session.projectId && item.available
4451
4518
  );
4452
4519
  if (!project) throw new Error("Reconnect the project folder before delivering changes.");
@@ -4460,7 +4527,7 @@ var init_index = __esm({
4460
4527
  publish,
4461
4528
  worktreesRoot,
4462
4529
  path11.join(dataRoot, "pi"),
4463
- (sessionId, messageId) => Effect16.runSync(store.hasMessage(sessionId, messageId)),
4530
+ (sessionId, messageId) => Effect17.runSync(store.hasMessage(sessionId, messageId)),
4464
4531
  (session) => delivery.settled(session.id)
4465
4532
  )
4466
4533
  );
@@ -4495,7 +4562,7 @@ var init_index = __esm({
4495
4562
  () => maintenance.ready(),
4496
4563
  (error) => app.log.error(error)
4497
4564
  );
4498
- for (const session of await Effect16.runPromise(store.list())) {
4565
+ for (const session of await Effect17.runPromise(store.list())) {
4499
4566
  if (session.gitDelivery?.status === "pending") {
4500
4567
  publish(
4501
4568
  {
@@ -4525,7 +4592,7 @@ var init_index = __esm({
4525
4592
  await delivery.recover();
4526
4593
  app.get("/api/connections", async () => githubCliStatus());
4527
4594
  app.get("/api/settings", async () => ({
4528
- projectsRoot: await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot)),
4595
+ projectsRoot: await Effect17.runPromise(store.projectsRoot(defaultProjectsRoot)),
4529
4596
  defaultModel: await models.defaultIdentity()
4530
4597
  }));
4531
4598
  app.get("/api/models", async (_request, reply) => {
@@ -4553,7 +4620,7 @@ var init_index = __esm({
4553
4620
  try {
4554
4621
  const defaultModel = await models.setDefault(parsed.data);
4555
4622
  return {
4556
- projectsRoot: await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot)),
4623
+ projectsRoot: await Effect17.runPromise(store.projectsRoot(defaultProjectsRoot)),
4557
4624
  defaultModel
4558
4625
  };
4559
4626
  } catch {
@@ -4566,13 +4633,13 @@ var init_index = __esm({
4566
4633
  "/api/sessions/:id/model",
4567
4634
  async (request, reply) => withSessionTaskLock(request.params.id, async () => {
4568
4635
  const parsed = ModelReferenceSchema.safeParse(request.body);
4569
- const session = await Effect16.runPromise(store.get(request.params.id));
4636
+ const session = await Effect17.runPromise(store.get(request.params.id));
4570
4637
  if (!session) return reply.code(404).send({ error: "Session not found." });
4571
4638
  if (!parsed.success)
4572
4639
  return reply.code(400).send({ error: "Choose a model from the server catalogue." });
4573
4640
  try {
4574
4641
  await models.resolve(parsed.data);
4575
- return await Effect16.runPromise(runtime.setModel(session, parsed.data));
4642
+ return await Effect17.runPromise(runtime.setModel(session, parsed.data));
4576
4643
  } catch {
4577
4644
  return reply.code(409).send({
4578
4645
  error: "Could not change the model. Wait for the current reply to finish and choose an available model."
@@ -4583,21 +4650,21 @@ var init_index = __esm({
4583
4650
  app.post("/api/settings", async (request, reply) => {
4584
4651
  const parsed = UpdateSettingsSchema.safeParse(request.body);
4585
4652
  if (!parsed.success) return reply.code(400).send({ error: "Enter a projects folder." });
4586
- const result = await Effect16.runPromise(
4587
- resolveProjectsRoot(parsed.data.projectsRoot).pipe(Effect16.result)
4653
+ const result = await Effect17.runPromise(
4654
+ resolveProjectsRoot(parsed.data.projectsRoot).pipe(Effect17.result)
4588
4655
  );
4589
4656
  if (result._tag === "Failure") return reply.code(400).send({ error: result.failure.message });
4590
- return Effect16.runPromise(store.setProjectsRoot(result.success));
4657
+ return Effect17.runPromise(store.setProjectsRoot(result.success));
4591
4658
  });
4592
4659
  app.get("/api/projects", async (_request, reply) => {
4593
- const root = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
4594
- 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));
4595
4662
  if (result._tag === "Failure") return reply.code(400).send({ error: result.failure.message });
4596
4663
  return result.success;
4597
4664
  });
4598
4665
  app.get("/api/projects/:id", async (request, reply) => {
4599
- const root = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
4600
- 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));
4601
4668
  if (result._tag === "Failure") return reply.code(400).send({ error: result.failure.message });
4602
4669
  const project = result.success.find((item) => item.id === request.params.id);
4603
4670
  return project ?? reply.code(404).send({ error: "Project not found on this server." });
@@ -4606,13 +4673,13 @@ var init_index = __esm({
4606
4673
  const parsed = z18.object({ path: z18.string().trim().min(1).max(4096) }).safeParse(request.body);
4607
4674
  if (!parsed.success)
4608
4675
  return reply.code(400).send({ error: "Enter the project's new folder path." });
4609
- const root = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
4610
- const result = await Effect16.runPromise(
4611
- 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)
4612
4679
  );
4613
4680
  return result._tag === "Failure" ? reply.code(400).send({ error: result.failure.message }) : result.success;
4614
4681
  });
4615
- app.get("/api/sessions", () => Effect16.runPromise(store.list()));
4682
+ app.get("/api/sessions", () => Effect17.runPromise(store.list()));
4616
4683
  app.get(
4617
4684
  "/api/tasks",
4618
4685
  (request) => boardTasks(store, request.query.projectId)
@@ -4625,15 +4692,15 @@ var init_index = __esm({
4625
4692
  if (!github.githubCli.authenticated)
4626
4693
  return reply.code(400).send({ error: "GitHub CLI must be authenticated on the server to open a PR." });
4627
4694
  }
4628
- const projectsRoot = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
4629
- const projects = await Effect16.runPromise(
4630
- 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)
4631
4698
  );
4632
4699
  if (projects._tag === "Failure" || !projects.success.some((project) => project.id === parsed.data.projectId && project.available))
4633
4700
  return reply.code(404).send({ error: "Project not found in the server's projects folder." });
4634
4701
  let files;
4635
4702
  try {
4636
- files = await Effect16.runPromise(
4703
+ files = await Effect17.runPromise(
4637
4704
  attachments.resolve(parsed.data.projectId, parsed.data.attachmentIds ?? [])
4638
4705
  );
4639
4706
  } catch (error) {
@@ -4641,7 +4708,7 @@ var init_index = __esm({
4641
4708
  }
4642
4709
  const now = (/* @__PURE__ */ new Date()).toISOString();
4643
4710
  const task = TaskSchema.parse({
4644
- id: randomUUID8(),
4711
+ id: randomUUID9(),
4645
4712
  ...parsed.data,
4646
4713
  attachments: files,
4647
4714
  status: "backlog",
@@ -4649,7 +4716,7 @@ var init_index = __esm({
4649
4716
  createdAt: now,
4650
4717
  updatedAt: now
4651
4718
  });
4652
- return reply.code(201).send(await Effect16.runPromise(store.saveTask(task)));
4719
+ return reply.code(201).send(await Effect17.runPromise(store.saveTask(task)));
4653
4720
  });
4654
4721
  app.patch("/api/tasks/:id", async (request, reply) => {
4655
4722
  const parsed = UpdateTaskSchema.safeParse(request.body);
@@ -4664,8 +4731,8 @@ var init_index = __esm({
4664
4731
  return reply.code(400).send({ error: "GitHub CLI must be authenticated on the server to open a PR." });
4665
4732
  }
4666
4733
  return await editTask(store, request.params.id, parsed.data, async (projectId, ids) => {
4667
- const result = await Effect16.runPromise(
4668
- attachments.resolve(projectId, ids).pipe(Effect16.result)
4734
+ const result = await Effect17.runPromise(
4735
+ attachments.resolve(projectId, ids).pipe(Effect17.result)
4669
4736
  );
4670
4737
  if (result._tag === "Failure") throw new TaskActionError(result.failure.message, 400);
4671
4738
  return result.success;
@@ -4679,11 +4746,11 @@ var init_index = __esm({
4679
4746
  app.delete("/api/tasks/:id", async (request, reply) => {
4680
4747
  try {
4681
4748
  await deleteTask(store, request.params.id, async (session) => {
4682
- await Effect16.runPromise(runtime.forget(session.id));
4749
+ await Effect17.runPromise(runtime.forget(session.id));
4683
4750
  await worktreeCleanup.remove(session);
4684
4751
  await removeRunFiles(dataRoot, session.id);
4685
4752
  });
4686
- await Effect16.runPromise(attachments.cleanup());
4753
+ await Effect17.runPromise(attachments.cleanup());
4687
4754
  return reply.code(204).send();
4688
4755
  } catch (error) {
4689
4756
  if (error instanceof TaskActionError)
@@ -4692,14 +4759,14 @@ var init_index = __esm({
4692
4759
  }
4693
4760
  });
4694
4761
  app.get("/api/sessions/:id", async (request, reply) => {
4695
- const session = await Effect16.runPromise(store.get(request.params.id));
4762
+ const session = await Effect17.runPromise(store.get(request.params.id));
4696
4763
  if (!session) return reply.code(404).send({ error: "Session not found." });
4697
4764
  return session;
4698
4765
  });
4699
4766
  app.post(
4700
4767
  "/api/sessions/:id/delivery",
4701
4768
  async (request, reply) => withSessionTaskLock(request.params.id, async () => {
4702
- const session = await Effect16.runPromise(store.get(request.params.id));
4769
+ const session = await Effect17.runPromise(store.get(request.params.id));
4703
4770
  if (!session) return reply.code(404).send({ error: "Session not found." });
4704
4771
  try {
4705
4772
  const result = await delivery.retry(session.id);
@@ -4716,10 +4783,10 @@ var init_index = __esm({
4716
4783
  try {
4717
4784
  const prepared = await preparation.retry(request.params.id);
4718
4785
  preparedSession = prepared.session;
4719
- const files = await Effect16.runPromise(
4786
+ const files = await Effect17.runPromise(
4720
4787
  attachments.resolve(prepared.session.projectId, prepared.input.attachmentIds ?? [])
4721
4788
  );
4722
- await Effect16.runPromise(runtime.send(prepared.session, prepared.input.prompt, files));
4789
+ await Effect17.runPromise(runtime.send(prepared.session, prepared.input.prompt, files));
4723
4790
  return reply.code(202).send(prepared.session);
4724
4791
  } catch (error) {
4725
4792
  if (preparedSession) {
@@ -4742,9 +4809,28 @@ var init_index = __esm({
4742
4809
  app.get("/api/sessions/:id/events", async (request, reply) => {
4743
4810
  const query = HistoryQuery.safeParse(request.query);
4744
4811
  if (!query.success) return reply.code(400).send({ error: "Invalid history cursor or limit." });
4745
- const session = await Effect16.runPromise(store.get(request.params.id));
4812
+ const session = await Effect17.runPromise(store.get(request.params.id));
4746
4813
  if (!session) return reply.code(404).send({ error: "Session not found." });
4747
- 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
+ }
4748
4834
  });
4749
4835
  app.post("/api/sessions", async (request, reply) => {
4750
4836
  const parsed = CreateSessionSchema.safeParse(request.body);
@@ -4780,8 +4866,8 @@ var init_index = __esm({
4780
4866
  const parsed = AutomationConfigSchema.safeParse(request.body);
4781
4867
  if (!parsed.success)
4782
4868
  return reply.code(400).send({ error: parsed.error.issues.map((issue) => issue.message).join(" ") });
4783
- const root = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
4784
- 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));
4785
4871
  if (parsed.data.projectIds.some(
4786
4872
  (id) => !projects.some((project) => project.id === id && project.available)
4787
4873
  ))
@@ -4809,7 +4895,7 @@ var init_index = __esm({
4809
4895
  app.post(
4810
4896
  "/api/sessions/:id/messages",
4811
4897
  async (request, reply) => withSessionTaskLock(request.params.id, async () => {
4812
- const session = await Effect16.runPromise(store.get(request.params.id));
4898
+ const session = await Effect17.runPromise(store.get(request.params.id));
4813
4899
  if (!session) return reply.code(404).send({ error: "Session not found." });
4814
4900
  const parsed = SendMessageSchema.safeParse(request.body);
4815
4901
  if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
@@ -4817,10 +4903,10 @@ var init_index = __esm({
4817
4903
  await delivery.assertCanWork(session.id);
4818
4904
  if (session.preparation && session.preparation.status !== "ready")
4819
4905
  throw new Error("Retry workspace preparation before sending a message.");
4820
- const files = await Effect16.runPromise(
4906
+ const files = await Effect17.runPromise(
4821
4907
  attachments.resolve(session.projectId, parsed.data.attachmentIds ?? [])
4822
4908
  );
4823
- await Effect16.runPromise(
4909
+ await Effect17.runPromise(
4824
4910
  runtime.send(
4825
4911
  session,
4826
4912
  parsed.data.message,
@@ -4838,15 +4924,15 @@ var init_index = __esm({
4838
4924
  app.post(
4839
4925
  "/api/sessions/:id/stop",
4840
4926
  async (request, reply) => withSessionTaskLock(request.params.id, async () => {
4841
- const session = await Effect16.runPromise(store.get(request.params.id));
4927
+ const session = await Effect17.runPromise(store.get(request.params.id));
4842
4928
  if (!session) return reply.code(404).send({ error: "Session not found." });
4843
- await Effect16.runPromise(runtime.stop(session));
4929
+ await Effect17.runPromise(runtime.stop(session));
4844
4930
  return reply.code(202).send({ accepted: true });
4845
4931
  })
4846
4932
  );
4847
4933
  attachEventServer(app, security, allowedOrigins, () => ({
4848
4934
  type: "snapshot",
4849
- sessions: Effect16.runSync(store.list())
4935
+ sessions: Effect17.runSync(store.list())
4850
4936
  }));
4851
4937
  closeControl = await startControl(
4852
4938
  dataRoot,
@@ -4858,7 +4944,7 @@ var init_index = __esm({
4858
4944
  connectedDevices: new Set(security.sockets.values()).size,
4859
4945
  connectedSockets: security.sockets.size,
4860
4946
  memory: process.memoryUsage().rss,
4861
- activeRuns: activeAgents(Effect16.runSync(store.list())),
4947
+ activeRuns: activeAgents(Effect17.runSync(store.list())),
4862
4948
  maintenance: maintenance.status()
4863
4949
  }),
4864
4950
  maintenance
@@ -4876,7 +4962,7 @@ var init_index = __esm({
4876
4962
  clearInterval(automationTimer);
4877
4963
  await withTaskLock("automation", async () => {
4878
4964
  });
4879
- await Effect16.runPromise(runtime.close());
4965
+ await Effect17.runPromise(runtime.close());
4880
4966
  await agentEffects.dispose();
4881
4967
  await attachmentEffects.dispose();
4882
4968
  await effects.dispose();