@teammy/server 0.0.8 → 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js CHANGED
@@ -3,6 +3,14 @@ import { createHash as createHash4 } from "node:crypto";
3
3
 
4
4
  // apps/server/files.ts
5
5
  import { createHash as createHash3 } from "node:crypto";
6
+
7
+ // packages/protocol/browse-order.ts
8
+ var names = new Intl.Collator("zh-CN", { numeric: true, sensitivity: "base" });
9
+ function compareNames(a, b) {
10
+ return names.compare(a, b) || (a < b ? -1 : a > b ? 1 : 0);
11
+ }
12
+
13
+ // apps/server/files.ts
6
14
  import { join as join2 } from "node:path";
7
15
 
8
16
  // packages/storage/local-blob-store.ts
@@ -110,10 +118,10 @@ function openDatabase(path, migrations2, busyTimeoutMs = 5e3) {
110
118
  `PRAGMA busy_timeout = ${busyTimeoutMs}; PRAGMA foreign_keys = ON; PRAGMA journal_mode = WAL; PRAGMA synchronous = FULL;`
111
119
  );
112
120
  transaction(db, () => {
113
- const version = Number(db.prepare("PRAGMA user_version").get().user_version);
114
- if (version > migrations2.length)
121
+ const version2 = Number(db.prepare("PRAGMA user_version").get().user_version);
122
+ if (version2 > migrations2.length)
115
123
  throw new Error("Database was created by a newer Teammy version.");
116
- for (let index = version; index < migrations2.length; index++) {
124
+ for (let index = version2; index < migrations2.length; index++) {
117
125
  db.exec(migrations2[index]);
118
126
  db.exec(`PRAGMA user_version = ${index + 1}`);
119
127
  }
@@ -966,7 +974,7 @@ var FileRepository = class {
966
974
  if (target > head)
967
975
  throw new HubError(400, "INVALID_REVISION", "Requested revision has not been published.");
968
976
  const prefix = path ? path + "/" : "";
969
- const rows = this.hub.db.prepare(
977
+ let rows = this.hub.db.prepare(
970
978
  `
971
979
  WITH latest AS (
972
980
  SELECT path, MAX(revision) revision FROM file_events
@@ -979,7 +987,7 @@ var FileRepository = class {
979
987
  SELECT *,CASE WHEN instr(tail,'/')=0 THEN tail ELSE substr(tail,1,instr(tail,'/')-1) END name FROM live
980
988
  )
981
989
  SELECT name,MAX(instr(tail,'/')>0) directory,COUNT(*) count,MIN(hash) hash,MIN(size) size,MAX(revision) revision
982
- FROM children WHERE name>? GROUP BY name ORDER BY name LIMIT ?
990
+ FROM children GROUP BY name
983
991
  `
984
992
  ).all(
985
993
  identity.workspaceId,
@@ -989,10 +997,15 @@ var FileRepository = class {
989
997
  identity.workspaceId,
990
998
  owner,
991
999
  prefix,
992
- prefix,
993
- after,
994
- limit + 1
1000
+ prefix
1001
+ );
1002
+ rows.sort(
1003
+ (a, b) => Number(b.directory) - Number(a.directory) || compareNames(String(a.name), String(b.name))
995
1004
  );
1005
+ const offset = after ? rows.findIndex((row) => row.name === after) + 1 : 0;
1006
+ if (after && !offset)
1007
+ throw new HubError(400, "INVALID_CURSOR", "Directory cursor is not in this snapshot.");
1008
+ rows = rows.slice(offset, offset + limit + 1);
996
1009
  return {
997
1010
  revision: target,
998
1011
  path,
@@ -1089,7 +1102,7 @@ function captureSnapshot(hub, identity, selection, options = {}) {
1089
1102
  return snapshot;
1090
1103
  }
1091
1104
  var SnapshotReader = class _SnapshotReader {
1092
- constructor(hub, identity, snapshot, budget = 6e4, sharedBudget) {
1105
+ constructor(hub, identity, snapshot, budget = Number.POSITIVE_INFINITY, sharedBudget) {
1093
1106
  this.hub = hub;
1094
1107
  this.identity = identity;
1095
1108
  this.snapshot = snapshot;
@@ -1099,8 +1112,6 @@ var SnapshotReader = class _SnapshotReader {
1099
1112
  identity;
1100
1113
  snapshot;
1101
1114
  readFiles = /* @__PURE__ */ new Set();
1102
- lineCounts = /* @__PURE__ */ new Map();
1103
- readRanges = /* @__PURE__ */ new Map();
1104
1115
  limitations = /* @__PURE__ */ new Set();
1105
1116
  budget;
1106
1117
  commonReader;
@@ -1116,21 +1127,16 @@ var SnapshotReader = class _SnapshotReader {
1116
1127
  this.budget
1117
1128
  );
1118
1129
  }
1130
+ forSnapshot(snapshot) {
1131
+ return new _SnapshotReader(this.hub, this.identity, snapshot, 0, this.budget);
1132
+ }
1119
1133
  afterIndex(after) {
1120
1134
  if (!after) return 0;
1121
1135
  const index = this.snapshot.files.findIndex((f) => f.path === after);
1122
1136
  if (index < 0) throw new HubError(400, "AI_CURSOR_INVALID", "Unknown snapshot cursor.");
1123
1137
  return index + 1;
1124
1138
  }
1125
- fullyRead(path) {
1126
- let end = 0;
1127
- for (const range of [...this.readRanges.get(path) ?? []].sort((a, b) => a[0] - b[0])) {
1128
- if (range[0] > end + 1) break;
1129
- end = Math.max(end, range[1]);
1130
- }
1131
- return this.lineCounts.has(path) && end >= this.lineCounts.get(path);
1132
- }
1133
- list(after = "", limit = 50) {
1139
+ list(after = "", limit = this.snapshot.files.length) {
1134
1140
  const entries = this.snapshot.files.slice(
1135
1141
  this.afterIndex(after),
1136
1142
  this.afterIndex(after) + limit + 1
@@ -1143,12 +1149,10 @@ var SnapshotReader = class _SnapshotReader {
1143
1149
  }
1144
1150
  source(path) {
1145
1151
  const file = this.snapshot.files.find((entry) => entry.path === path);
1146
- if (!file || !this.readFiles.has(path))
1147
- throw new HubError(
1148
- 422,
1149
- "AI_TASK_SOURCE_INVALID",
1150
- "Task source must be an actually read snapshot file."
1151
- );
1152
+ if (!file) {
1153
+ this.limitations.add(`\u6765\u6E90\u672A\u80FD\u5173\u8054\u5230\u672C\u8F6E\u8D44\u6599\uFF1A${path}`);
1154
+ return null;
1155
+ }
1152
1156
  const row = this.hub.db.prepare(
1153
1157
  "SELECT created_at FROM file_revisions WHERE workspace_id=? AND owner=? AND revision=?"
1154
1158
  ).get(this.identity.workspaceId, this.snapshot.owner, file.revision);
@@ -1156,45 +1160,35 @@ var SnapshotReader = class _SnapshotReader {
1156
1160
  throw new HubError(500, "AI_SOURCE_VERSION_MISSING", "Source revision metadata is missing.");
1157
1161
  return { ...file, syncedAt: String(row.created_at) };
1158
1162
  }
1159
- async text(path) {
1163
+ async bytes(path) {
1160
1164
  const entry = this.snapshot.files.find((f) => f.path === path);
1161
1165
  if (!entry) throw new HubError(403, "AI_SCOPE_VIOLATION", "File is outside the task snapshot.");
1162
- if (entry.size > 256 * 1024)
1163
- throw new HubError(413, "AI_FILE_LIMIT", "File exceeds the 256 KiB analysis limit.");
1164
1166
  const bytes = await new FileRepository(this.hub).download(this.identity, entry.sha256);
1165
1167
  if (bytes.byteLength !== entry.size || createHash4("sha256").update(bytes).digest("hex") !== entry.sha256)
1166
1168
  throw new HubError(500, "AI_CONTENT_CORRUPT", "Snapshot content verification failed.");
1167
- let text;
1168
- try {
1169
- text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
1170
- } catch {
1171
- throw new HubError(422, "AI_TEXT_REQUIRED", "Only UTF-8 text files can be analysed.");
1169
+ return bytes;
1170
+ }
1171
+ async text(path) {
1172
+ return new TextDecoder().decode(await this.bytes(path));
1173
+ }
1174
+ consume(size) {
1175
+ if (size > this.budget.remaining) {
1176
+ this.limitations.add("\u672C\u6B21\u5206\u6790\u7684\u7D2F\u8BA1\u6B63\u6587\u8BFB\u53D6\u91CF\u5DF2\u8FBE\u5230\u914D\u7F6E\u989D\u5EA6\u3002");
1177
+ throw new HubError(413, "AI_READ_BUDGET", "Workflow read allowance exceeded.");
1172
1178
  }
1173
- if (text.includes("\0"))
1174
- throw new HubError(422, "AI_TEXT_REQUIRED", "Binary files cannot be analysed.");
1175
- this.lineCounts.set(path, text.split("\n").length);
1176
- return text;
1179
+ this.budget.remaining -= size;
1177
1180
  }
1178
- async read(path, startLine = 1, limit = 150) {
1181
+ async read(path, startLine = 1, limit) {
1179
1182
  const text = await this.text(path), lines = text.split("\n");
1180
1183
  if (startLine > lines.length)
1181
1184
  throw new HubError(400, "AI_LINE_RANGE", "Line is outside the file.");
1182
- const selected = [];
1183
- let size = 0;
1184
- for (const line of lines.slice(startLine - 1, startLine - 1 + limit)) {
1185
- const cost = Buffer.byteLength(line) + 1;
1186
- if (size + cost > Math.min(12e3, this.budget.remaining)) break;
1187
- selected.push(line);
1188
- size += cost;
1189
- }
1190
- if (!selected.length) {
1191
- this.limitations.add("\u8BFB\u53D6\u9884\u7B97\u6216\u5355\u884C\u957F\u5EA6\u9650\u5236\u963B\u6B62\u4E86\u7EE7\u7EED\u8BFB\u53D6\u3002");
1192
- throw new HubError(413, "AI_READ_BUDGET", "Read budget exceeded or line is too long.");
1193
- }
1194
- this.budget.remaining -= size;
1185
+ const selected = lines.slice(
1186
+ startLine - 1,
1187
+ limit === void 0 ? void 0 : startLine - 1 + limit
1188
+ );
1189
+ this.consume(Buffer.byteLength(selected.join("\n")));
1195
1190
  this.readFiles.add(path);
1196
1191
  const endLine = startLine + selected.length - 1;
1197
- this.readRanges.set(path, [...this.readRanges.get(path) ?? [], [startLine, endLine]]);
1198
1192
  return {
1199
1193
  path,
1200
1194
  startLine,
@@ -1204,55 +1198,55 @@ var SnapshotReader = class _SnapshotReader {
1204
1198
  hasMore: endLine < lines.length
1205
1199
  };
1206
1200
  }
1207
- async whole(path, budget) {
1201
+ async whole(path) {
1208
1202
  const text = await this.text(path);
1209
- if (Buffer.byteLength(text) > budget)
1210
- throw new HubError(
1211
- 413,
1212
- "AI_CONTEXT_LIMIT",
1213
- "File is too large for this model budget; use directory analysis to read portions."
1214
- );
1203
+ this.consume(Buffer.byteLength(text));
1215
1204
  this.readFiles.add(path);
1216
- this.readRanges.set(path, [[1, text.split("\n").length]]);
1217
1205
  return text;
1218
1206
  }
1219
- async search(query, after = "", limit = 50) {
1220
- const files = this.snapshot.files.slice(this.afterIndex(after), this.afterIndex(after) + limit);
1221
- const matches = [];
1222
- let scannedBytes = 0;
1223
- for (const file of files) {
1224
- if (file.size > 256 * 1024) {
1225
- this.limitations.add("\u90E8\u5206\u5927\u6587\u4EF6\u672A\u53C2\u4E0E\u6B63\u6587\u641C\u7D22\u3002");
1226
- continue;
1227
- }
1228
- if (scannedBytes + file.size > 1024 * 1024) {
1229
- return {
1230
- matches,
1231
- nextCursor: files[files.indexOf(file) - 1]?.path ?? after,
1232
- limited: true
1233
- };
1234
- }
1235
- scannedBytes += file.size;
1207
+ async search(query, after = "", files = this.snapshot.files) {
1208
+ const fingerprint = snapshotDigest(files) + ":" + this.snapshot.owner + ":" + this.snapshot.revision;
1209
+ let fileIndex = 0, startLine = 1;
1210
+ if (after) {
1236
1211
  try {
1237
- const lines = (await this.text(file.path)).split("\n");
1238
- for (let i = 0; i < lines.length; i++)
1239
- if (lines[i].toLowerCase().includes(query.toLowerCase())) {
1240
- matches.push({ path: file.path, line: i + 1, excerpt: lines[i].slice(0, 400) });
1241
- if (matches.length >= 30) return { matches, nextCursor: file.path, limited: true };
1242
- }
1243
- } catch (e) {
1244
- if (e instanceof HubError && e.code === "AI_TEXT_REQUIRED") {
1245
- this.limitations.add("\u90E8\u5206\u975E\u6587\u672C\u6587\u4EF6\u672A\u53C2\u4E0E\u6B63\u6587\u641C\u7D22\u3002");
1246
- continue;
1212
+ const cursor = JSON.parse(Buffer.from(after, "base64url").toString("utf8"));
1213
+ fileIndex = files.findIndex((file) => file.path === cursor.path);
1214
+ if (fileIndex < 0 || cursor.query !== query || cursor.fingerprint !== fingerprint || !Number.isSafeInteger(cursor.line) || cursor.line < 1)
1215
+ throw new Error();
1216
+ startLine = cursor.line;
1217
+ } catch {
1218
+ throw new HubError(
1219
+ 400,
1220
+ "AI_CURSOR_INVALID",
1221
+ "Search cursor does not match this query and snapshot."
1222
+ );
1223
+ }
1224
+ }
1225
+ const matches = [];
1226
+ const needle = query.toLowerCase();
1227
+ for (let f = fileIndex; f < files.length; f++) {
1228
+ const file = files[f];
1229
+ const lines = (await this.text(file.path)).split("\n");
1230
+ for (let i = f === fileIndex ? startLine - 1 : 0; i < lines.length; i++) {
1231
+ if (!lines[i].toLowerCase().includes(needle)) continue;
1232
+ if (matches.length === 100) {
1233
+ return {
1234
+ matches,
1235
+ nextCursor: Buffer.from(
1236
+ JSON.stringify({
1237
+ fingerprint,
1238
+ query,
1239
+ path: file.path,
1240
+ line: i + 1
1241
+ })
1242
+ ).toString("base64url"),
1243
+ limited: true
1244
+ };
1247
1245
  }
1248
- throw e;
1246
+ matches.push({ path: file.path, line: i + 1, excerpt: lines[i] });
1249
1247
  }
1250
1248
  }
1251
- return {
1252
- matches,
1253
- nextCursor: files.length === limit ? files.at(-1).path : null,
1254
- limited: false
1255
- };
1249
+ return { matches, nextCursor: null, limited: false };
1256
1250
  }
1257
1251
  };
1258
1252
 
@@ -1438,6 +1432,10 @@ CREATE TABLE ai_defaults (
1438
1432
  scope TEXT PRIMARY KEY, profile_id TEXT REFERENCES ai_profiles(id), revision INTEGER NOT NULL
1439
1433
  );
1440
1434
  `;
1435
+ var aiRetryMigration = `
1436
+ ALTER TABLE ai_jobs ADD COLUMN auto_retry_count INTEGER NOT NULL DEFAULT 0;
1437
+ ALTER TABLE ai_jobs ADD COLUMN next_retry_at INTEGER;
1438
+ `;
1441
1439
 
1442
1440
  // apps/server/store.ts
1443
1441
  function digest2(token) {
@@ -1467,7 +1465,8 @@ var migrations = [
1467
1465
  equalMembershipMigration,
1468
1466
  taskBoardMigration,
1469
1467
  taskMaintenanceMigration,
1470
- sharedFilesMigration
1468
+ sharedFilesMigration,
1469
+ aiRetryMigration
1471
1470
  ];
1472
1471
  var HubStore = class {
1473
1472
  constructor(directory) {
@@ -1476,8 +1475,8 @@ var HubStore = class {
1476
1475
  if (existsSync(path)) {
1477
1476
  const existing = new DatabaseSync2(path, { readOnly: true });
1478
1477
  try {
1479
- const version = Number(existing.prepare("PRAGMA user_version").get().user_version);
1480
- if (version > 0 && version < 9)
1478
+ const version2 = Number(existing.prepare("PRAGMA user_version").get().user_version);
1479
+ if (version2 > 0 && version2 < 9)
1481
1480
  throw new Error(
1482
1481
  "Single-project data is not supported by this release. Start with a new --data-dir; existing data has been preserved."
1483
1482
  );
@@ -1572,10 +1571,11 @@ import { z as z6 } from "zod";
1572
1571
  import { z as z5 } from "zod";
1573
1572
  var TaskStatusSchema = z5.enum(["planned", "in_progress", "in_review", "completed"]);
1574
1573
  var TaskFieldsSchema = z5.object({
1575
- title: z5.string().trim().min(1).max(200),
1576
- goal: z5.string().trim().min(1).max(2e3).nullable(),
1574
+ title: z5.string().trim(),
1575
+ goal: z5.string().trim().nullable(),
1577
1576
  status: TaskStatusSchema.nullable(),
1578
- progress: z5.string().trim().min(1).max(2e3)
1577
+ progress: z5.string().trim(),
1578
+ needsConfirmation: z5.boolean().default(false)
1579
1579
  }).strict();
1580
1580
  var TaskSourceSchema = z5.object({
1581
1581
  path: FilePathSchema,
@@ -1587,14 +1587,17 @@ var TaskCardSchema = TaskFieldsSchema.extend({
1587
1587
  id: z5.uuid(),
1588
1588
  owner: NameSchema,
1589
1589
  jobId: z5.uuid(),
1590
- sources: z5.array(TaskSourceSchema).min(1),
1591
- syncedAt: z5.string(),
1590
+ sources: z5.array(TaskSourceSchema),
1591
+ syncedAt: z5.string().nullable(),
1592
1592
  corrected: z5.boolean(),
1593
1593
  needsReview: z5.boolean()
1594
1594
  });
1595
1595
  var TaskCorrectionSchema = z5.object({
1596
1596
  title: TaskFieldsSchema.shape.title.optional(),
1597
- status: TaskStatusSchema.nullable().optional()
1597
+ status: TaskStatusSchema.nullable().optional(),
1598
+ goal: TaskFieldsSchema.shape.goal.optional(),
1599
+ progress: TaskFieldsSchema.shape.progress.optional(),
1600
+ needsConfirmation: z5.boolean().optional()
1598
1601
  }).strict().refine((v) => Object.keys(v).length > 0, "Provide a correction.");
1599
1602
  var TaskBoardSchema = z5.object({
1600
1603
  tasks: z5.array(TaskCardSchema),
@@ -1605,17 +1608,11 @@ var TaskBoardSchema = z5.object({
1605
1608
  status: z5.enum(["empty", "waiting", "queued", "running", "succeeded", "failed", "cancelled"]),
1606
1609
  error: z5.string().nullable(),
1607
1610
  sourceChanged: z5.boolean(),
1608
- partial: z5.boolean(),
1609
1611
  uncertainties: z5.array(z5.string())
1610
1612
  })
1611
1613
  ),
1612
1614
  modelAvailable: z5.boolean()
1613
1615
  });
1614
- var TaskBootstrapRequestSchema = z5.object({ operationId: z5.uuid(), owner: NameSchema }).strict();
1615
- var TaskBootstrapResponseSchema = z5.object({
1616
- jobId: z5.uuid().nullable(),
1617
- empty: z5.boolean()
1618
- });
1619
1616
 
1620
1617
  // packages/protocol/ai.ts
1621
1618
  var AiProfileSchema = z6.object({
@@ -1624,13 +1621,8 @@ var AiProfileSchema = z6.object({
1624
1621
  protocol: z6.enum(["openai-completions", "openai-responses", "anthropic-messages"]),
1625
1622
  baseUrl: ServerUrlSchema,
1626
1623
  model: z6.string().trim().min(1).max(200),
1627
- enabled: z6.boolean().default(true),
1628
- contextWindow: z6.number().int().min(4096).max(2e6).default(32e3),
1629
- maxOutputTokens: z6.number().int().min(128).max(16e3).default(2048)
1630
- }).strict().refine(
1631
- (p) => p.maxOutputTokens < p.contextWindow,
1632
- "Output budget must be smaller than context window."
1633
- );
1624
+ enabled: z6.boolean().default(true)
1625
+ }).strict();
1634
1626
  var AiPublicProfileSchema = AiProfileSchema.safeExtend({
1635
1627
  revision: z6.number().int(),
1636
1628
  keyConfigured: z6.boolean()
@@ -1671,7 +1663,7 @@ var AiJobCreateSchema = z6.object({
1671
1663
  profileId: NameSchema.optional(),
1672
1664
  workflow: z6.enum(["file-summary", "directory-analysis", "task-bootstrap"]),
1673
1665
  selection: AiSelectionSchema,
1674
- question: z6.string().trim().max(2e3).default("")
1666
+ question: z6.string().trim().default("")
1675
1667
  }).strict();
1676
1668
  var AiCitationSchema = z6.object({
1677
1669
  path: FilePathSchema,
@@ -1681,26 +1673,31 @@ var AiCitationSchema = z6.object({
1681
1673
  });
1682
1674
  var AiCoverageSchema = z6.object({
1683
1675
  totalFiles: z6.number().int(),
1676
+ // Access diagnostics only; reading a file does not establish semantic completeness.
1684
1677
  readFiles: z6.array(FilePathSchema),
1685
- partial: z6.boolean(),
1678
+ accessTracking: z6.literal("unavailable").optional(),
1686
1679
  limitations: z6.array(z6.string())
1687
1680
  });
1688
1681
  var AiResultSchema = z6.object({
1689
- summary: z6.string().min(1).max(2e4),
1690
- findings: z6.array(z6.string().max(2e3)).max(30),
1691
- uncertainties: z6.array(z6.string().max(2e3)).max(30),
1692
- citations: z6.array(AiCitationSchema).max(50),
1682
+ summary: z6.string(),
1683
+ findings: z6.array(z6.string()),
1684
+ uncertainties: z6.array(z6.string()).default([]),
1685
+ citations: z6.array(AiCitationSchema),
1693
1686
  coverage: AiCoverageSchema
1694
1687
  });
1695
1688
  var TaskAnalysisResultSchema = z6.object({
1696
1689
  kind: z6.literal("task-bootstrap"),
1690
+ // Absent for historical full-list results; new workflows publish explicit patches.
1691
+ updateMode: z6.literal("patch").optional(),
1692
+ archive: z6.array(z6.uuid()).optional(),
1697
1693
  tasks: z6.array(
1698
1694
  TaskFieldsSchema.extend({
1699
1695
  taskId: z6.uuid().nullable().optional(),
1700
- sources: z6.array(TaskSourceSchema).min(1).max(50)
1696
+ sources: z6.array(TaskSourceSchema)
1701
1697
  })
1702
- ).max(100),
1703
- uncertainties: z6.array(z6.string().max(2e3)).max(50),
1698
+ ),
1699
+ // Read historical result notes without asking new task workflows to generate them.
1700
+ uncertainties: z6.array(z6.string()).optional(),
1704
1701
  coverage: AiCoverageSchema
1705
1702
  });
1706
1703
  var AiWorkflowResultSchema = z6.union([AiResultSchema, TaskAnalysisResultSchema]);
@@ -1788,11 +1785,18 @@ var AiConfig = class {
1788
1785
  return [iv, cipher.getAuthTag(), body].map((b) => b.toString("base64")).join(".");
1789
1786
  }
1790
1787
  list() {
1791
- return this.hub.db.prepare("SELECT * FROM ai_profiles ORDER BY id").all().map((row) => ({
1792
- ...AiProfileSchema.parse(JSON.parse(String(row.config_json))),
1793
- revision: Number(row.revision),
1794
- keyConfigured: !!row.secret
1795
- }));
1788
+ return this.hub.db.prepare("SELECT * FROM ai_profiles ORDER BY id").all().map((row) => {
1789
+ const {
1790
+ contextWindow: _context,
1791
+ maxOutputTokens: _output,
1792
+ ...profile
1793
+ } = JSON.parse(String(row.config_json));
1794
+ return {
1795
+ ...AiProfileSchema.parse(profile),
1796
+ revision: Number(row.revision),
1797
+ keyConfigured: !!row.secret
1798
+ };
1799
+ });
1796
1800
  }
1797
1801
  get(id) {
1798
1802
  const profile = this.list().find((p) => p.id === id);
@@ -1837,8 +1841,8 @@ var AiDefaults = class {
1837
1841
  this.hub = hub;
1838
1842
  }
1839
1843
  hub;
1840
- read(scope) {
1841
- const row = this.hub.db.prepare("SELECT * FROM ai_defaults WHERE scope=?").get(scope);
1844
+ read(scope2) {
1845
+ const row = this.hub.db.prepare("SELECT * FROM ai_defaults WHERE scope=?").get(scope2);
1842
1846
  return {
1843
1847
  profileId: row?.profile_id ? String(row.profile_id) : null,
1844
1848
  revision: Number(row?.revision ?? 0)
@@ -1861,10 +1865,10 @@ var AiDefaults = class {
1861
1865
  project(projectId2) {
1862
1866
  return this.view(projectId2);
1863
1867
  }
1864
- save(scope, value) {
1868
+ save(scope2, value) {
1865
1869
  const input = AiDefaultUpdateSchema.parse(value);
1866
1870
  transaction(this.hub.db, () => {
1867
- if (this.read(scope).revision !== input.expectedRevision)
1871
+ if (this.read(scope2).revision !== input.expectedRevision)
1868
1872
  throw new HubError(
1869
1873
  409,
1870
1874
  "AI_CONFIG_CONFLICT",
@@ -1874,7 +1878,7 @@ var AiDefaults = class {
1874
1878
  throw new HubError(400, "AI_PROFILE_DISABLED", "Model profile is disabled.");
1875
1879
  this.hub.db.prepare(
1876
1880
  `INSERT INTO ai_defaults VALUES(?,?,?) ON CONFLICT(scope) DO UPDATE SET profile_id=excluded.profile_id,revision=excluded.revision`
1877
- ).run(scope, input.profileId, input.expectedRevision + 1);
1881
+ ).run(scope2, input.profileId, input.expectedRevision + 1);
1878
1882
  });
1879
1883
  }
1880
1884
  saveSite(identity, value) {
@@ -1905,31 +1909,57 @@ import { resolve as resolve2 } from "node:path";
1905
1909
  import { fileURLToPath } from "node:url";
1906
1910
  import { z as z7 } from "zod";
1907
1911
  var WorkflowNameSchema = z7.enum(["file-summary", "directory-analysis", "task-bootstrap"]);
1908
- var ToolNameSchema = z7.enum(["list_files", "read_file", "search_files"]);
1909
- var ToolTextSchema = z7.object({ label: z7.string().min(1).max(100), description: z7.string().min(1).max(2e3) }).strict();
1912
+ var ToolNameSchema = z7.enum([
1913
+ "list_files",
1914
+ "read_file",
1915
+ "search_files",
1916
+ "inspect_changes",
1917
+ "query_tasks",
1918
+ "read",
1919
+ "bash",
1920
+ "grep",
1921
+ "find",
1922
+ "ls"
1923
+ ]);
1924
+ var ToolTextSchema = z7.object({ label: z7.string().min(1).max(100), description: z7.string().min(1) }).strict();
1910
1925
  var RuntimeConfigSchema = z7.object({
1911
1926
  version: z7.number().int().positive(),
1927
+ execution: z7.enum(["snapshot-tools", "native-sandbox"]).optional(),
1912
1928
  thinkingLevel: z7.enum(["off", "minimal", "low", "medium", "high"]),
1913
- tools: z7.array(ToolNameSchema).max(3).refine((v) => new Set(v).size === v.length),
1914
- maxRounds: z7.number().int().min(1).max(100),
1915
- maxToolCalls: z7.number().int().min(1).max(500),
1916
- timeoutMs: z7.number().int().min(1e3).max(9e5),
1917
- requestTimeoutMs: z7.number().int().min(1e3).max(12e4),
1918
- maxReadBytes: z7.number().int().min(1024).max(2e6),
1929
+ tools: z7.array(ToolNameSchema).refine((v) => new Set(v).size === v.length),
1930
+ maxRounds: z7.number().int().min(1).max(384),
1931
+ timeoutMs: z7.number().int().min(1e3).max(21e5),
1932
+ requestTimeoutMs: z7.number().int().min(1e3).max(3e5),
1933
+ maxReadBytes: z7.number().int().positive().max(1024 * 1024 * 1024).optional(),
1919
1934
  settleSeconds: z7.number().int().min(0).max(3600).optional()
1920
- }).strict();
1935
+ }).strict().refine((config) => {
1936
+ const allowed = config.execution === "native-sandbox" ? ["read", "bash", "grep", "find", "ls", "query_tasks"] : ["list_files", "read_file", "search_files", "inspect_changes", "query_tasks"];
1937
+ return config.tools.every((tool) => allowed.includes(tool));
1938
+ }, "Native file tools require the isolated runtime.");
1921
1939
  var WorkflowRuntimeSchema = z7.object({
1922
1940
  workflow: WorkflowNameSchema,
1923
- system: z7.string().min(1).max(32e3),
1924
- prompt: z7.string().min(1).max(32e3),
1941
+ system: z7.string().min(1),
1942
+ prompt: z7.string().min(1),
1925
1943
  config: RuntimeConfigSchema,
1926
1944
  toolText: z7.object({
1927
1945
  list_files: ToolTextSchema,
1928
1946
  read_file: ToolTextSchema,
1929
- search_files: ToolTextSchema
1947
+ search_files: ToolTextSchema,
1948
+ inspect_changes: ToolTextSchema.optional(),
1949
+ query_tasks: ToolTextSchema.optional()
1930
1950
  }).strict(),
1931
1951
  fingerprint: z7.string().regex(/^[a-f0-9]{64}$/)
1932
1952
  }).strict();
1953
+ function withoutLegacyToolLimit(value) {
1954
+ const { maxToolCalls: _removed, ...config } = value;
1955
+ return config;
1956
+ }
1957
+ function parseWorkflowRuntime(value) {
1958
+ return WorkflowRuntimeSchema.parse({
1959
+ ...value,
1960
+ config: withoutLegacyToolLimit(value.config)
1961
+ });
1962
+ }
1933
1963
  function loadWorkflow(workflow, directory = process.env.TEAMMY_AI_RUNTIME_DIR) {
1934
1964
  try {
1935
1965
  WorkflowNameSchema.parse(workflow);
@@ -1939,15 +1969,21 @@ function loadWorkflow(workflow, directory = process.env.TEAMMY_AI_RUNTIME_DIR) {
1939
1969
  workflow,
1940
1970
  system: read("shared/system.md"),
1941
1971
  prompt: read(`workflows/${workflow}/prompt.md`),
1942
- config: RuntimeConfigSchema.parse(JSON.parse(read(`workflows/${workflow}/config.json`))),
1972
+ config: RuntimeConfigSchema.parse(
1973
+ withoutLegacyToolLimit(JSON.parse(read(`workflows/${workflow}/config.json`)))
1974
+ ),
1943
1975
  toolText: JSON.parse(read("shared/tools.json"))
1944
1976
  };
1945
- if (workflow !== "file-summary" && !resource.config.tools.includes("read_file"))
1977
+ const native = resource.config.execution === "native-sandbox";
1978
+ if (workflow !== "file-summary" && !resource.config.tools.includes(native ? "read" : "read_file"))
1946
1979
  throw new Error("Reader required");
1947
1980
  if (workflow === "file-summary" && resource.config.tools.length)
1948
1981
  throw new Error("Direct workflow has no tools");
1949
1982
  if (workflow === "task-bootstrap" && resource.config.settleSeconds === void 0)
1950
1983
  throw new Error("Settle interval required");
1984
+ for (const tool of resource.config.tools)
1985
+ if (!["read", "bash", "grep", "find", "ls"].includes(tool) && !resource.toolText[tool])
1986
+ throw new Error("Missing tool description");
1951
1987
  return WorkflowRuntimeSchema.parse({
1952
1988
  ...resource,
1953
1989
  fingerprint: createHash6("sha256").update(JSON.stringify(resource)).digest("hex")
@@ -2002,35 +2038,49 @@ function applyTaskResult(hub, job, result) {
2002
2038
  "AI_SOURCE_CHANGED",
2003
2039
  "Workspace changed during analysis; analyse the new revision."
2004
2040
  );
2005
- const previous = hub.db.prepare("SELECT * FROM task_cards WHERE workspace_id=? AND owner=? AND active=1").all(job.workspace, job.snapshot.owner);
2041
+ const patch = result.updateMode === "patch";
2042
+ const previous = hub.db.prepare(
2043
+ "SELECT * FROM task_cards WHERE workspace_id=? AND owner=?" + (patch ? "" : " AND active=1")
2044
+ ).all(job.workspace, job.snapshot.owner);
2045
+ const previousById = new Map(previous.map((row) => [String(row.id), row]));
2046
+ const frozenIds = new Set(job.taskContext?.tasks.map((task) => task.taskId));
2006
2047
  const used = /* @__PURE__ */ new Set();
2007
2048
  const explicit = new Set(result.tasks.flatMap((task) => task.taskId ? [task.taskId] : []));
2049
+ const archive = new Set(result.archive ?? []);
2050
+ if (patch) {
2051
+ const ids = [...explicit, ...archive];
2052
+ if (explicit.size !== result.tasks.filter((task) => task.taskId).length || archive.size !== (result.archive ?? []).length || ids.some(
2053
+ (id) => explicit.has(id) && archive.has(id) || !previousById.has(id) || !frozenIds.has(id)
2054
+ ) || result.tasks.some((task) => task.taskId === void 0))
2055
+ throw new HubError(
2056
+ 422,
2057
+ "AI_TASK_ID_INVALID",
2058
+ "Task changes must reference unique identities in the frozen member context."
2059
+ );
2060
+ }
2008
2061
  for (const task of result.tasks) {
2009
- const candidates = previous.filter(
2010
- (row) => !used.has(String(row.id)) && !explicit.has(String(row.id))
2011
- );
2012
- let old = task.taskId ? previous.find((row) => row.id === task.taskId) : void 0;
2013
- if (task.taskId && (!old || used.has(task.taskId) || !job.taskContext?.tasks.some((item) => item.taskId === task.taskId)))
2062
+ let old = task.taskId ? previousById.get(task.taskId) : void 0;
2063
+ if (task.taskId && (!old || used.has(task.taskId) || !frozenIds.has(task.taskId)))
2014
2064
  throw new HubError(
2015
2065
  422,
2016
2066
  "AI_TASK_ID_INVALID",
2017
2067
  "Task identity is outside the frozen member context."
2018
2068
  );
2019
- if (task.taskId === void 0)
2020
- old = candidates.find(
2021
- (row) => paths(sources(row)) === paths(task.sources) && title(fields(row).title) === title(task.title)
2069
+ if (!patch && task.taskId === void 0)
2070
+ old = previous.find(
2071
+ (row) => !used.has(String(row.id)) && !explicit.has(String(row.id)) && paths(sources(row)) === paths(task.sources) && title(fields(row).title) === title(task.title)
2022
2072
  );
2023
- if (!old && task.taskId === void 0) {
2073
+ if (!patch && !old && task.taskId === void 0 && task.sources.length) {
2024
2074
  const primary = task.sources[0].path;
2025
- const oldGroup = previous.filter((row) => sources(row)[0].path === primary);
2026
- const newGroup = result.tasks.filter((item) => item.sources[0].path === primary);
2075
+ const oldGroup = previous.filter((row) => sources(row)[0]?.path === primary);
2076
+ const newGroup = result.tasks.filter((item) => item.sources[0]?.path === primary);
2027
2077
  if (oldGroup.length === 1 && newGroup.length === 1 && !used.has(String(oldGroup[0].id)) && !explicit.has(String(oldGroup[0].id)))
2028
2078
  old = oldGroup[0];
2029
2079
  }
2030
2080
  const id = old ? String(old.id) : randomUUID2();
2031
2081
  used.add(id);
2032
2082
  const { sources: source, taskId: _taskId, ...content } = task;
2033
- const syncedAt = source.map((item) => item.syncedAt).sort().at(-1);
2083
+ const syncedAt = source.map((item) => item.syncedAt).sort().at(-1) ?? "";
2034
2084
  hub.db.prepare(
2035
2085
  `INSERT INTO task_cards(id,workspace_id,owner,job_id,fields_json,sources_json,synced_at)
2036
2086
  VALUES(?,?,?,?,?,?,?) ON CONFLICT(id) DO UPDATE SET job_id=excluded.job_id,fields_json=excluded.fields_json,
@@ -2046,11 +2096,8 @@ function applyTaskResult(hub, job, result) {
2046
2096
  );
2047
2097
  }
2048
2098
  for (const old of previous)
2049
- if (!used.has(String(old.id))) {
2050
- const hasSource = sources(old).some(
2051
- (source) => job.snapshot.files.some((file) => file.path === source.path)
2052
- );
2053
- const retain = Boolean(old.correction_json) || result.coverage.partial && hasSource;
2099
+ if (patch ? archive.has(String(old.id)) : !used.has(String(old.id))) {
2100
+ const retain = Boolean(old.correction_json);
2054
2101
  hub.db.prepare("UPDATE task_cards SET active=?,needs_review=? WHERE id=?").run(retain ? 1 : 0, retain ? 1 : 0, old.id);
2055
2102
  }
2056
2103
  hub.db.prepare("UPDATE task_board_members SET applied_job_id=? WHERE workspace_id=? AND owner=?").run(job.id, job.workspace, job.snapshot.owner);
@@ -2068,9 +2115,9 @@ var TaskStore = class {
2068
2115
  owner: row.owner,
2069
2116
  jobId: row.job_id,
2070
2117
  sources: sources(row),
2071
- syncedAt: row.synced_at,
2118
+ syncedAt: row.synced_at || null,
2072
2119
  corrected: Boolean(row.correction_json),
2073
- needsReview: Boolean(row.needs_review)
2120
+ needsReview: Boolean(row.needs_review && row.correction_json)
2074
2121
  });
2075
2122
  }
2076
2123
  board(session) {
@@ -2097,8 +2144,7 @@ var TaskStore = class {
2097
2144
  status: row.status ?? (row.has_files ? "waiting" : "empty"),
2098
2145
  error: row.error ?? null,
2099
2146
  sourceChanged: snapshot ? !taskSnapshotCurrent(this.hub, session.workspaceId, snapshot) : false,
2100
- partial: result?.coverage.partial ?? false,
2101
- uncertainties: result ? [...result.uncertainties, ...result.coverage.limitations] : []
2147
+ uncertainties: result ? [...result.uncertainties ?? [], ...result.coverage.limitations] : []
2102
2148
  };
2103
2149
  }),
2104
2150
  modelAvailable: new AiDefaults(this.hub).project(session.workspaceId).available
@@ -2119,26 +2165,34 @@ var TaskStore = class {
2119
2165
  };
2120
2166
 
2121
2167
  // apps/server/ai/task-context.ts
2168
+ function changedPaths(beforeFiles, afterFiles) {
2169
+ const before = new Map(beforeFiles.map((file) => [file.path, file.sha256]));
2170
+ const after = new Map(afterFiles.map((file) => [file.path, file.sha256]));
2171
+ return {
2172
+ added: afterFiles.filter((file) => !before.has(file.path)).map((file) => file.path),
2173
+ modified: afterFiles.filter((file) => before.has(file.path) && before.get(file.path) !== file.sha256).map((file) => file.path),
2174
+ deleted: [...before.keys()].filter((path) => !after.has(path))
2175
+ };
2176
+ }
2122
2177
  function captureTaskContext(hub, workspace, snapshot) {
2123
2178
  const applied = hub.db.prepare(
2124
2179
  `SELECT j.snapshot_json FROM task_board_members b JOIN ai_jobs j ON j.id=b.applied_job_id
2125
2180
  WHERE b.workspace_id=? AND b.owner=?`
2126
2181
  ).get(workspace, snapshot.owner);
2127
2182
  const previous = applied ? JSON.parse(String(applied.snapshot_json)) : null;
2128
- const before = new Map(previous?.files.map((file) => [file.path, file.sha256]));
2129
- const after = new Map(snapshot.files.map((file) => [file.path, file.sha256]));
2130
2183
  return {
2131
2184
  previousRevision: previous?.revision ?? null,
2132
- changes: {
2133
- added: snapshot.files.filter((file) => !before.has(file.path)).map((file) => file.path),
2134
- modified: snapshot.files.filter((file) => before.has(file.path) && before.get(file.path) !== file.sha256).map((file) => file.path),
2135
- deleted: [...before.keys()].filter((path) => !after.has(path))
2136
- },
2185
+ baseline: previous,
2186
+ changes: changedPaths(previous?.files ?? [], snapshot.files),
2137
2187
  tasks: hub.db.prepare(
2138
- "SELECT id,fields_json,sources_json FROM task_cards WHERE workspace_id=? AND owner=? AND active=1 ORDER BY id"
2188
+ "SELECT id,fields_json,correction_json,sources_json,active FROM task_cards WHERE workspace_id=? AND owner=? ORDER BY id"
2139
2189
  ).all(workspace, snapshot.owner).map((row) => ({
2140
- ...JSON.parse(String(row.fields_json)),
2190
+ ...TaskFieldsSchema.parse({
2191
+ ...JSON.parse(String(row.fields_json)),
2192
+ ...row.correction_json ? JSON.parse(String(row.correction_json)) : {}
2193
+ }),
2141
2194
  taskId: String(row.id),
2195
+ active: Boolean(row.active),
2142
2196
  sources: JSON.parse(String(row.sources_json))
2143
2197
  }))
2144
2198
  };
@@ -2146,6 +2200,7 @@ function captureTaskContext(hub, workspace, snapshot) {
2146
2200
 
2147
2201
  // apps/server/ai/jobs.ts
2148
2202
  var now = () => (/* @__PURE__ */ new Date()).toISOString();
2203
+ var retryDelays = [5e3, 15e3, 45e3];
2149
2204
  var AiJobs = class {
2150
2205
  constructor(hub) {
2151
2206
  this.hub = hub;
@@ -2388,7 +2443,7 @@ var AiJobs = class {
2388
2443
  this.own(session, row);
2389
2444
  if (["queued", "running"].includes(String(row.status))) {
2390
2445
  this.hub.db.prepare(
2391
- "UPDATE ai_jobs SET status='cancelled',updated_at=?,run_token=NULL,lease_until=NULL WHERE id=?"
2446
+ "UPDATE ai_jobs SET status='cancelled',updated_at=?,run_token=NULL,lease_until=NULL,next_retry_at=NULL WHERE id=?"
2392
2447
  ).run(now(), id);
2393
2448
  this.hub.db.prepare(
2394
2449
  "UPDATE ai_attempts SET status='cancelled',ended_at=? WHERE job_id=? AND status='running'"
@@ -2408,20 +2463,49 @@ var AiJobs = class {
2408
2463
  if (!["failed", "cancelled"].includes(String(row.status)))
2409
2464
  throw new HubError(409, "AI_TASK_STATE", "Only failed or cancelled tasks can be retried.");
2410
2465
  this.hub.db.prepare(
2411
- "UPDATE ai_jobs SET status='queued',updated_at=?,error=NULL,result_json=NULL,recovery_count=0 WHERE id=?"
2466
+ "UPDATE ai_jobs SET status='queued',updated_at=?,error=NULL,result_json=NULL,recovery_count=0,auto_retry_count=0,next_retry_at=NULL WHERE id=?"
2412
2467
  ).run(now(), id);
2413
2468
  this.event(id, Number(row.attempt_count), "queued", "\u4F7F\u7528\u539F\u8F93\u5165\u5FEB\u7167\u91CD\u8BD5\u3002");
2414
2469
  return id;
2415
2470
  });
2416
2471
  return this.get(session.workspaceId, id);
2417
2472
  }
2473
+ cancelStaleTaskJobs() {
2474
+ return transaction(this.hub.db, () => {
2475
+ const rows = this.hub.db.prepare(
2476
+ "SELECT * FROM ai_jobs WHERE status IN ('queued','running') AND json_extract(request_json,'$.workflow')='task-bootstrap'"
2477
+ ).all();
2478
+ for (const row of rows) {
2479
+ if (taskSnapshotCurrent(
2480
+ this.hub,
2481
+ String(row.workspace_id),
2482
+ JSON.parse(String(row.snapshot_json))
2483
+ ))
2484
+ continue;
2485
+ this.hub.db.prepare(
2486
+ "UPDATE ai_jobs SET status='cancelled',error='AI_SOURCE_CHANGED',updated_at=?,run_token=NULL,lease_until=NULL,next_retry_at=NULL WHERE id=?"
2487
+ ).run(now(), row.id);
2488
+ this.hub.db.prepare(
2489
+ "UPDATE ai_attempts SET status='cancelled',ended_at=?,error='AI_SOURCE_CHANGED' WHERE job_id=? AND status='running'"
2490
+ ).run(now(), row.id);
2491
+ this.event(
2492
+ String(row.id),
2493
+ Number(row.attempt_count),
2494
+ "cancelled",
2495
+ "\u8D44\u6599\u5DF2\u66F4\u65B0\uFF0C\u53D6\u6D88\u65E7\u7248\u672C\u5206\u6790\uFF0C\u7B49\u5F85\u65B0\u7248\u672C\u6574\u7406\u3002"
2496
+ );
2497
+ }
2498
+ });
2499
+ }
2418
2500
  claim(at = Date.now()) {
2501
+ this.cancelStaleTaskJobs();
2419
2502
  return transaction(this.hub.db, () => {
2420
2503
  for (const old of this.hub.db.prepare("SELECT * FROM ai_jobs WHERE status='running' AND lease_until<=?").all(at)) {
2421
- const status = Number(old.recovery_count) < 1 ? "queued" : "failed";
2504
+ const retry = Number(old.auto_retry_count) < retryDelays.length;
2505
+ const status = retry ? "queued" : "failed";
2422
2506
  this.hub.db.prepare(
2423
- "UPDATE ai_jobs SET status=?,run_token=NULL,lease_until=NULL,error=?,updated_at=?,recovery_count=recovery_count+1 WHERE id=?"
2424
- ).run(status, "AI_WORKER_INTERRUPTED", now(), old.id);
2507
+ "UPDATE ai_jobs SET status=?,run_token=NULL,lease_until=NULL,error=?,updated_at=?,recovery_count=recovery_count+1,auto_retry_count=auto_retry_count+?,next_retry_at=NULL WHERE id=?"
2508
+ ).run(status, "AI_WORKER_INTERRUPTED", now(), retry ? 1 : 0, old.id);
2425
2509
  this.hub.db.prepare(
2426
2510
  "UPDATE ai_attempts SET status='interrupted',ended_at=?,error='AI_WORKER_INTERRUPTED' WHERE job_id=? AND status='running'"
2427
2511
  ).run(now(), old.id);
@@ -2434,11 +2518,13 @@ var AiJobs = class {
2434
2518
  }
2435
2519
  if (this.hub.db.prepare("SELECT 1 FROM ai_jobs WHERE status='running'").get())
2436
2520
  return void 0;
2437
- const row = this.hub.db.prepare("SELECT * FROM ai_jobs WHERE status='queued' ORDER BY created_at,id LIMIT 1").get();
2521
+ const row = this.hub.db.prepare(
2522
+ "SELECT * FROM ai_jobs WHERE status='queued' AND (next_retry_at IS NULL OR next_retry_at<=?) ORDER BY created_at,id LIMIT 1"
2523
+ ).get(at);
2438
2524
  if (!row) return void 0;
2439
2525
  const token = randomUUID3(), attempt = Number(row.attempt_count) + 1, date = now();
2440
2526
  this.hub.db.prepare(
2441
- "UPDATE ai_jobs SET status='running',attempt_count=?,run_token=?,lease_until=?,updated_at=?,error=NULL WHERE id=?"
2527
+ "UPDATE ai_jobs SET status='running',attempt_count=?,run_token=?,lease_until=?,updated_at=?,error=NULL,next_retry_at=NULL WHERE id=?"
2442
2528
  ).run(attempt, token, at + 3e4, date, row.id);
2443
2529
  this.hub.db.prepare("INSERT INTO ai_attempts(job_id,number,status,started_at) VALUES(?,?,?,?)").run(row.id, attempt, "running", date);
2444
2530
  this.event(String(row.id), attempt, "running", "\u5F00\u59CB\u5206\u6790\u3002");
@@ -2452,12 +2538,16 @@ var AiJobs = class {
2452
2538
  profile: JSON.parse(String(row.profile_json)),
2453
2539
  snapshot: JSON.parse(String(row.snapshot_json)),
2454
2540
  promptVersion: String(row.prompt_version),
2455
- runtime: row.runtime_json ? WorkflowRuntimeSchema.parse(JSON.parse(String(row.runtime_json))) : null,
2541
+ runtime: row.runtime_json ? parseWorkflowRuntime(JSON.parse(String(row.runtime_json))) : null,
2456
2542
  taskContext: row.task_context_json ? JSON.parse(String(row.task_context_json)) : null
2457
2543
  };
2458
2544
  });
2459
2545
  }
2460
2546
  heartbeat(job) {
2547
+ if (job.request.workflow === "task-bootstrap" && !taskSnapshotCurrent(this.hub, job.workspace, job.snapshot)) {
2548
+ this.cancelStaleTaskJobs();
2549
+ return false;
2550
+ }
2461
2551
  return this.hub.db.prepare(
2462
2552
  "UPDATE ai_jobs SET lease_until=? WHERE id=? AND run_token=? AND status='running' AND lease_until>?"
2463
2553
  ).run(Date.now() + 3e4, job.id, job.token, Date.now()).changes === 1;
@@ -2478,20 +2568,40 @@ var AiJobs = class {
2478
2568
  }
2479
2569
  finish(job, result, error, usage) {
2480
2570
  return transaction(this.hub.db, () => {
2481
- const status = result ? "succeeded" : "failed";
2571
+ const row = this.hub.db.prepare(
2572
+ "SELECT auto_retry_count,runtime_json FROM ai_jobs WHERE id=? AND run_token=? AND status='running' AND lease_until>?"
2573
+ ).get(job.id, job.token, Date.now());
2574
+ if (!row) return false;
2575
+ const obsolete = error === "AI_SOURCE_CHANGED" || error === "AI_TASK_SUPERSEDED";
2576
+ const retryCount = Number(row.auto_retry_count);
2577
+ const retry = !result && !obsolete && retryCount < retryDelays.length;
2578
+ const attemptStatus = result ? "succeeded" : obsolete ? "cancelled" : "failed";
2579
+ const status = retry ? "queued" : attemptStatus;
2482
2580
  if (result) {
2483
2581
  AiWorkflowResultSchema.parse(result);
2484
2582
  const taskResult = "kind" in result;
2485
2583
  if (job.request.workflow === "task-bootstrap" !== taskResult)
2486
2584
  throw new HubError(422, "AI_RESULT_INVALID", "Result does not match the workflow.");
2585
+ if (taskResult && row.runtime_json) {
2586
+ const runtime = JSON.parse(String(row.runtime_json));
2587
+ const expectsPatch = runtime.config.version >= 7;
2588
+ if (expectsPatch !== (result.updateMode === "patch"))
2589
+ throw new HubError(
2590
+ 422,
2591
+ "AI_RESULT_INVALID",
2592
+ "Task result does not match its frozen output contract."
2593
+ );
2594
+ }
2487
2595
  }
2488
2596
  const changed = this.hub.db.prepare(
2489
- "UPDATE ai_jobs SET status=?,result_json=?,error=?,run_token=NULL,lease_until=NULL,updated_at=? WHERE id=? AND run_token=? AND status='running' AND lease_until>?"
2597
+ "UPDATE ai_jobs SET status=?,result_json=?,error=?,run_token=NULL,lease_until=NULL,updated_at=?,auto_retry_count=auto_retry_count+?,next_retry_at=? WHERE id=? AND run_token=? AND status='running' AND lease_until>?"
2490
2598
  ).run(
2491
2599
  status,
2492
2600
  result ? JSON.stringify(result) : null,
2493
2601
  error,
2494
2602
  now(),
2603
+ retry ? 1 : 0,
2604
+ retry ? Date.now() + retryDelays[retryCount] : null,
2495
2605
  job.id,
2496
2606
  job.token,
2497
2607
  Date.now()
@@ -2500,8 +2610,27 @@ var AiJobs = class {
2500
2610
  if (result && "kind" in result) applyTaskResult(this.hub, job, result);
2501
2611
  this.hub.db.prepare(
2502
2612
  "UPDATE ai_attempts SET status=?,ended_at=?,error=?,usage_json=? WHERE job_id=? AND number=?"
2503
- ).run(status, now(), error, usage ? JSON.stringify(usage) : null, job.id, job.attempt);
2504
- this.event(job.id, job.attempt, status, result ? "\u5206\u6790\u5B8C\u6210\u3002" : error ?? "\u5206\u6790\u5931\u8D25\u3002");
2613
+ ).run(
2614
+ attemptStatus,
2615
+ now(),
2616
+ error,
2617
+ usage ? JSON.stringify(usage) : null,
2618
+ job.id,
2619
+ job.attempt
2620
+ );
2621
+ this.event(
2622
+ job.id,
2623
+ job.attempt,
2624
+ attemptStatus,
2625
+ result ? "\u5206\u6790\u5B8C\u6210\u3002" : error ?? "\u5206\u6790\u5931\u8D25\u3002"
2626
+ );
2627
+ if (retry)
2628
+ this.event(
2629
+ job.id,
2630
+ job.attempt,
2631
+ "queued",
2632
+ `\u5C06\u5728 ${retryDelays[retryCount] / 1e3} \u79D2\u540E\u81EA\u52A8\u91CD\u8BD5\uFF08${retryCount + 1}/3\uFF09\u3002`
2633
+ );
2505
2634
  }
2506
2635
  return changed === 1;
2507
2636
  });
@@ -2509,23 +2638,97 @@ var AiJobs = class {
2509
2638
  };
2510
2639
 
2511
2640
  // apps/server/ai/pi-runner.ts
2512
- import { mkdtemp, rm } from "node:fs/promises";
2513
- import { tmpdir } from "node:os";
2514
- import { join as join5 } from "node:path";
2641
+ import { mkdtemp as mkdtemp2, rm as rm2 } from "node:fs/promises";
2642
+ import { tmpdir as tmpdir2 } from "node:os";
2643
+ import { join as join7 } from "node:path";
2515
2644
  import { InMemoryCredentialStore, InMemoryModelsStore } from "@earendil-works/pi-ai";
2516
2645
 
2517
- // apps/server/ai/tools.ts
2646
+ // apps/server/ai/task-tools.ts
2518
2647
  import { Type } from "@earendil-works/pi-ai";
2519
2648
  import { z as z8 } from "zod";
2520
- function analysisTools(reader, signal, progress, runtime) {
2521
- let calls = 0;
2522
- const guard = () => {
2523
- signal.throwIfAborted();
2524
- if (++calls > runtime.config.maxToolCalls) {
2525
- reader.limitations.add("\u5DE5\u5177\u8C03\u7528\u5DF2\u8FBE\u5230\u672C\u6B21\u5206\u6790\u9884\u7B97\u3002");
2526
- throw new HubError(413, "AI_TOOL_BUDGET", "Tool call budget exhausted.");
2527
- }
2528
- };
2649
+ var scope = z8.enum(["member", "project"]).optional();
2650
+ var version = z8.enum(["current", "baseline"]).optional();
2651
+ var filter = {
2652
+ path: z8.string().optional(),
2653
+ query: z8.string().optional(),
2654
+ pattern: z8.string().optional()
2655
+ };
2656
+ function taskTools(explorer, signal, progress, runtime) {
2657
+ function tool(name, schema, execute) {
2658
+ const text = runtime.toolText[name];
2659
+ return {
2660
+ name,
2661
+ ...text,
2662
+ parameters: Type.Unsafe(z8.toJSONSchema(schema)),
2663
+ executionMode: "sequential",
2664
+ async execute(_id, params) {
2665
+ signal.throwIfAborted();
2666
+ const parsed = schema.parse(params);
2667
+ progress(name, `${text.label}\u3002`);
2668
+ const value = await execute(parsed);
2669
+ signal.throwIfAborted();
2670
+ return { content: [{ type: "text", text: JSON.stringify(value) }], details: {} };
2671
+ }
2672
+ };
2673
+ }
2674
+ return [
2675
+ tool(
2676
+ "list_files",
2677
+ z8.object({ ...filter, scope, version }).strict(),
2678
+ (p) => explorer.list(p, p.scope, p.version)
2679
+ ),
2680
+ tool(
2681
+ "read_file",
2682
+ z8.object({
2683
+ path: FilePathSchema,
2684
+ scope,
2685
+ version,
2686
+ startLine: z8.number().int().positive().optional(),
2687
+ limit: z8.number().int().positive().optional()
2688
+ }).strict(),
2689
+ (p) => explorer.reader(p.scope, p.version).read(p.path, p.startLine, p.limit)
2690
+ ),
2691
+ tool(
2692
+ "search_files",
2693
+ z8.object({
2694
+ path: filter.path,
2695
+ pattern: filter.pattern,
2696
+ query: z8.string().min(1),
2697
+ after: z8.string().optional(),
2698
+ scope,
2699
+ version
2700
+ }).strict(),
2701
+ (p) => explorer.search(p.query, p.after, { path: p.path, pattern: p.pattern }, p.scope, p.version)
2702
+ ),
2703
+ tool(
2704
+ "inspect_changes",
2705
+ z8.object({
2706
+ ...filter,
2707
+ scope,
2708
+ version,
2709
+ startLine: z8.number().int().positive().optional(),
2710
+ endLine: z8.number().int().positive().optional()
2711
+ }).strict(),
2712
+ (p) => explorer.inspect(p, p.scope)
2713
+ ),
2714
+ tool(
2715
+ "query_tasks",
2716
+ z8.object({
2717
+ id: z8.uuid().optional(),
2718
+ source: z8.string().optional(),
2719
+ query: z8.string().optional(),
2720
+ visibility: z8.enum(["active", "archived", "all"]).optional()
2721
+ }).strict(),
2722
+ (p) => explorer.queryTasks(p)
2723
+ )
2724
+ ].filter((tool2) => runtime.config.tools.some((name) => name === tool2.name));
2725
+ }
2726
+
2727
+ // apps/server/ai/tools.ts
2728
+ import { Type as Type2 } from "@earendil-works/pi-ai";
2729
+ import { z as z9 } from "zod";
2730
+ function analysisTools(reader, signal, progress, runtime, explorer) {
2731
+ if (explorer) return taskTools(explorer, signal, progress, runtime);
2529
2732
  const reply = (value) => ({
2530
2733
  content: [{ type: "text", text: JSON.stringify(value) }],
2531
2734
  details: {}
@@ -2534,44 +2737,44 @@ function analysisTools(reader, signal, progress, runtime) {
2534
2737
  {
2535
2738
  name: "list_files",
2536
2739
  ...runtime.toolText.list_files,
2537
- parameters: Type.Object(
2740
+ parameters: Type2.Object(
2538
2741
  {
2539
- after: Type.Optional(Type.String({ maxLength: 1024 })),
2540
- scope: Type.Optional(Type.Union([Type.Literal("member"), Type.Literal("project")]))
2742
+ after: Type2.Optional(Type2.String()),
2743
+ scope: Type2.Optional(Type2.Union([Type2.Literal("member"), Type2.Literal("project")]))
2541
2744
  },
2542
2745
  { additionalProperties: false }
2543
2746
  ),
2544
2747
  executionMode: "sequential",
2545
2748
  async execute(_id, params) {
2546
- guard();
2547
- const p = z8.object({
2548
- after: z8.string().max(1024).default(""),
2549
- scope: z8.enum(["member", "project"]).default("member")
2749
+ signal.throwIfAborted();
2750
+ const p = z9.object({
2751
+ after: z9.string().default(""),
2752
+ scope: z9.enum(["member", "project"]).default("member")
2550
2753
  }).strict().parse(params);
2551
2754
  progress("list", "\u6D4F\u89C8\u6587\u4EF6\u6E05\u5355\u3002");
2552
- return reply((p.scope === "project" ? reader.projectReader() : reader).list(p.after, 30));
2755
+ return reply((p.scope === "project" ? reader.projectReader() : reader).list(p.after));
2553
2756
  }
2554
2757
  },
2555
2758
  {
2556
2759
  name: "read_file",
2557
2760
  ...runtime.toolText.read_file,
2558
- parameters: Type.Object(
2761
+ parameters: Type2.Object(
2559
2762
  {
2560
- path: Type.String(),
2561
- scope: Type.Optional(Type.Union([Type.Literal("member"), Type.Literal("project")])),
2562
- startLine: Type.Optional(Type.Integer({ minimum: 1 })),
2563
- limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 150 }))
2763
+ path: Type2.String(),
2764
+ scope: Type2.Optional(Type2.Union([Type2.Literal("member"), Type2.Literal("project")])),
2765
+ startLine: Type2.Optional(Type2.Integer({ minimum: 1 })),
2766
+ limit: Type2.Optional(Type2.Integer({ minimum: 1 }))
2564
2767
  },
2565
2768
  { additionalProperties: false }
2566
2769
  ),
2567
2770
  executionMode: "sequential",
2568
2771
  async execute(_id, params) {
2569
- guard();
2570
- const p = z8.object({
2772
+ signal.throwIfAborted();
2773
+ const p = z9.object({
2571
2774
  path: FilePathSchema,
2572
- scope: z8.enum(["member", "project"]).default("member"),
2573
- startLine: z8.number().int().min(1).max(1e6).default(1),
2574
- limit: z8.number().int().min(1).max(150).default(150)
2775
+ scope: z9.enum(["member", "project"]).default("member"),
2776
+ startLine: z9.number().int().min(1).default(1),
2777
+ limit: z9.number().int().min(1).optional()
2575
2778
  }).strict().parse(params);
2576
2779
  progress("read", `\u8BFB\u53D6 ${p.path}\uFF0C\u7B2C ${p.startLine} \u884C\u8D77\u3002`);
2577
2780
  return reply(
@@ -2586,17 +2789,17 @@ function analysisTools(reader, signal, progress, runtime) {
2586
2789
  {
2587
2790
  name: "search_files",
2588
2791
  ...runtime.toolText.search_files,
2589
- parameters: Type.Object(
2792
+ parameters: Type2.Object(
2590
2793
  {
2591
- query: Type.String({ minLength: 1, maxLength: 200 }),
2592
- after: Type.Optional(Type.String({ maxLength: 1024 }))
2794
+ query: Type2.String({ minLength: 1 }),
2795
+ after: Type2.Optional(Type2.String())
2593
2796
  },
2594
2797
  { additionalProperties: false }
2595
2798
  ),
2596
2799
  executionMode: "sequential",
2597
2800
  async execute(_id, params) {
2598
- guard();
2599
- const p = z8.object({ query: z8.string().min(1).max(200), after: z8.string().max(1024).default("") }).strict().parse(params);
2801
+ signal.throwIfAborted();
2802
+ const p = z9.object({ query: z9.string().min(1), after: z9.string().default("") }).strict().parse(params);
2600
2803
  progress("search", "\u641C\u7D22\u5FEB\u7167\u6B63\u6587\u3002");
2601
2804
  return reply(await reader.search(p.query, p.after));
2602
2805
  }
@@ -2606,10 +2809,19 @@ function analysisTools(reader, signal, progress, runtime) {
2606
2809
  }
2607
2810
 
2608
2811
  // apps/server/ai/models.ts
2609
- import { createModels, createProvider, Type as Type2 } from "@earendil-works/pi-ai";
2812
+ import { createModels, createProvider, Type as Type3 } from "@earendil-works/pi-ai";
2610
2813
  import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
2611
2814
  import { openAIResponsesApi } from "@earendil-works/pi-ai/api/openai-responses.lazy";
2612
2815
  import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
2816
+ function withoutOutputLimits(payload) {
2817
+ const {
2818
+ max_tokens: _chat,
2819
+ max_completion_tokens: _completion,
2820
+ max_output_tokens: _response,
2821
+ ...rest
2822
+ } = payload;
2823
+ return rest;
2824
+ }
2613
2825
  function modelAccess(profile, key, reasoning = false) {
2614
2826
  const model = {
2615
2827
  id: profile.model,
@@ -2619,8 +2831,8 @@ function modelAccess(profile, key, reasoning = false) {
2619
2831
  baseUrl: profile.protocol === "anthropic-messages" ? profile.baseUrl.replace(/\/v1\/?$/, "") : profile.baseUrl,
2620
2832
  reasoning,
2621
2833
  input: ["text"],
2622
- contextWindow: profile.contextWindow,
2623
- maxTokens: profile.maxOutputTokens,
2834
+ contextWindow: 0,
2835
+ maxTokens: Number.MAX_SAFE_INTEGER,
2624
2836
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
2625
2837
  ...profile.protocol === "openai-completions" ? {
2626
2838
  compat: {
@@ -2666,11 +2878,11 @@ async function modelCall(profile, key, context, signal, extra = {}, thinkingLeve
2666
2878
  const { models, model } = modelAccess(profile, key, thinkingLevel !== "off");
2667
2879
  const options = {
2668
2880
  ...extra,
2669
- maxTokens: profile.maxOutputTokens,
2670
2881
  maxRetries: 0,
2671
- timeoutMs: extra.timeoutMs ?? 6e4,
2882
+ timeoutMs: extra.timeoutMs ?? 3e5,
2672
2883
  signal,
2673
- transport: "sse"
2884
+ transport: "sse",
2885
+ onPayload: async (payload, model2) => withoutOutputLimits(await extra.onPayload?.(payload, model2) ?? payload)
2674
2886
  };
2675
2887
  const stream = thinkingLevel === "off" ? models.stream(model, context, options) : models.streamSimple(model, context, { ...options, reasoning: thinkingLevel });
2676
2888
  let streamed = false;
@@ -2700,13 +2912,13 @@ async function probeModel(profile, key, mode, signal) {
2700
2912
  {
2701
2913
  name: "ping",
2702
2914
  description: "Get the verification marker.",
2703
- parameters: Type2.Object({ value: Type2.String() })
2915
+ parameters: Type3.Object({ value: Type3.String() })
2704
2916
  }
2705
2917
  ]
2706
2918
  } : {}
2707
2919
  };
2708
2920
  const first = await modelCall(
2709
- { ...profile, maxOutputTokens: Math.min(512, profile.maxOutputTokens) },
2921
+ profile,
2710
2922
  key,
2711
2923
  context,
2712
2924
  signal,
@@ -2762,8 +2974,522 @@ async function probeModel(profile, key, mode, signal) {
2762
2974
  return { ok: true, mode, streamed, inputTokens, outputTokens };
2763
2975
  }
2764
2976
 
2977
+ // apps/server/ai/native-sandbox.ts
2978
+ import { spawn } from "node:child_process";
2979
+ import { lstat, mkdtemp, readlink, rm } from "node:fs/promises";
2980
+ import { tmpdir } from "node:os";
2981
+ import { basename, dirname as dirname4, join as join6 } from "node:path";
2982
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
2983
+
2984
+ // apps/server/ai/agent-channel.ts
2985
+ import { createInterface } from "node:readline";
2986
+ var AgentChannel = class {
2987
+ constructor(input, output) {
2988
+ this.output = output;
2989
+ this.lines = createInterface({ input, crlfDelay: Infinity });
2990
+ this.iterator = this.lines[Symbol.asyncIterator]();
2991
+ output.on("error", () => {
2992
+ });
2993
+ }
2994
+ output;
2995
+ lines;
2996
+ iterator;
2997
+ writes = Promise.resolve();
2998
+ async *messages() {
2999
+ for await (const line of this.iterator) if (line) yield JSON.parse(line);
3000
+ }
3001
+ send(value) {
3002
+ const write = this.writes.then(
3003
+ () => new Promise((resolve4, reject) => {
3004
+ this.output.write(
3005
+ JSON.stringify(value) + "\n",
3006
+ (error) => error ? reject(error) : resolve4()
3007
+ );
3008
+ })
3009
+ );
3010
+ this.writes = write.catch(() => {
3011
+ });
3012
+ return write;
3013
+ }
3014
+ close() {
3015
+ this.lines.close();
3016
+ this.output.end();
3017
+ }
3018
+ };
3019
+
3020
+ // apps/server/ai/agent-workspace.ts
3021
+ import { mkdir as mkdir2, writeFile } from "node:fs/promises";
3022
+ import { dirname as dirname3, join as join5 } from "node:path";
3023
+
3024
+ // apps/server/ai/line-diff.ts
3025
+ import { Worker } from "node:worker_threads";
3026
+ async function lineChanges(before, after, signal) {
3027
+ signal?.throwIfAborted();
3028
+ const worker = new Worker(new URL("./line-diff-worker.js", import.meta.url), {
3029
+ workerData: { before, after }
3030
+ });
3031
+ let abort;
3032
+ try {
3033
+ return await new Promise((resolve4, reject) => {
3034
+ abort = () => reject(signal.reason);
3035
+ signal?.addEventListener("abort", abort, { once: true });
3036
+ worker.once("message", resolve4);
3037
+ worker.once("error", reject);
3038
+ worker.once(
3039
+ "exit",
3040
+ (code) => reject(new Error(`Line comparison exited without a result (${code}).`))
3041
+ );
3042
+ if (signal?.aborted) abort();
3043
+ });
3044
+ } finally {
3045
+ if (abort) signal?.removeEventListener("abort", abort);
3046
+ await worker.terminate();
3047
+ }
3048
+ }
3049
+
3050
+ // apps/server/ai/task-explorer.ts
3051
+ import { matchesGlob } from "node:path";
3052
+ var displayCount = 40;
3053
+ function abbreviated(items) {
3054
+ const shown = items.slice(0, displayCount), omitted = items.length - shown.length;
3055
+ return {
3056
+ items: shown,
3057
+ total: items.length,
3058
+ omitted,
3059
+ ...omitted ? { note: `\u2026\u2026\u7B49 ${omitted} \u9879\uFF1B\u53EF\u7F29\u5C0F\u67E5\u8BE2\u8303\u56F4\u3002` } : {}
3060
+ };
3061
+ }
3062
+ function matchesFile(path, filter2) {
3063
+ const prefix = filter2.path?.replace(/\/$/, "") ?? "";
3064
+ return (!prefix || path === prefix || path.startsWith(prefix + "/")) && (!filter2.query || path.toLowerCase().includes(filter2.query.toLowerCase())) && (!filter2.pattern || matchesGlob(path, filter2.pattern));
3065
+ }
3066
+ var TaskExplorer = class {
3067
+ constructor(current, context, signal) {
3068
+ this.current = current;
3069
+ this.context = context;
3070
+ this.signal = signal;
3071
+ this.baseline = context?.baseline ? current.forSnapshot(context.baseline) : void 0;
3072
+ }
3073
+ current;
3074
+ context;
3075
+ signal;
3076
+ baseline;
3077
+ ranges = /* @__PURE__ */ new Map();
3078
+ reader(scope2 = "member", version2 = "current") {
3079
+ const reader = version2 === "current" ? this.current : this.baseline;
3080
+ if (!reader)
3081
+ throw new HubError(
3082
+ 404,
3083
+ "AI_BASELINE_UNAVAILABLE",
3084
+ "There is no successful baseline snapshot."
3085
+ );
3086
+ return scope2 === "project" ? reader.projectReader() : reader;
3087
+ }
3088
+ files(scope2, version2) {
3089
+ const snapshot = version2 === "current" ? this.current.snapshot : this.context?.baseline;
3090
+ return (scope2 === "project" ? snapshot?.project?.files : snapshot?.files) ?? [];
3091
+ }
3092
+ list(filter2 = {}, scope2 = "member", version2 = "current") {
3093
+ this.reader(scope2, version2);
3094
+ const files = this.files(scope2, version2).filter((file) => matchesFile(file.path, filter2));
3095
+ const prefix = filter2.path?.replace(/\/$/, "") ?? "";
3096
+ const directories = /* @__PURE__ */ new Map();
3097
+ for (const file of files) {
3098
+ const relative = prefix && file.path.startsWith(prefix + "/") ? file.path.slice(prefix.length + 1) : file.path;
3099
+ if (!relative.includes("/")) continue;
3100
+ const path = (prefix ? prefix + "/" : "") + relative.split("/")[0];
3101
+ directories.set(path, (directories.get(path) ?? 0) + 1);
3102
+ }
3103
+ return {
3104
+ scope: scope2,
3105
+ version: version2,
3106
+ files: abbreviated(files.map(({ path, size }) => ({ path, size }))),
3107
+ directories: abbreviated([...directories].map(([path, files2]) => ({ path, files: files2 })))
3108
+ };
3109
+ }
3110
+ async search(query, after = "", filter2 = {}, scope2 = "member", version2 = "current") {
3111
+ const reader = this.reader(scope2, version2);
3112
+ return reader.search(
3113
+ query,
3114
+ after,
3115
+ reader.snapshot.files.filter((file) => matchesFile(file.path, filter2))
3116
+ );
3117
+ }
3118
+ changes(filter2 = {}, scope2 = "member") {
3119
+ const changes = changedPaths(this.files(scope2, "baseline"), this.files(scope2, "current"));
3120
+ return Object.entries(changes).flatMap(([kind, paths2]) => paths2.map((path) => ({ path, kind }))).filter((file) => matchesFile(file.path, filter2)).sort((a, b) => a.path.localeCompare(b.path));
3121
+ }
3122
+ overview() {
3123
+ return {
3124
+ owner: this.current.snapshot.owner,
3125
+ revision: this.current.snapshot.revision,
3126
+ mode: this.context?.baseline ? "incremental" : "cold_start",
3127
+ baselineRevision: this.context?.previousRevision ?? null,
3128
+ inventory: this.list(),
3129
+ projectReferences: this.current.snapshot.project ? this.list({}, "project") : null,
3130
+ changes: {
3131
+ member: abbreviated(this.changes()),
3132
+ project: abbreviated(this.changes({}, "project"))
3133
+ },
3134
+ previousTasks: {
3135
+ active: this.context?.tasks.filter((task) => task.active !== false).length ?? 0,
3136
+ archived: this.context?.tasks.filter((task) => task.active === false).length ?? 0
3137
+ }
3138
+ };
3139
+ }
3140
+ queryTasks(filter2 = {}) {
3141
+ const visibility = filter2.visibility ?? (filter2.id ? "all" : "active");
3142
+ return abbreviated(
3143
+ (this.context?.tasks ?? []).filter(
3144
+ (task) => (!filter2.id || task.taskId === filter2.id) && (visibility === "all" || (visibility === "active" ? task.active !== false : task.active === false)) && (!filter2.source || task.sources.some((source) => matchesFile(source.path, { path: filter2.source }))) && (!filter2.query || [
3145
+ task.title,
3146
+ task.goal ?? "",
3147
+ task.progress,
3148
+ ...task.sources.map((source) => source.path)
3149
+ ].some((text) => text.toLowerCase().includes(filter2.query.toLowerCase())))
3150
+ )
3151
+ );
3152
+ }
3153
+ async inspect(filter2 = {}, scope2 = "member") {
3154
+ const changes = this.changes(filter2, scope2);
3155
+ const response = abbreviated(changes);
3156
+ const items = [];
3157
+ for (const change of response.items) {
3158
+ this.signal.throwIfAborted();
3159
+ const before = this.files(scope2, "baseline").find((file) => file.path === change.path);
3160
+ const after = this.files(scope2, "current").find((file) => file.path === change.path);
3161
+ const key = `${before?.sha256 ?? ""}:${after?.sha256 ?? ""}`;
3162
+ try {
3163
+ let pending = this.ranges.get(key);
3164
+ if (!pending) {
3165
+ pending = (async () => lineChanges(
3166
+ before ? await this.reader(scope2, "baseline").text(change.path) : "",
3167
+ after ? await this.reader(scope2).text(change.path) : "",
3168
+ this.signal
3169
+ ))();
3170
+ this.ranges.set(key, pending);
3171
+ }
3172
+ const ranges = await pending;
3173
+ this.signal.throwIfAborted();
3174
+ const selected = ranges.filter((range) => {
3175
+ const location = filter2.version === "baseline" ? range.before : range.after;
3176
+ return (filter2.startLine === void 0 || location.startLine + Math.max(1, location.count) - 1 >= filter2.startLine) && (filter2.endLine === void 0 || location.startLine <= filter2.endLine);
3177
+ });
3178
+ items.push({ ...change, available: true, ranges: abbreviated(selected) });
3179
+ } catch (error) {
3180
+ this.signal.throwIfAborted();
3181
+ items.push({
3182
+ ...change,
3183
+ available: false,
3184
+ reason: error instanceof HubError ? error.code : "DIFF_UNAVAILABLE"
3185
+ });
3186
+ }
3187
+ }
3188
+ return { scope: scope2, ...response, items };
3189
+ }
3190
+ };
3191
+
3192
+ // apps/server/ai/agent-workspace.ts
3193
+ async function materializeWorkspace(root, reader, signal) {
3194
+ const owner = FileOwnerSchema.parse(reader.snapshot.owner);
3195
+ await mkdir2(join5(root, owner), { recursive: true });
3196
+ async function copy(source, prefix, shared = false) {
3197
+ for (const file of source.snapshot.files) {
3198
+ signal.throwIfAborted();
3199
+ const path = FilePathSchema.parse(file.path);
3200
+ if (shared && !sharedPathAllowed(path, DEFAULT_SHARED_PATHS)) continue;
3201
+ const target = join5(root, prefix, path);
3202
+ await mkdir2(dirname3(target), { recursive: true });
3203
+ await writeFile(target, await source.bytes(path), { flag: "wx", mode: 292 });
3204
+ }
3205
+ }
3206
+ await copy(reader, owner);
3207
+ if (reader.snapshot.project) await copy(reader.projectReader(), "", true);
3208
+ }
3209
+ function memberSourcePath(path, owner) {
3210
+ const relative = path.replace(/^\/workspaces\//, "").replace(/^\.\//, "");
3211
+ return relative.startsWith(owner + "/") ? relative.slice(owner.length + 1) : null;
3212
+ }
3213
+ async function taskInput(reader, context, signal) {
3214
+ const owner = reader.snapshot.owner;
3215
+ const lines = [
3216
+ `\u672C\u8F6E\u7EF4\u62A4\u6210\u5458 ${owner} \u7684\u4EFB\u52A1\u6E05\u5355\u3002`,
3217
+ `\u6A21\u5F0F\uFF1A${context?.baseline ? "\u589E\u91CF\u7EF4\u62A4" : "\u9996\u6B21\u6574\u7406"}\uFF1B\u5F53\u524D\u8D44\u6599\uFF1A/workspaces/${owner}/\u3002`,
3218
+ "\u516C\u5171\u8BF4\u660E\u53CA\u6A21\u677F\uFF08\u5982\u6709\uFF09\uFF1A/workspaces/README.md\u3001/workspaces/_template/\u3002",
3219
+ context?.baseline ? "\u5386\u53F2\u57FA\u7EBF\uFF1A/baseline/workspaces/\u3002" : "\u672C\u8F6E\u6CA1\u6709\u5386\u53F2\u57FA\u7EBF\u3002"
3220
+ ];
3221
+ if (context?.baseline) {
3222
+ const explorer = new TaskExplorer(reader, context, signal);
3223
+ const changes = [
3224
+ ...explorer.changes().map((change) => ({ ...change, scope: "member" })),
3225
+ ...explorer.changes({}, "project").map((change) => ({ ...change, scope: "project" }))
3226
+ ];
3227
+ lines.push("\u672C\u8F6E\u53D8\u5316\uFF1A");
3228
+ for (const change of changes.slice(0, 20)) {
3229
+ signal.throwIfAborted();
3230
+ const path = change.scope === "member" ? `${owner}/${change.path}` : change.path;
3231
+ let detail = "";
3232
+ if (change.kind === "modified") {
3233
+ const diff = await explorer.inspect({ path: change.path }, change.scope);
3234
+ const item = diff.items[0];
3235
+ if (item?.available) {
3236
+ detail = "\uFF1B\u53D8\u5316\u884C\uFF08\u65E7\u2192\u65B0\uFF09" + item.ranges.items.slice(0, 3).map(
3237
+ (range) => `${range.before.startLine}+${range.before.count}\u2192${range.after.startLine}+${range.after.count}`
3238
+ ).join("\u3001");
3239
+ if (item.ranges.total > 3) detail += `\uFF0C\u53E6\u6709 ${item.ranges.total - 3} \u5904`;
3240
+ }
3241
+ }
3242
+ lines.push(`- ${change.kind} ${JSON.stringify(path)}${detail}`);
3243
+ }
3244
+ if (changes.length > 20) lines.push(`\u2026\u2026\u53E6\u6709 ${changes.length - 20} \u4E2A\u6587\u4EF6\u53D1\u751F\u53D8\u5316\u3002`);
3245
+ if (!changes.length) lines.push("- \u65E0\u6587\u4EF6\u53D8\u5316\u3002");
3246
+ }
3247
+ lines.push("\u5DF2\u6709\u4EFB\u52A1\u53EF\u901A\u8FC7 query_tasks \u67E5\u8BE2\u3002");
3248
+ return lines.join("\n");
3249
+ }
3250
+
3251
+ // apps/server/ai/agent-protocol.ts
3252
+ import { z as z10 } from "zod";
3253
+ var TaskQuerySchema = z10.object({
3254
+ id: z10.uuid().optional(),
3255
+ source: z10.string().optional(),
3256
+ query: z10.string().optional(),
3257
+ visibility: z10.enum(["active", "archived", "all"]).optional()
3258
+ }).strict();
3259
+
3260
+ // apps/server/ai/task-query.ts
3261
+ function queryTasks(context, owner, input) {
3262
+ const filter2 = TaskQuerySchema.parse(input);
3263
+ const visibility = filter2.visibility ?? (filter2.id ? "all" : "active");
3264
+ return abbreviated(
3265
+ (context?.tasks ?? []).map((task) => ({
3266
+ ...task,
3267
+ sources: task.sources.map((source) => ({ ...source, path: `${owner}/${source.path}` }))
3268
+ })).filter(
3269
+ (task) => (!filter2.id || task.taskId === filter2.id) && (visibility === "all" || (visibility === "active" ? task.active !== false : task.active === false)) && (!filter2.source || task.sources.some((source) => matchesFile(source.path, { path: filter2.source }))) && (!filter2.query || [
3270
+ task.title,
3271
+ task.goal ?? "",
3272
+ task.progress,
3273
+ ...task.sources.map((source) => source.path)
3274
+ ].some((text) => text.toLowerCase().includes(filter2.query.toLowerCase())))
3275
+ )
3276
+ );
3277
+ }
3278
+
3279
+ // apps/server/ai/native-sandbox.ts
3280
+ async function sandboxCommand(root, baseline) {
3281
+ if (process.platform !== "linux")
3282
+ throw new HubError(
3283
+ 503,
3284
+ "AI_SANDBOX_UNAVAILABLE",
3285
+ "Native analysis requires a Linux worker with Bubblewrap."
3286
+ );
3287
+ let dependencies = dirname4(fileURLToPath2(import.meta.resolve("@earendil-works/pi-coding-agent")));
3288
+ while (basename(dependencies) !== "node_modules") {
3289
+ const parent = dirname4(dependencies);
3290
+ if (parent === dependencies) throw new Error("Pi dependency root not found.");
3291
+ dependencies = parent;
3292
+ }
3293
+ const args = [
3294
+ "--unshare-all",
3295
+ "--unshare-user",
3296
+ "--die-with-parent",
3297
+ "--new-session",
3298
+ "--cap-drop",
3299
+ "ALL",
3300
+ "--ro-bind",
3301
+ "/usr",
3302
+ "/usr"
3303
+ ];
3304
+ for (const path of ["/bin", "/sbin", "/lib", "/lib64"]) {
3305
+ const stat = await lstat(path).catch(() => null);
3306
+ if (!stat) continue;
3307
+ if (stat.isSymbolicLink()) args.push("--symlink", await readlink(path), path);
3308
+ else args.push("--ro-bind", path, path);
3309
+ }
3310
+ if (await lstat("/etc/alternatives").catch(() => null))
3311
+ args.push("--ro-bind", "/etc/alternatives", "/etc/alternatives");
3312
+ args.push(
3313
+ "--proc",
3314
+ "/proc",
3315
+ "--dev",
3316
+ "/dev",
3317
+ "--tmpfs",
3318
+ "/tmp",
3319
+ "--dir",
3320
+ "/home/agent",
3321
+ "--setenv",
3322
+ "HOME",
3323
+ "/home/agent",
3324
+ "--setenv",
3325
+ "PATH",
3326
+ "/opt/node-bin:/usr/local/bin:/usr/bin:/bin",
3327
+ "--setenv",
3328
+ "LANG",
3329
+ "C.UTF-8",
3330
+ "--setenv",
3331
+ "SHELL",
3332
+ "/bin/bash",
3333
+ "--setenv",
3334
+ "PI_SKIP_VERSION_CHECK",
3335
+ "1",
3336
+ "--ro-bind",
3337
+ process.execPath,
3338
+ "/opt/node-bin/node",
3339
+ "--ro-bind",
3340
+ dependencies,
3341
+ "/opt/teammy/node_modules",
3342
+ "--ro-bind",
3343
+ fileURLToPath2(new URL("./native-pi-entry.js", import.meta.url)),
3344
+ "/opt/teammy/native-pi-entry.mjs",
3345
+ "--ro-bind",
3346
+ join6(root, "workspaces"),
3347
+ "/workspaces"
3348
+ );
3349
+ if (baseline) args.push("--ro-bind", join6(root, "baseline/workspaces"), "/baseline/workspaces");
3350
+ args.push("--chdir", "/workspaces", "/opt/node-bin/node", "/opt/teammy/native-pi-entry.mjs");
3351
+ return { command: "bwrap", args };
3352
+ }
3353
+ async function runNativeAgent(profile, reader, key, signal, progress, runtime, systemPrompt, input, context = null) {
3354
+ signal.throwIfAborted();
3355
+ const root = await mkdtemp(join6(tmpdir(), "teammy-agent-"));
3356
+ try {
3357
+ await materializeWorkspace(join6(root, "workspaces"), reader, signal);
3358
+ if (context?.baseline)
3359
+ await materializeWorkspace(
3360
+ join6(root, "baseline/workspaces"),
3361
+ reader.forSnapshot(context.baseline),
3362
+ signal
3363
+ );
3364
+ const { command, args } = await sandboxCommand(root, Boolean(context?.baseline));
3365
+ signal.throwIfAborted();
3366
+ const child = spawn(command, args, {
3367
+ stdio: ["pipe", "pipe", "pipe"],
3368
+ detached: true,
3369
+ env: { PATH: "/usr/bin:/bin" }
3370
+ });
3371
+ const channel = new AgentChannel(child.stdout, child.stdin);
3372
+ const active = new AbortController();
3373
+ const requestSignal = AbortSignal.any([signal, active.signal]);
3374
+ let failure, text, rounds = 0;
3375
+ const usage = { inputTokens: 0, outputTokens: 0 };
3376
+ const stop = () => {
3377
+ active.abort();
3378
+ if (child.pid) {
3379
+ try {
3380
+ process.kill(-child.pid, "SIGKILL");
3381
+ } catch {
3382
+ }
3383
+ }
3384
+ };
3385
+ const closed = new Promise((resolve4) => {
3386
+ child.once("error", () => {
3387
+ failure ??= new HubError(
3388
+ 503,
3389
+ "AI_SANDBOX_UNAVAILABLE",
3390
+ "Could not start Bubblewrap; check the Linux worker setup."
3391
+ );
3392
+ });
3393
+ child.once("close", (code) => {
3394
+ if (code !== 0 && !signal.aborted)
3395
+ failure ??= new HubError(
3396
+ 503,
3397
+ "AI_SANDBOX_FAILED",
3398
+ "Sandbox exited unsuccessfully; check worker isolation support."
3399
+ );
3400
+ active.abort();
3401
+ resolve4();
3402
+ });
3403
+ });
3404
+ child.stderr.resume();
3405
+ signal.addEventListener("abort", stop, { once: true });
3406
+ const pending = /* @__PURE__ */ new Set();
3407
+ const handle = async (message) => {
3408
+ if (message.type === "progress") {
3409
+ if (runtime.config.tools.some((tool) => tool === message.tool))
3410
+ progress("tool", `\u4F7F\u7528 ${message.tool}\u3002`);
3411
+ } else if (message.type === "result") {
3412
+ if (typeof message.text !== "string" || text !== void 0)
3413
+ throw new Error("Invalid result.");
3414
+ text = message.text;
3415
+ } else if (message.type === "failed") {
3416
+ throw new HubError(422, "AI_RESULT_INVALID", "Agent did not complete analysis.");
3417
+ } else if (message.type === "request") {
3418
+ let value;
3419
+ if (message.method === "model") {
3420
+ if (++rounds > runtime.config.maxRounds)
3421
+ throw new HubError(413, "AI_ROUND_LIMIT", "Analysis exceeded its model round budget.");
3422
+ progress("model", `\u6A21\u578B\u5206\u6790\u7B2C ${rounds} \u8F6E\u3002`);
3423
+ const response = await modelCall(
3424
+ profile,
3425
+ key,
3426
+ message.input,
3427
+ requestSignal,
3428
+ { timeoutMs: runtime.config.requestTimeoutMs },
3429
+ runtime.config.thinkingLevel
3430
+ );
3431
+ value = response.message;
3432
+ usage.inputTokens += response.message.usage.input;
3433
+ usage.outputTokens += response.message.usage.output;
3434
+ } else if (message.method === "query_tasks" && runtime.config.tools.includes("query_tasks")) {
3435
+ value = queryTasks(context, reader.snapshot.owner, message.input);
3436
+ } else throw new HubError(403, "AI_TOOL_UNAVAILABLE", "Tool is unavailable for this job.");
3437
+ await channel.send({ type: "response", id: message.id, value });
3438
+ } else throw new Error("Invalid sandbox message.");
3439
+ };
3440
+ try {
3441
+ const { model } = modelAccess(
3442
+ { ...profile, baseUrl: "http://model.invalid" },
3443
+ "",
3444
+ runtime.config.thinkingLevel !== "off"
3445
+ );
3446
+ await channel.send({ type: "start", config: { model, runtime, systemPrompt, input } });
3447
+ if (signal.aborted) stop();
3448
+ for await (const message of channel.messages()) {
3449
+ const task = handle(message).catch((error) => {
3450
+ failure ??= error;
3451
+ stop();
3452
+ });
3453
+ pending.add(task);
3454
+ void task.finally(() => pending.delete(task));
3455
+ }
3456
+ await closed;
3457
+ await Promise.all(pending);
3458
+ signal.throwIfAborted();
3459
+ if (failure) throw failure;
3460
+ if (text === void 0) throw new HubError(422, "AI_RESULT_EMPTY", "No analysis result.");
3461
+ return { text, usage };
3462
+ } catch (error) {
3463
+ stop();
3464
+ await closed;
3465
+ signal.throwIfAborted();
3466
+ throw failure ?? error;
3467
+ } finally {
3468
+ signal.removeEventListener("abort", stop);
3469
+ stop();
3470
+ channel.close();
3471
+ await closed;
3472
+ await Promise.allSettled(pending);
3473
+ }
3474
+ } finally {
3475
+ await rm(root, { recursive: true, force: true });
3476
+ }
3477
+ }
3478
+
2765
3479
  // apps/server/ai/pi-runner.ts
2766
- async function runAgent(profile, reader, key, signal, progress, runtime, systemPrompt, input) {
3480
+ async function runAgent(profile, reader, key, signal, progress, runtime, systemPrompt, input, explorer, context = null) {
3481
+ if (runtime.config.execution === "native-sandbox")
3482
+ return runNativeAgent(
3483
+ profile,
3484
+ reader,
3485
+ key,
3486
+ signal,
3487
+ progress,
3488
+ runtime,
3489
+ systemPrompt,
3490
+ typeof input === "string" ? input : JSON.stringify(input),
3491
+ context
3492
+ );
2767
3493
  const {
2768
3494
  createAgentSession,
2769
3495
  createExtensionRuntime,
@@ -2771,7 +3497,7 @@ async function runAgent(profile, reader, key, signal, progress, runtime, systemP
2771
3497
  SessionManager,
2772
3498
  SettingsManager
2773
3499
  } = await import("@earendil-works/pi-coding-agent");
2774
- const cwd = await mkdtemp(join5(tmpdir(), "teammy-ai-run-"));
3500
+ const cwd = await mkdtemp2(join7(tmpdir2(), "teammy-ai-run-"));
2775
3501
  try {
2776
3502
  const { models, model } = modelAccess(profile, key, runtime.config.thinkingLevel !== "off");
2777
3503
  const modelRuntime = await ModelRuntime.create({
@@ -2812,29 +3538,29 @@ async function runAgent(profile, reader, key, signal, progress, runtime, systemP
2812
3538
  cacheWarming: "off"
2813
3539
  }),
2814
3540
  tools: runtime.config.tools,
2815
- customTools: analysisTools(reader, signal, progress, runtime)
3541
+ customTools: analysisTools(reader, signal, progress, runtime, explorer)
2816
3542
  });
2817
3543
  let requests = 0, budgetError;
2818
3544
  const usage = { inputTokens: 0, outputTokens: 0 };
2819
- session.agent.streamFunction = (_model, context, options) => {
3545
+ session.agent.streamFunction = (_model, context2, options) => {
2820
3546
  signal.throwIfAborted();
2821
- if (++requests > runtime.config.maxRounds || Buffer.byteLength(JSON.stringify(context)) + profile.maxOutputTokens > profile.contextWindow) {
3547
+ if (++requests > runtime.config.maxRounds) {
2822
3548
  budgetError = new HubError(
2823
3549
  413,
2824
- "AI_CONTEXT_OR_ROUND_LIMIT",
2825
- "Analysis exceeded its context or model round budget."
3550
+ "AI_ROUND_LIMIT",
3551
+ "Analysis exceeded its model round budget."
2826
3552
  );
2827
3553
  throw budgetError;
2828
3554
  }
2829
3555
  progress("model", `\u6A21\u578B\u5206\u6790\u7B2C ${requests} \u8F6E\u3002`);
2830
- return models.streamSimple(model, context, {
3556
+ return models.streamSimple(model, context2, {
2831
3557
  ...options,
2832
3558
  ...runtime.config.thinkingLevel !== "off" ? { reasoning: runtime.config.thinkingLevel } : {},
2833
3559
  signal: options?.signal ? AbortSignal.any([signal, options.signal]) : signal,
2834
3560
  maxRetries: 0,
2835
- maxTokens: profile.maxOutputTokens,
2836
3561
  timeoutMs: runtime.config.requestTimeoutMs,
2837
- transport: "sse"
3562
+ transport: "sse",
3563
+ onPayload: async (payload, model2) => withoutOutputLimits(await options?.onPayload?.(payload, model2) ?? payload)
2838
3564
  });
2839
3565
  };
2840
3566
  const unsubscribe = session.subscribe((event) => {
@@ -2861,25 +3587,25 @@ async function runAgent(profile, reader, key, signal, progress, runtime, systemP
2861
3587
  session.dispose();
2862
3588
  }
2863
3589
  } finally {
2864
- await rm(cwd, { recursive: true, force: true });
3590
+ await rm2(cwd, { recursive: true, force: true });
2865
3591
  }
2866
3592
  }
2867
3593
 
2868
3594
  // apps/server/ai/results.ts
2869
- import { z as z9 } from "zod";
2870
- var SummaryOutputSchema = z9.object({
2871
- summary: z9.string().min(1).max(2e4),
2872
- findings: z9.array(z9.string().max(2e3)).max(30),
2873
- uncertainties: z9.array(z9.string().max(2e3)).max(30),
2874
- citations: z9.array(
2875
- z9.object({
2876
- path: FilePathSchema,
2877
- startLine: z9.number().int().positive(),
2878
- endLine: z9.number().int().positive()
3595
+ import { z as z11 } from "zod";
3596
+ var SummaryOutputSchema = z11.object({
3597
+ summary: z11.string(),
3598
+ findings: z11.array(z11.string()),
3599
+ uncertainties: z11.array(z11.string()).optional(),
3600
+ citations: z11.array(
3601
+ z11.object({
3602
+ path: z11.string(),
3603
+ startLine: z11.number().int().positive(),
3604
+ endLine: z11.number().int().positive()
2879
3605
  }).strict()
2880
- ).min(1).max(50)
3606
+ )
2881
3607
  }).strict();
2882
- function validateResult(text, reader) {
3608
+ function validateResult(text, reader, native = false) {
2883
3609
  let output;
2884
3610
  try {
2885
3611
  output = SummaryOutputSchema.parse(
@@ -2892,29 +3618,29 @@ function validateResult(text, reader) {
2892
3618
  "Analysis did not produce the required JSON result."
2893
3619
  );
2894
3620
  }
2895
- const citations = output.citations.map((c) => {
2896
- const file = reader.snapshot.files.find((f) => f.path === c.path);
2897
- if (!file || !reader.readRanges.get(c.path)?.some(
2898
- ([start, end]) => c.startLine >= start && c.endLine <= end && c.endLine >= c.startLine
2899
- ))
2900
- throw new HubError(
2901
- 422,
2902
- "AI_CITATION_INVALID",
2903
- "Analysis cited a file or line range it did not read."
2904
- );
2905
- return { ...c, sha256: file.sha256 };
3621
+ const citations = output.citations.flatMap((c) => {
3622
+ const path = native ? memberSourcePath(c.path, reader.snapshot.owner) : c.path;
3623
+ const file = reader.snapshot.files.find((f) => f.path === path);
3624
+ if (c.endLine < c.startLine)
3625
+ throw new HubError(422, "AI_CITATION_INVALID", "Citation line range must be ordered.");
3626
+ if (!file) {
3627
+ reader.limitations.add(`\u5F15\u7528\u672A\u80FD\u5173\u8054\u5230\u672C\u8F6E\u8D44\u6599\uFF1A${c.path}`);
3628
+ return [];
3629
+ }
3630
+ return [{ ...c, path: file.path, sha256: file.sha256 }];
2906
3631
  });
2907
3632
  return AiResultSchema.parse({
2908
3633
  ...output,
2909
3634
  citations,
2910
- coverage: coverage(reader)
3635
+ uncertainties: output.uncertainties ?? [],
3636
+ coverage: coverage(reader, native)
2911
3637
  });
2912
3638
  }
2913
- function coverage(reader) {
3639
+ function coverage(reader, native = false) {
2914
3640
  return {
2915
3641
  totalFiles: reader.snapshot.files.length,
2916
3642
  readFiles: [...reader.readFiles],
2917
- partial: reader.snapshot.files.some((f) => !reader.fullyRead(f.path)) || reader.limitations.size > 0,
3643
+ ...native ? { accessTracking: "unavailable" } : {},
2918
3644
  limitations: [...reader.limitations]
2919
3645
  };
2920
3646
  }
@@ -2931,25 +3657,23 @@ async function analyzeDirectory(job, reader, key, signal, progress, runtime) {
2931
3657
  workflowPrompt(runtime, SummaryOutputSchema),
2932
3658
  {
2933
3659
  owner: job.snapshot.owner,
3660
+ workspace: `/workspaces/${job.snapshot.owner}/`,
2934
3661
  scope: job.snapshot.path || "/",
2935
3662
  revision: job.snapshot.revision,
2936
3663
  totalFiles: job.snapshot.files.length,
2937
3664
  question: job.request.question
2938
3665
  }
2939
3666
  );
2940
- return { result: validateResult(output.text, reader), usage: output.usage };
3667
+ return {
3668
+ result: validateResult(output.text, reader, runtime.config.execution === "native-sandbox"),
3669
+ usage: output.usage
3670
+ };
2941
3671
  }
2942
3672
 
2943
3673
  // apps/server/ai/workflows/file-summary.ts
2944
3674
  async function summarizeFile(job, reader, key, signal, runtime) {
2945
3675
  const path = job.snapshot.path;
2946
- const body = await reader.whole(
2947
- path,
2948
- Math.min(
2949
- runtime.config.maxReadBytes,
2950
- Math.max(1024, job.profile.contextWindow - job.profile.maxOutputTokens - 4e3)
2951
- )
2952
- );
3676
+ const body = await reader.whole(path);
2953
3677
  const context = {
2954
3678
  systemPrompt: workflowPrompt(runtime, SummaryOutputSchema),
2955
3679
  messages: [
@@ -2964,13 +3688,6 @@ async function summarizeFile(job, reader, key, signal, runtime) {
2964
3688
  }
2965
3689
  ]
2966
3690
  };
2967
- if (Buffer.byteLength(JSON.stringify(context)) + job.profile.maxOutputTokens > job.profile.contextWindow) {
2968
- throw new HubError(
2969
- 413,
2970
- "AI_CONTEXT_OR_ROUND_LIMIT",
2971
- "File exceeds the analysis context budget."
2972
- );
2973
- }
2974
3691
  const { message } = await modelCall(
2975
3692
  job.profile,
2976
3693
  key,
@@ -2988,61 +3705,81 @@ async function summarizeFile(job, reader, key, signal, runtime) {
2988
3705
  }
2989
3706
 
2990
3707
  // apps/server/ai/workflows/task-bootstrap.ts
2991
- import { z as z10 } from "zod";
2992
- var TaskOutputSchema = z10.object({
2993
- tasks: z10.array(
3708
+ import { z as z12 } from "zod";
3709
+ var LegacyTaskOutputSchema = z12.object({
3710
+ tasks: z12.array(
3711
+ TaskFieldsSchema.extend({
3712
+ taskId: z12.uuid().nullable().optional(),
3713
+ sources: z12.array(z12.string())
3714
+ })
3715
+ ),
3716
+ uncertainties: z12.array(z12.string()).optional()
3717
+ }).strict();
3718
+ var LegacyPatchOutputSchema = z12.object({
3719
+ upsert: z12.array(
3720
+ TaskFieldsSchema.extend({ taskId: z12.uuid().nullable(), sources: z12.array(z12.string()) })
3721
+ ),
3722
+ archive: z12.array(z12.uuid()),
3723
+ uncertainties: z12.array(z12.string()).optional()
3724
+ }).strict();
3725
+ var TaskOutputSchema = z12.object({
3726
+ upsert: z12.array(
2994
3727
  TaskFieldsSchema.extend({
2995
- taskId: z10.uuid().nullable().optional(),
2996
- sources: z10.array(FilePathSchema).min(1).max(50).refine((v) => new Set(v).size === v.length)
3728
+ taskId: z12.uuid().nullable(),
3729
+ sources: z12.array(z12.string()),
3730
+ needsConfirmation: z12.boolean()
2997
3731
  })
2998
- ).max(100),
2999
- uncertainties: z10.array(z10.string().max(2e3)).max(50)
3732
+ ),
3733
+ archive: z12.array(z12.uuid())
3000
3734
  }).strict();
3001
- function validateTasks(text, reader, context) {
3735
+ function validateTasks(text, reader, context, patch = true, native = false) {
3002
3736
  let output;
3003
3737
  try {
3004
- output = TaskOutputSchema.parse(
3005
- JSON.parse(text.trim().replace(/^```(?:json)?\s*\n([\s\S]*)\n```$/, "$1"))
3006
- );
3738
+ const json = JSON.parse(text.trim().replace(/^```(?:json)?\s*\n([\s\S]*)\n```$/, "$1"));
3739
+ if (patch) {
3740
+ const parsed = native ? TaskOutputSchema.parse(json) : LegacyPatchOutputSchema.parse(json);
3741
+ output = {
3742
+ tasks: parsed.upsert,
3743
+ archive: parsed.archive,
3744
+ uncertainties: native ? void 0 : parsed.uncertainties
3745
+ };
3746
+ } else output = LegacyTaskOutputSchema.parse(json);
3007
3747
  } catch {
3008
- throw new HubError(422, "AI_RESULT_INVALID", "Analysis did not produce a valid task list.");
3748
+ throw new HubError(422, "AI_RESULT_INVALID", "Analysis did not produce valid task changes.");
3009
3749
  }
3010
- if (!reader.readFiles.size)
3011
- throw new HubError(
3012
- 422,
3013
- "AI_TASK_SOURCE_INVALID",
3014
- "Read workspace material before deciding whether it contains tasks."
3015
- );
3016
- const keys = output.tasks.map((task) => JSON.stringify([task.title, [...task.sources].sort()]));
3017
- if (new Set(keys).size !== keys.length)
3018
- throw new HubError(422, "AI_TASK_DUPLICATE", "Task list contains identical entries.");
3019
3750
  const previous = new Map(context?.tasks.map((task) => [task.taskId, task]));
3020
3751
  const used = /* @__PURE__ */ new Set();
3021
- for (const task of output.tasks) {
3022
- if (!task.taskId) continue;
3023
- const old = previous.get(task.taskId);
3024
- if (!old || used.has(task.taskId) || !task.sources.some((path) => {
3025
- const current = reader.snapshot.files.find((file) => file.path === path);
3026
- return current && old.sources.some((source) => source.path === path || source.sha256 === current.sha256);
3027
- }))
3752
+ for (const id of [
3753
+ ...output.tasks.flatMap((task) => task.taskId ? [task.taskId] : []),
3754
+ ...output.archive ?? []
3755
+ ]) {
3756
+ if (!previous.has(id) || used.has(id))
3028
3757
  throw new HubError(
3029
3758
  422,
3030
3759
  "AI_TASK_ID_INVALID",
3031
- "Existing task identity must be unique and supported by this member\u2019s prior sources."
3760
+ "Existing task identity must be unique and belong to this member."
3032
3761
  );
3033
- used.add(task.taskId);
3762
+ used.add(id);
3034
3763
  }
3035
3764
  return TaskAnalysisResultSchema.parse({
3036
3765
  kind: "task-bootstrap",
3766
+ ...patch ? { updateMode: "patch", archive: output.archive } : {},
3037
3767
  tasks: output.tasks.map((task) => ({
3038
3768
  ...task,
3039
- sources: task.sources.map((path) => reader.source(path))
3769
+ sources: [...new Set(task.sources)].flatMap((path) => {
3770
+ const relative = native ? memberSourcePath(path, reader.snapshot.owner) : path;
3771
+ const source = reader.source(relative ?? path);
3772
+ return source ? [source] : [];
3773
+ })
3040
3774
  })),
3041
- uncertainties: output.uncertainties,
3042
- coverage: coverage(reader)
3775
+ ...!native ? { uncertainties: output.uncertainties ?? [] } : {},
3776
+ coverage: coverage(reader, native)
3043
3777
  });
3044
3778
  }
3045
3779
  async function bootstrapTasks(job, reader, key, signal, progress, runtime) {
3780
+ const patch = runtime.config.version >= 7;
3781
+ const native = runtime.config.execution === "native-sandbox";
3782
+ const explorer = patch && !native ? new TaskExplorer(reader, job.taskContext, signal) : void 0;
3046
3783
  const output = await runAgent(
3047
3784
  job.profile,
3048
3785
  reader,
@@ -3050,32 +3787,34 @@ async function bootstrapTasks(job, reader, key, signal, progress, runtime) {
3050
3787
  signal,
3051
3788
  progress,
3052
3789
  runtime,
3053
- workflowPrompt(runtime, TaskOutputSchema),
3054
- {
3790
+ workflowPrompt(
3791
+ runtime,
3792
+ native ? TaskOutputSchema : patch ? LegacyPatchOutputSchema : LegacyTaskOutputSchema
3793
+ ),
3794
+ native ? await taskInput(reader, job.taskContext, signal) : explorer?.overview() ?? {
3055
3795
  owner: job.snapshot.owner,
3056
3796
  revision: job.snapshot.revision,
3057
- inventory: reader.list("", 100),
3058
- previous: job.taskContext,
3059
- projectReferences: job.snapshot.project ? reader.projectReader().list("", 100) : null
3060
- }
3797
+ inventory: reader.list(),
3798
+ previous: job.taskContext ? {
3799
+ previousRevision: job.taskContext.previousRevision,
3800
+ changes: job.taskContext.changes,
3801
+ tasks: job.taskContext.tasks.filter((task) => task.active !== false)
3802
+ } : null,
3803
+ projectReferences: job.snapshot.project ? reader.projectReader().list() : null
3804
+ },
3805
+ explorer,
3806
+ job.taskContext
3061
3807
  );
3062
- return { result: validateTasks(output.text, reader, job.taskContext), usage: output.usage };
3808
+ return {
3809
+ result: validateTasks(output.text, reader, job.taskContext, patch, native),
3810
+ usage: output.usage
3811
+ };
3063
3812
  }
3064
3813
 
3065
3814
  // apps/server/ai/bootstrap.ts
3066
- function requestTaskBootstrap(hub, session, owner, operationId) {
3067
- requireAnalysisAccess(hub, session, owner);
3068
- if (!new FileRepository(hub).manifest(session, owner, void 0, "", 1).files.length && !hub.db.prepare("SELECT 1 FROM task_board_members WHERE workspace_id=? AND owner=?").get(session.workspaceId, owner))
3069
- return { jobId: null, empty: true };
3070
- const { job } = new AiJobs(hub).create(session, {
3071
- operationId,
3072
- workflow: "task-bootstrap",
3073
- question: "",
3074
- selection: { owner, kind: "directory", path: "" }
3075
- });
3076
- return { jobId: job.id, empty: false };
3077
- }
3078
3815
  function scheduleTaskBootstraps(hub, at = Date.now()) {
3816
+ const jobs = new AiJobs(hub);
3817
+ jobs.cancelStaleTaskJobs();
3079
3818
  const candidates = hub.db.prepare(
3080
3819
  `SELECT m.workspace_id,m.member_id,MAX(r.created_at,COALESCE(pr.created_at,r.created_at)) created_at
3081
3820
  FROM members m JOIN projects p ON p.id=m.workspace_id JOIN users u ON u.id=m.member_id
@@ -3091,7 +3830,7 @@ function scheduleTaskBootstraps(hub, at = Date.now()) {
3091
3830
  ).all();
3092
3831
  if (!candidates.length) return 0;
3093
3832
  const runtime = loadWorkflow("task-bootstrap");
3094
- const defaults = new AiDefaults(hub), jobs = new AiJobs(hub);
3833
+ const defaults = new AiDefaults(hub);
3095
3834
  let queued = 0;
3096
3835
  for (const row of candidates) {
3097
3836
  if (at - Date.parse(String(row.created_at)) < runtime.config.settleSeconds * 1e3) continue;
@@ -3162,9 +3901,10 @@ async function runAiCycle(hub, stop) {
3162
3901
  job,
3163
3902
  {
3164
3903
  kind: "task-bootstrap",
3904
+ ...runtime.config.version >= 7 ? { updateMode: "patch", archive: [] } : {},
3165
3905
  tasks: [],
3166
- uncertainties: [],
3167
- coverage: { totalFiles: 0, readFiles: [], partial: false, limitations: [] }
3906
+ ...runtime.config.execution !== "native-sandbox" ? { uncertainties: [] } : {},
3907
+ coverage: { totalFiles: 0, readFiles: [], limitations: [] }
3168
3908
  },
3169
3909
  null,
3170
3910
  { inputTokens: 0, outputTokens: 0 }
@@ -3181,10 +3921,7 @@ async function runAiCycle(hub, stop) {
3181
3921
  hub,
3182
3922
  { workspaceId: job.workspace, memberId: job.requester },
3183
3923
  job.snapshot,
3184
- Math.min(
3185
- runtime.config.maxReadBytes,
3186
- Math.max(2048, job.profile.contextWindow - job.profile.maxOutputTokens - 4e3)
3187
- )
3924
+ runtime.config.maxReadBytes
3188
3925
  );
3189
3926
  const key = config.secret(profile.id);
3190
3927
  const output = job.request.workflow === "file-summary" ? await summarizeFile(job, reader, key, signal, runtime) : await (job.request.workflow === "task-bootstrap" ? bootstrapTasks : analyzeDirectory)(
@@ -3243,11 +3980,11 @@ async function runAiWorker(directory, once = false) {
3243
3980
  }
3244
3981
 
3245
3982
  // apps/server/main.ts
3246
- import { fileURLToPath as fileURLToPath2 } from "node:url";
3983
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
3247
3984
  import { parseArgs } from "node:util";
3248
3985
 
3249
3986
  // apps/server/ai/job-routes.ts
3250
- import { z as z11 } from "zod";
3987
+ import { z as z13 } from "zod";
3251
3988
 
3252
3989
  // apps/server/web-auth.ts
3253
3990
  function cookieToken(request) {
@@ -3293,41 +4030,29 @@ function authorizeRead(store, request) {
3293
4030
  // apps/server/ai/job-routes.ts
3294
4031
  function registerAiJobRoutes(server, hub) {
3295
4032
  const jobs = new AiJobs(hub);
3296
- const id = (params) => z11.object({ id: z11.uuid() }).parse(params).id;
4033
+ const id = (params) => z13.object({ id: z13.uuid() }).parse(params).id;
3297
4034
  server.get("/ai/jobs", async (req) => {
3298
- const session = authorizeWeb(hub, req), query = z11.object({
3299
- after: z11.string().regex(/^\d{0,15}$/).default("")
4035
+ const session = authorizeWeb(hub, req), query = z13.object({
4036
+ after: z13.string().regex(/^\d{0,15}$/).default("")
3300
4037
  }).parse(req.query);
3301
4038
  return jobs.list(session.workspaceId, query.after);
3302
4039
  });
3303
- server.post("/ai/jobs", async (req) => {
3304
- webMutation(req);
3305
- return jobs.create(authorizeWeb(hub, req), AiJobCreateSchema.parse(req.body));
3306
- });
3307
4040
  server.get(
3308
4041
  "/ai/jobs/:id",
3309
4042
  async (req) => jobs.get(authorizeWeb(hub, req).workspaceId, id(req.params))
3310
4043
  );
3311
4044
  server.get("/ai/jobs/:id/events", async (req) => {
3312
- const query = z11.object({ after: z11.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).default(0) }).parse(req.query);
4045
+ const query = z13.object({ after: z13.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).default(0) }).parse(req.query);
3313
4046
  return jobs.events(authorizeWeb(hub, req).workspaceId, id(req.params), query.after);
3314
4047
  });
3315
4048
  server.post("/ai/jobs/:id/cancel", async (req) => {
3316
4049
  webMutation(req);
3317
4050
  return jobs.cancel(authorizeWeb(hub, req), id(req.params));
3318
4051
  });
3319
- server.post("/ai/jobs/:id/retry", async (req) => {
3320
- webMutation(req);
3321
- return jobs.retry(
3322
- authorizeWeb(hub, req),
3323
- id(req.params),
3324
- z11.object({ operationId: z11.uuid() }).strict().parse(req.body).operationId
3325
- );
3326
- });
3327
4052
  server.get("/ai/jobs/:id/source", async (req) => {
3328
4053
  const session = authorizeWeb(hub, req), jobId = id(req.params);
3329
4054
  jobs.get(session.workspaceId, jobId);
3330
- const query = z11.object({ path: z11.string().max(1024), startLine: z11.coerce.number().int().min(1).default(1) }).parse(req.query);
4055
+ const query = z13.object({ path: z13.string().max(1024), startLine: z13.coerce.number().int().min(1).default(1) }).parse(req.query);
3331
4056
  const row = hub.db.prepare("SELECT snapshot_json FROM ai_jobs WHERE id=? AND workspace_id=?").get(jobId, session.workspaceId);
3332
4057
  if (!row) throw new HubError(404, "AI_JOB_NOT_FOUND", "Task not found.");
3333
4058
  return new SnapshotReader(hub, session, JSON.parse(String(row.snapshot_json))).read(
@@ -3339,16 +4064,11 @@ function registerAiJobRoutes(server, hub) {
3339
4064
  }
3340
4065
 
3341
4066
  // apps/server/ai/task-routes.ts
3342
- import { z as z12 } from "zod";
4067
+ import { z as z14 } from "zod";
3343
4068
  function registerTaskRoutes(server, hub) {
3344
4069
  const store = new TaskStore(hub);
3345
- const id = (params) => z12.object({ id: z12.uuid() }).parse(params).id;
4070
+ const id = (params) => z14.object({ id: z14.uuid() }).parse(params).id;
3346
4071
  server.get("/tasks", async (req) => store.board(authorizeWeb(hub, req)));
3347
- server.post("/tasks/bootstrap", async (req) => {
3348
- webMutation(req);
3349
- const session = authorizeWeb(hub, req), input = TaskBootstrapRequestSchema.parse(req.body);
3350
- return requestTaskBootstrap(hub, session, input.owner, input.operationId);
3351
- });
3352
4072
  server.patch("/tasks/:id/correction", async (req) => {
3353
4073
  webMutation(req);
3354
4074
  return store.correct(
@@ -3417,7 +4137,7 @@ function registerAiRoutes(server, store) {
3417
4137
  profile,
3418
4138
  config.secret(profile.id),
3419
4139
  mode,
3420
- AbortSignal.any([controller.signal, AbortSignal.timeout(3e4)])
4140
+ AbortSignal.any([controller.signal, AbortSignal.timeout(3e5)])
3421
4141
  );
3422
4142
  } catch (e) {
3423
4143
  if (e instanceof HubError) throw e;
@@ -3434,11 +4154,11 @@ function registerAiRoutes(server, store) {
3434
4154
  }
3435
4155
 
3436
4156
  // apps/server/static.ts
3437
- import { join as join6 } from "node:path";
4157
+ import { join as join8 } from "node:path";
3438
4158
  import fastifyStatic from "@fastify/static";
3439
4159
  function registerWeb(server, root) {
3440
4160
  server.register(fastifyStatic, {
3441
- root: join6(root, "assets"),
4161
+ root: join8(root, "assets"),
3442
4162
  prefix: "/assets/",
3443
4163
  immutable: true,
3444
4164
  maxAge: "1y",
@@ -3476,20 +4196,20 @@ import { randomUUID as randomUUID4 } from "node:crypto";
3476
4196
  import Fastify from "fastify";
3477
4197
 
3478
4198
  // packages/protocol/index.ts
3479
- import { z as z13 } from "zod";
4199
+ import { z as z15 } from "zod";
3480
4200
  var PROTOCOL_VERSION = 1;
3481
- var HealthSchema = z13.object({ status: z13.literal("ok") });
3482
- var ServerInfoSchema = z13.object({
3483
- name: z13.literal("teammy"),
3484
- version: z13.string().min(1),
3485
- protocolVersion: z13.literal(PROTOCOL_VERSION),
3486
- capabilities: z13.array(z13.string())
4201
+ var HealthSchema = z15.object({ status: z15.literal("ok") });
4202
+ var ServerInfoSchema = z15.object({
4203
+ name: z15.literal("teammy"),
4204
+ version: z15.string().min(1),
4205
+ protocolVersion: z15.literal(PROTOCOL_VERSION),
4206
+ capabilities: z15.array(z15.string())
3487
4207
  });
3488
- var ErrorResponseSchema = z13.object({
3489
- error: z13.object({
3490
- code: z13.string(),
3491
- message: z13.string(),
3492
- requestId: z13.string()
4208
+ var ErrorResponseSchema = z15.object({
4209
+ error: z15.object({
4210
+ code: z15.string(),
4211
+ message: z15.string(),
4212
+ requestId: z15.string()
3493
4213
  })
3494
4214
  });
3495
4215
 
@@ -3497,43 +4217,43 @@ var ErrorResponseSchema = z13.object({
3497
4217
  import { ZodError } from "zod";
3498
4218
 
3499
4219
  // packages/protocol/devices.ts
3500
- import { z as z14 } from "zod";
3501
- var CredentialSchema = z14.string().regex(/^tmy_[a-f0-9]{64}$/);
3502
- var EnrollmentSchema = z14.object({
3503
- operationId: z14.uuid(),
4220
+ import { z as z16 } from "zod";
4221
+ var CredentialSchema = z16.string().regex(/^tmy_[a-f0-9]{64}$/);
4222
+ var EnrollmentSchema = z16.object({
4223
+ operationId: z16.uuid(),
3504
4224
  projectId: NameSchema,
3505
4225
  memberId: NameSchema,
3506
4226
  deviceId: DeviceIdSchema,
3507
4227
  credential: CredentialSchema
3508
4228
  }).strict();
3509
- var EffectivePolicySchema = z14.object({
3510
- checkIntervalSeconds: z14.number().min(1).max(86400),
3511
- fileSettleSeconds: z14.number().min(0).max(86400)
4229
+ var EffectivePolicySchema = z16.object({
4230
+ checkIntervalSeconds: z16.number().min(1).max(86400),
4231
+ fileSettleSeconds: z16.number().min(0).max(86400)
3512
4232
  }).strict();
3513
- var SyncSummarySchema = z14.object({
3514
- checkedAt: z14.string(),
3515
- published: z14.boolean(),
3516
- appliedFiles: z14.number().int().nonnegative(),
3517
- pendingFiles: z14.number().int().nonnegative(),
3518
- sessionSources: z14.number().int().nonnegative(),
3519
- errors: z14.array(z14.string().max(1e3)).max(20)
4233
+ var SyncSummarySchema = z16.object({
4234
+ checkedAt: z16.string(),
4235
+ published: z16.boolean(),
4236
+ appliedFiles: z16.number().int().nonnegative(),
4237
+ pendingFiles: z16.number().int().nonnegative(),
4238
+ sessionSources: z16.number().int().nonnegative(),
4239
+ errors: z16.array(z16.string().max(1e3)).max(20)
3520
4240
  }).strict();
3521
- var DeviceStatusSchema = z14.object({
3522
- clientVersion: z14.string().max(64),
4241
+ var DeviceStatusSchema = z16.object({
4242
+ clientVersion: z16.string().max(64),
3523
4243
  receivedRevision: RevisionSchema.nullable(),
3524
4244
  appliedRevision: RevisionSchema.nullable(),
3525
4245
  effectivePolicy: EffectivePolicySchema,
3526
- overrides: z14.array(z14.enum(["checkIntervalSeconds", "fileSettleSeconds"])).max(2),
3527
- configError: z14.string().max(2e3).nullable(),
3528
- reportError: z14.string().max(2e3).nullable(),
4246
+ overrides: z16.array(z16.enum(["checkIntervalSeconds", "fileSettleSeconds"])).max(2),
4247
+ configError: z16.string().max(2e3).nullable(),
4248
+ reportError: z16.string().max(2e3).nullable(),
3529
4249
  lastSync: SyncSummarySchema.nullable()
3530
4250
  }).strict();
3531
- var WebDevicesSchema = z14.object({
3532
- devices: z14.array(
3533
- z14.object({
4251
+ var WebDevicesSchema = z16.object({
4252
+ devices: z16.array(
4253
+ z16.object({
3534
4254
  memberId: NameSchema,
3535
- bound: z14.boolean(),
3536
- lastSeenAt: z14.string().nullable(),
4255
+ bound: z16.boolean(),
4256
+ lastSeenAt: z16.string().nullable(),
3537
4257
  status: DeviceStatusSchema.nullable()
3538
4258
  })
3539
4259
  )
@@ -3571,7 +4291,7 @@ function registerControlRoutes(server, store) {
3571
4291
  }
3572
4292
 
3573
4293
  // apps/server/project-routes.ts
3574
- import { z as z15 } from "zod";
4294
+ import { z as z17 } from "zod";
3575
4295
  function registerProjectRoutes(server, store) {
3576
4296
  const project = new ProjectRepository(store), site = new SiteRepository(store);
3577
4297
  server.get("/api/v1/projects", async (request) => ({
@@ -3618,7 +4338,7 @@ function registerProjectScopeRoutes(server, store) {
3618
4338
  });
3619
4339
  server.patch("/web/members/:id", async (request) => {
3620
4340
  webMutation(request);
3621
- const { id } = z15.object({ id: NameSchema }).parse(request.params);
4341
+ const { id } = z17.object({ id: NameSchema }).parse(request.params);
3622
4342
  return project.updateMember(
3623
4343
  authorizeWeb(store, request),
3624
4344
  id,
@@ -3627,13 +4347,13 @@ function registerProjectScopeRoutes(server, store) {
3627
4347
  });
3628
4348
  server.post("/web/members/:id/device-reset", async (request) => {
3629
4349
  webMutation(request);
3630
- const { id } = z15.object({ id: NameSchema }).parse(request.params);
4350
+ const { id } = z17.object({ id: NameSchema }).parse(request.params);
3631
4351
  return project.resetDevice(authorizeWeb(store, request), id);
3632
4352
  });
3633
4353
  }
3634
4354
 
3635
4355
  // apps/server/site-routes.ts
3636
- import { z as z16 } from "zod";
4356
+ import { z as z18 } from "zod";
3637
4357
 
3638
4358
  // apps/server/devices.ts
3639
4359
  var DeviceRepository = class {
@@ -3709,7 +4429,7 @@ function registerSiteRoutes(server, store) {
3709
4429
  "/api/v1/web/session",
3710
4430
  { onRequest: async (request) => webMutation(request) },
3711
4431
  async (request, reply) => {
3712
- const { id } = z16.object({ id: NameSchema }).strict().parse(request.body);
4432
+ const { id } = z18.object({ id: NameSchema }).strict().parse(request.body);
3713
4433
  const { token, session } = site.login(id);
3714
4434
  const secure = request.protocol === "https" || request.headers.origin?.startsWith("https://");
3715
4435
  reply.header(
@@ -3723,7 +4443,7 @@ function registerSiteRoutes(server, store) {
3723
4443
  server.patch("/api/v1/web/session", async (request) => {
3724
4444
  webMutation(request);
3725
4445
  const identity = site.identity(cookieToken(request));
3726
- const { displayName } = z16.object({ displayName: z16.string().trim().max(120) }).strict().parse(request.body);
4446
+ const { displayName } = z18.object({ displayName: z18.string().trim().max(120) }).strict().parse(request.body);
3727
4447
  return site.renameSelf(identity, displayName);
3728
4448
  });
3729
4449
  server.delete("/api/v1/web/session", async (request, reply) => {
@@ -3764,71 +4484,71 @@ function registerSiteRoutes(server, store) {
3764
4484
  }
3765
4485
 
3766
4486
  // apps/server/session-routes.ts
3767
- import { z as z18 } from "zod";
4487
+ import { z as z20 } from "zod";
3768
4488
 
3769
4489
  // packages/protocol/sessions.ts
3770
- import { z as z17 } from "zod";
3771
- var AgentSchema = z17.enum(["claude-code", "codex", "cursor", "pi", "dsh"]);
3772
- var SessionSnapshotSchema = z17.object({
3773
- formatVersion: z17.literal(1),
3774
- adapterVersion: z17.literal(1),
4490
+ import { z as z19 } from "zod";
4491
+ var AgentSchema = z19.enum(["claude-code", "codex", "cursor", "pi", "dsh"]);
4492
+ var SessionSnapshotSchema = z19.object({
4493
+ formatVersion: z19.literal(1),
4494
+ adapterVersion: z19.literal(1),
3775
4495
  agent: AgentSchema,
3776
- nativeId: z17.string().min(1).max(512),
3777
- cwd: z17.string().min(1).max(4096),
3778
- messages: z17.array(
3779
- z17.object({
3780
- id: z17.string().min(1).max(1024),
3781
- role: z17.enum(["user", "assistant"]),
3782
- text: z17.string().min(1).max(2 * 1024 * 1024),
3783
- source: z17.string().min(1).max(1024)
4496
+ nativeId: z19.string().min(1).max(512),
4497
+ cwd: z19.string().min(1).max(4096),
4498
+ messages: z19.array(
4499
+ z19.object({
4500
+ id: z19.string().min(1).max(1024),
4501
+ role: z19.enum(["user", "assistant"]),
4502
+ text: z19.string().min(1).max(2 * 1024 * 1024),
4503
+ source: z19.string().min(1).max(1024)
3784
4504
  }).strict()
3785
4505
  ).max(5e4)
3786
4506
  }).strict().refine(
3787
4507
  (s) => new Set(s.messages.map((m) => m.id)).size === s.messages.length,
3788
4508
  "Duplicate message IDs."
3789
4509
  );
3790
- var SessionCommitSchema = z17.object({
3791
- operationId: z17.uuid(),
4510
+ var SessionCommitSchema = z19.object({
4511
+ operationId: z19.uuid(),
3792
4512
  sessionKey: HashSchema,
3793
4513
  parentRevision: RevisionSchema,
3794
4514
  blob: BlobRefSchema
3795
4515
  }).strict();
3796
- var SessionInfoSchema = z17.object({
4516
+ var SessionInfoSchema = z19.object({
3797
4517
  owner: NameSchema,
3798
4518
  sessionKey: HashSchema,
3799
4519
  agent: AgentSchema,
3800
- nativeId: z17.string(),
3801
- cwd: z17.string(),
3802
- title: z17.string(),
4520
+ nativeId: z19.string(),
4521
+ cwd: z19.string(),
4522
+ title: z19.string(),
3803
4523
  revision: RevisionSchema,
3804
- updatedAt: z17.string(),
3805
- messageCount: z17.number().int().nonnegative(),
4524
+ updatedAt: z19.string(),
4525
+ messageCount: z19.number().int().nonnegative(),
3806
4526
  blob: BlobRefSchema
3807
4527
  });
3808
- var SessionListSchema = z17.object({
3809
- sessions: z17.array(SessionInfoSchema),
4528
+ var SessionListSchema = z19.object({
4529
+ sessions: z19.array(SessionInfoSchema),
3810
4530
  nextCursor: RevisionSchema.nullable()
3811
4531
  });
3812
- var SessionStatsSchema = z17.object({
3813
- totalSessions: z17.number().int().nonnegative(),
3814
- totalMessages: z17.number().int().nonnegative(),
3815
- memberCount: z17.number().int().nonnegative(),
3816
- updatedAt: z17.string().nullable()
4532
+ var SessionStatsSchema = z19.object({
4533
+ totalSessions: z19.number().int().nonnegative(),
4534
+ totalMessages: z19.number().int().nonnegative(),
4535
+ memberCount: z19.number().int().nonnegative(),
4536
+ updatedAt: z19.string().nullable()
3817
4537
  });
3818
- var SessionScopeSchema = z17.object({
4538
+ var SessionScopeSchema = z19.object({
3819
4539
  owner: NameSchema.optional(),
3820
4540
  sessionKey: HashSchema.optional()
3821
4541
  }).strict().refine((v) => !v.sessionKey || !!v.owner, "A session scope requires an owner.");
3822
- var SessionSearchSchema = z17.object({
3823
- matches: z17.array(
3824
- z17.object({
4542
+ var SessionSearchSchema = z19.object({
4543
+ matches: z19.array(
4544
+ z19.object({
3825
4545
  owner: NameSchema,
3826
4546
  sessionKey: HashSchema,
3827
4547
  revision: RevisionSchema,
3828
- messageId: z17.string(),
3829
- role: z17.enum(["user", "assistant"]),
3830
- snippet: z17.string(),
3831
- title: z17.string()
4548
+ messageId: z19.string(),
4549
+ role: z19.enum(["user", "assistant"]),
4550
+ snippet: z19.string(),
4551
+ title: z19.string()
3832
4552
  })
3833
4553
  ),
3834
4554
  nextCursor: RevisionSchema.nullable()
@@ -3989,31 +4709,46 @@ var SessionRepository = class {
3989
4709
  ).get(identity.workspaceId)
3990
4710
  );
3991
4711
  }
3992
- list(identity, after, limit, owner) {
3993
- const rows = this.hub.db.prepare(
3994
- `SELECT s.id, r.info_json FROM sessions s JOIN session_revisions r USING(workspace_id, owner, session_key, revision)
3995
- WHERE s.workspace_id=? AND s.id>? ${owner ? "AND s.owner=?" : ""} ORDER BY s.id LIMIT ?`
3996
- ).all(identity.workspaceId, after, ...owner ? [owner] : [], limit + 1);
4712
+ list(identity, after, limit, owner, order = "created") {
4713
+ const byTitle = order === "title";
4714
+ let rows = this.hub.db.prepare(
4715
+ `SELECT s.id, s.title, s.native_id, r.info_json FROM sessions s JOIN session_revisions r USING(workspace_id, owner, session_key, revision)
4716
+ WHERE s.workspace_id=? ${byTitle ? "" : "AND s.id>?"} ${owner ? "AND s.owner=?" : ""} ${byTitle ? "" : "ORDER BY s.id LIMIT ?"}`
4717
+ ).all(
4718
+ identity.workspaceId,
4719
+ ...byTitle ? [] : [after],
4720
+ ...owner ? [owner] : [],
4721
+ ...byTitle ? [] : [limit + 1]
4722
+ );
4723
+ if (byTitle) {
4724
+ rows.sort(
4725
+ (a, b) => compareNames(String(a.title || a.native_id), String(b.title || b.native_id)) || Number(a.id) - Number(b.id)
4726
+ );
4727
+ const offset = after ? rows.findIndex((row) => row.id === after) + 1 : 0;
4728
+ if (after && !offset)
4729
+ throw new HubError(400, "INVALID_CURSOR", "Session cursor is not in this scope.");
4730
+ rows = rows.slice(offset, offset + limit + 1);
4731
+ }
3997
4732
  return {
3998
4733
  sessions: rows.slice(0, limit).map((row) => SessionInfoSchema.parse(JSON.parse(String(row.info_json)))),
3999
4734
  nextCursor: rows.length > limit ? Number(rows[limit - 1].id) : null
4000
4735
  };
4001
4736
  }
4002
- search(identity, query, after, limit, scope = {}) {
4003
- SessionScopeSchema.parse(scope);
4737
+ search(identity, query, after, limit, scope2 = {}) {
4738
+ SessionScopeSchema.parse(scope2);
4004
4739
  const lower = query.toLowerCase();
4005
4740
  const indexed = [...lower].length >= 3;
4006
4741
  const rows = this.hub.db.prepare(
4007
4742
  `SELECT m.*,s.title,s.native_id FROM session_messages m JOIN sessions s USING(workspace_id,owner,session_key)
4008
4743
  WHERE m.workspace_id=? AND m.id>? AND instr(m.search_text, ?)>0
4009
- ${scope.owner ? "AND m.owner=?" : ""} ${scope.sessionKey ? "AND m.session_key=?" : ""}
4744
+ ${scope2.owner ? "AND m.owner=?" : ""} ${scope2.sessionKey ? "AND m.session_key=?" : ""}
4010
4745
  ${indexed ? "AND m.id IN (SELECT rowid FROM session_fts WHERE session_fts MATCH ?)" : ""} ORDER BY m.id LIMIT ?`
4011
4746
  ).all(
4012
4747
  identity.workspaceId,
4013
4748
  after,
4014
4749
  lower,
4015
- ...scope.owner ? [scope.owner] : [],
4016
- ...scope.sessionKey ? [scope.sessionKey] : [],
4750
+ ...scope2.owner ? [scope2.owner] : [],
4751
+ ...scope2.sessionKey ? [scope2.sessionKey] : [],
4017
4752
  ...indexed ? [`"${lower.replaceAll('"', '""')}"`] : [],
4018
4753
  limit + 1
4019
4754
  );
@@ -4045,9 +4780,9 @@ function registerSessionRoutes(server, store) {
4045
4780
  authorize(request);
4046
4781
  }
4047
4782
  };
4048
- const pagination = z18.object({
4049
- after: z18.coerce.number().pipe(RevisionSchema).default(0),
4050
- limit: z18.coerce.number().int().min(1).max(100).default(50)
4783
+ const pagination = z20.object({
4784
+ after: z20.coerce.number().pipe(RevisionSchema).default(0),
4785
+ limit: z20.coerce.number().int().min(1).max(100).default(50)
4051
4786
  });
4052
4787
  server.post(
4053
4788
  "/sessions/commits",
@@ -4059,15 +4794,18 @@ function registerSessionRoutes(server, store) {
4059
4794
  )
4060
4795
  );
4061
4796
  server.get("/sessions", async (request) => {
4062
- const q = pagination.extend({ owner: NameSchema.optional() }).parse(request.query);
4063
- return sessions.list(authorizeRead(store, request), q.after, q.limit, q.owner);
4797
+ const q = pagination.extend({
4798
+ owner: NameSchema.optional(),
4799
+ order: z20.enum(["created", "title"]).default("created")
4800
+ }).parse(request.query);
4801
+ return sessions.list(authorizeRead(store, request), q.after, q.limit, q.owner, q.order);
4064
4802
  });
4065
4803
  server.get("/sessions/stats", async (request) => sessions.stats(authorizeRead(store, request)));
4066
4804
  server.get("/sessions/search", async (request) => {
4067
4805
  const q = pagination.extend({
4068
4806
  owner: NameSchema.optional(),
4069
4807
  sessionKey: HashSchema.optional(),
4070
- q: z18.string().min(1).max(500).refine((s) => !s.includes("\0"))
4808
+ q: z20.string().min(1).max(500).refine((s) => !s.includes("\0"))
4071
4809
  }).parse(request.query);
4072
4810
  return sessions.search(
4073
4811
  authorizeRead(store, request),
@@ -4078,14 +4816,14 @@ function registerSessionRoutes(server, store) {
4078
4816
  );
4079
4817
  });
4080
4818
  server.get("/sessions/:owner/:key", async (request) => {
4081
- const p = z18.object({ owner: NameSchema, key: HashSchema }).parse(request.params);
4082
- const q = z18.object({ revision: z18.coerce.number().pipe(RevisionSchema).optional() }).parse(request.query);
4819
+ const p = z20.object({ owner: NameSchema, key: HashSchema }).parse(request.params);
4820
+ const q = z20.object({ revision: z20.coerce.number().pipe(RevisionSchema).optional() }).parse(request.query);
4083
4821
  return sessions.read(authorizeRead(store, request), p.owner, p.key, q.revision);
4084
4822
  });
4085
4823
  }
4086
4824
 
4087
4825
  // apps/server/file-routes.ts
4088
- import { z as z19 } from "zod";
4826
+ import { z as z21 } from "zod";
4089
4827
  function registerFileRoutes(server, store) {
4090
4828
  const files = new FileRepository(store);
4091
4829
  const shared = new SharedFilesRepository(store);
@@ -4116,13 +4854,13 @@ function registerFileRoutes(server, store) {
4116
4854
  missing: files.missing(authorize(request), BlobCheckSchema.parse(request.body).hashes)
4117
4855
  }));
4118
4856
  server.put("/blobs/:hash", { ...guard, bodyLimit: MAX_BLOB_BYTES }, async (request) => {
4119
- const { hash: hash2 } = z19.object({ hash: HashSchema }).parse(request.params);
4857
+ const { hash: hash2 } = z21.object({ hash: HashSchema }).parse(request.params);
4120
4858
  if (!Buffer.isBuffer(request.body))
4121
4859
  throw Object.assign(new Error("Expected binary content"), { statusCode: 400 });
4122
4860
  return files.upload(request.headers.authorization, device(request), hash2, request.body);
4123
4861
  });
4124
4862
  server.get("/blobs/:hash", async (request, reply) => {
4125
- const { hash: hash2 } = z19.object({ hash: HashSchema }).parse(request.params);
4863
+ const { hash: hash2 } = z21.object({ hash: HashSchema }).parse(request.params);
4126
4864
  return reply.type("application/octet-stream").send(Buffer.from(await files.download(authorizeRead(store, request), hash2)));
4127
4865
  });
4128
4866
  server.post(
@@ -4139,11 +4877,11 @@ function registerFileRoutes(server, store) {
4139
4877
  return files.commit(request.headers.authorization, device(request), batch, configRevision);
4140
4878
  });
4141
4879
  server.get("/files/:owner/manifest", async (request) => {
4142
- const { owner } = z19.object({ owner: FileOwnerSchema }).parse(request.params);
4143
- const query = z19.object({
4144
- revision: z19.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional(),
4145
- after: z19.string().max(1024).default(""),
4146
- limit: z19.coerce.number().int().min(1).max(500).default(200)
4880
+ const { owner } = z21.object({ owner: FileOwnerSchema }).parse(request.params);
4881
+ const query = z21.object({
4882
+ revision: z21.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional(),
4883
+ after: z21.string().max(1024).default(""),
4884
+ limit: z21.coerce.number().int().min(1).max(500).default(200)
4147
4885
  }).parse(request.query);
4148
4886
  return files.manifest(
4149
4887
  authorizeRead(store, request),
@@ -4154,28 +4892,28 @@ function registerFileRoutes(server, store) {
4154
4892
  );
4155
4893
  });
4156
4894
  server.get("/files/:owner/entry", async (request) => {
4157
- const { owner } = z19.object({ owner: FileOwnerSchema }).parse(request.params);
4158
- const q = z19.object({
4895
+ const { owner } = z21.object({ owner: FileOwnerSchema }).parse(request.params);
4896
+ const q = z21.object({
4159
4897
  path: FilePathSchema,
4160
- revision: z19.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional()
4898
+ revision: z21.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional()
4161
4899
  }).parse(request.query);
4162
4900
  return files.entry(authorizeRead(store, request), owner, q.path, q.revision);
4163
4901
  });
4164
4902
  server.get("/files/:owner/tree", async (request) => {
4165
- const { owner } = z19.object({ owner: FileOwnerSchema }).parse(request.params);
4166
- const q = z19.object({
4167
- path: z19.union([z19.literal(""), FilePathSchema]).default(""),
4168
- revision: z19.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional(),
4169
- after: z19.string().max(255).default(""),
4170
- limit: z19.coerce.number().int().min(1).max(200).default(100)
4903
+ const { owner } = z21.object({ owner: FileOwnerSchema }).parse(request.params);
4904
+ const q = z21.object({
4905
+ path: z21.union([z21.literal(""), FilePathSchema]).default(""),
4906
+ revision: z21.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional(),
4907
+ after: z21.string().max(255).default(""),
4908
+ limit: z21.coerce.number().int().min(1).max(200).default(100)
4171
4909
  }).parse(request.query);
4172
4910
  return files.tree(authorizeRead(store, request), owner, q.path, q.revision, q.after, q.limit);
4173
4911
  });
4174
4912
  server.get("/files/:owner/changes", async (request) => {
4175
- const { owner } = z19.object({ owner: FileOwnerSchema }).parse(request.params);
4176
- const query = z19.object({
4177
- after: z19.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).default(0),
4178
- limit: z19.coerce.number().int().min(1).max(100).default(100)
4913
+ const { owner } = z21.object({ owner: FileOwnerSchema }).parse(request.params);
4914
+ const query = z21.object({
4915
+ after: z21.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).default(0),
4916
+ limit: z21.coerce.number().int().min(1).max(100).default(100)
4179
4917
  }).parse(request.query);
4180
4918
  return files.changes(authorizeRead(store, request), owner, query.after, query.limit);
4181
4919
  });
@@ -4244,14 +4982,14 @@ function buildServer(options) {
4244
4982
  registerSiteRoutes(server, store);
4245
4983
  registerProjectRoutes(server, store);
4246
4984
  registerAiRoutes(server, store);
4247
- const projectRoutes = async (scope) => {
4248
- registerProjectScopeRoutes(scope, store);
4249
- registerAiJobRoutes(scope, store);
4250
- registerTaskRoutes(scope, store);
4251
- registerControlRoutes(scope, store);
4252
- registerFileRoutes(scope, store);
4253
- registerSessionRoutes(scope, store);
4254
- scope.get("/members", async (request) => ({
4985
+ const projectRoutes = async (scope2) => {
4986
+ registerProjectScopeRoutes(scope2, store);
4987
+ registerAiJobRoutes(scope2, store);
4988
+ registerTaskRoutes(scope2, store);
4989
+ registerControlRoutes(scope2, store);
4990
+ registerFileRoutes(scope2, store);
4991
+ registerSessionRoutes(scope2, store);
4992
+ scope2.get("/members", async (request) => ({
4255
4993
  members: store.members(authorizeDevice(store, request))
4256
4994
  }));
4257
4995
  };
@@ -4268,7 +5006,7 @@ function buildServer(options) {
4268
5006
 
4269
5007
  // apps/server/main.ts
4270
5008
  import { homedir } from "node:os";
4271
- import { join as join7, resolve as resolve3 } from "node:path";
5009
+ import { join as join9, resolve as resolve3 } from "node:path";
4272
5010
  var help = `teammy-server \u2014 the Teammy collaboration hub
4273
5011
 
4274
5012
  Usage: teammy-server [--host HOST] [--port PORT]
@@ -4293,13 +5031,13 @@ Open the server URL to initialize the Web dashboard and preset member IDs.
4293
5031
  File synchronization, session search and manual AI analysis are available.
4294
5032
  AI tasks require a separate ai-worker process using the same data directory.
4295
5033
  `;
4296
- async function runServerCli(args, version) {
5034
+ async function runServerCli(args, version2) {
4297
5035
  try {
4298
5036
  if (args[0] === "ai-worker") {
4299
5037
  const { values: values2 } = parseArgs({
4300
5038
  args: args.slice(1),
4301
5039
  options: {
4302
- "data-dir": { type: "string", default: join7(homedir(), ".teammy", "server") },
5040
+ "data-dir": { type: "string", default: join9(homedir(), ".teammy", "server") },
4303
5041
  once: { type: "boolean" }
4304
5042
  }
4305
5043
  });
@@ -4315,7 +5053,7 @@ async function runServerCli(args, version) {
4315
5053
  options: {
4316
5054
  workspace: { type: "string" },
4317
5055
  member: { type: "string" },
4318
- "data-dir": { type: "string", default: join7(homedir(), ".teammy", "server") }
5056
+ "data-dir": { type: "string", default: join9(homedir(), ".teammy", "server") }
4319
5057
  }
4320
5058
  });
4321
5059
  if (!values2.workspace || !values2.member)
@@ -4340,7 +5078,7 @@ async function runServerCli(args, version) {
4340
5078
  version: { type: "boolean", short: "v" },
4341
5079
  host: { type: "string", default: "127.0.0.1" },
4342
5080
  port: { type: "string", default: "4317" },
4343
- "data-dir": { type: "string", default: join7(homedir(), ".teammy", "server") }
5081
+ "data-dir": { type: "string", default: join9(homedir(), ".teammy", "server") }
4344
5082
  }
4345
5083
  });
4346
5084
  if (values.help) {
@@ -4348,7 +5086,7 @@ async function runServerCli(args, version) {
4348
5086
  return 0;
4349
5087
  }
4350
5088
  if (values.version) {
4351
- process.stdout.write(`${version}
5089
+ process.stdout.write(`${version2}
4352
5090
  `);
4353
5091
  return 0;
4354
5092
  }
@@ -4358,10 +5096,10 @@ async function runServerCli(args, version) {
4358
5096
  }
4359
5097
  if (!values.host?.trim()) throw new Error("Host must not be empty.");
4360
5098
  const server = buildServer({
4361
- version,
5099
+ version: version2,
4362
5100
  logger: true,
4363
5101
  dataDir: resolve3(values["data-dir"]),
4364
- webDir: fileURLToPath2(new URL("./web/", import.meta.url))
5102
+ webDir: fileURLToPath3(new URL("./web/", import.meta.url))
4365
5103
  });
4366
5104
  try {
4367
5105
  await server.listen({ host: values.host, port: Number(portText) });