// IAP envelope codec — wire encoder + read-both decoder + agent-facing render.
//
// Three halves (envelope-compaction F, 0.4.86):
// buildEnvelope → the WIRE form (legacy attr names, always-CDATA, full-ISO ts) —
// what bridges parse and durable stores hold; never slims.
// decodeEnvelope → READ-BOTH (legacy + compact names, /, ts),
// CDATA-AWARE so a body containing ``, ``,
// `]]>` or a literal CR round-trips by FIELD-semantic
// equivalence, not byte-equality.
// renderEnvelopeForAgent → the compact presentation for LLM-context hops (bottom).
//
// Historic origin: encoder from inter-agent-protocol preamble.ts; decoder from
// telegram-runtime parseIapEnvelope/extractIapEnvelopes. The sibling runtimes run
// their own copies of the decoder, SYNCED to this canon 14.07.2026 (telegram-runtime
// 0.26.0, notifier-runtime 0.3.6, voicetalk-runtime 0.1.1 — anchored attrValue, В37/В38,
// read-both); a decoder-behavior change here should be mirrored there (or flagged to
// their implementers). The wire form stays compatible with all of them regardless.
//
// Deliberately NO CR→LF fold here. The old telegram decoder did
// `xml.replace(/\r\n?/g, '\n')` to repair tmux paste (which rewrites LF→CR).
// That is a TRANSPORT concern: folding inside the codec silently destroys a
// literal CR that a caller legitimately put in the message, breaking round-trip.
// The fold lives in the transport / telegram pane-adapter, not here
// (blueprint §0.5-ish, brief: "CR→LF fold — в transport/telegram-адаптере, НЕ в codec").
import {
IAP_INSTRUCTION,
MAX_TOPIC_LEN,
normalizeIntelligenceValue,
type Intelligence,
type Runtime,
} from '../core/constants.ts'
import { IapError } from '../core/errors.ts'
// ─────────────────────────────────────────────────────────────────────────────
// Encoder (canon)
// ─────────────────────────────────────────────────────────────────────────────
export interface EnvelopeInput {
fromPersonality: string
fromRuntime: Runtime
fromIntelligence: Intelligence
/** ISO-8601 LOCAL instant with offset (formatSentAt) — the moment the router
* ACCEPTED the send; the same string is returned to the sender as the `ts`
* field of its send_to_peer result, so recipient-side `ts` and sender-side
* `ts` correspond literally. Rendered as the `ts` open-tag attribute.
* ADDITIVE and optional: every known envelope consumer reads attributes by
* NAME and ignores unknown ones; absent on legacy envelopes. On the WIRE this
* is always the FULL date-time (durable stores — disk queues, pane logs —
* must be unambiguous); the agent-facing render compacts it (see
* renderEnvelopeForAgent). */
sentAt?: string
topic?: string
attachments?: readonly string[]
message: string
}
const pad2 = (n: number): string => String(n).padStart(2, '0')
/** Format a Date as ISO-8601 LOCAL time with numeric offset, second precision:
* `2026-07-14T01:23:45+03:00`. Local (not UTC-Z) on purpose: the fleet's agents
* compare envelope timestamps against their LOCAL statusline clock — a Z-form
* invites silent off-by-offset misreads by an LLM reader. */
export function formatSentAt(date: Date): string {
const offMin = -date.getTimezoneOffset() // minutes EAST of UTC
const sign = offMin >= 0 ? '+' : '-'
const abs = Math.abs(offMin)
return (
`${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}` +
`T${pad2(date.getHours())}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())}` +
`${sign}${pad2(Math.floor(abs / 60))}:${pad2(abs % 60)}`
)
}
function escapeAttr(value: string): string {
return value
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(//g, '>')
}
function cdata(value: string): string {
// A literal `]]>` would terminate the CDATA early. Split it across two
// sections; the decoder reconstructs it by concatenating adjacent sections.
return `', ']]]]>')}]]>`
}
/** WIRE encoder — the canonical durable form (disk queues, bridge deliveries,
* pane logs): legacy attribute names + always-CDATA body, so every deployed
* sibling parser (telegram-/notifier-/voicetalk-runtime) keeps working
* unchanged. `ts` is the only additive attribute (envelope-compaction F).
* Agent-bound deliveries are re-rendered compactly at the LAST hop — see
* renderEnvelopeForAgent; the wire form itself never slims (its bytes cost no
* tokens; only the LLM-context presentation does). */
export function buildEnvelope(input: EnvelopeInput): string {
const attrs = [
`from-personality="${escapeAttr(input.fromPersonality)}"`,
`from-runtime="${escapeAttr(input.fromRuntime)}"`,
`from-intelligence="${escapeAttr(input.fromIntelligence)}"`,
]
if (input.sentAt) attrs.push(`ts="${escapeAttr(input.sentAt)}"`)
const topic = input.topic?.trim()
if (topic) {
attrs.push(`topic="${escapeAttr(topic.slice(0, MAX_TOPIC_LEN))}"`)
}
return [
``,
IAP_INSTRUCTION,
...(input.attachments?.length
? [`${cdata(input.attachments.join('\n'))}`]
: []),
`${cdata(input.message)}`,
'',
].join('\n')
}
// ─────────────────────────────────────────────────────────────────────────────
// CDATA-aware low-level scan
// ─────────────────────────────────────────────────────────────────────────────
const CDATA_OPEN = ''
/**
* Scan from `start` and return the index of the first occurrence of `needle`
* that lies OUTSIDE any CDATA section, or -1 if none (or if an unterminated
* CDATA section is hit — meaning the buffer is incomplete and the caller should
* wait for more input). CDATA sections are skipped wholesale: `]]>` inside them
* is the section terminator, never a match for `needle`.
*/
function indexOfOutsideCdata(buffer: string, needle: string, start: number): number {
let i = start
while (i < buffer.length) {
if (buffer.startsWith(CDATA_OPEN, i)) {
const term = buffer.indexOf(CDATA_CLOSE, i + CDATA_OPEN.length)
if (term < 0) return -1 // unterminated CDATA → incomplete buffer
i = term + CDATA_CLOSE.length
continue
}
if (buffer.startsWith(needle, i)) return i
i++
}
return -1
}
/**
* Extract and decode the content of `…`, treating the body as a
* sequence of CDATA sections (and any stray raw text) and concatenating them.
* Concatenating adjacent CDATA sections is exactly what reverses the
* `]]>` → `]]]]>` split the encoder performs, so this both
* (a) ignores `` / `` that appear inside CDATA, and
* (b) reconstructs a literal `]]>` in the original payload.
* Returns undefined when the open tag is absent or the close tag is never
* reached outside CDATA.
*/
function readTagContent(xml: string, tag: string): string | undefined {
const open = `<${tag}>`
const close = `${tag}>`
// В37 — the OPEN tag must be located CDATA-aware too (the close-side already was): a
// message whose CDATA body QUOTES `…` otherwise minted
// phantom attachments, and a quoted `` corrupted the decoded message.
const openIdx = indexOfOutsideCdata(xml, open, 0)
if (openIdx < 0) return undefined
let i = openIdx + open.length
let out = ''
while (i < xml.length) {
if (xml.startsWith(CDATA_OPEN, i)) {
const term = xml.indexOf(CDATA_CLOSE, i + CDATA_OPEN.length)
if (term < 0) {
// Unterminated CDATA — malformed; treat the remainder as raw content.
out += xml.slice(i + CDATA_OPEN.length)
return out
}
out += xml.slice(i + CDATA_OPEN.length, term)
i = term + CDATA_CLOSE.length
continue
}
if (xml.startsWith(close, i)) return out
out += xml[i]
i++
}
return undefined // close tag never found
}
// ─────────────────────────────────────────────────────────────────────────────
// Decoder
// ─────────────────────────────────────────────────────────────────────────────
export interface IapEnvelope {
fromPersonality: string
fromRuntime: Runtime
fromIntelligence?: Intelligence
/** The sender's dispatch instant (`ts` attribute) — full local ISO on the wire;
* absent on legacy envelopes. */
sentAt?: string
topic?: string
attachments: string[]
message: string
}
/** NAME-ANCHORED attribute lookup. The anchor `(^|\s)` is load-bearing: the old
* unanchored regex made `runtime="` match the TAIL of `from-runtime="…"` (and
* `intelligence="` the tail of `from-intelligence="…"`) — a latent mine that the
* read-both decoder below would have stepped on (the short-name lookup must NOT
* be satisfied by a legacy long-name attribute). */
function attrValue(attrs: string, name: string): string | undefined {
const re = new RegExp(`(?:^|\\s)${name}="([^"]*)"`)
const m = re.exec(attrs)
return m ? unescapeAttr(m[1]) : undefined
}
function unescapeAttr(value: string): string {
// Reverse escapeAttr. Order matters: & LAST so an escaped "<"
// in the source does not get double-decoded.
return value
.replace(/"/g, '"')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/&/g, '&')
}
/** READ-BOTH decoder: accepts the legacy wire names (`from-personality` /
* `from-runtime` / `from-intelligence`, ``) AND the compact
* presentation names (`from` / `runtime` / `intelligence`, ``) — so any
* envelope form ever emitted on this host (wire, agent-facing render, historic
* transcripts) decodes. Short names win when both are present. */
export function decodeEnvelope(xml: string): IapEnvelope {
const open = /^]*)>/.exec(xml.trim())
if (!open) throw new IapError('invalid IAP envelope: missing ')
const fromPersonality = attrValue(open[1], 'from') ?? attrValue(open[1], 'from-personality')
const fromRuntime = attrValue(open[1], 'runtime') ?? attrValue(open[1], 'from-runtime')
if (!fromPersonality || !fromRuntime) {
throw new IapError('invalid IAP envelope: missing from-personality/from-runtime')
}
// READ-COMPAT: a legacy peer (live telegram) may stamp from-intelligence="human";
// normalize human→natural / scripted→absent, drop a genuinely unknown value.
const fromIntelligence = normalizeIntelligenceValue(
attrValue(open[1], 'intelligence') ?? attrValue(open[1], 'from-intelligence'),
)
const message = readTagContent(xml, 'message') ?? readTagContent(xml, 'msg')
if (message === undefined) throw new IapError('invalid IAP envelope: missing message')
const attachmentsRaw = readTagContent(xml, 'attachments')
const topic = attrValue(open[1], 'topic')
const sentAt = attrValue(open[1], 'ts')
return {
fromPersonality,
fromRuntime,
...(fromIntelligence ? { fromIntelligence } : {}),
...(sentAt ? { sentAt } : {}),
...(topic ? { topic } : {}),
attachments: attachmentsRaw
? attachmentsRaw.split('\n').map(item => item.trim()).filter(Boolean)
: [],
message,
}
}
// В38 — a real envelope's open tag is bounded: personality/runtime ≤32 chars each,
// topic ≤200, intelligence one word, plus attr names and escaping. 1 KiB is far above
// any legitimate open tag; a longer '>'-less run after `' yet within bounds — wait for the next chunk). */
function openTagVerdict(candidate: string): 'valid' | 'invalid' | 'incomplete' {
const m = /^]*)>/.exec(candidate)
if (!m) {
const unclosed = candidate.indexOf('>') < 0
return unclosed && candidate.length <= MAX_OPEN_TAG_LEN ? 'incomplete' : 'invalid'
}
if (m[0].length > MAX_OPEN_TAG_LEN) return 'invalid'
// Read-both (envelope-compaction F): a real open tag carries the required pair
// under EITHER naming — legacy wire (from-personality/from-runtime) or compact
// presentation (from/runtime, name-anchored so a legacy tail never satisfies it).
const hasPersonality = m[1].includes('from-personality="') || /(?:^|\s)from="/.test(m[1])
const hasRuntime = m[1].includes('from-runtime="') || /(?:^|\s)runtime="/.test(m[1])
return hasPersonality && hasRuntime ? 'valid' : 'invalid'
}
/**
* Pull complete `…` envelopes out of a streaming buffer.
* The closing `` is located CDATA-aware, so an envelope whose message
* body contains the literal text `` is not truncated. `rest` holds the
* trailing bytes that do not yet form a complete envelope (incl. an envelope
* still mid-CDATA), to be prepended to the next chunk.
*
* В38 — a FALSE start (prose that merely contains ` 0) rest = rest.slice(start)
const verdict = openTagVerdict(rest)
if (verdict === 'incomplete') return { envelopes, rest } // open tag split across chunks → wait
if (verdict === 'invalid') {
rest = rest.slice(1) // false start: skip past this '<' and rescan for the next '', ''.length
const candidate = rest.slice(0, envelopeEnd)
try {
decodeEnvelope(candidate)
} catch {
// Structurally envelope-shaped but undecodable → a false start after all;
// resync so a REAL envelope inside/behind the candidate is still found.
rest = rest.slice(1)
continue
}
envelopes.push(candidate)
rest = rest.slice(envelopeEnd)
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Agent-facing presentation render (envelope-compaction F)
// ─────────────────────────────────────────────────────────────────────────────
/** Presentation-hybrid gate: a body that itself contains envelope machinery
* (a quoted envelope, tag markers, CDATA syntax) or a literal CR keeps its
* CDATA wrapping in the presentation too — the cost of the gate firing is
* purely COSMETIC (the agent sees today's CDATA form), whereas rendering such
* a body raw would show the agent a "broken" nested envelope. Plain `<` (code
* snippets etc.) renders raw: nothing parses the presentation. */
const PRESENTATION_CDATA_MARKERS = [
'',
'',
'',
'',
'',
'',
'',
'',
'\r',
] as const
function presentBody(value: string): string {
return PRESENTATION_CDATA_MARKERS.some(marker => value.includes(marker)) ? cdata(value) : value
}
/** Compact the wire `ts` (full local ISO with offset) for the agent's eyes:
* time-only `01:23:45` when the send happened on the SAME local calendar day
* as the render (the normal case), date-prefixed `2026-07-13 01:23:45` when it
* did not (queue drains, long sleeps, midnight crossings) — so a stale message
* can never masquerade as fresh. Renders happen at the LAST delivery hop, so
* "today" is delivery-time local. An unparseable ts passes through verbatim. */
function renderTsForAgent(sentAt: string, now: Date): string {
const d = new Date(sentAt)
if (Number.isNaN(d.getTime())) return sentAt
const time = `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`
const sameLocalDay =
d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate()
return sameLocalDay ? time : `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${time}`
}
/**
* Render a WIRE envelope into the compact agent-facing presentation:
*
*
* Reply via send_to_peer.
* raw body…
*
*
* Short attribute names, time-only `ts` (cross-day rule above), raw body
* (hybrid CDATA when the body quotes envelope machinery). Called ONLY at the
* two hops where text enters an LLM context (composeFirstMessage — wake boot +
* ephemeral drain; deliverWarm — live paths, gated on agent runtimes); bridge
* targets always receive the wire form their parsers expect.
*
* FAIL-OPEN: any decode/render problem returns the wire text unchanged — a
* presentation hiccup must never lose or mangle a delivery.
*/
export function renderEnvelopeForAgent(wire: string, now: Date = new Date()): string {
try {
const env = decodeEnvelope(wire)
const attrs = [`from="${escapeAttr(env.fromPersonality)}"`, `runtime="${escapeAttr(env.fromRuntime)}"`]
if (env.fromIntelligence) attrs.push(`intelligence="${escapeAttr(env.fromIntelligence)}"`)
if (env.sentAt) attrs.push(`ts="${escapeAttr(renderTsForAgent(env.sentAt, now))}"`)
if (env.topic) attrs.push(`topic="${escapeAttr(env.topic)}"`)
return [
``,
IAP_INSTRUCTION,
...(env.attachments.length
? [`${presentBody(env.attachments.join('\n'))}`]
: []),
`${presentBody(env.message)}`,
'',
].join('\n')
} catch {
return wire // fail-open: deliver the wire form rather than risk the message
}
}