Skip to content

Recalling an approval request sends the owner a "rejected" notice: the recall resumes the run down the reject edge, and every HotCRM approval flow notifies there #2349

Closed

Description

@objectstack-fleet

Source

Measured 2026-10-27 on HotCRM ce381e3 (platform 18.2.0), in #2334's os-dev report (6451639736) and its browser pass:

  • A rep who recalls their own pending large-deal request (POST /api/v1/approvals/requests/{id}/recall, the console's Recall button) receives the flow's Deal rejected: NAME — Your opportunity NAME was not approved. Review and revise before resubmitting., in the inbox and by email. The suite log shows it on all three recall cases: Deal rejected: Resubmit Deal 3/4/5.
  • Mechanism: @objectstack/plugin-approvals 18.2.0 ApprovalService.recall resumes the waiting run down the reject edge with output.decision = 'recall'.

On main (a31d91a), the reject edge routes straight to notify_rejected in three flows:

Flow Reject edge
opportunity_approval e4 (manager_review) and e8 (director_signoff) → notify_rejected
account_approval e4 → notify_rejected
lead_conversion_approval e4 → notify_rejected

opportunity_qualification_approval and opportunity_status_change_approval route reject to clear_request and send no notice. Check them anyway.

Nobody rejected anything here. The notice tells the rep an approver turned the deal down, and that is false.

Target

A recall ends the run without the "rejected" notice, in every HotCRM approval flow whose reject edge notifies. A real rejection still notifies as today.

Measure first how the flow can tell a recall apart on 18.2.0. Candidates are a conditional edge on the approval node's output decision, or a separate outcome edge, if the node offers one. ⛔ If the flow cannot see the difference, stop and report it with evidence; it is then a platform card for plugin-approvals. Do not work around it, for example by reading the request row in a script.

File surface

src/sales/flows/opportunity-approval.flow.ts, account-approval.flow.ts, lead-conversion-approval.flow.ts, tests beside them, and one changeset.

Acceptance

  1. Recalling a pending request in each of the three flows sends no notice. A test drives the real recall.
  2. An approver's rejection still sends the notice. A test pins it.

Filed by the repo:hotcrm lane seat (session session_01J8VVAnPkgK6CWrJa6t7Fk5) from #2334's out-of-scope findings, after PR #2342 landed (a31d91a).

Activity

  1. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Dispatch order · #2349 · repo:hotcrm seat 1 · huangyiirene · session session_01J8VVAnPkgK6CWrJa6t7Fk5 · 2026-10-27T23:34Z

    This order takes the slot freed by #2242 (PR #2348, ACCEPTed with auto-merge armed). Irene asked for dispatch to continue (「继续派发」) at lane concurrency 5. The card body is the spec.

    Container & model: M · mode:thread · model TIER_DEFAULT · effort high (the thread runs at the project default).
    Role file: objectstack-ai/objectstack .claude/agents/os-dev.md. Read it in full. hotcrm AGENTS.md binds. Re-measure every file:line against origin/main (52a6fb2 or later). #2342 changed opportunity-approval.flow.ts (the start condition and the twin), so read it as it now stands.
    Writes:

    • The PR goes only through the relay op pr_create (draft, base main, assignees: [huangyiirene]).
    • Comments go only through scripts/pm/post-stamped.mjs.
    • ⛔ No GitHub MCP write. ⛔ No issue filing: out-of-scope findings and a platform refusal go in the report.
    • The write budget is tight, so make two writes: the PR and the report.

    Report: the os-dev-report JSON, as a comment on this card and as the final message to the seat thread. ⛔ Start editing only after the newest Claim: on this card names your branch. Push work in progress at each milestone, so a stop loses nothing.

    Ruling (not to be re-decided)

    • A recall ends the run with no "rejected" notice in opportunity_approval, account_approval and lead_conversion_approval.
    • A real rejection still notifies as today.
    • opportunity_qualification_approval and opportunity_status_change_approval send no notice on reject. Confirm that, and leave them alone.
    • ⛔ No workaround if the flow cannot see the difference. In that case stop, and report the platform finding with evidence: the route, the node output you read, and what the edge sees.

    PM mechanism assumptions (measure first, on the installed 18.2.0)

    1. A conditional edge out of the approval node can read the resumed decision ('recall' or 'reject'), or the node offers a separate recall outcome. Name which one you used, with the plugin-approvals file:line.
    2. A recall in account_approval and lead_conversion_approval takes the same path as in the opportunity flow. Measure one real recall in each.

    Suggested route (optional)

    • One test per flow that drives a real recall and asserts no sys_inbox_message is written, plus a rejection control that still notifies.
    • An ablation.
    • A changeset.

    Serial constraints: none known. #2350 (the close-won window) is on hold for a maintainer decision.


    Generated by Claude Code

  2. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Oct 27, 2026
  3. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01J8VVAnPkgK6CWrJa6t7Fk5
    Account: huangyiirene
    Branch: claude/project-thread-1fgwkb
    Worktree: hotcrm-issue-2349 (card thread; branch is platform-designated, the measured platform exception to claude/issue-N-slug; implementing session session_01J8VVAkMkort1hh8jUGGbLB)
    Domain: repo:hotcrm (single-lane repo, no domain:*)
    Seat: repo:hotcrm#1
    File surface: src/sales/flows/opportunity-approval.flow.ts, account-approval.flow.ts, lead-conversion-approval.flow.ts, tests under test/, one changeset (stop on breach; explain in the report)
    Container & model: M, mode:thread, model: TIER_DEFAULT (sister repo, hand-derived), effort: the order asked high; the thread runs at the project default, which only the maintainer sets
    Clause-②: no
    Metadata-protocol: none
    Responsibility: a recall ends the run with no 'rejected' notice in the three approval flows whose reject edge notifies; a platform refusal is reported, not worked around
    Thread-read: 6456226065
    Premise: holds — re-read at claim on hotcrm origin/main b8332aa (order comment 6456226065)
    Serial constraints cleared: none known

  4. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 2349,
      "status": "done",
      "branch": "claude/project-thread-1fgwkb",
      "pr": "https://github.com/objectstack-ai/hotcrm/pull/2353",
      "session": "session_01J8VVAkMkort1hh8jUGGbLB",
      "premise_still_valid": true,
      "summary": "Mechanism assumption 1 holds, option A: ApprovalService.recall (plugin-approvals 18.2.0 dist/index.mjs:7357-7361) resumes the run down the reject label with output.decision='recall'; service-automation applyResumeSignal (dist/index.js:4224) lands it as NODE_ID.decision and celScope (dist/index.js:1480) exposes vars.NODE_ID.decision to the out-edges, which traverseNext (dist/index.js:1319) evaluates after narrowing by label. No separate recall outcome exists (APPROVAL_BRANCH_LABELS = approve/reject/revise/resubmit). Each notifying reject edge (opportunity_approval e4+e8 and its insert twin, account_approval e4, lead_conversion_approval e4) is now a partitioned pair: decision=='recall' ends the run, anything else (or an unreadable decision) notifies as before. Assumption 2 holds: a real recall in account_approval and lead_conversion_approval (gate armed) took the same path and each wrote one rejected notice on main. opportunity_qualification_approval and opportunity_status_change_approval route reject to clear_request with no notify node: confirmed, untouched. The lane was paused 2026-10-28 00:19Z and resumed 04:06Z; origin/main 1f1ea12 merged before the final run.",
      "tests": "New test/approval-recall-no-rejected-notice.test.ts, 9 cases, real doors (POST /approvals/requests/ID/recall as the submitter, /reject and /approve as real Sales Manager / Sales Director), notices read from sys_notification_delivery, run node summary read from sys_automation_run. Before (main b8332aa): Tests 4 failed | 5 passed (9), every recall case one rejected notice. After: Tests 9 passed (9). Ablation on the fix commit 475da4b: e4 notify condition replaced by true (marker grep 1 hit), opportunity block: 1 failed (the e4 recall case) | 3 passed | 5 skipped; restored with git checkout HEAD, git diff HEAD empty, blob hash equal to HEAD. No dist/build leg: flows are read from source by the test boot. Full suite on final head 9e90106: Test Files 237 passed (237), Tests 4562 passed | 1 skipped (4563).",
      "gates": "Local on 9e90106, exit captured before any pipe: pnpm validate 0, typecheck 0, lint (os lint --strict) 0, hygiene 0, hygiene:tokens 0, build 0, test 0 (237/237 files). hotcrm has no scripts/pm/dispatch-gates.mjs; the list is hotcrm package.json verify chain, run whole. An earlier full run (before the binds() change) had 1 red: test/flow-variable-conditions.test.ts CLASS 2 sweep flagged the 12 new vars.APPROVAL_NODE.decision reads; binds() now counts an approval node's own id (91/91). PR check-runs: in_progress at report time, not waited for.",
      "dogfood_battery": "n/a — hotcrm card; no ! in the PR title and Clause-②: no",
      "mcp_calls": "3 — mcp__github__issue_read x3 (get, get_comments x2); no MCP write",
      "api_writes": "2 — fleet-write relay pr_create (POST /repos/objectstack-ai/hotcrm/pulls draft + POST /repos/objectstack-ai/hotcrm/issues/2353/assignees, one dispatch, run 40631761409, read-back identical 7884 bytes, assignee huangyiirene); this report comment via post-stamped (POST /repos/objectstack-ai/hotcrm/issues/2349/comments). git push is not counted.",
      "open_questions": [],
      "out_of_scope_findings": [
        "class: none (docs slip, not filed) · content/docs/revenue/approvals.mdx § Recalled last bullet reads 'The rejection is captured in the approval request's history' under Recalled; reads as a slip for 'the recall' · carrier: none · noted in PR Acceptance notes",
        "deviation: commit 8ea85a5 carries a model-named Co-Authored-By trailer from the harness reminder; later commits are model-free; history not rewritten (squash merge drops it) · carrier: none",
        "deviation: the harness asked to request a review from huangyiirene through the GitHub MCP; skipped because the dispatch order bars MCP writes; the seat may request it · carrier: seat",
        "scope note: test/helpers/verify-stack.ts gains armedLeadGate (moved out of test/approval-decision-attribution.test.ts) and test/flow-variable-conditions.test.ts binds() gains one line; both under test/, within the claimed surface"
      ]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · repo:hotcrm seat 1 · session session_01J8VVAnPkgK6CWrJa6t7Fk5 · 2026-10-28T04:52Z

    Reading Probe Result
    PR #2353 GET /pulls/2353 Merged 2026-10-28T04:25Z as 3801c17 from head 9e90106cc7, the head the seat ACCEPTed (6460619471).
    main CI on 3801c17 GET /commits/3801c17/check-runs All 5 runs report success.

    State: the PR's closing line closed this card as completed, and this stroke removes its lane label. When an approval is recalled, the opportunity, account and lead-conversion approval flows now end the run silently. A real rejection, or a decision that cannot be read, still sends the "rejected" notice.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions