@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,23 @@
1
+ /**
2
+ * The two user-question endpoints (W783 §9 item 7).
3
+ *
4
+ * GET /api/questions → every question still answerable
5
+ * POST /api/questions/{id}/answer → the human's answer
6
+ *
7
+ * WHY THE ANSWER IS NOT `POST /api/turn`: while the model's `ask_user_question`
8
+ * call is parked, the session's turn slot is occupied. A message posted to
9
+ * `/api/turn` is therefore accepted as STEERING, and steering is only drained at
10
+ * a step boundary — a boundary the parked call never reaches (§2.2). Sending the
11
+ * answer here instead resolves the pending promise the tool is awaiting, so the
12
+ * tool wakes with an ordinary result and the turn continues (§4.2).
13
+ *
14
+ * The `GET` exists for §7 recovery: the `question` SSE frame can be missed (a
15
+ * reload, a dropped connection), so the list is the authoritative rebuild source.
16
+ * Every timing field is judged at READ time, so the client never trusts its own
17
+ * clock and a long-disconnected tab cannot resurrect an expired question.
18
+ */
19
+ import type { Hono } from "hono";
20
+ import type { RouteTable } from "../routes.js";
21
+ import { type Deps } from "./common.js";
22
+ /** The two question routes, in contract order. */
23
+ export declare function registerQuestions(app: Hono, deps: Deps, table: RouteTable): string[];
@@ -0,0 +1,115 @@
1
+ /**
2
+ * The two user-question endpoints (W783 §9 item 7).
3
+ *
4
+ * GET /api/questions → every question still answerable
5
+ * POST /api/questions/{id}/answer → the human's answer
6
+ *
7
+ * WHY THE ANSWER IS NOT `POST /api/turn`: while the model's `ask_user_question`
8
+ * call is parked, the session's turn slot is occupied. A message posted to
9
+ * `/api/turn` is therefore accepted as STEERING, and steering is only drained at
10
+ * a step boundary — a boundary the parked call never reaches (§2.2). Sending the
11
+ * answer here instead resolves the pending promise the tool is awaiting, so the
12
+ * tool wakes with an ordinary result and the turn continues (§4.2).
13
+ *
14
+ * The `GET` exists for §7 recovery: the `question` SSE frame can be missed (a
15
+ * reload, a dropped connection), so the list is the authoritative rebuild source.
16
+ * Every timing field is judged at READ time, so the client never trusts its own
17
+ * clock and a long-disconnected tab cannot resurrect an expired question.
18
+ */
19
+ import { errText } from "../store/result.js";
20
+ import { failJson, readJsonBody, strField } from "./common.js";
21
+ /** Read `answers`: every entry needs a string `id`, a string[] `selected` (optional `custom`). */
22
+ function readAnswers(body) {
23
+ const raw = body["answers"];
24
+ if (!Array.isArray(raw))
25
+ return { ok: false, error: "field 'answers' must be an array" };
26
+ const out = [];
27
+ for (const item of raw) {
28
+ const parsed = readAnswer(item);
29
+ if (typeof parsed === "string")
30
+ return { ok: false, error: parsed };
31
+ out.push(parsed);
32
+ }
33
+ return { ok: true, answers: out };
34
+ }
35
+ /** One answer entry, or the reason it is malformed. */
36
+ function readAnswer(item) {
37
+ if (typeof item !== "object" || item === null || Array.isArray(item))
38
+ return "each answer must be an object {id,selected[],custom?}";
39
+ const record = item;
40
+ if (typeof record["id"] !== "string")
41
+ return "each answer needs a string 'id'";
42
+ const selected = record["selected"];
43
+ if (!Array.isArray(selected) || selected.some((value) => typeof value !== "string")) {
44
+ return `answer '${record["id"]}' needs 'selected' as an array of strings (option LABELS, never indices)`;
45
+ }
46
+ const custom = record["custom"];
47
+ if (custom !== undefined && custom !== null && typeof custom !== "string") {
48
+ return `answer '${record["id"]}' has a non-string 'custom'`;
49
+ }
50
+ const answer = { id: record["id"], selected: selected };
51
+ if (typeof custom === "string")
52
+ answer.custom = custom;
53
+ return answer;
54
+ }
55
+ /** POST /api/questions/{id}/answer — resolve the parked tool call (§4.2). */
56
+ function registerAnswer(app, deps, table) {
57
+ const route = table.get("post_question_answer");
58
+ app.on(route.method, route.honoPath, async (c) => {
59
+ const requestId = c.req.param("id") ?? "";
60
+ const read = await readJsonBody(c);
61
+ if (!read.ok)
62
+ return read.response;
63
+ const answers = readAnswers(read.body);
64
+ if (!answers.ok)
65
+ return failJson(c, 422, answers.error);
66
+ // Optional guard: a stale tab must not answer another session's question by
67
+ // guessing an id (the id alone is not an authorization).
68
+ const guard = strField(c, read.body, "session");
69
+ if (!guard.ok)
70
+ return guard.response;
71
+ const answer = deps.runtime.answerQuestion;
72
+ if (answer === undefined)
73
+ return failJson(c, 404, `unknown or already settled question '${requestId}'`);
74
+ const outcome = answer.call(deps.runtime, requestId, answers.answers, guard.value);
75
+ if (!outcome.ok)
76
+ return refuse(c, requestId, outcome.reason);
77
+ // `timed_out:false` is a fact, not a placeholder: a REAL answer arrived, so
78
+ // the §6.2 race is resolved in the user's favour (whoever arrives first wins).
79
+ return c.json({ ok: true, id: requestId, session: outcome.session, timed_out: false });
80
+ });
81
+ return route.id;
82
+ }
83
+ /** The refusal status of one answer attempt (never a silent 200). */
84
+ function refuse(c, requestId, reason) {
85
+ if (reason === "mismatch")
86
+ return failJson(c, 409, `question '${requestId}' was asked by another session`);
87
+ if (reason === "timed_out" || reason === "settled") {
88
+ return failJson(c, 409, `question '${requestId}' already settled`, { timed_out: reason === "timed_out" });
89
+ }
90
+ return failJson(c, 404, `unknown or already settled question '${requestId}'`);
91
+ }
92
+ /** GET /api/questions — the §7 recovery list (pending questions only). */
93
+ function registerList(app, deps, table) {
94
+ const route = table.get("get_questions");
95
+ app.on(route.method, route.honoPath, (c) => {
96
+ const asked = (c.req.query("session") ?? "").trim();
97
+ const pending = deps.runtime.pendingQuestions;
98
+ if (pending === undefined)
99
+ return c.json({ ok: true, questions: [] });
100
+ try {
101
+ // An ABSENT filter is not the same as filtering by `null`: the latter would
102
+ // return only the detached generation's questions. Passing `undefined`
103
+ // keeps the whole table, which is what a reconnecting client needs.
104
+ return c.json({ ok: true, questions: pending.call(deps.runtime, asked === "" ? undefined : asked) });
105
+ }
106
+ catch (e) {
107
+ return failJson(c, 500, errText(e));
108
+ }
109
+ });
110
+ return route.id;
111
+ }
112
+ /** The two question routes, in contract order. */
113
+ export function registerQuestions(app, deps, table) {
114
+ return [registerList(app, deps, table), registerAnswer(app, deps, table)];
115
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * W870: the session-level MODEL switch endpoint (1).
3
+ *
4
+ * PUT /api/sessions/{id}/model -> rewrite `<session dir>/session.json.model`
5
+ * (key-preserving; `""` CLEARS the override)
6
+ * and invalidate THIS session so its next
7
+ * turn recomposes on the new model.
8
+ *
9
+ * Why the endpoint exists (the user-visible bug it fixes): the statusline badge
10
+ * is `GET /api/status?session=<focus>` polled every 2s, and that answer's `model`
11
+ * is the SESSION INSTANCE's profile — global base + `session.json.model`
12
+ * (`runtime/session-compose.ts` `profileFor`). The W750 picker only wrote the
13
+ * GLOBAL default (`POST /api/config`), so for a session carrying an override the
14
+ * optimistic badge was bounced back by the next poll (「切换模型后几秒又弹回原状」).
15
+ *
16
+ * Product semantic (W870, deliberate): the statusline's picker switches the
17
+ * FOCUSED SESSION's model — it sits on that session's statusline, so that is what
18
+ * the user means. The GLOBAL default stays `POST /api/config` and belongs to the
19
+ * settings page. This endpoint is the session-scoped half.
20
+ *
21
+ * Disciplines, all inherited from the endpoints next to it:
22
+ * - the write path is W729's `session.json` writer (title / mode / prompt are
23
+ * kept), so a switch is the SAME operation `POST /api/sessions {model}` performs;
24
+ * - the busy guard is `/compact` and `POST /api/sessions/{id}/mode`'s: 409 while
25
+ * THIS session's turn runs, because a half-applied switch (this turn on the old
26
+ * model, the next on the new one) is exactly the silent surprise W870 removes;
27
+ * - the effect is W516's `invalidateSession`, the same next-turn-boundary hook
28
+ * `PUT /api/sessions/{id}/tools` and `PUT /api/sessions/{id}/permission` use.
29
+ * Sessions other than this one are untouched.
30
+ *
31
+ * The response echoes the RESOLVED value (`model` = override ?? global default)
32
+ * plus `covered` / `effective.source`, so the client never has to guess whether an
33
+ * override is in force — that is what the picker's 「本会话已固定模型」 hint reads.
34
+ */
35
+ import type { Hono } from "hono";
36
+ import type { RouteTable } from "../routes.js";
37
+ import { type Deps } from "./common.js";
38
+ export declare function registerSessionModel(app: Hono, deps: Deps, table: RouteTable): string;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * W870: the session-level MODEL switch endpoint (1).
3
+ *
4
+ * PUT /api/sessions/{id}/model -> rewrite `<session dir>/session.json.model`
5
+ * (key-preserving; `""` CLEARS the override)
6
+ * and invalidate THIS session so its next
7
+ * turn recomposes on the new model.
8
+ *
9
+ * Why the endpoint exists (the user-visible bug it fixes): the statusline badge
10
+ * is `GET /api/status?session=<focus>` polled every 2s, and that answer's `model`
11
+ * is the SESSION INSTANCE's profile — global base + `session.json.model`
12
+ * (`runtime/session-compose.ts` `profileFor`). The W750 picker only wrote the
13
+ * GLOBAL default (`POST /api/config`), so for a session carrying an override the
14
+ * optimistic badge was bounced back by the next poll (「切换模型后几秒又弹回原状」).
15
+ *
16
+ * Product semantic (W870, deliberate): the statusline's picker switches the
17
+ * FOCUSED SESSION's model — it sits on that session's statusline, so that is what
18
+ * the user means. The GLOBAL default stays `POST /api/config` and belongs to the
19
+ * settings page. This endpoint is the session-scoped half.
20
+ *
21
+ * Disciplines, all inherited from the endpoints next to it:
22
+ * - the write path is W729's `session.json` writer (title / mode / prompt are
23
+ * kept), so a switch is the SAME operation `POST /api/sessions {model}` performs;
24
+ * - the busy guard is `/compact` and `POST /api/sessions/{id}/mode`'s: 409 while
25
+ * THIS session's turn runs, because a half-applied switch (this turn on the old
26
+ * model, the next on the new one) is exactly the silent surprise W870 removes;
27
+ * - the effect is W516's `invalidateSession`, the same next-turn-boundary hook
28
+ * `PUT /api/sessions/{id}/tools` and `PUT /api/sessions/{id}/permission` use.
29
+ * Sessions other than this one are untouched.
30
+ *
31
+ * The response echoes the RESOLVED value (`model` = override ?? global default)
32
+ * plus `covered` / `effective.source`, so the client never has to guess whether an
33
+ * override is in force — that is what the picker's 「本会话已固定模型」 hint reads.
34
+ */
35
+ import { readSessionMeta, writeSessionMeta } from "../store/session-meta.js";
36
+ import { errText } from "../store/result.js";
37
+ import { validateModelName } from "../store/validate.js";
38
+ import { failJson, readJsonBody, strField, storeFail } from "./common.js";
39
+ /** The frozen response body (one shape, one construction).
40
+ *
41
+ * `model` is the model this session will run next turn; `covered` says whether
42
+ * that comes from the session's OWN `session.json.model` (`effective.source`
43
+ * `"session"`) or from the global default (`"global"`, i.e. no override).
44
+ */
45
+ function sessionModelBody(deps, session, dir) {
46
+ const override = (readSessionMeta(dir)?.model ?? "").trim();
47
+ const covered = override !== "";
48
+ const base = deps.runtime.profile().model;
49
+ return {
50
+ ok: true,
51
+ session,
52
+ model: covered ? override : base,
53
+ covered,
54
+ effective: { model: covered ? override : base, base_model: base, source: covered ? "session" : "global", next_turn: true },
55
+ };
56
+ }
57
+ export function registerSessionModel(app, deps, table) {
58
+ const route = table.get("put_session_model");
59
+ app.on(route.method, route.honoPath, async (c) => {
60
+ // W815-5: resolve to the CANONICAL id BEFORE the busy guard (`require` trims
61
+ // and sanitizes the raw path segment, so a `%2F`-encoded id would otherwise
62
+ // never match the canonical instance and the guard could be bypassed).
63
+ const resolved = deps.sessions.require(c.req.param("id") ?? "");
64
+ if (!resolved.ok)
65
+ return storeFail(c, resolved);
66
+ const session = resolved.value.id;
67
+ if (deps.runtime.isBusy(session))
68
+ return failJson(c, 409, "turn 进行中,无法切换模型");
69
+ const read = await readJsonBody(c);
70
+ if (!read.ok)
71
+ return read.response;
72
+ const model = strField(c, read.body, "model");
73
+ if (!model.ok)
74
+ return model.response;
75
+ if (model.value === undefined)
76
+ return failJson(c, 422, "field 'model' must be a string");
77
+ const asked = model.value.trim();
78
+ // `""` (or whitespace) is the documented CLEAR: the session falls back to the
79
+ // global default. Anything else has to be a legal model id.
80
+ if (asked !== "") {
81
+ const bad = validateModelName(asked);
82
+ if (bad !== null)
83
+ return failJson(c, 400, bad);
84
+ }
85
+ try {
86
+ // W729 write path, key-preserving: title / mode / prompt survive the switch.
87
+ // An empty model DROPS the key (K8: absent = no override) instead of writing
88
+ // `"model": ""` — the composer's `sessionModel` hook reads the absent key as
89
+ // 「no override」 exactly like the empty string.
90
+ const meta = readSessionMeta(resolved.value.dir) ?? {};
91
+ if (asked === "")
92
+ delete meta.model;
93
+ else
94
+ meta.model = asked;
95
+ writeSessionMeta(resolved.value.dir, meta);
96
+ }
97
+ catch (e) {
98
+ return failJson(c, 500, `meta write failed: ${errText(e)}`);
99
+ }
100
+ // W516/W860: recompose THIS session at its next boundary (nothing else is touched).
101
+ deps.runtime.invalidateSession?.(session);
102
+ return c.json(sessionModelBody(deps, session, resolved.value.dir));
103
+ });
104
+ return route.id;
105
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Session endpoints, part 2: rename / branch / compact / archive / unarchive /
3
+ * batch-archive / batch-delete (`src/workspaces.rs:1382-1454,1580-1650`,
4
+ * `src/compact.rs:505-519`).
5
+ *
6
+ * Renaming the ACTIVE session takes the busy slot and re-persists
7
+ * `active_session`; archiving moves the directory into `.celestea-archived/`
8
+ * (id-preserving, reversible) while deleting moves it into `.celestea-trash/`
9
+ * with a timestamp suffix (recoverable, no longer addressable by id).
10
+ *
11
+ * W794 (裁决: the active marker is a state label, not a protection): archiving or
12
+ * deleting the ACTIVE session SUCCEEDS, and the two operations that take a
13
+ * session's directory away first call `releaseSession` — the engine's own
14
+ * cut-then-detach — so the model response in flight for that session is aborted
15
+ * (the same cooperative cancel `POST /api/cancel` sends) and its instance is
16
+ * released before the directory moves. Every id of a batch is cut the same way,
17
+ * and the response contract is untouched: still one 200 with per-id `failed[]`.
18
+ */
19
+ import type { Hono } from "hono";
20
+ import type { RouteTable } from "../routes.js";
21
+ import { type Deps } from "./common.js";
22
+ export declare function registerSessionMoves(app: Hono, deps: Deps, table: RouteTable): string[];
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Session endpoints, part 2: rename / branch / compact / archive / unarchive /
3
+ * batch-archive / batch-delete (`src/workspaces.rs:1382-1454,1580-1650`,
4
+ * `src/compact.rs:505-519`).
5
+ *
6
+ * Renaming the ACTIVE session takes the busy slot and re-persists
7
+ * `active_session`; archiving moves the directory into `.celestea-archived/`
8
+ * (id-preserving, reversible) while deleting moves it into `.celestea-trash/`
9
+ * with a timestamp suffix (recoverable, no longer addressable by id).
10
+ *
11
+ * W794 (裁决: the active marker is a state label, not a protection): archiving or
12
+ * deleting the ACTIVE session SUCCEEDS, and the two operations that take a
13
+ * session's directory away first call `releaseSession` — the engine's own
14
+ * cut-then-detach — so the model response in flight for that session is aborted
15
+ * (the same cooperative cancel `POST /api/cancel` sends) and its instance is
16
+ * released before the directory moves. Every id of a batch is cut the same way,
17
+ * and the response contract is untouched: still one 200 with per-id `failed[]`.
18
+ */
19
+ import { EngineError } from "../runtime-adapter.js";
20
+ import { strArrayField, strField, readJsonBody, failJson, storeFail } from "./common.js";
21
+ function isActive(deps, id) {
22
+ return deps.workspaces.activeSession() === id.trim();
23
+ }
24
+ /**
25
+ * W794: the engine-side half of taking a session's directory away.
26
+ *
27
+ * `releaseSession` (when the injected adapter has one) aborts the session's
28
+ * in-flight model response and disposes THAT session's instance; it is a no-op
29
+ * for an id with no live instance, which is why it is safe to call for every id
30
+ * of a batch — including the unknown ones, whose per-id `failed[]` row below is
31
+ * exactly what the contract promises.
32
+ *
33
+ * A failure inside the engine must not turn a deletion into a 5xx/failed row:
34
+ * the removal (the directory move) is what the caller asked for and what the
35
+ * per-id result reports. The instance is released — or, when the teardown threw
36
+ * halfway, recomposed on demand by the next activate/turn — either way the
37
+ * session is gone from the listing, which is the observable contract.
38
+ */
39
+ async function cutEngineSession(deps, id) {
40
+ try {
41
+ await deps.runtime.releaseSession?.(id);
42
+ }
43
+ catch {
44
+ // Swallowed on purpose — see above.
45
+ }
46
+ }
47
+ function registerRename(app, deps, table) {
48
+ const route = table.get("post_session_rename");
49
+ app.on(route.method, route.honoPath, async (c) => {
50
+ // W815-5: canonical id BEFORE the busy guard (raw `%2F`/`%20` segments
51
+ // bypassed the guard because the runtime keys instances by canonical id).
52
+ const resolved = deps.sessions.require(c.req.param("id") ?? "");
53
+ if (!resolved.ok)
54
+ return storeFail(c, resolved);
55
+ const session = resolved.value.id;
56
+ if (deps.runtime.isBusy(session))
57
+ return failJson(c, 409, "turn in progress; rename applies between turns");
58
+ const read = await readJsonBody(c);
59
+ if (!read.ok)
60
+ return read.response;
61
+ const title = strField(c, read.body, "new_title");
62
+ if (!title.ok)
63
+ return title.response;
64
+ const res = deps.sessionOps.rename(session, title.value ?? "");
65
+ if (!res.ok)
66
+ return storeFail(c, res);
67
+ if (isActive(deps, session)) {
68
+ const saved = deps.workspaces.setActiveSession(res.value);
69
+ if (!saved.ok)
70
+ return failJson(c, 500, `cannot persist active session: ${saved.error}`);
71
+ }
72
+ return c.json({ ok: true, id: res.value });
73
+ });
74
+ return route.id;
75
+ }
76
+ function registerBranch(app, deps, table) {
77
+ const route = table.get("post_session_branch");
78
+ app.on(route.method, route.honoPath, async (c) => {
79
+ const read = await readJsonBody(c, false);
80
+ if (!read.ok)
81
+ return read.response;
82
+ const title = strField(c, read.body, "title");
83
+ if (!title.ok)
84
+ return title.response;
85
+ const res = deps.sessionOps.branch(c.req.param("id") ?? "", title.value);
86
+ if (!res.ok)
87
+ return storeFail(c, res);
88
+ return c.json({ ok: true, id: res.value });
89
+ });
90
+ return route.id;
91
+ }
92
+ function registerCompact(app, deps, table) {
93
+ const route = table.get("post_session_compact");
94
+ app.on(route.method, route.honoPath, async (c) => {
95
+ // W815-5: canonical id before the two guards.
96
+ const resolved = deps.sessions.require(c.req.param("id") ?? "");
97
+ if (!resolved.ok)
98
+ return storeFail(c, resolved);
99
+ const session = resolved.value.id;
100
+ if (deps.runtime.isBusy(session))
101
+ return failJson(c, 409, "turn 进行中,无法压缩");
102
+ // W825 P0: a session with LIVE worker work is PINNED — its instance may not
103
+ // be evicted, so compacting it would rewrite the log under a live descriptor
104
+ // (the old fd survives the rename and every later append is lost). Refuse up
105
+ // front, exactly like the busy guard above; the lifecycle refuses again if a
106
+ // worker appears between this check and the eviction. `workerSessions()` only
107
+ // reports LIVE instances, which is exactly when a descriptor can be orphaned.
108
+ if (deps.runtime.workerSessions().some((row) => row.host_session === session && row.status === "RUNNING")) {
109
+ return failJson(c, 409, "worker 进行中,无法压缩");
110
+ }
111
+ try {
112
+ const out = await deps.runtime.compact(session);
113
+ deps.bus.emit("compact", 0, { session: out.session, kept_turns: out.kept_turns ?? 0, note: out.note, rebound: out.rebound });
114
+ const body = { ok: true, compacted: out.compacted, note: out.note };
115
+ if (out.compacted)
116
+ body["kept_turns"] = out.kept_turns ?? 0;
117
+ return c.json(body);
118
+ }
119
+ catch (e) {
120
+ return failJson(c, 500, e instanceof EngineError ? e.message : String(e));
121
+ }
122
+ });
123
+ return route.id;
124
+ }
125
+ function registerMove(app, deps, table, id) {
126
+ const route = table.get(id);
127
+ app.on(route.method, route.honoPath, async (c) => {
128
+ const raw = c.req.param("id") ?? "";
129
+ // W815-5: the engine cut must name the CANONICAL session. `unarchive` may
130
+ // address an id whose live dir is absent, so `resolve` (not `require`) is the
131
+ // right canonicalization; an unresolvable id falls through to the store's own
132
+ // (identical) error.
133
+ const resolved = deps.sessions.resolve(raw);
134
+ const target = resolved.ok ? resolved.value.id : raw;
135
+ // W794: archiving a LIVE session moves its directory away too, so it cuts the
136
+ // same way a delete does. `unarchive` restores a directory that has no live
137
+ // instance by construction (archiving released it) — nothing to cut.
138
+ if (id === "post_session_archive")
139
+ await cutEngineSession(deps, target);
140
+ const res = id === "post_session_archive" ? deps.sessionOps.archive(target) : deps.sessionOps.unarchive(target);
141
+ if (!res.ok)
142
+ return storeFail(c, res);
143
+ return c.json({ ok: true });
144
+ });
145
+ return route.id;
146
+ }
147
+ function registerBatch(app, deps, table, id) {
148
+ const route = table.get(id);
149
+ app.on(route.method, route.honoPath, async (c) => {
150
+ const read = await readJsonBody(c);
151
+ if (!read.ok)
152
+ return read.response;
153
+ const ids = strArrayField(c, read.body, "ids");
154
+ if (!ids.ok)
155
+ return ids.response;
156
+ if (ids.value === undefined)
157
+ return failJson(c, 422, "field 'ids' must be an array of strings");
158
+ // W794: cut every id before any directory moves. The response shape is
159
+ // unchanged — one 200, `deleted`/`archived` count plus the per-id `failed[]`
160
+ // (unknown ids included), so the client's optimistic update can rely on it.
161
+ // W815-5: cut each id by its CANONICAL form; the store still receives the
162
+ // caller's ids so the per-id `failed[]` row keeps the id the caller sent.
163
+ const canonical = ids.value.map((one) => {
164
+ const resolved = deps.sessions.resolve(one);
165
+ return resolved.ok ? resolved.value.id : one;
166
+ });
167
+ for (const one of canonical)
168
+ await cutEngineSession(deps, one);
169
+ if (id === "post_sessions_batch_archive") {
170
+ const out = deps.sessionOps.batchArchive(ids.value);
171
+ return c.json({ ok: true, archived: out.archived, failed: out.failed });
172
+ }
173
+ const out = deps.sessionOps.batchDelete(ids.value);
174
+ return c.json({ ok: true, deleted: out.deleted, failed: out.failed });
175
+ });
176
+ return route.id;
177
+ }
178
+ export function registerSessionMoves(app, deps, table) {
179
+ return [
180
+ registerRename(app, deps, table),
181
+ registerBranch(app, deps, table),
182
+ registerCompact(app, deps, table),
183
+ registerMove(app, deps, table, "post_session_archive"),
184
+ registerMove(app, deps, table, "post_session_unarchive"),
185
+ registerBatch(app, deps, table, "post_sessions_batch_archive"),
186
+ registerBatch(app, deps, table, "post_sessions_batch_delete"),
187
+ ];
188
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * W860: the session-level TOOL SWITCH endpoints (2).
3
+ *
4
+ * GET /api/sessions/{id}/tools -> the stored disabled list + the EFFECTIVE
5
+ * deny (preset `toolDeny` unioned with it);
6
+ * PUT /api/sessions/{id}/tools -> replace the disabled list (validated,
7
+ * normalized, atomic 0600) and invalidate the
8
+ * session — the SAME next-turn-boundary hook
9
+ * `PUT /api/sessions/{id}/permission` uses.
10
+ *
11
+ * Storage is `store/session-tools.ts`; the union itself lives in the engine's
12
+ * ONE grant reader (`runtime/engine-grants.ts`), so this module is the HTTP face
13
+ * only and the reported `effective.toolDeny` is read back through that reader —
14
+ * the endpoint cannot drift from what the next turn will actually offer.
15
+ */
16
+ import type { Hono } from "hono";
17
+ import type { RouteTable } from "../routes.js";
18
+ import { type Deps } from "./common.js";
19
+ export declare function registerSessionTools(app: Hono, deps: Deps, table: RouteTable): string[];
@@ -0,0 +1,75 @@
1
+ /**
2
+ * W860: the session-level TOOL SWITCH endpoints (2).
3
+ *
4
+ * GET /api/sessions/{id}/tools -> the stored disabled list + the EFFECTIVE
5
+ * deny (preset `toolDeny` unioned with it);
6
+ * PUT /api/sessions/{id}/tools -> replace the disabled list (validated,
7
+ * normalized, atomic 0600) and invalidate the
8
+ * session — the SAME next-turn-boundary hook
9
+ * `PUT /api/sessions/{id}/permission` uses.
10
+ *
11
+ * Storage is `store/session-tools.ts`; the union itself lives in the engine's
12
+ * ONE grant reader (`runtime/engine-grants.ts`), so this module is the HTTP face
13
+ * only and the reported `effective.toolDeny` is read back through that reader —
14
+ * the endpoint cannot drift from what the next turn will actually offer.
15
+ */
16
+ import { effectiveGrantsOf } from "../runtime/engine-grants.js";
17
+ import { nowSec } from "../store/grants-service.js";
18
+ import { errText } from "../store/result.js";
19
+ import { readSessionTools, writeSessionTools } from "../store/session-tools.js";
20
+ import { failJson, readJsonBody, storeFail } from "./common.js";
21
+ /** The frozen response body of both endpoints (one shape, one construction). */
22
+ function sessionToolsBody(deps, session, dir) {
23
+ const read = readSessionTools(dir, session);
24
+ const effective = effectiveGrantsOf(dir, session, deps.grants.env, nowSec(deps.grants));
25
+ const warnings = [...new Set([...read.warnings, ...effective.warnings])];
26
+ return {
27
+ ok: true,
28
+ session,
29
+ disabled: [...read.disabled],
30
+ effective: { toolDeny: [...effective.grants.toolDeny] },
31
+ ...(warnings.length === 0 ? {} : { warnings }),
32
+ };
33
+ }
34
+ function registerGetSessionTools(app, deps, table) {
35
+ const route = table.get("get_session_tools");
36
+ app.on(route.method, route.honoPath, (c) => {
37
+ const resolved = deps.sessions.require(c.req.param("id") ?? "");
38
+ if (!resolved.ok)
39
+ return storeFail(c, resolved);
40
+ return c.json(sessionToolsBody(deps, resolved.value.id, resolved.value.dir));
41
+ });
42
+ return route.id;
43
+ }
44
+ function registerPutSessionTools(app, deps, table) {
45
+ const route = table.get("put_session_tools");
46
+ app.on(route.method, route.honoPath, async (c) => {
47
+ const resolved = deps.sessions.require(c.req.param("id") ?? "");
48
+ if (!resolved.ok)
49
+ return storeFail(c, resolved);
50
+ const body = await readJsonBody(c);
51
+ if (!body.ok)
52
+ return body.response;
53
+ const raw = body.body["disabled"];
54
+ if (!Array.isArray(raw) || raw.some((name) => typeof name !== "string")) {
55
+ return failJson(c, 422, "field 'disabled' must be an array of strings");
56
+ }
57
+ if (raw.some((name) => name.trim() === "")) {
58
+ return failJson(c, 422, "field 'disabled' must not contain an empty tool name");
59
+ }
60
+ try {
61
+ writeSessionTools(resolved.value.dir, resolved.value.id, raw, nowSec(deps.grants));
62
+ }
63
+ catch (e) {
64
+ return failJson(c, 500, "cannot persist session tools: " + errText(e));
65
+ }
66
+ // W860: recompose the session at the next boundary — the same hook the
67
+ // permission switch and the grants writer use (nothing else is touched).
68
+ deps.runtime.invalidateSession?.(resolved.value.id);
69
+ return c.json(sessionToolsBody(deps, resolved.value.id, resolved.value.dir));
70
+ });
71
+ return route.id;
72
+ }
73
+ export function registerSessionTools(app, deps, table) {
74
+ return [registerGetSessionTools(app, deps, table), registerPutSessionTools(app, deps, table)];
75
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Session endpoints, part 1: list / create / transcript / activate / context.
3
+ *
4
+ * `GET /api/sessions` merges two sources: session directories across every
5
+ * registered workspace, plus the engine's in-memory worker sessions
6
+ * (`worker:<sid>`, pseudo-workspace "engine", `kind:"worker"`), sorted by id.
7
+ * W513: every row carries `kind` (`session` | `worker`) and `busy` (that
8
+ * session's own turn slot), and worker rows carry `wid` / `status` / `state` /
9
+ * `host_session`, so the UI can list and open them.
10
+ *
11
+ * `POST /api/sessions/{id}/activate` is "open this view + make sure the session
12
+ * HAS a runtime": it composes the instance on demand, persists the active
13
+ * session as a view preference, and NEVER returns 409 — a session that is
14
+ * already running is perfectly fine (that is the point of session independence).
15
+ *
16
+ * W791 (P1): `POST /api/sessions/{id}/mode` switches the session's working mode
17
+ * (the W729 `session.json.mode`, rewritten through the same writer) at a turn
18
+ * boundary — see `registerMode`.
19
+ *
20
+ * W791 (B): `GET /api/sessions?archived=1` lists the ARCHIVED sessions (the
21
+ * `<ws>/.celestea-archived/` rows `list()` skips); the default body is unchanged.
22
+ *
23
+ * W725: `GET /api/sessions/{id}/context` is the read-only "what does the model
24
+ * actually see" snapshot. The body is assembled by the ENGINE (the agent loop's
25
+ * own `buildRequest`, reached through `runtime.sessionContext`) and the usage
26
+ * block is the statusline's existing `context_usage`口径 — this handler adds
27
+ * only the 20k-per-entry wire guard.
28
+ */
29
+ import type { Hono } from "hono";
30
+ import type { RouteTable } from "../routes.js";
31
+ import { type Deps } from "./common.js";
32
+ export declare function registerSessions(app: Hono, deps: Deps, table: RouteTable): string[];