3.6.0
Released
Features
Recurring Quests have a settings screen,
/quests/recurring/[templateId]/settings, linked from a cycle's Details card ("Schedule and pause"): the owner changes the title, goal, context, constraints, schedule, party and completion rule with the same preview as the create form, and pauses or resumes the series; the Repeats row says when a series is paused. The task-list screen offers "Also apply to the current and next cycle", which brings their untouched copies up to date and leaves anything already ticked, reworded, assigned, dated, discussed or investigated alone (#868).Asking about a recurring Quest's task now builds on what earlier cycles already found (#895). Ask about this task opens with "Answered in Cycle N (date) — Show answer" above the starters when the task has an earlier answer; Show answer expands the most recent one in place, as Markdown, without spending credits, and Investigate is unchanged. The note is read from the #894 history route on open (
?limit=1), and only when the task card's count says there is something to find, so a one-off Quest's dialog makes no request and looks as before; a failed read shows nothing. When the worker runs a question on such a task it adds the summaries of the two most recent succeeded earlier answers with a summary — from cycles every current reader of that cycle can access — in aWhat you found in earlier cycles:section just before the answer instructions (one dated bullet each, 1,500 characters together, with an instruction to build on them rather than repeat them), in memory only: the route stores the prompt without them, and only their ids and cycle numbers are recorded, indetails.prior_answers([{ id, cycleIndex }]). So nothing from an earlier cycle is ever stored where a member of the asking cycle who cannot read that cycle — including one seated later — could read it; an owner whose cycle has a member seated part-way through gets only the cycles that member shares. No surcharge: the summaries are a few hundred tokens (seedocs/pricing.md). A one-off Quest's prompt is sent exactly as stored, and a failed read costs the section, never the answer.A recurring Quest's task card shows what earlier cycles already found out about the same task (#894). The investigations chip now appears on a cycle task with none of its own when its earlier copies have answers ("2 earlier"), and alongside the cycle's own count when both exist ("1 · 3 earlier"); the number is still the cycle's own. In the open panel a collapsed From earlier cycles (N) group follows the cycle's list — or stands in for its empty state — and loads only when expanded, from the new
GET /api/tasks/[id]/investigations/history, ten at a time with Show older; each answer reads "Cycle 3 · Mon, Oct 13 · model", renders as Markdown with the question that was asked, and links to that cycle ("Open Cycle 3"). The counts come from oneget_quest_investigation_history_counts()call per cycle page (useInvestigationHistoryCounts), made only when a loaded task carries a template lineage, so a one-off Quest and a cycle of hand-added tasks make no extra call and look exactly as before. If that count cannot be read, a task with a template lineage still shows the chip and the group, without a number ("earlier"), and expanding the group reads the history itself — a failed count never hides the only way to the answers. The answers are read-only; asking again is unchanged, and offering the last answer before a new question is the next issue in the batch.
Technical changes· 8 entries for database, tooling, CI, tests and docs
Database
A recurring Quest can be edited, paused and resumed, and a task-list edit can reach the cycles already created (#868).
20261122120000_recurrence_template_edit_pause.sqladdsquest_recurrence_templates.cycle_index_offset— a minted cycle'scycle_indexis now the offset plus its local index on the template's grid, so numbering carries on after a schedule change or a pause instead of colliding with (and silently skipping) or jumping past the cycles already minted; every existing template has offset 0 and nothing minted moves.mint_quest_recurrence_cycles()is the live body with only that split. New owner-only RPCs:update_quest_recurrence_template()saves the settings and schedule, refusing a next start before the cycles that remain end;set_quest_recurrence_template_active()pauses, or resumes from the next free slot on the grid;save_quest_recurrence_template_tasks()wrapsset_quest_recurrence_template_tasks()and, when asked, rewords, adds and removes the untouched copies in the open cycles (status active, window not over) throughtasks.template_task_id. Saving settings or pausing removes the template's cycles that have not started and that nobody has touched (recurrence_cycle_is_untouched(): worded as the template, every task an unedited todo copy, no comment, attachment, attendance or pending invitation); a worked-on cycle stays and the new schedule starts after it. Clients keep UPDATE on the template's descriptive columns only, so the schedule,is_activeand the offset move only through the RPCs.20261123120000_recurrence_edit_pause_review_fixes.sql(Codex, PR #904) narrows "untouched": a task copy counts only while nobody has edited it (updated_at = created_at— a task ticked and reopened no longer passes on matching wording), andtasks_set_updated_atskips an apply's own reword (questify.template_apply); a waiting cycle with any invitation, a seat that joined after it was minted, or edited constraints or due date is kept; and the first index after a re-base or resume skips the weekdays the mint skips, so a weekdays series resumed at the weekend numbers Monday's cycle next.20261124120000_recurrence_edit_pause_review_fixes_2.sql(Codex, PR #904) settles what "untouched" means for a cycle:quests.updated_atstill equal tocreated_at, so any edit of the Quest (images, schedule, persona, Year Theme…) or of its tasks keeps it; a seat added or removed stamps the waiting cycle throughtouch_waiting_recurrence_cycle(), so a removed member can no longer come back on a re-mint;quests_set_updated_atskips an apply's own writes;mint_quest_recurrence_cycles()takes each templatefor share, so it cannot re-mint a cycle a concurrent pause just removed; andget_quest_recurrence_next_start_floor()gives the settings form a suggested start after any worked-on waiting cycle.20261125120000_recurrence_apply_two_cycles.sql(Codex, PR #904) limits an apply to the first two open cycles, as the checkbox says, even when a kept waiting cycle leaves more open.20261126120000_recurrence_solo_copies_direct_seats.sql(Codex, PR #904) makes the mint copy only direct seats from the previous cycle of a template without a guild, so switching a guild-backed series to "Just me" no longer turns the guild's members into direct seats on later cycles.20261127120000_recurrence_first_index_and_reorder.sql(Codex, PR #904) makes a resume between windows (a daily one-hour cycle resumed at noon) number the next cycle straight after the last, and makes reordering a waiting cycle's tasks count as a change. The four RPCs are allowlisted insupabase/advisor-allowlist.txt(owner-only, checked in the body). Covered by the newsupabase/tests/quest_recurrence_edit_pause.sqlandrls.sql;quest_recurrence.sqlmoves its clock aspostgres.Earlier cycles' answers for an investigation prompt are read by the worker at run time, under a shared-access rule, and never stored (#895).
20261120120000_prior_answers_shared_access.sqladdsget_shared_prior_cycle_answers(_task_id, _limit)— the succeeded answers with a usable summary (summary ~ '\S', applied before the limit) of the other copies of the same template task on earlier cycles of the same template, newest cycle first, only from cycles that every current reader of the asking cycle can access — andquest_reader_ids(_quest_id), theuser_can_access_quest()rule as a set;20261117120000_prior_cycle_answers.sqladdsquest_accessible_to(_quest_id, _user_id), the same rule for a named user.20261121120000_prior_answers_run_time.sqlmakes the read SECURITY INVOKER and service role only, becausetask_investigations.promptanddetailsare readable by every reader of the asking cycle — including one seated after the question was asked — and any of them can requeue a shared investigation, so the summaries must neither be stored nor chosen by the asker's access orinitiated_by_id(Codex, PR #903). It also replaces the queue's requirement check withinvestigation_worker_requirements(_page_urls, _pricing, _task_id), which adds aprior_answerscapability requirement toinvestigation_row_capabilities()for every row on a cycle task, andclaim_next_capable_task_investigation()(its live body, grants kept) checks it, so a worker from before this change never claims such a row during a rolling deploy; rows on other tasks require what they did before. All four functions are service role only. (20261117120000–20261120120000also created, revised and dropped an initiator-scopedget_prior_cycle_answers()and a four-argumentinvestigation_worker_requirements(), which20261121120000drops.) Covered by the newsupabase/tests/prior_cycle_answers.sql(35 assertions: the shared rule from both sides — a member seated part-way through keeps the owner's earlier cycles out, removing them lets them in — blank and null summaries, the queue requirement for prompt-less and stored-prompt cycle rows alike, worker-only grants, and thatquest_accessible_to()andquest_reader_ids()agree withuser_can_access_quest()for every fixture user and quest, an ended guild seat and a deleted quest among them) and tworls.sqlgrant assertions.Removing a task from a recurring Quest's template no longer deletes its row (#894).
20261116120000_recurrence_template_task_soft_remove.sqladdsquest_recurrence_template_tasks.removed_at, andset_quest_recurrence_template_tasks()now marks the rows a save leaves out as removed instead of deleting them. Before, the delete firedtasks.template_task_id'son delete set null(#893) on every cycle's copy — today's and tomorrow's already-minted cycles included — so those copies lost their lineage and, with it, their earlier cycles' answers. A removed row is skipped bymint_quest_recurrence_cycles(), hidden by the owner's SELECT policy (so the editor lists live tasks only), and no longer holds a position:quest_recurrence_template_tasks_position_uniqueis now a unique index over live rows. Listing a removed row's id again restores it with its lineage, so a list saved from a page loaded before the removal does not create a duplicate. Deleting a whole template still cascades to its rows. Both functions are their live bodies with only these changes, and keep their grants. Covered by new assertions insupabase/tests/task_investigation_history.sql,recurrence_template_task_lineage.sql(whose removal assertions now expect the link kept) andrls.sql.A recurring Quest cycle task can now read the answers its earlier copies got (#894).
20261115120000_task_investigation_history.sqladdsget_task_investigation_history(_task_id, _before_id, _limit), which followstasks.template_task_id(#893) to the other copies of the same template task on cycles of the same template with a lowercycle_indexand returns their succeeded investigations, newest cycle first and keyset-paged on(cycle_index, created_at, id), each with its cycle's id, index and scheduled start; andget_quest_investigation_history_counts(_quest_id), the per-task count of the same rows for one cycle. Failed and pending runs are left out, since an earlier failure has nothing to reuse. Both return nothing unless the caller can access the asking cycle, and filter every earlier cycle byuser_can_access_quest()on that cycle, so a party member sees only the cycles they hold a seat on, a removed seat or a deleted cycle drops its answers, and a visitor to a public cycle sees none — investigations stay private. They are SECURITY DEFINER only becausetemplate_task_idis off the #570 client column allowlist, return the card's columns and neverprompt,error,imagesorinitiated_by_id, and are closed toanon; both are listed insupabase/advisor-allowlist.txt. Covered by the newsupabase/tests/task_investigation_history.sql(35 assertions) and onerls.sqlgrant assertion.A recurring Quest's cycle tasks now record which template task they were copied from (#893).
20261114120000_recurrence_template_task_lineage.sqladdstasks.template_task_id(FK →quest_recurrence_template_tasks,on delete set null, partial index), so the same task can be found across cycles. Removing a task from the template keeps every cycle's copy and drops only the link.mint_quest_recurrence_cycles()sets the column inside a declared template clone (questify.template_clone) and restores the flag after its insert. The copies stay ordinaryorigin = 'user'tasks, notorigin = 'template', which means a locked checklist (#477).tasks_guard_template_origin()nulls the column on a client insert and keeps the stored value on a client update, as it already does fororigin_ref, so only the mint can assert a lineage. The column is not on the #570 client SELECT allowlist; collaborators read it throughget_tasks_with_deadlines(), andmapTaskRow()maps it totemplateTaskId.set_quest_recurrence_template_tasks()now edits the list in place: an element carrying theidof one of the template's rows updates that row, rows left out are deleted, and elements without an id are inserted. Before, it deleted and re-inserted the whole list, which would have nulled every cycle's link on every save. The function refuses an id from another template, a duplicate id or a malformed one with22023. The recurring-Quest task editor sends each saved task's id back and remounts on the saved ids after a save.tasks_set_updated_atno longer stamps a write that changes onlytemplate_task_id, so neither the backfill nor the delete cascade moves a done task's completion time or counts as quest activity. Existing cycle tasks are backfilled where(sort_order, title)matches exactly one template task and exactly one task in that cycle; ambiguous or edited rows stay null. Covered by the newsupabase/tests/recurrence_template_task_lineage.sql(38 assertions) and threerls.sqlassertions on the column.
Tests
Unit coverage for the settings and pause actions, the apply-to-cycles save, the settings read and grid helper, the settings page, the pause card, the form's edit mode and the Details card's Repeats row, and a gated Playwright spec (
quest-recurrence-settings.spec.ts) that applies a task edit, renames, pauses and resumes a daily series (#868).The new worker
prior-answers.test.tspins the earlier-answers section and its cap — two answers at most, blank summaries skipped first, whitespace collapsed, 1,500 characters shared with a short summary's unused share passed on, cycles numbered from one with their date, none without any — its placement just before the answer instructions (or at the end of a prompt without them, and no change with nothing to carry), the loader's read by task, row mapping, unscheduled-cycle date and failed read degrading to none, and a re-queued row's list replaced (#895).processor.test.tspins the worker's advertised capabilities (now includingprior_answers) and covers the section in a fallback prompt and in a stored one (placed before its instructions, kept in the no-search retry prompt, never written back), itsget_shared_prior_cycle_answerscall, a row with no initiator,details.prior_answerson a succeeded and a failed run, and one-off prompts sent exactly as stored.route.test.tspins that the route neither reads earlier answers nor stores them inpromptordetails.investigation-prompt-dialog.test.tsxcovers the note's request and placement above the starters, Show/Hide answer, the summary fallback, no request at a zero count or without a task id, a lookup when the count is unknown, and nothing shown for an empty or failed read;fixtures.test.tsxthe fixture's Ask dialog. In a browser,e2e/earlier-cycle-investigations.spec.tsnow opens Ask about this task on thee2e-fixtures/earlier-cyclesroute and checks the note, the?limit=1read and the answer expanding without another request, and that the dialog opens normally when the real route answers 401.investigation-history.test.tscovers the row mapping, the one-extra-row page and its cursor, the defaults, the error path and the counts' failed fallback;history/route.test.tscovers a page and its cursor, the cursor passed through, 404 for a malformed id or an inaccessible task, 400 for a malformed cursor or page size, 401 signed out and a plain-language 500.task-investigations.test.tsxcovers the group's absence without earlier answers, collapsed until expanded with no fetch before it, loading, listed (cycle number, model, Markdown answer, question, link), standing in for the empty state, Show older and its failure, collapsing without re-fetching, retry after an error, an empty page and the summary fallback;task-footer.test.tsxthe chip's earlier-only and own-plus-earlier labels and the unchanged chip without;use-investigation-history-counts.test.tsxthe gated and stale reads;task-manager-earlier-cycles.test.tsxthat the counts read is enabled only once a task with a lineage has loaded, and that a failed read keeps an unnumbered chip on lineage tasks only;task-footer.test.tsxandtask-investigations.test.tsxcover the unknown-count chip and group;quest-recurrences/queries.test.tspins the editor read'sremoved_atfilter. In a browser,e2e/earlier-cycle-investigations.spec.ts(smoke mode, on the newe2e-fixtures/earlier-cyclesroute) checks the chip on a cycle task with none of its own, that opening the panel fetches nothing until the group is expanded, ten answers then Show older for the eleventh, collapsing without a second read, the link to the answer's cycle, and a retry after the real route's 401; the livee2e/cycle-investigation-history.spec.tsdrives the same flow against a real stack — a recurring Quest created in the UI, eleven settled answers and a failed one on its first cycle, listed, paged and linked from the second.e2e/quest-recurrence-tasks.spec.tsnow reads only live template tasks, since a removed one is kept (#894).