chatroom-cli 1.6.4 → 1.7.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 +732 -23
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10678,7 +10678,54 @@ var CURSOR_COMMAND = "agent", CURSOR_MODELS, CursorAgentService;
|
|
|
10678
10678
|
var init_cursor_agent_service = __esm(() => {
|
|
10679
10679
|
init_base_cli_agent_service();
|
|
10680
10680
|
init_cursor_stream_reader();
|
|
10681
|
-
CURSOR_MODELS = [
|
|
10681
|
+
CURSOR_MODELS = [
|
|
10682
|
+
"opus-4.6",
|
|
10683
|
+
"opus-4.6-thinking",
|
|
10684
|
+
"opus-4.5",
|
|
10685
|
+
"opus-4.5-thinking",
|
|
10686
|
+
"sonnet-4.6",
|
|
10687
|
+
"sonnet-4.6-thinking",
|
|
10688
|
+
"sonnet-4.5",
|
|
10689
|
+
"sonnet-4.5-thinking",
|
|
10690
|
+
"gpt-5.4-low",
|
|
10691
|
+
"gpt-5.4-medium",
|
|
10692
|
+
"gpt-5.4-medium-fast",
|
|
10693
|
+
"gpt-5.4-high",
|
|
10694
|
+
"gpt-5.4-high-fast",
|
|
10695
|
+
"gpt-5.4-xhigh",
|
|
10696
|
+
"gpt-5.4-xhigh-fast",
|
|
10697
|
+
"gpt-5.3-codex-low",
|
|
10698
|
+
"gpt-5.3-codex-low-fast",
|
|
10699
|
+
"gpt-5.3-codex",
|
|
10700
|
+
"gpt-5.3-codex-fast",
|
|
10701
|
+
"gpt-5.3-codex-high",
|
|
10702
|
+
"gpt-5.3-codex-high-fast",
|
|
10703
|
+
"gpt-5.3-codex-xhigh",
|
|
10704
|
+
"gpt-5.3-codex-xhigh-fast",
|
|
10705
|
+
"gpt-5.3-codex-spark-preview",
|
|
10706
|
+
"gpt-5.2",
|
|
10707
|
+
"gpt-5.2-high",
|
|
10708
|
+
"gpt-5.2-codex-low",
|
|
10709
|
+
"gpt-5.2-codex-low-fast",
|
|
10710
|
+
"gpt-5.2-codex",
|
|
10711
|
+
"gpt-5.2-codex-fast",
|
|
10712
|
+
"gpt-5.2-codex-high",
|
|
10713
|
+
"gpt-5.2-codex-high-fast",
|
|
10714
|
+
"gpt-5.2-codex-xhigh",
|
|
10715
|
+
"gpt-5.2-codex-xhigh-fast",
|
|
10716
|
+
"gpt-5.1-high",
|
|
10717
|
+
"gpt-5.1-codex-max",
|
|
10718
|
+
"gpt-5.1-codex-max-high",
|
|
10719
|
+
"gpt-5.1-codex-mini",
|
|
10720
|
+
"gemini-3.1-pro",
|
|
10721
|
+
"gemini-3-pro",
|
|
10722
|
+
"gemini-3-flash",
|
|
10723
|
+
"grok",
|
|
10724
|
+
"kimi-k2.5",
|
|
10725
|
+
"auto",
|
|
10726
|
+
"composer-1.5",
|
|
10727
|
+
"composer-1"
|
|
10728
|
+
];
|
|
10682
10729
|
CursorAgentService = class CursorAgentService extends BaseCLIAgentService {
|
|
10683
10730
|
id = "cursor";
|
|
10684
10731
|
displayName = "Cursor";
|
|
@@ -13975,29 +14022,149 @@ async function recoverAgentState(ctx) {
|
|
|
13975
14022
|
const entries = ctx.deps.machine.listAgentEntries(ctx.machineId);
|
|
13976
14023
|
if (entries.length === 0) {
|
|
13977
14024
|
console.log(` No agent entries found — nothing to recover`);
|
|
13978
|
-
|
|
13979
|
-
|
|
13980
|
-
|
|
13981
|
-
|
|
13982
|
-
|
|
13983
|
-
|
|
13984
|
-
|
|
13985
|
-
|
|
13986
|
-
|
|
13987
|
-
|
|
13988
|
-
|
|
13989
|
-
|
|
13990
|
-
|
|
13991
|
-
|
|
13992
|
-
|
|
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`);
|
|
13993
14059
|
}
|
|
13994
14060
|
}
|
|
13995
|
-
console.log(` Recovery complete: ${recovered} alive, ${cleared} stale cleared`);
|
|
13996
14061
|
}
|
|
13997
14062
|
var init_state_recovery = __esm(() => {
|
|
14063
|
+
init_api3();
|
|
13998
14064
|
init_shared();
|
|
13999
14065
|
});
|
|
14000
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
|
+
|
|
14001
14168
|
// src/commands/machine/pid.ts
|
|
14002
14169
|
import { createHash } from "node:crypto";
|
|
14003
14170
|
import { existsSync as existsSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4, unlinkSync as unlinkSync2, mkdirSync as mkdirSync4 } from "node:fs";
|
|
@@ -14228,7 +14395,8 @@ function createDefaultDeps16() {
|
|
|
14228
14395
|
clock: {
|
|
14229
14396
|
now: () => Date.now(),
|
|
14230
14397
|
delay: (ms) => new Promise((resolve2) => setTimeout(resolve2, ms))
|
|
14231
|
-
}
|
|
14398
|
+
},
|
|
14399
|
+
spawning: new HarnessSpawningService({ rateLimiter: new SpawnRateLimiter })
|
|
14232
14400
|
};
|
|
14233
14401
|
}
|
|
14234
14402
|
function validateAuthentication(convexUrl) {
|
|
@@ -14345,7 +14513,9 @@ async function initDaemon() {
|
|
|
14345
14513
|
config: config3,
|
|
14346
14514
|
deps,
|
|
14347
14515
|
events,
|
|
14348
|
-
agentServices
|
|
14516
|
+
agentServices,
|
|
14517
|
+
activeWorkingDirs: new Set,
|
|
14518
|
+
lastPushedGitState: new Map
|
|
14349
14519
|
};
|
|
14350
14520
|
registerEventListeners(ctx);
|
|
14351
14521
|
logStartup(ctx, availableModels);
|
|
@@ -14360,6 +14530,7 @@ var init_init2 = __esm(() => {
|
|
|
14360
14530
|
init_machine();
|
|
14361
14531
|
init_intentional_stops();
|
|
14362
14532
|
init_remote_agents();
|
|
14533
|
+
init_harness_spawning();
|
|
14363
14534
|
init_error_formatting();
|
|
14364
14535
|
init_version();
|
|
14365
14536
|
init_pid();
|
|
@@ -14468,6 +14639,7 @@ async function executeStartAgent(ctx, args) {
|
|
|
14468
14639
|
const { pid } = spawnResult;
|
|
14469
14640
|
const msg = `Agent spawned (PID: ${pid})`;
|
|
14470
14641
|
console.log(` ✅ ${msg}`);
|
|
14642
|
+
ctx.deps.spawning.recordSpawn(chatroomId);
|
|
14471
14643
|
try {
|
|
14472
14644
|
await ctx.deps.backend.mutation(api.machines.updateSpawnedAgent, {
|
|
14473
14645
|
sessionId: ctx.sessionId,
|
|
@@ -14490,7 +14662,9 @@ async function executeStartAgent(ctx, args) {
|
|
|
14490
14662
|
harness: agentHarness,
|
|
14491
14663
|
model
|
|
14492
14664
|
});
|
|
14665
|
+
ctx.activeWorkingDirs.add(workingDir);
|
|
14493
14666
|
spawnResult.onExit(({ code: code2, signal }) => {
|
|
14667
|
+
ctx.deps.spawning.recordExit(chatroomId);
|
|
14494
14668
|
const pendingReason = ctx.deps.stops.consume(chatroomId, role);
|
|
14495
14669
|
const stopReason = pendingReason ?? resolveStopReason(code2, signal, false);
|
|
14496
14670
|
ctx.events.emit("agent:exited", {
|
|
@@ -14535,6 +14709,12 @@ async function onRequestStartAgent(ctx, event) {
|
|
|
14535
14709
|
console.log(`[daemon] ⏰ Skipping expired agent.requestStart for role=${event.role} (deadline passed)`);
|
|
14536
14710
|
return;
|
|
14537
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
|
+
}
|
|
14538
14718
|
await executeStartAgent(ctx, {
|
|
14539
14719
|
chatroomId: event.chatroomId,
|
|
14540
14720
|
role: event.role,
|
|
@@ -14648,6 +14828,514 @@ function handlePing() {
|
|
|
14648
14828
|
return { result: "pong", failed: false };
|
|
14649
14829
|
}
|
|
14650
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
|
+
|
|
14651
15339
|
// src/commands/machine/daemon-start/command-loop.ts
|
|
14652
15340
|
async function refreshModels(ctx) {
|
|
14653
15341
|
if (!ctx.config)
|
|
@@ -14670,7 +15358,7 @@ async function refreshModels(ctx) {
|
|
|
14670
15358
|
console.warn(`[${formatTimestamp()}] ⚠️ Model refresh failed: ${error.message}`);
|
|
14671
15359
|
}
|
|
14672
15360
|
}
|
|
14673
|
-
function evictStaleDedupEntries(processedCommandIds, processedPingIds) {
|
|
15361
|
+
function evictStaleDedupEntries(processedCommandIds, processedPingIds, processedGitRefreshIds) {
|
|
14674
15362
|
const evictBefore = Date.now() - AGENT_REQUEST_DEADLINE_MS;
|
|
14675
15363
|
for (const [id, ts] of processedCommandIds) {
|
|
14676
15364
|
if (ts < evictBefore)
|
|
@@ -14680,8 +15368,12 @@ function evictStaleDedupEntries(processedCommandIds, processedPingIds) {
|
|
|
14680
15368
|
if (ts < evictBefore)
|
|
14681
15369
|
processedPingIds.delete(id);
|
|
14682
15370
|
}
|
|
15371
|
+
for (const [id, ts] of processedGitRefreshIds) {
|
|
15372
|
+
if (ts < evictBefore)
|
|
15373
|
+
processedGitRefreshIds.delete(id);
|
|
15374
|
+
}
|
|
14683
15375
|
}
|
|
14684
|
-
async function dispatchCommandEvent(ctx, event, processedCommandIds, processedPingIds) {
|
|
15376
|
+
async function dispatchCommandEvent(ctx, event, processedCommandIds, processedPingIds, processedGitRefreshIds) {
|
|
14685
15377
|
const eventId = event._id.toString();
|
|
14686
15378
|
if (event.type === "agent.requestStart") {
|
|
14687
15379
|
if (processedCommandIds.has(eventId))
|
|
@@ -14703,6 +15395,14 @@ async function dispatchCommandEvent(ctx, event, processedCommandIds, processedPi
|
|
|
14703
15395
|
machineId: ctx.machineId,
|
|
14704
15396
|
pingEventId: event._id
|
|
14705
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);
|
|
14706
15406
|
}
|
|
14707
15407
|
}
|
|
14708
15408
|
async function startCommandLoop(ctx) {
|
|
@@ -14714,15 +15414,21 @@ async function startCommandLoop(ctx) {
|
|
|
14714
15414
|
}).then(() => {
|
|
14715
15415
|
heartbeatCount++;
|
|
14716
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
|
+
});
|
|
14717
15420
|
}).catch((err) => {
|
|
14718
15421
|
console.warn(`[${formatTimestamp()}] ⚠️ Daemon heartbeat failed: ${err.message}`);
|
|
14719
15422
|
});
|
|
14720
15423
|
}, DAEMON_HEARTBEAT_INTERVAL_MS);
|
|
14721
15424
|
heartbeatTimer.unref();
|
|
15425
|
+
const gitPollingHandle = startGitPollingLoop(ctx);
|
|
15426
|
+
pushGitState(ctx).catch(() => {});
|
|
14722
15427
|
const shutdown = async () => {
|
|
14723
15428
|
console.log(`
|
|
14724
15429
|
[${formatTimestamp()}] Shutting down...`);
|
|
14725
15430
|
clearInterval(heartbeatTimer);
|
|
15431
|
+
gitPollingHandle.stop();
|
|
14726
15432
|
await onDaemonShutdown(ctx);
|
|
14727
15433
|
releaseLock();
|
|
14728
15434
|
process.exit(0);
|
|
@@ -14737,17 +15443,18 @@ Listening for commands...`);
|
|
|
14737
15443
|
`);
|
|
14738
15444
|
const processedCommandIds = new Map;
|
|
14739
15445
|
const processedPingIds = new Map;
|
|
15446
|
+
const processedGitRefreshIds = new Map;
|
|
14740
15447
|
wsClient2.onUpdate(api.machines.getCommandEvents, {
|
|
14741
15448
|
sessionId: ctx.sessionId,
|
|
14742
15449
|
machineId: ctx.machineId
|
|
14743
15450
|
}, async (result) => {
|
|
14744
15451
|
if (!result.events || result.events.length === 0)
|
|
14745
15452
|
return;
|
|
14746
|
-
evictStaleDedupEntries(processedCommandIds, processedPingIds);
|
|
15453
|
+
evictStaleDedupEntries(processedCommandIds, processedPingIds, processedGitRefreshIds);
|
|
14747
15454
|
for (const event of result.events) {
|
|
14748
15455
|
try {
|
|
14749
15456
|
console.log(`[${formatTimestamp()}] \uD83D\uDCE1 Stream command event: ${event.type}`);
|
|
14750
|
-
await dispatchCommandEvent(ctx, event, processedCommandIds, processedPingIds);
|
|
15457
|
+
await dispatchCommandEvent(ctx, event, processedCommandIds, processedPingIds, processedGitRefreshIds);
|
|
14751
15458
|
} catch (err) {
|
|
14752
15459
|
console.error(`[${formatTimestamp()}] ❌ Stream command event failed: ${err.message}`);
|
|
14753
15460
|
}
|
|
@@ -14771,6 +15478,8 @@ var init_command_loop = __esm(() => {
|
|
|
14771
15478
|
init_on_request_start_agent();
|
|
14772
15479
|
init_on_request_stop_agent();
|
|
14773
15480
|
init_pid();
|
|
15481
|
+
init_git_polling();
|
|
15482
|
+
init_git_heartbeat();
|
|
14774
15483
|
MODEL_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
|
|
14775
15484
|
});
|
|
14776
15485
|
|