@animalabs/connectome-host 0.7.2 → 0.7.4
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/CHANGELOG.md +203 -10
- package/HEADLESS-FLEET-PLAN.md +22 -0
- package/README.md +22 -11
- package/docs/AGENT-ONBOARDING.md +20 -1
- package/docs/debug-context-api.md +2 -2
- package/docs/retrieval-traces.md +173 -0
- package/docs/webui-deployment.md +2 -1
- package/package.json +3 -3
- package/scripts/audit-module-optins.ts +288 -0
- package/scripts/warmup-session.ts +17 -3
- package/src/codex-subscription-adapter.ts +13 -1
- package/src/framework-agent-config.ts +59 -4
- package/src/framework-strategy.ts +33 -3
- package/src/headless.ts +14 -0
- package/src/index.ts +95 -35
- package/src/logging-adapter.ts +13 -2
- package/src/mcpl-config.ts +8 -0
- package/src/modules/fleet-module.ts +60 -1
- package/src/modules/fleet-types.ts +30 -1
- package/src/modules/identity-module.ts +274 -0
- package/src/modules/mcpl-admin-module.ts +78 -5
- package/src/modules/observers-module.ts +12 -0
- package/src/modules/retrieval-module.ts +254 -52
- package/src/modules/retrieval-trace-page.ts +254 -0
- package/src/modules/retrieval-trace.ts +904 -0
- package/src/modules/settings-module.ts +28 -2
- package/src/modules/subscription-gc-module.ts +54 -1
- package/src/modules/tts-relay-module.ts +33 -18
- package/src/modules/web-ui-module.ts +445 -894
- package/src/recipe.ts +137 -12
- package/src/retrieval-config.ts +39 -0
- package/src/strategies/frontdesk-strategy.ts +34 -125
- package/src/tui.ts +325 -54
- package/src/web/panel-data.ts +1187 -0
- package/src/web/protocol.ts +75 -10
- package/test/audit-module-optins.test.ts +167 -0
- package/test/bedrock-prompt-caching.test.ts +170 -0
- package/test/fleet-panel-request.test.ts +90 -0
- package/test/framework-strategy-defaults.test.ts +110 -0
- package/test/frontdesk-strategy.test.ts +25 -37
- package/test/headless-panel-request.test.ts +201 -0
- package/test/identity-and-surfaces.test.ts +157 -0
- package/test/mcpl-admin-module.test.ts +23 -0
- package/test/mock-headless-child.ts +14 -0
- package/test/retrieval-auth-loopback.test.ts +49 -0
- package/test/retrieval-config.test.ts +74 -0
- package/test/retrieval-module.test.ts +821 -0
- package/test/subscription-gc-module.test.ts +152 -0
- package/test/tui-format.test.ts +106 -0
- package/test/web-ui-context-coverage.test.ts +1 -1
- package/test/web-ui-module.test.ts +189 -3
- package/test/web-ui-observers.test.ts +8 -5
- package/test/web-ui-protocol.test.ts +0 -0
- package/web/bun.lock +345 -0
- package/web/src/App.tsx +159 -44
- package/web/src/Context.tsx +35 -8
- package/web/src/ContextDocument.tsx +20 -5
- package/web/src/Files.tsx +2 -8
- package/web/src/Lessons.tsx +2 -38
- package/web/src/Mcpl.tsx +80 -14
- package/web/src/Pins.tsx +5 -0
- package/web/src/Settings.tsx +5 -0
- package/web/vite.config.ts +8 -2
package/src/index.ts
CHANGED
|
@@ -37,6 +37,7 @@ import { readFileSync, existsSync } from 'node:fs';
|
|
|
37
37
|
import { SubagentModule } from './modules/subagent-module.js';
|
|
38
38
|
import { LessonsModule } from './modules/lessons-module.js';
|
|
39
39
|
import { RetrievalModule } from './modules/retrieval-module.js';
|
|
40
|
+
import { buildRetrievalModuleConfig } from './retrieval-config.js';
|
|
40
41
|
import type { RecipeWorkspaceMount } from './recipe.js';
|
|
41
42
|
import { TuiModule } from './modules/tui-module.js';
|
|
42
43
|
import { TimeModule } from './modules/time-module.js';
|
|
@@ -46,6 +47,7 @@ import { SubscriptionGcModule } from './modules/subscription-gc-module.js';
|
|
|
46
47
|
import { ChannelModeModule } from './modules/channel-mode-module.js';
|
|
47
48
|
import { WebUiModule } from './modules/web-ui-module.js';
|
|
48
49
|
import { ObserversModule } from './modules/observers-module.js';
|
|
50
|
+
import { IdentityModule } from './modules/identity-module.js';
|
|
49
51
|
import { McplAdminModule } from './modules/mcpl-admin-module.js';
|
|
50
52
|
import { TtsRelayModule } from './modules/tts-relay-module.js';
|
|
51
53
|
import { loadMcplServers, applyAgentOverlay, DEFAULT_CONFIG_PATH, DEFAULT_AGENT_OVERLAY_PATH } from './mcpl-config.js';
|
|
@@ -62,7 +64,7 @@ import {
|
|
|
62
64
|
parseRecipeArg,
|
|
63
65
|
} from './recipe.js';
|
|
64
66
|
import { createBranchState, resetBranchState, handleExport, type BranchState } from './commands.js';
|
|
65
|
-
import { buildFrameworkAgentConfig } from './framework-agent-config.js';
|
|
67
|
+
import { buildFrameworkAgentConfig, membraneCachingOverride } from './framework-agent-config.js';
|
|
66
68
|
import { buildFrameworkStrategy } from './framework-strategy.js';
|
|
67
69
|
import { loadExtensions } from './extensions.js';
|
|
68
70
|
|
|
@@ -102,6 +104,10 @@ interface AppContext {
|
|
|
102
104
|
branchState: BranchState;
|
|
103
105
|
userMessageCount: number;
|
|
104
106
|
codexAdapter?: CodexSubscriptionAdapter;
|
|
107
|
+
/** Content-free recent provider-call ledger. Consumed by the panel-data
|
|
108
|
+
* layer (health snapshots) in BOTH runtimes — WebUI host and headless
|
|
109
|
+
* fleet child. Null when the provider adapter exposes no ledger. */
|
|
110
|
+
callLedger: CallLedger | null;
|
|
105
111
|
|
|
106
112
|
/** Stop current framework, switch to a different session, start new framework. */
|
|
107
113
|
switchSession(id: string): Promise<void>;
|
|
@@ -146,6 +152,11 @@ async function resolveRecipe(): Promise<Recipe> {
|
|
|
146
152
|
// Framework factory
|
|
147
153
|
// ---------------------------------------------------------------------------
|
|
148
154
|
|
|
155
|
+
function resolveModel(recipe: Recipe): string {
|
|
156
|
+
return config.model || recipe.agent.model ||
|
|
157
|
+
(recipe.agent.provider === 'openai-codex' ? 'gpt-5.4' : 'claude-opus-4-6');
|
|
158
|
+
}
|
|
159
|
+
|
|
149
160
|
async function createFramework(
|
|
150
161
|
membrane: Membrane,
|
|
151
162
|
storePath: string,
|
|
@@ -154,8 +165,7 @@ async function createFramework(
|
|
|
154
165
|
settingsModule: SettingsModule,
|
|
155
166
|
callLedger: CallLedger | null,
|
|
156
167
|
): Promise<AgentFramework> {
|
|
157
|
-
const model =
|
|
158
|
-
(recipe.agent.provider === 'openai-codex' ? 'gpt-5.4' : 'claude-opus-4-6');
|
|
168
|
+
const model = resolveModel(recipe);
|
|
159
169
|
const modules = recipe.modules ?? {};
|
|
160
170
|
const timeZone = resolveTimeZone(recipe.agent.timezone);
|
|
161
171
|
|
|
@@ -169,9 +179,10 @@ async function createFramework(
|
|
|
169
179
|
// adapter can read its state for cross-cutting concerns like reasoning).
|
|
170
180
|
const moduleInstances: Module[] = [new TuiModule(), new TimeModule(timeZone), settingsModule];
|
|
171
181
|
|
|
172
|
-
// Subagents
|
|
182
|
+
// Subagents. OPT-IN — not part of the standard recipe; enable explicitly
|
|
183
|
+
// via modules.subagents when an agent should fork parallel workers.
|
|
173
184
|
let subagentModule: SubagentModule | null = null;
|
|
174
|
-
if (modules.subagents
|
|
185
|
+
if (modules.subagents) {
|
|
175
186
|
const subagentConfig = typeof modules.subagents === 'object' ? modules.subagents : {};
|
|
176
187
|
subagentModule = new SubagentModule({
|
|
177
188
|
parentAgentName: agentName,
|
|
@@ -181,9 +192,10 @@ async function createFramework(
|
|
|
181
192
|
moduleInstances.push(subagentModule);
|
|
182
193
|
}
|
|
183
194
|
|
|
184
|
-
// Lessons
|
|
195
|
+
// Lessons. OPT-IN — not part of the standard recipe; enable explicitly via
|
|
196
|
+
// modules.lessons for agents that curate a lesson library.
|
|
185
197
|
let lessonsModule: LessonsModule | null = null;
|
|
186
|
-
if (modules.lessons
|
|
198
|
+
if (modules.lessons) {
|
|
187
199
|
const globalLessonsPath = resolve(join(storePath, '..', '..', 'lessons.json'));
|
|
188
200
|
lessonsModule = new LessonsModule({ globalPath: globalLessonsPath });
|
|
189
201
|
moduleInstances.push(lessonsModule);
|
|
@@ -219,14 +231,14 @@ async function createFramework(
|
|
|
219
231
|
moduleInstances.push(new FleetModule(fleetModuleConfig));
|
|
220
232
|
}
|
|
221
233
|
|
|
222
|
-
// Retrieval (requires lessons)
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
234
|
+
// Retrieval (requires lessons). OPT-IN — not part of the standard recipe:
|
|
235
|
+
// it injects context-dependent content into every compile (plus up to two
|
|
236
|
+
// configured retrieval-model calls), which adds per-turn context churn.
|
|
237
|
+
// Enable explicitly only when an agent actually curates a lesson library.
|
|
238
|
+
if (modules.retrieval && lessonsModule) {
|
|
239
|
+
moduleInstances.push(new RetrievalModule(
|
|
240
|
+
buildRetrievalModuleConfig(membrane, modules.retrieval, recipe.agent.provider),
|
|
241
|
+
));
|
|
230
242
|
}
|
|
231
243
|
|
|
232
244
|
// Gate config — core AF EventGate feature.
|
|
@@ -332,11 +344,27 @@ async function createFramework(
|
|
|
332
344
|
// MCPL self-administration — opt-in per recipe (grants the agent the
|
|
333
345
|
// ability to spawn arbitrary commands via mcpl_deploy; see recipe.ts).
|
|
334
346
|
let mcplAdminModule: McplAdminModule | null = null;
|
|
335
|
-
if (modules.mcplAdmin === true) {
|
|
336
|
-
|
|
347
|
+
if (modules.mcplAdmin === true || typeof modules.mcplAdmin === 'object') {
|
|
348
|
+
const surface = typeof modules.mcplAdmin === 'object' ? modules.mcplAdmin.surface : undefined;
|
|
349
|
+
mcplAdminModule = new McplAdminModule({ timeZone, ...(surface ? { surface } : {}) });
|
|
337
350
|
moduleInstances.push(mcplAdminModule);
|
|
338
351
|
}
|
|
339
352
|
|
|
353
|
+
// Archipelago identity — opt-in per recipe. Utilities-only: enrollment is
|
|
354
|
+
// one-time and token refresh is rare, so it costs no tool slots (see
|
|
355
|
+
// identity-module.ts header). Keypair lives at the dataDir level — an
|
|
356
|
+
// identity belongs to the deployment, not the session.
|
|
357
|
+
let identityModule: IdentityModule | null = null;
|
|
358
|
+
if (modules.identity !== undefined && modules.identity !== false) {
|
|
359
|
+
const idCfg = typeof modules.identity === 'object' ? modules.identity : {};
|
|
360
|
+
identityModule = new IdentityModule({
|
|
361
|
+
keyPath: process.env.IDENTITY_KEY_FILE || resolve(config.dataDir, 'identity-key.pem'),
|
|
362
|
+
home: idCfg.home ?? process.env.IDENTITY_HOME ?? 'id.animalabs.ai',
|
|
363
|
+
...(idCfg.audience ? { defaultAudience: idCfg.audience } : {}),
|
|
364
|
+
});
|
|
365
|
+
moduleInstances.push(identityModule);
|
|
366
|
+
}
|
|
367
|
+
|
|
340
368
|
// Web admin UI — opt-in per recipe
|
|
341
369
|
let webUiModule: WebUiModule | null = null;
|
|
342
370
|
if (modules.webui !== undefined && modules.webui !== false) {
|
|
@@ -356,7 +384,10 @@ async function createFramework(
|
|
|
356
384
|
...(callLedger ? { callLedger } : {}),
|
|
357
385
|
});
|
|
358
386
|
moduleInstances.push(webUiModule);
|
|
359
|
-
moduleInstances.push(new ObserversModule({
|
|
387
|
+
moduleInstances.push(new ObserversModule({
|
|
388
|
+
path: observersPath,
|
|
389
|
+
...(webuiConfig.observersSurface ? { surface: webuiConfig.observersSurface } : {}),
|
|
390
|
+
}));
|
|
360
391
|
}
|
|
361
392
|
|
|
362
393
|
// TTS relay tap — opt-in per recipe. Pure trace-bus consumer: mirrors the
|
|
@@ -415,6 +446,7 @@ async function createFramework(
|
|
|
415
446
|
if (recipeEntry.url !== undefined) merged.url = recipeEntry.url;
|
|
416
447
|
if (recipeEntry.transport !== undefined) merged.transport = recipeEntry.transport;
|
|
417
448
|
if (recipeEntry.token !== undefined) merged.token = recipeEntry.token;
|
|
449
|
+
if (recipeEntry.access !== undefined) merged.access = recipeEntry.access;
|
|
418
450
|
allServers.push(merged as { id: string; command?: string; url?: string; [k: string]: unknown });
|
|
419
451
|
} else if (recipeEntry.command || recipeEntry.url) {
|
|
420
452
|
// Recipe-defined server (not in the file config). Spread ALL recipe fields
|
|
@@ -426,12 +458,30 @@ async function createFramework(
|
|
|
426
458
|
// Apply the agent overlay (mcpl-servers.agent.json): servers the agent
|
|
427
459
|
// deployed for itself load unconditionally (no recipe opt-in), and
|
|
428
460
|
// tombstones suppress recipe/file servers the agent unloaded.
|
|
429
|
-
const finalServers = applyAgentOverlay(allServers, DEFAULT_AGENT_OVERLAY_PATH).map((server) =>
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
461
|
+
const finalServers = applyAgentOverlay(allServers, DEFAULT_AGENT_OVERLAY_PATH).map((server) => {
|
|
462
|
+
const withEnv: { id: string; command?: string; url?: string; [k: string]: unknown } = {
|
|
463
|
+
...server,
|
|
464
|
+
// Stdio MCPL children inherit a single agent-facing wall clock. Protocol
|
|
465
|
+
// timestamps remain UTC; only their rendered text uses this setting.
|
|
466
|
+
env: { ...(server.env ?? {}), AGENT_TIMEZONE: timeZone },
|
|
467
|
+
};
|
|
468
|
+
// `access` is a declarative name (recipe/file/overlay); the credential
|
|
469
|
+
// provider it implies is attached HERE, at load time — fresh credential
|
|
470
|
+
// per dial via the identity module, never serialized, never in model
|
|
471
|
+
// context (see identity-module.ts header).
|
|
472
|
+
if (typeof withEnv.access === 'string' && withEnv.access) {
|
|
473
|
+
if (identityModule) {
|
|
474
|
+
const identity = identityModule;
|
|
475
|
+
const audience = withEnv.access as string;
|
|
476
|
+
withEnv.accessProvider = () => identity.accessFor(audience);
|
|
477
|
+
} else {
|
|
478
|
+
console.error(
|
|
479
|
+
`[mcpl] server "${server.id}": access "${withEnv.access}" declared but the recipe has no identity module — connecting without credentials`,
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return withEnv;
|
|
484
|
+
});
|
|
435
485
|
|
|
436
486
|
// No server augmentation needed — gate is wired via FrameworkConfig.gate
|
|
437
487
|
|
|
@@ -513,6 +563,7 @@ agents: [agentConfig],
|
|
|
513
563
|
|
|
514
564
|
if (mcplAdminModule) {
|
|
515
565
|
mcplAdminModule.setFramework(framework);
|
|
566
|
+
if (identityModule) mcplAdminModule.setIdentity(identityModule);
|
|
516
567
|
}
|
|
517
568
|
|
|
518
569
|
if (workspaceModule) {
|
|
@@ -827,13 +878,18 @@ async function main() {
|
|
|
827
878
|
xTitle: recipe.agent.name ?? recipe.name,
|
|
828
879
|
})
|
|
829
880
|
: undefined;
|
|
830
|
-
// Bedrock:
|
|
831
|
-
// left the
|
|
832
|
-
//
|
|
833
|
-
//
|
|
834
|
-
//
|
|
835
|
-
//
|
|
836
|
-
//
|
|
881
|
+
// Bedrock: an alternate Claude transport. (Historically for models that
|
|
882
|
+
// left the direct API — as of 2026-07-31 every 3.5-era id and opus-4-0514
|
|
883
|
+
// are EOL on Bedrock too, so what actually runs here is the 4-era via
|
|
884
|
+
// inference profiles.) The adapter reads AWS_* env vars (AWS_REGION
|
|
885
|
+
// defaults us-west-2) and maps standard Claude model IDs to Bedrock IDs.
|
|
886
|
+
// Uses the Anthropic-native message shape, so NativeFormatter applies
|
|
887
|
+
// unchanged. Prompt caching is model-gated
|
|
888
|
+
// (bedrockModelSupportsPromptCaching): pre-GA families (Claude 3,
|
|
889
|
+
// 3.5 Sonnet) off, everything currently invokable caches — verified by
|
|
890
|
+
// live probe 2026-07-31. Still no CallLedger — it's
|
|
891
|
+
// anthropic-transport-only for now; cache metrics are visible in
|
|
892
|
+
// llm-calls.jsonl via the logging wrapper.
|
|
837
893
|
const bedrockAdapter = provider === 'bedrock'
|
|
838
894
|
? new LoggingBedrockAdapter({}, llmLogPath)
|
|
839
895
|
: undefined;
|
|
@@ -915,10 +971,13 @@ async function main() {
|
|
|
915
971
|
: recipe.agent.formatter === 'anthropic-xml'
|
|
916
972
|
? new AnthropicXmlFormatter()
|
|
917
973
|
: new NativeFormatter(),
|
|
918
|
-
//
|
|
919
|
-
//
|
|
920
|
-
//
|
|
921
|
-
|
|
974
|
+
// Caching default for internal callers (autobio compression,
|
|
975
|
+
// executeMerge), which read Membrane's defaultPromptCaching rather
|
|
976
|
+
// than the per-agent flag. Applies on EVERY provider whenever the
|
|
977
|
+
// recipe/model resolves an explicit answer — an Anthropic recipe with
|
|
978
|
+
// promptCaching:false must disable internal calls too, and on bedrock
|
|
979
|
+
// the model gate decides (see bedrockModelSupportsPromptCaching).
|
|
980
|
+
...membraneCachingOverride(recipe, resolveModel(recipe)),
|
|
922
981
|
// Anchor the assistant role for internal callers that don't set
|
|
923
982
|
// request.assistantParticipant themselves (autobio compression,
|
|
924
983
|
// executeMerge). Mismatch here flips stored assistant turns to
|
|
@@ -939,6 +998,7 @@ async function main() {
|
|
|
939
998
|
branchState: createBranchState(),
|
|
940
999
|
userMessageCount: 0,
|
|
941
1000
|
codexAdapter,
|
|
1001
|
+
callLedger,
|
|
942
1002
|
|
|
943
1003
|
async switchSession(id: string) {
|
|
944
1004
|
handleExport(this);
|
package/src/logging-adapter.ts
CHANGED
|
@@ -32,7 +32,11 @@ import { summarizeCacheControls, type ProviderCallRecord } from './call-ledger.j
|
|
|
32
32
|
/** Live read of the current reasoning setting. The host wires this to
|
|
33
33
|
* `SettingsModule.getReasoning()` so toggles via the `agent_settings` tool's
|
|
34
34
|
* reasoning_enabled field take effect on the next call without restart. */
|
|
35
|
-
export type ReasoningGetter = () => {
|
|
35
|
+
export type ReasoningGetter = () => {
|
|
36
|
+
enabled: boolean;
|
|
37
|
+
budgetTokens: number;
|
|
38
|
+
display?: 'summarized' | 'omitted';
|
|
39
|
+
};
|
|
36
40
|
export type ProviderCallObserver = (record: ProviderCallRecord) => void;
|
|
37
41
|
|
|
38
42
|
/** Exact first-system-block identity Anthropic requires on subscription
|
|
@@ -119,9 +123,16 @@ export class LoggingAnthropicAdapter extends AnthropicAdapter {
|
|
|
119
123
|
// request.extra; Object.assign(params, rest)`), so we route thinking
|
|
120
124
|
// through the typed `extra` bag — no type assertion, and it survives the
|
|
121
125
|
// next dependency reshuffle instead of hiding it from the compiler.
|
|
126
|
+
//
|
|
127
|
+
// `display`: models 4.7+ default to 'omitted' (empty `thinking` text,
|
|
128
|
+
// signature only). We pass the setting through — default 'summarized' —
|
|
129
|
+
// so reasoning summaries are visible again (stores, webui, estimators).
|
|
122
130
|
return {
|
|
123
131
|
...request,
|
|
124
|
-
extra: {
|
|
132
|
+
extra: {
|
|
133
|
+
...request.extra,
|
|
134
|
+
thinking: { type: 'adaptive', display: r.display ?? 'summarized' },
|
|
135
|
+
},
|
|
125
136
|
};
|
|
126
137
|
}
|
|
127
138
|
|
package/src/mcpl-config.ts
CHANGED
|
@@ -27,6 +27,14 @@ export interface ServerFileEntry {
|
|
|
27
27
|
disabledTools?: string[];
|
|
28
28
|
/** @deprecated One-time migration input for legacy installations. */
|
|
29
29
|
channelSubscription?: 'auto' | 'manual' | string[];
|
|
30
|
+
/**
|
|
31
|
+
* Name of a network access grant (an archipelago audience, e.g.
|
|
32
|
+
* "eidoverse"). Purely declarative here: at load/deploy time the host
|
|
33
|
+
* attaches a credential provider that fetches something fresh on every
|
|
34
|
+
* dial via the identity module. The agent (and this file) never holds a
|
|
35
|
+
* credential — `access` is a name, not a secret.
|
|
36
|
+
*/
|
|
37
|
+
access?: string;
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
export interface McplServersFile {
|
|
@@ -34,7 +34,7 @@ import { spawn as spawnProcess, type ChildProcess } from 'node:child_process';
|
|
|
34
34
|
import { connect as netConnect, type Socket } from 'node:net';
|
|
35
35
|
import { existsSync, mkdirSync, unlinkSync, openSync, closeSync, appendFileSync, realpathSync } from 'node:fs';
|
|
36
36
|
import { join, resolve, isAbsolute } from 'node:path';
|
|
37
|
-
import { type IncomingCommand, type WireEvent, matchesSubscription } from './fleet-types.js';
|
|
37
|
+
import { type IncomingCommand, type WireEvent, type PanelResponseEvent, matchesSubscription } from './fleet-types.js';
|
|
38
38
|
import { loadRecipe } from '../recipe.js';
|
|
39
39
|
import { REDUCER_REQUIRED_EVENTS } from '../state/agent-tree-reducer.js';
|
|
40
40
|
|
|
@@ -1621,6 +1621,65 @@ export class FleetModule implements Module {
|
|
|
1621
1621
|
catch { return false; }
|
|
1622
1622
|
}
|
|
1623
1623
|
|
|
1624
|
+
/** Monotonic corrId source for requestPanel. */
|
|
1625
|
+
private panelSeq = 0;
|
|
1626
|
+
|
|
1627
|
+
/**
|
|
1628
|
+
* Run one operator-panel op (see src/web/panel-data.ts) in a fleet child
|
|
1629
|
+
* and await its `panel-response`. Promise-based counterpart to the
|
|
1630
|
+
* fire-and-forget request* verbs above: HTTP proxy routes need to await a
|
|
1631
|
+
* body, and the WS handlers are simpler for it too.
|
|
1632
|
+
*
|
|
1633
|
+
* Never rejects — a dead child, send failure, or timeout resolves as
|
|
1634
|
+
* `{ok:false, error, status}` (502 unreachable, 504 timeout), so callers
|
|
1635
|
+
* translate straight into a response without try/catch.
|
|
1636
|
+
*/
|
|
1637
|
+
requestPanel(
|
|
1638
|
+
childName: string,
|
|
1639
|
+
op: string,
|
|
1640
|
+
params?: Record<string, unknown>,
|
|
1641
|
+
timeoutMs = 30_000,
|
|
1642
|
+
): Promise<{ ok: boolean; data?: unknown; error?: string; status?: number }> {
|
|
1643
|
+
const child = this.children.get(childName);
|
|
1644
|
+
if (!child || !child.socket) {
|
|
1645
|
+
return Promise.resolve({
|
|
1646
|
+
ok: false,
|
|
1647
|
+
error: child ? `child '${childName}' is ${child.status}, not running` : `unknown child: ${childName}`,
|
|
1648
|
+
status: child ? 502 : 404,
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
const corrId = `panel-${op}-${++this.panelSeq}-${Date.now().toString(36)}`;
|
|
1652
|
+
return new Promise((resolvePanel) => {
|
|
1653
|
+
let settled = false;
|
|
1654
|
+
const finish = (result: { ok: boolean; data?: unknown; error?: string; status?: number }): void => {
|
|
1655
|
+
if (settled) return;
|
|
1656
|
+
settled = true;
|
|
1657
|
+
unsub();
|
|
1658
|
+
clearTimeout(timer);
|
|
1659
|
+
resolvePanel(result);
|
|
1660
|
+
};
|
|
1661
|
+
const unsub = this.onChildEvent(childName, (_name, evt) => {
|
|
1662
|
+
if (evt.type !== 'panel-response') return;
|
|
1663
|
+
const e = evt as unknown as PanelResponseEvent;
|
|
1664
|
+
if (e.corrId !== corrId) return;
|
|
1665
|
+
finish({
|
|
1666
|
+
ok: e.ok === true,
|
|
1667
|
+
...(e.data !== undefined ? { data: e.data } : {}),
|
|
1668
|
+
...(typeof e.error === 'string' ? { error: e.error } : {}),
|
|
1669
|
+
...(typeof e.status === 'number' ? { status: e.status } : {}),
|
|
1670
|
+
});
|
|
1671
|
+
});
|
|
1672
|
+
const timer = setTimeout(() => {
|
|
1673
|
+
finish({ ok: false, error: `panel op '${op}' timed out after ${timeoutMs}ms (child '${childName}' unresponsive)`, status: 504 });
|
|
1674
|
+
}, timeoutMs);
|
|
1675
|
+
try {
|
|
1676
|
+
this.sendToChild(child, { type: 'panel-request', op, ...(params ? { params } : {}), corrId });
|
|
1677
|
+
} catch (err) {
|
|
1678
|
+
finish({ ok: false, error: `send to child failed: ${err instanceof Error ? err.message : String(err)}`, status: 502 });
|
|
1679
|
+
}
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1624
1683
|
private async killChild(child: FleetChild): Promise<void> {
|
|
1625
1684
|
if (child.status === 'exited' || child.status === 'crashed') return;
|
|
1626
1685
|
const proc = child.process;
|
|
@@ -38,7 +38,18 @@ export type IncomingCommand =
|
|
|
38
38
|
* `cancel-subagent-result`. The child looks the agent up in its own
|
|
39
39
|
* SubagentModule, so this is the only way to stop a subagent that lives
|
|
40
40
|
* in a fleet child rather than the conductor. */
|
|
41
|
-
| { type: 'cancel-subagent'; name: string; corrId?: string }
|
|
41
|
+
| { type: 'cancel-subagent'; name: string; corrId?: string }
|
|
42
|
+
/**
|
|
43
|
+
* Run one operator-panel operation in the child (see PANEL_OPS in
|
|
44
|
+
* src/web/panel-data.ts: mcpl / settings(-update|-reset|-cancel-transition)
|
|
45
|
+
* / pins / pin-add / pin-remove / health / context-makeup /
|
|
46
|
+
* context-coverage / context-curve / context-preview / debug-context).
|
|
47
|
+
* Response is a single `panel-response` with the same corrId. One generic
|
|
48
|
+
* verb rather than a verb per panel: both ends dispatch through the SAME
|
|
49
|
+
* shared handler (`runPanelOp`), so a new panel surface needs no protocol
|
|
50
|
+
* change to work across the fleet.
|
|
51
|
+
*/
|
|
52
|
+
| { type: 'panel-request'; op: string; params?: Record<string, unknown>; corrId?: string };
|
|
42
53
|
|
|
43
54
|
// ---------------------------------------------------------------------------
|
|
44
55
|
// Child → Parent: events
|
|
@@ -134,6 +145,23 @@ export interface CancelSubagentResultEvent {
|
|
|
134
145
|
ts?: number;
|
|
135
146
|
}
|
|
136
147
|
|
|
148
|
+
/** Response to a {type:'panel-request'} request. `data` is the same
|
|
149
|
+
* wire-shaped JSON the WebUI host serves locally for the given op;
|
|
150
|
+
* `ok:false` carries the error plus an HTTP-ish `status` so the parent's
|
|
151
|
+
* proxy routes can answer faithfully (404 unknown agent, 429 preview
|
|
152
|
+
* cooldown, 501 unsupported build). */
|
|
153
|
+
export interface PanelResponseEvent {
|
|
154
|
+
type: 'panel-response';
|
|
155
|
+
corrId?: string;
|
|
156
|
+
/** Echo of the requested op. */
|
|
157
|
+
op: string;
|
|
158
|
+
ok: boolean;
|
|
159
|
+
data?: unknown;
|
|
160
|
+
error?: string;
|
|
161
|
+
status?: number;
|
|
162
|
+
ts?: number;
|
|
163
|
+
}
|
|
164
|
+
|
|
137
165
|
/** Response to a {type:'request-workspace-file'} request. */
|
|
138
166
|
export interface WorkspaceFileSnapshotEvent {
|
|
139
167
|
type: 'workspace-file-snapshot';
|
|
@@ -163,6 +191,7 @@ export type WireEvent =
|
|
|
163
191
|
| WorkspaceTreeSnapshotEvent
|
|
164
192
|
| WorkspaceFileSnapshotEvent
|
|
165
193
|
| CancelSubagentResultEvent
|
|
194
|
+
| PanelResponseEvent
|
|
166
195
|
// Arbitrary framework TraceEvent passthrough. The child stamps every emitted
|
|
167
196
|
// event with `ts: Date.now()` in `emit()` (see headless.ts), so ts is always
|
|
168
197
|
// present on the wire even when the underlying TraceEvent doesn't declare it.
|