loop: every request is built from the log — boundary snapshot, header events, config-only waterfall

The loop is now transmission-stateless; a request is a pure function of
(session log, this step's rendered assembly, current AgentOptions):

- The reconstruction boundary is step/start: the messages snapshot is
  taken in the same synchronous frame immediately before the step/start
  append, so the request's messages are exactly the derivation over
  events[0..stepStartSeq) — an inject() from an agent/request listener
  (or any concurrent task) lands after the boundary and joins the NEXT
  request. This changes behavior for a synchronous step/start
  session/event listener that appends content (master derived after the
  append, so such a listener could reach the current request):
  agent/pre-step is the sanctioned seam for current-request content.
- agent/request is re-typed to config-only: (agent, turn, step,
  config: LlmCallConfig, next) → LlmCallConfig. The frozen seed comes
  from AgentOptions on a loop instance's first request (explicit options
  beat the logged baseline — fork overrides and resume reconfiguration
  stay correct) and from the log's folded header afterwards; listeners
  return a replacement to switch. Content shaping through the request is
  no longer expressible — model-visible content flows through the log
  channels.
- recordRequestHeader appends whatever header event the request owes the
  log before dispatch: an 'initial'/'resume' snapshot anchoring each
  loop instance, a round-trip-verified delta on change, a 'fallback'
  snapshot when the encoding cannot express it. Session.requestHeader()
  is the log's incrementally-folded baseline.
- Requests are deep-frozen before dispatch (deepFreeze exempts the
  AbortSignal — freezing one breaks AbortController.abort() outright);
  frozen + sessionId is the loop-built marker the dev invariant keys on.

Ported from #162 and re-anchored on the log: the append-extension /
frozen-end-to-end / compaction-resend / prompt-change property tests,
plus new specs for the boundary semantics, resume anchoring, and the
end-to-end theorem (every recorded request rebuilds byte-equal from the
log alone). Live cache-hit e2e (request-cache.e2e.ts) verified against
the real DeepSeek API. Snapshot goldens intentionally stale until the
single re-record after the compact/summary envelope lands.
This commit is contained in:
Tianyi Cui
2026-07-06 03:07:34 +08:00
parent a4f7e757fa
commit 2093a8898b
17 changed files with 745 additions and 68 deletions

View File

@@ -45,7 +45,10 @@ export function callConfigEquals(a: LlmCallConfig, b: LlmCallConfig): boolean {
* never rewrite it, so the wire bytes cannot silently desync from what the
* session log reconstructs. Guards against cycles with a WeakSet: loop-built
* requests hold `structuredClone`d JSON-validated session data, but the
* helper accepts arbitrarily constructed values.
* helper accepts arbitrarily constructed values. One exemption: an
* `AbortSignal` is never entered or frozen — it is the request's live
* cancellation channel, and freezing one breaks `AbortController.abort()`
* outright (Node stores the aborted flag as an own property of the signal).
* @param value - the value to freeze in place.
* @returns the same value, frozen.
*/
@@ -53,6 +56,7 @@ export function deepFreeze<T>(value: T): T {
const seen = new WeakSet<object>()
const walk = (node: unknown): void => {
if (node === null || typeof node !== 'object') return
if (node instanceof AbortSignal) return
if (seen.has(node)) return
seen.add(node)
Object.freeze(node)

View File

@@ -33,6 +33,18 @@ describe('deepFreeze', () => {
expect(() => { (value.a.b[1] as { c: string }).c = 'y' }).toThrow(TypeError)
})
it('never freezes an AbortSignal: the live cancellation channel keeps working', () => {
const controller = new AbortController()
const request = deepFreeze({ model: 'm', signal: controller.signal })
expect(Object.isFrozen(request)).toBe(true)
expect(Object.isFrozen(controller.signal)).toBe(false)
let fired = false
controller.signal.addEventListener('abort', () => { fired = true }, { once: true })
controller.abort('stop')
expect(fired).toBe(true)
expect(controller.signal.aborted).toBe(true)
})
it('passes primitives through and terminates on cycles', () => {
expect(deepFreeze(42)).toBe(42)
expect(deepFreeze(null)).toBeNull()