Reginald's context is tiered so the model always sees what matters without ever copying ticket data into the prompt: - HOT (this module) — charter + active directives + the focus snapshot, packed under a hard token budget (2k). Assembled fresh each turn as the prompt seed. - WARM — the append-only directive/event ledger + digest, summarized on demand. - COLD — gitea + sidecar via query_project. Ticket bodies/comments/detail live here and are NEVER inlined; the model fetches them by number when needed. - `assembleHotContext(inputs, budget=2000)`: focus (tiny, always kept) → most recent active directives (each while they fit ~⅔) → charter fills the true remainder, truncated on a line boundary. Measures the fixed tail exactly and reserves for header/joiner/ellipsis so the total never exceeds budget. - `estimateTokens` (tokenizer-free ~4 chars/token, slight over-estimate so a real tokenizer stays under), `activeDirectives` (accepted/amended, most-recent-first). Acceptance met: hot assembles under the 2k budget even with a ~34k-token charter; nothing ticket-shaped is inlined (only numbers + titles for focus). +5 core tests; full core suite + typecheck green. Follow-up: wire assembleHotContext into the live system prompt in main (needs charter + directives + focus at chat time) — the assembly + budget is the tested core. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
72 lines
3.2 KiB
TypeScript
72 lines
3.2 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import type { DirectiveEntry } from '../directives/record-directive-v0.js'
|
|
import type { Focus, ScheduledItem } from '../scheduler/scheduler-v0.js'
|
|
import {
|
|
activeDirectives,
|
|
assembleHotContext,
|
|
estimateTokens,
|
|
HOT_CONTEXT_BUDGET_TOKENS,
|
|
} from './memory-v0.js'
|
|
|
|
const item = (number: number, title: string): ScheduledItem =>
|
|
({ number, title, labels: [], order: 0, startDay: 0, endDay: 1, durationDays: 1, blockedBy: [], blocks: [], critical: false, rationale: '' })
|
|
|
|
const focus: Focus = { now: item(7, 'Fix lifecycle inference'), next: item(8, 'Webhook listener'), later: null }
|
|
|
|
const directive = (over: Partial<DirectiveEntry>): DirectiveEntry => ({
|
|
id: 'd1',
|
|
ts: '2026-02-10T09:00:00Z',
|
|
status: 'accepted',
|
|
kind: 'note',
|
|
quote: 'pilots come first',
|
|
...over,
|
|
})
|
|
|
|
describe('memory-v0 (#27)', () => {
|
|
it('estimateTokens is a slight over-estimate (~4 chars/token)', () => {
|
|
expect(estimateTokens('')).toBe(0)
|
|
expect(estimateTokens('abcd')).toBe(1)
|
|
expect(estimateTokens('a'.repeat(4001))).toBe(1001)
|
|
})
|
|
|
|
it('activeDirectives keeps accepted/amended, most-recent-first', () => {
|
|
const ds = [
|
|
directive({ id: 'a', ts: '2026-02-01T00:00:00Z', status: 'accepted', quote: 'old' }),
|
|
directive({ id: 'b', ts: '2026-02-11T00:00:00Z', status: 'amended', quote: 'new' }),
|
|
directive({ id: 'c', ts: '2026-02-12T00:00:00Z', status: 'withdrawn', quote: 'gone' }),
|
|
directive({ id: 'd', ts: '2026-02-09T00:00:00Z', status: 'proposed', quote: 'maybe' }),
|
|
]
|
|
expect(activeDirectives(ds).map((d) => d.quote)).toEqual(['new', 'old'])
|
|
})
|
|
|
|
it('assembles hot context under the 2k budget even with a huge charter', () => {
|
|
const huge = 'Charter line that goes on and on. '.repeat(2000) // ~34k tokens
|
|
const ds = Array.from({ length: 50 }, (_, i) =>
|
|
directive({ id: `d${i}`, ts: `2026-02-${String((i % 27) + 1).padStart(2, '0')}T00:00:00Z`, quote: `directive number ${i}` }),
|
|
)
|
|
const out = assembleHotContext({ charter: huge, directives: ds, focus })
|
|
|
|
expect(estimateTokens(out)).toBeLessThanOrEqual(HOT_CONTEXT_BUDGET_TOKENS)
|
|
// focus (tiny) is always kept; the charter is the part that gets truncated
|
|
expect(out).toContain('## Focus')
|
|
expect(out).toContain('#7 Fix lifecycle inference')
|
|
expect(out).toContain('…') // charter was clamped
|
|
// at least some recent directives survived
|
|
expect(out).toContain('## Active directives')
|
|
})
|
|
|
|
it('never inlines ticket bodies — only numbers + titles appear for focus', () => {
|
|
// The assembler takes no issue bodies by construction; focus shows #number title only.
|
|
const out = assembleHotContext({ charter: 'Ship the beta.', directives: [directive({})], focus })
|
|
expect(out).toContain('Now: #7 Fix lifecycle inference')
|
|
expect(out).toContain('[note] pilots come first')
|
|
expect(out).not.toMatch(/body|description|comment/i)
|
|
})
|
|
|
|
it('degrades to just focus when there is no charter or directives', () => {
|
|
const out = assembleHotContext({ charter: '', directives: [], focus })
|
|
expect(out).toBe(['## Focus', 'Now: #7 Fix lifecycle inference', 'Next: #8 Webhook listener', 'Later: —'].join('\n'))
|
|
})
|
|
})
|