flower
/
All briefs
complete feedback flower
from feedback #79 · Decisions are pull-only: an operator answer reaches...

Feedback #79: Decisions are pull-only: an operator answer reaches the assigned daemon only on its next ~15m poll (not a recall_decisions bug). Idea: notify/wake the assignee on answer, not poll-only.

canonical · plan

Spec

markdown

hand-off · dispatch

Dispatch

Auto-dispatch

when it reaches planned

Design-loop

design pass before build

This brief is complete — dispatch is closed.

#93 done fresh flower · flower/179-decisions-notify-assignee
agent: claude 3 scratchpads
You are being dispatched from flower Brief #179: Feedback #79: Decisions are pull-only: an operator answer reaches the assigned daemon only on its next ~15m poll (not a recall_decisions bug). Idea: notify/wake the assignee on answer, not poll-only.

Recall pointer:
- Use recall_brief with id 179 for the full folder if you need provenance.

Target:
- project: flower (/Users/mikeferrara/Documents/code/flower)
- branch: flower/179-decisions-notify-assignee
- worktree: not specified
- kind: fresh

Current brief spec:
## Objective — **HIGH PRIORITY** (operator, 2026-07-04)
When an operator answers a decision (`decision_answer`), **proactively wake/notify the assigned daemon** so it picks up the answer promptly — instead of waiting up to a full poll interval (~15m; longer when dormant) for its next `recall_decisions`. This is the #1 adoption gap for the decisions feature (surfaced dogfooding #170, decision #33) and the operator asked to prioritize it.

## Problem (grounded)
- Decisions are **pull-only**: the assignee only sees an answer on its next `recall_decisions` poll. `released_at` is set **synchronously** on answer (so the answer is available immediately) — but the daemon doesn't know to look until its next heartbeat. Result: up to ~15m (or the dormant interval) latency; reads to the operator as "I answered but it didn't see it" (the exact #33 incident).
- **PR-2 already emits decision broadcasts/events** — but nothing turns an answer into a **wake** of the assigned daemon.

## Change — wake-on-answer
On `decision_answer` (release), notify the **assigned daemon** (`assigned_to` = canonical `{project}-{role}`):
1. **Resolve the assignee's live process** — map `assigned_to` → the live `DaemonAgent` for that project+role (roster) → its `solo_process_id`.
2. **Deliver a wake** via the established coordination path — enqueue a `daemon_poke` / coordination signal (`CoordinationQueue`) to the assignee (drained on its next heartbeat), or, for low latency, mirror the orchestrator poke: `mcp__solo__timer_set(delay_ms=1000)` to its `solo_process_id`. Wake body: "you have an answered decision — run `recall_decisions`." Prefer the **queued/coordination path** (durable, ordered) per the queued-by-default model; reserve direct `timer_set` for the urgent case.
3. **No live assignee → no-op** (don't fail the answer; the decision stays pullable and is picked up on the daemon's next poll / when it comes online).
4. **Reuse the PR-2 decision broadcast** rather than adding a parallel mechanism where possible.

## Scope / non-goals
- Wakes only the **assigned** daemon, not a broadcast to all.
- Does **not** change pull semantics (`recall_decisions` / `decision_ack` unchanged) — this is a push *nudge* on top.
- Idempotent: repeat answers / re-releases must not spam the assignee.

## Acceptance
- Answering a decision assigned to a **live** daemon enqueues a wake/poke to that daemon's `solo_process_id` within ≤ one coordination drain (not a full poll interval); a test asserts the wake is enqueued on `decision_answer`.
- **No live assignee** → answer still succeeds, no wake, decision remains pullable.
- `php artisan test` green + `./vendor/bin/pint`. `Brief: #179` trailer.

## Priority / provenance
**HIGH priority** (operator, 2026-07-04). Promoted from feedback **#79** (idea) → this is an **operator-gated** brief; it needs operator **approval** to release for dispatch (approve on `/feedback` or `/briefs`). Surfaced dogfooding **#170** (decision #33). **Coordinate with #155** (its item 4 "wake-on-operator-action" is the same primitive, broader scope) and consider filing as a **#95** decisions child PR; reuse **PR-2** decision broadcasts.

Recent/key trace events:
[1] participant_joined flower-ops: (no body)
[2] note_added flower-ops: Feedback #79
Authority: OPERATOR APPROVAL REQUIRED
Funnel: A - operator approved brief
Gate: this brief must not be dispatchable until the operator approves it.
Kind: idea
Source: flower-refine

Summary:
Decisions are pull-only: an operator answer reaches the assigned daemon only on its next ~15m poll (not a recall_decisions bug). Idea: notify/wake the assignee on answer, not poll-only.

Detail:
Surfaced dogfooding #170 (decision spine). Operator answered decision #33 (confirm → approve) at 00:45:36 and expected the assigned daemon (flower-refine) to have it; the daemon reported it unanswered on its prior poll and the operator flagged a possible bug.

DEBUG → NOT a bug:
- `decisions` row #33: `answered_at` == `released_at` == 2026-07-04T00:45:36, `assigned_to` = `flower-refine` → release is SYNCHRONOUS with the answer (no delayed-release); `Decision::scopeAwaitingAckFor` filters released + assigned + unacked from the live DB (no cache/Meili lag).
- `recall_decisions(actor_ref=flower-refine)` AND `recall_decisions(actor_ref=flower-refine, project=flower)` both return #33 correctly once past the answer time. (My first instinct that the `project` filter dropped project-scoped decisions was wrong — re-tested, both work.)
- Root cause: the assignee is a POLLING daemon (recall_decisions on its ~15m heartbeat; longer when dormant). Its poll ran ~4 min BEFORE the 00:45:36 answer → correctly returned 0; the next poll delivered it. The operator's "I answered before the last poll" was off by the poll gap.

SUGGESTION (idea): decisions are pull-only, so an assigned daemon can be up to a full poll interval behind on answers, which reads to the operator as "I answered but it didn't see it." On `decision_answer`, proactively WAKE/notify the assigned daemon — reuse the PR-2 decision broadcast, or enqueue a poke / coordination signal to the assignee's `solo_process_id` — so answers are picked up promptly instead of on the next poll. Ties to #157/#155 (wake-on-operator-action) and the decisions dogfood (#170).

MINOR secondary snag observed while filing this: `flower_feedback.summary` has a 255-char cap that rejected two longer drafts (no partial-accept / truncation hint) — low priority, just noting.

Context JSON:
{
    "tool": "recall_decisions",
    "related": [
        "#170",
        "#157",
        "#155"
    ],
    "verdict": "not a bug - poll latency; release is synchronous",
    "answered_at": "2026-07-04T00:45:36",
    "assigned_to": "flower-refine",
    "decision_id": 33,
    "released_at": "2026-07-04T00:45:36"
}
[3] link_added flower-ops: (no body)
[4] participant_joined flower-refine: (no body)
[5] plan_proposed flower-refine: ## Objective — **HIGH PRIORITY** (operator, 2026-07-04)
When an operator answers a decision (`decision_answer`), **proactively wake/notify the assigned daemon** so it picks up the answer promptly — instead of waiting up to a full poll interval (~15m; longer when dormant) for its next `recall_decisions`. This is the #1 adoption gap for the decisions feature (surfaced dogfooding #170, decision #33) and the operator asked to prioritize it.

## Problem (grounded)
- Decisions are **pull-only**: the assignee only sees an answer on its next `recall_decisions` poll. `released_at` is set **synchronously** on answer (so the answer is available immediately) — but the daemon doesn't know to look until its next heartbeat. Result: up to ~15m (or the dormant interval) latency; reads to the operator as "I answered but it didn't see it" (the exact #33 incident).
- **PR-2 already emits decision broadcasts/events** — but nothing turns an answer into a **wake** of the assigned daemon.

## Change — wake-on-answer
On `decision_answer` (release), notify the **assigned daemon** (`assigned_to` = canonical `{project}-{role}`):
1. **Resolve the assignee's live process** — map `assigned_to` → the live `DaemonAgent` for that project+role (roster) → its `solo_process_id`.
2. **Deliver a wake** via the established coordination path — enqueue a `daemon_poke` / coordination signal (`CoordinationQueue`) to the assignee (drained on its next heartbeat), or, for low latency, mirror the orchestrator poke: `mcp__solo__timer_set(delay_ms=1000)` to its `solo_process_id`. Wake body: "you have an answered decision — run `recall_decisions`." Prefer the **queued/coordination path** (durable, ordered) per the queued-by-default model; reserve direct `timer_set` for the urgent case.
3. **No live assignee → no-op** (don't fail the answer; the decision stays pullable and is picked up on the daemon's next poll / when it comes online).
4. **Reuse the PR-2 decision broadcast** rather than adding a parallel mechanism where possible.

## Scope / non-goals
- Wakes only the **assigned** daemon, not a broadcast to all.
- Does **not** change pull semantics (`recall_decisions` / `decision_ack` unchanged) — this is a push *nudge* on top.
- Idempotent: repeat answers / re-releases must not spam the assignee.

## Acceptance
- Answering a decision assigned to a **live** daemon enqueues a wake/poke to that daemon's `solo_process_id` within ≤ one coordination drain (not a full poll interval); a test asserts the wake is enqueued on `decision_answer`.
- **No live assignee** → answer still succeeds, no wake, decision remains pullable.
- `php artisan test` green + `./vendor/bin/pint`. `Brief: #179` trailer.

## Priority / provenance
**HIGH priority** (operator, 2026-07-04). Promoted from feedback **#79** (idea) → this is an **operator-gated** brief; it needs operator **approval** to release for dispatch (approve on `/feedback` or `/briefs`). Surfaced dogfooding **#170** (decision #33). **Coordinate with #155** (its item 4 "wake-on-operator-action" is the same primitive, broader scope) and consider filing as a **#95** decisions child PR; reuse **PR-2** decision broadcasts.
[6] participant_joined operator:mike: (no body)
[7] note_added operator:mike: Operator approved this feedback-born brief for dispatch.
[8] status_change operator:mike: (no body)
[9] link_added flower-ops: (no body)
[10] link_added flower-ops: (no body)
[11] link_added flower-ops: (no body)

Recommended linked context:
{
    "todos": [],
    "scratchpads": [
        {
            "id": 333,
            "solo_scratchpad_id": "1005",
            "name": "flower-ops — triage log",
            "archived": false,
            "revision": 268
        },
        {
            "id": 382,
            "solo_scratchpad_id": "1074",
            "name": "Feedback #87 fix-spec",
            "archived": false,
            "revision": 2
        },
        {
            "id": 384,
            "solo_scratchpad_id": "1076",
            "name": "ESCALATION — embed-stage OOM cluster degrading the pipeline",
            "archived": false,
            "revision": 2
        }
    ]
}

Execution notes:
- Treat the brief as the source of truth.
- Keep work scoped to this dispatch request.
- Use brief_append / brief_update_status when reporting material progress; as your final dispatched-worker step, call brief_dispatch_complete with dispatch_request_id (or brief_id) and actor_ref.
- Codex workers should verify mutating Flower tools with tool_search query `brief_append brief_dispatch_complete flower_feedback` (limit 20) when tool availability is in doubt; report raw SEE/LOAD vs NOT visible instead of silently using local fallbacks.
- Add a git commit trailer `Brief: #179` to every commit for this brief so flower can exact-link commits back to the brief.

provenance · append-only

Trace

live
or paste a screenshot uploading…
  1. link added 1d ago
    agent · system:commit-trailer
  2. link added 1d ago
    agent · system:commit-trailer
  3. participant joined 1d ago
    system · system:commit-trailer
  4. merged 1d ago

    Merged to master by flower-orchestrator (daemon 29). Merge commit 2115bec (worker commit 7d3c258, branch flower/179-decisions-notify-assignee). Wake-on-answer for decisions: CoordinationQueue.enqueueDecisionWake() (new KIND_DECISION_WAKE, deduped by decision-id fingerprint) fired from DecisionService::release() — resolves the canonical assigned_to ({project}-{role}) to a live daemon with a solo_process_id (mirrors FeedbackPromotionService::liveOrchestrator) and enqueues the wake on the SAME durable DaemonSignal queue the orchestrator drains via recall_signals (correct queued pattern per charter, not a direct interjection). Graceful: unset/operator:/system: assignee or no live daemon → no-op (decision stays pullable); queue failure caught+logged; never throws. Hooks release() so gated-set + withdraw releases wake each member too; pull semantics unchanged. Additive on #178 (clean merge, no conflict). DecisionWakeTest 5 cases. Full suite green on MAIN: 995 tests / 993 passed / 2 skipped / 0 failed. Worker also filed dogfooding nit mcp_issue #106 (recall_brief validation error doubles its sentence).

    agent · flower-orchestrator
  5. status change 1d ago
    agent · flower-179-decisions-notify-worker
  6. dispatched 1d ago

    Dispatch request #93 marked done.

    agent · flower-179-decisions-notify-worker
  7. note added 1d ago

    Built wake-on-answer for decisions (push nudge over poll-only). Branch: flower/179-decisions-notify-assignee (off master). SHA: 7d3c258. What changed: - app/Services/Daemons/CoordinationQueue.php: new KIND_DECISION_WAKE const + enqueueDecisionWake(DaemonAgent, decisionId, header, requestedBy). Mirrors enqueuePoke/KIND_ROUTE_FEEDBACK — rides the same durable, orchestrator-drained DaemonSignal queue (recall_signals surfaces it kind-agnostically; the payload `message` tells the orchestrator to deliver "run recall_decisions"). The decision id in the payload makes wakes for different decisions distinct while fingerprint-deduping repeat wakes for the same one → assignee never spammed. - app/Services/Briefs/DecisionService.php: injected CoordinationQueue. release() (the single choke point where released_at flips + DecisionReleased fires) now calls wakeAssignee(). resolveLiveAssignee() maps the canonical assigned_to ({project}-{role}) → a DaemonAgent that is status=Live AND has a solo_process_id (mirrors FeedbackPromotionService::liveOrchestrator), tolerating a legacy/raw ref via DaemonActorRef::normalize. Fires the wake with requested_by = the answerer. - Graceful degrade (never throws): unset / operator: / system: assignee, or no live+reachable daemon → no-op (decision stays pullable on the next poll). Any wake/queue failure is caught + logged, never failing the answer. - Additive on #178's broadcastAwaiting() (kept intact). Pull semantics (recall_decisions / decision_ack) unchanged — this is a push nudge on top. Because it hooks release(), gated-set releases and withdraw-triggered set releases wake each member's assignee too. Tests: tests/Feature/Decisions/DecisionWakeTest.php (5 cases) — wake fires for a live assignee (asserts kind/target/decision_id/solo_process_id/requested_by); no-op when no daemon registered; no-op for stale or processless daemon; idempotent across repeat answers; no-op for an operator-pinned assignee. Full suite green: MEILISEARCH_KEY=LARAVEL-HERD ~/bin/php artisan test → 992 passed / 1 pre-existing skip. pint clean on changed files. Not merged — left for flower-orchestrator to review + merge.

    agent · flower-179-decisions-notify-worker
  8. participant joined 1d ago
    system · flower-179-decisions-notify-worker
  9. dispatched 1d ago

    Dispatch request #93 queued for flower.

    agent · flower-orchestrator
  10. status change 1d ago
    agent · flower-orchestrator
  11. participant joined 1d ago
    system · flower-orchestrator
  12. link added 1d ago
    agent · flower-ops
  13. link added 1d ago
    agent · flower-ops
  14. link added 1d ago
    agent · flower-ops
  15. status change 1d ago
    agent · operator:mike
  16. note added 1d ago

    Operator approved this feedback-born brief for dispatch.

    operator · operator:mike
  17. participant joined 1d ago
    system · operator:mike
  18. plan proposed 1d ago

    ## Objective — **HIGH PRIORITY** (operator, 2026-07-04) When an operator answers a decision (`decision_answer`), **proactively wake/notify the assigned daemon** so it picks up the answer promptly — instead of waiting up to a full poll interval (~15m; longer when dormant) for its next `recall_decisions`. This is the #1 adoption gap for the decisions feature (surfaced dogfooding #170, decision #33) and the operator asked to prioritize it. ## Problem (grounded) - Decisions are **pull-only**: the assignee only sees an answer on its next `recall_decisions` poll. `released_at` is set **synchronously** on answer (so the answer is available immediately) — but the daemon doesn't know to look until its next heartbeat. Result: up to ~15m (or the dormant interval) latency; reads to the operator as "I answered but it didn't see it" (the exact #33 incident). - **PR-2 already emits decision broadcasts/events** — but nothing turns an answer into a **wake** of the assigned daemon. ## Change — wake-on-answer On `decision_answer` (release), notify the **assigned daemon** (`assigned_to` = canonical `{project}-{role}`): 1. **Resolve the assignee's live process** — map `assigned_to` → the live `DaemonAgent` for that project+role (roster) → its `solo_process_id`. 2. **Deliver a wake** via the established coordination path — enqueue a `daemon_poke` / coordination signal (`CoordinationQueue`) to the assignee (drained on its next heartbeat), or, for low latency, mirror the orchestrator poke: `mcp__solo__timer_set(delay_ms=1000)` to its `solo_process_id`. Wake body: "you have an answered decision — run `recall_decisions`." Prefer the **queued/coordination path** (durable, ordered) per the queued-by-default model; reserve direct `timer_set` for the urgent case. 3. **No live assignee → no-op** (don't fail the answer; the decision stays pullable and is picked up on the daemon's next poll / when it comes online). 4. **Reuse the PR-2 decision broadcast** rather than adding a parallel mechanism where possible. ## Scope / non-goals - Wakes only the **assigned** daemon, not a broadcast to all. - Does **not** change pull semantics (`recall_decisions` / `decision_ack` unchanged) — this is a push *nudge* on top. - Idempotent: repeat answers / re-releases must not spam the assignee. ## Acceptance - Answering a decision assigned to a **live** daemon enqueues a wake/poke to that daemon's `solo_process_id` within ≤ one coordination drain (not a full poll interval); a test asserts the wake is enqueued on `decision_answer`. - **No live assignee** → answer still succeeds, no wake, decision remains pullable. - `php artisan test` green + `./vendor/bin/pint`. `Brief: #179` trailer. ## Priority / provenance **HIGH priority** (operator, 2026-07-04). Promoted from feedback **#79** (idea) → this is an **operator-gated** brief; it needs operator **approval** to release for dispatch (approve on `/feedback` or `/briefs`). Surfaced dogfooding **#170** (decision #33). **Coordinate with #155** (its item 4 "wake-on-operator-action" is the same primitive, broader scope) and consider filing as a **#95** decisions child PR; reuse **PR-2** decision broadcasts.

    agent · flower-refine
  19. participant joined 1d ago
    system · flower-refine
  20. link added 1d ago
    agent · flower-ops
  21. note added 1d ago

    Feedback #79 Authority: OPERATOR APPROVAL REQUIRED Funnel: A - operator approved brief Gate: this brief must not be dispatchable until the operator approves it. Kind: idea Source: flower-refine Summary: Decisions are pull-only: an operator answer reaches the assigned daemon only on its next ~15m poll (not a recall_decisions bug). Idea: notify/wake the assignee on answer, not poll-only. Detail: Surfaced dogfooding #170 (decision spine). Operator answered decision #33 (confirm → approve) at 00:45:36 and expected the assigned daemon (flower-refine) to have it; the daemon reported it unanswered on its prior poll and the operator flagged a possible bug. DEBUG → NOT a bug: - `decisions` row #33: `answered_at` == `released_at` == 2026-07-04T00:45:36, `assigned_to` = `flower-refine` → release is SYNCHRONOUS with the answer (no delayed-release); `Decision::scopeAwaitingAckFor` filters released + assigned + unacked from the live DB (no cache/Meili lag). - `recall_decisions(actor_ref=flower-refine)` AND `recall_decisions(actor_ref=flower-refine, project=flower)` both return #33 correctly once past the answer time. (My first instinct that the `project` filter dropped project-scoped decisions was wrong — re-tested, both work.) - Root cause: the assignee is a POLLING daemon (recall_decisions on its ~15m heartbeat; longer when dormant). Its poll ran ~4 min BEFORE the 00:45:36 answer → correctly returned 0; the next poll delivered it. The operator's "I answered before the last poll" was off by the poll gap. SUGGESTION (idea): decisions are pull-only, so an assigned daemon can be up to a full poll interval behind on answers, which reads to the operator as "I answered but it didn't see it." On `decision_answer`, proactively WAKE/notify the assigned daemon — reuse the PR-2 decision broadcast, or enqueue a poke / coordination signal to the assignee's `solo_process_id` — so answers are picked up promptly instead of on the next poll. Ties to #157/#155 (wake-on-operator-action) and the decisions dogfood (#170). MINOR secondary snag observed while filing this: `flower_feedback.summary` has a 255-char cap that rejected two longer drafts (no partial-accept / truncation hint) — low priority, just noting. Context JSON: { "tool": "recall_decisions", "related": [ "#170", "#157", "#155" ], "verdict": "not a bug - poll latency; release is synchronous", "answered_at": "2026-07-04T00:45:36", "assigned_to": "flower-refine", "decision_id": 33, "released_at": "2026-07-04T00:45:36" }

    agent · flower-ops
  22. participant joined 1d ago
    system · flower-ops

epic · dependencies

Relationships

epic parent

depends on

No dependencies — dispatchable once planned.

agents · waves

Participants

  • flower-ops participant · active
  • flower-refine participant · active
  • operator:mike participant · active
  • flower-orchestrator participant · active
  • flower-179-decisions-notify-worker participant · active
  • system:commit-trailer participant · active

trace · graph

Links

  • Commit #3990 execution
  • Commit #3991 execution
  • Scratchpad #384 execution
  • Scratchpad #382 execution
  • Scratchpad #333 execution
  • Feedback #79 seed

scope

Projects

  • flower · primary

dogfood · read-only

Agent’s-eye view

The literal recall_brief payload an agent gets — same service path as the MCP tool.