nextclaw 0.9.2 → 0.9.4

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 (25) hide show
  1. package/dist/cli/index.js +674 -251
  2. package/package.json +2 -2
  3. package/ui-dist/assets/{ChannelsList-CkCpHSto.js → ChannelsList-Bga6n85j.js} +1 -1
  4. package/ui-dist/assets/ChatPage-B-Yk3kkv.js +32 -0
  5. package/ui-dist/assets/{DocBrowser-B5Aqiz6W.js → DocBrowser-dv57PRp5.js} +1 -1
  6. package/ui-dist/assets/{MarketplacePage-BIi0bBdW.js → MarketplacePage-j6p73Hjo.js} +1 -1
  7. package/ui-dist/assets/{ModelConfig-BTFiEAxQ.js → ModelConfig-BiKSDp5h.js} +1 -1
  8. package/ui-dist/assets/{ProvidersList-cdk1d-G_.js → ProvidersList-B7ZfRUkD.js} +1 -1
  9. package/ui-dist/assets/{RuntimeConfig-CFqFsXmR.js → RuntimeConfig-Bpt9UNb6.js} +1 -1
  10. package/ui-dist/assets/{SecretsConfig-CIKasCek.js → SecretsConfig-Ds00G-_O.js} +2 -2
  11. package/ui-dist/assets/{SessionsConfig-mnCLFtbo.js → SessionsConfig-Mjet4opU.js} +1 -1
  12. package/ui-dist/assets/{card-C1BUfR85.js → card-C7JJ5BGA.js} +1 -1
  13. package/ui-dist/assets/index-BiJ2xs5X.css +1 -0
  14. package/ui-dist/assets/{index-Dxas8MJ9.js → index-Cb9xiqC5.js} +2 -2
  15. package/ui-dist/assets/{label-CwWfYbuj.js → label-DHJKdaUl.js} +1 -1
  16. package/ui-dist/assets/{logos-DDyjHSEU.js → logos-fPO_amyL.js} +1 -1
  17. package/ui-dist/assets/{page-layout-DKTRKcHL.js → page-layout-CF0JQsWW.js} +1 -1
  18. package/ui-dist/assets/{switch-Bi3yeYiC.js → switch-C1hgy-fE.js} +1 -1
  19. package/ui-dist/assets/{tabs-custom-HZFNZrc0.js → tabs-custom-OyoLf5ZM.js} +1 -1
  20. package/ui-dist/assets/useConfig-D_G46zbo.js +6 -0
  21. package/ui-dist/assets/{useConfirmDialog-DwD21HlD.js → useConfirmDialog-_0u6i3cI.js} +1 -1
  22. package/ui-dist/index.html +2 -2
  23. package/ui-dist/assets/ChatPage-DM4XNsrW.js +0 -32
  24. package/ui-dist/assets/index-P4YzN9iS.css +0 -1
  25. package/ui-dist/assets/useConfig-CgzVQTZl.js +0 -6
package/dist/cli/index.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  loadConfig as loadConfig7,
10
10
  saveConfig as saveConfig6,
11
11
  getConfigPath as getConfigPath4,
12
- getDataDir as getDataDir7,
12
+ getDataDir as getDataDir8,
13
13
  ConfigSchema as ConfigSchema2,
14
14
  getWorkspacePath as getWorkspacePath6,
15
15
  expandHome as expandHome2,
@@ -26,8 +26,8 @@ import {
26
26
  resolvePluginChannelMessageToolHints as resolvePluginChannelMessageToolHints2,
27
27
  setPluginRuntimeBridge as setPluginRuntimeBridge2
28
28
  } from "@nextclaw/openclaw-compat";
29
- import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
30
- import { join as join6, resolve as resolve9 } from "path";
29
+ import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
30
+ import { join as join7, resolve as resolve9 } from "path";
31
31
  import { createInterface as createInterface2 } from "readline";
32
32
  import { fileURLToPath as fileURLToPath4 } from "url";
33
33
  import { spawn as spawn3 } from "child_process";
@@ -2128,6 +2128,11 @@ var DiagnosticsCommands = class {
2128
2128
  issues.push(`Managed service health check failed: ${managedHealth.detail}`);
2129
2129
  recommendations.push(`Check logs at ${serviceState?.logPath ?? resolveServiceLogPath()}.`);
2130
2130
  }
2131
+ if (running && serviceState?.startupState === "degraded" && managedHealth.state !== "ok") {
2132
+ const startupHint = serviceState.startupLastProbeError ? ` (${serviceState.startupLastProbeError})` : "";
2133
+ issues.push(`Service is in degraded startup state${startupHint}.`);
2134
+ recommendations.push(`Wait and re-check ${APP_NAME} status; if it does not recover, inspect logs and restart.`);
2135
+ }
2131
2136
  if (!running) {
2132
2137
  recommendations.push(`Run ${APP_NAME} start to launch the service.`);
2133
2138
  }
@@ -2244,8 +2249,8 @@ import {
2244
2249
  stopPluginChannelGateways
2245
2250
  } from "@nextclaw/openclaw-compat";
2246
2251
  import { startUiServer } from "@nextclaw/server";
2247
- import { closeSync, cpSync, existsSync as existsSync7, mkdirSync as mkdirSync3, openSync, rmSync as rmSync3 } from "fs";
2248
- import { dirname, isAbsolute as isAbsolute2, join as join4, relative, resolve as resolve7 } from "path";
2252
+ import { appendFileSync, closeSync, cpSync, existsSync as existsSync8, mkdirSync as mkdirSync4, openSync, rmSync as rmSync3 } from "fs";
2253
+ import { dirname, isAbsolute as isAbsolute2, join as join5, relative, resolve as resolve7 } from "path";
2249
2254
  import { spawn as spawn2 } from "child_process";
2250
2255
  import { request as httpRequest } from "http";
2251
2256
  import { request as httpsRequest } from "https";
@@ -2980,6 +2985,497 @@ var GatewayAgentRuntimePool = class {
2980
2985
  }
2981
2986
  };
2982
2987
 
2988
+ // src/cli/commands/ui-chat-run-coordinator.ts
2989
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
2990
+ import { join as join4 } from "path";
2991
+ import {
2992
+ getDataDir as getDataDir5,
2993
+ parseAgentScopedSessionKey as parseAgentScopedSessionKey2,
2994
+ safeFilename
2995
+ } from "@nextclaw/core";
2996
+ var RUNS_DIR = join4(getDataDir5(), "runs");
2997
+ var NON_TERMINAL_STATES = /* @__PURE__ */ new Set(["queued", "running"]);
2998
+ function createRunId() {
2999
+ const now = Date.now().toString(36);
3000
+ const rand = Math.random().toString(36).slice(2, 10);
3001
+ return `run-${now}-${rand}`;
3002
+ }
3003
+ function isRecord(value) {
3004
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
3005
+ }
3006
+ function readOptionalString(value) {
3007
+ if (typeof value !== "string") {
3008
+ return void 0;
3009
+ }
3010
+ const trimmed = value.trim();
3011
+ return trimmed || void 0;
3012
+ }
3013
+ function isAbortError(error) {
3014
+ if (error instanceof DOMException && error.name === "AbortError") {
3015
+ return true;
3016
+ }
3017
+ if (error instanceof Error) {
3018
+ if (error.name === "AbortError") {
3019
+ return true;
3020
+ }
3021
+ const message = error.message.toLowerCase();
3022
+ if (message.includes("aborted") || message.includes("abort")) {
3023
+ return true;
3024
+ }
3025
+ }
3026
+ return false;
3027
+ }
3028
+ function cloneEvent(event) {
3029
+ return JSON.parse(JSON.stringify(event));
3030
+ }
3031
+ var UiChatRunCoordinator = class {
3032
+ constructor(options) {
3033
+ this.options = options;
3034
+ mkdirSync3(RUNS_DIR, { recursive: true });
3035
+ this.loadPersistedRuns();
3036
+ }
3037
+ runs = /* @__PURE__ */ new Map();
3038
+ sessionRuns = /* @__PURE__ */ new Map();
3039
+ startRun(input) {
3040
+ const request = this.resolveRequest(input);
3041
+ const stopCapability = this.options.runtimePool.supportsTurnAbort({
3042
+ sessionKey: request.sessionKey,
3043
+ agentId: request.agentId,
3044
+ channel: request.channel,
3045
+ chatId: request.chatId,
3046
+ metadata: request.metadata
3047
+ });
3048
+ const run = {
3049
+ runId: request.runId,
3050
+ sessionKey: request.sessionKey,
3051
+ ...request.agentId ? { agentId: request.agentId } : {},
3052
+ ...request.model ? { model: request.model, requestedModel: request.model } : {},
3053
+ state: "queued",
3054
+ requestedAt: (/* @__PURE__ */ new Date()).toISOString(),
3055
+ stopSupported: stopCapability.supported,
3056
+ ...stopCapability.reason ? { stopReason: stopCapability.reason } : {},
3057
+ events: [],
3058
+ waiters: /* @__PURE__ */ new Set(),
3059
+ abortController: null,
3060
+ cancelRequested: false
3061
+ };
3062
+ this.runs.set(run.runId, run);
3063
+ this.bindRunToSession(run.sessionKey, run.runId);
3064
+ this.syncSessionRunState(run);
3065
+ this.persistRun(run);
3066
+ this.emitRunUpdated(run);
3067
+ void this.executeRun(run, request);
3068
+ return this.toRunView(run);
3069
+ }
3070
+ getRun(params) {
3071
+ const run = this.getRunRecord(params.runId);
3072
+ return run ? this.toRunView(run) : null;
3073
+ }
3074
+ listRuns(params = {}) {
3075
+ const sessionKey = readOptionalString(params.sessionKey);
3076
+ const stateFilter = Array.isArray(params.states) && params.states.length > 0 ? new Set(params.states) : null;
3077
+ const limit = Number.isFinite(params.limit) ? Math.max(0, Math.trunc(params.limit)) : 0;
3078
+ const records = Array.from(this.runs.values()).filter((run) => {
3079
+ if (sessionKey && run.sessionKey !== sessionKey) {
3080
+ return false;
3081
+ }
3082
+ if (stateFilter && !stateFilter.has(run.state)) {
3083
+ return false;
3084
+ }
3085
+ return true;
3086
+ }).sort((left, right) => Date.parse(right.requestedAt) - Date.parse(left.requestedAt));
3087
+ const total = records.length;
3088
+ const runs = (limit > 0 ? records.slice(0, limit) : records).map((run) => this.toRunView(run));
3089
+ return { runs, total };
3090
+ }
3091
+ async *streamRun(params) {
3092
+ const run = this.getRunRecord(params.runId);
3093
+ if (!run) {
3094
+ throw new Error(`chat run not found: ${params.runId}`);
3095
+ }
3096
+ let cursor = Number.isFinite(params.fromEventIndex) ? Math.max(0, Math.trunc(params.fromEventIndex)) : 0;
3097
+ while (true) {
3098
+ while (cursor < run.events.length) {
3099
+ yield cloneEvent(run.events[cursor]);
3100
+ cursor += 1;
3101
+ }
3102
+ if (!NON_TERMINAL_STATES.has(run.state)) {
3103
+ return;
3104
+ }
3105
+ if (params.signal?.aborted) {
3106
+ return;
3107
+ }
3108
+ await this.waitForRunUpdate(run, params.signal);
3109
+ }
3110
+ }
3111
+ async stopRun(params) {
3112
+ const runId = readOptionalString(params.runId) ?? "";
3113
+ if (!runId) {
3114
+ return {
3115
+ stopped: false,
3116
+ runId: "",
3117
+ reason: "runId is required"
3118
+ };
3119
+ }
3120
+ const run = this.getRunRecord(runId);
3121
+ if (!run) {
3122
+ return {
3123
+ stopped: false,
3124
+ runId,
3125
+ ...readOptionalString(params.sessionKey) ? { sessionKey: readOptionalString(params.sessionKey) } : {},
3126
+ reason: "run not found or already completed"
3127
+ };
3128
+ }
3129
+ const requestedSessionKey = readOptionalString(params.sessionKey);
3130
+ if (requestedSessionKey && requestedSessionKey !== run.sessionKey) {
3131
+ return {
3132
+ stopped: false,
3133
+ runId,
3134
+ sessionKey: run.sessionKey,
3135
+ reason: "session key mismatch"
3136
+ };
3137
+ }
3138
+ if (!run.stopSupported) {
3139
+ return {
3140
+ stopped: false,
3141
+ runId,
3142
+ sessionKey: run.sessionKey,
3143
+ reason: run.stopReason ?? "run stop is not supported"
3144
+ };
3145
+ }
3146
+ if (!NON_TERMINAL_STATES.has(run.state)) {
3147
+ return {
3148
+ stopped: false,
3149
+ runId,
3150
+ sessionKey: run.sessionKey,
3151
+ reason: `run already ${run.state}`
3152
+ };
3153
+ }
3154
+ run.cancelRequested = true;
3155
+ if (run.abortController) {
3156
+ run.abortController.abort(new Error("chat turn stopped by user"));
3157
+ }
3158
+ return {
3159
+ stopped: true,
3160
+ runId,
3161
+ sessionKey: run.sessionKey
3162
+ };
3163
+ }
3164
+ resolveRequest(input) {
3165
+ const message = readOptionalString(input.message) ?? "";
3166
+ const sessionKey = readOptionalString(input.sessionKey) ?? `ui:${Date.now().toString(36)}:${Math.random().toString(36).slice(2, 8)}`;
3167
+ const explicitAgentId = readOptionalString(input.agentId);
3168
+ const parsedAgentId = parseAgentScopedSessionKey2(sessionKey)?.agentId;
3169
+ const agentId = explicitAgentId ?? readOptionalString(parsedAgentId);
3170
+ const model = readOptionalString(input.model);
3171
+ const metadata = isRecord(input.metadata) ? { ...input.metadata } : {};
3172
+ if (model) {
3173
+ metadata.model = model;
3174
+ }
3175
+ const runId = readOptionalString(input.runId) ?? createRunId();
3176
+ return {
3177
+ runId,
3178
+ message,
3179
+ sessionKey,
3180
+ ...agentId ? { agentId } : {},
3181
+ ...model ? { model } : {},
3182
+ metadata,
3183
+ channel: readOptionalString(input.channel) ?? "ui",
3184
+ chatId: readOptionalString(input.chatId) ?? "web-ui"
3185
+ };
3186
+ }
3187
+ async executeRun(run, request) {
3188
+ if (run.cancelRequested) {
3189
+ this.transitionState(run, "aborted");
3190
+ return;
3191
+ }
3192
+ this.transitionState(run, "running");
3193
+ const abortController = run.stopSupported ? new AbortController() : null;
3194
+ run.abortController = abortController;
3195
+ const assistantDeltaParts = [];
3196
+ try {
3197
+ const reply = await this.options.runtimePool.processDirect({
3198
+ content: request.message,
3199
+ sessionKey: request.sessionKey,
3200
+ channel: request.channel,
3201
+ chatId: request.chatId,
3202
+ agentId: request.agentId,
3203
+ metadata: request.metadata,
3204
+ ...abortController ? { abortSignal: abortController.signal } : {},
3205
+ onAssistantDelta: (delta) => {
3206
+ if (typeof delta !== "string" || delta.length === 0) {
3207
+ return;
3208
+ }
3209
+ assistantDeltaParts.push(delta);
3210
+ this.pushStreamEvent(run, { type: "delta", delta });
3211
+ },
3212
+ onSessionEvent: (event) => {
3213
+ this.pushStreamEvent(run, {
3214
+ type: "session_event",
3215
+ event: this.mapSessionEvent(event)
3216
+ });
3217
+ }
3218
+ });
3219
+ this.pushStreamEvent(run, {
3220
+ type: "final",
3221
+ result: {
3222
+ reply,
3223
+ sessionKey: request.sessionKey,
3224
+ ...request.agentId ? { agentId: request.agentId } : {},
3225
+ ...request.model ? { model: request.model } : {}
3226
+ }
3227
+ });
3228
+ run.reply = reply;
3229
+ this.transitionState(run, "completed");
3230
+ } catch (error) {
3231
+ const aborted = (abortController?.signal.aborted ?? false) || isAbortError(error);
3232
+ if (aborted) {
3233
+ const partialReply = assistantDeltaParts.join("");
3234
+ if (partialReply.trim().length > 0) {
3235
+ this.persistAbortedAssistantReply(run.sessionKey, partialReply);
3236
+ }
3237
+ this.pushStreamEvent(run, {
3238
+ type: "final",
3239
+ result: {
3240
+ reply: partialReply,
3241
+ sessionKey: request.sessionKey,
3242
+ ...request.agentId ? { agentId: request.agentId } : {},
3243
+ ...request.model ? { model: request.model } : {}
3244
+ }
3245
+ });
3246
+ run.reply = partialReply;
3247
+ this.transitionState(run, "aborted", {
3248
+ error: abortController?.signal.reason instanceof Error ? abortController.signal.reason.message : readOptionalString(abortController?.signal.reason)
3249
+ });
3250
+ return;
3251
+ }
3252
+ const errorMessage = error instanceof Error ? error.message : String(error);
3253
+ this.pushStreamEvent(run, {
3254
+ type: "error",
3255
+ error: errorMessage
3256
+ });
3257
+ this.transitionState(run, "failed", { error: errorMessage });
3258
+ } finally {
3259
+ run.abortController = null;
3260
+ this.persistRun(run);
3261
+ this.notifyRunWaiters(run);
3262
+ }
3263
+ }
3264
+ transitionState(run, next, options = {}) {
3265
+ run.state = next;
3266
+ if (next === "running") {
3267
+ run.startedAt = (/* @__PURE__ */ new Date()).toISOString();
3268
+ }
3269
+ if (!NON_TERMINAL_STATES.has(next)) {
3270
+ run.completedAt = (/* @__PURE__ */ new Date()).toISOString();
3271
+ }
3272
+ if (options.error) {
3273
+ run.error = options.error;
3274
+ } else if (next === "completed") {
3275
+ run.error = void 0;
3276
+ }
3277
+ this.syncSessionRunState(run);
3278
+ this.persistRun(run);
3279
+ this.emitRunUpdated(run);
3280
+ this.notifyRunWaiters(run);
3281
+ }
3282
+ pushStreamEvent(run, event) {
3283
+ run.events.push(event);
3284
+ this.persistRun(run);
3285
+ this.notifyRunWaiters(run);
3286
+ this.emitRunUpdated(run);
3287
+ }
3288
+ waitForRunUpdate(run, signal) {
3289
+ return new Promise((resolve10) => {
3290
+ const wake = () => {
3291
+ cleanup();
3292
+ resolve10();
3293
+ };
3294
+ const onAbort = () => {
3295
+ cleanup();
3296
+ resolve10();
3297
+ };
3298
+ const cleanup = () => {
3299
+ run.waiters.delete(wake);
3300
+ signal?.removeEventListener("abort", onAbort);
3301
+ };
3302
+ run.waiters.add(wake);
3303
+ signal?.addEventListener("abort", onAbort, { once: true });
3304
+ });
3305
+ }
3306
+ notifyRunWaiters(run) {
3307
+ if (run.waiters.size === 0) {
3308
+ return;
3309
+ }
3310
+ const waiters = Array.from(run.waiters);
3311
+ run.waiters.clear();
3312
+ for (const wake of waiters) {
3313
+ wake();
3314
+ }
3315
+ }
3316
+ bindRunToSession(sessionKey, runId) {
3317
+ const existing = this.sessionRuns.get(sessionKey);
3318
+ if (existing) {
3319
+ existing.add(runId);
3320
+ return;
3321
+ }
3322
+ this.sessionRuns.set(sessionKey, /* @__PURE__ */ new Set([runId]));
3323
+ }
3324
+ syncSessionRunState(run) {
3325
+ const session = this.options.sessionManager.getOrCreate(run.sessionKey);
3326
+ const metadata = session.metadata;
3327
+ metadata.ui_last_run_id = run.runId;
3328
+ metadata.ui_last_run_state = run.state;
3329
+ metadata.ui_last_run_updated_at = (/* @__PURE__ */ new Date()).toISOString();
3330
+ if (NON_TERMINAL_STATES.has(run.state)) {
3331
+ metadata.ui_active_run_id = run.runId;
3332
+ metadata.ui_run_state = run.state;
3333
+ metadata.ui_run_requested_at = run.requestedAt;
3334
+ if (run.startedAt) {
3335
+ metadata.ui_run_started_at = run.startedAt;
3336
+ }
3337
+ } else if (metadata.ui_active_run_id === run.runId) {
3338
+ delete metadata.ui_active_run_id;
3339
+ delete metadata.ui_run_state;
3340
+ delete metadata.ui_run_requested_at;
3341
+ delete metadata.ui_run_started_at;
3342
+ }
3343
+ session.updatedAt = /* @__PURE__ */ new Date();
3344
+ this.options.sessionManager.save(session);
3345
+ }
3346
+ persistAbortedAssistantReply(sessionKey, partialReply) {
3347
+ const session = this.options.sessionManager.getOrCreate(sessionKey);
3348
+ const latest = session.messages.length > 0 ? session.messages[session.messages.length - 1] : null;
3349
+ if (latest?.role === "assistant" && typeof latest.content === "string" && latest.content === partialReply) {
3350
+ return;
3351
+ }
3352
+ this.options.sessionManager.addMessage(session, "assistant", partialReply);
3353
+ this.options.sessionManager.save(session);
3354
+ }
3355
+ mapSessionEvent(event) {
3356
+ const raw = event.data?.message;
3357
+ const messageRecord = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : null;
3358
+ const message = messageRecord && typeof messageRecord.role === "string" ? {
3359
+ role: messageRecord.role,
3360
+ content: messageRecord.content,
3361
+ timestamp: typeof messageRecord.timestamp === "string" ? messageRecord.timestamp : event.timestamp,
3362
+ ...typeof messageRecord.name === "string" ? { name: messageRecord.name } : {},
3363
+ ...typeof messageRecord.tool_call_id === "string" ? { tool_call_id: messageRecord.tool_call_id } : {},
3364
+ ...Array.isArray(messageRecord.tool_calls) ? { tool_calls: messageRecord.tool_calls } : {},
3365
+ ...typeof messageRecord.reasoning_content === "string" ? { reasoning_content: messageRecord.reasoning_content } : {}
3366
+ } : void 0;
3367
+ return {
3368
+ seq: event.seq,
3369
+ type: event.type,
3370
+ timestamp: event.timestamp,
3371
+ ...message ? { message } : {}
3372
+ };
3373
+ }
3374
+ emitRunUpdated(run) {
3375
+ this.options.onRunUpdated?.(this.toRunView(run));
3376
+ }
3377
+ toRunView(run) {
3378
+ return {
3379
+ runId: run.runId,
3380
+ sessionKey: run.sessionKey,
3381
+ ...run.agentId ? { agentId: run.agentId } : {},
3382
+ ...run.model ? { model: run.model } : {},
3383
+ state: run.state,
3384
+ requestedAt: run.requestedAt,
3385
+ ...run.startedAt ? { startedAt: run.startedAt } : {},
3386
+ ...run.completedAt ? { completedAt: run.completedAt } : {},
3387
+ stopSupported: run.stopSupported,
3388
+ ...run.stopReason ? { stopReason: run.stopReason } : {},
3389
+ ...run.error ? { error: run.error } : {},
3390
+ ...typeof run.reply === "string" ? { reply: run.reply } : {},
3391
+ eventCount: run.events.length
3392
+ };
3393
+ }
3394
+ getRunPath(runId) {
3395
+ return join4(RUNS_DIR, `${safeFilename(runId)}.json`);
3396
+ }
3397
+ persistRun(run) {
3398
+ const persisted = {
3399
+ runId: run.runId,
3400
+ sessionKey: run.sessionKey,
3401
+ ...run.agentId ? { agentId: run.agentId } : {},
3402
+ ...run.model ? { model: run.model } : {},
3403
+ state: run.state,
3404
+ requestedAt: run.requestedAt,
3405
+ ...run.startedAt ? { startedAt: run.startedAt } : {},
3406
+ ...run.completedAt ? { completedAt: run.completedAt } : {},
3407
+ stopSupported: run.stopSupported,
3408
+ ...run.stopReason ? { stopReason: run.stopReason } : {},
3409
+ ...run.error ? { error: run.error } : {},
3410
+ ...typeof run.reply === "string" ? { reply: run.reply } : {},
3411
+ events: run.events
3412
+ };
3413
+ writeFileSync3(this.getRunPath(run.runId), `${JSON.stringify(persisted, null, 2)}
3414
+ `);
3415
+ }
3416
+ loadPersistedRuns() {
3417
+ if (!existsSync7(RUNS_DIR)) {
3418
+ return;
3419
+ }
3420
+ for (const entry of readdirSync(RUNS_DIR, { withFileTypes: true })) {
3421
+ if (!entry.isFile() || !entry.name.endsWith(".json")) {
3422
+ continue;
3423
+ }
3424
+ const path = join4(RUNS_DIR, entry.name);
3425
+ try {
3426
+ const parsed = JSON.parse(readFileSync6(path, "utf-8"));
3427
+ const runId = readOptionalString(parsed.runId);
3428
+ const sessionKey = readOptionalString(parsed.sessionKey);
3429
+ if (!runId || !sessionKey) {
3430
+ continue;
3431
+ }
3432
+ const state = this.normalizeRunState(parsed.state);
3433
+ const events = Array.isArray(parsed.events) ? parsed.events : [];
3434
+ const run = {
3435
+ runId,
3436
+ sessionKey,
3437
+ ...readOptionalString(parsed.agentId) ? { agentId: readOptionalString(parsed.agentId) } : {},
3438
+ ...readOptionalString(parsed.model) ? { model: readOptionalString(parsed.model) } : {},
3439
+ state,
3440
+ requestedAt: readOptionalString(parsed.requestedAt) ?? (/* @__PURE__ */ new Date()).toISOString(),
3441
+ ...readOptionalString(parsed.startedAt) ? { startedAt: readOptionalString(parsed.startedAt) } : {},
3442
+ ...readOptionalString(parsed.completedAt) ? { completedAt: readOptionalString(parsed.completedAt) } : {},
3443
+ stopSupported: Boolean(parsed.stopSupported),
3444
+ ...readOptionalString(parsed.stopReason) ? { stopReason: readOptionalString(parsed.stopReason) } : {},
3445
+ ...readOptionalString(parsed.error) ? { error: readOptionalString(parsed.error) } : {},
3446
+ ...typeof parsed.reply === "string" ? { reply: parsed.reply } : {},
3447
+ events,
3448
+ waiters: /* @__PURE__ */ new Set(),
3449
+ abortController: null,
3450
+ cancelRequested: false
3451
+ };
3452
+ if (NON_TERMINAL_STATES.has(run.state)) {
3453
+ run.state = "failed";
3454
+ run.error = run.error ?? "run interrupted by service restart";
3455
+ run.completedAt = (/* @__PURE__ */ new Date()).toISOString();
3456
+ this.persistRun(run);
3457
+ }
3458
+ this.runs.set(run.runId, run);
3459
+ this.bindRunToSession(run.sessionKey, run.runId);
3460
+ } catch {
3461
+ }
3462
+ }
3463
+ }
3464
+ normalizeRunState(value) {
3465
+ if (value === "queued" || value === "running" || value === "completed" || value === "failed" || value === "aborted") {
3466
+ return value;
3467
+ }
3468
+ return "failed";
3469
+ }
3470
+ getRunRecord(runId) {
3471
+ const normalized = readOptionalString(runId);
3472
+ if (!normalized) {
3473
+ return null;
3474
+ }
3475
+ return this.runs.get(normalized) ?? null;
3476
+ }
3477
+ };
3478
+
2983
3479
  // src/cli/commands/service.ts
2984
3480
  var {
2985
3481
  APP_NAME: APP_NAME2,
@@ -2987,7 +3483,7 @@ var {
2987
3483
  CronService: CronService2,
2988
3484
  getApiBase,
2989
3485
  getConfigPath: getConfigPath3,
2990
- getDataDir: getDataDir5,
3486
+ getDataDir: getDataDir6,
2991
3487
  getProvider,
2992
3488
  getProviderName,
2993
3489
  getWorkspacePath: getWorkspacePath5,
@@ -2999,7 +3495,7 @@ var {
2999
3495
  resolveConfigSecrets: resolveConfigSecrets2,
3000
3496
  saveConfig: saveConfig5,
3001
3497
  SessionManager,
3002
- parseAgentScopedSessionKey: parseAgentScopedSessionKey2
3498
+ parseAgentScopedSessionKey: parseAgentScopedSessionKey3
3003
3499
  } = NextclawCore;
3004
3500
  function createSkillsLoader(workspace) {
3005
3501
  const ctor = NextclawCore.SkillsLoader;
@@ -3008,21 +3504,6 @@ function createSkillsLoader(workspace) {
3008
3504
  }
3009
3505
  return new ctor(workspace);
3010
3506
  }
3011
- function isAbortError(error) {
3012
- if (error instanceof DOMException && error.name === "AbortError") {
3013
- return true;
3014
- }
3015
- if (error instanceof Error) {
3016
- if (error.name === "AbortError") {
3017
- return true;
3018
- }
3019
- const message = error.message.toLowerCase();
3020
- if (message.includes("aborted") || message.includes("abort")) {
3021
- return true;
3022
- }
3023
- }
3024
- return false;
3025
- }
3026
3507
  var ServiceCommands = class {
3027
3508
  constructor(deps) {
3028
3509
  this.deps = deps;
@@ -3059,7 +3540,7 @@ var ServiceCommands = class {
3059
3540
  }
3060
3541
  }
3061
3542
  };
3062
- const cronStorePath = join4(getDataDir5(), "cron", "jobs.json");
3543
+ const cronStorePath = join5(getDataDir6(), "cron", "jobs.json");
3063
3544
  const cron2 = new CronService2(cronStorePath);
3064
3545
  const uiConfig = resolveUiConfig(config2, options.uiOverrides);
3065
3546
  const uiStaticDir = options.uiStaticDir === void 0 ? resolveUiStaticDir() : options.uiStaticDir;
@@ -3213,7 +3694,7 @@ var ServiceCommands = class {
3213
3694
  } else {
3214
3695
  console.log("Warning: No channels enabled");
3215
3696
  }
3216
- this.startUiIfEnabled(uiConfig, uiStaticDir, cron2, runtimePool);
3697
+ this.startUiIfEnabled(uiConfig, uiStaticDir, cron2, runtimePool, sessionManager);
3217
3698
  const cronStatus = cron2.status();
3218
3699
  if (cronStatus.jobs > 0) {
3219
3700
  console.log(`\u2713 Cron: ${cronStatus.jobs} scheduled jobs`);
@@ -3327,7 +3808,7 @@ var ServiceCommands = class {
3327
3808
  }
3328
3809
  const sessionKey = sentinelSessionKey ?? fallbackSessionKey ?? "cli:default";
3329
3810
  const parsedSession = parseSessionKey(sessionKey);
3330
- const parsedAgentSession = parseAgentScopedSessionKey2(sessionKey);
3811
+ const parsedAgentSession = parseAgentScopedSessionKey3(sessionKey);
3331
3812
  const parsedSessionRoute = parsedSession && parsedSession.channel !== "agent" ? parsedSession : null;
3332
3813
  const context = payload.deliveryContext;
3333
3814
  const channel = this.normalizeOptionalString(context?.channel) ?? parsedSessionRoute?.channel ?? this.normalizeOptionalString((params.sessionManager.getIfExists(sessionKey)?.metadata ?? {}).last_channel);
@@ -3426,50 +3907,65 @@ var ServiceCommands = class {
3426
3907
  }
3427
3908
  const logPath = resolveServiceLogPath();
3428
3909
  const logDir = resolve7(logPath, "..");
3429
- mkdirSync3(logDir, { recursive: true });
3910
+ mkdirSync4(logDir, { recursive: true });
3430
3911
  const logFd = openSync(logPath, "a");
3912
+ const readinessTimeoutMs = this.resolveStartupTimeoutMs(options.startupTimeoutMs);
3913
+ const quickPhaseTimeoutMs = Math.min(8e3, readinessTimeoutMs);
3914
+ const extendedPhaseTimeoutMs = Math.max(0, readinessTimeoutMs - quickPhaseTimeoutMs);
3915
+ this.appendStartupStage(
3916
+ logPath,
3917
+ `start requested: ui=${uiConfig.host}:${uiConfig.port}, readinessTimeoutMs=${readinessTimeoutMs}`
3918
+ );
3919
+ console.log(`Starting ${APP_NAME2} background service (readiness timeout ${Math.ceil(readinessTimeoutMs / 1e3)}s)...`);
3431
3920
  const serveArgs = buildServeArgs({
3432
3921
  uiPort: uiConfig.port
3433
3922
  });
3923
+ this.appendStartupStage(logPath, `spawning background process: ${process.execPath} ${[...process.execArgv, ...serveArgs].join(" ")}`);
3434
3924
  const child = spawn2(process.execPath, [...process.execArgv, ...serveArgs], {
3435
3925
  env: process.env,
3436
3926
  stdio: ["ignore", logFd, logFd],
3437
3927
  detached: true
3438
3928
  });
3929
+ this.appendStartupStage(logPath, `spawned background process pid=${child.pid ?? "unknown"}`);
3439
3930
  closeSync(logFd);
3440
3931
  if (!child.pid) {
3932
+ this.appendStartupStage(logPath, "spawn failed: child pid missing");
3441
3933
  console.error("Error: Failed to start background service.");
3442
3934
  return;
3443
3935
  }
3444
3936
  const healthUrl = `${apiUrl}/health`;
3937
+ this.appendStartupStage(logPath, `health probe started: ${healthUrl} (phase=quick, timeoutMs=${quickPhaseTimeoutMs})`);
3445
3938
  let readiness = await this.waitForBackgroundServiceReady({
3446
3939
  pid: child.pid,
3447
3940
  healthUrl,
3448
- timeoutMs: 8e3
3941
+ timeoutMs: quickPhaseTimeoutMs
3449
3942
  });
3450
- if (!readiness.ready && isProcessRunning(child.pid)) {
3451
- const extendedTimeoutMs = process.platform === "win32" ? 2e4 : 25e3;
3943
+ if (!readiness.ready && isProcessRunning(child.pid) && extendedPhaseTimeoutMs > 0) {
3452
3944
  console.warn(
3453
- `Warning: Background service is still running but not ready after 8s; waiting up to ${Math.ceil(extendedTimeoutMs / 1e3)}s more.`
3945
+ `Warning: Background service is still running but not ready after ${Math.ceil(quickPhaseTimeoutMs / 1e3)}s; waiting up to ${Math.ceil(extendedPhaseTimeoutMs / 1e3)}s more.`
3946
+ );
3947
+ this.appendStartupStage(
3948
+ logPath,
3949
+ `health probe entering extended phase (timeoutMs=${extendedPhaseTimeoutMs}, lastError=${readiness.lastProbeError ?? "none"})`
3454
3950
  );
3455
3951
  readiness = await this.waitForBackgroundServiceReady({
3456
3952
  pid: child.pid,
3457
3953
  healthUrl,
3458
- timeoutMs: extendedTimeoutMs
3954
+ timeoutMs: extendedPhaseTimeoutMs
3459
3955
  });
3460
3956
  }
3461
3957
  if (!readiness.ready) {
3462
- if (isProcessRunning(child.pid)) {
3463
- try {
3464
- process.kill(child.pid, "SIGTERM");
3465
- await waitForExit(child.pid, 2e3);
3466
- } catch {
3467
- }
3958
+ if (!isProcessRunning(child.pid)) {
3959
+ clearServiceState();
3960
+ const hint = readiness.lastProbeError ? ` Last probe error: ${readiness.lastProbeError}` : "";
3961
+ this.appendStartupStage(logPath, `startup failed: process exited before ready.${hint}`);
3962
+ console.error(`Error: Failed to start background service. Check logs: ${logPath}.${hint}`);
3963
+ return;
3468
3964
  }
3469
- clearServiceState();
3470
- const hint = readiness.lastProbeError ? ` Last probe error: ${readiness.lastProbeError}` : "";
3471
- console.error(`Error: Failed to start background service. Check logs: ${logPath}.${hint}`);
3472
- return;
3965
+ this.appendStartupStage(
3966
+ logPath,
3967
+ `startup degraded: process alive but health probe timed out after ${readinessTimeoutMs}ms (lastError=${readiness.lastProbeError ?? "none"})`
3968
+ );
3473
3969
  }
3474
3970
  child.unref();
3475
3971
  const state = {
@@ -3479,10 +3975,22 @@ var ServiceCommands = class {
3479
3975
  apiUrl,
3480
3976
  uiHost: uiConfig.host,
3481
3977
  uiPort: uiConfig.port,
3482
- logPath
3978
+ logPath,
3979
+ startupState: readiness.ready ? "ready" : "degraded",
3980
+ startupLastProbeError: readiness.lastProbeError,
3981
+ startupTimeoutMs: readinessTimeoutMs,
3982
+ startupCheckedAt: (/* @__PURE__ */ new Date()).toISOString()
3483
3983
  };
3484
3984
  writeServiceState(state);
3485
- console.log(`\u2713 ${APP_NAME2} started in background (PID ${state.pid})`);
3985
+ if (!readiness.ready) {
3986
+ const hint = readiness.lastProbeError ? ` Last probe error: ${readiness.lastProbeError}` : "";
3987
+ console.warn(
3988
+ `Warning: ${APP_NAME2} is running (PID ${state.pid}) but not healthy yet after ${Math.ceil(readinessTimeoutMs / 1e3)}s. Marked as degraded.${hint}`
3989
+ );
3990
+ console.warn(`Tip: Run "${APP_NAME2} status --json" and check logs: ${logPath}`);
3991
+ } else {
3992
+ console.log(`\u2713 ${APP_NAME2} started in background (PID ${state.pid})`);
3993
+ }
3486
3994
  console.log(`UI: ${uiUrl}`);
3487
3995
  console.log(`API: ${apiUrl}`);
3488
3996
  await this.printPublicUiUrls(uiConfig.host, uiConfig.port);
@@ -3544,6 +4052,22 @@ var ServiceCommands = class {
3544
4052
  }
3545
4053
  return { ready: false, lastProbeError };
3546
4054
  }
4055
+ resolveStartupTimeoutMs(overrideTimeoutMs) {
4056
+ const fallback = process.platform === "win32" ? 28e3 : 33e3;
4057
+ const envRaw = process.env.NEXTCLAW_START_TIMEOUT_MS?.trim();
4058
+ const envValue = envRaw ? Number(envRaw) : Number.NaN;
4059
+ const fromEnv = Number.isFinite(envValue) && envValue > 0 ? Math.floor(envValue) : null;
4060
+ const fromOverride = Number.isFinite(overrideTimeoutMs) && Number(overrideTimeoutMs) > 0 ? Math.floor(Number(overrideTimeoutMs)) : null;
4061
+ const resolved = fromOverride ?? fromEnv ?? fallback;
4062
+ return Math.max(3e3, resolved);
4063
+ }
4064
+ appendStartupStage(logPath, message) {
4065
+ try {
4066
+ appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [startup] ${message}
4067
+ `, "utf-8");
4068
+ } catch {
4069
+ }
4070
+ }
3547
4071
  async probeHealthEndpoint(healthUrl) {
3548
4072
  let parsed;
3549
4073
  try {
@@ -3651,11 +4175,10 @@ var ServiceCommands = class {
3651
4175
  console.log(` - Check status: ${APP_NAME2} status`);
3652
4176
  console.log(` - If you need to stop the service, run: ${APP_NAME2} stop`);
3653
4177
  }
3654
- startUiIfEnabled(uiConfig, uiStaticDir, cronService, runtimePool) {
4178
+ startUiIfEnabled(uiConfig, uiStaticDir, cronService, runtimePool, sessionManager) {
3655
4179
  if (!uiConfig.enabled) {
3656
4180
  return;
3657
4181
  }
3658
- const activeTurnRuns = /* @__PURE__ */ new Map();
3659
4182
  const resolveStopCapability = (params) => runtimePool.supportsTurnAbort({
3660
4183
  sessionKey: params.sessionKey,
3661
4184
  agentId: params.agentId,
@@ -3665,7 +4188,7 @@ var ServiceCommands = class {
3665
4188
  });
3666
4189
  const resolveChatTurnParams = (params) => {
3667
4190
  const sessionKey = typeof params.sessionKey === "string" && params.sessionKey.trim().length > 0 ? params.sessionKey.trim() : `ui:${Date.now().toString(36)}:${Math.random().toString(36).slice(2, 8)}`;
3668
- const inferredAgentId = typeof params.agentId === "string" && params.agentId.trim().length > 0 ? params.agentId.trim() : parseAgentScopedSessionKey2(sessionKey)?.agentId;
4191
+ const inferredAgentId = typeof params.agentId === "string" && params.agentId.trim().length > 0 ? params.agentId.trim() : parseAgentScopedSessionKey3(sessionKey)?.agentId;
3669
4192
  const model = typeof params.model === "string" && params.model.trim().length > 0 ? params.model.trim() : void 0;
3670
4193
  const metadata = params.metadata && typeof params.metadata === "object" && !Array.isArray(params.metadata) ? { ...params.metadata } : {};
3671
4194
  if (model) {
@@ -3688,6 +4211,14 @@ var ServiceCommands = class {
3688
4211
  ...params.inferredAgentId ? { agentId: params.inferredAgentId } : {},
3689
4212
  ...params.model ? { model: params.model } : {}
3690
4213
  });
4214
+ let publishUiEvent = null;
4215
+ const runCoordinator = new UiChatRunCoordinator({
4216
+ runtimePool,
4217
+ sessionManager,
4218
+ onRunUpdated: (run) => {
4219
+ publishUiEvent?.({ type: "run.updated", payload: { run } });
4220
+ }
4221
+ });
3691
4222
  const uiServer = startUiServer({
3692
4223
  host: uiConfig.host,
3693
4224
  port: uiConfig.port,
@@ -3737,153 +4268,32 @@ var ServiceCommands = class {
3737
4268
  model: resolved.model
3738
4269
  });
3739
4270
  },
3740
- stopTurn: async (params) => {
3741
- const runId = typeof params.runId === "string" ? params.runId.trim() : "";
3742
- if (!runId) {
3743
- return {
3744
- stopped: false,
3745
- runId: "",
3746
- reason: "runId is required"
3747
- };
3748
- }
3749
- const active = activeTurnRuns.get(runId);
3750
- if (!active) {
3751
- return {
3752
- stopped: false,
3753
- runId,
3754
- ...typeof params.sessionKey === "string" && params.sessionKey.trim().length > 0 ? { sessionKey: params.sessionKey.trim() } : {},
3755
- reason: "run not found or already completed"
3756
- };
3757
- }
3758
- const requestedSessionKey = typeof params.sessionKey === "string" ? params.sessionKey.trim() : "";
3759
- if (requestedSessionKey && requestedSessionKey !== active.sessionKey) {
3760
- return {
3761
- stopped: false,
3762
- runId,
3763
- sessionKey: active.sessionKey,
3764
- reason: "session key mismatch"
3765
- };
4271
+ startTurnRun: async (params) => {
4272
+ return runCoordinator.startRun(params);
4273
+ },
4274
+ listRuns: async (params) => {
4275
+ return runCoordinator.listRuns(params);
4276
+ },
4277
+ getRun: async (params) => {
4278
+ return runCoordinator.getRun(params);
4279
+ },
4280
+ streamRun: async function* (params) {
4281
+ for await (const event of runCoordinator.streamRun(params)) {
4282
+ yield event;
3766
4283
  }
3767
- active.controller.abort(new Error("chat turn stopped by user"));
3768
- return {
3769
- stopped: true,
3770
- runId,
3771
- sessionKey: active.sessionKey
3772
- };
4284
+ },
4285
+ stopTurn: async (params) => {
4286
+ return await runCoordinator.stopRun(params);
3773
4287
  },
3774
4288
  processTurnStream: async function* (params) {
3775
- const resolved = resolveChatTurnParams(params);
3776
- const runId = resolved.runId ?? `run-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
3777
- const stopCapability = resolveStopCapability({
3778
- sessionKey: resolved.sessionKey,
3779
- agentId: resolved.inferredAgentId,
3780
- channel: resolved.channel,
3781
- chatId: resolved.chatId,
3782
- metadata: resolved.metadata
3783
- });
3784
- const controller = stopCapability.supported ? new AbortController() : null;
3785
- if (controller) {
3786
- activeTurnRuns.set(runId, {
3787
- controller,
3788
- sessionKey: resolved.sessionKey,
3789
- ...resolved.inferredAgentId ? { agentId: resolved.inferredAgentId } : {}
3790
- });
3791
- }
3792
- const queue = [];
3793
- const assistantDeltaParts = [];
3794
- let waiter = null;
3795
- const push = (event) => {
3796
- queue.push(event);
3797
- const currentWaiter = waiter;
3798
- waiter = null;
3799
- currentWaiter?.();
3800
- };
3801
- const run = runtimePool.processDirect({
3802
- content: params.message,
3803
- sessionKey: resolved.sessionKey,
3804
- channel: resolved.channel,
3805
- chatId: resolved.chatId,
3806
- agentId: resolved.inferredAgentId,
3807
- metadata: resolved.metadata,
3808
- ...controller ? { abortSignal: controller.signal } : {},
3809
- onAssistantDelta: (delta) => {
3810
- if (typeof delta !== "string" || delta.length === 0) {
3811
- return;
3812
- }
3813
- assistantDeltaParts.push(delta);
3814
- push({ type: "delta", delta });
3815
- },
3816
- onSessionEvent: (event) => {
3817
- const raw = event.data?.message;
3818
- const messageRecord = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : null;
3819
- const message = messageRecord && typeof messageRecord.role === "string" ? {
3820
- role: messageRecord.role,
3821
- content: messageRecord.content,
3822
- timestamp: typeof messageRecord.timestamp === "string" ? messageRecord.timestamp : event.timestamp,
3823
- ...typeof messageRecord.name === "string" ? { name: messageRecord.name } : {},
3824
- ...typeof messageRecord.tool_call_id === "string" ? { tool_call_id: messageRecord.tool_call_id } : {},
3825
- ...Array.isArray(messageRecord.tool_calls) ? { tool_calls: messageRecord.tool_calls } : {},
3826
- ...typeof messageRecord.reasoning_content === "string" ? { reasoning_content: messageRecord.reasoning_content } : {}
3827
- } : void 0;
3828
- push({
3829
- type: "session_event",
3830
- event: {
3831
- seq: event.seq,
3832
- type: event.type,
3833
- timestamp: event.timestamp,
3834
- ...message ? { message } : {}
3835
- }
3836
- });
3837
- }
3838
- }).then((reply) => {
3839
- push({
3840
- type: "final",
3841
- result: buildTurnResult({
3842
- reply,
3843
- sessionKey: resolved.sessionKey,
3844
- inferredAgentId: resolved.inferredAgentId,
3845
- model: resolved.model
3846
- })
3847
- });
3848
- }).catch((error) => {
3849
- if ((controller?.signal.aborted ?? false) || isAbortError(error)) {
3850
- const partialReply = assistantDeltaParts.join("");
3851
- push({
3852
- type: "final",
3853
- result: buildTurnResult({
3854
- reply: partialReply,
3855
- sessionKey: resolved.sessionKey,
3856
- inferredAgentId: resolved.inferredAgentId,
3857
- model: resolved.model
3858
- })
3859
- });
3860
- return;
3861
- }
3862
- push({ type: "error", error: String(error) });
3863
- }).finally(() => {
3864
- activeTurnRuns.delete(runId);
3865
- });
3866
- while (true) {
3867
- if (queue.length === 0) {
3868
- await new Promise((resolve10) => {
3869
- waiter = resolve10;
3870
- });
3871
- }
3872
- while (queue.length > 0) {
3873
- const event = queue.shift();
3874
- if (!event) {
3875
- continue;
3876
- }
3877
- yield event;
3878
- if (event.type === "final" || event.type === "error") {
3879
- await run;
3880
- return;
3881
- }
3882
- }
4289
+ const run = runCoordinator.startRun(params);
4290
+ for await (const event of runCoordinator.streamRun({ runId: run.runId })) {
4291
+ yield event;
3883
4292
  }
3884
4293
  }
3885
4294
  }
3886
4295
  });
4296
+ publishUiEvent = uiServer.publish;
3887
4297
  const uiUrl = `http://${uiServer.host}:${uiServer.port}`;
3888
4298
  console.log(`\u2713 UI API: ${uiUrl}/api`);
3889
4299
  if (uiStaticDir) {
@@ -3940,17 +4350,17 @@ var ServiceCommands = class {
3940
4350
  const workspace = getWorkspacePath5(loadConfig6().agents.defaults.workspace);
3941
4351
  const skillName = this.resolveGitSkillName(params.skill, source);
3942
4352
  const destination = this.resolveSkillInstallPath(workspace, params.installPath, skillName);
3943
- const destinationSkillFile = join4(destination, "SKILL.md");
3944
- if (existsSync7(destinationSkillFile) && !params.force) {
4353
+ const destinationSkillFile = join5(destination, "SKILL.md");
4354
+ if (existsSync8(destinationSkillFile) && !params.force) {
3945
4355
  return {
3946
4356
  message: `${skillName} is already installed`,
3947
4357
  output: destination
3948
4358
  };
3949
4359
  }
3950
- if (existsSync7(destination) && !params.force) {
4360
+ if (existsSync8(destination) && !params.force) {
3951
4361
  throw new Error(`Skill install path already exists: ${destination} (use force to overwrite)`);
3952
4362
  }
3953
- if (existsSync7(destination) && params.force) {
4363
+ if (existsSync8(destination) && params.force) {
3954
4364
  rmSync3(destination, { recursive: true, force: true });
3955
4365
  }
3956
4366
  const skildArgs = ["--yes", "skild", "install", source, "--target", "agents", "--local", "--json", "--skill", skillName];
@@ -3985,11 +4395,11 @@ var ServiceCommands = class {
3985
4395
  throw new Error("skild returned null json payload even after force reinstall");
3986
4396
  }
3987
4397
  const installDir = typeof payload.installDir === "string" ? payload.installDir.trim() : "";
3988
- const installSkillFile = installDir ? join4(installDir, "SKILL.md") : "";
3989
- if (!installDir || !existsSync7(installSkillFile)) {
4398
+ const installSkillFile = installDir ? join5(installDir, "SKILL.md") : "";
4399
+ if (!installDir || !existsSync8(installSkillFile)) {
3990
4400
  throw new Error(`skild install did not produce a valid skill directory for ${skillName}`);
3991
4401
  }
3992
- mkdirSync3(dirname(destination), { recursive: true });
4402
+ mkdirSync4(dirname(destination), { recursive: true });
3993
4403
  if (resolve7(installDir) !== resolve7(destination)) {
3994
4404
  cpSync(installDir, destination, { recursive: true, force: true });
3995
4405
  }
@@ -4020,9 +4430,9 @@ var ServiceCommands = class {
4020
4430
  }
4021
4431
  async uninstallMarketplaceSkill(slug) {
4022
4432
  const workspace = getWorkspacePath5(loadConfig6().agents.defaults.workspace);
4023
- const targetDir = join4(workspace, "skills", slug);
4024
- const skildDir = join4(workspace, ".agents", "skills", slug);
4025
- const existingTargets = [targetDir, skildDir].filter((path) => existsSync7(path));
4433
+ const targetDir = join5(workspace, "skills", slug);
4434
+ const skildDir = join5(workspace, ".agents", "skills", slug);
4435
+ const existingTargets = [targetDir, skildDir].filter((path) => existsSync8(path));
4026
4436
  if (existingTargets.length === 0) {
4027
4437
  throw new Error(`Skill not installed in workspace: ${slug}`);
4028
4438
  }
@@ -4036,9 +4446,9 @@ var ServiceCommands = class {
4036
4446
  }
4037
4447
  installBuiltinMarketplaceSkill(slug, force) {
4038
4448
  const workspace = getWorkspacePath5(loadConfig6().agents.defaults.workspace);
4039
- const destination = join4(workspace, "skills", slug);
4040
- const destinationSkillFile = join4(destination, "SKILL.md");
4041
- if (existsSync7(destinationSkillFile) && !force) {
4449
+ const destination = join5(workspace, "skills", slug);
4450
+ const destinationSkillFile = join5(destination, "SKILL.md");
4451
+ if (existsSync8(destinationSkillFile) && !force) {
4042
4452
  return {
4043
4453
  message: `${slug} is already installed`,
4044
4454
  output: destination
@@ -4047,7 +4457,7 @@ var ServiceCommands = class {
4047
4457
  const loader = createSkillsLoader(workspace);
4048
4458
  const builtin = (loader?.listSkills(false) ?? []).find((skill) => skill.name === slug && skill.source === "builtin");
4049
4459
  if (!builtin) {
4050
- if (existsSync7(destinationSkillFile)) {
4460
+ if (existsSync8(destinationSkillFile)) {
4051
4461
  return {
4052
4462
  message: `${slug} is already installed`,
4053
4463
  output: destination
@@ -4055,7 +4465,7 @@ var ServiceCommands = class {
4055
4465
  }
4056
4466
  return null;
4057
4467
  }
4058
- mkdirSync3(join4(workspace, "skills"), { recursive: true });
4468
+ mkdirSync4(join5(workspace, "skills"), { recursive: true });
4059
4469
  cpSync(dirname(builtin.path), destination, { recursive: true, force: true });
4060
4470
  return {
4061
4471
  message: `Installed skill: ${slug}`,
@@ -4082,7 +4492,7 @@ var ServiceCommands = class {
4082
4492
  return skillName;
4083
4493
  }
4084
4494
  resolveSkillInstallPath(workspace, installPath, skillName) {
4085
- const requested = typeof installPath === "string" && installPath.trim().length > 0 ? installPath.trim() : join4("skills", skillName);
4495
+ const requested = typeof installPath === "string" && installPath.trim().length > 0 ? installPath.trim() : join5("skills", skillName);
4086
4496
  if (isAbsolute2(requested)) {
4087
4497
  throw new Error("installPath must be relative to workspace");
4088
4498
  }
@@ -4197,11 +4607,11 @@ ${stderr}`.trim();
4197
4607
  };
4198
4608
 
4199
4609
  // src/cli/workspace.ts
4200
- import { cpSync as cpSync2, existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync6, readdirSync, rmSync as rmSync4, writeFileSync as writeFileSync3 } from "fs";
4610
+ import { cpSync as cpSync2, existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync7, readdirSync as readdirSync2, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "fs";
4201
4611
  import { createRequire } from "module";
4202
- import { dirname as dirname2, join as join5, resolve as resolve8 } from "path";
4612
+ import { dirname as dirname2, join as join6, resolve as resolve8 } from "path";
4203
4613
  import { fileURLToPath as fileURLToPath3 } from "url";
4204
- import { APP_NAME as APP_NAME3, getDataDir as getDataDir6 } from "@nextclaw/core";
4614
+ import { APP_NAME as APP_NAME3, getDataDir as getDataDir7 } from "@nextclaw/core";
4205
4615
  import { spawnSync as spawnSync4 } from "child_process";
4206
4616
  var WorkspaceManager = class {
4207
4617
  constructor(logo) {
@@ -4229,30 +4639,30 @@ var WorkspaceManager = class {
4229
4639
  { source: "memory/MEMORY.md", target: "memory/MEMORY.md" }
4230
4640
  ];
4231
4641
  for (const entry of templateFiles) {
4232
- const filePath = join5(workspace, entry.target);
4233
- if (!force && existsSync8(filePath)) {
4642
+ const filePath = join6(workspace, entry.target);
4643
+ if (!force && existsSync9(filePath)) {
4234
4644
  continue;
4235
4645
  }
4236
- const templatePath = join5(templateDir, entry.source);
4237
- if (!existsSync8(templatePath)) {
4646
+ const templatePath = join6(templateDir, entry.source);
4647
+ if (!existsSync9(templatePath)) {
4238
4648
  console.warn(`Warning: Template file missing: ${templatePath}`);
4239
4649
  continue;
4240
4650
  }
4241
- const raw = readFileSync6(templatePath, "utf-8");
4651
+ const raw = readFileSync7(templatePath, "utf-8");
4242
4652
  const content = raw.replace(/\$\{APP_NAME\}/g, APP_NAME3);
4243
- mkdirSync4(dirname2(filePath), { recursive: true });
4244
- writeFileSync3(filePath, content);
4653
+ mkdirSync5(dirname2(filePath), { recursive: true });
4654
+ writeFileSync4(filePath, content);
4245
4655
  created.push(entry.target);
4246
4656
  }
4247
- const memoryDir = join5(workspace, "memory");
4248
- if (!existsSync8(memoryDir)) {
4249
- mkdirSync4(memoryDir, { recursive: true });
4250
- created.push(join5("memory", ""));
4657
+ const memoryDir = join6(workspace, "memory");
4658
+ if (!existsSync9(memoryDir)) {
4659
+ mkdirSync5(memoryDir, { recursive: true });
4660
+ created.push(join6("memory", ""));
4251
4661
  }
4252
- const skillsDir = join5(workspace, "skills");
4253
- if (!existsSync8(skillsDir)) {
4254
- mkdirSync4(skillsDir, { recursive: true });
4255
- created.push(join5("skills", ""));
4662
+ const skillsDir = join6(workspace, "skills");
4663
+ if (!existsSync9(skillsDir)) {
4664
+ mkdirSync5(skillsDir, { recursive: true });
4665
+ created.push(join6("skills", ""));
4256
4666
  }
4257
4667
  const seeded = this.seedBuiltinSkills(skillsDir, { force });
4258
4668
  if (seeded > 0) {
@@ -4267,16 +4677,16 @@ var WorkspaceManager = class {
4267
4677
  }
4268
4678
  const force = Boolean(options.force);
4269
4679
  let seeded = 0;
4270
- for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
4680
+ for (const entry of readdirSync2(sourceDir, { withFileTypes: true })) {
4271
4681
  if (!entry.isDirectory()) {
4272
4682
  continue;
4273
4683
  }
4274
- const src = join5(sourceDir, entry.name);
4275
- if (!existsSync8(join5(src, "SKILL.md"))) {
4684
+ const src = join6(sourceDir, entry.name);
4685
+ if (!existsSync9(join6(src, "SKILL.md"))) {
4276
4686
  continue;
4277
4687
  }
4278
- const dest = join5(targetDir, entry.name);
4279
- if (!force && existsSync8(dest)) {
4688
+ const dest = join6(targetDir, entry.name);
4689
+ if (!force && existsSync9(dest)) {
4280
4690
  continue;
4281
4691
  }
4282
4692
  cpSync2(src, dest, { recursive: true, force: true });
@@ -4289,12 +4699,12 @@ var WorkspaceManager = class {
4289
4699
  const require2 = createRequire(import.meta.url);
4290
4700
  const entry = require2.resolve("@nextclaw/core");
4291
4701
  const pkgRoot = resolve8(dirname2(entry), "..");
4292
- const distSkills = join5(pkgRoot, "dist", "skills");
4293
- if (existsSync8(distSkills)) {
4702
+ const distSkills = join6(pkgRoot, "dist", "skills");
4703
+ if (existsSync9(distSkills)) {
4294
4704
  return distSkills;
4295
4705
  }
4296
- const srcSkills = join5(pkgRoot, "src", "agent", "skills");
4297
- if (existsSync8(srcSkills)) {
4706
+ const srcSkills = join6(pkgRoot, "src", "agent", "skills");
4707
+ if (existsSync9(srcSkills)) {
4298
4708
  return srcSkills;
4299
4709
  }
4300
4710
  return null;
@@ -4309,17 +4719,17 @@ var WorkspaceManager = class {
4309
4719
  }
4310
4720
  const cliDir = resolve8(fileURLToPath3(new URL(".", import.meta.url)));
4311
4721
  const pkgRoot = resolve8(cliDir, "..", "..");
4312
- const candidates = [join5(pkgRoot, "templates")];
4722
+ const candidates = [join6(pkgRoot, "templates")];
4313
4723
  for (const candidate of candidates) {
4314
- if (existsSync8(candidate)) {
4724
+ if (existsSync9(candidate)) {
4315
4725
  return candidate;
4316
4726
  }
4317
4727
  }
4318
4728
  return null;
4319
4729
  }
4320
4730
  getBridgeDir() {
4321
- const userBridge = join5(getDataDir6(), "bridge");
4322
- if (existsSync8(join5(userBridge, "dist", "index.js"))) {
4731
+ const userBridge = join6(getDataDir7(), "bridge");
4732
+ if (existsSync9(join6(userBridge, "dist", "index.js"))) {
4323
4733
  return userBridge;
4324
4734
  }
4325
4735
  if (!which("npm")) {
@@ -4328,12 +4738,12 @@ var WorkspaceManager = class {
4328
4738
  }
4329
4739
  const cliDir = resolve8(fileURLToPath3(new URL(".", import.meta.url)));
4330
4740
  const pkgRoot = resolve8(cliDir, "..", "..");
4331
- const pkgBridge = join5(pkgRoot, "bridge");
4332
- const srcBridge = join5(pkgRoot, "..", "..", "bridge");
4741
+ const pkgBridge = join6(pkgRoot, "bridge");
4742
+ const srcBridge = join6(pkgRoot, "..", "..", "bridge");
4333
4743
  let source = null;
4334
- if (existsSync8(join5(pkgBridge, "package.json"))) {
4744
+ if (existsSync9(join6(pkgBridge, "package.json"))) {
4335
4745
  source = pkgBridge;
4336
- } else if (existsSync8(join5(srcBridge, "package.json"))) {
4746
+ } else if (existsSync9(join6(srcBridge, "package.json"))) {
4337
4747
  source = srcBridge;
4338
4748
  }
4339
4749
  if (!source) {
@@ -4341,8 +4751,8 @@ var WorkspaceManager = class {
4341
4751
  process.exit(1);
4342
4752
  }
4343
4753
  console.log(`${this.logo} Setting up bridge...`);
4344
- mkdirSync4(resolve8(userBridge, ".."), { recursive: true });
4345
- if (existsSync8(userBridge)) {
4754
+ mkdirSync5(resolve8(userBridge, ".."), { recursive: true });
4755
+ if (existsSync9(userBridge)) {
4346
4756
  rmSync4(userBridge, { recursive: true, force: true });
4347
4757
  }
4348
4758
  cpSync2(source, userBridge, {
@@ -4471,7 +4881,7 @@ var CliRuntime = class {
4471
4881
  const delayMs = typeof params.delayMs === "number" && Number.isFinite(params.delayMs) ? Math.max(0, Math.floor(params.delayMs)) : 100;
4472
4882
  const cliPath = process.env.NEXTCLAW_SELF_RELAUNCH_CLI?.trim() || fileURLToPath4(new URL("./index.js", import.meta.url));
4473
4883
  const startArgs = [cliPath, "start", "--ui-port", String(uiPort)];
4474
- const serviceStatePath = resolve9(getDataDir7(), "run", "service.json");
4884
+ const serviceStatePath = resolve9(getDataDir8(), "run", "service.json");
4475
4885
  const helperScript = [
4476
4886
  'const { spawnSync } = require("node:child_process");',
4477
4887
  'const { readFileSync } = require("node:fs");',
@@ -4601,16 +5011,16 @@ var CliRuntime = class {
4601
5011
  const force = Boolean(options.force);
4602
5012
  const configPath = getConfigPath4();
4603
5013
  let createdConfig = false;
4604
- if (!existsSync9(configPath)) {
5014
+ if (!existsSync10(configPath)) {
4605
5015
  const config3 = ConfigSchema2.parse({});
4606
5016
  saveConfig6(config3);
4607
5017
  createdConfig = true;
4608
5018
  }
4609
5019
  const config2 = loadConfig7();
4610
5020
  const workspaceSetting = config2.agents.defaults.workspace;
4611
- const workspacePath = !workspaceSetting || workspaceSetting === DEFAULT_WORKSPACE_PATH ? join6(getDataDir7(), DEFAULT_WORKSPACE_DIR) : expandHome2(workspaceSetting);
4612
- const workspaceExisted = existsSync9(workspacePath);
4613
- mkdirSync5(workspacePath, { recursive: true });
5021
+ const workspacePath = !workspaceSetting || workspaceSetting === DEFAULT_WORKSPACE_PATH ? join7(getDataDir8(), DEFAULT_WORKSPACE_DIR) : expandHome2(workspaceSetting);
5022
+ const workspaceExisted = existsSync10(workspacePath);
5023
+ mkdirSync6(workspacePath, { recursive: true });
4614
5024
  const templateResult = this.workspaceManager.createWorkspaceTemplates(
4615
5025
  workspacePath,
4616
5026
  { force }
@@ -4669,6 +5079,7 @@ ${this.logo} ${APP_NAME4} is ready! (${source})`);
4669
5079
  });
4670
5080
  }
4671
5081
  async start(opts) {
5082
+ const startupTimeoutMs = this.parseStartTimeoutMs(opts.startTimeout);
4672
5083
  await this.init({ source: "start", auto: true });
4673
5084
  const uiOverrides = {
4674
5085
  enabled: true,
@@ -4680,7 +5091,8 @@ ${this.logo} ${APP_NAME4} is ready! (${source})`);
4680
5091
  }
4681
5092
  await this.serviceCommands.startService({
4682
5093
  uiOverrides,
4683
- open: Boolean(opts.open)
5094
+ open: Boolean(opts.open),
5095
+ startupTimeoutMs
4684
5096
  });
4685
5097
  }
4686
5098
  async restart(opts) {
@@ -4711,6 +5123,17 @@ ${this.logo} ${APP_NAME4} is ready! (${source})`);
4711
5123
  open: Boolean(opts.open)
4712
5124
  });
4713
5125
  }
5126
+ parseStartTimeoutMs(value) {
5127
+ if (value === void 0) {
5128
+ return void 0;
5129
+ }
5130
+ const parsed = Number(value);
5131
+ if (!Number.isFinite(parsed) || parsed <= 0) {
5132
+ console.error("Invalid --start-timeout value. Provide milliseconds (e.g. 45000).");
5133
+ process.exit(1);
5134
+ }
5135
+ return Math.floor(parsed);
5136
+ }
4714
5137
  async stop() {
4715
5138
  await this.serviceCommands.stopService();
4716
5139
  }
@@ -4784,10 +5207,10 @@ ${this.logo} ${APP_NAME4} is ready! (${source})`);
4784
5207
  `${this.logo} Interactive mode (type exit or Ctrl+C to quit)
4785
5208
  `
4786
5209
  );
4787
- const historyFile = join6(getDataDir7(), "history", "cli_history");
5210
+ const historyFile = join7(getDataDir8(), "history", "cli_history");
4788
5211
  const historyDir = resolve9(historyFile, "..");
4789
- mkdirSync5(historyDir, { recursive: true });
4790
- const history = existsSync9(historyFile) ? readFileSync7(historyFile, "utf-8").split("\n").filter(Boolean) : [];
5212
+ mkdirSync6(historyDir, { recursive: true });
5213
+ const history = existsSync10(historyFile) ? readFileSync8(historyFile, "utf-8").split("\n").filter(Boolean) : [];
4791
5214
  const rl = createInterface2({
4792
5215
  input: process.stdin,
4793
5216
  output: process.stdout
@@ -4796,7 +5219,7 @@ ${this.logo} ${APP_NAME4} is ready! (${source})`);
4796
5219
  const merged = history.concat(
4797
5220
  rl.history ?? []
4798
5221
  );
4799
- writeFileSync4(historyFile, merged.join("\n"));
5222
+ writeFileSync5(historyFile, merged.join("\n"));
4800
5223
  process.exit(0);
4801
5224
  });
4802
5225
  let running = true;
@@ -4972,8 +5395,8 @@ program.command("onboard").description(`Initialize ${APP_NAME5} configuration an
4972
5395
  program.command("init").description(`Initialize ${APP_NAME5} configuration and workspace`).option("-f, --force", "Overwrite existing template files").action(async (opts) => runtime.init({ force: Boolean(opts.force) }));
4973
5396
  program.command("gateway").description(`Start the ${APP_NAME5} gateway`).option("-p, --port <port>", "Gateway port", "18790").option("-v, --verbose", "Verbose output", false).option("--ui", "Enable UI server", false).option("--ui-port <port>", "UI port").option("--ui-open", "Open browser when UI starts", false).action(async (opts) => runtime.gateway(opts));
4974
5397
  program.command("ui").description(`Start the ${APP_NAME5} UI with gateway`).option("--port <port>", "UI port").option("--no-open", "Disable opening browser").action(async (opts) => runtime.ui(opts));
4975
- program.command("start").description(`Start the ${APP_NAME5} gateway + UI in the background`).option("--ui-port <port>", "UI port").option("--open", "Open browser after start", false).action(async (opts) => runtime.start(opts));
4976
- program.command("restart").description(`Restart the ${APP_NAME5} background service`).option("--ui-port <port>", "UI port").option("--open", "Open browser after restart", false).action(async (opts) => runtime.restart(opts));
5398
+ program.command("start").description(`Start the ${APP_NAME5} gateway + UI in the background`).option("--ui-port <port>", "UI port").option("--start-timeout <ms>", "Maximum wait time for startup readiness in milliseconds").option("--open", "Open browser after start", false).action(async (opts) => runtime.start(opts));
5399
+ program.command("restart").description(`Restart the ${APP_NAME5} background service`).option("--ui-port <port>", "UI port").option("--start-timeout <ms>", "Maximum wait time for startup readiness in milliseconds").option("--open", "Open browser after restart", false).action(async (opts) => runtime.restart(opts));
4977
5400
  program.command("serve").description(`Run the ${APP_NAME5} gateway + UI in the foreground`).option("--ui-port <port>", "UI port").option("--open", "Open browser after start", false).action(async (opts) => runtime.serve(opts));
4978
5401
  program.command("stop").description(`Stop the ${APP_NAME5} background service`).action(async () => runtime.stop());
4979
5402
  program.command("agent").description("Interact with the agent directly").option("-m, --message <message>", "Message to send to the agent").option("-s, --session <session>", "Session ID", "cli:default").option("--model <model>", "Session model override for this run").option("--no-markdown", "Disable Markdown rendering").action(async (opts) => runtime.agent(opts));