@animalabs/connectome-host 0.7.4 → 0.8.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/.env.example +12 -5
- package/.github/PULL_REQUEST_TEMPLATE.md +3 -2
- package/.github/workflows/changelog.yml +9 -4
- package/.github/workflows/ci.yml +5 -3
- package/.github/workflows/publish.yml +12 -6
- package/CHANGELOG.md +320 -0
- package/CONTRIBUTING.md +47 -19
- package/README.md +27 -0
- package/bun.lock +26 -32
- package/changelog.d/README.md +28 -0
- package/package.json +6 -6
- package/recipes/SETUP.md +11 -5
- package/recipes/TRIUMVIRATE-SETUP.md +68 -14
- package/recipes/knowledge-miner.json +0 -30
- package/recipes/mock-test.json +19 -0
- package/recipes/triumvirate.json +6 -1
- package/scripts/release-changelog.ts +210 -21
- package/src/cache-keepalive-log.ts +41 -0
- package/src/commands.ts +221 -32
- package/src/framework-agent-config.ts +3 -0
- package/src/framework-strategy.ts +42 -0
- package/src/gate-telemetry.ts +134 -0
- package/src/headless.ts +10 -0
- package/src/index.ts +194 -55
- package/src/mcpl-config.ts +99 -1
- package/src/modules/identity-module.ts +310 -2
- package/src/modules/instructions-module.ts +265 -0
- package/src/modules/mcpl-admin-module.ts +58 -11
- package/src/modules/subagent-module.ts +18 -0
- package/src/modules/web-ui-module.ts +32 -4
- package/src/recipe.ts +821 -25
- package/src/web/panel-data.ts +44 -1
- package/src/workspace-mounts.ts +73 -0
- package/test/audit-module-optins.test.ts +10 -3
- package/test/cache-keepalive-log.test.ts +83 -0
- package/test/commands-qa-family.test.ts +239 -0
- package/test/conversations-recipe.test.ts +142 -0
- package/test/count-tokens-model.test.ts +31 -0
- package/test/framework-fkm-composition.test.ts +35 -3
- package/test/framework-strategy-defaults.test.ts +60 -0
- package/test/gate-telemetry-adapter.test.ts +84 -0
- package/test/gate-telemetry.test.ts +124 -0
- package/test/identity-and-surfaces.test.ts +212 -1
- package/test/instructions-module.test.ts +258 -0
- package/test/mcpl-admin-module.test.ts +41 -0
- package/test/mcpl-agent-overlay.test.ts +51 -3
- package/test/mcpl-child-env.test.ts +64 -0
- package/test/nudge-command.test.ts +47 -0
- package/test/recipe-cache-keepalive.test.ts +59 -0
- package/test/recipe-compression-fallback.test.ts +19 -0
- package/test/recipe-hybrid-prose-routing.test.ts +12 -0
- package/test/recipe-instructions.test.ts +176 -0
- package/test/recipe-kv-unified.test.ts +87 -0
- package/test/recipe-mcp-source.test.ts +54 -0
- package/test/recipe-openai-compatible.test.ts +54 -0
- package/test/recipe-path-resolution.test.ts +19 -8
- package/test/recipe-provider.test.ts +14 -0
- package/test/recipe-save-unresolved.test.ts +244 -0
- package/test/recipe-source-only.test.ts +38 -0
- package/test/release-changelog.test.ts +202 -0
- package/test/subagent-prose-routing.test.ts +109 -0
- package/test/subconscious-recipe.test.ts +86 -0
- package/test/tool-wrapper-prose-guard-recipe.test.ts +37 -0
- package/test/web-ui-module.test.ts +41 -0
- package/test/workspace-mounts.test.ts +68 -0
- package/web/src/App.tsx +10 -0
- package/web/src/Health.tsx +61 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { buildFrameworkAgentConfig } from '../src/framework-agent-config.js';
|
|
3
|
+
import { validateRecipe } from '../src/recipe.js';
|
|
4
|
+
|
|
5
|
+
const base = (value?: unknown) => ({
|
|
6
|
+
name: 'tool-wrapper-prose-guard',
|
|
7
|
+
agent: {
|
|
8
|
+
systemPrompt: 'sys',
|
|
9
|
+
...(value === undefined ? {} : { toolWrapperProseGuard: value }),
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe('toolWrapperProseGuard recipe wiring', () => {
|
|
14
|
+
test('is default-off by omission and composes true into AgentConfig', () => {
|
|
15
|
+
const absent = validateRecipe(base());
|
|
16
|
+
expect(absent.agent.toolWrapperProseGuard).toBeUndefined();
|
|
17
|
+
expect(buildFrameworkAgentConfig(absent, 'a', 'm', { kind: 's' } as never).toolWrapperProseGuard).toBeUndefined();
|
|
18
|
+
|
|
19
|
+
const enabled = validateRecipe(base(true));
|
|
20
|
+
expect(enabled.agent.toolWrapperProseGuard).toBe(true);
|
|
21
|
+
expect(buildFrameworkAgentConfig(enabled, 'a', 'm', { kind: 's' } as never).toolWrapperProseGuard).toBe(true);
|
|
22
|
+
|
|
23
|
+
const disabled = validateRecipe(base(false));
|
|
24
|
+
expect(disabled.agent.toolWrapperProseGuard).toBe(false);
|
|
25
|
+
expect(buildFrameworkAgentConfig(disabled, 'b', 'm', { kind: 's' } as never).toolWrapperProseGuard).toBe(false);
|
|
26
|
+
|
|
27
|
+
const reloaded = validateRecipe(JSON.parse(JSON.stringify(enabled)));
|
|
28
|
+
expect(buildFrameworkAgentConfig(reloaded, 'c', 'm', { kind: 's' } as never).toolWrapperProseGuard).toBe(true);
|
|
29
|
+
expect(buildFrameworkAgentConfig(absent, 'd', 'm', { kind: 's' } as never).toolWrapperProseGuard).toBeUndefined();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('rejects non-boolean values instead of silently disabling the guard', () => {
|
|
33
|
+
for (const value of ['true', 1, null, {}]) {
|
|
34
|
+
expect(() => validateRecipe(base(value))).toThrow(/toolWrapperProseGuard/);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -330,6 +330,47 @@ describe('WebUiModule HTTP', () => {
|
|
|
330
330
|
const body = res.status < 500 ? await res.text() : '';
|
|
331
331
|
expect(body).not.toContain('pwned');
|
|
332
332
|
});
|
|
333
|
+
|
|
334
|
+
// Honest-error regression set: unknown API routes and missing bundle
|
|
335
|
+
// assets used to fall through to the SPA shell with a 200, and wrong
|
|
336
|
+
// methods executed the GET handlers.
|
|
337
|
+
|
|
338
|
+
test('unknown /debug/* route returns a JSON 404, not the SPA shell', async () => {
|
|
339
|
+
for (const path of ['/debug/context/', '/debug/CONTEXT', '/debug/nonexistent']) {
|
|
340
|
+
const res = await fetch(`http://127.0.0.1:${handle.port}${path}`, {
|
|
341
|
+
headers: { authorization: basicAuthHeader(BASIC_USER, BASIC_PASS) },
|
|
342
|
+
});
|
|
343
|
+
expect(res.status).toBe(404);
|
|
344
|
+
expect(res.headers.get('content-type')).toContain('application/json');
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test('missing /assets/* returns 404, not HTML', async () => {
|
|
349
|
+
const res = await fetch(`http://127.0.0.1:${handle.port}/assets/nonexistent.js`, {
|
|
350
|
+
headers: { authorization: basicAuthHeader(BASIC_USER, BASIC_PASS) },
|
|
351
|
+
});
|
|
352
|
+
expect(res.status).toBe(404);
|
|
353
|
+
expect(res.headers.get('content-type') ?? '').not.toContain('text/html');
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test('non-GET methods get 405 with Allow header', async () => {
|
|
357
|
+
for (const method of ['POST', 'DELETE', 'PUT']) {
|
|
358
|
+
const res = await fetch(`http://127.0.0.1:${handle.port}/debug/context`, {
|
|
359
|
+
method,
|
|
360
|
+
headers: { authorization: basicAuthHeader(BASIC_USER, BASIC_PASS) },
|
|
361
|
+
});
|
|
362
|
+
expect(res.status).toBe(405);
|
|
363
|
+
expect(res.headers.get('allow')).toContain('GET');
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
test('SPA client-side routes still fall back to the shell', async () => {
|
|
368
|
+
const res = await fetch(`http://127.0.0.1:${handle.port}/totally/fake/route`, {
|
|
369
|
+
headers: { authorization: basicAuthHeader(BASIC_USER, BASIC_PASS) },
|
|
370
|
+
});
|
|
371
|
+
expect(res.status).toBe(200);
|
|
372
|
+
expect(await res.text()).toContain('<!doctype html>');
|
|
373
|
+
});
|
|
333
374
|
});
|
|
334
375
|
|
|
335
376
|
describe('WebUiModule WebSocket', () => {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { buildWorkspaceMounts } from '../src/workspace-mounts.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Contract tests for the shared mount builder. validateRecipe's instructions
|
|
7
|
+
* cross-check reasons over this exact output, so these pin the properties the
|
|
8
|
+
* validator depends on — most importantly that `_config` is NOT
|
|
9
|
+
* auto-materialized (agent edits stay Chronicle-side between branch-changing
|
|
10
|
+
* commands), which is why the validator rejects it as an instructions path.
|
|
11
|
+
*/
|
|
12
|
+
describe('buildWorkspaceMounts', () => {
|
|
13
|
+
test('workspace: false disables mounts entirely', () => {
|
|
14
|
+
expect(buildWorkspaceMounts(false, '/store')).toBeNull();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('implicit default (omitted or true): input ro + products rw, neither auto-materialized', () => {
|
|
18
|
+
for (const ws of [undefined, true as const]) {
|
|
19
|
+
const mounts = buildWorkspaceMounts(ws, '/store')!;
|
|
20
|
+
expect(mounts.map((m) => m.name)).toEqual(['input', 'products']);
|
|
21
|
+
const [input, products] = mounts;
|
|
22
|
+
expect(input.mode).toBe('read-only');
|
|
23
|
+
expect(products.mode).toBe('read-write');
|
|
24
|
+
expect(input.autoMaterialize).toBeUndefined();
|
|
25
|
+
expect(products.autoMaterialize).toBeUndefined();
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('explicit mounts pass through declared fields and default mode/watch', () => {
|
|
30
|
+
const mounts = buildWorkspaceMounts({
|
|
31
|
+
mounts: [
|
|
32
|
+
{ name: 'instructions', path: './instructions', autoMaterialize: true },
|
|
33
|
+
{ name: 'refs', path: './refs', mode: 'read-only', watch: 'always' },
|
|
34
|
+
],
|
|
35
|
+
}, '/store')!;
|
|
36
|
+
expect(mounts[0]).toMatchObject({
|
|
37
|
+
name: 'instructions',
|
|
38
|
+
path: resolve('./instructions'),
|
|
39
|
+
mode: 'read-write', // defaulted
|
|
40
|
+
watch: 'never', // defaulted (no chokidar by default)
|
|
41
|
+
autoMaterialize: true,
|
|
42
|
+
});
|
|
43
|
+
expect(mounts[1]).toMatchObject({ mode: 'read-only', watch: 'always' });
|
|
44
|
+
expect(mounts[1].autoMaterialize).toBeUndefined();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('_config mount (configMount: true) is read-write and NOT auto-materialized', () => {
|
|
48
|
+
// THE contract behind rejecting `_config/...` as an instructions path:
|
|
49
|
+
// the host materializes this mount only after branch-changing commands,
|
|
50
|
+
// never on ordinary agent writes. If this test starts failing because
|
|
51
|
+
// `_config` gained autoMaterialize, revisit the validator's rejection.
|
|
52
|
+
const mounts = buildWorkspaceMounts({ mounts: [], configMount: true }, '/store')!;
|
|
53
|
+
const config = mounts.find((m) => m.name === '_config')!;
|
|
54
|
+
expect(config).toBeDefined();
|
|
55
|
+
expect(config.mode).toBe('read-write');
|
|
56
|
+
expect(config.autoMaterialize).toBeUndefined();
|
|
57
|
+
expect(config.path).toBe(resolve('/store/config'));
|
|
58
|
+
expect(config.watch).toBe('always');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('configMount composes with the implicit default mounts only in object form', () => {
|
|
62
|
+
// Matches the host: `workspace: true` cannot request the config mount.
|
|
63
|
+
const objForm = buildWorkspaceMounts({ mounts: undefined as never, configMount: true }, '/s');
|
|
64
|
+
expect((objForm ?? []).some((m) => m.name === '_config')).toBe(true);
|
|
65
|
+
const boolForm = buildWorkspaceMounts(true, '/s')!;
|
|
66
|
+
expect(boolForm.some((m) => m.name === '_config')).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
});
|
package/web/src/App.tsx
CHANGED
|
@@ -1058,6 +1058,15 @@ export function App() {
|
|
|
1058
1058
|
if (!text) return;
|
|
1059
1059
|
setDraft('');
|
|
1060
1060
|
if (text.startsWith('/')) {
|
|
1061
|
+
// /clear is display-local (the TUI clears its scrollback the same
|
|
1062
|
+
// way): wipe this client's transcript view without touching the
|
|
1063
|
+
// Chronicle or other clients. Sending it to the host was a no-op
|
|
1064
|
+
// that APPENDED a "(cleared)" line — the opposite of clearing.
|
|
1065
|
+
if (text === '/clear' || text.startsWith('/clear ')) {
|
|
1066
|
+
setMessages(produce((arr) => { arr.length = 0; }));
|
|
1067
|
+
setStreamLines([]);
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1061
1070
|
wire.send({ type: 'command', command: text });
|
|
1062
1071
|
return;
|
|
1063
1072
|
}
|
|
@@ -2065,6 +2074,7 @@ const COMMANDS: CommandHint[] = [
|
|
|
2065
2074
|
{ name: '/restore', blurb: 'switch to checkpoint' },
|
|
2066
2075
|
{ name: '/undo', blurb: 'revert before last agent turn' },
|
|
2067
2076
|
{ name: '/redo', blurb: 're-apply last undone action' },
|
|
2077
|
+
{ name: '/nudge', blurb: 'run inference on current context, no new events' },
|
|
2068
2078
|
{ name: '/history', blurb: 'recent messages' },
|
|
2069
2079
|
{ name: '/lessons', blurb: 'list active lessons' },
|
|
2070
2080
|
{ name: '/export', blurb: 'export lessons to ./output/' },
|
package/web/src/Health.tsx
CHANGED
|
@@ -72,6 +72,36 @@ export interface HealthSnapshot {
|
|
|
72
72
|
} | null;
|
|
73
73
|
}>;
|
|
74
74
|
compressionQuarantine?: Record<string, { count?: number; keys?: string[] }>;
|
|
75
|
+
|
|
76
|
+
/** cm's single-authority debt reduction (getCompressionDebt) — the QUEUE
|
|
77
|
+
|
|
78
|
+
* of closed-but-uncompressed chunks. Distinct from contextComposition:
|
|
79
|
+
|
|
80
|
+
* composition says what the last compile RENDERED (summaries L1 = 0 there
|
|
81
|
+
|
|
82
|
+
* means "no L1 tokens in the window", often healthy consolidation);
|
|
83
|
+
|
|
84
|
+
* this block says what the memory organ still OWES. Absent per-agent
|
|
85
|
+
|
|
86
|
+
* entry = the stack predates the reduction — render that as
|
|
87
|
+
|
|
88
|
+
* "not reported", never as zero (misread twice on 2026-08-29). */
|
|
89
|
+
|
|
90
|
+
compressionDebt?: Record<string, {
|
|
91
|
+
|
|
92
|
+
state?: 'healthy' | 'degraded' | 'critical';
|
|
93
|
+
|
|
94
|
+
pendingChunks?: number;
|
|
95
|
+
|
|
96
|
+
oldestPendingAgeMs?: number | null;
|
|
97
|
+
|
|
98
|
+
mergeQueueDepth?: number;
|
|
99
|
+
|
|
100
|
+
mergeQuarantineCount?: number;
|
|
101
|
+
|
|
102
|
+
compressionQuarantineCount?: number;
|
|
103
|
+
|
|
104
|
+
}>;
|
|
75
105
|
runtimeSettings?: Record<string, {
|
|
76
106
|
contextBudgetTokens?: number;
|
|
77
107
|
tailTokens?: number;
|
|
@@ -301,6 +331,7 @@ export function HealthPanel(props: {
|
|
|
301
331
|
}) {
|
|
302
332
|
const agents = () => props.health?.agents ?? [];
|
|
303
333
|
const quarantine = (name: string) => props.health?.compressionQuarantine?.[name];
|
|
334
|
+
const debt = (name: string) => props.health?.compressionDebt?.[name];
|
|
304
335
|
const settings = (name: string) => props.health?.runtimeSettings?.[name];
|
|
305
336
|
const composition = (name: string) => props.health?.contextComposition?.[name];
|
|
306
337
|
|
|
@@ -343,7 +374,10 @@ export function HealthPanel(props: {
|
|
|
343
374
|
<span>up {fmtUptime(props.health!.uptimeSec!)}</span>
|
|
344
375
|
</Show>
|
|
345
376
|
<Show when={props.health!.pendingRequests !== undefined}>
|
|
346
|
-
|
|
377
|
+
{/* the INFERENCE queue (requests waiting to run) — not compression
|
|
378
|
+
debt; that lives per-agent below. A bare "queued" invited the
|
|
379
|
+
misread, hence the explicit label. */}
|
|
380
|
+
<span>{props.health!.pendingRequests} inference queued</span>
|
|
347
381
|
</Show>
|
|
348
382
|
<Show when={props.health!.activeStreams !== undefined}>
|
|
349
383
|
<span>{props.health!.activeStreams!.length} streaming</span>
|
|
@@ -388,6 +422,32 @@ export function HealthPanel(props: {
|
|
|
388
422
|
</div>
|
|
389
423
|
</Show>
|
|
390
424
|
|
|
425
|
+
{/* Compression debt: the organ's QUEUE, not the render composition.
|
|
426
|
+
An old stack that reports nothing must say so — absence
|
|
427
|
+
rendered as zero invites misreads. */}
|
|
428
|
+
<Show when={debt(a.name)} fallback={
|
|
429
|
+
<div class="text-neutral-600">compression debt: not reported by this stack</div>
|
|
430
|
+
}>
|
|
431
|
+
<div class={
|
|
432
|
+
debt(a.name)!.state === 'critical' ? 'text-rose-300'
|
|
433
|
+
: debt(a.name)!.state === 'degraded' ? 'text-amber-300/90'
|
|
434
|
+
: 'text-neutral-500'
|
|
435
|
+
}>
|
|
436
|
+
compression debt: {debt(a.name)!.state ?? '?'}
|
|
437
|
+
{' · '}{debt(a.name)!.pendingChunks ?? 0} chunk(s) pending
|
|
438
|
+
<Show when={(debt(a.name)!.pendingChunks ?? 0) > 0}>
|
|
439
|
+
<span>
|
|
440
|
+
{debt(a.name)!.oldestPendingAgeMs != null
|
|
441
|
+
? ` · oldest ${Math.round(debt(a.name)!.oldestPendingAgeMs! / 60000)}m`
|
|
442
|
+
: ' · age unknown'}
|
|
443
|
+
</span>
|
|
444
|
+
</Show>
|
|
445
|
+
<Show when={(debt(a.name)!.mergeQueueDepth ?? 0) > 0}>
|
|
446
|
+
<span> · merge queue {debt(a.name)!.mergeQueueDepth}</span>
|
|
447
|
+
</Show>
|
|
448
|
+
</div>
|
|
449
|
+
</Show>
|
|
450
|
+
|
|
391
451
|
<Show when={a.refusalStats && (a.refusalStats.total ?? 0) > 0}>
|
|
392
452
|
<div class="text-amber-300/90">
|
|
393
453
|
refusals: {a.refusalStats!.total}
|