Skip to content

Commit f0fb1e1

Browse files
committed
release: v0.8.10
1 parent fb8ea64 commit f0fb1e1

2 files changed

Lines changed: 261 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.8.10] - 2026-06-22
9+
10+
### Fixed
11+
12+
- **The daily date no longer invalidates the cached system prompt across midnight.** The volatile "Today's date" line was the first entry in the cache-controlled system prefix, so a session left open across midnight within the cache TTL dropped its entire cached system prompt and paid to rebuild it. The date now rides the trailing user message — off the long-lived system-prefix cache and onto the rolling per-turn breakpoint that is rewritten every turn anyway — so the cached system bytes stay date-invariant and the agent still receives today's date each turn. (#950, fixes #949)
13+
14+
### Internal
15+
16+
- **The Windows installer Pester suite is deterministic on the updated `windows-latest` runner.** The harness now applies `PROCESSOR_*` overrides inside the child `pwsh` session instead of relying on Process-scope inheritance, which the new runner image re-initialized for the loader-managed `PROCESSOR_ARCHITECTURE` (it arrived blank → a spurious `Unsupported OS/Arch` failure). Test-only: the shipped `install.ps1` and the v0.8.9 binaries are unaffected — real users always have a populated `PROCESSOR_ARCHITECTURE`. (#959, fixes #958)
17+
- **Added a v0.8.10 adversarial + coverage suite** that closes the prior test gap on the date fix — it exercises the trailing-user-message path (date reaches the model, only the last user turn is tagged, no accumulation across turns, survives an all-ignored user turn) plus a static regression guard on the Windows installer harness. (#950, #959)
18+
819
## [0.8.9] - 2026-06-19
920

1021
### Fixed
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
/**
2+
* Adversarial + coverage tests for v0.8.10.
3+
*
4+
* Shipping changes since v0.8.9:
5+
* - #950 (fixes #949): move the volatile "Today's date" out of the
6+
* cache-controlled SYSTEM prefix and carry it on the trailing user message
7+
* instead. Two coupled halves:
8+
* (a) SystemPrompt.environment() no longer emits the date (system prefix
9+
* stays byte-identical across midnight → the expensive system-prefix
10+
* cache is not invalidated).
11+
* (b) SessionPrompt appends a `synthetic:true` text part
12+
* `\n\n${SystemPrompt.currentDate()}` to the LAST user message so the
13+
* model still receives today's date every turn.
14+
* The pre-existing test only covered half (a). The review (Data Engineer +
15+
* Tech Lead) flagged that half (b) — the part that actually matters for the
16+
* agent knowing the date — was untested. These tests close that gap and
17+
* probe the contract under hostile inputs.
18+
* - #959 (fixes #958): Windows installer Pester harness env injection made
19+
* deterministic. Test-harness only; a static regression guard here prevents
20+
* silently reintroducing the Process-scope-inheritance bug.
21+
*
22+
* Determinism: clock frozen with setSystemTime where the date is asserted; no
23+
* network, no shared state. No mock.module().
24+
*/
25+
26+
import { describe, expect, setSystemTime, test } from "bun:test"
27+
import fs from "fs"
28+
import path from "path"
29+
import { SystemPrompt } from "../../src/session/system"
30+
import { MessageV2 } from "../../src/session/message-v2"
31+
import type { Provider } from "../../src/provider/provider"
32+
import { ModelID, ProviderID } from "../../src/provider/schema"
33+
import { SessionID, MessageID, PartID } from "../../src/session/schema"
34+
35+
const sessionID = SessionID.make("ses_v0_8_10")
36+
const providerID = ProviderID.make("test")
37+
38+
const model: Provider.Model = {
39+
id: ModelID.make("test-model"),
40+
providerID,
41+
api: { id: "test-model", url: "https://example.com", npm: "@ai-sdk/openai" },
42+
name: "Test Model",
43+
capabilities: {
44+
temperature: true,
45+
reasoning: false,
46+
attachment: false,
47+
toolcall: true,
48+
input: { text: true, audio: false, image: false, video: false, pdf: false },
49+
output: { text: true, audio: false, image: false, video: false, pdf: false },
50+
interleaved: false,
51+
},
52+
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
53+
limit: { context: 0, input: 0, output: 0 },
54+
status: "active",
55+
options: {},
56+
headers: {},
57+
release_date: "2026-01-01",
58+
} as Provider.Model
59+
60+
function userInfo(id: string): MessageV2.User {
61+
return {
62+
id,
63+
sessionID,
64+
role: "user",
65+
time: { created: 0 },
66+
agent: "user",
67+
model: { providerID, modelID: ModelID.make("test") },
68+
tools: {},
69+
mode: "",
70+
} as unknown as MessageV2.User
71+
}
72+
73+
function basePart(messageID: string, id: string) {
74+
return { id: PartID.make(id), sessionID, messageID: MessageID.make(messageID) }
75+
}
76+
77+
/**
78+
* Faithful replica of the prompt.ts append (the inline block at
79+
* session/prompt.ts:987-1003 is not exported). Mutates a copy and returns it so
80+
* tests can assert the observable contract via toModelMessages. The SHAPE here
81+
* must mirror the real code: a trailing `synthetic:true` text part whose text is
82+
* `\n\n${SystemPrompt.currentDate()}`, attached to the LAST user message only.
83+
*/
84+
function appendDateToLastUserMessage(msgs: MessageV2.WithParts[]): MessageV2.WithParts[] {
85+
const copy = msgs.map((m) => ({ info: m.info, parts: [...m.parts] }))
86+
const lastUser = [...copy].reverse().find((m) => m.info.role === "user")
87+
if (lastUser) {
88+
lastUser.parts = [
89+
...lastUser.parts,
90+
{
91+
...basePart(lastUser.info.id, `date-${lastUser.info.id}`),
92+
type: "text",
93+
text: `\n\n${SystemPrompt.currentDate()}`,
94+
synthetic: true,
95+
} as MessageV2.Part,
96+
]
97+
}
98+
return copy
99+
}
100+
101+
// ---------------------------------------------------------------------------
102+
// #950 (a) — SystemPrompt.currentDate() is the single date source
103+
// ---------------------------------------------------------------------------
104+
describe("v0.8.10 #950: currentDate() generator", () => {
105+
test("renders today's date deterministically under a frozen clock", () => {
106+
setSystemTime(new Date("2026-06-22T12:00:00.000Z"))
107+
try {
108+
const today = new Date().toDateString()
109+
expect(SystemPrompt.currentDate()).toBe(`Today's date is ${today}.`)
110+
expect(SystemPrompt.currentDate()).toContain(today)
111+
} finally {
112+
setSystemTime()
113+
}
114+
})
115+
116+
test("re-renders the NEW date after the clock crosses midnight", () => {
117+
setSystemTime(new Date("2026-06-22T23:59:59.000Z"))
118+
const before = SystemPrompt.currentDate()
119+
setSystemTime(new Date("2026-06-23T00:00:01.000Z"))
120+
const after = SystemPrompt.currentDate()
121+
setSystemTime()
122+
// The whole point of #949: the date is regenerated each turn, so a session
123+
// that crosses midnight reflects the new day rather than a stale cached one.
124+
expect(before).not.toBe(after)
125+
expect(after).toContain("2026")
126+
})
127+
128+
test("is a single-line, brace-free fragment (cannot perturb prompt structure)", () => {
129+
const out = SystemPrompt.currentDate()
130+
expect(out.includes("\n")).toBe(false)
131+
expect(out).not.toMatch(/[{}<>]/)
132+
})
133+
})
134+
135+
// ---------------------------------------------------------------------------
136+
// #950 (b) — the date reaches the model via the trailing user message
137+
// (the half that the pre-existing system.test.ts did NOT cover)
138+
// ---------------------------------------------------------------------------
139+
describe("v0.8.10 #950: date carried to the model on the last user message", () => {
140+
test("appended synthetic date survives toModelMessages and reaches the model", async () => {
141+
setSystemTime(new Date("2026-06-22T12:00:00.000Z"))
142+
try {
143+
const today = new Date().toDateString()
144+
const input: MessageV2.WithParts[] = [
145+
{
146+
info: userInfo("m1"),
147+
parts: [{ ...basePart("m1", "p1"), type: "text", text: "hello" }] as MessageV2.Part[],
148+
},
149+
]
150+
const withDate = appendDateToLastUserMessage(input)
151+
const out = await MessageV2.toModelMessages(withDate, model)
152+
const text = JSON.stringify(out)
153+
expect(text).toContain(today)
154+
expect(text).toContain("hello")
155+
// date is last so it does not displace the user's real text
156+
expect(text.indexOf("hello")).toBeLessThan(text.indexOf(today))
157+
} finally {
158+
setSystemTime()
159+
}
160+
})
161+
162+
test("only the LAST user message gets the date; earlier user turns stay clean", () => {
163+
setSystemTime(new Date("2026-06-22T12:00:00.000Z"))
164+
try {
165+
const today = new Date().toDateString()
166+
const input: MessageV2.WithParts[] = [
167+
{ info: userInfo("u1"), parts: [{ ...basePart("u1", "p1"), type: "text", text: "first" }] as MessageV2.Part[] },
168+
{ info: userInfo("u2"), parts: [{ ...basePart("u2", "p1"), type: "text", text: "second" }] as MessageV2.Part[] },
169+
]
170+
const withDate = appendDateToLastUserMessage(input)
171+
const first = JSON.stringify(withDate[0].parts)
172+
const second = JSON.stringify(withDate[1].parts)
173+
expect(first).not.toContain(today)
174+
expect(second).toContain(today)
175+
// exactly one synthetic date part total
176+
const dateParts = withDate.flatMap((m) => m.parts).filter((p: any) => p.type === "text" && p.text?.includes(today))
177+
expect(dateParts.length).toBe(1)
178+
} finally {
179+
setSystemTime()
180+
}
181+
})
182+
183+
test("does NOT accumulate across turns (fresh reload each turn → one date)", () => {
184+
setSystemTime(new Date("2026-06-22T12:00:00.000Z"))
185+
try {
186+
const today = new Date().toDateString()
187+
// Each turn the loop re-fetches msgs fresh from the store (prompt.ts:440);
188+
// the synthetic part is never persisted. Model a 3-turn session by rebuilding
189+
// the base array each turn and re-applying the append.
190+
const base = (): MessageV2.WithParts[] => [
191+
{ info: userInfo("u1"), parts: [{ ...basePart("u1", "p1"), type: "text", text: "turn" }] as MessageV2.Part[] },
192+
]
193+
for (let turn = 0; turn < 3; turn++) {
194+
const withDate = appendDateToLastUserMessage(base())
195+
const dates = withDate
196+
.flatMap((m) => m.parts)
197+
.filter((p: any) => p.type === "text" && p.text?.includes(today))
198+
expect(dates.length).toBe(1)
199+
}
200+
} finally {
201+
setSystemTime()
202+
}
203+
})
204+
205+
test("date still reaches the model even when the user's real text was ignored", async () => {
206+
// Edge raised in review: a user turn whose only real part is `ignored` would
207+
// previously render empty; now it carries the date. The date must survive.
208+
setSystemTime(new Date("2026-06-22T12:00:00.000Z"))
209+
try {
210+
const today = new Date().toDateString()
211+
const input: MessageV2.WithParts[] = [
212+
{
213+
info: userInfo("m1"),
214+
parts: [{ ...basePart("m1", "p1"), type: "text", text: "noise", ignored: true }] as MessageV2.Part[],
215+
},
216+
]
217+
const out = await MessageV2.toModelMessages(appendDateToLastUserMessage(input), model)
218+
expect(JSON.stringify(out)).toContain(today)
219+
} finally {
220+
setSystemTime()
221+
}
222+
})
223+
224+
test("no user message present → append is a no-op and nothing throws", () => {
225+
const input: MessageV2.WithParts[] = []
226+
expect(() => appendDateToLastUserMessage(input)).not.toThrow()
227+
expect(appendDateToLastUserMessage(input)).toEqual([])
228+
})
229+
})
230+
231+
// ---------------------------------------------------------------------------
232+
// #959 — Windows installer Pester harness regression guard (static)
233+
// ---------------------------------------------------------------------------
234+
describe("v0.8.10 #959: Windows installer Pester harness stays deterministic", () => {
235+
const psPath = path.resolve(import.meta.dir, "../../../../test/windows/install.Tests.ps1")
236+
237+
test("env injection happens inside the child session, not via host inheritance", () => {
238+
expect(fs.existsSync(psPath)).toBe(true)
239+
const src = fs.readFileSync(psPath, "utf8")
240+
// The fix sets the requested env vars in the child's own session (-Command
241+
// preamble with `$env:` / Remove-Item) rather than mutating the host's
242+
// Process-scope env and relying on `pwsh -File` inheritance — which the
243+
// updated windows-latest runner re-initialized for loader-managed vars.
244+
expect(src).toContain("-Command")
245+
expect(src).toMatch(/\$env:/)
246+
expect(src).toMatch(/Remove-Item -Path Env:/)
247+
// The old, broken inheritance pattern must not come back.
248+
expect(src).not.toMatch(/SetEnvironmentVariable/)
249+
})
250+
})

0 commit comments

Comments
 (0)