mixdog 1.0.7 → 1.0.9
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 +2 -1
- package/scripts/lib/run-node-tests.mjs +13 -0
- package/scripts/session-efficiency-diag.mjs +283 -0
- package/src/defaults/skills/computer-use/SKILL.md +3 -3
- package/src/defaults/skills/local-provider/SKILL.md +2 -1
- package/src/defaults/skills/setup/SKILL.md +4 -4
- package/src/defaults/skills/setup/references/actions.md +1 -3
- package/src/defaults/skills/setup/references/surfaces.md +2 -3
- package/src/headless-exec.mjs +1 -0
- package/src/rules/shared/70-delivery.md +2 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +10 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +8 -4
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request.mjs +10 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +18 -3
- package/src/runtime/agent/orchestrator/providers/client-version-store.mjs +50 -0
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +27 -4
- package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +9 -5
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +18 -4
- package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-models.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/npm-cli-version.mjs +25 -2
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +8 -0
- package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +17 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/retry-classification.mjs +16 -0
- package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +0 -20
- package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +12 -10
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +15 -3
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +23 -9
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +15 -6
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +7 -0
- package/src/runtime/channels/lib/voice-transcription.mjs +30 -4
- package/src/runtime/computer-bridge/action-schema.mjs +3 -3
- package/src/runtime/computer-bridge/actions.mjs +4 -0
- package/src/runtime/computer-bridge/bridge-env.test-support.mjs +22 -0
- package/src/runtime/computer-bridge/client.mjs +10 -183
- package/src/runtime/computer-bridge/core-actions.mjs +2 -1
- package/src/runtime/computer-bridge/error-recovery.mjs +46 -34
- package/src/runtime/computer-bridge/result-canonical.mjs +181 -0
- package/src/runtime/local-provider/asset-installer.mjs +20 -10
- package/src/runtime/local-provider/catalog.mjs +4 -7
- package/src/runtime/local-provider/data/manifest.json +14 -1
- package/src/runtime/local-provider/hardware.mjs +36 -5
- package/src/runtime/local-provider/resumable-installations.mjs +2 -2
- package/src/runtime/local-provider/server.mjs +1 -1
- package/src/runtime/memory/index.mjs +2 -4
- package/src/runtime/memory/lib/core-memory-file.mjs +3 -8
- package/src/runtime/memory/lib/core-memory-store.mjs +14 -61
- package/src/runtime/memory/lib/cycle-scheduler/backlog-probe.mjs +5 -9
- package/src/runtime/memory/lib/cycle1/cycle1-plan.mjs +7 -0
- package/src/runtime/memory/lib/cycle1/cycle1-rows.mjs +43 -16
- package/src/runtime/memory/lib/cycle1/cycle1-window.mjs +11 -2
- package/src/runtime/memory/lib/embedding-provider.mjs +2 -5
- package/src/runtime/memory/lib/embedding-reindex.mjs +1 -5
- package/src/runtime/memory/lib/http-router/lifecycle-routes.mjs +0 -1
- package/src/runtime/memory/lib/memory-action-handlers/maintenance-actions.mjs +1 -1
- package/src/runtime/memory/lib/memory-schema/core-and-meta.mjs +4 -22
- package/src/runtime/memory/lib/memory.mjs +79 -32
- package/src/runtime/memory/lib/query-maintenance-handlers.mjs +0 -2
- package/src/runtime/office/com/com-adapter.mjs +3 -0
- package/src/runtime/office/pdf/pdf-adapter.mjs +50 -14
- package/src/runtime/office/pdf/pdf-render.mjs +2 -6
- package/src/runtime/office/pdf/pdf-security.mjs +8 -2
- package/src/runtime/office/portable/font-provisioner.mjs +28 -0
- package/src/runtime/office/portable/portable-docx-edits.mjs +28 -1
- package/src/runtime/office/portable/portable-soffice.mjs +44 -7
- package/src/runtime/shared/llm/anthropic-betas.mjs +7 -1
- package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +1 -1
- package/src/runtime/shared/llm/default-context-window.mjs +42 -0
- package/src/runtime/shared/llm/model-catalog-projection.mjs +2 -0
- package/src/runtime/shared/llm/model-catalog.mjs +2 -0
- package/src/runtime/shared/plugin-paths.mjs +9 -2
- package/src/runtime/shared/worker-exec-argv.mjs +25 -0
- package/src/runtime/web-search/lib/formatter.mjs +43 -18
- package/src/session-runtime/automation-agents.mjs +88 -0
- package/src/session-runtime/automation-workflow.mjs +19 -6
- package/src/session-runtime/boot/apis.mjs +0 -1
- package/src/session-runtime/cwd-plugins/core-memory-context.mjs +14 -12
- package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +4 -25
- package/src/session-runtime/model-recency.mjs +10 -2
- package/src/session-runtime/model-route/set-route.mjs +7 -1
- package/src/session-runtime/resource-api.mjs +8 -2
- package/src/session-runtime/resource-plugins-api.mjs +14 -2
- package/src/session-runtime/route-state.mjs +6 -2
- package/src/session-runtime/schedule-session-run.mjs +7 -1
- package/src/session-runtime/services/agent-tool/render.mjs +0 -5
- package/src/session-runtime/settings-builtin-tools-api.mjs +0 -16
- package/src/session-runtime/setup-tool/executor.mjs +7 -13
- package/src/session-runtime/setup-tool/tool-defs.mjs +2 -3
- package/src/session-runtime/tool-policy-refresh.mjs +0 -2
- package/src/session-runtime/webhook-session-run.mjs +10 -11
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +5 -0
- package/src/standalone/daemon-agent-control.mjs +8 -1
- package/src/standalone/daemon.mjs +4 -0
- package/src/tui/dist/index.mjs +3 -2
- package/src/tui/session/agent-job-feed/notification-router.mjs +7 -0
- package/src/tui/session/live-share.mjs +16 -1
- package/src/tui/session/notification-plan.mjs +4 -0
- package/src/session-runtime/bridge-first-use-gate.mjs +0 -72
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mixdog",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Open-source coding agent that matches Codex on Terminal-Bench 2.1 with 63% fewer tokens.",
|
|
@@ -106,6 +106,7 @@
|
|
|
106
106
|
"bench:office:quality:live": "node src/runtime/office/bench/quality-live-benchmark.mjs",
|
|
107
107
|
"diag:sessions": "node scripts/session-diag.mjs",
|
|
108
108
|
"diag:tools": "node scripts/tool-efficiency-diag.mjs",
|
|
109
|
+
"diag:efficiency": "node scripts/session-efficiency-diag.mjs",
|
|
109
110
|
"diag:sweep": "node scripts/session-sweep.mjs",
|
|
110
111
|
"bench:session": "node scripts/session-bench.mjs",
|
|
111
112
|
"bench:task": "node scripts/task-bench.mjs",
|
|
@@ -13,6 +13,8 @@ const SUMMARY_REPORTER = new URL('./test-summary-reporter.mjs', import.meta.url)
|
|
|
13
13
|
// instead of dying with spawn ENAMETOOLONG before any test runs.
|
|
14
14
|
export const ARG_BUDGET = process.platform === 'win32' ? 30_000 : 1_000_000;
|
|
15
15
|
|
|
16
|
+
const HEAP_FLAG = /^--(?:max-old-space-size|max-semi-space-size)=\d+$/;
|
|
17
|
+
|
|
16
18
|
// An argument costs its own characters plus the separator and its quotes.
|
|
17
19
|
const argCost = (arg) => arg.length + 3;
|
|
18
20
|
|
|
@@ -145,6 +147,12 @@ export async function runNodeTests(
|
|
|
145
147
|
argCost('.999'); // the per-batch log suffix
|
|
146
148
|
const batches = chunkFileArgs(fileArgs, Math.max(argBudget - fixedCost, 1));
|
|
147
149
|
const coverageDir = coverage ? await mkdtemp(join(tmpdir(), 'mixdog-v8-coverage-')) : '';
|
|
150
|
+
// Node 24's test runner rebuilds each test process's execArgv from its option
|
|
151
|
+
// table and drops V8 flags on the way, so a heap cap passed to the runner no
|
|
152
|
+
// longer reached a test (a 128 MB cap ran with the default 4 GB). The
|
|
153
|
+
// environment still reaches every test process.
|
|
154
|
+
const heapOptions = nodeArgs.filter((arg) => HEAP_FLAG.test(arg));
|
|
155
|
+
const nodeOptions = [process.env.NODE_OPTIONS, ...heapOptions].filter(Boolean).join(' ');
|
|
148
156
|
// Failure records travel by environment, not argv: the command line is
|
|
149
157
|
// already budgeted, and a third reporter would warn about listeners.
|
|
150
158
|
const childEnv = (recordsPath) => ({
|
|
@@ -152,11 +160,16 @@ export async function runNodeTests(
|
|
|
152
160
|
TEMP: scratchDir,
|
|
153
161
|
TMP: scratchDir,
|
|
154
162
|
TMPDIR: scratchDir,
|
|
163
|
+
// A fixture must never write into the signed-in app's data (~/.mixdog):
|
|
164
|
+
// schedule/webhook logs, design-library bindings and the like resolve the
|
|
165
|
+
// default data dir. The desktop preload already does this per process.
|
|
166
|
+
MIXDOG_DATA_DIR: join(scratchDir, 'data'),
|
|
155
167
|
// Only the immutable validator binary is shared; fixture data stays private.
|
|
156
168
|
// A gate supplies one directory across lanes; a standalone run owns its own.
|
|
157
169
|
MIXDOG_TEST_OOXML_CACHE_DIR: process.env.MIXDOG_TEST_OOXML_CACHE_DIR || join(scratchDir, 'ooxml-cache'),
|
|
158
170
|
[FAILURE_RECORDS_ENV]: recordsPath,
|
|
159
171
|
...(coverageDir ? { NODE_V8_COVERAGE: coverageDir } : {}),
|
|
172
|
+
...(nodeOptions ? { NODE_OPTIONS: nodeOptions } : {}),
|
|
160
173
|
});
|
|
161
174
|
let status = 0;
|
|
162
175
|
const failures = [];
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Token-efficiency diag over the stored session transcripts. The trace-based
|
|
3
|
+
// diags (session-diag, tool-efficiency-diag) need agent-trace.jsonl, which a
|
|
4
|
+
// shipped build does not write; the session store is always there.
|
|
5
|
+
// Usage: node scripts/session-efficiency-diag.mjs [--since 5d] [--data-dir <dir>] [--agent lead] [--json]
|
|
6
|
+
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
7
|
+
import { homedir } from 'node:os';
|
|
8
|
+
import { join, resolve } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { argValue, hasFlag } from './lib/cli-args.mjs';
|
|
11
|
+
import { parseSince } from './lib/parse-since.mjs';
|
|
12
|
+
import { DEFAULT_SHELL_AUTO_BACKGROUND_MS } from '../src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs';
|
|
13
|
+
|
|
14
|
+
const CONTEXT_BUCKETS = [50_000, 100_000, 200_000, 300_000, 500_000, Infinity];
|
|
15
|
+
// Fallback for a session that never recorded a provider context size.
|
|
16
|
+
const FALLBACK_TOKENS_PER_BYTE = 0.3;
|
|
17
|
+
|
|
18
|
+
const size = (value) => {
|
|
19
|
+
if (value == null) return 0;
|
|
20
|
+
return typeof value === 'string' ? value.length : JSON.stringify(value).length;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function replayBytes(message) {
|
|
24
|
+
let bytes = 0;
|
|
25
|
+
for (const item of message.providerReplay?.items || []) {
|
|
26
|
+
if (item?.type !== 'thinking' && item?.type !== 'redacted_thinking' && item?.type !== 'reasoning') continue;
|
|
27
|
+
bytes += size(item.thinking) + size(item.signature) + size(item.data) + size(item.encrypted_content);
|
|
28
|
+
}
|
|
29
|
+
return bytes;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function userCategory(message) {
|
|
33
|
+
return message.meta === 'skill' ? 'skills' : 'user';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function add(map, key, fields) {
|
|
37
|
+
const row = map.get(key) || {};
|
|
38
|
+
for (const [name, value] of Object.entries(fields)) row[name] = (row[name] || 0) + value;
|
|
39
|
+
map.set(key, row);
|
|
40
|
+
return row;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createSummary() {
|
|
44
|
+
return {
|
|
45
|
+
sessions: 0,
|
|
46
|
+
requests: 0,
|
|
47
|
+
groups: new Map(),
|
|
48
|
+
carry: new Map(),
|
|
49
|
+
tools: new Map(),
|
|
50
|
+
buckets: new Map(),
|
|
51
|
+
shell: {},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Folds one stored session into the summary. A message's carry is its size
|
|
56
|
+
// times the requests sent after it: every later request re-sends it.
|
|
57
|
+
export function addSession(summary, session) {
|
|
58
|
+
const messages = Array.isArray(session?.messages) ? session.messages : [];
|
|
59
|
+
const requests = messages.filter((message) => message.role === 'assistant').length;
|
|
60
|
+
if (!requests) return;
|
|
61
|
+
summary.sessions++;
|
|
62
|
+
summary.requests += requests;
|
|
63
|
+
const group = add(summary.groups, `${session.agent || '-'} @${session.model || '-'}`, { sessions: 1, requests });
|
|
64
|
+
const shell = summary.shell;
|
|
65
|
+
const callsById = new Map();
|
|
66
|
+
const promoted = new Map();
|
|
67
|
+
const prefixes = [];
|
|
68
|
+
let prefix = size(session.tools);
|
|
69
|
+
let later = requests;
|
|
70
|
+
for (const message of messages) {
|
|
71
|
+
if (message.role === 'assistant') {
|
|
72
|
+
later--;
|
|
73
|
+
prefixes.push(prefix);
|
|
74
|
+
const calls = Array.isArray(message.toolCalls) ? message.toolCalls : [];
|
|
75
|
+
let argBytes = 0;
|
|
76
|
+
for (const call of calls) {
|
|
77
|
+
argBytes += size(call.arguments) + size(call.name);
|
|
78
|
+
callsById.set(call.id, { ...call, alone: calls.length === 1 });
|
|
79
|
+
}
|
|
80
|
+
const thinking = replayBytes(message);
|
|
81
|
+
const text = size(message.content);
|
|
82
|
+
add(summary.carry, 'tool args', { bytes: argBytes * later });
|
|
83
|
+
add(summary.carry, 'thinking', { bytes: thinking * later });
|
|
84
|
+
add(summary.carry, 'assistant text', { bytes: text * later });
|
|
85
|
+
group.calls = (group.calls || 0) + calls.length;
|
|
86
|
+
if (calls.length) group.toolRequests = (group.toolRequests || 0) + 1;
|
|
87
|
+
if (calls.length === 1) group.singleCall = (group.singleCall || 0) + 1;
|
|
88
|
+
if (calls.length && calls.every((call) => call.name === 'task' && call.arguments?.action === 'wait')) {
|
|
89
|
+
shell.taskWaitOnlyRequests = (shell.taskWaitOnlyRequests || 0) + 1;
|
|
90
|
+
}
|
|
91
|
+
prefix += argBytes + thinking + text;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const bytes = size(message.content);
|
|
95
|
+
prefix += bytes;
|
|
96
|
+
if (message.role === 'system') add(summary.carry, 'system', { bytes: bytes * later });
|
|
97
|
+
else if (message.role === 'user') add(summary.carry, userCategory(message), { bytes: bytes * later });
|
|
98
|
+
else if (message.role === 'tool') {
|
|
99
|
+
add(summary.carry, 'tool results', { bytes: bytes * later });
|
|
100
|
+
const call = callsById.get(message.toolCallId);
|
|
101
|
+
if (!call) continue;
|
|
102
|
+
add(summary.tools, call.name, { calls: 1, bytes, carry: bytes * later });
|
|
103
|
+
const text = typeof message.content === 'string' ? message.content : '';
|
|
104
|
+
if (call.name === 'shell') recordShellCall(shell, promoted, call, text);
|
|
105
|
+
else if (call.name === 'task') recordTaskResult(shell, promoted, call, text);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const lastContext = Number(session.lastContextTokens) || 0;
|
|
109
|
+
const tokensPerByte = lastContext > 0 && prefix > 0 ? lastContext / prefix : FALLBACK_TOKENS_PER_BYTE;
|
|
110
|
+
for (const bytes of prefixes) {
|
|
111
|
+
const context = bytes * tokensPerByte;
|
|
112
|
+
group.tokenRequests = (group.tokenRequests || 0) + context;
|
|
113
|
+
add(
|
|
114
|
+
summary.buckets,
|
|
115
|
+
CONTEXT_BUCKETS.find((limit) => context < limit),
|
|
116
|
+
{ requests: 1, tokenRequests: context }
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function recordShellCall(shell, promoted, call, text) {
|
|
122
|
+
shell.calls = (shell.calls || 0) + 1;
|
|
123
|
+
const waitMs = call.arguments?.wait_ms;
|
|
124
|
+
const shortWait = Number.isFinite(waitMs) && waitMs < DEFAULT_SHELL_AUTO_BACKGROUND_MS;
|
|
125
|
+
if (shortWait) shell.shortWait = (shell.shortWait || 0) + 1;
|
|
126
|
+
if (!text.startsWith('background task')) return;
|
|
127
|
+
shell.promoted = (shell.promoted || 0) + 1;
|
|
128
|
+
if (shortWait) shell.shortWaitPromoted = (shell.shortWaitPromoted || 0) + 1;
|
|
129
|
+
const taskId = /^task_id: (\S+)/m.exec(text)?.[1];
|
|
130
|
+
if (taskId) promoted.set(taskId, { shortWait, durationKnown: false });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// The first task result that reports both timestamps settles how long the
|
|
134
|
+
// promoted command really ran.
|
|
135
|
+
function recordTaskResult(shell, promoted, call, text) {
|
|
136
|
+
const job = promoted.get(call.arguments?.task_id);
|
|
137
|
+
if (!job || job.durationKnown) return;
|
|
138
|
+
const started = Date.parse(/^started: (\S+)/m.exec(text)?.[1] || '');
|
|
139
|
+
const finished = Date.parse(/^finished: (\S+)/m.exec(text)?.[1] || '');
|
|
140
|
+
if (!Number.isFinite(started) || !Number.isFinite(finished)) return;
|
|
141
|
+
job.durationKnown = true;
|
|
142
|
+
if (!job.shortWait) return;
|
|
143
|
+
shell.shortWaitTimed = (shell.shortWaitTimed || 0) + 1;
|
|
144
|
+
if (finished - started <= DEFAULT_SHELL_AUTO_BACKGROUND_MS) {
|
|
145
|
+
shell.shortWaitInsideDefault = (shell.shortWaitInsideDefault || 0) + 1;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const share = (part, whole) => (whole > 0 ? Math.round((part / whole) * 1000) / 10 : 0);
|
|
150
|
+
|
|
151
|
+
export function buildReport(summary) {
|
|
152
|
+
const carryTotal = [...summary.carry.values()].reduce((sum, row) => sum + row.bytes, 0);
|
|
153
|
+
const toolCarryTotal = [...summary.tools.values()].reduce((sum, row) => sum + row.carry, 0);
|
|
154
|
+
const tokenRequestTotal = [...summary.buckets.values()].reduce((sum, row) => sum + row.tokenRequests, 0);
|
|
155
|
+
return {
|
|
156
|
+
sessions: summary.sessions,
|
|
157
|
+
requests: summary.requests,
|
|
158
|
+
groups: [...summary.groups.entries()]
|
|
159
|
+
.map(([name, row]) => ({
|
|
160
|
+
name,
|
|
161
|
+
sessions: row.sessions,
|
|
162
|
+
requests: row.requests,
|
|
163
|
+
callsPerToolRequest: row.toolRequests ? Math.round((row.calls / row.toolRequests) * 100) / 100 : 0,
|
|
164
|
+
singleCallPct: share(row.singleCall || 0, row.toolRequests || 0),
|
|
165
|
+
avgContextTokens: Math.round((row.tokenRequests || 0) / row.requests),
|
|
166
|
+
tokenRequests: Math.round(row.tokenRequests || 0),
|
|
167
|
+
}))
|
|
168
|
+
.sort((a, b) => b.tokenRequests - a.tokenRequests),
|
|
169
|
+
carry: [...summary.carry.entries()]
|
|
170
|
+
.map(([name, row]) => ({ name, pct: share(row.bytes, carryTotal) }))
|
|
171
|
+
.sort((a, b) => b.pct - a.pct),
|
|
172
|
+
tools: [...summary.tools.entries()]
|
|
173
|
+
.map(([name, row]) => ({
|
|
174
|
+
name,
|
|
175
|
+
calls: row.calls,
|
|
176
|
+
avgResultBytes: Math.round(row.bytes / row.calls),
|
|
177
|
+
carryPct: share(row.carry, toolCarryTotal),
|
|
178
|
+
}))
|
|
179
|
+
.sort((a, b) => b.carryPct - a.carryPct),
|
|
180
|
+
requestContext: CONTEXT_BUCKETS.map((limit) => {
|
|
181
|
+
const row = summary.buckets.get(limit) || { requests: 0, tokenRequests: 0 };
|
|
182
|
+
return {
|
|
183
|
+
under: Number.isFinite(limit) ? limit : null,
|
|
184
|
+
requestPct: share(row.requests, summary.requests),
|
|
185
|
+
tokenRequestPct: share(row.tokenRequests, tokenRequestTotal),
|
|
186
|
+
};
|
|
187
|
+
}),
|
|
188
|
+
shell: {
|
|
189
|
+
calls: summary.shell.calls || 0,
|
|
190
|
+
promoted: summary.shell.promoted || 0,
|
|
191
|
+
waitUnderDefault: summary.shell.shortWait || 0,
|
|
192
|
+
waitUnderDefaultPromoted: summary.shell.shortWaitPromoted || 0,
|
|
193
|
+
waitUnderDefaultTimed: summary.shell.shortWaitTimed || 0,
|
|
194
|
+
waitUnderDefaultFinishedInsideDefault: summary.shell.shortWaitInsideDefault || 0,
|
|
195
|
+
taskWaitOnlyRequests: summary.shell.taskWaitOnlyRequests || 0,
|
|
196
|
+
taskWaitOnlyPct: share(summary.shell.taskWaitOnlyRequests || 0, summary.requests),
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const compact = (value) => {
|
|
202
|
+
if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
|
|
203
|
+
return value >= 1e3 ? `${(value / 1e3).toFixed(1)}k` : String(Math.round(value));
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
function table(rows) {
|
|
207
|
+
const widths = rows[0].map((_, column) => Math.max(...rows.map((row) => String(row[column]).length)));
|
|
208
|
+
return rows.map((row) =>
|
|
209
|
+
row
|
|
210
|
+
.map((cell, column) => String(cell).padEnd(widths[column]))
|
|
211
|
+
.join(' ')
|
|
212
|
+
.trimEnd()
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function renderReport(report, sinceTs) {
|
|
217
|
+
const lines = [
|
|
218
|
+
`session efficiency: ${report.sessions} sessions, ${report.requests} requests${sinceTs ? ` since ${new Date(sinceTs).toISOString()}` : ''}`,
|
|
219
|
+
'',
|
|
220
|
+
...table([
|
|
221
|
+
['agent @model', 'sessions', 'requests', 'calls/req', 'single-call%', 'avgContext', 'tokenRequests'],
|
|
222
|
+
...report.groups
|
|
223
|
+
.slice(0, 12)
|
|
224
|
+
.map((row) => [
|
|
225
|
+
row.name,
|
|
226
|
+
row.sessions,
|
|
227
|
+
row.requests,
|
|
228
|
+
row.callsPerToolRequest,
|
|
229
|
+
row.singleCallPct,
|
|
230
|
+
compact(row.avgContextTokens),
|
|
231
|
+
compact(row.tokenRequests),
|
|
232
|
+
]),
|
|
233
|
+
]),
|
|
234
|
+
'',
|
|
235
|
+
`carried context (size x later requests): ${report.carry.map((row) => `${row.name} ${row.pct}%`).join(', ')}`,
|
|
236
|
+
`tool results by carry: ${report.tools
|
|
237
|
+
.slice(0, 10)
|
|
238
|
+
.map((row) => `${row.name} ${row.carryPct}% (${row.calls} calls, avg ${compact(row.avgResultBytes)}B)`)
|
|
239
|
+
.join(', ')}`,
|
|
240
|
+
`request context: ${report.requestContext
|
|
241
|
+
.map(
|
|
242
|
+
(row) =>
|
|
243
|
+
`${row.under ? `<${compact(row.under)}` : 'larger'} ${row.requestPct}% of requests / ${row.tokenRequestPct}% of token-requests`
|
|
244
|
+
)
|
|
245
|
+
.join(', ')}`,
|
|
246
|
+
`shell foreground window: ${report.shell.calls} calls, ${report.shell.promoted} promoted to background; wait_ms under the ${DEFAULT_SHELL_AUTO_BACKGROUND_MS}ms default on ${report.shell.waitUnderDefault} calls (${report.shell.waitUnderDefaultPromoted} promoted, ${report.shell.waitUnderDefaultFinishedInsideDefault} of ${report.shell.waitUnderDefaultTimed} timed ones finished inside the default); task-wait-only requests: ${report.shell.taskWaitOnlyRequests} (${report.shell.taskWaitOnlyPct}%)`,
|
|
247
|
+
];
|
|
248
|
+
return `${lines.join('\n')}\n`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function main() {
|
|
252
|
+
const mixdogHome = process.env.MIXDOG_HOME || resolve(homedir(), '.mixdog');
|
|
253
|
+
const dataDir = resolve(argValue('--data-dir', process.env.MIXDOG_DATA_DIR || join(mixdogHome, 'data')));
|
|
254
|
+
const sinceTs = parseSince(argValue('--since', '5d'));
|
|
255
|
+
const agentFilter = argValue('--agent', null);
|
|
256
|
+
const sessionsDir = join(dataDir, 'sessions');
|
|
257
|
+
const summary = createSummary();
|
|
258
|
+
let names;
|
|
259
|
+
try {
|
|
260
|
+
names = readdirSync(sessionsDir).filter((name) => name.endsWith('.json'));
|
|
261
|
+
} catch {
|
|
262
|
+
process.stderr.write(`no sessions directory at ${sessionsDir}\n`);
|
|
263
|
+
return 1;
|
|
264
|
+
}
|
|
265
|
+
for (const name of names) {
|
|
266
|
+
const file = join(sessionsDir, name);
|
|
267
|
+
try {
|
|
268
|
+
if (sinceTs != null && statSync(file).mtimeMs < sinceTs) continue;
|
|
269
|
+
const session = JSON.parse(readFileSync(file, 'utf8'));
|
|
270
|
+
if (agentFilter && String(session.agent || '') !== agentFilter) continue;
|
|
271
|
+
addSession(summary, session);
|
|
272
|
+
} catch {
|
|
273
|
+
/* a session mid-write or unreadable: skip it */
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const report = buildReport(summary);
|
|
277
|
+
process.stdout.write(hasFlag('--json') ? `${JSON.stringify(report, null, 2)}\n` : renderReport(report, sinceTs));
|
|
278
|
+
return 0;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (resolve(process.argv[1] || '') === fileURLToPath(import.meta.url)) {
|
|
282
|
+
process.exitCode = main();
|
|
283
|
+
}
|
|
@@ -166,9 +166,9 @@ GNOME with the Window Calls extension); prefer accessibility refs there.
|
|
|
166
166
|
`mouse_down` holds a button past the end of its command and is
|
|
167
167
|
background-only; pair it with `mouse_up` on a fresh observation.
|
|
168
168
|
`key_down` holds a key the same way and is foreground-only, because no window
|
|
169
|
-
message can leave a key physically down; pair it with `key_up`.
|
|
170
|
-
exit releases what the session pressed, and a
|
|
171
|
-
rather than assumed.
|
|
169
|
+
message can leave a key physically down; pair it with `key_up`. The end of
|
|
170
|
+
the turn, like any session exit, releases what the session pressed, and a
|
|
171
|
+
failed release is reported rather than assumed.
|
|
172
172
|
Execution stops at the first failure or when the target transitions
|
|
173
173
|
(popup, dialog, window change) and returns one fresh observation. An act
|
|
174
174
|
whose first action uses a `ref` returns accessibility only (`ax`) unless
|
|
@@ -87,7 +87,8 @@ available before Local Provider is installed or enabled.
|
|
|
87
87
|
- For startup failure, use `starting`, `lastError`, and `lastExit` diagnostics.
|
|
88
88
|
Do not print authentication keys or attempt unmanaged server commands.
|
|
89
89
|
- Hardware status may initially be `checking`; inspect again after detection
|
|
90
|
-
completes. All NVIDIA GPUs
|
|
90
|
+
completes. All NVIDIA GPUs on Windows, or the Apple Silicon GPU on a Mac
|
|
91
|
+
(Metal, on unified memory), are checked without blocking the app. Loading
|
|
91
92
|
chooses a compatible GPU with enough free memory for the catalog's estimate;
|
|
92
93
|
that estimate is a guard, not a guarantee against driver/runtime allocation
|
|
93
94
|
failure. Never bypass a failed resource check.
|
|
@@ -65,13 +65,13 @@ that a call returned without transport error.
|
|
|
65
65
|
provider restores Main inheritance; omission never resets it. Disabling an
|
|
66
66
|
agent preserves its route for re-enabling.
|
|
67
67
|
- Web Search model selection and Web Search tool exposure are separate.
|
|
68
|
-
- Memory master state controls tools
|
|
69
|
-
`set_recap_enabled` changes only the
|
|
68
|
+
- Memory master state controls tools and background cycles; Core Memory
|
|
69
|
+
injection stays on regardless. `set_recap_enabled` changes only the
|
|
70
|
+
background cycles. Core Memory entries
|
|
70
71
|
are read and changed with the `memory` tool, not setup.
|
|
71
72
|
- Built-ins have installation state separate from enabled state. Browser Use,
|
|
72
73
|
Computer Use, and voice actions require this conversation open in the local
|
|
73
|
-
Desktop window.
|
|
74
|
-
Browser Use and Computer Use only, not voice.
|
|
74
|
+
Desktop window.
|
|
75
75
|
- Desktop appearance changes affect the Desktop host, not a paired phone.
|
|
76
76
|
Display language is independent of profile response language. A required
|
|
77
77
|
reload is reported, never performed automatically.
|
|
@@ -27,7 +27,6 @@ Read this when choosing a `setup` mutation.
|
|
|
27
27
|
| Background memory cycles | `set_recap_enabled` |
|
|
28
28
|
| Web Search tool exposure | `set_web_search_enabled` |
|
|
29
29
|
| Git, Office, Code Tidy, Local Provider, Browser, Computer, or voice enabled state | `set_builtin_enabled` |
|
|
30
|
-
| One approval per session before the first Browser Use or Computer Use call (`name`: browser or computer; on by default) | `set_first_use_approval` |
|
|
31
30
|
| Git, Memory, Office, Code Tidy, Local Provider, Browser, Computer, or voice preparation | `install_builtin` |
|
|
32
31
|
| Managed local model installation (use the local-provider skill) | `install_local_model` |
|
|
33
32
|
| Start or resume a background runtime/model download | `start_local_installation` |
|
|
@@ -150,8 +149,7 @@ the Browser Use / Computer Use installed and enabled markers are read with
|
|
|
150
149
|
- Browser Use, Computer Use, and voice installation/toggles use the same
|
|
151
150
|
`install_builtin` and `set_builtin_enabled` actions through a live Desktop
|
|
152
151
|
receipt. Computer Use runs on Windows, macOS, and Linux desktops. These
|
|
153
|
-
actions do not approve OS permission dialogs
|
|
154
|
-
the user's behalf.
|
|
152
|
+
actions do not approve OS permission dialogs on the user's behalf.
|
|
155
153
|
- Environment feature overrides are diagnostics for headless or benchmark
|
|
156
154
|
environments, not ordinary user settings.
|
|
157
155
|
|
|
@@ -59,9 +59,8 @@ toggles, Projects, and linked-device revocation through the attached
|
|
|
59
59
|
Desktop. A receipt identifies `scope:desktop-host`; appearance is not applied
|
|
60
60
|
to the paired browser. No Desktop claimant means no confirmed change.
|
|
61
61
|
|
|
62
|
-
Computer observe-only
|
|
63
|
-
|
|
64
|
-
setup is their only supported route. Usage pin is the pin toggle on the activity rail usage card.
|
|
62
|
+
Computer observe-only and background Memory cycles (`set_recap_enabled`)
|
|
63
|
+
have no settings control in either app; setup is their only supported route. Usage pin is the pin toggle on the activity rail usage card.
|
|
65
64
|
|
|
66
65
|
Schedules/webhooks and workflow/agent/skill definitions use their runtime
|
|
67
66
|
actions directly. These do not require clicking their Desktop rails.
|
package/src/headless-exec.mjs
CHANGED
|
@@ -396,6 +396,7 @@ async function createHeadlessSessionRuntime(boundary, { provider, model, cwd, we
|
|
|
396
396
|
const createRuntime = runtimeFactory || (await import('./mixdog-session-runtime.mjs')).createMixdogSessionRuntime;
|
|
397
397
|
process.env.MIXDOG_FEATURE_WEB_SEARCH = webSearch === true ? '1' : '0';
|
|
398
398
|
process.env.MIXDOG_FEATURE_MEMORY = '0';
|
|
399
|
+
process.env.MIXDOG_BOOT_CORE_MEMORY = '0';
|
|
399
400
|
return createRuntime({
|
|
400
401
|
provider,
|
|
401
402
|
model,
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
results in the conversation instead.
|
|
6
6
|
- Reference files and folders in inline code by a Project-root-relative or
|
|
7
7
|
absolute path, optionally `:line[:column]` (`deliverables/report.docx`,
|
|
8
|
-
`src/app.ts:42`);
|
|
9
|
-
`file://` URI.
|
|
8
|
+
`src/app.ts:42`); files outside the Project always by absolute path; never a
|
|
9
|
+
bare name, a path relative to another folder, or a `file://` URI.
|
|
10
10
|
- Deliver requested visual artifacts (SVG, diagrams, HTML pages) as saved files
|
|
11
11
|
and give their path; never paste their source as the answer unless the user
|
|
12
12
|
asks for the code.
|
|
@@ -18,6 +18,10 @@ import { headerValue } from './retry-classification.mjs';
|
|
|
18
18
|
// Server-declared reason header for a fast-mode rejection caused by missing
|
|
19
19
|
// overage billing (matches the reference client's header name).
|
|
20
20
|
const OVERAGE_DISABLED_HEADER = 'anthropic-ratelimit-unified-overage-disabled-reason';
|
|
21
|
+
// Body-only form of the same rejection ("Usage credits are required for fast
|
|
22
|
+
// mode."), sent as a 429 rate_limit_error without the header above.
|
|
23
|
+
const CREDITS_REQUIRED_PATTERN = /usage credits are required for fast mode/i;
|
|
24
|
+
const CREDITS_REQUIRED_REASON = 'usage_credits_required';
|
|
21
25
|
|
|
22
26
|
// A Retry-After under this bound is short enough that waiting keeps fast mode
|
|
23
27
|
// (and its cache prefix) worthwhile.
|
|
@@ -66,6 +70,12 @@ export function noteFastModeCapacityError(err, { fast = false, now = Date.now()
|
|
|
66
70
|
_disabledReason = String(overageReason);
|
|
67
71
|
return 'disabled';
|
|
68
72
|
}
|
|
73
|
+
// Same terminal condition reported only in the error body: the account has
|
|
74
|
+
// no usage credits for fast mode, so no cooldown can make it succeed.
|
|
75
|
+
if (CREDITS_REQUIRED_PATTERN.test(String(err?.message || ''))) {
|
|
76
|
+
_disabledReason = CREDITS_REQUIRED_REASON;
|
|
77
|
+
return 'disabled';
|
|
78
|
+
}
|
|
69
79
|
|
|
70
80
|
const retryAfterMs = Number(err?.retryAfterMs);
|
|
71
81
|
const hasWindow = Number.isFinite(retryAfterMs) && retryAfterMs >= 0;
|
|
@@ -48,7 +48,7 @@ function fallbackAnthropicMaxTokens(model) {
|
|
|
48
48
|
if (MAX_TOKENS[model]) return MAX_TOKENS[model];
|
|
49
49
|
const id = String(model || '').toLowerCase();
|
|
50
50
|
if (id.includes('opus')) return 65536;
|
|
51
|
-
if (id.includes('fable')) return 65536;
|
|
51
|
+
if (id.includes('fable') || id.includes('mythos')) return 65536;
|
|
52
52
|
const sonnetVersion = id.match(/^claude-sonnet-(\d+)/);
|
|
53
53
|
if (sonnetVersion) return Number(sonnetVersion[1]) >= 5 ? 65536 : 16384;
|
|
54
54
|
if (id.includes('sonnet')) return 16384;
|
|
@@ -17,7 +17,7 @@ const VERSION_GATE_PATTERN =
|
|
|
17
17
|
/Claude Code\s+(\d{1,4}\.\d{1,4}\.\d{1,6})\s+does not support this model;\s*version\s+(\d{1,4}\.\d{1,4}\.\d{1,6})\s+or newer is required\b/i;
|
|
18
18
|
|
|
19
19
|
// Effective version = max(floor, learned minimum, live @anthropic-ai/claude-code).
|
|
20
|
-
const liveCliVersion = createNpmVersionSource('@anthropic-ai/claude-code');
|
|
20
|
+
const liveCliVersion = createNpmVersionSource('@anthropic-ai/claude-code', { persistKey: 'claude-cli' });
|
|
21
21
|
|
|
22
22
|
let learnedCliVersion = null;
|
|
23
23
|
let learnedCliVersionLoaded = false;
|
|
@@ -51,14 +51,18 @@ export async function judgeInitialStatus(result, requestBody, { provider, cleanu
|
|
|
51
51
|
release();
|
|
52
52
|
// Initial-response failure: nothing was sampled, so the typed retry
|
|
53
53
|
// rules upstream own the decision. Subscription 429s never retry
|
|
54
|
-
// in-loop
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
// in-loop; a drained or unavailable fast pool says nothing about
|
|
55
|
+
// standard-speed capacity, so `speed` is dropped and the error is
|
|
56
|
+
// flagged for a single standard-speed replay by the caller.
|
|
57
|
+
const fastDecision = noteFastModeCapacityError(
|
|
58
|
+
{ httpStatus: status, headers: result?.response?.headers, message: quotaText },
|
|
58
59
|
{ fast: requestBody?.speed === 'fast' }
|
|
59
60
|
);
|
|
61
|
+
const fastDowngraded = fastDecision === 'downgrade' || fastDecision === 'disabled';
|
|
62
|
+
if (fastDowngraded) delete requestBody.speed;
|
|
60
63
|
throw Object.assign(anthropicQuotaError(status, result?.response?.headers, provider.scrubTokens(quotaText)), {
|
|
61
64
|
initialResponseError: true,
|
|
65
|
+
fastDowngraded,
|
|
62
66
|
});
|
|
63
67
|
}
|
|
64
68
|
const err = new Error(`Anthropic OAuth API ${status}`);
|
|
@@ -97,7 +97,16 @@ export function createAnthropicOAuthRequest({
|
|
|
97
97
|
withRetry(
|
|
98
98
|
async ({ signal: attemptSignal }) => {
|
|
99
99
|
const result = await doRequestImpl(accessToken, attemptSignal, requestBody);
|
|
100
|
-
|
|
100
|
+
try {
|
|
101
|
+
await judgeInitialStatus(result, requestBody, { provider, cleanupCancelHandler });
|
|
102
|
+
} catch (err) {
|
|
103
|
+
// Fast-pool 429 before any byte was sampled: `speed` is already
|
|
104
|
+
// dropped, so replay once at standard speed within this attempt.
|
|
105
|
+
if (err?.fastDowngraded !== true) throw err;
|
|
106
|
+
const replay = await doRequestImpl(accessToken, attemptSignal, requestBody);
|
|
107
|
+
await judgeInitialStatus(replay, requestBody, { provider, cleanupCancelHandler });
|
|
108
|
+
return replay;
|
|
109
|
+
}
|
|
101
110
|
return result;
|
|
102
111
|
},
|
|
103
112
|
{
|
|
@@ -77,7 +77,7 @@ const ANTHROPIC_SSE_ERROR_TYPE_STATUS = new Map([
|
|
|
77
77
|
['request_too_large', 413],
|
|
78
78
|
['rate_limit_error', 429],
|
|
79
79
|
['api_error', 500],
|
|
80
|
-
['overloaded_error',
|
|
80
|
+
['overloaded_error', 529],
|
|
81
81
|
]);
|
|
82
82
|
|
|
83
83
|
function _statusForAnthropicSseError(event, payload) {
|
|
@@ -20,6 +20,8 @@ import { writeJsonAtomicSync } from '../../../shared/atomic-file.mjs';
|
|
|
20
20
|
import { boundProviderAuthPath } from '../../../shared/provider-auth-binding.mjs';
|
|
21
21
|
import { normalizeExpiresAtMs as _normalizeExpiresAt, scrubOAuthSecrets } from './lib/oauth-token-utils.mjs';
|
|
22
22
|
import { ANTIGRAVITY_MODELS } from '../../../shared/llm/provider-model-identities.mjs';
|
|
23
|
+
import { readLastKnownVersion, rememberLastKnownVersion } from './client-version-store.mjs';
|
|
24
|
+
import { maxSemver } from './npm-cli-version.mjs';
|
|
23
25
|
export { ANTIGRAVITY_MODELS } from '../../../shared/llm/provider-model-identities.mjs';
|
|
24
26
|
|
|
25
27
|
// The Antigravity IDE's installed-app OAuth client, which every copy of that
|
|
@@ -62,7 +64,7 @@ export const PROJECT_ENDPOINT = CONTENT_ENDPOINT;
|
|
|
62
64
|
|
|
63
65
|
export const DEFAULT_ANTIGRAVITY_MODEL = ANTIGRAVITY_MODELS[0].id;
|
|
64
66
|
|
|
65
|
-
const ANTIGRAVITY_VERSION_FALLBACK = '2.
|
|
67
|
+
const ANTIGRAVITY_VERSION_FALLBACK = '2.19.1';
|
|
66
68
|
export const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
67
69
|
export const TOKEN_TIMEOUT_MS = 30_000;
|
|
68
70
|
export const PROJECT_TIMEOUT_MS = 30_000;
|
|
@@ -71,21 +73,31 @@ export const TOKEN_REFRESH_SKEW_MS = 5 * 60 * 1000;
|
|
|
71
73
|
|
|
72
74
|
// The backend gates models on the client version in the User-Agent, so the
|
|
73
75
|
// version tracks the latest Antigravity release via its update manifest.
|
|
74
|
-
// The
|
|
76
|
+
// The last discovered version is kept on disk for offline starts; the pinned
|
|
77
|
+
// value is only the first-run fallback. The env override always wins.
|
|
75
78
|
const VERSION_MANIFEST_URL =
|
|
76
79
|
'https://antigravity-hub-auto-updater-974169037036.us-central1.run.app/manifest/latest-arm64-mac.yml';
|
|
77
80
|
const VERSION_FETCH_TIMEOUT_MS = 5_000;
|
|
78
81
|
const VERSION_RETRY_AFTER_MS = 10 * 60 * 1000;
|
|
82
|
+
const VERSION_LAST_KNOWN_KEY = 'antigravity-hub';
|
|
79
83
|
let _discoveredVersion = null;
|
|
84
|
+
let _lastKnownVersion;
|
|
80
85
|
let _versionFetch = null;
|
|
81
86
|
let _versionFailedAt = 0;
|
|
82
87
|
|
|
83
88
|
function antigravityVersion() {
|
|
84
89
|
return (
|
|
85
|
-
String(process.env.MIXDOG_ANTIGRAVITY_VERSION || '').trim() ||
|
|
90
|
+
String(process.env.MIXDOG_ANTIGRAVITY_VERSION || '').trim() ||
|
|
91
|
+
_discoveredVersion ||
|
|
92
|
+
maxSemver(lastKnownAntigravityVersion(), ANTIGRAVITY_VERSION_FALLBACK)
|
|
86
93
|
);
|
|
87
94
|
}
|
|
88
95
|
|
|
96
|
+
function lastKnownAntigravityVersion() {
|
|
97
|
+
if (_lastKnownVersion === undefined) _lastKnownVersion = readLastKnownVersion(VERSION_LAST_KNOWN_KEY);
|
|
98
|
+
return _lastKnownVersion;
|
|
99
|
+
}
|
|
100
|
+
|
|
89
101
|
/** Version from an electron-builder update manifest, or null when absent. */
|
|
90
102
|
export function parseAntigravityManifestVersion(text) {
|
|
91
103
|
for (const line of String(text || '').split(/\r?\n/)) {
|
|
@@ -113,6 +125,7 @@ export function ensureAntigravityVersion({ fetchFn = fetch } = {}) {
|
|
|
113
125
|
signal: AbortSignal.timeout(VERSION_FETCH_TIMEOUT_MS),
|
|
114
126
|
});
|
|
115
127
|
if (res.ok) _discoveredVersion = parseAntigravityManifestVersion(await res.text());
|
|
128
|
+
if (_discoveredVersion) rememberLastKnownVersion(VERSION_LAST_KNOWN_KEY, _discoveredVersion);
|
|
116
129
|
} catch {
|
|
117
130
|
// The pinned fallback stays valid when discovery fails.
|
|
118
131
|
} finally {
|
|
@@ -125,6 +138,8 @@ export function ensureAntigravityVersion({ fetchFn = fetch } = {}) {
|
|
|
125
138
|
}
|
|
126
139
|
|
|
127
140
|
export function _resetAntigravityVersionForTest() {
|
|
141
|
+
rememberLastKnownVersion(VERSION_LAST_KNOWN_KEY, null);
|
|
142
|
+
_lastKnownVersion = undefined;
|
|
128
143
|
_discoveredVersion = null;
|
|
129
144
|
_versionFetch = null;
|
|
130
145
|
_versionFailedAt = 0;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Last live client version each provider saw, kept on disk under one key per
|
|
2
|
+
// client. A new or offline process starts from the newest version this machine
|
|
3
|
+
// has observed instead of a floor that only moves with a release. Skipped
|
|
4
|
+
// entirely when MIXDOG_DISABLE_LIVE_CLI_VERSIONS=1.
|
|
5
|
+
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { updateJsonAtomicSync } from '../../../shared/atomic-file.mjs';
|
|
9
|
+
import { resolvePluginData } from '../../../shared/plugin-paths.mjs';
|
|
10
|
+
|
|
11
|
+
const LAST_KNOWN_FILE_NAME = 'client-versions.json';
|
|
12
|
+
const LAST_KNOWN_SCHEMA_VERSION = 1;
|
|
13
|
+
const LAST_KNOWN_VALUE = /^[\w.+-]{1,64}$/;
|
|
14
|
+
|
|
15
|
+
const liveLookupDisabled = () => process.env.MIXDOG_DISABLE_LIVE_CLI_VERSIONS === '1';
|
|
16
|
+
const lastKnownPath = () => join(resolvePluginData(), LAST_KNOWN_FILE_NAME);
|
|
17
|
+
|
|
18
|
+
/** The last live version remembered under `key`, or null. */
|
|
19
|
+
export function readLastKnownVersion(key) {
|
|
20
|
+
if (!key || liveLookupDisabled()) return null;
|
|
21
|
+
try {
|
|
22
|
+
const raw = JSON.parse(readFileSync(lastKnownPath(), 'utf-8'));
|
|
23
|
+
if (raw?.version !== LAST_KNOWN_SCHEMA_VERSION) return null;
|
|
24
|
+
const value = raw.entries?.[key]?.value;
|
|
25
|
+
return typeof value === 'string' && LAST_KNOWN_VALUE.test(value) ? value : null;
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Remember a live version; `null` forgets it. Best-effort: never throws. */
|
|
32
|
+
export function rememberLastKnownVersion(key, value) {
|
|
33
|
+
if (!key || liveLookupDisabled()) return;
|
|
34
|
+
if (value != null && !LAST_KNOWN_VALUE.test(String(value))) return;
|
|
35
|
+
try {
|
|
36
|
+
updateJsonAtomicSync(
|
|
37
|
+
lastKnownPath(),
|
|
38
|
+
(current) => {
|
|
39
|
+
const entries = current?.version === LAST_KNOWN_SCHEMA_VERSION ? { ...current.entries } : {};
|
|
40
|
+
if ((entries[key]?.value ?? null) === value) return undefined;
|
|
41
|
+
if (value == null) delete entries[key];
|
|
42
|
+
else entries[key] = { value, fetchedAt: Date.now() };
|
|
43
|
+
return { version: LAST_KNOWN_SCHEMA_VERSION, entries };
|
|
44
|
+
},
|
|
45
|
+
{ lock: true }
|
|
46
|
+
);
|
|
47
|
+
} catch {
|
|
48
|
+
/* a read-only data directory must not break the lookup */
|
|
49
|
+
}
|
|
50
|
+
}
|