@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.
- package/LICENSE +21 -0
- package/dist/app.d.ts +111 -0
- package/dist/app.js +219 -0
- package/dist/auth/api-token.d.ts +76 -0
- package/dist/auth/api-token.js +172 -0
- package/dist/auth/htpasswd.d.ts +22 -0
- package/dist/auth/htpasswd.js +61 -0
- package/dist/auth/index.d.ts +14 -0
- package/dist/auth/index.js +14 -0
- package/dist/auth/page.d.ts +19 -0
- package/dist/auth/page.js +73 -0
- package/dist/auth/rate-limit.d.ts +27 -0
- package/dist/auth/rate-limit.js +47 -0
- package/dist/auth/token.d.ts +49 -0
- package/dist/auth/token.js +112 -0
- package/dist/config.d.ts +78 -0
- package/dist/config.js +72 -0
- package/dist/deployment.d.ts +92 -0
- package/dist/deployment.js +154 -0
- package/dist/fake-runtime-adapter.d.ts +37 -0
- package/dist/fake-runtime-adapter.js +276 -0
- package/dist/handlers/auth.d.ts +24 -0
- package/dist/handlers/auth.js +155 -0
- package/dist/handlers/common.d.ts +76 -0
- package/dist/handlers/common.js +127 -0
- package/dist/handlers/config-shape.d.ts +76 -0
- package/dist/handlers/config-shape.js +227 -0
- package/dist/handlers/config.d.ts +26 -0
- package/dist/handlers/config.js +161 -0
- package/dist/handlers/context-shape.d.ts +33 -0
- package/dist/handlers/context-shape.js +59 -0
- package/dist/handlers/dialog.d.ts +22 -0
- package/dist/handlers/dialog.js +292 -0
- package/dist/handlers/exec.d.ts +22 -0
- package/dist/handlers/exec.js +131 -0
- package/dist/handlers/fs-read.d.ts +63 -0
- package/dist/handlers/fs-read.js +159 -0
- package/dist/handlers/fs.d.ts +57 -0
- package/dist/handlers/fs.js +153 -0
- package/dist/handlers/grants-shape.d.ts +45 -0
- package/dist/handlers/grants-shape.js +130 -0
- package/dist/handlers/grants.d.ts +21 -0
- package/dist/handlers/grants.js +267 -0
- package/dist/handlers/health.d.ts +37 -0
- package/dist/handlers/health.js +139 -0
- package/dist/handlers/index.d.ts +40 -0
- package/dist/handlers/index.js +85 -0
- package/dist/handlers/permissions.d.ts +9 -0
- package/dist/handlers/permissions.js +183 -0
- package/dist/handlers/plugins.d.ts +18 -0
- package/dist/handlers/plugins.js +23 -0
- package/dist/handlers/prompts.d.ts +12 -0
- package/dist/handlers/prompts.js +120 -0
- package/dist/handlers/providers.d.ts +12 -0
- package/dist/handlers/providers.js +178 -0
- package/dist/handlers/questions.d.ts +23 -0
- package/dist/handlers/questions.js +115 -0
- package/dist/handlers/session-model.d.ts +38 -0
- package/dist/handlers/session-model.js +105 -0
- package/dist/handlers/session-move.d.ts +22 -0
- package/dist/handlers/session-move.js +188 -0
- package/dist/handlers/session-tools.d.ts +19 -0
- package/dist/handlers/session-tools.js +75 -0
- package/dist/handlers/sessions.d.ts +32 -0
- package/dist/handlers/sessions.js +242 -0
- package/dist/handlers/usage.d.ts +26 -0
- package/dist/handlers/usage.js +77 -0
- package/dist/handlers/worker.d.ts +12 -0
- package/dist/handlers/worker.js +82 -0
- package/dist/handlers/workspaces.d.ts +12 -0
- package/dist/handlers/workspaces.js +88 -0
- package/dist/harness.test-util.d.ts +101 -0
- package/dist/harness.test-util.js +176 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +33 -0
- package/dist/main.d.ts +13 -0
- package/dist/main.js +35 -0
- package/dist/plugins.d.ts +105 -0
- package/dist/plugins.js +97 -0
- package/dist/question-registry.d.ts +124 -0
- package/dist/question-registry.js +212 -0
- package/dist/question-rows.d.ts +32 -0
- package/dist/question-rows.js +43 -0
- package/dist/replay/compare.d.ts +47 -0
- package/dist/replay/compare.js +89 -0
- package/dist/replay/e2e-replay.d.ts +46 -0
- package/dist/replay/e2e-replay.js +91 -0
- package/dist/replay/expect-compact.d.ts +31 -0
- package/dist/replay/expect-compact.js +92 -0
- package/dist/replay/fixtures.d.ts +52 -0
- package/dist/replay/fixtures.js +53 -0
- package/dist/replay/host.d.ts +32 -0
- package/dist/replay/host.js +62 -0
- package/dist/replay/index.d.ts +16 -0
- package/dist/replay/index.js +16 -0
- package/dist/replay/probes.d.ts +55 -0
- package/dist/replay/probes.js +225 -0
- package/dist/replay/report.d.ts +13 -0
- package/dist/replay/report.js +103 -0
- package/dist/replay/session-e2e.d.ts +44 -0
- package/dist/replay/session-e2e.js +91 -0
- package/dist/routes.d.ts +47 -0
- package/dist/routes.js +59 -0
- package/dist/runtime/attachments-llm.d.ts +13 -0
- package/dist/runtime/attachments-llm.js +37 -0
- package/dist/runtime/boot-recovery.d.ts +32 -0
- package/dist/runtime/boot-recovery.js +71 -0
- package/dist/runtime/context-snapshot.d.ts +59 -0
- package/dist/runtime/context-snapshot.js +113 -0
- package/dist/runtime/engine-grants.d.ts +121 -0
- package/dist/runtime/engine-grants.js +338 -0
- package/dist/runtime/engine-permissions.d.ts +29 -0
- package/dist/runtime/engine-permissions.js +92 -0
- package/dist/runtime/engine-plugins.d.ts +189 -0
- package/dist/runtime/engine-plugins.js +292 -0
- package/dist/runtime/engine-profile.d.ts +28 -0
- package/dist/runtime/engine-profile.js +88 -0
- package/dist/runtime/engine-session.d.ts +9 -0
- package/dist/runtime/engine-session.js +9 -0
- package/dist/runtime/fallback-contract.d.ts +32 -0
- package/dist/runtime/fallback-contract.js +10 -0
- package/dist/runtime/fallback-host.d.ts +134 -0
- package/dist/runtime/fallback-host.js +299 -0
- package/dist/runtime/host-autowake.d.ts +79 -0
- package/dist/runtime/host-autowake.js +86 -0
- package/dist/runtime/image-downgrade.d.ts +49 -0
- package/dist/runtime/image-downgrade.js +90 -0
- package/dist/runtime/inbox-message.d.ts +11 -0
- package/dist/runtime/inbox-message.js +20 -0
- package/dist/runtime/index.d.ts +17 -0
- package/dist/runtime/index.js +17 -0
- package/dist/runtime/ledger-view.d.ts +40 -0
- package/dist/runtime/ledger-view.js +56 -0
- package/dist/runtime/llm-assembly.d.ts +66 -0
- package/dist/runtime/llm-assembly.js +114 -0
- package/dist/runtime/offline-llm.d.ts +71 -0
- package/dist/runtime/offline-llm.js +132 -0
- package/dist/runtime/provider-target.d.ts +10 -0
- package/dist/runtime/provider-target.js +10 -0
- package/dist/runtime/question-host.d.ts +33 -0
- package/dist/runtime/question-host.js +41 -0
- package/dist/runtime/question-view.d.ts +48 -0
- package/dist/runtime/question-view.js +76 -0
- package/dist/runtime/real-runtime-adapter.d.ts +95 -0
- package/dist/runtime/real-runtime-adapter.js +553 -0
- package/dist/runtime/recovery-audit.d.ts +81 -0
- package/dist/runtime/recovery-audit.js +95 -0
- package/dist/runtime/recovery-view.d.ts +36 -0
- package/dist/runtime/recovery-view.js +63 -0
- package/dist/runtime/session-compose.d.ts +235 -0
- package/dist/runtime/session-compose.js +350 -0
- package/dist/runtime/session-grants.d.ts +36 -0
- package/dist/runtime/session-grants.js +69 -0
- package/dist/runtime/session-lifecycle.d.ts +40 -0
- package/dist/runtime/session-lifecycle.js +86 -0
- package/dist/runtime/session-publisher.d.ts +40 -0
- package/dist/runtime/session-publisher.js +48 -0
- package/dist/runtime/session-release.d.ts +53 -0
- package/dist/runtime/session-release.js +80 -0
- package/dist/runtime/test-util.d.ts +78 -0
- package/dist/runtime/test-util.js +162 -0
- package/dist/runtime/watchdog-view.d.ts +33 -0
- package/dist/runtime/watchdog-view.js +45 -0
- package/dist/runtime/worker-bridge.d.ts +76 -0
- package/dist/runtime/worker-bridge.js +156 -0
- package/dist/runtime/worker-live.d.ts +22 -0
- package/dist/runtime/worker-live.js +25 -0
- package/dist/runtime/worker-recovery.d.ts +35 -0
- package/dist/runtime/worker-recovery.js +64 -0
- package/dist/runtime/worker-table.d.ts +58 -0
- package/dist/runtime/worker-table.js +70 -0
- package/dist/runtime-adapter.d.ts +421 -0
- package/dist/runtime-adapter.js +76 -0
- package/dist/serial-queue.d.ts +16 -0
- package/dist/serial-queue.js +22 -0
- package/dist/server.d.ts +44 -0
- package/dist/server.js +119 -0
- package/dist/settings.d.ts +23 -0
- package/dist/settings.js +31 -0
- package/dist/sse.d.ts +67 -0
- package/dist/sse.js +186 -0
- package/dist/static.d.ts +20 -0
- package/dist/static.js +120 -0
- package/dist/store/builtin-sections.d.ts +52 -0
- package/dist/store/builtin-sections.js +118 -0
- package/dist/store/celestea-home.d.ts +12 -0
- package/dist/store/celestea-home.js +11 -0
- package/dist/store/fs-json.d.ts +51 -0
- package/dist/store/fs-json.js +119 -0
- package/dist/store/grants-audit.d.ts +84 -0
- package/dist/store/grants-audit.js +109 -0
- package/dist/store/grants-service.d.ts +44 -0
- package/dist/store/grants-service.js +41 -0
- package/dist/store/grants-tokens.d.ts +62 -0
- package/dist/store/grants-tokens.js +105 -0
- package/dist/store/grants.d.ts +113 -0
- package/dist/store/grants.js +238 -0
- package/dist/store/index.d.ts +36 -0
- package/dist/store/index.js +36 -0
- package/dist/store/mode.d.ts +35 -0
- package/dist/store/mode.js +41 -0
- package/dist/store/permissions.d.ts +61 -0
- package/dist/store/permissions.js +139 -0
- package/dist/store/prompts-compose.d.ts +56 -0
- package/dist/store/prompts-compose.js +62 -0
- package/dist/store/prompts-template.d.ts +26 -0
- package/dist/store/prompts-template.js +99 -0
- package/dist/store/prompts.d.ts +142 -0
- package/dist/store/prompts.js +273 -0
- package/dist/store/provider-probe.d.ts +81 -0
- package/dist/store/provider-probe.js +154 -0
- package/dist/store/providers.d.ts +97 -0
- package/dist/store/providers.js +219 -0
- package/dist/store/result.d.ts +29 -0
- package/dist/store/result.js +30 -0
- package/dist/store/session-id.d.ts +119 -0
- package/dist/store/session-id.js +220 -0
- package/dist/store/session-meta.d.ts +41 -0
- package/dist/store/session-meta.js +66 -0
- package/dist/store/session-ops.d.ts +109 -0
- package/dist/store/session-ops.js +264 -0
- package/dist/store/session-tools.d.ts +42 -0
- package/dist/store/session-tools.js +69 -0
- package/dist/store/sessions.d.ts +162 -0
- package/dist/store/sessions.js +307 -0
- package/dist/store/validate.d.ts +13 -0
- package/dist/store/validate.js +38 -0
- package/dist/store/workspaces.d.ts +67 -0
- package/dist/store/workspaces.js +228 -0
- package/dist/user-questions.d.ts +58 -0
- package/dist/user-questions.js +157 -0
- package/dist/version.d.ts +14 -0
- package/dist/version.js +52 -0
- package/package.json +38 -0
- package/webdist/assets/index-Bngs7gUm.js +98 -0
- package/webdist/assets/index-Bu6ci_rN.css +1 -0
- package/webdist/assets/katex-CCbS2qAY.js +257 -0
- package/webdist/assets/mhchem-CnUN8HwY.js +1 -0
- package/webdist/build-meta.json +7 -0
- 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[];
|