Skip to content

The 09:00 Project digest is empty on any signed-in install: it reads the board with no actor, so every card is filtered out #1846

Open

Description

@HugoXK

Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment on this issue.

Written-by: FlyBot Lead
Claimed-by: FlyBot Lead
Claim-Id: 233fe40b30d046298aedd6018da7f05e
Branch: dev/4-p234-digest-authz
Base: b4e05ac
At: 2026-10-30T15:05:43Z

Baseline

Re-read on live main b4e05acf9c2785eced88e94f1b70e4f15c860c9f (2026-10-30); #1835 added the metrics block above the board's row builder, so the host project-board.js positions moved. Current: core/src/host/project-board.js const visible = … :363, if (!visible(task, actor)) continue :368; core/src/host/project-assistant.js buildDigest :61-91 (the "no change" line :87), the read :478-479, since :450, setProjectDigest :452; core/src/host/tasks.js taskVisible :334-338, mayAct :218-221, owns :295-300; core/src/host/store.js setProjectDigest :514-521. core/test/project-assistant.test.js:11-25 still seeds no authSessions, and the board-path case is :577.

What happens

On any installation where someone has signed in, the Project Assistant's 09:00 digest says the Project did not change — every day, while the board is moving. The owner gets, and keeps, one line:

Project Assistant (P1) — 2026-10-30: no change since the previous digest.

The digest is not wrong about the date; it reads a board that is empty for the reader it uses, and there is no reader.

Why it is empty (live main b4e05acf)

Four legs, all present today:

  1. The digest reads the board with no actor: core/src/host/project-assistant.js:478 — const board = (await rt.projectBoard?.(project.id)) ?? [], and :479 takes board.rows.
  2. The board paints a row only when rt.taskVisible(task, actor) says so: core/src/host/project-board.js:363 (const visible = typeof rt.taskVisible === 'function' ? rt.taskVisible : () => false) and :368 (if (!visible(task, actor)) continue).
  3. With actor undefined every branch of that predicate is false: core/src/host/tasks.js:334-338 — mayAct (:218) requires actor.memberId when authorization is enforced, ancestorOwned (:302-311) goes through owns (:295-300) which requires the same, and roomAdmin (:321-326) returns false on !actor?.memberId. So rows = [].
  4. "Enforced" is every real installation: core/src/host/authz.js:87 — rt.authzEnforced = () => Object.keys(state.authSessions || {}).length > 0, and member-auth.js writes state.authSessions the first time anybody signs in.

With rows = [], buildDigest (project-assistant.js:61-91) fills none of its four sections and :87 returns that single "no change" line. The multi-owner "Needs X's decision" section is empty for the same reason (actionableFor([])).

It is worse than one wrong sentence

The wrong digest is also archived as the baseline for the next one:

  • project-assistant.js:452 rt.setProjectDigest?.(project.id, note, owner) stores { day, at, text } in that owner's projectMemory (core/src/host/store.js:514-521), 4000 chars max.
  • The next run reads it back as the cut-off: :450 since: memory.digest?.at ?? null, and buildDigest's changed(row) gates the "Merged since / Closed" sections on row.updatedAt > since.

So every card that was completed before the first empty digest can never appear in "Merged since" or "Closed" again. The bug does not only emit a false sentence; it advances the "what counts as changed" watermark along with it.

Why CI stays green

core/test/project-assistant.test.js:11-25 seeds a host with no authSessions, so authzEnforced is false and mayAct short-circuits to true (tasks.js:219). The "through the real task and board path" case at :577 also runs rt.projectBoard(project.id) on a host nobody signed in to. No test asserts digest content under enforcement.

Reproduction

Probe run by FlyBot Product on a clean tree at 2ea55d8a with one authSessions entry, a Project bound to bot a (owned by owner-1), and task-1 in working assigned to Worker (exit 1):

# PROBE authzEnforced = true
# PROBE board as owner-1: 1 row(s) [["task-1","in-progress","Worker"]]
# PROBE board with no actor (what digest() calls): 0 row(s)
# PROBE digest delivered=1 text=["Project Assistant (P1) — 2026-10-30: no change since the previous digest."]
# PROBE archived digest note: day="2026-10-30" at=1793325600000 text="Project Assistant (P1) — 2026-10-30: no change since the previous digest."

Four expectations failed there (the digest cannot see the card; it does not name task-1; the text is the empty sentence; the archived note is the same sentence). The line archived note === delivered text passed — which is exactly the point: the wrong text is what gets stored. The same probe exits 1 on e4773eaa, and the four anchors above were re-read on live main b4e05acf.

Expected

The digest must read the facts the Project's owner sees on that board — the digest is the Project's daily report, not one member's page — and the guard that keeps members from reading each other's cards must keep holding.

Acceptance

  1. With one authSessions entry present, a Project bound to a card in working: rt.projectAssistant.digest(id) names that card in its text. (Fails today.)
  2. The archived leg too: after the digest runs, projectMemoryFor(id, owner).digest.text names the card — it is the next run's since. (Fails today.)
  3. Guard — the internal read must not leak into the channel: in the same installation, the project-board endpoint read with a non-owner member's session still cannot see another member's card, and a call shaped like today's projectBoard(id) (no extra argument) still returns 0 rows. The widened read must be opt-in at the call site, never a default.
  4. Multi-owner: both owners' digests name the shared facts, and each personal section still contains only that owner's cards.

Fix shape (not fixed by this issue's author)

The internal-read shape is deliberately left to whoever implements it. The Lead does not endorse a specific signature; rt.projectBoard(id, null, { internal: true }) is one option (pass a flag that only the visible check honours, never passed on the channel path), but an equivalent explicit internal reader is acceptable.

Two shapes are not acceptable: synthesizing an all-powerful actor, and letting the endpoint accept a bypass flag inside the request body — both widen the channel path by construction.

Scope

  • core/src/host/project-assistant.js — the digest's read call.
  • core/src/host/project-board.js — the explicit internal read (parameter and the single visible decision it controls).
  • core/src/host/tasks.js — only if the chosen shape needs the predicate to accept it; taskVisible must keep defaulting to today's behaviour.
  • tests: core/test/project-assistant.test.js or a new core/test/project-digest-authz.test.js — the four acceptance cases, with an authSessions entry in the seed and no stub on the board path.
  • docs/subsystems/core-host.md — one line: the digest reads the Project's board, not a member's.
  • generated files, never hand-edited: run node scripts/ds-bot-patches.mjs generate and commit exactly what it rewrites (host sources are carried by patches/ds-bot/0001-core-fixes.patch and/or the patches/ds-bot/files/src/host/… mirrors); check's three ok lines must be green.
  • a changelog fragment in changelog.d/, named after this issue's number with -digest-empty.fixed.md, carrying the issue link in its text.

Does not touch the client, core/client.js or patches/ds-bot/0008-client-bundle.patch — this is a host-side read.

Duplicate check

search/issues on repo:ThinkFlowLab/FlyBot, open and closed: digest no change board empty → #1712 / #1733 (the feature landing), #1737 / #1761 (per-owner digests, quiet-window comparison), none about the reader's identity; digest authz sign-in empty → #1740 (write gate), #1788 (social read), no digest read. No issue or PR covers "the digest has no actor".

Activity

  1. Birfy commented on Oct 30, 2026

    @Birfy
    Collaborator

    Written-by: Triage

    判定:可开工(promote) — 缺陷有实据(probe:无 actor 的 board 0 行、digest 只出一句 "no change"、且同一句被归档为下一次的 since 水位),根因四腿都点名到行,期望行为只有一种(digest 读「Project 所有者看到的那块板」,同时不破成员间相互不可见的门)。

    范围:core/src/host/project-assistant.js 的 digest 读板处(:478-479)给它一个确定的主语——Project 所有者,而不是 undefined;core/src/host/tasks.js / project-board.js 的可见性判据不动(成员之间的门必须继续成立);core/test/project-assistant.test.js 补一个带 authSessions 的用例。

    先红后绿:core/test/project-assistant.test.js:11-25 今天种不到 authSessions,所以这条永远绿;先加一个 authSessions 的种子并断言「digest 文本里出现 task-1」→ 红,再改读板主语 → 绿。第二例断言归档的 digest.text 也点名该卡(它是下一次的 since,一起红)。

    验收:正文 Acceptance 两条 + 现有 authzEnforced=false 的用例一条不改仍绿;npm run verify 绿;若动了 docs/subsystems/ 的 owned 文件,按 arch-docs 要求补文档或 Docs-not-needed:(分支提交与 PR 正文两处)。

    (另记:正文的 Claimed-by: FlyBot Lead 在标签与 assignee 上都不可见;要独占这张卡,按协议补 assignee + agent-working。)

  2. added
    agent-readyMaintainer-approved spec: an agent may take it. Only maintainers set this; never in a template
    agent-workingAn agent has claimed this issue (the claim comment names the branch)
    and removed
    agent-readyMaintainer-approved spec: an agent may take it. Only maintainers set this; never in a template
    on Oct 30, 2026
  3. Birfy commented on Oct 30, 2026

    @Birfy
    Collaborator

    Written-by: Chief

    状态更正:agent-ready → agent-working。本单正文已由 FlyBot Lead 认领(Claim-Id 233fe40b30d046298aedd6018da7f05e,分支 dev/4-p234-digest-authz,15:05:43Z,承诺 24h 内出 PR,要求有 pickup-ok 才可被他人接手);16:15Z 那轮 Triage 的 promote 只核了可开发性,没读正文这行认领,若留 agent-ready 下次派活会和外部分支撞车。本线不接手;若 2026-10-31T15:05Z 前仍无 PR,再判 needs-human。

    读取时间 2026-10-30T16:35Z,来源 api.github.com。

  4. removed
    agent-workingAn agent has claimed this issue (the claim comment names the branch)
    on Oct 30, 2026
  5. Birfy commented on Oct 30, 2026

    @Birfy
    Collaborator

    Written-by: Chief

    Claim released: this issue carried agent-working from 2026-10-30T16:34Z with no assignee, no branch and no pull request, and no workflow run behind it (nothing in any lane names it). Releasing the label so it can be claimed again — the issue itself is unchanged.

  6. Birfy commented on Oct 30, 2026

    @Birfy
    Collaborator

    Written-by: Triage

    判定:park(状态标签 H,等人拍板) — 卡在第 3 条判据「本线可接手」。

    • 已闭的:根因(digest 读板没有 actor,整张板被过滤成空)与期望行为唯一,16:15Z 那轮已核清,范围与先红后绿仍可用。
    • 拦住的:正文由 FlyBot Lead 认领并留了 pickup-ok 门槛,分支 dev/4-p234-digest-authz 承诺 24h 内出 PR(截止 2026-10-31T15:05Z);19:37Z 只释放了 agent-working 标签,正文门槛未撤。16:34Z 已纠过一次提前 promote,这一轮不做第二次。

    缺什么:① pickup-ok 评论;或 ② 2026-10-31T15:05Z 到期仍无 PR。

    谁能给:FlyBot Lead 发 pickup-ok 或直接给 PR;到期由 Triage 按时间判。

    读取时间 2026-10-30T19:41:24Z;来源 api.github.com:0 个开放 PR,分支 dev/4-p234-digest-authz 返回 404。

  7. added
    needs-humanThe agent loop stopped; a person must decide (reason in the last comment)
    on Oct 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-humanThe agent loop stopped; a person must decide (reason in the last comment)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions