chatroom-cli 1.6.5 → 1.7.1
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 +684 -22
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -14022,29 +14022,149 @@ async function recoverAgentState(ctx) {
|
|
|
14022
14022
|
const entries = ctx.deps.machine.listAgentEntries(ctx.machineId);
|
|
14023
14023
|
if (entries.length === 0) {
|
|
14024
14024
|
console.log(` No agent entries found — nothing to recover`);
|
|
14025
|
-
|
|
14026
|
-
|
|
14027
|
-
|
|
14028
|
-
|
|
14029
|
-
|
|
14030
|
-
|
|
14031
|
-
|
|
14032
|
-
|
|
14033
|
-
|
|
14034
|
-
|
|
14035
|
-
|
|
14036
|
-
|
|
14037
|
-
|
|
14038
|
-
|
|
14039
|
-
|
|
14025
|
+
} else {
|
|
14026
|
+
let recovered = 0;
|
|
14027
|
+
let cleared = 0;
|
|
14028
|
+
const chatroomIds = new Set;
|
|
14029
|
+
for (const { chatroomId, role, entry } of entries) {
|
|
14030
|
+
const { pid, harness } = entry;
|
|
14031
|
+
const service = ctx.agentServices.get(harness) ?? ctx.agentServices.values().next().value;
|
|
14032
|
+
const alive = service ? service.isAlive(pid) : false;
|
|
14033
|
+
if (alive) {
|
|
14034
|
+
console.log(` ✅ Recovered: ${role} (PID ${pid}, harness: ${harness})`);
|
|
14035
|
+
recovered++;
|
|
14036
|
+
chatroomIds.add(chatroomId);
|
|
14037
|
+
} else {
|
|
14038
|
+
console.log(` \uD83E\uDDF9 Stale PID ${pid} for ${role} — clearing`);
|
|
14039
|
+
await clearAgentPidEverywhere(ctx, chatroomId, role);
|
|
14040
|
+
cleared++;
|
|
14041
|
+
}
|
|
14042
|
+
}
|
|
14043
|
+
console.log(` Recovery complete: ${recovered} alive, ${cleared} stale cleared`);
|
|
14044
|
+
for (const chatroomId of chatroomIds) {
|
|
14045
|
+
try {
|
|
14046
|
+
const configsResult = await ctx.deps.backend.query(api.machines.getMachineAgentConfigs, {
|
|
14047
|
+
sessionId: ctx.sessionId,
|
|
14048
|
+
chatroomId
|
|
14049
|
+
});
|
|
14050
|
+
for (const config3 of configsResult.configs) {
|
|
14051
|
+
if (config3.machineId === ctx.machineId && config3.workingDir) {
|
|
14052
|
+
ctx.activeWorkingDirs.add(config3.workingDir);
|
|
14053
|
+
}
|
|
14054
|
+
}
|
|
14055
|
+
} catch {}
|
|
14056
|
+
}
|
|
14057
|
+
if (ctx.activeWorkingDirs.size > 0) {
|
|
14058
|
+
console.log(` \uD83D\uDD00 Recovered ${ctx.activeWorkingDirs.size} active working dir(s) for git tracking`);
|
|
14040
14059
|
}
|
|
14041
14060
|
}
|
|
14042
|
-
console.log(` Recovery complete: ${recovered} alive, ${cleared} stale cleared`);
|
|
14043
14061
|
}
|
|
14044
14062
|
var init_state_recovery = __esm(() => {
|
|
14063
|
+
init_api3();
|
|
14045
14064
|
init_shared();
|
|
14046
14065
|
});
|
|
14047
14066
|
|
|
14067
|
+
// src/infrastructure/services/harness-spawning/rate-limiter.ts
|
|
14068
|
+
class SpawnRateLimiter {
|
|
14069
|
+
config;
|
|
14070
|
+
buckets = new Map;
|
|
14071
|
+
constructor(config3 = {}) {
|
|
14072
|
+
this.config = { ...DEFAULT_CONFIG, ...config3 };
|
|
14073
|
+
}
|
|
14074
|
+
tryConsume(chatroomId, reason) {
|
|
14075
|
+
if (reason.startsWith("user.")) {
|
|
14076
|
+
return { allowed: true };
|
|
14077
|
+
}
|
|
14078
|
+
const bucket = this._getOrCreateBucket(chatroomId);
|
|
14079
|
+
this._refill(bucket);
|
|
14080
|
+
if (bucket.tokens < 1) {
|
|
14081
|
+
const elapsed = Date.now() - bucket.lastRefillAt;
|
|
14082
|
+
const retryAfterMs = this.config.refillRateMs - elapsed;
|
|
14083
|
+
console.warn(`⚠️ [RateLimiter] Agent spawn rate-limited for chatroom ${chatroomId} (reason: ${reason}). Retry after ${retryAfterMs}ms`);
|
|
14084
|
+
return { allowed: false, retryAfterMs };
|
|
14085
|
+
}
|
|
14086
|
+
bucket.tokens -= 1;
|
|
14087
|
+
const remaining = Math.floor(bucket.tokens);
|
|
14088
|
+
if (remaining <= LOW_TOKEN_THRESHOLD) {
|
|
14089
|
+
console.warn(`⚠️ [RateLimiter] Agent spawn tokens running low for chatroom ${chatroomId} (${remaining}/${this.config.maxTokens} remaining)`);
|
|
14090
|
+
}
|
|
14091
|
+
return { allowed: true };
|
|
14092
|
+
}
|
|
14093
|
+
getStatus(chatroomId) {
|
|
14094
|
+
const bucket = this._getOrCreateBucket(chatroomId);
|
|
14095
|
+
this._refill(bucket);
|
|
14096
|
+
return {
|
|
14097
|
+
remaining: Math.floor(bucket.tokens),
|
|
14098
|
+
total: this.config.maxTokens
|
|
14099
|
+
};
|
|
14100
|
+
}
|
|
14101
|
+
_getOrCreateBucket(chatroomId) {
|
|
14102
|
+
if (!this.buckets.has(chatroomId)) {
|
|
14103
|
+
this.buckets.set(chatroomId, {
|
|
14104
|
+
tokens: this.config.initialTokens,
|
|
14105
|
+
lastRefillAt: Date.now()
|
|
14106
|
+
});
|
|
14107
|
+
}
|
|
14108
|
+
return this.buckets.get(chatroomId);
|
|
14109
|
+
}
|
|
14110
|
+
_refill(bucket) {
|
|
14111
|
+
const now = Date.now();
|
|
14112
|
+
const elapsed = now - bucket.lastRefillAt;
|
|
14113
|
+
if (elapsed >= this.config.refillRateMs) {
|
|
14114
|
+
const tokensToAdd = Math.floor(elapsed / this.config.refillRateMs);
|
|
14115
|
+
bucket.tokens = Math.min(this.config.maxTokens, bucket.tokens + tokensToAdd);
|
|
14116
|
+
bucket.lastRefillAt += tokensToAdd * this.config.refillRateMs;
|
|
14117
|
+
}
|
|
14118
|
+
}
|
|
14119
|
+
}
|
|
14120
|
+
var DEFAULT_CONFIG, LOW_TOKEN_THRESHOLD = 1;
|
|
14121
|
+
var init_rate_limiter = __esm(() => {
|
|
14122
|
+
DEFAULT_CONFIG = {
|
|
14123
|
+
maxTokens: 5,
|
|
14124
|
+
refillRateMs: 60000,
|
|
14125
|
+
initialTokens: 5
|
|
14126
|
+
};
|
|
14127
|
+
});
|
|
14128
|
+
|
|
14129
|
+
// src/infrastructure/services/harness-spawning/harness-spawning-service.ts
|
|
14130
|
+
class HarnessSpawningService {
|
|
14131
|
+
rateLimiter;
|
|
14132
|
+
concurrentAgents = new Map;
|
|
14133
|
+
constructor({ rateLimiter }) {
|
|
14134
|
+
this.rateLimiter = rateLimiter;
|
|
14135
|
+
}
|
|
14136
|
+
shouldAllowSpawn(chatroomId, reason) {
|
|
14137
|
+
const current = this.concurrentAgents.get(chatroomId) ?? 0;
|
|
14138
|
+
if (current >= MAX_CONCURRENT_AGENTS_PER_CHATROOM) {
|
|
14139
|
+
console.warn(`⚠️ [HarnessSpawningService] Concurrent agent limit reached for chatroom ${chatroomId} ` + `(${current}/${MAX_CONCURRENT_AGENTS_PER_CHATROOM} active agents). Spawn rejected.`);
|
|
14140
|
+
return { allowed: false };
|
|
14141
|
+
}
|
|
14142
|
+
const result = this.rateLimiter.tryConsume(chatroomId, reason);
|
|
14143
|
+
if (!result.allowed) {
|
|
14144
|
+
console.warn(`⚠️ [HarnessSpawningService] Spawn blocked by rate limiter for chatroom ${chatroomId} ` + `(reason: ${reason}).`);
|
|
14145
|
+
}
|
|
14146
|
+
return result;
|
|
14147
|
+
}
|
|
14148
|
+
recordSpawn(chatroomId) {
|
|
14149
|
+
const current = this.concurrentAgents.get(chatroomId) ?? 0;
|
|
14150
|
+
this.concurrentAgents.set(chatroomId, current + 1);
|
|
14151
|
+
}
|
|
14152
|
+
recordExit(chatroomId) {
|
|
14153
|
+
const current = this.concurrentAgents.get(chatroomId) ?? 0;
|
|
14154
|
+
const next = Math.max(0, current - 1);
|
|
14155
|
+
this.concurrentAgents.set(chatroomId, next);
|
|
14156
|
+
}
|
|
14157
|
+
getConcurrentCount(chatroomId) {
|
|
14158
|
+
return this.concurrentAgents.get(chatroomId) ?? 0;
|
|
14159
|
+
}
|
|
14160
|
+
}
|
|
14161
|
+
var MAX_CONCURRENT_AGENTS_PER_CHATROOM = 10;
|
|
14162
|
+
|
|
14163
|
+
// src/infrastructure/services/harness-spawning/index.ts
|
|
14164
|
+
var init_harness_spawning = __esm(() => {
|
|
14165
|
+
init_rate_limiter();
|
|
14166
|
+
});
|
|
14167
|
+
|
|
14048
14168
|
// src/commands/machine/pid.ts
|
|
14049
14169
|
import { createHash } from "node:crypto";
|
|
14050
14170
|
import { existsSync as existsSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4, unlinkSync as unlinkSync2, mkdirSync as mkdirSync4 } from "node:fs";
|
|
@@ -14275,7 +14395,8 @@ function createDefaultDeps16() {
|
|
|
14275
14395
|
clock: {
|
|
14276
14396
|
now: () => Date.now(),
|
|
14277
14397
|
delay: (ms) => new Promise((resolve2) => setTimeout(resolve2, ms))
|
|
14278
|
-
}
|
|
14398
|
+
},
|
|
14399
|
+
spawning: new HarnessSpawningService({ rateLimiter: new SpawnRateLimiter })
|
|
14279
14400
|
};
|
|
14280
14401
|
}
|
|
14281
14402
|
function validateAuthentication(convexUrl) {
|
|
@@ -14392,7 +14513,9 @@ async function initDaemon() {
|
|
|
14392
14513
|
config: config3,
|
|
14393
14514
|
deps,
|
|
14394
14515
|
events,
|
|
14395
|
-
agentServices
|
|
14516
|
+
agentServices,
|
|
14517
|
+
activeWorkingDirs: new Set,
|
|
14518
|
+
lastPushedGitState: new Map
|
|
14396
14519
|
};
|
|
14397
14520
|
registerEventListeners(ctx);
|
|
14398
14521
|
logStartup(ctx, availableModels);
|
|
@@ -14407,6 +14530,7 @@ var init_init2 = __esm(() => {
|
|
|
14407
14530
|
init_machine();
|
|
14408
14531
|
init_intentional_stops();
|
|
14409
14532
|
init_remote_agents();
|
|
14533
|
+
init_harness_spawning();
|
|
14410
14534
|
init_error_formatting();
|
|
14411
14535
|
init_version();
|
|
14412
14536
|
init_pid();
|
|
@@ -14515,6 +14639,7 @@ async function executeStartAgent(ctx, args) {
|
|
|
14515
14639
|
const { pid } = spawnResult;
|
|
14516
14640
|
const msg = `Agent spawned (PID: ${pid})`;
|
|
14517
14641
|
console.log(` ✅ ${msg}`);
|
|
14642
|
+
ctx.deps.spawning.recordSpawn(chatroomId);
|
|
14518
14643
|
try {
|
|
14519
14644
|
await ctx.deps.backend.mutation(api.machines.updateSpawnedAgent, {
|
|
14520
14645
|
sessionId: ctx.sessionId,
|
|
@@ -14537,7 +14662,9 @@ async function executeStartAgent(ctx, args) {
|
|
|
14537
14662
|
harness: agentHarness,
|
|
14538
14663
|
model
|
|
14539
14664
|
});
|
|
14665
|
+
ctx.activeWorkingDirs.add(workingDir);
|
|
14540
14666
|
spawnResult.onExit(({ code: code2, signal }) => {
|
|
14667
|
+
ctx.deps.spawning.recordExit(chatroomId);
|
|
14541
14668
|
const pendingReason = ctx.deps.stops.consume(chatroomId, role);
|
|
14542
14669
|
const stopReason = pendingReason ?? resolveStopReason(code2, signal, false);
|
|
14543
14670
|
ctx.events.emit("agent:exited", {
|
|
@@ -14582,6 +14709,12 @@ async function onRequestStartAgent(ctx, event) {
|
|
|
14582
14709
|
console.log(`[daemon] ⏰ Skipping expired agent.requestStart for role=${event.role} (deadline passed)`);
|
|
14583
14710
|
return;
|
|
14584
14711
|
}
|
|
14712
|
+
const spawnCheck = ctx.deps.spawning.shouldAllowSpawn(event.chatroomId, event.reason);
|
|
14713
|
+
if (!spawnCheck.allowed) {
|
|
14714
|
+
const retryMsg = spawnCheck.retryAfterMs ? ` Retry after ${spawnCheck.retryAfterMs}ms.` : "";
|
|
14715
|
+
console.warn(`[daemon] ⚠️ Spawn suppressed for chatroom=${event.chatroomId} role=${event.role} reason=${event.reason}.${retryMsg}`);
|
|
14716
|
+
return;
|
|
14717
|
+
}
|
|
14585
14718
|
await executeStartAgent(ctx, {
|
|
14586
14719
|
chatroomId: event.chatroomId,
|
|
14587
14720
|
role: event.role,
|
|
@@ -14695,6 +14828,514 @@ function handlePing() {
|
|
|
14695
14828
|
return { result: "pong", failed: false };
|
|
14696
14829
|
}
|
|
14697
14830
|
|
|
14831
|
+
// src/infrastructure/git/types.ts
|
|
14832
|
+
function makeGitStateKey(machineId, workingDir) {
|
|
14833
|
+
return `${machineId}::${workingDir}`;
|
|
14834
|
+
}
|
|
14835
|
+
var FULL_DIFF_MAX_BYTES = 500000, COMMITS_PER_PAGE = 20;
|
|
14836
|
+
|
|
14837
|
+
// src/infrastructure/git/git-reader.ts
|
|
14838
|
+
import { exec as exec2 } from "node:child_process";
|
|
14839
|
+
import { promisify as promisify2 } from "node:util";
|
|
14840
|
+
async function runGit(args, cwd) {
|
|
14841
|
+
try {
|
|
14842
|
+
const result = await execAsync2(`git ${args}`, {
|
|
14843
|
+
cwd,
|
|
14844
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_PAGER: "cat", NO_COLOR: "1" },
|
|
14845
|
+
maxBuffer: FULL_DIFF_MAX_BYTES + 64 * 1024
|
|
14846
|
+
});
|
|
14847
|
+
return result;
|
|
14848
|
+
} catch (err) {
|
|
14849
|
+
return { error: err };
|
|
14850
|
+
}
|
|
14851
|
+
}
|
|
14852
|
+
function isGitNotInstalled(message) {
|
|
14853
|
+
return message.includes("command not found") || message.includes("ENOENT") || message.includes("not found") || message.includes("'git' is not recognized");
|
|
14854
|
+
}
|
|
14855
|
+
function isNotAGitRepo(message) {
|
|
14856
|
+
return message.includes("not a git repository") || message.includes("Not a git repository");
|
|
14857
|
+
}
|
|
14858
|
+
function isPermissionDenied(message) {
|
|
14859
|
+
return message.includes("Permission denied") || message.includes("EACCES");
|
|
14860
|
+
}
|
|
14861
|
+
function isEmptyRepo(stderr) {
|
|
14862
|
+
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");
|
|
14863
|
+
}
|
|
14864
|
+
function classifyError(errMessage) {
|
|
14865
|
+
if (isGitNotInstalled(errMessage)) {
|
|
14866
|
+
return { status: "error", message: "git is not installed or not in PATH" };
|
|
14867
|
+
}
|
|
14868
|
+
if (isNotAGitRepo(errMessage)) {
|
|
14869
|
+
return { status: "not_found" };
|
|
14870
|
+
}
|
|
14871
|
+
if (isPermissionDenied(errMessage)) {
|
|
14872
|
+
return { status: "error", message: `Permission denied: ${errMessage}` };
|
|
14873
|
+
}
|
|
14874
|
+
return { status: "error", message: errMessage.trim() };
|
|
14875
|
+
}
|
|
14876
|
+
async function isGitRepo(workingDir) {
|
|
14877
|
+
const result = await runGit("rev-parse --git-dir", workingDir);
|
|
14878
|
+
if ("error" in result)
|
|
14879
|
+
return false;
|
|
14880
|
+
return result.stdout.trim().length > 0;
|
|
14881
|
+
}
|
|
14882
|
+
async function getBranch(workingDir) {
|
|
14883
|
+
const result = await runGit("rev-parse --abbrev-ref HEAD", workingDir);
|
|
14884
|
+
if ("error" in result) {
|
|
14885
|
+
const errMsg = result.error.message;
|
|
14886
|
+
if (errMsg.includes("unknown revision") || errMsg.includes("No such file or directory") || errMsg.includes("does not have any commits")) {
|
|
14887
|
+
return { status: "available", branch: "HEAD" };
|
|
14888
|
+
}
|
|
14889
|
+
return classifyError(errMsg);
|
|
14890
|
+
}
|
|
14891
|
+
const branch = result.stdout.trim();
|
|
14892
|
+
if (!branch) {
|
|
14893
|
+
return { status: "error", message: "git rev-parse returned empty output" };
|
|
14894
|
+
}
|
|
14895
|
+
return { status: "available", branch };
|
|
14896
|
+
}
|
|
14897
|
+
async function isDirty(workingDir) {
|
|
14898
|
+
const result = await runGit("status --porcelain", workingDir);
|
|
14899
|
+
if ("error" in result)
|
|
14900
|
+
return false;
|
|
14901
|
+
return result.stdout.trim().length > 0;
|
|
14902
|
+
}
|
|
14903
|
+
function parseDiffStatLine(statLine) {
|
|
14904
|
+
const filesMatch = statLine.match(/(\d+)\s+file/);
|
|
14905
|
+
const insertMatch = statLine.match(/(\d+)\s+insertion/);
|
|
14906
|
+
const deleteMatch = statLine.match(/(\d+)\s+deletion/);
|
|
14907
|
+
return {
|
|
14908
|
+
filesChanged: filesMatch ? parseInt(filesMatch[1], 10) : 0,
|
|
14909
|
+
insertions: insertMatch ? parseInt(insertMatch[1], 10) : 0,
|
|
14910
|
+
deletions: deleteMatch ? parseInt(deleteMatch[1], 10) : 0
|
|
14911
|
+
};
|
|
14912
|
+
}
|
|
14913
|
+
async function getDiffStat(workingDir) {
|
|
14914
|
+
const result = await runGit("diff HEAD --stat", workingDir);
|
|
14915
|
+
if ("error" in result) {
|
|
14916
|
+
const errMsg = result.error.message;
|
|
14917
|
+
if (isEmptyRepo(result.error.message)) {
|
|
14918
|
+
return { status: "no_commits" };
|
|
14919
|
+
}
|
|
14920
|
+
const classified = classifyError(errMsg);
|
|
14921
|
+
if (classified.status === "not_found")
|
|
14922
|
+
return { status: "not_found" };
|
|
14923
|
+
return classified;
|
|
14924
|
+
}
|
|
14925
|
+
const output = result.stdout;
|
|
14926
|
+
const stderr = result.stderr;
|
|
14927
|
+
if (isEmptyRepo(stderr)) {
|
|
14928
|
+
return { status: "no_commits" };
|
|
14929
|
+
}
|
|
14930
|
+
if (!output.trim()) {
|
|
14931
|
+
return {
|
|
14932
|
+
status: "available",
|
|
14933
|
+
diffStat: { filesChanged: 0, insertions: 0, deletions: 0 }
|
|
14934
|
+
};
|
|
14935
|
+
}
|
|
14936
|
+
const lines = output.trim().split(`
|
|
14937
|
+
`);
|
|
14938
|
+
const summaryLine = lines[lines.length - 1] ?? "";
|
|
14939
|
+
const diffStat = parseDiffStatLine(summaryLine);
|
|
14940
|
+
return { status: "available", diffStat };
|
|
14941
|
+
}
|
|
14942
|
+
async function getFullDiff(workingDir) {
|
|
14943
|
+
const result = await runGit("diff HEAD", workingDir);
|
|
14944
|
+
if ("error" in result) {
|
|
14945
|
+
const errMsg = result.error.message;
|
|
14946
|
+
if (isEmptyRepo(errMsg)) {
|
|
14947
|
+
return { status: "no_commits" };
|
|
14948
|
+
}
|
|
14949
|
+
const classified = classifyError(errMsg);
|
|
14950
|
+
if (classified.status === "not_found")
|
|
14951
|
+
return { status: "not_found" };
|
|
14952
|
+
return classified;
|
|
14953
|
+
}
|
|
14954
|
+
const stderr = result.stderr;
|
|
14955
|
+
if (isEmptyRepo(stderr)) {
|
|
14956
|
+
return { status: "no_commits" };
|
|
14957
|
+
}
|
|
14958
|
+
const raw = result.stdout;
|
|
14959
|
+
const byteLength2 = Buffer.byteLength(raw, "utf8");
|
|
14960
|
+
if (byteLength2 > FULL_DIFF_MAX_BYTES) {
|
|
14961
|
+
const truncated = Buffer.from(raw, "utf8").subarray(0, FULL_DIFF_MAX_BYTES).toString("utf8");
|
|
14962
|
+
return { status: "truncated", content: truncated, truncated: true };
|
|
14963
|
+
}
|
|
14964
|
+
return { status: "available", content: raw, truncated: false };
|
|
14965
|
+
}
|
|
14966
|
+
async function getRecentCommits(workingDir, count = 20, skip = 0) {
|
|
14967
|
+
const format = "%H%x00%h%x00%s%x00%an%x00%aI";
|
|
14968
|
+
const skipArg = skip > 0 ? ` --skip=${skip}` : "";
|
|
14969
|
+
const result = await runGit(`log -${count}${skipArg} --format=${format}`, workingDir);
|
|
14970
|
+
if ("error" in result) {
|
|
14971
|
+
return [];
|
|
14972
|
+
}
|
|
14973
|
+
const output = result.stdout.trim();
|
|
14974
|
+
if (!output)
|
|
14975
|
+
return [];
|
|
14976
|
+
const commits = [];
|
|
14977
|
+
for (const line of output.split(`
|
|
14978
|
+
`)) {
|
|
14979
|
+
const trimmed = line.trim();
|
|
14980
|
+
if (!trimmed)
|
|
14981
|
+
continue;
|
|
14982
|
+
const parts = trimmed.split("\x00");
|
|
14983
|
+
if (parts.length !== 5)
|
|
14984
|
+
continue;
|
|
14985
|
+
const [sha, shortSha, message, author, date] = parts;
|
|
14986
|
+
commits.push({ sha, shortSha, message, author, date });
|
|
14987
|
+
}
|
|
14988
|
+
return commits;
|
|
14989
|
+
}
|
|
14990
|
+
async function getCommitDetail(workingDir, sha) {
|
|
14991
|
+
const result = await runGit(`show ${sha} --format="" --stat -p`, workingDir);
|
|
14992
|
+
if ("error" in result) {
|
|
14993
|
+
const errMsg = result.error.message;
|
|
14994
|
+
const classified = classifyError(errMsg);
|
|
14995
|
+
if (classified.status === "not_found")
|
|
14996
|
+
return { status: "not_found" };
|
|
14997
|
+
if (isEmptyRepo(errMsg) || errMsg.includes("unknown revision") || errMsg.includes("bad object") || errMsg.includes("does not exist")) {
|
|
14998
|
+
return { status: "not_found" };
|
|
14999
|
+
}
|
|
15000
|
+
return classified;
|
|
15001
|
+
}
|
|
15002
|
+
const raw = result.stdout;
|
|
15003
|
+
const byteLength2 = Buffer.byteLength(raw, "utf8");
|
|
15004
|
+
if (byteLength2 > FULL_DIFF_MAX_BYTES) {
|
|
15005
|
+
const truncated = Buffer.from(raw, "utf8").subarray(0, FULL_DIFF_MAX_BYTES).toString("utf8");
|
|
15006
|
+
return { status: "truncated", content: truncated, truncated: true };
|
|
15007
|
+
}
|
|
15008
|
+
return { status: "available", content: raw, truncated: false };
|
|
15009
|
+
}
|
|
15010
|
+
async function getCommitMetadata(workingDir, sha) {
|
|
15011
|
+
const format = "%s%x00%an%x00%aI";
|
|
15012
|
+
const result = await runGit(`log -1 --format=${format} ${sha}`, workingDir);
|
|
15013
|
+
if ("error" in result)
|
|
15014
|
+
return null;
|
|
15015
|
+
const output = result.stdout.trim();
|
|
15016
|
+
if (!output)
|
|
15017
|
+
return null;
|
|
15018
|
+
const parts = output.split("\x00");
|
|
15019
|
+
if (parts.length !== 3)
|
|
15020
|
+
return null;
|
|
15021
|
+
return { message: parts[0], author: parts[1], date: parts[2] };
|
|
15022
|
+
}
|
|
15023
|
+
var execAsync2;
|
|
15024
|
+
var init_git_reader = __esm(() => {
|
|
15025
|
+
execAsync2 = promisify2(exec2);
|
|
15026
|
+
});
|
|
15027
|
+
|
|
15028
|
+
// src/commands/machine/daemon-start/git-polling.ts
|
|
15029
|
+
function startGitPollingLoop(ctx) {
|
|
15030
|
+
const timer = setInterval(() => {
|
|
15031
|
+
runPollingTick(ctx).catch((err) => {
|
|
15032
|
+
console.warn(`[${formatTimestamp()}] ⚠️ Git polling tick failed: ${err.message}`);
|
|
15033
|
+
});
|
|
15034
|
+
}, GIT_POLLING_INTERVAL_MS);
|
|
15035
|
+
timer.unref();
|
|
15036
|
+
console.log(`[${formatTimestamp()}] \uD83D\uDD00 Git polling loop started (interval: ${GIT_POLLING_INTERVAL_MS}ms)`);
|
|
15037
|
+
return {
|
|
15038
|
+
stop: () => {
|
|
15039
|
+
clearInterval(timer);
|
|
15040
|
+
console.log(`[${formatTimestamp()}] \uD83D\uDD00 Git polling loop stopped`);
|
|
15041
|
+
}
|
|
15042
|
+
};
|
|
15043
|
+
}
|
|
15044
|
+
function extractDiffStatFromShowOutput(content) {
|
|
15045
|
+
for (const line of content.split(`
|
|
15046
|
+
`)) {
|
|
15047
|
+
if (/\d+\s+file.*changed/.test(line)) {
|
|
15048
|
+
return parseDiffStatLine(line);
|
|
15049
|
+
}
|
|
15050
|
+
}
|
|
15051
|
+
return { filesChanged: 0, insertions: 0, deletions: 0 };
|
|
15052
|
+
}
|
|
15053
|
+
async function processFullDiff(ctx, req) {
|
|
15054
|
+
const result = await getFullDiff(req.workingDir);
|
|
15055
|
+
if (result.status === "available" || result.status === "truncated") {
|
|
15056
|
+
const diffStatResult = await getDiffStat(req.workingDir);
|
|
15057
|
+
const diffStat = diffStatResult.status === "available" ? diffStatResult.diffStat : { filesChanged: 0, insertions: 0, deletions: 0 };
|
|
15058
|
+
await ctx.deps.backend.mutation(api.workspaces.upsertFullDiff, {
|
|
15059
|
+
sessionId: ctx.sessionId,
|
|
15060
|
+
machineId: ctx.machineId,
|
|
15061
|
+
workingDir: req.workingDir,
|
|
15062
|
+
diffContent: result.content,
|
|
15063
|
+
truncated: result.truncated,
|
|
15064
|
+
diffStat
|
|
15065
|
+
});
|
|
15066
|
+
console.log(`[${formatTimestamp()}] \uD83D\uDCC4 Full diff pushed: ${req.workingDir} (${diffStat.filesChanged} files, ${result.truncated ? "truncated" : "complete"})`);
|
|
15067
|
+
} else {
|
|
15068
|
+
await ctx.deps.backend.mutation(api.workspaces.upsertFullDiff, {
|
|
15069
|
+
sessionId: ctx.sessionId,
|
|
15070
|
+
machineId: ctx.machineId,
|
|
15071
|
+
workingDir: req.workingDir,
|
|
15072
|
+
diffContent: "",
|
|
15073
|
+
truncated: false,
|
|
15074
|
+
diffStat: { filesChanged: 0, insertions: 0, deletions: 0 }
|
|
15075
|
+
});
|
|
15076
|
+
console.log(`[${formatTimestamp()}] \uD83D\uDCC4 Full diff pushed (empty): ${req.workingDir} (${result.status})`);
|
|
15077
|
+
}
|
|
15078
|
+
}
|
|
15079
|
+
async function processCommitDetail(ctx, req) {
|
|
15080
|
+
if (!req.sha) {
|
|
15081
|
+
throw new Error("commit_detail request missing sha");
|
|
15082
|
+
}
|
|
15083
|
+
const [result, metadata] = await Promise.all([
|
|
15084
|
+
getCommitDetail(req.workingDir, req.sha),
|
|
15085
|
+
getCommitMetadata(req.workingDir, req.sha)
|
|
15086
|
+
]);
|
|
15087
|
+
if (result.status === "not_found") {
|
|
15088
|
+
await ctx.deps.backend.mutation(api.workspaces.upsertCommitDetail, {
|
|
15089
|
+
sessionId: ctx.sessionId,
|
|
15090
|
+
machineId: ctx.machineId,
|
|
15091
|
+
workingDir: req.workingDir,
|
|
15092
|
+
sha: req.sha,
|
|
15093
|
+
status: "not_found",
|
|
15094
|
+
message: metadata?.message,
|
|
15095
|
+
author: metadata?.author,
|
|
15096
|
+
date: metadata?.date
|
|
15097
|
+
});
|
|
15098
|
+
return;
|
|
15099
|
+
}
|
|
15100
|
+
if (result.status === "error") {
|
|
15101
|
+
await ctx.deps.backend.mutation(api.workspaces.upsertCommitDetail, {
|
|
15102
|
+
sessionId: ctx.sessionId,
|
|
15103
|
+
machineId: ctx.machineId,
|
|
15104
|
+
workingDir: req.workingDir,
|
|
15105
|
+
sha: req.sha,
|
|
15106
|
+
status: "error",
|
|
15107
|
+
errorMessage: result.message,
|
|
15108
|
+
message: metadata?.message,
|
|
15109
|
+
author: metadata?.author,
|
|
15110
|
+
date: metadata?.date
|
|
15111
|
+
});
|
|
15112
|
+
return;
|
|
15113
|
+
}
|
|
15114
|
+
const diffStat = extractDiffStatFromShowOutput(result.content);
|
|
15115
|
+
await ctx.deps.backend.mutation(api.workspaces.upsertCommitDetail, {
|
|
15116
|
+
sessionId: ctx.sessionId,
|
|
15117
|
+
machineId: ctx.machineId,
|
|
15118
|
+
workingDir: req.workingDir,
|
|
15119
|
+
sha: req.sha,
|
|
15120
|
+
status: "available",
|
|
15121
|
+
diffContent: result.content,
|
|
15122
|
+
truncated: result.truncated,
|
|
15123
|
+
message: metadata?.message,
|
|
15124
|
+
author: metadata?.author,
|
|
15125
|
+
date: metadata?.date,
|
|
15126
|
+
diffStat
|
|
15127
|
+
});
|
|
15128
|
+
console.log(`[${formatTimestamp()}] \uD83D\uDD0D Commit detail pushed: ${req.sha.slice(0, 7)} in ${req.workingDir}`);
|
|
15129
|
+
}
|
|
15130
|
+
async function processMoreCommits(ctx, req) {
|
|
15131
|
+
const offset = req.offset ?? 0;
|
|
15132
|
+
const commits = await getRecentCommits(req.workingDir, COMMITS_PER_PAGE, offset);
|
|
15133
|
+
const hasMoreCommits = commits.length >= COMMITS_PER_PAGE;
|
|
15134
|
+
await ctx.deps.backend.mutation(api.workspaces.appendMoreCommits, {
|
|
15135
|
+
sessionId: ctx.sessionId,
|
|
15136
|
+
machineId: ctx.machineId,
|
|
15137
|
+
workingDir: req.workingDir,
|
|
15138
|
+
commits,
|
|
15139
|
+
hasMoreCommits
|
|
15140
|
+
});
|
|
15141
|
+
console.log(`[${formatTimestamp()}] \uD83D\uDCDC More commits appended: ${req.workingDir} (+${commits.length} commits, offset=${offset})`);
|
|
15142
|
+
}
|
|
15143
|
+
async function runPollingTick(ctx) {
|
|
15144
|
+
const requests = await ctx.deps.backend.query(api.workspaces.getPendingRequests, {
|
|
15145
|
+
sessionId: ctx.sessionId,
|
|
15146
|
+
machineId: ctx.machineId
|
|
15147
|
+
});
|
|
15148
|
+
if (requests.length === 0)
|
|
15149
|
+
return;
|
|
15150
|
+
for (const req of requests) {
|
|
15151
|
+
try {
|
|
15152
|
+
await ctx.deps.backend.mutation(api.workspaces.updateRequestStatus, {
|
|
15153
|
+
sessionId: ctx.sessionId,
|
|
15154
|
+
requestId: req._id,
|
|
15155
|
+
status: "processing"
|
|
15156
|
+
});
|
|
15157
|
+
switch (req.requestType) {
|
|
15158
|
+
case "full_diff":
|
|
15159
|
+
await processFullDiff(ctx, req);
|
|
15160
|
+
break;
|
|
15161
|
+
case "commit_detail":
|
|
15162
|
+
await processCommitDetail(ctx, req);
|
|
15163
|
+
break;
|
|
15164
|
+
case "more_commits":
|
|
15165
|
+
await processMoreCommits(ctx, req);
|
|
15166
|
+
break;
|
|
15167
|
+
}
|
|
15168
|
+
await ctx.deps.backend.mutation(api.workspaces.updateRequestStatus, {
|
|
15169
|
+
sessionId: ctx.sessionId,
|
|
15170
|
+
requestId: req._id,
|
|
15171
|
+
status: "done"
|
|
15172
|
+
});
|
|
15173
|
+
} catch (err) {
|
|
15174
|
+
console.warn(`[${formatTimestamp()}] ⚠️ Failed to process ${req.requestType} request: ${err.message}`);
|
|
15175
|
+
await ctx.deps.backend.mutation(api.workspaces.updateRequestStatus, {
|
|
15176
|
+
sessionId: ctx.sessionId,
|
|
15177
|
+
requestId: req._id,
|
|
15178
|
+
status: "error"
|
|
15179
|
+
}).catch(() => {});
|
|
15180
|
+
}
|
|
15181
|
+
}
|
|
15182
|
+
}
|
|
15183
|
+
var GIT_POLLING_INTERVAL_MS = 5000;
|
|
15184
|
+
var init_git_polling = __esm(() => {
|
|
15185
|
+
init_api3();
|
|
15186
|
+
init_git_reader();
|
|
15187
|
+
});
|
|
15188
|
+
|
|
15189
|
+
// src/commands/machine/daemon-start/git-heartbeat.ts
|
|
15190
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
15191
|
+
async function pushGitState(ctx) {
|
|
15192
|
+
if (ctx.activeWorkingDirs.size === 0)
|
|
15193
|
+
return;
|
|
15194
|
+
for (const workingDir of ctx.activeWorkingDirs) {
|
|
15195
|
+
try {
|
|
15196
|
+
await pushSingleWorkspaceGitState(ctx, workingDir);
|
|
15197
|
+
} catch (err) {
|
|
15198
|
+
console.warn(`[${formatTimestamp()}] ⚠️ Git state push failed for ${workingDir}: ${err.message}`);
|
|
15199
|
+
}
|
|
15200
|
+
}
|
|
15201
|
+
}
|
|
15202
|
+
async function pushSingleWorkspaceGitState(ctx, workingDir) {
|
|
15203
|
+
const stateKey = makeGitStateKey(ctx.machineId, workingDir);
|
|
15204
|
+
const isRepo = await isGitRepo(workingDir);
|
|
15205
|
+
if (!isRepo) {
|
|
15206
|
+
const stateHash2 = "not_found";
|
|
15207
|
+
if (ctx.lastPushedGitState.get(stateKey) === stateHash2)
|
|
15208
|
+
return;
|
|
15209
|
+
await ctx.deps.backend.mutation(api.workspaces.upsertWorkspaceGitState, {
|
|
15210
|
+
sessionId: ctx.sessionId,
|
|
15211
|
+
machineId: ctx.machineId,
|
|
15212
|
+
workingDir,
|
|
15213
|
+
status: "not_found"
|
|
15214
|
+
});
|
|
15215
|
+
ctx.lastPushedGitState.set(stateKey, stateHash2);
|
|
15216
|
+
return;
|
|
15217
|
+
}
|
|
15218
|
+
const [branchResult, dirtyResult, diffStatResult, commits] = await Promise.all([
|
|
15219
|
+
getBranch(workingDir),
|
|
15220
|
+
isDirty(workingDir),
|
|
15221
|
+
getDiffStat(workingDir),
|
|
15222
|
+
getRecentCommits(workingDir, COMMITS_PER_PAGE)
|
|
15223
|
+
]);
|
|
15224
|
+
if (branchResult.status === "error") {
|
|
15225
|
+
const stateHash2 = `error:${branchResult.message}`;
|
|
15226
|
+
if (ctx.lastPushedGitState.get(stateKey) === stateHash2)
|
|
15227
|
+
return;
|
|
15228
|
+
await ctx.deps.backend.mutation(api.workspaces.upsertWorkspaceGitState, {
|
|
15229
|
+
sessionId: ctx.sessionId,
|
|
15230
|
+
machineId: ctx.machineId,
|
|
15231
|
+
workingDir,
|
|
15232
|
+
status: "error",
|
|
15233
|
+
errorMessage: branchResult.message
|
|
15234
|
+
});
|
|
15235
|
+
ctx.lastPushedGitState.set(stateKey, stateHash2);
|
|
15236
|
+
return;
|
|
15237
|
+
}
|
|
15238
|
+
if (branchResult.status === "not_found") {
|
|
15239
|
+
return;
|
|
15240
|
+
}
|
|
15241
|
+
const branch = branchResult.branch;
|
|
15242
|
+
const isDirty2 = dirtyResult;
|
|
15243
|
+
const diffStat = diffStatResult.status === "available" ? diffStatResult.diffStat : { filesChanged: 0, insertions: 0, deletions: 0 };
|
|
15244
|
+
const hasMoreCommits = commits.length >= COMMITS_PER_PAGE;
|
|
15245
|
+
const stateHash = createHash2("md5").update(JSON.stringify({ branch, isDirty: isDirty2, diffStat, shas: commits.map((c) => c.sha) })).digest("hex");
|
|
15246
|
+
if (ctx.lastPushedGitState.get(stateKey) === stateHash) {
|
|
15247
|
+
return;
|
|
15248
|
+
}
|
|
15249
|
+
await ctx.deps.backend.mutation(api.workspaces.upsertWorkspaceGitState, {
|
|
15250
|
+
sessionId: ctx.sessionId,
|
|
15251
|
+
machineId: ctx.machineId,
|
|
15252
|
+
workingDir,
|
|
15253
|
+
status: "available",
|
|
15254
|
+
branch,
|
|
15255
|
+
isDirty: isDirty2,
|
|
15256
|
+
diffStat,
|
|
15257
|
+
recentCommits: commits,
|
|
15258
|
+
hasMoreCommits
|
|
15259
|
+
});
|
|
15260
|
+
ctx.lastPushedGitState.set(stateKey, stateHash);
|
|
15261
|
+
console.log(`[${formatTimestamp()}] \uD83D\uDD00 Git state pushed: ${workingDir} (${branch}${isDirty2 ? ", dirty" : ", clean"})`);
|
|
15262
|
+
prefetchMissingCommitDetails(ctx, workingDir, commits).catch((err) => {
|
|
15263
|
+
console.warn(`[${formatTimestamp()}] ⚠️ Commit pre-fetch failed for ${workingDir}: ${err.message}`);
|
|
15264
|
+
});
|
|
15265
|
+
}
|
|
15266
|
+
async function prefetchMissingCommitDetails(ctx, workingDir, commits) {
|
|
15267
|
+
if (commits.length === 0)
|
|
15268
|
+
return;
|
|
15269
|
+
const shas = commits.map((c) => c.sha);
|
|
15270
|
+
const missingShas = await ctx.deps.backend.query(api.workspaces.getMissingCommitShas, {
|
|
15271
|
+
sessionId: ctx.sessionId,
|
|
15272
|
+
machineId: ctx.machineId,
|
|
15273
|
+
workingDir,
|
|
15274
|
+
shas
|
|
15275
|
+
});
|
|
15276
|
+
if (missingShas.length === 0)
|
|
15277
|
+
return;
|
|
15278
|
+
console.log(`[${formatTimestamp()}] \uD83D\uDD0D Pre-fetching ${missingShas.length} commit(s) for ${workingDir}`);
|
|
15279
|
+
for (const sha of missingShas) {
|
|
15280
|
+
try {
|
|
15281
|
+
await prefetchSingleCommit(ctx, workingDir, sha, commits);
|
|
15282
|
+
} catch (err) {
|
|
15283
|
+
console.warn(`[${formatTimestamp()}] ⚠️ Pre-fetch failed for ${sha.slice(0, 7)}: ${err.message}`);
|
|
15284
|
+
}
|
|
15285
|
+
}
|
|
15286
|
+
}
|
|
15287
|
+
async function prefetchSingleCommit(ctx, workingDir, sha, commits) {
|
|
15288
|
+
const metadata = commits.find((c) => c.sha === sha);
|
|
15289
|
+
const result = await getCommitDetail(workingDir, sha);
|
|
15290
|
+
if (result.status === "not_found") {
|
|
15291
|
+
await ctx.deps.backend.mutation(api.workspaces.upsertCommitDetail, {
|
|
15292
|
+
sessionId: ctx.sessionId,
|
|
15293
|
+
machineId: ctx.machineId,
|
|
15294
|
+
workingDir,
|
|
15295
|
+
sha,
|
|
15296
|
+
status: "not_found",
|
|
15297
|
+
message: metadata?.message,
|
|
15298
|
+
author: metadata?.author,
|
|
15299
|
+
date: metadata?.date
|
|
15300
|
+
});
|
|
15301
|
+
return;
|
|
15302
|
+
}
|
|
15303
|
+
if (result.status === "error") {
|
|
15304
|
+
await ctx.deps.backend.mutation(api.workspaces.upsertCommitDetail, {
|
|
15305
|
+
sessionId: ctx.sessionId,
|
|
15306
|
+
machineId: ctx.machineId,
|
|
15307
|
+
workingDir,
|
|
15308
|
+
sha,
|
|
15309
|
+
status: "error",
|
|
15310
|
+
errorMessage: result.message,
|
|
15311
|
+
message: metadata?.message,
|
|
15312
|
+
author: metadata?.author,
|
|
15313
|
+
date: metadata?.date
|
|
15314
|
+
});
|
|
15315
|
+
return;
|
|
15316
|
+
}
|
|
15317
|
+
const diffStat = extractDiffStatFromShowOutput(result.content);
|
|
15318
|
+
await ctx.deps.backend.mutation(api.workspaces.upsertCommitDetail, {
|
|
15319
|
+
sessionId: ctx.sessionId,
|
|
15320
|
+
machineId: ctx.machineId,
|
|
15321
|
+
workingDir,
|
|
15322
|
+
sha,
|
|
15323
|
+
status: "available",
|
|
15324
|
+
diffContent: result.content,
|
|
15325
|
+
truncated: result.truncated,
|
|
15326
|
+
message: metadata?.message,
|
|
15327
|
+
author: metadata?.author,
|
|
15328
|
+
date: metadata?.date,
|
|
15329
|
+
diffStat
|
|
15330
|
+
});
|
|
15331
|
+
console.log(`[${formatTimestamp()}] ✅ Pre-fetched: ${sha.slice(0, 7)} in ${workingDir}`);
|
|
15332
|
+
}
|
|
15333
|
+
var init_git_heartbeat = __esm(() => {
|
|
15334
|
+
init_api3();
|
|
15335
|
+
init_git_reader();
|
|
15336
|
+
init_git_polling();
|
|
15337
|
+
});
|
|
15338
|
+
|
|
14698
15339
|
// src/commands/machine/daemon-start/command-loop.ts
|
|
14699
15340
|
async function refreshModels(ctx) {
|
|
14700
15341
|
if (!ctx.config)
|
|
@@ -14717,7 +15358,7 @@ async function refreshModels(ctx) {
|
|
|
14717
15358
|
console.warn(`[${formatTimestamp()}] ⚠️ Model refresh failed: ${error.message}`);
|
|
14718
15359
|
}
|
|
14719
15360
|
}
|
|
14720
|
-
function evictStaleDedupEntries(processedCommandIds, processedPingIds) {
|
|
15361
|
+
function evictStaleDedupEntries(processedCommandIds, processedPingIds, processedGitRefreshIds) {
|
|
14721
15362
|
const evictBefore = Date.now() - AGENT_REQUEST_DEADLINE_MS;
|
|
14722
15363
|
for (const [id, ts] of processedCommandIds) {
|
|
14723
15364
|
if (ts < evictBefore)
|
|
@@ -14727,8 +15368,12 @@ function evictStaleDedupEntries(processedCommandIds, processedPingIds) {
|
|
|
14727
15368
|
if (ts < evictBefore)
|
|
14728
15369
|
processedPingIds.delete(id);
|
|
14729
15370
|
}
|
|
15371
|
+
for (const [id, ts] of processedGitRefreshIds) {
|
|
15372
|
+
if (ts < evictBefore)
|
|
15373
|
+
processedGitRefreshIds.delete(id);
|
|
15374
|
+
}
|
|
14730
15375
|
}
|
|
14731
|
-
async function dispatchCommandEvent(ctx, event, processedCommandIds, processedPingIds) {
|
|
15376
|
+
async function dispatchCommandEvent(ctx, event, processedCommandIds, processedPingIds, processedGitRefreshIds) {
|
|
14732
15377
|
const eventId = event._id.toString();
|
|
14733
15378
|
if (event.type === "agent.requestStart") {
|
|
14734
15379
|
if (processedCommandIds.has(eventId))
|
|
@@ -14750,6 +15395,14 @@ async function dispatchCommandEvent(ctx, event, processedCommandIds, processedPi
|
|
|
14750
15395
|
machineId: ctx.machineId,
|
|
14751
15396
|
pingEventId: event._id
|
|
14752
15397
|
});
|
|
15398
|
+
} else if (event.type === "daemon.gitRefresh") {
|
|
15399
|
+
if (processedGitRefreshIds.has(eventId))
|
|
15400
|
+
return;
|
|
15401
|
+
processedGitRefreshIds.set(eventId, Date.now());
|
|
15402
|
+
const stateKey = makeGitStateKey(ctx.machineId, event.workingDir);
|
|
15403
|
+
ctx.lastPushedGitState.delete(stateKey);
|
|
15404
|
+
console.log(`[${formatTimestamp()}] \uD83D\uDD04 Git refresh requested for ${event.workingDir}`);
|
|
15405
|
+
await pushGitState(ctx);
|
|
14753
15406
|
}
|
|
14754
15407
|
}
|
|
14755
15408
|
async function startCommandLoop(ctx) {
|
|
@@ -14761,15 +15414,21 @@ async function startCommandLoop(ctx) {
|
|
|
14761
15414
|
}).then(() => {
|
|
14762
15415
|
heartbeatCount++;
|
|
14763
15416
|
console.log(`[${formatTimestamp()}] \uD83D\uDC93 Daemon heartbeat #${heartbeatCount} OK`);
|
|
15417
|
+
pushGitState(ctx).catch((err) => {
|
|
15418
|
+
console.warn(`[${formatTimestamp()}] ⚠️ Git state push failed: ${err.message}`);
|
|
15419
|
+
});
|
|
14764
15420
|
}).catch((err) => {
|
|
14765
15421
|
console.warn(`[${formatTimestamp()}] ⚠️ Daemon heartbeat failed: ${err.message}`);
|
|
14766
15422
|
});
|
|
14767
15423
|
}, DAEMON_HEARTBEAT_INTERVAL_MS);
|
|
14768
15424
|
heartbeatTimer.unref();
|
|
15425
|
+
const gitPollingHandle = startGitPollingLoop(ctx);
|
|
15426
|
+
pushGitState(ctx).catch(() => {});
|
|
14769
15427
|
const shutdown = async () => {
|
|
14770
15428
|
console.log(`
|
|
14771
15429
|
[${formatTimestamp()}] Shutting down...`);
|
|
14772
15430
|
clearInterval(heartbeatTimer);
|
|
15431
|
+
gitPollingHandle.stop();
|
|
14773
15432
|
await onDaemonShutdown(ctx);
|
|
14774
15433
|
releaseLock();
|
|
14775
15434
|
process.exit(0);
|
|
@@ -14784,17 +15443,18 @@ Listening for commands...`);
|
|
|
14784
15443
|
`);
|
|
14785
15444
|
const processedCommandIds = new Map;
|
|
14786
15445
|
const processedPingIds = new Map;
|
|
15446
|
+
const processedGitRefreshIds = new Map;
|
|
14787
15447
|
wsClient2.onUpdate(api.machines.getCommandEvents, {
|
|
14788
15448
|
sessionId: ctx.sessionId,
|
|
14789
15449
|
machineId: ctx.machineId
|
|
14790
15450
|
}, async (result) => {
|
|
14791
15451
|
if (!result.events || result.events.length === 0)
|
|
14792
15452
|
return;
|
|
14793
|
-
evictStaleDedupEntries(processedCommandIds, processedPingIds);
|
|
15453
|
+
evictStaleDedupEntries(processedCommandIds, processedPingIds, processedGitRefreshIds);
|
|
14794
15454
|
for (const event of result.events) {
|
|
14795
15455
|
try {
|
|
14796
15456
|
console.log(`[${formatTimestamp()}] \uD83D\uDCE1 Stream command event: ${event.type}`);
|
|
14797
|
-
await dispatchCommandEvent(ctx, event, processedCommandIds, processedPingIds);
|
|
15457
|
+
await dispatchCommandEvent(ctx, event, processedCommandIds, processedPingIds, processedGitRefreshIds);
|
|
14798
15458
|
} catch (err) {
|
|
14799
15459
|
console.error(`[${formatTimestamp()}] ❌ Stream command event failed: ${err.message}`);
|
|
14800
15460
|
}
|
|
@@ -14818,6 +15478,8 @@ var init_command_loop = __esm(() => {
|
|
|
14818
15478
|
init_on_request_start_agent();
|
|
14819
15479
|
init_on_request_stop_agent();
|
|
14820
15480
|
init_pid();
|
|
15481
|
+
init_git_polling();
|
|
15482
|
+
init_git_heartbeat();
|
|
14821
15483
|
MODEL_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
|
|
14822
15484
|
});
|
|
14823
15485
|
|