@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,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker orchestration bridge: the `POST /api/worker/*` surface is a thin proxy
|
|
3
|
+
* over the engine's own worker tools (`src/api.rs:437-503`), and the two
|
|
4
|
+
* `GET` surfaces read the in-process registries directly.
|
|
5
|
+
*
|
|
6
|
+
* Keeping the mapping here means the adapter never re-implements orchestration:
|
|
7
|
+
* `spawn_worker` / `session_send_message` are DISPATCHED through the composed
|
|
8
|
+
* `ToolRegistry` (so the HTTP surface and the model's tool surface cannot
|
|
9
|
+
* drift), while `worker_status` reads the same registry summary the tool reads.
|
|
10
|
+
*
|
|
11
|
+
* W513: each session runtime owns its own registry, so the rows carry the
|
|
12
|
+
* owning host session (`host_session`) and the worker id / registry status /
|
|
13
|
+
* driver state — enough for `GET /api/sessions` to list workers per session and
|
|
14
|
+
* for the host to aggregate a process-wide `worker_status`.
|
|
15
|
+
*/
|
|
16
|
+
import { isRecord } from "@celestea/core";
|
|
17
|
+
import { projectMessages } from "@celestea/session";
|
|
18
|
+
import { getExtra, workerAttempt } from "@celestea/workers";
|
|
19
|
+
/**
|
|
20
|
+
* Engine-memory worker sessions (`worker:<sid>`, pseudo-workspace "engine").
|
|
21
|
+
*
|
|
22
|
+
* W740: the row is ENTRY-driven, not conversation-driven. A settled worker gives
|
|
23
|
+
* up its conversation (W736's receipt path; the watchdog's F2 release), so a
|
|
24
|
+
* projection that only walked `registry.sessions.metas()` dropped every
|
|
25
|
+
* DONE/FAILED worker out of the panel the moment it finished — exactly the
|
|
26
|
+
* opposite of what a status view is for. Each registry row emits a panel row,
|
|
27
|
+
* enriched with the live conversation while one still exists.
|
|
28
|
+
*/
|
|
29
|
+
export function workerSessionsOf(registry, hostSessionId) {
|
|
30
|
+
if (registry === null)
|
|
31
|
+
return [];
|
|
32
|
+
return registry.ownEntries().map((entry) => {
|
|
33
|
+
const sid = getExtra(entry, "sess") ?? "";
|
|
34
|
+
const meta = sid === "" ? undefined : registry.sessions.get(sid)?.meta;
|
|
35
|
+
return {
|
|
36
|
+
id: `worker:${sid === "" ? entry.wid : sid}`,
|
|
37
|
+
workspace: "engine",
|
|
38
|
+
kind: "worker",
|
|
39
|
+
title: meta?.title ?? entry.wid,
|
|
40
|
+
model: meta?.model ?? getExtra(entry, "model"),
|
|
41
|
+
// W729 §2.3: the mode recorded at spawn (parent mode unless overridden).
|
|
42
|
+
mode: meta?.mode ?? getExtra(entry, "mode") ?? "standard",
|
|
43
|
+
size: registry.sessions.logOf(sid)?.events().length ?? 0,
|
|
44
|
+
modified: 0,
|
|
45
|
+
active: false,
|
|
46
|
+
wid: entry.wid,
|
|
47
|
+
status: entry.status,
|
|
48
|
+
state: getExtra(entry, "state") ?? "",
|
|
49
|
+
host_session: hostSessionId,
|
|
50
|
+
// E §2.3 P1 ③ (W787): which try this row is and the key of the receipt it
|
|
51
|
+
// already delivered — the two facts a coordinator needs to tell a
|
|
52
|
+
// re-dispatch from a duplicate.
|
|
53
|
+
attempt: workerAttempt(entry),
|
|
54
|
+
last_receipt: getExtra(entry, "receipt"),
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/** Process-wide `worker_status` fold over the merged rows (W513). */
|
|
59
|
+
export function aggregateWorkerStatus(rows, wid) {
|
|
60
|
+
const scoped = wid === undefined ? [...rows] : rows.filter((row) => row.wid === wid);
|
|
61
|
+
const by_status = {};
|
|
62
|
+
const by_state = {};
|
|
63
|
+
for (const row of scoped) {
|
|
64
|
+
const status = row.status ?? "RUNNING";
|
|
65
|
+
const state = row.state ?? "idle";
|
|
66
|
+
by_status[status] = (by_status[status] ?? 0) + 1;
|
|
67
|
+
by_state[state] = (by_state[state] ?? 0) + 1;
|
|
68
|
+
}
|
|
69
|
+
if (wid !== undefined && scoped.length === 0) {
|
|
70
|
+
return { ok: false, total: 0, by_status, by_state, workers: [], wid, error: `no worker ${wid} in registry` };
|
|
71
|
+
}
|
|
72
|
+
return { ok: scoped.length > 0, total: scoped.length, by_status, by_state, workers: scoped, ...(wid === undefined ? {} : { wid }) };
|
|
73
|
+
}
|
|
74
|
+
/** Merged worker rows over every live instance (W513 aggregate view). */
|
|
75
|
+
export function mergedWorkerRows(entries) {
|
|
76
|
+
const rows = [];
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
for (const row of workerSessionsOf(entry.runtime.workers, entry.runtime.hostSessionId)) {
|
|
79
|
+
rows.push({ ...row, host_session: entry.sessionId, busy: entry.inFlight });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return rows;
|
|
83
|
+
}
|
|
84
|
+
/** The messages of one worker session, from whichever live instance owns it. */
|
|
85
|
+
export function workerMessagesAcross(entries, sessionId) {
|
|
86
|
+
for (const entry of entries) {
|
|
87
|
+
const found = workerMessagesOf(entry.runtime.workers, sessionId);
|
|
88
|
+
if (found !== null)
|
|
89
|
+
return found;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
/** `POST /api/worker/spawn`: the spawn tool, dispatched through the session's own registry. */
|
|
94
|
+
export async function spawnWorkerThrough(entry, req, callId) {
|
|
95
|
+
const args = { wid: req.wid, brief: req.brief };
|
|
96
|
+
for (const key of ["title", "model"]) {
|
|
97
|
+
const value = req[key];
|
|
98
|
+
if (value !== undefined)
|
|
99
|
+
args[key] = value;
|
|
100
|
+
}
|
|
101
|
+
// W513: an unaddressed worker reports back to the session that spawned it.
|
|
102
|
+
args["report_to"] = req.report_to ?? entry.runtime.hostSessionId ?? "";
|
|
103
|
+
return spawnOutcomeOf(await dispatchWorkerTool(entry.runtime.tools, "spawn_worker", args, callId));
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* `POST /api/worker/send`: the worker's registry is per session, so the message is
|
|
107
|
+
* routed to the instance that owns the target — the first one that accepts it wins.
|
|
108
|
+
*/
|
|
109
|
+
export async function sendWorkerThrough(entries, req, callId) {
|
|
110
|
+
let last = null;
|
|
111
|
+
for (const entry of entries) {
|
|
112
|
+
const body = sendBodyOf(await dispatchWorkerTool(entry.runtime.tools, "send_message", { target: req.target, content: req.content }, callId()));
|
|
113
|
+
if (body["ok"] === true)
|
|
114
|
+
return body;
|
|
115
|
+
last = body;
|
|
116
|
+
}
|
|
117
|
+
return last ?? { ok: false, delivered: false, error: "worker registry is not wired" };
|
|
118
|
+
}
|
|
119
|
+
export function workerMessagesOf(registry, sessionId) {
|
|
120
|
+
const sid = sessionId.startsWith("worker:") ? sessionId.slice("worker:".length) : sessionId;
|
|
121
|
+
const log = registry?.sessions.logOf(sid);
|
|
122
|
+
return log === undefined ? null : projectMessages(log.events());
|
|
123
|
+
}
|
|
124
|
+
/** Dispatch one worker tool through the composed registry. */
|
|
125
|
+
export async function dispatchWorkerTool(registry, name, args, callId) {
|
|
126
|
+
if (registry === null)
|
|
127
|
+
return null;
|
|
128
|
+
const out = await registry.dispatch({ call_id: callId, name, args });
|
|
129
|
+
return isRecord(out.value) ? out.value : null;
|
|
130
|
+
}
|
|
131
|
+
/** `spawn_worker` value envelope -> the HTTP outcome (`{ok, sessionId, title, wid}`). */
|
|
132
|
+
export function spawnOutcomeOf(value) {
|
|
133
|
+
if (value === null)
|
|
134
|
+
return { ok: false, error: "worker registry is not wired" };
|
|
135
|
+
if (value["ok"] !== true)
|
|
136
|
+
return { ok: false, error: String(value["error"] ?? "worker spawn failed"), value };
|
|
137
|
+
return { ok: true, sessionId: String(value["sessionId"]), title: String(value["title"]), wid: String(value["wid"]) };
|
|
138
|
+
}
|
|
139
|
+
/** `send_message` value envelope -> the response body (verbatim). */
|
|
140
|
+
export function sendBodyOf(value) {
|
|
141
|
+
return value ?? { ok: false, delivered: false, error: "worker registry is not wired" };
|
|
142
|
+
}
|
|
143
|
+
/** `worker_status` tool/registry payload -> the frozen HTTP shape. */
|
|
144
|
+
export function toStatusReport(raw, wid) {
|
|
145
|
+
if (raw === null || raw["ok"] !== true) {
|
|
146
|
+
const error = raw === null ? "worker registry is not wired" : String(raw["error"] ?? "");
|
|
147
|
+
return { ok: false, total: 0, by_status: {}, by_state: {}, workers: [], ...(wid === undefined ? {} : { wid }), error };
|
|
148
|
+
}
|
|
149
|
+
const byStatus = (raw["by_status"] ?? {});
|
|
150
|
+
const byState = (raw["by_state"] ?? {});
|
|
151
|
+
if (wid === undefined) {
|
|
152
|
+
return { ok: true, total: Number(raw["total"] ?? 0), by_status: byStatus, by_state: byState, workers: (raw["workers"] ?? []) };
|
|
153
|
+
}
|
|
154
|
+
const workers = raw["worker"] === undefined ? [] : [raw["worker"]];
|
|
155
|
+
return { ok: true, total: workers.length, by_status: byStatus, by_state: byState, workers, wid };
|
|
156
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Does this session instance still hold LIVE worker work?" (W742 §1 / §2.2.4).
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `real-runtime-adapter.ts` verbatim: the predicate is about the
|
|
5
|
+
* WORKER table, not about the adapter, and keeping it beside the other worker
|
|
6
|
+
* views is what keeps the adapter inside the §4.1 file budget.
|
|
7
|
+
*
|
|
8
|
+
* Two things count as live (and only these):
|
|
9
|
+
* - a RUNNING row — the brief has no terminal verdict yet (W736 freezes a row
|
|
10
|
+
* exactly once, so RUNNING really does mean "not delivered");
|
|
11
|
+
* - a worker session with an OPEN turn — a follow-up message being answered;
|
|
12
|
+
* by then the row is already settled, so the log is the only witness.
|
|
13
|
+
* A parked, settled worker is addressable but idle: it must NOT keep its
|
|
14
|
+
* session's generation frozen, or a config change would never land there.
|
|
15
|
+
*/
|
|
16
|
+
import { type WorkerRegistry } from "@celestea/workers";
|
|
17
|
+
export interface LiveWorkerHost {
|
|
18
|
+
runtime: {
|
|
19
|
+
workers: WorkerRegistry | null;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export declare function hasLiveWorkersOf(entry: LiveWorkerHost): boolean;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Does this session instance still hold LIVE worker work?" (W742 §1 / §2.2.4).
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `real-runtime-adapter.ts` verbatim: the predicate is about the
|
|
5
|
+
* WORKER table, not about the adapter, and keeping it beside the other worker
|
|
6
|
+
* views is what keeps the adapter inside the §4.1 file budget.
|
|
7
|
+
*
|
|
8
|
+
* Two things count as live (and only these):
|
|
9
|
+
* - a RUNNING row — the brief has no terminal verdict yet (W736 freezes a row
|
|
10
|
+
* exactly once, so RUNNING really does mean "not delivered");
|
|
11
|
+
* - a worker session with an OPEN turn — a follow-up message being answered;
|
|
12
|
+
* by then the row is already settled, so the log is the only witness.
|
|
13
|
+
* A parked, settled worker is addressable but idle: it must NOT keep its
|
|
14
|
+
* session's generation frozen, or a config change would never land there.
|
|
15
|
+
*/
|
|
16
|
+
import { getExtra, hasInProgressTurn } from "@celestea/workers";
|
|
17
|
+
export function hasLiveWorkersOf(entry) {
|
|
18
|
+
const workers = entry.runtime.workers;
|
|
19
|
+
return workers !== null && workers.ownEntries().some((row) => row.status === "RUNNING" || openTurnOf(workers, row));
|
|
20
|
+
}
|
|
21
|
+
/** W742 §1: is a turn OPEN on this worker's own session log? (W736's rule.) */
|
|
22
|
+
function openTurnOf(workers, row) {
|
|
23
|
+
const log = workers.sessions.logOf(getExtra(row, "sess") ?? "");
|
|
24
|
+
return log !== undefined && hasInProgressTurn(log.events());
|
|
25
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boot observation of the studio's own worker table (E §2.3 P0 ③).
|
|
3
|
+
*
|
|
4
|
+
* The startup sequence is: read the table this studio wrote in an earlier life →
|
|
5
|
+
* judge every row (dead owner? missing host session?) → write ONE audit line per
|
|
6
|
+
* finding plus one summary line → report on stderr.
|
|
7
|
+
*
|
|
8
|
+
* WHAT IT DELIBERATELY DOES NOT DO: re-dispatch, settle, or rewrite a row. That
|
|
9
|
+
* is the whole P0/P2 split — acting on a stale row before the tool
|
|
10
|
+
* side-effect table exists turns "crash recovery" into a side-effect amplifier
|
|
11
|
+
* (§5.1), and the DSH-side plugin is already the owner of liveness actions for
|
|
12
|
+
* its own fleet. P0 only makes the situation VISIBLE, in three places:
|
|
13
|
+
*
|
|
14
|
+
* 1. `<data dir>/recovery-audit.jsonl` (the durable record);
|
|
15
|
+
* 2. `GET /api/worker/status` → `stale[]` / `orphans[]` (the live view);
|
|
16
|
+
* 3. `[celestea-worker-recovery]` on stderr (the operator's log).
|
|
17
|
+
*/
|
|
18
|
+
import type { WorkerRecoveryReport } from "@celestea/workers";
|
|
19
|
+
import type { RecoveryAuditWriter } from "./recovery-audit.js";
|
|
20
|
+
export interface WorkerBootObservationInput {
|
|
21
|
+
/** The configured table path (null = in-memory: nothing to observe). */
|
|
22
|
+
path: string | null;
|
|
23
|
+
/** Does the host session that dispatched a worker still exist? */
|
|
24
|
+
knownHost?: (sid: string) => boolean;
|
|
25
|
+
/** Results dir of the deliverable probe. */
|
|
26
|
+
resultsDir: string;
|
|
27
|
+
audit?: RecoveryAuditWriter | null;
|
|
28
|
+
now?: () => number;
|
|
29
|
+
warn?: (message: string) => void;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Observe the table once, at boot. Never throws: an unreadable table is reported
|
|
33
|
+
* and treated as empty (a damaged file must not stop the studio from starting).
|
|
34
|
+
*/
|
|
35
|
+
export declare function observeWorkerTableOnBoot(input: WorkerBootObservationInput): WorkerRecoveryReport;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boot observation of the studio's own worker table (E §2.3 P0 ③).
|
|
3
|
+
*
|
|
4
|
+
* The startup sequence is: read the table this studio wrote in an earlier life →
|
|
5
|
+
* judge every row (dead owner? missing host session?) → write ONE audit line per
|
|
6
|
+
* finding plus one summary line → report on stderr.
|
|
7
|
+
*
|
|
8
|
+
* WHAT IT DELIBERATELY DOES NOT DO: re-dispatch, settle, or rewrite a row. That
|
|
9
|
+
* is the whole P0/P2 split — acting on a stale row before the tool
|
|
10
|
+
* side-effect table exists turns "crash recovery" into a side-effect amplifier
|
|
11
|
+
* (§5.1), and the DSH-side plugin is already the owner of liveness actions for
|
|
12
|
+
* its own fleet. P0 only makes the situation VISIBLE, in three places:
|
|
13
|
+
*
|
|
14
|
+
* 1. `<data dir>/recovery-audit.jsonl` (the durable record);
|
|
15
|
+
* 2. `GET /api/worker/status` → `stale[]` / `orphans[]` (the live view);
|
|
16
|
+
* 3. `[celestea-worker-recovery]` on stderr (the operator's log).
|
|
17
|
+
*/
|
|
18
|
+
import { workerRecoveryBlock } from "./worker-table.js";
|
|
19
|
+
/**
|
|
20
|
+
* Observe the table once, at boot. Never throws: an unreadable table is reported
|
|
21
|
+
* and treated as empty (a damaged file must not stop the studio from starting).
|
|
22
|
+
*/
|
|
23
|
+
export function observeWorkerTableOnBoot(input) {
|
|
24
|
+
const report = workerRecoveryBlock({
|
|
25
|
+
path: input.path,
|
|
26
|
+
...(input.knownHost === undefined ? {} : { knownHost: input.knownHost }),
|
|
27
|
+
resultsDir: input.resultsDir,
|
|
28
|
+
...(input.now === undefined ? {} : { now: input.now }),
|
|
29
|
+
});
|
|
30
|
+
for (const row of report.stale)
|
|
31
|
+
auditRow(input, row, "worker_stale");
|
|
32
|
+
for (const row of report.orphans)
|
|
33
|
+
auditRow(input, row, "worker_orphan");
|
|
34
|
+
input.audit?.write({
|
|
35
|
+
event: "worker_observed",
|
|
36
|
+
session: null,
|
|
37
|
+
count: report.stale.length + report.orphans.length,
|
|
38
|
+
detail: `table=${input.path ?? "<memory>"} rows=${report.frozen.length + report.live.length + report.stale.length} live=${report.live.length} frozen=${report.frozen.length}`,
|
|
39
|
+
});
|
|
40
|
+
announce(input, report);
|
|
41
|
+
return report;
|
|
42
|
+
}
|
|
43
|
+
/** One audit line per finding — the row's identity, never its brief (§4.4). */
|
|
44
|
+
function auditRow(input, row, event) {
|
|
45
|
+
input.audit?.write({
|
|
46
|
+
event,
|
|
47
|
+
session: row.host_session,
|
|
48
|
+
wid: row.wid,
|
|
49
|
+
attempt: row.attempt,
|
|
50
|
+
host_session: row.host_session,
|
|
51
|
+
reason: row.reason,
|
|
52
|
+
action: row.action,
|
|
53
|
+
count: row.retries,
|
|
54
|
+
detail: `lease=${row.lease_pid === null ? "none" : `${row.lease_pid}@${row.lease_at ?? 0}`} artifact=${row.artifact ? "yes" : "no"}`,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/** The stderr summary: one line, and only when there is something to say. */
|
|
58
|
+
function announce(input, report) {
|
|
59
|
+
const warn = input.warn ?? ((message) => process.stderr.write(`${message}\n`));
|
|
60
|
+
if (report.stale.length === 0 && report.orphans.length === 0)
|
|
61
|
+
return;
|
|
62
|
+
const names = (rows) => rows.map((r) => `${r.wid}(${r.reason}${r.artifact ? ",artifact" : ""})`).join(", ");
|
|
63
|
+
warn(`[celestea-worker-recovery] observed only, no re-dispatch: stale=[${names(report.stale)}] orphans=[${names(report.orphans)}]`);
|
|
64
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The studio's worker TABLE path and its boot-observation view (E §2.2.1/§2.2.4).
|
|
3
|
+
*
|
|
4
|
+
* Two host decisions live here, both about the same file:
|
|
5
|
+
*
|
|
6
|
+
* 1. WHERE the table is. §2.2.1: the studio and the DSH-side plugin
|
|
7
|
+
* (`celes-worker-spawn`) each own a table and they must NEVER write each
|
|
8
|
+
* other's (R2-1, asserted by B6). The studio's default is
|
|
9
|
+
* `<data dir>/worker-registry.tsv`, overridable with
|
|
10
|
+
* `CELESTEA_WORKER_REGISTRY`; an EMPTY value means "in-memory only"
|
|
11
|
+
* (`tsvPath: null`, the test/embedded option that must stay available).
|
|
12
|
+
* 2. WHAT a persisted row means at boot: `stale[]` / `orphans[]` for
|
|
13
|
+
* `GET /api/worker/status`. P0 OBSERVES ONLY — the judgement is the pure
|
|
14
|
+
* function in `@celestea/workers` and nothing here settles or re-dispatches.
|
|
15
|
+
*
|
|
16
|
+
* Reading is also how the boot observer writes its audit line, so the two share
|
|
17
|
+
* this module instead of each parsing the file on its own.
|
|
18
|
+
*/
|
|
19
|
+
import type { WorkerEntry } from "@celestea/core";
|
|
20
|
+
import { type WorkerRecoveryReport } from "@celestea/workers";
|
|
21
|
+
/** `CELESTEA_WORKER_REGISTRY` — the table path; empty = in-memory only. */
|
|
22
|
+
export declare const ENV_WORKER_REGISTRY = "CELESTEA_WORKER_REGISTRY";
|
|
23
|
+
/** File name inside `<data dir>` (never the DSH plugin's `workerBase`). */
|
|
24
|
+
export declare const WORKER_REGISTRY_FILE = "worker-registry.tsv";
|
|
25
|
+
export interface WorkerTablePathInput {
|
|
26
|
+
env: NodeJS.ProcessEnv;
|
|
27
|
+
/** `<data dir>` — where workspaces.json / the ledger live. */
|
|
28
|
+
dataDir?: string | null;
|
|
29
|
+
/** `<data dir>/worker-results`; its parent is the data dir when unset. */
|
|
30
|
+
resultsDir?: string | null;
|
|
31
|
+
/** Explicit option wins (`null` = in-memory); `undefined` = derive it. */
|
|
32
|
+
override?: string | null;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The configured table path, or null for an in-memory table. Precedence:
|
|
36
|
+
* explicit option > `CELESTEA_WORKER_REGISTRY` > `<data dir>/worker-registry.tsv`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function workerTablePath(input: WorkerTablePathInput): string | null;
|
|
39
|
+
/** Parse one table; a missing or unreadable file is an EMPTY table, never a throw. */
|
|
40
|
+
export declare function readWorkerTable(path: string | null): {
|
|
41
|
+
entries: WorkerEntry[];
|
|
42
|
+
error: string | null;
|
|
43
|
+
};
|
|
44
|
+
export interface WorkerRecoveryBlockInput {
|
|
45
|
+
/** The table path (null = in-memory: there is nothing to observe). */
|
|
46
|
+
path: string | null;
|
|
47
|
+
/** Does the host session that dispatched a worker still exist? */
|
|
48
|
+
knownHost?: (sid: string) => boolean;
|
|
49
|
+
/** Results dir of the deliverable probe (`results/<wid>*.md`). */
|
|
50
|
+
resultsDir: string;
|
|
51
|
+
now?: () => number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Judge the table for the status view. `stale`/`orphans` are PURE ADDITIONS to
|
|
55
|
+
* `/api/worker/status`; a foreign-process row is the NORMAL case here (that is
|
|
56
|
+
* what a persisted table means), so the view must show it.
|
|
57
|
+
*/
|
|
58
|
+
export declare function workerRecoveryBlock(input: WorkerRecoveryBlockInput): WorkerRecoveryReport;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The studio's worker TABLE path and its boot-observation view (E §2.2.1/§2.2.4).
|
|
3
|
+
*
|
|
4
|
+
* Two host decisions live here, both about the same file:
|
|
5
|
+
*
|
|
6
|
+
* 1. WHERE the table is. §2.2.1: the studio and the DSH-side plugin
|
|
7
|
+
* (`celes-worker-spawn`) each own a table and they must NEVER write each
|
|
8
|
+
* other's (R2-1, asserted by B6). The studio's default is
|
|
9
|
+
* `<data dir>/worker-registry.tsv`, overridable with
|
|
10
|
+
* `CELESTEA_WORKER_REGISTRY`; an EMPTY value means "in-memory only"
|
|
11
|
+
* (`tsvPath: null`, the test/embedded option that must stay available).
|
|
12
|
+
* 2. WHAT a persisted row means at boot: `stale[]` / `orphans[]` for
|
|
13
|
+
* `GET /api/worker/status`. P0 OBSERVES ONLY — the judgement is the pure
|
|
14
|
+
* function in `@celestea/workers` and nothing here settles or re-dispatches.
|
|
15
|
+
*
|
|
16
|
+
* Reading is also how the boot observer writes its audit line, so the two share
|
|
17
|
+
* this module instead of each parsing the file on its own.
|
|
18
|
+
*/
|
|
19
|
+
import { readFileSync } from "node:fs";
|
|
20
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
21
|
+
import { hasDeliverable, observeWorkerTable, parseRegistryTsv } from "@celestea/workers";
|
|
22
|
+
/** `CELESTEA_WORKER_REGISTRY` — the table path; empty = in-memory only. */
|
|
23
|
+
export const ENV_WORKER_REGISTRY = "CELESTEA_WORKER_REGISTRY";
|
|
24
|
+
/** File name inside `<data dir>` (never the DSH plugin's `workerBase`). */
|
|
25
|
+
export const WORKER_REGISTRY_FILE = "worker-registry.tsv";
|
|
26
|
+
/**
|
|
27
|
+
* The configured table path, or null for an in-memory table. Precedence:
|
|
28
|
+
* explicit option > `CELESTEA_WORKER_REGISTRY` > `<data dir>/worker-registry.tsv`.
|
|
29
|
+
*/
|
|
30
|
+
export function workerTablePath(input) {
|
|
31
|
+
if (input.override !== undefined)
|
|
32
|
+
return input.override === null ? null : resolve(input.override);
|
|
33
|
+
const raw = input.env[ENV_WORKER_REGISTRY];
|
|
34
|
+
if (raw !== undefined)
|
|
35
|
+
return raw.trim() === "" ? null : resolve(raw);
|
|
36
|
+
const dir = input.dataDir ?? (input.resultsDir == null ? null : resolve(input.resultsDir, ".."));
|
|
37
|
+
if (dir === null || dir === "")
|
|
38
|
+
return null;
|
|
39
|
+
return join(isAbsolute(dir) ? dir : resolve(dir), WORKER_REGISTRY_FILE);
|
|
40
|
+
}
|
|
41
|
+
/** Parse one table; a missing or unreadable file is an EMPTY table, never a throw. */
|
|
42
|
+
export function readWorkerTable(path) {
|
|
43
|
+
if (path === null)
|
|
44
|
+
return { entries: [], error: null };
|
|
45
|
+
let text;
|
|
46
|
+
try {
|
|
47
|
+
text = readFileSync(path, "utf8");
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
// ENOENT is the normal state before the first spawn (no file yet).
|
|
51
|
+
return { entries: [], error: e.code === "ENOENT" ? null : messageOf(e) };
|
|
52
|
+
}
|
|
53
|
+
return { entries: parseRegistryTsv(text).entries, error: null };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Judge the table for the status view. `stale`/`orphans` are PURE ADDITIONS to
|
|
57
|
+
* `/api/worker/status`; a foreign-process row is the NORMAL case here (that is
|
|
58
|
+
* what a persisted table means), so the view must show it.
|
|
59
|
+
*/
|
|
60
|
+
export function workerRecoveryBlock(input) {
|
|
61
|
+
const { entries } = readWorkerTable(input.path);
|
|
62
|
+
return observeWorkerTable(entries, {
|
|
63
|
+
...(input.knownHost === undefined ? {} : { knownHost: input.knownHost }),
|
|
64
|
+
artifactExists: (entry) => hasDeliverable(input.resultsDir, entry.wid).found,
|
|
65
|
+
...(input.now === undefined ? {} : { now: input.now() }),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function messageOf(e) {
|
|
69
|
+
return e instanceof Error ? e.message : String(e);
|
|
70
|
+
}
|