@celestea/studio 2.7.1

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.
Files changed (240) hide show
  1. package/LICENSE +21 -0
  2. package/dist/app.d.ts +111 -0
  3. package/dist/app.js +219 -0
  4. package/dist/auth/api-token.d.ts +76 -0
  5. package/dist/auth/api-token.js +172 -0
  6. package/dist/auth/htpasswd.d.ts +22 -0
  7. package/dist/auth/htpasswd.js +61 -0
  8. package/dist/auth/index.d.ts +14 -0
  9. package/dist/auth/index.js +14 -0
  10. package/dist/auth/page.d.ts +19 -0
  11. package/dist/auth/page.js +73 -0
  12. package/dist/auth/rate-limit.d.ts +27 -0
  13. package/dist/auth/rate-limit.js +47 -0
  14. package/dist/auth/token.d.ts +49 -0
  15. package/dist/auth/token.js +112 -0
  16. package/dist/config.d.ts +78 -0
  17. package/dist/config.js +72 -0
  18. package/dist/deployment.d.ts +92 -0
  19. package/dist/deployment.js +154 -0
  20. package/dist/fake-runtime-adapter.d.ts +37 -0
  21. package/dist/fake-runtime-adapter.js +276 -0
  22. package/dist/handlers/auth.d.ts +24 -0
  23. package/dist/handlers/auth.js +155 -0
  24. package/dist/handlers/common.d.ts +76 -0
  25. package/dist/handlers/common.js +127 -0
  26. package/dist/handlers/config-shape.d.ts +76 -0
  27. package/dist/handlers/config-shape.js +227 -0
  28. package/dist/handlers/config.d.ts +26 -0
  29. package/dist/handlers/config.js +161 -0
  30. package/dist/handlers/context-shape.d.ts +33 -0
  31. package/dist/handlers/context-shape.js +59 -0
  32. package/dist/handlers/dialog.d.ts +22 -0
  33. package/dist/handlers/dialog.js +292 -0
  34. package/dist/handlers/exec.d.ts +22 -0
  35. package/dist/handlers/exec.js +131 -0
  36. package/dist/handlers/fs-read.d.ts +63 -0
  37. package/dist/handlers/fs-read.js +159 -0
  38. package/dist/handlers/fs.d.ts +57 -0
  39. package/dist/handlers/fs.js +153 -0
  40. package/dist/handlers/grants-shape.d.ts +45 -0
  41. package/dist/handlers/grants-shape.js +130 -0
  42. package/dist/handlers/grants.d.ts +21 -0
  43. package/dist/handlers/grants.js +267 -0
  44. package/dist/handlers/health.d.ts +37 -0
  45. package/dist/handlers/health.js +139 -0
  46. package/dist/handlers/index.d.ts +40 -0
  47. package/dist/handlers/index.js +85 -0
  48. package/dist/handlers/permissions.d.ts +9 -0
  49. package/dist/handlers/permissions.js +183 -0
  50. package/dist/handlers/plugins.d.ts +18 -0
  51. package/dist/handlers/plugins.js +23 -0
  52. package/dist/handlers/prompts.d.ts +12 -0
  53. package/dist/handlers/prompts.js +120 -0
  54. package/dist/handlers/providers.d.ts +12 -0
  55. package/dist/handlers/providers.js +178 -0
  56. package/dist/handlers/questions.d.ts +23 -0
  57. package/dist/handlers/questions.js +115 -0
  58. package/dist/handlers/session-model.d.ts +38 -0
  59. package/dist/handlers/session-model.js +105 -0
  60. package/dist/handlers/session-move.d.ts +22 -0
  61. package/dist/handlers/session-move.js +188 -0
  62. package/dist/handlers/session-tools.d.ts +19 -0
  63. package/dist/handlers/session-tools.js +75 -0
  64. package/dist/handlers/sessions.d.ts +32 -0
  65. package/dist/handlers/sessions.js +242 -0
  66. package/dist/handlers/usage.d.ts +26 -0
  67. package/dist/handlers/usage.js +77 -0
  68. package/dist/handlers/worker.d.ts +12 -0
  69. package/dist/handlers/worker.js +82 -0
  70. package/dist/handlers/workspaces.d.ts +12 -0
  71. package/dist/handlers/workspaces.js +88 -0
  72. package/dist/harness.test-util.d.ts +101 -0
  73. package/dist/harness.test-util.js +176 -0
  74. package/dist/index.d.ts +33 -0
  75. package/dist/index.js +33 -0
  76. package/dist/main.d.ts +13 -0
  77. package/dist/main.js +35 -0
  78. package/dist/plugins.d.ts +105 -0
  79. package/dist/plugins.js +97 -0
  80. package/dist/question-registry.d.ts +124 -0
  81. package/dist/question-registry.js +212 -0
  82. package/dist/question-rows.d.ts +32 -0
  83. package/dist/question-rows.js +43 -0
  84. package/dist/replay/compare.d.ts +47 -0
  85. package/dist/replay/compare.js +89 -0
  86. package/dist/replay/e2e-replay.d.ts +46 -0
  87. package/dist/replay/e2e-replay.js +91 -0
  88. package/dist/replay/expect-compact.d.ts +31 -0
  89. package/dist/replay/expect-compact.js +92 -0
  90. package/dist/replay/fixtures.d.ts +52 -0
  91. package/dist/replay/fixtures.js +53 -0
  92. package/dist/replay/host.d.ts +32 -0
  93. package/dist/replay/host.js +62 -0
  94. package/dist/replay/index.d.ts +16 -0
  95. package/dist/replay/index.js +16 -0
  96. package/dist/replay/probes.d.ts +55 -0
  97. package/dist/replay/probes.js +225 -0
  98. package/dist/replay/report.d.ts +13 -0
  99. package/dist/replay/report.js +103 -0
  100. package/dist/replay/session-e2e.d.ts +44 -0
  101. package/dist/replay/session-e2e.js +91 -0
  102. package/dist/routes.d.ts +47 -0
  103. package/dist/routes.js +59 -0
  104. package/dist/runtime/attachments-llm.d.ts +13 -0
  105. package/dist/runtime/attachments-llm.js +37 -0
  106. package/dist/runtime/boot-recovery.d.ts +32 -0
  107. package/dist/runtime/boot-recovery.js +71 -0
  108. package/dist/runtime/context-snapshot.d.ts +59 -0
  109. package/dist/runtime/context-snapshot.js +113 -0
  110. package/dist/runtime/engine-grants.d.ts +121 -0
  111. package/dist/runtime/engine-grants.js +338 -0
  112. package/dist/runtime/engine-permissions.d.ts +29 -0
  113. package/dist/runtime/engine-permissions.js +92 -0
  114. package/dist/runtime/engine-plugins.d.ts +189 -0
  115. package/dist/runtime/engine-plugins.js +292 -0
  116. package/dist/runtime/engine-profile.d.ts +28 -0
  117. package/dist/runtime/engine-profile.js +88 -0
  118. package/dist/runtime/engine-session.d.ts +9 -0
  119. package/dist/runtime/engine-session.js +9 -0
  120. package/dist/runtime/fallback-contract.d.ts +32 -0
  121. package/dist/runtime/fallback-contract.js +10 -0
  122. package/dist/runtime/fallback-host.d.ts +134 -0
  123. package/dist/runtime/fallback-host.js +299 -0
  124. package/dist/runtime/host-autowake.d.ts +79 -0
  125. package/dist/runtime/host-autowake.js +86 -0
  126. package/dist/runtime/image-downgrade.d.ts +49 -0
  127. package/dist/runtime/image-downgrade.js +90 -0
  128. package/dist/runtime/inbox-message.d.ts +11 -0
  129. package/dist/runtime/inbox-message.js +20 -0
  130. package/dist/runtime/index.d.ts +17 -0
  131. package/dist/runtime/index.js +17 -0
  132. package/dist/runtime/ledger-view.d.ts +40 -0
  133. package/dist/runtime/ledger-view.js +56 -0
  134. package/dist/runtime/llm-assembly.d.ts +66 -0
  135. package/dist/runtime/llm-assembly.js +114 -0
  136. package/dist/runtime/offline-llm.d.ts +71 -0
  137. package/dist/runtime/offline-llm.js +132 -0
  138. package/dist/runtime/provider-target.d.ts +10 -0
  139. package/dist/runtime/provider-target.js +10 -0
  140. package/dist/runtime/question-host.d.ts +33 -0
  141. package/dist/runtime/question-host.js +41 -0
  142. package/dist/runtime/question-view.d.ts +48 -0
  143. package/dist/runtime/question-view.js +76 -0
  144. package/dist/runtime/real-runtime-adapter.d.ts +95 -0
  145. package/dist/runtime/real-runtime-adapter.js +553 -0
  146. package/dist/runtime/recovery-audit.d.ts +81 -0
  147. package/dist/runtime/recovery-audit.js +95 -0
  148. package/dist/runtime/recovery-view.d.ts +36 -0
  149. package/dist/runtime/recovery-view.js +63 -0
  150. package/dist/runtime/session-compose.d.ts +235 -0
  151. package/dist/runtime/session-compose.js +350 -0
  152. package/dist/runtime/session-grants.d.ts +36 -0
  153. package/dist/runtime/session-grants.js +69 -0
  154. package/dist/runtime/session-lifecycle.d.ts +40 -0
  155. package/dist/runtime/session-lifecycle.js +86 -0
  156. package/dist/runtime/session-publisher.d.ts +40 -0
  157. package/dist/runtime/session-publisher.js +48 -0
  158. package/dist/runtime/session-release.d.ts +53 -0
  159. package/dist/runtime/session-release.js +80 -0
  160. package/dist/runtime/test-util.d.ts +78 -0
  161. package/dist/runtime/test-util.js +162 -0
  162. package/dist/runtime/watchdog-view.d.ts +33 -0
  163. package/dist/runtime/watchdog-view.js +45 -0
  164. package/dist/runtime/worker-bridge.d.ts +76 -0
  165. package/dist/runtime/worker-bridge.js +156 -0
  166. package/dist/runtime/worker-live.d.ts +22 -0
  167. package/dist/runtime/worker-live.js +25 -0
  168. package/dist/runtime/worker-recovery.d.ts +35 -0
  169. package/dist/runtime/worker-recovery.js +64 -0
  170. package/dist/runtime/worker-table.d.ts +58 -0
  171. package/dist/runtime/worker-table.js +70 -0
  172. package/dist/runtime-adapter.d.ts +421 -0
  173. package/dist/runtime-adapter.js +76 -0
  174. package/dist/serial-queue.d.ts +16 -0
  175. package/dist/serial-queue.js +22 -0
  176. package/dist/server.d.ts +44 -0
  177. package/dist/server.js +119 -0
  178. package/dist/settings.d.ts +23 -0
  179. package/dist/settings.js +31 -0
  180. package/dist/sse.d.ts +67 -0
  181. package/dist/sse.js +186 -0
  182. package/dist/static.d.ts +20 -0
  183. package/dist/static.js +120 -0
  184. package/dist/store/builtin-sections.d.ts +52 -0
  185. package/dist/store/builtin-sections.js +118 -0
  186. package/dist/store/celestea-home.d.ts +12 -0
  187. package/dist/store/celestea-home.js +11 -0
  188. package/dist/store/fs-json.d.ts +51 -0
  189. package/dist/store/fs-json.js +119 -0
  190. package/dist/store/grants-audit.d.ts +84 -0
  191. package/dist/store/grants-audit.js +109 -0
  192. package/dist/store/grants-service.d.ts +44 -0
  193. package/dist/store/grants-service.js +41 -0
  194. package/dist/store/grants-tokens.d.ts +62 -0
  195. package/dist/store/grants-tokens.js +105 -0
  196. package/dist/store/grants.d.ts +113 -0
  197. package/dist/store/grants.js +238 -0
  198. package/dist/store/index.d.ts +36 -0
  199. package/dist/store/index.js +36 -0
  200. package/dist/store/mode.d.ts +35 -0
  201. package/dist/store/mode.js +41 -0
  202. package/dist/store/permissions.d.ts +61 -0
  203. package/dist/store/permissions.js +139 -0
  204. package/dist/store/prompts-compose.d.ts +56 -0
  205. package/dist/store/prompts-compose.js +62 -0
  206. package/dist/store/prompts-template.d.ts +26 -0
  207. package/dist/store/prompts-template.js +99 -0
  208. package/dist/store/prompts.d.ts +142 -0
  209. package/dist/store/prompts.js +273 -0
  210. package/dist/store/provider-probe.d.ts +81 -0
  211. package/dist/store/provider-probe.js +154 -0
  212. package/dist/store/providers.d.ts +97 -0
  213. package/dist/store/providers.js +219 -0
  214. package/dist/store/result.d.ts +29 -0
  215. package/dist/store/result.js +30 -0
  216. package/dist/store/session-id.d.ts +119 -0
  217. package/dist/store/session-id.js +220 -0
  218. package/dist/store/session-meta.d.ts +41 -0
  219. package/dist/store/session-meta.js +66 -0
  220. package/dist/store/session-ops.d.ts +109 -0
  221. package/dist/store/session-ops.js +264 -0
  222. package/dist/store/session-tools.d.ts +42 -0
  223. package/dist/store/session-tools.js +69 -0
  224. package/dist/store/sessions.d.ts +162 -0
  225. package/dist/store/sessions.js +307 -0
  226. package/dist/store/validate.d.ts +13 -0
  227. package/dist/store/validate.js +38 -0
  228. package/dist/store/workspaces.d.ts +67 -0
  229. package/dist/store/workspaces.js +228 -0
  230. package/dist/user-questions.d.ts +58 -0
  231. package/dist/user-questions.js +157 -0
  232. package/dist/version.d.ts +14 -0
  233. package/dist/version.js +52 -0
  234. package/package.json +38 -0
  235. package/webdist/assets/index-Bngs7gUm.js +98 -0
  236. package/webdist/assets/index-Bu6ci_rN.css +1 -0
  237. package/webdist/assets/katex-CCbS2qAY.js +257 -0
  238. package/webdist/assets/mhchem-CnUN8HwY.js +1 -0
  239. package/webdist/build-meta.json +7 -0
  240. package/webdist/index.html +261 -0
@@ -0,0 +1,92 @@
1
+ /**
2
+ * INDEPENDENT re-derivation of the frozen compaction plan (W259).
3
+ *
4
+ * This is deliberately a SECOND implementation: the runtime's planner lives in
5
+ * `packages/runtime/src/compact/plan.ts` (written as a straight port of
6
+ * `celestea_studio/src/compact.rs`), while this one is written from the spec text
7
+ * with a different shape (index scan + slice, no helper reuse). A P5 comparison
8
+ * between the two is therefore a cross-implementation check of the STRUCTURE
9
+ * (head turn, kept-tail selection, renumbering, dropping an unterminated tail),
10
+ * not a tautology.
11
+ *
12
+ * The summary string is passed in (the replay reads it back out of the compacted
13
+ * log), so the summary itself is out of scope for this comparison.
14
+ */
15
+ export const SPEC_THRESHOLD = 8;
16
+ export const SPEC_KEEP = 4;
17
+ export const SPEC_HEAD_PREFIX = "【上下文压缩】";
18
+ export const SPEC_HEAD_ASSISTANT = "上下文已压缩,以上为历史摘要。";
19
+ /** Index ranges [start, end) of every complete turn (`turn_start` .. `turn_end`). */
20
+ export function completeTurnRanges(events) {
21
+ const ranges = [];
22
+ let start = -1;
23
+ for (let i = 0; i < events.length; i++) {
24
+ const ev = events[i];
25
+ if (ev === undefined)
26
+ continue;
27
+ if (ev.type === "turn_start") {
28
+ start = i;
29
+ continue;
30
+ }
31
+ if (ev.type === "turn_end" && start >= 0) {
32
+ ranges.push([start, i + 1]);
33
+ start = -1;
34
+ }
35
+ }
36
+ return ranges;
37
+ }
38
+ /** The expected post-compaction log, or null when the history is too short. */
39
+ export function expectedCompactLog(events, summary, keep = SPEC_KEEP) {
40
+ const ranges = completeTurnRanges(events);
41
+ if (ranges.length <= SPEC_THRESHOLD)
42
+ return null;
43
+ const kept = ranges.slice(Math.max(0, ranges.length - Math.max(1, Math.min(keep, ranges.length))));
44
+ const out = [
45
+ { type: "turn_start", id: "turn-1" },
46
+ // W888: the head summary row carries origin: "compact" (the runtime planner
47
+ // does too; this second implementation must track the plan shape).
48
+ { type: "user_message", text: `${SPEC_HEAD_PREFIX}${summary}`, origin: "compact" },
49
+ { type: "assistant_message", text: SPEC_HEAD_ASSISTANT },
50
+ { type: "turn_end", id: "turn-1", outcome: "completed" },
51
+ ];
52
+ kept.forEach(([from, to], i) => {
53
+ const id = `turn-${i + 2}`;
54
+ for (const ev of events.slice(from, to)) {
55
+ if (ev.type === "turn_start")
56
+ out.push({ type: "turn_start", id });
57
+ else if (ev.type === "turn_end")
58
+ out.push({ type: "turn_end", id, ...(ev.outcome === undefined ? {} : { outcome: ev.outcome }) });
59
+ else
60
+ out.push(ev);
61
+ }
62
+ });
63
+ return out;
64
+ }
65
+ /** The inner (non-boundary) rows of a turn: what renumbering must NOT touch. */
66
+ export function turnBody(events) {
67
+ return events.filter((ev) => ev.type !== "turn_start" && ev.type !== "turn_end");
68
+ }
69
+ /** Split a serialized log into one entry per complete turn (raw text lines). */
70
+ export function rawTurnBodies(text, parse) {
71
+ const lines = text.split("\n").filter((l) => l.trim() !== "");
72
+ const events = parse(text).events;
73
+ const bodies = [];
74
+ let current = null;
75
+ events.forEach((ev, i) => {
76
+ if (ev.type === "turn_start")
77
+ current = [];
78
+ else if (ev.type === "turn_end") {
79
+ if (current !== null)
80
+ bodies.push(current);
81
+ current = null;
82
+ }
83
+ else if (current !== null)
84
+ current.push(lines[i] ?? "");
85
+ });
86
+ return bodies;
87
+ }
88
+ /** The summary text embedded in a compacted log's head turn (null when absent). */
89
+ export function headSummary(events) {
90
+ const head = events.find((ev) => ev.type === "user_message" && ev.text.startsWith(SPEC_HEAD_PREFIX));
91
+ return head === undefined || head.type !== "user_message" ? null : head.text.slice(SPEC_HEAD_PREFIX.length);
92
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Golden-fixture access for the P5 double-run comparison.
3
+ *
4
+ * The fixtures are the P0 export of the retired backend
5
+ * (`fixtures/index.json` + `fixtures/sessions/<slug>/…`): an unmodified session
6
+ * log, the Studio `messages` projection captured over HTTP, and (for small
7
+ * sessions) the derived SSE transcript. Nothing here writes into `fixtures/`:
8
+ * every replay works on a COPY under a temp root.
9
+ */
10
+ /** One fixture session entry of the manifest. */
11
+ export interface FixtureSession {
12
+ id: string;
13
+ slug: string;
14
+ roles: string[];
15
+ events: number;
16
+ turns: number;
17
+ danglingToolCalls: number;
18
+ subCalls: number;
19
+ expectedMessages: number;
20
+ sseFrames: number;
21
+ }
22
+ export interface FixtureManifest {
23
+ generatedAt: string;
24
+ studio: string;
25
+ sessions: FixtureSession[];
26
+ counts: Record<string, unknown>;
27
+ }
28
+ /** The four golden artifacts of one session fixture. */
29
+ export interface FixtureFiles {
30
+ dir: string;
31
+ log: string;
32
+ messages: string;
33
+ derivedMessages: string;
34
+ sse: string | null;
35
+ }
36
+ export declare function readJson<T>(path: string): T;
37
+ /** Load `fixtures/index.json` (throws with the export hint when missing). */
38
+ export declare function loadManifest(fixturesDir: string): FixtureManifest;
39
+ /** Absolute paths of one session's golden files. */
40
+ export declare function fixtureFiles(fixturesDir: string, slug: string): FixtureFiles;
41
+ /** Split an id into its workspace and session-directory halves. */
42
+ export declare function splitSessionId(id: string): {
43
+ workspace: string;
44
+ session: string;
45
+ };
46
+ /** Copy a session fixture into `<root>/<workspace>/<session>/cli-main.jsonl`. */
47
+ export declare function plantFixture(root: string, files: FixtureFiles, id: string): {
48
+ dir: string;
49
+ logBytes: string;
50
+ };
51
+ /** Parse one JSONL fixture file (one JSON value per non-empty line). */
52
+ export declare function readJsonl(path: string): unknown[];
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Golden-fixture access for the P5 double-run comparison.
3
+ *
4
+ * The fixtures are the P0 export of the retired backend
5
+ * (`fixtures/index.json` + `fixtures/sessions/<slug>/…`): an unmodified session
6
+ * log, the Studio `messages` projection captured over HTTP, and (for small
7
+ * sessions) the derived SSE transcript. Nothing here writes into `fixtures/`:
8
+ * every replay works on a COPY under a temp root.
9
+ */
10
+ import { copyFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
11
+ import { join, resolve } from "node:path";
12
+ export function readJson(path) {
13
+ return JSON.parse(readFileSync(path, "utf8"));
14
+ }
15
+ /** Load `fixtures/index.json` (throws with the export hint when missing). */
16
+ export function loadManifest(fixturesDir) {
17
+ const path = join(resolve(fixturesDir), "index.json");
18
+ if (!existsSync(path))
19
+ throw new Error(`fixtures manifest not found: ${path} (run \`pnpm golden:export\` first)`);
20
+ return readJson(path);
21
+ }
22
+ /** Absolute paths of one session's golden files. */
23
+ export function fixtureFiles(fixturesDir, slug) {
24
+ const dir = join(resolve(fixturesDir), "sessions", slug);
25
+ const sse = join(dir, "sse-transcript-derived.jsonl");
26
+ return {
27
+ dir,
28
+ log: join(dir, "cli-main.jsonl"),
29
+ messages: join(dir, "messages-expected.json"),
30
+ derivedMessages: join(dir, "derive-messages-expected.json"),
31
+ sse: existsSync(sse) ? sse : null,
32
+ };
33
+ }
34
+ /** Split an id into its workspace and session-directory halves. */
35
+ export function splitSessionId(id) {
36
+ const slash = id.indexOf("/");
37
+ return { workspace: id.slice(0, slash), session: id.slice(slash + 1) };
38
+ }
39
+ /** Copy a session fixture into `<root>/<workspace>/<session>/cli-main.jsonl`. */
40
+ export function plantFixture(root, files, id) {
41
+ const { workspace, session } = splitSessionId(id);
42
+ const dir = join(root, workspace, session);
43
+ mkdirSync(dir, { recursive: true });
44
+ copyFileSync(files.log, join(dir, "cli-main.jsonl"));
45
+ return { dir, logBytes: readFileSync(files.log, "utf8") };
46
+ }
47
+ /** Parse one JSONL fixture file (one JSON value per non-empty line). */
48
+ export function readJsonl(path) {
49
+ return readFileSync(path, "utf8")
50
+ .split("\n")
51
+ .filter((line) => line.trim() !== "")
52
+ .map((line) => JSON.parse(line));
53
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The replay host: a throwaway Studio app whose engine is the REAL runtime.
3
+ *
4
+ * Every P5 run gets its own temp data root (workspaces.json + one session
5
+ * directory per fixture copy) and its own engine generation, so a replay can
6
+ * append turns, compact logs and fail loudly without touching `fixtures/` or the
7
+ * operator's data files. The engine's tool roots are pinned to the temp root, so
8
+ * the production path guard stays mounted.
9
+ */
10
+ import { type StudioApp } from "../app.js";
11
+ import type { Hono } from "hono";
12
+ import { type OfflineLlmOptions } from "../runtime/offline-llm.js";
13
+ import { type RealRuntimeAdapter } from "../runtime/real-runtime-adapter.js";
14
+ export interface ReplayHostOptions {
15
+ /** Workspace names to register (their directories are created under root). */
16
+ workspaces: readonly string[];
17
+ /** Offline LLM options for the composed generations. */
18
+ llm?: OfflineLlmOptions;
19
+ /** Static root for the app (defaults to an empty temp build). */
20
+ staticRoot?: string;
21
+ }
22
+ export interface ReplayHost {
23
+ app: Hono;
24
+ studio: StudioApp;
25
+ runtime: RealRuntimeAdapter;
26
+ /** Temp data root (workspaces + session copies live here). */
27
+ root: string;
28
+ workspacePath(name: string): string;
29
+ cleanup(): void;
30
+ }
31
+ /** Build a host over a fresh temp root. */
32
+ export declare function createReplayHost(opts: ReplayHostOptions): ReplayHost;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The replay host: a throwaway Studio app whose engine is the REAL runtime.
3
+ *
4
+ * Every P5 run gets its own temp data root (workspaces.json + one session
5
+ * directory per fixture copy) and its own engine generation, so a replay can
6
+ * append turns, compact logs and fail loudly without touching `fixtures/` or the
7
+ * operator's data files. The engine's tool roots are pinned to the temp root, so
8
+ * the production path guard stays mounted.
9
+ */
10
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { mkdtempSync } from "node:fs";
13
+ import { tmpdir } from "node:os";
14
+ import { createStudioApp } from "../app.js";
15
+ import { loadStudioConfig } from "../config.js";
16
+ import { createOfflineLlm } from "../runtime/offline-llm.js";
17
+ import { createRealRuntimeAdapter } from "../runtime/real-runtime-adapter.js";
18
+ /** Build a host over a fresh temp root. */
19
+ export function createReplayHost(opts) {
20
+ const root = mkdtempSync(join(tmpdir(), "replay-host-"));
21
+ const staticRoot = opts.staticRoot ?? join(root, "dist");
22
+ mkdirSync(staticRoot, { recursive: true });
23
+ writeFileSync(join(staticRoot, "index.html"), "<!doctype html><title>replay</title>\n");
24
+ const workspaces = [...new Set(opts.workspaces)];
25
+ for (const name of workspaces)
26
+ mkdirSync(join(root, name), { recursive: true });
27
+ // The registry is read EAGERLY at compose time, so it must exist up front.
28
+ writeFileSync(join(root, "workspaces.json"), `${JSON.stringify({ workspaces: workspaces.map((name) => ({ path: join(root, name) })), active_session: null }, null, 2)}\n`);
29
+ const env = { ...process.env, CELESTEA_TOOL_ROOTS: root };
30
+ const studio = createStudioApp({
31
+ config: loadStudioConfig({ cwd: root, env, paths: { staticRoot } }),
32
+ env,
33
+ runtime: (stores) => createRealRuntimeAdapter({
34
+ profile: {
35
+ model: "offline-replay-model",
36
+ base_url: "http://127.0.0.1:9/v1",
37
+ api_key_env: "CELESTEA_API_KEY",
38
+ reasoning_effort: null,
39
+ max_steps: 4096,
40
+ max_parallel_tool_calls: 4,
41
+ max_output_tokens: null,
42
+ context_window: 1_000_000,
43
+ system_prompt: "engine identity prompt",
44
+ },
45
+ env,
46
+ llm: () => createOfflineLlm(opts.llm ?? {}),
47
+ resultsDir: join(root, "worker-results"),
48
+ resolveSession: (id) => {
49
+ const resolved = stores.sessions.require(id);
50
+ return resolved.ok ? { sessionId: id, dir: resolved.value.dir } : null;
51
+ },
52
+ }),
53
+ });
54
+ return {
55
+ app: studio.app,
56
+ studio,
57
+ runtime: studio.services.runtime,
58
+ root,
59
+ workspacePath: (name) => join(root, name),
60
+ cleanup: () => rmSync(root, { recursive: true, force: true }),
61
+ };
62
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * P5 double-run comparison harness (`pnpm replay:e2e`).
3
+ *
4
+ * Replays the golden fixtures from the retired backend through the
5
+ * TS host (real runtime + offline LLM) and reports, per artifact, whether the
6
+ * output is byte-identical, frozen-golden, independently re-derived or merely
7
+ * self-consistent — plus what still has to be captured before P6.
8
+ */
9
+ export * from "./compare.js";
10
+ export * from "./expect-compact.js";
11
+ export * from "./fixtures.js";
12
+ export * from "./host.js";
13
+ export * from "./probes.js";
14
+ export * from "./session-e2e.js";
15
+ export * from "./e2e-replay.js";
16
+ export * from "./report.js";
@@ -0,0 +1,16 @@
1
+ /**
2
+ * P5 double-run comparison harness (`pnpm replay:e2e`).
3
+ *
4
+ * Replays the golden fixtures from the retired backend through the
5
+ * TS host (real runtime + offline LLM) and reports, per artifact, whether the
6
+ * output is byte-identical, frozen-golden, independently re-derived or merely
7
+ * self-consistent — plus what still has to be captured before P6.
8
+ */
9
+ export * from "./compare.js";
10
+ export * from "./expect-compact.js";
11
+ export * from "./fixtures.js";
12
+ export * from "./host.js";
13
+ export * from "./probes.js";
14
+ export * from "./session-e2e.js";
15
+ export * from "./e2e-replay.js";
16
+ export * from "./report.js";
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The end-to-end probes of one replayed session: a REAL turn through the host,
3
+ * the SSE transport check and the compaction probe.
4
+ *
5
+ * Every probe reads back the artifact it produced (the JSONL file, the SSE wire,
6
+ * the `.precompact` backup) so the report can state byte-level facts instead of
7
+ * "the call returned 200".
8
+ */
9
+ import { type Finding } from "./compare.js";
10
+ import type { ReplayHost } from "./host.js";
11
+ /** The frozen "nothing to compact" note. */
12
+ export declare const SKIP_NOTE = "\u5386\u53F2\u4E0D\u8DB3\uFF0C\u65E0\u9700\u538B\u7F29";
13
+ /** The frozen compacted note for K = 4. */
14
+ export declare const COMPACT_NOTE = "\u5DF2\u538B\u7F29\uFF1A\u6458\u8981\u8F6E + \u6700\u8FD14\u8F6E";
15
+ export interface WireFrame {
16
+ event: string;
17
+ turn: number;
18
+ payload: Record<string, unknown>;
19
+ }
20
+ /** Activate a session over the HTTP contract (the engine binds it per turn). */
21
+ export declare function activate(host: ReplayHost, id: string): Promise<number>;
22
+ /** Wait until the host reports the engine idle again. */
23
+ export declare function waitIdle(host: ReplayHost, timeoutMs?: number): Promise<void>;
24
+ /** Drive one real turn over `POST /api/turn`, collecting its SSE frames. */
25
+ export declare function runTurn(host: ReplayHost, input: string): Promise<{
26
+ status: number;
27
+ frames: WireFrame[];
28
+ }>;
29
+ /** Byte-level checks of the turn a probe appended to a session log. */
30
+ export declare function turnFindings(id: string, before: string, after: string, input: string, frames: readonly WireFrame[]): Finding[];
31
+ /** Parse the SSE wire text produced by `GET /api/events` into frames. */
32
+ export declare function parseWire(text: string): WireFrame[];
33
+ /**
34
+ * Push frames through the REAL `GET /api/events` endpoint and read them back.
35
+ * Frames are emitted in bursts smaller than the bus capacity (512): a larger
36
+ * backlog would be DROPPED on purpose (the frozen `lagged` degradation), which
37
+ * is a property of the bus, not of the replay.
38
+ */
39
+ export interface SseCapture {
40
+ frames: WireFrame[];
41
+ /** Raw SSE blocks (`event: …\ndata: …`) exactly as the endpoint wrote them. */
42
+ blocks: string[];
43
+ }
44
+ export declare function captureSseWire(host: ReplayHost, frames: readonly WireFrame[], session?: string | null, timeoutMs?: number): Promise<SseCapture>;
45
+ /**
46
+ * Byte-level wire check: every frame the endpoint wrote must equal the block
47
+ * re-serialized from its own envelope — which pins the field ORDER
48
+ * (`{v,session,turn,seq,payload}` since W513), the SSE framing and
49
+ * one-block-per-frame, not merely the parsed values.
50
+ */
51
+ export declare function compareSseWire(scope: string, expected: readonly WireFrame[], capture: SseCapture, note: string, session?: string | null): Finding;
52
+ /** SSE comparison of a session's derived transcript + the wire-level transport check. */
53
+ export declare function sseFindings(id: string, derived: unknown[], golden: unknown[] | null, capture: SseCapture): Finding[];
54
+ /** The compaction probe: run it over HTTP and verify every produced artifact. */
55
+ export declare function compactFindings(host: ReplayHost, id: string, dir: string): Promise<Finding[]>;
@@ -0,0 +1,225 @@
1
+ /**
2
+ * The end-to-end probes of one replayed session: a REAL turn through the host,
3
+ * the SSE transport check and the compaction probe.
4
+ *
5
+ * Every probe reads back the artifact it produced (the JSONL file, the SSE wire,
6
+ * the `.precompact` backup) so the report can state byte-level facts instead of
7
+ * "the call returned 200".
8
+ */
9
+ import { readFileSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { parseSessionJsonl } from "@celestea/session";
12
+ import { serializeEventLog } from "@celestea/runtime";
13
+ import { SESSION_LOG_NAME } from "../runtime/engine-session.js";
14
+ import { compareBytes, compareJson, note } from "./compare.js";
15
+ import { expectedCompactLog, headSummary, rawTurnBodies } from "./expect-compact.js";
16
+ /** Terminal status phases of a turn. */
17
+ const TERMINAL = ["completed", "cancelled", "error", "step_limit", "interrupted"];
18
+ /** The frozen "nothing to compact" note. */
19
+ export const SKIP_NOTE = "历史不足,无需压缩";
20
+ /** The frozen compacted note for K = 4. */
21
+ export const COMPACT_NOTE = "已压缩:摘要轮 + 最近4轮";
22
+ function delay(ms) {
23
+ return new Promise((resolve) => setTimeout(() => resolve(null), ms));
24
+ }
25
+ /** Activate a session over the HTTP contract (the engine binds it per turn). */
26
+ export async function activate(host, id) {
27
+ const res = await host.app.request(`/api/sessions/${encodeURIComponent(id)}/activate`, { method: "POST" });
28
+ return res.status;
29
+ }
30
+ /** Wait until the host reports the engine idle again. */
31
+ export async function waitIdle(host, timeoutMs = 10_000) {
32
+ const deadline = Date.now() + timeoutMs;
33
+ while (host.runtime.isBusy()) {
34
+ if (Date.now() > deadline)
35
+ throw new Error("engine did not settle");
36
+ await delay(5);
37
+ }
38
+ }
39
+ /** Drive one real turn over `POST /api/turn`, collecting its SSE frames. */
40
+ export async function runTurn(host, input) {
41
+ const sub = host.studio.services.bus.subscribe();
42
+ const frames = [];
43
+ const res = await host.app.request("/api/turn", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ input }) });
44
+ if (res.status === 202) {
45
+ const deadline = Date.now() + 10_000;
46
+ for (;;) {
47
+ const left = deadline - Date.now();
48
+ if (left <= 0)
49
+ break;
50
+ const frame = await Promise.race([sub.next(), delay(left)]);
51
+ if (frame === null)
52
+ break;
53
+ const payload = (frame.envelope.payload ?? {});
54
+ frames.push({ event: frame.event, turn: frame.envelope.turn, payload });
55
+ if (frame.event === "status" && TERMINAL.includes(String(payload["phase"])))
56
+ break;
57
+ }
58
+ }
59
+ sub.close();
60
+ await waitIdle(host);
61
+ return { status: res.status, frames };
62
+ }
63
+ /** Byte-level checks of the turn a probe appended to a session log. */
64
+ export function turnFindings(id, before, after, input, frames) {
65
+ const findings = [];
66
+ const newEvents = parseSessionJsonl(after).events.slice(parseSessionJsonl(before).events.length);
67
+ findings.push(compareBytes(`${id} :: turn-append-bytes`, before + serializeEventLog(newEvents), after, "appended turn is exactly its serialized events"));
68
+ findings.push(compareJson(`${id} :: turn-append-shape`, "byte-exact", ["turn_start", "user_message", "assistant_message", "turn_end"], newEvents.map((e) => e.type), "appended turn shape"));
69
+ const text = newEvents.find((e) => e.type === "assistant_message");
70
+ findings.push(compareJson(`${id} :: turn-answer`, "byte-exact", `echo: ${input}`, text?.type === "assistant_message" ? text.text : null, "offline answer"));
71
+ const end = newEvents.find((e) => e.type === "turn_end");
72
+ findings.push(compareJson(`${id} :: turn-outcome`, "byte-exact", "completed", end?.type === "turn_end" ? end.outcome : null, "terminal outcome"));
73
+ const names = frames.map((f) => f.event);
74
+ const ok = names[0] === "status" && names.includes("turn_end") && names[names.length - 1] === "status";
75
+ findings.push(ok
76
+ ? note(`${id} :: turn-sse`, "byte-exact", `turn frames ${names.join(">")}`, "match")
77
+ : note(`${id} :: turn-sse`, "golden", `turn frames ${names.join(">")}`, "diff"));
78
+ return findings;
79
+ }
80
+ /** Parse the SSE wire text produced by `GET /api/events` into frames. */
81
+ export function parseWire(text) {
82
+ const out = [];
83
+ for (const block of text.split("\n\n")) {
84
+ if (block.trim() === "")
85
+ continue;
86
+ const eventLine = block.split("\n").find((l) => l.startsWith("event: "));
87
+ const dataLine = block.split("\n").find((l) => l.startsWith("data: "));
88
+ if (eventLine === undefined || dataLine === undefined)
89
+ continue;
90
+ const envelope = JSON.parse(dataLine.slice("data: ".length));
91
+ out.push({ event: eventLine.slice("event: ".length), turn: envelope.turn, payload: (envelope.payload ?? {}) });
92
+ }
93
+ return out;
94
+ }
95
+ export async function captureSseWire(host, frames, session = null, timeoutMs = 20_000) {
96
+ const res = await host.app.request("/api/events");
97
+ const reader = res.body?.getReader();
98
+ if (reader === undefined)
99
+ throw new Error("/api/events returned no body");
100
+ await delay(20); // let the streamSSE handler subscribe before the first emit
101
+ const decoder = new TextDecoder();
102
+ let wire = "";
103
+ const burst = 128;
104
+ for (let i = 0; i < frames.length; i += burst) {
105
+ const target = Math.min(i + burst, frames.length);
106
+ for (const frame of frames.slice(i, target))
107
+ host.studio.services.bus.emit(frame.event, frame.turn, frame.payload, session);
108
+ const deadline = Date.now() + timeoutMs;
109
+ while (parseWire(wire).length < target && Date.now() < deadline) {
110
+ const chunk = await Promise.race([reader.read(), delay(300).then(() => null)]);
111
+ if (chunk === null || chunk.done === true)
112
+ break;
113
+ wire += decoder.decode(chunk.value);
114
+ }
115
+ }
116
+ await reader.cancel();
117
+ return { frames: parseWire(wire), blocks: wire.split("\n\n").filter((b) => b.trim() !== "") };
118
+ }
119
+ /**
120
+ * Byte-level wire check: every frame the endpoint wrote must equal the block
121
+ * re-serialized from its own envelope — which pins the field ORDER
122
+ * (`{v,session,turn,seq,payload}` since W513), the SSE framing and
123
+ * one-block-per-frame, not merely the parsed values.
124
+ */
125
+ export function compareSseWire(scope, expected, capture, note, session = null) {
126
+ if (capture.frames.length !== expected.length) {
127
+ return wireDiff(scope, `${note}: ${capture.frames.length} frame(s) on the wire, expected ${expected.length}`, []);
128
+ }
129
+ const diffs = [];
130
+ for (let i = 0; i < expected.length; i++) {
131
+ const frame = expected[i];
132
+ const block = capture.blocks[i] ?? "";
133
+ // `seq` is a process-global counter the replay cannot predict: take it off
134
+ // the wire, then demand the REST of the block be byte-identical.
135
+ const seq = seqOf(block);
136
+ const rebuilt = `event: ${frame.event}\ndata: ${JSON.stringify({ v: 2, session, turn: frame.turn, seq, payload: frame.payload })}`;
137
+ if (block !== rebuilt && diffs.length < 3)
138
+ diffs.push(`$[${i}] wire block differs: ${block.slice(0, 80)} != ${rebuilt.slice(0, 80)}`);
139
+ }
140
+ if (diffs.length > 0)
141
+ return wireDiff(scope, `${note}: ${diffs.length} wire divergence(s)`, diffs);
142
+ return { scope, kind: "byte-exact", verdict: "match", detail: `${note}: ${capture.frames.length} wire block(s) byte-identical (envelope order v,session,turn,seq,payload)` };
143
+ }
144
+ /** One wire-level divergence finding (only byte-level evidence is reported here). */
145
+ function wireDiff(scope, detail, diffs) {
146
+ return { scope, kind: "byte-exact", verdict: "diff", detail, diffs };
147
+ }
148
+ /** `seq` of a written block (a process-global counter the replay cannot predict). */
149
+ function seqOf(block) {
150
+ const data = block.split("\n").find((l) => l.startsWith("data: "));
151
+ if (data === undefined)
152
+ return null;
153
+ try {
154
+ return JSON.parse(data.slice("data: ".length)).seq ?? null;
155
+ }
156
+ catch {
157
+ return null;
158
+ }
159
+ }
160
+ /** SSE comparison of a session's derived transcript + the wire-level transport check. */
161
+ export function sseFindings(id, derived, golden, capture) {
162
+ const findings = [];
163
+ if (golden === null) {
164
+ findings.push(note(`${id} :: sse-transcript`, "self-check", `golden transcript not stored for this session; ${derived.length} frame(s) regenerated in-memory`));
165
+ }
166
+ else {
167
+ findings.push(compareJson(`${id} :: sse-transcript`, "self-check", golden, derived, "TS-derived transcript (legacy capture is a P6 gap)"));
168
+ }
169
+ const expected = derived.map((f) => ({
170
+ event: f.event,
171
+ turn: f.data.turn,
172
+ payload: f.data.payload,
173
+ }));
174
+ const source = golden === null ? "TS-derived frames" : "stored golden transcript frames";
175
+ findings.push(compareSseWire(`${id} :: sse-transport`, expected, capture, `GET /api/events replayed ${source}`, id));
176
+ return findings;
177
+ }
178
+ /** The compaction probe: run it over HTTP and verify every produced artifact. */
179
+ export async function compactFindings(host, id, dir) {
180
+ const logPath = join(dir, SESSION_LOG_NAME);
181
+ const before = readFileSync(logPath, "utf8");
182
+ const beforeEvents = parseSessionJsonl(before).events;
183
+ const sub = host.studio.services.bus.subscribe();
184
+ const res = await host.app.request(`/api/sessions/${encodeURIComponent(id)}/compact`, { method: "POST" });
185
+ const body = (await res.json());
186
+ const frame = await Promise.race([sub.next(), delay(2_000)]);
187
+ sub.close();
188
+ const after = readFileSync(logPath, "utf8");
189
+ return expectedCompactLog(beforeEvents, "", 4) === null
190
+ ? shortHistoryFindings(id, before, after, body, frame)
191
+ : compactedFindings({ id, dir, before, after, body, frame });
192
+ }
193
+ /** History at/below the threshold: nothing may change. */
194
+ function shortHistoryFindings(id, before, after, body, frame) {
195
+ const findings = [compareBytes(`${id} :: compact-log`, before, after, "log untouched (history below threshold)")];
196
+ findings.push(compareJson(`${id} :: compact-response`, "golden", { compacted: false, note: SKIP_NOTE }, { compacted: body["compacted"], note: body["note"] }, "skip branch"));
197
+ findings.push(skipFrameFinding(id, frame));
198
+ return findings;
199
+ }
200
+ function skipFrameFinding(id, frame) {
201
+ const payload = frame === null ? null : frame.envelope.payload;
202
+ return compareJson(`${id} :: compact-sse`, "golden", { compacted: false, note: SKIP_NOTE }, { compacted: false, note: payload?.["note"] }, "compact frame");
203
+ }
204
+ /** History above the threshold: structure, backup and kept-turn bytes. */
205
+ function compactedFindings(input) {
206
+ const { id, dir, before, after, body, frame } = input;
207
+ const findings = [];
208
+ const events = parseSessionJsonl(after).events;
209
+ const summary = headSummary(events);
210
+ const expected = summary === null ? null : expectedCompactLog(parseSessionJsonl(before).events, summary, 4);
211
+ if (expected === null) {
212
+ findings.push(note(`${id} :: compact-log`, "spec-derived", "compacted log has no 【上下文压缩】 head turn", "diff"));
213
+ }
214
+ else {
215
+ findings.push(compareBytes(`${id} :: compact-log`, serializeEventLog(expected), after, "independent re-derivation of the W259 plan"));
216
+ }
217
+ findings.push(compareBytes(`${id} :: compact-backup`, before, readFileSync(join(dir, "cli-main.jsonl.precompact"), "utf8"), ".precompact backup of the pre-compaction log"));
218
+ const keptBefore = rawTurnBodies(before, parseSessionJsonl).slice(-4);
219
+ const keptAfter = rawTurnBodies(after, parseSessionJsonl).slice(-4);
220
+ findings.push(compareJson(`${id} :: compact-kept-turn-bodies`, "byte-exact", keptBefore, keptAfter, "kept turns are byte-identical apart from renumbering"));
221
+ findings.push(compareJson(`${id} :: compact-response`, "golden", { compacted: true, kept_turns: 4, note: COMPACT_NOTE }, { compacted: body["compacted"], kept_turns: body["kept_turns"], note: body["note"] }, "compacted branch"));
222
+ const payload = frame === null ? null : frame.envelope.payload;
223
+ findings.push(compareJson(`${id} :: compact-sse`, "golden", { session: id, kept_turns: 4, note: COMPACT_NOTE, rebound: true }, payload, "compact frame"));
224
+ return findings;
225
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Markdown / console rendering of the P5 double-run report.
3
+ *
4
+ * The markdown is the deliverable of the replay: it states, per finding, WHICH
5
+ * evidence class backs the claim (byte-exact / frozen golden / spec-derived /
6
+ * self-check) so a reader can tell a proven byte-level match from a
7
+ * self-consistency check, and it lists the P6 gaps next to the verdict.
8
+ */
9
+ import type { ReplayE2EReport } from "./e2e-replay.js";
10
+ /** The full markdown report. */
11
+ export declare function renderReplayMarkdown(report: ReplayE2EReport): string;
12
+ /** One-line console summary (the CLI prints this plus the session rows). */
13
+ export declare function renderReplayConsole(report: ReplayE2EReport): string;