Compare commits

...
Author SHA1 Message Date
paseo-bot[bot]andfdkevin 4c62a88a1c test(server): preserve startup notice diagnosis cases
Record the duplicate notice on reload, the resumed-session startup-row contract, and distinct newly supplied notifications that share text. These diagnostic tests intentionally expose unresolved behavior.

Co-authored-by: fdkevin <25277753+fdkevin0@users.noreply.github.com>
2026-11-07 12:44:59 +00:00
fdkevin 6394cc2d0c fix(server): check durable timeline prefix and preserve version-switch notices 2026-11-07 18:50:47 +08:00
fdkevin d765000f44 fix(server): avoid duplicating startup notices when timeline is already primed 2026-11-07 17:19:29 +08:00
3 changed files with 213 additions and 6 deletions
@@ -12650,8 +12650,10 @@ test("commits startup notices once on create and after restored history", async
},
};
const limitedToolOutput = limitAgentTimelineItemContent(toolOutput);
let activeNotice = notice;
let lastSession: NotifyingSession | null = null;
class NotifyingSession extends TestAgentSession {
readonly initialTimeline = [{ item: notice, timestamp: "2026-09-22T00:00:00.000Z" }];
readonly initialTimeline = [{ item: activeNotice, timestamp: "2026-09-22T00:00:00.000Z" }];
override async *streamHistory(): AsyncGenerator<AgentStreamEvent> {
yield {
type: "timeline",
@@ -12670,10 +12672,14 @@ test("commits startup notices once on create and after restored history", async
}
const client = new (class extends TestAgentClient {
override async createSession(config: AgentSessionConfig) {
return new NotifyingSession(config);
const session = new NotifyingSession(config);
lastSession = session;
return session;
}
override async resumeSession() {
return new NotifyingSession({ provider: "codex", cwd: workdir });
const session = new NotifyingSession({ provider: "codex", cwd: workdir });
lastSession = session;
return session;
}
})();
const store = new RecordingTimelineStore();
@@ -12687,6 +12693,55 @@ test("commits startup notices once on create and after restored history", async
const created = await manager.createAgent({ provider: "codex", cwd: workdir }, undefined, {});
ids.push(created.id);
expect((await manager.getTimelineRows(created.id)).map((row) => row.item)).toEqual([notice]);
await manager.reloadAgentSession(created.id);
expect((await manager.getTimelineRows(created.id)).map((row) => row.item)).toEqual([notice]);
await manager.closeAgent(created.id);
await manager.resumeAgentFromPersistence(
{
provider: "codex",
sessionId: "existing-created",
metadata: { cwd: workdir },
},
undefined,
created.id,
);
expect((await manager.getTimelineRows(created.id)).map((row) => row.item)).toEqual([notice]);
const followUp: AgentTimelineItem = {
type: "user_message",
text: "after cold restore",
messageId: "msg-after-cold-restore",
};
lastSession!.pushEvent({ type: "timeline", provider: "codex", item: followUp });
await manager.reloadAgentSession(created.id);
expect((await manager.getTimelineRows(created.id)).map((row) => row.item)).toEqual([
notice,
followUp,
]);
const v1Notice: AgentTimelineItem = {
type: "notification",
level: "info",
message: "This chat uses OpenCode v1.",
};
const v2Notice: AgentTimelineItem = {
type: "notification",
level: "info",
message: "This chat uses OpenCode v2.",
};
activeNotice = v1Notice;
await manager.reloadAgentSession(created.id);
activeNotice = v2Notice;
await manager.reloadAgentSession(created.id);
activeNotice = v1Notice;
await manager.reloadAgentSession(created.id);
await manager.reloadAgentSession(created.id);
expect((await manager.getTimelineRows(created.id)).map((row) => row.item)).toEqual([
notice,
followUp,
v1Notice,
v2Notice,
v1Notice,
]);
activeNotice = notice;
const resumed = await manager.resumeAgentFromPersistence({
provider: "codex",
sessionId: "existing",
@@ -12713,6 +12768,34 @@ test("commits startup notices once on create and after restored history", async
}
});
test("commits distinct new startup rows even when notification text matches", async () => {
const notice = { type: "notification", level: "warning", message: "Runtime warning" } as const;
const initialTimeline = [
{ item: notice, timestamp: "2026-11-07T00:00:00.000Z" },
{ item: notice, timestamp: "2026-11-07T00:01:00.000Z" },
];
const client = new (class extends TestAgentClient {
override async createSession(config: AgentSessionConfig) {
return new (class extends TestAgentSession {
readonly initialTimeline = initialTimeline;
})(config);
}
})();
const manager = new AgentManager({ clients: { [client.provider]: client }, logger });
const agent = await manager.createAgent(
{ provider: client.provider, cwd: tmpdir() },
undefined,
{},
);
try {
expect(
(await manager.getTimelineRows(agent.id)).map(({ item, timestamp }) => ({ item, timestamp })),
).toEqual(initialTimeline);
} finally {
await manager.closeAgent(agent.id);
}
});
test("failed startup history closes the session without registering an agent", async () => {
let closed = false;
class FailingHistorySession extends TestAgentSession {
@@ -4029,9 +4029,11 @@ export class AgentManager {
// Legacy/imported chats need their existing history before startup rows.
await this.primeTimelineFromLegacyProviderHistory(managed, false, startupHistory);
} else {
for (const entry of buildImportedTimelineRows(session.initialTimeline)) {
this.recordTimeline(managed.id, entry.item, { timestamp: entry.timestamp });
}
await this.recordInitialTimelineIfMissing({
agent: managed,
initialTimeline: session.initialTimeline,
durableTimelineHasRows,
});
}
this.refreshSessionPersistence(managed);
}
@@ -4224,6 +4226,37 @@ export class AgentManager {
};
}
private async recordInitialTimelineIfMissing(options: {
agent: ActiveManagedAgent;
initialTimeline: NonNullable<AgentSession["initialTimeline"]>;
durableTimelineHasRows: boolean;
}): Promise<void> {
const { agent, initialTimeline, durableTimelineHasRows } = options;
const inMemoryRows = this.timelineStore.getRows(agent.id);
const firstInMemorySeq = inMemoryRows[0]?.seq ?? Number.POSITIVE_INFINITY;
const existingItems: AgentTimelineItem[] = [];
if (durableTimelineHasRows && this.durableTimelineStore && firstInMemorySeq > 1) {
for (const row of await this.durableTimelineStore.getCommittedRows(agent.id)) {
if (row.seq < firstInMemorySeq) {
existingItems.push(row.item);
}
}
}
for (const row of inMemoryRows) {
existingItems.push(row.item);
}
for (const entry of buildImportedTimelineRows(initialTimeline)) {
if (
entry.item.type === "notification" &&
isDuplicateInitialTimelineNotification({ existingItems, incoming: entry.item })
) {
continue;
}
this.recordTimeline(agent.id, entry.item, { timestamp: entry.timestamp });
existingItems.push(entry.item);
}
}
private prepareAgentForClosure(
agent: LiveManagedAgent,
cancelReason: string,
@@ -5966,6 +5999,31 @@ export class AgentManager {
}
}
function isDuplicateInitialTimelineNotification(input: {
existingItems: readonly AgentTimelineItem[];
incoming: Extract<AgentTimelineItem, { type: "notification" }>;
}): boolean {
const { existingItems, incoming } = input;
const isOpenCodeRuntimeVersionNotice = /^This chat uses OpenCode v\d+\.$/.test(incoming.message);
if (isOpenCodeRuntimeVersionNotice) {
const lastVersionNotice = existingItems.findLast(
(item): item is Extract<AgentTimelineItem, { type: "notification" }> =>
item.type === "notification" && /^This chat uses OpenCode v\d+\.$/.test(item.message),
);
return (
lastVersionNotice !== undefined &&
lastVersionNotice.level === incoming.level &&
lastVersionNotice.message === incoming.message
);
}
return existingItems.some(
(item) =>
item.type === "notification" &&
item.level === incoming.level &&
item.message === incoming.message,
);
}
function matchesImportableSessionQuery(
session: ImportableProviderSession,
rawQuery: string | undefined,
@@ -1724,6 +1724,72 @@ test("commits notices emitted during session open as the initial timeline with s
);
});
test("reopening a plugin agent does not append the same startup notice", async () => {
await withObservedProvider(
{
openChildren(sessionId, emit) {
emit({
type: "session.notice",
sessionId,
notice: {
id: "full-access",
severity: "warning",
title: "Full access",
},
});
},
},
async (client) => {
const manager = new AgentManager({
clients: { [client.provider]: client },
logger: createTestLogger(),
});
const agent = await manager.createAgent(
{ provider: client.provider, cwd: tmpdir() },
undefined,
{},
);
try {
for (let attempt = 0; attempt < 4; attempt++) await manager.reloadAgentSession(agent.id);
expect(
(await manager.getTimelineRows(agent.id)).filter(
(row) => row.item.type === "notification",
),
).toHaveLength(1);
} finally {
await manager.closeAgent(agent.id);
}
},
);
});
test("a resumed plugin session offers only new startup rows to the manager", async () => {
await withObservedProvider(
{
openChildren(sessionId, emit) {
emit({
type: "session.notice",
sessionId,
notice: { id: "full-access", severity: "warning", title: "Full access" },
});
},
},
async (client) => {
const config = { provider: client.provider, cwd: tmpdir() };
const original = await client.createSession(config);
const handle = original.describePersistence()!;
expect(original.initialTimeline).toHaveLength(1);
await original.close();
const resumed = await client.resumeSession(handle, config);
try {
expect(resumed.initialTimeline).toEqual([]);
} finally {
await resumed.close();
}
},
);
});
describe("pending provider responses", () => {
test.each<RequestKind>(["session.open", "catalog", "session.configure", "session.prompt"])(
"preserves the send error for %s",