Skip to content

feat(dream-daemon): gate per-mind memory consolidation behind dreamDaemon feature flag#369

Open
johnhain-msft wants to merge 27 commits into
ianphil:masterfrom
johnhain-msft:feature/dream-daemon-memory-consolidation
Open

feat(dream-daemon): gate per-mind memory consolidation behind dreamDaemon feature flag#369
johnhain-msft wants to merge 27 commits into
ianphil:masterfrom
johnhain-msft:feature/dream-daemon-memory-consolidation

Conversation

@johnhain-msft

Copy link
Copy Markdown

Summary

Adds the Dream Daemon — a per-mind background job that consolidates the structured log.md into a refined memory.md between turns — behind a new dreamDaemon app feature flag. Defaults: off in stable, on in insiders. When the flag is off, no runtime is constructed, no IPC is reachable, no UI is shown, and IdentityLoader reports the working-memory section as disabled regardless of any per-mind .chamber.json opt-in (so a stable build never includes log content in the system prompt for a mind that was opted in under insiders).

What it does

Surface Flag ON (insiders default) Flag OFF (stable default)
Runtime MindMemoryService constructed in initializeRuntime(); wired to mind:loaded / mind:unloaded and app.before-quit Not constructed; no scheduler timers
IPC IPC.MIND.SET_DREAM_DAEMON resolves to updated MindContext Rejects with "Dream Daemon is not available in this build"
Genesis genesis.create({ enableDreamDaemon: true }) writes .chamber.json with workingMemory.consolidation.enabled: true Server-side coerced to false regardless of payload
Prompt WorkingMemoryComposer includes consolidated log.md excerpts Forced disabled in IdentityLoader.resolveComposerConfig; no log content in prompt
UI Genesis RoleScreen shows the Dream Daemon switch; AgentProfileModal shows the toggle row Both hidden; MindProfileService reports dreamDaemonEnabled: false
E2E hook __chamberMindMemoryService exposed under CHAMBER_E2E=1 Absent

Defense in depth: even if a renderer is compromised, the IPC throws, the genesis path coerces, and MindManager.enableDreamDaemon itself rejects when the accessor returns false.

Changes

  • Shared typesdreamDaemon added to AppFeatureFlags, defaults, resolver, complete-policy parser, with tests.
  • Remote flag policydocs/flags/v1/flags.json adds dreamDaemon: false (stable) / true (insiders).
  • FeatureFlagService — complete-policy fixtures cover dreamDaemon in remote success, cache hit, and all-false fallback paths.
  • Runtime composition (apps/desktop/src/main.ts) — buildMindMemoryService only called when flag is on; uses the master-aligned loadBetterSqlite3() injection (no separate sqlite resolution).
  • IPC gatesmind.ts and genesis.ts accept featureEnabled parameter; server-side coercion on genesis.create.
  • ServicesMindManager.enableDreamDaemon / disableDreamDaemon and IdentityLoader.resolveComposerConfig take a dreamDaemonFeatureEnabled accessor; MindProfileService masks per-mind state when off.
  • UIRoleScreen, GenesisFlow, AgentProfileModal consume useAppFeatureFlags() and hide controls / coerce payloads when off.
  • Docsai-docs/feature-flags.md adds the dreamDaemon row + behavior subsection.
  • Sync with master — merges v0.64.0-insiders.4 (cron run history persistence, SDK 0.3.0 RuntimeConnection.forStdio API migration, macOS prepackaged code-signing scripts, prewarm-no-await black-screen fix). Test fakes that previously used session.destroy updated to session.disconnect to match the SDK 0.3 surface that upstream's master now uses.

Verification

  • npm run lint — clean (tsc + eslint + deps:check + lint:yaml + lint:md)
  • npm test2475 passed / 1 skipped / 0 failed across 208 test files
  • npm run smoke:sdk — pinned runtime resolves
  • npm run smoke:packaged-runtime — sqlite runtime + ledger path pack correctly
  • ⚠️ npm run smoke:desktop25 passed / 16 skipped / 4 failed; all 4 failures are pre-existing on master:
    • 3 marketplace specs fail in this environment because there is no local access to private agency-microsoft/genesis-minds
    • 1 conversation-history-smoke.spec.ts sidebar render lag — pre-existing on master baseline
    • Net better than baseline: model-switch-context-smoke (failed pre-merge) now passes, likely fixed by upstream's cron-history work
  • ✅ Manual click-through smoke (flag ON + flag OFF), 28 screenshots, no console or main-process errors. UI gates verified visible (ON) / hidden (OFF) on RoleScreen and AgentProfileModal.

Notes for reviewer

  • The dreamDaemon flag defaults to off in stable — merging this PR does not change behavior for stable users until docs/flags/v1/flags.json is graduated.
  • The enableConfigDiscovery: false + configDir change from 97abd51 is preserved; the dream-daemon prompt-path gate is orthogonal (per-mind .chamber.json, not the SDK config discovery flag).
  • chamber-copilot-runtime/ was bumped by upstream to SDK 0.3.0; nothing in this PR re-pins it.

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

johnhain-msft and others added 27 commits May 12, 2026 19:20
… dbPath

Test-only accessor on the MindMemoryService interface, matching the existing `__resetMindMutexForTesting` pattern in consolidation-scheduler. Returns `{ daemon, dbPath } | null` so a Playwright driver attached via `electronApp.evaluate()` can drive forceRun()/getStatus() and read the per-mind dream.db from disk without us building a renderer-facing IPC bridge.
…e handle)

Two narrow, env-gated hooks so the chamber-ui-tester Playwright driver can exercise the consolidation cycle deterministically without a renderer bridge:

1. DreamDaemon: `maybeTestSleep()` between the memory.md write and the log.md prune, gated on `CHAMBER_E2E=1 && CHAMBER_DREAM_TEST_SLEEP_MS > 0`. Lets M7 reproduce the mid-cycle append race (turn arrives after snapshot but before prune). Production builds never see CHAMBER_E2E=1, so this is a no-op by construction.

2. main.ts: when CHAMBER_E2E=1, stash mindMemoryService on globalThis.__chamberMindMemoryService. `electronApp.evaluate(...)` can then call `__debugGet(mindId).daemon.forceRun()` and read `dbPath` directly — no IPC channel, no preload bridge, no shared-package type contract.
… (v0.59.7)

Two production-blocking defects surfaced during manual testing of
feature/dream-daemon-memory-consolidation: every fresh mind logged
"[MindScaffold] Capability bootstrap failed (non-fatal): Error: Upgrade
skill not found in genesis repo", and "[WorkingMemoryComposer] log.md
is unstructured" repeated on every system-prompt rebuild.

Root causes:
1. Epic ianphil#67 moved the upgrade skill from ianphil/genesis to
   ianphil/genesis-frontier in commit 17580f5, but
   MindScaffold.GENESIS_SOURCE was never updated.
2. MindScaffold.createStructure() wrote a zero-byte log.md placeholder
   which violated the chamber-structured-log/v1 sentinel contract, so
   WorkingMemoryComposer.readLog warned and skipped on every prompt
   rebuild until the first DailyLogWriter rotation fired.

Fixes:
- MindScaffold now points at ianphil/genesis-frontier@main; the
  upgrade-skill not-found error names the repo and branch.
- createStructure() pre-seeds log.md with the sentinel + double newline
  (matching DailyLogWriter.seedFreshLog byte-for-byte) BEFORE the
  WORKING_MEMORY_FILES placeholder loop runs.
- Genesis prompt no longer instructs the LLM to write to log.md (the
  file is reserved for DailyLogWriter frames). SOUL.md "Continuity"
  rewritten and the Mind Index drops the log.md bullet.
- WorkingMemoryComposer.readLog downgrades the unstructured-log message
  from warn to info for the migration window.

Validation:
- New tests/integration/mindScaffold.integration.test.ts (7 tests) drives
  the full MindScaffold.create() against mkdtempSync with fake registry
  + SDK clients. Locks the on-disk contract for new minds AND the
  cross-cutting migration story for existing pre-fix minds (composer
  info -> DailyLogWriter rotates -> composer silent).
- 49/49 targeted unit tests across the 4 affected files.
- Full suite: 2035/2035 pass; lint clean.
- Live Electron smoke (chamber-ui-tester) confirmed both error patterns
  are gone end-to-end.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Brings in A2A mailbox relay (v0.60.0, v0.61.0) and the v0.59.7 revert of
"Refresh models" (issue ianphil#90 / docs/model-cache-investigation.md).

Conflict resolutions:
- CHANGELOG.md: dream-daemon entry renumbered to v0.62.0; master's v0.61.0,
  v0.60.0, v0.59.7 entries preserved below.
- package.json: version 0.62.0; @github/copilot 1.0.45 (master).
- config/vitest.config.ts: kept both new include entries
  (tests/integration/** + .github/extensions/**/*.mjs) and pool: 'forks'.
- apps/desktop/src/main.ts: adopted master's A2A composition
  (activeA2AResolver, a2aRelayModeService, MessageRouter, A2aToolProvider,
  updated wireLifecycleEvents + setupA2AIPC signatures); kept
  mindMemoryComposition + CHAMBER_E2E hook; requestQuit now closes
  mindMemoryComposition, then mindManager.shutdown, then settles
  a2aRelayModeService.disconnect + stopMvpServer.
- packages/services/src/mind/MindManager.ts: accepted master's deletion of
  recycleClientForMind; kept enableDreamDaemon/disableDreamDaemon/
  toggleDreamDaemon/daemonToggling and patchChamberMindConfig/
  rollbackToUnstructured imports.
- package-lock.json: regenerated via full npm install
  (npm install --package-lock-only strips optional emnapi entries
  CI needs).

Preflight: npm run lint clean, 2158/2159 tests pass (1 intentional skip).
Brings dream-daemon-memory-consolidation up to date with master:

- Adopts master's chamber-sqlite-runtime + loadBetterSqlite3 injection. The bespoke better-sqlite3 loader in buildMindMemoryService.ts is gone; main.ts feeds the already-resolved Database constructor in. Both the task ledger and the dream daemon now resolve sqlite via the unified runtime.

- Moves dream-daemon composition (buildMindMemoryService + mind:loaded/mind:unloaded listeners) from top-level into initializeRuntime(), right after mindManager.setProviders(...), so it slots into master's new boot sequence. Module-level mindMemoryComposition/mindMemoryService lets keep requestQuit() teardown and the CHAMBER_E2E global wired the same as before.

- Reconciles ChatService constructor with master (mindManager, turnQueue, dateTimeContextProvider?, byoLlmModelsProvider?) and keeps add/removeObserver as field-initialized infra. Test sites use svc.addObserver(...).

- Keeps both enableDreamDaemon/disableDreamDaemon (HEAD) and BYO LLM provider config integration (master) describe blocks in MindManager.test.ts.

- forge.config.ts: drops better-sqlite3/bindings/file-uri-to-path from the ASAR unpack glob (they ship via chamber-sqlite-runtime extraResource now).

- CHANGELOG: moves the dream-daemon v0.62.0 entry into Unreleased > Added (Keep-a-Changelog 1.1.0).

Next: Phases 2-7 wire the dream-daemon surfaces behind a new app-level dreamDaemon feature flag, then Phase 8 runs full regression.
Adds dreamDaemon to AppFeatureFlags, DEFAULT_APP_FEATURE_FLAGS, getAppFeatureFlags, parseFeatureFlags, parseCompleteFeatureFlags. DEV defaults to true; remote policy (stable + insiders) defaults to false.

Extends shared parser tests with dreamDaemon assertions and a missing-field rejection. Updates FeatureFlagService.test.ts complete-policy fixtures and renderer/test-helper fixtures so AppFeatureFlags-shaped literals satisfy the new required field.

No runtime gating yet — Phase 3 wraps the dream-daemon composition in main.ts behind this flag.
…aemon flag

Layered defense-in-depth so the dream-daemon stack is fully inert when

the new app-level dreamDaemon feature flag is off:

- apps/desktop/src/main.ts: module-level dreamDaemonFeatureEnabled() reads

  appFeatureFlags at call time; buildMindMemoryService composition + the

  __chamberMindMemoryService E2E global are wrapped in if(flag); accessor

  is passed positionally into IdentityLoader (arg 3), MindManager (arg 7),

  MindProfileService (arg 4), setupMindIPC and setupGenesisIPC configs.

- main/ipc/mind.ts: SET_DREAM_DAEMON rejects when enabled && !flag, disable

  is always allowed so a stable build can clean up insiders opt-in state.

- main/ipc/genesis.ts: new GenesisIPCOptions; CREATE handler coerces

  enableDreamDaemon=false in sanitizedConfig server-side when flag is off.

- MindManager.doToggleDreamDaemon: throws Error('Dream Daemon is not

  available in this build') before mind lookup when enabled && !flag.

- IdentityLoader.resolveComposerConfig: forces enabled=false in the

  composer config when the accessor returns false, regardless of the

  per-mind .chamber.json. Caps (lastKTurns, perTurnMaxBytes, memoryMaxBytes)

  remain faithful so a future flip-on does not lose the user's settings.

- MindProfileService.getProfile: AND'd the accessor with the .chamber.json

  value so the (now-hidden) UI never sees a stale ON state.

- renderer RoleScreen/GenesisFlow/AgentProfileModal: read featureFlags from

  useAppState; hide the Switch / toggle row when flag is off and coerce

  enableDreamDaemon=false at every emit boundary.

Tests: 6 new service-layer gate tests across MindManager, IdentityLoader,

and MindProfileService (146 passing). 4 new renderer flag-off tests across

RoleScreen, GenesisFlow, AgentProfileModal (25 passing). Existing tests

updated to pass testInitialState={ featureFlags: { dreamDaemon: true } }

where they assert on the dream-daemon surface.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add dreamDaemon row to the Current flags table (dev-only rollout: stable

  remote=false, insiders remote=false, dev=true).

- New 'Dream Daemon' subsection enumerates every off-state behavior

  (buildMindMemoryService not called, IPC reject, genesis.create coercion,

  MindManager throw, IdentityLoader enabled=false override, MindProfileService

  payload override, RoleScreen/GenesisFlow/AgentProfileModal UI hide).

- Update DEV_FEATURE_FLAGS snippet and the docs/flags/v1/flags.json example

  to include dreamDaemon.

- Fix MD022/MD032 in AGENTS.md (blank lines around the Dream Daemon heading

  added on the branch) so 'npm run lint' is clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…(v0.64.0-insiders.4)

Brings in: cron run history persistence, packaged copilot SDK 0.3.0 with RuntimeConnection.forStdio API, macOS code signing prepackaged scripts, prewarm-no-await black-screen fix.
….0 API

SDK 0.3.0 CopilotSession exposes disconnect() not destroy(). The merge auto-took the dream-daemon-branch mock shape (destroy:) while production code (MindScaffold.generateSoul finally block) calls session.disconnect() — same as upstream master. Aligning fakes with the real SDK surface.
…olidation

Resolves conflicts in MindManager.ts and main.ts constructor arg list. Both branches added a new positional arg after byoDefaultModelProvider: this branch added dreamDaemonFeatureEnabled (slot 7), upstream/master added managedSkillService (now slot 8). Both have defaults / are optional, so existing callers stay valid. The one upstream test (line 471) that explicitly passed managedSkillService at slot 7 is updated to pass undefined,managedSkillService. Brings in: author-your-own ttasks cron automation (fdc0891), skill loading fix (ed7181e), managed skills marketplace (61d0fce), v0.64.0 version bump (91ae68e). Regression: lint clean, npm test 2540/0fail, smoke:sdk + smoke:packaged-runtime green, smoke:desktop 24/16skip/6fail (4 pre-existing baseline + 2 environmental LLM-timeout tests on upstream's own new cron-tools spec and conversation-history flake).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant