@botlearn-course/daemon 0.0.13-beta.1 → 0.0.13

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.
@@ -7,6 +7,7 @@ export interface AgentServiceSandboxOptions {
7
7
  sandboxToken: string;
8
8
  runtimes: Map<string, CourseRuntime>;
9
9
  daemonVersion: string;
10
+ deepseekTuiVersion?: string;
10
11
  log?: Logger;
11
12
  random?: () => number;
12
13
  sleep?: (ms: number) => Promise<void>;
@@ -313,6 +313,7 @@ export class AgentServiceSandboxClient {
313
313
  return {
314
314
  workspaceDir: prepared.workspaceDir,
315
315
  transcriptFile: prepared.transcriptFile,
316
+ runtimeStateDir: prepared.rootDir,
316
317
  nativeSessionId: session.nativeSessionId,
317
318
  contextRevision,
318
319
  runtimeEnv,
@@ -678,6 +679,12 @@ export class AgentServiceSandboxClient {
678
679
  await this.sendControlFrame("sandbox.ready", {
679
680
  protocol_versions: [AGENT_SERVICE_WS_SCHEMA],
680
681
  daemon_version: this.options.daemonVersion,
682
+ runtime_versions: {
683
+ course_daemon: this.options.daemonVersion,
684
+ ...(this.options.deepseekTuiVersion
685
+ ? { deepseek_tui: this.options.deepseekTuiVersion }
686
+ : {}),
687
+ },
681
688
  resumed_sessions: Object.keys(this.state.sessions),
682
689
  spool_frames: this.spoolFrameCount(),
683
690
  });
package/dist/cli.d.ts CHANGED
@@ -20,4 +20,5 @@ export interface PollLoopDeps {
20
20
  log?: Logger;
21
21
  }
22
22
  export declare function runPollLoop(deps: PollLoopDeps): Promise<PollLoopExit>;
23
+ export declare function assertManagedCourseDaemonVersion(actualVersion: string, selectedVersion: string | undefined): void;
23
24
  export declare function runCli(argv: string[]): Promise<number>;
package/dist/cli.js CHANGED
@@ -337,6 +337,11 @@ async function readSandboxBootstrapFromStdin() {
337
337
  chunk.fill(0);
338
338
  }
339
339
  }
340
+ export function assertManagedCourseDaemonVersion(actualVersion, selectedVersion) {
341
+ if (selectedVersion && selectedVersion !== actualVersion) {
342
+ throw new Error("Managed Course Daemon version does not match the selected release");
343
+ }
344
+ }
340
345
  async function cmdAgentServiceSession(args) {
341
346
  const bootstrap = args.flags["bootstrap-stdin"] === true
342
347
  ? await readSandboxBootstrapFromStdin()
@@ -350,6 +355,7 @@ async function cmdAgentServiceSession(args) {
350
355
  console.error("Agent Service sandbox environment is incomplete");
351
356
  return 1;
352
357
  }
358
+ assertManagedCourseDaemonVersion(pkg.version, process.env.BOTLEARN_MANAGED_COURSE_DAEMON_VERSION?.trim() || undefined);
353
359
  augmentProcessPath();
354
360
  const runtimes = new Map();
355
361
  for (const mod of RUNTIME_MODULES) {
@@ -363,6 +369,7 @@ async function cmdAgentServiceSession(args) {
363
369
  sandboxToken,
364
370
  runtimes,
365
371
  daemonVersion: pkg.version,
372
+ deepseekTuiVersion: process.env.BOTLEARN_MANAGED_DEEPSEEK_TUI_VERSION?.trim() || undefined,
366
373
  });
367
374
  clearAgentServiceControlEnv();
368
375
  await client.run();
@@ -12,6 +12,7 @@ export interface RunDispatcherOptions {
12
12
  export interface PreparedPersistentTurn {
13
13
  workspaceDir: string;
14
14
  transcriptFile: string;
15
+ runtimeStateDir?: string;
15
16
  nativeSessionId: string | null;
16
17
  contextRevision: number;
17
18
  runtimeEnv?: NodeJS.ProcessEnv;
@@ -685,6 +685,9 @@ export class RunDispatcher {
685
685
  inputAttachments,
686
686
  ...(persistentTurn
687
687
  ? {
688
+ ...(persistentTurn.runtimeStateDir
689
+ ? { runtimeStateDir: persistentTurn.runtimeStateDir }
690
+ : {}),
688
691
  nativeSessionId: persistentTurn.nativeSessionId,
689
692
  contextRevision: persistentTurn.contextRevision,
690
693
  ...(persistentTurn.runtimeEnv
@@ -17,6 +17,8 @@ const AGENT_SERVICE_SUPERVISOR_ENV_KEYS = [
17
17
  "BOTLEARN_RUNTIME_HOME",
18
18
  "BOTLEARN_RUNTIME_LAUNCHER",
19
19
  "BOTLEARN_RUNTIME_LAUNCH_MODE",
20
+ "BOTLEARN_MANAGED_COURSE_DAEMON_VERSION",
21
+ "BOTLEARN_MANAGED_DEEPSEEK_TUI_VERSION",
20
22
  "BOTLEARN_AGENT_SERVICE_WORKSPACE_ROOT",
21
23
  "BOTLEARN_AGENT_SERVICE_PROFILE_ROOT",
22
24
  ];
@@ -38,13 +38,20 @@ export declare class DeepseekTuiAdapter implements EngineAdapter {
38
38
  private resolveBinary;
39
39
  private acquireHandle;
40
40
  /**
41
- * 不设置 DEEPSEEK_RUNTIME_DIR:BYOA server 可跨 run 池化,直接使用用户本机
42
- * deepseek 默认状态目录(含已登录凭据);Agent Service server activation 回收。
41
+ * BYOA continues to use the user's default DeepSeek state directory. Managed mode
42
+ * pins the durable RuntimeThreadStore to the RuntimeSession workspace even though
43
+ * the credential-bearing server process is still reclaimed after every activation.
43
44
  */
44
45
  private spawnEnv;
46
+ private prepareManagedRuntimeDir;
47
+ private compactionMarkerPath;
48
+ private compactionRequired;
49
+ private markCompactionRequired;
50
+ private clearCompactionRequired;
45
51
  private managedActivationId;
46
52
  private createThread;
47
53
  private patchThreadSystemContext;
54
+ private compactThread;
48
55
  private startTurnAndReadEvents;
49
56
  private interruptTurn;
50
57
  private readEvents;
@@ -1,5 +1,5 @@
1
1
  import { spawn } from "node:child_process";
2
- import { chmodSync, existsSync, realpathSync, writeFileSync } from "node:fs";
2
+ import { chmodSync, existsSync, lstatSync, mkdirSync, realpathSync, rmSync, writeFileSync, } from "node:fs";
3
3
  import path from "node:path";
4
4
  import net from "node:net";
5
5
  import { MAX_PROGRESS_EVENTS_PER_ATTEMPT } from "../mcp/report-progress.js";
@@ -27,6 +27,12 @@ const STARTUP_POLL_MS = 250;
27
27
  /** 单轮流式 assistant 文本字节上限。 */
28
28
  const SSE_TEXT_CAP = 1 * 1024 * 1024;
29
29
  const VISION_MODEL_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,159}$/;
30
+ const MANAGED_RUNTIME_DIR_NAME = ".botlearn-deepseek-runtime";
31
+ const COMPACTION_REQUIRED_MARKER = "deepseek-native-compaction-required";
32
+ const DEFAULT_DEEPSEEK_CONTEXT_WINDOW_TOKENS = 1_000_000;
33
+ const LEGACY_DEEPSEEK_CONTEXT_WINDOW_TOKENS = 128_000;
34
+ const UNKNOWN_MODEL_COMPACTION_THRESHOLD_TOKENS = 102_400;
35
+ const COMPACTION_THRESHOLD_PERCENT = 80;
30
36
  function createManagedVisionConfig(opts, progressMcpConfig) {
31
37
  const model = opts.env?.BOTLEARN_DEEPSEEK_VISION_MODEL?.trim();
32
38
  if (!model)
@@ -161,6 +167,8 @@ export class DeepseekTuiAdapter {
161
167
  let countedInFlight = false;
162
168
  let releaseTurn;
163
169
  const managedActivationId = this.managedActivationId(opts);
170
+ let threadId = opts.sessionId?.trim() || "";
171
+ let fallbackUsed = false;
164
172
  try {
165
173
  // The local server has a process-level kill fallback when turn-scoped interrupt
166
174
  // fails. Serialize turns so cancelling one run can never terminate another run.
@@ -176,10 +184,6 @@ export class DeepseekTuiAdapter {
176
184
  if (handle.idleTimer)
177
185
  clearTimeout(handle.idleTimer);
178
186
  const headers = authHeaders(handle.token);
179
- // Agent Service model credentials are activation-scoped. The local DeepSeek
180
- // server reads them only at process startup, so its native thread cache cannot
181
- // safely cross activations; durable Course context rebuilds the new thread.
182
- let threadId = managedActivationId ? "" : (opts.sessionId?.trim() || "");
183
187
  if (threadId && !isValidThreadId(threadId)) {
184
188
  return {
185
189
  text: "",
@@ -187,39 +191,93 @@ export class DeepseekTuiAdapter {
187
191
  error: "deepseek-tui: invalid sessionId",
188
192
  };
189
193
  }
190
- if (!threadId) {
191
- threadId = await this.createThread(handle.baseUrl, headers, opts, turnAbort.signal);
194
+ const resumedThreadId = threadId;
195
+ let turnOpts = opts;
196
+ let runResult;
197
+ let maintenanceUsage;
198
+ while (true) {
199
+ try {
200
+ if (!threadId) {
201
+ threadId = await this.createThread(handle.baseUrl, headers, opts, turnAbort.signal);
202
+ }
203
+ else {
204
+ if (opts.systemContext !== undefined) {
205
+ await this.patchThreadSystemContext(handle.baseUrl, headers, threadId, opts.systemContext, turnAbort.signal);
206
+ }
207
+ if (managedActivationId && this.compactionRequired(opts)) {
208
+ const compacted = await this.compactThread({
209
+ baseUrl: handle.baseUrl,
210
+ headers,
211
+ threadId,
212
+ opts,
213
+ signal: turnAbort.signal,
214
+ handle,
215
+ });
216
+ maintenanceUsage = compacted.usage;
217
+ if (compacted.error) {
218
+ throw new Error(`native compaction failed: ${compacted.error}`);
219
+ }
220
+ }
221
+ }
222
+ runResult = await this.startTurnAndReadEvents({
223
+ baseUrl: handle.baseUrl,
224
+ headers,
225
+ threadId,
226
+ opts: turnOpts,
227
+ signal: turnAbort.signal,
228
+ handle,
229
+ });
230
+ break;
231
+ }
232
+ catch (error) {
233
+ // A persisted native thread is a recoverable cache over the authoritative Course
234
+ // conversation. Rebuild it in the same learner turn only when DeepSeek explicitly
235
+ // confirms that the old thread is missing or expired.
236
+ if (!fallbackUsed
237
+ && Boolean(resumedThreadId)
238
+ && threadId === resumedThreadId
239
+ && isMissingThreadHttpError(error)) {
240
+ fallbackUsed = true;
241
+ threadId = "";
242
+ maintenanceUsage = undefined;
243
+ if (managedActivationId)
244
+ this.clearCompactionRequired(opts);
245
+ turnOpts = {
246
+ ...opts,
247
+ text: opts.recoveryText ?? opts.text,
248
+ };
249
+ continue;
250
+ }
251
+ throw error;
252
+ }
192
253
  }
193
- else if (opts.systemContext !== undefined) {
194
- await this.patchThreadSystemContext(handle.baseUrl, headers, threadId, opts.systemContext, turnAbort.signal);
254
+ if (managedActivationId
255
+ && runResult.usage?.input_tokens !== undefined
256
+ && runResult.usage.input_tokens >= deepseekCompactionThreshold(runResult.model ?? parseDeepseekRuntimeSelection(opts.extraArgs).model)) {
257
+ this.markCompactionRequired(opts);
195
258
  }
196
- const runResult = await this.startTurnAndReadEvents({
197
- baseUrl: handle.baseUrl,
198
- headers,
199
- threadId,
200
- opts,
201
- signal: turnAbort.signal,
202
- handle,
203
- });
204
259
  const text = runResult.text;
205
260
  const error = runResult.error ?? (text === "" ? emptyCompletionError(handle.stderrTail) : undefined);
261
+ const usage = mergeRuntimeUsage(maintenanceUsage, runResult.usage);
206
262
  return {
207
263
  text,
208
- newSessionId: managedActivationId ? "" : threadId,
264
+ newSessionId: threadId,
209
265
  ...(runResult.progressDispositions
210
266
  ? { progressDispositions: runResult.progressDispositions }
211
267
  : {}),
212
- ...(runResult.usage ? { usage: runResult.usage } : {}),
268
+ ...(usage ? { usage } : {}),
213
269
  ...(error ? { error } : {}),
214
270
  };
215
271
  }
216
272
  catch (err) {
217
273
  const message = err instanceof Error ? err.message : String(err);
218
- // 服务端明确确认线程不存在/已过期时才清空 sessionId,让下一轮从 durable context 重建。
219
- const staleSession = Boolean(opts.sessionId) && isMissingThreadHttpError(err);
274
+ // Only an explicit 404/410 discards the native id. Transient failures preserve it.
275
+ const staleSession = Boolean(threadId) && isMissingThreadHttpError(err);
220
276
  return {
221
277
  text: "",
222
- newSessionId: managedActivationId || staleSession ? "" : (opts.sessionId ?? ""),
278
+ newSessionId: staleSession || (fallbackUsed && !threadId)
279
+ ? ""
280
+ : (threadId || opts.sessionId || ""),
223
281
  error: `deepseek-tui: ${message}`,
224
282
  };
225
283
  }
@@ -355,8 +413,9 @@ export class DeepseekTuiAdapter {
355
413
  return handle;
356
414
  }
357
415
  /**
358
- * 不设置 DEEPSEEK_RUNTIME_DIR:BYOA server 可跨 run 池化,直接使用用户本机
359
- * deepseek 默认状态目录(含已登录凭据);Agent Service server activation 回收。
416
+ * BYOA continues to use the user's default DeepSeek state directory. Managed mode
417
+ * pins the durable RuntimeThreadStore to the RuntimeSession workspace even though
418
+ * the credential-bearing server process is still reclaimed after every activation.
360
419
  */
361
420
  spawnEnv(opts, progressMcpConfigPath, visionConfigPath) {
362
421
  const env = {
@@ -368,8 +427,40 @@ export class DeepseekTuiAdapter {
368
427
  env.DEEPSEEK_MCP_CONFIG = progressMcpConfigPath;
369
428
  if (visionConfigPath)
370
429
  env.DEEPSEEK_CONFIG_PATH = visionConfigPath;
430
+ if (this.managedActivationId(opts)) {
431
+ env.DEEPSEEK_RUNTIME_DIR = this.prepareManagedRuntimeDir(opts);
432
+ }
371
433
  return env;
372
434
  }
435
+ prepareManagedRuntimeDir(opts) {
436
+ const runtimeDir = path.join(opts.cwd, MANAGED_RUNTIME_DIR_NAME);
437
+ mkdirSync(runtimeDir, { recursive: true, mode: 0o770 });
438
+ const stat = lstatSync(runtimeDir);
439
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
440
+ throw new Error("deepseek-tui: managed runtime directory is not a real directory");
441
+ }
442
+ // The control process owns the directory; the fixed runtime group needs write access.
443
+ chmodSync(runtimeDir, 0o770);
444
+ return runtimeDir;
445
+ }
446
+ compactionMarkerPath(opts) {
447
+ if (!opts.runtimeStateDir) {
448
+ throw new Error("deepseek-tui: managed RuntimeSession state directory is unavailable");
449
+ }
450
+ return path.join(opts.runtimeStateDir, COMPACTION_REQUIRED_MARKER);
451
+ }
452
+ compactionRequired(opts) {
453
+ return existsSync(this.compactionMarkerPath(opts));
454
+ }
455
+ markCompactionRequired(opts) {
456
+ writeFileSync(this.compactionMarkerPath(opts), "required\n", {
457
+ encoding: "utf8",
458
+ mode: 0o600,
459
+ });
460
+ }
461
+ clearCompactionRequired(opts) {
462
+ rmSync(this.compactionMarkerPath(opts), { force: true });
463
+ }
373
464
  managedActivationId(opts) {
374
465
  if (this.explicitServerUrl)
375
466
  return null;
@@ -418,6 +509,74 @@ export class DeepseekTuiAdapter {
418
509
  signal,
419
510
  });
420
511
  }
512
+ async compactThread(args) {
513
+ const { baseUrl, headers, threadId, opts, signal, handle } = args;
514
+ const eventsUrl = `${baseUrl}/v1/threads/${encodeURIComponent(threadId)}/events?since_seq=0`;
515
+ const eventsAbort = new AbortController();
516
+ let turnId = "";
517
+ let interruptPromise;
518
+ const interrupt = () => {
519
+ if (!turnId)
520
+ return Promise.resolve();
521
+ interruptPromise ??= this.interruptTurn(baseUrl, headers, threadId, turnId).catch((error) => {
522
+ log.warn("deepseek-tui compaction interrupt failed", {
523
+ error: error instanceof Error ? error.message : String(error),
524
+ });
525
+ if (!this.explicitServerUrl)
526
+ shutdownHandle(handle, "compaction-interrupt-failed");
527
+ });
528
+ return interruptPromise;
529
+ };
530
+ const onAbort = () => {
531
+ eventsAbort.abort();
532
+ void interrupt();
533
+ };
534
+ signal.addEventListener("abort", onAbort, { once: true });
535
+ let eventsError;
536
+ const quietOpts = {
537
+ ...opts,
538
+ onBlock: () => undefined,
539
+ onStatus: () => undefined,
540
+ };
541
+ const eventsReaderPromise = this.readEvents(eventsUrl, headers, quietOpts, eventsAbort.signal).catch((error) => {
542
+ eventsError = error;
543
+ return null;
544
+ });
545
+ try {
546
+ if (signal.aborted)
547
+ throw abortReason(signal);
548
+ const started = await this.requestJson(`${baseUrl}/v1/threads/${encodeURIComponent(threadId)}/compact`, {
549
+ method: "POST",
550
+ headers,
551
+ body: JSON.stringify({
552
+ reason: "BotLearn automatic long-context compaction",
553
+ }),
554
+ signal: AbortSignal.timeout(5_000),
555
+ });
556
+ turnId = stringField(started?.turn, "id") ?? stringField(started, "turn_id") ?? "";
557
+ if (signal.aborted)
558
+ await interrupt();
559
+ const eventsReader = await eventsReaderPromise;
560
+ if (!eventsReader)
561
+ throw eventsError ?? new Error("compaction events stream failed");
562
+ const result = await eventsReader(turnId);
563
+ const model = stringField(started?.thread, "model");
564
+ return {
565
+ ...result,
566
+ ...(model ? { model } : {}),
567
+ };
568
+ }
569
+ finally {
570
+ if (signal.aborted) {
571
+ if (turnId)
572
+ await interrupt();
573
+ else if (!this.explicitServerUrl)
574
+ shutdownHandle(handle, "cancelled-before-compaction-id");
575
+ }
576
+ eventsAbort.abort();
577
+ signal.removeEventListener("abort", onAbort);
578
+ }
579
+ }
421
580
  async startTurnAndReadEvents(args) {
422
581
  const { baseUrl, headers, threadId, opts, signal, handle } = args;
423
582
  // 事件流必须先于 turn 打开,否则 turn 早期事件会丢。
@@ -476,7 +635,12 @@ export class DeepseekTuiAdapter {
476
635
  const eventsReader = await eventsReaderPromise;
477
636
  if (!eventsReader)
478
637
  throw eventsError ?? new Error("events stream failed");
479
- return await eventsReader(turnId);
638
+ const result = await eventsReader(turnId);
639
+ const model = stringField(started?.thread, "model");
640
+ return {
641
+ ...result,
642
+ ...(model ? { model } : {}),
643
+ };
480
644
  }
481
645
  finally {
482
646
  if (signal.aborted) {
@@ -754,6 +918,55 @@ export function extractDeepseekUsage(payload) {
754
918
  ...(requestId ? { provider_request_ids: [requestId] } : {}),
755
919
  };
756
920
  }
921
+ /** Mirrors DeepSeek TUI 0.8.39's model-window compaction threshold selection. */
922
+ function deepseekCompactionThreshold(model) {
923
+ if (!model)
924
+ return UNKNOWN_MODEL_COMPACTION_THRESHOLD_TOKENS;
925
+ const lower = model.toLowerCase();
926
+ let contextWindow;
927
+ if (lower.includes("deepseek")) {
928
+ const explicit = lower.match(/(?:^|[^a-z0-9])(\d{1,4})k(?:$|[^a-z0-9])/);
929
+ const kiloTokens = explicit?.[1] ? Number(explicit[1]) : Number.NaN;
930
+ if (Number.isInteger(kiloTokens) && kiloTokens >= 8 && kiloTokens <= 1024) {
931
+ contextWindow = kiloTokens * 1_000;
932
+ }
933
+ else {
934
+ contextWindow = lower.includes("v4")
935
+ ? DEFAULT_DEEPSEEK_CONTEXT_WINDOW_TOKENS
936
+ : LEGACY_DEEPSEEK_CONTEXT_WINDOW_TOKENS;
937
+ }
938
+ }
939
+ else if (lower.includes("claude")) {
940
+ contextWindow = 200_000;
941
+ }
942
+ if (contextWindow === undefined)
943
+ return UNKNOWN_MODEL_COMPACTION_THRESHOLD_TOKENS;
944
+ return Math.floor((contextWindow * COMPACTION_THRESHOLD_PERCENT) / 100);
945
+ }
946
+ function mergeRuntimeUsage(first, second) {
947
+ if (!first)
948
+ return second;
949
+ if (!second)
950
+ return first;
951
+ const sum = (left, right) => left !== undefined || right !== undefined ? (left ?? 0) + (right ?? 0) : undefined;
952
+ const requestIds = [...new Set([
953
+ ...(first.provider_request_ids ?? []),
954
+ ...(second.provider_request_ids ?? []),
955
+ ])];
956
+ const input = sum(first.input_tokens, second.input_tokens);
957
+ const cached = sum(first.cached_input_tokens, second.cached_input_tokens);
958
+ const output = sum(first.output_tokens, second.output_tokens);
959
+ const total = sum(first.total_tokens, second.total_tokens);
960
+ const cost = sum(first.cost_usd, second.cost_usd);
961
+ return {
962
+ ...(input !== undefined ? { input_tokens: input } : {}),
963
+ ...(cached !== undefined ? { cached_input_tokens: cached } : {}),
964
+ ...(output !== undefined ? { output_tokens: output } : {}),
965
+ ...(total !== undefined ? { total_tokens: total } : {}),
966
+ ...(cost !== undefined ? { cost_usd: cost } : {}),
967
+ ...(requestIds.length > 0 ? { provider_request_ids: requestIds } : {}),
968
+ };
969
+ }
757
970
  function isToolStarted(eventName, payload) {
758
971
  const itemKind = payload?.payload?.item?.kind ?? payload?.item?.kind;
759
972
  return ((eventName === "item.started" &&
@@ -34,9 +34,13 @@ export type RuntimeStatusEvent = {
34
34
  };
35
35
  export interface EngineRunOptions {
36
36
  text: string;
37
+ /** Full durable conversation bootstrap, used only if a resumed native thread is missing. */
38
+ recoveryText?: string;
37
39
  /** runtime 原生会话 id(resume 用);null 表示新会话。 */
38
40
  sessionId: string | null;
39
41
  cwd: string;
42
+ /** Daemon-owned state directory for runtime metadata that the model must not mutate. */
43
+ runtimeStateDir?: string;
40
44
  signal: AbortSignal;
41
45
  extraArgs?: string[];
42
46
  systemContext?: string;
@@ -152,14 +152,14 @@ export function wrapEngineAdapter(id, engine, opts) {
152
152
  const payload = run.payload;
153
153
  // DeepSeek persists and replays the native thread history. The durable Course Service
154
154
  // conversation is recovery/bootstrap data, not a second history to inject on every turn.
155
- // Agent Service activations intentionally start a fresh local server because their model
156
- // credential expires with the turn, so a cached thread id cannot suppress durable context.
157
- const managedActivation = Boolean(run.runtimeEnv?.BOTLEARN_AGENT_SERVICE_ACTIVATION_ID?.trim());
158
- const resumesDeepseekThread = id === "deepseek-tui" && !managedActivation && Boolean(run.nativeSessionId?.trim());
155
+ const resumesDeepseekThread = id === "deepseek-tui" && Boolean(run.nativeSessionId?.trim());
159
156
  const learnerInput = resumesDeepseekThread
160
157
  ? renderCurrentLearnerRequest(payload)
161
158
  : renderConversationInput(payload);
162
159
  const text = renderInputAttachments(run, learnerInput);
160
+ const recoveryText = resumesDeepseekThread
161
+ ? renderInputAttachments(run, renderConversationInput(payload))
162
+ : undefined;
163
163
  if (!text.trim()) {
164
164
  throw new RuntimeExecutionError("empty task brief");
165
165
  }
@@ -190,8 +190,10 @@ export function wrapEngineAdapter(id, engine, opts) {
190
190
  };
191
191
  const result = await engine.run({
192
192
  text,
193
+ ...(recoveryText !== undefined ? { recoveryText } : {}),
193
194
  sessionId: run.nativeSessionId ?? null,
194
195
  cwd: run.workspaceDir,
196
+ ...(run.runtimeStateDir ? { runtimeStateDir: run.runtimeStateDir } : {}),
195
197
  signal,
196
198
  ...(run.runtimeEnv ? { env: run.runtimeEnv } : {}),
197
199
  ...(extraArgs.length > 0 ? { extraArgs } : {}),
package/dist/types.d.ts CHANGED
@@ -185,6 +185,8 @@ export interface RunExecution {
185
185
  payload: RunStartPayload;
186
186
  /** Runtime cwd. Managed persistent sessions reuse one session-scoped workspace. */
187
187
  workspaceDir: string;
188
+ /** Daemon-owned state directory for one managed RuntimeSession; never exposed to the model. */
189
+ runtimeStateDir?: string;
188
190
  /** Runtime-native thread/session id to resume; null creates the first native session. */
189
191
  nativeSessionId?: string | null;
190
192
  /** Monotonic Course Service context revision accepted for this turn. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botlearn-course/daemon",
3
- "version": "0.0.13-beta.1",
3
+ "version": "0.0.13",
4
4
  "description": "Lightweight BotLearn Course daemon: run course tasks on your own machine with your own agent runtime (BYOA).",
5
5
  "type": "module",
6
6
  "bin": {