@agentrific/server 0.1.0 → 0.1.4

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
@@ -33,6 +33,9 @@ var cronBounds = [
33
33
  [1, 12],
34
34
  [0, 6]
35
35
  ];
36
+ function cronRange(base, min, max, stepped) {
37
+ return base === "*" ? [min, max] : base.includes("-") ? base.split("-").map(Number) : [Number(base), stepped ? max : Number(base)];
38
+ }
36
39
  function cronFields(expression) {
37
40
  const fields = expression.trim().split(/\s+/);
38
41
  if (fields.length !== 5)
@@ -45,7 +48,7 @@ function cronFields(expression) {
45
48
  if (!match) throw new Error("Use numeric cron fields, *, ranges, lists and steps.");
46
49
  const base = match[1];
47
50
  const step = Number(match[2] ?? 1);
48
- const [start, end] = base === "*" ? [min, max] : base.includes("-") ? base.split("-").map(Number) : [Number(base), match[2] ? max : Number(base)];
51
+ const [start, end] = cronRange(base, min, max, Boolean(match[2]));
49
52
  if (!Number.isInteger(step) || step < 1 || step > max - min + 1 || start < min || end > max || start > end)
50
53
  throw new Error("Cron field is outside its allowed range.");
51
54
  for (let value = start; value <= end; value += step) values.add(value);
@@ -287,6 +290,7 @@ var TaskSchema = z.object({
287
290
  gitFlow: GitFlowSchema.optional(),
288
291
  status: TaskStatusSchema,
289
292
  sessionId: z.string().nullable(),
293
+ pullRequestUrl: z.string().url().optional(),
290
294
  createdAt: z.string().datetime(),
291
295
  updatedAt: z.string().datetime(),
292
296
  deletedAt: z.string().datetime().optional()
@@ -304,7 +308,7 @@ var UpdateTaskSchema = CreateTaskSchema.pick({
304
308
  prompt: true,
305
309
  attachmentIds: true,
306
310
  gitFlow: true
307
- }).strict();
311
+ }).extend({ gitFlow: GitFlowSchema.nullable().optional() }).strict();
308
312
  var PROTOCOL_VERSION = 1;
309
313
  var ServerUpdateStatusSchema = z.object({
310
314
  currentVersion: z.string(),
@@ -910,8 +914,20 @@ async function startControl(directory, security2, status, maintenance2) {
910
914
  // package.json
911
915
  var package_default = {
912
916
  name: "@agentrific/server",
913
- version: "0.1.0",
917
+ version: "0.1.4",
914
918
  description: "Self-hosted control plane for long-running coding agents",
919
+ keywords: [
920
+ "agentrific",
921
+ "ai",
922
+ "android",
923
+ "cli",
924
+ "coding-agents",
925
+ "ios",
926
+ "pi",
927
+ "remote-control",
928
+ "self-hosted"
929
+ ],
930
+ homepage: "https://agentrific.com",
915
931
  repository: {
916
932
  type: "git",
917
933
  url: "git+https://github.com/devular/agentrific.git",
@@ -931,7 +947,7 @@ var package_default = {
931
947
  cli: "AGENTRIFIC_DATA_DIR=./data tsx src/cli.ts"
932
948
  },
933
949
  dependencies: {
934
- "@earendil-works/pi-coding-agent": "^0.87.1",
950
+ "@earendil-works/pi-coding-agent": "1.0.0",
935
951
  "@fastify/cors": "^11.3.0",
936
952
  "@fastify/multipart": "^10.1.2",
937
953
  "@inquirer/select": "^5.2.5",
@@ -1157,6 +1173,9 @@ function taskStatusForSession(status, session) {
1157
1173
  return "needs_user_input";
1158
1174
  }
1159
1175
  }
1176
+ function pullRequestUrl(session) {
1177
+ return session.gitDelivery?.status === "pr_opened" ? session.gitDelivery.url : void 0;
1178
+ }
1160
1179
  function sessionTask(session) {
1161
1180
  return TaskSchema.parse({
1162
1181
  id: `session-${session.id}`,
@@ -1167,6 +1186,7 @@ function sessionTask(session) {
1167
1186
  gitFlow: session.gitFlow,
1168
1187
  status: taskStatusForSession(session.status, session),
1169
1188
  sessionId: session.id,
1189
+ pullRequestUrl: pullRequestUrl(session),
1170
1190
  createdAt: session.createdAt,
1171
1191
  updatedAt: session.updatedAt
1172
1192
  });
@@ -1178,12 +1198,13 @@ async function boardTasks(store2, projectId) {
1178
1198
  tasks.flatMap((task) => task.sessionId ? [task.sessionId] : [])
1179
1199
  );
1180
1200
  const linkedTasks = tasks.filter((task) => !task.deletedAt).map((task) => {
1181
- const session = sessions.find((item) => item.taskId === task.id) ?? sessions.find((item) => item.id === task.sessionId);
1182
- if (!session) return { ...task, status: "backlog" };
1201
+ const session = sessions.find((item) => item.id === task.sessionId) ?? sessions.find((item) => item.taskId === task.id);
1202
+ if (!session) return { ...task, status: "backlog", pullRequestUrl: void 0 };
1183
1203
  linkedSessionIds.add(session.id);
1184
1204
  return {
1185
1205
  ...task,
1186
1206
  sessionId: session.id,
1207
+ pullRequestUrl: pullRequestUrl(session),
1187
1208
  status: taskStatusForSession(session.status, session)
1188
1209
  };
1189
1210
  });
@@ -1201,7 +1222,9 @@ async function findTask(store2, id) {
1201
1222
  function editTask(store2, id, input, resolveAttachments) {
1202
1223
  return withTaskLock(id, async () => {
1203
1224
  const task = await findTask(store2, id);
1204
- const { attachmentIds, ...updates2 } = input;
1225
+ const { attachmentIds, gitFlow, ...updates2 } = input;
1226
+ const configuration = { gitFlow: task.gitFlow };
1227
+ if (gitFlow !== void 0) configuration.gitFlow = gitFlow ?? void 0;
1205
1228
  let files = task.attachments;
1206
1229
  if (attachmentIds) {
1207
1230
  if (!resolveAttachments)
@@ -1212,6 +1235,7 @@ function editTask(store2, id, input, resolveAttachments) {
1212
1235
  store2.saveTask({
1213
1236
  ...task,
1214
1237
  ...updates2,
1238
+ ...configuration,
1215
1239
  attachments: files,
1216
1240
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1217
1241
  })
@@ -1455,7 +1479,7 @@ var AgentController = class {
1455
1479
  Effect7.gen({ self: this }, function* () {
1456
1480
  if (this.closed)
1457
1481
  return yield* Effect7.fail(new AgentError({ message: "Server is shutting down" }));
1458
- if (!session.branch)
1482
+ if (!session.branch && (session.gitFlow || !session.workspacePath))
1459
1483
  return yield* Effect7.fail(new AgentError({ message: "Session checkout is not ready" }));
1460
1484
  if (this.hasMessage(session.id, messageId)) return;
1461
1485
  const entry = yield* this.open(session);
@@ -1762,45 +1786,7 @@ ${error ? workflowError(error instanceof Error ? error : String(error)) : messag
1762
1786
  if (await this.git(session, ["branch", "--show-current"], signal) !== session.branch)
1763
1787
  throw new Error("Restore the assigned run branch before requesting delivery.");
1764
1788
  if (dirty) {
1765
- await this.git(session, ["add", "--all"], signal);
1766
- await this.stage(job, "drafting");
1767
- const diff = await this.git(session, ["diff", "--cached", "HEAD", "--"], signal);
1768
- const files = await this.git(
1769
- session,
1770
- ["ls-files", "--others", "--exclude-standard"],
1771
- signal
1772
- );
1773
- if (this.options.draft) {
1774
- try {
1775
- const draft = await this.options.draft(
1776
- session,
1777
- input,
1778
- `${diff}
1779
- New files:
1780
- ${files}`.slice(0, 4e4),
1781
- signal
1782
- );
1783
- job.title = draft.title;
1784
- job.body = draft.body;
1785
- } catch (error) {
1786
- job.logs = workflowLog(
1787
- `${job.logs}
1788
- Message helper unavailable; using task summary. ${workflowError(error instanceof Error ? error : String(error))}`
1789
- );
1790
- }
1791
- }
1792
- await this.stage(job, "committing");
1793
- if (await this.git(session, ["branch", "--show-current"], signal) !== session.branch)
1794
- throw new Error("The workspace changed branches while preparing delivery.");
1795
- const output = await this.git(
1796
- session,
1797
- ["commit", "-m", `${job.title}
1798
-
1799
- Agentrific run ${id}.`],
1800
- signal
1801
- );
1802
- job.logs = workflowLog(`${job.logs}
1803
- ${output}`);
1789
+ await this.commitChanges(session, job, input, signal);
1804
1790
  } else {
1805
1791
  await this.git(session, ["hook", "run", "--ignore-missing", "pre-commit"], signal);
1806
1792
  }
@@ -1827,6 +1813,44 @@ ${workflowError(error instanceof Error ? error : String(error))}`
1827
1813
  }
1828
1814
  });
1829
1815
  }
1816
+ async commitChanges(session, job, input, signal) {
1817
+ const id = session.id;
1818
+ await this.git(session, ["add", "--all"], signal);
1819
+ await this.stage(job, "drafting");
1820
+ const diff = await this.git(session, ["diff", "--cached", "HEAD", "--"], signal);
1821
+ const files = await this.git(session, ["ls-files", "--others", "--exclude-standard"], signal);
1822
+ if (this.options.draft) {
1823
+ try {
1824
+ const draft = await this.options.draft(
1825
+ session,
1826
+ input,
1827
+ `${diff}
1828
+ New files:
1829
+ ${files}`.slice(0, 4e4),
1830
+ signal
1831
+ );
1832
+ job.title = draft.title;
1833
+ job.body = draft.body;
1834
+ } catch (error) {
1835
+ job.logs = workflowLog(
1836
+ `${job.logs}
1837
+ Message helper unavailable; using task summary. ${workflowError(error instanceof Error ? error : String(error))}`
1838
+ );
1839
+ }
1840
+ }
1841
+ await this.stage(job, "committing");
1842
+ if (await this.git(session, ["branch", "--show-current"], signal) !== session.branch)
1843
+ throw new Error("The workspace changed branches while preparing delivery.");
1844
+ const output = await this.git(
1845
+ session,
1846
+ ["commit", "-m", `${job.title}
1847
+
1848
+ Agentrific run ${id}.`],
1849
+ signal
1850
+ );
1851
+ job.logs = workflowLog(`${job.logs}
1852
+ ${output}`);
1853
+ }
1830
1854
  fallbackBody(id, input) {
1831
1855
  return `${input.summary}
1832
1856
 
@@ -1899,6 +1923,31 @@ Agentrific run ${id}.`;
1899
1923
  );
1900
1924
  }
1901
1925
  }
1926
+ async mergeChanges(session, job, projectPath, commit) {
1927
+ await this.stage(job, "integrating");
1928
+ if (session.gitFlow.workspace === "checkout" && await this.command(projectPath, "git", ["branch", "--show-current"]) === session.branch)
1929
+ await this.command(projectPath, "git", ["switch", "main"]);
1930
+ if (await this.command(projectPath, "git", ["branch", "--show-current"]) !== "main")
1931
+ throw new Error("Switch the project checkout to main, then retry delivery.");
1932
+ if (await this.command(projectPath, "git", ["status", "--porcelain"]))
1933
+ throw new Error(
1934
+ "Waiting for local changes in the main checkout. Commit or move them, then retry delivery."
1935
+ );
1936
+ const included = await this.command(projectPath, "git", ["merge-base", "main", commit]) === commit;
1937
+ if (!included) {
1938
+ try {
1939
+ await this.command(projectPath, "git", ["merge", "--no-edit", commit]);
1940
+ } catch (error) {
1941
+ await this.command(projectPath, "git", ["merge", "--abort"]).catch(() => void 0);
1942
+ throw new Error(
1943
+ `Integration failed. Resolve conflicts against main in the run workspace, then call deliver_changes again.
1944
+ ${workflowError(error instanceof Error ? error : String(error))}`
1945
+ );
1946
+ }
1947
+ }
1948
+ job.result = "merged";
1949
+ job.message = "Committed changes are on main.";
1950
+ }
1902
1951
  async integrate(id) {
1903
1952
  const job = await this.job(id);
1904
1953
  if (!job?.commitSha) return;
@@ -1912,29 +1961,7 @@ Agentrific run ${id}.`;
1912
1961
  "The workspace changed after requesting delivery. Call deliver_changes again for the new changes."
1913
1962
  );
1914
1963
  if (session.gitFlow.delivery === "merge") {
1915
- await this.stage(job, "integrating");
1916
- if (session.gitFlow.workspace === "checkout" && await this.command(projectPath, "git", ["branch", "--show-current"]) === session.branch)
1917
- await this.command(projectPath, "git", ["switch", "main"]);
1918
- if (await this.command(projectPath, "git", ["branch", "--show-current"]) !== "main")
1919
- throw new Error("Switch the project checkout to main, then retry delivery.");
1920
- if (await this.command(projectPath, "git", ["status", "--porcelain"]))
1921
- throw new Error(
1922
- "Waiting for local changes in the main checkout. Commit or move them, then retry delivery."
1923
- );
1924
- const included = await this.command(projectPath, "git", ["merge-base", "main", commit]) === commit;
1925
- if (!included) {
1926
- try {
1927
- await this.command(projectPath, "git", ["merge", "--no-edit", commit]);
1928
- } catch (error) {
1929
- await this.command(projectPath, "git", ["merge", "--abort"]).catch(() => void 0);
1930
- throw new Error(
1931
- `Integration failed. Resolve conflicts against main in the run workspace, then call deliver_changes again.
1932
- ${workflowError(error instanceof Error ? error : String(error))}`
1933
- );
1934
- }
1935
- }
1936
- job.result = "merged";
1937
- job.message = "Committed changes are on main.";
1964
+ await this.mergeChanges(session, job, projectPath, commit);
1938
1965
  } else {
1939
1966
  await this.stage(job, "pushing");
1940
1967
  await this.git(session, ["push", "origin", `${commit}:refs/heads/${session.branch}`]);
@@ -2012,6 +2039,7 @@ var PreparationCoordinator = class {
2012
2039
  return [...this.preparing.values()].map((id) => `preparation:${id}`);
2013
2040
  }
2014
2041
  async prepare(session, projectPath, input) {
2042
+ if (!session.gitFlow) return { ...session, branch: null, workspacePath: projectPath };
2015
2043
  const preparation2 = Symbol(session.id);
2016
2044
  this.preparing.set(preparation2, session.id);
2017
2045
  return withTaskLock(`prepare:${session.id}`, async () => {
@@ -2044,56 +2072,7 @@ ${logs}` : ""}`);
2044
2072
  try {
2045
2073
  await record("creating", "Creating the run workspace\u2026");
2046
2074
  const workspace = session.workspacePath;
2047
- const checkout = session.gitFlow?.workspace === "checkout";
2048
- const branchAtWorkspace = await command(workspace, "git", [
2049
- "branch",
2050
- "--show-current"
2051
- ]).catch(() => void 0);
2052
- if (branchAtWorkspace !== session.branch) {
2053
- if (checkout) {
2054
- if (branchAtWorkspace !== "main" || await command(projectPath, "git", ["status", "--porcelain"]))
2055
- throw new Error(
2056
- "The project checkout must be clean and on main before starting a checkout run."
2057
- );
2058
- await command(
2059
- projectPath,
2060
- "git",
2061
- ["switch", "-c", session.branch, "main"],
2062
- void 0,
2063
- true
2064
- );
2065
- } else {
2066
- if (branchAtWorkspace !== void 0)
2067
- throw new Error("The run workspace already exists on a different branch.");
2068
- await mkdir3(path4.dirname(workspace), { recursive: true });
2069
- try {
2070
- await command(
2071
- projectPath,
2072
- "git",
2073
- [
2074
- "worktree",
2075
- "add",
2076
- "-b",
2077
- session.branch,
2078
- workspace,
2079
- session.gitFlow ? "main" : "HEAD"
2080
- ],
2081
- void 0,
2082
- true
2083
- );
2084
- } catch (error) {
2085
- if (await command(workspace, "git", ["branch", "--show-current"]).catch(
2086
- () => void 0
2087
- ) !== session.branch)
2088
- throw error;
2089
- await record(
2090
- "creating",
2091
- "Workspace created; a checkout hook failed. Preparing dependencies\u2026",
2092
- workflowError(error instanceof Error ? error : String(error))
2093
- );
2094
- }
2095
- }
2096
- }
2075
+ await this.createWorkspace(session, projectPath, command, record);
2097
2076
  await record("installing", "Installing run dependencies\u2026");
2098
2077
  await this.install(workspace, command);
2099
2078
  await record("ready", "Workspace ready.");
@@ -2109,6 +2088,57 @@ ${logs}` : ""}`);
2109
2088
  }
2110
2089
  }).finally(() => this.preparing.delete(preparation2));
2111
2090
  }
2091
+ async createWorkspace(session, projectPath, command, record) {
2092
+ const workspace = session.workspacePath;
2093
+ const checkout = session.gitFlow?.workspace === "checkout";
2094
+ if (!await command(projectPath, "git", ["rev-parse", "--verify", "HEAD"]).catch(() => void 0))
2095
+ throw new Error(
2096
+ "Git Flow needs an initial commit. Create one in the project, or turn off Git Flow for a plain chat."
2097
+ );
2098
+ const branchAtWorkspace = await command(workspace, "git", ["branch", "--show-current"]).catch(
2099
+ () => void 0
2100
+ );
2101
+ if (branchAtWorkspace !== session.branch) {
2102
+ if (checkout) {
2103
+ if (branchAtWorkspace !== "main" || await command(projectPath, "git", ["status", "--porcelain"]))
2104
+ throw new Error(
2105
+ "The project checkout must be clean and on main before starting a checkout run."
2106
+ );
2107
+ await command(
2108
+ projectPath,
2109
+ "git",
2110
+ ["switch", "-c", session.branch, "main"],
2111
+ void 0,
2112
+ true
2113
+ );
2114
+ } else {
2115
+ await this.createWorktree(session, projectPath, command, record, branchAtWorkspace);
2116
+ }
2117
+ }
2118
+ }
2119
+ async createWorktree(session, projectPath, command, record, branchAtWorkspace) {
2120
+ const workspace = session.workspacePath;
2121
+ if (branchAtWorkspace !== void 0)
2122
+ throw new Error("The run workspace already exists on a different branch.");
2123
+ await mkdir3(path4.dirname(workspace), { recursive: true });
2124
+ try {
2125
+ await command(
2126
+ projectPath,
2127
+ "git",
2128
+ ["worktree", "add", "-b", session.branch, workspace, "main"],
2129
+ void 0,
2130
+ true
2131
+ );
2132
+ } catch (error) {
2133
+ if (await command(workspace, "git", ["branch", "--show-current"]).catch(() => void 0) !== session.branch)
2134
+ throw error;
2135
+ await record(
2136
+ "creating",
2137
+ "Workspace created; a checkout hook failed. Preparing dependencies\u2026",
2138
+ workflowError(error instanceof Error ? error : String(error))
2139
+ );
2140
+ }
2141
+ }
2112
2142
  async install(workspace, command) {
2113
2143
  const packageFile = await readFile3(path4.join(workspace, "package.json"), "utf8").catch(
2114
2144
  () => void 0
@@ -2198,10 +2228,10 @@ ${logs}` : ""}`);
2198
2228
  import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent";
2199
2229
  import { z as z9 } from "zod";
2200
2230
  var DraftSchema = z9.object({ title: z9.string().trim().min(1).max(120), body: z9.string().trim().min(1).max(1e4) }).strict();
2201
- function deliveryWriter(models2) {
2231
+ function deliveryWriter(models2, createSession = createAgentSession) {
2202
2232
  return async (run, input, diff, signal) => {
2203
2233
  signal?.throwIfAborted();
2204
- const { session } = await createAgentSession({
2234
+ const { session } = await createSession({
2205
2235
  cwd: run.workspacePath,
2206
2236
  model: await models2.resolve(run.model ?? await models2.defaultReference()),
2207
2237
  modelRuntime: models2.runtime,
@@ -2404,11 +2434,11 @@ function toolOutput(value) {
2404
2434
  const text = parsed.success ? parsed.data.content?.filter((part) => part.type === "text").map((part) => part.text || "").join("\n") || "Completed without text output." : boundedJson(value, 4e3);
2405
2435
  return text.length > 4e3 ? `${text.slice(0, 4e3)}\u2026 [truncated]` : text;
2406
2436
  }
2407
- function createPiFactory(files, models2, delivery2) {
2437
+ function createPiFactory(files, models2, delivery2, createSession = createAgentSession2) {
2408
2438
  return {
2409
2439
  async open(sessionId, cwd, sessionDirectory, selected = legacyModel, run) {
2410
2440
  const model = await models2.resolve(selected);
2411
- const { session } = await createAgentSession2({
2441
+ const { session } = await createSession({
2412
2442
  cwd,
2413
2443
  model,
2414
2444
  modelRuntime: models2.runtime,
@@ -2833,31 +2863,7 @@ var SqliteSessionStore = class {
2833
2863
  this.db.prepare("DELETE FROM projects WHERE id = ?").run(replacedId);
2834
2864
  }
2835
2865
  if (previousPath && previousPath !== project.path) {
2836
- this.db.prepare(
2837
- "UPDATE workflows SET payload = json_set(payload, '$.projectPath', ?) WHERE json_extract(payload, '$.kind') = 'preparation' AND json_extract(payload, '$.projectPath') = ?"
2838
- ).run(project.path, previousPath);
2839
- const relocate = (workspace) => workspace === previousPath || workspace.startsWith(`${previousPath}${path6.sep}`) ? project.path + workspace.slice(previousPath.length) : workspace;
2840
- for (const row of this.db.prepare(
2841
- "SELECT id, payload FROM sessions WHERE json_extract(payload, '$.projectId') = ?"
2842
- ).all(project.id)) {
2843
- const session = SessionSchema.parse(JSON.parse(PayloadRow.parse(row).payload));
2844
- if (session.workspacePath) {
2845
- session.workspacePath = String(relocate(session.workspacePath));
2846
- this.db.prepare("UPDATE sessions SET payload = ? WHERE id = ?").run(JSON.stringify(session), session.id);
2847
- }
2848
- }
2849
- for (const row of this.db.prepare(
2850
- "SELECT sequence, payload FROM events WHERE json_extract(payload, '$.session.projectId') = ?"
2851
- ).all(project.id)) {
2852
- const event = SessionSchema.parse(JSON.parse(PayloadRow.parse(row).payload).session);
2853
- if (event.workspacePath) {
2854
- event.workspacePath = String(relocate(event.workspacePath));
2855
- this.db.prepare("UPDATE events SET payload = ? WHERE sequence = ?").run(
2856
- JSON.stringify({ type: "session.updated", session: event }),
2857
- Number(row.sequence)
2858
- );
2859
- }
2860
- }
2866
+ this.relocateProjectHistory(project, previousPath);
2861
2867
  }
2862
2868
  this.db.prepare(
2863
2869
  "INSERT INTO projects(id, payload) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET payload = excluded.payload"
@@ -2870,6 +2876,28 @@ var SqliteSessionStore = class {
2870
2876
  }
2871
2877
  });
2872
2878
  }
2879
+ relocateProjectHistory(project, previousPath) {
2880
+ this.db.prepare(
2881
+ "UPDATE workflows SET payload = json_set(payload, '$.projectPath', ?) WHERE json_extract(payload, '$.kind') = 'preparation' AND json_extract(payload, '$.projectPath') = ?"
2882
+ ).run(project.path, previousPath);
2883
+ const relocate = (workspace) => workspace === previousPath || workspace.startsWith(`${previousPath}${path6.sep}`) ? project.path + workspace.slice(previousPath.length) : workspace;
2884
+ for (const row of this.db.prepare("SELECT id, payload FROM sessions WHERE json_extract(payload, '$.projectId') = ?").all(project.id)) {
2885
+ const session = SessionSchema.parse(JSON.parse(PayloadRow.parse(row).payload));
2886
+ if (session.workspacePath) {
2887
+ session.workspacePath = String(relocate(session.workspacePath));
2888
+ this.db.prepare("UPDATE sessions SET payload = ? WHERE id = ?").run(JSON.stringify(session), session.id);
2889
+ }
2890
+ }
2891
+ for (const row of this.db.prepare(
2892
+ "SELECT sequence, payload FROM events WHERE json_extract(payload, '$.session.projectId') = ?"
2893
+ ).all(project.id)) {
2894
+ const event = SessionSchema.parse(JSON.parse(PayloadRow.parse(row).payload).session);
2895
+ if (event.workspacePath) {
2896
+ event.workspacePath = String(relocate(event.workspacePath));
2897
+ this.db.prepare("UPDATE events SET payload = ? WHERE sequence = ?").run(JSON.stringify({ type: "session.updated", session: event }), Number(row.sequence));
2898
+ }
2899
+ }
2900
+ }
2873
2901
  defaultModel() {
2874
2902
  return this.query(() => {
2875
2903
  const row = this.db.prepare("SELECT payload FROM settings WHERE id = 'default-model'").get();
@@ -3474,16 +3502,27 @@ app.get("/api/sessions/:id/events", async (request, reply) => {
3474
3502
  if (!session) return reply.code(404).send({ error: "Session not found." });
3475
3503
  return Effect14.runPromise(store.history(session.id, query.data.after, query.data.limit));
3476
3504
  });
3477
- async function createAndStartSession(input) {
3478
- maintenance.assertReady();
3479
- if (input.gitFlow?.delivery === "pull_request") {
3480
- const github = await githubCliStatus();
3481
- if (!github.githubCli.authenticated)
3482
- return { error: "GitHub CLI must be authenticated on the server to open a PR.", status: 400 };
3505
+ async function selectSessionModel(input, hasImages) {
3506
+ try {
3507
+ const reference = input.model ?? await models.defaultReference();
3508
+ const selected = await models.resolve(reference);
3509
+ if (hasImages && !selected.input.includes("image"))
3510
+ return {
3511
+ error: "This model cannot read images. Choose a vision model in Settings.",
3512
+ status: 400
3513
+ };
3514
+ return { model: models.identity(selected.provider, selected.id) };
3515
+ } catch {
3516
+ return {
3517
+ error: "The default model is unavailable. Choose an available model in Settings.",
3518
+ status: 400
3519
+ };
3483
3520
  }
3521
+ }
3522
+ async function resolveSessionProject(projectId) {
3484
3523
  const projectsRoot = await Effect14.runPromise(store.projectsRoot(defaultProjectsRoot));
3485
3524
  const project = (await Effect14.runPromise(discoverProjects(projectsRoot, store))).find(
3486
- (item) => item.id === input.projectId && item.available
3525
+ (item) => item.id === projectId && item.available
3487
3526
  );
3488
3527
  if (!project)
3489
3528
  return { error: "Project folder is missing. Reconnect it before starting a run.", status: 404 };
@@ -3491,6 +3530,37 @@ async function createAndStartSession(input) {
3491
3530
  const resolvedRoot = await realpath2(projectsRoot);
3492
3531
  if (path7.dirname(projectPath) !== resolvedRoot)
3493
3532
  return { error: "Project path is outside projects root.", status: 400 };
3533
+ return { projectPath };
3534
+ }
3535
+ async function resolveSessionWorkspace(id, gitFlow, projectPath) {
3536
+ const worktreePath = path7.join(worktreesRoot, id);
3537
+ const branch = gitFlow ? `agentrific/${id.slice(0, 8)}` : null;
3538
+ const useProjectCheckout = gitFlow?.workspace === "checkout";
3539
+ const workspacePath = !gitFlow || useProjectCheckout ? projectPath : worktreePath;
3540
+ if (useProjectCheckout) {
3541
+ const serverPath = await realpath2(process.cwd());
3542
+ if (serverPath === projectPath || serverPath.startsWith(`${projectPath}${path7.sep}`))
3543
+ return {
3544
+ error: "The server runs from this project. Choose a worktree for this task.",
3545
+ status: 409
3546
+ };
3547
+ }
3548
+ return { branch, workspacePath };
3549
+ }
3550
+ function sessionTaskUnavailable(task, projectId) {
3551
+ return !task || Boolean(task.deletedAt) || task.projectId !== projectId;
3552
+ }
3553
+ async function sessionDeliveryAvailable(gitFlow) {
3554
+ if (gitFlow?.delivery !== "pull_request") return true;
3555
+ return (await githubCliStatus()).githubCli.authenticated;
3556
+ }
3557
+ async function createAndStartSession(input) {
3558
+ maintenance.assertReady();
3559
+ if (!await sessionDeliveryAvailable(input.gitFlow))
3560
+ return { error: "GitHub CLI must be authenticated on the server to open a PR.", status: 400 };
3561
+ const location = await resolveSessionProject(input.projectId);
3562
+ if (location.error !== void 0) return { error: location.error, status: location.status };
3563
+ const projectPath = location.projectPath;
3494
3564
  let files;
3495
3565
  try {
3496
3566
  files = await Effect14.runPromise(
@@ -3499,25 +3569,15 @@ async function createAndStartSession(input) {
3499
3569
  } catch (error) {
3500
3570
  return { error: String(error), status: 400 };
3501
3571
  }
3502
- let selectedModel;
3503
- try {
3504
- const reference = input.model ?? await models.defaultReference();
3505
- const selected = await models.resolve(reference);
3506
- if (files.some((file) => file.kind === "image") && !selected.input.includes("image"))
3507
- return {
3508
- error: "This model cannot read images. Choose a vision model in Settings.",
3509
- status: 400
3510
- };
3511
- selectedModel = models.identity(selected.provider, selected.id);
3512
- } catch {
3513
- return {
3514
- error: "The default model is unavailable. Choose an available model in Settings.",
3515
- status: 400
3516
- };
3517
- }
3572
+ const selection = await selectSessionModel(
3573
+ input,
3574
+ files.some((file) => file.kind === "image")
3575
+ );
3576
+ if (selection.error !== void 0) return { error: selection.error, status: selection.status };
3577
+ const selectedModel = selection.model;
3518
3578
  const now = (/* @__PURE__ */ new Date()).toISOString();
3519
3579
  const task = input.taskId ? await Effect14.runPromise(store.getTask(input.taskId)) : void 0;
3520
- if (input.taskId && (!task || task.deletedAt || task.projectId !== input.projectId))
3580
+ if (input.taskId && sessionTaskUnavailable(task, input.projectId))
3521
3581
  return { error: "Task not found.", status: 404 };
3522
3582
  const session = SessionSchema.parse({
3523
3583
  id: randomUUID7(),
@@ -3532,18 +3592,9 @@ async function createAndStartSession(input) {
3532
3592
  createdAt: now,
3533
3593
  updatedAt: now
3534
3594
  });
3535
- const worktreePath = path7.join(worktreesRoot, session.id);
3536
- const branch = `agentrific/${session.id.slice(0, 8)}`;
3537
- const useProjectCheckout = input.gitFlow?.workspace === "checkout";
3538
- const workspacePath = useProjectCheckout ? projectPath : worktreePath;
3539
- if (useProjectCheckout) {
3540
- const serverPath = await realpath2(process.cwd());
3541
- if (serverPath === projectPath || serverPath.startsWith(`${projectPath}${path7.sep}`))
3542
- return {
3543
- error: "The server runs from this project. Choose a worktree for this task.",
3544
- status: 409
3545
- };
3546
- }
3595
+ const workspace = await resolveSessionWorkspace(session.id, input.gitFlow, projectPath);
3596
+ if (workspace.error !== void 0) return { error: workspace.error, status: workspace.status };
3597
+ const { branch, workspacePath } = workspace;
3547
3598
  const initial = { ...session, branch, workspacePath };
3548
3599
  publish({ type: "session.updated", session: initial });
3549
3600
  let running;