chatroom-cli 1.11.1 → 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 +1614 -1435
- 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
|
|
@@ -11134,34 +11134,10 @@ var init_daemon_state = __esm(() => {
|
|
|
11134
11134
|
STATE_DIR = join4(CHATROOM_DIR3, "machines", "state");
|
|
11135
11135
|
});
|
|
11136
11136
|
|
|
11137
|
-
// src/infrastructure/machine/intentional-stops.ts
|
|
11138
|
-
function agentKey2(chatroomId, role) {
|
|
11139
|
-
return `${chatroomId}:${role.toLowerCase()}`;
|
|
11140
|
-
}
|
|
11141
|
-
function markIntentionalStop(chatroomId, role, reason = "user.stop") {
|
|
11142
|
-
pendingStops.set(agentKey2(chatroomId, role), reason);
|
|
11143
|
-
}
|
|
11144
|
-
function consumeIntentionalStop(chatroomId, role) {
|
|
11145
|
-
const key = agentKey2(chatroomId, role);
|
|
11146
|
-
const reason = pendingStops.get(key) ?? null;
|
|
11147
|
-
if (reason !== null) {
|
|
11148
|
-
pendingStops.delete(key);
|
|
11149
|
-
}
|
|
11150
|
-
return reason;
|
|
11151
|
-
}
|
|
11152
|
-
function clearIntentionalStop(chatroomId, role) {
|
|
11153
|
-
pendingStops.delete(agentKey2(chatroomId, role));
|
|
11154
|
-
}
|
|
11155
|
-
var pendingStops;
|
|
11156
|
-
var init_intentional_stops = __esm(() => {
|
|
11157
|
-
pendingStops = new Map;
|
|
11158
|
-
});
|
|
11159
|
-
|
|
11160
11137
|
// src/infrastructure/machine/index.ts
|
|
11161
11138
|
var init_machine = __esm(() => {
|
|
11162
11139
|
init_storage2();
|
|
11163
11140
|
init_daemon_state();
|
|
11164
|
-
init_intentional_stops();
|
|
11165
11141
|
});
|
|
11166
11142
|
|
|
11167
11143
|
// src/commands/auth-status/index.ts
|
|
@@ -11709,8 +11685,8 @@ var init_utils = __esm(() => {
|
|
|
11709
11685
|
|
|
11710
11686
|
// ../../services/backend/prompts/base/shared/getting-started-content.ts
|
|
11711
11687
|
var init_getting_started_content = __esm(() => {
|
|
11712
|
-
init_utils();
|
|
11713
11688
|
init_reminder();
|
|
11689
|
+
init_utils();
|
|
11714
11690
|
});
|
|
11715
11691
|
|
|
11716
11692
|
// ../../services/backend/prompts/cli/index.ts
|
|
@@ -12344,8 +12320,7 @@ async function classify(chatroomId, options, deps) {
|
|
|
12344
12320
|
console.error(`❌ \`classify\` is only available to the entry point role (${entryPoint}). Your role is ${role}.`);
|
|
12345
12321
|
console.error("");
|
|
12346
12322
|
console.error(" Entry point roles receive user messages and must classify them.");
|
|
12347
|
-
console.error(" Other roles receive handoffs
|
|
12348
|
-
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.");
|
|
12349
12324
|
process.exit(1);
|
|
12350
12325
|
}
|
|
12351
12326
|
if (originMessageClassification === "new_feature") {
|
|
@@ -12839,12 +12814,19 @@ __export(exports_backlog, {
|
|
|
12839
12814
|
patchBacklog: () => patchBacklog,
|
|
12840
12815
|
markForReviewBacklog: () => markForReviewBacklog,
|
|
12841
12816
|
listBacklog: () => listBacklog,
|
|
12817
|
+
importBacklog: () => importBacklog,
|
|
12842
12818
|
historyBacklog: () => historyBacklog,
|
|
12819
|
+
exportBacklog: () => exportBacklog,
|
|
12820
|
+
computeContentHash: () => computeContentHash,
|
|
12843
12821
|
completeBacklog: () => completeBacklog,
|
|
12822
|
+
closeBacklog: () => closeBacklog,
|
|
12844
12823
|
addBacklog: () => addBacklog
|
|
12845
12824
|
});
|
|
12825
|
+
import { createHash } from "node:crypto";
|
|
12826
|
+
import * as nodePath from "node:path";
|
|
12846
12827
|
async function createDefaultDeps11() {
|
|
12847
12828
|
const client2 = await getConvexClient();
|
|
12829
|
+
const fs = await import("node:fs/promises");
|
|
12848
12830
|
return {
|
|
12849
12831
|
backend: {
|
|
12850
12832
|
mutation: (endpoint, args) => client2.mutation(endpoint, args),
|
|
@@ -12854,6 +12836,11 @@ async function createDefaultDeps11() {
|
|
|
12854
12836
|
getSessionId,
|
|
12855
12837
|
getConvexUrl,
|
|
12856
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)
|
|
12857
12844
|
}
|
|
12858
12845
|
};
|
|
12859
12846
|
}
|
|
@@ -12880,17 +12867,19 @@ async function listBacklog(chatroomId, options, deps) {
|
|
|
12880
12867
|
const backlogItems = await d.backend.query(api.backlog.listBacklogItems, {
|
|
12881
12868
|
sessionId,
|
|
12882
12869
|
chatroomId,
|
|
12883
|
-
statusFilter: "
|
|
12870
|
+
statusFilter: "backlog",
|
|
12871
|
+
sort: options.sort,
|
|
12872
|
+
filter: options.filter,
|
|
12884
12873
|
limit
|
|
12885
12874
|
});
|
|
12886
12875
|
console.log("");
|
|
12887
12876
|
console.log("══════════════════════════════════════════════════");
|
|
12888
|
-
console.log("\uD83D\uDCCB
|
|
12877
|
+
console.log("\uD83D\uDCCB BACKLOG");
|
|
12889
12878
|
console.log("══════════════════════════════════════════════════");
|
|
12890
12879
|
console.log(`Chatroom: ${chatroomId}`);
|
|
12891
12880
|
console.log("");
|
|
12892
12881
|
if (backlogItems.length === 0) {
|
|
12893
|
-
console.log("No
|
|
12882
|
+
console.log("No backlog items.");
|
|
12894
12883
|
} else {
|
|
12895
12884
|
console.log("──────────────────────────────────────────────────");
|
|
12896
12885
|
for (let i2 = 0;i2 < backlogItems.length; i2++) {
|
|
@@ -13173,7 +13162,7 @@ async function historyBacklog(chatroomId, options, deps) {
|
|
|
13173
13162
|
const sessionId = requireAuth2(d);
|
|
13174
13163
|
validateChatroomId(chatroomId);
|
|
13175
13164
|
const now = Date.now();
|
|
13176
|
-
const defaultFrom = now -
|
|
13165
|
+
const defaultFrom = now - 2592000000;
|
|
13177
13166
|
let fromMs;
|
|
13178
13167
|
let toMs;
|
|
13179
13168
|
if (options.from) {
|
|
@@ -13263,6 +13252,38 @@ async function historyBacklog(chatroomId, options, deps) {
|
|
|
13263
13252
|
return;
|
|
13264
13253
|
}
|
|
13265
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
|
+
}
|
|
13266
13287
|
function getStatusEmoji(status) {
|
|
13267
13288
|
switch (status) {
|
|
13268
13289
|
case "pending":
|
|
@@ -13283,10 +13304,121 @@ function getStatusEmoji(status) {
|
|
|
13283
13304
|
return "⚫";
|
|
13284
13305
|
}
|
|
13285
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";
|
|
13286
13417
|
var init_backlog = __esm(() => {
|
|
13287
13418
|
init_api3();
|
|
13288
13419
|
init_storage();
|
|
13289
13420
|
init_client2();
|
|
13421
|
+
STALENESS_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1000;
|
|
13290
13422
|
});
|
|
13291
13423
|
|
|
13292
13424
|
// src/utils/file-content.ts
|
|
@@ -13378,6 +13510,32 @@ async function taskRead(chatroomId, options, deps) {
|
|
|
13378
13510
|
console.log(`✅ Task content:`);
|
|
13379
13511
|
console.log(` Task ID: ${result.taskId}`);
|
|
13380
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
|
+
}
|
|
13381
13539
|
console.log(`
|
|
13382
13540
|
${result.content}`);
|
|
13383
13541
|
} catch (error) {
|
|
@@ -14280,310 +14438,98 @@ var init_get_system_prompt = __esm(() => {
|
|
|
14280
14438
|
// ../../services/backend/config/reliability.ts
|
|
14281
14439
|
var DAEMON_HEARTBEAT_INTERVAL_MS = 30000, AGENT_REQUEST_DEADLINE_MS = 120000;
|
|
14282
14440
|
|
|
14283
|
-
// src/
|
|
14284
|
-
function
|
|
14285
|
-
|
|
14286
|
-
|
|
14287
|
-
|
|
14288
|
-
|
|
14289
|
-
async function onAgentShutdown(ctx, options) {
|
|
14290
|
-
const { chatroomId, role, pid, skipKill } = options;
|
|
14291
|
-
try {
|
|
14292
|
-
ctx.deps.stops.mark(chatroomId, role, options.stopReason ?? "user.stop");
|
|
14293
|
-
} catch (e) {
|
|
14294
|
-
console.log(` ⚠️ Failed to mark intentional stop for ${role}: ${e.message}`);
|
|
14295
|
-
}
|
|
14296
|
-
let killed = false;
|
|
14297
|
-
if (!skipKill) {
|
|
14298
|
-
try {
|
|
14299
|
-
ctx.deps.processes.kill(-pid, "SIGTERM");
|
|
14300
|
-
} catch (e) {
|
|
14301
|
-
const isEsrch = e.code === "ESRCH" || e.message?.includes("ESRCH");
|
|
14302
|
-
if (isEsrch) {
|
|
14303
|
-
killed = true;
|
|
14304
|
-
}
|
|
14305
|
-
if (!isEsrch) {
|
|
14306
|
-
console.log(` ⚠️ Failed to send SIGTERM to ${role}: ${e.message}`);
|
|
14307
|
-
}
|
|
14308
|
-
}
|
|
14309
|
-
if (!killed) {
|
|
14310
|
-
const SIGTERM_TIMEOUT_MS = 1e4;
|
|
14311
|
-
const POLL_INTERVAL_MS2 = 500;
|
|
14312
|
-
const deadline = Date.now() + SIGTERM_TIMEOUT_MS;
|
|
14313
|
-
while (Date.now() < deadline) {
|
|
14314
|
-
await ctx.deps.clock.delay(POLL_INTERVAL_MS2);
|
|
14315
|
-
try {
|
|
14316
|
-
ctx.deps.processes.kill(pid, 0);
|
|
14317
|
-
} catch {
|
|
14318
|
-
killed = true;
|
|
14319
|
-
break;
|
|
14320
|
-
}
|
|
14321
|
-
}
|
|
14322
|
-
}
|
|
14323
|
-
if (!killed) {
|
|
14324
|
-
try {
|
|
14325
|
-
ctx.deps.processes.kill(-pid, "SIGKILL");
|
|
14326
|
-
} catch {
|
|
14327
|
-
killed = true;
|
|
14328
|
-
}
|
|
14329
|
-
}
|
|
14330
|
-
if (!killed) {
|
|
14331
|
-
await ctx.deps.clock.delay(5000);
|
|
14332
|
-
try {
|
|
14333
|
-
ctx.deps.processes.kill(pid, 0);
|
|
14334
|
-
console.log(` ⚠️ Process ${pid} (${role}) still alive after SIGKILL — possible zombie`);
|
|
14335
|
-
} catch {
|
|
14336
|
-
killed = true;
|
|
14337
|
-
}
|
|
14338
|
-
}
|
|
14339
|
-
}
|
|
14340
|
-
if (killed || skipKill) {
|
|
14341
|
-
try {
|
|
14342
|
-
ctx.deps.machine.clearAgentPid(ctx.machineId, chatroomId, role);
|
|
14343
|
-
} catch (e) {
|
|
14344
|
-
console.log(` ⚠️ Failed to clear local PID for ${role}: ${e.message}`);
|
|
14345
|
-
}
|
|
14346
|
-
}
|
|
14347
|
-
return {
|
|
14348
|
-
killed: killed || (skipKill ?? false),
|
|
14349
|
-
cleaned: killed || (skipKill ?? false)
|
|
14350
|
-
};
|
|
14351
|
-
}
|
|
14352
|
-
|
|
14353
|
-
// src/events/lifecycle/on-daemon-shutdown.ts
|
|
14354
|
-
async function onDaemonShutdown(ctx) {
|
|
14355
|
-
const agents = ctx.deps.machine.listAgentEntries(ctx.machineId);
|
|
14356
|
-
if (agents.length > 0) {
|
|
14357
|
-
console.log(`[${formatTimestamp()}] Stopping ${agents.length} agent(s)...`);
|
|
14358
|
-
await Promise.allSettled(agents.map(async ({ chatroomId, role, entry }) => {
|
|
14359
|
-
const result = await onAgentShutdown(ctx, {
|
|
14360
|
-
chatroomId,
|
|
14361
|
-
role,
|
|
14362
|
-
pid: entry.pid
|
|
14363
|
-
});
|
|
14364
|
-
if (result.killed) {
|
|
14365
|
-
console.log(` Sent SIGTERM to ${role} (PID ${entry.pid})`);
|
|
14366
|
-
} else {
|
|
14367
|
-
console.log(` ${role} (PID ${entry.pid}) already exited`);
|
|
14368
|
-
}
|
|
14369
|
-
return result;
|
|
14370
|
-
}));
|
|
14371
|
-
await ctx.deps.clock.delay(AGENT_SHUTDOWN_TIMEOUT_MS);
|
|
14372
|
-
for (const { role, entry } of agents) {
|
|
14373
|
-
try {
|
|
14374
|
-
ctx.deps.processes.kill(entry.pid, 0);
|
|
14375
|
-
ctx.deps.processes.kill(entry.pid, "SIGKILL");
|
|
14376
|
-
console.log(` Force-killed ${role} (PID ${entry.pid})`);
|
|
14377
|
-
} catch {}
|
|
14378
|
-
}
|
|
14379
|
-
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;
|
|
14380
14447
|
}
|
|
14381
|
-
|
|
14382
|
-
|
|
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, {
|
|
14383
14461
|
sessionId: ctx.sessionId,
|
|
14462
|
+
chatroomId: event.chatroomId,
|
|
14384
14463
|
machineId: ctx.machineId,
|
|
14385
|
-
|
|
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}`);
|
|
14386
14469
|
});
|
|
14387
|
-
}
|
|
14470
|
+
}
|
|
14388
14471
|
}
|
|
14389
|
-
var
|
|
14390
|
-
var init_on_daemon_shutdown = __esm(() => {
|
|
14472
|
+
var init_on_request_start_agent = __esm(() => {
|
|
14391
14473
|
init_api3();
|
|
14392
14474
|
});
|
|
14393
14475
|
|
|
14394
|
-
// src/commands/machine/daemon-start/handlers/
|
|
14395
|
-
async function
|
|
14396
|
-
|
|
14397
|
-
|
|
14398
|
-
|
|
14399
|
-
|
|
14400
|
-
|
|
14401
|
-
|
|
14402
|
-
|
|
14403
|
-
|
|
14404
|
-
|
|
14405
|
-
|
|
14406
|
-
}
|
|
14407
|
-
|
|
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 };
|
|
14408
14491
|
}
|
|
14409
|
-
var init_shared = __esm(() => {
|
|
14410
|
-
init_api3();
|
|
14411
|
-
});
|
|
14412
14492
|
|
|
14413
|
-
// src/
|
|
14414
|
-
async function
|
|
14415
|
-
|
|
14416
|
-
|
|
14417
|
-
|
|
14418
|
-
} else {
|
|
14419
|
-
let recovered = 0;
|
|
14420
|
-
let cleared = 0;
|
|
14421
|
-
const chatroomIds = new Set;
|
|
14422
|
-
for (const { chatroomId, role, entry } of entries) {
|
|
14423
|
-
const { pid, harness } = entry;
|
|
14424
|
-
const service = ctx.agentServices.get(harness) ?? ctx.agentServices.values().next().value;
|
|
14425
|
-
const alive = service ? service.isAlive(pid) : false;
|
|
14426
|
-
if (alive) {
|
|
14427
|
-
console.log(` ✅ Recovered: ${role} (PID ${pid}, harness: ${harness})`);
|
|
14428
|
-
recovered++;
|
|
14429
|
-
chatroomIds.add(chatroomId);
|
|
14430
|
-
} else {
|
|
14431
|
-
console.log(` \uD83E\uDDF9 Stale PID ${pid} for ${role} — clearing`);
|
|
14432
|
-
await clearAgentPidEverywhere(ctx, chatroomId, role);
|
|
14433
|
-
cleared++;
|
|
14434
|
-
}
|
|
14435
|
-
}
|
|
14436
|
-
console.log(` Recovery complete: ${recovered} alive, ${cleared} stale cleared`);
|
|
14437
|
-
for (const chatroomId of chatroomIds) {
|
|
14438
|
-
try {
|
|
14439
|
-
const configsResult = await ctx.deps.backend.query(api.machines.getMachineAgentConfigs, {
|
|
14440
|
-
sessionId: ctx.sessionId,
|
|
14441
|
-
chatroomId
|
|
14442
|
-
});
|
|
14443
|
-
for (const config3 of configsResult.configs) {
|
|
14444
|
-
if (config3.machineId === ctx.machineId && config3.workingDir) {
|
|
14445
|
-
ctx.activeWorkingDirs.add(config3.workingDir);
|
|
14446
|
-
}
|
|
14447
|
-
}
|
|
14448
|
-
} catch {}
|
|
14449
|
-
}
|
|
14450
|
-
if (ctx.activeWorkingDirs.size > 0) {
|
|
14451
|
-
console.log(` \uD83D\uDD00 Recovered ${ctx.activeWorkingDirs.size} active working dir(s) for git tracking`);
|
|
14452
|
-
}
|
|
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;
|
|
14453
14498
|
}
|
|
14499
|
+
await executeStopAgent(ctx, {
|
|
14500
|
+
chatroomId: event.chatroomId,
|
|
14501
|
+
role: event.role,
|
|
14502
|
+
reason: event.reason
|
|
14503
|
+
});
|
|
14454
14504
|
}
|
|
14455
|
-
var
|
|
14456
|
-
init_api3();
|
|
14457
|
-
init_shared();
|
|
14458
|
-
});
|
|
14505
|
+
var init_on_request_stop_agent = () => {};
|
|
14459
14506
|
|
|
14460
|
-
// src/
|
|
14461
|
-
|
|
14462
|
-
|
|
14463
|
-
|
|
14464
|
-
|
|
14465
|
-
|
|
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 });
|
|
14466
14522
|
}
|
|
14467
|
-
|
|
14468
|
-
|
|
14469
|
-
|
|
14470
|
-
|
|
14471
|
-
|
|
14472
|
-
|
|
14473
|
-
|
|
14474
|
-
|
|
14475
|
-
|
|
14476
|
-
|
|
14477
|
-
return { allowed: false, retryAfterMs };
|
|
14478
|
-
}
|
|
14479
|
-
bucket.tokens -= 1;
|
|
14480
|
-
const remaining = Math.floor(bucket.tokens);
|
|
14481
|
-
if (remaining <= LOW_TOKEN_THRESHOLD) {
|
|
14482
|
-
console.warn(`⚠️ [RateLimiter] Agent spawn tokens running low for chatroom ${chatroomId} (${remaining}/${this.config.maxTokens} remaining)`);
|
|
14483
|
-
}
|
|
14484
|
-
return { allowed: true };
|
|
14485
|
-
}
|
|
14486
|
-
getStatus(chatroomId) {
|
|
14487
|
-
const bucket = this._getOrCreateBucket(chatroomId);
|
|
14488
|
-
this._refill(bucket);
|
|
14489
|
-
return {
|
|
14490
|
-
remaining: Math.floor(bucket.tokens),
|
|
14491
|
-
total: this.config.maxTokens
|
|
14492
|
-
};
|
|
14493
|
-
}
|
|
14494
|
-
_getOrCreateBucket(chatroomId) {
|
|
14495
|
-
if (!this.buckets.has(chatroomId)) {
|
|
14496
|
-
this.buckets.set(chatroomId, {
|
|
14497
|
-
tokens: this.config.initialTokens,
|
|
14498
|
-
lastRefillAt: Date.now()
|
|
14499
|
-
});
|
|
14500
|
-
}
|
|
14501
|
-
return this.buckets.get(chatroomId);
|
|
14502
|
-
}
|
|
14503
|
-
_refill(bucket) {
|
|
14504
|
-
const now = Date.now();
|
|
14505
|
-
const elapsed = now - bucket.lastRefillAt;
|
|
14506
|
-
if (elapsed >= this.config.refillRateMs) {
|
|
14507
|
-
const tokensToAdd = Math.floor(elapsed / this.config.refillRateMs);
|
|
14508
|
-
bucket.tokens = Math.min(this.config.maxTokens, bucket.tokens + tokensToAdd);
|
|
14509
|
-
bucket.lastRefillAt += tokensToAdd * this.config.refillRateMs;
|
|
14510
|
-
}
|
|
14511
|
-
}
|
|
14512
|
-
}
|
|
14513
|
-
var DEFAULT_CONFIG, LOW_TOKEN_THRESHOLD = 1;
|
|
14514
|
-
var init_rate_limiter = __esm(() => {
|
|
14515
|
-
DEFAULT_CONFIG = {
|
|
14516
|
-
maxTokens: 5,
|
|
14517
|
-
refillRateMs: 60000,
|
|
14518
|
-
initialTokens: 5
|
|
14519
|
-
};
|
|
14520
|
-
});
|
|
14521
|
-
|
|
14522
|
-
// src/infrastructure/services/harness-spawning/harness-spawning-service.ts
|
|
14523
|
-
class HarnessSpawningService {
|
|
14524
|
-
rateLimiter;
|
|
14525
|
-
concurrentAgents = new Map;
|
|
14526
|
-
constructor({ rateLimiter }) {
|
|
14527
|
-
this.rateLimiter = rateLimiter;
|
|
14528
|
-
}
|
|
14529
|
-
shouldAllowSpawn(chatroomId, reason) {
|
|
14530
|
-
const current = this.concurrentAgents.get(chatroomId) ?? 0;
|
|
14531
|
-
if (current >= MAX_CONCURRENT_AGENTS_PER_CHATROOM) {
|
|
14532
|
-
console.warn(`⚠️ [HarnessSpawningService] Concurrent agent limit reached for chatroom ${chatroomId} ` + `(${current}/${MAX_CONCURRENT_AGENTS_PER_CHATROOM} active agents). Spawn rejected.`);
|
|
14533
|
-
return { allowed: false };
|
|
14534
|
-
}
|
|
14535
|
-
const result = this.rateLimiter.tryConsume(chatroomId, reason);
|
|
14536
|
-
if (!result.allowed) {
|
|
14537
|
-
console.warn(`⚠️ [HarnessSpawningService] Spawn blocked by rate limiter for chatroom ${chatroomId} ` + `(reason: ${reason}).`);
|
|
14538
|
-
}
|
|
14539
|
-
return result;
|
|
14540
|
-
}
|
|
14541
|
-
recordSpawn(chatroomId) {
|
|
14542
|
-
const current = this.concurrentAgents.get(chatroomId) ?? 0;
|
|
14543
|
-
this.concurrentAgents.set(chatroomId, current + 1);
|
|
14544
|
-
}
|
|
14545
|
-
recordExit(chatroomId) {
|
|
14546
|
-
const current = this.concurrentAgents.get(chatroomId) ?? 0;
|
|
14547
|
-
const next = Math.max(0, current - 1);
|
|
14548
|
-
this.concurrentAgents.set(chatroomId, next);
|
|
14549
|
-
}
|
|
14550
|
-
getConcurrentCount(chatroomId) {
|
|
14551
|
-
return this.concurrentAgents.get(chatroomId) ?? 0;
|
|
14552
|
-
}
|
|
14553
|
-
}
|
|
14554
|
-
var MAX_CONCURRENT_AGENTS_PER_CHATROOM = 10;
|
|
14555
|
-
|
|
14556
|
-
// src/infrastructure/services/harness-spawning/index.ts
|
|
14557
|
-
var init_harness_spawning = __esm(() => {
|
|
14558
|
-
init_rate_limiter();
|
|
14559
|
-
});
|
|
14560
|
-
|
|
14561
|
-
// src/commands/machine/pid.ts
|
|
14562
|
-
import { createHash } from "node:crypto";
|
|
14563
|
-
import { existsSync as existsSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4, unlinkSync as unlinkSync2, mkdirSync as mkdirSync4 } from "node:fs";
|
|
14564
|
-
import { homedir as homedir4 } from "node:os";
|
|
14565
|
-
import { join as join5 } from "node:path";
|
|
14566
|
-
function getUrlHash() {
|
|
14567
|
-
const url = getConvexUrl();
|
|
14568
|
-
return createHash("sha256").update(url).digest("hex").substring(0, 8);
|
|
14569
|
-
}
|
|
14570
|
-
function getPidFileName() {
|
|
14571
|
-
return `daemon-${getUrlHash()}.pid`;
|
|
14572
|
-
}
|
|
14573
|
-
function ensureChatroomDir() {
|
|
14574
|
-
if (!existsSync4(CHATROOM_DIR4)) {
|
|
14575
|
-
mkdirSync4(CHATROOM_DIR4, { recursive: true, mode: 448 });
|
|
14576
|
-
}
|
|
14577
|
-
}
|
|
14578
|
-
function getPidFilePath() {
|
|
14579
|
-
return join5(CHATROOM_DIR4, getPidFileName());
|
|
14580
|
-
}
|
|
14581
|
-
function isProcessRunning(pid) {
|
|
14582
|
-
try {
|
|
14583
|
-
process.kill(pid, 0);
|
|
14584
|
-
return true;
|
|
14585
|
-
} catch {
|
|
14586
|
-
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;
|
|
14587
14533
|
}
|
|
14588
14534
|
}
|
|
14589
14535
|
function readPid() {
|
|
@@ -14641,852 +14587,566 @@ function releaseLock() {
|
|
|
14641
14587
|
var CHATROOM_DIR4;
|
|
14642
14588
|
var init_pid = __esm(() => {
|
|
14643
14589
|
init_client2();
|
|
14644
|
-
CHATROOM_DIR4 =
|
|
14590
|
+
CHATROOM_DIR4 = join6(homedir4(), ".chatroom");
|
|
14645
14591
|
});
|
|
14646
14592
|
|
|
14647
|
-
// src/
|
|
14648
|
-
|
|
14649
|
-
|
|
14650
|
-
on(event, listener) {
|
|
14651
|
-
if (!this.listeners.has(event)) {
|
|
14652
|
-
this.listeners.set(event, new Set);
|
|
14653
|
-
}
|
|
14654
|
-
this.listeners.get(event).add(listener);
|
|
14655
|
-
return () => {
|
|
14656
|
-
this.listeners.get(event)?.delete(listener);
|
|
14657
|
-
};
|
|
14658
|
-
}
|
|
14659
|
-
emit(event, payload) {
|
|
14660
|
-
const set = this.listeners.get(event);
|
|
14661
|
-
if (!set)
|
|
14662
|
-
return;
|
|
14663
|
-
for (const listener of set) {
|
|
14664
|
-
try {
|
|
14665
|
-
listener(payload);
|
|
14666
|
-
} catch (err) {
|
|
14667
|
-
console.warn(`[EventBus] Listener error on "${event}": ${err.message}`);
|
|
14668
|
-
}
|
|
14669
|
-
}
|
|
14670
|
-
}
|
|
14671
|
-
removeAllListeners() {
|
|
14672
|
-
this.listeners.clear();
|
|
14673
|
-
}
|
|
14593
|
+
// src/commands/machine/daemon-start/utils.ts
|
|
14594
|
+
function formatTimestamp() {
|
|
14595
|
+
return new Date().toISOString().replace("T", " ").substring(0, 19);
|
|
14674
14596
|
}
|
|
14675
14597
|
|
|
14676
|
-
// src/
|
|
14677
|
-
function
|
|
14678
|
-
|
|
14679
|
-
const ts = formatTimestamp();
|
|
14680
|
-
console.log(`[${ts}] Agent stopped: ${stopReason} (${role})`);
|
|
14681
|
-
const isDaemonRespawn = stopReason === "daemon.respawn";
|
|
14682
|
-
const isIntentional = intentional && !isDaemonRespawn;
|
|
14683
|
-
if (isIntentional) {
|
|
14684
|
-
console.log(`[${ts}] ℹ️ Agent process exited after intentional stop ` + `(PID: ${pid}, role: ${role}, code: ${code2}, signal: ${signal})`);
|
|
14685
|
-
} else if (isDaemonRespawn) {
|
|
14686
|
-
console.log(`[${ts}] \uD83D\uDD04 Agent process stopped for respawn ` + `(PID: ${pid}, role: ${role}, code: ${code2}, signal: ${signal})`);
|
|
14687
|
-
} else {
|
|
14688
|
-
console.log(`[${ts}] ⚠️ Agent process exited ` + `(PID: ${pid}, role: ${role}, code: ${code2}, signal: ${signal})`);
|
|
14689
|
-
}
|
|
14690
|
-
ctx.deps.backend.mutation(api.machines.recordAgentExited, {
|
|
14691
|
-
sessionId: ctx.sessionId,
|
|
14692
|
-
machineId: ctx.machineId,
|
|
14693
|
-
chatroomId,
|
|
14694
|
-
role,
|
|
14695
|
-
pid,
|
|
14696
|
-
intentional,
|
|
14697
|
-
stopReason,
|
|
14698
|
-
stopSignal: stopReason === "agent_process.signal" ? signal ?? undefined : undefined,
|
|
14699
|
-
exitCode: code2 ?? undefined,
|
|
14700
|
-
signal: signal ?? undefined
|
|
14701
|
-
}).catch((err) => {
|
|
14702
|
-
console.log(` ⚠️ Failed to record agent exit event: ${err.message}`);
|
|
14703
|
-
});
|
|
14704
|
-
ctx.deps.machine.clearAgentPid(ctx.machineId, chatroomId, role);
|
|
14705
|
-
for (const service of ctx.agentServices.values()) {
|
|
14706
|
-
service.untrack(pid);
|
|
14707
|
-
}
|
|
14598
|
+
// src/infrastructure/git/types.ts
|
|
14599
|
+
function makeGitStateKey(machineId, workingDir) {
|
|
14600
|
+
return `${machineId}::${workingDir}`;
|
|
14708
14601
|
}
|
|
14709
|
-
var
|
|
14710
|
-
init_api3();
|
|
14711
|
-
});
|
|
14602
|
+
var FULL_DIFF_MAX_BYTES = 500000, COMMITS_PER_PAGE = 20;
|
|
14712
14603
|
|
|
14713
|
-
// src/
|
|
14714
|
-
|
|
14715
|
-
|
|
14716
|
-
|
|
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
|
+
}
|
|
14717
14618
|
}
|
|
14718
|
-
|
|
14719
|
-
|
|
14720
|
-
// src/events/daemon/agent/on-agent-stopped.ts
|
|
14721
|
-
function onAgentStopped(ctx, payload) {
|
|
14722
|
-
const ts = formatTimestamp();
|
|
14723
|
-
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");
|
|
14724
14621
|
}
|
|
14725
|
-
|
|
14726
|
-
|
|
14727
|
-
// src/events/daemon/register-listeners.ts
|
|
14728
|
-
function registerEventListeners(ctx) {
|
|
14729
|
-
const unsubs = [];
|
|
14730
|
-
unsubs.push(ctx.events.on("agent:exited", (payload) => onAgentExited(ctx, payload)));
|
|
14731
|
-
unsubs.push(ctx.events.on("agent:started", (payload) => onAgentStarted(ctx, payload)));
|
|
14732
|
-
unsubs.push(ctx.events.on("agent:stopped", (payload) => onAgentStopped(ctx, payload)));
|
|
14733
|
-
return () => {
|
|
14734
|
-
for (const unsub of unsubs) {
|
|
14735
|
-
unsub();
|
|
14736
|
-
}
|
|
14737
|
-
};
|
|
14622
|
+
function isNotAGitRepo(message) {
|
|
14623
|
+
return message.includes("not a git repository") || message.includes("Not a git repository");
|
|
14738
14624
|
}
|
|
14739
|
-
|
|
14740
|
-
|
|
14741
|
-
|
|
14742
|
-
|
|
14743
|
-
|
|
14744
|
-
|
|
14745
|
-
|
|
14746
|
-
|
|
14747
|
-
|
|
14748
|
-
|
|
14749
|
-
|
|
14750
|
-
|
|
14751
|
-
|
|
14752
|
-
|
|
14753
|
-
|
|
14754
|
-
|
|
14755
|
-
|
|
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" };
|
|
14756
14655
|
}
|
|
14656
|
+
return classifyError(errMsg);
|
|
14757
14657
|
}
|
|
14758
|
-
|
|
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 };
|
|
14759
14663
|
}
|
|
14760
|
-
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/);
|
|
14761
14674
|
return {
|
|
14762
|
-
|
|
14763
|
-
|
|
14764
|
-
|
|
14765
|
-
},
|
|
14766
|
-
query: async () => {
|
|
14767
|
-
throw new Error("Backend not initialized");
|
|
14768
|
-
}
|
|
14769
|
-
},
|
|
14770
|
-
processes: {
|
|
14771
|
-
kill: (pid, signal) => process.kill(pid, signal)
|
|
14772
|
-
},
|
|
14773
|
-
fs: {
|
|
14774
|
-
stat
|
|
14775
|
-
},
|
|
14776
|
-
stops: {
|
|
14777
|
-
mark: markIntentionalStop,
|
|
14778
|
-
consume: consumeIntentionalStop,
|
|
14779
|
-
clear: clearIntentionalStop
|
|
14780
|
-
},
|
|
14781
|
-
machine: {
|
|
14782
|
-
clearAgentPid,
|
|
14783
|
-
persistAgentPid,
|
|
14784
|
-
listAgentEntries,
|
|
14785
|
-
persistEventCursor,
|
|
14786
|
-
loadEventCursor
|
|
14787
|
-
},
|
|
14788
|
-
clock: {
|
|
14789
|
-
now: () => Date.now(),
|
|
14790
|
-
delay: (ms) => new Promise((resolve2) => setTimeout(resolve2, ms))
|
|
14791
|
-
},
|
|
14792
|
-
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
|
|
14793
14678
|
};
|
|
14794
14679
|
}
|
|
14795
|
-
function
|
|
14796
|
-
const
|
|
14797
|
-
if (
|
|
14798
|
-
const
|
|
14799
|
-
|
|
14800
|
-
|
|
14801
|
-
console.error(`
|
|
14802
|
-
\uD83D\uDCA1 You have sessions for other environments:`);
|
|
14803
|
-
for (const url of otherUrls) {
|
|
14804
|
-
console.error(` • ${url}`);
|
|
14805
|
-
}
|
|
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" };
|
|
14806
14686
|
}
|
|
14807
|
-
|
|
14808
|
-
|
|
14809
|
-
|
|
14810
|
-
|
|
14687
|
+
const classified = classifyError(errMsg);
|
|
14688
|
+
if (classified.status === "not_found")
|
|
14689
|
+
return { status: "not_found" };
|
|
14690
|
+
return classified;
|
|
14811
14691
|
}
|
|
14812
|
-
|
|
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 };
|
|
14813
14708
|
}
|
|
14814
|
-
async function
|
|
14815
|
-
const
|
|
14816
|
-
if (
|
|
14817
|
-
|
|
14818
|
-
|
|
14819
|
-
|
|
14820
|
-
|
|
14821
|
-
|
|
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 };
|
|
14822
14730
|
}
|
|
14731
|
+
return { status: "available", content: raw, truncated: false };
|
|
14823
14732
|
}
|
|
14824
|
-
function
|
|
14825
|
-
|
|
14826
|
-
const
|
|
14827
|
-
|
|
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;
|
|
14828
14756
|
}
|
|
14829
|
-
async function
|
|
14830
|
-
const
|
|
14831
|
-
|
|
14832
|
-
|
|
14833
|
-
|
|
14834
|
-
|
|
14835
|
-
|
|
14836
|
-
|
|
14837
|
-
|
|
14838
|
-
|
|
14839
|
-
|
|
14840
|
-
availableModels
|
|
14841
|
-
});
|
|
14842
|
-
} catch (error) {
|
|
14843
|
-
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;
|
|
14844
14768
|
}
|
|
14845
|
-
|
|
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 };
|
|
14846
14776
|
}
|
|
14847
|
-
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) {
|
|
14848
14791
|
try {
|
|
14849
|
-
await
|
|
14850
|
-
|
|
14851
|
-
|
|
14852
|
-
|
|
14792
|
+
const result = await execAsync2(command, {
|
|
14793
|
+
cwd,
|
|
14794
|
+
env: { ...process.env, NO_COLOR: "1" },
|
|
14795
|
+
timeout: 15000
|
|
14853
14796
|
});
|
|
14854
|
-
|
|
14855
|
-
|
|
14856
|
-
|
|
14857
|
-
} else {
|
|
14858
|
-
console.error(`❌ Failed to update daemon status: ${error.message}`);
|
|
14859
|
-
}
|
|
14860
|
-
releaseLock();
|
|
14861
|
-
process.exit(1);
|
|
14797
|
+
return result;
|
|
14798
|
+
} catch (err) {
|
|
14799
|
+
return { error: err };
|
|
14862
14800
|
}
|
|
14863
14801
|
}
|
|
14864
|
-
function
|
|
14865
|
-
|
|
14866
|
-
|
|
14867
|
-
|
|
14868
|
-
|
|
14869
|
-
|
|
14870
|
-
|
|
14871
|
-
|
|
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;
|
|
14872
14813
|
}
|
|
14873
|
-
async function
|
|
14874
|
-
|
|
14875
|
-
|
|
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 [];
|
|
14876
14833
|
try {
|
|
14877
|
-
|
|
14878
|
-
|
|
14879
|
-
|
|
14880
|
-
|
|
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 [];
|
|
14881
14846
|
}
|
|
14882
14847
|
}
|
|
14883
|
-
|
|
14884
|
-
|
|
14885
|
-
|
|
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
|
+
}
|
|
14886
14889
|
}
|
|
14887
|
-
|
|
14888
|
-
const sessionId = validateAuthentication(convexUrl);
|
|
14889
|
-
const client2 = await getConvexClient();
|
|
14890
|
-
const typedSessionId = sessionId;
|
|
14891
|
-
await validateSession(client2, typedSessionId, convexUrl);
|
|
14892
|
-
const config3 = setupMachine();
|
|
14893
|
-
const { machineId } = config3;
|
|
14894
|
-
initHarnessRegistry();
|
|
14895
|
-
const agentServices = new Map(getAllHarnesses().map((s) => [s.id, s]));
|
|
14896
|
-
const availableModels = await registerCapabilities(client2, typedSessionId, config3, agentServices);
|
|
14897
|
-
await connectDaemon(client2, typedSessionId, machineId, convexUrl);
|
|
14898
|
-
const deps = createDefaultDeps19();
|
|
14899
|
-
deps.backend.mutation = (endpoint, args) => client2.mutation(endpoint, args);
|
|
14900
|
-
deps.backend.query = (endpoint, args) => client2.query(endpoint, args);
|
|
14901
|
-
const events = new DaemonEventBus;
|
|
14902
|
-
const ctx = {
|
|
14903
|
-
client: client2,
|
|
14904
|
-
sessionId: typedSessionId,
|
|
14905
|
-
machineId,
|
|
14906
|
-
config: config3,
|
|
14907
|
-
deps,
|
|
14908
|
-
events,
|
|
14909
|
-
agentServices,
|
|
14910
|
-
activeWorkingDirs: new Set,
|
|
14911
|
-
lastPushedGitState: new Map,
|
|
14912
|
-
agentEndedTurn: new Map
|
|
14913
|
-
};
|
|
14914
|
-
registerEventListeners(ctx);
|
|
14915
|
-
logStartup(ctx, availableModels);
|
|
14916
|
-
await recoverState(ctx);
|
|
14917
|
-
return ctx;
|
|
14918
|
-
}
|
|
14919
|
-
var init_init2 = __esm(() => {
|
|
14920
|
-
init_state_recovery();
|
|
14921
|
-
init_api3();
|
|
14922
|
-
init_storage();
|
|
14923
|
-
init_client2();
|
|
14924
|
-
init_machine();
|
|
14925
|
-
init_intentional_stops();
|
|
14926
|
-
init_remote_agents();
|
|
14927
|
-
init_harness_spawning();
|
|
14928
|
-
init_error_formatting();
|
|
14929
|
-
init_version();
|
|
14930
|
-
init_pid();
|
|
14931
|
-
init_register_listeners();
|
|
14932
|
-
});
|
|
14933
|
-
|
|
14934
|
-
// src/infrastructure/machine/stop-reason.ts
|
|
14935
|
-
function resolveStopReason(code2, signal, wasIntentional) {
|
|
14936
|
-
if (wasIntentional)
|
|
14937
|
-
return "user.stop";
|
|
14938
|
-
if (signal !== null)
|
|
14939
|
-
return "agent_process.signal";
|
|
14940
|
-
if (code2 === 0)
|
|
14941
|
-
return "agent_process.exited_clean";
|
|
14942
|
-
return "agent_process.crashed";
|
|
14890
|
+
return { filesChanged: 0, insertions: 0, deletions: 0 };
|
|
14943
14891
|
}
|
|
14944
|
-
|
|
14945
|
-
|
|
14946
|
-
|
|
14947
|
-
|
|
14948
|
-
|
|
14949
|
-
|
|
14950
|
-
console.log(` Role: ${role}`);
|
|
14951
|
-
console.log(` Harness: ${agentHarness}`);
|
|
14952
|
-
if (reason) {
|
|
14953
|
-
console.log(` Reason: ${reason}`);
|
|
14954
|
-
}
|
|
14955
|
-
if (model) {
|
|
14956
|
-
console.log(` Model: ${model}`);
|
|
14957
|
-
}
|
|
14958
|
-
if (!workingDir) {
|
|
14959
|
-
const msg2 = `No workingDir provided in command payload for ${chatroomId}/${role}`;
|
|
14960
|
-
console.log(` ⚠️ ${msg2}`);
|
|
14961
|
-
return { result: msg2, failed: true };
|
|
14962
|
-
}
|
|
14963
|
-
console.log(` Working dir: ${workingDir}`);
|
|
14964
|
-
try {
|
|
14965
|
-
const dirStat = await ctx.deps.fs.stat(workingDir);
|
|
14966
|
-
if (!dirStat.isDirectory()) {
|
|
14967
|
-
const msg2 = `Working directory is not a directory: ${workingDir}`;
|
|
14968
|
-
console.log(` ⚠️ ${msg2}`);
|
|
14969
|
-
return { result: msg2, failed: true };
|
|
14970
|
-
}
|
|
14971
|
-
} catch {
|
|
14972
|
-
const msg2 = `Working directory does not exist: ${workingDir}`;
|
|
14973
|
-
console.log(` ⚠️ ${msg2}`);
|
|
14974
|
-
return { result: msg2, failed: true };
|
|
14975
|
-
}
|
|
14976
|
-
try {
|
|
14977
|
-
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, {
|
|
14978
14898
|
sessionId: ctx.sessionId,
|
|
14979
|
-
|
|
14980
|
-
|
|
14981
|
-
|
|
14982
|
-
|
|
14983
|
-
|
|
14984
|
-
const localPid = localEntry?.entry.pid;
|
|
14985
|
-
const pidsToKill = [
|
|
14986
|
-
...new Set([backendPid, localPid].filter((p) => p !== undefined))
|
|
14987
|
-
];
|
|
14988
|
-
const anyService = ctx.agentServices.values().next().value;
|
|
14989
|
-
for (const pid2 of pidsToKill) {
|
|
14990
|
-
const isAlive = anyService ? anyService.isAlive(pid2) : false;
|
|
14991
|
-
if (isAlive) {
|
|
14992
|
-
console.log(` ⚠️ Existing agent detected (PID: ${pid2}) — stopping before respawn`);
|
|
14993
|
-
await onAgentShutdown(ctx, { chatroomId, role, pid: pid2, stopReason: "daemon.respawn" });
|
|
14994
|
-
console.log(` ✅ Existing agent stopped (PID: ${pid2})`);
|
|
14995
|
-
}
|
|
14996
|
-
}
|
|
14997
|
-
} catch (e) {
|
|
14998
|
-
console.log(` ⚠️ Could not check for existing agent (proceeding): ${e.message}`);
|
|
14999
|
-
}
|
|
15000
|
-
const convexUrl = getConvexUrl();
|
|
15001
|
-
const initPromptResult = await ctx.deps.backend.query(api.messages.getInitPrompt, {
|
|
15002
|
-
sessionId: ctx.sessionId,
|
|
15003
|
-
chatroomId,
|
|
15004
|
-
role,
|
|
15005
|
-
convexUrl
|
|
15006
|
-
});
|
|
15007
|
-
if (!initPromptResult?.prompt) {
|
|
15008
|
-
const msg2 = "Failed to fetch init prompt from backend";
|
|
15009
|
-
console.log(` ⚠️ ${msg2}`);
|
|
15010
|
-
return { result: msg2, failed: true };
|
|
15011
|
-
}
|
|
15012
|
-
console.log(` Fetched split init prompt from backend`);
|
|
15013
|
-
const service = ctx.agentServices.get(agentHarness);
|
|
15014
|
-
if (!service) {
|
|
15015
|
-
const msg2 = `Unknown agent harness: ${agentHarness}`;
|
|
15016
|
-
console.log(` ⚠️ ${msg2}`);
|
|
15017
|
-
return { result: msg2, failed: true };
|
|
15018
|
-
}
|
|
15019
|
-
let spawnResult;
|
|
15020
|
-
try {
|
|
15021
|
-
spawnResult = await service.spawn({
|
|
15022
|
-
workingDir,
|
|
15023
|
-
prompt: initPromptResult.initialMessage,
|
|
15024
|
-
systemPrompt: initPromptResult.rolePrompt,
|
|
15025
|
-
model,
|
|
15026
|
-
context: { machineId: ctx.machineId, chatroomId, role }
|
|
14899
|
+
machineId: ctx.machineId,
|
|
14900
|
+
workingDir: req.workingDir,
|
|
14901
|
+
diffContent: result.content,
|
|
14902
|
+
truncated: result.truncated,
|
|
14903
|
+
diffStat
|
|
15027
14904
|
});
|
|
15028
|
-
|
|
15029
|
-
|
|
15030
|
-
|
|
15031
|
-
return { result: msg2, failed: true };
|
|
15032
|
-
}
|
|
15033
|
-
const { pid } = spawnResult;
|
|
15034
|
-
const msg = `Agent spawned (PID: ${pid})`;
|
|
15035
|
-
console.log(` ✅ ${msg}`);
|
|
15036
|
-
const agentEndKey = `${chatroomId}:${role.toLowerCase()}`;
|
|
15037
|
-
ctx.agentEndedTurn.delete(agentEndKey);
|
|
15038
|
-
ctx.deps.spawning.recordSpawn(chatroomId);
|
|
15039
|
-
try {
|
|
15040
|
-
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, {
|
|
15041
14908
|
sessionId: ctx.sessionId,
|
|
15042
14909
|
machineId: ctx.machineId,
|
|
15043
|
-
|
|
15044
|
-
|
|
15045
|
-
|
|
15046
|
-
|
|
15047
|
-
reason
|
|
15048
|
-
});
|
|
15049
|
-
console.log(` Updated backend with PID: ${pid}`);
|
|
15050
|
-
ctx.deps.machine.persistAgentPid(ctx.machineId, chatroomId, role, pid, agentHarness);
|
|
15051
|
-
} catch (e) {
|
|
15052
|
-
console.log(` ⚠️ Failed to update PID in backend: ${e.message}`);
|
|
15053
|
-
}
|
|
15054
|
-
ctx.events.emit("agent:started", {
|
|
15055
|
-
chatroomId,
|
|
15056
|
-
role,
|
|
15057
|
-
pid,
|
|
15058
|
-
harness: agentHarness,
|
|
15059
|
-
model
|
|
15060
|
-
});
|
|
15061
|
-
ctx.activeWorkingDirs.add(workingDir);
|
|
15062
|
-
spawnResult.onExit(({ code: code2, signal }) => {
|
|
15063
|
-
ctx.deps.spawning.recordExit(chatroomId);
|
|
15064
|
-
const pendingReason = ctx.deps.stops.consume(chatroomId, role);
|
|
15065
|
-
const stopReason = pendingReason ?? resolveStopReason(code2, signal, false);
|
|
15066
|
-
ctx.events.emit("agent:exited", {
|
|
15067
|
-
chatroomId,
|
|
15068
|
-
role,
|
|
15069
|
-
pid,
|
|
15070
|
-
code: code2,
|
|
15071
|
-
signal,
|
|
15072
|
-
stopReason,
|
|
15073
|
-
intentional: pendingReason !== null
|
|
15074
|
-
});
|
|
15075
|
-
});
|
|
15076
|
-
if (spawnResult.onAgentEnd) {
|
|
15077
|
-
spawnResult.onAgentEnd(() => {
|
|
15078
|
-
ctx.agentEndedTurn.set(agentEndKey, true);
|
|
15079
|
-
try {
|
|
15080
|
-
ctx.deps.processes.kill(-pid, "SIGTERM");
|
|
15081
|
-
} catch {}
|
|
14910
|
+
workingDir: req.workingDir,
|
|
14911
|
+
diffContent: "",
|
|
14912
|
+
truncated: false,
|
|
14913
|
+
diffStat: { filesChanged: 0, insertions: 0, deletions: 0 }
|
|
15082
14914
|
});
|
|
14915
|
+
console.log(`[${formatTimestamp()}] \uD83D\uDCC4 Full diff pushed (empty): ${req.workingDir} (${result.status})`);
|
|
15083
14916
|
}
|
|
15084
|
-
let lastReportedTokenAt = 0;
|
|
15085
|
-
spawnResult.onOutput(() => {
|
|
15086
|
-
const now = Date.now();
|
|
15087
|
-
if (now - lastReportedTokenAt >= 30000) {
|
|
15088
|
-
lastReportedTokenAt = now;
|
|
15089
|
-
ctx.deps.backend.mutation(api.participants.updateTokenActivity, {
|
|
15090
|
-
sessionId: ctx.sessionId,
|
|
15091
|
-
chatroomId,
|
|
15092
|
-
role
|
|
15093
|
-
}).catch(() => {});
|
|
15094
|
-
}
|
|
15095
|
-
});
|
|
15096
|
-
return { result: msg, failed: false };
|
|
15097
14917
|
}
|
|
15098
|
-
|
|
15099
|
-
|
|
15100
|
-
|
|
15101
|
-
}
|
|
15102
|
-
|
|
15103
|
-
|
|
15104
|
-
|
|
15105
|
-
|
|
15106
|
-
|
|
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
|
+
});
|
|
15107
14937
|
return;
|
|
15108
14938
|
}
|
|
15109
|
-
|
|
15110
|
-
|
|
15111
|
-
|
|
15112
|
-
|
|
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
|
+
});
|
|
15113
14951
|
return;
|
|
15114
14952
|
}
|
|
15115
|
-
|
|
15116
|
-
|
|
15117
|
-
|
|
15118
|
-
|
|
15119
|
-
|
|
15120
|
-
|
|
15121
|
-
|
|
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
|
|
15122
14966
|
});
|
|
14967
|
+
console.log(`[${formatTimestamp()}] \uD83D\uDD0D Commit detail pushed: ${req.sha.slice(0, 7)} in ${req.workingDir}`);
|
|
15123
14968
|
}
|
|
15124
|
-
|
|
15125
|
-
|
|
15126
|
-
|
|
15127
|
-
|
|
15128
|
-
|
|
15129
|
-
async function executeStopAgent(ctx, args) {
|
|
15130
|
-
const { chatroomId, role, reason } = args;
|
|
15131
|
-
const stopReason = reason;
|
|
15132
|
-
console.log(` ↪ stop-agent command received`);
|
|
15133
|
-
console.log(` Chatroom: ${chatroomId}`);
|
|
15134
|
-
console.log(` Role: ${role}`);
|
|
15135
|
-
console.log(` Reason: ${reason}`);
|
|
15136
|
-
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, {
|
|
15137
14974
|
sessionId: ctx.sessionId,
|
|
15138
|
-
|
|
14975
|
+
machineId: ctx.machineId,
|
|
14976
|
+
workingDir: req.workingDir,
|
|
14977
|
+
commits,
|
|
14978
|
+
hasMoreCommits
|
|
15139
14979
|
});
|
|
15140
|
-
|
|
15141
|
-
|
|
15142
|
-
|
|
15143
|
-
const
|
|
15144
|
-
const
|
|
15145
|
-
|
|
15146
|
-
|
|
15147
|
-
|
|
15148
|
-
|
|
15149
|
-
|
|
15150
|
-
|
|
15151
|
-
let anyKilled = false;
|
|
15152
|
-
let lastError = null;
|
|
15153
|
-
for (const pid of allPids) {
|
|
15154
|
-
console.log(` Stopping agent with PID: ${pid}`);
|
|
15155
|
-
const isAlive = anyService ? anyService.isAlive(pid) : false;
|
|
15156
|
-
if (!isAlive) {
|
|
15157
|
-
console.log(` ⚠️ PID ${pid} not found — process already exited or was never started`);
|
|
15158
|
-
await clearAgentPidEverywhere(ctx, chatroomId, role);
|
|
15159
|
-
console.log(` Cleared stale PID`);
|
|
15160
|
-
try {
|
|
15161
|
-
await ctx.deps.backend.mutation(api.participants.leave, {
|
|
15162
|
-
sessionId: ctx.sessionId,
|
|
15163
|
-
chatroomId,
|
|
15164
|
-
role
|
|
15165
|
-
});
|
|
15166
|
-
console.log(` Removed participant record`);
|
|
15167
|
-
} 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))
|
|
15168
14991
|
continue;
|
|
15169
|
-
|
|
14992
|
+
processedRequestIds.set(requestId, Date.now());
|
|
15170
14993
|
try {
|
|
15171
|
-
|
|
15172
|
-
|
|
15173
|
-
|
|
15174
|
-
|
|
15175
|
-
stopReason
|
|
14994
|
+
await ctx.deps.backend.mutation(api.workspaces.updateRequestStatus, {
|
|
14995
|
+
sessionId: ctx.sessionId,
|
|
14996
|
+
requestId: req._id,
|
|
14997
|
+
status: "processing"
|
|
15176
14998
|
});
|
|
15177
|
-
|
|
15178
|
-
|
|
15179
|
-
|
|
15180
|
-
|
|
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;
|
|
15181
15009
|
}
|
|
15182
|
-
|
|
15183
|
-
|
|
15184
|
-
|
|
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(() => {});
|
|
15185
15022
|
}
|
|
15186
15023
|
}
|
|
15187
|
-
if (lastError && !anyKilled) {
|
|
15188
|
-
const msg = `Failed to stop agent: ${lastError.message}`;
|
|
15189
|
-
console.log(` ⚠️ ${msg}`);
|
|
15190
|
-
return { result: msg, failed: true };
|
|
15191
|
-
}
|
|
15192
|
-
if (!anyKilled) {
|
|
15193
|
-
return {
|
|
15194
|
-
result: `All recorded PIDs appear stale (processes not found or belong to different programs)`,
|
|
15195
|
-
failed: true
|
|
15196
|
-
};
|
|
15197
|
-
}
|
|
15198
|
-
const killedCount = allPids.length > 1 ? ` (${allPids.length} PIDs)` : ``;
|
|
15199
|
-
return { result: `Agent stopped${killedCount}`, failed: false };
|
|
15200
15024
|
}
|
|
15201
|
-
var
|
|
15025
|
+
var init_git_subscription = __esm(() => {
|
|
15202
15026
|
init_api3();
|
|
15203
|
-
|
|
15027
|
+
init_git_reader();
|
|
15204
15028
|
});
|
|
15205
15029
|
|
|
15206
|
-
// src/
|
|
15207
|
-
|
|
15208
|
-
|
|
15209
|
-
|
|
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}`);
|
|
15210
15041
|
return;
|
|
15211
15042
|
}
|
|
15212
|
-
|
|
15213
|
-
|
|
15214
|
-
|
|
15215
|
-
|
|
15216
|
-
|
|
15217
|
-
|
|
15218
|
-
|
|
15219
|
-
|
|
15220
|
-
});
|
|
15221
|
-
|
|
15222
|
-
// src/commands/machine/daemon-start/handlers/ping.ts
|
|
15223
|
-
function handlePing() {
|
|
15224
|
-
console.log(` ↪ Responding: pong`);
|
|
15225
|
-
return { result: "pong", failed: false };
|
|
15226
|
-
}
|
|
15227
|
-
|
|
15228
|
-
// src/infrastructure/git/types.ts
|
|
15229
|
-
function makeGitStateKey(machineId, workingDir) {
|
|
15230
|
-
return `${machineId}::${workingDir}`;
|
|
15231
|
-
}
|
|
15232
|
-
var FULL_DIFF_MAX_BYTES = 500000, COMMITS_PER_PAGE = 20;
|
|
15233
|
-
|
|
15234
|
-
// src/infrastructure/git/git-reader.ts
|
|
15235
|
-
import { exec as exec2 } from "node:child_process";
|
|
15236
|
-
import { promisify as promisify2 } from "node:util";
|
|
15237
|
-
async function runGit(args, cwd) {
|
|
15238
|
-
try {
|
|
15239
|
-
const result = await execAsync2(`git ${args}`, {
|
|
15240
|
-
cwd,
|
|
15241
|
-
env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_PAGER: "cat", NO_COLOR: "1" },
|
|
15242
|
-
maxBuffer: FULL_DIFF_MAX_BYTES + 64 * 1024
|
|
15243
|
-
});
|
|
15244
|
-
return result;
|
|
15245
|
-
} catch (err) {
|
|
15246
|
-
return { error: err };
|
|
15247
|
-
}
|
|
15248
|
-
}
|
|
15249
|
-
function isGitNotInstalled(message) {
|
|
15250
|
-
return message.includes("command not found") || message.includes("ENOENT") || message.includes("not found") || message.includes("'git' is not recognized");
|
|
15251
|
-
}
|
|
15252
|
-
function isNotAGitRepo(message) {
|
|
15253
|
-
return message.includes("not a git repository") || message.includes("Not a git repository");
|
|
15254
|
-
}
|
|
15255
|
-
function isPermissionDenied(message) {
|
|
15256
|
-
return message.includes("Permission denied") || message.includes("EACCES");
|
|
15257
|
-
}
|
|
15258
|
-
function isEmptyRepo(stderr) {
|
|
15259
|
-
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");
|
|
15260
|
-
}
|
|
15261
|
-
function classifyError(errMessage) {
|
|
15262
|
-
if (isGitNotInstalled(errMessage)) {
|
|
15263
|
-
return { status: "error", message: "git is not installed or not in PATH" };
|
|
15264
|
-
}
|
|
15265
|
-
if (isNotAGitRepo(errMessage)) {
|
|
15266
|
-
return { status: "not_found" };
|
|
15267
|
-
}
|
|
15268
|
-
if (isPermissionDenied(errMessage)) {
|
|
15269
|
-
return { status: "error", message: `Permission denied: ${errMessage}` };
|
|
15270
|
-
}
|
|
15271
|
-
return { status: "error", message: errMessage.trim() };
|
|
15272
|
-
}
|
|
15273
|
-
async function isGitRepo(workingDir) {
|
|
15274
|
-
const result = await runGit("rev-parse --git-dir", workingDir);
|
|
15275
|
-
if ("error" in result)
|
|
15276
|
-
return false;
|
|
15277
|
-
return result.stdout.trim().length > 0;
|
|
15278
|
-
}
|
|
15279
|
-
async function getBranch(workingDir) {
|
|
15280
|
-
const result = await runGit("rev-parse --abbrev-ref HEAD", workingDir);
|
|
15281
|
-
if ("error" in result) {
|
|
15282
|
-
const errMsg = result.error.message;
|
|
15283
|
-
if (errMsg.includes("unknown revision") || errMsg.includes("No such file or directory") || errMsg.includes("does not have any commits")) {
|
|
15284
|
-
return { status: "available", branch: "HEAD" };
|
|
15285
|
-
}
|
|
15286
|
-
return classifyError(errMsg);
|
|
15287
|
-
}
|
|
15288
|
-
const branch = result.stdout.trim();
|
|
15289
|
-
if (!branch) {
|
|
15290
|
-
return { status: "error", message: "git rev-parse returned empty output" };
|
|
15291
|
-
}
|
|
15292
|
-
return { status: "available", branch };
|
|
15293
|
-
}
|
|
15294
|
-
async function isDirty(workingDir) {
|
|
15295
|
-
const result = await runGit("status --porcelain", workingDir);
|
|
15296
|
-
if ("error" in result)
|
|
15297
|
-
return false;
|
|
15298
|
-
return result.stdout.trim().length > 0;
|
|
15299
|
-
}
|
|
15300
|
-
function parseDiffStatLine(statLine) {
|
|
15301
|
-
const filesMatch = statLine.match(/(\d+)\s+file/);
|
|
15302
|
-
const insertMatch = statLine.match(/(\d+)\s+insertion/);
|
|
15303
|
-
const deleteMatch = statLine.match(/(\d+)\s+deletion/);
|
|
15304
|
-
return {
|
|
15305
|
-
filesChanged: filesMatch ? parseInt(filesMatch[1], 10) : 0,
|
|
15306
|
-
insertions: insertMatch ? parseInt(insertMatch[1], 10) : 0,
|
|
15307
|
-
deletions: deleteMatch ? parseInt(deleteMatch[1], 10) : 0
|
|
15308
|
-
};
|
|
15309
|
-
}
|
|
15310
|
-
async function getDiffStat(workingDir) {
|
|
15311
|
-
const result = await runGit("diff HEAD --stat", workingDir);
|
|
15312
|
-
if ("error" in result) {
|
|
15313
|
-
const errMsg = result.error.message;
|
|
15314
|
-
if (isEmptyRepo(result.error.message)) {
|
|
15315
|
-
return { status: "no_commits" };
|
|
15316
|
-
}
|
|
15317
|
-
const classified = classifyError(errMsg);
|
|
15318
|
-
if (classified.status === "not_found")
|
|
15319
|
-
return { status: "not_found" };
|
|
15320
|
-
return classified;
|
|
15321
|
-
}
|
|
15322
|
-
const output = result.stdout;
|
|
15323
|
-
const stderr = result.stderr;
|
|
15324
|
-
if (isEmptyRepo(stderr)) {
|
|
15325
|
-
return { status: "no_commits" };
|
|
15326
|
-
}
|
|
15327
|
-
if (!output.trim()) {
|
|
15328
|
-
return {
|
|
15329
|
-
status: "available",
|
|
15330
|
-
diffStat: { filesChanged: 0, insertions: 0, deletions: 0 }
|
|
15331
|
-
};
|
|
15332
|
-
}
|
|
15333
|
-
const lines = output.trim().split(`
|
|
15334
|
-
`);
|
|
15335
|
-
const summaryLine = lines[lines.length - 1] ?? "";
|
|
15336
|
-
const diffStat = parseDiffStatLine(summaryLine);
|
|
15337
|
-
return { status: "available", diffStat };
|
|
15338
|
-
}
|
|
15339
|
-
async function getFullDiff(workingDir) {
|
|
15340
|
-
const result = await runGit("diff HEAD", workingDir);
|
|
15341
|
-
if ("error" in result) {
|
|
15342
|
-
const errMsg = result.error.message;
|
|
15343
|
-
if (isEmptyRepo(errMsg)) {
|
|
15344
|
-
return { status: "no_commits" };
|
|
15345
|
-
}
|
|
15346
|
-
const classified = classifyError(errMsg);
|
|
15347
|
-
if (classified.status === "not_found")
|
|
15348
|
-
return { status: "not_found" };
|
|
15349
|
-
return classified;
|
|
15350
|
-
}
|
|
15351
|
-
const stderr = result.stderr;
|
|
15352
|
-
if (isEmptyRepo(stderr)) {
|
|
15353
|
-
return { status: "no_commits" };
|
|
15354
|
-
}
|
|
15355
|
-
const raw = result.stdout;
|
|
15356
|
-
const byteLength2 = Buffer.byteLength(raw, "utf8");
|
|
15357
|
-
if (byteLength2 > FULL_DIFF_MAX_BYTES) {
|
|
15358
|
-
const truncated = Buffer.from(raw, "utf8").subarray(0, FULL_DIFF_MAX_BYTES).toString("utf8");
|
|
15359
|
-
return { status: "truncated", content: truncated, truncated: true };
|
|
15360
|
-
}
|
|
15361
|
-
return { status: "available", content: raw, truncated: false };
|
|
15362
|
-
}
|
|
15363
|
-
async function getRecentCommits(workingDir, count = 20, skip = 0) {
|
|
15364
|
-
const format = "%H%x00%h%x00%s%x00%an%x00%aI";
|
|
15365
|
-
const skipArg = skip > 0 ? ` --skip=${skip}` : "";
|
|
15366
|
-
const result = await runGit(`log -${count}${skipArg} --format=${format}`, workingDir);
|
|
15367
|
-
if ("error" in result) {
|
|
15368
|
-
return [];
|
|
15369
|
-
}
|
|
15370
|
-
const output = result.stdout.trim();
|
|
15371
|
-
if (!output)
|
|
15372
|
-
return [];
|
|
15373
|
-
const commits = [];
|
|
15374
|
-
for (const line of output.split(`
|
|
15375
|
-
`)) {
|
|
15376
|
-
const trimmed = line.trim();
|
|
15377
|
-
if (!trimmed)
|
|
15378
|
-
continue;
|
|
15379
|
-
const parts = trimmed.split("\x00");
|
|
15380
|
-
if (parts.length !== 5)
|
|
15381
|
-
continue;
|
|
15382
|
-
const [sha, shortSha, message, author, date] = parts;
|
|
15383
|
-
commits.push({ sha, shortSha, message, author, date });
|
|
15384
|
-
}
|
|
15385
|
-
return commits;
|
|
15386
|
-
}
|
|
15387
|
-
async function getCommitDetail(workingDir, sha) {
|
|
15388
|
-
const result = await runGit(`show ${sha} --format="" --stat -p`, workingDir);
|
|
15389
|
-
if ("error" in result) {
|
|
15390
|
-
const errMsg = result.error.message;
|
|
15391
|
-
const classified = classifyError(errMsg);
|
|
15392
|
-
if (classified.status === "not_found")
|
|
15393
|
-
return { status: "not_found" };
|
|
15394
|
-
if (isEmptyRepo(errMsg) || errMsg.includes("unknown revision") || errMsg.includes("bad object") || errMsg.includes("does not exist")) {
|
|
15395
|
-
return { status: "not_found" };
|
|
15396
|
-
}
|
|
15397
|
-
return classified;
|
|
15398
|
-
}
|
|
15399
|
-
const raw = result.stdout;
|
|
15400
|
-
const byteLength2 = Buffer.byteLength(raw, "utf8");
|
|
15401
|
-
if (byteLength2 > FULL_DIFF_MAX_BYTES) {
|
|
15402
|
-
const truncated = Buffer.from(raw, "utf8").subarray(0, FULL_DIFF_MAX_BYTES).toString("utf8");
|
|
15403
|
-
return { status: "truncated", content: truncated, truncated: true };
|
|
15404
|
-
}
|
|
15405
|
-
return { status: "available", content: raw, truncated: false };
|
|
15406
|
-
}
|
|
15407
|
-
async function getCommitMetadata(workingDir, sha) {
|
|
15408
|
-
const format = "%s%x00%an%x00%aI";
|
|
15409
|
-
const result = await runGit(`log -1 --format=${format} ${sha}`, workingDir);
|
|
15410
|
-
if ("error" in result)
|
|
15411
|
-
return null;
|
|
15412
|
-
const output = result.stdout.trim();
|
|
15413
|
-
if (!output)
|
|
15414
|
-
return null;
|
|
15415
|
-
const parts = output.split("\x00");
|
|
15416
|
-
if (parts.length !== 3)
|
|
15417
|
-
return null;
|
|
15418
|
-
return { message: parts[0], author: parts[1], date: parts[2] };
|
|
15419
|
-
}
|
|
15420
|
-
var execAsync2;
|
|
15421
|
-
var init_git_reader = __esm(() => {
|
|
15422
|
-
execAsync2 = promisify2(exec2);
|
|
15423
|
-
});
|
|
15424
|
-
|
|
15425
|
-
// src/commands/machine/daemon-start/git-polling.ts
|
|
15426
|
-
function startGitPollingLoop(ctx) {
|
|
15427
|
-
const timer = setInterval(() => {
|
|
15428
|
-
runPollingTick(ctx).catch((err) => {
|
|
15429
|
-
console.warn(`[${formatTimestamp()}] ⚠️ Git polling tick failed: ${err.message}`);
|
|
15430
|
-
});
|
|
15431
|
-
}, GIT_POLLING_INTERVAL_MS);
|
|
15432
|
-
timer.unref();
|
|
15433
|
-
console.log(`[${formatTimestamp()}] \uD83D\uDD00 Git polling loop started (interval: ${GIT_POLLING_INTERVAL_MS}ms)`);
|
|
15434
|
-
return {
|
|
15435
|
-
stop: () => {
|
|
15436
|
-
clearInterval(timer);
|
|
15437
|
-
console.log(`[${formatTimestamp()}] \uD83D\uDD00 Git polling loop stopped`);
|
|
15438
|
-
}
|
|
15439
|
-
};
|
|
15440
|
-
}
|
|
15441
|
-
function extractDiffStatFromShowOutput(content) {
|
|
15442
|
-
for (const line of content.split(`
|
|
15443
|
-
`)) {
|
|
15444
|
-
if (/\d+\s+file.*changed/.test(line)) {
|
|
15445
|
-
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}`);
|
|
15446
15051
|
}
|
|
15447
15052
|
}
|
|
15448
|
-
return { filesChanged: 0, insertions: 0, deletions: 0 };
|
|
15449
15053
|
}
|
|
15450
|
-
async function
|
|
15451
|
-
const
|
|
15452
|
-
|
|
15453
|
-
|
|
15454
|
-
const
|
|
15455
|
-
|
|
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, {
|
|
15456
15062
|
sessionId: ctx.sessionId,
|
|
15457
15063
|
machineId: ctx.machineId,
|
|
15458
|
-
workingDir
|
|
15459
|
-
|
|
15460
|
-
truncated: result.truncated,
|
|
15461
|
-
diffStat
|
|
15064
|
+
workingDir,
|
|
15065
|
+
status: "not_found"
|
|
15462
15066
|
});
|
|
15463
|
-
|
|
15464
|
-
|
|
15465
|
-
|
|
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, {
|
|
15466
15081
|
sessionId: ctx.sessionId,
|
|
15467
15082
|
machineId: ctx.machineId,
|
|
15468
|
-
workingDir
|
|
15469
|
-
|
|
15470
|
-
|
|
15471
|
-
diffStat: { filesChanged: 0, insertions: 0, deletions: 0 }
|
|
15083
|
+
workingDir,
|
|
15084
|
+
status: "error",
|
|
15085
|
+
errorMessage: branchResult.message
|
|
15472
15086
|
});
|
|
15473
|
-
|
|
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;
|
|
15474
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
|
+
});
|
|
15475
15119
|
}
|
|
15476
|
-
async function
|
|
15477
|
-
if (
|
|
15478
|
-
|
|
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
|
+
}
|
|
15479
15139
|
}
|
|
15480
|
-
|
|
15481
|
-
|
|
15482
|
-
|
|
15483
|
-
|
|
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);
|
|
15484
15144
|
if (result.status === "not_found") {
|
|
15485
15145
|
await ctx.deps.backend.mutation(api.workspaces.upsertCommitDetail, {
|
|
15486
15146
|
sessionId: ctx.sessionId,
|
|
15487
15147
|
machineId: ctx.machineId,
|
|
15488
|
-
workingDir
|
|
15489
|
-
sha
|
|
15148
|
+
workingDir,
|
|
15149
|
+
sha,
|
|
15490
15150
|
status: "not_found",
|
|
15491
15151
|
message: metadata?.message,
|
|
15492
15152
|
author: metadata?.author,
|
|
@@ -15498,8 +15158,8 @@ async function processCommitDetail(ctx, req) {
|
|
|
15498
15158
|
await ctx.deps.backend.mutation(api.workspaces.upsertCommitDetail, {
|
|
15499
15159
|
sessionId: ctx.sessionId,
|
|
15500
15160
|
machineId: ctx.machineId,
|
|
15501
|
-
workingDir
|
|
15502
|
-
sha
|
|
15161
|
+
workingDir,
|
|
15162
|
+
sha,
|
|
15503
15163
|
status: "error",
|
|
15504
15164
|
errorMessage: result.message,
|
|
15505
15165
|
message: metadata?.message,
|
|
@@ -15512,8 +15172,8 @@ async function processCommitDetail(ctx, req) {
|
|
|
15512
15172
|
await ctx.deps.backend.mutation(api.workspaces.upsertCommitDetail, {
|
|
15513
15173
|
sessionId: ctx.sessionId,
|
|
15514
15174
|
machineId: ctx.machineId,
|
|
15515
|
-
workingDir
|
|
15516
|
-
sha
|
|
15175
|
+
workingDir,
|
|
15176
|
+
sha,
|
|
15517
15177
|
status: "available",
|
|
15518
15178
|
diffContent: result.content,
|
|
15519
15179
|
truncated: result.truncated,
|
|
@@ -15522,371 +15182,876 @@ async function processCommitDetail(ctx, req) {
|
|
|
15522
15182
|
date: metadata?.date,
|
|
15523
15183
|
diffStat
|
|
15524
15184
|
});
|
|
15525
|
-
console.log(`[${formatTimestamp()}]
|
|
15185
|
+
console.log(`[${formatTimestamp()}] ✅ Pre-fetched: ${sha.slice(0, 7)} in ${workingDir}`);
|
|
15526
15186
|
}
|
|
15527
|
-
|
|
15528
|
-
|
|
15529
|
-
|
|
15530
|
-
|
|
15531
|
-
|
|
15532
|
-
|
|
15533
|
-
|
|
15534
|
-
|
|
15535
|
-
|
|
15536
|
-
|
|
15537
|
-
});
|
|
15538
|
-
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 };
|
|
15539
15197
|
}
|
|
15540
|
-
|
|
15541
|
-
|
|
15542
|
-
|
|
15543
|
-
|
|
15544
|
-
|
|
15545
|
-
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`);
|
|
15546
15205
|
return;
|
|
15547
|
-
|
|
15206
|
+
}
|
|
15207
|
+
const chatroomIds = new Set(activeSlots.map((s) => s.chatroomId));
|
|
15208
|
+
let registeredCount = 0;
|
|
15209
|
+
for (const chatroomId of chatroomIds) {
|
|
15548
15210
|
try {
|
|
15549
|
-
await ctx.deps.backend.
|
|
15211
|
+
const configsResult = await ctx.deps.backend.query(api.machines.getMachineAgentConfigs, {
|
|
15550
15212
|
sessionId: ctx.sessionId,
|
|
15551
|
-
|
|
15552
|
-
status: "processing"
|
|
15213
|
+
chatroomId
|
|
15553
15214
|
});
|
|
15554
|
-
|
|
15555
|
-
|
|
15556
|
-
|
|
15557
|
-
|
|
15558
|
-
|
|
15559
|
-
|
|
15560
|
-
|
|
15561
|
-
|
|
15562
|
-
|
|
15563
|
-
|
|
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
|
+
}
|
|
15564
15229
|
}
|
|
15565
|
-
|
|
15566
|
-
|
|
15567
|
-
|
|
15568
|
-
|
|
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()
|
|
15569
15350
|
});
|
|
15570
|
-
} catch (err) {
|
|
15571
|
-
console.warn(`[${formatTimestamp()}] ⚠️ Failed to process ${req.requestType} request: ${err.message}`);
|
|
15572
|
-
await ctx.deps.backend.mutation(api.workspaces.updateRequestStatus, {
|
|
15573
|
-
sessionId: ctx.sessionId,
|
|
15574
|
-
requestId: req._id,
|
|
15575
|
-
status: "error"
|
|
15576
|
-
}).catch(() => {});
|
|
15577
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;
|
|
15578
15403
|
}
|
|
15579
15404
|
}
|
|
15580
|
-
var
|
|
15581
|
-
|
|
15582
|
-
|
|
15583
|
-
|
|
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();
|
|
15584
15410
|
});
|
|
15585
15411
|
|
|
15586
|
-
// src/
|
|
15587
|
-
|
|
15588
|
-
|
|
15589
|
-
|
|
15590
|
-
|
|
15591
|
-
|
|
15592
|
-
|
|
15593
|
-
|
|
15594
|
-
|
|
15595
|
-
|
|
15596
|
-
|
|
15597
|
-
|
|
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";
|
|
15598
15451
|
}
|
|
15599
|
-
|
|
15600
|
-
|
|
15601
|
-
|
|
15602
|
-
|
|
15603
|
-
|
|
15604
|
-
|
|
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) {
|
|
15605
15508
|
return;
|
|
15606
|
-
|
|
15607
|
-
|
|
15608
|
-
|
|
15609
|
-
|
|
15610
|
-
|
|
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}`);
|
|
15611
15537
|
});
|
|
15612
|
-
|
|
15613
|
-
|
|
15614
|
-
|
|
15615
|
-
|
|
15616
|
-
|
|
15617
|
-
|
|
15618
|
-
|
|
15619
|
-
|
|
15620
|
-
]);
|
|
15621
|
-
if (branchResult.status === "error") {
|
|
15622
|
-
const stateHash2 = `error:${branchResult.message}`;
|
|
15623
|
-
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);
|
|
15624
15546
|
return;
|
|
15625
|
-
|
|
15626
|
-
|
|
15627
|
-
|
|
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,
|
|
15628
15560
|
workingDir,
|
|
15629
|
-
|
|
15630
|
-
|
|
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
|
+
});
|
|
15631
15573
|
});
|
|
15632
|
-
ctx.lastPushedGitState.set(stateKey, stateHash2);
|
|
15633
|
-
return;
|
|
15634
15574
|
}
|
|
15635
|
-
|
|
15636
|
-
return;
|
|
15575
|
+
getSlot(chatroomId, role) {
|
|
15576
|
+
return this.slots.get(agentKey2(chatroomId, role));
|
|
15637
15577
|
}
|
|
15638
|
-
|
|
15639
|
-
|
|
15640
|
-
|
|
15641
|
-
|
|
15642
|
-
|
|
15643
|
-
|
|
15644
|
-
|
|
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;
|
|
15645
15587
|
}
|
|
15646
|
-
|
|
15647
|
-
|
|
15648
|
-
|
|
15649
|
-
|
|
15650
|
-
|
|
15651
|
-
|
|
15652
|
-
|
|
15653
|
-
|
|
15654
|
-
|
|
15655
|
-
|
|
15656
|
-
|
|
15657
|
-
|
|
15658
|
-
|
|
15659
|
-
|
|
15660
|
-
|
|
15661
|
-
|
|
15662
|
-
|
|
15663
|
-
|
|
15664
|
-
|
|
15665
|
-
|
|
15666
|
-
|
|
15667
|
-
|
|
15668
|
-
|
|
15669
|
-
|
|
15670
|
-
workingDir,
|
|
15671
|
-
shas
|
|
15672
|
-
});
|
|
15673
|
-
if (missingShas.length === 0)
|
|
15674
|
-
return;
|
|
15675
|
-
console.log(`[${formatTimestamp()}] \uD83D\uDD0D Pre-fetching ${missingShas.length} commit(s) for ${workingDir}`);
|
|
15676
|
-
for (const sha of missingShas) {
|
|
15677
|
-
try {
|
|
15678
|
-
await prefetchSingleCommit(ctx, workingDir, sha, commits);
|
|
15679
|
-
} catch (err) {
|
|
15680
|
-
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
|
+
}
|
|
15681
15612
|
}
|
|
15613
|
+
console.log(`[AgentProcessManager] Recovery: ${recovered} alive, ${cleaned} cleaned up`);
|
|
15682
15614
|
}
|
|
15683
|
-
|
|
15684
|
-
|
|
15685
|
-
|
|
15686
|
-
|
|
15687
|
-
|
|
15688
|
-
|
|
15689
|
-
|
|
15690
|
-
machineId: ctx.machineId,
|
|
15691
|
-
workingDir,
|
|
15692
|
-
sha,
|
|
15693
|
-
status: "not_found",
|
|
15694
|
-
message: metadata?.message,
|
|
15695
|
-
author: metadata?.author,
|
|
15696
|
-
date: metadata?.date
|
|
15697
|
-
});
|
|
15698
|
-
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;
|
|
15699
15622
|
}
|
|
15700
|
-
|
|
15701
|
-
|
|
15702
|
-
|
|
15703
|
-
|
|
15704
|
-
|
|
15705
|
-
|
|
15706
|
-
|
|
15707
|
-
|
|
15708
|
-
|
|
15709
|
-
|
|
15710
|
-
|
|
15711
|
-
|
|
15712
|
-
|
|
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 };
|
|
15713
15823
|
}
|
|
15714
|
-
const diffStat = extractDiffStatFromShowOutput(result.content);
|
|
15715
|
-
await ctx.deps.backend.mutation(api.workspaces.upsertCommitDetail, {
|
|
15716
|
-
sessionId: ctx.sessionId,
|
|
15717
|
-
machineId: ctx.machineId,
|
|
15718
|
-
workingDir,
|
|
15719
|
-
sha,
|
|
15720
|
-
status: "available",
|
|
15721
|
-
diffContent: result.content,
|
|
15722
|
-
truncated: result.truncated,
|
|
15723
|
-
message: metadata?.message,
|
|
15724
|
-
author: metadata?.author,
|
|
15725
|
-
date: metadata?.date,
|
|
15726
|
-
diffStat
|
|
15727
|
-
});
|
|
15728
|
-
console.log(`[${formatTimestamp()}] ✅ Pre-fetched: ${sha.slice(0, 7)} in ${workingDir}`);
|
|
15729
15824
|
}
|
|
15730
|
-
var
|
|
15825
|
+
var init_agent_process_manager = __esm(() => {
|
|
15731
15826
|
init_api3();
|
|
15732
|
-
init_git_reader();
|
|
15733
|
-
init_git_polling();
|
|
15734
15827
|
});
|
|
15735
15828
|
|
|
15736
|
-
// src/
|
|
15737
|
-
|
|
15738
|
-
|
|
15739
|
-
|
|
15740
|
-
|
|
15741
|
-
if (
|
|
15742
|
-
|
|
15743
|
-
|
|
15744
|
-
|
|
15745
|
-
|
|
15746
|
-
|
|
15747
|
-
if (task.status === "in_progress") {
|
|
15748
|
-
return agentConfig.spawnedAgentPid == null;
|
|
15749
|
-
}
|
|
15750
|
-
if (task.status === "pending" || task.status === "acknowledged") {
|
|
15751
|
-
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
|
+
}
|
|
15752
15840
|
}
|
|
15753
|
-
return false;
|
|
15754
15841
|
}
|
|
15842
|
+
return results;
|
|
15755
15843
|
}
|
|
15756
|
-
|
|
15757
|
-
|
|
15758
|
-
|
|
15759
|
-
|
|
15760
|
-
|
|
15761
|
-
|
|
15762
|
-
|
|
15763
|
-
|
|
15764
|
-
|
|
15765
|
-
|
|
15766
|
-
|
|
15767
|
-
|
|
15768
|
-
|
|
15769
|
-
|
|
15770
|
-
|
|
15771
|
-
|
|
15772
|
-
|
|
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}`);
|
|
15773
15885
|
}
|
|
15774
15886
|
}
|
|
15775
|
-
|
|
15776
|
-
|
|
15777
|
-
|
|
15778
|
-
|
|
15779
|
-
return false;
|
|
15780
|
-
}
|
|
15781
|
-
const key = `${task.chatroomId}:${agentConfig.role}`;
|
|
15782
|
-
const hasEndedTurn = context.agentEndedTurn.get(key);
|
|
15783
|
-
return hasEndedTurn === true;
|
|
15887
|
+
console.error(`
|
|
15888
|
+
Run: chatroom auth login`);
|
|
15889
|
+
releaseLock();
|
|
15890
|
+
process.exit(1);
|
|
15784
15891
|
}
|
|
15892
|
+
return sessionId;
|
|
15785
15893
|
}
|
|
15786
|
-
function
|
|
15787
|
-
|
|
15788
|
-
|
|
15789
|
-
|
|
15790
|
-
|
|
15791
|
-
|
|
15792
|
-
|
|
15793
|
-
|
|
15794
|
-
id: harness,
|
|
15795
|
-
shouldStartAgent: () => false
|
|
15796
|
-
};
|
|
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);
|
|
15797
15902
|
}
|
|
15798
15903
|
}
|
|
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
|
-
|
|
15824
|
-
|
|
15825
|
-
|
|
15826
|
-
|
|
15827
|
-
|
|
15828
|
-
|
|
15829
|
-
|
|
15830
|
-
|
|
15831
|
-
|
|
15832
|
-
|
|
15833
|
-
|
|
15834
|
-
createdAt: taskInfo.createdAt
|
|
15835
|
-
},
|
|
15836
|
-
agentConfig: taskInfo.agentConfig
|
|
15837
|
-
};
|
|
15838
|
-
const policy = getRestartPolicyForHarness(agentConfig.agentHarness);
|
|
15839
|
-
const shouldStart = policy.shouldStartAgent({ task, agentConfig }, agentEndContext);
|
|
15840
|
-
if (!shouldStart)
|
|
15841
|
-
continue;
|
|
15842
|
-
if (!canAttemptRestart(task.chatroomId, agentConfig.role, now)) {
|
|
15843
|
-
continue;
|
|
15844
|
-
}
|
|
15845
|
-
if (!agentConfig.workingDir) {
|
|
15846
|
-
console.warn(`[${formatTimestamp()}] ⚠️ Missing workingDir for ${task.chatroomId}/${agentConfig.role}` + ` — skipping`);
|
|
15847
|
-
continue;
|
|
15848
|
-
}
|
|
15849
|
-
console.log(`[${formatTimestamp()}] \uD83D\uDCE1 Task monitor: starting agent for ` + `${task.chatroomId}/${agentConfig.role} (harness: ${agentConfig.agentHarness})`);
|
|
15850
|
-
recordRestartAttempt(task.chatroomId, agentConfig.role, now);
|
|
15851
|
-
try {
|
|
15852
|
-
await executeStartAgent(ctx, {
|
|
15853
|
-
chatroomId: task.chatroomId,
|
|
15854
|
-
role: agentConfig.role,
|
|
15855
|
-
agentHarness: agentConfig.agentHarness,
|
|
15856
|
-
model: agentConfig.model,
|
|
15857
|
-
workingDir: agentConfig.workingDir,
|
|
15858
|
-
reason: "daemon.task_monitor"
|
|
15859
|
-
});
|
|
15860
|
-
} catch (err) {
|
|
15861
|
-
console.error(`[${formatTimestamp()}] ❌ Task monitor failed to start agent ` + `for ${task.chatroomId}/${agentConfig.role}: ${err.message}`);
|
|
15862
|
-
}
|
|
15863
|
-
}
|
|
15864
|
-
});
|
|
15865
|
-
console.log(`[${formatTimestamp()}] \uD83D\uDD0D Task monitor started`);
|
|
15866
|
-
} catch (err) {
|
|
15867
|
-
if (isRunning) {
|
|
15868
|
-
console.error(`[${formatTimestamp()}] ❌ Task monitor error: ${err.message}`);
|
|
15869
|
-
setTimeout(startMonitoring, 5000);
|
|
15870
|
-
}
|
|
15871
|
-
}
|
|
15872
|
-
};
|
|
15873
|
-
startMonitoring();
|
|
15874
|
-
return {
|
|
15875
|
-
stop: () => {
|
|
15876
|
-
isRunning = false;
|
|
15877
|
-
if (unsubscribe) {
|
|
15878
|
-
unsubscribe();
|
|
15879
|
-
unsubscribe = null;
|
|
15880
|
-
}
|
|
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}`);
|
|
15881
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
|
|
15882
16004
|
};
|
|
16005
|
+
registerEventListeners(ctx);
|
|
16006
|
+
logStartup(ctx, availableModels);
|
|
16007
|
+
await recoverState(ctx);
|
|
16008
|
+
return ctx;
|
|
15883
16009
|
}
|
|
15884
|
-
var
|
|
15885
|
-
|
|
16010
|
+
var init_init2 = __esm(() => {
|
|
16011
|
+
init_state_recovery();
|
|
15886
16012
|
init_api3();
|
|
16013
|
+
init_register_listeners();
|
|
16014
|
+
init_storage();
|
|
15887
16015
|
init_client2();
|
|
15888
|
-
|
|
15889
|
-
|
|
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();
|
|
15890
16055
|
});
|
|
15891
16056
|
|
|
15892
16057
|
// src/commands/machine/daemon-start/command-loop.ts
|
|
@@ -15975,15 +16140,14 @@ async function startCommandLoop(ctx) {
|
|
|
15975
16140
|
});
|
|
15976
16141
|
}, DAEMON_HEARTBEAT_INTERVAL_MS);
|
|
15977
16142
|
heartbeatTimer.unref();
|
|
15978
|
-
|
|
15979
|
-
const taskMonitorHandle = startTaskMonitor(ctx);
|
|
16143
|
+
let gitSubscriptionHandle = null;
|
|
15980
16144
|
pushGitState(ctx).catch(() => {});
|
|
15981
16145
|
const shutdown = async () => {
|
|
15982
16146
|
console.log(`
|
|
15983
16147
|
[${formatTimestamp()}] Shutting down...`);
|
|
15984
16148
|
clearInterval(heartbeatTimer);
|
|
15985
|
-
|
|
15986
|
-
|
|
16149
|
+
if (gitSubscriptionHandle)
|
|
16150
|
+
gitSubscriptionHandle.stop();
|
|
15987
16151
|
await onDaemonShutdown(ctx);
|
|
15988
16152
|
releaseLock();
|
|
15989
16153
|
process.exit(0);
|
|
@@ -15992,6 +16156,7 @@ async function startCommandLoop(ctx) {
|
|
|
15992
16156
|
process.on("SIGTERM", shutdown);
|
|
15993
16157
|
process.on("SIGHUP", shutdown);
|
|
15994
16158
|
const wsClient2 = await getConvexWsClient();
|
|
16159
|
+
gitSubscriptionHandle = startGitRequestSubscription(ctx, wsClient2);
|
|
15995
16160
|
console.log(`
|
|
15996
16161
|
Listening for commands...`);
|
|
15997
16162
|
console.log(`Press Ctrl+C to stop
|
|
@@ -16008,7 +16173,7 @@ Listening for commands...`);
|
|
|
16008
16173
|
evictStaleDedupEntries(processedCommandIds, processedPingIds, processedGitRefreshIds);
|
|
16009
16174
|
for (const event of result.events) {
|
|
16010
16175
|
try {
|
|
16011
|
-
console.log(`[${formatTimestamp()}] \uD83D\uDCE1 Stream command event: ${event.type}`);
|
|
16176
|
+
console.log(`[${formatTimestamp()}] \uD83D\uDCE1 Stream command event: ${event.type} (id: ${event._id})`);
|
|
16012
16177
|
await dispatchCommandEvent(ctx, event, processedCommandIds, processedPingIds, processedGitRefreshIds);
|
|
16013
16178
|
} catch (err) {
|
|
16014
16179
|
console.error(`[${formatTimestamp()}] ❌ Stream command event failed: ${err.message}`);
|
|
@@ -16025,17 +16190,16 @@ Listening for commands...`);
|
|
|
16025
16190
|
}
|
|
16026
16191
|
var MODEL_REFRESH_INTERVAL_MS;
|
|
16027
16192
|
var init_command_loop = __esm(() => {
|
|
16028
|
-
init_api3();
|
|
16029
|
-
init_client2();
|
|
16030
|
-
init_machine();
|
|
16031
|
-
init_on_daemon_shutdown();
|
|
16032
|
-
init_init2();
|
|
16033
16193
|
init_on_request_start_agent();
|
|
16034
16194
|
init_on_request_stop_agent();
|
|
16035
16195
|
init_pid();
|
|
16036
|
-
init_git_polling();
|
|
16037
16196
|
init_git_heartbeat();
|
|
16038
|
-
|
|
16197
|
+
init_git_subscription();
|
|
16198
|
+
init_init2();
|
|
16199
|
+
init_api3();
|
|
16200
|
+
init_on_daemon_shutdown();
|
|
16201
|
+
init_client2();
|
|
16202
|
+
init_machine();
|
|
16039
16203
|
MODEL_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
|
|
16040
16204
|
});
|
|
16041
16205
|
|
|
@@ -16047,8 +16211,6 @@ async function daemonStart() {
|
|
|
16047
16211
|
var init_daemon_start = __esm(() => {
|
|
16048
16212
|
init_command_loop();
|
|
16049
16213
|
init_init2();
|
|
16050
|
-
init_start_agent();
|
|
16051
|
-
init_stop_agent();
|
|
16052
16214
|
init_state_recovery();
|
|
16053
16215
|
});
|
|
16054
16216
|
|
|
@@ -16706,7 +16868,7 @@ program2.command("task-started").description("[LEGACY] Acknowledge a task and op
|
|
|
16706
16868
|
noClassify: skipClassification
|
|
16707
16869
|
});
|
|
16708
16870
|
});
|
|
16709
|
-
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) => {
|
|
16710
16872
|
await maybeRequireAuth();
|
|
16711
16873
|
const validClassifications = ["question", "new_feature", "follow_up"];
|
|
16712
16874
|
if (!validClassifications.includes(options.originMessageClassification)) {
|
|
@@ -16792,12 +16954,14 @@ program2.command("report-progress").description("Report progress on current task
|
|
|
16792
16954
|
});
|
|
16793
16955
|
});
|
|
16794
16956
|
var backlogCommand = program2.command("backlog").description("Manage task queue and backlog");
|
|
16795
|
-
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) => {
|
|
16796
16958
|
await maybeRequireAuth();
|
|
16797
16959
|
const { listBacklog: listBacklog2 } = await Promise.resolve().then(() => (init_backlog(), exports_backlog));
|
|
16798
16960
|
await listBacklog2(options.chatroomId, {
|
|
16799
16961
|
role: options.role,
|
|
16800
|
-
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
|
|
16801
16965
|
});
|
|
16802
16966
|
});
|
|
16803
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) => {
|
|
@@ -16847,6 +17011,21 @@ backlogCommand.command("history").description("View completed and closed backlog
|
|
|
16847
17011
|
limit: options.limit ? parseInt(options.limit, 10) : undefined
|
|
16848
17012
|
});
|
|
16849
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
|
+
});
|
|
16850
17029
|
var taskCommand = program2.command("task").description("Manage tasks");
|
|
16851
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) => {
|
|
16852
17031
|
await maybeRequireAuth();
|