mixdog 0.9.175 → 1.0.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/package.json +1 -1
- package/scripts/context-renewal-live-check.mjs +2 -1
- package/scripts/plan-runtime-release-tag.mjs +57 -0
- package/src/defaults/skills/xlsx/SKILL.md +1 -0
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +7 -12
- package/src/runtime/agent/orchestrator/providers/anthropic-api-transport.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/cursor.mjs +20 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -0
- package/src/runtime/agent/orchestrator/runtime-core/goal-tasks.mjs +12 -1
- package/src/runtime/agent/orchestrator/session/result-classification.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/transcript-restore/tool-result-status.mjs +7 -3
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/benign-exit.mjs +7 -3
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +1 -0
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +39 -3
- package/src/runtime/memory/lib/runtime-fetcher.mjs +94 -23
- package/src/runtime/office/capabilities-catalog.mjs +2 -0
- package/src/runtime/office/capabilities-signatures.mjs +12 -6
- package/src/runtime/office/capabilities.mjs +30 -10
- package/src/runtime/office/core/office-actions-open.mjs +1 -6
- package/src/runtime/office/core/office-sessions.mjs +9 -0
- package/src/runtime/office/portable/portable-xlsx-operations.mjs +2 -49
- package/src/runtime/office/portable/xlsx-contract.mjs +37 -3
- package/src/runtime/office/tool-defs.mjs +2 -2
- package/src/runtime/shared/llm/cost.mjs +16 -4
- package/src/runtime/shared/llm/model-catalog-projection.mjs +3 -2
- package/src/runtime/shared/llm/model-catalog.mjs +48 -0
- package/src/runtime/shared/llm/model-pricing-rates.mjs +11 -4
- package/src/runtime/shared/llm/usage-accounting.mjs +12 -11
- package/src/runtime/shared/llm/usage-context.mjs +7 -0
- package/src/runtime/shared/llm/usage-ledger-repair.mjs +3 -1
- package/src/runtime/shared/llm/usage-ledger.mjs +1 -1
- package/src/runtime/shared/tool-card-model/card-status.mjs +7 -5
- package/src/runtime/shared/tool-card-model/git-result.mjs +10 -0
- package/src/runtime/shared/tool-card-model/terminal-status.mjs +6 -2
- package/src/runtime/shared/tool-card-model.golden-cases.mjs +2 -1
- package/src/runtime/shared/tool-result-summary.mjs +1 -0
- package/src/runtime/shared/tool-status.mjs +35 -2
- package/src/runtime/tidy/report.mjs +2 -2
- package/src/runtime/tidy/tool.mjs +6 -1
- package/src/tui/dist/index.mjs +88 -37
- package/src/tui/session/session-api/settings.mjs +16 -10
- package/src/vendor/statusline/src/gateway/route-meta.mjs +7 -2
package/package.json
CHANGED
|
@@ -125,7 +125,8 @@ try {
|
|
|
125
125
|
for (const spec of specs) {
|
|
126
126
|
const dataDir = path.join(sandbox, spec.name);
|
|
127
127
|
const db = path.join(dataDir, 'pgdata');
|
|
128
|
-
const
|
|
128
|
+
const runtimeKey = fs.readFileSync(path.join(dataDir, 'runtime', 'active-version'), 'utf8').trim();
|
|
129
|
+
const ctl = path.join(dataDir, 'runtime', `runtime-${runtimeKey}`, 'bin', 'pg_ctl.exe');
|
|
129
130
|
if (process.platform === 'win32' && fs.existsSync(ctl) && fs.existsSync(path.join(db, 'postmaster.pid'))) {
|
|
130
131
|
const stopped = spawnSync(ctl, ['-D', db, '-m', 'fast', '-w', 'stop'], { stdio: 'inherit' });
|
|
131
132
|
if (stopped.status !== 0) cleanupFailed = true;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Decides the release tag a memory runtime build publishes under.
|
|
2
|
+
//
|
|
3
|
+
// Published runtime assets are immutable: every app that shipped a manifest
|
|
4
|
+
// naming a tag verifies that tag's assets by size and sha256, so replacing an
|
|
5
|
+
// asset under a shipped tag breaks the next runtime install of all of those
|
|
6
|
+
// apps. A build that would replace an asset of a shipped tag (any tag at or
|
|
7
|
+
// below the bundled manifest's release_tag) moves to the next patch tag; the
|
|
8
|
+
// manifest sync then points new app builds at it.
|
|
9
|
+
//
|
|
10
|
+
// CLI: node scripts/plan-runtime-release-tag.mjs <requested-tag> <shipped-tag> <needed-assets-file> <published-assets-file>
|
|
11
|
+
// prints the tag to build. The asset files hold one asset name per line.
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
13
|
+
import { pathToFileURL } from 'node:url';
|
|
14
|
+
|
|
15
|
+
const TAG_RE = /^runtime-v(\d+)\.(\d+)\.(\d+)$/;
|
|
16
|
+
|
|
17
|
+
function parseTag(tag) {
|
|
18
|
+
const match = TAG_RE.exec(String(tag ?? '').trim());
|
|
19
|
+
if (!match) throw new Error(`invalid runtime release tag: ${tag}`);
|
|
20
|
+
return match.slice(1).map(Number);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function compareTags(left, right) {
|
|
24
|
+
const a = parseTag(left);
|
|
25
|
+
const b = parseTag(right);
|
|
26
|
+
for (let index = 0; index < 3; index += 1) {
|
|
27
|
+
if (a[index] !== b[index]) return a[index] - b[index];
|
|
28
|
+
}
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function planRuntimeReleaseTag({ requestedTag, shippedTag, neededAssets, publishedAssets }) {
|
|
33
|
+
const published = new Set(publishedAssets);
|
|
34
|
+
const replacesPublished = neededAssets.some((name) => published.has(name));
|
|
35
|
+
if (!replacesPublished || compareTags(requestedTag, shippedTag) > 0) return requestedTag;
|
|
36
|
+
const [major, minor, patch] = parseTag(shippedTag);
|
|
37
|
+
return `runtime-v${major}.${minor}.${patch + 1}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readNames(path) {
|
|
41
|
+
return readFileSync(path, 'utf8')
|
|
42
|
+
.split(/\r?\n/)
|
|
43
|
+
.map((line) => line.trim())
|
|
44
|
+
.filter(Boolean);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (import.meta.url === pathToFileURL(process.argv[1] || '').href) {
|
|
48
|
+
const [requestedTag, shippedTag, neededPath, publishedPath] = process.argv.slice(2);
|
|
49
|
+
process.stdout.write(
|
|
50
|
+
`${planRuntimeReleaseTag({
|
|
51
|
+
requestedTag,
|
|
52
|
+
shippedTag,
|
|
53
|
+
neededAssets: readNames(neededPath),
|
|
54
|
+
publishedAssets: readNames(publishedPath),
|
|
55
|
+
})}\n`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -37,6 +37,7 @@ Excel proves the numbers and makes their meaning readable. Keep sourced inputs a
|
|
|
37
37
|
1. Settle the table first: headers, one row per record, consistent units, numeric cells as numbers (not text), dates as ISO strings (both backends store `2026-09-30` as the Excel date under `yyyy-mm-dd`, so date arithmetic is `=D5-DATE(2026,9,22)`, not `DATEVALUE`), percentages as fractions (`0.15`, formatted `0.0%`). A figure written as text is stored as the number Excel types from it, on both backends — `"1,234"` under `#,##0`, `"15%"` as 0.15 under `0%` — and any other text stays exactly as written (`"1-2"`, `"007"`, `"1e3"`, `"TRUE"` never become a date, a 7, a 1000, or a truth value). Keep the same `design.content` model as the deck or document in the same package.
|
|
38
38
|
2. For a report, design the reading order, sheet roles, column proportions, chart placement and intended screen/print use. Author with native cell, style, merge and chart operations by default. `compose_sheet` is an opt-in preset only when its table-plus-panel structure already fits; do not select it merely to save calls. Batch all known operations. Use `describe format:'xlsx' operation:<op>` when fields are unknown; cell styles belong in `properties`.
|
|
39
39
|
3. Add sheets with `add_sheet`, then native ranges and formulas targeting `sheet`. Use `set_formula` for derived cells and `define_name` for reused inputs. Check representative formulas against the intended ranges before extending a model; clean recalculation proves evaluation, not correct modelling. A design specimen is optional, not required for every workbook.
|
|
40
|
+
If `create` fails, stop dependent `open`, `render`, and `finalize` calls: a failed creation is not an artifact. Correct the reported inputs and resume only from a successful `create` result.
|
|
40
41
|
4. Raw tabular files (`.csv`, `.tsv`) take `set_range` with a 2D `values` array and `append_row`; they carry no styles, formulas, or multiple sheets.
|
|
41
42
|
5. `finalize` recalculates and returns current rendered pages for review; add `auditProfile:'financial-model'` for a decision model. Read the actual images as in `report-design.md`, with concrete keep/fix observations about hierarchy, charts and print layout. Correct material issues and rerender, rather than filling a pass checklist. Complete with `design:{ reviewed:true, reviewToken:<current token>, critique:[{ page:1, verdict:'pass', note:<page-specific observations> }, ...] }`. This records agent review, not user approval. CSV/TSV needs no visual approval. Resolve validation failures against their original baseline, never by reopening as a new baseline.
|
|
42
43
|
|
|
@@ -4,6 +4,7 @@ import { countJsonNextCalls } from './tools/next-call-utils.mjs';
|
|
|
4
4
|
import { parseGrepContextHeader, splitGrepLinePrefix } from './tools/builtin/grep-formatting.mjs';
|
|
5
5
|
import { isReadOnlyNavigationMiss } from './session/result-classification.mjs';
|
|
6
6
|
import { nonNegativeInt } from '../../shared/numbers.mjs';
|
|
7
|
+
import { isUserControlCancellation } from '../../shared/tool-status.mjs';
|
|
7
8
|
import {
|
|
8
9
|
appendAgentTrace,
|
|
9
10
|
normalizeSessionId,
|
|
@@ -128,7 +129,7 @@ function traceAgentCompact({
|
|
|
128
129
|
}
|
|
129
130
|
|
|
130
131
|
const TOOL_ARG_KEYS = {
|
|
131
|
-
read: ['path', 'offset', 'limit', 'line', 'context'],
|
|
132
|
+
read: ['file_path', 'path', 'offset', 'limit', 'line', 'context'],
|
|
132
133
|
grep: ['pattern', 'path', 'glob', 'output_mode', 'head_limit', 'offset'],
|
|
133
134
|
glob: ['pattern', 'path', 'head_limit', 'offset', 'sort'],
|
|
134
135
|
find: ['query', 'path', 'head_limit'],
|
|
@@ -211,12 +212,11 @@ function summarizeToolArgs(toolName, args) {
|
|
|
211
212
|
for (const key of keys) {
|
|
212
213
|
if (Object.hasOwn(args, key)) out[key] = compactTraceArgValue(args[key], key);
|
|
213
214
|
}
|
|
214
|
-
|
|
215
|
+
// Arrays are clipped to 6 entries above; the count keeps batch-limit
|
|
216
|
+
// failures (more than 10 read/grep targets) diagnosable.
|
|
217
|
+
for (const countKey of ['edits', 'writes', 'file_path', 'path']) {
|
|
215
218
|
if (Array.isArray(args[countKey])) out[`${countKey}_count`] = args[countKey].length;
|
|
216
219
|
}
|
|
217
|
-
if (toolName === 'read' && Array.isArray(args.path)) {
|
|
218
|
-
out.path_count = args.path.length;
|
|
219
|
-
}
|
|
220
220
|
return Object.keys(out).length ? out : null;
|
|
221
221
|
}
|
|
222
222
|
|
|
@@ -414,17 +414,12 @@ function classifyToolFailure(resultText, toolName) {
|
|
|
414
414
|
if (/\[tool-input-validation\]/.test(text)) return 'schema/args';
|
|
415
415
|
// These are emitted validation/cancellation contracts, not keywords found
|
|
416
416
|
// somewhere in a quoted document, command output, or error preview.
|
|
417
|
-
if (
|
|
418
|
-
/^(?:Browser command interrupted by local user input\.|computer_session_aborted: queued command was cancelled before execution|computer_user_control_active:)/i.test(
|
|
419
|
-
leading
|
|
420
|
-
)
|
|
421
|
-
)
|
|
422
|
-
return 'expected-cancellation';
|
|
417
|
+
if (isUserControlCancellation(leading)) return 'expected-cancellation';
|
|
423
418
|
if (
|
|
424
419
|
/^The arguments provided to `[^`]+` are invalid JSON and could not be parsed:/i.test(leading) ||
|
|
425
420
|
/^new Goal tasks must omit ids; use updates for existing tasks$/i.test(leading) ||
|
|
426
421
|
/^goal task exceeds \d+ characters$/i.test(leading) ||
|
|
427
|
-
/^goal (?:tasks|updates) support at most \d+ entries
|
|
422
|
+
/^goal (?:tasks|updates) support at most \d+ entries(?:$|;)/i.test(leading) ||
|
|
428
423
|
/^goal arguments contain unknown fields:/i.test(leading) ||
|
|
429
424
|
/^Unsupported field for GitHub [\w.]+\.$/i.test(leading) ||
|
|
430
425
|
/^browser action "[^"]+" (?:does not accept input field|requires input\.|accepts only one input target form|fields must be inside input:|input\.[^\n]*(?:must be|requires|accepts at most))/i.test(
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
} from './retry-classifier.mjs';
|
|
22
22
|
import { PROVIDER_FIRST_BYTE_TIMEOUT_MS, createTimeoutSignal } from '../stall-policy.mjs';
|
|
23
23
|
import { noteFastModeCapacityError } from './anthropic-fast-mode.mjs';
|
|
24
|
+
import { noteRequestServiceTier } from '../../../shared/llm/usage-context.mjs';
|
|
24
25
|
import { notifyCurrentAnthropicRateLimit } from './admission-scheduler.mjs';
|
|
25
26
|
|
|
26
27
|
/**
|
|
@@ -103,6 +104,7 @@ export function createAnthropicApiTransport({ client, label, opts, useModel, par
|
|
|
103
104
|
const requestStreamingResponse = () =>
|
|
104
105
|
withRetry(
|
|
105
106
|
async ({ signal: attemptSignal }) => {
|
|
107
|
+
noteRequestServiceTier(params.speed);
|
|
106
108
|
const res = await client.messages.create(params, requestOptions(attemptSignal)).asResponse();
|
|
107
109
|
if (!res.ok) {
|
|
108
110
|
const text = await res.text().catch(() => '');
|
|
@@ -156,7 +158,10 @@ export function createAnthropicApiTransport({ client, label, opts, useModel, par
|
|
|
156
158
|
|
|
157
159
|
const requestNonStreamingMessage = (nonStreamingParams, signal) =>
|
|
158
160
|
withRetry(
|
|
159
|
-
async ({ signal: attemptSignal }) =>
|
|
161
|
+
async ({ signal: attemptSignal }) => {
|
|
162
|
+
noteRequestServiceTier(nonStreamingParams.speed);
|
|
163
|
+
return client.messages.create(nonStreamingParams, requestOptions(attemptSignal));
|
|
164
|
+
},
|
|
160
165
|
{
|
|
161
166
|
...retryPolicy(signal, `${label} Anthropic non-streaming fallback`, `${label}-nonstreaming-request`),
|
|
162
167
|
onRetry: ({ attempt, maxAttempts, lastErr, delayMs }) =>
|
|
@@ -12,6 +12,7 @@ import { traceAgentFetch } from '../agent-trace.mjs';
|
|
|
12
12
|
import { createAbortController } from '../../../shared/abort-controller.mjs';
|
|
13
13
|
import { notifyCurrentAnthropicRateLimit } from './admission-scheduler.mjs';
|
|
14
14
|
import { noteFastModeCapacityError } from './anthropic-fast-mode.mjs';
|
|
15
|
+
import { noteRequestServiceTier } from '../../../shared/llm/usage-context.mjs';
|
|
15
16
|
import {
|
|
16
17
|
ANTHROPIC_RETRY_BACKOFF_MS,
|
|
17
18
|
ANTHROPIC_RETRY_JITTER_RATIO,
|
|
@@ -66,6 +67,7 @@ export function createAnthropicOAuthRequest({
|
|
|
66
67
|
// provider-visible cache breakpoint off the cached one — the
|
|
67
68
|
// exact COLD-turn bug this change fixes. Order is fixed:
|
|
68
69
|
// build → sanitize (once) → mark → prepare image bytes → JSON.stringify.
|
|
70
|
+
noteRequestServiceTier(requestBody?.speed);
|
|
69
71
|
const response = await postMessages({
|
|
70
72
|
accessToken,
|
|
71
73
|
requestBody,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { getAgentApiKey } from '../../../shared/config.mjs';
|
|
3
|
+
import { noteRequestServiceTier } from '../../../shared/llm/usage-context.mjs';
|
|
3
4
|
import {
|
|
4
5
|
knownToolNamesFromOpenAITools,
|
|
5
6
|
parseToolCalls,
|
|
@@ -510,6 +511,15 @@ function selectParameterizedCursorVariant(group, requested, sendOpts = {}) {
|
|
|
510
511
|
};
|
|
511
512
|
}
|
|
512
513
|
|
|
514
|
+
// Fast is a RequestedModel parameter on parameterized models and an id
|
|
515
|
+
// suffix on raw variants.
|
|
516
|
+
function cursorSelectionIsFast(selection) {
|
|
517
|
+
if (selection.parameters?.length) {
|
|
518
|
+
return selection.parameters.some((parameter) => parameter.id === 'fast' && parameter.value === 'true');
|
|
519
|
+
}
|
|
520
|
+
return parseCursorVariantId(selection.modelId).fast;
|
|
521
|
+
}
|
|
522
|
+
|
|
513
523
|
function selectCursorVariant(group, { effort = null, fast = false } = {}) {
|
|
514
524
|
if (!Array.isArray(group) || group.length === 0) return null;
|
|
515
525
|
const normalizedEffort =
|
|
@@ -680,11 +690,13 @@ class CursorProviderBase {
|
|
|
680
690
|
sendOpts.sessionId || sendOpts.providerCacheKey || sendOpts.promptCacheKey || `cursor-call:${randomUUID()}`
|
|
681
691
|
);
|
|
682
692
|
const openAiTools = tools?.length ? toOpenAITools(tools) : undefined;
|
|
683
|
-
const dispatch = () =>
|
|
684
|
-
|
|
693
|
+
const dispatch = () => {
|
|
694
|
+
noteRequestServiceTier(cursorSelectionIsFast(cursorSelection) ? 'fast' : '');
|
|
695
|
+
return this._dispatchChat(
|
|
685
696
|
this._chatBody(messages, { cursorSelection, sessionScope, openAiTools, toolChoice: sendOpts.toolChoice }),
|
|
686
697
|
{ runtime, accessToken, signal, sendOpts, openAiTools }
|
|
687
698
|
);
|
|
699
|
+
};
|
|
688
700
|
let assembled;
|
|
689
701
|
try {
|
|
690
702
|
assembled = await dispatch();
|
|
@@ -702,9 +714,15 @@ class CursorProviderBase {
|
|
|
702
714
|
assembled = await dispatch();
|
|
703
715
|
}
|
|
704
716
|
const rawUsage = assembled.rawUsage;
|
|
717
|
+
// A pinned raw variant (effort/Fast suffix) bills as its catalog model;
|
|
718
|
+
// the Fast part is carried by the request's tier, not the id.
|
|
719
|
+
const pinned = parseCursorVariantId(canonicalCursorModelId(String(model || '').trim()));
|
|
720
|
+
const pricingModel =
|
|
721
|
+
pinned.baseId !== pinned.id && this._cursorCatalog?.groups?.has(pinned.baseId) ? pinned.baseId : undefined;
|
|
705
722
|
return {
|
|
706
723
|
content: assembled.content || '',
|
|
707
724
|
model: model || assembled.model || 'auto',
|
|
725
|
+
...(pricingModel ? { pricingModel } : {}),
|
|
708
726
|
toolCalls: assembled.toolCalls,
|
|
709
727
|
stopReason: assembled.stopReason,
|
|
710
728
|
...(assembled.reasoningContent ? { reasoningContent: assembled.reasoningContent } : {}),
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
resolveProviderCacheKey,
|
|
40
40
|
} from '../agent-runtime/cache-strategy.mjs';
|
|
41
41
|
import { preconnect } from '../../../shared/llm/http-agent.mjs';
|
|
42
|
+
import { noteRequestServiceTier } from '../../../shared/llm/usage-context.mjs';
|
|
42
43
|
import { sendViaHttpSse, _envFlag } from './openai-oauth-http-sse.mjs';
|
|
43
44
|
import { warmCodexClientVersion } from './codex-client-meta.mjs';
|
|
44
45
|
import { CODEX_BACKEND_ORIGIN } from './openai-codex-endpoints.mjs';
|
|
@@ -209,6 +210,8 @@ export class OpenAIOAuthProvider {
|
|
|
209
210
|
const authState = { tokens: await _authP };
|
|
210
211
|
await _verP;
|
|
211
212
|
const body = await _bodyP;
|
|
213
|
+
// Every WS/HTTP attempt of this send carries this body's tier unchanged.
|
|
214
|
+
noteRequestServiceTier(body.service_tier);
|
|
212
215
|
// poolKey != cacheKey by design. poolKey isolates socket/delta state per
|
|
213
216
|
// session. cacheKey is body.prompt_cache_key and affects prompt-cache
|
|
214
217
|
// routing only; Codex handshake identity comes from sendOpts and remains
|
|
@@ -23,6 +23,7 @@ import { applyOpenAIDirectCachePolicy, openAiDirectSupportsFast } from './openai
|
|
|
23
23
|
import { streamCallbacks } from './lib/send-callbacks.mjs';
|
|
24
24
|
import { getAgentApiKey } from '../../../shared/provider-api-key.mjs';
|
|
25
25
|
import { getLlmDispatcher } from '../../../shared/llm/http-agent.mjs';
|
|
26
|
+
import { noteRequestServiceTier } from '../../../shared/llm/usage-context.mjs';
|
|
26
27
|
import { resolveProviderCacheKey, resolveProviderPromptCacheLane } from '../agent-runtime/cache-strategy.mjs';
|
|
27
28
|
|
|
28
29
|
function applyOpenAIDirectFastTier(body, model, opts) {
|
|
@@ -122,6 +123,7 @@ export class OpenAIDirectProvider {
|
|
|
122
123
|
// so a model can opt into Fast even when the OAuth catalog does not
|
|
123
124
|
// advertise a Fast tier for its OAuth endpoint.
|
|
124
125
|
applyOpenAIDirectFastTier(body, useModel, opts);
|
|
126
|
+
noteRequestServiceTier(body.service_tier);
|
|
125
127
|
// Keep public response storage and model-specific cache options out of
|
|
126
128
|
// the shared OAuth payload. Storage opt-out still forces full frames in
|
|
127
129
|
// _computeDelta; retained output items make those frames self-contained.
|
|
@@ -34,7 +34,18 @@ export function optionalGoalTaskChanges({ tasks, updates } = {}) {
|
|
|
34
34
|
export function normalizeGoalTasks(input, previous = [], { strict = false } = {}) {
|
|
35
35
|
if (input == null) return previous.map((task) => ({ ...task }));
|
|
36
36
|
if (!Array.isArray(input)) throw new Error('goal tasks must be an array');
|
|
37
|
-
if (input.length > MAX_GOAL_TASKS)
|
|
37
|
+
if (input.length > MAX_GOAL_TASKS) {
|
|
38
|
+
// Settled rows count toward the cap; set_tasks may omit them (only
|
|
39
|
+
// unfinished tasks must be retained), which is how a long Goal makes room.
|
|
40
|
+
const settled = previous.some((task) => GOAL_TASK_SETTLED.includes(task?.status));
|
|
41
|
+
throw new Error(
|
|
42
|
+
`goal tasks support at most ${MAX_GOAL_TASKS} entries${
|
|
43
|
+
settled
|
|
44
|
+
? '; completed and dropped tasks count, so to add more send set_tasks with only the unfinished tasks plus the new ones'
|
|
45
|
+
: ''
|
|
46
|
+
}`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
38
49
|
const previousByText = new Map(previous.map((task) => [clean(task?.text), task]));
|
|
39
50
|
const reservedIds = new Set([...previous, ...input].map((task) => clean(task?.id)).filter(Boolean));
|
|
40
51
|
let nextId = 1;
|
|
@@ -61,7 +61,7 @@ const ZERO_MATCH_PREFIXES = [
|
|
|
61
61
|
const READ_ONLY_NAVIGATION_TOOLS = new Set(['find', 'glob', 'grep', 'list', 'read', 'code_graph']);
|
|
62
62
|
|
|
63
63
|
const NAVIGATION_MISS_RE =
|
|
64
|
-
/\b(?:enoent|enotdir)\b|path does not exist|directory does not exist|no such (?:file|path)|not found at this path|file not found in
|
|
64
|
+
/\b(?:enoent|enotdir)\b|path does not exist|directory does not exist|no such (?:file|path)|not found at this path|file not found\b|symbol "[^"\n]*" not found in /i;
|
|
65
65
|
|
|
66
66
|
export function isReadOnlyNavigationMiss(toolName, result) {
|
|
67
67
|
if (!READ_ONLY_NAVIGATION_TOOLS.has(String(toolName || '').toLowerCase())) return false;
|
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
* session runtime.
|
|
4
4
|
*/
|
|
5
5
|
import { stripShellExitHeader, toolErrorDisplay } from './tool-result-text.mjs';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
hasBenignExitOutcome,
|
|
8
|
+
normalizeToolTerminalStatus,
|
|
9
|
+
toolResultTerminalStatus,
|
|
10
|
+
} from '../../../../shared/tool-status.mjs';
|
|
7
11
|
import { isReadOnlyNavigationMiss } from '../result-classification.mjs';
|
|
8
12
|
import { formatAggregateDetail, summarizeToolResult, toolLoadingTargets } from '../../../../shared/tool-surface.mjs';
|
|
9
13
|
import { normalizeToolName } from '../../../../shared/tool-primitives.mjs';
|
|
@@ -43,7 +47,7 @@ export function shellCommandExitCode(text) {
|
|
|
43
47
|
export function toolCallOutcome(message, rawText) {
|
|
44
48
|
if (normalizeToolName(message?.toolName || message?.name) === 'git') {
|
|
45
49
|
const exitCode = gitResultExitCode(rawText);
|
|
46
|
-
if (exitCode !== null) return { isCallError: false, isExitError:
|
|
50
|
+
if (exitCode !== null) return { isCallError: false, isExitError: !hasBenignExitOutcome(rawText), exitCode };
|
|
47
51
|
if (gitResultError(rawText)) return { isCallError: true, isExitError: false, exitCode: null };
|
|
48
52
|
}
|
|
49
53
|
const exitCode = shellCommandExitCode(rawText);
|
|
@@ -51,7 +55,7 @@ export function toolCallOutcome(message, rawText) {
|
|
|
51
55
|
// Every completed shell result carries `[exit code: N]` — including 0.
|
|
52
56
|
// A non-zero code is a command failure unless execution marked a known
|
|
53
57
|
// successful no-match/no-change outcome.
|
|
54
|
-
const benignExit = exitCode !== 0 &&
|
|
58
|
+
const benignExit = exitCode !== 0 && hasBenignExitOutcome(rawText);
|
|
55
59
|
return { isCallError: false, isExitError: exitCode !== 0 && !benignExit, exitCode };
|
|
56
60
|
}
|
|
57
61
|
if (isReadOnlyNavigationMiss(message?.toolName || message?.name, rawText)) {
|
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
// shared shell tokenizers, so quoted/commented `;` `|` `grep` can never
|
|
15
15
|
// masquerade as a connector/command and hide a real failure.
|
|
16
16
|
const _SEARCH_HEADS = new Set(['select-string', 'sls', 'grep', 'egrep', 'fgrep', 'findstr']);
|
|
17
|
+
// `git diff` flags whose exit 1 reports a difference, not a failure.
|
|
18
|
+
export const GIT_DIFF_EXIT_SIGNAL_FLAGS = new Set(['--exit-code', '--quiet', '--check']);
|
|
17
19
|
const _GIT_GLOBAL_VALUE_OPTS = new Set([
|
|
18
20
|
'-c',
|
|
19
21
|
'-C',
|
|
@@ -75,12 +77,14 @@ export function _isBenignSearchExitOne(command, exitCode, signal, stderr) {
|
|
|
75
77
|
const head = _normalizeHead(tokens[0]);
|
|
76
78
|
if (_SEARCH_HEADS.has(head)) return true;
|
|
77
79
|
if (head !== 'git') return false;
|
|
78
|
-
// `git [global-opts]
|
|
79
|
-
// diff-index/diff-files/difftool —
|
|
80
|
+
// `git [global-opts] grep ...` (no match), or `git [global-opts] diff ...`
|
|
81
|
+
// only — exact `diff` subcommand, never diff-index/diff-files/difftool —
|
|
82
|
+
// with exit-code semantics.
|
|
80
83
|
let i = 1;
|
|
81
84
|
while (i < tokens.length && tokens[i].startsWith('-')) {
|
|
82
85
|
i += _GIT_GLOBAL_VALUE_OPTS.has(tokens[i]) && !tokens[i].includes('=') ? 2 : 1;
|
|
83
86
|
}
|
|
87
|
+
if (tokens[i] === 'grep') return true;
|
|
84
88
|
if (tokens[i] !== 'diff') return false;
|
|
85
|
-
return tokens.slice(i + 1).some((t) => t
|
|
89
|
+
return tokens.slice(i + 1).some((t) => GIT_DIFF_EXIT_SIGNAL_FLAGS.has(t));
|
|
86
90
|
}
|
|
@@ -3,6 +3,7 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
5
5
|
import { tokenizeDirectArgv } from './shell-direct-exe.mjs';
|
|
6
|
+
import { GIT_DIFF_EXIT_SIGNAL_FLAGS } from './bash-tool/benign-exit.mjs';
|
|
6
7
|
import { withBuiltinPathLocks } from './path-locks.mjs';
|
|
7
8
|
import { withAdvisoryLocks } from './advisory-lock.mjs';
|
|
8
9
|
import { withGitRepoReadLock, withGitRepoWriteLock } from './git-repo-rw-lock.mjs';
|
|
@@ -154,8 +155,13 @@ function commandResult(plan, result, limit) {
|
|
|
154
155
|
const output = capOutput(appendText(String(result.stdout || ''), String(result.stderr || '')), limit);
|
|
155
156
|
if (succeeded(result)) return { text: output, failed: false };
|
|
156
157
|
const processError = result.error || result.timedOut || result.aborted || result.overflow || result.exitCode == null;
|
|
158
|
+
// An exit signal is still a non-zero exit, so a command array stops on it;
|
|
159
|
+
// the outcome line only keeps the card from reporting a failure.
|
|
160
|
+
const exitHeader = exitSignal(plan, result)
|
|
161
|
+
? `exit ${result.exitCode}\n[outcome: no-match]`
|
|
162
|
+
: `exit ${result.exitCode}`;
|
|
157
163
|
return {
|
|
158
|
-
text: appendText(processError ? fail(gitFailureReason(plan, result)) :
|
|
164
|
+
text: appendText(processError ? fail(gitFailureReason(plan, result)) : exitHeader, output),
|
|
159
165
|
failed: true,
|
|
160
166
|
};
|
|
161
167
|
}
|
|
@@ -337,6 +343,18 @@ function succeeded(result) {
|
|
|
337
343
|
return result?.exitCode === 0 && !result.error && !result.timedOut && !result.aborted && !result.overflow;
|
|
338
344
|
}
|
|
339
345
|
|
|
346
|
+
// `git grep` exits 1 for no match and `git diff --exit-code/--quiet/--check`
|
|
347
|
+
// exits 1 to report a difference; the shell tool marks the same exits as
|
|
348
|
+
// signals, so the git tool does too.
|
|
349
|
+
function exitSignal(plan, result) {
|
|
350
|
+
if (result.exitCode !== 1 || String(result.stderr || '').trim()) return false;
|
|
351
|
+
if (plan.operation === 'grep') return true;
|
|
352
|
+
if (plan.operation !== 'diff') return false;
|
|
353
|
+
const separator = plan.args.indexOf('--');
|
|
354
|
+
const options = separator === -1 ? plan.args : plan.args.slice(0, separator);
|
|
355
|
+
return options.some((arg) => GIT_DIFF_EXIT_SIGNAL_FLAGS.has(arg));
|
|
356
|
+
}
|
|
357
|
+
|
|
340
358
|
// ENOENT from the spawn itself is a capability fact, not a git error: the
|
|
341
359
|
// executable is absent. Naming it stops the caller from re-running git a
|
|
342
360
|
// different way to find out.
|
|
@@ -780,8 +798,26 @@ function splitChainedGitCommands(command) {
|
|
|
780
798
|
return commands.length > 1 ? commands : null;
|
|
781
799
|
}
|
|
782
800
|
|
|
783
|
-
|
|
784
|
-
|
|
801
|
+
// The tool only runs git, so a submitted command that omits the leading `git`
|
|
802
|
+
// ("status --short") gets it instead of a rejection. Only whole submitted
|
|
803
|
+
// commands are prefixed; a `&&` piece after the first still has to be a full
|
|
804
|
+
// git command, so non-git chain segments stay refused.
|
|
805
|
+
function withGitPrefix(command) {
|
|
806
|
+
if (typeof command !== 'string') return command;
|
|
807
|
+
const text = String(unwrapQuotedCommand(command)).trim();
|
|
808
|
+
const first = text ? tokenizeDirectArgv(text)?.[0] : '';
|
|
809
|
+
if (!first || /(^|[\\/])git(?:\.exe)?$/i.test(first)) return command;
|
|
810
|
+
return `git ${text}`;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
export async function executeGitTool(rawInput, workDir, options = {}) {
|
|
814
|
+
if (!rawInput || typeof rawInput !== 'object' || Array.isArray(rawInput))
|
|
815
|
+
return fail('git requires an arguments object');
|
|
816
|
+
const input = {
|
|
817
|
+
...rawInput,
|
|
818
|
+
command: Array.isArray(rawInput.command) ? rawInput.command.map(withGitPrefix) : withGitPrefix(rawInput.command),
|
|
819
|
+
};
|
|
820
|
+
if (rawInput.command === undefined) delete input.command;
|
|
785
821
|
const action = input.action ?? 'command';
|
|
786
822
|
if (action === 'stage') {
|
|
787
823
|
if (input.command !== undefined || input.include_stage_ids !== undefined) {
|
|
@@ -7,9 +7,12 @@ import { __mixdogMemoryLog } from './memory-log.mjs';
|
|
|
7
7
|
// Downloads and verifies a prebuilt native PG runtime from the mixdog GitHub
|
|
8
8
|
// release manifest.
|
|
9
9
|
//
|
|
10
|
-
// Layout: <dataDir>/runtime/runtime-{
|
|
10
|
+
// Layout: <dataDir>/runtime/runtime-{key}/ + <dataDir>/runtime/active-version
|
|
11
|
+
// holding {key}. New installs key the directory by version plus asset digest;
|
|
12
|
+
// installs from before that keep their bare-version key and stay valid.
|
|
11
13
|
// Atomic swap: write active-version.tmp then rename → active-version.
|
|
12
|
-
// GC: removes stale runtime-* dirs and staging-* dirs on every ensureRuntime
|
|
14
|
+
// GC: removes stale runtime-* dirs and staging-* dirs on every ensureRuntime
|
|
15
|
+
// call, but only a dir that can be moved aside whole (never one in use).
|
|
13
16
|
//
|
|
14
17
|
// Public API: ensureRuntime(dataDir) → { runtimeDir, pgBinDir, libDir, sharePath, version }
|
|
15
18
|
|
|
@@ -21,6 +24,7 @@ import {
|
|
|
21
24
|
openSync,
|
|
22
25
|
readFileSync,
|
|
23
26
|
readdirSync,
|
|
27
|
+
renameSync,
|
|
24
28
|
rmSync,
|
|
25
29
|
statSync,
|
|
26
30
|
unlinkSync,
|
|
@@ -109,6 +113,38 @@ function runtimeVerDir(runtimeDir, ver) {
|
|
|
109
113
|
return join(runtimeDir, `runtime-${ver}`);
|
|
110
114
|
}
|
|
111
115
|
|
|
116
|
+
// A rebuilt asset of the same version gets its own directory, so installing it
|
|
117
|
+
// never touches the tree a still-running PostgreSQL may hold open.
|
|
118
|
+
function runtimeInstallKey(version, sha256) {
|
|
119
|
+
return `${version}-${String(sha256).slice(0, 12)}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// True while a PostgreSQL from this runtime is running: a running executable
|
|
123
|
+
// cannot be opened for writing (EBUSY on Windows, ETXTBSY on Linux).
|
|
124
|
+
function runtimeDirInUse(dir) {
|
|
125
|
+
const exe = join(dir, 'bin', process.platform === 'win32' ? 'postgres.exe' : 'postgres');
|
|
126
|
+
try {
|
|
127
|
+
closeSync(openSync(exe, 'r+'));
|
|
128
|
+
return false;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
return error?.code === 'EBUSY' || error?.code === 'ETXTBSY';
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Remove a runtime dir only when nothing runs from it, moving it aside whole
|
|
135
|
+
// first. Deleting in place could strip a running PostgreSQL's files and the
|
|
136
|
+
// sha stamp while the locked binaries survive, leaving a tree that looks
|
|
137
|
+
// installed but is not; an in-use dir makes this throw and stays intact.
|
|
138
|
+
function discardRuntimeDir(runtimeDir, dir) {
|
|
139
|
+
if (runtimeDirInUse(dir)) throw new Error(`[runtime-fetcher] runtime dir is in use: ${dir}`);
|
|
140
|
+
const trash = join(
|
|
141
|
+
runtimeDir,
|
|
142
|
+
`staging-discard-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`
|
|
143
|
+
);
|
|
144
|
+
renameSync(dir, trash);
|
|
145
|
+
rmSync(trash, { recursive: true, force: true });
|
|
146
|
+
}
|
|
147
|
+
|
|
112
148
|
function runtimePaths(verDir) {
|
|
113
149
|
return {
|
|
114
150
|
pgBinDir: join(verDir, 'bin'),
|
|
@@ -370,9 +406,11 @@ function gcRuntimeDir(runtimeDir, keepVer) {
|
|
|
370
406
|
continue;
|
|
371
407
|
}
|
|
372
408
|
const dir = join(runtimeDir, name);
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
409
|
+
// A download (staging-{tag}.tar.gz) is guarded by its staging dir's lock.
|
|
410
|
+
const guardName = name.endsWith('.tar.gz') ? name.slice(0, -'.tar.gz'.length) : name;
|
|
411
|
+
const lockPath = stagingLockPath(join(runtimeDir, guardName));
|
|
412
|
+
// Never wipe a staging dir or download guarded by a LIVE lock — that is
|
|
413
|
+
// a sibling process mid-download/extract/swap.
|
|
376
414
|
if (_stagingLockIsLive(lockPath)) continue;
|
|
377
415
|
try {
|
|
378
416
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -382,8 +420,10 @@ function gcRuntimeDir(runtimeDir, keepVer) {
|
|
|
382
420
|
if (existsSync(lockPath)) unlinkSync(lockPath);
|
|
383
421
|
} catch {}
|
|
384
422
|
} else if (name.startsWith('runtime-') && name !== `runtime-${keepVer}`) {
|
|
423
|
+
const path = join(runtimeDir, name);
|
|
385
424
|
try {
|
|
386
|
-
|
|
425
|
+
if (statSync(path).isDirectory()) discardRuntimeDir(runtimeDir, path);
|
|
426
|
+
else rmSync(path, { force: true });
|
|
387
427
|
} catch {}
|
|
388
428
|
}
|
|
389
429
|
}
|
|
@@ -451,11 +491,36 @@ function selectRuntimeAsset(manifest, pkey) {
|
|
|
451
491
|
/** Fast path: the active-version pointer names this version and the extracted
|
|
452
492
|
* tree carries the expected sha256. Returns the reusable dir, else null. */
|
|
453
493
|
function cachedRuntimeDir(runtimeBaseDir, version, sha256) {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
494
|
+
const key = readActiveVersion(runtimeBaseDir);
|
|
495
|
+
if (key !== version && key !== runtimeInstallKey(version, sha256)) return null;
|
|
496
|
+
const verDir = runtimeVerDir(runtimeBaseDir, key);
|
|
497
|
+
return readRuntimeStamp(verDir) === sha256 ? verDir : null;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// The asset digest stamped into a complete runtime tree, '' when absent.
|
|
501
|
+
function readRuntimeStamp(verDir) {
|
|
502
|
+
try {
|
|
503
|
+
return readFileSync(join(verDir, '.version-sha256'), 'utf8').trim();
|
|
504
|
+
} catch {
|
|
505
|
+
return '';
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Publish an extracted, stamped staging tree as runtime-{key} and point
|
|
510
|
+
// active-version at it. A sibling that raced this install may already have
|
|
511
|
+
// published the same asset: that tree is kept (it may be running) and this
|
|
512
|
+
// copy dropped. Any other runtime-{key} tree is an interrupted install of this
|
|
513
|
+
// asset; it is moved aside whole, and an in-use one fails the publish intact.
|
|
514
|
+
function publishRuntimeDir(runtimeBaseDir, stagingDir, key, sha256) {
|
|
515
|
+
const verDir = runtimeVerDir(runtimeBaseDir, key);
|
|
516
|
+
if (readRuntimeStamp(verDir) === sha256) {
|
|
517
|
+
rmSync(stagingDir, { recursive: true, force: true });
|
|
518
|
+
} else {
|
|
519
|
+
if (existsSync(verDir)) discardRuntimeDir(runtimeBaseDir, verDir);
|
|
520
|
+
renameWithRetrySync(stagingDir, verDir);
|
|
521
|
+
}
|
|
522
|
+
writeFileAtomicSync(activeVersionPath(runtimeBaseDir), key, { fsyncDir: true });
|
|
523
|
+
return verDir;
|
|
459
524
|
}
|
|
460
525
|
|
|
461
526
|
/**
|
|
@@ -476,10 +541,11 @@ async function installRuntimeVersion({ runtimeBaseDir, url, sha256, size, versio
|
|
|
476
541
|
// process fail to obtain the lock for its own staging dir).
|
|
477
542
|
const stagingTag = `${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`;
|
|
478
543
|
const stagingDir = join(runtimeBaseDir, `staging-${stagingTag}`);
|
|
479
|
-
|
|
544
|
+
// Named after the staging dir so the same live lock guards it from GC.
|
|
545
|
+
const tarPath = `${stagingDir}.tar.gz`;
|
|
480
546
|
|
|
481
|
-
const
|
|
482
|
-
const
|
|
547
|
+
const key = runtimeInstallKey(version, sha256);
|
|
548
|
+
const verDir = runtimeVerDir(runtimeBaseDir, key);
|
|
483
549
|
|
|
484
550
|
// Cross-process lock: hold the staging lockfile (O_EXCL) for the WHOLE
|
|
485
551
|
// download → extract → swap lifetime. GC in any concurrently-booting sibling
|
|
@@ -527,16 +593,11 @@ async function installRuntimeVersion({ runtimeBaseDir, url, sha256, size, versio
|
|
|
527
593
|
normalizeBinExecBit(stagingDir);
|
|
528
594
|
|
|
529
595
|
// Atomic swap:
|
|
530
|
-
// 1. Rename staging → runtime-{
|
|
596
|
+
// 1. Rename staging → runtime-{key} (or reuse a sibling's identical publish)
|
|
531
597
|
// 2. Write active-version.tmp → rename to active-version
|
|
532
598
|
// Stale dirs cleaned up by GC after.
|
|
533
599
|
try {
|
|
534
|
-
|
|
535
|
-
if (existsSync(verDir)) {
|
|
536
|
-
rmSync(verDir, { recursive: true, force: true });
|
|
537
|
-
}
|
|
538
|
-
renameWithRetrySync(stagingDir, verDir);
|
|
539
|
-
writeFileAtomicSync(avPath, version, { fsyncDir: true });
|
|
600
|
+
publishRuntimeDir(runtimeBaseDir, stagingDir, key, sha256);
|
|
540
601
|
} catch (swapErr) {
|
|
541
602
|
__mixdogMemoryLog(`[runtime-fetcher] atomic swap failed: ${swapErr.message}\n`);
|
|
542
603
|
// Attempt to leave things in a recoverable state: if verDir landed but
|
|
@@ -544,10 +605,10 @@ async function installRuntimeVersion({ runtimeBaseDir, url, sha256, size, versio
|
|
|
544
605
|
throw swapErr;
|
|
545
606
|
}
|
|
546
607
|
|
|
547
|
-
// GC: remove stale runtime-* dirs (anything that isn't runtime-{
|
|
608
|
+
// GC: remove stale runtime-* dirs (anything that isn't runtime-{key}).
|
|
548
609
|
// Still under the lock so the just-swapped staging lockfile reap below sees
|
|
549
610
|
// a consistent view.
|
|
550
|
-
gcRuntimeDir(runtimeBaseDir,
|
|
611
|
+
gcRuntimeDir(runtimeBaseDir, key);
|
|
551
612
|
} finally {
|
|
552
613
|
releaseStagingLock(stagingLock);
|
|
553
614
|
}
|
|
@@ -556,6 +617,16 @@ async function installRuntimeVersion({ runtimeBaseDir, url, sha256, size, versio
|
|
|
556
617
|
return verDir;
|
|
557
618
|
}
|
|
558
619
|
|
|
620
|
+
export const _runtimeLayoutInternals = {
|
|
621
|
+
acquireStagingLock,
|
|
622
|
+
cachedRuntimeDir,
|
|
623
|
+
discardRuntimeDir,
|
|
624
|
+
gcRuntimeDir,
|
|
625
|
+
publishRuntimeDir,
|
|
626
|
+
releaseStagingLock,
|
|
627
|
+
runtimeInstallKey,
|
|
628
|
+
};
|
|
629
|
+
|
|
559
630
|
export async function ensureRuntime(dataDir) {
|
|
560
631
|
const key = resolve(dataDir);
|
|
561
632
|
if (runtimeCache.has(key)) return runtimeCache.get(key);
|