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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# pi-server
|
|
2
|
+
|
|
3
|
+
Session multiplexer for [pi-coding-agent](https://www.npmjs.com/package/@mariozechner/pi-coding-agent). Exposes N independent `AgentSession` instances through WebSocket and stdio transports.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/tryingET/pi-server/actions/workflows/ci.yml)
|
|
6
|
+
[](https://www.npmjs.com/package/pi-app-server)
|
|
7
|
+
|
|
8
|
+
> Note: This is a standalone pi server package, not an extension/skills/themes bundle.
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- **Dual transport**: WebSocket (port 3141) + stdio (JSON lines)
|
|
13
|
+
- **Session lifecycle**: Create, delete, list, switch sessions
|
|
14
|
+
- **Command execution**: Deterministic lane serialization per session
|
|
15
|
+
- **Idempotent replay**: Atomic outcome storage with free replay lookups
|
|
16
|
+
- **Optimistic concurrency**: Session versioning for conflict detection
|
|
17
|
+
- **Extension UI**: Full round-trip support for `select`, `confirm`, `input`, `editor`, `interview`
|
|
18
|
+
- **Resource governance**: Rate limiting, session limits, message size limits
|
|
19
|
+
- **Graceful shutdown**: Drain in-flight commands, notify clients
|
|
20
|
+
- **Protocol versioning**: `serverVersion` + `protocolVersion` for compatibility checks
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install pi-app-server
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
### WebSocket
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Start server
|
|
34
|
+
npx pi-server
|
|
35
|
+
|
|
36
|
+
# Connect with wscat
|
|
37
|
+
wscat -c ws://localhost:3141
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
// Create and use a session
|
|
42
|
+
ws> {"type":"create_session","sessionId":"my-session"}
|
|
43
|
+
ws> {"type":"switch_session","sessionId":"my-session"}
|
|
44
|
+
ws> {"id":"cmd-1","type":"prompt","sessionId":"my-session","message":"Hello!"}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### stdio
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
echo '{"type":"create_session","sessionId":"test"}
|
|
51
|
+
{"type":"switch_session","sessionId":"test"}
|
|
52
|
+
{"id":"cmd-1","type":"prompt","sessionId":"test","message":"Hello!"}' | npx pi-server
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Architecture
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
src/
|
|
59
|
+
├── server.ts # transports, connection lifecycle, routing glue
|
|
60
|
+
├── session-manager.ts # orchestration: coordinates stores, engines, sessions
|
|
61
|
+
├── command-router.ts # session command handlers, routing
|
|
62
|
+
├── command-classification.ts # pure command classification (timeout, mutation)
|
|
63
|
+
├── command-replay-store.ts # idempotency, duplicate detection, outcome history
|
|
64
|
+
├── session-version-store.ts # monotonic version counters per session
|
|
65
|
+
├── command-execution-engine.ts # lane serialization, dependency waits, timeouts
|
|
66
|
+
├── resource-governor.ts # limits, rate controls, health/metrics
|
|
67
|
+
├── extension-ui.ts # pending UI request tracking
|
|
68
|
+
├── server-ui-context.ts # ExtensionUIContext for remote clients
|
|
69
|
+
├── validation.ts # command validation
|
|
70
|
+
└── types.ts # wire protocol types + SessionResolver interface
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Core invariants
|
|
74
|
+
|
|
75
|
+
- For each admitted command, there is exactly one terminal response.
|
|
76
|
+
- For each session ID, there is at most one live `AgentSession`.
|
|
77
|
+
- Subscriber session sets are always a subset of active sessions.
|
|
78
|
+
- Session version is monotonic and mutation-sensitive.
|
|
79
|
+
- Fingerprint excludes retry identity (`id`, `idempotencyKey`) for semantic equivalence.
|
|
80
|
+
|
|
81
|
+
### Key abstractions
|
|
82
|
+
|
|
83
|
+
- **`SessionResolver`** — Interface for session access (enables test doubles, future clustering)
|
|
84
|
+
- **`CommandReplayStore`** — Idempotency and duplicate detection
|
|
85
|
+
- **`SessionVersionStore`** — Optimistic concurrency via version counters
|
|
86
|
+
- **`CommandExecutionEngine`** — Deterministic lane serialization and timeout management
|
|
87
|
+
|
|
88
|
+
## Protocol
|
|
89
|
+
|
|
90
|
+
See [PROTOCOL.md](./PROTOCOL.md) for the normative wire contract.
|
|
91
|
+
|
|
92
|
+
### Command → Response
|
|
93
|
+
|
|
94
|
+
Every command receives exactly one response:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{"id": "cmd-1", "type": "prompt", "sessionId": "s1", "message": "hello"}
|
|
98
|
+
{"id": "cmd-1", "type": "response", "command": "prompt", "success": true}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Event Broadcast
|
|
102
|
+
|
|
103
|
+
Events flow session → subscribers:
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{"type": "event", "sessionId": "s1", "event": {"type": "agent_start", ...}}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Extension UI Round-Trip
|
|
110
|
+
|
|
111
|
+
1. Extension calls `ui.select()` → server creates pending request
|
|
112
|
+
2. Server broadcasts `extension_ui_request` event with `requestId`
|
|
113
|
+
3. Client sends `extension_ui_response` command with same `requestId`
|
|
114
|
+
4. Server resolves pending promise → extension continues
|
|
115
|
+
|
|
116
|
+
### Idempotency & Replay
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
// First request with idempotency key
|
|
120
|
+
{"id": "cmd-1", "type": "list_sessions", "idempotencyKey": "key-1"}
|
|
121
|
+
{"id": "cmd-1", "type": "response", "command": "list_sessions", "success": true, ...}
|
|
122
|
+
|
|
123
|
+
// Retry with same key → replayed (free, no rate limit charge)
|
|
124
|
+
{"id": "cmd-2", "type": "list_sessions", "idempotencyKey": "key-1"}
|
|
125
|
+
{"id": "cmd-2", "type": "response", "command": "list_sessions", "success": true, "replayed": true, ...}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Timeout semantics (ADR-0001)
|
|
129
|
+
|
|
130
|
+
- Timeout is a **terminal stored outcome** (`timedOut: true`), not an indeterminate placeholder.
|
|
131
|
+
- Replay of the same command identity returns the **same timeout response**.
|
|
132
|
+
- Late underlying completion does **not** overwrite the stored timeout outcome.
|
|
133
|
+
|
|
134
|
+
## Development
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
# Install dependencies
|
|
138
|
+
npm install
|
|
139
|
+
|
|
140
|
+
# Build
|
|
141
|
+
npm run build
|
|
142
|
+
|
|
143
|
+
# Run tests
|
|
144
|
+
npm test # Unit tests (83)
|
|
145
|
+
npm run test:integration # Integration tests (26)
|
|
146
|
+
npm run test:fuzz # Fuzz tests (17)
|
|
147
|
+
|
|
148
|
+
# Module tests (141)
|
|
149
|
+
node --experimental-vm-modules dist/test-command-classification.js
|
|
150
|
+
node --experimental-vm-modules dist/test-session-version-store.js
|
|
151
|
+
node --experimental-vm-modules dist/test-command-replay-store.js
|
|
152
|
+
node --experimental-vm-modules dist/test-command-execution-engine.js
|
|
153
|
+
|
|
154
|
+
# Type check + lint
|
|
155
|
+
npm run check
|
|
156
|
+
|
|
157
|
+
# Full CI
|
|
158
|
+
npm run ci
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Release Process
|
|
162
|
+
|
|
163
|
+
This project uses [release-please](https://github.com/googleapis/release-please) for automated versioning.
|
|
164
|
+
|
|
165
|
+
### Automated Flow
|
|
166
|
+
|
|
167
|
+
1. Push to `main` → release-please creates/updates a release PR
|
|
168
|
+
2. Merge the release PR → Creates GitHub release + git tag
|
|
169
|
+
3. Release published → GitHub Action publishes to npm with provenance
|
|
170
|
+
|
|
171
|
+
### Manual Release Check
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
npm run release:check
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
This validates:
|
|
178
|
+
- `package.json` has required fields
|
|
179
|
+
- `dist/` exists with compiled files
|
|
180
|
+
- Entry point has correct shebang
|
|
181
|
+
- `npm pack` produces expected files
|
|
182
|
+
- Full CI passes
|
|
183
|
+
|
|
184
|
+
## Documentation
|
|
185
|
+
|
|
186
|
+
| Document | Purpose |
|
|
187
|
+
|----------|---------|
|
|
188
|
+
| [AGENTS.md](./AGENTS.md) | Crystallized learnings, patterns, anti-patterns |
|
|
189
|
+
| [PROTOCOL.md](./PROTOCOL.md) | Normative wire contract |
|
|
190
|
+
| [ADR-0001](./docs/adr/0001-atomic-outcome-storage.md) | Atomic outcome storage decision |
|
|
191
|
+
| [ROADMAP.md](./ROADMAP.md) | Phase tracking and milestones |
|
|
192
|
+
|
|
193
|
+
## License
|
|
194
|
+
|
|
195
|
+
MIT
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command Classification - unified source of truth for command behavior.
|
|
3
|
+
*
|
|
4
|
+
* This module consolidates all command classification logic that was
|
|
5
|
+
* previously scattered across multiple modules. Single source of truth
|
|
6
|
+
* prevents drift and makes adding new commands easier.
|
|
7
|
+
*
|
|
8
|
+
* Classification dimensions:
|
|
9
|
+
* - Timeout policy: short (30s), long (5min), or none (uncancellable)
|
|
10
|
+
* - Mutation: does the command change session state?
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Get the timeout policy for a command type.
|
|
14
|
+
* @returns Timeout in ms, or null for uncancellable commands
|
|
15
|
+
*/
|
|
16
|
+
export declare function getCommandTimeoutPolicy(commandType: string, options?: {
|
|
17
|
+
defaultTimeoutMs?: number;
|
|
18
|
+
shortTimeoutMs?: number;
|
|
19
|
+
}): number | null;
|
|
20
|
+
/**
|
|
21
|
+
* Check if a command has a short timeout.
|
|
22
|
+
*/
|
|
23
|
+
export declare function isShortTimeoutCommand(commandType: string): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Check if a command cannot be timed out.
|
|
26
|
+
*/
|
|
27
|
+
export declare function isNoTimeoutCommand(commandType: string): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Check if a command type mutates session state.
|
|
30
|
+
* Mutating commands advance the session version.
|
|
31
|
+
*/
|
|
32
|
+
export declare function isMutationCommand(commandType: string): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Check if a command is read-only.
|
|
35
|
+
*/
|
|
36
|
+
export declare function isReadOnlyCommand(commandType: string): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Full classification of a command type.
|
|
39
|
+
*/
|
|
40
|
+
export interface CommandClassification {
|
|
41
|
+
/** Timeout in milliseconds, or null for uncancellable */
|
|
42
|
+
timeoutMs: number | null;
|
|
43
|
+
/** Whether this is a short timeout command */
|
|
44
|
+
isShortTimeout: boolean;
|
|
45
|
+
/** Whether this command can be timed out */
|
|
46
|
+
isCancellable: boolean;
|
|
47
|
+
/** Whether this command mutates session state */
|
|
48
|
+
isMutation: boolean;
|
|
49
|
+
/** Whether this command is read-only */
|
|
50
|
+
isReadOnly: boolean;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Get full classification for a command type.
|
|
54
|
+
*/
|
|
55
|
+
export declare function classifyCommand(commandType: string, options?: {
|
|
56
|
+
defaultTimeoutMs?: number;
|
|
57
|
+
shortTimeoutMs?: number;
|
|
58
|
+
}): CommandClassification;
|
|
59
|
+
//# sourceMappingURL=command-classification.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"command-classification.d.ts","sourceRoot":"","sources":["../src/command-classification.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAiCH;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE;IACR,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,GACA,MAAM,GAAG,IAAI,CASf;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAElE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAE/D;AAiCD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAI9D;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAE9D;AAMD;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,yDAAyD;IACzD,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,8CAA8C;IAC9C,cAAc,EAAE,OAAO,CAAC;IACxB,4CAA4C;IAC5C,aAAa,EAAE,OAAO,CAAC;IACvB,iDAAiD;IACjD,UAAU,EAAE,OAAO,CAAC;IACpB,wCAAwC;IACxC,UAAU,EAAE,OAAO,CAAC;CACrB;AAED;;GAEG;AACH,wBAAgB,eAAe,CAC7B,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE;IACR,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,GACA,qBAAqB,CASvB"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const SHORT_TIMEOUT_COMMANDS = /* @__PURE__ */ new Set([
|
|
2
|
+
"get_state",
|
|
3
|
+
"get_messages",
|
|
4
|
+
"get_available_models",
|
|
5
|
+
"get_commands",
|
|
6
|
+
"get_skills",
|
|
7
|
+
"get_tools",
|
|
8
|
+
"list_session_files",
|
|
9
|
+
"get_session_stats",
|
|
10
|
+
"get_fork_messages",
|
|
11
|
+
"get_last_assistant_text",
|
|
12
|
+
"get_context_usage",
|
|
13
|
+
"set_session_name"
|
|
14
|
+
]);
|
|
15
|
+
const NO_TIMEOUT_COMMANDS = /* @__PURE__ */ new Set([
|
|
16
|
+
"create_session"
|
|
17
|
+
// Session creation is atomic
|
|
18
|
+
]);
|
|
19
|
+
function getCommandTimeoutPolicy(commandType, options) {
|
|
20
|
+
if (NO_TIMEOUT_COMMANDS.has(commandType)) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const defaultTimeout = options?.defaultTimeoutMs ?? 5 * 60 * 1e3;
|
|
24
|
+
const shortTimeout = options?.shortTimeoutMs ?? 30 * 1e3;
|
|
25
|
+
return SHORT_TIMEOUT_COMMANDS.has(commandType) ? shortTimeout : defaultTimeout;
|
|
26
|
+
}
|
|
27
|
+
function isShortTimeoutCommand(commandType) {
|
|
28
|
+
return SHORT_TIMEOUT_COMMANDS.has(commandType);
|
|
29
|
+
}
|
|
30
|
+
function isNoTimeoutCommand(commandType) {
|
|
31
|
+
return NO_TIMEOUT_COMMANDS.has(commandType);
|
|
32
|
+
}
|
|
33
|
+
const READ_ONLY_COMMANDS = /* @__PURE__ */ new Set([
|
|
34
|
+
"get_state",
|
|
35
|
+
"get_messages",
|
|
36
|
+
"get_available_models",
|
|
37
|
+
"get_commands",
|
|
38
|
+
"get_skills",
|
|
39
|
+
"get_tools",
|
|
40
|
+
"list_session_files",
|
|
41
|
+
"get_session_stats",
|
|
42
|
+
"get_fork_messages",
|
|
43
|
+
"get_last_assistant_text",
|
|
44
|
+
"get_context_usage",
|
|
45
|
+
"switch_session"
|
|
46
|
+
// Switches client focus, doesn't change session
|
|
47
|
+
]);
|
|
48
|
+
const SPECIAL_SESSION_COMMANDS = /* @__PURE__ */ new Set([
|
|
49
|
+
"extension_ui_response"
|
|
50
|
+
// Handled by ExtensionUIManager, not session
|
|
51
|
+
]);
|
|
52
|
+
function isMutationCommand(commandType) {
|
|
53
|
+
if (READ_ONLY_COMMANDS.has(commandType)) return false;
|
|
54
|
+
if (SPECIAL_SESSION_COMMANDS.has(commandType)) return false;
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
function isReadOnlyCommand(commandType) {
|
|
58
|
+
return READ_ONLY_COMMANDS.has(commandType);
|
|
59
|
+
}
|
|
60
|
+
function classifyCommand(commandType, options) {
|
|
61
|
+
const timeoutMs = getCommandTimeoutPolicy(commandType, options);
|
|
62
|
+
return {
|
|
63
|
+
timeoutMs,
|
|
64
|
+
isShortTimeout: isShortTimeoutCommand(commandType),
|
|
65
|
+
isCancellable: timeoutMs !== null,
|
|
66
|
+
isMutation: isMutationCommand(commandType),
|
|
67
|
+
isReadOnly: isReadOnlyCommand(commandType)
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export {
|
|
71
|
+
classifyCommand,
|
|
72
|
+
getCommandTimeoutPolicy,
|
|
73
|
+
isMutationCommand,
|
|
74
|
+
isNoTimeoutCommand,
|
|
75
|
+
isReadOnlyCommand,
|
|
76
|
+
isShortTimeoutCommand
|
|
77
|
+
};
|
|
78
|
+
//# sourceMappingURL=command-classification.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/command-classification.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Command Classification - unified source of truth for command behavior.\n *\n * This module consolidates all command classification logic that was\n * previously scattered across multiple modules. Single source of truth\n * prevents drift and makes adding new commands easier.\n *\n * Classification dimensions:\n * - Timeout policy: short (30s), long (5min), or none (uncancellable)\n * - Mutation: does the command change session state?\n */\n\n// =============================================================================\n// TIMEOUT CLASSIFICATION\n// =============================================================================\n\n/**\n * Commands that should have shorter timeout (30 seconds).\n * These are fast operations that don't involve LLM calls.\n */\nconst SHORT_TIMEOUT_COMMANDS = new Set([\n \"get_state\",\n \"get_messages\",\n \"get_available_models\",\n \"get_commands\",\n \"get_skills\",\n \"get_tools\",\n \"list_session_files\",\n \"get_session_stats\",\n \"get_fork_messages\",\n \"get_last_assistant_text\",\n \"get_context_usage\",\n \"set_session_name\",\n]);\n\n/**\n * Commands that should not use command timeout (cannot be safely cancelled).\n * These have side effects that must complete once started.\n */\nconst NO_TIMEOUT_COMMANDS = new Set([\n \"create_session\", // Session creation is atomic\n]);\n\n/**\n * Get the timeout policy for a command type.\n * @returns Timeout in ms, or null for uncancellable commands\n */\nexport function getCommandTimeoutPolicy(\n commandType: string,\n options?: {\n defaultTimeoutMs?: number;\n shortTimeoutMs?: number;\n }\n): number | null {\n if (NO_TIMEOUT_COMMANDS.has(commandType)) {\n return null;\n }\n\n const defaultTimeout = options?.defaultTimeoutMs ?? 5 * 60 * 1000;\n const shortTimeout = options?.shortTimeoutMs ?? 30 * 1000;\n\n return SHORT_TIMEOUT_COMMANDS.has(commandType) ? shortTimeout : defaultTimeout;\n}\n\n/**\n * Check if a command has a short timeout.\n */\nexport function isShortTimeoutCommand(commandType: string): boolean {\n return SHORT_TIMEOUT_COMMANDS.has(commandType);\n}\n\n/**\n * Check if a command cannot be timed out.\n */\nexport function isNoTimeoutCommand(commandType: string): boolean {\n return NO_TIMEOUT_COMMANDS.has(commandType);\n}\n\n// =============================================================================\n// MUTATION CLASSIFICATION\n// =============================================================================\n\n/**\n * Commands that don't mutate session state (read-only).\n * These don't advance the session version on success.\n */\nconst READ_ONLY_COMMANDS = new Set([\n \"get_state\",\n \"get_messages\",\n \"get_available_models\",\n \"get_commands\",\n \"get_skills\",\n \"get_tools\",\n \"list_session_files\",\n \"get_session_stats\",\n \"get_fork_messages\",\n \"get_last_assistant_text\",\n \"get_context_usage\",\n \"switch_session\", // Switches client focus, doesn't change session\n]);\n\n/**\n * Commands that appear to target a session but are handled specially.\n * These don't count as session mutations for version purposes.\n */\nconst SPECIAL_SESSION_COMMANDS = new Set([\n \"extension_ui_response\", // Handled by ExtensionUIManager, not session\n]);\n\n/**\n * Check if a command type mutates session state.\n * Mutating commands advance the session version.\n */\nexport function isMutationCommand(commandType: string): boolean {\n if (READ_ONLY_COMMANDS.has(commandType)) return false;\n if (SPECIAL_SESSION_COMMANDS.has(commandType)) return false;\n return true;\n}\n\n/**\n * Check if a command is read-only.\n */\nexport function isReadOnlyCommand(commandType: string): boolean {\n return READ_ONLY_COMMANDS.has(commandType);\n}\n\n// =============================================================================\n// COMBINED QUERIES\n// =============================================================================\n\n/**\n * Full classification of a command type.\n */\nexport interface CommandClassification {\n /** Timeout in milliseconds, or null for uncancellable */\n timeoutMs: number | null;\n /** Whether this is a short timeout command */\n isShortTimeout: boolean;\n /** Whether this command can be timed out */\n isCancellable: boolean;\n /** Whether this command mutates session state */\n isMutation: boolean;\n /** Whether this command is read-only */\n isReadOnly: boolean;\n}\n\n/**\n * Get full classification for a command type.\n */\nexport function classifyCommand(\n commandType: string,\n options?: {\n defaultTimeoutMs?: number;\n shortTimeoutMs?: number;\n }\n): CommandClassification {\n const timeoutMs = getCommandTimeoutPolicy(commandType, options);\n return {\n timeoutMs,\n isShortTimeout: isShortTimeoutCommand(commandType),\n isCancellable: timeoutMs !== null,\n isMutation: isMutationCommand(commandType),\n isReadOnly: isReadOnlyCommand(commandType),\n };\n}\n"],
|
|
5
|
+
"mappings": "AAoBA,MAAM,yBAAyB,oBAAI,IAAI;AAAA,EACrC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAMD,MAAM,sBAAsB,oBAAI,IAAI;AAAA,EAClC;AAAA;AACF,CAAC;AAMM,SAAS,wBACd,aACA,SAIe;AACf,MAAI,oBAAoB,IAAI,WAAW,GAAG;AACxC,WAAO;AAAA,EACT;AAEA,QAAM,iBAAiB,SAAS,oBAAoB,IAAI,KAAK;AAC7D,QAAM,eAAe,SAAS,kBAAkB,KAAK;AAErD,SAAO,uBAAuB,IAAI,WAAW,IAAI,eAAe;AAClE;AAKO,SAAS,sBAAsB,aAA8B;AAClE,SAAO,uBAAuB,IAAI,WAAW;AAC/C;AAKO,SAAS,mBAAmB,aAA8B;AAC/D,SAAO,oBAAoB,IAAI,WAAW;AAC5C;AAUA,MAAM,qBAAqB,oBAAI,IAAI;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AACF,CAAC;AAMD,MAAM,2BAA2B,oBAAI,IAAI;AAAA,EACvC;AAAA;AACF,CAAC;AAMM,SAAS,kBAAkB,aAA8B;AAC9D,MAAI,mBAAmB,IAAI,WAAW,EAAG,QAAO;AAChD,MAAI,yBAAyB,IAAI,WAAW,EAAG,QAAO;AACtD,SAAO;AACT;AAKO,SAAS,kBAAkB,aAA8B;AAC9D,SAAO,mBAAmB,IAAI,WAAW;AAC3C;AAyBO,SAAS,gBACd,aACA,SAIuB;AACvB,QAAM,YAAY,wBAAwB,aAAa,OAAO;AAC9D,SAAO;AAAA,IACL;AAAA,IACA,gBAAgB,sBAAsB,WAAW;AAAA,IACjD,eAAe,cAAc;AAAA,IAC7B,YAAY,kBAAkB,WAAW;AAAA,IACzC,YAAY,kBAAkB,WAAW;AAAA,EAC3C;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command Execution Engine - manages lane serialization, dependency waits, and timeouts.
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities:
|
|
5
|
+
* - Deterministic per-lane command serialization
|
|
6
|
+
* - Dependency resolution with timeout
|
|
7
|
+
* - Command timeout orchestration with abort hooks
|
|
8
|
+
* - Lifecycle event emission
|
|
9
|
+
*/
|
|
10
|
+
import type { AgentSession } from "@mariozechner/pi-coding-agent";
|
|
11
|
+
import type { RpcCommand, RpcResponse, SessionResolver } from "./types.js";
|
|
12
|
+
import type { CommandReplayStore } from "./command-replay-store.js";
|
|
13
|
+
import type { SessionVersionStore } from "./session-version-store.js";
|
|
14
|
+
/**
|
|
15
|
+
* Abort handler for a specific command type.
|
|
16
|
+
* Called when a command times out to attempt cancellation.
|
|
17
|
+
*/
|
|
18
|
+
export type AbortHandler = (session: AgentSession) => void | Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Wrap a promise with a timeout.
|
|
21
|
+
* Returns the promise result or throws on timeout.
|
|
22
|
+
*/
|
|
23
|
+
export declare function withTimeout<T>(promise: Promise<T>, timeoutMs: number, commandType: string, onTimeout?: () => void | Promise<void>): Promise<T>;
|
|
24
|
+
/**
|
|
25
|
+
* Configuration options for the execution engine.
|
|
26
|
+
*/
|
|
27
|
+
export interface ExecutionEngineOptions {
|
|
28
|
+
defaultCommandTimeoutMs?: number;
|
|
29
|
+
shortCommandTimeoutMs?: number;
|
|
30
|
+
dependencyWaitTimeoutMs?: number;
|
|
31
|
+
/** Custom abort handlers for command types (extends defaults) */
|
|
32
|
+
abortHandlers?: Partial<Record<string, AbortHandler>>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Command Execution Engine - manages lane serialization and dependency waits.
|
|
36
|
+
*
|
|
37
|
+
* Extracted from PiSessionManager to isolate:
|
|
38
|
+
* - Per-lane command serialization
|
|
39
|
+
* - Dependency resolution with timeout
|
|
40
|
+
* - Command timeout with abort hooks
|
|
41
|
+
*/
|
|
42
|
+
export declare class CommandExecutionEngine {
|
|
43
|
+
/** Deterministic per-lane command serialization tails. */
|
|
44
|
+
private laneTails;
|
|
45
|
+
private readonly replayStore;
|
|
46
|
+
private readonly versionStore;
|
|
47
|
+
private readonly sessionResolver;
|
|
48
|
+
private readonly abortHandlers;
|
|
49
|
+
private readonly defaultCommandTimeoutMs;
|
|
50
|
+
private readonly shortCommandTimeoutMs;
|
|
51
|
+
private readonly dependencyWaitTimeoutMs;
|
|
52
|
+
constructor(replayStore: CommandReplayStore, versionStore: SessionVersionStore, sessionResolver: SessionResolver, options?: ExecutionEngineOptions);
|
|
53
|
+
/**
|
|
54
|
+
* Get statistics about the execution engine state.
|
|
55
|
+
*/
|
|
56
|
+
getStats(): {
|
|
57
|
+
laneCount: number;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Get the lane key for a command.
|
|
61
|
+
* Session commands serialize per-session; server commands serialize together.
|
|
62
|
+
*/
|
|
63
|
+
getLaneKey(command: RpcCommand): string;
|
|
64
|
+
/**
|
|
65
|
+
* Run a task in a deterministic serialized lane.
|
|
66
|
+
* Commands in the same lane execute sequentially.
|
|
67
|
+
*/
|
|
68
|
+
runOnLane<T>(laneKey: string, task: () => Promise<T>): Promise<T>;
|
|
69
|
+
/**
|
|
70
|
+
* Wait for dependency commands to complete.
|
|
71
|
+
* Returns error if any dependency fails or times out.
|
|
72
|
+
*
|
|
73
|
+
* Note: Cross-lane dependency cycles (A→B, B→A) are detected by timeout
|
|
74
|
+
* rather than explicit cycle detection. This is acceptable because:
|
|
75
|
+
* 1. Cross-lane dependencies are rare
|
|
76
|
+
* 2. The dependencyWaitTimeoutMs (default 30s) prevents indefinite deadlock
|
|
77
|
+
* 3. Same-lane cycles are explicitly detected below
|
|
78
|
+
*/
|
|
79
|
+
awaitDependencies(dependsOn: string[], laneKey: string): Promise<{
|
|
80
|
+
ok: true;
|
|
81
|
+
} | {
|
|
82
|
+
ok: false;
|
|
83
|
+
error: string;
|
|
84
|
+
}>;
|
|
85
|
+
/**
|
|
86
|
+
* Resolve timeout policy for a command.
|
|
87
|
+
* Returns null for commands that cannot be safely cancelled.
|
|
88
|
+
*/
|
|
89
|
+
getCommandTimeoutMs(commandType: string): number | null;
|
|
90
|
+
/**
|
|
91
|
+
* Best-effort cancellation for timed-out commands.
|
|
92
|
+
* Uses configured abort handlers (defaults + custom overrides).
|
|
93
|
+
*/
|
|
94
|
+
abortTimedOutCommand(command: RpcCommand): Promise<void>;
|
|
95
|
+
/**
|
|
96
|
+
* Execute a command with timeout.
|
|
97
|
+
*/
|
|
98
|
+
executeWithTimeout(commandType: string, promise: Promise<RpcResponse>, command: RpcCommand): Promise<RpcResponse>;
|
|
99
|
+
/**
|
|
100
|
+
* Check if a session version matches the expected version.
|
|
101
|
+
* Returns error object if mismatch, undefined if OK.
|
|
102
|
+
*
|
|
103
|
+
* @param sessionId - The session to check
|
|
104
|
+
* @param ifSessionVersion - The expected version
|
|
105
|
+
* @param commandType - The command type for error context (preserves caller's context)
|
|
106
|
+
*/
|
|
107
|
+
checkSessionVersion(sessionId: string, ifSessionVersion: number, commandType: string): {
|
|
108
|
+
type: "response";
|
|
109
|
+
command: string;
|
|
110
|
+
success: false;
|
|
111
|
+
error: string;
|
|
112
|
+
} | undefined;
|
|
113
|
+
/**
|
|
114
|
+
* Clear all lane state (used during disposal).
|
|
115
|
+
*/
|
|
116
|
+
clear(): void;
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=command-execution-engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"command-execution-engine.d.ts","sourceRoot":"","sources":["../src/command-execution-engine.ts"],"names":[],"mappings":"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"}
|