Skip to content

Campaign member: unsubscribing a member who had responded wipes the response (has_responded, response_date), and the campaign's response count counts by status, so the response rate drops on every unsubscribe #2365

Closed

Description

@objectstack-fleet

Ruled: Irene · 2026-10-29T02:08Z · marketing D4 → keep the response record: an unsubscribe does not erase the fact that the person responded.

Source

This is the #2320 browser pass, marketing track, decision D4. Irene ruled with 「同意」 at 2026-10-29T02:08Z in the HotCRM project thread, on the director-decision doc that recommended keeping the record. Filing gate ③ a task the maintainer directed.

A member who responded and then unsubscribes is moved responded → unsubscribed. Two sites then make the campaign forget that the response happened:

# Site (origin/main 353dace) Today
a src/marketing/objects/campaign_member.hook.ts, campaign_member_lifecycle. The clause at :253–262 starts with "Moving BACK out of a responded state …". Any move out of responded / converted clears has_responded and response_date. That includes a move to unsubscribed.
b src/marketing/objects/campaign.hook.ts:171, the campaign metric recompute num_responses counts members whose status is responded or converted. Even with (a) fixed, an unsubscribe still lowers the count and the response rate.

Target (ruling, not to be re-decided)

  • (a) A move from responded / converted to unsubscribed keeps has_responded: true and the original response_date. A move back to sent (the mis-click or re-send case the clause was written for) still clears both. If a future status means "never engaged", it follows sent. Today the options are sent, responded, converted and unsubscribed (campaign_member.object.ts:258–261), and bounced was removed.
  • (b) num_responses counts members with has_responded: true, so the response rate is "ever responded".
  • test/seed-consistency.test.ts:419 ("track the responded status exactly") becomes: responded/converted ⇒ has_responded; sent ⇒ not; unsubscribed may be either. Seeded unsubscribed members that had responded carry has_responded: true and a response_date.
  • ⛔ No change to who may unsubscribe a member, or to the status options.

Done when

  • Tests in test/ show each of these, and each goes red with its site reverted:
    • responded → unsubscribed keeps the flag and the date;
    • responded → sent clears them;
    • num_responses is unchanged by an unsubscribe and drops on a reset to sent.
  • The seed is consistent under the new rule.
  • The Marketing Manual (src/marketing/docs/crm_marketing_campaigns.md and its .zh.md) has one line saying a person who responded and later unsubscribed still counts as a response.
  • pnpm verify is green, and the PR includes a changeset.

Grade: priority:p2. A wrong figure on a campaign's response rate after normal use. Size: S.

Dedupe: the 25 open hotcrm issues, matched on has_responded, num_responses, unsubscribe and response rate. No match.

Filed by the repo:hotcrm lane seat (session session_01J8VVAnPkgK6CWrJa6t7Fk5) from Irene's D4 ruling.

Activity

  1. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Dispatch order · #2365 · repo:hotcrm seat 1 · huangyiirene · session session_01J8VVAnPkgK6CWrJa6t7Fk5 · 2026-10-29T02:15Z

    This card takes the lane's second slot, beside #2350 (p1). The card body is the spec. Irene's D4 ruling is final: keep the response record.

    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 (353dace or later).
    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 platform refusals go in the report.
    • The write budget is tight, so make two writes: the PR and the report.

    Report: the os-dev-report JSON, posted as a comment on this card and sent 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.

    Scope

    • (a) campaign_member_lifecycle: responded/converted → unsubscribed keeps has_responded and response_date. → sent still clears both.
    • (b) campaign.hook.ts metric recompute: num_responses counts has_responded: true.
    • The seed-consistency rule and seed rows, as the card says.
    • One line in the Marketing Manual (en + zh).
    • ⛔ No change to the status options or to who may unsubscribe.

    Tests (each must go red with its site reverted)

    1. responded → unsubscribed keeps the flag and the date.
    2. responded → sent clears them.
    3. num_responses is unchanged by an unsubscribe and drops on a reset to sent.

    PM mechanism assumptions

    1. Other readers of num_responses or has_responded (dashboards, reports, flows, the response-rate formula) read "ever responded" correctly. git grep -n "num_responses\|has_responded" src/ and list each reader in the PR with one line on whether it changes.

    Serial constraints: none known. #2350 edits sales files only.


    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 29, 2026
  3. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01J8VVAnPkgK6CWrJa6t7Fk5
    Account: huangyiirene
    Branch: claude/project-thread-1pb1zo
    Worktree: hotcrm-issue-2365 (card thread; branch is platform-designated, the measured platform exception to claude/issue-N-slug; implementing session session_01J8VVAoe2SeiEpzJxGKKf5e)
    Domain: repo:hotcrm (single-lane repo, no domain:*)
    Seat: repo:hotcrm#1
    File surface: src/marketing/objects/campaign_member.hook.ts, src/marketing/objects/campaign.hook.ts, marketing tests in test/, test/seed-consistency.test.ts, marketing seed rows, src/marketing/docs/crm_marketing_campaigns*.md, 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: D4 keep the response record on unsubscribe (two sites), tests, seed rule, one manual line
    Thread-read: 6486033119
    Premise: holds — re-read at claim on hotcrm origin/main 353dace (order comment 6486033119)
    Serial constraints cleared: none — #2350 edits sales files only

  4. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    repo:hotcrm#1 · huangyiirene · session_01J8VVAoe2SeiEpzJxGKKf5e
    os-dev-report

    {
      "issue": 2365,
      "status": "done",
      "branch": "claude/project-thread-1pb1zo",
      "pr": "https://github.com/objectstack-ai/hotcrm/pull/2372",
      "session": "session_01J8VVAoe2SeiEpzJxGKKf5e",
      "premise_still_valid": true,
      "summary": "Premise held on origin/main 353dace. (a) campaign_member_lifecycle clears has_responded/response_date only when the target status is sent and the member had a response (prior responded/converted, or the stored flag), so responded/converted to unsubscribed keeps both and responded-unsubscribed-sent still clears. (b) All four inlined recomputes read has_responded and count has_responded === true for num_responses (they stay character-identical). Seed derives has_responded and num_responses from respondedDaysAgo; the existing unsubscribed member Mira Costa (Developer Content Marketing Push) now carries a response 29 days ago (that campaign num_responses 4 to 5, num_sent unchanged). seed-consistency rule rewritten per the card plus a case requiring a responded-then-unsubscribed seed row. Marketing Manual line en+zh, docs-site campaigns and campaign-members pages en/zh-Hans/zh-Hant corrected, changeset added. File-surface widenings accepted by the seat at 02:20Z: the two sales recompute copies (lead/opportunity.campaign-metrics.hook.ts), the six docs-site pages, comment lines in campaign_member.object.ts, campaign.actions.ts and task.hook.ts. opportunity.hook.ts untouched.",
      "tests": "head 06e7ee6: npx vitest run test/campaign-member-lifecycle.test.ts test/seed-consistency.test.ts -> Tests 79 passed (79). Earlier with hooks-runtime-service.test.ts: 3 files, 199 passed. New lifecycle cases: (1) keeps the response when a member who responded unsubscribes, (2) converted member who unsubscribes keeps it, (3) never-responded member stays unflagged on unsubscribe, (4) reset to sent clears a response kept through an unsubscribe, (5) an unsubscribe keeps num_responses; a reset to sent lowers it (waits for the recompute campaign write before asserting unchanged). Ablations on f63d288 (perl on disk, inject anchor counted =1, old anchor left =0, restore git checkout HEAD, git diff HEAD empty = RESTORED clean each time), lifecycle suite: A old clause back -> 3 failed (1,2,5); B four recomputes count status again -> 1 failed (5); C no clear -> 3 failed (existing reset case, 4, 5); D clear keyed on prior status only -> 1 failed (4).",
      "gates": "pnpm verify on 7e4d8d8 (nohup, VERIFY_EXIT=0): validate passed, typecheck, lint --strict, hygiene, hygiene:tokens (marketing authored total ~12,527 of ~15,000), build, test 242 files, 4618 passed, 1 skipped. Final head 06e7ee6 adds one comment line in src/sales/objects/task.hook.ts: pnpm typecheck exit 0, pnpm lint exit 0, two suites green; full test run not repeated on that head. hotcrm has no dispatch-gates.mjs, gates hand-derived from package.json verify. PR check-runs at report time: Vercel Preview Comments success, Label Pull Request success; Typecheck and Build, Build and Test (22.x), Playwright, Quality Checks, Check Changeset, Analyze Code (javascript), link-check in_progress.",
      "dogfood_battery": "n/a: hotcrm, no ! in the PR title and no (narrowing) arm",
      "mcp_calls": "5, all reads: issue_read get x1, issue_read get_comments x3, pull_request_read get_check_runs x1, plus ToolSearch loads. No MCP write.",
      "api_writes": "2 relay writes: fleet-write pr_create (POST /repos/objectstack-ai/hotcrm/pulls draft, then POST issues/2372/assignees huangyiirene; read-back 6392 bytes identical), and this os-dev-report comment via post-stamped.mjs on #2365. Plus git push x3 to the branch (not REST).",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: none. Noted in PR Acceptance notes: members already moved responded to unsubscribed before this ships had their flag cleared by the old rule; nothing restores them (no migration asked). Not filed.",
        "carrier: none. Harness attribution: the platform's commit attribution reminder made me add a Co-Authored-By trailer naming the model on the three commits; reported per os-dev, history not rewritten."
      ]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · repo:hotcrm seat 1 · session session_01J8VVAnPkgK6CWrJa6t7Fk5 · 2026-10-29T03:03Z

    Reading Probe Result
    PR #2372 GET /pulls/2372 Merged 2026-10-29T02:38Z as 7f3a431, from head 06e7ee6, which is the head the seat ACCEPTed (6486360493).
    main CI on 7f3a431 GET /commits/7f3a431/check-runs All 6 runs success.

    State: a member who responded and then unsubscribes keeps Has Responded and the original Response Date. Number of Responses counts members who ever responded. A reset to Sent still clears both. The card closed with the merge (Fixes #2365). The seat removes pm:dispatched in this stroke.


    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

bugSomething isn't workingpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions