@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.
Files changed (67) hide show
  1. package/.env.example +12 -5
  2. package/.github/PULL_REQUEST_TEMPLATE.md +3 -2
  3. package/.github/workflows/changelog.yml +9 -4
  4. package/.github/workflows/ci.yml +5 -3
  5. package/.github/workflows/publish.yml +12 -6
  6. package/CHANGELOG.md +320 -0
  7. package/CONTRIBUTING.md +47 -19
  8. package/README.md +27 -0
  9. package/bun.lock +26 -32
  10. package/changelog.d/README.md +28 -0
  11. package/package.json +6 -6
  12. package/recipes/SETUP.md +11 -5
  13. package/recipes/TRIUMVIRATE-SETUP.md +68 -14
  14. package/recipes/knowledge-miner.json +0 -30
  15. package/recipes/mock-test.json +19 -0
  16. package/recipes/triumvirate.json +6 -1
  17. package/scripts/release-changelog.ts +210 -21
  18. package/src/cache-keepalive-log.ts +41 -0
  19. package/src/commands.ts +221 -32
  20. package/src/framework-agent-config.ts +3 -0
  21. package/src/framework-strategy.ts +42 -0
  22. package/src/gate-telemetry.ts +134 -0
  23. package/src/headless.ts +10 -0
  24. package/src/index.ts +194 -55
  25. package/src/mcpl-config.ts +99 -1
  26. package/src/modules/identity-module.ts +310 -2
  27. package/src/modules/instructions-module.ts +265 -0
  28. package/src/modules/mcpl-admin-module.ts +58 -11
  29. package/src/modules/subagent-module.ts +18 -0
  30. package/src/modules/web-ui-module.ts +32 -4
  31. package/src/recipe.ts +821 -25
  32. package/src/web/panel-data.ts +44 -1
  33. package/src/workspace-mounts.ts +73 -0
  34. package/test/audit-module-optins.test.ts +10 -3
  35. package/test/cache-keepalive-log.test.ts +83 -0
  36. package/test/commands-qa-family.test.ts +239 -0
  37. package/test/conversations-recipe.test.ts +142 -0
  38. package/test/count-tokens-model.test.ts +31 -0
  39. package/test/framework-fkm-composition.test.ts +35 -3
  40. package/test/framework-strategy-defaults.test.ts +60 -0
  41. package/test/gate-telemetry-adapter.test.ts +84 -0
  42. package/test/gate-telemetry.test.ts +124 -0
  43. package/test/identity-and-surfaces.test.ts +212 -1
  44. package/test/instructions-module.test.ts +258 -0
  45. package/test/mcpl-admin-module.test.ts +41 -0
  46. package/test/mcpl-agent-overlay.test.ts +51 -3
  47. package/test/mcpl-child-env.test.ts +64 -0
  48. package/test/nudge-command.test.ts +47 -0
  49. package/test/recipe-cache-keepalive.test.ts +59 -0
  50. package/test/recipe-compression-fallback.test.ts +19 -0
  51. package/test/recipe-hybrid-prose-routing.test.ts +12 -0
  52. package/test/recipe-instructions.test.ts +176 -0
  53. package/test/recipe-kv-unified.test.ts +87 -0
  54. package/test/recipe-mcp-source.test.ts +54 -0
  55. package/test/recipe-openai-compatible.test.ts +54 -0
  56. package/test/recipe-path-resolution.test.ts +19 -8
  57. package/test/recipe-provider.test.ts +14 -0
  58. package/test/recipe-save-unresolved.test.ts +244 -0
  59. package/test/recipe-source-only.test.ts +38 -0
  60. package/test/release-changelog.test.ts +202 -0
  61. package/test/subagent-prose-routing.test.ts +109 -0
  62. package/test/subconscious-recipe.test.ts +86 -0
  63. package/test/tool-wrapper-prose-guard-recipe.test.ts +37 -0
  64. package/test/web-ui-module.test.ts +41 -0
  65. package/test/workspace-mounts.test.ts +68 -0
  66. package/web/src/App.tsx +10 -0
  67. 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/' },
@@ -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
- <span>{props.health!.pendingRequests} queued</span>
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}