@pi-harness/pi-harness 0.1.89 → 0.1.91

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.
@@ -2167,7 +2167,10 @@ export default {
2167
2167
  }
2168
2168
  events.length = 0;
2169
2169
  }
2170
- await unlink(path);
2170
+ // A newly created active session may not have a JSONL file yet: Pi defers persistence until the first entry. Treat that missing file as already deleted, while preserving failures for unexpected paths such as directories.
2171
+ await unlink(path).catch((error: unknown) => {
2172
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
2173
+ });
2171
2174
  await mutateSessionMetadata(manager, context.logger, (metadata) => {
2172
2175
  delete metadata[path];
2173
2176
  });
@@ -2394,6 +2397,15 @@ export default {
2394
2397
  sendJson(response, 400, { error: "Invalid session path" });
2395
2398
  return;
2396
2399
  }
2400
+ // 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.
2401
+ if (path === services.runtime.session.sessionFile && !existsSync(path)) {
2402
+ try {
2403
+ persistSessionBeforeFirstAssistant(manager);
2404
+ } catch (error) {
2405
+ // Another tab may win the first-persistence race between existsSync and writeFileSync; its file is the same export target, so continue with the read.
2406
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
2407
+ }
2408
+ }
2397
2409
  const content = await readFile(path, "utf8");
2398
2410
  response.writeHead(200, {
2399
2411
  "content-type": "application/x-ndjson; charset=utf-8",
@@ -3762,6 +3762,82 @@ describe("API gateway plugin", () => {
3762
3762
  expect(JSON.parse(await readFile(join(sessionDir, ".pi-harness-session-meta.json"), "utf8"))).toEqual({});
3763
3763
  });
3764
3764
 
3765
+ test("deletes an empty active session whose JSONL file has not been persisted yet", async () => {
3766
+ const context = new Context();
3767
+ contexts.push(context);
3768
+ const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-empty-session-delete-"));
3769
+ temporaryDirectories.push(directory);
3770
+ const sessionDir = join(directory, "sessions");
3771
+ await mkdir(sessionDir);
3772
+ await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
3773
+ const target = join(sessionDir, "2026-08-30T00-00-00-000Z_empty.jsonl");
3774
+ const manager = SessionManager.create("/tmp", sessionDir);
3775
+ let activePath: string | undefined = target;
3776
+ const session = {
3777
+ sessionId: "empty-session",
3778
+ get sessionFile() {
3779
+ return activePath;
3780
+ },
3781
+ messages: [],
3782
+ isStreaming: false,
3783
+ sessionManager: manager,
3784
+ extensionRunner: { setUIContext() {} },
3785
+ subscribe: () => () => {},
3786
+ };
3787
+ const sessionRuntime = {
3788
+ newSession() {
3789
+ activePath = undefined;
3790
+ return Promise.resolve({ cancelled: false });
3791
+ },
3792
+ };
3793
+ context.provide("piRuntime", { session, sessionRuntime, prompt: () => Promise.resolve() } as never);
3794
+ context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
3795
+ context.provide("piHarnessLaunch", { cwd: "/tmp", agentDir: "/tmp/agent", args: [], requestExit() {} });
3796
+ await context.plugin(apiPlugin);
3797
+
3798
+ const response = await fetch(context.webServer.url + "/api/session/delete", {
3799
+ method: "POST",
3800
+ headers: { "content-type": "application/json" },
3801
+ body: JSON.stringify({ path: target, confirm: true }),
3802
+ });
3803
+ expect(response.status).toBe(200);
3804
+ await expect(response.json()).resolves.toMatchObject({ deleted: true, path: target });
3805
+ await expect(stat(target)).rejects.toMatchObject({ code: "ENOENT" });
3806
+ });
3807
+
3808
+ test("exports an empty active session by persisting its in-memory header", async () => {
3809
+ const context = new Context();
3810
+ contexts.push(context);
3811
+ const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-empty-session-export-"));
3812
+ temporaryDirectories.push(directory);
3813
+ const sessionDir = join(directory, "sessions");
3814
+ await mkdir(sessionDir);
3815
+ await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
3816
+ const manager = SessionManager.create("/tmp", sessionDir);
3817
+ const target = manager.getSessionFile();
3818
+ if (!target) throw new Error("Expected a new session path");
3819
+ const session = {
3820
+ sessionId: manager.getSessionId(),
3821
+ get sessionFile() {
3822
+ return manager.getSessionFile();
3823
+ },
3824
+ messages: [],
3825
+ isStreaming: false,
3826
+ sessionManager: manager,
3827
+ subscribe: () => () => {},
3828
+ };
3829
+ context.provide("piRuntime", { session, prompt: () => Promise.resolve() } as never);
3830
+ context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
3831
+ context.provide("piHarnessLaunch", { cwd: "/tmp", agentDir: "/tmp/agent", args: [], requestExit() {} });
3832
+ await context.plugin(apiPlugin);
3833
+
3834
+ const response = await fetch(context.webServer.url + "/api/session/export");
3835
+ expect(response.status).toBe(200);
3836
+ expect(response.headers.get("content-type")).toContain("application/x-ndjson");
3837
+ expect(await response.text()).toContain('"type":"session"');
3838
+ await expect(stat(target)).resolves.toBeDefined();
3839
+ });
3840
+
3765
3841
  test("keeps batch deletion going past a failing session and persists the metadata it did remove", async () => {
3766
3842
  const context = new Context();
3767
3843
  contexts.push(context);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/web-app",
3
- "version": "0.1.89",
3
+ "version": "0.1.91",
4
4
  "private": true,
5
5
  "description": "Patchable Web application bundle for Pi Harness",
6
6
  "homepage": "https://github.com/pi-harness/pi-harness#readme",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/cli",
3
- "version": "0.1.89",
3
+ "version": "0.1.91",
4
4
  "private": true,
5
5
  "description": "Plugin-first Pi Harness CLI",
6
6
  "homepage": "https://github.com/pi-harness/pi-harness#readme",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/host-webserver",
3
- "version": "0.1.89",
3
+ "version": "0.1.91",
4
4
  "private": true,
5
5
  "description": "Cordis route-registration service for the Pi Harness web host",
6
6
  "homepage": "https://github.com/pi-harness/pi-harness#readme",