Omniscio documentation
Browse all documentation
  1. Getting Started13
  2. Sessions & Agents115
  3. Inbox & Notifications59
  4. Projects & Tasks95
  5. Automation & Scheduling75
  6. Knowledge & Memory26
  7. AI Features60
  8. Integrations100
  9. Plugins & Marketplace33
  10. Cloud & Teams56
  11. Settings & Customization58
  12. Account & Billing28
  13. Troubleshooting84
  14. CLI & API Reference22
  15. Legal & Policies4
  16. Uncategorised22

Weekly Summary (Monday-morning recap, folded into Daily Digest) (part 2)

Part 2 of the Weekly Summary page: the feature recommendation rules, the Build it now buttons a suggestion can carry, the first-launch backfill and its past-due gate, the nominal AI usage figure, the phrase exclusion, the commit tally, the cost cap, the Workflow Coach retirement and the stored tables and settings keys.

What it is

This is part 2 of the Weekly Summary (Monday-morning recap, folded into Daily Digest) page. It covers what the recap is allowed to recommend, the buttons a suggestion can carry, the first-launch backfill and its freshness gate, the nominal AI usage figure, the phrase exclusions behind the quick-reply suggestion, the deterministic commit tally, the cost cap, the Workflow Coach retirement, and the stored tables, settings keys and renderer surfaces behind all of it.

Where to find it

The same surface as the full page: the Briefings entry in the Omniscio sidebar, the two Briefings rows in the Inbox, and Settings → Email & Summaries → Daily digest → Weekly Summary for the master toggle, the generation hour, the cost cap and Run now.

How it behaves

How it works

Feature recommendations (2026-08-31)

Reduce may emit at most ONE suggestion per week that recommends an Omniscio feature the user is not yet using — "you run a lot of sessions and have never used Auto-Run (Away Mode)". It closes a gap that had been open since the detectors shipped: featureGaps / featureUsage could already tell the model WHICH features were untouched, but both are keyed on the telemetry FEATURE_REGISTRY, whose description fields describe what is counted ("counts each session created, launched, renamed…"), not what the feature is for. The model could see a gap and had nothing to pitch into it.

  • The menu is featureCatalog on the envelope: Array<{ title, pitch, goals }> produced by buildWeeklyFeatureCatalog(), a pure projection of the generated RECOMMENDATION_INDEX — the same artifact Ask Omniscio reads for "what should I use for X?" (see ask-amc.md). Written once, surfaced in both places; there is deliberately no second hand-authored list. Entries with an empty title or pitch are dropped — an entry with no pitch can only produce an invented one. goals is comma-joined, not an array: the envelope is serialized with 2-space indentation, so 160 arrays of one or two short strings spend ~1.4 KB on brackets and newlines for a shape the model reads identically either way.
  • The rules, all pinned in REDUCE_SYSTEM_PROMPT and each locked by a named test in weekly-summary-prompt-feature-recommendation.test.ts: at most one per week and only when the week's evidence genuinely fits (this is a recap, not a feature tour); recommend ONLY a title present in featureCatalog (anything absent may be unreleased or unavailable to this user); cite a concrete usage signal proving non-use, and emit nothing without one; never recommend the weekly summary, the suggestions card, or the daily digest; write the pitch in the same language as summaryMd; put both the catalog entry and the usage signal in groundednessCheck.signalsUsed.
  • Why the cite-or-abstain rule is load-bearing: most of the 160 catalog features have no telemetry counterpart at all, so absence from featureUsage is silence, not evidence. Without that rule the model would confidently pitch a feature the user runs daily. The prompt says so explicitly.
  • No action kind. Every indexed feature currently has an empty docLink (0 carry publicHelpPage, 0 carry llmDocsPage), so there is nothing for a button to open. The universal Start session button already on every SuggestionCard is the call to action.
  • Fail-safe. buildFeatureCatalog() wraps the projection in try/catch returning [], mirroring buildPriorDismissals() beside it: a catalog that cannot be built costs the recommendation, never the week's summary. The prompt handles the empty case by emitting no feature suggestion.
  • Budget. The serialized block is ~31 KB / ~7.8k tokens, paid on all three phases: ≈ $0.042 per weekly run (Map $0.0016 + Reduce $0.0388 + Verify $0.0016), ≈ $2.18/user/year. Pinned by a test — re-adding a heavy field (triggerText alone is most of the 184 KB index) fails loudly instead of silently multiplying the weekly prompt cost.
  • weekly-feature-catalog.ts is MAIN-PROCESS ONLY. It statically imports the 184 KB generated index; a renderer import would drag that into a bundle with a real Brotli entry budget. Guarded by weekly-feature-catalog-main-only.test.ts.

Build-it-now actions

A suggestion can carry an optional action: { kind, payload } — when present the renderer shows a primary button whose label and icon match the action (resolved by resolveSuggestionActionPresentation(); an unknown kind falls back to a neutral Take action). Clicking it routes to a renderer-side primitive via dispatchSuggestionAction(). The valid kind values are pinned in the Reduce system prompt (anything else toasts a warning and no-ops). save-quick-reply was retired 2026-07-06 — repeated phrases now get their own standalone inbox nudge, so the digest no longer emits it (the dispatcher still handles it, labeled Add quick reply, for any already-stored summaries):

action.kind Button label What clicking does
create-automation Create automation navigateToSettingsProject({ section: 'automations', automationConfig }) so the Automation Builder opens pre-filled. Accepts either { config: {...} } (nested sub-object) or the whole payload as the config (fallback).
open-keybindings Change shortcut Sets pendingSettingId = keybinding-<actionId> (actionId comes from the hotkeyAdoption detector — any bound shortcut the user clicks past, ranked by time lost), then navigateToSettingsProject({ section: 'keybindings' }). The panel selects the action's group and useScrollToSetting scrolls to + flashes the exact data-setting-id="keybinding-<actionId>" row — the button lands on the specific shortcut, not just the section.
archive-idle-sessions Review & archive The model sends { projectName } — NEVER session ids (they never reach the LLM). So the dispatcher resolves the currently-idle sessions itself from a fresh, complete SESSION_LIST fetch (the renderer store omits ready/terminating idle sessions at boot, so filtering it would under-count), scoped to the named project (or all idle when projectName is omitted) via the shared idle predicate (isSessionIdle). Then useConfirmDialog (warning variant) shows the TRUE current count, mergeSessions makes any non-resident rows resident, and bulkArchiveSessions(ids, 'user-context-menu') archives (terminate-before-archive + undo). Explicit sessionIds still win (back-compat).
explore-feature Take a look Resolves a Settings section from section (used as-is) else featureId (cron* → cronjobs; ids that ARE section ids like mempalace/contextdock pass through; unknown → null = Settings home). If settingId is in the payload, sets pendingSettingId so the panel highlights the target on open. Then navigateToSettingsProject({ section }).

Hard rules baked into the dispatcher:

  • payload is coerced to {} if null or non-object — a malformed Reduce output can't crash the click.
  • archive-idle-sessions with no explicit sessionIds resolves the idle set from projectName (fresh SESSION_LIST); an unfound project toasts a 'warning', and a genuinely-empty idle set toasts a neutral 'info' — never the old always-fires "No idle sessions to archive."
  • An unknown kind toasts a neutral 'This action isn't available.' and console.errors the raw kind (humanization — the discriminant never reaches the toast).
  • The Reduce prompt pins the EXACT action.payload shape per kind (archive-idle-sessions → { projectName? }, explore-feature → { section }, open-keybindings → { settingId }, create-automation → { config }) so the producer and this consumer can't drift. That drift — the model emitting keys the dispatcher never read (projectName/featureId) — is exactly what left these buttons dead until 2026-07-13; see postmortem.
  • The action payload never contains raw user text (the system prompt's "action.payload" stays small (a few keys) rule is reinforced by the privacy canary on the envelope side).

The dispatcher takes deps: { toast, confirm } so it stays a pure module — SuggestionCard.tsx injects useToast() + useConfirmDialog().confirm at the call site.

First-launch backfill + the freshness gate (2026-08-03)

backfillRecentWeeks() is a private method called once from start()'s IIFE. The first time the service boots after upgrade (or after a fresh install), weeklySummaryBackfillCompleted is false. It builds the 4 most-recently-completed weeks, then a freshness gate filters them, so on any real gap it generates at most the single most-recent completed week — never a backlog:

  1. Returns early if weeklySummaryBackfillCompleted is already true.
  2. Builds the 4 most-recent completed weeks (the just-completed week + the 3 before it), oldest-first for chronological insert order.
  3. Freshness filter (isWeekReportPastDue). A weekly recap is "due" the Monday after its week ends and becomes past-due once a full grace-week (WEEKLY_REPORT_PAST_DUE_GRACE_DAYS = 7) has elapsed past that delivery Monday — i.e. once the next week's recap has itself come due. Past-due weeks are dropped up front, so only the most-recent completed week survives (the older 3 are always past-due). This is the owner rule (2026-08-03) that stops a stranded/returning user getting a burst of weeks-old recaps. GRACE = 7 is a documented floor — the current week's own margin shrinks to ~1 day on a Sunday, so a smaller grace would start suppressing the current week. Local-time, DST-safe math in /src/main/services/weekly-summary-week-bounds.ts.
  4. Pre-flights the daily cap before each surviving week (getTodayCostByLabel('weekly-summary') >= cap); generate() re-checks it too. A past-due skip is NOT a cap-halt — it never sets capHalted.
  5. In finally, flips weeklySummaryBackfillCompleted: true unless halted by a real cap break — a transient API error still completes it (so it doesn't re-loop), a genuine cap break leaves it unset to retry next boot (F069), and a fully-past-due backfill (nothing to generate) still completes. Internal-only, not in the renderer Zod schema.
  6. Skips weeks that already have a row (via hasWeeklySummaryForWeek) — re-runs after a settings change are safe.

catchUp() is gated the same way. It builds a window of candidate weeks and applies isWeekReportPastDue, so after a >1-week outage it fills only the most-recent completed week rather than every missed week — this intentionally supersedes the old multi-week catch-up (F117). The two automatic paths (catch-up + backfill) are the only ones gated; periodicCheck() (the current week — never past-due) and the manual Run now (generate()) are never gated, so a user can always regenerate any week on demand. Guard tests: /tests/unit/services/weekly-summary-week-bounds.test.ts (the past-due math incl. the Sunday floor sweep) and the backfill/catch-up suites in /tests/unit/services/weekly-summary-service.test.ts.

AI usage is a nominal, pay-as-you-go value (2026-08-03)

The week's AI figure (stats.aiCostUsd = SUM(sessions.cost_usd)) is metered at standard API list prices regardless of the account's plan — so for a user on a Claude subscription (e.g. Max) it is the pay-as-you-go equivalent, NOT what they actually paid (their real out-of-pocket is typically far lower). Two surfaces keep that honest:

  • The stat tile is labeled "AI usage" (was "AI cost") with an info tooltip: "Pay-as-you-go value: what this week's AI usage would cost at standard pay-as-you-go API rates. On a Claude subscription (like Max), your actual cost is usually far lower." — StatsCard.tsx, i18n key statsCard.aiUsageTooltip.
  • The REDUCE system prompt instructs the recap writer to frame the figure as pay-as-you-go usage value ("$X in AI usage at pay-as-you-go rates"), never "you spent $X". The tile tooltip is the hard guarantee; the LLM framing is best-effort. Guard: /tests/unit/services/weekly-summary-prompt-nominal-spend.test.ts.

Button-phrase exclusion in the "save as a quick reply" suggestion

runAllDetectors() accepts an optional excludePhrases: string[] on every call. The Weekly Summary passes [settings.quickReplyText, NUDGE_PROMPT, ...every saved quick reply's text] so that Detector A (detectRepeatedOutgoingText) and Detector K (detectTopOperatorPhrases) never flag a phrase the user has ALREADY saved as a one-click button — the configured zap "proceed" phrase, the "Please continue" nudge (from src/shared/nudge-prompt.ts), AND every row in the quick_replies table. The saved texts come from the resilient getSavedQuickReplyTexts() helper (listQuickReplies() wrapped in try/catch → [] on a failed read, mirroring buildPriorDismissals — a broken quick-reply read costs only the exclusion, never the whole summary). Exclusion works on the bucket-insensitive signatureWords() form (the first 8 normalized words, no length bucket), so punctuation/casing variants match (a user-typed "Please continue." is caught by the canonical "Please continue") AND a saved reply whose text is the sent phrase PLUS a trailing sentence still matches — the real "Git Prep" reply normalizes to the long bucket while the bare sentence the user sends is medium, so the old full-signaturePhrase() match missed it and re-suggested it weekly (fixed 2026-06-22). Divider/folder rows carry empty text and are dropped by each detector's own .filter(p => p.trim().length > 0). It is an additional skip on top of the existing is_auto_response = 0 filter — it does not relax that filter. (Known limit: a phrase buried in the MIDDLE/END of a longer saved reply, or a prepend/append-composed send whose leading words differ, can still surface — out of scope.) Contract invariant button-bound-phrases-excluded-on-request: workflow-coach-phrase-detection-contract.md.

Deterministic commit tally

WeeklySummaryStats.commits ({ total, projectCount }, optional) is a deterministic count gathered before any LLM call — never model-generated. gatherWeekCommits(db, bounds) (calling gatherCommitActivity() in src/main/services/commit-activity.ts) counts authored commits across all local branches, not just HEAD, because Omniscio's worktree-per-session model lands commits on per-session branches the main checkout can't reach. Since 2026-10-01 the read is git rev-list --branches --since --until --min-parents=1 --max-parents=1 --format=… rather than a bare --count: a commit counts only if a person authored it, it has exactly one parent (so no merge and no parentless root), and it changed something. Emptiness is decided by comparing tree oids — --numstat would have to diff blob content and measured ~297 s for a week against ~0.9 s for the tree compare. The parent filter and the "a machine wrote this" test are the shared declarations in scripts/lib/git-authored-history.mjs, the SAME ones the Weekly/Daily Contributor Report uses, so the two surfaces cannot disagree about those two questions. They are not the same number, and should not be read as one: the report additionally keeps only commits that touch code or tests, collapses rebased copies, and resolves its roster before the machine filter. This is the plain count. On this repo for 2026-09-23→09-30 the old tally read 31,746 and this one reads 25,654 — 6,092 commits (19.2%) that were never anybody's work. The query scopes to every normal project that had ≥1 session in the Mon→Sun window; virtual-project sentinels (__claude__, __nothari__, __plugin_*__, …) are excluded in SQL via NOT GLOB '__*'. Rendered in StatsCard as "N commits across M projects this week" (hidden when zero).

Cost cap

The default cap is $0.10/day. The cap is the cumulative spend for the 'weekly-summary' source-label on the calendar day, queried via getTodayCostByLabel('weekly-summary') against api_cost_log. Backfill, scheduled runs, and Run now all gate against the same number — so a backfill that drains the cap will also block the manual button until the next day. The cap can be raised or lowered in Settings (range $0.05 – $5.00).

Subsume Workflow Coach — migration details

The retirement of the Workflow Coach feature happened on 2026-05-14 in the same PR as Weekly Summary's first ship:

  • The Suggestions sidebar entry, its SuggestionsView / SuggestionRow / suggestions-store modules, and the standalone weekly inbox alert were deleted.
  • The six detectors in /src/main/services/workflow-coach/detectors.ts live on — Weekly Summary calls runAllDetectors() as a sub-step of every generation.
  • The privacy canary test at /tests/unit/services/workflow-coach-privacy.test.ts is unchanged and continues to pin the no-raw-text contract.
  • Settings → Experimental → Workflow Coach is gone. The replacement panel is Settings → Email & Summaries → Daily digest → Weekly Summary.
  • Existing users get a one-time soft migration on first launch after the upgrade: softMigrateFromWorkflowCoachOnce() reads the old workflowCoachEnabled / workflowCoachDailyCostCapUsd keys and copies them onto the new ones, then sets weeklyCoachMigrationDone: true. If you had Workflow Coach on, you get Weekly Summary on with your old cap.
  • The legacy __suggestions__ virtual-project row is soft-deleted by migration v172 — it stays in projects with is_deleted = 1 so a user who had pinned or customized it has a recoverable record, but it stops rendering.

For agents

Privacy contract (carried over from Workflow Coach)

The same canary test (/tests/unit/services/workflow-coach-privacy.test.ts) that pinned Workflow Coach's privacy guarantee continues to pin the detector output. Raw operator text never enters AggregatedSignals — only hashes from workflow-coach/hash.ts, counts, and a single PII-scrubbed sampleText ≤200 chars per detector. The Map-phase prompt is built from RawStats (factual aggregates) + AggregatedSignals (hashes/counts) — nothing else. If the canary string appears in the prompt, the test fails CI.

Storage

Migration v171 creates weekly_summaries (extended by v195 with model_chain_json):

id TEXT PRIMARY KEY
week_start_iso TEXT NOT NULL UNIQUE  -- 'YYYY-MM-DD' local Monday
week_end_iso TEXT NOT NULL           -- ISO datetime, local Sunday 23:59:59.999
generated_at TEXT NOT NULL
stats_json TEXT NOT NULL             -- JSON WeeklySummaryStats
summary_md TEXT NOT NULL
suggestions_json TEXT NOT NULL       -- JSON Array<WeeklySummarySuggestion>
detector_signals_json TEXT NOT NULL  -- JSON AggregatedSignals (debugging only)
input_tokens INTEGER NOT NULL DEFAULT 0
output_tokens INTEGER NOT NULL DEFAULT 0
cost_usd REAL NOT NULL DEFAULT 0
model_chain_json TEXT NOT NULL DEFAULT '[]'  -- v195: JSON ModelChainEntry[]
is_read INTEGER NOT NULL DEFAULT 0
is_archived INTEGER NOT NULL DEFAULT 0
snoozed_until TEXT NULL              -- ISO datetime, null = not snoozed

Migration v208 creates weekly_summary_dismissals:

id TEXT PRIMARY KEY
weekly_summary_id TEXT NOT NULL      -- FK → weekly_summaries(id) ON DELETE CASCADE
suggestion_index INTEGER NOT NULL    -- 0-based index into the summary's suggestions[]
suggestion_title TEXT NOT NULL       -- snapshot at dismissal time
suggestion_body TEXT NOT NULL        -- snapshot at dismissal time
reason TEXT NOT NULL                 -- 'already-doing' | 'not-useful' | 'not-relevant' | 'wrong' | 'other'
dismissed_at TEXT NOT NULL
UNIQUE(weekly_summary_id, suggestion_index)  -- one dismissal per slot, last-write-wins via upsert

Indexes: idx_wsd_dismissed_at (drives the 90-day envelope read), idx_wsd_weekly_summary_id (drives the per-summary dismissals slice population on GET).

Migration v172 soft-deletes the legacy __suggestions__ virtual-project row that Workflow Coach used to seed; the row stays in projects (because is_deleted = 1) for any user who had pinned/customized it, but it no longer renders in the sidebar.

Migration v179 (2026-05-14) soft-deletes the __weekly_summary__ virtual-project row when the merge into Daily Digest landed. Same pattern — the row stays in projects with is_deleted = 1 so prior customizations are recoverable, but the standalone sidebar entry stops rendering.

Queries: /src/main/db/queries-weekly-summary.ts — insertWeeklySummary, getWeeklySummary, listWeeklySummaries, markRead, archive, snooze, plus the four dismissal queries upsertWeeklySummaryDismissal, deleteWeeklySummaryDismissal, listDismissalsForSummary, getRecentWeeklySummaryDismissals(daysBack, limit = 100). Tests at /tests/unit/db/queries/queries-weekly-summary-dismissals.test.ts pin upsert idempotency, FK cascade, 90-day cutoff math, default limit of 100, modelChain JSON round-trip, and the GET-time dismissals slice population.

IPC: /src/main/ipc/weekly-summary-handlers.ts — list, get, mark-read, archive, snooze, generate-now, unread-count, dismiss-suggestion, undismiss-suggestion. Push events: WEEKLY_SUMMARY_GENERATED, WEEKLY_SUMMARY_UPDATED (also fires after dismiss/undismiss so other open viewers refresh), WEEKLY_SUMMARY_UNREAD_CHANGED.

Renderer surface

  • Settings: /src/renderer/src/features/settings/WeeklySummarySettings.tsx — rendered at the bottom of DailyDigestSettings.tsx (Settings → Email & Summaries → Daily digest).
  • Briefing card + viewer: weekly summaries reuse the same row + viewer components as daily digests inside the Briefings sidebar — see daily-digest.md for the canonical surface.
  • Store: /src/renderer/src/stores/weekly-summary-store.ts with the recap inbox slice in /src/renderer/src/stores/weekly-summary-inbox-items.ts. The activeInboxSummary slot is one of the inbox mutex slices — picking a Weekly Summary clears the others, and every bulk action's snapshot/fallback/rollback restores activeInboxSummary along with the rest.
  • Two inbox cards (2026-07-20): the recap stays on the reserved weekly-summary integration above; the AI's suggestions surface as a SEPARATE derived weekly-suggestions source (/src/renderer/src/stores/weekly-suggestions-inbox-items.ts) that reads the SAME store, surfaces the LATEST week only (newest by generatedAt — never walks back through older summaries; the 2026-07-20 backlog fix), opens a dedicated in-pane detail (WeeklySuggestionsViewer.tsx) via the generic activeInboxDetail slice, and gates/labels/self-clears on the LIST-only openSuggestionCount (total − dismissals, computed in queries-weekly-summary.ts). The recap's inbox pane hides suggestions (hideSuggestions on WeeklySummaryViewer); the dedicated sidebar viewer keeps the full recap + suggestions, both via the shared WeeklySuggestionsContent.tsx. The recap inbox card also offers the shared Start session button (components/inbox/StartSessionDialog.tsx, source weekly-summary-start-session), gated by showStartSession so it appears on the inbox recap ONLY — the sidebar review viewer omits it, and the suggestions card carries its own per-suggestion Start session. Invariants: weekly-suggestions-card-contract.md.

Settings deep-link compatibility

'weekly-summary' stays in the SettingsSection union and in VALID_SECTIONS so old URLs (e.g. ?section=weekly-summary) still type-check. The LEGACY_FEATURE_SECTIONS map in Settings-types.ts single-hops 'weekly-summary' to 'email' (the Email & Summaries panel that hosts the merged Daily Digest card), so the deep link lands on the right panel automatically. The search index in settings-search-index.ts lists each weekly-summary-* entry with section: 'daily-digest', so search hits (e.g. typing "weekly", "monday", "haiku", "briefing") scroll the user to the Weekly Summary sub-section inside the Daily Digest panel.

Related

This is part 2 of the Weekly Summary (Monday-morning recap, folded into Daily Digest) page — the overview, the day-to-day usage and the first half of the behaviour live there. Its closest sibling page is Daily Digest, which describes the shared Briefings surface both cadences use.

Last verified 2026-10-01