mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-23 17:52:49 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4943b09764 | ||
|
|
5599f9e567 | ||
|
|
4e9a4582b7 | ||
|
|
3fea128382 | ||
|
|
53ee9cd993 |
@@ -1,5 +1,15 @@
|
||||
# Changelog
|
||||
|
||||
## 0.10.2 - 2026-09-30
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed OpenCode v2 turns longer than five minutes failing with `UND_ERR_HEADERS_TIMEOUT` ([#5674](https://github.com/getpaseo/paseo/pull/5674))
|
||||
- Fixed the context meter staying empty during OpenCode v2 turns and disappearing after them ([#5710](https://github.com/getpaseo/paseo/pull/5710) by [@mcowger](https://github.com/mcowger))
|
||||
- Fixed OpenCode v2 question cards showing only the header and offering no typed answer ([#5679](https://github.com/getpaseo/paseo/pull/5679))
|
||||
- Fixed OpenCode v2 patch edits from GPT models showing as a raw Patch card instead of an edit diff ([#5696](https://github.com/getpaseo/paseo/pull/5696))
|
||||
- Fixed completed OpenCode v2 edits showing no diff ([#5609](https://github.com/getpaseo/paseo/pull/5609))
|
||||
|
||||
## 0.10.1 - 2026-09-29
|
||||
|
||||
### Added
|
||||
|
||||
@@ -4017,6 +4017,38 @@ describe("HostRuntimeStore initial connection hint bootstrap", () => {
|
||||
expect(store.getSnapshot("srv_deep")?.connectionStatus).toBe("error");
|
||||
store.syncHosts([]);
|
||||
});
|
||||
it("stops connecting to a host removed while its first probe is pending", async () => {
|
||||
useHostRuntimeClock();
|
||||
const firstProbe = createDeferred<void>();
|
||||
let connectCalls = 0;
|
||||
const store = new HostRuntimeStore({
|
||||
storage: createMemoryHostRuntimeStorage(),
|
||||
deps: {
|
||||
createClient: () => new FakeDaemonClient() as unknown as DaemonClient,
|
||||
connectToDaemon: async ({ host }) => {
|
||||
connectCalls += 1;
|
||||
if (connectCalls === 1) await firstProbe.promise;
|
||||
return {
|
||||
client: makeConnectedProbeClient(5) as unknown as DaemonClient,
|
||||
serverId: host.serverId,
|
||||
hostname: host.label ?? null,
|
||||
};
|
||||
},
|
||||
getClientId: async () => "cid_test_removed_host",
|
||||
},
|
||||
});
|
||||
await store.upsertDirectConnection({ serverId: "srv_removed", endpoint: "lan:6767" });
|
||||
await vi.waitFor(() => expect(connectCalls).toBe(1));
|
||||
|
||||
await store.removeHost("srv_removed");
|
||||
firstProbe.resolve();
|
||||
await vi.advanceTimersByTimeAsync(60_000);
|
||||
|
||||
expect(connectCalls).toBe(1);
|
||||
expect(store.getSnapshot("srv_removed")).toBeNull();
|
||||
expect(useSessionStore.getState().sessions["srv_removed"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("saves and reconnects a rejected saved host after changing its password", async () => {
|
||||
const store = new HostRuntimeStore({
|
||||
storage: createMemoryHostRuntimeStorage(),
|
||||
|
||||
@@ -677,8 +677,15 @@ export class HostRuntimeController {
|
||||
connectionId: options.initialConnection.connectionId,
|
||||
existingClient: options.initialConnection.existingClient,
|
||||
});
|
||||
if (!this.started) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
await this.runProbeCycleNow();
|
||||
// stop() can run while startup is pending; a stopped controller never probes again.
|
||||
if (!this.started) {
|
||||
return;
|
||||
}
|
||||
if (options?.autoProbe !== false) {
|
||||
this.probeIntervalHandle = setInterval(() => {
|
||||
void this.runProbeCycleNow();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Command } from "commander";
|
||||
import { connectToDaemon } from "../../utils/client.js";
|
||||
import type { AgentDeepLinkTarget } from "@getpaseo/protocol/agent-deep-link";
|
||||
import { openDesktopWithAgent } from "../open.js";
|
||||
import type {
|
||||
CommandError,
|
||||
@@ -30,10 +31,21 @@ export function addOpenOptions(command: Command): Command {
|
||||
.option("--server <server-id>", "Server ID (defaults to the local daemon)");
|
||||
}
|
||||
|
||||
async function resolveServerId(options: CommandOptions): Promise<string> {
|
||||
function toOpenError(err: unknown): CommandError {
|
||||
if (err && typeof err === "object" && "code" in err) {
|
||||
return err as CommandError;
|
||||
}
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return { code: "OPEN_FAILED", message: `Failed to open agent: ${message}` };
|
||||
}
|
||||
|
||||
async function resolveAgentTarget(
|
||||
agentId: string,
|
||||
options: CommandOptions,
|
||||
): Promise<AgentDeepLinkTarget> {
|
||||
const explicitServerId = typeof options.server === "string" ? options.server.trim() : "";
|
||||
if (explicitServerId) {
|
||||
return explicitServerId;
|
||||
return { serverId: explicitServerId, agentId };
|
||||
}
|
||||
|
||||
const client = await connectToDaemon({ target: options.daemonTarget });
|
||||
@@ -46,7 +58,19 @@ async function resolveServerId(options: CommandOptions): Promise<string> {
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
return serverId;
|
||||
|
||||
const fetchResult = await client.fetchAgent({ agentId });
|
||||
if (!fetchResult) {
|
||||
const error: CommandError = {
|
||||
code: "AGENT_NOT_FOUND",
|
||||
message: `Agent not found: ${agentId}`,
|
||||
details: 'Use "paseo ls" to list available agents',
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
return { serverId, agentId: fetchResult.agent.id };
|
||||
} catch (err) {
|
||||
throw toOpenError(err);
|
||||
} finally {
|
||||
await client.close().catch(() => {});
|
||||
}
|
||||
@@ -66,12 +90,12 @@ export async function runOpenCommand(
|
||||
throw error;
|
||||
}
|
||||
|
||||
const serverId = await resolveServerId(options);
|
||||
await openDesktopWithAgent({ serverId, agentId });
|
||||
const target = await resolveAgentTarget(agentId, options);
|
||||
await openDesktopWithAgent(target);
|
||||
|
||||
return {
|
||||
type: "single",
|
||||
data: { agentId, serverId, status: "opened" },
|
||||
data: { ...target, status: "opened" },
|
||||
schema: openAgentSchema,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
import assert from "node:assert";
|
||||
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { runPaseoCli, startTestDaemon } from "./helpers/test-daemon.ts";
|
||||
|
||||
console.log("=== Agent Open Command Tests ===\n");
|
||||
|
||||
if (process.platform !== "linux") {
|
||||
console.log("Skipped: the fake Desktop app is a Linux AppImage path.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const daemon = await startTestDaemon();
|
||||
|
||||
// runPaseoCli sets HOME to the daemon's home, where the CLI looks for
|
||||
// ~/Applications/Paseo.AppImage on Linux. The fake app records its launch.
|
||||
const launchRecord = join(daemon.paseoHome, "desktop-launches.txt");
|
||||
const fakeDesktop = join(daemon.paseoHome, "Applications", "Paseo.AppImage");
|
||||
await mkdir(join(daemon.paseoHome, "Applications"), { recursive: true });
|
||||
await writeFile(fakeDesktop, `#!/bin/sh\necho "$@" >> "${launchRecord}"\n`);
|
||||
await chmod(fakeDesktop, 0o755);
|
||||
|
||||
try {
|
||||
{
|
||||
console.log("Test 1: an unknown agent ID fails without opening Desktop");
|
||||
const result = await runPaseoCli(daemon, [
|
||||
"agent",
|
||||
"open",
|
||||
"does-not-exist",
|
||||
"--host",
|
||||
`127.0.0.1:${daemon.port}`,
|
||||
"--json",
|
||||
]);
|
||||
|
||||
assert.notStrictEqual(result.exitCode, 0, "open should fail for an unknown agent");
|
||||
assert.match(JSON.parse(result.stderr).error.message, /Agent not found: does-not-exist/);
|
||||
// Give a wrongly spawned detached app time to write its record.
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
assert.ok(
|
||||
!existsSync(launchRecord),
|
||||
`Desktop should not open, but was launched with: ${existsSync(launchRecord) ? await readFile(launchRecord, "utf8") : ""}`,
|
||||
);
|
||||
console.log("✓ unknown agent ID fails without opening Desktop\n");
|
||||
}
|
||||
} finally {
|
||||
await daemon.stop();
|
||||
}
|
||||
|
||||
console.log("=== All agent open tests passed ===");
|
||||
@@ -1,9 +1,7 @@
|
||||
import type { SpawnOptions } from "node:child_process";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { listAvailableEditorTargets, openEditorTarget } from "./registry.js";
|
||||
import { createEditorTargetRuntime } from "./runtime.js";
|
||||
import { zedTarget } from "./targets/zed.js";
|
||||
|
||||
interface SpawnRecord {
|
||||
command: string;
|
||||
@@ -12,23 +10,6 @@ interface SpawnRecord {
|
||||
unrefed: boolean;
|
||||
}
|
||||
|
||||
function recordSpawns(records: SpawnRecord[]) {
|
||||
return (command: string, args: string[], options: SpawnOptions) => {
|
||||
const record = { command, args, options, unrefed: false };
|
||||
records.push(record);
|
||||
const child = {
|
||||
once(event: "error" | "spawn", handler: (error?: Error) => void) {
|
||||
if (event === "spawn") queueMicrotask(() => handler());
|
||||
return child;
|
||||
},
|
||||
unref() {
|
||||
record.unrefed = true;
|
||||
},
|
||||
};
|
||||
return child;
|
||||
};
|
||||
}
|
||||
|
||||
describe("editor target runtime", () => {
|
||||
it("resolves command aliases and safely launches Windows command scripts", async () => {
|
||||
const records: SpawnRecord[] = [];
|
||||
@@ -39,7 +20,20 @@ describe("editor target runtime", () => {
|
||||
ELECTRON_RUN_AS_NODE: "1",
|
||||
},
|
||||
pathExists: (targetPath) => targetPath === "C:/Program Files/Editors & Tools/bin/code.cmd",
|
||||
spawn: recordSpawns(records),
|
||||
spawn: (command, args, options) => {
|
||||
const record = { command, args, options, unrefed: false };
|
||||
records.push(record);
|
||||
const child = {
|
||||
once(event: "error" | "spawn", handler: (error?: Error) => void) {
|
||||
if (event === "spawn") queueMicrotask(() => handler());
|
||||
return child;
|
||||
},
|
||||
unref() {
|
||||
record.unrefed = true;
|
||||
},
|
||||
};
|
||||
return child;
|
||||
},
|
||||
});
|
||||
|
||||
const command = runtime.resolveCommand(["missing", "code"]);
|
||||
@@ -63,32 +57,4 @@ describe("editor target runtime", () => {
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["system", "/var/lib/flatpak/exports/bin/dev.zed.Zed"],
|
||||
["user", "/home/dev/.local/share/flatpak/exports/bin/dev.zed.Zed"],
|
||||
])("finds and opens Zed from a %s Flatpak install on Linux", async (_scope, flatpakExport) => {
|
||||
const records: SpawnRecord[] = [];
|
||||
const existingPaths = new Set([flatpakExport, "/repo", "/repo/src/app.ts"]);
|
||||
const runtime = createEditorTargetRuntime({
|
||||
platform: "linux",
|
||||
env: { PATH: "/usr/local/bin:/usr/bin:/bin" },
|
||||
homeDirectory: "/home/dev",
|
||||
pathExists: (targetPath) => existingPaths.has(targetPath),
|
||||
spawn: recordSpawns(records),
|
||||
loadIcon: async (fileName) => ({ kind: "image", dataUrl: fileName }),
|
||||
});
|
||||
|
||||
const targets = await listAvailableEditorTargets(runtime, [zedTarget]);
|
||||
expect(targets.map((target) => target.id)).toEqual(["zed"]);
|
||||
|
||||
await openEditorTarget(
|
||||
{ editorId: "zed", workspacePath: "/repo", filePath: "/repo/src/app.ts", line: 3 },
|
||||
runtime,
|
||||
[zedTarget],
|
||||
);
|
||||
expect(records.map(({ command, args }) => ({ command, args }))).toEqual([
|
||||
{ command: flatpakExport, args: ["/repo", "/repo/src/app.ts:3"] },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -56,28 +56,10 @@ function isAbsolutePath(value: string, platform: NodeJS.Platform): boolean {
|
||||
return platform === "win32" ? win32.isAbsolute(value) : posix.isAbsolute(value);
|
||||
}
|
||||
|
||||
// Flatpak exports app launchers here, but does not put these directories on PATH.
|
||||
function flatpakExportDirectories(env: NodeJS.ProcessEnv, homeDirectory: string): string[] {
|
||||
const userDataDirectory = env.XDG_DATA_HOME || `${homeDirectory}/.local/share`;
|
||||
return [`${userDataDirectory}/flatpak/exports/bin`, "/var/lib/flatpak/exports/bin"];
|
||||
}
|
||||
|
||||
function commandSearchDirectories(input: {
|
||||
env: NodeJS.ProcessEnv;
|
||||
platform: NodeJS.Platform;
|
||||
homeDirectory: string;
|
||||
}): string[] {
|
||||
const pathValue = input.env.PATH ?? input.env.Path ?? input.env.path ?? "";
|
||||
const pathDelimiter = input.platform === "win32" ? ";" : ":";
|
||||
const directories = pathValue.split(pathDelimiter).filter(Boolean);
|
||||
if (input.platform !== "linux") return directories;
|
||||
return [...directories, ...flatpakExportDirectories(input.env, input.homeDirectory)];
|
||||
}
|
||||
|
||||
function resolveExecutable(
|
||||
commands: readonly string[],
|
||||
input: {
|
||||
searchDirectories: readonly string[];
|
||||
env: NodeJS.ProcessEnv;
|
||||
pathExists: (path: string) => boolean;
|
||||
platform: NodeJS.Platform;
|
||||
},
|
||||
@@ -87,7 +69,10 @@ function resolveExecutable(
|
||||
return command;
|
||||
}
|
||||
|
||||
for (const directory of input.searchDirectories) {
|
||||
const pathValue = input.env.PATH ?? input.env.Path ?? input.env.path ?? "";
|
||||
const pathDelimiter = input.platform === "win32" ? ";" : ":";
|
||||
for (const directory of pathValue.split(pathDelimiter)) {
|
||||
if (!directory) continue;
|
||||
const candidate = `${directory}/${command}`;
|
||||
if (input.platform !== "win32") {
|
||||
if (input.pathExists(candidate)) return candidate;
|
||||
@@ -155,12 +140,7 @@ export function createEditorTargetRuntime(
|
||||
env,
|
||||
pathExists,
|
||||
isAbsolutePath: (targetPath) => isAbsolutePath(targetPath, platform),
|
||||
resolveCommand: (commands) =>
|
||||
resolveExecutable(commands, {
|
||||
searchDirectories: commandSearchDirectories({ env, platform, homeDirectory }),
|
||||
pathExists,
|
||||
platform,
|
||||
}),
|
||||
resolveCommand: (commands) => resolveExecutable(commands, { env, pathExists, platform }),
|
||||
async spawnDetached({ command, args }) {
|
||||
const commandScript = isWindowsCommandScript(command, platform);
|
||||
const launchCommand = commandScript ? escapeWindowsCmdValue(command) : command;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { EditorTarget, EditorTargetLaunchInput } from "../target.js";
|
||||
|
||||
const COMMANDS = ["zed", "zeditor", "dev.zed.Zed"] as const;
|
||||
const COMMANDS = ["zed", "zeditor"] as const;
|
||||
|
||||
function location(input: EditorTargetLaunchInput): string {
|
||||
if (!input.line) return input.filePath!;
|
||||
|
||||
@@ -381,7 +381,7 @@ const AgentCapabilityFlagsSchema: z.ZodType<AgentCapabilityFlags> = z
|
||||
})
|
||||
.catchall(z.boolean());
|
||||
|
||||
const AgentUsageSchema: z.ZodType<AgentUsage> = z.object({
|
||||
export const AgentUsageSchema: z.ZodType<AgentUsage> = z.object({
|
||||
inputTokens: z.number().optional(),
|
||||
cachedInputTokens: z.number().optional(),
|
||||
outputTokens: z.number().optional(),
|
||||
|
||||
@@ -110,7 +110,11 @@ export async function ensureAgentLoaded(
|
||||
handle,
|
||||
buildConfigOverrides(record),
|
||||
agentId,
|
||||
{ ...extractTimestamps(record), attention: extractAttention(record) },
|
||||
{
|
||||
...extractTimestamps(record),
|
||||
attention: extractAttention(record),
|
||||
lastUsage: record.lastUsage,
|
||||
},
|
||||
record.archivedAt ? { purpose: "history" } : undefined,
|
||||
);
|
||||
deps.logger.info({ agentId, provider: record.provider }, "Agent resumed from persistence");
|
||||
|
||||
@@ -1310,6 +1310,7 @@ export class AgentManager {
|
||||
workspaceId?: string;
|
||||
owner?: AgentOwner;
|
||||
attention?: AttentionState;
|
||||
lastUsage?: AgentUsage;
|
||||
},
|
||||
resumeOptions?: AgentResumeSessionOptions,
|
||||
): Promise<ManagedAgent> {
|
||||
@@ -1342,6 +1343,7 @@ export class AgentManager {
|
||||
workspaceId?: string;
|
||||
owner?: AgentOwner;
|
||||
attention?: AttentionState;
|
||||
lastUsage?: AgentUsage;
|
||||
},
|
||||
resumeOptions?: AgentResumeSessionOptions,
|
||||
): Promise<ManagedAgent> {
|
||||
@@ -1904,7 +1906,7 @@ export class AgentManager {
|
||||
persistence: record.persistence ?? null,
|
||||
historyPrimed: true,
|
||||
lastUserMessageAt: record.lastUserMessageAt ? new Date(record.lastUserMessageAt) : null,
|
||||
lastUsage: undefined,
|
||||
lastUsage: record.lastUsage,
|
||||
lastError: record.lastError ?? undefined,
|
||||
attention,
|
||||
internal: record.internal,
|
||||
|
||||
@@ -90,6 +90,7 @@ export function toStoredAgentRecord(
|
||||
runtimeInfo,
|
||||
features: normalizeFeatures(agent.features),
|
||||
persistence,
|
||||
lastUsage: sanitizeUsage(agent.lastUsage),
|
||||
lastError: agent.lastError ?? undefined,
|
||||
requiresAttention: agent.attention.requiresAttention,
|
||||
attentionReason: agent.attention.requiresAttention ? agent.attention.attentionReason : null,
|
||||
@@ -243,6 +244,7 @@ export function buildStoredAgentPayload(
|
||||
pendingPermissions: [],
|
||||
persistence,
|
||||
title: record.title ?? null,
|
||||
...(record.lastUsage ? { lastUsage: record.lastUsage } : {}),
|
||||
requiresAttention: record.requiresAttention ?? false,
|
||||
attentionReason: record.attentionReason ?? null,
|
||||
attentionTimestamp: record.attentionTimestamp ?? null,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { z } from "zod";
|
||||
import type { Logger } from "pino";
|
||||
|
||||
import { writeJsonFileAtomic } from "../atomic-file.js";
|
||||
import { AgentFeatureSchema, AgentStatusSchema } from "../messages.js";
|
||||
import { AgentFeatureSchema, AgentStatusSchema, AgentUsageSchema } from "../messages.js";
|
||||
import { toStoredAgentRecord } from "./agent-projections.js";
|
||||
import type { ManagedAgent } from "./agent-manager.js";
|
||||
import type { AgentSessionConfig } from "./agent-sdk-types.js";
|
||||
@@ -68,6 +68,7 @@ const STORED_AGENT_SCHEMA = z.object({
|
||||
.optional(),
|
||||
features: z.array(AgentFeatureSchema).optional(),
|
||||
persistence: PERSISTENCE_HANDLE_SCHEMA,
|
||||
lastUsage: AgentUsageSchema.optional(),
|
||||
lastError: z.string().nullable().optional(),
|
||||
requiresAttention: z.boolean().optional(),
|
||||
attentionReason: z.enum(["finished", "error", "permission"]).nullable().optional(),
|
||||
|
||||
@@ -656,6 +656,29 @@ describe("codex tool-call mapper", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
type: "commandExecution",
|
||||
id: "codex-declined-command",
|
||||
status: "declined",
|
||||
command: "printf ok > /outside/permission.txt",
|
||||
cwd: "/tmp/repo",
|
||||
aggregatedOutput: null,
|
||||
exitCode: null,
|
||||
},
|
||||
{
|
||||
type: "fileChange",
|
||||
id: "codex-declined-patch",
|
||||
status: "declined",
|
||||
changes: [],
|
||||
},
|
||||
])("settles a declined $type as failed instead of running", (threadItem) => {
|
||||
const item = expectMapped(mapCodexToolCallFromThreadItem(threadItem));
|
||||
|
||||
expect(item.callId).toBe(threadItem.id);
|
||||
expect(item.status).toBe("failed");
|
||||
});
|
||||
|
||||
it("maps unknown tools to unknown detail with raw payloads", () => {
|
||||
const item = expectMapped(
|
||||
mapCodexToolCallEnvelope({
|
||||
|
||||
@@ -7,6 +7,7 @@ const FAILED_STATUS_VOCAB = new Set([
|
||||
"errored",
|
||||
"rejected",
|
||||
"denied",
|
||||
"declined",
|
||||
]);
|
||||
const CANCELED_STATUS_VOCAB = new Set(["canceled", "cancelled", "interrupted", "aborted"]);
|
||||
const COMPLETED_STATUS_VOCAB = new Set(["completed", "complete", "done", "success", "succeeded"]);
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { createDaemonTestContext, type DaemonTestContext } from "../test-utils/index.js";
|
||||
|
||||
describe("daemon E2E - usage after restart", () => {
|
||||
let ctx: DaemonTestContext | null = null;
|
||||
const dirs: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await ctx?.cleanup();
|
||||
ctx = null;
|
||||
for (const dir of dirs.splice(0)) {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
}, 60_000);
|
||||
|
||||
test("a finished agent reports its last usage after the daemon restarts", async () => {
|
||||
const cwd = mkdtempSync(path.join(tmpdir(), "usage-restart-cwd-"));
|
||||
const paseoHomeRoot = mkdtempSync(path.join(tmpdir(), "usage-restart-home-"));
|
||||
dirs.push(cwd, paseoHomeRoot);
|
||||
|
||||
ctx = await createDaemonTestContext({ paseoHomeRoot, cleanup: false });
|
||||
const agent = await ctx.client.createAgent({
|
||||
provider: "codex",
|
||||
cwd,
|
||||
title: "Usage restart agent",
|
||||
modeId: "full-access",
|
||||
});
|
||||
await ctx.client.sendMessage(agent.id, "Say 'done' and nothing else");
|
||||
const finished = await ctx.client.waitForFinish(agent.id, 5_000);
|
||||
expect(finished.status).toBe("idle");
|
||||
const usageBeforeRestart = finished.final?.lastUsage;
|
||||
expect(usageBeforeRestart).toBeDefined();
|
||||
|
||||
await ctx.cleanup();
|
||||
ctx = await createDaemonTestContext({ paseoHomeRoot, cleanup: false });
|
||||
|
||||
const stored = await ctx.client.fetchAgent(agent.id);
|
||||
expect(stored?.agent.lastUsage).toEqual(usageBeforeRestart);
|
||||
|
||||
await ctx.client.fetchAgentTimeline(agent.id, { direction: "tail", limit: 0 });
|
||||
const reopened = await ctx.client.fetchAgent(agent.id);
|
||||
expect(reopened?.agent.status).toBe("idle");
|
||||
expect(reopened?.agent.lastUsage).toEqual(usageBeforeRestart);
|
||||
}, 30_000);
|
||||
});
|
||||
Reference in New Issue
Block a user