@zokizuan/satori-mcp 6.2.0 → 6.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -2
- package/dist/core/canonical-symbol-identity.js +33 -5
- package/dist/core/manage-indexing-handlers.js +7 -0
- package/dist/core/manage-types.d.ts +1 -1
- package/dist/core/relationship-backed-call-graph.js +12 -13
- package/dist/core/search-debug-helpers.js +1 -0
- package/dist/core/search-response-helpers.d.ts +3 -2
- package/dist/core/search-response-helpers.js +5 -3
- package/dist/core/search-types.d.ts +1 -0
- package/dist/core/sync.d.ts +13 -0
- package/dist/core/sync.js +122 -51
- package/dist/index.js +31 -4
- package/dist/server/provider-runtime.d.ts +3 -0
- package/dist/server/provider-runtime.js +23 -3
- package/dist/server/shared-runtime-client.d.ts +11 -0
- package/dist/server/shared-runtime-client.js +360 -0
- package/dist/server/shared-runtime-host.d.ts +24 -0
- package/dist/server/shared-runtime-host.js +322 -0
- package/dist/server/shared-runtime-identity.d.ts +51 -0
- package/dist/server/shared-runtime-identity.js +160 -0
- package/dist/server/shared-runtime-lifecycle.d.ts +33 -0
- package/dist/server/shared-runtime-lifecycle.js +259 -0
- package/dist/server/shared-runtime-transport.d.ts +28 -0
- package/dist/server/shared-runtime-transport.js +158 -0
- package/dist/server/shared-runtime.d.ts +83 -0
- package/dist/server/shared-runtime.js +300 -0
- package/dist/server/start-server.d.ts +9 -32
- package/dist/server/start-server.js +15 -142
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -7,12 +7,18 @@ Most users should install Satori through `@zokizuan/satori-cli`. The installer w
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
npm install -g @zokizuan/satori-cli@latest
|
|
11
|
+
satori install --client all
|
|
12
|
+
satori doctor
|
|
12
13
|
```
|
|
13
14
|
|
|
14
15
|
The local Potion runtime currently supports Linux x64, including Windows through WSL2. Connected Voyage and explicit local Ollama configurations are also available. See the [main README](https://github.com/ham-zax/satori#quick-start) for runtime choices.
|
|
15
16
|
|
|
17
|
+
When installed through the CLI, compatible offline Potion + LanceDB clients
|
|
18
|
+
share one private local host, provider/LanceDB state, and one Potion worker. Each
|
|
19
|
+
client remains an independent MCP session. Direct `npx @zokizuan/satori-mcp`
|
|
20
|
+
execution is still isolated and does not join the managed host.
|
|
21
|
+
|
|
16
22
|
Direct package execution is intended for inspection and custom harnesses:
|
|
17
23
|
|
|
18
24
|
```bash
|
|
@@ -64,6 +70,9 @@ In a fresh two-task OpenCode comparison where both arms answered correctly, Sato
|
|
|
64
70
|
- Inbound call-graph evidence is advisory and should be verified before blast-radius edits.
|
|
65
71
|
- Provider, model, dimensions, projection, and vector backend are persisted compatibility identities; changing them requires a reindex.
|
|
66
72
|
- Multiple incompatible live Satori runtimes are blocked from mutating the same publication.
|
|
73
|
+
- Managed offline Potion + LanceDB clients on Linux x64/WSL2 share one private
|
|
74
|
+
local host. Connected providers, Milvus, and explicit Ollama runtimes keep
|
|
75
|
+
the direct per-client lifecycle.
|
|
67
76
|
- Offline neural reranking is not shipped today. The candidate boundary permits a future complete-set local scorer to fail back atomically to exact + BM25 + single-vector ordering.
|
|
68
77
|
|
|
69
78
|
## Development
|
|
@@ -8,6 +8,25 @@ export function buildCanonicalSymbolRegistryView(symbols) {
|
|
|
8
8
|
}
|
|
9
9
|
return symbolsByKey;
|
|
10
10
|
}
|
|
11
|
+
function spanContains(parent, child) {
|
|
12
|
+
if (parent.file !== child.file) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
if (parent.span.startByte !== undefined
|
|
16
|
+
&& parent.span.endByte !== undefined
|
|
17
|
+
&& child.span.startByte !== undefined
|
|
18
|
+
&& child.span.endByte !== undefined) {
|
|
19
|
+
return parent.span.startByte <= child.span.startByte
|
|
20
|
+
&& child.span.endByte <= parent.span.endByte;
|
|
21
|
+
}
|
|
22
|
+
return parent.span.startLine <= child.span.startLine
|
|
23
|
+
&& child.span.endLine <= parent.span.endLine;
|
|
24
|
+
}
|
|
25
|
+
function spanSize(symbol, useBytes) {
|
|
26
|
+
return useBytes
|
|
27
|
+
? Math.max(0, symbol.span.endByte - symbol.span.startByte)
|
|
28
|
+
: Math.max(0, symbol.span.endLine - symbol.span.startLine);
|
|
29
|
+
}
|
|
11
30
|
function resolveParent(input) {
|
|
12
31
|
const { symbol } = input;
|
|
13
32
|
if (!symbol.parentKey) {
|
|
@@ -17,17 +36,26 @@ function resolveParent(input) {
|
|
|
17
36
|
: "missing",
|
|
18
37
|
};
|
|
19
38
|
}
|
|
20
|
-
const
|
|
21
|
-
.filter((candidate) => candidate.symbolInstanceId !== symbol.symbolInstanceId
|
|
22
|
-
|
|
39
|
+
const containingCandidates = (input.registry.get(symbol.parentKey) || [])
|
|
40
|
+
.filter((candidate) => (candidate.symbolInstanceId !== symbol.symbolInstanceId
|
|
41
|
+
&& spanContains(candidate, symbol)));
|
|
42
|
+
if (containingCandidates.length === 0) {
|
|
23
43
|
return { state: "missing" };
|
|
24
44
|
}
|
|
25
|
-
|
|
45
|
+
const byteCandidates = containingCandidates.filter((candidate) => (candidate.span.startByte !== undefined
|
|
46
|
+
&& candidate.span.endByte !== undefined
|
|
47
|
+
&& symbol.span.startByte !== undefined
|
|
48
|
+
&& symbol.span.endByte !== undefined));
|
|
49
|
+
const candidates = byteCandidates.length > 0 ? byteCandidates : containingCandidates;
|
|
50
|
+
const useBytes = byteCandidates.length > 0;
|
|
51
|
+
const innermostSize = Math.min(...candidates.map((candidate) => spanSize(candidate, useBytes)));
|
|
52
|
+
const innermost = candidates.filter((candidate) => spanSize(candidate, useBytes) === innermostSize);
|
|
53
|
+
if (innermost.length !== 1) {
|
|
26
54
|
return { state: "ambiguous" };
|
|
27
55
|
}
|
|
28
56
|
return {
|
|
29
57
|
state: "resolved",
|
|
30
|
-
parentSymbolId:
|
|
58
|
+
parentSymbolId: innermost[0].symbolInstanceId,
|
|
31
59
|
};
|
|
32
60
|
}
|
|
33
61
|
export function projectCanonicalSymbolIdentity(input) {
|
|
@@ -868,6 +868,13 @@ export class ManageIndexingHandlers {
|
|
|
868
868
|
}
|
|
869
869
|
else {
|
|
870
870
|
const operation = persistOperation("blocked");
|
|
871
|
+
if (result.reason === "repair_proof_limit") {
|
|
872
|
+
return this.host.manageResponse("repair", absolutePath, "blocked", result.message, {
|
|
873
|
+
reason: "repair_proof_limit",
|
|
874
|
+
repairProof: result.proof,
|
|
875
|
+
...(operation ? { operation } : {}),
|
|
876
|
+
});
|
|
877
|
+
}
|
|
871
878
|
const createHint = this.host.buildCreateHint(absolutePath);
|
|
872
879
|
return this.host.manageResponse("repair", absolutePath, "blocked", result.message, {
|
|
873
880
|
reason: result.reason || "needs_create",
|
|
@@ -7,7 +7,7 @@ export type ManageIndexAction = (typeof MANAGE_INDEX_ACTIONS)[number];
|
|
|
7
7
|
export type ManageIndexStatus = "ok" | "not_ready" | "not_indexed" | "requires_reindex" | "blocked" | "error";
|
|
8
8
|
export declare const MANAGE_INDEX_STATUS_DETAILS: readonly ["summary", "capabilities", "diagnostics", "full"];
|
|
9
9
|
export type ManageIndexStatusDetail = (typeof MANAGE_INDEX_STATUS_DETAILS)[number];
|
|
10
|
-
export type ManageIndexReason = "indexing" | "not_indexed" | "requires_reindex" | "unnecessary_reindex_ignore_only" | "preflight_unknown" | "backend_timeout" | "remote_delete_pending" | "missing_provider_config" | "vector_backend_unavailable" | "runtime_owner_conflict" | "mutation_in_progress" | "needs_create";
|
|
10
|
+
export type ManageIndexReason = "indexing" | "not_indexed" | "requires_reindex" | "unnecessary_reindex_ignore_only" | "preflight_unknown" | "backend_timeout" | "remote_delete_pending" | "missing_provider_config" | "vector_backend_unavailable" | "runtime_owner_conflict" | "mutation_in_progress" | "needs_create" | "repair_proof_limit";
|
|
11
11
|
export type VectorBackendResponseCode = "ZILLIZ_CLUSTER_STOPPED" | "VECTOR_BACKEND_AUTH_FAILED" | "VECTOR_BACKEND_UNREACHABLE" | "VECTOR_BACKEND_TIMEOUT" | "VECTOR_BACKEND_CONNECTION_CLOSED";
|
|
12
12
|
export type ManageReindexPreflightOutcome = "reindex_required" | "reindex_unnecessary_ignore_only" | "unknown" | "probe_failed";
|
|
13
13
|
export interface ManageIndexToolHint {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { compareContractStrings, getGraphNeighbors, getRelationshipsForSymbol, isTestOrFixturePath, } from "@zokizuan/satori-core";
|
|
2
2
|
import { DEFAULT_CALL_GRAPH_TEST_REFERENCE_LIMIT } from "./call-graph.js";
|
|
3
3
|
import { buildSourceBackedPythonCalleeFallback, buildSourceBackedPythonCallerFallback, } from "./python-call-fallback.js";
|
|
4
|
-
import {
|
|
4
|
+
import { buildInboundVerificationSearchQuery } from "./search-response-helpers.js";
|
|
5
5
|
function compareNullableNumbersAsc(a, b) {
|
|
6
6
|
const left = typeof a === "number" ? a : Number.POSITIVE_INFINITY;
|
|
7
7
|
const right = typeof b === "number" ? b : Number.POSITIVE_INFINITY;
|
|
@@ -28,7 +28,7 @@ export function uniqueInboundCallerSiteFile(notes) {
|
|
|
28
28
|
continue;
|
|
29
29
|
}
|
|
30
30
|
const file = typeof note.file === "string" ? note.file.trim() : "";
|
|
31
|
-
if (!file) {
|
|
31
|
+
if (!file || file === "(aggregate)") {
|
|
32
32
|
continue;
|
|
33
33
|
}
|
|
34
34
|
allSites.add(file);
|
|
@@ -43,6 +43,7 @@ export function uniqueInboundCallerSiteFile(notes) {
|
|
|
43
43
|
return [...preferred][0];
|
|
44
44
|
}
|
|
45
45
|
const MAX_DETAILED_TEST_SUPPRESSED_CALLER_NOTES = 3;
|
|
46
|
+
const INBOUND_COVERAGE_PARTIAL_WARNING = "CALL_GRAPH_INBOUND_COVERAGE_PARTIAL";
|
|
46
47
|
/**
|
|
47
48
|
* Production suppressed callers first; collapse excess test/fixture caller notes into one summary.
|
|
48
49
|
*/
|
|
@@ -408,11 +409,13 @@ export class RelationshipBackedCallGraph {
|
|
|
408
409
|
...combinedEdges.flatMap((edge) => [edge.srcSymbolId, edge.dstSymbolId]),
|
|
409
410
|
]);
|
|
410
411
|
const combinedNodes = this.sortNodes([...nodeById.values()].filter((node) => referencedNodeIds.has(node.symbolId)));
|
|
412
|
+
const hasNoInboundEdges = (input.direction === "callers" || input.direction === "both") && !combinedEdges.some((edge) => (edge.dstSymbolId === input.resolvedSymbol.symbolInstanceId));
|
|
411
413
|
const warnings = [...new Set([
|
|
412
414
|
...neighbors.warnings,
|
|
413
415
|
...(droppedEdgesOutsideSourceSpan > 0 ? [`CALL_GRAPH_EDGE_OUTSIDE_SOURCE_SPAN:${droppedEdgesOutsideSourceSpan}`] : []),
|
|
414
416
|
...(addedDynamicCalleeEdges.length > 0 ? [`SOURCE_BACKED_DYNAMIC_CALLEES:${addedDynamicCalleeEdges.length}`] : []),
|
|
415
417
|
...(addedDynamicCallerEdges.length > 0 ? [`SOURCE_BACKED_DYNAMIC_CALLERS:${addedDynamicCallerEdges.length}`] : []),
|
|
418
|
+
...(hasNoInboundEdges ? [INBOUND_COVERAGE_PARTIAL_WARNING] : []),
|
|
416
419
|
])].sort(compareContractStrings);
|
|
417
420
|
// Sort first for determinism within bands, then production-first inbound note priority.
|
|
418
421
|
const combinedNotes = prioritizeInboundSuppressedNotes(this.sortNotes([
|
|
@@ -420,17 +423,13 @@ export class RelationshipBackedCallGraph {
|
|
|
420
423
|
...(addedDynamicCalleeEdges.length > 0 ? dynamicCalleeFallback.notes : []),
|
|
421
424
|
...(addedDynamicCallerEdges.length > 0 ? dynamicCallerFallback.notes : []),
|
|
422
425
|
]));
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
&& typeof note.detail === "string"
|
|
427
|
-
&& note.detail.includes("caller candidate")));
|
|
428
|
-
// Notes-only inbound: promote executable must: identifier search (no fake edges).
|
|
429
|
-
// path: uses unique suppressed *caller site* file when available — prefer production
|
|
430
|
-
// over test/fixture sites (never the callee defining file alone).
|
|
426
|
+
// Empty inbound traversal: disclose advisory coverage and promote executable
|
|
427
|
+
// must: identifier search without fabricating an edge. path: uses a unique
|
|
428
|
+
// suppressed caller site when proven, never the callee defining file alone.
|
|
431
429
|
let hints;
|
|
432
|
-
if (
|
|
433
|
-
const constructed =
|
|
430
|
+
if (hasNoInboundEdges) {
|
|
431
|
+
const constructed = buildInboundVerificationSearchQuery({
|
|
432
|
+
symbolName: input.resolvedSymbol.name,
|
|
434
433
|
symbolLabel: input.resolvedSymbol.label,
|
|
435
434
|
symbolId: input.resolvedSymbol.symbolInstanceId,
|
|
436
435
|
file: uniqueInboundCallerSiteFile(combinedNotes),
|
|
@@ -446,7 +445,7 @@ export class RelationshipBackedCallGraph {
|
|
|
446
445
|
scope: "runtime",
|
|
447
446
|
resultMode: "grouped",
|
|
448
447
|
},
|
|
449
|
-
reason: "Inbound graph
|
|
448
|
+
reason: "Inbound graph coverage is partial and returned no callers; use deterministic must: search to verify production call sites.",
|
|
450
449
|
},
|
|
451
450
|
],
|
|
452
451
|
};
|
|
@@ -95,6 +95,7 @@ export function buildChangedCodeDebug(input) {
|
|
|
95
95
|
const symbols = changedSymbols.slice(0, input.maxSymbols);
|
|
96
96
|
const cappedDirectCallers = directCallers.slice(0, input.maxDirectCallers);
|
|
97
97
|
return {
|
|
98
|
+
basis: "git_tracked_worktree",
|
|
98
99
|
files,
|
|
99
100
|
symbols,
|
|
100
101
|
directCallers: cappedDirectCallers,
|
|
@@ -20,10 +20,11 @@ export declare function buildSearchGroupRecommendedAction(codebaseRoot: string,
|
|
|
20
20
|
export declare function buildTopRecommendedSearchAction(codebaseRoot: string, results: SearchGroupResult[]): SearchRecommendedNextAction | undefined;
|
|
21
21
|
export declare function buildTopRecommendedRawSearchAction(codebaseRoot: string, results: SearchChunkResult[]): SearchRecommendedNextAction | undefined;
|
|
22
22
|
/**
|
|
23
|
-
* Build a must: identifier query for
|
|
23
|
+
* Build a must: identifier query for empty inbound graph verification.
|
|
24
24
|
* Never feeds raw multi-token labels into must: (operator tokenizer is whitespace-based).
|
|
25
25
|
*/
|
|
26
|
-
export declare function
|
|
26
|
+
export declare function buildInboundVerificationSearchQuery(input: {
|
|
27
|
+
symbolName?: string;
|
|
27
28
|
symbolLabel?: string;
|
|
28
29
|
symbolId?: string;
|
|
29
30
|
file?: string;
|
|
@@ -377,11 +377,13 @@ export function buildTopRecommendedRawSearchAction(codebaseRoot, results) {
|
|
|
377
377
|
};
|
|
378
378
|
}
|
|
379
379
|
/**
|
|
380
|
-
* Build a must: identifier query for
|
|
380
|
+
* Build a must: identifier query for empty inbound graph verification.
|
|
381
381
|
* Never feeds raw multi-token labels into must: (operator tokenizer is whitespace-based).
|
|
382
382
|
*/
|
|
383
|
-
export function
|
|
384
|
-
const
|
|
383
|
+
export function buildInboundVerificationSearchQuery(input) {
|
|
384
|
+
const symbolName = input.symbolName?.trim();
|
|
385
|
+
const identifier = (symbolName && !/\s/.test(symbolName) ? symbolName : undefined)
|
|
386
|
+
|| extractIdentifierFromSymbolLabel(input.symbolLabel)
|
|
385
387
|
|| extractIdentifierFromSymbolLabel(input.symbolId);
|
|
386
388
|
if (!identifier) {
|
|
387
389
|
return { query: "", pathFilterIncluded: false };
|
package/dist/core/sync.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ interface SyncManagerOptions {
|
|
|
10
10
|
mutationLeaseCoordinator?: MutationLeaseCoordinator;
|
|
11
11
|
crossProcessJoinTimeoutMs?: number;
|
|
12
12
|
crossProcessJoinPollMs?: number;
|
|
13
|
+
onLifecycleActivityChanged?: () => void;
|
|
13
14
|
}
|
|
14
15
|
export type FreshnessDecisionMode = 'synced' | 'skipped_recent' | 'skipped_source_unchanged' | 'coalesced' | 'skipped_indexing' | 'skipped_requires_reindex' | 'skipped_source_checkpoint_unavailable' | 'skipped_mutation_in_progress' | 'skipped_missing_path' | 'reconciled_ignore_change' | 'ignore_reload_failed';
|
|
15
16
|
export interface FreshnessDecision {
|
|
@@ -100,6 +101,8 @@ export declare class SyncManager {
|
|
|
100
101
|
private activeSyncs;
|
|
101
102
|
private lastSyncTimes;
|
|
102
103
|
private backgroundSyncTimer;
|
|
104
|
+
private backgroundSyncEnabled;
|
|
105
|
+
private backgroundSyncFlight;
|
|
103
106
|
private watcherModeStarted;
|
|
104
107
|
private watchEnabled;
|
|
105
108
|
private watchDebounceMs;
|
|
@@ -112,6 +115,7 @@ export declare class SyncManager {
|
|
|
112
115
|
private ignoreRulesVersions;
|
|
113
116
|
private pendingIgnoreChangeEdits;
|
|
114
117
|
private activeIgnoreReconciles;
|
|
118
|
+
private activeWatcherSyncs;
|
|
115
119
|
private freshnessEpochs;
|
|
116
120
|
private sourceCheckpointObservations;
|
|
117
121
|
private sourceCheckpointStatuses;
|
|
@@ -120,6 +124,7 @@ export declare class SyncManager {
|
|
|
120
124
|
private readonly mutationLeaseCoordinator?;
|
|
121
125
|
private readonly crossProcessJoinTimeoutMs;
|
|
122
126
|
private readonly crossProcessJoinPollMs;
|
|
127
|
+
private readonly onLifecycleActivityChanged?;
|
|
123
128
|
constructor(context: Context, snapshotManager: SnapshotManager, options?: SyncManagerOptions);
|
|
124
129
|
private bumpFreshnessEpoch;
|
|
125
130
|
getPreparedReadObservation(codebasePath: string): PreparedReadObservationResult;
|
|
@@ -140,7 +145,15 @@ export declare class SyncManager {
|
|
|
140
145
|
private joinCrossProcessSync;
|
|
141
146
|
handleSyncIndex(): Promise<void>;
|
|
142
147
|
startBackgroundSync(): void;
|
|
148
|
+
private scheduleBackgroundSync;
|
|
149
|
+
private runBackgroundSync;
|
|
143
150
|
stopBackgroundSync(): void;
|
|
151
|
+
/**
|
|
152
|
+
* Stops new provider-owned synchronization work and joins every lifecycle
|
|
153
|
+
* flight that may still hold mutation or backend authority.
|
|
154
|
+
*/
|
|
155
|
+
stopAndDrainLifecycle(): Promise<void>;
|
|
156
|
+
getActiveLifecycleOperationCount(): number;
|
|
144
157
|
getWatchDebounceMs(): number;
|
|
145
158
|
private canScheduleWatchSync;
|
|
146
159
|
private getIgnoreRuleVersion;
|
package/dist/core/sync.js
CHANGED
|
@@ -42,6 +42,8 @@ export class SyncManager {
|
|
|
42
42
|
this.activeSyncs = new Map();
|
|
43
43
|
this.lastSyncTimes = new Map();
|
|
44
44
|
this.backgroundSyncTimer = null;
|
|
45
|
+
this.backgroundSyncEnabled = false;
|
|
46
|
+
this.backgroundSyncFlight = null;
|
|
45
47
|
this.watcherModeStarted = false;
|
|
46
48
|
this.watchedCodebases = new Set();
|
|
47
49
|
this.watchers = new Map();
|
|
@@ -52,6 +54,7 @@ export class SyncManager {
|
|
|
52
54
|
this.ignoreRulesVersions = new Map();
|
|
53
55
|
this.pendingIgnoreChangeEdits = new Map();
|
|
54
56
|
this.activeIgnoreReconciles = new Map();
|
|
57
|
+
this.activeWatcherSyncs = new Set();
|
|
55
58
|
this.freshnessEpochs = new Map();
|
|
56
59
|
this.sourceCheckpointObservations = new Map();
|
|
57
60
|
this.sourceCheckpointStatuses = new Map();
|
|
@@ -62,6 +65,7 @@ export class SyncManager {
|
|
|
62
65
|
this.now = options.now || (() => Date.now());
|
|
63
66
|
this.onSyncCompleted = options.onSyncCompleted;
|
|
64
67
|
this.mutationLeaseCoordinator = options.mutationLeaseCoordinator;
|
|
68
|
+
this.onLifecycleActivityChanged = options.onLifecycleActivityChanged;
|
|
65
69
|
this.crossProcessJoinTimeoutMs = Math.max(1, options.crossProcessJoinTimeoutMs ?? DEFAULT_CROSS_PROCESS_JOIN_TIMEOUT_MS);
|
|
66
70
|
this.crossProcessJoinPollMs = Math.max(1, options.crossProcessJoinPollMs ?? DEFAULT_CROSS_PROCESS_JOIN_POLL_MS);
|
|
67
71
|
}
|
|
@@ -167,20 +171,22 @@ export class SyncManager {
|
|
|
167
171
|
if (previousObservation && previousObservation !== checkpointEvidence.observationToken) {
|
|
168
172
|
this.bumpFreshnessEpoch(codebasePath);
|
|
169
173
|
}
|
|
170
|
-
return
|
|
174
|
+
return { checkpoint: checkpointEvidence };
|
|
171
175
|
}
|
|
172
176
|
if (!checkpointEvidence)
|
|
173
|
-
return null;
|
|
177
|
+
return { checkpoint: null };
|
|
174
178
|
this.sourceCheckpointStatuses.set(codebasePath, checkpointEvidence.status);
|
|
175
179
|
this.sourceCheckpointObservations.delete(codebasePath);
|
|
176
180
|
this.lastSyncTimes.delete(codebasePath);
|
|
177
181
|
this.bumpFreshnessEpoch(codebasePath);
|
|
178
182
|
return {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
183
|
+
failure: {
|
|
184
|
+
mode: 'skipped_source_checkpoint_unavailable',
|
|
185
|
+
checkedAt,
|
|
186
|
+
thresholdMs,
|
|
187
|
+
checkpointStatus: checkpointEvidence.status,
|
|
188
|
+
errorMessage: checkpointEvidence.message,
|
|
189
|
+
},
|
|
184
190
|
};
|
|
185
191
|
}
|
|
186
192
|
persistOwnedOperationStart(lease, ownsLease) {
|
|
@@ -278,10 +284,10 @@ export class SyncManager {
|
|
|
278
284
|
const checkedAtMs = this.now();
|
|
279
285
|
const checkedAt = new Date(checkedAtMs).toISOString();
|
|
280
286
|
if (options.reason === 'ignore_change') {
|
|
281
|
-
const
|
|
282
|
-
if (
|
|
283
|
-
return
|
|
284
|
-
return this.runIgnoreReconcile(codebasePath, options.coalescedEdits, undefined, options.mutationLease);
|
|
287
|
+
const checkpointValidation = await this.validateSourceFreshnessCheckpoint(codebasePath, checkedAt, thresholdMs, options.preparedVectorReceipt);
|
|
288
|
+
if ('failure' in checkpointValidation)
|
|
289
|
+
return checkpointValidation.failure;
|
|
290
|
+
return this.runIgnoreReconcile(codebasePath, options.coalescedEdits, undefined, options.mutationLease, checkpointValidation.checkpoint?.generationReceipt);
|
|
285
291
|
}
|
|
286
292
|
// Join a live mutation before inspecting its checkpoint. The owner may be
|
|
287
293
|
// between marker withdrawal and checkpoint publication.
|
|
@@ -307,16 +313,16 @@ export class SyncManager {
|
|
|
307
313
|
// Source-freshness ownership is a precondition for every incremental path,
|
|
308
314
|
// including ignore reconciliation. The identity comes from Core authority,
|
|
309
315
|
// never from the lifecycle snapshot.
|
|
310
|
-
const
|
|
311
|
-
if (
|
|
312
|
-
return
|
|
316
|
+
const checkpointValidation = await this.validateSourceFreshnessCheckpoint(codebasePath, checkedAt, thresholdMs, options.preparedVectorReceipt);
|
|
317
|
+
if ('failure' in checkpointValidation)
|
|
318
|
+
return checkpointValidation.failure;
|
|
313
319
|
let currentIgnoreControlSignature;
|
|
314
320
|
if (options.skipIgnoreControlCheck !== true) {
|
|
315
321
|
currentIgnoreControlSignature = await this.computeIgnoreControlSignature(codebasePath);
|
|
316
322
|
const persistedIgnoreControlSignature = this.snapshotManager.getCodebaseIgnoreControlSignature?.(codebasePath);
|
|
317
323
|
if (typeof persistedIgnoreControlSignature === 'string') {
|
|
318
324
|
if (persistedIgnoreControlSignature !== currentIgnoreControlSignature) {
|
|
319
|
-
return this.runIgnoreReconcile(codebasePath, 1, currentIgnoreControlSignature, options.mutationLease);
|
|
325
|
+
return this.runIgnoreReconcile(codebasePath, 1, currentIgnoreControlSignature, options.mutationLease, checkpointValidation.checkpoint?.generationReceipt);
|
|
320
326
|
}
|
|
321
327
|
}
|
|
322
328
|
else if ((this.snapshotManager.getCodebaseStatus(codebasePath) === 'indexed'
|
|
@@ -329,7 +335,7 @@ export class SyncManager {
|
|
|
329
335
|
? this.context.hasSynchronizerForCodebase(codebasePath)
|
|
330
336
|
: false;
|
|
331
337
|
if (indexedPaths.length > 0 || hasSynchronizer) {
|
|
332
|
-
return this.runIgnoreReconcile(codebasePath, 1, currentIgnoreControlSignature, options.mutationLease);
|
|
338
|
+
return this.runIgnoreReconcile(codebasePath, 1, currentIgnoreControlSignature, options.mutationLease, checkpointValidation.checkpoint?.generationReceipt);
|
|
333
339
|
}
|
|
334
340
|
}
|
|
335
341
|
}
|
|
@@ -367,7 +373,7 @@ export class SyncManager {
|
|
|
367
373
|
try {
|
|
368
374
|
return await this.syncCodebase(codebasePath, options.mutationLease, currentIgnoreControlSignature, {
|
|
369
375
|
exactSourceComparisonPaths: options.exactSourceComparisonPaths,
|
|
370
|
-
});
|
|
376
|
+
}, checkpointValidation.checkpoint?.generationReceipt);
|
|
371
377
|
}
|
|
372
378
|
catch (e) {
|
|
373
379
|
// Log and rethrow to allow callers to handle/see failure
|
|
@@ -409,7 +415,7 @@ export class SyncManager {
|
|
|
409
415
|
errorMessage: outcome.errorMessage,
|
|
410
416
|
};
|
|
411
417
|
}
|
|
412
|
-
async runIgnoreReconcile(codebasePath, coalescedEdits = 1, nextIgnoreControlSignature, existingLease) {
|
|
418
|
+
async runIgnoreReconcile(codebasePath, coalescedEdits = 1, nextIgnoreControlSignature, existingLease, preparedVectorReceipt) {
|
|
413
419
|
const reconcileKey = this.normalizeReconcileKey(codebasePath);
|
|
414
420
|
const inFlight = this.activeIgnoreReconciles.get(reconcileKey);
|
|
415
421
|
const checkedAtMs = this.now();
|
|
@@ -447,7 +453,7 @@ export class SyncManager {
|
|
|
447
453
|
try {
|
|
448
454
|
lastDurableOperation = this.persistOwnedOperationStart(lease, releaseLease);
|
|
449
455
|
console.log(`[SYNC] 🔁 Ignore control files changed for '${codebasePath}', running reconciliation.`);
|
|
450
|
-
const promise = this.reconcileIgnoreRulesChange(codebasePath, coalescedEdits, nextIgnoreControlSignature, lease);
|
|
456
|
+
const promise = this.reconcileIgnoreRulesChange(codebasePath, coalescedEdits, nextIgnoreControlSignature, lease, preparedVectorReceipt);
|
|
451
457
|
this.activeIgnoreReconciles.set(reconcileKey, promise);
|
|
452
458
|
const decision = await promise;
|
|
453
459
|
const phase = decision.mode === "ignore_reload_failed" ? "failed" : "completed";
|
|
@@ -478,7 +484,7 @@ export class SyncManager {
|
|
|
478
484
|
}
|
|
479
485
|
}
|
|
480
486
|
}
|
|
481
|
-
async reconcileIgnoreRulesChange(codebasePath, coalescedEdits = 1, nextIgnoreControlSignature, mutationLease) {
|
|
487
|
+
async reconcileIgnoreRulesChange(codebasePath, coalescedEdits = 1, nextIgnoreControlSignature, mutationLease, preparedVectorReceipt) {
|
|
482
488
|
const checkedAtMs = this.now();
|
|
483
489
|
const checkedAt = new Date(checkedAtMs).toISOString();
|
|
484
490
|
const startedAt = checkedAtMs;
|
|
@@ -531,9 +537,13 @@ export class SyncManager {
|
|
|
531
537
|
}
|
|
532
538
|
this.assertMutationCurrent(mutationLease);
|
|
533
539
|
this.snapshotManager.saveCodebaseSnapshot();
|
|
540
|
+
// Deleting newly ignored payload invalidates ordinary live proof.
|
|
541
|
+
// Carry the pre-delete receipt so Core can revalidate that exact
|
|
542
|
+
// source generation after the mutation lease is held.
|
|
534
543
|
const syncDecision = await this.ensureFreshness(codebasePath, 0, {
|
|
535
544
|
skipIgnoreControlCheck: true,
|
|
536
545
|
mutationLease,
|
|
546
|
+
preparedVectorReceipt,
|
|
537
547
|
});
|
|
538
548
|
const lastSyncAt = syncDecision.lastSyncAt;
|
|
539
549
|
const lastSyncMs = lastSyncAt ? Date.parse(lastSyncAt) : undefined;
|
|
@@ -597,7 +607,7 @@ export class SyncManager {
|
|
|
597
607
|
};
|
|
598
608
|
}
|
|
599
609
|
}
|
|
600
|
-
async syncCodebase(codebasePath, existingLease, currentIgnoreControlSignature, joinRequest = {}) {
|
|
610
|
+
async syncCodebase(codebasePath, existingLease, currentIgnoreControlSignature, joinRequest = {}, preparedVectorReceipt) {
|
|
601
611
|
if (this.snapshotManager.getCodebaseStatus(codebasePath) === 'indexing') {
|
|
602
612
|
console.log(`[SYNC] ⏭️ Skipping sync for '${codebasePath}' because indexing is active.`);
|
|
603
613
|
return { mode: 'skipped_indexing' };
|
|
@@ -672,7 +682,7 @@ export class SyncManager {
|
|
|
672
682
|
this.mutationLeaseCoordinator.publishWhileCurrent(lease, publish);
|
|
673
683
|
}
|
|
674
684
|
: undefined;
|
|
675
|
-
const fencedCheckpoint = await this.inspectSourceFreshnessCheckpoint(codebasePath);
|
|
685
|
+
const fencedCheckpoint = await this.inspectSourceFreshnessCheckpoint(codebasePath, preparedVectorReceipt);
|
|
676
686
|
if (fencedCheckpoint && fencedCheckpoint.status !== 'valid') {
|
|
677
687
|
throw new Error(`Incremental sync cannot continue because its authoritative source checkpoint is ${fencedCheckpoint.status}: ${fencedCheckpoint.message}`);
|
|
678
688
|
}
|
|
@@ -980,23 +990,76 @@ export class SyncManager {
|
|
|
980
990
|
}
|
|
981
991
|
}
|
|
982
992
|
startBackgroundSync() {
|
|
983
|
-
if (this.
|
|
993
|
+
if (this.backgroundSyncEnabled) {
|
|
984
994
|
return;
|
|
985
995
|
}
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
this.backgroundSyncTimer = setTimeout(
|
|
996
|
+
this.backgroundSyncEnabled = true;
|
|
997
|
+
this.scheduleBackgroundSync(BACKGROUND_SYNC_INITIAL_DELAY_MS);
|
|
998
|
+
}
|
|
999
|
+
scheduleBackgroundSync(delayMs) {
|
|
1000
|
+
if (!this.backgroundSyncEnabled)
|
|
1001
|
+
return;
|
|
1002
|
+
this.backgroundSyncTimer = setTimeout(() => {
|
|
1003
|
+
this.backgroundSyncTimer = null;
|
|
1004
|
+
void this.runBackgroundSync();
|
|
1005
|
+
}, delayMs);
|
|
1006
|
+
}
|
|
1007
|
+
runBackgroundSync() {
|
|
1008
|
+
const flight = (async () => {
|
|
1009
|
+
try {
|
|
1010
|
+
await this.handleSyncIndex();
|
|
1011
|
+
}
|
|
1012
|
+
catch (error) {
|
|
1013
|
+
console.error('[SYNC] Periodic synchronization pass failed:', error);
|
|
1014
|
+
}
|
|
1015
|
+
})();
|
|
1016
|
+
this.backgroundSyncFlight = flight;
|
|
1017
|
+
this.onLifecycleActivityChanged?.();
|
|
1018
|
+
void flight.finally(() => {
|
|
1019
|
+
if (this.backgroundSyncFlight === flight) {
|
|
1020
|
+
this.backgroundSyncFlight = null;
|
|
1021
|
+
this.onLifecycleActivityChanged?.();
|
|
1022
|
+
}
|
|
1023
|
+
this.scheduleBackgroundSync(BACKGROUND_SYNC_INTERVAL_MS);
|
|
1024
|
+
});
|
|
1025
|
+
return flight;
|
|
993
1026
|
}
|
|
994
1027
|
stopBackgroundSync() {
|
|
1028
|
+
this.backgroundSyncEnabled = false;
|
|
995
1029
|
if (this.backgroundSyncTimer) {
|
|
996
1030
|
clearTimeout(this.backgroundSyncTimer);
|
|
997
1031
|
this.backgroundSyncTimer = null;
|
|
998
1032
|
}
|
|
999
1033
|
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Stops new provider-owned synchronization work and joins every lifecycle
|
|
1036
|
+
* flight that may still hold mutation or backend authority.
|
|
1037
|
+
*/
|
|
1038
|
+
async stopAndDrainLifecycle() {
|
|
1039
|
+
this.stopBackgroundSync();
|
|
1040
|
+
await this.stopWatcherMode();
|
|
1041
|
+
for (;;) {
|
|
1042
|
+
const pending = new Set();
|
|
1043
|
+
if (this.backgroundSyncFlight) {
|
|
1044
|
+
pending.add(this.backgroundSyncFlight);
|
|
1045
|
+
}
|
|
1046
|
+
for (const flight of this.activeWatcherSyncs) {
|
|
1047
|
+
pending.add(flight);
|
|
1048
|
+
}
|
|
1049
|
+
for (const flight of this.activeSyncs.values()) {
|
|
1050
|
+
pending.add(flight);
|
|
1051
|
+
}
|
|
1052
|
+
for (const flight of this.activeIgnoreReconciles.values()) {
|
|
1053
|
+
pending.add(flight);
|
|
1054
|
+
}
|
|
1055
|
+
if (pending.size === 0)
|
|
1056
|
+
return;
|
|
1057
|
+
await Promise.allSettled(pending);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
getActiveLifecycleOperationCount() {
|
|
1061
|
+
return (this.backgroundSyncFlight ? 1 : 0) + this.activeWatcherSyncs.size;
|
|
1062
|
+
}
|
|
1000
1063
|
getWatchDebounceMs() {
|
|
1001
1064
|
return this.watchDebounceMs;
|
|
1002
1065
|
}
|
|
@@ -1144,29 +1207,38 @@ export class SyncManager {
|
|
|
1144
1207
|
const current = this.pendingIgnoreChangeEdits.get(codebasePath) || 0;
|
|
1145
1208
|
this.pendingIgnoreChangeEdits.set(codebasePath, current + 1);
|
|
1146
1209
|
}
|
|
1147
|
-
const timer = setTimeout(
|
|
1210
|
+
const timer = setTimeout(() => {
|
|
1148
1211
|
this.debounceTimers.delete(codebasePath);
|
|
1149
|
-
const
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1212
|
+
const flight = (async () => {
|
|
1213
|
+
const coalescedIgnoreEdits = this.pendingIgnoreChangeEdits.get(codebasePath) || 0;
|
|
1214
|
+
this.pendingIgnoreChangeEdits.delete(codebasePath);
|
|
1215
|
+
try {
|
|
1216
|
+
if (coalescedIgnoreEdits > 0) {
|
|
1217
|
+
const decision = await this.ensureFreshness(codebasePath, 0, {
|
|
1218
|
+
reason: 'ignore_change',
|
|
1219
|
+
coalescedEdits: coalescedIgnoreEdits,
|
|
1220
|
+
});
|
|
1221
|
+
if (decision.mode === 'ignore_reload_failed') {
|
|
1222
|
+
console.warn(`[SYNC-WATCH] Ignore-rule reconcile failed for '${codebasePath}': ${decision.errorMessage || 'unknown_error'} (fallbackSyncExecuted=${decision.fallbackSyncExecuted === true})`);
|
|
1223
|
+
}
|
|
1224
|
+
else {
|
|
1225
|
+
console.log(`[SYNC-WATCH] Ignore-rule reconcile completed for '${codebasePath}' (version=${decision.ignoreRulesVersion ?? 'n/a'}, deleted=${decision.deletedFiles ?? 0}, added=${decision.addedFiles ?? 0}, coalesced=${decision.coalescedEdits ?? 1})`);
|
|
1226
|
+
}
|
|
1227
|
+
return;
|
|
1162
1228
|
}
|
|
1163
|
-
|
|
1229
|
+
await this.ensureFreshness(codebasePath, 0);
|
|
1164
1230
|
}
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1231
|
+
catch (error) {
|
|
1232
|
+
console.error(`[SYNC-WATCH] Debounced sync failed for '${codebasePath}':`, error);
|
|
1233
|
+
}
|
|
1234
|
+
})();
|
|
1235
|
+
this.activeWatcherSyncs.add(flight);
|
|
1236
|
+
this.onLifecycleActivityChanged?.();
|
|
1237
|
+
void flight.finally(() => {
|
|
1238
|
+
if (this.activeWatcherSyncs.delete(flight)) {
|
|
1239
|
+
this.onLifecycleActivityChanged?.();
|
|
1240
|
+
}
|
|
1241
|
+
});
|
|
1170
1242
|
}, this.watchDebounceMs);
|
|
1171
1243
|
this.debounceTimers.set(codebasePath, timer);
|
|
1172
1244
|
}
|
|
@@ -1325,7 +1397,6 @@ export class SyncManager {
|
|
|
1325
1397
|
this.debounceTimers.clear();
|
|
1326
1398
|
this.watcherIgnoreMatchers.clear();
|
|
1327
1399
|
this.pendingIgnoreChangeEdits.clear();
|
|
1328
|
-
this.activeIgnoreReconciles.clear();
|
|
1329
1400
|
this.lastSyncTimes.clear();
|
|
1330
1401
|
this.ignoreRulesVersions.clear();
|
|
1331
1402
|
this.freshnessEpochs.clear();
|