Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
fce9e60
fix(acp): 重开会话时保留已投递指令记录
nateEc Oct 25, 2026
6072d28
fix(acp): 保留引导中断后的进程内指令状态
nateEc Oct 25, 2026
10d2a22
fix(acp): 新指令发送前失效旧投递记录
nateEc Oct 25, 2026
fd2a787
fix(acp): 清理空闲压缩和回滚后的指令缓存
nateEc Oct 25, 2026
206ef81
Merge remote-tracking branch 'github/main' into takeover-24740
maria-rcks Oct 26, 2026
8bf89eb
fix(acp): persist instruction invalidation across idle sessions
maria-rcks Oct 26, 2026
30702d9
fix(acp): retain invalidations through run finalization
maria-rcks Oct 26, 2026
767503e
fix(acp): acknowledge instructions after cancelled prompts
maria-rcks Oct 26, 2026
cb0b437
fix(acp): preserve invalidation cleanup after session release
maria-rcks Oct 26, 2026
473afc2
fix(acp): 恢复会话时保留完整原生元数据
nateEc Oct 26, 2026
7f20d1b
fix(acp): 在主线会话替换路径中清除旧指令状态
nateEc Oct 28, 2026
0d78d83
fix(server): 让指令失效重试释放会话清理锁
nateEc Oct 28, 2026
25b3b73
merge: sync main into acp instruction state fix
maria-rcks Oct 31, 2026
27752f2
fix(acp): invalidate instructions after deferred finalization
maria-rcks Oct 31, 2026
bb028cd
merge: sync main into acp instruction state fix
maria-rcks Nov 2, 2026
a4e10f9
fix(acp): preserve invalidation ownership during cleanup
maria-rcks Nov 2, 2026
e10de58
test(acp): expect retained instructions in kiro resume replay
maria-rcks Nov 2, 2026
81cf36e
chore(acp): 合并主线并保留指令状态修复
nateEc Nov 7, 2026
755344d
fix(acp): 持久化失效失败时保留运行中的会话
nateEc Nov 7, 2026
8af2a5d
Merge remote-tracking branch 'github/main' into codex/fix-24465-acp-i…
maria-rcks Nov 11, 2026
dc29543
chore(sync): 合并主线到 ACP 指令恢复修复
nateEc Nov 12, 2026
b87e7fc
fix(server): 释放 ACP 会话时补写指令失效记录
nateEc Nov 12, 2026
c6f4f74
fix(acp): 保留关闭前尚未消费的指令失效记录
nateEc Nov 12, 2026
4255923
fix(acp): 恢复失败时保留指令失效状态
nateEc Nov 12, 2026
5a78f25
Merge origin/main into codex/fix-24465-acp-instruction-state
juliusmarminge Nov 12, 2026
9297edf
fix(acp): invalidate instruction receipts after focused /compact
juliusmarminge Nov 12, 2026
a9c5ac5
chore(acp): 协调指令记录与主线的压缩逻辑
nateEc Nov 14, 2026
973d7b8
fix(acp): 保留压缩后重试和拒绝回执的语义
nateEc Nov 14, 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
10 changes: 10 additions & 0 deletions apps/server/scripts/acp-mock-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ const failLoadSessionAfterConfigReplay =
process.env.T3_ACP_FAIL_LOAD_SESSION_AFTER_CONFIG_REPLAY === "1";
const emitLoadReplay = process.env.T3_ACP_EMIT_LOAD_REPLAY === "1";
const failPrompt = process.env.T3_ACP_FAIL_PROMPT === "1";
const echoAcceptedPrompt = process.env.T3_ACP_ECHO_ACCEPTED_PROMPT === "1";
// JSON for the `data` of an "Internal error" response to `session/new`, as
// Antigravity sends when its runtime cannot reach its local harness.
const newSessionErrorData =
Expand Down Expand Up @@ -868,6 +869,15 @@ const program = Effect.gen(function* () {
yield* agent.handlePrompt((request) =>
Effect.gen(function* () {
const requestedSessionId = String(request.sessionId ?? sessionId);
if (echoAcceptedPrompt) {
yield* agent.client.sessionUpdate({
sessionId: requestedSessionId,
update: {
sessionUpdate: "user_message_chunk",
content: { type: "text", text: "Accepted test prompt" },
},
});
}
beginAcpMockPrompt(cancelledSessions, requestedSessionId);
promptCount += 1;
if (idleError === "grok-background" && promptCount === 1) {
Expand Down
295 changes: 287 additions & 8 deletions apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts

Large diffs are not rendered by default.

295 changes: 291 additions & 4 deletions apps/server/src/orchestration-v2/ProviderSessionManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
ProjectId,
ProviderDriverKind,
ProviderInstanceId,
type ProviderSessionId,
ProviderSessionId,
RunId,
ThreadId,
TurnItemId,
Expand Down Expand Up @@ -158,7 +158,7 @@ const layerPausingAttachEventSink = (pause: {
).pipe(Layer.provide(layerTestEventSink));

const layerBeforeUnchangedWriteEventSink = (
before: Ref.Ref<Effect.Effect<void>>,
before: Ref.Ref<Effect.Effect<void, EventSink.EventSinkWriteError>>,
// One effect runs before each turn-item write, in order.
beforeTurnItems: Ref.Ref<ReadonlyArray<Effect.Effect<void>>> | undefined,
) =>
Expand Down Expand Up @@ -445,6 +445,9 @@ const makeProviderAdapter = Effect.fnUntraced(function* (
state: Ref.Ref<TestProviderRuntimeState>,
options: {
readonly failEventStream?: boolean;
readonly readInstructionInvalidations?: Effect.Effect<
ReadonlyArray<OrchestrationV2ProviderThread>
>;
readonly normalizeThreadLoadModelSelection?: (selection: ModelSelection) => ModelSelection;
readonly capabilities?: OrchestrationV2ProviderCapabilities;
readonly extensionStatuses?: OrchestrationV2ProviderSession["extensionStatuses"];
Expand Down Expand Up @@ -538,6 +541,9 @@ const makeProviderAdapter = Effect.fnUntraced(function* (
instanceId: ProviderInstanceId.make("codex"),
driver: CODEX_DRIVER,
providerSessionId: input.providerSessionId,
...(options.readInstructionInvalidations === undefined
? {}
: { readInstructionInvalidations: options.readInstructionInvalidations }),
...(options.normalizeThreadLoadModelSelection === undefined
? {}
: { normalizeThreadLoadModelSelection: options.normalizeThreadLoadModelSelection }),
Expand Down Expand Up @@ -631,7 +637,10 @@ function layerTest(input: {
readonly failReleaseEventWrites?: boolean;
readonly flakyReleaseWrites?: FlakyReleaseWrites;
readonly pauseAttachWrite?: Parameters<typeof layerPausingAttachEventSink>[0];
readonly beforeUnchangedWrite?: Ref.Ref<Effect.Effect<void>>;
readonly beforeUnchangedWrite?: Ref.Ref<Effect.Effect<void, EventSink.EventSinkWriteError>>;
readonly readInstructionInvalidations?: Effect.Effect<
ReadonlyArray<OrchestrationV2ProviderThread>
>;
readonly beforeTurnItemsWrite?: Ref.Ref<ReadonlyArray<Effect.Effect<void>>>;
readonly pauseResolve?: {
readonly armed: Ref.Ref<boolean>;
Expand Down Expand Up @@ -668,6 +677,9 @@ function layerTest(input: {
const layerRegistry = Layer.unwrap(
makeProviderAdapter(input.state, {
failEventStream: input.failEventStream ?? false,
...(input.readInstructionInvalidations === undefined
? {}
: { readInstructionInvalidations: input.readInstructionInvalidations }),
...(input.normalizeThreadLoadModelSelection === undefined
? {}
: { normalizeThreadLoadModelSelection: input.normalizeThreadLoadModelSelection }),
Expand Down Expand Up @@ -3478,7 +3490,9 @@ it.effect.each(["a runtime whose tasks can outlive it", "server shutdown"] as co
it.effect("ProviderSessionManagerV2 keeps provider updates that land while release clears", () =>
Effect.gen(function* () {
const state = yield* Ref.make(emptyState);
const beforeUnchangedWrite = yield* Ref.make<Effect.Effect<void>>(Effect.void);
const beforeUnchangedWrite = yield* Ref.make<
Effect.Effect<void, EventSink.EventSinkWriteError>
>(Effect.void);
const beforeTurnItemsWrite = yield* Ref.make<ReadonlyArray<Effect.Effect<void>>>([]);
const effect = Effect.gen(function* () {
const manager = yield* ProviderSessionManager.ProviderSessionManagerV2;
Expand Down Expand Up @@ -4130,6 +4144,279 @@ it.effect("ProviderSessionManagerV2 settles a request the event pump persists du
}),
);

it.effect.each([
"current",
"queued-completion",
"released-completion",
"repeated-close",
"contended-close",
"detached-close",
"failed-close",
"failed-event",
"release-after-failed-event",
"retry-after-failed-release",
"detach-after-failed-event",
"release-before-event",
"newer-run",
"newer-session",
] as const)(
"ProviderSessionManagerV2 persists instruction invalidation only for its owner (%s)",
(owner) =>
Effect.gen(function* () {
const state = yield* Ref.make(emptyState);
const beforeUnchangedWrite = yield* Ref.make<
Effect.Effect<void, EventSink.EventSinkWriteError>
>(Effect.void);
const adapterInvalidations = yield* Ref.make<ReadonlyArray<OrchestrationV2ProviderThread>>(
[],
);
yield* Effect.gen(function* () {
const eventSink = yield* EventSink.EventSinkV2;
const idAllocator = yield* IdAllocator.IdAllocatorV2;
const manager = yield* ProviderSessionManager.ProviderSessionManagerV2;
const projectionStore = yield* ProjectionStore.ProjectionStoreV2;
const now = yield* DateTime.now;
const threadId = ThreadId.make("instruction-invalidation");
const providerSessionId = yield* idAllocator.allocate.providerSession({
providerInstanceId: modelSelection.instanceId,
threadId,
});
const providerThread = {
...makeProviderThread({ idAllocator, threadId, providerSessionId, now }),
lastRunOrdinal: 1,
nativeMetadata: {
title: "current title",
acpInstructions: {
nativeSessionId: "native-thread",
digest: "delivered-instructions",
},
},
};
const stored = {
...providerThread,
...(owner === "newer-run" ? { lastRunOrdinal: 2 } : {}),
...(owner === "newer-session"
? { providerSessionId: ProviderSessionId.make("newer-session") }
: {}),
};
yield* eventSink.write({
events: [
yield* makeThreadCreatedEvent({ idAllocator, threadId, now }),
{
id: yield* idAllocator.allocate.event({ threadId, providerSessionId }),
type: "provider-thread.updated",
threadId,
driver: CODEX_DRIVER,
occurredAt: now,
payload: stored,
},
],
});
const before = (yield* projectionStore.getThreadRecords(threadId, ["providerThreads"]))
.providerThreads[0]!;
const runtime = yield* manager.open({
threadId,
providerSessionId,
modelSelection,
runtimePolicy,
});
const invalidation = {
...providerThread,
nativeMetadata: { title: "stale title", acpInstructions: null },
};
if (owner === "release-before-event") {
// The adapter has recorded compaction, but the event pump has not seen it.
yield* Ref.set(adapterInvalidations, [invalidation]);
yield* manager.release({ providerSessionId, reason: "manual_shutdown" });
const projection = yield* projectionStore.getThreadRecords(threadId, ["providerThreads"]);
assert.isNull(projection.providerThreads[0]?.nativeMetadata?.acpInstructions);
return;
}
const subscription = yield* runtime.subscribeEvents!;
const queue = (yield* Ref.get(state)).eventQueues.get(String(providerSessionId));
assert.isDefined(queue);
const failedWrite = Effect.fail(new EventSink.EventSinkWriteError({ eventCount: 1 }));
if (
owner === "failed-event" ||
owner === "release-after-failed-event" ||
owner === "retry-after-failed-release" ||
owner === "detach-after-failed-event"
) {
yield* Ref.set(beforeUnchangedWrite, failedWrite);
}
yield* Queue.offer(queue!, {
type: "provider_thread.updated",
driver: CODEX_DRIVER,
providerThread: invalidation,
});
const completionEventId = yield* idAllocator.allocate.event({
threadId,
providerSessionId,
});
const restoreCompletion = eventSink.write({
events: [
{
id: completionEventId,
type: "provider-thread.updated",
threadId,
driver: CODEX_DRIVER,
occurredAt: now,
payload: before,
},
],
});
let conflicts = 0;
let release: Fiber.Fiber<void> | undefined;
const raceWithCleanup: Effect.Effect<void> = Effect.gen(function* () {
conflicts++;
if (conflicts === 1) {
release = yield* manager
.release({ providerSessionId, reason: "manual_shutdown" })
.pipe(Effect.orDie, Effect.forkChild({ startImmediately: true }));
} else {
assert.isFalse(
yield* manager.isLive(providerSessionId),
"release must enter between retries",
);
}
yield* eventSink.write({
events: [
{
id: yield* idAllocator.allocate.event({ threadId, providerSessionId }),
type: "provider-thread.updated",
threadId,
driver: CODEX_DRIVER,
occurredAt: now,
payload: {
...before,
nativeMetadata: { ...before.nativeMetadata, title: `conflict ${conflicts}` },
},
},
],
});
if (conflicts < 4) yield* Ref.set(beforeUnchangedWrite, raceWithCleanup);
}).pipe(Effect.orDie);
yield* subscription.events.pipe(
Stream.tap(() =>
Effect.gen(function* () {
if (owner === "failed-event") {
assert.isTrue(yield* manager.isLive(providerSessionId));
assert.deepEqual(
(yield* projectionStore.getThreadRecords(threadId, ["providerThreads"]))
.providerThreads[0]?.nativeMetadata?.acpInstructions,
before.nativeMetadata?.acpInstructions,
);
}
if (owner === "released-completion")
yield* manager.release({ providerSessionId, reason: "manual_shutdown" });
if (
owner === "release-after-failed-event" ||
owner === "retry-after-failed-release" ||
owner === "detach-after-failed-event"
) {
const release =
owner === "detach-after-failed-event"
? manager.detach({ providerSessionId, threadId })
: manager.release({ providerSessionId, reason: "manual_shutdown" });
if (owner === "retry-after-failed-release") {
yield* Ref.set(beforeUnchangedWrite, failedWrite);
assert.isTrue(Exit.isFailure(yield* Effect.exit(release)));
assert.isFalse(yield* manager.isLive(providerSessionId));
assert.equal((yield* Ref.get(state)).closeCount, 1);
const invalidation = yield* eventSink
.stream({
threadId,
afterSequence: yield* eventSink.latestSequence({ threadId }),
eventType: "provider-thread.updated",
})
.pipe(Stream.runHead, Effect.forkScoped);
yield* TestClock.adjust("1 second");
yield* Fiber.join(invalidation);
} else {
yield* release;
}
// The subscriber is still open, so it cannot have retried the write.
assert.isNull(
(yield* projectionStore.getThreadRecords(threadId, ["providerThreads"]))
.providerThreads[0]?.nativeMetadata?.acpInstructions,
);
}
if (owner === "queued-completion" || owner === "released-completion")
yield* restoreCompletion;
if (owner === "detached-close") {
yield* restoreCompletion;
yield* manager.detach({ providerSessionId, threadId });
}
if (owner === "failed-close") {
yield* restoreCompletion;
yield* Ref.set(
beforeUnchangedWrite,
Effect.fail(new EventSink.EventSinkWriteError({ eventCount: 1 })),
);
}
if (owner === "contended-close") {
yield* restoreCompletion;
yield* Ref.set(beforeUnchangedWrite, raceWithCleanup);
}
}),
),
Stream.runHead,
);
if (release !== undefined) yield* Fiber.join(release);
if (owner === "failed-event") {
assert.isTrue(yield* manager.isLive(providerSessionId));
assert.equal((yield* Ref.get(state)).closeCount, 0);
}
if (owner === "contended-close") assert.equal(conflicts, 4);
if (owner === "failed-close") yield* subscription.close;
if (owner === "repeated-close") {
yield* restoreCompletion;
yield* subscription.close;
}
const projection = yield* projectionStore.getThreadRecords(threadId, ["providerThreads"]);
const updated = projection.providerThreads.find(
(candidate) => candidate.id === providerThread.id,
);
assert.deepEqual(
updated,
owner === "current" ||
owner === "queued-completion" ||
owner === "released-completion" ||
owner === "repeated-close" ||
owner === "contended-close" ||
owner === "failed-close" ||
owner === "failed-event" ||
owner === "release-after-failed-event" ||
owner === "retry-after-failed-release" ||
owner === "detach-after-failed-event"
? {
...before,
nativeMetadata: {
...before.nativeMetadata,
...(owner === "contended-close" ? { title: "conflict 4" } : {}),
acpInstructions: null,
},
}
: before,
);
}).pipe(
Effect.provide(
layerTest({
state,
idleTimeoutMs: 1000,
beforeUnchangedWrite,
...(owner === "detach-after-failed-event"
? { capabilities: ExclusiveCapabilities }
: {}),
...(owner === "release-before-event"
? { readInstructionInvalidations: Ref.get(adapterInvalidations) }
: {}),
}),
),
);
}),
);

it.effect.each(["manual_shutdown", "server_shutdown"] as const)(
"ProviderSessionManagerV2 preserves questions only for server shutdown (%s)",
(reason) =>
Expand Down
Loading
Loading