Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
98b3cb3
fix(antigravity): start fresh after a lost thinking signature
Nov 1, 2026
833ae1c
fix(server): keep broken history invalid after sign-out
Nov 1, 2026
af46368
fix(antigravity): preserve broken history across dispatch paths
maria-rcks Nov 3, 2026
13c48d4
fix(antigravity): start fresh after a lost thinking signature
Nov 1, 2026
16956f9
fix(server): keep broken history invalid after sign-out
Nov 1, 2026
77851ea
fix(antigravity): preserve broken history across dispatch paths
maria-rcks Nov 3, 2026
8e52820
fix(antigravity): retain broken history when stop races outcome
maria-rcks Nov 3, 2026
4eae6f6
chore: merge main into issue-watch/28870
maria-rcks Nov 3, 2026
57868a9
test(antigravity): keep stop coverage in the existing outcome case
maria-rcks Nov 3, 2026
a0c408d
chore: integrate updated issue-watch/28870 head
maria-rcks Nov 3, 2026
1d84b5d
chore: merge main into issue-watch/28870
maria-rcks Nov 4, 2026
330a74d
chore: merge main into issue-watch/28870
shivamhwp Nov 4, 2026
891a00e
test(antigravity): drop the quoted-reply row that passes on main
shivamhwp Nov 4, 2026
387b9e9
fix(acp): tag broken native history errors
shivamhwp Nov 4, 2026
c74af76
Merge remote-tracking branch 'origin/main' into issue-watch/28870
shivamhwp Nov 4, 2026
b9e23a1
Merge remote-tracking branch 'origin/main' into issue-watch/28870
shivamhwp Nov 5, 2026
f1762d8
docs(acp): clarify recovery after broken native history
shivamhwp Nov 5, 2026
fc24779
Merge remote-tracking branch 'origin/main' into issue-watch/28870
shivamhwp Nov 5, 2026
1a70c97
Merge remote-tracking branch 'origin/main' into issue-watch/28870
shivamhwp Nov 5, 2026
7a1af72
fix(server): resume-fallback recovery no longer reintroduces context …
shivamhwp Nov 5, 2026
9b3f640
Merge remote-tracking branch 'origin/main' into issue-watch/28870
shivamhwp Nov 5, 2026
789d8a8
Merge remote-tracking branch 'origin/main' into issue-watch/28870
shivamhwp Nov 7, 2026
623c24e
fix(server): keep a broken Antigravity session marked across instance…
shivamhwp Nov 7, 2026
154f5cd
test(server): cover broken history across queued instance handover
shivamhwp Nov 7, 2026
7338d41
chore: merge main into issue-watch/28870
shivamhwp Nov 10, 2026
c41e125
Merge remote-tracking branch 'origin/main' into t3/antigravity-test-trim
shivamhwp Nov 10, 2026
22e0c58
test(antigravity): drop the lost-signature tests another row already …
shivamhwp Nov 10, 2026
596651e
Merge remote-tracking branch 'origin/main' into issue-watch/28870
shivamhwp Nov 10, 2026
1377f90
chore: merge main into issue-watch/28870
shivamhwp Nov 12, 2026
505568f
Merge branch 'main' into issue-watch/28870
shivamhwp Nov 12, 2026
07e85c3
Merge branch 'main' into issue-watch/28870
shivamhwp Nov 12, 2026
fd63f32
Merge branch 'main' into issue-watch/28870
shivamhwp Nov 12, 2026
004ad52
Merge branch 'main' into issue-watch/28870
shivamhwp Nov 12, 2026
8ed1910
Merge main (587deab7d31) into issue-watch/28870
shivamhwp Nov 13, 2026
c4ef466
Merge main (0465d4a87d0) into issue-watch/28870
shivamhwp Nov 13, 2026
e256f44
Merge main (ee645553a97) into issue-watch/28870
shivamhwp Nov 13, 2026
3092bce
Merge main (10fbff5ab6d) into issue-watch/28870
shivamhwp Nov 13, 2026
637b2d7
Merge origin/main into issue-watch/28870
juliusmarminge Nov 14, 2026
fffa532
Merge main (6cb47acfb8c) into issue-watch/28870
shivamhwp Nov 14, 2026
55b008c
Merge main (f52e5c19198) into issue-watch/28870
shivamhwp Nov 14, 2026
3a2a435
Merge main (0cb9328ea00) into issue-watch/28870
shivamhwp Nov 14, 2026
50b5c3c
Merge remote-tracking branch 'origin/main' into t3/28889-live-antigra…
shivamhwp Nov 15, 2026
5ca0848
fix(antigravity): a restarted server no longer resumes an unusable se…
shivamhwp Nov 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 87 additions & 8 deletions apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1329,6 +1329,10 @@ describe("AcpAdapterV2", () => {
);
it.live("fails an end_turn the outcome check rejects and restarts before the next turn", () =>
Effect.gen(function* () {
const checking = yield* Deferred.make<void>();
const releaseCheck = yield* Deferred.make<void>();
const cancelled = yield* Deferred.make<void>();
const idAllocator = yield* IdAllocator.IdAllocatorV2;
const instanceId = ProviderInstanceId.make("acp-test-prompt-outcome");
const threadId = ThreadId.make("thread-acp-prompt-outcome");
const quota = {
Expand All @@ -1338,6 +1342,12 @@ describe("AcpAdapterV2", () => {
retryable: true,
resetAt: "2030-01-01T00:00:00.000Z",
} as const;
const poisoned = {
class: "provider_error",
message: "Saved history is unusable.",
code: "INVALID_THINKING_SIGNATURE",
retryable: true,
} as const;
// The runtime ordinal of each prompt, and the session each check watched.
const promptRuntimes: Array<number> = [];
const promptTexts: Array<string> = [];
Expand All @@ -1351,14 +1361,27 @@ describe("AcpAdapterV2", () => {
watchPromptOutcome: (sessionId) =>
Effect.sync(() => {
watchedSessions.push(sessionId);
const failed = watchedSessions.length === 1;
return () => Effect.succeed(failed ? quota : undefined);
const outcome =
watchedSessions.length === 1
? { failure: quota, threadDisposition: "reusable" as const }
: watchedSessions.length >= 3
? { failure: poisoned, threadDisposition: "broken" as const }
: undefined;
const interrupted = watchedSessions.length === 4;
return () =>
interrupted
? Deferred.succeed(checking, undefined).pipe(
Effect.andThen(Deferred.await(releaseCheck)),
Effect.as(outcome),
)
: Effect.succeed(outcome);
}),
makeRuntime: makeMockRuntime({
childProcessSpawner: yield* ChildProcessSpawner.ChildProcessSpawner,
mockAgentPath: yield* bundledMockAgentPath,
wrapRuntime: (runtime, runtimeOrdinal) => ({
...runtime,
cancel: Deferred.succeed(cancelled, undefined).pipe(Effect.asVoid),
prompt: (payload) =>
Effect.sync(() => {
promptRuntimes.push(runtimeOrdinal);
Expand Down Expand Up @@ -1402,26 +1425,82 @@ describe("AcpAdapterV2", () => {
ordinal,
}),
);
const events = yield* runtime.events.pipe(
Stream.takeUntil((event) => event.type === "turn.terminal"),
Stream.runCollect,
return Array.from(
yield* runtime.events.pipe(
Stream.takeUntil((event) => event.type === "turn.terminal"),
Stream.runCollect,
),
);
return events.at(-1);
});

const failed = yield* runTurn(1);
const failed = (yield* runTurn(1)).at(-1);
if (failed?.type !== "turn.terminal" || failed.status !== "failed") {
return assert.fail("expected a failed terminal");
}
assert.deepEqual(failed.failure, quota);
const completed = yield* runTurn(2);
const completed = (yield* runTurn(2)).at(-1);
assert.equal(completed?.type === "turn.terminal" ? completed.status : null, "completed");
assert.deepEqual(promptRuntimes, [1, 2], "the next turn runs on a fresh runtime");
// The fresh runtime has no record of what reached the agent, so it sends T3's notes again.
assert.include(promptTexts[1] ?? "", "<runtime_info>");
assert.include(promptTexts[1] ?? "", "T3 Code interaction mode: Default");
assert.equal(watchedSessions.length, 2);
assert.equal(watchedSessions[0], watchedSessions[1]);

// Poisoned native history marks the thread broken; its persisted error row is never resumed.
const poisonedEvents = yield* runTurn(3);
const broken = poisonedEvents.at(-1);
if (broken?.type !== "turn.terminal" || broken.status !== "failed") {
return assert.fail("expected a failed terminal");
}
assert.deepEqual(broken.failure, poisoned);
assert.equal(broken.threadDisposition, "broken");
const brokenThread = poisonedEvents.findLast(
(event) => event.type === "provider_thread.updated",
);
assert.equal(
brokenThread?.type === "provider_thread.updated"
? brokenThread.providerThread.status
: null,
"error",
);
const resumed = yield* Effect.result(
runtime.resumeThread({
threadId,
providerThread: { ...providerThread, status: "error" },
modelSelection,
runtimePolicy,
}),
);
assert.equal(
resumed._tag === "Failure" ? resumed.failure._tag : null,
"ProviderAdapterResumeThreadError",
);

const turn = yield* Effect.forkChild(runTurn(4));
yield* Deferred.await(checking);
const stop = yield* Effect.forkChild(
runtime.interruptTurn({
providerThread,
providerTurnId: idAllocator.derive.providerTurn({
driver: ACP_TEST_DRIVER,
nativeTurnId: acpScopedNativeId(instanceId, `${watchedSessions[3]}:turn:4`),
}),
}),
);
yield* Deferred.await(cancelled);
yield* Deferred.succeed(releaseCheck, undefined);
const interruptedEvents = yield* Fiber.join(turn);
yield* Fiber.join(stop);
const interrupted = interruptedEvents.at(-1);
assert.equal(
interrupted?.type === "turn.terminal" ? interrupted.status : null,
"interrupted",
);
assert.equal(
interrupted?.type === "turn.terminal" ? interrupted.threadDisposition : null,
"broken",
);
}).pipe(Effect.provide(layerTest)),
);
it.live("retains returned usage when Stop finalizes before the prompt callback", () =>
Expand Down
37 changes: 37 additions & 0 deletions apps/server/src/orchestration-v2/ContextHandoffService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
import { createOrchestrationV2TurnItemVisibility } from "@t3tools/shared/orchestrationV2Timeline";
import type { ProjectionStoreV2Shape } from "./ProjectionStore.ts";
import { cancelledBeforeStart } from "./RestartBackgroundNote.ts";

export class ContextHandoffPrepareError extends Schema.TaggedError<ContextHandoffPrepareError>()(
"ContextHandoffPrepareError",
Expand Down Expand Up @@ -116,6 +117,42 @@ function compactText(text: string, maxLength = 240): string {
return `${compacted.slice(0, last >= 0xd800 && last <= 0xdbff ? end - 1 : end)}...`;
}

/** A fresh-session schedule run starts a side conversation; the thread's own one continues past it. */
export function isScheduledFreshRun(
run: Pick<OrchestrationV2Run, "freshContext" | "userMessageId">,
turnItems: OrchestrationV2ThreadProjection["turnItems"],
): boolean {
// Turn items, unlike messages, are not narrowed to the runs since the latest compaction.
return (
run.freshContext === true &&
turnItems.some(
(item) =>
item.type === "user_message" &&
item.messageId === run.userMessageId &&
item.scheduledTaskId !== undefined,
)
);
}

/** Ordinal of the latest started fresh-context run. Runs before it never reach a provider again. */
export function contextBoundaryOrdinal(
projection: Pick<OrchestrationV2ThreadProjection, "runs" | "turnItems">,
): number {
let boundary = 0;
for (const run of projection.runs) {
if (
run.freshContext === true &&
run.status !== "queued" &&
!cancelledBeforeStart(run) &&
run.ordinal > boundary &&
!isScheduledFreshRun(run, projection.turnItems)
) {
boundary = run.ordinal;
}
}
return boundary;
}

export function visibleDeltaRunOrdinals(
projection: { readonly runs: ReadonlyArray<Pick<OrchestrationV2Run, "id" | "ordinal">> },
items: ReadonlyArray<Pick<OrchestrationV2TurnItem, "runId">>,
Expand Down
43 changes: 5 additions & 38 deletions apps/server/src/orchestration-v2/Orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ import {
import { CommandReceiptStoreV2, type CommandReceiptV2 } from "./CommandReceiptStore.ts";
import {
ContextHandoffServiceV2,
contextBoundaryOrdinal,
isScheduledFreshRun,
readForkHistoryItems,
readInheritedHandoffItems,
visibleConversationItems,
Expand Down Expand Up @@ -1364,42 +1366,6 @@ function appendContextHandoffId(
return handoffId === null ? handoffIds : Array.from(new Set([...handoffIds, handoffId]));
}

/** A fresh-session schedule run starts a side conversation; the thread's own one continues past it. */
function isScheduledFreshRun(
run: Pick<OrchestrationV2Run, "freshContext" | "userMessageId">,
turnItems: OrchestrationV2ThreadProjection["turnItems"],
): boolean {
// Turn items, unlike messages, are not narrowed to the runs since the latest compaction.
return (
run.freshContext === true &&
turnItems.some(
(item) =>
item.type === "user_message" &&
item.messageId === run.userMessageId &&
item.scheduledTaskId !== undefined,
)
);
}

/** Ordinal of the latest started fresh-context run. Runs before it never reach a provider again. */
function contextBoundaryOrdinal(
projection: Pick<OrchestrationV2ThreadProjection, "runs" | "turnItems">,
): number {
let boundary = 0;
for (const run of projection.runs) {
if (
run.freshContext === true &&
run.status !== "queued" &&
!cancelledBeforeStart(run) &&
run.ordinal > boundary &&
!isScheduledFreshRun(run, projection.turnItems)
) {
boundary = run.ordinal;
}
}
return boundary;
}

/** The provider thread a message continues: the active one, unless a fresh-session schedule run started it. */
function conversationProviderThread(
projection: Pick<
Expand Down Expand Up @@ -2745,6 +2711,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
const deliveryProviderThread = canResumeAcrossInstances
? {
...queuedProviderThread,
status: activeProviderThread.status,
nativeThreadRef: activeProviderThread.nativeThreadRef,
nativeConversationHeadRef: activeProviderThread.nativeConversationHeadRef,
nativeMetadata: activeProviderThread.nativeMetadata,
Expand Down Expand Up @@ -2936,7 +2903,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
const providerThread: OrchestrationV2ProviderThread = {
...deliveryProviderThread,
providerSessionId,
status: "not_loaded",
status: deliveryProviderThread.status === "error" ? "error" : "not_loaded",
firstRunOrdinal: queuedProviderThread.firstRunOrdinal ?? ordinal,
lastRunOrdinal: ordinal,
handoffIds: appendContextHandoffId(
Expand Down Expand Up @@ -8721,7 +8688,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
: null;
const providerThread: OrchestrationV2ProviderThread = {
...ensuredProviderThread,
status: "active",
status: ensuredProviderThread.status === "error" ? "error" : "active",
firstRunOrdinal: ensuredProviderThread.firstRunOrdinal ?? ordinal,
lastRunOrdinal: ordinal,
handoffIds: [
Expand Down
18 changes: 15 additions & 3 deletions apps/server/src/orchestration-v2/ProviderTurnStartService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2915,11 +2915,20 @@ effectIt.effect("does not mistake a failed state read for a superseded run", ()
}),
);

effectIt.effect(
"signs out the existing native provider before opening the newly selected provider",
() =>
effectIt.effect.each(["not_loaded", "error"] as const)(
"signs out the existing native provider before opening the newly selected provider with status %s",
(providerThreadStatus) =>
Effect.gen(function* () {
const harness = makeLocalCommandHarness({ text: "/logout", previousNativeSession: true });
const initial = harness.projection();
harness.setProjection({
...initial,
providerThreads: initial.providerThreads.map((thread) =>
thread.id === "new-provider-thread"
? { ...thread, status: providerThreadStatus }
: thread,
),
});

yield* harness.start;
yield* harness.start;
Expand All @@ -2945,6 +2954,9 @@ effectIt.effect(
]);
expect(projection.providerTurns).toEqual([]);
expect(projection.checkpoints).toEqual([]);
expect(
projection.providerThreads.find((thread) => thread.id === "new-provider-thread")?.status,
).toBe(providerThreadStatus);
}),
);

Expand Down
44 changes: 34 additions & 10 deletions apps/server/src/orchestration-v2/ProviderTurnStartService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,12 @@ export const layer: Layer.Layer<
},
providerThreadUpdate: {
...providerThread,
status: providerThread.nativeThreadRef === null ? "not_loaded" : "idle",
status:
providerThread.status === "error"
? "error"
: providerThread.nativeThreadRef === null
? "not_loaded"
: "idle",
updatedAt: now,
},
});
Expand Down Expand Up @@ -780,6 +785,12 @@ export const layer: Layer.Layer<
run,
checkpointScope,
});
const brokenNativeThread = providerThread.status === "error";
// Opened without the broken native id, a session that was not live starts fresh.
const opensFreshSession =
brokenNativeThread &&
providerThread.nativeThreadRef !== null &&
!(yield* providerSessions.isLive(providerSessionId));
const sessionResult = yield* Effect.result(
providerSessions.open({
threadId: projection.thread.id,
Expand All @@ -789,7 +800,7 @@ export const layer: Layer.Layer<
...(existingSessionProjection === undefined
? {}
: { resumeFromSession: existingSessionProjection }),
...(providerThread.nativeThreadRef?.nativeId == null
...(providerThread.nativeThreadRef?.nativeId == null || brokenNativeThread
? {}
: { initialNativeThreadId: providerThread.nativeThreadRef.nativeId }),
...(providerThread.nativeMetadata?.itemIdentityVersion === undefined
Expand Down Expand Up @@ -1071,6 +1082,13 @@ export const layer: Layer.Layer<
type: "provider_resume_fallback",
});
const createdAt = yield* DateTime.now;
const history = yield* projectionStore.getTurnStartHistory(input.threadId);
// Turn-start context carries only this run's turn items; the boundary needs the thread's.
const boundary = ContextHandoffService.contextBoundaryOrdinal({
runs: projection.runs,
turnItems: history,
});
const from = Math.max(1, boundary);
const handoff = yield* contextHandoffService.prepareProviderHandoff({
threadId: projection.thread.id,
targetRunId: run.id,
Expand All @@ -1079,15 +1097,18 @@ export const layer: Layer.Layer<
toProviderThreadId: providerThread.id,
fromProviderInstanceId: providerThread.providerInstanceId,
toProviderInstanceId: run.providerInstanceId,
coveredRunOrdinals: { from: 1, to: Math.max(1, run.ordinal - 1) },
coveredRunOrdinals: { from, to: Math.max(from, run.ordinal - 1) },
strategy: "full_thread_summary",
runs: projection.runs,
items: (yield* projectionStore.getTurnStartHistory(input.threadId)).filter(
(item) =>
item.runId === null ||
projection.runs.some(
(source) => source.id === item.runId && source.ordinal < run.ordinal,
),
items: history.filter((item) =>
item.runId === null
? boundary === 0
: projection.runs.some(
(source) =>
source.id === item.runId &&
source.ordinal >= from &&
source.ordinal < run.ordinal,
),
),
createdAt,
});
Expand Down Expand Up @@ -1417,7 +1438,10 @@ export const layer: Layer.Layer<
providerSessionId,
// Clear the native ref to force a fresh session while preserving row identity.
existingProviderThread: { ...providerThread, nativeThreadRef: null },
forceNewNativeThread: true,
// A fresh session that never loaded the broken history needs no replacement.
...(opensFreshSession && resumed._tag === "Failure"
? {}
: { forceNewNativeThread: true }),
}),
);
if (replacement === undefined) return undefined;
Expand Down
Loading
Loading