Repository navigation
fix(server): idle compaction re-arms after a server restart #27926
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
478e266
5d8e32a
a3a331e
254431b
b994622
83b03fa
db9c04a
77be0d2
98fe000
50acbf9
c91e8c3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,19 +15,20 @@ import * as Stream from "effect/Stream"; | |
|
|
||
| import { forkParked } from "../serverActivation.ts"; | ||
| import * as ServerSettings from "../serverSettings.ts"; | ||
| import { idleCompactionWindow } from "./IdleCompaction.ts"; | ||
| import { idleCompactionWindow, MAX_PROMPT_CACHE_TTL } from "./IdleCompaction.ts"; | ||
| import * as EventSink from "./EventSink.ts"; | ||
| import * as Orchestrator from "./Orchestrator.ts"; | ||
| import * as ProjectionStore from "./ProjectionStore.ts"; | ||
|
|
||
| /** | ||
| * Beta: when a run completes on a Claude instance set to compact before the | ||
| * cache expires, waits until shortly before its prompt cache expires and sends | ||
| * `/compact` as a visible T3 Code turn. Timers live in | ||
| * memory, so a server restart skips compactions that were still waiting. | ||
| * `/compact` as a visible T3 Code turn. On start it re-arms runs that completed before a restart. | ||
| */ | ||
| export const make = Effect.gen(function* () { | ||
| const orchestrator = yield* Orchestrator.OrchestratorV2; | ||
| const projections = yield* ProjectionStore.ProjectionStoreV2; | ||
| const eventSink = yield* EventSink.EventSinkV2; | ||
| const settings = yield* ServerSettings.ServerSettingsService; | ||
|
|
||
| const compactWhenDue = Effect.fn("IdleCompactionService.compactWhenDue")( | ||
|
|
@@ -131,11 +132,41 @@ export const make = Effect.gen(function* () { | |
| const start = Effect.fn("IdleCompactionService.start")(function* () { | ||
| // Keyed by thread: a newer completed run replaces the waiting timer. | ||
| const timers = yield* FiberMap.make<ThreadId>(); | ||
| // Events after this sequence come from the stream; earlier ones are already in the projections the scan reads. | ||
| const afterSequence = yield* eventSink.latestSequence().pipe(Effect.orDie); | ||
| const rearm = Effect.fn("IdleCompactionService.rearm")( | ||
| function* () { | ||
| const completedAfter = DateTime.subtractDuration(yield* DateTime.now, MAX_PROMPT_CACHE_TTL); | ||
| const candidates = yield* projections.listIdleCompactionCandidates({ completedAfter }); | ||
| yield* Effect.forEach( | ||
| candidates, | ||
| ({ threadId, runId }) => | ||
| // A timer armed by a live event is newer than the scan. | ||
| FiberMap.run(timers, threadId, compactWhenDue(threadId, runId), { | ||
| onlyIfMissing: true, | ||
| }), | ||
| { discard: true }, | ||
| ); | ||
| }, | ||
| Effect.catch((cause) => Effect.logWarning("idle compaction rearm skipped", { cause })), | ||
| ); | ||
| yield* forkParked( | ||
| Stream.runForEach(orchestrator.streamDomainEvents, (event) => | ||
| event.type === "run.updated" && event.payload.status === "completed" | ||
| ? FiberMap.run(timers, event.threadId, compactWhenDue(event.threadId, event.payload.id)) | ||
| : Effect.void, | ||
| Effect.all( | ||
| [ | ||
| Stream.runForEach( | ||
| eventSink.stream({ afterSequence, eventType: "run.updated" }), | ||
| ({ event }) => | ||
| event.type === "run.updated" && event.payload.status === "completed" | ||
| ? FiberMap.run( | ||
| timers, | ||
| event.threadId, | ||
| compactWhenDue(event.threadId, event.payload.id), | ||
| ) | ||
| : Effect.void, | ||
| ), | ||
| rearm(), | ||
| ], | ||
| { concurrency: "unbounded", discard: true }, | ||
|
Comment on lines
+154
to
+169
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Close the gap between the scan and event stream. If a run completes after
|
||
| ), | ||
| ); | ||
| }); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Script executed:
Repository: pingdotgg/t3code
Length of output: 1765
Script executed:
Repository: pingdotgg/t3code
Length of output: 41497
Script executed:
Repository: pingdotgg/t3code
Length of output: 16580
Script executed:
Repository: pingdotgg/t3code
Length of output: 12653
Script executed:
Repository: pingdotgg/t3code
Length of output: 2595
Script executed:
Repository: pingdotgg/t3code
Length of output: 35675
Script executed:
Repository: pingdotgg/t3code
Length of output: 3128
Retry the startup candidate read before skipping rearm.
If
listIdleCompactionCandidateshas a transient SQLite read failure, the catch logs the error and ends the only startup scan. A pre-restart completion has no laterrun.updatedevent, so it gets no timer and its automatic/compactis skipped. Add a bounded retry to the candidate read, while keeping the existing catch so startup and live events continue.Suggested fix
Committable suggestion