Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d424e6a
fix(server): show the native history of imported ACP sessions
only21mil Oct 20, 2026
526d793
fix(server): bound the ACP session close and open history imports thr…
only21mil Oct 20, 2026
da62b61
fix(server): imported ACP history survives forks and failed reads
only21mil Oct 24, 2026
b14157f
fix(server): repair ACP history without losing thread edits or Delete…
only21mil Oct 24, 2026
a02c74a
refactor(server): drop the pre-marker ACP history branch no release w…
only21mil Oct 31, 2026
077759d
fix(server): fit the ACP history import to current main
only21mil Oct 31, 2026
62458d6
fix(server): an ACP agent that never answers can't hold the import lock
only21mil Nov 1, 2026
77ab499
test(server): a hung native session releases the ACP import lock
only21mil Nov 1, 2026
8e0a44c
merge: update ACP history import with main
maria-rcks Nov 2, 2026
3735605
fix(server): avoid replaying imported ACP history into its native ses…
maria-rcks Nov 2, 2026
fa34498
refactor(server): move the ACP session import into a service
only21mil Nov 8, 2026
93ca03c
merge: update the ACP session import with main
only21mil Nov 8, 2026
de09c91
test(server): adapt the ACP import fixtures to main
only21mil Nov 8, 2026
061f6e9
fix(server): imported ACP thinking shows as reasoning, not replies
only21mil Nov 10, 2026
54f9223
test(server): cover whole replayed ACP thoughts as reasoning
only21mil Nov 10, 2026
bfea397
merge: update the ACP session import with main
maria-rcks Nov 10, 2026
57c141a
fix(server): drop imports the main merge duplicated in ws tests
maria-rcks Nov 10, 2026
8ee6ebc
Merge remote-tracking branch 'origin/main' into r9/g14-h1
juliusmarminge Nov 11, 2026
19ade02
test: trim ACP import tests to the regressions they catch
juliusmarminge Nov 11, 2026
bd36eaf
Merge remote-tracking branch 'origin/main' into r9/g14-h1
juliusmarminge Nov 11, 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
4 changes: 4 additions & 0 deletions apps/server/scripts/acp-mock-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const emitInterleavedAssistantToolCalls =
process.env.T3_ACP_EMIT_INTERLEAVED_ASSISTANT_TOOL_CALLS === "1";
const emitV2Fidelity = process.env.T3_ACP_EMIT_V2_FIDELITY === "1";
const emitNotice = process.env.T3_ACP_EMIT_NOTICE === "1";
const hangCloseSession = process.env.T3_ACP_HANG_CLOSE_SESSION === "1";
const vibeRetryOutcome = process.env.T3_ACP_VIBE_RETRY_OUTCOME;
const emitGenericToolPlaceholders = process.env.T3_ACP_EMIT_GENERIC_TOOL_PLACEHOLDERS === "1";
const emitInTurnTaskOutputThenLateDuplicate =
Expand Down Expand Up @@ -731,6 +732,9 @@ const program = Effect.gen(function* () {
yield* agent.handleCloseSession(() =>
Effect.gen(function* () {
yield* requireAuthentication();
if (hangCloseSession) {
return yield* Effect.never;
}
return {};
}),
);
Expand Down
82 changes: 81 additions & 1 deletion apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2620,11 +2620,30 @@ describe("AcpAdapterV2", () => {
content: { type: "text", text: "after plan" },
},
});
yield* sessionUpdateHandler!({
sessionId: "mock-session-1",
update: {
sessionUpdate: "agent_thought_chunk",
content: { type: "text", text: "weighing it" },
},
});
yield* sessionUpdateHandler!({
sessionId: "mock-session-1",
update: {
sessionUpdate: "agent_thought",
messageId: "whole-thought",
content: [{ type: "text", text: "settled on it" }],
},
});

const snapshot = yield* runtime.readThreadSnapshot({ providerThread });
assert.deepEqual(
snapshot.messages.map((message) => message.text),
["before plan", "after plan"],
["before plan", "after plan", "weighing it", "settled on it"],
);
assert.deepEqual(
snapshot.reasoningMessageIds,
snapshot.messages.slice(2).map((message) => message.id),
);
}).pipe(Effect.provide(layerTest), Effect.scoped),
);
Expand Down Expand Up @@ -5453,6 +5472,67 @@ describe("AcpAdapterV2", () => {
}).pipe(Effect.provide(layerTest), Effect.scoped),
);

it.effect("closes the ACP runtime when the agent never answers session/close", () =>
Effect.gen(function* () {
const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const path = yield* Path.Path;
const selfInvocation = yield* resolveSelfInvocation();
const mockAgentPath = yield* path.fromFileUrl(
new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),
);
const protocolEvents = yield* Queue.bounded<EffectAcpProtocol.AcpProtocolLogEvent>(256);
const makeRuntime = makeMockRuntime({
childProcessSpawner,
mockAgentPath,
protocolEvents,
environment: { T3_ACP_HANG_CLOSE_SESSION: "1" },
});
let runtimeClosed = false;
const instanceId = ProviderInstanceId.make("acp-test");
const adapter = yield* makeAcpAdapterV2({
instanceId,
flavor: {
driver: ACP_TEST_DRIVER,
capabilities: AcpProviderCapabilitiesV2,
makeRuntime: (runtimeInput) =>
Effect.addFinalizer(() =>
Effect.sync(() => {
runtimeClosed = true;
}),
).pipe(Effect.andThen(makeRuntime(runtimeInput))),
},
selfInvocation,
});
const sessionScope = yield* Scope.make();
yield* adapter
.openSession({
threadId: ThreadId.make("thread-acp-unanswered-close"),
providerSessionId: ProviderSessionId.make("provider-session-acp-unanswered-close"),
modelSelection: { instanceId, model: "default" },
runtimePolicy: ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
runtimeMode: "full-access",
interactionMode: "default",
cwd: process.cwd(),
}),
})
.pipe(Effect.provideService(Scope.Scope, sessionScope));
const closing = yield* Scope.close(sessionScope, Exit.void).pipe(Effect.forkDetach);
yield* Stream.fromQueue(protocolEvents).pipe(
Stream.filter(
(event) => event.direction === "outgoing" && rawProtocolMethod(event) === "session/close",
),
Stream.runHead,
);
yield* TestClock.adjust("10 seconds");
const closed = yield* Fiber.await(closing).pipe(
Effect.timeoutOption("5 seconds"),
TestClock.withLive,
);
assert.isTrue(Option.isSome(closed));
assert.isTrue(runtimeClosed);
}).pipe(Effect.provide(layerTest)),
);

// Oracle: Grok incident frames. Command completion must release the turn even
// when the CLI keeps the prompt RPC and process alive. Command polls stay visible.
it.live(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ it("reissues imported context until a V2 run completes", () => {
legacyImportItemCount: 2,
}),
);
assert.isFalse(
shouldPrepareLegacyImportHandoff({
historyOrigin: "v1_import",
hasCompletedRun: false,
legacyImportItemCount: 2,
hasNativeHistory: true,
}),
);
assert.isFalse(
shouldPrepareLegacyImportHandoff({
historyOrigin: "v1_import",
Expand Down
9 changes: 8 additions & 1 deletion apps/server/src/orchestration-v2/Orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1007,12 +1007,16 @@ function latestContextTransfer(
}

export function shouldPrepareLegacyImportHandoff(input: {
readonly hasNativeHistory?: boolean;
readonly hasCompletedRun: boolean;
readonly historyOrigin: OrchestrationV2AppThread["historyOrigin"];
readonly legacyImportItemCount: number;
}): boolean {
return (
input.historyOrigin === "v1_import" && !input.hasCompletedRun && input.legacyImportItemCount > 0
input.historyOrigin === "v1_import" &&
!input.hasNativeHistory &&
!input.hasCompletedRun &&
input.legacyImportItemCount > 0
);
}

Expand Down Expand Up @@ -6872,6 +6876,9 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
nativeThreadId: `pending:${runId}`,
});
const legacyImportHandoff = shouldPrepareLegacyImportHandoff({
hasNativeHistory:
activeProviderThread?.driver === "acpRegistry" &&
activeProviderThread.nativeThreadRef !== null,
historyOrigin: projection.thread.historyOrigin,
hasCompletedRun: latestCompletedRun !== undefined,
legacyImportItemCount: legacyImportItems.length,
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/orchestration-v2/runtimeLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,8 @@ const layerAgentSessionImporterProvided = AgentSessionImporter.layer.pipe(
layerEventSink,
IdAllocator.layer,
ProviderSessionRuntime.layer,
ProviderAdapterRegistry.layerFromProviderInstanceRegistry,
ThreadCommandExecutor.layer,
),
),
);
Expand Down
Loading
Loading