Skip to content

fix(server): a fork no longer inherits the source thread's pin, auto-settle and delegation state - #28378

Merged
maria-rcks merged 1 commit into
pingdotgg:mainfrom
only21mil:fix/audit-m022
Nov 1, 2026
Merged

maria-rcks merged 1 commit into
pingdotgg:mainfrom
only21mil:fix/audit-m022

Conversation

@only21mil

Copy link
Copy Markdown
Contributor

Problem

Forking a pinned thread gives you a second pinned thread. The fork plan copies the source thread and resets a list of fields, but the list misses the pin, the sidebar order key, the auto-settle opt-out, the last snooze wake and the linked-environment delegation. So a fork:

  • lands in Pinned (pinnedAt, pinOrderKey), or in the source's dragged slot in Active (activeOrderKey) instead of at the top like any new thread;
  • never auto-settles if the source had auto-settle turned off (autoSettleDisabledAt, checked in ThreadSettlementService);
  • carries the source's lastSnoozeWakeAt, though snoozedAt and snoozedUntil are already reset;
  • when the source was delegated by a linked environment, claims that environment's thread as its parent (delegatedFrom). Web and mobile show a "Subagent of …" row for it, the client groups it under that remote thread, and the fork can't start a side chat.

Change

ThreadForkService.plan resets pinnedAt, pinOrderKey, activeOrderKey, autoSettleDisabledAt and lastSnoozeWakeAt next to the existing snooze resets, and leaves delegatedFrom off the fork (destructured out, since exactOptionalPropertyTypes rules out undefined). Subagent children already start with the pin pair and auto-settle opt-out cleared (#26035 added the last of those).

linkOrigin and historyOrigin still carry over: the contract says derived work keeps its link origin, and a fork's inherited v1 history rows need historyOrigin.

activeOrderKey goes slightly past the pin itself. It is the manual order in the Active section, and thread.settle already clears it together with the pin pair.

Scope and approval

No issue or discussion: this is a very small, focused fix for an obvious bug, which CONTRIBUTING allows without one. Five reset fields and one dropped field in one function, plus test assertions. It adds no new behavior or setting; a fork already starts unsnoozed, unsettled and unarchived, and now also starts unpinned and undelegated.

Verification

  • ThreadForkService.test.ts: the existing fork-reset case now gives the source all five reset fields plus delegatedFrom, historyOrigin and linkOrigin, and asserts the fork clears the five, drops delegatedFrom and keeps both origins. It failed on main (first failure: lastSnoozeWakeAt) and passes with the fix.
  • vp test run apps/server/src/orchestration-v2/ThreadForkService.test.ts apps/server/src/orchestration-v2/ThreadFork.execution.test.ts: 15/15 pass.
  • vp run check:changed 15fe2832727893cbdd2214383f9f971474849695: format, lint, knip, typecheck (3 packages) and tests pass.
  • Real clients, before (main) and after, with synthetic threads and the Replay (dev) provider: web, Android (Pixel 10, Android 16) and iOS (iPhone 17 Pro and iPad, iOS 26.5). Each fork was made with "Fork from this response".

A fork of a pinned thread. Web, before (main): the fork shows a pin like its source:

Web sidebar on main: source and fork both pinned

After, the source stays pinned and the fork is not:

Web sidebar with this change: only the source is pinned

Android. Before (main), source and fork both show a pin:

Android thread list on main: source and fork both pinned

After, only the source shows a pin:

Android thread list with this change: only the source is pinned

iOS. Before (main), source and fork both show a pin:

iOS thread list on main: source and fork both pinned

After, only the source shows a pin:

iOS thread list with this change: only the source is pinned

A fork of a delegated thread. The source's parent is "R" and its link origin is "L". Web, before (main): the fork's conversation opens with a "Subagent of R" divider:

Web fork on main: the conversation opens with "Subagent of · R on L"

After, the divider is gone:

Web fork with this change: no "Subagent of" divider

Web, the fork's Lineage panel before (main), listing the source's remote parent:

Web fork on main: Lineage shows "Remote parent on Synthetic remote"

After, it lists only its local source:

Web fork with this change: Lineage shows only the local source

iOS (iPad). Before (main), the fork's header reads "P · Dev · Subagent of R":

iPad fork on main: header "P · Dev · Subagent of R"

After, it reads "P · Dev · From L":

iPad fork with this change: header "P · Dev · From L"

Android. Before (main), "P · Dev · Subagent of R":

Android fork on main: header "P · Dev · Subagent of R"

After, "P · Dev · From L":

Android fork with this change: header "P · Dev · From L"

In every client the source thread still shows "Subagent of R" after the change.

Not checked:

  • A real cross-environment delegation. The delegated source was made by adding linkOrigin and delegatedFrom to a synthetic thread's projection in an isolated dev database, not through two linked environments.
  • GPU rendering on Android. The Android emulator for the delegated-fork shots renders in software; these shots check header text, not rendering.

Prior PRs

None found. No open or closed PR or upstream branch changes this reset list. Nearby, not the same:

Generated with Claude Opus 5.5 and GPT-6.1 Terra in T3 Code Nightly

…settle and delegation state

A fork was built by spreading the source thread, so it copied the source's
pin, sidebar order key, auto-settle opt-out, last snooze wake and linked
delegation. A fork of a pinned thread landed in Pinned, never auto-settled
if the source opted out, and a fork of a delegated thread showed a false
"Subagent of" row and refused side chats.

The fork plan now resets pinnedAt, pinOrderKey, activeOrderKey,
autoSettleDisabledAt and lastSnoozeWakeAt next to the existing snooze
resets, and drops delegatedFrom. linkOrigin still carries over, as derived
work keeps its origin, and so does historyOrigin, which the fork's
inherited history rows need.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XS 0-9 changed lines (additions + deletions). labels Oct 31, 2026
@macroscopeapp

macroscopeapp Bot commented Oct 31, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs Human Approval

Macroscope's review found this PR requires human approval — New forks stop inheriting pin, ordering, auto-settle opt-out and delegation state, without changing their source threads. Human confirmation is needed for the new defaults, particularly automatic settlement of forks whose source disabled it.

You can add or adjust custom eligibility rules. Learn more.

@coderabbitai

coderabbitai Bot commented Oct 31, 2026

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 54c5f25 and 4c3ff79.

Files selected for processing: 2
  • apps/server/src/orchestration-v2/ThreadForkService.test.ts
  • apps/server/src/orchestration-v2/ThreadForkService.ts

Limit details: You’ve used all 10 included reviews currently available.

Walkthrough

Forked threads now clear inherited delegation, snooze wake, pin, ordering, and auto-settle state. Tests verify that the fork preserves historyOrigin and linkOrigin.

Changes: Fork state reset

Layer / File(s) Summary
Reset and verify fork state
apps/server/src/orchestration-v2/ThreadForkService.ts, apps/server/src/orchestration-v2/ThreadForkService.test.ts
The fork omits delegatedFrom and initializes selected state fields to null. The test verifies those resets and confirms that origin metadata remains preserved.

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix

Possibly related PRs

Suggested labels: bugfix, size:XS

Suggested reviewers: t3dotgg

Merge Risk: Minimal · up to 4c3ff

The fork-state reset is ready to merge; no actionable current-head risk was established.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title names the main change: a fork no longer inherits the source thread's pin, auto-settle and delegation state. The changeset resets pinnedAt, pinOrderKey, activeOrderKey, autoSettleDisabledAt a…
Description check Passed The description follows the repository template. It has Problem, Change, Scope and approval, and Verification sections. The Problem and Change sections explain the bug and the fix in detail. The Scope…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Title check

Explanation

The title names the main change: a fork no longer inherits the source thread's pin, auto-settle and delegation state. The changeset resets pinnedAt, pinOrderKey, activeOrderKey, autoSettleDisabledAt and lastSnoozeWakeAt in ThreadForkService.plan and drops delegatedFrom, matching the title. The title is long (96 characters) and uses the conventional commit prefix, but it stays specific and accurate.

Full details: Description check

Explanation

The description follows the repository template. It has Problem, Change, Scope and approval, and Verification sections. The Problem and Change sections explain the bug and the fix in detail. The Scope section explains why no prior issue is needed under the small-fix exemption. The Verification section lists focused test commands with results and gives before/after screenshots for web, Android and iOS. It also states what was not checked. The template's optional agent-model note is present at the end. The description is thorough and meets the template's requirements.

Finishing Touches
Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Usage-based review receipt

Note

This review was completed with usage-based billing: files reviewed beyond your plan's included limits are billed at $0.25/file. View usage-based billing.

@maria-rcks

maria-rcks commented Nov 1, 2026 •

Copy link
Copy Markdown
Collaborator

Note

Written by gpt-6.1-sol on behalf of Maria

Verified in a real dev environment

verified unchanged head 4c3ff7916083062aa4259baa664e20f005c2d3ba on linux with playwright, /usr/bin/google-chrome, the real dev web client and codex gpt-6.1-sol through the local proxy. projects and conversations were created through the app, without mocks or synthetic thread records. only one dev server ran at a time.

  • reproduced on github/main (8b74a69019) in a separate installed worktree: pin a completed codex thread, disable auto-settle, then click "fork from this response". the fork inherited both settings.
  • repeated on the exact pr head: the fork starts unpinned with auto-settle enabled; its source remains pinned with auto-settle disabled. persisted pin order also resets.
  • continued the fork with a real codex turn asking for the prior response's marker. it returned fork verification source ready., confirming inherited conversation context still works.
  • pin/unpin and settle/un-settle still work on the fork. snoozing and waking the source, then forking again, leaves the source's wake timestamp intact and clears it on the fork.

short runtime transcript, read from the dev databases after the ui actions:

main: source pinned=true, autoSettleDisabled=true
main: fork   pinned=true, autoSettleDisabled=true
pr:   source pinned=true, pinOrderKey=n, autoSettleDisabled=true
pr:   fork   pinned=false, pinOrderKey=null, autoSettleDisabled=false
pr after snooze/wake: source lastSnoozeWakeAt=2026-11-01T02:10:24.431Z
pr after snooze/wake: fork   lastSnoozeWakeAt=null
pr provider runs: codex source=completed, codex fork continuation=completed

before, main:

main fork inherits pin and disabled auto-settle

after, pr head, with the same prompt and source settings:

pr fork is unpinned with auto-settle enabled

fork interaction and real codex continuation, with startup frames trimmed; shown as an animated gif for github; mp4 playback and seeking also checked:

fork defaults and real codex continuation

blacksmith: both fork test files passed, 15/15. vp run check:changed 15fe2832727893cbdd2214383f9f971474849695 passed format, lint, knip, graphql generation, typecheck of 3 packages and its selected tests. existing ci reports 30 successful checks. no code changes needed.

Not verified

real cross-environment delegation and its fork/side-chat behavior, since this run used one environment and one dev server; non-null manual active ordering; imported historyOrigin/linkOrigin; elapsed-time automatic settlement. the reset/origin cases have test coverage, but that is not runtime proof of these paths. native ios/android, electron, other providers and remote/relay connections were not exercised on this linux web verification pass.

@maria-rcks
maria-rcks merged commit 8670378 into pingdotgg:main Nov 1, 2026
30 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XS 0-9 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants