Follow-up issue per @anshulsao's review on #48 — the discussion below was originally posted as a PR comment. Mirroring it as an issue so it stays discoverable after the PR merges.
Context
Surfaced while exercising flow run playbook --here end-to-end against the playbook-smoke fixture (run task playbook-smoke--2026-05-14-11-29). The friction: a session bound to a regular task can't fire a playbook run in-place — the binary refuses at cmdDoHere's "session already bound elsewhere" check (do.go:521-528), and --force doesn't override it.
PR #48 keeps the hard limit (one task ↔ one Claude session). This issue records the design question for whoever wants to revisit it.
The question
Should the session_id uniqueness invariant be relaxed for kind=playbook_run?
Today the partial unique index on tasks.session_id (db.go schema) enforces 1:1 between a Claude session and a non-archived task. The transcript filter shipped in PR #48 (commit 2146704) scopes flow transcript <slug> to timestamp >= session_started, which is one of the two jobs the uniqueness constraint was doing. Specifically:
| Constraint job |
Status after PR #48 |
| Transcript scope — given a task, find its conversation slice |
Unbundled from uniqueness via the timestamp filter. A range form (session_started_THIS ≤ ts < session_started_NEXT) would extend cleanly to shared sessions. |
Identity resolution — given $CLAUDE_CODE_SESSION_ID, find THE task it belongs to |
Still 1:1. Every flow show task (no-arg) call site assumes one row. Relaxing means picking a tie-breaker per call. |
The case for relaxing (for playbook_run only)
- Playbook runs are ephemeral fire-and-close invocations; stacking them in one conversation matches how users mentally treat them.
- Resolves the immediate friction: a session bound to a regular task can't fire a playbook run in-place — has to open a new tab.
- Regular tasks would stay 1:1 (they model long-running work; stacking would scramble the mental model).
The case for keeping the hard limit (current posture)
flow show task (no-arg) becomes ambiguous in a session hosting multiple runs. Every consumer (skill workflows for save-note, mark-done, project context, scope-creep detection) needs a tie-breaker — each with real edge cases:
- Latest non-done by
session_started — clean for sequential stacking, ambiguous when two runs are open simultaneously.
- Explicit
is_session_active flag — needs schema + --here semantics to bump it.
- LIFO stack — needs ordering; complicated when a middle run closes.
- No "current" — always specify — different product; loses bound-session ergonomics.
- The session-start hook's text injection ("you are bound to task X") becomes "you are bound to tasks X, Y, Z" with no obvious primary.
- Skill §4.13 (
Bound session — two options ONLY) is a workflow rule that depends on the 1:1 model.
Open questions to answer before any change
- What's the actual use case shape? Sequential ("fire X, close, fire Y, close") vs. interleaved ("fire X, jump to Y, back to X"). Sequential makes "latest non-done" almost trivially correct; interleaved breaks it.
- Does "a session hosts multiple runs" feel right, or is the session more "the user's workspace" with runs as tool invocations? The framing changes whether stacking is the right primitive or whether something lighter (e.g. a release-and-rebind escape hatch) is the better answer.
- How much friction is acceptable to "go back" to a closed run for follow-up notes?
- Would a simpler alternative solve enough of the pain? E.g.
flow release-binding (clears session_id from the bound task, orphans its transcript-resume but frees the session) or --here --force-release. Cheaper than full stacking; worse for transcript continuity.
Status
Context
Surfaced while exercising
flow run playbook --hereend-to-end against theplaybook-smokefixture (run taskplaybook-smoke--2026-05-14-11-29). The friction: a session bound to a regular task can't fire a playbook run in-place — the binary refuses atcmdDoHere's "session already bound elsewhere" check (do.go:521-528), and--forcedoesn't override it.PR #48 keeps the hard limit (one task ↔ one Claude session). This issue records the design question for whoever wants to revisit it.
The question
Should the session_id uniqueness invariant be relaxed for
kind=playbook_run?Today the partial unique index on
tasks.session_id(db.go schema) enforces 1:1 between a Claude session and a non-archived task. The transcript filter shipped in PR #48 (commit 2146704) scopesflow transcript <slug>totimestamp >= session_started, which is one of the two jobs the uniqueness constraint was doing. Specifically:session_started_THIS ≤ ts < session_started_NEXT) would extend cleanly to shared sessions.$CLAUDE_CODE_SESSION_ID, find THE task it belongs toflow show task(no-arg) call site assumes one row. Relaxing means picking a tie-breaker per call.The case for relaxing (for
playbook_runonly)The case for keeping the hard limit (current posture)
flow show task(no-arg) becomes ambiguous in a session hosting multiple runs. Every consumer (skill workflows for save-note, mark-done, project context, scope-creep detection) needs a tie-breaker — each with real edge cases:session_started— clean for sequential stacking, ambiguous when two runs are open simultaneously.is_session_activeflag — needs schema +--heresemantics to bump it.Bound session — two options ONLY) is a workflow rule that depends on the 1:1 model.Open questions to answer before any change
flow release-binding(clearssession_idfrom the bound task, orphans its transcript-resume but frees the session) or--here --force-release. Cheaper than full stacking; worse for transcript continuity.Status