episoda 0.2.114 → 0.2.116
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.
|
@@ -2815,7 +2815,7 @@ var require_package = __commonJS({
|
|
|
2815
2815
|
"package.json"(exports2, module2) {
|
|
2816
2816
|
module2.exports = {
|
|
2817
2817
|
name: "episoda",
|
|
2818
|
-
version: "0.2.
|
|
2818
|
+
version: "0.2.115",
|
|
2819
2819
|
description: "CLI tool for Episoda local development workflow orchestration",
|
|
2820
2820
|
main: "dist/index.js",
|
|
2821
2821
|
types: "dist/index.d.ts",
|
|
@@ -8714,7 +8714,7 @@ var AgentManager = class {
|
|
|
8714
8714
|
* EP1173: Added autonomousMode parameter for permission-free execution
|
|
8715
8715
|
*/
|
|
8716
8716
|
async startSession(options) {
|
|
8717
|
-
const { sessionId, moduleId, moduleUid, projectPath, provider = "claude", autonomousMode = true, canWrite = true, readOnlyReason, mcpMode = "standard", message, credentials, systemPrompt, onChunk, onComplete, onError } = options;
|
|
8717
|
+
const { sessionId, moduleId, moduleUid, projectPath, provider = "claude", autonomousMode = true, canWrite = true, readOnlyReason, mcpMode = "standard", message, credentials, systemPrompt, onChunk, onToolUse, onComplete, onError } = options;
|
|
8718
8718
|
const existingSession = this.sessions.get(sessionId);
|
|
8719
8719
|
if (existingSession) {
|
|
8720
8720
|
if (existingSession.provider === provider && existingSession.moduleId === moduleId) {
|
|
@@ -8727,6 +8727,8 @@ var AgentManager = class {
|
|
|
8727
8727
|
canWrite,
|
|
8728
8728
|
readOnlyReason,
|
|
8729
8729
|
onChunk,
|
|
8730
|
+
onToolUse,
|
|
8731
|
+
// EP1236: Pass tool use callback
|
|
8730
8732
|
onComplete,
|
|
8731
8733
|
onError
|
|
8732
8734
|
});
|
|
@@ -8788,6 +8790,8 @@ var AgentManager = class {
|
|
|
8788
8790
|
message,
|
|
8789
8791
|
isFirstMessage: true,
|
|
8790
8792
|
onChunk,
|
|
8793
|
+
onToolUse,
|
|
8794
|
+
// EP1236: Pass tool use callback
|
|
8791
8795
|
onComplete,
|
|
8792
8796
|
onError
|
|
8793
8797
|
});
|
|
@@ -8799,7 +8803,7 @@ var AgentManager = class {
|
|
|
8799
8803
|
* EP1133: Supports both Claude Code and Codex CLI with provider-specific handling.
|
|
8800
8804
|
*/
|
|
8801
8805
|
async sendMessage(options) {
|
|
8802
|
-
const { sessionId, message, isFirstMessage, canWrite, readOnlyReason, agentSessionId, claudeSessionId, onChunk, onComplete, onError } = options;
|
|
8806
|
+
const { sessionId, message, isFirstMessage, canWrite, readOnlyReason, agentSessionId, claudeSessionId, onChunk, onToolUse, onComplete, onError } = options;
|
|
8803
8807
|
const session = this.sessions.get(sessionId);
|
|
8804
8808
|
if (!session) {
|
|
8805
8809
|
return { success: false, error: "Session not found" };
|
|
@@ -9099,6 +9103,7 @@ If changes are needed, explain what needs to be done.`;
|
|
|
9099
9103
|
let stdoutEventCount = 0;
|
|
9100
9104
|
let chunksSent = 0;
|
|
9101
9105
|
const streamStartTime = Date.now();
|
|
9106
|
+
let hasContent = false;
|
|
9102
9107
|
childProcess.stdout?.on("data", (data) => {
|
|
9103
9108
|
const rawData = data.toString();
|
|
9104
9109
|
stdoutBuffer += rawData;
|
|
@@ -9148,13 +9153,25 @@ If changes are needed, explain what needs to be done.`;
|
|
|
9148
9153
|
switch (parsed.type) {
|
|
9149
9154
|
case "assistant":
|
|
9150
9155
|
if (parsed.message?.content) {
|
|
9156
|
+
if (hasContent) {
|
|
9157
|
+
onChunk("\n\n");
|
|
9158
|
+
}
|
|
9151
9159
|
for (const block of parsed.message.content) {
|
|
9152
9160
|
if (block.type === "text" && block.text) {
|
|
9161
|
+
hasContent = true;
|
|
9153
9162
|
chunksSent++;
|
|
9154
9163
|
if (chunksSent <= 5) {
|
|
9155
9164
|
console.log(`[AgentManager] EP1191: Sending chunk ${chunksSent} via assistant event - ${block.text.length} chars`);
|
|
9156
9165
|
}
|
|
9157
9166
|
onChunk(block.text);
|
|
9167
|
+
} else if (block.type === "tool_use" && options.onToolUse) {
|
|
9168
|
+
const toolEvent = {
|
|
9169
|
+
id: block.id,
|
|
9170
|
+
name: block.name,
|
|
9171
|
+
input: block.input || {}
|
|
9172
|
+
};
|
|
9173
|
+
console.log(`[AgentManager] EP1236: Tool invoked - ${toolEvent.name} (${toolEvent.id})`);
|
|
9174
|
+
options.onToolUse(toolEvent);
|
|
9158
9175
|
}
|
|
9159
9176
|
}
|
|
9160
9177
|
}
|
|
@@ -9306,6 +9323,48 @@ If changes are needed, explain what needs to be done.`;
|
|
|
9306
9323
|
getAllSessions() {
|
|
9307
9324
|
return Array.from(this.sessions.values());
|
|
9308
9325
|
}
|
|
9326
|
+
/**
|
|
9327
|
+
* EP1237: Get all active sessions with their current status
|
|
9328
|
+
* Used for reconciliation after WebSocket reconnection.
|
|
9329
|
+
*
|
|
9330
|
+
* Returns session info needed for server to compare with DB state:
|
|
9331
|
+
* - sessionId: Local session identifier
|
|
9332
|
+
* - moduleId: Module UUID for DB lookup
|
|
9333
|
+
* - moduleUid: Module UID for logging
|
|
9334
|
+
* - status: Current local status
|
|
9335
|
+
* - agentSessionId: Provider's session ID for resume
|
|
9336
|
+
* - lastActivityAt: For staleness detection
|
|
9337
|
+
*/
|
|
9338
|
+
getActiveSessionsForReconciliation() {
|
|
9339
|
+
const result = [];
|
|
9340
|
+
for (const [sessionId, session] of this.sessions) {
|
|
9341
|
+
let reconStatus;
|
|
9342
|
+
switch (session.status) {
|
|
9343
|
+
case "starting":
|
|
9344
|
+
case "running":
|
|
9345
|
+
reconStatus = "running";
|
|
9346
|
+
break;
|
|
9347
|
+
case "stopping":
|
|
9348
|
+
case "stopped":
|
|
9349
|
+
reconStatus = "idle";
|
|
9350
|
+
break;
|
|
9351
|
+
case "error":
|
|
9352
|
+
reconStatus = "error";
|
|
9353
|
+
break;
|
|
9354
|
+
default:
|
|
9355
|
+
reconStatus = "idle";
|
|
9356
|
+
}
|
|
9357
|
+
result.push({
|
|
9358
|
+
sessionId,
|
|
9359
|
+
moduleId: session.moduleId,
|
|
9360
|
+
moduleUid: session.moduleUid,
|
|
9361
|
+
status: reconStatus,
|
|
9362
|
+
agentSessionId: session.agentSessionId || session.claudeSessionId,
|
|
9363
|
+
lastActivityAt: session.lastActivityAt
|
|
9364
|
+
});
|
|
9365
|
+
}
|
|
9366
|
+
return result;
|
|
9367
|
+
}
|
|
9309
9368
|
/**
|
|
9310
9369
|
* Check if a session exists
|
|
9311
9370
|
*/
|
|
@@ -9369,6 +9428,208 @@ If changes are needed, explain what needs to be done.`;
|
|
|
9369
9428
|
}
|
|
9370
9429
|
};
|
|
9371
9430
|
|
|
9431
|
+
// src/agent/agent-command-queue.ts
|
|
9432
|
+
var instance4 = null;
|
|
9433
|
+
function getAgentCommandQueue() {
|
|
9434
|
+
if (!instance4) {
|
|
9435
|
+
instance4 = new AgentCommandQueue();
|
|
9436
|
+
}
|
|
9437
|
+
return instance4;
|
|
9438
|
+
}
|
|
9439
|
+
var AgentCommandQueue = class {
|
|
9440
|
+
constructor() {
|
|
9441
|
+
/** Map of sessionId -> PendingCommand */
|
|
9442
|
+
this.pendingCommands = /* @__PURE__ */ new Map();
|
|
9443
|
+
/** Maximum retry attempts before giving up */
|
|
9444
|
+
this.maxRetries = 3;
|
|
9445
|
+
/** Command timeout in ms (commands older than this are discarded) */
|
|
9446
|
+
this.commandTimeout = 6e4;
|
|
9447
|
+
// 1 minute
|
|
9448
|
+
/** Cleanup interval handle */
|
|
9449
|
+
this.cleanupInterval = null;
|
|
9450
|
+
this.cleanupInterval = setInterval(() => {
|
|
9451
|
+
this.cleanup();
|
|
9452
|
+
}, 3e4);
|
|
9453
|
+
}
|
|
9454
|
+
/**
|
|
9455
|
+
* Add a command to the queue before processing.
|
|
9456
|
+
* If a command for this session already exists, it's replaced.
|
|
9457
|
+
*
|
|
9458
|
+
* @param sessionId - Agent session ID
|
|
9459
|
+
* @param command - The agent command to execute
|
|
9460
|
+
* @param commandId - WebSocket message ID for response routing
|
|
9461
|
+
*/
|
|
9462
|
+
enqueue(sessionId, command, commandId) {
|
|
9463
|
+
const existing = this.pendingCommands.get(sessionId);
|
|
9464
|
+
if (existing) {
|
|
9465
|
+
console.log(`[AgentCommandQueue] EP1237: Replacing existing command for session ${sessionId}`);
|
|
9466
|
+
}
|
|
9467
|
+
this.pendingCommands.set(sessionId, {
|
|
9468
|
+
command,
|
|
9469
|
+
commandId,
|
|
9470
|
+
receivedAt: Date.now(),
|
|
9471
|
+
retryCount: 0,
|
|
9472
|
+
state: "pending"
|
|
9473
|
+
});
|
|
9474
|
+
console.log(`[AgentCommandQueue] EP1237: Enqueued command for session ${sessionId} (action: ${command.action})`);
|
|
9475
|
+
}
|
|
9476
|
+
/**
|
|
9477
|
+
* Mark a command as being processed.
|
|
9478
|
+
* This prevents duplicate processing attempts.
|
|
9479
|
+
*
|
|
9480
|
+
* @param sessionId - Agent session ID
|
|
9481
|
+
*/
|
|
9482
|
+
markProcessing(sessionId) {
|
|
9483
|
+
const pending = this.pendingCommands.get(sessionId);
|
|
9484
|
+
if (pending) {
|
|
9485
|
+
pending.state = "processing";
|
|
9486
|
+
console.log(`[AgentCommandQueue] EP1237: Marked session ${sessionId} as processing`);
|
|
9487
|
+
}
|
|
9488
|
+
}
|
|
9489
|
+
/**
|
|
9490
|
+
* Remove a command from the queue after successful completion.
|
|
9491
|
+
*
|
|
9492
|
+
* @param sessionId - Agent session ID
|
|
9493
|
+
*/
|
|
9494
|
+
complete(sessionId) {
|
|
9495
|
+
const pending = this.pendingCommands.get(sessionId);
|
|
9496
|
+
if (pending) {
|
|
9497
|
+
console.log(`[AgentCommandQueue] EP1237: Completed command for session ${sessionId}`);
|
|
9498
|
+
this.pendingCommands.delete(sessionId);
|
|
9499
|
+
}
|
|
9500
|
+
}
|
|
9501
|
+
/**
|
|
9502
|
+
* Mark a command as failed (will not be retried).
|
|
9503
|
+
*
|
|
9504
|
+
* @param sessionId - Agent session ID
|
|
9505
|
+
* @param error - Error message for logging
|
|
9506
|
+
*/
|
|
9507
|
+
fail(sessionId, error) {
|
|
9508
|
+
const pending = this.pendingCommands.get(sessionId);
|
|
9509
|
+
if (pending) {
|
|
9510
|
+
pending.state = "failed";
|
|
9511
|
+
console.log(`[AgentCommandQueue] EP1237: Failed command for session ${sessionId}: ${error}`);
|
|
9512
|
+
this.pendingCommands.delete(sessionId);
|
|
9513
|
+
}
|
|
9514
|
+
}
|
|
9515
|
+
/**
|
|
9516
|
+
* Check if a session has a pending command.
|
|
9517
|
+
*
|
|
9518
|
+
* @param sessionId - Agent session ID
|
|
9519
|
+
* @returns True if there's a pending or processing command
|
|
9520
|
+
*/
|
|
9521
|
+
has(sessionId) {
|
|
9522
|
+
return this.pendingCommands.has(sessionId);
|
|
9523
|
+
}
|
|
9524
|
+
/**
|
|
9525
|
+
* Get a pending command by session ID.
|
|
9526
|
+
*
|
|
9527
|
+
* @param sessionId - Agent session ID
|
|
9528
|
+
* @returns The pending command or undefined
|
|
9529
|
+
*/
|
|
9530
|
+
get(sessionId) {
|
|
9531
|
+
return this.pendingCommands.get(sessionId);
|
|
9532
|
+
}
|
|
9533
|
+
/**
|
|
9534
|
+
* Get all commands that need retry after reconnect.
|
|
9535
|
+
* Returns commands that are:
|
|
9536
|
+
* - In 'pending' or 'processing' state (processing may have failed mid-way)
|
|
9537
|
+
* - Within the retry limit
|
|
9538
|
+
* - Not timed out
|
|
9539
|
+
*
|
|
9540
|
+
* @returns Array of pending commands eligible for retry
|
|
9541
|
+
*/
|
|
9542
|
+
getPendingCommands() {
|
|
9543
|
+
const now = Date.now();
|
|
9544
|
+
const retryable = [];
|
|
9545
|
+
for (const [sessionId, pending] of this.pendingCommands) {
|
|
9546
|
+
if (pending.state === "completed" || pending.state === "failed") {
|
|
9547
|
+
continue;
|
|
9548
|
+
}
|
|
9549
|
+
if (now - pending.receivedAt > this.commandTimeout) {
|
|
9550
|
+
console.log(`[AgentCommandQueue] EP1237: Command for session ${sessionId} timed out`);
|
|
9551
|
+
this.pendingCommands.delete(sessionId);
|
|
9552
|
+
continue;
|
|
9553
|
+
}
|
|
9554
|
+
if (pending.retryCount >= this.maxRetries) {
|
|
9555
|
+
console.log(`[AgentCommandQueue] EP1237: Command for session ${sessionId} exceeded max retries`);
|
|
9556
|
+
continue;
|
|
9557
|
+
}
|
|
9558
|
+
retryable.push(pending);
|
|
9559
|
+
}
|
|
9560
|
+
return retryable;
|
|
9561
|
+
}
|
|
9562
|
+
/**
|
|
9563
|
+
* Increment the retry count for a command.
|
|
9564
|
+
* Call this before retrying a command.
|
|
9565
|
+
*
|
|
9566
|
+
* @param sessionId - Agent session ID
|
|
9567
|
+
*/
|
|
9568
|
+
incrementRetryCount(sessionId) {
|
|
9569
|
+
const pending = this.pendingCommands.get(sessionId);
|
|
9570
|
+
if (pending) {
|
|
9571
|
+
pending.retryCount++;
|
|
9572
|
+
pending.state = "pending";
|
|
9573
|
+
console.log(`[AgentCommandQueue] EP1237: Retry ${pending.retryCount}/${this.maxRetries} for session ${sessionId}`);
|
|
9574
|
+
}
|
|
9575
|
+
}
|
|
9576
|
+
/**
|
|
9577
|
+
* Get commands that have exceeded the retry limit.
|
|
9578
|
+
* These should be reported as permanently failed.
|
|
9579
|
+
*
|
|
9580
|
+
* @returns Array of commands that exceeded retry limit
|
|
9581
|
+
*/
|
|
9582
|
+
getFailedCommands() {
|
|
9583
|
+
const failed = [];
|
|
9584
|
+
for (const pending of this.pendingCommands.values()) {
|
|
9585
|
+
if (pending.retryCount >= this.maxRetries && pending.state !== "completed" && pending.state !== "failed") {
|
|
9586
|
+
failed.push(pending);
|
|
9587
|
+
}
|
|
9588
|
+
}
|
|
9589
|
+
return failed;
|
|
9590
|
+
}
|
|
9591
|
+
/**
|
|
9592
|
+
* Clean up stale commands (older than timeout).
|
|
9593
|
+
* Called periodically and on reconnect.
|
|
9594
|
+
*/
|
|
9595
|
+
cleanup() {
|
|
9596
|
+
const now = Date.now();
|
|
9597
|
+
let cleaned = 0;
|
|
9598
|
+
for (const [sessionId, pending] of this.pendingCommands) {
|
|
9599
|
+
if (now - pending.receivedAt > this.commandTimeout) {
|
|
9600
|
+
this.pendingCommands.delete(sessionId);
|
|
9601
|
+
cleaned++;
|
|
9602
|
+
}
|
|
9603
|
+
}
|
|
9604
|
+
if (cleaned > 0) {
|
|
9605
|
+
console.log(`[AgentCommandQueue] EP1237: Cleaned up ${cleaned} stale command(s)`);
|
|
9606
|
+
}
|
|
9607
|
+
}
|
|
9608
|
+
/**
|
|
9609
|
+
* Get the maximum number of retries.
|
|
9610
|
+
*/
|
|
9611
|
+
getMaxRetries() {
|
|
9612
|
+
return this.maxRetries;
|
|
9613
|
+
}
|
|
9614
|
+
/**
|
|
9615
|
+
* Get the current queue size.
|
|
9616
|
+
*/
|
|
9617
|
+
size() {
|
|
9618
|
+
return this.pendingCommands.size;
|
|
9619
|
+
}
|
|
9620
|
+
/**
|
|
9621
|
+
* Shutdown the queue (stop cleanup interval).
|
|
9622
|
+
* Call this when the daemon is shutting down.
|
|
9623
|
+
*/
|
|
9624
|
+
shutdown() {
|
|
9625
|
+
if (this.cleanupInterval) {
|
|
9626
|
+
clearInterval(this.cleanupInterval);
|
|
9627
|
+
this.cleanupInterval = null;
|
|
9628
|
+
}
|
|
9629
|
+
console.log(`[AgentCommandQueue] EP1237: Shutdown with ${this.pendingCommands.size} pending command(s)`);
|
|
9630
|
+
}
|
|
9631
|
+
};
|
|
9632
|
+
|
|
9372
9633
|
// src/utils/dev-server.ts
|
|
9373
9634
|
var import_child_process13 = require("child_process");
|
|
9374
9635
|
var import_core11 = __toESM(require_dist());
|
|
@@ -10680,7 +10941,13 @@ var Daemon = class _Daemon {
|
|
|
10680
10941
|
const cmd = message.command;
|
|
10681
10942
|
console.log(`[Daemon] EP912: Received agent command for ${projectId}:`, cmd.action);
|
|
10682
10943
|
client.updateActivity();
|
|
10944
|
+
const commandQueue = getAgentCommandQueue();
|
|
10945
|
+
if (cmd.action === "start" || cmd.action === "message") {
|
|
10946
|
+
commandQueue.enqueue(cmd.sessionId, cmd, message.id);
|
|
10947
|
+
commandQueue.markProcessing(cmd.sessionId);
|
|
10948
|
+
}
|
|
10683
10949
|
let daemonChunkCount = 0;
|
|
10950
|
+
let daemonToolUseCount = 0;
|
|
10684
10951
|
const daemonStreamStart = Date.now();
|
|
10685
10952
|
const createStreamingCallbacks = (sessionId, commandId) => ({
|
|
10686
10953
|
onChunk: async (chunk) => {
|
|
@@ -10698,9 +10965,29 @@ var Daemon = class _Daemon {
|
|
|
10698
10965
|
console.error(`[Daemon] EP912: Failed to send chunk (WebSocket may be disconnected):`, sendError);
|
|
10699
10966
|
}
|
|
10700
10967
|
},
|
|
10968
|
+
// EP1236: Forward tool invocations to platform for chat UI visibility
|
|
10969
|
+
onToolUse: async (event) => {
|
|
10970
|
+
daemonToolUseCount++;
|
|
10971
|
+
console.log(`[Daemon] EP1236: Forwarding tool_use ${daemonToolUseCount} via WebSocket - ${event.name}`);
|
|
10972
|
+
try {
|
|
10973
|
+
await client.send({
|
|
10974
|
+
type: "agent_result",
|
|
10975
|
+
commandId,
|
|
10976
|
+
result: {
|
|
10977
|
+
success: true,
|
|
10978
|
+
status: "tool_use",
|
|
10979
|
+
sessionId,
|
|
10980
|
+
toolUse: event
|
|
10981
|
+
}
|
|
10982
|
+
});
|
|
10983
|
+
} catch (sendError) {
|
|
10984
|
+
console.error(`[Daemon] EP1236: Failed to send tool_use (WebSocket may be disconnected):`, sendError);
|
|
10985
|
+
}
|
|
10986
|
+
},
|
|
10701
10987
|
onComplete: async (claudeSessionId) => {
|
|
10702
10988
|
const duration = Date.now() - daemonStreamStart;
|
|
10703
|
-
console.log(`[Daemon] EP1191: Stream complete - ${daemonChunkCount} chunks forwarded in ${duration}ms`);
|
|
10989
|
+
console.log(`[Daemon] EP1191: Stream complete - ${daemonChunkCount} chunks, ${daemonToolUseCount} tool uses forwarded in ${duration}ms`);
|
|
10990
|
+
commandQueue.complete(sessionId);
|
|
10704
10991
|
try {
|
|
10705
10992
|
await client.send({
|
|
10706
10993
|
type: "agent_result",
|
|
@@ -10714,6 +11001,7 @@ var Daemon = class _Daemon {
|
|
|
10714
11001
|
onError: async (error) => {
|
|
10715
11002
|
const duration = Date.now() - daemonStreamStart;
|
|
10716
11003
|
console.log(`[Daemon] EP1191: Stream error after ${daemonChunkCount} chunks in ${duration}ms - ${error}`);
|
|
11004
|
+
commandQueue.complete(sessionId);
|
|
10717
11005
|
try {
|
|
10718
11006
|
await client.send({
|
|
10719
11007
|
type: "agent_result",
|
|
@@ -10832,6 +11120,8 @@ var Daemon = class _Daemon {
|
|
|
10832
11120
|
});
|
|
10833
11121
|
console.log(`[Daemon] EP912: Agent command ${cmd.action} completed for session ${cmd.action === "start" || cmd.action === "message" ? cmd.sessionId : cmd.sessionId}`);
|
|
10834
11122
|
} catch (error) {
|
|
11123
|
+
const sessionId = cmd.sessionId || "unknown";
|
|
11124
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
10835
11125
|
try {
|
|
10836
11126
|
await client.send({
|
|
10837
11127
|
type: "agent_result",
|
|
@@ -10839,12 +11129,13 @@ var Daemon = class _Daemon {
|
|
|
10839
11129
|
result: {
|
|
10840
11130
|
success: false,
|
|
10841
11131
|
status: "error",
|
|
10842
|
-
sessionId
|
|
10843
|
-
error:
|
|
11132
|
+
sessionId,
|
|
11133
|
+
error: errorMsg
|
|
10844
11134
|
}
|
|
10845
11135
|
});
|
|
11136
|
+
commandQueue.complete(sessionId);
|
|
10846
11137
|
} catch (sendError) {
|
|
10847
|
-
console.error(`[Daemon]
|
|
11138
|
+
console.error(`[Daemon] EP1237: Failed to send error result (WebSocket disconnected), command will be retried on reconnect`);
|
|
10848
11139
|
}
|
|
10849
11140
|
console.error(`[Daemon] EP912: Agent command execution error:`, error);
|
|
10850
11141
|
}
|
|
@@ -10974,6 +11265,9 @@ var Daemon = class _Daemon {
|
|
|
10974
11265
|
this.reconcileWorktrees(projectId, projectPath, client).catch((err) => {
|
|
10975
11266
|
console.warn("[Daemon] EP1003: Reconciliation report failed:", err.message);
|
|
10976
11267
|
});
|
|
11268
|
+
this.runAgentReconciliation(projectId, projectPath, client).catch((err) => {
|
|
11269
|
+
console.warn("[Daemon] EP1237: Agent reconciliation failed:", err.message);
|
|
11270
|
+
});
|
|
10977
11271
|
});
|
|
10978
11272
|
client.on("module_state_changed", async (message) => {
|
|
10979
11273
|
if (message.type === "module_state_changed") {
|
|
@@ -11027,6 +11321,22 @@ var Daemon = class _Daemon {
|
|
|
11027
11321
|
console.error("[Daemon] EP1095: Error handling machine_uuid_update:", error instanceof Error ? error.message : error);
|
|
11028
11322
|
}
|
|
11029
11323
|
});
|
|
11324
|
+
client.on("agent_reconciliation_commands", async (message) => {
|
|
11325
|
+
try {
|
|
11326
|
+
const cmdMsg = message;
|
|
11327
|
+
console.log(`[Daemon] EP1237: Received agent reconciliation commands - abort: ${cmdMsg.abort.length}, restart: ${cmdMsg.restart.length}`);
|
|
11328
|
+
const agentManager = getAgentManager();
|
|
11329
|
+
for (const sessionId of cmdMsg.abort) {
|
|
11330
|
+
console.log(`[Daemon] EP1237: Aborting orphaned session ${sessionId}`);
|
|
11331
|
+
await agentManager.abortSession(sessionId);
|
|
11332
|
+
}
|
|
11333
|
+
if (cmdMsg.restart.length > 0) {
|
|
11334
|
+
console.log(`[Daemon] EP1237: Server will send agent_command for ${cmdMsg.restart.length} session(s) that need restart`);
|
|
11335
|
+
}
|
|
11336
|
+
} catch (error) {
|
|
11337
|
+
console.error("[Daemon] EP1237: Error handling agent_reconciliation_commands:", error instanceof Error ? error.message : error);
|
|
11338
|
+
}
|
|
11339
|
+
});
|
|
11030
11340
|
client.on("disconnected", (event) => {
|
|
11031
11341
|
const disconnectEvent = event;
|
|
11032
11342
|
console.log(`[Daemon] Connection closed for ${projectId}: code=${disconnectEvent.code}, willReconnect=${disconnectEvent.willReconnect}`);
|
|
@@ -11508,6 +11818,121 @@ var Daemon = class _Daemon {
|
|
|
11508
11818
|
// EP1025: Removed cleanupModuleWorktree - was dead code (never called).
|
|
11509
11819
|
// EP1188: Background ops queue removed - worktree cleanup now handled by Inngest events
|
|
11510
11820
|
// (worktree/cleanup) which send WebSocket commands to connected daemons via ws-proxy.
|
|
11821
|
+
/**
|
|
11822
|
+
* EP1237: Run agent reconciliation steps sequentially.
|
|
11823
|
+
*
|
|
11824
|
+
* This orchestrates the two reconciliation steps in order:
|
|
11825
|
+
* 1. Report pending commands (updates retry counts, reports failures)
|
|
11826
|
+
* 2. Reconcile sessions (reports active sessions to server)
|
|
11827
|
+
*
|
|
11828
|
+
* Running these sequentially avoids race conditions where retry logic
|
|
11829
|
+
* might modify state while reconciliation is reading it.
|
|
11830
|
+
*/
|
|
11831
|
+
async runAgentReconciliation(projectId, projectPath, client) {
|
|
11832
|
+
console.log("[Daemon] EP1237: Running agent reconciliation sequence");
|
|
11833
|
+
try {
|
|
11834
|
+
await this.reportPendingAgentCommands(projectId, projectPath, client);
|
|
11835
|
+
} catch (err) {
|
|
11836
|
+
console.warn("[Daemon] EP1237: Report pending commands failed:", err instanceof Error ? err.message : err);
|
|
11837
|
+
}
|
|
11838
|
+
try {
|
|
11839
|
+
await this.reconcileAgentSessions(projectId, client);
|
|
11840
|
+
} catch (err) {
|
|
11841
|
+
console.warn("[Daemon] EP1237: Reconcile sessions failed:", err instanceof Error ? err.message : err);
|
|
11842
|
+
}
|
|
11843
|
+
console.log("[Daemon] EP1237: Agent reconciliation sequence complete");
|
|
11844
|
+
}
|
|
11845
|
+
/**
|
|
11846
|
+
* EP1237: Report pending agent commands after WebSocket reconnection.
|
|
11847
|
+
*
|
|
11848
|
+
* Commands that were processing when the WebSocket disconnected need attention.
|
|
11849
|
+
* We report their status via agent_reconciliation_report, and the server will
|
|
11850
|
+
* re-send commands for sessions that need to be restarted.
|
|
11851
|
+
*
|
|
11852
|
+
* This is safer than trying to re-process locally because:
|
|
11853
|
+
* 1. Credentials may have expired and server needs to refresh them
|
|
11854
|
+
* 2. Session state in DB needs to be consistent with our local state
|
|
11855
|
+
* 3. Server can make informed decisions about which sessions to restart
|
|
11856
|
+
*/
|
|
11857
|
+
async reportPendingAgentCommands(projectId, projectPath, client) {
|
|
11858
|
+
const commandQueue = getAgentCommandQueue();
|
|
11859
|
+
const pendingCommands = commandQueue.getPendingCommands();
|
|
11860
|
+
const failedCommands = commandQueue.getFailedCommands();
|
|
11861
|
+
if (pendingCommands.length === 0 && failedCommands.length === 0) {
|
|
11862
|
+
console.log("[Daemon] EP1237: No pending agent commands");
|
|
11863
|
+
return;
|
|
11864
|
+
}
|
|
11865
|
+
console.log(`[Daemon] EP1237: Found ${pendingCommands.length} pending and ${failedCommands.length} failed command(s)`);
|
|
11866
|
+
for (const pending of failedCommands) {
|
|
11867
|
+
const cmd = pending.command;
|
|
11868
|
+
const sessionId = cmd.sessionId || "unknown";
|
|
11869
|
+
console.log(`[Daemon] EP1237: Reporting failed command for session ${sessionId} (exceeded max retries)`);
|
|
11870
|
+
try {
|
|
11871
|
+
await client.send({
|
|
11872
|
+
type: "agent_result",
|
|
11873
|
+
commandId: pending.commandId,
|
|
11874
|
+
result: {
|
|
11875
|
+
success: false,
|
|
11876
|
+
status: "error",
|
|
11877
|
+
sessionId,
|
|
11878
|
+
error: `Command failed after ${pending.retryCount} retry attempts during WebSocket reconnection`
|
|
11879
|
+
}
|
|
11880
|
+
});
|
|
11881
|
+
commandQueue.fail(sessionId, "Max retries exceeded");
|
|
11882
|
+
} catch (sendError) {
|
|
11883
|
+
console.error("[Daemon] EP1237: Failed to report max retry failure:", sendError);
|
|
11884
|
+
}
|
|
11885
|
+
}
|
|
11886
|
+
for (const pending of pendingCommands) {
|
|
11887
|
+
const cmd = pending.command;
|
|
11888
|
+
const sessionId = cmd.sessionId || "unknown";
|
|
11889
|
+
if (pending.retryCount >= commandQueue.getMaxRetries()) {
|
|
11890
|
+
console.log(`[Daemon] EP1237: Command for session ${sessionId} will exceed max retries on next attempt`);
|
|
11891
|
+
commandQueue.fail(sessionId, "Max retries exceeded");
|
|
11892
|
+
continue;
|
|
11893
|
+
}
|
|
11894
|
+
commandQueue.incrementRetryCount(sessionId);
|
|
11895
|
+
console.log(`[Daemon] EP1237: Command for session ${sessionId} ready for retry (attempt ${pending.retryCount + 1}/${commandQueue.getMaxRetries()})`);
|
|
11896
|
+
}
|
|
11897
|
+
}
|
|
11898
|
+
/**
|
|
11899
|
+
* EP1237: Reconcile agent sessions on connect/reconnect.
|
|
11900
|
+
*
|
|
11901
|
+
* Reports active local agent sessions to the server so it can compare with DB state.
|
|
11902
|
+
* Server responds with abort/restart commands to fix any mismatches.
|
|
11903
|
+
*/
|
|
11904
|
+
async reconcileAgentSessions(projectId, client) {
|
|
11905
|
+
console.log(`[Daemon] EP1237: Starting agent session reconciliation for project ${projectId}`);
|
|
11906
|
+
try {
|
|
11907
|
+
if (!this.machineUuid) {
|
|
11908
|
+
console.log("[Daemon] EP1237: Cannot reconcile agent sessions - machineUuid not available yet");
|
|
11909
|
+
return;
|
|
11910
|
+
}
|
|
11911
|
+
const agentManager = getAgentManager();
|
|
11912
|
+
const activeSessions = agentManager.getActiveSessionsForReconciliation();
|
|
11913
|
+
console.log(`[Daemon] EP1237: Reporting ${activeSessions.length} active agent session(s)`);
|
|
11914
|
+
const report = {
|
|
11915
|
+
projectId,
|
|
11916
|
+
machineId: this.machineUuid,
|
|
11917
|
+
sessions: activeSessions.map((s) => ({
|
|
11918
|
+
sessionId: s.sessionId,
|
|
11919
|
+
moduleId: s.moduleId,
|
|
11920
|
+
moduleUid: s.moduleUid,
|
|
11921
|
+
status: s.status,
|
|
11922
|
+
agentSessionId: s.agentSessionId,
|
|
11923
|
+
lastActivityAt: s.lastActivityAt.toISOString()
|
|
11924
|
+
}))
|
|
11925
|
+
};
|
|
11926
|
+
await client.send({
|
|
11927
|
+
type: "agent_reconciliation_report",
|
|
11928
|
+
report
|
|
11929
|
+
});
|
|
11930
|
+
console.log("[Daemon] EP1237: Agent session reconciliation report sent - awaiting server commands");
|
|
11931
|
+
} catch (error) {
|
|
11932
|
+
console.error("[Daemon] EP1237: Agent session reconciliation error:", error instanceof Error ? error.message : error);
|
|
11933
|
+
throw error;
|
|
11934
|
+
}
|
|
11935
|
+
}
|
|
11511
11936
|
/**
|
|
11512
11937
|
* EP1002: Handle worktree_setup command from server
|
|
11513
11938
|
* This provides a unified setup flow for both local and cloud environments.
|