request-log.ts 3.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
  1. /**
  2. * Per-loop-instance transmission bookkeeping for the reconstructability
  3. * contract: which header event to append before a request so the session log
  4. * always explains the request (the reconstructability RFC). The loop is
  5. * otherwise transmission-stateless — the comparison baseline is the log's own
  6. * folded header (`Session.requestHeader()`), so resume and fork need no
  7. * special path: a fresh loop instance simply logs a `'resume'` snapshot on
  8. * its first request and deltas from there.
  9. *
  10. * @module dsh-agent-loop/request-log
  11. */
  12. import { diffHeader, headerEquals, applyHeaderDelta } from '@deepseek-ai/dsh-session'
  13. import type { EpochHeader, Session } from '@deepseek-ai/dsh-session'
  14. /** Per-loop-instance bookkeeping: whether THIS instance has logged a header yet. */
  15. export interface TransmissionLog {
  16. /** True once this loop instance appended its anchoring `request/header` snapshot. */
  17. loggedHeader: boolean
  18. }
  19. /** Fresh bookkeeping for a newly-started loop instance. */
  20. export function createTransmissionLog(): TransmissionLog {
  21. return { loggedHeader: false }
  22. }
  23. /**
  24. * Append whatever header event this request owes the log, so folding the log
  25. * reproduces the header the request was built under. Exactly one of four
  26. * things happens:
  27. *
  28. * 1. This loop instance has not logged a header yet → a full `request/header`
  29. * snapshot anchors the fold: reason `'initial'` when the log has no header
  30. * events at all (a new conversation), `'resume'` when it does (process
  31. * restart, fork seed — the boundary itself is a recorded fact, so the
  32. * snapshot is appended even when nothing changed).
  33. * 2. The header equals the folded baseline → nothing; the log already
  34. * explains this request.
  35. * 3. It differs and the delta round-trips (`applyHeaderDelta` on the baseline
  36. * reproduces the header exactly) → a `request/header-delta`.
  37. * 4. It differs and the delta encoding cannot express the change (a pure tool
  38. * reordering) → a full snapshot with reason `'fallback'`; deltas are an
  39. * encoding optimization, never a correctness dependency.
  40. *
  41. * @param session - the session whose log explains the request.
  42. * @param state - this loop instance's bookkeeping (mutated on first log).
  43. * @param header - the canonical header the request will ACTUALLY use
  44. * (post-`agent/request`).
  45. */
  46. export function recordRequestHeader(session: Session, state: TransmissionLog, header: EpochHeader): void {
  47. if (!state.loggedHeader) {
  48. session.append('request/header', { header, reason: session.requestHeader() === undefined ? 'initial' : 'resume' })
  49. state.loggedHeader = true
  50. return
  51. }
  52. // This instance logged a snapshot, so the fold is necessarily defined.
  53. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  54. const baseline = session.requestHeader()!
  55. if (headerEquals(baseline, header)) return
  56. const delta = diffHeader(baseline, header)
  57. /* v8 ignore next -- headerEquals false ⟹ diffHeader defined: both compare the same three parts */
  58. if (delta === undefined) return
  59. if (headerEquals(applyHeaderDelta(baseline, delta), header)) {
  60. session.append('request/header-delta', delta)
  61. } else {
  62. session.append('request/header', { header, reason: 'fallback' })
  63. }
  64. }