pi-freeflow 1.0.6 → 1.0.8
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 +8 -12
- package/extensions/index.ts +69 -25
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -137,17 +137,6 @@ export default {
|
|
|
137
137
|
status: response.status,
|
|
138
138
|
headers: response.headers,
|
|
139
139
|
});
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
After deploying, register your Cloudflare Worker URL in OMP:
|
|
145
|
-
```text
|
|
146
|
-
/freeflow use https://your-worker-name.your-subdomain.workers.dev
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
---
|
|
150
|
-
|
|
151
140
|
### 2. Vercel Relay
|
|
152
141
|
Deploy using the built-in deploy command:
|
|
153
142
|
```text
|
|
@@ -159,13 +148,20 @@ Deploy using the built-in deploy command:
|
|
|
159
148
|
|
|
160
149
|
## 🛠️ Diagnostics & Troubleshooting
|
|
161
150
|
|
|
151
|
+
### Custom Port Configuration
|
|
152
|
+
By default, `pi-freeflow` uses a **Single Shared Port** (`18080`). All concurrent sub-agents automatically share the same master proxy port without spawning redundant servers.
|
|
153
|
+
|
|
154
|
+
To change the base port:
|
|
155
|
+
```env
|
|
156
|
+
FREEFLOW_PORT=19000
|
|
157
|
+
```
|
|
158
|
+
|
|
162
159
|
View live debug logs:
|
|
163
160
|
```bash
|
|
164
161
|
cat ~/.pi/agent/pi-freeflow.log | tail -n 30
|
|
165
162
|
# or inside OMP:
|
|
166
163
|
/freeflow logs
|
|
167
164
|
```
|
|
168
|
-
|
|
169
165
|
---
|
|
170
166
|
|
|
171
167
|
## 📄 License
|
package/extensions/index.ts
CHANGED
|
@@ -99,7 +99,11 @@ function loadRelayState(): RelayState {
|
|
|
99
99
|
}
|
|
100
100
|
function saveRelayState(s: RelayState): void {
|
|
101
101
|
try {
|
|
102
|
-
|
|
102
|
+
const dir = path.dirname(RELAY_STATE_FILE);
|
|
103
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
104
|
+
const tmpPath = `${RELAY_STATE_FILE}.${randomUUID()}.tmp`;
|
|
105
|
+
fs.writeFileSync(tmpPath, JSON.stringify(s, null, 2), "utf8");
|
|
106
|
+
fs.renameSync(tmpPath, RELAY_STATE_FILE);
|
|
103
107
|
} catch (e) {
|
|
104
108
|
log("warn", "could not persist relay state", { error: String(e) });
|
|
105
109
|
}
|
|
@@ -663,6 +667,7 @@ const ALLOWED_METHODS = new Set(["GET", "POST", "OPTIONS", "HEAD"]);
|
|
|
663
667
|
const STRIP_HEADERS = new Set([
|
|
664
668
|
"authorization",
|
|
665
669
|
"host",
|
|
670
|
+
"content-length",
|
|
666
671
|
"x-forwarded-for",
|
|
667
672
|
"x-forwarded-host",
|
|
668
673
|
"x-forwarded-proto",
|
|
@@ -942,12 +947,16 @@ function normalizeRequestBody(
|
|
|
942
947
|
}
|
|
943
948
|
}
|
|
944
949
|
|
|
945
|
-
// 3. Max tokens
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
950
|
+
// 3. Max & Min tokens clamping (OpenCode Zen requires min 16; Vercel relay caps at 64k)
|
|
951
|
+
const mt = (body.max_tokens ?? body.maxTokens ?? body.max_output_tokens) as number | undefined;
|
|
952
|
+
if (typeof mt === "number") {
|
|
953
|
+
let clamped = mt;
|
|
954
|
+
if (clamped < 16) clamped = 16;
|
|
955
|
+
else if (isRelay && clamped > RELAY_MAX_TOKENS) clamped = RELAY_MAX_TOKENS;
|
|
956
|
+
|
|
957
|
+
if (body.max_tokens !== undefined) body.max_tokens = clamped;
|
|
958
|
+
if (body.maxTokens !== undefined) body.maxTokens = clamped;
|
|
959
|
+
if (body.max_output_tokens !== undefined) body.max_output_tokens = clamped;
|
|
951
960
|
}
|
|
952
961
|
|
|
953
962
|
return body;
|
|
@@ -1002,6 +1011,12 @@ function pipeUpstreamStream(
|
|
|
1002
1011
|
req.on("close", () => {
|
|
1003
1012
|
if (!nodeStream.destroyed) nodeStream.destroy();
|
|
1004
1013
|
});
|
|
1014
|
+
res.on("close", () => {
|
|
1015
|
+
if (!nodeStream.destroyed) nodeStream.destroy();
|
|
1016
|
+
});
|
|
1017
|
+
res.on("error", () => {
|
|
1018
|
+
if (!nodeStream.destroyed) nodeStream.destroy();
|
|
1019
|
+
});
|
|
1005
1020
|
}
|
|
1006
1021
|
|
|
1007
1022
|
// ── Start local proxy ──────────────────────────────────────────────
|
|
@@ -1083,13 +1098,15 @@ function startProxy(
|
|
|
1083
1098
|
if (isKilo && parsedBody) {
|
|
1084
1099
|
// KiloCode gateway routing (free models are keyless)
|
|
1085
1100
|
const isStream = parsedBody.stream === true;
|
|
1101
|
+
const kiloBodyObj = structuredClone(parsedBody);
|
|
1102
|
+
normalizeRequestBody(kiloBodyObj, true);
|
|
1086
1103
|
const response = await relayFetch(KILO_CHAT_URL, {
|
|
1087
1104
|
method: "POST",
|
|
1088
1105
|
headers: {
|
|
1089
1106
|
"Content-Type": "application/json",
|
|
1090
1107
|
Authorization: "Bearer kilo-free",
|
|
1091
1108
|
},
|
|
1092
|
-
body: JSON.stringify(
|
|
1109
|
+
body: JSON.stringify(kiloBodyObj),
|
|
1093
1110
|
signal: AbortSignal.timeout(300_000),
|
|
1094
1111
|
});
|
|
1095
1112
|
if (isStream && response.ok && response.body) {
|
|
@@ -1215,12 +1232,18 @@ function startProxy(
|
|
|
1215
1232
|
res.end();
|
|
1216
1233
|
}
|
|
1217
1234
|
});
|
|
1218
|
-
proxy.setTimeout(
|
|
1235
|
+
proxy.setTimeout(300_000, () => {
|
|
1219
1236
|
proxy.destroy(new Error("timeout"));
|
|
1220
1237
|
});
|
|
1221
1238
|
req.on("aborted", () => {
|
|
1222
1239
|
if (!proxy.destroyed) proxy.destroy();
|
|
1223
1240
|
});
|
|
1241
|
+
req.on("close", () => {
|
|
1242
|
+
if (!proxy.destroyed) proxy.destroy();
|
|
1243
|
+
});
|
|
1244
|
+
res.on("close", () => {
|
|
1245
|
+
if (!proxy.destroyed) proxy.destroy();
|
|
1246
|
+
});
|
|
1224
1247
|
// ponytail: body already buffered in bodyChunks above for model routing;
|
|
1225
1248
|
// req is drained so pipe() would send an empty body → upstream hang → 502.
|
|
1226
1249
|
proxy.end(directBody);
|
|
@@ -1267,23 +1290,44 @@ function startProxy(
|
|
|
1267
1290
|
});
|
|
1268
1291
|
}
|
|
1269
1292
|
|
|
1270
|
-
//
|
|
1271
|
-
|
|
1272
|
-
log("info", "extension loading...");
|
|
1273
|
-
let server: http.Server;
|
|
1274
|
-
let actualPort: number;
|
|
1293
|
+
// Probe whether an existing freeflow proxy is already running on a port
|
|
1294
|
+
async function isProxyAlive(port: number): Promise<boolean> {
|
|
1275
1295
|
try {
|
|
1276
|
-
const
|
|
1277
|
-
|
|
1278
|
-
|
|
1296
|
+
const res = await fetch(`http://${HOST}:${port}/v1/models`, {
|
|
1297
|
+
signal: AbortSignal.timeout(500),
|
|
1298
|
+
});
|
|
1299
|
+
const ct = res.headers.get("content-type") || "";
|
|
1300
|
+
return res.ok && ct.includes("application/json");
|
|
1279
1301
|
} catch {
|
|
1280
|
-
|
|
1281
|
-
"error",
|
|
1282
|
-
"extension inactive — could not bind proxy port. resolve the port conflict and restart pi.",
|
|
1283
|
-
);
|
|
1284
|
-
return;
|
|
1302
|
+
return false;
|
|
1285
1303
|
}
|
|
1304
|
+
}
|
|
1286
1305
|
|
|
1306
|
+
// ── Main extension ─────────────────────────────────────────────────
|
|
1307
|
+
export default async function (pi: ExtensionAPI) {
|
|
1308
|
+
log("info", "extension loading...");
|
|
1309
|
+
let server: http.Server | null = null;
|
|
1310
|
+
let actualPort = PORT;
|
|
1311
|
+
|
|
1312
|
+
// Single-Port Shared Pattern: If proxy is already running on PORT (e.g. parent session),
|
|
1313
|
+
// subagents reuse http://127.0.0.1:18080 directly without spawning redundant servers!
|
|
1314
|
+
const alreadyRunning = await isProxyAlive(PORT);
|
|
1315
|
+
if (alreadyRunning) {
|
|
1316
|
+
log("info", `reusing existing freeflow proxy on http://${HOST}:${PORT}`);
|
|
1317
|
+
actualPort = PORT;
|
|
1318
|
+
} else {
|
|
1319
|
+
try {
|
|
1320
|
+
const r = await startProxy();
|
|
1321
|
+
server = r.server;
|
|
1322
|
+
actualPort = r.port;
|
|
1323
|
+
} catch {
|
|
1324
|
+
log(
|
|
1325
|
+
"error",
|
|
1326
|
+
"extension inactive — could not bind proxy port. resolve the port conflict and restart pi.",
|
|
1327
|
+
);
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1287
1331
|
// Health check opencode models
|
|
1288
1332
|
log("info", `checking ${KNOWN_MODELS.length} opencode model(s)...`);
|
|
1289
1333
|
const opencodeChecks = await Promise.all(
|
|
@@ -1591,7 +1635,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1591
1635
|
pi.registerCommand("bansos", commandSpec);
|
|
1592
1636
|
|
|
1593
1637
|
// Reload persisted state on session start/resume (env overrides still win).
|
|
1594
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
1638
|
+
pi.on?.("session_start", async (_event, ctx) => {
|
|
1595
1639
|
relayState = resolveRelayState();
|
|
1596
1640
|
statusUi = ctx.ui;
|
|
1597
1641
|
ctx.ui?.setStatus?.("freeflow", undefined);
|
|
@@ -1600,7 +1644,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1600
1644
|
|
|
1601
1645
|
// Pi normally pauses after threshold compaction. Queue a follow-up while the
|
|
1602
1646
|
// original run is still active so the core agent continues automatically.
|
|
1603
|
-
pi.on("session_compact", (event, ctx) => {
|
|
1647
|
+
pi.on?.("session_compact", (event, ctx) => {
|
|
1604
1648
|
if (
|
|
1605
1649
|
event.reason !== "threshold" ||
|
|
1606
1650
|
event.willRetry ||
|
|
@@ -1609,7 +1653,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1609
1653
|
) {
|
|
1610
1654
|
return;
|
|
1611
1655
|
}
|
|
1612
|
-
pi.sendUserMessage(
|
|
1656
|
+
pi.sendUserMessage?.(
|
|
1613
1657
|
"Continue the current task from the compacted context. Do not wait for another user message; proceed with the next required step.",
|
|
1614
1658
|
{ deliverAs: "followUp" },
|
|
1615
1659
|
);
|