wiki-viewer 1.5.2 → 1.6.0
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/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/app-proxy/[...path]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/browse/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/app/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/upload/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0pqaawz._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/middleware-manifest.json +1 -1
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/static/chunks/{17~r3.8awh3qg.js → 0-qve5g~myvo-.js} +1 -1
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/packages/wiki-viewer-mcp/bench-e2e.mjs +166 -0
- package/.next/standalone/packages/wiki-viewer-mcp/bench-latency.mjs +87 -0
- package/.next/standalone/packages/wiki-viewer-mcp/src/index.ts +28 -0
- package/.next/standalone/src/app/page.tsx +11 -1
- package/.next/standalone/src/tests/proof/bench-write.ts +179 -0
- package/.next/standalone/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- /package/.next/standalone/.next/static/{_avhhNXTnEvzPnQc1sIDF → HeeYGbBsAF0r0T0mY60Mo}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{_avhhNXTnEvzPnQc1sIDF → HeeYGbBsAF0r0T0mY60Mo}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{_avhhNXTnEvzPnQc1sIDF → HeeYGbBsAF0r0T0mY60Mo}/_ssgManifest.js +0 -0
|
@@ -29,6 +29,33 @@ import * as z from "zod";
|
|
|
29
29
|
import { parseArgs } from "node:util";
|
|
30
30
|
import { WikiViewerClient, IfMatchError, CollabActiveError, WikiViewerError } from "./http-client.js";
|
|
31
31
|
import * as stateCache from "./state-cache.js";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Enable pooled HTTP keep-alive for the global fetch the client uses.
|
|
35
|
+
*
|
|
36
|
+
* Without this, every tool call opens a fresh connection and pays a full
|
|
37
|
+
* TCP + TLS handshake (~3 RTT) before the request even goes out. Over a WAN
|
|
38
|
+
* link that turns a sub-100ms write into 300ms-1s+, and multiplies across the
|
|
39
|
+
* GET+PUT an edit performs. Reusing connections removes that per-call penalty.
|
|
40
|
+
*
|
|
41
|
+
* Wrapped in try/catch + dynamic import so a missing/edge undici never breaks
|
|
42
|
+
* startup; it just falls back to default (non-pooled) fetch.
|
|
43
|
+
*/
|
|
44
|
+
async function enableKeepAlive(): Promise<void> {
|
|
45
|
+
try {
|
|
46
|
+
const { Agent, setGlobalDispatcher } = await import("undici");
|
|
47
|
+
setGlobalDispatcher(
|
|
48
|
+
new Agent({
|
|
49
|
+
keepAliveTimeout: 30_000,
|
|
50
|
+
keepAliveMaxTimeout: 60_000,
|
|
51
|
+
connections: 16,
|
|
52
|
+
pipelining: 1,
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
} catch {
|
|
56
|
+
// undici unavailable — default fetch still works, just without pooling.
|
|
57
|
+
}
|
|
58
|
+
}
|
|
32
59
|
import {
|
|
33
60
|
register,
|
|
34
61
|
RegisterScope,
|
|
@@ -433,6 +460,7 @@ function buildJsonSchema(val: AnyZodVal): Record<string, unknown> {
|
|
|
433
460
|
// ─── Entrypoints ─────────────────────────────────────────────────────────────
|
|
434
461
|
|
|
435
462
|
async function main() {
|
|
463
|
+
await enableKeepAlive();
|
|
436
464
|
const client = createClient();
|
|
437
465
|
const server = createServer(client);
|
|
438
466
|
const transport = new StdioServerTransport();
|
|
@@ -272,6 +272,7 @@ export default function Page() {
|
|
|
272
272
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
|
273
273
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
274
274
|
const [fileContent, setFileContent] = useState<string | null>(null);
|
|
275
|
+
const [fileRevision, setFileRevision] = useState(0);
|
|
275
276
|
const [fileLoading, setFileLoading] = useState(false);
|
|
276
277
|
const [editing, setEditing] = useState(false);
|
|
277
278
|
const [editContent, setEditContent] = useState("");
|
|
@@ -411,6 +412,7 @@ export default function Page() {
|
|
|
411
412
|
if (res.ok) {
|
|
412
413
|
const d: { content: string } = await res.json();
|
|
413
414
|
setFileContent(d.content);
|
|
415
|
+
setFileRevision(Number(res.headers.get("X-Wiki-Revision") ?? 0));
|
|
414
416
|
}
|
|
415
417
|
} catch {
|
|
416
418
|
/* ignore */
|
|
@@ -536,6 +538,7 @@ export default function Page() {
|
|
|
536
538
|
setEditing(false);
|
|
537
539
|
setSaveError(null);
|
|
538
540
|
setFileContent(null);
|
|
541
|
+
setFileRevision(0);
|
|
539
542
|
const kind = viewerKindFor(node.name, node.type);
|
|
540
543
|
if (!["editor", "text"].includes(kind) && !isText(node.name)) return;
|
|
541
544
|
setFileLoading(true);
|
|
@@ -546,6 +549,7 @@ export default function Page() {
|
|
|
546
549
|
if (res.ok) {
|
|
547
550
|
const d: { content: string } = await res.json();
|
|
548
551
|
setFileContent(d.content);
|
|
552
|
+
setFileRevision(Number(res.headers.get("X-Wiki-Revision") ?? 0));
|
|
549
553
|
}
|
|
550
554
|
} catch {
|
|
551
555
|
/* ignore */
|
|
@@ -624,9 +628,15 @@ export default function Page() {
|
|
|
624
628
|
const res = await fetch("/api/wiki/content", {
|
|
625
629
|
method: "PUT",
|
|
626
630
|
headers: { "Content-Type": "application/json" },
|
|
627
|
-
body: JSON.stringify({
|
|
631
|
+
body: JSON.stringify({
|
|
632
|
+
path: openFile.path,
|
|
633
|
+
content: editContent,
|
|
634
|
+
baseRevision: fileRevision,
|
|
635
|
+
}),
|
|
628
636
|
});
|
|
629
637
|
if (res.ok) {
|
|
638
|
+
const d: { revision?: number } = await res.json();
|
|
639
|
+
if (typeof d.revision === "number") setFileRevision(d.revision);
|
|
630
640
|
setFileContent(editContent);
|
|
631
641
|
setEditing(false);
|
|
632
642
|
} else {
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Isolated write-path benchmark — NOT a test, NOT touching production state.
|
|
3
|
+
*
|
|
4
|
+
* Drives the real Tier-1 route handlers (filePUT / fileGET) against a throwaway
|
|
5
|
+
* temp rootDir + temp HOME (lock dir), exactly like agent-fs.test.ts, and
|
|
6
|
+
* measures end-to-end server-side latency for the write path:
|
|
7
|
+
* - non-.md overwrite (raw, no lock/sidecar)
|
|
8
|
+
* - .md overwrite (cross-proc lock + reconcile + sidecar write + datasync)
|
|
9
|
+
* - edit_file equivalent (GET + PUT round-trip on .md)
|
|
10
|
+
*
|
|
11
|
+
* Run: npx tsx src/tests/proof/bench-write.ts
|
|
12
|
+
* Optional env: BENCH_ITERS (default 300), BENCH_WARMUP (default 30)
|
|
13
|
+
*/
|
|
14
|
+
import { mkdtemp, writeFile, rm } from "node:fs/promises";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
18
|
+
|
|
19
|
+
import { setRootDir } from "../../lib/root-dir.js";
|
|
20
|
+
import { ensureRegistry, addAgent, hashToken } from "../../lib/proof/registry.js";
|
|
21
|
+
|
|
22
|
+
const ITERS = Number(process.env.BENCH_ITERS) || 300;
|
|
23
|
+
const WARMUP = Number(process.env.BENCH_WARMUP) || 30;
|
|
24
|
+
|
|
25
|
+
function sha256(buf: Buffer): string {
|
|
26
|
+
return "sha256:" + createHash("sha256").update(buf).digest("hex");
|
|
27
|
+
}
|
|
28
|
+
function hdrs(token: string, id: string): Record<string, string> {
|
|
29
|
+
return { Authorization: `Bearer ${token}`, "X-Agent-Id": id };
|
|
30
|
+
}
|
|
31
|
+
function fileUrl(rel: string, qs = ""): string {
|
|
32
|
+
return `http://localhost/api/agent/fs/file/${rel}${qs}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function pct(sorted: number[], p: number): number {
|
|
36
|
+
const i = Math.min(sorted.length - 1, Math.floor((p / 100) * sorted.length));
|
|
37
|
+
return sorted[i];
|
|
38
|
+
}
|
|
39
|
+
function stats(label: string, samples: number[]) {
|
|
40
|
+
const s = [...samples].sort((a, b) => a - b);
|
|
41
|
+
const sum = s.reduce((a, b) => a + b, 0);
|
|
42
|
+
const mean = sum / s.length;
|
|
43
|
+
return {
|
|
44
|
+
label,
|
|
45
|
+
n: s.length,
|
|
46
|
+
mean: +mean.toFixed(3),
|
|
47
|
+
p50: +pct(s, 50).toFixed(3),
|
|
48
|
+
p95: +pct(s, 95).toFixed(3),
|
|
49
|
+
p99: +pct(s, 99).toFixed(3),
|
|
50
|
+
min: +s[0].toFixed(3),
|
|
51
|
+
max: +s[s.length - 1].toFixed(3),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function timeIt(fn: () => Promise<void>): Promise<number> {
|
|
56
|
+
const t0 = performance.now();
|
|
57
|
+
await fn();
|
|
58
|
+
return performance.now() - t0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function main() {
|
|
62
|
+
const tmpHome = await mkdtemp(path.join(tmpdir(), "bench-home-"));
|
|
63
|
+
const tmpRoot = await mkdtemp(path.join(tmpdir(), "bench-root-"));
|
|
64
|
+
process.env.HOME = tmpHome;
|
|
65
|
+
setRootDir(tmpRoot);
|
|
66
|
+
await ensureRegistry();
|
|
67
|
+
|
|
68
|
+
const TOKEN = randomBytes(32).toString("hex");
|
|
69
|
+
await addAgent({
|
|
70
|
+
id: "ai:bench",
|
|
71
|
+
displayName: "Bench",
|
|
72
|
+
tokenHash: hashToken(TOKEN),
|
|
73
|
+
scope: { paths: ["**/*"], ops: ["read", "mutate", "delete"] },
|
|
74
|
+
createdAt: new Date().toISOString(),
|
|
75
|
+
lastSeen: new Date().toISOString(),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const fileRoute = await import("../../app/api/agent/fs/file/[...path]/route.js");
|
|
79
|
+
const filePUT = fileRoute.PUT;
|
|
80
|
+
const fileGET = fileRoute.GET;
|
|
81
|
+
const H = hdrs(TOKEN, "ai:bench");
|
|
82
|
+
|
|
83
|
+
const ctx = (rel: string) => ({ params: Promise.resolve({ path: rel.split("/") }) });
|
|
84
|
+
|
|
85
|
+
// Seed bodies (~1KB, realistic small doc)
|
|
86
|
+
const body = Buffer.from("# Doc\n\n" + "lorem ipsum dolor sit amet. ".repeat(40));
|
|
87
|
+
const txtBody = Buffer.from("config_value = " + "x".repeat(900));
|
|
88
|
+
|
|
89
|
+
async function putOnce(rel: string, data: Buffer, ifMatch?: string, ifCollab?: string): Promise<Response> {
|
|
90
|
+
const headers: Record<string, string> = { ...H, "Content-Type": "application/octet-stream" };
|
|
91
|
+
if (ifMatch) headers["If-Match"] = ifMatch;
|
|
92
|
+
if (ifCollab) headers["If-Collab-Match"] = ifCollab;
|
|
93
|
+
return filePUT(new Request(fileUrl(rel), { method: "PUT", headers, body: new Uint8Array(data) }), ctx(rel));
|
|
94
|
+
}
|
|
95
|
+
async function getSha(rel: string): Promise<{ sha: string; collabRev: string | null }> {
|
|
96
|
+
const res = await fileGET(new Request(fileUrl(rel), { headers: H }), ctx(rel));
|
|
97
|
+
const etag = (res.headers.get("ETag") ?? "").replace(/"/g, "");
|
|
98
|
+
return { sha: etag, collabRev: res.headers.get("X-Collab-Revision") };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Scenario A: non-.md overwrite (lean path) ──────────────────────────────
|
|
102
|
+
await putOnce("bench.txt", txtBody, undefined); // create
|
|
103
|
+
const txtSamples: number[] = [];
|
|
104
|
+
{
|
|
105
|
+
let sha = (await getSha("bench.txt")).sha;
|
|
106
|
+
for (let i = 0; i < WARMUP + ITERS; i++) {
|
|
107
|
+
const data = Buffer.from(txtBody.toString() + i);
|
|
108
|
+
const t = await timeIt(async () => {
|
|
109
|
+
const res = await putOnce("bench.txt", data, sha);
|
|
110
|
+
if (res.status !== 200) throw new Error("txt PUT " + res.status);
|
|
111
|
+
sha = (await res.json() as { sha256: string }).sha256;
|
|
112
|
+
});
|
|
113
|
+
if (i >= WARMUP) txtSamples.push(t);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Scenario B: .md overwrite (lock + reconcile + sidecar + datasync) ──────
|
|
118
|
+
await putOnce("bench.md", body, undefined); // create
|
|
119
|
+
const mdSamples: number[] = [];
|
|
120
|
+
{
|
|
121
|
+
let sha = (await getSha("bench.md")).sha;
|
|
122
|
+
for (let i = 0; i < WARMUP + ITERS; i++) {
|
|
123
|
+
const data = Buffer.from(body.toString() + "\n\nedit " + i);
|
|
124
|
+
const t = await timeIt(async () => {
|
|
125
|
+
const res = await putOnce("bench.md", data, sha);
|
|
126
|
+
if (res.status !== 200) throw new Error("md PUT " + res.status + " " + await res.text());
|
|
127
|
+
sha = (await res.json() as { sha256: string }).sha256;
|
|
128
|
+
});
|
|
129
|
+
if (i >= WARMUP) mdSamples.push(t);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── Scenario C: edit_file equivalent — GET + PUT round-trip on .md ─────────
|
|
134
|
+
const editSamples: number[] = [];
|
|
135
|
+
{
|
|
136
|
+
for (let i = 0; i < WARMUP + ITERS; i++) {
|
|
137
|
+
const data = Buffer.from(body.toString() + "\n\nrt " + i);
|
|
138
|
+
const t = await timeIt(async () => {
|
|
139
|
+
const { sha } = await getSha("bench.md");
|
|
140
|
+
const res = await putOnce("bench.md", data, sha);
|
|
141
|
+
if (res.status !== 200) throw new Error("edit PUT " + res.status);
|
|
142
|
+
await res.json();
|
|
143
|
+
});
|
|
144
|
+
if (i >= WARMUP) editSamples.push(t);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const results = [
|
|
149
|
+
stats("non-.md overwrite (lean)", txtSamples),
|
|
150
|
+
stats(".md overwrite (lock+reconcile+sidecar+datasync)", mdSamples),
|
|
151
|
+
stats("edit_file (GET+PUT .md round-trip)", editSamples),
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
console.log(`\nwrite-path benchmark (iters=${ITERS}, warmup=${WARMUP}, ~1KB bodies)\n`);
|
|
155
|
+
console.log(
|
|
156
|
+
["scenario".padEnd(48), "p50", "p95", "p99", "mean", "max"].join(" "),
|
|
157
|
+
);
|
|
158
|
+
for (const r of results) {
|
|
159
|
+
console.log(
|
|
160
|
+
[
|
|
161
|
+
r.label.padEnd(48),
|
|
162
|
+
`${r.p50}ms`.padStart(7),
|
|
163
|
+
`${r.p95}ms`.padStart(7),
|
|
164
|
+
`${r.p99}ms`.padStart(7),
|
|
165
|
+
`${r.mean}ms`.padStart(7),
|
|
166
|
+
`${r.max}ms`.padStart(7),
|
|
167
|
+
].join(" "),
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
console.log("");
|
|
171
|
+
|
|
172
|
+
await rm(tmpHome, { recursive: true, force: true });
|
|
173
|
+
await rm(tmpRoot, { recursive: true, force: true });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
main().catch((e) => {
|
|
177
|
+
console.error(e);
|
|
178
|
+
process.exit(1);
|
|
179
|
+
});
|