@evandu/super-pi 0.1.99 → 0.1.100

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.
@@ -24,5 +24,5 @@
24
24
  "src/client/types.ts": "6487df00c164370550b5977f9b23b17e22aff4bfb59dfe1e52200042340e3769",
25
25
  "src/client/vite-env.d.ts": "65996936fbb042915f7b74a200fcdde7e410f32a669b1ab9597cfaa4b0faddb5"
26
26
  },
27
- "recordedAt": "2026-10-21T05:09:03.121Z"
27
+ "recordedAt": "2026-10-21T05:29:09.656Z"
28
28
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evandu/super-pi",
3
- "version": "0.1.99",
3
+ "version": "0.1.100",
4
4
  "description": "Web control plane and companion for Pi",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -121,6 +121,44 @@ export async function diagnose({ repositoryRoot, turns = 2, onProgress = () => {
121
121
  result.catalogSource = catalog.catalog?.source ?? catalog.source ?? "unknown";
122
122
  result.models = (catalog.catalog?.models ?? catalog.models ?? []).length;
123
123
 
124
+ // Span 1b: THE PICKER AFTER A WORKSPACE DEFAULT IS SET.
125
+ //
126
+ // THIS IS THE PATH THE FIRST MEASUREMENT MISSED, and it is the one most people are actually on.
127
+ // `createConversation` copies the workspace's default model onto the new conversation, and it
128
+ // VALIDATES that default against the catalog first - so creating a conversation with a default
129
+ // model READS THE CATALOG, and a picker opened afterwards asks for the LEVELS of that model, which
130
+ // is a different catalog key. A measurement that never sets a default model never sees any of it.
131
+ //
132
+ // The report says which path it measured, because a number for a path nobody is on is worse than
133
+ // no number at all.
134
+ const defaultModel = (catalog.catalog?.models ?? catalog.models ?? []).find((m) => m.modelId)
135
+ ?? (catalog.catalog?.defaultModel ?? catalog.defaultModel);
136
+ if (defaultModel) {
137
+ onProgress("setting a workspace default model, then creating a conversation...");
138
+ send({
139
+ type: "workspace.settings",
140
+ workspaceId: "workspace_default",
141
+ model: { provider: defaultModel.provider, modelId: defaultModel.modelId },
142
+ thinkingLevel: null,
143
+ });
144
+ await sleep(400);
145
+ const withDefaultStart = Date.now();
146
+ send({ type: "conversation.create", workspaceId: "workspace_default" });
147
+ const withDefault = await waitFor((e) => e.type === "state"
148
+ && e.state.conversations.length >= 2, "a conversation with the workspace default");
149
+ result.conversationWithDefaultMs = Date.now() - withDefaultStart;
150
+ const withDefaultConversation = events.filter((e) => e.type === "state").at(-1).state.conversations[0];
151
+ // And the picker for THAT conversation, which is the one a person actually opens.
152
+ const defaultPickerStart = Date.now();
153
+ send({ type: "catalog.fetch", conversationId: withDefaultConversation.id });
154
+ // The conversation id is a SIBLING of the catalog, not inside it - `broadcastCatalog` sends
155
+ // `{ type, catalog, conversationId }` - and reading it from the wrong place times out on a
156
+ // catalog that has already arrived.
157
+ await waitFor((e) => e.type === "model.catalog" && e.conversationId === withDefaultConversation.id,
158
+ "the catalog for the conversation with a default model");
159
+ result.pickerWithDefaultMs = Date.now() - defaultPickerStart;
160
+ }
161
+
124
162
  // Spans 2 and 3, per turn.
125
163
  result.turns = [];
126
164
  for (let turn = 1; turn <= turns; turn += 1) {
@@ -153,6 +191,65 @@ export async function diagnose({ repositoryRoot, turns = 2, onProgress = () => {
153
191
  reused: (added.match(/phase=session\.reused/g) ?? []).length,
154
192
  });
155
193
  }
194
+ // Span 4: THE INTERRUPT.
195
+ //
196
+ // The second thing people report is "打断并回答 → 开始回答还要好十秒", and it is a DIFFERENT
197
+ // chain from sending a message: it waits for the running turn to unwind, waits for its slot, and
198
+ // only then asks the model. Those first two waits are Super Pi's, and they are the ones that can
199
+ // be slow; the model's share is the same as any other turn.
200
+ //
201
+ // So it is measured as its own span, with a tool running so there is something to interrupt.
202
+ onProgress("measuring the interrupt...");
203
+ const interruptMark = events.length;
204
+ const interruptLogMark = log.length;
205
+ send({
206
+ type: "message.send",
207
+ conversationId: conversation.id,
208
+ content: '用 bash 运行:node -e "setTimeout(()=>{},20000)"。等它跑完再回复。',
209
+ });
210
+ let toolStarted = false;
211
+ const toolDeadline = Date.now() + 60000;
212
+ while (!toolStarted && Date.now() < toolDeadline) {
213
+ toolStarted = events.slice(interruptMark).some((e) => e.type === "message.tools");
214
+ if (!toolStarted) await sleep(30);
215
+ }
216
+ if (toolStarted) {
217
+ // Queue the message that the interrupt will answer.
218
+ send({ type: "message.send", conversationId: conversation.id, content: "被打断的那条:只回答「第二」两个字。" });
219
+ let queued = null;
220
+ const queueDeadline = Date.now() + 60000;
221
+ while (!queued && Date.now() < queueDeadline) {
222
+ queued = events.filter((e) => e.type === "state").at(-1)?.state?.conversations
223
+ ?.find((c) => c.id === conversation.id)?.messages
224
+ ?.find((m) => m.content?.includes("被打断的那条") && m.delivery === "waiting");
225
+ if (!queued) await sleep(30);
226
+ }
227
+ if (queued) {
228
+ await sleep(800);
229
+ const clicked = Date.now();
230
+ send({ type: "message.interrupt", conversationId: conversation.id, messageId: queued.id });
231
+ let interruptWaiting = 0;
232
+ try {
233
+ await waitFor((e, i) => i >= interruptMark && e.type === "state"
234
+ && e.state.conversations.find((c) => c.id === conversation.id)?.status === "waiting"
235
+ && Date.now() > clicked, "the interrupted turn to report waiting", 60000);
236
+ interruptWaiting = Date.now() - clicked;
237
+ await waitFor((e, i) => i >= interruptMark && e.type === "message.delta" && e.conversationId === conversation.id
238
+ && Date.now() > clicked + 50, "the interrupted turn's first character", 180000);
239
+ } catch { /* reported as not measured below */ }
240
+ const interruptText = Date.now() - clicked;
241
+ const interruptLog = log.slice(interruptLogMark);
242
+ result.interrupt = {
243
+ toWaitingMs: interruptWaiting || undefined,
244
+ toTextMs: interruptText,
245
+ modelMs: interruptText - (interruptWaiting || 0),
246
+ spawned: (interruptLog.match(/phase=pi\.spawned/g) ?? []).length,
247
+ built: (interruptLog.match(/phase=session\.build\.begin/g) ?? []).length,
248
+ reused: (interruptLog.match(/phase=session\.reused/g) ?? []).length,
249
+ };
250
+ }
251
+ }
252
+
156
253
  socket.close();
157
254
  } catch (error) {
158
255
  result.error = error instanceof Error ? error.message : String(error);
@@ -185,10 +282,27 @@ export function renderReport(result) {
185
282
  lines.push("");
186
283
  lines.push(" 每一段耗时 耗时 归属");
187
284
  lines.push(` 1. picker 有数据 ${ms(result.pickerMs)} Super Pi (catalog 读取)`);
285
+ if (typeof result.conversationWithDefaultMs === "number") {
286
+ lines.push(` 1b. 建会话(工作区有默认模型) ${ms(result.conversationWithDefaultMs)} Super Pi (建会话要校验默认模型,会再读一次 catalog)`);
287
+ lines.push(` 1c. 该会话的 picker ${ms(result.pickerWithDefaultMs)} Super Pi (带默认模型时读的是另一份 catalog)`);
288
+ } else {
289
+ lines.push(" 1b/1c. 未测:这次没有可用的模型来设工作区默认模型");
290
+ }
188
291
  for (const turn of result.turns ?? []) {
189
292
  lines.push(` 第${turn.turn}轮 发送 -> 等待中 ${ms(turn.toWaitingMs)} Super Pi (建会话 + 首个 prompt)`);
190
293
  lines.push(` 第${turn.turn}轮 等待中 -> 出字 ${ms(turn.modelMs)} 模型 (不是 Super Pi)`);
191
294
  }
295
+ if (result.interrupt) {
296
+ lines.push("");
297
+ lines.push(" 打断(点「打断并回答」之后):");
298
+ lines.push(` 点击 -> 新一轮报等待中 ${ms(result.interrupt.toWaitingMs)} Super Pi (等旧轮退完 + 等槽位)`);
299
+ lines.push(` 点击 -> 出字 ${ms(result.interrupt.toTextMs)} 整条链`);
300
+ lines.push(` 其中模型 ${ms(result.interrupt.modelMs)} 模型 (不是 Super Pi)`);
301
+ lines.push(` 打断后启动的 Pi 进程数: ${result.interrupt.spawned}`);
302
+ } else {
303
+ lines.push("");
304
+ lines.push(" 打断:未测(需要一次正在跑工具的轮次)");
305
+ }
192
306
  lines.push("");
193
307
  lines.push(` catalog: ${result.catalogSource},${result.models} 个模型`);
194
308