timeboxing: asked ≠ started — a question to the Schedular is answered, never turned into a session (#316–#320) - #328
Open
hugolytics wants to merge 16 commits into
Open
timeboxing: asked ≠ started — a question to the Schedular is answered, never turned into a session (#316–#320)#328hugolytics wants to merge 16 commits into
hugolytics wants to merge 16 commits into
Conversation
…, never turned into a session (#316-#320) Design approved 2026-09-05 (S2 kernel outcome, N1 judged no_session with cancel, A1 planner_agent answers, D1 describe from StageCard, F1 demote to receptionist) and the five-task plan the subagents work from. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…#316) AskQuestion joins the intent union and Asked the outcome union. The kernel returns Asked before it applies or saves anything, so the revision the next load sees is the one the turn loaded. Every open state offers question; a cancelled session still accepts nothing. The user's words are bound by the host, verbatim. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e state it hit (#316) The first guard iterated two snapshots that both landed on capture and committed, so planning_day, skeleton, review_commit and refine were never reached while the docstring claimed per-state coverage. One parametrised case per state `_display_context` returns, each asserting the state name it hit before the decision set, so a miswired snapshot cannot hide behind another state's answer. cancelled is pinned separately at (). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…and the contract says why (#320) The channel default is a no-op when it is itself timeboxing_agent. The receptionist refers rather than starts, which is what a card's thread needs. Two rules land in the proposal contract: a surface owns its thread over focus, and a workflow-owning agent offers question in every state. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… with the session described (#317) describe_session is the prose renderer beside the Block Kit one, over the same StageCard. The host answers an Asked outcome through planner_agent, which holds the calendar tools; the thinking card becomes the answer and no stage card moves. A failed answer is one failure line and one metered error. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… question or cancel (#318) The unconditional StartSession before a planning day is gone, with the docstring claim that there was nothing to decide. A fresh session is a no_session state the interpreter reads; an empty opening turn still starts. cancel rides along, which is #299's option 3. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…nd the start guard guards (#318) Cancel is now on offer at zero artifacts, so a fresh session can reach cancelled with no artifacts at all -- and a no_session branch read before the status would hand that closed thread a start, then kill it on the confirmation with the generic failure line. The order is reversed and the cancelled state is pinned for both shapes of closed session. The AST guard counted one StartSession return, which the code this ticket deletes also had; it now asserts the enclosing if tests user_text. The prompt fragment glosses start, which until now the model saw only as a name in the allowed set. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…reak-it check says the paragraph is not what carries it (#319) Question-vs-start-vs-cancel on a fresh session and question-vs-fact-vs-revise on a committed one, eight draws each, seven to pass. Every decision landed: across two full runs, no draw that recorded an outcome chose the wrong intent. The break-it check does not hold, and the record says so rather than asserting less. Strip QUESTION_PARAGRAPH and the three question cases still read as questions at 7/8, 8/8 and 8/8 -- for a plain interrogative the paragraph is not the discriminator, `question` being in allowed_decisions already is. The patch does bite: exactly its 570 bytes leave the system prompt. And the paragraph is load-bearing, just not on these texts -- "did you move lunch? I sleep 00:30-08:30" reads as the fact 8/8 with it and as a question 7/8 without, which is the clause about a reply that asks and also supplies one. The break-it cases name the wrong texts; that is a case-list decision, so it is reported, not quietly made here. Every miss whose cause was recorded was a LengthFinishReasonError, never a wrong decision: the pin runs away to 16384 completion tokens on roughly 3-5% of calls. That is why `_count` now prints what the misses were and not only how many -- without it the first run's 5/8 reads as a prompt failure, and the fix for it would have been an edit to a paragraph that was working. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…d a draw that reached no decision is retried once (#319) The old break-it cases were plain interrogatives, and they never measured the paragraph: strip it and "is it planned?" and "what did we settle on for lunch?" still answer question at 7/8 and 8/8, because the label is already in allowed_decisions and the preamble says to choose from that list. What the paragraph carries is the clause about a reply that asks *and* supplies a fact. Aimed there, the check bites -- "did you move lunch? I sleep 00:30-08:30" is the fact 8/8 with the paragraph and 2/8 without, and "is deep work still at 9? also I get up at 07:00" is 8/8 against 0/8. Both ride as positive cases too, so the pair states the discrimination in both directions. Without the paragraph the user gets an answer to their question while the sleep boundary they just stated goes nowhere, which is the silent-wrong-answer shape the ban exists to stop. A draw that raises reached no decision, never the wrong one: the interpreter returns a typed intent for every reading the model produces and only raises when the endpoint gave it nothing to read. Those are retried once and counted; the count is printed, never asserted, because the endpoint's error rate is #325's problem and folding it into the rate is what made a working paragraph look like a prompt bug in the first run. Four retries in this run of 144 draws. "did you add the gym?" as an eval case is now "did you put the gym in?" -- its exact words are in QUESTION_PARAGRAPH, so it measured recall of the prompt. The paragraph is unchanged. Spec section 5's break-it bullet now says what was measured instead of what was assumed. 18 passed, on deepseek/deepseek-v4-pro-0813:nitro via the timeboxing_agent pin. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ts one (#318) `cancel` as the first word typed into a DM wrote `cancelled` at revision 1 on the thread-blind `{channel}:dm` key, and every later turn in that DM hit a cancelled session and the generic failure sentence. Asking wrote a revision-0 `open` envelope, which is a session as far as the host's nudge suppressor is concerned. `_turn_guarded` now loads without creating: a question is answered over an in-memory snapshot, a cancel with no row is refused as `nothing_to_cancel`, and only an intent that starts something reaches `load_or_create`. A cancel against a row that locked no day and holds no artifacts is refused too, so an envelope an earlier code path left behind cannot be closed either. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…t either (#317) The turn handler marked the user active, cancelled the pending Admonisher ladder and persisted a session row before it knew the turn was a question. On a DM with no session that row silenced the planning nudge for an hour: the user asks "is it planned?", is told no, and the reminder that would have told them to plan it is suppressed by the asking. Both reads now go through `_load_or_new`, which loads and never writes, and the two activity signals wait until the intent is derived and are skipped for `AskQuestion`. The button path never carries a question, so it is unchanged. The turn-handler fakes gain the `load` the repository protocol always had, in place of the `load_or_create` the handler no longer calls. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… does (#317) The docstring promises the same fields the card renders, and the `asking` branch rendered the question alone -- not why it is needed, not the options on screen, not what each would do. A user looking at two blocker buttons who types "what are my choices?" got planner_agent answering from a description that contained neither. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The code demotes any `timeboxing_agent` no explicit per-thread binding chose, the channel default included -- pinned by `test_a_planning_thread_in_a_timeboxing_channel_is_demoted_to_the_receptionist`. The bullet said "arrived by focus", which is narrower than the rule. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s facts (#316) The comment the Task 1 ruling asked for: a `question` reading that arrives carrying facts is the model contradicting its own prompt -- which says a question that carries a fact is a fact, measured 8/8 -- so the words the user typed win and the structure is dropped. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…loads (#317) Same change as the unit fakes: the turn handler reads through `load` now, and a fake that only offers `load_or_create` fails the turn with an AttributeError the walk reports as a wrong first card. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…and measures the clause the ticket is named for (#319) The retry was blind. `except Exception` around a draw was justified as "the interpreter only raises when the endpoint gave it nothing to read", and three raise sites say otherwise: a decision outside allowed_decisions, output that does not fit the narrowed schema, and the binder's own refusals. The first is the exact degenerate answer a stripped paragraph is supposed to produce, so a blind retry re-rolled the break-it result until the run agreed with it. Proven against a stub: a disallowed decision took 8 retries and 16 calls before, and 0 and 8 after, while a transport failure is still redrawn once and a second failure is still reported. Both break-it families now assert the flip rather than an absence. A bar of 'the right decision fell below threshold' is cleared by two lost calls with the paragraph doing nothing at all; the wrong decision outnumbering the right one is not, because a lost draw subtracts from both counts. Adds the fresh-session half, which is the regression the branch is named for: stripped, "what's on my calendar tomorrow?" answers StartSession 8/8 against AskQuestion 0/8, and unstripped it answers AskQuestion 8/8 with StartSession never once. Asserted as a flip, not a bar -- 6/8 and 6/8 on two earlier draws would have failed a bar while the paragraph was plainly working. Seven case texts were quoted verbatim inside QUESTION_PARAGRAPH and measured recall of the prompt rather than the judgement; each is reworded to the same intent in other words. Spec section 5 now names the cases the code runs. 19 passed. Every positive case 7/8 or 8/8, every miss a LengthFinishReasonError and not one a wrong decision. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This was referenced Sep 5, 2026
Open
Open
Open
This was referenced Sep 8, 2026
llm: every surface interpreter on one client row, benched, and the runaway bounded (#336, #325)
#409
Open
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.
Linked issues
wayfinder:task) · routing half fix(slack): the planning card's own thread is resolved before sticky focus (recovered work) #310 · found on the way receptionist: _follow_up_plan decides follow-up timing by '?' in the message — a CLAUDE.md violation on the reply path #321 timeboxing interpreter: the pro pin runs away to 16384 completion tokens on ~3–5% of calls, so one typed turn in ~25 fails as SurfaceIntentError #325feat/asked-not-startedThe problem
03:43 today, under the Admonisher's planning card ("Not added yet"), Hugo typed "Is it planned?" The card's interpreter read it correctly — a question,
decision: none(logs/llm_io_20260905_010643_9295.jsonl, record 3) — and the reply was still handed totimeboxing_agent, which opened a fresh five-stage session and posted the Stage-1 day-confirm card. No model was consulted for that.Two faults. The routing half — sticky DM focus outranking a planning card's ownership of its own thread — landed as #310. This branch is the other half: the Schedular had no door that does not start a session.
derive_timebox_intentreturnedStartSession()for any text before a day was locked, with no model asked, on the docstring's claim that "there is nothing to decide about"; a committed session offered onlyprovide_factsorrevise, and the interpreter had nonone, so a question to the agent that just planned the day was coerced into a fact or a revision (#287 is the memory-side echo of that same coercion).Hugo's expectation, stated: either the timeboxing agent that just did the session answers, or the Schedular that can pull my calendar sees there is no planning session and says so. The receptionist already refers "is a session planned?" to
planner_agent— the mechanism existed and was never reached.Spec:
docs/superpowers/specs/2026-09-05-asked-not-started-design.md. Map: #157. Tickets: #316 #320 #317 #318 #319.What lands
Asked is not started, and asked is not revised. A question changes nothing in the session it is asked of — and, after the final review, nothing one layer up either.
AskQuestionintent andAskedoutcome.kernel.turnreturnsAskedbefore applying or saving anything; the snapshot the next load sees is byte-identical (tested by fullmodel_dump()equality).questionis offered in every open state;cancelledstill offers nothing. The user's words reach the answerer verbatim — the schema carries no text field for the decision, so the model cannot paraphrase them.TurnFailed(nothing_to_cancel); a cancel on an existing day-less, artifact-less row is refused the same way. Before this, "never mind" as the first words in a DM wrotecancelledonto the thread-blind DM key and every later typed turn died with the generic sentence.no_sessionoffersstart/question/cancel; the interpreter decides. Only an empty opening turn (a bare command) starts without asking. The AST guard proves the one remainingStartSession()return sits under anifonuser_text— and was shown to fail on the reverted code.cancelbefore a day is auto-start: a restart during the opening turn leaves a half-open, day-less session that blocks for an hour and cannot be cancelled or expired #299's option 3, landed here.describe_sessionis the prose renderer beside the Block Kit one, over the sameStageCard(day, stage, context, decided items by owner, the open question with itswhy_neededand offered options, the commit receipt). AnAskedoutcome is sent toplanner_agent— the calendar's answerer, the same one the receptionist refers to — as the described session plus the verbatim question; the "thinking…" card becomes the answer. No card transition, no panel sync, no relabel, no activity mark, no ladder cancel, no row. A failed answer is one in-thread line and one metered error (surface_intent/answer_failure), never a retry, never a session start.timeboxing_agent. The two general rules are now indocs/architecture/proposal_object_contract.md§7: a surface owns the thread its root posted, over focus; an agent that owns a workflow offersquestionin every state its surface allows.tests/integration/test_eval_timebox_question.py,@slow, n=8 / threshold 7, on the production interpreter client (build_autogen_chat_client("timeboxing_agent")→ the deepseek pro pin as.envhas it), no temperature pin. 19/19 cases, every positive ≥ 7/8, zero wrong decisions. The break-it check as first specified was unpassable and the eval said so: withquestioninallowed_decisions, the label alone reads a pure interrogative as a question (7–8/8 with the paragraph stripped). What the paragraph actually carries, and what the check now strips — asserting the flip (wrong outnumbering right), not the absence of the right answer: a question carrying a fact on a committed session ("did you move lunch? I sleep 00:30-08:30"→ fact 8/8 with it, question wins without), and a fresh-session calendar question ("what's on my calendar tomorrow?"→AskQuestion8/8 with it,StartSession8/8 vs 0/8 without — the incident, reproduced on demand). A draw is redrawn once only when its failure carries a transport__cause__; a wrong decision is never redrawn. Every case whose words appeared in the paragraph was reworded. The final commit,a47e0bb, is from the peer sessionadmonish-1-34whose Task 5 agent measured the fresh-session flip; its report istask-5-report-second-agent.mdin the (git-ignored) SDD workspace.Verification
no_sessionhad been placed before thecancelledcheck, and the AST guard passed on the reverted code). Whole-branch review (opus): 1 Critical, 2 Important, 3 promoted Minors — one fix wave, scoped re-review clean. Every ruling and every deferred minor is in the ledger,.superpowers/sdd/2026-09-05-asked-not-started/progress.md(git-ignored; ask for it).7ef1013: 3096 passed, 10 skipped, 1 xfailed, 1 failed —tests/e2e/test_slack_handoff_flow.py::test_slack_handoff_sets_focus_and_forwards, pre-existing onmain(asserts the*agent*\ntextformat fix(slack): a reply on a proposal thread is read against the proposal (#88 increment one) #281 removed; bisected by a peer to a71f213, theirs to fix).Commands run (from
.worktrees/asked-not-started):Found on the way, not fixed here
reasoning_tokens=73, $0.043 each). Every miss in 272 eval draws was this, never a misjudgement. ~1 typed timeboxing turn in 25 gets "I couldn't read that reply". Pre-existing, invisible to the unit suite. A peer session (admonish-1-34) measured a reproducible skew toward fresh-session questions — 9 retries in 152 draws (5.9%): fresh questions 5, fresh starts 3, committed 1 — not an absolute; that bears on where amax_tokenscap lands."?" in message.content, a CLAUDE.md violation whose path this branch widens (bounded to a 5-vs-15-minute delay).google_gemini-3.6-flashfor both interpreters at 03:43 while.envcarries no google id;.envwas modified at 15:59. models: the direct-API side runs on the decided pins, the judge reads them, and an agent never changes one #323/docs(agents): the timeboxing instruction files stop naming gemini as the model they talk to #324 have since cleaned the last gemini references — confirm the running process picked up the pins after a restart.Deferred, by ruling (in the ledger, none a merge gate)
A question turn is not idempotent (never saved, so a Slack redelivery re-asks — forced by the byte-identical invariant; the answer edits one message in place). The
nothing_to_cancelcard still draws Cancel/Retry. An interpreter that raises no longer marks activity (it is already a loudSurfaceIntentError).strftime('%A')in the description is locale-dependent. Thestartgloss inQUESTION_PARAGRAPHhas positive evidence but no break-it of its own. Map #157's compounding ask: the second read-only intent is cheaper fordescribe_session, and still pays a label in seven hand-built_display_contexttuples — a shared suffix and a_READ_ONLY_INTENTStuple would make it near-free.Before merging (Hugo)
scripts/demo.py start, and confirm the log names the pins, not gemini.user_focusto the Schedular), then type "Is it planned?" under a planning card. Expected: the receptionist →planner_agentanswers from the calendar in the card's thread; no Stage-1 card.timeboxing_session_statesrow created, and the planning nudge for that hour still fires.timeboxing_session_states.max_tokenscap on the interpreter client, or re-measuringhigheffort on it) — an agent changes no pin.🤖 Generated with Claude Code