feat(system): typed intent seam between mutators and listeners (ADR-049)#325
Merged
Conversation
Refine ADR-023's row-as-intent pattern by making the mutator↔listener
payload an explicit, shared, typed view both sides import — instead of a
bare status string re-spelled at five sites and a hand-rolled content_json
parse duplicated across two daemon listeners.
New `@friday/shared/sync/intents.ts` (node-free):
- INTENT_STATUS: the 11 transient status tokens, centralized. Values are
frozen (keyed by in-flight rows + DB CHECK + trigger predicate), so this
moves the declaration, not the strings — no migration.
- buildUserMessageContent / parseUserMessageContent: the typed view over a
user block's content_json. The byte-identical JSON.parse+regex block is
removed from dispatch-listener and resume-listener; a {ok,content}
discriminator preserves each side's distinct behavior (dispatch forks
empty, resume bails corrupt) with one parser.
All seven side-effect mutators and their listeners (dispatch, resume,
abort, cancel, archive, scheduler, memory) adopt the const. Apps remain
the documented next adopter; updateSettings is the documented whole-row-
reconcile exception. Idempotency (status precondition + atomic-claim
UPDATE...WHERE status=token) is untouched.
Contract tests (intents.test.ts): build↔parse round-trip, token↔live-DB-
CHECK membership, and per-mutator token writes. Daemon-layer tests pin the
dispatch-forks-empty vs resume-bails-corrupt divergence at the layer it
lives in (the jsonb null literal is the only prod-reachable ok:false input
given content_json is NOT NULL and rowFromDb re-stringifies).
Docs: add ADR-049; fix the ADR-023 catalog drift (sendUserMessage is
pending → queued|complete, never the documented `dispatched`).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PD86tp8FdJ12DgDoMT6UaQ
The 6 unit configs already set testTimeout: 20_000 for cold-start headroom
but left hookTimeout at the 10s default. A `beforeAll` that runs
`createTestDb` (scratch Postgres + full migrations) plus heavy dynamic
imports — e.g. api/evolve-dreaming.test.ts:167 — intermittently exceeds 10s
on a cold CI runner and fails the whole file ("Hook timed out in 10000ms"),
even though it runs in ~3s locally. Mirror the testTimeout mitigation on the
hook path across all six unit configs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PD86tp8FdJ12DgDoMT6UaQ
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Refines ADR-023 (row-as-intent) — it does not reopen it. The seam stays the Postgres row +
NOTIFY; nointentstable is introduced. The friction it fixes is narrower: the payload crossing the@friday/sharedmutator ↔services/daemonlistener boundary was implicit and untyped — a barestatusstring re-spelled at five sites, plus a byte-identical hand-rolledcontent_jsonparse duplicated across two listeners. Nothing forced the two sides to agree (the documentedsendUserMessagepostcondition had already drifted from the code).What changed
New
packages/shared/src/sync/intents.ts(node-free; imported by both the client-safesyncsurface and the daemon barrel):INTENT_STATUS— the 11 transient status tokens, centralized. Values frozen (keyed by in-flight rows + DB CHECK + trigger predicate) → this moves the declaration, not the strings, so no migration.buildUserMessageContent/parseUserMessageContent— the typed view over a user block'scontent_json. The duplicatedJSON.parse+regex block is removed fromdispatch-listenerandresume-listener; a{ ok, content }discriminator preserves each side's distinct behavior (dispatch forks-empty, resume bails-corrupt) with one parser.Adoption: all 7 side-effect mutators + their listeners (dispatch, resume, abort, cancel, archive, scheduler, memory). Apps remain the documented next adopter;
updateSettingsis the documented whole-row-reconcile exception. Idempotency (status precondition + atomic-claimUPDATE … WHERE status=token) is untouched.Tests (
intents.test.ts+ daemon-layer): build↔parse round-trip, token↔live-DB-CHECK membership, per-mutator token writes, and the dispatch-forks-empty vs resume-bails-corrupt divergence pinned at the layer it lives in.Docs: add ADR-049; fix the ADR-023 catalog drift (
sendUserMessageispending → queued|complete, never the documenteddispatched).How it was built
Multi-agent (ultracode): a 10-agent inventory mapped every seam/token/CHECK/ADR constraint; a 9-agent find→verify adversarial review surfaced 2 real issues (a misleading docstring and a test that named the listener divergence but only tested the parser) — both fixed, including the daemon-layer branch tests.
Verification
svelte-checkclean · prettier clean.pg-provisiontests (host DB on a divergent migration chain — CI authoritative; host row untouched).🤖 Generated with Claude Code
https://claude.ai/code/session_01PD86tp8FdJ12DgDoMT6UaQ