@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/README.md +7 -5
- package/dist/line-diff-worker.js +32 -0
- package/dist/main.js +1206 -465
- package/dist/native-pi-entry.js +247 -0
- package/dist/runtime/README.md +4 -2
- package/dist/runtime/shared/system.md +2 -2
- package/dist/runtime/shared/tools.json +11 -3
- package/dist/runtime/workflows/directory-analysis/config.json +6 -7
- package/dist/runtime/workflows/directory-analysis/prompt.md +4 -2
- package/dist/runtime/workflows/file-summary/config.json +4 -6
- package/dist/runtime/workflows/file-summary/prompt.md +2 -2
- package/dist/runtime/workflows/task-bootstrap/config.json +7 -8
- package/dist/runtime/workflows/task-bootstrap/prompt.md +43 -7
- package/dist/web/assets/Dashboard-CAyrJ3RP.js +1 -0
- package/dist/web/assets/{Files-cG0tkAfP.js → Files-B9FJqcJ4.js} +1 -1
- package/dist/web/assets/{Markdown-Bt-6EDRz.js → Markdown-DPF_hcrA.js} +1 -1
- package/dist/web/assets/{Sessions-B7ABJvRH.js → Sessions-DAIRx5CI.js} +1 -1
- package/dist/web/assets/{index-f4fiI3WA.js → index-CETlKRD0.js} +5 -5
- package/dist/web/index.html +1 -1
- package/package.json +2 -1
- package/dist/web/assets/Dashboard-CsQ-GV-q.js +0 -1
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
|
|
114
|
-
if (
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
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
|
|
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
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
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
|
|
1188
|
+
async whole(path) {
|
|
1208
1189
|
const text = await this.text(path);
|
|
1209
|
-
|
|
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 = "",
|
|
1220
|
-
const
|
|
1221
|
-
|
|
1222
|
-
|
|
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
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
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
|
-
|
|
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
|
|
1480
|
-
if (
|
|
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()
|
|
1576
|
-
goal: z5.string().trim().
|
|
1561
|
+
title: z5.string().trim(),
|
|
1562
|
+
goal: z5.string().trim().nullable(),
|
|
1577
1563
|
status: TaskStatusSchema.nullable(),
|
|
1578
|
-
progress: z5.string().trim()
|
|
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)
|
|
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
|
-
|
|
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().
|
|
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
|
-
|
|
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()
|
|
1690
|
-
findings: z6.array(z6.string()
|
|
1691
|
-
uncertainties: z6.array(z6.string()
|
|
1692
|
-
citations: z6.array(AiCitationSchema)
|
|
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)
|
|
1688
|
+
sources: z6.array(TaskSourceSchema)
|
|
1701
1689
|
})
|
|
1702
|
-
)
|
|
1703
|
-
|
|
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
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
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(
|
|
1841
|
-
const row = this.hub.db.prepare("SELECT * FROM ai_defaults WHERE scope=?").get(
|
|
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(
|
|
1860
|
+
save(scope2, value) {
|
|
1865
1861
|
const input = AiDefaultUpdateSchema.parse(value);
|
|
1866
1862
|
transaction(this.hub.db, () => {
|
|
1867
|
-
if (this.read(
|
|
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(
|
|
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([
|
|
1909
|
-
|
|
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).
|
|
1914
|
-
maxRounds: z7.number().int().min(1).max(
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
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)
|
|
1924
|
-
prompt: z7.string().min(1)
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2010
|
-
|
|
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 =
|
|
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]
|
|
2026
|
-
const newGroup = result.tasks.filter((item) => item.sources[0]
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2133
|
-
|
|
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=?
|
|
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
|
-
...
|
|
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
|
|
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(
|
|
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 ?
|
|
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
|
|
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(
|
|
2504
|
-
|
|
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
|
|
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
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
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:
|
|
2732
|
+
parameters: Type2.Object(
|
|
2538
2733
|
{
|
|
2539
|
-
after:
|
|
2540
|
-
scope:
|
|
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
|
-
|
|
2547
|
-
const p =
|
|
2548
|
-
after:
|
|
2549
|
-
scope:
|
|
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
|
|
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:
|
|
2753
|
+
parameters: Type2.Object(
|
|
2559
2754
|
{
|
|
2560
|
-
path:
|
|
2561
|
-
scope:
|
|
2562
|
-
startLine:
|
|
2563
|
-
limit:
|
|
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
|
-
|
|
2570
|
-
const p =
|
|
2764
|
+
signal.throwIfAborted();
|
|
2765
|
+
const p = z9.object({
|
|
2571
2766
|
path: FilePathSchema,
|
|
2572
|
-
scope:
|
|
2573
|
-
startLine:
|
|
2574
|
-
limit:
|
|
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:
|
|
2784
|
+
parameters: Type2.Object(
|
|
2590
2785
|
{
|
|
2591
|
-
query:
|
|
2592
|
-
after:
|
|
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
|
-
|
|
2599
|
-
const p =
|
|
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
|
|
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:
|
|
2623
|
-
maxTokens:
|
|
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 ??
|
|
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:
|
|
2907
|
+
parameters: Type3.Object({ value: Type3.String() })
|
|
2704
2908
|
}
|
|
2705
2909
|
]
|
|
2706
2910
|
} : {}
|
|
2707
2911
|
};
|
|
2708
2912
|
const first = await modelCall(
|
|
2709
|
-
|
|
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
|
|
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,
|
|
3537
|
+
session.agent.streamFunction = (_model, context2, options) => {
|
|
2820
3538
|
signal.throwIfAborted();
|
|
2821
|
-
if (++requests > runtime.config.maxRounds
|
|
3539
|
+
if (++requests > runtime.config.maxRounds) {
|
|
2822
3540
|
budgetError = new HubError(
|
|
2823
3541
|
413,
|
|
2824
|
-
"
|
|
2825
|
-
"Analysis exceeded its
|
|
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,
|
|
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
|
|
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
|
|
2870
|
-
var SummaryOutputSchema =
|
|
2871
|
-
summary:
|
|
2872
|
-
findings:
|
|
2873
|
-
uncertainties:
|
|
2874
|
-
citations:
|
|
2875
|
-
|
|
2876
|
-
path:
|
|
2877
|
-
startLine:
|
|
2878
|
-
endLine:
|
|
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
|
-
)
|
|
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.
|
|
2896
|
-
const
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
2992
|
-
var
|
|
2993
|
-
tasks:
|
|
3700
|
+
import { z as z12 } from "zod";
|
|
3701
|
+
var LegacyTaskOutputSchema = z12.object({
|
|
3702
|
+
tasks: z12.array(
|
|
2994
3703
|
TaskFieldsSchema.extend({
|
|
2995
|
-
taskId:
|
|
2996
|
-
sources:
|
|
3704
|
+
taskId: z12.uuid().nullable().optional(),
|
|
3705
|
+
sources: z12.array(z12.string())
|
|
2997
3706
|
})
|
|
2998
|
-
)
|
|
2999
|
-
uncertainties:
|
|
3707
|
+
),
|
|
3708
|
+
uncertainties: z12.array(z12.string()).optional()
|
|
3000
3709
|
}).strict();
|
|
3001
|
-
|
|
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
|
-
|
|
3005
|
-
|
|
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
|
|
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
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
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
|
|
3752
|
+
"Existing task identity must be unique and belong to this member."
|
|
3032
3753
|
);
|
|
3033
|
-
used.add(
|
|
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.
|
|
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(
|
|
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(
|
|
3058
|
-
previous: job.taskContext
|
|
3059
|
-
|
|
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 {
|
|
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)
|
|
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: [],
|
|
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
|
-
|
|
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
|
|
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
|
|
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) =>
|
|
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 =
|
|
3299
|
-
after:
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
|
4083
|
+
import { z as z14 } from "zod";
|
|
3343
4084
|
function registerTaskRoutes(server, hub) {
|
|
3344
4085
|
const store = new TaskStore(hub);
|
|
3345
|
-
const id = (params) =>
|
|
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(
|
|
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
|
|
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:
|
|
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
|
|
4220
|
+
import { z as z15 } from "zod";
|
|
3480
4221
|
var PROTOCOL_VERSION = 1;
|
|
3481
|
-
var HealthSchema =
|
|
3482
|
-
var ServerInfoSchema =
|
|
3483
|
-
name:
|
|
3484
|
-
version:
|
|
3485
|
-
protocolVersion:
|
|
3486
|
-
capabilities:
|
|
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 =
|
|
3489
|
-
error:
|
|
3490
|
-
code:
|
|
3491
|
-
message:
|
|
3492
|
-
requestId:
|
|
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
|
|
3501
|
-
var CredentialSchema =
|
|
3502
|
-
var EnrollmentSchema =
|
|
3503
|
-
operationId:
|
|
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 =
|
|
3510
|
-
checkIntervalSeconds:
|
|
3511
|
-
fileSettleSeconds:
|
|
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 =
|
|
3514
|
-
checkedAt:
|
|
3515
|
-
published:
|
|
3516
|
-
appliedFiles:
|
|
3517
|
-
pendingFiles:
|
|
3518
|
-
sessionSources:
|
|
3519
|
-
errors:
|
|
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 =
|
|
3522
|
-
clientVersion:
|
|
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:
|
|
3527
|
-
configError:
|
|
3528
|
-
reportError:
|
|
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 =
|
|
3532
|
-
devices:
|
|
3533
|
-
|
|
4272
|
+
var WebDevicesSchema = z16.object({
|
|
4273
|
+
devices: z16.array(
|
|
4274
|
+
z16.object({
|
|
3534
4275
|
memberId: NameSchema,
|
|
3535
|
-
bound:
|
|
3536
|
-
lastSeenAt:
|
|
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
|
|
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 } =
|
|
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 } =
|
|
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
|
|
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 } =
|
|
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 } =
|
|
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
|
|
4508
|
+
import { z as z20 } from "zod";
|
|
3768
4509
|
|
|
3769
4510
|
// packages/protocol/sessions.ts
|
|
3770
|
-
import { z as
|
|
3771
|
-
var AgentSchema =
|
|
3772
|
-
var SessionSnapshotSchema =
|
|
3773
|
-
formatVersion:
|
|
3774
|
-
adapterVersion:
|
|
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:
|
|
3777
|
-
cwd:
|
|
3778
|
-
messages:
|
|
3779
|
-
|
|
3780
|
-
id:
|
|
3781
|
-
role:
|
|
3782
|
-
text:
|
|
3783
|
-
source:
|
|
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 =
|
|
3791
|
-
operationId:
|
|
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 =
|
|
4537
|
+
var SessionInfoSchema = z19.object({
|
|
3797
4538
|
owner: NameSchema,
|
|
3798
4539
|
sessionKey: HashSchema,
|
|
3799
4540
|
agent: AgentSchema,
|
|
3800
|
-
nativeId:
|
|
3801
|
-
cwd:
|
|
3802
|
-
title:
|
|
4541
|
+
nativeId: z19.string(),
|
|
4542
|
+
cwd: z19.string(),
|
|
4543
|
+
title: z19.string(),
|
|
3803
4544
|
revision: RevisionSchema,
|
|
3804
|
-
updatedAt:
|
|
3805
|
-
messageCount:
|
|
4545
|
+
updatedAt: z19.string(),
|
|
4546
|
+
messageCount: z19.number().int().nonnegative(),
|
|
3806
4547
|
blob: BlobRefSchema
|
|
3807
4548
|
});
|
|
3808
|
-
var SessionListSchema =
|
|
3809
|
-
sessions:
|
|
4549
|
+
var SessionListSchema = z19.object({
|
|
4550
|
+
sessions: z19.array(SessionInfoSchema),
|
|
3810
4551
|
nextCursor: RevisionSchema.nullable()
|
|
3811
4552
|
});
|
|
3812
|
-
var SessionStatsSchema =
|
|
3813
|
-
totalSessions:
|
|
3814
|
-
totalMessages:
|
|
3815
|
-
memberCount:
|
|
3816
|
-
updatedAt:
|
|
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 =
|
|
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 =
|
|
3823
|
-
matches:
|
|
3824
|
-
|
|
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:
|
|
3829
|
-
role:
|
|
3830
|
-
snippet:
|
|
3831
|
-
title:
|
|
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,
|
|
4003
|
-
SessionScopeSchema.parse(
|
|
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
|
-
${
|
|
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
|
-
...
|
|
4016
|
-
...
|
|
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 =
|
|
4049
|
-
after:
|
|
4050
|
-
limit:
|
|
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:
|
|
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 =
|
|
4082
|
-
const q =
|
|
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
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
4143
|
-
const query =
|
|
4144
|
-
revision:
|
|
4145
|
-
after:
|
|
4146
|
-
limit:
|
|
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 } =
|
|
4158
|
-
const q =
|
|
4898
|
+
const { owner } = z21.object({ owner: FileOwnerSchema }).parse(request.params);
|
|
4899
|
+
const q = z21.object({
|
|
4159
4900
|
path: FilePathSchema,
|
|
4160
|
-
revision:
|
|
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 } =
|
|
4166
|
-
const q =
|
|
4167
|
-
path:
|
|
4168
|
-
revision:
|
|
4169
|
-
after:
|
|
4170
|
-
limit:
|
|
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 } =
|
|
4176
|
-
const query =
|
|
4177
|
-
after:
|
|
4178
|
-
limit:
|
|
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 (
|
|
4248
|
-
registerProjectScopeRoutes(
|
|
4249
|
-
registerAiJobRoutes(
|
|
4250
|
-
registerTaskRoutes(
|
|
4251
|
-
registerControlRoutes(
|
|
4252
|
-
registerFileRoutes(
|
|
4253
|
-
registerSessionRoutes(
|
|
4254
|
-
|
|
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
|
|
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,
|
|
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:
|
|
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:
|
|
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:
|
|
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(`${
|
|
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:
|
|
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) });
|