@nanobpm/agentic 0.1.0
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/README.md +22 -0
- package/dist/blackboard/family.d.ts +40 -0
- package/dist/blackboard/family.js +151 -0
- package/dist/blackboard/index.d.ts +21 -0
- package/dist/blackboard/index.js +19 -0
- package/dist/blackboard/schema.d.ts +30 -0
- package/dist/blackboard/schema.js +42 -0
- package/dist/blackboard/store.d.ts +138 -0
- package/dist/blackboard/store.js +216 -0
- package/dist/blackboard/test-db.d.ts +5 -0
- package/dist/blackboard/test-db.js +42 -0
- package/dist/channel/auth.d.ts +41 -0
- package/dist/channel/auth.js +67 -0
- package/dist/channel/clock.d.ts +11 -0
- package/dist/channel/clock.js +4 -0
- package/dist/channel/connection.d.ts +75 -0
- package/dist/channel/connection.js +14 -0
- package/dist/channel/dispatch.d.ts +46 -0
- package/dist/channel/dispatch.js +86 -0
- package/dist/channel/hub.d.ts +78 -0
- package/dist/channel/hub.js +157 -0
- package/dist/channel/index.d.ts +27 -0
- package/dist/channel/index.js +20 -0
- package/dist/channel/registry.d.ts +68 -0
- package/dist/channel/registry.js +84 -0
- package/dist/channel/ws-transport.d.ts +23 -0
- package/dist/channel/ws-transport.js +178 -0
- package/dist/cockpit/boot.d.ts +68 -0
- package/dist/cockpit/boot.js +202 -0
- package/dist/cockpit/fake-dom.d.ts +37 -0
- package/dist/cockpit/fake-dom.js +73 -0
- package/dist/cockpit/index.d.ts +27 -0
- package/dist/cockpit/index.js +27 -0
- package/dist/cockpit/relay-client.d.ts +52 -0
- package/dist/cockpit/relay-client.js +192 -0
- package/dist/cockpit/render.d.ts +58 -0
- package/dist/cockpit/render.js +122 -0
- package/dist/cockpit/terminal-session.d.ts +95 -0
- package/dist/cockpit/terminal-session.js +123 -0
- package/dist/cockpit/view.d.ts +79 -0
- package/dist/cockpit/view.js +58 -0
- package/dist/demand/c8-rest.d.ts +77 -0
- package/dist/demand/c8-rest.js +123 -0
- package/dist/demand/index.d.ts +24 -0
- package/dist/demand/index.js +24 -0
- package/dist/demand/model.d.ts +68 -0
- package/dist/demand/model.js +118 -0
- package/dist/demand/taskdef.d.ts +40 -0
- package/dist/demand/taskdef.js +67 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +17 -0
- package/dist/presence/family.d.ts +40 -0
- package/dist/presence/family.js +166 -0
- package/dist/presence/index.d.ts +19 -0
- package/dist/presence/index.js +17 -0
- package/dist/presence/schema.d.ts +20 -0
- package/dist/presence/schema.js +32 -0
- package/dist/presence/store.d.ts +130 -0
- package/dist/presence/store.js +191 -0
- package/dist/presence/test-db.d.ts +5 -0
- package/dist/presence/test-db.js +42 -0
- package/dist/protocol/conformance/frames.d.ts +24 -0
- package/dist/protocol/conformance/frames.js +116 -0
- package/dist/protocol/conformance/index.d.ts +13 -0
- package/dist/protocol/conformance/index.js +13 -0
- package/dist/protocol/conformance/malformed.d.ts +14 -0
- package/dist/protocol/conformance/malformed.js +44 -0
- package/dist/protocol/conformance/tokens.d.ts +19 -0
- package/dist/protocol/conformance/tokens.js +49 -0
- package/dist/protocol/conformance/vocab.d.ts +23 -0
- package/dist/protocol/conformance/vocab.js +97 -0
- package/dist/protocol/families.d.ts +32 -0
- package/dist/protocol/families.js +45 -0
- package/dist/protocol/frame.d.ts +45 -0
- package/dist/protocol/frame.js +114 -0
- package/dist/protocol/hex.d.ts +7 -0
- package/dist/protocol/hex.js +26 -0
- package/dist/protocol/index.d.ts +23 -0
- package/dist/protocol/index.js +23 -0
- package/dist/protocol/lanes.d.ts +36 -0
- package/dist/protocol/lanes.js +40 -0
- package/dist/protocol/payloads.d.ts +64 -0
- package/dist/protocol/payloads.js +122 -0
- package/dist/protocol/token.d.ts +34 -0
- package/dist/protocol/token.js +81 -0
- package/dist/protocol/vocab/schema.d.ts +51 -0
- package/dist/protocol/vocab/schema.js +218 -0
- package/dist/relay/incarnation.d.ts +17 -0
- package/dist/relay/incarnation.js +50 -0
- package/dist/relay/index.d.ts +25 -0
- package/dist/relay/index.js +22 -0
- package/dist/relay/relay-family.d.ts +68 -0
- package/dist/relay/relay-family.js +272 -0
- package/dist/relay/ring.d.ts +49 -0
- package/dist/relay/ring.js +105 -0
- package/dist/relay/scheduler.d.ts +72 -0
- package/dist/relay/scheduler.js +180 -0
- package/dist/relay/validate.d.ts +29 -0
- package/dist/relay/validate.js +39 -0
- package/dist/transcript/index.d.ts +18 -0
- package/dist/transcript/index.js +17 -0
- package/dist/transcript/schema.d.ts +32 -0
- package/dist/transcript/schema.js +48 -0
- package/dist/transcript/store.d.ts +192 -0
- package/dist/transcript/store.js +347 -0
- package/dist/transcript/test-db.d.ts +5 -0
- package/dist/transcript/test-db.js +41 -0
- package/dist/vocab/core-vocab.d.ts +26 -0
- package/dist/vocab/core-vocab.js +67 -0
- package/dist/vocab/diversity.d.ts +78 -0
- package/dist/vocab/diversity.js +89 -0
- package/dist/vocab/index.d.ts +22 -0
- package/dist/vocab/index.js +22 -0
- package/dist/vocab/merge.d.ts +9 -0
- package/dist/vocab/merge.js +104 -0
- package/dist/vocab/requires.d.ts +49 -0
- package/dist/vocab/requires.js +107 -0
- package/dist/vocab/resolver.d.ts +62 -0
- package/dist/vocab/resolver.js +149 -0
- package/dist/vocab/serve.d.ts +39 -0
- package/dist/vocab/serve.js +36 -0
- package/package.json +108 -0
- package/page/cockpit.css +114 -0
- package/page/cockpit.page.json +33 -0
- package/page/embed.html +40 -0
- package/page/mount.js +78 -0
- package/page/standalone.html +43 -0
- package/src/blackboard/family.test.ts +280 -0
- package/src/blackboard/family.ts +208 -0
- package/src/blackboard/index.ts +42 -0
- package/src/blackboard/schema.test.ts +60 -0
- package/src/blackboard/schema.ts +44 -0
- package/src/blackboard/store.test.ts +189 -0
- package/src/blackboard/store.ts +331 -0
- package/src/blackboard/test-db.ts +47 -0
- package/src/channel/auth.test.ts +64 -0
- package/src/channel/auth.ts +101 -0
- package/src/channel/clock.ts +14 -0
- package/src/channel/connection.ts +77 -0
- package/src/channel/dispatch.test.ts +83 -0
- package/src/channel/dispatch.ts +102 -0
- package/src/channel/hub.test.ts +335 -0
- package/src/channel/hub.ts +222 -0
- package/src/channel/index.ts +55 -0
- package/src/channel/registry.test.ts +73 -0
- package/src/channel/registry.ts +137 -0
- package/src/channel/ws-transport.test.ts +234 -0
- package/src/channel/ws-transport.ts +212 -0
- package/src/cockpit/boot.test.ts +374 -0
- package/src/cockpit/boot.ts +280 -0
- package/src/cockpit/fake-dom.ts +90 -0
- package/src/cockpit/index.ts +63 -0
- package/src/cockpit/relay-client.test.ts +359 -0
- package/src/cockpit/relay-client.ts +234 -0
- package/src/cockpit/render.test.ts +149 -0
- package/src/cockpit/render.ts +194 -0
- package/src/cockpit/terminal-session.test.ts +252 -0
- package/src/cockpit/terminal-session.ts +194 -0
- package/src/cockpit/view.test.ts +117 -0
- package/src/cockpit/view.ts +140 -0
- package/src/demand/c8-rest.test.ts +140 -0
- package/src/demand/c8-rest.ts +167 -0
- package/src/demand/index.ts +42 -0
- package/src/demand/model.test.ts +197 -0
- package/src/demand/model.ts +183 -0
- package/src/demand/taskdef.test.ts +85 -0
- package/src/demand/taskdef.ts +78 -0
- package/src/index.ts +17 -0
- package/src/presence/family.test.ts +252 -0
- package/src/presence/family.ts +205 -0
- package/src/presence/index.ts +26 -0
- package/src/presence/schema.test.ts +53 -0
- package/src/presence/schema.ts +34 -0
- package/src/presence/store.test.ts +190 -0
- package/src/presence/store.ts +287 -0
- package/src/presence/test-db.test.ts +57 -0
- package/src/presence/test-db.ts +47 -0
- package/src/protocol/conformance/corpus.test.ts +66 -0
- package/src/protocol/conformance/frames.ts +142 -0
- package/src/protocol/conformance/index.ts +29 -0
- package/src/protocol/conformance/malformed.ts +59 -0
- package/src/protocol/conformance/tokens.ts +70 -0
- package/src/protocol/conformance/vocab.ts +122 -0
- package/src/protocol/families.ts +54 -0
- package/src/protocol/frame.test.ts +116 -0
- package/src/protocol/frame.ts +171 -0
- package/src/protocol/hex.ts +28 -0
- package/src/protocol/index.ts +84 -0
- package/src/protocol/lanes.test.ts +82 -0
- package/src/protocol/lanes.ts +54 -0
- package/src/protocol/payloads.test.ts +91 -0
- package/src/protocol/payloads.ts +201 -0
- package/src/protocol/token.test.ts +57 -0
- package/src/protocol/token.ts +123 -0
- package/src/protocol/vocab/schema.test.ts +67 -0
- package/src/protocol/vocab/schema.ts +281 -0
- package/src/relay/incarnation.test.ts +53 -0
- package/src/relay/incarnation.ts +54 -0
- package/src/relay/index.ts +34 -0
- package/src/relay/integration.test.ts +135 -0
- package/src/relay/relay-family.test.ts +236 -0
- package/src/relay/relay-family.ts +336 -0
- package/src/relay/ring.test.ts +138 -0
- package/src/relay/ring.ts +136 -0
- package/src/relay/scheduler.test.ts +233 -0
- package/src/relay/scheduler.ts +208 -0
- package/src/relay/validate.test.ts +43 -0
- package/src/relay/validate.ts +44 -0
- package/src/transcript/index.ts +33 -0
- package/src/transcript/integration.test.ts +108 -0
- package/src/transcript/schema.test.ts +69 -0
- package/src/transcript/schema.ts +51 -0
- package/src/transcript/store.test.ts +285 -0
- package/src/transcript/store.ts +530 -0
- package/src/transcript/test-db.ts +46 -0
- package/src/vocab/core-vocab.test.ts +34 -0
- package/src/vocab/core-vocab.ts +88 -0
- package/src/vocab/diversity.test.ts +153 -0
- package/src/vocab/diversity.ts +169 -0
- package/src/vocab/index.ts +55 -0
- package/src/vocab/merge.test.ts +73 -0
- package/src/vocab/merge.ts +117 -0
- package/src/vocab/requires.test.ts +69 -0
- package/src/vocab/requires.ts +155 -0
- package/src/vocab/resolver.test.ts +118 -0
- package/src/vocab/resolver.ts +187 -0
- package/src/vocab/serve.test.ts +64 -0
- package/src/vocab/serve.ts +66 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cockpit boot/orchestration layer — S8.
|
|
3
|
+
*
|
|
4
|
+
* Wires the whole page together from injected capabilities, with **no** direct
|
|
5
|
+
* dependency on the browser, a socket implementation, or xterm.js — everything is
|
|
6
|
+
* passed in ({@link CockpitEnv}). That is what makes the page render *identically*
|
|
7
|
+
* embedded (console App View) and standalone (the two shells differ only in the
|
|
8
|
+
* `host` element and the concrete capabilities they inject), and what makes the
|
|
9
|
+
* live-refresh + drill-in + resume-on-reconnect path unit-testable on Node.
|
|
10
|
+
*
|
|
11
|
+
* Responsibilities:
|
|
12
|
+
* - a **self-scheduling** poll of the S4 demand×supply report that re-renders the
|
|
13
|
+
* matrix/lights each pass (a slow fetch can't overlap the next — mirrors the
|
|
14
|
+
* nano-workforce poller discipline);
|
|
15
|
+
* - **drill-into-a-worker**: open a {@link RelayChannelClient} + {@link TerminalSession}
|
|
16
|
+
* for the selected stream, mount its output into a *persistent* terminal region
|
|
17
|
+
* (so a matrix refresh never wipes it), and re-attach on every reconnect so the
|
|
18
|
+
* terminal **survives a cockpit reconnect** via resume-from-offset.
|
|
19
|
+
*/
|
|
20
|
+
import type { DemandSupplyReport } from "../demand/index.ts";
|
|
21
|
+
import { type Scheduler, type SocketFactory } from "./relay-client.ts";
|
|
22
|
+
import { type DocumentLike, type ElementLike } from "./render.ts";
|
|
23
|
+
import { type TerminalSink } from "./terminal-session.ts";
|
|
24
|
+
/** Mounts a terminal into `host` and returns the sink relay output is written to. */
|
|
25
|
+
export type CreateTerminal = (host: ElementLike) => TerminalSink;
|
|
26
|
+
/** An opaque poll-timer handle (a Node `Timeout` or a browser timer id). */
|
|
27
|
+
export type TimerHandle = unknown;
|
|
28
|
+
export interface CockpitEnv {
|
|
29
|
+
/** The element the cockpit renders into (standalone: `document.body`; embedded: the App View host). */
|
|
30
|
+
readonly host: ElementLike;
|
|
31
|
+
/** The document the renderer creates elements from. */
|
|
32
|
+
readonly doc: DocumentLike;
|
|
33
|
+
/** Fetches the latest S4 demand×supply report (e.g. over HTTP or the channel). */
|
|
34
|
+
readonly fetchReport: () => Promise<DemandSupplyReport>;
|
|
35
|
+
/** Opens a socket to the app relay channel (one per drill-in connection). */
|
|
36
|
+
readonly connectRelay: SocketFactory;
|
|
37
|
+
/** Mounts the terminal widget (xterm.js in the browser) and returns its write sink. */
|
|
38
|
+
readonly createTerminal: CreateTerminal;
|
|
39
|
+
/** Reconnect scheduler for the relay client. Default `setTimeout(run, 0)`. */
|
|
40
|
+
readonly schedule?: Scheduler;
|
|
41
|
+
/** Poll scheduler. Default `setTimeout`. Injected so tests drive it by hand. Must be paired with {@link clearTimer}. */
|
|
42
|
+
readonly setTimer?: (run: () => void, ms: number) => TimerHandle;
|
|
43
|
+
/** Cancels a poll timer. Default `clearTimeout`. Must be paired with {@link setTimer}. */
|
|
44
|
+
readonly clearTimer?: (handle: TimerHandle) => void;
|
|
45
|
+
/** Poll interval in ms. Default 2000. */
|
|
46
|
+
readonly refreshMs?: number;
|
|
47
|
+
/** Bulk credit granted per terminal (re)subscribe. Default 1024. */
|
|
48
|
+
readonly credit?: number;
|
|
49
|
+
/** Notified of a fetch/render/relay error (the poll keeps going). */
|
|
50
|
+
readonly onError?: (err: unknown) => void;
|
|
51
|
+
}
|
|
52
|
+
/** The running cockpit; dispose to stop polling and tear down the terminal. */
|
|
53
|
+
export interface CockpitHandle {
|
|
54
|
+
/** Run one fetch→render pass now (also the poll body). Resolves when rendered. */
|
|
55
|
+
refresh(): Promise<void>;
|
|
56
|
+
/** Start the self-scheduling poll loop (runs one pass immediately). */
|
|
57
|
+
start(): void;
|
|
58
|
+
/** Stop the poll loop (leaves the last render in place). */
|
|
59
|
+
stop(): void;
|
|
60
|
+
/** Drill into a worker's relay stream, opening a resumable live terminal. */
|
|
61
|
+
drill(stream: string): void;
|
|
62
|
+
/** The stream currently drilled into, if any. */
|
|
63
|
+
readonly currentStream: string | undefined;
|
|
64
|
+
/** Stop everything and release the terminal connection. */
|
|
65
|
+
dispose(): void;
|
|
66
|
+
}
|
|
67
|
+
/** Boot the cockpit against an injected environment. Call {@link CockpitHandle.start} to poll. */
|
|
68
|
+
export declare function bootCockpit(env: CockpitEnv): CockpitHandle;
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { isPosInt } from "../relay/index.js";
|
|
2
|
+
import { RelayChannelClient } from "./relay-client.js";
|
|
3
|
+
import { renderCockpit } from "./render.js";
|
|
4
|
+
import { TerminalSession } from "./terminal-session.js";
|
|
5
|
+
import { cockpitView } from "./view.js";
|
|
6
|
+
const DEFAULT_REFRESH_MS = 2000;
|
|
7
|
+
class Cockpit {
|
|
8
|
+
#env;
|
|
9
|
+
#matrixRegion;
|
|
10
|
+
#terminalHost;
|
|
11
|
+
#refreshMs;
|
|
12
|
+
#setTimer;
|
|
13
|
+
#clearTimer;
|
|
14
|
+
#timeouts = new Map();
|
|
15
|
+
#nextTimerId = 0;
|
|
16
|
+
#timer;
|
|
17
|
+
#running = false;
|
|
18
|
+
#disposed = false;
|
|
19
|
+
#drill;
|
|
20
|
+
// The currently mounted terminal, tracked so switching streams (and dispose)
|
|
21
|
+
// tears down the prior xterm instance instead of leaking it + its listeners.
|
|
22
|
+
#terminal;
|
|
23
|
+
// Bumped by every start()/stop() so an in-flight #tick() from a previous
|
|
24
|
+
// start cycle can't reschedule after a stop→start race and leave two
|
|
25
|
+
// overlapping poll chains running against the same cockpit.
|
|
26
|
+
#generation = 0;
|
|
27
|
+
constructor(env) {
|
|
28
|
+
this.#env = env;
|
|
29
|
+
this.#refreshMs = env.refreshMs ?? DEFAULT_REFRESH_MS;
|
|
30
|
+
// refreshMs feeds setTimeout as a poll delay. A negative/NaN/fractional/unsafe
|
|
31
|
+
// value silently collapses to a ~0ms delay, turning the poll into a hot loop
|
|
32
|
+
// that hammers the demand endpoint and burns CPU. Require a positive safe
|
|
33
|
+
// integer up-front so a bad env option fails loudly instead.
|
|
34
|
+
if (!isPosInt(this.#refreshMs)) {
|
|
35
|
+
throw new RangeError(`CockpitEnv.refreshMs must be a positive safe integer, got ${this.#refreshMs}`);
|
|
36
|
+
}
|
|
37
|
+
// setTimer/clearTimer are a matched pair: a caller-supplied setTimer returns
|
|
38
|
+
// opaque handles the default clearTimer (which only understands the internal
|
|
39
|
+
// numeric-handle Map) cannot cancel, leaving an un-stoppable poll loop. Fail
|
|
40
|
+
// fast rather than silently accept one without the other.
|
|
41
|
+
if ((env.setTimer === undefined) !== (env.clearTimer === undefined)) {
|
|
42
|
+
throw new Error("CockpitEnv.setTimer and clearTimer must be provided together (or neither)");
|
|
43
|
+
}
|
|
44
|
+
// Default timer path uses numeric handles backed by a Map, so no unchecked
|
|
45
|
+
// cast is needed to feed an opaque handle back to clearTimeout (`as` is banned).
|
|
46
|
+
this.#setTimer =
|
|
47
|
+
env.setTimer ??
|
|
48
|
+
((run, ms) => {
|
|
49
|
+
const id = this.#nextTimerId++;
|
|
50
|
+
this.#timeouts.set(id, setTimeout(() => {
|
|
51
|
+
this.#timeouts.delete(id);
|
|
52
|
+
run();
|
|
53
|
+
}, ms));
|
|
54
|
+
return id;
|
|
55
|
+
});
|
|
56
|
+
this.#clearTimer =
|
|
57
|
+
env.clearTimer ??
|
|
58
|
+
((handle) => {
|
|
59
|
+
if (typeof handle !== "number")
|
|
60
|
+
return;
|
|
61
|
+
const timeout = this.#timeouts.get(handle);
|
|
62
|
+
if (timeout !== undefined) {
|
|
63
|
+
clearTimeout(timeout);
|
|
64
|
+
this.#timeouts.delete(handle);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
// Build the stable skeleton once: a volatile matrix region the poll
|
|
68
|
+
// re-renders, and a PERSISTENT terminal region a refresh never touches.
|
|
69
|
+
env.host.replaceChildren();
|
|
70
|
+
const shell = env.doc.createElement("div");
|
|
71
|
+
shell.className = "cockpit-shell";
|
|
72
|
+
this.#matrixRegion = env.doc.createElement("div");
|
|
73
|
+
this.#matrixRegion.className = "cockpit-matrix-region";
|
|
74
|
+
const terminalPanel = env.doc.createElement("section");
|
|
75
|
+
terminalPanel.className = "cockpit-terminal";
|
|
76
|
+
const title = env.doc.createElement("h2");
|
|
77
|
+
title.className = "cockpit-panel-title";
|
|
78
|
+
title.textContent = "Worker terminal";
|
|
79
|
+
terminalPanel.appendChild(title);
|
|
80
|
+
this.#terminalHost = env.doc.createElement("div");
|
|
81
|
+
this.#terminalHost.className = "cockpit-terminal-host";
|
|
82
|
+
this.#terminalHost.setAttribute("data-terminal", "host");
|
|
83
|
+
terminalPanel.appendChild(this.#terminalHost);
|
|
84
|
+
shell.appendChild(this.#matrixRegion);
|
|
85
|
+
shell.appendChild(terminalPanel);
|
|
86
|
+
env.host.appendChild(shell);
|
|
87
|
+
}
|
|
88
|
+
get currentStream() {
|
|
89
|
+
return this.#drill?.stream;
|
|
90
|
+
}
|
|
91
|
+
async refresh() {
|
|
92
|
+
if (this.#disposed)
|
|
93
|
+
return;
|
|
94
|
+
let report;
|
|
95
|
+
try {
|
|
96
|
+
report = await this.#env.fetchReport();
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
this.#env.onError?.(err);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (this.#disposed)
|
|
103
|
+
return;
|
|
104
|
+
try {
|
|
105
|
+
renderCockpit(this.#matrixRegion, this.#env.doc, cockpitView(report), {
|
|
106
|
+
onDrill: (stream) => this.drill(stream),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
this.#env.onError?.(err);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
start() {
|
|
114
|
+
if (this.#disposed || this.#running)
|
|
115
|
+
return;
|
|
116
|
+
this.#running = true;
|
|
117
|
+
const generation = ++this.#generation;
|
|
118
|
+
this.#tick(generation);
|
|
119
|
+
}
|
|
120
|
+
stop() {
|
|
121
|
+
this.#running = false;
|
|
122
|
+
// Invalidate any in-flight tick so its finally can't reschedule after this.
|
|
123
|
+
this.#generation++;
|
|
124
|
+
if (this.#timer !== undefined) {
|
|
125
|
+
this.#clearTimer(this.#timer);
|
|
126
|
+
this.#timer = undefined;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
#tick(generation) {
|
|
130
|
+
// Self-scheduling: schedule the NEXT pass only after this one settles, so a
|
|
131
|
+
// slow fetch can never overlap its successor. The generation guard drops a
|
|
132
|
+
// stale tick whose start cycle was already stopped (and possibly restarted),
|
|
133
|
+
// so a stop→start race never leaves two poll chains scheduling in parallel.
|
|
134
|
+
void this.refresh().finally(() => {
|
|
135
|
+
if (generation !== this.#generation || !this.#running || this.#disposed)
|
|
136
|
+
return;
|
|
137
|
+
this.#timer = this.#setTimer(() => this.#tick(generation), this.#refreshMs);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
drill(stream) {
|
|
141
|
+
if (this.#disposed)
|
|
142
|
+
return;
|
|
143
|
+
if (this.#drill?.stream === stream)
|
|
144
|
+
return;
|
|
145
|
+
// Close and drop the prior drill up-front so a synchronous failure while
|
|
146
|
+
// building the new one (e.g. createTerminal, an invalid TerminalSession
|
|
147
|
+
// credit, or connect throwing) can't leave #drill pointing at an
|
|
148
|
+
// already-closed client — currentStream would then report a dead stream.
|
|
149
|
+
// #drill is re-set only once the new client is fully wired and opened, so
|
|
150
|
+
// state stays consistent even when the new drill fails to initialize; the
|
|
151
|
+
// failure is routed to onError like every other relay/terminal error.
|
|
152
|
+
this.#drill?.client.close();
|
|
153
|
+
this.#drill = undefined;
|
|
154
|
+
// Tear down the prior terminal before mounting a fresh one so repeated
|
|
155
|
+
// drills don't leak xterm instances/listeners (replaceChildren only drops
|
|
156
|
+
// the DOM node, not the widget). Tracked in #terminal so a failed (re)drill
|
|
157
|
+
// still cleans it up on the next drill or on dispose().
|
|
158
|
+
this.#terminal?.dispose?.();
|
|
159
|
+
this.#terminal = undefined;
|
|
160
|
+
try {
|
|
161
|
+
// Fresh terminal for the newly selected worker.
|
|
162
|
+
this.#terminalHost.replaceChildren();
|
|
163
|
+
const sink = this.#env.createTerminal(this.#terminalHost);
|
|
164
|
+
this.#terminal = sink;
|
|
165
|
+
let session;
|
|
166
|
+
const client = new RelayChannelClient({
|
|
167
|
+
connect: this.#env.connectRelay,
|
|
168
|
+
onRelay: (message) => session?.handle(message),
|
|
169
|
+
// Re-attach on EVERY (re)connect → resume-from-offset: the terminal
|
|
170
|
+
// survives a cockpit reconnect without losing or double-writing output.
|
|
171
|
+
onOpen: () => session?.attach(),
|
|
172
|
+
schedule: this.#env.schedule,
|
|
173
|
+
onError: (err) => this.#env.onError?.(err),
|
|
174
|
+
});
|
|
175
|
+
session = new TerminalSession({
|
|
176
|
+
stream,
|
|
177
|
+
sink,
|
|
178
|
+
send: (message) => client.sendRelay(message),
|
|
179
|
+
credit: this.#env.credit,
|
|
180
|
+
});
|
|
181
|
+
client.open();
|
|
182
|
+
this.#drill = { stream, client };
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
this.#env.onError?.(err);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
dispose() {
|
|
189
|
+
if (this.#disposed)
|
|
190
|
+
return;
|
|
191
|
+
this.#disposed = true;
|
|
192
|
+
this.stop();
|
|
193
|
+
this.#drill?.client.close();
|
|
194
|
+
this.#drill = undefined;
|
|
195
|
+
this.#terminal?.dispose?.();
|
|
196
|
+
this.#terminal = undefined;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/** Boot the cockpit against an injected environment. Call {@link CockpitHandle.start} to poll. */
|
|
200
|
+
export function bootCockpit(env) {
|
|
201
|
+
return new Cockpit(env);
|
|
202
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A minimal in-memory DOM that satisfies the renderer's structural
|
|
3
|
+
* {@link ElementLike} / {@link DocumentLike} types, for unit tests only.
|
|
4
|
+
*
|
|
5
|
+
* It is NOT part of the published build (excluded in `tsconfig.build.json`); it
|
|
6
|
+
* lets the DOM renderer and the boot layer be exercised on Node with no DOM
|
|
7
|
+
* library and no `as` cast — the fake is *structurally* the same subset the
|
|
8
|
+
* browser's real `Document`/`Element` provide.
|
|
9
|
+
*/
|
|
10
|
+
import type { DocumentLike, ElementLike } from "./render.ts";
|
|
11
|
+
export declare class FakeElement implements ElementLike {
|
|
12
|
+
#private;
|
|
13
|
+
className: string;
|
|
14
|
+
textContent: string | null;
|
|
15
|
+
readonly tagName: string;
|
|
16
|
+
readonly attributes: Map<string, string>;
|
|
17
|
+
readonly children: FakeElement[];
|
|
18
|
+
constructor(tagName: string);
|
|
19
|
+
setAttribute(name: string, value: string): void;
|
|
20
|
+
getAttribute(name: string): string | undefined;
|
|
21
|
+
appendChild(child: ElementLike): ElementLike;
|
|
22
|
+
replaceChildren(): void;
|
|
23
|
+
addEventListener(type: string, handler: () => void): void;
|
|
24
|
+
/** Fire every listener registered for `type` (test driver for clicks). */
|
|
25
|
+
dispatch(type: string): void;
|
|
26
|
+
/** Depth-first walk of this element and its descendants. */
|
|
27
|
+
walk(): IterableIterator<FakeElement>;
|
|
28
|
+
/** Every descendant (and self) whose className contains `cls`. */
|
|
29
|
+
byClass(cls: string): FakeElement[];
|
|
30
|
+
/** Every descendant (and self) with `data-<key>` equal to `value` (any value if omitted). */
|
|
31
|
+
byData(key: string, value?: string): FakeElement[];
|
|
32
|
+
/** The concatenated text content of this subtree (own text then children). */
|
|
33
|
+
text(): string;
|
|
34
|
+
}
|
|
35
|
+
export declare class FakeDocument implements DocumentLike {
|
|
36
|
+
createElement(tagName: string): ElementLike;
|
|
37
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export class FakeElement {
|
|
2
|
+
className = "";
|
|
3
|
+
textContent = null;
|
|
4
|
+
tagName;
|
|
5
|
+
attributes = new Map();
|
|
6
|
+
children = [];
|
|
7
|
+
#listeners = new Map();
|
|
8
|
+
constructor(tagName) {
|
|
9
|
+
this.tagName = tagName;
|
|
10
|
+
}
|
|
11
|
+
setAttribute(name, value) {
|
|
12
|
+
this.attributes.set(name, value);
|
|
13
|
+
}
|
|
14
|
+
getAttribute(name) {
|
|
15
|
+
return this.attributes.get(name);
|
|
16
|
+
}
|
|
17
|
+
appendChild(child) {
|
|
18
|
+
if (child instanceof FakeElement)
|
|
19
|
+
this.children.push(child);
|
|
20
|
+
return child;
|
|
21
|
+
}
|
|
22
|
+
replaceChildren() {
|
|
23
|
+
this.children.length = 0;
|
|
24
|
+
}
|
|
25
|
+
addEventListener(type, handler) {
|
|
26
|
+
const list = this.#listeners.get(type) ?? [];
|
|
27
|
+
list.push(handler);
|
|
28
|
+
this.#listeners.set(type, list);
|
|
29
|
+
}
|
|
30
|
+
/** Fire every listener registered for `type` (test driver for clicks). */
|
|
31
|
+
dispatch(type) {
|
|
32
|
+
for (const handler of this.#listeners.get(type) ?? [])
|
|
33
|
+
handler();
|
|
34
|
+
}
|
|
35
|
+
/** Depth-first walk of this element and its descendants. */
|
|
36
|
+
*walk() {
|
|
37
|
+
yield this;
|
|
38
|
+
for (const child of this.children)
|
|
39
|
+
yield* child.walk();
|
|
40
|
+
}
|
|
41
|
+
/** Every descendant (and self) whose className contains `cls`. */
|
|
42
|
+
byClass(cls) {
|
|
43
|
+
const out = [];
|
|
44
|
+
for (const node of this.walk()) {
|
|
45
|
+
if (node.className.split(/\s+/).includes(cls))
|
|
46
|
+
out.push(node);
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
/** Every descendant (and self) with `data-<key>` equal to `value` (any value if omitted). */
|
|
51
|
+
byData(key, value) {
|
|
52
|
+
const attr = `data-${key}`;
|
|
53
|
+
const out = [];
|
|
54
|
+
for (const node of this.walk()) {
|
|
55
|
+
const got = node.attributes.get(attr);
|
|
56
|
+
if (got !== undefined && (value === undefined || got === value))
|
|
57
|
+
out.push(node);
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
/** The concatenated text content of this subtree (own text then children). */
|
|
62
|
+
text() {
|
|
63
|
+
let out = this.textContent ?? "";
|
|
64
|
+
for (const child of this.children)
|
|
65
|
+
out += child.text();
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export class FakeDocument {
|
|
70
|
+
createElement(tagName) {
|
|
71
|
+
return new FakeElement(tagName);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nanobpm/agentic-cockpit — the visibility page (the cockpit) for the Nano
|
|
3
|
+
* agentic protocol (ADR 0056, slice S8).
|
|
4
|
+
*
|
|
5
|
+
* The operator's cockpit over the one app-tier channel: a per-network
|
|
6
|
+
* demand×supply matrix, a red light per *missing agent type*, the diversity-SLO
|
|
7
|
+
* state (all from the S4 {@link cockpitView} projection of `@nanobpm/agentic-demand`),
|
|
8
|
+
* and drill-into-a-worker → a live terminal over the S5 relay
|
|
9
|
+
* ({@link TerminalSession} + {@link RelayChannelClient}) that **survives a cockpit
|
|
10
|
+
* reconnect** via resume-from-offset.
|
|
11
|
+
*
|
|
12
|
+
* The package is a framework-free, injection-based core: {@link bootCockpit}
|
|
13
|
+
* wires polling, rendering and the terminal from an injected {@link CockpitEnv},
|
|
14
|
+
* with no hard dependency on the DOM, a socket implementation, or xterm.js. Both
|
|
15
|
+
* shells under `page/` — the standalone one and the console App-View embed — call
|
|
16
|
+
* the SAME {@link bootCockpit} against the SAME {@link renderCockpit}, so the page
|
|
17
|
+
* renders identically whether embedded or standalone.
|
|
18
|
+
*
|
|
19
|
+
* The wire contract is `@nanobpm/agentic-protocol`; the report comes from
|
|
20
|
+
* `@nanobpm/agentic-demand`; the relay sub-protocol from `@nanobpm/agentic-relay`.
|
|
21
|
+
* Nothing here rides the Camunda-8 engine or its transport.
|
|
22
|
+
*/
|
|
23
|
+
export { cockpitView, type CockpitLight, type CockpitNetworkRow, type CockpitTokenRow, type CockpitView, type SloStatus, } from "./view.ts";
|
|
24
|
+
export { TerminalSession, type RelayInbound, type RelayOutbound, type RelaySend, type TerminalSessionOptions, type TerminalSink, } from "./terminal-session.ts";
|
|
25
|
+
export { RelayChannelClient, type RawSocket, type RelayChannelClientOptions, type Scheduler, type SocketFactory, } from "./relay-client.ts";
|
|
26
|
+
export { renderCockpit, type CockpitDom, type DocumentLike, type ElementLike, type RenderOptions, } from "./render.ts";
|
|
27
|
+
export { bootCockpit, type CockpitEnv, type CockpitHandle, type CreateTerminal, type TimerHandle, } from "./boot.ts";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nanobpm/agentic-cockpit — the visibility page (the cockpit) for the Nano
|
|
3
|
+
* agentic protocol (ADR 0056, slice S8).
|
|
4
|
+
*
|
|
5
|
+
* The operator's cockpit over the one app-tier channel: a per-network
|
|
6
|
+
* demand×supply matrix, a red light per *missing agent type*, the diversity-SLO
|
|
7
|
+
* state (all from the S4 {@link cockpitView} projection of `@nanobpm/agentic-demand`),
|
|
8
|
+
* and drill-into-a-worker → a live terminal over the S5 relay
|
|
9
|
+
* ({@link TerminalSession} + {@link RelayChannelClient}) that **survives a cockpit
|
|
10
|
+
* reconnect** via resume-from-offset.
|
|
11
|
+
*
|
|
12
|
+
* The package is a framework-free, injection-based core: {@link bootCockpit}
|
|
13
|
+
* wires polling, rendering and the terminal from an injected {@link CockpitEnv},
|
|
14
|
+
* with no hard dependency on the DOM, a socket implementation, or xterm.js. Both
|
|
15
|
+
* shells under `page/` — the standalone one and the console App-View embed — call
|
|
16
|
+
* the SAME {@link bootCockpit} against the SAME {@link renderCockpit}, so the page
|
|
17
|
+
* renders identically whether embedded or standalone.
|
|
18
|
+
*
|
|
19
|
+
* The wire contract is `@nanobpm/agentic-protocol`; the report comes from
|
|
20
|
+
* `@nanobpm/agentic-demand`; the relay sub-protocol from `@nanobpm/agentic-relay`.
|
|
21
|
+
* Nothing here rides the Camunda-8 engine or its transport.
|
|
22
|
+
*/
|
|
23
|
+
export { cockpitView, } from "./view.js";
|
|
24
|
+
export { TerminalSession, } from "./terminal-session.js";
|
|
25
|
+
export { RelayChannelClient, } from "./relay-client.js";
|
|
26
|
+
export { renderCockpit, } from "./render.js";
|
|
27
|
+
export { bootCockpit, } from "./boot.js";
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { RelayInbound, RelayOutbound } from "./terminal-session.ts";
|
|
2
|
+
/** The minimal socket surface the client drives (a browser `WebSocket` adapts to this). */
|
|
3
|
+
export interface RawSocket {
|
|
4
|
+
/** Send one binary frame. */
|
|
5
|
+
send(bytes: Uint8Array): void;
|
|
6
|
+
/** Close the socket. */
|
|
7
|
+
close(): void;
|
|
8
|
+
/** Register the inbound-binary-frame listener. */
|
|
9
|
+
onMessage(listener: (bytes: Uint8Array) => void): void;
|
|
10
|
+
/** Register the open listener (fired once per successful connect). */
|
|
11
|
+
onOpen(listener: () => void): void;
|
|
12
|
+
/** Register the close listener (fired once when the socket drops). */
|
|
13
|
+
onClose(listener: () => void): void;
|
|
14
|
+
}
|
|
15
|
+
/** Opens a fresh socket. Called once per (re)connect. */
|
|
16
|
+
export type SocketFactory = () => RawSocket;
|
|
17
|
+
/** Schedules a reconnect attempt. Injected so tests can run it synchronously. */
|
|
18
|
+
export type Scheduler = (run: () => void) => void;
|
|
19
|
+
export interface RelayChannelClientOptions {
|
|
20
|
+
/** Opens a fresh socket for each (re)connect. */
|
|
21
|
+
readonly connect: SocketFactory;
|
|
22
|
+
/** Receives every inbound `relay`-family payload (data chunk or resume ack). */
|
|
23
|
+
readonly onRelay: (message: RelayInbound) => void;
|
|
24
|
+
/** Fired on every (re)connect — the caller re-attaches the session here. */
|
|
25
|
+
readonly onOpen?: () => void;
|
|
26
|
+
/** Fired when the socket drops (before a reconnect is scheduled). */
|
|
27
|
+
readonly onClose?: () => void;
|
|
28
|
+
/** Notified of a decode/dispatch error on an inbound frame. */
|
|
29
|
+
readonly onError?: (err: unknown) => void;
|
|
30
|
+
/** Reconnect scheduler. Default `setTimeout(run, 0)`. */
|
|
31
|
+
readonly schedule?: Scheduler;
|
|
32
|
+
/** Reconnect automatically on close. Default true. */
|
|
33
|
+
readonly autoReconnect?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Manages one relay socket with automatic resume-on-reconnect. Construct once
|
|
37
|
+
* per drill-in and pair with a {@link TerminalSession}: wire the session's
|
|
38
|
+
* `send` to {@link sendRelay}, feed {@link RelayChannelClientOptions.onRelay} to
|
|
39
|
+
* `session.handle`, and call `session.attach()` from `onOpen`.
|
|
40
|
+
*/
|
|
41
|
+
export declare class RelayChannelClient {
|
|
42
|
+
#private;
|
|
43
|
+
constructor(options: RelayChannelClientOptions);
|
|
44
|
+
/** True once {@link close} has been called (no further reconnects). */
|
|
45
|
+
get isClosed(): boolean;
|
|
46
|
+
/** Open the first socket and wire its lifecycle. Idempotent while connected. */
|
|
47
|
+
open(): void;
|
|
48
|
+
/** Encode and send one outbound relay message on the control lane. */
|
|
49
|
+
sendRelay(message: RelayOutbound): void;
|
|
50
|
+
/** Close for good — no reconnect will follow. */
|
|
51
|
+
close(): void;
|
|
52
|
+
}
|