Update files

This commit is contained in:
Mohamed Boudra
2026-01-26 18:03:51 +07:00
parent 6095bf975b
commit c233c99cd3
19 changed files with 774 additions and 178 deletions
+131 -63
View File
@@ -213,6 +213,16 @@ const DEFAULT_RECONNECT_BASE_DELAY_MS = 1500;
const DEFAULT_RECONNECT_MAX_DELAY_MS = 30000;
const DEFAULT_MESSAGE_QUEUE_LIMIT = 0;
/** Default timeout for waiting for connection before sending queued messages */
const DEFAULT_SEND_QUEUE_TIMEOUT_MS = 10000;
interface PendingSend {
message: SessionInboundMessage;
resolve: () => void;
reject: (error: Error) => void;
timeoutHandle: ReturnType<typeof setTimeout>;
}
export class DaemonClientV2 {
private transport: DaemonTransport | null = null;
private transportCleanup: Array<() => void> = [];
@@ -238,6 +248,7 @@ export class DaemonClientV2 {
private messageQueueLimit: number | null;
private agentIndex: Map<string, AgentSnapshotPayload> = new Map();
private logger: Logger;
private pendingSendQueue: PendingSend[] = [];
constructor(private config: DaemonClientV2Config) {
this.messageQueueLimit =
@@ -308,6 +319,7 @@ export class DaemonClientV2 {
this.lastErrorValue = null;
this.reconnectAttempt = 0;
this.updateConnectionState({ status: "connected" });
this.flushPendingSendQueue();
this.resolveConnect();
}),
transport.onClose((event) => {
@@ -493,12 +505,17 @@ export class DaemonClientV2 {
// Core Send Helpers
// ============================================================================
/**
* Send a session message. For fire-and-forget messages (heartbeats, etc.),
* failures are suppressed if `suppressSendErrors` is configured.
* For RPC methods that wait for responses, use `sendSessionMessageOrThrow` instead.
*/
private sendSessionMessage(message: SessionInboundMessage): void {
if (!this.transport || this.connectionState.status !== "connected") {
if (this.config.suppressSendErrors) {
return;
}
throw new Error("Transport not connected");
throw new Error(`Transport not connected (status: ${this.connectionState.status})`);
}
const payload = SessionInboundMessageSchema.parse(message);
try {
@@ -511,6 +528,78 @@ export class DaemonClientV2 {
}
}
/**
* Send a session message for RPC methods that create waiters.
* If the connection is still being established ("connecting"), the message
* is queued and will be sent once connected (or rejected after timeout).
* This prevents waiters from hanging forever when called during connection.
*/
private sendSessionMessageOrThrow(message: SessionInboundMessage): Promise<void> {
const status = this.connectionState.status;
// If connected, send immediately
if (this.transport && status === "connected") {
const payload = SessionInboundMessageSchema.parse(message);
this.transport.send(JSON.stringify({ type: "session", message: payload }));
return Promise.resolve();
}
// If connecting, queue the message to be sent once connected
if (status === "connecting") {
return new Promise((resolve, reject) => {
const timeoutHandle = setTimeout(() => {
// Remove from queue
const idx = this.pendingSendQueue.findIndex((p) => p.resolve === resolve);
if (idx !== -1) {
this.pendingSendQueue.splice(idx, 1);
}
reject(new Error(`Timed out waiting for connection to send message`));
}, DEFAULT_SEND_QUEUE_TIMEOUT_MS);
this.pendingSendQueue.push({ message, resolve, reject, timeoutHandle });
});
}
// Not connected and not connecting - fail immediately
return Promise.reject(new Error(`Transport not connected (status: ${status})`));
}
/**
* Flush pending send queue - called when connection is established.
*/
private flushPendingSendQueue(): void {
const queue = this.pendingSendQueue;
this.pendingSendQueue = [];
for (const pending of queue) {
clearTimeout(pending.timeoutHandle);
try {
if (this.transport && this.connectionState.status === "connected") {
const payload = SessionInboundMessageSchema.parse(pending.message);
this.transport.send(JSON.stringify({ type: "session", message: payload }));
pending.resolve();
} else {
pending.reject(new Error("Connection lost before message could be sent"));
}
} catch (error) {
pending.reject(error instanceof Error ? error : new Error(String(error)));
}
}
}
/**
* Reject all pending sends - called when connection fails or is closed.
*/
private rejectPendingSendQueue(error: Error): void {
const queue = this.pendingSendQueue;
this.pendingSendQueue = [];
for (const pending of queue) {
clearTimeout(pending.timeoutHandle);
pending.reject(error);
}
}
private sendSessionMessageStrict(message: SessionInboundMessage): void {
if (!this.transport || this.connectionState.status !== "connected") {
throw new Error("Transport not connected");
@@ -589,7 +678,7 @@ export class DaemonClientV2 {
10000,
{ skipQueue: true }
);
this.sendSessionMessage(message);
await this.sendSessionMessageOrThrow(message);
return response;
}
@@ -612,7 +701,7 @@ export class DaemonClientV2 {
10000,
{ skipQueue: true }
);
this.sendSessionMessage(message);
await this.sendSessionMessageOrThrow(message);
return response;
}
@@ -639,7 +728,7 @@ export class DaemonClientV2 {
10000,
{ skipQueue: true }
);
this.sendSessionMessage(message);
await this.sendSessionMessageOrThrow(message);
return response;
}
@@ -682,7 +771,7 @@ export class DaemonClientV2 {
{ skipQueue: true }
);
this.sendSessionMessage(message);
await this.sendSessionMessageOrThrow(message);
const status = await statusPromise;
if (status.status === "agent_create_failed") {
throw new Error(status.error);
@@ -723,7 +812,7 @@ export class DaemonClientV2 {
{ skipQueue: true }
);
this.sendSessionMessage(message);
await this.sendSessionMessageOrThrow(message);
return response;
}
@@ -747,7 +836,7 @@ export class DaemonClientV2 {
10000,
{ skipQueue: true }
);
this.sendSessionMessage(message);
await this.sendSessionMessageOrThrow(message);
await response;
}
@@ -790,7 +879,7 @@ export class DaemonClientV2 {
{ skipQueue: true }
);
this.sendSessionMessage(message);
await this.sendSessionMessageOrThrow(message);
const status = await statusPromise;
return this.waitForAgentState(
@@ -824,7 +913,7 @@ export class DaemonClientV2 {
15000,
{ skipQueue: true }
);
this.sendSessionMessage(message);
await this.sendSessionMessageOrThrow(message);
return response;
}
@@ -851,7 +940,7 @@ export class DaemonClientV2 {
10000,
{ skipQueue: true }
);
this.sendSessionMessage(message);
await this.sendSessionMessageOrThrow(message);
const payload = await response;
if (payload.error) {
throw new Error(payload.error);
@@ -926,7 +1015,7 @@ export class DaemonClientV2 {
{ skipQueue: true }
);
this.sendSessionMessage(message);
await this.sendSessionMessageOrThrow(message);
return response;
}
@@ -1046,10 +1135,6 @@ export class DaemonClientV2 {
if (!requestId) {
const existing = this.checkoutStatusInFlight.get(agentId);
if (existing) {
this.logger.debug(
{ agentId, inFlightCount: this.checkoutStatusInFlight.size },
"getCheckoutStatus: returning existing in-flight request"
);
return existing;
}
}
@@ -1061,11 +1146,6 @@ export class DaemonClientV2 {
requestId: resolvedRequestId,
});
this.logger.debug(
{ agentId, requestId: resolvedRequestId, waiterCount: this.waiters.size },
"getCheckoutStatus: creating new request"
);
const responsePromise = (async () => {
const response = this.waitFor(
(msg) => {
@@ -1080,30 +1160,17 @@ export class DaemonClientV2 {
60000,
{ skipQueue: true }
);
this.sendSessionMessage(message);
await this.sendSessionMessageOrThrow(message);
return response;
})();
if (!requestId) {
this.checkoutStatusInFlight.set(agentId, responsePromise);
responsePromise
.then(() => {
this.logger.debug(
{ agentId, requestId: resolvedRequestId },
"getCheckoutStatus: request completed successfully"
);
})
.catch((err) => {
this.logger.debug(
{ agentId, requestId: resolvedRequestId, error: err?.message },
"getCheckoutStatus: request failed"
);
})
.finally(() => {
if (this.checkoutStatusInFlight.get(agentId) === responsePromise) {
this.checkoutStatusInFlight.delete(agentId);
}
});
responsePromise.finally(() => {
if (this.checkoutStatusInFlight.get(agentId) === responsePromise) {
this.checkoutStatusInFlight.delete(agentId);
}
});
}
return responsePromise;
@@ -1134,7 +1201,7 @@ export class DaemonClientV2 {
60000,
{ skipQueue: true }
);
this.sendSessionMessage(message);
await this.sendSessionMessageOrThrow(message);
return response;
}
@@ -1164,7 +1231,7 @@ export class DaemonClientV2 {
60000,
{ skipQueue: true }
);
this.sendSessionMessage(message);
await this.sendSessionMessageOrThrow(message);
return response;
}
@@ -1195,7 +1262,7 @@ export class DaemonClientV2 {
60000,
{ skipQueue: true }
);
this.sendSessionMessage(message);
await this.sendSessionMessageOrThrow(message);
return response;
}
@@ -1225,7 +1292,7 @@ export class DaemonClientV2 {
60000,
{ skipQueue: true }
);
this.sendSessionMessage(message);
await this.sendSessionMessageOrThrow(message);
return response;
}
@@ -1249,7 +1316,7 @@ export class DaemonClientV2 {
60000,
{ skipQueue: true }
);
this.sendSessionMessage(message);
await this.sendSessionMessageOrThrow(message);
return response;
}
@@ -1280,7 +1347,7 @@ export class DaemonClientV2 {
60000,
{ skipQueue: true }
);
this.sendSessionMessage(message);
await this.sendSessionMessageOrThrow(message);
return response;
}
@@ -1307,7 +1374,7 @@ export class DaemonClientV2 {
60000,
{ skipQueue: true }
);
this.sendSessionMessage(message);
await this.sendSessionMessageOrThrow(message);
return response;
}
@@ -1335,7 +1402,7 @@ export class DaemonClientV2 {
60000,
{ skipQueue: true }
);
this.sendSessionMessage(message);
await this.sendSessionMessageOrThrow(message);
return response;
}
@@ -1364,7 +1431,7 @@ export class DaemonClientV2 {
20000,
{ skipQueue: true }
);
this.sendSessionMessage(message);
await this.sendSessionMessageOrThrow(message);
return response;
}
@@ -1391,7 +1458,7 @@ export class DaemonClientV2 {
10000,
{ skipQueue: true }
);
this.sendSessionMessage(message);
await this.sendSessionMessageOrThrow(message);
return response;
}
@@ -1418,7 +1485,7 @@ export class DaemonClientV2 {
10000,
{ skipQueue: true }
);
this.sendSessionMessage(message);
await this.sendSessionMessageOrThrow(message);
return response;
}
@@ -1464,7 +1531,7 @@ export class DaemonClientV2 {
10000,
{ skipQueue: true }
);
this.sendSessionMessage(message);
await this.sendSessionMessageOrThrow(message);
return response;
}
@@ -1499,7 +1566,7 @@ export class DaemonClientV2 {
10000,
{ skipQueue: true }
);
this.sendSessionMessage(message);
await this.sendSessionMessageOrThrow(message);
return response;
}
@@ -1528,7 +1595,7 @@ export class DaemonClientV2 {
10000,
{ skipQueue: true }
);
this.sendSessionMessage(message);
await this.sendSessionMessageOrThrow(message);
return response;
}
@@ -1560,7 +1627,7 @@ export class DaemonClientV2 {
30000,
{ skipQueue: true }
);
this.sendSessionMessage(message);
await this.sendSessionMessageOrThrow(message);
return response;
}
@@ -1587,7 +1654,7 @@ export class DaemonClientV2 {
30000,
{ skipQueue: true }
);
this.sendSessionMessage(message);
await this.sendSessionMessageOrThrow(message);
return response;
}
@@ -1618,7 +1685,7 @@ export class DaemonClientV2 {
30000,
{ skipQueue: true }
);
this.sendSessionMessage(message);
await this.sendSessionMessageOrThrow(message);
return response;
}
@@ -1667,7 +1734,7 @@ export class DaemonClientV2 {
timeout,
{ skipQueue: true }
);
this.sendSessionMessage(message);
await this.sendSessionMessageOrThrow(message);
return resolved;
}
@@ -1907,7 +1974,7 @@ export class DaemonClientV2 {
10000,
{ skipQueue: true }
);
this.sendSessionMessage(message);
await this.sendSessionMessageOrThrow(message);
return response;
}
@@ -1936,7 +2003,7 @@ export class DaemonClientV2 {
10000,
{ skipQueue: true }
);
this.sendSessionMessage(message);
await this.sendSessionMessageOrThrow(message);
return response;
}
@@ -1963,7 +2030,7 @@ export class DaemonClientV2 {
10000,
{ skipQueue: true }
);
this.sendSessionMessage(message);
await this.sendSessionMessageOrThrow(message);
return response;
}
@@ -2008,7 +2075,7 @@ export class DaemonClientV2 {
10000,
{ skipQueue: true }
);
this.sendSessionMessage(message);
await this.sendSessionMessageOrThrow(message);
return response;
}
@@ -2121,9 +2188,10 @@ export class DaemonClientV2 {
this.lastErrorValue = reason.trim();
}
// Clear all pending waiters since the connection was lost and responses
// from the previous connection will never arrive.
// Clear all pending waiters and queued sends since the connection was lost
// and responses from the previous connection will never arrive.
this.clearWaiters(new Error(reason ?? "Connection lost"));
this.rejectPendingSendQueue(new Error(reason ?? "Connection lost"));
this.updateConnectionState({
status: "disconnected",
@@ -848,6 +848,10 @@ export class AgentManager {
if (!this.registry) {
return;
}
// Don't persist internal agents - they're ephemeral system tasks
if (agent.internal) {
return;
}
await this.registry.applySnapshot(agent, options);
}
+2 -1
View File
@@ -3930,10 +3930,11 @@ export class Session {
);
// Add persisted agents that have not been lazily initialized yet
// (excluding internal agents which are for ephemeral system tasks)
const registryRecords = await this.agentStorage.list();
const liveIds = new Set(agentSnapshots.map((a) => a.id));
const persistedAgents = registryRecords
.filter((record) => !liveIds.has(record.id))
.filter((record) => !liveIds.has(record.id) && !record.internal)
.map((record) => this.buildStoredAgentPayload(record));
const agents = [...liveAgents, ...persistedAgents];