@hienlh/ppm 0.2.12 → 0.2.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hienlh/ppm",
3
- "version": "0.2.12",
3
+ "version": "0.2.14",
4
4
  "description": "Personal Project Manager — mobile-first web IDE with AI assistance",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
@@ -13,6 +13,34 @@ import type {
13
13
  UsageInfo,
14
14
  } from "./provider.interface.ts";
15
15
  import { configService } from "../services/config.service.ts";
16
+ import { resolve } from "node:path";
17
+ import { homedir } from "node:os";
18
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
19
+
20
+ /** Persistent PPM sessionId → SDK sessionId mapping */
21
+ const SESSION_MAP_FILE = resolve(homedir(), ".ppm", "session-map.json");
22
+
23
+ function loadSessionMap(): Record<string, string> {
24
+ try {
25
+ if (existsSync(SESSION_MAP_FILE)) return JSON.parse(readFileSync(SESSION_MAP_FILE, "utf-8"));
26
+ } catch {}
27
+ return {};
28
+ }
29
+
30
+ function saveSessionMapping(ppmId: string, sdkId: string): void {
31
+ const map = loadSessionMap();
32
+ map[ppmId] = sdkId;
33
+ try {
34
+ const dir = resolve(homedir(), ".ppm");
35
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
36
+ writeFileSync(SESSION_MAP_FILE, JSON.stringify(map));
37
+ } catch {}
38
+ }
39
+
40
+ function getSdkSessionId(ppmId: string): string {
41
+ const map = loadSessionMap();
42
+ return map[ppmId] ?? ppmId;
43
+ }
16
44
 
17
45
  /**
18
46
  * Pending approval: canUseTool callback creates a promise,
@@ -225,12 +253,15 @@ export class ClaudeAgentSdkProvider implements AIProvider {
225
253
 
226
254
  try {
227
255
  const providerConfig = this.getProviderConfig();
256
+ // Resolve SDK's actual session ID for resume (may differ from PPM's UUID)
257
+ const sdkId = getSdkSessionId(sessionId);
258
+ console.log(`[sdk] query: session=${sessionId} sdkId=${sdkId} isFirst=${isFirstMessage} cwd=${meta.projectPath ?? "(none)"}`);
228
259
 
229
260
  const q = query({
230
261
  prompt: message,
231
262
  options: {
232
263
  sessionId: isFirstMessage ? sessionId : undefined,
233
- resume: isFirstMessage ? undefined : sessionId,
264
+ resume: isFirstMessage ? undefined : sdkId,
234
265
  cwd: meta.projectPath,
235
266
  // Use full Claude Code system prompt (coding guidelines, security, response style)
236
267
  systemPrompt: { type: "preset", preset: "claude_code" },
@@ -285,7 +316,9 @@ export class ClaudeAgentSdkProvider implements AIProvider {
285
316
  const initMsg = msg as any;
286
317
  // SDK may assign a different session_id than our UUID
287
318
  if (initMsg.session_id && initMsg.session_id !== sessionId) {
288
- // Update our mapping so resume works with SDK's actual ID
319
+ // Persist mapping so resume works after server restart
320
+ saveSessionMapping(sessionId, initMsg.session_id);
321
+ // Update our in-memory mapping
289
322
  const oldMeta = this.activeSessions.get(sessionId);
290
323
  if (oldMeta) {
291
324
  this.activeSessions.set(initMsg.session_id, { ...oldMeta, id: initMsg.session_id });
@@ -298,7 +331,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
298
331
  // the SDK has finished executing tools. Fetch tool_results from session history.
299
332
  if (pendingToolCount > 0 && (msg.type === "assistant" || (msg as any).type === "partial" || (msg as any).type === "stream_event")) {
300
333
  try {
301
- const sessionMsgs = await getSessionMessages(sessionId);
334
+ const sessionMsgs = await getSessionMessages(sdkId);
302
335
  // Find the last user message — it contains tool_result blocks
303
336
  const lastUserMsg = [...sessionMsgs].reverse().find(
304
337
  (m: any) => m.type === "user",
@@ -403,7 +436,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
403
436
  // Flush any remaining pending tool_results before finishing
404
437
  if (pendingToolCount > 0) {
405
438
  try {
406
- const sessionMsgs = await getSessionMessages(sessionId);
439
+ const sessionMsgs = await getSessionMessages(sdkId);
407
440
  const lastUserMsg = [...sessionMsgs].reverse().find(
408
441
  (m: any) => m.type === "user",
409
442
  );
@@ -437,14 +470,20 @@ export class ClaudeAgentSdkProvider implements AIProvider {
437
470
 
438
471
  // Surface non-success subtypes as errors so FE can display them
439
472
  if (subtype && subtype !== "success") {
473
+ // Extract error detail from SDK result if available
474
+ const sdkError = result.error ?? result.error_message ?? result.message ?? "";
475
+ const sdkDetail = typeof sdkError === "string" ? sdkError : JSON.stringify(sdkError);
440
476
  const errorMessages: Record<string, string> = {
441
477
  error_max_turns: "Agent reached maximum turn limit.",
442
478
  error_max_budget_usd: "Agent reached budget limit.",
443
479
  error_during_execution: "Agent encountered an error during execution.",
444
480
  };
481
+ const baseMsg = errorMessages[subtype] ?? `Agent stopped: ${subtype}`;
482
+ const fullMsg = sdkDetail ? `${baseMsg}\n${sdkDetail}` : baseMsg;
483
+ console.error(`[sdk] result error: subtype=${subtype} turns=${result.num_turns ?? 0} detail=${sdkDetail || "(none)"} raw=${JSON.stringify(result).slice(0, 500)}`);
445
484
  yield {
446
485
  type: "error",
447
- message: errorMessages[subtype] ?? `Agent stopped: ${subtype}`,
486
+ message: fullMsg,
448
487
  };
449
488
  }
450
489
 
@@ -493,7 +532,8 @@ export class ClaudeAgentSdkProvider implements AIProvider {
493
532
 
494
533
  async getMessages(sessionId: string): Promise<ChatMessage[]> {
495
534
  try {
496
- const messages = await getSessionMessages(sessionId);
535
+ const sdkId = getSdkSessionId(sessionId);
536
+ const messages = await getSessionMessages(sdkId);
497
537
  const parsed = messages.map((msg) => parseSessionMessage(msg));
498
538
 
499
539
  // Merge tool_result user messages into the preceding assistant message