@juspay/shooter 1.21.0 → 1.23.0
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/.claude/hooks/notifier.cjs +94 -1
- package/build/client/_app/immutable/assets/2.JWRrnR-w.css +1 -0
- package/build/client/_app/immutable/assets/2.JWRrnR-w.css.br +0 -0
- package/build/client/_app/immutable/assets/2.JWRrnR-w.css.gz +0 -0
- package/build/client/_app/immutable/chunks/{DOEXXmsh.js → Bj5wFimK.js} +2 -2
- package/build/client/_app/immutable/chunks/Bj5wFimK.js.br +0 -0
- package/build/client/_app/immutable/chunks/Bj5wFimK.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{EqMAkEha.js → BjYr_-Ss.js} +1 -1
- package/build/client/_app/immutable/chunks/BjYr_-Ss.js.br +0 -0
- package/build/client/_app/immutable/chunks/BjYr_-Ss.js.gz +0 -0
- package/build/client/_app/immutable/chunks/C4Hns_Wl.js +1 -0
- package/build/client/_app/immutable/chunks/C4Hns_Wl.js.br +0 -0
- package/build/client/_app/immutable/chunks/C4Hns_Wl.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DULfdsh6.js +6 -0
- package/build/client/_app/immutable/chunks/DULfdsh6.js.br +0 -0
- package/build/client/_app/immutable/chunks/DULfdsh6.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{BmfLecb1.js → fcNfTA-E.js} +1 -1
- package/build/client/_app/immutable/chunks/fcNfTA-E.js.br +0 -0
- package/build/client/_app/immutable/chunks/fcNfTA-E.js.gz +0 -0
- package/build/client/_app/immutable/entry/{app.CeSxgGat.js → app.Bvoqymnp.js} +2 -2
- package/build/client/_app/immutable/entry/app.Bvoqymnp.js.br +0 -0
- package/build/client/_app/immutable/entry/app.Bvoqymnp.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.BqXCPPZJ.js +1 -0
- package/build/client/_app/immutable/entry/start.BqXCPPZJ.js.br +2 -0
- package/build/client/_app/immutable/entry/start.BqXCPPZJ.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{0.oaPwxh1O.js → 0.Bv_TwEnq.js} +1 -1
- package/build/client/_app/immutable/nodes/0.Bv_TwEnq.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.Bv_TwEnq.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{1.DMPyoM-M.js → 1.7lffTIeb.js} +1 -1
- package/build/client/_app/immutable/nodes/1.7lffTIeb.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.7lffTIeb.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{10.Cbm7nQKK.js → 10.ChiIrIDl.js} +1 -1
- package/build/client/_app/immutable/nodes/10.ChiIrIDl.js.br +0 -0
- package/build/client/_app/immutable/nodes/10.ChiIrIDl.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{11.CKmZjP_a.js → 11.DO3vyXEv.js} +2 -2
- package/build/client/_app/immutable/nodes/11.DO3vyXEv.js.br +0 -0
- package/build/client/_app/immutable/nodes/{11.CKmZjP_a.js.gz → 11.DO3vyXEv.js.gz} +0 -0
- package/build/client/_app/immutable/nodes/2.iMIqsE7n.js +23 -0
- package/build/client/_app/immutable/nodes/2.iMIqsE7n.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.iMIqsE7n.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{3.BgLpGnzb.js → 3.CArnSHOO.js} +1 -1
- package/build/client/_app/immutable/nodes/3.CArnSHOO.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.CArnSHOO.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{5.Avc1-gVb.js → 5.DziEu9rx.js} +1 -1
- package/build/client/_app/immutable/nodes/5.DziEu9rx.js.br +0 -0
- package/build/client/_app/immutable/nodes/5.DziEu9rx.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{6.Dw2wEssJ.js → 6.B8l1RwkB.js} +1 -1
- package/build/client/_app/immutable/nodes/6.B8l1RwkB.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.B8l1RwkB.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{7.DwKZjoBg.js → 7.BPyfhDis.js} +1 -1
- package/build/client/_app/immutable/nodes/7.BPyfhDis.js.br +0 -0
- package/build/client/_app/immutable/nodes/7.BPyfhDis.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{8.ZUAI6g5E.js → 8.D_vszZ9E.js} +1 -1
- package/build/client/_app/immutable/nodes/8.D_vszZ9E.js.br +0 -0
- package/build/client/_app/immutable/nodes/8.D_vszZ9E.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{9.I_KGXPwB.js → 9.Drah-do-.js} +1 -1
- package/build/client/_app/immutable/nodes/9.Drah-do-.js.br +0 -0
- package/build/client/_app/immutable/nodes/9.Drah-do-.js.gz +0 -0
- package/build/client/_app/version.json +1 -1
- package/build/client/_app/version.json.br +0 -0
- package/build/client/_app/version.json.gz +0 -0
- package/build/pty-holder.cjs +6 -0
- package/build/server/chunks/{0-vrTNAfZB.js → 0-DAB_6Vm1.js} +2 -2
- package/build/server/chunks/{0-vrTNAfZB.js.map → 0-DAB_6Vm1.js.map} +1 -1
- package/build/server/chunks/{1-nbr-bOoF.js → 1-D-qMYaCx.js} +2 -2
- package/build/server/chunks/{1-nbr-bOoF.js.map → 1-D-qMYaCx.js.map} +1 -1
- package/build/server/chunks/{10-ChyvvJ6w.js → 10-CeFFGo-X.js} +2 -2
- package/build/server/chunks/{10-ChyvvJ6w.js.map → 10-CeFFGo-X.js.map} +1 -1
- package/build/server/chunks/{11-6ZAjL3uU.js → 11-DRMu_ATU.js} +2 -2
- package/build/server/chunks/{11-6ZAjL3uU.js.map → 11-DRMu_ATU.js.map} +1 -1
- package/build/server/chunks/{2-DWFRVDWJ.js → 2-B7OLBMNH.js} +4 -4
- package/build/server/chunks/{2-DWFRVDWJ.js.map → 2-B7OLBMNH.js.map} +1 -1
- package/build/server/chunks/{3-CKANM_WM.js → 3-B38ZarLw.js} +2 -2
- package/build/server/chunks/{3-CKANM_WM.js.map → 3-B38ZarLw.js.map} +1 -1
- package/build/server/chunks/{5-BxVjs2qi.js → 5-D-Uv1voC.js} +2 -2
- package/build/server/chunks/{5-BxVjs2qi.js.map → 5-D-Uv1voC.js.map} +1 -1
- package/build/server/chunks/{6-Cbf1AAMQ.js → 6-DP46cUej.js} +2 -2
- package/build/server/chunks/{6-Cbf1AAMQ.js.map → 6-DP46cUej.js.map} +1 -1
- package/build/server/chunks/{7-CMK2quEf.js → 7-B29_3ar6.js} +2 -2
- package/build/server/chunks/{7-CMK2quEf.js.map → 7-B29_3ar6.js.map} +1 -1
- package/build/server/chunks/{8-DhdfkfDM.js → 8-DCnSDVrX.js} +2 -2
- package/build/server/chunks/{8-DhdfkfDM.js.map → 8-DCnSDVrX.js.map} +1 -1
- package/build/server/chunks/{9-CPpxtRM5.js → 9-BwqDc8wC.js} +2 -2
- package/build/server/chunks/{9-CPpxtRM5.js.map → 9-BwqDc8wC.js.map} +1 -1
- package/build/server/chunks/_page.svelte-8OFzwdNA.js +758 -0
- package/build/server/chunks/_page.svelte-8OFzwdNA.js.map +1 -0
- package/build/server/chunks/{_server.ts-BWVlO8iV.js → _server.ts-05JJOdcX.js} +15 -12
- package/build/server/chunks/_server.ts-05JJOdcX.js.map +1 -0
- package/build/server/chunks/{_server.ts-BevnuePu.js → _server.ts-BCljU9Sg.js} +7 -3
- package/build/server/chunks/_server.ts-BCljU9Sg.js.map +1 -0
- package/build/server/chunks/{_server.ts-D-vgx5UZ.js → _server.ts-BTmknWpO.js} +2 -2
- package/build/server/chunks/{_server.ts-D-vgx5UZ.js.map → _server.ts-BTmknWpO.js.map} +1 -1
- package/build/server/chunks/{_server.ts-tChyh9FX.js → _server.ts-BXhmLZwN.js} +4 -2
- package/build/server/chunks/{_server.ts-tChyh9FX.js.map → _server.ts-BXhmLZwN.js.map} +1 -1
- package/build/server/chunks/{_server.ts-CvJKTS3Z.js → _server.ts-BbRSpB74.js} +4 -2
- package/build/server/chunks/{_server.ts-CvJKTS3Z.js.map → _server.ts-BbRSpB74.js.map} +1 -1
- package/build/server/chunks/{_server.ts-CC2K8-L2.js → _server.ts-Blx6TuRU.js} +4 -2
- package/build/server/chunks/_server.ts-Blx6TuRU.js.map +1 -0
- package/build/server/chunks/_server.ts-C6NRpe7e.js +33 -0
- package/build/server/chunks/_server.ts-C6NRpe7e.js.map +1 -0
- package/build/server/chunks/_server.ts-CGqCOCdK.js +53 -0
- package/build/server/chunks/_server.ts-CGqCOCdK.js.map +1 -0
- package/build/server/chunks/{_server.ts-X1R7L_QI.js → _server.ts-CYWXjihn.js} +4 -2
- package/build/server/chunks/{_server.ts-X1R7L_QI.js.map → _server.ts-CYWXjihn.js.map} +1 -1
- package/build/server/chunks/{_server.ts-CD7JP3fz.js → _server.ts-D0___krA.js} +4 -2
- package/build/server/chunks/_server.ts-D0___krA.js.map +1 -0
- package/build/server/chunks/{_server.ts-VzDcFFgy.js → _server.ts-DPHRUFYS.js} +4 -2
- package/build/server/chunks/_server.ts-DPHRUFYS.js.map +1 -0
- package/build/server/chunks/{_server.ts-D0zRDSx0.js → _server.ts-D_WRex0k.js} +4 -2
- package/build/server/chunks/_server.ts-D_WRex0k.js.map +1 -0
- package/build/server/chunks/{_server.ts-CA5KUENM.js → _server.ts-Da1kSClZ.js} +4 -2
- package/build/server/chunks/_server.ts-Da1kSClZ.js.map +1 -0
- package/build/server/chunks/{_server.ts-Dp-hXW_I.js → _server.ts-l3cd4Cto.js} +4 -2
- package/build/server/chunks/_server.ts-l3cd4Cto.js.map +1 -0
- package/build/server/chunks/{library-apns-Dl3iRE2h.js → library-apns-D8RPINlv.js} +62 -7
- package/build/server/chunks/library-apns-D8RPINlv.js.map +1 -0
- package/build/server/chunks/{pending-requests-C9p57WoU.js → pending-requests-8rWjrF6d.js} +3 -2
- package/build/server/chunks/pending-requests-8rWjrF6d.js.map +1 -0
- package/build/server/chunks/presence-store-Bx_g0-Gd.js +23 -0
- package/build/server/chunks/presence-store-Bx_g0-Gd.js.map +1 -0
- package/build/server/chunks/{pty-manager-ZqXqa-6A.js → pty-manager-DDjG7DlH.js} +297 -31
- package/build/server/chunks/pty-manager-DDjG7DlH.js.map +1 -0
- package/build/server/chunks/shooter-home-4f_HkdGI.js +10 -0
- package/build/server/chunks/shooter-home-4f_HkdGI.js.map +1 -0
- package/build/server/index.js +1 -1
- package/build/server/index.js.map +1 -1
- package/build/server/manifest.js +38 -24
- package/build/server/manifest.js.map +1 -1
- package/package.json +4 -2
- package/server.ts +2 -2
- package/src/lib/modules/client/common/index.ts +1 -0
- package/src/lib/modules/client/common/presence.ts +47 -0
- package/src/lib/modules/client/dashboard/AutopilotPanel.svelte +188 -4
- package/src/lib/modules/client/dashboard/autopilot-driver.svelte.ts +681 -0
- package/src/lib/modules/client/dashboard/decide-injection.ts +127 -0
- package/src/lib/modules/client/dashboard/store.svelte.ts +65 -24
- package/src/lib/modules/client/neurolink/fetch-proxy.ts +38 -1
- package/src/lib/modules/client/terminal/xterm-wrapper.ts +52 -12
- package/src/lib/modules/server/apn/apns-payload.ts +50 -0
- package/src/lib/modules/server/apn/library-apns.ts +50 -8
- package/src/lib/modules/server/apn/pending-requests.ts +3 -1
- package/src/lib/modules/server/sessions/autopilot-context.ts +57 -0
- package/src/lib/modules/server/sessions/autopilot-engine.ts +148 -43
- package/src/lib/modules/server/sessions/litellm-client.ts +90 -34
- package/src/lib/modules/server/sessions/next-step-consensus.ts +27 -2
- package/src/lib/modules/server/sessions/summary-store.ts +3 -1
- package/src/lib/modules/server/terminal/agent-launch.ts +26 -0
- package/src/lib/modules/server/terminal/pty-holder.cjs +6 -0
- package/src/lib/modules/server/terminal/pty-manager.ts +292 -38
- package/src/lib/modules/server/terminal/session-watcher.ts +12 -2
- package/src/lib/modules/server/terminal/terminal-emulator.ts +102 -0
- package/src/lib/modules/server/terminal/terminal-store.ts +3 -1
- package/src/lib/modules/server/utils/shooter-home.ts +16 -0
- package/src/lib/modules/server/ws/presence-store.ts +50 -0
- package/src/lib/modules/server/ws/server.ts +18 -2
- package/src/lib/modules/server/ws/terminal-handler.ts +11 -6
- package/src/lib/types/autopilot.ts +65 -0
- package/src/lib/types/generated/WsProtocol.ts +10 -1
- package/src/lib/types/server.ts +27 -1
- package/src/lib/types/terminal-client.ts +3 -0
- package/src/lib/types/ws.ts +3 -2
- package/src/routes/api/autopilot/goal/+server.ts +72 -0
- package/src/routes/api/neurolink-proxy/+server.ts +8 -2
- package/src/routes/api/notify/+server.ts +22 -15
- package/src/routes/api/presence/+server.ts +39 -0
- package/src/routes/api/ws-status/+server.ts +8 -5
- package/build/client/_app/immutable/assets/2.BHi6pjT2.css +0 -1
- package/build/client/_app/immutable/assets/2.BHi6pjT2.css.br +0 -0
- package/build/client/_app/immutable/assets/2.BHi6pjT2.css.gz +0 -0
- package/build/client/_app/immutable/chunks/BmfLecb1.js.br +0 -0
- package/build/client/_app/immutable/chunks/BmfLecb1.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CRkG7oE4.js +0 -1
- package/build/client/_app/immutable/chunks/CRkG7oE4.js.br +0 -0
- package/build/client/_app/immutable/chunks/CRkG7oE4.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DOEXXmsh.js.br +0 -0
- package/build/client/_app/immutable/chunks/DOEXXmsh.js.gz +0 -0
- package/build/client/_app/immutable/chunks/EqMAkEha.js.br +0 -0
- package/build/client/_app/immutable/chunks/EqMAkEha.js.gz +0 -0
- package/build/client/_app/immutable/chunks/J5-Cr5oR.js +0 -6
- package/build/client/_app/immutable/chunks/J5-Cr5oR.js.br +0 -0
- package/build/client/_app/immutable/chunks/J5-Cr5oR.js.gz +0 -0
- package/build/client/_app/immutable/entry/app.CeSxgGat.js.br +0 -0
- package/build/client/_app/immutable/entry/app.CeSxgGat.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.DrnJFwxA.js +0 -1
- package/build/client/_app/immutable/entry/start.DrnJFwxA.js.br +0 -2
- package/build/client/_app/immutable/entry/start.DrnJFwxA.js.gz +0 -0
- package/build/client/_app/immutable/nodes/0.oaPwxh1O.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.oaPwxh1O.js.gz +0 -0
- package/build/client/_app/immutable/nodes/1.DMPyoM-M.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.DMPyoM-M.js.gz +0 -0
- package/build/client/_app/immutable/nodes/10.Cbm7nQKK.js.br +0 -0
- package/build/client/_app/immutable/nodes/10.Cbm7nQKK.js.gz +0 -0
- package/build/client/_app/immutable/nodes/11.CKmZjP_a.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.zlrdNFtH.js +0 -13
- package/build/client/_app/immutable/nodes/2.zlrdNFtH.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.zlrdNFtH.js.gz +0 -0
- package/build/client/_app/immutable/nodes/3.BgLpGnzb.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.BgLpGnzb.js.gz +0 -0
- package/build/client/_app/immutable/nodes/5.Avc1-gVb.js.br +0 -0
- package/build/client/_app/immutable/nodes/5.Avc1-gVb.js.gz +0 -0
- package/build/client/_app/immutable/nodes/6.Dw2wEssJ.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.Dw2wEssJ.js.gz +0 -0
- package/build/client/_app/immutable/nodes/7.DwKZjoBg.js.br +0 -0
- package/build/client/_app/immutable/nodes/7.DwKZjoBg.js.gz +0 -0
- package/build/client/_app/immutable/nodes/8.ZUAI6g5E.js.br +0 -0
- package/build/client/_app/immutable/nodes/8.ZUAI6g5E.js.gz +0 -0
- package/build/client/_app/immutable/nodes/9.I_KGXPwB.js.br +0 -0
- package/build/client/_app/immutable/nodes/9.I_KGXPwB.js.gz +0 -0
- package/build/server/chunks/_page.svelte-tBuIq8Pg.js +0 -159
- package/build/server/chunks/_page.svelte-tBuIq8Pg.js.map +0 -1
- package/build/server/chunks/_server.ts-BWVlO8iV.js.map +0 -1
- package/build/server/chunks/_server.ts-BevnuePu.js.map +0 -1
- package/build/server/chunks/_server.ts-CA5KUENM.js.map +0 -1
- package/build/server/chunks/_server.ts-CC2K8-L2.js.map +0 -1
- package/build/server/chunks/_server.ts-CD7JP3fz.js.map +0 -1
- package/build/server/chunks/_server.ts-D0zRDSx0.js.map +0 -1
- package/build/server/chunks/_server.ts-Dp-hXW_I.js.map +0 -1
- package/build/server/chunks/_server.ts-VzDcFFgy.js.map +0 -1
- package/build/server/chunks/library-apns-Dl3iRE2h.js.map +0 -1
- package/build/server/chunks/pending-requests-C9p57WoU.js.map +0 -1
- package/build/server/chunks/pty-manager-ZqXqa-6A.js.map +0 -1
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Pure, deterministic auto-inject safety logic for the phone-resident autonomous loop.
|
|
2
|
+
// No I/O, no LLM. Fully unit-testable (tests/decide-injection.test.cjs).
|
|
3
|
+
// See docs/superpowers/specs/2026-06-01-phone-autonomous-agent-design.md
|
|
4
|
+
// §"Auto-inject safety model".
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
CommandVerdict,
|
|
8
|
+
ConsensusResult,
|
|
9
|
+
GateDecision,
|
|
10
|
+
InjectionPolicy,
|
|
11
|
+
InjectionState,
|
|
12
|
+
} from '$lib/types';
|
|
13
|
+
|
|
14
|
+
/** Default thresholds. Conservative — auto-inject is dangerous, so err toward NOT acting. */
|
|
15
|
+
export const DEFAULT_INJECTION_POLICY: InjectionPolicy = {
|
|
16
|
+
humanGraceMs: 5_000,
|
|
17
|
+
injectConfidence: 0.7,
|
|
18
|
+
maxAutoActions: 8,
|
|
19
|
+
minIntervalMs: 30_000,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Max length of a single injected command. Longer → rejected (likely not one command). */
|
|
23
|
+
const MAX_COMMAND_LENGTH = 400;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Coarse dangerous-payload patterns. This is a SEATBELT, not a security boundary:
|
|
27
|
+
* auto-inject is the user's accepted risk. We only block the obviously catastrophic.
|
|
28
|
+
*/
|
|
29
|
+
const DANGEROUS_PATTERNS: readonly RegExp[] = [
|
|
30
|
+
/\brm\s+-[a-z]*r[a-z]*f?\s+(\/|~|\/\*|\$home)/i, // rm -rf /, rm -rf ~, rm -rf /*
|
|
31
|
+
/\brm\s+-[a-z]*f[a-z]*r?\s+(\/|~|\/\*|\$home)/i, // rm -fr variants
|
|
32
|
+
/:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/, // classic fork bomb :(){ :|:& };:
|
|
33
|
+
/\bdd\b[^\n]*\bof=\/dev\//i, // dd of=/dev/...
|
|
34
|
+
/>\s*\/dev\/(sd|nvme|disk|hd)/i, // redirect onto a raw disk
|
|
35
|
+
/\bmkfs(\.[a-z0-9]+)?\b/i, // mkfs, mkfs.ext4, ...
|
|
36
|
+
/\b(shutdown|reboot|halt|poweroff)\b/i, // power state
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The GATE: given a per-terminal snapshot + the current consensus, decide whether the
|
|
41
|
+
* autonomous loop should act (produce + inject a command). Pure; `now` is passed in so
|
|
42
|
+
* the function stays deterministic and testable.
|
|
43
|
+
*
|
|
44
|
+
* Order matters — the first failing guard short-circuits with a reason.
|
|
45
|
+
*/
|
|
46
|
+
export function decideInjection(
|
|
47
|
+
state: InjectionState,
|
|
48
|
+
consensus: ConsensusResult,
|
|
49
|
+
now: number,
|
|
50
|
+
policy: InjectionPolicy = DEFAULT_INJECTION_POLICY,
|
|
51
|
+
opts: { allowTentative?: boolean } = {}
|
|
52
|
+
): GateDecision {
|
|
53
|
+
const top = consensus.steps[0];
|
|
54
|
+
if (!top) {
|
|
55
|
+
return { act: false, reason: 'no consensus step' };
|
|
56
|
+
}
|
|
57
|
+
// `tentative` = the 5 distinct lenses didn't reach quorum on the exact next step. That is a
|
|
58
|
+
// PRECISION filter, not a safety boundary (the real guards are the confidence floor, dedup,
|
|
59
|
+
// circuit breaker, and guardCommand). For AGENT terminals the injection is a natural-language
|
|
60
|
+
// PROMPT the agent re-grounds against its goal — not a raw command run verbatim — so a best-
|
|
61
|
+
// ranked-but-tentative next step is fine and keeps the autonomous loop live instead of stalling
|
|
62
|
+
// after step 1. Callers pass allowTentative only for agent terminals; shell terminals stay strict.
|
|
63
|
+
if (top.tentative && !opts.allowTentative) {
|
|
64
|
+
return { act: false, reason: 'consensus is tentative (no quorum)' };
|
|
65
|
+
}
|
|
66
|
+
if (!state.isManaged) {
|
|
67
|
+
return { act: false, reason: 'terminal is external / read-only (cannot inject)' };
|
|
68
|
+
}
|
|
69
|
+
if (state.lastEventType !== 'agent-idle') {
|
|
70
|
+
return { act: false, reason: `agent not idle (last event: ${state.lastEventType})` };
|
|
71
|
+
}
|
|
72
|
+
if (now - state.lastActivityAt < policy.humanGraceMs) {
|
|
73
|
+
return { act: false, reason: 'recent activity — within human grace window' };
|
|
74
|
+
}
|
|
75
|
+
if (now - state.lastInjectedAt < policy.minIntervalMs) {
|
|
76
|
+
return { act: false, reason: 'rate-limited (min inject interval)' };
|
|
77
|
+
}
|
|
78
|
+
if (state.autoActionCount >= policy.maxAutoActions) {
|
|
79
|
+
return { act: false, reason: 'circuit breaker — max consecutive auto-actions reached' };
|
|
80
|
+
}
|
|
81
|
+
if (top.confidence < policy.injectConfidence) {
|
|
82
|
+
return { act: false, reason: `confidence ${top.confidence.toFixed(2)} below floor` };
|
|
83
|
+
}
|
|
84
|
+
if (state.lastActedStep !== null && normalizeStep(top.text) === state.lastActedStep) {
|
|
85
|
+
return { act: false, reason: 'already acted on this step' };
|
|
86
|
+
}
|
|
87
|
+
return { act: true, reason: 'idle + high-confidence consensus', step: top };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Vet a CONCRETE command (produced by the decide step) before it is written to the PTY:
|
|
92
|
+
* single-line, length-bounded, not obviously catastrophic, not a duplicate of the last one.
|
|
93
|
+
*/
|
|
94
|
+
export function guardCommand(command: string, lastInjectedCommand: null | string): CommandVerdict {
|
|
95
|
+
const trimmed = command.trim();
|
|
96
|
+
if (trimmed.length === 0) {
|
|
97
|
+
return { command: trimmed, reason: 'empty command', safe: false };
|
|
98
|
+
}
|
|
99
|
+
if (/[\r\n]/.test(command)) {
|
|
100
|
+
return {
|
|
101
|
+
command: trimmed,
|
|
102
|
+
reason: 'multi-line command rejected (single command only)',
|
|
103
|
+
safe: false,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (trimmed.length > MAX_COMMAND_LENGTH) {
|
|
107
|
+
return { command: trimmed, reason: `command too long (> ${MAX_COMMAND_LENGTH})`, safe: false };
|
|
108
|
+
}
|
|
109
|
+
for (const pattern of DANGEROUS_PATTERNS) {
|
|
110
|
+
if (pattern.test(trimmed)) {
|
|
111
|
+
return { command: trimmed, reason: 'matches a dangerous-command pattern', safe: false };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (lastInjectedCommand !== null && trimmed === lastInjectedCommand.trim()) {
|
|
115
|
+
return { command: trimmed, reason: 'duplicate of last injected command', safe: false };
|
|
116
|
+
}
|
|
117
|
+
return { command: trimmed, reason: 'ok', safe: true };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Normalize a step text for dedup comparison (mirror of the consensus normalizer). */
|
|
121
|
+
export function normalizeStep(text: string): string {
|
|
122
|
+
return text
|
|
123
|
+
.toLowerCase()
|
|
124
|
+
.trim()
|
|
125
|
+
.replace(/\s+/g, ' ')
|
|
126
|
+
.replace(/[.,;:!?]+$/, '');
|
|
127
|
+
}
|
|
@@ -238,6 +238,18 @@ async function connectWs(apiKey: string): Promise<void> {
|
|
|
238
238
|
}
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
+
// Claude Code injects these wrappers as "user" messages (slash-command caveats, system reminders,
|
|
242
|
+
// etc.); none is the real goal. Mirror of SYSTEM_TAG_PREFIXES in server/sessions/jsonl-reader.ts.
|
|
243
|
+
const HARNESS_TAG_PREFIXES = [
|
|
244
|
+
'<local-command',
|
|
245
|
+
'<command-name>',
|
|
246
|
+
'<command-message>',
|
|
247
|
+
'<command-args>',
|
|
248
|
+
'<system-reminder>',
|
|
249
|
+
'<task-notification>',
|
|
250
|
+
'Caveat:',
|
|
251
|
+
];
|
|
252
|
+
|
|
241
253
|
function extractGoalText(content: string | { content?: string; type: string }[]): string {
|
|
242
254
|
let text = '';
|
|
243
255
|
if (typeof content === 'string') {
|
|
@@ -246,7 +258,10 @@ function extractGoalText(content: string | { content?: string; type: string }[])
|
|
|
246
258
|
const textPart = content.find((p) => p.type === 'text');
|
|
247
259
|
text = textPart?.content ?? '';
|
|
248
260
|
}
|
|
249
|
-
|
|
261
|
+
const trimmed = text.slice(0, 200).trim();
|
|
262
|
+
// A harness wrapper is not the user's goal — skip it so the caller keeps scanning for the real one
|
|
263
|
+
// (this is what let "<local-command-caveat>Caveat: …" become the pinned goal).
|
|
264
|
+
return isHarnessText(trimmed) ? '' : trimmed;
|
|
250
265
|
}
|
|
251
266
|
|
|
252
267
|
async function fetchTerminals(apiKey: string): Promise<void> {
|
|
@@ -291,8 +306,6 @@ function getApiKey(): string {
|
|
|
291
306
|
return '';
|
|
292
307
|
}
|
|
293
308
|
|
|
294
|
-
// -- Public API -----------------------------------------------------------
|
|
295
|
-
|
|
296
309
|
function handleWsMessage(raw: RawEvent): void {
|
|
297
310
|
const type = raw.type as string | undefined;
|
|
298
311
|
if (!type || type === 'welcome') {
|
|
@@ -367,6 +380,12 @@ function handleWsMessage(raw: RawEvent): void {
|
|
|
367
380
|
sessions = sortSessions(sessions);
|
|
368
381
|
}
|
|
369
382
|
|
|
383
|
+
function isHarnessText(text: string): boolean {
|
|
384
|
+
return HARNESS_TAG_PREFIXES.some((p) => text.startsWith(p));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// -- Public API -----------------------------------------------------------
|
|
388
|
+
|
|
370
389
|
function makeSessionState(t: DashboardTerminalRecord): SessionState {
|
|
371
390
|
return {
|
|
372
391
|
command: t.command,
|
|
@@ -435,14 +454,17 @@ function mergeSessions(
|
|
|
435
454
|
if (prev.status !== 'error') {
|
|
436
455
|
prev.status = mapStatus(t.status);
|
|
437
456
|
}
|
|
438
|
-
// Schedule goal extraction if still missing
|
|
439
|
-
|
|
457
|
+
// Schedule goal extraction if still missing — but never for an exited terminal (its socket
|
|
458
|
+
// would open, get a session-end, and the poll would keep reopening it).
|
|
459
|
+
if (!prev.goal && t.exitedAt === null && t.status !== 'exited') {
|
|
440
460
|
void openSessionSocket(t.id);
|
|
441
461
|
}
|
|
442
462
|
} else {
|
|
443
463
|
map.set(t.id, makeSessionState(t));
|
|
444
|
-
// Open a session WS for goal extraction on newly discovered terminals
|
|
445
|
-
|
|
464
|
+
// Open a session WS for goal extraction on newly discovered (live) terminals only
|
|
465
|
+
if (t.exitedAt === null && t.status !== 'exited') {
|
|
466
|
+
void openSessionSocket(t.id);
|
|
467
|
+
}
|
|
446
468
|
}
|
|
447
469
|
}
|
|
448
470
|
|
|
@@ -495,6 +517,12 @@ async function openSessionSocket(terminalId: string): Promise<void> {
|
|
|
495
517
|
}
|
|
496
518
|
const data = raw as Record<string, unknown>;
|
|
497
519
|
|
|
520
|
+
// Session ended — stop watching (no goal will ever arrive on this socket).
|
|
521
|
+
if (data.type === 'session-end') {
|
|
522
|
+
closeSessionSocket(terminalId);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
498
526
|
// Check if we already have a goal for this terminal
|
|
499
527
|
const currentSession = sessions.find((s) => s.terminalId === terminalId);
|
|
500
528
|
const hasGoal = currentSession?.goal && currentSession.goal.length > 0;
|
|
@@ -517,25 +545,23 @@ async function openSessionSocket(terminalId: string): Promise<void> {
|
|
|
517
545
|
`[DashboardStore] Found ${messages.length} messages, looking for first user message...`
|
|
518
546
|
);
|
|
519
547
|
|
|
520
|
-
// Find
|
|
548
|
+
// Find the FIRST non-harness user message and use it as the goal. Keep scanning past
|
|
549
|
+
// harness-only messages (slash-command caveats, system reminders) so they never become
|
|
550
|
+
// the goal — and do NOT close the socket when none is found yet: closing here made the
|
|
551
|
+
// 15s poll reopen it every cycle (the session-watcher churn).
|
|
521
552
|
for (const m of messages) {
|
|
522
|
-
if (m.role
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
updateSessionGoal(terminalId, goal);
|
|
530
|
-
}
|
|
531
|
-
// Goal extracted — close the socket, we no longer need it
|
|
553
|
+
if (m.role !== 'user') {
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
const goal = extractGoalText(m.content);
|
|
557
|
+
if (goal) {
|
|
558
|
+
updateSessionGoal(terminalId, goal);
|
|
559
|
+
syncEngineGoal(terminalId, goal);
|
|
532
560
|
closeSessionSocket(terminalId);
|
|
533
561
|
return;
|
|
534
562
|
}
|
|
535
563
|
}
|
|
536
|
-
|
|
537
|
-
console.log(`[DashboardStore] No user message found in history for ${terminalId}`);
|
|
538
|
-
// No user message yet — keep socket open for incoming messages
|
|
564
|
+
// No real user goal in history yet — keep the socket open for incoming live messages.
|
|
539
565
|
return;
|
|
540
566
|
}
|
|
541
567
|
|
|
@@ -546,10 +572,8 @@ async function openSessionSocket(terminalId: string): Promise<void> {
|
|
|
546
572
|
data.content as string | { content?: string; type: string }[]
|
|
547
573
|
);
|
|
548
574
|
if (goal) {
|
|
549
|
-
console.log(
|
|
550
|
-
`[DashboardStore] Extracted goal from message for ${terminalId}: "${goal.substring(0, 50)}..."`
|
|
551
|
-
);
|
|
552
575
|
updateSessionGoal(terminalId, goal);
|
|
576
|
+
syncEngineGoal(terminalId, goal);
|
|
553
577
|
closeSessionSocket(terminalId);
|
|
554
578
|
}
|
|
555
579
|
return;
|
|
@@ -625,6 +649,23 @@ function sortSessions(list: SessionState[]): SessionState[] {
|
|
|
625
649
|
});
|
|
626
650
|
}
|
|
627
651
|
|
|
652
|
+
// Push a freshly-extracted goal to the SERVER-side autopilot engine so its per-cycle LLM context is
|
|
653
|
+
// anchored to the real user intent. Without this the engine always ran goal-less: its goal store
|
|
654
|
+
// was only reachable via this route, which nothing ever called (the client only updated local
|
|
655
|
+
// state). Best-effort — the engine may not be running (503); the next extraction retries.
|
|
656
|
+
function syncEngineGoal(terminalId: string, goal: string): void {
|
|
657
|
+
if (!storedApiKey) {
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
void fetch('/api/autopilot/goal', {
|
|
661
|
+
body: JSON.stringify({ goal, terminalId }),
|
|
662
|
+
headers: { Authorization: `Bearer ${storedApiKey}`, 'Content-Type': 'application/json' },
|
|
663
|
+
method: 'POST',
|
|
664
|
+
}).catch(() => {
|
|
665
|
+
// best-effort; nothing to do on failure
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
|
|
628
669
|
function triggerSummarization(session: SessionState): void {
|
|
629
670
|
session.isSummarizing = true;
|
|
630
671
|
|
|
@@ -25,7 +25,7 @@ export function installFetchProxy(): void {
|
|
|
25
25
|
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
|
26
26
|
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
|
|
27
27
|
|
|
28
|
-
const provider =
|
|
28
|
+
const provider = resolveProvider(url);
|
|
29
29
|
|
|
30
30
|
if (!provider) {
|
|
31
31
|
return originalFetch(input, init);
|
|
@@ -68,3 +68,40 @@ export function installFetchProxy(): void {
|
|
|
68
68
|
});
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The runtime LiteLLM base URL injected by the root layout into window.process.env, or ''.
|
|
74
|
+
* Read via window['process'] (bracket access) so the bundler does NOT constant-fold it: a
|
|
75
|
+
* direct `globalThis.process.env` gets frozen to Node's build-time env by Vite/Rollup, which
|
|
76
|
+
* never contains the runtime-injected value.
|
|
77
|
+
*/
|
|
78
|
+
function litellmBaseUrl(): string {
|
|
79
|
+
if (typeof window === 'undefined') {
|
|
80
|
+
return '';
|
|
81
|
+
}
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/dot-notation -- bracket access is deliberate: it stops the bundler constant-folding process.env to the build-time value
|
|
83
|
+
const proc = (window as unknown as Record<string, unknown>)['process'] as
|
|
84
|
+
| undefined
|
|
85
|
+
| { env?: Record<string, string | undefined> };
|
|
86
|
+
const base = proc?.env?.LITELLM_BASE_URL;
|
|
87
|
+
return typeof base === 'string' ? base : '';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Resolve which proxy provider (if any) a URL routes through. Static cloud prefixes plus the
|
|
92
|
+
* runtime LiteLLM base URL — LiteLLM is self-hosted at a configurable origin, so its prefix
|
|
93
|
+
* is not known at build time and must be read from the injected env.
|
|
94
|
+
*/
|
|
95
|
+
function resolveProvider(url: string): string | undefined {
|
|
96
|
+
const staticMatch = Object.entries(PROXY_PREFIXES).find(([prefix]) =>
|
|
97
|
+
url.startsWith(prefix)
|
|
98
|
+
)?.[1];
|
|
99
|
+
if (staticMatch) {
|
|
100
|
+
return staticMatch;
|
|
101
|
+
}
|
|
102
|
+
const base = litellmBaseUrl();
|
|
103
|
+
if (base && url.startsWith(base)) {
|
|
104
|
+
return 'litellm';
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
@@ -138,6 +138,23 @@ export async function createTerminal(options: TerminalOptions): Promise<Terminal
|
|
|
138
138
|
let reconnectTimer: null | ReturnType<typeof setTimeout> = null;
|
|
139
139
|
let reconnectDelay = 1000;
|
|
140
140
|
let disposed = false;
|
|
141
|
+
let lastSeq = 0; // highest output seq seen; sent on reconnect by Phase 2
|
|
142
|
+
|
|
143
|
+
// Reconnect with exponential backoff + jitter. The jitter spreads a fleet of
|
|
144
|
+
// clients out so they don't reconnect in lockstep after a server restart.
|
|
145
|
+
function scheduleReconnect(): void {
|
|
146
|
+
if (disposed) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (reconnectTimer) {
|
|
150
|
+
clearTimeout(reconnectTimer);
|
|
151
|
+
}
|
|
152
|
+
const jitter = Math.random() * reconnectDelay * 0.5;
|
|
153
|
+
reconnectTimer = setTimeout(() => {
|
|
154
|
+
reconnectDelay = Math.min(reconnectDelay * 2, 30_000);
|
|
155
|
+
void connect();
|
|
156
|
+
}, reconnectDelay + jitter);
|
|
157
|
+
}
|
|
141
158
|
|
|
142
159
|
async function connect(): Promise<void> {
|
|
143
160
|
if (disposed) {
|
|
@@ -151,10 +168,7 @@ export async function createTerminal(options: TerminalOptions): Promise<Terminal
|
|
|
151
168
|
// Ticket fetch failed — schedule a retry
|
|
152
169
|
if (!disposed) {
|
|
153
170
|
options.onDisconnect?.();
|
|
154
|
-
|
|
155
|
-
reconnectDelay = Math.min(reconnectDelay * 2, 30_000);
|
|
156
|
-
void connect();
|
|
157
|
-
}, reconnectDelay);
|
|
171
|
+
scheduleReconnect();
|
|
158
172
|
}
|
|
159
173
|
return;
|
|
160
174
|
}
|
|
@@ -163,7 +177,14 @@ export async function createTerminal(options: TerminalOptions): Promise<Terminal
|
|
|
163
177
|
return;
|
|
164
178
|
}
|
|
165
179
|
|
|
166
|
-
|
|
180
|
+
// Advertise snapshot capability so the server sends a serialized
|
|
181
|
+
// current-screen {snapshot} on join (correct for alt-screen TUIs) instead
|
|
182
|
+
// of a raw scrollback replay. On a reconnect (lastSeq > 0) also pass the
|
|
183
|
+
// last applied seq so the server replays only the missing frames from its
|
|
184
|
+
// ring — a seamless catch-up — falling back to a snapshot if the gap is
|
|
185
|
+
// too old to bridge.
|
|
186
|
+
const resume = lastSeq > 0 ? `&lastSeq=${String(lastSeq)}` : '';
|
|
187
|
+
ws = new WebSocket(`${options.wsUrl}?ticket=${ticket}&caps=snapshot${resume}`);
|
|
167
188
|
|
|
168
189
|
ws.onopen = (): void => {
|
|
169
190
|
reconnectDelay = 1000; // Reset backoff
|
|
@@ -173,6 +194,22 @@ export async function createTerminal(options: TerminalOptions): Promise<Terminal
|
|
|
173
194
|
ws.onmessage = (event: MessageEvent): void => {
|
|
174
195
|
const msg = JSON.parse(event.data as string) as WsTerminalInboundMessage;
|
|
175
196
|
if (msg.type === 'output') {
|
|
197
|
+
if (typeof msg.seq === 'number') {
|
|
198
|
+
// Drop frames already covered by a snapshot or reconnect replay —
|
|
199
|
+
// the snapshot's seq is the high-water mark (foundation contract §1.1).
|
|
200
|
+
if (msg.seq <= lastSeq) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
lastSeq = msg.seq;
|
|
204
|
+
}
|
|
205
|
+
term.write(msg.data ?? '');
|
|
206
|
+
} else if (msg.type === 'snapshot') {
|
|
207
|
+
// Authoritative current-screen snapshot — clear and restore, then the
|
|
208
|
+
// live tail (output frames with seq > this) applies on top.
|
|
209
|
+
term.reset();
|
|
210
|
+
if (typeof msg.seq === 'number') {
|
|
211
|
+
lastSeq = msg.seq;
|
|
212
|
+
}
|
|
176
213
|
term.write(msg.data ?? '');
|
|
177
214
|
} else if (msg.type === 'scrollback') {
|
|
178
215
|
term.write(msg.data ?? '');
|
|
@@ -185,7 +222,14 @@ export async function createTerminal(options: TerminalOptions): Promise<Terminal
|
|
|
185
222
|
}
|
|
186
223
|
options.onExit?.(msg.code ?? 0);
|
|
187
224
|
} else if (msg.type === 'output-dropped') {
|
|
188
|
-
|
|
225
|
+
// The server withholds further frames and resnapshots us to the current
|
|
226
|
+
// screen once our socket drains (Phase 2). Show a transient notice; the
|
|
227
|
+
// incoming snapshot resets the screen and clears it.
|
|
228
|
+
const note =
|
|
229
|
+
typeof msg.bytes === 'number' && msg.bytes > 0
|
|
230
|
+
? `[${String(msg.bytes)} bytes dropped — resyncing…]`
|
|
231
|
+
: '[resyncing…]';
|
|
232
|
+
term.write(`\r\n\x1b[33m${note}\x1b[0m\r\n`);
|
|
189
233
|
} else if (msg.type === 'activity') {
|
|
190
234
|
options.onActivity?.(msg.active ?? false);
|
|
191
235
|
} else if (msg.type === 'cwd') {
|
|
@@ -204,11 +248,7 @@ export async function createTerminal(options: TerminalOptions): Promise<Terminal
|
|
|
204
248
|
return;
|
|
205
249
|
}
|
|
206
250
|
options.onDisconnect?.();
|
|
207
|
-
|
|
208
|
-
reconnectTimer = setTimeout(() => {
|
|
209
|
-
reconnectDelay = Math.min(reconnectDelay * 2, 30_000);
|
|
210
|
-
void connect();
|
|
211
|
-
}, reconnectDelay);
|
|
251
|
+
scheduleReconnect();
|
|
212
252
|
};
|
|
213
253
|
}
|
|
214
254
|
|
|
@@ -258,7 +298,7 @@ export async function createTerminal(options: TerminalOptions): Promise<Terminal
|
|
|
258
298
|
}
|
|
259
299
|
}
|
|
260
300
|
|
|
261
|
-
return { dispose, fitAddon, sendInput, term };
|
|
301
|
+
return { dispose, fitAddon, getLastSeq: () => lastSeq, sendInput, term };
|
|
262
302
|
}
|
|
263
303
|
|
|
264
304
|
// Helper to send signals
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Pure: cap an APNs JSON payload to APNs' size limit by truncating the alert body (then the
|
|
2
|
+
// subtitle) so a long agent message can't blow past the ~4 KB cap and 413 / fail to deliver.
|
|
3
|
+
// No imports — unit-tested in isolation (tests/apns-payload.test.cjs).
|
|
4
|
+
|
|
5
|
+
/** APNs caps an alert payload at 4096 bytes; leave headroom for transport framing. */
|
|
6
|
+
export const APNS_MAX_BYTES = 3900;
|
|
7
|
+
|
|
8
|
+
const ELLIPSIS = '…';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Shrink an APNs payload so its JSON fits under `maxBytes`. Truncates `alert.body` first (the
|
|
12
|
+
* agent's last message — the usual culprit), then `alert.subtitle`, marking cuts with an
|
|
13
|
+
* ellipsis. Mutates and returns `body`. Title and custom data are preserved.
|
|
14
|
+
*/
|
|
15
|
+
export function fitApnsPayload(
|
|
16
|
+
body: Record<string, unknown>,
|
|
17
|
+
maxBytes: number = APNS_MAX_BYTES
|
|
18
|
+
): Record<string, unknown> {
|
|
19
|
+
if (payloadBytes(body) <= maxBytes) {
|
|
20
|
+
return body;
|
|
21
|
+
}
|
|
22
|
+
const aps = body.aps as Record<string, unknown> | undefined;
|
|
23
|
+
const alert = aps?.alert as Record<string, unknown> | undefined;
|
|
24
|
+
if (!alert) {
|
|
25
|
+
return body; // nothing safely trimmable (e.g. silent push) — leave as-is
|
|
26
|
+
}
|
|
27
|
+
for (const field of ['body', 'subtitle'] as const) {
|
|
28
|
+
if (payloadBytes(body) <= maxBytes) {
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
const value = alert[field];
|
|
32
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
let text = value;
|
|
36
|
+
while (payloadBytes(body) > maxBytes && text.length > 0) {
|
|
37
|
+
const cut = Math.max(1, Math.ceil(text.length * 0.12));
|
|
38
|
+
text = text.slice(0, text.length - cut);
|
|
39
|
+
// When a field is fully truncated away, OMIT it (undefined → dropped by JSON.stringify)
|
|
40
|
+
// rather than leaving an empty string — applies to body as well as subtitle.
|
|
41
|
+
alert[field] = text.length > 0 ? text + ELLIPSIS : undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return body;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** UTF-8 byte length of the JSON-serialised payload. */
|
|
48
|
+
function payloadBytes(body: Record<string, unknown>): number {
|
|
49
|
+
return Buffer.byteLength(JSON.stringify(body), 'utf8');
|
|
50
|
+
}
|
|
@@ -6,6 +6,7 @@ import jwt from 'jsonwebtoken';
|
|
|
6
6
|
import { promisify } from 'util';
|
|
7
7
|
|
|
8
8
|
import { toErrorMessage } from '../utils/error';
|
|
9
|
+
import { fitApnsPayload } from './apns-payload.js';
|
|
9
10
|
|
|
10
11
|
// APNs delivery via curl. Replaces @parse/node-apn (which times out on Node 24
|
|
11
12
|
// regardless of version: 7.1.0, 8.1.0, both reproduce). Node's native http2 +
|
|
@@ -109,7 +110,48 @@ export class LibraryAPNsService {
|
|
|
109
110
|
void _ignoredAps;
|
|
110
111
|
Object.assign(body, customData);
|
|
111
112
|
}
|
|
112
|
-
|
|
113
|
+
return this.deliver(deviceToken, body, 'alert', '10');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Send a SILENT (content-available) background push to wake a backgrounded app without
|
|
118
|
+
* showing an alert — used to wake the phone-resident agent loop so it can run a burst.
|
|
119
|
+
* Uses apns-push-type:background + apns-priority:5 (required by APNs for silent pushes).
|
|
120
|
+
* iOS throttles these (a few per hour); delivery is best-effort.
|
|
121
|
+
*/
|
|
122
|
+
async sendSilentNotification(
|
|
123
|
+
deviceToken: string,
|
|
124
|
+
data?: Record<string, unknown>
|
|
125
|
+
): Promise<APNsSendResult> {
|
|
126
|
+
if (!this.configured) {
|
|
127
|
+
throw new Error('APNs service not configured properly');
|
|
128
|
+
}
|
|
129
|
+
if (!deviceToken) {
|
|
130
|
+
throw new Error('Device token is required');
|
|
131
|
+
}
|
|
132
|
+
const body: Record<string, unknown> = { aps: { 'content-available': 1 } };
|
|
133
|
+
if (data) {
|
|
134
|
+
const { aps: _ignoredAps, ...customData } = data;
|
|
135
|
+
void _ignoredAps;
|
|
136
|
+
Object.assign(body, customData);
|
|
137
|
+
}
|
|
138
|
+
return this.deliver(deviceToken, body, 'background', '5');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
shutdown(): void {
|
|
142
|
+
// No persistent state to release.
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Shared curl/HTTP-2 delivery. `pushType` is 'alert' | 'background', `priority` '10' | '5'. */
|
|
146
|
+
private async deliver(
|
|
147
|
+
deviceToken: string,
|
|
148
|
+
body: Record<string, unknown>,
|
|
149
|
+
pushType: 'alert' | 'background',
|
|
150
|
+
priority: '5' | '10'
|
|
151
|
+
): Promise<APNsSendResult> {
|
|
152
|
+
// Cap the payload to APNs' size limit. A long agent message in alert.body otherwise blows
|
|
153
|
+
// past ~4 KB → APNs 413 PayloadTooLarge (or curl E2BIG), so the notification never arrives.
|
|
154
|
+
const bodyJson = JSON.stringify(fitApnsPayload(body));
|
|
113
155
|
const jwtToken = this.getJwt();
|
|
114
156
|
const url = `https://${this.host}/3/device/${deviceToken}`;
|
|
115
157
|
|
|
@@ -127,9 +169,9 @@ export class LibraryAPNsService {
|
|
|
127
169
|
'-H',
|
|
128
170
|
`authorization: bearer ${jwtToken}`,
|
|
129
171
|
'-H',
|
|
130
|
-
|
|
172
|
+
`apns-push-type: ${pushType}`,
|
|
131
173
|
'-H',
|
|
132
|
-
|
|
174
|
+
`apns-priority: ${priority}`,
|
|
133
175
|
'-d',
|
|
134
176
|
bodyJson,
|
|
135
177
|
'-w',
|
|
@@ -171,16 +213,16 @@ export class LibraryAPNsService {
|
|
|
171
213
|
console.error(`[apns] Delivery failed (status=${status}): ${reason}`);
|
|
172
214
|
return { error: reason, failed: 1, sent: 0, success: false };
|
|
173
215
|
} catch (err) {
|
|
174
|
-
|
|
216
|
+
// A failed execFile echoes the whole curl command — which carries the APNs JWT and the
|
|
217
|
+
// device token. Redact both before logging or returning so they don't leak into logs/responses.
|
|
218
|
+
const msg = toErrorMessage(err)
|
|
219
|
+
.replace(/bearer\s+[A-Za-z0-9._-]+/gi, 'bearer [REDACTED]')
|
|
220
|
+
.replace(/device\/[A-Fa-f0-9]+/g, 'device/[REDACTED]');
|
|
175
221
|
console.error(`[apns] curl transport error: ${msg}`);
|
|
176
222
|
return { error: msg, failed: 1, sent: 0, success: false };
|
|
177
223
|
}
|
|
178
224
|
}
|
|
179
225
|
|
|
180
|
-
shutdown(): void {
|
|
181
|
-
// No persistent state to release.
|
|
182
|
-
}
|
|
183
|
-
|
|
184
226
|
private getJwt(): string {
|
|
185
227
|
const now = Date.now();
|
|
186
228
|
if (this.cachedJwt && now - this.cachedJwtAt < JWT_REFRESH_INTERVAL_MS) {
|
|
@@ -29,11 +29,13 @@ import Database from 'better-sqlite3';
|
|
|
29
29
|
import * as fs from 'fs';
|
|
30
30
|
import * as path from 'path';
|
|
31
31
|
|
|
32
|
+
import { shooterDataDir } from '../utils/shooter-home.js';
|
|
33
|
+
|
|
32
34
|
export type { PendingRequest };
|
|
33
35
|
|
|
34
36
|
const MAX_AGE_MS = 5 * 60 * 1000;
|
|
35
37
|
|
|
36
|
-
const DB_DIR =
|
|
38
|
+
const DB_DIR = shooterDataDir();
|
|
37
39
|
const DB_PATH = path.join(DB_DIR, 'shooter.db');
|
|
38
40
|
|
|
39
41
|
export class PendingRequestsStore {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Pure context-building + goal store for the autopilot engine.
|
|
2
|
+
//
|
|
3
|
+
// Kept free of server imports (no pty-manager, no SQLite) so it is unit-testable in isolation,
|
|
4
|
+
// the same way next-step-consensus.ts and decide-injection.ts are.
|
|
5
|
+
//
|
|
6
|
+
// WHY a goal store: the engine builds its LLM context from the last ~12 session events, with no
|
|
7
|
+
// memory of what the session is trying to achieve. After a few autonomous cycles the original
|
|
8
|
+
// goal scrolls out of that window and consensus drifts toward "what the agent just did". Pinning
|
|
9
|
+
// a per-terminal goal and prepending it to every context keeps the lenses (and thus the injected
|
|
10
|
+
// next-steps) anchored. It is also the literal "set it as a goal" the autonomous loop needs.
|
|
11
|
+
|
|
12
|
+
// Per-terminal goal store. Lives in the engine's module graph; the /api/autopilot/goal route
|
|
13
|
+
// reaches setEngineGoal via the globalThis control object (it never imports the engine directly).
|
|
14
|
+
const goals = new Map<string, string>();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Build the LLM context string for one pipeline run. When a goal is present it leads the string
|
|
18
|
+
* so the summary + every lens see it first; otherwise the format is unchanged from before.
|
|
19
|
+
*/
|
|
20
|
+
export function buildEngineContext(input: {
|
|
21
|
+
errorCount: number;
|
|
22
|
+
events: string[];
|
|
23
|
+
goal?: string;
|
|
24
|
+
projectName: string;
|
|
25
|
+
status: string;
|
|
26
|
+
toolCallCount: number;
|
|
27
|
+
trigger: string;
|
|
28
|
+
}): string {
|
|
29
|
+
const goalLine = input.goal && input.goal.trim().length > 0 ? `Goal: ${input.goal.trim()}\n` : '';
|
|
30
|
+
return (
|
|
31
|
+
`${goalLine}Project: ${input.projectName}\nStatus: ${input.status}\n` +
|
|
32
|
+
`Errors: ${input.errorCount}\nTool calls: ${input.toolCallCount}\n` +
|
|
33
|
+
`Recent events: ${input.events.slice(-12).join('; ')}\nTrigger: ${input.trigger}`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Drop a terminal's goal (called when the terminal exits). */
|
|
38
|
+
export function clearEngineGoal(terminalId: string): void {
|
|
39
|
+
goals.delete(terminalId);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The pinned goal for a terminal, or undefined if none is set. */
|
|
43
|
+
export function getEngineGoal(terminalId: string): string | undefined {
|
|
44
|
+
return goals.get(terminalId);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Set (or, with a blank value, clear) the goal that anchors a terminal's autopilot context. */
|
|
48
|
+
export function setEngineGoal(terminalId: string, goal: string): void {
|
|
49
|
+
const trimmed = goal.trim();
|
|
50
|
+
if (trimmed.length > 0) {
|
|
51
|
+
// Cap defensively: the goal is prepended to EVERY engine LLM context, so an unbounded string
|
|
52
|
+
// would permanently bloat the prompt. The /api/autopilot/goal route also rejects > 500.
|
|
53
|
+
goals.set(terminalId, trimmed.slice(0, 500));
|
|
54
|
+
} else {
|
|
55
|
+
goals.delete(terminalId);
|
|
56
|
+
}
|
|
57
|
+
}
|