aicodeman 1.44.0 → 1.46.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/README.md +32 -8
- package/dist/agent-codex-identity.d.ts +26 -0
- package/dist/agent-codex-identity.d.ts.map +1 -0
- package/dist/agent-codex-identity.js +253 -0
- package/dist/agent-codex-identity.js.map +1 -0
- package/dist/agent-inspection.d.ts +32 -0
- package/dist/agent-inspection.d.ts.map +1 -0
- package/dist/agent-inspection.js +196 -0
- package/dist/agent-inspection.js.map +1 -0
- package/dist/agent-staging.d.ts +130 -0
- package/dist/agent-staging.d.ts.map +1 -0
- package/dist/agent-staging.js +312 -0
- package/dist/agent-staging.js.map +1 -0
- package/dist/case-rename.d.ts +65 -0
- package/dist/case-rename.d.ts.map +1 -0
- package/dist/case-rename.js +198 -0
- package/dist/case-rename.js.map +1 -0
- package/dist/cli-agent.d.ts +91 -3
- package/dist/cli-agent.d.ts.map +1 -1
- package/dist/cli-agent.js +501 -44
- package/dist/cli-agent.js.map +1 -1
- package/dist/cli-history.d.ts +20 -0
- package/dist/cli-history.d.ts.map +1 -0
- package/dist/cli-history.js +61 -0
- package/dist/cli-history.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +6 -1
- package/dist/cli.js.map +1 -1
- package/dist/codex-rollout-format.d.ts +66 -0
- package/dist/codex-rollout-format.d.ts.map +1 -0
- package/dist/codex-rollout-format.js +111 -0
- package/dist/codex-rollout-format.js.map +1 -0
- package/dist/codex-transcript.d.ts +3 -0
- package/dist/codex-transcript.d.ts.map +1 -1
- package/dist/codex-transcript.js +13 -41
- package/dist/codex-transcript.js.map +1 -1
- package/dist/config/auth-config.d.ts +16 -1
- package/dist/config/auth-config.d.ts.map +1 -1
- package/dist/config/auth-config.js +16 -1
- package/dist/config/auth-config.js.map +1 -1
- package/dist/config/cli-registry/schema.d.ts +12 -7
- package/dist/config/cli-registry/schema.d.ts.map +1 -1
- package/dist/config/cli-registry/schema.js +26 -0
- package/dist/config/cli-registry/schema.js.map +1 -1
- package/dist/config/cli-registry/stock.d.ts.map +1 -1
- package/dist/config/cli-registry/stock.js +40 -1
- package/dist/config/cli-registry/stock.js.map +1 -1
- package/dist/config/cli-registry/types.d.ts +57 -0
- package/dist/config/cli-registry/types.d.ts.map +1 -1
- package/dist/config/server-timing.d.ts +6 -0
- package/dist/config/server-timing.d.ts.map +1 -1
- package/dist/config/server-timing.js +9 -0
- package/dist/config/server-timing.js.map +1 -1
- package/dist/daemon-control.d.ts +1 -0
- package/dist/daemon-control.d.ts.map +1 -1
- package/dist/daemon-control.js +2 -0
- package/dist/daemon-control.js.map +1 -1
- package/dist/deepseek-transcript.d.ts +9 -7
- package/dist/deepseek-transcript.d.ts.map +1 -1
- package/dist/deepseek-transcript.js +9 -7
- package/dist/deepseek-transcript.js.map +1 -1
- package/dist/docker-hosts.d.ts +8 -0
- package/dist/docker-hosts.d.ts.map +1 -1
- package/dist/docker-hosts.js +29 -0
- package/dist/docker-hosts.js.map +1 -1
- package/dist/git-fetch-scheduler.d.ts +124 -0
- package/dist/git-fetch-scheduler.d.ts.map +1 -0
- package/dist/git-fetch-scheduler.js +249 -0
- package/dist/git-fetch-scheduler.js.map +1 -0
- package/dist/git-fetch.d.ts +241 -0
- package/dist/git-fetch.d.ts.map +1 -0
- package/dist/git-fetch.js +392 -0
- package/dist/git-fetch.js.map +1 -0
- package/dist/git-workspace-status.d.ts +36 -1
- package/dist/git-workspace-status.d.ts.map +1 -1
- package/dist/git-workspace-status.js +42 -5
- package/dist/git-workspace-status.js.map +1 -1
- package/dist/hooks-config.d.ts +14 -2
- package/dist/hooks-config.d.ts.map +1 -1
- package/dist/hooks-config.js +39 -3
- package/dist/hooks-config.js.map +1 -1
- package/dist/host-sensors.d.ts +115 -0
- package/dist/host-sensors.d.ts.map +1 -0
- package/dist/host-sensors.js +457 -0
- package/dist/host-sensors.js.map +1 -0
- package/dist/local-history.d.ts +71 -0
- package/dist/local-history.d.ts.map +1 -0
- package/dist/local-history.js +750 -0
- package/dist/local-history.js.map +1 -0
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +2 -2
- package/dist/mcp-server.js.map +1 -1
- package/dist/mux-interface.d.ts +5 -2
- package/dist/mux-interface.d.ts.map +1 -1
- package/dist/prompt-cache-keepwarm.d.ts.map +1 -1
- package/dist/prompt-cache-keepwarm.js +5 -3
- package/dist/prompt-cache-keepwarm.js.map +1 -1
- package/dist/remote-hosts.d.ts +7 -0
- package/dist/remote-hosts.d.ts.map +1 -1
- package/dist/remote-hosts.js +17 -3
- package/dist/remote-hosts.js.map +1 -1
- package/dist/remote-pane-history.d.ts.map +1 -1
- package/dist/remote-pane-history.js +2 -2
- package/dist/remote-pane-history.js.map +1 -1
- package/dist/session.d.ts +113 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +354 -13
- package/dist/session.js.map +1 -1
- package/dist/terminal-input-classifier.d.ts +8 -0
- package/dist/terminal-input-classifier.d.ts.map +1 -0
- package/dist/terminal-input-classifier.js +60 -0
- package/dist/terminal-input-classifier.js.map +1 -0
- package/dist/tmux-manager.d.ts +30 -4
- package/dist/tmux-manager.d.ts.map +1 -1
- package/dist/tmux-manager.js +101 -21
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tmux-prefix-table.d.ts +37 -5
- package/dist/tmux-prefix-table.d.ts.map +1 -1
- package/dist/tmux-prefix-table.js +40 -5
- package/dist/tmux-prefix-table.js.map +1 -1
- package/dist/tui/tui-app.d.ts +8 -1
- package/dist/tui/tui-app.d.ts.map +1 -1
- package/dist/tui/tui-app.js +49 -12
- package/dist/tui/tui-app.js.map +1 -1
- package/dist/tui/tui-client.d.ts +90 -3
- package/dist/tui/tui-client.d.ts.map +1 -1
- package/dist/tui/tui-client.js +195 -26
- package/dist/tui/tui-client.js.map +1 -1
- package/dist/types/session.d.ts +15 -0
- package/dist/types/session.d.ts.map +1 -1
- package/dist/types/session.js.map +1 -1
- package/dist/web/agent-inbox-persistence.d.ts +52 -0
- package/dist/web/agent-inbox-persistence.d.ts.map +1 -0
- package/dist/web/agent-inbox-persistence.js +139 -0
- package/dist/web/agent-inbox-persistence.js.map +1 -0
- package/dist/web/agent-inbox.d.ts +140 -0
- package/dist/web/agent-inbox.d.ts.map +1 -0
- package/dist/web/agent-inbox.js +258 -0
- package/dist/web/agent-inbox.js.map +1 -0
- package/dist/web/middleware/auth.d.ts +2 -0
- package/dist/web/middleware/auth.d.ts.map +1 -1
- package/dist/web/middleware/auth.js +145 -52
- package/dist/web/middleware/auth.js.map +1 -1
- package/dist/web/network-auth-policy.d.ts +13 -3
- package/dist/web/network-auth-policy.d.ts.map +1 -1
- package/dist/web/network-auth-policy.js +15 -3
- package/dist/web/network-auth-policy.js.map +1 -1
- package/dist/web/ports/infra-port.d.ts +2 -0
- package/dist/web/ports/infra-port.d.ts.map +1 -1
- package/dist/web/public/admin-ui.js.gz +0 -0
- package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
- package/dist/web/public/app.3093527f.js +43 -0
- package/dist/web/public/app.3093527f.js.br +0 -0
- package/dist/web/public/app.3093527f.js.gz +0 -0
- package/dist/web/public/approvals-ui.js.gz +0 -0
- package/dist/web/public/{constants.4866ecc3.js → constants.affcdde6.js} +622 -12
- package/dist/web/public/constants.affcdde6.js.br +0 -0
- package/dist/web/public/constants.affcdde6.js.gz +0 -0
- package/dist/web/public/cron-ui.js.gz +0 -0
- package/dist/web/public/entrance-animations.js.gz +0 -0
- package/dist/web/public/git-status-ui.js +88 -5
- package/dist/web/public/git-status-ui.js.br +0 -0
- package/dist/web/public/git-status-ui.js.gz +0 -0
- package/dist/web/public/header-divider.eb2d2463.js +1 -0
- package/dist/web/public/header-divider.eb2d2463.js.br +0 -0
- package/dist/web/public/header-divider.eb2d2463.js.gz +0 -0
- package/dist/web/public/home-sessions.js.gz +0 -0
- package/dist/web/public/host-wake-ui.js.gz +0 -0
- package/dist/web/public/i18n.70265257.js +2 -0
- package/dist/web/public/i18n.70265257.js.br +0 -0
- package/dist/web/public/i18n.70265257.js.gz +0 -0
- package/dist/web/public/image-input.941386e7.js.gz +0 -0
- package/dist/web/public/index.html +170 -16
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/input-cjk.8bc46081.js.gz +0 -0
- package/dist/web/public/keyboard-accessory.4e8b20ac.js.gz +0 -0
- package/dist/web/public/{mobile-handlers.44adaa07.js → mobile-handlers.f7c3f692.js} +26 -2
- package/dist/web/public/mobile-handlers.f7c3f692.js.br +0 -0
- package/dist/web/public/mobile-handlers.f7c3f692.js.gz +0 -0
- package/dist/web/public/mobile-ime-preview.2dbaf262.js.gz +0 -0
- package/dist/web/public/mobile-overview.js.gz +0 -0
- package/dist/web/public/mobile.1bbd2d67.css +1 -0
- package/dist/web/public/mobile.1bbd2d67.css.br +0 -0
- package/dist/web/public/mobile.1bbd2d67.css.gz +0 -0
- package/dist/web/public/notification-manager.feb82c04.js.gz +0 -0
- package/dist/web/public/orchestrator-panel.js.gz +0 -0
- package/dist/web/public/{panels-ui.cbb24ba1.js → panels-ui.4a56ee63.js} +76 -70
- package/dist/web/public/panels-ui.4a56ee63.js.br +0 -0
- package/dist/web/public/panels-ui.4a56ee63.js.gz +0 -0
- package/dist/web/public/ralph-panel.b350e2ef.js.gz +0 -0
- package/dist/web/public/ralph-wizard.b6c0dd19.js.gz +0 -0
- package/dist/web/public/readmymind-ui.js.gz +0 -0
- package/dist/web/public/reboot-restore-ui.js.gz +0 -0
- package/dist/web/public/respawn-ui.d7adcf6a.js.gz +0 -0
- package/dist/web/public/sanitize-html.e7147e76.js.gz +0 -0
- package/dist/web/public/session-lineage.js.gz +0 -0
- package/dist/web/public/session-ui.186f9419.js +106 -0
- package/dist/web/public/session-ui.186f9419.js.br +0 -0
- package/dist/web/public/session-ui.186f9419.js.gz +0 -0
- package/dist/web/public/settings-ui.a191154e.js +106 -0
- package/dist/web/public/settings-ui.a191154e.js.br +0 -0
- package/dist/web/public/settings-ui.a191154e.js.gz +0 -0
- package/dist/web/public/spreadsheet-preview-worker.js.gz +0 -0
- package/dist/web/public/spreadsheet-preview.75c751de.js.gz +0 -0
- package/dist/web/public/spreadsheet-xlsx-core.js.gz +0 -0
- package/dist/web/public/styles.7238eae8.css +1 -0
- package/dist/web/public/styles.7238eae8.css.br +0 -0
- package/dist/web/public/styles.7238eae8.css.gz +0 -0
- package/dist/web/public/subagent-windows.07277a58.js.gz +0 -0
- package/dist/web/public/sw.js +2 -2
- package/dist/web/public/sw.js.br +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/public/tab-layout-browser.2d6189f7.js.gz +0 -0
- package/dist/web/public/{tab-rail-resize.7157e0bd.js → tab-rail-resize.b5cdceaf.js} +1 -1
- package/dist/web/public/tab-rail-resize.b5cdceaf.js.br +0 -0
- package/dist/web/public/tab-rail-resize.b5cdceaf.js.gz +0 -0
- package/dist/web/public/terminal-keycode229-recovery.af09f99a.js.gz +0 -0
- package/dist/web/public/terminal-split.js.gz +0 -0
- package/dist/web/public/terminal-tile.js +10 -1
- package/dist/web/public/terminal-tile.js.br +0 -0
- package/dist/web/public/terminal-tile.js.gz +0 -0
- package/dist/web/public/terminal-ui.7e787829.js +4 -0
- package/dist/web/public/terminal-ui.7e787829.js.br +0 -0
- package/dist/web/public/terminal-ui.7e787829.js.gz +0 -0
- package/dist/web/public/tile-grid.js.gz +0 -0
- package/dist/web/public/ultracode-panel.js.gz +0 -0
- package/dist/web/public/ultracode-windows.js.gz +0 -0
- package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
- package/dist/web/public/vendor/exceljs.min.js.gz +0 -0
- package/dist/web/public/vendor/fflate.min.js.gz +0 -0
- package/dist/web/public/vendor/marked.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
- package/dist/web/public/vendor/{xterm-predictive-echo.ae5e3d5d.js → xterm-predictive-echo.276b11d9.js} +1 -1
- package/dist/web/public/vendor/xterm-predictive-echo.276b11d9.js.br +0 -0
- package/dist/web/public/vendor/xterm-predictive-echo.276b11d9.js.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.1be42307.js +5 -0
- package/dist/web/public/vendor/xterm-zerolag-input.1be42307.js.br +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.1be42307.js.gz +0 -0
- package/dist/web/public/vendor/xterm.css.gz +0 -0
- package/dist/web/public/vendor/xterm.min.js.gz +0 -0
- package/dist/web/public/voice-input.2342c37b.js.gz +0 -0
- package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
- package/dist/web/public/webview-tabs.js.gz +0 -0
- package/dist/web/public/whats-new.json +9 -9
- package/dist/web/route-helpers.d.ts +57 -6
- package/dist/web/route-helpers.d.ts.map +1 -1
- package/dist/web/route-helpers.js +72 -4
- package/dist/web/route-helpers.js.map +1 -1
- package/dist/web/routes/agent-session-routes.d.ts +20 -0
- package/dist/web/routes/agent-session-routes.d.ts.map +1 -0
- package/dist/web/routes/agent-session-routes.js +160 -0
- package/dist/web/routes/agent-session-routes.js.map +1 -0
- package/dist/web/routes/case-routes.d.ts.map +1 -1
- package/dist/web/routes/case-routes.js +203 -16
- package/dist/web/routes/case-routes.js.map +1 -1
- package/dist/web/routes/config-backup-routes.js +2 -2
- package/dist/web/routes/config-backup-routes.js.map +1 -1
- package/dist/web/routes/git-status-routes.d.ts +21 -7
- package/dist/web/routes/git-status-routes.d.ts.map +1 -1
- package/dist/web/routes/git-status-routes.js +78 -12
- package/dist/web/routes/git-status-routes.js.map +1 -1
- package/dist/web/routes/inbox-routes.d.ts +43 -0
- package/dist/web/routes/inbox-routes.d.ts.map +1 -0
- package/dist/web/routes/inbox-routes.js +148 -0
- package/dist/web/routes/inbox-routes.js.map +1 -0
- package/dist/web/routes/index.d.ts +2 -0
- package/dist/web/routes/index.d.ts.map +1 -1
- package/dist/web/routes/index.js +2 -0
- package/dist/web/routes/index.js.map +1 -1
- package/dist/web/routes/ralph-routes.d.ts.map +1 -1
- package/dist/web/routes/ralph-routes.js +7 -16
- package/dist/web/routes/ralph-routes.js.map +1 -1
- package/dist/web/routes/session-routes.d.ts.map +1 -1
- package/dist/web/routes/session-routes.js +120 -74
- package/dist/web/routes/session-routes.js.map +1 -1
- package/dist/web/routes/system-routes.d.ts.map +1 -1
- package/dist/web/routes/system-routes.js +107 -59
- package/dist/web/routes/system-routes.js.map +1 -1
- package/dist/web/routes/ws-routes.d.ts +5 -0
- package/dist/web/routes/ws-routes.d.ts.map +1 -1
- package/dist/web/routes/ws-routes.js +84 -47
- package/dist/web/routes/ws-routes.js.map +1 -1
- package/dist/web/schemas.d.ts +56 -7
- package/dist/web/schemas.d.ts.map +1 -1
- package/dist/web/schemas.js +88 -1
- package/dist/web/schemas.js.map +1 -1
- package/dist/web/sensitive-path.d.ts.map +1 -1
- package/dist/web/sensitive-path.js +2 -0
- package/dist/web/sensitive-path.js.map +1 -1
- package/dist/web/server.d.ts +7 -2
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +144 -16
- package/dist/web/server.js.map +1 -1
- package/dist/web/session-wait-registry.d.ts +48 -4
- package/dist/web/session-wait-registry.d.ts.map +1 -1
- package/dist/web/session-wait-registry.js +144 -6
- package/dist/web/session-wait-registry.js.map +1 -1
- package/dist/web/settings-writer.d.ts +47 -0
- package/dist/web/settings-writer.d.ts.map +1 -0
- package/dist/web/settings-writer.js +110 -0
- package/dist/web/settings-writer.js.map +1 -0
- package/dist/web/sse-events.d.ts +4 -1
- package/dist/web/sse-events.d.ts.map +1 -1
- package/dist/web/sse-events.js +4 -1
- package/dist/web/sse-events.js.map +1 -1
- package/package.json +1 -1
- package/skills/codeman/SKILL.md +30 -8
- package/skills/codeman/preamble.sh +3 -3
- package/skills/codeman/reference/endpoints.md +24 -3
- package/skills/codeman/reference/history.md +84 -0
- package/skills/codeman/reference/inspection-and-staging.md +152 -0
- package/skills/codeman/reference/messaging.md +1 -1
- package/skills/codeman/reference/verbs.md +65 -3
- package/dist/web/public/app.9314ee60.js +0 -43
- package/dist/web/public/app.9314ee60.js.br +0 -0
- package/dist/web/public/app.9314ee60.js.gz +0 -0
- package/dist/web/public/constants.4866ecc3.js.br +0 -0
- package/dist/web/public/constants.4866ecc3.js.gz +0 -0
- package/dist/web/public/i18n.22ca902b.js +0 -2
- package/dist/web/public/i18n.22ca902b.js.br +0 -0
- package/dist/web/public/i18n.22ca902b.js.gz +0 -0
- package/dist/web/public/mobile-handlers.44adaa07.js.br +0 -0
- package/dist/web/public/mobile-handlers.44adaa07.js.gz +0 -0
- package/dist/web/public/mobile.12fb18de.css +0 -1
- package/dist/web/public/mobile.12fb18de.css.br +0 -0
- package/dist/web/public/mobile.12fb18de.css.gz +0 -0
- package/dist/web/public/panels-ui.cbb24ba1.js.br +0 -0
- package/dist/web/public/panels-ui.cbb24ba1.js.gz +0 -0
- package/dist/web/public/session-ui.9c1f009b.js +0 -104
- package/dist/web/public/session-ui.9c1f009b.js.br +0 -0
- package/dist/web/public/session-ui.9c1f009b.js.gz +0 -0
- package/dist/web/public/settings-ui.fea9ddae.js +0 -106
- package/dist/web/public/settings-ui.fea9ddae.js.br +0 -0
- package/dist/web/public/settings-ui.fea9ddae.js.gz +0 -0
- package/dist/web/public/styles.e096fcd6.css +0 -1
- package/dist/web/public/styles.e096fcd6.css.br +0 -0
- package/dist/web/public/styles.e096fcd6.css.gz +0 -0
- package/dist/web/public/tab-rail-resize.7157e0bd.js.br +0 -0
- package/dist/web/public/tab-rail-resize.7157e0bd.js.gz +0 -0
- package/dist/web/public/terminal-ui.68dbd352.js +0 -2
- package/dist/web/public/terminal-ui.68dbd352.js.br +0 -0
- package/dist/web/public/terminal-ui.68dbd352.js.gz +0 -0
- package/dist/web/public/vendor/xterm-predictive-echo.ae5e3d5d.js.br +0 -0
- package/dist/web/public/vendor/xterm-predictive-echo.ae5e3d5d.js.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js +0 -4
- package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js.br +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js.gz +0 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Git fetch on an interval: the sessions the user turned on, fetched by a tick the server owns
|
|
3
|
+
* (`src/git-fetch-scheduler.ts`, wired in `src/web/server.ts`).
|
|
4
|
+
*
|
|
5
|
+
* The fetch module (src/git-fetch.ts) is the only git command the Git panel runs that writes, and on a timer
|
|
6
|
+
* it is the only one that can run without being asked (cloning and worktree management write too, but those
|
|
7
|
+
* happen when the user asks for them). Which is why the whole feature is off until the user turns it on, in
|
|
8
|
+
* Settings → Bottom bar, per session in Session Options → Git.
|
|
9
|
+
*
|
|
10
|
+
* The scheduler holds no timers of its own. The server ticks it (`GIT_FETCH_TICK_INTERVAL_MS`) and every
|
|
11
|
+
* tick reads the app setting FRESH, so a live toggle takes effect on the next tick instead of at boot, and
|
|
12
|
+
* turning the feature off actually stops the fetching. A session can override both the switch and the
|
|
13
|
+
* interval; a session with no override follows the app.
|
|
14
|
+
*
|
|
15
|
+
* Rules the code keeps and the tests pin:
|
|
16
|
+
* - Nothing runs when nothing is on: no repository set is read and nothing is spawned, so a quiet
|
|
17
|
+
* install costs one settings read per tick. A session that turned itself on still runs even while the
|
|
18
|
+
* app-wide switch is off: that is what a per-session switch has to mean.
|
|
19
|
+
* - Remote and Docker sessions are never fetched, exactly as the Git panel excludes them, and the Docker
|
|
20
|
+
* workspace roots are checked again every tick, so a session whose case became a Docker case stops
|
|
21
|
+
* fetching at the next tick. A Docker case list that cannot be read fails CLOSED: the tick fetches
|
|
22
|
+
* nothing (a fetch runs a repository's hooks, and without the list nothing tells which folders a
|
|
23
|
+
* container can write to), the folders it picked stay due for the next tick, and it is logged once
|
|
24
|
+
* until a read succeeds again.
|
|
25
|
+
* - A folder is fetched by at most one run at a time, and the run belongs to the FOLDER, not to one
|
|
26
|
+
* session: two sessions open on the same folder are the same clone, so they share one run and both get
|
|
27
|
+
* its result. State is keyed by `resolve(workingDir)`, deliberately not `realpath`, because `realpath`
|
|
28
|
+
* is itself one of the unbounded fs calls rule #516 bounds; the bounded probe below is the fs read.
|
|
29
|
+
* Every session open on a folder is remembered, so closing the one holding the override does not stop
|
|
30
|
+
* the folder's fetching while another session is still on it. A folder is due when ANY session open on it
|
|
31
|
+
* is due, and the tick decides that per folder in a second pass: the order the sessions were read cannot
|
|
32
|
+
* change which interval wins, so a 30 s session on a folder a 3600 s session last fetched ten minutes ago
|
|
33
|
+
* is fetched now. The folder is probed once, not once per session on it.
|
|
34
|
+
* - Before anything is fetched, the folder is probed BOUNDED with the default options (rule #516): a case
|
|
35
|
+
* on a mount that stopped answering is skipped, never touched, and a probe that answers `unknown` is not
|
|
36
|
+
* read as "not a folder".
|
|
37
|
+
* - A schedule is stamped at SELECTION, when a session goes into `due`, not when its run starts, so a
|
|
38
|
+
* tick that is still running cannot pick the same folder again; and a tick is skipped while the previous
|
|
39
|
+
* one is still going, so the bound below is the bound for the whole tick, not per tick.
|
|
40
|
+
* - At most `FETCH_CONCURRENCY` git processes at a time across the whole tick, because each is a git
|
|
41
|
+
* process: ONE bound is shared by every run of the tick, so `FETCH_CONCURRENCY` is the tick's ceiling
|
|
42
|
+
* rather than each folder's own. The folders get their OWN bound (`mapLimited`), because one semaphore
|
|
43
|
+
* cannot serve both levels: a folder that holds a slot and then waits for one for its own fetch is
|
|
44
|
+
* waiting on itself, and the folders that fill the bound are exactly the ones blocking it.
|
|
45
|
+
* - The interval is timed from the START of a run, so a run that takes three minutes does not push the
|
|
46
|
+
* next one out by three minutes; it lands on the tick after its own interval elapses.
|
|
47
|
+
* - The per-repository limit and the git timeout stay the module's defaults (`MAX_REPOS`,
|
|
48
|
+
* `DEFAULT_GIT_TIMEOUT_MS`): both settings are per device and never reach the server, so the server
|
|
49
|
+
* cannot know what a device chose.
|
|
50
|
+
* - A failure is reported, not retried harder: the folder keeps its schedule and gets its next run at its
|
|
51
|
+
* own interval. A settings read that fails skips this tick only, rather than deciding from defaults.
|
|
52
|
+
* settings.json is replaced atomically, so such a failure is a file that does not parse (a hand edit) or
|
|
53
|
+
* a real read error, which lasts until someone fixes it: it is logged once, and again only after a read
|
|
54
|
+
* has succeeded in between, not every 15 s tick.
|
|
55
|
+
*
|
|
56
|
+
* @module git-fetch-scheduler
|
|
57
|
+
*/
|
|
58
|
+
import { type GitFetchOptions, type GitFetchRun, type GitFetchSettings, type GitFetchStatus, type GitFetchTrigger } from './git-fetch.js';
|
|
59
|
+
import type { PathProbeKind } from './utils/bounded-path-probe.js';
|
|
60
|
+
/** What the scheduler needs from a session: the facts a decision needs, and nothing else. */
|
|
61
|
+
export interface GitFetchSessionView {
|
|
62
|
+
id: string;
|
|
63
|
+
workingDir: string;
|
|
64
|
+
/** `unknown`, not `boolean`: a session's remote/Docker markers are objects, and the only question here is whether either is set at all. */
|
|
65
|
+
remote?: unknown;
|
|
66
|
+
docker?: unknown;
|
|
67
|
+
gitFetchEnabled?: boolean;
|
|
68
|
+
gitFetchIntervalSeconds?: number;
|
|
69
|
+
}
|
|
70
|
+
export interface GitFetchPorts {
|
|
71
|
+
sessionIds(): Iterable<string>;
|
|
72
|
+
getSession(id: string): GitFetchSessionView | undefined;
|
|
73
|
+
/** Read FRESH every tick; that is the whole point of the tick-driven shape. */
|
|
74
|
+
readSettings(): Promise<GitFetchSettings>;
|
|
75
|
+
/**
|
|
76
|
+
* Host paths of every Docker case workspace. REJECTS when the list exists but cannot be read or parsed; the
|
|
77
|
+
* tick then fetches nothing and the folders it picked are due again on the next tick.
|
|
78
|
+
*/
|
|
79
|
+
dockerWorkspaces(): Promise<string[]>;
|
|
80
|
+
/**
|
|
81
|
+
* Bounded probe (rule #516), default options: a bulk scan keeps the stall cap, so this must not be called
|
|
82
|
+
* with `pastCap`. Anything that is not `directory` (including `unknown`, which is never `absent`) is
|
|
83
|
+
* skipped, not fetched.
|
|
84
|
+
*/
|
|
85
|
+
probePath(path: string): Promise<PathProbeKind>;
|
|
86
|
+
/** Called through `fetchWorkspaceOnce`, so a run already going for this folder is joined, not stacked. */
|
|
87
|
+
fetch(cwd: string, opts: GitFetchOptions, trigger: GitFetchTrigger): Promise<GitFetchRun>;
|
|
88
|
+
publish(id: string, status: GitFetchStatus): void;
|
|
89
|
+
log?(message: string): void;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* What one session will actually do: its own override wins, otherwise the app-wide setting. The interval is
|
|
93
|
+
* clamped again here because a persisted value can predate the schema's bounds.
|
|
94
|
+
*/
|
|
95
|
+
export declare function effectiveGitFetch(view: GitFetchSessionView, settings: GitFetchSettings): {
|
|
96
|
+
enabled: boolean;
|
|
97
|
+
intervalSeconds: number;
|
|
98
|
+
};
|
|
99
|
+
export declare class GitFetchScheduler {
|
|
100
|
+
private readonly ports;
|
|
101
|
+
/** When each FOLDER last started a run; timed from the start, not the end. */
|
|
102
|
+
private readonly lastRunAt;
|
|
103
|
+
private readonly inFlight;
|
|
104
|
+
/** Session → folder, so `remove()` can keep the folder's schedule while other sessions are still on it. */
|
|
105
|
+
private readonly folderOf;
|
|
106
|
+
private running;
|
|
107
|
+
private stopped;
|
|
108
|
+
/** The last settings read failed and was logged; cleared by the next read that succeeds. */
|
|
109
|
+
private settingsUnreadable;
|
|
110
|
+
/** The same for the Docker case list. */
|
|
111
|
+
private dockerCasesUnreadable;
|
|
112
|
+
constructor(ports: GitFetchPorts);
|
|
113
|
+
/** One tick: fetch every folder whose sessions are on and due. Returns how many folders ran. */
|
|
114
|
+
tick(now?: number): Promise<number>;
|
|
115
|
+
/**
|
|
116
|
+
* A session that closed or was detached: its schedule goes with it, but the FOLDER's stays while another
|
|
117
|
+
* session is still open on it: the run belongs to the clone, not to the session that asked for it.
|
|
118
|
+
*/
|
|
119
|
+
remove(id: string): void;
|
|
120
|
+
stop(): void;
|
|
121
|
+
/** Folders the scheduler still remembers a run for, so a test can see a `remove()` actually took. */
|
|
122
|
+
tracked(): number;
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=git-fetch-scheduler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-fetch-scheduler.d.ts","sourceRoot":"","sources":["../src/git-fetch-scheduler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AAIH,OAAO,EAKL,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,eAAe,EACrB,MAAM,gBAAgB,CAAC;AAExB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAEnE,6FAA6F;AAC7F,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,2IAA2I;IAC3I,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,uBAAuB,CAAC,EAAE,MAAM,CAAC;CAClC;AAQD,MAAM,WAAW,aAAa;IAC5B,UAAU,IAAI,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC/B,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS,CAAC;IACxD,+EAA+E;IAC/E,YAAY,IAAI,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC1C;;;OAGG;IACH,gBAAgB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACtC;;;;OAIG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IAChD,0GAA0G;IAC1G,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IAC1F,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,GAAG,IAAI,CAAC;IAClD,GAAG,CAAC,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,mBAAmB,EACzB,QAAQ,EAAE,gBAAgB,GACzB;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,eAAe,EAAE,MAAM,CAAA;CAAE,CAU/C;AAED,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgB;IACtC,8EAA8E;IAC9E,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA6B;IACvD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqB;IAC9C,2GAA2G;IAC3G,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA6B;IACtD,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,OAAO,CAAS;IACxB,4FAA4F;IAC5F,OAAO,CAAC,kBAAkB,CAAS;IACnC,yCAAyC;IACzC,OAAO,CAAC,qBAAqB,CAAS;gBAE1B,KAAK,EAAE,aAAa;IAIhC,gGAAgG;IAC1F,IAAI,CAAC,GAAG,SAAa,GAAG,OAAO,CAAC,MAAM,CAAC;IAiI7C;;;OAGG;IACH,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAWxB,IAAI,IAAI,IAAI;IAOZ,qGAAqG;IACrG,OAAO,IAAI,MAAM;CAGlB"}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Git fetch on an interval: the sessions the user turned on, fetched by a tick the server owns
|
|
3
|
+
* (`src/git-fetch-scheduler.ts`, wired in `src/web/server.ts`).
|
|
4
|
+
*
|
|
5
|
+
* The fetch module (src/git-fetch.ts) is the only git command the Git panel runs that writes, and on a timer
|
|
6
|
+
* it is the only one that can run without being asked (cloning and worktree management write too, but those
|
|
7
|
+
* happen when the user asks for them). Which is why the whole feature is off until the user turns it on, in
|
|
8
|
+
* Settings → Bottom bar, per session in Session Options → Git.
|
|
9
|
+
*
|
|
10
|
+
* The scheduler holds no timers of its own. The server ticks it (`GIT_FETCH_TICK_INTERVAL_MS`) and every
|
|
11
|
+
* tick reads the app setting FRESH, so a live toggle takes effect on the next tick instead of at boot, and
|
|
12
|
+
* turning the feature off actually stops the fetching. A session can override both the switch and the
|
|
13
|
+
* interval; a session with no override follows the app.
|
|
14
|
+
*
|
|
15
|
+
* Rules the code keeps and the tests pin:
|
|
16
|
+
* - Nothing runs when nothing is on: no repository set is read and nothing is spawned, so a quiet
|
|
17
|
+
* install costs one settings read per tick. A session that turned itself on still runs even while the
|
|
18
|
+
* app-wide switch is off: that is what a per-session switch has to mean.
|
|
19
|
+
* - Remote and Docker sessions are never fetched, exactly as the Git panel excludes them, and the Docker
|
|
20
|
+
* workspace roots are checked again every tick, so a session whose case became a Docker case stops
|
|
21
|
+
* fetching at the next tick. A Docker case list that cannot be read fails CLOSED: the tick fetches
|
|
22
|
+
* nothing (a fetch runs a repository's hooks, and without the list nothing tells which folders a
|
|
23
|
+
* container can write to), the folders it picked stay due for the next tick, and it is logged once
|
|
24
|
+
* until a read succeeds again.
|
|
25
|
+
* - A folder is fetched by at most one run at a time, and the run belongs to the FOLDER, not to one
|
|
26
|
+
* session: two sessions open on the same folder are the same clone, so they share one run and both get
|
|
27
|
+
* its result. State is keyed by `resolve(workingDir)`, deliberately not `realpath`, because `realpath`
|
|
28
|
+
* is itself one of the unbounded fs calls rule #516 bounds; the bounded probe below is the fs read.
|
|
29
|
+
* Every session open on a folder is remembered, so closing the one holding the override does not stop
|
|
30
|
+
* the folder's fetching while another session is still on it. A folder is due when ANY session open on it
|
|
31
|
+
* is due, and the tick decides that per folder in a second pass: the order the sessions were read cannot
|
|
32
|
+
* change which interval wins, so a 30 s session on a folder a 3600 s session last fetched ten minutes ago
|
|
33
|
+
* is fetched now. The folder is probed once, not once per session on it.
|
|
34
|
+
* - Before anything is fetched, the folder is probed BOUNDED with the default options (rule #516): a case
|
|
35
|
+
* on a mount that stopped answering is skipped, never touched, and a probe that answers `unknown` is not
|
|
36
|
+
* read as "not a folder".
|
|
37
|
+
* - A schedule is stamped at SELECTION, when a session goes into `due`, not when its run starts, so a
|
|
38
|
+
* tick that is still running cannot pick the same folder again; and a tick is skipped while the previous
|
|
39
|
+
* one is still going, so the bound below is the bound for the whole tick, not per tick.
|
|
40
|
+
* - At most `FETCH_CONCURRENCY` git processes at a time across the whole tick, because each is a git
|
|
41
|
+
* process: ONE bound is shared by every run of the tick, so `FETCH_CONCURRENCY` is the tick's ceiling
|
|
42
|
+
* rather than each folder's own. The folders get their OWN bound (`mapLimited`), because one semaphore
|
|
43
|
+
* cannot serve both levels: a folder that holds a slot and then waits for one for its own fetch is
|
|
44
|
+
* waiting on itself, and the folders that fill the bound are exactly the ones blocking it.
|
|
45
|
+
* - The interval is timed from the START of a run, so a run that takes three minutes does not push the
|
|
46
|
+
* next one out by three minutes; it lands on the tick after its own interval elapses.
|
|
47
|
+
* - The per-repository limit and the git timeout stay the module's defaults (`MAX_REPOS`,
|
|
48
|
+
* `DEFAULT_GIT_TIMEOUT_MS`): both settings are per device and never reach the server, so the server
|
|
49
|
+
* cannot know what a device chose.
|
|
50
|
+
* - A failure is reported, not retried harder: the folder keeps its schedule and gets its next run at its
|
|
51
|
+
* own interval. A settings read that fails skips this tick only, rather than deciding from defaults.
|
|
52
|
+
* settings.json is replaced atomically, so such a failure is a file that does not parse (a hand edit) or
|
|
53
|
+
* a real read error, which lasts until someone fixes it: it is logged once, and again only after a read
|
|
54
|
+
* has succeeded in between, not every 15 s tick.
|
|
55
|
+
*
|
|
56
|
+
* @module git-fetch-scheduler
|
|
57
|
+
*/
|
|
58
|
+
import { resolve } from 'node:path';
|
|
59
|
+
import { getErrorMessage } from './types.js';
|
|
60
|
+
import { FETCH_CONCURRENCY, MAX_GIT_FETCH_INTERVAL_SECONDS, MIN_GIT_FETCH_INTERVAL_SECONDS, gitFetchStatusFrom, } from './git-fetch.js';
|
|
61
|
+
import { clampInt, DEFAULT_GIT_TIMEOUT_MS, MAX_REPOS, createLimiter, mapLimited } from './git-workspace-status.js';
|
|
62
|
+
/**
|
|
63
|
+
* What one session will actually do: its own override wins, otherwise the app-wide setting. The interval is
|
|
64
|
+
* clamped again here because a persisted value can predate the schema's bounds.
|
|
65
|
+
*/
|
|
66
|
+
export function effectiveGitFetch(view, settings) {
|
|
67
|
+
return {
|
|
68
|
+
enabled: view.gitFetchEnabled ?? settings.enabled,
|
|
69
|
+
intervalSeconds: clampInt(view.gitFetchIntervalSeconds ?? settings.intervalSeconds, MIN_GIT_FETCH_INTERVAL_SECONDS, MAX_GIT_FETCH_INTERVAL_SECONDS, MIN_GIT_FETCH_INTERVAL_SECONDS),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export class GitFetchScheduler {
|
|
73
|
+
ports;
|
|
74
|
+
/** When each FOLDER last started a run; timed from the start, not the end. */
|
|
75
|
+
lastRunAt = new Map();
|
|
76
|
+
inFlight = new Set();
|
|
77
|
+
/** Session → folder, so `remove()` can keep the folder's schedule while other sessions are still on it. */
|
|
78
|
+
folderOf = new Map();
|
|
79
|
+
running = false;
|
|
80
|
+
stopped = false;
|
|
81
|
+
/** The last settings read failed and was logged; cleared by the next read that succeeds. */
|
|
82
|
+
settingsUnreadable = false;
|
|
83
|
+
/** The same for the Docker case list. */
|
|
84
|
+
dockerCasesUnreadable = false;
|
|
85
|
+
constructor(ports) {
|
|
86
|
+
this.ports = ports;
|
|
87
|
+
}
|
|
88
|
+
/** One tick: fetch every folder whose sessions are on and due. Returns how many folders ran. */
|
|
89
|
+
async tick(now = Date.now()) {
|
|
90
|
+
if (this.stopped || this.running)
|
|
91
|
+
return 0;
|
|
92
|
+
this.running = true;
|
|
93
|
+
try {
|
|
94
|
+
let settings;
|
|
95
|
+
try {
|
|
96
|
+
settings = await this.ports.readSettings();
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
// Writes are atomic, so this is a real read error or a file that does not parse (a hand edit), and it
|
|
100
|
+
// stays that way until it is fixed: logged once, not on every tick. The reader never puts the file's
|
|
101
|
+
// text in its error (readGitFetchSettings), since settings.json can hold an API key.
|
|
102
|
+
if (!this.settingsUnreadable) {
|
|
103
|
+
this.settingsUnreadable = true;
|
|
104
|
+
this.ports.log?.(`git fetch: settings unreadable, ticks skipped until it reads again: ${getErrorMessage(err)}`);
|
|
105
|
+
}
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
this.settingsUnreadable = false;
|
|
109
|
+
// Pass 1: who is on what, before anything is decided. Every session open on a folder is recorded here,
|
|
110
|
+
// not only the one that would get picked, so `remove()` can still see who is open on it when the session
|
|
111
|
+
// holding the override is the one that closes.
|
|
112
|
+
const groups = new Map();
|
|
113
|
+
for (const id of this.ports.sessionIds()) {
|
|
114
|
+
const view = this.ports.getSession(id);
|
|
115
|
+
if (!view || view.remote || view.docker)
|
|
116
|
+
continue;
|
|
117
|
+
const effective = effectiveGitFetch(view, settings);
|
|
118
|
+
if (!effective.enabled)
|
|
119
|
+
continue;
|
|
120
|
+
const folder = resolve(view.workingDir);
|
|
121
|
+
this.folderOf.set(id, folder);
|
|
122
|
+
const group = groups.get(folder);
|
|
123
|
+
const member = { view, intervalSeconds: effective.intervalSeconds };
|
|
124
|
+
if (group)
|
|
125
|
+
group.push(member);
|
|
126
|
+
else
|
|
127
|
+
groups.set(folder, [member]);
|
|
128
|
+
}
|
|
129
|
+
// Pass 2: the folder's turn, decided from the whole group rather than from whichever session happened to
|
|
130
|
+
// be read first. A folder is due when ANY session open on it is due: a session on a 30 s interval on a
|
|
131
|
+
// folder a 3600 s session last fetched ten minutes ago has to be fetched now, because that same run
|
|
132
|
+
// satisfies the long interval too. Deciding in one pass means the session read first owns the clock: a
|
|
133
|
+
// group ordered [short, long] and a group ordered [long, short] would have behaved differently, and the
|
|
134
|
+
// run belongs to the folder, so no session in the group should.
|
|
135
|
+
const due = new Map();
|
|
136
|
+
// What each picked folder's clock said before this tick stamped it, so a tick that ends up fetching
|
|
137
|
+
// nothing (an unreadable Docker case list, below) can leave the folders due again.
|
|
138
|
+
const stampedOver = new Map();
|
|
139
|
+
for (const [folder, group] of groups) {
|
|
140
|
+
if (this.inFlight.has(folder))
|
|
141
|
+
continue;
|
|
142
|
+
const started = this.lastRunAt.get(folder);
|
|
143
|
+
const isDue = started === undefined || group.some((m) => now - started >= m.intervalSeconds * 1000);
|
|
144
|
+
if (!isDue)
|
|
145
|
+
continue;
|
|
146
|
+
// Bounded probe (rule #516), once per folder rather than once per session on it.
|
|
147
|
+
const kind = await this.ports.probePath(folder);
|
|
148
|
+
if (kind !== 'directory') {
|
|
149
|
+
this.ports.log?.(`git fetch: ${folder} skipped, answered ${kind}`);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
this.inFlight.add(folder);
|
|
153
|
+
// The clock starts at the selection, so a slow folder does not shift its own schedule.
|
|
154
|
+
stampedOver.set(folder, started);
|
|
155
|
+
this.lastRunAt.set(folder, now);
|
|
156
|
+
due.set(folder, group);
|
|
157
|
+
}
|
|
158
|
+
if (!due.size)
|
|
159
|
+
return 0;
|
|
160
|
+
// Only now, because nothing above needs it: a quiet tick does not read the Docker case list.
|
|
161
|
+
let dockerWorkspaces;
|
|
162
|
+
try {
|
|
163
|
+
dockerWorkspaces = await this.ports.dockerWorkspaces();
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
// Fail CLOSED: a fetch runs the repository's hooks, and without the list nothing tells which folders a
|
|
167
|
+
// container can write to. Nothing ran, so the picks are undone (a stamp a `remove()` already cleared
|
|
168
|
+
// stays cleared) and the folders are due again on the next tick instead of after their interval.
|
|
169
|
+
for (const [folder, started] of stampedOver) {
|
|
170
|
+
this.inFlight.delete(folder);
|
|
171
|
+
if (this.lastRunAt.get(folder) !== now)
|
|
172
|
+
continue;
|
|
173
|
+
if (started === undefined)
|
|
174
|
+
this.lastRunAt.delete(folder);
|
|
175
|
+
else
|
|
176
|
+
this.lastRunAt.set(folder, started);
|
|
177
|
+
}
|
|
178
|
+
if (!this.dockerCasesUnreadable) {
|
|
179
|
+
this.dockerCasesUnreadable = true;
|
|
180
|
+
this.ports.log?.(`git fetch: Docker case list unreadable, nothing fetched until it reads again: ${getErrorMessage(err)}`);
|
|
181
|
+
}
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
this.dockerCasesUnreadable = false;
|
|
185
|
+
// ONE bound for the git processes of the whole tick, handed to every run so no folder gets its own:
|
|
186
|
+
// each fetch is one git process, and a folder can hold up to `MAX_REPOS_LIMIT` of them, so a bound per
|
|
187
|
+
// run would let `FETCH_CONCURRENCY` folders × `MAX_REPOS_LIMIT` processes run at once.
|
|
188
|
+
const fetches = createLimiter(FETCH_CONCURRENCY);
|
|
189
|
+
// The folders get their OWN bound, because the same semaphore cannot serve both levels: a folder that
|
|
190
|
+
// holds a slot and then waits for a slot for its own fetch is waiting on itself: once
|
|
191
|
+
// `FETCH_CONCURRENCY` folders fill the bound, the ones holding slots are exactly the ones that can
|
|
192
|
+
// never get one back. Both bounds are `FETCH_CONCURRENCY`, so the tick still never runs more git
|
|
193
|
+
// processes than the module's ceiling.
|
|
194
|
+
await mapLimited([...due.entries()], FETCH_CONCURRENCY, async ([folder, group]) => {
|
|
195
|
+
try {
|
|
196
|
+
const run = await this.ports.fetch(folder, {
|
|
197
|
+
dockerWorkspaces,
|
|
198
|
+
maxRepos: MAX_REPOS,
|
|
199
|
+
timeoutMs: DEFAULT_GIT_TIMEOUT_MS,
|
|
200
|
+
limiter: fetches,
|
|
201
|
+
}, 'interval');
|
|
202
|
+
// Every session on the folder gets the SAME run, with the trigger of the run that actually happened:
|
|
203
|
+
// if this tick joined a run a click had already started, the row has to say it was a click, because
|
|
204
|
+
// that is what fetched it.
|
|
205
|
+
for (const member of group)
|
|
206
|
+
this.ports.publish(member.view.id, gitFetchStatusFrom(run.report, run.trigger));
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
// The run that failed keeps its reason in the panel's own row; here only the folder-level failure is
|
|
210
|
+
// worth a line, because nothing else would say a scheduled fetch could not run at all.
|
|
211
|
+
this.ports.log?.(`git fetch: ${folder} could not run: ${getErrorMessage(err)}`);
|
|
212
|
+
}
|
|
213
|
+
finally {
|
|
214
|
+
this.inFlight.delete(folder);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
return due.size;
|
|
218
|
+
}
|
|
219
|
+
finally {
|
|
220
|
+
this.running = false;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* A session that closed or was detached: its schedule goes with it, but the FOLDER's stays while another
|
|
225
|
+
* session is still open on it: the run belongs to the clone, not to the session that asked for it.
|
|
226
|
+
*/
|
|
227
|
+
remove(id) {
|
|
228
|
+
const folder = this.folderOf.get(id);
|
|
229
|
+
this.folderOf.delete(id);
|
|
230
|
+
if (!folder)
|
|
231
|
+
return;
|
|
232
|
+
const stillOpen = [...this.folderOf.values()].includes(folder);
|
|
233
|
+
if (!stillOpen) {
|
|
234
|
+
this.lastRunAt.delete(folder);
|
|
235
|
+
this.inFlight.delete(folder);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
stop() {
|
|
239
|
+
this.stopped = true;
|
|
240
|
+
this.lastRunAt.clear();
|
|
241
|
+
this.inFlight.clear();
|
|
242
|
+
this.folderOf.clear();
|
|
243
|
+
}
|
|
244
|
+
/** Folders the scheduler still remembers a run for, so a test can see a `remove()` actually took. */
|
|
245
|
+
tracked() {
|
|
246
|
+
return new Set([...this.lastRunAt.keys(), ...this.inFlight]).size;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
//# sourceMappingURL=git-fetch-scheduler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-fetch-scheduler.js","sourceRoot":"","sources":["../src/git-fetch-scheduler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview `git fetch` for the repositories the Git panel shows, on the user's click
|
|
3
|
+
* (`POST /api/sessions/:id/git-fetch`) or on an interval the user turned on
|
|
4
|
+
* (`src/git-fetch-scheduler.ts`).
|
|
5
|
+
*
|
|
6
|
+
* The status module (src/git-workspace-status.ts) is deliberately offline: "behind" is as of the last
|
|
7
|
+
* fetch, so a number that is stale can only be made current by fetching. This is the only git command the
|
|
8
|
+
* Git panel runs that writes, and the only git command Codeman runs on a timer. Cloning a repository and
|
|
9
|
+
* managing worktrees write too, but those are things the user asked for at the moment they asked; this is
|
|
10
|
+
* the one that can happen later, by itself. Nothing here commits, checks out, merges or touches the working
|
|
11
|
+
* tree: `git fetch` updates remote-tracking
|
|
12
|
+
* refs and nothing else, and `FETCH_CONFIG_ARGS` keeps it from starting a gc in a repository an agent is
|
|
13
|
+
* already using. It is not harmless in the way a read is: a fetch holds git's ref lock, so an agent's own
|
|
14
|
+
* `git pull` in the same clone can collide with it (`cannot lock ref`), which is why one folder is fetched
|
|
15
|
+
* by at most one run at a time (`fetchWorkspaceOnce`).
|
|
16
|
+
*
|
|
17
|
+
* Same split as git-clone.ts: the pure half (`buildFetchArgs`, `describeFetchFailure`, `fetchEnvFor`,
|
|
18
|
+
* `gitFetchSettingsFrom`, `gitFetchStatusFrom`) holds every decision and tests without spawning, and the IO
|
|
19
|
+
* half runs one bounded `git fetch --quiet` per repository.
|
|
20
|
+
*
|
|
21
|
+
* Rules the code keeps and the tests pin:
|
|
22
|
+
* - The repository set is `resolveWorkspaceRepos`, the single source the panel and the diff route
|
|
23
|
+
* already use, so Fetch cannot reach a repository the panel would not show. The Docker rule is
|
|
24
|
+
* checked before any git runs: a repository a container can write to is refused outright, because a
|
|
25
|
+
* planted config would run on the host, and `git fetch` is exactly the command that can run a
|
|
26
|
+
* repository's own hooks (`reference-transaction`, and `pre-auto-gc` through the automatic maintenance
|
|
27
|
+
* that follows a fetch) and its config commands such as `credential.helper`, which is why a timer only
|
|
28
|
+
* runs it after the user opts in.
|
|
29
|
+
* - One run per folder at a time: `fetchWorkspaceOnce` joins a run already going for the same folder
|
|
30
|
+
* instead of starting a second one, because two fetches in one clone collide on the ref lock.
|
|
31
|
+
* - Bounded: the git timeout from Settings → Bottom bar per repository, `MAX_REPOS_LIMIT` repositories at
|
|
32
|
+
* most, a hard wall-clock bound on each spawn, and `FETCH_CONCURRENCY` git processes at a time for the
|
|
33
|
+
* whole run: one `Limiter`, shared by the scheduler's tick, not one bound per folder.
|
|
34
|
+
* - Non-interactive: `GIT_TERMINAL_PROMPT=0` and BatchMode ssh, so a private repository that needs a
|
|
35
|
+
* login fails fast instead of hanging the request on an invisible username prompt. Codeman collects
|
|
36
|
+
* no credentials; a repository the user's own git is already signed in to works because that is the
|
|
37
|
+
* user's existing setup, so unlike a clone no credential helper is disabled here. That BatchMode ssh is
|
|
38
|
+
* a default, never an override: `fetchEnvFor` keeps whatever the repository's `core.sshCommand`, the
|
|
39
|
+
* user's own `GIT_SSH_COMMAND` or a `GIT_SSH` already says, because on a machine with several ssh setups
|
|
40
|
+
* that choice is usually the one that works. ⚠️ The default still leaves a record: `StrictHostKeyChecking=accept-new`
|
|
41
|
+
* writes an unknown host key into `~/.ssh/known_hosts` without asking: BatchMode stops a prompt, not a
|
|
42
|
+
* write.
|
|
43
|
+
* - The timeout kills the PROCESS GROUP, not just the `git` parent. A fetch over http spawns
|
|
44
|
+
* `git-remote-http` children, and `execFile`'s `timeout` signals the parent only, so the answer can go
|
|
45
|
+
* out while a helper is still running, and on a timer that repeats every tick with nobody watching.
|
|
46
|
+
* `spawn` with `detached: true` puts the tree in its own group so the escalation (SIGTERM, then SIGKILL
|
|
47
|
+
* `FETCH_KILL_ESCALATION_MS` later) reaches the children too, which is why the negative pid is used
|
|
48
|
+
* rather than `child.kill()`. Same shape as `runGit` in git-clone.ts, without that module's slot queue:
|
|
49
|
+
* fetch is bounded by `FETCH_CONCURRENCY`, not by a clone queue.
|
|
50
|
+
* - Failure text reaching a client is control-stripped and credential-redacted (`sanitizeGitOutput`),
|
|
51
|
+
* and the browser writes it with `textContent` only.
|
|
52
|
+
*
|
|
53
|
+
* @module git-fetch
|
|
54
|
+
*/
|
|
55
|
+
import { type GitFailureCode } from './git-clone.js';
|
|
56
|
+
import { type Limiter, type GitRunner } from './git-workspace-status.js';
|
|
57
|
+
/**
|
|
58
|
+
* Git processes in flight at once, for one run, and for a whole scheduler tick, because the tick hands ONE
|
|
59
|
+
* bound to every run (see `createLimiter`): each fetch is one git process, and a folder can hold up to
|
|
60
|
+
* `MAX_REPOS_LIMIT`. The tick bounds its folders separately, because the same semaphore cannot serve both
|
|
61
|
+
* levels: a folder holding a slot would then wait for a slot its own fetch needs.
|
|
62
|
+
*/
|
|
63
|
+
export declare const FETCH_CONCURRENCY = 4;
|
|
64
|
+
/**
|
|
65
|
+
* How long the process group gets to answer SIGTERM before the same escalation goes to SIGKILL. Kept as a
|
|
66
|
+
* const so a test can say how long it is willing to wait for the group to actually die.
|
|
67
|
+
*/
|
|
68
|
+
export declare const FETCH_KILL_ESCALATION_MS = 3000;
|
|
69
|
+
/**
|
|
70
|
+
* Config overrides set ahead of the argv, so an unattended fetch cannot do more than move refs: no
|
|
71
|
+
* fsmonitor hook, and no automatic maintenance or gc. A fetch is the git command that can run a
|
|
72
|
+
* repository's own hooks (`reference-transaction`, and `pre-auto-gc` through the maintenance that follows a
|
|
73
|
+
* fetch), and a gc started by a timer in a repository an agent is working in is exactly the kind of extra
|
|
74
|
+
* write this module must not do. Kept as a const so a test can pin it without spawning git.
|
|
75
|
+
*/
|
|
76
|
+
export declare const FETCH_CONFIG_ARGS: string[];
|
|
77
|
+
/** The interval the user can turn on (Settings → Bottom bar, and Session Options → Git): bounds and default. */
|
|
78
|
+
export declare const DEFAULT_GIT_FETCH_INTERVAL_SECONDS = 300;
|
|
79
|
+
export declare const MIN_GIT_FETCH_INTERVAL_SECONDS = 30;
|
|
80
|
+
export declare const MAX_GIT_FETCH_INTERVAL_SECONDS = 3600;
|
|
81
|
+
export interface GitFetchSettings {
|
|
82
|
+
enabled: boolean;
|
|
83
|
+
intervalSeconds: number;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* A settings object as the scheduler should read it: absent means OFF, because nothing runs on a timer
|
|
87
|
+
* until the user turns this on, and a number outside the schema's bounds is clamped rather than trusted.
|
|
88
|
+
* Pure, so the reading itself is testable without a settings file.
|
|
89
|
+
*/
|
|
90
|
+
export declare function gitFetchSettingsFrom(settings: Record<string, unknown> | undefined): GitFetchSettings;
|
|
91
|
+
/**
|
|
92
|
+
* The only git command this module runs. Kept as a function so a test can pin the verb: nothing here
|
|
93
|
+
* commits, checks out, merges or prunes, and no operand is interpolated: the repository is the process
|
|
94
|
+
* `cwd` and the argv is fixed.
|
|
95
|
+
*/
|
|
96
|
+
export declare function buildFetchArgs(): string[];
|
|
97
|
+
/**
|
|
98
|
+
* The `git` process also gets the `FETCH_CONFIG_ARGS` overrides ahead of this argv; they are kept separate
|
|
99
|
+
* so pinning the verb here does not have to say anything about maintenance, and so both can be tested
|
|
100
|
+
* without spawning.
|
|
101
|
+
*
|
|
102
|
+
* A repository with no remote is a fact, not a fault. Git's wordings for it differ by version (2.47
|
|
103
|
+
* exits quietly and this code is never reached, older git says "no default remote repository"), so the
|
|
104
|
+
* branch is pinned on literal fixtures and the code the panel shows is the code this writes.
|
|
105
|
+
*/
|
|
106
|
+
export type FetchFailureCode = GitFailureCode | 'NO_REMOTE';
|
|
107
|
+
export interface FetchFailure {
|
|
108
|
+
code: FetchFailureCode;
|
|
109
|
+
/** User-facing summary. */
|
|
110
|
+
message: string;
|
|
111
|
+
/** Tail of git's own stderr, control-stripped and credential-redacted. */
|
|
112
|
+
stderr: string;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* A thrown git error as the panel should show it. `classifyGitFailure` (git-clone.ts) already knows the
|
|
116
|
+
* network reasons (auth, an unreachable host, a missing git), so they are reused here; only its timeout
|
|
117
|
+
* wording is written for clones, so the fetch path replaces it with what the user can actually do.
|
|
118
|
+
*/
|
|
119
|
+
export declare function describeFetchFailure(err: unknown): FetchFailure;
|
|
120
|
+
/**
|
|
121
|
+
* The environment a fetch gets, as a pure decision so it can be pinned without spawning git.
|
|
122
|
+
*
|
|
123
|
+
* `gitNonInteractiveEnv` supplies the BatchMode default, and the default is only a default: git reads
|
|
124
|
+
* `GIT_SSH_COMMAND` from the environment ahead of the repository's `core.sshCommand`, so setting it would
|
|
125
|
+
* override a repository that already chose its own ssh (an alternative host, an identity file, a jump box)
|
|
126
|
+
* and break a setup that works today. The server's own `GIT_SSH` is the same kind of answer: git reads it
|
|
127
|
+
* after both of those (`GIT_SSH_COMMAND`, then `core.sshCommand`, then `GIT_SSH`), so a written default would
|
|
128
|
+
* hide it too. That is the only case where this module makes an ssh choice on the user's behalf, so it is the
|
|
129
|
+
* whole decision here: the caller supplies the value it read, and this decides between the user's
|
|
130
|
+
* environment and the repository's own answer.
|
|
131
|
+
*/
|
|
132
|
+
export declare function fetchEnvFor(base: NodeJS.ProcessEnv, repoSshCommand: string | null): NodeJS.ProcessEnv;
|
|
133
|
+
/**
|
|
134
|
+
* The repository's own `core.sshCommand`, or null. One bounded, read-only `git config --get` per fetch:
|
|
135
|
+
* git would honour that config itself if we did not write `GIT_SSH_COMMAND` over it, so honouring it means
|
|
136
|
+
* not writing one. `--get` reads local and global config, which is the value git would have used, and no
|
|
137
|
+
* hook or filter runs on the way to it.
|
|
138
|
+
*
|
|
139
|
+
* The read happens inside the same slot and the same wall-clock bound as the fetch it serves, so it cannot
|
|
140
|
+
* add a fifth git process or push a fetch past the user's timeout: it spends part of it.
|
|
141
|
+
*/
|
|
142
|
+
export declare function readRepoSshCommand(cwd: string, timeoutMs: number): Promise<string | null>;
|
|
143
|
+
/**
|
|
144
|
+
* Fetch is allowed to write refs, so this runner does not carry `--no-optional-locks`: fetch takes a
|
|
145
|
+
* required lock anyway.
|
|
146
|
+
*
|
|
147
|
+
* `spawn`, not `execFile`, because the bound has to reach the whole tree (see the fileoverview): the
|
|
148
|
+
* caller is answered at the timeout, and the SIGTERM → SIGKILL escalation is deliberately NOT cancelled
|
|
149
|
+
* when the answer goes out, because the promise is a promise about the answer while the group still has to
|
|
150
|
+
* die. That is the difference from `runGit` in git-clone.ts, which disarms the escalation at `finish` and so
|
|
151
|
+
* can leave a `git-remote-http` child running after a timed-out call.
|
|
152
|
+
*
|
|
153
|
+
* The thrown error keeps the shape `describeFetchFailure` reads: `stderr` for the reason, `message` for a
|
|
154
|
+
* spawn failure (`spawn git ENOENT` → GIT_MISSING), `killed` and `code` for the timeout. A non-zero exit
|
|
155
|
+
* rejects the way `execFile` would (`code` is then the exit code), because resolving it would report a real
|
|
156
|
+
* failure as a clean run with an empty reason.
|
|
157
|
+
*
|
|
158
|
+
* `timeoutMs` bounds the `core.sshCommand` read and the fetch TOGETHER: the spawn is armed with what the read
|
|
159
|
+
* left, so a `git` that never answers costs one timeout, not two. A read that used the whole bound answers
|
|
160
|
+
* as a timeout without starting the fetch.
|
|
161
|
+
*/
|
|
162
|
+
export declare const runFetch: GitRunner;
|
|
163
|
+
export interface GitFetchEntry {
|
|
164
|
+
/** Folder name of the repository (its root's basename), as the panel names it. */
|
|
165
|
+
name: string;
|
|
166
|
+
/** The repository root relative to the working directory: `.`, `..`, `api`, `apps/web`. */
|
|
167
|
+
path: string;
|
|
168
|
+
ok: boolean;
|
|
169
|
+
failure?: FetchFailure;
|
|
170
|
+
}
|
|
171
|
+
export interface GitFetchReport {
|
|
172
|
+
/** `ok` when at least one repository was fetched (or attempted); the other states are as in the overview. */
|
|
173
|
+
state: 'ok' | 'not-a-repo' | 'unsupported' | 'error';
|
|
174
|
+
reason?: 'remote' | 'docker';
|
|
175
|
+
error?: string;
|
|
176
|
+
/** One entry per repository fetched, in the same order the panel lists them. */
|
|
177
|
+
repos: GitFetchEntry[];
|
|
178
|
+
/** More than `repoLimit` repositories were found; only the first were fetched. */
|
|
179
|
+
reposTruncated: boolean;
|
|
180
|
+
/** The most repositories this fetch would list (the caller's setting, or `MAX_REPOS`). */
|
|
181
|
+
repoLimit?: number;
|
|
182
|
+
/** When the last repository finished, so the panel can say what the "behind" number is now based on. */
|
|
183
|
+
fetchedAt: number;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* What one run did, in the shape a session keeps and the Session Options Git row shows. `trigger` says
|
|
187
|
+
* whether the user clicked or the interval fired, because a number the user did not ask for should read
|
|
188
|
+
* differently from one they did.
|
|
189
|
+
*/
|
|
190
|
+
/** What a run was, and why it happened. */
|
|
191
|
+
export type GitFetchTrigger = 'click' | 'interval';
|
|
192
|
+
export interface GitFetchStatus {
|
|
193
|
+
ranAt: number;
|
|
194
|
+
trigger: GitFetchTrigger;
|
|
195
|
+
/** As in the report: `ok`, or the state that stopped the run. */
|
|
196
|
+
state: GitFetchReport['state'];
|
|
197
|
+
/** Repositories this run covered. */
|
|
198
|
+
repos: number;
|
|
199
|
+
/** How many of them could not be fetched. */
|
|
200
|
+
failed: number;
|
|
201
|
+
/** The first failure, named: enough for one row without reprinting the whole list. */
|
|
202
|
+
firstFailure?: {
|
|
203
|
+
name: string;
|
|
204
|
+
code: FetchFailureCode;
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* One run, with the trigger that caused it. A joiner gets the run already going, so what it reports is that
|
|
209
|
+
* run's trigger rather than the one that made it ask: a click that joins an interval run did not fetch
|
|
210
|
+
* anything itself, and the row the panel shows has to describe the fetch that actually happened.
|
|
211
|
+
*/
|
|
212
|
+
export interface GitFetchRun {
|
|
213
|
+
report: GitFetchReport;
|
|
214
|
+
trigger: GitFetchTrigger;
|
|
215
|
+
}
|
|
216
|
+
export declare function gitFetchStatusFrom(report: GitFetchReport, trigger: GitFetchTrigger): GitFetchStatus;
|
|
217
|
+
/** Options for the fetch path: the same repository rules and limits as an overview, without its cache clock. */
|
|
218
|
+
export interface GitFetchOptions {
|
|
219
|
+
git?: GitRunner;
|
|
220
|
+
home?: string;
|
|
221
|
+
dockerWorkspaces?: string[];
|
|
222
|
+
maxRepos?: number;
|
|
223
|
+
timeoutMs?: number;
|
|
224
|
+
/**
|
|
225
|
+
* The shared slot count for a whole scheduler tick. Passed in so `FETCH_CONCURRENCY` bounds the tick
|
|
226
|
+
* rather than each folder separately; without it, a tick over more than `FETCH_CONCURRENCY` folders could
|
|
227
|
+
* have that many git processes going at once inside each one.
|
|
228
|
+
*/
|
|
229
|
+
limiter?: Limiter;
|
|
230
|
+
}
|
|
231
|
+
export declare const emptyReport: (state: GitFetchReport["state"], extra?: Partial<GitFetchReport>) => GitFetchReport;
|
|
232
|
+
/**
|
|
233
|
+
* Fetch every repository the panel would show for `cwd`, through the same resolver so the two cannot
|
|
234
|
+
* disagree. A repository that failed keeps its row with the reason, exactly as an unreadable repository
|
|
235
|
+
* stays in the status list: the panel must show that something was not fetched rather than quietly skip it.
|
|
236
|
+
*/
|
|
237
|
+
export declare function fetchGitWorkspaceOverview(cwd: string, opts?: GitFetchOptions): Promise<GitFetchReport>;
|
|
238
|
+
export declare function fetchWorkspaceOnce(folder: string, opts: GitFetchOptions | undefined, trigger: GitFetchTrigger): Promise<GitFetchRun>;
|
|
239
|
+
/** Test seam: an in-memory lane is not cleared by stopping the scheduler, so a test has to clear it itself. */
|
|
240
|
+
export declare function clearGitFetchLane(): void;
|
|
241
|
+
//# sourceMappingURL=git-fetch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-fetch.d.ts","sourceRoot":"","sources":["../src/git-fetch.ts"],"names":[],"mappings":"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"}
|