very-happy-cli 0.2.165 → 0.2.167
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/README.md +1 -1
- package/dist/{AcpBackend-DCfNt9bD.cjs → AcpBackend-D1IKJGz_.cjs} +3 -3
- package/dist/{AcpBackend-C11FnVw5.mjs → AcpBackend-kFZlee5E.mjs} +3 -3
- package/dist/{AcpSessionManager-BQnDJvct.mjs → AcpSessionManager-B7FIrsys.mjs} +1 -1
- package/dist/{AcpSessionManager-9yN2iH4b.cjs → AcpSessionManager-Zf00QnlL.cjs} +1 -1
- package/dist/{BasePermissionHandler-BwQHxl6M.cjs → BasePermissionHandler-DHxYAK5L.cjs} +1 -1
- package/dist/{BasePermissionHandler-CwJ48yiB.mjs → BasePermissionHandler-DS0kRTNI.mjs} +1 -1
- package/dist/{BaseReasoningProcessor-ClF2g1qz.mjs → BaseReasoningProcessor-BRtCCt7q.mjs} +1 -1
- package/dist/{BaseReasoningProcessor-81lf_M8G.cjs → BaseReasoningProcessor-CsjVdt_H.cjs} +1 -1
- package/dist/{agentGuidance-CQuNlNx8.mjs → agentGuidance-BDYt2Qy2.mjs} +4 -2
- package/dist/{agentGuidance-B5NATuFa.cjs → agentGuidance-BtBx2xxE.cjs} +4 -1
- package/dist/{agentHome-DvR176E5.mjs → agentHome-Ct-VXFeG.mjs} +4 -4
- package/dist/{agentHome-D3Nn82kI.cjs → agentHome-DeAWzhSe.cjs} +4 -4
- package/dist/{api-AzdzTKFs.cjs → api-BZYLRDM9.cjs} +93 -13
- package/dist/{api-DTVPUE7g.mjs → api-CReZazeT.mjs} +93 -13
- package/dist/{apiMachine-B_cJldfa.cjs → apiMachine-BgMxGPsR.cjs} +2173 -66
- package/dist/{apiMachine-BQ-b4Lu3.mjs → apiMachine-CWndf1Vn.mjs} +2176 -69
- package/dist/{attachmentContent-muDCe4Gy.cjs → attachmentContent-HcJqFQW8.cjs} +2 -2
- package/dist/{attachmentContent-dzIs-wis.mjs → attachmentContent-j875mwU7.mjs} +2 -2
- package/dist/{attachmentLimits-eJZ4ZBjF.cjs → attachmentLimits-BEYEEBm8.cjs} +40 -2
- package/dist/{attachmentLimits-B8nzhw-i.mjs → attachmentLimits-CxG3T18E.mjs} +40 -2
- package/dist/{auth-Jtmvh1w4.mjs → auth-BIHuc7vY.mjs} +10 -10
- package/dist/{auth-BY5ujhGn.mjs → auth-CFS9Lfgv.mjs} +5 -5
- package/dist/{auth-DtP-B4gf.cjs → auth-Myr2Q42x.cjs} +10 -10
- package/dist/{auth-CfLd3phE.cjs → auth-VzWpbo5P.cjs} +5 -5
- package/dist/{auto-ClNnbSoF.cjs → auto-Bf6Dr7_o.cjs} +85 -21
- package/dist/{auto-DjWfR1UB.mjs → auto-Df9-zmuN.mjs} +84 -22
- package/dist/{bootstrap-D_ExGrY2.mjs → bootstrap-BSOxYpaP.mjs} +2 -2
- package/dist/{bootstrap-B1dMZA-u.cjs → bootstrap-Dj8WBiR2.cjs} +2 -2
- package/dist/{browser-mwzpOmcw.mjs → browser-BCFIHOvo.mjs} +1 -1
- package/dist/{browser-_gV7tcPs.cjs → browser-BoCV40yC.cjs} +1 -1
- package/dist/{claudeLocal-G5UmJ81F.cjs → claudeLocal-CGsq7Kul.cjs} +7 -7
- package/dist/{claudeLocal-BeHToEK5.mjs → claudeLocal-D5-eA9SC.mjs} +7 -7
- package/dist/{claudeLocalLauncher-Cq6bqAT4.mjs → claudeLocalLauncher-C9Cc438l.mjs} +15 -12
- package/dist/{claudeLocalLauncher-B4y2BsmO.cjs → claudeLocalLauncher-Cf9QeYDT.cjs} +15 -12
- package/dist/{claudeSessionFork-CDJq_b9r.cjs → claudeSessionFork-Byd8zD1U.cjs} +1 -1
- package/dist/{claudeSessionFork-CokUhS4r.mjs → claudeSessionFork-CMzAva8G.mjs} +1 -1
- package/dist/{client-Do7cGPEq.cjs → client-BKnKRLSt.cjs} +3 -3
- package/dist/{client-CJZKBZCE.mjs → client-C7FYMer0.mjs} +2 -2
- package/dist/{client-uN9d6Git.cjs → client-D0T5-6Ds.cjs} +2 -2
- package/dist/{client-BwaWdlCm.mjs → client-Dsfy_SAb.mjs} +3 -3
- package/dist/codex/happyMcpStdioBridge.cjs +23 -18
- package/dist/codex/happyMcpStdioBridge.mjs +22 -17
- package/dist/{codexAppServerClient-B0QaMQsC.mjs → codexAppServerClient-Dd_4TxHv.mjs} +48 -4
- package/dist/{codexAppServerClient-BuSdeUxT.cjs → codexAppServerClient-TiW9OBk4.cjs} +47 -3
- package/dist/{codexCommand-C3gdjMAw.mjs → codexCommand-BZSr4MKI.mjs} +170 -69
- package/dist/{codexCommand-YdYmykAR.cjs → codexCommand-Bxryqh90.cjs} +170 -69
- package/dist/{config-CGCIFRCN.mjs → config-C_Pj7yxV.mjs} +3 -3
- package/dist/{config-ChoHFnfO.cjs → config-DXPash-r.cjs} +3 -3
- package/dist/{configuration-BE61XNsg.cjs → configuration-C3piBtxY.cjs} +1 -1
- package/dist/{configuration-L1qcGTFM.mjs → configuration-WspKd2C2.mjs} +2 -2
- package/dist/{connect-D5IKfcfd.cjs → connect-BN-IBElA.cjs} +14 -14
- package/dist/{connect-CFD0Em6a.mjs → connect-DGPZs5px.mjs} +14 -14
- package/dist/{context-DaCCB4cm.mjs → context-Cr6a3NTg.mjs} +1 -1
- package/dist/{context-3WVw8qPz.cjs → context-Jrq4-eCZ.cjs} +1 -1
- package/dist/{controlClient-RWTlQA4_.cjs → controlClient-BI6Fw3fQ.cjs} +28 -5
- package/dist/{controlClient-Ci7HTMII.mjs → controlClient-DTheOlvC.mjs} +27 -6
- package/dist/{createSessionMetadata-CWR-AB2A.mjs → createSessionMetadata-BEZ4DIDA.mjs} +57 -5
- package/dist/{createSessionMetadata-CPGTztXP.cjs → createSessionMetadata-zQSz3hgk.cjs} +56 -3
- package/dist/defaultPermissionMode-Cif_RHGb.mjs +191 -0
- package/dist/defaultPermissionMode-DNKNzk7l.cjs +197 -0
- package/dist/{deterministicJson-BBwG-Ase.mjs → deterministicJson-BMMifViN.mjs} +36 -52
- package/dist/{deterministicJson-C4Jo2RWy.cjs → deterministicJson-C7O2ulKY.cjs} +41 -59
- package/dist/{doctor-ZOQLE5AH.cjs → doctor-BykBaMTr.cjs} +6 -6
- package/dist/{doctor-CAC6qY9Z.mjs → doctor-D1KKyPzB.mjs} +6 -6
- package/dist/{assistantTools-BsrG3PD-.cjs → downloadTarget-CWTb2-EM.cjs} +44 -6
- package/dist/{assistantTools-DPGga5lM.mjs → downloadTarget-Cp48jrIP.mjs} +45 -8
- package/dist/{ensureDaemonRunning-b1pvEFcS.mjs → ensureDaemonRunning-Ba5fYoxb.mjs} +5 -5
- package/dist/{ensureDaemonRunning-mIgX2vnc.cjs → ensureDaemonRunning-CCxX7MoP.cjs} +5 -5
- package/dist/{handleResumeCommand-CTGbZkCL.mjs → handleResumeCommand-BTUGlNjB.mjs} +5 -5
- package/dist/{handleResumeCommand-DGNhbv-3.cjs → handleResumeCommand-yPdDQHqz.cjs} +5 -5
- package/dist/{index-vVWpCoTr.mjs → index-B0HEEYGh.mjs} +22 -5
- package/dist/{index-BzIxE0Eu.cjs → index-BjhKqDa0.cjs} +3 -3
- package/dist/{index-BgzjPv07.mjs → index-C3_83bqS.mjs} +3 -3
- package/dist/{index-DqlyNgt4.cjs → index-C9hI8a10.cjs} +26 -4
- package/dist/{index-DjtBengz.mjs → index-CE2GgPbr.mjs} +40 -39
- package/dist/{index-Diu6HdWH.mjs → index-D8m8Ripb.mjs} +4 -4
- package/dist/{index-DrSPR59N.cjs → index-DifM62wX.cjs} +39 -38
- package/dist/{index-DIodyAqF.cjs → index-PNMQPKhz.cjs} +4 -4
- package/dist/index.cjs +2 -2
- package/dist/index.mjs +2 -2
- package/dist/{install-BO_W7VhL.mjs → install-BdvKiOQw.mjs} +2 -0
- package/dist/{install-Cp_aB-NQ.mjs → install-C0-QFn9s.mjs} +3 -3
- package/dist/{install-Cs5qAPrB.cjs → install-Cf6AFSWC.cjs} +2 -0
- package/dist/{install-DrUQk2DJ.cjs → install-CqZVeze8.cjs} +3 -3
- package/dist/lib.cjs +12 -12
- package/dist/lib.d.cts +116 -2
- package/dist/lib.d.mts +116 -2
- package/dist/lib.mjs +12 -12
- package/dist/{lidState-BLHLmhwd.mjs → lidState-B2ZxdREe.mjs} +1 -1
- package/dist/{lidState-z9ekRpZu.cjs → lidState-Bc2K9H9l.cjs} +1 -1
- package/dist/{localHappyAgentAuth-BWvmc_Hg.mjs → localHappyAgentAuth-B5XPEMwk.mjs} +1 -1
- package/dist/{localHappyAgentAuth-BCx0y1qW.cjs → localHappyAgentAuth-T5GOvRha.cjs} +1 -1
- package/dist/{logger-D0hkk7Vo.cjs → logger-B_MWPbZz.cjs} +2 -2
- package/dist/{logger-CknSVkIe.mjs → logger-CbBv3q_b.mjs} +2 -2
- package/dist/{machineMetadata-BDCYOXQg.cjs → machineMetadata-BafS4JY8.cjs} +4 -3
- package/dist/{machineMetadata-Cj1DoFot.mjs → machineMetadata-DHg238U4.mjs} +4 -3
- package/dist/{main-COAPGIso.cjs → main-DNrA0qBy.cjs} +45 -45
- package/dist/{main-CKUfkVG_.mjs → main-DXAK0n7x.mjs} +45 -45
- package/dist/{manager-BQ4flvQZ.mjs → manager-BEsI4qt3.mjs} +9 -3
- package/dist/{manager-Cly8xVIR.cjs → manager-HrEj442o.cjs} +8 -2
- package/dist/{mcp-BILnw4si.cjs → mcp-AvPOhDXy.cjs} +24 -19
- package/dist/{mcp-DeYUfpo8.mjs → mcp-BotAjCdD.mjs} +23 -18
- package/dist/mirrorProtocol-BdcN6QlN.cjs +1270 -0
- package/dist/mirrorProtocol-BwHm8odn.mjs +1247 -0
- package/dist/{package-UkmvSFsg.cjs → package-C9ZP9bpX.cjs} +7 -7
- package/dist/{package-VIUXrYZ1.mjs → package-DU1DxhH9.mjs} +7 -7
- package/dist/{peerTools-CttfOxti.cjs → peerTools-Dkbu7gag.cjs} +4 -4
- package/dist/{peerTools-SWITnkqz.mjs → peerTools-os2DVjA0.mjs} +4 -4
- package/dist/{persistence-C6ztVW1W.cjs → persistence-BYGl0QYw.cjs} +147 -25
- package/dist/{persistence-DujtUH94.mjs → persistence-ON0zpHfb.mjs} +123 -12
- package/dist/{piRunnerArgs-BVJHIjlk.mjs → piRunnerArgs-CYsnT9sV.mjs} +1 -1
- package/dist/{piRunnerArgs-tBQg6xDw.cjs → piRunnerArgs-DpbvstUX.cjs} +1 -1
- package/dist/{piRuntime-DZzjF9It.cjs → piRuntime-CDnILlCd.cjs} +1 -1
- package/dist/{piRuntime-DKFA8u8t.mjs → piRuntime-CmEid-wu.mjs} +1 -1
- package/dist/{piTerminal-BTSaFlpV.mjs → piTerminal-0eNlXSlL.mjs} +17 -17
- package/dist/{piTerminal-t5pQHCPT.cjs → piTerminal-B57TgMON.cjs} +17 -17
- package/dist/precheckCompat-CWOi5c9L.cjs +29 -0
- package/dist/precheckCompat-DeEbilbr.mjs +26 -0
- package/dist/rateLimitWindows-CusGIqlq.cjs +258 -0
- package/dist/rateLimitWindows-vZ9Bo7aT.mjs +248 -0
- package/dist/{registerCommonHandlers-BR0J4g64.mjs → registerCommonHandlers-B0PVff2U.mjs} +2 -2
- package/dist/{registerCommonHandlers--8ds2auE.cjs → registerCommonHandlers-DVYB3iVn.cjs} +2 -2
- package/dist/{registerKillSessionHandler-Czz0tdki.mjs → registerKillSessionHandler-BJMHJHXe.mjs} +15 -10
- package/dist/{registerKillSessionHandler-CI23l8aJ.cjs → registerKillSessionHandler-BwXGKtg_.cjs} +15 -10
- package/dist/{remoteSessionClient-DCAUHgPz.mjs → remoteSessionClient-BzPXSC21.mjs} +5 -5
- package/dist/{remoteSessionClient-BAkIwM2S.cjs → remoteSessionClient-PyUAIhwK.cjs} +5 -5
- package/dist/{tools-BVRoGcQH.mjs → reportProgressTool-BvB13EwC.mjs} +67 -10
- package/dist/{tools-mWqsE6ji.cjs → reportProgressTool-D4KB_4cP.cjs} +73 -9
- package/dist/{run-CkKujjn8.cjs → run-BC8jzq_h.cjs} +558 -122
- package/dist/{run-B75qm2I6.mjs → run-O2M8LsWf.mjs} +556 -120
- package/dist/{runClaude-B1T9DLLi.cjs → runClaude-C7jPa8VP.cjs} +93 -26
- package/dist/{runClaude-aBfBweec.mjs → runClaude-DGkr5E7s.mjs} +93 -26
- package/dist/{runClaudeDeps-BGjNGfuZ.cjs → runClaudeDeps-5kzncqME.cjs} +113 -35
- package/dist/{runClaudeDeps-BOrPP4J1.mjs → runClaudeDeps-CX_TxS78.mjs} +116 -37
- package/dist/{runGemini-BuAA_OQb.cjs → runGemini-BfYA4Uqc.cjs} +43 -36
- package/dist/{runGemini-a-V7JE-J.mjs → runGemini-CwmGBH3L.mjs} +43 -36
- package/dist/{runOpenClaw-DUSwXg1A.mjs → runOpenClaw-BYAPYFyL.mjs} +18 -18
- package/dist/{runOpenClaw-BBVffIh9.cjs → runOpenClaw-C72jut7v.cjs} +18 -18
- package/dist/{sandbox-Fd2G5yaq.cjs → sandbox-Bqkp8IT7.cjs} +4 -4
- package/dist/{sandbox-Brb_5wzc.mjs → sandbox-DlVnK1Vi.mjs} +4 -4
- package/dist/{selfCheck-NuJKMInJ.cjs → selfCheck-B4lLZplk.cjs} +34 -34
- package/dist/selfCheck-DYTGBdIO.mjs +84 -0
- package/dist/{send-DEQJiq_x.mjs → send-B5bWW9Tl.mjs} +8 -8
- package/dist/{send-4dgHKC3u.cjs → send-BCwS6n_s.cjs} +8 -8
- package/dist/{server-C6AlUFkw.mjs → server-CEJfRQfj.mjs} +4 -4
- package/dist/{server-DWGcDmHG.cjs → server-CFWZehnC.cjs} +6 -6
- package/dist/{sessionLock-Dm8QyGo4.mjs → sessionLock-AwOYsKT7.mjs} +12 -4
- package/dist/{sessionLock-B1GgaO1q.cjs → sessionLock-LBWNjpHf.cjs} +12 -3
- package/dist/{sessionModeFile-BvOEJ4Fl.cjs → sessionModeFile-CyPuZRIU.cjs} +1 -1
- package/dist/{sessionModeFile-CuNAWCKL.mjs → sessionModeFile-fvFn1JVG.mjs} +1 -1
- package/dist/{sessionOps-DG5MMEp_.mjs → sessionOps-DZRH4-hV.mjs} +4 -4
- package/dist/{sessionOps-JP64uJmc.cjs → sessionOps-DzbZI3Eq.cjs} +4 -4
- package/dist/{sessionScanner-DqSk5Ol_.mjs → sessionScanner-Q_Ei2flB.mjs} +75 -19
- package/dist/{sessionScanner-BBPEgJ5S.cjs → sessionScanner-i-cgwFVw.cjs} +75 -19
- package/dist/{sessions-4isbBAWi.mjs → sessions-DAmjMAZN.mjs} +9 -9
- package/dist/{sessions-DgJuw_i0.cjs → sessions-DYK5hD4T.cjs} +9 -9
- package/dist/{setupOfflineReconnection-_MoZtsro.mjs → setupOfflineReconnection-BfDBxNdo.mjs} +2 -2
- package/dist/{setupOfflineReconnection-CKJKVIUS.cjs → setupOfflineReconnection-C2LO3hSP.cjs} +2 -2
- package/dist/{spawn-BTOG4tTm.mjs → spawn-BmjG5j5Y.mjs} +15 -14
- package/dist/{spawn-CshERWbY.cjs → spawn-GK_sXE8h.cjs} +15 -14
- package/dist/{spawnAgents-Czi2VDzj.mjs → spawnAgents-HDaE3C0-.mjs} +1 -1
- package/dist/{spawnAgents-McixdtV1.cjs → spawnAgents-vglexX9N.cjs} +1 -1
- package/dist/{spawnHappyCLI-Dc4xYm8P.mjs → spawnHappyCLI-B2h0P4b5.mjs} +1 -1
- package/dist/{spawnHappyCLI-Dd5_8Vex.cjs → spawnHappyCLI-J4iEhfyZ.cjs} +1 -1
- package/dist/specialCommands-DisVxagO.mjs +93 -0
- package/dist/specialCommands-DmJMKspE.cjs +96 -0
- package/dist/{startFileWatcher-B0VlG-Fn.mjs → startFileWatcher-CeK1k5gt.mjs} +1 -1
- package/dist/{startFileWatcher-1LedLBY-.cjs → startFileWatcher-xKo8AoYl.cjs} +1 -1
- package/dist/{streamRelay-C0AffuX6.cjs → streamRelay-Ce9sgcOV.cjs} +1 -1
- package/dist/{streamRelay-BzMEEvJU.mjs → streamRelay-Iogsh51a.mjs} +1 -1
- package/dist/{sync-DwUUGGFz.cjs → sync-Bvc-vh6C.cjs} +1 -1
- package/dist/{sync-Dfm6K2te.mjs → sync-CCwYpJMS.mjs} +1 -1
- package/dist/{systemPrompt-D41Zw1a3.mjs → systemPrompt-BNIkP3tR.mjs} +3 -3
- package/dist/{systemPrompt-CB1Ls0AE.cjs → systemPrompt-DwOddbuG.cjs} +3 -3
- package/dist/{teams-D8Ly3i6u.mjs → teams-DRrLBcys.mjs} +7 -7
- package/dist/{teams-NWv2ji16.cjs → teams-GHMCRZOw.cjs} +7 -7
- package/dist/{time-BbzJ7PJQ.cjs → time-BXvtjhe9.cjs} +1 -1
- package/dist/{time-v1Y_qEJp.mjs → time-PG5Dfwdv.mjs} +1 -1
- package/dist/{titleGenerator-cH6IkPRi.cjs → titleGenerator-B07bZA0V.cjs} +2 -2
- package/dist/{titleGenerator-BhoblAQY.mjs → titleGenerator-CIzfpfZW.mjs} +1 -1
- package/dist/{todo-D1hgWV6d.cjs → todo-B0aeoNu3.cjs} +6 -6
- package/dist/{todo-CuNct-fw.mjs → todo-CfL7SBFc.mjs} +6 -6
- package/dist/{tools-jof0Bu54.mjs → tools-D91mq5Ib.mjs} +1 -1
- package/dist/{tools-c2fb0CF5.cjs → tools-DJIANHSh.cjs} +1 -1
- package/dist/transcriptCursor-B7T_gkQ0.mjs +102 -0
- package/dist/transcriptCursor-DcXGLeSc.cjs +106 -0
- package/dist/{uninstall-BB7WYYAk.mjs → uninstall-5xz3Xvhf.mjs} +3 -3
- package/dist/{uninstall-VPH5GfrA.cjs → uninstall-AtzOfdrj.cjs} +3 -3
- package/package.json +7 -7
- package/scripts/terminal_status_forwarder.cjs +89 -0
- package/dist/defaultPermissionMode-BnxJPVHT.mjs +0 -4
- package/dist/defaultPermissionMode-CxmTXUQR.cjs +0 -7
- package/dist/mirrorProtocol-BNLtoWjO.mjs +0 -170
- package/dist/mirrorProtocol-BUtmg1ws.cjs +0 -186
- package/dist/selfCheck-BgwJ3NGS.mjs +0 -84
- package/dist/specialCommands-BSqhYg5X.mjs +0 -52
- package/dist/specialCommands-DSsTeRej.cjs +0 -54
|
@@ -1,56 +1,57 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var socket_ioClient = require('socket.io-client');
|
|
4
|
-
var logger = require('./logger-
|
|
5
|
-
var lidState = require('./lidState-
|
|
6
|
-
var mirrorProtocol = require('./mirrorProtocol-
|
|
7
|
-
var configuration = require('./configuration-
|
|
4
|
+
var logger = require('./logger-B_MWPbZz.cjs');
|
|
5
|
+
var lidState = require('./lidState-Bc2K9H9l.cjs');
|
|
6
|
+
var mirrorProtocol = require('./mirrorProtocol-BdcN6QlN.cjs');
|
|
7
|
+
var configuration = require('./configuration-C3piBtxY.cjs');
|
|
8
8
|
var cliUpdate = require('./cliUpdate-DsKXPFYp.cjs');
|
|
9
|
-
var registerCommonHandlers = require('./registerCommonHandlers
|
|
9
|
+
var registerCommonHandlers = require('./registerCommonHandlers-DVYB3iVn.cjs');
|
|
10
10
|
var promises = require('node:fs/promises');
|
|
11
11
|
var path = require('node:path');
|
|
12
12
|
var os = require('node:os');
|
|
13
13
|
var node_child_process = require('node:child_process');
|
|
14
|
-
var persistence = require('./persistence-
|
|
14
|
+
var persistence = require('./persistence-BYGl0QYw.cjs');
|
|
15
15
|
var authRelay = require('./authRelay-BdYkC1Po.cjs');
|
|
16
16
|
var limits = require('./limits-C-a7gr04.cjs');
|
|
17
|
-
var time = require('./time-
|
|
17
|
+
var time = require('./time-BXvtjhe9.cjs');
|
|
18
18
|
var pty = require('node-pty');
|
|
19
19
|
var terminals = require('./terminals-B_bfC9c2.cjs');
|
|
20
20
|
var tmuxSocket = require('./tmuxSocket-D-Xt6Vn-.cjs');
|
|
21
|
-
var index = require('./index-
|
|
21
|
+
var index = require('./index-PNMQPKhz.cjs');
|
|
22
22
|
var node_crypto = require('node:crypto');
|
|
23
23
|
var node_fs = require('node:fs');
|
|
24
24
|
var axios = require('axios');
|
|
25
|
+
var projectPath = require('./projectPath-ChLhdOyt.cjs');
|
|
25
26
|
var ids = require('./ids-B6ZC0HGt.cjs');
|
|
26
27
|
var detectCLI = require('./detectCLI-DR9ZaN6B.cjs');
|
|
27
|
-
var localHappyAgentAuth = require('./localHappyAgentAuth-
|
|
28
|
+
var localHappyAgentAuth = require('./localHappyAgentAuth-T5GOvRha.cjs');
|
|
28
29
|
var path$1 = require('./path-B7zW-WwE.cjs');
|
|
29
30
|
var oneShotPrompts = require('./oneShotPrompts-CL0dip9C.cjs');
|
|
30
|
-
var claudeSessionFork = require('./claudeSessionFork-
|
|
31
|
-
var codexAppServerClient = require('./codexAppServerClient-
|
|
32
|
-
var index$1 = require('./index-
|
|
31
|
+
var claudeSessionFork = require('./claudeSessionFork-Byd8zD1U.cjs');
|
|
32
|
+
var codexAppServerClient = require('./codexAppServerClient-TiW9OBk4.cjs');
|
|
33
|
+
var index$1 = require('./index-C9hI8a10.cjs');
|
|
33
34
|
var codexThreadFork = require('./codexThreadFork-DNekT4nQ.cjs');
|
|
35
|
+
var rateLimitWindows = require('./rateLimitWindows-CusGIqlq.cjs');
|
|
34
36
|
require('chalk');
|
|
35
37
|
require('node:util');
|
|
36
38
|
require('os');
|
|
37
39
|
require('child_process');
|
|
38
|
-
require('./package-
|
|
40
|
+
require('./package-C9ZP9bpX.cjs');
|
|
39
41
|
require('util');
|
|
40
42
|
require('fs/promises');
|
|
41
43
|
require('crypto');
|
|
42
44
|
require('path');
|
|
43
45
|
require('cross-spawn');
|
|
44
|
-
require('./projectPath-ChLhdOyt.cjs');
|
|
45
|
-
require('url');
|
|
46
46
|
require('zod');
|
|
47
47
|
require('tweetnacl');
|
|
48
48
|
require('./terminalTitleSuggest-DhvRFX3H.cjs');
|
|
49
49
|
require('./loginShellEnv-BkuYtExZ.cjs');
|
|
50
|
+
require('url');
|
|
50
51
|
require('fs');
|
|
51
52
|
require('node:readline');
|
|
52
53
|
require('./contentLogMetadata-ZSFk_NjQ.cjs');
|
|
53
|
-
require('./manager-
|
|
54
|
+
require('./manager-HrEj442o.cjs');
|
|
54
55
|
require('@anthropic-ai/sandbox-runtime');
|
|
55
56
|
require('@paralleldrive/cuid2');
|
|
56
57
|
|
|
@@ -207,7 +208,7 @@ const TODO_LIST_MAX_ITEMS = 500;
|
|
|
207
208
|
const TODO_TITLE_MAX_CHARS = 500;
|
|
208
209
|
const FIELD_MAX_CHARS = 200;
|
|
209
210
|
const PRIORITIES = ["none", "low", "medium", "high"];
|
|
210
|
-
function str(value, max = FIELD_MAX_CHARS) {
|
|
211
|
+
function str$1(value, max = FIELD_MAX_CHARS) {
|
|
211
212
|
if (typeof value !== "string") return void 0;
|
|
212
213
|
const flat = value.replace(/\s+/g, " ").trim();
|
|
213
214
|
if (!flat) return void 0;
|
|
@@ -238,8 +239,8 @@ function parseTodoList(stdout) {
|
|
|
238
239
|
continue;
|
|
239
240
|
}
|
|
240
241
|
const r = raw;
|
|
241
|
-
const id = str(r.id);
|
|
242
|
-
const title = str(r.title, TODO_TITLE_MAX_CHARS);
|
|
242
|
+
const id = str$1(r.id);
|
|
243
|
+
const title = str$1(r.title, TODO_TITLE_MAX_CHARS);
|
|
243
244
|
if (!id || !title) {
|
|
244
245
|
dropped++;
|
|
245
246
|
continue;
|
|
@@ -249,10 +250,10 @@ function parseTodoList(stdout) {
|
|
|
249
250
|
id,
|
|
250
251
|
title,
|
|
251
252
|
status: r.status === "done" ? "done" : "open",
|
|
252
|
-
...str(r.due) ? { due: str(r.due) } : {},
|
|
253
|
+
...str$1(r.due) ? { due: str$1(r.due) } : {},
|
|
253
254
|
...priority ? { priority } : {},
|
|
254
|
-
...str(r.group) ? { group: str(r.group) } : {},
|
|
255
|
-
...str(r.note) ? { note: str(r.note) } : {}
|
|
255
|
+
...str$1(r.group) ? { group: str$1(r.group) } : {},
|
|
256
|
+
...str$1(r.note) ? { note: str$1(r.note) } : {}
|
|
256
257
|
});
|
|
257
258
|
}
|
|
258
259
|
return { items, dropped, truncated: hitCap };
|
|
@@ -4989,8 +4990,8 @@ function buildCaptureBatch(o) {
|
|
|
4989
4990
|
const CAPTURE_FULL_BUDGET_BYTES = 1024 * 1024;
|
|
4990
4991
|
const ALT_ENTER = Buffer.from("\x1B[?1049h", "ascii");
|
|
4991
4992
|
const CURSOR_HOME = Buffer.from("\x1B[H", "ascii");
|
|
4992
|
-
const CRLF = Buffer.from("\r\n", "ascii");
|
|
4993
|
-
const SGR_RESET = Buffer.from("\x1B[0m", "ascii");
|
|
4993
|
+
const CRLF$1 = Buffer.from("\r\n", "ascii");
|
|
4994
|
+
const SGR_RESET$1 = Buffer.from("\x1B[0m", "ascii");
|
|
4994
4995
|
function normalizeCaptureLines(blob) {
|
|
4995
4996
|
if (blob.length === 0) return blob;
|
|
4996
4997
|
const lines = [];
|
|
@@ -5005,7 +5006,7 @@ function normalizeCaptureLines(blob) {
|
|
|
5005
5006
|
if (start < blob.length) lines.push(blob.subarray(start));
|
|
5006
5007
|
const out = [];
|
|
5007
5008
|
lines.forEach((l, i) => {
|
|
5008
|
-
if (i > 0) out.push(CRLF);
|
|
5009
|
+
if (i > 0) out.push(CRLF$1);
|
|
5009
5010
|
out.push(l);
|
|
5010
5011
|
});
|
|
5011
5012
|
return Buffer.concat(out);
|
|
@@ -5033,31 +5034,31 @@ function assembleRestore(responses, paneState, budget = CAPTURE_FULL_BUDGET_BYTE
|
|
|
5033
5034
|
realCount = lines.length;
|
|
5034
5035
|
while (lines.length < rows) lines.push(EMPTY);
|
|
5035
5036
|
}
|
|
5036
|
-
const realBody = joinCrlf(lines.slice(0, realCount));
|
|
5037
|
+
const realBody = joinCrlf$1(lines.slice(0, realCount));
|
|
5037
5038
|
const padCount = lines.length - realCount;
|
|
5038
|
-
const padBody = padCount > 0 ? Buffer.concat([realCount > 0 ? CRLF : EMPTY, SGR_RESET, joinCrlf(new Array(padCount).fill(EMPTY))]) : EMPTY;
|
|
5039
|
+
const padBody = padCount > 0 ? Buffer.concat([realCount > 0 ? CRLF$1 : EMPTY, SGR_RESET$1, joinCrlf$1(new Array(padCount).fill(EMPTY))]) : EMPTY;
|
|
5039
5040
|
const cursor = paneState ? Buffer.from(`\x1B[${paneState.cursorY + 1};${paneState.cursorX + 1}H`, "ascii") : Buffer.alloc(0);
|
|
5040
|
-
return Buffer.concat([realBody, padBody, SGR_RESET, tail, cursor]);
|
|
5041
|
+
return Buffer.concat([realBody, padBody, SGR_RESET$1, tail, cursor]);
|
|
5041
5042
|
};
|
|
5042
|
-
const wrapFull = (b) => Buffer.concat([SGR_RESET, b, SGR_RESET]);
|
|
5043
|
-
const wrapSmall = (b) => Buffer.concat([b, SGR_RESET]);
|
|
5043
|
+
const wrapFull = (b) => Buffer.concat([SGR_RESET$1, b, SGR_RESET$1]);
|
|
5044
|
+
const wrapSmall = (b) => Buffer.concat([b, SGR_RESET$1]);
|
|
5044
5045
|
if (!alternateOn) {
|
|
5045
5046
|
const history2 = trimTrailingBlankRows(normalizeCaptureLines(get("history")));
|
|
5046
5047
|
const smallHistory = trimTrailingBlankRows(normalizeCaptureLines(get("smallHistory")));
|
|
5047
5048
|
const withHistory = (h) => Buffer.concat(
|
|
5048
|
-
h.length > 0 ? [truncateToBudget(h, budget), SGR_RESET, CRLF, screen()] : [screen()]
|
|
5049
|
+
h.length > 0 ? [truncateToBudget(h, budget), SGR_RESET$1, CRLF$1, screen()] : [screen()]
|
|
5049
5050
|
);
|
|
5050
5051
|
return { full: wrapFull(withHistory(history2)), small: wrapSmall(withHistory(smallHistory)), alternateOn };
|
|
5051
5052
|
}
|
|
5052
5053
|
const history = trimTrailingBlankRows(normalizeCaptureLines(get("history")));
|
|
5053
5054
|
const saved = trimTrailingBlankRows(normalizeCaptureLines(get("altSaved")));
|
|
5054
5055
|
const scrollback = truncateToBudget(
|
|
5055
|
-
Buffer.concat(history.length > 0 && saved.length > 0 ? [history, SGR_RESET, CRLF, saved] : [history, saved]),
|
|
5056
|
+
Buffer.concat(history.length > 0 && saved.length > 0 ? [history, SGR_RESET$1, CRLF$1, saved] : [history, saved]),
|
|
5056
5057
|
budget
|
|
5057
5058
|
);
|
|
5058
|
-
const altScreen = Buffer.concat([ALT_ENTER, CURSOR_HOME, SGR_RESET, screen()]);
|
|
5059
|
+
const altScreen = Buffer.concat([ALT_ENTER, CURSOR_HOME, SGR_RESET$1, screen()]);
|
|
5059
5060
|
return {
|
|
5060
|
-
full: wrapFull(Buffer.concat([scrollback, scrollback.length > 0 ? CRLF : Buffer.alloc(0), altScreen])),
|
|
5061
|
+
full: wrapFull(Buffer.concat([scrollback, scrollback.length > 0 ? CRLF$1 : Buffer.alloc(0), altScreen])),
|
|
5061
5062
|
small: wrapSmall(altScreen),
|
|
5062
5063
|
alternateOn
|
|
5063
5064
|
};
|
|
@@ -5071,7 +5072,7 @@ function stripTrailingNewlines(blob) {
|
|
|
5071
5072
|
function trimTrailingBlankRows(blob) {
|
|
5072
5073
|
const lines = splitLines(blob);
|
|
5073
5074
|
while (lines.length > 0 && lines[lines.length - 1].length === 0) lines.pop();
|
|
5074
|
-
return joinCrlf(lines);
|
|
5075
|
+
return joinCrlf$1(lines);
|
|
5075
5076
|
}
|
|
5076
5077
|
function splitLines(blob) {
|
|
5077
5078
|
const out = [];
|
|
@@ -5086,10 +5087,10 @@ function splitLines(blob) {
|
|
|
5086
5087
|
if (start < blob.length) out.push(blob.subarray(start));
|
|
5087
5088
|
return out;
|
|
5088
5089
|
}
|
|
5089
|
-
function joinCrlf(lines) {
|
|
5090
|
+
function joinCrlf$1(lines) {
|
|
5090
5091
|
const parts = [];
|
|
5091
5092
|
lines.forEach((l, i) => {
|
|
5092
|
-
if (i > 0) parts.push(CRLF);
|
|
5093
|
+
if (i > 0) parts.push(CRLF$1);
|
|
5093
5094
|
parts.push(l);
|
|
5094
5095
|
});
|
|
5095
5096
|
return Buffer.concat(parts);
|
|
@@ -5455,7 +5456,7 @@ class TerminalNotifyTracker {
|
|
|
5455
5456
|
* Feed one observation for a terminal. Returns the notification event this
|
|
5456
5457
|
* observation triggers, or null. Call once per terminal per tick.
|
|
5457
5458
|
*/
|
|
5458
|
-
observe(terminalId, state, now, agentKind) {
|
|
5459
|
+
observe(terminalId, state, now, agentKind, opts = {}) {
|
|
5459
5460
|
let t = this.tracks.get(terminalId);
|
|
5460
5461
|
if (!t || t.agentKind !== agentKind || now - t.lastObservedAt > 45e3) {
|
|
5461
5462
|
t = {
|
|
@@ -5486,6 +5487,7 @@ class TerminalNotifyTracker {
|
|
|
5486
5487
|
t.candidateCount += 1;
|
|
5487
5488
|
t.lastCountedAt = now;
|
|
5488
5489
|
}
|
|
5490
|
+
if (opts.authoritative) t.candidateCount = Math.max(t.candidateCount, this.stableTicks);
|
|
5489
5491
|
if (t.candidateCount < this.stableTicks) return null;
|
|
5490
5492
|
const prev = t.confirmed;
|
|
5491
5493
|
t.confirmed = t.candidate;
|
|
@@ -5542,6 +5544,403 @@ function sendTerminalNotification(opts) {
|
|
|
5542
5544
|
});
|
|
5543
5545
|
}
|
|
5544
5546
|
|
|
5547
|
+
const MIN_HOOK_CLAUDE_VERSION = "2.0.0";
|
|
5548
|
+
const EVENT_FIRST_VERSIONS = [
|
|
5549
|
+
["SessionStart", "2.0.0"],
|
|
5550
|
+
["UserPromptSubmit", "2.0.0"],
|
|
5551
|
+
["Stop", "2.0.0"],
|
|
5552
|
+
["Notification", "2.0.0"],
|
|
5553
|
+
["PostToolUse", "2.0.0"],
|
|
5554
|
+
["SessionEnd", "2.0.0"],
|
|
5555
|
+
["PermissionRequest", "2.0.45"],
|
|
5556
|
+
["PostCompact", "2.1.76"],
|
|
5557
|
+
["StopFailure", "2.1.78"]
|
|
5558
|
+
];
|
|
5559
|
+
const KNOWN_EVENTS = new Set(EVENT_FIRST_VERSIONS.map(([name]) => name));
|
|
5560
|
+
const TOOL_MATCHER_EVENTS = /* @__PURE__ */ new Set(["PostToolUse", "PermissionRequest"]);
|
|
5561
|
+
function parseVersion(v) {
|
|
5562
|
+
const m = typeof v === "string" ? v.trim().match(/^(\d+)\.(\d+)\.(\d+)$/) : null;
|
|
5563
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
5564
|
+
}
|
|
5565
|
+
function versionAtLeast(version, floor) {
|
|
5566
|
+
const a = parseVersion(version);
|
|
5567
|
+
const b = parseVersion(floor);
|
|
5568
|
+
if (!a || !b) return false;
|
|
5569
|
+
for (let i = 0; i < 3; i++) {
|
|
5570
|
+
if (a[i] !== b[i]) return a[i] > b[i];
|
|
5571
|
+
}
|
|
5572
|
+
return true;
|
|
5573
|
+
}
|
|
5574
|
+
function terminalHookEventPlan(claudeVersion) {
|
|
5575
|
+
if (!versionAtLeast(claudeVersion, MIN_HOOK_CLAUDE_VERSION)) return null;
|
|
5576
|
+
return EVENT_FIRST_VERSIONS.filter(([, first]) => versionAtLeast(claudeVersion, first)).map(([name]) => name);
|
|
5577
|
+
}
|
|
5578
|
+
const TERMINAL_HOOK_TIMEOUT_SECONDS = 10;
|
|
5579
|
+
const SAFE_PATH_RE = /^[A-Za-z0-9_./@:+ -]+$/;
|
|
5580
|
+
const SAFE_ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
5581
|
+
function terminalHookCommand(opts) {
|
|
5582
|
+
const { forwarderPath, terminalId, happyHome } = opts;
|
|
5583
|
+
if (!SAFE_PATH_RE.test(forwarderPath) || !SAFE_PATH_RE.test(happyHome) || !SAFE_ID_RE.test(terminalId)) return null;
|
|
5584
|
+
return `[ -f '${forwarderPath}' ] && node '${forwarderPath}' '${terminalId}' '${happyHome}' "$PPID" || true`;
|
|
5585
|
+
}
|
|
5586
|
+
function buildTerminalHookSettings(events, command) {
|
|
5587
|
+
const hooks = {};
|
|
5588
|
+
for (const event of events) {
|
|
5589
|
+
const entry = { type: "command", command, timeout: TERMINAL_HOOK_TIMEOUT_SECONDS };
|
|
5590
|
+
hooks[event] = [TOOL_MATCHER_EVENTS.has(event) ? { matcher: "*", hooks: [entry] } : { hooks: [entry] }];
|
|
5591
|
+
}
|
|
5592
|
+
return { hooks };
|
|
5593
|
+
}
|
|
5594
|
+
function injectClaudeSettingsFlag(command, settingsPath) {
|
|
5595
|
+
if (!SAFE_PATH_RE.test(settingsPath) || !isInjectableClaudeCommand(command)) return command;
|
|
5596
|
+
const m = command.match(/^(\s*)claude/);
|
|
5597
|
+
return `${m[1]}claude --settings '${settingsPath}'${command.slice(m[0].length)}`;
|
|
5598
|
+
}
|
|
5599
|
+
function isInjectableClaudeCommand(command) {
|
|
5600
|
+
return /^\s*claude(?=\s|$)/.test(command) && !/(^|\s)--settings(=|\s|$)/.test(command);
|
|
5601
|
+
}
|
|
5602
|
+
const str = (v, max = 256) => typeof v === "string" && v.length > 0 && v.length <= max ? v : void 0;
|
|
5603
|
+
function parseTerminalAgentHook(body) {
|
|
5604
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return null;
|
|
5605
|
+
const raw = body;
|
|
5606
|
+
const event = raw.hook_event_name;
|
|
5607
|
+
if (typeof event !== "string" || !KNOWN_EVENTS.has(event)) return null;
|
|
5608
|
+
const terminalId = str(raw.terminalId, 64);
|
|
5609
|
+
if (!terminalId || !SAFE_ID_RE.test(terminalId)) return null;
|
|
5610
|
+
const claudeSessionId = str(raw.session_id, 128);
|
|
5611
|
+
if (!claudeSessionId) return null;
|
|
5612
|
+
const pid = typeof raw.claudePid === "number" ? raw.claudePid : typeof raw.claudePid === "string" && /^\d{1,10}$/.test(raw.claudePid) ? Number(raw.claudePid) : void 0;
|
|
5613
|
+
const turnId = str(raw.prompt_id, 128) ?? str(raw.promptHash, 64);
|
|
5614
|
+
return {
|
|
5615
|
+
event,
|
|
5616
|
+
terminalId,
|
|
5617
|
+
claudeSessionId,
|
|
5618
|
+
...pid !== void 0 && Number.isInteger(pid) && pid > 1 ? { claudePid: pid } : {},
|
|
5619
|
+
...turnId ? { turnId } : {},
|
|
5620
|
+
...str(raw.agent_id, 128) ? { agentId: str(raw.agent_id, 128) } : {},
|
|
5621
|
+
...str(raw.notification_type, 64) ? { notificationType: str(raw.notification_type, 64) } : {},
|
|
5622
|
+
...raw.permissionHint === true ? { permissionHint: true } : {},
|
|
5623
|
+
...str(raw.trigger, 32) ? { trigger: str(raw.trigger, 32) } : {},
|
|
5624
|
+
...str(raw.source, 32) ? { source: str(raw.source, 32) } : {},
|
|
5625
|
+
...str(raw.transcript_path, 4096) ? { transcriptPath: str(raw.transcript_path, 4096) } : {}
|
|
5626
|
+
};
|
|
5627
|
+
}
|
|
5628
|
+
const NEEDS_INPUT_NOTIFICATIONS = /* @__PURE__ */ new Set(["permission_prompt", "elicitation_dialog"]);
|
|
5629
|
+
const IDLE_NOTIFICATIONS = /* @__PURE__ */ new Set(["idle_prompt"]);
|
|
5630
|
+
function phaseTo(prev, phase, now, extra = {}) {
|
|
5631
|
+
const { interrupted: _drop, ...rest } = prev;
|
|
5632
|
+
return { ...rest, phase, seq: prev.seq + 1, phaseStartedAt: now, updatedAt: now, ...extra };
|
|
5633
|
+
}
|
|
5634
|
+
function reduceHookRecord(prev, ev, now) {
|
|
5635
|
+
if (ev.event === "SessionStart") {
|
|
5636
|
+
if (prev && prev.claudeSessionId === ev.claudeSessionId && ev.source !== "clear" && ev.source !== "compact") {
|
|
5637
|
+
return { record: { ...prev, updatedAt: now, ...ev.claudePid ? { claudePid: ev.claudePid } : {} }, changed: false };
|
|
5638
|
+
}
|
|
5639
|
+
return {
|
|
5640
|
+
record: {
|
|
5641
|
+
claudeSessionId: ev.claudeSessionId,
|
|
5642
|
+
...ev.claudePid ? { claudePid: ev.claudePid } : prev?.claudePid ? { claudePid: prev.claudePid } : {},
|
|
5643
|
+
phase: "done",
|
|
5644
|
+
seq: (prev?.seq ?? 0) + 1,
|
|
5645
|
+
phaseStartedAt: now,
|
|
5646
|
+
updatedAt: now,
|
|
5647
|
+
confirmedTurn: false,
|
|
5648
|
+
...ev.transcriptPath ? { transcriptPath: ev.transcriptPath } : {}
|
|
5649
|
+
},
|
|
5650
|
+
changed: prev?.phase !== "done"
|
|
5651
|
+
};
|
|
5652
|
+
}
|
|
5653
|
+
if (prev && prev.claudeSessionId !== ev.claudeSessionId) return { record: prev, changed: false };
|
|
5654
|
+
const base = prev ?? {
|
|
5655
|
+
claudeSessionId: ev.claudeSessionId,
|
|
5656
|
+
...ev.claudePid ? { claudePid: ev.claudePid } : {},
|
|
5657
|
+
phase: "done",
|
|
5658
|
+
seq: 0,
|
|
5659
|
+
phaseStartedAt: now,
|
|
5660
|
+
updatedAt: now,
|
|
5661
|
+
confirmedTurn: false
|
|
5662
|
+
};
|
|
5663
|
+
const withPid = ev.claudePid && !base.claudePid ? { ...base, claudePid: ev.claudePid } : base;
|
|
5664
|
+
const fresh = !prev;
|
|
5665
|
+
const renew = () => ({ record: { ...withPid, updatedAt: now }, changed: fresh });
|
|
5666
|
+
switch (ev.event) {
|
|
5667
|
+
case "SessionEnd":
|
|
5668
|
+
return { record: void 0, changed: !!prev };
|
|
5669
|
+
case "UserPromptSubmit":
|
|
5670
|
+
return { record: phaseTo(withPid, "working", now, { confirmedTurn: true, turnId: ev.turnId }), changed: true };
|
|
5671
|
+
case "PostToolUse":
|
|
5672
|
+
if (withPid.phase === "working") return renew();
|
|
5673
|
+
return { record: phaseTo(withPid, "working", now), changed: true };
|
|
5674
|
+
case "PermissionRequest":
|
|
5675
|
+
return { record: phaseTo(withPid, "needs_input", now), changed: withPid.phase !== "needs_input" || fresh };
|
|
5676
|
+
case "Notification": {
|
|
5677
|
+
if (ev.notificationType && IDLE_NOTIFICATIONS.has(ev.notificationType)) {
|
|
5678
|
+
if (withPid.phase === "done") return renew();
|
|
5679
|
+
return { record: phaseTo(withPid, "done", now), changed: true };
|
|
5680
|
+
}
|
|
5681
|
+
const needsInput = ev.notificationType ? NEEDS_INPUT_NOTIFICATIONS.has(ev.notificationType) : ev.permissionHint === true;
|
|
5682
|
+
if (!needsInput) return renew();
|
|
5683
|
+
return { record: phaseTo(withPid, "needs_input", now), changed: withPid.phase !== "needs_input" || fresh };
|
|
5684
|
+
}
|
|
5685
|
+
case "Stop":
|
|
5686
|
+
case "StopFailure":
|
|
5687
|
+
if (ev.agentId) return renew();
|
|
5688
|
+
return { record: phaseTo(withPid, "done", now), changed: withPid.phase !== "done" || fresh };
|
|
5689
|
+
case "PostCompact":
|
|
5690
|
+
if (ev.trigger !== "manual") return renew();
|
|
5691
|
+
return { record: phaseTo(withPid, "done", now), changed: withPid.phase !== "done" || fresh };
|
|
5692
|
+
default:
|
|
5693
|
+
return renew();
|
|
5694
|
+
}
|
|
5695
|
+
}
|
|
5696
|
+
const HOOK_WORKING_STALE_MS = 10 * 6e4;
|
|
5697
|
+
function fuseAgentState(screen, record, now) {
|
|
5698
|
+
const screenOnly = { ...screen ?? {}, source: "screen" };
|
|
5699
|
+
if (!record) return screenOnly;
|
|
5700
|
+
const s = screen?.agentState;
|
|
5701
|
+
if (s === "shell") return screenOnly;
|
|
5702
|
+
if (screen?.agentKind && screen.agentKind !== "claude") return screenOnly;
|
|
5703
|
+
const hook = (agentState) => ({ agentKind: "claude", agentState, source: "hook" });
|
|
5704
|
+
if (s === "needs_input" || s === "working") return { agentKind: "claude", agentState: s, source: "screen" };
|
|
5705
|
+
switch (record.phase) {
|
|
5706
|
+
case "working":
|
|
5707
|
+
case "needs_input":
|
|
5708
|
+
if (now - record.updatedAt > HOOK_WORKING_STALE_MS) return screenOnly;
|
|
5709
|
+
return hook(record.phase);
|
|
5710
|
+
case "done":
|
|
5711
|
+
return hook("idle");
|
|
5712
|
+
}
|
|
5713
|
+
}
|
|
5714
|
+
const INTERRUPT_CHECK_DELAYS_MS = [600, 2e3, 5e3];
|
|
5715
|
+
const INTERRUPT_CONFIRM_WINDOW_MS = INTERRUPT_CHECK_DELAYS_MS[INTERRUPT_CHECK_DELAYS_MS.length - 1] + 1500;
|
|
5716
|
+
const KITTY_ESCAPE_PRESS_RE = /^\x1b\[27(?:;1(?::1)?)?u$/;
|
|
5717
|
+
function isInterruptInput(text) {
|
|
5718
|
+
return text === "\x1B" || text === "" || KITTY_ESCAPE_PRESS_RE.test(text);
|
|
5719
|
+
}
|
|
5720
|
+
function isDialogAnswerInput(text) {
|
|
5721
|
+
return text === "\r" || text === "\n" || text === "\r\n";
|
|
5722
|
+
}
|
|
5723
|
+
function armsInterruptCheck(text, phase) {
|
|
5724
|
+
if (phase === "working") return isInterruptInput(text);
|
|
5725
|
+
if (phase === "needs_input") return isInterruptInput(text) || isDialogAnswerInput(text);
|
|
5726
|
+
return false;
|
|
5727
|
+
}
|
|
5728
|
+
function pendingInterruptFor(record, now) {
|
|
5729
|
+
if (!record || !record.confirmedTurn) return null;
|
|
5730
|
+
if (record.phase !== "working" && record.phase !== "needs_input") return null;
|
|
5731
|
+
return {
|
|
5732
|
+
claudeSessionId: record.claudeSessionId,
|
|
5733
|
+
seq: record.seq,
|
|
5734
|
+
phaseStartedAt: record.phaseStartedAt,
|
|
5735
|
+
...record.turnId ? { turnId: record.turnId } : {},
|
|
5736
|
+
at: now
|
|
5737
|
+
};
|
|
5738
|
+
}
|
|
5739
|
+
function decideInterrupt(record, pending, screen, now) {
|
|
5740
|
+
if (!record || !record.confirmedTurn) return "drop";
|
|
5741
|
+
if (record.phase !== "working" && record.phase !== "needs_input") return "drop";
|
|
5742
|
+
if (record.claudeSessionId !== pending.claudeSessionId || record.seq !== pending.seq || record.phaseStartedAt !== pending.phaseStartedAt || record.turnId !== pending.turnId) return "drop";
|
|
5743
|
+
if (now - record.updatedAt > HOOK_WORKING_STALE_MS) return "drop";
|
|
5744
|
+
if (now - pending.at > INTERRUPT_CONFIRM_WINDOW_MS) return "drop";
|
|
5745
|
+
if (screen?.agentKind === "claude" && screen.agentState === "idle") return "synthesize";
|
|
5746
|
+
return "wait";
|
|
5747
|
+
}
|
|
5748
|
+
function synthesizeInterruptDone(record, now) {
|
|
5749
|
+
return { ...phaseTo(record, "done", now), interrupted: true };
|
|
5750
|
+
}
|
|
5751
|
+
|
|
5752
|
+
function isPidAlive(pid) {
|
|
5753
|
+
try {
|
|
5754
|
+
process.kill(pid, 0);
|
|
5755
|
+
return true;
|
|
5756
|
+
} catch (error) {
|
|
5757
|
+
return error?.code === "EPERM";
|
|
5758
|
+
}
|
|
5759
|
+
}
|
|
5760
|
+
class TerminalAgentHookStore {
|
|
5761
|
+
constructor(pidAlive = isPidAlive) {
|
|
5762
|
+
this.pidAlive = pidAlive;
|
|
5763
|
+
}
|
|
5764
|
+
records = /* @__PURE__ */ new Map();
|
|
5765
|
+
pending = /* @__PURE__ */ new Map();
|
|
5766
|
+
/** Apply one forwarded hook body. Returns the terminal whose list-relevant
|
|
5767
|
+
* state changed (caller kicks a list refresh), or null. Never throws. */
|
|
5768
|
+
applyHook(body, now) {
|
|
5769
|
+
const ev = parseTerminalAgentHook(body);
|
|
5770
|
+
if (!ev) return null;
|
|
5771
|
+
const prev = this.valid(ev.terminalId);
|
|
5772
|
+
const { record, changed } = reduceHookRecord(prev, ev, now);
|
|
5773
|
+
if (record) this.records.set(ev.terminalId, record);
|
|
5774
|
+
else this.records.delete(ev.terminalId);
|
|
5775
|
+
if (record?.seq !== prev?.seq) this.pending.delete(ev.terminalId);
|
|
5776
|
+
return { terminalId: ev.terminalId, changed };
|
|
5777
|
+
}
|
|
5778
|
+
/** The record if its claude process is still alive; dead ones are dropped. */
|
|
5779
|
+
valid(terminalId) {
|
|
5780
|
+
const record = this.records.get(terminalId);
|
|
5781
|
+
if (!record) return void 0;
|
|
5782
|
+
if (record.claudePid !== void 0 && !this.pidAlive(record.claudePid)) {
|
|
5783
|
+
this.records.delete(terminalId);
|
|
5784
|
+
this.pending.delete(terminalId);
|
|
5785
|
+
return void 0;
|
|
5786
|
+
}
|
|
5787
|
+
return record;
|
|
5788
|
+
}
|
|
5789
|
+
/** Screen observation (undefined = probe failed) → fused state. */
|
|
5790
|
+
fuse(terminalId, screen, now) {
|
|
5791
|
+
return fuseAgentState(screen, this.valid(terminalId), now);
|
|
5792
|
+
}
|
|
5793
|
+
/** A web keystroke reached the pane. True when it armed an inference
|
|
5794
|
+
* check (Esc/Ctrl+C during a turn; also Enter on a dialog). */
|
|
5795
|
+
noteInput(terminalId, text, now) {
|
|
5796
|
+
const record = this.records.get(terminalId);
|
|
5797
|
+
if (!record || !armsInterruptCheck(text, record.phase)) return false;
|
|
5798
|
+
const pending = pendingInterruptFor(this.valid(terminalId), now);
|
|
5799
|
+
if (!pending) return false;
|
|
5800
|
+
this.pending.set(terminalId, pending);
|
|
5801
|
+
return true;
|
|
5802
|
+
}
|
|
5803
|
+
/** Re-check an armed interrupt against a fresh screen observation. */
|
|
5804
|
+
checkInterrupt(terminalId, screen, now) {
|
|
5805
|
+
const pending = this.pending.get(terminalId);
|
|
5806
|
+
if (!pending) return "none";
|
|
5807
|
+
const record = this.valid(terminalId);
|
|
5808
|
+
const verdict = decideInterrupt(record, pending, screen, now);
|
|
5809
|
+
if (verdict === "wait") return "wait";
|
|
5810
|
+
this.pending.delete(terminalId);
|
|
5811
|
+
if (verdict === "drop" || !record) return "drop";
|
|
5812
|
+
this.records.set(terminalId, synthesizeInterruptDone(record, now));
|
|
5813
|
+
return "synthesized";
|
|
5814
|
+
}
|
|
5815
|
+
has(terminalId) {
|
|
5816
|
+
return this.records.has(terminalId);
|
|
5817
|
+
}
|
|
5818
|
+
forget(terminalId) {
|
|
5819
|
+
this.records.delete(terminalId);
|
|
5820
|
+
this.pending.delete(terminalId);
|
|
5821
|
+
}
|
|
5822
|
+
/** Drop records of terminals that no longer exist. */
|
|
5823
|
+
prune(liveIds) {
|
|
5824
|
+
const keep = new Set(liveIds);
|
|
5825
|
+
for (const id of this.records.keys()) if (!keep.has(id)) this.forget(id);
|
|
5826
|
+
for (const id of this.pending.keys()) if (!keep.has(id)) this.pending.delete(id);
|
|
5827
|
+
}
|
|
5828
|
+
}
|
|
5829
|
+
|
|
5830
|
+
const TERMINAL_STATUS_FORWARDER_BASENAME = "terminal_status_forwarder.cjs";
|
|
5831
|
+
const VERSION_TTL_MS = 6 * 60 * 6e4;
|
|
5832
|
+
const VERSION_RETRY_MS = 5 * 6e4;
|
|
5833
|
+
function hooksDir() {
|
|
5834
|
+
return path.join(configuration.configuration.happyHomeDir, "tmp", "terminal-hooks");
|
|
5835
|
+
}
|
|
5836
|
+
function terminalHookSettingsPath(terminalId) {
|
|
5837
|
+
return path.join(hooksDir(), `${terminalId}.json`);
|
|
5838
|
+
}
|
|
5839
|
+
function probeClaudeVersion(env) {
|
|
5840
|
+
return new Promise((resolveVersion) => {
|
|
5841
|
+
try {
|
|
5842
|
+
node_child_process.execFile("claude", ["--version"], { timeout: 5e3, maxBuffer: 32 * 1024, env }, (error, stdout) => {
|
|
5843
|
+
resolveVersion(error ? null : mirrorProtocol.stableVersion(String(stdout)));
|
|
5844
|
+
});
|
|
5845
|
+
} catch {
|
|
5846
|
+
resolveVersion(null);
|
|
5847
|
+
}
|
|
5848
|
+
});
|
|
5849
|
+
}
|
|
5850
|
+
class ClaudeVersionCache {
|
|
5851
|
+
constructor(env, probe = probeClaudeVersion) {
|
|
5852
|
+
this.env = env;
|
|
5853
|
+
this.probe = probe;
|
|
5854
|
+
}
|
|
5855
|
+
version = null;
|
|
5856
|
+
checkedAt = 0;
|
|
5857
|
+
inFlight = null;
|
|
5858
|
+
started = false;
|
|
5859
|
+
start(now = Date.now()) {
|
|
5860
|
+
this.started = true;
|
|
5861
|
+
if (this.version === null || now - this.checkedAt > VERSION_TTL_MS) this.refresh(now);
|
|
5862
|
+
}
|
|
5863
|
+
current(now = Date.now()) {
|
|
5864
|
+
if (!this.started) return null;
|
|
5865
|
+
const ttl = this.version ? VERSION_TTL_MS : VERSION_RETRY_MS;
|
|
5866
|
+
if (now - this.checkedAt > ttl) this.refresh(now);
|
|
5867
|
+
return this.version;
|
|
5868
|
+
}
|
|
5869
|
+
/** current(), but waits (bounded) for a probe already in flight —
|
|
5870
|
+
* for paths that are async anyway (auto-restore). */
|
|
5871
|
+
async settled(timeoutMs) {
|
|
5872
|
+
const pending = this.inFlight;
|
|
5873
|
+
if (pending) {
|
|
5874
|
+
let timer;
|
|
5875
|
+
await Promise.race([pending, new Promise((r) => {
|
|
5876
|
+
timer = setTimeout(r, timeoutMs);
|
|
5877
|
+
})]);
|
|
5878
|
+
if (timer) clearTimeout(timer);
|
|
5879
|
+
}
|
|
5880
|
+
return this.current();
|
|
5881
|
+
}
|
|
5882
|
+
refresh(now) {
|
|
5883
|
+
if (this.inFlight) return;
|
|
5884
|
+
this.checkedAt = now;
|
|
5885
|
+
this.inFlight = this.probe(this.env()).then((parsed) => {
|
|
5886
|
+
this.checkedAt = Date.now();
|
|
5887
|
+
if (parsed) this.version = parsed;
|
|
5888
|
+
logger.logger.debug(`[TERMINAL HOOKS] claude --version \u2192 ${parsed ?? "unresolved"}`);
|
|
5889
|
+
}, () => void 0).finally(() => {
|
|
5890
|
+
this.inFlight = null;
|
|
5891
|
+
});
|
|
5892
|
+
}
|
|
5893
|
+
}
|
|
5894
|
+
function withTerminalHooks(command, terminalId, claudeVersion) {
|
|
5895
|
+
if (!isInjectableClaudeCommand(command)) return command;
|
|
5896
|
+
const events = terminalHookEventPlan(claudeVersion);
|
|
5897
|
+
if (!events) return command;
|
|
5898
|
+
const hookCommand = terminalHookCommand({
|
|
5899
|
+
forwarderPath: path.resolve(projectPath.projectPath(), "scripts", TERMINAL_STATUS_FORWARDER_BASENAME),
|
|
5900
|
+
terminalId,
|
|
5901
|
+
happyHome: configuration.configuration.happyHomeDir
|
|
5902
|
+
});
|
|
5903
|
+
if (!hookCommand) return command;
|
|
5904
|
+
const path$1 = terminalHookSettingsPath(terminalId);
|
|
5905
|
+
try {
|
|
5906
|
+
node_fs.mkdirSync(hooksDir(), { recursive: true });
|
|
5907
|
+
node_fs.writeFileSync(path$1, JSON.stringify(buildTerminalHookSettings(events, hookCommand), null, 2));
|
|
5908
|
+
} catch (error) {
|
|
5909
|
+
logger.logger.debug(`[TERMINAL HOOKS] settings write failed for ${terminalId}: ${error}`);
|
|
5910
|
+
return command;
|
|
5911
|
+
}
|
|
5912
|
+
const injected = injectClaudeSettingsFlag(command, path$1);
|
|
5913
|
+
if (injected !== command) logger.logger.debug(`[TERMINAL HOOKS] hooks injected for ${terminalId} (claude ${claudeVersion}, ${events.length} events)`);
|
|
5914
|
+
return injected;
|
|
5915
|
+
}
|
|
5916
|
+
function removeTerminalHookSettings(terminalId) {
|
|
5917
|
+
try {
|
|
5918
|
+
const path = terminalHookSettingsPath(terminalId);
|
|
5919
|
+
if (node_fs.existsSync(path)) node_fs.unlinkSync(path);
|
|
5920
|
+
} catch {
|
|
5921
|
+
}
|
|
5922
|
+
}
|
|
5923
|
+
function sweepStaleTerminalHookSettings(isLive) {
|
|
5924
|
+
const removed = [];
|
|
5925
|
+
let names;
|
|
5926
|
+
try {
|
|
5927
|
+
names = node_fs.readdirSync(hooksDir());
|
|
5928
|
+
} catch {
|
|
5929
|
+
return removed;
|
|
5930
|
+
}
|
|
5931
|
+
for (const name of names) {
|
|
5932
|
+
const m = name.match(/^([A-Za-z0-9_-]{1,64})\.json$/);
|
|
5933
|
+
if (!m) continue;
|
|
5934
|
+
try {
|
|
5935
|
+
if (isLive(m[1])) continue;
|
|
5936
|
+
node_fs.unlinkSync(path.join(hooksDir(), name));
|
|
5937
|
+
removed.push(m[1]);
|
|
5938
|
+
} catch {
|
|
5939
|
+
}
|
|
5940
|
+
}
|
|
5941
|
+
return removed;
|
|
5942
|
+
}
|
|
5943
|
+
|
|
5545
5944
|
const AUTO_RESTORE_DEFAULT_MAX = 6;
|
|
5546
5945
|
const AUTO_RESTORE_DEFAULT_WINDOW_HOURS = 24;
|
|
5547
5946
|
const AUTO_RESTORE_HARD_MAX = 20;
|
|
@@ -5692,6 +6091,626 @@ function directTerminalItems(sessions, tmuxIds) {
|
|
|
5692
6091
|
return out.sort((a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
5693
6092
|
}
|
|
5694
6093
|
|
|
6094
|
+
const SCROLLBACK_FILE_MAX_BYTES = 512 * 1024;
|
|
6095
|
+
const SCROLLBACK_TOTAL_MAX_BYTES = 64 * 1024 * 1024;
|
|
6096
|
+
const SCROLLBACK_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
6097
|
+
const SCROLLBACK_CAPTURE_LINES = 5e3;
|
|
6098
|
+
const CHECKPOINT_QUIET_MS = 3e3;
|
|
6099
|
+
const CHECKPOINT_MIN_INTERVAL_MS = 3e4;
|
|
6100
|
+
const CHECKPOINT_MAX_DIRTY_MS = 12e4;
|
|
6101
|
+
const CHECKPOINTS_PER_TICK = 4;
|
|
6102
|
+
const SCROLLBACK_PERSIST_DEFAULT = true;
|
|
6103
|
+
function resolveScrollbackConfig(raw) {
|
|
6104
|
+
const v = raw?.terminalScrollbackPersist;
|
|
6105
|
+
return { enabled: typeof v === "boolean" ? v : SCROLLBACK_PERSIST_DEFAULT };
|
|
6106
|
+
}
|
|
6107
|
+
const ESC = 27;
|
|
6108
|
+
const SGR_RESET = Buffer.from("\x1B[0m", "ascii");
|
|
6109
|
+
const CRLF = Buffer.from("\r\n", "ascii");
|
|
6110
|
+
function sanitizeArchivedScrollback(input) {
|
|
6111
|
+
const out = Buffer.allocUnsafe(input.length);
|
|
6112
|
+
let o = 0;
|
|
6113
|
+
const n = input.length;
|
|
6114
|
+
let i = 0;
|
|
6115
|
+
while (i < n) {
|
|
6116
|
+
const b = input[i];
|
|
6117
|
+
if (b === ESC) {
|
|
6118
|
+
const next = i + 1 < n ? input[i + 1] : -1;
|
|
6119
|
+
if (next === 91) {
|
|
6120
|
+
let j = i + 2;
|
|
6121
|
+
let paramsOk = true;
|
|
6122
|
+
while (j < n && input[j] >= 48 && input[j] <= 63) {
|
|
6123
|
+
const c = input[j];
|
|
6124
|
+
if (!(c >= 48 && c <= 57 || c === 59 || c === 58)) paramsOk = false;
|
|
6125
|
+
j++;
|
|
6126
|
+
}
|
|
6127
|
+
let intermediates = false;
|
|
6128
|
+
while (j < n && input[j] >= 32 && input[j] <= 47) {
|
|
6129
|
+
intermediates = true;
|
|
6130
|
+
j++;
|
|
6131
|
+
}
|
|
6132
|
+
if (j < n && input[j] >= 64 && input[j] <= 126) {
|
|
6133
|
+
if (input[j] === 109 && paramsOk && !intermediates) {
|
|
6134
|
+
input.copy(out, o, i, j + 1);
|
|
6135
|
+
o += j + 1 - i;
|
|
6136
|
+
}
|
|
6137
|
+
i = j + 1;
|
|
6138
|
+
} else {
|
|
6139
|
+
i = skipToLineEnd(input, j);
|
|
6140
|
+
}
|
|
6141
|
+
continue;
|
|
6142
|
+
}
|
|
6143
|
+
if (next === 93 || next === 80 || next === 95 || next === 94 || next === 88) {
|
|
6144
|
+
let j = i + 2;
|
|
6145
|
+
while (j < n) {
|
|
6146
|
+
if (input[j] === 7 && next === 93) {
|
|
6147
|
+
j += 1;
|
|
6148
|
+
break;
|
|
6149
|
+
}
|
|
6150
|
+
if (input[j] === ESC && j + 1 < n && input[j + 1] === 92) {
|
|
6151
|
+
j += 2;
|
|
6152
|
+
break;
|
|
6153
|
+
}
|
|
6154
|
+
if (input[j] === 10) break;
|
|
6155
|
+
j++;
|
|
6156
|
+
}
|
|
6157
|
+
i = j;
|
|
6158
|
+
continue;
|
|
6159
|
+
}
|
|
6160
|
+
i += next === -1 ? 1 : 2;
|
|
6161
|
+
continue;
|
|
6162
|
+
}
|
|
6163
|
+
if (b < 32 && b !== 9 && b !== 10 && b !== 13) {
|
|
6164
|
+
i++;
|
|
6165
|
+
continue;
|
|
6166
|
+
}
|
|
6167
|
+
if (b === 127) {
|
|
6168
|
+
i++;
|
|
6169
|
+
continue;
|
|
6170
|
+
}
|
|
6171
|
+
if (b === 194 && i + 1 < n && input[i + 1] >= 128 && input[i + 1] <= 159) {
|
|
6172
|
+
i += 2;
|
|
6173
|
+
continue;
|
|
6174
|
+
}
|
|
6175
|
+
out[o++] = b;
|
|
6176
|
+
i++;
|
|
6177
|
+
}
|
|
6178
|
+
return out.subarray(0, o);
|
|
6179
|
+
}
|
|
6180
|
+
function skipToLineEnd(buf, from) {
|
|
6181
|
+
const nl = buf.indexOf(10, from);
|
|
6182
|
+
return nl === -1 ? buf.length : nl;
|
|
6183
|
+
}
|
|
6184
|
+
function splitCrlfLines(blob) {
|
|
6185
|
+
const out = [];
|
|
6186
|
+
let start = 0;
|
|
6187
|
+
for (let i = 0; i < blob.length; i++) {
|
|
6188
|
+
if (blob[i] !== 10) continue;
|
|
6189
|
+
let end = i;
|
|
6190
|
+
if (end > start && blob[end - 1] === 13) end -= 1;
|
|
6191
|
+
out.push(blob.subarray(start, end));
|
|
6192
|
+
start = i + 1;
|
|
6193
|
+
}
|
|
6194
|
+
if (start < blob.length) out.push(blob.subarray(start));
|
|
6195
|
+
return out;
|
|
6196
|
+
}
|
|
6197
|
+
function isBlankRow(row) {
|
|
6198
|
+
return /^(?:\x1b\[[0-9;:]*m| )*$/.test(row.toString("latin1"));
|
|
6199
|
+
}
|
|
6200
|
+
function joinCrlf(lines) {
|
|
6201
|
+
const parts = [];
|
|
6202
|
+
lines.forEach((l, i) => {
|
|
6203
|
+
if (i > 0) parts.push(CRLF);
|
|
6204
|
+
parts.push(l);
|
|
6205
|
+
});
|
|
6206
|
+
return Buffer.concat(parts);
|
|
6207
|
+
}
|
|
6208
|
+
function buildCheckpointBody(parts, budget = SCROLLBACK_FILE_MAX_BYTES) {
|
|
6209
|
+
const cleaned = [];
|
|
6210
|
+
for (const part of parts) {
|
|
6211
|
+
const lines = splitCrlfLines(normalizeCaptureLines(sanitizeArchivedScrollback(part)));
|
|
6212
|
+
while (lines.length > 0 && isBlankRow(lines[lines.length - 1])) lines.pop();
|
|
6213
|
+
if (lines.length > 0) cleaned.push(joinCrlf(lines));
|
|
6214
|
+
}
|
|
6215
|
+
const joined = [];
|
|
6216
|
+
cleaned.forEach((c, i) => {
|
|
6217
|
+
if (i > 0) joined.push(SGR_RESET, CRLF);
|
|
6218
|
+
joined.push(c);
|
|
6219
|
+
});
|
|
6220
|
+
return truncateToBudget(Buffer.concat(joined), budget);
|
|
6221
|
+
}
|
|
6222
|
+
const FILE_MAGIC = "VHSB1";
|
|
6223
|
+
function encodeArchiveFile(file) {
|
|
6224
|
+
return Buffer.concat([Buffer.from(`${FILE_MAGIC} ${Math.floor(file.savedAt)}
|
|
6225
|
+
`, "ascii"), file.body]);
|
|
6226
|
+
}
|
|
6227
|
+
function decodeArchiveFile(raw) {
|
|
6228
|
+
if (!raw || raw.length === 0) return null;
|
|
6229
|
+
const nl = raw.indexOf(10);
|
|
6230
|
+
if (nl <= 0 || nl > 64) return null;
|
|
6231
|
+
const m = /^VHSB1 (\d{1,16})$/.exec(raw.subarray(0, nl).toString("ascii"));
|
|
6232
|
+
if (!m) return null;
|
|
6233
|
+
const savedAt = Number(m[1]);
|
|
6234
|
+
if (!Number.isFinite(savedAt) || savedAt <= 0) return null;
|
|
6235
|
+
const body = truncateToBudget(sanitizeArchivedScrollback(raw.subarray(nl + 1)), SCROLLBACK_FILE_MAX_BYTES);
|
|
6236
|
+
if (body.length === 0) return null;
|
|
6237
|
+
return { savedAt, body };
|
|
6238
|
+
}
|
|
6239
|
+
const DIM = "\x1B[0;2m";
|
|
6240
|
+
const RESET = "\x1B[0m";
|
|
6241
|
+
const REBUILD_SEPARATOR = Buffer.from(`${DIM}\u2500\u2500 \u91CD\u5EFA \xB7 rebuilt \u2500\u2500${RESET}`, "utf8");
|
|
6242
|
+
function chainArchive(prev, live, budget = SCROLLBACK_FILE_MAX_BYTES) {
|
|
6243
|
+
if (!prev) return live ? { savedAt: live.savedAt, body: truncateToBudget(live.body, budget) } : null;
|
|
6244
|
+
if (!live) return { savedAt: prev.savedAt, body: truncateToBudget(prev.body, budget) };
|
|
6245
|
+
const body = Buffer.concat([prev.body, SGR_RESET, CRLF, REBUILD_SEPARATOR, CRLF, live.body]);
|
|
6246
|
+
return { savedAt: Math.max(prev.savedAt, live.savedAt), body: truncateToBudget(body, budget) };
|
|
6247
|
+
}
|
|
6248
|
+
function pad2(n) {
|
|
6249
|
+
return n < 10 ? `0${n}` : String(n);
|
|
6250
|
+
}
|
|
6251
|
+
function formatSavedAt(savedAt) {
|
|
6252
|
+
const d = new Date(savedAt);
|
|
6253
|
+
return `${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
|
|
6254
|
+
}
|
|
6255
|
+
function archiveHeaderLine(savedAt) {
|
|
6256
|
+
return Buffer.from(`${DIM}\u2500\u2500 \u4EE5\u4E0B\u4E3A\u4E0A\u6B21\u4F1A\u8BDD\u7684\u8F93\u51FA \xB7 previous session (${formatSavedAt(savedAt)}) \u2500\u2500${RESET}`, "utf8");
|
|
6257
|
+
}
|
|
6258
|
+
const ARCHIVE_FOOTER_LINE = Buffer.from(`${DIM}\u2500\u2500 \u4EE5\u4E0A\u4E3A\u4E0A\u6B21\u4F1A\u8BDD\u7684\u8F93\u51FA \xB7 \u7EC8\u7AEF\u5DF2\u91CD\u5EFA\uFF0C\u8FDB\u7A0B\u662F\u65B0\u7684 \u2500\u2500${RESET}`, "utf8");
|
|
6259
|
+
function withArchivedScrollback(payload, archive) {
|
|
6260
|
+
if (!archive || archive.body.length === 0) return payload;
|
|
6261
|
+
const full = Buffer.concat([
|
|
6262
|
+
SGR_RESET,
|
|
6263
|
+
archiveHeaderLine(archive.savedAt),
|
|
6264
|
+
CRLF,
|
|
6265
|
+
archive.body,
|
|
6266
|
+
SGR_RESET,
|
|
6267
|
+
CRLF,
|
|
6268
|
+
ARCHIVE_FOOTER_LINE,
|
|
6269
|
+
CRLF,
|
|
6270
|
+
payload.full
|
|
6271
|
+
]);
|
|
6272
|
+
return { ...payload, full };
|
|
6273
|
+
}
|
|
6274
|
+
const CHECKPOINT_PROBE_FORMAT = "#{session_name}|#{window_activity}|#{alternate_on}|#{@vh_attach}";
|
|
6275
|
+
const ID_RE = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
6276
|
+
function parseCheckpointProbe(stdout) {
|
|
6277
|
+
const out = [];
|
|
6278
|
+
for (const line of stdout.split("\n")) {
|
|
6279
|
+
const parts = line.split("|");
|
|
6280
|
+
if (parts.length < 4) continue;
|
|
6281
|
+
const [name, activity, alt] = parts;
|
|
6282
|
+
const attach = parts.slice(3).join("|");
|
|
6283
|
+
if (!name.startsWith("vh-")) continue;
|
|
6284
|
+
const id = name.slice(3);
|
|
6285
|
+
if (!ID_RE.test(id)) continue;
|
|
6286
|
+
if (attach.trim().length > 0) continue;
|
|
6287
|
+
const secs = Number(activity);
|
|
6288
|
+
if (!Number.isFinite(secs) || secs <= 0) continue;
|
|
6289
|
+
out.push({ id, activityAt: secs * 1e3, alternateOn: alt === "1" });
|
|
6290
|
+
}
|
|
6291
|
+
return out;
|
|
6292
|
+
}
|
|
6293
|
+
const ACTIVITY_RESOLUTION_MS = 1e3;
|
|
6294
|
+
function planCheckpoints(terminals, state, now, opts = {}) {
|
|
6295
|
+
const quietMs = opts.quietMs ?? CHECKPOINT_QUIET_MS;
|
|
6296
|
+
const minIntervalMs = opts.minIntervalMs ?? CHECKPOINT_MIN_INTERVAL_MS;
|
|
6297
|
+
const maxDirtyMs = opts.maxDirtyMs ?? CHECKPOINT_MAX_DIRTY_MS;
|
|
6298
|
+
const max = opts.max ?? CHECKPOINTS_PER_TICK;
|
|
6299
|
+
const due = [];
|
|
6300
|
+
for (const t of terminals) {
|
|
6301
|
+
const prev = state.get(t.id);
|
|
6302
|
+
if (prev && t.activityAt + ACTIVITY_RESOLUTION_MS <= prev.at) continue;
|
|
6303
|
+
const lastAt = prev?.at ?? 0;
|
|
6304
|
+
const quiet = now - t.activityAt >= quietMs && now - lastAt >= minIntervalMs;
|
|
6305
|
+
const overdue = now - lastAt >= maxDirtyMs;
|
|
6306
|
+
if (quiet || overdue) due.push({ id: t.id, lastAt });
|
|
6307
|
+
}
|
|
6308
|
+
due.sort((a, b) => a.lastAt - b.lastAt || (a.id < b.id ? -1 : 1));
|
|
6309
|
+
return due.slice(0, Math.max(0, max)).map((d) => d.id);
|
|
6310
|
+
}
|
|
6311
|
+
function checkpointCaptureArgs(id, alternateOn, lines = SCROLLBACK_CAPTURE_LINES) {
|
|
6312
|
+
const t = `=vh-${id}:`;
|
|
6313
|
+
const n = Math.max(1, Math.floor(lines));
|
|
6314
|
+
if (!alternateOn) return [["capture-pane", "-peqJN", "-t", t, "-S", `-${n}`]];
|
|
6315
|
+
return [
|
|
6316
|
+
["capture-pane", "-peqJN", "-t", t, "-S", `-${n}`, "-E", "-1"],
|
|
6317
|
+
["capture-pane", "-peqJN", "-t", t, "-a"]
|
|
6318
|
+
];
|
|
6319
|
+
}
|
|
6320
|
+
const FILE_RE = /^([a-zA-Z0-9_-]{1,64})\.(live|prev)\.vhsb$/;
|
|
6321
|
+
function archiveFileName(id, kind) {
|
|
6322
|
+
if (!ID_RE.test(id)) throw new Error(`invalid terminal id: ${id}`);
|
|
6323
|
+
return `${id}.${kind}.vhsb`;
|
|
6324
|
+
}
|
|
6325
|
+
function parseArchiveFileName(name) {
|
|
6326
|
+
const m = FILE_RE.exec(name);
|
|
6327
|
+
return m ? { id: m[1], kind: m[2] } : null;
|
|
6328
|
+
}
|
|
6329
|
+
function planScrollbackGc(files, keepIds, now, ttlMs = SCROLLBACK_TTL_MS, totalMax = SCROLLBACK_TOTAL_MAX_BYTES) {
|
|
6330
|
+
const doomed = /* @__PURE__ */ new Set();
|
|
6331
|
+
for (const f of files) {
|
|
6332
|
+
if (!f.id || !f.kind) {
|
|
6333
|
+
doomed.add(f.name);
|
|
6334
|
+
continue;
|
|
6335
|
+
}
|
|
6336
|
+
if (now - f.mtimeMs > ttlMs) {
|
|
6337
|
+
doomed.add(f.name);
|
|
6338
|
+
continue;
|
|
6339
|
+
}
|
|
6340
|
+
if (keepIds && !keepIds.has(f.id)) doomed.add(f.name);
|
|
6341
|
+
}
|
|
6342
|
+
const survivors = files.filter((f) => !doomed.has(f.name)).sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
6343
|
+
let total = survivors.reduce((s, f) => s + f.bytes, 0);
|
|
6344
|
+
for (const f of survivors) {
|
|
6345
|
+
if (total <= totalMax) break;
|
|
6346
|
+
doomed.add(f.name);
|
|
6347
|
+
total -= f.bytes;
|
|
6348
|
+
}
|
|
6349
|
+
return [...doomed];
|
|
6350
|
+
}
|
|
6351
|
+
|
|
6352
|
+
const CONFIG_REFRESH_MS = 6e4;
|
|
6353
|
+
const GC_INTERVAL_MS = 10 * 6e4;
|
|
6354
|
+
const SHUTDOWN_FLUSH_BUDGET_MS = 300;
|
|
6355
|
+
const KILL_CHECKPOINT_BUDGET_MS = 300;
|
|
6356
|
+
const TMUX_TIMEOUT_MS = 5e3;
|
|
6357
|
+
const TMUX_MAX_BUFFER = 32 * 1024 * 1024;
|
|
6358
|
+
function defaultTmux(env) {
|
|
6359
|
+
return {
|
|
6360
|
+
run: (args, timeoutMs) => new Promise((resolve) => {
|
|
6361
|
+
try {
|
|
6362
|
+
node_child_process.execFile(
|
|
6363
|
+
"tmux",
|
|
6364
|
+
tmuxSocket.tmuxArgs(args),
|
|
6365
|
+
{ env: env(), encoding: "buffer", timeout: Math.max(1, timeoutMs ?? TMUX_TIMEOUT_MS), maxBuffer: TMUX_MAX_BUFFER },
|
|
6366
|
+
(err, stdout) => resolve(err ? null : stdout)
|
|
6367
|
+
);
|
|
6368
|
+
} catch {
|
|
6369
|
+
resolve(null);
|
|
6370
|
+
}
|
|
6371
|
+
}),
|
|
6372
|
+
runSync: (args, timeoutMs) => {
|
|
6373
|
+
try {
|
|
6374
|
+
const r = node_child_process.spawnSync("tmux", tmuxSocket.tmuxArgs(args), { env: env(), timeout: Math.max(1, timeoutMs), maxBuffer: TMUX_MAX_BUFFER });
|
|
6375
|
+
return r.status === 0 && r.stdout ? r.stdout : null;
|
|
6376
|
+
} catch {
|
|
6377
|
+
return null;
|
|
6378
|
+
}
|
|
6379
|
+
}
|
|
6380
|
+
};
|
|
6381
|
+
}
|
|
6382
|
+
class ScrollbackCheckpointer {
|
|
6383
|
+
opts;
|
|
6384
|
+
tmux;
|
|
6385
|
+
now;
|
|
6386
|
+
readConfig;
|
|
6387
|
+
/** Cached switch; the default until settings were read once. */
|
|
6388
|
+
enabled = SCROLLBACK_PERSIST_DEFAULT;
|
|
6389
|
+
configReadAt = 0;
|
|
6390
|
+
lastGcAt = 0;
|
|
6391
|
+
busy = false;
|
|
6392
|
+
/** Set once a tick ran in this process — the shutdown flush is a no-op
|
|
6393
|
+
* before that (unit tests that only construct+stop never touch tmux). */
|
|
6394
|
+
started = false;
|
|
6395
|
+
stopped = false;
|
|
6396
|
+
state = /* @__PURE__ */ new Map();
|
|
6397
|
+
/** Bumped by promote(): an in-flight capture of an earlier generation must
|
|
6398
|
+
* not overwrite `.live` after the promotion moved it to `.prev`. */
|
|
6399
|
+
generation = /* @__PURE__ */ new Map();
|
|
6400
|
+
constructor(opts) {
|
|
6401
|
+
this.opts = opts;
|
|
6402
|
+
this.tmux = opts.tmux ?? defaultTmux(opts.env);
|
|
6403
|
+
this.now = opts.now ?? Date.now;
|
|
6404
|
+
this.readConfig = opts.readConfig ?? (() => {
|
|
6405
|
+
try {
|
|
6406
|
+
return resolveScrollbackConfig(JSON.parse(node_fs.readFileSync(path.join(this.opts.homeDir(), "settings.json"), "utf8")));
|
|
6407
|
+
} catch {
|
|
6408
|
+
return resolveScrollbackConfig(null);
|
|
6409
|
+
}
|
|
6410
|
+
});
|
|
6411
|
+
}
|
|
6412
|
+
dir() {
|
|
6413
|
+
return path.join(this.opts.homeDir(), "terminal-scrollback");
|
|
6414
|
+
}
|
|
6415
|
+
filePath(id, kind) {
|
|
6416
|
+
return path.join(this.dir(), archiveFileName(id, kind));
|
|
6417
|
+
}
|
|
6418
|
+
isEnabled() {
|
|
6419
|
+
this.refreshConfig();
|
|
6420
|
+
return this.enabled;
|
|
6421
|
+
}
|
|
6422
|
+
// ── tick ────────────────────────────────────────────────────────────────
|
|
6423
|
+
/** Fire-and-forget from the list-track tick. Never throws. */
|
|
6424
|
+
tick(closedIds) {
|
|
6425
|
+
if (this.busy || this.stopped) return;
|
|
6426
|
+
this.busy = true;
|
|
6427
|
+
this.started = true;
|
|
6428
|
+
void this.runTick(closedIds).catch((e) => logger.logger.debug(`[SCROLLBACK] tick failed: ${e}`)).finally(() => {
|
|
6429
|
+
this.busy = false;
|
|
6430
|
+
});
|
|
6431
|
+
}
|
|
6432
|
+
/** One tick, awaitable (tests). */
|
|
6433
|
+
async runTick(closedIds) {
|
|
6434
|
+
this.started = true;
|
|
6435
|
+
this.refreshConfig();
|
|
6436
|
+
if (!this.enabled) return;
|
|
6437
|
+
const out = await this.tmux.run(["list-sessions", "-F", CHECKPOINT_PROBE_FORMAT]);
|
|
6438
|
+
const probed = out ? parseCheckpointProbe(out.toString("utf8")) : null;
|
|
6439
|
+
if (probed) {
|
|
6440
|
+
this.seedState(probed);
|
|
6441
|
+
const now = this.now();
|
|
6442
|
+
for (const id of planCheckpoints(probed, this.state, now)) {
|
|
6443
|
+
if (this.stopped || !this.enabled) break;
|
|
6444
|
+
const t = probed.find((p) => p.id === id);
|
|
6445
|
+
await this.checkpointOne(t);
|
|
6446
|
+
}
|
|
6447
|
+
}
|
|
6448
|
+
this.maybeGc(probed ? /* @__PURE__ */ new Set([...probed.map((p) => p.id), ...closedIds]) : null);
|
|
6449
|
+
}
|
|
6450
|
+
/** Re-read the switch (at most every CONFIG_REFRESH_MS; always on first
|
|
6451
|
+
* use, so a replay right after daemon start already honours `false`). */
|
|
6452
|
+
refreshConfig() {
|
|
6453
|
+
const now = this.now();
|
|
6454
|
+
if (this.configReadAt && now - this.configReadAt < CONFIG_REFRESH_MS) return;
|
|
6455
|
+
this.configReadAt = now;
|
|
6456
|
+
let enabled = SCROLLBACK_PERSIST_DEFAULT;
|
|
6457
|
+
try {
|
|
6458
|
+
enabled = this.readConfig().enabled;
|
|
6459
|
+
} catch {
|
|
6460
|
+
}
|
|
6461
|
+
const turnedOff = this.enabled && !enabled;
|
|
6462
|
+
this.enabled = enabled;
|
|
6463
|
+
if (!enabled) {
|
|
6464
|
+
if (turnedOff) logger.logger.info("[SCROLLBACK] terminalScrollbackPersist=false: deleting saved terminal scrollback");
|
|
6465
|
+
this.purge();
|
|
6466
|
+
}
|
|
6467
|
+
}
|
|
6468
|
+
/** First sight of an id after a daemon start: an existing `.live` file
|
|
6469
|
+
* counts as the last checkpoint (by mtime), so a restart does not re-take
|
|
6470
|
+
* every terminal at once. */
|
|
6471
|
+
seedState(probed) {
|
|
6472
|
+
for (const t of probed) {
|
|
6473
|
+
if (this.state.has(t.id)) continue;
|
|
6474
|
+
try {
|
|
6475
|
+
const mtime = node_fs.statSync(this.filePath(t.id, "live")).mtimeMs;
|
|
6476
|
+
this.state.set(t.id, { at: mtime - ACTIVITY_RESOLUTION_MS });
|
|
6477
|
+
} catch {
|
|
6478
|
+
}
|
|
6479
|
+
}
|
|
6480
|
+
}
|
|
6481
|
+
async checkpointOne(t) {
|
|
6482
|
+
const gen = this.generation.get(t.id) ?? 0;
|
|
6483
|
+
const startedAt = this.now();
|
|
6484
|
+
const parts = [];
|
|
6485
|
+
for (const args of checkpointCaptureArgs(t.id, t.alternateOn)) {
|
|
6486
|
+
const out = await this.tmux.run(args);
|
|
6487
|
+
if (!out) return;
|
|
6488
|
+
parts.push(out);
|
|
6489
|
+
}
|
|
6490
|
+
if ((this.generation.get(t.id) ?? 0) !== gen || this.stopped || !this.enabled) return;
|
|
6491
|
+
this.writeLive(t.id, buildCheckpointBody(parts), this.now());
|
|
6492
|
+
this.state.set(t.id, { at: startedAt });
|
|
6493
|
+
}
|
|
6494
|
+
writeLive(id, body, now) {
|
|
6495
|
+
if (body.length === 0) {
|
|
6496
|
+
this.removeFile(id, "live");
|
|
6497
|
+
return;
|
|
6498
|
+
}
|
|
6499
|
+
this.writeFile(id, "live", { savedAt: now, body });
|
|
6500
|
+
}
|
|
6501
|
+
// ── promote / replay ────────────────────────────────────────────────────
|
|
6502
|
+
/**
|
|
6503
|
+
* Spec D3: called right before a terminal is rebuilt (new tmux session,
|
|
6504
|
+
* same id). `.live` (the life that ended) is chained onto `.prev` and
|
|
6505
|
+
* removed. Synchronous on purpose — the caller creates the session in the
|
|
6506
|
+
* same turn, so no async capture can slip in between.
|
|
6507
|
+
*
|
|
6508
|
+
* Returns an UNDO that puts both files back exactly as they were: the
|
|
6509
|
+
* caller runs it when the rebuild did not happen (new-session failed —
|
|
6510
|
+
* e.g. a timed-out has-session misread a live session as gone), so the
|
|
6511
|
+
* same life is not archived twice and replayed as a duplicate.
|
|
6512
|
+
* `.live` is only removed once the chained `.prev` was written.
|
|
6513
|
+
*/
|
|
6514
|
+
promote(id) {
|
|
6515
|
+
this.generation.set(id, (this.generation.get(id) ?? 0) + 1);
|
|
6516
|
+
this.state.delete(id);
|
|
6517
|
+
const noop = () => {
|
|
6518
|
+
};
|
|
6519
|
+
if (!this.isEnabled()) return noop;
|
|
6520
|
+
try {
|
|
6521
|
+
const liveRaw = this.readFile(id, "live");
|
|
6522
|
+
const prevRaw = this.readFile(id, "prev");
|
|
6523
|
+
const live = decodeArchiveFile(liveRaw);
|
|
6524
|
+
const chained = chainArchive(decodeArchiveFile(prevRaw), live);
|
|
6525
|
+
if (!chained || !live || !liveRaw) return noop;
|
|
6526
|
+
if (!this.writeFile(id, "prev", chained)) return noop;
|
|
6527
|
+
this.removeFile(id, "live");
|
|
6528
|
+
return () => {
|
|
6529
|
+
this.generation.set(id, (this.generation.get(id) ?? 0) + 1);
|
|
6530
|
+
if (prevRaw) this.writeRaw(id, "prev", prevRaw);
|
|
6531
|
+
else this.removeFile(id, "prev");
|
|
6532
|
+
this.writeRaw(id, "live", liveRaw);
|
|
6533
|
+
};
|
|
6534
|
+
} catch (e) {
|
|
6535
|
+
logger.logger.debug(`[SCROLLBACK] promote ${id} failed: ${e}`);
|
|
6536
|
+
return noop;
|
|
6537
|
+
}
|
|
6538
|
+
}
|
|
6539
|
+
/** The archive to replay above a capture of `id`, or null. */
|
|
6540
|
+
replayFor(id) {
|
|
6541
|
+
if (!this.isEnabled()) return null;
|
|
6542
|
+
try {
|
|
6543
|
+
return decodeArchiveFile(this.readFile(id, "prev"));
|
|
6544
|
+
} catch {
|
|
6545
|
+
return null;
|
|
6546
|
+
}
|
|
6547
|
+
}
|
|
6548
|
+
// ── shutdown ────────────────────────────────────────────────────────────
|
|
6549
|
+
/**
|
|
6550
|
+
* Daemon exit: capture every terminal with output newer than its last
|
|
6551
|
+
* checkpoint — ASYNC and in PARALLEL, newest activity first, within
|
|
6552
|
+
* `budgetMs`. Never throws and always resolves by the deadline, so the
|
|
6553
|
+
* caller's state/lock cleanup runs regardless. Best-effort: on a machine
|
|
6554
|
+
* shutdown tmux may already be gone; the periodic checkpoint is the real
|
|
6555
|
+
* guarantee.
|
|
6556
|
+
*/
|
|
6557
|
+
async flushOnShutdown(budgetMs = SHUTDOWN_FLUSH_BUDGET_MS) {
|
|
6558
|
+
if (this.stopped) return;
|
|
6559
|
+
this.stopped = true;
|
|
6560
|
+
if (!this.started || !this.enabled) return;
|
|
6561
|
+
let timer;
|
|
6562
|
+
const deadline = new Promise((r) => {
|
|
6563
|
+
timer = setTimeout(r, Math.max(0, budgetMs));
|
|
6564
|
+
timer.unref?.();
|
|
6565
|
+
});
|
|
6566
|
+
const startedAt = Date.now();
|
|
6567
|
+
const left = () => Math.max(1, budgetMs - (Date.now() - startedAt));
|
|
6568
|
+
const work = (async () => {
|
|
6569
|
+
const out = await this.tmux.run(["list-sessions", "-F", CHECKPOINT_PROBE_FORMAT], left());
|
|
6570
|
+
if (!out) return;
|
|
6571
|
+
const probed = parseCheckpointProbe(out.toString("utf8"));
|
|
6572
|
+
this.seedState(probed);
|
|
6573
|
+
const dirty = planCheckpoints(probed, this.state, this.now(), {
|
|
6574
|
+
quietMs: 0,
|
|
6575
|
+
minIntervalMs: 0,
|
|
6576
|
+
maxDirtyMs: 0,
|
|
6577
|
+
max: Number.MAX_SAFE_INTEGER
|
|
6578
|
+
});
|
|
6579
|
+
const byRecent = dirty.map((id) => probed.find((p) => p.id === id)).sort((a, b) => b.activityAt - a.activityAt);
|
|
6580
|
+
let written = 0;
|
|
6581
|
+
await Promise.all(byRecent.map(async (t) => {
|
|
6582
|
+
const at = this.now();
|
|
6583
|
+
const parts = await Promise.all(checkpointCaptureArgs(t.id, t.alternateOn).map((a) => this.tmux.run(a, left())));
|
|
6584
|
+
if (parts.some((p) => !p) || Date.now() - startedAt > budgetMs) return;
|
|
6585
|
+
this.writeLive(t.id, buildCheckpointBody(parts), this.now());
|
|
6586
|
+
this.state.set(t.id, { at });
|
|
6587
|
+
written++;
|
|
6588
|
+
}));
|
|
6589
|
+
if (dirty.length > 0) logger.logger.debug(`[SCROLLBACK] shutdown flush: ${written}/${dirty.length} terminal(s)`);
|
|
6590
|
+
})().catch((e) => logger.logger.debug(`[SCROLLBACK] shutdown flush failed: ${e}`));
|
|
6591
|
+
await Promise.race([work, deadline]);
|
|
6592
|
+
if (timer) clearTimeout(timer);
|
|
6593
|
+
}
|
|
6594
|
+
/**
|
|
6595
|
+
* Synchronous single-terminal capture right before the user kills it, so
|
|
6596
|
+
* a ↻ restore of the close record replays its last output. Bounded by
|
|
6597
|
+
* `budgetMs`; never throws; attach terminals are skipped.
|
|
6598
|
+
*/
|
|
6599
|
+
checkpointSync(id, budgetMs = KILL_CHECKPOINT_BUDGET_MS) {
|
|
6600
|
+
try {
|
|
6601
|
+
if (!/^[a-zA-Z0-9_-]{1,64}$/.test(id) || !this.isEnabled()) return false;
|
|
6602
|
+
const deadline = Date.now() + budgetMs;
|
|
6603
|
+
const left = () => deadline - Date.now();
|
|
6604
|
+
const probe = this.tmux.runSync(["display-message", "-p", "-t", `=vh-${id}:`, "#{alternate_on}|#{@vh_attach}"], left());
|
|
6605
|
+
if (!probe) return false;
|
|
6606
|
+
const [alt, ...attach] = probe.toString("utf8").replace(/\r?\n$/, "").split("|");
|
|
6607
|
+
if (attach.join("|").trim().length > 0) return false;
|
|
6608
|
+
const at = this.now();
|
|
6609
|
+
const parts = [];
|
|
6610
|
+
for (const args of checkpointCaptureArgs(id, alt === "1")) {
|
|
6611
|
+
const timeout = left();
|
|
6612
|
+
const part = timeout > 0 ? this.tmux.runSync(args, timeout) : null;
|
|
6613
|
+
if (!part) return false;
|
|
6614
|
+
parts.push(part);
|
|
6615
|
+
}
|
|
6616
|
+
this.writeLive(id, buildCheckpointBody(parts), this.now());
|
|
6617
|
+
this.state.set(id, { at });
|
|
6618
|
+
return true;
|
|
6619
|
+
} catch (e) {
|
|
6620
|
+
logger.logger.debug(`[SCROLLBACK] pre-kill checkpoint ${id} failed: ${e}`);
|
|
6621
|
+
return false;
|
|
6622
|
+
}
|
|
6623
|
+
}
|
|
6624
|
+
// ── fs ──────────────────────────────────────────────────────────────────
|
|
6625
|
+
ensureDir() {
|
|
6626
|
+
const dir = this.dir();
|
|
6627
|
+
node_fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
6628
|
+
try {
|
|
6629
|
+
node_fs.chmodSync(dir, 448);
|
|
6630
|
+
} catch {
|
|
6631
|
+
}
|
|
6632
|
+
return dir;
|
|
6633
|
+
}
|
|
6634
|
+
readFile(id, kind) {
|
|
6635
|
+
try {
|
|
6636
|
+
const p = this.filePath(id, kind);
|
|
6637
|
+
if (node_fs.statSync(p).size > SCROLLBACK_FILE_MAX_BYTES * 2) return null;
|
|
6638
|
+
return node_fs.readFileSync(p);
|
|
6639
|
+
} catch {
|
|
6640
|
+
return null;
|
|
6641
|
+
}
|
|
6642
|
+
}
|
|
6643
|
+
/** Atomic 0600 write; false on any failure. */
|
|
6644
|
+
writeFile(id, kind, file) {
|
|
6645
|
+
return this.writeRaw(id, kind, encodeArchiveFile(file));
|
|
6646
|
+
}
|
|
6647
|
+
writeRaw(id, kind, bytes) {
|
|
6648
|
+
try {
|
|
6649
|
+
this.ensureDir();
|
|
6650
|
+
const target = this.filePath(id, kind);
|
|
6651
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
6652
|
+
node_fs.writeFileSync(tmp, bytes, { mode: 384 });
|
|
6653
|
+
node_fs.chmodSync(tmp, 384);
|
|
6654
|
+
node_fs.renameSync(tmp, target);
|
|
6655
|
+
return true;
|
|
6656
|
+
} catch (e) {
|
|
6657
|
+
logger.logger.debug(`[SCROLLBACK] write ${id}.${kind} failed: ${e}`);
|
|
6658
|
+
return false;
|
|
6659
|
+
}
|
|
6660
|
+
}
|
|
6661
|
+
removeFile(id, kind) {
|
|
6662
|
+
try {
|
|
6663
|
+
node_fs.unlinkSync(this.filePath(id, kind));
|
|
6664
|
+
} catch {
|
|
6665
|
+
}
|
|
6666
|
+
}
|
|
6667
|
+
listFiles() {
|
|
6668
|
+
let names;
|
|
6669
|
+
try {
|
|
6670
|
+
names = node_fs.readdirSync(this.dir());
|
|
6671
|
+
} catch {
|
|
6672
|
+
return [];
|
|
6673
|
+
}
|
|
6674
|
+
const out = [];
|
|
6675
|
+
for (const name of names) {
|
|
6676
|
+
try {
|
|
6677
|
+
const st = node_fs.statSync(path.join(this.dir(), name));
|
|
6678
|
+
if (!st.isFile()) continue;
|
|
6679
|
+
const parsed = parseArchiveFileName(name);
|
|
6680
|
+
out.push({ name, id: parsed?.id ?? null, kind: parsed?.kind ?? null, mtimeMs: st.mtimeMs, bytes: st.size });
|
|
6681
|
+
} catch {
|
|
6682
|
+
}
|
|
6683
|
+
}
|
|
6684
|
+
return out;
|
|
6685
|
+
}
|
|
6686
|
+
maybeGc(keepIds) {
|
|
6687
|
+
const now = this.now();
|
|
6688
|
+
if (this.lastGcAt && now - this.lastGcAt < GC_INTERVAL_MS) return;
|
|
6689
|
+
this.lastGcAt = now;
|
|
6690
|
+
this.gc(keepIds, now);
|
|
6691
|
+
}
|
|
6692
|
+
/** Exposed for tests. */
|
|
6693
|
+
gc(keepIds, now = this.now()) {
|
|
6694
|
+
const doomed = planScrollbackGc(this.listFiles(), keepIds, now);
|
|
6695
|
+
for (const name of doomed) {
|
|
6696
|
+
try {
|
|
6697
|
+
node_fs.unlinkSync(path.join(this.dir(), name));
|
|
6698
|
+
} catch {
|
|
6699
|
+
}
|
|
6700
|
+
}
|
|
6701
|
+
if (doomed.length > 0) logger.logger.debug(`[SCROLLBACK] gc removed ${doomed.length} file(s)`);
|
|
6702
|
+
return doomed;
|
|
6703
|
+
}
|
|
6704
|
+
/** Switch turned off: the saved scrollback goes away entirely. */
|
|
6705
|
+
purge() {
|
|
6706
|
+
try {
|
|
6707
|
+
node_fs.rmSync(this.dir(), { recursive: true, force: true });
|
|
6708
|
+
} catch {
|
|
6709
|
+
}
|
|
6710
|
+
this.state.clear();
|
|
6711
|
+
}
|
|
6712
|
+
}
|
|
6713
|
+
|
|
5695
6714
|
const TOMBSTONE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
5696
6715
|
function tmuxKillVerified(kill, probe) {
|
|
5697
6716
|
return !kill.error && !probe.error && typeof probe.status === "number" && probe.status !== 0;
|
|
@@ -5836,6 +6855,10 @@ function terminalListSignature(items) {
|
|
|
5836
6855
|
function hostListSignature(items) {
|
|
5837
6856
|
return `${isTmuxAvailable() ? "tmux" : "no-tmux"}|${terminalListSignature(items)}`;
|
|
5838
6857
|
}
|
|
6858
|
+
function screenObservation(probe) {
|
|
6859
|
+
if (probe.agentObservedAt === void 0) return void 0;
|
|
6860
|
+
return { agentKind: probe.agentKind, agentState: probe.agentState };
|
|
6861
|
+
}
|
|
5839
6862
|
function looksLikeClaudeCommand(currentCommand) {
|
|
5840
6863
|
const cmd = (currentCommand || "").trim().replace(/^-/, "").toLowerCase();
|
|
5841
6864
|
return cmd === "claude" || cmd === "node" || /^\d+\.\d+(\.\d+)?$/.test(cmd);
|
|
@@ -6307,6 +7330,8 @@ class WebTerminalManager {
|
|
|
6307
7330
|
* refreshes and notifications into a process that is exiting. */
|
|
6308
7331
|
shuttingDown = false;
|
|
6309
7332
|
onAutoRestoreSummaryCb = null;
|
|
7333
|
+
/** B-550: scrollback checkpoints across rebuilds (scrollbackCheckpoint.ts). */
|
|
7334
|
+
scrollback = new ScrollbackCheckpointer({ homeDir: () => configuration.configuration.happyHomeDir, env: ptyEnv });
|
|
6310
7335
|
/** Report line for the account notification (wired by apiMachine). */
|
|
6311
7336
|
setOnAutoRestoreSummary(fn) {
|
|
6312
7337
|
this.onAutoRestoreSummaryCb = fn;
|
|
@@ -6345,6 +7370,22 @@ class WebTerminalManager {
|
|
|
6345
7370
|
// callback ⇒ tracking is skipped entirely.
|
|
6346
7371
|
onNotify;
|
|
6347
7372
|
notifyTracker = new TerminalNotifyTracker();
|
|
7373
|
+
// ── Hook-driven agent status (B-547) ────────────────────────────────────
|
|
7374
|
+
// `claude` commands the daemon types into a terminal get `--settings`
|
|
7375
|
+
// with managed hooks (terminalHookSettings.ts); the forwarded events land
|
|
7376
|
+
// in `agentHooks` and are fused with the screen classifier in
|
|
7377
|
+
// probeAgentState. The screen stays the fallback for everything else.
|
|
7378
|
+
agentHooks = new TerminalAgentHookStore();
|
|
7379
|
+
claudeVersion = new ClaudeVersionCache(ptyEnv);
|
|
7380
|
+
/** Terminals whose LAST probe was decided by a hook record — their
|
|
7381
|
+
* transitions skip the screen debounce in the notification tracker.
|
|
7382
|
+
* Daemon-internal: never pushed (wire shape unchanged). */
|
|
7383
|
+
hookSourced = /* @__PURE__ */ new Set();
|
|
7384
|
+
/** Last `#{pane_current_command}` identity seen per terminal, so an
|
|
7385
|
+
* interrupt re-check can classify the screen without a list read. */
|
|
7386
|
+
lastPolledIdentity = /* @__PURE__ */ new Map();
|
|
7387
|
+
interruptTimers = /* @__PURE__ */ new Map();
|
|
7388
|
+
hookSettingsSwept = false;
|
|
6348
7389
|
// ── Terminal mirror integration (B-105) ─────────────────────────────────
|
|
6349
7390
|
/** terminalId → shadow session id (daemon's mirror manager). Feeds the
|
|
6350
7391
|
* list push (toggle availability) and closed-terminal records. */
|
|
@@ -6371,8 +7412,72 @@ class WebTerminalManager {
|
|
|
6371
7412
|
requestListRefresh() {
|
|
6372
7413
|
this.kickListRefresh();
|
|
6373
7414
|
}
|
|
6374
|
-
|
|
6375
|
-
|
|
7415
|
+
/** B-547: one forwarded status hook (control server /terminal-agent-hook).
|
|
7416
|
+
* A list-relevant change pushes within the kick debounce instead of
|
|
7417
|
+
* waiting for the 10 s tick. Never throws. */
|
|
7418
|
+
ingestAgentHook(body) {
|
|
7419
|
+
try {
|
|
7420
|
+
const result = this.agentHooks.applyHook(body, Date.now());
|
|
7421
|
+
if (result?.changed) this.kickListRefresh();
|
|
7422
|
+
} catch (e) {
|
|
7423
|
+
logger.logger.debug(`[WEB TERMINAL] agent hook ingest failed: ${e}`);
|
|
7424
|
+
}
|
|
7425
|
+
}
|
|
7426
|
+
/** B-547: test/diagnostic view — does this terminal hold a hook record? */
|
|
7427
|
+
hasAgentHookRecord(terminalId) {
|
|
7428
|
+
return this.agentHooks.has(terminalId);
|
|
7429
|
+
}
|
|
7430
|
+
/** B-547: once per daemon, drop settings files of terminals whose tmux
|
|
7431
|
+
* session is gone. Per-id `has-session` (not a list read, whose transient
|
|
7432
|
+
* failure would look like "no terminals") decides liveness. */
|
|
7433
|
+
sweepHookSettingsOnce() {
|
|
7434
|
+
if (this.hookSettingsSwept || !isTmuxAvailable()) return;
|
|
7435
|
+
this.hookSettingsSwept = true;
|
|
7436
|
+
try {
|
|
7437
|
+
const env = ptyEnv();
|
|
7438
|
+
const removed = sweepStaleTerminalHookSettings((id) => this.terminals.has(id) || node_child_process.spawnSync("tmux", tmuxSocket.tmuxArgs(["has-session", "-t", `=vh-${id}:`]), { stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env }).status !== 1);
|
|
7439
|
+
if (removed.length > 0) logger.logger.debug(`[WEB TERMINAL] removed ${removed.length} stale terminal hook settings file(s)`);
|
|
7440
|
+
} catch (e) {
|
|
7441
|
+
logger.logger.debug(`[WEB TERMINAL] hook settings sweep failed: ${e}`);
|
|
7442
|
+
}
|
|
7443
|
+
}
|
|
7444
|
+
/** B-547: a terminal is gone — drop its hook record, timers and file. */
|
|
7445
|
+
forgetAgentHooks(terminalId) {
|
|
7446
|
+
this.agentHooks.forget(terminalId);
|
|
7447
|
+
this.hookSourced.delete(terminalId);
|
|
7448
|
+
this.lastPolledIdentity.delete(terminalId);
|
|
7449
|
+
for (const t of this.interruptTimers.get(terminalId) ?? []) clearTimeout(t);
|
|
7450
|
+
this.interruptTimers.delete(terminalId);
|
|
7451
|
+
removeTerminalHookSettings(terminalId);
|
|
7452
|
+
}
|
|
7453
|
+
/** B-547: re-check an armed Esc/Ctrl+C at the spec's delays. A later
|
|
7454
|
+
* keypress re-arms (fresh baseline, fresh timers). */
|
|
7455
|
+
scheduleInterruptChecks(terminalId) {
|
|
7456
|
+
for (const t of this.interruptTimers.get(terminalId) ?? []) clearTimeout(t);
|
|
7457
|
+
const timers = INTERRUPT_CHECK_DELAYS_MS.map((delay) => {
|
|
7458
|
+
const t = setTimeout(() => this.runInterruptCheck(terminalId), delay);
|
|
7459
|
+
t.unref?.();
|
|
7460
|
+
return t;
|
|
7461
|
+
});
|
|
7462
|
+
this.interruptTimers.set(terminalId, timers);
|
|
7463
|
+
}
|
|
7464
|
+
runInterruptCheck(terminalId) {
|
|
7465
|
+
try {
|
|
7466
|
+
const screen = this.probeScreenState(`vh-${terminalId}`, this.lastPolledIdentity.get(terminalId));
|
|
7467
|
+
const verdict = this.agentHooks.checkInterrupt(terminalId, screenObservation(screen), Date.now());
|
|
7468
|
+
if (verdict === "wait") return;
|
|
7469
|
+
for (const t of this.interruptTimers.get(terminalId) ?? []) clearTimeout(t);
|
|
7470
|
+
this.interruptTimers.delete(terminalId);
|
|
7471
|
+
if (verdict === "synthesized") {
|
|
7472
|
+
logger.logger.debug(`[WEB TERMINAL] ${terminalId}: inferred interrupt (Esc/Ctrl+C, no Stop hook)`);
|
|
7473
|
+
this.kickListRefresh();
|
|
7474
|
+
}
|
|
7475
|
+
} catch (e) {
|
|
7476
|
+
logger.logger.debug(`[WEB TERMINAL] interrupt check failed: ${e}`);
|
|
7477
|
+
}
|
|
7478
|
+
}
|
|
7479
|
+
constructor(emit, onNotify) {
|
|
7480
|
+
this.emit = emit;
|
|
6376
7481
|
this.onNotify = onNotify ?? null;
|
|
6377
7482
|
this.reaper = setInterval(() => this.reapIdle(), REAP_INTERVAL_MS);
|
|
6378
7483
|
this.reaper.unref?.();
|
|
@@ -6385,6 +7490,8 @@ class WebTerminalManager {
|
|
|
6385
7490
|
*/
|
|
6386
7491
|
startListTracking(cb, intervalMs = LIST_TRACK_INTERVAL_MS) {
|
|
6387
7492
|
this.listChangedCb = cb;
|
|
7493
|
+
this.claudeVersion.start();
|
|
7494
|
+
this.sweepHookSettingsOnce();
|
|
6388
7495
|
this.lastActivityEmitted = {};
|
|
6389
7496
|
if (!this.listTrackTimer) {
|
|
6390
7497
|
this.listTrackTimer = setInterval(() => this.listTrackTick(), intervalMs);
|
|
@@ -6407,6 +7514,12 @@ class WebTerminalManager {
|
|
|
6407
7514
|
clearTimeout(this.activityFlushTimer);
|
|
6408
7515
|
this.activityFlushTimer = null;
|
|
6409
7516
|
}
|
|
7517
|
+
for (const timers of this.interruptTimers.values()) for (const t of timers) clearTimeout(t);
|
|
7518
|
+
this.interruptTimers.clear();
|
|
7519
|
+
}
|
|
7520
|
+
/** B-550: last scrollback checkpoint before the daemon exits (async, ~300ms, never throws). */
|
|
7521
|
+
flushScrollbackForShutdown() {
|
|
7522
|
+
return this.scrollback.flushOnShutdown();
|
|
6410
7523
|
}
|
|
6411
7524
|
/**
|
|
6412
7525
|
* One pty chunk arrived → make sure a realtime activity frame goes out
|
|
@@ -6537,6 +7650,8 @@ class WebTerminalManager {
|
|
|
6537
7650
|
try {
|
|
6538
7651
|
const list = this.buildTerminalList();
|
|
6539
7652
|
this.trackClosures(list);
|
|
7653
|
+
if (list.length > 0) this.agentHooks.prune([...list.map((t) => t.id), ...this.terminals.keys()]);
|
|
7654
|
+
this.scrollback.tick(this.closedTerminals.map((r) => r.id));
|
|
6540
7655
|
this.trackNotifications(list);
|
|
6541
7656
|
this.pruneActivityTable(list);
|
|
6542
7657
|
this.emitActivity(Object.fromEntries(
|
|
@@ -6594,6 +7709,7 @@ class WebTerminalManager {
|
|
|
6594
7709
|
closedAt: now
|
|
6595
7710
|
});
|
|
6596
7711
|
changed = true;
|
|
7712
|
+
this.forgetAgentHooks(id);
|
|
6597
7713
|
logger.logger.debug(`[WEB TERMINAL] recorded closed terminal ${id}`);
|
|
6598
7714
|
try {
|
|
6599
7715
|
this.onTerminalClosedCb?.(id);
|
|
@@ -6679,6 +7795,7 @@ class WebTerminalManager {
|
|
|
6679
7795
|
if (!isTmuxAvailable()) return;
|
|
6680
7796
|
const settings = await persistence.readSettings();
|
|
6681
7797
|
const config = resolveAutoRestoreConfig(settings);
|
|
7798
|
+
await this.claudeVersion.settled(3e3);
|
|
6682
7799
|
const selection = selectAutoRestore(candidates, {
|
|
6683
7800
|
now: Date.now(),
|
|
6684
7801
|
config,
|
|
@@ -6787,6 +7904,7 @@ class WebTerminalManager {
|
|
|
6787
7904
|
{ stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env }
|
|
6788
7905
|
).status === 0;
|
|
6789
7906
|
if (exists) return false;
|
|
7907
|
+
const undoPromote = plan.attachTmux ? void 0 : this.scrollback.promote(plan.terminalId);
|
|
6790
7908
|
const envFlags = tmuxSupportsEnvFlag() ? [
|
|
6791
7909
|
"-e",
|
|
6792
7910
|
CLAUDE_CLASSIC_RENDERER_ENV,
|
|
@@ -6807,6 +7925,7 @@ class WebTerminalManager {
|
|
|
6807
7925
|
{ stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env }
|
|
6808
7926
|
).status === 0;
|
|
6809
7927
|
if (!live) {
|
|
7928
|
+
undoPromote?.();
|
|
6810
7929
|
logger.logger.debug(`[WEB TERMINAL] auto-restore: tmux create failed for ${plan.terminalId}`);
|
|
6811
7930
|
return false;
|
|
6812
7931
|
}
|
|
@@ -6840,7 +7959,8 @@ class WebTerminalManager {
|
|
|
6840
7959
|
);
|
|
6841
7960
|
}
|
|
6842
7961
|
if (plan.command) {
|
|
6843
|
-
|
|
7962
|
+
const command = withTerminalHooks(plan.command, plan.terminalId, this.claudeVersion.current());
|
|
7963
|
+
for (const argv of startupInjectionArgs(name, command)) {
|
|
6844
7964
|
node_child_process.spawnSync("tmux", tmuxSocket.tmuxArgs(argv), { stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env });
|
|
6845
7965
|
}
|
|
6846
7966
|
}
|
|
@@ -6873,7 +7993,13 @@ class WebTerminalManager {
|
|
|
6873
7993
|
const now = Date.now();
|
|
6874
7994
|
this.notifyTracker.prune(list.map((t) => t.id));
|
|
6875
7995
|
for (const item of list) {
|
|
6876
|
-
const event = this.notifyTracker.observe(
|
|
7996
|
+
const event = this.notifyTracker.observe(
|
|
7997
|
+
item.id,
|
|
7998
|
+
item.agentState,
|
|
7999
|
+
now,
|
|
8000
|
+
item.agentKind,
|
|
8001
|
+
{ authoritative: this.hookSourced.has(item.id) }
|
|
8002
|
+
);
|
|
6877
8003
|
if (!event) continue;
|
|
6878
8004
|
try {
|
|
6879
8005
|
this.onNotify({ terminalId: item.id, title: item.title || "Terminal", event });
|
|
@@ -7232,7 +8358,8 @@ class WebTerminalManager {
|
|
|
7232
8358
|
logger.logger.debug(`[WEB TERMINAL] ${tmuxSession} attaches user tmux session ${attachTarget.id} (${attachTarget.name})`);
|
|
7233
8359
|
return { id: attachTarget.id, name: attachTarget.name };
|
|
7234
8360
|
}
|
|
7235
|
-
const
|
|
8361
|
+
const normalized = normalizeStartupCommand(startupCommand);
|
|
8362
|
+
const startup = normalized && tmuxSession.startsWith("vh-") ? withTerminalHooks(normalized, tmuxSession.slice(3), this.claudeVersion.current()) : normalized;
|
|
7236
8363
|
if (startup) {
|
|
7237
8364
|
try {
|
|
7238
8365
|
for (const a of startupInjectionArgs(tmuxSession, startup)) {
|
|
@@ -7348,7 +8475,10 @@ class WebTerminalManager {
|
|
|
7348
8475
|
});
|
|
7349
8476
|
const paneState = parsePaneState((byKey.panes ?? Buffer.alloc(0)).toString("utf8"));
|
|
7350
8477
|
if (paneState) session.paneId = paneState.paneId;
|
|
7351
|
-
const payload =
|
|
8478
|
+
const payload = withArchivedScrollback(
|
|
8479
|
+
assembleRestore({ ...byKey, tail: unescapeOctal(byKey.tail ?? Buffer.alloc(0)) }, paneState),
|
|
8480
|
+
this.scrollback.replayFor(session.id)
|
|
8481
|
+
);
|
|
7352
8482
|
if (fresh) {
|
|
7353
8483
|
session.restoreHeadless(payload.full);
|
|
7354
8484
|
}
|
|
@@ -7407,6 +8537,7 @@ class WebTerminalManager {
|
|
|
7407
8537
|
if (!session) return;
|
|
7408
8538
|
session.lastTouch = Date.now();
|
|
7409
8539
|
const text = Buffer.from(dataBase64, "base64").toString("utf8");
|
|
8540
|
+
if (this.agentHooks.noteInput(terminalId, text, Date.now())) this.scheduleInterruptChecks(terminalId);
|
|
7410
8541
|
if (session.transport.kind === "pty") {
|
|
7411
8542
|
session.transport.pty.write(text);
|
|
7412
8543
|
return;
|
|
@@ -7540,6 +8671,7 @@ class WebTerminalManager {
|
|
|
7540
8671
|
this.tombstones[terminalId] = Date.now();
|
|
7541
8672
|
saveTombstones(this.tombstones);
|
|
7542
8673
|
this.notifyTracker.remove(terminalId);
|
|
8674
|
+
this.forgetAgentHooks(terminalId);
|
|
7543
8675
|
logger.logger.debug(`[WEB TERMINAL] killed direct pty ${terminalId}`);
|
|
7544
8676
|
this.kickListRefresh();
|
|
7545
8677
|
return true;
|
|
@@ -7549,6 +8681,7 @@ class WebTerminalManager {
|
|
|
7549
8681
|
return t ? seenInfoOf(t) : void 0;
|
|
7550
8682
|
})();
|
|
7551
8683
|
const target = `=vh-${terminalId}:`;
|
|
8684
|
+
this.scrollback.checkpointSync(terminalId);
|
|
7552
8685
|
const killed = node_child_process.spawnSync("tmux", tmuxSocket.tmuxArgs(["kill-session", "-t", target]), { stdio: "ignore", env: ptyEnv() });
|
|
7553
8686
|
const verified = node_child_process.spawnSync("tmux", tmuxSocket.tmuxArgs(["has-session", "-t", target]), { stdio: "ignore", env: ptyEnv() });
|
|
7554
8687
|
if (!tmuxKillVerified(killed, verified)) {
|
|
@@ -7590,6 +8723,7 @@ class WebTerminalManager {
|
|
|
7590
8723
|
this.tombstones[terminalId] = Date.now();
|
|
7591
8724
|
saveTombstones(this.tombstones);
|
|
7592
8725
|
this.notifyTracker.remove(terminalId);
|
|
8726
|
+
this.forgetAgentHooks(terminalId);
|
|
7593
8727
|
logger.logger.debug(`[WEB TERMINAL] killed session vh-${terminalId}`);
|
|
7594
8728
|
this.kickListRefresh();
|
|
7595
8729
|
return true;
|
|
@@ -7691,6 +8825,25 @@ class WebTerminalManager {
|
|
|
7691
8825
|
* you've opened this daemon lifetime costs nothing. Cold tmux-only sessions
|
|
7692
8826
|
* (pty reaped / never attached) fall back to the tmux probe. */
|
|
7693
8827
|
probeAgentState(sessionName, polledCommand) {
|
|
8828
|
+
const id = sessionName.startsWith("vh-") ? sessionName.slice(3) : sessionName;
|
|
8829
|
+
if (polledCommand) this.lastPolledIdentity.set(id, polledCommand);
|
|
8830
|
+
const screen = this.probeScreenState(sessionName, polledCommand);
|
|
8831
|
+
const now = Date.now();
|
|
8832
|
+
const fused = this.agentHooks.fuse(id, screenObservation(screen), now);
|
|
8833
|
+
if (fused.source === "screen") {
|
|
8834
|
+
this.hookSourced.delete(id);
|
|
8835
|
+
return screen;
|
|
8836
|
+
}
|
|
8837
|
+
this.hookSourced.add(id);
|
|
8838
|
+
return {
|
|
8839
|
+
agentState: fused.agentState,
|
|
8840
|
+
agentKind: fused.agentKind,
|
|
8841
|
+
agentObservedAt: screen.agentObservedAt ?? now,
|
|
8842
|
+
claudeConfident: screen.claudeConfident
|
|
8843
|
+
};
|
|
8844
|
+
}
|
|
8845
|
+
/** The screen classifier alone (pre-B-547 probeAgentState). */
|
|
8846
|
+
probeScreenState(sessionName, polledCommand) {
|
|
7694
8847
|
const id = sessionName.startsWith("vh-") ? sessionName.slice(3) : sessionName;
|
|
7695
8848
|
const live = this.terminals.get(id);
|
|
7696
8849
|
if (live) {
|
|
@@ -7885,7 +9038,7 @@ class WebTerminalManager {
|
|
|
7885
9038
|
}
|
|
7886
9039
|
}
|
|
7887
9040
|
|
|
7888
|
-
const UUID_RE$
|
|
9041
|
+
const UUID_RE$4 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
7889
9042
|
async function listClaudeProjectDirs(projectsRoot) {
|
|
7890
9043
|
let names;
|
|
7891
9044
|
try {
|
|
@@ -7916,7 +9069,7 @@ async function collectCandidates$1(projectDirs, exclude) {
|
|
|
7916
9069
|
for (const name of names) {
|
|
7917
9070
|
if (!name.endsWith(".jsonl")) continue;
|
|
7918
9071
|
const id = name.slice(0, -".jsonl".length);
|
|
7919
|
-
if (!UUID_RE$
|
|
9072
|
+
if (!UUID_RE$4.test(id) || exclude.has(id.toLowerCase())) continue;
|
|
7920
9073
|
const path$1 = path.join(dir, name);
|
|
7921
9074
|
try {
|
|
7922
9075
|
const s = await promises.stat(path$1);
|
|
@@ -7945,11 +9098,11 @@ async function readHead$1(path, bytes) {
|
|
|
7945
9098
|
await fh.close();
|
|
7946
9099
|
}
|
|
7947
9100
|
}
|
|
7948
|
-
function oneLine$
|
|
9101
|
+
function oneLine$2(text, max) {
|
|
7949
9102
|
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
7950
9103
|
return collapsed.length > max ? `${collapsed.slice(0, max - 1)}\u2026` : collapsed;
|
|
7951
9104
|
}
|
|
7952
|
-
function parseTimestamp$
|
|
9105
|
+
function parseTimestamp$2(raw) {
|
|
7953
9106
|
if (typeof raw === "number" && Number.isFinite(raw)) return raw;
|
|
7954
9107
|
if (typeof raw === "string") {
|
|
7955
9108
|
const t = Date.parse(raw);
|
|
@@ -7976,7 +9129,7 @@ function parseClaudeHistoryHead(head, promptChars) {
|
|
|
7976
9129
|
}
|
|
7977
9130
|
if (!parsed || typeof parsed !== "object") continue;
|
|
7978
9131
|
if (parsed.type === "summary" && typeof parsed.summary === "string" && !summary) {
|
|
7979
|
-
const s = oneLine$
|
|
9132
|
+
const s = oneLine$2(parsed.summary, promptChars);
|
|
7980
9133
|
if (s) summary = s;
|
|
7981
9134
|
continue;
|
|
7982
9135
|
}
|
|
@@ -7984,7 +9137,7 @@ function parseClaudeHistoryHead(head, promptChars) {
|
|
|
7984
9137
|
if (typeof parsed.entrypoint === "string" && parsed.entrypoint && !entrypoint) entrypoint = parsed.entrypoint;
|
|
7985
9138
|
if (typeof parsed.gitBranch === "string" && parsed.gitBranch && !gitBranch) gitBranch = parsed.gitBranch;
|
|
7986
9139
|
if (typeof parsed.version === "string" && parsed.version && !version) version = parsed.version;
|
|
7987
|
-
if (startedAt === null) startedAt = parseTimestamp$
|
|
9140
|
+
if (startedAt === null) startedAt = parseTimestamp$2(parsed.timestamp);
|
|
7988
9141
|
if (!firstPrompt && parsed.type === "user" && !parsed.isSidechain) {
|
|
7989
9142
|
const content = parsed.message?.content;
|
|
7990
9143
|
let text = null;
|
|
@@ -7996,7 +9149,7 @@ function parseClaudeHistoryHead(head, promptChars) {
|
|
|
7996
9149
|
}
|
|
7997
9150
|
if (text) {
|
|
7998
9151
|
if (oneShotPrompts.isVeryHappyOneShotPrompt(text)) return null;
|
|
7999
|
-
const cleaned = oneLine$
|
|
9152
|
+
const cleaned = oneLine$2(stripHarnessTags(text), promptChars);
|
|
8000
9153
|
if (cleaned) firstPrompt = cleaned;
|
|
8001
9154
|
}
|
|
8002
9155
|
}
|
|
@@ -8050,8 +9203,8 @@ async function listClaudeSessionHistory(options) {
|
|
|
8050
9203
|
return { entries, truncated, scanned };
|
|
8051
9204
|
}
|
|
8052
9205
|
|
|
8053
|
-
const UUID_RE$
|
|
8054
|
-
const ROLLOUT_RE = /^rollout-.*-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i;
|
|
9206
|
+
const UUID_RE$3 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
9207
|
+
const ROLLOUT_RE$1 = /^rollout-.*-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i;
|
|
8055
9208
|
const VERY_HAPPY_CODEX_ORIGINATOR = "happy-codex";
|
|
8056
9209
|
function getCodexHomeDir() {
|
|
8057
9210
|
return process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
|
|
@@ -8073,7 +9226,7 @@ async function collectCandidates(root, exclude) {
|
|
|
8073
9226
|
}
|
|
8074
9227
|
for (const name of names) {
|
|
8075
9228
|
const full = path.join(dir, name);
|
|
8076
|
-
const match = ROLLOUT_RE.exec(name);
|
|
9229
|
+
const match = ROLLOUT_RE$1.exec(name);
|
|
8077
9230
|
if (match) {
|
|
8078
9231
|
const id = match[1].toLowerCase();
|
|
8079
9232
|
if (exclude.has(id)) continue;
|
|
@@ -8111,11 +9264,11 @@ async function readHead(path, bytes) {
|
|
|
8111
9264
|
await fh.close();
|
|
8112
9265
|
}
|
|
8113
9266
|
}
|
|
8114
|
-
function oneLine(text, max) {
|
|
9267
|
+
function oneLine$1(text, max) {
|
|
8115
9268
|
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
8116
9269
|
return collapsed.length > max ? `${collapsed.slice(0, max - 1)}\u2026` : collapsed;
|
|
8117
9270
|
}
|
|
8118
|
-
function parseTimestamp(raw) {
|
|
9271
|
+
function parseTimestamp$1(raw) {
|
|
8119
9272
|
if (typeof raw === "number" && Number.isFinite(raw)) return raw;
|
|
8120
9273
|
if (typeof raw === "string") {
|
|
8121
9274
|
const t = Date.parse(raw);
|
|
@@ -8171,14 +9324,14 @@ function parseCodexHistoryHead(head, promptChars) {
|
|
|
8171
9324
|
if (parsed.type === "session_meta" && payload && typeof payload === "object") {
|
|
8172
9325
|
if (isCodexSubThread(payload)) return null;
|
|
8173
9326
|
sawMeta = true;
|
|
8174
|
-
if (typeof payload.id === "string" && UUID_RE$
|
|
9327
|
+
if (typeof payload.id === "string" && UUID_RE$3.test(payload.id)) id = payload.id.toLowerCase();
|
|
8175
9328
|
if (typeof payload.cwd === "string" && payload.cwd) cwd = payload.cwd;
|
|
8176
9329
|
if (typeof payload.source === "string" && payload.source) entrypoint = payload.source;
|
|
8177
9330
|
if (typeof payload.originator === "string" && payload.originator) originator = payload.originator;
|
|
8178
9331
|
if (typeof payload.cli_version === "string" && payload.cli_version) version = payload.cli_version;
|
|
8179
9332
|
const branch = payload.git && typeof payload.git === "object" ? payload.git.branch : void 0;
|
|
8180
9333
|
if (typeof branch === "string" && branch) gitBranch = branch;
|
|
8181
|
-
startedAt = parseTimestamp(payload.timestamp) ?? parseTimestamp(parsed.timestamp);
|
|
9334
|
+
startedAt = parseTimestamp$1(payload.timestamp) ?? parseTimestamp$1(parsed.timestamp);
|
|
8182
9335
|
continue;
|
|
8183
9336
|
}
|
|
8184
9337
|
if (!sawMeta) return null;
|
|
@@ -8186,11 +9339,11 @@ function parseCodexHistoryHead(head, promptChars) {
|
|
|
8186
9339
|
if (!payload || typeof payload !== "object") continue;
|
|
8187
9340
|
if (parsed.type === "event_msg") {
|
|
8188
9341
|
if (payload.type === "user_message" && typeof payload.message === "string") {
|
|
8189
|
-
const cleaned = oneLine(stripHarnessBlocks(payload.message), promptChars);
|
|
9342
|
+
const cleaned = oneLine$1(stripHarnessBlocks(payload.message), promptChars);
|
|
8190
9343
|
if (cleaned) firstPrompt = cleaned;
|
|
8191
9344
|
} else if (payload.type === "item_completed" && payload.item && payload.item.type === "UserMessage") {
|
|
8192
9345
|
const text = textFromUserContent(payload.item.content);
|
|
8193
|
-
const cleaned = text ? oneLine(stripHarnessBlocks(text), promptChars) : "";
|
|
9346
|
+
const cleaned = text ? oneLine$1(stripHarnessBlocks(text), promptChars) : "";
|
|
8194
9347
|
if (cleaned) firstPrompt = cleaned;
|
|
8195
9348
|
}
|
|
8196
9349
|
continue;
|
|
@@ -8198,7 +9351,7 @@ function parseCodexHistoryHead(head, promptChars) {
|
|
|
8198
9351
|
if (parsed.type === "response_item" && payload.type === "message" && payload.role === "user" && !fallbackPrompt) {
|
|
8199
9352
|
const text = textFromUserContent(payload.content);
|
|
8200
9353
|
if (text && !isHarnessUserText(text)) {
|
|
8201
|
-
const cleaned = oneLine(stripHarnessBlocks(text), promptChars);
|
|
9354
|
+
const cleaned = oneLine$1(stripHarnessBlocks(text), promptChars);
|
|
8202
9355
|
if (cleaned) fallbackPrompt = cleaned;
|
|
8203
9356
|
}
|
|
8204
9357
|
}
|
|
@@ -8228,7 +9381,7 @@ function parseCodexSessionIndex(text) {
|
|
|
8228
9381
|
continue;
|
|
8229
9382
|
}
|
|
8230
9383
|
if (!parsed || typeof parsed !== "object") continue;
|
|
8231
|
-
if (typeof parsed.id !== "string" || !UUID_RE$
|
|
9384
|
+
if (typeof parsed.id !== "string" || !UUID_RE$3.test(parsed.id)) continue;
|
|
8232
9385
|
if (typeof parsed.thread_name !== "string" || !parsed.thread_name.trim()) continue;
|
|
8233
9386
|
names.set(parsed.id.toLowerCase(), parsed.thread_name.trim());
|
|
8234
9387
|
}
|
|
@@ -8279,12 +9432,819 @@ async function listCodexSessionHistory(options) {
|
|
|
8279
9432
|
updatedAt: Math.round(candidate.updatedAt),
|
|
8280
9433
|
sizeBytes: candidate.sizeBytes,
|
|
8281
9434
|
...rest,
|
|
8282
|
-
...summary ? { summary: oneLine(summary, promptChars) } : {}
|
|
9435
|
+
...summary ? { summary: oneLine$1(summary, promptChars) } : {}
|
|
8283
9436
|
});
|
|
8284
9437
|
}
|
|
8285
9438
|
return { entries, truncated, scanned };
|
|
8286
9439
|
}
|
|
8287
9440
|
|
|
9441
|
+
const MAX_MESSAGE_CHARS = 4e3;
|
|
9442
|
+
const MAX_SESSION_CHARS = 1e6;
|
|
9443
|
+
const MAX_TITLE_CHARS = 200;
|
|
9444
|
+
const UUID_RE$2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
9445
|
+
function emptyDocState() {
|
|
9446
|
+
return { meta: {}, msgs: [], chars: 0 };
|
|
9447
|
+
}
|
|
9448
|
+
function oneLine(text, max) {
|
|
9449
|
+
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
9450
|
+
return collapsed.length > max ? `${collapsed.slice(0, max - 1)}\u2026` : collapsed;
|
|
9451
|
+
}
|
|
9452
|
+
function parseTimestamp(raw) {
|
|
9453
|
+
if (typeof raw === "number" && Number.isFinite(raw)) return raw;
|
|
9454
|
+
if (typeof raw === "string") {
|
|
9455
|
+
const t = Date.parse(raw);
|
|
9456
|
+
if (Number.isFinite(t)) return t;
|
|
9457
|
+
}
|
|
9458
|
+
return void 0;
|
|
9459
|
+
}
|
|
9460
|
+
function clip(text) {
|
|
9461
|
+
const trimmed = text.trim();
|
|
9462
|
+
return trimmed.length > MAX_MESSAGE_CHARS ? trimmed.slice(0, MAX_MESSAGE_CHARS) : trimmed;
|
|
9463
|
+
}
|
|
9464
|
+
function push(state, role, text, at) {
|
|
9465
|
+
if (state.full) return;
|
|
9466
|
+
const x = clip(text);
|
|
9467
|
+
if (!x) return;
|
|
9468
|
+
if (state.chars + x.length > MAX_SESSION_CHARS) {
|
|
9469
|
+
state.full = true;
|
|
9470
|
+
return;
|
|
9471
|
+
}
|
|
9472
|
+
state.msgs.push(at === void 0 ? { r: role, x } : { r: role, t: at, x });
|
|
9473
|
+
state.chars += x.length;
|
|
9474
|
+
}
|
|
9475
|
+
const CLAUDE_HARNESS_BLOCK = /<(system-reminder|task-notification|command-message|command-name|local-command-stdout|local-command-stderr|local-command-caveat|bash-input|bash-stdout|bash-stderr|user-prompt-submit-hook)>[\s\S]*?<\/\1>/g;
|
|
9476
|
+
const CLAUDE_HARNESS_TAG = /<\/?(system-reminder|task-notification|command-message|command-name|command-args|local-command-stdout|local-command-stderr|local-command-caveat|bash-input|bash-stdout|bash-stderr|user-prompt-submit-hook)>/g;
|
|
9477
|
+
function stripClaudeHarness(text) {
|
|
9478
|
+
return text.replace(CLAUDE_HARNESS_BLOCK, " ").replace(CLAUDE_HARNESS_TAG, " ");
|
|
9479
|
+
}
|
|
9480
|
+
function claudeText(content) {
|
|
9481
|
+
if (typeof content === "string") return content;
|
|
9482
|
+
if (!Array.isArray(content)) return null;
|
|
9483
|
+
const parts = [];
|
|
9484
|
+
for (const block of content) {
|
|
9485
|
+
if (block && typeof block === "object" && block.type === "text" && typeof block.text === "string") {
|
|
9486
|
+
parts.push(block.text);
|
|
9487
|
+
}
|
|
9488
|
+
}
|
|
9489
|
+
return parts.length > 0 ? parts.join("\n") : null;
|
|
9490
|
+
}
|
|
9491
|
+
function codexText(content) {
|
|
9492
|
+
if (typeof content === "string") return content;
|
|
9493
|
+
if (!Array.isArray(content)) return null;
|
|
9494
|
+
const parts = [];
|
|
9495
|
+
for (const part of content) {
|
|
9496
|
+
if (!part || typeof part !== "object") continue;
|
|
9497
|
+
const type = part.type;
|
|
9498
|
+
const text = part.text;
|
|
9499
|
+
if ((type === "input_text" || type === "output_text" || type === "text") && typeof text === "string") parts.push(text);
|
|
9500
|
+
}
|
|
9501
|
+
return parts.length > 0 ? parts.join("\n") : null;
|
|
9502
|
+
}
|
|
9503
|
+
function lineMayMatter(agent, line) {
|
|
9504
|
+
if (agent === "claude") {
|
|
9505
|
+
return line.includes('"user"') || line.includes('"assistant"') || line.includes('"summary"');
|
|
9506
|
+
}
|
|
9507
|
+
return line.includes('"session_meta"') || line.includes('"message"');
|
|
9508
|
+
}
|
|
9509
|
+
function extractClaudeLine(state, line) {
|
|
9510
|
+
if (state.meta.skip || !line || !lineMayMatter("claude", line)) return;
|
|
9511
|
+
let parsed;
|
|
9512
|
+
try {
|
|
9513
|
+
parsed = JSON.parse(line);
|
|
9514
|
+
} catch {
|
|
9515
|
+
return;
|
|
9516
|
+
}
|
|
9517
|
+
if (!parsed || typeof parsed !== "object") return;
|
|
9518
|
+
const meta = state.meta;
|
|
9519
|
+
if (parsed.type === "summary") {
|
|
9520
|
+
if (typeof parsed.summary === "string" && !meta.summary) {
|
|
9521
|
+
const s = oneLine(parsed.summary, MAX_TITLE_CHARS);
|
|
9522
|
+
if (s) meta.summary = s;
|
|
9523
|
+
}
|
|
9524
|
+
return;
|
|
9525
|
+
}
|
|
9526
|
+
if (parsed.isSidechain) return;
|
|
9527
|
+
if (parsed.isMeta || parsed.isCompactSummary) return;
|
|
9528
|
+
if (typeof parsed.cwd === "string" && parsed.cwd && !meta.cwd) meta.cwd = parsed.cwd;
|
|
9529
|
+
if (typeof parsed.gitBranch === "string" && parsed.gitBranch && !meta.gitBranch) meta.gitBranch = parsed.gitBranch;
|
|
9530
|
+
if (typeof parsed.entrypoint === "string" && parsed.entrypoint && !meta.entrypoint) meta.entrypoint = parsed.entrypoint;
|
|
9531
|
+
if (typeof parsed.version === "string" && parsed.version && !meta.version) meta.version = parsed.version;
|
|
9532
|
+
const at = parseTimestamp(parsed.timestamp);
|
|
9533
|
+
if (meta.startedAt === void 0 && at !== void 0) meta.startedAt = at;
|
|
9534
|
+
if (parsed.type !== "user" && parsed.type !== "assistant") return;
|
|
9535
|
+
const raw = claudeText(parsed.message?.content);
|
|
9536
|
+
if (!raw) return;
|
|
9537
|
+
if (parsed.type === "user") {
|
|
9538
|
+
if (!meta.firstPrompt) {
|
|
9539
|
+
if (oneShotPrompts.isVeryHappyOneShotPrompt(raw)) {
|
|
9540
|
+
meta.skip = "one-shot";
|
|
9541
|
+
state.msgs = [];
|
|
9542
|
+
state.chars = 0;
|
|
9543
|
+
return;
|
|
9544
|
+
}
|
|
9545
|
+
}
|
|
9546
|
+
const text = stripClaudeHarness(raw);
|
|
9547
|
+
if (!meta.firstPrompt) {
|
|
9548
|
+
const first = oneLine(text, MAX_TITLE_CHARS);
|
|
9549
|
+
if (first) meta.firstPrompt = first;
|
|
9550
|
+
}
|
|
9551
|
+
push(state, "u", text, at);
|
|
9552
|
+
return;
|
|
9553
|
+
}
|
|
9554
|
+
push(state, "a", raw, at);
|
|
9555
|
+
}
|
|
9556
|
+
function extractCodexLine(state, line) {
|
|
9557
|
+
if (state.meta.skip || !line) return;
|
|
9558
|
+
const meta = state.meta;
|
|
9559
|
+
if (!meta.sawMeta) {
|
|
9560
|
+
let first;
|
|
9561
|
+
try {
|
|
9562
|
+
first = JSON.parse(line);
|
|
9563
|
+
} catch {
|
|
9564
|
+
meta.skip = "not-rollout";
|
|
9565
|
+
return;
|
|
9566
|
+
}
|
|
9567
|
+
const payload2 = first?.payload;
|
|
9568
|
+
if (first?.type !== "session_meta" || !payload2 || typeof payload2 !== "object") {
|
|
9569
|
+
meta.skip = "not-rollout";
|
|
9570
|
+
return;
|
|
9571
|
+
}
|
|
9572
|
+
if (isCodexSubThread(payload2)) {
|
|
9573
|
+
meta.skip = "subthread";
|
|
9574
|
+
return;
|
|
9575
|
+
}
|
|
9576
|
+
meta.sawMeta = true;
|
|
9577
|
+
if (typeof payload2.id === "string" && UUID_RE$2.test(payload2.id)) meta.id = payload2.id.toLowerCase();
|
|
9578
|
+
if (typeof payload2.cwd === "string" && payload2.cwd) meta.cwd = payload2.cwd;
|
|
9579
|
+
if (typeof payload2.source === "string" && payload2.source) meta.entrypoint = payload2.source;
|
|
9580
|
+
if (typeof payload2.originator === "string" && payload2.originator) meta.originator = payload2.originator;
|
|
9581
|
+
if (typeof payload2.cli_version === "string" && payload2.cli_version) meta.version = payload2.cli_version;
|
|
9582
|
+
const branch = payload2.git && typeof payload2.git === "object" ? payload2.git.branch : void 0;
|
|
9583
|
+
if (typeof branch === "string" && branch) meta.gitBranch = branch;
|
|
9584
|
+
const at2 = parseTimestamp(payload2.timestamp) ?? parseTimestamp(first.timestamp);
|
|
9585
|
+
if (at2 !== void 0) meta.startedAt = at2;
|
|
9586
|
+
return;
|
|
9587
|
+
}
|
|
9588
|
+
if (!lineMayMatter("codex", line)) return;
|
|
9589
|
+
let parsed;
|
|
9590
|
+
try {
|
|
9591
|
+
parsed = JSON.parse(line);
|
|
9592
|
+
} catch {
|
|
9593
|
+
return;
|
|
9594
|
+
}
|
|
9595
|
+
const payload = parsed?.payload;
|
|
9596
|
+
if (parsed?.type !== "response_item" || !payload || payload.type !== "message") return;
|
|
9597
|
+
if (payload.role !== "user" && payload.role !== "assistant") return;
|
|
9598
|
+
const raw = codexText(payload.content);
|
|
9599
|
+
if (!raw) return;
|
|
9600
|
+
const at = parseTimestamp(parsed.timestamp);
|
|
9601
|
+
if (payload.role === "user") {
|
|
9602
|
+
if (isHarnessUserText(raw)) return;
|
|
9603
|
+
const text = stripHarnessBlocks(raw);
|
|
9604
|
+
if (!meta.firstPrompt) {
|
|
9605
|
+
const first = oneLine(text, MAX_TITLE_CHARS);
|
|
9606
|
+
if (first) meta.firstPrompt = first;
|
|
9607
|
+
}
|
|
9608
|
+
push(state, "u", text, at);
|
|
9609
|
+
return;
|
|
9610
|
+
}
|
|
9611
|
+
push(state, "a", raw, at);
|
|
9612
|
+
}
|
|
9613
|
+
function extractLine(agent, state, line) {
|
|
9614
|
+
if (agent === "claude") extractClaudeLine(state, line);
|
|
9615
|
+
else extractCodexLine(state, line);
|
|
9616
|
+
}
|
|
9617
|
+
|
|
9618
|
+
const INDEX_VERSION = 1;
|
|
9619
|
+
const INDEX_FILE_NAME = `index.v${INDEX_VERSION}.jsonl`;
|
|
9620
|
+
const DEFAULT_MAX_TEXT_CHARS = 32e6;
|
|
9621
|
+
function planFile(prev, cur) {
|
|
9622
|
+
if (!prev) return "reread";
|
|
9623
|
+
if (prev.ino !== cur.ino) return "reread";
|
|
9624
|
+
if (cur.size === prev.size && cur.mtimeMs === prev.mtimeMs) return "skip";
|
|
9625
|
+
if (cur.size < prev.offset || cur.size < prev.size) return "reread";
|
|
9626
|
+
if (cur.size === prev.size) return "reread";
|
|
9627
|
+
return prev.evicted ? "reread" : "append";
|
|
9628
|
+
}
|
|
9629
|
+
function totalChars(records) {
|
|
9630
|
+
let sum = 0;
|
|
9631
|
+
for (const r of records) sum += r.chars;
|
|
9632
|
+
return sum;
|
|
9633
|
+
}
|
|
9634
|
+
function enforceTextCap(records, maxChars) {
|
|
9635
|
+
let total = totalChars(records.values());
|
|
9636
|
+
if (total <= maxChars) return 0;
|
|
9637
|
+
const byAge = [...records.values()].filter((r) => r.chars > 0).sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
9638
|
+
let evicted = 0;
|
|
9639
|
+
for (const r of byAge) {
|
|
9640
|
+
if (total <= maxChars) break;
|
|
9641
|
+
total -= r.chars;
|
|
9642
|
+
r.msgs = [];
|
|
9643
|
+
r.chars = 0;
|
|
9644
|
+
r.evicted = true;
|
|
9645
|
+
evicted += 1;
|
|
9646
|
+
}
|
|
9647
|
+
return evicted;
|
|
9648
|
+
}
|
|
9649
|
+
function serializeIndex(records, savedAt) {
|
|
9650
|
+
const lines = [JSON.stringify({ v: INDEX_VERSION, savedAt })];
|
|
9651
|
+
for (const r of records) lines.push(JSON.stringify(r));
|
|
9652
|
+
return lines.join("\n") + "\n";
|
|
9653
|
+
}
|
|
9654
|
+
function isRecord(value) {
|
|
9655
|
+
return !!value && typeof value === "object" && typeof value.path === "string" && (value.agent === "claude" || value.agent === "codex") && typeof value.id === "string" && typeof value.ino === "number" && typeof value.size === "number" && typeof value.mtimeMs === "number" && typeof value.offset === "number" && !!value.meta && typeof value.meta === "object" && Array.isArray(value.msgs) && typeof value.chars === "number";
|
|
9656
|
+
}
|
|
9657
|
+
function parseIndex(text) {
|
|
9658
|
+
const lines = text.split("\n");
|
|
9659
|
+
let header;
|
|
9660
|
+
try {
|
|
9661
|
+
header = JSON.parse(lines[0] ?? "");
|
|
9662
|
+
} catch {
|
|
9663
|
+
return null;
|
|
9664
|
+
}
|
|
9665
|
+
if (!header || header.v !== INDEX_VERSION || typeof header.savedAt !== "number") return null;
|
|
9666
|
+
const records = /* @__PURE__ */ new Map();
|
|
9667
|
+
for (let i = 1; i < lines.length; i++) {
|
|
9668
|
+
const line = lines[i];
|
|
9669
|
+
if (!line) continue;
|
|
9670
|
+
let parsed;
|
|
9671
|
+
try {
|
|
9672
|
+
parsed = JSON.parse(line);
|
|
9673
|
+
} catch {
|
|
9674
|
+
continue;
|
|
9675
|
+
}
|
|
9676
|
+
if (isRecord(parsed)) records.set(parsed.path, parsed);
|
|
9677
|
+
}
|
|
9678
|
+
return { savedAt: header.savedAt, records };
|
|
9679
|
+
}
|
|
9680
|
+
function indexFilePath(indexDir) {
|
|
9681
|
+
return path.join(indexDir, INDEX_FILE_NAME);
|
|
9682
|
+
}
|
|
9683
|
+
async function loadIndex(indexDir) {
|
|
9684
|
+
let text;
|
|
9685
|
+
try {
|
|
9686
|
+
text = await promises.readFile(indexFilePath(indexDir), "utf-8");
|
|
9687
|
+
} catch {
|
|
9688
|
+
return null;
|
|
9689
|
+
}
|
|
9690
|
+
return parseIndex(text);
|
|
9691
|
+
}
|
|
9692
|
+
async function saveIndex(indexDir, records, savedAt) {
|
|
9693
|
+
await promises.mkdir(indexDir, { recursive: true, mode: 448 });
|
|
9694
|
+
try {
|
|
9695
|
+
await promises.chmod(indexDir, 448);
|
|
9696
|
+
} catch {
|
|
9697
|
+
}
|
|
9698
|
+
const target = indexFilePath(indexDir);
|
|
9699
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
9700
|
+
await promises.writeFile(tmp, serializeIndex(records, savedAt), { mode: 384 });
|
|
9701
|
+
await promises.rename(tmp, target);
|
|
9702
|
+
}
|
|
9703
|
+
async function clearIndexDir(indexDir) {
|
|
9704
|
+
await promises.rm(indexDir, { recursive: true, force: true });
|
|
9705
|
+
}
|
|
9706
|
+
|
|
9707
|
+
const UUID_RE$1 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
9708
|
+
const ROLLOUT_RE = /^rollout-.*-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i;
|
|
9709
|
+
const MAX_LINE_BYTES = 2 * 1024 * 1024;
|
|
9710
|
+
const READ_CHUNK_BYTES = 1024 * 1024;
|
|
9711
|
+
async function statFile(path) {
|
|
9712
|
+
try {
|
|
9713
|
+
const s = await promises.stat(path);
|
|
9714
|
+
if (!s.isFile()) return null;
|
|
9715
|
+
return { ino: Number(s.ino) || 0, size: s.size, mtimeMs: s.mtimeMs };
|
|
9716
|
+
} catch {
|
|
9717
|
+
return null;
|
|
9718
|
+
}
|
|
9719
|
+
}
|
|
9720
|
+
async function listDir(path) {
|
|
9721
|
+
try {
|
|
9722
|
+
return await promises.readdir(path);
|
|
9723
|
+
} catch {
|
|
9724
|
+
return [];
|
|
9725
|
+
}
|
|
9726
|
+
}
|
|
9727
|
+
async function isDir(path) {
|
|
9728
|
+
try {
|
|
9729
|
+
return (await promises.stat(path)).isDirectory();
|
|
9730
|
+
} catch {
|
|
9731
|
+
return false;
|
|
9732
|
+
}
|
|
9733
|
+
}
|
|
9734
|
+
async function discoverClaudeFiles(projectsRoot) {
|
|
9735
|
+
const out = [];
|
|
9736
|
+
for (const dirName of await listDir(projectsRoot)) {
|
|
9737
|
+
const dir = path.join(projectsRoot, dirName);
|
|
9738
|
+
if (!await isDir(dir)) continue;
|
|
9739
|
+
for (const name of await listDir(dir)) {
|
|
9740
|
+
if (!name.endsWith(".jsonl")) continue;
|
|
9741
|
+
const id = name.slice(0, -".jsonl".length);
|
|
9742
|
+
if (!UUID_RE$1.test(id)) continue;
|
|
9743
|
+
const path$1 = path.join(dir, name);
|
|
9744
|
+
const s = await statFile(path$1);
|
|
9745
|
+
if (!s || s.size === 0) continue;
|
|
9746
|
+
out.push({ path: path$1, agent: "claude", id: id.toLowerCase(), ...s });
|
|
9747
|
+
}
|
|
9748
|
+
}
|
|
9749
|
+
return out;
|
|
9750
|
+
}
|
|
9751
|
+
async function discoverCodexFiles(sessionsRoot) {
|
|
9752
|
+
const out = [];
|
|
9753
|
+
async function walk(dir, depth) {
|
|
9754
|
+
for (const name of await listDir(dir)) {
|
|
9755
|
+
const full = path.join(dir, name);
|
|
9756
|
+
const match = ROLLOUT_RE.exec(name);
|
|
9757
|
+
if (match) {
|
|
9758
|
+
const s = await statFile(full);
|
|
9759
|
+
if (s && s.size > 0) out.push({ path: full, agent: "codex", id: match[1].toLowerCase(), ...s });
|
|
9760
|
+
continue;
|
|
9761
|
+
}
|
|
9762
|
+
if (name.endsWith(".jsonl") || depth >= 4) continue;
|
|
9763
|
+
if (await isDir(full)) await walk(full, depth + 1);
|
|
9764
|
+
}
|
|
9765
|
+
}
|
|
9766
|
+
await walk(sessionsRoot, 0);
|
|
9767
|
+
return out;
|
|
9768
|
+
}
|
|
9769
|
+
async function readCompleteLines(path, start, onLine, maxLineBytes = MAX_LINE_BYTES) {
|
|
9770
|
+
const fh = await promises.open(path, "r");
|
|
9771
|
+
try {
|
|
9772
|
+
const buf = Buffer.allocUnsafe(READ_CHUNK_BYTES);
|
|
9773
|
+
let pos = start;
|
|
9774
|
+
let committed = start;
|
|
9775
|
+
let pending = [];
|
|
9776
|
+
let pendingLen = 0;
|
|
9777
|
+
let skipping = false;
|
|
9778
|
+
for (; ; ) {
|
|
9779
|
+
const { bytesRead } = await fh.read(buf, 0, buf.length, pos);
|
|
9780
|
+
if (bytesRead === 0) break;
|
|
9781
|
+
let i = 0;
|
|
9782
|
+
while (i < bytesRead) {
|
|
9783
|
+
const nl = buf.indexOf(10, i);
|
|
9784
|
+
if (nl === -1 || nl >= bytesRead) {
|
|
9785
|
+
const rest = bytesRead - i;
|
|
9786
|
+
if (!skipping) {
|
|
9787
|
+
if (pendingLen + rest > maxLineBytes) {
|
|
9788
|
+
skipping = true;
|
|
9789
|
+
pending = [];
|
|
9790
|
+
pendingLen = 0;
|
|
9791
|
+
} else {
|
|
9792
|
+
pending.push(Buffer.from(buf.subarray(i, bytesRead)));
|
|
9793
|
+
pendingLen += rest;
|
|
9794
|
+
}
|
|
9795
|
+
}
|
|
9796
|
+
break;
|
|
9797
|
+
}
|
|
9798
|
+
if (!skipping) {
|
|
9799
|
+
const seg = buf.subarray(i, nl);
|
|
9800
|
+
if (pendingLen + seg.length <= maxLineBytes) {
|
|
9801
|
+
const line = pendingLen > 0 ? Buffer.concat([...pending, seg]).toString("utf-8") : seg.toString("utf-8");
|
|
9802
|
+
onLine(line.endsWith("\r") ? line.slice(0, -1) : line);
|
|
9803
|
+
}
|
|
9804
|
+
}
|
|
9805
|
+
pending = [];
|
|
9806
|
+
pendingLen = 0;
|
|
9807
|
+
skipping = false;
|
|
9808
|
+
committed = pos + nl + 1;
|
|
9809
|
+
i = nl + 1;
|
|
9810
|
+
}
|
|
9811
|
+
pos += bytesRead;
|
|
9812
|
+
}
|
|
9813
|
+
return committed;
|
|
9814
|
+
} finally {
|
|
9815
|
+
await fh.close();
|
|
9816
|
+
}
|
|
9817
|
+
}
|
|
9818
|
+
async function indexFile(file, prev, plan) {
|
|
9819
|
+
const base = plan === "append" && prev ? { meta: { ...prev.meta }, msgs: prev.msgs.slice(), chars: prev.chars, ...prev.full ? { full: true } : {} } : emptyDocState();
|
|
9820
|
+
const start = plan === "append" && prev ? prev.offset : 0;
|
|
9821
|
+
let offset = start;
|
|
9822
|
+
if (!base.meta.skip) {
|
|
9823
|
+
offset = await readCompleteLines(file.path, start, (line) => extractLine(file.agent, base, line));
|
|
9824
|
+
} else {
|
|
9825
|
+
offset = file.size;
|
|
9826
|
+
}
|
|
9827
|
+
if (base.meta.skip) {
|
|
9828
|
+
base.msgs = [];
|
|
9829
|
+
base.chars = 0;
|
|
9830
|
+
}
|
|
9831
|
+
return {
|
|
9832
|
+
path: file.path,
|
|
9833
|
+
agent: file.agent,
|
|
9834
|
+
id: file.agent === "codex" && base.meta.id ? base.meta.id : file.id,
|
|
9835
|
+
ino: file.ino,
|
|
9836
|
+
size: file.size,
|
|
9837
|
+
mtimeMs: file.mtimeMs,
|
|
9838
|
+
offset,
|
|
9839
|
+
meta: base.meta,
|
|
9840
|
+
msgs: base.msgs,
|
|
9841
|
+
chars: base.chars,
|
|
9842
|
+
...base.full ? { full: true } : {}
|
|
9843
|
+
};
|
|
9844
|
+
}
|
|
9845
|
+
const yieldToLoop = () => new Promise((resolve) => setImmediate(resolve));
|
|
9846
|
+
async function refreshRecords(records, options) {
|
|
9847
|
+
const now = options.now ?? Date.now;
|
|
9848
|
+
const cancelled = () => options.isCancelled?.() === true;
|
|
9849
|
+
const [claude, codex, names] = await Promise.all([
|
|
9850
|
+
discoverClaudeFiles(options.roots.claudeProjectsRoot),
|
|
9851
|
+
discoverCodexFiles(options.roots.codexSessionsRoot),
|
|
9852
|
+
promises.readFile(options.roots.codexIndexPath, "utf-8").then(parseCodexSessionIndex, () => /* @__PURE__ */ new Map())
|
|
9853
|
+
]);
|
|
9854
|
+
if (cancelled()) return { changed: false, cancelled: true, evicted: 0, filesTotal: 0 };
|
|
9855
|
+
const files = [...claude, ...codex];
|
|
9856
|
+
files.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
9857
|
+
const total = files.length;
|
|
9858
|
+
let changed = false;
|
|
9859
|
+
const present = new Set(files.map((f) => f.path));
|
|
9860
|
+
for (const path of [...records.keys()]) {
|
|
9861
|
+
if (!present.has(path)) {
|
|
9862
|
+
records.delete(path);
|
|
9863
|
+
changed = true;
|
|
9864
|
+
}
|
|
9865
|
+
}
|
|
9866
|
+
let done = 0;
|
|
9867
|
+
let lastCheckpoint = now();
|
|
9868
|
+
options.onProgress?.({ filesTotal: total, filesDone: 0 });
|
|
9869
|
+
for (const file of files) {
|
|
9870
|
+
if (cancelled()) return { changed, cancelled: true, evicted: 0, filesTotal: total };
|
|
9871
|
+
const prev = records.get(file.path);
|
|
9872
|
+
const plan = planFile(prev, file);
|
|
9873
|
+
if (plan !== "skip") {
|
|
9874
|
+
try {
|
|
9875
|
+
const next = await indexFile(file, prev, plan);
|
|
9876
|
+
if (cancelled()) return { changed, cancelled: true, evicted: 0, filesTotal: total };
|
|
9877
|
+
records.set(file.path, next);
|
|
9878
|
+
changed = true;
|
|
9879
|
+
} catch {
|
|
9880
|
+
}
|
|
9881
|
+
await yieldToLoop();
|
|
9882
|
+
}
|
|
9883
|
+
done += 1;
|
|
9884
|
+
if (done % 50 === 0 || done === total) options.onProgress?.({ filesTotal: total, filesDone: done });
|
|
9885
|
+
if (options.checkpoint && changed && now() - lastCheckpoint >= (options.checkpointEveryMs ?? 2e4)) {
|
|
9886
|
+
lastCheckpoint = now();
|
|
9887
|
+
await options.checkpoint();
|
|
9888
|
+
}
|
|
9889
|
+
}
|
|
9890
|
+
for (const record of records.values()) {
|
|
9891
|
+
if (record.agent !== "codex") continue;
|
|
9892
|
+
const name = names.get(record.id);
|
|
9893
|
+
if (name && record.meta.summary !== name) {
|
|
9894
|
+
record.meta = { ...record.meta, summary: name };
|
|
9895
|
+
changed = true;
|
|
9896
|
+
}
|
|
9897
|
+
}
|
|
9898
|
+
const evicted = enforceTextCap(records, options.maxTextChars);
|
|
9899
|
+
if (evicted > 0) changed = true;
|
|
9900
|
+
options.onProgress?.({ filesTotal: total, filesDone: total });
|
|
9901
|
+
return { changed, cancelled: false, evicted, filesTotal: total };
|
|
9902
|
+
}
|
|
9903
|
+
|
|
9904
|
+
const MAX_QUERY_CHARS = 200;
|
|
9905
|
+
const MAX_QUERY_TERMS = 8;
|
|
9906
|
+
const MAX_RESULTS = 50;
|
|
9907
|
+
const DEFAULT_RESULTS = 20;
|
|
9908
|
+
const MAX_SNIPPETS = 3;
|
|
9909
|
+
const SNIPPET_CHARS = 180;
|
|
9910
|
+
const SNIPPET_LEAD = 50;
|
|
9911
|
+
function escapeRegExp(text) {
|
|
9912
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
9913
|
+
}
|
|
9914
|
+
function parseQuery(input) {
|
|
9915
|
+
if (typeof input !== "string") return null;
|
|
9916
|
+
const raw = input.replace(/\s+/g, " ").trim().slice(0, MAX_QUERY_CHARS).trim();
|
|
9917
|
+
if (!raw) return null;
|
|
9918
|
+
const words = [...new Set(raw.split(" ").filter(Boolean).map((w) => w.toLowerCase()))].slice(0, MAX_QUERY_TERMS);
|
|
9919
|
+
const terms = words.map((w) => new RegExp(escapeRegExp(w), "i"));
|
|
9920
|
+
const phrase = words.length > 1 ? new RegExp(raw.split(" ").map(escapeRegExp).join("\\s+"), "i") : null;
|
|
9921
|
+
return { terms, phrase, raw };
|
|
9922
|
+
}
|
|
9923
|
+
function matchRanges(text, terms) {
|
|
9924
|
+
const ranges = [];
|
|
9925
|
+
for (const term of terms) {
|
|
9926
|
+
const global = new RegExp(term.source, "gi");
|
|
9927
|
+
let m;
|
|
9928
|
+
while ((m = global.exec(text)) !== null) {
|
|
9929
|
+
if (m[0].length === 0) {
|
|
9930
|
+
global.lastIndex += 1;
|
|
9931
|
+
continue;
|
|
9932
|
+
}
|
|
9933
|
+
ranges.push([m.index, m.index + m[0].length]);
|
|
9934
|
+
if (ranges.length > 64) break;
|
|
9935
|
+
}
|
|
9936
|
+
}
|
|
9937
|
+
ranges.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
|
|
9938
|
+
const merged = [];
|
|
9939
|
+
for (const r of ranges) {
|
|
9940
|
+
const last = merged[merged.length - 1];
|
|
9941
|
+
if (last && r[0] <= last[1]) last[1] = Math.max(last[1], r[1]);
|
|
9942
|
+
else merged.push([r[0], r[1]]);
|
|
9943
|
+
}
|
|
9944
|
+
return merged;
|
|
9945
|
+
}
|
|
9946
|
+
function buildSnippet(text, terms) {
|
|
9947
|
+
let first = -1;
|
|
9948
|
+
for (const term of terms) {
|
|
9949
|
+
const m = term.exec(text);
|
|
9950
|
+
if (m && (first === -1 || m.index < first)) first = m.index;
|
|
9951
|
+
}
|
|
9952
|
+
const start = Math.max(0, (first === -1 ? 0 : first) - SNIPPET_LEAD);
|
|
9953
|
+
const end = Math.min(text.length, start + SNIPPET_CHARS);
|
|
9954
|
+
let window = text.slice(start, end).replace(/\s+/g, " ");
|
|
9955
|
+
if (start > 0) window = `\u2026${window.trimStart()}`;
|
|
9956
|
+
if (end < text.length) window = `${window.trimEnd()}\u2026`;
|
|
9957
|
+
return { text: window, ranges: matchRanges(window, terms) };
|
|
9958
|
+
}
|
|
9959
|
+
function titleOf(record) {
|
|
9960
|
+
return record.meta.summary || record.meta.firstPrompt || "";
|
|
9961
|
+
}
|
|
9962
|
+
function searchRecords(records, query, options = {}) {
|
|
9963
|
+
const limit = Math.max(1, Math.min(Math.floor(options.limit ?? DEFAULT_RESULTS), MAX_RESULTS));
|
|
9964
|
+
const newest = /* @__PURE__ */ new Map();
|
|
9965
|
+
for (const r of records) {
|
|
9966
|
+
if (r.meta.skip || !r.meta.cwd) continue;
|
|
9967
|
+
if (options.agent && r.agent !== options.agent) continue;
|
|
9968
|
+
if (!r.meta.firstPrompt && !r.meta.summary) continue;
|
|
9969
|
+
const key = `${r.agent}:${r.id}`;
|
|
9970
|
+
const prev = newest.get(key);
|
|
9971
|
+
if (!prev || r.mtimeMs > prev.mtimeMs) newest.set(key, r);
|
|
9972
|
+
}
|
|
9973
|
+
const scored = [];
|
|
9974
|
+
for (const record of newest.values()) {
|
|
9975
|
+
const title = titleOf(record);
|
|
9976
|
+
const head = `${title}
|
|
9977
|
+
${record.meta.firstPrompt ?? ""}
|
|
9978
|
+
${record.meta.cwd ?? ""}`;
|
|
9979
|
+
const found = new Array(query.terms.length).fill(false);
|
|
9980
|
+
query.terms.forEach((t, i) => {
|
|
9981
|
+
if (t.test(head)) found[i] = true;
|
|
9982
|
+
});
|
|
9983
|
+
const matched = [];
|
|
9984
|
+
let phrase = query.phrase ? query.phrase.test(head) : true;
|
|
9985
|
+
record.msgs.forEach((msg, index) => {
|
|
9986
|
+
let n = 0;
|
|
9987
|
+
query.terms.forEach((t, i) => {
|
|
9988
|
+
if (t.test(msg.x)) {
|
|
9989
|
+
n += 1;
|
|
9990
|
+
found[i] = true;
|
|
9991
|
+
}
|
|
9992
|
+
});
|
|
9993
|
+
if (n > 0) {
|
|
9994
|
+
matched.push({ msg, hits: n, index });
|
|
9995
|
+
if (!phrase && query.phrase && query.phrase.test(msg.x)) phrase = true;
|
|
9996
|
+
}
|
|
9997
|
+
});
|
|
9998
|
+
if (!found.every(Boolean)) continue;
|
|
9999
|
+
scored.push({ record, phrase, matched });
|
|
10000
|
+
}
|
|
10001
|
+
scored.sort((a, b) => Number(b.phrase) - Number(a.phrase) || b.record.mtimeMs - a.record.mtimeMs);
|
|
10002
|
+
const results = scored.slice(0, limit).map(({ record, matched }) => {
|
|
10003
|
+
const picks = matched.slice().sort((a, b) => b.hits - a.hits || a.index - b.index).slice(0, MAX_SNIPPETS).sort((a, b) => a.index - b.index);
|
|
10004
|
+
const meta = record.meta;
|
|
10005
|
+
return {
|
|
10006
|
+
agent: record.agent,
|
|
10007
|
+
id: record.id,
|
|
10008
|
+
title: titleOf(record),
|
|
10009
|
+
firstPrompt: meta.firstPrompt ?? meta.summary ?? "",
|
|
10010
|
+
...meta.summary ? { summary: meta.summary } : {},
|
|
10011
|
+
cwd: meta.cwd ?? "",
|
|
10012
|
+
startedAt: meta.startedAt ?? 0,
|
|
10013
|
+
updatedAt: Math.round(record.mtimeMs),
|
|
10014
|
+
sizeBytes: record.size,
|
|
10015
|
+
...meta.gitBranch ? { gitBranch: meta.gitBranch } : {},
|
|
10016
|
+
...meta.entrypoint ? { entrypoint: meta.entrypoint } : {},
|
|
10017
|
+
...meta.originator ? { originator: meta.originator } : {},
|
|
10018
|
+
matchCount: matched.length,
|
|
10019
|
+
snippets: picks.map(({ msg }) => {
|
|
10020
|
+
const s = buildSnippet(msg.x, query.terms);
|
|
10021
|
+
return {
|
|
10022
|
+
role: msg.r === "u" ? "user" : "assistant",
|
|
10023
|
+
text: s.text,
|
|
10024
|
+
...msg.t !== void 0 ? { at: msg.t } : {},
|
|
10025
|
+
ranges: s.ranges
|
|
10026
|
+
};
|
|
10027
|
+
})
|
|
10028
|
+
};
|
|
10029
|
+
});
|
|
10030
|
+
return { results, truncated: scored.length > limit, total: scored.length };
|
|
10031
|
+
}
|
|
10032
|
+
|
|
10033
|
+
class HistorySearchService {
|
|
10034
|
+
records = null;
|
|
10035
|
+
loading = null;
|
|
10036
|
+
refreshing = null;
|
|
10037
|
+
progress = { filesTotal: 0, filesDone: 0 };
|
|
10038
|
+
lastRefreshStartedAt = 0;
|
|
10039
|
+
lastIndexedAt;
|
|
10040
|
+
error;
|
|
10041
|
+
/** Bumped by `clear`; a pass started under an older generation stops. */
|
|
10042
|
+
generation = 0;
|
|
10043
|
+
/** A `clear` in progress. Searches arriving meanwhile neither load the
|
|
10044
|
+
* old file back nor start a pass that would recreate the directory. */
|
|
10045
|
+
clearing = null;
|
|
10046
|
+
idleTimer = null;
|
|
10047
|
+
opts;
|
|
10048
|
+
constructor(options) {
|
|
10049
|
+
this.opts = {
|
|
10050
|
+
maxTextChars: DEFAULT_MAX_TEXT_CHARS,
|
|
10051
|
+
refreshIntervalMs: 3e4,
|
|
10052
|
+
idleUnloadMs: 10 * 6e4,
|
|
10053
|
+
loadWaitMs: 5e3,
|
|
10054
|
+
now: Date.now,
|
|
10055
|
+
...options
|
|
10056
|
+
};
|
|
10057
|
+
}
|
|
10058
|
+
/** RPC entry point. Validates loosely; unknown fields are ignored. */
|
|
10059
|
+
async handle(params) {
|
|
10060
|
+
const p = params && typeof params === "object" ? params : {};
|
|
10061
|
+
const op = p.op === "status" || p.op === "clear" ? p.op : "search";
|
|
10062
|
+
if (op === "clear") {
|
|
10063
|
+
await this.clear();
|
|
10064
|
+
return { type: "success", status: this.status(), results: [], truncated: false };
|
|
10065
|
+
}
|
|
10066
|
+
if (op === "status") {
|
|
10067
|
+
return { type: "success", status: this.status(), results: [], truncated: false };
|
|
10068
|
+
}
|
|
10069
|
+
const agent = p.agent === "claude" || p.agent === "codex" ? p.agent : void 0;
|
|
10070
|
+
const limit = typeof p.limit === "number" && Number.isFinite(p.limit) ? p.limit : DEFAULT_RESULTS;
|
|
10071
|
+
return this.search(p.query, { agent, limit });
|
|
10072
|
+
}
|
|
10073
|
+
async search(query, options = {}) {
|
|
10074
|
+
if (this.clearing) {
|
|
10075
|
+
await this.clearing;
|
|
10076
|
+
return { type: "success", status: this.status(), results: [], truncated: false };
|
|
10077
|
+
}
|
|
10078
|
+
this.touch();
|
|
10079
|
+
await this.ensureLoaded();
|
|
10080
|
+
this.maybeRefresh();
|
|
10081
|
+
const parsed = parseQuery(query);
|
|
10082
|
+
if (!parsed || !this.records) {
|
|
10083
|
+
return { type: "success", status: this.status(), results: [], truncated: false };
|
|
10084
|
+
}
|
|
10085
|
+
const started = this.opts.now();
|
|
10086
|
+
const out = searchRecords(this.records.values(), parsed, options);
|
|
10087
|
+
logger.logger.debug(`[HISTORY SEARCH] query len=${parsed.raw.length} terms=${parsed.terms.length} \u2192 ${out.total} sessions (${this.opts.now() - started}ms)`);
|
|
10088
|
+
return { type: "success", status: this.status(), results: out.results, truncated: out.truncated };
|
|
10089
|
+
}
|
|
10090
|
+
status() {
|
|
10091
|
+
const records = this.records;
|
|
10092
|
+
let sessions = 0;
|
|
10093
|
+
let evicted = 0;
|
|
10094
|
+
if (records) {
|
|
10095
|
+
for (const r of records.values()) {
|
|
10096
|
+
if (r.meta.skip) continue;
|
|
10097
|
+
sessions += 1;
|
|
10098
|
+
if (r.evicted) evicted += 1;
|
|
10099
|
+
}
|
|
10100
|
+
}
|
|
10101
|
+
const state = this.refreshing || this.loading ? "indexing" : this.error ? "error" : this.lastIndexedAt !== void 0 ? "ready" : "idle";
|
|
10102
|
+
return {
|
|
10103
|
+
state,
|
|
10104
|
+
filesTotal: this.progress.filesTotal,
|
|
10105
|
+
filesDone: this.progress.filesDone,
|
|
10106
|
+
sessions,
|
|
10107
|
+
textChars: records ? totalChars(records.values()) : 0,
|
|
10108
|
+
maxTextChars: this.opts.maxTextChars,
|
|
10109
|
+
evicted,
|
|
10110
|
+
...this.lastIndexedAt !== void 0 ? { lastIndexedAt: this.lastIndexedAt } : {},
|
|
10111
|
+
...this.error ? { error: this.error } : {}
|
|
10112
|
+
};
|
|
10113
|
+
}
|
|
10114
|
+
async clear() {
|
|
10115
|
+
if (this.clearing) return this.clearing;
|
|
10116
|
+
const run = this.doClear().finally(() => {
|
|
10117
|
+
if (this.clearing === run) this.clearing = null;
|
|
10118
|
+
});
|
|
10119
|
+
this.clearing = run;
|
|
10120
|
+
return run;
|
|
10121
|
+
}
|
|
10122
|
+
async doClear() {
|
|
10123
|
+
const running = this.refreshing;
|
|
10124
|
+
this.generation += 1;
|
|
10125
|
+
this.records = null;
|
|
10126
|
+
this.loading = null;
|
|
10127
|
+
this.refreshing = null;
|
|
10128
|
+
this.progress = { filesTotal: 0, filesDone: 0 };
|
|
10129
|
+
this.lastRefreshStartedAt = 0;
|
|
10130
|
+
this.lastIndexedAt = void 0;
|
|
10131
|
+
this.error = void 0;
|
|
10132
|
+
if (this.idleTimer) {
|
|
10133
|
+
clearTimeout(this.idleTimer);
|
|
10134
|
+
this.idleTimer = null;
|
|
10135
|
+
}
|
|
10136
|
+
await running?.catch(() => void 0);
|
|
10137
|
+
await clearIndexDir(this.opts.indexDir);
|
|
10138
|
+
logger.logger.debug("[HISTORY SEARCH] index cleared");
|
|
10139
|
+
}
|
|
10140
|
+
/** Resolves when the running refresh (if any) is done — tests and scripts. */
|
|
10141
|
+
async whenIdle() {
|
|
10142
|
+
while (this.loading || this.refreshing) {
|
|
10143
|
+
await (this.loading ?? this.refreshing);
|
|
10144
|
+
}
|
|
10145
|
+
}
|
|
10146
|
+
touch() {
|
|
10147
|
+
if (this.idleTimer) clearTimeout(this.idleTimer);
|
|
10148
|
+
this.idleTimer = setTimeout(() => this.unload(), this.opts.idleUnloadMs);
|
|
10149
|
+
this.idleTimer.unref?.();
|
|
10150
|
+
}
|
|
10151
|
+
unload() {
|
|
10152
|
+
this.idleTimer = null;
|
|
10153
|
+
if (this.refreshing || this.loading) {
|
|
10154
|
+
this.touch();
|
|
10155
|
+
return;
|
|
10156
|
+
}
|
|
10157
|
+
this.records = null;
|
|
10158
|
+
logger.logger.debug("[HISTORY SEARCH] in-memory index unloaded (idle)");
|
|
10159
|
+
}
|
|
10160
|
+
async ensureLoaded() {
|
|
10161
|
+
if (this.clearing) await this.clearing;
|
|
10162
|
+
if (this.records) return;
|
|
10163
|
+
if (!this.loading) {
|
|
10164
|
+
const gen = this.generation;
|
|
10165
|
+
const run = (async () => {
|
|
10166
|
+
const loaded = await loadIndex(this.opts.indexDir);
|
|
10167
|
+
if (gen !== this.generation) return;
|
|
10168
|
+
this.records = loaded?.records ?? /* @__PURE__ */ new Map();
|
|
10169
|
+
if (loaded && this.lastIndexedAt === void 0) this.lastIndexedAt = loaded.savedAt;
|
|
10170
|
+
})().catch((error) => {
|
|
10171
|
+
if (gen !== this.generation) return;
|
|
10172
|
+
logger.logger.debug("[HISTORY SEARCH] index load failed; starting empty", error);
|
|
10173
|
+
this.records = /* @__PURE__ */ new Map();
|
|
10174
|
+
}).finally(() => {
|
|
10175
|
+
if (this.loading === run) this.loading = null;
|
|
10176
|
+
});
|
|
10177
|
+
this.loading = run;
|
|
10178
|
+
}
|
|
10179
|
+
const pending = this.loading;
|
|
10180
|
+
let timer;
|
|
10181
|
+
await Promise.race([
|
|
10182
|
+
pending,
|
|
10183
|
+
new Promise((resolve) => {
|
|
10184
|
+
timer = setTimeout(resolve, this.opts.loadWaitMs);
|
|
10185
|
+
timer.unref?.();
|
|
10186
|
+
})
|
|
10187
|
+
]);
|
|
10188
|
+
if (timer) clearTimeout(timer);
|
|
10189
|
+
}
|
|
10190
|
+
maybeRefresh() {
|
|
10191
|
+
if (this.clearing || this.refreshing || !this.records) return;
|
|
10192
|
+
const now = this.opts.now();
|
|
10193
|
+
if (this.lastRefreshStartedAt && now - this.lastRefreshStartedAt < this.opts.refreshIntervalMs) return;
|
|
10194
|
+
this.lastRefreshStartedAt = now;
|
|
10195
|
+
const gen = this.generation;
|
|
10196
|
+
const records = this.records;
|
|
10197
|
+
const save = async () => {
|
|
10198
|
+
if (gen !== this.generation || this.clearing) return;
|
|
10199
|
+
await saveIndex(this.opts.indexDir, records.values(), this.opts.now());
|
|
10200
|
+
};
|
|
10201
|
+
const run = (async () => {
|
|
10202
|
+
const roots = await this.opts.resolveRoots();
|
|
10203
|
+
if (gen !== this.generation) return;
|
|
10204
|
+
const started = this.opts.now();
|
|
10205
|
+
const result = await refreshRecords(records, {
|
|
10206
|
+
roots,
|
|
10207
|
+
maxTextChars: this.opts.maxTextChars,
|
|
10208
|
+
isCancelled: () => gen !== this.generation,
|
|
10209
|
+
onProgress: (progress) => {
|
|
10210
|
+
if (gen === this.generation) this.progress = progress;
|
|
10211
|
+
},
|
|
10212
|
+
checkpoint: async () => {
|
|
10213
|
+
if (gen === this.generation) await save();
|
|
10214
|
+
},
|
|
10215
|
+
now: this.opts.now
|
|
10216
|
+
});
|
|
10217
|
+
if (result.cancelled || gen !== this.generation) return;
|
|
10218
|
+
if (result.changed) await save();
|
|
10219
|
+
this.lastIndexedAt = this.opts.now();
|
|
10220
|
+
this.error = void 0;
|
|
10221
|
+
logger.logger.debug(`[HISTORY SEARCH] refresh: ${result.filesTotal} files, changed=${result.changed}, evicted=${result.evicted} (${this.opts.now() - started}ms)`);
|
|
10222
|
+
})().catch((error) => {
|
|
10223
|
+
if (gen !== this.generation) return;
|
|
10224
|
+
this.error = error instanceof Error ? error.message : String(error);
|
|
10225
|
+
logger.logger.debug("[HISTORY SEARCH] refresh failed", error);
|
|
10226
|
+
}).finally(() => {
|
|
10227
|
+
if (this.refreshing === run) this.refreshing = null;
|
|
10228
|
+
});
|
|
10229
|
+
this.refreshing = run;
|
|
10230
|
+
}
|
|
10231
|
+
}
|
|
10232
|
+
|
|
10233
|
+
async function resolveHistoryRoots() {
|
|
10234
|
+
const homes = await index.refreshAgentHomes(Date.now(), { allowStale: true });
|
|
10235
|
+
return {
|
|
10236
|
+
claudeProjectsRoot: path.join(homes.claudeConfigDir.path, "projects"),
|
|
10237
|
+
codexSessionsRoot: path.join(homes.codexHome.path, "sessions"),
|
|
10238
|
+
codexIndexPath: path.join(homes.codexHome.path, "session_index.jsonl")
|
|
10239
|
+
};
|
|
10240
|
+
}
|
|
10241
|
+
function historySearchIndexDir() {
|
|
10242
|
+
return path.join(configuration.configuration.happyHomeDir, "history-search");
|
|
10243
|
+
}
|
|
10244
|
+
function createHistorySearchService() {
|
|
10245
|
+
return new HistorySearchService({ indexDir: historySearchIndexDir(), resolveRoots: resolveHistoryRoots });
|
|
10246
|
+
}
|
|
10247
|
+
|
|
8288
10248
|
function selectLowestLatencyRelay(candidates, probes) {
|
|
8289
10249
|
const rttById = new Map(probes.map((probe) => [probe.relayId, probe.rttMs]));
|
|
8290
10250
|
let selected = null;
|
|
@@ -8403,6 +10363,74 @@ async function discoverAndClaimRelay(input) {
|
|
|
8403
10363
|
}
|
|
8404
10364
|
}
|
|
8405
10365
|
|
|
10366
|
+
const RATE_LIMIT_MIN_WRITE_INTERVAL_MS = 6e4;
|
|
10367
|
+
const MAX_PENDING_REPORTS = 16;
|
|
10368
|
+
class RateLimitPublisher {
|
|
10369
|
+
constructor(deps) {
|
|
10370
|
+
this.deps = deps;
|
|
10371
|
+
this.now = deps.now ?? Date.now;
|
|
10372
|
+
this.minIntervalMs = deps.minIntervalMs ?? RATE_LIMIT_MIN_WRITE_INTERVAL_MS;
|
|
10373
|
+
this.setTimer = deps.setTimer ?? ((fn, ms) => {
|
|
10374
|
+
const handle = setTimeout(fn, ms);
|
|
10375
|
+
handle.unref?.();
|
|
10376
|
+
return handle;
|
|
10377
|
+
});
|
|
10378
|
+
this.clearTimer = deps.clearTimer ?? ((handle) => clearTimeout(handle));
|
|
10379
|
+
}
|
|
10380
|
+
pending = [];
|
|
10381
|
+
timer = null;
|
|
10382
|
+
writing = false;
|
|
10383
|
+
lastWriteAt = 0;
|
|
10384
|
+
stopped = false;
|
|
10385
|
+
now;
|
|
10386
|
+
minIntervalMs;
|
|
10387
|
+
setTimer;
|
|
10388
|
+
clearTimer;
|
|
10389
|
+
report(report) {
|
|
10390
|
+
if (this.stopped) return;
|
|
10391
|
+
this.pending.push(report);
|
|
10392
|
+
if (this.pending.length > MAX_PENDING_REPORTS) {
|
|
10393
|
+
this.pending.splice(0, this.pending.length - MAX_PENDING_REPORTS);
|
|
10394
|
+
}
|
|
10395
|
+
this.schedule();
|
|
10396
|
+
}
|
|
10397
|
+
stop() {
|
|
10398
|
+
this.stopped = true;
|
|
10399
|
+
this.pending = [];
|
|
10400
|
+
if (this.timer) this.clearTimer(this.timer);
|
|
10401
|
+
this.timer = null;
|
|
10402
|
+
}
|
|
10403
|
+
schedule() {
|
|
10404
|
+
if (this.timer || this.writing || this.stopped || this.pending.length === 0) return;
|
|
10405
|
+
const wait = this.lastWriteAt === 0 ? 0 : Math.max(0, this.lastWriteAt + this.minIntervalMs - this.now());
|
|
10406
|
+
this.timer = this.setTimer(() => {
|
|
10407
|
+
this.timer = null;
|
|
10408
|
+
void this.flush();
|
|
10409
|
+
}, wait);
|
|
10410
|
+
}
|
|
10411
|
+
/** Exposed for tests; normally driven by the timer. */
|
|
10412
|
+
async flush() {
|
|
10413
|
+
if (this.writing || this.stopped) return;
|
|
10414
|
+
const reports = this.pending;
|
|
10415
|
+
this.pending = [];
|
|
10416
|
+
if (!rateLimitWindows.planRateLimitWrite(this.deps.getCurrent(), reports, this.now())) return;
|
|
10417
|
+
this.writing = true;
|
|
10418
|
+
this.lastWriteAt = this.now();
|
|
10419
|
+
let written = false;
|
|
10420
|
+
try {
|
|
10421
|
+
written = await this.deps.write(reports);
|
|
10422
|
+
} catch {
|
|
10423
|
+
written = false;
|
|
10424
|
+
} finally {
|
|
10425
|
+
this.writing = false;
|
|
10426
|
+
}
|
|
10427
|
+
if (!written && !this.stopped) {
|
|
10428
|
+
this.pending = [...reports, ...this.pending].slice(-MAX_PENDING_REPORTS);
|
|
10429
|
+
}
|
|
10430
|
+
this.schedule();
|
|
10431
|
+
}
|
|
10432
|
+
}
|
|
10433
|
+
|
|
8406
10434
|
const guardOptions = { log: (message) => logger.logger.debug(message) };
|
|
8407
10435
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
8408
10436
|
function requireNonEmptyString(value, name) {
|
|
@@ -8434,6 +10462,9 @@ class ApiMachineClient {
|
|
|
8434
10462
|
registerCommonHandlers.registerCommonHandlers(this.rpcHandlerManager, process.cwd());
|
|
8435
10463
|
registerFsHandlers(this.rpcHandlerManager);
|
|
8436
10464
|
registerTodoHandlers(this.rpcHandlerManager);
|
|
10465
|
+
if (typeof configuration.configuration.happyHomeDir === "string") {
|
|
10466
|
+
mirrorProtocol.registerWorktreeHandlers(this.rpcHandlerManager, mirrorProtocol.configureWorktreeService({ home: configuration.configuration.happyHomeDir, machineId: this.machine.id, log: (msg, err) => logger.logger.debug(msg, err) }));
|
|
10467
|
+
}
|
|
8437
10468
|
this.wireAutoRestoreReport();
|
|
8438
10469
|
}
|
|
8439
10470
|
socket;
|
|
@@ -8444,6 +10475,8 @@ class ApiMachineClient {
|
|
|
8444
10475
|
keepAliveInterval = null;
|
|
8445
10476
|
lastKnownCLIAvailability = null;
|
|
8446
10477
|
lastKnownResumeSupport = null;
|
|
10478
|
+
/** B-549: created on the first `history-search` call. */
|
|
10479
|
+
historySearch = null;
|
|
8447
10480
|
/** Wall-clock of the last CLI availability probe. Initialised as "already
|
|
8448
10481
|
* stale" so the FIRST keep-alive tick always probes and populates the
|
|
8449
10482
|
* machine metadata that a connecting/reconnecting web client reads. */
|
|
@@ -8453,6 +10486,13 @@ class ApiMachineClient {
|
|
|
8453
10486
|
cliUpdateState = null;
|
|
8454
10487
|
cliUpdatePushChain = Promise.resolve();
|
|
8455
10488
|
claudeAuthState = null;
|
|
10489
|
+
/** B-587: the machine's hibernation threshold, published as a capability. */
|
|
10490
|
+
agentHibernationIdleMs = null;
|
|
10491
|
+
rateLimitPushChain = Promise.resolve();
|
|
10492
|
+
rateLimitPublisher = new RateLimitPublisher({
|
|
10493
|
+
getCurrent: () => this.machine.daemonState?.rateLimits,
|
|
10494
|
+
write: (reports) => this.writeRateLimits(reports)
|
|
10495
|
+
});
|
|
8456
10496
|
rpcHandlerManager;
|
|
8457
10497
|
resumeSessionHandler = null;
|
|
8458
10498
|
restartSessionHandler = null;
|
|
@@ -8513,6 +10553,11 @@ class ApiMachineClient {
|
|
|
8513
10553
|
setTerminalTitle(terminalId, title, ifAbsent) {
|
|
8514
10554
|
return this.webTerminal.setTitle(terminalId, title, ifAbsent);
|
|
8515
10555
|
}
|
|
10556
|
+
/** B-547: a status hook forwarded from a daemon-launched claude inside a
|
|
10557
|
+
* web terminal (control server /terminal-agent-hook → here). */
|
|
10558
|
+
ingestTerminalAgentHook(body) {
|
|
10559
|
+
this.webTerminal.ingestAgentHook(body);
|
|
10560
|
+
}
|
|
8516
10561
|
/**
|
|
8517
10562
|
* Push text to the clipboard of every web client the user has open
|
|
8518
10563
|
* (terminal-path claude → `very-happy mcp` → daemon /clipboard → here).
|
|
@@ -8536,13 +10581,14 @@ class ApiMachineClient {
|
|
|
8536
10581
|
return { delivered: true, truncated: prepared.truncated, totalBytes: prepared.totalBytes };
|
|
8537
10582
|
}
|
|
8538
10583
|
/** Queue a terminal preview with the machine key. Delivery does not prove a browser opened it. */
|
|
8539
|
-
pushFilePreview(terminalId, path, mode = "file") {
|
|
10584
|
+
pushFilePreview(terminalId, path, mode = "file", intent) {
|
|
8540
10585
|
if (!this.socket?.connected) return { delivered: false, error: "daemon is not connected to the server" };
|
|
8541
10586
|
this.socket.emit("file-preview-push", {
|
|
8542
10587
|
terminalId,
|
|
8543
10588
|
payload: authRelay.encodeBase64(authRelay.encrypt(this.machine.encryptionKey, this.machine.encryptionVariant, path)),
|
|
8544
10589
|
enc: true,
|
|
8545
|
-
mode
|
|
10590
|
+
mode,
|
|
10591
|
+
...intent ? { intent } : {}
|
|
8546
10592
|
});
|
|
8547
10593
|
return { delivered: true };
|
|
8548
10594
|
}
|
|
@@ -8565,6 +10611,26 @@ class ApiMachineClient {
|
|
|
8565
10611
|
if (!this.socket?.connected) return;
|
|
8566
10612
|
this.cliUpdatePushChain = this.cliUpdatePushChain.then(() => this.updateDaemonState((current) => cliUpdate.withCurrentCliUpdateState(current, this.cliUpdateState))).catch((error) => logger.logger.debug("[API MACHINE] Failed to publish CLI update policy:", error));
|
|
8567
10613
|
}
|
|
10614
|
+
/** B-555: a wrapper's rate-limit observation; written throttled by the publisher. */
|
|
10615
|
+
reportRateLimits(report) {
|
|
10616
|
+
this.rateLimitPublisher.report(report);
|
|
10617
|
+
}
|
|
10618
|
+
writeRateLimits(reports) {
|
|
10619
|
+
if (!this.socket?.connected) return Promise.resolve(false);
|
|
10620
|
+
const attempt = this.rateLimitPushChain.then(() => this.updateDaemonState((current) => {
|
|
10621
|
+
const merged = rateLimitWindows.mergeRateLimitReports(current?.rateLimits, reports, Date.now());
|
|
10622
|
+
return {
|
|
10623
|
+
...current ?? {},
|
|
10624
|
+
status: current?.status ?? "running",
|
|
10625
|
+
...merged ? { rateLimits: merged } : {}
|
|
10626
|
+
};
|
|
10627
|
+
})).then(() => true).catch((error) => {
|
|
10628
|
+
logger.logger.debug("[API MACHINE] Failed to publish rate limits:", error);
|
|
10629
|
+
return false;
|
|
10630
|
+
});
|
|
10631
|
+
this.rateLimitPushChain = attempt.then(() => void 0);
|
|
10632
|
+
return attempt;
|
|
10633
|
+
}
|
|
8568
10634
|
/**
|
|
8569
10635
|
* B-276: publish the daemon-context Claude auth preflight into daemonState
|
|
8570
10636
|
* (same serialized push chain as the CLI update policy). Resolves true when
|
|
@@ -8586,6 +10652,34 @@ class ApiMachineClient {
|
|
|
8586
10652
|
this.cliUpdatePushChain = attempt.then(() => void 0);
|
|
8587
10653
|
return attempt;
|
|
8588
10654
|
}
|
|
10655
|
+
/**
|
|
10656
|
+
* B-587: `agent-hibernation-set {idleMs}` (0 = off) and the
|
|
10657
|
+
* `daemonState.agentHibernation` capability. Call before connect().
|
|
10658
|
+
*/
|
|
10659
|
+
setAgentHibernationHandlers(initialIdleMs, set) {
|
|
10660
|
+
this.agentHibernationIdleMs = initialIdleMs;
|
|
10661
|
+
this.rpcHandlerManager.registerHandler("agent-hibernation-set", async (params) => {
|
|
10662
|
+
const raw = params?.idleMs;
|
|
10663
|
+
if (typeof raw !== "number" || !Number.isFinite(raw) || raw < 0) return { error: "invalid-idle-ms" };
|
|
10664
|
+
const idleMs = await set(raw);
|
|
10665
|
+
this.agentHibernationIdleMs = idleMs;
|
|
10666
|
+
await this.publishAgentHibernation();
|
|
10667
|
+
return { agentHibernation: { idleMs } };
|
|
10668
|
+
});
|
|
10669
|
+
}
|
|
10670
|
+
publishAgentHibernation() {
|
|
10671
|
+
const idleMs = this.agentHibernationIdleMs;
|
|
10672
|
+
if (idleMs === null || !this.socket?.connected) return Promise.resolve();
|
|
10673
|
+
const attempt = this.cliUpdatePushChain.then(() => this.updateDaemonState((current) => ({
|
|
10674
|
+
...current ?? {},
|
|
10675
|
+
status: current?.status ?? "running",
|
|
10676
|
+
agentHibernation: { idleMs, rpcAvailable: true, detectedAt: Date.now() }
|
|
10677
|
+
}))).catch((error) => {
|
|
10678
|
+
logger.logger.debug("[API MACHINE] Failed to publish agent hibernation:", error);
|
|
10679
|
+
});
|
|
10680
|
+
this.cliUpdatePushChain = attempt.then(() => void 0);
|
|
10681
|
+
return attempt;
|
|
10682
|
+
}
|
|
8589
10683
|
/** B-276 machine RPCs. Responses always carry `claudeAuth` so the web can refresh in place. */
|
|
8590
10684
|
setClaudeAuthHandlers(handlers) {
|
|
8591
10685
|
this.rpcHandlerManager.registerHandler("claude-auth-probe", async () => {
|
|
@@ -8915,6 +11009,10 @@ class ApiMachineClient {
|
|
|
8915
11009
|
});
|
|
8916
11010
|
return { type: "success", ...result };
|
|
8917
11011
|
});
|
|
11012
|
+
this.rpcHandlerManager.registerHandler("history-search", async (params) => {
|
|
11013
|
+
this.historySearch ??= createHistorySearchService();
|
|
11014
|
+
return this.historySearch.handle(params);
|
|
11015
|
+
});
|
|
8918
11016
|
this.rpcHandlerManager.registerHandler("codex-import-session", async (params) => {
|
|
8919
11017
|
const { directory, codexThreadId, approvedNewDirectoryCreation, permissionMode, title } = params || {};
|
|
8920
11018
|
if (typeof directory !== "string" || directory.length === 0) {
|
|
@@ -9293,13 +11391,17 @@ class ApiMachineClient {
|
|
|
9293
11391
|
claudeHistory: { rpcAvailable: true, detectedAt: now },
|
|
9294
11392
|
// B-464 capability flag: `codex-list-history` / `codex-import-session`.
|
|
9295
11393
|
codexHistory: { rpcAvailable: true, detectedAt: now },
|
|
11394
|
+
// B-549 capability flag: `history-search` (same restamp discipline).
|
|
11395
|
+
historySearch: { rpcAvailable: true, detectedAt: now },
|
|
9296
11396
|
// B-084: closed records survive daemon restarts (persisted
|
|
9297
11397
|
// in closed-terminals.json), so the connect snapshot ships
|
|
9298
11398
|
// them too — not just the incremental pushes.
|
|
9299
11399
|
closedTerminals: this.webTerminal.getClosedTerminals(),
|
|
9300
11400
|
// B-276: re-ship the last preflight so a reconnect never
|
|
9301
11401
|
// leaves a stale-pid value visible longer than needed.
|
|
9302
|
-
...this.claudeAuthState ? { claudeAuth: this.claudeAuthState } : {}
|
|
11402
|
+
...this.claudeAuthState ? { claudeAuth: this.claudeAuthState } : {},
|
|
11403
|
+
// B-587 capability + setting (same restamp discipline).
|
|
11404
|
+
...this.agentHibernationIdleMs !== null ? { agentHibernation: { idleMs: this.agentHibernationIdleMs, rpcAvailable: true, detectedAt: now } } : {}
|
|
9303
11405
|
};
|
|
9304
11406
|
});
|
|
9305
11407
|
this.webTerminal.startListTracking((terminals) => this.pushTerminalList(terminals));
|
|
@@ -9595,9 +11697,14 @@ class ApiMachineClient {
|
|
|
9595
11697
|
logger.logger.debug("[API MACHINE] Keep-alive stopped");
|
|
9596
11698
|
}
|
|
9597
11699
|
}
|
|
11700
|
+
/** B-550: final terminal scrollback checkpoint; resolves within ~300ms, never rejects. */
|
|
11701
|
+
flushTerminalScrollback() {
|
|
11702
|
+
return this.webTerminal.flushScrollbackForShutdown().catch(() => void 0);
|
|
11703
|
+
}
|
|
9598
11704
|
shutdown() {
|
|
9599
11705
|
logger.logger.debug("[API MACHINE] Shutting down");
|
|
9600
11706
|
this.webTerminal.stopListTracking();
|
|
11707
|
+
this.rateLimitPublisher.stop();
|
|
9601
11708
|
this.stopKeepAlive();
|
|
9602
11709
|
if (this.reconnectInterval) {
|
|
9603
11710
|
clearInterval(this.reconnectInterval);
|