feat(home-feed): replace reflection producer with activity-log roll-up [JARVIS-512]#25594
Conversation
…JARVIS-512]
First batch of background-job instrumentation for the home activity
log. Wires emitFeedEvent into the safe, bounded completion points
where a real user-visible signal lands:
scheduler.ts — 4 emit points:
- notify-mode one-shot success (dedupKey: oneshot:<jobId>)
- notify-mode recurring success (dedupKey: schedule-run:<runId>)
- execute-mode task success (dedupKey: schedule-run:<runId>)
- execute-mode message success (dedupKey: schedule-run:<runId>)
Execute-mode emits are gated on !job.quiet to match the existing
notifySchedule behavior; notify-mode emits always fire since
notify-mode IS a notification by design.
sequence/engine.ts — emit after recordSend on each successful step
(dedupKey: sequence-step:<enrollmentId>:<stepIndex>). Skipped for
requireApproval draft steps — those pause without sending, so
there's no real signal yet.
All emits use source="assistant", are fire-and-forget via .catch so
a writer hiccup can never interrupt the 15s scheduler tick, and key
on the schedule-run / enrollment-step identifier so each run is a
distinct activity-log entry (the writer's per-source cap bounds
total volume).
Deferred to follow-up:
- Gmail watcher event processing (loop-risk if the LLM-processing
path re-triggers work)
- task-runner direct (manual non-schedule task runs) — needs a
dedup story with the scheduler layer to avoid double-emits
- skill runner completion — no single well-defined hook to latch
onto yet
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…p [JARVIS-512]
Pivot the LLM-driven producer from "reflect on relationship state from
nothing" → "consolidate recent action items into digests/threads."
Same skeleton, same cadence mechanism (30 min), new input and new
intent.
Rename reflection-producer.ts → rollup-producer.ts (git rename
tracked so blame history is preserved). The new producer:
- Reads recent `action` items from readHomeFeed() via a new
defaultLoadRecentActions helper (sorted newest-first, capped at
MAX_ACTIONS_IN_PROMPT = 30 for token budget). Non-action items
are excluded — the roll-up's input is the raw activity log, not
existing consolidations.
- Still loads relationship state for context, but only as
secondary framing (explicitly instructed in the prompt NOT to
invent roll-ups from facts alone).
- Narrows the write_feed_items tool schema to type: digest | thread
only. No nudges, no actions — actions come from background jobs,
not the LLM.
- New `no_actions` skip reason: when the activity log is empty,
the producer short-circuits before the provider call. The
scheduler treats this like no_provider — does NOT advance the
cooldown gate, so the next tick retries as soon as new actions
land instead of waiting the full 30-minute window. Real LLM
attempts (success/empty_items/malformed_output/provider_error)
still advance the gate to preserve backoff.
feed-scheduler.ts renamed symbols to match:
reflectionRunner → rollupRunner
reflectionRan → rollupRan
REFLECTION_INTERVAL_MS → ROLLUP_INTERVAL_MS
lastReflectionAt → lastRollupAt
Cadence stays at 30 min in this PR. The drop to 120 min + on-visit
refresh in home-feed-routes.ts is the next step in JARVIS-512.
Tests migrated from reflection-producer.test.ts → rollup-producer.
test.ts with all existing coverage plus new cases:
- no_actions short-circuits before the provider call
- action items are serialized into the user prompt
- nudge/action types are rejected at coercion time even if the
model bypasses the tool schema
feed-scheduler.test.ts adds a no_actions cooldown test alongside
the existing no_provider test.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| * small set of digests or threads." This is the replacement for the | ||
| * old "reflect from nothing" producer: the roll-up starts from real | ||
| * side effects instead of prompting the model to hallucinate signal | ||
| * from relationship state alone. |
There was a problem hiding this comment.
🟡 AGENTS.md violation: new comment references removed code
Lines 7–10 of the new module comment explicitly reference the removed reflection producer: "This is the replacement for the old 'reflect from nothing' producer". This violates the mandatory rule in assistant/AGENTS.md:19-21: "When writing or updating comments, do not reference code that has been removed. Comments should describe the current state of the codebase, not narrate its history." The comment should describe what the rollup producer does without referencing the old implementation.
| * small set of digests or threads." This is the replacement for the | |
| * old "reflect from nothing" producer: the roll-up starts from real | |
| * side effects instead of prompting the model to hallucinate signal | |
| * from relationship state alone. | |
| * small set of digests or threads." The roll-up starts from real | |
| * side effects (action items deposited by background jobs) rather | |
| * than asking the model to invent signal from relationship state | |
| * alone. |
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5f65fbea98
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| "Seconds the user must be away before this item appears. Use 0 for a roll-up the user should see immediately.", | ||
| }, | ||
| }, | ||
| required: ["type", "title", "summary"], |
There was a problem hiding this comment.
Require
source in roll-up items
The roll-up path depends on source-scoped replacement for digests, but source is optional here (required omits it), so model output without source is accepted and written. In mergeIncoming (feed writer), digest replacement only happens when incoming.source is present, so source-less digests/threads will accumulate as new rows on repeated runs instead of collapsing, which degrades feed quality and can grow the file unexpectedly.
Useful? React with 👍 / 👎.
| return feed.items | ||
| .filter((i) => i.type === "action") | ||
| .sort((a, b) => { |
There was a problem hiding this comment.
Restrict roll-up input to fresh action events
This loader includes every persisted action item and only sorts by createdAt; it does not filter by freshness/status. Because newly-added schedule/sequence emits create non-expiring actions by default, actions.length will remain non-zero after initial activity, so the new no_actions fast-path almost never triggers and the model keeps reprocessing stale history on each cooldown cycle. That increases token spend and tends to regenerate repetitive summaries rather than reacting to truly new activity.
Useful? React with 👍 / 👎.
…p [JARVIS-512] (#25594) * feat(home-feed): instrument scheduler + sequence with emitFeedEvent [JARVIS-512] First batch of background-job instrumentation for the home activity log. Wires emitFeedEvent into the safe, bounded completion points where a real user-visible signal lands: scheduler.ts — 4 emit points: - notify-mode one-shot success (dedupKey: oneshot:<jobId>) - notify-mode recurring success (dedupKey: schedule-run:<runId>) - execute-mode task success (dedupKey: schedule-run:<runId>) - execute-mode message success (dedupKey: schedule-run:<runId>) Execute-mode emits are gated on !job.quiet to match the existing notifySchedule behavior; notify-mode emits always fire since notify-mode IS a notification by design. sequence/engine.ts — emit after recordSend on each successful step (dedupKey: sequence-step:<enrollmentId>:<stepIndex>). Skipped for requireApproval draft steps — those pause without sending, so there's no real signal yet. All emits use source="assistant", are fire-and-forget via .catch so a writer hiccup can never interrupt the 15s scheduler tick, and key on the schedule-run / enrollment-step identifier so each run is a distinct activity-log entry (the writer's per-source cap bounds total volume). Deferred to follow-up: - Gmail watcher event processing (loop-risk if the LLM-processing path re-triggers work) - task-runner direct (manual non-schedule task runs) — needs a dedup story with the scheduler layer to avoid double-emits - skill runner completion — no single well-defined hook to latch onto yet Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(home-feed): replace reflection producer with activity-log roll-up [JARVIS-512] Pivot the LLM-driven producer from "reflect on relationship state from nothing" → "consolidate recent action items into digests/threads." Same skeleton, same cadence mechanism (30 min), new input and new intent. Rename reflection-producer.ts → rollup-producer.ts (git rename tracked so blame history is preserved). The new producer: - Reads recent `action` items from readHomeFeed() via a new defaultLoadRecentActions helper (sorted newest-first, capped at MAX_ACTIONS_IN_PROMPT = 30 for token budget). Non-action items are excluded — the roll-up's input is the raw activity log, not existing consolidations. - Still loads relationship state for context, but only as secondary framing (explicitly instructed in the prompt NOT to invent roll-ups from facts alone). - Narrows the write_feed_items tool schema to type: digest | thread only. No nudges, no actions — actions come from background jobs, not the LLM. - New `no_actions` skip reason: when the activity log is empty, the producer short-circuits before the provider call. The scheduler treats this like no_provider — does NOT advance the cooldown gate, so the next tick retries as soon as new actions land instead of waiting the full 30-minute window. Real LLM attempts (success/empty_items/malformed_output/provider_error) still advance the gate to preserve backoff. feed-scheduler.ts renamed symbols to match: reflectionRunner → rollupRunner reflectionRan → rollupRan REFLECTION_INTERVAL_MS → ROLLUP_INTERVAL_MS lastReflectionAt → lastRollupAt Cadence stays at 30 min in this PR. The drop to 120 min + on-visit refresh in home-feed-routes.ts is the next step in JARVIS-512. Tests migrated from reflection-producer.test.ts → rollup-producer. test.ts with all existing coverage plus new cases: - no_actions short-circuits before the provider call - action items are serialized into the user prompt - nudge/action types are rejected at coercion time even if the model bypasses the tool schema feed-scheduler.test.ts adds a no_actions cooldown test alongside the existing no_provider test. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Fourth step of JARVIS-512, stacked on #25593 (instrumentation, open). Pivots the LLM-driven producer from "reflect on relationship state from nothing" → "consolidate recent action items into digests/threads." Same skeleton, same cadence mechanism, new input and new intent.
What changes
Rename:
reflection-producer.ts→rollup-producer.ts(git rename tracked so blame history is preserved). The new producer:readHomeFeed()via a newdefaultLoadRecentActionshelper — sorted newest-first, capped atMAX_ACTIONS_IN_PROMPT = 30for token budget. Non-action items are excluded (the input is the raw activity log, not existing consolidations).type: digest | threadonly. No nudges, no actions. Actions come from background jobs; nudges aren't this producer's concern.no_actionsskip reason for when the activity log is empty. The producer short-circuits before the provider call.Scheduler update (
feed-scheduler.ts):reflectionRunner/reflectionRan/REFLECTION_INTERVAL_MS/lastReflectionAt→rollup*.no_actionstreated likeno_provider: does NOT advance the cooldown gate, so the next tick retries as soon as new actions land instead of waiting the full 30-minute window. Real LLM attempts (success/empty_items/malformed_output/provider_error) still advance the gate to preserve backoff.Cadence stays at 30 min in this PR. The drop to 120 min + on-visit refresh in
home-feed-routes.tsis the next (and final) step in JARVIS-512.Loop / cost audit
no_actionsshort-circuit means an empty feed produces zero LLM calls even on every tick (the producer bails beforeprovider.sendMessage).readHomeFeed()is the only new data dependency; it's a synchronous file read that the writer's merge loop already uses, so no new I/O cost.Testing
bun run typecheck— cleanbun run lint— cleanbun test src/home/— 147/147 passdigest/threadfixture valuesno_actionsshort-circuit, action-item serialization into the prompt, nudge/action types rejected at coercion,no_actionscooldown behavior in the schedulerRemaining JARVIS-512 scope
home-feed-routes.ts(debounced fire-and-forget fromhandleGetHomeFeed)🤖 Generated with Claude Code