@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 +12 -0
- package/package.json +1 -1
- package/src/providers/claude-agent-sdk.ts +27 -3
- package/src/server/routes/chat.ts +5 -3
- package/src/services/db.service.ts +23 -3
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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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> {
|