@magnusekdahl/parallix 1.3.2 → 1.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/agents.md +1 -1
- package/docs/use-cases.md +1 -1
- package/lib/agents/agents.js +20 -3
- package/lib/agents/agents.ts +14 -3
- package/lib/agents/mistral-telemetry.js +122 -23
- package/lib/agents/mistral-telemetry.ts +141 -26
- package/lib/agents/mistral.js +128 -14
- package/lib/agents/mistral.ts +145 -5
- package/lib/commands/active.js +69 -39
- package/lib/commands/active.ts +97 -60
- package/lib/commands/config.ts +3 -3
- package/lib/commands/coverage-gate.ts +1 -1
- package/lib/commands/draft.js +1 -1
- package/lib/commands/draft.ts +1 -1
- package/lib/commands/handoff.js +13 -8
- package/lib/commands/handoff.ts +24 -18
- package/lib/commands/rebase.js +1 -1
- package/lib/commands/rebase.ts +2 -2
- package/lib/commands/repair-handoff.js +141 -20
- package/lib/commands/repair-handoff.ts +185 -45
- package/lib/commands/resolve-conflict.js +1 -1
- package/lib/commands/resolve-conflict.ts +2 -2
- package/lib/commands/stats-backfill.ts +10 -10
- package/lib/commands/stats.js +38 -11
- package/lib/commands/stats.ts +40 -96
- package/lib/core/fmt.ts +2 -2
- package/lib/core/git.ts +2 -2
- package/lib/core/gitignore.ts +2 -2
- package/lib/core/mission-utils.js +2 -2
- package/lib/core/mission-utils.ts +2 -2
- package/lib/core/persistent-data-migration.ts +2 -2
- package/lib/core/spawn-tee.ts +1 -1
- package/lib/core/state-map.ts +2 -2
- package/lib/core/storage.ts +1 -1
- package/lib/core/verification.ts +1 -1
- package/lib/review/rebase.ts +12 -12
- package/lib/review/review-artifacts.ts +35 -35
- package/lib/review/review-commands.ts +40 -40
- package/lib/review/review-events.ts +12 -12
- package/lib/review/review-loop.js +236 -7
- package/lib/review/review-loop.ts +338 -22
- package/lib/review/review-polling.ts +6 -6
- package/lib/review/review-prompts.js +8 -4
- package/lib/review/review-prompts.ts +12 -8
- package/lib/review/review-state.js +1 -1
- package/lib/review/review-state.ts +2 -2
- package/package.json +3 -2
- package/prompts/review-verbose.md +1 -1
- package/prompts/review.md +1 -1
- package/px.js +8 -4
package/docs/agents.md
CHANGED
|
@@ -26,7 +26,7 @@ Opencode (custom agent family) may encounter issues with concurrent tool calls o
|
|
|
26
26
|
|---------|----------------------------------------------------------------------|
|
|
27
27
|
| codex | `codex exec --sandbox danger-full-access --cd <worktree> <prompt>` with a worktree-local `HOME` under `.workflow/codex-home`; resume uses `codex exec resume <session-id-or---last> <prompt>`; the launcher also seeds `.workflow/codex-home/.codex/config.toml` with the repo-standard trusted posture and copies `.codex/auth.json` so headless review commands can start and keep localhost Forgejo access |
|
|
28
28
|
| claude | `claude --dangerously-skip-permissions --output-format stream-json --verbose --include-partial-messages -p <prompt>` (cwd=worktree) — uses `--output-format stream-json --verbose --include-partial-messages` to stream real-time JSONL events (tool calls, assistant text chunks) to the operator's terminal via the spawn-tee mechanism. `--include-partial-messages` is required: without it, the assistant event contains the full response at once and no intermediate progress is emitted. Session-id extraction parses the `result` event from stream-json output, falling back to the `claude --resume <id>` regex on plain text. |
|
|
29
|
-
| mistral | `vibe --prompt <prompt> --trust --output text` (cwd=worktree) — **Note: NOT resume-capable in current Vibe version**; session management uses internal state in `~/.vibe/logs/session/` but does not emit a parseable resume hint to stdout/stderr. |
|
|
29
|
+
| mistral | `vibe --prompt <prompt> --trust --yolo --output text` (cwd=worktree) — `--yolo` approves tool calls non-interactively (mirrors `--dangerously-skip-permissions` for claude/opencode and codex's `trust_level = "trusted"`); `--trust` only bypasses the working-directory trust prompt and does not itself skip tool-call approval. **Note: NOT resume-capable in current Vibe version**; session management uses internal state in `~/.vibe/logs/session/` but does not emit a parseable resume hint to stdout/stderr. |
|
|
30
30
|
| custom | `opencode run --pure --dangerously-skip-permissions <prompt>` (cwd=worktree); resume uses `-s <session>` when a session id is known or `--continue` when only the family marker is known |
|
|
31
31
|
|
|
32
32
|
## Launch output watchdog
|
package/docs/use-cases.md
CHANGED
|
@@ -53,7 +53,7 @@ Each use case carries the four required parts: **(P) persona/buyer**, **(B) befo
|
|
|
53
53
|
### UC-6 — See which agent family actually pays off, across every repo one runtime drives
|
|
54
54
|
|
|
55
55
|
- **(P)** Operator/buyer deciding which paid agent subscriptions to keep or cut.
|
|
56
|
-
- **(B)** *Before:* no durable, cross-repo record of how each agent performs, so the keep/cut decision is a hunch. *After:* a single parallix-owned `stats.csv` accumulates per-agent telemetry (`classification, implementer, pr_fix_rounds`, plus an extended
|
|
56
|
+
- **(B)** *Before:* no durable, cross-repo record of how each agent performs, so the keep/cut decision is a hunch. *After:* a single parallix-owned `stats.csv` accumulates per-agent telemetry (`classification, implementer, pr_fix_rounds`, plus an extended 22-column schema including a `closed` flag) across every repository one runtime drives, keyed so the same mission in different repos stays distinct.
|
|
57
57
|
- **(E)** `lib/commands/stats.js:14` (legacy 5-col schema), `:21-30` (extended schema). Tested: `test/stats.test.js` — `upsertStatsRow writes the workflow stats schema and updates existing missions idempotently`, `task-1314: upsertStatsRow keys on (repo, mission, stage) so same mission in different repos stays distinct`. The kind of agent-comparison this enables is demonstrated in `../visualBoard/docs/missions/2026/task-1023/RETROSPECTIVE_P5.md:198-243` (per-family PRs, reviews/PR, durations).
|
|
58
58
|
- **(C)** **Partial.** Schema and CSV upsert are tested, but the value is bounded: the richest per-agent comparison in the evidence came from Forgejo PR data, not `stats.csv`, and two of four families record honest zeros for token usage (`opencode`/local custom and `mistral`/vibe telemetry are zeroed by design, per `README.md:230-231` describing `opencode-telemetry.js`/`mistral-telemetry.js`). So cross-agent *cost/value* comparison is complete only for `codex` and `claude` today.
|
|
59
59
|
|
package/lib/agents/agents.js
CHANGED
|
@@ -115,11 +115,14 @@ const NON_BLOCKING_LAUNCH_ERROR_PATTERNS = Object.freeze([
|
|
|
115
115
|
/\bmodel\s+(?:identifier|id)\s+(?:is\s+)?invalid\b/i,
|
|
116
116
|
/\b(?:model\s+not\s+found|no\s+such\s+model)\b/i,
|
|
117
117
|
/\bunknown\s+option\b/i,
|
|
118
|
+
/\bunsupported\s+(flag|option)\b/i,
|
|
118
119
|
/\bauth(?:entication)?\b/i,
|
|
119
120
|
/\bunauthorized\b/i,
|
|
120
121
|
/\bforbidden\b/i,
|
|
121
122
|
/\bapi\s+key\b/i,
|
|
122
|
-
/\bread-only file system\b/i
|
|
123
|
+
/\bread-only file system\b/i,
|
|
124
|
+
/\b(home|bootstrap)\s+(error|failed|cannot|denied|not\s+found)\b/i,
|
|
125
|
+
/\bpermission\s+denied\b/i
|
|
123
126
|
]);
|
|
124
127
|
function workflowLauncherStatus(agent) {
|
|
125
128
|
const resolver = RESOLVERS[agent];
|
|
@@ -162,6 +165,10 @@ function commandInPath(name) {
|
|
|
162
165
|
});
|
|
163
166
|
return result.status === 0 && result.stdout.trim().length > 0;
|
|
164
167
|
}
|
|
168
|
+
// Deterministic config/setup errors (invalid model IDs, auth failures,
|
|
169
|
+
// unsupported CLI flags, home/bootstrap failures) must not poison the
|
|
170
|
+
// persistent blocklist — only transient failures (runtime crashes, network
|
|
171
|
+
// errors) deserve a block. Custom agents are never blocked.
|
|
165
172
|
function shouldPersistLaunchFailureBlock(agent, result) {
|
|
166
173
|
if (!result || agent === 'custom') {
|
|
167
174
|
return false;
|
|
@@ -543,7 +550,7 @@ function defaultIsAgentBlockedNow(agent) {
|
|
|
543
550
|
const config = readAgentConfig(CONFIG_PATH, {});
|
|
544
551
|
return isAgentBlocked(agent, config);
|
|
545
552
|
}
|
|
546
|
-
catch (
|
|
553
|
+
catch (_err) {
|
|
547
554
|
// If the config is malformed, surface that through the launcher path
|
|
548
555
|
// (assertAgentSupported / launch) instead of silently rerouting. Treat as
|
|
549
556
|
// not-blocked here so the existing error path runs.
|
|
@@ -823,10 +830,20 @@ async function startAgent(step, opts = { prompt: '' }) {
|
|
|
823
830
|
// setup/config errors (invalid model id, auth failure, read-only HOME, etc.)
|
|
824
831
|
// should fall through to the next family without poisoning agents.local.json.
|
|
825
832
|
if (shouldPersistLaunchFailureBlock(chosen || '', result)) {
|
|
833
|
+
let blockReason = 'transient crash';
|
|
834
|
+
if (result?.signal) {
|
|
835
|
+
blockReason = `signal ${result.signal}`;
|
|
836
|
+
}
|
|
837
|
+
else if (result?.error?.code) {
|
|
838
|
+
blockReason = result.error.code;
|
|
839
|
+
}
|
|
840
|
+
else if (result?.status !== null && result?.status !== 0) {
|
|
841
|
+
blockReason = `exit ${result.status}`;
|
|
842
|
+
}
|
|
826
843
|
const blockUntil = (0, limit_hit_js_1.formatBlockUntil)(new Date(Date.now() + limit_hit_js_1.DEFAULT_FALLBACK_HOURS * 60 * 60 * 1000));
|
|
827
844
|
try {
|
|
828
845
|
const blockResult = updateAgentBlockFn(chosen || '', blockUntil);
|
|
829
|
-
log(fmt.status('INFO', `Wrote blocklist entry for ${fmt.agent(chosen || '')} -> ${fmt.path(blockResult.path)} (${limit_hit_js_1.DEFAULT_FALLBACK_HOURS}h block)`));
|
|
846
|
+
log(fmt.status('INFO', `Wrote blocklist entry for ${fmt.agent(chosen || '')} -> ${fmt.path(blockResult.path)} (${limit_hit_js_1.DEFAULT_FALLBACK_HOURS}h block, ${blockReason})`));
|
|
830
847
|
}
|
|
831
848
|
catch (err) {
|
|
832
849
|
log(fmt.status('WARN', `Could not persist blocklist entry for ${fmt.agent(chosen || '')}: ${err.message}`));
|
package/lib/agents/agents.ts
CHANGED
|
@@ -115,11 +115,14 @@ const NON_BLOCKING_LAUNCH_ERROR_PATTERNS = Object.freeze([
|
|
|
115
115
|
/\bmodel\s+(?:identifier|id)\s+(?:is\s+)?invalid\b/i,
|
|
116
116
|
/\b(?:model\s+not\s+found|no\s+such\s+model)\b/i,
|
|
117
117
|
/\bunknown\s+option\b/i,
|
|
118
|
+
/\bunsupported\s+(flag|option)\b/i,
|
|
118
119
|
/\bauth(?:entication)?\b/i,
|
|
119
120
|
/\bunauthorized\b/i,
|
|
120
121
|
/\bforbidden\b/i,
|
|
121
122
|
/\bapi\s+key\b/i,
|
|
122
|
-
/\bread-only file system\b/i
|
|
123
|
+
/\bread-only file system\b/i,
|
|
124
|
+
/\b(home|bootstrap)\s+(error|failed|cannot|denied|not\s+found)\b/i,
|
|
125
|
+
/\bpermission\s+denied\b/i
|
|
123
126
|
]);
|
|
124
127
|
|
|
125
128
|
function workflowLauncherStatus(agent: string): LauncherStatus {
|
|
@@ -168,6 +171,10 @@ function commandInPath(name: string) {
|
|
|
168
171
|
return result.status === 0 && result.stdout.trim().length > 0;
|
|
169
172
|
}
|
|
170
173
|
|
|
174
|
+
// Deterministic config/setup errors (invalid model IDs, auth failures,
|
|
175
|
+
// unsupported CLI flags, home/bootstrap failures) must not poison the
|
|
176
|
+
// persistent blocklist — only transient failures (runtime crashes, network
|
|
177
|
+
// errors) deserve a block. Custom agents are never blocked.
|
|
171
178
|
function shouldPersistLaunchFailureBlock(agent: string, result: LaunchResultLike | null | undefined) {
|
|
172
179
|
if (!result || agent === 'custom') {return false;}
|
|
173
180
|
const combined = [
|
|
@@ -592,7 +599,7 @@ function defaultIsAgentBlockedNow(agent: string) {
|
|
|
592
599
|
try {
|
|
593
600
|
const config = readAgentConfig(CONFIG_PATH, {});
|
|
594
601
|
return isAgentBlocked(agent, config);
|
|
595
|
-
} catch (
|
|
602
|
+
} catch (_err) {
|
|
596
603
|
// If the config is malformed, surface that through the launcher path
|
|
597
604
|
// (assertAgentSupported / launch) instead of silently rerouting. Treat as
|
|
598
605
|
// not-blocked here so the existing error path runs.
|
|
@@ -907,10 +914,14 @@ async function startAgent(step: string, opts: StartAgentOptions = { prompt: '' }
|
|
|
907
914
|
// setup/config errors (invalid model id, auth failure, read-only HOME, etc.)
|
|
908
915
|
// should fall through to the next family without poisoning agents.local.json.
|
|
909
916
|
if (shouldPersistLaunchFailureBlock(chosen || '', result)) {
|
|
917
|
+
let blockReason = 'transient crash';
|
|
918
|
+
if (result?.signal) { blockReason = `signal ${result.signal}`; }
|
|
919
|
+
else if (result?.error?.code) { blockReason = result.error.code; }
|
|
920
|
+
else if (result?.status !== null && result?.status !== 0) { blockReason = `exit ${result.status}`; }
|
|
910
921
|
const blockUntil = formatBlockUntil(new Date(Date.now() + DEFAULT_FALLBACK_HOURS * 60 * 60 * 1000));
|
|
911
922
|
try {
|
|
912
923
|
const blockResult = updateAgentBlockFn(chosen || '', blockUntil);
|
|
913
|
-
log(fmt.status('INFO', `Wrote blocklist entry for ${fmt.agent(chosen || '')} -> ${fmt.path(blockResult.path)} (${DEFAULT_FALLBACK_HOURS}h block)`));
|
|
924
|
+
log(fmt.status('INFO', `Wrote blocklist entry for ${fmt.agent(chosen || '')} -> ${fmt.path(blockResult.path)} (${DEFAULT_FALLBACK_HOURS}h block, ${blockReason})`));
|
|
914
925
|
} catch (err) {
|
|
915
926
|
log(fmt.status('WARN', `Could not persist blocklist entry for ${fmt.agent(chosen || '')}: ${(err as any).message}`));
|
|
916
927
|
}
|
|
@@ -1,40 +1,139 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DEFAULT_MISTRAL_LOG_DIR = void 0;
|
|
7
|
+
exports.parseMistralMeta = parseMistralMeta;
|
|
8
|
+
exports.extractMistralTelemetry = extractMistralTelemetry;
|
|
9
|
+
exports.getMistralProviderModel = getMistralProviderModel;
|
|
10
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
11
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
2
13
|
/**
|
|
3
|
-
* Mistral (Vibe) Telemetry
|
|
14
|
+
* Mistral (Vibe) Telemetry Parser
|
|
15
|
+
*
|
|
16
|
+
* Mistral Vibe writes structured token-usage data to per-session meta files:
|
|
17
|
+
* ~/.vibe/logs/session/<session_id>/meta.json
|
|
4
18
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
19
|
+
* Each meta.json contains a `stats` object with token counts:
|
|
20
|
+
* - session_prompt_tokens → inputTokens
|
|
21
|
+
* - session_completion_tokens → outputTokens
|
|
22
|
+
* - session_total_llm_tokens → totalTokens
|
|
9
23
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
24
|
+
* This module scans that directory for the most recent meta.json, parses the
|
|
25
|
+
* stats block, and returns a telemetry object mirroring the shape used by
|
|
26
|
+
* codex-telemetry.ts.
|
|
13
27
|
*
|
|
14
|
-
* See task-
|
|
15
|
-
* verification is tracked as follow-up task-1288 ("Verify vibe/mistral
|
|
16
|
-
* telemetry once usage-unblocked"), which names the missing evidence and the
|
|
17
|
-
* reason it could not be collected in the task-1285 environment.
|
|
28
|
+
* See task-1288 for the discovery that confirmed the structured source.
|
|
18
29
|
*/
|
|
19
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
-
exports.extractMistralTelemetry = extractMistralTelemetry;
|
|
21
|
-
exports.getMistralProviderModel = getMistralProviderModel;
|
|
22
30
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
31
|
+
* The default directory where Vibe writes its session meta files.
|
|
32
|
+
* Overridden by tests via extractMistralTelemetry(basePath).
|
|
33
|
+
*/
|
|
34
|
+
exports.DEFAULT_MISTRAL_LOG_DIR = node_path_1.default.join(node_os_1.default.homedir(), '.vibe', 'logs', 'session');
|
|
35
|
+
/**
|
|
36
|
+
* Parse the `stats` block from a meta.json file into a telemetry object.
|
|
37
|
+
* Returns null when the content yields no usable signal (missing stats,
|
|
38
|
+
* empty object, or non-object stats).
|
|
39
|
+
*/
|
|
40
|
+
function parseMistralMeta(meta) {
|
|
41
|
+
if (!meta || typeof meta !== 'object') {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const stats = meta.stats;
|
|
45
|
+
if (!stats || typeof stats !== 'object') {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const s = stats;
|
|
49
|
+
const inputTokens = Number(s.session_prompt_tokens) || 0;
|
|
50
|
+
const outputTokens = Number(s.session_completion_tokens) || 0;
|
|
51
|
+
const totalTokens = Number(s.session_total_llm_tokens) || 0;
|
|
52
|
+
// Return null when there is no usable signal (all zeros).
|
|
53
|
+
// Mirrors the codex-telemetry pattern where honest zeros still indicate
|
|
54
|
+
// a parseable source was found but contained no actual usage.
|
|
55
|
+
if (!inputTokens && !outputTokens && !totalTokens) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
inputTokens,
|
|
60
|
+
outputTokens,
|
|
61
|
+
totalTokens,
|
|
62
|
+
contextTokens: Number(s.context_tokens) || 0,
|
|
63
|
+
toolCallsAgreed: Number(s.tool_calls_agreed) || 0,
|
|
64
|
+
toolCallsRejected: Number(s.tool_calls_rejected) || 0,
|
|
65
|
+
toolCallsFailed: Number(s.tool_calls_failed) || 0,
|
|
66
|
+
toolCallsSucceeded: Number(s.tool_calls_succeeded) || 0,
|
|
67
|
+
sessionCost: Number(s.session_cost) || 0,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Scan ~/.vibe/logs/session/ for the most recent session meta.json, parse its
|
|
72
|
+
* stats block, and return a telemetry object.
|
|
25
73
|
*
|
|
26
|
-
* @param
|
|
27
|
-
* @
|
|
74
|
+
* @param result - Legacy launcher result object (ignored; kept for API compat)
|
|
75
|
+
* @param basePath - Override the default session log directory. Used by tests.
|
|
28
76
|
*/
|
|
29
|
-
function extractMistralTelemetry() {
|
|
30
|
-
//
|
|
77
|
+
function extractMistralTelemetry(result, basePath) {
|
|
78
|
+
void result; // legacy param, ignored — telemetry comes from on-disk meta.json
|
|
79
|
+
const scanDir = basePath || exports.DEFAULT_MISTRAL_LOG_DIR;
|
|
80
|
+
// Scan session subdirectories for the newest meta.json.
|
|
81
|
+
// Basenames are session_<YYYYMMDD>_<HHMMSS>_<id>, so alphabetical sort = chronological.
|
|
82
|
+
let sessionDirs;
|
|
83
|
+
try {
|
|
84
|
+
sessionDirs = node_fs_1.default.readdirSync(scanDir);
|
|
85
|
+
}
|
|
86
|
+
catch (_) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const dirs = sessionDirs
|
|
90
|
+
.filter((d) => {
|
|
91
|
+
if (!d.startsWith('session_')) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
const full = node_path_1.default.join(scanDir, d);
|
|
95
|
+
try {
|
|
96
|
+
return node_fs_1.default.statSync(full).isDirectory();
|
|
97
|
+
}
|
|
98
|
+
catch (_) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
.sort();
|
|
103
|
+
if (dirs.length === 0) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
// Walk newest-first; return the first session that has a parseable meta.json.
|
|
107
|
+
for (let i = dirs.length - 1; i >= 0; i--) {
|
|
108
|
+
const metaPath = node_path_1.default.join(scanDir, dirs[i], 'meta.json');
|
|
109
|
+
if (!node_fs_1.default.existsSync(metaPath)) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
let content;
|
|
113
|
+
try {
|
|
114
|
+
content = node_fs_1.default.readFileSync(metaPath, 'utf8');
|
|
115
|
+
}
|
|
116
|
+
catch (_) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
let meta;
|
|
120
|
+
try {
|
|
121
|
+
meta = JSON.parse(content);
|
|
122
|
+
}
|
|
123
|
+
catch (_) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const telemetry = parseMistralMeta(meta);
|
|
127
|
+
if (!telemetry) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
return { ...telemetry, path: metaPath };
|
|
131
|
+
}
|
|
31
132
|
return null;
|
|
32
133
|
}
|
|
33
134
|
/**
|
|
34
135
|
* Return the provider/model pair for mistral tasks.
|
|
35
136
|
* Used as fallback when telemetry is null.
|
|
36
|
-
*
|
|
37
|
-
* @returns {{provider: string, model: string}}
|
|
38
137
|
*/
|
|
39
138
|
function getMistralProviderModel() {
|
|
40
139
|
return { provider: 'mistral', model: 'mistral' };
|
|
@@ -1,44 +1,159 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
1
5
|
/**
|
|
2
|
-
* Mistral (Vibe) Telemetry
|
|
6
|
+
* Mistral (Vibe) Telemetry Parser
|
|
7
|
+
*
|
|
8
|
+
* Mistral Vibe writes structured token-usage data to per-session meta files:
|
|
9
|
+
* ~/.vibe/logs/session/<session_id>/meta.json
|
|
3
10
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
11
|
+
* Each meta.json contains a `stats` object with token counts:
|
|
12
|
+
* - session_prompt_tokens → inputTokens
|
|
13
|
+
* - session_completion_tokens → outputTokens
|
|
14
|
+
* - session_total_llm_tokens → totalTokens
|
|
8
15
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
16
|
+
* This module scans that directory for the most recent meta.json, parses the
|
|
17
|
+
* stats block, and returns a telemetry object mirroring the shape used by
|
|
18
|
+
* codex-telemetry.ts.
|
|
12
19
|
*
|
|
13
|
-
* See task-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
* See task-1288 for the discovery that confirmed the structured source.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The default directory where Vibe writes its session meta files.
|
|
25
|
+
* Overridden by tests via extractMistralTelemetry(basePath).
|
|
17
26
|
*/
|
|
27
|
+
export const DEFAULT_MISTRAL_LOG_DIR = path.join(os.homedir(), '.vibe', 'logs', 'session');
|
|
28
|
+
|
|
29
|
+
interface TelemetryResult {
|
|
30
|
+
inputTokens: number;
|
|
31
|
+
outputTokens: number;
|
|
32
|
+
totalTokens: number;
|
|
33
|
+
contextTokens: number;
|
|
34
|
+
toolCallsAgreed: number;
|
|
35
|
+
toolCallsRejected: number;
|
|
36
|
+
toolCallsFailed: number;
|
|
37
|
+
toolCallsSucceeded: number;
|
|
38
|
+
sessionCost: number;
|
|
39
|
+
path?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface ParseableMeta {
|
|
43
|
+
stats?: Record<string, unknown>;
|
|
44
|
+
[key: string]: unknown;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface StatsBlock {
|
|
48
|
+
session_prompt_tokens?: unknown;
|
|
49
|
+
session_completion_tokens?: unknown;
|
|
50
|
+
session_total_llm_tokens?: unknown;
|
|
51
|
+
context_tokens?: unknown;
|
|
52
|
+
tool_calls_agreed?: unknown;
|
|
53
|
+
tool_calls_rejected?: unknown;
|
|
54
|
+
tool_calls_failed?: unknown;
|
|
55
|
+
tool_calls_succeeded?: unknown;
|
|
56
|
+
session_cost?: unknown;
|
|
57
|
+
[key: string]: unknown;
|
|
58
|
+
}
|
|
18
59
|
|
|
19
60
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
61
|
+
* Parse the `stats` block from a meta.json file into a telemetry object.
|
|
62
|
+
* Returns null when the content yields no usable signal (missing stats,
|
|
63
|
+
* empty object, or non-object stats).
|
|
64
|
+
*/
|
|
65
|
+
export function parseMistralMeta(meta: ParseableMeta | null | undefined): TelemetryResult | null {
|
|
66
|
+
if (!meta || typeof meta !== 'object') {return null;}
|
|
67
|
+
|
|
68
|
+
const stats = meta.stats;
|
|
69
|
+
if (!stats || typeof stats !== 'object') {return null;}
|
|
70
|
+
|
|
71
|
+
const s = stats as StatsBlock;
|
|
72
|
+
const inputTokens = Number(s.session_prompt_tokens) || 0;
|
|
73
|
+
const outputTokens = Number(s.session_completion_tokens) || 0;
|
|
74
|
+
const totalTokens = Number(s.session_total_llm_tokens) || 0;
|
|
75
|
+
|
|
76
|
+
// Return null when there is no usable signal (all zeros).
|
|
77
|
+
// Mirrors the codex-telemetry pattern where honest zeros still indicate
|
|
78
|
+
// a parseable source was found but contained no actual usage.
|
|
79
|
+
if (!inputTokens && !outputTokens && !totalTokens) {return null;}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
inputTokens,
|
|
83
|
+
outputTokens,
|
|
84
|
+
totalTokens,
|
|
85
|
+
contextTokens: Number(s.context_tokens) || 0,
|
|
86
|
+
toolCallsAgreed: Number(s.tool_calls_agreed) || 0,
|
|
87
|
+
toolCallsRejected: Number(s.tool_calls_rejected) || 0,
|
|
88
|
+
toolCallsFailed: Number(s.tool_calls_failed) || 0,
|
|
89
|
+
toolCallsSucceeded: Number(s.tool_calls_succeeded) || 0,
|
|
90
|
+
sessionCost: Number(s.session_cost) || 0,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Scan ~/.vibe/logs/session/ for the most recent session meta.json, parse its
|
|
96
|
+
* stats block, and return a telemetry object.
|
|
22
97
|
*
|
|
23
|
-
* @param
|
|
24
|
-
* @
|
|
98
|
+
* @param result - Legacy launcher result object (ignored; kept for API compat)
|
|
99
|
+
* @param basePath - Override the default session log directory. Used by tests.
|
|
25
100
|
*/
|
|
26
|
-
function extractMistralTelemetry() {
|
|
27
|
-
//
|
|
101
|
+
export function extractMistralTelemetry(result: unknown, basePath?: string): TelemetryResult | null {
|
|
102
|
+
void result; // legacy param, ignored — telemetry comes from on-disk meta.json
|
|
103
|
+
|
|
104
|
+
const scanDir = basePath || DEFAULT_MISTRAL_LOG_DIR;
|
|
105
|
+
|
|
106
|
+
// Scan session subdirectories for the newest meta.json.
|
|
107
|
+
// Basenames are session_<YYYYMMDD>_<HHMMSS>_<id>, so alphabetical sort = chronological.
|
|
108
|
+
let sessionDirs: string[];
|
|
109
|
+
try {
|
|
110
|
+
sessionDirs = fs.readdirSync(scanDir);
|
|
111
|
+
} catch (_) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const dirs = sessionDirs
|
|
116
|
+
.filter((d: string) => {
|
|
117
|
+
if (!d.startsWith('session_')) {return false;}
|
|
118
|
+
const full = path.join(scanDir, d);
|
|
119
|
+
try { return fs.statSync(full).isDirectory(); } catch (_) { return false; }
|
|
120
|
+
})
|
|
121
|
+
.sort();
|
|
122
|
+
|
|
123
|
+
if (dirs.length === 0) {return null;}
|
|
124
|
+
|
|
125
|
+
// Walk newest-first; return the first session that has a parseable meta.json.
|
|
126
|
+
for (let i = dirs.length - 1; i >= 0; i--) {
|
|
127
|
+
const metaPath = path.join(scanDir, dirs[i], 'meta.json');
|
|
128
|
+
if (!fs.existsSync(metaPath)) {continue;}
|
|
129
|
+
|
|
130
|
+
let content: string;
|
|
131
|
+
try {
|
|
132
|
+
content = fs.readFileSync(metaPath, 'utf8');
|
|
133
|
+
} catch (_) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let meta: ParseableMeta;
|
|
138
|
+
try {
|
|
139
|
+
meta = JSON.parse(content);
|
|
140
|
+
} catch (_) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const telemetry = parseMistralMeta(meta);
|
|
145
|
+
if (!telemetry) {continue;}
|
|
146
|
+
|
|
147
|
+
return { ...telemetry, path: metaPath };
|
|
148
|
+
}
|
|
149
|
+
|
|
28
150
|
return null;
|
|
29
151
|
}
|
|
30
152
|
|
|
31
153
|
/**
|
|
32
154
|
* Return the provider/model pair for mistral tasks.
|
|
33
155
|
* Used as fallback when telemetry is null.
|
|
34
|
-
*
|
|
35
|
-
* @returns {{provider: string, model: string}}
|
|
36
156
|
*/
|
|
37
|
-
function getMistralProviderModel() {
|
|
157
|
+
export function getMistralProviderModel(): { provider: string; model: string } {
|
|
38
158
|
return { provider: 'mistral', model: 'mistral' };
|
|
39
159
|
}
|
|
40
|
-
|
|
41
|
-
export {
|
|
42
|
-
extractMistralTelemetry,
|
|
43
|
-
getMistralProviderModel,
|
|
44
|
-
};
|
package/lib/agents/mistral.js
CHANGED
|
@@ -1,22 +1,128 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getMistralProviderModel = void 0;
|
|
3
7
|
exports.buildMistralInvocation = buildMistralInvocation;
|
|
4
8
|
exports.extractMistralSessionId = extractMistralSessionId;
|
|
9
|
+
exports.processResult = processResult;
|
|
5
10
|
exports.resolveMistralCommand = resolveMistralCommand;
|
|
6
11
|
exports.startMistralAgent = startMistralAgent;
|
|
7
12
|
const spawn_tee_js_1 = require("../core/spawn-tee.js");
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
const mistral_telemetry_js_1 = require("./mistral-telemetry.js");
|
|
14
|
+
Object.defineProperty(exports, "getMistralProviderModel", { enumerable: true, get: function () { return mistral_telemetry_js_1.getMistralProviderModel; } });
|
|
15
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
16
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
17
|
+
/**
|
|
18
|
+
* Maximum acceptable age (in minutes) for a mistral session's start_time
|
|
19
|
+
* relative to the invocation start. Sessions older than this window are
|
|
20
|
+
* rejected as potentially misattributed across concurrent missions.
|
|
21
|
+
*/
|
|
22
|
+
const MAX_SESSION_AGE_MINUTES = 120;
|
|
23
|
+
function processResult(result, basePath, invocationStart) {
|
|
24
|
+
if (!result || typeof result !== 'object') {
|
|
25
|
+
return { sessionId: null, telemetry: null };
|
|
26
|
+
}
|
|
27
|
+
const scanDir = basePath || mistral_telemetry_js_1.DEFAULT_MISTRAL_LOG_DIR;
|
|
28
|
+
// Determine the invocation window for session correlation.
|
|
29
|
+
// When invocationStart is provided, only consider sessions whose
|
|
30
|
+
// start_time falls within MAX_SESSION_AGE_MINUTES of the invocation.
|
|
31
|
+
// This prevents cross-mission telemetry misattribution when multiple
|
|
32
|
+
// mistral phases run concurrently against a shared session directory.
|
|
33
|
+
let invokeTime = NaN;
|
|
34
|
+
let invokeWindow = null;
|
|
35
|
+
if (invocationStart) {
|
|
36
|
+
invokeTime = Date.parse(invocationStart);
|
|
37
|
+
if (!Number.isNaN(invokeTime)) {
|
|
38
|
+
const deltaMs = MAX_SESSION_AGE_MINUTES * 60000;
|
|
39
|
+
invokeWindow = { start: invokeTime - deltaMs, end: invokeTime + deltaMs };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Scan session directories chronologically (sorted by basename).
|
|
43
|
+
// For each session, check if its start_time falls within the invocation
|
|
44
|
+
// window, then pick the session closest to the invocation start time.
|
|
45
|
+
// This replaces the previous approach of calling extractMistralTelemetry
|
|
46
|
+
// which always returned the globally newest session regardless of which
|
|
47
|
+
// invocation it belonged to.
|
|
48
|
+
let bestTelemetry = null;
|
|
49
|
+
let bestDistance = Infinity;
|
|
50
|
+
try {
|
|
51
|
+
const entries = node_fs_1.default.readdirSync(scanDir);
|
|
52
|
+
const dirs = entries.filter((d) => d.startsWith('session_')).sort();
|
|
53
|
+
for (const dir of dirs) {
|
|
54
|
+
const metaPath = node_path_1.default.join(scanDir, dir, 'meta.json');
|
|
55
|
+
if (!node_fs_1.default.existsSync(metaPath)) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
let content;
|
|
59
|
+
try {
|
|
60
|
+
content = node_fs_1.default.readFileSync(metaPath, 'utf8');
|
|
61
|
+
}
|
|
62
|
+
catch (_) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
let meta;
|
|
66
|
+
try {
|
|
67
|
+
meta = JSON.parse(content);
|
|
68
|
+
}
|
|
69
|
+
catch (_) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const startTime = meta.start_time;
|
|
73
|
+
if (typeof startTime !== 'string') {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const sessionTime = Date.parse(startTime);
|
|
77
|
+
if (Number.isNaN(sessionTime)) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
// Check invocation window if applicable
|
|
81
|
+
if (invokeWindow && (sessionTime < invokeWindow.start || sessionTime > invokeWindow.end)) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const telemetry = (0, mistral_telemetry_js_1.parseMistralMeta)(meta);
|
|
85
|
+
if (!telemetry) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
// When no invocationStart is provided, pick the first valid session.
|
|
89
|
+
// When invocationStart is provided, pick the session closest in time.
|
|
90
|
+
if (Number.isNaN(invokeTime)) {
|
|
91
|
+
bestTelemetry = telemetry;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
const distance = Math.abs(sessionTime - invokeTime);
|
|
95
|
+
if (distance < bestDistance) {
|
|
96
|
+
bestTelemetry = telemetry;
|
|
97
|
+
bestDistance = distance;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch (_) {
|
|
102
|
+
// Directory unreadable — fall through to null telemetry
|
|
103
|
+
}
|
|
104
|
+
if (!bestTelemetry) {
|
|
105
|
+
return { ...result, sessionId: result.sessionId || null, telemetry: null };
|
|
106
|
+
}
|
|
107
|
+
const allToolCalls = (bestTelemetry.toolCallsAgreed || 0) +
|
|
108
|
+
(bestTelemetry.toolCallsRejected || 0) +
|
|
109
|
+
(bestTelemetry.toolCallsFailed || 0) +
|
|
110
|
+
(bestTelemetry.toolCallsSucceeded || 0);
|
|
111
|
+
const pm = (0, mistral_telemetry_js_1.getMistralProviderModel)();
|
|
112
|
+
const model = bestTelemetry.contextTokens > 0 || bestTelemetry.inputTokens > 0 ? 'mistral' : pm.model;
|
|
113
|
+
result.telemetry = {
|
|
114
|
+
provider: pm.provider,
|
|
115
|
+
model,
|
|
116
|
+
inputTokens: bestTelemetry.inputTokens,
|
|
117
|
+
outputTokens: bestTelemetry.outputTokens,
|
|
118
|
+
cachedTokens: bestTelemetry.contextTokens,
|
|
119
|
+
totalTokens: bestTelemetry.totalTokens,
|
|
120
|
+
toolCalls: allToolCalls,
|
|
121
|
+
usagePercent: null,
|
|
122
|
+
cost_usd: bestTelemetry.sessionCost,
|
|
123
|
+
};
|
|
124
|
+
return { ...result, sessionId: result.sessionId || null };
|
|
125
|
+
}
|
|
20
126
|
function extractMistralSessionId(stdout) {
|
|
21
127
|
void stdout;
|
|
22
128
|
// Vibe does not currently emit a resume hint in programmatic mode.
|
|
@@ -29,7 +135,14 @@ function resolveMistralCommand() {
|
|
|
29
135
|
function buildMistralInvocation({ prompt, worktree, env, resume, sessionId, model = null }) {
|
|
30
136
|
void resume;
|
|
31
137
|
void sessionId;
|
|
32
|
-
|
|
138
|
+
// --trust only bypasses the working-directory trust prompt; tool-call
|
|
139
|
+
// approval is a separate gate that vibe --help documents as controlled by
|
|
140
|
+
// --auto-approve/--yolo. Without it, any prompt that needs a tool call
|
|
141
|
+
// blocks on interactive approval outside a TTY and fails with a generic
|
|
142
|
+
// error, which then gets misread as a real launch failure and persisted
|
|
143
|
+
// to the blocklist (claude/opencode/codex all pass their own equivalent
|
|
144
|
+
// non-interactive bypass already).
|
|
145
|
+
const args = ['--prompt', prompt, '--trust', '--yolo', '--output', 'text'];
|
|
33
146
|
// Vibe programmatic mode does not support --resume flag in the same way
|
|
34
147
|
// as other agents. The --resume flag exists but requires interactive selection
|
|
35
148
|
// or a session picker. Since we cannot reliably pass a session ID via
|
|
@@ -50,11 +163,12 @@ function buildMistralInvocation({ prompt, worktree, env, resume, sessionId, mode
|
|
|
50
163
|
}
|
|
51
164
|
function startMistralAgent({ prompt, worktree, env, resume = false, sessionId = null, model = null, teeOptions = {} }) {
|
|
52
165
|
const invocation = buildMistralInvocation({ prompt, worktree, env, resume, sessionId, model });
|
|
166
|
+
const invocationStart = new Date().toISOString();
|
|
53
167
|
const resultPromise = (0, spawn_tee_js_1.spawnAndTee)(invocation.command, invocation.args, { ...invocation.options, ...teeOptions }).then((result) => {
|
|
54
168
|
if (result && result.stdout) {
|
|
55
169
|
result.sessionId = extractMistralSessionId(result.stdout);
|
|
56
170
|
}
|
|
57
|
-
return result;
|
|
171
|
+
return processResult(result, undefined, invocationStart);
|
|
58
172
|
});
|
|
59
173
|
return { invocation, resultPromise };
|
|
60
174
|
}
|