@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,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared test harness for the Studio HTTP layer.
|
|
3
|
+
*
|
|
4
|
+
* Every test gets a THROWAWAY data directory (workspaces/providers/prompts +
|
|
5
|
+
* one registered workspace holding one session) and a throwaway static root, so
|
|
6
|
+
* no test can read or write a production data file, and the fake runtime adapter
|
|
7
|
+
* keeps the engine seam deterministic.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import { canonicalScopeHash } from "./store/grants.js";
|
|
13
|
+
import { createStudioApp } from "./app.js";
|
|
14
|
+
import { createFakeRuntimeAdapter } from "./fake-runtime-adapter.js";
|
|
15
|
+
import { loadStudioConfig } from "./config.js";
|
|
16
|
+
/**
|
|
17
|
+
* A fake adapter that always reports ITS BUSY SLOT as taken: every session is
|
|
18
|
+
* busy, so the process-wide guards (config / prompts / providers) 409 while
|
|
19
|
+
* `/api/turn` takes the interjection path and activate reports `busy:true`.
|
|
20
|
+
* A `Proxy` is used because spreading a class instance would drop its methods.
|
|
21
|
+
*/
|
|
22
|
+
export function busyRuntime(base = createFakeRuntimeAdapter()) {
|
|
23
|
+
return new Proxy(base, {
|
|
24
|
+
get(target, prop, receiver) {
|
|
25
|
+
if (prop === "isBusy")
|
|
26
|
+
return () => true;
|
|
27
|
+
if (prop === "ensureSession")
|
|
28
|
+
return () => ({ runtime: "created", busy: true, rebuilt: false });
|
|
29
|
+
if (prop === "inject")
|
|
30
|
+
return (req) => {
|
|
31
|
+
// W847: the proxy mirrors the real/fake decision table so a queue test
|
|
32
|
+
// can drive the handler; the omitted/default request stays steering.
|
|
33
|
+
const steering = req.mode !== "queue";
|
|
34
|
+
return { turn: 0, injected: steering, pending: 1, placement: steering ? "steering" : "queued", duplicate: false };
|
|
35
|
+
};
|
|
36
|
+
if (prop === "busySessions")
|
|
37
|
+
return () => target.liveSessions();
|
|
38
|
+
return Reflect.get(target, prop, receiver);
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
export const FIXED_NOW = 1_700_000_000_000;
|
|
43
|
+
export const FIXED_TS = "1700000000.0";
|
|
44
|
+
function plantFiles(root, files) {
|
|
45
|
+
for (const [rel, value] of Object.entries(files)) {
|
|
46
|
+
const path = join(root, rel);
|
|
47
|
+
mkdirSync(join(path, ".."), { recursive: true });
|
|
48
|
+
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, typeof value === "string" ? {} : { mode: 0o644 });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Write raw content at `rel` under `root`, creating the parent directory. */
|
|
52
|
+
function plantRawFiles(root, files) {
|
|
53
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
54
|
+
const path = join(root, rel);
|
|
55
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
56
|
+
writeFileSync(path, content);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function plantSession(workspace, session) {
|
|
60
|
+
if (session === undefined)
|
|
61
|
+
return;
|
|
62
|
+
const dir = join(workspace, session.name);
|
|
63
|
+
mkdirSync(dir, { recursive: true });
|
|
64
|
+
writeFileSync(join(dir, "cli-main.jsonl"), session.log ?? "");
|
|
65
|
+
if (session.meta !== undefined)
|
|
66
|
+
writeFileSync(join(dir, "session.json"), JSON.stringify(session.meta));
|
|
67
|
+
}
|
|
68
|
+
export function makeHarness(opts = {}) {
|
|
69
|
+
const root = mkdtempSync(join(tmpdir(), "studio-"));
|
|
70
|
+
const workspace = join(root, "sample-ws");
|
|
71
|
+
const staticRoot = join(root, "dist");
|
|
72
|
+
mkdirSync(workspace, { recursive: true });
|
|
73
|
+
mkdirSync(join(staticRoot, "assets"), { recursive: true });
|
|
74
|
+
writeFileSync(join(staticRoot, "index.html"), "<!doctype html><title>studio</title>\n");
|
|
75
|
+
writeFileSync(join(staticRoot, "assets", "app.js"), "export const x = 1;\n");
|
|
76
|
+
writeFileSync(join(staticRoot, "secret.txt"), "TOP-SECRET-STATIC\n");
|
|
77
|
+
writeFileSync(join(root, "workspaces.json"), JSON.stringify({ workspaces: [{ path: workspace }], active_session: null }, null, 2));
|
|
78
|
+
plantFiles(root, opts.files ?? {});
|
|
79
|
+
plantRawFiles(root, opts.rawFiles ?? {});
|
|
80
|
+
plantSession(workspace, opts.session);
|
|
81
|
+
const config = loadStudioConfig({
|
|
82
|
+
cwd: root,
|
|
83
|
+
env: {},
|
|
84
|
+
paths: { staticRoot, ...(opts.paths ?? {}), ...(opts.config?.paths ?? {}) },
|
|
85
|
+
});
|
|
86
|
+
const runtime = opts.runtime ?? createFakeRuntimeAdapter({ profile: { model: "test-model" } });
|
|
87
|
+
const studio = createStudioApp({
|
|
88
|
+
config,
|
|
89
|
+
runtime: opts.engineFactory ?? runtime,
|
|
90
|
+
now: () => FIXED_NOW,
|
|
91
|
+
...(opts.env === undefined ? {} : { env: opts.env }),
|
|
92
|
+
});
|
|
93
|
+
return {
|
|
94
|
+
app: studio.app,
|
|
95
|
+
studio,
|
|
96
|
+
runtime: studio.services.runtime,
|
|
97
|
+
root,
|
|
98
|
+
workspace,
|
|
99
|
+
staticRoot,
|
|
100
|
+
cleanup: () => rmSync(root, { recursive: true, force: true }),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* W864: the built-in `full-access` preset now also opens every path
|
|
105
|
+
* (`allPaths`), so a test that wants to observe the PATH/grants boundary must
|
|
106
|
+
* pin a baseline that is path-limited. This plants exactly that — network on,
|
|
107
|
+
* workspace writable, `allPaths` off — as a custom preset in <data dir> and
|
|
108
|
+
* selects it for the session, through the same two files the engine reads
|
|
109
|
+
* (`store/permissions.ts`, `runtime/engine-permissions.ts`).
|
|
110
|
+
*/
|
|
111
|
+
export const PATH_ONLY_PRESET = {
|
|
112
|
+
id: "w864-path-only",
|
|
113
|
+
label: "path only",
|
|
114
|
+
network: true,
|
|
115
|
+
workspaceWritable: true,
|
|
116
|
+
toolRootsWritable: false,
|
|
117
|
+
writeRoots: [],
|
|
118
|
+
allPaths: false,
|
|
119
|
+
unsandboxed: false,
|
|
120
|
+
toolDeny: [],
|
|
121
|
+
};
|
|
122
|
+
export function pinPathOnly(h, name = "s1") {
|
|
123
|
+
writeFileSync(join(h.root, "permissions.json"), JSON.stringify({ version: 1, updated_at: 0, presets: [PATH_ONLY_PRESET] }));
|
|
124
|
+
writeFileSync(join(h.workspace, name, "permission.json"), JSON.stringify({ version: 1, session: `sample-ws/${name}`, preset: PATH_ONLY_PRESET.id, updated_at: 0 }));
|
|
125
|
+
}
|
|
126
|
+
export function jsonRequest(method, body) {
|
|
127
|
+
if (body === undefined)
|
|
128
|
+
return { method };
|
|
129
|
+
return { method, headers: { "content-type": "application/json" }, body: JSON.stringify(body) };
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* W516 grant helpers: a grant needs a one-shot token from the same-origin
|
|
133
|
+
* token endpoint, so every test drives the same two-step handshake the UI does.
|
|
134
|
+
*/
|
|
135
|
+
export async function grantToken(h, id, cap, scope) {
|
|
136
|
+
const hash = canonicalScopeHash(cap, scope);
|
|
137
|
+
const res = await getJson(h.app, `/api/sessions/${id}/grants/confirm-token?cap=${cap}&scope_hash=${hash}`, {
|
|
138
|
+
headers: { "sec-fetch-site": "same-origin" },
|
|
139
|
+
});
|
|
140
|
+
// An unsupported cap has no token to issue; the POST's own 400 still wins
|
|
141
|
+
// because the body is validated before the token is looked at.
|
|
142
|
+
return res.status === 200 ? String(res.body["token"]) : "";
|
|
143
|
+
}
|
|
144
|
+
/** POST a grant (token minted automatically unless one is passed / `null`). */
|
|
145
|
+
export async function grant(h, id, body, token) {
|
|
146
|
+
const headers = { "content-type": "application/json" };
|
|
147
|
+
const scope = (body["scope"] ?? {});
|
|
148
|
+
const used = token === null ? null : (token ?? (await grantToken(h, id, String(body["cap"]), scope)));
|
|
149
|
+
if (used !== null)
|
|
150
|
+
headers["x-celestea-grant-confirm"] = used;
|
|
151
|
+
return getJson(h.app, `/api/sessions/${id}/grants`, { method: "POST", headers, body: JSON.stringify(body) });
|
|
152
|
+
}
|
|
153
|
+
/** The local (authoritative) grants audit channel of a harness data dir. */
|
|
154
|
+
export function auditLines(h) {
|
|
155
|
+
const path = join(h.root, "grants-audit.jsonl");
|
|
156
|
+
if (!existsSync(path))
|
|
157
|
+
return [];
|
|
158
|
+
return readFileSync(path, "utf8")
|
|
159
|
+
.split("\n")
|
|
160
|
+
.filter((line) => line.trim() !== "")
|
|
161
|
+
.map((line) => JSON.parse(line));
|
|
162
|
+
}
|
|
163
|
+
export async function getJson(app, path, init) {
|
|
164
|
+
const res = await app.request(path, init);
|
|
165
|
+
const text = await res.text();
|
|
166
|
+
let body = {};
|
|
167
|
+
try {
|
|
168
|
+
body = JSON.parse(text);
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
body = { raw: text };
|
|
172
|
+
}
|
|
173
|
+
return { status: res.status, body };
|
|
174
|
+
}
|
|
175
|
+
/** W885: file permission bits are meaningful (false on Windows; W883 E10). */
|
|
176
|
+
export const FILE_MODES_MEANINGFUL = process.platform !== "win32";
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@celestea/studio` — the L3 host application (Hono HTTP layer + data stores).
|
|
3
|
+
*
|
|
4
|
+
* Module map:
|
|
5
|
+
* app.ts createStudioApp: compose + 39 routes + static/SPA
|
|
6
|
+
* routes.ts the frozen contract route table (id -> method+path)
|
|
7
|
+
* config.ts host paths + the constants the legacy backend hardcoded
|
|
8
|
+
* runtime-adapter.ts the ONE engine seam (RuntimeAdapter interface)
|
|
9
|
+
* fake-runtime-adapter.ts scripted P4 stand-in for the engine (tests)
|
|
10
|
+
* runtime/ the REAL engine (packages/runtime + offline LLM seam)
|
|
11
|
+
* replay/ P5 double-run comparison harness (pnpm replay:e2e)
|
|
12
|
+
* sse.ts SSE bus: envelope / 8 events / lagged degradation
|
|
13
|
+
* static.ts read-only Vite build + traversal hardening
|
|
14
|
+
* plugins.ts compose root: store plugins -> Context services
|
|
15
|
+
* settings.ts host-side system_prompt / base_url overrides
|
|
16
|
+
* handlers/ one module per endpoint group
|
|
17
|
+
* store/ data stores (workspaces / providers / prompts)
|
|
18
|
+
* auth/ the login-cookie gate + H's self-cert API token
|
|
19
|
+
*/
|
|
20
|
+
export * from "./routes.js";
|
|
21
|
+
export * from "./auth/index.js";
|
|
22
|
+
export * from "./config.js";
|
|
23
|
+
export * from "./runtime-adapter.js";
|
|
24
|
+
export * from "./fake-runtime-adapter.js";
|
|
25
|
+
export * from "./runtime/index.js";
|
|
26
|
+
export * from "./replay/index.js";
|
|
27
|
+
export * from "./sse.js";
|
|
28
|
+
export * from "./static.js";
|
|
29
|
+
export * from "./settings.js";
|
|
30
|
+
export * from "./plugins.js";
|
|
31
|
+
export * from "./app.js";
|
|
32
|
+
export * from "./server.js";
|
|
33
|
+
export * from "./store/index.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@celestea/studio` — the L3 host application (Hono HTTP layer + data stores).
|
|
3
|
+
*
|
|
4
|
+
* Module map:
|
|
5
|
+
* app.ts createStudioApp: compose + 39 routes + static/SPA
|
|
6
|
+
* routes.ts the frozen contract route table (id -> method+path)
|
|
7
|
+
* config.ts host paths + the constants the legacy backend hardcoded
|
|
8
|
+
* runtime-adapter.ts the ONE engine seam (RuntimeAdapter interface)
|
|
9
|
+
* fake-runtime-adapter.ts scripted P4 stand-in for the engine (tests)
|
|
10
|
+
* runtime/ the REAL engine (packages/runtime + offline LLM seam)
|
|
11
|
+
* replay/ P5 double-run comparison harness (pnpm replay:e2e)
|
|
12
|
+
* sse.ts SSE bus: envelope / 8 events / lagged degradation
|
|
13
|
+
* static.ts read-only Vite build + traversal hardening
|
|
14
|
+
* plugins.ts compose root: store plugins -> Context services
|
|
15
|
+
* settings.ts host-side system_prompt / base_url overrides
|
|
16
|
+
* handlers/ one module per endpoint group
|
|
17
|
+
* store/ data stores (workspaces / providers / prompts)
|
|
18
|
+
* auth/ the login-cookie gate + H's self-cert API token
|
|
19
|
+
*/
|
|
20
|
+
export * from "./routes.js";
|
|
21
|
+
export * from "./auth/index.js";
|
|
22
|
+
export * from "./config.js";
|
|
23
|
+
export * from "./runtime-adapter.js";
|
|
24
|
+
export * from "./fake-runtime-adapter.js";
|
|
25
|
+
export * from "./runtime/index.js";
|
|
26
|
+
export * from "./replay/index.js";
|
|
27
|
+
export * from "./sse.js";
|
|
28
|
+
export * from "./static.js";
|
|
29
|
+
export * from "./settings.js";
|
|
30
|
+
export * from "./plugins.js";
|
|
31
|
+
export * from "./app.js";
|
|
32
|
+
export * from "./server.js";
|
|
33
|
+
export * from "./store/index.js";
|
package/dist/main.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Studio process entry (CLI entry point: console output is allowed here).
|
|
3
|
+
*
|
|
4
|
+
* Source default port is 3778 (so a throwaway instance can run next to the
|
|
5
|
+
* production one on 3777); production sets STUDIO_TS_PORT=3777 via
|
|
6
|
+
* scripts/run-studio-ts.sh. Nothing here touches production data files unless
|
|
7
|
+
* the caller points the path env vars at them.
|
|
8
|
+
*
|
|
9
|
+
* H: the bootstrap itself (banner + W742 graceful teardown) lives in
|
|
10
|
+
* `server.ts` so the `celestea` CLI drives the SAME code; this file only pins
|
|
11
|
+
* the historical source-checkout defaults and installs the signal handlers.
|
|
12
|
+
*/
|
|
13
|
+
export {};
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Studio process entry (CLI entry point: console output is allowed here).
|
|
3
|
+
*
|
|
4
|
+
* Source default port is 3778 (so a throwaway instance can run next to the
|
|
5
|
+
* production one on 3777); production sets STUDIO_TS_PORT=3777 via
|
|
6
|
+
* scripts/run-studio-ts.sh. Nothing here touches production data files unless
|
|
7
|
+
* the caller points the path env vars at them.
|
|
8
|
+
*
|
|
9
|
+
* H: the bootstrap itself (banner + W742 graceful teardown) lives in
|
|
10
|
+
* `server.ts` so the `celestea` CLI drives the SAME code; this file only pins
|
|
11
|
+
* the historical source-checkout defaults and installs the signal handlers.
|
|
12
|
+
*/
|
|
13
|
+
import { verifyContractsAtStartup } from "@celestea/core";
|
|
14
|
+
import { startStudioServer } from "./server.js";
|
|
15
|
+
const port = Number.parseInt(process.env["STUDIO_TS_PORT"] ?? "3778", 10);
|
|
16
|
+
const hostname = process.env["STUDIO_TS_BIND"] ?? "127.0.0.1";
|
|
17
|
+
/**
|
|
18
|
+
* W807 -- explicit contract gate. The frozen-count contract files are read and
|
|
19
|
+
* validated ONCE here, before the port is bound, and the validated snapshot is
|
|
20
|
+
* cached for the whole process lifetime. A drifted contracts/*.json refuses the
|
|
21
|
+
* boot loudly (file / expected / actual) instead of booting into a later 500.
|
|
22
|
+
*/
|
|
23
|
+
try {
|
|
24
|
+
verifyContractsAtStartup();
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
console.error("[celestea-studio-ts] FATAL: " + (e instanceof Error ? e.message : String(e)));
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
const server = startStudioServer({ port, hostname });
|
|
31
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
32
|
+
process.on(signal, () => {
|
|
33
|
+
void server.stop(signal);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Studio store plugins — "everything is a plugin" at the host layer.
|
|
3
|
+
*
|
|
4
|
+
* Each store is mounted as a `Plugin` that `provide`s exactly one service into
|
|
5
|
+
* the shared `Context`; nothing is `new`ed by a consumer, so a test can mount a
|
|
6
|
+
* store over a temp directory and the handlers never learn the difference.
|
|
7
|
+
* Mount order is semantic (a later plugin consumes an earlier service):
|
|
8
|
+
*
|
|
9
|
+
* workspaces -> registry (also the workspace-name -> path lookup)
|
|
10
|
+
* sessions -> session scan / resolve / transcript (consumes workspaces)
|
|
11
|
+
* sessionOps -> rename / branch / archive / trash (consumes both)
|
|
12
|
+
* providers -> providers.json + public_view
|
|
13
|
+
* prompts -> prompts.json registries
|
|
14
|
+
* bus -> the SSE bus (the RuntimeAdapter attaches to it)
|
|
15
|
+
* runtime -> the injected RuntimeAdapter (engine seam)
|
|
16
|
+
* settings -> host-side overrides (system_prompt / base_url)
|
|
17
|
+
*/
|
|
18
|
+
import { Context, type Plugin } from "@celestea/core";
|
|
19
|
+
import { type StudioBus } from "./sse.js";
|
|
20
|
+
import { type GrantsServices } from "./store/grants-service.js";
|
|
21
|
+
import { SessionOps } from "./store/session-ops.js";
|
|
22
|
+
import { SessionsStore } from "./store/sessions.js";
|
|
23
|
+
import { ProvidersStore } from "./store/providers.js";
|
|
24
|
+
import { PromptsStore } from "./store/prompts.js";
|
|
25
|
+
import { WorkspacesStore } from "./store/workspaces.js";
|
|
26
|
+
import { StudioSettings } from "./settings.js";
|
|
27
|
+
import { SerialQueue } from "./serial-queue.js";
|
|
28
|
+
import type { StudioConfig } from "./config.js";
|
|
29
|
+
import type { RuntimeAdapter } from "./runtime-adapter.js";
|
|
30
|
+
/** Service tokens: the typed keys of the studio Context. */
|
|
31
|
+
export declare const WORKSPACES_SERVICE = "studio.workspaces";
|
|
32
|
+
export declare const SESSIONS_SERVICE = "studio.sessions";
|
|
33
|
+
export declare const SESSION_OPS_SERVICE = "studio.sessionOps";
|
|
34
|
+
export declare const PROVIDERS_SERVICE = "studio.providers";
|
|
35
|
+
export declare const PROMPTS_SERVICE = "studio.prompts";
|
|
36
|
+
export declare const BUS_SERVICE = "studio.bus";
|
|
37
|
+
export declare const RUNTIME_SERVICE = "studio.runtime";
|
|
38
|
+
export declare const SETTINGS_SERVICE = "studio.settings";
|
|
39
|
+
/** The four data stores composed BEFORE the engine (the engine resolves sessions). */
|
|
40
|
+
export interface StoreServices {
|
|
41
|
+
workspaces: WorkspacesStore;
|
|
42
|
+
sessions: SessionsStore;
|
|
43
|
+
sessionOps: SessionOps;
|
|
44
|
+
providers: ProvidersStore;
|
|
45
|
+
prompts: PromptsStore;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Engine factory: the real adapter needs the session store to resolve
|
|
49
|
+
* `<workspace>/<session>` -> directory, so the host may inject a factory that
|
|
50
|
+
* receives the composed stores instead of a ready-made adapter.
|
|
51
|
+
*/
|
|
52
|
+
export type EngineFactory = (stores: StoreServices) => RuntimeAdapter;
|
|
53
|
+
export interface ComposeInput {
|
|
54
|
+
config: StudioConfig;
|
|
55
|
+
/** Injected engine seam, or a factory over the composed stores. */
|
|
56
|
+
runtime: RuntimeAdapter | EngineFactory;
|
|
57
|
+
/** Process environment (grant audit channel, unsandboxed availability). */
|
|
58
|
+
env?: NodeJS.ProcessEnv;
|
|
59
|
+
/** Deterministic clock for tests (session dir suffixes, trash stamps). */
|
|
60
|
+
now?: () => number;
|
|
61
|
+
}
|
|
62
|
+
export interface StudioServices {
|
|
63
|
+
ctx: Context;
|
|
64
|
+
config: StudioConfig;
|
|
65
|
+
bus: StudioBus;
|
|
66
|
+
runtime: RuntimeAdapter;
|
|
67
|
+
settings: StudioSettings;
|
|
68
|
+
workspaces: WorkspacesStore;
|
|
69
|
+
sessions: SessionsStore;
|
|
70
|
+
sessionOps: SessionOps;
|
|
71
|
+
providers: ProvidersStore;
|
|
72
|
+
prompts: PromptsStore;
|
|
73
|
+
/** W516: grant file I/O helpers, audit channel, confirm tokens, limits. */
|
|
74
|
+
grants: GrantsServices;
|
|
75
|
+
/**
|
|
76
|
+
* W815-N2: the ONE serial queue behind the studio's hot-apply writes — the
|
|
77
|
+
* prompt registry (`POST /api/prompts`) and `POST /api/config` both snapshot,
|
|
78
|
+
* mutate and `await` the engine, so the two must never interleave.
|
|
79
|
+
*/
|
|
80
|
+
applyQueue: SerialQueue;
|
|
81
|
+
/**
|
|
82
|
+
* W860: the names of the plugins THIS composition mounted at startup, in mount
|
|
83
|
+
* order (the store plugins, then the host singletons) — recorded with
|
|
84
|
+
* `pluginNames` over the very arrays that were mounted, so
|
|
85
|
+
* `GET /api/plugins` can never drift from `storePlugins`/`hostPlugins`.
|
|
86
|
+
*
|
|
87
|
+
* Boundary: this is the HOST startup layer only. Plugins the engine mounts
|
|
88
|
+
* while composing a session (its own tool/disclosure/loop plugins) are not
|
|
89
|
+
* part of this list.
|
|
90
|
+
*/
|
|
91
|
+
hostPluginNames: string[];
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Store plugins. `workspaces/sessions/sessionOps` share one clock so a test can
|
|
95
|
+
* pin the `<secs>.<nanos>` suffix of a created session directory.
|
|
96
|
+
*/
|
|
97
|
+
export declare function storePlugins(config: StudioConfig, now: () => number): Plugin[];
|
|
98
|
+
/** Bus + runtime + settings: the three host singletons. */
|
|
99
|
+
export declare function hostPlugins(runtime: RuntimeAdapter, bus: StudioBus): Plugin[];
|
|
100
|
+
/**
|
|
101
|
+
* Compose the studio context in TWO phases: the stores first (so an engine
|
|
102
|
+
* factory can resolve session directories), then the host singletons. The
|
|
103
|
+
* caller owns the runtime adapter instance either way.
|
|
104
|
+
*/
|
|
105
|
+
export declare function composeStudio(input: ComposeInput): StudioServices;
|
package/dist/plugins.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Studio store plugins — "everything is a plugin" at the host layer.
|
|
3
|
+
*
|
|
4
|
+
* Each store is mounted as a `Plugin` that `provide`s exactly one service into
|
|
5
|
+
* the shared `Context`; nothing is `new`ed by a consumer, so a test can mount a
|
|
6
|
+
* store over a temp directory and the handlers never learn the difference.
|
|
7
|
+
* Mount order is semantic (a later plugin consumes an earlier service):
|
|
8
|
+
*
|
|
9
|
+
* workspaces -> registry (also the workspace-name -> path lookup)
|
|
10
|
+
* sessions -> session scan / resolve / transcript (consumes workspaces)
|
|
11
|
+
* sessionOps -> rename / branch / archive / trash (consumes both)
|
|
12
|
+
* providers -> providers.json + public_view
|
|
13
|
+
* prompts -> prompts.json registries
|
|
14
|
+
* bus -> the SSE bus (the RuntimeAdapter attaches to it)
|
|
15
|
+
* runtime -> the injected RuntimeAdapter (engine seam)
|
|
16
|
+
* settings -> host-side overrides (system_prompt / base_url)
|
|
17
|
+
*/
|
|
18
|
+
import { Context, definePlugin, mountPlugins, pluginNames } from "@celestea/core";
|
|
19
|
+
import { dirname } from "node:path";
|
|
20
|
+
import { createStudioBus } from "./sse.js";
|
|
21
|
+
import { createGrantsServices } from "./store/grants-service.js";
|
|
22
|
+
import { SessionOps } from "./store/session-ops.js";
|
|
23
|
+
import { SessionsStore } from "./store/sessions.js";
|
|
24
|
+
import { ProvidersStore } from "./store/providers.js";
|
|
25
|
+
import { PromptsStore } from "./store/prompts.js";
|
|
26
|
+
import { WorkspacesStore } from "./store/workspaces.js";
|
|
27
|
+
import { StudioSettings } from "./settings.js";
|
|
28
|
+
import { SerialQueue } from "./serial-queue.js";
|
|
29
|
+
/** Service tokens: the typed keys of the studio Context. */
|
|
30
|
+
export const WORKSPACES_SERVICE = "studio.workspaces";
|
|
31
|
+
export const SESSIONS_SERVICE = "studio.sessions";
|
|
32
|
+
export const SESSION_OPS_SERVICE = "studio.sessionOps";
|
|
33
|
+
export const PROVIDERS_SERVICE = "studio.providers";
|
|
34
|
+
export const PROMPTS_SERVICE = "studio.prompts";
|
|
35
|
+
export const BUS_SERVICE = "studio.bus";
|
|
36
|
+
export const RUNTIME_SERVICE = "studio.runtime";
|
|
37
|
+
export const SETTINGS_SERVICE = "studio.settings";
|
|
38
|
+
/**
|
|
39
|
+
* Store plugins. `workspaces/sessions/sessionOps` share one clock so a test can
|
|
40
|
+
* pin the `<secs>.<nanos>` suffix of a created session directory.
|
|
41
|
+
*/
|
|
42
|
+
export function storePlugins(config, now) {
|
|
43
|
+
return [
|
|
44
|
+
definePlugin("studio/workspaces", (ctx) => ctx.provide(WORKSPACES_SERVICE, new WorkspacesStore(config.paths.workspacesFile))),
|
|
45
|
+
definePlugin("studio/sessions", (ctx) => ctx.provide(SESSIONS_SERVICE, new SessionsStore(ctx.require(WORKSPACES_SERVICE), now))),
|
|
46
|
+
definePlugin("studio/session-ops", (ctx) => ctx.provide(SESSION_OPS_SERVICE, new SessionOps(ctx.require(WORKSPACES_SERVICE), ctx.require(SESSIONS_SERVICE), now))),
|
|
47
|
+
definePlugin("studio/providers", (ctx) => ctx.provide(PROVIDERS_SERVICE, new ProvidersStore(config.paths.providersFile))),
|
|
48
|
+
definePlugin("studio/prompts", (ctx) => ctx.provide(PROMPTS_SERVICE, new PromptsStore(config.paths.promptsFile))),
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
/** Bus + runtime + settings: the three host singletons. */
|
|
52
|
+
export function hostPlugins(runtime, bus) {
|
|
53
|
+
return [
|
|
54
|
+
definePlugin("studio/bus", (ctx) => ctx.provide(BUS_SERVICE, bus)),
|
|
55
|
+
definePlugin("studio/runtime", (ctx) => ctx.provide(RUNTIME_SERVICE, runtime)),
|
|
56
|
+
definePlugin("studio/settings", (ctx) => ctx.provide(SETTINGS_SERVICE, new StudioSettings())),
|
|
57
|
+
];
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Compose the studio context in TWO phases: the stores first (so an engine
|
|
61
|
+
* factory can resolve session directories), then the host singletons. The
|
|
62
|
+
* caller owns the runtime adapter instance either way.
|
|
63
|
+
*/
|
|
64
|
+
export function composeStudio(input) {
|
|
65
|
+
const ctx = Context.root();
|
|
66
|
+
const storePluginList = storePlugins(input.config, input.now ?? Date.now);
|
|
67
|
+
mountPlugins(ctx, storePluginList);
|
|
68
|
+
const stores = {
|
|
69
|
+
workspaces: ctx.require(WORKSPACES_SERVICE),
|
|
70
|
+
sessions: ctx.require(SESSIONS_SERVICE),
|
|
71
|
+
sessionOps: ctx.require(SESSION_OPS_SERVICE),
|
|
72
|
+
providers: ctx.require(PROVIDERS_SERVICE),
|
|
73
|
+
prompts: ctx.require(PROMPTS_SERVICE),
|
|
74
|
+
};
|
|
75
|
+
const runtime = typeof input.runtime === "function" ? input.runtime(stores) : input.runtime;
|
|
76
|
+
const bus = createStudioBus({ statusline: () => runtime.statusline() });
|
|
77
|
+
runtime.attach(bus);
|
|
78
|
+
const hostPluginList = hostPlugins(runtime, bus);
|
|
79
|
+
mountPlugins(ctx, hostPluginList);
|
|
80
|
+
const grants = createGrantsServices({
|
|
81
|
+
dataDir: dirname(input.config.paths.workspacesFile),
|
|
82
|
+
workspacesFile: input.config.paths.workspacesFile,
|
|
83
|
+
...(input.env === undefined ? {} : { env: input.env }),
|
|
84
|
+
...(input.now === undefined ? {} : { now: input.now }),
|
|
85
|
+
});
|
|
86
|
+
return {
|
|
87
|
+
ctx,
|
|
88
|
+
config: input.config,
|
|
89
|
+
bus,
|
|
90
|
+
runtime,
|
|
91
|
+
settings: ctx.require(SETTINGS_SERVICE),
|
|
92
|
+
grants,
|
|
93
|
+
applyQueue: new SerialQueue(),
|
|
94
|
+
hostPluginNames: [...pluginNames(storePluginList), ...pluginNames(hostPluginList)],
|
|
95
|
+
...stores,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
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 type { AskUserQuestionItem, AskUserQuestionOutcome } from "@celestea/core";
|
|
24
|
+
import { UserQuestionError } from "@celestea/core";
|
|
25
|
+
/** How a pending question ended (what the SSE frame and the log row report). */
|
|
26
|
+
export type QuestionSettlement = "answered" | "timed_out" | "cancelled" | "aborted";
|
|
27
|
+
/** Everything one pending question needs to exist. */
|
|
28
|
+
export interface PendingQuestionInit {
|
|
29
|
+
requestId: string;
|
|
30
|
+
sessionId: string | null;
|
|
31
|
+
questions: readonly AskUserQuestionItem[];
|
|
32
|
+
/** Absolute deadline in ms (the authoritative half of the §6.1 double track). */
|
|
33
|
+
expiresAt: number;
|
|
34
|
+
/** The RESOLVED maximum wait (what the log row and the SSE frame report). */
|
|
35
|
+
timeoutMs: number;
|
|
36
|
+
signal?: AbortSignal;
|
|
37
|
+
}
|
|
38
|
+
/** The empty answer a timeout returns — the system decides NOTHING for the model. */
|
|
39
|
+
export declare const TIMED_OUT_OUTCOME: AskUserQuestionOutcome;
|
|
40
|
+
/** One unanswered question, awaiting the human. */
|
|
41
|
+
export declare class PendingQuestion {
|
|
42
|
+
readonly requestId: string;
|
|
43
|
+
readonly sessionId: string | null;
|
|
44
|
+
readonly questions: readonly AskUserQuestionItem[];
|
|
45
|
+
readonly expiresAt: number;
|
|
46
|
+
/** The resolved maximum wait in ms (§6.4). */
|
|
47
|
+
readonly timeoutMs: number;
|
|
48
|
+
/** The promise the answerer waterfall returns to the parked tool call. */
|
|
49
|
+
readonly result: Promise<AskUserQuestionOutcome>;
|
|
50
|
+
private readonly completion;
|
|
51
|
+
private readonly signal;
|
|
52
|
+
private readonly onAbort;
|
|
53
|
+
/** Rejection sentinel that asks the waterfall to try the layer behind us. */
|
|
54
|
+
private readonly delegated;
|
|
55
|
+
private settled;
|
|
56
|
+
private timer;
|
|
57
|
+
private outcome;
|
|
58
|
+
constructor(init: PendingQuestionInit);
|
|
59
|
+
/** Has this question already been settled (answered, expired, cancelled…)? */
|
|
60
|
+
get isSettled(): boolean;
|
|
61
|
+
/** How it ended, or null while it is still answerable. */
|
|
62
|
+
get settlement(): QuestionSettlement | null;
|
|
63
|
+
/** Read-time expiry (§6.1, the authoritative half): `now >= expires_at`. */
|
|
64
|
+
isExpired(now: number): boolean;
|
|
65
|
+
/** Milliseconds left, floored at 0 (what the UI counts down). */
|
|
66
|
+
remainingMs(now: number): number;
|
|
67
|
+
/**
|
|
68
|
+
* The human answered. Returns false when the question was already settled —
|
|
69
|
+
* that is the §6.2 race, not a defect, so the caller reports it instead of
|
|
70
|
+
* throwing. `timed_out` is always false here: a real answer arrived.
|
|
71
|
+
*/
|
|
72
|
+
answer(answers: AskUserQuestionOutcome["answers"]): boolean;
|
|
73
|
+
/** The deadline passed: settle with the EMPTY answer set (§6.3). */
|
|
74
|
+
timeout(): boolean;
|
|
75
|
+
/** The user dismissed the card (§5.1 `cancel`). */
|
|
76
|
+
cancel(reason?: string): boolean;
|
|
77
|
+
/** Transport / scope / plugin lifetime ended (§5.1 `abort`). */
|
|
78
|
+
abort(reason: unknown): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* This layer will not answer: reject with the delegation sentinel so the
|
|
81
|
+
* waterfall runs the next layer. Silent when already settled (DSH behaviour).
|
|
82
|
+
*/
|
|
83
|
+
delegate(): boolean;
|
|
84
|
+
/** Did [delegate] produce this rejection reason? */
|
|
85
|
+
isDelegation(reason: unknown): boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Arm the active half of the double track (§6.1). The timer only ever calls
|
|
88
|
+
* [timeout], which is a no-op once the user answered first.
|
|
89
|
+
*/
|
|
90
|
+
armTimer(now: () => number): void;
|
|
91
|
+
/** One settlement, ever: the strict guard that exposes a real double-settle. */
|
|
92
|
+
private settle;
|
|
93
|
+
/** Drop the timer and the abort listener — no handle outlives the question. */
|
|
94
|
+
private disarm;
|
|
95
|
+
}
|
|
96
|
+
/** The rejection an aborted wait produces (§5.3 `ASK_ABORTED`). */
|
|
97
|
+
export declare function abortedQuestion(cause?: unknown): UserQuestionError;
|
|
98
|
+
/**
|
|
99
|
+
* The process-wide pending table.
|
|
100
|
+
*
|
|
101
|
+
* It is ONE registry per host process, not one per session: the answer endpoint
|
|
102
|
+
* addresses a request by id alone and must find it without being told which
|
|
103
|
+
* session asked. Each generation's own service registers into this shared table,
|
|
104
|
+
* which is also what lets `GET /api/sessions/{id}/questions` filter by session.
|
|
105
|
+
*/
|
|
106
|
+
export declare class QuestionRegistry {
|
|
107
|
+
private readonly pending;
|
|
108
|
+
private counter;
|
|
109
|
+
/** Mint a request id (`q-<n>`); unique for the life of the process. */
|
|
110
|
+
nextRequestId(): string;
|
|
111
|
+
/** Register a question; a duplicate id is a bug, so it fails loudly. */
|
|
112
|
+
add(question: PendingQuestion): void;
|
|
113
|
+
/** Drop a settled question (idempotent). */
|
|
114
|
+
remove(requestId: string): void;
|
|
115
|
+
get(requestId: string): PendingQuestion | undefined;
|
|
116
|
+
/** Every unanswered question, oldest first (registration order). */
|
|
117
|
+
all(): PendingQuestion[];
|
|
118
|
+
/** The unanswered questions of ONE session (`null` = the detached generation). */
|
|
119
|
+
ofSession(sessionId: string | null): PendingQuestion[];
|
|
120
|
+
/** How many questions are still parked (diagnostics / tests). */
|
|
121
|
+
size(): number;
|
|
122
|
+
}
|
|
123
|
+
/** A fresh table (one per host process). */
|
|
124
|
+
export declare function createQuestionRegistry(): QuestionRegistry;
|