pi-freeflow 1.9.0 → 1.9.1
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/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/src/index.ts +67 -82
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to pi-freeflow. Public, user-visible behavior only.
|
|
4
4
|
|
|
5
|
+
## 1.9.1 - 2026-08-30
|
|
6
|
+
|
|
7
|
+
### Fixes
|
|
8
|
+
- Stale-daemon guard completed: a pre-1.9.0 daemon cannot report its in-flight requests, and usage cannot be verified — so it is now left running instead of being replaced. Previously such a daemon could still be killed mid-stream while busy (a one-time window when upgrading from 1.8.x). The guarantee now holds in every case: only older, verified-idle daemons are replaced.
|
|
9
|
+
- The stale-daemon kill ritual was consolidated into one path (it was duplicated at five call sites); behavior unchanged.
|
|
10
|
+
- Tests: new coverage for the pre-1.9 daemon case; shared sandbox helpers extracted.
|
|
11
|
+
|
|
5
12
|
## 1.9.0 - 2026-08-30
|
|
6
13
|
|
|
7
14
|
### Development hardening
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-freeflow",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.9.
|
|
4
|
+
"version": "1.9.1",
|
|
5
5
|
"description": "Thin provider for OMP/Pi — model list + dumb relay proxy + log; host pi-ai owns thinking/normalization",
|
|
6
6
|
"main": "extensions/index.ts",
|
|
7
7
|
"types": "src/index.ts",
|
package/src/index.ts
CHANGED
|
@@ -165,8 +165,9 @@ export function buildProviderConfig(
|
|
|
165
165
|
* Stale-daemon replacement guard.
|
|
166
166
|
* Never interrupt a working shared daemon: a newer-version daemon is left
|
|
167
167
|
* running (downgrade case), an in-flight daemon is left running (busy case —
|
|
168
|
-
* its streams would die mid-flight),
|
|
169
|
-
*
|
|
168
|
+
* its streams would die mid-flight), a pre-1.9 daemon that cannot report
|
|
169
|
+
* usage is left running (cannot verify), and NO_KILL_ENV disables replacement
|
|
170
|
+
* entirely. Only older, VERIFIED-idle daemons are replaced.
|
|
170
171
|
*/
|
|
171
172
|
async function shouldReplaceDaemon(port: number, remoteVer: string): Promise<boolean> {
|
|
172
173
|
if (NO_KILL_ENV && process.env[NO_KILL_ENV] === "1") {
|
|
@@ -182,7 +183,16 @@ async function shouldReplaceDaemon(port: number, remoteVer: string): Promise<boo
|
|
|
182
183
|
return false;
|
|
183
184
|
}
|
|
184
185
|
const health = await getDaemonHealth(port);
|
|
185
|
-
if (health
|
|
186
|
+
if (health === null || health.activeRequests === undefined) {
|
|
187
|
+
// Pre-1.9 daemons do not report in-flight requests: usage cannot be
|
|
188
|
+
// verified, so the running daemon is left untouched. Interrupting a
|
|
189
|
+
// possibly-busy session is worse than keeping an older daemon alive.
|
|
190
|
+
logInfo(
|
|
191
|
+
`Reusing existing pi-freeflow proxy daemon on http://${HOST}:${port} (cannot verify usage — leaving the running daemon untouched)`,
|
|
192
|
+
);
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
if (health.activeRequests > 0) {
|
|
186
196
|
logInfo(
|
|
187
197
|
`Reusing existing pi-freeflow proxy daemon on http://${HOST}:${port} (${health.activeRequests} active request${health.activeRequests === 1 ? "" : "s"} — not interrupted)`,
|
|
188
198
|
);
|
|
@@ -190,6 +200,33 @@ async function shouldReplaceDaemon(port: number, remoteVer: string): Promise<boo
|
|
|
190
200
|
}
|
|
191
201
|
return true;
|
|
192
202
|
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Guarded kill ritual for a stale daemon: decide (older-only, verified-idle,
|
|
206
|
+
* NO_KILL) → log → kill → wait for the port to free. Returns true when the
|
|
207
|
+
* port stopped answering (caller re-binds or re-attaches below); false when
|
|
208
|
+
* the daemon is left running — every skip reason is logged here.
|
|
209
|
+
*/
|
|
210
|
+
async function killStaleDaemon(
|
|
211
|
+
port: number,
|
|
212
|
+
remoteVer: string,
|
|
213
|
+
what: string,
|
|
214
|
+
): Promise<boolean> {
|
|
215
|
+
if (!(await shouldReplaceDaemon(port, remoteVer))) return false;
|
|
216
|
+
logWarn(`stale ${what} v${remoteVer} on :${port} (need v${PKG_VERSION}) — replacing`, {
|
|
217
|
+
remoteVer,
|
|
218
|
+
expected: PKG_VERSION,
|
|
219
|
+
});
|
|
220
|
+
await killPortHolder(port);
|
|
221
|
+
for (let i = 0; i < 10; i++) {
|
|
222
|
+
await new Promise<void>((r) => setTimeout(r, 200));
|
|
223
|
+
if (!(await isProxyAlive(port))) return true;
|
|
224
|
+
}
|
|
225
|
+
logInfo(
|
|
226
|
+
`Reusing existing pi-freeflow proxy daemon on http://${HOST}:${port} (stale kill did not free port)`,
|
|
227
|
+
);
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
193
230
|
/**
|
|
194
231
|
* Main extension entrypoint
|
|
195
232
|
*/
|
|
@@ -209,19 +246,8 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
209
246
|
let alreadyRunning = await isProxyAlive(PORT);
|
|
210
247
|
if (alreadyRunning) {
|
|
211
248
|
const remoteVer = await getDaemonVersion(PORT);
|
|
212
|
-
if (remoteVer !== null && remoteVer !== PKG_VERSION
|
|
213
|
-
|
|
214
|
-
remoteVer,
|
|
215
|
-
expected: PKG_VERSION,
|
|
216
|
-
});
|
|
217
|
-
const killed = await killPortHolder(PORT);
|
|
218
|
-
if (killed) {
|
|
219
|
-
for (let i = 0; i < 10; i++) {
|
|
220
|
-
await new Promise<void>((r) => setTimeout(r, 200));
|
|
221
|
-
if (!(await isProxyAlive(PORT))) break;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
if (!(await isProxyAlive(PORT))) {
|
|
249
|
+
if (remoteVer !== null && remoteVer !== PKG_VERSION) {
|
|
250
|
+
if (await killStaleDaemon(PORT, remoteVer, "proxy daemon")) {
|
|
225
251
|
try {
|
|
226
252
|
const r = await startProxy();
|
|
227
253
|
server = r.server;
|
|
@@ -234,7 +260,6 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
234
260
|
actualPort = PORT;
|
|
235
261
|
}
|
|
236
262
|
} else {
|
|
237
|
-
logInfo(`Reusing existing pi-freeflow proxy daemon on http://${HOST}:${PORT} (stale kill did not free port)`);
|
|
238
263
|
actualPort = PORT;
|
|
239
264
|
}
|
|
240
265
|
} else {
|
|
@@ -243,19 +268,8 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
243
268
|
}
|
|
244
269
|
} else if (PORT !== LEGACY_PORT && (await isProxyAlive(LEGACY_PORT))) {
|
|
245
270
|
const remoteVer = await getDaemonVersion(LEGACY_PORT);
|
|
246
|
-
if (remoteVer !== null && remoteVer !== PKG_VERSION
|
|
247
|
-
|
|
248
|
-
remoteVer,
|
|
249
|
-
expected: PKG_VERSION,
|
|
250
|
-
});
|
|
251
|
-
const killed = await killPortHolder(LEGACY_PORT);
|
|
252
|
-
if (killed) {
|
|
253
|
-
for (let i = 0; i < 10; i++) {
|
|
254
|
-
await new Promise<void>((r) => setTimeout(r, 200));
|
|
255
|
-
if (!(await isProxyAlive(LEGACY_PORT))) break;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
if (!(await isProxyAlive(LEGACY_PORT)) && !(await isProxyAlive(PORT))) {
|
|
271
|
+
if (remoteVer !== null && remoteVer !== PKG_VERSION) {
|
|
272
|
+
if (await killStaleDaemon(LEGACY_PORT, remoteVer, "legacy proxy daemon")) {
|
|
259
273
|
try {
|
|
260
274
|
const r = await startProxy();
|
|
261
275
|
server = r.server;
|
|
@@ -269,7 +283,6 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
269
283
|
actualPort = LEGACY_PORT;
|
|
270
284
|
}
|
|
271
285
|
} else {
|
|
272
|
-
logInfo(`Reusing existing legacy pi-freeflow proxy daemon on http://${HOST}:${LEGACY_PORT}`);
|
|
273
286
|
alreadyRunning = true;
|
|
274
287
|
actualPort = LEGACY_PORT;
|
|
275
288
|
}
|
|
@@ -313,73 +326,45 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
313
326
|
if (await isProxyAlive(actualPort)) {
|
|
314
327
|
const v = await getDaemonVersion(actualPort);
|
|
315
328
|
if (v === null || v === PKG_VERSION) return;
|
|
316
|
-
if (!(await
|
|
317
|
-
logWarn(`proxy on :${actualPort} is stale v${v} (need v${PKG_VERSION}) — replacing`, {
|
|
318
|
-
remoteVer: v,
|
|
319
|
-
expected: PKG_VERSION,
|
|
320
|
-
});
|
|
321
|
-
const killed = await killPortHolder(actualPort);
|
|
322
|
-
if (killed) {
|
|
323
|
-
for (let i = 0; i < 10; i++) {
|
|
324
|
-
await new Promise<void>((r) => setTimeout(r, 200));
|
|
325
|
-
if (!(await isProxyAlive(actualPort))) break;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
if (await isProxyAlive(actualPort)) return;
|
|
329
|
+
if (!(await killStaleDaemon(actualPort, v, "proxy daemon"))) return;
|
|
329
330
|
}
|
|
330
331
|
if (await isProxyAlive(PORT)) {
|
|
331
332
|
const v = await getDaemonVersion(PORT);
|
|
332
|
-
if (v
|
|
333
|
-
logWarn(`proxy on :${PORT} is stale v${v} (need v${PKG_VERSION}) — replacing`, {
|
|
334
|
-
remoteVer: v,
|
|
335
|
-
expected: PKG_VERSION,
|
|
336
|
-
});
|
|
337
|
-
const killed = await killPortHolder(PORT);
|
|
338
|
-
if (killed) {
|
|
339
|
-
for (let i = 0; i < 10; i++) {
|
|
340
|
-
await new Promise<void>((r) => setTimeout(r, 200));
|
|
341
|
-
if (!(await isProxyAlive(PORT))) break;
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
if (await isProxyAlive(PORT)) {
|
|
345
|
-
actualPort = PORT;
|
|
346
|
-
registerCatalog(getAliveCatalog());
|
|
347
|
-
logInfo(`Re-attached to proxy daemon on http://${HOST}:${PORT} (stale kill did not free port)`);
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
} else {
|
|
333
|
+
if (v === null || v === PKG_VERSION) {
|
|
351
334
|
actualPort = PORT;
|
|
352
335
|
registerCatalog(getAliveCatalog());
|
|
353
336
|
logInfo(`Re-attached to proxy daemon on http://${HOST}:${PORT}`);
|
|
354
337
|
return;
|
|
355
338
|
}
|
|
339
|
+
if (await killStaleDaemon(PORT, v, "proxy daemon")) {
|
|
340
|
+
// Port freed — fall through to legacy probe / re-bind below.
|
|
341
|
+
} else if (await isProxyAlive(PORT)) {
|
|
342
|
+
actualPort = PORT;
|
|
343
|
+
registerCatalog(getAliveCatalog());
|
|
344
|
+
logInfo(`Re-attached to proxy daemon on http://${HOST}:${PORT} (stale kill did not free port)`);
|
|
345
|
+
return;
|
|
346
|
+
} else {
|
|
347
|
+
return; // daemon replaced; re-bind happens below
|
|
348
|
+
}
|
|
356
349
|
}
|
|
357
350
|
if (PORT !== LEGACY_PORT && (await isProxyAlive(LEGACY_PORT))) {
|
|
358
351
|
const v = await getDaemonVersion(LEGACY_PORT);
|
|
359
|
-
if (v
|
|
360
|
-
logWarn(`legacy proxy on :${LEGACY_PORT} is stale v${v} (need v${PKG_VERSION}) — replacing`, {
|
|
361
|
-
remoteVer: v,
|
|
362
|
-
expected: PKG_VERSION,
|
|
363
|
-
});
|
|
364
|
-
const killed = await killPortHolder(LEGACY_PORT);
|
|
365
|
-
if (killed) {
|
|
366
|
-
for (let i = 0; i < 10; i++) {
|
|
367
|
-
await new Promise<void>((r) => setTimeout(r, 200));
|
|
368
|
-
if (!(await isProxyAlive(LEGACY_PORT))) break;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
if (await isProxyAlive(LEGACY_PORT)) {
|
|
372
|
-
actualPort = LEGACY_PORT;
|
|
373
|
-
registerCatalog(getAliveCatalog());
|
|
374
|
-
logInfo(`Re-attached to legacy proxy daemon on http://${HOST}:${LEGACY_PORT} (stale kill did not free port)`);
|
|
375
|
-
return;
|
|
376
|
-
}
|
|
377
|
-
} else {
|
|
352
|
+
if (v === null || v === PKG_VERSION) {
|
|
378
353
|
actualPort = LEGACY_PORT;
|
|
379
354
|
registerCatalog(getAliveCatalog());
|
|
380
355
|
logInfo(`Re-attached to legacy proxy daemon on http://${HOST}:${LEGACY_PORT}`);
|
|
381
356
|
return;
|
|
382
357
|
}
|
|
358
|
+
if (await killStaleDaemon(LEGACY_PORT, v, "legacy proxy daemon")) {
|
|
359
|
+
// Port freed — fall through to the re-bind below.
|
|
360
|
+
} else if (await isProxyAlive(LEGACY_PORT)) {
|
|
361
|
+
actualPort = LEGACY_PORT;
|
|
362
|
+
registerCatalog(getAliveCatalog());
|
|
363
|
+
logInfo(`Re-attached to legacy proxy daemon on http://${HOST}:${LEGACY_PORT} (stale kill did not free port)`);
|
|
364
|
+
return;
|
|
365
|
+
} else {
|
|
366
|
+
return; // daemon replaced; re-bind happens below
|
|
367
|
+
}
|
|
383
368
|
}
|
|
384
369
|
const r = await startProxy();
|
|
385
370
|
if (r.server) server = r.server;
|