@hecer/yoke 1.21.1 → 1.22.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +27 -0
- package/README.md +6 -1
- package/bench/analyze-codex-comparison.mjs +90 -17
- package/bench/compare-codex.mjs +159 -36
- package/bench/result-schema.mjs +132 -0
- package/canon/manifest.yaml +1 -1
- package/dist/agents/pi-telemetry.js +2 -1
- package/dist/agents/process-streams.js +12 -64
- package/dist/agents/provider-selection.js +12 -0
- package/dist/agents/telemetry.js +52 -52
- package/dist/change/inbox.js +8 -3
- package/dist/check/command.js +69 -17
- package/dist/check/delivery.js +121 -0
- package/dist/cli.js +12 -3
- package/dist/code-intelligence/budgets.js +138 -0
- package/dist/code-intelligence/contracts.js +2 -0
- package/dist/code-intelligence/coordinator.js +156 -84
- package/dist/code-intelligence/evidence.js +87 -34
- package/dist/code-intelligence/mcp-client.js +10 -2
- package/dist/code-intelligence/mcp-server.js +10 -10
- package/dist/dashboard/analytics.js +5 -3
- package/dist/goals/command.js +183 -53
- package/dist/goals/usage.js +87 -0
- package/dist/loop/candidate-cleanup.js +47 -17
- package/dist/loop/candidates.js +17 -11
- package/dist/loop/dispatcher.js +89 -26
- package/dist/loop/failure.js +104 -0
- package/dist/loop/gate-snapshot.js +19 -0
- package/dist/loop/git.js +1 -1
- package/dist/loop/loop.js +100 -66
- package/dist/loop/parallel-adapters.js +22 -4
- package/dist/loop/parallel-command.js +32 -5
- package/dist/loop/recovery.js +23 -5
- package/dist/loop/reporter.js +21 -4
- package/dist/loop/run-command.js +95 -46
- package/dist/loop/runner.js +5 -4
- package/dist/loop/worker.js +145 -91
- package/dist/observability/history.js +1 -1
- package/dist/observability/invocation.js +42 -0
- package/dist/observability/usage.js +17 -0
- package/dist/prd/command.js +20 -7
- package/dist/prd/decompose.js +5 -2
- package/dist/retrofit/config.js +26 -1
- package/dist/retrofit/gitignore.js +2 -0
- package/dist/routing/attempts.js +241 -0
- package/dist/routing/capability.js +13 -9
- package/dist/routing/optimization.js +73 -0
- package/dist/routing/registry.js +7 -1
- package/dist/routing/router.js +280 -127
- package/dist/setup/command.js +8 -2
- package/dist/smoke/command.js +302 -74
- package/docs/BENCHMARK-MANIFEST.md +131 -0
- package/docs/CODE-INTELLIGENCE.md +43 -1
- package/docs/CODEX-COMPARISON-2026-09-29.md +15 -0
- package/docs/DELIVERY-JOURNEYS.md +199 -0
- package/docs/ECONOMIC-ROUTING.md +180 -0
- package/docs/GOALS.md +61 -4
- package/docs/RELEASE-VALIDATION-1.22.0.md +115 -0
- package/docs/parallel-execution.md +37 -9
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { shorten } from './evidence.js';
|
|
2
|
+
const TRUNCATED = 'BUDGET_EXCEEDED: result truncated; coverage is partial.';
|
|
3
|
+
/** The provider/tokenizer is unknown to this facade. Count one budget unit per
|
|
4
|
+
* UTF-8 byte conservatively, including the response's own metrics. This is not
|
|
5
|
+
* provider-measured token usage. JSON-RPC transport framing is outside this
|
|
6
|
+
* structuredContent response and is not included. */
|
|
7
|
+
export function measureResponse(response) {
|
|
8
|
+
response.metrics.token_count_kind = 'estimated';
|
|
9
|
+
response.metrics.token_count_method = 'utf8_bytes_conservative';
|
|
10
|
+
for (let i = 0; i < 10; i++) {
|
|
11
|
+
const bytes = Buffer.byteLength(JSON.stringify(response), 'utf8');
|
|
12
|
+
if (response.metrics.returned_bytes === bytes && response.metrics.result_tokens === bytes)
|
|
13
|
+
return response;
|
|
14
|
+
response.metrics.returned_bytes = bytes;
|
|
15
|
+
response.metrics.result_tokens = bytes;
|
|
16
|
+
}
|
|
17
|
+
return response;
|
|
18
|
+
}
|
|
19
|
+
export function responseByteLimit(limits) {
|
|
20
|
+
return Math.max(0, Math.floor(Math.min(limits.tokenBudget, limits.maxBytes)));
|
|
21
|
+
}
|
|
22
|
+
/** This fixed-size failure is the only exception when a caller's budget cannot
|
|
23
|
+
* even represent the required error envelope. It never echoes backend content. */
|
|
24
|
+
export function budgetError(response) {
|
|
25
|
+
return measureResponse({
|
|
26
|
+
schema_version: response.schema_version, request_id: shorten(response.request_id, 64), workspace_id: shorten(response.workspace_id, 128), snapshot_id: shorten(response.snapshot_id, 128),
|
|
27
|
+
status: 'error', data: null,
|
|
28
|
+
coverage: { backends_requested: [], backends_used: [], backends_missing: [], structural: 'unavailable', semantic: 'unavailable', documents: 'unavailable' },
|
|
29
|
+
provenance: [], warnings: [], metrics: { latency_ms: response.metrics.latency_ms, result_tokens: 0, token_count_kind: 'estimated', returned_bytes: 0 }, artifact_uri: null,
|
|
30
|
+
error: { code: 'BUDGET_EXCEEDED', message: 'Budget cannot represent the mandatory response envelope.' },
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function markPartial(response) {
|
|
34
|
+
if (response.status === 'success')
|
|
35
|
+
response.status = 'partial';
|
|
36
|
+
for (const key of ['structural', 'semantic', 'documents'])
|
|
37
|
+
if (response.coverage[key] !== 'unavailable')
|
|
38
|
+
response.coverage[key] = 'partial';
|
|
39
|
+
}
|
|
40
|
+
function usedEvidence(value, ids = new Set()) {
|
|
41
|
+
if (Array.isArray(value))
|
|
42
|
+
for (const entry of value)
|
|
43
|
+
usedEvidence(entry, ids);
|
|
44
|
+
else if (value && typeof value === 'object')
|
|
45
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
46
|
+
if (key === 'evidence_ids' && Array.isArray(entry)) {
|
|
47
|
+
for (const id of entry)
|
|
48
|
+
if (typeof id === 'string')
|
|
49
|
+
ids.add(id);
|
|
50
|
+
}
|
|
51
|
+
else
|
|
52
|
+
usedEvidence(entry, ids);
|
|
53
|
+
}
|
|
54
|
+
return ids;
|
|
55
|
+
}
|
|
56
|
+
function pruneProvenance(response) {
|
|
57
|
+
const ids = usedEvidence(response.data);
|
|
58
|
+
response.provenance = response.provenance.filter(item => item.evidence_id && ids.has(item.evidence_id));
|
|
59
|
+
}
|
|
60
|
+
function shortenLargestText(value) {
|
|
61
|
+
const fields = [];
|
|
62
|
+
function visit(node) {
|
|
63
|
+
if (Array.isArray(node))
|
|
64
|
+
for (const item of node)
|
|
65
|
+
visit(item);
|
|
66
|
+
else if (node && typeof node === 'object')
|
|
67
|
+
for (const [key, entry] of Object.entries(node)) {
|
|
68
|
+
if (['excerpt', 'signature', 'message', 'label'].includes(key) && typeof entry === 'string' && entry.length > 64)
|
|
69
|
+
fields.push({ object: node, key, value: entry });
|
|
70
|
+
else if (entry && typeof entry === 'object')
|
|
71
|
+
visit(entry);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
visit(value);
|
|
75
|
+
const field = fields.sort((a, b) => Buffer.byteLength(b.value) - Buffer.byteLength(a.value))[0];
|
|
76
|
+
if (!field)
|
|
77
|
+
return false;
|
|
78
|
+
field.object[field.key] = shorten(field.value, Math.max(63, Math.floor(field.value.length / 2))) + '…';
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
function removeResultEntry(value) {
|
|
82
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
83
|
+
return false;
|
|
84
|
+
const data = value;
|
|
85
|
+
// All facade result collections remain arrays; identifiers and operation
|
|
86
|
+
// receipts stay intact. Never replace typed data with a prose excerpt.
|
|
87
|
+
const keys = ['items', 'symbols', 'references', 'diagnostics', 'nodes', 'edges', 'semantic_findings', 'structural_candidates', 'documents', 'suggested_test_paths', 'unresolved', 'changed_paths'];
|
|
88
|
+
const candidates = keys.filter(key => Array.isArray(data[key]) && data[key].length > 0);
|
|
89
|
+
candidates.sort((a, b) => Buffer.byteLength(JSON.stringify(data[b].at(-1))) - Buffer.byteLength(JSON.stringify(data[a].at(-1))));
|
|
90
|
+
const key = candidates[0];
|
|
91
|
+
if (!key)
|
|
92
|
+
return false;
|
|
93
|
+
data[key].pop();
|
|
94
|
+
if (key === 'nodes') {
|
|
95
|
+
const ids = new Set(data.nodes.map((node) => node.id));
|
|
96
|
+
data.edges = data.edges.filter((edge) => ids.has(edge.from) && ids.has(edge.to));
|
|
97
|
+
data.frontier_remaining = (data.frontier_remaining ?? 0) + 1;
|
|
98
|
+
data.traversal_complete = false;
|
|
99
|
+
}
|
|
100
|
+
else if (key === 'edges') {
|
|
101
|
+
data.frontier_remaining = (data.frontier_remaining ?? 0) + 1;
|
|
102
|
+
data.traversal_complete = false;
|
|
103
|
+
}
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
/** Bound the complete structured response, not just result.data. Retained data
|
|
107
|
+
* continues to satisfy its shape, and retained evidence links stay resolvable. */
|
|
108
|
+
export function fitResponse(response, limits) {
|
|
109
|
+
const limit = responseByteLimit(limits);
|
|
110
|
+
if (measureResponse(response).metrics.returned_bytes <= limit)
|
|
111
|
+
return response;
|
|
112
|
+
markPartial(response);
|
|
113
|
+
response.warnings = [TRUNCATED, ...response.warnings.filter(item => item !== TRUNCATED).map(item => shorten(item, 256))];
|
|
114
|
+
pruneProvenance(response);
|
|
115
|
+
while (measureResponse(response).metrics.returned_bytes > limit) {
|
|
116
|
+
if (shortenLargestText(response.data))
|
|
117
|
+
continue;
|
|
118
|
+
if (removeResultEntry(response.data)) {
|
|
119
|
+
pruneProvenance(response);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (response.warnings.length > 1) {
|
|
123
|
+
response.warnings = [TRUNCATED, 'Additional warnings omitted.'];
|
|
124
|
+
if (measureResponse(response).metrics.returned_bytes <= limit)
|
|
125
|
+
break;
|
|
126
|
+
response.warnings = [TRUNCATED];
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
// A long diagnostic is allowed to shrink, but error codes and rejection
|
|
130
|
+
// status survive; an exhausted error body becomes the bounded budget error.
|
|
131
|
+
if (response.error && response.error.message.length > 64) {
|
|
132
|
+
response.error.message = shorten(response.error.message, 63) + '…';
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
return budgetError(response);
|
|
136
|
+
}
|
|
137
|
+
return response;
|
|
138
|
+
}
|
|
@@ -5,6 +5,7 @@ export const StatusSchema = z.enum(['success', 'partial', 'blocked', 'error']);
|
|
|
5
5
|
export const ResolutionSchema = z.enum(['resolved', 'unresolved', 'ambiguous', 'not_applicable']);
|
|
6
6
|
export const FreshnessSchema = z.enum(['current', 'stale', 'unknown']);
|
|
7
7
|
export const ProvenanceSchema = z.object({
|
|
8
|
+
evidence_id: z.string().min(1).optional(),
|
|
8
9
|
backend: BackendNameSchema,
|
|
9
10
|
version: z.string().min(1),
|
|
10
11
|
source_path: z.string().nullable(),
|
|
@@ -26,6 +27,7 @@ export const MetricsSchema = z.object({
|
|
|
26
27
|
latency_ms: z.number().nonnegative(),
|
|
27
28
|
result_tokens: z.number().int().nonnegative(),
|
|
28
29
|
token_count_kind: z.enum(['exact', 'estimated']),
|
|
30
|
+
token_count_method: z.literal('utf8_bytes_conservative').optional(),
|
|
29
31
|
returned_bytes: z.number().int().nonnegative(),
|
|
30
32
|
});
|
|
31
33
|
export const ErrorSchema = z.object({
|
|
@@ -3,19 +3,18 @@ import { copyFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { ContextInputSchema, EditApplyInputSchema, EditPreviewInputSchema, ImpactInputSchema, SymbolInputSchema, TraceInputSchema, } from './contracts.js';
|
|
5
5
|
import { createGraftAdapter, createGraphifyAdapter, createSerenaAdapter } from './adapters/index.js';
|
|
6
|
-
import { normalizeEdges, normalizeItems, normalizeSymbols } from './evidence.js';
|
|
7
|
-
import {
|
|
6
|
+
import { normalizeEdges, normalizeItems, normalizeSymbols, normalizeReferenceEdges, serenaSymbolId, shorten } from './evidence.js';
|
|
7
|
+
import { budgetError, fitResponse, measureResponse, responseByteLimit } from './budgets.js';
|
|
8
|
+
import { assertSafePath, assertSnapshotCurrent, createSnapshot, loadSnapshot, workspaceId } from './snapshots.js';
|
|
8
9
|
import { applyEditPlan } from './transactions.js';
|
|
9
10
|
import { createEditPlan, loadEditPlan } from './edit-plans.js';
|
|
10
11
|
function nowMs() { return Date.now(); }
|
|
11
|
-
function compact(value, maxBytes) {
|
|
12
|
-
const encoded = JSON.stringify(value) ?? '';
|
|
13
|
-
if (Buffer.byteLength(encoded) <= maxBytes)
|
|
14
|
-
return value;
|
|
15
|
-
return { truncated: true, excerpt: encoded.slice(0, Math.max(0, maxBytes - 64)) };
|
|
16
|
-
}
|
|
17
12
|
function statusFor(missing, hard = false) { return hard ? 'error' : missing.length ? 'partial' : 'success'; }
|
|
18
13
|
function errorCode(message) {
|
|
14
|
+
if (/deadline|timed out/iu.test(message))
|
|
15
|
+
return 'BACKEND_TIMEOUT';
|
|
16
|
+
if (/budget/iu.test(message))
|
|
17
|
+
return 'BUDGET_EXCEEDED';
|
|
19
18
|
if (/outside|workspace|symbolic/u.test(message))
|
|
20
19
|
return 'WORKSPACE_DENIED';
|
|
21
20
|
if (/stale/u.test(message))
|
|
@@ -34,6 +33,8 @@ function errorCode(message) {
|
|
|
34
33
|
return 'SCOPE_VIOLATION';
|
|
35
34
|
return 'INVALID_ARGUMENT';
|
|
36
35
|
}
|
|
36
|
+
const INPUT_SCHEMAS = { code_context: ContextInputSchema, code_symbol: SymbolInputSchema, code_trace: TraceInputSchema, code_impact: ImpactInputSchema, code_edit_preview: EditPreviewInputSchema, code_edit_apply: EditApplyInputSchema };
|
|
37
|
+
const SYNCHRONOUS_DEADLINE = 'Request deadline exceeded during synchronous processing; retained receipts describe completed operations.';
|
|
37
38
|
function symbolArgs(symbol, fallbackPath = '') {
|
|
38
39
|
if (symbol.startsWith('serena:')) {
|
|
39
40
|
try {
|
|
@@ -64,47 +65,75 @@ export class CodeIntelligenceCoordinator {
|
|
|
64
65
|
async dispatch(name, input) {
|
|
65
66
|
const requestId = randomUUID();
|
|
66
67
|
const started = nowMs();
|
|
67
|
-
const suppliedWorkspace = typeof input === 'object' && input !== null && typeof input.workspace_id === 'string' ? input.workspace_id : this.workspace_id;
|
|
68
68
|
let snapshot;
|
|
69
|
+
let limits = { tokenBudget: this.options.limits?.tokenBudget ?? 2400, maxBytes: this.options.limits?.maxBytes ?? 2_000_000 };
|
|
69
70
|
try {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
71
|
+
const schema = INPUT_SCHEMAS[name];
|
|
72
|
+
if (!schema)
|
|
73
|
+
throw new Error(`unknown code-intelligence tool: ${shorten(name, 64)}`);
|
|
74
|
+
const parsed = schema.parse(input);
|
|
75
|
+
if ('token_budget' in parsed)
|
|
76
|
+
limits = { ...limits, tokenBudget: Math.min(limits.tokenBudget, parsed.token_budget) };
|
|
77
|
+
const timeout = Math.min(this.options.limits?.timeoutMs ?? 10000, 'timeout_ms' in parsed ? parsed.timeout_ms : 10000);
|
|
78
|
+
const state = { used: [], missing: [], warnings: [], deadline: started + timeout, limits };
|
|
79
|
+
const minimum = budgetError(this.errorResponse(requestId, this.workspace_id, '0'.repeat(64), started, new Error('budget')));
|
|
80
|
+
if (minimum.metrics.returned_bytes > responseByteLimit(limits))
|
|
81
|
+
return minimum;
|
|
82
|
+
snapshot = this.snapshotFor(parsed);
|
|
83
|
+
state.receiptFits = data => {
|
|
84
|
+
const envelope = {
|
|
85
|
+
...this.errorResponse(requestId, this.workspace_id, snapshot.snapshot_id, started, new Error('budget')),
|
|
86
|
+
status: 'partial', data, error: null, warnings: [SYNCHRONOUS_DEADLINE],
|
|
87
|
+
};
|
|
88
|
+
// Reserve a late-operation warning and the maximum finite latency width
|
|
89
|
+
// before a transaction can mutate. Apply returns this exact public shape.
|
|
90
|
+
envelope.metrics.latency_ms = Number.MAX_SAFE_INTEGER;
|
|
91
|
+
return measureResponse(envelope).metrics.returned_bytes <= responseByteLimit(limits);
|
|
92
|
+
};
|
|
76
93
|
let result;
|
|
77
94
|
switch (name) {
|
|
78
95
|
case 'code_context':
|
|
79
|
-
result = await this.context(
|
|
96
|
+
result = await this.context(parsed, snapshot, state);
|
|
80
97
|
break;
|
|
81
98
|
case 'code_symbol':
|
|
82
|
-
result = await this.symbol(
|
|
99
|
+
result = await this.symbol(parsed, snapshot, state);
|
|
83
100
|
break;
|
|
84
101
|
case 'code_trace':
|
|
85
|
-
result = await this.trace(
|
|
102
|
+
result = await this.trace(parsed, snapshot, state);
|
|
86
103
|
break;
|
|
87
104
|
case 'code_impact':
|
|
88
|
-
result = await this.impact(
|
|
105
|
+
result = await this.impact(parsed, snapshot, state);
|
|
89
106
|
break;
|
|
90
107
|
case 'code_edit_preview':
|
|
91
|
-
result = await this.preview(
|
|
108
|
+
result = await this.preview(parsed, snapshot, state);
|
|
92
109
|
break;
|
|
93
110
|
case 'code_edit_apply':
|
|
94
|
-
result = await this.apply(
|
|
111
|
+
result = await this.apply(parsed, snapshot, state);
|
|
95
112
|
break;
|
|
96
113
|
default: throw new Error(`unknown code-intelligence tool: ${name}`);
|
|
97
114
|
}
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
115
|
+
const read = !name.startsWith('code_edit_');
|
|
116
|
+
if (read)
|
|
117
|
+
result.warnings.push('Backend freshness and exhaustive coverage are unverified; candidates may require source checks.');
|
|
118
|
+
let response = fitResponse({
|
|
101
119
|
schema_version: '0.1.0', request_id: requestId, workspace_id: this.workspace_id, snapshot_id: snapshot.snapshot_id,
|
|
102
|
-
status: statusFor(result.missing
|
|
103
|
-
warnings: result.warnings, metrics: { latency_ms: nowMs() - started, result_tokens:
|
|
104
|
-
};
|
|
120
|
+
status: result.hard ? 'error' : read ? 'partial' : statusFor(result.missing), data: result.data, coverage: this.coverage(result.used, result.missing), provenance: result.provenance,
|
|
121
|
+
warnings: [...new Set(result.warnings)], metrics: { latency_ms: nowMs() - started, result_tokens: 0, token_count_kind: 'estimated', returned_bytes: 0 }, artifact_uri: null, error: null,
|
|
122
|
+
}, limits);
|
|
123
|
+
response.metrics.latency_ms = nowMs() - started;
|
|
124
|
+
response = fitResponse(response, limits);
|
|
125
|
+
// A filesystem operation or normalization cannot be preempted mid-call.
|
|
126
|
+
// Check after reduction too, and never hide a receipt for an applied edit.
|
|
127
|
+
if (nowMs() >= state.deadline && response.status !== 'error' && response.status !== 'blocked') {
|
|
128
|
+
response.status = 'partial';
|
|
129
|
+
response.warnings.push(SYNCHRONOUS_DEADLINE);
|
|
130
|
+
response.metrics.latency_ms = nowMs() - started;
|
|
131
|
+
response = fitResponse(response, limits);
|
|
132
|
+
}
|
|
133
|
+
return response;
|
|
105
134
|
}
|
|
106
135
|
catch (error) {
|
|
107
|
-
return this.errorResponse(requestId, this.workspace_id, snapshot.
|
|
136
|
+
return fitResponse(this.errorResponse(requestId, this.workspace_id, snapshot?.snapshot_id ?? '0'.repeat(64), started, error), limits);
|
|
108
137
|
}
|
|
109
138
|
}
|
|
110
139
|
snapshotFor(input) {
|
|
@@ -116,23 +145,15 @@ export class CodeIntelligenceCoordinator {
|
|
|
116
145
|
assertSnapshotCurrent(this.root, snapshot, this.options.policy);
|
|
117
146
|
return snapshot;
|
|
118
147
|
}
|
|
119
|
-
safeSnapshotId() {
|
|
120
|
-
try {
|
|
121
|
-
return createSnapshot(this.root, this.options.policy, this.workspace_id).snapshot_id;
|
|
122
|
-
}
|
|
123
|
-
catch {
|
|
124
|
-
return '0'.repeat(64);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
148
|
coverage(used, missing) {
|
|
128
149
|
return {
|
|
129
150
|
backends_requested: [...new Set([...used, ...missing])], backends_used: [...new Set(used)], backends_missing: [...new Set(missing)],
|
|
130
|
-
structural: used.includes('graft') ? '
|
|
131
|
-
semantic: used.includes('serena-lsp') ? '
|
|
132
|
-
documents: used.includes('graphify') ? '
|
|
151
|
+
structural: used.includes('graft') ? 'partial' : 'unavailable',
|
|
152
|
+
semantic: used.includes('serena-lsp') ? 'partial' : 'unavailable',
|
|
153
|
+
documents: used.includes('graphify') ? 'partial' : 'unavailable',
|
|
133
154
|
};
|
|
134
155
|
}
|
|
135
|
-
async call(backend, tool, args, timeout, state) {
|
|
156
|
+
async call(backend, tool, args, timeout, state, adapter = this.adapters[backend]) {
|
|
136
157
|
const seen = new Set([...state.used, ...state.missing]);
|
|
137
158
|
const maxBackends = this.options.limits?.maxBackends ?? 3;
|
|
138
159
|
if (!seen.has(backend) && seen.size >= maxBackends) {
|
|
@@ -140,8 +161,17 @@ export class CodeIntelligenceCoordinator {
|
|
|
140
161
|
state.warnings.push(`${backend}: backend budget exceeded`);
|
|
141
162
|
return null;
|
|
142
163
|
}
|
|
164
|
+
let timer;
|
|
143
165
|
try {
|
|
144
|
-
const
|
|
166
|
+
const remaining = Math.min(timeout, state.deadline - nowMs());
|
|
167
|
+
if (remaining <= 0)
|
|
168
|
+
throw new Error('request deadline exceeded; backend was not started');
|
|
169
|
+
const expired = new Promise((_resolve, reject) => {
|
|
170
|
+
timer = setTimeout(() => { reject(new Error('request deadline exceeded')); void adapter.close().catch(() => { }); }, remaining);
|
|
171
|
+
});
|
|
172
|
+
const value = await Promise.race([adapter.call({ tool, arguments: args }, remaining), expired]);
|
|
173
|
+
if (nowMs() >= state.deadline)
|
|
174
|
+
throw new Error('request deadline exceeded; late backend result discarded');
|
|
145
175
|
if (!state.used.includes(backend))
|
|
146
176
|
state.used.push(backend);
|
|
147
177
|
return value;
|
|
@@ -149,35 +179,39 @@ export class CodeIntelligenceCoordinator {
|
|
|
149
179
|
catch (error) {
|
|
150
180
|
if (!state.missing.includes(backend))
|
|
151
181
|
state.missing.push(backend);
|
|
152
|
-
state.warnings.push(`${backend}: ${error.message}`);
|
|
182
|
+
state.warnings.push(`${backend}: ${shorten(error.message, 256)}`);
|
|
153
183
|
return null;
|
|
154
184
|
}
|
|
185
|
+
finally {
|
|
186
|
+
if (timer)
|
|
187
|
+
clearTimeout(timer);
|
|
188
|
+
}
|
|
155
189
|
}
|
|
156
190
|
evidenceOptions(snapshot, backend, maxChars = 12000) {
|
|
157
191
|
return { root: this.root, snapshot, adapter: this.adapters[backend], maxChars };
|
|
158
192
|
}
|
|
159
|
-
async context(input, snapshot) {
|
|
160
|
-
const state = { used: [], missing: [], warnings: [] };
|
|
193
|
+
async context(input, snapshot, state) {
|
|
161
194
|
const provenance = [];
|
|
162
195
|
let items = [];
|
|
163
|
-
const raw = await this.call('graft', 'context', { query: input.query, limit: Math.max(1, Math.floor(
|
|
196
|
+
const raw = await this.call('graft', 'context', { query: input.query, limit: Math.max(1, Math.floor(state.limits.tokenBudget / 300)), ...(input.paths[0] ? { in: input.paths[0] } : {}) }, input.timeout_ms, state);
|
|
164
197
|
if (raw !== null) {
|
|
165
198
|
const normalized = normalizeItems(raw, this.evidenceOptions(snapshot, 'graft'), 'code', 100);
|
|
166
199
|
items.push(...normalized.items);
|
|
167
200
|
provenance.push(...normalized.provenance);
|
|
201
|
+
state.warnings.push(...normalized.warnings);
|
|
168
202
|
}
|
|
169
203
|
if (input.include_docs) {
|
|
170
|
-
const docs = await this.call('graphify', 'context', { question: input.query, depth: 3, token_budget:
|
|
204
|
+
const docs = await this.call('graphify', 'context', { question: input.query, depth: 3, token_budget: state.limits.tokenBudget }, input.timeout_ms, state);
|
|
171
205
|
if (docs !== null) {
|
|
172
206
|
const normalized = normalizeItems(docs, this.evidenceOptions(snapshot, 'graphify'), 'document', 50);
|
|
173
207
|
items.push(...normalized.items);
|
|
174
208
|
provenance.push(...normalized.provenance);
|
|
209
|
+
state.warnings.push(...normalized.warnings);
|
|
175
210
|
}
|
|
176
211
|
}
|
|
177
212
|
return { data: { items, next_cursor: null }, provenance, ...state, warnings: state.warnings };
|
|
178
213
|
}
|
|
179
|
-
async symbol(input, snapshot) {
|
|
180
|
-
const state = { used: [], missing: [], warnings: [] };
|
|
214
|
+
async symbol(input, snapshot, state) {
|
|
181
215
|
const provenance = [];
|
|
182
216
|
let symbols = [];
|
|
183
217
|
let references = [];
|
|
@@ -196,13 +230,15 @@ export class CodeIntelligenceCoordinator {
|
|
|
196
230
|
const normalized = normalizeSymbols(symbol, this.evidenceOptions(snapshot, 'serena-lsp'));
|
|
197
231
|
symbols.push(...normalized.symbols);
|
|
198
232
|
provenance.push(...normalized.provenance);
|
|
233
|
+
state.warnings.push(...normalized.warnings);
|
|
199
234
|
}
|
|
200
235
|
if (input.include.includes('references')) {
|
|
201
236
|
const raw = await this.call('serena-lsp', 'references', reference, input.timeout_ms, state);
|
|
202
237
|
if (raw !== null) {
|
|
203
|
-
const normalized =
|
|
204
|
-
references = normalized.
|
|
238
|
+
const normalized = normalizeSymbols(raw, this.evidenceOptions(snapshot, 'serena-lsp'));
|
|
239
|
+
references = normalized.symbols.map(symbol => ({ symbol_id: input.symbol_id ?? serenaSymbolId(reference.name_path, reference.relative_path), path: symbol.path, line: null, excerpt: shorten(symbol.signature ?? '', 4000), evidence_ids: symbol.evidence_ids }));
|
|
205
240
|
provenance.push(...normalized.provenance);
|
|
241
|
+
state.warnings.push(...normalized.warnings);
|
|
206
242
|
}
|
|
207
243
|
}
|
|
208
244
|
if (input.include.includes('implementations')) {
|
|
@@ -211,45 +247,83 @@ export class CodeIntelligenceCoordinator {
|
|
|
211
247
|
const normalized = normalizeSymbols(raw, this.evidenceOptions(snapshot, 'serena-lsp'));
|
|
212
248
|
symbols.push(...normalized.symbols);
|
|
213
249
|
provenance.push(...normalized.provenance);
|
|
250
|
+
state.warnings.push(...normalized.warnings);
|
|
214
251
|
}
|
|
215
252
|
}
|
|
216
253
|
if (input.include.includes('diagnostics')) {
|
|
217
254
|
const raw = await this.call('serena-lsp', 'diagnostics', { relative_path: reference.relative_path }, input.timeout_ms, state);
|
|
218
255
|
if (raw !== null) {
|
|
219
256
|
const normalized = normalizeItems(raw, this.evidenceOptions(snapshot, 'serena-lsp'), 'code', 100);
|
|
220
|
-
diagnostics = normalized.items.map(item => ({ path: item.path, line: null, severity: 'unknown', message: item.excerpt, evidence_ids: item.evidence_ids }));
|
|
257
|
+
diagnostics = normalized.items.map(item => ({ path: item.path, line: null, severity: 'unknown', message: shorten(item.excerpt, 4000), evidence_ids: item.evidence_ids }));
|
|
221
258
|
provenance.push(...normalized.provenance);
|
|
259
|
+
state.warnings.push(...normalized.warnings);
|
|
222
260
|
}
|
|
223
261
|
}
|
|
224
262
|
return { data: { symbols, references, diagnostics }, provenance, ...state, warnings: state.warnings };
|
|
225
263
|
}
|
|
226
|
-
async trace(input, snapshot) {
|
|
227
|
-
const state = { used: [], missing: [], warnings: [] };
|
|
264
|
+
async trace(input, snapshot, state) {
|
|
228
265
|
const provenance = [];
|
|
229
266
|
let nodes = [];
|
|
230
267
|
let edges = [];
|
|
231
|
-
const
|
|
232
|
-
if (
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
provenance.push(...normalized.provenance);
|
|
237
|
-
}
|
|
238
|
-
if (input.require_resolved || input.relations.some(relation => relation === 'implements' || relation === 'extends')) {
|
|
239
|
-
const rawSemantic = await this.call('serena-lsp', 'references', symbolArgs(input.symbol_id), input.timeout_ms, state);
|
|
240
|
-
if (rawSemantic !== null) {
|
|
241
|
-
const normalized = normalizeEdges(rawSemantic, this.evidenceOptions(snapshot, 'serena-lsp'), 'references');
|
|
268
|
+
const reference = symbolArgs(input.symbol_id);
|
|
269
|
+
if (input.relations.includes('calls') && !input.require_resolved) {
|
|
270
|
+
const raw = await this.call('graft', 'trace', { symbol: reference.name_path, direction: input.direction, depth: input.max_depth }, input.timeout_ms, state);
|
|
271
|
+
if (raw !== null) {
|
|
272
|
+
const normalized = normalizeEdges(raw, this.evidenceOptions(snapshot, 'graft'), 'calls');
|
|
242
273
|
nodes.push(...normalized.nodes);
|
|
243
|
-
edges.push(...normalized.edges);
|
|
244
274
|
provenance.push(...normalized.provenance);
|
|
275
|
+
state.warnings.push(...normalized.warnings);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const supported = input.direction === 'in' ? input.relations.filter((relation) => relation === 'references' || relation === 'implements') : [];
|
|
279
|
+
if (supported.length && !reference.relative_path) {
|
|
280
|
+
const raw = await this.call('serena-lsp', 'symbol', { name_path_pattern: reference.name_path, relative_path: '', depth: 0, include_body: false, max_matches: 2 }, input.timeout_ms, state);
|
|
281
|
+
if (raw !== null) {
|
|
282
|
+
const normalized = normalizeSymbols(raw, this.evidenceOptions(snapshot, 'serena-lsp'));
|
|
283
|
+
provenance.push(...normalized.provenance);
|
|
284
|
+
state.warnings.push(...normalized.warnings);
|
|
285
|
+
if (normalized.symbols.length === 1)
|
|
286
|
+
Object.assign(reference, symbolArgs(normalized.symbols[0].symbol_id));
|
|
287
|
+
else
|
|
288
|
+
state.warnings.push('reference target is unresolved or ambiguous; use a path-bound symbol_id');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (supported.length && reference.relative_path) {
|
|
292
|
+
let validTarget = false;
|
|
293
|
+
try {
|
|
294
|
+
validTarget = snapshot.files.some(file => file.path === assertSafePath(this.root, reference.relative_path, this.options.policy));
|
|
245
295
|
}
|
|
296
|
+
catch { /* unresolved target is reported below */ }
|
|
297
|
+
if (!validTarget)
|
|
298
|
+
state.warnings.push('reference target is absent from the permitted snapshot');
|
|
299
|
+
else
|
|
300
|
+
for (const relation of supported) {
|
|
301
|
+
const raw = await this.call('serena-lsp', relation === 'implements' ? 'implementations' : 'references', reference, input.timeout_ms, state);
|
|
302
|
+
if (raw === null)
|
|
303
|
+
continue;
|
|
304
|
+
const target = { ...reference, symbol_id: serenaSymbolId(reference.name_path, reference.relative_path) };
|
|
305
|
+
const normalized = normalizeReferenceEdges(raw, this.evidenceOptions(snapshot, 'serena-lsp'), target, relation);
|
|
306
|
+
nodes.unshift(...normalized.nodes);
|
|
307
|
+
edges.push(...normalized.edges);
|
|
308
|
+
provenance.push(...normalized.provenance);
|
|
309
|
+
state.warnings.push(...normalized.warnings);
|
|
310
|
+
}
|
|
311
|
+
if (input.max_depth > 1)
|
|
312
|
+
state.warnings.push('Semantic trace contains one-hop results; deeper traversal is unverified.');
|
|
246
313
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
314
|
+
const unsupported = input.relations.filter(relation => !supported.includes(relation));
|
|
315
|
+
if (unsupported.length)
|
|
316
|
+
state.warnings.push(`No validated ${input.direction} edge contract for: ${unsupported.join(', ')}`);
|
|
317
|
+
const allNodes = [...new Map(nodes.map(node => [node.id, node])).values()];
|
|
318
|
+
nodes = allNodes.slice(0, input.max_nodes);
|
|
319
|
+
const ids = new Set(nodes.map(node => node.id));
|
|
320
|
+
edges = [...new Map(edges.filter(edge => ids.has(edge.from) && ids.has(edge.to)).map(edge => [`${edge.from}\0${edge.to}\0${edge.relation}`, edge])).values()];
|
|
321
|
+
const omitted = allNodes.length - nodes.length;
|
|
322
|
+
if (omitted)
|
|
323
|
+
state.warnings.push(`${omitted} trace nodes omitted by max_nodes; traversal is incomplete.`);
|
|
324
|
+
return { data: { nodes, edges, frontier_remaining: omitted, traversal_complete: false }, provenance, ...state, warnings: state.warnings };
|
|
250
325
|
}
|
|
251
|
-
async impact(input, snapshot) {
|
|
252
|
-
const state = { used: [], missing: [], warnings: [] };
|
|
326
|
+
async impact(input, snapshot, state) {
|
|
253
327
|
const provenance = [];
|
|
254
328
|
const structural = [];
|
|
255
329
|
const semantic = [];
|
|
@@ -287,7 +361,7 @@ export class CodeIntelligenceCoordinator {
|
|
|
287
361
|
}
|
|
288
362
|
}
|
|
289
363
|
if (input.include_docs) {
|
|
290
|
-
const docs = await this.call('graphify', 'context', { question: `architecture impact ${JSON.stringify(targets)}`, depth: input.max_depth, token_budget:
|
|
364
|
+
const docs = await this.call('graphify', 'context', { question: `architecture impact ${JSON.stringify(targets)}`, depth: input.max_depth, token_budget: state.limits.tokenBudget }, input.timeout_ms, state);
|
|
291
365
|
if (docs !== null) {
|
|
292
366
|
const normalized = normalizeItems(docs, this.evidenceOptions(snapshot, 'graphify'), 'document', 50);
|
|
293
367
|
documents.push(...normalized.items);
|
|
@@ -297,10 +371,9 @@ export class CodeIntelligenceCoordinator {
|
|
|
297
371
|
const suggested = [...new Set(snapshot.files.map(file => file.path).filter(path => /(^|\/)(test|tests|__tests__|spec)(\/|\.)/u.test(path)))].slice(0, 20);
|
|
298
372
|
return { data: { semantic_findings: semantic, structural_candidates: structural, documents, suggested_test_paths: suggested, unresolved }, provenance, ...state, warnings: state.warnings };
|
|
299
373
|
}
|
|
300
|
-
async preview(input, snapshot) {
|
|
374
|
+
async preview(input, snapshot, state) {
|
|
301
375
|
if (this.options.mode !== 'active')
|
|
302
376
|
throw new Error('code edits require code-intelligence mode=active');
|
|
303
|
-
const state = { used: [], missing: [], warnings: [] };
|
|
304
377
|
const provenance = [];
|
|
305
378
|
const sandbox = join(this.root, '.yoke', 'code-intelligence', 'worktrees', `preview-${randomUUID()}`);
|
|
306
379
|
mkdirSync(sandbox, { recursive: true });
|
|
@@ -317,15 +390,8 @@ export class CodeIntelligenceCoordinator {
|
|
|
317
390
|
const reference = symbolArgs(operation.symbol_id);
|
|
318
391
|
const args = 'new_name' in operation ? { ...reference, new_name: operation.new_name } : operation.kind === 'safe_delete' ? { name_path_pattern: reference.name_path, relative_path: reference.relative_path } : { ...reference, body: 'content' in operation ? operation.content : '' };
|
|
319
392
|
const tool = operation.kind === 'safe_delete' ? 'safe_delete' : operation.kind;
|
|
320
|
-
|
|
321
|
-
await adapter.call({ tool, arguments: args }, input.timeout_ms);
|
|
322
|
-
state.used.push('serena-lsp');
|
|
323
|
-
}
|
|
324
|
-
catch (error) {
|
|
325
|
-
state.missing.push('serena-lsp');
|
|
326
|
-
state.warnings.push(`serena-lsp: ${error.message}`);
|
|
393
|
+
if (await this.call('serena-lsp', tool, args, input.timeout_ms, state, adapter) === null)
|
|
327
394
|
break;
|
|
328
|
-
}
|
|
329
395
|
}
|
|
330
396
|
}
|
|
331
397
|
finally {
|
|
@@ -350,22 +416,28 @@ export class CodeIntelligenceCoordinator {
|
|
|
350
416
|
const plan = createEditPlan(this.root, snapshot, sandbox, input.operations, state.missing.length ? 'incomplete' : 'passed', input.validation_profile, [...changed], diff);
|
|
351
417
|
return { data: plan, provenance, ...state, warnings: state.warnings };
|
|
352
418
|
}
|
|
353
|
-
async apply(input, snapshot) {
|
|
419
|
+
async apply(input, snapshot, state) {
|
|
354
420
|
if (this.options.mode !== 'active')
|
|
355
421
|
throw new Error('code edits require code-intelligence mode=active');
|
|
356
|
-
|
|
422
|
+
if (nowMs() >= state.deadline)
|
|
423
|
+
throw new Error('request deadline exceeded before apply');
|
|
357
424
|
const plan = loadEditPlan(this.root, input.plan_id);
|
|
358
425
|
if (plan.workspace_id !== this.workspace_id || plan.snapshot_id !== input.expected_snapshot_id)
|
|
359
426
|
throw new Error('edit plan scope does not match workspace or expected snapshot');
|
|
360
427
|
const expected = loadSnapshot(this.root, input.expected_snapshot_id);
|
|
361
428
|
assertSnapshotCurrent(this.root, expected, this.options.policy);
|
|
429
|
+
const receipt = { plan_id: plan.plan_id, transaction_id: `tx-${'0'.repeat(36)}`, changed_paths: plan.changed_paths, new_snapshot_id: expected.snapshot_id, validation: plan.validation, committed: false, merged: false };
|
|
430
|
+
if (!state.receiptFits?.(receipt))
|
|
431
|
+
throw new Error('Budget cannot represent the required apply receipt; no transaction was started');
|
|
432
|
+
if (nowMs() >= state.deadline)
|
|
433
|
+
throw new Error('request deadline exceeded before apply');
|
|
362
434
|
const result = applyEditPlan(this.root, plan, expected, input.idempotency_key);
|
|
363
435
|
return { data: { plan_id: result.plan_id, transaction_id: result.transaction_id, changed_paths: result.changed_paths, new_snapshot_id: result.new_snapshot_id, validation: result.validation, committed: false, merged: false }, provenance: [], ...state, warnings: state.warnings };
|
|
364
436
|
}
|
|
365
437
|
errorResponse(requestId, workspace, snapshotId, started, error) {
|
|
366
438
|
const code = errorCode(error.message);
|
|
367
439
|
const blocked = ['WORKSPACE_DENIED', 'SNAPSHOT_STALE', 'APPROVAL_REQUIRED', 'PLAN_EXPIRED', 'CAPABILITY_UNAVAILABLE', 'SCOPE_VIOLATION', 'WRITE_CONFLICT'].includes(code);
|
|
368
|
-
return { schema_version: '0.1.0', request_id: requestId, workspace_id: workspace, snapshot_id: snapshotId, status: blocked ? 'blocked' : 'error', data: null, coverage: { backends_requested: [], backends_used: [], backends_missing: [], structural: 'unavailable', semantic: 'unavailable', documents: 'unavailable' }, provenance: [], warnings: [], metrics: { latency_ms: nowMs() - started, result_tokens: 0, token_count_kind: '
|
|
440
|
+
return measureResponse({ schema_version: '0.1.0', request_id: requestId, workspace_id: workspace, snapshot_id: snapshotId, status: blocked ? 'blocked' : 'error', data: null, coverage: { backends_requested: [], backends_used: [], backends_missing: [], structural: 'unavailable', semantic: 'unavailable', documents: 'unavailable' }, provenance: [], warnings: [], metrics: { latency_ms: nowMs() - started, result_tokens: 0, token_count_kind: 'estimated', returned_bytes: 0 }, artifact_uri: null, error: { code, message: shorten(error.message, 256) } });
|
|
369
441
|
}
|
|
370
442
|
}
|
|
371
443
|
export function createCodeIntelligenceCoordinator(root, options) { return new CodeIntelligenceCoordinator(root, options); }
|