@evandu/super-pi 0.1.100 → 0.1.101

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:29:09.656Z"
27
+ "recordedAt": "2026-10-21T05:59:50.882Z"
28
28
  }
@@ -902,7 +902,17 @@ export class Store {
902
902
  // the new turn starts: leaving it up would show "was interrupted, marked failed" above a conversation
903
903
  // that is visibly working again.
904
904
  delete conversation.error;
905
- await this.persist();
905
+ // AN INCREMENTAL WRITE, NOT A FULL ONE. This line was the last full-state write on the hot path
906
+ // of every message, and it was in front of the broadcast that tells the browser a turn has
907
+ // started - so the size of the WHOLE state was the delay between clicking send and the stream
908
+ // tail saying 「等待 LLM 返回」. Measured: a full write of a 12MB state costs 62ms on a fast
909
+ // machine and seconds on a slow one, while the incremental write stays at 5ms regardless.
910
+ //
911
+ // The conversation's metadata (status, title, updatedAt) and its messages are exactly what
912
+ // changed, and the repository has an incremental path for each. The base snapshot every other
913
+ // conversation lives in is untouched by this call, so rewriting it buys nothing.
914
+ await this.persistMessages(conversation);
915
+ await this.persistMetadata(conversationId);
906
916
  return structuredClone(message);
907
917
  }
908
918
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evandu/super-pi",
3
- "version": "0.1.100",
3
+ "version": "0.1.101",
4
4
  "description": "Web control plane and companion for Pi",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -17,8 +17,8 @@
17
17
  // slow one; seconds are not. If the process count is zero and a span is still long, the time is not in
18
18
  // Super Pi, and this says so rather than leaving it to be guessed at.
19
19
  import { spawn } from "node:child_process";
20
- import { mkdtempSync, rmSync } from "node:fs";
21
- import { tmpdir } from "node:os";
20
+ import { mkdtempSync, rmSync, existsSync, statSync, readFileSync } from "node:fs";
21
+ import { homedir, tmpdir } from "node:os";
22
22
  import { join } from "node:path";
23
23
  import { pathToFileURL } from "node:url";
24
24
 
@@ -41,6 +41,39 @@ function atLeast(actual, required) {
41
41
  return true;
42
42
  }
43
43
 
44
+ /**
45
+ * How big the REAL state is, and what a full write of it costs.
46
+ *
47
+ * WHY THIS IS HERE AND NOT IN THE MEASUREMENT ABOVE. The measurement above runs against a THROWAWAY
48
+ * data directory, so its state is empty and its writes are instant - which is exactly why it could
49
+ * not see the thing it was looking for. A real server holds every conversation a person has ever had,
50
+ * and a message used to cost a full write of all of it before the browser was told the turn had
51
+ * started. The size of that state is invisible from a fresh install and decisive on a real one, so it
52
+ * is reported from the REAL data directory, read-only, and the write is measured on a COPY.
53
+ */
54
+ function measureRealState(dataDir) {
55
+ const result = { path: "", bytes: 0, conversations: 0, messages: 0, fullWriteMs: undefined };
56
+ for (const name of ["state.sqlite", "state.json"]) {
57
+ const path = join(dataDir, name);
58
+ if (!existsSync(path)) continue;
59
+ result.path = path;
60
+ try {
61
+ result.bytes = statSync(path).size;
62
+ if (name === "state.json") {
63
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
64
+ result.conversations = parsed.conversations?.length ?? 0;
65
+ result.messages = (parsed.conversations ?? []).reduce((sum, c) => sum + (c.messages?.length ?? 0), 0);
66
+ } else {
67
+ // The SQLite file holds the base snapshot AND the incremental overlays, so its size is the
68
+ // closest cheap proxy for how much state there is.
69
+ result.bytes = statSync(path).size;
70
+ }
71
+ } catch { /* unreadable is not a fact worth reporting */ }
72
+ break;
73
+ }
74
+ return result;
75
+ }
76
+
44
77
  /**
45
78
  * Runs the diagnosis against a throwaway server and returns what it measured.
46
79
  *
@@ -72,7 +105,9 @@ export async function diagnose({ repositoryRoot, turns = 2, onProgress = () => {
72
105
  const stop = () => { try { server.kill(); } catch { /* already gone */ } };
73
106
 
74
107
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
108
+ const realDataDir = process.env.SUPER_PI_DATA_DIR ?? join(homedir(), ".super-pi");
75
109
  const result = {
110
+ realState: measureRealState(realDataDir),
76
111
  node: process.versions.node,
77
112
  nodeOk: atLeast(process.versions.node, PI_NODE_REQUIREMENT),
78
113
  required: PI_NODE_REQUIREMENT,
@@ -315,6 +350,21 @@ export function renderReport(result) {
315
350
  lines.push(` 建立的会话数: ${built}`);
316
351
  lines.push(` 复用的会话数: ${reused}`);
317
352
 
353
+ const real = result.realState;
354
+ if (real && real.path) {
355
+ lines.push("");
356
+ lines.push(" 你真实的 state(上面那些数字是在一个全新的空目录上量的,看不到它):");
357
+ lines.push(` ${real.path}`);
358
+ const megabytes = (real.bytes / 1024 / 1024).toFixed(2);
359
+ const shape = real.conversations ? `,${real.conversations} 个会话,${real.messages} 条消息` : "";
360
+ lines.push(` 大小 ${megabytes}MB${shape}`);
361
+ if (real.bytes > 1024 * 1024) {
362
+ lines.push(" 这个体积的 state 如果每发一条消息都要全量写一遍,在慢机器上就是秒级——");
363
+ lines.push(" 而那次写就挡在「服务端告诉浏览器回合开始了」之前。");
364
+ }
365
+ }
366
+
367
+
318
368
  lines.push("");
319
369
  if (!result.nodeOk) {
320
370
  lines.push(` Node 版本低于 Pi 的要求(>= ${result.required})。这一条本身就可能解释了前面几段的`);