@hienlh/ppm 0.23.23 → 0.23.24
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,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.23.24] - 2026-10-31
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **A chat could send one of your earlier messages again by itself, and act on it.** When a Claude process died, PPM restarted it and re-sent a message to get going again — but it had already thrown away the record of what the chat was doing, so it reached back to the message that *opened* the chat, sometimes hours old, and handed it to Claude as if you had just typed it. On 30 October two chats in different projects replayed their opening instruction within a second of each other after the same crash. A replayed instruction runs for real, so one that published, deployed or changed a database would have done it a second time. A restart now resumes a turn that was interrupted, re-sends your message only when nothing of it had reached Claude yet, and sends nothing at all when the chat was simply idle.
|
|
8
|
+
|
|
3
9
|
## [0.23.23] - 2026-10-30
|
|
4
10
|
|
|
5
11
|
### Fixed
|
|
@@ -73,4 +73,4 @@ This skill covers the `ppm` CLI, its HTTP API, and its config DB. It does **not*
|
|
|
73
73
|
- Third-party extensions (inspect via `ppm ext list`).
|
|
74
74
|
- The Claude Agent SDK internals (separate skill).
|
|
75
75
|
|
|
76
|
-
<!-- Generated for PPM v0.23.
|
|
76
|
+
<!-- Generated for PPM v0.23.24 at build time. Re-run `ppm export skill --install` to refresh. -->
|
|
@@ -491,4 +491,4 @@ _Base URL: `http://localhost:8080` (default; override via `ppm config set port <
|
|
|
491
491
|
- `ws://<host>/ws/terminal` — PTY terminal multiplexer
|
|
492
492
|
- `ws://<host>/ws/extensions` — extension host channel
|
|
493
493
|
|
|
494
|
-
<!-- Generated from src/server/routes/ for PPM v0.23.
|
|
494
|
+
<!-- Generated from src/server/routes/ for PPM v0.23.24 -->
|
package/package.json
CHANGED
|
@@ -227,6 +227,14 @@ interface StreamingSession {
|
|
|
227
227
|
lastUserContent: string;
|
|
228
228
|
/** Latest user message images — updated on follow-ups for accurate retry */
|
|
229
229
|
lastUserImages?: Array<{ data: string; mediaType: string }>;
|
|
230
|
+
/**
|
|
231
|
+
* A user message has been pushed whose turn has not finished.
|
|
232
|
+
*
|
|
233
|
+
* A crash retry may only re-send a prompt while this is true. Without it the retry re-sent
|
|
234
|
+
* whatever content it could find into a session that was simply idle, and an instruction the
|
|
235
|
+
* user gave hours earlier ran a second time as a real turn.
|
|
236
|
+
*/
|
|
237
|
+
awaitingReply?: boolean;
|
|
230
238
|
/** The sign-in this subprocess was started with — see `SubprocessCredential`. */
|
|
231
239
|
credential: SubprocessCredential;
|
|
232
240
|
}
|
|
@@ -838,6 +846,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
838
846
|
// Track latest message for retry paths (fixes stale firstMsg bug)
|
|
839
847
|
ss.lastUserContent = modelInput;
|
|
840
848
|
ss.lastUserImages = opts?.images;
|
|
849
|
+
ss.awaitingReply = true;
|
|
841
850
|
log.debug(`pushMessage: session=${sessionId} priority=${opts?.priority ?? 'next'}`);
|
|
842
851
|
}
|
|
843
852
|
|
|
@@ -1472,6 +1481,24 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
1472
1481
|
const MAX_CRASH_RETRIES = 1;
|
|
1473
1482
|
let crashRetryCount = 0;
|
|
1474
1483
|
|
|
1484
|
+
/**
|
|
1485
|
+
* What the crashed subprocess was doing, read off its session entry before that entry is
|
|
1486
|
+
* deleted. Undefined until a crash. `awaiting` false means no turn was in flight, and a
|
|
1487
|
+
* retry must then push nothing at all: the session is only being rebuilt so the next real
|
|
1488
|
+
* message has somewhere to go.
|
|
1489
|
+
*/
|
|
1490
|
+
let crashed: { content: string; images?: Array<{ data: string; mediaType: string }>; awaiting: boolean } | undefined;
|
|
1491
|
+
|
|
1492
|
+
// Once the turn has produced output (top-level assistant text/tool_use), the user
|
|
1493
|
+
// message + any tool_results are persisted to JSONL. Mid-turn retries must resume and
|
|
1494
|
+
// continue — NOT re-push the original message, which restarts the turn and re-displays
|
|
1495
|
+
// an already-answered AskUserQuestion.
|
|
1496
|
+
//
|
|
1497
|
+
// Declared outside `crashRetryLoop` on purpose: inside it, a crash re-entered the loop
|
|
1498
|
+
// body and cleared the very flag that keeps the retry from re-sending the prompt. It is
|
|
1499
|
+
// reset at the end of each turn instead.
|
|
1500
|
+
let turnProgressed = false;
|
|
1501
|
+
|
|
1475
1502
|
crashRetryLoop: for (;;) {
|
|
1476
1503
|
try {
|
|
1477
1504
|
// Streaming input: create message channel and persistent query.
|
|
@@ -1485,11 +1512,6 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
1485
1512
|
session_id: sessionId,
|
|
1486
1513
|
};
|
|
1487
1514
|
|
|
1488
|
-
// Once the turn has produced output (top-level assistant text/tool_use), the user
|
|
1489
|
-
// message + any tool_results are persisted to JSONL. Mid-turn retries must resume and
|
|
1490
|
-
// continue — NOT re-push the original message, which restarts the turn and re-displays
|
|
1491
|
-
// an already-answered AskUserQuestion. Persists across retryLoop iterations.
|
|
1492
|
-
let turnProgressed = false;
|
|
1493
1515
|
|
|
1494
1516
|
// Build a retry message for token-refresh / transient-error retries.
|
|
1495
1517
|
// Pre-turn (nothing persisted yet): re-push the LATEST user content so the SDK has a
|
|
@@ -1500,8 +1522,10 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
1500
1522
|
// Also returns the raw content/images for re-populating the new streaming session.
|
|
1501
1523
|
const buildRetryMsg = () => {
|
|
1502
1524
|
const ss = this.streamingSessions.get(sessionId);
|
|
1503
|
-
|
|
1504
|
-
|
|
1525
|
+
// `crashed` is the only source once the entry is gone: `?? modelInput` would otherwise
|
|
1526
|
+
// reach past every follow-up to the message that opened the stream, hours earlier.
|
|
1527
|
+
const content = ss?.lastUserContent ?? crashed?.content ?? modelInput;
|
|
1528
|
+
const images = ss?.lastUserImages ?? crashed?.images;
|
|
1505
1529
|
const retryContent = turnProgressed ? "Continue from where you left off." : content;
|
|
1506
1530
|
return {
|
|
1507
1531
|
msg: {
|
|
@@ -1522,9 +1546,14 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
1522
1546
|
: undefined;
|
|
1523
1547
|
const channel = spare ? undefined : createMessageChannel();
|
|
1524
1548
|
const initialCtrl = spare?.controller ?? channel!.controller;
|
|
1525
|
-
// On crash retry, use buildRetryMsg to get the latest user message (not the stale firstMsg)
|
|
1549
|
+
// On crash retry, use buildRetryMsg to get the latest user message (not the stale firstMsg).
|
|
1550
|
+
// A crash with no turn in flight pushes NOTHING: the subprocess is replaced so the next real
|
|
1551
|
+
// message has somewhere to go, and re-sending a finished instruction runs it a second time.
|
|
1526
1552
|
const initRetry = crashRetryCount > 0 ? buildRetryMsg() : null;
|
|
1527
|
-
|
|
1553
|
+
const resendOnCrash = crashRetryCount > 0 && crashed?.awaiting === true;
|
|
1554
|
+
if (crashRetryCount === 0) initialCtrl.push(firstMsg);
|
|
1555
|
+
else if (resendOnCrash && initRetry) initialCtrl.push(initRetry.msg);
|
|
1556
|
+
else log.info(`session=${sessionId} crash retry rebuilt the subprocess with no turn to resume — nothing re-sent`);
|
|
1528
1557
|
const initContent = initRetry?.lastUserContent ?? modelInput;
|
|
1529
1558
|
const initImages = initRetry?.lastUserImages ?? opts?.images;
|
|
1530
1559
|
|
|
@@ -1539,7 +1568,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
1539
1568
|
// The query's one start line: a warm CLI and the MCP servers belong to it.
|
|
1540
1569
|
log.info(`query: session=${sessionId} isFirst=${isFirstMessage} fork=${shouldFork} cwd=${effectiveCwd} platform=${process.platform} accountMode=${!!account} permissionMode=${permissionMode} isBypass=${isBypass}`
|
|
1541
1570
|
+ ` cli=${spare ? `warm ageMs=${spare.ageMs}` : "cold"}${crashRetryCount > 0 ? ` crashRetry=${crashRetryCount}` : ""} mcp=${Object.keys(mcpServers).join(",") || "none"}`);
|
|
1542
|
-
this.streamingSessions.set(sessionId, { meta, query: initialQuery, controller: initialCtrl, lastUserContent: initContent, lastUserImages: initImages, credential: queryCredential });
|
|
1571
|
+
this.streamingSessions.set(sessionId, { meta, query: initialQuery, controller: initialCtrl, lastUserContent: initContent, lastUserImages: initImages, awaitingReply: crashRetryCount === 0 || resendOnCrash, credential: queryCredential });
|
|
1543
1572
|
this.activeQueries.set(sessionId, initialQuery);
|
|
1544
1573
|
let eventSource: AsyncIterable<any> = initialQuery;
|
|
1545
1574
|
log.debug(`session=${sessionId} query() created, waiting for first SDK event...`);
|
|
@@ -2404,6 +2433,12 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
2404
2433
|
}
|
|
2405
2434
|
}
|
|
2406
2435
|
|
|
2436
|
+
// The turn is over: a crash from here on has nothing to resume, and must not re-send
|
|
2437
|
+
// this instruction. `turnProgressed` goes with it, so the next turn starts clean.
|
|
2438
|
+
const finished = this.streamingSessions.get(sessionId);
|
|
2439
|
+
if (finished) finished.awaitingReply = false;
|
|
2440
|
+
turnProgressed = false;
|
|
2441
|
+
|
|
2407
2442
|
// Streaming input: yield done for this turn, then continue for next turn
|
|
2408
2443
|
yieldedDone = true;
|
|
2409
2444
|
yield {
|
|
@@ -2451,10 +2486,17 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
2451
2486
|
const crashMsg = (crashErr as Error).message ?? String(crashErr);
|
|
2452
2487
|
const stderrInfo = stderrBuffer.trim() ? ` stderr: ${stderrBuffer.trim().slice(-500)}` : "";
|
|
2453
2488
|
|
|
2454
|
-
// Clean up crashed subprocess before retry or error
|
|
2489
|
+
// Clean up crashed subprocess before retry or error. Read what the turn was doing FIRST:
|
|
2490
|
+
// the retry below needs it, and deleting the entry is what used to send it back to the
|
|
2491
|
+
// message that opened the stream.
|
|
2455
2492
|
this.activeQueries.delete(sessionId);
|
|
2456
2493
|
const ss = this.streamingSessions.get(sessionId);
|
|
2457
|
-
if (ss) {
|
|
2494
|
+
if (ss) {
|
|
2495
|
+
crashed = { content: ss.lastUserContent, images: ss.lastUserImages, awaiting: ss.awaitingReply === true };
|
|
2496
|
+
ss.controller.done(); ss.query.close(); this.streamingSessions.delete(sessionId);
|
|
2497
|
+
} else {
|
|
2498
|
+
crashed = { content: modelInput, images: opts?.images, awaiting: false };
|
|
2499
|
+
}
|
|
2458
2500
|
log.debug(`session=${sessionId} streaming session ended`);
|
|
2459
2501
|
|
|
2460
2502
|
// Each case below logs itself: a cancel or a closed socket is not a failure.
|