aicodeman 1.47.0 → 1.48.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 +3 -0
- package/dist/cli-notify.d.ts +34 -0
- package/dist/cli-notify.d.ts.map +1 -0
- package/dist/cli-notify.js +100 -0
- package/dist/cli-notify.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +16 -0
- package/dist/cli.js.map +1 -1
- package/dist/config/cli-registry/patterns.d.ts +1 -1
- package/dist/config/cli-registry/patterns.d.ts.map +1 -1
- package/dist/config/cli-registry/patterns.js +4 -0
- package/dist/config/cli-registry/patterns.js.map +1 -1
- package/dist/config/cli-registry/schema.d.ts +6 -0
- package/dist/config/cli-registry/schema.d.ts.map +1 -1
- package/dist/config/cli-registry/schema.js +62 -2
- 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 +43 -26
- package/dist/config/cli-registry/stock.js.map +1 -1
- package/dist/config/cli-registry/types.d.ts +47 -17
- package/dist/config/cli-registry/types.d.ts.map +1 -1
- package/dist/config/path-probe.d.ts +6 -0
- package/dist/config/path-probe.d.ts.map +1 -1
- package/dist/config/path-probe.js +6 -0
- package/dist/config/path-probe.js.map +1 -1
- package/dist/config-backup.d.ts +1 -1
- package/dist/config-backup.d.ts.map +1 -1
- package/dist/config-backup.js +2 -0
- package/dist/config-backup.js.map +1 -1
- package/dist/custom-model-hosts.d.ts +32 -8
- package/dist/custom-model-hosts.d.ts.map +1 -1
- package/dist/custom-model-hosts.js +10 -0
- package/dist/custom-model-hosts.js.map +1 -1
- package/dist/custom-model-injection-apply.d.ts +19 -5
- package/dist/custom-model-injection-apply.d.ts.map +1 -1
- package/dist/custom-model-injection-apply.js +88 -8
- package/dist/custom-model-injection-apply.js.map +1 -1
- package/dist/custom-model-injection.d.ts +6 -0
- package/dist/custom-model-injection.d.ts.map +1 -1
- package/dist/custom-model-injection.js +92 -10
- package/dist/custom-model-injection.js.map +1 -1
- package/dist/discord-bridge.d.ts +338 -0
- package/dist/discord-bridge.d.ts.map +1 -0
- package/dist/discord-bridge.js +1256 -0
- package/dist/discord-bridge.js.map +1 -0
- package/dist/edit-history-local-source.d.ts +83 -0
- package/dist/edit-history-local-source.d.ts.map +1 -0
- package/dist/edit-history-local-source.js +198 -0
- package/dist/edit-history-local-source.js.map +1 -0
- package/dist/edit-history-parse.d.ts +114 -0
- package/dist/edit-history-parse.d.ts.map +1 -0
- package/dist/edit-history-parse.js +312 -0
- package/dist/edit-history-parse.js.map +1 -0
- package/dist/edit-history-store.d.ts +209 -0
- package/dist/edit-history-store.d.ts.map +1 -0
- package/dist/edit-history-store.js +599 -0
- package/dist/edit-history-store.js.map +1 -0
- package/dist/git-clone.d.ts +6 -1
- package/dist/git-clone.d.ts.map +1 -1
- package/dist/git-clone.js +79 -14
- package/dist/git-clone.js.map +1 -1
- package/dist/image-watcher.js +4 -4
- package/dist/image-watcher.js.map +1 -1
- package/dist/mux-interface.d.ts +10 -1
- package/dist/mux-interface.d.ts.map +1 -1
- package/dist/session-cli-registry-bridge.d.ts.map +1 -1
- package/dist/session-cli-registry-bridge.js +4 -0
- package/dist/session-cli-registry-bridge.js.map +1 -1
- package/dist/session.d.ts +70 -3
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +147 -4
- package/dist/session.js.map +1 -1
- package/dist/tmux-manager.d.ts +40 -0
- package/dist/tmux-manager.d.ts.map +1 -1
- package/dist/tmux-manager.js +62 -0
- package/dist/tmux-manager.js.map +1 -1
- package/dist/types/discord.d.ts +55 -0
- package/dist/types/discord.d.ts.map +1 -0
- package/dist/types/discord.js +18 -0
- package/dist/types/discord.js.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/session.d.ts +13 -0
- package/dist/types/session.d.ts.map +1 -1
- package/dist/types/session.js.map +1 -1
- package/dist/web/case-path.d.ts +2 -0
- package/dist/web/case-path.d.ts.map +1 -1
- package/dist/web/case-path.js +3 -3
- package/dist/web/case-path.js.map +1 -1
- package/dist/web/edit-history-sync.d.ts +158 -0
- package/dist/web/edit-history-sync.d.ts.map +1 -0
- package/dist/web/edit-history-sync.js +445 -0
- package/dist/web/edit-history-sync.js.map +1 -0
- package/dist/web/paste-image-gc.d.ts +6 -0
- package/dist/web/paste-image-gc.d.ts.map +1 -1
- package/dist/web/paste-image-gc.js +6 -0
- package/dist/web/paste-image-gc.js.map +1 -1
- package/dist/web/ports/session-port.d.ts +13 -0
- package/dist/web/ports/session-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.fe8a6b4c.js → app.963b5ba8.js} +6 -6
- package/dist/web/public/app.963b5ba8.js.br +0 -0
- package/dist/web/public/app.963b5ba8.js.gz +0 -0
- package/dist/web/public/approvals-ui.js.gz +0 -0
- package/dist/web/public/{constants.affcdde6.js → constants.782170e1.js} +114 -25
- package/dist/web/public/constants.782170e1.js.br +0 -0
- package/dist/web/public/constants.782170e1.js.gz +0 -0
- package/dist/web/public/cron-ui.js.gz +0 -0
- package/dist/web/public/edit-history-ui.js +630 -0
- package/dist/web/public/edit-history-ui.js.br +0 -0
- package/dist/web/public/edit-history-ui.js.gz +0 -0
- package/dist/web/public/entrance-animations.js.gz +0 -0
- package/dist/web/public/git-status-ui.js.gz +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.33f930d7.js +2 -0
- package/dist/web/public/i18n.33f930d7.js.br +0 -0
- package/dist/web/public/i18n.33f930d7.js.gz +0 -0
- package/dist/web/public/image-input.a5d0f8f3.js.gz +0 -0
- package/dist/web/public/index.html +160 -14
- 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.ff64fafc.js → keyboard-accessory.d4ec5957.js} +72 -1
- package/dist/web/public/keyboard-accessory.d4ec5957.js.br +0 -0
- package/dist/web/public/keyboard-accessory.d4ec5957.js.gz +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.5ef2ce85.css +1 -0
- package/dist/web/public/mobile.5ef2ce85.css.br +0 -0
- package/dist/web/public/mobile.5ef2ce85.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.4a56ee63.js → panels-ui.b100727d.js} +2 -2
- package/dist/web/public/panels-ui.b100727d.js.br +0 -0
- package/dist/web/public/{panels-ui.4a56ee63.js.gz → panels-ui.b100727d.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 → respawn-ui.304ba0d4.js} +1 -1
- package/dist/web/public/respawn-ui.304ba0d4.js.br +0 -0
- package/dist/web/public/respawn-ui.304ba0d4.js.gz +0 -0
- package/dist/web/public/sanitize-html.e7147e76.js.gz +0 -0
- package/dist/web/public/security-notices.json +1 -21
- package/dist/web/public/session-lineage.js.gz +0 -0
- package/dist/web/public/{session-ui.426fb671.js → session-ui.5ac82f2c.js} +30 -30
- package/dist/web/public/session-ui.5ac82f2c.js.br +0 -0
- package/dist/web/public/session-ui.5ac82f2c.js.gz +0 -0
- package/dist/web/public/settings-ui.d1d4fb99.js +106 -0
- package/dist/web/public/settings-ui.d1d4fb99.js.br +0 -0
- package/dist/web/public/settings-ui.d1d4fb99.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.77dc538f.css +1 -0
- package/dist/web/public/styles.77dc538f.css.br +0 -0
- package/dist/web/public/styles.77dc538f.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.b5cdceaf.js.gz +0 -0
- package/dist/web/public/terminal-keycode229-recovery.98843069.js.gz +0 -0
- package/dist/web/public/terminal-split.js.gz +0 -0
- package/dist/web/public/terminal-tile.js.gz +0 -0
- package/dist/web/public/{terminal-ui.7e787829.js → terminal-ui.d7c75ce8.js} +3 -3
- package/dist/web/public/terminal-ui.d7c75ce8.js.br +0 -0
- package/dist/web/public/terminal-ui.d7c75ce8.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.276b11d9.js.gz +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 -8
- package/dist/web/routes/case-routes.d.ts.map +1 -1
- package/dist/web/routes/case-routes.js +104 -8
- package/dist/web/routes/case-routes.js.map +1 -1
- package/dist/web/routes/config-backup-routes.d.ts +5 -1
- package/dist/web/routes/config-backup-routes.d.ts.map +1 -1
- package/dist/web/routes/config-backup-routes.js +4 -1
- package/dist/web/routes/config-backup-routes.js.map +1 -1
- package/dist/web/routes/custom-model-routes.d.ts +7 -2
- package/dist/web/routes/custom-model-routes.d.ts.map +1 -1
- package/dist/web/routes/custom-model-routes.js +51 -6
- package/dist/web/routes/custom-model-routes.js.map +1 -1
- package/dist/web/routes/discord-routes.d.ts +25 -0
- package/dist/web/routes/discord-routes.d.ts.map +1 -0
- package/dist/web/routes/discord-routes.js +206 -0
- package/dist/web/routes/discord-routes.js.map +1 -0
- package/dist/web/routes/edit-history-routes.d.ts +43 -0
- package/dist/web/routes/edit-history-routes.d.ts.map +1 -0
- package/dist/web/routes/edit-history-routes.js +106 -0
- package/dist/web/routes/edit-history-routes.js.map +1 -0
- package/dist/web/routes/file-routes.d.ts.map +1 -1
- package/dist/web/routes/file-routes.js +62 -2
- package/dist/web/routes/file-routes.js.map +1 -1
- 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/session-routes.d.ts +17 -0
- package/dist/web/routes/session-routes.d.ts.map +1 -1
- package/dist/web/routes/session-routes.js +87 -37
- 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 +1 -0
- package/dist/web/routes/system-routes.js.map +1 -1
- package/dist/web/schemas.d.ts +68 -1
- package/dist/web/schemas.d.ts.map +1 -1
- package/dist/web/schemas.js +77 -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 +15 -0
- package/dist/web/sensitive-path.js.map +1 -1
- package/dist/web/server.d.ts +43 -0
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +219 -11
- package/dist/web/server.js.map +1 -1
- package/package.json +1 -1
- package/dist/web/public/app.fe8a6b4c.js.br +0 -0
- package/dist/web/public/app.fe8a6b4c.js.gz +0 -0
- package/dist/web/public/constants.affcdde6.js.br +0 -0
- package/dist/web/public/constants.affcdde6.js.gz +0 -0
- package/dist/web/public/i18n.a618ef4e.js +0 -2
- package/dist/web/public/i18n.a618ef4e.js.br +0 -0
- package/dist/web/public/i18n.a618ef4e.js.gz +0 -0
- package/dist/web/public/keyboard-accessory.ff64fafc.js.br +0 -0
- package/dist/web/public/keyboard-accessory.ff64fafc.js.gz +0 -0
- package/dist/web/public/mobile.1bbd2d67.css +0 -1
- package/dist/web/public/mobile.1bbd2d67.css.br +0 -0
- package/dist/web/public/mobile.1bbd2d67.css.gz +0 -0
- package/dist/web/public/panels-ui.4a56ee63.js.br +0 -0
- package/dist/web/public/respawn-ui.d7adcf6a.js.br +0 -0
- package/dist/web/public/respawn-ui.d7adcf6a.js.gz +0 -0
- package/dist/web/public/session-ui.426fb671.js.br +0 -0
- package/dist/web/public/session-ui.426fb671.js.gz +0 -0
- package/dist/web/public/settings-ui.a191154e.js +0 -106
- 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/styles.4f0c67a0.css +0 -1
- package/dist/web/public/styles.4f0c67a0.css.br +0 -0
- package/dist/web/public/styles.4f0c67a0.css.gz +0 -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/server.js
CHANGED
|
@@ -44,6 +44,8 @@ import { looksLikeHostReboot, newestPersistedActivity, planRebootRestore } from
|
|
|
44
44
|
import { rebootRestoreRegistry } from './reboot-restore-registry.js';
|
|
45
45
|
import { dataPath, getDataDir, CODEMAN_INSTANCE } from '../config/instance.js';
|
|
46
46
|
import { WebhookNotifier, readWebhookConfig } from '../webhook-notify.js';
|
|
47
|
+
import { DiscordBridge, screenTail } from '../discord-bridge.js';
|
|
48
|
+
import { isExternalCliTranscriptMode } from './response-viewer-transcript.js';
|
|
47
49
|
import { stopHostSensors } from '../host-sensors.js';
|
|
48
50
|
import { webviewFetch } from './webview-egress.js';
|
|
49
51
|
import { readRemoteHosts, rehydrateRemoteHostFields } from '../remote-hosts.js';
|
|
@@ -66,8 +68,8 @@ import { attachmentRegistry, buildFileThumbnailRoute, registerExternalAttachment
|
|
|
66
68
|
import { folderGrants } from '../folder-listing.js';
|
|
67
69
|
import { getCli, enabledClis, listClis } from '../config/cli-registry/registry.js';
|
|
68
70
|
import { isCliEntryInstalled, probeStockCliAvailability } from '../utils/cli-installed-probes.js';
|
|
69
|
-
import { readCustomModelHosts } from '../custom-model-hosts.js';
|
|
70
|
-
import { applyCustomModelInjection, customModelConfigDir, removeConfigDir } from '../custom-model-injection-apply.js';
|
|
71
|
+
import { readCustomModelHosts, resolveCustomModelContextLength } from '../custom-model-hosts.js';
|
|
72
|
+
import { applyCustomModelInjection, customModelConfigDir, removeConfigDir, } from '../custom-model-injection-apply.js';
|
|
71
73
|
import { registerGeneratedArtifactAttachment } from '../generated-artifact-attachments.js';
|
|
72
74
|
import { buildDetectedAttachmentHistoryItem, buildExternalAttachmentHistoryItem, } from '../session-attachment-history.js';
|
|
73
75
|
import { TranscriptWatcher } from '../transcript-watcher.js';
|
|
@@ -99,6 +101,9 @@ import { probePathKind } from '../utils/bounded-path-probe.js';
|
|
|
99
101
|
import { defaultDockerWorkspaces } from './routes/git-status-routes.js';
|
|
100
102
|
import { composerIsEmpty, plainFrame } from '../session-submit-verifier.js';
|
|
101
103
|
import { detectUsageLimitPause } from '../usage-limit-patterns.js';
|
|
104
|
+
import { EditHistorySync } from './edit-history-sync.js';
|
|
105
|
+
import { EditHistoryStore } from '../edit-history-store.js';
|
|
106
|
+
import { probeLocalPaths, readClaudeEditSources, resolveClaudeTranscript } from '../edit-history-local-source.js';
|
|
102
107
|
import { stopDeepSeekWeb } from '../deepseek-web-server.js';
|
|
103
108
|
import { wireRespawnListeners, setupTimedRespawn, restoreRespawnController, saveRespawnConfig, } from './respawn-event-wiring.js';
|
|
104
109
|
import { reconcileUpdateOnBoot } from './self-update.js';
|
|
@@ -136,7 +141,7 @@ import { bootstrapInitialAdmin, hasUsers, resolveClaudeModeForUsername } from '.
|
|
|
136
141
|
import { installRouteErrorHandler } from './route-error-handler.js';
|
|
137
142
|
import { installEmptyApiPostParser } from './request-body-parser.js';
|
|
138
143
|
import { isExplicitlyEnabled, isLoopbackBindHost, buildHostPolicy } from './network-auth-policy.js';
|
|
139
|
-
import { registerPushRoutes, registerTeamRoutes, registerMuxRoutes, registerFileRoutes, registerHtmlViewRoutes, registerScheduledRoutes, registerHookEventRoutes, registerApprovalRoutes, registerInboxRoutes, registerRebootRestoreRoutes, registerReadMyMindRoutes, registerGitStatusRoutes, registerStatusTelemetryRoutes, registerSystemRoutes, registerCaseRoutes, registerWorktreeRoutes, registerSessionRoutes, registerAgentSessionRoutes, registerRespawnRoutes, registerRalphRoutes, registerPlanRoutes, registerClipboardRoutes, registerSearchRoutes, registerOrchestratorRoutes, registerCronRoutes, registerMeRoutes, registerAdminRoutes, registerWsRoutes, registerVoiceRoutes, registerWebviewRoutes, registerTabLayoutRoutes, registerMcpSyncRoutes, registerConfigBackupRoutes, runConfigBackup, registerWebhookRoutes, registerDoctorRoutes, registerCustomModelRoutes, refreshAllCustomModelHosts, readCustomModelEndpointsEnabled, closeAllLlamaSwapLogTails, detectCustomModelSwapDisplacements, pruneIdleLlamaSwapLogTails, tryWebviewRefererFallback, registerCliRegistryRoutes, } from './routes/index.js';
|
|
144
|
+
import { registerPushRoutes, registerTeamRoutes, registerMuxRoutes, registerFileRoutes, registerHtmlViewRoutes, registerScheduledRoutes, registerHookEventRoutes, registerApprovalRoutes, registerInboxRoutes, registerRebootRestoreRoutes, registerReadMyMindRoutes, registerGitStatusRoutes, registerEditHistoryRoutes, registerStatusTelemetryRoutes, registerSystemRoutes, registerCaseRoutes, registerWorktreeRoutes, registerSessionRoutes, registerAgentSessionRoutes, registerRespawnRoutes, registerRalphRoutes, registerPlanRoutes, registerClipboardRoutes, registerSearchRoutes, registerOrchestratorRoutes, registerCronRoutes, registerMeRoutes, registerAdminRoutes, registerWsRoutes, registerVoiceRoutes, registerWebviewRoutes, registerTabLayoutRoutes, registerMcpSyncRoutes, registerConfigBackupRoutes, runConfigBackup, registerWebhookRoutes, registerDiscordRoutes, registerDoctorRoutes, registerCustomModelRoutes, refreshAllCustomModelHosts, readCustomModelEndpointsEnabled, closeAllLlamaSwapLogTails, detectCustomModelSwapDisplacements, pruneIdleLlamaSwapLogTails, tryWebviewRefererFallback, registerCliRegistryRoutes, } from './routes/index.js';
|
|
140
145
|
import { isLostWebviewFrameNavigation } from './webview-proxy.js';
|
|
141
146
|
import { CronService } from '../cron/cron-service.js';
|
|
142
147
|
import { caseSessionSettled } from '../case-rename.js';
|
|
@@ -205,6 +210,19 @@ function getOrCreateSelfSignedCert() {
|
|
|
205
210
|
export class WebServer extends EventEmitter {
|
|
206
211
|
app;
|
|
207
212
|
sessions = new Map();
|
|
213
|
+
/**
|
|
214
|
+
* The edit history panel's reader (edit-history-sync.ts). Reads only when the panel asks;
|
|
215
|
+
* the main transcript is the one the transcript watcher follows (hook-reported path), or,
|
|
216
|
+
* before the first hook after a restart, a lookup under the session's claude config dir.
|
|
217
|
+
*/
|
|
218
|
+
editHistorySync = new EditHistorySync({
|
|
219
|
+
transcriptPath: (session) => resolveClaudeTranscript(this.transcriptWatchers.get(session.id)?.getPath(), [session.claudeSessionId || session.id, session.id], session.claudeConfigDir),
|
|
220
|
+
readSources: readClaudeEditSources,
|
|
221
|
+
probe: probeLocalPaths,
|
|
222
|
+
loadStore: (sessionId) => EditHistoryStore.load(sessionId),
|
|
223
|
+
deleteStore: (sessionId) => EditHistoryStore.delete(sessionId),
|
|
224
|
+
sweepStores: (keep) => EditHistoryStore.sweep(keep),
|
|
225
|
+
});
|
|
208
226
|
respawnControllers = new Map();
|
|
209
227
|
respawnTimers = new Map();
|
|
210
228
|
runSummaryTrackers = new Map();
|
|
@@ -351,6 +369,29 @@ export class WebServer extends EventEmitter {
|
|
|
351
369
|
pushStore = new PushSubscriptionStore();
|
|
352
370
|
/** ntfy / Slack / Discord / generic webhook for the push events; config in webhook.json (0600). */
|
|
353
371
|
webhookNotifier = new WebhookNotifier(() => readWebhookConfig(getDataDir()), webviewFetch);
|
|
372
|
+
/** Two-way Discord bridge (thread per tab); config in discord.json (0600). Idle until enabled in Settings. */
|
|
373
|
+
/** Set by the session routes when they register (see SessionPort). */
|
|
374
|
+
lastResponseFor;
|
|
375
|
+
discordBridge = new DiscordBridge({
|
|
376
|
+
configDir: getDataDir(),
|
|
377
|
+
fetchImpl: webviewFetch,
|
|
378
|
+
sendInput: async (sessionId, text) => {
|
|
379
|
+
const session = this.sessions.get(sessionId);
|
|
380
|
+
if (!session)
|
|
381
|
+
return false;
|
|
382
|
+
// A Discord reply is a person's own input, so it names the tab like any other prompt.
|
|
383
|
+
if (await session.writeViaMux(text, { fromUser: true }).catch(() => false))
|
|
384
|
+
return true;
|
|
385
|
+
return session.write(text, { fromUser: true });
|
|
386
|
+
},
|
|
387
|
+
agentExited: (id) => !!this.sessions.get(id)?.paneExit,
|
|
388
|
+
awaitingChoice: (id) => approvalInbox.listPending().some((item) => item.sessionId === id && item.kind !== 'idle'),
|
|
389
|
+
// Remote (ssh) sessions keep their folder on another host, so received files have nowhere local to go.
|
|
390
|
+
sessionDir: (sessionId) => {
|
|
391
|
+
const session = this.sessions.get(sessionId);
|
|
392
|
+
return session && !session.remote ? session.workingDir : undefined;
|
|
393
|
+
},
|
|
394
|
+
});
|
|
354
395
|
teamWatcher = new TeamWatcher();
|
|
355
396
|
_orchestratorLoop = null;
|
|
356
397
|
titleHostname;
|
|
@@ -682,6 +723,7 @@ export class WebServer extends EventEmitter {
|
|
|
682
723
|
addSession: this.registerSessionWithLayout.bind(this),
|
|
683
724
|
tabLayouts: this.tabLayouts,
|
|
684
725
|
cleanupSession: this.cleanupSession.bind(this),
|
|
726
|
+
forgetEditHistory: this.forgetEditHistory.bind(this),
|
|
685
727
|
setupSessionListeners: this.setupSessionListeners.bind(this),
|
|
686
728
|
persistSessionState: this.persistSessionState.bind(this),
|
|
687
729
|
persistSessionStateNow: this._persistSessionStateNow.bind(this),
|
|
@@ -1086,6 +1128,7 @@ export class WebServer extends EventEmitter {
|
|
|
1086
1128
|
registerRebootRestoreRoutes(this.app, ctx);
|
|
1087
1129
|
registerReadMyMindRoutes(this.app, ctx);
|
|
1088
1130
|
registerGitStatusRoutes(this.app, ctx);
|
|
1131
|
+
registerEditHistoryRoutes(this.app, ctx, this.editHistorySync);
|
|
1089
1132
|
// Git fetch on an interval: this is only the wake-up, the scheduler itself decides what is due. Off in
|
|
1090
1133
|
// tests for the same reason as the polls below: a test drives `tick()` directly instead of waiting on
|
|
1091
1134
|
// a timer the harness would have to keep alive. The setting is read FRESH inside `tick()`, so turning
|
|
@@ -1104,6 +1147,8 @@ export class WebServer extends EventEmitter {
|
|
|
1104
1147
|
// The registry's lifetime is the server's: it drops per-session wake state on every
|
|
1105
1148
|
// cleanup path and resolves in-flight wakes on shutdown.
|
|
1106
1149
|
this.remoteWake = registerSessionRoutes(this.app, ctx);
|
|
1150
|
+
// The routes' ctx is a separate object: take the last-response reader they registered on it (Discord alerts).
|
|
1151
|
+
this.lastResponseFor = ctx.lastResponseFor;
|
|
1107
1152
|
registerAgentSessionRoutes(this.app, ctx);
|
|
1108
1153
|
registerRespawnRoutes(this.app, ctx);
|
|
1109
1154
|
registerRalphRoutes(this.app, ctx);
|
|
@@ -1116,12 +1161,23 @@ export class WebServer extends EventEmitter {
|
|
|
1116
1161
|
registerWebviewRoutes(this.app, ctx, this.basePath);
|
|
1117
1162
|
registerTabLayoutRoutes(this.app, ctx);
|
|
1118
1163
|
registerMcpSyncRoutes(this.app);
|
|
1119
|
-
registerConfigBackupRoutes(this.app);
|
|
1164
|
+
registerConfigBackupRoutes(this.app, { reloadDiscord: () => this.discordBridge.reload() });
|
|
1120
1165
|
registerWebhookRoutes(this.app, {
|
|
1121
1166
|
notifier: this.webhookNotifier,
|
|
1122
1167
|
configDir: getDataDir(),
|
|
1123
1168
|
hostTitle: () => this.windowTitle,
|
|
1124
1169
|
});
|
|
1170
|
+
registerDiscordRoutes(this.app, {
|
|
1171
|
+
bridge: this.discordBridge,
|
|
1172
|
+
configDir: getDataDir(),
|
|
1173
|
+
ctx,
|
|
1174
|
+
hostTitle: () => this.windowTitle,
|
|
1175
|
+
});
|
|
1176
|
+
if (!this.testMode) {
|
|
1177
|
+
// The gateway waits for restoreMuxSessions(): a reply during restore would be told the tab is gone.
|
|
1178
|
+
this.discordBridge.hold();
|
|
1179
|
+
void this.discordBridge.ensureLoaded().catch(() => undefined);
|
|
1180
|
+
}
|
|
1125
1181
|
registerDoctorRoutes(this.app);
|
|
1126
1182
|
registerCustomModelRoutes(this.app);
|
|
1127
1183
|
registerCliRegistryRoutes(this.app);
|
|
@@ -1240,13 +1296,46 @@ export class WebServer extends EventEmitter {
|
|
|
1240
1296
|
return undefined;
|
|
1241
1297
|
}
|
|
1242
1298
|
try {
|
|
1243
|
-
|
|
1299
|
+
// Boot recovery omits both `workingDir` (no reason to re-answer a trust dialog that
|
|
1300
|
+
// already fired once) and the session-env real-home lookup: the injected VALUES are
|
|
1301
|
+
// never persisted, so after a real restart the session can no longer carry its
|
|
1302
|
+
// allowlisted `DSH_HOME` either (`getEnvOverridesForPersist()` keeps only
|
|
1303
|
+
// `CLAUDE_CODE_*`/`CLAUDE_CONFIG_DIR`): the lookup could only ever hit on the first
|
|
1304
|
+
// apply in-process. Real-home resolution therefore falls back to the SERVER's
|
|
1305
|
+
// `DSH_HOME` or the recipe's `realHomeFallback` (~/.dsh).
|
|
1306
|
+
return applyCustomModelInjection(entry, endpoint, saved.modelId, session.id, resolveCustomModelContextLength(endpoint, saved.modelId));
|
|
1244
1307
|
}
|
|
1245
1308
|
catch (err) {
|
|
1246
1309
|
console.warn('[WebServer] Failed to rebuild custom-model env on recovery:', err);
|
|
1247
1310
|
return undefined;
|
|
1248
1311
|
}
|
|
1249
1312
|
}
|
|
1313
|
+
/**
|
|
1314
|
+
* Restore one persisted custom-model selection (both boot-recovery paths). The rebuilt
|
|
1315
|
+
* bookkeeping must come from the CURRENT recipe, not from the saved record: this is the
|
|
1316
|
+
* first feature where a shipped recipe changed KIND (deepseek: env redirect -> per-session
|
|
1317
|
+
* patch home), so a selection saved by an old build restores with e.g. no `configDir`
|
|
1318
|
+
* while the new recipe writes one, and clearing it later could then never delete the
|
|
1319
|
+
* generated dir. `envKeys` is the UNION of saved and applied keys: the tmux session may
|
|
1320
|
+
* still carry `setenv`s from both, and a clear has to unset every one.
|
|
1321
|
+
*/
|
|
1322
|
+
async _restoreCustomModel(session, saved) {
|
|
1323
|
+
const applied = await this._rebuildCustomModelEnv(session, saved);
|
|
1324
|
+
if (!applied) {
|
|
1325
|
+
// Endpoint gone or CLI unregistered: restore the bookkeeping exactly as saved, which
|
|
1326
|
+
// is what a later clear needs.
|
|
1327
|
+
session.setCustomModel(saved);
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
session.setCustomModel({
|
|
1331
|
+
...saved,
|
|
1332
|
+
// Prefer the current recipe's dir; keep the saved one when the recipe writes none,
|
|
1333
|
+
// so a clear can still delete a directory an older recipe created.
|
|
1334
|
+
configDir: applied.configDir ?? saved.configDir,
|
|
1335
|
+
launchModel: applied.launchModel,
|
|
1336
|
+
envKeys: [...new Set([...saved.envKeys, ...applied.envKeys])],
|
|
1337
|
+
}, applied.envOverrides);
|
|
1338
|
+
}
|
|
1250
1339
|
/** Persists full session state including respawn config to state.json */
|
|
1251
1340
|
_persistSessionStateNow(session) {
|
|
1252
1341
|
// See session-manager.updateSessionState: __envOverrides is an internal disk-only
|
|
@@ -1496,7 +1585,9 @@ export class WebServer extends EventEmitter {
|
|
|
1496
1585
|
// Only remove from state.json if we're also killing the mux session.
|
|
1497
1586
|
// When killMux=false (server shutdown), preserve state for recovery.
|
|
1498
1587
|
if (killMux) {
|
|
1499
|
-
|
|
1588
|
+
// The edit history goes with the record; a pinned session's demoted record keeps it.
|
|
1589
|
+
if (this.store.demoteOrRemoveSession(sessionId) !== 'preserved')
|
|
1590
|
+
this.forgetEditHistory(sessionId);
|
|
1500
1591
|
}
|
|
1501
1592
|
}
|
|
1502
1593
|
// Release anything blocked on this session, in the documented order: 'exit'
|
|
@@ -1510,8 +1601,11 @@ export class WebServer extends EventEmitter {
|
|
|
1510
1601
|
// detach: mail posted before that is still there when the session is re-attached
|
|
1511
1602
|
// (while detached, the routes answer 404, so nothing new arrives). Before
|
|
1512
1603
|
// cancelAll, so a reader released by it reads an empty inbox.
|
|
1513
|
-
if (killMux)
|
|
1604
|
+
if (killMux) {
|
|
1514
1605
|
agentInbox.drop(sessionId);
|
|
1606
|
+
// A real delete only: a detach (killMux=false) leaves the session running and re-attachable.
|
|
1607
|
+
void this.discordBridge.sessionDeleted(sessionId).catch(() => undefined);
|
|
1608
|
+
}
|
|
1515
1609
|
sessionWaits.notifySignal(sessionId, 'exit');
|
|
1516
1610
|
sessionWaits.cancelAll(sessionId);
|
|
1517
1611
|
approvalInbox.resolveForSession(sessionId, 'session_ended');
|
|
@@ -2237,9 +2331,25 @@ export class WebServer extends EventEmitter {
|
|
|
2237
2331
|
const lifecycleLog = getLifecycleLog();
|
|
2238
2332
|
for (const s of result.cleaned) {
|
|
2239
2333
|
lifecycleLog.log({ event: 'stale_cleaned', sessionId: s.id, name: s.name });
|
|
2334
|
+
this.forgetEditHistory(s.id);
|
|
2240
2335
|
}
|
|
2336
|
+
// History files no session owns (a crash between removing a record and deleting its file).
|
|
2337
|
+
const exists = (id) => this.sessions.has(id) || !!this.store.getSession(id);
|
|
2338
|
+
await this.editHistorySync.sweep(exists).catch((err) => {
|
|
2339
|
+
console.warn('[EditHistory] Sweeping orphaned histories failed:', getErrorMessage(err));
|
|
2340
|
+
});
|
|
2241
2341
|
return result.count;
|
|
2242
2342
|
}
|
|
2343
|
+
/**
|
|
2344
|
+
* Delete a session's edit history (diffs can hold secrets) once its record is gone: deleted
|
|
2345
|
+
* live (`cleanupSession`), deleted as a record only (DELETE route), or cleaned as stale
|
|
2346
|
+
* (startup, `POST /api/cleanup-state`).
|
|
2347
|
+
*/
|
|
2348
|
+
forgetEditHistory(sessionId) {
|
|
2349
|
+
void this.editHistorySync.remove(sessionId).catch((err) => {
|
|
2350
|
+
console.warn(`[EditHistory] Removing history of ${sessionId} failed:`, getErrorMessage(err));
|
|
2351
|
+
});
|
|
2352
|
+
}
|
|
2243
2353
|
/**
|
|
2244
2354
|
* Get lightweight state for SSE init - excludes full terminal buffers
|
|
2245
2355
|
* to prevent browser freezes. Terminal buffers are fetched on-demand.
|
|
@@ -2340,6 +2450,23 @@ export class WebServer extends EventEmitter {
|
|
|
2340
2450
|
// Multi-user: derive an ownership routing hint so an event only reaches the
|
|
2341
2451
|
// clients entitled to it (no-op in single-user — hint stays undefined).
|
|
2342
2452
|
this.sse.broadcast(event, data, isMultiUserMode() ? this.deriveSseHint(event, data) : undefined);
|
|
2453
|
+
this.bridgeSessionEvent(event, data);
|
|
2454
|
+
}
|
|
2455
|
+
/** Keep the Discord thread in step with the tab: created, renamed, deleted. Never throws. */
|
|
2456
|
+
bridgeSessionEvent(event, data) {
|
|
2457
|
+
// Deletion is NOT handled here: `session:deleted` also fires for a detach (killMux=false), where the
|
|
2458
|
+
// session keeps running and can be re-attached. The real delete calls the bridge from the cleanup path.
|
|
2459
|
+
if (event !== SseEvent.SessionCreated && event !== SseEvent.SessionUpdated)
|
|
2460
|
+
return;
|
|
2461
|
+
const d = (data ?? {});
|
|
2462
|
+
const s = d.session ?? d;
|
|
2463
|
+
if (!s.id)
|
|
2464
|
+
return;
|
|
2465
|
+
const id = s.id;
|
|
2466
|
+
const run = event === SseEvent.SessionCreated
|
|
2467
|
+
? this.discordBridge.sessionCreated(id, s.name).then(() => this.discordBridge.sessionRenamed(id, s.name))
|
|
2468
|
+
: this.discordBridge.sessionRenamed(id, s.name);
|
|
2469
|
+
void run.catch(() => undefined);
|
|
2343
2470
|
}
|
|
2344
2471
|
/**
|
|
2345
2472
|
* Map an SSE event + payload to a routing hint (multi-user). Session-scoped
|
|
@@ -2447,15 +2574,26 @@ export class WebServer extends EventEmitter {
|
|
|
2447
2574
|
const getPaneExit = this.mux.getPaneExit?.bind(this.mux);
|
|
2448
2575
|
if (!getPaneExit)
|
|
2449
2576
|
return;
|
|
2577
|
+
const getPaneCurrentPath = this.mux.getPaneCurrentPath?.bind(this.mux);
|
|
2450
2578
|
for (const session of this.sessions.values()) {
|
|
2451
2579
|
const muxName = session.muxName;
|
|
2452
2580
|
// No pane, so nothing to report — and `setPaneExit()` would force UNKNOWN
|
|
2453
2581
|
// for such a session anyway.
|
|
2454
2582
|
if (!muxName)
|
|
2455
2583
|
continue;
|
|
2456
|
-
|
|
2584
|
+
// The pane's live directory rides the same read: it is the base a relative
|
|
2585
|
+
// path printed in the terminal resolves against. The manager's answer is
|
|
2586
|
+
// applied as given, `undefined` included: it keeps its map across a failed
|
|
2587
|
+
// or empty read and carries a blank `#{pane_current_path}` forward, so an
|
|
2588
|
+
// `undefined` here means an authoritative read listed no single directory
|
|
2589
|
+
// for the session (split into several panes, or gone from the socket).
|
|
2590
|
+
const paneCwdChanged = getPaneCurrentPath ? session.setPaneCwd(getPaneCurrentPath(muxName)) : false;
|
|
2591
|
+
if (session.setPaneExit(getPaneExit(muxName))) {
|
|
2592
|
+
this.persistSessionState(session);
|
|
2593
|
+
}
|
|
2594
|
+
else if (!paneCwdChanged) {
|
|
2457
2595
|
continue;
|
|
2458
|
-
|
|
2596
|
+
}
|
|
2459
2597
|
this.broadcastSessionStateDebounced(session.id);
|
|
2460
2598
|
}
|
|
2461
2599
|
this.closeCleanlyExitedSessions();
|
|
@@ -2566,6 +2704,26 @@ export class WebServer extends EventEmitter {
|
|
|
2566
2704
|
host: this.windowTitle,
|
|
2567
2705
|
})
|
|
2568
2706
|
.catch(() => undefined);
|
|
2707
|
+
// Discord: same event, same independence from Web Push. It adds the tail of what the CLI printed,
|
|
2708
|
+
// so the person reading the thread can see what is being asked without opening the tab.
|
|
2709
|
+
const dcSession = sessionId ? this.sessions.get(sessionId) : undefined;
|
|
2710
|
+
// A question or permission dialog lives on the screen, so it keeps the screen tail (its options are
|
|
2711
|
+
// there); a finished turn posts the CLI's whole last message instead, from its transcript.
|
|
2712
|
+
const dialogEvent = event === SseEvent.HookPermissionPrompt || event === SseEvent.HookElicitationDialog;
|
|
2713
|
+
void this.discordBridge
|
|
2714
|
+
.notify({
|
|
2715
|
+
event,
|
|
2716
|
+
title: template.title,
|
|
2717
|
+
body,
|
|
2718
|
+
urgency: template.urgency,
|
|
2719
|
+
sessionId: sessionId || undefined,
|
|
2720
|
+
sessionName: sessionName || undefined,
|
|
2721
|
+
// A function, so the screen is only read (a synchronous tmux exec) once the bridge has decided to post.
|
|
2722
|
+
tail: dcSession ? () => this.discordTail(dcSession, !dialogEvent) : undefined,
|
|
2723
|
+
fullText: dcSession && !dialogEvent ? () => this.discordFullText(dcSession) : undefined,
|
|
2724
|
+
host: this.windowTitle,
|
|
2725
|
+
})
|
|
2726
|
+
.catch(() => undefined);
|
|
2569
2727
|
const subscriptions = this.pushStore.getAll();
|
|
2570
2728
|
if (subscriptions.length === 0)
|
|
2571
2729
|
return;
|
|
@@ -2640,6 +2798,33 @@ export class WebServer extends EventEmitter {
|
|
|
2640
2798
|
});
|
|
2641
2799
|
}
|
|
2642
2800
|
}
|
|
2801
|
+
/**
|
|
2802
|
+
* What the person would see on the tab right now, for a Discord alert: the rendered tmux screen
|
|
2803
|
+
* (`capture-pane -p`), never the raw output stream, which a full-screen UI repaints in place and
|
|
2804
|
+
* reads as garbage. A session with no pane falls back to its plain text output, else nothing.
|
|
2805
|
+
*/
|
|
2806
|
+
discordTail(session, cutComposer) {
|
|
2807
|
+
const pane = session.muxName ? (this.mux.capturePaneText?.(session.muxName) ?? null) : null;
|
|
2808
|
+
// The composer row and the status lines under it are screen chrome, not what the CLI said.
|
|
2809
|
+
const glyph = cutComposer ? getCli(session.mode)?.capabilities.workDetect?.promptGlyph : undefined;
|
|
2810
|
+
return screenTail(pane ?? session.textOutput ?? '', 14, 900, glyph);
|
|
2811
|
+
}
|
|
2812
|
+
/**
|
|
2813
|
+
* The CLI's whole last message from the response viewer's reader (the CLI's own transcript, or the pane
|
|
2814
|
+
* segmented into turns for the CLIs that keep none); undefined for a shell, which has no turns, and the
|
|
2815
|
+
* screen tail stands in whenever the reader finds nothing.
|
|
2816
|
+
*/
|
|
2817
|
+
async discordFullText(session) {
|
|
2818
|
+
if (!this.lastResponseFor)
|
|
2819
|
+
return undefined;
|
|
2820
|
+
// A CLI with a transcript reader, or whose pane the viewer segments into turns; a shell has neither.
|
|
2821
|
+
if (getCli(session.mode)?.capabilities.transcript === 'none' && !isExternalCliTranscriptMode(session.mode)) {
|
|
2822
|
+
return undefined;
|
|
2823
|
+
}
|
|
2824
|
+
const r = await this.lastResponseFor(session);
|
|
2825
|
+
// The turn it came from: the transcript's stamp, else when the person last submitted (a pane has no stamp).
|
|
2826
|
+
return { text: r.text, version: r.timestamp || String(session.lastSubmitAt ?? '') };
|
|
2827
|
+
}
|
|
2643
2828
|
/** The notification body for an event (shared by Web Push and the webhook channel). */
|
|
2644
2829
|
static pushBodyText(event, data, sessionName) {
|
|
2645
2830
|
let body = sessionName ? `[${sessionName}]` : '';
|
|
@@ -2779,6 +2964,21 @@ export class WebServer extends EventEmitter {
|
|
|
2779
2964
|
console.log(`[AgentInbox] restored ${restoredMail} pending message(s)`);
|
|
2780
2965
|
const restored = await this.restoreMuxSessions();
|
|
2781
2966
|
await this.finalizeRestoredState(restored);
|
|
2967
|
+
void this.discordBridge
|
|
2968
|
+
.release()
|
|
2969
|
+
.then(async () => {
|
|
2970
|
+
// Only after a successful restore, like the agent inbox prune and the tab layout reconciliation, and
|
|
2971
|
+
// with the same keep set: a reboot-restore rebuild reuses the old id.
|
|
2972
|
+
if (!restored)
|
|
2973
|
+
return;
|
|
2974
|
+
await this.discordBridge.prune([
|
|
2975
|
+
...this.sessions.keys(),
|
|
2976
|
+
...Object.keys(this.store.getSessions()),
|
|
2977
|
+
...rebootRestoreRegistry.list(() => true).map((entry) => entry.sessionId),
|
|
2978
|
+
]);
|
|
2979
|
+
})
|
|
2980
|
+
.then(() => this.discordBridge.startStaleSweep())
|
|
2981
|
+
.catch(() => undefined);
|
|
2782
2982
|
// Instance-scoped reaper: after restore, `docker rm -f` managed containers of
|
|
2783
2983
|
// THIS instance whose case is gone from docker-cases.json (best-effort, never
|
|
2784
2984
|
// touches another instance's containers). Runs after restore so containers
|
|
@@ -3155,7 +3355,7 @@ export class WebServer extends EventEmitter {
|
|
|
3155
3355
|
// bookkeeping survives a restart and the values are re-derived here.
|
|
3156
3356
|
const savedCustomModel = saved.__customModel;
|
|
3157
3357
|
if (savedCustomModel) {
|
|
3158
|
-
|
|
3358
|
+
await this._restoreCustomModel(session, savedCustomModel);
|
|
3159
3359
|
}
|
|
3160
3360
|
if (saved.niceEnabled !== undefined || saved.niceValue !== undefined) {
|
|
3161
3361
|
session.setNice({ enabled: saved.niceEnabled, niceValue: saved.niceValue });
|
|
@@ -3293,6 +3493,9 @@ export class WebServer extends EventEmitter {
|
|
|
3293
3493
|
catch (err) {
|
|
3294
3494
|
console.warn(`[Server] releasing the tab layout slot failed: ${getErrorMessage(err)}`);
|
|
3295
3495
|
}
|
|
3496
|
+
// Per-tab bridge state only: a reboot-restore rebuild reuses the old id and stays on offer for a
|
|
3497
|
+
// retry, so its thread must survive the discard.
|
|
3498
|
+
this.discordBridge.forgetTab(sessionId);
|
|
3296
3499
|
// Any `session:updated` the half-built session emitted before it failed left a
|
|
3297
3500
|
// tab on every other open board, and the client's handler is an upsert.
|
|
3298
3501
|
this.broadcast(SseEvent.SessionDeleted, { id: sessionId });
|
|
@@ -3451,7 +3654,7 @@ export class WebServer extends EventEmitter {
|
|
|
3451
3654
|
// the isolated config dir; an endpoint that has since been deleted still gets
|
|
3452
3655
|
// the bookkeeping restored, which is what a later clear needs to unset.
|
|
3453
3656
|
if (savedCustomModel) {
|
|
3454
|
-
|
|
3657
|
+
await this._restoreCustomModel(session, savedCustomModel);
|
|
3455
3658
|
}
|
|
3456
3659
|
// Update session name if it was a "Restored:" placeholder or doesn't match saved name
|
|
3457
3660
|
if (savedState?.name && muxSession.name !== savedState.name) {
|
|
@@ -3820,6 +4023,8 @@ export class WebServer extends EventEmitter {
|
|
|
3820
4023
|
// Same teardown rule: the per-endpoint llama-swap log tails are otherwise closed
|
|
3821
4024
|
// only by the periodic idle sweep, whose interval is disposed just below.
|
|
3822
4025
|
closeAllLlamaSwapLogTails();
|
|
4026
|
+
// Close the Discord gateway socket and its heartbeat, and clear the bridge's per-tab maps.
|
|
4027
|
+
this.discordBridge.stop();
|
|
3823
4028
|
// Dispose all managed timers (intervals + resettable timeouts)
|
|
3824
4029
|
this.cleanup.dispose();
|
|
3825
4030
|
// Gracefully close all SSE connections and clear batching state
|
|
@@ -3929,6 +4134,9 @@ export class WebServer extends EventEmitter {
|
|
|
3929
4134
|
this.respawnTimers.clear();
|
|
3930
4135
|
this.runSummaryTrackers.clear();
|
|
3931
4136
|
this.transcriptWatchers.clear();
|
|
4137
|
+
await this.editHistorySync.flushAll().catch((err) => {
|
|
4138
|
+
console.warn('[EditHistory] Saving on shutdown failed:', getErrorMessage(err));
|
|
4139
|
+
});
|
|
3932
4140
|
this.sessionListenerRefs.clear();
|
|
3933
4141
|
this.scheduledRuns.clear();
|
|
3934
4142
|
// Dispose StaleExpirationMaps (stops internal cleanup timers)
|