@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,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `EngineProfile` (the HTTP-facing host view) <-> `Profile` (the runtime's
|
|
3
|
+
* frozen 12-key compose config).
|
|
4
|
+
*
|
|
5
|
+
* The two shapes differ on purpose: the host view uses the contract names of
|
|
6
|
+
* `GET /api/config` (`context_window`, no key source), while the engine profile
|
|
7
|
+
* carries the key RESOLUTION (env var name + optional file) and the request
|
|
8
|
+
* format. This module is the only translation point, so a new profile key can
|
|
9
|
+
* never be added on one side only.
|
|
10
|
+
*/
|
|
11
|
+
import { defaultAgentConfig } from "@celestea/core";
|
|
12
|
+
import { CONTEXT_WINDOW, MIN_STEPS } from "../config.js";
|
|
13
|
+
/** The only request format the TS engine composes today (OpenAI-compatible). */
|
|
14
|
+
export const ENGINE_REQUEST_FORMAT = "chat_completions";
|
|
15
|
+
/** Host view of a composed profile. */
|
|
16
|
+
export function engineProfileOf(profile) {
|
|
17
|
+
return {
|
|
18
|
+
model: profile.model,
|
|
19
|
+
base_url: profile.base_url,
|
|
20
|
+
reasoning_effort: profile.reasoning_effort,
|
|
21
|
+
max_steps: profile.max_steps,
|
|
22
|
+
max_parallel_tool_calls: profile.max_parallel_tool_calls,
|
|
23
|
+
max_output_tokens: profile.max_output_tokens,
|
|
24
|
+
context_window: profile.context_window_tokens,
|
|
25
|
+
api_key_env: profile.api_key_env,
|
|
26
|
+
system_prompt: profile.system_prompt,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** Compose profile from the host view (extra keys inherit from `base`). */
|
|
30
|
+
export function profileFromEngine(engine, base) {
|
|
31
|
+
return {
|
|
32
|
+
model: engine.model,
|
|
33
|
+
base_url: engine.base_url,
|
|
34
|
+
api_key_env: engine.api_key_env,
|
|
35
|
+
api_key_file: base?.api_key_file ?? null,
|
|
36
|
+
max_steps: engine.max_steps,
|
|
37
|
+
max_parallel_tool_calls: engine.max_parallel_tool_calls,
|
|
38
|
+
reasoning_effort: engine.reasoning_effort,
|
|
39
|
+
max_output_tokens: engine.max_output_tokens,
|
|
40
|
+
context_window_tokens: engine.context_window,
|
|
41
|
+
system_prompt: engine.system_prompt,
|
|
42
|
+
request_format: base?.request_format ?? ENGINE_REQUEST_FORMAT,
|
|
43
|
+
temperature: base?.temperature ?? null,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** Apply an accepted `POST /api/config` patch (the host already validated it). */
|
|
47
|
+
export function applyProfilePatch(profile, patch) {
|
|
48
|
+
const next = { ...profile };
|
|
49
|
+
if (patch.model !== undefined)
|
|
50
|
+
next.model = patch.model;
|
|
51
|
+
if (patch.reasoning_effort !== undefined)
|
|
52
|
+
next.reasoning_effort = patch.reasoning_effort;
|
|
53
|
+
if (patch.base_url !== undefined)
|
|
54
|
+
next.base_url = patch.base_url;
|
|
55
|
+
if (patch.max_steps !== undefined)
|
|
56
|
+
next.max_steps = Math.max(MIN_STEPS, Math.trunc(patch.max_steps));
|
|
57
|
+
if (patch.max_output_tokens !== undefined) {
|
|
58
|
+
next.max_output_tokens = patch.max_output_tokens === null ? null : Math.trunc(patch.max_output_tokens);
|
|
59
|
+
}
|
|
60
|
+
if (patch.context_window !== undefined)
|
|
61
|
+
next.context_window_tokens = Math.trunc(patch.context_window);
|
|
62
|
+
if (patch.system_prompt !== undefined)
|
|
63
|
+
next.system_prompt = patch.system_prompt;
|
|
64
|
+
return next;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The startup profile: the host's frozen constants (`MIN_STEPS`,
|
|
68
|
+
* `CONTEXT_WINDOW`, both read off the frozen `/api/config` snapshot) plus the env
|
|
69
|
+
* overrides the host honors. The loop budget derives from this profile
|
|
70
|
+
* (`agentConfigFromProfile`), so the statusline window and the trim budget can
|
|
71
|
+
* never disagree.
|
|
72
|
+
*/
|
|
73
|
+
export function defaultEngineProfile(env, apiKeyEnv) {
|
|
74
|
+
const base = defaultAgentConfig();
|
|
75
|
+
const maxSteps = env["CELESTEA_MAX_STEPS"];
|
|
76
|
+
const contextWindow = env["CELESTEA_CONTEXT_WINDOW"];
|
|
77
|
+
return {
|
|
78
|
+
model: env["CELESTEA_MODEL"] ?? "unknown",
|
|
79
|
+
base_url: env["CELESTEA_BASE_URL"] ?? "http://127.0.0.1:3001/v1",
|
|
80
|
+
reasoning_effort: env["CELESTEA_REASONING_EFFORT"] ?? null,
|
|
81
|
+
max_steps: maxSteps === undefined ? MIN_STEPS : Math.max(MIN_STEPS, Number(maxSteps) || MIN_STEPS),
|
|
82
|
+
max_parallel_tool_calls: base.max_parallel_tool_calls,
|
|
83
|
+
max_output_tokens: null,
|
|
84
|
+
context_window: contextWindow === undefined ? CONTEXT_WINDOW : Number(contextWindow) || CONTEXT_WINDOW,
|
|
85
|
+
api_key_env: apiKeyEnv,
|
|
86
|
+
system_prompt: base.system_prompt,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W747 compat shim — this module moved to the runtime's host layer:
|
|
3
|
+
* `packages/runtime/src/host/engine-session.ts` (exported as `@celestea/runtime`).
|
|
4
|
+
*
|
|
5
|
+
* The file is kept (and its export list is unchanged) so every existing
|
|
6
|
+
* `./engine-session.js` / `../runtime/engine-session.js` import keeps working:
|
|
7
|
+
* a caller migrates by deleting its import, not by editing the engine.
|
|
8
|
+
*/
|
|
9
|
+
export { PROCESS_CHECKPOINT_IDENTITY, SESSION_LOG_ID, SESSION_LOG_NAME, bindingFor, closeLog, memoryBindingFor, openSessionLog, workerSessionPrefix, type CheckpointWiring, type SessionTarget, } from "@celestea/runtime";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W747 compat shim — this module moved to the runtime's host layer:
|
|
3
|
+
* `packages/runtime/src/host/engine-session.ts` (exported as `@celestea/runtime`).
|
|
4
|
+
*
|
|
5
|
+
* The file is kept (and its export list is unchanged) so every existing
|
|
6
|
+
* `./engine-session.js` / `../runtime/engine-session.js` import keeps working:
|
|
7
|
+
* a caller migrates by deleting its import, not by editing the engine.
|
|
8
|
+
*/
|
|
9
|
+
export { PROCESS_CHECKPOINT_IDENTITY, SESSION_LOG_ID, SESSION_LOG_NAME, bindingFor, closeLog, memoryBindingFor, openSessionLog, workerSessionPrefix, } from "@celestea/runtime";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two fallback VIEWS the host layer promises (E §4.2.3 #2/#4, W785).
|
|
3
|
+
*
|
|
4
|
+
* They live in their own leaf module on purpose: `runtime-adapter.ts` (the seam)
|
|
5
|
+
* must be able to name them, and importing them from `fallback-host.ts` closed a
|
|
6
|
+
* cycle (`runtime-adapter -> fallback-host -> llm-assembly -> runtime-adapter`,
|
|
7
|
+
* caught by `no-circular`). A leaf with no imports of its own keeps the seam a
|
|
8
|
+
* leaf too.
|
|
9
|
+
*/
|
|
10
|
+
/** The `status` frame the host publishes when a hand-over happens (§4.2.3 #2). */
|
|
11
|
+
export interface FallbackFrame {
|
|
12
|
+
phase: "fallback";
|
|
13
|
+
from: string | null;
|
|
14
|
+
to: string;
|
|
15
|
+
reason: string;
|
|
16
|
+
attempt: number;
|
|
17
|
+
effective_model: string;
|
|
18
|
+
}
|
|
19
|
+
/** The `fallback` block of `/api/status` (§4.2.3 #4). */
|
|
20
|
+
export interface FallbackStatusView {
|
|
21
|
+
active: boolean;
|
|
22
|
+
chain: string[];
|
|
23
|
+
effective_model: string | null;
|
|
24
|
+
last_reason: string | null;
|
|
25
|
+
targets: Array<{
|
|
26
|
+
name: string;
|
|
27
|
+
model: string;
|
|
28
|
+
available: boolean;
|
|
29
|
+
cooling: boolean;
|
|
30
|
+
}>;
|
|
31
|
+
problems: string[];
|
|
32
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two fallback VIEWS the host layer promises (E §4.2.3 #2/#4, W785).
|
|
3
|
+
*
|
|
4
|
+
* They live in their own leaf module on purpose: `runtime-adapter.ts` (the seam)
|
|
5
|
+
* must be able to name them, and importing them from `fallback-host.ts` closed a
|
|
6
|
+
* cycle (`runtime-adapter -> fallback-host -> llm-assembly -> runtime-adapter`,
|
|
7
|
+
* caught by `no-circular`). A leaf with no imports of its own keeps the seam a
|
|
8
|
+
* leaf too.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fallback wiring of the studio host (iteration E §4.2.3, P1) — the visibility
|
|
3
|
+
* half of the `Llm` decorator.
|
|
4
|
+
*
|
|
5
|
+
* `packages/llm` owns the RULES (trigger table, produced lock, cooldown); this
|
|
6
|
+
* module owns everything the rules cannot know: which targets exist in THIS
|
|
7
|
+
* deployment (sidecar config + the composed profile), their credentials (env var
|
|
8
|
+
* NAMES only, U7 inventory), and the three places a hand-over must be visible
|
|
9
|
+
* (§4.2.3, "三者缺一视为未实现"):
|
|
10
|
+
*
|
|
11
|
+
* 1. local append-only `fallbacks-audit.jsonl` (authoritative) + best-effort
|
|
12
|
+
* platform `POST /api/audit` when `CELESTEA_AUDIT_URL` is set;
|
|
13
|
+
* 2. one SSE `status` frame with `phase:"fallback"` — the event NAME is frozen
|
|
14
|
+
* (K5), only payload fields are added;
|
|
15
|
+
* 3. the `/api/status` view (`effective_model` + `fallback.{active,chain,
|
|
16
|
+
* last_reason,targets,problems}`), with `model` keeping its old meaning.
|
|
17
|
+
*
|
|
18
|
+
* The switch is OFF by default: when `CELESTEA_LLM_FALLBACK` is not on, `wrap()`
|
|
19
|
+
* returns null and the caller keeps the pre-P1 path byte-for-byte (D9). Nothing
|
|
20
|
+
* here reads a credential VALUE, and no target's key ever leaves its env var.
|
|
21
|
+
*/
|
|
22
|
+
import { type FallbackStepSink, type LlmTarget } from "@celestea/llm";
|
|
23
|
+
import type { Llm, Statusline } from "@celestea/core";
|
|
24
|
+
import type { StudioBus } from "../sse.js";
|
|
25
|
+
import type { Profile } from "@celestea/runtime";
|
|
26
|
+
import type { FallbackFrame, FallbackStatusView } from "./fallback-contract.js";
|
|
27
|
+
/** `<data dir>/fallbacks-audit.jsonl` (§4.2.3 #3, same discipline as grants). */
|
|
28
|
+
export declare const FALLBACKS_AUDIT_FILE = "fallbacks-audit.jsonl";
|
|
29
|
+
/** Rotate at 16 MiB, keeping the previous chain (LTS ops audit rules). */
|
|
30
|
+
export declare const FALLBACKS_AUDIT_MAX_BYTES: number;
|
|
31
|
+
export declare const ENV_AUDIT_URL = "CELESTEA_AUDIT_URL";
|
|
32
|
+
export declare const ENV_CENTER_TOKEN = "CELESTEA_CENTER_TOKEN";
|
|
33
|
+
/** One audit line: target NAMES and reasons only, never a key or a prompt. */
|
|
34
|
+
export interface FallbackAuditEvent {
|
|
35
|
+
ts: number;
|
|
36
|
+
event: "fallback" | "target_unavailable" | "platform_audit_failed";
|
|
37
|
+
session: string | null;
|
|
38
|
+
/** The five hand-over fields exist on `fallback` lines; they are absent on
|
|
39
|
+
* operational lines (`target_unavailable`, `platform_audit_failed`). */
|
|
40
|
+
from?: string | null;
|
|
41
|
+
to?: string | null;
|
|
42
|
+
reason?: string | null;
|
|
43
|
+
attempt?: number | null;
|
|
44
|
+
model?: string | null;
|
|
45
|
+
detail?: string;
|
|
46
|
+
}
|
|
47
|
+
export type { FallbackFrame, FallbackStatusView } from "./fallback-contract.js";
|
|
48
|
+
export interface FallbackWrapInput {
|
|
49
|
+
/** The composed engine seam (used verbatim while the capability is off). */
|
|
50
|
+
inner: Llm;
|
|
51
|
+
profile: Profile;
|
|
52
|
+
sessionId: string | null;
|
|
53
|
+
/** Per-attempt ledger booking (the ledger makes N attempts visible, D6). */
|
|
54
|
+
steps: FallbackStepSink | null;
|
|
55
|
+
/** `providers.json` row id for the ledger's `provider` column. */
|
|
56
|
+
provider: string | null;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The `status` payload of one hand-over (§4.2.3 #2). The event NAME is frozen
|
|
60
|
+
* (K5); these five keys are the ones declared in
|
|
61
|
+
* `contracts/sse-events.json#payloadExtensions.status`, and they live HERE so the
|
|
62
|
+
* emitted key set and the contract declaration cannot drift apart.
|
|
63
|
+
*/
|
|
64
|
+
export declare function fallbackFramePayload(frame: FallbackFrame, statusline: unknown): Record<string, unknown>;
|
|
65
|
+
/** `/api/status.fallback` of one session (null = the capability is off). */
|
|
66
|
+
export declare function fallbackViewOf(wiring: FallbackWiring, sessionId: string | null): FallbackStatusView | null;
|
|
67
|
+
/**
|
|
68
|
+
* The adapter-facing glue of the capability: the process-wide wiring plus the two
|
|
69
|
+
* host effects a hand-over has (the SSE frame and the `/api/status` view). It
|
|
70
|
+
* lives here, not in `real-runtime-adapter.ts`, for the same reason
|
|
71
|
+
* `ledger-view.ts` does — the adapter stays a thin seam inside the §4.1 budget.
|
|
72
|
+
*/
|
|
73
|
+
export declare class AdapterFallback {
|
|
74
|
+
private readonly deps;
|
|
75
|
+
/** Handed to `SessionComposer`; `wrap()` answers null while the switch is off. */
|
|
76
|
+
readonly wiring: FallbackWiring;
|
|
77
|
+
constructor(deps: {
|
|
78
|
+
dataDir?: string | null;
|
|
79
|
+
/** Falls back to the ledger's own directory: both files are process-level. */
|
|
80
|
+
ledgerFile?: {
|
|
81
|
+
path: string;
|
|
82
|
+
} | null;
|
|
83
|
+
env: NodeJS.ProcessEnv;
|
|
84
|
+
now?: () => number;
|
|
85
|
+
bus: () => StudioBus | null;
|
|
86
|
+
peek: (sessionId: string | null) => {
|
|
87
|
+
turnNo: number;
|
|
88
|
+
runtime: {
|
|
89
|
+
statusline(): Statusline;
|
|
90
|
+
};
|
|
91
|
+
} | null;
|
|
92
|
+
});
|
|
93
|
+
/** `/api/status.fallback` of one session (null = the capability is off). */
|
|
94
|
+
view(sessionId: string | null): FallbackStatusView | null;
|
|
95
|
+
/** One `status` frame per hand-over (frozen event name, declared payload). */
|
|
96
|
+
emit(sessionId: string | null, frame: FallbackFrame): void;
|
|
97
|
+
/** Await in-flight platform deliveries (tests / shutdown). */
|
|
98
|
+
flush(): Promise<void>;
|
|
99
|
+
/** In-flight platform deliveries (diagnostics; bounded by construction). */
|
|
100
|
+
pendingCount(): number;
|
|
101
|
+
}
|
|
102
|
+
export interface FallbackWiring {
|
|
103
|
+
/** True only when the switch is on AND a chain could be assembled. */
|
|
104
|
+
readonly enabled: boolean;
|
|
105
|
+
/** The decorated seam, or null = "fallback off, use your normal path". */
|
|
106
|
+
wrap(input: FallbackWrapInput): Llm | null;
|
|
107
|
+
/** The `/api/status` half for one session. */
|
|
108
|
+
view(sessionId: string | null): FallbackStatusView;
|
|
109
|
+
/** Await in-flight platform deliveries (tests / shutdown). */
|
|
110
|
+
flush(): Promise<void>;
|
|
111
|
+
/** In-flight platform deliveries (bounded: a delivered event leaves). */
|
|
112
|
+
pendingCount(): number;
|
|
113
|
+
}
|
|
114
|
+
export interface FallbackHostOptions {
|
|
115
|
+
/** `<data dir>`: `fallbacks.json` and `fallbacks-audit.jsonl` live here. */
|
|
116
|
+
dataDir?: string | null;
|
|
117
|
+
env?: NodeJS.ProcessEnv;
|
|
118
|
+
/** Bus sink: one `status` frame per hand-over. */
|
|
119
|
+
emit?: (sessionId: string | null, frame: FallbackFrame) => void;
|
|
120
|
+
now?: () => number;
|
|
121
|
+
sleep?: (ms: number) => Promise<void>;
|
|
122
|
+
/**
|
|
123
|
+
* Injectable per-target client builder (tests): the production default builds
|
|
124
|
+
* a live provider client from the composed profile (`liveEngineLlmFor`), and a
|
|
125
|
+
* test can hand in a scripted seam without touching the network.
|
|
126
|
+
*/
|
|
127
|
+
clientFor?: (target: LlmTarget, profile: Profile) => Llm;
|
|
128
|
+
/** Injectable platform transport (tests); default `fetch`. */
|
|
129
|
+
post?: (url: string, body: string, headers: Record<string, string>) => Promise<{
|
|
130
|
+
ok: boolean;
|
|
131
|
+
status: number;
|
|
132
|
+
}>;
|
|
133
|
+
}
|
|
134
|
+
export declare function createFallbackWiring(opts?: FallbackHostOptions): FallbackWiring;
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fallback wiring of the studio host (iteration E §4.2.3, P1) — the visibility
|
|
3
|
+
* half of the `Llm` decorator.
|
|
4
|
+
*
|
|
5
|
+
* `packages/llm` owns the RULES (trigger table, produced lock, cooldown); this
|
|
6
|
+
* module owns everything the rules cannot know: which targets exist in THIS
|
|
7
|
+
* deployment (sidecar config + the composed profile), their credentials (env var
|
|
8
|
+
* NAMES only, U7 inventory), and the three places a hand-over must be visible
|
|
9
|
+
* (§4.2.3, "三者缺一视为未实现"):
|
|
10
|
+
*
|
|
11
|
+
* 1. local append-only `fallbacks-audit.jsonl` (authoritative) + best-effort
|
|
12
|
+
* platform `POST /api/audit` when `CELESTEA_AUDIT_URL` is set;
|
|
13
|
+
* 2. one SSE `status` frame with `phase:"fallback"` — the event NAME is frozen
|
|
14
|
+
* (K5), only payload fields are added;
|
|
15
|
+
* 3. the `/api/status` view (`effective_model` + `fallback.{active,chain,
|
|
16
|
+
* last_reason,targets,problems}`), with `model` keeping its old meaning.
|
|
17
|
+
*
|
|
18
|
+
* The switch is OFF by default: when `CELESTEA_LLM_FALLBACK` is not on, `wrap()`
|
|
19
|
+
* returns null and the caller keeps the pre-P1 path byte-for-byte (D9). Nothing
|
|
20
|
+
* here reads a credential VALUE, and no target's key ever leaves its env var.
|
|
21
|
+
*/
|
|
22
|
+
import { appendFileSync, renameSync, statSync } from "node:fs";
|
|
23
|
+
import { dirname, join } from "node:path";
|
|
24
|
+
import { configProblems, createFallbackLlm, FallbackState, loadFallbackConfig, } from "@celestea/llm";
|
|
25
|
+
import { bridgeProviderLlm, liveEngineLlmFor } from "./llm-assembly.js";
|
|
26
|
+
/** `<data dir>/fallbacks-audit.jsonl` (§4.2.3 #3, same discipline as grants). */
|
|
27
|
+
export const FALLBACKS_AUDIT_FILE = "fallbacks-audit.jsonl";
|
|
28
|
+
/** Rotate at 16 MiB, keeping the previous chain (LTS ops audit rules). */
|
|
29
|
+
export const FALLBACKS_AUDIT_MAX_BYTES = 16 * 1024 * 1024;
|
|
30
|
+
export const ENV_AUDIT_URL = "CELESTEA_AUDIT_URL";
|
|
31
|
+
export const ENV_CENTER_TOKEN = "CELESTEA_CENTER_TOKEN";
|
|
32
|
+
/**
|
|
33
|
+
* The `status` payload of one hand-over (§4.2.3 #2). The event NAME is frozen
|
|
34
|
+
* (K5); these five keys are the ones declared in
|
|
35
|
+
* `contracts/sse-events.json#payloadExtensions.status`, and they live HERE so the
|
|
36
|
+
* emitted key set and the contract declaration cannot drift apart.
|
|
37
|
+
*/
|
|
38
|
+
export function fallbackFramePayload(frame, statusline) {
|
|
39
|
+
return {
|
|
40
|
+
phase: frame.phase,
|
|
41
|
+
statusline,
|
|
42
|
+
effective_model: frame.effective_model,
|
|
43
|
+
from: frame.from,
|
|
44
|
+
to: frame.to,
|
|
45
|
+
reason: frame.reason,
|
|
46
|
+
attempt: frame.attempt,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** `/api/status.fallback` of one session (null = the capability is off). */
|
|
50
|
+
export function fallbackViewOf(wiring, sessionId) {
|
|
51
|
+
return wiring.enabled ? wiring.view(sessionId) : null;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The adapter-facing glue of the capability: the process-wide wiring plus the two
|
|
55
|
+
* host effects a hand-over has (the SSE frame and the `/api/status` view). It
|
|
56
|
+
* lives here, not in `real-runtime-adapter.ts`, for the same reason
|
|
57
|
+
* `ledger-view.ts` does — the adapter stays a thin seam inside the §4.1 budget.
|
|
58
|
+
*/
|
|
59
|
+
export class AdapterFallback {
|
|
60
|
+
deps;
|
|
61
|
+
/** Handed to `SessionComposer`; `wrap()` answers null while the switch is off. */
|
|
62
|
+
wiring;
|
|
63
|
+
constructor(deps) {
|
|
64
|
+
this.deps = deps;
|
|
65
|
+
this.wiring = createFallbackWiring({
|
|
66
|
+
dataDir: deps.dataDir ?? (deps.ledgerFile == null ? null : dirname(deps.ledgerFile.path)),
|
|
67
|
+
env: deps.env,
|
|
68
|
+
emit: (sessionId, frame) => this.emit(sessionId, frame),
|
|
69
|
+
...(deps.now === undefined ? {} : { now: deps.now }),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/** `/api/status.fallback` of one session (null = the capability is off). */
|
|
73
|
+
view(sessionId) {
|
|
74
|
+
return fallbackViewOf(this.wiring, sessionId);
|
|
75
|
+
}
|
|
76
|
+
/** One `status` frame per hand-over (frozen event name, declared payload). */
|
|
77
|
+
emit(sessionId, frame) {
|
|
78
|
+
const entry = this.deps.peek(sessionId);
|
|
79
|
+
this.deps.bus()?.emit("status", entry?.turnNo ?? 0, fallbackFramePayload(frame, entry?.runtime.statusline() ?? {}), sessionId);
|
|
80
|
+
}
|
|
81
|
+
/** Await in-flight platform deliveries (tests / shutdown). */
|
|
82
|
+
flush() {
|
|
83
|
+
return this.wiring.flush();
|
|
84
|
+
}
|
|
85
|
+
/** In-flight platform deliveries (diagnostics; bounded by construction). */
|
|
86
|
+
pendingCount() {
|
|
87
|
+
return this.wiring.pendingCount();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** What `/api/status` reports while the capability is off (never null there). */
|
|
91
|
+
const DISABLED_VIEW = {
|
|
92
|
+
active: false,
|
|
93
|
+
chain: [],
|
|
94
|
+
effective_model: null,
|
|
95
|
+
last_reason: null,
|
|
96
|
+
targets: [],
|
|
97
|
+
problems: [],
|
|
98
|
+
};
|
|
99
|
+
export function createFallbackWiring(opts = {}) {
|
|
100
|
+
const env = opts.env ?? process.env;
|
|
101
|
+
const config = loadFallbackConfig({ dataDir: opts.dataDir ?? null, env });
|
|
102
|
+
const audit = new FallbackAudit(opts);
|
|
103
|
+
// OFF (or nothing configured) is the default: no chain, no state, no file.
|
|
104
|
+
if (config === null || !config.enabled)
|
|
105
|
+
return disabledWiring(config, audit);
|
|
106
|
+
const problems = configProblems(config, env);
|
|
107
|
+
const state = new FallbackState();
|
|
108
|
+
const sessions = new Map();
|
|
109
|
+
const now = opts.now ?? Date.now;
|
|
110
|
+
for (const problem of problems)
|
|
111
|
+
audit.write({ event: "target_unavailable", session: null, detail: problem });
|
|
112
|
+
const deps = { opts, env, state, sessions, audit, now, problems, config };
|
|
113
|
+
return {
|
|
114
|
+
enabled: true,
|
|
115
|
+
wrap: (input) => armedLlm(deps, input),
|
|
116
|
+
view: (sessionId) => statusView(deps, sessionId),
|
|
117
|
+
flush: () => audit.flush(),
|
|
118
|
+
pendingCount: () => audit.pendingCount(),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/** The OFF wiring: `wrap` hands the caller nothing, so the path cannot change. */
|
|
122
|
+
function disabledWiring(config, audit) {
|
|
123
|
+
if (config !== null && !config.enabled) {
|
|
124
|
+
audit.write({ event: "target_unavailable", session: null, detail: "config declares enabled:false" });
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
enabled: false,
|
|
128
|
+
wrap: () => null,
|
|
129
|
+
view: () => DISABLED_VIEW,
|
|
130
|
+
flush: () => audit.flush(),
|
|
131
|
+
pendingCount: () => audit.pendingCount(),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/** The chain: the configured targets, or the composed profile as its own target. */
|
|
135
|
+
function chainOf(deps, profile) {
|
|
136
|
+
const configured = deps.config.targets;
|
|
137
|
+
if (configured.length > 0)
|
|
138
|
+
return configured;
|
|
139
|
+
return [
|
|
140
|
+
{
|
|
141
|
+
name: "primary",
|
|
142
|
+
provider: "profile",
|
|
143
|
+
model: profile.model,
|
|
144
|
+
baseUrl: profile.base_url,
|
|
145
|
+
apiKeyEnv: profile.api_key_env,
|
|
146
|
+
},
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
function armedLlm(deps, input) {
|
|
150
|
+
const targets = chainOf(deps, input.profile);
|
|
151
|
+
// The decorator lives on the PROVIDER seam (it must see a provider's
|
|
152
|
+
// `failed{kindOf:"timeout"}`); the engine consumes core's seam. The input
|
|
153
|
+
// clients are already core-shaped (only the documented `kindOf` widening is
|
|
154
|
+
// lost, and `liveEngineLlmFor` never produces "timeout" — it reports a torn
|
|
155
|
+
// stream instead), so the cast is a type-level bridge only; the OUTPUT is
|
|
156
|
+
// bridged for real by [bridgeProviderLlm].
|
|
157
|
+
const decorated = createFallbackLlm({
|
|
158
|
+
targets,
|
|
159
|
+
state: deps.state,
|
|
160
|
+
policy: deps.config.policy,
|
|
161
|
+
clientFor: (target) => asProviderSeam(deps.opts.clientFor?.(target, input.profile) ?? liveEngineLlmFor(input.profile, target, deps.env)),
|
|
162
|
+
steps: input.steps,
|
|
163
|
+
...(deps.opts.now === undefined ? {} : { now: deps.opts.now }),
|
|
164
|
+
...(deps.opts.sleep === undefined ? {} : { sleep: deps.opts.sleep }),
|
|
165
|
+
onAttempt: (info) => report(deps, input.sessionId, info),
|
|
166
|
+
});
|
|
167
|
+
return bridgeProviderLlm(decorated);
|
|
168
|
+
}
|
|
169
|
+
/** Type-level only (see [armedLlm]): the two seams differ in one union member. */
|
|
170
|
+
function asProviderSeam(llm) {
|
|
171
|
+
return llm;
|
|
172
|
+
}
|
|
173
|
+
/** §4.2.3: one hand-over reaches the bus AND the audit channel, never just one. */
|
|
174
|
+
function report(deps, sessionId, info) {
|
|
175
|
+
deps.sessions.set(sessionKey(sessionId), { model: info.model, name: info.target, reason: info.reason });
|
|
176
|
+
deps.opts.emit?.(sessionId, {
|
|
177
|
+
phase: "fallback",
|
|
178
|
+
from: info.from,
|
|
179
|
+
to: info.target,
|
|
180
|
+
reason: info.reason,
|
|
181
|
+
attempt: info.attempt,
|
|
182
|
+
effective_model: info.model,
|
|
183
|
+
});
|
|
184
|
+
deps.audit.write({
|
|
185
|
+
event: "fallback",
|
|
186
|
+
session: sessionId,
|
|
187
|
+
from: info.from,
|
|
188
|
+
to: info.target,
|
|
189
|
+
reason: info.reason,
|
|
190
|
+
attempt: info.attempt,
|
|
191
|
+
model: info.model,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
/** `/api/status.fallback`: the chain, what is really serving, and what is wrong. */
|
|
195
|
+
function statusView(deps, sessionId) {
|
|
196
|
+
const known = deps.sessions.get(sessionKey(sessionId));
|
|
197
|
+
const now = deps.now();
|
|
198
|
+
return {
|
|
199
|
+
active: true,
|
|
200
|
+
chain: deps.config.targets.map((t) => t.name),
|
|
201
|
+
effective_model: known?.model ?? null,
|
|
202
|
+
last_reason: known?.reason ?? null,
|
|
203
|
+
targets: deps.config.targets.map((t) => {
|
|
204
|
+
const envName = t.apiKeyEnv ?? null;
|
|
205
|
+
return {
|
|
206
|
+
name: t.name,
|
|
207
|
+
model: t.model,
|
|
208
|
+
available: envName === null || (deps.env[envName] ?? "") !== "",
|
|
209
|
+
cooling: deps.state.isCooling(t.name, now),
|
|
210
|
+
};
|
|
211
|
+
}),
|
|
212
|
+
problems: deps.problems,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function sessionKey(sessionId) {
|
|
216
|
+
return sessionId ?? "(default)";
|
|
217
|
+
}
|
|
218
|
+
/** Local append-only channel (authoritative) + best-effort platform delivery. */
|
|
219
|
+
class FallbackAudit {
|
|
220
|
+
path;
|
|
221
|
+
env;
|
|
222
|
+
now;
|
|
223
|
+
post;
|
|
224
|
+
pending = [];
|
|
225
|
+
constructor(opts) {
|
|
226
|
+
this.path = opts.dataDir === null || opts.dataDir === undefined ? null : join(opts.dataDir, FALLBACKS_AUDIT_FILE);
|
|
227
|
+
this.env = opts.env ?? process.env;
|
|
228
|
+
this.now = opts.now ?? Date.now;
|
|
229
|
+
this.post = opts.post ?? httpPost;
|
|
230
|
+
}
|
|
231
|
+
write(event) {
|
|
232
|
+
const line = { ts: event.ts ?? Math.floor(this.now() / 1000), ...event };
|
|
233
|
+
if (this.path !== null)
|
|
234
|
+
appendRotating(this.path, line);
|
|
235
|
+
// W833 (R3 B8 / W816 F4): a delivered line LEAVES the ledger, so the array
|
|
236
|
+
// is bounded by the number of in-flight deliveries — it used to grow with
|
|
237
|
+
// every event the process ever produced.
|
|
238
|
+
const task = this.deliver(line);
|
|
239
|
+
this.pending.push(task);
|
|
240
|
+
void task
|
|
241
|
+
.finally(() => {
|
|
242
|
+
const at = this.pending.indexOf(task);
|
|
243
|
+
if (at >= 0)
|
|
244
|
+
this.pending.splice(at, 1);
|
|
245
|
+
})
|
|
246
|
+
.catch(() => undefined);
|
|
247
|
+
}
|
|
248
|
+
/** In-flight platform deliveries (diagnostics / bound assertion). */
|
|
249
|
+
pendingCount() {
|
|
250
|
+
return this.pending.length;
|
|
251
|
+
}
|
|
252
|
+
async flush() {
|
|
253
|
+
// Await everything in flight; completed deliveries have already removed
|
|
254
|
+
// themselves, and nothing new is written during shutdown.
|
|
255
|
+
while (this.pending.length > 0)
|
|
256
|
+
await Promise.all([...this.pending]);
|
|
257
|
+
}
|
|
258
|
+
/** Unset `CELESTEA_AUDIT_URL` = local channel only; a failed one is recorded. */
|
|
259
|
+
async deliver(line) {
|
|
260
|
+
const url = this.env[ENV_AUDIT_URL];
|
|
261
|
+
if (url === undefined || url.trim() === "")
|
|
262
|
+
return;
|
|
263
|
+
const token = this.env[ENV_CENTER_TOKEN];
|
|
264
|
+
const body = JSON.stringify({
|
|
265
|
+
category: "audit",
|
|
266
|
+
summary: `llm fallback ${line.from ?? "-"} -> ${line.to ?? "-"} (${line.reason ?? "-"})`,
|
|
267
|
+
detail: JSON.stringify(line).slice(0, 8192),
|
|
268
|
+
});
|
|
269
|
+
const headers = { "content-type": "application/json" };
|
|
270
|
+
if (token !== undefined && token !== "")
|
|
271
|
+
headers["x-center-token"] = token;
|
|
272
|
+
try {
|
|
273
|
+
const res = await this.post(url, body, headers);
|
|
274
|
+
if (!res.ok && this.path !== null)
|
|
275
|
+
appendRotating(this.path, { ...line, ts: line.ts, event: "platform_audit_failed", detail: `http ${res.status}` });
|
|
276
|
+
}
|
|
277
|
+
catch (e) {
|
|
278
|
+
if (this.path !== null) {
|
|
279
|
+
appendRotating(this.path, { ...line, ts: line.ts, event: "platform_audit_failed", detail: e instanceof Error ? e.message : String(e) });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/** Append one line, rotating at 16 MiB; a failure is reported, never thrown. */
|
|
285
|
+
function appendRotating(path, line) {
|
|
286
|
+
try {
|
|
287
|
+
const size = statSync(path, { throwIfNoEntry: false })?.size ?? 0;
|
|
288
|
+
if (size >= FALLBACKS_AUDIT_MAX_BYTES)
|
|
289
|
+
renameSync(path, `${path}.1`);
|
|
290
|
+
appendFileSync(path, `${JSON.stringify(line)}\n`, { mode: 0o600 });
|
|
291
|
+
}
|
|
292
|
+
catch (e) {
|
|
293
|
+
process.stderr.write(`fallbacks audit: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
async function httpPost(url, body, headers) {
|
|
297
|
+
const res = await fetch(url, { method: "POST", headers, body });
|
|
298
|
+
return { ok: res.ok, status: res.status };
|
|
299
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W769 — the studio host's auto-wake wiring.
|
|
3
|
+
*
|
|
4
|
+
* The loop itself is `@celestea/runtime`'s `AutowakeLoop` (mailbox → busy check →
|
|
5
|
+
* wake). This module owns the studio-specific part: WHICH conversations have a
|
|
6
|
+
* loop (one per host session, keyed by session id / `cli-main` for the detached
|
|
7
|
+
* generation) and how the loop reads the state of the generation in force.
|
|
8
|
+
*
|
|
9
|
+
* It lives outside the adapter on purpose: `real-runtime-adapter.ts` sits at the
|
|
10
|
+
* eslint size budget, and "which sessions can be woken" is a policy of its own —
|
|
11
|
+
* a worker's receipt is addressed to the session that spawned it, so the set of
|
|
12
|
+
* queues that can carry one is exactly the set of live host sessions.
|
|
13
|
+
*/
|
|
14
|
+
import type { SessionMailbox } from "@celestea/workers";
|
|
15
|
+
/** What the adapter must tell the wiring. */
|
|
16
|
+
export interface HostAutowakeOptions {
|
|
17
|
+
/** `CELESTEA_AUTOWAKE` (read once by the adapter at construction). */
|
|
18
|
+
enabled: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Current state of one host conversation (null = no live generation).
|
|
21
|
+
* W855 (C8): `userPending` is the size of the USER's `next-turn` lane; the
|
|
22
|
+
* loop only observes it (the turn-start drain is the lane's consumer).
|
|
23
|
+
*/
|
|
24
|
+
lookup: (session: string | null) => {
|
|
25
|
+
mailbox: SessionMailbox | null;
|
|
26
|
+
busy: boolean;
|
|
27
|
+
userPending: number;
|
|
28
|
+
} | null;
|
|
29
|
+
/** Claim the slot and run one ordinary turn over the drained receipts. */
|
|
30
|
+
wake: (session: string | null, input: string | null) => boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* W855 (C8): the generation state the loop reads on every pass, seen
|
|
34
|
+
* structurally (the registry entry satisfies it). Keeping this here — instead of
|
|
35
|
+
* inline in `real-runtime-adapter.ts` — is what keeps that file under its
|
|
36
|
+
* eslint `max-lines` budget.
|
|
37
|
+
*/
|
|
38
|
+
export interface AutowakeGeneration {
|
|
39
|
+
inFlight: boolean;
|
|
40
|
+
runtime: {
|
|
41
|
+
workers?: {
|
|
42
|
+
mailbox?: SessionMailbox | null;
|
|
43
|
+
} | null;
|
|
44
|
+
pendingInjections(lane?: "next-turn"): number;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Mailbox + busy + the USER's next-turn lane depth, from one entry (or null). */
|
|
48
|
+
export declare function autowakeStateOf(entry: AutowakeGeneration | null): {
|
|
49
|
+
mailbox: SessionMailbox | null;
|
|
50
|
+
busy: boolean;
|
|
51
|
+
userPending: number;
|
|
52
|
+
} | null;
|
|
53
|
+
/** The studio's log line for auto-wake decisions (stderr, like boot recovery). */
|
|
54
|
+
export declare function autowakeLog(session: string | null, line: string): void;
|
|
55
|
+
export declare class HostAutowake {
|
|
56
|
+
private readonly loops;
|
|
57
|
+
private readonly opts;
|
|
58
|
+
constructor(opts: HostAutowakeOptions);
|
|
59
|
+
/** Is auto-wake on? (`CELESTEA_AUTOWAKE`, default on.) */
|
|
60
|
+
get running(): boolean;
|
|
61
|
+
/** Live loops (diagnostics / tests). */
|
|
62
|
+
get count(): number;
|
|
63
|
+
/**
|
|
64
|
+
* Mount the (idempotent) loop of one host conversation. Called from the
|
|
65
|
+
* generation's build callback: the hooks read the CURRENT entry on every pass,
|
|
66
|
+
* so a rebuilt instance is picked up without re-mounting anything.
|
|
67
|
+
*/
|
|
68
|
+
ensure(session: string | null): void;
|
|
69
|
+
/**
|
|
70
|
+
* W794: a DELETED session never comes back under that id, so its loop is
|
|
71
|
+
* unparked for good — otherwise it keeps re-binding to a generation that can
|
|
72
|
+
* never exist again and re-logs `no live generation to bind` forever (the
|
|
73
|
+
* timer is real). A rebuild / idle eviction must NOT call this: the same id
|
|
74
|
+
* recomposes, and its loop has to be waiting for it. Idempotent.
|
|
75
|
+
*/
|
|
76
|
+
forget(session: string | null): Promise<void>;
|
|
77
|
+
/** Unpark every loop (process shutdown; idempotent). */
|
|
78
|
+
stop(): Promise<void>;
|
|
79
|
+
}
|