Skip to content

feat(denkraum): split pending, inbox and delivery onto the store (DR-04b) - #669

Merged
mergify[bot] merged 2 commits into
mainfrom
claude/dr-04b-denkraum-delivery
Oct 8, 2026
Merged

mergify[bot] merged 2 commits into
mainfrom
claude/dr-04b-denkraum-delivery

Conversation

@Cuarroc

@Cuarroc Cuarroc commented Oct 8, 2026 •

Copy link
Copy Markdown
Owner

Der Denkraum kann jetzt auf dem neuen Speicherkern offene Antworten auflisten, einen Posteingang für Root bilden und Benachrichtigungen mit stabiler Zustellungskennung verschicken, genauso wie im bisherigen Stand P1.
Ohne konfigurierte Root-Kennung lehnt er diese drei Wege im neuen Datenformat mit Fehler 503 ab, statt still etwas Falsches zu zeigen oder zu senden.
Fortschritt, Ideen und die Bedienoberfläche folgen in eigenen Paketen; zwei Fremdprüfungen und Roots Abnahme stehen noch aus.

Report

Package: DR-04b (Z1), PLAN row DR-04, parent ticket 55ea21ce. Released by Orchestrator 13:28Z (msg-0muzknusg) as the serial second half of DR-04 after the 04a/04b split (Orchestrator accepted the split 13:19Z). Owner: DR-03/04 session (Claude claude-opus-5-5[1m]). Stacked on #666: base claude/dr-04-denkraum-answers @ 1e11bb8d45d26cca4d31974314bcb8af2763ed94. #666 is stacked on #661 @ c80456a; Root retargets after those merge. Tier A (persistence, D2 trust boundary, outbound delivery).

What changed (2 new files, 191 ALL incl. the R669-O1 fix)

  • tools/denkraum/store/delivery.mjs (69): class DeliveryStore extends AnswerStore with pending(), inbox(), flushNotifications(eventId) and the private #deliver, plus the per-instance #notificationQueue. Moved from frozen P1 store.mjs (sha256 592b309d…a2c9) lines 515–569. No new product logic. Only two renames: this.#change( → this.change( (DR-03 core) and the P1 constant rootAgentId → injected this.rootAgentId (D2, 4 places).
    • On V2, every receiptFor lookup in these three paths therefore fails closed with 503 without a configured root id.
    • R669-O1 fix (dc34cae, the only behaviour change against P1): a module helper requireRoot checks the root id on V2 right after the state read in pending(), inbox() and inside the #deliver transaction, before any event is filtered. So an empty V2 ledger and flushNotifications('other-event') also answer 503. P1 could not show this case because its root id was a constant.
    • V1 behaviour is unchanged: the DR-02 receiptFor returns false for V1 before the root check, so V1 pending/inbox read the legacy ack and need no root id.
  • tools/denkraum/store/delivery.test.mjs (122): 8 store-level tests, synthetic ids only.
    • Pending and inbox until the Root receipt (V2) or the legacy ack (V1).
    • No transport or V1 ledger: no write and no call.
    • A failed delivery stays pending with the fixed code transport-failed; the transport detail is not persisted. The retry reuses the event id as delivery id. The payload carries only immutable refs (type, eventId, contentRevision, deliveryId). A queued event is not sent again and is not a receipt.
    • An interrupted queue commit retries the same id. Two concurrent flushes in one store do not duplicate.
    • A receipt or a newer answer arriving during network IO wins and the file stays unchanged.
    • A queued delivery older than 7 days gets a new delivery id for the same event; a failed renewal keeps the new id; nothing is sent after the Root receipt.
    • D2: V2 pending/inbox/flushNotifications fail with 503, write nothing and never call the transport.
    • R669-O1: the same on a V2 ledger without any answer, including flushNotifications('other-event'); without a transport the result stays not-configured.

Relation to the Planungsarchitekt DR-04 draft (ticket 55ea21ce, 01:18:48Z)

  • Used byte-exact for the moved code: server proof dr02-proof-20261008/after/store/delivery.mjs (sha256 18f4d5f3…dae7) lines 108–162 with this[CHANGE]( → this.change( and this.io.rootAgentId → this.rootAgentId applied, diff against delivery.mjs lines 10–64: exit 0, no hunk. The #notificationQueue line (proof 8 ↔ 9): exit 0.
  • The deviations are the same as in DR-04a: answers, receipts and acks on the store core + C-3 root check #666 and are justified there: the accepted DR-03 core instead of the proof core's CHANGE/PROGRESS symbols and ENV default; no store.mjs re-export yet; no C-3b exception (Chief 13:19Z).
  • Draft test "C-3b: v1 ack works without root id" is replaced by the D2 rule. The V1 read paths stay rootless only because V1 has no receipts. That is the DR-02 model as accepted, not a new exception.

Evidence

Step Command / site Result
RED on base 1e11bb8 server worktree 13:38:00Z, test file only, node --test --test-name-pattern='V2 pending inbox and delivery fail closed' tools/denkraum/store/delivery.test.mjs Node exit 1, ERR_MODULE_NOT_FOUND for delivery.mjs (new feature module, same red pattern as DR-03 core); log sha256 cdd7b979…db0d
GREEN local node --test tools/denkraum/store/delivery.test.mjs; npm run test:denkraum (Windows) exit 0, 7/7; exit 0, 52/52, hygiene 14 files 0 findings
D1 split = P1 behaviour (scratch, not committed) frozen P1 siblings incl. unchanged sender.test.mjs + P1 ideas.test.mjs + this model/core/answers/delivery + remaining P1 ops (264–415) as DeskStore extends DeliveryStore with injected root id; P1 515–569 removed from the shim; node --test --test-concurrency=1 *.test.mjs exit 0, 101/101
Commit + hooks server, CARGO_TARGET_DIR=~/targets/slot5, CARGO_BUILD_JOBS=2, foreground ssh_exec first attempt 13:37:24Z refused by the commit hook: the test name contained commas, which the trailer check reads as a list. No commit was made. Test renamed; 144f12f precommit exit 0 (13:38:14Z, incl. secrets, typecheck, denkraum, architecture-drift)
prepush server bash scripts/ci/gates.sh lane prepush on 144f12f exit 0, 13:41:11–13:42:39Z, 22 gates gruen, rust-suite nextest 1873 run / 1873 passed / 7 skipped; log sha256 f264d398…a0e. An earlier identical run (13:38:52Z) also finished green but its exit code was not captured because the SSH session closed; it does not count as evidence.
red-first server bash scripts/ci/red-first.sh exit 0; base 7e3a7af; all 6 trailers in the stack red/absent at the merge base and green at the head
R669-O1 RED on 144f12f server 14:23:38Z, new test only, node --test tools/denkraum/store/delivery.test.mjs Node exit 1, 7 pass / 1 fail, Missing expected rejection; log sha256 8261ef883824f6c0…
R669-O1 commit + prepush server, dc34cae precommit exit 0 (14:24:23–14:24:36Z); prepush exit 0 14:24:52–14:26:34Z, 22 gates gruen, nextest 1873 run / 1873 passed / 7 skipped; log sha256 0ee97b5f6bc19242…; local npm run test:denkraum (Windows) exit 0, 53/53, hygiene 0 findings
R669-O1 red-first + push server bash scripts/ci/red-first.sh; git push red-first exit 0, the new trailer red at merge base 7e3a7af and green at the head. First push 14:26:51Z was rejected by the remote (reason not captured: -q); the retry reported 144f12f..dc34cae, exit 0. git ls-remote 14:28:55Z = dc34cae381e0e113a7ed406d81b018f0e26d1cee
push (original head) git push + git ls-remote 13:43:33Z PA_PREPUSH=light git push -u after the full prepush on the same head, exit 0; git ls-remote 13:44:15Z = 144f12faa3dbebabbdeb0c4d743b05a87253537c; #666 branch unchanged at 1e11bb8, #661 branch at c80456a

Limits (unchanged, not solved here)

  • #notificationQueue serializes deliveries per store instance only. Two instances or processes can each deliver once; Root dedupes by deliveryId. Same class as R661-02.
  • F-01 (total ledger bounds) stays open. activeEvents is quadratic and runs in pending, inbox and every flush.

Review

Tier A. OpenAI R1 on 144f12f: approve with conditions (R669-O1). OpenAI R2 delta on dc34cae: approve with conditions — R669-O1 fixed except the no-transport case (tools/denkraum/store/delivery.mjs:35: a rootless store without transport returns not-configured before the root check; no write, no delivery, ledger bytes unchanged). Google R1 (Gemini 3.1 Pro) on dc34cae: approve; R669-G1 low on the same line, judged not a real gap; npm run test:denkraum exit 0.

ID Source Severity Disposition
R669-O1 (remainder) / R669-G1 OpenAI / Google medium / low Follow-up (Root, 2026-10-08): no exposure, no state change, no delivery; reorder the root check before the transport check in a small follow-up package after the stack lands, so the fail-closed contract holds literally.

Root acceptance: two foreign vendors approve at the current head; the remainder is a tracked follow-up.

ID Source Severity Finding Disposition
R669-O1 OpenAI R1 medium Without a configured root id the V2 paths only answered 503 once an event reached receiptFor. An empty V2 ledger gave pending() [], inbox() empty lists and flushNotifications() empty. flushNotifications('other-event') also gave empty. Accepted, fixed in dc34cae. Test: delivery.test.mjs::V2 reads and delivery refuse a missing root agent id even when no event needs a receipt check (R669-O1).
R669-O1 (sub-question) OpenAI R1 / Root – Should not-configured stay for a missing transport? Kept. P1 #deliver returns not-configured before it reads the ledger, so no event and no root-bound lookup is touched. Moving the root check in front of it would change P1 behaviour without a security gain (D1), and on V1 the check does not apply anyway. The test pins this with a rootless store without a transport.

NICHT ABGEDECKT

  • Progress, patches, ideas and question import are not composed. The unchanged frozen store.test.mjs and sender.test.mjs run only in the scratch composition.
  • Server lifecycle (startup drain, backoff, timers), webhook signing, HTTP routes and CLI notify are DR-06 and not tested here.
  • No ENV default for the root id (DR-06), no UI, no live switch, no usability claim. F-01 and R661-02 stay open in the DR-03 parent acceptance.
  • Gate block of this run: Linux half only. The #[cfg(windows)] tests (run_in_pty, npm_shim, ConPTY, DPAPI) and the Windows arms of clippy are not covered. Browser smoke, frontend build and workflow gates run only in lane linux. External services are not checked by any gate. The Windows verdict comes from the merge queue.

🤖 Generated with Claude Code

…04b)

Move pending(), inbox() and flushNotifications()/#deliver from the
frozen P1 DeskStore (lines 515-569) into tools/denkraum/store/delivery.mjs
as DeliveryStore extends AnswerStore. The code equals the split proof's
delivery.mjs lines 108-162 after two renames: this[CHANGE]( to
this.change( and this.io.rootAgentId to the injected this.rootAgentId (D2).

The delivery id stays the event id until a queued delivery is older than
seven days; a failed attempt keeps a fixed error code and no transport
detail. On V2 every receipt lookup fails closed with 503 while no root
agent id is configured. No new product logic, no HTTP, ENV or UI.

Test-First: tools/denkraum/store/delivery.test.mjs::V2 pending inbox and delivery fail closed without a configured root agent id

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…69-O1)

OpenAI R1 on #669 found that pending(), inbox() and flushNotifications()
only failed closed once an event reached receiptFor(). On a V2 ledger
without active events, or with flushNotifications('other-event'), they
answered [] / empty lists / 'empty' without a configured root agent id.

All three paths now check the root id on V2 right after reading the
state, before any event is filtered. V1 stays unchanged (no receipts).
A missing transport still returns 'not-configured' first: P1 returns it
before reading the ledger, so no root-bound lookup runs (D1).

Test-First: tools/denkraum/store/delivery.test.mjs::V2 reads and delivery refuse a missing root agent id even when no event needs a receipt check (R669-O1)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Base automatically changed from claude/dr-04-denkraum-answers to main October 8, 2026 16:04
@Cuarroc
Cuarroc marked this pull request as ready for review October 8, 2026 16:15
@Cuarroc Cuarroc added the review-ok Pipeline: review disposition complete, no open high finding label Oct 8, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-08T16:20:53.507343Z dc34cae Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dc34cae381

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const ref = eligible.find(e => e.content === a).eventRef;
if (!Object.hasOwn(a, 'webhookDelivery')) a.webhookDelivery = { status: 'pending', attempts: 0, lastAttemptAt: null, queuedAt: null, lastError: null };
if (a.webhookDelivery?.status === 'queued') {
a.webhookDelivery.status = 'pending'; a.webhookDelivery.deliveryId = randomUUID(); a.webhookDelivery.queuedAt = null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Koordiniere die Erneuerung der Zustellungs-ID pro Ereignis

Wenn zwei DeliveryStore-Instanzen oder Prozesse dasselbe seit sieben Tagen als queued markierte Ereignis gleichzeitig abarbeiten, lesen beide den alten Zustand und erzeugen hier unabhängig voneinander neue UUIDs. Dadurch gehen zwei notifyEvent-Aufrufe mit unterschiedlichen deliveryId-Werten für dasselbe Ereignis heraus; die vorgesehene Deduplizierung durch Root anhand der Zustellungs-ID kann dann nicht greifen und das Ereignis kann doppelt verarbeitet werden. Die Erneuerungs-ID muss deterministisch sein oder interprozesssicher genau einmal persistiert werden, bevor ein Sender zustellt.

Useful? React with 👍 / 👎.

requireRoot(state, this.rootAgentId); const at = this.io.clock();
const eligible = activeEvents(state).filter(e => (eventId === undefined || e.eventRef.eventId === eventId) && !receiptFor(state, e, this.rootAgentId));
const a = eligible.map(e => e.content).filter(a =>
(a.webhookDelivery?.status !== 'queued' || isoTime(a.webhookDelivery.queuedAt) && at - Date.parse(a.webhookDelivery.queuedAt) >= 7 * 86400000))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Lehne beschädigte Queue-Zeitstempel ab

Wenn ein gespeicherter webhookDelivery-Eintrag den Status queued, aber keinen gültigen queuedAt-Zeitstempel enthält, liefert isoTime(...) hier false und der Filter verwirft das Ereignis still. Da validateState die Outbox-Felder nicht prüft, wird das Ledger weiterhin erfolgreich gelesen und jeder spätere flushNotifications()-Aufruf meldet empty; die Antwort wird dadurch dauerhaft nicht zugestellt, statt einen 503-Fehler oder einen reparierbaren Pending-Zustand zu erzeugen. Der Queue-Eintrag sollte vor der Eignungsfilterung validiert werden.

Useful? React with 👍 / 👎.

this.#notificationQueue = work.catch(() => {}); return work;
}
async #deliver(eventId) {
if (typeof this.io.notifyEvent !== 'function') return { status: 'not-configured' };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prüfe die Root-ID vor dem Transport-Fallback

Bei einem V2-Ledger ohne konfigurierte rootAgentId und zugleich ohne notifyEvent kehrt #deliver hier vor dem Lesen des Ledgers und vor requireRoot mit dem normalen Status not-configured zurück. Damit wird die fehlende Root-Bindung gerade in der üblichen Konfiguration mit deaktivierten Benachrichtigungen nicht wie vom D2-Vertrag in docs/PLAN.md:58-60 gefordert mit 503 abgelehnt, während derselbe Aufruf mit vorhandenem Transport korrekt fehlschlägt. Die V2-Root-Prüfung muss vor diesem Rückgabepfad stattfinden, ohne das V1-Verhalten zu ändern.

Useful? React with 👍 / 👎.

@mergify mergify Bot mentioned this pull request Oct 8, 2026
30 of 46 tasks
@mergify mergify Bot added the queued label Oct 8, 2026
@mergify
mergify Bot merged commit 963a34f into main Oct 8, 2026
8 checks passed
@mergify
mergify Bot deleted the claude/dr-04b-denkraum-delivery branch October 8, 2026 16:35
@mergify

mergify Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Merge Queue Status

  • ✅ Entered queue — 2026-10-08 16:22 UTC · Rule: default · triggered by merge protections
  • ✅ Checks passed · on draft merge queue: checking #669 on main (ec4ba35) #699
  • ✅ Merged — 2026-10-08 16:35 UTC · at 963a34f3ed7fa7d55ffdcfa0404cb701aa7fdfb8 · merge

This pull request spent 13 minutes 38 seconds in the queue, including 13 minutes 20 seconds running CI.

Required conditions to merge
  • check-success = gates (linux)
  • check-success = gates (windows)
  • check-success = red-first
  • github-review-approved [🛡 GitHub repository ruleset rule main-pr-required-checks]
  • label != do-not-merge
  • any of [🛡 GitHub repository ruleset rule main-pr-required-checks]:
    • check-success = @github-actions/gates (linux)
    • check-neutral = @github-actions/gates (linux)
    • check-skipped = @github-actions/gates (linux)
  • any of [🛡 GitHub repository ruleset rule main-pr-required-checks]:
    • check-success = @github-actions/gates (windows)
    • check-neutral = @github-actions/gates (windows)
    • check-skipped = @github-actions/gates (windows)
  • any of [🛡 GitHub repository ruleset rule main-pr-required-checks]:
    • check-success = @github-actions/red-first
    • check-neutral = @github-actions/red-first
    • check-skipped = @github-actions/red-first
  • any of [🛡 GitHub repository ruleset rule main-pr-required-checks]:
    • check-neutral = @mergify/Mergify Merge Protections
    • check-skipped = @mergify/Mergify Merge Protections
    • check-success = @mergify/Mergify Merge Protections

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review-ok Pipeline: review disposition complete, no open high finding

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant