@evandu/super-pi 0.1.97 → 0.1.98
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/dist/client/.super-pi-client-sources.json +1 -1
- package/dist/server/index.js +44 -0
- package/dist/server/model-filter.js +5 -0
- package/dist/server/pi-command.d.ts +2 -1
- package/dist/server/pi-command.js +7 -2
- package/dist/server/pi-session-pool.d.ts +12 -0
- package/dist/server/pi-session-pool.js +54 -2
- package/dist/server/pi-session.d.ts +102 -6
- package/dist/server/pi-session.js +121 -5
- package/dist/server/runtime.d.ts +65 -3
- package/dist/server/runtime.js +201 -18
- package/package.json +2 -3
- package/scripts/patch-brace-expansion.mjs +155 -74
package/dist/server/runtime.js
CHANGED
|
@@ -17,7 +17,7 @@ import { describeDeliveredUserMessage, describePiQueue, handleSessionEvent, Turn
|
|
|
17
17
|
// them; the one-shot drain loop below reads the same records through the same two functions, so both
|
|
18
18
|
// paths recognise a delivery identically. Re-exported because they were part of this module's surface.
|
|
19
19
|
export { describeDeliveredUserMessage, describePiQueue } from "./pi-session-events.js";
|
|
20
|
-
import { createPiSession, piSdkAvailable, superPiExtensionPaths } from "./pi-session.js";
|
|
20
|
+
import { createPiSession, createSdkCatalog, piSdkAvailable, resolveTranscriptFile, superPiExtensionPaths } from "./pi-session.js";
|
|
21
21
|
import { TASK_CONVERSATION_ENV, TASK_DEPTH_ENV, TASK_ENDPOINT_ENV, TASK_EXEC_ENDPOINT_ENV, TASK_ROSTER_ENV, TASK_TOOL_NAME, TODO_ENDPOINT_ENV, TODO_TOOL_NAME, WORKFLOW_ENDPOINT_ENV, WORKFLOW_TOOL_NAME, RUN_ASYNC_TOOL_NAME, rosterPayload, } from "./task-tool.js";
|
|
22
22
|
import { BLOCKING_UI_METHODS, EXTENSION_UI_METHODS, } from "./types.js";
|
|
23
23
|
import { loadConversationImages } from "./upload.js";
|
|
@@ -193,8 +193,19 @@ const CATALOG_TTL_MS = 10 * 60_000;
|
|
|
193
193
|
/** A FAILED catalog is cached for far less time, so one broken Pi cannot be retried on every keystroke of a model switch. */
|
|
194
194
|
const CATALOG_FAILURE_TTL_MS = 30_000;
|
|
195
195
|
const CATALOG_COMMAND_TIMEOUT_MS = 15_000;
|
|
196
|
-
/**
|
|
197
|
-
|
|
196
|
+
/**
|
|
197
|
+
* Keys Pi's model answers by. Exported so the store and the tests agree with the runtime.
|
|
198
|
+
*
|
|
199
|
+
* THE MODEL IS NOT PART OF THE KEY, and that is the second half of why the picker no longer starts a
|
|
200
|
+
* process. The MODEL LIST is the same answer whichever model a conversation has selected - it is the
|
|
201
|
+
* list of models Pi offers, not the ones one conversation may use. The only thing that varies with the
|
|
202
|
+
* selection is the THINKING LEVELS, which belong to the model in use.
|
|
203
|
+
*
|
|
204
|
+
* So the key records just whether levels were asked for. Keying on the model (as this used to) meant a
|
|
205
|
+
* cache miss for every model a person ever picked, and every miss was a process start - so the picker
|
|
206
|
+
* went grey again after every model change, which is exactly when a person is moving between models.
|
|
207
|
+
*/
|
|
208
|
+
export const catalogKey = (selection) => (selection?.provider && selection.modelId ? "levels" : "list");
|
|
198
209
|
/**
|
|
199
210
|
* Where the `task` extension lives, as an ABSOLUTE path.
|
|
200
211
|
*
|
|
@@ -255,6 +266,8 @@ export class PiRpcRuntime {
|
|
|
255
266
|
* in the pool, and a turn is a run ON that session rather than a process OF its own.
|
|
256
267
|
*/
|
|
257
268
|
sessionPool;
|
|
269
|
+
/** Where Super Pi itself is installed, which is also where its own extensions are found. */
|
|
270
|
+
repositoryRoot;
|
|
258
271
|
/**
|
|
259
272
|
* The turn currently running on each conversation's session.
|
|
260
273
|
*
|
|
@@ -297,8 +310,25 @@ export class PiRpcRuntime {
|
|
|
297
310
|
/** Distinguishes steer command ids. Never reused, so a late response cannot settle a later steer. */
|
|
298
311
|
steerSeq = 0;
|
|
299
312
|
catalogs = new Map();
|
|
300
|
-
/**
|
|
313
|
+
/**
|
|
314
|
+
* In-flight reads, so N clients asking at once cost ONE read rather than N.
|
|
315
|
+
*
|
|
316
|
+
* "One read", not "one Pi spawn": the catalog is answered out of Pi's SDK and no process is started
|
|
317
|
+
* at all. The wording here used to promise a shared spawn, which is what the old implementation did.
|
|
318
|
+
*/
|
|
301
319
|
pendingCatalogs = new Map();
|
|
320
|
+
/**
|
|
321
|
+
* Pi's SDK catalog reader, built once and reused.
|
|
322
|
+
*
|
|
323
|
+
* The ModelRuntime and SettingsManager it holds read the auth store and the model store from disk, so
|
|
324
|
+
* building a new one per call would put back the I/O this exists to avoid. They are read-mostly
|
|
325
|
+
* views of files that change rarely, and Pi's own long-lived session holds the same objects for its
|
|
326
|
+
* whole life.
|
|
327
|
+
*
|
|
328
|
+
* A FAILED build is remembered as a failure rather than retried per call: a broken SDK would
|
|
329
|
+
* otherwise be re-imported on every picker open.
|
|
330
|
+
*/
|
|
331
|
+
sdkCatalogPromise;
|
|
302
332
|
/** Latches the "no extension on disk" warning, so it is said once rather than once per turn. */
|
|
303
333
|
warnedMissingExtension = false;
|
|
304
334
|
/** Latches the "Pi's SDK is not installed" warning, for the same reason. */
|
|
@@ -376,7 +406,8 @@ export class PiRpcRuntime {
|
|
|
376
406
|
console.warn(`PiRpcRuntime: ${this.pi.reason}`);
|
|
377
407
|
this.sessionMode = this.options.sessionMode
|
|
378
408
|
?? (process.env.SUPER_PI_SESSION_MODE === "oneshot" ? "oneshot" : "sdk");
|
|
379
|
-
this.
|
|
409
|
+
this.repositoryRoot = repositoryRootFrom();
|
|
410
|
+
this.sessionPool = new PiSessionPool(sessionDir, this.repositoryRoot, undefined, (conversationId) => {
|
|
380
411
|
// A session's credential dies with the session. The session is what HELD the token - its
|
|
381
412
|
// extension read it out of the environment when it was loaded - so a session that is gone must
|
|
382
413
|
// not leave a usable token behind.
|
|
@@ -727,8 +758,10 @@ export class PiRpcRuntime {
|
|
|
727
758
|
let firstTextSeen = false;
|
|
728
759
|
const finished = (async () => {
|
|
729
760
|
try {
|
|
730
|
-
|
|
731
|
-
|
|
761
|
+
const promptOptions = images?.length
|
|
762
|
+
? { images: images, preflightResult: () => traceLatency(conversation.id, "session.prompt.accepted", latencyStartedAt) }
|
|
763
|
+
: { preflightResult: () => traceLatency(conversation.id, "session.prompt.accepted", latencyStartedAt) };
|
|
764
|
+
await session.prompt(prompt, promptOptions);
|
|
732
765
|
// `prompt()` RETURNING IS NOT THE TURN ENDING. Pi's `prompt` resolves when the message has
|
|
733
766
|
// been ACCEPTED and the run has been started; the agent is still streaming, still executing
|
|
734
767
|
// tools, still settling. A turn that ended here would release the reader while the model was
|
|
@@ -1633,6 +1666,23 @@ export class PiRpcRuntime {
|
|
|
1633
1666
|
if (!info?.isDirectory())
|
|
1634
1667
|
throw new Error(`工作区目录不存在或不是目录,无法在其中找到这个会话的 Pi 记录:${childCwd}`);
|
|
1635
1668
|
await mkdir(this.sessionDir, { recursive: true });
|
|
1669
|
+
// THE SDK ANSWERS THIS, AND ANSWERING IT WITH A PROCESS IS WHAT USED TO MAKE REVERT SLOW.
|
|
1670
|
+
//
|
|
1671
|
+
// Pi looks a session up by id AND by working directory, and the SDK's session manager resolves the
|
|
1672
|
+
// same thing the RPC command would have - so the library is asked first, and a process is started
|
|
1673
|
+
// only when there is no library to ask. The verification below is unchanged and still decides
|
|
1674
|
+
// whether the file may be written: `truncateSessionFile` proves the header belongs to this
|
|
1675
|
+
// conversation and validates the boundary, so a wrong answer from either source still cannot
|
|
1676
|
+
// truncate somebody else's history.
|
|
1677
|
+
const resolved = await this.resolveTranscriptFileWithoutSpawn(childCwd, conversation.id);
|
|
1678
|
+
if (resolved !== undefined) {
|
|
1679
|
+
const found = await stat(resolved).catch(() => undefined);
|
|
1680
|
+
if (!found?.isFile())
|
|
1681
|
+
throw new Error(`Pi 还没有为这个会话建立记录文件(${resolved}),没有可以回退的历史。`);
|
|
1682
|
+
const result = await truncateSessionFile(resolved, boundaryEntryId, conversation.id, beforeBoundary);
|
|
1683
|
+
await this.sessionPool.evict(conversation.id);
|
|
1684
|
+
return { sessionFile: resolved, linesKept: result.linesKept, entriesDropped: result.entriesDropped };
|
|
1685
|
+
}
|
|
1636
1686
|
// NO agent flags and NO extension on this spawn. It sends no prompt and must leave nothing behind:
|
|
1637
1687
|
// `SessionManager._persist` only creates a file once a session holds a message
|
|
1638
1688
|
// (`session-manager.ts:1160-1170`), so an answer to `get_state` cannot write anything. Adding the
|
|
@@ -1724,10 +1774,10 @@ export class PiRpcRuntime {
|
|
|
1724
1774
|
return { entriesCopied: 0 };
|
|
1725
1775
|
if (!this.pi.found)
|
|
1726
1776
|
throw new Error(this.pi.reason);
|
|
1777
|
+
const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
|
|
1778
|
+
const target = join(this.sessionDir, `${stamp}_${forkedConversationId}.jsonl`);
|
|
1727
1779
|
const cached = conversation.piSessionFile;
|
|
1728
1780
|
if (cached && (await stat(cached).catch(() => undefined))?.isFile()) {
|
|
1729
|
-
const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
|
|
1730
|
-
const target = join(this.sessionDir, `${stamp}_${forkedConversationId}.jsonl`);
|
|
1731
1781
|
const result = await forkSessionFile(cached, target, conversation.id, forkedConversationId);
|
|
1732
1782
|
return { sessionFile: target, entriesCopied: result.entriesCopied };
|
|
1733
1783
|
}
|
|
@@ -1736,6 +1786,16 @@ export class PiRpcRuntime {
|
|
|
1736
1786
|
if (!info?.isDirectory())
|
|
1737
1787
|
throw new Error(`工作区目录不存在或不是目录,无法在其中找到这个会话的 Pi 记录:${childCwd}`);
|
|
1738
1788
|
await mkdir(this.sessionDir, { recursive: true });
|
|
1789
|
+
// THE SAME SDK LOOKUP AS REVERT, FOR THE SAME REASON: a process would be started only to be told
|
|
1790
|
+
// which file Pi uses, and Pi's own session manager answers that without a process.
|
|
1791
|
+
const resolved = await this.resolveTranscriptFileWithoutSpawn(childCwd, conversation.id);
|
|
1792
|
+
if (resolved !== undefined) {
|
|
1793
|
+
const found = await stat(resolved).catch(() => undefined);
|
|
1794
|
+
if (!found?.isFile())
|
|
1795
|
+
return { entriesCopied: 0 };
|
|
1796
|
+
const copied = await forkSessionFile(resolved, target, conversation.id, forkedConversationId);
|
|
1797
|
+
return { sessionFile: target, entriesCopied: copied.entriesCopied };
|
|
1798
|
+
}
|
|
1739
1799
|
// The same lookup spawn as `revert`, and the same reason for leaving the agent flags off: this child
|
|
1740
1800
|
// sends no prompt and must leave nothing behind.
|
|
1741
1801
|
const child = spawn(shellQuote(this.pi.command), ["--mode", "rpc", "--session-dir", this.sessionDir, "--session-id", conversation.id], {
|
|
@@ -1778,8 +1838,6 @@ export class PiRpcRuntime {
|
|
|
1778
1838
|
// is the fork's, not the source's: the file header keeps the SOURCE timestamp verbatim (it is history),
|
|
1779
1839
|
// while the filename carries when this copy was made, which is what makes the ordering of a directory
|
|
1780
1840
|
// full of forks mean anything.
|
|
1781
|
-
const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
|
|
1782
|
-
const target = join(this.sessionDir, `${stamp}_${forkedConversationId}.jsonl`);
|
|
1783
1841
|
const result = await forkSessionFile(sessionFile, target, conversation.id, forkedConversationId);
|
|
1784
1842
|
return { sessionFile: target, entriesCopied: result.entriesCopied };
|
|
1785
1843
|
}
|
|
@@ -2091,28 +2149,146 @@ export class PiRpcRuntime {
|
|
|
2091
2149
|
const key = catalogKey(selection);
|
|
2092
2150
|
const cached = this.catalogs.get(key);
|
|
2093
2151
|
const ttl = cached?.value.source === "pi" ? CATALOG_TTL_MS : CATALOG_FAILURE_TTL_MS;
|
|
2094
|
-
if (cached && Date.now() - cached.at < ttl)
|
|
2152
|
+
if (cached && Date.now() - cached.at < ttl) {
|
|
2153
|
+
if (process.env.SUPER_PI_TRACE_LATENCY === "1")
|
|
2154
|
+
console.warn(`[super-pi latency] catalog key=${key} phase=catalog.cache-hit`);
|
|
2095
2155
|
return cached.value;
|
|
2156
|
+
}
|
|
2096
2157
|
const inFlight = this.pendingCatalogs.get(key);
|
|
2097
|
-
if (inFlight)
|
|
2158
|
+
if (inFlight) {
|
|
2159
|
+
if (process.env.SUPER_PI_TRACE_LATENCY === "1")
|
|
2160
|
+
console.warn(`[super-pi latency] catalog key=${key} phase=catalog.inflight-join`);
|
|
2098
2161
|
return inFlight;
|
|
2162
|
+
}
|
|
2163
|
+
if (process.env.SUPER_PI_TRACE_LATENCY === "1")
|
|
2164
|
+
console.warn(`[super-pi latency] catalog key=${key} phase=catalog.read-begin`);
|
|
2099
2165
|
const pending = this.readCatalog(selection, key).finally(() => this.pendingCatalogs.delete(key));
|
|
2100
2166
|
this.pendingCatalogs.set(key, pending);
|
|
2101
2167
|
return pending;
|
|
2102
2168
|
}
|
|
2103
2169
|
/**
|
|
2104
|
-
*
|
|
2170
|
+
* Reads the model catalog WITHOUT starting a Pi process, and falls back to a spawned Pi only when
|
|
2171
|
+
* the SDK is not available.
|
|
2105
2172
|
*
|
|
2106
|
-
*
|
|
2107
|
-
*
|
|
2108
|
-
*
|
|
2109
|
-
* a
|
|
2173
|
+
* WHY THIS IS THE DEFAULT. The picker used to be answered by starting a `pi --mode rpc` child,
|
|
2174
|
+
* asking it three questions and killing it: a process start, measured at 866ms on a fast machine and
|
|
2175
|
+
* seconds on a slow one. And the cache key is the SELECTED MODEL, so it started again for every model
|
|
2176
|
+
* a person picked - which is why "the picker stays grey for ten seconds" followed choosing a model,
|
|
2177
|
+
* and not only the first time.
|
|
2178
|
+
*
|
|
2179
|
+
* Pi's SDK is the same code Pi's RPC mode is built on, so the answers are the same, and the two are
|
|
2180
|
+
* compared field by field in `scripts/probe-catalog-equivalence.mjs`.
|
|
2110
2181
|
*/
|
|
2111
2182
|
async readCatalog(selection, key) {
|
|
2112
2183
|
const unavailable = (error) => {
|
|
2113
2184
|
console.warn(`PiRpcRuntime: catalog for ${key} could not be read: ${error}`);
|
|
2114
2185
|
return { source: "unavailable", models: [], levels: [], fetchedAt: new Date().toISOString(), error };
|
|
2115
2186
|
};
|
|
2187
|
+
// AN EXPLICITLY NAMED PI IS THE ONE THAT WILL RUN THE CONVERSATION, SO IT IS THE ONE THAT GETS
|
|
2188
|
+
// ASKED. Naming a Pi is a statement about which Pi runs a turn, and the model list a person sees
|
|
2189
|
+
// has to be THAT Pi's list - otherwise the picker offers a model that no turn could ever use, and a
|
|
2190
|
+
// test harness driving its own Pi is refused models its Pi actually has.
|
|
2191
|
+
//
|
|
2192
|
+
// The SDK's list is the right answer only when the SDK IS the thing that will run the conversation,
|
|
2193
|
+
// which is the ordinary case: Super Pi depends on that package, so the SDK and the conversation
|
|
2194
|
+
// are the same Pi. It is the wrong answer the moment something else is - and then that something
|
|
2195
|
+
// else is asked, at the cost this change was made to remove.
|
|
2196
|
+
// NOT `this.pi.found &&`: a NAMED command that does not exist is still a named command, and the
|
|
2197
|
+
// honest answer to "this Pi cannot be asked" is still "unavailable" rather than a list from some
|
|
2198
|
+
// other Pi. `found: false` is exactly the case that must NOT be answered from elsewhere.
|
|
2199
|
+
const namedPiIsInCharge = this.pi.explicit === true || this.sessionMode === "oneshot";
|
|
2200
|
+
if (namedPiIsInCharge) {
|
|
2201
|
+
if (process.env.SUPER_PI_TRACE_LATENCY === "1")
|
|
2202
|
+
console.warn(`[super-pi latency] catalog key=${key} phase=catalog.asks-named-pi explicit=${this.pi.explicit ?? "not-found"} mode=${this.sessionMode}`);
|
|
2203
|
+
return this.readCatalogBySpawningPi(selection, key, unavailable);
|
|
2204
|
+
}
|
|
2205
|
+
const sdkRead = await this.readCatalogFromSdk(selection, unavailable);
|
|
2206
|
+
if (sdkRead) {
|
|
2207
|
+
this.catalogs.set(key, { at: Date.now(), value: sdkRead });
|
|
2208
|
+
return sdkRead;
|
|
2209
|
+
}
|
|
2210
|
+
return this.readCatalogBySpawningPi(selection, key, unavailable);
|
|
2211
|
+
}
|
|
2212
|
+
/**
|
|
2213
|
+
* The transcript file for a conversation, out of Pi's SDK, or undefined when the SDK cannot answer.
|
|
2214
|
+
*
|
|
2215
|
+
* Undefined means "ask someone else", not "no such file": a missing SDK and a missing transcript
|
|
2216
|
+
* are different facts, and only the first one should send the caller to the spawning path.
|
|
2217
|
+
*
|
|
2218
|
+
* For the same reason as the catalog, a NAMED Pi is not asked here: when something else will run the
|
|
2219
|
+
* conversation, it is also the only one that knows which file that conversation's transcript is in.
|
|
2220
|
+
*/
|
|
2221
|
+
async resolveTranscriptFileWithoutSpawn(cwd, conversationId) {
|
|
2222
|
+
if (!this.pi.found || this.pi.explicit || this.sessionMode === "oneshot")
|
|
2223
|
+
return undefined;
|
|
2224
|
+
const support = await this.sessionSupport();
|
|
2225
|
+
if (!support.available)
|
|
2226
|
+
return undefined;
|
|
2227
|
+
try {
|
|
2228
|
+
return await resolveTranscriptFile(cwd, conversationId, this.sessionDir);
|
|
2229
|
+
}
|
|
2230
|
+
catch (error) {
|
|
2231
|
+
console.warn(`PiRpcRuntime: Pi's SDK could not locate the transcript of ${conversationId}: ${error instanceof Error ? error.message : String(error)}. Falling back to asking a Pi process, which is slower but the same answer.`);
|
|
2232
|
+
return undefined;
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
async sdkCatalog() {
|
|
2236
|
+
if (!this.sdkCatalogPromise) {
|
|
2237
|
+
this.sdkCatalogPromise = createSdkCatalog(this.repositoryRoot).catch((error) => {
|
|
2238
|
+
console.warn(`PiRpcRuntime: Pi's SDK could not be used for the catalog: ${error instanceof Error ? error.message : String(error)}`);
|
|
2239
|
+
return undefined;
|
|
2240
|
+
});
|
|
2241
|
+
}
|
|
2242
|
+
return this.sdkCatalogPromise;
|
|
2243
|
+
}
|
|
2244
|
+
/**
|
|
2245
|
+
* The catalog, out of Pi's SDK. Returns undefined when the SDK is not available, which is the only
|
|
2246
|
+
* case in which the spawning path is reached.
|
|
2247
|
+
*/
|
|
2248
|
+
async readCatalogFromSdk(selection, unavailable) {
|
|
2249
|
+
const support = await this.sessionSupport();
|
|
2250
|
+
if (!support.available)
|
|
2251
|
+
return undefined;
|
|
2252
|
+
try {
|
|
2253
|
+
const reader = await this.sdkCatalog();
|
|
2254
|
+
if (!reader)
|
|
2255
|
+
return undefined;
|
|
2256
|
+
const models = describeModels(reader.models);
|
|
2257
|
+
if (!models.length)
|
|
2258
|
+
return unavailable("Pi's SDK reported no usable models");
|
|
2259
|
+
const pick = (model) => (model ? { provider: model.provider, modelId: model.modelId, label: model.label } : undefined);
|
|
2260
|
+
// The levels belong to the SELECTED model, so they are asked of the model in question rather
|
|
2261
|
+
// than of whatever Pi would have defaulted to.
|
|
2262
|
+
const forModel = selection ?? reader.defaultModel;
|
|
2263
|
+
const levels = forModel ? await reader.thinkingLevelsFor(forModel) : [];
|
|
2264
|
+
const value = {
|
|
2265
|
+
source: "pi",
|
|
2266
|
+
models,
|
|
2267
|
+
levels,
|
|
2268
|
+
defaultModel: pick(reader.defaultModel ? describeModel({ provider: reader.defaultModel.provider, id: reader.defaultModel.modelId, name: reader.defaultModel.modelId }) : undefined),
|
|
2269
|
+
defaultThinkingLevel: reader.defaultThinkingLevel ?? levels[0],
|
|
2270
|
+
fetchedAt: new Date().toISOString(),
|
|
2271
|
+
};
|
|
2272
|
+
return value;
|
|
2273
|
+
}
|
|
2274
|
+
catch (error) {
|
|
2275
|
+
// A failure here is a REASON TO WARN, not a reason to start a process: falling back to the spawn
|
|
2276
|
+
// would put the cost back exactly when the machine is already in trouble.
|
|
2277
|
+
console.warn(`PiRpcRuntime: the SDK catalog read failed, and this catalog will be empty rather than slow: ${error instanceof Error ? error.message : String(error)}`);
|
|
2278
|
+
return unavailable(`Pi's SDK could not answer: ${error instanceof Error ? error.message : String(error)}`);
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
/**
|
|
2282
|
+
* Asks one throwaway Pi for its model list, its thinking levels and its own current selection.
|
|
2283
|
+
*
|
|
2284
|
+
* Only reached when Pi's SDK is not available - which is a broken installation, not a normal one.
|
|
2285
|
+
*
|
|
2286
|
+
* The three commands are written back to back and only then awaited, so Pi works on them in
|
|
2287
|
+
* parallel - measured at 394ms for all three, against 714ms for the slowest one alone. A Pi
|
|
2288
|
+
* scoped to one conversation's model needs `set_model` FIRST and alone, because thinking levels are
|
|
2289
|
+
* a property of the selected model, not of Pi's default.
|
|
2290
|
+
*/
|
|
2291
|
+
async readCatalogBySpawningPi(selection, key, unavailable) {
|
|
2116
2292
|
// Catalog probes get their OWN session directory and a fixed session id: they must never write
|
|
2117
2293
|
// into a conversation's transcript, and a fixed id means a refresh reuses one file instead of
|
|
2118
2294
|
// leaving a new one behind on every model switch.
|
|
@@ -2125,6 +2301,8 @@ export class PiRpcRuntime {
|
|
|
2125
2301
|
let child;
|
|
2126
2302
|
try {
|
|
2127
2303
|
child = spawn(shellQuote(this.pi.command), ["--mode", "rpc", "--session-dir", directory, "--session-id", "catalog"], { cwd: directory, stdio: ["pipe", "pipe", "pipe"], shell: process.platform === "win32" });
|
|
2304
|
+
if (process.env.SUPER_PI_TRACE_LATENCY === "1")
|
|
2305
|
+
console.warn(`[super-pi latency] catalog key=${key} phase=catalog.spawned pid=${child.pid ?? "unknown"}`);
|
|
2128
2306
|
}
|
|
2129
2307
|
catch (error) {
|
|
2130
2308
|
return unavailable(`could not spawn Pi: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
@@ -2185,6 +2363,9 @@ export class PiRpcRuntime {
|
|
|
2185
2363
|
// under load in scripts/workspace-settings-browser.test.mjs, which timed out at 60s with an
|
|
2186
2364
|
// accepted command, an empty error channel and an untouched state.json.
|
|
2187
2365
|
if (child.exitCode === null && child.pid !== undefined) {
|
|
2366
|
+
const killedAt = performance.now();
|
|
2367
|
+
if (process.env.SUPER_PI_TRACE_LATENCY === "1")
|
|
2368
|
+
console.warn(`[super-pi latency] catalog key=${key} phase=catalog.kill.begin pid=${child.pid}`);
|
|
2188
2369
|
if (!child.killed) {
|
|
2189
2370
|
if (process.platform === "win32")
|
|
2190
2371
|
await killTree(child.pid);
|
|
@@ -2193,6 +2374,8 @@ export class PiRpcRuntime {
|
|
|
2193
2374
|
}
|
|
2194
2375
|
if (child.exitCode === null)
|
|
2195
2376
|
await new Promise((resolve) => { child.once("exit", resolve); setTimeout(resolve, 5000); });
|
|
2377
|
+
if (process.env.SUPER_PI_TRACE_LATENCY === "1")
|
|
2378
|
+
console.warn(`[super-pi latency] catalog key=${key} phase=catalog.kill.end elapsedMs=${Math.round(performance.now() - killedAt)}`);
|
|
2196
2379
|
}
|
|
2197
2380
|
}
|
|
2198
2381
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evandu/super-pi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.98",
|
|
4
4
|
"description": "Web control plane and companion for Pi",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -36,8 +36,7 @@
|
|
|
36
36
|
"start": "node dist/server/index.js",
|
|
37
37
|
"restart:server": "node scripts/restart-server.mjs",
|
|
38
38
|
"smoke": "npm run build && node scripts/smoke.mjs",
|
|
39
|
-
"check": "node scripts/run-check.mjs"
|
|
40
|
-
"postinstall": "node scripts/patch-brace-expansion.mjs"
|
|
39
|
+
"check": "node scripts/run-check.mjs"
|
|
41
40
|
},
|
|
42
41
|
"check:logic": [
|
|
43
42
|
"scripts/stream-checkpoint.test.mjs",
|