pi-app-server 0.1.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/LICENSE +21 -0
- package/README.md +195 -0
- package/dist/command-classification.d.ts +59 -0
- package/dist/command-classification.d.ts.map +1 -0
- package/dist/command-classification.js +78 -0
- package/dist/command-classification.js.map +7 -0
- package/dist/command-execution-engine.d.ts +118 -0
- package/dist/command-execution-engine.d.ts.map +1 -0
- package/dist/command-execution-engine.js +259 -0
- package/dist/command-execution-engine.js.map +7 -0
- package/dist/command-replay-store.d.ts +241 -0
- package/dist/command-replay-store.d.ts.map +1 -0
- package/dist/command-replay-store.js +306 -0
- package/dist/command-replay-store.js.map +7 -0
- package/dist/command-router.d.ts +25 -0
- package/dist/command-router.d.ts.map +1 -0
- package/dist/command-router.js +353 -0
- package/dist/command-router.js.map +7 -0
- package/dist/extension-ui.d.ts +139 -0
- package/dist/extension-ui.d.ts.map +1 -0
- package/dist/extension-ui.js +189 -0
- package/dist/extension-ui.js.map +7 -0
- package/dist/resource-governor.d.ts +254 -0
- package/dist/resource-governor.d.ts.map +1 -0
- package/dist/resource-governor.js +603 -0
- package/dist/resource-governor.js.map +7 -0
- package/dist/server-command-handlers.d.ts +120 -0
- package/dist/server-command-handlers.d.ts.map +1 -0
- package/dist/server-command-handlers.js +234 -0
- package/dist/server-command-handlers.js.map +7 -0
- package/dist/server-ui-context.d.ts +22 -0
- package/dist/server-ui-context.d.ts.map +1 -0
- package/dist/server-ui-context.js +221 -0
- package/dist/server-ui-context.js.map +7 -0
- package/dist/server.d.ts +82 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +561 -0
- package/dist/server.js.map +7 -0
- package/dist/session-lock-manager.d.ts +100 -0
- package/dist/session-lock-manager.d.ts.map +1 -0
- package/dist/session-lock-manager.js +199 -0
- package/dist/session-lock-manager.js.map +7 -0
- package/dist/session-manager.d.ts +196 -0
- package/dist/session-manager.d.ts.map +1 -0
- package/dist/session-manager.js +1010 -0
- package/dist/session-manager.js.map +7 -0
- package/dist/session-store.d.ts +190 -0
- package/dist/session-store.d.ts.map +1 -0
- package/dist/session-store.js +446 -0
- package/dist/session-store.js.map +7 -0
- package/dist/session-version-store.d.ts +83 -0
- package/dist/session-version-store.d.ts.map +1 -0
- package/dist/session-version-store.js +117 -0
- package/dist/session-version-store.js.map +7 -0
- package/dist/type-guards.d.ts +59 -0
- package/dist/type-guards.d.ts.map +1 -0
- package/dist/type-guards.js +40 -0
- package/dist/type-guards.js.map +7 -0
- package/dist/types.d.ts +621 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +23 -0
- package/dist/types.js.map +7 -0
- package/dist/validation.d.ts +22 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +323 -0
- package/dist/validation.js.map +7 -0
- package/package.json +135 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { getSessionId } from "./types.js";
|
|
2
|
+
import { getCommandTimeoutPolicy } from "./command-classification.js";
|
|
3
|
+
const DEFAULT_COMMAND_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
4
|
+
const SHORT_COMMAND_TIMEOUT_MS = 30 * 1e3;
|
|
5
|
+
const DEFAULT_DEPENDENCY_WAIT_TIMEOUT_MS = 30 * 1e3;
|
|
6
|
+
const DEFAULT_ABORT_HANDLERS = {
|
|
7
|
+
prompt: (session) => session.abort(),
|
|
8
|
+
steer: (session) => session.abort(),
|
|
9
|
+
follow_up: (session) => session.abort(),
|
|
10
|
+
compact: (session) => session.abortCompaction(),
|
|
11
|
+
bash: (session) => session.abortBash(),
|
|
12
|
+
new_session: (session) => session.abort(),
|
|
13
|
+
switch_session_file: (session) => session.abort(),
|
|
14
|
+
fork: (session) => session.abort()
|
|
15
|
+
};
|
|
16
|
+
function withTimeout(promise, timeoutMs, commandType, onTimeout) {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
let settled = false;
|
|
19
|
+
const timer = setTimeout(() => {
|
|
20
|
+
if (settled) return;
|
|
21
|
+
settled = true;
|
|
22
|
+
let onTimeoutPromise;
|
|
23
|
+
try {
|
|
24
|
+
onTimeoutPromise = Promise.resolve(onTimeout?.());
|
|
25
|
+
} catch {
|
|
26
|
+
onTimeoutPromise = Promise.resolve();
|
|
27
|
+
}
|
|
28
|
+
onTimeoutPromise.catch(() => {
|
|
29
|
+
}).finally(() => {
|
|
30
|
+
reject(new Error(`Command '${commandType}' timed out after ${timeoutMs}ms`));
|
|
31
|
+
});
|
|
32
|
+
}, timeoutMs);
|
|
33
|
+
promise.then((result) => {
|
|
34
|
+
if (settled) return;
|
|
35
|
+
settled = true;
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
resolve(result);
|
|
38
|
+
}).catch((error) => {
|
|
39
|
+
if (settled) return;
|
|
40
|
+
settled = true;
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
reject(error);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
class CommandExecutionEngine {
|
|
47
|
+
/** Deterministic per-lane command serialization tails. */
|
|
48
|
+
laneTails = /* @__PURE__ */ new Map();
|
|
49
|
+
replayStore;
|
|
50
|
+
versionStore;
|
|
51
|
+
sessionResolver;
|
|
52
|
+
abortHandlers;
|
|
53
|
+
defaultCommandTimeoutMs;
|
|
54
|
+
shortCommandTimeoutMs;
|
|
55
|
+
dependencyWaitTimeoutMs;
|
|
56
|
+
constructor(replayStore, versionStore, sessionResolver, options = {}) {
|
|
57
|
+
this.replayStore = replayStore;
|
|
58
|
+
this.versionStore = versionStore;
|
|
59
|
+
this.sessionResolver = sessionResolver;
|
|
60
|
+
this.abortHandlers = { ...DEFAULT_ABORT_HANDLERS, ...options.abortHandlers };
|
|
61
|
+
this.defaultCommandTimeoutMs = typeof options.defaultCommandTimeoutMs === "number" && options.defaultCommandTimeoutMs > 0 ? options.defaultCommandTimeoutMs : DEFAULT_COMMAND_TIMEOUT_MS;
|
|
62
|
+
this.shortCommandTimeoutMs = typeof options.shortCommandTimeoutMs === "number" && options.shortCommandTimeoutMs > 0 ? options.shortCommandTimeoutMs : SHORT_COMMAND_TIMEOUT_MS;
|
|
63
|
+
this.dependencyWaitTimeoutMs = typeof options.dependencyWaitTimeoutMs === "number" && options.dependencyWaitTimeoutMs > 0 ? options.dependencyWaitTimeoutMs : DEFAULT_DEPENDENCY_WAIT_TIMEOUT_MS;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Get statistics about the execution engine state.
|
|
67
|
+
*/
|
|
68
|
+
getStats() {
|
|
69
|
+
return { laneCount: this.laneTails.size };
|
|
70
|
+
}
|
|
71
|
+
// ==========================================================================
|
|
72
|
+
// LANE SERIALIZATION
|
|
73
|
+
// ==========================================================================
|
|
74
|
+
/**
|
|
75
|
+
* Get the lane key for a command.
|
|
76
|
+
* Session commands serialize per-session; server commands serialize together.
|
|
77
|
+
*/
|
|
78
|
+
getLaneKey(command) {
|
|
79
|
+
const sessionId = getSessionId(command);
|
|
80
|
+
if (sessionId) return `session:${sessionId}`;
|
|
81
|
+
return "server";
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Run a task in a deterministic serialized lane.
|
|
85
|
+
* Commands in the same lane execute sequentially.
|
|
86
|
+
*/
|
|
87
|
+
async runOnLane(laneKey, task) {
|
|
88
|
+
const previousTail = this.laneTails.get(laneKey) ?? Promise.resolve();
|
|
89
|
+
let releaseCurrent;
|
|
90
|
+
const currentTail = new Promise((resolve) => {
|
|
91
|
+
releaseCurrent = resolve;
|
|
92
|
+
});
|
|
93
|
+
const laneTail = previousTail.then(
|
|
94
|
+
() => currentTail,
|
|
95
|
+
() => currentTail
|
|
96
|
+
);
|
|
97
|
+
this.laneTails.set(laneKey, laneTail);
|
|
98
|
+
await previousTail.catch((error) => {
|
|
99
|
+
if (error !== void 0) {
|
|
100
|
+
console.error(`[CommandExecutionEngine] Previous lane task failed for ${laneKey}:`, error);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
try {
|
|
104
|
+
return await task();
|
|
105
|
+
} finally {
|
|
106
|
+
releaseCurrent?.();
|
|
107
|
+
if (this.laneTails.get(laneKey) === laneTail) {
|
|
108
|
+
this.laneTails.delete(laneKey);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// ==========================================================================
|
|
113
|
+
// DEPENDENCY RESOLUTION
|
|
114
|
+
// ==========================================================================
|
|
115
|
+
/**
|
|
116
|
+
* Wait for dependency commands to complete.
|
|
117
|
+
* Returns error if any dependency fails or times out.
|
|
118
|
+
*
|
|
119
|
+
* Note: Cross-lane dependency cycles (A→B, B→A) are detected by timeout
|
|
120
|
+
* rather than explicit cycle detection. This is acceptable because:
|
|
121
|
+
* 1. Cross-lane dependencies are rare
|
|
122
|
+
* 2. The dependencyWaitTimeoutMs (default 30s) prevents indefinite deadlock
|
|
123
|
+
* 3. Same-lane cycles are explicitly detected below
|
|
124
|
+
*/
|
|
125
|
+
async awaitDependencies(dependsOn, laneKey) {
|
|
126
|
+
for (const dependencyId of dependsOn) {
|
|
127
|
+
if (!dependencyId) {
|
|
128
|
+
return { ok: false, error: "Dependency ID must be non-empty" };
|
|
129
|
+
}
|
|
130
|
+
const inFlight = this.replayStore.getInFlight(dependencyId);
|
|
131
|
+
if (inFlight) {
|
|
132
|
+
if (inFlight.laneKey === laneKey) {
|
|
133
|
+
return {
|
|
134
|
+
ok: false,
|
|
135
|
+
error: `Dependency '${dependencyId}' is queued in the same lane and cannot be awaited from this command`
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
const dependencyResponse = await withTimeout(
|
|
140
|
+
inFlight.promise,
|
|
141
|
+
this.dependencyWaitTimeoutMs,
|
|
142
|
+
`dependsOn:${dependencyId}`
|
|
143
|
+
);
|
|
144
|
+
if (!dependencyResponse.success) {
|
|
145
|
+
return {
|
|
146
|
+
ok: false,
|
|
147
|
+
error: `Dependency '${dependencyId}' failed: ${dependencyResponse.error ?? "unknown error"}`
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
continue;
|
|
151
|
+
} catch (error) {
|
|
152
|
+
return {
|
|
153
|
+
ok: false,
|
|
154
|
+
error: error instanceof Error ? error.message : String(error)
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const completed = this.replayStore.getCommandOutcome(dependencyId);
|
|
159
|
+
if (!completed) {
|
|
160
|
+
return { ok: false, error: `Dependency '${dependencyId}' is unknown` };
|
|
161
|
+
}
|
|
162
|
+
if (!completed.success) {
|
|
163
|
+
return {
|
|
164
|
+
ok: false,
|
|
165
|
+
error: `Dependency '${dependencyId}' failed: ${completed.error ?? "unknown error"}`
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return { ok: true };
|
|
170
|
+
}
|
|
171
|
+
// ==========================================================================
|
|
172
|
+
// TIMEOUT MANAGEMENT
|
|
173
|
+
// ==========================================================================
|
|
174
|
+
/**
|
|
175
|
+
* Resolve timeout policy for a command.
|
|
176
|
+
* Returns null for commands that cannot be safely cancelled.
|
|
177
|
+
*/
|
|
178
|
+
getCommandTimeoutMs(commandType) {
|
|
179
|
+
return getCommandTimeoutPolicy(commandType, {
|
|
180
|
+
defaultTimeoutMs: this.defaultCommandTimeoutMs,
|
|
181
|
+
shortTimeoutMs: this.shortCommandTimeoutMs
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Best-effort cancellation for timed-out commands.
|
|
186
|
+
* Uses configured abort handlers (defaults + custom overrides).
|
|
187
|
+
*/
|
|
188
|
+
async abortTimedOutCommand(command) {
|
|
189
|
+
const sessionId = getSessionId(command);
|
|
190
|
+
if (!sessionId) return;
|
|
191
|
+
const session = this.sessionResolver.getSession(sessionId);
|
|
192
|
+
if (!session) return;
|
|
193
|
+
const abortHandler = this.abortHandlers[command.type];
|
|
194
|
+
if (!abortHandler) return;
|
|
195
|
+
try {
|
|
196
|
+
await Promise.resolve(abortHandler(session));
|
|
197
|
+
} catch (error) {
|
|
198
|
+
console.error(
|
|
199
|
+
`[timeout] Failed to abort timed out command '${command.type}' for session ${sessionId}:`,
|
|
200
|
+
error
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Execute a command with timeout.
|
|
206
|
+
*/
|
|
207
|
+
async executeWithTimeout(commandType, promise, command) {
|
|
208
|
+
const timeoutMs = this.getCommandTimeoutMs(commandType);
|
|
209
|
+
if (timeoutMs === null) {
|
|
210
|
+
return promise;
|
|
211
|
+
}
|
|
212
|
+
return withTimeout(promise, timeoutMs, commandType, () => this.abortTimedOutCommand(command));
|
|
213
|
+
}
|
|
214
|
+
// ==========================================================================
|
|
215
|
+
// VERSION CHECKS
|
|
216
|
+
// ==========================================================================
|
|
217
|
+
/**
|
|
218
|
+
* Check if a session version matches the expected version.
|
|
219
|
+
* Returns error object if mismatch, undefined if OK.
|
|
220
|
+
*
|
|
221
|
+
* @param sessionId - The session to check
|
|
222
|
+
* @param ifSessionVersion - The expected version
|
|
223
|
+
* @param commandType - The command type for error context (preserves caller's context)
|
|
224
|
+
*/
|
|
225
|
+
checkSessionVersion(sessionId, ifSessionVersion, commandType) {
|
|
226
|
+
const current = this.versionStore.getVersion(sessionId);
|
|
227
|
+
if (current === void 0) {
|
|
228
|
+
return {
|
|
229
|
+
type: "response",
|
|
230
|
+
command: commandType,
|
|
231
|
+
success: false,
|
|
232
|
+
error: `Session ${sessionId} not found for ifSessionVersion=${ifSessionVersion}`
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
if (current !== ifSessionVersion) {
|
|
236
|
+
return {
|
|
237
|
+
type: "response",
|
|
238
|
+
command: commandType,
|
|
239
|
+
success: false,
|
|
240
|
+
error: `Session version mismatch: expected ${ifSessionVersion}, got ${current}`
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
return void 0;
|
|
244
|
+
}
|
|
245
|
+
// ==========================================================================
|
|
246
|
+
// LIFECYCLE
|
|
247
|
+
// ==========================================================================
|
|
248
|
+
/**
|
|
249
|
+
* Clear all lane state (used during disposal).
|
|
250
|
+
*/
|
|
251
|
+
clear() {
|
|
252
|
+
this.laneTails.clear();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
export {
|
|
256
|
+
CommandExecutionEngine,
|
|
257
|
+
withTimeout
|
|
258
|
+
};
|
|
259
|
+
//# sourceMappingURL=command-execution-engine.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/command-execution-engine.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Command Execution Engine - manages lane serialization, dependency waits, and timeouts.\n *\n * Responsibilities:\n * - Deterministic per-lane command serialization\n * - Dependency resolution with timeout\n * - Command timeout orchestration with abort hooks\n * - Lifecycle event emission\n */\n\nimport type { AgentSession } from \"@mariozechner/pi-coding-agent\";\nimport type { RpcCommand, RpcResponse, SessionResolver } from \"./types.js\";\nimport { getSessionId } from \"./types.js\";\nimport type { CommandReplayStore } from \"./command-replay-store.js\";\nimport type { SessionVersionStore } from \"./session-version-store.js\";\nimport { getCommandTimeoutPolicy } from \"./command-classification.js\";\n\n/** Default timeout for session commands (5 minutes for LLM operations) */\nconst DEFAULT_COMMAND_TIMEOUT_MS = 5 * 60 * 1000;\n\n/** Short command timeout (30 seconds) */\nconst SHORT_COMMAND_TIMEOUT_MS = 30 * 1000;\n\n/** Max time to wait for a dependency command to complete. */\nconst DEFAULT_DEPENDENCY_WAIT_TIMEOUT_MS = 30 * 1000;\n\n/**\n * Abort handler for a specific command type.\n * Called when a command times out to attempt cancellation.\n */\nexport type AbortHandler = (session: AgentSession) => void | Promise<void>;\n\n/**\n * Default abort handlers for built-in command types.\n * Maps command types to their abort methods on AgentSession.\n */\nconst DEFAULT_ABORT_HANDLERS: Partial<Record<string, AbortHandler>> = {\n prompt: (session) => session.abort(),\n steer: (session) => session.abort(),\n follow_up: (session) => session.abort(),\n compact: (session) => session.abortCompaction(),\n bash: (session) => session.abortBash(),\n new_session: (session) => session.abort(),\n switch_session_file: (session) => session.abort(),\n fork: (session) => session.abort(),\n};\n\n/**\n * Wrap a promise with a timeout.\n * Returns the promise result or throws on timeout.\n */\nexport function withTimeout<T>(\n promise: Promise<T>,\n timeoutMs: number,\n commandType: string,\n onTimeout?: () => void | Promise<void>\n): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n let settled = false;\n\n const timer = setTimeout(() => {\n if (settled) return;\n settled = true;\n\n // Wrap onTimeout in try-catch to handle both sync and async errors\n let onTimeoutPromise: Promise<void>;\n try {\n onTimeoutPromise = Promise.resolve(onTimeout?.());\n } catch {\n onTimeoutPromise = Promise.resolve();\n }\n\n onTimeoutPromise\n .catch(() => {\n // Ignore cancellation hook errors; timeout response still returned.\n })\n .finally(() => {\n reject(new Error(`Command '${commandType}' timed out after ${timeoutMs}ms`));\n });\n }, timeoutMs);\n\n promise\n .then((result) => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n resolve(result);\n })\n .catch((error) => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n reject(error);\n });\n });\n}\n\n/**\n * Configuration options for the execution engine.\n */\nexport interface ExecutionEngineOptions {\n defaultCommandTimeoutMs?: number;\n shortCommandTimeoutMs?: number;\n dependencyWaitTimeoutMs?: number;\n /** Custom abort handlers for command types (extends defaults) */\n abortHandlers?: Partial<Record<string, AbortHandler>>;\n}\n\n/**\n * Command Execution Engine - manages lane serialization and dependency waits.\n *\n * Extracted from PiSessionManager to isolate:\n * - Per-lane command serialization\n * - Dependency resolution with timeout\n * - Command timeout with abort hooks\n */\nexport class CommandExecutionEngine {\n /** Deterministic per-lane command serialization tails. */\n private laneTails = new Map<string, Promise<void>>();\n\n private readonly replayStore: CommandReplayStore;\n private readonly versionStore: SessionVersionStore;\n private readonly sessionResolver: SessionResolver;\n private readonly abortHandlers: Partial<Record<string, AbortHandler>>;\n\n private readonly defaultCommandTimeoutMs: number;\n private readonly shortCommandTimeoutMs: number;\n private readonly dependencyWaitTimeoutMs: number;\n\n constructor(\n replayStore: CommandReplayStore,\n versionStore: SessionVersionStore,\n sessionResolver: SessionResolver,\n options: ExecutionEngineOptions = {}\n ) {\n this.replayStore = replayStore;\n this.versionStore = versionStore;\n this.sessionResolver = sessionResolver;\n\n // Merge custom abort handlers with defaults (custom takes precedence)\n this.abortHandlers = { ...DEFAULT_ABORT_HANDLERS, ...options.abortHandlers };\n\n this.defaultCommandTimeoutMs =\n typeof options.defaultCommandTimeoutMs === \"number\" && options.defaultCommandTimeoutMs > 0\n ? options.defaultCommandTimeoutMs\n : DEFAULT_COMMAND_TIMEOUT_MS;\n this.shortCommandTimeoutMs =\n typeof options.shortCommandTimeoutMs === \"number\" && options.shortCommandTimeoutMs > 0\n ? options.shortCommandTimeoutMs\n : SHORT_COMMAND_TIMEOUT_MS;\n this.dependencyWaitTimeoutMs =\n typeof options.dependencyWaitTimeoutMs === \"number\" && options.dependencyWaitTimeoutMs > 0\n ? options.dependencyWaitTimeoutMs\n : DEFAULT_DEPENDENCY_WAIT_TIMEOUT_MS;\n }\n\n /**\n * Get statistics about the execution engine state.\n */\n getStats(): { laneCount: number } {\n return { laneCount: this.laneTails.size };\n }\n\n // ==========================================================================\n // LANE SERIALIZATION\n // ==========================================================================\n\n /**\n * Get the lane key for a command.\n * Session commands serialize per-session; server commands serialize together.\n */\n getLaneKey(command: RpcCommand): string {\n const sessionId = getSessionId(command);\n if (sessionId) return `session:${sessionId}`;\n return \"server\";\n }\n\n /**\n * Run a task in a deterministic serialized lane.\n * Commands in the same lane execute sequentially.\n */\n async runOnLane<T>(laneKey: string, task: () => Promise<T>): Promise<T> {\n const previousTail = this.laneTails.get(laneKey) ?? Promise.resolve();\n\n let releaseCurrent: (() => void) | undefined;\n const currentTail = new Promise<void>((resolve) => {\n releaseCurrent = resolve;\n });\n\n // Store the lane tail promise for later comparison\n const laneTail = previousTail.then(\n () => currentTail,\n () => currentTail\n );\n this.laneTails.set(laneKey, laneTail);\n\n await previousTail.catch((error) => {\n // Previous command failure should not break lane sequencing.\n // Log for observability but continue.\n if (error !== undefined) {\n console.error(`[CommandExecutionEngine] Previous lane task failed for ${laneKey}:`, error);\n }\n });\n\n try {\n return await task();\n } finally {\n releaseCurrent?.();\n // Only delete if our lane tail is still the current one (not replaced by another task)\n if (this.laneTails.get(laneKey) === laneTail) {\n this.laneTails.delete(laneKey);\n }\n }\n }\n\n // ==========================================================================\n // DEPENDENCY RESOLUTION\n // ==========================================================================\n\n /**\n * Wait for dependency commands to complete.\n * Returns error if any dependency fails or times out.\n *\n * Note: Cross-lane dependency cycles (A\u2192B, B\u2192A) are detected by timeout\n * rather than explicit cycle detection. This is acceptable because:\n * 1. Cross-lane dependencies are rare\n * 2. The dependencyWaitTimeoutMs (default 30s) prevents indefinite deadlock\n * 3. Same-lane cycles are explicitly detected below\n */\n async awaitDependencies(\n dependsOn: string[],\n laneKey: string\n ): Promise<{ ok: true } | { ok: false; error: string }> {\n for (const dependencyId of dependsOn) {\n if (!dependencyId) {\n return { ok: false, error: \"Dependency ID must be non-empty\" };\n }\n\n const inFlight = this.replayStore.getInFlight(dependencyId);\n if (inFlight) {\n // Same-lane check: commands in the same lane execute sequentially,\n // so waiting for a same-lane dependency would deadlock\n if (inFlight.laneKey === laneKey) {\n return {\n ok: false,\n error: `Dependency '${dependencyId}' is queued in the same lane and cannot be awaited from this command`,\n };\n }\n\n try {\n const dependencyResponse = await withTimeout(\n inFlight.promise,\n this.dependencyWaitTimeoutMs,\n `dependsOn:${dependencyId}`\n );\n if (!dependencyResponse.success) {\n return {\n ok: false,\n error: `Dependency '${dependencyId}' failed: ${dependencyResponse.error ?? \"unknown error\"}`,\n };\n }\n continue;\n } catch (error) {\n return {\n ok: false,\n error: error instanceof Error ? error.message : String(error),\n };\n }\n }\n\n const completed = this.replayStore.getCommandOutcome(dependencyId);\n if (!completed) {\n return { ok: false, error: `Dependency '${dependencyId}' is unknown` };\n }\n\n if (!completed.success) {\n return {\n ok: false,\n error: `Dependency '${dependencyId}' failed: ${completed.error ?? \"unknown error\"}`,\n };\n }\n }\n\n return { ok: true };\n }\n\n // ==========================================================================\n // TIMEOUT MANAGEMENT\n // ==========================================================================\n\n /**\n * Resolve timeout policy for a command.\n * Returns null for commands that cannot be safely cancelled.\n */\n getCommandTimeoutMs(commandType: string): number | null {\n return getCommandTimeoutPolicy(commandType, {\n defaultTimeoutMs: this.defaultCommandTimeoutMs,\n shortTimeoutMs: this.shortCommandTimeoutMs,\n });\n }\n\n /**\n * Best-effort cancellation for timed-out commands.\n * Uses configured abort handlers (defaults + custom overrides).\n */\n async abortTimedOutCommand(command: RpcCommand): Promise<void> {\n const sessionId = getSessionId(command);\n if (!sessionId) return;\n\n const session = this.sessionResolver.getSession(sessionId);\n if (!session) return;\n\n const abortHandler = this.abortHandlers[command.type];\n if (!abortHandler) return;\n\n try {\n await Promise.resolve(abortHandler(session));\n } catch (error) {\n console.error(\n `[timeout] Failed to abort timed out command '${command.type}' for session ${sessionId}:`,\n error\n );\n }\n }\n\n /**\n * Execute a command with timeout.\n */\n async executeWithTimeout(\n commandType: string,\n promise: Promise<RpcResponse>,\n command: RpcCommand\n ): Promise<RpcResponse> {\n const timeoutMs = this.getCommandTimeoutMs(commandType);\n\n if (timeoutMs === null) {\n return promise;\n }\n\n return withTimeout(promise, timeoutMs, commandType, () => this.abortTimedOutCommand(command));\n }\n\n // ==========================================================================\n // VERSION CHECKS\n // ==========================================================================\n\n /**\n * Check if a session version matches the expected version.\n * Returns error object if mismatch, undefined if OK.\n *\n * @param sessionId - The session to check\n * @param ifSessionVersion - The expected version\n * @param commandType - The command type for error context (preserves caller's context)\n */\n checkSessionVersion(\n sessionId: string,\n ifSessionVersion: number,\n commandType: string\n ): { type: \"response\"; command: string; success: false; error: string } | undefined {\n const current = this.versionStore.getVersion(sessionId);\n if (current === undefined) {\n return {\n type: \"response\",\n command: commandType,\n success: false,\n error: `Session ${sessionId} not found for ifSessionVersion=${ifSessionVersion}`,\n };\n }\n if (current !== ifSessionVersion) {\n return {\n type: \"response\",\n command: commandType,\n success: false,\n error: `Session version mismatch: expected ${ifSessionVersion}, got ${current}`,\n };\n }\n return undefined;\n }\n\n // ==========================================================================\n // LIFECYCLE\n // ==========================================================================\n\n /**\n * Clear all lane state (used during disposal).\n */\n clear(): void {\n this.laneTails.clear();\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command Replay Store - manages idempotency, duplicate detection, and outcome history.
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities:
|
|
5
|
+
* - Idempotency key replay (cached responses for retry safety)
|
|
6
|
+
* - Command ID deduplication (completed outcomes + in-flight tracking)
|
|
7
|
+
* - Fingerprint conflict detection (prevent same ID with different payload)
|
|
8
|
+
* - Bounded outcome retention (LRU-style trimming)
|
|
9
|
+
*/
|
|
10
|
+
import type { RpcCommand, RpcResponse } from "./types.js";
|
|
11
|
+
/** Reserved prefix for server-generated command IDs. Client IDs matching this are rejected. */
|
|
12
|
+
export declare const SYNTHETIC_ID_PREFIX = "anon:";
|
|
13
|
+
/**
|
|
14
|
+
* Record of a completed command execution.
|
|
15
|
+
* Used for dependency resolution and duplicate-id replay.
|
|
16
|
+
*/
|
|
17
|
+
export interface CommandOutcomeRecord {
|
|
18
|
+
commandId: string;
|
|
19
|
+
commandType: string;
|
|
20
|
+
laneKey: string;
|
|
21
|
+
fingerprint: string;
|
|
22
|
+
success: boolean;
|
|
23
|
+
error?: string;
|
|
24
|
+
response: RpcResponse;
|
|
25
|
+
sessionVersion?: number;
|
|
26
|
+
finishedAt: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Record of an in-flight command execution.
|
|
30
|
+
* Used for dependency waits and duplicate-id replay.
|
|
31
|
+
*/
|
|
32
|
+
export interface InFlightCommandRecord {
|
|
33
|
+
commandType: string;
|
|
34
|
+
laneKey: string;
|
|
35
|
+
fingerprint: string;
|
|
36
|
+
promise: Promise<RpcResponse>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Input for caching an idempotency result.
|
|
40
|
+
*/
|
|
41
|
+
export interface IdempotencyCacheInput {
|
|
42
|
+
command: RpcCommand;
|
|
43
|
+
idempotencyKey: string;
|
|
44
|
+
commandType: string;
|
|
45
|
+
fingerprint: string;
|
|
46
|
+
response: RpcResponse;
|
|
47
|
+
}
|
|
48
|
+
export interface IdempotencyCacheEntry {
|
|
49
|
+
expiresAt: number;
|
|
50
|
+
commandType: string;
|
|
51
|
+
fingerprint: string;
|
|
52
|
+
response: RpcResponse;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Result of checking for replay conflicts.
|
|
56
|
+
*
|
|
57
|
+
* This discriminated union enables type-safe handling of all replay scenarios:
|
|
58
|
+
*
|
|
59
|
+
* - `proceed`: No replay possible, execute the command normally
|
|
60
|
+
* - `conflict`: Fingerprint mismatch - same ID/key but different payload
|
|
61
|
+
* - `replay_cached`: Found cached response, return it immediately
|
|
62
|
+
* - `replay_inflight`: Command with same ID is executing, await its promise
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```typescript
|
|
66
|
+
* const result = store.checkReplay(command, fingerprint);
|
|
67
|
+
* switch (result.kind) {
|
|
68
|
+
* case "proceed": // execute normally
|
|
69
|
+
* case "conflict": return result.response; // error response
|
|
70
|
+
* case "replay_cached": return result.response; // cached response
|
|
71
|
+
* case "replay_inflight": return await result.promise; // wait for in-flight
|
|
72
|
+
* }
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export type ReplayCheckResult = {
|
|
76
|
+
kind: "proceed";
|
|
77
|
+
} | {
|
|
78
|
+
kind: "conflict";
|
|
79
|
+
response: RpcResponse;
|
|
80
|
+
} | {
|
|
81
|
+
kind: "replay_cached";
|
|
82
|
+
response: RpcResponse;
|
|
83
|
+
} | {
|
|
84
|
+
kind: "replay_inflight";
|
|
85
|
+
promise: Promise<RpcResponse>;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Configuration options for the replay store.
|
|
89
|
+
*/
|
|
90
|
+
export interface ReplayStoreOptions {
|
|
91
|
+
idempotencyTtlMs?: number;
|
|
92
|
+
maxCommandOutcomes?: number;
|
|
93
|
+
/** Maximum in-flight commands to track. Excess is rejected (ADR-0001: reject, don't evict). */
|
|
94
|
+
maxInFlightCommands?: number;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Statistics about store state for monitoring.
|
|
98
|
+
*/
|
|
99
|
+
export interface ReplayStoreStats {
|
|
100
|
+
/** Number of in-flight commands being tracked */
|
|
101
|
+
inFlightCount: number;
|
|
102
|
+
/** Number of completed command outcomes stored */
|
|
103
|
+
outcomeCount: number;
|
|
104
|
+
/** Number of idempotency cache entries */
|
|
105
|
+
idempotencyCacheSize: number;
|
|
106
|
+
/** Maximum configured in-flight commands */
|
|
107
|
+
maxInFlightCommands: number;
|
|
108
|
+
/** Maximum configured command outcomes */
|
|
109
|
+
maxCommandOutcomes: number;
|
|
110
|
+
/** Count of in-flight rejections due to exceeding max (ADR-0001: reject, don't evict) */
|
|
111
|
+
inFlightRejections: number;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Command Replay Store - manages idempotency and duplicate detection.
|
|
115
|
+
*
|
|
116
|
+
* Extracted from PiSessionManager to isolate:
|
|
117
|
+
* - Replay semantics (idempotency keys, command IDs)
|
|
118
|
+
* - Fingerprint conflict detection
|
|
119
|
+
* - Bounded outcome history
|
|
120
|
+
*/
|
|
121
|
+
export declare class CommandReplayStore {
|
|
122
|
+
/** In-flight commands by command id (for dependency waits and duplicate-id replay). */
|
|
123
|
+
private commandInFlightById;
|
|
124
|
+
/** Insertion order for in-flight commands (for bounded eviction). */
|
|
125
|
+
private inFlightOrder;
|
|
126
|
+
/** Completed command outcomes (for dependency checks and duplicate-id replay). */
|
|
127
|
+
private commandOutcomes;
|
|
128
|
+
/** Bounded insertion order to trim commandOutcomes memory. */
|
|
129
|
+
private commandOutcomeOrder;
|
|
130
|
+
/** Idempotency replay cache. */
|
|
131
|
+
private idempotencyCache;
|
|
132
|
+
/** Sequence for synthetic command IDs when client omits id. */
|
|
133
|
+
private syntheticCommandSequence;
|
|
134
|
+
/** Process start time for unique synthetic IDs that don't collide after clear(). */
|
|
135
|
+
private readonly processStartTime;
|
|
136
|
+
private readonly idempotencyTtlMs;
|
|
137
|
+
private readonly maxCommandOutcomes;
|
|
138
|
+
private readonly maxInFlightCommands;
|
|
139
|
+
private inFlightRejections;
|
|
140
|
+
constructor(options?: ReplayStoreOptions);
|
|
141
|
+
/**
|
|
142
|
+
* Get statistics about store state for monitoring.
|
|
143
|
+
*/
|
|
144
|
+
getStats(): ReplayStoreStats;
|
|
145
|
+
/**
|
|
146
|
+
* Get or create a deterministic command ID.
|
|
147
|
+
* Returns explicit ID if provided, otherwise generates a synthetic one.
|
|
148
|
+
*
|
|
149
|
+
* Synthetic IDs use process-start-time + sequence to guarantee uniqueness:
|
|
150
|
+
* 1. Process start time distinguishes IDs across server restarts
|
|
151
|
+
* 2. Sequence guarantees uniqueness within a process lifetime
|
|
152
|
+
* 3. Clear() resets sequence but start time stays same (collision-safe within run)
|
|
153
|
+
*
|
|
154
|
+
* NOTE: This method has a side effect (increments sequence) when generating.
|
|
155
|
+
*/
|
|
156
|
+
getOrCreateCommandId(command: RpcCommand): string;
|
|
157
|
+
/**
|
|
158
|
+
* Compute a fingerprint for conflict detection.
|
|
159
|
+
* Excludes retry identity fields (id, idempotencyKey) since those
|
|
160
|
+
* don't affect semantic equivalence - only determine replay mechanics.
|
|
161
|
+
*/
|
|
162
|
+
getCommandFingerprint(command: RpcCommand): string;
|
|
163
|
+
/**
|
|
164
|
+
* Build a cache key for idempotency lookup.
|
|
165
|
+
*/
|
|
166
|
+
private buildIdempotencyCacheKey;
|
|
167
|
+
/**
|
|
168
|
+
* Remove expired entries from the idempotency cache.
|
|
169
|
+
*/
|
|
170
|
+
cleanupIdempotencyCache(now?: number): void;
|
|
171
|
+
/**
|
|
172
|
+
* Store a response in the idempotency cache.
|
|
173
|
+
*/
|
|
174
|
+
cacheIdempotencyResult(input: IdempotencyCacheInput): void;
|
|
175
|
+
/**
|
|
176
|
+
* Trim old command outcomes to bound memory.
|
|
177
|
+
*/
|
|
178
|
+
private trimCommandOutcomes;
|
|
179
|
+
/**
|
|
180
|
+
* Store a completed command outcome.
|
|
181
|
+
*/
|
|
182
|
+
storeCommandOutcome(outcome: CommandOutcomeRecord): void;
|
|
183
|
+
/**
|
|
184
|
+
* Get a completed command outcome by ID.
|
|
185
|
+
*/
|
|
186
|
+
getCommandOutcome(commandId: string): CommandOutcomeRecord | undefined;
|
|
187
|
+
/**
|
|
188
|
+
* Check whether an in-flight command can be tracked for this command ID.
|
|
189
|
+
*
|
|
190
|
+
* Returns true if:
|
|
191
|
+
* - the command ID is already tracked (replacement/update path), OR
|
|
192
|
+
* - there is free capacity for a new tracked command.
|
|
193
|
+
*/
|
|
194
|
+
canRegisterInFlight(commandId: string): boolean;
|
|
195
|
+
/**
|
|
196
|
+
* Register an in-flight command.
|
|
197
|
+
*
|
|
198
|
+
* ADR-0001: Rejects when limit exceeded instead of evicting.
|
|
199
|
+
* Eviction breaks dependency chains - if command A depends on command B,
|
|
200
|
+
* and B is evicted, A fails with "unknown dependency". Rejection preserves
|
|
201
|
+
* correctness at the cost of temporary unavailability under load.
|
|
202
|
+
*
|
|
203
|
+
* @returns true if registered, false if limit exceeded
|
|
204
|
+
*/
|
|
205
|
+
registerInFlight(commandId: string, record: InFlightCommandRecord): boolean;
|
|
206
|
+
/**
|
|
207
|
+
* Unregister an in-flight command.
|
|
208
|
+
* Only removes if the record matches (prevents race conditions).
|
|
209
|
+
*/
|
|
210
|
+
unregisterInFlight(commandId: string, record: InFlightCommandRecord): void;
|
|
211
|
+
/**
|
|
212
|
+
* Get an in-flight command by ID.
|
|
213
|
+
*/
|
|
214
|
+
getInFlight(commandId: string): InFlightCommandRecord | undefined;
|
|
215
|
+
/**
|
|
216
|
+
* Create a conflict response for fingerprint mismatch.
|
|
217
|
+
*/
|
|
218
|
+
private createConflictResponse;
|
|
219
|
+
/**
|
|
220
|
+
* Clone a cached response for a new request.
|
|
221
|
+
* Preserves or strips ID based on whether the new request has one.
|
|
222
|
+
*/
|
|
223
|
+
private cloneResponseForRequest;
|
|
224
|
+
/**
|
|
225
|
+
* Check for replay opportunities or conflicts.
|
|
226
|
+
*
|
|
227
|
+
* Call this BEFORE executing a command. Returns:
|
|
228
|
+
* - "proceed": No replay possible, execute normally
|
|
229
|
+
* - "conflict": Fingerprint mismatch, return error response
|
|
230
|
+
* - "replay_cached": Found cached response, return it (with replayed: true)
|
|
231
|
+
* - "replay_inflight": Found in-flight command, await its promise
|
|
232
|
+
*/
|
|
233
|
+
checkReplay(command: RpcCommand, fingerprint: string): ReplayCheckResult;
|
|
234
|
+
/**
|
|
235
|
+
* Clear all state (used during disposal).
|
|
236
|
+
* Note: Does NOT reset syntheticCommandSequence to prevent ID collisions.
|
|
237
|
+
* Process start time ensures uniqueness even after clear.
|
|
238
|
+
*/
|
|
239
|
+
clear(): void;
|
|
240
|
+
}
|
|
241
|
+
//# sourceMappingURL=command-replay-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"command-replay-store.d.ts","sourceRoot":"","sources":["../src/command-replay-store.ts"],"names":[],"mappings":"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"}
|