@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,212 @@
1
+ /**
2
+ * The pending-question table (W783 §5.1) — one [PendingQuestion] per unanswered
3
+ * request, plus the process-wide registry the HTTP layer answers into.
4
+ *
5
+ * Shape mirrors the official DSH `PendingQuestion` (same four members, same
6
+ * delegation sentinel, same abort wiring), with the two behaviours this repo
7
+ * adds:
8
+ *
9
+ * 1. **it can EXPEND** (§6). `expires_at` is an ABSOLUTE deadline judged at
10
+ * read time (the `grants.ts: isExpired` pattern), and `timeout()` is the
11
+ * active half that makes the parked `await` settle even if nobody ever
12
+ * reads the table. The timer is cleared on every settlement, so a live
13
+ * registry never holds a stray handle.
14
+ * 2. **the §6.2 race is not a bug.** The user answering and the clock
15
+ * expiring are both legitimate; whoever arrives first wins and the loser
16
+ * is TOLD it lost (`answer()` returns false) instead of throwing. The
17
+ * strict double-settle guard stays internal, where a second settlement
18
+ * really is a defect.
19
+ *
20
+ * `result` is the promise the waterfall layer hands back, so an answer RESOLVES
21
+ * the parked tool call directly — the answer never travels as a message (§4.2).
22
+ */
23
+ import { UserQuestionError } from "@celestea/core";
24
+ function deferred() {
25
+ let resolve;
26
+ let reject;
27
+ let done = false;
28
+ const promise = new Promise((res, rej) => {
29
+ resolve = (value) => {
30
+ done = true;
31
+ res(value);
32
+ };
33
+ reject = (reason) => {
34
+ done = true;
35
+ rej(reason);
36
+ };
37
+ });
38
+ return { promise, resolve, reject, settled: () => done };
39
+ }
40
+ /** The empty answer a timeout returns — the system decides NOTHING for the model. */
41
+ export const TIMED_OUT_OUTCOME = { answers: [], timed_out: true };
42
+ /** One unanswered question, awaiting the human. */
43
+ export class PendingQuestion {
44
+ requestId;
45
+ sessionId;
46
+ questions;
47
+ expiresAt;
48
+ /** The resolved maximum wait in ms (§6.4). */
49
+ timeoutMs;
50
+ /** The promise the answerer waterfall returns to the parked tool call. */
51
+ result;
52
+ completion = deferred();
53
+ signal;
54
+ onAbort;
55
+ /** Rejection sentinel that asks the waterfall to try the layer behind us. */
56
+ delegated = Symbol("pending question delegated");
57
+ settled = false;
58
+ timer = null;
59
+ outcome = null;
60
+ constructor(init) {
61
+ this.requestId = init.requestId;
62
+ this.sessionId = init.sessionId;
63
+ this.questions = init.questions;
64
+ this.expiresAt = init.expiresAt;
65
+ this.timeoutMs = init.timeoutMs;
66
+ this.result = this.completion.promise;
67
+ this.signal = init.signal;
68
+ if (init.signal === undefined) {
69
+ this.onAbort = undefined;
70
+ return;
71
+ }
72
+ this.onAbort = () => this.abort(abortedQuestion());
73
+ init.signal.addEventListener("abort", this.onAbort, { once: true });
74
+ // A signal that was ALREADY aborted never fires the event again.
75
+ if (init.signal.aborted)
76
+ this.onAbort();
77
+ }
78
+ /** Has this question already been settled (answered, expired, cancelled…)? */
79
+ get isSettled() {
80
+ return this.settled;
81
+ }
82
+ /** How it ended, or null while it is still answerable. */
83
+ get settlement() {
84
+ return this.outcome;
85
+ }
86
+ /** Read-time expiry (§6.1, the authoritative half): `now >= expires_at`. */
87
+ isExpired(now) {
88
+ return now >= this.expiresAt;
89
+ }
90
+ /** Milliseconds left, floored at 0 (what the UI counts down). */
91
+ remainingMs(now) {
92
+ return Math.max(0, this.expiresAt - now);
93
+ }
94
+ /**
95
+ * The human answered. Returns false when the question was already settled —
96
+ * that is the §6.2 race, not a defect, so the caller reports it instead of
97
+ * throwing. `timed_out` is always false here: a real answer arrived.
98
+ */
99
+ answer(answers) {
100
+ return this.settle("answered", () => this.completion.resolve({ answers, timed_out: false }));
101
+ }
102
+ /** The deadline passed: settle with the EMPTY answer set (§6.3). */
103
+ timeout() {
104
+ return this.settle("timed_out", () => this.completion.resolve(TIMED_OUT_OUTCOME));
105
+ }
106
+ /** The user dismissed the card (§5.1 `cancel`). */
107
+ cancel(reason = "the user cancelled ask_user_question") {
108
+ return this.settle("cancelled", () => this.completion.reject(new UserQuestionError(reason, "ASK_CANCELLED")));
109
+ }
110
+ /** Transport / scope / plugin lifetime ended (§5.1 `abort`). */
111
+ abort(reason) {
112
+ return this.settle("aborted", () => this.completion.reject(reason));
113
+ }
114
+ /**
115
+ * This layer will not answer: reject with the delegation sentinel so the
116
+ * waterfall runs the next layer. Silent when already settled (DSH behaviour).
117
+ */
118
+ delegate() {
119
+ if (this.settled)
120
+ return false;
121
+ return this.settle("aborted", () => this.completion.reject(this.delegated));
122
+ }
123
+ /** Did [delegate] produce this rejection reason? */
124
+ isDelegation(reason) {
125
+ return reason === this.delegated;
126
+ }
127
+ /**
128
+ * Arm the active half of the double track (§6.1). The timer only ever calls
129
+ * [timeout], which is a no-op once the user answered first.
130
+ */
131
+ armTimer(now) {
132
+ if (this.settled || this.timer !== null)
133
+ return;
134
+ this.timer = setTimeout(() => {
135
+ this.timer = null;
136
+ this.timeout();
137
+ }, Math.max(0, this.expiresAt - now()));
138
+ // Never hold the process open for a question nobody is waiting on.
139
+ this.timer.unref?.();
140
+ }
141
+ /** One settlement, ever: the strict guard that exposes a real double-settle. */
142
+ settle(kind, commit) {
143
+ if (this.settled)
144
+ return false;
145
+ this.settled = true;
146
+ this.outcome = kind;
147
+ this.disarm();
148
+ commit();
149
+ return true;
150
+ }
151
+ /** Drop the timer and the abort listener — no handle outlives the question. */
152
+ disarm() {
153
+ if (this.timer !== null) {
154
+ clearTimeout(this.timer);
155
+ this.timer = null;
156
+ }
157
+ if (this.signal !== undefined && this.onAbort !== undefined) {
158
+ this.signal.removeEventListener("abort", this.onAbort);
159
+ }
160
+ }
161
+ }
162
+ /** The rejection an aborted wait produces (§5.3 `ASK_ABORTED`). */
163
+ export function abortedQuestion(cause) {
164
+ return new UserQuestionError("ask_user_question was aborted before the user answered", "ASK_ABORTED", cause === undefined ? undefined : { cause });
165
+ }
166
+ /**
167
+ * The process-wide pending table.
168
+ *
169
+ * It is ONE registry per host process, not one per session: the answer endpoint
170
+ * addresses a request by id alone and must find it without being told which
171
+ * session asked. Each generation's own service registers into this shared table,
172
+ * which is also what lets `GET /api/sessions/{id}/questions` filter by session.
173
+ */
174
+ export class QuestionRegistry {
175
+ pending = new Map();
176
+ counter = 0;
177
+ /** Mint a request id (`q-<n>`); unique for the life of the process. */
178
+ nextRequestId() {
179
+ this.counter += 1;
180
+ return `q-${this.counter}`;
181
+ }
182
+ /** Register a question; a duplicate id is a bug, so it fails loudly. */
183
+ add(question) {
184
+ if (this.pending.has(question.requestId)) {
185
+ throw new Error(`question '${question.requestId}' is already pending`);
186
+ }
187
+ this.pending.set(question.requestId, question);
188
+ }
189
+ /** Drop a settled question (idempotent). */
190
+ remove(requestId) {
191
+ this.pending.delete(requestId);
192
+ }
193
+ get(requestId) {
194
+ return this.pending.get(requestId);
195
+ }
196
+ /** Every unanswered question, oldest first (registration order). */
197
+ all() {
198
+ return [...this.pending.values()];
199
+ }
200
+ /** The unanswered questions of ONE session (`null` = the detached generation). */
201
+ ofSession(sessionId) {
202
+ return this.all().filter((question) => question.sessionId === sessionId);
203
+ }
204
+ /** How many questions are still parked (diagnostics / tests). */
205
+ size() {
206
+ return this.pending.size;
207
+ }
208
+ }
209
+ /** A fresh table (one per host process). */
210
+ export function createQuestionRegistry() {
211
+ return new QuestionRegistry();
212
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * W783 §7 — the two session-log rows of the user-question feature.
3
+ *
4
+ * WHY THEY EXIST AT ALL: `tool_result` has no `decision` field, so a question's
5
+ * state would otherwise live only in an SSE frame and vanish on reload. These
6
+ * rows are the durable record: a reconnecting client rebuilds the card from
7
+ * `user_question`, and a replay can tell "the user answered" from "the clock ran
8
+ * out" by finding (or not finding) the matching `user_answer`.
9
+ *
10
+ * WHAT THEY ARE NOT: model-visible history. Both projections SKIP them, because
11
+ * the model already receives the outcome as the ordinary `tool_result` of
12
+ * `ask_user_question` — projecting them too would invent a second copy of the
13
+ * same decision and break the tool_call/tool_result pairing.
14
+ *
15
+ * The row shape is additive (W783 §7.2): a reader that does not know these two
16
+ * types treats them exactly like any other unknown row — a torn tail.
17
+ */
18
+ import type { AskUserQuestionAnswerItem, AskUserQuestionItem, SessionEvent } from "@celestea/core";
19
+ import type { PendingQuestion } from "./question-registry.js";
20
+ /** The `user_question` row of one parked request. */
21
+ export declare function questionAskedRow(question: PendingQuestion): SessionEvent;
22
+ /**
23
+ * The `user_answer` row of one settled request.
24
+ *
25
+ * `timedOut` is written as `timed_out` and is what distinguishes the §6.3
26
+ * expiry ("nobody was there") from a real answer that selected nothing.
27
+ */
28
+ export declare function questionAnsweredRow(requestId: string, answers: readonly AskUserQuestionAnswerItem[], timedOut: boolean): SessionEvent;
29
+ /** The questions of a `user_question` row, as the seam's item shape. */
30
+ export declare function askedItemsOf(event: {
31
+ questions?: unknown;
32
+ }): AskUserQuestionItem[];
@@ -0,0 +1,43 @@
1
+ /**
2
+ * W783 §7 — the two session-log rows of the user-question feature.
3
+ *
4
+ * WHY THEY EXIST AT ALL: `tool_result` has no `decision` field, so a question's
5
+ * state would otherwise live only in an SSE frame and vanish on reload. These
6
+ * rows are the durable record: a reconnecting client rebuilds the card from
7
+ * `user_question`, and a replay can tell "the user answered" from "the clock ran
8
+ * out" by finding (or not finding) the matching `user_answer`.
9
+ *
10
+ * WHAT THEY ARE NOT: model-visible history. Both projections SKIP them, because
11
+ * the model already receives the outcome as the ordinary `tool_result` of
12
+ * `ask_user_question` — projecting them too would invent a second copy of the
13
+ * same decision and break the tool_call/tool_result pairing.
14
+ *
15
+ * The row shape is additive (W783 §7.2): a reader that does not know these two
16
+ * types treats them exactly like any other unknown row — a torn tail.
17
+ */
18
+ /** The `user_question` row of one parked request. */
19
+ export function questionAskedRow(question) {
20
+ return {
21
+ type: "user_question",
22
+ id: question.requestId,
23
+ questions: [...question.questions],
24
+ // Both timings are written explicitly, so a replay can compute "expired"
25
+ // from the row alone (read-time judgement, §6.1) with no timer involved.
26
+ expires_at: question.expiresAt,
27
+ timeout_ms: question.timeoutMs,
28
+ };
29
+ }
30
+ /**
31
+ * The `user_answer` row of one settled request.
32
+ *
33
+ * `timedOut` is written as `timed_out` and is what distinguishes the §6.3
34
+ * expiry ("nobody was there") from a real answer that selected nothing.
35
+ */
36
+ export function questionAnsweredRow(requestId, answers, timedOut) {
37
+ return { type: "user_answer", id: requestId, answers: [...answers], timed_out: timedOut };
38
+ }
39
+ /** The questions of a `user_question` row, as the seam's item shape. */
40
+ export function askedItemsOf(event) {
41
+ const raw = event.questions;
42
+ return Array.isArray(raw) ? raw : [];
43
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Comparison primitives + the finding vocabulary of the P5 report.
3
+ *
4
+ * Four evidence classes, deliberately distinguished (the point of a double-run
5
+ * report is to say WHICH claims are golden and which are self-consistency):
6
+ * - `byte-exact` the artifact was reproduced byte for byte (JSONL logs,
7
+ * SSE wire frames, backups);
8
+ * - `golden` compared against a capture of the retired backend
9
+ * (`messages-expected.json`);
10
+ * - `spec-derived` compared against an independent re-derivation of the
11
+ * frozen spec (compaction planning), i.e. cross-implementation;
12
+ * - `self-check` compared against a TS-derived artifact (SSE transcript of a
13
+ * session whose transcript was never captured);
14
+ * - `info` / `skip` — reported, never a failure.
15
+ */
16
+ export type FindingKind = "byte-exact" | "golden" | "spec-derived" | "self-check" | "info";
17
+ export type FindingVerdict = "match" | "diff" | "skip";
18
+ export interface Finding {
19
+ scope: string;
20
+ kind: FindingKind;
21
+ verdict: FindingVerdict;
22
+ detail: string;
23
+ /** Up to a handful of concrete divergences (paths + values). */
24
+ diffs?: string[];
25
+ }
26
+ /** sha256 of a text or buffer (fixture manifests are hash-checked). */
27
+ export declare function sha256(text: string | Buffer): string;
28
+ /** Short single-line rendering of a value for a diff detail line. */
29
+ export declare function brief(value: unknown, max?: number): string;
30
+ /** First index where two byte strings differ (-1 = equal). */
31
+ export declare function firstByteDiff(a: string, b: string): number;
32
+ /** Byte-level comparison of two artifacts. */
33
+ export declare function compareBytes(scope: string, expected: string, actual: string, note: string): Finding;
34
+ /** How many concrete divergences one finding carries (keeps reports readable). */
35
+ export declare const MAX_DIFFS = 3;
36
+ /** Element-wise JSON comparison with the first [MAX_DIFFS] divergences. */
37
+ export declare function compareJson(scope: string, kind: FindingKind, expected: unknown, actual: unknown, note: string): Finding;
38
+ /** A `skip`/`info` finding (never fails the run). */
39
+ export declare function note(scope: string, kind: FindingKind, detail: string, verdict?: FindingVerdict): Finding;
40
+ /** Count findings by verdict. */
41
+ export declare function tally(findings: readonly Finding[]): {
42
+ matched: number;
43
+ diffed: number;
44
+ skipped: number;
45
+ byteExact: number;
46
+ golden: number;
47
+ };
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Comparison primitives + the finding vocabulary of the P5 report.
3
+ *
4
+ * Four evidence classes, deliberately distinguished (the point of a double-run
5
+ * report is to say WHICH claims are golden and which are self-consistency):
6
+ * - `byte-exact` the artifact was reproduced byte for byte (JSONL logs,
7
+ * SSE wire frames, backups);
8
+ * - `golden` compared against a capture of the retired backend
9
+ * (`messages-expected.json`);
10
+ * - `spec-derived` compared against an independent re-derivation of the
11
+ * frozen spec (compaction planning), i.e. cross-implementation;
12
+ * - `self-check` compared against a TS-derived artifact (SSE transcript of a
13
+ * session whose transcript was never captured);
14
+ * - `info` / `skip` — reported, never a failure.
15
+ */
16
+ import { createHash } from "node:crypto";
17
+ import { firstJsonDiff, stableStringify } from "@celestea/core";
18
+ /** sha256 of a text or buffer (fixture manifests are hash-checked). */
19
+ export function sha256(text) {
20
+ return createHash("sha256").update(text).digest("hex");
21
+ }
22
+ /** Short single-line rendering of a value for a diff detail line. */
23
+ export function brief(value, max = 120) {
24
+ const text = typeof value === "string" ? value : stableStringify(value ?? null);
25
+ return text.length <= max ? text : `${text.slice(0, max)}…`;
26
+ }
27
+ /** First index where two byte strings differ (-1 = equal). */
28
+ export function firstByteDiff(a, b) {
29
+ const n = Math.min(a.length, b.length);
30
+ for (let i = 0; i < n; i++)
31
+ if (a[i] !== b[i])
32
+ return i;
33
+ return a.length === b.length ? -1 : n;
34
+ }
35
+ /** Byte-level comparison of two artifacts. */
36
+ export function compareBytes(scope, expected, actual, note) {
37
+ if (expected === actual) {
38
+ return { scope, kind: "byte-exact", verdict: "match", detail: `${note}: ${Buffer.byteLength(actual)} bytes identical (sha256 ${sha256(actual).slice(0, 12)})` };
39
+ }
40
+ const at = firstByteDiff(expected, actual);
41
+ return {
42
+ scope,
43
+ kind: "byte-exact",
44
+ verdict: "diff",
45
+ detail: `${note}: byte ${at} differs (expected ${Buffer.byteLength(expected)} B, actual ${Buffer.byteLength(actual)} B)`,
46
+ diffs: [
47
+ `expected@${at}: ${brief(expected.slice(Math.max(0, at - 40), at + 80))}`,
48
+ `actual@${at}: ${brief(actual.slice(Math.max(0, at - 40), at + 80))}`,
49
+ ],
50
+ };
51
+ }
52
+ /** How many concrete divergences one finding carries (keeps reports readable). */
53
+ export const MAX_DIFFS = 3;
54
+ /** Element-wise JSON comparison with the first [MAX_DIFFS] divergences. */
55
+ export function compareJson(scope, kind, expected, actual, note) {
56
+ const max = MAX_DIFFS;
57
+ if (stableStringify(expected) === stableStringify(actual)) {
58
+ const size = Array.isArray(actual) ? `${actual.length} item(s)` : "value";
59
+ return { scope, kind, verdict: "match", detail: `${note}: ${size} identical` };
60
+ }
61
+ const diffs = [];
62
+ if (Array.isArray(expected) && Array.isArray(actual)) {
63
+ if (expected.length !== actual.length)
64
+ diffs.push(`length ${expected.length} != ${actual.length}`);
65
+ for (let i = 0; i < Math.min(expected.length, actual.length) && diffs.length < max; i++) {
66
+ const d = firstJsonDiff(expected[i], actual[i], `$[${i}]`);
67
+ if (d !== null)
68
+ diffs.push(d);
69
+ }
70
+ }
71
+ else {
72
+ diffs.push(firstJsonDiff(expected, actual, "$") ?? "values differ");
73
+ }
74
+ return { scope, kind, verdict: "diff", detail: `${note}: ${diffs.length} divergence(s)`, diffs };
75
+ }
76
+ /** A `skip`/`info` finding (never fails the run). */
77
+ export function note(scope, kind, detail, verdict = "skip") {
78
+ return { scope, kind, detail, verdict };
79
+ }
80
+ /** Count findings by verdict. */
81
+ export function tally(findings) {
82
+ return {
83
+ matched: findings.filter((f) => f.verdict === "match").length,
84
+ diffed: findings.filter((f) => f.verdict === "diff").length,
85
+ skipped: findings.filter((f) => f.verdict === "skip").length,
86
+ byteExact: findings.filter((f) => f.verdict === "match" && f.kind === "byte-exact").length,
87
+ golden: findings.filter((f) => f.verdict === "match" && f.kind === "golden").length,
88
+ };
89
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The P5 double-run driver: replay every golden session through the TS host and
3
+ * aggregate the structured report.
4
+ *
5
+ * The run is SEQUENTIAL on purpose (one host, one engine generation, one session
6
+ * at a time): a replay mutates the session copy it works on (probe turn +
7
+ * compaction), and a shared host would let one session's compaction be observed
8
+ * by another's comparison.
9
+ */
10
+ import { type Finding } from "./compare.js";
11
+ import { type SessionE2E } from "./session-e2e.js";
12
+ export interface ReplayE2EOptions {
13
+ fixturesDir?: string;
14
+ /** Input of the real probe turn appended to every session copy. */
15
+ probeInput?: string;
16
+ /** Replay only the first N sessions (smoke runs). */
17
+ maxSessions?: number;
18
+ }
19
+ export interface ReplayE2ESummary {
20
+ sessions: number;
21
+ findings: number;
22
+ matched: number;
23
+ byteExact: number;
24
+ golden: number;
25
+ diffed: number;
26
+ skipped: number;
27
+ errors: number;
28
+ verdict: "match" | "diff" | "error";
29
+ }
30
+ export interface ReplayE2EReport {
31
+ generatedAt: string;
32
+ fixtures: string;
33
+ fixturesGeneratedAt: string;
34
+ probeInput: string;
35
+ summary: ReplayE2ESummary;
36
+ sessions: SessionE2E[];
37
+ findings: Finding[];
38
+ errors: string[];
39
+ gaps: string[];
40
+ }
41
+ /** What is NOT yet golden after P5 (each item names the missing capture). */
42
+ export declare const P6_GAPS: readonly string[];
43
+ /** Replay all fixture sessions and aggregate the report. */
44
+ export declare function runReplayE2E(opts?: ReplayE2EOptions): Promise<ReplayE2EReport>;
45
+ /** Exit code of a report: 0 match, 1 diff, 2 harness error. */
46
+ export declare function exitCodeOf(report: ReplayE2EReport): number;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * The P5 double-run driver: replay every golden session through the TS host and
3
+ * aggregate the structured report.
4
+ *
5
+ * The run is SEQUENTIAL on purpose (one host, one engine generation, one session
6
+ * at a time): a replay mutates the session copy it works on (probe turn +
7
+ * compaction), and a shared host would let one session's compaction be observed
8
+ * by another's comparison.
9
+ */
10
+ import { resolve } from "node:path";
11
+ import { note, tally } from "./compare.js";
12
+ import { loadManifest, splitSessionId } from "./fixtures.js";
13
+ import { createReplayHost } from "./host.js";
14
+ import { replaySession } from "./session-e2e.js";
15
+ /** What is NOT yet golden after P5 (each item names the missing capture). */
16
+ export const P6_GAPS = [
17
+ "compact 后日志:TS 侧与「独立重推导(spec-derived)」逐字节一致,但实机 compact 产物尚未捕获 —— P6 需用打桩上游(非流式摘要)跑一次 compact,导出 compact-expected.jsonl 作为真黄金。",
18
+ "SSE 序列:逐帧对比的黄金是 TS 自推导 transcript(P0 导出器生成),fixtures/sse/live-capture.raw.txt 为 0 字节(P0 禁止 POST /api/turn)—— P6 需驱动一次真实 turn 抓取 live SSE(8 事件 + lagged 降级),并把 JSONL transcript 换成实机原生产物。",
19
+ "LLM 是离线确定性 mock:usage / cache_hit_ratio / context_usage 的数值来自 mock 的 usage 帧,未经真实 provider 的 SSE/usage 解析链路 —— P6 需接 packages/llm 的 mock-upstream(本地假上游,仍禁真网)验证解析与状态线口径。",
20
+ "live/*.json(实机只读快照)尚未纳入 e2e 逐字段对拍(本阶段只验形状与口径)—— P6 把 status/config/tools/health 快照纳入逐字段对比。",
21
+ "worker 编排:spawn/send/status 走真实 registry 且 driven=true,但表是内存实现(tsvPath=null,不写共享 registry.tsv),receipt/report 文件协议未对拍 —— P6 与实机的 registry.tsv / WORKER_<wid>_DONE 回执对拍。",
22
+ "compact 摘要正文由 mock 生成,只做结构性校验(头部轮、保留轮、重编号、备份)—— P6 接入真实/打桩摘要后再逐字节对拍摘要正文。",
23
+ "大 fixture 的 SSE 采用分批推送(每批 < 总线容量 512),未覆盖容量溢出后的 lagged 降级 —— 该路径由 apps/studio/src/sse.test.ts 覆盖,P6 需在 e2e 中补一条真机溢出用例。",
24
+ ];
25
+ /** Replay all fixture sessions and aggregate the report. */
26
+ export async function runReplayE2E(opts = {}) {
27
+ const fixturesDir = resolve(opts.fixturesDir ?? "fixtures");
28
+ const manifest = loadManifest(fixturesDir);
29
+ const entries = (opts.maxSessions === undefined ? manifest.sessions : manifest.sessions.slice(0, opts.maxSessions));
30
+ const host = createReplayHost({ workspaces: [...new Set(entries.map((e) => splitSessionId(e.id).workspace))] });
31
+ const probeInput = opts.probeInput ?? "P5 重放探针";
32
+ const sessions = [];
33
+ const errors = [];
34
+ try {
35
+ for (const entry of entries)
36
+ sessions.push(await replayOne(host, fixturesDir, entry, probeInput, errors));
37
+ }
38
+ finally {
39
+ host.cleanup();
40
+ }
41
+ return buildReport({ fixturesDir, manifestGeneratedAt: manifest.generatedAt, probeInput, sessions, errors });
42
+ }
43
+ async function replayOne(host, fixturesDir, entry, probeInput, errors) {
44
+ try {
45
+ return await replaySession({ host, fixturesDir, entry, probeInput });
46
+ }
47
+ catch (e) {
48
+ const message = e instanceof Error ? e.message : String(e);
49
+ errors.push(`${entry.id}: ${message}`);
50
+ return {
51
+ id: entry.id,
52
+ slug: entry.slug,
53
+ roles: entry.roles,
54
+ events: entry.events,
55
+ turns: entry.turns,
56
+ findings: [note(`${entry.id} :: replay`, "info", `replay aborted: ${message}`, "diff")],
57
+ verdict: "diff",
58
+ };
59
+ }
60
+ }
61
+ function buildReport(input) {
62
+ const findings = input.sessions.flatMap((s) => s.findings);
63
+ const counts = tally(findings);
64
+ return {
65
+ generatedAt: new Date().toISOString(),
66
+ fixtures: input.fixturesDir,
67
+ fixturesGeneratedAt: input.manifestGeneratedAt,
68
+ probeInput: input.probeInput,
69
+ summary: {
70
+ sessions: input.sessions.length,
71
+ findings: findings.length,
72
+ matched: counts.matched,
73
+ byteExact: counts.byteExact,
74
+ golden: counts.golden,
75
+ diffed: counts.diffed,
76
+ skipped: counts.skipped,
77
+ errors: input.errors.length,
78
+ verdict: input.errors.length > 0 ? "error" : counts.diffed > 0 ? "diff" : "match",
79
+ },
80
+ sessions: input.sessions,
81
+ findings,
82
+ errors: input.errors,
83
+ gaps: [...P6_GAPS],
84
+ };
85
+ }
86
+ /** Exit code of a report: 0 match, 1 diff, 2 harness error. */
87
+ export function exitCodeOf(report) {
88
+ if (report.summary.errors > 0)
89
+ return 2;
90
+ return report.summary.diffed > 0 ? 1 : 0;
91
+ }
@@ -0,0 +1,31 @@
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
+ import type { SessionEvent } from "@celestea/core";
16
+ export declare const SPEC_THRESHOLD = 8;
17
+ export declare const SPEC_KEEP = 4;
18
+ export declare const SPEC_HEAD_PREFIX = "\u3010\u4E0A\u4E0B\u6587\u538B\u7F29\u3011";
19
+ export declare const SPEC_HEAD_ASSISTANT = "\u4E0A\u4E0B\u6587\u5DF2\u538B\u7F29\uFF0C\u4EE5\u4E0A\u4E3A\u5386\u53F2\u6458\u8981\u3002";
20
+ /** Index ranges [start, end) of every complete turn (`turn_start` .. `turn_end`). */
21
+ export declare function completeTurnRanges(events: readonly SessionEvent[]): Array<[number, number]>;
22
+ /** The expected post-compaction log, or null when the history is too short. */
23
+ export declare function expectedCompactLog(events: readonly SessionEvent[], summary: string, keep?: number): SessionEvent[] | null;
24
+ /** The inner (non-boundary) rows of a turn: what renumbering must NOT touch. */
25
+ export declare function turnBody(events: readonly SessionEvent[]): SessionEvent[];
26
+ /** Split a serialized log into one entry per complete turn (raw text lines). */
27
+ export declare function rawTurnBodies(text: string, parse: (t: string) => {
28
+ events: SessionEvent[];
29
+ }): string[][];
30
+ /** The summary text embedded in a compacted log's head turn (null when absent). */
31
+ export declare function headSummary(events: readonly SessionEvent[]): string | null;