Questify

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 a What 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, in details.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 (see docs/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 one get_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.sql adds quest_recurrence_templates.cycle_index_offset — a minted cycle's cycle_index is 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() wraps set_quest_recurrence_template_tasks() and, when asked, rewords, adds and removes the untouched copies in the open cycles (status active, window not over) through tasks.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_active and 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), and tasks_set_updated_at skips 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_at still equal to created_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 through touch_waiting_recurrence_cycle(), so a removed member can no longer come back on a re-mint; quests_set_updated_at skips an apply's own writes; mint_quest_recurrence_cycles() takes each template for share, so it cannot re-mint a cycle a concurrent pause just removed; and get_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 in supabase/advisor-allowlist.txt (owner-only, checked in the body). Covered by the new supabase/tests/quest_recurrence_edit_pause.sql and rls.sql; quest_recurrence.sql moves its clock as postgres.

  • 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.sql adds get_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 — and quest_reader_ids(_quest_id), the user_can_access_quest() rule as a set; 20261117120000_prior_cycle_answers.sql adds quest_accessible_to(_quest_id, _user_id), the same rule for a named user. 20261121120000_prior_answers_run_time.sql makes the read SECURITY INVOKER and service role only, because task_investigations.prompt and details are 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 or initiated_by_id (Codex, PR #903). It also replaces the queue's requirement check with investigation_worker_requirements(_page_urls, _pricing, _task_id), which adds a prior_answers capability requirement to investigation_row_capabilities() for every row on a cycle task, and claim_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–20261120120000 also created, revised and dropped an initiator-scoped get_prior_cycle_answers() and a four-argument investigation_worker_requirements(), which 20261121120000 drops.) Covered by the new supabase/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 that quest_accessible_to() and quest_reader_ids() agree with user_can_access_quest() for every fixture user and quest, an ended guild seat and a deleted quest among them) and two rls.sql grant assertions.

  • Removing a task from a recurring Quest's template no longer deletes its row (#894). 20261116120000_recurrence_template_task_soft_remove.sql adds quest_recurrence_template_tasks.removed_at, and set_quest_recurrence_template_tasks() now marks the rows a save leaves out as removed instead of deleting them. Before, the delete fired tasks.template_task_id's on 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 by mint_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_unique is 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 in supabase/tests/task_investigation_history.sql, recurrence_template_task_lineage.sql (whose removal assertions now expect the link kept) and rls.sql.

  • A recurring Quest cycle task can now read the answers its earlier copies got (#894). 20261115120000_task_investigation_history.sql adds get_task_investigation_history(_task_id, _before_id, _limit), which follows tasks.template_task_id (#893) to the other copies of the same template task on cycles of the same template with a lower cycle_index and 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; and get_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 by user_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 because template_task_id is off the #570 client column allowlist, return the card's columns and never prompt, error, images or initiated_by_id, and are closed to anon; both are listed in supabase/advisor-allowlist.txt. Covered by the new supabase/tests/task_investigation_history.sql (35 assertions) and one rls.sql grant assertion.

  • A recurring Quest's cycle tasks now record which template task they were copied from (#893). 20261114120000_recurrence_template_task_lineage.sql adds tasks.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 ordinary origin = 'user' tasks, not origin = '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 for origin_ref, so only the mint can assert a lineage. The column is not on the #570 client SELECT allowlist; collaborators read it through get_tasks_with_deadlines(), and mapTaskRow() maps it to templateTaskId. set_quest_recurrence_template_tasks() now edits the list in place: an element carrying the id of 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 with 22023. The recurring-Quest task editor sends each saved task's id back and remounts on the saved ids after a save. tasks_set_updated_at no longer stamps a write that changes only template_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 new supabase/tests/recurrence_template_task_lineage.sql (38 assertions) and three rls.sql assertions 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.ts pins 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.ts pins the worker's advertised capabilities (now including prior_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), its get_shared_prior_cycle_answers call, a row with no initiator, details.prior_answers on a succeeded and a failed run, and one-off prompts sent exactly as stored. route.test.ts pins that the route neither reads earlier answers nor stores them in prompt or details. investigation-prompt-dialog.test.tsx covers 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.tsx the fixture's Ask dialog. In a browser, e2e/earlier-cycle-investigations.spec.ts now opens Ask about this task on the e2e-fixtures/earlier-cycles route and checks the note, the ?limit=1 read and the answer expanding without another request, and that the dialog opens normally when the real route answers 401.

  • investigation-history.test.ts covers the row mapping, the one-extra-row page and its cursor, the defaults, the error path and the counts' failed fallback; history/route.test.ts covers 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.tsx covers 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.tsx the chip's earlier-only and own-plus-earlier labels and the unchanged chip without; use-investigation-history-counts.test.tsx the gated and stale reads; task-manager-earlier-cycles.test.tsx that 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.tsx and task-investigations.test.tsx cover the unknown-count chip and group; quest-recurrences/queries.test.ts pins the editor read's removed_at filter. In a browser, e2e/earlier-cycle-investigations.spec.ts (smoke mode, on the new e2e-fixtures/earlier-cycles route) 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 live e2e/cycle-investigation-history.spec.ts drives 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.ts now reads only live template tasks, since a removed one is kept (#894).