@hienlh/ppm 0.9.69 → 0.9.70

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,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.70] - 2026-04-09
4
+
5
+ ### Fixed
6
+ - **SDK subprocess crash on resume**: Resumed sessions with existing JSONL files would crash (exit code 1) because `--session-id` was used instead of `--resume`. Now always uses `--resume` for resumed sessions.
7
+ - **Session lookup**: Use targeted `getSessionInfo()` with correct project dir instead of listing all sessions.
8
+ - **SDK stderr capture**: Subprocess stderr is now logged on crash for diagnostics.
9
+ - **Thinking budget option**: Fixed `thinkingBudgetTokens` → `maxThinkingTokens` (correct SDK option name).
10
+
3
11
  ## [0.9.69] - 2026-04-09
4
12
 
5
13
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hienlh/ppm",
3
- "version": "0.9.69",
3
+ "version": "0.9.70",
4
4
  "description": "Personal Project Manager — mobile-first web IDE with AI assistance",
5
5
  "author": "hienlh",
6
6
  "license": "MIT",
@@ -280,18 +280,33 @@ export class ClaudeAgentSdkProvider implements AIProvider {
280
280
  // Restore project_path from DB so resumed sessions can find JSONL
281
281
  const dbProjectPath = getSessionProjectPath(sessionId) ?? undefined;
282
282
 
283
+ // Try targeted lookup first (searches all project dirs), then fall back to list scan
283
284
  try {
284
- const sdkSessions = await sdkListSessions({ limit: 100 });
285
- const found = sdkSessions.find(
286
- (s) => s.sessionId === sessionId || s.sessionId === mappedSdkId,
287
- );
288
- if (found) {
285
+ const lookupId = mappedSdkId ?? sessionId;
286
+ const info = await sdkGetSessionInfo(lookupId, { dir: dbProjectPath });
287
+ if (!info && mappedSdkId) {
288
+ // Try the original PPM session ID as well
289
+ const info2 = await sdkGetSessionInfo(sessionId, { dir: dbProjectPath });
290
+ if (info2) {
291
+ const meta: Session = {
292
+ id: sessionId,
293
+ providerId: this.id,
294
+ title: info2.customTitle ?? info2.summary ?? "Resumed Chat",
295
+ projectPath: dbProjectPath,
296
+ createdAt: new Date(info2.lastModified).toISOString(),
297
+ };
298
+ this.activeSessions.set(sessionId, meta);
299
+ this.messageCount.set(sessionId, 1);
300
+ return meta;
301
+ }
302
+ }
303
+ if (info) {
289
304
  const meta: Session = {
290
305
  id: sessionId,
291
306
  providerId: this.id,
292
- title: found.customTitle ?? found.summary ?? "Resumed Chat",
307
+ title: info.customTitle ?? info.summary ?? "Resumed Chat",
293
308
  projectPath: dbProjectPath,
294
- createdAt: new Date(found.lastModified).toISOString(),
309
+ createdAt: new Date(info.lastModified).toISOString(),
295
310
  };
296
311
  this.activeSessions.set(sessionId, meta);
297
312
  this.messageCount.set(sessionId, 1);
@@ -301,8 +316,11 @@ export class ClaudeAgentSdkProvider implements AIProvider {
301
316
  // SDK not available
302
317
  }
303
318
 
304
- // Session not found in SDK historytreat as new so first message
305
- // creates a fresh SDK session instead of trying to resume.
319
+ // Session not found in SDK listit may still have a JSONL on disk
320
+ // (sdkListSessions may not search the correct project directory).
321
+ // Use messageCount=1 so sendMessage uses --resume instead of --session-id.
322
+ // --resume gracefully fails if no JSONL exists, while --session-id crashes
323
+ // when a JSONL file for the same ID is already present on disk.
306
324
  const meta: Session = {
307
325
  id: sessionId,
308
326
  providerId: this.id,
@@ -311,7 +329,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
311
329
  createdAt: new Date().toISOString(),
312
330
  };
313
331
  this.activeSessions.set(sessionId, meta);
314
- this.messageCount.set(sessionId, 0);
332
+ this.messageCount.set(sessionId, 1);
315
333
  return meta;
316
334
  }
317
335
 
@@ -677,6 +695,14 @@ export class ClaudeAgentSdkProvider implements AIProvider {
677
695
  const mcpServers = mcpConfigService.list();
678
696
  const hasMcp = Object.keys(mcpServers).length > 0;
679
697
 
698
+ // Buffer subprocess stderr for crash diagnostics
699
+ let stderrBuffer = "";
700
+ const stderrCallback = (chunk: string) => {
701
+ stderrBuffer += chunk;
702
+ // Keep only last 2KB to avoid unbounded growth
703
+ if (stderrBuffer.length > 2048) stderrBuffer = stderrBuffer.slice(-2048);
704
+ };
705
+
680
706
  const queryOptions: Record<string, any> = {
681
707
  // On Windows, child_process.spawn("bun") fails with ENOENT — force node
682
708
  ...(process.platform === "win32" && { executable: "node" }),
@@ -697,9 +723,10 @@ export class ClaudeAgentSdkProvider implements AIProvider {
697
723
  maxTurns: providerConfig.max_turns ?? 1000,
698
724
  ...(providerConfig.max_budget_usd && { maxBudgetUsd: providerConfig.max_budget_usd }),
699
725
  ...(providerConfig.thinking_budget_tokens != null && {
700
- thinkingBudgetTokens: providerConfig.thinking_budget_tokens,
726
+ maxThinkingTokens: providerConfig.thinking_budget_tokens,
701
727
  }),
702
728
  includePartialMessages: true,
729
+ stderr: stderrCallback,
703
730
  };
704
731
 
705
732
  // Crash retry: if subprocess exits with non-zero code before producing events,
@@ -1391,7 +1418,8 @@ export class ClaudeAgentSdkProvider implements AIProvider {
1391
1418
  break crashRetryLoop; // Normal completion — exit crash retry loop
1392
1419
  } catch (crashErr) {
1393
1420
  const crashMsg = (crashErr as Error).message ?? String(crashErr);
1394
- console.error(`[sdk] session=${sessionId} cwd=${meta.projectPath} error: ${crashMsg}`);
1421
+ const stderrInfo = stderrBuffer.trim() ? ` stderr: ${stderrBuffer.trim().slice(-500)}` : "";
1422
+ console.error(`[sdk] session=${sessionId} cwd=${meta.projectPath} error: ${crashMsg}${stderrInfo}`);
1395
1423
 
1396
1424
  // Clean up crashed subprocess before retry or error
1397
1425
  this.activeQueries.delete(sessionId);
@@ -1404,12 +1432,14 @@ export class ClaudeAgentSdkProvider implements AIProvider {
1404
1432
  } else if (crashMsg.includes("exited with code") && crashRetryCount < MAX_CRASH_RETRIES) {
1405
1433
  // Subprocess crashed — auto-retry once before surfacing the error
1406
1434
  crashRetryCount++;
1407
- console.warn(`[sdk] session=${sessionId} subprocess crashed: ${crashMsg} — auto-retrying (attempt ${crashRetryCount}/${MAX_CRASH_RETRIES})`);
1435
+ console.warn(`[sdk] session=${sessionId} subprocess crashed: ${crashMsg} — auto-retrying (attempt ${crashRetryCount}/${MAX_CRASH_RETRIES})${stderrInfo}`);
1436
+ stderrBuffer = ""; // Reset for retry
1408
1437
  await new Promise((r) => setTimeout(r, 1000));
1409
1438
  continue crashRetryLoop;
1410
1439
  } else if (crashMsg.includes("exited with code")) {
1411
- console.warn(`[sdk] session=${sessionId} subprocess crashed after retry: ${crashMsg}`);
1412
- yield { type: "error", message: `SDK subprocess crashed. Send another message to auto-recover.` };
1440
+ console.warn(`[sdk] session=${sessionId} subprocess crashed after retry: ${crashMsg}${stderrInfo}`);
1441
+ const userHint = stderrInfo ? ` (${stderrBuffer.trim().slice(-200)})` : "";
1442
+ yield { type: "error", message: `SDK subprocess crashed.${userHint} Send another message to auto-recover.` };
1413
1443
  } else {
1414
1444
  yield { type: "error", message: `SDK error: ${crashMsg}` };
1415
1445
  }