@hienlh/ppm 0.9.76 → 0.9.78
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/AGENTS.md +80 -0
- package/CHANGELOG.md +12 -0
- package/dist/web/assets/{chat-tab-jKL8IW3Y.js → chat-tab-B3gpx-qv.js} +2 -2
- package/dist/web/assets/{code-editor-CR8ddAzL.js → code-editor-kyaXcsZW.js} +1 -1
- package/dist/web/assets/{database-viewer-B9mqA0gn.js → database-viewer-DmAux3OF.js} +1 -1
- package/dist/web/assets/{diff-viewer-BHJnCxdy.js → diff-viewer-Cikon1YK.js} +1 -1
- package/dist/web/assets/{extension-webview-qOmDNW1k.js → extension-webview-DVvC7SQ-.js} +1 -1
- package/dist/web/assets/{git-graph-C_JG3CzH.js → git-graph-Bon2J1_A.js} +1 -1
- package/dist/web/assets/{index-_YxJ4vi1.js → index-Buc4QA5O.js} +3 -3
- package/dist/web/assets/keybindings-store-CT_EvCrb.js +1 -0
- package/dist/web/assets/{markdown-renderer-_rUM4Qox.js → markdown-renderer-ttL1fRGG.js} +1 -1
- package/dist/web/assets/{port-forwarding-tab-BR3UIrwb.js → port-forwarding-tab-Bljq2XEH.js} +1 -1
- package/dist/web/assets/{postgres-viewer-B9lLK97W.js → postgres-viewer-CqburCkJ.js} +1 -1
- package/dist/web/assets/{settings-tab-BAQdizup.js → settings-tab-CQVn8u_D.js} +1 -1
- package/dist/web/assets/{sql-query-editor-DZK6zX5t.js → sql-query-editor-DP6Kh2R8.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-vLFjca8E.js → sqlite-viewer-CrqzbhyF.js} +1 -1
- package/dist/web/assets/{terminal-tab-B1mmTE_I.js → terminal-tab-BmBB838x.js} +1 -1
- package/dist/web/assets/{use-monaco-theme-B21s-870.js → use-monaco-theme-ZmSrfclJ.js} +1 -1
- package/dist/web/index.html +1 -1
- package/dist/web/sw.js +1 -1
- package/output/pdf/ppm-app-summary.pdf +80 -0
- package/package.json +1 -1
- package/src/cli/commands/restart.ts +5 -3
- package/src/providers/claude-agent-sdk.ts +25 -26
- package/src/server/index.ts +5 -2
- package/src/server/ws/chat.ts +2 -1
- package/src/services/cloud-ws.service.ts +6 -0
- package/src/services/supervisor-state.ts +9 -1
- package/src/services/supervisor.ts +17 -4
- package/src/services/tunnel.service.ts +19 -7
- package/src/types/config.ts +1 -1
- package/src/web/components/chat/message-list.tsx +12 -4
- package/dist/web/assets/keybindings-store-A0Ag0ZYI.js +0 -1
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
%PDF-1.4
|
|
2
|
+
%���� ReportLab Generated PDF document (opensource)
|
|
3
|
+
1 0 obj
|
|
4
|
+
<<
|
|
5
|
+
/F1 2 0 R /F2 3 0 R /F3 4 0 R
|
|
6
|
+
>>
|
|
7
|
+
endobj
|
|
8
|
+
2 0 obj
|
|
9
|
+
<<
|
|
10
|
+
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
|
|
11
|
+
>>
|
|
12
|
+
endobj
|
|
13
|
+
3 0 obj
|
|
14
|
+
<<
|
|
15
|
+
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
|
|
16
|
+
>>
|
|
17
|
+
endobj
|
|
18
|
+
4 0 obj
|
|
19
|
+
<<
|
|
20
|
+
/BaseFont /ZapfDingbats /Name /F3 /Subtype /Type1 /Type /Font
|
|
21
|
+
>>
|
|
22
|
+
endobj
|
|
23
|
+
5 0 obj
|
|
24
|
+
<<
|
|
25
|
+
/Contents 9 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
|
|
26
|
+
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
|
27
|
+
>> /Rotate 0 /Trans <<
|
|
28
|
+
|
|
29
|
+
>>
|
|
30
|
+
/Type /Page
|
|
31
|
+
>>
|
|
32
|
+
endobj
|
|
33
|
+
6 0 obj
|
|
34
|
+
<<
|
|
35
|
+
/PageMode /UseNone /Pages 8 0 R /Type /Catalog
|
|
36
|
+
>>
|
|
37
|
+
endobj
|
|
38
|
+
7 0 obj
|
|
39
|
+
<<
|
|
40
|
+
/Author (Codex) /CreationDate (D:20260410093008+07'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260410093008+07'00') /Producer (ReportLab PDF Library - \(opensource\))
|
|
41
|
+
/Subject (\(unspecified\)) /Title (PPM App Summary) /Trapped /False
|
|
42
|
+
>>
|
|
43
|
+
endobj
|
|
44
|
+
8 0 obj
|
|
45
|
+
<<
|
|
46
|
+
/Count 1 /Kids [ 5 0 R ] /Type /Pages
|
|
47
|
+
>>
|
|
48
|
+
endobj
|
|
49
|
+
9 0 obj
|
|
50
|
+
<<
|
|
51
|
+
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2835
|
|
52
|
+
>>
|
|
53
|
+
stream
|
|
54
|
+
Gau`V>Bedl&q9SYW2EcR$UQe6j?1DdPO\'OiYZ9JAklYNWA1\)[F-Wmr3F(cX'-=T_N5>SEJWY*]";/'=-p*o=CC.3df5%N^r7"Bmjt\ZR*NNg%2YnV_q"DGl^:b6F?RM\K4g7)d28e>'uM$e^?pM\#,W,o_6Cq^B9#:7S`+TiCVbR9MQ>K>"gkjXe(VL)Rh)]1H4SJ3aj)8]F_mJ^g6V.%Y%*%AkW`IbQK=)r*F!\ub5uP'ZF$Tl)'@r+NW[IG:h?(Nq$;1,j,t->K"2RE8=g4Ib;eKaa:E+2*kV'Fg0!:d,<#TIn4b:7l*kRmH=IgfELjAnK09PM)[=h3a^P0!Y"13/Wk)ApFPQ8'VRBacVImp!2eI:/n\IZNr#_]RDQ2;k[eG'`$Fn*jl<,6WE5iCU>$K`ngG(6_eM8X&U.tO)LmBtNgDZ%Rg5Ur2JjF='g6HgdpsQd:igto/quZZ.O=LJV=GG&A0<ZL!^d.F#5$/)E]_*E_ZT-orGq;tCm'O7]1Ut03)Wp]jBsdrbauCh8A8kCX#2NQ5dTN)["nfr["gs5GE"Q`,I2io?i)*bCG6O=IS?W3!8:0[?qm_/CLdYU0=M\TC[Ed6*`dG`O%V&WEDlt6/=Q9hY@OB&dEB0]N&ZgY5:cSooYn-8b=2Fjr@s?G,n?\Vm"Q`tth?g`*)g:-VNTWOLk&,t,E.l4iORH9V@u`O*N1GS6M1CC8Gp;H.=Q-\<"sGR^,0gi3M$R84U0P%&1s.>7'aXJ_.2A&-BPa^LcPIeca<@$!]]d90/pe13%u&(6\>:/VM%J92'\_YO&Ym7BPU-X!cVih%ElFFT(CdY:.[PPY0iu97m1kl,XkH:Bm<]S*:HnJA`Xh4_r;h9ppq@<J7qS#?lJ*$%h"Z#4fuuV(X9"coC`5W`SaAGNMO)OkNVT-b^Yb7%Y4J/2I3@tq1dU#R'OWIFRKuFSma*QHA&%hMTL/:W22Q6hY3U<YmirW8InJ@g.#\%W,#p:M@qGKn.)*TH$N+,"#`0ud:J+<r(mgqp;\c,q+YhTfo/TeGXl"^hPFP(8/CRa]iS2I]#W/H!qnTOi3jlX(I')qf!-l[i1+;.HYDWQr*!Kf.G^ee]M$P2JJJeM^#(31#`dO,`YUm1QhG!sh8gfr[QC5G-H"?ZGODBfI#.[<VmZS879rVqqQUI7;b)ZR]q7*V$/g]17#s,_Zfm<%>F)k\i'J*Kc!j>m,,tonhjC9p$p1*C=(%+LEFapp6m_b:]$)PLaJpINq8!oMQ+fl#OPc>(#e^q%`a?.N7"oNZ%7t&7[E7%"p2"!5aZlW,a+_MZgp-WPR?`q,M`_Q]u"j+dcWjbI[28Tmd6Ghg4mr?1!2^hAg,fb+bBpS9-\$`M0h2p4Y:MWE$oP;QG!V((j/8=lC5QgVSd&\djW)R]VG1';L@MCIUH<q?W2kSu8Q^6f$U/%27U(HQS/N@)foGln\9Cg0:`3@NT&^TL!D0W&$km_4]&p[$&@3S>1oJl]\gm:t,0pRBTF],u2($VsK,Y&?sFM-P1]OOkZp2fjift`f+cF+2NLkb>g'H,&kkT".k-4or2r.Q;g>OXDCKO4,mDAMOe)%Nou)n+L=/lZ`Y:Pie$6Z=PT,.sqH(m-UR-WNXa!D*Hjho_4](@-ut*KEiBpT_4BeDf$(_m(nj9;43_EYQR%O".rC,H]Y7Bh4_dkhu"OIM8-r<+3Yuack-TLG:R[bpZPX1),j;+[*;9PA(E76ljN&Jss,b$i;UlV(UGf(O_rC^6(M/V"CA)YuX<7Rh"afY\tt:W,'s-!q!cdW^JjHVb]Td]7JENUQGK)ne4s_ik.9bd,?Zp($S&@#Lud%XCEj%W'T#Z<?395V7NAs\iU"Jg48PYE"d)qC@;mp&e]^uRpb!Z+;rd6$V`gk<"EU&HqmuC7R&*$+(;1sJ<JNnN4-5?<s$)]M0RB=guB<;dlE9NSarik"ZH(]'^)U0DE;:/\"^\<Y[#$!KU!.)alS#]%Q["_i3JoQFtcZTdU'uB9?Ra*is_\=H?d^G.fNGm8KS^]1nb'0C4+98SE[\k+\m1.(a?H\p^VPW"NBLcGs'$F'c^:o:W@.]J^^iLM:HF@UZWV30F0=[k,i-N8NJrN]GY/Dc5Ld_M0l;$9rcLF)e&R_Xcg7WF2S$47\6MFr%XpKG<0@7LbCKlmkVR:QS3R-J).QOeaXV"&CPE()!_jOVZh^A+$.rUrT<Z[;j/;H%]=Y?='tO#e""jK31L>c>rVc/D.`ELIFOGiI,1pqf:H`,f3,lLP(,&nKoU93ob4>l&jmS\QiWO97hNkLn.Aq9rA[rPr79^%H.NZ.&-[rM;J-C)!se(6?Dt3a14Kkkn&b_Skm9_s=-,1`MLjF\_<bWaObW)<70e=,b7"<nKK?<8c!Ch52"45sg6"%gLd)0ZFkhf].Gp>^D`KoNY[msX8EbXK+>\`bC8Z!qO:29R]Fn'^cjkVLjB:`HYR5O]Oe72>hDU8;@YJ+H%g2KF8#+i4:NL.=&[NV!-.([H.d<[RX*\T]Wg,Z^QRX%HQKNX@[=0I&eMN8hrK:'1U,CZg4Tss#X_t%`A^kmr5?A?fe$lAF:Z*amc/FTp+I4&cm`5i2)iTn@1'>:,cs&_doEaM<!^,%7hn6`#@lc?mkV]eBi<-)Zj'IT/[=/V<4@C7;nNB%T)!$gTZ0NV;)PnKT_:&EJ(.:2M3b$MFmV%j:1Ue=&\uVgmcln55d=rURe+V#!np5/@e5EE-NV^>n]ANDprnO+`?9g[]L@:)*1WA8DAHiBWhk-UOY;X,COFeE2mFN*dR-8<tqmj91RCP2X):;^l8QiIYLnFd<hi490TC`N:rX\QP\5!~>endstream
|
|
55
|
+
endobj
|
|
56
|
+
xref
|
|
57
|
+
0 10
|
|
58
|
+
0000000000 65535 f
|
|
59
|
+
0000000061 00000 n
|
|
60
|
+
0000000112 00000 n
|
|
61
|
+
0000000219 00000 n
|
|
62
|
+
0000000331 00000 n
|
|
63
|
+
0000000414 00000 n
|
|
64
|
+
0000000607 00000 n
|
|
65
|
+
0000000675 00000 n
|
|
66
|
+
0000000949 00000 n
|
|
67
|
+
0000001008 00000 n
|
|
68
|
+
trailer
|
|
69
|
+
<<
|
|
70
|
+
/ID
|
|
71
|
+
[<ed781e36a58178ba295c4b649581811a><ed781e36a58178ba295c4b649581811a>]
|
|
72
|
+
% ReportLab generated PDF document -- digest (opensource)
|
|
73
|
+
|
|
74
|
+
/Info 7 0 R
|
|
75
|
+
/Root 6 0 R
|
|
76
|
+
/Size 10
|
|
77
|
+
>>
|
|
78
|
+
startxref
|
|
79
|
+
3934
|
|
80
|
+
%%EOF
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { readFileSync, writeFileSync, existsSync, openSync, unlinkSync } from "node:fs";
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync, openSync, unlinkSync, renameSync } from "node:fs";
|
|
4
4
|
|
|
5
5
|
const PPM_DIR = resolve(homedir(), ".ppm");
|
|
6
6
|
const STATUS_FILE = resolve(PPM_DIR, "status.json");
|
|
@@ -237,12 +237,14 @@ async function main() {
|
|
|
237
237
|
childPid = child.pid;
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
// Update status.json with new PID, keep tunnel info
|
|
240
|
+
// Update status.json with new PID, keep tunnel info (atomic write to avoid cross-process races)
|
|
241
241
|
try {
|
|
242
242
|
const status = JSON.parse(readFileSync(P.statusFile, "utf-8"));
|
|
243
243
|
status.pid = childPid;
|
|
244
244
|
status.serverScript = P.serverScript;
|
|
245
|
-
|
|
245
|
+
const tmp = P.statusFile + ".tmp." + process.pid;
|
|
246
|
+
writeFileSync(tmp, JSON.stringify(status));
|
|
247
|
+
renameSync(tmp, P.statusFile);
|
|
246
248
|
writeFileSync(P.pidFile, String(childPid));
|
|
247
249
|
} catch {}
|
|
248
250
|
|
|
@@ -723,13 +723,17 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
723
723
|
const mcpServers = mcpConfigService.list();
|
|
724
724
|
const hasMcp = Object.keys(mcpServers).length > 0;
|
|
725
725
|
|
|
726
|
-
// Buffer subprocess stderr for crash diagnostics
|
|
726
|
+
// Buffer subprocess stderr for crash diagnostics + log in real-time
|
|
727
727
|
let stderrBuffer = "";
|
|
728
728
|
const stderrCallback = (chunk: string) => {
|
|
729
729
|
stderrBuffer += chunk;
|
|
730
|
-
// Keep only last 2KB to avoid unbounded growth
|
|
731
730
|
if (stderrBuffer.length > 2048) stderrBuffer = stderrBuffer.slice(-2048);
|
|
731
|
+
const trimmed = chunk.trim();
|
|
732
|
+
if (trimmed) console.log(`[sdk] session=${sessionId} stderr: ${trimmed.slice(0, 500)}`);
|
|
732
733
|
};
|
|
734
|
+
if (hasMcp) {
|
|
735
|
+
console.log(`[sdk] session=${sessionId} mcpServers: ${Object.keys(mcpServers).join(", ")}`);
|
|
736
|
+
}
|
|
733
737
|
|
|
734
738
|
const queryOptions: Record<string, any> = {
|
|
735
739
|
// On Windows, child_process.spawn("bun") fails with ENOENT — force node
|
|
@@ -786,6 +790,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
786
790
|
this.streamingSessions.set(sessionId, { meta, query: initialQuery, controller: initialCtrl });
|
|
787
791
|
this.activeQueries.set(sessionId, initialQuery);
|
|
788
792
|
let eventSource: AsyncIterable<any> = initialQuery;
|
|
793
|
+
console.log(`[sdk] session=${sessionId} query() created, waiting for first SDK event...`);
|
|
789
794
|
|
|
790
795
|
// Helper: close the CURRENT streaming session (not stale closure refs).
|
|
791
796
|
// All retry paths must use this instead of closing captured variables directly.
|
|
@@ -920,10 +925,9 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
920
925
|
const retryEnv = this.buildQueryEnv(meta.projectPath, refreshedAccount);
|
|
921
926
|
closeCurrentStream();
|
|
922
927
|
const { generator: earlyAuthGen, controller: earlyAuthCtrl } = createMessageChannel();
|
|
923
|
-
const
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
const retryOpts = { ...queryOptions, sessionId: undefined, resume: canResume ? currentSdkId : undefined, env: retryEnv };
|
|
928
|
+
const hasHistory = (this.messageCount.get(sessionId) ?? 0) > 0;
|
|
929
|
+
if (!hasHistory) earlyAuthCtrl.push(firstMsg);
|
|
930
|
+
const retryOpts = { ...queryOptions, sessionId: undefined, resume: hasHistory ? getSdkSessionId(sessionId) : undefined, env: retryEnv };
|
|
927
931
|
const rq = query({
|
|
928
932
|
prompt: earlyAuthGen,
|
|
929
933
|
options: { ...retryOpts, ...(permissionHooks && { hooks: permissionHooks }), canUseTool } as any,
|
|
@@ -947,10 +951,9 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
947
951
|
const switchEnv = this.buildQueryEnv(meta.projectPath, nextAcc);
|
|
948
952
|
closeCurrentStream();
|
|
949
953
|
const { generator: switchGen, controller: switchCtrl } = createMessageChannel();
|
|
950
|
-
const
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
const retryOpts = { ...queryOptions, sessionId: undefined, resume: canResume ? currentSdkId : undefined, env: switchEnv };
|
|
954
|
+
const hasHistory = (this.messageCount.get(sessionId) ?? 0) > 0;
|
|
955
|
+
if (!hasHistory) switchCtrl.push(firstMsg);
|
|
956
|
+
const retryOpts = { ...queryOptions, sessionId: undefined, resume: hasHistory ? getSdkSessionId(sessionId) : undefined, env: switchEnv };
|
|
954
957
|
const rq = query({
|
|
955
958
|
prompt: switchGen,
|
|
956
959
|
options: { ...retryOpts, ...(permissionHooks && { hooks: permissionHooks }), canUseTool } as any,
|
|
@@ -1099,10 +1102,9 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
1099
1102
|
// resume a non-existent session, causing the SDK to hang forever.
|
|
1100
1103
|
closeCurrentStream();
|
|
1101
1104
|
const { generator: authRetryGen, controller: authRetryCtrl } = createMessageChannel();
|
|
1102
|
-
const
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
const retryOpts = { ...queryOptions, sessionId: undefined, resume: canResume ? currentSdkId : undefined, env: retryEnv };
|
|
1105
|
+
const hasHistory = (this.messageCount.get(sessionId) ?? 0) > 0;
|
|
1106
|
+
if (!hasHistory) authRetryCtrl.push(firstMsg);
|
|
1107
|
+
const retryOpts = { ...queryOptions, sessionId: undefined, resume: hasHistory ? getSdkSessionId(sessionId) : undefined, env: retryEnv };
|
|
1106
1108
|
const rq = query({
|
|
1107
1109
|
prompt: authRetryGen,
|
|
1108
1110
|
options: { ...retryOpts, ...(permissionHooks && { hooks: permissionHooks }), canUseTool } as any,
|
|
@@ -1153,10 +1155,9 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
1153
1155
|
closeCurrentStream();
|
|
1154
1156
|
const rlRetryEnv = this.buildQueryEnv(meta.projectPath, account);
|
|
1155
1157
|
const { generator: rlRetryGen, controller: rlRetryCtrl } = createMessageChannel();
|
|
1156
|
-
const
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
const retryOpts = { ...queryOptions, sessionId: undefined, resume: rlCanResume ? rlCurrentSdkId : undefined, env: rlRetryEnv };
|
|
1158
|
+
const rlHasHistory = (this.messageCount.get(sessionId) ?? 0) > 0;
|
|
1159
|
+
if (!rlHasHistory) rlRetryCtrl.push(firstMsg);
|
|
1160
|
+
const retryOpts = { ...queryOptions, sessionId: undefined, resume: rlHasHistory ? getSdkSessionId(sessionId) : undefined, env: rlRetryEnv };
|
|
1160
1161
|
const rq = query({
|
|
1161
1162
|
prompt: rlRetryGen,
|
|
1162
1163
|
options: { ...retryOpts, ...(permissionHooks && { hooks: permissionHooks }), canUseTool } as any,
|
|
@@ -1251,10 +1252,9 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
1251
1252
|
closeCurrentStream();
|
|
1252
1253
|
const rlRetryEnv = this.buildQueryEnv(meta.projectPath, account);
|
|
1253
1254
|
const { generator: rlRetryGen, controller: rlRetryCtrl } = createMessageChannel();
|
|
1254
|
-
const
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
const retryOpts = { ...queryOptions, sessionId: undefined, resume: rlCanResume2 ? rlCurrentSdkId2 : undefined, env: rlRetryEnv };
|
|
1255
|
+
const rlHasHistory2 = (this.messageCount.get(sessionId) ?? 0) > 0;
|
|
1256
|
+
if (!rlHasHistory2) rlRetryCtrl.push(firstMsg);
|
|
1257
|
+
const retryOpts = { ...queryOptions, sessionId: undefined, resume: rlHasHistory2 ? getSdkSessionId(sessionId) : undefined, env: rlRetryEnv };
|
|
1258
1258
|
const rq = query({
|
|
1259
1259
|
prompt: rlRetryGen,
|
|
1260
1260
|
options: { ...retryOpts, ...(permissionHooks && { hooks: permissionHooks }), canUseTool } as any,
|
|
@@ -1283,10 +1283,9 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
1283
1283
|
closeCurrentStream();
|
|
1284
1284
|
const retryEnv = this.buildQueryEnv(meta.projectPath, refreshedAccount);
|
|
1285
1285
|
const { generator: authRetryGen2, controller: authRetryCtrl2 } = createMessageChannel();
|
|
1286
|
-
const
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
const retryOpts = { ...queryOptions, sessionId: undefined, resume: authCanResume2 ? authCurrentSdkId2 : undefined, env: retryEnv };
|
|
1286
|
+
const authHasHistory2 = (this.messageCount.get(sessionId) ?? 0) > 0;
|
|
1287
|
+
if (!authHasHistory2) authRetryCtrl2.push(firstMsg);
|
|
1288
|
+
const retryOpts = { ...queryOptions, sessionId: undefined, resume: authHasHistory2 ? getSdkSessionId(sessionId) : undefined, env: retryEnv };
|
|
1290
1289
|
const rq = query({
|
|
1291
1290
|
prompt: authRetryGen2,
|
|
1292
1291
|
options: { ...retryOpts, ...(permissionHooks && { hooks: permissionHooks }), canUseTool } as any,
|
package/src/server/index.ts
CHANGED
|
@@ -491,12 +491,15 @@ if (process.argv.includes("__serve__")) {
|
|
|
491
491
|
try {
|
|
492
492
|
const { resolve: r } = await import("node:path");
|
|
493
493
|
const { homedir: h } = await import("node:os");
|
|
494
|
-
const { readFileSync: rf, writeFileSync: wf } = await import("node:fs");
|
|
494
|
+
const { readFileSync: rf, writeFileSync: wf, renameSync: rn } = await import("node:fs");
|
|
495
495
|
const statusFile = r(h(), ".ppm", "status.json");
|
|
496
496
|
const status = JSON.parse(rf(statusFile, "utf-8"));
|
|
497
497
|
// Write running server version — source of truth for heartbeat
|
|
498
498
|
status.serverVersion = VERSION;
|
|
499
|
-
|
|
499
|
+
// Atomic write: tmp + rename to avoid cross-process partial-read races
|
|
500
|
+
const tmp = statusFile + ".tmp." + process.pid;
|
|
501
|
+
wf(tmp, JSON.stringify(status));
|
|
502
|
+
rn(tmp, statusFile);
|
|
500
503
|
if (status.shareUrl) {
|
|
501
504
|
const { tunnelService } = await import("../services/tunnel.service.ts");
|
|
502
505
|
if (status.tunnelPid) tunnelService.setExternalPid(status.tunnelPid);
|
package/src/server/ws/chat.ts
CHANGED
|
@@ -241,7 +241,8 @@ async function startSessionConsumer(sessionId: string, providerId: string, conte
|
|
|
241
241
|
}
|
|
242
242
|
|
|
243
243
|
// First content event — stop heartbeat, transition phase
|
|
244
|
-
|
|
244
|
+
// status_update is PPM's pre-flight account selection — not actual SDK content
|
|
245
|
+
const isMetadataEvent = evType === "account_info" || evType === "account_retry" || evType === "streaming_status" || evType === "status_update";
|
|
245
246
|
if (!firstEventReceived && !isMetadataEvent) {
|
|
246
247
|
firstEventReceived = true;
|
|
247
248
|
const waitMs = Date.now() - startTime;
|
|
@@ -204,6 +204,12 @@ function doConnect(): void {
|
|
|
204
204
|
reconnecting = false;
|
|
205
205
|
ws = null;
|
|
206
206
|
if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; }
|
|
207
|
+
// 4010 = server replaced this connection (another is already active) or reconnecting
|
|
208
|
+
// too fast — do NOT reconnect to avoid feedback loop
|
|
209
|
+
if (event.code === 4010) {
|
|
210
|
+
log("INFO", "Cloud WS closed with 4010 (replaced/throttled), not reconnecting");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
207
213
|
if (shouldConnect) scheduleReconnect("onclose");
|
|
208
214
|
};
|
|
209
215
|
|
|
@@ -37,6 +37,14 @@ export function triggerResume(): void {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
// ─── Status file helpers ───────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
/** Atomic write: write to tmp file then rename (prevents partial-read races across processes) */
|
|
42
|
+
function atomicWriteJson(filePath: string, data: unknown) {
|
|
43
|
+
const tmp = filePath + ".tmp." + process.pid;
|
|
44
|
+
writeFileSync(tmp, JSON.stringify(data));
|
|
45
|
+
renameSync(tmp, filePath);
|
|
46
|
+
}
|
|
47
|
+
|
|
40
48
|
export function readStatus(): Record<string, unknown> {
|
|
41
49
|
try {
|
|
42
50
|
if (existsSync(STATUS_FILE)) return JSON.parse(readFileSync(STATUS_FILE, "utf-8"));
|
|
@@ -47,7 +55,7 @@ export function readStatus(): Record<string, unknown> {
|
|
|
47
55
|
export function updateStatus(patch: Record<string, unknown>) {
|
|
48
56
|
try {
|
|
49
57
|
const data = { ...readStatus(), ...patch };
|
|
50
|
-
|
|
58
|
+
atomicWriteJson(STATUS_FILE, data);
|
|
51
59
|
} catch {}
|
|
52
60
|
}
|
|
53
61
|
|
|
@@ -27,8 +27,8 @@ const BACKOFF_MAX_MS = 60_000;
|
|
|
27
27
|
const STABLE_WINDOW_MS = 300_000; // 5min stable → reset restart counter
|
|
28
28
|
const SERVER_HEALTH_INTERVAL_MS = 30_000;
|
|
29
29
|
const SERVER_HEALTH_FAIL_THRESHOLD = 3;
|
|
30
|
-
const TUNNEL_PROBE_INTERVAL_MS =
|
|
31
|
-
const TUNNEL_PROBE_FAIL_THRESHOLD =
|
|
30
|
+
const TUNNEL_PROBE_INTERVAL_MS = 30_000; // 30s — adopted tunnels have no `exited` promise
|
|
31
|
+
const TUNNEL_PROBE_FAIL_THRESHOLD = 3; // 3 HTTP failures before regenerating (PID check is instant)
|
|
32
32
|
const TUNNEL_URL_REGEX = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
|
|
33
33
|
const UPGRADE_CHECK_INTERVAL_MS = 900_000; // 15min
|
|
34
34
|
const UPGRADE_SKIP_INITIAL_MS = 300_000; // 5min delay before first check
|
|
@@ -415,12 +415,25 @@ async function selfReplace(): Promise<{ success: boolean; error?: string }> {
|
|
|
415
415
|
// Set restarting flag so server child's stopTunnel() skips killing the tunnel
|
|
416
416
|
try { writeFileSync(RESTARTING_FLAG, ""); } catch {}
|
|
417
417
|
|
|
418
|
+
// Clear probe timer FIRST to prevent race between flush check and queued callback
|
|
419
|
+
if (tunnelProbeTimer) { clearInterval(tunnelProbeTimer); tunnelProbeTimer = null; }
|
|
420
|
+
|
|
421
|
+
// Final tunnel liveness check before handing off to new supervisor —
|
|
422
|
+
// if the adopted tunnel died since the last probe, clear status so the
|
|
423
|
+
// new supervisor spawns fresh instead of discovering ESRCH.
|
|
424
|
+
if (adoptedTunnelPid && !tunnelChild) {
|
|
425
|
+
try { process.kill(adoptedTunnelPid, 0); } catch {
|
|
426
|
+
log("WARN", "Pre-upgrade: adopted tunnel dead, clearing for new supervisor to spawn fresh");
|
|
427
|
+
adoptedTunnelPid = null;
|
|
428
|
+
tunnelUrl = null;
|
|
429
|
+
updateStatus({ shareUrl: null, tunnelPid: null });
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
418
433
|
// Kill server child to free the port; keep tunnel alive for domain continuity
|
|
419
434
|
log("INFO", "Stopping server before spawning new supervisor (tunnel kept alive)");
|
|
420
435
|
if (serverChild) { try { serverChild.kill(); } catch {} serverChild = null; }
|
|
421
|
-
// Clear health timers so we don't try to respawn killed children
|
|
422
436
|
if (healthTimer) { clearInterval(healthTimer); healthTimer = null; }
|
|
423
|
-
if (tunnelProbeTimer) { clearInterval(tunnelProbeTimer); tunnelProbeTimer = null; }
|
|
424
437
|
// Brief wait for port release
|
|
425
438
|
await Bun.sleep(500);
|
|
426
439
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Subprocess } from "bun";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import { existsSync, unlinkSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { existsSync, unlinkSync, readFileSync, writeFileSync, renameSync } from "node:fs";
|
|
5
5
|
import { ensureCloudflared } from "./cloudflared.service.ts";
|
|
6
6
|
|
|
7
7
|
const TUNNEL_URL_REGEX = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
|
|
@@ -19,6 +19,8 @@ class TunnelService {
|
|
|
19
19
|
private externalPid: number | null = null;
|
|
20
20
|
private url: string | null = null;
|
|
21
21
|
private cleanupHandler: (() => void) | null = null;
|
|
22
|
+
/** True when tunnel is owned by the supervisor (adopted), not by this server process */
|
|
23
|
+
private supervisorManaged = false;
|
|
22
24
|
|
|
23
25
|
/** Spawn cloudflared Quick Tunnel and return public URL */
|
|
24
26
|
async startTunnel(port: number): Promise<string> {
|
|
@@ -81,12 +83,13 @@ class TunnelService {
|
|
|
81
83
|
});
|
|
82
84
|
|
|
83
85
|
this.url = url;
|
|
86
|
+
this.supervisorManaged = false; // this process owns the tunnel now
|
|
84
87
|
this.persistToStatusFile();
|
|
85
88
|
this.syncToCloud();
|
|
86
89
|
return url;
|
|
87
90
|
}
|
|
88
91
|
|
|
89
|
-
/** Kill the cloudflared child process (skipped during restart) */
|
|
92
|
+
/** Kill the cloudflared child process (skipped during restart or when supervisor-managed) */
|
|
90
93
|
stopTunnel(): void {
|
|
91
94
|
if (this.cleanupHandler) {
|
|
92
95
|
process.off("SIGINT", this.cleanupHandler);
|
|
@@ -101,16 +104,22 @@ class TunnelService {
|
|
|
101
104
|
this.url = null;
|
|
102
105
|
return;
|
|
103
106
|
}
|
|
107
|
+
// Only kill tunnels this process spawned; externally-managed tunnels (set via
|
|
108
|
+
// setExternalPid) belong to the supervisor — killing them causes silent URL resets
|
|
104
109
|
if (this.childProcess) {
|
|
105
110
|
try { this.childProcess.kill(); } catch {}
|
|
106
111
|
this.childProcess = null;
|
|
107
112
|
}
|
|
108
113
|
if (this.externalPid) {
|
|
109
|
-
|
|
114
|
+
// Don't kill — supervisor owns this process and monitors it via probe
|
|
110
115
|
this.externalPid = null;
|
|
111
116
|
}
|
|
112
117
|
this.url = null;
|
|
113
|
-
|
|
118
|
+
// Don't persist nulls to status.json when tunnel is supervisor-managed;
|
|
119
|
+
// the supervisor is the source of truth for tunnel state
|
|
120
|
+
if (!this.supervisorManaged) {
|
|
121
|
+
this.persistToStatusFile();
|
|
122
|
+
}
|
|
114
123
|
this.stopCloudSync();
|
|
115
124
|
}
|
|
116
125
|
|
|
@@ -131,12 +140,13 @@ class TunnelService {
|
|
|
131
140
|
this.syncToCloud();
|
|
132
141
|
}
|
|
133
142
|
|
|
134
|
-
/** Adopt an externally-started tunnel by PID (
|
|
143
|
+
/** Adopt an externally-started tunnel by PID (supervisor-managed, don't kill on exit) */
|
|
135
144
|
setExternalPid(pid: number): void {
|
|
136
145
|
this.externalPid = pid;
|
|
146
|
+
this.supervisorManaged = true;
|
|
137
147
|
}
|
|
138
148
|
|
|
139
|
-
/** Persist shareUrl + tunnelPid to status.json (
|
|
149
|
+
/** Persist shareUrl + tunnelPid to status.json (atomic write to avoid cross-process races) */
|
|
140
150
|
private persistToStatusFile(): void {
|
|
141
151
|
const statusFile = resolve(homedir(), ".ppm", "status.json");
|
|
142
152
|
if (!existsSync(statusFile)) return;
|
|
@@ -144,7 +154,9 @@ class TunnelService {
|
|
|
144
154
|
const data = JSON.parse(readFileSync(statusFile, "utf-8"));
|
|
145
155
|
data.shareUrl = this.url;
|
|
146
156
|
data.tunnelPid = this.getTunnelPid() ?? null;
|
|
147
|
-
|
|
157
|
+
const tmp = statusFile + ".tmp." + process.pid;
|
|
158
|
+
writeFileSync(tmp, JSON.stringify(data));
|
|
159
|
+
renameSync(tmp, statusFile);
|
|
148
160
|
} catch {}
|
|
149
161
|
}
|
|
150
162
|
|
package/src/types/config.ts
CHANGED
|
@@ -719,12 +719,20 @@ function InterleavedEvents({ events, isStreaming, projectName }: { events: ChatE
|
|
|
719
719
|
/** Collapsible thinking block — shows Claude's reasoning, collapsed by default when done */
|
|
720
720
|
function ThinkingBlock({ content, isStreaming }: { content: string; isStreaming: boolean }) {
|
|
721
721
|
const [expanded, setExpanded] = useState(isStreaming);
|
|
722
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
722
723
|
|
|
723
724
|
// Auto-collapse when streaming finishes
|
|
724
725
|
useEffect(() => {
|
|
725
726
|
if (!isStreaming && content.length > 0) setExpanded(false);
|
|
726
727
|
}, [isStreaming, content.length]);
|
|
727
728
|
|
|
729
|
+
// Auto-scroll to bottom during streaming
|
|
730
|
+
useEffect(() => {
|
|
731
|
+
if (isStreaming && expanded && scrollRef.current) {
|
|
732
|
+
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
733
|
+
}
|
|
734
|
+
}, [content, isStreaming, expanded]);
|
|
735
|
+
|
|
728
736
|
return (
|
|
729
737
|
<div className="rounded border border-border/50 bg-surface/30 text-xs">
|
|
730
738
|
<button
|
|
@@ -736,11 +744,11 @@ function ThinkingBlock({ content, isStreaming }: { content: string; isStreaming:
|
|
|
736
744
|
{!isStreaming && <span className="text-text-subtle/50 ml-auto">{content.length > 100 ? `${Math.round(content.length / 4)} tokens` : ""}</span>}
|
|
737
745
|
</button>
|
|
738
746
|
{expanded && (
|
|
739
|
-
<
|
|
740
|
-
<
|
|
747
|
+
<div ref={scrollRef} className="max-h-60 overflow-y-auto">
|
|
748
|
+
<div className="px-2 pb-2 text-text-subtle/80 whitespace-pre-wrap text-[11px] leading-relaxed">
|
|
741
749
|
{content}
|
|
742
|
-
</
|
|
743
|
-
</
|
|
750
|
+
</div>
|
|
751
|
+
</div>
|
|
744
752
|
)}
|
|
745
753
|
</div>
|
|
746
754
|
);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import"./react-nm2Ru1Pt.js";import"./api-client-BfBM3I7n.js";import{G as e}from"./index-_YxJ4vi1.js";export{e as useKeybindingsStore};
|