AW-04 — Live Feed · Task breakdown
Ordered work derived from plan.md, behaviour from spec.md. Every task carries explicit paths and a definition of "done". Execute top to bottom.
Epic ID: AW-04-live-feed
Spec: ./spec.md · Plan: ./plan.md
Status: Draft
Last updated: 2026-09-06
How to use
- Tasks are sequential unless marked
(parallel), which means they may land alongside the task immediately above them. P1/P2/P3mark the phase. Each phase is independently shippable and must leavedevelopgreen on its own.- Every schema change ships its migration in the same PR (Constitution V). The migration task is not a follow-up.
- Formatting: tabs, width 4, 120 columns, single quotes, semicolons, no trailing commas (root Prettier config wins). Files are kebab-case; React components are PascalCase files.
- Run
pnpm format && pnpm lint && pnpm type-checkbefore every PR.
Phase P1 — the feed, readable and paginated
Contracts and data model
-
T1 · P1. Add the feed contract types.
- Create
packages/contracts/src/api/feed/feed-kind.ts—export type FeedKind = 'work' | 'decision' | 'delivery' | 'problem' | 'system';plusexport const FEED_KINDS: readonly FeedKind[]. - Create
packages/contracts/src/api/feed/feed.dto.tswithFeedActorDto,FeedNarrationDto,FeedEntryDto,FeedPageDto,FeedActorSummaryDto,FeedAwaySummaryDto(shapes in plan §3.4). - Create
packages/contracts/src/api/feed/index.ts; re-export frompackages/contracts/src/api/index.ts. - Done:
pnpm --filter @ever-works/contracts buildemits declarations with no conditional-spread DTS failure; the new types are importable as@ever-works/contracts.
- Create
-
T2 · P1. Add the actor columns to the activity record.
packages/agent/src/entities/activity-log.entity.ts— addactorKind?(varchar(16), nullable),actorAgentId?(uuid, nullable, no@ManyToOne— see the Tier-C cycle-avoidance comment already ontenantId),actorLabel?(varchar(120), nullable).- Add
@Index('idx_activity_log_user_actor_created', ['userId', 'actorAgentId', 'createdAt'])and@Index('idx_activity_log_user_created_id', ['userId', 'createdAt', 'id']). packages/agent/src/entities/activity-log.types.ts— add the same three optional fields toCreateActivityLogDto(line 321) and addactorAgentIds?: string[],kinds?: FeedKind[],failedOnly?: boolean,cursor?: { createdAt: Date; id: string }toActivityLogQueryOptions(line 335).- Done: all three are optional, every existing
ActivityLogService.log()call site compiles untouched,pnpm --filter @ever-works/agent type-checkis green.
-
T3 · P1. Ship the migration for T2, in the same PR.
- From
apps/api/:pnpm typeorm migration:generate -d typeorm.config.ts src/migrations/AddActivityLogFeedActor - Rename the emitted file to
apps/api/src/migrations/1791040000000-AddActivityLogFeedActor.ts(AW-04 slot 00, README §5 rule 10; re-stamp before merge if a newer migration has landed ondevelop). - Hand-edit both index statements to
CREATE INDEX CONCURRENTLY/DROP INDEX CONCURRENTLYand remove the migration's implicit transaction if the driver requires it. - Done:
up()contains onlyADD COLUMN … NULLand the twoCREATE INDEX; there is noDROP COLUMN, noNOT NULL, noUPDATE/backfill;down()reverses exactly those; running the migration against a seeded local DB completes without lockingactivity_log.
- From
Narration and classification
-
T4 · P1. Feed-kind mapper.
- Create
packages/agent/src/activity-log/feed-kind.tsexportingresolveFeedKind(actionType, status): FeedKindusing the rules in plan §2.5 — theproblemrule is evaluated first. - Test:
packages/agent/src/activity-log/feed-kind.spec.ts— every one of the 162ActivityActionTypemembers maps to exactly one kind (a table-driven test that fails if a new enum member is added without a mapping decision), and afailed/cancelledstatus overrides the cluster rule. - Done: no action type falls through to
undefined; spec FR-17/FR-18 covered.
- Create
-
T5 · P1. Narrator with 48 bespoke entries.
- Create
packages/agent/src/activity-log/feed-narration.tsexportingnarrate(activity): { key: string; params: Record<string, string | number> }and a private table keyed byActivityActionType. Each entry declares thedetails/metadatakeys it may read; reading any other key is impossible by construction. - Include a
sanitizeNarrationParam(value)helper: strip<and>, collapse newlines, truncate to 120 chars with…(mirrorssanitizeLabelinpackages/agent/src/notifications/notification.service.ts). - Unknown action type →
{ key: 'fallback', params: { actor, action } }whereactionis the humanised action type. - The 48 keys are listed in plan §8 — keep the file's order identical to that list so the two are diffable.
- Test:
packages/agent/src/activity-log/feed-narration.spec.ts— one case per entry; an unknown type returnsfallback; adetailsvalue containing<script>comes back stripped; a 200-char value comes back at 120 +…; adetailskey not on the entry's allow-list never appears inparams. - Done: spec FR-14/FR-15/FR-16 covered; the file exports no English string.
- Create
Read model
-
T6 · P1. Keyset reads on the repository.
packages/agent/src/database/repositories/activity-log.repository.ts— addfindFeedPage(options)(keysetWHERE userId = :uid AND (createdAt, id) < (:t, :i),ORDER BY createdAt DESC, id DESC,LIMIT :n + 1to derivehasMore),countSince(userId, scope, since),aggregateActors(userId, scope, since)andaggregateAwayCounts(userId, scope, since, cap).- Every method applies
userIdand the active organization scope; none accepts a caller-supplied user id. - Test:
packages/agent/src/database/repositories/activity-log.repository.feed.spec.ts— asserts the generated query shape for the keyset predicate, the agent filter, the kind filter and the scope filter. - Done: no
OFFSETanywhere in the new methods.
-
T7 · P1.
FeedService.- Create
packages/agent/src/activity-log/feed.service.tswithgetPage(userId, scope, filters),getActors(userId, scope, windowHours),encodeCursor/decodeCursor(base64url of{t, i}), the 5-rung actor-resolution ladder (plan §2.4) including the batched agent lookup for legacy rows, andresolveHref(activity)(plan §5.5) built fromROUTES-equivalent constants held server-side. - Register in
packages/agent/src/activity-log/activity-log.module.ts(providers + exports) and export frompackages/agent/src/activity-log/index.ts. - Test:
packages/agent/src/activity-log/feed.service.spec.ts— cursor round-trip; a malformed cursor throws the typed error; ordering stays stable when a row is inserted at the head between two page reads; all 5 actor rungs;hrefprecedence order; the 20-agent cap. - Done: spec FR-2/FR-12/FR-19/FR-46 covered.
- Create
API
-
T8 · P1. Request DTOs.
- Create
apps/api/src/activity-log/dto/feed-query.dto.ts(agentIds— transformed from csv,@ArrayMaxSize(20),@IsUUID('4', { each: true });kinds—@IsIn(FEED_KINDS, { each: true });failedOnly—@IsBooleanString();cursor—@IsString()@MaxLength(256);limit—@IsInt()@Min(1)@Max(50), default 30) andapps/api/src/activity-log/dto/feed-actors-query.dto.ts(windowHours1-720, default 168). - Done: over-limit
agentIdsproduces400 { error: 'too-many-agents', max: 20 }, not a silent truncation (spec FR-38).
- Create
-
T9 · P1.
FeedController— page and actors.- Create
apps/api/src/activity-log/feed.controller.ts,@Controller('api/feed'),@UseGuards(AuthSessionGuard),@ApiTags('Live Feed'),@ApiBearerAuth('JWT-auth'). GET /→FeedPageDto(@Throttle120/min);GET /actors→{ actors }(60/min).- Scope is read from
ScopeContextService— never from a query parameter. Follow the reasoning documented inapps/api/src/digest/digest.controller.ts. - Register the controller in
apps/api/src/activity-log/activity-log.module.ts(do not create a new module — one store, one module). - Add
@ApiOperation/@ApiResponseon both handlers. - Done:
GET /api/feedreturns 401 unauthenticated, 400 on a bad cursor, and never returns a row belonging to another user or another organization.
- Create
-
T10 · P1. Controller spec (P1 subset).
- Create
apps/api/src/activity-log/feed.controller.spec.tsmodelled on the existingapps/api/src/activity-log/activity-log.controller.spec.ts. - Cases: auth required; limit clamped to 50;
400 invalid-cursor;400 too-many-agents; scope taken fromScopeContextServiceand ignored when supplied in the query; an out-of-scope record id returns the same response as a non-existent one (spec FR-59). - Done:
cd apps/api && pnpm test -- feed.controllergreen.
- Create
Web
-
T11 · P1. Typed client and server actions.
- Create
apps/web/src/lib/api/feed.ts(feedAPI.page(),.actors()) usingserverFetchfrom./server-api, attachingX-Scope-Slug— mirrorapps/web/src/lib/api/activity-log.ts. - Create
apps/web/src/app/actions/feed.tswith auth-guardedgetFeedPage/getFeedActors, mirroringapps/web/src/app/actions/activity-log.ts. - Done: both are server-only; no API token can reach a client bundle.
- Create
-
T12 · P1. Route constant and navigation.
apps/web/src/lib/constants.ts— addDASHBOARD_FEED: '/feed'next toDASHBOARD_ACTIVITY(line 110).apps/web/src/components/dashboard/DashboardSidebar.tsx— insert the nav item immediately above the existing activity item (line 198), labelt('navigation.feed'), iconRadiofromlucide-react.- Done:
/activityis unchanged in position, label and behaviour.
-
T13 · P1. Page shell and client.
- Create
apps/web/src/app/[locale]/(dashboard)/feed/page.tsx— RSC shell; parallel-fetch page 1 and the actor roster; exportgenerateMetadatareadingmetadata.pages.feed. - Create
apps/web/src/app/[locale]/(dashboard)/feed/feed-client.tsx—'use client'; owns filter state, entry list, selection, and the URL sync block (mirror therouter.replacepattern atactivity-client.tsx:111-123). - Done: first paint renders real rows server-side, not a skeleton.
- Create
-
T14 · P1. Feed components.
- Create under
apps/web/src/components/feed/:FeedList.tsx,FeedRow.tsx,FeedKindPill.tsx,FeedFilters.tsx,FeedAgentPicker.tsx,FeedEmptyState.tsx,FeedErrorState.tsx,FeedEndCard.tsx,FeedSkeleton.tsx. - Copy comes only from
useTranslations('dashboard.feed').FeedKindPillalways renders a text label alongside its colour.FeedSkeletonrows are the same height asFeedRowso nothing shifts on hydration. FeedRowrenderst(`narration.${entry.narration.key}`, entry.narration.params)and falls back tonarration.fallbackif the key is missing.- Done: wireframes and copy in spec §6.2, §6.5, §6.6, §6.7, §6.8, §6.9 are reproduced exactly.
- Create under
-
T15 · P1. Paging hook and the floors.
- Create
apps/web/src/lib/hooks/use-feed-paging.ts— cursor state, dedupe by entry id,IntersectionObserverwithrootMargin: '400px', a real Load older button for keyboard users, and the two stops: 20 pages per visit and 90 days. - Done: spec FR-44/45/46/48 covered; no page-number control exists anywhere in the UI.
- Create
-
T16 · P1. Keyboard affordances.
- In
feed-client.tsx, bindj,k,Enter/o,Esc,a,1–5,x,Shift+Lper spec §6.10, active only when no input has focus. - Add
role="feed"on the list witharia-busyduring loads, androle="separator"readiness for the P2 divider. - Done: the whole P1 flow is completable with no mouse.
- In
-
T17 · P1. i18n — English.
apps/web/messages/en.json— add the fulldashboard.feednamespace from plan §8, plusdashboard.sidebar.navigation.feedandmetadata.pages.feed. Values are the copy table in spec §6.9.- Every leaf name is camelCase and contains no literal
.— a dot in a leaf makes next-intl throw at runtime and reds several e2e shards at once. - Done:
dashboard.feed.narrationhas exactly 48 keys plusfallback.
-
T18 · P1 (parallel with T17). i18n — the other 20 locales.
- Run
pnpm --filter web translate:messages, then reviewapps/web/messages/{ar,bg,de,es,fr,he,hi,id,it,ja,ko,nl,pl,pt,ru,th,tr,uk,vi,zh}.json. - Done: a key-set diff against
en.jsonis empty for all 20 files.
- Run
P1 tests
-
T19 · P1. Web unit specs.
apps/web/src/components/feed/FeedRow.unit.spec.tsx— narration key + params render; a missinghrefrenders plain text, not a link; the kind pill always carries text.apps/web/src/lib/hooks/use-feed-paging.unit.spec.ts— no duplicate ids across pages; both floors stop the loader.- Done:
cd apps/web && pnpm testgreen.
-
T20 · P1. e2e — filters and paging.
apps/web/e2e/feed-filters-paging.spec.tscovering spec S4, S5, S8, S12, S15: agent chip filter + URL round-trip + reload persistence; the 21st-agent refusal message; three auto-loaded pages with no repeated entry; Only failed; the filtered-empty state; the terminal card.- Done: green headless and in CI.
-
T21 · P1. e2e — empty and error.
apps/web/e2e/feed-empty-and-error.spec.tscovering S11 and S13, including both call-to-action buttons on the never-any state and both actions on the error state.
-
T22 · P1. Regression guard.
- Run
apps/web/e2e/activity-log.spec.ts,activity-log-export.spec.ts,activity-log-audit.spec.tsand theflow-activity-*.spec.tssuite unchanged. - Done: all pass with no edits — the proof that
/activitywas not disturbed (spec §7).
- Run
Phase P2 — live, seen, and the emission gaps
The event and the stream
-
T23 · P2. The
activity.loggedevent.- Create
packages/agent/src/events/activity-logged.event.ts—ActivityLoggedEvent extends BaseEvent,static EVENT_NAME = 'activity.logged', carrying the persistedActivityLog. - Export from
packages/agent/src/events/index.ts. packages/agent/src/activity-log/activity-log.service.ts— add@Optional() @Inject(EventEmitter2)to the constructor (line 24-31) and a privatedispatchFeedEvent(activity)called fromlog()(line 95) right afterdispatchAnalytics(activity). It must be a no-op when the emitter is not bound — copy the posture ofNotificationService.dispatchFanout().- Test: extend
packages/agent/src/activity-log/activity-log.service.spec.ts—log()emits once; no emitter bound is a silent no-op; a throwing listener does not faillog(). - Done: no existing agent-package unit test needs a new module import.
- Create
-
T24 · P2. Stream registry.
- Create
apps/api/src/activity-log/feed-stream.registry.ts—Map<userId, Set<Subscriber>>,subscribe()refusing a 4th concurrent connection per user, per-connection emitted-idSetcapped at 2,000 with FIFO eviction, and filter matching (agents / kinds / failedOnly). - Test:
apps/api/src/activity-log/feed-stream.registry.spec.ts— subscribe/unsubscribe, the 4th-connection refusal, dedupe, the cap, filter matching.
- Create
-
T25 · P2. Stream listener. - Create
apps/api/src/activity-log/feed-stream.listener.ts—@OnEvent('activity.logged', { async: true, suppressErrors: true }), narrates and pushes to matching subscribers. - Test:apps/api/src/activity-log/feed-stream.listener.spec.ts— only matching subscribers receive; a listener error is suppressed and never surfaces to the writer. -
T26 · P2. SSE endpoint.
- Add
GET /streamtoapps/api/src/activity-log/feed.controller.ts, modelled line-for-line onapps/api/src/email/email.controller.ts:178-255:text/event-stream+no-cache, no-transform+keep-alive+flushHeaders();: pingevery 15 s; safety poll every 15 s; a 10-minute lifetime cap that callscleanup()andres.end();cleanup()also bound toreq.on('close'),res.on('close')andreq.socket?.on('close'). - Emit named
feedevents withid:set to the entry id soLast-Event-IDresumes on reconnect.@Throttle12/min. - Done: killing a connection abruptly leaves no timer running (assert in the registry spec);
a 4th concurrent connection gets
429withRetry-After: 30.
- Add
-
T27 · P2. Web BFF proxy for the stream.
- Create
apps/web/src/app/api/feed/stream/route.ts—export const dynamic = 'force-dynamic', read the auth cookie withgetAuthAccessCookie(), forward as a bearer to${API_URL}/feed/stream, pipeupstream.body. Near-copy ofapps/web/src/app/api/email/messages/stream/route.ts. - Done: the auth token never appears in a client bundle or a client-visible response.
- Create
-
T28 · P2. Stream hook with fallback and tab leadership.
- Create
apps/web/src/lib/hooks/use-feed-stream.ts—EventSourceagainst the BFF; backoff 1 s → 30 s with ±20% jitter; after 3 failures fall back to a 10 s refresh; leadership viaBroadcastChannel('ever-works-feed')plus alocalStorageleasefeed-stream-leader(renewed every 5 s, taken over after a 12 s stale lease); followers receive relayed entries; noBroadcastChannel→ every tab uses the 10 s poll. - Test:
apps/web/src/lib/hooks/use-feed-stream.unit.spec.ts— backoff schedule and jitter bounds; the 3-failure fallback; lease acquire / renew / take-over. - Done: spec FR-4/6/7/8/9 and S9/S10 covered.
- Create
-
T29 · P2. Queue pill and the connection banner.
- Create
apps/web/src/components/feed/FeedNewPill.tsxandapps/web/src/components/feed/FeedConnectionBanner.tsx; wire intofeed-client.tsx. - Queue when scrolled > 200 px from the top; cap the queue at 200 entries; the pill reads
99+beyond 99;tand clicking both scroll to top and release. - Done: spec FR-49/50/51/52 and S6 covered; the list never shifts under a reader.
- Create
Seen state and the away summary
-
T30 · P2.
FeedReadStateentity.- Create
packages/agent/src/entities/feed-read-state.entity.tsper plan §3.3, including both partial unique indexes (uq_feed_read_state_user_organduq_feed_read_state_user_no_org). - Export from
packages/agent/src/entities/index.ts. - Create
packages/agent/src/database/repositories/feed-read-state.repository.tswith agetOrCreateand a monotonicadvance. - Test:
packages/agent/src/database/repositories/feed-read-state.repository.spec.ts.
- Create
-
T31 · P2. Migration for T30, in the same PR.
apps/api/src/migrations/1791040100000-CreateFeedReadState.ts(AW-04 slot 01) —CREATE TABLE, the FK touserswithON DELETE CASCADE, and the two partial unique indexes.- Done: forward-only;
down()drops only the table and its indexes.
-
T32 · P2.
FeedReadStateService.- Create
packages/agent/src/activity-log/feed-read-state.service.ts—getState,markSeen({ upToActivityId, upToCreatedAt })(monotonic: a request to move backwards succeeds and does nothing),countUnseen,dismissAwaySummary. - Register in
packages/agent/src/activity-log/activity-log.module.ts, export from the barrel. - Test:
packages/agent/src/activity-log/feed-read-state.service.spec.ts— lazy create; backwards write is a no-op; per-organization isolation; dismiss and re-arm. - Done: spec FR-21/24/25/27/34 covered.
- Create
-
T33 · P2. Away-summary service.
- Create
packages/agent/src/activity-log/feed-away-summary.service.ts— the 30-minute and ≥1-unseen gates, the 7-day window clamp, the 1,000-row scan cap with atruncatedToScanCapflag, counts by kind, top 5 actors plus the remainder, decisions waiting, failures, all inside a 3-second budget. Follows the deterministic-first, degrade-with-a-reason posture ofpackages/agent/src/digest/digest.service.ts. - Test:
packages/agent/src/activity-log/feed-away-summary.service.spec.ts— both gates, the clamp, the cap flag, the remainder count, the zero-window{ shown: false }branch, the timeout branch. - Done: spec FR-28..FR-35 covered.
- Create
-
T34 · P2. API endpoints for seen and the summary.
apps/api/src/activity-log/dto/feed-seen.dto.ts—upToActivityId?(@IsUUID),upToCreatedAt?(@IsISO8601), at least one required.apps/api/src/activity-log/feed.controller.ts— addPOST /seen(60/min),GET /away-summary(20/min),POST /away-summary/dismiss(30/min,204).- Extend
apps/api/src/activity-log/feed.controller.spec.tswith the new cases, including thatPOST /seenis idempotent and monotonic.
-
T35 · P2. Web plumbing for seen and the summary.
- Extend
apps/web/src/lib/api/feed.tsandapps/web/src/app/actions/feed.ts. - Create the client-callable BFF routes
apps/web/src/app/api/feed/seen/route.tsandapps/web/src/app/api/feed/away-summary/dismiss/route.ts— thin cookie-forwarding proxies, same shape asapps/web/src/app/api/activity-log/[id]/route.ts. - Create
apps/web/src/lib/hooks/use-feed-seen.ts— the three-condition advance rule (tab visible and feed focused ≥ 3 s and newest entry in the viewport), throttled to once per 2 s, plusMark all seenand cross-tab sync over the sameBroadcastChannel. - Test:
apps/web/src/lib/hooks/use-feed-seen.unit.spec.ts.
- Extend
-
T36 · P2. Divider, badge and away card.
- Create
apps/web/src/components/feed/FeedDivider.tsx(sticky,role="separator", anchored to thelastSeenAtcaptured at page load and not moved as the watermark advances),apps/web/src/components/feed/AwaySummaryCard.tsx(all four states: normal, truncated, failed, dismissed — every number is a control that filters the feed), andapps/web/src/components/dashboard/SidebarFeedBadge.tsx(exact ≤ 99 then99+, refreshed at most every 30 s, reading the open stream's count when this tab holds it). - Wire the badge into
DashboardSidebar.tsxbeside the item added in T12. - Test:
apps/web/src/components/feed/AwaySummaryCard.unit.spec.tsx. - Done: spec FR-22/23/26/32 and S2/S3 covered.
- Create
Closing the emission gaps
-
T37 · P2. New run action types.
packages/agent/src/entities/activity-log.types.ts— appendAGENT_RUN_STARTED,AGENT_RUN_COMPLETED,AGENT_RUN_FAILEDwith a comment stating thatactionTypeis a plainvarchar(50), so no migration is required, and that the threeagent_heartbeat_*members are unchanged.- Add the three to the T4 kind map and the T5 narrator (they are already in the 48).
- Done:
pnpm --filter @ever-works/agent testgreen; the Activity page's existing type filter is unaffected.
-
T38 · P2. Emit run start and terminal records for all 5 trigger kinds.
packages/agent/src/agents/agent-run.service.ts:- in
execute()(line 247), emitAGENT_RUN_STARTEDwithactorKind: 'agent',actorAgentId,actorLabel(the agent's currentname) anddetails.runId/details.triggerKind; - in
finalize()(line 1474), widen the existingcontext.kind === 'heartbeat'gates (lines ~1512 and ~1541) so heartbeat keepsAGENT_HEARTBEAT_COMPLETED/AGENT_HEARTBEAT_FAILEDand the other four kinds (manual,task,chat,event) emitAGENT_RUN_COMPLETED/AGENT_RUN_FAILED; - extend
logActivity()(lines 2155-2188) to accept and pass through the three actor fields.
- in
- Idempotency: set a deterministic
ingestEventIdofrun:${runId}:${phase}on every emit so a worker retry cannot produce a second feed entry (spec FR-56). - Scope: stamp
userId,tenantIdandorganizationIdfrom the agent being run, since these emits happen outside a request context (spec FR-55). - Test:
packages/agent/src/agents/__tests__/agent-run-feed-activity.spec.ts— one record per terminal state for each of the 5 trigger kinds; heartbeat still uses the heartbeat action types; a repeatedfinalize()writes exactly one record. - Done: spec FR-53/54/55/56 covered.
-
T39 · P2 (parallel with T38). Actor stamping at the other high-value writers.
- Add
actorKind/actorAgentId/actorLabelto theActivityLogService.log()calls that already know the acting agent, starting withpackages/agent/src/agents/agent-export.service.ts(line ~803),apps/api/src/agents/agents.controller.ts(line ~1626) andapps/api/src/agents/agent-collaborators.controller.ts(line ~216). - Done:
feed.narration.fallbackRateand the read-time actor ladder are measurably less loaded; no call site's behaviour changes otherwise.
- Add
P2 tests
-
T40 · P2. e2e — live.
apps/web/e2e/feed-live.spec.tscovering S1, S9, S10: a written record appears within 5 s; the reconnecting banner; the degraded banner after 3 failures; no duplicates after recovery; the second-tab notice.
-
T41 · P2. e2e — seen and away.
apps/web/e2e/feed-seen-divider.spec.ts(S2, S3, S16) andapps/web/e2e/feed-away-summary.spec.ts(S2, S14).
-
T42 · P2. e2e — scope isolation and accessibility.
apps/web/e2e/feed-scope-isolation.spec.ts(S18) andapps/web/e2e/feed-a11y.spec.ts(full keyboard flow per spec §6.10 plus an axe pass with zero serious or critical violations).
-
T43 · P2. Telemetry.
- Emit the client signals (
feed_opened,feed_marked_seen,feed_filter_changed,feed_page_loaded,feed_stream_state) and the API gauges (feed.stream.*,feed.push.latencyMs,feed.poll.caughtCount,feed.narration.fallbackRate) throughpackages/monitoring/. - Add the Sentry tags
feature:aw-04-live-feed,feed.kind,feed.actorKind. - Done: plan §9.1 fully wired; no new
ActivityActionTypeis added for the feed's own use (spec FR-57).
- Emit the client signals (
Phase P3 — polish and what other epics need
-
T44 · P3. Optional written narrative, dispatched.
- Create
packages/agent/src/tasks/feed-away-summary-dispatcher.ts—export const FEED_AWAY_SUMMARY_DISPATCHER = Symbol('FEED_AWAY_SUMMARY_DISPATCHER')plus the dispatcher interface. - Create
packages/tasks/src/tasks/trigger/feed-away-summary.task.tsand register it inpackages/tasks/src/tasks/trigger/index.ts. - The model call goes through
packages/agent/src/facades/ai.facade.tsonly, capped at 1,200 characters / 400 tokens, degrading to counts-only with a visible reason. - Idempotency key:
feed-away:${userId}:${organizationId ?? 'none'}:${windowStartEpoch}. GET /api/feed/away-summaryreturns counts immediately withnarrative: null, narrativeStatus: 'queued'; the card polls once after 3 s.- The card footer shows the model and token cost that produced the paragraph
(
dashboard.feed.away.narrativeCost), per program rule #9. - Done: no call site imports
@trigger.dev/sdk; the feature is off by default (spec FR-36).
- Create
-
T45 · P3. Compact feed block for Home.
- Export a
CompactFeedfromapps/web/src/components/feed/CompactFeed.tsx— same rows, page size 8, no filters, no divider, a "See all" link to/feed. - Done: AW-19 can import it without touching feed internals; the open question in spec §9 about which entries it shows is resolved before this task starts.
- Export a
-
T46 · P3. Narration coverage beyond 48.
- Read
feed.narration.fallbackRatefrom production, rank the uncovered action types by frequency, and add narrator entries + i18n keys in that order. - Done: fallback rate below 10% of rendered entries.
- Read
-
T47 · P3. Shortcut sheet handover.
- Register the feed's shortcuts with AW-01's
?sheet rather than duplicating a local overlay.
- Register the feed's shortcuts with AW-01's
-
T48 · P3. Attention hooks for AW-13.
- Expose the feed-kind classification and the unseen count as a stable internal contract that AW-13 can build the notification matrix on, so the two do not each grow their own copy of the mapping.
Docs and rollout
-
T49. User-facing documentation.
- Create
docs/features/live-feed.md; add'features/live-feed'toapps/docs/sidebarsPlatform.tsimmediately above the existing'features/activity'entry (line 98); cross-link fromdocs/features/activity.md. - Done:
pnpm --filter ever-works-docs buildproduces no broken-link warnings.
- Create
-
T50. Tracker and status.
- Update
docs/specs/features/agent-workspace/TRACKER.md— set AW-04's Spec column toDrafton this PR, then to the implementation state as each phase lands. - Set this file's and
plan.md's status toDone, andspec.md's toImplemented, only after P2 lands.
- Update
-
T51. Full gate.
pnpm format && pnpm lint && pnpm type-check && pnpm test && pnpm buildfrom the repo root.cd apps/web && pnpm test:e2efor the 7 new feed specs plus the activity regression suite.
Definition of done
- Every checkbox above ticked for the phase being shipped.
- Every functional requirement in spec §4 has at least one passing automated test.
- Every acceptance criterion in spec §8 verified by hand once and by a test thereafter.
- Both migrations applied cleanly on a copy of production-shaped data, with no lock held on
activity_logfor longer than a metadata change. /activitybehaves identically todevelop— proved by T22.- All 21 message files carry the identical
dashboard.feedkey set, with no literal.in any leaf. - Constitution gates in plan §12 re-confirmed at review time.