Skip to content

[pull] main from danny-avila:main - #153

Merged
pull[bot] merged 7 commits into
innFactory:mainfrom
danny-avila:main
Jul 30, 2026
Merged

[pull] main from danny-avila:main#153
pull[bot] merged 7 commits into
innFactory:mainfrom
danny-avila:main

Conversation

@pull

@pull pull Bot commented Jul 30, 2026

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

* 🧭 fix: Anchor Summary Coverage to a Source Message ID

A persisted summary's reach was inferred from where its content block sits
in the payload: everything before that position was dropped. But the block
is emitted on the assistant message that *follows* compaction, while the
recency tail retained by `retainRecent` sits earlier. Positional trimming
could not tell the retained tail apart from covered history, so on the next
run the tail was discarded — messages the summary never covered, and that no
longer appeared anywhere in the prompt.

Summaries now declare their own extent. `createSummarizeNode` records the
last refined message that carries a source ID as `coverage.throughMessageId`,
and `formatAgentMessages` resolves that ID against the payload, trimming
through the covered message and leaving everything after it verbatim.

Blocks without resolvable coverage — written before this field existed, or
covering a message no longer in the payload — keep the positional reading
unchanged, so existing compacted conversations are unaffected.

* 🧭 fix: Skip Coverage IDs That Straddle the Recency Boundary

A steer expands one source message into pre-steer, steer, and post-steer
messages that all carry its ID, and `splitAtRecencyBoundary` splits on any
human-type message — including the steer. The newest refined message could
therefore carry an ID whose remaining messages are in the retained tail, and
declaring it would make the next run drop the half this run kept.

Coverage now skips IDs still present in the tail and declares the newest
fully covered one instead, falling back to positional semantics when every
refined ID straddles the boundary.

* 🧭 fix: Anchor Coverage to the First Retained Message

Addresses the codex review on 2b6012c.

Anchoring coverage to the last *covered* message left two states with no
correct value. A steer expands one source message into pre-steer, steer, and
post-steer entries sharing its ID and the recency split lands between them,
so the newest covered ID could be half-retained; when every refined ID
straddled, coverage was omitted and the reader read that as legacy, dropping
the tail it was meant to protect. Separately, a reducer-stamped synthetic at
the end of the head could be persisted as an ID matching no payload entry.

Anchoring to the first *retained* message removes all three: a straddling
message is the anchor and survives whole, "nothing fully covered" becomes an
ordinary anchor rather than an absent one, and the anchor is read from the
retained tail instead of the synthetic-prone head. The boundary is now
exclusive, and the straddle-skipping set is gone.

Coverage mode also no longer skips token adjustment for the entry holding the
block. `formatAssistantMessage` filters that summary part out while its text
is accounted separately as `summary.tokenCount`, so charging the entry for
both inflated the prompt estimate and pruned early. Summary text length is
captured during the existing scan, since it nests under `content[]` where
`contentPartCharLength` does not reach.

* 🧭 fix: Measure Tool Calls and Skip Injected Context

Addresses the second codex review, on 1854731.

`contentPartCharLength` reads only top-level fields, so a tool call — whose
args and output nest under `tool_call` — measured as zero characters. An
entry holding a summary plus a tool call therefore scaled to the summary's
share alone and collapsed to 1 token, while `formatAssistantMessage` still
emitted the call and its ToolMessage: a 3000-token entry reported 1, hiding
the payload from the pruner. Tool-call content is now measured, and the
discount is skipped outright when retained parts retain no measurable length,
so an unrecognized shape over-counts rather than vanishing.

Coverage anchors also skip injected context. `convertInjectedMessages` builds
hook and skill-body entries as marked HumanMessages that the reducer stamps
with a UUID no payload entry carries; anchoring there looked resolvable at
write time and silently degraded to positional trimming on read.

* 🧭 fix: Narrow the Injected-Context Check to Spare Steers

Addresses the third codex review, on 51376bb.

The previous guard treated every non-null `additional_kwargs.source` as
in-run context. Steers carry `source: 'steer'` but are replayed by
`formatAgentMessages` from a payload entry and stamped with its ID, so they
are valid anchors. When compaction lands after a retained steer but before
any post-steer message exists, the steer is the only retained entry — the
guard skipped it, left coverage undefined, and the next run fell back to
positional trimming and dropped the retained steer this coverage exists to
protect.

The check is now `isMeta === true || source === 'hook'`, the two unambiguous
in-run markers. Skill bodies already carry `isMeta` and no source ID.
Skipping too little only costs an unresolvable anchor and a positional
fallback; skipping too much loses history, so the check errs narrow.

The straddle fixtures also now carry the real steer marker. They passed
against the broken predicate because their post-steer message repeated the
same ID, which masked the skip — fixture drift from the shape production
actually produces.

* 📝 docs: Record the Unstamped-Payload-Entry Limitation

Addresses the fourth codex review, on 2e26c1d — documentation only.

A payload entry that omits messageId is never stamped, so the reducer's UUID
is recorded as the anchor and cannot resolve on the next run. There is no
write-time fix: such an entry has no stable ID in the next payload either, so
no anchor can name it. The reader's positional fallback is what main already
does, so the anchor degrades to current behavior rather than misleading.

* 🧭 fix: Require Every Retained Part to Be Measurable

Addresses the fifth codex review, on fb2ed1d.

The measurability backstop was an aggregate check: it allowed the discount
whenever the retained characters totalled more than zero. An entry retaining
both a short caption and an image therefore passed it — the caption
contributes characters, the image contributes none but still costs its image
token estimate — so the scale charged the entry for the caption alone. A 1100
token entry reported 22.

The check is now per-part: any retained part the char heuristic cannot see
skips the discount entirely, so an unrecognized shape over-counts and prunes
early rather than sending an over-context request.

The round-two test covered an image alone, where the aggregate check already
held, so it never exercised the mixed case.

* 🧭 fix: Separate Zero-Cost Parts From Unmeasurable Ones

Addresses the sixth codex review, on c8127e4.

Two refinements of tradeoffs taken in earlier rounds, both of which the
reviewer showed were tighter than they needed to be.

The measurability check conflated a part that costs nothing with a part the
char heuristic cannot see. An empty text block is filtered by
`formatAssistantMessage` and contributes no prompt tokens, so its zero length
is accurate — but it disabled the discount and restored the double count.
Parts dropped by formatting are now excluded from the requirement, so only
genuinely unreadable shapes suppress the adjustment.

The injected-context check was a denylist of one label, which missed hook
output carrying `source: 'skill'` or `'system'` without `isMeta` —
`InjectedMessage` leaves the flag optional for every source. Inverted to an
allowlist: `steer` is the only label that is source-backed, because a steer is
replayed from a payload entry and stamped with its ID. Everything else marked
with a source is created in-run.

* ♻️ refactor: Discount Summary Tokens by Subtraction, Not Char Ratio

Addresses the seventh codex review, on 6ac48d9 — and retires the approach
that produced four of its findings.

Coverage mode was scaling the summary-bearing entry's token count by the share
of characters that survive formatting. Characters are the wrong proxy for
token cost, so each round found another shape the heuristic could not see:
tool-call payloads (round two), media (round five), a measurable part beside
an unmeasurable one (round five), zero-cost versus unreadable parts (round
six), and now media nested inside a tool output. Enumerating shapes cannot
converge.

The summary block already records its own token count, so the entry is now
discounted by subtracting it. No measurement of the retained content is
involved, which removes the whole class: media, tool calls, and nesting are
irrelevant to a subtraction. The guard is a value comparison — skip when the
summary claims at least the entry's tokens — rather than a shape predicate.

Also drops the injected-context filter. Provenance is not recorded in
`additional_kwargs`: an in-run injected entry and a replayed payload steer can
carry identical markers, and only the reader holds the payload needed to
decide. Filtering on markers cost a real bug in round three (skipping
`source: 'steer'` dropped retained steers) and bought nothing, because in-run
context is always appended — `[...toolMessages, ...injected]` — so a tail
beginning with injected context has no payload-backed message later to reach.
An unresolvable anchor falls back to positional trimming, which is what main
does for every summary today.

`contentPartCharLength` keeps its nested tool-call measurement: that is an
independent accuracy fix for the positional path, which still scales by chars.

* 🧭 fix: Skip Synthetic Context When Anchoring Coverage

Addresses the eighth codex review, on 20b392d. The reviewer produced the
counter-example I asked for, and my reasoning was wrong.

I removed this filter in the previous commit on the grounds that in-run context
is always appended, so a tail beginning with synthetic context could never have
a resolvable message behind it. That holds for ToolNode and StandardGraph
injections but not for formatAgentMessages, which reconstructs skill bodies
inside its own payload loop and keeps processing payload entries afterwards. An
unstamped skill body — a reducer UUID by the time compaction sees it — is
therefore followed by stamped messages, and anchoring on the UUID resolves to
nothing on the next run and drops the retained tail.

The filter is restored. `isMeta` catches the reconstructed skill body; `steer`
stays exempt because a replayed steer is stamped from its payload entry and
skipping it dropped retained steers when this check once rejected every marked
source.

The residual ambiguity is documented rather than papered over:
`InjectedMessage` also permits `source: 'steer'`, and an in-run injected steer
is unstamped, so the exemption accepts a UUID for it. Nothing in
`additional_kwargs` separates the two. Both directions bottom out in the
reader's positional fallback, so the exemption favours replayed steers as the
common case.

* 🧭 fix: Record Injected Provenance at the Source

Addresses the ninth codex review, on 649626d, by taking its first suggestion:
stamp injected messages rather than infer their origin downstream.

`InjectedMessage` leaves both `isMeta` and `source` optional, so a bare
injected turn carried only its role and the coverage anchor read its
reducer UUID as source-backed. `convertInjectedMessages` now records
`injected: true` on everything it builds, which is the one place that knows.

Kept separate from `isMeta` deliberately: that flag has UI and prompt-cache
meaning of its own (`isMetaOrSkillMessage` in messages/cache.ts), so setting
it unconditionally would change cache placement for callers that had left it
unset. `injected` has no other consumers.

This also resolves the injected-steer case documented as unfixable in the
previous commit. Those go through the same funnel, so they now carry the
marker and are skipped, while a steer replayed from a payload entry — stamped
with its ID, never through this funnel — still anchors.

The remaining `isMeta`/`source` checks stay for the constructors that build
synthetic entries directly rather than through `convertInjectedMessages`: hook
context in ToolNode and StandardGraph, handoff cues, and reconstructed skill
bodies.

* 🧮 fix: Discount Summaries in the Reader's Own Token Units

Addresses the tenth codex review, on 688e7f2.

The subtraction introduced two commits ago mixed token spaces. `tokenCount` is
the injection budget: provider output-token space whenever usage was reported,
plus the wrapper added later at injection time, and for reasoning models the
provider figure can include thinking blocks that `extractResponseText` drops.
`indexTokenCountMap` is kept in the consumer's own tokenizer. Subtracting one
from the other over-subtracted — a 1000-token entry fell to 100 in test — which
undercounts retained content and can send an over-context request.

The summary block now records `rawTokenCount` alongside it: the summary text
measured with the consumer's `tokenCounter`, no wrapper, which is the space
`indexTokenCountMap` uses. The reader subtracts only that. Blocks without the
field get no discount, so nothing written before this change is touched.

Also stamps multi-agent routing prompts. The handoff and fan-in `HumanMessage`s
in MultiAgentGraph carried no provenance at all, so a retained routing prompt
was read as source-backed and its UUID persisted as an anchor that cannot
resolve. They are built in-run and never persisted as standalone payload
entries, so they are marked synthetic at construction — the same fix as the
injected-message funnel, applied to the constructors that bypass it.

* 🧹 fix: Stop Discounting the Summary-Bearing Entry

Addresses the eleventh codex review, on 5dcc4d6, by removing the adjustment
rather than attempting it a fourth way.

The reviewer showed that a token count recorded at write time is in the writing
run's units: `Run.create` derives its counter from the model in play, and a
consumer may supply its own, so a conversation continued on a different model
subtracts across encodings. The reader cannot recover the right figure — this
function receives no tokenizer, and has no reliable identity for one.

That is the third approach-level failure for this one adjustment: the wrong
proxy (characters for tokens), then the wrong space (provider for local), now
the wrong tokenizer. Each fix relocated the error rather than removing it,
because the quantity is not available at the reader without a consumer-facing
change, which this PR otherwise avoids entirely. A `provider`-equality guard
would only be a fourth proxy.

Coverage mode therefore leaves the entry at its full count. The consequence is
that the summary's tokens are counted twice — once in the entry, once as
`summary.tokenCount` — which over-states the prompt and prunes earlier than
strictly necessary. That is the safe direction; every version of the discount
risked under-counting and an over-context request instead. The reasoning is
recorded at the call site, and passing the reader a tokenizer remains the way
to fix it properly.

The positional path is untouched: it slices content and scales by the
characters that survive, entirely within one run's units.

* 🛟 fix: Require Measurable Retained Parts on the Positional Path

Addresses the twelfth codex review, on 7665fdd.

The positional path scales a summary-bearing entry by the share of characters
after the slice, and atomic media contributes none. An entry retaining only an
image after its summary therefore collapsed to 1 token while the image still
cost its fixed provider price, which can exceed the context window on replay.

This is a pre-existing fault, not one this branch introduced: verified against
unmodified main, `[text, summary, image]` takes a 1200-token entry to 1. What
this branch did was widen the set of shapes that reach it — measuring nested
tool-call content made `[tool_call, summary, image]` pass the `totalCharLen > 0`
guard that previously skipped the adjustment by accident.

Both are fixed by the discipline the coverage path already learned: every
retained part must be represented before scaling. Parts that formatting drops
are exempt, since their zero length is accurate rather than a blind spot.
Unmeasurable retained content now keeps the original count, over-counting and
pruning early instead of under-counting and overflowing.

* 🔒 fix: Gate the Positional Ratio on an Allowlist

Addresses the thirteenth codex review, on 10945d1.

The previous guard rejected retained parts that measured zero characters, which
missed media nested inside `tool_call.output`: serializing that output gives the
image a nonzero length, so the guard read it as measurable while the token
counter charges its fixed media cost. A long text run before the summary and a
short image URL nested after it took a 4000-token entry to 68.

Rejecting unmeasurable shapes case by case has now missed a nesting level twice,
so the predicate is inverted. Only `TEXT` and `THINKING` — the two shapes whose
characters `contentPartCharLength` actually reads — are eligible for the ratio.
Everything else, at any depth and including content types added later, is
ineligible by default and the entry keeps its original count.

The common case is unaffected: a summary block mid-message is normally followed
by assistant text, so all ten pre-existing positional tests still proportion as
before. Only entries retaining media, tool calls, or resources lose the
discount, in the safe direction.

Removing the positional adjustment outright was the alternative, as was done for
coverage mode, but it is not equivalent: positional slices content out of the
message, so without it the entry would be charged for everything the slice
removed — a large over-count for legacy conversations rather than a small one.

* 🔒 fix: Cancel the Positional Ratio on Any Ineligible Part

Addresses the fourteenth codex review, on 68d2fba.

The previous commit validated only the retained side. A removed tool payload
carrying a base64 image serializes to a very large denominator while the token
counter charges that media a fixed estimate, so the ratio dragged retained text
far below its real cost — the mirror image of the retained-side collapse, and
equally able to exceed the window.

Both sides are now required to be ratio-eligible: a single ineligible part
cancels the discount wherever it sits. Excluding unmeasurable removed parts from
the denominator was the narrower option, but it discards legitimate text — a
removed tool input of plain JSON really is text whose cost tracks its length —
and separating that from a media-bearing payload means recursing into arbitrary
nested output, which has already missed a level twice here.

This changes pre-existing behaviour: a legacy entry containing any tool call now
keeps its full count rather than being proportioned, so it over-counts by the
sliced-away payload and prunes earlier. Two tests that encoded the old behaviour
are reframed accordingly. The exchange is deliberate — over-counting degrades
quality, under-counting sends a request the provider rejects — and entries of
plain text and reasoning, the common shape, still proportion as before.
* 🧵 feat: Previous-Headers Continuity for Activity Labels

Threads committed headers from earlier batches into the activity-label
prompt so consecutive same-activity batches extend the run's story
instead of restating a line already on screen.

- types/activityLabel.ts: `previousLabels?: string[]` on
  RunActivityLabelOptions — run order, most recent last; hosts pass only
  COMMITTED labels (a pending slot's text is empty and a dropped fill
  never surfaced to the user).
- prompts/activityLabel.ts: renders the list as the prompt's FIRST
  section, capped at MAX_PREVIOUS_LABELS (3); dropped wholesale under
  any active tool-output redaction policy — per-agent overlays mean an
  earlier header may have been generated under ANOTHER agent's weaker
  policy, and a handoff must not leak that phrasing into this trace.
- run.ts: generateActivityLabel passes the field through to the builder.

Evidence (eval corpus replaying captured production payloads,
claude-haiku-4-5, 28 steps x 3 samples): continuity context eliminated
restatement incidents, produced correct setup-then-payoff pairs on
consecutive same-activity batches (the production failure this
addresses), and prevented premature-conclusion labels on setup batches;
+12% label-call cost from the larger prompt.

Consumer: LibreChat threads committed labels at request-build time via
its PostToolBatch hook accumulator (follow-up to LibreChat#14391).

* 🛡️ fix: Bound and Flatten Previous Labels in the Prompt

Codex round-1 findings (both P2, same root cause: previous labels are the
one input that RE-ENTERS the prompt on every later batch of a run, so one
malformed label persistently steers unrelated later labels instead of
affecting a single request).

- prompts/activityLabel.ts: `sanitizePreviousLabel` collapses whitespace
  and clips at PREVIOUS_LABEL_LIMIT (200) before interpolation. Sections
  here are delimited by blank lines, so an embedded newline could
  otherwise forge an apparent `Tool calls:` or `Label:` section — from
  ordinary noncompliance or from injection surfacing through a tool
  result. The clip matches how `lastAssistantText` and reasoning excerpts
  are already bounded: oversized headers must not inflate later requests
  past the fast model's window and starve the run of labels.
- Labels that sanitize to nothing are filtered, and the section is
  omitted entirely when none survive, so no bare bullet implies a missing
  header.
- run.ts: `extractLabel` collapses whitespace at the source. A header
  renders as one row, and hosts feed committed labels back as continuity
  context, so a multi-line result must not carry line breaks forward.

Tests: multi-line label yields exactly one header bullet with the
injected framing inert inside it (and exactly one real `Tool calls:` /
`Label:` section survives); a 5,000-char label is clipped, not inlined;
an all-blank list omits the section.
shapeRootSpan reduces a root span's observation input/output to the last
user question and assistant answer (item 2 of the Langfuse-team feedback
in #288/#316). For chain/agent roots that is pure presentation — the
child generation still records the complete prompt. But a generation
that IS the trace root (a bare model.invoke with no wrapping chain,
i.e. the activity-label path) has no child: the reduction discarded the
SystemMessage — the entire label instruction — from the only place it
is traced, making the instruction impossible to A/B against production
traffic.

Skip the observation-level reduction when the root is itself a
generation; trace-level input/output still reduce, so the trace list
keeps showing question and answer. Titles and the main stream are
unaffected (their roots are chains).

Verified end-to-end with an OTLP capture probe against the built dist:
the exported label generation now carries both messages, trace input
still reduces to the user prompt.
Ports the host fallback builder's entries framing onto the live path:
the entries heading becomes "What it called, and what came back (do not
restate these):" and the terminal cue becomes "Header:". LibreChat's
fallback has always framed the list this way — its runtime.ts documents
that without it the model "hands back a transcription" of the list —
but the guard had never been on the SDK path, which sent a bare
"Tool calls:" … "Label:".

Measured before porting, via the eval harness (agents #360, ported from
LibreChat #14527): across three independent 3-sample sweeps the guard
framing had fewer template-redundancy flags (pooled 9 vs 18 for the
control) and fewer length violations in every arm, with no per-case
regressions by eye. A post-change sweep against this builder on a local
merge with the harness confirms the as-shipped rendering: the
host-shipped arm scored its best length-violation count of any run
(8 vs 12-16 for prior controls) with zero tool-name echoes.

The section-forging defense test now attacks the new markers; the
sanitizer rationale comment follows the rename.
…ng Docs (#361)

* 📐 docs: Langfuse trace-shaping invariants and module map in AGENTS.md

Documents what "well-shaped" means for Langfuse traces (stable span
names, observation types, root input/output reduction, control-flow and
usage normalization, redaction, identity propagation, deterministic
trace ids, self-contained trace identity), where the shaping code
lives, and how to verify changes against the specs and a live Langfuse
project.

* 🧭 fix: Detach Langfuse Roots From Foreign Ambient Spans

Production traces surfaced three shaping gaps, all traced to root
observations inheriting identity from a host's ambient OpenTelemetry
span (HTTP auto-instrumentation on the global provider) that is never
exported to Langfuse:

- Trace roots arrived with dangling parents, so `shapeRootSpan` never
  fired and trace input/output stayed null.
- An agent run and the previous turn's title run fired inside one
  request context inherited the same foreign trace id and merged into
  a single trace with racing names (AgentRun vs TitleRun) and unioned
  tags (`agent` + `title`).
- `deterministicTraceId` was silently bypassed: the seeded id generator
  only runs for true roots.

Fixes:
- `RoutingLangfuseSpanProcessor` registers spans it exports in a
  WeakSet registry; `ScopedLangfuseCallbackHandler` detaches foreign
  ambient spans from the OTEL context for detached runs (roots, or
  starts whose `parentRunId` the handler never tracked — the base
  handler's runMap-miss fallback would otherwise parent them on the
  ambient span). Langfuse-managed ambient spans still parent root
  observations so hosts can group runs deliberately.
- `generateTitle` seeds its runtime scope (`title-<runId>`) mirroring
  the activity-label path, so an inherited parent seed can no longer
  collapse the title into the run's trace.
- The outer workflow node span is named with the bare agent id;
  ephemeral ids (`endpoint__model___sender[____index]`, LibreChat's
  `encodeEphemeralAgentId` format) now reduce to their stable sender
  name so switching models no longer renames the span.

Verified against live Langfuse cloud ingestion: separate AgentRun and
TitleRun traces at their deterministic ids, trace input/output
populated, root typed `agent`, `Probe Agent` span name, and nothing on
the foreign trace id.

* 🛡️ fix: Address Codex Review — Destination-Aware Detach, Stricter Id Parse

P1: a Langfuse-managed ambient span is only a safe parent for runs
exporting to the SAME destination. The span registry now records each
span's destination key (moved the processor params/key derivation into
langfuseSpanRegistry, with the credential helpers relocated to
langfuseConfig and re-exported from langfuse for API stability), and
detachment compares the ambient span's destination against the starting
run's — a tenant-B run inside tenant-A's managed span now starts its
own trace instead of dangling in B's project with A's trace id.

P2: `extractEphemeralAgentSender` now validates the full encoded-id
format — the `endpoint__model` prefix must contain both segments and no
whitespace — and root observations are never rename candidates, so
display names that merely embed `___` (e.g. `LibreChat Agent: Ops___EU`)
are left alone.

* 🎯 fix: Address Codex Round 2 — Metadata-Gated Rename, Pure Destination Identity

- Gate ephemeral-id decoding to actual workflow-agent nodes: the span
  must carry `langgraph_node` observation metadata equal to its own
  name (verified against production span attributes; `@langfuse/tracing`
  flattens string metadata raw), so ordinary chains shaped like
  `pipeline__stage___EU` are never renamed.
- Successful decodes now become `agent` observations, matching the
  inner `agent=<id>` node shaping.
- Destination identity excludes processor policy: `toolOutputTracing`
  stays in the processor cache key (redaction is baked per processor)
  but no longer splits destination keys, so a host grouping span and a
  nested run with different redaction settings still parent correctly.
@pull pull Bot locked and limited conversation to collaborators Jul 30, 2026
@pull pull Bot added the ⤵️ pull label Jul 30, 2026
@pull
pull Bot merged commit d8ab349 into innFactory:main Jul 30, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant