2 Commits

Author SHA1 Message Date
377e02c943 fix(memory): make the loop actually promote real recall into system
`promoteSnapshotSections` accepted only ONE carrier spelling: `content: []`.
The plugin never emits that shape. `lib/index.js:932` always attaches exactly
one `{ type: 'text', text: injected }` block whose text IS `sections[0].text`,
so `promotedSection`'s `if (blocks.length > 0) return null` matched on every
real recall and the promotion was a no-op in production. Recall stayed in
user/message history — the exact defect 6d9ad62 claimed to fix.

6d9ad62's "surface recall as a system section instead of history" was never
true: the promotion it relies on was blocked by that content-non-empty check.
The unit tests could not see it because their `snapshotMessage()` helper built
`content: []` — a shape no producer emits — and the old P4 asserted "real
content blocks are NOT promoted", which is the opposite of reality.

It was confirmed today, from `lib/index.js:932`'s constant one-text-block
injection, and is fixed here.

The rule is now "move the text, don't rewrite it": a snapshot carrier with
content is promoted only when the rendered blocks equal the sections' text
under one of the two canonical joins (`''` glued, `'\n\n'` spaced), and the
promoted section text is the rendered content, so relocation is assertable.
Non-text blocks, mismatched text, and the stock runtime-context snapshot (whose
content carries a preamble the sections do not) pass through untouched.

Tests: S1-S7 plus real multi-block S2c/S2d pin the acceptance surface; every
negative case carries a positive control in the same decision, so the old
"content non-empty => reject" rule cannot satisfy it. P4 is split into P4a/b
(content == sections promotes; content != sections does not) with P4c proving
the split is decided by text, not by presence of content.
`test/loop-system-e2e.test.js` mounts the real HologramAgentLoop on real dsh
services, runs one turn, and asserts on the request the adapter received: the
recall is in the system prompt (E1) and in no non-system message body (E2),
carried by exactly one message (E2b).
2026-11-04 18:24:58 +08:00
6d9ad62a8f feat(memory): surface recall as a system section instead of history
Per-turn recall currently enters through the `agent/pre-step` waterfall, and
that hook can only return `messages`. The recall block therefore lands in
conversation history as a user message: the model reads its own memory as if a
user had said it. `systemPrompt.context` cannot fix this — it runs before the
turn text is known, so it can only render input-independent text.

The loop's `preStep` is the first seam holding BOTH the assembled prompt and
this turn's claimed messages. `lib/loop.js` subclasses the harness AgentLoop and
wraps that seam on the agent instance: a pure snapshot carrier message is moved
out of `messages` and into `assembly.sections`, so it becomes part of the system
prompt for that turn and never enters history.

Deliberately decoupled from the memory plugin: nothing imports it or matches its
name. Promotion keys off the shape every snapshot-style injector already uses
(`source.sections`, the same shape `dsh-time-context` emits).

Constraints honoured, each of which was a measured failure elsewhere:
- The promoted section carries `interpolate: false`; recalled text is free text
  and may contain `{{...}}`, which the strict interpolator rejects.
- The subclass defines no constructor and no static `inject`. The parent already
  registers the turnBoundary + inbox projections, calls `setFactory(this)`, and
  declares `{{provider}}`/`{{model}}`; re-registering any of them throws at
  startup.
- Only the instance's `preStep` is patched, never the prototype, which other
  agents share.

Also wires the new peer: `scripts/link-dsh.sh` gains dsh-agent/dsh-agent-loop
(without them the package cannot resolve at test time), package.json declares
dsh-agent-loop as a peer and adds the new test file, and G2b's allowlist admits
dsh-agent-loop as a first-party harness package, with the reason recorded.

npm test: 247 pass, 0 fail.
2026-11-04 14:54:49 +08:00