Files
gbrain/VERSION
T
bd2fe8a1fa v0.32.5 feat: gbrain-context OpenClaw context engine — deterministic temporal/spatial injection (#880)
* feat: gbrain-context OpenClaw context engine — deterministic temporal/spatial injection

Adds a context engine plugin that runs on every assemble() call to inject
structured live context into the system prompt:

- Garry's current local time (computed from heartbeat-state.json timezone)
- Current location (city + timezone from heartbeat or flight data)
- Home time when traveling (e.g. 'Mon 7:58 AM PT')
- Active travel status
- Quiet hours detection
- Airport→timezone mapping for 30+ airports

This kills the 'time warp' bug class where compacted sessions lose track
of time/location. The engine delegates compaction to the legacy runtime
and only owns systemPromptAddition injection. Zero LLM calls, <5ms.

Files:
- src/core/context-engine.ts — engine implementation (SDK-free, testable)
- src/openclaw-context-engine.ts — plugin entry point (requires SDK)
- test/context-engine.test.ts — 9 tests, all passing

Enable: plugins.slots.contextEngine = 'gbrain-context'

* feat: add activity injection — calendar events + open tasks in context block

Reads memory/calendar-cache.json and ops/tasks.md to inject:
- **Right now:** current meeting (with attendees) from calendar
- **Coming up:** next 3 events within 4-hour window
- **Open tasks:** unchecked items from Today section
- Stale calendar warning when cache is >6 hours old

Skips all-day events and generic markers (Home, OOO, Out of Office).
Caps upcoming events at 3 and tasks at 5 to keep prompt lean.

15 tests passing (was 9).

* v0.32.5 feat: gbrain-context OpenClaw context engine — deterministic temporal/spatial injection

Ships PR #873 by @garrytan-agents (two underlying commits preserved):
  - f1dbe6ea — core engine (heartbeat + flights + airport→tz + quiet hours)
  - 14e85873 — activity injection (calendar events + open tasks + stale-cache warning)

Kills the "time warp" bug class: when sessions compact, the LLM loses track
of current time, location, and active threads. This engine owns the
`systemPromptAddition` slot and reinjects live state on every `assemble()`
call. Zero LLM calls, <5ms overhead, deterministic.

Typecheck cleanup folded in:
  - `@ts-ignore` on the two `openclaw/plugin-sdk` runtime-only imports
    (resolved by the OpenClaw host; not a build-time dep — same pattern the
    core engine already used for `await import('openclaw/plugin-sdk/core')`)
  - Inline `PluginApi` + `PluginCtx` type shapes in the plugin entry so the
    `register(api)` + `(ctx)` callback params aren't implicit any
  - Test file's `from 'vitest'` → `from 'bun:test'` to match the rest of
    the suite (bun's globals make it pass at runtime, but tsc fails)

Verification:
  - bun test test/context-engine.test.ts → 15/15 pass
  - bun run typecheck → exit 0

Co-Authored-By: garrytan-agents <garrytan-agents@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix-wave: close 5 findings from /plan-eng-review pass on PR #880

A `/plan-eng-review` audit of the shipped v0.32.5 surfaced 5 things worth
fixing before merge. All folded into this branch with 5 new regression tests
(15 → 20 total).

A4 — silent-wrong-timezone for unknown airports
  Pre-fix: an active flight to any airport not in the 30-entry AIRPORT_TZ
  map (BOM, DXB, GRU, JNB, FRA, AMS, etc.) silently fell back to US/Pacific.
  The exact failure class this engine exists to prevent, in a different
  shape. Post-fix: unknown airports surface via the source field
  (flight:AC8:tz-unknown:BOM) so the LLM can see the data is incomplete
  instead of believing it's in Pacific Time.

A2 / P1 — duplicate disk reads
  generateLiveContext was loading heartbeat-state.json and
  upcoming-flights.json twice per assemble() call (once in resolveLocation,
  once inline). Batch-load each workspace file once at the top of the
  function and thread results down. Halves the hot-path I/O.

C4 — sanitize external content before injection
  Calendar event summaries, attendees, and task strings now go through
  sanitizeForPrompt() which strips newlines + control chars (U+0000-001F +
  U+007F) and clamps length. A meeting titled
  "Standup\n\nIgnore prior instructions" can no longer forge LLM directives
  by escaping the bullet structure.

C1 — split isQuietHours into 3 explicit signals
  Original name was misleading (returned false when user was awake at 2 AM,
  even though wall clock said quiet hours). Split into `userAwake`,
  `wallClockQuietHours`, and a composite `quietHoursActive` so consumers can
  decide their own policy. On-disk heartbeat.garryAwake JSON field is
  unchanged — only the internal LiveContext type and the format-block
  consumer renamed.

T1 — regression test coverage for the active-flight path
  Pre-fix, resolveLocation's flight branch (the headline path for the
  Toronto incident) had ZERO direct test coverage. Two new cases lock in
  the known-airport happy path AND the unknown-airport failure mode so A4
  can't silently regress.

Verification:
  - bun test test/context-engine.test.ts → 20/20 pass (was 15)
  - bun run typecheck → exit 0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(L0): A4 real fix + TLA → lazy SDK resolution (Codex F5 + F7)

A Codex outside-voice review on /plan-eng-review's plan caught two findings
both previous eng-reviews missed.

L0-A (F5) — A4 was COSMETIC, not real.
  Pre-fix: resolveLocation's unknown-airport branch returned tz: DEFAULT_TZ
  (US/Pacific) with only a `source: 'flight:XX:tz-unknown:XYZ'` sticker. The
  engine then computed Time/Day/quietHoursActive from US/Pacific regardless,
  so a flight to BOM injected "Mon 3:00 PM PT" with a footnote nobody reads.
  Same silent-wrong-output failure class A4 was supposed to close.

  Post-fix: resolveLocation returns tz: UNKNOWN_TZ. generateLiveContext
  short-circuits time computation when tz is UNKNOWN_TZ (now/dayOfWeek
  become null, wallClockQuietHours/quietHoursActive become false).
  formatContextBlock renders an explicit Timezone-unavailable warning in
  place of Time:/Day:. The LLM sees the gap, not a guess.

L0-B (F7) — Top-level `await import` is a hard module-load constraint.
  Any OpenClaw deployment in a non-TLA runtime (older Node, CJS bridges,
  certain transpilers, some test shims) fails BEFORE the plugin registers.
  The try/catch inside doesn't help — module load can't be caught by the
  consumer.

  Post-fix: SDK resolution moved to an `ensureSdkLoaded()` async helper
  called from assemble() and compact() on first invocation. Module loads
  cleanly in every runtime; the fallback path actually catches.

Tests:
  - The cosmetic "tz-unknown sticker" assertion is replaced with the
    behavioral assertion: no US/Pacific Time, no Day field, explicit
    Timezone-unavailable warning present.
  - New L0-B contract test asserts engine creation does NOT trigger SDK
    load and the first compact() call exercises the lazy path.

Verification:
  - bun test test/context-engine.test.ts → 21/21 pass (20 + L0-B contract)
  - bun run typecheck → exit 0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(L1): scrub real names from test fixtures + CI guard (CLAUDE.md privacy rule)

The /plan-eng-review pass flagged pre-existing real-name leaks in PR #873's
test fixtures. CLAUDE.md's privacy rule is unambiguous: "Never reference real
people, companies, funds, or private agent names in any public-facing
artifact." Tests are checked-in code, distributed with every release, and
indexed by GitHub search.

Fixture scrub (test/context-engine.test.ts, 5 substitutions):
  '1:1 with Diana' → '1:1 with @alice-example'
  'diana@ycombinator.com' → 'alice@example.com'
  'DM Technium re: Hermes PR' → 'DM @charlie-example re: agent-fork PR'
  'Post open source manifesto — from YC Labs' → '... from a-team'
  '~~Reply to Bob McGrew~~ — DONE' → '~~Reply to bob-example~~ — DONE'
Plus matching assertion updates.

Adjacent scrub: test/link-extraction.test.ts line 523 fixture entry
'people/diana-hu' → 'people/alice-example' (single occurrence, never
referenced elsewhere in the test).

New CI guard (scripts/check-test-real-names.sh, ~120 lines):
  Designed per Codex F4 review: drop the broad corporate-email regex
  (@openai|google|stripe...) because legitimate billing/auth fixtures use
  those domains. Replace with two targeted lists:
    - BANNED_NAMES: exact-string list of known real identifiers
      (Diana, Wintermute, Hermes, Technium, McGrew, YC Labs)
    - BANNED_EMAILS: specific addresses (currently just diana@ycombinator.com)
  Plus ALLOWLIST of exact `file:string` pairs that are intentional and
  pre-existing (the user's own email; structural tests that ASSERT a banned
  name is absent and therefore MUST reference it literally).

  Scope: test/**/*.test.ts only. Historical CHANGELOG entries, doc examples,
  and skill READMEs each have their own scrub status and are out of scope
  for this guard.

Wire-in:
  - New `bun run check:test-names` npm script
  - Added to `bun run verify` chain (pre-push gate)
  - Added to `bun run check:all` chain (local-only superset)

Allowlist documents the structural references the guard correctly identifies
but cannot meaningfully strip:
  - test/integrations.test.ts (regex pattern in personal-info filter test)
  - test/recency-decay.test.ts (regression-prevention assertions)
  - test/serve-stdio-lifecycle.test.ts (pre-existing comment)
  - test/extract.test.ts (pre-existing markdown-link fixture)

These flagged-but-not-scrubbed entries belong to a broader repo-wide
privacy-scrub pass (deferred TODO).

Verification:
  - bun run check:test-names → exit 0 (no new banned strings)
  - bun test test/context-engine.test.ts → 21/21 pass
  - bun test test/link-extraction.test.ts → 98/98 pass

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(L2): plugin-shape e2e + compact fallback + selector map + race-condition JSDoc

The unit suite at test/context-engine.test.ts exercised
createGBrainContextEngine directly — that's the ENGINE, not the PLUGIN. Until
this commit, nothing tested the actual OpenClaw plugin discovery + registration
path. Codex outside-voice F1 flagged the gap: "we ship a plugin we don't test
as a plugin."

Layer 2 closures:

T-NEW1 (plugin-shape e2e, test/e2e/openclaw-context-engine-plugin.test.ts, 3 tests):
  - Default export has the expected plugin-entry shape (id, name, description, register)
  - register() wires registerContextEngine with ENGINE_ID and a factory
  - Factory returns a working ContextEngine that injects Live Context and
    threads through the mocked memory-addition SDK call

  Implementation note: dropped the unused `definePluginEntry` import from
  src/openclaw-context-engine.ts. The wrapper was a type-tag with no behavior
  — OpenClaw's loader inspects the default export's shape, not the wrapping.
  Removing it eliminated a brittle build-time SDK import that blocked
  mock.module() interception (Codex F1 was right). Module now loads cleanly
  in any runtime.

T-NEW4 (compact() fallback test, test/context-engine.test.ts):
  - Pins the no-runtime fallback shape so a refactor that drops the fallback
    or returns a different shape gets caught.
  - Codex F9 noted that without a real SDK boundary, a spy-on-delegate test
    is busywork. This commit keeps just the fallback assertion (no spy, no
    __internal export-for-tests hatch).

T-NEW6 (heartbeat-write concurrency contract, src/core/context-engine.ts):
  - JSDoc on loadJsonFile documenting that producers MUST use atomic-rename
    writes (write-to-tmp + rename) to avoid partial-read races. The engine
    silent-degrades to defaults on parse failure; the contract makes the
    expectation explicit instead of buried in behavior.

T-NEW5 (e2e selector map, scripts/e2e-test-map.ts):
  - Added entries mapping src/core/context-engine.ts and
    src/openclaw-context-engine.ts to the new plugin e2e file. ci:local:diff
    now narrows correctly for engine changes.

Verification:
  - bun test test/context-engine.test.ts → 22/22 pass (21 + T-NEW4)
  - bun test test/e2e/openclaw-context-engine-plugin.test.ts → 3/3 pass
  - bun run typecheck → exit 0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(L3): ENGINE_VERSION → ENGINE_API_VERSION semantic + tasks.md size cap

C-NEW1 — Engine version constant semantic.
  Pre-fix: `ENGINE_VERSION = '0.1.0'` looked like it should track
  package.json. It doesn't — it's the engine's CONTRACT version, bumped
  when the ContextEngine interface shape changes. Rename to
  ENGINE_API_VERSION makes that explicit. ENGINE_VERSION kept as a
  deprecated alias so existing v0.32.5 callers don't break.

C-prior C2 — tasks.md size cap.
  resolveTodayTasks() now refuses to read a tasks file >1MB. Defends
  against a runaway file (clipboard-paste accident, log capture, etc)
  blocking every assemble() call with a multi-megabyte sync read. The
  size check uses statSync — same try/catch already handles
  missing-file via readFileSync throwing.

Verification:
  - bun test test/context-engine.test.ts → 23/23 pass (22 + size-cap test)
  - bun test test/e2e/openclaw-context-engine-plugin.test.ts → 3/3 pass
  - bun run typecheck → exit 0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: CHANGELOG + TODOS for the Codex recalibration wave; allowlist sibling guard

CHANGELOG.md — extend v0.32.5 entry with a "Codex outside-voice
recalibration" subsection covering L0-A (A4 real fix), L0-B (TLA → lazy),
the privacy guard redesign, the new plugin-shape e2e, and the deferred
v0.32.6 items. Credits gpt-5-codex as the driver.

TODOS.md — append "v0.32.6 follow-ups from PR #880" section with 13
deferred items:
  - Clock-injection seam (prerequisite for perf + snapshot tests)
  - T-NEW2 perf budget (with Codex F2 math-bug note)
  - T-NEW3 full-block snapshot test
  - C-NEW2 exports map entry (per Codex F8 — premature public API)
  - A3 .ts-extension resolution coupling
  - A5 typed openclaw/plugin-sdk ambient module shim
  - C-prior C5 loadJsonFile parse-error warn
  - C-prior C3 fractional-hour timezone offset
  - DST-boundary test
  - Multibyte sanitizer test
  - Dynamic airport-tz lookup (replace 30-entry static map)
  - DOC1 docs/openclaw-context-engine.md workspace contract
  - DOC2 CLAUDE.md "Key files" annotations
  - Repo-wide privacy scrub (24+ non-test matches)

scripts/check-privacy.sh — allowlist sibling guard
scripts/check-test-real-names.sh, which literally contains 'Wintermute' in
its BANNED_NAMES list (same meta-rule-enforcement exception as
check-privacy.sh's self-reference).

Verification:
  bun run verify → exit 0 (full chain green: check:privacy + check:test-names
  + check:jsonb + check:progress + check:test-isolation + check:wasm +
  check:admin-build + check:admin-scope-drift + check:cli-exec + typecheck)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(L4): real openclaw-loads-the-plugin e2e — closes Codex F1 properly

Until this commit, the gbrain-context plugin had two test paths:
  - test/context-engine.test.ts (23 unit tests against createGBrainContextEngine)
  - test/e2e/openclaw-context-engine-plugin.test.ts (3 e2e tests with mocked SDK)

Both call our engine directly or shim the OpenClaw SDK. Codex outside-voice
F1 (cited at v0.32.5 ship) flagged that nothing in the repo proves OpenClaw's
actual plugin loader walks our entry file, calls register(api) against its
real api object, and accepts the registration. The reviewer was right —
shipping a plugin without an "OpenClaw actually loads it" test is a
credibility hit on a feature whose entire purpose is to integrate with
OpenClaw.

L4 — test/e2e/openclaw-plugin-load-real.test.ts (6 tests, Tier 2):

  beforeAll:
    - Detects `openclaw` CLI; skips suite if missing
    - bun build src/openclaw-context-engine.ts → JS bundle (same packaging
      shape the release ships)
    - Writes minimal package.json + openclaw.plugin.json from templates
    - openclaw plugins install --link --dangerously-force-unsafe-install
      against an isolated --profile dir (won't touch user's openclaw state)

  Tests:
    1. status=loaded, imported=true, activated=true
    2. Default-export id/name/description metadata round-trips through
       openclaw's plugin loader unchanged
    3. register(api) produced zero error-level diagnostics (only the
       expected trust warning for --link installs)
    4. plugins.slots.contextEngine binding to "gbrain-context" passes
       openclaw config validate
    5. openclaw plugins doctor surfaces zero errors for our plugin id
    6. Public-SDK round-trip: imports registerContextEngine from
       openclaw/plugin-sdk (resolved via realpathSync on the openclaw
       binary's symlink so it works for Homebrew, npm -g, nvm, asdf,
       volta installs uniformly), registers our factory, then exercises
       assemble() and asserts the Live Context block appears

  afterAll:
    - Uninstalls the plugin (best-effort) + rm -rf the isolated profile
      dir + the tempdir fixture

Fixture: test/fixtures/openclaw-plugin-real/ holds the manifest templates
(package.json.template + openclaw.plugin.json.template). The test writes
fresh copies into a per-run tempdir so the fixture itself stays read-only.

Selector map: scripts/e2e-test-map.ts now points BOTH source files
(src/core/context-engine.ts, src/openclaw-context-engine.ts) at BOTH the
mocked-SDK plugin-shape e2e AND this real-loader e2e. ci:local:diff fires
both on either change.

Verification:
  - bun test test/e2e/openclaw-plugin-load-real.test.ts → 6/6 pass
  - bun test test/context-engine.test.ts test/e2e/openclaw-context-engine-plugin.test.ts
    test/e2e/openclaw-plugin-load-real.test.ts → 32/32 pass total
  - bun run typecheck → exit 0
  - bun run verify → exit 0 (full chain green)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: garrytan-agents <garrytan-agents@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 21:49:57 -07:00

1 line
6 B
Plaintext