Skip to content

fix(imessage): the world model must keep learning on a MUTED channel - #253

Merged
dshakes merged 1 commit into
masterfrom
fix/world-model-runs-when-muted
Sep 19, 2026
Merged

dshakes merged 1 commit into
masterfrom
fix/world-model-runs-when-muted

Conversation

@dshakes

@dshakes dshakes commented Sep 19, 2026

Copy link
Copy Markdown
Owner

Live finding, 24 hours after the world model deployed: it had not run once.

The iMessage bridge is muted. maybeRefreshWorldModel sat below the proactivePaused() early return, and that returns true whenever this.muted is set. On a muted channel the entire refresh was unreachable — the feature shipped switched off by an unrelated flag.

That is exactly backwards:

Change

  • maybeRefreshWorldModel moves above the pause gate, still under the killswitch.
  • Quiet hours and an explicit quiet Nh still defer it — its FYI would wake the owner. The next tick after the window picks it up.
  • The killswitch (the do-nothing-at-all switch) still stops it.

The regression test is structural on purpose: the bug was where the call sat. Below that return it is dead code, and no behavioural test of refreshWorldModel itself would ever have caught it.

Verification

check result
world-model-mute-scope.test.ts (new, 3 cases) pass
imessage-bridge full suite 120 pass / 0 fail
tsc clean

Live behaviour UNVERIFIED until merged and deployed; then the first refresh logs world model: within ~45 min.

…— it was dead code for the one incident it was built for

Live finding, 24h after deploy: the world-model refresh had not run ONCE.
The iMessage bridge is muted, and `maybeRefreshWorldModel` sat below the
`proactivePaused()` early return — which is true whenever `this.muted` is
set. On a muted channel the whole refresh was unreachable.

That is exactly backwards. Mute means "don't SEND to contacts" (#250), not
"stop thinking". The refresh messages no one: it reads the owner's own mail,
calendar and self-chat notes and updates the OWNER's profile. And the bridge
WAS muted on 2026-09-18 — so the refresh that would have corrected the
stale grand-opening date never ran, on the very day the stale date made the
bot correct a friend twice. The feature shipped switched off by an unrelated
flag.

- `maybeRefreshWorldModel` moves ABOVE the pause gate, under the killswitch.
- Quiet hours and an explicit "quiet Nh" still defer it (its FYI would wake
  the owner); the next tick after the window picks it up.
- The killswitch — the do-nothing-at-all switch — still stops it.
- Regression test is structural, because the bug was WHERE the call sat:
  below that return it is dead code, and no behavioural test of the function
  itself would have caught it.

Verified: imessage-bridge 120/0, tsc clean.
@github-actions github-actions Bot added the domain:core Core library / business logic label Sep 19, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🔎 Codex cross-audit (agent:audit)

No Blocking findings.

I didn’t find a correctness or security regression in the PR diff. Moving maybeRefreshWorldModel() above proactivePaused() does what the PR intends: it still honors kill switch, owner-channel existence, quiet hours, and explicit proactive quiet windows, while bypassing only global auto-reply mute.

Residual note: the new regression test is structural/source-order based, but that style already exists in this package and is acceptable for this narrow wiring invariant.

Verification: targeted test could not be run because vitest is not installed in services/imessage-bridge (sh: 1: vitest: not found).

@compass-sdlc-bot compass-sdlc-bot Bot added the agent:reviewed-clean Reviewer found no Blocking issues this round label Sep 19, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🛡️ Vuln scan — ❌ vulnerable dependency found

cd services/control-plane && govulncheck ./...
=== Symbol Results ===

Vulnerability #1: GO-2026-6443
    Server panic via missing authority or Host headers in google.golang.org/grpc
  More info: https://pkg.go.dev/vuln/GO-2026-6443
  Module: google.golang.org/grpc
    Found in: google.golang.org/grpc@v1.82.1
    Fixed in: google.golang.org/grpc@v1.82.2
    Example traces found:
      #1: cmd/server/main.go:263:32: server.main calls grpc.Server.Serve, which eventually calls transport.http2Server.HandleStreams

Vulnerability #2: GO-2026-6348
    Heap Memory Exhaustion (OOM) via HTTP/2 DATA Frame Fragmentation in
    google.golang.org/grpc
  More info: https://pkg.go.dev/vuln/GO-2026-6348
  Module: google.golang.org/grpc
    Found in: google.golang.org/grpc@v1.82.1
    Fixed in: google.golang.org/grpc@v1.83.1
    Example traces found:
      #1: internal/handlers/runs.go:447:25: handlers.RunService.StreamRunEvents calls grpc.GenericServerStream[github.com/dshakes/lantern/gen/go/lantern/v1.StreamRunEventsRequest, github.com/dshakes/lantern/gen/go/lantern/v1.StreamEvent].Send, which eventually calls mem.BufferSlice.MaterializeToBuffer
      #2: internal/handlers/runs.go:447:25: handlers.RunService.StreamRunEvents calls grpc.GenericServerStream[github.com/dshakes/lantern/gen/go/lantern/v1.StreamRunEventsRequest, github.com/dshakes/lantern/gen/go/lantern/v1.StreamEvent].Send, which eventually calls mem.IsBelowBufferPoolingThreshold
      #3: internal/handlers/runs.go:447:25: handlers.RunService.StreamRunEvents calls grpc.GenericServerStream[github.com/dshakes/lantern/gen/go/lantern/v1.StreamRunEventsRequest, github.com/dshakes/lantern/gen/go/lantern/v1.StreamEvent].Send, which eventually calls mem.NewBuffer
      #4: internal/handlers/dataplane.go:519:30: handlers.DataPlaneService.RunStream calls grpc.GenericServerStream[github.com/dshakes/lantern/gen/go/lantern/v1.DpRunStreamClientMsg, github.com/dshakes/lantern/gen/go/lantern/v1.DpRunStreamServerMsg].Recv, which eventually calls mem.ReadAll
      #5: internal/handlers/llm_proxy.go:3919:104: handlers.LlmProxyHandler.Transcribe calls multierr.multiError.Error, which eventually calls mem.writer.Write
      #6: internal/handlers/runtime.go:316:28: handlers.grpcSchedulerClient.Exec calls grpc.clientStream.CloseSend, which eventually calls transport.ClientStream.Close
      #7: internal/handlers/runtime.go:316:28: handlers.grpcSchedulerClient.Exec calls grpc.clientStream.CloseSend, which eventually calls transport.ClientStream.Header
      #8: internal/handlers/dataplane.go:519:30: handlers.DataPlaneService.RunStream calls grpc.GenericServerStream[github.com/dshakes/lantern/gen/go/lantern/v1.DpRunStreamClientMsg, github.com/dshakes/lantern/gen/go/lantern/v1.DpRunStreamServerMsg].Recv, which eventually calls transport.ClientStream.Read
      #9: internal/handlers/runtime.go:316:28: handlers.grpcSchedulerClient.Exec calls grpc.clientStream.CloseSend, which eventually calls transport.ClientStream.RecvCompress
      #10: internal/handlers/runtime.go:316:28: handlers.grpcSchedulerClient.Exec calls grpc.clientStream.CloseSend, which eventually calls transport.ClientStream.TrailersOnly
      #11: internal/handlers/schedules.go:237:33: handlers.RESTHandler.UpdateSchedule calls time.LoadLocation, which eventually calls transport.NewHTTP2Client
      #12: cmd/server/main.go:263:32: server.main calls grpc.Server.Serve, which eventually calls transport.NewServerTransport
      #13: internal/handlers/dataplane.go:519:30: handlers.DataPlaneService.RunStream calls grpc.GenericServerStream[github.com/dshakes/lantern/gen/go/lantern/v1.DpRunStreamClientMsg, github.com/dshakes/lantern/gen/go/lantern/v1.DpRunStreamServerMsg].Recv, which eventually calls transport.ServerStream.Read
      #14: internal/handlers/dataplane.go:519:30: handlers.DataPlaneService.RunStream calls grpc.GenericServerStream[github.com/dshakes/lantern/gen/go/lantern/v1.DpRunStreamClientMsg, github.com/dshakes/lantern/gen/go/lantern/v1.DpRunStreamServerMsg].Recv, which eventually calls transport.Stream.ReadMessageHeader
      #15: internal/handlers/schedules.go:237:33: handlers.RESTHandler.UpdateSchedule calls time.LoadLocation, which eventually calls transport.http2Client.Close
      #16: internal/handlers/schedules.go:237:33: handlers.RESTHandler.UpdateSchedule calls time.LoadLocation, which eventually calls transport.http2Client.GracefulClose
      #17: internal/handlers/runtime.go:316:28: handlers.grpcSchedulerClient.Exec calls grpc.clientStream.CloseSend, which eventually calls transport.http2Client.NewStream
      #18: cmd/server/main.go:906:25: server.main calls grpc.Server.GracefulStop, which eventually calls transport.http2Server.Close
      #19: cmd/server/main.go:263:32: server.main calls grpc.Server.Serve, which eventually calls transport.http2Server.HandleStreams

Your code is affected by 2 vulnerabilities from 1 module.
This scan also found 1 vulnerability in packages you import and 19
vulnerabilities in modules you require, but your code doesn't appear to call
these vulnerabilities.
Use '-show verbose' for more details.
make: *** [Makefile:270: audit] Error 3

@github-actions

Copy link
Copy Markdown
Contributor

🔷 Gemini cross-audit (agent:audit-gemini)

An audit of the unified diff in pr-diff.patch against the base repository has been completed.

Audit Verdict: 🟢 PASS (No Blocking Issues)

The patch is sound, secure, and ready to merge. It implements the desired behavior correctly, maintains all existing security boundaries, avoids resource overhead, and includes a precise, well-structured test suite.


Detailed Findings

1. Correctness & Mute Gating (Verified)

  • The Problem: The World Model refresh logic previously sat below if (this.proactivePaused()) return; in runAnticipationTick(). Since proactivePaused() returns true when the bridge is muted (this.muted = true), the world model would never update the owner's profile while the channel was muted.
  • The Solution: The patch correctly lifts maybeRefreshWorldModel above the proactivePaused() check.
  • The Notification Safety: If the world model derives new facts, it notifies the owner via this.send(target, formatWorldModelFyi(applied.changes)). This notification is safe and will not be blocked during a channel mute because the send boundary explicitly exempts owner targets (this.isOwnerTarget(to)).
  • Respecting Quiet Hours & Snoozes: The patch correctly retains quiet hours and proactive snoozes for the world model by manually checking isQuietHours(new Date(), defaultQuietHours()) || now < this.proactiveMuteUntil. This ensures the owner is not disturbed during overnight quiet hours or temporary snooze windows.
  • The Master Killswitch: The master killSwitch check remains at the absolute top of runAnticipationTick(), ensuring that if the owner engages the master killswitch, even the background world model stops completely ("do nothing at all").

2. Security & Privacy Boundaries (Verified)

  • All operations of the world model (reading local Mail index, Calendar, local Notes, and local WhatsApp history) are executed fully locally on the owner's machine.
  • The updated state is persisted to ~/.lantern/owner-profile.md with mode 0600 (owner-only read/write).
  • Moving this above the pause gate introduces zero data leakage risks because only the owner's local profile is updated, and the only outgoing notification is routed exclusively to the owner's self-chat.

3. Performance & Resource Efficiency (Verified)

  • maybeRefreshWorldModel has an internal throttle that limits its execution to once every LANTERN_WORLD_MODEL_HOURS (defaulting to 6 hours):
    if (now - this.worldModelLastRunAt < hours * 3_600_000) return;
  • Consequently, lifting it above the pause gate inside the 45-minute anticipation tick introduces no extra CPU, disk, or LLM overhead.

4. Test Quality & Coverage (Verified)

  • The added test file (world-model-mute-scope.test.ts) leverages vitest correctly (which matches @lantern/imessage-bridge's test suite configuration).
  • The tests perform highly precise static analysis on session.ts's actual AST/code text to guarantee that:
    1. maybeRefreshWorldModel is called above the proactivePaused return.
    2. The quiet hours check is strictly integrated.
    3. The killswitch is honored above the world model call.
  • This ensures future changes will not silently regress this critical design layout.

@dshakes
dshakes merged commit 66a408a into master Sep 19, 2026
15 of 16 checks passed
@dshakes
dshakes deleted the fix/world-model-runs-when-muted branch September 19, 2026 14:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent:reviewed-clean Reviewer found no Blocking issues this round domain:core Core library / business logic

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant