@gmickel/gno 1.17.0 → 1.18.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 +14 -2
- package/assets/skill/SKILL.md +17 -1
- package/assets/skill/mcp-reference.md +21 -0
- package/package.json +2 -2
- package/src/cli/commands/daemon.ts +69 -2
- package/src/cli/commands/models/pull.ts +13 -3
- package/src/cli/commands/status.ts +2 -0
- package/src/cli/detach.ts +37 -20
- package/src/cli/program.ts +74 -27
- package/src/config/index.ts +3 -0
- package/src/config/types.ts +37 -0
- package/src/core/job-manager.ts +19 -0
- package/src/core/mutation-generations.ts +33 -0
- package/src/llm/cache.ts +13 -3
- package/src/llm/nodeLlamaCpp/adapter.ts +10 -1
- package/src/llm/nodeLlamaCpp/lifecycle.ts +71 -0
- package/src/mcp/context.ts +161 -0
- package/src/mcp/http-security.ts +477 -0
- package/src/mcp/http-session.ts +272 -0
- package/src/mcp/http-transport.ts +370 -0
- package/src/mcp/resources/index.ts +141 -134
- package/src/mcp/server.ts +19 -79
- package/src/mcp/tools/add-collection.ts +3 -1
- package/src/mcp/tools/capture.ts +3 -0
- package/src/mcp/tools/clear-collection-embeddings.ts +2 -0
- package/src/mcp/tools/context.ts +9 -8
- package/src/mcp/tools/embed.ts +62 -52
- package/src/mcp/tools/index-cmd.ts +88 -74
- package/src/mcp/tools/index.ts +22 -2
- package/src/mcp/tools/remove-collection.ts +2 -0
- package/src/mcp/tools/status.ts +11 -0
- package/src/mcp/tools/sync.ts +16 -14
- package/src/mcp/tools/workspace-write.ts +7 -3
- package/src/serve/background-runtime.ts +12 -212
- package/src/serve/embed-scheduler.ts +74 -43
- package/src/serve/index.ts +9 -0
- package/src/serve/jobs.ts +78 -80
- package/src/serve/public/components/HealthCenter.tsx +74 -1
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/pages/Dashboard.tsx +1 -0
- package/src/serve/resident-admission.ts +159 -0
- package/src/serve/resident-background-work.ts +39 -0
- package/src/serve/resident-request.ts +55 -0
- package/src/serve/resident-runtime.ts +490 -0
- package/src/serve/resident-status.ts +96 -0
- package/src/serve/routes/api.ts +263 -167
- package/src/serve/routes/mcp.ts +69 -0
- package/src/serve/server.ts +191 -37
- package/src/serve/status-model.ts +51 -0
- package/src/serve/status.ts +5 -0
- package/src/store/sqlite/adapter.ts +26 -9
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
/** Single-process ownership boundary shared by serve and daemon surfaces. */
|
|
2
|
+
|
|
3
|
+
// node:path resolve/dirname/join have no Bun path utility equivalents.
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
import type { Config } from "../config/types";
|
|
7
|
+
import type { WriteLockHandle } from "../core/file-lock";
|
|
8
|
+
import type { SyncResult } from "../ingestion";
|
|
9
|
+
import type { ModelManager } from "../llm/nodeLlamaCpp/lifecycle";
|
|
10
|
+
import type { ToolContext } from "../mcp/context";
|
|
11
|
+
import type { HttpMcpTransportStatus } from "../mcp/http-transport";
|
|
12
|
+
import type { DocumentEventBus } from "./doc-events";
|
|
13
|
+
import type { EmbedResult, EmbedScheduler } from "./embed-scheduler";
|
|
14
|
+
import type { ContextHolder } from "./routes/api";
|
|
15
|
+
import type { ResidentStatus } from "./status-model";
|
|
16
|
+
import type {
|
|
17
|
+
CollectionWatchCallbacks,
|
|
18
|
+
CollectionWatchService,
|
|
19
|
+
} from "./watch-service";
|
|
20
|
+
|
|
21
|
+
import { DEFAULT_INDEX_NAME, getIndexDbPath } from "../app/constants";
|
|
22
|
+
import {
|
|
23
|
+
canonicalizeIndexName,
|
|
24
|
+
INDEX_NAME_REQUIREMENTS,
|
|
25
|
+
isValidIndexName,
|
|
26
|
+
} from "../app/index-name";
|
|
27
|
+
import {
|
|
28
|
+
ensureDirectories,
|
|
29
|
+
formatConfigWarnings,
|
|
30
|
+
getConfigPaths,
|
|
31
|
+
isInitialized,
|
|
32
|
+
loadConfig,
|
|
33
|
+
} from "../config";
|
|
34
|
+
import { acquireWriteLock } from "../core/file-lock";
|
|
35
|
+
import { JobManager } from "../core/job-manager";
|
|
36
|
+
import { recordContentMutation } from "../core/mutation-generations";
|
|
37
|
+
import { defaultSyncService, withContentTypeRules } from "../ingestion";
|
|
38
|
+
import { getModelManager } from "../llm/nodeLlamaCpp/lifecycle";
|
|
39
|
+
import { getModelConfig } from "../llm/registry";
|
|
40
|
+
import { getActivePreset } from "../llm/registry";
|
|
41
|
+
import { createToolContext, Mutex } from "../mcp/context";
|
|
42
|
+
import { SqliteAdapter } from "../store/sqlite/adapter";
|
|
43
|
+
import {
|
|
44
|
+
createServerContext,
|
|
45
|
+
type CreateServerContextOptions,
|
|
46
|
+
disposeServerContext,
|
|
47
|
+
type ServerContext,
|
|
48
|
+
} from "./context";
|
|
49
|
+
import { createEmbedScheduler } from "./embed-scheduler";
|
|
50
|
+
import { AdmissionController, ReaderGate } from "./resident-admission";
|
|
51
|
+
import { ResidentBackgroundWork } from "./resident-background-work";
|
|
52
|
+
import { buildResidentStatusSnapshot } from "./resident-status";
|
|
53
|
+
import { CollectionWatchService as DefaultCollectionWatchService } from "./watch-service";
|
|
54
|
+
|
|
55
|
+
const DEFAULT_SHUTDOWN_DEADLINE_MS = 5_000;
|
|
56
|
+
const OWNER_LOCK_TIMEOUT_MS = 0;
|
|
57
|
+
|
|
58
|
+
export type ResidentMode = "serve" | "daemon";
|
|
59
|
+
|
|
60
|
+
export interface ResidentRuntimeOptions {
|
|
61
|
+
configPath?: string;
|
|
62
|
+
index?: string;
|
|
63
|
+
mode?: ResidentMode;
|
|
64
|
+
requireCollections?: boolean;
|
|
65
|
+
offline?: boolean;
|
|
66
|
+
eventBus?: DocumentEventBus | null;
|
|
67
|
+
watchCallbacks?: CollectionWatchCallbacks;
|
|
68
|
+
readerLimit?: number;
|
|
69
|
+
readerQueueLimit?: number;
|
|
70
|
+
shutdownDeadlineMs?: number;
|
|
71
|
+
shutdownAbortSettleMs?: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface ResidentGeneration {
|
|
75
|
+
content: number;
|
|
76
|
+
index: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface ResidentRequestHandle {
|
|
80
|
+
id: string;
|
|
81
|
+
signal: AbortSignal;
|
|
82
|
+
finish(): void;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface ResidentRuntime {
|
|
86
|
+
readonly mode: ResidentMode;
|
|
87
|
+
readonly store: SqliteAdapter;
|
|
88
|
+
readonly config: Config;
|
|
89
|
+
readonly actualConfigPath: string;
|
|
90
|
+
readonly ctxHolder: ContextHolder;
|
|
91
|
+
readonly scheduler: EmbedScheduler;
|
|
92
|
+
readonly eventBus: DocumentEventBus | null;
|
|
93
|
+
readonly watchService: CollectionWatchService;
|
|
94
|
+
readonly toolMutex: Mutex;
|
|
95
|
+
readonly readerGate: ReaderGate;
|
|
96
|
+
readonly jobManager: JobManager;
|
|
97
|
+
readonly modelManager: ModelManager;
|
|
98
|
+
readonly mcpContext: ToolContext;
|
|
99
|
+
readonly generations: ResidentGeneration;
|
|
100
|
+
readonly activeRequests: number;
|
|
101
|
+
readonly activeSessions: number;
|
|
102
|
+
readonly isShuttingDown: boolean;
|
|
103
|
+
getStatus(): ResidentStatus;
|
|
104
|
+
setListenerPort(port: number | null): void;
|
|
105
|
+
setTransportStatusProvider(
|
|
106
|
+
provider: (() => HttpMcpTransportStatus) | null
|
|
107
|
+
): void;
|
|
108
|
+
admitRequest(signal?: AbortSignal): ResidentRequestHandle | null;
|
|
109
|
+
withModelLease<T>(operation: () => Promise<T>): Promise<T>;
|
|
110
|
+
markContentMutation(): void;
|
|
111
|
+
markIndexMutation(): void;
|
|
112
|
+
startBackgroundWork(
|
|
113
|
+
operation: (signal: AbortSignal) => Promise<void>
|
|
114
|
+
): boolean;
|
|
115
|
+
openSession(): () => void;
|
|
116
|
+
syncAll(options?: {
|
|
117
|
+
gitPull?: boolean;
|
|
118
|
+
runUpdateCmd?: boolean;
|
|
119
|
+
triggerEmbed?: boolean;
|
|
120
|
+
}): Promise<{ syncResult: SyncResult; embedResult: EmbedResult | null }>;
|
|
121
|
+
dispose(): Promise<void>;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export type ResidentRuntimeResult =
|
|
125
|
+
| { success: true; runtime: ResidentRuntime }
|
|
126
|
+
| { success: false; error: string };
|
|
127
|
+
|
|
128
|
+
export type ResidentRuntimeDeps = {
|
|
129
|
+
isInitialized?: typeof isInitialized;
|
|
130
|
+
loadConfig?: typeof loadConfig;
|
|
131
|
+
getConfigPaths?: typeof getConfigPaths;
|
|
132
|
+
ensureDirectories?: typeof ensureDirectories;
|
|
133
|
+
acquireOwnerLock?: (
|
|
134
|
+
path: string,
|
|
135
|
+
timeoutMs: number
|
|
136
|
+
) => Promise<WriteLockHandle | null>;
|
|
137
|
+
storeFactory?: () => SqliteAdapter;
|
|
138
|
+
createServerContext?: (
|
|
139
|
+
store: SqliteAdapter,
|
|
140
|
+
config: Config,
|
|
141
|
+
options?: CreateServerContextOptions
|
|
142
|
+
) => Promise<ServerContext>;
|
|
143
|
+
disposeServerContext?: (ctx: ServerContext) => Promise<void>;
|
|
144
|
+
createEmbedScheduler?: typeof createEmbedScheduler;
|
|
145
|
+
syncAllService?: typeof defaultSyncService.syncAll;
|
|
146
|
+
watchServiceFactory?: (options: {
|
|
147
|
+
collections: Config["collections"];
|
|
148
|
+
store: SqliteAdapter;
|
|
149
|
+
scheduler: EmbedScheduler | null;
|
|
150
|
+
eventBus?: DocumentEventBus | null;
|
|
151
|
+
callbacks?: CollectionWatchCallbacks;
|
|
152
|
+
syncOptions?: Parameters<typeof withContentTypeRules>[0];
|
|
153
|
+
}) => CollectionWatchService;
|
|
154
|
+
modelManagerFactory?: (config: Config) => ModelManager;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export async function startResidentRuntime(
|
|
158
|
+
options: ResidentRuntimeOptions = {},
|
|
159
|
+
deps: ResidentRuntimeDeps = {}
|
|
160
|
+
): Promise<ResidentRuntimeResult> {
|
|
161
|
+
if (options.index !== undefined && !isValidIndexName(options.index)) {
|
|
162
|
+
return {
|
|
163
|
+
success: false,
|
|
164
|
+
error: `Invalid index name: ${INDEX_NAME_REQUIREMENTS}.`,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const initialized = await (deps.isInitialized ?? isInitialized)(
|
|
169
|
+
options.configPath
|
|
170
|
+
);
|
|
171
|
+
if (!initialized)
|
|
172
|
+
return { success: false, error: "GNO not initialized. Run: gno init" };
|
|
173
|
+
|
|
174
|
+
const configResult = await (deps.loadConfig ?? loadConfig)(
|
|
175
|
+
options.configPath
|
|
176
|
+
);
|
|
177
|
+
if (!configResult.ok)
|
|
178
|
+
return { success: false, error: configResult.error.message };
|
|
179
|
+
for (const warning of formatConfigWarnings(configResult.warnings))
|
|
180
|
+
console.warn(warning);
|
|
181
|
+
const initialConfig = configResult.value;
|
|
182
|
+
if (options.requireCollections && initialConfig.collections.length === 0) {
|
|
183
|
+
return {
|
|
184
|
+
success: false,
|
|
185
|
+
error: "No collections configured. Run: gno collection add <path>",
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
await (deps.ensureDirectories ?? ensureDirectories)();
|
|
190
|
+
const dbPath = getIndexDbPath(options.index);
|
|
191
|
+
const ownerLockPath = join(dirname(dbPath), ".resident-owner.lock");
|
|
192
|
+
const ownerLock = await (deps.acquireOwnerLock ?? acquireWriteLock)(
|
|
193
|
+
ownerLockPath,
|
|
194
|
+
OWNER_LOCK_TIMEOUT_MS
|
|
195
|
+
);
|
|
196
|
+
if (!ownerLock) {
|
|
197
|
+
return {
|
|
198
|
+
success: false,
|
|
199
|
+
error: `Resident runtime already active for index "${canonicalizeIndexName(options.index ?? DEFAULT_INDEX_NAME)}". Stop the owning gno serve or gno daemon process and retry.`,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const store = deps.storeFactory?.() ?? new SqliteAdapter();
|
|
204
|
+
const paths = (deps.getConfigPaths ?? getConfigPaths)();
|
|
205
|
+
const actualConfigPath = resolve(options.configPath ?? paths.configFile);
|
|
206
|
+
store.setConfigPath(actualConfigPath);
|
|
207
|
+
const openResult = await store.open(dbPath, initialConfig.ftsTokenizer);
|
|
208
|
+
if (!openResult.ok) {
|
|
209
|
+
await ownerLock.release();
|
|
210
|
+
return { success: false, error: openResult.error.message };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const failStartup = async (error: string): Promise<ResidentRuntimeResult> => {
|
|
214
|
+
await Promise.allSettled([store.close(), ownerLock.release()]);
|
|
215
|
+
return { success: false, error };
|
|
216
|
+
};
|
|
217
|
+
const syncCollections = await store.syncCollections(
|
|
218
|
+
initialConfig.collections
|
|
219
|
+
);
|
|
220
|
+
if (!syncCollections.ok) return failStartup(syncCollections.error.message);
|
|
221
|
+
const syncContexts = await store.syncContexts(initialConfig.contexts ?? []);
|
|
222
|
+
if (!syncContexts.ok) return failStartup(syncContexts.error.message);
|
|
223
|
+
|
|
224
|
+
let ctx: ServerContext;
|
|
225
|
+
try {
|
|
226
|
+
ctx = await (deps.createServerContext ?? createServerContext)(
|
|
227
|
+
store,
|
|
228
|
+
initialConfig,
|
|
229
|
+
{
|
|
230
|
+
offline: options.offline ?? false,
|
|
231
|
+
indexName: options.index,
|
|
232
|
+
}
|
|
233
|
+
);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
return failStartup(error instanceof Error ? error.message : String(error));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const ctxHolder: ContextHolder = {
|
|
239
|
+
current: ctx,
|
|
240
|
+
config: initialConfig,
|
|
241
|
+
scheduler: null,
|
|
242
|
+
eventBus: options.eventBus ?? null,
|
|
243
|
+
watchService: null,
|
|
244
|
+
};
|
|
245
|
+
const modelManager =
|
|
246
|
+
deps.modelManagerFactory?.(initialConfig) ??
|
|
247
|
+
getModelManager(getModelConfig(initialConfig));
|
|
248
|
+
const generations: ResidentGeneration = { content: 0, index: 0 };
|
|
249
|
+
ctxHolder.markContentMutation = () => {
|
|
250
|
+
generations.content += 1;
|
|
251
|
+
};
|
|
252
|
+
ctxHolder.markIndexMutation = () => {
|
|
253
|
+
generations.index += 1;
|
|
254
|
+
};
|
|
255
|
+
const scheduler = (deps.createEmbedScheduler ?? createEmbedScheduler)({
|
|
256
|
+
db: store.getRawDb(),
|
|
257
|
+
getEmbedPort: () => ctxHolder.current.embedPort,
|
|
258
|
+
getVectorIndex: () => ctxHolder.current.vectorIndex,
|
|
259
|
+
getModelUri: () => getActivePreset(ctxHolder.config).embed,
|
|
260
|
+
acquireModelLease: () => modelManager.acquireLease(),
|
|
261
|
+
onEmbedded: () => {
|
|
262
|
+
generations.index += 1;
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
ctxHolder.scheduler = scheduler;
|
|
266
|
+
ctxHolder.current.scheduler = scheduler;
|
|
267
|
+
ctxHolder.current.eventBus = options.eventBus ?? null;
|
|
268
|
+
|
|
269
|
+
const watchService = (
|
|
270
|
+
deps.watchServiceFactory ??
|
|
271
|
+
((watchOptions) => new DefaultCollectionWatchService(watchOptions))
|
|
272
|
+
)({
|
|
273
|
+
collections: initialConfig.collections,
|
|
274
|
+
store,
|
|
275
|
+
scheduler,
|
|
276
|
+
eventBus: options.eventBus ?? null,
|
|
277
|
+
callbacks: {
|
|
278
|
+
...options.watchCallbacks,
|
|
279
|
+
onSyncComplete: (event) => {
|
|
280
|
+
recordContentMutation(event.result, () => {
|
|
281
|
+
generations.content += 1;
|
|
282
|
+
});
|
|
283
|
+
options.watchCallbacks?.onSyncComplete?.(event);
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
syncOptions: withContentTypeRules({}, initialConfig),
|
|
287
|
+
});
|
|
288
|
+
watchService.start();
|
|
289
|
+
ctxHolder.watchService = watchService;
|
|
290
|
+
ctxHolder.current.watchService = watchService;
|
|
291
|
+
|
|
292
|
+
const toolMutex = new Mutex();
|
|
293
|
+
const serverInstanceId = crypto.randomUUID();
|
|
294
|
+
const writeLockPath = join(dirname(dbPath), ".mcp-write.lock");
|
|
295
|
+
const jobManager = new JobManager({
|
|
296
|
+
lockPath: writeLockPath,
|
|
297
|
+
serverInstanceId,
|
|
298
|
+
toolMutex,
|
|
299
|
+
});
|
|
300
|
+
ctxHolder.jobManager = jobManager;
|
|
301
|
+
const admission = new AdmissionController();
|
|
302
|
+
const readerGate = new ReaderGate(
|
|
303
|
+
options.readerLimit,
|
|
304
|
+
options.readerQueueLimit
|
|
305
|
+
);
|
|
306
|
+
const startedAt = Date.now();
|
|
307
|
+
let listenerPort: number | null = null;
|
|
308
|
+
let transportStatusProvider: (() => HttpMcpTransportStatus) | null = null;
|
|
309
|
+
let shutdownState: ResidentStatus["shutdown"]["state"] = "none";
|
|
310
|
+
let admissionState: ResidentStatus["admission"]["state"] = "accepting";
|
|
311
|
+
let disposed = false;
|
|
312
|
+
const backgroundWork = new ResidentBackgroundWork(
|
|
313
|
+
() => !disposed && admission.accepting
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
const mcpContext = createToolContext({
|
|
317
|
+
store,
|
|
318
|
+
getConfig: () => ctxHolder.config,
|
|
319
|
+
setConfig: (config) => {
|
|
320
|
+
ctxHolder.config = config;
|
|
321
|
+
ctxHolder.current = { ...ctxHolder.current, config };
|
|
322
|
+
ctxHolder.watchService?.updateCollections(
|
|
323
|
+
config.collections,
|
|
324
|
+
withContentTypeRules({}, config)
|
|
325
|
+
);
|
|
326
|
+
},
|
|
327
|
+
actualConfigPath,
|
|
328
|
+
indexName: canonicalizeIndexName(options.index ?? DEFAULT_INDEX_NAME),
|
|
329
|
+
toolMutex,
|
|
330
|
+
jobManager,
|
|
331
|
+
serverInstanceId,
|
|
332
|
+
writeLockPath,
|
|
333
|
+
enableWrite: false,
|
|
334
|
+
isShuttingDown: () => disposed || !admission.accepting,
|
|
335
|
+
acquireModelLease: () => modelManager.acquireLease(),
|
|
336
|
+
markContentMutation: () => {
|
|
337
|
+
generations.content += 1;
|
|
338
|
+
},
|
|
339
|
+
markIndexMutation: () => {
|
|
340
|
+
generations.index += 1;
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const runtime: ResidentRuntime = {
|
|
345
|
+
mode: options.mode ?? "serve",
|
|
346
|
+
store,
|
|
347
|
+
get config() {
|
|
348
|
+
return ctxHolder.config;
|
|
349
|
+
},
|
|
350
|
+
actualConfigPath,
|
|
351
|
+
ctxHolder,
|
|
352
|
+
scheduler,
|
|
353
|
+
eventBus: options.eventBus ?? null,
|
|
354
|
+
watchService,
|
|
355
|
+
toolMutex,
|
|
356
|
+
readerGate,
|
|
357
|
+
jobManager,
|
|
358
|
+
modelManager,
|
|
359
|
+
mcpContext,
|
|
360
|
+
generations,
|
|
361
|
+
get activeRequests() {
|
|
362
|
+
return admission.active;
|
|
363
|
+
},
|
|
364
|
+
get activeSessions() {
|
|
365
|
+
return transportStatusProvider?.().activeSessions ?? 0;
|
|
366
|
+
},
|
|
367
|
+
get isShuttingDown() {
|
|
368
|
+
return disposed || !admission.accepting;
|
|
369
|
+
},
|
|
370
|
+
admitRequest: (signal) => admission.admit(signal),
|
|
371
|
+
async withModelLease<T>(operation: () => Promise<T>): Promise<T> {
|
|
372
|
+
const lease = modelManager.acquireLease();
|
|
373
|
+
try {
|
|
374
|
+
return await operation();
|
|
375
|
+
} finally {
|
|
376
|
+
lease.release();
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
markContentMutation() {
|
|
380
|
+
generations.content += 1;
|
|
381
|
+
},
|
|
382
|
+
markIndexMutation() {
|
|
383
|
+
generations.index += 1;
|
|
384
|
+
},
|
|
385
|
+
startBackgroundWork(operation) {
|
|
386
|
+
return backgroundWork.start(operation);
|
|
387
|
+
},
|
|
388
|
+
openSession() {
|
|
389
|
+
return () => undefined;
|
|
390
|
+
},
|
|
391
|
+
getStatus() {
|
|
392
|
+
const transport = transportStatusProvider?.() ?? {
|
|
393
|
+
activeRequests: 0,
|
|
394
|
+
activeSessions: 0,
|
|
395
|
+
queuedRequests: 0,
|
|
396
|
+
maxConcurrentRequests: 0,
|
|
397
|
+
maxQueuedRequests: 0,
|
|
398
|
+
maxSessions: 0,
|
|
399
|
+
};
|
|
400
|
+
const jobs = jobManager.listJobs(100);
|
|
401
|
+
return buildResidentStatusSnapshot({
|
|
402
|
+
mode: options.mode ?? "serve",
|
|
403
|
+
startedAt,
|
|
404
|
+
listenerPort,
|
|
405
|
+
admission: {
|
|
406
|
+
state: admissionState,
|
|
407
|
+
activeRequests: admission.active,
|
|
408
|
+
},
|
|
409
|
+
shutdown: { state: shutdownState },
|
|
410
|
+
transport,
|
|
411
|
+
readers: {
|
|
412
|
+
active: readerGate.active,
|
|
413
|
+
queued: readerGate.queued,
|
|
414
|
+
limit: readerGate.limit,
|
|
415
|
+
maxQueued: readerGate.maxQueued,
|
|
416
|
+
},
|
|
417
|
+
models: modelManager.getLifecycleStats(),
|
|
418
|
+
jobs: {
|
|
419
|
+
active: jobs.active.length,
|
|
420
|
+
recent: jobs.recent.length,
|
|
421
|
+
failed: jobs.recent.filter((job) => job.status === "failed").length,
|
|
422
|
+
},
|
|
423
|
+
generations: { ...generations },
|
|
424
|
+
});
|
|
425
|
+
},
|
|
426
|
+
setListenerPort(port) {
|
|
427
|
+
listenerPort = port;
|
|
428
|
+
},
|
|
429
|
+
setTransportStatusProvider(provider) {
|
|
430
|
+
transportStatusProvider = provider;
|
|
431
|
+
},
|
|
432
|
+
async syncAll(syncOptions = {}) {
|
|
433
|
+
const config = ctxHolder.config;
|
|
434
|
+
const syncAllService = deps.syncAllService
|
|
435
|
+
? (...args: Parameters<typeof defaultSyncService.syncAll>) =>
|
|
436
|
+
deps.syncAllService!(...args)
|
|
437
|
+
: defaultSyncService.syncAll.bind(defaultSyncService);
|
|
438
|
+
const syncResult = await syncAllService(
|
|
439
|
+
config.collections,
|
|
440
|
+
store,
|
|
441
|
+
withContentTypeRules(
|
|
442
|
+
{
|
|
443
|
+
gitPull: syncOptions.gitPull,
|
|
444
|
+
runUpdateCmd: syncOptions.runUpdateCmd,
|
|
445
|
+
},
|
|
446
|
+
config
|
|
447
|
+
)
|
|
448
|
+
);
|
|
449
|
+
recordContentMutation(syncResult, () => {
|
|
450
|
+
generations.content += 1;
|
|
451
|
+
});
|
|
452
|
+
const embedResult =
|
|
453
|
+
syncOptions.triggerEmbed === false
|
|
454
|
+
? null
|
|
455
|
+
: await scheduler.triggerNow();
|
|
456
|
+
return { syncResult, embedResult };
|
|
457
|
+
},
|
|
458
|
+
async dispose() {
|
|
459
|
+
if (disposed) return;
|
|
460
|
+
disposed = true;
|
|
461
|
+
admissionState = "draining";
|
|
462
|
+
shutdownState = "graceful";
|
|
463
|
+
const deadlineReached = await admission.closeAndDrain(
|
|
464
|
+
options.shutdownDeadlineMs ?? DEFAULT_SHUTDOWN_DEADLINE_MS,
|
|
465
|
+
options.shutdownAbortSettleMs ?? DEFAULT_SHUTDOWN_DEADLINE_MS
|
|
466
|
+
);
|
|
467
|
+
if (deadlineReached) shutdownState = "deadline";
|
|
468
|
+
await backgroundWork.cancelAndDrain();
|
|
469
|
+
await jobManager.shutdown().catch(() => undefined);
|
|
470
|
+
await Promise.allSettled([
|
|
471
|
+
Promise.resolve().then(() => watchService.dispose()),
|
|
472
|
+
Promise.resolve().then(() => options.eventBus?.close()),
|
|
473
|
+
]);
|
|
474
|
+
await Promise.allSettled([scheduler.dispose()]);
|
|
475
|
+
await Promise.allSettled([
|
|
476
|
+
(deps.disposeServerContext ?? disposeServerContext)(ctxHolder.current),
|
|
477
|
+
]);
|
|
478
|
+
await Promise.allSettled([modelManager.disposeAll()]);
|
|
479
|
+
await Promise.allSettled([store.close()]);
|
|
480
|
+
await Promise.allSettled([ownerLock.release()]);
|
|
481
|
+
admissionState = "closed";
|
|
482
|
+
transportStatusProvider = null;
|
|
483
|
+
listenerPort = null;
|
|
484
|
+
},
|
|
485
|
+
};
|
|
486
|
+
ctxHolder.startBackgroundWork = (operation) =>
|
|
487
|
+
runtime.startBackgroundWork(operation);
|
|
488
|
+
mcpContext.getResidentStatus = () => runtime.getStatus();
|
|
489
|
+
return { success: true, runtime };
|
|
490
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/** Safe, transport-neutral resident lifecycle status projection. */
|
|
2
|
+
|
|
3
|
+
import type { ResidentStatus, RuntimeMode } from "./status-model";
|
|
4
|
+
|
|
5
|
+
const EMPTY_MODELS: ResidentStatus["models"] = {
|
|
6
|
+
activeLeases: 0,
|
|
7
|
+
leaseAcquisitions: 0,
|
|
8
|
+
leaseReleases: 0,
|
|
9
|
+
loadedModels: 0,
|
|
10
|
+
loadAttempts: 0,
|
|
11
|
+
loadSuccesses: 0,
|
|
12
|
+
loadFailures: 0,
|
|
13
|
+
inflightLoads: 0,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function createStandaloneResidentStatus(
|
|
17
|
+
mode: Extract<RuntimeMode, "stdio" | "direct-cli">
|
|
18
|
+
): ResidentStatus {
|
|
19
|
+
return {
|
|
20
|
+
schemaVersion: "1.0",
|
|
21
|
+
mode,
|
|
22
|
+
resident: false,
|
|
23
|
+
uptimeSeconds: null,
|
|
24
|
+
listenerPort: null,
|
|
25
|
+
admission: { state: "closed", activeRequests: 0 },
|
|
26
|
+
shutdown: { state: "none" },
|
|
27
|
+
transport: {
|
|
28
|
+
activeRequests: 0,
|
|
29
|
+
activeSessions: 0,
|
|
30
|
+
queuedRequests: 0,
|
|
31
|
+
maxConcurrentRequests: 0,
|
|
32
|
+
maxQueuedRequests: 0,
|
|
33
|
+
maxSessions: 0,
|
|
34
|
+
},
|
|
35
|
+
readers: { active: 0, queued: 0, limit: 0, maxQueued: 0 },
|
|
36
|
+
models: { ...EMPTY_MODELS },
|
|
37
|
+
jobs: { active: 0, recent: 0, failed: 0 },
|
|
38
|
+
generations: { content: 0, index: 0 },
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ResidentStatusSnapshotInput {
|
|
43
|
+
mode: Extract<RuntimeMode, "serve" | "daemon">;
|
|
44
|
+
startedAt: number;
|
|
45
|
+
listenerPort: number | null;
|
|
46
|
+
admission: ResidentStatus["admission"];
|
|
47
|
+
shutdown: ResidentStatus["shutdown"];
|
|
48
|
+
transport: ResidentStatus["transport"];
|
|
49
|
+
readers: ResidentStatus["readers"];
|
|
50
|
+
models: ResidentStatus["models"];
|
|
51
|
+
jobs: ResidentStatus["jobs"];
|
|
52
|
+
generations: ResidentStatus["generations"];
|
|
53
|
+
now?: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function buildResidentStatusSnapshot(
|
|
57
|
+
input: ResidentStatusSnapshotInput
|
|
58
|
+
): ResidentStatus {
|
|
59
|
+
return {
|
|
60
|
+
schemaVersion: "1.0",
|
|
61
|
+
mode: input.mode,
|
|
62
|
+
resident: true,
|
|
63
|
+
uptimeSeconds: Math.max(
|
|
64
|
+
0,
|
|
65
|
+
Math.floor(((input.now ?? Date.now()) - input.startedAt) / 1000)
|
|
66
|
+
),
|
|
67
|
+
listenerPort: input.listenerPort,
|
|
68
|
+
admission: { ...input.admission },
|
|
69
|
+
shutdown: { ...input.shutdown },
|
|
70
|
+
transport: { ...input.transport },
|
|
71
|
+
readers: { ...input.readers },
|
|
72
|
+
models: { ...input.models },
|
|
73
|
+
jobs: { ...input.jobs },
|
|
74
|
+
generations: { ...input.generations },
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function isResidentStatus(value: unknown): value is ResidentStatus {
|
|
79
|
+
if (typeof value !== "object" || value === null) return false;
|
|
80
|
+
const candidate = value as Partial<ResidentStatus>;
|
|
81
|
+
return (
|
|
82
|
+
candidate.schemaVersion === "1.0" &&
|
|
83
|
+
typeof candidate.mode === "string" &&
|
|
84
|
+
typeof candidate.resident === "boolean" &&
|
|
85
|
+
typeof candidate.admission === "object" &&
|
|
86
|
+
candidate.admission !== null &&
|
|
87
|
+
typeof candidate.transport === "object" &&
|
|
88
|
+
candidate.transport !== null &&
|
|
89
|
+
typeof candidate.models === "object" &&
|
|
90
|
+
candidate.models !== null &&
|
|
91
|
+
typeof candidate.jobs === "object" &&
|
|
92
|
+
candidate.jobs !== null &&
|
|
93
|
+
typeof candidate.generations === "object" &&
|
|
94
|
+
candidate.generations !== null
|
|
95
|
+
);
|
|
96
|
+
}
|