@hienlh/ppm 0.9.76 → 0.9.77
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 +5 -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 +18 -24
- 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
|
|
|
@@ -920,10 +920,9 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
920
920
|
const retryEnv = this.buildQueryEnv(meta.projectPath, refreshedAccount);
|
|
921
921
|
closeCurrentStream();
|
|
922
922
|
const { generator: earlyAuthGen, controller: earlyAuthCtrl } = createMessageChannel();
|
|
923
|
-
const
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
const retryOpts = { ...queryOptions, sessionId: undefined, resume: canResume ? currentSdkId : undefined, env: retryEnv };
|
|
923
|
+
const hasHistory = (this.messageCount.get(sessionId) ?? 0) > 0;
|
|
924
|
+
if (!hasHistory) earlyAuthCtrl.push(firstMsg);
|
|
925
|
+
const retryOpts = { ...queryOptions, sessionId: undefined, resume: hasHistory ? getSdkSessionId(sessionId) : undefined, env: retryEnv };
|
|
927
926
|
const rq = query({
|
|
928
927
|
prompt: earlyAuthGen,
|
|
929
928
|
options: { ...retryOpts, ...(permissionHooks && { hooks: permissionHooks }), canUseTool } as any,
|
|
@@ -947,10 +946,9 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
947
946
|
const switchEnv = this.buildQueryEnv(meta.projectPath, nextAcc);
|
|
948
947
|
closeCurrentStream();
|
|
949
948
|
const { generator: switchGen, controller: switchCtrl } = createMessageChannel();
|
|
950
|
-
const
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
const retryOpts = { ...queryOptions, sessionId: undefined, resume: canResume ? currentSdkId : undefined, env: switchEnv };
|
|
949
|
+
const hasHistory = (this.messageCount.get(sessionId) ?? 0) > 0;
|
|
950
|
+
if (!hasHistory) switchCtrl.push(firstMsg);
|
|
951
|
+
const retryOpts = { ...queryOptions, sessionId: undefined, resume: hasHistory ? getSdkSessionId(sessionId) : undefined, env: switchEnv };
|
|
954
952
|
const rq = query({
|
|
955
953
|
prompt: switchGen,
|
|
956
954
|
options: { ...retryOpts, ...(permissionHooks && { hooks: permissionHooks }), canUseTool } as any,
|
|
@@ -1099,10 +1097,9 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
1099
1097
|
// resume a non-existent session, causing the SDK to hang forever.
|
|
1100
1098
|
closeCurrentStream();
|
|
1101
1099
|
const { generator: authRetryGen, controller: authRetryCtrl } = createMessageChannel();
|
|
1102
|
-
const
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
const retryOpts = { ...queryOptions, sessionId: undefined, resume: canResume ? currentSdkId : undefined, env: retryEnv };
|
|
1100
|
+
const hasHistory = (this.messageCount.get(sessionId) ?? 0) > 0;
|
|
1101
|
+
if (!hasHistory) authRetryCtrl.push(firstMsg);
|
|
1102
|
+
const retryOpts = { ...queryOptions, sessionId: undefined, resume: hasHistory ? getSdkSessionId(sessionId) : undefined, env: retryEnv };
|
|
1106
1103
|
const rq = query({
|
|
1107
1104
|
prompt: authRetryGen,
|
|
1108
1105
|
options: { ...retryOpts, ...(permissionHooks && { hooks: permissionHooks }), canUseTool } as any,
|
|
@@ -1153,10 +1150,9 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
1153
1150
|
closeCurrentStream();
|
|
1154
1151
|
const rlRetryEnv = this.buildQueryEnv(meta.projectPath, account);
|
|
1155
1152
|
const { generator: rlRetryGen, controller: rlRetryCtrl } = createMessageChannel();
|
|
1156
|
-
const
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
const retryOpts = { ...queryOptions, sessionId: undefined, resume: rlCanResume ? rlCurrentSdkId : undefined, env: rlRetryEnv };
|
|
1153
|
+
const rlHasHistory = (this.messageCount.get(sessionId) ?? 0) > 0;
|
|
1154
|
+
if (!rlHasHistory) rlRetryCtrl.push(firstMsg);
|
|
1155
|
+
const retryOpts = { ...queryOptions, sessionId: undefined, resume: rlHasHistory ? getSdkSessionId(sessionId) : undefined, env: rlRetryEnv };
|
|
1160
1156
|
const rq = query({
|
|
1161
1157
|
prompt: rlRetryGen,
|
|
1162
1158
|
options: { ...retryOpts, ...(permissionHooks && { hooks: permissionHooks }), canUseTool } as any,
|
|
@@ -1251,10 +1247,9 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
1251
1247
|
closeCurrentStream();
|
|
1252
1248
|
const rlRetryEnv = this.buildQueryEnv(meta.projectPath, account);
|
|
1253
1249
|
const { generator: rlRetryGen, controller: rlRetryCtrl } = createMessageChannel();
|
|
1254
|
-
const
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
const retryOpts = { ...queryOptions, sessionId: undefined, resume: rlCanResume2 ? rlCurrentSdkId2 : undefined, env: rlRetryEnv };
|
|
1250
|
+
const rlHasHistory2 = (this.messageCount.get(sessionId) ?? 0) > 0;
|
|
1251
|
+
if (!rlHasHistory2) rlRetryCtrl.push(firstMsg);
|
|
1252
|
+
const retryOpts = { ...queryOptions, sessionId: undefined, resume: rlHasHistory2 ? getSdkSessionId(sessionId) : undefined, env: rlRetryEnv };
|
|
1258
1253
|
const rq = query({
|
|
1259
1254
|
prompt: rlRetryGen,
|
|
1260
1255
|
options: { ...retryOpts, ...(permissionHooks && { hooks: permissionHooks }), canUseTool } as any,
|
|
@@ -1283,10 +1278,9 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
1283
1278
|
closeCurrentStream();
|
|
1284
1279
|
const retryEnv = this.buildQueryEnv(meta.projectPath, refreshedAccount);
|
|
1285
1280
|
const { generator: authRetryGen2, controller: authRetryCtrl2 } = createMessageChannel();
|
|
1286
|
-
const
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
const retryOpts = { ...queryOptions, sessionId: undefined, resume: authCanResume2 ? authCurrentSdkId2 : undefined, env: retryEnv };
|
|
1281
|
+
const authHasHistory2 = (this.messageCount.get(sessionId) ?? 0) > 0;
|
|
1282
|
+
if (!authHasHistory2) authRetryCtrl2.push(firstMsg);
|
|
1283
|
+
const retryOpts = { ...queryOptions, sessionId: undefined, resume: authHasHistory2 ? getSdkSessionId(sessionId) : undefined, env: retryEnv };
|
|
1290
1284
|
const rq = query({
|
|
1291
1285
|
prompt: authRetryGen2,
|
|
1292
1286
|
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};
|