mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-08 17:34:51 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c62a88a1c | ||
|
|
6394cc2d0c | ||
|
|
d765000f44 |
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user