fix(coding-agent): fixed parked subagent restoration from persisted sessions

- Fixed cold revival flow so parked subagents are restored from persisted sessions at startup.
- Fixed session-init persistence to include spawns and readSummarize fields for replay accuracy.
- Fixed latest-session lookup by adding peekSessionInit for lock-free persisted contract access.
- Added lifecycle and session tests for cold-revive success, decline, and retry paths.
This commit is contained in:
can1357
2026-06-17 00:21:20 +02:00
parent d6e390c1ca
commit ef5e5fd27c
9 changed files with 438 additions and 9 deletions
+1
View File
@@ -33,6 +33,7 @@
- Fixed `omp --continue`/`-c` sometimes resuming into a subagent transcript instead of the interactive session. Subagent (and HTML-export) `SessionManager.open()` calls run in the parent's terminal and were clobbering the per-TTY `--continue` breadcrumb with their own artifact-dir session file; these headless opens now suppress the breadcrumb. `continueRecent()` also recovers already-poisoned breadcrumbs by resolving any session file inside a parent's artifacts dir (`<parent>/<agentId>.jsonl`) back up to the top-level session.
- Fixed the Agent Hub stacking duplicate `Agent Hub · N running` frames and stranding garbage rows in scrollback while navigating with subagents still streaming. The hub was a non-fullscreen overlay composited over a live transcript, so each time a running subagent's progress grew the frame and scrolled the window the previously-painted hub copy was pushed permanently into the terminal's native scrollback (which the engine can't rewrite). It now renders inline in the editor slot — the same anchored region every other selector and the `ask` tool use — riding the normal append-only commit path, so the transcript commits above it exactly once and the hub repaints in place instead of leaking copies. (Avoids borrowing the alternate screen.)
- Fixed every subagent registering itself as its own parent in the agent registry (`parentId === id`), so the Agent Hub rendered each agent as `sub · of <itself>` and the ←← parent-navigation gesture looped on the same agent. The SDK was reusing `parentTaskPrefix` — the agent's own artifact/output-id prefix — as the registry parent link; spawns now pass a separate `parentAgentId` (the spawning agent's id: `Main` for top-level `task` spawns, the parent subagent for nested spawns and eval `agent()`, the focused agent for `/tan`) and the registry records that as the parent.
- Fixed messaging a `parked` subagent that was restored from disk (Agent Hub scan, or a resumed/restarted session) failing with `cannot be revived (no reviver registered)` even though its transcript was intact. Such refs carry a session file but no in-memory reviver — the executor's live reviver closure dies with the spawning turn/process — so IRC sends and Agent Hub focus refused them. `AgentLifecycleManager.ensureLive` now cold-revives them through a persisted-subagent reviver factory (installed by the top-level interactive/RPC session) that rebuilds the subagent from its JSONL the way `--resume` rebuilds a session: it reopens the file and replays it through `createAgentSession`, but sources the runtime contract from a now-readable `session_init` record (`SessionManager.peekSessionInit`) so tools, system prompt, output schema, and kind are restored rather than resurrected as a default top-level session. `session_init` now also persists the effective `spawns` allowlist and read-summarization flag so a cold revive keeps the original capability surface (old files without them deny re-spawning rather than defaulting to wildcard). Isolated runs and pre-`session_init` files whose recorded workspace no longer exists stay transcript-only (`history://`).
## [16.0.2] - 2026-06-16
+20
View File
@@ -55,6 +55,7 @@ import type { PrintModeOptions } from "./modes/print-mode";
import { CURRENT_SETUP_VERSION } from "./modes/setup-version";
import { initTheme, stopThemeWatcher } from "./modes/theme/theme";
import type { SubmittedUserInput } from "./modes/types";
import { AgentLifecycleManager } from "./registry/agent-lifecycle";
import {
type CreateAgentSessionOptions,
type CreateAgentSessionResult,
@@ -68,6 +69,7 @@ import { resolveResumableSession, type SessionInfo } from "./session/session-lis
import { SessionManager } from "./session/session-manager";
import { executeBuiltinSlashCommand } from "./slash-commands/builtin-registry";
import { discoverTitleSystemPromptFile, resolvePromptInput } from "./system-prompt";
import { createPersistedSubagentReviverFactory } from "./task/persisted-revive";
import { initTelemetryExport, isTelemetryExportEnabled } from "./telemetry-export";
import { AUTO_THINKING } from "./thinking";
import type { LspStartupServerInfo } from "./tools";
@@ -1262,6 +1264,24 @@ export async function runRootCommand(
eventBus,
preloadedExtensions: extensionsResult,
});
// Cold-revive support: a `parked` subagent ref restored from disk (Agent Hub
// scan, collab mirror, resumed process) has a sessionFile but no in-memory
// reviver, so `ensureLive` (IRC sends, hub focus) would refuse it. Install a
// factory — bound to THIS top-level session — that rebuilds the subagent from
// its persisted JSONL (see persisted-revive.ts). Scoped to the non-ACP
// bootstrap: ACP keeps several concurrent top-level sessions and a single
// process-global factory must not be clobbered by the most recent one.
AgentLifecycleManager.global().setPersistedSubagentReviverFactory(
createPersistedSubagentReviverFactory({
session,
authStorage,
modelRegistry,
settings: settingsInstance,
enableLsp: sessionOptions.enableLsp ?? true,
}),
Math.trunc(Number(settingsInstance.get("task.agentIdleTtlMs") ?? 420_000) || 0),
);
if (parsedArgs.apiKey && !sessionOptions.model && session.model) {
authStorage.setRuntimeApiKey(session.model.provider, parsedArgs.apiKey);
}
@@ -12,10 +12,19 @@
import { logger } from "@oh-my-pi/pi-utils";
import type { AgentSession } from "../session/agent-session";
import { AgentRegistry, MAIN_AGENT_ID, type RegistryEvent } from "./agent-registry";
import { type AgentRef, AgentRegistry, MAIN_AGENT_ID, type RegistryEvent } from "./agent-registry";
export type AgentReviver = () => Promise<AgentSession>;
/**
* Builds a reviver for a `parked` ref restored from disk (Agent Hub scan,
* collab mirror, resumed process) that carries a sessionFile but no in-memory
* adoption. Returns undefined when the ref cannot be faithfully rebuilt (no
* persisted session contract, or its workspace is gone). Injected from the
* top-level session so this manager stays free of sdk/SessionManager imports.
*/
export type PersistedSubagentReviverFactory = (ref: AgentRef) => Promise<AgentReviver | undefined>;
export interface AdoptOptions {
/** TTL before an idle agent is parked. <= 0 disables parking. */
idleTtlMs: number;
@@ -51,6 +60,7 @@ export class AgentLifecycleManager {
current.#adopted.clear();
current.#revivals.clear();
current.#parking.clear();
current.#persistedReviverFactory = undefined;
}
AgentLifecycleManager.#global = undefined;
}
@@ -62,12 +72,26 @@ export class AgentLifecycleManager {
/** In-flight revives, so concurrent {@link ensureLive} calls coalesce. */
readonly #revivals = new Map<string, Promise<AgentSession>>();
#unsubscribe: (() => void) | undefined;
#persistedReviverFactory: PersistedSubagentReviverFactory | undefined;
/** TTL applied when a cold-revived ref is adopted on demand. */
#persistedReviveTtlMs = 0;
constructor(registry: AgentRegistry = AgentRegistry.global()) {
this.#registry = registry;
this.#unsubscribe = registry.onChange(event => this.#onRegistryEvent(event));
}
/**
* Install the factory used to cold-revive `parked` refs restored from disk
* (Agent Hub scan, collab mirror, resumed process) — they carry a sessionFile
* but no adoption. Set by the top-level session, which owns the ambient deps
* (auth, models, MCP, artifacts) the factory needs at revive time.
*/
setPersistedSubagentReviverFactory(factory: PersistedSubagentReviverFactory, idleTtlMs: number): void {
this.#persistedReviverFactory = factory;
this.#persistedReviveTtlMs = idleTtlMs;
}
/**
* Take ownership of a finished subagent. Caller has already set registry
* status to "idle". Arms the TTL timer (idleTtlMs <= 0 adopts without one).
@@ -137,13 +161,7 @@ export class AgentLifecycleManager {
if (ref.session) return ref.session;
const inflight = this.#revivals.get(id);
if (inflight) return inflight;
const adopted = this.#adopted.get(id);
if (ref.status !== "parked" || !adopted?.revive) {
throw new Error(
`Agent "${id}" is ${ref.status} and cannot be revived${adopted?.revive ? "" : " (no reviver registered)"}. Its transcript remains readable at history://${id}.`,
);
}
const revival = this.#revive(id, adopted.revive, ref.sessionFile);
const revival = this.#resolveAndRevive(id, ref);
this.#revivals.set(id, revival);
try {
return await revival;
@@ -152,6 +170,39 @@ export class AgentLifecycleManager {
}
}
/**
* Resolve a reviver and bring the agent back to a live session. A ref
* restored from disk is `parked` with a sessionFile but no in-memory
* adoption; build a reviver via the injected persisted-subagent factory and
* adopt it so the agent rejoins the normal idle↔parked lifecycle. Throws
* when the agent is not revivable or no reviver can be produced.
*/
async #resolveAndRevive(id: string, ref: AgentRef): Promise<AgentSession> {
let revive = this.#adopted.get(id)?.revive;
let coldAdopted = false;
if (!revive && ref.status === "parked" && ref.sessionFile && this.#persistedReviverFactory) {
revive = await this.#persistedReviverFactory(ref);
if (revive) {
this.#adopted.set(id, { idleTtlMs: this.#persistedReviveTtlMs, revive });
coldAdopted = true;
}
}
if (ref.status !== "parked" || !revive) {
throw new Error(
`Agent "${id}" is ${ref.status} and cannot be revived${revive ? "" : " (no reviver registered)"}. Its transcript remains readable at history://${id}.`,
);
}
try {
return await this.#revive(id, revive, ref.sessionFile);
} catch (error) {
// A failed cold revive (stale ctx, missing cwd, bad MCP) must not leave a
// poisoned reviver stuck in #adopted — drop it so a later ensureLive
// rebuilds via the factory (which may have fresher context by then).
if (coldAdopted) this.#adopted.delete(id);
throw error;
}
}
/** Hard removal: dispose if live, unregister from registry, drop timers. */
async release(id: string): Promise<void> {
const adopted = this.#adopted.get(id);
@@ -176,6 +227,7 @@ export class AgentLifecycleManager {
await Promise.all(ids.map(id => this.release(id)));
this.#revivals.clear();
this.#parking.clear();
this.#persistedReviverFactory = undefined;
}
async #revive(id: string, revive: AgentReviver, sessionFile: string | null): Promise<AgentSession> {
@@ -124,6 +124,10 @@ export interface SessionInitEntry extends SessionEntryBase {
tools: string[];
/** Output schema if structured output was requested */
outputSchema?: unknown;
/** Spawn allowlist the subagent ran with ("" = none, "*" = any, else CSV); absent on pre-spawns files. */
spawns?: string;
/** The agent's `readSummarize` setting (`false` = read summarization disabled); absent uses the session default. */
readSummarize?: boolean;
}
/** Mode change entry - tracks agent mode transitions (e.g. plan mode). */
@@ -1178,7 +1178,14 @@ export class SessionManager {
return entry.id;
}
appendSessionInit(init: { systemPrompt: string; task: string; tools: string[]; outputSchema?: unknown }): string {
appendSessionInit(init: {
systemPrompt: string;
task: string;
tools: string[];
outputSchema?: unknown;
spawns?: string;
readSummarize?: boolean;
}): string {
const entry: SessionInitEntry = { type: "session_init", ...this.#freshEntryFields(), ...init };
this.#recordEntry(entry);
return entry.id;
@@ -1561,6 +1568,62 @@ export class SessionManager {
return manager;
}
/**
* Lock-free peek for cold subagent revival: returns the recorded working
* directory (session header) and the latest `session_init` contract (system
* prompt / tools / output schema) WITHOUT taking the single-writer lock that
* {@link open} acquires — the caller re-opens for the actual revive. Returns
* null when the file can't be read; `init` is null for files written before
* `session_init` was recorded (no faithful contract to rebuild from).
*/
static async peekSessionInit(
filePath: string,
storage: SessionStorage = new FileSessionStorage(),
): Promise<{
cwd: string;
init: {
systemPrompt: string;
task: string;
tools: string[];
outputSchema?: unknown;
spawns?: string;
readSummarize?: boolean;
} | null;
} | null> {
let loaded: FileEntry[];
try {
loaded = await loadEntriesFromFile(filePath, storage);
} catch {
return null;
}
// A missing/empty file has no usable session — nothing to revive from.
if (loaded.length === 0) return null;
const header = loaded.find(entry => entry.type === "session") as SessionHeader | undefined;
let init: {
systemPrompt: string;
task: string;
tools: string[];
outputSchema?: unknown;
spawns?: string;
readSummarize?: boolean;
} | null = null;
for (let index = loaded.length - 1; index >= 0; index--) {
const entry = loaded[index];
if (entry.type === "session_init") {
init = {
systemPrompt: entry.systemPrompt,
task: entry.task,
tools: entry.tools,
outputSchema: entry.outputSchema,
readSummarize: entry.readSummarize,
spawns: entry.spawns,
};
break;
}
}
return { cwd: header?.cwd ?? getProjectDir(), init };
}
/** Continue the most recent session, or create a new one if none exists. */
static async continueRecent(
cwd: string,
@@ -2098,6 +2098,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
systemPrompt: session.agent.state.systemPrompt.join("\n\n"),
task,
tools: session.getActiveToolNames(),
spawns: spawnsEnv,
readSummarize: agent.readSummarize,
outputSchema,
});
@@ -0,0 +1,128 @@
import * as fs from "node:fs/promises";
import type { ModelRegistry } from "../config/model-registry";
import type { Settings } from "../config/settings";
import { MCPManager } from "../mcp/manager";
import type { PersistedSubagentReviverFactory } from "../registry/agent-lifecycle";
import { AgentRegistry, MAIN_AGENT_ID } from "../registry/agent-registry";
import { createAgentSession } from "../sdk";
import type { AgentSession } from "../session/agent-session";
import type { AuthStorage } from "../session/auth-storage";
import { SessionManager } from "../session/session-manager";
import { createMCPProxyTools, createSubagentSettings } from "./executor";
/**
* Ambient context the reviver needs at revive time. The top-level session is
* kept LIVE (cwd / artifact manager read on demand) so a later `/new` or cwd
* move is followed rather than snapshotted; auth/models/settings are
* process-stable and captured by reference.
*/
export interface PersistedSubagentReviveContext {
session: AgentSession;
authStorage: AuthStorage;
modelRegistry: ModelRegistry;
settings: Settings;
/** LSP policy of the top-level session; revived subagents inherit it rather than defaulting on. */
enableLsp: boolean;
}
/**
* Build the factory the {@link AgentLifecycleManager} uses to cold-revive a
* `parked` subagent ref restored from disk (Agent Hub scan, collab mirror, or a
* resumed process). Such a ref carries a sessionFile but no in-memory adoption —
* the executor's live reviver closure died with the process/turn that spawned
* it — so `ensureLive` (IRC sends, hub focus) would otherwise refuse it.
*
* This rebuilds the subagent the same way `--resume` rebuilds a session: reopen
* the JSONL and replay it through {@link createAgentSession}. The catch is that
* resume restores only conversation/model from the file — the runtime contract
* (tools / system prompt / output schema / kind) is built from options, so a
* bare reopen would resurrect a wrong (top-level) session. We source that
* contract from the persisted `session_init` entry instead, and mirror the
* executor's subagent wiring (MCP proxy tools, depth-derived gating,
* yield-required, active-tool clamp, registry status sync).
*/
export function createPersistedSubagentReviverFactory(
ctx: PersistedSubagentReviveContext,
): PersistedSubagentReviverFactory {
const registry = AgentRegistry.global();
return async ref => {
const sessionFile = ref.sessionFile;
if (!sessionFile) return undefined;
const peek = await SessionManager.peekSessionInit(sessionFile);
// No persisted contract (pre-session_init file) or the recorded workspace
// is gone (isolated/merged worktree, moved dir): leave it transcript-only
// (history://) rather than resurrect a wrong or broken session.
if (!peek?.init) return undefined;
try {
await fs.stat(peek.cwd);
} catch {
return undefined;
}
const init = peek.init;
// taskDepth drives real capability gating (task-spawn allowance, memory
// startup, …); derive it from the persisted parent chain rather than
// assuming a fixed level.
let taskDepth = 1;
let parentId = ref.parentId;
const seen = new Set<string>();
while (parentId && parentId !== MAIN_AGENT_ID && !seen.has(parentId)) {
seen.add(parentId);
taskDepth++;
parentId = registry.get(parentId)?.parentId;
}
return async () => {
// Re-open fresh on every revive: park closes the writer, so this takes
// the single-writer lock cleanly and restores the full message history.
const reopened = await SessionManager.open(sessionFile, undefined, undefined, {
suppressBreadcrumb: true,
});
const artifactManager = ctx.session.sessionManager.getArtifactManager();
if (artifactManager) reopened.adoptArtifactManager(artifactManager);
// Reuse the parent's live MCP connections via proxy tools (no
// re-discovery), exactly as the executor does for live subagents.
const mcpManager = MCPManager.instance();
const mcpProxyTools = mcpManager ? createMCPProxyTools(mcpManager) : [];
const { session } = await createAgentSession({
cwd: ctx.session.sessionManager.getCwd(),
authStorage: ctx.authStorage,
modelRegistry: ctx.modelRegistry,
settings: createSubagentSettings(
ctx.settings,
init.readSummarize === false ? { "read.summarize.enabled": false } : undefined,
),
sessionManager: reopened,
agentId: ref.id,
agentDisplayName: ref.displayName,
parentTaskPrefix: ref.id,
parentAgentId: ref.parentId,
taskDepth,
toolNames: init.tools,
outputSchema: init.outputSchema,
requireYieldTool: true,
systemPrompt: () => [init.systemPrompt],
// Old files predate persisted spawns: deny re-spawning rather than let
// createAgentSession default to wildcard ("*").
spawns: init.spawns ?? "",
hasUI: false,
enableLsp: ctx.enableLsp,
enableMCP: !mcpManager,
mcpManager,
customTools: mcpProxyTools.length > 0 ? mcpProxyTools : undefined,
});
// Clamp the active set to the persisted list: createAgentSession's
// `alwaysInclude` can re-add non-defaultInactive extension/custom tools
// the original run didn't carry. Unknown/missing names are ignored.
await session.setActiveToolsByName(init.tools);
// Cold revives must drive registry status themselves — createAgentSession
// doesn't wire this generically (the live path does it in the executor).
// Without it the idle-TTL timer never clears on a turn and the lifecycle
// could park the agent mid-run.
session.subscribe(event => {
if (event.type === "agent_start") registry.setStatus(ref.id, "running");
else if (event.type === "agent_end") registry.setStatus(ref.id, "idle");
});
return session;
};
};
}
@@ -156,6 +156,81 @@ describe("AgentLifecycleManager", () => {
await expect(lifecycle.ensureLive("5-Sub")).rejects.toThrow(/cannot be revived.*no reviver registered/);
});
it("ensureLive cold-revives a parked ref via the persisted factory and rejoins the lifecycle", async () => {
vi.useFakeTimers();
const revived = makeSessionStub();
// Restored from disk (hub scan / resume): parked with a sessionFile but NEVER adopted.
registry.register({
id: "6-Sub",
displayName: "task",
kind: "sub",
session: null,
sessionFile: "/tmp/6-Sub.jsonl",
status: "parked",
});
let factoryCalls = 0;
lifecycle.setPersistedSubagentReviverFactory(async () => {
factoryCalls++;
return async () => revived.session;
}, TTL);
const session = await lifecycle.ensureLive("6-Sub");
expect(factoryCalls).toBe(1);
expect(session).toBe(revived.session);
expect(registry.get("6-Sub")?.status).toBe("idle");
expect(registry.get("6-Sub")?.session).toBe(revived.session);
// Adopted on demand with the configured TTL: it re-parks like any idle subagent.
vi.advanceTimersByTime(TTL);
await flushAsync();
expect(registry.get("6-Sub")?.status).toBe("parked");
expect(revived.disposeCalls()).toBe(1);
});
it("a persisted factory that declines leaves the parked ref transcript-only", async () => {
registry.register({
id: "7-Sub",
displayName: "task",
kind: "sub",
session: null,
sessionFile: "/tmp/7-Sub.jsonl",
status: "parked",
});
lifecycle.setPersistedSubagentReviverFactory(async () => undefined, TTL);
await expect(lifecycle.ensureLive("7-Sub")).rejects.toThrow(/cannot be revived.*no reviver registered/);
});
it("a failed cold revive is not sticky: the next ensureLive re-runs the factory", async () => {
const revived = makeSessionStub();
registry.register({
id: "8-Sub",
displayName: "task",
kind: "sub",
session: null,
sessionFile: "/tmp/8-Sub.jsonl",
status: "parked",
});
let factoryCalls = 0;
lifecycle.setPersistedSubagentReviverFactory(async () => {
factoryCalls++;
const failFirst = factoryCalls === 1;
return async () => {
if (failFirst) throw new Error("stale context");
return revived.session;
};
}, TTL);
await expect(lifecycle.ensureLive("8-Sub")).rejects.toThrow(/stale context/);
expect(registry.get("8-Sub")?.status).toBe("parked");
const session = await lifecycle.ensureLive("8-Sub");
expect(factoryCalls).toBe(2);
expect(session).toBe(revived.session);
expect(registry.get("8-Sub")?.status).toBe("idle");
});
it("release disposes a live adopted agent, unregisters it, and leaves no pending park", async () => {
vi.useFakeTimers();
const stub = makeSessionStub();
@@ -0,0 +1,84 @@
import { afterEach, describe, expect, it } from "bun:test";
import * as path from "node:path";
import { getBundledModel } from "@oh-my-pi/pi-catalog/models";
import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager";
import { TempDir } from "@oh-my-pi/pi-utils";
const tempDirs: TempDir[] = [];
function makeTempDir(prefix: string): string {
const dir = TempDir.createSync(prefix);
tempDirs.push(dir);
return dir.path();
}
afterEach(async () => {
await Promise.all(tempDirs.splice(0).map(dir => dir.remove()));
});
function assistantMessage(text: string) {
const model = getBundledModel("anthropic", "claude-sonnet-4-5");
if (!model) throw new Error("Expected built-in anthropic model to exist");
return {
role: "assistant" as const,
content: [{ type: "text" as const, text }],
api: model.api,
provider: model.provider,
model: model.id,
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "stop" as const,
timestamp: Date.now(),
};
}
describe("SessionManager.peekSessionInit", () => {
it("returns the latest session_init contract (tools/spawns/readSummarize) and the header cwd", async () => {
const cwd = makeTempDir("@pi-peek-cwd-");
const manager = SessionManager.create(cwd, path.join(cwd, "sessions"));
const sessionFile = manager.getSessionFile();
if (!sessionFile) throw new Error("Expected a persisted session file path");
manager.appendSessionInit({ systemPrompt: "first", task: "t1", tools: ["read"], spawns: "" });
manager.appendSessionInit({
systemPrompt: "second",
task: "t2",
tools: ["read", "bash", "yield"],
spawns: "task",
readSummarize: false,
});
// Flush buffered entries (header + inits) so the lock-free peek can read them off disk.
manager.appendMessage(assistantMessage("flush"));
const peek = await SessionManager.peekSessionInit(sessionFile);
expect(peek?.cwd).toBe(manager.getCwd());
// Latest init wins — the reviver must rebuild from the most recent contract.
expect(peek?.init?.systemPrompt).toBe("second");
expect(peek?.init?.tools).toEqual(["read", "bash", "yield"]);
expect(peek?.init?.spawns).toBe("task");
expect(peek?.init?.readSummarize).toBe(false);
});
it("returns init: null for a session file with no session_init (a main/legacy session)", async () => {
const cwd = makeTempDir("@pi-peek-legacy-");
const manager = SessionManager.create(cwd, path.join(cwd, "sessions"));
const sessionFile = manager.getSessionFile();
if (!sessionFile) throw new Error("Expected a persisted session file path");
manager.appendMessage(assistantMessage("hi"));
const peek = await SessionManager.peekSessionInit(sessionFile);
expect(peek?.cwd).toBe(manager.getCwd());
expect(peek?.init).toBeNull();
});
it("returns null for a file that cannot be read", async () => {
const peek = await SessionManager.peekSessionInit(path.join(makeTempDir("@pi-peek-missing-"), "nope.jsonl"));
expect(peek).toBeNull();
});
});