mixdog 1.3.2 → 1.3.4
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/package.json +8 -8
- package/scripts/verify-runtime-dependency-versions.mjs +158 -0
- package/src/agents/reviewer/AGENT.md +5 -7
- package/src/agents/reviewer/agent.json +1 -1
- package/src/defaults/shipped-history.json +4 -0
- package/src/defaults/skills/browser-use/SKILL.md +6 -3
- package/src/defaults/skills/local-provider/SKILL.md +33 -6
- package/src/rules/lead/LEAD.md +5 -8
- package/src/rules/shared/45-destructive.md +5 -1
- package/src/rules/shared/60-verification.md +6 -4
- package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +38 -11
- package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +15 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +17 -4
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +14 -1
- package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +14 -1
- package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/registry.mjs +10 -0
- package/src/runtime/agent/orchestrator/providers/wire-gateway.mjs +13 -1
- package/src/runtime/agent/orchestrator/runtime-core/goal-runtime.mjs +5 -0
- package/src/runtime/agent/orchestrator/runtime-core/goal-turns.mjs +10 -1
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +40 -5
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +32 -11
- package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +6 -3
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/agent-room.d.mts +13 -0
- package/src/runtime/agent/orchestrator/session/agent-room.mjs +68 -12
- package/src/runtime/agent/orchestrator/session/compact/continuation.mjs +15 -6
- package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +31 -8
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +44 -0
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +117 -19
- package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/human-gate/delegation.mjs +147 -137
- package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +37 -74
- package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +0 -2
- package/src/runtime/agent/orchestrator/session/human-gate/run-policy.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +13 -7
- package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/continuation.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/empty-nudge.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/output-limit.mjs +62 -13
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +22 -6
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +12 -20
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/call-prep.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +11 -5
- package/src/runtime/agent/orchestrator/session/manager/agent-listing-notice.mjs +102 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +13 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +24 -15
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-steering-drain.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +32 -1
- package/src/runtime/agent/orchestrator/session/manager/orchestration-mode-switch-notice.mjs +17 -1
- package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +36 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +28 -9
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +7 -5
- package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +7 -3
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/routine-result.mjs +7 -4
- package/src/runtime/agent/orchestrator/session/runtime-user-context.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/store/sweep/stale-sweep.mjs +5 -4
- package/src/runtime/agent/orchestrator/session/store/sweep/sub-agent-retention.mjs +140 -0
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-plan.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-record.mjs +7 -2
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +76 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +33 -1
- package/src/runtime/agent/orchestrator/session/store-summary-visibility.d.mts +5 -0
- package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +37 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +1 -0
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +18 -7
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +127 -20
- package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +557 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +6 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +8 -2
- package/src/runtime/attachments/legacy-office.mjs +110 -0
- package/src/runtime/attachments/prompt-restore.mjs +70 -0
- package/src/runtime/attachments/store.mjs +60 -3
- package/src/runtime/effort-judge/judge-client.mjs +61 -14
- package/src/runtime/effort-judge/model-install.mjs +34 -2
- package/src/runtime/memory/lib/memory-chunk-quality.mjs +14 -2
- package/src/runtime/office/portable/portable-soffice.mjs +132 -30
- package/src/runtime/shared/agent-avatar-sprites.mjs +23 -8
- package/src/runtime/shared/agent-avatar.mjs +3 -2
- package/src/runtime/shared/agent-room-host.d.mts +31 -0
- package/src/runtime/shared/agent-room-host.mjs +58 -0
- package/src/runtime/shared/bot-hops.mjs +16 -11
- package/src/runtime/shared/llm/usage-rollup.mjs +4 -383
- package/src/runtime/shared/redact-directories.mjs +18 -0
- package/src/runtime/shared/remove-directory.mjs +28 -0
- package/src/runtime/shared/session-sidebar-host.d.mts +11 -2
- package/src/runtime/shared/session-sidebar-host.mjs +16 -3
- package/src/runtime/shared/tool-execution-contract.mjs +4 -0
- package/src/runtime/shared/tool-work-units.mjs +12 -9
- package/src/runtime/shared/turn-snapshot.mjs +233 -122
- package/src/runtime/shared/user-data-guard.mjs +51 -19
- package/src/runtime/shared/worktrees/activity-gate.mjs +209 -227
- package/src/runtime/shared/worktrees/contract.mjs +11 -3
- package/src/runtime/shared/worktrees/maintenance.mjs +11 -2
- package/src/runtime/shared/worktrees/manager.mjs +419 -52
- package/src/runtime/shared/worktrees/registry.mjs +3 -1
- package/src/runtime/shared/worktrees/tool-host.mjs +39 -0
- package/src/runtime/tidy/process.mjs +41 -8
- package/src/runtime/tidy/resolve.mjs +10 -7
- package/src/session-runtime/agent-home-delivery.mjs +73 -91
- package/src/session-runtime/agent-profile-changes.mjs +16 -0
- package/src/session-runtime/agent-room-delivery.mjs +2 -63
- package/src/session-runtime/agent-task-sessions.mjs +194 -64
- package/src/session-runtime/agent-tool-routing.mjs +49 -4
- package/src/session-runtime/boot/apis.mjs +3 -0
- package/src/session-runtime/boot/facade.mjs +5 -6
- package/src/session-runtime/boot/tools.mjs +23 -1
- package/src/session-runtime/held-requests.mjs +20 -25
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +5 -22
- package/src/session-runtime/model-recency.mjs +1 -0
- package/src/session-runtime/provider-models/row-hydration.mjs +8 -1
- package/src/session-runtime/quick-model-rows/model-meta.mjs +2 -0
- package/src/session-runtime/quick-model-rows/provider-rows.mjs +7 -1
- package/src/session-runtime/runtime-review-api.mjs +4 -1
- package/src/session-runtime/services/agent-tool/close-flow.mjs +14 -2
- package/src/session-runtime/services/agent-tool/execute.mjs +6 -1
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +44 -20
- package/src/session-runtime/services/agent-tool/render.mjs +3 -1
- package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +3 -0
- package/src/session-runtime/services/agent-tool/tool-def.mjs +1 -1
- package/src/session-runtime/services/provider-admin.mjs +12 -2
- package/src/session-runtime/services/worktree-access.mjs +27 -0
- package/src/session-runtime/session-hooks.mjs +9 -17
- package/src/session-runtime/settings-builtin-tools-api.mjs +2 -0
- package/src/session-runtime/setup-tool/executor.mjs +4 -0
- package/src/session-runtime/setup-tool/settings-contract.mjs +3 -0
- package/src/session-runtime/tool-policy/agent-profile-fold.mjs +98 -14
- package/src/session-runtime/tool-policy/policy-stage.mjs +23 -5
- package/src/session-runtime/tool-policy-refresh.mjs +1 -0
- package/src/session-runtime/tool-surface.mjs +9 -5
- package/src/session-runtime/turn/session-ops.mjs +8 -0
- package/src/session-runtime/turn/turn-run.mjs +5 -1
- package/src/session-runtime/usage-stats-api.mjs +1 -1
- package/src/session-runtime/workflow-agents-api/agent-editor.mjs +10 -2
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +39 -17
- package/src/session-runtime/workspace/transition-steps.mjs +1 -18
- package/src/session-runtime/workspace/transitions.mjs +279 -67
- package/src/session-runtime/workspace/worktree-tool.mjs +194 -0
- package/src/standalone/daemon-lag-profiler.mjs +3 -1
- package/src/standalone/daemon-session-control.mjs +140 -23
- package/src/standalone/daemon-telemetry.mjs +9 -0
- package/src/standalone/daemon.mjs +5 -0
- package/src/standalone/local-session-runtime.mjs +11 -2
- package/src/standalone/session-protocol.mjs +1 -0
- package/src/standalone/session-runtime-inline-host/boot-loaders.mjs +2 -0
- package/src/standalone/session-service/agent-route-follow.mjs +69 -0
- package/src/standalone/session-service/session-calls/session-turns.mjs +33 -4
- package/src/standalone/session-service.mjs +10 -0
- package/src/standalone/session-tool-output.mjs +1 -0
- package/src/standalone/staged-import.mjs +47 -0
- package/src/tui/App.jsx +2 -0
- package/src/tui/app/message-selector.mjs +15 -2
- package/src/tui/app/model-options.mjs +16 -2
- package/src/tui/app/model-picker.mjs +2 -1
- package/src/tui/app/route-pickers.mjs +3 -1
- package/src/tui/dist/index.mjs +403 -210
- package/src/tui/paste-attachments.mjs +0 -1
- package/src/tui/paste-text-policy.mjs +4 -5
- package/src/tui/session/agent-job-feed/execution-delivery.mjs +4 -1
- package/src/tui/session/notice-surface.mjs +3 -0
- package/src/tui/session/session-api/intake/abort.mjs +33 -17
- package/src/tui/session/session-api/intake/rewind.mjs +39 -4
- package/src/tui/session/session-api/routes-agent-definitions.mjs +1 -0
- package/src/tui/session/tool-approval.mjs +11 -48
- package/src/vendor/statusline/src/gateway/claude-current.mjs +0 -58
- package/src/vendor/statusline/src/gateway/route-meta.mjs +1 -361
- package/src/vendor/statusline/src/gateway/session-routes.mjs +0 -55
- package/src/session-runtime/room-member-gate.mjs +0 -61
- package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +0 -285
|
@@ -75,7 +75,11 @@ import { createLineDiffBudget, lineDiffHunks, unifiedHunksBody } from './bounded
|
|
|
75
75
|
// baseline of the session-wide review.
|
|
76
76
|
//
|
|
77
77
|
// Worker trackers remain separate until the worker completes: then its
|
|
78
|
-
// segments, owned paths and counts join the
|
|
78
|
+
// segments, owned paths and counts join the Lead turn that is running at that
|
|
79
|
+
// moment, or — when the Lead's turn already ended (sealed or sealing) — the
|
|
80
|
+
// next Lead turn that opens or reopens: the turn that receives the worker's
|
|
81
|
+
// completion and reports it. A worker's edits so land in exactly one turn
|
|
82
|
+
// record, and never in a record that was already frozen.
|
|
79
83
|
|
|
80
84
|
const DISABLED = /^(0|false|off)$/i.test(String(process.env.MIXDOG_TURN_SNAPSHOT || ''));
|
|
81
85
|
const TURN_CACHE_MAX = 32;
|
|
@@ -99,18 +103,46 @@ const _changeMutations = new Map();
|
|
|
99
103
|
// `${sessionId}\0${checkpointId}` → path keys whose worktree state is unknown
|
|
100
104
|
// and whose record could not even be marked so (the store refused every write).
|
|
101
105
|
const _unknownChangeStates = new Map();
|
|
106
|
+
// Owner session → worker key → completed worker review waiting for the
|
|
107
|
+
// owner's next turn (its turn had ended when they completed), in completion
|
|
108
|
+
// order. Bounded like a turn's agents (trimWorkerReviews).
|
|
109
|
+
const _parkedWorkerReviews = new Map();
|
|
110
|
+
// Owner session → how many times its change history was deleted: a worker
|
|
111
|
+
// bound before a delete never writes into the history after it.
|
|
112
|
+
const _historyEpochs = new Map();
|
|
113
|
+
|
|
114
|
+
/** A parked review owns its worker's bound tracker content (`release`). */
|
|
115
|
+
function releaseParkedReview(review) {
|
|
116
|
+
if (review.release) releaseDiffTrackerContent(review.source);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function dropParkedWorkerReviews(ownerSessionId) {
|
|
120
|
+
for (const review of _parkedWorkerReviews.get(ownerSessionId)?.values() || []) releaseParkedReview(review);
|
|
121
|
+
_parkedWorkerReviews.delete(ownerSessionId);
|
|
122
|
+
}
|
|
102
123
|
|
|
103
124
|
function trimTurnCache() {
|
|
104
125
|
while (_turnsBySession.size > TURN_CACHE_MAX) {
|
|
105
126
|
const oldest = _turnsBySession.keys().next().value;
|
|
106
127
|
if (oldest === undefined) break;
|
|
107
128
|
_turnsBySession.delete(oldest);
|
|
129
|
+
dropParkedWorkerReviews(oldest);
|
|
108
130
|
for (const [sessionId, tracker] of _diffTrackersBySession) {
|
|
109
131
|
if (sessionId === oldest || tracker.ownerSessionId === oldest) {
|
|
110
132
|
_diffTrackersBySession.delete(sessionId);
|
|
133
|
+
// A bound worker's content is read by nothing else; the Lead's own
|
|
134
|
+
// tracker may still be sealing, so it is only unregistered.
|
|
135
|
+
if (sessionId !== oldest) releaseDiffTrackerContent(tracker);
|
|
111
136
|
}
|
|
112
137
|
}
|
|
113
138
|
}
|
|
139
|
+
// Parked reviews of a Lead with no turn in memory (a worker resumed after a
|
|
140
|
+
// restart) are bounded on their own: oldest owner first.
|
|
141
|
+
while (_parkedWorkerReviews.size > TURN_CACHE_MAX) {
|
|
142
|
+
const oldest = _parkedWorkerReviews.keys().next().value;
|
|
143
|
+
if (oldest === undefined) break;
|
|
144
|
+
dropParkedWorkerReviews(oldest);
|
|
145
|
+
}
|
|
114
146
|
}
|
|
115
147
|
|
|
116
148
|
function mergePatches(values) {
|
|
@@ -695,13 +727,17 @@ function mergeTrackedContent(target, source) {
|
|
|
695
727
|
}
|
|
696
728
|
}
|
|
697
729
|
|
|
698
|
-
|
|
730
|
+
/** Bound worker reviews keyed by worker (a turn's agents, an owner's parked
|
|
731
|
+
* reviews): oldest out first, by count and retained patch bytes. */
|
|
732
|
+
function trimWorkerReviews(reviews, evicted = () => {}) {
|
|
699
733
|
const retainedBytes = () =>
|
|
700
|
-
[...
|
|
701
|
-
while (
|
|
702
|
-
const oldest =
|
|
734
|
+
[...reviews.values()].reduce((total, review) => total + Buffer.byteLength(review.patch || '', 'utf8'), 0);
|
|
735
|
+
while (reviews.size > MAX_AGENT_REVIEWS_PER_TURN || retainedBytes() > MAX_AGENT_REVIEW_BYTES_PER_TURN) {
|
|
736
|
+
const oldest = reviews.keys().next().value;
|
|
703
737
|
if (oldest === undefined) break;
|
|
704
|
-
|
|
738
|
+
const review = reviews.get(oldest);
|
|
739
|
+
reviews.delete(oldest);
|
|
740
|
+
evicted(review);
|
|
705
741
|
}
|
|
706
742
|
}
|
|
707
743
|
|
|
@@ -741,6 +777,7 @@ function continueTurnSnapshot(ownerSessionId) {
|
|
|
741
777
|
tracker.sealed = false;
|
|
742
778
|
tracker.sealEpoch += 1;
|
|
743
779
|
markWorktreeContention(ownerSessionId, tracker);
|
|
780
|
+
adoptParkedWorkerReviews(ownerSessionId, turn, tracker);
|
|
744
781
|
return true;
|
|
745
782
|
}
|
|
746
783
|
|
|
@@ -757,16 +794,8 @@ export async function beginTurnSnapshot(worktree, sessionId, options = {}) {
|
|
|
757
794
|
// Turn start and Undo/Reapply share the per-session lock: a start waits for
|
|
758
795
|
// an in-flight mutation, and a mutation queued after this start re-checks
|
|
759
796
|
// under the lock and finds the turn running.
|
|
760
|
-
// Late-worker re-seals still queued for the previous turn run first, in
|
|
761
|
-
// this same step — including one a worker queued while an earlier one ran —
|
|
762
|
-
// so the workers' edits stay in that turn's record and nothing the new turn
|
|
763
|
-
// writes can reach it. (A queued re-seal implies a chain.)
|
|
764
797
|
const { token, tracker } = _changeMutations.has(ownerSessionId)
|
|
765
|
-
? await serializeChangeMutation(ownerSessionId, async () =>
|
|
766
|
-
const previous = _diffTrackersBySession.get(ownerSessionId);
|
|
767
|
-
while (previous?.resealQueued) await runQueuedReseal(ownerSessionId, previous).catch(() => {});
|
|
768
|
-
return openTurnSnapshot(worktree, ownerSessionId, options);
|
|
769
|
-
})
|
|
798
|
+
? await serializeChangeMutation(ownerSessionId, async () => openTurnSnapshot(worktree, ownerSessionId, options))
|
|
770
799
|
: openTurnSnapshot(worktree, ownerSessionId, options);
|
|
771
800
|
if (!tracker) return token;
|
|
772
801
|
try {
|
|
@@ -796,15 +825,15 @@ function openTurnSnapshot(worktree, ownerSessionId, options) {
|
|
|
796
825
|
// worktree lock ahead of this turn's baseline.
|
|
797
826
|
const prior = _diffTrackersBySession.get(ownerSessionId);
|
|
798
827
|
if (prior && prior.ownerGeneration > 0 && !prior.sealed) void sealTurn(ownerSessionId, prior).catch(() => {});
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
}
|
|
828
|
+
// Workers still running keep their bound trackers across the new turn:
|
|
829
|
+
// their edits join whichever turn is running when they complete.
|
|
802
830
|
_turnsBySession.delete(ownerSessionId);
|
|
803
|
-
|
|
831
|
+
const turn = {
|
|
804
832
|
generation,
|
|
805
833
|
checkpointId,
|
|
806
834
|
agents: new Map(),
|
|
807
|
-
}
|
|
835
|
+
};
|
|
836
|
+
_turnsBySession.set(ownerSessionId, turn);
|
|
808
837
|
const worktreeRequest = pathKey(worktree);
|
|
809
838
|
const tracker = resetDiffTracker(ownerSessionId, {
|
|
810
839
|
ownerGeneration: generation,
|
|
@@ -813,7 +842,10 @@ function openTurnSnapshot(worktree, ownerSessionId, options) {
|
|
|
813
842
|
worktreeRoot: resolve(clean(worktree) || '.'),
|
|
814
843
|
});
|
|
815
844
|
trimTurnCache();
|
|
816
|
-
if (tracker)
|
|
845
|
+
if (tracker) {
|
|
846
|
+
markWorktreeContention(ownerSessionId, tracker);
|
|
847
|
+
adoptParkedWorkerReviews(ownerSessionId, turn, tracker);
|
|
848
|
+
}
|
|
817
849
|
_turnOpenedHookForTest?.(ownerSessionId, checkpointId);
|
|
818
850
|
return { token: { generation, checkpointId }, tracker };
|
|
819
851
|
}
|
|
@@ -825,7 +857,9 @@ export function _setTurnOpenedHookForTest(hook) {
|
|
|
825
857
|
_turnOpenedHookForTest = typeof hook === 'function' ? hook : null;
|
|
826
858
|
}
|
|
827
859
|
|
|
828
|
-
/** Bind one worker execution to
|
|
860
|
+
/** Bind one worker execution to its Lead. A Lead with no turn in memory (a
|
|
861
|
+
* worker resumed after a restart) binds generation 0: its completion waits
|
|
862
|
+
* for the Lead's next turn. */
|
|
829
863
|
export function beginAgentTurnReview(ownerSessionId, childSessionId, meta = {}) {
|
|
830
864
|
if (DISABLED) return null;
|
|
831
865
|
const owner = clean(ownerSessionId);
|
|
@@ -833,24 +867,21 @@ export function beginAgentTurnReview(ownerSessionId, childSessionId, meta = {})
|
|
|
833
867
|
// Durable: the worker's edits join the Lead's session review even when it
|
|
834
868
|
// runs its own session turn, and whatever the turn's agent list keeps.
|
|
835
869
|
recordReviewOwner(child, owner);
|
|
836
|
-
|
|
837
|
-
|
|
870
|
+
if (!owner || !child) return null;
|
|
871
|
+
const generation = _turnsBySession.get(owner)?.generation || 0;
|
|
838
872
|
const existingTracker = _diffTrackersBySession.get(child);
|
|
839
|
-
if (
|
|
840
|
-
!existingTracker ||
|
|
841
|
-
existingTracker.ownerSessionId !== owner ||
|
|
842
|
-
existingTracker.ownerGeneration !== turn.generation
|
|
843
|
-
) {
|
|
873
|
+
if (!existingTracker || existingTracker.ownerSessionId !== owner || existingTracker.ownerGeneration !== generation) {
|
|
844
874
|
resetDiffTracker(child, {
|
|
845
875
|
ownerSessionId: owner,
|
|
846
|
-
ownerGeneration:
|
|
876
|
+
ownerGeneration: generation,
|
|
847
877
|
});
|
|
848
878
|
}
|
|
849
879
|
_agentTurnSeq += 1;
|
|
850
880
|
return {
|
|
851
881
|
id: `agent-review-${_agentTurnSeq}`,
|
|
852
882
|
ownerSessionId: owner,
|
|
853
|
-
generation
|
|
883
|
+
generation,
|
|
884
|
+
historyEpoch: _historyEpochs.get(owner) || 0,
|
|
854
885
|
sessionId: child,
|
|
855
886
|
agent: clean(meta.agent) || null,
|
|
856
887
|
tag: clean(meta.tag) || null,
|
|
@@ -858,62 +889,145 @@ export function beginAgentTurnReview(ownerSessionId, childSessionId, meta = {})
|
|
|
858
889
|
};
|
|
859
890
|
}
|
|
860
891
|
|
|
861
|
-
/**
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
892
|
+
/** The Lead turn is running: not sealed, not sealing. */
|
|
893
|
+
const turnRunning = (turn, tracker) =>
|
|
894
|
+
Boolean(turn && tracker) &&
|
|
895
|
+
tracker.ownerGeneration === turn.generation &&
|
|
896
|
+
!tracker.sealed &&
|
|
897
|
+
!tracker.freezing &&
|
|
898
|
+
!tracker.discarded;
|
|
899
|
+
|
|
900
|
+
/** Attribute one completed worker review to `turn` and fold its own segments,
|
|
901
|
+
* owned paths and counts into the turn's tracker. */
|
|
902
|
+
function joinWorkerReview(turn, ownerTracker, review) {
|
|
903
|
+
const { key, source, tracked, patch } = review;
|
|
904
|
+
const prior = turn.agents.get(key);
|
|
905
|
+
if (!patch) {
|
|
906
|
+
if (!(tracked && prior)) return false;
|
|
907
|
+
turn.agents.delete(key);
|
|
908
|
+
return true;
|
|
909
|
+
}
|
|
910
|
+
turn.agents.delete(key);
|
|
911
|
+
turn.agents.set(key, {
|
|
912
|
+
sessionId: key,
|
|
913
|
+
agent: review.agent || prior?.agent || null,
|
|
914
|
+
tag: review.tag || prior?.tag || null,
|
|
915
|
+
patch: tracked ? patch : mergePatches([prior?.patch, patch]),
|
|
916
|
+
});
|
|
917
|
+
if (source && ownerTracker !== source) foldWorkerTracker(ownerTracker, source);
|
|
918
|
+
trimWorkerReviews(turn.agents);
|
|
919
|
+
return true;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/** Fold a finished worker's owned paths, counts and own segments into
|
|
923
|
+
* `target`. */
|
|
924
|
+
function foldWorkerTracker(target, source) {
|
|
925
|
+
for (const path of source.ownedPaths) target.ownedPaths.add(path);
|
|
926
|
+
mergeOwnStats(target.ownStats, source.ownStats);
|
|
927
|
+
mergeTrackedContent(target, source);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
/** One parked review of a worker that completed again before its Lead's next
|
|
931
|
+
* turn: both runs' sources fold into one tracker this review owns — the
|
|
932
|
+
* older run's own bound tracker, else a fresh one, never a worker session's
|
|
933
|
+
* own tracker — exactly as both would have joined a running turn. The patch
|
|
934
|
+
* is the merged tracker's cumulative view when tracked, else the merged
|
|
935
|
+
* apply_patch diffs. */
|
|
936
|
+
function mergeParkedReview(prior, review) {
|
|
937
|
+
const sources = [prior.source, review.source].filter(Boolean);
|
|
938
|
+
let source = null;
|
|
939
|
+
if (sources.length > 0) {
|
|
940
|
+
source = prior.release && prior.source ? prior.source : createDiffTracker();
|
|
941
|
+
for (const other of sources) if (other !== source) foldWorkerTracker(source, other);
|
|
942
|
+
}
|
|
943
|
+
releaseParkedReview(review);
|
|
944
|
+
const tracked = prior.tracked || review.tracked;
|
|
945
|
+
return {
|
|
946
|
+
key: review.key,
|
|
947
|
+
source,
|
|
948
|
+
tracked,
|
|
949
|
+
patch: tracked ? trackerView(source).patch : mergePatches([prior.patch, review.patch]),
|
|
950
|
+
agent: review.agent || prior.agent,
|
|
951
|
+
tag: review.tag || prior.tag,
|
|
952
|
+
release: Boolean(source),
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/** Park a completed worker review for its Lead's next turn. A newer
|
|
957
|
+
* completion of the same worker merges into the parked one (every run's
|
|
958
|
+
* edits are kept, as a running turn keeps them); the parked reviews are
|
|
959
|
+
* bounded by the turn's own limits, and an evicted one releases its tracker
|
|
960
|
+
* content. */
|
|
961
|
+
function parkWorkerReview(ownerSessionId, review) {
|
|
962
|
+
const parked = _parkedWorkerReviews.get(ownerSessionId) || new Map();
|
|
963
|
+
const prior = parked.get(review.key);
|
|
964
|
+
if (!prior && !review.patch) {
|
|
965
|
+
releaseParkedReview(review);
|
|
966
|
+
return false;
|
|
967
|
+
}
|
|
968
|
+
parked.delete(review.key);
|
|
969
|
+
parked.set(review.key, prior ? mergeParkedReview(prior, review) : review);
|
|
970
|
+
trimWorkerReviews(parked, releaseParkedReview);
|
|
971
|
+
_parkedWorkerReviews.set(ownerSessionId, parked);
|
|
972
|
+
trimTurnCache();
|
|
973
|
+
return true;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/** A turn that just opened (or reopened) takes the worker reviews that
|
|
977
|
+
* completed while its Lead had no running turn. */
|
|
978
|
+
function adoptParkedWorkerReviews(ownerSessionId, turn, tracker) {
|
|
979
|
+
const parked = _parkedWorkerReviews.get(ownerSessionId);
|
|
980
|
+
if (!parked) return;
|
|
981
|
+
_parkedWorkerReviews.delete(ownerSessionId);
|
|
982
|
+
for (const review of parked.values()) {
|
|
983
|
+
joinWorkerReview(turn, tracker, review);
|
|
984
|
+
releaseParkedReview(review);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/** Hand a finished worker's own edits (its tracker, else its apply_patch
|
|
989
|
+
* diffs) to its Lead: into the Lead turn running now, or — when none runs —
|
|
990
|
+
* parked for the Lead's next turn to open or reopen. Everything a seal
|
|
991
|
+
* reads is published before this returns, so a seal started right after it
|
|
992
|
+
* already includes the worker. */
|
|
865
993
|
export function completeAgentTurnReview(handle, patches = []) {
|
|
866
994
|
if (!handle || handle.completed === true) return false;
|
|
867
995
|
handle.completed = true;
|
|
868
996
|
const ownerSessionId = clean(handle.ownerSessionId);
|
|
869
997
|
const key = clean(handle.sessionId) || handle.id;
|
|
870
|
-
const turn = _turnsBySession.get(ownerSessionId);
|
|
871
998
|
const tracker = _diffTrackersBySession.get(key);
|
|
872
999
|
const matchingTracker =
|
|
873
1000
|
tracker && tracker.ownerSessionId === ownerSessionId && tracker.ownerGeneration === handle.generation;
|
|
1001
|
+
// This execution is over: its bound tracker leaves the registry, so a next
|
|
1002
|
+
// execution of the worker starts its own.
|
|
1003
|
+
if (matchingTracker) _diffTrackersBySession.delete(key);
|
|
1004
|
+
// The Lead's change history was deleted after this worker was bound.
|
|
1005
|
+
if ((_historyEpochs.get(ownerSessionId) || 0) !== (handle.historyEpoch || 0)) {
|
|
1006
|
+
if (matchingTracker) releaseDiffTrackerContent(tracker);
|
|
1007
|
+
return false;
|
|
1008
|
+
}
|
|
874
1009
|
// A worker that runs its own session turn replaces the bound tracker with
|
|
875
1010
|
// its turn's own one (no owner). beginAgentTurnReview reset any older
|
|
876
1011
|
// tracker, so an unowned one here holds this execution's edits.
|
|
877
1012
|
const source = matchingTracker || (tracker && !tracker.ownerSessionId) ? tracker : null;
|
|
1013
|
+
const tracked = Boolean(source) && source.revision > 0;
|
|
1014
|
+
const review = {
|
|
1015
|
+
key,
|
|
1016
|
+
source,
|
|
1017
|
+
tracked,
|
|
1018
|
+
patch: tracked ? trackerView(source).patch : mergePatches(patches),
|
|
1019
|
+
agent: handle.agent,
|
|
1020
|
+
tag: handle.tag,
|
|
1021
|
+
release: Boolean(matchingTracker),
|
|
1022
|
+
};
|
|
1023
|
+
const turn = _turnsBySession.get(ownerSessionId);
|
|
878
1024
|
const ownerTracker = _diffTrackersBySession.get(ownerSessionId);
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
const patch = tracked ? trackerView(source).patch : mergePatches(patches);
|
|
884
|
-
if (!patch) {
|
|
885
|
-
if (tracked && prior) {
|
|
886
|
-
turn.agents.delete(key);
|
|
887
|
-
return true;
|
|
888
|
-
}
|
|
889
|
-
return false;
|
|
890
|
-
}
|
|
891
|
-
turn.agents.delete(key);
|
|
892
|
-
turn.agents.set(key, {
|
|
893
|
-
sessionId: key,
|
|
894
|
-
agent: handle.agent || prior?.agent || null,
|
|
895
|
-
tag: handle.tag || prior?.tag || null,
|
|
896
|
-
patch: tracked ? patch : mergePatches([prior?.patch, patch]),
|
|
897
|
-
});
|
|
898
|
-
if (source && ownerTracker && ownerTracker !== source) {
|
|
899
|
-
for (const path of source.ownedPaths) ownerTracker.ownedPaths.add(path);
|
|
900
|
-
mergeOwnStats(ownerTracker.ownStats, source.ownStats);
|
|
901
|
-
mergeTrackedContent(ownerTracker, source);
|
|
902
|
-
}
|
|
903
|
-
trimAgentReviews(turn);
|
|
904
|
-
// A worker that finishes after its Lead turn sealed (or while it seals)
|
|
905
|
-
// re-seals that turn's change record (its edits are part of the turn's net
|
|
906
|
-
// worktree diff).
|
|
907
|
-
if (ownerTracker && ownerTracker !== tracker && ownerTracker.ownerGeneration === handle.generation) {
|
|
908
|
-
resealTurn(ownerSessionId, ownerTracker);
|
|
909
|
-
}
|
|
910
|
-
return true;
|
|
911
|
-
} finally {
|
|
912
|
-
if (matchingTracker && _diffTrackersBySession.get(key) === tracker) {
|
|
913
|
-
releaseDiffTrackerContent(tracker);
|
|
914
|
-
_diffTrackersBySession.delete(key);
|
|
915
|
-
}
|
|
1025
|
+
if (turnRunning(turn, ownerTracker)) {
|
|
1026
|
+
const joined = joinWorkerReview(turn, ownerTracker, review);
|
|
1027
|
+
releaseParkedReview(review);
|
|
1028
|
+
return joined;
|
|
916
1029
|
}
|
|
1030
|
+
return parkWorkerReview(ownerSessionId, review);
|
|
917
1031
|
}
|
|
918
1032
|
|
|
919
1033
|
/** Persist the durable half of a finished review: the baseline tree hash plus
|
|
@@ -1092,15 +1206,12 @@ const pendingFreezeKey = (ownerSessionId, checkpointId) => `${ownerSessionId}\0$
|
|
|
1092
1206
|
* Seal a Lead turn: capture the worktree one last time, persist the review
|
|
1093
1207
|
* baseline and freeze the change record. Single-flight per tracker; a seal
|
|
1094
1208
|
* requested while another runs waits for it and then seals again only if a
|
|
1095
|
-
* completion wake reopened the turn meanwhile.
|
|
1096
|
-
* re-freezes a turn that is still sealed, after any seal in flight: that one
|
|
1097
|
-
* captured its agents before the worker was attributed.
|
|
1209
|
+
* completion wake reopened the turn meanwhile.
|
|
1098
1210
|
*/
|
|
1099
|
-
function sealTurn(ownerSessionId, tracker
|
|
1211
|
+
function sealTurn(ownerSessionId, tracker) {
|
|
1100
1212
|
// Its history was deleted (clearSessionChangeHistory): never written again.
|
|
1101
1213
|
if (tracker.discarded) return Promise.resolve();
|
|
1102
1214
|
if (tracker.freezing) {
|
|
1103
|
-
if (reseal) return tracker.freezing.then(() => sealTurn(ownerSessionId, tracker, { reseal: true }));
|
|
1104
1215
|
return tracker.freezing.then(() => (tracker.sealed ? undefined : sealTurn(ownerSessionId, tracker)));
|
|
1105
1216
|
}
|
|
1106
1217
|
const epoch = tracker.sealEpoch;
|
|
@@ -1115,9 +1226,7 @@ function sealTurn(ownerSessionId, tracker, { reseal = false } = {}) {
|
|
|
1115
1226
|
// Best-effort refresh: seal with the last captured worktree state.
|
|
1116
1227
|
}
|
|
1117
1228
|
}
|
|
1118
|
-
if (tracker.discarded || tracker.sealEpoch !== epoch
|
|
1119
|
-
// A re-seal never rewrites a turn once the next one opened.
|
|
1120
|
-
if (reseal && turnSuperseded(ownerSessionId, tracker)) return;
|
|
1229
|
+
if (tracker.discarded || tracker.sealEpoch !== epoch) return;
|
|
1121
1230
|
tracker.sealed = true;
|
|
1122
1231
|
await persistTurnSnapshotRecord(ownerSessionId, tracker);
|
|
1123
1232
|
try {
|
|
@@ -1125,8 +1234,8 @@ function sealTurn(ownerSessionId, tracker, { reseal = false } = {}) {
|
|
|
1125
1234
|
} catch {
|
|
1126
1235
|
/* the change record adds Undo history; it never fails a turn */
|
|
1127
1236
|
}
|
|
1128
|
-
// The bounded own before/after content stays until the next turn: a
|
|
1129
|
-
//
|
|
1237
|
+
// The bounded own before/after content stays until the next turn: a
|
|
1238
|
+
// completion wake reopening it and an Undo/Reapply of this turn fold into it.
|
|
1130
1239
|
})().finally(() => {
|
|
1131
1240
|
if (tracker.freezing === run) tracker.freezing = null;
|
|
1132
1241
|
const count = (_pendingFreezes.get(key) || 1) - 1;
|
|
@@ -1137,31 +1246,6 @@ function sealTurn(ownerSessionId, tracker, { reseal = false } = {}) {
|
|
|
1137
1246
|
return run;
|
|
1138
1247
|
}
|
|
1139
1248
|
|
|
1140
|
-
function resealTurn(ownerSessionId, tracker) {
|
|
1141
|
-
// A turn still running seals with every published agent; one sealed or
|
|
1142
|
-
// sealing queues its re-seal (sealTurn runs it after a seal in flight).
|
|
1143
|
-
if (!(tracker.sealed || tracker.freezing) || !clean(tracker.checkpointId) || tracker.resealQueued) return;
|
|
1144
|
-
// On the session lock: an in-flight Undo/Reapply lands before the re-freeze
|
|
1145
|
-
// reads the worktree. A turn start queued ahead of it runs it before opening
|
|
1146
|
-
// (beginTurnSnapshot), so one queued re-seal covers every late worker.
|
|
1147
|
-
tracker.resealQueued = true;
|
|
1148
|
-
void serializeChangeMutation(ownerSessionId, () => runQueuedReseal(ownerSessionId, tracker)).catch(() => {});
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
/** True once a newer turn of the session opened over `tracker`'s. */
|
|
1152
|
-
function turnSuperseded(ownerSessionId, tracker) {
|
|
1153
|
-
const turn = _turnsBySession.get(ownerSessionId);
|
|
1154
|
-
return Boolean(turn) && turn.generation !== tracker.ownerGeneration;
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
/** Run `tracker`'s queued re-seal once, whichever lock step reaches it first. */
|
|
1158
|
-
function runQueuedReseal(ownerSessionId, tracker) {
|
|
1159
|
-
if (!tracker.resealQueued) return undefined;
|
|
1160
|
-
tracker.resealQueued = false;
|
|
1161
|
-
if (turnSuperseded(ownerSessionId, tracker)) return undefined;
|
|
1162
|
-
return sealTurn(ownerSessionId, tracker, { reseal: true });
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
1249
|
/** Freeze a completed (or cancelled) Lead turn while retaining only its
|
|
1166
1250
|
* bounded review data. `expected` — the `{ generation }` beginTurnSnapshot
|
|
1167
1251
|
* resolved to — makes a delayed call a no-op once a newer turn began. */
|
|
@@ -1947,20 +2031,8 @@ export async function clearSessionChangeHistory(sessionId) {
|
|
|
1947
2031
|
throw new Error('change history cannot be deleted while this session is running a turn');
|
|
1948
2032
|
}
|
|
1949
2033
|
// Invalidate every producer of the old history BEFORE the awaited
|
|
1950
|
-
// deletion
|
|
1951
|
-
|
|
1952
|
-
// discarded, so nothing begun before the delete can write it back.
|
|
1953
|
-
for (const [trackedSessionId, tracker] of _diffTrackersBySession) {
|
|
1954
|
-
if (trackedSessionId !== ownerSessionId && tracker.ownerSessionId !== ownerSessionId) continue;
|
|
1955
|
-
tracker.discarded = true;
|
|
1956
|
-
tracker.resealQueued = false;
|
|
1957
|
-
releaseDiffTrackerContent(tracker);
|
|
1958
|
-
_diffTrackersBySession.delete(trackedSessionId);
|
|
1959
|
-
}
|
|
1960
|
-
_turnsBySession.delete(ownerSessionId);
|
|
1961
|
-
for (const key of _unknownChangeStates.keys()) {
|
|
1962
|
-
if (key.startsWith(`${ownerSessionId}\0`)) _unknownChangeStates.delete(key);
|
|
1963
|
-
}
|
|
2034
|
+
// deletion, so nothing begun before the delete can write it back.
|
|
2035
|
+
discardSessionChangeState(ownerSessionId);
|
|
1964
2036
|
// The session's first-seen baselines go too: later edits start fresh ones.
|
|
1965
2037
|
const [turns] = await Promise.all([
|
|
1966
2038
|
clearStoredChangeHistory(ownerSessionId),
|
|
@@ -1970,12 +2042,51 @@ export async function clearSessionChangeHistory(sessionId) {
|
|
|
1970
2042
|
});
|
|
1971
2043
|
}
|
|
1972
2044
|
|
|
2045
|
+
/** Release a Lead session's in-memory change state: a worker bound before
|
|
2046
|
+
* this finds its history epoch moved on (its completion parks nothing), its
|
|
2047
|
+
* parked worker reviews are dropped, and its own and bound worker trackers
|
|
2048
|
+
* are discarded (anything earlier that seals writes nothing). */
|
|
2049
|
+
function discardSessionChangeState(ownerSessionId) {
|
|
2050
|
+
_historyEpochs.set(ownerSessionId, (_historyEpochs.get(ownerSessionId) || 0) + 1);
|
|
2051
|
+
dropParkedWorkerReviews(ownerSessionId);
|
|
2052
|
+
for (const [trackedSessionId, tracker] of _diffTrackersBySession) {
|
|
2053
|
+
if (trackedSessionId !== ownerSessionId && tracker.ownerSessionId !== ownerSessionId) continue;
|
|
2054
|
+
tracker.discarded = true;
|
|
2055
|
+
releaseDiffTrackerContent(tracker);
|
|
2056
|
+
_diffTrackersBySession.delete(trackedSessionId);
|
|
2057
|
+
}
|
|
2058
|
+
_turnsBySession.delete(ownerSessionId);
|
|
2059
|
+
for (const key of _unknownChangeStates.keys()) {
|
|
2060
|
+
if (key.startsWith(`${ownerSessionId}\0`)) _unknownChangeStates.delete(key);
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
/** The session was deleted: release its turn, parked worker reviews and
|
|
2065
|
+
* bound worker trackers (its stored records go with the session purge). */
|
|
2066
|
+
export function forgetTurnSnapshotSession(sessionId) {
|
|
2067
|
+
const ownerSessionId = clean(sessionId);
|
|
2068
|
+
if (ownerSessionId) discardSessionChangeState(ownerSessionId);
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
export function _diffTrackerForTest(sessionId) {
|
|
2072
|
+
return _diffTrackersBySession.get(clean(sessionId));
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
export function _parkedWorkerReviewsForTest(ownerSessionId) {
|
|
2076
|
+
return [...(_parkedWorkerReviews.get(clean(ownerSessionId))?.values() || [])].map(({ key, patch }) => ({
|
|
2077
|
+
key,
|
|
2078
|
+
patch,
|
|
2079
|
+
}));
|
|
2080
|
+
}
|
|
2081
|
+
|
|
1973
2082
|
export function _resetTurnSnapshotForTest() {
|
|
1974
2083
|
_turnsBySession.clear();
|
|
1975
2084
|
_diffTrackersBySession.clear();
|
|
1976
2085
|
_pendingFreezes.clear();
|
|
1977
2086
|
_changeMutations.clear();
|
|
1978
2087
|
_unknownChangeStates.clear();
|
|
2088
|
+
_parkedWorkerReviews.clear();
|
|
2089
|
+
_historyEpochs.clear();
|
|
1979
2090
|
_turnOpenedHookForTest = null;
|
|
1980
2091
|
_agentTurnSeq = 0;
|
|
1981
2092
|
_resetTurnWorktreeSnapshotsForTest();
|
|
@@ -389,9 +389,20 @@ function copyTree(src, dst, copied) {
|
|
|
389
389
|
copied.push(dst);
|
|
390
390
|
}
|
|
391
391
|
|
|
392
|
-
|
|
392
|
+
// Only a published manifest makes a backup count toward retention.
|
|
393
|
+
const manifestPath = (scopeDir, name) => join(scopeDir, name, BACKUP_MANIFEST);
|
|
394
|
+
|
|
395
|
+
/** Prune one data dir's scope only; other scopes, legacy entries and
|
|
396
|
+
* unfinished reservations stay. */
|
|
393
397
|
function pruneBackups(scopeDir, names, own) {
|
|
394
|
-
|
|
398
|
+
const completed = names.filter((name) => {
|
|
399
|
+
try {
|
|
400
|
+
return statSync(manifestPath(scopeDir, name)).isFile();
|
|
401
|
+
} catch {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
for (const name of backupsToPrune(completed, own)) {
|
|
395
406
|
try {
|
|
396
407
|
rmSync(join(scopeDir, name), { recursive: true, force: true });
|
|
397
408
|
} catch {}
|
|
@@ -461,16 +472,24 @@ export function backupUserData(dataDir, reason = 'snapshot', { dirs = [] } = {})
|
|
|
461
472
|
const scopeDir = scopeDirFor(dataDir);
|
|
462
473
|
const { backupDir, names } = reserveBackupDir(scopeDir, reason);
|
|
463
474
|
const copied = [];
|
|
464
|
-
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
const
|
|
470
|
-
|
|
475
|
+
try {
|
|
476
|
+
for (const rel of USER_DATA_FILES) {
|
|
477
|
+
const src = join(dataDir, rel);
|
|
478
|
+
if (existsSync(src)) copyTree(src, join(backupDir, rel), copied);
|
|
479
|
+
}
|
|
480
|
+
for (const rel of [...USER_DATA_DIRS, ...dirs]) {
|
|
481
|
+
const src = join(dataDir, rel);
|
|
482
|
+
if (existsSync(src)) copyTree(src, join(backupDir, rel), copied);
|
|
483
|
+
}
|
|
484
|
+
if (copied.length > 0) finishBackup(dataDir, backupDir, reason);
|
|
485
|
+
} catch (error) {
|
|
486
|
+
// Drop only this call's reservation; a failed cleanup must not mask the cause.
|
|
487
|
+
try {
|
|
488
|
+
rmSync(backupDir, { recursive: true, force: true });
|
|
489
|
+
} catch {}
|
|
490
|
+
throw error;
|
|
471
491
|
}
|
|
472
492
|
if (copied.length > 0) {
|
|
473
|
-
finishBackup(dataDir, backupDir, reason);
|
|
474
493
|
pruneBackups(scopeDir, names, basename(backupDir));
|
|
475
494
|
if (process.env.MIXDOG_SETUP_QUIET !== '1') {
|
|
476
495
|
process.stderr.write(`[user-data-backup] ${reason}: copied ${copied.length} file(s) to ${backupDir}\n`);
|
|
@@ -499,7 +518,13 @@ async function copyTreeAsync(src, dst, copied) {
|
|
|
499
518
|
}
|
|
500
519
|
|
|
501
520
|
async function pruneBackupsAsync(scopeDir, names, own) {
|
|
502
|
-
|
|
521
|
+
const completed = [];
|
|
522
|
+
for (const name of names) {
|
|
523
|
+
try {
|
|
524
|
+
if ((await statP(manifestPath(scopeDir, name))).isFile()) completed.push(name);
|
|
525
|
+
} catch {}
|
|
526
|
+
}
|
|
527
|
+
for (const name of backupsToPrune(completed, own)) {
|
|
503
528
|
try {
|
|
504
529
|
await rmP(join(scopeDir, name), { recursive: true, force: true });
|
|
505
530
|
} catch {}
|
|
@@ -512,16 +537,23 @@ export async function backupUserDataAsync(dataDir, reason = 'snapshot') {
|
|
|
512
537
|
const scopeDir = scopeDirFor(dataDir);
|
|
513
538
|
const { backupDir, names } = await reserveBackupDirAsync(scopeDir, reason);
|
|
514
539
|
const copied = [];
|
|
515
|
-
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
const
|
|
521
|
-
|
|
540
|
+
try {
|
|
541
|
+
for (const rel of USER_DATA_FILES) {
|
|
542
|
+
const src = join(dataDir, rel);
|
|
543
|
+
if (existsSync(src)) await copyTreeAsync(src, join(backupDir, rel), copied);
|
|
544
|
+
}
|
|
545
|
+
for (const rel of USER_DATA_DIRS) {
|
|
546
|
+
const src = join(dataDir, rel);
|
|
547
|
+
if (existsSync(src)) await copyTreeAsync(src, join(backupDir, rel), copied);
|
|
548
|
+
}
|
|
549
|
+
if (copied.length > 0) finishBackup(dataDir, backupDir, reason);
|
|
550
|
+
} catch (error) {
|
|
551
|
+
try {
|
|
552
|
+
await rmP(backupDir, { recursive: true, force: true });
|
|
553
|
+
} catch {}
|
|
554
|
+
throw error;
|
|
522
555
|
}
|
|
523
556
|
if (copied.length > 0) {
|
|
524
|
-
finishBackup(dataDir, backupDir, reason);
|
|
525
557
|
await pruneBackupsAsync(scopeDir, names, basename(backupDir));
|
|
526
558
|
if (process.env.MIXDOG_SETUP_QUIET !== '1') {
|
|
527
559
|
process.stderr.write(`[user-data-backup] ${reason}: copied ${copied.length} file(s) to ${backupDir}\n`);
|