@pi-harness/pi-harness 0.1.90 → 0.1.92
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/web/dist/assets/{index-BHsP1uCF.js → index-9WIYl98Q.js} +1 -1
- package/apps/web/dist/index.html +1 -1
- package/apps/web/package.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/index.d.ts.map +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/index.js +22 -0
- package/node_modules/@pi-harness/api-gateway/dist/index.js.map +1 -1
- package/node_modules/@pi-harness/api-gateway/package.json +1 -1
- package/node_modules/@pi-harness/cli/package.json +1 -1
- package/node_modules/@pi-harness/host-webserver/package.json +1 -1
- package/node_modules/@pi-harness/web-app/package.json +1 -1
- package/package.json +6 -6
- package/packages/api-gateway/dist/index.d.ts.map +1 -1
- package/packages/api-gateway/dist/index.js +22 -0
- package/packages/api-gateway/dist/index.js.map +1 -1
- package/packages/api-gateway/package.json +1 -1
- package/packages/api-gateway/src/index.ts +18 -0
- package/packages/api-gateway/test/index.test.ts +78 -0
- package/packages/bundle-web-app/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/host-webserver/package.json +1 -1
|
@@ -2303,6 +2303,15 @@ export default {
|
|
|
2303
2303
|
sendJson(response, 400, { error: "Invalid session path" });
|
|
2304
2304
|
return;
|
|
2305
2305
|
}
|
|
2306
|
+
// Pi defers creating a JSONL file for a new empty session until its first assistant response. A duplicate is still a durable action, so materialize the active session before listing it; missing non-active paths remain a 404.
|
|
2307
|
+
if (sourcePath === services.runtime.session.sessionFile && !existsSync(sourcePath)) {
|
|
2308
|
+
try {
|
|
2309
|
+
persistSessionBeforeFirstAssistant(manager);
|
|
2310
|
+
} catch (error) {
|
|
2311
|
+
// Another tab may win the first-persistence race between existsSync and writeFileSync; its file is the same fork source, so continue with the list.
|
|
2312
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2306
2315
|
const sessions = await SessionManager.list(activeCwd(services), manager.getSessionDir());
|
|
2307
2316
|
const source = sessions.find((item) => item.path === sourcePath);
|
|
2308
2317
|
if (!source) {
|
|
@@ -2397,6 +2406,15 @@ export default {
|
|
|
2397
2406
|
sendJson(response, 400, { error: "Invalid session path" });
|
|
2398
2407
|
return;
|
|
2399
2408
|
}
|
|
2409
|
+
// Pi defers creating the JSONL file for a new empty session until its first entry. Exporting that active session is still a durable user action, so materialize its in-memory header and entries before reading it; missing non-active paths remain a 404.
|
|
2410
|
+
if (path === services.runtime.session.sessionFile && !existsSync(path)) {
|
|
2411
|
+
try {
|
|
2412
|
+
persistSessionBeforeFirstAssistant(manager);
|
|
2413
|
+
} catch (error) {
|
|
2414
|
+
// Another tab may win the first-persistence race between existsSync and writeFileSync; its file is the same export target, so continue with the read.
|
|
2415
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2400
2418
|
const content = await readFile(path, "utf8");
|
|
2401
2419
|
response.writeHead(200, {
|
|
2402
2420
|
"content-type": "application/x-ndjson; charset=utf-8",
|
|
@@ -2093,6 +2093,51 @@ describe("API gateway plugin", () => {
|
|
|
2093
2093
|
expect(sessionPayload.items).toEqual(expect.arrayContaining([expect.objectContaining({ sessionId: fork.sessionId, forked: true })]));
|
|
2094
2094
|
});
|
|
2095
2095
|
|
|
2096
|
+
test("forks an empty active session before its JSONL file is created", async () => {
|
|
2097
|
+
const context = new Context();
|
|
2098
|
+
contexts.push(context);
|
|
2099
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
2100
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-active-empty-fork-"));
|
|
2101
|
+
temporaryDirectories.push(directory);
|
|
2102
|
+
const launchCwd = join(directory, "launch");
|
|
2103
|
+
const activeCwd = join(directory, "active");
|
|
2104
|
+
const manager = SessionManager.create(activeCwd, directory);
|
|
2105
|
+
const sourcePath = manager.getSessionFile();
|
|
2106
|
+
expect(sourcePath).toBeDefined();
|
|
2107
|
+
const session = {
|
|
2108
|
+
get sessionId() {
|
|
2109
|
+
return manager.getSessionId();
|
|
2110
|
+
},
|
|
2111
|
+
get sessionFile() {
|
|
2112
|
+
return manager.getSessionFile();
|
|
2113
|
+
},
|
|
2114
|
+
get messages() {
|
|
2115
|
+
return manager.buildSessionContext().messages;
|
|
2116
|
+
},
|
|
2117
|
+
isStreaming: false,
|
|
2118
|
+
sessionManager: manager,
|
|
2119
|
+
subscribe: () => () => {},
|
|
2120
|
+
};
|
|
2121
|
+
context.provide("piRuntime", { session, sessionRuntime: { cwd: activeCwd }, prompt: () => Promise.resolve() } as never);
|
|
2122
|
+
context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
|
|
2123
|
+
context.provide("piHarnessLaunch", { cwd: launchCwd, agentDir: directory, args: [], requestExit() {} });
|
|
2124
|
+
await context.plugin(apiPlugin);
|
|
2125
|
+
|
|
2126
|
+
const response = await fetch(context.webServer.url + "/api/session/fork", {
|
|
2127
|
+
method: "POST",
|
|
2128
|
+
headers: { "content-type": "application/json" },
|
|
2129
|
+
body: JSON.stringify({ path: sourcePath }),
|
|
2130
|
+
});
|
|
2131
|
+
|
|
2132
|
+
expect(response.status).toBe(200);
|
|
2133
|
+
const fork = (await response.json()) as { sessionId: string; sessionFile?: string; cwd: string };
|
|
2134
|
+
expect(fork).toMatchObject({ cwd: activeCwd });
|
|
2135
|
+
expect(fork.sessionFile).toBeDefined();
|
|
2136
|
+
expect(fork.sessionFile).not.toBe(sourcePath);
|
|
2137
|
+
const sourceContent = await readFile(sourcePath ?? "", "utf8");
|
|
2138
|
+
expect(sourceContent).toContain('"type":"session"');
|
|
2139
|
+
});
|
|
2140
|
+
|
|
2096
2141
|
test("opens a forked persisted session from the session list and exposes its relationship", async () => {
|
|
2097
2142
|
const context = new Context();
|
|
2098
2143
|
contexts.push(context);
|
|
@@ -3805,6 +3850,39 @@ describe("API gateway plugin", () => {
|
|
|
3805
3850
|
await expect(stat(target)).rejects.toMatchObject({ code: "ENOENT" });
|
|
3806
3851
|
});
|
|
3807
3852
|
|
|
3853
|
+
test("exports an empty active session by persisting its in-memory header", async () => {
|
|
3854
|
+
const context = new Context();
|
|
3855
|
+
contexts.push(context);
|
|
3856
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-empty-session-export-"));
|
|
3857
|
+
temporaryDirectories.push(directory);
|
|
3858
|
+
const sessionDir = join(directory, "sessions");
|
|
3859
|
+
await mkdir(sessionDir);
|
|
3860
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
3861
|
+
const manager = SessionManager.create("/tmp", sessionDir);
|
|
3862
|
+
const target = manager.getSessionFile();
|
|
3863
|
+
if (!target) throw new Error("Expected a new session path");
|
|
3864
|
+
const session = {
|
|
3865
|
+
sessionId: manager.getSessionId(),
|
|
3866
|
+
get sessionFile() {
|
|
3867
|
+
return manager.getSessionFile();
|
|
3868
|
+
},
|
|
3869
|
+
messages: [],
|
|
3870
|
+
isStreaming: false,
|
|
3871
|
+
sessionManager: manager,
|
|
3872
|
+
subscribe: () => () => {},
|
|
3873
|
+
};
|
|
3874
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve() } as never);
|
|
3875
|
+
context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
|
|
3876
|
+
context.provide("piHarnessLaunch", { cwd: "/tmp", agentDir: "/tmp/agent", args: [], requestExit() {} });
|
|
3877
|
+
await context.plugin(apiPlugin);
|
|
3878
|
+
|
|
3879
|
+
const response = await fetch(context.webServer.url + "/api/session/export");
|
|
3880
|
+
expect(response.status).toBe(200);
|
|
3881
|
+
expect(response.headers.get("content-type")).toContain("application/x-ndjson");
|
|
3882
|
+
expect(await response.text()).toContain('"type":"session"');
|
|
3883
|
+
await expect(stat(target)).resolves.toBeDefined();
|
|
3884
|
+
});
|
|
3885
|
+
|
|
3808
3886
|
test("keeps batch deletion going past a failing session and persists the metadata it did remove", async () => {
|
|
3809
3887
|
const context = new Context();
|
|
3810
3888
|
contexts.push(context);
|