Repository navigation
Stage 4.2: per-user personalized 09:00 digests with quiet, opt-out and authorization controls (#1550) #1733
Description
Written-by: Chief
Part of #1550, fourth slice of Stage 4. Runs under the owner's 2026-10-29 instruction to carry #1550 through Stages 1-4, and under the identity decision recorded on #1712.
Depends on
2.5 (#1712, merged d34e4e62, shipped in v0.2.14): the Project Assistant, its private FlyBot-side memory, one 09:00 Asia/Shanghai schedule per assistant Session, and the identity rule — a single-install owner's GitHub identity is the host connector account; an unsigned-in connector, or a multi-user host whose ownerUserId is not the host user, refuses and escalates needs-human, and never falls back to the shared App.
3.1 (#1720, merged in 80709a71): the typed task record (taskClass, reworkOf, handoff, evidence, lease) this slice reads when it prints actionable items.
Why
Stage 4's acceptance test is a comparison, not a delivery: two users receive different personalized digests derived from the same factual project status without revealing one another's private memory. Today one assistant serves one owner; nothing states what happens when two assistants live on one host, nothing lets a user hold or silence their own 09:00 digest, and no test proves one assistant cannot read the other's memory. This slice builds exactly that boundary and the controls around it.
What to build
- One digest per (project, owner), from one status read. Both assistants render from a single read of the same project status (the 2.4 reader and the 2.3 board read model — do not add a second reader). A test asserts the two digests quote the same underlying facts and different personalization, and that digest A carries no line originating in digest B's private memory.
- Quiet hours per user. A quiet window in the user's own timezone suppresses delivery, not generation: a digest due inside the window is held and delivered once when the window ends, labelled as held; a window that covers 09:00 means no 09:00 message at all, and that is the expected outcome, not a failure.
- Opt-out and authorization. A user can turn their digest off — no generation and no schedule wake-up — and back on; the state lives with the assistant's own record, not in a global setting. An assistant reads only the project it is bound to plus its own owner's memory; a request for another user's memory is refused with the boundary named, and a test proves the refusal.
- Actionable decisions block. Each digest lists at most N items (one constant) that need that user's decision, each with its task id and a one-line ask; an item the user has no authority over never appears.
- Failure isolation. One user's render or delivery failing does not stop the other: both 09:00 digests still go out, and the failure is recorded in the digest's evidence instead of being swallowed.
Acceptance
- Tests in
core/test/: two owners on one host get two digests from one status read with no private-memory bleed (both directions); a quiet window suppresses and then delivers once, labelled; a window covering 09:00 delivers nothing; opt-out stops generation and schedule wake-up and re-enabling restores it; a cross-owner memory read is refused naming the boundary; a failing render for user A still delivers user B's digest and records A's failure. - No second project-status reader, no second store;
npm run verifygreen at the tip withTZ=UTC;core/client.jsandpatches/ds-bot/regenerated from source only;docs/subsystems/core-host.mdupdated (or a validDocs-not-needed:in both the branch commit and the PR body). - PR body:
Part of #1550,Fixes #<this issue>,Owner: Chief,Reviewer: Reviewer, checkpoint block withclaim: Chief.
Out of scope
Stage 4.1 meetings (#1731) and every Stage 5 research question. No new UI page: these controls ship on the existing Project Assistant settings and the digest record, not as a new board or a new scheduler.
Open question for the owner (only if it blocks the build)
If two maintainers arrive with no GitHub identity resolvable under the 2.5 rule, the slice still refuses and escalates needs-human — that behaviour stands and does not need a new decision. Only a request for a delivery channel other than the owner's own Session would need one; it is not in scope here.
Activity
Birfy commentedon Oct 29, 2026
Written-by: Triage
Scope for the implementation node — Stage 4.2 slice of #1550 (#1733).
Gap today.
digest()incore/src/host/project-assistant.jsserves one owner: memory is keyed by Project id only (projectMemoryOf/setProjectDigestincore/src/host/store.js), and the whole digest is gated onidentity(), which also blocks in-app delivery. Quiet hours (core/src/shared/quiet-hours.js) only refuse a new/edited schedule (#1166) and never hold a due digest; no opt-out exists anywhere.Build.
rt.projectBoard(project.id)read plus onelistProjectState(project.workDir)read, two renders. Digest A must carry no line originating in B's memory (both directions); a cross-owner memory read is refused naming the boundary. No second project-status reader, no second store.state.prefs): a due digest is held and delivered once when the window ends, labelled held; a window covering 09:00 delivers nothing (expected outcome, not a failure). Generation may proceed while delivery waits.rt.scheduleEndpoint); on restores both.Files.
core/src/host/project-assistant.js(main),core/src/host/store.js(owner dimension in the memory/digest record),core/src/host/schedules.js(opt-out path),core/test/project-assistant.test.jsandcore/test/quiet-hours.test.js(or one newcore/test/project-digest-owners.test.js),docs/subsystems/core-host.md. Leavecore/src/shared/**untouched: touching it is T3, while the host-only change is T2.Tests (red first,
TZ=UTC). Two owners on one host: two digests from one status read, same underlying facts/different personalization, no private-memory bleed in either direction; a quiet window suppresses then delivers once labelled held; a window covering 09:00 delivers nothing; opt-out stops generation and schedule wake-up and re-enabling restores both; a cross-owner memory read is refused naming the boundary; A's render throws while B's digest is still delivered and A's failure is recorded.Before the PR. Branch from the live
origin/main: open PR #1727 touchescore/src/host/schedules.js,core/src/host/store.jsanddocs/subsystems/core-host.md, so a stale base will conflict.npm run verifywithTZ=UTC; regeneratepatches/ds-bot/andcore/client.jsfrom source only; PR body carriesPart of #1550,Fixes #1733,Owner: Chief,Reviewer: Reviewerand the checkpoint block withclaim: Chief.Risk: host code (
core/src/host/**) is T2 by.github/agent/risk-rules.json; keeping the change host-side keeps it T2.Birfy commentedon Oct 29, 2026
Written-by: Dev
Claimed #1733 — first step: per-(project, owner) memory in the store, then the two-owner digest test. Branch: dev/1733-per-owner-digest.
Address the review of the per-owner digest
Gate the Project owner endpoints behind requireProjectOwner
Gate the Project owner endpoints behind requireProjectOwner