@animalabs/connectome-host 0.5.3 → 0.5.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 CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.4 — 2026-07-26
4
+
5
+ ### Fixed
6
+
7
+ - **`dry run + show context` was a 110-second agent stall.** Measured 110,348ms
8
+ against ~8s for the numbers-only dry run. `select()` builds the rendered
9
+ entries either way, so the extra ~102s was pure serialization of 353 full
10
+ entries — content the pane never showed, since it truncated every body past
11
+ 600 chars. The server now projects to `{i, who, chars, media, truncated,
12
+ text}` with text capped at 1,200 chars; content blocks never leave the
13
+ process and media is counted rather than inlined, which also removes the
14
+ blob-resolution heap exposure `/curve` warns about.
15
+ - The cost disclosure was understated by ~20× and in the dangerous direction
16
+ ("seconds… briefly pauses the agent"). It now states that the compile runs on
17
+ the agent's thread and the agent does nothing else meanwhile, quotes the
18
+ measured cost, and notes runs are serialized so a second click is refused
19
+ rather than queueing another pause.
20
+
3
21
  ## Unreleased
4
22
 
5
23
  ## 0.5.3 — 2026-07-26
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@animalabs/connectome-host",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "description": "General-purpose agent TUI host with recipe-based configuration",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -14,7 +14,7 @@
14
14
  "postinstall": "test -d web && npm --prefix web install && npm --prefix web run build || true"
15
15
  },
16
16
  "dependencies": {
17
- "@animalabs/agent-framework": "^0.7.0",
17
+ "@animalabs/agent-framework": "^0.7.2",
18
18
  "@animalabs/chronicle": "^0.2.0",
19
19
  "@animalabs/context-manager": "^0.6.0",
20
20
  "@animalabs/membrane": "^0.5.75",
@@ -34,6 +34,8 @@ const PASSTHROUGH_KEYS: ReadonlyArray<keyof RecipeStrategy> = [
34
34
  'summarySystemPrompt',
35
35
  'summaryUserPrompt',
36
36
  'summaryContextLabel',
37
+ 'witnessedBeforeSequence',
38
+ 'witnessedInstruction',
37
39
  ];
38
40
 
39
41
  export function buildFrameworkStrategy(
@@ -1212,6 +1212,51 @@ export class WebUiModule implements Module {
1212
1212
  private previewLastAt = 0;
1213
1213
  private static readonly PREVIEW_COOLDOWN_MS = 3_000;
1214
1214
 
1215
+ /**
1216
+ * Replace the dry run's full rendered entries with a compact display
1217
+ * projection.
1218
+ *
1219
+ * Shipping the entries verbatim cost ~110s of BLOCKED AGENT on Mythos (353
1220
+ * entries, megabytes of content plus any inlined media) against ~8s for the
1221
+ * numbers-only path — and select() builds those entries either way, so the
1222
+ * extra ~100s was pure serialization of data the UI never shows in full: the
1223
+ * pane truncates every body past 600 chars anyway.
1224
+ *
1225
+ * So: keep identity, size and a bounded text preview; drop content blocks and
1226
+ * never inline media. Also removes the blob-resolution heap risk the /curve
1227
+ * handler warns about.
1228
+ */
1229
+ private projectDryEntries(result: unknown): unknown {
1230
+ const r = result as { entries?: unknown[] } & Record<string, unknown>;
1231
+ if (!Array.isArray(r.entries)) return result;
1232
+ const MAX_TEXT = 1_200;
1233
+ const projected = r.entries.map((e, i) => {
1234
+ const o = (e ?? {}) as { participant?: string; role?: string; content?: unknown };
1235
+ let text = '';
1236
+ let media = 0;
1237
+ const blocks = Array.isArray(o.content) ? o.content : [];
1238
+ for (const b of blocks) {
1239
+ if (!b || typeof b !== 'object') { text += String(b ?? ''); continue; }
1240
+ const t = (b as { type?: string }).type;
1241
+ if (t === 'text') text += (b as { text?: string }).text ?? '';
1242
+ else if (t === 'image') { media++; text += '[image]'; }
1243
+ else if (t === 'thinking' || t === 'redacted_thinking') text += '[thinking]';
1244
+ else if (t === 'tool_use') text += `[tool_use ${(b as { name?: string }).name ?? ''}]`;
1245
+ else if (t === 'tool_result') text += '[tool_result]';
1246
+ }
1247
+ if (typeof o.content === 'string') text = o.content;
1248
+ return {
1249
+ i,
1250
+ who: o.participant ?? o.role ?? '?',
1251
+ chars: text.length,
1252
+ media,
1253
+ truncated: text.length > MAX_TEXT,
1254
+ text: text.length > MAX_TEXT ? text.slice(0, MAX_TEXT) : text,
1255
+ };
1256
+ });
1257
+ return { ...r, entries: projected };
1258
+ }
1259
+
1215
1260
  private handleContextPreview(url: URL): Response {
1216
1261
  const app = sharedServer?.app;
1217
1262
  if (!app) return Response.json({ error: 'app not bound yet' }, { status: 503 });
@@ -1326,7 +1371,7 @@ export class WebUiModule implements Module {
1326
1371
  },
1327
1372
  /** How long the agent was blocked, so the operator sees the real cost. */
1328
1373
  elapsedMs: Date.now() - startedAt,
1329
- preview: result,
1374
+ preview: wantRender ? this.projectDryEntries(result) : result,
1330
1375
  });
1331
1376
  } catch (err) {
1332
1377
  return Response.json(
package/src/recipe.ts CHANGED
@@ -85,6 +85,13 @@ export interface RecipeStrategy {
85
85
  summarySystemPrompt?: string;
86
86
  summaryUserPrompt?: string;
87
87
  summaryContextLabel?: string;
88
+ /** As-of perspective pin: chunks wholly below this message sequence are
89
+ * compressed as inherited/witnessed record, not first-person memory.
90
+ * For residents continued from a shared log they joined partway through. */
91
+ witnessedBeforeSequence?: number;
92
+ /** Override wording for the witnessed-record instruction ({targetTokens}
93
+ * substituted). */
94
+ witnessedInstruction?: string;
88
95
  }
89
96
 
90
97
  export interface RecipeAgent {
@@ -12,7 +12,7 @@
12
12
  * actual layout that budget would produce — not an approximation.
13
13
  */
14
14
 
15
- import { createSignal, For, Show } from 'solid-js';
15
+ import { For, Show } from 'solid-js';
16
16
 
17
17
  interface Seg { messages: number; tokens: number }
18
18
  interface DryStats {
@@ -29,47 +29,30 @@ export interface DryContextData {
29
29
 
30
30
  const fmt = (n: number) => n.toLocaleString();
31
31
 
32
- function textOf(content: unknown): string {
33
- if (Array.isArray(content)) {
34
- return content
35
- .map((b) => {
36
- if (!b || typeof b !== 'object') return String(b ?? '');
37
- const t = (b as { type?: string }).type;
38
- if (t === 'text') return (b as { text?: string }).text ?? '';
39
- if (t === 'image') return '[image]';
40
- if (t === 'thinking' || t === 'redacted_thinking') return '[thinking]';
41
- if (t === 'tool_use') return `[tool_use ${(b as { name?: string }).name ?? ''}]`;
42
- if (t === 'tool_result') return '[tool_result]';
43
- return '';
44
- })
45
- .join('');
46
- }
47
- return String(content ?? '');
48
- }
49
-
50
32
  /** Recall pairs render as a CM prompt followed by the summary text. */
51
33
  const RECALL_MARKERS = ['what do you remember', 'recall memory', '[cm]'];
52
34
  const isRecallPrompt = (s: string) =>
53
35
  s.length < 400 && RECALL_MARKERS.some((m) => s.toLowerCase().includes(m));
54
36
 
55
37
  export function DryContext(props: { data: DryContextData | null; onClose(): void }) {
56
- const [expanded, setExpanded] = createSignal<Set<number>>(new Set());
57
- const toggle = (i: number) => {
58
- const next = new Set(expanded());
59
- next.has(i) ? next.delete(i) : next.add(i);
60
- setExpanded(next);
61
- };
62
-
63
38
  const rows = () =>
64
39
  (props.data?.entries ?? []).map((e, i) => {
65
- const o = (e ?? {}) as { participant?: string; role?: string; content?: unknown };
66
- const body = textOf(o.content);
40
+ // Server-side projection (see projectDryEntries): identity, size and a
41
+ // bounded text preview. Full content blocks are deliberately NOT sent —
42
+ // shipping them cost ~110s of blocked agent.
43
+ const o = (e ?? {}) as {
44
+ i?: number; who?: string; text?: string; chars?: number;
45
+ media?: number; truncated?: boolean;
46
+ };
47
+ const body = o.text ?? '';
67
48
  return {
68
- i,
69
- who: o.participant ?? o.role ?? '?',
49
+ i: o.i ?? i,
50
+ who: o.who ?? '?',
70
51
  body,
71
52
  recall: isRecallPrompt(body),
72
- chars: body.length,
53
+ chars: o.chars ?? body.length,
54
+ media: o.media ?? 0,
55
+ truncated: o.truncated === true,
73
56
  };
74
57
  });
75
58
 
@@ -136,22 +119,20 @@ export function DryContext(props: { data: DryContextData | null; onClose(): void
136
119
  {r.who}
137
120
  </span>
138
121
  <span class="text-[10px] text-neutral-600">{fmt(r.chars)} chars</span>
139
- <Show when={r.chars > 600}>
140
- <button
141
- type="button"
142
- class="ml-auto text-[10px] font-mono text-neutral-500 hover:text-neutral-300"
143
- onClick={() => toggle(r.i)}
144
- >
145
- {expanded().has(r.i) ? 'collapse' : 'expand'}
146
- </button>
122
+ <Show when={r.media > 0}>
123
+ <span class="text-[10px] text-neutral-600">{r.media} media</span>
124
+ </Show>
125
+ <Show when={r.truncated}>
126
+ <span class="ml-auto text-[10px] font-mono text-neutral-600"
127
+ title="Bodies are truncated server-side; the full text is not sent.">
128
+ truncated
129
+ </span>
147
130
  </Show>
148
131
  </div>
149
132
  <div
150
133
  class="text-[12px] text-neutral-300 whitespace-pre-wrap break-words leading-relaxed"
151
134
  >
152
- {expanded().has(r.i) || r.chars <= 600
153
- ? r.body
154
- : r.body.slice(0, 600) + ' …'}
135
+ {r.body}{r.truncated ? ' …' : ''}
155
136
  </div>
156
137
  </div>
157
138
  )}
@@ -384,7 +384,7 @@ export function SettingsPanel(props: {
384
384
  class="px-2 py-0.5 text-[10px] rounded font-mono bg-neutral-800 hover:bg-neutral-700
385
385
  text-neutral-200 disabled:opacity-30"
386
386
  onClick={() => void runDryRun(false)}
387
- title="Compile at these settings without applying them. Does not commit anything."
387
+ title="Compile at these settings without applying them. Does not commit anything. Measured ~8s on a large store."
388
388
  >
389
389
  dry run
390
390
  </button>
@@ -394,7 +394,7 @@ export function SettingsPanel(props: {
394
394
  class="px-2 py-0.5 text-[10px] rounded font-mono bg-cyan-900/40 hover:bg-cyan-900/60
395
395
  text-cyan-200 disabled:opacity-30"
396
396
  onClick={() => void runDryRun(true)}
397
- title="Dry run and show the resulting context in the main pane."
397
+ title="Dry run, and show the resulting context in the main pane. Costs more than a plain dry run — bodies are truncated server-side to keep it close."
398
398
  >
399
399
  dry run + show context
400
400
  </button>
@@ -406,8 +406,11 @@ export function SettingsPanel(props: {
406
406
  </Show>
407
407
  </div>
408
408
  <div class="text-[10px] text-neutral-600 mb-1.5 leading-relaxed">
409
- A dry run is a full compile — seconds on a large store — and it briefly pauses the
410
- agent. It commits nothing: no fold resolutions, no compression queued.
409
+ A dry run is a <b class="text-neutral-500">full compile</b> and it runs on the agent's
410
+ thread — while it runs the agent does nothing else (no heartbeat, no Discord, no MCPL).
411
+ Measured ~8s on a large store; it commits nothing (no fold resolutions, no compression
412
+ queued). Runs are serialized with a short cooldown, so a second click is refused rather
413
+ than queueing another pause.
411
414
  </div>
412
415
 
413
416
  <Show when={!props.state!.previewAvailable}>