@teammy/server 0.0.8 → 0.0.9

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