@hienlh/ppm 0.8.93 → 0.8.95

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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.8.95] - 2026-04-02
4
+
5
+ ### Fixed
6
+ - **Streaming auth loop**: SDK auth errors in streaming mode don't emit result events, leaving the session alive with broken credentials — every follow-up message fails with 401 forever. Now breaks the loop, cooldowns the account, and tears down the session so the next message picks a different account.
7
+ - **Streaming session resource leak**: `finally` block now properly closes SDK subprocess and generator instead of just removing from map.
8
+
9
+ ## [0.8.94] - 2026-04-02
10
+
11
+ ### Fixed
12
+ - **Session JSONL "not found" after disconnect/restart**: Debug endpoint relied on in-memory `projectPath` which was lost when session cleanup timer expired or server restarted. Now persists `project_path` in `session_map` DB table (migration 11) and falls back to DB lookup when in-memory state is gone.
13
+ - **Session mapping missing project info**: `setSessionMapping` now saves `projectName` and `projectPath` at both session creation and SDK init, ensuring resumed sessions can locate their JSONL files.
14
+
3
15
  ## [0.8.93] - 2026-04-02
4
16
 
5
17
  ### Removed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hienlh/ppm",
3
- "version": "0.8.93",
3
+ "version": "0.8.95",
4
4
  "description": "Personal Project Manager — mobile-first web IDE with AI assistance",
5
5
  "author": "hienlh",
6
6
  "license": "MIT",
@@ -15,7 +15,7 @@ import type {
15
15
  import { configService } from "../services/config.service.ts";
16
16
  import { mcpConfigService } from "../services/mcp-config.service.ts";
17
17
  import { updateFromSdkEvent } from "../services/claude-usage.service.ts";
18
- import { getSessionMapping, setSessionMapping, getSessionTitles } from "../services/db.service.ts";
18
+ import { getSessionMapping, getSessionProjectPath, setSessionMapping, getSessionTitles } from "../services/db.service.ts";
19
19
  import { accountSelector } from "../services/account-selector.service.ts";
20
20
  import { accountService } from "../services/account.service.ts";
21
21
  import { resolve } from "node:path";
@@ -244,6 +244,8 @@ export class ClaudeAgentSdkProvider implements AIProvider {
244
244
  };
245
245
  this.activeSessions.set(id, meta);
246
246
  this.messageCount.set(id, 0);
247
+ // Pre-persist mapping so project_path survives server restarts
248
+ setSessionMapping(id, id, config.projectName, config.projectPath);
247
249
  return meta;
248
250
  }
249
251
 
@@ -253,6 +255,8 @@ export class ClaudeAgentSdkProvider implements AIProvider {
253
255
 
254
256
  // Check if we have a mapped SDK session ID (from a previous query)
255
257
  const mappedSdkId = getSdkSessionId(sessionId);
258
+ // Restore project_path from DB so resumed sessions can find JSONL
259
+ const dbProjectPath = getSessionProjectPath(sessionId) ?? undefined;
256
260
 
257
261
  try {
258
262
  const sdkSessions = await sdkListSessions({ limit: 100 });
@@ -264,6 +268,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
264
268
  id: sessionId,
265
269
  providerId: this.id,
266
270
  title: found.customTitle ?? found.summary ?? "Resumed Chat",
271
+ projectPath: dbProjectPath,
267
272
  createdAt: new Date(found.lastModified).toISOString(),
268
273
  };
269
274
  this.activeSessions.set(sessionId, meta);
@@ -280,6 +285,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
280
285
  id: sessionId,
281
286
  providerId: this.id,
282
287
  title: "Resumed Chat",
288
+ projectPath: dbProjectPath,
283
289
  createdAt: new Date().toISOString(),
284
290
  };
285
291
  this.activeSessions.set(sessionId, meta);
@@ -730,7 +736,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
730
736
  if (subtype === "init") {
731
737
  const initMsg = msg as any;
732
738
  if (initMsg.session_id && initMsg.session_id !== sessionId) {
733
- setSessionMapping(sessionId, initMsg.session_id);
739
+ setSessionMapping(sessionId, initMsg.session_id, meta.projectName, meta.projectPath);
734
740
  const oldMeta = this.activeSessions.get(sessionId);
735
741
  if (oldMeta) {
736
742
  this.activeSessions.set(initMsg.session_id, { ...oldMeta, id: initMsg.session_id });
@@ -904,6 +910,18 @@ export class ClaudeAgentSdkProvider implements AIProvider {
904
910
  }
905
911
  }
906
912
 
913
+ // Auth failed permanently after retry — cooldown account and break loop.
914
+ // SDK doesn't send a result event after auth errors in streaming mode,
915
+ // so the streaming session would stay alive with broken credentials forever.
916
+ // Breaking here lets the finally block tear down the session, so the next
917
+ // user message creates a fresh session with a different account.
918
+ if (assistantError === "authentication_failed" && account && authRetried) {
919
+ accountSelector.onAuthError(account.id);
920
+ console.warn(`[sdk] session=${sessionId} auth permanently failed — tearing down streaming session`);
921
+ yield { type: "error", message: "API authentication failed. Check your account credentials in Settings → Accounts." };
922
+ break;
923
+ }
924
+
907
925
  const errorHints: Record<string, string> = {
908
926
  authentication_failed: "API authentication failed. Check your account credentials in Settings → Accounts.",
909
927
  billing_error: "Billing error on this account. Check your subscription status.",
@@ -1152,7 +1170,13 @@ export class ClaudeAgentSdkProvider implements AIProvider {
1152
1170
  }
1153
1171
  } finally {
1154
1172
  this.activeQueries.delete(sessionId);
1155
- this.streamingSessions.delete(sessionId);
1173
+ // Properly close streaming session: terminate subprocess + generator
1174
+ const ss = this.streamingSessions.get(sessionId);
1175
+ if (ss) {
1176
+ ss.controller.done();
1177
+ ss.query.close();
1178
+ this.streamingSessions.delete(sessionId);
1179
+ }
1156
1180
  console.log(`[sdk] session=${sessionId} streaming session ended`);
1157
1181
  }
1158
1182
 
@@ -8,7 +8,7 @@ import { renameSession as sdkRenameSession } from "@anthropic-ai/claude-agent-sd
8
8
  import { listSlashItems } from "../../services/slash-items.service.ts";
9
9
  import { getCachedUsage, refreshUsageNow } from "../../services/claude-usage.service.ts";
10
10
  import { getSessionLog } from "../../services/session-log.service.ts";
11
- import { getSessionMapping, setSessionMapping, setSessionTitle, getPinnedSessionIds, pinSession, unpinSession, deleteSessionMapping, deleteSessionTitle } from "../../services/db.service.ts";
11
+ import { getSessionMapping, getSessionProjectPath, setSessionMapping, setSessionTitle, getPinnedSessionIds, pinSession, unpinSession, deleteSessionMapping, deleteSessionTitle } from "../../services/db.service.ts";
12
12
  import { ok, err } from "../../types/api.ts";
13
13
 
14
14
  type Env = { Variables: { projectPath: string; projectName: string } };
@@ -236,11 +236,13 @@ chatRoutes.get("/sessions/:id/logs", (c) => {
236
236
  chatRoutes.get("/sessions/:id/debug", (c) => {
237
237
  const ppmId = c.req.param("id");
238
238
  const sdkId = getSessionMapping(ppmId) ?? ppmId;
239
- const projectName = c.req.query("project") ?? "";
240
239
  // Resolve JSONL path: ~/.claude/projects/<encoded-cwd>/<sdkId>.jsonl
241
240
  const homedir = process.env.HOME ?? process.env.USERPROFILE ?? "";
242
241
  const provider = providerRegistry.get("claude") as any;
243
- const projectPath = provider?.activeSessions?.get(ppmId)?.projectPath ?? "";
242
+ // Try in-memory first, fall back to DB-persisted project_path
243
+ const projectPath = provider?.activeSessions?.get(ppmId)?.projectPath
244
+ ?? getSessionProjectPath(ppmId)
245
+ ?? "";
244
246
  const encodedCwd = projectPath ? projectPath.replace(/\//g, "-") : "";
245
247
  const jsonlDir = encodedCwd ? resolve(homedir, ".claude", "projects", encodedCwd) : "";
246
248
  const jsonlPath = jsonlDir ? resolve(jsonlDir, `${sdkId}.jsonl`) : "";
@@ -271,6 +271,21 @@ function runMigrations(database: Database): void {
271
271
  PRAGMA user_version = 10;
272
272
  `);
273
273
  }
274
+
275
+ if (current < 11) {
276
+ try {
277
+ database.exec(`ALTER TABLE session_map ADD COLUMN project_path TEXT`);
278
+ } catch {
279
+ // Column may already exist
280
+ }
281
+ // Backfill project_path from projects table where project_name matches
282
+ database.exec(`
283
+ UPDATE session_map SET project_path = (
284
+ SELECT path FROM projects WHERE projects.name = session_map.project_name
285
+ ) WHERE project_path IS NULL AND project_name IS NOT NULL
286
+ `);
287
+ database.exec(`PRAGMA user_version = 11`);
288
+ }
274
289
  }
275
290
 
276
291
  // ---------------------------------------------------------------------------
@@ -365,10 +380,15 @@ export function getSessionMapping(ppmId: string): string | null {
365
380
  return row?.sdk_id ?? null;
366
381
  }
367
382
 
368
- export function setSessionMapping(ppmId: string, sdkId: string, projectName?: string): void {
383
+ export function getSessionProjectPath(ppmId: string): string | null {
384
+ const row = getDb().query("SELECT project_path FROM session_map WHERE ppm_id = ?").get(ppmId) as { project_path: string } | null;
385
+ return row?.project_path ?? null;
386
+ }
387
+
388
+ export function setSessionMapping(ppmId: string, sdkId: string, projectName?: string, projectPath?: string): void {
369
389
  getDb().query(
370
- "INSERT INTO session_map (ppm_id, sdk_id, project_name) VALUES (?, ?, ?) ON CONFLICT(ppm_id) DO UPDATE SET sdk_id = excluded.sdk_id, project_name = excluded.project_name",
371
- ).run(ppmId, sdkId, projectName ?? null);
390
+ "INSERT INTO session_map (ppm_id, sdk_id, project_name, project_path) VALUES (?, ?, ?, ?) ON CONFLICT(ppm_id) DO UPDATE SET sdk_id = excluded.sdk_id, project_name = COALESCE(excluded.project_name, session_map.project_name), project_path = COALESCE(excluded.project_path, session_map.project_path)",
391
+ ).run(ppmId, sdkId, projectName ?? null, projectPath ?? null);
372
392
  }
373
393
 
374
394
  export function getAllSessionMappings(): Record<string, string> {