@nextclaw/kernel 0.8.0 → 0.8.2

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/dist/index.js CHANGED
@@ -16,7 +16,7 @@ import { fileURLToPath } from "node:url";
16
16
  import { BUILTIN_PROVIDER_PLUGINS } from "@nextclaw/runtime";
17
17
  import { McpRegistryService, McpServerLifecycleManager } from "@nextclaw/mcp";
18
18
  import { McpNcpToolRegistryAdapter } from "@nextclaw/ncp-mcp";
19
- import { DefaultNcpAgentConversationStateManager, insertMessageByTimeline } from "@nextclaw/ncp-toolkit";
19
+ import { DefaultNcpAgentConversationStateManager, insertMessageByTimeline, mergeToolExecutionTiming } from "@nextclaw/ncp-toolkit";
20
20
  import { parse } from "yaml";
21
21
  import { createInterface } from "node:readline";
22
22
  import { HttpRuntimeConfigResolver, HttpRuntimeNcpAgentRuntime } from "@nextclaw/nextclaw-ncp-runtime-http-client";
@@ -2876,17 +2876,16 @@ var AppPackageManager = class {
2876
2876
  installRuntimeHooks = (hooks) => {
2877
2877
  this.runtimeHooks = hooks;
2878
2878
  };
2879
- listPackages = async () => {
2880
- await this.ensureBuiltInPackages();
2879
+ start = async () => await this.ensureBuiltInPackages();
2880
+ listPackages = async (options = {}) => {
2881
2881
  const records = await this.registryService.listApps();
2882
- return { entries: await Promise.all(records.map(async (record) => await this.toPackageView(await this.installationService.info(record.appId)))) };
2882
+ return { entries: await Promise.all(records.map(async (record) => await this.toPackageView(await this.installationService.info(record.appId, { measureStorageUsage: options.includeStorageUsage !== false })))) };
2883
2883
  };
2884
2884
  listInstalledDataOwners = async () => (await this.registryService.listApps()).map((record) => ({
2885
2885
  id: record.appId,
2886
2886
  name: record.name
2887
2887
  }));
2888
2888
  getPackage = async (appId) => {
2889
- await this.ensureBuiltInPackages();
2890
2889
  try {
2891
2890
  return await this.toPackageView(await this.installationService.info(appId));
2892
2891
  } catch (error) {
@@ -2895,7 +2894,6 @@ var AppPackageManager = class {
2895
2894
  }
2896
2895
  };
2897
2896
  listActiveComponentSources = async () => {
2898
- await this.ensureBuiltInPackages();
2899
2897
  const records = await this.registryService.listApps();
2900
2898
  return (await Promise.all(records.filter((record) => record.enabled).map(async (record) => {
2901
2899
  await this.installationService.assertVersionIntegrity(record.appId, record.activeVersion);
@@ -3212,10 +3210,22 @@ var AppDataManager = class {
3212
3210
  appHomeService;
3213
3211
  fileLockService = new FileLockService();
3214
3212
  inventoryService = new AppInstanceInventoryService();
3213
+ reconciliationDiagnostics = [];
3215
3214
  constructor(params) {
3216
3215
  this.params = params;
3217
3216
  this.appHomeService = new AppHomeService(params.appHomeDirectory);
3218
3217
  }
3218
+ start = async () => {
3219
+ const workspacePath = path.resolve(this.params.getWorkspacePath());
3220
+ const [packageDiagnostics, workspaceDiagnostics] = await Promise.all([this.inventoryService.reconcileDeletions(this.appHomeService.getInstancesDirectory()), this.inventoryService.reconcileDeletions(this.getWorkspaceInstancesRoot(workspacePath))]);
3221
+ this.reconciliationDiagnostics = [...packageDiagnostics.map((entry) => ({
3222
+ ...entry,
3223
+ source: "package"
3224
+ })), ...workspaceDiagnostics.map((entry) => ({
3225
+ ...entry,
3226
+ source: "workspace-service"
3227
+ }))];
3228
+ };
3219
3229
  list = async () => {
3220
3230
  const workspacePath = path.resolve(this.params.getWorkspacePath());
3221
3231
  const workspaceInstancesRoot = this.getWorkspaceInstancesRoot(workspacePath);
@@ -3228,26 +3238,31 @@ var AppDataManager = class {
3228
3238
  const packageOwners = new Map(packageList.map((entry) => [entry.id, entry]));
3229
3239
  const workspaceOwnerMap = new Map(workspaceOwners.map((entry) => [entry.id, entry]));
3230
3240
  const workspaceScope = this.workspaceScope(workspacePath);
3241
+ const entries = [...packageInventory.entries.map((entry) => this.toEntry({
3242
+ entry,
3243
+ source: "package",
3244
+ displayName: packageOwners.get(entry.appId)?.name ?? entry.appId,
3245
+ active: packageOwners.has(entry.appId)
3246
+ })), ...workspaceInventory.entries.map((entry) => this.toEntry({
3247
+ entry,
3248
+ source: "workspace-service",
3249
+ displayName: workspaceOwnerMap.get(entry.appId)?.title ?? entry.appId,
3250
+ active: workspaceOwnerMap.has(entry.appId),
3251
+ scope: workspaceScope
3252
+ }))].sort((left, right) => left.displayName.localeCompare(right.displayName) || left.id.localeCompare(right.id));
3253
+ const currentDiagnostics = [...packageInventory.diagnostics.map((diagnostic) => ({
3254
+ ...diagnostic,
3255
+ source: "package"
3256
+ })), ...workspaceInventory.diagnostics.map((diagnostic) => ({
3257
+ ...diagnostic,
3258
+ source: "workspace-service"
3259
+ }))];
3260
+ const currentDiagnosticKeys = new Set(currentDiagnostics.map((entry) => `${entry.source}:${entry.instanceDirectory}`));
3261
+ const diagnostics = /* @__PURE__ */ new Map();
3262
+ for (const entry of [...currentDiagnostics, ...this.reconciliationDiagnostics.filter((diagnostic) => currentDiagnosticKeys.has(`${diagnostic.source}:${diagnostic.instanceDirectory}`))]) diagnostics.set(`${entry.source}:${entry.instanceDirectory}`, entry);
3231
3263
  return {
3232
- entries: [...packageInventory.entries.map((entry) => this.toEntry({
3233
- entry,
3234
- source: "package",
3235
- displayName: packageOwners.get(entry.appId)?.name ?? entry.appId,
3236
- active: packageOwners.has(entry.appId)
3237
- })), ...workspaceInventory.entries.map((entry) => this.toEntry({
3238
- entry,
3239
- source: "workspace-service",
3240
- displayName: workspaceOwnerMap.get(entry.appId)?.title ?? entry.appId,
3241
- active: workspaceOwnerMap.has(entry.appId),
3242
- scope: workspaceScope
3243
- }))].sort((left, right) => left.displayName.localeCompare(right.displayName) || left.id.localeCompare(right.id)),
3244
- diagnostics: [...packageInventory.diagnostics.map((entry) => ({
3245
- ...entry,
3246
- source: "package"
3247
- })), ...workspaceInventory.diagnostics.map((entry) => ({
3248
- ...entry,
3249
- source: "workspace-service"
3250
- }))]
3264
+ entries,
3265
+ diagnostics: Array.from(diagnostics.values())
3251
3266
  };
3252
3267
  };
3253
3268
  deleteRetained = async (dataId, confirmAppId) => {
@@ -6698,7 +6713,7 @@ async function replayNcpAgentSessionEvents(events, seedMessages = [], activeMess
6698
6713
  const replayMessageId = readReplayMessageId(replayEvent);
6699
6714
  if (replayMessageId) knownMessageIds.add(replayMessageId);
6700
6715
  await stateManager.dispatch(replayEvent);
6701
- if (replayEvent.type === NcpEventType.MessageToolCallResult) toolResultsByCallId.set(replayEvent.payload.toolCallId, replayEvent.payload);
6716
+ if (replayEvent.type === NcpEventType.MessageToolCallResult && replayEvent.payload.final !== false) toolResultsByCallId.set(replayEvent.payload.toolCallId, replayEvent.payload);
6702
6717
  }
6703
6718
  const snapshot = stateManager.getSnapshot();
6704
6719
  return (snapshot.streamingMessage ? insertMessageByTimeline(snapshot.messages, snapshot.streamingMessage) : snapshot.messages).map(compactionRecovery.terminalize);
@@ -6727,7 +6742,7 @@ function readReplayEventOccurredAt(event) {
6727
6742
  function mergeReplayCompletedToolResults(message, toolResultsByCallId) {
6728
6743
  let changed = false;
6729
6744
  const parts = message.parts.map((part) => {
6730
- if (part.type !== "tool-invocation" || part.state === "result" || !part.toolCallId) return part;
6745
+ if (part.type !== "tool-invocation" || !part.toolCallId) return part;
6731
6746
  const result = toolResultsByCallId.get(part.toolCallId);
6732
6747
  if (!result) return part;
6733
6748
  changed = true;
@@ -6735,7 +6750,8 @@ function mergeReplayCompletedToolResults(message, toolResultsByCallId) {
6735
6750
  ...part,
6736
6751
  state: "result",
6737
6752
  result: result.content,
6738
- resultContentItems: result.contentItems
6753
+ ...result.contentItems ? { resultContentItems: result.contentItems } : {},
6754
+ ...result.execution ? { execution: mergeToolExecutionTiming(result.execution, part.execution) ?? result.execution } : {}
6739
6755
  };
6740
6756
  });
6741
6757
  return changed ? {
@@ -6793,7 +6809,8 @@ function readStreamingMessageId(event) {
6793
6809
  case NcpEventType.MessageReasoningDelta:
6794
6810
  case NcpEventType.MessageReasoningEnd:
6795
6811
  case NcpEventType.MessageToolCallStart:
6796
- case NcpEventType.MessageToolCallArgsDelta: return event.payload.messageId?.trim() || null;
6812
+ case NcpEventType.MessageToolCallArgsDelta:
6813
+ case NcpEventType.MessageToolExecutionStarted: return event.payload.messageId?.trim() || null;
6797
6814
  default: return null;
6798
6815
  }
6799
6816
  }
@@ -6976,7 +6993,7 @@ function applySessionOverrides(params) {
6976
6993
  //#endregion
6977
6994
  //#region src/utils/session-context-inheritance.utils.ts
6978
6995
  const CONTEXT_INHERITANCE_METADATA_KEY = "context_inheritance";
6979
- const INHERITED_FROM_SESSION_METADATA_KEY = "inherited_from_session_id";
6996
+ const INHERITED_FROM_SESSION_METADATA_KEY$1 = "inherited_from_session_id";
6980
6997
  const INHERITED_FROM_MESSAGE_METADATA_KEY = "inherited_from_message_id";
6981
6998
  function hasToolCall(message, toolCallId) {
6982
6999
  return message.parts.some((part) => part.type === "tool-invocation" && part.toolCallId === toolCallId);
@@ -6995,7 +7012,7 @@ function createInheritedContextMessage(params) {
6995
7012
  const { childSessionId, index, sourceMessage, sourceSessionId } = params;
6996
7013
  const message = structuredClone(sourceMessage);
6997
7014
  const metadata = structuredClone(message.metadata ?? {});
6998
- metadata[INHERITED_FROM_SESSION_METADATA_KEY] = sourceSessionId;
7015
+ metadata[INHERITED_FROM_SESSION_METADATA_KEY$1] = sourceSessionId;
6999
7016
  metadata[INHERITED_FROM_MESSAGE_METADATA_KEY] = sourceMessage.id;
7000
7017
  return {
7001
7018
  ...message,
@@ -7050,6 +7067,71 @@ function createSessionContextInheritance(params) {
7050
7067
  };
7051
7068
  }
7052
7069
  //#endregion
7070
+ //#region src/managers/session-token-usage.manager.ts
7071
+ const INHERITED_FROM_SESSION_METADATA_KEY = "inherited_from_session_id";
7072
+ function sumNullableUsageMetric(executions, metric) {
7073
+ const values = executions.flatMap((execution) => {
7074
+ const value = execution.usage[metric];
7075
+ return value === null ? [] : [value];
7076
+ });
7077
+ return values.length > 0 ? values.reduce((total, value) => total + value, 0) : null;
7078
+ }
7079
+ function buildTokenUsageTotals(executions) {
7080
+ const inputTokens = sumNullableUsageMetric(executions, "inputTokens");
7081
+ const cachedInputTokens = sumNullableUsageMetric(executions, "cachedInputTokens");
7082
+ const hasCompleteCacheUsage = executions.length > 0 && executions.every((execution) => execution.usage.inputTokens !== null && execution.usage.cachedInputTokens !== null);
7083
+ return {
7084
+ inputTokens,
7085
+ outputTokens: sumNullableUsageMetric(executions, "outputTokens"),
7086
+ cachedInputTokens,
7087
+ totalTokens: sumNullableUsageMetric(executions, "totalTokens"),
7088
+ cacheHitRate: hasCompleteCacheUsage && inputTokens !== null && inputTokens > 0 && cachedInputTokens !== null && cachedInputTokens <= inputTokens ? cachedInputTokens / inputTokens : null
7089
+ };
7090
+ }
7091
+ function resolveTokenUsageStatus(executions) {
7092
+ if (executions.length === 0 || executions.every((execution) => execution.usage.status === "unavailable")) return "unavailable";
7093
+ return executions.every((execution) => execution.usage.status === "reported") ? "reported" : "partial";
7094
+ }
7095
+ function buildModelTokenUsage(model, executions) {
7096
+ return {
7097
+ model,
7098
+ ...buildTokenUsageTotals(executions),
7099
+ runCount: executions.length,
7100
+ modelCallCount: sumNullableUsageMetric(executions, "modelCallCount"),
7101
+ reportedModelCallCount: sumNullableUsageMetric(executions, "reportedModelCallCount"),
7102
+ status: resolveTokenUsageStatus(executions)
7103
+ };
7104
+ }
7105
+ function buildSessionTokenUsageSummary(params) {
7106
+ const executionsByRunId = /* @__PURE__ */ new Map();
7107
+ for (const message of params.messages) {
7108
+ if (message.role !== "assistant" || typeof message.metadata?.[INHERITED_FROM_SESSION_METADATA_KEY] === "string") continue;
7109
+ const execution = readNcpAiExecutionMetadata(message.metadata);
7110
+ if (execution) executionsByRunId.set(execution.runId, execution);
7111
+ }
7112
+ const executions = [...executionsByRunId.values()];
7113
+ const executionsByModel = /* @__PURE__ */ new Map();
7114
+ for (const execution of executions) {
7115
+ const modelExecutions = executionsByModel.get(execution.model) ?? [];
7116
+ modelExecutions.push(execution);
7117
+ executionsByModel.set(execution.model, modelExecutions);
7118
+ }
7119
+ const models = [...executionsByModel.entries()].map(([model, modelExecutions]) => buildModelTokenUsage(model, modelExecutions)).sort((left, right) => {
7120
+ const leftTotal = left.totalTokens ?? -1;
7121
+ const rightTotal = right.totalTokens ?? -1;
7122
+ return rightTotal === leftTotal ? left.model.localeCompare(right.model) : rightTotal - leftTotal;
7123
+ });
7124
+ return {
7125
+ sessionId: params.sessionId,
7126
+ totals: buildTokenUsageTotals(executions),
7127
+ models,
7128
+ runCount: executions.length,
7129
+ modelCallCount: sumNullableUsageMetric(executions, "modelCallCount"),
7130
+ reportedModelCallCount: sumNullableUsageMetric(executions, "reportedModelCallCount"),
7131
+ status: resolveTokenUsageStatus(executions)
7132
+ };
7133
+ }
7134
+ //#endregion
7053
7135
  //#region src/services/session-event-ingestion.service.ts
7054
7136
  const SESSION_METADATA_PATCH_RUN_METADATA_KIND = "session_metadata_patch";
7055
7137
  function isDurableSessionEvent(event) {
@@ -7338,6 +7420,13 @@ var SessionManager = class {
7338
7420
  }))?.messages ?? [];
7339
7421
  return await this.options.journalStore.listSessionMessages(normalizedSessionId);
7340
7422
  };
7423
+ getSessionTokenUsage = async (sessionId) => {
7424
+ const record = await this.getSessionRecord(sessionId);
7425
+ return record ? buildSessionTokenUsageSummary({
7426
+ sessionId: record.sessionId,
7427
+ messages: record.messages
7428
+ }) : null;
7429
+ };
7341
7430
  listSessionMessagePage = async (sessionId, options) => {
7342
7431
  const { cursor, limit } = options;
7343
7432
  const normalizedSessionId = normalizeSessionId(sessionId);
@@ -10115,7 +10204,8 @@ var ServiceAppRecordService = class {
10115
10204
  const dirPath = join(serviceAppsPath, dirName);
10116
10205
  try {
10117
10206
  const manifest = await readServiceAppManifest(dirPath);
10118
- return this.fromManifest(dirPath, manifest, void 0, await this.materializeWorkspaceStorage(manifest.id));
10207
+ if (manifest.id !== dirName) throw new Error(`service app manifest id must match directory name: ${dirName}`);
10208
+ return this.fromManifest(dirPath, manifest, void 0, await this.inspectWorkspaceStorage(manifest.id));
10119
10209
  } catch (error) {
10120
10210
  if (this.isMissingFileError(error)) return null;
10121
10211
  return this.failedWorkspaceRecord(dirName, dirPath, error);
@@ -10144,13 +10234,22 @@ var ServiceAppRecordService = class {
10144
10234
  }))).filter((entry) => Boolean(entry));
10145
10235
  };
10146
10236
  materializeWorkspaceStorage = async (serviceId) => {
10147
- const instanceDirectory = join(this.params.getWorkspacePath(), ".nextclaw", "app-instances", serviceId, "default");
10237
+ const instanceDirectory = this.getWorkspaceInstanceDirectory(serviceId);
10148
10238
  return (await this.instanceStorageService.materialize({
10149
10239
  appId: serviceId,
10150
10240
  instanceId: "default",
10151
10241
  instanceDirectory
10152
10242
  })).storage;
10153
10243
  };
10244
+ inspectWorkspaceStorage = async (serviceId) => {
10245
+ const instanceDirectory = this.getWorkspaceInstanceDirectory(serviceId);
10246
+ if (!await this.pathExists(instanceDirectory)) return;
10247
+ return (await this.instanceStorageService.inspect({
10248
+ appId: serviceId,
10249
+ instanceId: "default",
10250
+ instanceDirectory
10251
+ })).storage;
10252
+ };
10154
10253
  fromManifest = (dirPath, manifest, packageSource, storage) => {
10155
10254
  const runtimeStatus = this.params.runtimeService.getStatus(manifest.id);
10156
10255
  return {
@@ -10210,10 +10309,26 @@ var ServiceAppRecordService = class {
10210
10309
  isolation: source.isolation
10211
10310
  });
10212
10311
  toTitle = (value) => basename(value).replace(/[-_]+/g, " ").trim() || value;
10312
+ getWorkspaceInstanceDirectory = (serviceId) => join(this.params.getWorkspacePath(), ".nextclaw", "app-instances", serviceId, "default");
10313
+ pathExists = async (targetPath) => {
10314
+ try {
10315
+ await access(targetPath);
10316
+ return true;
10317
+ } catch (error) {
10318
+ if (this.isMissingFileError(error)) return false;
10319
+ throw error;
10320
+ }
10321
+ };
10213
10322
  isMissingFileError = (error) => typeof error === "object" && error !== null && error.code === "ENOENT";
10214
10323
  };
10215
10324
  //#endregion
10216
10325
  //#region src/services/service-app-removal.service.ts
10326
+ const TRANSACTION_ID = "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}";
10327
+ const CURRENT_DELETION_PATTERN = new RegExp(`^\\.deleting-(?<appId>[a-z0-9]+(?:-[a-z0-9]+)*)-(?<transactionId>${TRANSACTION_ID})$`);
10328
+ const LEGACY_DELETION_PATTERN = new RegExp(`^(?<appId>[a-z0-9]+(?:-[a-z0-9]+)*)\\.deleting-(?<transactionId>${TRANSACTION_ID})$`);
10329
+ function isServiceAppDeletionTombstone(directoryName) {
10330
+ return Boolean(parseServiceAppDeletionTombstone(directoryName));
10331
+ }
10217
10332
  var ServiceAppRemovalCleanupError = class extends Error {
10218
10333
  constructor(cause) {
10219
10334
  super(`临时目录清理失败:${cause instanceof Error ? cause.message : String(cause)}`);
@@ -10223,6 +10338,7 @@ var ServiceAppRemovalCleanupError = class extends Error {
10223
10338
  };
10224
10339
  var ServiceAppRemovalService = class {
10225
10340
  fileLockService = new FileLockService();
10341
+ listCanonicalDirectoryNames = async (serviceAppsPath) => (await this.listDirectories(serviceAppsPath)).filter((directoryName) => !directoryName.startsWith(".") && !isServiceAppDeletionTombstone(directoryName));
10226
10342
  remove = async (params) => await this.fileLockService.withLock(params.lockPath, async () => {
10227
10343
  const record = await params.loadRecord();
10228
10344
  await this.removeLocked({
@@ -10231,14 +10347,39 @@ var ServiceAppRemovalService = class {
10231
10347
  });
10232
10348
  return record;
10233
10349
  });
10350
+ reconcile = async (params) => {
10351
+ const { grantStore, lockPathForAppId, serviceAppsPath } = params;
10352
+ const diagnostics = [];
10353
+ for (const directoryName of await this.listDirectories(serviceAppsPath)) {
10354
+ const appId = parseServiceAppDeletionTombstone(directoryName);
10355
+ if (!appId) continue;
10356
+ const stagedPath = join(serviceAppsPath, directoryName);
10357
+ const canonicalPath = join(serviceAppsPath, appId);
10358
+ try {
10359
+ await this.fileLockService.withLock(lockPathForAppId(appId), async () => {
10360
+ const manifest = await readServiceAppManifest(stagedPath);
10361
+ if (manifest.id !== appId) throw new Error(`Service App 删除墓碑 identity 不匹配:${manifest.id}`);
10362
+ if (!await this.pathExists(canonicalPath)) await grantStore.revokeActionsByPrefix(`${appId}.`);
10363
+ await rm(stagedPath, { recursive: true });
10364
+ });
10365
+ } catch (error) {
10366
+ diagnostics.push({
10367
+ appId,
10368
+ stagedPath,
10369
+ message: error instanceof Error ? error.message : String(error)
10370
+ });
10371
+ }
10372
+ }
10373
+ return diagnostics.sort((left, right) => left.stagedPath.localeCompare(right.stagedPath));
10374
+ };
10234
10375
  removeLocked = async (params) => {
10235
10376
  const { grantStore, purgeData, record, stopRuntime } = params;
10236
10377
  const grants = (await grantStore.list()).filter((grant) => grant.actionId.startsWith(`${record.id}.`));
10237
10378
  const stagedPaths = [];
10238
10379
  try {
10239
10380
  await stopRuntime(record);
10240
- await this.stagePath(record.dirPath, stagedPaths);
10241
- if (purgeData && record.storage) await this.stagePath(record.storage.instanceDirectory, stagedPaths);
10381
+ await this.stageSourcePath(record.dirPath, stagedPaths);
10382
+ if (purgeData && record.storage) await this.stageInstancePath(record.storage.instanceDirectory, stagedPaths);
10242
10383
  await grantStore.revokeActionsByPrefix(`${record.id}.`);
10243
10384
  } catch (error) {
10244
10385
  const recoveryErrors = [...await this.restoreStagedPaths(stagedPaths), ...await this.restoreGrants(grantStore, grants)];
@@ -10251,7 +10392,15 @@ var ServiceAppRemovalService = class {
10251
10392
  throw new ServiceAppRemovalCleanupError(error);
10252
10393
  }
10253
10394
  };
10254
- stagePath = async (originalPath, stagedPaths) => {
10395
+ stageSourcePath = async (originalPath, stagedPaths) => {
10396
+ const stagedPath = join(dirname(originalPath), `.deleting-${basename(originalPath)}-${randomUUID()}`);
10397
+ await rename(originalPath, stagedPath);
10398
+ stagedPaths.push({
10399
+ originalPath,
10400
+ stagedPath
10401
+ });
10402
+ };
10403
+ stageInstancePath = async (originalPath, stagedPaths) => {
10255
10404
  const stagedPath = `${originalPath}.deleting-${randomUUID()}`;
10256
10405
  await rename(originalPath, stagedPath);
10257
10406
  stagedPaths.push({
@@ -10259,6 +10408,24 @@ var ServiceAppRemovalService = class {
10259
10408
  stagedPath
10260
10409
  });
10261
10410
  };
10411
+ listDirectories = async (directory) => {
10412
+ try {
10413
+ return (await readdir(directory, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
10414
+ } catch (error) {
10415
+ if (this.isMissingFileError(error)) return [];
10416
+ throw error;
10417
+ }
10418
+ };
10419
+ pathExists = async (targetPath) => {
10420
+ try {
10421
+ await access(targetPath);
10422
+ return true;
10423
+ } catch (error) {
10424
+ if (this.isMissingFileError(error)) return false;
10425
+ throw error;
10426
+ }
10427
+ };
10428
+ isMissingFileError = (error) => typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
10262
10429
  restoreStagedPaths = async (stagedPaths) => {
10263
10430
  const errors = [];
10264
10431
  for (const entry of [...stagedPaths].reverse()) try {
@@ -10278,6 +10445,9 @@ var ServiceAppRemovalService = class {
10278
10445
  return errors;
10279
10446
  };
10280
10447
  };
10448
+ function parseServiceAppDeletionTombstone(directoryName) {
10449
+ return CURRENT_DELETION_PATTERN.exec(directoryName)?.groups?.appId ?? LEGACY_DELETION_PATTERN.exec(directoryName)?.groups?.appId;
10450
+ }
10281
10451
  //#endregion
10282
10452
  //#region src/stores/service-action-grant.store.ts
10283
10453
  const EMPTY_GRANTS = {
@@ -10425,13 +10595,12 @@ var ServiceAppError = class extends Error {
10425
10595
  this.name = "ServiceAppError";
10426
10596
  }
10427
10597
  };
10428
- function isServiceAppError(error) {
10429
- return error instanceof ServiceAppError;
10430
- }
10598
+ const isServiceAppError = (error) => error instanceof ServiceAppError;
10431
10599
  var ServiceAppManager = class {
10432
10600
  removalService = new ServiceAppRemovalService();
10433
10601
  runtimeService;
10434
10602
  recordService;
10603
+ reconciliationDiagnostics = [];
10435
10604
  constructor(params) {
10436
10605
  this.params = params;
10437
10606
  this.runtimeService = params.runtimeService ?? new McpServiceAppRuntimeService({ getConfig: () => params.configManager.config });
@@ -10440,6 +10609,13 @@ var ServiceAppManager = class {
10440
10609
  runtimeService: this.runtimeService
10441
10610
  });
10442
10611
  }
10612
+ start = async () => {
10613
+ this.reconciliationDiagnostics = await this.removalService.reconcile({
10614
+ grantStore: this.createGrantStore(),
10615
+ lockPathForAppId: (appId) => this.getServiceAppLockPath(this.getWorkspacePath(), appId),
10616
+ serviceAppsPath: this.getServiceAppsPath(this.getWorkspacePath())
10617
+ });
10618
+ };
10443
10619
  listServiceApps = async () => {
10444
10620
  const workspacePath = this.getWorkspacePath();
10445
10621
  const serviceAppsPath = this.getServiceAppsPath(workspacePath);
@@ -10450,6 +10626,7 @@ var ServiceAppManager = class {
10450
10626
  return {
10451
10627
  workspacePath,
10452
10628
  serviceAppsPath,
10629
+ diagnostics: this.reconciliationDiagnostics,
10453
10630
  entries: [...workspaceEntries, ...packageEntries].filter((entry) => Boolean(entry)).sort((left, right) => left.title.localeCompare(right.title))
10454
10631
  };
10455
10632
  };
@@ -10462,7 +10639,7 @@ var ServiceAppManager = class {
10462
10639
  return await Promise.all(actions.map(async (action) => await this.withGrantState(action, params)));
10463
10640
  };
10464
10641
  discoverServiceAppActions = async (appId) => {
10465
- const { manifest, record } = await this.requireServiceApp(appId);
10642
+ const { manifest, record } = await this.requireServiceApp(appId, true);
10466
10643
  return mergeServiceAppRuntimeActions({
10467
10644
  record,
10468
10645
  manifest,
@@ -10475,7 +10652,7 @@ var ServiceAppManager = class {
10475
10652
  invokeServiceAction = async (actionId, request) => {
10476
10653
  this.assertCaller(request.caller);
10477
10654
  this.assertDeclaredAction(actionId, request.declaredActions);
10478
- const { manifest, record } = await this.requireServiceAppForAction(actionId);
10655
+ const { manifest, record } = await this.requireServiceAppForAction(actionId, true);
10479
10656
  const actionName = getServiceActionName(actionId, record.id);
10480
10657
  if (!Object.hasOwn(manifest.actions, actionName)) throw new ServiceAppError("SERVICE_APP_ACTION_NOT_FOUND", "service action not found");
10481
10658
  const declaredRisk = manifest.actions[actionName]?.risk ?? "dangerous";
@@ -10535,12 +10712,11 @@ var ServiceAppManager = class {
10535
10712
  try {
10536
10713
  record = await this.removalService.remove({
10537
10714
  grantStore: this.createGrantStore(),
10538
- lockPath: join(workspacePath, ".nextclaw", "locks", "service-apps", `${appId}.lock`),
10715
+ lockPath: this.getServiceAppLockPath(workspacePath, appId),
10539
10716
  purgeData,
10540
10717
  loadRecord: async () => {
10541
10718
  const { record } = await this.requireServiceApp(appId);
10542
10719
  if (record.sourceKind === "package") throw new ServiceAppError("SERVICE_APP_MANAGED_SOURCE", `package service must be managed through Apps: ${record.packageId}`);
10543
- if (purgeData && !record.storage) throw new ServiceAppError("SERVICE_APP_READ_FAILED", `Service App ${appId} 缺少受管数据目录,已停止删除。`);
10544
10720
  return record;
10545
10721
  },
10546
10722
  stopRuntime: async (loaded) => await this.runtimeService.restart(loaded.id)
@@ -10624,12 +10800,12 @@ var ServiceAppManager = class {
10624
10800
  if (!action) throw new ServiceAppError("SERVICE_APP_ACTION_NOT_FOUND", "service action not found");
10625
10801
  return action;
10626
10802
  };
10627
- requireServiceAppForAction = async (actionId) => {
10803
+ requireServiceAppForAction = async (actionId, materializeStorage = false) => {
10628
10804
  const appId = actionId.split(".")[0]?.trim();
10629
10805
  if (!appId) throw new ServiceAppError("SERVICE_APP_INVALID_ACTION", "service action id is invalid");
10630
- return await this.requireServiceApp(appId);
10806
+ return await this.requireServiceApp(appId, materializeStorage);
10631
10807
  };
10632
- requireServiceApp = async (appId) => {
10808
+ requireServiceApp = async (appId, materializeStorage = false) => {
10633
10809
  let dirPath = join(this.getServiceAppsPath(this.getWorkspacePath()), appId);
10634
10810
  let packageSource;
10635
10811
  try {
@@ -10644,9 +10820,10 @@ var ServiceAppManager = class {
10644
10820
  if (!(await stat(dirPath)).isDirectory()) throw new ServiceAppError("SERVICE_APP_NOT_FOUND", "service app not found");
10645
10821
  const manifest = await readServiceAppManifest(dirPath);
10646
10822
  if (manifest.id !== appId) throw new ServiceAppError("SERVICE_APP_INVALID_MANIFEST", "service app manifest id must match directory name");
10823
+ const storage = packageSource?.storage ?? (materializeStorage ? await this.recordService.materializeWorkspaceStorage(manifest.id) : await this.recordService.inspectWorkspaceStorage(manifest.id));
10647
10824
  return {
10648
10825
  manifest,
10649
- record: this.recordService.fromManifest(dirPath, manifest, packageSource, packageSource?.storage ?? await this.recordService.materializeWorkspaceStorage(manifest.id))
10826
+ record: this.recordService.fromManifest(dirPath, manifest, packageSource, storage)
10650
10827
  };
10651
10828
  } catch (error) {
10652
10829
  if (isServiceAppError(error)) throw error;
@@ -10664,7 +10841,7 @@ var ServiceAppManager = class {
10664
10841
  const manifest = await readServiceAppManifest(dirPath);
10665
10842
  return {
10666
10843
  manifest,
10667
- record: this.recordService.fromManifest(dirPath, manifest, void 0, await this.recordService.materializeWorkspaceStorage(manifest.id))
10844
+ record: this.recordService.fromManifest(dirPath, manifest, void 0, await this.recordService.inspectWorkspaceStorage(manifest.id))
10668
10845
  };
10669
10846
  } catch {
10670
10847
  return null;
@@ -10692,13 +10869,13 @@ var ServiceAppManager = class {
10692
10869
  normalizeActionIds = (actionIds) => Array.from(new Set(actionIds.map((actionId) => actionId.trim()).filter((actionId) => actionId.length > 0)));
10693
10870
  getWorkspacePath = () => getWorkspacePathFromConfig(this.params.configManager.config);
10694
10871
  getServiceAppsPath = (workspacePath) => join(workspacePath, DEFAULT_SERVICE_APPS_DIR);
10872
+ getServiceAppLockPath = (workspacePath, appId) => join(workspacePath, ".nextclaw", "locks", "service-apps", `${appId}.lock`);
10695
10873
  createGrantStore = () => new ServiceActionGrantStore(join(this.getServiceAppsPath(this.getWorkspacePath()), SERVICE_ACTION_GRANTS_FILE_NAME));
10696
10874
  listPackageComponentSources = async () => await this.params.listPackageComponentSources?.() ?? [];
10697
10875
  listServiceAppDirNames = async (serviceAppsPath) => {
10698
10876
  try {
10699
- return (await readdir(serviceAppsPath, { withFileTypes: true })).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => entry.name);
10877
+ return await this.removalService.listCanonicalDirectoryNames(serviceAppsPath);
10700
10878
  } catch (error) {
10701
- if (this.isMissingFileError(error)) return [];
10702
10879
  throw new ServiceAppError("SERVICE_APP_READ_FAILED", error instanceof Error ? error.message : String(error));
10703
10880
  }
10704
10881
  };
@@ -15970,6 +16147,9 @@ var NextclawKernel = class {
15970
16147
  };
15971
16148
  getGatewayController = () => this.gatewayController;
15972
16149
  start = async () => {
16150
+ await this.appPackageManager.start();
16151
+ await this.appDataManager.start();
16152
+ await this.serviceAppManager.start();
15973
16153
  this.sessionSearch.start();
15974
16154
  this.mcpManager.start();
15975
16155
  this.providerModelCatalog.start();