chatroom-cli 1.11.2 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1613 -1423
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10895,10 +10895,10 @@ function initHarnessRegistry() {
|
|
|
10895
10895
|
}
|
|
10896
10896
|
var initialized = false;
|
|
10897
10897
|
var init_init_registry = __esm(() => {
|
|
10898
|
-
|
|
10898
|
+
init_cursor();
|
|
10899
10899
|
init_opencode();
|
|
10900
10900
|
init_pi();
|
|
10901
|
-
|
|
10901
|
+
init_registry();
|
|
10902
10902
|
});
|
|
10903
10903
|
|
|
10904
10904
|
// src/infrastructure/services/remote-agents/index.ts
|
|
@@ -11685,8 +11685,8 @@ var init_utils = __esm(() => {
|
|
|
11685
11685
|
|
|
11686
11686
|
// ../../services/backend/prompts/base/shared/getting-started-content.ts
|
|
11687
11687
|
var init_getting_started_content = __esm(() => {
|
|
11688
|
-
init_utils();
|
|
11689
11688
|
init_reminder();
|
|
11689
|
+
init_utils();
|
|
11690
11690
|
});
|
|
11691
11691
|
|
|
11692
11692
|
// ../../services/backend/prompts/cli/index.ts
|
|
@@ -12320,8 +12320,7 @@ async function classify(chatroomId, options, deps) {
|
|
|
12320
12320
|
console.error(`❌ \`classify\` is only available to the entry point role (${entryPoint}). Your role is ${role}.`);
|
|
12321
12321
|
console.error("");
|
|
12322
12322
|
console.error(" Entry point roles receive user messages and must classify them.");
|
|
12323
|
-
console.error(" Other roles receive handoffs
|
|
12324
|
-
console.error(` ${cliEnvPrefix}chatroom task-started --chatroom-id=${chatroomId} --role=${role} --task-id=<task-id> --no-classify`);
|
|
12323
|
+
console.error(" Other roles receive handoffs — use `task read` to mark in_progress.");
|
|
12325
12324
|
process.exit(1);
|
|
12326
12325
|
}
|
|
12327
12326
|
if (originMessageClassification === "new_feature") {
|
|
@@ -12815,12 +12814,19 @@ __export(exports_backlog, {
|
|
|
12815
12814
|
patchBacklog: () => patchBacklog,
|
|
12816
12815
|
markForReviewBacklog: () => markForReviewBacklog,
|
|
12817
12816
|
listBacklog: () => listBacklog,
|
|
12817
|
+
importBacklog: () => importBacklog,
|
|
12818
12818
|
historyBacklog: () => historyBacklog,
|
|
12819
|
+
exportBacklog: () => exportBacklog,
|
|
12820
|
+
computeContentHash: () => computeContentHash,
|
|
12819
12821
|
completeBacklog: () => completeBacklog,
|
|
12822
|
+
closeBacklog: () => closeBacklog,
|
|
12820
12823
|
addBacklog: () => addBacklog
|
|
12821
12824
|
});
|
|
12825
|
+
import { createHash } from "node:crypto";
|
|
12826
|
+
import * as nodePath from "node:path";
|
|
12822
12827
|
async function createDefaultDeps11() {
|
|
12823
12828
|
const client2 = await getConvexClient();
|
|
12829
|
+
const fs = await import("node:fs/promises");
|
|
12824
12830
|
return {
|
|
12825
12831
|
backend: {
|
|
12826
12832
|
mutation: (endpoint, args) => client2.mutation(endpoint, args),
|
|
@@ -12830,6 +12836,11 @@ async function createDefaultDeps11() {
|
|
|
12830
12836
|
getSessionId,
|
|
12831
12837
|
getConvexUrl,
|
|
12832
12838
|
getOtherSessionUrls
|
|
12839
|
+
},
|
|
12840
|
+
fs: {
|
|
12841
|
+
writeFile: (path2, data) => fs.writeFile(path2, data, "utf-8"),
|
|
12842
|
+
readFile: (path2, encoding) => fs.readFile(path2, { encoding }),
|
|
12843
|
+
mkdir: (path2, options) => fs.mkdir(path2, options)
|
|
12833
12844
|
}
|
|
12834
12845
|
};
|
|
12835
12846
|
}
|
|
@@ -12856,17 +12867,19 @@ async function listBacklog(chatroomId, options, deps) {
|
|
|
12856
12867
|
const backlogItems = await d.backend.query(api.backlog.listBacklogItems, {
|
|
12857
12868
|
sessionId,
|
|
12858
12869
|
chatroomId,
|
|
12859
|
-
statusFilter: "
|
|
12870
|
+
statusFilter: "backlog",
|
|
12871
|
+
sort: options.sort,
|
|
12872
|
+
filter: options.filter,
|
|
12860
12873
|
limit
|
|
12861
12874
|
});
|
|
12862
12875
|
console.log("");
|
|
12863
12876
|
console.log("══════════════════════════════════════════════════");
|
|
12864
|
-
console.log("\uD83D\uDCCB
|
|
12877
|
+
console.log("\uD83D\uDCCB BACKLOG");
|
|
12865
12878
|
console.log("══════════════════════════════════════════════════");
|
|
12866
12879
|
console.log(`Chatroom: ${chatroomId}`);
|
|
12867
12880
|
console.log("");
|
|
12868
12881
|
if (backlogItems.length === 0) {
|
|
12869
|
-
console.log("No
|
|
12882
|
+
console.log("No backlog items.");
|
|
12870
12883
|
} else {
|
|
12871
12884
|
console.log("──────────────────────────────────────────────────");
|
|
12872
12885
|
for (let i2 = 0;i2 < backlogItems.length; i2++) {
|
|
@@ -13149,7 +13162,7 @@ async function historyBacklog(chatroomId, options, deps) {
|
|
|
13149
13162
|
const sessionId = requireAuth2(d);
|
|
13150
13163
|
validateChatroomId(chatroomId);
|
|
13151
13164
|
const now = Date.now();
|
|
13152
|
-
const defaultFrom = now -
|
|
13165
|
+
const defaultFrom = now - 2592000000;
|
|
13153
13166
|
let fromMs;
|
|
13154
13167
|
let toMs;
|
|
13155
13168
|
if (options.from) {
|
|
@@ -13239,6 +13252,38 @@ async function historyBacklog(chatroomId, options, deps) {
|
|
|
13239
13252
|
return;
|
|
13240
13253
|
}
|
|
13241
13254
|
}
|
|
13255
|
+
async function closeBacklog(chatroomId, options, deps) {
|
|
13256
|
+
const d = deps ?? await createDefaultDeps11();
|
|
13257
|
+
const sessionId = requireAuth2(d);
|
|
13258
|
+
validateChatroomId(chatroomId);
|
|
13259
|
+
if (!options.backlogItemId || options.backlogItemId.trim().length === 0) {
|
|
13260
|
+
console.error(`❌ Backlog item ID is required`);
|
|
13261
|
+
process.exit(1);
|
|
13262
|
+
return;
|
|
13263
|
+
}
|
|
13264
|
+
if (!options.reason || options.reason.trim().length === 0) {
|
|
13265
|
+
console.error(`❌ Reason is required when closing a backlog item`);
|
|
13266
|
+
process.exit(1);
|
|
13267
|
+
return;
|
|
13268
|
+
}
|
|
13269
|
+
try {
|
|
13270
|
+
await d.backend.mutation(api.backlog.closeBacklogItem, {
|
|
13271
|
+
sessionId,
|
|
13272
|
+
itemId: options.backlogItemId,
|
|
13273
|
+
reason: options.reason
|
|
13274
|
+
});
|
|
13275
|
+
console.log("");
|
|
13276
|
+
console.log("✅ Backlog item closed");
|
|
13277
|
+
console.log(` ID: ${options.backlogItemId}`);
|
|
13278
|
+
console.log(` Status: closed`);
|
|
13279
|
+
console.log(` Reason: ${options.reason}`);
|
|
13280
|
+
console.log("");
|
|
13281
|
+
} catch (error) {
|
|
13282
|
+
console.error(`❌ Failed to close backlog item: ${error.message}`);
|
|
13283
|
+
process.exit(1);
|
|
13284
|
+
return;
|
|
13285
|
+
}
|
|
13286
|
+
}
|
|
13242
13287
|
function getStatusEmoji(status) {
|
|
13243
13288
|
switch (status) {
|
|
13244
13289
|
case "pending":
|
|
@@ -13259,10 +13304,121 @@ function getStatusEmoji(status) {
|
|
|
13259
13304
|
return "⚫";
|
|
13260
13305
|
}
|
|
13261
13306
|
}
|
|
13307
|
+
function computeContentHash(content) {
|
|
13308
|
+
return createHash("sha256").update(content).digest("hex");
|
|
13309
|
+
}
|
|
13310
|
+
async function exportBacklog(chatroomId, options, deps) {
|
|
13311
|
+
const d = deps ?? await createDefaultDeps11();
|
|
13312
|
+
const sessionId = requireAuth2(d);
|
|
13313
|
+
validateChatroomId(chatroomId);
|
|
13314
|
+
if (!d.fs) {
|
|
13315
|
+
console.error("❌ File system operations not available");
|
|
13316
|
+
process.exit(1);
|
|
13317
|
+
return;
|
|
13318
|
+
}
|
|
13319
|
+
try {
|
|
13320
|
+
const backlogItems = await d.backend.query(api.backlog.listBacklogItems, {
|
|
13321
|
+
sessionId,
|
|
13322
|
+
chatroomId,
|
|
13323
|
+
statusFilter: "backlog"
|
|
13324
|
+
});
|
|
13325
|
+
const exportData = {
|
|
13326
|
+
exportedAt: Date.now(),
|
|
13327
|
+
chatroomId,
|
|
13328
|
+
items: backlogItems.map((item) => {
|
|
13329
|
+
const exportItem = {
|
|
13330
|
+
contentHash: computeContentHash(item.content),
|
|
13331
|
+
content: item.content,
|
|
13332
|
+
status: item.status,
|
|
13333
|
+
createdBy: item.createdBy ?? "unknown",
|
|
13334
|
+
createdAt: item.createdAt
|
|
13335
|
+
};
|
|
13336
|
+
if (item.complexity)
|
|
13337
|
+
exportItem.complexity = item.complexity;
|
|
13338
|
+
if (item.value)
|
|
13339
|
+
exportItem.value = item.value;
|
|
13340
|
+
if (item.priority !== undefined)
|
|
13341
|
+
exportItem.priority = item.priority;
|
|
13342
|
+
return exportItem;
|
|
13343
|
+
})
|
|
13344
|
+
};
|
|
13345
|
+
const exportDir = options.path ?? nodePath.join(process.cwd(), DEFAULT_EXPORT_DIR);
|
|
13346
|
+
await d.fs.mkdir(exportDir, { recursive: true });
|
|
13347
|
+
const filePath = nodePath.join(exportDir, BACKLOG_EXPORT_FILENAME);
|
|
13348
|
+
await d.fs.writeFile(filePath, JSON.stringify(exportData, null, 2));
|
|
13349
|
+
console.log("");
|
|
13350
|
+
console.log(`✅ Exported ${exportData.items.length} backlog item(s)`);
|
|
13351
|
+
console.log(` File: ${filePath}`);
|
|
13352
|
+
console.log("");
|
|
13353
|
+
} catch (error) {
|
|
13354
|
+
console.error(`❌ Failed to export backlog items: ${error.message}`);
|
|
13355
|
+
process.exit(1);
|
|
13356
|
+
return;
|
|
13357
|
+
}
|
|
13358
|
+
}
|
|
13359
|
+
async function importBacklog(chatroomId, options, deps) {
|
|
13360
|
+
const d = deps ?? await createDefaultDeps11();
|
|
13361
|
+
const sessionId = requireAuth2(d);
|
|
13362
|
+
validateChatroomId(chatroomId);
|
|
13363
|
+
if (!d.fs) {
|
|
13364
|
+
console.error("❌ File system operations not available");
|
|
13365
|
+
process.exit(1);
|
|
13366
|
+
return;
|
|
13367
|
+
}
|
|
13368
|
+
try {
|
|
13369
|
+
const importDir = options.path ?? nodePath.join(process.cwd(), DEFAULT_EXPORT_DIR);
|
|
13370
|
+
const filePath = nodePath.join(importDir, BACKLOG_EXPORT_FILENAME);
|
|
13371
|
+
const raw = await d.fs.readFile(filePath, "utf-8");
|
|
13372
|
+
const exportData = JSON.parse(raw);
|
|
13373
|
+
const ageMs = Date.now() - exportData.exportedAt;
|
|
13374
|
+
if (ageMs > STALENESS_THRESHOLD_MS) {
|
|
13375
|
+
const ageDays = Math.floor(ageMs / 86400000);
|
|
13376
|
+
console.log(`⚠️ This export is ${ageDays} days old and may be stale.`);
|
|
13377
|
+
}
|
|
13378
|
+
const existingItems = await d.backend.query(api.backlog.listBacklogItems, {
|
|
13379
|
+
sessionId,
|
|
13380
|
+
chatroomId,
|
|
13381
|
+
statusFilter: "backlog"
|
|
13382
|
+
});
|
|
13383
|
+
const existingHashes = new Set(existingItems.map((item) => computeContentHash(item.content)));
|
|
13384
|
+
let imported = 0;
|
|
13385
|
+
let skipped = 0;
|
|
13386
|
+
for (const item of exportData.items) {
|
|
13387
|
+
const hash = computeContentHash(item.content);
|
|
13388
|
+
if (existingHashes.has(hash)) {
|
|
13389
|
+
skipped++;
|
|
13390
|
+
continue;
|
|
13391
|
+
}
|
|
13392
|
+
await d.backend.mutation(api.backlog.createBacklogItem, {
|
|
13393
|
+
sessionId,
|
|
13394
|
+
chatroomId,
|
|
13395
|
+
content: item.content,
|
|
13396
|
+
createdBy: item.createdBy,
|
|
13397
|
+
priority: item.priority,
|
|
13398
|
+
complexity: item.complexity,
|
|
13399
|
+
value: item.value
|
|
13400
|
+
});
|
|
13401
|
+
existingHashes.add(hash);
|
|
13402
|
+
imported++;
|
|
13403
|
+
}
|
|
13404
|
+
console.log("");
|
|
13405
|
+
console.log(`✅ Import complete`);
|
|
13406
|
+
console.log(` Total items in file: ${exportData.items.length}`);
|
|
13407
|
+
console.log(` Imported: ${imported}`);
|
|
13408
|
+
console.log(` Skipped (duplicate): ${skipped}`);
|
|
13409
|
+
console.log("");
|
|
13410
|
+
} catch (error) {
|
|
13411
|
+
console.error(`❌ Failed to import backlog items: ${error.message}`);
|
|
13412
|
+
process.exit(1);
|
|
13413
|
+
return;
|
|
13414
|
+
}
|
|
13415
|
+
}
|
|
13416
|
+
var BACKLOG_EXPORT_FILENAME = "backlog-export.json", STALENESS_THRESHOLD_MS, DEFAULT_EXPORT_DIR = ".chatroom/exports";
|
|
13262
13417
|
var init_backlog = __esm(() => {
|
|
13263
13418
|
init_api3();
|
|
13264
13419
|
init_storage();
|
|
13265
13420
|
init_client2();
|
|
13421
|
+
STALENESS_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1000;
|
|
13266
13422
|
});
|
|
13267
13423
|
|
|
13268
13424
|
// src/utils/file-content.ts
|
|
@@ -13354,6 +13510,32 @@ async function taskRead(chatroomId, options, deps) {
|
|
|
13354
13510
|
console.log(`✅ Task content:`);
|
|
13355
13511
|
console.log(` Task ID: ${result.taskId}`);
|
|
13356
13512
|
console.log(` Status: ${result.status}`);
|
|
13513
|
+
if (result.context) {
|
|
13514
|
+
console.log("");
|
|
13515
|
+
console.log("PINNED CONTEXT");
|
|
13516
|
+
console.log("---");
|
|
13517
|
+
console.log("<context>");
|
|
13518
|
+
console.log(result.context.content);
|
|
13519
|
+
console.log("</context>");
|
|
13520
|
+
if (result.context.triggerMessageContent) {
|
|
13521
|
+
console.log("in response to");
|
|
13522
|
+
const senderTag = result.context.triggerMessageSenderRole ?? "unknown";
|
|
13523
|
+
console.log(`<${senderTag}-message>`);
|
|
13524
|
+
console.log(result.context.triggerMessageContent);
|
|
13525
|
+
console.log(`</${senderTag}-message>`);
|
|
13526
|
+
}
|
|
13527
|
+
const hoursAgo = Math.round(result.context.elapsedHours);
|
|
13528
|
+
const msgsSince = result.context.messagesSinceContext;
|
|
13529
|
+
const isStale = hoursAgo >= 24 || msgsSince >= 50;
|
|
13530
|
+
if (isStale) {
|
|
13531
|
+
const ageLabel = hoursAgo >= 48 ? `${Math.round(hoursAgo / 24)}d old` : hoursAgo >= 24 ? `${hoursAgo}h old` : `${msgsSince} messages old`;
|
|
13532
|
+
console.log(`<system-notice>`);
|
|
13533
|
+
console.log(`⚠️ Context is ${ageLabel}.`);
|
|
13534
|
+
console.log(` Entry point role will update when needed.`);
|
|
13535
|
+
console.log(`</system-notice>`);
|
|
13536
|
+
}
|
|
13537
|
+
console.log("---");
|
|
13538
|
+
}
|
|
13357
13539
|
console.log(`
|
|
13358
13540
|
${result.content}`);
|
|
13359
13541
|
} catch (error) {
|
|
@@ -14256,317 +14438,98 @@ var init_get_system_prompt = __esm(() => {
|
|
|
14256
14438
|
// ../../services/backend/config/reliability.ts
|
|
14257
14439
|
var DAEMON_HEARTBEAT_INTERVAL_MS = 30000, AGENT_REQUEST_DEADLINE_MS = 120000;
|
|
14258
14440
|
|
|
14259
|
-
// src/
|
|
14260
|
-
function
|
|
14261
|
-
|
|
14262
|
-
|
|
14263
|
-
|
|
14264
|
-
|
|
14265
|
-
function agentKey2(chatroomId, role) {
|
|
14266
|
-
return `${chatroomId}:${role.toLowerCase()}`;
|
|
14267
|
-
}
|
|
14268
|
-
|
|
14269
|
-
// src/events/lifecycle/on-agent-shutdown.ts
|
|
14270
|
-
async function onAgentShutdown(ctx, options) {
|
|
14271
|
-
const { chatroomId, role, pid, skipKill } = options;
|
|
14272
|
-
try {
|
|
14273
|
-
ctx.pendingStops.set(agentKey2(chatroomId, role), options.stopReason ?? "user.stop");
|
|
14274
|
-
} catch (e) {
|
|
14275
|
-
console.log(` ⚠️ Failed to mark intentional stop for ${role}: ${e.message}`);
|
|
14276
|
-
}
|
|
14277
|
-
let killed = false;
|
|
14278
|
-
if (!skipKill) {
|
|
14279
|
-
try {
|
|
14280
|
-
ctx.deps.processes.kill(-pid, "SIGTERM");
|
|
14281
|
-
} catch (e) {
|
|
14282
|
-
const isEsrch = e.code === "ESRCH" || e.message?.includes("ESRCH");
|
|
14283
|
-
if (isEsrch) {
|
|
14284
|
-
killed = true;
|
|
14285
|
-
}
|
|
14286
|
-
if (!isEsrch) {
|
|
14287
|
-
console.log(` ⚠️ Failed to send SIGTERM to ${role}: ${e.message}`);
|
|
14288
|
-
}
|
|
14289
|
-
}
|
|
14290
|
-
if (!killed) {
|
|
14291
|
-
const SIGTERM_TIMEOUT_MS = 1e4;
|
|
14292
|
-
const POLL_INTERVAL_MS2 = 500;
|
|
14293
|
-
const deadline = Date.now() + SIGTERM_TIMEOUT_MS;
|
|
14294
|
-
while (Date.now() < deadline) {
|
|
14295
|
-
await ctx.deps.clock.delay(POLL_INTERVAL_MS2);
|
|
14296
|
-
try {
|
|
14297
|
-
ctx.deps.processes.kill(pid, 0);
|
|
14298
|
-
} catch {
|
|
14299
|
-
killed = true;
|
|
14300
|
-
break;
|
|
14301
|
-
}
|
|
14302
|
-
}
|
|
14303
|
-
}
|
|
14304
|
-
if (!killed) {
|
|
14305
|
-
try {
|
|
14306
|
-
ctx.deps.processes.kill(-pid, "SIGKILL");
|
|
14307
|
-
} catch {
|
|
14308
|
-
killed = true;
|
|
14309
|
-
}
|
|
14310
|
-
}
|
|
14311
|
-
if (!killed) {
|
|
14312
|
-
await ctx.deps.clock.delay(5000);
|
|
14313
|
-
try {
|
|
14314
|
-
ctx.deps.processes.kill(pid, 0);
|
|
14315
|
-
console.log(` ⚠️ Process ${pid} (${role}) still alive after SIGKILL — possible zombie`);
|
|
14316
|
-
} catch {
|
|
14317
|
-
killed = true;
|
|
14318
|
-
}
|
|
14319
|
-
}
|
|
14320
|
-
}
|
|
14321
|
-
if (killed || skipKill) {
|
|
14322
|
-
try {
|
|
14323
|
-
ctx.deps.machine.clearAgentPid(ctx.machineId, chatroomId, role);
|
|
14324
|
-
} catch (e) {
|
|
14325
|
-
console.log(` ⚠️ Failed to clear local PID for ${role}: ${e.message}`);
|
|
14326
|
-
}
|
|
14327
|
-
}
|
|
14328
|
-
return {
|
|
14329
|
-
killed: killed || (skipKill ?? false),
|
|
14330
|
-
cleaned: killed || (skipKill ?? false)
|
|
14331
|
-
};
|
|
14332
|
-
}
|
|
14333
|
-
var init_on_agent_shutdown = () => {};
|
|
14334
|
-
|
|
14335
|
-
// src/events/lifecycle/on-daemon-shutdown.ts
|
|
14336
|
-
async function onDaemonShutdown(ctx) {
|
|
14337
|
-
const agents = ctx.deps.machine.listAgentEntries(ctx.machineId);
|
|
14338
|
-
if (agents.length > 0) {
|
|
14339
|
-
console.log(`[${formatTimestamp()}] Stopping ${agents.length} agent(s)...`);
|
|
14340
|
-
await Promise.allSettled(agents.map(async ({ chatroomId, role, entry }) => {
|
|
14341
|
-
const result = await onAgentShutdown(ctx, {
|
|
14342
|
-
chatroomId,
|
|
14343
|
-
role,
|
|
14344
|
-
pid: entry.pid
|
|
14345
|
-
});
|
|
14346
|
-
if (result.killed) {
|
|
14347
|
-
console.log(` Sent SIGTERM to ${role} (PID ${entry.pid})`);
|
|
14348
|
-
} else {
|
|
14349
|
-
console.log(` ${role} (PID ${entry.pid}) already exited`);
|
|
14350
|
-
}
|
|
14351
|
-
return result;
|
|
14352
|
-
}));
|
|
14353
|
-
await ctx.deps.clock.delay(AGENT_SHUTDOWN_TIMEOUT_MS);
|
|
14354
|
-
for (const { role, entry } of agents) {
|
|
14355
|
-
try {
|
|
14356
|
-
ctx.deps.processes.kill(entry.pid, 0);
|
|
14357
|
-
ctx.deps.processes.kill(entry.pid, "SIGKILL");
|
|
14358
|
-
console.log(` Force-killed ${role} (PID ${entry.pid})`);
|
|
14359
|
-
} catch {}
|
|
14360
|
-
}
|
|
14361
|
-
console.log(`[${formatTimestamp()}] All agents stopped`);
|
|
14441
|
+
// src/events/daemon/agent/on-request-start-agent.ts
|
|
14442
|
+
async function onRequestStartAgent(ctx, event) {
|
|
14443
|
+
const eventId = event._id.toString();
|
|
14444
|
+
if (Date.now() > event.deadline) {
|
|
14445
|
+
console.log(`[daemon] ⏰ Skipping expired agent.requestStart for role=${event.role} (id: ${eventId}, deadline passed)`);
|
|
14446
|
+
return;
|
|
14362
14447
|
}
|
|
14363
|
-
|
|
14364
|
-
|
|
14448
|
+
console.log(`[daemon] Processing agent.requestStart (id: ${eventId})`);
|
|
14449
|
+
const result = await ctx.deps.agentProcessManager.ensureRunning({
|
|
14450
|
+
chatroomId: event.chatroomId,
|
|
14451
|
+
role: event.role,
|
|
14452
|
+
agentHarness: event.agentHarness,
|
|
14453
|
+
model: event.model,
|
|
14454
|
+
workingDir: event.workingDir,
|
|
14455
|
+
reason: event.reason
|
|
14456
|
+
});
|
|
14457
|
+
if (!result.success) {
|
|
14458
|
+
console.log(`[daemon] Agent start rejected for role=${event.role}: ${result.error ?? "unknown"}`);
|
|
14459
|
+
} else {
|
|
14460
|
+
ctx.deps.backend.mutation(api.workspaces.registerWorkspace, {
|
|
14365
14461
|
sessionId: ctx.sessionId,
|
|
14462
|
+
chatroomId: event.chatroomId,
|
|
14366
14463
|
machineId: ctx.machineId,
|
|
14367
|
-
|
|
14464
|
+
workingDir: event.workingDir,
|
|
14465
|
+
hostname: ctx.config?.hostname ?? "unknown",
|
|
14466
|
+
registeredBy: event.role
|
|
14467
|
+
}).catch((err) => {
|
|
14468
|
+
console.warn(`[daemon] ⚠️ Failed to register workspace: ${err.message}`);
|
|
14368
14469
|
});
|
|
14369
|
-
}
|
|
14470
|
+
}
|
|
14370
14471
|
}
|
|
14371
|
-
var
|
|
14372
|
-
var init_on_daemon_shutdown = __esm(() => {
|
|
14472
|
+
var init_on_request_start_agent = __esm(() => {
|
|
14373
14473
|
init_api3();
|
|
14374
|
-
init_on_agent_shutdown();
|
|
14375
14474
|
});
|
|
14376
14475
|
|
|
14377
|
-
// src/commands/machine/daemon-start/handlers/
|
|
14378
|
-
async function
|
|
14379
|
-
|
|
14380
|
-
|
|
14381
|
-
|
|
14382
|
-
|
|
14383
|
-
|
|
14384
|
-
|
|
14385
|
-
|
|
14386
|
-
|
|
14387
|
-
|
|
14388
|
-
|
|
14389
|
-
}
|
|
14390
|
-
|
|
14476
|
+
// src/commands/machine/daemon-start/handlers/stop-agent.ts
|
|
14477
|
+
async function executeStopAgent(ctx, args) {
|
|
14478
|
+
const { chatroomId, role, reason } = args;
|
|
14479
|
+
console.log(` ↪ stop-agent command received`);
|
|
14480
|
+
console.log(` Chatroom: ${chatroomId}`);
|
|
14481
|
+
console.log(` Role: ${role}`);
|
|
14482
|
+
console.log(` Reason: ${reason}`);
|
|
14483
|
+
const result = await ctx.deps.agentProcessManager.stop({
|
|
14484
|
+
chatroomId,
|
|
14485
|
+
role,
|
|
14486
|
+
reason
|
|
14487
|
+
});
|
|
14488
|
+
const msg = result.success ? `Agent stopped (${role})` : `Failed to stop agent (${role})`;
|
|
14489
|
+
console.log(` ${result.success ? "✅" : "⚠️ "} ${msg}`);
|
|
14490
|
+
return { result: msg, failed: !result.success };
|
|
14391
14491
|
}
|
|
14392
|
-
var init_shared = __esm(() => {
|
|
14393
|
-
init_api3();
|
|
14394
|
-
});
|
|
14395
14492
|
|
|
14396
|
-
// src/
|
|
14397
|
-
async function
|
|
14398
|
-
|
|
14399
|
-
|
|
14400
|
-
|
|
14401
|
-
} else {
|
|
14402
|
-
let recovered = 0;
|
|
14403
|
-
let cleared = 0;
|
|
14404
|
-
const chatroomIds = new Set;
|
|
14405
|
-
for (const { chatroomId, role, entry } of entries) {
|
|
14406
|
-
const { pid, harness } = entry;
|
|
14407
|
-
const service = ctx.agentServices.get(harness) ?? ctx.agentServices.values().next().value;
|
|
14408
|
-
const alive = service ? service.isAlive(pid) : false;
|
|
14409
|
-
if (alive) {
|
|
14410
|
-
console.log(` ✅ Recovered: ${role} (PID ${pid}, harness: ${harness})`);
|
|
14411
|
-
recovered++;
|
|
14412
|
-
chatroomIds.add(chatroomId);
|
|
14413
|
-
} else {
|
|
14414
|
-
console.log(` \uD83E\uDDF9 Stale PID ${pid} for ${role} — clearing`);
|
|
14415
|
-
await clearAgentPidEverywhere(ctx, chatroomId, role);
|
|
14416
|
-
cleared++;
|
|
14417
|
-
}
|
|
14418
|
-
}
|
|
14419
|
-
console.log(` Recovery complete: ${recovered} alive, ${cleared} stale cleared`);
|
|
14420
|
-
for (const chatroomId of chatroomIds) {
|
|
14421
|
-
try {
|
|
14422
|
-
const configsResult = await ctx.deps.backend.query(api.machines.getMachineAgentConfigs, {
|
|
14423
|
-
sessionId: ctx.sessionId,
|
|
14424
|
-
chatroomId
|
|
14425
|
-
});
|
|
14426
|
-
for (const config3 of configsResult.configs) {
|
|
14427
|
-
if (config3.machineId === ctx.machineId && config3.workingDir) {
|
|
14428
|
-
ctx.activeWorkingDirs.add(config3.workingDir);
|
|
14429
|
-
}
|
|
14430
|
-
}
|
|
14431
|
-
} catch {}
|
|
14432
|
-
}
|
|
14433
|
-
if (ctx.activeWorkingDirs.size > 0) {
|
|
14434
|
-
console.log(` \uD83D\uDD00 Recovered ${ctx.activeWorkingDirs.size} active working dir(s) for git tracking`);
|
|
14435
|
-
}
|
|
14493
|
+
// src/events/daemon/agent/on-request-stop-agent.ts
|
|
14494
|
+
async function onRequestStopAgent(ctx, event) {
|
|
14495
|
+
if (Date.now() > event.deadline) {
|
|
14496
|
+
console.log(`[daemon] ⏰ Skipping expired agent.requestStop for role=${event.role} (deadline passed)`);
|
|
14497
|
+
return;
|
|
14436
14498
|
}
|
|
14499
|
+
await executeStopAgent(ctx, {
|
|
14500
|
+
chatroomId: event.chatroomId,
|
|
14501
|
+
role: event.role,
|
|
14502
|
+
reason: event.reason
|
|
14503
|
+
});
|
|
14437
14504
|
}
|
|
14438
|
-
var
|
|
14439
|
-
init_api3();
|
|
14440
|
-
init_shared();
|
|
14441
|
-
});
|
|
14505
|
+
var init_on_request_stop_agent = () => {};
|
|
14442
14506
|
|
|
14443
|
-
// src/
|
|
14444
|
-
|
|
14445
|
-
|
|
14446
|
-
|
|
14447
|
-
|
|
14448
|
-
|
|
14507
|
+
// src/commands/machine/pid.ts
|
|
14508
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
14509
|
+
import { existsSync as existsSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4, unlinkSync as unlinkSync2, mkdirSync as mkdirSync4 } from "node:fs";
|
|
14510
|
+
import { homedir as homedir4 } from "node:os";
|
|
14511
|
+
import { join as join6 } from "node:path";
|
|
14512
|
+
function getUrlHash() {
|
|
14513
|
+
const url = getConvexUrl();
|
|
14514
|
+
return createHash2("sha256").update(url).digest("hex").substring(0, 8);
|
|
14515
|
+
}
|
|
14516
|
+
function getPidFileName() {
|
|
14517
|
+
return `daemon-${getUrlHash()}.pid`;
|
|
14518
|
+
}
|
|
14519
|
+
function ensureChatroomDir() {
|
|
14520
|
+
if (!existsSync4(CHATROOM_DIR4)) {
|
|
14521
|
+
mkdirSync4(CHATROOM_DIR4, { recursive: true, mode: 448 });
|
|
14449
14522
|
}
|
|
14450
|
-
|
|
14451
|
-
|
|
14452
|
-
|
|
14453
|
-
|
|
14454
|
-
|
|
14455
|
-
|
|
14456
|
-
|
|
14457
|
-
|
|
14458
|
-
|
|
14459
|
-
|
|
14460
|
-
return { allowed: false, retryAfterMs };
|
|
14461
|
-
}
|
|
14462
|
-
bucket.tokens -= 1;
|
|
14463
|
-
const remaining = Math.floor(bucket.tokens);
|
|
14464
|
-
if (remaining <= LOW_TOKEN_THRESHOLD) {
|
|
14465
|
-
console.warn(`⚠️ [RateLimiter] Agent spawn tokens running low for chatroom ${chatroomId} (${remaining}/${this.config.maxTokens} remaining)`);
|
|
14466
|
-
}
|
|
14467
|
-
return { allowed: true };
|
|
14468
|
-
}
|
|
14469
|
-
getStatus(chatroomId) {
|
|
14470
|
-
const bucket = this._getOrCreateBucket(chatroomId);
|
|
14471
|
-
this._refill(bucket);
|
|
14472
|
-
return {
|
|
14473
|
-
remaining: Math.floor(bucket.tokens),
|
|
14474
|
-
total: this.config.maxTokens
|
|
14475
|
-
};
|
|
14476
|
-
}
|
|
14477
|
-
_getOrCreateBucket(chatroomId) {
|
|
14478
|
-
if (!this.buckets.has(chatroomId)) {
|
|
14479
|
-
this.buckets.set(chatroomId, {
|
|
14480
|
-
tokens: this.config.initialTokens,
|
|
14481
|
-
lastRefillAt: Date.now()
|
|
14482
|
-
});
|
|
14483
|
-
}
|
|
14484
|
-
return this.buckets.get(chatroomId);
|
|
14485
|
-
}
|
|
14486
|
-
_refill(bucket) {
|
|
14487
|
-
const now = Date.now();
|
|
14488
|
-
const elapsed = now - bucket.lastRefillAt;
|
|
14489
|
-
if (elapsed >= this.config.refillRateMs) {
|
|
14490
|
-
const tokensToAdd = Math.floor(elapsed / this.config.refillRateMs);
|
|
14491
|
-
bucket.tokens = Math.min(this.config.maxTokens, bucket.tokens + tokensToAdd);
|
|
14492
|
-
bucket.lastRefillAt += tokensToAdd * this.config.refillRateMs;
|
|
14493
|
-
}
|
|
14494
|
-
}
|
|
14495
|
-
}
|
|
14496
|
-
var DEFAULT_CONFIG, LOW_TOKEN_THRESHOLD = 1;
|
|
14497
|
-
var init_rate_limiter = __esm(() => {
|
|
14498
|
-
DEFAULT_CONFIG = {
|
|
14499
|
-
maxTokens: 5,
|
|
14500
|
-
refillRateMs: 60000,
|
|
14501
|
-
initialTokens: 5
|
|
14502
|
-
};
|
|
14503
|
-
});
|
|
14504
|
-
|
|
14505
|
-
// src/infrastructure/services/harness-spawning/harness-spawning-service.ts
|
|
14506
|
-
class HarnessSpawningService {
|
|
14507
|
-
rateLimiter;
|
|
14508
|
-
concurrentAgents = new Map;
|
|
14509
|
-
constructor({ rateLimiter }) {
|
|
14510
|
-
this.rateLimiter = rateLimiter;
|
|
14511
|
-
}
|
|
14512
|
-
shouldAllowSpawn(chatroomId, reason) {
|
|
14513
|
-
const current = this.concurrentAgents.get(chatroomId) ?? 0;
|
|
14514
|
-
if (current >= MAX_CONCURRENT_AGENTS_PER_CHATROOM) {
|
|
14515
|
-
console.warn(`⚠️ [HarnessSpawningService] Concurrent agent limit reached for chatroom ${chatroomId} ` + `(${current}/${MAX_CONCURRENT_AGENTS_PER_CHATROOM} active agents). Spawn rejected.`);
|
|
14516
|
-
return { allowed: false };
|
|
14517
|
-
}
|
|
14518
|
-
const result = this.rateLimiter.tryConsume(chatroomId, reason);
|
|
14519
|
-
if (!result.allowed) {
|
|
14520
|
-
console.warn(`⚠️ [HarnessSpawningService] Spawn blocked by rate limiter for chatroom ${chatroomId} ` + `(reason: ${reason}).`);
|
|
14521
|
-
}
|
|
14522
|
-
return result;
|
|
14523
|
-
}
|
|
14524
|
-
recordSpawn(chatroomId) {
|
|
14525
|
-
const current = this.concurrentAgents.get(chatroomId) ?? 0;
|
|
14526
|
-
this.concurrentAgents.set(chatroomId, current + 1);
|
|
14527
|
-
}
|
|
14528
|
-
recordExit(chatroomId) {
|
|
14529
|
-
const current = this.concurrentAgents.get(chatroomId) ?? 0;
|
|
14530
|
-
const next = Math.max(0, current - 1);
|
|
14531
|
-
this.concurrentAgents.set(chatroomId, next);
|
|
14532
|
-
}
|
|
14533
|
-
getConcurrentCount(chatroomId) {
|
|
14534
|
-
return this.concurrentAgents.get(chatroomId) ?? 0;
|
|
14535
|
-
}
|
|
14536
|
-
}
|
|
14537
|
-
var MAX_CONCURRENT_AGENTS_PER_CHATROOM = 10;
|
|
14538
|
-
|
|
14539
|
-
// src/infrastructure/services/harness-spawning/index.ts
|
|
14540
|
-
var init_harness_spawning = __esm(() => {
|
|
14541
|
-
init_rate_limiter();
|
|
14542
|
-
});
|
|
14543
|
-
|
|
14544
|
-
// src/commands/machine/pid.ts
|
|
14545
|
-
import { createHash } from "node:crypto";
|
|
14546
|
-
import { existsSync as existsSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4, unlinkSync as unlinkSync2, mkdirSync as mkdirSync4 } from "node:fs";
|
|
14547
|
-
import { homedir as homedir4 } from "node:os";
|
|
14548
|
-
import { join as join5 } from "node:path";
|
|
14549
|
-
function getUrlHash() {
|
|
14550
|
-
const url = getConvexUrl();
|
|
14551
|
-
return createHash("sha256").update(url).digest("hex").substring(0, 8);
|
|
14552
|
-
}
|
|
14553
|
-
function getPidFileName() {
|
|
14554
|
-
return `daemon-${getUrlHash()}.pid`;
|
|
14555
|
-
}
|
|
14556
|
-
function ensureChatroomDir() {
|
|
14557
|
-
if (!existsSync4(CHATROOM_DIR4)) {
|
|
14558
|
-
mkdirSync4(CHATROOM_DIR4, { recursive: true, mode: 448 });
|
|
14559
|
-
}
|
|
14560
|
-
}
|
|
14561
|
-
function getPidFilePath() {
|
|
14562
|
-
return join5(CHATROOM_DIR4, getPidFileName());
|
|
14563
|
-
}
|
|
14564
|
-
function isProcessRunning(pid) {
|
|
14565
|
-
try {
|
|
14566
|
-
process.kill(pid, 0);
|
|
14567
|
-
return true;
|
|
14568
|
-
} catch {
|
|
14569
|
-
return false;
|
|
14523
|
+
}
|
|
14524
|
+
function getPidFilePath() {
|
|
14525
|
+
return join6(CHATROOM_DIR4, getPidFileName());
|
|
14526
|
+
}
|
|
14527
|
+
function isProcessRunning(pid) {
|
|
14528
|
+
try {
|
|
14529
|
+
process.kill(pid, 0);
|
|
14530
|
+
return true;
|
|
14531
|
+
} catch {
|
|
14532
|
+
return false;
|
|
14570
14533
|
}
|
|
14571
14534
|
}
|
|
14572
14535
|
function readPid() {
|
|
@@ -14624,858 +14587,566 @@ function releaseLock() {
|
|
|
14624
14587
|
var CHATROOM_DIR4;
|
|
14625
14588
|
var init_pid = __esm(() => {
|
|
14626
14589
|
init_client2();
|
|
14627
|
-
CHATROOM_DIR4 =
|
|
14590
|
+
CHATROOM_DIR4 = join6(homedir4(), ".chatroom");
|
|
14628
14591
|
});
|
|
14629
14592
|
|
|
14630
|
-
// src/
|
|
14631
|
-
|
|
14632
|
-
|
|
14633
|
-
on(event, listener) {
|
|
14634
|
-
if (!this.listeners.has(event)) {
|
|
14635
|
-
this.listeners.set(event, new Set);
|
|
14636
|
-
}
|
|
14637
|
-
this.listeners.get(event).add(listener);
|
|
14638
|
-
return () => {
|
|
14639
|
-
this.listeners.get(event)?.delete(listener);
|
|
14640
|
-
};
|
|
14641
|
-
}
|
|
14642
|
-
emit(event, payload) {
|
|
14643
|
-
const set = this.listeners.get(event);
|
|
14644
|
-
if (!set)
|
|
14645
|
-
return;
|
|
14646
|
-
for (const listener of set) {
|
|
14647
|
-
try {
|
|
14648
|
-
listener(payload);
|
|
14649
|
-
} catch (err) {
|
|
14650
|
-
console.warn(`[EventBus] Listener error on "${event}": ${err.message}`);
|
|
14651
|
-
}
|
|
14652
|
-
}
|
|
14653
|
-
}
|
|
14654
|
-
removeAllListeners() {
|
|
14655
|
-
this.listeners.clear();
|
|
14656
|
-
}
|
|
14593
|
+
// src/commands/machine/daemon-start/utils.ts
|
|
14594
|
+
function formatTimestamp() {
|
|
14595
|
+
return new Date().toISOString().replace("T", " ").substring(0, 19);
|
|
14657
14596
|
}
|
|
14658
14597
|
|
|
14659
|
-
// src/
|
|
14660
|
-
function
|
|
14661
|
-
|
|
14662
|
-
const ts = formatTimestamp();
|
|
14663
|
-
console.log(`[${ts}] Agent stopped: ${stopReason} (${role})`);
|
|
14664
|
-
const isDaemonRespawn = stopReason === "daemon.respawn";
|
|
14665
|
-
const isIntentional = stopReason === "user.stop" || stopReason === "platform.team_switch" || stopReason === "agent_process.turn_end" || stopReason === "agent_process.turn_end_quick_fail";
|
|
14666
|
-
if (isIntentional && !isDaemonRespawn) {
|
|
14667
|
-
console.log(`[${ts}] ℹ️ Agent process exited after intentional stop ` + `(PID: ${pid}, role: ${role}, code: ${code2}, signal: ${signal})`);
|
|
14668
|
-
} else if (isDaemonRespawn) {
|
|
14669
|
-
console.log(`[${ts}] \uD83D\uDD04 Agent process stopped for respawn ` + `(PID: ${pid}, role: ${role}, code: ${code2}, signal: ${signal})`);
|
|
14670
|
-
} else {
|
|
14671
|
-
console.log(`[${ts}] ⚠️ Agent process exited ` + `(PID: ${pid}, role: ${role}, code: ${code2}, signal: ${signal})`);
|
|
14672
|
-
}
|
|
14673
|
-
ctx.deps.backend.mutation(api.machines.recordAgentExited, {
|
|
14674
|
-
sessionId: ctx.sessionId,
|
|
14675
|
-
machineId: ctx.machineId,
|
|
14676
|
-
chatroomId,
|
|
14677
|
-
role,
|
|
14678
|
-
pid,
|
|
14679
|
-
stopReason,
|
|
14680
|
-
stopSignal: stopReason === "agent_process.signal" ? signal ?? undefined : undefined,
|
|
14681
|
-
exitCode: code2 ?? undefined,
|
|
14682
|
-
signal: signal ?? undefined,
|
|
14683
|
-
agentHarness: payload.agentHarness
|
|
14684
|
-
}).catch((err) => {
|
|
14685
|
-
console.log(` ⚠️ Failed to record agent exit event: ${err.message}`);
|
|
14686
|
-
});
|
|
14687
|
-
ctx.deps.machine.clearAgentPid(ctx.machineId, chatroomId, role);
|
|
14688
|
-
for (const service of ctx.agentServices.values()) {
|
|
14689
|
-
service.untrack(pid);
|
|
14690
|
-
}
|
|
14598
|
+
// src/infrastructure/git/types.ts
|
|
14599
|
+
function makeGitStateKey(machineId, workingDir) {
|
|
14600
|
+
return `${machineId}::${workingDir}`;
|
|
14691
14601
|
}
|
|
14692
|
-
var
|
|
14693
|
-
init_api3();
|
|
14694
|
-
});
|
|
14602
|
+
var FULL_DIFF_MAX_BYTES = 500000, COMMITS_PER_PAGE = 20;
|
|
14695
14603
|
|
|
14696
|
-
// src/
|
|
14697
|
-
|
|
14698
|
-
|
|
14699
|
-
|
|
14604
|
+
// src/infrastructure/git/git-reader.ts
|
|
14605
|
+
import { exec as exec2 } from "node:child_process";
|
|
14606
|
+
import { promisify as promisify2 } from "node:util";
|
|
14607
|
+
async function runGit(args, cwd) {
|
|
14608
|
+
try {
|
|
14609
|
+
const result = await execAsync2(`git ${args}`, {
|
|
14610
|
+
cwd,
|
|
14611
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_PAGER: "cat", NO_COLOR: "1" },
|
|
14612
|
+
maxBuffer: FULL_DIFF_MAX_BYTES + 64 * 1024
|
|
14613
|
+
});
|
|
14614
|
+
return result;
|
|
14615
|
+
} catch (err) {
|
|
14616
|
+
return { error: err };
|
|
14617
|
+
}
|
|
14700
14618
|
}
|
|
14701
|
-
|
|
14702
|
-
|
|
14703
|
-
// src/events/daemon/agent/on-agent-stopped.ts
|
|
14704
|
-
function onAgentStopped(ctx, payload) {
|
|
14705
|
-
const ts = formatTimestamp();
|
|
14706
|
-
console.log(`[${ts}] \uD83D\uDD34 Agent stopped: ${payload.role} (PID: ${payload.pid})`);
|
|
14619
|
+
function isGitNotInstalled(message) {
|
|
14620
|
+
return message.includes("command not found") || message.includes("ENOENT") || message.includes("not found") || message.includes("'git' is not recognized");
|
|
14707
14621
|
}
|
|
14708
|
-
|
|
14709
|
-
|
|
14710
|
-
// src/events/daemon/register-listeners.ts
|
|
14711
|
-
function registerEventListeners(ctx) {
|
|
14712
|
-
const unsubs = [];
|
|
14713
|
-
unsubs.push(ctx.events.on("agent:exited", (payload) => onAgentExited(ctx, payload)));
|
|
14714
|
-
unsubs.push(ctx.events.on("agent:started", (payload) => onAgentStarted(ctx, payload)));
|
|
14715
|
-
unsubs.push(ctx.events.on("agent:stopped", (payload) => onAgentStopped(ctx, payload)));
|
|
14716
|
-
return () => {
|
|
14717
|
-
for (const unsub of unsubs) {
|
|
14718
|
-
unsub();
|
|
14719
|
-
}
|
|
14720
|
-
};
|
|
14622
|
+
function isNotAGitRepo(message) {
|
|
14623
|
+
return message.includes("not a git repository") || message.includes("Not a git repository");
|
|
14721
14624
|
}
|
|
14722
|
-
|
|
14723
|
-
|
|
14724
|
-
|
|
14725
|
-
|
|
14726
|
-
|
|
14727
|
-
|
|
14728
|
-
|
|
14729
|
-
|
|
14730
|
-
|
|
14731
|
-
|
|
14732
|
-
|
|
14733
|
-
|
|
14734
|
-
|
|
14735
|
-
|
|
14736
|
-
|
|
14737
|
-
|
|
14738
|
-
|
|
14625
|
+
function isPermissionDenied(message) {
|
|
14626
|
+
return message.includes("Permission denied") || message.includes("EACCES");
|
|
14627
|
+
}
|
|
14628
|
+
function isEmptyRepo(stderr) {
|
|
14629
|
+
return stderr.includes("does not have any commits yet") || stderr.includes("no commits yet") || stderr.includes("ambiguous argument 'HEAD'") || stderr.includes("unknown revision or path");
|
|
14630
|
+
}
|
|
14631
|
+
function classifyError(errMessage) {
|
|
14632
|
+
if (isGitNotInstalled(errMessage)) {
|
|
14633
|
+
return { status: "error", message: "git is not installed or not in PATH" };
|
|
14634
|
+
}
|
|
14635
|
+
if (isNotAGitRepo(errMessage)) {
|
|
14636
|
+
return { status: "not_found" };
|
|
14637
|
+
}
|
|
14638
|
+
if (isPermissionDenied(errMessage)) {
|
|
14639
|
+
return { status: "error", message: `Permission denied: ${errMessage}` };
|
|
14640
|
+
}
|
|
14641
|
+
return { status: "error", message: errMessage.trim() };
|
|
14642
|
+
}
|
|
14643
|
+
async function isGitRepo(workingDir) {
|
|
14644
|
+
const result = await runGit("rev-parse --git-dir", workingDir);
|
|
14645
|
+
if ("error" in result)
|
|
14646
|
+
return false;
|
|
14647
|
+
return result.stdout.trim().length > 0;
|
|
14648
|
+
}
|
|
14649
|
+
async function getBranch(workingDir) {
|
|
14650
|
+
const result = await runGit("rev-parse --abbrev-ref HEAD", workingDir);
|
|
14651
|
+
if ("error" in result) {
|
|
14652
|
+
const errMsg = result.error.message;
|
|
14653
|
+
if (errMsg.includes("unknown revision") || errMsg.includes("No such file or directory") || errMsg.includes("does not have any commits")) {
|
|
14654
|
+
return { status: "available", branch: "HEAD" };
|
|
14739
14655
|
}
|
|
14656
|
+
return classifyError(errMsg);
|
|
14740
14657
|
}
|
|
14741
|
-
|
|
14658
|
+
const branch = result.stdout.trim();
|
|
14659
|
+
if (!branch) {
|
|
14660
|
+
return { status: "error", message: "git rev-parse returned empty output" };
|
|
14661
|
+
}
|
|
14662
|
+
return { status: "available", branch };
|
|
14742
14663
|
}
|
|
14743
|
-
function
|
|
14664
|
+
async function isDirty(workingDir) {
|
|
14665
|
+
const result = await runGit("status --porcelain", workingDir);
|
|
14666
|
+
if ("error" in result)
|
|
14667
|
+
return false;
|
|
14668
|
+
return result.stdout.trim().length > 0;
|
|
14669
|
+
}
|
|
14670
|
+
function parseDiffStatLine(statLine) {
|
|
14671
|
+
const filesMatch = statLine.match(/(\d+)\s+file/);
|
|
14672
|
+
const insertMatch = statLine.match(/(\d+)\s+insertion/);
|
|
14673
|
+
const deleteMatch = statLine.match(/(\d+)\s+deletion/);
|
|
14744
14674
|
return {
|
|
14745
|
-
|
|
14746
|
-
|
|
14747
|
-
|
|
14748
|
-
},
|
|
14749
|
-
query: async () => {
|
|
14750
|
-
throw new Error("Backend not initialized");
|
|
14751
|
-
}
|
|
14752
|
-
},
|
|
14753
|
-
processes: {
|
|
14754
|
-
kill: (pid, signal) => process.kill(pid, signal)
|
|
14755
|
-
},
|
|
14756
|
-
fs: {
|
|
14757
|
-
stat
|
|
14758
|
-
},
|
|
14759
|
-
machine: {
|
|
14760
|
-
clearAgentPid,
|
|
14761
|
-
persistAgentPid,
|
|
14762
|
-
listAgentEntries,
|
|
14763
|
-
persistEventCursor,
|
|
14764
|
-
loadEventCursor
|
|
14765
|
-
},
|
|
14766
|
-
clock: {
|
|
14767
|
-
now: () => Date.now(),
|
|
14768
|
-
delay: (ms) => new Promise((resolve2) => setTimeout(resolve2, ms))
|
|
14769
|
-
},
|
|
14770
|
-
spawning: new HarnessSpawningService({ rateLimiter: new SpawnRateLimiter })
|
|
14675
|
+
filesChanged: filesMatch ? parseInt(filesMatch[1], 10) : 0,
|
|
14676
|
+
insertions: insertMatch ? parseInt(insertMatch[1], 10) : 0,
|
|
14677
|
+
deletions: deleteMatch ? parseInt(deleteMatch[1], 10) : 0
|
|
14771
14678
|
};
|
|
14772
14679
|
}
|
|
14773
|
-
function
|
|
14774
|
-
const
|
|
14775
|
-
if (
|
|
14776
|
-
const
|
|
14777
|
-
|
|
14778
|
-
|
|
14779
|
-
console.error(`
|
|
14780
|
-
\uD83D\uDCA1 You have sessions for other environments:`);
|
|
14781
|
-
for (const url of otherUrls) {
|
|
14782
|
-
console.error(` • ${url}`);
|
|
14783
|
-
}
|
|
14680
|
+
async function getDiffStat(workingDir) {
|
|
14681
|
+
const result = await runGit("diff HEAD --stat", workingDir);
|
|
14682
|
+
if ("error" in result) {
|
|
14683
|
+
const errMsg = result.error.message;
|
|
14684
|
+
if (isEmptyRepo(result.error.message)) {
|
|
14685
|
+
return { status: "no_commits" };
|
|
14784
14686
|
}
|
|
14785
|
-
|
|
14786
|
-
|
|
14787
|
-
|
|
14788
|
-
|
|
14687
|
+
const classified = classifyError(errMsg);
|
|
14688
|
+
if (classified.status === "not_found")
|
|
14689
|
+
return { status: "not_found" };
|
|
14690
|
+
return classified;
|
|
14789
14691
|
}
|
|
14790
|
-
|
|
14692
|
+
const output = result.stdout;
|
|
14693
|
+
const stderr = result.stderr;
|
|
14694
|
+
if (isEmptyRepo(stderr)) {
|
|
14695
|
+
return { status: "no_commits" };
|
|
14696
|
+
}
|
|
14697
|
+
if (!output.trim()) {
|
|
14698
|
+
return {
|
|
14699
|
+
status: "available",
|
|
14700
|
+
diffStat: { filesChanged: 0, insertions: 0, deletions: 0 }
|
|
14701
|
+
};
|
|
14702
|
+
}
|
|
14703
|
+
const lines = output.trim().split(`
|
|
14704
|
+
`);
|
|
14705
|
+
const summaryLine = lines[lines.length - 1] ?? "";
|
|
14706
|
+
const diffStat = parseDiffStatLine(summaryLine);
|
|
14707
|
+
return { status: "available", diffStat };
|
|
14791
14708
|
}
|
|
14792
|
-
async function
|
|
14793
|
-
const
|
|
14794
|
-
if (
|
|
14795
|
-
|
|
14796
|
-
|
|
14797
|
-
|
|
14798
|
-
|
|
14799
|
-
|
|
14709
|
+
async function getFullDiff(workingDir) {
|
|
14710
|
+
const result = await runGit("diff HEAD", workingDir);
|
|
14711
|
+
if ("error" in result) {
|
|
14712
|
+
const errMsg = result.error.message;
|
|
14713
|
+
if (isEmptyRepo(errMsg)) {
|
|
14714
|
+
return { status: "no_commits" };
|
|
14715
|
+
}
|
|
14716
|
+
const classified = classifyError(errMsg);
|
|
14717
|
+
if (classified.status === "not_found")
|
|
14718
|
+
return { status: "not_found" };
|
|
14719
|
+
return classified;
|
|
14720
|
+
}
|
|
14721
|
+
const stderr = result.stderr;
|
|
14722
|
+
if (isEmptyRepo(stderr)) {
|
|
14723
|
+
return { status: "no_commits" };
|
|
14724
|
+
}
|
|
14725
|
+
const raw = result.stdout;
|
|
14726
|
+
const byteLength2 = Buffer.byteLength(raw, "utf8");
|
|
14727
|
+
if (byteLength2 > FULL_DIFF_MAX_BYTES) {
|
|
14728
|
+
const truncated = Buffer.from(raw, "utf8").subarray(0, FULL_DIFF_MAX_BYTES).toString("utf8");
|
|
14729
|
+
return { status: "truncated", content: truncated, truncated: true };
|
|
14800
14730
|
}
|
|
14731
|
+
return { status: "available", content: raw, truncated: false };
|
|
14801
14732
|
}
|
|
14802
|
-
function
|
|
14803
|
-
|
|
14804
|
-
const
|
|
14805
|
-
|
|
14733
|
+
async function getRecentCommits(workingDir, count = 20, skip = 0) {
|
|
14734
|
+
const format = "%H%x00%h%x00%s%x00%an%x00%aI";
|
|
14735
|
+
const skipArg = skip > 0 ? ` --skip=${skip}` : "";
|
|
14736
|
+
const result = await runGit(`log -${count}${skipArg} --format=${format}`, workingDir);
|
|
14737
|
+
if ("error" in result) {
|
|
14738
|
+
return [];
|
|
14739
|
+
}
|
|
14740
|
+
const output = result.stdout.trim();
|
|
14741
|
+
if (!output)
|
|
14742
|
+
return [];
|
|
14743
|
+
const commits = [];
|
|
14744
|
+
for (const line of output.split(`
|
|
14745
|
+
`)) {
|
|
14746
|
+
const trimmed = line.trim();
|
|
14747
|
+
if (!trimmed)
|
|
14748
|
+
continue;
|
|
14749
|
+
const parts = trimmed.split("\x00");
|
|
14750
|
+
if (parts.length !== 5)
|
|
14751
|
+
continue;
|
|
14752
|
+
const [sha, shortSha, message, author, date] = parts;
|
|
14753
|
+
commits.push({ sha, shortSha, message, author, date });
|
|
14754
|
+
}
|
|
14755
|
+
return commits;
|
|
14806
14756
|
}
|
|
14807
|
-
async function
|
|
14808
|
-
const
|
|
14809
|
-
|
|
14810
|
-
|
|
14811
|
-
|
|
14812
|
-
|
|
14813
|
-
|
|
14814
|
-
|
|
14815
|
-
|
|
14816
|
-
|
|
14817
|
-
|
|
14818
|
-
availableModels
|
|
14819
|
-
});
|
|
14820
|
-
} catch (error) {
|
|
14821
|
-
console.warn(`⚠️ Machine registration update failed: ${error.message}`);
|
|
14757
|
+
async function getCommitDetail(workingDir, sha) {
|
|
14758
|
+
const result = await runGit(`show ${sha} --format="" --stat -p`, workingDir);
|
|
14759
|
+
if ("error" in result) {
|
|
14760
|
+
const errMsg = result.error.message;
|
|
14761
|
+
const classified = classifyError(errMsg);
|
|
14762
|
+
if (classified.status === "not_found")
|
|
14763
|
+
return { status: "not_found" };
|
|
14764
|
+
if (isEmptyRepo(errMsg) || errMsg.includes("unknown revision") || errMsg.includes("bad object") || errMsg.includes("does not exist")) {
|
|
14765
|
+
return { status: "not_found" };
|
|
14766
|
+
}
|
|
14767
|
+
return classified;
|
|
14822
14768
|
}
|
|
14823
|
-
|
|
14769
|
+
const raw = result.stdout;
|
|
14770
|
+
const byteLength2 = Buffer.byteLength(raw, "utf8");
|
|
14771
|
+
if (byteLength2 > FULL_DIFF_MAX_BYTES) {
|
|
14772
|
+
const truncated = Buffer.from(raw, "utf8").subarray(0, FULL_DIFF_MAX_BYTES).toString("utf8");
|
|
14773
|
+
return { status: "truncated", content: truncated, truncated: true };
|
|
14774
|
+
}
|
|
14775
|
+
return { status: "available", content: raw, truncated: false };
|
|
14824
14776
|
}
|
|
14825
|
-
async function
|
|
14777
|
+
async function getCommitMetadata(workingDir, sha) {
|
|
14778
|
+
const format = "%s%x00%an%x00%aI";
|
|
14779
|
+
const result = await runGit(`log -1 --format=${format} ${sha}`, workingDir);
|
|
14780
|
+
if ("error" in result)
|
|
14781
|
+
return null;
|
|
14782
|
+
const output = result.stdout.trim();
|
|
14783
|
+
if (!output)
|
|
14784
|
+
return null;
|
|
14785
|
+
const parts = output.split("\x00");
|
|
14786
|
+
if (parts.length !== 3)
|
|
14787
|
+
return null;
|
|
14788
|
+
return { message: parts[0], author: parts[1], date: parts[2] };
|
|
14789
|
+
}
|
|
14790
|
+
async function runCommand(command, cwd) {
|
|
14826
14791
|
try {
|
|
14827
|
-
await
|
|
14828
|
-
|
|
14829
|
-
|
|
14830
|
-
|
|
14792
|
+
const result = await execAsync2(command, {
|
|
14793
|
+
cwd,
|
|
14794
|
+
env: { ...process.env, NO_COLOR: "1" },
|
|
14795
|
+
timeout: 15000
|
|
14831
14796
|
});
|
|
14832
|
-
|
|
14833
|
-
|
|
14834
|
-
|
|
14835
|
-
} else {
|
|
14836
|
-
console.error(`❌ Failed to update daemon status: ${error.message}`);
|
|
14837
|
-
}
|
|
14838
|
-
releaseLock();
|
|
14839
|
-
process.exit(1);
|
|
14797
|
+
return result;
|
|
14798
|
+
} catch (err) {
|
|
14799
|
+
return { error: err };
|
|
14840
14800
|
}
|
|
14841
14801
|
}
|
|
14842
|
-
function
|
|
14843
|
-
|
|
14844
|
-
|
|
14845
|
-
|
|
14846
|
-
|
|
14847
|
-
|
|
14848
|
-
|
|
14849
|
-
|
|
14802
|
+
function parseRepoSlug(remoteUrl) {
|
|
14803
|
+
const trimmed = remoteUrl.trim();
|
|
14804
|
+
const httpsMatch = trimmed.match(/https?:\/\/[^/]+\/([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
14805
|
+
if (httpsMatch && httpsMatch[1] && httpsMatch[2]) {
|
|
14806
|
+
return `${httpsMatch[1]}/${httpsMatch[2]}`;
|
|
14807
|
+
}
|
|
14808
|
+
const sshMatch = trimmed.match(/git@[^:]+:([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
14809
|
+
if (sshMatch && sshMatch[1] && sshMatch[2]) {
|
|
14810
|
+
return `${sshMatch[1]}/${sshMatch[2]}`;
|
|
14811
|
+
}
|
|
14812
|
+
return null;
|
|
14850
14813
|
}
|
|
14851
|
-
async function
|
|
14852
|
-
|
|
14853
|
-
|
|
14814
|
+
async function getOriginRepoSlug(cwd) {
|
|
14815
|
+
const result = await runGit("remote get-url origin", cwd);
|
|
14816
|
+
if ("error" in result)
|
|
14817
|
+
return null;
|
|
14818
|
+
const url = result.stdout.trim();
|
|
14819
|
+
if (!url)
|
|
14820
|
+
return null;
|
|
14821
|
+
return parseRepoSlug(url);
|
|
14822
|
+
}
|
|
14823
|
+
async function getOpenPRsForBranch(cwd, branch) {
|
|
14824
|
+
const repoSlug = await getOriginRepoSlug(cwd);
|
|
14825
|
+
const repoFlag = repoSlug ? ` --repo ${JSON.stringify(repoSlug)}` : "";
|
|
14826
|
+
const result = await runCommand(`gh pr list --head ${JSON.stringify(branch)} --state open --json number,title,url,headRefName,state --limit 5${repoFlag}`, cwd);
|
|
14827
|
+
if ("error" in result) {
|
|
14828
|
+
return [];
|
|
14829
|
+
}
|
|
14830
|
+
const output = result.stdout.trim();
|
|
14831
|
+
if (!output)
|
|
14832
|
+
return [];
|
|
14854
14833
|
try {
|
|
14855
|
-
|
|
14856
|
-
|
|
14857
|
-
|
|
14858
|
-
|
|
14834
|
+
const parsed = JSON.parse(output);
|
|
14835
|
+
if (!Array.isArray(parsed))
|
|
14836
|
+
return [];
|
|
14837
|
+
return parsed.filter((item) => typeof item === "object" && item !== null && typeof item.number === "number" && typeof item.title === "string" && typeof item.url === "string" && typeof item.headRefName === "string" && typeof item.state === "string").map((item) => ({
|
|
14838
|
+
number: item.number,
|
|
14839
|
+
title: item.title,
|
|
14840
|
+
url: item.url,
|
|
14841
|
+
headRefName: item.headRefName,
|
|
14842
|
+
state: item.state
|
|
14843
|
+
}));
|
|
14844
|
+
} catch {
|
|
14845
|
+
return [];
|
|
14859
14846
|
}
|
|
14860
14847
|
}
|
|
14861
|
-
|
|
14862
|
-
|
|
14863
|
-
|
|
14848
|
+
var execAsync2;
|
|
14849
|
+
var init_git_reader = __esm(() => {
|
|
14850
|
+
execAsync2 = promisify2(exec2);
|
|
14851
|
+
});
|
|
14852
|
+
|
|
14853
|
+
// src/commands/machine/daemon-start/git-subscription.ts
|
|
14854
|
+
function startGitRequestSubscription(ctx, wsClient2) {
|
|
14855
|
+
const processedRequestIds = new Map;
|
|
14856
|
+
const DEDUP_TTL_MS = 5 * 60 * 1000;
|
|
14857
|
+
let processing = false;
|
|
14858
|
+
const unsubscribe = wsClient2.onUpdate(api.workspaces.getPendingRequests, {
|
|
14859
|
+
sessionId: ctx.sessionId,
|
|
14860
|
+
machineId: ctx.machineId
|
|
14861
|
+
}, (requests) => {
|
|
14862
|
+
if (!requests || requests.length === 0)
|
|
14863
|
+
return;
|
|
14864
|
+
if (processing)
|
|
14865
|
+
return;
|
|
14866
|
+
processing = true;
|
|
14867
|
+
processRequests(ctx, requests, processedRequestIds, DEDUP_TTL_MS).catch((err) => {
|
|
14868
|
+
console.warn(`[${formatTimestamp()}] ⚠️ Git request processing failed: ${err.message}`);
|
|
14869
|
+
}).finally(() => {
|
|
14870
|
+
processing = false;
|
|
14871
|
+
});
|
|
14872
|
+
}, (err) => {
|
|
14873
|
+
console.warn(`[${formatTimestamp()}] ⚠️ Git request subscription error: ${err.message}`);
|
|
14874
|
+
});
|
|
14875
|
+
console.log(`[${formatTimestamp()}] \uD83D\uDD00 Git request subscription started (reactive)`);
|
|
14876
|
+
return {
|
|
14877
|
+
stop: () => {
|
|
14878
|
+
unsubscribe();
|
|
14879
|
+
console.log(`[${formatTimestamp()}] \uD83D\uDD00 Git request subscription stopped`);
|
|
14880
|
+
}
|
|
14881
|
+
};
|
|
14882
|
+
}
|
|
14883
|
+
function extractDiffStatFromShowOutput(content) {
|
|
14884
|
+
for (const line of content.split(`
|
|
14885
|
+
`)) {
|
|
14886
|
+
if (/\d+\s+file.*changed/.test(line)) {
|
|
14887
|
+
return parseDiffStatLine(line);
|
|
14888
|
+
}
|
|
14864
14889
|
}
|
|
14865
|
-
|
|
14866
|
-
const sessionId = validateAuthentication(convexUrl);
|
|
14867
|
-
const client2 = await getConvexClient();
|
|
14868
|
-
const typedSessionId = sessionId;
|
|
14869
|
-
await validateSession(client2, typedSessionId, convexUrl);
|
|
14870
|
-
const config3 = setupMachine();
|
|
14871
|
-
const { machineId } = config3;
|
|
14872
|
-
initHarnessRegistry();
|
|
14873
|
-
const agentServices = new Map(getAllHarnesses().map((s) => [s.id, s]));
|
|
14874
|
-
const availableModels = await registerCapabilities(client2, typedSessionId, config3, agentServices);
|
|
14875
|
-
await connectDaemon(client2, typedSessionId, machineId, convexUrl);
|
|
14876
|
-
const deps = createDefaultDeps19();
|
|
14877
|
-
deps.backend.mutation = (endpoint, args) => client2.mutation(endpoint, args);
|
|
14878
|
-
deps.backend.query = (endpoint, args) => client2.query(endpoint, args);
|
|
14879
|
-
const events = new DaemonEventBus;
|
|
14880
|
-
const ctx = {
|
|
14881
|
-
client: client2,
|
|
14882
|
-
sessionId: typedSessionId,
|
|
14883
|
-
machineId,
|
|
14884
|
-
config: config3,
|
|
14885
|
-
deps,
|
|
14886
|
-
events,
|
|
14887
|
-
agentServices,
|
|
14888
|
-
activeWorkingDirs: new Set,
|
|
14889
|
-
lastPushedGitState: new Map,
|
|
14890
|
-
pendingStops: new Map
|
|
14891
|
-
};
|
|
14892
|
-
registerEventListeners(ctx);
|
|
14893
|
-
logStartup(ctx, availableModels);
|
|
14894
|
-
await recoverState(ctx);
|
|
14895
|
-
return ctx;
|
|
14896
|
-
}
|
|
14897
|
-
var init_init2 = __esm(() => {
|
|
14898
|
-
init_state_recovery();
|
|
14899
|
-
init_api3();
|
|
14900
|
-
init_storage();
|
|
14901
|
-
init_client2();
|
|
14902
|
-
init_machine();
|
|
14903
|
-
init_remote_agents();
|
|
14904
|
-
init_harness_spawning();
|
|
14905
|
-
init_error_formatting();
|
|
14906
|
-
init_version();
|
|
14907
|
-
init_pid();
|
|
14908
|
-
init_register_listeners();
|
|
14909
|
-
});
|
|
14910
|
-
|
|
14911
|
-
// src/infrastructure/machine/stop-reason.ts
|
|
14912
|
-
function resolveStopReason(code2, signal, wasIntentional) {
|
|
14913
|
-
if (wasIntentional)
|
|
14914
|
-
return "user.stop";
|
|
14915
|
-
if (signal !== null)
|
|
14916
|
-
return "agent_process.signal";
|
|
14917
|
-
if (code2 === 0)
|
|
14918
|
-
return "agent_process.exited_clean";
|
|
14919
|
-
return "agent_process.crashed";
|
|
14890
|
+
return { filesChanged: 0, insertions: 0, deletions: 0 };
|
|
14920
14891
|
}
|
|
14921
|
-
|
|
14922
|
-
|
|
14923
|
-
|
|
14924
|
-
|
|
14925
|
-
|
|
14926
|
-
|
|
14927
|
-
console.log(` Role: ${role}`);
|
|
14928
|
-
console.log(` Harness: ${agentHarness}`);
|
|
14929
|
-
if (reason) {
|
|
14930
|
-
console.log(` Reason: ${reason}`);
|
|
14931
|
-
}
|
|
14932
|
-
if (model) {
|
|
14933
|
-
console.log(` Model: ${model}`);
|
|
14934
|
-
}
|
|
14935
|
-
if (!workingDir) {
|
|
14936
|
-
const msg2 = `No workingDir provided in command payload for ${chatroomId}/${role}`;
|
|
14937
|
-
console.log(` ⚠️ ${msg2}`);
|
|
14938
|
-
return { result: msg2, failed: true };
|
|
14939
|
-
}
|
|
14940
|
-
console.log(` Working dir: ${workingDir}`);
|
|
14941
|
-
try {
|
|
14942
|
-
const dirStat = await ctx.deps.fs.stat(workingDir);
|
|
14943
|
-
if (!dirStat.isDirectory()) {
|
|
14944
|
-
const msg2 = `Working directory is not a directory: ${workingDir}`;
|
|
14945
|
-
console.log(` ⚠️ ${msg2}`);
|
|
14946
|
-
return { result: msg2, failed: true };
|
|
14947
|
-
}
|
|
14948
|
-
} catch {
|
|
14949
|
-
const msg2 = `Working directory does not exist: ${workingDir}`;
|
|
14950
|
-
console.log(` ⚠️ ${msg2}`);
|
|
14951
|
-
return { result: msg2, failed: true };
|
|
14952
|
-
}
|
|
14953
|
-
try {
|
|
14954
|
-
const existingConfigs = await ctx.deps.backend.query(api.machines.getMachineAgentConfigs, {
|
|
14892
|
+
async function processFullDiff(ctx, req) {
|
|
14893
|
+
const result = await getFullDiff(req.workingDir);
|
|
14894
|
+
if (result.status === "available" || result.status === "truncated") {
|
|
14895
|
+
const diffStatResult = await getDiffStat(req.workingDir);
|
|
14896
|
+
const diffStat = diffStatResult.status === "available" ? diffStatResult.diffStat : { filesChanged: 0, insertions: 0, deletions: 0 };
|
|
14897
|
+
await ctx.deps.backend.mutation(api.workspaces.upsertFullDiff, {
|
|
14955
14898
|
sessionId: ctx.sessionId,
|
|
14956
|
-
|
|
14957
|
-
|
|
14958
|
-
|
|
14959
|
-
|
|
14960
|
-
|
|
14961
|
-
const localPid = localEntry?.entry.pid;
|
|
14962
|
-
const pidsToKill = [
|
|
14963
|
-
...new Set([backendPid, localPid].filter((p) => p !== undefined))
|
|
14964
|
-
];
|
|
14965
|
-
const anyService = ctx.agentServices.values().next().value;
|
|
14966
|
-
for (const pid2 of pidsToKill) {
|
|
14967
|
-
const isAlive = anyService ? anyService.isAlive(pid2) : false;
|
|
14968
|
-
if (isAlive) {
|
|
14969
|
-
console.log(` ⚠️ Existing agent detected (PID: ${pid2}) — stopping before respawn`);
|
|
14970
|
-
await onAgentShutdown(ctx, { chatroomId, role, pid: pid2, stopReason: "daemon.respawn" });
|
|
14971
|
-
console.log(` ✅ Existing agent stopped (PID: ${pid2})`);
|
|
14972
|
-
}
|
|
14973
|
-
}
|
|
14974
|
-
} catch (e) {
|
|
14975
|
-
console.log(` ⚠️ Could not check for existing agent (proceeding): ${e.message}`);
|
|
14976
|
-
}
|
|
14977
|
-
const convexUrl = getConvexUrl();
|
|
14978
|
-
const initPromptResult = await ctx.deps.backend.query(api.messages.getInitPrompt, {
|
|
14979
|
-
sessionId: ctx.sessionId,
|
|
14980
|
-
chatroomId,
|
|
14981
|
-
role,
|
|
14982
|
-
convexUrl
|
|
14983
|
-
});
|
|
14984
|
-
if (!initPromptResult?.prompt) {
|
|
14985
|
-
const msg2 = "Failed to fetch init prompt from backend";
|
|
14986
|
-
console.log(` ⚠️ ${msg2}`);
|
|
14987
|
-
return { result: msg2, failed: true };
|
|
14988
|
-
}
|
|
14989
|
-
console.log(` Fetched split init prompt from backend`);
|
|
14990
|
-
const service = ctx.agentServices.get(agentHarness);
|
|
14991
|
-
if (!service) {
|
|
14992
|
-
const msg2 = `Unknown agent harness: ${agentHarness}`;
|
|
14993
|
-
console.log(` ⚠️ ${msg2}`);
|
|
14994
|
-
return { result: msg2, failed: true };
|
|
14995
|
-
}
|
|
14996
|
-
let spawnResult;
|
|
14997
|
-
try {
|
|
14998
|
-
spawnResult = await service.spawn({
|
|
14999
|
-
workingDir,
|
|
15000
|
-
prompt: initPromptResult.initialMessage,
|
|
15001
|
-
systemPrompt: initPromptResult.rolePrompt,
|
|
15002
|
-
model,
|
|
15003
|
-
context: { machineId: ctx.machineId, chatroomId, role }
|
|
14899
|
+
machineId: ctx.machineId,
|
|
14900
|
+
workingDir: req.workingDir,
|
|
14901
|
+
diffContent: result.content,
|
|
14902
|
+
truncated: result.truncated,
|
|
14903
|
+
diffStat
|
|
15004
14904
|
});
|
|
15005
|
-
|
|
15006
|
-
|
|
15007
|
-
|
|
15008
|
-
return { result: msg2, failed: true };
|
|
15009
|
-
}
|
|
15010
|
-
const { pid } = spawnResult;
|
|
15011
|
-
const spawnedAt = Date.now();
|
|
15012
|
-
const msg = `Agent spawned (PID: ${pid})`;
|
|
15013
|
-
console.log(` ✅ ${msg}`);
|
|
15014
|
-
ctx.deps.spawning.recordSpawn(chatroomId);
|
|
15015
|
-
try {
|
|
15016
|
-
await ctx.deps.backend.mutation(api.machines.updateSpawnedAgent, {
|
|
14905
|
+
console.log(`[${formatTimestamp()}] \uD83D\uDCC4 Full diff pushed: ${req.workingDir} (${diffStat.filesChanged} files, ${result.truncated ? "truncated" : "complete"})`);
|
|
14906
|
+
} else {
|
|
14907
|
+
await ctx.deps.backend.mutation(api.workspaces.upsertFullDiff, {
|
|
15017
14908
|
sessionId: ctx.sessionId,
|
|
15018
14909
|
machineId: ctx.machineId,
|
|
15019
|
-
|
|
15020
|
-
|
|
15021
|
-
|
|
15022
|
-
|
|
15023
|
-
reason
|
|
15024
|
-
});
|
|
15025
|
-
console.log(` Updated backend with PID: ${pid}`);
|
|
15026
|
-
ctx.deps.machine.persistAgentPid(ctx.machineId, chatroomId, role, pid, agentHarness);
|
|
15027
|
-
} catch (e) {
|
|
15028
|
-
console.log(` ⚠️ Failed to update PID in backend: ${e.message}`);
|
|
15029
|
-
}
|
|
15030
|
-
ctx.events.emit("agent:started", {
|
|
15031
|
-
chatroomId,
|
|
15032
|
-
role,
|
|
15033
|
-
pid,
|
|
15034
|
-
harness: agentHarness,
|
|
15035
|
-
model
|
|
15036
|
-
});
|
|
15037
|
-
ctx.activeWorkingDirs.add(workingDir);
|
|
15038
|
-
spawnResult.onExit(({ code: code2, signal }) => {
|
|
15039
|
-
ctx.deps.spawning.recordExit(chatroomId);
|
|
15040
|
-
const key = agentKey2(chatroomId, role);
|
|
15041
|
-
const pendingReason = ctx.pendingStops.get(key) ?? null;
|
|
15042
|
-
if (pendingReason) {
|
|
15043
|
-
ctx.pendingStops.delete(key);
|
|
15044
|
-
}
|
|
15045
|
-
const stopReason = pendingReason ?? resolveStopReason(code2, signal, false);
|
|
15046
|
-
ctx.events.emit("agent:exited", {
|
|
15047
|
-
chatroomId,
|
|
15048
|
-
role,
|
|
15049
|
-
pid,
|
|
15050
|
-
code: code2,
|
|
15051
|
-
signal,
|
|
15052
|
-
stopReason,
|
|
15053
|
-
agentHarness
|
|
15054
|
-
});
|
|
15055
|
-
});
|
|
15056
|
-
if (spawnResult.onAgentEnd) {
|
|
15057
|
-
spawnResult.onAgentEnd(() => {
|
|
15058
|
-
const elapsed = Date.now() - spawnedAt;
|
|
15059
|
-
const isHealthyTurn = elapsed >= MIN_HEALTHY_TURN_MS;
|
|
15060
|
-
const stopReason = isHealthyTurn ? "agent_process.turn_end" : "agent_process.turn_end_quick_fail";
|
|
15061
|
-
const key = agentKey2(chatroomId, role);
|
|
15062
|
-
ctx.pendingStops.set(key, stopReason);
|
|
15063
|
-
try {
|
|
15064
|
-
ctx.deps.processes.kill(-pid, "SIGTERM");
|
|
15065
|
-
} catch {}
|
|
14910
|
+
workingDir: req.workingDir,
|
|
14911
|
+
diffContent: "",
|
|
14912
|
+
truncated: false,
|
|
14913
|
+
diffStat: { filesChanged: 0, insertions: 0, deletions: 0 }
|
|
15066
14914
|
});
|
|
14915
|
+
console.log(`[${formatTimestamp()}] \uD83D\uDCC4 Full diff pushed (empty): ${req.workingDir} (${result.status})`);
|
|
15067
14916
|
}
|
|
15068
|
-
let lastReportedTokenAt = 0;
|
|
15069
|
-
spawnResult.onOutput(() => {
|
|
15070
|
-
const now = Date.now();
|
|
15071
|
-
if (now - lastReportedTokenAt >= 30000) {
|
|
15072
|
-
lastReportedTokenAt = now;
|
|
15073
|
-
ctx.deps.backend.mutation(api.participants.updateTokenActivity, {
|
|
15074
|
-
sessionId: ctx.sessionId,
|
|
15075
|
-
chatroomId,
|
|
15076
|
-
role
|
|
15077
|
-
}).catch(() => {});
|
|
15078
|
-
}
|
|
15079
|
-
});
|
|
15080
|
-
return { result: msg, failed: false };
|
|
15081
14917
|
}
|
|
15082
|
-
|
|
15083
|
-
|
|
15084
|
-
|
|
15085
|
-
|
|
15086
|
-
|
|
15087
|
-
|
|
15088
|
-
|
|
15089
|
-
|
|
15090
|
-
|
|
15091
|
-
|
|
15092
|
-
|
|
15093
|
-
|
|
14918
|
+
async function processCommitDetail(ctx, req) {
|
|
14919
|
+
if (!req.sha) {
|
|
14920
|
+
throw new Error("commit_detail request missing sha");
|
|
14921
|
+
}
|
|
14922
|
+
const [result, metadata] = await Promise.all([
|
|
14923
|
+
getCommitDetail(req.workingDir, req.sha),
|
|
14924
|
+
getCommitMetadata(req.workingDir, req.sha)
|
|
14925
|
+
]);
|
|
14926
|
+
if (result.status === "not_found") {
|
|
14927
|
+
await ctx.deps.backend.mutation(api.workspaces.upsertCommitDetail, {
|
|
14928
|
+
sessionId: ctx.sessionId,
|
|
14929
|
+
machineId: ctx.machineId,
|
|
14930
|
+
workingDir: req.workingDir,
|
|
14931
|
+
sha: req.sha,
|
|
14932
|
+
status: "not_found",
|
|
14933
|
+
message: metadata?.message,
|
|
14934
|
+
author: metadata?.author,
|
|
14935
|
+
date: metadata?.date
|
|
14936
|
+
});
|
|
15094
14937
|
return;
|
|
15095
14938
|
}
|
|
15096
|
-
|
|
15097
|
-
|
|
15098
|
-
|
|
15099
|
-
|
|
14939
|
+
if (result.status === "error") {
|
|
14940
|
+
await ctx.deps.backend.mutation(api.workspaces.upsertCommitDetail, {
|
|
14941
|
+
sessionId: ctx.sessionId,
|
|
14942
|
+
machineId: ctx.machineId,
|
|
14943
|
+
workingDir: req.workingDir,
|
|
14944
|
+
sha: req.sha,
|
|
14945
|
+
status: "error",
|
|
14946
|
+
errorMessage: result.message,
|
|
14947
|
+
message: metadata?.message,
|
|
14948
|
+
author: metadata?.author,
|
|
14949
|
+
date: metadata?.date
|
|
14950
|
+
});
|
|
15100
14951
|
return;
|
|
15101
14952
|
}
|
|
15102
|
-
|
|
15103
|
-
await
|
|
15104
|
-
|
|
15105
|
-
|
|
15106
|
-
|
|
15107
|
-
|
|
15108
|
-
|
|
15109
|
-
|
|
14953
|
+
const diffStat = extractDiffStatFromShowOutput(result.content);
|
|
14954
|
+
await ctx.deps.backend.mutation(api.workspaces.upsertCommitDetail, {
|
|
14955
|
+
sessionId: ctx.sessionId,
|
|
14956
|
+
machineId: ctx.machineId,
|
|
14957
|
+
workingDir: req.workingDir,
|
|
14958
|
+
sha: req.sha,
|
|
14959
|
+
status: "available",
|
|
14960
|
+
diffContent: result.content,
|
|
14961
|
+
truncated: result.truncated,
|
|
14962
|
+
message: metadata?.message,
|
|
14963
|
+
author: metadata?.author,
|
|
14964
|
+
date: metadata?.date,
|
|
14965
|
+
diffStat
|
|
15110
14966
|
});
|
|
14967
|
+
console.log(`[${formatTimestamp()}] \uD83D\uDD0D Commit detail pushed: ${req.sha.slice(0, 7)} in ${req.workingDir}`);
|
|
15111
14968
|
}
|
|
15112
|
-
|
|
15113
|
-
|
|
15114
|
-
|
|
15115
|
-
|
|
15116
|
-
|
|
15117
|
-
async function executeStopAgent(ctx, args) {
|
|
15118
|
-
const { chatroomId, role, reason } = args;
|
|
15119
|
-
const stopReason = reason;
|
|
15120
|
-
console.log(` ↪ stop-agent command received`);
|
|
15121
|
-
console.log(` Chatroom: ${chatroomId}`);
|
|
15122
|
-
console.log(` Role: ${role}`);
|
|
15123
|
-
console.log(` Reason: ${reason}`);
|
|
15124
|
-
const configsResult = await ctx.deps.backend.query(api.machines.getMachineAgentConfigs, {
|
|
14969
|
+
async function processMoreCommits(ctx, req) {
|
|
14970
|
+
const offset = req.offset ?? 0;
|
|
14971
|
+
const commits = await getRecentCommits(req.workingDir, COMMITS_PER_PAGE, offset);
|
|
14972
|
+
const hasMoreCommits = commits.length >= COMMITS_PER_PAGE;
|
|
14973
|
+
await ctx.deps.backend.mutation(api.workspaces.appendMoreCommits, {
|
|
15125
14974
|
sessionId: ctx.sessionId,
|
|
15126
|
-
|
|
14975
|
+
machineId: ctx.machineId,
|
|
14976
|
+
workingDir: req.workingDir,
|
|
14977
|
+
commits,
|
|
14978
|
+
hasMoreCommits
|
|
15127
14979
|
});
|
|
15128
|
-
|
|
15129
|
-
|
|
15130
|
-
|
|
15131
|
-
const
|
|
15132
|
-
const
|
|
15133
|
-
|
|
15134
|
-
|
|
15135
|
-
|
|
15136
|
-
|
|
15137
|
-
|
|
15138
|
-
|
|
15139
|
-
let anyKilled = false;
|
|
15140
|
-
let lastError = null;
|
|
15141
|
-
for (const pid of allPids) {
|
|
15142
|
-
console.log(` Stopping agent with PID: ${pid}`);
|
|
15143
|
-
const isAlive = anyService ? anyService.isAlive(pid) : false;
|
|
15144
|
-
if (!isAlive) {
|
|
15145
|
-
console.log(` ⚠️ PID ${pid} not found — process already exited or was never started`);
|
|
15146
|
-
await clearAgentPidEverywhere(ctx, chatroomId, role);
|
|
15147
|
-
console.log(` Cleared stale PID`);
|
|
15148
|
-
try {
|
|
15149
|
-
await ctx.deps.backend.mutation(api.participants.leave, {
|
|
15150
|
-
sessionId: ctx.sessionId,
|
|
15151
|
-
chatroomId,
|
|
15152
|
-
role
|
|
15153
|
-
});
|
|
15154
|
-
console.log(` Removed participant record`);
|
|
15155
|
-
} catch {}
|
|
14980
|
+
console.log(`[${formatTimestamp()}] \uD83D\uDCDC More commits appended: ${req.workingDir} (+${commits.length} commits, offset=${offset})`);
|
|
14981
|
+
}
|
|
14982
|
+
async function processRequests(ctx, requests, processedRequestIds, dedupTtlMs) {
|
|
14983
|
+
const evictBefore = Date.now() - dedupTtlMs;
|
|
14984
|
+
for (const [id, ts] of processedRequestIds) {
|
|
14985
|
+
if (ts < evictBefore)
|
|
14986
|
+
processedRequestIds.delete(id);
|
|
14987
|
+
}
|
|
14988
|
+
for (const req of requests) {
|
|
14989
|
+
const requestId = req._id.toString();
|
|
14990
|
+
if (processedRequestIds.has(requestId))
|
|
15156
14991
|
continue;
|
|
15157
|
-
|
|
14992
|
+
processedRequestIds.set(requestId, Date.now());
|
|
15158
14993
|
try {
|
|
15159
|
-
|
|
15160
|
-
|
|
15161
|
-
|
|
15162
|
-
|
|
15163
|
-
stopReason
|
|
14994
|
+
await ctx.deps.backend.mutation(api.workspaces.updateRequestStatus, {
|
|
14995
|
+
sessionId: ctx.sessionId,
|
|
14996
|
+
requestId: req._id,
|
|
14997
|
+
status: "processing"
|
|
15164
14998
|
});
|
|
15165
|
-
|
|
15166
|
-
|
|
15167
|
-
|
|
15168
|
-
|
|
14999
|
+
switch (req.requestType) {
|
|
15000
|
+
case "full_diff":
|
|
15001
|
+
await processFullDiff(ctx, req);
|
|
15002
|
+
break;
|
|
15003
|
+
case "commit_detail":
|
|
15004
|
+
await processCommitDetail(ctx, req);
|
|
15005
|
+
break;
|
|
15006
|
+
case "more_commits":
|
|
15007
|
+
await processMoreCommits(ctx, req);
|
|
15008
|
+
break;
|
|
15169
15009
|
}
|
|
15170
|
-
|
|
15171
|
-
|
|
15172
|
-
|
|
15010
|
+
await ctx.deps.backend.mutation(api.workspaces.updateRequestStatus, {
|
|
15011
|
+
sessionId: ctx.sessionId,
|
|
15012
|
+
requestId: req._id,
|
|
15013
|
+
status: "done"
|
|
15014
|
+
});
|
|
15015
|
+
} catch (err) {
|
|
15016
|
+
console.warn(`[${formatTimestamp()}] ⚠️ Failed to process ${req.requestType} request: ${err.message}`);
|
|
15017
|
+
await ctx.deps.backend.mutation(api.workspaces.updateRequestStatus, {
|
|
15018
|
+
sessionId: ctx.sessionId,
|
|
15019
|
+
requestId: req._id,
|
|
15020
|
+
status: "error"
|
|
15021
|
+
}).catch(() => {});
|
|
15173
15022
|
}
|
|
15174
15023
|
}
|
|
15175
|
-
if (lastError && !anyKilled) {
|
|
15176
|
-
const msg = `Failed to stop agent: ${lastError.message}`;
|
|
15177
|
-
console.log(` ⚠️ ${msg}`);
|
|
15178
|
-
return { result: msg, failed: true };
|
|
15179
|
-
}
|
|
15180
|
-
if (!anyKilled) {
|
|
15181
|
-
return {
|
|
15182
|
-
result: `All recorded PIDs appear stale (processes not found or belong to different programs)`,
|
|
15183
|
-
failed: true
|
|
15184
|
-
};
|
|
15185
|
-
}
|
|
15186
|
-
const killedCount = allPids.length > 1 ? ` (${allPids.length} PIDs)` : ``;
|
|
15187
|
-
return { result: `Agent stopped${killedCount}`, failed: false };
|
|
15188
15024
|
}
|
|
15189
|
-
var
|
|
15025
|
+
var init_git_subscription = __esm(() => {
|
|
15190
15026
|
init_api3();
|
|
15191
|
-
|
|
15192
|
-
init_shared();
|
|
15027
|
+
init_git_reader();
|
|
15193
15028
|
});
|
|
15194
15029
|
|
|
15195
|
-
// src/
|
|
15196
|
-
|
|
15197
|
-
|
|
15198
|
-
|
|
15030
|
+
// src/commands/machine/daemon-start/git-heartbeat.ts
|
|
15031
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
15032
|
+
async function pushGitState(ctx) {
|
|
15033
|
+
let workspaces;
|
|
15034
|
+
try {
|
|
15035
|
+
workspaces = await ctx.deps.backend.query(api.workspaces.listWorkspacesForMachine, {
|
|
15036
|
+
sessionId: ctx.sessionId,
|
|
15037
|
+
machineId: ctx.machineId
|
|
15038
|
+
});
|
|
15039
|
+
} catch (err) {
|
|
15040
|
+
console.warn(`[${formatTimestamp()}] ⚠️ Failed to query workspaces for git sync: ${err.message}`);
|
|
15199
15041
|
return;
|
|
15200
15042
|
}
|
|
15201
|
-
|
|
15202
|
-
|
|
15203
|
-
|
|
15204
|
-
|
|
15205
|
-
|
|
15206
|
-
|
|
15207
|
-
|
|
15208
|
-
|
|
15209
|
-
});
|
|
15210
|
-
|
|
15211
|
-
// src/commands/machine/daemon-start/handlers/ping.ts
|
|
15212
|
-
function handlePing() {
|
|
15213
|
-
console.log(` ↪ Responding: pong`);
|
|
15214
|
-
return { result: "pong", failed: false };
|
|
15215
|
-
}
|
|
15216
|
-
|
|
15217
|
-
// src/infrastructure/git/types.ts
|
|
15218
|
-
function makeGitStateKey(machineId, workingDir) {
|
|
15219
|
-
return `${machineId}::${workingDir}`;
|
|
15220
|
-
}
|
|
15221
|
-
var FULL_DIFF_MAX_BYTES = 500000, COMMITS_PER_PAGE = 20;
|
|
15222
|
-
|
|
15223
|
-
// src/infrastructure/git/git-reader.ts
|
|
15224
|
-
import { exec as exec2 } from "node:child_process";
|
|
15225
|
-
import { promisify as promisify2 } from "node:util";
|
|
15226
|
-
async function runGit(args, cwd) {
|
|
15227
|
-
try {
|
|
15228
|
-
const result = await execAsync2(`git ${args}`, {
|
|
15229
|
-
cwd,
|
|
15230
|
-
env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_PAGER: "cat", NO_COLOR: "1" },
|
|
15231
|
-
maxBuffer: FULL_DIFF_MAX_BYTES + 64 * 1024
|
|
15232
|
-
});
|
|
15233
|
-
return result;
|
|
15234
|
-
} catch (err) {
|
|
15235
|
-
return { error: err };
|
|
15236
|
-
}
|
|
15237
|
-
}
|
|
15238
|
-
function isGitNotInstalled(message) {
|
|
15239
|
-
return message.includes("command not found") || message.includes("ENOENT") || message.includes("not found") || message.includes("'git' is not recognized");
|
|
15240
|
-
}
|
|
15241
|
-
function isNotAGitRepo(message) {
|
|
15242
|
-
return message.includes("not a git repository") || message.includes("Not a git repository");
|
|
15243
|
-
}
|
|
15244
|
-
function isPermissionDenied(message) {
|
|
15245
|
-
return message.includes("Permission denied") || message.includes("EACCES");
|
|
15246
|
-
}
|
|
15247
|
-
function isEmptyRepo(stderr) {
|
|
15248
|
-
return stderr.includes("does not have any commits yet") || stderr.includes("no commits yet") || stderr.includes("ambiguous argument 'HEAD'") || stderr.includes("unknown revision or path");
|
|
15249
|
-
}
|
|
15250
|
-
function classifyError(errMessage) {
|
|
15251
|
-
if (isGitNotInstalled(errMessage)) {
|
|
15252
|
-
return { status: "error", message: "git is not installed or not in PATH" };
|
|
15253
|
-
}
|
|
15254
|
-
if (isNotAGitRepo(errMessage)) {
|
|
15255
|
-
return { status: "not_found" };
|
|
15256
|
-
}
|
|
15257
|
-
if (isPermissionDenied(errMessage)) {
|
|
15258
|
-
return { status: "error", message: `Permission denied: ${errMessage}` };
|
|
15259
|
-
}
|
|
15260
|
-
return { status: "error", message: errMessage.trim() };
|
|
15261
|
-
}
|
|
15262
|
-
async function isGitRepo(workingDir) {
|
|
15263
|
-
const result = await runGit("rev-parse --git-dir", workingDir);
|
|
15264
|
-
if ("error" in result)
|
|
15265
|
-
return false;
|
|
15266
|
-
return result.stdout.trim().length > 0;
|
|
15267
|
-
}
|
|
15268
|
-
async function getBranch(workingDir) {
|
|
15269
|
-
const result = await runGit("rev-parse --abbrev-ref HEAD", workingDir);
|
|
15270
|
-
if ("error" in result) {
|
|
15271
|
-
const errMsg = result.error.message;
|
|
15272
|
-
if (errMsg.includes("unknown revision") || errMsg.includes("No such file or directory") || errMsg.includes("does not have any commits")) {
|
|
15273
|
-
return { status: "available", branch: "HEAD" };
|
|
15274
|
-
}
|
|
15275
|
-
return classifyError(errMsg);
|
|
15276
|
-
}
|
|
15277
|
-
const branch = result.stdout.trim();
|
|
15278
|
-
if (!branch) {
|
|
15279
|
-
return { status: "error", message: "git rev-parse returned empty output" };
|
|
15280
|
-
}
|
|
15281
|
-
return { status: "available", branch };
|
|
15282
|
-
}
|
|
15283
|
-
async function isDirty(workingDir) {
|
|
15284
|
-
const result = await runGit("status --porcelain", workingDir);
|
|
15285
|
-
if ("error" in result)
|
|
15286
|
-
return false;
|
|
15287
|
-
return result.stdout.trim().length > 0;
|
|
15288
|
-
}
|
|
15289
|
-
function parseDiffStatLine(statLine) {
|
|
15290
|
-
const filesMatch = statLine.match(/(\d+)\s+file/);
|
|
15291
|
-
const insertMatch = statLine.match(/(\d+)\s+insertion/);
|
|
15292
|
-
const deleteMatch = statLine.match(/(\d+)\s+deletion/);
|
|
15293
|
-
return {
|
|
15294
|
-
filesChanged: filesMatch ? parseInt(filesMatch[1], 10) : 0,
|
|
15295
|
-
insertions: insertMatch ? parseInt(insertMatch[1], 10) : 0,
|
|
15296
|
-
deletions: deleteMatch ? parseInt(deleteMatch[1], 10) : 0
|
|
15297
|
-
};
|
|
15298
|
-
}
|
|
15299
|
-
async function getDiffStat(workingDir) {
|
|
15300
|
-
const result = await runGit("diff HEAD --stat", workingDir);
|
|
15301
|
-
if ("error" in result) {
|
|
15302
|
-
const errMsg = result.error.message;
|
|
15303
|
-
if (isEmptyRepo(result.error.message)) {
|
|
15304
|
-
return { status: "no_commits" };
|
|
15305
|
-
}
|
|
15306
|
-
const classified = classifyError(errMsg);
|
|
15307
|
-
if (classified.status === "not_found")
|
|
15308
|
-
return { status: "not_found" };
|
|
15309
|
-
return classified;
|
|
15310
|
-
}
|
|
15311
|
-
const output = result.stdout;
|
|
15312
|
-
const stderr = result.stderr;
|
|
15313
|
-
if (isEmptyRepo(stderr)) {
|
|
15314
|
-
return { status: "no_commits" };
|
|
15315
|
-
}
|
|
15316
|
-
if (!output.trim()) {
|
|
15317
|
-
return {
|
|
15318
|
-
status: "available",
|
|
15319
|
-
diffStat: { filesChanged: 0, insertions: 0, deletions: 0 }
|
|
15320
|
-
};
|
|
15321
|
-
}
|
|
15322
|
-
const lines = output.trim().split(`
|
|
15323
|
-
`);
|
|
15324
|
-
const summaryLine = lines[lines.length - 1] ?? "";
|
|
15325
|
-
const diffStat = parseDiffStatLine(summaryLine);
|
|
15326
|
-
return { status: "available", diffStat };
|
|
15327
|
-
}
|
|
15328
|
-
async function getFullDiff(workingDir) {
|
|
15329
|
-
const result = await runGit("diff HEAD", workingDir);
|
|
15330
|
-
if ("error" in result) {
|
|
15331
|
-
const errMsg = result.error.message;
|
|
15332
|
-
if (isEmptyRepo(errMsg)) {
|
|
15333
|
-
return { status: "no_commits" };
|
|
15334
|
-
}
|
|
15335
|
-
const classified = classifyError(errMsg);
|
|
15336
|
-
if (classified.status === "not_found")
|
|
15337
|
-
return { status: "not_found" };
|
|
15338
|
-
return classified;
|
|
15339
|
-
}
|
|
15340
|
-
const stderr = result.stderr;
|
|
15341
|
-
if (isEmptyRepo(stderr)) {
|
|
15342
|
-
return { status: "no_commits" };
|
|
15343
|
-
}
|
|
15344
|
-
const raw = result.stdout;
|
|
15345
|
-
const byteLength2 = Buffer.byteLength(raw, "utf8");
|
|
15346
|
-
if (byteLength2 > FULL_DIFF_MAX_BYTES) {
|
|
15347
|
-
const truncated = Buffer.from(raw, "utf8").subarray(0, FULL_DIFF_MAX_BYTES).toString("utf8");
|
|
15348
|
-
return { status: "truncated", content: truncated, truncated: true };
|
|
15349
|
-
}
|
|
15350
|
-
return { status: "available", content: raw, truncated: false };
|
|
15351
|
-
}
|
|
15352
|
-
async function getRecentCommits(workingDir, count = 20, skip = 0) {
|
|
15353
|
-
const format = "%H%x00%h%x00%s%x00%an%x00%aI";
|
|
15354
|
-
const skipArg = skip > 0 ? ` --skip=${skip}` : "";
|
|
15355
|
-
const result = await runGit(`log -${count}${skipArg} --format=${format}`, workingDir);
|
|
15356
|
-
if ("error" in result) {
|
|
15357
|
-
return [];
|
|
15358
|
-
}
|
|
15359
|
-
const output = result.stdout.trim();
|
|
15360
|
-
if (!output)
|
|
15361
|
-
return [];
|
|
15362
|
-
const commits = [];
|
|
15363
|
-
for (const line of output.split(`
|
|
15364
|
-
`)) {
|
|
15365
|
-
const trimmed = line.trim();
|
|
15366
|
-
if (!trimmed)
|
|
15367
|
-
continue;
|
|
15368
|
-
const parts = trimmed.split("\x00");
|
|
15369
|
-
if (parts.length !== 5)
|
|
15370
|
-
continue;
|
|
15371
|
-
const [sha, shortSha, message, author, date] = parts;
|
|
15372
|
-
commits.push({ sha, shortSha, message, author, date });
|
|
15373
|
-
}
|
|
15374
|
-
return commits;
|
|
15375
|
-
}
|
|
15376
|
-
async function getCommitDetail(workingDir, sha) {
|
|
15377
|
-
const result = await runGit(`show ${sha} --format="" --stat -p`, workingDir);
|
|
15378
|
-
if ("error" in result) {
|
|
15379
|
-
const errMsg = result.error.message;
|
|
15380
|
-
const classified = classifyError(errMsg);
|
|
15381
|
-
if (classified.status === "not_found")
|
|
15382
|
-
return { status: "not_found" };
|
|
15383
|
-
if (isEmptyRepo(errMsg) || errMsg.includes("unknown revision") || errMsg.includes("bad object") || errMsg.includes("does not exist")) {
|
|
15384
|
-
return { status: "not_found" };
|
|
15385
|
-
}
|
|
15386
|
-
return classified;
|
|
15387
|
-
}
|
|
15388
|
-
const raw = result.stdout;
|
|
15389
|
-
const byteLength2 = Buffer.byteLength(raw, "utf8");
|
|
15390
|
-
if (byteLength2 > FULL_DIFF_MAX_BYTES) {
|
|
15391
|
-
const truncated = Buffer.from(raw, "utf8").subarray(0, FULL_DIFF_MAX_BYTES).toString("utf8");
|
|
15392
|
-
return { status: "truncated", content: truncated, truncated: true };
|
|
15393
|
-
}
|
|
15394
|
-
return { status: "available", content: raw, truncated: false };
|
|
15395
|
-
}
|
|
15396
|
-
async function getCommitMetadata(workingDir, sha) {
|
|
15397
|
-
const format = "%s%x00%an%x00%aI";
|
|
15398
|
-
const result = await runGit(`log -1 --format=${format} ${sha}`, workingDir);
|
|
15399
|
-
if ("error" in result)
|
|
15400
|
-
return null;
|
|
15401
|
-
const output = result.stdout.trim();
|
|
15402
|
-
if (!output)
|
|
15403
|
-
return null;
|
|
15404
|
-
const parts = output.split("\x00");
|
|
15405
|
-
if (parts.length !== 3)
|
|
15406
|
-
return null;
|
|
15407
|
-
return { message: parts[0], author: parts[1], date: parts[2] };
|
|
15408
|
-
}
|
|
15409
|
-
var execAsync2;
|
|
15410
|
-
var init_git_reader = __esm(() => {
|
|
15411
|
-
execAsync2 = promisify2(exec2);
|
|
15412
|
-
});
|
|
15413
|
-
|
|
15414
|
-
// src/commands/machine/daemon-start/git-polling.ts
|
|
15415
|
-
function startGitPollingLoop(ctx) {
|
|
15416
|
-
const timer = setInterval(() => {
|
|
15417
|
-
runPollingTick(ctx).catch((err) => {
|
|
15418
|
-
console.warn(`[${formatTimestamp()}] ⚠️ Git polling tick failed: ${err.message}`);
|
|
15419
|
-
});
|
|
15420
|
-
}, GIT_POLLING_INTERVAL_MS);
|
|
15421
|
-
timer.unref();
|
|
15422
|
-
console.log(`[${formatTimestamp()}] \uD83D\uDD00 Git polling loop started (interval: ${GIT_POLLING_INTERVAL_MS}ms)`);
|
|
15423
|
-
return {
|
|
15424
|
-
stop: () => {
|
|
15425
|
-
clearInterval(timer);
|
|
15426
|
-
console.log(`[${formatTimestamp()}] \uD83D\uDD00 Git polling loop stopped`);
|
|
15427
|
-
}
|
|
15428
|
-
};
|
|
15429
|
-
}
|
|
15430
|
-
function extractDiffStatFromShowOutput(content) {
|
|
15431
|
-
for (const line of content.split(`
|
|
15432
|
-
`)) {
|
|
15433
|
-
if (/\d+\s+file.*changed/.test(line)) {
|
|
15434
|
-
return parseDiffStatLine(line);
|
|
15043
|
+
const uniqueWorkingDirs = new Set(workspaces.map((ws) => ws.workingDir));
|
|
15044
|
+
if (uniqueWorkingDirs.size === 0)
|
|
15045
|
+
return;
|
|
15046
|
+
for (const workingDir of uniqueWorkingDirs) {
|
|
15047
|
+
try {
|
|
15048
|
+
await pushSingleWorkspaceGitState(ctx, workingDir);
|
|
15049
|
+
} catch (err) {
|
|
15050
|
+
console.warn(`[${formatTimestamp()}] ⚠️ Git state push failed for ${workingDir}: ${err.message}`);
|
|
15435
15051
|
}
|
|
15436
15052
|
}
|
|
15437
|
-
return { filesChanged: 0, insertions: 0, deletions: 0 };
|
|
15438
15053
|
}
|
|
15439
|
-
async function
|
|
15440
|
-
const
|
|
15441
|
-
|
|
15442
|
-
|
|
15443
|
-
const
|
|
15444
|
-
|
|
15054
|
+
async function pushSingleWorkspaceGitState(ctx, workingDir) {
|
|
15055
|
+
const stateKey = makeGitStateKey(ctx.machineId, workingDir);
|
|
15056
|
+
const isRepo = await isGitRepo(workingDir);
|
|
15057
|
+
if (!isRepo) {
|
|
15058
|
+
const stateHash2 = "not_found";
|
|
15059
|
+
if (ctx.lastPushedGitState.get(stateKey) === stateHash2)
|
|
15060
|
+
return;
|
|
15061
|
+
await ctx.deps.backend.mutation(api.workspaces.upsertWorkspaceGitState, {
|
|
15445
15062
|
sessionId: ctx.sessionId,
|
|
15446
15063
|
machineId: ctx.machineId,
|
|
15447
|
-
workingDir
|
|
15448
|
-
|
|
15449
|
-
truncated: result.truncated,
|
|
15450
|
-
diffStat
|
|
15064
|
+
workingDir,
|
|
15065
|
+
status: "not_found"
|
|
15451
15066
|
});
|
|
15452
|
-
|
|
15453
|
-
|
|
15454
|
-
|
|
15067
|
+
ctx.lastPushedGitState.set(stateKey, stateHash2);
|
|
15068
|
+
return;
|
|
15069
|
+
}
|
|
15070
|
+
const [branchResult, dirtyResult, diffStatResult, commits] = await Promise.all([
|
|
15071
|
+
getBranch(workingDir),
|
|
15072
|
+
isDirty(workingDir),
|
|
15073
|
+
getDiffStat(workingDir),
|
|
15074
|
+
getRecentCommits(workingDir, COMMITS_PER_PAGE)
|
|
15075
|
+
]);
|
|
15076
|
+
if (branchResult.status === "error") {
|
|
15077
|
+
const stateHash2 = `error:${branchResult.message}`;
|
|
15078
|
+
if (ctx.lastPushedGitState.get(stateKey) === stateHash2)
|
|
15079
|
+
return;
|
|
15080
|
+
await ctx.deps.backend.mutation(api.workspaces.upsertWorkspaceGitState, {
|
|
15455
15081
|
sessionId: ctx.sessionId,
|
|
15456
15082
|
machineId: ctx.machineId,
|
|
15457
|
-
workingDir
|
|
15458
|
-
|
|
15459
|
-
|
|
15460
|
-
diffStat: { filesChanged: 0, insertions: 0, deletions: 0 }
|
|
15083
|
+
workingDir,
|
|
15084
|
+
status: "error",
|
|
15085
|
+
errorMessage: branchResult.message
|
|
15461
15086
|
});
|
|
15462
|
-
|
|
15087
|
+
ctx.lastPushedGitState.set(stateKey, stateHash2);
|
|
15088
|
+
return;
|
|
15089
|
+
}
|
|
15090
|
+
if (branchResult.status === "not_found") {
|
|
15091
|
+
return;
|
|
15092
|
+
}
|
|
15093
|
+
const openPRs = await getOpenPRsForBranch(workingDir, branchResult.branch);
|
|
15094
|
+
const branch = branchResult.branch;
|
|
15095
|
+
const isDirty2 = dirtyResult;
|
|
15096
|
+
const diffStat = diffStatResult.status === "available" ? diffStatResult.diffStat : { filesChanged: 0, insertions: 0, deletions: 0 };
|
|
15097
|
+
const hasMoreCommits = commits.length >= COMMITS_PER_PAGE;
|
|
15098
|
+
const stateHash = createHash3("md5").update(JSON.stringify({ branch, isDirty: isDirty2, diffStat, shas: commits.map((c) => c.sha), prs: openPRs.map((pr) => pr.number) })).digest("hex");
|
|
15099
|
+
if (ctx.lastPushedGitState.get(stateKey) === stateHash) {
|
|
15100
|
+
return;
|
|
15463
15101
|
}
|
|
15102
|
+
await ctx.deps.backend.mutation(api.workspaces.upsertWorkspaceGitState, {
|
|
15103
|
+
sessionId: ctx.sessionId,
|
|
15104
|
+
machineId: ctx.machineId,
|
|
15105
|
+
workingDir,
|
|
15106
|
+
status: "available",
|
|
15107
|
+
branch,
|
|
15108
|
+
isDirty: isDirty2,
|
|
15109
|
+
diffStat,
|
|
15110
|
+
recentCommits: commits,
|
|
15111
|
+
hasMoreCommits,
|
|
15112
|
+
openPullRequests: openPRs
|
|
15113
|
+
});
|
|
15114
|
+
ctx.lastPushedGitState.set(stateKey, stateHash);
|
|
15115
|
+
console.log(`[${formatTimestamp()}] \uD83D\uDD00 Git state pushed: ${workingDir} (${branch}${isDirty2 ? ", dirty" : ", clean"})`);
|
|
15116
|
+
prefetchMissingCommitDetails(ctx, workingDir, commits).catch((err) => {
|
|
15117
|
+
console.warn(`[${formatTimestamp()}] ⚠️ Commit pre-fetch failed for ${workingDir}: ${err.message}`);
|
|
15118
|
+
});
|
|
15464
15119
|
}
|
|
15465
|
-
async function
|
|
15466
|
-
if (
|
|
15467
|
-
|
|
15120
|
+
async function prefetchMissingCommitDetails(ctx, workingDir, commits) {
|
|
15121
|
+
if (commits.length === 0)
|
|
15122
|
+
return;
|
|
15123
|
+
const shas = commits.map((c) => c.sha);
|
|
15124
|
+
const missingShas = await ctx.deps.backend.query(api.workspaces.getMissingCommitShas, {
|
|
15125
|
+
sessionId: ctx.sessionId,
|
|
15126
|
+
machineId: ctx.machineId,
|
|
15127
|
+
workingDir,
|
|
15128
|
+
shas
|
|
15129
|
+
});
|
|
15130
|
+
if (missingShas.length === 0)
|
|
15131
|
+
return;
|
|
15132
|
+
console.log(`[${formatTimestamp()}] \uD83D\uDD0D Pre-fetching ${missingShas.length} commit(s) for ${workingDir}`);
|
|
15133
|
+
for (const sha of missingShas) {
|
|
15134
|
+
try {
|
|
15135
|
+
await prefetchSingleCommit(ctx, workingDir, sha, commits);
|
|
15136
|
+
} catch (err) {
|
|
15137
|
+
console.warn(`[${formatTimestamp()}] ⚠️ Pre-fetch failed for ${sha.slice(0, 7)}: ${err.message}`);
|
|
15138
|
+
}
|
|
15468
15139
|
}
|
|
15469
|
-
|
|
15470
|
-
|
|
15471
|
-
|
|
15472
|
-
|
|
15140
|
+
}
|
|
15141
|
+
async function prefetchSingleCommit(ctx, workingDir, sha, commits) {
|
|
15142
|
+
const metadata = commits.find((c) => c.sha === sha);
|
|
15143
|
+
const result = await getCommitDetail(workingDir, sha);
|
|
15473
15144
|
if (result.status === "not_found") {
|
|
15474
15145
|
await ctx.deps.backend.mutation(api.workspaces.upsertCommitDetail, {
|
|
15475
15146
|
sessionId: ctx.sessionId,
|
|
15476
15147
|
machineId: ctx.machineId,
|
|
15477
|
-
workingDir
|
|
15478
|
-
sha
|
|
15148
|
+
workingDir,
|
|
15149
|
+
sha,
|
|
15479
15150
|
status: "not_found",
|
|
15480
15151
|
message: metadata?.message,
|
|
15481
15152
|
author: metadata?.author,
|
|
@@ -15487,8 +15158,8 @@ async function processCommitDetail(ctx, req) {
|
|
|
15487
15158
|
await ctx.deps.backend.mutation(api.workspaces.upsertCommitDetail, {
|
|
15488
15159
|
sessionId: ctx.sessionId,
|
|
15489
15160
|
machineId: ctx.machineId,
|
|
15490
|
-
workingDir
|
|
15491
|
-
sha
|
|
15161
|
+
workingDir,
|
|
15162
|
+
sha,
|
|
15492
15163
|
status: "error",
|
|
15493
15164
|
errorMessage: result.message,
|
|
15494
15165
|
message: metadata?.message,
|
|
@@ -15501,8 +15172,8 @@ async function processCommitDetail(ctx, req) {
|
|
|
15501
15172
|
await ctx.deps.backend.mutation(api.workspaces.upsertCommitDetail, {
|
|
15502
15173
|
sessionId: ctx.sessionId,
|
|
15503
15174
|
machineId: ctx.machineId,
|
|
15504
|
-
workingDir
|
|
15505
|
-
sha
|
|
15175
|
+
workingDir,
|
|
15176
|
+
sha,
|
|
15506
15177
|
status: "available",
|
|
15507
15178
|
diffContent: result.content,
|
|
15508
15179
|
truncated: result.truncated,
|
|
@@ -15511,371 +15182,876 @@ async function processCommitDetail(ctx, req) {
|
|
|
15511
15182
|
date: metadata?.date,
|
|
15512
15183
|
diffStat
|
|
15513
15184
|
});
|
|
15514
|
-
console.log(`[${formatTimestamp()}]
|
|
15185
|
+
console.log(`[${formatTimestamp()}] ✅ Pre-fetched: ${sha.slice(0, 7)} in ${workingDir}`);
|
|
15515
15186
|
}
|
|
15516
|
-
|
|
15517
|
-
|
|
15518
|
-
|
|
15519
|
-
|
|
15520
|
-
|
|
15521
|
-
|
|
15522
|
-
|
|
15523
|
-
|
|
15524
|
-
|
|
15525
|
-
|
|
15526
|
-
});
|
|
15527
|
-
console.log(`[${formatTimestamp()}] \uD83D\uDCDC More commits appended: ${req.workingDir} (+${commits.length} commits, offset=${offset})`);
|
|
15187
|
+
var init_git_heartbeat = __esm(() => {
|
|
15188
|
+
init_git_subscription();
|
|
15189
|
+
init_api3();
|
|
15190
|
+
init_git_reader();
|
|
15191
|
+
});
|
|
15192
|
+
|
|
15193
|
+
// src/commands/machine/daemon-start/handlers/ping.ts
|
|
15194
|
+
function handlePing() {
|
|
15195
|
+
console.log(` ↪ Responding: pong`);
|
|
15196
|
+
return { result: "pong", failed: false };
|
|
15528
15197
|
}
|
|
15529
|
-
|
|
15530
|
-
|
|
15531
|
-
|
|
15532
|
-
|
|
15533
|
-
|
|
15534
|
-
if (
|
|
15198
|
+
|
|
15199
|
+
// src/commands/machine/daemon-start/handlers/state-recovery.ts
|
|
15200
|
+
async function recoverAgentState(ctx) {
|
|
15201
|
+
await ctx.deps.agentProcessManager.recover();
|
|
15202
|
+
const activeSlots = ctx.deps.agentProcessManager.listActive();
|
|
15203
|
+
if (activeSlots.length === 0) {
|
|
15204
|
+
console.log(` No active agents after recovery`);
|
|
15535
15205
|
return;
|
|
15536
|
-
|
|
15206
|
+
}
|
|
15207
|
+
const chatroomIds = new Set(activeSlots.map((s) => s.chatroomId));
|
|
15208
|
+
let registeredCount = 0;
|
|
15209
|
+
for (const chatroomId of chatroomIds) {
|
|
15537
15210
|
try {
|
|
15538
|
-
await ctx.deps.backend.
|
|
15211
|
+
const configsResult = await ctx.deps.backend.query(api.machines.getMachineAgentConfigs, {
|
|
15539
15212
|
sessionId: ctx.sessionId,
|
|
15540
|
-
|
|
15541
|
-
status: "processing"
|
|
15213
|
+
chatroomId
|
|
15542
15214
|
});
|
|
15543
|
-
|
|
15544
|
-
|
|
15545
|
-
|
|
15546
|
-
|
|
15547
|
-
|
|
15548
|
-
|
|
15549
|
-
|
|
15550
|
-
|
|
15551
|
-
|
|
15552
|
-
|
|
15215
|
+
for (const config3 of configsResult.configs) {
|
|
15216
|
+
if (config3.machineId === ctx.machineId && config3.workingDir) {
|
|
15217
|
+
registeredCount++;
|
|
15218
|
+
ctx.deps.backend.mutation(api.workspaces.registerWorkspace, {
|
|
15219
|
+
sessionId: ctx.sessionId,
|
|
15220
|
+
chatroomId,
|
|
15221
|
+
machineId: ctx.machineId,
|
|
15222
|
+
workingDir: config3.workingDir,
|
|
15223
|
+
hostname: ctx.config?.hostname ?? "unknown",
|
|
15224
|
+
registeredBy: config3.role
|
|
15225
|
+
}).catch((err) => {
|
|
15226
|
+
console.warn(`[daemon] ⚠️ Failed to register workspace on recovery: ${err.message}`);
|
|
15227
|
+
});
|
|
15228
|
+
}
|
|
15553
15229
|
}
|
|
15554
|
-
|
|
15555
|
-
|
|
15556
|
-
|
|
15557
|
-
|
|
15230
|
+
} catch {}
|
|
15231
|
+
}
|
|
15232
|
+
if (registeredCount > 0) {
|
|
15233
|
+
console.log(` \uD83D\uDD00 Registered ${registeredCount} workspace(s) on recovery`);
|
|
15234
|
+
}
|
|
15235
|
+
}
|
|
15236
|
+
var init_state_recovery = __esm(() => {
|
|
15237
|
+
init_api3();
|
|
15238
|
+
});
|
|
15239
|
+
|
|
15240
|
+
// src/events/daemon/event-bus.ts
|
|
15241
|
+
class DaemonEventBus {
|
|
15242
|
+
listeners = new Map;
|
|
15243
|
+
on(event, listener) {
|
|
15244
|
+
if (!this.listeners.has(event)) {
|
|
15245
|
+
this.listeners.set(event, new Set);
|
|
15246
|
+
}
|
|
15247
|
+
this.listeners.get(event).add(listener);
|
|
15248
|
+
return () => {
|
|
15249
|
+
this.listeners.get(event)?.delete(listener);
|
|
15250
|
+
};
|
|
15251
|
+
}
|
|
15252
|
+
emit(event, payload) {
|
|
15253
|
+
const set = this.listeners.get(event);
|
|
15254
|
+
if (!set)
|
|
15255
|
+
return;
|
|
15256
|
+
for (const listener of set) {
|
|
15257
|
+
try {
|
|
15258
|
+
listener(payload);
|
|
15259
|
+
} catch (err) {
|
|
15260
|
+
console.warn(`[EventBus] Listener error on "${event}": ${err.message}`);
|
|
15261
|
+
}
|
|
15262
|
+
}
|
|
15263
|
+
}
|
|
15264
|
+
removeAllListeners() {
|
|
15265
|
+
this.listeners.clear();
|
|
15266
|
+
}
|
|
15267
|
+
}
|
|
15268
|
+
|
|
15269
|
+
// src/events/daemon/agent/on-agent-exited.ts
|
|
15270
|
+
function onAgentExited(ctx, payload) {
|
|
15271
|
+
ctx.deps.agentProcessManager.handleExit({
|
|
15272
|
+
chatroomId: payload.chatroomId,
|
|
15273
|
+
role: payload.role,
|
|
15274
|
+
pid: payload.pid,
|
|
15275
|
+
code: payload.code,
|
|
15276
|
+
signal: payload.signal
|
|
15277
|
+
});
|
|
15278
|
+
}
|
|
15279
|
+
|
|
15280
|
+
// src/events/daemon/agent/on-agent-started.ts
|
|
15281
|
+
function onAgentStarted(ctx, payload) {
|
|
15282
|
+
const ts = formatTimestamp();
|
|
15283
|
+
console.log(`[${ts}] \uD83D\uDFE2 Agent started: ${payload.role} (PID: ${payload.pid}, harness: ${payload.harness})`);
|
|
15284
|
+
}
|
|
15285
|
+
var init_on_agent_started = () => {};
|
|
15286
|
+
|
|
15287
|
+
// src/events/daemon/agent/on-agent-stopped.ts
|
|
15288
|
+
function onAgentStopped(ctx, payload) {
|
|
15289
|
+
const ts = formatTimestamp();
|
|
15290
|
+
console.log(`[${ts}] \uD83D\uDD34 Agent stopped: ${payload.role} (PID: ${payload.pid})`);
|
|
15291
|
+
}
|
|
15292
|
+
var init_on_agent_stopped = () => {};
|
|
15293
|
+
|
|
15294
|
+
// src/events/daemon/register-listeners.ts
|
|
15295
|
+
function registerEventListeners(ctx) {
|
|
15296
|
+
const unsubs = [];
|
|
15297
|
+
unsubs.push(ctx.events.on("agent:exited", (payload) => onAgentExited(ctx, payload)));
|
|
15298
|
+
unsubs.push(ctx.events.on("agent:started", (payload) => onAgentStarted(ctx, payload)));
|
|
15299
|
+
unsubs.push(ctx.events.on("agent:stopped", (payload) => onAgentStopped(ctx, payload)));
|
|
15300
|
+
return () => {
|
|
15301
|
+
for (const unsub of unsubs) {
|
|
15302
|
+
unsub();
|
|
15303
|
+
}
|
|
15304
|
+
};
|
|
15305
|
+
}
|
|
15306
|
+
var init_register_listeners = __esm(() => {
|
|
15307
|
+
init_on_agent_started();
|
|
15308
|
+
init_on_agent_stopped();
|
|
15309
|
+
});
|
|
15310
|
+
|
|
15311
|
+
// src/infrastructure/services/harness-spawning/rate-limiter.ts
|
|
15312
|
+
class SpawnRateLimiter {
|
|
15313
|
+
config;
|
|
15314
|
+
buckets = new Map;
|
|
15315
|
+
constructor(config3 = {}) {
|
|
15316
|
+
this.config = { ...DEFAULT_CONFIG, ...config3 };
|
|
15317
|
+
}
|
|
15318
|
+
tryConsume(chatroomId, reason) {
|
|
15319
|
+
if (reason.startsWith("user.")) {
|
|
15320
|
+
return { allowed: true };
|
|
15321
|
+
}
|
|
15322
|
+
const bucket = this._getOrCreateBucket(chatroomId);
|
|
15323
|
+
this._refill(bucket);
|
|
15324
|
+
if (bucket.tokens < 1) {
|
|
15325
|
+
const elapsed = Date.now() - bucket.lastRefillAt;
|
|
15326
|
+
const retryAfterMs = this.config.refillRateMs - elapsed;
|
|
15327
|
+
console.warn(`⚠️ [RateLimiter] Agent spawn rate-limited for chatroom ${chatroomId} (reason: ${reason}). Retry after ${retryAfterMs}ms`);
|
|
15328
|
+
return { allowed: false, retryAfterMs };
|
|
15329
|
+
}
|
|
15330
|
+
bucket.tokens -= 1;
|
|
15331
|
+
const remaining = Math.floor(bucket.tokens);
|
|
15332
|
+
if (remaining <= LOW_TOKEN_THRESHOLD) {
|
|
15333
|
+
console.warn(`⚠️ [RateLimiter] Agent spawn tokens running low for chatroom ${chatroomId} (${remaining}/${this.config.maxTokens} remaining)`);
|
|
15334
|
+
}
|
|
15335
|
+
return { allowed: true };
|
|
15336
|
+
}
|
|
15337
|
+
getStatus(chatroomId) {
|
|
15338
|
+
const bucket = this._getOrCreateBucket(chatroomId);
|
|
15339
|
+
this._refill(bucket);
|
|
15340
|
+
return {
|
|
15341
|
+
remaining: Math.floor(bucket.tokens),
|
|
15342
|
+
total: this.config.maxTokens
|
|
15343
|
+
};
|
|
15344
|
+
}
|
|
15345
|
+
_getOrCreateBucket(chatroomId) {
|
|
15346
|
+
if (!this.buckets.has(chatroomId)) {
|
|
15347
|
+
this.buckets.set(chatroomId, {
|
|
15348
|
+
tokens: this.config.initialTokens,
|
|
15349
|
+
lastRefillAt: Date.now()
|
|
15558
15350
|
});
|
|
15559
|
-
} catch (err) {
|
|
15560
|
-
console.warn(`[${formatTimestamp()}] ⚠️ Failed to process ${req.requestType} request: ${err.message}`);
|
|
15561
|
-
await ctx.deps.backend.mutation(api.workspaces.updateRequestStatus, {
|
|
15562
|
-
sessionId: ctx.sessionId,
|
|
15563
|
-
requestId: req._id,
|
|
15564
|
-
status: "error"
|
|
15565
|
-
}).catch(() => {});
|
|
15566
15351
|
}
|
|
15352
|
+
return this.buckets.get(chatroomId);
|
|
15353
|
+
}
|
|
15354
|
+
_refill(bucket) {
|
|
15355
|
+
const now = Date.now();
|
|
15356
|
+
const elapsed = now - bucket.lastRefillAt;
|
|
15357
|
+
if (elapsed >= this.config.refillRateMs) {
|
|
15358
|
+
const tokensToAdd = Math.floor(elapsed / this.config.refillRateMs);
|
|
15359
|
+
bucket.tokens = Math.min(this.config.maxTokens, bucket.tokens + tokensToAdd);
|
|
15360
|
+
bucket.lastRefillAt += tokensToAdd * this.config.refillRateMs;
|
|
15361
|
+
}
|
|
15362
|
+
}
|
|
15363
|
+
}
|
|
15364
|
+
var DEFAULT_CONFIG, LOW_TOKEN_THRESHOLD = 1;
|
|
15365
|
+
var init_rate_limiter = __esm(() => {
|
|
15366
|
+
DEFAULT_CONFIG = {
|
|
15367
|
+
maxTokens: 5,
|
|
15368
|
+
refillRateMs: 60000,
|
|
15369
|
+
initialTokens: 5
|
|
15370
|
+
};
|
|
15371
|
+
});
|
|
15372
|
+
|
|
15373
|
+
// src/infrastructure/services/harness-spawning/harness-spawning-service.ts
|
|
15374
|
+
class HarnessSpawningService {
|
|
15375
|
+
rateLimiter;
|
|
15376
|
+
concurrentAgents = new Map;
|
|
15377
|
+
constructor({ rateLimiter }) {
|
|
15378
|
+
this.rateLimiter = rateLimiter;
|
|
15379
|
+
}
|
|
15380
|
+
shouldAllowSpawn(chatroomId, reason) {
|
|
15381
|
+
const current = this.concurrentAgents.get(chatroomId) ?? 0;
|
|
15382
|
+
if (current >= MAX_CONCURRENT_AGENTS_PER_CHATROOM) {
|
|
15383
|
+
console.warn(`⚠️ [HarnessSpawningService] Concurrent agent limit reached for chatroom ${chatroomId} ` + `(${current}/${MAX_CONCURRENT_AGENTS_PER_CHATROOM} active agents). Spawn rejected.`);
|
|
15384
|
+
return { allowed: false };
|
|
15385
|
+
}
|
|
15386
|
+
const result = this.rateLimiter.tryConsume(chatroomId, reason);
|
|
15387
|
+
if (!result.allowed) {
|
|
15388
|
+
console.warn(`⚠️ [HarnessSpawningService] Spawn blocked by rate limiter for chatroom ${chatroomId} ` + `(reason: ${reason}).`);
|
|
15389
|
+
}
|
|
15390
|
+
return result;
|
|
15391
|
+
}
|
|
15392
|
+
recordSpawn(chatroomId) {
|
|
15393
|
+
const current = this.concurrentAgents.get(chatroomId) ?? 0;
|
|
15394
|
+
this.concurrentAgents.set(chatroomId, current + 1);
|
|
15395
|
+
}
|
|
15396
|
+
recordExit(chatroomId) {
|
|
15397
|
+
const current = this.concurrentAgents.get(chatroomId) ?? 0;
|
|
15398
|
+
const next = Math.max(0, current - 1);
|
|
15399
|
+
this.concurrentAgents.set(chatroomId, next);
|
|
15400
|
+
}
|
|
15401
|
+
getConcurrentCount(chatroomId) {
|
|
15402
|
+
return this.concurrentAgents.get(chatroomId) ?? 0;
|
|
15567
15403
|
}
|
|
15568
15404
|
}
|
|
15569
|
-
var
|
|
15570
|
-
|
|
15571
|
-
|
|
15572
|
-
|
|
15405
|
+
var MAX_CONCURRENT_AGENTS_PER_CHATROOM = 10;
|
|
15406
|
+
|
|
15407
|
+
// src/infrastructure/services/harness-spawning/index.ts
|
|
15408
|
+
var init_harness_spawning = __esm(() => {
|
|
15409
|
+
init_rate_limiter();
|
|
15573
15410
|
});
|
|
15574
15411
|
|
|
15575
|
-
// src/
|
|
15576
|
-
|
|
15577
|
-
|
|
15578
|
-
|
|
15579
|
-
|
|
15580
|
-
|
|
15581
|
-
|
|
15582
|
-
|
|
15583
|
-
|
|
15584
|
-
|
|
15585
|
-
|
|
15586
|
-
|
|
15412
|
+
// src/infrastructure/machine/crash-loop-tracker.ts
|
|
15413
|
+
class CrashLoopTracker {
|
|
15414
|
+
history = new Map;
|
|
15415
|
+
record(chatroomId, role, now = Date.now()) {
|
|
15416
|
+
const key = `${chatroomId}:${role.toLowerCase()}`;
|
|
15417
|
+
const windowStart = now - CRASH_LOOP_WINDOW_MS;
|
|
15418
|
+
const raw = this.history.get(key) ?? [];
|
|
15419
|
+
const recent = raw.filter((ts) => ts >= windowStart);
|
|
15420
|
+
recent.push(now);
|
|
15421
|
+
this.history.set(key, recent);
|
|
15422
|
+
const restartCount = recent.length;
|
|
15423
|
+
const allowed = restartCount <= CRASH_LOOP_MAX_RESTARTS;
|
|
15424
|
+
return { allowed, restartCount, windowMs: CRASH_LOOP_WINDOW_MS };
|
|
15425
|
+
}
|
|
15426
|
+
clear(chatroomId, role) {
|
|
15427
|
+
const key = `${chatroomId}:${role.toLowerCase()}`;
|
|
15428
|
+
this.history.delete(key);
|
|
15429
|
+
}
|
|
15430
|
+
getCount(chatroomId, role, now = Date.now()) {
|
|
15431
|
+
const key = `${chatroomId}:${role.toLowerCase()}`;
|
|
15432
|
+
const windowStart = now - CRASH_LOOP_WINDOW_MS;
|
|
15433
|
+
const raw = this.history.get(key) ?? [];
|
|
15434
|
+
return raw.filter((ts) => ts >= windowStart).length;
|
|
15435
|
+
}
|
|
15436
|
+
}
|
|
15437
|
+
var CRASH_LOOP_MAX_RESTARTS = 3, CRASH_LOOP_WINDOW_MS;
|
|
15438
|
+
var init_crash_loop_tracker = __esm(() => {
|
|
15439
|
+
CRASH_LOOP_WINDOW_MS = 5 * 60 * 1000;
|
|
15440
|
+
});
|
|
15441
|
+
|
|
15442
|
+
// src/infrastructure/machine/stop-reason.ts
|
|
15443
|
+
function resolveStopReason(code2, signal, wasIntentional) {
|
|
15444
|
+
if (wasIntentional)
|
|
15445
|
+
return "user.stop";
|
|
15446
|
+
if (signal !== null)
|
|
15447
|
+
return "agent_process.signal";
|
|
15448
|
+
if (code2 === 0)
|
|
15449
|
+
return "agent_process.exited_clean";
|
|
15450
|
+
return "agent_process.crashed";
|
|
15587
15451
|
}
|
|
15588
|
-
|
|
15589
|
-
|
|
15590
|
-
|
|
15591
|
-
|
|
15592
|
-
|
|
15593
|
-
|
|
15452
|
+
|
|
15453
|
+
// src/infrastructure/services/agent-process-manager/agent-process-manager.ts
|
|
15454
|
+
function agentKey2(chatroomId, role) {
|
|
15455
|
+
return `${chatroomId}:${role.toLowerCase()}`;
|
|
15456
|
+
}
|
|
15457
|
+
|
|
15458
|
+
class AgentProcessManager {
|
|
15459
|
+
deps;
|
|
15460
|
+
slots = new Map;
|
|
15461
|
+
pendingStopReasons = new Map;
|
|
15462
|
+
constructor(deps) {
|
|
15463
|
+
this.deps = deps;
|
|
15464
|
+
}
|
|
15465
|
+
async ensureRunning(opts) {
|
|
15466
|
+
const key = agentKey2(opts.chatroomId, opts.role);
|
|
15467
|
+
const slot = this.getOrCreateSlot(key);
|
|
15468
|
+
if (slot.state === "running") {
|
|
15469
|
+
return { success: true, pid: slot.pid };
|
|
15470
|
+
}
|
|
15471
|
+
if (slot.state === "spawning" && slot.pendingOperation) {
|
|
15472
|
+
return slot.pendingOperation;
|
|
15473
|
+
}
|
|
15474
|
+
if (slot.state === "stopping" && slot.pendingOperation) {
|
|
15475
|
+
await slot.pendingOperation;
|
|
15476
|
+
}
|
|
15477
|
+
const operation = this.doEnsureRunning(key, slot, opts);
|
|
15478
|
+
slot.pendingOperation = operation;
|
|
15479
|
+
return operation;
|
|
15480
|
+
}
|
|
15481
|
+
async stop(opts) {
|
|
15482
|
+
const key = agentKey2(opts.chatroomId, opts.role);
|
|
15483
|
+
const slot = this.slots.get(key);
|
|
15484
|
+
if (!slot || slot.state === "idle") {
|
|
15485
|
+
return { success: true };
|
|
15486
|
+
}
|
|
15487
|
+
if (slot.state === "stopping" && slot.pendingOperation) {
|
|
15488
|
+
await slot.pendingOperation;
|
|
15489
|
+
return { success: true };
|
|
15490
|
+
}
|
|
15491
|
+
const pid = slot.pid;
|
|
15492
|
+
if (!pid) {
|
|
15493
|
+
slot.state = "idle";
|
|
15494
|
+
slot.pendingOperation = undefined;
|
|
15495
|
+
return { success: true };
|
|
15496
|
+
}
|
|
15497
|
+
this.pendingStopReasons.set(key, opts.reason);
|
|
15498
|
+
slot.state = "stopping";
|
|
15499
|
+
const operation = this.doStop(key, slot, pid, opts);
|
|
15500
|
+
slot.pendingOperation = operation;
|
|
15501
|
+
await operation;
|
|
15502
|
+
return { success: true };
|
|
15503
|
+
}
|
|
15504
|
+
handleExit(opts) {
|
|
15505
|
+
const key = agentKey2(opts.chatroomId, opts.role);
|
|
15506
|
+
const slot = this.slots.get(key);
|
|
15507
|
+
if (!slot || slot.pid !== opts.pid) {
|
|
15594
15508
|
return;
|
|
15595
|
-
|
|
15596
|
-
|
|
15597
|
-
|
|
15598
|
-
|
|
15599
|
-
|
|
15509
|
+
}
|
|
15510
|
+
if (slot.state === "stopping") {
|
|
15511
|
+
return;
|
|
15512
|
+
}
|
|
15513
|
+
const intentionalReason = this.pendingStopReasons.get(key);
|
|
15514
|
+
this.pendingStopReasons.delete(key);
|
|
15515
|
+
const stopReason = intentionalReason ?? resolveStopReason(opts.code, opts.signal, false);
|
|
15516
|
+
this.deps.spawning.recordExit(opts.chatroomId);
|
|
15517
|
+
const harness = slot.harness;
|
|
15518
|
+
const model = slot.model;
|
|
15519
|
+
const workingDir = slot.workingDir;
|
|
15520
|
+
slot.state = "idle";
|
|
15521
|
+
slot.pid = undefined;
|
|
15522
|
+
slot.startedAt = undefined;
|
|
15523
|
+
slot.pendingOperation = undefined;
|
|
15524
|
+
this.deps.backend.mutation(api.machines.recordAgentExited, {
|
|
15525
|
+
sessionId: this.deps.sessionId,
|
|
15526
|
+
machineId: this.deps.machineId,
|
|
15527
|
+
chatroomId: opts.chatroomId,
|
|
15528
|
+
role: opts.role,
|
|
15529
|
+
pid: opts.pid,
|
|
15530
|
+
stopReason,
|
|
15531
|
+
stopSignal: stopReason === "agent_process.signal" ? opts.signal ?? undefined : undefined,
|
|
15532
|
+
exitCode: opts.code ?? undefined,
|
|
15533
|
+
signal: opts.signal ?? undefined,
|
|
15534
|
+
agentHarness: harness
|
|
15535
|
+
}).catch((err) => {
|
|
15536
|
+
console.log(` ⚠️ Failed to record agent exit event: ${err.message}`);
|
|
15600
15537
|
});
|
|
15601
|
-
|
|
15602
|
-
|
|
15603
|
-
|
|
15604
|
-
|
|
15605
|
-
|
|
15606
|
-
|
|
15607
|
-
|
|
15608
|
-
|
|
15609
|
-
]);
|
|
15610
|
-
if (branchResult.status === "error") {
|
|
15611
|
-
const stateHash2 = `error:${branchResult.message}`;
|
|
15612
|
-
if (ctx.lastPushedGitState.get(stateKey) === stateHash2)
|
|
15538
|
+
this.deps.persistence.clearAgentPid(this.deps.machineId, opts.chatroomId, opts.role);
|
|
15539
|
+
for (const service of this.deps.agentServices.values()) {
|
|
15540
|
+
service.untrack(opts.pid);
|
|
15541
|
+
}
|
|
15542
|
+
const isIntentionalStop = stopReason === "user.stop" || stopReason === "platform.team_switch";
|
|
15543
|
+
const isDaemonRespawn = stopReason === "daemon.respawn";
|
|
15544
|
+
if (isIntentionalStop) {
|
|
15545
|
+
this.deps.crashLoop.clear(opts.chatroomId, opts.role);
|
|
15613
15546
|
return;
|
|
15614
|
-
|
|
15615
|
-
|
|
15616
|
-
|
|
15547
|
+
}
|
|
15548
|
+
if (isDaemonRespawn) {
|
|
15549
|
+
return;
|
|
15550
|
+
}
|
|
15551
|
+
if (!harness || !workingDir) {
|
|
15552
|
+
console.log(`[AgentProcessManager] ⚠️ Cannot restart — missing harness or workingDir ` + `(role: ${opts.role}, harness: ${harness ?? "none"}, workingDir: ${workingDir ?? "none"})`);
|
|
15553
|
+
return;
|
|
15554
|
+
}
|
|
15555
|
+
this.ensureRunning({
|
|
15556
|
+
chatroomId: opts.chatroomId,
|
|
15557
|
+
role: opts.role,
|
|
15558
|
+
agentHarness: harness,
|
|
15559
|
+
model,
|
|
15617
15560
|
workingDir,
|
|
15618
|
-
|
|
15619
|
-
|
|
15561
|
+
reason: "platform.crash_recovery"
|
|
15562
|
+
}).catch((err) => {
|
|
15563
|
+
console.log(` ⚠️ Failed to restart agent: ${err.message}`);
|
|
15564
|
+
this.deps.backend.mutation(api.machines.emitAgentStartFailed, {
|
|
15565
|
+
sessionId: this.deps.sessionId,
|
|
15566
|
+
machineId: this.deps.machineId,
|
|
15567
|
+
chatroomId: opts.chatroomId,
|
|
15568
|
+
role: opts.role,
|
|
15569
|
+
error: err.message
|
|
15570
|
+
}).catch((emitErr) => {
|
|
15571
|
+
console.log(` ⚠️ Failed to emit startFailed event: ${emitErr.message}`);
|
|
15572
|
+
});
|
|
15620
15573
|
});
|
|
15621
|
-
ctx.lastPushedGitState.set(stateKey, stateHash2);
|
|
15622
|
-
return;
|
|
15623
15574
|
}
|
|
15624
|
-
|
|
15625
|
-
return;
|
|
15575
|
+
getSlot(chatroomId, role) {
|
|
15576
|
+
return this.slots.get(agentKey2(chatroomId, role));
|
|
15626
15577
|
}
|
|
15627
|
-
|
|
15628
|
-
|
|
15629
|
-
|
|
15630
|
-
|
|
15631
|
-
|
|
15632
|
-
|
|
15633
|
-
|
|
15578
|
+
listActive() {
|
|
15579
|
+
const result = [];
|
|
15580
|
+
for (const [key, slot] of this.slots) {
|
|
15581
|
+
if (slot.state === "running" || slot.state === "spawning") {
|
|
15582
|
+
const [chatroomId, role] = key.split(":");
|
|
15583
|
+
result.push({ chatroomId, role, slot });
|
|
15584
|
+
}
|
|
15585
|
+
}
|
|
15586
|
+
return result;
|
|
15634
15587
|
}
|
|
15635
|
-
|
|
15636
|
-
|
|
15637
|
-
|
|
15638
|
-
|
|
15639
|
-
|
|
15640
|
-
|
|
15641
|
-
|
|
15642
|
-
|
|
15643
|
-
|
|
15644
|
-
|
|
15645
|
-
|
|
15646
|
-
|
|
15647
|
-
|
|
15648
|
-
|
|
15649
|
-
|
|
15650
|
-
|
|
15651
|
-
|
|
15652
|
-
|
|
15653
|
-
|
|
15654
|
-
|
|
15655
|
-
|
|
15656
|
-
|
|
15657
|
-
|
|
15658
|
-
|
|
15659
|
-
workingDir,
|
|
15660
|
-
shas
|
|
15661
|
-
});
|
|
15662
|
-
if (missingShas.length === 0)
|
|
15663
|
-
return;
|
|
15664
|
-
console.log(`[${formatTimestamp()}] \uD83D\uDD0D Pre-fetching ${missingShas.length} commit(s) for ${workingDir}`);
|
|
15665
|
-
for (const sha of missingShas) {
|
|
15666
|
-
try {
|
|
15667
|
-
await prefetchSingleCommit(ctx, workingDir, sha, commits);
|
|
15668
|
-
} catch (err) {
|
|
15669
|
-
console.warn(`[${formatTimestamp()}] ⚠️ Pre-fetch failed for ${sha.slice(0, 7)}: ${err.message}`);
|
|
15588
|
+
async recover() {
|
|
15589
|
+
const entries = this.deps.persistence.listAgentEntries(this.deps.machineId);
|
|
15590
|
+
let recovered = 0;
|
|
15591
|
+
let cleaned = 0;
|
|
15592
|
+
for (const { chatroomId, role, entry } of entries) {
|
|
15593
|
+
const key = agentKey2(chatroomId, role);
|
|
15594
|
+
let alive = false;
|
|
15595
|
+
try {
|
|
15596
|
+
this.deps.processes.kill(entry.pid, 0);
|
|
15597
|
+
alive = true;
|
|
15598
|
+
} catch {
|
|
15599
|
+
alive = false;
|
|
15600
|
+
}
|
|
15601
|
+
if (alive) {
|
|
15602
|
+
this.slots.set(key, {
|
|
15603
|
+
state: "running",
|
|
15604
|
+
pid: entry.pid,
|
|
15605
|
+
harness: entry.harness
|
|
15606
|
+
});
|
|
15607
|
+
recovered++;
|
|
15608
|
+
} else {
|
|
15609
|
+
this.deps.persistence.clearAgentPid(this.deps.machineId, chatroomId, role);
|
|
15610
|
+
cleaned++;
|
|
15611
|
+
}
|
|
15670
15612
|
}
|
|
15613
|
+
console.log(`[AgentProcessManager] Recovery: ${recovered} alive, ${cleaned} cleaned up`);
|
|
15671
15614
|
}
|
|
15672
|
-
|
|
15673
|
-
|
|
15674
|
-
|
|
15675
|
-
|
|
15676
|
-
|
|
15677
|
-
|
|
15678
|
-
|
|
15679
|
-
machineId: ctx.machineId,
|
|
15680
|
-
workingDir,
|
|
15681
|
-
sha,
|
|
15682
|
-
status: "not_found",
|
|
15683
|
-
message: metadata?.message,
|
|
15684
|
-
author: metadata?.author,
|
|
15685
|
-
date: metadata?.date
|
|
15686
|
-
});
|
|
15687
|
-
return;
|
|
15615
|
+
getOrCreateSlot(key) {
|
|
15616
|
+
let slot = this.slots.get(key);
|
|
15617
|
+
if (!slot) {
|
|
15618
|
+
slot = { state: "idle" };
|
|
15619
|
+
this.slots.set(key, slot);
|
|
15620
|
+
}
|
|
15621
|
+
return slot;
|
|
15688
15622
|
}
|
|
15689
|
-
|
|
15690
|
-
|
|
15691
|
-
|
|
15692
|
-
|
|
15693
|
-
|
|
15694
|
-
|
|
15695
|
-
|
|
15696
|
-
|
|
15697
|
-
|
|
15698
|
-
|
|
15699
|
-
|
|
15700
|
-
|
|
15701
|
-
|
|
15623
|
+
async doEnsureRunning(key, slot, opts) {
|
|
15624
|
+
slot.state = "spawning";
|
|
15625
|
+
try {
|
|
15626
|
+
const spawnCheck = this.deps.spawning.shouldAllowSpawn(opts.chatroomId, opts.reason);
|
|
15627
|
+
if (!spawnCheck.allowed) {
|
|
15628
|
+
slot.state = "idle";
|
|
15629
|
+
slot.pendingOperation = undefined;
|
|
15630
|
+
return { success: false, error: "rate_limited" };
|
|
15631
|
+
}
|
|
15632
|
+
if (opts.reason === "platform.crash_recovery") {
|
|
15633
|
+
const loopCheck = this.deps.crashLoop.record(opts.chatroomId, opts.role);
|
|
15634
|
+
if (!loopCheck.allowed) {
|
|
15635
|
+
this.deps.backend.mutation(api.machines.emitRestartLimitReached, {
|
|
15636
|
+
sessionId: this.deps.sessionId,
|
|
15637
|
+
machineId: this.deps.machineId,
|
|
15638
|
+
chatroomId: opts.chatroomId,
|
|
15639
|
+
role: opts.role,
|
|
15640
|
+
restartCount: loopCheck.restartCount,
|
|
15641
|
+
windowMs: loopCheck.windowMs
|
|
15642
|
+
}).catch((err) => {
|
|
15643
|
+
console.log(` ⚠️ Failed to emit restartLimitReached event: ${err.message}`);
|
|
15644
|
+
});
|
|
15645
|
+
slot.state = "idle";
|
|
15646
|
+
slot.pendingOperation = undefined;
|
|
15647
|
+
return { success: false, error: "crash_loop" };
|
|
15648
|
+
}
|
|
15649
|
+
}
|
|
15650
|
+
try {
|
|
15651
|
+
const dirStat = await this.deps.fs.stat(opts.workingDir);
|
|
15652
|
+
if (!dirStat.isDirectory()) {
|
|
15653
|
+
slot.state = "idle";
|
|
15654
|
+
slot.pendingOperation = undefined;
|
|
15655
|
+
return {
|
|
15656
|
+
success: false,
|
|
15657
|
+
error: `Working directory is not a directory: ${opts.workingDir}`
|
|
15658
|
+
};
|
|
15659
|
+
}
|
|
15660
|
+
} catch {
|
|
15661
|
+
slot.state = "idle";
|
|
15662
|
+
slot.pendingOperation = undefined;
|
|
15663
|
+
return { success: false, error: `Working directory does not exist: ${opts.workingDir}` };
|
|
15664
|
+
}
|
|
15665
|
+
if (slot.pid) {
|
|
15666
|
+
try {
|
|
15667
|
+
this.deps.processes.kill(-slot.pid, "SIGTERM");
|
|
15668
|
+
} catch {}
|
|
15669
|
+
slot.pid = undefined;
|
|
15670
|
+
}
|
|
15671
|
+
let initPromptResult;
|
|
15672
|
+
try {
|
|
15673
|
+
initPromptResult = await this.deps.backend.query(api.messages.getInitPrompt, {
|
|
15674
|
+
sessionId: this.deps.sessionId,
|
|
15675
|
+
chatroomId: opts.chatroomId,
|
|
15676
|
+
role: opts.role,
|
|
15677
|
+
convexUrl: this.deps.convexUrl
|
|
15678
|
+
});
|
|
15679
|
+
} catch (e) {
|
|
15680
|
+
slot.state = "idle";
|
|
15681
|
+
slot.pendingOperation = undefined;
|
|
15682
|
+
return { success: false, error: `Failed to fetch init prompt: ${e.message}` };
|
|
15683
|
+
}
|
|
15684
|
+
if (!initPromptResult?.prompt) {
|
|
15685
|
+
slot.state = "idle";
|
|
15686
|
+
slot.pendingOperation = undefined;
|
|
15687
|
+
return { success: false, error: "Failed to fetch init prompt from backend" };
|
|
15688
|
+
}
|
|
15689
|
+
const service = this.deps.agentServices.get(opts.agentHarness);
|
|
15690
|
+
if (!service) {
|
|
15691
|
+
slot.state = "idle";
|
|
15692
|
+
slot.pendingOperation = undefined;
|
|
15693
|
+
return { success: false, error: `Unknown agent harness: ${opts.agentHarness}` };
|
|
15694
|
+
}
|
|
15695
|
+
let spawnResult;
|
|
15696
|
+
try {
|
|
15697
|
+
spawnResult = await service.spawn({
|
|
15698
|
+
workingDir: opts.workingDir,
|
|
15699
|
+
prompt: initPromptResult.initialMessage,
|
|
15700
|
+
systemPrompt: initPromptResult.rolePrompt,
|
|
15701
|
+
model: opts.model,
|
|
15702
|
+
context: {
|
|
15703
|
+
machineId: this.deps.machineId,
|
|
15704
|
+
chatroomId: opts.chatroomId,
|
|
15705
|
+
role: opts.role
|
|
15706
|
+
}
|
|
15707
|
+
});
|
|
15708
|
+
} catch (e) {
|
|
15709
|
+
slot.state = "idle";
|
|
15710
|
+
slot.pendingOperation = undefined;
|
|
15711
|
+
return { success: false, error: `Failed to spawn agent: ${e.message}` };
|
|
15712
|
+
}
|
|
15713
|
+
const { pid } = spawnResult;
|
|
15714
|
+
this.deps.spawning.recordSpawn(opts.chatroomId);
|
|
15715
|
+
slot.state = "running";
|
|
15716
|
+
slot.pid = pid;
|
|
15717
|
+
slot.harness = opts.agentHarness;
|
|
15718
|
+
slot.model = opts.model;
|
|
15719
|
+
slot.workingDir = opts.workingDir;
|
|
15720
|
+
slot.startedAt = this.deps.clock.now();
|
|
15721
|
+
slot.pendingOperation = undefined;
|
|
15722
|
+
this.deps.backend.mutation(api.machines.updateSpawnedAgent, {
|
|
15723
|
+
sessionId: this.deps.sessionId,
|
|
15724
|
+
machineId: this.deps.machineId,
|
|
15725
|
+
chatroomId: opts.chatroomId,
|
|
15726
|
+
role: opts.role,
|
|
15727
|
+
pid,
|
|
15728
|
+
model: opts.model,
|
|
15729
|
+
reason: opts.reason
|
|
15730
|
+
}).catch((err) => {
|
|
15731
|
+
console.log(` ⚠️ Failed to update PID in backend: ${err.message}`);
|
|
15732
|
+
});
|
|
15733
|
+
try {
|
|
15734
|
+
this.deps.persistence.persistAgentPid(this.deps.machineId, opts.chatroomId, opts.role, pid, opts.agentHarness);
|
|
15735
|
+
} catch {}
|
|
15736
|
+
spawnResult.onExit(({ code: code2, signal }) => {
|
|
15737
|
+
this.handleExit({
|
|
15738
|
+
chatroomId: opts.chatroomId,
|
|
15739
|
+
role: opts.role,
|
|
15740
|
+
pid,
|
|
15741
|
+
code: code2,
|
|
15742
|
+
signal
|
|
15743
|
+
});
|
|
15744
|
+
});
|
|
15745
|
+
if (spawnResult.onAgentEnd) {
|
|
15746
|
+
spawnResult.onAgentEnd(() => {
|
|
15747
|
+
try {
|
|
15748
|
+
this.deps.processes.kill(-pid, "SIGTERM");
|
|
15749
|
+
} catch {}
|
|
15750
|
+
});
|
|
15751
|
+
}
|
|
15752
|
+
let lastReportedTokenAt = 0;
|
|
15753
|
+
spawnResult.onOutput(() => {
|
|
15754
|
+
const now = this.deps.clock.now();
|
|
15755
|
+
if (now - lastReportedTokenAt >= 30000) {
|
|
15756
|
+
lastReportedTokenAt = now;
|
|
15757
|
+
this.deps.backend.mutation(api.participants.updateTokenActivity, {
|
|
15758
|
+
sessionId: this.deps.sessionId,
|
|
15759
|
+
chatroomId: opts.chatroomId,
|
|
15760
|
+
role: opts.role
|
|
15761
|
+
}).catch(() => {});
|
|
15762
|
+
}
|
|
15763
|
+
});
|
|
15764
|
+
return { success: true, pid };
|
|
15765
|
+
} catch (e) {
|
|
15766
|
+
slot.state = "idle";
|
|
15767
|
+
slot.pendingOperation = undefined;
|
|
15768
|
+
return { success: false, error: `Unexpected error: ${e.message}` };
|
|
15769
|
+
}
|
|
15770
|
+
}
|
|
15771
|
+
async doStop(key, slot, pid, opts) {
|
|
15772
|
+
try {
|
|
15773
|
+
try {
|
|
15774
|
+
this.deps.processes.kill(-pid, "SIGTERM");
|
|
15775
|
+
} catch {}
|
|
15776
|
+
let dead = false;
|
|
15777
|
+
for (let i2 = 0;i2 < 20; i2++) {
|
|
15778
|
+
await this.deps.clock.delay(500);
|
|
15779
|
+
try {
|
|
15780
|
+
this.deps.processes.kill(pid, 0);
|
|
15781
|
+
} catch {
|
|
15782
|
+
dead = true;
|
|
15783
|
+
break;
|
|
15784
|
+
}
|
|
15785
|
+
}
|
|
15786
|
+
if (!dead) {
|
|
15787
|
+
try {
|
|
15788
|
+
this.deps.processes.kill(-pid, "SIGKILL");
|
|
15789
|
+
} catch {}
|
|
15790
|
+
for (let i2 = 0;i2 < 10; i2++) {
|
|
15791
|
+
await this.deps.clock.delay(500);
|
|
15792
|
+
try {
|
|
15793
|
+
this.deps.processes.kill(pid, 0);
|
|
15794
|
+
} catch {
|
|
15795
|
+
dead = true;
|
|
15796
|
+
break;
|
|
15797
|
+
}
|
|
15798
|
+
}
|
|
15799
|
+
}
|
|
15800
|
+
} catch {}
|
|
15801
|
+
slot.state = "idle";
|
|
15802
|
+
slot.pid = undefined;
|
|
15803
|
+
slot.startedAt = undefined;
|
|
15804
|
+
slot.pendingOperation = undefined;
|
|
15805
|
+
this.deps.backend.mutation(api.machines.recordAgentExited, {
|
|
15806
|
+
sessionId: this.deps.sessionId,
|
|
15807
|
+
machineId: this.deps.machineId,
|
|
15808
|
+
chatroomId: opts.chatroomId,
|
|
15809
|
+
role: opts.role,
|
|
15810
|
+
pid,
|
|
15811
|
+
stopReason: opts.reason,
|
|
15812
|
+
exitCode: undefined,
|
|
15813
|
+
signal: undefined,
|
|
15814
|
+
agentHarness: slot.harness
|
|
15815
|
+
}).catch((err) => {
|
|
15816
|
+
console.log(` ⚠️ Failed to record agent exit event: ${err.message}`);
|
|
15817
|
+
});
|
|
15818
|
+
this.deps.persistence.clearAgentPid(this.deps.machineId, opts.chatroomId, opts.role);
|
|
15819
|
+
for (const service of this.deps.agentServices.values()) {
|
|
15820
|
+
service.untrack(pid);
|
|
15821
|
+
}
|
|
15822
|
+
return { success: true };
|
|
15702
15823
|
}
|
|
15703
|
-
const diffStat = extractDiffStatFromShowOutput(result.content);
|
|
15704
|
-
await ctx.deps.backend.mutation(api.workspaces.upsertCommitDetail, {
|
|
15705
|
-
sessionId: ctx.sessionId,
|
|
15706
|
-
machineId: ctx.machineId,
|
|
15707
|
-
workingDir,
|
|
15708
|
-
sha,
|
|
15709
|
-
status: "available",
|
|
15710
|
-
diffContent: result.content,
|
|
15711
|
-
truncated: result.truncated,
|
|
15712
|
-
message: metadata?.message,
|
|
15713
|
-
author: metadata?.author,
|
|
15714
|
-
date: metadata?.date,
|
|
15715
|
-
diffStat
|
|
15716
|
-
});
|
|
15717
|
-
console.log(`[${formatTimestamp()}] ✅ Pre-fetched: ${sha.slice(0, 7)} in ${workingDir}`);
|
|
15718
15824
|
}
|
|
15719
|
-
var
|
|
15825
|
+
var init_agent_process_manager = __esm(() => {
|
|
15720
15826
|
init_api3();
|
|
15721
|
-
init_git_reader();
|
|
15722
|
-
init_git_polling();
|
|
15723
15827
|
});
|
|
15724
15828
|
|
|
15725
|
-
// src/
|
|
15726
|
-
|
|
15727
|
-
|
|
15728
|
-
|
|
15729
|
-
|
|
15730
|
-
if (
|
|
15731
|
-
|
|
15732
|
-
|
|
15733
|
-
|
|
15734
|
-
|
|
15735
|
-
|
|
15736
|
-
if (task.status === "in_progress") {
|
|
15737
|
-
return agentConfig.spawnedAgentPid == null;
|
|
15738
|
-
}
|
|
15739
|
-
if (task.status === "pending" || task.status === "acknowledged") {
|
|
15740
|
-
return agentConfig.spawnedAgentPid == null;
|
|
15829
|
+
// src/commands/machine/daemon-start/init.ts
|
|
15830
|
+
import { stat } from "node:fs/promises";
|
|
15831
|
+
async function discoverModels(agentServices) {
|
|
15832
|
+
const results = {};
|
|
15833
|
+
for (const [harness, service] of agentServices) {
|
|
15834
|
+
if (service.isInstalled()) {
|
|
15835
|
+
try {
|
|
15836
|
+
results[harness] = await service.listModels();
|
|
15837
|
+
} catch {
|
|
15838
|
+
results[harness] = [];
|
|
15839
|
+
}
|
|
15741
15840
|
}
|
|
15742
|
-
return false;
|
|
15743
15841
|
}
|
|
15842
|
+
return results;
|
|
15744
15843
|
}
|
|
15745
|
-
|
|
15746
|
-
|
|
15747
|
-
|
|
15748
|
-
|
|
15749
|
-
|
|
15750
|
-
|
|
15751
|
-
|
|
15752
|
-
|
|
15753
|
-
|
|
15754
|
-
|
|
15755
|
-
|
|
15756
|
-
|
|
15757
|
-
|
|
15758
|
-
|
|
15759
|
-
|
|
15760
|
-
|
|
15761
|
-
|
|
15844
|
+
function createDefaultDeps19() {
|
|
15845
|
+
return {
|
|
15846
|
+
backend: {
|
|
15847
|
+
mutation: async () => {
|
|
15848
|
+
throw new Error("Backend not initialized");
|
|
15849
|
+
},
|
|
15850
|
+
query: async () => {
|
|
15851
|
+
throw new Error("Backend not initialized");
|
|
15852
|
+
}
|
|
15853
|
+
},
|
|
15854
|
+
processes: {
|
|
15855
|
+
kill: (pid, signal) => process.kill(pid, signal)
|
|
15856
|
+
},
|
|
15857
|
+
fs: {
|
|
15858
|
+
stat
|
|
15859
|
+
},
|
|
15860
|
+
machine: {
|
|
15861
|
+
clearAgentPid,
|
|
15862
|
+
persistAgentPid,
|
|
15863
|
+
listAgentEntries,
|
|
15864
|
+
persistEventCursor,
|
|
15865
|
+
loadEventCursor
|
|
15866
|
+
},
|
|
15867
|
+
clock: {
|
|
15868
|
+
now: () => Date.now(),
|
|
15869
|
+
delay: (ms) => new Promise((resolve2) => setTimeout(resolve2, ms))
|
|
15870
|
+
},
|
|
15871
|
+
spawning: new HarnessSpawningService({ rateLimiter: new SpawnRateLimiter }),
|
|
15872
|
+
agentProcessManager: null
|
|
15873
|
+
};
|
|
15874
|
+
}
|
|
15875
|
+
function validateAuthentication(convexUrl) {
|
|
15876
|
+
const sessionId = getSessionId();
|
|
15877
|
+
if (!sessionId) {
|
|
15878
|
+
const otherUrls = getOtherSessionUrls();
|
|
15879
|
+
console.error(`❌ Not authenticated for: ${convexUrl}`);
|
|
15880
|
+
if (otherUrls.length > 0) {
|
|
15881
|
+
console.error(`
|
|
15882
|
+
\uD83D\uDCA1 You have sessions for other environments:`);
|
|
15883
|
+
for (const url of otherUrls) {
|
|
15884
|
+
console.error(` • ${url}`);
|
|
15762
15885
|
}
|
|
15763
15886
|
}
|
|
15764
|
-
|
|
15765
|
-
|
|
15766
|
-
|
|
15767
|
-
|
|
15768
|
-
return false;
|
|
15769
|
-
}
|
|
15770
|
-
const key = `${task.chatroomId}:${agentConfig.role.toLowerCase()}`;
|
|
15771
|
-
const pendingReason = context.pendingStops.get(key);
|
|
15772
|
-
return pendingReason === "agent_process.turn_end";
|
|
15887
|
+
console.error(`
|
|
15888
|
+
Run: chatroom auth login`);
|
|
15889
|
+
releaseLock();
|
|
15890
|
+
process.exit(1);
|
|
15773
15891
|
}
|
|
15892
|
+
return sessionId;
|
|
15774
15893
|
}
|
|
15775
|
-
function
|
|
15776
|
-
|
|
15777
|
-
|
|
15778
|
-
|
|
15779
|
-
|
|
15780
|
-
|
|
15781
|
-
|
|
15782
|
-
|
|
15783
|
-
id: harness,
|
|
15784
|
-
shouldStartAgent: () => false
|
|
15785
|
-
};
|
|
15894
|
+
async function validateSession(client2, sessionId, convexUrl) {
|
|
15895
|
+
const validation = await client2.query(api.cliAuth.validateSession, { sessionId });
|
|
15896
|
+
if (!validation.valid) {
|
|
15897
|
+
console.error(`❌ Session invalid: ${validation.reason}`);
|
|
15898
|
+
console.error(`
|
|
15899
|
+
Run: chatroom auth login`);
|
|
15900
|
+
releaseLock();
|
|
15901
|
+
process.exit(1);
|
|
15786
15902
|
}
|
|
15787
15903
|
}
|
|
15788
|
-
|
|
15789
|
-
|
|
15790
|
-
|
|
15791
|
-
|
|
15792
|
-
|
|
15793
|
-
|
|
15794
|
-
}
|
|
15795
|
-
|
|
15796
|
-
|
|
15797
|
-
|
|
15798
|
-
|
|
15799
|
-
|
|
15800
|
-
|
|
15801
|
-
|
|
15802
|
-
|
|
15803
|
-
|
|
15804
|
-
|
|
15805
|
-
|
|
15806
|
-
|
|
15807
|
-
|
|
15808
|
-
|
|
15809
|
-
|
|
15810
|
-
|
|
15811
|
-
|
|
15812
|
-
|
|
15813
|
-
|
|
15814
|
-
|
|
15815
|
-
|
|
15816
|
-
|
|
15817
|
-
|
|
15818
|
-
|
|
15819
|
-
|
|
15820
|
-
|
|
15821
|
-
|
|
15822
|
-
|
|
15823
|
-
createdAt: taskInfo.createdAt
|
|
15824
|
-
},
|
|
15825
|
-
agentConfig: taskInfo.agentConfig
|
|
15826
|
-
};
|
|
15827
|
-
const policy = getRestartPolicyForHarness(agentConfig.agentHarness);
|
|
15828
|
-
const shouldStart = policy.shouldStartAgent({ task, agentConfig }, agentEndContext);
|
|
15829
|
-
if (!shouldStart)
|
|
15830
|
-
continue;
|
|
15831
|
-
if (!canAttemptRestart(task.chatroomId, agentConfig.role, now)) {
|
|
15832
|
-
continue;
|
|
15833
|
-
}
|
|
15834
|
-
if (!agentConfig.workingDir) {
|
|
15835
|
-
console.warn(`[${formatTimestamp()}] ⚠️ Missing workingDir for ${task.chatroomId}/${agentConfig.role}` + ` — skipping`);
|
|
15836
|
-
continue;
|
|
15837
|
-
}
|
|
15838
|
-
console.log(`[${formatTimestamp()}] \uD83D\uDCE1 Task monitor: starting agent for ` + `${task.chatroomId}/${agentConfig.role} (harness: ${agentConfig.agentHarness})`);
|
|
15839
|
-
recordRestartAttempt(task.chatroomId, agentConfig.role, now);
|
|
15840
|
-
try {
|
|
15841
|
-
await executeStartAgent(ctx, {
|
|
15842
|
-
chatroomId: task.chatroomId,
|
|
15843
|
-
role: agentConfig.role,
|
|
15844
|
-
agentHarness: agentConfig.agentHarness,
|
|
15845
|
-
model: agentConfig.model,
|
|
15846
|
-
workingDir: agentConfig.workingDir,
|
|
15847
|
-
reason: "daemon.task_monitor"
|
|
15848
|
-
});
|
|
15849
|
-
} catch (err) {
|
|
15850
|
-
console.error(`[${formatTimestamp()}] ❌ Task monitor failed to start agent ` + `for ${task.chatroomId}/${agentConfig.role}: ${err.message}`);
|
|
15851
|
-
}
|
|
15852
|
-
}
|
|
15853
|
-
});
|
|
15854
|
-
console.log(`[${formatTimestamp()}] \uD83D\uDD0D Task monitor started`);
|
|
15855
|
-
} catch (err) {
|
|
15856
|
-
if (isRunning) {
|
|
15857
|
-
console.error(`[${formatTimestamp()}] ❌ Task monitor error: ${err.message}`);
|
|
15858
|
-
setTimeout(startMonitoring, 5000);
|
|
15859
|
-
}
|
|
15860
|
-
}
|
|
15861
|
-
};
|
|
15862
|
-
startMonitoring();
|
|
15863
|
-
return {
|
|
15864
|
-
stop: () => {
|
|
15865
|
-
isRunning = false;
|
|
15866
|
-
if (unsubscribe) {
|
|
15867
|
-
unsubscribe();
|
|
15868
|
-
unsubscribe = null;
|
|
15869
|
-
}
|
|
15904
|
+
function setupMachine() {
|
|
15905
|
+
ensureMachineRegistered();
|
|
15906
|
+
const config3 = loadMachineConfig();
|
|
15907
|
+
return config3;
|
|
15908
|
+
}
|
|
15909
|
+
async function registerCapabilities(client2, sessionId, config3, agentServices) {
|
|
15910
|
+
const { machineId } = config3;
|
|
15911
|
+
const availableModels = await discoverModels(agentServices);
|
|
15912
|
+
try {
|
|
15913
|
+
await client2.mutation(api.machines.register, {
|
|
15914
|
+
sessionId,
|
|
15915
|
+
machineId,
|
|
15916
|
+
hostname: config3.hostname,
|
|
15917
|
+
os: config3.os,
|
|
15918
|
+
availableHarnesses: config3.availableHarnesses,
|
|
15919
|
+
harnessVersions: config3.harnessVersions,
|
|
15920
|
+
availableModels
|
|
15921
|
+
});
|
|
15922
|
+
} catch (error) {
|
|
15923
|
+
console.warn(`⚠️ Machine registration update failed: ${error.message}`);
|
|
15924
|
+
}
|
|
15925
|
+
return availableModels;
|
|
15926
|
+
}
|
|
15927
|
+
async function connectDaemon(client2, sessionId, machineId, convexUrl) {
|
|
15928
|
+
try {
|
|
15929
|
+
await client2.mutation(api.machines.updateDaemonStatus, {
|
|
15930
|
+
sessionId,
|
|
15931
|
+
machineId,
|
|
15932
|
+
connected: true
|
|
15933
|
+
});
|
|
15934
|
+
} catch (error) {
|
|
15935
|
+
if (isNetworkError(error)) {
|
|
15936
|
+
formatConnectivityError(error, convexUrl);
|
|
15937
|
+
} else {
|
|
15938
|
+
console.error(`❌ Failed to update daemon status: ${error.message}`);
|
|
15870
15939
|
}
|
|
15940
|
+
releaseLock();
|
|
15941
|
+
process.exit(1);
|
|
15942
|
+
}
|
|
15943
|
+
}
|
|
15944
|
+
function logStartup(ctx, availableModels) {
|
|
15945
|
+
console.log(`[${formatTimestamp()}] \uD83D\uDE80 Daemon started`);
|
|
15946
|
+
console.log(` CLI version: ${getVersion()}`);
|
|
15947
|
+
console.log(` Machine ID: ${ctx.machineId}`);
|
|
15948
|
+
console.log(` Hostname: ${ctx.config?.hostname ?? "unknown"}`);
|
|
15949
|
+
console.log(` Available harnesses: ${ctx.config?.availableHarnesses.join(", ") || "none"}`);
|
|
15950
|
+
console.log(` Available models: ${Object.keys(availableModels).length > 0 ? `${Object.values(availableModels).flat().length} models across ${Object.keys(availableModels).join(", ")}` : "none discovered"}`);
|
|
15951
|
+
console.log(` PID: ${process.pid}`);
|
|
15952
|
+
}
|
|
15953
|
+
async function recoverState(ctx) {
|
|
15954
|
+
console.log(`
|
|
15955
|
+
[${formatTimestamp()}] \uD83D\uDD04 Recovering agent state...`);
|
|
15956
|
+
try {
|
|
15957
|
+
await recoverAgentState(ctx);
|
|
15958
|
+
} catch (e) {
|
|
15959
|
+
console.log(` ⚠️ Recovery failed: ${e.message}`);
|
|
15960
|
+
console.log(` Continuing with fresh state`);
|
|
15961
|
+
}
|
|
15962
|
+
}
|
|
15963
|
+
async function initDaemon() {
|
|
15964
|
+
if (!acquireLock()) {
|
|
15965
|
+
process.exit(1);
|
|
15966
|
+
}
|
|
15967
|
+
const convexUrl = getConvexUrl();
|
|
15968
|
+
const sessionId = validateAuthentication(convexUrl);
|
|
15969
|
+
const client2 = await getConvexClient();
|
|
15970
|
+
const typedSessionId = sessionId;
|
|
15971
|
+
await validateSession(client2, typedSessionId, convexUrl);
|
|
15972
|
+
const config3 = setupMachine();
|
|
15973
|
+
const { machineId } = config3;
|
|
15974
|
+
initHarnessRegistry();
|
|
15975
|
+
const agentServices = new Map(getAllHarnesses().map((s) => [s.id, s]));
|
|
15976
|
+
const availableModels = await registerCapabilities(client2, typedSessionId, config3, agentServices);
|
|
15977
|
+
await connectDaemon(client2, typedSessionId, machineId, convexUrl);
|
|
15978
|
+
const deps = createDefaultDeps19();
|
|
15979
|
+
deps.backend.mutation = (endpoint, args) => client2.mutation(endpoint, args);
|
|
15980
|
+
deps.backend.query = (endpoint, args) => client2.query(endpoint, args);
|
|
15981
|
+
deps.agentProcessManager = new AgentProcessManager({
|
|
15982
|
+
agentServices,
|
|
15983
|
+
backend: deps.backend,
|
|
15984
|
+
sessionId: typedSessionId,
|
|
15985
|
+
machineId,
|
|
15986
|
+
processes: deps.processes,
|
|
15987
|
+
clock: deps.clock,
|
|
15988
|
+
fs: deps.fs,
|
|
15989
|
+
persistence: deps.machine,
|
|
15990
|
+
spawning: deps.spawning,
|
|
15991
|
+
crashLoop: new CrashLoopTracker,
|
|
15992
|
+
convexUrl
|
|
15993
|
+
});
|
|
15994
|
+
const events = new DaemonEventBus;
|
|
15995
|
+
const ctx = {
|
|
15996
|
+
client: client2,
|
|
15997
|
+
sessionId: typedSessionId,
|
|
15998
|
+
machineId,
|
|
15999
|
+
config: config3,
|
|
16000
|
+
deps,
|
|
16001
|
+
events,
|
|
16002
|
+
agentServices,
|
|
16003
|
+
lastPushedGitState: new Map
|
|
15871
16004
|
};
|
|
16005
|
+
registerEventListeners(ctx);
|
|
16006
|
+
logStartup(ctx, availableModels);
|
|
16007
|
+
await recoverState(ctx);
|
|
16008
|
+
return ctx;
|
|
15872
16009
|
}
|
|
15873
|
-
var
|
|
15874
|
-
|
|
16010
|
+
var init_init2 = __esm(() => {
|
|
16011
|
+
init_state_recovery();
|
|
15875
16012
|
init_api3();
|
|
16013
|
+
init_register_listeners();
|
|
16014
|
+
init_storage();
|
|
15876
16015
|
init_client2();
|
|
15877
|
-
|
|
15878
|
-
|
|
16016
|
+
init_machine();
|
|
16017
|
+
init_harness_spawning();
|
|
16018
|
+
init_crash_loop_tracker();
|
|
16019
|
+
init_agent_process_manager();
|
|
16020
|
+
init_remote_agents();
|
|
16021
|
+
init_error_formatting();
|
|
16022
|
+
init_version();
|
|
16023
|
+
init_pid();
|
|
16024
|
+
});
|
|
16025
|
+
|
|
16026
|
+
// src/events/lifecycle/on-daemon-shutdown.ts
|
|
16027
|
+
async function onDaemonShutdown(ctx) {
|
|
16028
|
+
const activeAgents = ctx.deps.agentProcessManager.listActive();
|
|
16029
|
+
if (activeAgents.length > 0) {
|
|
16030
|
+
console.log(`[${formatTimestamp()}] Stopping ${activeAgents.length} agent(s)...`);
|
|
16031
|
+
await Promise.allSettled(activeAgents.map(async ({ chatroomId, role, slot }) => {
|
|
16032
|
+
try {
|
|
16033
|
+
await ctx.deps.agentProcessManager.stop({
|
|
16034
|
+
chatroomId,
|
|
16035
|
+
role,
|
|
16036
|
+
reason: "user.stop"
|
|
16037
|
+
});
|
|
16038
|
+
console.log(` Stopped ${role} (PID ${slot.pid})`);
|
|
16039
|
+
} catch (e) {
|
|
16040
|
+
console.log(` ⚠️ Failed to stop ${role}: ${e.message}`);
|
|
16041
|
+
}
|
|
16042
|
+
}));
|
|
16043
|
+
console.log(`[${formatTimestamp()}] All agents stopped`);
|
|
16044
|
+
}
|
|
16045
|
+
try {
|
|
16046
|
+
await ctx.deps.backend.mutation(api.machines.updateDaemonStatus, {
|
|
16047
|
+
sessionId: ctx.sessionId,
|
|
16048
|
+
machineId: ctx.machineId,
|
|
16049
|
+
connected: false
|
|
16050
|
+
});
|
|
16051
|
+
} catch {}
|
|
16052
|
+
}
|
|
16053
|
+
var init_on_daemon_shutdown = __esm(() => {
|
|
16054
|
+
init_api3();
|
|
15879
16055
|
});
|
|
15880
16056
|
|
|
15881
16057
|
// src/commands/machine/daemon-start/command-loop.ts
|
|
@@ -15964,15 +16140,14 @@ async function startCommandLoop(ctx) {
|
|
|
15964
16140
|
});
|
|
15965
16141
|
}, DAEMON_HEARTBEAT_INTERVAL_MS);
|
|
15966
16142
|
heartbeatTimer.unref();
|
|
15967
|
-
|
|
15968
|
-
const taskMonitorHandle = startTaskMonitor(ctx);
|
|
16143
|
+
let gitSubscriptionHandle = null;
|
|
15969
16144
|
pushGitState(ctx).catch(() => {});
|
|
15970
16145
|
const shutdown = async () => {
|
|
15971
16146
|
console.log(`
|
|
15972
16147
|
[${formatTimestamp()}] Shutting down...`);
|
|
15973
16148
|
clearInterval(heartbeatTimer);
|
|
15974
|
-
|
|
15975
|
-
|
|
16149
|
+
if (gitSubscriptionHandle)
|
|
16150
|
+
gitSubscriptionHandle.stop();
|
|
15976
16151
|
await onDaemonShutdown(ctx);
|
|
15977
16152
|
releaseLock();
|
|
15978
16153
|
process.exit(0);
|
|
@@ -15981,6 +16156,7 @@ async function startCommandLoop(ctx) {
|
|
|
15981
16156
|
process.on("SIGTERM", shutdown);
|
|
15982
16157
|
process.on("SIGHUP", shutdown);
|
|
15983
16158
|
const wsClient2 = await getConvexWsClient();
|
|
16159
|
+
gitSubscriptionHandle = startGitRequestSubscription(ctx, wsClient2);
|
|
15984
16160
|
console.log(`
|
|
15985
16161
|
Listening for commands...`);
|
|
15986
16162
|
console.log(`Press Ctrl+C to stop
|
|
@@ -16014,17 +16190,16 @@ Listening for commands...`);
|
|
|
16014
16190
|
}
|
|
16015
16191
|
var MODEL_REFRESH_INTERVAL_MS;
|
|
16016
16192
|
var init_command_loop = __esm(() => {
|
|
16017
|
-
init_api3();
|
|
16018
|
-
init_client2();
|
|
16019
|
-
init_machine();
|
|
16020
|
-
init_on_daemon_shutdown();
|
|
16021
|
-
init_init2();
|
|
16022
16193
|
init_on_request_start_agent();
|
|
16023
16194
|
init_on_request_stop_agent();
|
|
16024
16195
|
init_pid();
|
|
16025
|
-
init_git_polling();
|
|
16026
16196
|
init_git_heartbeat();
|
|
16027
|
-
|
|
16197
|
+
init_git_subscription();
|
|
16198
|
+
init_init2();
|
|
16199
|
+
init_api3();
|
|
16200
|
+
init_on_daemon_shutdown();
|
|
16201
|
+
init_client2();
|
|
16202
|
+
init_machine();
|
|
16028
16203
|
MODEL_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
|
|
16029
16204
|
});
|
|
16030
16205
|
|
|
@@ -16036,8 +16211,6 @@ async function daemonStart() {
|
|
|
16036
16211
|
var init_daemon_start = __esm(() => {
|
|
16037
16212
|
init_command_loop();
|
|
16038
16213
|
init_init2();
|
|
16039
|
-
init_start_agent();
|
|
16040
|
-
init_stop_agent();
|
|
16041
16214
|
init_state_recovery();
|
|
16042
16215
|
});
|
|
16043
16216
|
|
|
@@ -16695,7 +16868,7 @@ program2.command("task-started").description("[LEGACY] Acknowledge a task and op
|
|
|
16695
16868
|
noClassify: skipClassification
|
|
16696
16869
|
});
|
|
16697
16870
|
});
|
|
16698
|
-
program2.command("classify").description("Classify a task's origin message (entry-point role only).
|
|
16871
|
+
program2.command("classify").description("Classify a task's origin message (entry-point role only).").requiredOption("--chatroom-id <id>", "Chatroom identifier").requiredOption("--role <role>", "Your role (must be entry-point role)").requiredOption("--task-id <taskId>", "Task ID to acknowledge").requiredOption("--origin-message-classification <type>", "Original message classification: question, new_feature, or follow_up").action(async (options) => {
|
|
16699
16872
|
await maybeRequireAuth();
|
|
16700
16873
|
const validClassifications = ["question", "new_feature", "follow_up"];
|
|
16701
16874
|
if (!validClassifications.includes(options.originMessageClassification)) {
|
|
@@ -16781,12 +16954,14 @@ program2.command("report-progress").description("Report progress on current task
|
|
|
16781
16954
|
});
|
|
16782
16955
|
});
|
|
16783
16956
|
var backlogCommand = program2.command("backlog").description("Manage task queue and backlog");
|
|
16784
|
-
backlogCommand.command("list").description("List
|
|
16957
|
+
backlogCommand.command("list").description("List backlog items").requiredOption("--chatroom-id <id>", "Chatroom identifier").requiredOption("--role <role>", "Your role").option("--limit <n>", "Maximum number of items to show").option("--sort <sort>", "Sort order: date:desc (default) | priority:desc").option("--filter <filter>", "Filter: unscored (only items without priority score)").action(async (options) => {
|
|
16785
16958
|
await maybeRequireAuth();
|
|
16786
16959
|
const { listBacklog: listBacklog2 } = await Promise.resolve().then(() => (init_backlog(), exports_backlog));
|
|
16787
16960
|
await listBacklog2(options.chatroomId, {
|
|
16788
16961
|
role: options.role,
|
|
16789
|
-
limit: options.limit ? parseInt(options.limit, 10) : undefined
|
|
16962
|
+
limit: options.limit ? parseInt(options.limit, 10) : undefined,
|
|
16963
|
+
sort: options.sort,
|
|
16964
|
+
filter: options.filter
|
|
16790
16965
|
});
|
|
16791
16966
|
});
|
|
16792
16967
|
backlogCommand.command("add").description("Add a backlog item").requiredOption("--chatroom-id <id>", "Chatroom identifier").requiredOption("--role <role>", "Your role (creator)").requiredOption("--content-file <path>", "Path to file containing task content").action(async (options) => {
|
|
@@ -16836,6 +17011,21 @@ backlogCommand.command("history").description("View completed and closed backlog
|
|
|
16836
17011
|
limit: options.limit ? parseInt(options.limit, 10) : undefined
|
|
16837
17012
|
});
|
|
16838
17013
|
});
|
|
17014
|
+
backlogCommand.command("close").description("Close a backlog item (mark as stale/superseded)").requiredOption("--chatroom-id <id>", "Chatroom identifier").requiredOption("--role <role>", "Your role").requiredOption("--backlog-item-id <id>", "Backlog item ID to close").requiredOption("--reason <text>", "Reason for closing (required for audit trail)").action(async (options) => {
|
|
17015
|
+
await maybeRequireAuth();
|
|
17016
|
+
const { closeBacklog: closeBacklog2 } = await Promise.resolve().then(() => (init_backlog(), exports_backlog));
|
|
17017
|
+
await closeBacklog2(options.chatroomId, options);
|
|
17018
|
+
});
|
|
17019
|
+
backlogCommand.command("export").description("Export backlog items to a JSON file").requiredOption("--chatroom-id <id>", "Chatroom identifier").requiredOption("--role <role>", "Your role").option("--path <path>", "Directory path to export to (default: .chatroom/exports/)").action(async (options) => {
|
|
17020
|
+
await maybeRequireAuth();
|
|
17021
|
+
const { exportBacklog: exportBacklog2 } = await Promise.resolve().then(() => (init_backlog(), exports_backlog));
|
|
17022
|
+
await exportBacklog2(options.chatroomId, { role: options.role, path: options.path });
|
|
17023
|
+
});
|
|
17024
|
+
backlogCommand.command("import").description("Import backlog items from a JSON export file").requiredOption("--chatroom-id <id>", "Chatroom identifier").requiredOption("--role <role>", "Your role").option("--path <path>", "Directory path to import from (default: .chatroom/exports/)").action(async (options) => {
|
|
17025
|
+
await maybeRequireAuth();
|
|
17026
|
+
const { importBacklog: importBacklog2 } = await Promise.resolve().then(() => (init_backlog(), exports_backlog));
|
|
17027
|
+
await importBacklog2(options.chatroomId, { role: options.role, path: options.path });
|
|
17028
|
+
});
|
|
16839
17029
|
var taskCommand = program2.command("task").description("Manage tasks");
|
|
16840
17030
|
taskCommand.command("read").description("Read a task and mark it as in_progress").requiredOption("--chatroom-id <id>", "Chatroom identifier").requiredOption("--role <role>", "Your role in the chatroom").requiredOption("--task-id <taskId>", "Task ID to read").action(async (options) => {
|
|
16841
17031
|
await maybeRequireAuth();
|