@pellux/goodvibes-tui 1.27.0 → 2.0.1
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/CHANGELOG.md +125 -2
- package/README.md +7 -9
- package/bin/goodvibes +2 -2
- package/bin/launcher-support.js +8 -4
- package/docs/README.md +11 -2
- package/docs/foundation-artifacts/operator-contract.json +6403 -2347
- package/package.json +14 -25
- package/scripts/postinstall.js +35 -35
- package/src/audio/spoken-turn-model-routing.ts +1 -1
- package/src/cli/bundle-command.ts +5 -5
- package/src/cli/completions/generate.ts +1 -1
- package/src/cli/doctor.ts +3 -3
- package/src/cli/entrypoint.ts +12 -7
- package/src/cli/help.ts +4 -5
- package/src/cli/hooks-command.ts +1 -1
- package/src/cli/index.ts +1 -4
- package/src/cli/launch-auto-update.ts +59 -7
- package/src/cli/management-commands.ts +6 -6
- package/src/cli/management-utils.ts +5 -5
- package/src/cli/management.ts +7 -7
- package/src/cli/network-posture.ts +1 -1
- package/src/cli/package-verification.ts +7 -6
- package/src/cli/plugin-bundles-command.ts +1 -1
- package/src/cli/plugin-command.ts +2 -2
- package/src/cli/service-command.ts +2 -2
- package/src/cli/service-posture.ts +4 -4
- package/src/cli/status.ts +9 -8
- package/src/cli/surface-command.ts +4 -4
- package/src/cli/tui-startup.ts +12 -3
- package/src/cli-flags.ts +3 -3
- package/src/config/goodvibes-home-audit.ts +0 -2
- package/src/config/secret-config.ts +37 -5
- package/src/config/tui-extension-settings.ts +1 -1
- package/src/core/approval-alert.ts +2 -2
- package/src/core/budget-breach-notifier.ts +3 -3
- package/src/core/conversation-compaction-render.ts +1 -1
- package/src/core/conversation-line-cache.ts +1 -1
- package/src/core/conversation-render-context.ts +1 -1
- package/src/core/conversation-rendering.ts +2 -2
- package/src/core/conversation.ts +10 -10
- package/src/core/long-task-notifier.ts +2 -2
- package/src/core/memory-diagnostics-gateway.ts +1 -1
- package/src/core/scriptable-statusline.ts +1 -1
- package/src/core/session-auto-titler.ts +1 -1
- package/src/core/session-resume-core.ts +2 -2
- package/src/core/stream-event-wiring.ts +24 -44
- package/src/core/terminal-notifier.ts +3 -3
- package/src/core/turn-cancellation.ts +1 -1
- package/src/core/turn-event-wiring.ts +5 -5
- package/src/input/command-registry.ts +6 -6
- package/src/input/commands/channel-pairing.ts +1 -1
- package/src/input/commands/cluster-runtime.ts +1 -1
- package/src/input/commands/codebase-runtime.ts +1 -1
- package/src/input/commands/devices-runtime.ts +1 -1
- package/src/input/commands/hosted-runtime.ts +330 -0
- package/src/input/commands/local-setup-review.ts +1 -1
- package/src/input/commands/local-setup-transfer.ts +2 -2
- package/src/input/commands/local-setup.ts +3 -3
- package/src/input/commands/managed-runtime.ts +2 -2
- package/src/input/commands/operator-rpc.ts +24 -110
- package/src/input/commands/payment-card-intake.ts +1 -1
- package/src/input/commands/permissions-provenance.ts +2 -2
- package/src/input/commands/plugin-runtime.ts +1 -1
- package/src/input/commands/provider.ts +1 -1
- package/src/input/commands/relay-runtime.ts +18 -9
- package/src/input/commands/rewind-runtime.ts +1 -1
- package/src/input/commands/runtime-services.ts +1 -1
- package/src/input/commands/services-runtime.ts +1 -1
- package/src/input/commands/session-workflow.ts +1 -1
- package/src/input/commands/settings-sync-runtime.ts +2 -2
- package/src/input/commands/share-runtime.ts +1 -1
- package/src/input/commands/tasks-runtime.ts +145 -55
- package/src/input/commands/update-runtime.ts +31 -28
- package/src/input/commands/work-plan-runtime.ts +2 -2
- package/src/input/commands/workstream-attempts.ts +1 -1
- package/src/input/commands/workstream-runtime.ts +2 -2
- package/src/input/commands.ts +2 -0
- package/src/input/feature-unit-layout.ts +1 -1
- package/src/input/feed-context-factory.ts +3 -3
- package/src/input/handler-content-actions.ts +2 -2
- package/src/input/handler-feed-routes.ts +1 -1
- package/src/input/handler-feed.ts +3 -3
- package/src/input/handler-modal-token-routes.ts +1 -1
- package/src/input/handler-onboarding-daemon-adopt.ts +3 -2
- package/src/input/handler-onboarding.ts +1 -1
- package/src/input/handler-types.ts +2 -2
- package/src/input/handler-ui-state.ts +1 -1
- package/src/input/handler.ts +4 -4
- package/src/input/input-history.ts +1 -1
- package/src/input/onboarding/onboarding-feature-units.ts +2 -2
- package/src/input/onboarding/onboarding-runtime-status.ts +15 -1
- package/src/input/onboarding/onboarding-wizard-apply.ts +17 -8
- package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +5 -3
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +1 -1
- package/src/input/onboarding/onboarding-wizard-types.ts +1 -1
- package/src/input/payments-config.ts +6 -6
- package/src/input/search.ts +1 -1
- package/src/input/session-picker-modal.ts +46 -0
- package/src/input/settings-modal-activation.ts +1 -1
- package/src/input/settings-modal-behavior.ts +4 -4
- package/src/input/settings-modal-commit.ts +11 -0
- package/src/input/settings-modal-data.ts +13 -80
- package/src/input/settings-modal-mutations.ts +33 -0
- package/src/input/settings-modal-secrets.ts +34 -0
- package/src/input/settings-modal-subscriptions.ts +1 -1
- package/src/input/settings-modal-types.ts +15 -1
- package/src/input/settings-modal.ts +28 -3
- package/src/input/worktree-setup-config.ts +1 -1
- package/src/main.ts +11 -9
- package/src/panels/agent-inspector-shared.ts +2 -2
- package/src/panels/base-panel.ts +2 -2
- package/src/panels/builtin/operations.ts +6 -8
- package/src/panels/builtin/session.ts +17 -0
- package/src/panels/builtin/shared.ts +5 -5
- package/src/panels/builtin-modals.ts +3 -3
- package/src/panels/confirm-state.ts +1 -1
- package/src/panels/cost-tracker-panel.ts +4 -4
- package/src/panels/diff-panel.ts +2 -2
- package/src/panels/diff-review-panel.ts +2 -2
- package/src/panels/expandable-list-panel.ts +2 -2
- package/src/panels/fleet-acts.ts +1 -1
- package/src/panels/fleet-observed-render.ts +1 -1
- package/src/panels/fleet-panel-format.ts +1 -1
- package/src/panels/fleet-panel.ts +1 -1
- package/src/panels/fleet-spawn.ts +1 -1
- package/src/panels/fleet-steer.ts +1 -1
- package/src/panels/fleet-transcript.ts +1 -1
- package/src/panels/git-panel.ts +1 -1
- package/src/panels/hosted-session-feed.ts +352 -0
- package/src/panels/hosted-session-panel.ts +221 -0
- package/src/panels/local-auth-panel.ts +3 -3
- package/src/panels/modals/devices-modal.ts +1 -1
- package/src/panels/modals/local-auth-modal.ts +1 -1
- package/src/panels/modals/pairing-modal.ts +1 -1
- package/src/panels/modals/services-modal.ts +1 -1
- package/src/panels/modals/settings-sync-modal.ts +1 -1
- package/src/panels/modals/subscription-modal.ts +1 -1
- package/src/panels/modals/work-plan-modal.ts +1 -1
- package/src/panels/notifications-panel.ts +1 -1
- package/src/panels/panel-confirm-overlay.ts +1 -1
- package/src/panels/plugins-panel.ts +2 -2
- package/src/panels/polish-core.ts +2 -2
- package/src/panels/polish-tables.ts +1 -1
- package/src/panels/polish.ts +2 -2
- package/src/panels/scrollable-list-panel.ts +3 -3
- package/src/panels/skills-panel.ts +3 -3
- package/src/panels/token-budget-panel.ts +5 -5
- package/src/panels/types.ts +1 -2
- package/src/permissions/prompt.ts +1 -1
- package/src/renderer/autocomplete-overlay.ts +1 -1
- package/src/renderer/block-actions-overlay.ts +1 -1
- package/src/renderer/bookmark-modal.ts +1 -1
- package/src/renderer/buffer.ts +1 -1
- package/src/renderer/code-block.ts +1 -1
- package/src/renderer/composer-fragments.ts +1 -1
- package/src/renderer/compositor.ts +2 -2
- package/src/renderer/config-modal.ts +1 -1
- package/src/renderer/context-inspector.ts +1 -1
- package/src/renderer/conversation-layout.ts +2 -2
- package/src/renderer/conversation-overlays.ts +1 -1
- package/src/renderer/conversation-surface.ts +2 -2
- package/src/renderer/diff-view.ts +1 -1
- package/src/renderer/diff.ts +1 -1
- package/src/renderer/file-picker-overlay.ts +1 -1
- package/src/renderer/fleet-tab-strip.ts +1 -1
- package/src/renderer/fullscreen-primitives.ts +2 -2
- package/src/renderer/fullscreen-workspace.ts +1 -1
- package/src/renderer/header-line.ts +1 -1
- package/src/renderer/help-overlay.ts +1 -1
- package/src/renderer/history-search-overlay.ts +2 -2
- package/src/renderer/layout.ts +5 -8
- package/src/renderer/markdown-inline.ts +1 -1
- package/src/renderer/markdown-table.ts +1 -1
- package/src/renderer/markdown.ts +1 -1
- package/src/renderer/mcp-workspace.ts +1 -1
- package/src/renderer/modal-factory.ts +2 -2
- package/src/renderer/model-workspace.ts +2 -2
- package/src/renderer/onboarding/onboarding-wizard.ts +1 -1
- package/src/renderer/overlay-box.ts +1 -1
- package/src/renderer/panel-composite.ts +1 -1
- package/src/renderer/panel-workspace-bar.ts +1 -1
- package/src/renderer/process-indicator.ts +1 -1
- package/src/renderer/profile-picker-modal.ts +1 -1
- package/src/renderer/search-overlay.ts +2 -2
- package/src/renderer/selection-modal-overlay.ts +1 -1
- package/src/renderer/session-picker-modal.ts +124 -3
- package/src/renderer/settings-modal-helpers.ts +2 -0
- package/src/renderer/settings-modal.ts +2 -2
- package/src/renderer/shell-surface.ts +2 -2
- package/src/renderer/system-message.ts +1 -1
- package/src/renderer/tab-strip.ts +1 -1
- package/src/renderer/theme-mode-config.ts +1 -1
- package/src/renderer/theme.ts +11 -12
- package/src/renderer/thinking.ts +1 -1
- package/src/renderer/tool-call.ts +2 -2
- package/src/renderer/tool-result-expanded-lines.ts +1 -1
- package/src/renderer/turn-injection.ts +7 -8
- package/src/renderer/ui-factory.ts +3 -3
- package/src/runtime/bootstrap-command-context.ts +4 -4
- package/src/runtime/bootstrap-command-parts.ts +5 -5
- package/src/runtime/bootstrap-core.ts +30 -30
- package/src/runtime/bootstrap-shell.ts +4 -4
- package/src/runtime/bootstrap.ts +128 -157
- package/src/runtime/client/approval-updates.ts +82 -0
- package/src/runtime/client/build-floors.ts +167 -0
- package/src/runtime/client/daemon-attach-handshake.ts +115 -0
- package/src/runtime/client/fleet-union.ts +147 -0
- package/src/runtime/client/host-service-status.ts +71 -0
- package/src/runtime/client/hosted-exit.ts +86 -0
- package/src/runtime/client/hosted-roster.ts +110 -0
- package/src/runtime/client/hosted-session-stream.ts +178 -0
- package/src/runtime/client/hosted-sessions.ts +187 -0
- package/src/runtime/client/operator-endpoint.ts +213 -0
- package/src/runtime/client/spine-adoption.ts +99 -0
- package/src/runtime/cloudflare-control-plane.ts +1 -1
- package/src/runtime/context-accounting-source.ts +1 -1
- package/src/runtime/context.ts +2 -2
- package/src/runtime/conversation-rewind-port.ts +42 -14
- package/src/runtime/daemon-attach-notices.ts +55 -19
- package/src/runtime/diagnostics/panels/ops.ts +1 -1
- package/src/runtime/disposal-wiring.ts +4 -24
- package/src/runtime/index.ts +119 -105
- package/src/runtime/install-self-check-startup.ts +2 -2
- package/src/runtime/interaction-seams.ts +1 -1
- package/src/runtime/knowledge-services.ts +8 -2
- package/src/runtime/legacy-daemon-migration.ts +15 -3
- package/src/runtime/legacy-daemon-reconcile.ts +3 -2
- package/src/runtime/memory-spine-transport.ts +37 -267
- package/src/runtime/notification-dispatch.ts +2 -2
- package/src/runtime/onboarding/apply.ts +2 -2
- package/src/runtime/onboarding/derivation.ts +2 -2
- package/src/runtime/onboarding/markers.ts +2 -2
- package/src/runtime/onboarding/progress.ts +2 -2
- package/src/runtime/onboarding/snapshot.ts +1 -1
- package/src/runtime/onboarding/state.ts +2 -2
- package/src/runtime/onboarding/types.ts +2 -2
- package/src/runtime/orchestrator-core-services.ts +1 -1
- package/src/runtime/path-shadow-startup.ts +28 -221
- package/src/runtime/power-keepawake-remote.ts +3 -2
- package/src/runtime/process-lifecycle.ts +2 -2
- package/src/runtime/provider-fallback.ts +10 -66
- package/src/runtime/recovery-autosave.ts +1 -1
- package/src/runtime/recovery-offer-wiring.ts +39 -23
- package/src/runtime/recovery-prompt.ts +31 -17
- package/src/runtime/relay-reachability-bridge.ts +53 -42
- package/src/runtime/release-artifacts.ts +2 -1
- package/src/runtime/runtime-services-types.ts +82 -35
- package/src/runtime/services.ts +305 -300
- package/src/runtime/session-resume-liveness-confirm.ts +1 -1
- package/src/runtime/ui-services.ts +7 -6
- package/src/runtime/update-check.ts +23 -43
- package/src/runtime/workspace-checkpointing.ts +1 -1
- package/src/runtime/wrfc-persistence.ts +2 -2
- package/src/shell/ui-openers.ts +30 -7
- package/src/utils/clipboard.ts +1 -1
- package/src/verification/verification-ledger.ts +10 -8
- package/src/version.ts +1 -1
- package/bin/goodvibes-daemon +0 -90
- package/src/cli/config-overrides.ts +0 -267
- package/src/cli/endpoints.ts +0 -109
- package/src/cli/parser.ts +0 -395
- package/src/cli/redaction.ts +0 -170
- package/src/cli/types.ts +0 -84
- package/src/cluster/commands.ts +0 -382
- package/src/cluster/remote-daemon-target.ts +0 -225
- package/src/cluster/render.ts +0 -268
- package/src/config/atomic-write.ts +0 -70
- package/src/config/credential-availability.ts +0 -122
- package/src/config/goodvibes-home.ts +0 -149
- package/src/config/index.ts +0 -88
- package/src/config/provider-model.ts +0 -23
- package/src/config/read-versioned.ts +0 -228
- package/src/core/alert-gating.ts +0 -67
- package/src/core/consolidation-receipt.ts +0 -41
- package/src/core/focus-tracker.ts +0 -41
- package/src/core/history.ts +0 -45
- package/src/core/memory-status.ts +0 -118
- package/src/core/orchestrator.ts +0 -7
- package/src/core/pairing-devices.ts +0 -49
- package/src/core/pairing-handoff.ts +0 -101
- package/src/core/pairing-offers.ts +0 -83
- package/src/core/pairing-origin.ts +0 -78
- package/src/core/rewind-turn-anchors.ts +0 -311
- package/src/core/session-recovery.ts +0 -210
- package/src/core/stable-host.ts +0 -171
- package/src/core/transcript-journal.ts +0 -503
- package/src/daemon/cli.ts +0 -641
- package/src/daemon/handlers/context.ts +0 -29
- package/src/daemon/handlers/contracts.ts +0 -77
- package/src/daemon/handlers/credentials.ts +0 -139
- package/src/daemon/handlers/drafts/draft-store.ts +0 -427
- package/src/daemon/handlers/drafts/index.ts +0 -17
- package/src/daemon/handlers/drafts/register.ts +0 -331
- package/src/daemon/handlers/errors.ts +0 -18
- package/src/daemon/handlers/inbox/cursor-store.ts +0 -457
- package/src/daemon/handlers/inbox/index.ts +0 -284
- package/src/daemon/handlers/inbox/mapping.ts +0 -192
- package/src/daemon/handlers/inbox/poller.ts +0 -212
- package/src/daemon/handlers/inbox/provider-adapter.ts +0 -156
- package/src/daemon/handlers/inbox/providers/discord.ts +0 -252
- package/src/daemon/handlers/inbox/providers/email.ts +0 -152
- package/src/daemon/handlers/inbox/providers/imap-client.ts +0 -300
- package/src/daemon/handlers/inbox/providers/route-util.ts +0 -24
- package/src/daemon/handlers/inbox/providers/slack.ts +0 -263
- package/src/daemon/handlers/index.ts +0 -117
- package/src/daemon/handlers/register.ts +0 -168
- package/src/daemon/handlers/remote/backends/cloud-terminal.ts +0 -142
- package/src/daemon/handlers/remote/backends/docker.ts +0 -79
- package/src/daemon/handlers/remote/backends/index.ts +0 -40
- package/src/daemon/handlers/remote/backends/local-process.ts +0 -113
- package/src/daemon/handlers/remote/backends/process-runner.ts +0 -127
- package/src/daemon/handlers/remote/backends/ssh.ts +0 -125
- package/src/daemon/handlers/remote/backends/types.ts +0 -97
- package/src/daemon/handlers/remote/dispatcher.ts +0 -181
- package/src/daemon/handlers/remote/index.ts +0 -119
- package/src/daemon/handlers/remote/peer-registry.ts +0 -357
- package/src/daemon/handlers/remote/service.ts +0 -191
- package/src/daemon/handlers/routing/inbox-bridge.ts +0 -71
- package/src/daemon/handlers/routing/index.ts +0 -261
- package/src/daemon/handlers/routing/route-store.ts +0 -319
- package/src/daemon/handlers/routing/routing-resolver.ts +0 -75
- package/src/daemon/handlers/sqlite-store.ts +0 -278
- package/src/daemon/handlers/triage/index.ts +0 -57
- package/src/daemon/handlers/triage/integration.ts +0 -213
- package/src/daemon/handlers/triage/pipeline.ts +0 -274
- package/src/daemon/handlers/triage/scorer.ts +0 -287
- package/src/daemon/handlers/triage/tagger/discord.ts +0 -187
- package/src/daemon/handlers/triage/tagger/imap.ts +0 -384
- package/src/daemon/handlers/triage/tagger/index.ts +0 -184
- package/src/daemon/handlers/triage/tagger/shared.ts +0 -70
- package/src/daemon/handlers/triage/tagger/slack.ts +0 -69
- package/src/daemon/handlers/triage/types.ts +0 -50
- package/src/daemon/lifecycle.ts +0 -41
- package/src/daemon/provision-wake-model.ts +0 -81
- package/src/daemon/safe-serve.ts +0 -61
- package/src/daemon/send/channels.ts +0 -200
- package/src/daemon/send/command.ts +0 -332
- package/src/daemon/send/composition.ts +0 -99
- package/src/daemon/send/failure-text.ts +0 -93
- package/src/daemon/send/inert-text.ts +0 -225
- package/src/daemon/send/stdin.ts +0 -24
- package/src/daemon/service-commands.ts +0 -329
- package/src/export/cost-utils.ts +0 -319
- package/src/input/selection.ts +0 -135
- package/src/plugins/loader.ts +0 -15
- package/src/renderer/bottom-bar.ts +0 -58
- package/src/renderer/conversation-tree.ts +0 -228
- package/src/runtime/agent-graph-composition.ts +0 -78
- package/src/runtime/channel-composition.ts +0 -46
- package/src/runtime/cluster-composition.ts +0 -124
- package/src/runtime/cluster-group-composition.ts +0 -284
- package/src/runtime/code-index-services.ts +0 -167
- package/src/runtime/daemon-handler-composition.ts +0 -76
- package/src/runtime/device-posture-composition.ts +0 -110
- package/src/runtime/durability-housekeeping.ts +0 -183
- package/src/runtime/durability-services.ts +0 -155
- package/src/runtime/embedded-service-factories.ts +0 -58
- package/src/runtime/feature-settings.ts +0 -154
- package/src/runtime/fleet-needs-input-push.ts +0 -70
- package/src/runtime/fleet-services.ts +0 -41
- package/src/runtime/idle-power-services.ts +0 -85
- package/src/runtime/mail-composition.ts +0 -65
- package/src/runtime/memory-fold.ts +0 -55
- package/src/runtime/operator-token-cleanup.ts +0 -109
- package/src/runtime/path-shadow.ts +0 -418
- package/src/runtime/perf/panel-health-monitor.ts +0 -18
- package/src/runtime/provider-optimizer-wiring.ts +0 -55
- package/src/runtime/reachability-notice.ts +0 -100
- package/src/runtime/remote-execution-composition.ts +0 -62
- package/src/runtime/session-liveness-marker.ts +0 -257
- package/src/runtime/session-pointer-surface.ts +0 -22
- package/src/runtime/session-storage-services.ts +0 -41
- package/src/runtime/surface-feature-flags.ts +0 -96
- package/src/runtime/terminal-output-guard.ts +0 -235
- package/src/runtime/trust/workspace-registration.ts +0 -192
- package/src/runtime/trust/workspace-trust.ts +0 -229
- package/src/runtime/ui/index.ts +0 -39
- package/src/runtime/ui/model-picker/data-provider.ts +0 -182
- package/src/runtime/ui/model-picker/health-enrichment.ts +0 -231
- package/src/runtime/ui/model-picker/index.ts +0 -59
- package/src/runtime/ui/model-picker/types.ts +0 -153
- package/src/runtime/ui/provider-health/data-provider.ts +0 -244
- package/src/runtime/ui/provider-health/fallback-visualizer.ts +0 -71
- package/src/runtime/ui/provider-health/index.ts +0 -46
- package/src/runtime/ui/provider-health/types.ts +0 -146
- package/src/runtime/ui-events.ts +0 -1
- package/src/runtime/ui-read-model-helpers.ts +0 -1
- package/src/runtime/ui-read-models-observability-maintenance.ts +0 -1
- package/src/runtime/ui-read-models-observability-options.ts +0 -1
- package/src/runtime/ui-read-models-observability-remote.ts +0 -1
- package/src/runtime/ui-read-models-observability-security.ts +0 -1
- package/src/runtime/ui-read-models-observability-system.ts +0 -1
- package/src/runtime/ui-read-models-observability.ts +0 -1
- package/src/runtime/ui-service-queries.ts +0 -1
- package/src/runtime/voice-setup-services.ts +0 -98
- package/src/runtime/workstream-attempts-validation.ts +0 -77
- package/src/runtime/workstream-draft-edits.ts +0 -95
- package/src/runtime/workstream-draft-store.ts +0 -274
- package/src/runtime/workstream-draft-types.ts +0 -42
- package/src/runtime/workstream-services.ts +0 -373
- package/src/tools/index.ts +0 -1
- package/src/types/grid.ts +0 -48
- package/src/types/sql-js.d.ts +0 -15
- package/src/work-plans/work-plan-store.ts +0 -702
|
@@ -1,503 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* transcript-journal.ts — WAL-style append-only transcript journal.
|
|
3
|
-
*
|
|
4
|
-
* Purpose
|
|
5
|
-
* ───────
|
|
6
|
-
* Between full snapshots (written by persistConversation / writeRecoveryFile),
|
|
7
|
-
* a SIGKILL loses every conversation turn since the last snapshot. This module
|
|
8
|
-
* provides an append-only journal that records each durable conversation event
|
|
9
|
-
* (user message submitted, assistant turn finalised, tool results appended,
|
|
10
|
-
* compaction performed) so that a kill at any moment loses at most the
|
|
11
|
-
* in-flight append — never a full turn.
|
|
12
|
-
*
|
|
13
|
-
* File format (NDJSON)
|
|
14
|
-
* ────────────────────
|
|
15
|
-
* Line 0 — header: { version: 1, sessionId: "...", createdAt: <epochMs> }
|
|
16
|
-
* Line 1+ — records: { type, seq, ts, messages: ConversationMessageSnapshot[] }
|
|
17
|
-
*
|
|
18
|
-
* The header carries the schemaVersion so that a reader from a future process
|
|
19
|
-
* can gate on it (readVersioned convention: unknown version → quarantine).
|
|
20
|
-
*
|
|
21
|
-
* Durability / performance tradeoff
|
|
22
|
-
* ──────────────────────────────────
|
|
23
|
-
* appendRecord() performs one appendFileSync + one fsyncSync per call.
|
|
24
|
-
* This means one fsync per durable conversation event (user message,
|
|
25
|
-
* assistant turn, tool result batch, compaction). It does NOT fsync per
|
|
26
|
-
* streaming token — the streaming path never calls appendRecord().
|
|
27
|
-
*
|
|
28
|
-
* At typical usage (a few events per user turn), this is 2–6 fsyncs/min,
|
|
29
|
-
* well within the durability/throughput envelope of any modern filesystem.
|
|
30
|
-
* The tradeoff is explicit: we accept per-event write amplification in
|
|
31
|
-
* exchange for at-most-one-record loss on SIGKILL.
|
|
32
|
-
*
|
|
33
|
-
* Recovery semantics
|
|
34
|
-
* ──────────────────
|
|
35
|
-
* 1. Read the header line. Gate on version — quarantine if unrecognised.
|
|
36
|
-
* 2. Read subsequent lines until EOF. Stop at the first line that is not
|
|
37
|
-
* valid JSON or lacks the expected shape. Quarantine the remainder of
|
|
38
|
-
* the file from that point onward (rename to .unrecognized). Never crash.
|
|
39
|
-
* 3. Return only records whose `ts` is strictly greater than the provided
|
|
40
|
-
* `snapshotTimestamp` (i.e. events that occurred after the last snapshot).
|
|
41
|
-
* 4. Caller replays the returned records in `seq` order atop the snapshot
|
|
42
|
-
* to reconstruct the conversation, then writes a fresh snapshot and
|
|
43
|
-
* calls `journal.rotate()` to truncate the journal.
|
|
44
|
-
*
|
|
45
|
-
* Rotation
|
|
46
|
-
* ────────
|
|
47
|
-
* After a fresh snapshot is written, call journal.rotate() which deletes the
|
|
48
|
-
* journal file. The next append will recreate it with a fresh header.
|
|
49
|
-
*
|
|
50
|
-
* Journal path convention
|
|
51
|
-
* ───────────────────────
|
|
52
|
-
* <homeDirectory>/.goodvibes/tui/transcript-<sessionId>.journal
|
|
53
|
-
* This mirrors the recovery-file location (homeDirectory-scoped, not
|
|
54
|
-
* workingDir-scoped) so all per-session durability artefacts live together.
|
|
55
|
-
*/
|
|
56
|
-
|
|
57
|
-
import {
|
|
58
|
-
appendFileSync,
|
|
59
|
-
closeSync,
|
|
60
|
-
existsSync,
|
|
61
|
-
fsyncSync,
|
|
62
|
-
mkdirSync,
|
|
63
|
-
openSync,
|
|
64
|
-
readFileSync,
|
|
65
|
-
readdirSync,
|
|
66
|
-
renameSync,
|
|
67
|
-
statSync,
|
|
68
|
-
unlinkSync,
|
|
69
|
-
} from 'node:fs';
|
|
70
|
-
import { dirname, join } from 'node:path';
|
|
71
|
-
import type { ConversationMessageSnapshot } from '@pellux/goodvibes-sdk/platform/core';
|
|
72
|
-
import { UNRECOGNIZED_SUFFIX } from '@/config/read-versioned.ts';
|
|
73
|
-
import type { SessionSurface } from '@/runtime/index.ts';
|
|
74
|
-
|
|
75
|
-
// ─── Constants ──────────────────────────────────────────────────────────────
|
|
76
|
-
|
|
77
|
-
export const JOURNAL_SCHEMA_VERSION = 1;
|
|
78
|
-
|
|
79
|
-
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
80
|
-
|
|
81
|
-
export interface JournalHeader {
|
|
82
|
-
readonly version: typeof JOURNAL_SCHEMA_VERSION;
|
|
83
|
-
readonly sessionId: string;
|
|
84
|
-
readonly createdAt: number;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export type JournalEventType =
|
|
88
|
-
| 'user_message'
|
|
89
|
-
| 'assistant_turn'
|
|
90
|
-
| 'tool_results'
|
|
91
|
-
| 'compaction';
|
|
92
|
-
|
|
93
|
-
export interface JournalRecord {
|
|
94
|
-
/** Discriminator for the kind of durable event. */
|
|
95
|
-
readonly type: JournalEventType;
|
|
96
|
-
/** Monotonically increasing sequence number (0-based, per journal file). */
|
|
97
|
-
readonly seq: number;
|
|
98
|
-
/** Wall-clock timestamp (Date.now()) when the record was appended. */
|
|
99
|
-
readonly ts: number;
|
|
100
|
-
/** Full conversation message snapshot at the time of the event. */
|
|
101
|
-
readonly messages: ConversationMessageSnapshot[];
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export interface ReplayResult {
|
|
105
|
-
/** Records whose ts is strictly after snapshotTimestamp, in seq order. */
|
|
106
|
-
readonly records: JournalRecord[];
|
|
107
|
-
/**
|
|
108
|
-
* True if the journal tail was corrupt (a partial write from a kill).
|
|
109
|
-
* The corrupt tail has been quarantined; replay stopped at the last
|
|
110
|
-
* good record.
|
|
111
|
-
*/
|
|
112
|
-
readonly hadCorruptTail: boolean;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// ─── Public API ─────────────────────────────────────────────────────────────
|
|
116
|
-
|
|
117
|
-
export interface TranscriptJournal {
|
|
118
|
-
/**
|
|
119
|
-
* Append one durable event record and fsync it to disk.
|
|
120
|
-
*
|
|
121
|
-
* Best-effort: if the write fails (e.g. disk full), the error is swallowed
|
|
122
|
-
* — the journal is durability-enhancing, never a hard requirement.
|
|
123
|
-
*/
|
|
124
|
-
appendRecord(type: JournalEventType, messages: ConversationMessageSnapshot[]): void;
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Delete the journal file (called after a fresh snapshot is written).
|
|
128
|
-
* The next appendRecord() will recreate the file with a fresh header.
|
|
129
|
-
* Best-effort — silently swallows errors.
|
|
130
|
-
*/
|
|
131
|
-
rotate(): void;
|
|
132
|
-
|
|
133
|
-
/** Absolute path to the journal file. */
|
|
134
|
-
readonly path: string;
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Point this journal at a different session's file, resetting the sequence
|
|
138
|
-
* counter and initialization flag. Used when the session a journal was
|
|
139
|
-
* opened for is switched out from under it (e.g. `/session resume` or
|
|
140
|
-
* `/session fork` reassigning `runtime.sessionId`) — without this, the
|
|
141
|
-
* journal keeps appending the NEW session's records into the OLD session's
|
|
142
|
-
* file. Does not touch the old file on disk; it is simply no longer
|
|
143
|
-
* written to.
|
|
144
|
-
*/
|
|
145
|
-
rebind(journalPath: string, sessionId: string): void;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// ─── Factory ────────────────────────────────────────────────────────────────
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Create a TranscriptJournal for the given session.
|
|
152
|
-
*
|
|
153
|
-
* The journal file is created lazily on the first appendRecord() call.
|
|
154
|
-
* Calling openTranscriptJournal() does not perform any I/O.
|
|
155
|
-
*/
|
|
156
|
-
export function openTranscriptJournal(
|
|
157
|
-
journalPath: string,
|
|
158
|
-
sessionId: string,
|
|
159
|
-
): TranscriptJournal {
|
|
160
|
-
return new TranscriptJournalImpl(journalPath, sessionId);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Build the canonical journal path for a session.
|
|
165
|
-
*
|
|
166
|
-
* Both the home directory and the scope segment come off the caller's
|
|
167
|
-
* SessionSurface, so the journal lands beside the sessions and recovery
|
|
168
|
-
* snapshots it fills gaps for rather than under a separately-spelled scope.
|
|
169
|
-
*
|
|
170
|
-
* @param surface The app's declare-once session-storage handle.
|
|
171
|
-
* @param sessionId The session identifier.
|
|
172
|
-
*/
|
|
173
|
-
export function journalPathFor(surface: SessionSurface, sessionId: string): string {
|
|
174
|
-
return join(surface.homeDirectory, '.goodvibes', surface.surfaceRoot, `transcript-${sessionId}.journal`);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Replay journal records that post-date `snapshotTimestamp`.
|
|
179
|
-
*
|
|
180
|
-
* Returns an empty result if the journal file does not exist.
|
|
181
|
-
* Corrupt tail lines (partial write from a kill) are quarantined; replay
|
|
182
|
-
* stops at the first unparseable line.
|
|
183
|
-
*
|
|
184
|
-
* @param journalPath Absolute path to the journal file.
|
|
185
|
-
* @param snapshotTimestamp The `writtenAt` / `timestamp` of the last known
|
|
186
|
-
* good snapshot. Only records with ts > this value
|
|
187
|
-
* are returned.
|
|
188
|
-
*/
|
|
189
|
-
export function replayJournal(
|
|
190
|
-
journalPath: string,
|
|
191
|
-
snapshotTimestamp: number,
|
|
192
|
-
): ReplayResult {
|
|
193
|
-
if (!existsSync(journalPath)) {
|
|
194
|
-
return { records: [], hadCorruptTail: false };
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
let raw: string;
|
|
198
|
-
try {
|
|
199
|
-
raw = readFileSync(journalPath, 'utf-8');
|
|
200
|
-
} catch {
|
|
201
|
-
return { records: [], hadCorruptTail: false };
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const lines = raw.split('\n').filter((l) => l.trim().length > 0);
|
|
205
|
-
if (lines.length === 0) {
|
|
206
|
-
return { records: [], hadCorruptTail: false };
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// ── Validate header ─────────────────────────────────────────────────────
|
|
210
|
-
let header: unknown;
|
|
211
|
-
try {
|
|
212
|
-
header = JSON.parse(lines[0]);
|
|
213
|
-
} catch {
|
|
214
|
-
quarantineJournal(journalPath);
|
|
215
|
-
return { records: [], hadCorruptTail: true };
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (
|
|
219
|
-
!isPlainObject(header) ||
|
|
220
|
-
typeof header['version'] !== 'number' ||
|
|
221
|
-
header['version'] !== JOURNAL_SCHEMA_VERSION
|
|
222
|
-
) {
|
|
223
|
-
quarantineJournal(journalPath);
|
|
224
|
-
return { records: [], hadCorruptTail: true };
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// ── Read records ────────────────────────────────────────────────────────
|
|
228
|
-
const records: JournalRecord[] = [];
|
|
229
|
-
let firstBadLine = -1;
|
|
230
|
-
|
|
231
|
-
for (let i = 1; i < lines.length; i++) {
|
|
232
|
-
let parsed: unknown;
|
|
233
|
-
try {
|
|
234
|
-
parsed = JSON.parse(lines[i]);
|
|
235
|
-
} catch {
|
|
236
|
-
firstBadLine = i;
|
|
237
|
-
break;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
if (!isValidRecord(parsed)) {
|
|
241
|
-
firstBadLine = i;
|
|
242
|
-
break;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (parsed.ts > snapshotTimestamp) {
|
|
246
|
-
records.push(parsed);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// ── Quarantine corrupt tail ──────────────────────────────────────────────
|
|
251
|
-
let hadCorruptTail = false;
|
|
252
|
-
if (firstBadLine !== -1) {
|
|
253
|
-
hadCorruptTail = true;
|
|
254
|
-
// Quarantine the remainder: rename the file. Caller will rotate after
|
|
255
|
-
// replay anyway, but we quarantine now so the original file is not
|
|
256
|
-
// accidentally replayed again if the process is killed during recovery.
|
|
257
|
-
quarantineJournal(journalPath);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// Sort by seq to guarantee ordering in case lines were reordered (they
|
|
261
|
-
// should not be, but be defensive).
|
|
262
|
-
records.sort((a, b) => a.seq - b.seq);
|
|
263
|
-
|
|
264
|
-
return { records, hadCorruptTail };
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// ─── Implementation ─────────────────────────────────────────────────────────
|
|
268
|
-
|
|
269
|
-
class TranscriptJournalImpl implements TranscriptJournal {
|
|
270
|
-
// Not `readonly` at the class level (unlike the public interface) so
|
|
271
|
-
// rebind() can repoint it; external callers only ever see it through the
|
|
272
|
-
// `TranscriptJournal` interface, which keeps it read-only for them.
|
|
273
|
-
path: string;
|
|
274
|
-
private _sessionId: string;
|
|
275
|
-
private _seq = 0;
|
|
276
|
-
private _initialised = false;
|
|
277
|
-
|
|
278
|
-
constructor(journalPath: string, sessionId: string) {
|
|
279
|
-
this.path = journalPath;
|
|
280
|
-
this._sessionId = sessionId;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
rebind(journalPath: string, sessionId: string): void {
|
|
284
|
-
this.path = journalPath;
|
|
285
|
-
this._sessionId = sessionId;
|
|
286
|
-
this._seq = 0;
|
|
287
|
-
this._initialised = false;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
appendRecord(type: JournalEventType, messages: ConversationMessageSnapshot[]): void {
|
|
291
|
-
try {
|
|
292
|
-
this._ensureInitialised();
|
|
293
|
-
const record: JournalRecord = {
|
|
294
|
-
type,
|
|
295
|
-
seq: this._seq++,
|
|
296
|
-
ts: Date.now(),
|
|
297
|
-
messages,
|
|
298
|
-
};
|
|
299
|
-
const line = JSON.stringify(record) + '\n';
|
|
300
|
-
appendFileSync(this.path, line, { mode: 0o600 });
|
|
301
|
-
// fsync to flush the append to durable storage before returning.
|
|
302
|
-
const fd = openSync(this.path, 'r+');
|
|
303
|
-
try {
|
|
304
|
-
fsyncSync(fd);
|
|
305
|
-
} finally {
|
|
306
|
-
closeSync(fd);
|
|
307
|
-
}
|
|
308
|
-
} catch {
|
|
309
|
-
// Best-effort — never crash the TUI over a journal failure.
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
rotate(): void {
|
|
314
|
-
try {
|
|
315
|
-
if (existsSync(this.path)) {
|
|
316
|
-
unlinkSync(this.path);
|
|
317
|
-
}
|
|
318
|
-
this._initialised = false;
|
|
319
|
-
this._seq = 0;
|
|
320
|
-
} catch {
|
|
321
|
-
// Best-effort.
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
private _ensureInitialised(): void {
|
|
326
|
-
if (this._initialised && journalHasContent(this.path)) return;
|
|
327
|
-
|
|
328
|
-
mkdirSync(dirname(this.path), { recursive: true });
|
|
329
|
-
const header: JournalHeader = {
|
|
330
|
-
version: JOURNAL_SCHEMA_VERSION,
|
|
331
|
-
sessionId: this._sessionId,
|
|
332
|
-
createdAt: Date.now(),
|
|
333
|
-
};
|
|
334
|
-
// Append the header as the first line. If the file already exists with
|
|
335
|
-
// content (e.g. process restarted mid-session), we start appending records
|
|
336
|
-
// after whatever is already there — the replay function handles seq
|
|
337
|
-
// ordering.
|
|
338
|
-
//
|
|
339
|
-
// The guard is `journalHasContent`, not `existsSync`: a crash between
|
|
340
|
-
// creating the file and writing the header leaves a zero-byte journal, and
|
|
341
|
-
// an existence-only check would then skip the header forever. Every record
|
|
342
|
-
// appended after that would sit in a header-less file, so the next replay
|
|
343
|
-
// would read record 0 where the header belongs, fail the version gate, and
|
|
344
|
-
// quarantine the entire journal — losing exactly the turns this module
|
|
345
|
-
// exists to preserve. Deciding on content instead means a zero-byte
|
|
346
|
-
// journal is re-headered and stays replayable.
|
|
347
|
-
if (!journalHasContent(this.path)) {
|
|
348
|
-
appendFileSync(this.path, JSON.stringify(header) + '\n', { mode: 0o600 });
|
|
349
|
-
}
|
|
350
|
-
this._initialised = true;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// ─── Internal helpers ────────────────────────────────────────────────────────
|
|
355
|
-
|
|
356
|
-
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
357
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
function isValidRecord(value: unknown): value is JournalRecord {
|
|
361
|
-
if (!isPlainObject(value)) return false;
|
|
362
|
-
const v = value as Record<string, unknown>;
|
|
363
|
-
return (
|
|
364
|
-
typeof v['type'] === 'string' &&
|
|
365
|
-
typeof v['seq'] === 'number' &&
|
|
366
|
-
typeof v['ts'] === 'number' &&
|
|
367
|
-
Array.isArray(v['messages'])
|
|
368
|
-
);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
function quarantineJournal(journalPath: string): void {
|
|
372
|
-
try {
|
|
373
|
-
renameSync(journalPath, `${journalPath}${UNRECOGNIZED_SUFFIX}`);
|
|
374
|
-
} catch {
|
|
375
|
-
// Best-effort — if rename fails, proceed silently.
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
/** True when the journal file exists AND holds at least one byte. */
|
|
380
|
-
function journalHasContent(journalPath: string): boolean {
|
|
381
|
-
try {
|
|
382
|
-
return statSync(journalPath).size > 0;
|
|
383
|
-
} catch {
|
|
384
|
-
return false;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// ─── Reaping orphaned journals ──────────────────────────────────────────────
|
|
389
|
-
//
|
|
390
|
-
// `rotate()` deletes a journal once its records have been folded into a
|
|
391
|
-
// snapshot or replayed. A session that crashes and is never resumed never
|
|
392
|
-
// reaches either, so its journal stays on disk forever — one file per
|
|
393
|
-
// abandoned session. The SDK's registered append-only retention sweep does not
|
|
394
|
-
// cover this home-scoped path, so the reclaim happens here.
|
|
395
|
-
|
|
396
|
-
const JOURNAL_PREFIX = 'transcript-';
|
|
397
|
-
const JOURNAL_SUFFIX = '.journal';
|
|
398
|
-
|
|
399
|
-
/** How long an untouched journal for a non-live session is kept: 7 days. */
|
|
400
|
-
export const JOURNAL_ORPHAN_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
401
|
-
|
|
402
|
-
/**
|
|
403
|
-
* Hard ceiling on reapable journals kept after the age rule, newest kept.
|
|
404
|
-
* A burst of crashes inside the age window is still bounded.
|
|
405
|
-
*/
|
|
406
|
-
export const JOURNAL_ORPHAN_MAX_FILES = 50;
|
|
407
|
-
|
|
408
|
-
export interface JournalReapResult {
|
|
409
|
-
/** Journal files examined this sweep. */
|
|
410
|
-
readonly scanned: number;
|
|
411
|
-
/** Journal files deleted this sweep. */
|
|
412
|
-
readonly reaped: number;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
export interface JournalReapOptions {
|
|
416
|
-
/**
|
|
417
|
-
* Is this session open in a still-running process? Injected rather than
|
|
418
|
-
* imported so this module keeps no dependency on the liveness marker — the
|
|
419
|
-
* composition point (runtime/durability-housekeeping.ts) supplies the real
|
|
420
|
-
* check, and tests supply their own.
|
|
421
|
-
*/
|
|
422
|
-
readonly isSessionLive: (sessionId: string) => boolean;
|
|
423
|
-
/** The session this process is writing right now; never reaped. */
|
|
424
|
-
readonly currentSessionId?: string | null;
|
|
425
|
-
readonly now?: () => number;
|
|
426
|
-
/** Override the age window (tests). */
|
|
427
|
-
readonly maxAgeMs?: number;
|
|
428
|
-
/** Override the count cap (tests). */
|
|
429
|
-
readonly maxFiles?: number;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
/**
|
|
433
|
-
* Delete transcript journals belonging to sessions that crashed and were never
|
|
434
|
-
* resumed.
|
|
435
|
-
*
|
|
436
|
-
* A journal is reapable only when it is neither the current session's nor
|
|
437
|
-
* apparently open in another running process. Of those, one is deleted when it
|
|
438
|
-
* is empty (a zero-byte file holds no records, so nothing can be lost) or
|
|
439
|
-
* untouched for longer than the age window; whatever survives both rules is
|
|
440
|
-
* then capped by count, newest kept.
|
|
441
|
-
*
|
|
442
|
-
* The rules are deliberately mtime- and liveness-based, never parse-based: an
|
|
443
|
-
* unparseable TAIL is the normal, expected shape of a journal killed
|
|
444
|
-
* mid-append and is exactly the data replay is there to salvage, so a parse
|
|
445
|
-
* failure must never make a journal reapable.
|
|
446
|
-
*
|
|
447
|
-
* Idempotent and concurrency-safe: a journal another sweeper unlinked between
|
|
448
|
-
* the listing and this unlink (ENOENT) counts as reaped, not as an error.
|
|
449
|
-
*/
|
|
450
|
-
export function reapOrphanedJournals(surface: SessionSurface, options: JournalReapOptions): JournalReapResult {
|
|
451
|
-
const dir = join(surface.homeDirectory, '.goodvibes', surface.surfaceRoot);
|
|
452
|
-
let names: string[];
|
|
453
|
-
try {
|
|
454
|
-
names = readdirSync(dir).filter((n) => n.startsWith(JOURNAL_PREFIX) && n.endsWith(JOURNAL_SUFFIX));
|
|
455
|
-
} catch {
|
|
456
|
-
return { scanned: 0, reaped: 0 };
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
const now = options.now?.() ?? Date.now();
|
|
460
|
-
const maxAgeMs = options.maxAgeMs ?? JOURNAL_ORPHAN_MAX_AGE_MS;
|
|
461
|
-
const maxFiles = options.maxFiles ?? JOURNAL_ORPHAN_MAX_FILES;
|
|
462
|
-
|
|
463
|
-
let reaped = 0;
|
|
464
|
-
const survivors: { readonly path: string; readonly mtimeMs: number }[] = [];
|
|
465
|
-
|
|
466
|
-
for (const name of names) {
|
|
467
|
-
const sessionId = name.slice(JOURNAL_PREFIX.length, name.length - JOURNAL_SUFFIX.length);
|
|
468
|
-
if (sessionId.length === 0) continue;
|
|
469
|
-
if (options.currentSessionId && sessionId === options.currentSessionId) continue;
|
|
470
|
-
if (options.isSessionLive(sessionId)) continue;
|
|
471
|
-
|
|
472
|
-
const path = join(dir, name);
|
|
473
|
-
let stats: { size: number; mtimeMs: number };
|
|
474
|
-
try {
|
|
475
|
-
stats = statSync(path);
|
|
476
|
-
} catch {
|
|
477
|
-
continue; // vanished under us
|
|
478
|
-
}
|
|
479
|
-
if (stats.size === 0 || now - stats.mtimeMs > maxAgeMs) {
|
|
480
|
-
if (unlinkJournalIfPresent(path)) reaped++;
|
|
481
|
-
continue;
|
|
482
|
-
}
|
|
483
|
-
survivors.push({ path, mtimeMs: stats.mtimeMs });
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
if (survivors.length > maxFiles) {
|
|
487
|
-
survivors.sort((a, b) => a.mtimeMs - b.mtimeMs); // oldest first
|
|
488
|
-
for (const victim of survivors.slice(0, survivors.length - maxFiles)) {
|
|
489
|
-
if (unlinkJournalIfPresent(victim.path)) reaped++;
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
return { scanned: names.length, reaped };
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
function unlinkJournalIfPresent(path: string): boolean {
|
|
497
|
-
try {
|
|
498
|
-
unlinkSync(path);
|
|
499
|
-
return true;
|
|
500
|
-
} catch (err) {
|
|
501
|
-
return (err as NodeJS.ErrnoException | undefined)?.code === 'ENOENT';
|
|
502
|
-
}
|
|
503
|
-
}
|