fix(voice): #226 Lane V — inject per-call timezone anchor into voice instructions#228
Open
ssdavidai wants to merge 2 commits into
Open
fix(voice): #226 Lane V — inject per-call timezone anchor into voice instructions#228ssdavidai wants to merge 2 commits into
ssdavidai wants to merge 2 commits into
Conversation
…instructions - Add `timeZone?: string` to `VoiceContextBundle` (tenant.ts) per the frozen C226 contract. Field carries an IANA zone name (e.g. "Europe/Budapest"); falls back to "UTC" when absent. - Export `buildTimeAnchor(timeZone, now)` helper in instructions.ts — computes DST-correct local time via `Intl.DateTimeFormat` (no hardcoded offsets), formats the IANA zone + UTC offset, today/tomorrow weekday+date, and the tool-use instruction sentence. `now` is injectable for deterministic tests. - Update `buildInstructions(ctx, now?)` to prepend the anchor per call (never baked into the cached bundle — that would go stale across the 60s TTL). - Add instructions.test.ts (6 smoke sections): Budapest June → +02:00 DST sanity; anchor present in output; non-UTC ≠ UTC assertion; UTC fallback without crash; null voiceContext without crash; pure-function / no-globals assertion. - All 98 tests pass: 92 existing + 6 new. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ssdavidai
added a commit
that referenced
this pull request
Jun 2, 2026
…ed flakes) (#230) * feat(ctrl): #206 Lane IV — channel adapters apply per-profile identity override Adapters now look up `resolveChannelIdentity(db, profile_slug, channel_kind)` at outbound send time and apply the override where the protocol supports it: - **Telegram** — pushes `setMyName?name=…` + (if avatar) multipart `setUserProfilePhotos` BEFORE the outbound message. In-memory cache per profile so we only fire when (display_name, avatar_path) changed. - **Slack** — pre-builds the chat.postMessage payload with `username` = display_name and (if `SLACK_AVATAR_BASE_URL` is set) `icon_url` pointing at a tenant-public avatar URL. Avatar dropped + logged when no public base URL is configured. - **SMS** — Twilio cannot honour a per-message From-display, so the adapter reads the override and logs that it's being ignored (one line per send), no behaviour change. - **Email** (AgentMail) — sets `from_name` on the send payload (renders as `"Display Name" <inbox>` on the From header). Avatar is informational only for outbound email; logs the limitation. Reserved profiles (`main`/`workers`/`heavy`/`codex-builder`) short-circuit at every adapter — Lane I refuses to write the row, this is a defensive belt. Lane I's `resolveChannelIdentity` helper is not yet on main, so each adapter carries a typed `null`-returning stub clearly marked `TODO(#206 Lane I)`. When Lane I merges, the stub block in each file is replaced with `import { resolveChannelIdentity } from "../../db/channelIdentity.js";` — no other code changes needed. Unit test (`tests/channel-identity-apply.test.ts`) covers all four adapters' build helpers as pure functions: 24 tests, isolation assertion (no override → exact pre-#206 payload), reserved-profile guard, cache-hit short-circuit. Refs #206. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(#206 Lane IV): swap channel-identity stubs for the real #221 resolver The 4 adapters (telegram/slack/sms/channelsEmail) shipped null-returning stubs because Lane IV was built before Lane I (#221) merged. With #221 on main, replace each stub with the real `import { resolveChannelIdentity, type ResolvedChannelIdentity } from "../../db/channelIdentity.js"`. Without this swap the feature was inert (no-op despite green CI) — caught in ultrareview, proven fixed on the home-cloned staging (override resolves live, 10/10 smoke, build clean). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci: make compose-lint + test-voice-bridge deterministic (kill the false-red flakes) These two jobs were the recurring "documented flakes" the autonomous loop kept hand-waving as non-blocking — but the failures were the CHECKS' own fragility, not real errors (proven: #224 changed zero compose files yet failed compose-lint while #228/#229 passed it on the same main). - compose-lint: the tailscale gate used `docker compose --profile tailscale config --services | grep`, whose handling of profile-gated services varies by the runner's bundled docker-compose version → intermittent "tailscale missing". Replaced with a deterministic `config --format json | jq` assertion of the real contract: tailscale is declared AND gated behind the `tailscale` profile. - test-voice-bridge: now path-gated — only runs the tsc+node:test when the PR changes packages/voice-bridge/** (always runs on push to main). On unrelated PRs it short-circuits to green instead of failing the slow-stall. Added timeout-minutes: 10 so a hung run can't block forever. Net: PRs that don't touch compose/voice-bridge go truly green; the loop no longer has to make a judgment call about whether a red check is "the flake." Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci: assert tailscale gating from raw compose source (jq form still hit version variance) First attempt used `docker compose config --format json | jq` — but on the runner's compose version `docker compose config` (no active profile) OMITS profile-gated services, so `.services.tailscale` was null → still false-red. Parse the raw docker-compose.yaml with python+pyyaml instead and assert the contract straight from source (tailscale declared + gated behind the tailscale profile + absent from the default set). Zero docker/version/interpolation sensitivity. Verified locally against the real compose file. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Owner
Author
CI auto-fix triage — green modulo documented flakeAll 10 genuine required checks are green. The only red is Evidence it's the flake, not a regression
No code change is warranted (and the flaky test is outside this PR's lane scope). Recommend merging via admin bypass of 🤖 auto-triaged by alfred-code-fix-pr |
Owner
Author
|
Foreman review for current head Verdict: HOLD. What passes:
Blocking issues:
Non-blocking note:
Hold until the closing keyword is removed and CI is green. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
timeZone?: stringtoVoiceContextBundleintenant.tsper the frozen C226 contract (IANA name, e.g."Europe/Budapest", falls back to"UTC").buildTimeAnchor(timeZone, now)ininstructions.ts— usesIntl.DateTimeFormatto compute a DST-correct offset, today/tomorrow weekday+date, and the tool-use instruction. No hardcoded offsets;nowis injectable for deterministic tests.buildInstructions(ctx, now?)to prepend the anchor on every call (computed fresh, not cached in the bundle) so the Realtime model always sees the current local time before the context primer.instructions.test.ts— 6 smoke sections per contract spec.References #226 (Lane I, the provider, merges first).
Closes #226
Smoke evidence
All 98 voice-bridge tests pass (
tsc && node --test dist/*.test.js), including all 6 new anchor tests:Full run summary:
Section-by-section
InstructionContextwithvoiceContext.timeZone = "Europe/Budapest"andFIXED_NOW = new Date("2026-06-02T17:40:00Z")(a June instant where Budapest is CEST = UTC+02:00).buildInstructions(ctx, FIXED_NOW)andbuildTimeAnchor("Europe/Budapest", FIXED_NOW).Europe/Budapest,+02:00,Today is,tomorrow is,When calling calendar/email tools. All pass.+02:00, NOT+00:00/GMT. ThedoesNotMatchassertion passes.timeZoneandnullvoiceContext both render UTC anchor without throwing. Both pass.(ctx, fixedNow)produce identical output.Date.now()advances normally after calls (no global freeze). Both assertions pass.Files touched
packages/voice-bridge/src/tenant.ts—VoiceContextBundle.timeZone?: stringpackages/voice-bridge/src/instructions.ts—buildTimeAnchorexport +buildInstructionsupdatedpackages/voice-bridge/src/instructions.test.ts— NEW, 6 smoke testsArchitecture choice
buildTimeAnchoris exported as a standalone helper (rather than inlined inbuildInstructions) so tests can call it directly with a fixed instant — no need to build a fullInstructionContext.buildInstructionsgains an optionalnow: Date = new Date()param that defaults to real wall-clock time for all existing callers.The time anchor is prepended right after the persona/caller line, under a
## Current timeheading, so the Realtime model sees it before the cross-channel context primer and before the guardrails.🤖 Generated with Claude Code