@nextclaw/kernel 0.1.4 → 0.1.6

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.
Files changed (3) hide show
  1. package/dist/index.d.ts +14 -1
  2. package/dist/index.js +881 -112
  3. package/package.json +14 -14
package/dist/index.js CHANGED
@@ -3,9 +3,9 @@ import { AgentRouteResolver, CONTEXT_COMPACTION_METADATA_KEY, ChannelManager, Ch
3
3
  import { EventBus, Ingress, createAppEventKey, createTypedKey, eventKeys } from "@nextclaw/shared";
4
4
  import { DefaultNcpAgentRuntime, LocalAssetStore, buildAssetContentPath, buildNcpUserContent, buildOpenAiFunctionTool } from "@nextclaw/ncp-agent-runtime";
5
5
  import { NcpEventType, readAssistantReasoningNormalizationMode, readAssistantReasoningNormalizationModeFromMetadata, sanitizeAssistantReplyTags, writeAssistantReasoningNormalizationModeToMetadata } from "@nextclaw/ncp";
6
- import { DefaultNcpAgentBackend, createAgentClientFromServer } from "@nextclaw/ncp-toolkit";
6
+ import { DefaultNcpAgentBackend, DefaultNcpAgentConversationStateManager, createAgentClientFromServer } from "@nextclaw/ncp-toolkit";
7
7
  import { basename, delimiter, dirname, join, resolve } from "node:path";
8
- import { access, readFile } from "node:fs/promises";
8
+ import { access, appendFile, mkdir, readFile, readdir, unlink, writeFile } from "node:fs/promises";
9
9
  import { appendFileSync, constants, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
10
10
  import { BUILTIN_PROVIDER_PLUGINS } from "@nextclaw/runtime";
11
11
  import { HttpRuntimeConfigResolver, HttpRuntimeNcpAgentRuntime } from "@nextclaw/nextclaw-ncp-runtime-http-client";
@@ -592,10 +592,10 @@ var AgentRuntimeRegistry = class {
592
592
  //#endregion
593
593
  //#region src/features/ncp-dispatch/utils/agent-runtime-handle.utils.ts
594
594
  function createAgentRuntimeHandle(params) {
595
- const { backend, runtimeRegistry, refreshPluginRuntimeRegistrations, refreshConfiguredRuntimeEntries, applyMcpConfig, dispose, assetStore } = params;
595
+ const { agentClientEndpoint, backend, runtimeRegistry, refreshPluginRuntimeRegistrations, refreshConfiguredRuntimeEntries, applyMcpConfig, dispose, assetStore } = params;
596
596
  return {
597
597
  basePath: "/api/ncp/agent",
598
- agentClientEndpoint: createAgentClientFromServer(backend),
598
+ agentClientEndpoint: agentClientEndpoint ?? createAgentClientFromServer(backend),
599
599
  streamProvider: backend,
600
600
  runApi: backend,
601
601
  sessionApi: backend,
@@ -625,11 +625,11 @@ function normalizeString(value) {
625
625
  const trimmed = value.trim();
626
626
  return trimmed.length > 0 ? trimmed : null;
627
627
  }
628
- function isRecord$3(value) {
628
+ function isRecord$5(value) {
629
629
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
630
630
  }
631
631
  function cloneMetadata(value) {
632
- return isRecord$3(value) ? structuredClone(value) : void 0;
632
+ return isRecord$5(value) ? structuredClone(value) : void 0;
633
633
  }
634
634
  function readStringArray(value) {
635
635
  if (!Array.isArray(value)) return null;
@@ -1876,7 +1876,7 @@ var LlmUsageManager = class {
1876
1876
  };
1877
1877
  //#endregion
1878
1878
  //#region src/tools/session-search.tools.ts
1879
- function isRecord$2(value) {
1879
+ function isRecord$4(value) {
1880
1880
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1881
1881
  }
1882
1882
  function readOptionalInteger(value) {
@@ -1926,7 +1926,7 @@ var SessionSearchTool = class {
1926
1926
  return issues;
1927
1927
  };
1928
1928
  execute = async (args) => {
1929
- if (!isRecord$2(args)) throw new Error("session_search requires an object argument.");
1929
+ if (!isRecord$4(args)) throw new Error("session_search requires an object argument.");
1930
1930
  const issues = this.validateArgs(args);
1931
1931
  if (issues.length > 0) throw new Error(issues.join(" "));
1932
1932
  return this.queryService.search({
@@ -1939,13 +1939,13 @@ var SessionSearchTool = class {
1939
1939
  };
1940
1940
  //#endregion
1941
1941
  //#region src/features/native-runtime/tools/ncp-asset.tools.ts
1942
- function readOptionalString$2(value) {
1942
+ function readOptionalString$4(value) {
1943
1943
  if (typeof value !== "string") return null;
1944
1944
  const trimmed = value.trim();
1945
1945
  return trimmed.length > 0 ? trimmed : null;
1946
1946
  }
1947
1947
  function readOptionalBase64Bytes(value) {
1948
- const base64 = readOptionalString$2(value);
1948
+ const base64 = readOptionalString$4(value);
1949
1949
  if (!base64) return null;
1950
1950
  try {
1951
1951
  return Buffer.from(base64, "base64");
@@ -1996,18 +1996,18 @@ var AssetPutTool = class {
1996
1996
  this.contentBasePath = contentBasePath;
1997
1997
  }
1998
1998
  validateArgs = (args) => {
1999
- const path = readOptionalString$2(args.path);
2000
- const bytesBase64 = readOptionalString$2(args.bytesBase64);
2001
- const fileName = readOptionalString$2(args.fileName);
1999
+ const path = readOptionalString$4(args.path);
2000
+ const bytesBase64 = readOptionalString$4(args.bytesBase64);
2001
+ const fileName = readOptionalString$4(args.fileName);
2002
2002
  if (path && bytesBase64) return ["Provide either path, or bytesBase64 + fileName, not both."];
2003
2003
  if (path) return [];
2004
2004
  if (bytesBase64) return fileName ? [] : ["fileName is required when using bytesBase64."];
2005
2005
  return ["Provide either path, or bytesBase64 + fileName."];
2006
2006
  };
2007
2007
  execute = async (args) => {
2008
- const path = readOptionalString$2(args?.path);
2009
- const fileName = readOptionalString$2(args?.fileName);
2010
- const mimeType = readOptionalString$2(args?.mimeType);
2008
+ const path = readOptionalString$4(args?.path);
2009
+ const fileName = readOptionalString$4(args?.fileName);
2010
+ const mimeType = readOptionalString$4(args?.mimeType);
2011
2011
  const bytes = readOptionalBase64Bytes(args?.bytesBase64);
2012
2012
  if (path) return {
2013
2013
  ok: true,
@@ -2050,8 +2050,8 @@ var AssetExportTool = class {
2050
2050
  this.assetStore = assetStore;
2051
2051
  }
2052
2052
  execute = async (args) => {
2053
- const assetUri = readOptionalString$2(args?.assetUri);
2054
- const targetPath = readOptionalString$2(args?.targetPath);
2053
+ const assetUri = readOptionalString$4(args?.assetUri);
2054
+ const targetPath = readOptionalString$4(args?.targetPath);
2055
2055
  if (!assetUri || !targetPath) throw new Error("asset_export requires assetUri and targetPath.");
2056
2056
  return {
2057
2057
  ok: true,
@@ -2077,7 +2077,7 @@ var AssetStatTool = class {
2077
2077
  this.contentBasePath = contentBasePath;
2078
2078
  }
2079
2079
  execute = async (args) => {
2080
- const assetUri = readOptionalString$2(args?.assetUri);
2080
+ const assetUri = readOptionalString$4(args?.assetUri);
2081
2081
  if (!assetUri) throw new Error("asset_stat requires assetUri.");
2082
2082
  const record = await this.assetStore.statRecord(assetUri);
2083
2083
  if (!record) return {
@@ -2252,7 +2252,7 @@ function readRequiredString$1(params, key) {
2252
2252
  if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${key} must be a non-empty string.`);
2253
2253
  return value.trim();
2254
2254
  }
2255
- function readOptionalString$1(params, key) {
2255
+ function readOptionalString$3(params, key) {
2256
2256
  const value = params[key];
2257
2257
  if (typeof value !== "string") return;
2258
2258
  const trimmed = value.trim();
@@ -2313,7 +2313,7 @@ var SessionRequestTool = class extends Tool {
2313
2313
  const target = params.target;
2314
2314
  if (!target || typeof target !== "object" || Array.isArray(target)) throw new Error("target must be an object.");
2315
2315
  const task = readRequiredString$1(params, "task");
2316
- const notifyMode = readOptionalString$1(params, "notify")?.toLowerCase();
2316
+ const notifyMode = readOptionalString$3(params, "notify")?.toLowerCase();
2317
2317
  if (notifyMode !== "none" && notifyMode !== "final_reply") throw new Error("notify must be \"none\" or \"final_reply\".");
2318
2318
  const { toolCallId, updateToolCallResult } = context;
2319
2319
  return this.manager.requestSession({
@@ -2322,7 +2322,7 @@ var SessionRequestTool = class extends Tool {
2322
2322
  updateToolCallResult,
2323
2323
  targetSessionId: readRequiredString$1(target, "session_id"),
2324
2324
  task,
2325
- title: readOptionalString$1(params, "title"),
2325
+ title: readOptionalString$3(params, "title"),
2326
2326
  notify: notifyMode,
2327
2327
  handoffDepth: this.handoffDepth
2328
2328
  });
@@ -2334,13 +2334,13 @@ function readRequiredString(value, key) {
2334
2334
  if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${key} must be a non-empty string.`);
2335
2335
  return value.trim();
2336
2336
  }
2337
- function readOptionalString(value) {
2337
+ function readOptionalString$2(value) {
2338
2338
  if (typeof value !== "string") return;
2339
2339
  const trimmed = value.trim();
2340
2340
  return trimmed.length > 0 ? trimmed : void 0;
2341
2341
  }
2342
2342
  function readSpawnScope(value) {
2343
- const normalized = readOptionalString(value)?.toLowerCase();
2343
+ const normalized = readOptionalString$2(value)?.toLowerCase();
2344
2344
  if (!normalized || normalized === "standalone") return "standalone";
2345
2345
  if (normalized === "child") return "child";
2346
2346
  throw new Error("scope must be \"standalone\" or \"child\".");
@@ -2348,7 +2348,7 @@ function readSpawnScope(value) {
2348
2348
  function readSpawnRequestOptions(value) {
2349
2349
  if (typeof value === "undefined") return;
2350
2350
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("request must be an object.");
2351
- const notifyMode = readOptionalString(value.notify)?.toLowerCase();
2351
+ const notifyMode = readOptionalString$2(value.notify)?.toLowerCase();
2352
2352
  if (notifyMode === "none" || notifyMode === "final_reply") return { notify: notifyMode };
2353
2353
  throw new Error("request.notify must be \"none\" or \"final_reply\".");
2354
2354
  }
@@ -2429,10 +2429,10 @@ var SessionSpawnTool = class extends Tool {
2429
2429
  updateToolCallResult,
2430
2430
  sourceSessionMetadata: this.sourceSessionMetadata,
2431
2431
  task,
2432
- title: readOptionalString(rawTitle),
2433
- agentId: readOptionalString(rawAgentId),
2434
- model: readOptionalString(rawModel),
2435
- runtime: readOptionalString(rawRuntime),
2432
+ title: readOptionalString$2(rawTitle),
2433
+ agentId: readOptionalString$2(rawAgentId),
2434
+ model: readOptionalString$2(rawModel),
2435
+ runtime: readOptionalString$2(rawRuntime),
2436
2436
  handoffDepth: this.handoffDepth,
2437
2437
  ...parentSessionId ? { parentSessionId } : {},
2438
2438
  notify: request.notify
@@ -2440,11 +2440,11 @@ var SessionSpawnTool = class extends Tool {
2440
2440
  const session = this.sessionManager.createSession({
2441
2441
  sourceSessionId: this.sourceSessionId,
2442
2442
  task,
2443
- title: readOptionalString(rawTitle),
2443
+ title: readOptionalString$2(rawTitle),
2444
2444
  sourceSessionMetadata: this.sourceSessionMetadata,
2445
- agentId: readOptionalString(rawAgentId),
2446
- model: readOptionalString(rawModel),
2447
- runtime: readOptionalString(rawRuntime),
2445
+ agentId: readOptionalString$2(rawAgentId),
2446
+ model: readOptionalString$2(rawModel),
2447
+ runtime: readOptionalString$2(rawRuntime),
2448
2448
  ...parentSessionId ? { parentSessionId } : {}
2449
2449
  });
2450
2450
  this.onSessionUpdated?.(session.sessionId);
@@ -2469,10 +2469,10 @@ var SessionSpawnTool = class extends Tool {
2469
2469
  //#endregion
2470
2470
  //#region src/features/native-runtime/services/nextclaw-ncp-tool-registry.service.ts
2471
2471
  function toToolParams(args) {
2472
- if (isRecord$3(args)) return args;
2472
+ if (isRecord$5(args)) return args;
2473
2473
  if (typeof args === "string") try {
2474
2474
  const parsed = JSON.parse(args);
2475
- return isRecord$3(parsed) ? parsed : {};
2475
+ return isRecord$5(parsed) ? parsed : {};
2476
2476
  } catch {
2477
2477
  return {};
2478
2478
  }
@@ -2664,14 +2664,14 @@ function readAccountIdForHints(metadata, sessionMetadata) {
2664
2664
  //#endregion
2665
2665
  //#region src/features/native-runtime/services/nextclaw-ncp-context-builder.service.ts
2666
2666
  const TIME_HINT_TRIGGER_PATTERNS = [/\b(now|right now|current time|what time|today|tonight|tomorrow|yesterday|this morning|this afternoon|this evening|date)\b/i, /(现在|此刻|当前时间|现在几点|几点了|今天|今晚|今早|今晨|明天|昨天|日期)/];
2667
- function isRecord$1(value) {
2667
+ function isRecord$3(value) {
2668
2668
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
2669
2669
  }
2670
2670
  function mergeInputMetadata(input) {
2671
- const messageMetadata = input.messages.slice().reverse().find((message) => isRecord$1(message.metadata))?.metadata;
2671
+ const messageMetadata = input.messages.slice().reverse().find((message) => isRecord$3(message.metadata))?.metadata;
2672
2672
  return {
2673
- ...isRecord$1(messageMetadata) ? structuredClone(messageMetadata) : {},
2674
- ...isRecord$1(input.metadata) ? structuredClone(input.metadata) : {}
2673
+ ...isRecord$3(messageMetadata) ? structuredClone(messageMetadata) : {},
2674
+ ...isRecord$3(input.metadata) ? structuredClone(input.metadata) : {}
2675
2675
  };
2676
2676
  }
2677
2677
  const REQUESTED_SKILLS_METADATA_READER = new RequestedSkillsMetadataReader();
@@ -2978,12 +2978,12 @@ function createContextWindowUpdatedEvent(params) {
2978
2978
  }
2979
2979
  };
2980
2980
  }
2981
- function isRecord(value) {
2981
+ function isRecord$2(value) {
2982
2982
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
2983
2983
  }
2984
2984
  function resolveNativeReasoningNormalizationMode(params) {
2985
2985
  const runtimeEntry = params.config.agents.runtimes.entries.native?.config ?? params.config.ui.ncp.runtimes.native;
2986
- const runtimeMetadata = isRecord(runtimeEntry) ? runtimeEntry : {};
2986
+ const runtimeMetadata = isRecord$2(runtimeEntry) ? runtimeEntry : {};
2987
2987
  return readAssistantReasoningNormalizationModeFromMetadata(params.sessionMetadata) ?? readAssistantReasoningNormalizationMode(runtimeMetadata.reasoningNormalization) ?? readAssistantReasoningNormalizationMode(runtimeMetadata.reasoning_normalization) ?? readAssistantReasoningNormalizationMode(runtimeMetadata.reasoningNormalizationMode) ?? readAssistantReasoningNormalizationMode(runtimeMetadata.reasoning_normalization_mode) ?? "think-tags";
2988
2988
  }
2989
2989
  function createSessionSearchTools(params) {
@@ -3240,6 +3240,24 @@ function createAgentRuntimeSessionRequestDispatcher(options) {
3240
3240
  }
3241
3241
  //#endregion
3242
3242
  //#region src/managers/agent-runtime.manager.ts
3243
+ function readOptionalString$1(value) {
3244
+ if (typeof value !== "string") return null;
3245
+ const trimmed = value.trim();
3246
+ return trimmed.length > 0 ? trimmed : null;
3247
+ }
3248
+ function readMessageTask(message) {
3249
+ for (const part of message.parts) if ((part.type === "text" || part.type === "rich-text" || part.type === "reasoning") && part.text.trim()) return part.text.trim();
3250
+ return "Session";
3251
+ }
3252
+ function readSessionMetadataString(metadata, ...keys) {
3253
+ for (const key of keys) {
3254
+ const value = readOptionalString$1(metadata?.[key]);
3255
+ if (value) return value;
3256
+ }
3257
+ }
3258
+ async function consumeAgentEvents(events) {
3259
+ for await (const event of events);
3260
+ }
3243
3261
  var AgentRuntimeManager = class {
3244
3262
  runtimeRegistry = new AgentRuntimeRegistry();
3245
3263
  mcpRuntimeSupport;
@@ -3268,6 +3286,7 @@ var AgentRuntimeManager = class {
3268
3286
  get currentHandle() {
3269
3287
  return this.handle;
3270
3288
  }
3289
+ isLiveSessionRunning = (sessionId) => this.backend?.isLiveSessionRunning(sessionId) ?? false;
3271
3290
  connectGatewayController = (gatewayController) => {
3272
3291
  this.assertNotDisposed();
3273
3292
  this.gatewayController = gatewayController;
@@ -3321,6 +3340,7 @@ var AgentRuntimeManager = class {
3321
3340
  await this.backend.start();
3322
3341
  this.handle = createAgentRuntimeHandle({
3323
3342
  backend: this.backend,
3343
+ agentClientEndpoint: this.createMaterializingAgentClientEndpoint(this.backend),
3324
3344
  runtimeRegistry: this.runtimeRegistry,
3325
3345
  refreshPluginRuntimeRegistrations: this.pluginRuntimeRegistrationController.refreshPluginRuntimeRegistrations,
3326
3346
  refreshConfiguredRuntimeEntries: this.refreshConfiguredRuntimeEntries,
@@ -3330,6 +3350,68 @@ var AgentRuntimeManager = class {
3330
3350
  });
3331
3351
  return this.handle;
3332
3352
  };
3353
+ materializeAgentSendEnvelope = (envelope) => {
3354
+ const existingSessionId = readOptionalString$1(envelope.sessionId) ?? readOptionalString$1(envelope.message.sessionId);
3355
+ if (existingSessionId) return {
3356
+ ...envelope,
3357
+ sessionId: existingSessionId,
3358
+ message: {
3359
+ ...envelope.message,
3360
+ sessionId: existingSessionId
3361
+ }
3362
+ };
3363
+ const metadata = envelope.metadata ?? {};
3364
+ const createdSession = this.params.sessions.createSession({
3365
+ task: readMessageTask(envelope.message),
3366
+ title: readSessionMetadataString(metadata, "label", "title"),
3367
+ sourceSessionMetadata: {},
3368
+ metadataOverrides: metadata,
3369
+ agentId: readSessionMetadataString(metadata, "agent_id", "agentId"),
3370
+ model: readSessionMetadataString(metadata, "preferred_model", "model"),
3371
+ runtime: readSessionMetadataString(metadata, "runtime", "session_type"),
3372
+ sessionType: readSessionMetadataString(metadata, "session_type", "runtime"),
3373
+ thinkingLevel: readSessionMetadataString(metadata, "preferred_thinking", "thinking"),
3374
+ projectRoot: readSessionMetadataString(metadata, "project_root")
3375
+ });
3376
+ this.params.onSessionUpdated(createdSession.sessionId);
3377
+ return {
3378
+ ...envelope,
3379
+ sessionId: createdSession.sessionId,
3380
+ message: {
3381
+ ...envelope.message,
3382
+ sessionId: createdSession.sessionId
3383
+ }
3384
+ };
3385
+ };
3386
+ createMaterializingAgentClientEndpoint = (backend) => ({
3387
+ get manifest() {
3388
+ return backend.manifest;
3389
+ },
3390
+ start: backend.start,
3391
+ stop: backend.stop,
3392
+ subscribe: backend.subscribe,
3393
+ stream: async (payload) => {
3394
+ await consumeAgentEvents(backend.stream(payload));
3395
+ },
3396
+ abort: backend.abort,
3397
+ send: async (envelope) => {
3398
+ await consumeAgentEvents(backend.send(this.materializeAgentSendEnvelope(envelope)));
3399
+ },
3400
+ emit: async (event) => {
3401
+ switch (event.type) {
3402
+ case NcpEventType.MessageRequest:
3403
+ await consumeAgentEvents(backend.send(this.materializeAgentSendEnvelope(event.payload)));
3404
+ return;
3405
+ case NcpEventType.MessageStreamRequest:
3406
+ await consumeAgentEvents(backend.stream(event.payload));
3407
+ return;
3408
+ case NcpEventType.MessageAbort:
3409
+ await backend.abort(event.payload);
3410
+ return;
3411
+ default: await backend.emit(event);
3412
+ }
3413
+ }
3414
+ });
3333
3415
  warmDerivedCapabilities = async () => {
3334
3416
  this.assertNotDisposed();
3335
3417
  this.warmupPromise ??= this.runDerivedCapabilityWarmup();
@@ -4069,11 +4151,12 @@ var NcpLifecycleEventBridge = class {
4069
4151
  //#endregion
4070
4152
  //#region src/utils/ncp-session-summary.utils.ts
4071
4153
  function createNcpSessionSummary(params) {
4072
- const { sessionId, agentId, messages, updatedAt, status, metadata, contextWindow } = params;
4154
+ const { sessionId, agentId, messages, createdAt, updatedAt, status, metadata, contextWindow } = params;
4073
4155
  return {
4074
4156
  sessionId,
4075
4157
  ...agentId ? { agentId } : {},
4076
4158
  messageCount: messages.length,
4159
+ createdAt,
4077
4160
  updatedAt,
4078
4161
  ...messages.length > 0 ? { lastMessageAt: messages[messages.length - 1]?.timestamp ?? updatedAt } : {},
4079
4162
  status,
@@ -4100,6 +4183,18 @@ function buildUpdatedMetadata(params) {
4100
4183
  function normalizeSessionId(sessionId) {
4101
4184
  return sessionId.trim();
4102
4185
  }
4186
+ function createSessionListSummary(record) {
4187
+ return {
4188
+ sessionId: record.sessionId,
4189
+ ...record.agentId ? { agentId: record.agentId } : {},
4190
+ messageCount: record.messageCount ?? 0,
4191
+ createdAt: record.created_at,
4192
+ updatedAt: record.updated_at,
4193
+ ...record.lastMessageAt ? { lastMessageAt: record.lastMessageAt } : {},
4194
+ status: "idle",
4195
+ metadata: structuredClone(record.metadata)
4196
+ };
4197
+ }
4103
4198
  var NcpSessionApiService = class {
4104
4199
  unsubscribeSessionUpdated = null;
4105
4200
  contextWindowPreview;
@@ -4133,39 +4228,47 @@ var NcpSessionApiService = class {
4133
4228
  this.options.eventBus.emit(eventKeys.sessionSummaryDelete, { sessionKey: normalizedSessionKey });
4134
4229
  };
4135
4230
  listSessions = async (options) => {
4136
- const summaries = [];
4137
- for (const record of this.options.sessionManager.listSessions()) {
4138
- const sessionId = normalizeSessionId(record.key);
4139
- if (!sessionId) continue;
4140
- const session = this.options.sessionManager.getIfExists(sessionId);
4141
- if (!session) continue;
4142
- summaries.push(createNcpSessionSummary({
4143
- sessionId,
4144
- agentId: session.agentId,
4145
- messages: toNcpMessages(sessionId, session.messages),
4146
- updatedAt: session.updatedAt.toISOString(),
4147
- status: "idle",
4148
- metadata: session.metadata
4149
- }));
4150
- }
4151
- summaries.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
4152
- return applyLimit(summaries, options?.limit);
4231
+ if (this.options.ncpAgentSessionStore?.listSessionSummaries) return applyLimit(await this.options.ncpAgentSessionStore.listSessionSummaries(), options?.limit).map(this.withLiveSessionStatus);
4232
+ return applyLimit(this.options.sessionManager.listSessions().map((record) => ({
4233
+ ...record,
4234
+ sessionId: normalizeSessionId(record.key)
4235
+ })).filter((record) => Boolean(record)).sort((left, right) => (right.lastMessageAt ?? right.created_at).localeCompare(left.lastMessageAt ?? left.created_at)), options?.limit).map(createSessionListSummary).map(this.withLiveSessionStatus);
4153
4236
  };
4154
4237
  listSessionMessages = async (sessionId, options) => {
4155
4238
  const normalizedSessionId = normalizeSessionId(sessionId);
4239
+ if (normalizedSessionId && this.options.ncpAgentSessionStore?.listSessionMessages) return applyLimit(await this.options.ncpAgentSessionStore.listSessionMessages(normalizedSessionId), options?.limit);
4156
4240
  const session = normalizedSessionId ? this.options.sessionManager.getIfExists(normalizedSessionId) : null;
4157
4241
  if (!session) return [];
4158
4242
  return applyLimit(toNcpMessages(normalizedSessionId, session.messages).map((message) => structuredClone(message)), options?.limit);
4159
4243
  };
4160
4244
  getSession = async (sessionId) => {
4161
4245
  const normalizedSessionId = normalizeSessionId(sessionId);
4246
+ if (normalizedSessionId && this.options.ncpAgentSessionStore?.getSession) {
4247
+ const record = await this.options.ncpAgentSessionStore.getSession(normalizedSessionId);
4248
+ if (record) return this.withLiveSessionStatus(createNcpSessionSummary({
4249
+ sessionId: normalizedSessionId,
4250
+ agentId: record.agentId,
4251
+ messages: record.messages,
4252
+ createdAt: record.createdAt ?? record.updatedAt,
4253
+ updatedAt: record.updatedAt,
4254
+ status: "idle",
4255
+ metadata: record.metadata,
4256
+ contextWindow: this.contextWindowPreview.preview({
4257
+ contextWindowOwner: "nextclaw",
4258
+ requestMetadata: record.metadata ?? {},
4259
+ sessionId: normalizedSessionId,
4260
+ sessionMessages: record.messages
4261
+ })
4262
+ }));
4263
+ }
4162
4264
  const session = normalizedSessionId ? this.options.sessionManager.getIfExists(normalizedSessionId) : null;
4163
4265
  if (!session) return null;
4164
4266
  const messages = toNcpMessages(normalizedSessionId, session.messages);
4165
- return createNcpSessionSummary({
4267
+ return this.withLiveSessionStatus(createNcpSessionSummary({
4166
4268
  sessionId: normalizedSessionId,
4167
4269
  agentId: session.agentId,
4168
4270
  messages,
4271
+ createdAt: session.createdAt.toISOString(),
4169
4272
  updatedAt: session.updatedAt.toISOString(),
4170
4273
  status: "idle",
4171
4274
  metadata: session.metadata,
@@ -4175,10 +4278,25 @@ var NcpSessionApiService = class {
4175
4278
  sessionId: normalizedSessionId,
4176
4279
  sessionMessages: messages
4177
4280
  })
4178
- });
4281
+ }));
4179
4282
  };
4180
4283
  updateSession = async (sessionId, patch) => {
4181
4284
  const normalizedSessionId = normalizeSessionId(sessionId);
4285
+ if (normalizedSessionId && this.options.ncpAgentSessionStore) {
4286
+ const existing = await this.options.ncpAgentSessionStore.getSession(normalizedSessionId);
4287
+ if (existing) {
4288
+ await this.options.ncpAgentSessionStore.replaceSession({
4289
+ ...existing,
4290
+ metadata: buildUpdatedMetadata({
4291
+ existingMetadata: existing.metadata,
4292
+ patch
4293
+ }),
4294
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
4295
+ });
4296
+ await this.publishSessionChange(normalizedSessionId);
4297
+ return await this.getSession(normalizedSessionId);
4298
+ }
4299
+ }
4182
4300
  const session = normalizedSessionId ? this.options.sessionManager.getIfExists(normalizedSessionId) : null;
4183
4301
  if (!session) return null;
4184
4302
  session.metadata = buildUpdatedMetadata({
@@ -4193,9 +4311,14 @@ var NcpSessionApiService = class {
4193
4311
  deleteSession = async (sessionId) => {
4194
4312
  const normalizedSessionId = normalizeSessionId(sessionId);
4195
4313
  if (!normalizedSessionId) return;
4314
+ if (this.options.ncpAgentSessionStore) await this.options.ncpAgentSessionStore.deleteSession(normalizedSessionId);
4196
4315
  this.options.sessionManager.delete(normalizedSessionId);
4197
4316
  await this.publishSessionChange(normalizedSessionId);
4198
4317
  };
4318
+ withLiveSessionStatus = (summary) => this.options.isLiveSessionRunning?.(summary.sessionId) ? {
4319
+ ...summary,
4320
+ status: "running"
4321
+ } : summary;
4199
4322
  };
4200
4323
  //#endregion
4201
4324
  //#region src/utils/ncp-session-metadata.utils.ts
@@ -4205,58 +4328,72 @@ function resolvePersistedSessionMetadata(params) {
4205
4328
  return mergeSessionMetadata(preserveExistingMetadata ? mergeSessionMetadata(currentMetadata, messageMetadata) : mergeSessionMetadata({}, messageMetadata), cloneMetadata(sessionRecord.metadata));
4206
4329
  }
4207
4330
  //#endregion
4208
- //#region src/services/ncp-agent-session-store-adapter.service.ts
4209
- function readAgentIdFromMetadata(metadata) {
4210
- return normalizeString(metadata?.agent_id)?.toLowerCase() ?? normalizeString(metadata?.agentId)?.toLowerCase() ?? void 0;
4211
- }
4331
+ //#region src/stores/ncp-agent-legacy-session.store.ts
4212
4332
  function resolveSessionRecordAgentId(record) {
4213
- return normalizeString(record.agentId)?.toLowerCase() ?? readAgentIdFromMetadata(record.metadata);
4333
+ return normalizeString(record.agentId)?.toLowerCase() ?? normalizeString(record.metadata?.agent_id)?.toLowerCase() ?? normalizeString(record.metadata?.agentId)?.toLowerCase();
4214
4334
  }
4215
- var NcpAgentSessionStoreAdapter = class {
4335
+ var NcpAgentLegacySessionStore = class {
4216
4336
  constructor(sessionManager, options = {}) {
4217
4337
  this.sessionManager = sessionManager;
4218
4338
  this.options = options;
4219
4339
  }
4220
- getSession = async (sessionId) => {
4340
+ getSession = (sessionId) => {
4221
4341
  const session = this.sessionManager.getIfExists(sessionId);
4222
4342
  if (!session) return null;
4223
4343
  return {
4224
4344
  sessionId,
4225
4345
  ...session.agentId ? { agentId: session.agentId } : {},
4226
4346
  messages: toNcpMessages(sessionId, session.messages),
4347
+ createdAt: session.createdAt.toISOString(),
4227
4348
  updatedAt: session.updatedAt.toISOString(),
4228
4349
  metadata: structuredClone(session.metadata)
4229
4350
  };
4230
4351
  };
4231
- listSessions = async () => {
4232
- const records = this.sessionManager.listSessions();
4233
- const sessions = [];
4234
- for (const record of records) {
4235
- const sessionId = normalizeString(record.key);
4236
- if (!sessionId) continue;
4237
- const session = this.sessionManager.getIfExists(sessionId);
4238
- if (!session) continue;
4239
- sessions.push({
4240
- sessionId,
4241
- ...session.agentId ? { agentId: session.agentId } : {},
4242
- messages: toNcpMessages(sessionId, session.messages),
4243
- updatedAt: session.updatedAt.toISOString(),
4244
- metadata: structuredClone(session.metadata)
4245
- });
4246
- }
4247
- sessions.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
4248
- return sessions;
4352
+ getSessionSummary = (sessionId) => {
4353
+ const session = this.getSession(sessionId);
4354
+ if (!session) return null;
4355
+ const lastMessageAt = session.messages.at(-1)?.timestamp;
4356
+ return {
4357
+ sessionId: session.sessionId,
4358
+ ...session.agentId ? { agentId: session.agentId } : {},
4359
+ messageCount: session.messages.length,
4360
+ createdAt: session.createdAt,
4361
+ updatedAt: session.updatedAt,
4362
+ ...lastMessageAt ? { lastMessageAt } : {},
4363
+ status: "idle",
4364
+ metadata: session.metadata
4365
+ };
4249
4366
  };
4250
- persistSession = async (sessionRecord, options) => {
4251
- if (this.options.writeMode === "runtime-owned") return;
4367
+ listSessionMessages = (sessionId) => this.getSession(sessionId)?.messages ?? [];
4368
+ listSessionSummaries = () => this.sessionManager.listSessions().map((record) => ({
4369
+ sessionId: record.key,
4370
+ ...record.agentId ? { agentId: record.agentId } : {},
4371
+ messageCount: record.messageCount ?? 0,
4372
+ createdAt: record.created_at,
4373
+ updatedAt: record.updated_at,
4374
+ ...record.lastMessageAt ? { lastMessageAt: record.lastMessageAt } : {},
4375
+ status: "idle",
4376
+ metadata: structuredClone(record.metadata)
4377
+ }));
4378
+ saveSession = (sessionRecord) => this.persistSession(sessionRecord, true);
4379
+ replaceSession = (sessionRecord) => this.persistSession(sessionRecord, false);
4380
+ deleteSession = (sessionId) => {
4381
+ const existing = this.getSession(sessionId);
4382
+ if (!existing) return null;
4383
+ this.sessionManager.delete(sessionId);
4384
+ this.options.onSessionUpdated?.(sessionId);
4385
+ return existing;
4386
+ };
4387
+ persistSession = async (sessionRecord, preserveExistingMetadata) => {
4252
4388
  const session = this.sessionManager.getIfExists(sessionRecord.sessionId) ?? this.sessionManager.getOrCreate(sessionRecord.sessionId);
4253
4389
  const legacyMessages = toLegacyMessages(sessionRecord.messages);
4254
4390
  const nextAgentId = resolveSessionRecordAgentId(sessionRecord);
4255
4391
  if (nextAgentId) session.agentId = nextAgentId;
4392
+ if (sessionRecord.createdAt) session.createdAt = new Date(ensureIsoTimestamp(sessionRecord.createdAt, session.createdAt.toISOString()));
4256
4393
  session.metadata = resolvePersistedSessionMetadata({
4257
4394
  currentMetadata: session.metadata,
4258
4395
  sessionRecord,
4259
- preserveExistingMetadata: options.preserveExistingMetadata
4396
+ preserveExistingMetadata
4260
4397
  });
4261
4398
  this.sessionManager.clear(session);
4262
4399
  for (const message of legacyMessages) this.sessionManager.appendEvent(session, {
@@ -4268,19 +4405,647 @@ var NcpAgentSessionStoreAdapter = class {
4268
4405
  this.sessionManager.save(session);
4269
4406
  this.options.onSessionUpdated?.(sessionRecord.sessionId);
4270
4407
  };
4408
+ };
4409
+ //#endregion
4410
+ //#region src/services/ncp-agent-session-store-adapter.service.ts
4411
+ function readSessionActivityAt(record) {
4412
+ return record.messages.at(-1)?.timestamp ?? record.createdAt ?? record.updatedAt;
4413
+ }
4414
+ var NcpAgentSessionStoreAdapter = class {
4415
+ appendSessionEvent;
4416
+ legacyStore;
4417
+ constructor(sessionManager, options = {}) {
4418
+ this.options = options;
4419
+ this.legacyStore = new NcpAgentLegacySessionStore(sessionManager, { onSessionUpdated: options.onSessionUpdated });
4420
+ const journalStore = options.journalStore;
4421
+ if (journalStore) this.appendSessionEvent = async (params) => {
4422
+ const { event, session } = params;
4423
+ if (!await journalStore.hasSession(session.sessionId)) {
4424
+ const legacySession = this.legacyStore.getSession(session.sessionId);
4425
+ if (legacySession) await journalStore.replaceSession(legacySession);
4426
+ }
4427
+ await journalStore.appendSessionEvent(params);
4428
+ if (isSessionSummaryRefreshEvent(event)) this.options.onSessionUpdated?.(session.sessionId);
4429
+ };
4430
+ }
4431
+ getSession = async (sessionId) => await this.options.journalStore?.getSession(sessionId) ?? this.legacyStore.getSession(sessionId);
4432
+ getSessionSummary = async (sessionId) => await this.options.journalStore?.getSessionSummary(sessionId) ?? this.legacyStore.getSessionSummary(sessionId);
4433
+ listSessionMessages = async (sessionId) => {
4434
+ const journalStore = this.options.journalStore;
4435
+ return journalStore && await journalStore.hasSession(sessionId) ? await journalStore.listSessionMessages(sessionId) : this.legacyStore.listSessionMessages(sessionId);
4436
+ };
4437
+ listSessionSummaries = async () => {
4438
+ const journalSummaries = await this.options.journalStore?.listSessionSummaries() ?? [];
4439
+ const journalIds = new Set(journalSummaries.map((summary) => summary.sessionId));
4440
+ const legacySummaries = this.legacyStore.listSessionSummaries().filter((summary) => !journalIds.has(summary.sessionId));
4441
+ return [...journalSummaries, ...legacySummaries].sort((left, right) => readSummaryActivityAt(right).localeCompare(readSummaryActivityAt(left)));
4442
+ };
4443
+ listSessions = async () => {
4444
+ const summaries = await this.listSessionSummaries();
4445
+ const sessions = [];
4446
+ for (const summary of summaries) {
4447
+ const sessionId = summary.sessionId.trim();
4448
+ if (!sessionId) continue;
4449
+ const session = await this.getSession(sessionId);
4450
+ if (!session) continue;
4451
+ sessions.push(session);
4452
+ }
4453
+ sessions.sort((left, right) => readSessionActivityAt(right).localeCompare(readSessionActivityAt(left)));
4454
+ return sessions;
4455
+ };
4271
4456
  saveSession = async (sessionRecord) => {
4272
- await this.persistSession(sessionRecord, { preserveExistingMetadata: true });
4457
+ const journalStore = this.options.journalStore;
4458
+ if (journalStore && await journalStore.hasSession(sessionRecord.sessionId)) {
4459
+ await journalStore.replaceSession(sessionRecord);
4460
+ this.options.onSessionUpdated?.(sessionRecord.sessionId);
4461
+ return;
4462
+ }
4463
+ await this.legacyStore.saveSession(sessionRecord);
4273
4464
  };
4274
4465
  replaceSession = async (sessionRecord) => {
4275
- await this.persistSession(sessionRecord, { preserveExistingMetadata: false });
4466
+ const journalStore = this.options.journalStore;
4467
+ if (journalStore && await journalStore.hasSession(sessionRecord.sessionId)) {
4468
+ await journalStore.replaceSession(sessionRecord);
4469
+ this.options.onSessionUpdated?.(sessionRecord.sessionId);
4470
+ return;
4471
+ }
4472
+ await this.legacyStore.replaceSession(sessionRecord);
4276
4473
  };
4277
4474
  deleteSession = async (sessionId) => {
4278
- const existing = await this.getSession(sessionId);
4279
- if (!existing) return null;
4280
- this.sessionManager.delete(sessionId);
4281
- this.options.onSessionUpdated?.(sessionId);
4475
+ const journalSession = await this.options.journalStore?.deleteSession(sessionId);
4476
+ const legacySession = this.legacyStore.deleteSession(sessionId);
4477
+ if (journalSession && !legacySession) this.options.onSessionUpdated?.(sessionId);
4478
+ return journalSession ?? legacySession;
4479
+ };
4480
+ };
4481
+ function readSummaryActivityAt(summary) {
4482
+ return summary.lastMessageAt ?? summary.createdAt ?? summary.updatedAt;
4483
+ }
4484
+ function isSessionSummaryRefreshEvent(event) {
4485
+ switch (event.type) {
4486
+ case NcpEventType.MessageSent:
4487
+ case NcpEventType.MessageCompleted:
4488
+ case NcpEventType.MessageAbort:
4489
+ case NcpEventType.RunFinished:
4490
+ case NcpEventType.RunError: return true;
4491
+ default: return false;
4492
+ }
4493
+ }
4494
+ const NCP_AGENT_SESSION_JOURNAL_INDEX_FILE = ".ncp-agent-session-index.json";
4495
+ const AUTO_SESSION_LABEL_MAX_LENGTH = 64;
4496
+ function normalizeNcpSessionId(sessionId) {
4497
+ return sessionId.trim();
4498
+ }
4499
+ function safeNcpSessionFilename(value) {
4500
+ return value.replace(/[^a-zA-Z0-9._-]/g, "_");
4501
+ }
4502
+ function normalizeNcpAgentId(agentId) {
4503
+ const normalized = agentId?.trim().toLowerCase();
4504
+ return normalized ? normalized : void 0;
4505
+ }
4506
+ function isRecord$1(value) {
4507
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
4508
+ }
4509
+ function toIsoString(value, fallback) {
4510
+ if (typeof value !== "string") return fallback;
4511
+ const parsed = Date.parse(value);
4512
+ return Number.isFinite(parsed) ? new Date(parsed).toISOString() : fallback;
4513
+ }
4514
+ function createNcpAgentSessionSummary(record) {
4515
+ const metadata = structuredClone(record.metadata ?? {});
4516
+ const label = readOptionalText(metadata.label) ?? resolveAutoSessionLabel(record.messages);
4517
+ if (label) metadata.label = label;
4518
+ const lastMessageAt = readMessageTimestamp(record.messages.at(-1));
4519
+ return {
4520
+ sessionId: record.sessionId,
4521
+ ...normalizeNcpAgentId(record.agentId) ? { agentId: normalizeNcpAgentId(record.agentId) } : {},
4522
+ messageCount: record.messages.length,
4523
+ ...record.createdAt ? { createdAt: record.createdAt } : {},
4524
+ updatedAt: record.updatedAt,
4525
+ ...lastMessageAt ? { lastMessageAt } : {},
4526
+ status: "idle",
4527
+ ...Object.keys(metadata).length > 0 ? { metadata } : {}
4528
+ };
4529
+ }
4530
+ function createNcpAgentSessionJournalMetadataEntry(record) {
4531
+ return {
4532
+ _type: "metadata",
4533
+ version: 1,
4534
+ created_at: record.createdAt ?? record.updatedAt,
4535
+ updated_at: record.updatedAt,
4536
+ ...normalizeNcpAgentId(record.agentId) ? { agent_id: normalizeNcpAgentId(record.agentId) } : {},
4537
+ metadata: structuredClone(record.metadata ?? {})
4538
+ };
4539
+ }
4540
+ function upsertNcpAgentSessionSummaryEvent(params) {
4541
+ const { current, event, session, updatedAt } = params;
4542
+ const metadata = {
4543
+ ...current?.metadata ? structuredClone(current.metadata) : {},
4544
+ ...session.metadata ? structuredClone(session.metadata) : {}
4545
+ };
4546
+ const label = readOptionalText(metadata.label) ?? readSummaryLabelFromEvent(event);
4547
+ if (label) metadata.label = truncateLabel(label);
4548
+ const eventMessage = readMessageFromSummaryEvent(event);
4549
+ const messageCount = current ? current.messageCount + (eventMessage ? 1 : 0) : eventMessage ? 1 : 0;
4550
+ const lastMessageAt = readMessageTimestamp(eventMessage) ?? current?.lastMessageAt;
4551
+ return {
4552
+ sessionId: session.sessionId,
4553
+ ...normalizeNcpAgentId(session.agentId ?? current?.agentId) ? { agentId: normalizeNcpAgentId(session.agentId ?? current?.agentId) } : {},
4554
+ messageCount,
4555
+ createdAt: current?.createdAt ?? session.createdAt ?? updatedAt,
4556
+ updatedAt,
4557
+ ...lastMessageAt ? { lastMessageAt } : {},
4558
+ status: "idle",
4559
+ ...Object.keys(metadata).length > 0 ? { metadata } : {}
4560
+ };
4561
+ }
4562
+ async function replayNcpAgentSessionEvents(events) {
4563
+ const stateManager = new DefaultNcpAgentConversationStateManager();
4564
+ for (const event of events) await stateManager.dispatch(event.type === NcpEventType.MessageCompleted ? createCompletedMessageEvent(event) : structuredClone(event));
4565
+ const snapshot = stateManager.getSnapshot();
4566
+ return [...snapshot.messages.map((message) => structuredClone(message)), ...snapshot.streamingMessage ? [structuredClone(snapshot.streamingMessage)] : []];
4567
+ }
4568
+ function readNcpSessionSummaryActivityAt(summary) {
4569
+ return summary.lastMessageAt ?? summary.createdAt ?? summary.updatedAt;
4570
+ }
4571
+ function readMessageTimestamp(message) {
4572
+ return typeof message?.timestamp === "string" && Number.isFinite(Date.parse(message.timestamp)) ? new Date(message.timestamp).toISOString() : void 0;
4573
+ }
4574
+ function truncateLabel(value) {
4575
+ const chars = Array.from(value);
4576
+ return chars.length <= AUTO_SESSION_LABEL_MAX_LENGTH ? value : `${chars.slice(0, AUTO_SESSION_LABEL_MAX_LENGTH).join("")}...`;
4577
+ }
4578
+ function readOptionalText(value) {
4579
+ if (typeof value !== "string") return null;
4580
+ const trimmed = value.trim();
4581
+ return trimmed ? trimmed : null;
4582
+ }
4583
+ function resolveAutoSessionLabel(messages) {
4584
+ for (const message of messages) {
4585
+ if (message.role !== "user") continue;
4586
+ for (const part of message.parts) if (part.type === "text" || part.type === "rich-text") {
4587
+ const text = readOptionalText(part.text);
4588
+ if (text) return truncateLabel(text);
4589
+ }
4590
+ }
4591
+ return null;
4592
+ }
4593
+ function readMessageFromSummaryEvent(event) {
4594
+ switch (event.type) {
4595
+ case NcpEventType.MessageSent: return event.payload.message;
4596
+ case NcpEventType.MessageCompleted: return event.payload.message;
4597
+ default: return;
4598
+ }
4599
+ }
4600
+ function readSummaryLabelFromEvent(event) {
4601
+ const message = readMessageFromSummaryEvent(event);
4602
+ if (message?.role !== "user") return null;
4603
+ return resolveAutoSessionLabel([message]);
4604
+ }
4605
+ function createCompletedMessageEvent(event) {
4606
+ return {
4607
+ type: NcpEventType.MessageSent,
4608
+ payload: {
4609
+ sessionId: event.payload.sessionId,
4610
+ message: structuredClone(event.payload.message),
4611
+ ...event.payload.correlationId ? { correlationId: event.payload.correlationId } : {},
4612
+ metadata: event.payload.metadata
4613
+ }
4614
+ };
4615
+ }
4616
+ //#endregion
4617
+ //#region src/stores/ncp-agent-session-journal.store.ts
4618
+ var NcpAgentSessionJournalStore = class {
4619
+ sessions = /* @__PURE__ */ new Map();
4620
+ nextSeqBySession = /* @__PURE__ */ new Map();
4621
+ writeChains = /* @__PURE__ */ new Map();
4622
+ summaryIndex = null;
4623
+ constructor(journalDir) {
4624
+ this.journalDir = journalDir;
4625
+ }
4626
+ appendSessionEvent = async (params) => {
4627
+ const sessionId = normalizeNcpSessionId(params.session.sessionId);
4628
+ if (!sessionId) return;
4629
+ const next = (this.writeChains.get(sessionId) ?? Promise.resolve()).then(() => this.appendSessionEventNow({
4630
+ ...params,
4631
+ session: {
4632
+ ...params.session,
4633
+ sessionId
4634
+ }
4635
+ }));
4636
+ this.writeChains.set(sessionId, next.catch(() => void 0));
4637
+ await next;
4638
+ };
4639
+ getSession = async (sessionId) => {
4640
+ const normalizedSessionId = normalizeNcpSessionId(sessionId);
4641
+ if (!normalizedSessionId) return null;
4642
+ const cached = this.sessions.get(normalizedSessionId);
4643
+ if (cached) return structuredClone(cached.record);
4644
+ const loaded = await this.loadSession(normalizedSessionId);
4645
+ if (!loaded) return null;
4646
+ this.sessions.set(normalizedSessionId, loaded);
4647
+ return structuredClone(loaded.record);
4648
+ };
4649
+ getSessionSummary = async (sessionId) => {
4650
+ const normalizedSessionId = normalizeNcpSessionId(sessionId);
4651
+ if (!normalizedSessionId) return null;
4652
+ const indexed = (await this.loadSummaryIndex()).get(normalizedSessionId);
4653
+ if (indexed) return structuredClone(indexed);
4654
+ const record = await this.getSession(normalizedSessionId);
4655
+ return record ? createNcpAgentSessionSummary(record) : null;
4656
+ };
4657
+ listSessionSummaries = async () => {
4658
+ return [...(await this.loadSummaryIndex()).values()].map((summary) => structuredClone(summary)).sort((left, right) => readNcpSessionSummaryActivityAt(right).localeCompare(readNcpSessionSummaryActivityAt(left)));
4659
+ };
4660
+ listSessionMessages = async (sessionId) => {
4661
+ const session = await this.getSession(sessionId);
4662
+ return session ? session.messages.map((message) => structuredClone(message)) : [];
4663
+ };
4664
+ replaceSession = async (record) => {
4665
+ const sessionId = normalizeNcpSessionId(record.sessionId);
4666
+ if (!sessionId) return;
4667
+ await this.ensureJournalDir();
4668
+ const nextRecord = structuredClone({
4669
+ ...record,
4670
+ sessionId
4671
+ });
4672
+ const entries = [createNcpAgentSessionJournalMetadataEntry(nextRecord), ...nextRecord.messages.map((message, index) => ({
4673
+ _type: "event",
4674
+ version: 1,
4675
+ seq: index + 1,
4676
+ timestamp: message.timestamp ?? nextRecord.updatedAt,
4677
+ event: {
4678
+ type: NcpEventType.MessageSent,
4679
+ payload: {
4680
+ sessionId,
4681
+ message: structuredClone(message)
4682
+ }
4683
+ }
4684
+ }))];
4685
+ await writeFile(this.sessionPath(sessionId), `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`, "utf-8");
4686
+ const nextSeq = nextRecord.messages.length + 1;
4687
+ this.sessions.set(sessionId, {
4688
+ record: nextRecord,
4689
+ nextSeq
4690
+ });
4691
+ this.nextSeqBySession.set(sessionId, nextSeq);
4692
+ await this.upsertSummaryIndex(createNcpAgentSessionSummary(nextRecord));
4693
+ };
4694
+ deleteSession = async (sessionId) => {
4695
+ const normalizedSessionId = normalizeNcpSessionId(sessionId);
4696
+ if (!normalizedSessionId) return null;
4697
+ const existing = await this.getSession(normalizedSessionId);
4698
+ this.sessions.delete(normalizedSessionId);
4699
+ this.nextSeqBySession.delete(normalizedSessionId);
4700
+ try {
4701
+ await unlink(this.sessionPath(normalizedSessionId));
4702
+ } catch {}
4703
+ await this.removeSummaryIndex(normalizedSessionId);
4282
4704
  return existing;
4283
4705
  };
4706
+ hasSession = async (sessionId) => {
4707
+ const normalizedSessionId = normalizeNcpSessionId(sessionId);
4708
+ if (!normalizedSessionId) return false;
4709
+ return (await this.loadSummaryIndex()).has(normalizedSessionId);
4710
+ };
4711
+ appendSessionEventNow = async (params) => {
4712
+ const { event, session, updatedAt } = params;
4713
+ const { sessionId } = session;
4714
+ await this.ensureJournalDir();
4715
+ const existing = this.sessions.get(sessionId) ?? (this.nextSeqBySession.has(sessionId) ? null : await this.loadSession(sessionId));
4716
+ const hasJournal = Boolean(existing) || this.nextSeqBySession.has(sessionId);
4717
+ const nextSeq = this.nextSeqBySession.get(sessionId) ?? existing?.nextSeq ?? 1;
4718
+ const path = this.sessionPath(sessionId);
4719
+ if (!hasJournal) await appendFile(path, `${JSON.stringify(createNcpAgentSessionJournalMetadataEntry(session))}\n`, "utf-8");
4720
+ const entry = {
4721
+ _type: "event",
4722
+ version: 1,
4723
+ seq: nextSeq,
4724
+ timestamp: updatedAt,
4725
+ event: structuredClone(event)
4726
+ };
4727
+ await appendFile(path, `${JSON.stringify(entry)}\n`, "utf-8");
4728
+ this.nextSeqBySession.set(sessionId, nextSeq + 1);
4729
+ this.sessions.delete(sessionId);
4730
+ await this.upsertSummaryIndexForEvent({
4731
+ session,
4732
+ event,
4733
+ updatedAt
4734
+ });
4735
+ };
4736
+ loadSession = async (sessionId) => {
4737
+ let raw;
4738
+ try {
4739
+ raw = await readFile(this.sessionPath(sessionId), "utf-8");
4740
+ } catch {
4741
+ return null;
4742
+ }
4743
+ const { agentId, createdAt, events, metadata, nextSeq, updatedAt } = this.parseSessionJournal(raw);
4744
+ const messages = await replayNcpAgentSessionEvents(events);
4745
+ const record = {
4746
+ sessionId,
4747
+ ...agentId ? { agentId } : {},
4748
+ messages,
4749
+ createdAt,
4750
+ updatedAt,
4751
+ metadata
4752
+ };
4753
+ this.nextSeqBySession.set(sessionId, nextSeq);
4754
+ return {
4755
+ record,
4756
+ nextSeq
4757
+ };
4758
+ };
4759
+ parseSessionJournal = (raw) => {
4760
+ let metadata = {};
4761
+ let agentId;
4762
+ let createdAt = (/* @__PURE__ */ new Date()).toISOString();
4763
+ let updatedAt = createdAt;
4764
+ let nextSeq = 1;
4765
+ const events = [];
4766
+ for (const line of raw.split("\n")) {
4767
+ if (!line.trim()) continue;
4768
+ const parsed = JSON.parse(line);
4769
+ if (!isRecord$1(parsed)) continue;
4770
+ if (parsed._type === "metadata") {
4771
+ metadata = isRecord$1(parsed.metadata) ? structuredClone(parsed.metadata) : {};
4772
+ agentId = normalizeNcpAgentId(typeof parsed.agent_id === "string" ? parsed.agent_id : void 0);
4773
+ createdAt = toIsoString(parsed.created_at, createdAt);
4774
+ updatedAt = toIsoString(parsed.updated_at, updatedAt);
4775
+ continue;
4776
+ }
4777
+ if (parsed._type === "event" && isRecord$1(parsed.event)) {
4778
+ const seq = Number(parsed.seq);
4779
+ nextSeq = Math.max(nextSeq, Number.isFinite(seq) ? Math.trunc(seq) + 1 : nextSeq);
4780
+ updatedAt = toIsoString(parsed.timestamp, updatedAt);
4781
+ events.push(structuredClone(parsed.event));
4782
+ }
4783
+ }
4784
+ return {
4785
+ metadata,
4786
+ ...agentId ? { agentId } : {},
4787
+ createdAt,
4788
+ updatedAt,
4789
+ nextSeq,
4790
+ events
4791
+ };
4792
+ };
4793
+ loadSummaryIndex = async () => {
4794
+ if (this.summaryIndex) return this.summaryIndex;
4795
+ try {
4796
+ const parsed = JSON.parse(await readFile(this.indexPath(), "utf-8"));
4797
+ if (parsed.version === 1 && Array.isArray(parsed.records)) {
4798
+ this.summaryIndex = new Map(parsed.records.map((record) => [record.sessionId, structuredClone(record)]));
4799
+ return this.summaryIndex;
4800
+ }
4801
+ } catch {}
4802
+ this.summaryIndex = await this.rebuildSummaryIndex();
4803
+ await this.persistSummaryIndex();
4804
+ return this.summaryIndex;
4805
+ };
4806
+ rebuildSummaryIndex = async () => {
4807
+ const records = /* @__PURE__ */ new Map();
4808
+ let entries = [];
4809
+ try {
4810
+ entries = await readdir(this.journalDir);
4811
+ } catch {
4812
+ return records;
4813
+ }
4814
+ for (const entry of entries) {
4815
+ if (!entry.endsWith(".jsonl")) continue;
4816
+ const sessionId = entry.replace(/\.jsonl$/, "").replace(/_/g, ":");
4817
+ const loaded = await this.loadSession(sessionId);
4818
+ if (!loaded) continue;
4819
+ this.sessions.set(sessionId, loaded);
4820
+ records.set(sessionId, createNcpAgentSessionSummary(loaded.record));
4821
+ }
4822
+ return records;
4823
+ };
4824
+ upsertSummaryIndex = async (summary) => {
4825
+ (await this.loadSummaryIndex()).set(summary.sessionId, structuredClone(summary));
4826
+ await this.persistSummaryIndex();
4827
+ };
4828
+ upsertSummaryIndexForEvent = async (params) => {
4829
+ const { event, session, updatedAt } = params;
4830
+ const index = await this.loadSummaryIndex();
4831
+ const summary = upsertNcpAgentSessionSummaryEvent({
4832
+ current: index.get(session.sessionId),
4833
+ session,
4834
+ event,
4835
+ updatedAt
4836
+ });
4837
+ index.set(summary.sessionId, summary);
4838
+ await this.persistSummaryIndex();
4839
+ };
4840
+ removeSummaryIndex = async (sessionId) => {
4841
+ (await this.loadSummaryIndex()).delete(sessionId);
4842
+ await this.persistSummaryIndex();
4843
+ };
4844
+ persistSummaryIndex = async () => {
4845
+ const records = [...this.summaryIndex?.values() ?? []].map((summary) => structuredClone(summary)).sort((left, right) => readNcpSessionSummaryActivityAt(right).localeCompare(readNcpSessionSummaryActivityAt(left)));
4846
+ await this.ensureJournalDir();
4847
+ await writeFile(this.indexPath(), `${JSON.stringify({
4848
+ version: 1,
4849
+ records
4850
+ })}\n`, "utf-8");
4851
+ };
4852
+ ensureJournalDir = async () => {
4853
+ await mkdir(this.journalDir, { recursive: true });
4854
+ };
4855
+ sessionPath = (sessionId) => {
4856
+ return join(this.journalDir, `${safeNcpSessionFilename(sessionId.replace(/:/g, "_"))}.jsonl`);
4857
+ };
4858
+ indexPath = () => resolve(this.journalDir, NCP_AGENT_SESSION_JOURNAL_INDEX_FILE);
4859
+ };
4860
+ //#endregion
4861
+ //#region src/contributions/session-activity-preview/utils/session-activity-preview-ncp-event.utils.ts
4862
+ const PREVIEW_TEXT_MAX_LENGTH = 160;
4863
+ function readSessionId(value) {
4864
+ if (typeof value !== "string") return null;
4865
+ const trimmed = value.trim();
4866
+ return trimmed.length > 0 ? trimmed : null;
4867
+ }
4868
+ function compactPreviewText(value) {
4869
+ return value.replace(/\s+/g, " ").trim();
4870
+ }
4871
+ function truncatePreviewText(value) {
4872
+ const compacted = compactPreviewText(value);
4873
+ if (compacted.length <= PREVIEW_TEXT_MAX_LENGTH) return compacted;
4874
+ return compacted.slice(0, PREVIEW_TEXT_MAX_LENGTH - 1).trimEnd();
4875
+ }
4876
+ function readMessagePreviewText(message) {
4877
+ const chunks = [];
4878
+ for (const part of message.parts) if ((part.type === "text" || part.type === "rich-text") && part.text.trim()) chunks.push(part.text);
4879
+ const previewText = truncatePreviewText(chunks.join(" "));
4880
+ return previewText.length > 0 ? previewText : null;
4881
+ }
4882
+ function createProjection(sessionId, preview) {
4883
+ if (!sessionId) return null;
4884
+ return {
4885
+ sessionId,
4886
+ preview
4887
+ };
4888
+ }
4889
+ function formatErrorStatus(error) {
4890
+ if (typeof error === "string" && error.trim()) return `运行出错:${truncatePreviewText(error)}`;
4891
+ if (error && typeof error === "object" && "message" in error) {
4892
+ const message = error.message;
4893
+ if (typeof message === "string" && message.trim()) return `运行出错:${truncatePreviewText(message)}`;
4894
+ }
4895
+ return "运行出错";
4896
+ }
4897
+ function createSessionActivityPreviewFromNcpEvent(event, timestamp) {
4898
+ switch (event.type) {
4899
+ case NcpEventType.RunStarted: return createProjection(readSessionId(event.payload.sessionId), {
4900
+ state: "running",
4901
+ statusText: "正在处理...",
4902
+ timestamp
4903
+ });
4904
+ case NcpEventType.RunFinished: return createProjection(readSessionId(event.payload.sessionId), {
4905
+ state: "completed",
4906
+ timestamp
4907
+ });
4908
+ case NcpEventType.RunError: return createProjection(readSessionId(event.payload.sessionId), {
4909
+ state: "failed",
4910
+ statusText: formatErrorStatus(event.payload.error),
4911
+ timestamp
4912
+ });
4913
+ case NcpEventType.MessageSent: {
4914
+ const text = readMessagePreviewText(event.payload.message);
4915
+ if (!text || event.payload.message.role !== "user") return null;
4916
+ return createProjection(readSessionId(event.payload.sessionId), {
4917
+ state: "running",
4918
+ statusText: text,
4919
+ timestamp: event.payload.message.timestamp || timestamp
4920
+ });
4921
+ }
4922
+ case NcpEventType.MessageCompleted: {
4923
+ const text = readMessagePreviewText(event.payload.message);
4924
+ if (!text || event.payload.message.role !== "assistant") return null;
4925
+ return createProjection(readSessionId(event.payload.sessionId), {
4926
+ state: "completed",
4927
+ replyText: text,
4928
+ timestamp: event.payload.message.timestamp || timestamp
4929
+ });
4930
+ }
4931
+ case NcpEventType.MessageFailed: return createProjection(readSessionId(event.payload.sessionId), {
4932
+ state: "failed",
4933
+ statusText: formatErrorStatus(event.payload.error),
4934
+ timestamp
4935
+ });
4936
+ case NcpEventType.MessageToolCallStart: return createProjection(readSessionId(event.payload.sessionId), {
4937
+ state: "running",
4938
+ statusText: `正在调用工具:${event.payload.toolName}`,
4939
+ timestamp
4940
+ });
4941
+ case NcpEventType.MessageToolCallEnd:
4942
+ case NcpEventType.MessageToolCallResult: return createProjection(readSessionId(event.payload.sessionId), {
4943
+ state: "running",
4944
+ statusText: "工具调用完成",
4945
+ timestamp
4946
+ });
4947
+ default: return null;
4948
+ }
4949
+ }
4950
+ //#endregion
4951
+ //#region src/contributions/session-activity-preview/utils/session-activity-preview-metadata.utils.ts
4952
+ const SESSION_ACTIVITY_PREVIEW_METADATA_KEY = "last_activity_preview";
4953
+ const SESSION_ACTIVITY_PREVIEW_STATES = new Set([
4954
+ "running",
4955
+ "completed",
4956
+ "failed",
4957
+ "idle"
4958
+ ]);
4959
+ function isRecord(value) {
4960
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
4961
+ }
4962
+ function readOptionalString(value) {
4963
+ if (typeof value !== "string") return;
4964
+ const trimmed = value.trim();
4965
+ return trimmed.length > 0 ? trimmed : void 0;
4966
+ }
4967
+ function readSessionActivityPreviewMetadata(value) {
4968
+ if (!isRecord(value)) return null;
4969
+ const state = value.state;
4970
+ const timestamp = readOptionalString(value.timestamp);
4971
+ if (!SESSION_ACTIVITY_PREVIEW_STATES.has(state) || !timestamp) return null;
4972
+ return {
4973
+ state,
4974
+ timestamp,
4975
+ ...readOptionalString(value.statusText) ? { statusText: readOptionalString(value.statusText) } : {},
4976
+ ...readOptionalString(value.replyText) ? { replyText: readOptionalString(value.replyText) } : {}
4977
+ };
4978
+ }
4979
+ function compareIsoTimestamp(left, right) {
4980
+ const leftTime = Date.parse(left);
4981
+ const rightTime = Date.parse(right);
4982
+ if (!Number.isFinite(leftTime) || !Number.isFinite(rightTime)) return left.localeCompare(right);
4983
+ return leftTime - rightTime;
4984
+ }
4985
+ function mergeSessionActivityPreview(current, incoming) {
4986
+ if (current && compareIsoTimestamp(incoming.timestamp, current.timestamp) < 0) return current;
4987
+ return {
4988
+ state: incoming.state,
4989
+ timestamp: incoming.timestamp,
4990
+ ...incoming.statusText ?? (incoming.state === "completed" ? current?.statusText : void 0) ? { statusText: incoming.statusText ?? current?.statusText } : {},
4991
+ ...incoming.replyText ?? current?.replyText ? { replyText: incoming.replyText ?? current?.replyText } : {}
4992
+ };
4993
+ }
4994
+ function areSessionActivityPreviewsEqual(left, right) {
4995
+ return Boolean(left) && left?.state === right.state && left.timestamp === right.timestamp && left.statusText === right.statusText && left.replyText === right.replyText;
4996
+ }
4997
+ function writeSessionActivityPreviewMetadata(metadata, projection) {
4998
+ const current = readSessionActivityPreviewMetadata(metadata?.[SESSION_ACTIVITY_PREVIEW_METADATA_KEY]);
4999
+ const next = mergeSessionActivityPreview(current, projection.preview);
5000
+ if (areSessionActivityPreviewsEqual(current, next)) return null;
5001
+ return {
5002
+ ...metadata ?? {},
5003
+ [SESSION_ACTIVITY_PREVIEW_METADATA_KEY]: next
5004
+ };
5005
+ }
5006
+ //#endregion
5007
+ //#region src/contributions/session-activity-preview/index.ts
5008
+ function formatBackgroundError(error) {
5009
+ if (error instanceof Error) return error.stack ?? error.message;
5010
+ return String(error);
5011
+ }
5012
+ function readMetadata(value) {
5013
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
5014
+ return value;
5015
+ }
5016
+ var SessionActivityPreviewContribution = class {
5017
+ unsubscribeNcpEvent = null;
5018
+ stopped = true;
5019
+ constructor(kernel) {
5020
+ this.kernel = kernel;
5021
+ }
5022
+ start = () => {
5023
+ if (this.unsubscribeNcpEvent) return;
5024
+ this.stopped = false;
5025
+ this.unsubscribeNcpEvent = this.kernel.eventBus.on(eventKeys.ncpEvent, (event) => {
5026
+ const projection = createSessionActivityPreviewFromNcpEvent(event, (/* @__PURE__ */ new Date()).toISOString());
5027
+ if (!projection) return;
5028
+ this.updatePreview(projection);
5029
+ });
5030
+ };
5031
+ dispose = () => {
5032
+ this.unsubscribeNcpEvent?.();
5033
+ this.unsubscribeNcpEvent = null;
5034
+ this.stopped = true;
5035
+ };
5036
+ updatePreview = (projection) => {
5037
+ this.persistPreview(projection).catch((error) => {
5038
+ console.error(`[session-activity-preview] failed to update ${projection.sessionId}: ${formatBackgroundError(error)}`);
5039
+ });
5040
+ };
5041
+ persistPreview = async (projection) => {
5042
+ if (this.stopped) return;
5043
+ const summary = await this.kernel.ncpSessionApi.getSession(projection.sessionId);
5044
+ if (!summary || this.stopped) return;
5045
+ const nextMetadata = writeSessionActivityPreviewMetadata(readMetadata(summary.metadata), projection);
5046
+ if (!nextMetadata) return;
5047
+ await this.kernel.ncpSessionApi.updateSession(projection.sessionId, { metadata: nextMetadata });
5048
+ };
4284
5049
  };
4285
5050
  //#endregion
4286
5051
  //#region src/app/nextclaw-kernel.ts
@@ -4326,7 +5091,8 @@ var NextclawKernel = class {
4326
5091
  learningLoop;
4327
5092
  sessionLifecycleEvents;
4328
5093
  ncpAgentSessionStore;
4329
- startPromise = null;
5094
+ ncpAgentSessionJournalStore;
5095
+ contributions;
4330
5096
  constructor(options = {}) {
4331
5097
  const sessionsDir = resolveKernelSessionsDir(options);
4332
5098
  this.sessions = new SessionManager({ sessionsDir });
@@ -4336,7 +5102,11 @@ var NextclawKernel = class {
4336
5102
  sessionsDir,
4337
5103
  onSessionUpdated: this.publishSessionUpdated
4338
5104
  });
4339
- this.ncpAgentSessionStore = new NcpAgentSessionStoreAdapter(this.sessions, { onSessionUpdated: this.sessionSearch.handleSessionUpdated });
5105
+ this.ncpAgentSessionJournalStore = new NcpAgentSessionJournalStore(resolve(sessionsDir, ".ncp-agent-journal"));
5106
+ this.ncpAgentSessionStore = new NcpAgentSessionStoreAdapter(this.sessions, {
5107
+ journalStore: this.ncpAgentSessionJournalStore,
5108
+ onSessionUpdated: this.sessionSearch.handleSessionUpdated
5109
+ });
4340
5110
  this.sessionRequests = new SessionRequestManager({
4341
5111
  sessions: this.sessions,
4342
5112
  dispatcher: createAgentRuntimeSessionRequestDispatcher({
@@ -4360,11 +5130,6 @@ var NextclawKernel = class {
4360
5130
  channels: this.channels,
4361
5131
  providerManager: this.llmProviders
4362
5132
  });
4363
- this.ncpSessionApi = new NcpSessionApiService({
4364
- eventBus: this.eventBus,
4365
- getConfig: this.configManager.loadConfig,
4366
- sessionManager: this.sessions
4367
- });
4368
5133
  this.agentRuntimeManager = new AgentRuntimeManager({
4369
5134
  bus: this.messageBus,
4370
5135
  providerManager: this.llmProviders,
@@ -4381,30 +5146,34 @@ var NextclawKernel = class {
4381
5146
  llmUsage: this.llmUsage,
4382
5147
  onSessionUpdated: this.publishSessionUpdated
4383
5148
  });
5149
+ this.ncpSessionApi = new NcpSessionApiService({
5150
+ eventBus: this.eventBus,
5151
+ getConfig: this.configManager.loadConfig,
5152
+ isLiveSessionRunning: this.agentRuntimeManager.isLiveSessionRunning,
5153
+ ncpAgentSessionStore: this.ncpAgentSessionStore,
5154
+ sessionManager: this.sessions
5155
+ });
4384
5156
  this.learningLoop = new LearningLoopManager({
4385
5157
  eventBus: this.eventBus,
4386
5158
  sessionManager: this.sessions,
4387
5159
  sessionRequester: this.sessionRequests,
4388
5160
  resolveLearningLoopConfig: () => readLearningLoopRuntimeConfig(this.configManager.loadConfig())
4389
5161
  });
5162
+ this.contributions = [new SessionActivityPreviewContribution(this)];
4390
5163
  }
4391
5164
  start = async () => {
4392
- this.startPromise ??= Promise.resolve().then(() => {
4393
- this.ncpSessionApi.start();
4394
- }).then(() => this.sessionSearch.start()).then(() => this.agentRuntimeManager.bootstrap()).then(() => {
4395
- this.learningLoop.start();
4396
- }).catch((error) => {
4397
- this.startPromise = null;
4398
- throw error;
4399
- });
4400
- return this.startPromise;
5165
+ this.ncpSessionApi.start();
5166
+ this.sessionSearch.start();
5167
+ for (const contribution of this.contributions) contribution.start();
5168
+ this.agentRuntimeManager.bootstrap();
5169
+ this.learningLoop.start();
4401
5170
  };
4402
5171
  dispose = async () => {
4403
5172
  this.learningLoop.dispose();
5173
+ for (const contribution of this.contributions) contribution.dispose();
4404
5174
  this.ncpSessionApi.dispose();
4405
5175
  await this.agentRuntimeManager.dispose();
4406
5176
  await this.sessionSearch.dispose();
4407
- this.startPromise = null;
4408
5177
  };
4409
5178
  publishSessionUpdated = (sessionKey) => {
4410
5179
  this.sessionLifecycleEvents.publishSessionUpdated(sessionKey);