mixdog 1.2.6 → 1.3.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/LICENSES/simple-icons-CC0-1.0.txt +30 -0
- package/NOTICE.md +19 -0
- package/package.json +4 -1
- package/scripts/check-model-recommendations.mjs +89 -0
- package/scripts/gate-local.mjs +7 -0
- package/scripts/lib/isolated-test-env.mjs +16 -0
- package/src/agents/reviewer/AGENT.md +4 -2
- package/src/agents/reviewer/agent.json +1 -1
- package/src/defaults/shipped-history.json +7 -0
- package/src/defaults/skills/browser-use/SKILL.md +5 -0
- package/src/defaults/skills/memory-management/SKILL.md +4 -1
- package/src/defaults/skills/setup/references/actions.md +11 -4
- package/src/defaults/skills/setup-guide/SKILL.md +175 -0
- package/src/defaults/skills/setup-guide/model-recommendations.json +333 -0
- package/src/lib/rules-builder.cjs +55 -6
- package/src/rules/agent/AGENT.md +4 -0
- package/src/rules/agent-mode/AGENT_MODE.md +24 -0
- package/src/rules/agent-mode/MIXY.md +21 -0
- package/src/rules/agent-mode/ROUTINE.md +6 -0
- package/src/rules/lead/LEAD.md +7 -3
- package/src/rules/shared/70-delivery.md +3 -0
- package/src/runtime/agent/orchestrator/agent-runtime/room-supervisor-completion.mjs +86 -0
- package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +77 -14
- package/src/runtime/agent/orchestrator/context/role-instructions.mjs +182 -13
- package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +4 -2
- package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +215 -0
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +5 -4
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +10 -1
- package/src/runtime/agent/orchestrator/mcp/client.mjs +115 -11
- package/src/runtime/agent/orchestrator/mcp/oauth-account.mjs +97 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +49 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-credentials.mjs +59 -9
- package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +32 -7
- package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +45 -6
- package/src/runtime/agent/orchestrator/mcp/oauth.mjs +195 -6
- package/src/runtime/agent/orchestrator/mcp/server-connectors.mjs +17 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +10 -3
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +17 -12
- package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +10 -0
- package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +96 -1
- package/src/runtime/agent/orchestrator/runtime-core/context-status-shape.mjs +4 -0
- package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +6 -4
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +12 -0
- package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +1 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-bridge.mjs +12 -6
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +32 -5
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +10 -4
- package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +4 -1
- package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +50 -20
- package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/agent-home.d.mts +4 -0
- package/src/runtime/agent/orchestrator/session/agent-home.mjs +33 -0
- package/src/runtime/agent/orchestrator/session/agent-mode.mjs +62 -0
- package/src/runtime/agent/orchestrator/session/agent-room-supervisor.d.mts +31 -0
- package/src/runtime/agent/orchestrator/session/agent-room-supervisor.mjs +241 -0
- package/src/runtime/agent/orchestrator/session/agent-room.d.mts +162 -0
- package/src/runtime/agent/orchestrator/session/agent-room.mjs +641 -0
- package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +7 -1
- package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +6 -2
- package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +165 -0
- package/src/runtime/agent/orchestrator/session/human-gate/held-message.mjs +95 -0
- package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +221 -0
- package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +195 -0
- package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +167 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +10 -0
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +16 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +74 -15
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +9 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +5 -4
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +13 -3
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-lifecycle-epoch.mjs +0 -8
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +44 -16
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +22 -5
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +39 -4
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +27 -5
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +55 -8
- package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +16 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +3 -2
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/manager/workflow-switch-notice.mjs +58 -0
- package/src/runtime/agent/orchestrator/session/mixy-name.mjs +24 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +32 -5
- package/src/runtime/agent/orchestrator/session/routine-result.d.mts +70 -0
- package/src/runtime/agent/orchestrator/session/routine-result.mjs +277 -0
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/store-record-strict-scan.mjs +28 -9
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +16 -2
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +44 -12
- package/src/runtime/agent/orchestrator/session/store-summary-last-message.mjs +46 -0
- package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +0 -4
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +44 -1
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +113 -5
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +4 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +25 -1
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +6 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +3 -0
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +75 -57
- package/src/runtime/browser-bridge/client.mjs +24 -0
- package/src/runtime/channels/lib/scheduler.mjs +40 -12
- package/src/runtime/channels/lib/schedules-db.mjs +71 -12
- package/src/runtime/channels/lib/webhook/session-dispatch.mjs +36 -16
- package/src/runtime/channels/lib/webhook.mjs +0 -1
- package/src/runtime/channels/lib/webhooks-db.mjs +100 -10
- package/src/runtime/channels/lib/worker-main.mjs +17 -20
- package/src/runtime/computer-bridge/client.mjs +28 -1
- package/src/runtime/connectors/gmail-app-password.mjs +690 -0
- package/src/runtime/connectors/google-mcp.mjs +446 -0
- package/src/runtime/memory/lib/core-memory-access.mjs +69 -0
- package/src/runtime/memory/lib/core-memory-file.mjs +16 -8
- package/src/runtime/memory/lib/core-memory-index.mjs +15 -9
- package/src/runtime/memory/lib/core-memory-management.mjs +10 -4
- package/src/runtime/memory/lib/core-memory-store.mjs +53 -30
- package/src/runtime/memory/lib/core-memory-uniqueness.mjs +10 -6
- package/src/runtime/memory/lib/http-router/session-routes.mjs +3 -2
- package/src/runtime/memory/lib/memory-action-handlers/core-actions.mjs +44 -14
- package/src/runtime/memory/lib/memory-schema/core-and-meta.mjs +11 -0
- package/src/runtime/memory/lib/memory.mjs +7 -1
- package/src/runtime/memory/lib/query-core-recall.mjs +9 -1
- package/src/runtime/memory/lib/query-hybrid-search.mjs +2 -0
- package/src/runtime/memory/lib/tool-call-handler.mjs +2 -0
- package/src/runtime/memory/tool-defs.mjs +5 -0
- package/src/runtime/shared/agent-avatar-sprites.mjs +445 -0
- package/src/runtime/shared/agent-avatar.d.mts +64 -0
- package/src/runtime/shared/agent-avatar.mjs +320 -0
- package/src/runtime/shared/agent-route-config.d.mts +3 -0
- package/src/runtime/shared/agent-route-config.mjs +16 -0
- package/src/runtime/shared/atomic-file.mjs +14 -1
- package/src/runtime/shared/config.mjs +8 -1
- package/src/runtime/shared/connector-accounts.mjs +15 -0
- package/src/runtime/shared/connector-catalog.d.mts +52 -0
- package/src/runtime/shared/connector-catalog.mjs +230 -0
- package/src/runtime/shared/connector-triggers.d.mts +3 -0
- package/src/runtime/shared/connector-triggers.mjs +43 -0
- package/src/runtime/shared/google-oauth-client.mjs +24 -0
- package/src/runtime/shared/held-request-host.d.mts +12 -0
- package/src/runtime/shared/held-request-host.mjs +71 -0
- package/src/runtime/shared/plain-preview.d.mts +1 -0
- package/src/runtime/shared/plain-preview.mjs +28 -0
- package/src/runtime/shared/routine-limits.d.mts +11 -0
- package/src/runtime/shared/routine-limits.mjs +21 -0
- package/src/runtime/shared/routine-trigger.d.mts +10 -0
- package/src/runtime/shared/routine-trigger.mjs +44 -0
- package/src/runtime/shared/schedule-model-ref.mjs +9 -10
- package/src/runtime/shared/slow-span.mjs +4 -12
- package/src/runtime/shared/statusline/statusline-segments.mjs +13 -1
- package/src/runtime/shared/transcript-metadata.mjs +10 -0
- package/src/runtime/shared/turn-changes.mjs +0 -8
- package/src/runtime/shared/usage-readout-totals.mjs +56 -0
- package/src/runtime/shared/user-data-guard.mjs +114 -36
- package/src/session-runtime/agent-home-delivery.mjs +459 -0
- package/src/session-runtime/agent-profile-changes.mjs +21 -0
- package/src/session-runtime/agent-room-delivery.mjs +92 -0
- package/src/session-runtime/agent-task-sessions.mjs +200 -0
- package/src/session-runtime/boot/apis.mjs +50 -18
- package/src/session-runtime/boot/facade.mjs +5 -8
- package/src/session-runtime/boot/infrastructure.mjs +28 -17
- package/src/session-runtime/boot/tools.mjs +3 -0
- package/src/session-runtime/channel-config-api.mjs +53 -5
- package/src/session-runtime/cwd-plugins/core-memory-context.mjs +36 -20
- package/src/session-runtime/cwd-plugins/plugin-status.mjs +6 -1
- package/src/session-runtime/held-requests.mjs +228 -0
- package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +121 -0
- package/src/session-runtime/internal-tool-executor/feature-tools.mjs +5 -1
- package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +31 -2
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +31 -2
- package/src/session-runtime/internal-tool-executor.mjs +2 -0
- package/src/session-runtime/lifecycle/session-catalog.mjs +9 -0
- package/src/session-runtime/lifecycle/session-switch/resume.mjs +4 -0
- package/src/session-runtime/lifecycle/shared.mjs +6 -0
- package/src/session-runtime/mcp-glue/server-catalog.mjs +8 -0
- package/src/session-runtime/mcp-glue/server-input.mjs +3 -0
- package/src/session-runtime/mcp-oauth-login.mjs +112 -15
- package/src/session-runtime/model-route/set-route.mjs +14 -3
- package/src/session-runtime/owned-automation-run.mjs +229 -0
- package/src/session-runtime/resource-api.mjs +5 -0
- package/src/session-runtime/resource-mcp-api.mjs +107 -13
- package/src/session-runtime/resource-skills-api.mjs +12 -0
- package/src/session-runtime/room-member-gate.mjs +48 -0
- package/src/session-runtime/routine-home-turn.mjs +156 -0
- package/src/session-runtime/routine-owner.mjs +48 -0
- package/src/session-runtime/schedule-session-run.mjs +30 -64
- package/src/session-runtime/services/agent-tool/helpers.mjs +10 -0
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +19 -4
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +2 -0
- package/src/session-runtime/services/channel-admin.mjs +93 -24
- package/src/session-runtime/services/memory-access.mjs +37 -0
- package/src/session-runtime/services/provider-admin.mjs +74 -39
- package/src/session-runtime/services/schedule-access.mjs +91 -0
- package/src/session-runtime/session-create/session-options.mjs +34 -7
- package/src/session-runtime/session-hooks.mjs +16 -8
- package/src/session-runtime/setup-tool/executor.mjs +68 -6
- package/src/session-runtime/setup-tool/extended-actions.mjs +72 -14
- package/src/session-runtime/setup-tool/settings-contract.mjs +8 -6
- package/src/session-runtime/setup-tool/tool-defs.mjs +8 -0
- package/src/session-runtime/skills-api.mjs +16 -1
- package/src/session-runtime/tool-policy-refresh.mjs +238 -29
- package/src/session-runtime/turn/session-ops.mjs +8 -1
- package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +10 -2
- package/src/session-runtime/turn/turn-run/turn-prepare.mjs +3 -0
- package/src/session-runtime/webhook-session-run.mjs +49 -67
- package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +239 -0
- package/src/session-runtime/workflow-agents-api/agent-editor.mjs +556 -22
- package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +11 -1
- package/src/session-runtime/workflow-agents-api.mjs +4 -1
- package/src/standalone/daemon-agent-control.mjs +1 -8
- package/src/standalone/daemon-session-control.mjs +11 -2
- package/src/standalone/daemon.mjs +18 -4
- package/src/standalone/session-protocol.mjs +27 -0
- package/src/standalone/session-service/session-calls/session-turns.mjs +9 -0
- package/src/standalone/session-service.mjs +19 -0
- package/src/tui/app/slash-commands.mjs +20 -0
- package/src/tui/dist/index.mjs +164 -86
- package/src/tui/session/context-state.mjs +6 -1
- package/src/tui/session/notice-surface.mjs +35 -4
- package/src/tui/session/queue-helpers.mjs +2 -1
- package/src/tui/session/session-api/extensions.mjs +4 -0
- package/src/tui/session/session-api/intake/submission.mjs +5 -0
- package/src/tui/session/session-api/integrations.mjs +8 -0
- package/src/tui/session/session-api/lifecycle/session-reset.mjs +13 -3
- package/src/tui/session/session-api/lifecycle.mjs +17 -1
- package/src/tui/session/session-api/routes.mjs +48 -3
- package/src/tui/session/session-flow/drain.mjs +12 -3
- package/src/tui/session/session-flow/queue/entry-shape.mjs +2 -1
- package/src/tui/session/session-flow/queue/take-entries.mjs +3 -2
- package/src/tui/session/session-flow/steering.mjs +58 -24
- package/src/tui/session/tool-approval.mjs +26 -5
- package/src/tui/session/transcript-intake.mjs +6 -2
- package/src/tui/session/transcript-spill.mjs +5 -1
- package/src/tui/session/tui-steering-persist.mjs +65 -42
- package/src/tui/session/turn-live-call.mjs +56 -0
- package/src/tui/session/turn.mjs +22 -6
- package/src/session-runtime/automation-agents.mjs +0 -94
- package/src/session-runtime/automation-workflow.mjs +0 -59
|
@@ -27,7 +27,6 @@ import {
|
|
|
27
27
|
export const TURN_CHANGE_FILE_PATCH_MAX_BYTES = 8 * 1024 * 1024;
|
|
28
28
|
const APPLY_TIMEOUT_MS = 60_000;
|
|
29
29
|
const APPLY_OUTPUT_MAX_BYTES = 4 * 1024 * 1024;
|
|
30
|
-
const STATUSES = new Set(['A', 'M', 'D', 'R']);
|
|
31
30
|
// Characters a patch header cannot carry unquoted; such a path is recorded but
|
|
32
31
|
// never replayed.
|
|
33
32
|
const UNSAFE_PATH_CHARACTERS = /[\t\n\r"\0]/;
|
|
@@ -37,13 +36,6 @@ export function changePathKey(value) {
|
|
|
37
36
|
return process.platform === 'win32' ? text.toLowerCase() : text;
|
|
38
37
|
}
|
|
39
38
|
|
|
40
|
-
/** git status letter → record status (copies create a new file). */
|
|
41
|
-
export function normalizeChangeStatus(code) {
|
|
42
|
-
const letter = String(code || 'M').charAt(0);
|
|
43
|
-
if (STATUSES.has(letter)) return letter;
|
|
44
|
-
return letter === 'C' ? 'A' : 'M';
|
|
45
|
-
}
|
|
46
|
-
|
|
47
39
|
/** A root-relative forward-slash path that a patch may name, or ''. */
|
|
48
40
|
function safeChangePath(value) {
|
|
49
41
|
const text = clean(value).replace(/\\/g, '/');
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Session-cumulative figures behind the composer's usage readout, kept on the
|
|
2
|
+
// session record (`session.usageReadout`) so they survive restart, reload and
|
|
3
|
+
// compaction. One fold per model call of the session itself; calls of other
|
|
4
|
+
// sessions (delegated sub-agents have their own records) never enter.
|
|
5
|
+
//
|
|
6
|
+
// - cache hit = cacheRead / (input + cacheRead + cacheWrite) over every call
|
|
7
|
+
// whose prompt was measured. A call that could not measure its prompt
|
|
8
|
+
// (promptKnown === false) has unknown zeros, so it adds nothing.
|
|
9
|
+
// - speed = output tokens over generation ms (first visible token → end of the
|
|
10
|
+
// response) of the calls long enough to time. A reply too short says nothing
|
|
11
|
+
// about the model's speed, so each call is held to the floors below BEFORE it
|
|
12
|
+
// is summed.
|
|
13
|
+
// - total tokens = input + cacheRead + cacheWrite + output, from the same sums.
|
|
14
|
+
export const MIN_SPEED_OUTPUT_TOKENS = 20;
|
|
15
|
+
export const MIN_SPEED_MS = 500;
|
|
16
|
+
|
|
17
|
+
function num(value) {
|
|
18
|
+
const n = Number(value);
|
|
19
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The zeroed totals record. */
|
|
23
|
+
export function emptyUsageReadout() {
|
|
24
|
+
return { input: 0, cacheRead: 0, cacheWrite: 0, output: 0, speedOutputTokens: 0, speedMs: 0 };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Fold one `onUsageDelta` event (and the call's generation ms, 0 when it was
|
|
29
|
+
* not timed) into the totals; returns a new record, never mutates `totals`.
|
|
30
|
+
* Providers differ on whether input counts cache; the prompt total does not,
|
|
31
|
+
* so fresh input is the prompt less its cache read/write.
|
|
32
|
+
*/
|
|
33
|
+
export function foldUsageReadout(totals, delta = {}, generationMs = 0) {
|
|
34
|
+
const base = totals || emptyUsageReadout();
|
|
35
|
+
const next = { ...base };
|
|
36
|
+
if (delta.promptKnown !== false) {
|
|
37
|
+
const cacheRead = num(delta.deltaCachedRead);
|
|
38
|
+
const cacheWrite = num(delta.deltaCacheWrite);
|
|
39
|
+
const prompt = num(delta.deltaPrompt) || num(delta.deltaInput) + cacheRead + cacheWrite;
|
|
40
|
+
next.input = num(base.input) + Math.max(0, prompt - cacheRead - cacheWrite);
|
|
41
|
+
next.cacheRead = num(base.cacheRead) + cacheRead;
|
|
42
|
+
next.cacheWrite = num(base.cacheWrite) + cacheWrite;
|
|
43
|
+
}
|
|
44
|
+
const output = num(delta.deltaOutput);
|
|
45
|
+
// Every call's output enters the session's total tokens, timed or not. A
|
|
46
|
+
// record saved before the field existed has no output sum to extend, and a
|
|
47
|
+
// partial one would pass for the whole: it stays without until the session
|
|
48
|
+
// starts over.
|
|
49
|
+
if (Number.isFinite(base.output)) next.output = base.output + output;
|
|
50
|
+
const ms = num(generationMs);
|
|
51
|
+
if (ms >= MIN_SPEED_MS && output >= MIN_SPEED_OUTPUT_TOKENS) {
|
|
52
|
+
next.speedOutputTokens = num(base.speedOutputTokens) + output;
|
|
53
|
+
next.speedMs = num(base.speedMs) + ms;
|
|
54
|
+
}
|
|
55
|
+
return next;
|
|
56
|
+
}
|
|
@@ -61,25 +61,92 @@ function scopeDirFor(dataDir) {
|
|
|
61
61
|
let lastBackupMs = 0;
|
|
62
62
|
let backupSeq = 0;
|
|
63
63
|
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
//
|
|
64
|
+
// `<iso-ms>-<10-digit seq>-<reason>`; an older build's name has no sequence.
|
|
65
|
+
const BACKUP_NAME_RE = /^(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z-(?:(\d{10})-)?/;
|
|
66
|
+
|
|
67
|
+
// The newest backup order among `names` (time, then sequence). An older
|
|
68
|
+
// build's name sorts after every sequenced name of its millisecond, so its
|
|
69
|
+
// sequence counts as unbounded. Names that are no backup's are ignored.
|
|
70
|
+
function newestBackupOrder(names) {
|
|
71
|
+
let ms = 0;
|
|
72
|
+
let seq = -1;
|
|
73
|
+
for (const name of names) {
|
|
74
|
+
const match = BACKUP_NAME_RE.exec(name);
|
|
75
|
+
if (!match) continue;
|
|
76
|
+
const time = Date.parse(`${match[1]}T${match[2]}:${match[3]}:${match[4]}.${match[5]}Z`);
|
|
77
|
+
if (!Number.isFinite(time)) continue;
|
|
78
|
+
const order = match[6] === undefined ? Number.POSITIVE_INFINITY : Number(match[6]);
|
|
79
|
+
if (time > ms || (time === ms && order > seq)) {
|
|
80
|
+
ms = time;
|
|
81
|
+
seq = order;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return { ms, seq };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// A name that sorts after every backup in `names`, whichever process wrote
|
|
88
|
+
// them and whatever its clock said: the backup a restore picks as the newest
|
|
89
|
+
// is then the one written last, and pruning never takes it.
|
|
90
|
+
function nextBackupName(names, reason) {
|
|
91
|
+
const newest = newestBackupOrder(names);
|
|
92
|
+
lastBackupMs = Math.max(Date.now(), lastBackupMs, newest.ms);
|
|
93
|
+
if (lastBackupMs === newest.ms) {
|
|
94
|
+
if (newest.seq === Number.POSITIVE_INFINITY) lastBackupMs += 1;
|
|
95
|
+
else backupSeq = Math.max(backupSeq, newest.seq + 1);
|
|
96
|
+
}
|
|
97
|
+
const time = new Date(lastBackupMs).toISOString().replace(/[:.]/g, '-');
|
|
98
|
+
return `${time}-${String(backupSeq++).padStart(10, '0')}-${safeReason(reason)}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// The scope's backup directory names. A reservation lists the scope first and
|
|
102
|
+
// a listing failure throws: an order guessed without it could sort before the
|
|
103
|
+
// newest backup.
|
|
104
|
+
function scopeBackupNames(entries) {
|
|
105
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Claims the next name with an exclusive mkdir (EEXIST retries after it).
|
|
109
|
+
// Returns the claimed dir and the scope's names including it, which pruning
|
|
110
|
+
// reuses instead of listing the scope again.
|
|
68
111
|
function reserveBackupDir(scopeDir, reason) {
|
|
69
112
|
mkdirSync(scopeDir, { recursive: true });
|
|
113
|
+
const names = scopeBackupNames(readdirSync(scopeDir, { withFileTypes: true }));
|
|
114
|
+
for (;;) {
|
|
115
|
+
const name = nextBackupName(names, reason);
|
|
116
|
+
names.push(name);
|
|
117
|
+
try {
|
|
118
|
+
mkdirSync(join(scopeDir, name));
|
|
119
|
+
return { backupDir: join(scopeDir, name), names };
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function reserveBackupDirAsync(scopeDir, reason) {
|
|
127
|
+
await mkdirP(scopeDir, { recursive: true });
|
|
128
|
+
const names = scopeBackupNames(await readdirP(scopeDir, { withFileTypes: true }));
|
|
70
129
|
for (;;) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const backupDir = join(scopeDir, `${time}-${String(backupSeq++).padStart(10, '0')}-${safeReason(reason)}`);
|
|
130
|
+
const name = nextBackupName(names, reason);
|
|
131
|
+
names.push(name);
|
|
74
132
|
try {
|
|
75
|
-
|
|
76
|
-
return backupDir;
|
|
133
|
+
await mkdirP(join(scopeDir, name));
|
|
134
|
+
return { backupDir: join(scopeDir, name), names };
|
|
77
135
|
} catch (error) {
|
|
78
136
|
if (error?.code !== 'EEXIST') throw error;
|
|
79
137
|
}
|
|
80
138
|
}
|
|
81
139
|
}
|
|
82
140
|
|
|
141
|
+
// The names past the newest `keep`, never the just-written backup.
|
|
142
|
+
function backupsToPrune(names, own, keep = 40) {
|
|
143
|
+
return [...names]
|
|
144
|
+
.sort()
|
|
145
|
+
.reverse()
|
|
146
|
+
.slice(keep)
|
|
147
|
+
.filter((name) => name !== own);
|
|
148
|
+
}
|
|
149
|
+
|
|
83
150
|
function safeReason(reason) {
|
|
84
151
|
return (
|
|
85
152
|
String(reason || 'snapshot')
|
|
@@ -197,14 +264,30 @@ function isStructurallyCompleteMixdogConfigBackup(parsed) {
|
|
|
197
264
|
return true;
|
|
198
265
|
}
|
|
199
266
|
|
|
200
|
-
|
|
201
|
-
|
|
267
|
+
// A backup that is absent, not a backup, or not JSON is no backup; any other
|
|
268
|
+
// failure (EIO, EACCES, EMFILE…) is transient and must not pass for one.
|
|
269
|
+
function isAbsentBackupError(error) {
|
|
270
|
+
return error instanceof SyntaxError || error?.code === 'ENOENT' || error?.code === 'ENOTDIR';
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Backup directory names in `dir`, newest first; empty when absent, throws on other failures. */
|
|
274
|
+
function listBackupDirsNewestFirst(dir) {
|
|
202
275
|
try {
|
|
203
276
|
return readdirSync(dir, { withFileTypes: true })
|
|
204
277
|
.filter((entry) => entry.isDirectory())
|
|
205
278
|
.map((entry) => entry.name)
|
|
206
279
|
.sort()
|
|
207
280
|
.reverse();
|
|
281
|
+
} catch (error) {
|
|
282
|
+
if (isAbsentBackupError(error)) return [];
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Backup directory names in `dir`, newest first; empty when unreadable. */
|
|
288
|
+
function backupDirsNewestFirst(dir) {
|
|
289
|
+
try {
|
|
290
|
+
return listBackupDirsNewestFirst(dir);
|
|
208
291
|
} catch {
|
|
209
292
|
return [];
|
|
210
293
|
}
|
|
@@ -213,7 +296,8 @@ function backupDirsNewestFirst(dir) {
|
|
|
213
296
|
/**
|
|
214
297
|
* A backup is restorable only when its manifest names this data dir and this
|
|
215
298
|
* incarnation of it. Legacy unscoped backups carry no manifest, so they stay
|
|
216
|
-
* on disk but never match.
|
|
299
|
+
* on disk but never match. A transient read failure throws: skipping the
|
|
300
|
+
* backup would restore an older one instead.
|
|
217
301
|
*/
|
|
218
302
|
function readRestorableConfig(backupDir, dataDirKey, instanceId) {
|
|
219
303
|
try {
|
|
@@ -222,20 +306,21 @@ function readRestorableConfig(backupDir, dataDirKey, instanceId) {
|
|
|
222
306
|
if (manifest.instanceId !== instanceId) return null;
|
|
223
307
|
const parsed = JSON.parse(readFileSync(join(backupDir, 'mixdog-config.json'), 'utf8'));
|
|
224
308
|
return isStructurallyCompleteMixdogConfigBackup(parsed) ? parsed : null;
|
|
225
|
-
} catch {
|
|
226
|
-
return null;
|
|
309
|
+
} catch (error) {
|
|
310
|
+
if (isAbsentBackupError(error)) return null;
|
|
311
|
+
throw error;
|
|
227
312
|
}
|
|
228
313
|
}
|
|
229
314
|
|
|
230
315
|
function findRestorableConfig(dataDir, instanceId) {
|
|
231
316
|
const key = normalizeDataDir(dataDir);
|
|
232
317
|
const scopeDir = scopeDirFor(dataDir);
|
|
233
|
-
for (const name of
|
|
318
|
+
for (const name of listBackupDirsNewestFirst(scopeDir)) {
|
|
234
319
|
const parsed = readRestorableConfig(join(scopeDir, name), key, instanceId);
|
|
235
320
|
if (parsed) return parsed;
|
|
236
321
|
}
|
|
237
322
|
const root = getBackupRoot();
|
|
238
|
-
for (const name of
|
|
323
|
+
for (const name of listBackupDirsNewestFirst(root)) {
|
|
239
324
|
if (name.startsWith(SCOPE_PREFIX)) continue;
|
|
240
325
|
const parsed = readRestorableConfig(join(root, name), key, instanceId);
|
|
241
326
|
if (parsed) return parsed;
|
|
@@ -245,13 +330,15 @@ function findRestorableConfig(dataDir, instanceId) {
|
|
|
245
330
|
|
|
246
331
|
// Per-process restore lookup, keyed by data dir. A hit is reused while the
|
|
247
332
|
// backup root and the marker's instanceId are unchanged; this process's own
|
|
248
|
-
// backups invalidate it
|
|
333
|
+
// backups invalidate it, and a `fresh` lookup (an RMW write, which another
|
|
334
|
+
// process's newer backup may precede) always rescans.
|
|
249
335
|
const restoreLookupCache = new Map();
|
|
250
336
|
|
|
251
337
|
/**
|
|
252
338
|
* Newest structurally complete mixdog-config.json backed up from THIS data
|
|
253
339
|
* dir (skips degenerate single-section snapshots from a prior failed RMW).
|
|
254
340
|
* Never another data dir's config, and nothing for an uninitialized one.
|
|
341
|
+
* Throws on a transient read failure, which is never cached.
|
|
255
342
|
*/
|
|
256
343
|
export function loadLatestMixdogConfigFromBackup(dataDir, options) {
|
|
257
344
|
const instanceId = readInitInstanceId(dataDir);
|
|
@@ -259,7 +346,8 @@ export function loadLatestMixdogConfigFromBackup(dataDir, options) {
|
|
|
259
346
|
const key = normalizeDataDir(dataDir);
|
|
260
347
|
const root = getBackupRoot();
|
|
261
348
|
let hit = restoreLookupCache.get(key);
|
|
262
|
-
if (!hit || hit.root !== root || hit.instanceId !== instanceId) {
|
|
349
|
+
if (options?.fresh || !hit || hit.root !== root || hit.instanceId !== instanceId) {
|
|
350
|
+
restoreLookupCache.delete(key);
|
|
263
351
|
hit = { root, instanceId, config: findRestorableConfig(dataDir, instanceId) };
|
|
264
352
|
restoreLookupCache.set(key, hit);
|
|
265
353
|
}
|
|
@@ -297,8 +385,8 @@ function copyTree(src, dst, copied) {
|
|
|
297
385
|
}
|
|
298
386
|
|
|
299
387
|
/** Prune one data dir's scope only; other scopes and legacy entries stay. */
|
|
300
|
-
function pruneBackups(scopeDir,
|
|
301
|
-
for (const name of
|
|
388
|
+
function pruneBackups(scopeDir, names, own) {
|
|
389
|
+
for (const name of backupsToPrune(names, own)) {
|
|
302
390
|
try {
|
|
303
391
|
rmSync(join(scopeDir, name), { recursive: true, force: true });
|
|
304
392
|
} catch {}
|
|
@@ -368,7 +456,7 @@ export function backupUserData(dataDir, reason = 'snapshot') {
|
|
|
368
456
|
if (skipBackups()) return { dir: null, copied: [] };
|
|
369
457
|
if (!dataDir || !existsSync(dataDir)) return { dir: null, copied: [] };
|
|
370
458
|
const scopeDir = scopeDirFor(dataDir);
|
|
371
|
-
const backupDir = reserveBackupDir(scopeDir, reason);
|
|
459
|
+
const { backupDir, names } = reserveBackupDir(scopeDir, reason);
|
|
372
460
|
const copied = [];
|
|
373
461
|
for (const rel of USER_DATA_FILES) {
|
|
374
462
|
const src = join(dataDir, rel);
|
|
@@ -380,7 +468,7 @@ export function backupUserData(dataDir, reason = 'snapshot') {
|
|
|
380
468
|
}
|
|
381
469
|
if (copied.length > 0) {
|
|
382
470
|
finishBackup(dataDir, backupDir, reason);
|
|
383
|
-
pruneBackups(scopeDir);
|
|
471
|
+
pruneBackups(scopeDir, names, basename(backupDir));
|
|
384
472
|
if (process.env.MIXDOG_SETUP_QUIET !== '1') {
|
|
385
473
|
process.stderr.write(`[user-data-backup] ${reason}: copied ${copied.length} file(s) to ${backupDir}\n`);
|
|
386
474
|
}
|
|
@@ -408,18 +496,8 @@ async function copyTreeAsync(src, dst, copied) {
|
|
|
408
496
|
copied.push(dst);
|
|
409
497
|
}
|
|
410
498
|
|
|
411
|
-
async function pruneBackupsAsync(scopeDir,
|
|
412
|
-
|
|
413
|
-
try {
|
|
414
|
-
entries = (await readdirP(scopeDir, { withFileTypes: true }))
|
|
415
|
-
.filter((entry) => entry.isDirectory())
|
|
416
|
-
.map((entry) => entry.name)
|
|
417
|
-
.sort()
|
|
418
|
-
.reverse();
|
|
419
|
-
} catch {
|
|
420
|
-
return;
|
|
421
|
-
}
|
|
422
|
-
for (const name of entries.slice(keep)) {
|
|
499
|
+
async function pruneBackupsAsync(scopeDir, names, own) {
|
|
500
|
+
for (const name of backupsToPrune(names, own)) {
|
|
423
501
|
try {
|
|
424
502
|
await rmP(join(scopeDir, name), { recursive: true, force: true });
|
|
425
503
|
} catch {}
|
|
@@ -430,7 +508,7 @@ export async function backupUserDataAsync(dataDir, reason = 'snapshot') {
|
|
|
430
508
|
if (skipBackups()) return { dir: null, copied: [] };
|
|
431
509
|
if (!dataDir || !existsSync(dataDir)) return { dir: null, copied: [] };
|
|
432
510
|
const scopeDir = scopeDirFor(dataDir);
|
|
433
|
-
const backupDir =
|
|
511
|
+
const { backupDir, names } = await reserveBackupDirAsync(scopeDir, reason);
|
|
434
512
|
const copied = [];
|
|
435
513
|
for (const rel of USER_DATA_FILES) {
|
|
436
514
|
const src = join(dataDir, rel);
|
|
@@ -442,7 +520,7 @@ export async function backupUserDataAsync(dataDir, reason = 'snapshot') {
|
|
|
442
520
|
}
|
|
443
521
|
if (copied.length > 0) {
|
|
444
522
|
finishBackup(dataDir, backupDir, reason);
|
|
445
|
-
await pruneBackupsAsync(scopeDir);
|
|
523
|
+
await pruneBackupsAsync(scopeDir, names, basename(backupDir));
|
|
446
524
|
if (process.env.MIXDOG_SETUP_QUIET !== '1') {
|
|
447
525
|
process.stderr.write(`[user-data-backup] ${reason}: copied ${copied.length} file(s) to ${backupDir}\n`);
|
|
448
526
|
}
|