@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.
Files changed (63) hide show
  1. package/CHANGELOG.md +203 -10
  2. package/HEADLESS-FLEET-PLAN.md +22 -0
  3. package/README.md +22 -11
  4. package/docs/AGENT-ONBOARDING.md +20 -1
  5. package/docs/debug-context-api.md +2 -2
  6. package/docs/retrieval-traces.md +173 -0
  7. package/docs/webui-deployment.md +2 -1
  8. package/package.json +3 -3
  9. package/scripts/audit-module-optins.ts +288 -0
  10. package/scripts/warmup-session.ts +17 -3
  11. package/src/codex-subscription-adapter.ts +13 -1
  12. package/src/framework-agent-config.ts +59 -4
  13. package/src/framework-strategy.ts +33 -3
  14. package/src/headless.ts +14 -0
  15. package/src/index.ts +95 -35
  16. package/src/logging-adapter.ts +13 -2
  17. package/src/mcpl-config.ts +8 -0
  18. package/src/modules/fleet-module.ts +60 -1
  19. package/src/modules/fleet-types.ts +30 -1
  20. package/src/modules/identity-module.ts +274 -0
  21. package/src/modules/mcpl-admin-module.ts +78 -5
  22. package/src/modules/observers-module.ts +12 -0
  23. package/src/modules/retrieval-module.ts +254 -52
  24. package/src/modules/retrieval-trace-page.ts +254 -0
  25. package/src/modules/retrieval-trace.ts +904 -0
  26. package/src/modules/settings-module.ts +28 -2
  27. package/src/modules/subscription-gc-module.ts +54 -1
  28. package/src/modules/tts-relay-module.ts +33 -18
  29. package/src/modules/web-ui-module.ts +445 -894
  30. package/src/recipe.ts +137 -12
  31. package/src/retrieval-config.ts +39 -0
  32. package/src/strategies/frontdesk-strategy.ts +34 -125
  33. package/src/tui.ts +325 -54
  34. package/src/web/panel-data.ts +1187 -0
  35. package/src/web/protocol.ts +75 -10
  36. package/test/audit-module-optins.test.ts +167 -0
  37. package/test/bedrock-prompt-caching.test.ts +170 -0
  38. package/test/fleet-panel-request.test.ts +90 -0
  39. package/test/framework-strategy-defaults.test.ts +110 -0
  40. package/test/frontdesk-strategy.test.ts +25 -37
  41. package/test/headless-panel-request.test.ts +201 -0
  42. package/test/identity-and-surfaces.test.ts +157 -0
  43. package/test/mcpl-admin-module.test.ts +23 -0
  44. package/test/mock-headless-child.ts +14 -0
  45. package/test/retrieval-auth-loopback.test.ts +49 -0
  46. package/test/retrieval-config.test.ts +74 -0
  47. package/test/retrieval-module.test.ts +821 -0
  48. package/test/subscription-gc-module.test.ts +152 -0
  49. package/test/tui-format.test.ts +106 -0
  50. package/test/web-ui-context-coverage.test.ts +1 -1
  51. package/test/web-ui-module.test.ts +189 -3
  52. package/test/web-ui-observers.test.ts +8 -5
  53. package/test/web-ui-protocol.test.ts +0 -0
  54. package/web/bun.lock +345 -0
  55. package/web/src/App.tsx +159 -44
  56. package/web/src/Context.tsx +35 -8
  57. package/web/src/ContextDocument.tsx +20 -5
  58. package/web/src/Files.tsx +2 -8
  59. package/web/src/Lessons.tsx +2 -38
  60. package/web/src/Mcpl.tsx +80 -14
  61. package/web/src/Pins.tsx +5 -0
  62. package/web/src/Settings.tsx +5 -0
  63. package/web/vite.config.ts +8 -2
@@ -33,6 +33,14 @@ import type {
33
33
  export interface ReasoningSettings {
34
34
  enabled: boolean;
35
35
  budgetTokens: number;
36
+ /**
37
+ * How thinking content comes back from the API: 'summarized' returns a
38
+ * readable reasoning summary in the `thinking` field; 'omitted' returns an
39
+ * empty `thinking` field with only the encrypted signature. Models 4.7+
40
+ * default to 'omitted' server-side — we default to 'summarized' to restore
41
+ * the pre-4.7 behavior (visible reasoning in stores, webui, estimators).
42
+ */
43
+ display: 'summarized' | 'omitted';
36
44
  }
37
45
 
38
46
  export interface SettingsState {
@@ -40,7 +48,7 @@ export interface SettingsState {
40
48
  }
41
49
 
42
50
  const DEFAULTS: SettingsState = {
43
- reasoning: { enabled: false, budgetTokens: 8192 },
51
+ reasoning: { enabled: false, budgetTokens: 8192, display: 'summarized' },
44
52
  };
45
53
 
46
54
  export class SettingsModule implements Module {
@@ -112,8 +120,16 @@ export class SettingsModule implements Module {
112
120
  type: 'number',
113
121
  description: 'Token budget for thinking blocks (min 1024).',
114
122
  },
123
+ reasoning_display: {
124
+ type: 'string',
125
+ enum: ['summarized', 'omitted'],
126
+ description:
127
+ "How your thinking is returned: 'summarized' (a readable summary of your reasoning " +
128
+ "is recorded alongside the signature) or 'omitted' (signature only, slightly faster " +
129
+ 'first token; your reasoning is not visible to anyone, including you on replay).',
130
+ },
115
131
  },
116
- keys: ['reasoning_enabled', 'reasoning_budget_tokens'],
132
+ keys: ['reasoning_enabled', 'reasoning_budget_tokens', 'reasoning_display'],
117
133
  get: () => this.reasoningSettingsView(),
118
134
  update: (_agentName, patch) => {
119
135
  const next = { ...this.state.reasoning };
@@ -130,6 +146,12 @@ export class SettingsModule implements Module {
130
146
  }
131
147
  next.budgetTokens = Math.max(1024, Math.round(budget));
132
148
  }
149
+ if (patch.reasoning_display !== undefined) {
150
+ if (patch.reasoning_display !== 'summarized' && patch.reasoning_display !== 'omitted') {
151
+ throw new Error("reasoning_display must be 'summarized' or 'omitted'");
152
+ }
153
+ next.display = patch.reasoning_display;
154
+ }
133
155
  this.state.reasoning = next;
134
156
  this.ctx?.setState(this.state);
135
157
  return this.reasoningSettingsView();
@@ -142,6 +164,9 @@ export class SettingsModule implements Module {
142
164
  if (all || keys?.includes('reasoning_budget_tokens')) {
143
165
  this.state.reasoning.budgetTokens = DEFAULTS.reasoning.budgetTokens;
144
166
  }
167
+ if (all || keys?.includes('reasoning_display')) {
168
+ this.state.reasoning.display = DEFAULTS.reasoning.display;
169
+ }
145
170
  this.ctx?.setState(this.state);
146
171
  return this.reasoningSettingsView();
147
172
  },
@@ -153,6 +178,7 @@ export class SettingsModule implements Module {
153
178
  return {
154
179
  reasoning_enabled: this.state.reasoning.enabled,
155
180
  reasoning_budget_tokens: this.state.reasoning.budgetTokens,
181
+ reasoning_display: this.state.reasoning.display,
156
182
  };
157
183
  }
158
184
 
@@ -336,11 +336,25 @@ export class SubscriptionGcModule implements Module {
336
336
  // Cross the threshold → close and clear the counter.
337
337
  delete this.state.counters[channelId];
338
338
  this.persistNow();
339
+ // A CONFIGURED numeric budget for this channel is an explicit idle
340
+ // lease: someone chose a close-at-N budget for this specific channel,
341
+ // so the registry may close even an explicitly-opened one. The state
342
+ // does not record WHO configured it (agent via agent_settings,
343
+ // operator, or imported before these semantics existed), so nothing
344
+ // downstream may claim "agent-set" — receipts say 'configured-budget',
345
+ // actor unknown. The global default is not consent of any kind; the
346
+ // registry refuses machine closes of explicit opens under it (#5).
347
+ const hasConfiguredLease = typeof this.state.overrides[channelId] === 'number';
339
348
  const result = await this.ctx
340
349
  ?.callTool({
341
350
  id: `gc-unsub-${this.callSeq++}`,
342
351
  name: 'channel_close',
343
- input: { channelId, serverId: this.serverId },
352
+ input: {
353
+ channelId,
354
+ serverId: this.serverId,
355
+ source: 'subscription-gc',
356
+ overrideExplicitOpen: hasConfiguredLease,
357
+ },
344
358
  })
345
359
  .catch((err: unknown) => ({
346
360
  success: false,
@@ -349,6 +363,34 @@ export class SubscriptionGcModule implements Module {
349
363
  }));
350
364
 
351
365
  if (result && result.success) {
366
+ // Operator-side receipt (privacy-minimal: ids and thresholds, no
367
+ // content) — a GC close changes durable listening state and must
368
+ // not look spontaneous from outside the transcript. Duck-typed
369
+ // against a framework that may not have ModuleContext.notifyOps yet
370
+ // (skipped there), and invoked THROUGH the context object: the real
371
+ // ModuleContextImpl.notifyOps reads `this`, so a detached
372
+ // `const f = ctx.notifyOps; f(...)` throws in production while
373
+ // passing against arrow-function mocks.
374
+ const opsCtx = this.ctx as unknown as {
375
+ notifyOps?: (kind: string, agent: string, message: string, data?: Record<string, unknown>) => void;
376
+ } | null;
377
+ opsCtx?.notifyOps?.(
378
+ 'subscription-gc-close',
379
+ this.ctx?.getAgents()[0]?.name ?? 'unknown',
380
+ `subscription-gc auto-closed channel ${channelId} (over ${limit} ambient chars ` +
381
+ `since last activation${hasConfiguredLease ? ', configured per-channel budget' : ', default budget'}). ` +
382
+ `Restore: channel_open ${channelId}, or agent_settings channel_idle_limits.`,
383
+ {
384
+ channelId,
385
+ limitChars: limit,
386
+ decisionSource: 'subscription-gc',
387
+ // 'configured-budget' deliberately does NOT claim an actor: the
388
+ // override state records no provenance (agent, operator, or
389
+ // imported are all possible).
390
+ lease: hasConfiguredLease ? 'configured-budget' : 'default',
391
+ restore: `channel_open ${channelId}`,
392
+ },
393
+ );
352
394
  return {
353
395
  addMessages: [
354
396
  {
@@ -367,6 +409,17 @@ export class SubscriptionGcModule implements Module {
367
409
  ],
368
410
  };
369
411
  }
412
+
413
+ // The registry refused because the channel was explicitly opened and
414
+ // we hold no lease: stand down quietly — the janitor doesn't argue
415
+ // with stated intent. The counter stays cleared, so the next refusal
416
+ // is at least a full budget away; if the channel's provenance later
417
+ // changes (reopened by policy), GC self-heals.
418
+ const refusal = (result as { data?: { refusal?: string } } | undefined)?.data?.refusal;
419
+ if (refusal === 'explicit-open') {
420
+ return {};
421
+ }
422
+
370
423
  // Close failed — keep the channel counted so we retry on the next
371
424
  // ambient message rather than silently giving up.
372
425
  this.state.counters[channelId] = next;
@@ -232,6 +232,13 @@ function rawChannelId(mcplId: string): string {
232
232
  return ix === -1 ? mcplId : mcplId.slice(ix + 1);
233
233
  }
234
234
 
235
+ /** Channel-bound inference traces carry `channelId` at runtime, but the
236
+ * framework's TraceEvent union doesn't declare it — read it through a cast. */
237
+ function traceChannelId(event: unknown): string | undefined {
238
+ const v = (event as { channelId?: unknown }).channelId;
239
+ return typeof v === 'string' ? v : undefined;
240
+ }
241
+
235
242
  const collapseWs = (s: string): string => s.replace(/\s+/g, ' ').trim();
236
243
 
237
244
  export class TtsRelayModule implements Module {
@@ -338,18 +345,41 @@ export class TtsRelayModule implements Module {
338
345
  }
339
346
 
340
347
  private onTraceEvent(event: TraceEvent): void {
348
+ // `mcpl:speech-routed` is a custom trace type emitted by the speech
349
+ // router, not part of the framework's TraceEvent union — the typed
350
+ // switch below could never narrow to it. Handle it up front via the
351
+ // standard custom-trace cast.
352
+ if ((event as { type: string }).type === 'mcpl:speech-routed') {
353
+ // Remember what landed where, so an interruption can find its edit
354
+ // target: (channelId, messageId, text) per posted segment.
355
+ const e = event as unknown as {
356
+ channelId: string; messageId?: string; text: string; timestamp: number;
357
+ };
358
+ this.routed.push({
359
+ rawChannelId: rawChannelId(e.channelId),
360
+ mcplChannelId: e.channelId,
361
+ messageId: e.messageId,
362
+ text: e.text,
363
+ at: e.timestamp,
364
+ });
365
+ if (this.routed.length > ROUTED_RING_MAX) {
366
+ this.routed.splice(0, this.routed.length - ROUTED_RING_MAX);
367
+ }
368
+ return;
369
+ }
370
+
341
371
  switch (event.type) {
342
372
  case 'inference:started': {
343
373
  // A context-budget restart re-emits inference:started mid-turn; the
344
374
  // existing activation (announced or not) simply carries on.
345
375
  const st = this.activation(event.agentName);
346
- this.adoptChannel(st, event.agentName, event.channelId);
376
+ this.adoptChannel(st, event.agentName, traceChannelId(event));
347
377
  break;
348
378
  }
349
379
 
350
380
  case 'inference:tokens': {
351
381
  const st = this.activation(event.agentName);
352
- this.adoptChannel(st, event.agentName, event.channelId);
382
+ this.adoptChannel(st, event.agentName, traceChannelId(event));
353
383
  if (!st.rawChannelId) break; // no locus — nothing to voice into
354
384
  st.blocks.set(event.blockIndex, (st.blocks.get(event.blockIndex) ?? '') + event.content);
355
385
  this.client?.emit('chunk', {
@@ -367,7 +397,7 @@ export class TtsRelayModule implements Module {
367
397
 
368
398
  case 'inference:content_block': {
369
399
  const st = this.activation(event.agentName);
370
- this.adoptChannel(st, event.agentName, event.channelId);
400
+ this.adoptChannel(st, event.agentName, traceChannelId(event));
371
401
  if (!st.rawChannelId) break;
372
402
  const base = {
373
403
  channelId: st.rawChannelId,
@@ -398,21 +428,6 @@ export class TtsRelayModule implements Module {
398
428
  this.endActivation(event.agentName, 'error');
399
429
  break;
400
430
 
401
- case 'mcpl:speech-routed': {
402
- // Remember what landed where, so an interruption can find its edit
403
- // target: (channelId, messageId, text) per posted segment.
404
- this.routed.push({
405
- rawChannelId: rawChannelId(event.channelId),
406
- mcplChannelId: event.channelId,
407
- messageId: event.messageId,
408
- text: event.text,
409
- at: event.timestamp,
410
- });
411
- if (this.routed.length > ROUTED_RING_MAX) {
412
- this.routed.splice(0, this.routed.length - ROUTED_RING_MAX);
413
- }
414
- break;
415
- }
416
431
 
417
432
  default:
418
433
  break;