Skip to content

Commit 8b6cc70

Browse files
authored
fix: stabilize subagent settings and goal compaction (#192)
## Related Issue N/A — maintainer-authored PR. ## Problem Subagent model overrides could be lost during provider refreshes, and the settings UI could not explicitly restore inheritance from the calling agent. Goal continuations could also race automatic context compaction or continue after compaction was cancelled or failed. ## What changed - Add an explicit subagent model inheritance option across the web, gateway, SDK, and config contracts. - Preserve configured subagent model aliases during provider discovery and models.dev refreshes. - Coordinate v1 and v2 goal turns with automatic compaction and stop continuation after cancellation or failure. - Prevent agent-gateway tests from posting production telemetry. - Synchronize the branch with current `main` and rebuild the committed web bundle. ## Verification - `pnpm lint` - `pnpm typecheck` - `pnpm build` - Web tests: 986 passed. - OAuth tests: 120 passed. - Agent-core tests: 4,172 passed with 3 expected failures. - Focused v2 compaction and goal-operation regressions passed. - `pnpm run check:web` - `node scripts/check-nix-workspace.mjs` The full root test run had one unchanged tower-store test exceed its 5-second timeout under suite load; the same test passed alone in 1.2 seconds. ## Checklist - [x] I have read the [CONTRIBUTING](https://github.com/PyModel/pythinker-code/blob/main/CONTRIBUTING.md) document. - [x] Related issue is not required for this maintainer-authored PR. - [x] I have added tests that prove the changed behavior works. - [x] Ran `gen-changesets`; this PR includes three user-facing patch changesets. - [x] No user documentation update is required; the settings label is self-explanatory and the compaction change restores expected behavior. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added an “Inherit agent model” option for subagent settings, allowing the secondary-model override to be cleared. * Added desktop-only update controls, including update channels, status details, timestamps, and check-for-updates handling. * Moved the conversation table-of-contents toggle to Lab settings. * **Bug Fixes** * Preserved subagent model aliases and configurations during provider refreshes, imports, edits, and removals. * Improved goal behavior when automatic context compaction is cancelled or fails, preventing unintended continuation requests. * **Tests** * Expanded coverage for settings, model inheritance, provider updates, alias preservation, and compaction cancellation and failure scenarios. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 35ca233 commit 8b6cc70

129 files changed

Lines changed: 875 additions & 403 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@pymodel/pythinker-code": patch
3+
---
4+
5+
Preserve subagent model aliases when provider models refresh.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@pymodel/pythinker-code": patch
3+
---
4+
5+
Stop goal turns when automatic context compaction is cancelled or fails.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@pymodel/pythinker-code": patch
3+
---
4+
5+
Let subagents inherit the calling agent model from the Agent settings tab.
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
# Goal pause during compaction assessment
2+
3+
- Date: 2026-08-25
4+
- Status: implemented and verified
5+
- Severity: medium
6+
- Confidence: high for v2; high from source for the v1 custom-strategy path
7+
8+
## Verdict
9+
10+
The repeated cancellation bug is real in v2 when auto-compaction can run in the background. The
11+
recurring cancellation normally comes from the history-safety guard, not from the blocked step's
12+
abort signal.
13+
14+
The proposed pause/resume fix is directionally correct, but reusing the current goal pause and
15+
`resumeGoal()` paths is not safe. The fix must preserve the live goal turn, make a below-block-ratio
16+
goal step wait for compaction, and resume only after a successful compaction with no later user,
17+
budget, goal, or restart override.
18+
19+
Decision:
20+
21+
- Fix auto-compaction only. Manual compaction already requires loop quiescence.
22+
- Fix v2 as the active production path.
23+
- Add v1 parity for custom `compactionStrategy` users, but use a v1-specific continuation launch.
24+
- Use the existing `goal.updated` TUI marker. Do not add a second goal-state path to the compaction
25+
component.
26+
27+
## Findings
28+
29+
### F1 — The repeated cancellation path is the unsafe-history guard
30+
31+
When `loopControl.compactionTriggerRatio` is below `0.85`, v2 uses the lower value as the trigger
32+
ratio and keeps `0.85` as the block ratio. This enables non-blocking after-step checks
33+
(`packages/agent-core-v2/src/agent/fullCompaction/strategy.ts:62-64,91-100`).
34+
35+
The repeating sequence is:
36+
37+
1. `afterStep()` starts an auto-compaction without waiting for it
38+
(`fullCompactionService.ts:502-506`).
39+
2. The goal turn ends and `handleTurnEnded()` enqueues another continuation
40+
(`goalAgentRuntime.ts:658-691`).
41+
3. The loop materializes that continuation into context before its next step
42+
(`loopService.ts:807-821`). Its origin is `system_trigger/goal_continuation`
43+
(`goalAgentRuntime.ts:761-782`).
44+
4. The compaction result sees a new non-user tail. `historySafeToCompact()` rejects it
45+
(`fullCompactionService.ts:732-738,835-842`) because `system_trigger` messages are dropped
46+
(`compactionHandoff.ts:159-182`).
47+
5. Compaction emits `compaction.cancelled`. The still-large goal turn reaches the same boundary and
48+
starts the next compaction.
49+
50+
A temporary contract test used the real v2 loop, goal, context, and compaction services and stubbed
51+
only the model boundary. It forced the same after-step `source: 'auto'` seam, observed three
52+
compaction attempts and two consecutive cancellations, and passed. The temporary file was removed
53+
after evidence capture.
54+
55+
This path does not require the step signal to abort. `propagateBlockingAbort()`
56+
(`fullCompactionService.ts:551-558`) is a separate cancellation source for a compaction that is
57+
already blocking a step.
58+
59+
### F2 — The default blocking path does not create the reported loop by itself
60+
61+
The default trigger and block ratios are both `0.85`. `beforeStep()` starts compaction and waits in
62+
`block()` before the model step can continue (`fullCompactionService.ts:494-499,535-549`). Context
63+
overflow recovery also always waits (`fullCompactionService.ts:464-473`). The turn cannot end and
64+
the goal cannot enqueue another continuation while that wait is unresolved.
65+
66+
If an external cancel, deadline, shutdown, or user interrupt aborts that step, the abort listener can
67+
cancel compaction. The same turn then ends abnormally and the goal runtime pauses it
68+
(`goalAgentRuntime.ts:719-735`). That is not the recurring continuation loop.
69+
70+
Current regression evidence supports this distinction: the existing active-goal/default-compaction
71+
test completes successfully and reinjects the goal reminder before the post-compaction request.
72+
73+
### F3 — A normal goal pause would cancel the compaction
74+
75+
In v2, leaving `active` calls `cancelPendingContinuation()` unless the caller sets
76+
`preserveLiveContinuation` (`goalAgentRuntime.ts:883-904`). That function aborts the queued receipt
77+
or cancels its assigned loop turn (`goalAgentRuntime.ts:822-833`). For a blocking goal continuation,
78+
the turn signal then reaches `propagateBlockingAbort()` and cancels compaction.
79+
80+
The compaction pause therefore needs a dedicated internal transition that uses the existing
81+
`preserveLiveContinuation` option. Calling the current public `pauseGoal()` or `pauseActiveGoal()`
82+
unchanged would reproduce the failure.
83+
84+
A status pause is also insufficient when compaction starts in `beforeStep()` below the `0.85` block
85+
ratio. That same model step can still run and mutate history. The goal's before-step hook must wait
86+
for the active auto-compaction task before it calls the next hook. Register this ordering explicitly
87+
after the `full-compaction` hook.
88+
89+
### F4 — Reusing `resumeGoal()` is not robust
90+
91+
The v2 `resumeGoal()` method launches work only for actor `user` with `continueIfPaused` or
92+
`continueIfBlocked` (`goalAgentRuntime.ts:393-420`). A runtime actor changes the status to `active`
93+
but can leave an idle goal with no continuation. Pretending the runtime is the user gives incorrect
94+
telemetry and can set `resumeContinuation`; a later real interruption can then launch another turn
95+
from the cancelled-turn branch (`goalAgentRuntime.ts:663-672`).
96+
97+
Use an internal compaction-success resume operation. It must consume one transient resume token and
98+
apply this state table:
99+
100+
A user resume request while compaction is still live must mean "resume after successful compaction."
101+
It must keep the goal paused and must not launch a turn immediately.
102+
103+
| Finish state | Required result |
104+
| --- | --- |
105+
| Success; same goal is still paused for this compaction | Set `active` as actor `runtime`. If a preserved turn is live, launch nothing. If the loop is idle with no pending continuation, launch exactly one continuation. |
106+
| Compaction cancel or failure | Stay paused. Replace the promise-to-resume reason with a truthful failure reason. |
107+
| User paused, cancelled, or replaced the goal during compaction | User intent wins. Consume the token and never launch stale work. |
108+
| A goal budget became final or the goal became blocked/complete | Do not resume. Preserve the newer terminal state. |
109+
| Process replay after an in-flight compaction | Do not auto-resume. The transient token is gone; replace the stale reason with an agent-restart pause reason. |
110+
111+
The current goal fold only changes `terminalReason` when status changes
112+
(`goalAgentRuntime.ts:1361-1368`). The fix needs a narrow same-status reason update so user pause,
113+
compaction failure, and replay cannot leave the text "will resume" after auto-resume was cancelled.
114+
115+
### F5 — v1 and TUI need different scope than proposed
116+
117+
V1 accepts `loopControl.compactionTriggerRatio` in its schema, but its production
118+
`FullCompaction` constructor applies only `reservedContextSize`
119+
(`packages/agent-core/src/config/schema.ts:153-159`,
120+
`packages/agent-core/src/agent/compaction/full.ts:98-112`). Its default trigger and block ratios are
121+
equal, so normal v1 auto-compaction is synchronous. The reported background loop is reachable only
122+
through the public custom `compactionStrategy` option (`packages/agent-core/src/agent/index.ts:94,232`).
123+
124+
That custom path has the same unsafe-tail check (`packages/agent-core/src/agent/compaction/full.ts:588-606`),
125+
but v1 has no independent continuation launcher. After a background compaction pause makes
126+
`driveGoal()` exit (`packages/agent-core/src/agent/turn/index.ts:471-536`), `resumeGoal()` only changes
127+
state. V1 must explicitly launch one continuation after successful compaction when no turn is active.
128+
129+
The current TUI paths are under `apps/pythinker-code`, not the upstream app path. The live
130+
`goal.updated` handler already renders lifecycle markers (`session-event-handler.ts:751-801`). A
131+
pause reason with the `Paused` prefix renders as `Goal paused ...`
132+
(`components/messages/goal-markers.ts:153-159`), and `/goal status` also shows `terminalReason`
133+
(`components/messages/goal-panel.ts:131-167`). This already supplies the required feedback:
134+
135+
> Goal paused because context compaction is in progress; it will resume after compaction completes
136+
137+
`CompactionComponent` can remain the generic compaction progress block. It does not receive reliable
138+
goal state in `compaction.started`, so adding combined copy there would duplicate lifecycle state.
139+
140+
## Minimum v2 remediation contract
141+
142+
1. Store a transient compaction-pause token with `goalId`. Set it synchronously on auto-compaction
143+
start, then perform an awaited, re-entrant-safe durable pause with actor `runtime` and
144+
`preserveLiveContinuation: true`.
145+
2. Gate every continuation launch while the token exists. In the goal before-step hook, wait for the
146+
active auto-compaction task after the `full-compaction` hook so a below-block-ratio step cannot
147+
race the summary.
148+
3. Use the compaction task promise as the authoritative outcome. Resume only on promise success;
149+
cancellation and failure remain paused.
150+
4. Add an internal guarded resume. Recheck goal ID, exact pause cause, current status, budget, live
151+
turn, pending receipt, and loop idle state. Consume the token before any launch.
152+
5. Let explicit user pause, cancel, and replace actions suppress automatic resume. A user resume
153+
request keeps the token but launches nothing until success. Normalize a persisted compaction
154+
pause after replay to a non-resuming reason. Support same-status reason replacement in the
155+
durable goal fold.
156+
157+
Recommended constants:
158+
159+
- Live pause: `Paused because context compaction is in progress; it will resume after compaction completes`
160+
- Failed finish: `Paused because context compaction did not complete`
161+
- Restart: `Paused because context compaction was interrupted by agent restart`
162+
163+
## Required regression tests
164+
165+
1. V2 after-step background auto-compaction pauses the goal, starts no continuation while running,
166+
completes once, resumes, and launches exactly one continuation.
167+
2. V2 before-step auto-compaction below the block ratio prevents the goal model request until
168+
compaction settles, then continues the preserved turn without a duplicate launch.
169+
3. Cancellation and summarizer failure leave the goal paused with truthful text and no API turn.
170+
4. User pause/cancel/replace, budget stop, duplicate finish, and process replay never auto-resume stale
171+
work.
172+
5. V1 custom-background-strategy parity and TUI pause/resume marker copy pass; v1 default synchronous
173+
behavior remains unchanged.
174+
175+
## Implementation result
176+
177+
V2 now pauses through the official `onWillCompact` hook and also observes the service's active task
178+
from its ordered before-step and after-step gates. The second path closes a hook-scheduling race in
179+
which another step hook can start compaction before the goal hook runs. The pause is durable, but its
180+
task identity and automatic-resume intent remain transient. Successful settlement rechecks goal ID,
181+
pause reason, status, budget, live turn, pending work, and loop idleness before resuming.
182+
183+
V1 exposes start/finish task events from `FullCompaction`, pauses `GoalMode`, and waits at both turn
184+
step boundaries. The existing v1 goal driver continues its preserved turn after success. Both
185+
engines keep failures paused, defer an explicit resume until success, suppress stale resume after a
186+
user action, and replace a persisted live-compaction reason after process replay.
187+
188+
The TUI uses the existing `goal.updated` lifecycle marker. It renders the required pause reason and
189+
does not duplicate goal state inside the generic compaction component.
190+
191+
## Evidence run
192+
193+
- RED: the v1 and v2 production-path tests first observed an active goal during compaction. The v1
194+
reminder test also observed a stale paused reminder before the post-compaction active reminder.
195+
- Focused GREEN: v2 coordination 6/6, v2 goal 114/114, v2 goal operations 12/12, v1 compaction
196+
63 passed with 1 skipped, v1 goal/injection/tools 66/66, and TUI goal markers 10/10.
197+
- Full GREEN: `packages/agent-core-v2` 347 files and 5,661 tests; `packages/agent-core` 228 files,
198+
4,171 passed, 3 expected failures, 30 skipped, and 1 todo.
199+
- Static gates: v1/v2 `tsc` and `tsgo`, v2 import lint, repository no-comment check, root lint, and
200+
`git diff --check` exit 0. Root lint reports existing warnings and no errors.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
2-
"sourceHash": "f1f4f846df4abed27745e6cf05a8cf9a6411b3e5b8e0a4cc66344de574dc5edd",
2+
"sourceHash": "f543ac87a432db7c0b215013c9eac288288452e363003ad0fe874bc517dcf5f2",
33
"sourceFileCount": 399
44
}

0 commit comments

Comments
 (0)