@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 +18 -0
- package/package.json +2 -2
- package/src/framework-strategy.ts +2 -0
- package/src/modules/web-ui-module.ts +46 -1
- package/src/recipe.ts +7 -0
- package/web/src/DryContext.tsx +23 -42
- package/web/src/Settings.tsx +7 -4
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
|
+
"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.
|
|
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",
|
|
@@ -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 {
|
package/web/src/DryContext.tsx
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* actual layout that budget would produce — not an approximation.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import {
|
|
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
|
-
|
|
66
|
-
|
|
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.
|
|
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.
|
|
140
|
-
<
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
</
|
|
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
|
-
{
|
|
153
|
-
? r.body
|
|
154
|
-
: r.body.slice(0, 600) + ' …'}
|
|
135
|
+
{r.body}{r.truncated ? ' …' : ''}
|
|
155
136
|
</div>
|
|
156
137
|
</div>
|
|
157
138
|
)}
|
package/web/src/Settings.tsx
CHANGED
|
@@ -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
|
|
410
|
-
agent
|
|
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}>
|