@evandu/super-pi 0.1.32 → 0.1.34
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/README.md +4 -0
- package/agents/super-pi-task/index.ts +4 -0
- package/dist/client/assets/index-BhsIL1Qa.js +50 -0
- package/dist/client/assets/index-Da9PkxxS.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/extension.js +7 -2
- package/dist/server/active-memory.d.ts +15 -0
- package/dist/server/active-memory.js +36 -1
- package/dist/server/application/conversation-service.d.ts +39 -0
- package/dist/server/application/conversation-service.js +65 -4
- package/dist/server/application/extension-ui-service.js +16 -0
- package/dist/server/application/run-service.d.ts +21 -0
- package/dist/server/application/run-service.js +130 -19
- package/dist/server/command-handler.d.ts +14 -0
- package/dist/server/command-handler.js +22 -0
- package/dist/server/git.d.ts +37 -0
- package/dist/server/git.js +30 -0
- package/dist/server/http-server.js +126 -8
- package/dist/server/index.js +7 -2
- package/dist/server/memory.d.ts +27 -0
- package/dist/server/memory.js +56 -0
- package/dist/server/persistence/json-state-repository.d.ts +2 -0
- package/dist/server/persistence/json-state-repository.js +24 -5
- package/dist/server/runtime.d.ts +4 -1
- package/dist/server/runtime.js +51 -14
- package/dist/server/store.d.ts +6 -0
- package/dist/server/store.js +46 -3
- package/dist/server/terminal.js +20 -7
- package/dist/server/types.d.ts +36 -2
- package/dist/server/upload.js +8 -19
- package/dist/server/ws-server.js +89 -28
- package/package.json +5 -3
- package/dist/client/assets/index-BBfRhf7h.js +0 -50
- package/dist/client/assets/index-sZQBUBuS.css +0 -1
package/dist/server/upload.js
CHANGED
|
@@ -109,8 +109,8 @@ export async function findUploadFile(uploadsDir, conversationId, filenameOrId) {
|
|
|
109
109
|
return null;
|
|
110
110
|
const directPath = join(uploadsDir, conversationId, filenameOrId);
|
|
111
111
|
if (existsSync(directPath)) {
|
|
112
|
-
const s = await stat(directPath);
|
|
113
|
-
if (s
|
|
112
|
+
const s = await stat(directPath).catch(() => null);
|
|
113
|
+
if (s?.isFile()) {
|
|
114
114
|
const mime = mimeForExt(extname(directPath));
|
|
115
115
|
return { filePath: directPath, mimeType: mime, size: s.size };
|
|
116
116
|
}
|
|
@@ -121,29 +121,18 @@ export async function findUploadFile(uploadsDir, conversationId, filenameOrId) {
|
|
|
121
121
|
for (const entry of entries) {
|
|
122
122
|
if (!entry.endsWith(".meta.json") && (entry === filenameOrId || entry.startsWith(`${filenameOrId}.`))) {
|
|
123
123
|
const fp = join(dir, entry);
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
// A file listed by readdir can be gone by the time stat runs; that is a miss, not a failure.
|
|
125
|
+
const s = await stat(fp).catch(() => null);
|
|
126
|
+
if (s?.isFile()) {
|
|
126
127
|
return { filePath: fp, mimeType: mimeForExt(extname(fp)), size: s.size };
|
|
127
128
|
}
|
|
128
129
|
}
|
|
129
130
|
}
|
|
130
131
|
}
|
|
131
132
|
}
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const dir = join(uploadsDir, conv);
|
|
136
|
-
const entries = await readdir(dir).catch(() => []);
|
|
137
|
-
for (const entry of entries) {
|
|
138
|
-
if (!entry.endsWith(".meta.json") && (entry === filenameOrId || entry.startsWith(`${filenameOrId}.`))) {
|
|
139
|
-
const fp = join(dir, entry);
|
|
140
|
-
const s = await stat(fp);
|
|
141
|
-
if (s.isFile()) {
|
|
142
|
-
return { filePath: fp, mimeType: mimeForExt(extname(fp)), size: s.size };
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
133
|
+
// Deliberately NO cross-conversation fallback here: an upload URL carries its own conversation id, and
|
|
134
|
+
// serving one conversation's request from another conversation's directory would leak images across
|
|
135
|
+
// conversations. A miss is the caller's 404, not a wider search.
|
|
147
136
|
return null;
|
|
148
137
|
}
|
|
149
138
|
export async function deleteConversationUploads(uploadsDir, conversationId) {
|
package/dist/server/ws-server.js
CHANGED
|
@@ -116,14 +116,28 @@ export class WsServer {
|
|
|
116
116
|
socket.send(JSON.stringify({ type: "subagent.catalog", catalog }));
|
|
117
117
|
}
|
|
118
118
|
}).catch((error) => console.warn(`Super Pi: sub-agent catalog for a new connection failed: ${error instanceof Error ? error.message : "unknown error"}`));
|
|
119
|
+
/**
|
|
120
|
+
* Liveness follows the standard ws pattern: every pong marks the peer alive, each heartbeat
|
|
121
|
+
* marks it unanswered and pings, and a peer that is STILL unanswered on the next heartbeat is
|
|
122
|
+
* terminated. Without this, a half-open connection (frozen tab, gone NAT entry, pulled cable)
|
|
123
|
+
* stays OPEN forever and every broadcast serializes full state to it.
|
|
124
|
+
*/
|
|
125
|
+
let isAlive = true;
|
|
126
|
+
socket.on("pong", () => { isAlive = true; });
|
|
119
127
|
const heartbeat = setInterval(() => {
|
|
120
|
-
if (socket.readyState
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
128
|
+
if (socket.readyState !== WebSocket.OPEN)
|
|
129
|
+
return;
|
|
130
|
+
if (!isAlive) {
|
|
131
|
+
// Fires 'close', which runs the per-socket cleanup below.
|
|
132
|
+
socket.terminate();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
isAlive = false;
|
|
136
|
+
socket.ping();
|
|
137
|
+
if (this.rootDir) {
|
|
138
|
+
const v = checkDiskVersion(this.rootDir);
|
|
139
|
+
if (v.needsRestart) {
|
|
140
|
+
socket.send(JSON.stringify({ type: "server.version", version: v }));
|
|
127
141
|
}
|
|
128
142
|
}
|
|
129
143
|
}, 30_000);
|
|
@@ -136,30 +150,77 @@ export class WsServer {
|
|
|
136
150
|
this.onSocketClose?.([...terminalSessionIds]);
|
|
137
151
|
terminalSessionIds.clear();
|
|
138
152
|
});
|
|
139
|
-
|
|
140
|
-
|
|
153
|
+
/**
|
|
154
|
+
* Commands from ONE socket are SERIALIZED through a promise chain. `handleCommand` is async, so
|
|
155
|
+
* a bare `async` listener lets command N+1 start while command N is still awaiting inside the
|
|
156
|
+
* services, interleaving that defeats their check-then-act guards. Chaining each command off the
|
|
157
|
+
* previous one makes same-socket execution strictly in-order while different sockets keep their
|
|
158
|
+
* own independent chains. Error handling is unchanged: each command catches its own errors and
|
|
159
|
+
* reports them on the socket, and the chain-level catch only stops an unexpected rejection from
|
|
160
|
+
* poisoning every later command on the same socket.
|
|
161
|
+
*/
|
|
162
|
+
let commandQueue = Promise.resolve();
|
|
163
|
+
socket.on("message", (raw) => {
|
|
164
|
+
let parsed;
|
|
141
165
|
try {
|
|
142
|
-
|
|
143
|
-
if (command.type === "terminal.open")
|
|
144
|
-
terminalSessionIds.add(command.sessionId);
|
|
145
|
-
const entityId = await this.commandHandler.handleCommand(command, (event) => {
|
|
146
|
-
if (socket.readyState === WebSocket.OPEN) {
|
|
147
|
-
socket.send(JSON.stringify(event));
|
|
148
|
-
}
|
|
149
|
-
}, remoteAddress);
|
|
150
|
-
if (command.operationRequestId)
|
|
151
|
-
socket.send(JSON.stringify({ type: "operation.result", requestId: command.operationRequestId, ok: true, entityId }));
|
|
166
|
+
parsed = JSON.parse(raw.toString());
|
|
152
167
|
}
|
|
153
|
-
catch
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
socket.send(JSON.stringify({
|
|
159
|
-
type: "error",
|
|
160
|
-
message: error instanceof Error ? error.message : "command failed",
|
|
161
|
-
}));
|
|
168
|
+
catch {
|
|
169
|
+
socket.send(JSON.stringify({ type: "error", message: "command failed" }));
|
|
170
|
+
return;
|
|
162
171
|
}
|
|
172
|
+
// A send represents a streaming operation. It must be admitted immediately so a second send
|
|
173
|
+
// can observe the active run and enter the durable follow-up queue; waiting for the first model
|
|
174
|
+
// turn here makes every follow-up test time out and defeats the queue semantics.
|
|
175
|
+
const runIndependently = parsed.type === "message.send" || parsed.type === "run.stop";
|
|
176
|
+
// ...WHICH MEANS A SEND CAN OVERTAKE A REVERT SENT JUST BEFORE IT, because the revert below is
|
|
177
|
+
// queued behind whatever this socket was already doing. The service only learns it is cutting
|
|
178
|
+
// when the command EXECUTES, so a send handled in between would find the conversation open and
|
|
179
|
+
// be admitted onto a transcript the cut has not reached yet. Claiming here closes that gap: this
|
|
180
|
+
// is the arrival edge, and the only one early enough to be of use to a command that does not wait.
|
|
181
|
+
if (parsed.type === "message.revert")
|
|
182
|
+
this.commandHandler.claimRevert(parsed.conversationId);
|
|
183
|
+
const execute = async () => {
|
|
184
|
+
let command;
|
|
185
|
+
try {
|
|
186
|
+
command = parsed;
|
|
187
|
+
if (command.type === "terminal.open")
|
|
188
|
+
terminalSessionIds.add(command.sessionId);
|
|
189
|
+
const entityId = await this.commandHandler.handleCommand(command, (event) => {
|
|
190
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
191
|
+
socket.send(JSON.stringify(event));
|
|
192
|
+
}
|
|
193
|
+
}, remoteAddress);
|
|
194
|
+
if (command.operationRequestId)
|
|
195
|
+
socket.send(JSON.stringify({ type: "operation.result", requestId: command.operationRequestId, ok: true, entityId }));
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
if (command?.operationRequestId) {
|
|
199
|
+
socket.send(JSON.stringify({ type: "operation.result", requestId: command.operationRequestId, ok: false, message: error instanceof Error ? error.message : String(error) }));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
socket.send(JSON.stringify({
|
|
203
|
+
type: "error",
|
|
204
|
+
message: error instanceof Error ? error.message : "command failed",
|
|
205
|
+
}));
|
|
206
|
+
}
|
|
207
|
+
finally {
|
|
208
|
+
// Hand the claim back whatever happened - including a revert that threw before it ever
|
|
209
|
+
// reached the service. Without this a refused or malformed revert would leave the
|
|
210
|
+
// conversation permanently unable to send, which is a worse failure than the one the claim
|
|
211
|
+
// was added to prevent.
|
|
212
|
+
if (parsed.type === "message.revert")
|
|
213
|
+
this.commandHandler.releaseRevert(parsed.conversationId);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
if (runIndependently)
|
|
217
|
+
void execute().catch((error) => {
|
|
218
|
+
console.warn(`Super Pi: command dispatch failed: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
219
|
+
});
|
|
220
|
+
else
|
|
221
|
+
commandQueue = commandQueue.then(execute).catch((error) => {
|
|
222
|
+
console.warn(`Super Pi: command dispatch failed: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
223
|
+
});
|
|
163
224
|
});
|
|
164
225
|
});
|
|
165
226
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evandu/super-pi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.34",
|
|
4
4
|
"description": "Web control plane and companion for Pi",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"prepack": "npm run build",
|
|
26
26
|
"dev": "concurrently --kill-others-on-fail \"npm:dev:server\" \"npm:dev:client\"",
|
|
27
27
|
"dev:client": "vite --host 0.0.0.0",
|
|
28
|
-
"dev:server": "tsx watch src/server/index.ts",
|
|
28
|
+
"dev:server": "cross-env SUPER_PI_PORT=3081 tsx watch src/server/index.ts",
|
|
29
|
+
"dev:installed": "npm run build && cross-env SUPER_PI_PORT=3080 node bin/super-pi.js",
|
|
29
30
|
"build": "tsc -b && vite build",
|
|
30
31
|
"start": "node dist/server/index.js",
|
|
31
32
|
"restart:server": "node scripts/restart-server.mjs",
|
|
@@ -135,6 +136,7 @@
|
|
|
135
136
|
"react-dom": "latest",
|
|
136
137
|
"tsx": "latest",
|
|
137
138
|
"typescript": "latest",
|
|
138
|
-
"vite": "latest"
|
|
139
|
+
"vite": "latest",
|
|
140
|
+
"cross-env": "latest"
|
|
139
141
|
}
|
|
140
142
|
}
|