@gmickel/gno 2.11.0 → 2.12.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 +1 -1
- package/assets/skill/SKILL.md +3 -2
- package/assets/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v2.11.0.zip → gno-browser-clipper-v2.12.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.12.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +8 -5
- package/spec/cli.md +100 -34
- package/spec/evals-agentic.md +3 -1
- package/spec/mcp.md +9 -3
- package/spec/output-schemas/status.schema.json +28 -0
- package/src/app/context-agent-projection.ts +10 -1
- package/src/cli/commands/agents/block.ts +2 -2
- package/src/cli/commands/agents/commands.ts +6 -5
- package/src/cli/commands/agents/engine.ts +15 -2
- package/src/cli/commands/agents/harnesses.ts +21 -14
- package/src/cli/commands/cleanup.ts +46 -6
- package/src/cli/commands/collection/remove.ts +1 -0
- package/src/cli/commands/collection/rename.ts +1 -0
- package/src/cli/commands/doctor.ts +37 -0
- package/src/cli/commands/mcp/paths.ts +2 -1
- package/src/cli/commands/setup-semantic.ts +6 -1
- package/src/cli/commands/shared.ts +26 -1
- package/src/cli/commands/skill/paths.ts +20 -2
- package/src/cli/commands/status.ts +89 -15
- package/src/cli/program.ts +19 -3
- package/src/core/activation-probe-plan.ts +0 -1
- package/src/core/config-mutation.ts +72 -55
- package/src/core/memory-remember.ts +0 -1
- package/src/core/module-path.ts +33 -0
- package/src/core/runtime-entrypoint.ts +3 -1
- package/src/ingestion/file-processor.ts +6 -3
- package/src/ingestion/prepare-file.ts +14 -4
- package/src/ingestion/source-availability/root.ts +138 -0
- package/src/ingestion/sync.ts +5 -19
- package/src/llm/native-worker/client.ts +8 -4
- package/src/llm/native-worker/errors.ts +26 -3
- package/src/mcp/http-transport.ts +7 -2
- package/src/mcp/server.ts +3 -0
- package/src/mcp/tool-descriptions-core.ts +2 -2
- package/src/mcp/tools/index.ts +30 -1
- package/src/mcp/tools/status.ts +17 -2
- package/src/pipeline/query-snippet.ts +266 -0
- package/src/pipeline/search.ts +149 -19
- package/src/sdk/client.ts +14 -2
- package/src/sdk/types.ts +6 -1
- package/src/serve/public/components/ThoroughnessSelector.tsx +1 -1
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/pages/Collections.tsx +8 -0
- package/src/serve/public/pages/Search.tsx +9 -1
- package/src/serve/resident-request.ts +8 -0
- package/src/serve/resident-runtime.ts +25 -3
- package/src/serve/spa-production-build.ts +7 -3
- package/src/serve/status-model.ts +2 -0
- package/src/serve/status.ts +29 -0
- package/src/sessions/config-refresh.ts +6 -0
- package/src/sessions/import-child.ts +4 -1
- package/src/store/sqlite/adapter.ts +159 -18
- package/src/store/types.ts +45 -7
- package/browser-extension/artifacts/gno-browser-clipper-v2.11.0.zip.sha256 +0 -1
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* @module src/cli/commands/cleanup
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type { CleanupStats } from "../../store/types";
|
|
9
|
+
import type { CleanupStats, StaleCollection } from "../../store/types";
|
|
10
10
|
|
|
11
11
|
import { getIndexDbPath } from "../../app/constants";
|
|
12
12
|
import { isInitialized, loadConfig } from "../../config";
|
|
@@ -20,14 +20,26 @@ export interface CleanupOptions {
|
|
|
20
20
|
configPath?: string;
|
|
21
21
|
/** Index name */
|
|
22
22
|
indexName?: string;
|
|
23
|
+
/** Called with the collections about to be deleted, before deleting them. */
|
|
24
|
+
onDropping?: (collections: StaleCollection[]) => void;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
/**
|
|
26
28
|
* Result of cleanup command.
|
|
27
29
|
*/
|
|
28
30
|
export type CleanupResult =
|
|
29
|
-
| {
|
|
30
|
-
|
|
31
|
+
| {
|
|
32
|
+
success: true;
|
|
33
|
+
stats: CleanupStats;
|
|
34
|
+
/** Indexed collections the config omits, deleted by this cleanup. */
|
|
35
|
+
droppedCollections: StaleCollection[];
|
|
36
|
+
}
|
|
37
|
+
| {
|
|
38
|
+
success: false;
|
|
39
|
+
error: string;
|
|
40
|
+
/** Collections already deleted when a later step failed. */
|
|
41
|
+
droppedCollections?: StaleCollection[];
|
|
42
|
+
};
|
|
31
43
|
|
|
32
44
|
/**
|
|
33
45
|
* Execute gno cleanup command.
|
|
@@ -64,17 +76,32 @@ export async function cleanup(
|
|
|
64
76
|
try {
|
|
65
77
|
// Drop collections no longer in the config first (their documents follow
|
|
66
78
|
// by cascade), so their content, chunks and vectors count as orphans.
|
|
67
|
-
|
|
79
|
+
// This is the explicit removal path; it names what it deletes.
|
|
80
|
+
const stale = await store.listStaleCollections(
|
|
81
|
+
config.collections.map(({ name }) => name)
|
|
82
|
+
);
|
|
83
|
+
if (!stale.ok) {
|
|
84
|
+
return { success: false, error: stale.error.message };
|
|
85
|
+
}
|
|
86
|
+
const droppedCollections = stale.value;
|
|
87
|
+
if (droppedCollections.length > 0) options.onDropping?.(droppedCollections);
|
|
88
|
+
const syncResult = await store.syncCollections(config.collections, {
|
|
89
|
+
remove: "stale",
|
|
90
|
+
});
|
|
68
91
|
if (!syncResult.ok) {
|
|
69
92
|
return { success: false, error: syncResult.error.message };
|
|
70
93
|
}
|
|
71
94
|
|
|
72
95
|
const cleanupResult = await store.cleanupOrphans();
|
|
73
96
|
if (!cleanupResult.ok) {
|
|
74
|
-
return {
|
|
97
|
+
return {
|
|
98
|
+
success: false,
|
|
99
|
+
error: cleanupResult.error.message,
|
|
100
|
+
droppedCollections,
|
|
101
|
+
};
|
|
75
102
|
}
|
|
76
103
|
|
|
77
|
-
return { success: true, stats: cleanupResult.value };
|
|
104
|
+
return { success: true, stats: cleanupResult.value, droppedCollections };
|
|
78
105
|
} finally {
|
|
79
106
|
await store.close();
|
|
80
107
|
}
|
|
@@ -118,3 +145,16 @@ export function formatCleanup(result: CleanupResult): string {
|
|
|
118
145
|
|
|
119
146
|
return lines.join("\n");
|
|
120
147
|
}
|
|
148
|
+
|
|
149
|
+
/** Lines naming the collections cleanup is about to delete. */
|
|
150
|
+
export function formatDroppingCollections(
|
|
151
|
+
collections: readonly StaleCollection[]
|
|
152
|
+
): string {
|
|
153
|
+
return [
|
|
154
|
+
"Deleting collections not in this config:",
|
|
155
|
+
...collections.map(
|
|
156
|
+
(c) =>
|
|
157
|
+
` ${c.name}: ${c.documents} ${c.documents === 1 ? "document" : "documents"}`
|
|
158
|
+
),
|
|
159
|
+
].join("\n");
|
|
160
|
+
}
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
import { formatVectorPartitionLines } from "../../core/vector-partition-status";
|
|
34
34
|
import { getEmbeddingFingerprint } from "../../embed/fingerprint";
|
|
35
35
|
import { getCodeChunkingStatus } from "../../ingestion/chunker";
|
|
36
|
+
import { checkCollectionRoots } from "../../ingestion/source-availability/root";
|
|
36
37
|
import { ModelCache } from "../../llm/cache";
|
|
37
38
|
import { getActivePreset, resolveModelUri } from "../../llm/registry";
|
|
38
39
|
import { SqliteAdapter } from "../../store/sqlite/adapter";
|
|
@@ -739,6 +740,37 @@ export async function checkFindingsPass(
|
|
|
739
740
|
};
|
|
740
741
|
}
|
|
741
742
|
|
|
743
|
+
/** Configured collection roots that are missing or unreadable right now. */
|
|
744
|
+
export async function checkCollectionRootAccess(
|
|
745
|
+
config: Config
|
|
746
|
+
): Promise<DoctorCheck> {
|
|
747
|
+
const name = "collection-roots";
|
|
748
|
+
const errors = await checkCollectionRoots(config.collections);
|
|
749
|
+
const unavailable = config.collections.flatMap((collection) => {
|
|
750
|
+
const error = errors[collection.name];
|
|
751
|
+
return error ? [{ collection, error }] : [];
|
|
752
|
+
});
|
|
753
|
+
if (unavailable.length === 0) {
|
|
754
|
+
return {
|
|
755
|
+
name,
|
|
756
|
+
status: "ok",
|
|
757
|
+
message: `${config.collections.length} collection root(s) available`,
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
return {
|
|
761
|
+
name,
|
|
762
|
+
status: "warn",
|
|
763
|
+
message: `${unavailable.length} collection root(s) missing or unreadable: ${unavailable.map(({ collection }) => collection.name).join(", ")}`,
|
|
764
|
+
details: [
|
|
765
|
+
...unavailable.map(
|
|
766
|
+
({ collection, error }) =>
|
|
767
|
+
`${collection.name}: ${collection.path} [${error.code}] ${error.message}`
|
|
768
|
+
),
|
|
769
|
+
"Reconnect the drive or restore the folder; if it moved, change the collection's path in the config, then run: gno update",
|
|
770
|
+
],
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
|
|
742
774
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
743
775
|
// Implementation
|
|
744
776
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -779,6 +811,10 @@ export async function doctor(
|
|
|
779
811
|
// Code chunking capability
|
|
780
812
|
checks.push(checkCodeChunking());
|
|
781
813
|
|
|
814
|
+
// Collection roots, checked now (documents stay indexed while one is away)
|
|
815
|
+
const rootsCheck = await checkCollectionRootAccess(config);
|
|
816
|
+
checks.push(rootsCheck);
|
|
817
|
+
|
|
782
818
|
// Embedding fingerprint freshness
|
|
783
819
|
checks.push(...(await checkEmbeddingFingerprints(config, options.indexName)));
|
|
784
820
|
|
|
@@ -801,6 +837,7 @@ export async function doctor(
|
|
|
801
837
|
return {
|
|
802
838
|
healthy:
|
|
803
839
|
!hasErrors &&
|
|
840
|
+
rootsCheck.status === "ok" &&
|
|
804
841
|
activation.healthy &&
|
|
805
842
|
isConnectorActivationComplete(activation),
|
|
806
843
|
checks,
|
|
@@ -12,6 +12,7 @@ import type { ConnectorWorkspaceEnvironment } from "../../../core/connector-envi
|
|
|
12
12
|
|
|
13
13
|
import { resolveDirs } from "../../../app/constants";
|
|
14
14
|
import { assertValidIndexName } from "../../../app/index-name";
|
|
15
|
+
import { modulePath } from "../../../core/module-path";
|
|
15
16
|
import { getCurrentGnoEntrypoint } from "../../../core/runtime-entrypoint";
|
|
16
17
|
import {
|
|
17
18
|
DEFAULT_MCP_TOOL_PROFILE,
|
|
@@ -459,7 +460,7 @@ export function resolveLaunchPaths(): {
|
|
|
459
460
|
entrypoint: LaunchPath;
|
|
460
461
|
} {
|
|
461
462
|
const entrypoint = getCurrentGnoEntrypoint();
|
|
462
|
-
if (isBunfsPath(import.meta
|
|
463
|
+
if (isBunfsPath(modulePath(import.meta))) {
|
|
463
464
|
return {
|
|
464
465
|
bun: { path: process.execPath, pinned: false },
|
|
465
466
|
entrypoint: { path: entrypoint, pinned: false },
|
|
@@ -21,6 +21,7 @@ import type { FolderSetupReceipt } from "../../core/setup-receipt";
|
|
|
21
21
|
import { VERSION } from "../../app/constants";
|
|
22
22
|
import { canonicalizeIndexName } from "../../app/index-name";
|
|
23
23
|
import { withWriteLock } from "../../core/file-lock";
|
|
24
|
+
import { moduleDir } from "../../core/module-path";
|
|
24
25
|
import {
|
|
25
26
|
setupFingerprint,
|
|
26
27
|
setupRootFingerprint,
|
|
@@ -427,7 +428,11 @@ function createSemanticReceipt(
|
|
|
427
428
|
async function defaultSpawnWorker(
|
|
428
429
|
receipt: SetupSemanticReceipt
|
|
429
430
|
): Promise<SpawnedSemanticWorker> {
|
|
430
|
-
const workerPath = join(
|
|
431
|
+
const workerPath = join(
|
|
432
|
+
moduleDir(import.meta),
|
|
433
|
+
"..",
|
|
434
|
+
"setup-semantic-worker.ts"
|
|
435
|
+
);
|
|
431
436
|
const descriptor = openSync(receipt.logPath, "a", 0o600);
|
|
432
437
|
try {
|
|
433
438
|
const child = Bun.spawn({
|
|
@@ -22,7 +22,10 @@ import {
|
|
|
22
22
|
} from "../../config";
|
|
23
23
|
import { tracePhase } from "../../core/phase-trace";
|
|
24
24
|
import { BUDGET_ERROR_CODES } from "../../ingestion/file-processor";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
SqliteAdapter,
|
|
27
|
+
staleCollectionsMessage,
|
|
28
|
+
} from "../../store/sqlite/adapter";
|
|
26
29
|
import { assertCliSessionBinding } from "../session-binding";
|
|
27
30
|
|
|
28
31
|
/**
|
|
@@ -53,6 +56,10 @@ export interface InitStoreOptions {
|
|
|
53
56
|
syncConfig?: boolean;
|
|
54
57
|
/** Permit management commands that only need an existing index database */
|
|
55
58
|
allowEmptyCollections?: boolean;
|
|
59
|
+
/** Open without syncing even when the index holds collections the config
|
|
60
|
+
* omits: config mutations report them through their own sync, and a
|
|
61
|
+
* connector proof reads one collection through a subset config. */
|
|
62
|
+
allowStaleCollections?: boolean;
|
|
56
63
|
}
|
|
57
64
|
|
|
58
65
|
/**
|
|
@@ -132,7 +139,25 @@ export async function initStore(
|
|
|
132
139
|
return { ok: false, error: openResult.error.message };
|
|
133
140
|
}
|
|
134
141
|
|
|
142
|
+
if (options.syncConfig === false && options.allowStaleCollections) {
|
|
143
|
+
return { ok: true, store, config, collections, actualConfigPath };
|
|
144
|
+
}
|
|
135
145
|
if (options.syncConfig === false) {
|
|
146
|
+
// Reads skip the sync but must not search collections another config
|
|
147
|
+
// indexed; they fail the same way a sync would.
|
|
148
|
+
const stale = await store.listStaleCollections(
|
|
149
|
+
config.collections.map(({ name }) => name)
|
|
150
|
+
);
|
|
151
|
+
const indexed = stale.ok ? stale.value.filter((c) => c.documents > 0) : [];
|
|
152
|
+
if (!stale.ok || indexed.length > 0) {
|
|
153
|
+
await store.close();
|
|
154
|
+
return {
|
|
155
|
+
ok: false,
|
|
156
|
+
error: stale.ok
|
|
157
|
+
? staleCollectionsMessage(indexed)
|
|
158
|
+
: stale.error.message,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
136
161
|
return { ok: true, store, config, collections, actualConfigPath };
|
|
137
162
|
}
|
|
138
163
|
|
|
@@ -11,6 +11,11 @@ import { existsSync, realpathSync } from "node:fs";
|
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
12
|
import { dirname, isAbsolute, join, normalize, relative, sep } from "node:path";
|
|
13
13
|
|
|
14
|
+
import {
|
|
15
|
+
harnessConfigDirFromEnv,
|
|
16
|
+
type HarnessId,
|
|
17
|
+
} from "../agents/harnesses.js";
|
|
18
|
+
|
|
14
19
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
15
20
|
// Environment Variables
|
|
16
21
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -84,6 +89,11 @@ interface TargetPathConfig {
|
|
|
84
89
|
userBase: string; // e.g., ".claude" (joined with homedir) or ".config/opencode"
|
|
85
90
|
skillsSubdir: string; // e.g., "skills" or "skill"
|
|
86
91
|
envVar: string;
|
|
92
|
+
/**
|
|
93
|
+
* Harness whose config-dir env var (CLAUDE_CONFIG_DIR, CODEX_HOME) replaces
|
|
94
|
+
* the user base, resolved exactly as `gno agents` does.
|
|
95
|
+
*/
|
|
96
|
+
configDirHarness?: HarnessId;
|
|
87
97
|
}
|
|
88
98
|
|
|
89
99
|
const TARGET_CONFIGS: Record<SkillTarget, TargetPathConfig> = {
|
|
@@ -92,12 +102,14 @@ const TARGET_CONFIGS: Record<SkillTarget, TargetPathConfig> = {
|
|
|
92
102
|
userBase: ".claude",
|
|
93
103
|
skillsSubdir: "skills",
|
|
94
104
|
envVar: ENV_CLAUDE_SKILLS_DIR,
|
|
105
|
+
configDirHarness: "claude",
|
|
95
106
|
},
|
|
96
107
|
codex: {
|
|
97
108
|
projectBase: ".codex",
|
|
98
109
|
userBase: ".codex",
|
|
99
110
|
skillsSubdir: "skills",
|
|
100
111
|
envVar: ENV_CODEX_SKILLS_DIR,
|
|
112
|
+
configDirHarness: "codex",
|
|
101
113
|
},
|
|
102
114
|
opencode: {
|
|
103
115
|
projectBase: ".opencode",
|
|
@@ -150,8 +162,14 @@ export function resolveSkillPaths(opts: SkillPathOptions): SkillPaths {
|
|
|
150
162
|
let base: string;
|
|
151
163
|
|
|
152
164
|
if (scope === "user") {
|
|
153
|
-
|
|
154
|
-
|
|
165
|
+
// Any home override (option or env) is an isolation request and
|
|
166
|
+
// suppresses the harness config-dir env vars, as in `gno agents`.
|
|
167
|
+
const explicitHome = homeDir ?? process.env[ENV_SKILLS_HOME_OVERRIDE];
|
|
168
|
+
const configDir =
|
|
169
|
+
explicitHome === undefined && config.configDirHarness
|
|
170
|
+
? harnessConfigDirFromEnv(config.configDirHarness)
|
|
171
|
+
: undefined;
|
|
172
|
+
base = configDir ?? join(explicitHome ?? homedir(), config.userBase);
|
|
155
173
|
} else {
|
|
156
174
|
const projectRoot = cwd ?? process.cwd();
|
|
157
175
|
base = join(projectRoot, config.projectBase);
|
|
@@ -9,7 +9,7 @@ import type { ContentTypeBoostStatus } from "../../config/content-types";
|
|
|
9
9
|
import type { ActivationStatus } from "../../core/activation-status";
|
|
10
10
|
import type { MemoryStatus } from "../../core/memory-diagnostics";
|
|
11
11
|
import type { BackgroundIssue } from "../../serve/status-model";
|
|
12
|
-
import type { IndexStatus } from "../../store/types";
|
|
12
|
+
import type { IndexStatus, StaleCollection } from "../../store/types";
|
|
13
13
|
|
|
14
14
|
import { getIndexDbPath, getModelsCachePath } from "../../app/constants";
|
|
15
15
|
import {
|
|
@@ -33,6 +33,10 @@ import {
|
|
|
33
33
|
} from "../../core/memory-diagnostics";
|
|
34
34
|
import { formatVectorPartitionLines } from "../../core/vector-partition-status";
|
|
35
35
|
import { BUDGET_ERROR_CODES } from "../../ingestion/file-processor";
|
|
36
|
+
import {
|
|
37
|
+
checkCollectionRoots,
|
|
38
|
+
type CollectionRootError,
|
|
39
|
+
} from "../../ingestion/source-availability/root";
|
|
36
40
|
import { ModelCache } from "../../llm/cache";
|
|
37
41
|
import { getActivePreset, resolveModelUri } from "../../llm/registry";
|
|
38
42
|
import { getConnectorVerificationTargets } from "../../serve/connectors";
|
|
@@ -80,6 +84,10 @@ export type StatusResult =
|
|
|
80
84
|
runtimeIssues?: RuntimeIssue[];
|
|
81
85
|
/** Files whose latest conversion stopped at the per-file budget. */
|
|
82
86
|
budgetStops: BudgetStop[];
|
|
87
|
+
/** Indexed collections this config does not list (kept until cleanup). */
|
|
88
|
+
staleCollections?: StaleCollection[];
|
|
89
|
+
/** Configured collections whose root is missing or unreadable now. */
|
|
90
|
+
rootErrors?: Record<string, CollectionRootError>;
|
|
83
91
|
}
|
|
84
92
|
| { success: false; error: string };
|
|
85
93
|
|
|
@@ -91,6 +99,18 @@ export interface BudgetStop {
|
|
|
91
99
|
message: string;
|
|
92
100
|
}
|
|
93
101
|
|
|
102
|
+
/** Indexed collections the config omits; an unreadable list is not fatal. */
|
|
103
|
+
async function collectStaleCollections(
|
|
104
|
+
store: SqliteAdapter,
|
|
105
|
+
configured: string[]
|
|
106
|
+
): Promise<StaleCollection[]> {
|
|
107
|
+
const stale = await store.listStaleCollections(configured);
|
|
108
|
+
return stale.ok ? stale.value.filter((c) => c.documents > 0) : [];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const STALE_COLLECTIONS_HINT =
|
|
112
|
+
'not in this config; add them back, or run "gno cleanup" to delete their index data';
|
|
113
|
+
|
|
94
114
|
/** Most recent ingest errors inspected for budget stops. */
|
|
95
115
|
const BUDGET_STOP_SCAN_LIMIT = 100;
|
|
96
116
|
/** Most budget stops listed in status output. */
|
|
@@ -164,12 +184,14 @@ function connectorProjectionLine(activation: ActivationStatus): string | null {
|
|
|
164
184
|
|
|
165
185
|
function isStatusHealthy(
|
|
166
186
|
indexStatus: IndexStatus,
|
|
167
|
-
activation: ActivationStatus
|
|
187
|
+
activation: ActivationStatus,
|
|
188
|
+
rootErrors: Record<string, CollectionRootError> = {}
|
|
168
189
|
): boolean {
|
|
169
190
|
return (
|
|
170
191
|
indexStatus.healthy &&
|
|
171
192
|
activation.healthy &&
|
|
172
|
-
isConnectorActivationComplete(activation)
|
|
193
|
+
isConnectorActivationComplete(activation) &&
|
|
194
|
+
Object.keys(rootErrors).length === 0
|
|
173
195
|
);
|
|
174
196
|
}
|
|
175
197
|
|
|
@@ -180,7 +202,8 @@ function formatTerminal(
|
|
|
180
202
|
indexStatus: IndexStatus,
|
|
181
203
|
activation: ActivationStatus,
|
|
182
204
|
contentTypeBoost: ContentTypeBoostStatus,
|
|
183
|
-
memory: MemoryStatus
|
|
205
|
+
memory: MemoryStatus,
|
|
206
|
+
rootErrors: Record<string, CollectionRootError> = {}
|
|
184
207
|
): string {
|
|
185
208
|
const lines: string[] = [];
|
|
186
209
|
|
|
@@ -203,6 +226,11 @@ function formatTerminal(
|
|
|
203
226
|
` ${c.name}: ${c.activeDocuments} docs, ${c.totalChunks} chunks` +
|
|
204
227
|
(c.embeddedChunks > 0 ? `, ${c.embeddedChunks} embedded` : "")
|
|
205
228
|
);
|
|
229
|
+
const rootError = rootErrors[c.name];
|
|
230
|
+
if (rootError) {
|
|
231
|
+
lines.push(` Root unavailable: ${c.path}`);
|
|
232
|
+
lines.push(` [${rootError.code}] ${rootError.message}`);
|
|
233
|
+
}
|
|
206
234
|
const workspace = formatLinkWorkspace(c);
|
|
207
235
|
if (workspace) lines.push(` Link workspace: ${workspace}`);
|
|
208
236
|
}
|
|
@@ -240,7 +268,7 @@ function formatTerminal(
|
|
|
240
268
|
lines.push(`Ranking fingerprint: ${contentTypeBoost.rulesFingerprint}`);
|
|
241
269
|
|
|
242
270
|
lines.push(
|
|
243
|
-
`Health: ${isStatusHealthy(indexStatus, activation) ? "OK" : "DEGRADED"}`
|
|
271
|
+
`Health: ${isStatusHealthy(indexStatus, activation, rootErrors) ? "OK" : "DEGRADED"}`
|
|
244
272
|
);
|
|
245
273
|
lines.push("");
|
|
246
274
|
const activationState = activation.healthy
|
|
@@ -290,7 +318,8 @@ function formatMarkdown(
|
|
|
290
318
|
indexStatus: IndexStatus,
|
|
291
319
|
activation: ActivationStatus,
|
|
292
320
|
contentTypeBoost: ContentTypeBoostStatus,
|
|
293
|
-
memory: MemoryStatus
|
|
321
|
+
memory: MemoryStatus,
|
|
322
|
+
rootErrors: Record<string, CollectionRootError> = {}
|
|
294
323
|
): string {
|
|
295
324
|
const lines: string[] = [];
|
|
296
325
|
|
|
@@ -299,7 +328,7 @@ function formatMarkdown(
|
|
|
299
328
|
lines.push(`- **Config**: ${indexStatus.configPath}`);
|
|
300
329
|
lines.push(`- **Database**: ${indexStatus.dbPath}`);
|
|
301
330
|
lines.push(
|
|
302
|
-
`- **Health**: ${isStatusHealthy(indexStatus, activation) ? "✓ OK" : "⚠ DEGRADED"}`
|
|
331
|
+
`- **Health**: ${isStatusHealthy(indexStatus, activation, rootErrors) ? "✓ OK" : "⚠ DEGRADED"}`
|
|
303
332
|
);
|
|
304
333
|
lines.push("");
|
|
305
334
|
|
|
@@ -314,6 +343,15 @@ function formatMarkdown(
|
|
|
314
343
|
);
|
|
315
344
|
}
|
|
316
345
|
lines.push("");
|
|
346
|
+
for (const c of indexStatus.collections) {
|
|
347
|
+
const rootError = rootErrors[c.name];
|
|
348
|
+
if (rootError) {
|
|
349
|
+
lines.push(
|
|
350
|
+
`- **Root unavailable**: ${c.name} (${c.path}) [${rootError.code}] ${rootError.message}`
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (Object.keys(rootErrors).length > 0) lines.push("");
|
|
317
355
|
}
|
|
318
356
|
|
|
319
357
|
lines.push("## Summary");
|
|
@@ -390,6 +428,8 @@ export async function status(
|
|
|
390
428
|
return { success: false, error: configResult.error.message };
|
|
391
429
|
}
|
|
392
430
|
const config = configResult.value;
|
|
431
|
+
// Read-time root check, overlapping the index reads below; it never rejects.
|
|
432
|
+
const rootErrors = checkCollectionRoots(config.collections);
|
|
393
433
|
|
|
394
434
|
// Open database
|
|
395
435
|
const store = new SqliteAdapter();
|
|
@@ -446,6 +486,11 @@ export async function status(
|
|
|
446
486
|
...(await checkResidentRuntimes()),
|
|
447
487
|
],
|
|
448
488
|
budgetStops: await collectBudgetStops(store),
|
|
489
|
+
staleCollections: await collectStaleCollections(
|
|
490
|
+
store,
|
|
491
|
+
config.collections.map(({ name }) => name)
|
|
492
|
+
),
|
|
493
|
+
rootErrors: await rootErrors,
|
|
449
494
|
};
|
|
450
495
|
} finally {
|
|
451
496
|
await store.close();
|
|
@@ -482,6 +527,9 @@ export function formatStatus(
|
|
|
482
527
|
documentCount: c.activeDocuments,
|
|
483
528
|
chunkCount: c.totalChunks,
|
|
484
529
|
embeddedCount: c.embeddedChunks,
|
|
530
|
+
...(result.rootErrors?.[c.name]
|
|
531
|
+
? { rootError: result.rootErrors[c.name] }
|
|
532
|
+
: {}),
|
|
485
533
|
})),
|
|
486
534
|
totalDocuments: s.activeDocuments,
|
|
487
535
|
totalChunks: s.totalChunks,
|
|
@@ -491,7 +539,7 @@ export function formatStatus(
|
|
|
491
539
|
typedMetadata: s.typedMetadata,
|
|
492
540
|
chunking: s.chunking,
|
|
493
541
|
lastUpdated: s.lastUpdatedAt,
|
|
494
|
-
healthy: isStatusHealthy(s, result.activation),
|
|
542
|
+
healthy: isStatusHealthy(s, result.activation, result.rootErrors),
|
|
495
543
|
contentTypeBoost: result.contentTypeBoost,
|
|
496
544
|
activation: result.activation,
|
|
497
545
|
memory: result.memory,
|
|
@@ -501,19 +549,37 @@ export function formatStatus(
|
|
|
501
549
|
...(result.runtimeIssues?.length
|
|
502
550
|
? { runtimeIssues: result.runtimeIssues }
|
|
503
551
|
: {}),
|
|
552
|
+
...(result.staleCollections?.length
|
|
553
|
+
? { staleCollections: result.staleCollections }
|
|
554
|
+
: {}),
|
|
504
555
|
},
|
|
505
556
|
null,
|
|
506
557
|
2
|
|
507
558
|
);
|
|
508
559
|
}
|
|
509
560
|
|
|
561
|
+
const staleLines = (result.staleCollections ?? []).map(
|
|
562
|
+
(c) =>
|
|
563
|
+
`${c.name}: ${c.documents} ${c.documents === 1 ? "document" : "documents"}`
|
|
564
|
+
);
|
|
510
565
|
if (options.md) {
|
|
511
|
-
return
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
566
|
+
return [
|
|
567
|
+
formatMarkdown(
|
|
568
|
+
result.status,
|
|
569
|
+
result.activation,
|
|
570
|
+
result.contentTypeBoost,
|
|
571
|
+
result.memory,
|
|
572
|
+
result.rootErrors
|
|
573
|
+
),
|
|
574
|
+
...(staleLines.length
|
|
575
|
+
? [
|
|
576
|
+
"",
|
|
577
|
+
`## Collections ${STALE_COLLECTIONS_HINT}`,
|
|
578
|
+
"",
|
|
579
|
+
...staleLines.map((line) => `- ${line}`),
|
|
580
|
+
]
|
|
581
|
+
: []),
|
|
582
|
+
].join("\n");
|
|
517
583
|
}
|
|
518
584
|
|
|
519
585
|
const issueLines = result.backgroundIssues.map(
|
|
@@ -529,8 +595,16 @@ export function formatStatus(
|
|
|
529
595
|
result.status,
|
|
530
596
|
result.activation,
|
|
531
597
|
result.contentTypeBoost,
|
|
532
|
-
result.memory
|
|
598
|
+
result.memory,
|
|
599
|
+
result.rootErrors
|
|
533
600
|
),
|
|
601
|
+
...(staleLines.length
|
|
602
|
+
? [
|
|
603
|
+
"",
|
|
604
|
+
`Collections ${STALE_COLLECTIONS_HINT}:`,
|
|
605
|
+
...staleLines.map((line) => ` ${line}`),
|
|
606
|
+
]
|
|
607
|
+
: []),
|
|
534
608
|
...(stopLines.length
|
|
535
609
|
? [
|
|
536
610
|
"",
|
package/src/cli/program.ts
CHANGED
|
@@ -3651,9 +3651,14 @@ function wireManagementCommands(program: Command): void {
|
|
|
3651
3651
|
|
|
3652
3652
|
// cleanup - Clean stale data
|
|
3653
3653
|
addWriteLeaseFlags(
|
|
3654
|
-
program
|
|
3654
|
+
program
|
|
3655
|
+
.command("cleanup")
|
|
3656
|
+
.description("Clean orphaned data from index")
|
|
3657
|
+
.option("--json", "JSON output")
|
|
3655
3658
|
).action(async (cmdOpts: Record<string, unknown>) => {
|
|
3656
|
-
const { cleanup, formatCleanup } =
|
|
3659
|
+
const { cleanup, formatCleanup, formatDroppingCollections } =
|
|
3660
|
+
await import("./commands/cleanup");
|
|
3661
|
+
const json = Boolean(cmdOpts.json) || getGlobals().json;
|
|
3657
3662
|
const lease = parseWriteLeaseFlags(cmdOpts);
|
|
3658
3663
|
const result = await withCliWriteLease(
|
|
3659
3664
|
{
|
|
@@ -3665,6 +3670,13 @@ function wireManagementCommands(program: Command): void {
|
|
|
3665
3670
|
cleanup({
|
|
3666
3671
|
configPath: getGlobals().config,
|
|
3667
3672
|
indexName: getGlobals().index,
|
|
3673
|
+
// Name what is about to be deleted before deleting it.
|
|
3674
|
+
onDropping: (collections) => {
|
|
3675
|
+
const plan = json
|
|
3676
|
+
? JSON.stringify({ deleting: collections })
|
|
3677
|
+
: formatDroppingCollections(collections);
|
|
3678
|
+
process.stderr.write(`${plan}\n`);
|
|
3679
|
+
},
|
|
3668
3680
|
})
|
|
3669
3681
|
);
|
|
3670
3682
|
throwIfWriteLeaseBusy(result, false);
|
|
@@ -3672,7 +3684,11 @@ function wireManagementCommands(program: Command): void {
|
|
|
3672
3684
|
if (!result.success) {
|
|
3673
3685
|
throw new CliError("RUNTIME", result.error ?? "Cleanup failed");
|
|
3674
3686
|
}
|
|
3675
|
-
process.stdout.write(
|
|
3687
|
+
process.stdout.write(
|
|
3688
|
+
json
|
|
3689
|
+
? `${JSON.stringify({ droppedCollections: result.droppedCollections, ...result.stats }, null, 2)}\n`
|
|
3690
|
+
: `${formatCleanup(result)}\n`
|
|
3691
|
+
);
|
|
3676
3692
|
});
|
|
3677
3693
|
|
|
3678
3694
|
// reset - Reset GNO to fresh state
|
|
@@ -281,7 +281,6 @@ export async function findEphemeralActivationProbeMatch(
|
|
|
281
281
|
const search = await store.searchFts(candidate.term, {
|
|
282
282
|
collection: plan.collection,
|
|
283
283
|
limit: PROBE_RESULT_LIMIT,
|
|
284
|
-
snippet: false,
|
|
285
284
|
});
|
|
286
285
|
if (!search.ok) {
|
|
287
286
|
return err(search.error.code, search.error.message, search.error.cause);
|