@reconcrap/boss-recommend-mcp 2.1.23 → 2.1.25

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 (67) hide show
  1. package/README.md +5 -0
  2. package/bin/boss-recommend-mcp.js +4 -4
  3. package/package.json +6 -1
  4. package/scripts/install-macos.sh +280 -280
  5. package/scripts/postinstall.cjs +44 -44
  6. package/skills/boss-chat/README.md +43 -42
  7. package/skills/boss-chat/SKILL.md +107 -106
  8. package/skills/boss-recommend-pipeline/README.md +13 -13
  9. package/skills/boss-recommend-pipeline/SKILL.md +47 -47
  10. package/skills/boss-recruit-pipeline/README.md +19 -19
  11. package/skills/boss-recruit-pipeline/SKILL.md +89 -89
  12. package/src/chat-mcp.js +301 -127
  13. package/src/chat-runtime-config.js +7 -5
  14. package/src/core/boss-cards/index.js +199 -199
  15. package/src/core/browser/index.js +302 -145
  16. package/src/core/capture/index.js +2930 -1201
  17. package/src/core/cv-acquisition/index.js +512 -238
  18. package/src/core/cv-capture-target/index.js +513 -299
  19. package/src/core/greet-quota/index.js +71 -71
  20. package/src/core/infinite-list/index.js +31 -31
  21. package/src/core/reporting/legacy-csv.js +12 -12
  22. package/src/core/run/detached-launcher.js +305 -0
  23. package/src/core/run/index.js +109 -55
  24. package/src/core/run/timing.js +33 -33
  25. package/src/core/run/windows-detached-worker.ps1 +106 -0
  26. package/src/core/screening/index.js +2400 -2135
  27. package/src/core/self-heal/index.js +989 -973
  28. package/src/core/self-heal/viewport.js +1505 -564
  29. package/src/detached-worker.js +99 -99
  30. package/src/domains/chat/action-journal.js +536 -0
  31. package/src/domains/chat/cards.js +137 -137
  32. package/src/domains/chat/constants.js +9 -9
  33. package/src/domains/chat/detail.js +1684 -401
  34. package/src/domains/chat/index.js +8 -7
  35. package/src/domains/chat/jobs.js +620 -620
  36. package/src/domains/chat/page-guard.js +157 -122
  37. package/src/domains/chat/roots.js +56 -56
  38. package/src/domains/chat/run-service.js +1801 -760
  39. package/src/domains/common/account-rights-panel.js +314 -314
  40. package/src/domains/common/recovery-settle.js +159 -159
  41. package/src/domains/recommend/actions.js +1219 -472
  42. package/src/domains/recommend/cards.js +243 -243
  43. package/src/domains/recommend/colleague-contact.js +1079 -333
  44. package/src/domains/recommend/constants.js +92 -92
  45. package/src/domains/recommend/detail.js +4037 -136
  46. package/src/domains/recommend/filters.js +608 -590
  47. package/src/domains/recommend/index.js +9 -9
  48. package/src/domains/recommend/jobs.js +571 -542
  49. package/src/domains/recommend/location.js +754 -707
  50. package/src/domains/recommend/refresh.js +677 -392
  51. package/src/domains/recommend/roots.js +80 -80
  52. package/src/domains/recommend/run-service.js +4048 -1447
  53. package/src/domains/recommend/scopes.js +246 -246
  54. package/src/domains/recruit/actions.js +277 -277
  55. package/src/domains/recruit/cards.js +74 -74
  56. package/src/domains/recruit/constants.js +236 -236
  57. package/src/domains/recruit/detail.js +588 -588
  58. package/src/domains/recruit/index.js +9 -9
  59. package/src/domains/recruit/instruction-parser.js +866 -866
  60. package/src/domains/recruit/refresh.js +45 -45
  61. package/src/domains/recruit/roots.js +68 -68
  62. package/src/domains/recruit/run-service.js +1817 -1620
  63. package/src/domains/recruit/search.js +3229 -3229
  64. package/src/index.js +16 -1
  65. package/src/parser.js +1296 -1296
  66. package/src/recommend-mcp.js +1061 -450
  67. package/src/recommend-scheduler.js +75 -75
package/src/chat-mcp.js CHANGED
@@ -1,8 +1,8 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import process from "node:process";
4
- import { spawn } from "node:child_process";
5
- import { fileURLToPath } from "node:url";
1
+ import fs from "node:fs";
2
+ import { randomUUID } from "node:crypto";
3
+ import path from "node:path";
4
+ import process from "node:process";
5
+ import { fileURLToPath } from "node:url";
6
6
  import {
7
7
  assertNoForbiddenCdpCalls,
8
8
  bringPageToFront,
@@ -34,8 +34,10 @@ import {
34
34
  resolveChatSelfHealRoots,
35
35
  runSelfHealCheck
36
36
  } from "./core/self-heal/index.js";
37
- import {
38
- CHAT_TARGET_URL,
37
+ import {
38
+ CHAT_COLLECT_CV_PROCESSING_FLOOR_MAX_MS,
39
+ CHAT_COLLECT_CV_PROCESSING_FLOOR_MIN_MS,
40
+ CHAT_TARGET_URL,
39
41
  closeChatResumeModal,
40
42
  closeChatJobDropdown,
41
43
  createChatRunService,
@@ -54,7 +56,8 @@ import {
54
56
  resolveHumanBehaviorForRun,
55
57
  resolveBossScreeningConfig
56
58
  } from "./chat-runtime-config.js";
57
- import { DEFAULT_MAX_IMAGE_PAGES } from "./core/cv-acquisition/index.js";
59
+ import { DEFAULT_MAX_IMAGE_PAGES } from "./core/cv-acquisition/index.js";
60
+ import { launchDetachedWorker } from "./core/run/detached-launcher.js";
58
61
 
59
62
  const DEFAULT_CHAT_HOST = "127.0.0.1";
60
63
  const DEFAULT_CHAT_PORT = 9222;
@@ -85,11 +88,15 @@ const ARTIFACT_STATUSES = new Set([
85
88
  RUN_STATUS_PAUSED
86
89
  ]);
87
90
 
88
- const STALE_PROCESS_STATUSES = new Set([
89
- "queued",
90
- "running",
91
- RUN_STATUS_CANCELING
92
- ]);
91
+ const STALE_PROCESS_STATUSES = new Set([
92
+ "queued",
93
+ "running",
94
+ RUN_STATUS_CANCELING
95
+ ]);
96
+
97
+ const CHAT_ATOMIC_RENAME_RETRY_CODES = new Set(["EPERM", "EACCES", "EBUSY"]);
98
+ const CHAT_ATOMIC_RENAME_RETRY_DELAYS_MS = Object.freeze([25, 50, 100, 200, 400, 500]);
99
+ const CHAT_ATOMIC_RENAME_SLEEP_CELL = new Int32Array(new SharedArrayBuffer(4));
93
100
 
94
101
  const CHAT_REQUEST_RESUME_ACTIONS = new Set([
95
102
  "request_cv",
@@ -115,10 +122,11 @@ const CHAT_DISABLE_REQUEST_RESUME_ACTIONS = new Set([
115
122
  "不请求简历"
116
123
  ]);
117
124
 
118
- let chatWorkflowImpl = runChatWorkflow;
119
- let chatConnectorImpl = connectChatChromeSession;
120
- let chatJobReaderImpl = readChatJobOptionsFromSession;
121
- let chatRunService = createChatRunService({
125
+ let chatWorkflowImpl = runChatWorkflow;
126
+ let chatConnectorImpl = connectChatChromeSession;
127
+ let chatJobReaderImpl = readChatJobOptionsFromSession;
128
+ let chatDetachedWorkerLauncherImpl = launchDetachedWorker;
129
+ let chatRunService = createChatRunService({
122
130
  idPrefix: "mcp_chat",
123
131
  workflow: (...args) => chatWorkflowImpl(...args),
124
132
  onSnapshot: persistChatLifecycleSnapshot
@@ -187,12 +195,60 @@ function ensureDirectory(dirPath) {
187
195
  fs.mkdirSync(dirPath, { recursive: true });
188
196
  }
189
197
 
190
- function writeJsonAtomic(filePath, payload) {
191
- ensureDirectory(path.dirname(filePath));
192
- const tempPath = `${filePath}.tmp`;
193
- fs.writeFileSync(tempPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
194
- fs.renameSync(tempPath, filePath);
195
- }
198
+ function sleepSync(milliseconds) {
199
+ const delayMs = Math.max(0, Math.floor(Number(milliseconds) || 0));
200
+ if (delayMs <= 0) return;
201
+ Atomics.wait(CHAT_ATOMIC_RENAME_SLEEP_CELL, 0, 0, delayMs);
202
+ }
203
+
204
+ function writeJsonAtomic(filePath, payload, {
205
+ renameSyncImpl = fs.renameSync,
206
+ sleepSyncImpl = sleepSync,
207
+ randomUUIDImpl = randomUUID
208
+ } = {}) {
209
+ ensureDirectory(path.dirname(filePath));
210
+ const tempPath = `${filePath}.${process.pid}.${randomUUIDImpl()}.tmp`;
211
+ let renameAttempts = 0;
212
+ const retryDelays = [];
213
+ try {
214
+ fs.writeFileSync(tempPath, `${JSON.stringify(payload, null, 2)}\n`, {
215
+ encoding: "utf8",
216
+ flag: "wx",
217
+ mode: 0o600
218
+ });
219
+ while (true) {
220
+ renameAttempts += 1;
221
+ try {
222
+ renameSyncImpl(tempPath, filePath);
223
+ return;
224
+ } catch (error) {
225
+ const retryDelayMs = CHAT_ATOMIC_RENAME_RETRY_DELAYS_MS[renameAttempts - 1];
226
+ if (
227
+ !CHAT_ATOMIC_RENAME_RETRY_CODES.has(error?.code)
228
+ || retryDelayMs === undefined
229
+ ) {
230
+ try {
231
+ error.atomic_rename_attempts = renameAttempts;
232
+ error.atomic_rename_retry_delays_ms = [...retryDelays];
233
+ error.atomic_rename_source = tempPath;
234
+ error.atomic_rename_target = filePath;
235
+ } catch {
236
+ // Preserve the original filesystem error when it is not extensible.
237
+ }
238
+ throw error;
239
+ }
240
+ retryDelays.push(retryDelayMs);
241
+ sleepSyncImpl(retryDelayMs);
242
+ }
243
+ }
244
+ } finally {
245
+ try {
246
+ if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
247
+ } catch {
248
+ // Best-effort cleanup only; never hide the original persistence outcome.
249
+ }
250
+ }
251
+ }
196
252
 
197
253
  function readJsonFile(filePath) {
198
254
  try {
@@ -218,9 +274,10 @@ function selectedChatJobForCsv(meta = {}, snapshot = {}) {
218
274
  };
219
275
  }
220
276
 
221
- function buildChatCsvInputRows(snapshot = {}, meta = {}) {
222
- const normalized = meta.normalized || {};
223
- const context = snapshot.context || {};
277
+ function buildChatCsvInputRows(snapshot = {}, meta = {}) {
278
+ const normalized = meta.normalized || {};
279
+ const context = snapshot.context || {};
280
+ const processingFloor = snapshot.summary?.collect_cv_processing_floor || {};
224
281
  const postAction = shouldRequestChatResume(meta.args, context)
225
282
  ? "request_cv"
226
283
  : normalizeText(meta.args?.post_action || meta.args?.action || "") || "none";
@@ -243,12 +300,18 @@ function buildChatCsvInputRows(snapshot = {}, meta = {}) {
243
300
  max_greet_count: meta.args?.max_greet_count ?? ""
244
301
  },
245
302
  followUp: meta.args?.follow_up || null,
246
- extraRows: [
247
- ["chat_params.greeting_text", normalized.greetingText || meta.args?.greeting_text || meta.args?.greetingText || context.greeting_text || DEFAULT_CHAT_GREETING_TEXT],
248
- ["chat_params.profile", normalized.profile || meta.args?.profile || context.profile || "default"]
249
- ]
250
- });
251
- }
303
+ extraRows: [
304
+ ["chat_params.greeting_text", normalized.greetingText || meta.args?.greeting_text || meta.args?.greetingText || context.greeting_text || DEFAULT_CHAT_GREETING_TEXT],
305
+ ["chat_params.profile", normalized.profile || meta.args?.profile || context.profile || "default"],
306
+ ["chat_params.collect_cv_processing_floor_enabled", context.collect_cv_processing_floor_enabled === true],
307
+ ["chat_params.collect_cv_processing_floor_min_ms", context.collect_cv_processing_floor_min_ms ?? ""],
308
+ ["chat_params.collect_cv_processing_floor_max_ms", context.collect_cv_processing_floor_max_ms ?? ""],
309
+ ["chat_params.collect_cv_processing_floor_count", processingFloor.count ?? snapshot.progress?.collect_cv_processing_floor_count ?? 0],
310
+ ["chat_params.collect_cv_processing_floor_delay_requested_ms", processingFloor.total_delay_requested_ms ?? snapshot.progress?.collect_cv_processing_floor_delay_requested_ms ?? 0],
311
+ ["chat_params.collect_cv_processing_floor_delay_ms", processingFloor.total_delay_elapsed_ms ?? snapshot.progress?.collect_cv_processing_floor_delay_ms ?? 0]
312
+ ]
313
+ });
314
+ }
252
315
 
253
316
  function writeChatLegacyCsvAtomic(filePath, rows = [], snapshot = {}, meta = {}) {
254
317
  writeLegacyScreenCsv(filePath, {
@@ -257,11 +320,37 @@ function writeChatLegacyCsvAtomic(filePath, rows = [], snapshot = {}, meta = {})
257
320
  });
258
321
  }
259
322
 
260
- function readChatRunState(runId) {
261
- const artifacts = getChatRunArtifacts(runId);
262
- if (!artifacts) return null;
263
- return readJsonFile(artifacts.run_state_path);
264
- }
323
+ function readChatRunState(runId) {
324
+ const artifacts = getChatRunArtifacts(runId);
325
+ if (!artifacts) return null;
326
+ return readJsonFile(artifacts.run_state_path);
327
+ }
328
+
329
+ function hasChatCheckpointPayload(value) {
330
+ return Boolean(
331
+ value
332
+ && typeof value === "object"
333
+ && !Array.isArray(value)
334
+ && Object.keys(value).length > 0
335
+ );
336
+ }
337
+
338
+ function readHydratedChatCheckpoint(persisted = {}) {
339
+ const runId = normalizeRunId(persisted.run_id || persisted.runId);
340
+ const artifacts = getChatRunArtifacts(runId);
341
+ const checkpointPaths = [
342
+ persisted.resume?.checkpoint_path,
343
+ persisted.artifacts?.checkpoint_path,
344
+ artifacts?.checkpoint_path
345
+ ].map((value) => normalizeText(value)).filter(Boolean);
346
+ for (const checkpointPath of new Set(checkpointPaths)) {
347
+ const checkpoint = readJsonFile(checkpointPath);
348
+ if (checkpoint) return checkpoint;
349
+ }
350
+ return persisted.checkpoint && typeof persisted.checkpoint === "object"
351
+ ? persisted.checkpoint
352
+ : {};
353
+ }
265
354
 
266
355
  function writeChatRunState(runId, payload) {
267
356
  const artifacts = getChatRunArtifacts(runId);
@@ -281,10 +370,13 @@ function buildInitialChatDetachedState(runId, {
281
370
  pid = process.pid
282
371
  } = {}) {
283
372
  const artifacts = getChatRunArtifacts(runId);
284
- const now = new Date().toISOString();
285
- const isAllTarget = normalized.publicTargetCount === "all";
286
- const processedLimit = isAllTarget ? CHAT_ALL_MAX_CANDIDATES : Math.max(1, Number(normalized.targetCount) || 1);
287
- return {
373
+ const now = new Date().toISOString();
374
+ const isAllTarget = normalized.publicTargetCount === "all";
375
+ const processedLimit = isAllTarget ? CHAT_ALL_MAX_CANDIDATES : Math.max(1, Number(normalized.targetCount) || 1);
376
+ const criteria = normalizeText(normalized.criteria || args.criteria || "");
377
+ const screeningMode = normalizeScreeningModeArg(args, { ...normalized, criteria });
378
+ const collectCvProcessingFloorEnabled = screeningMode === "collect_cv";
379
+ return {
288
380
  run_id: runId,
289
381
  mode: RUN_MODE_ASYNC,
290
382
  state: "queued",
@@ -306,9 +398,16 @@ function buildInitialChatDetachedState(runId, {
306
398
  passed: 0,
307
399
  skipped: 0,
308
400
  requested: 0,
309
- request_satisfied: 0,
310
- request_skipped: 0,
311
- greet_count: 0
401
+ request_satisfied: 0,
402
+ request_skipped: 0,
403
+ greet_count: 0,
404
+ collect_cv_processing_floor_enabled: collectCvProcessingFloorEnabled,
405
+ collect_cv_processing_floor_min_ms: collectCvProcessingFloorEnabled ? CHAT_COLLECT_CV_PROCESSING_FLOOR_MIN_MS : null,
406
+ collect_cv_processing_floor_max_ms: collectCvProcessingFloorEnabled ? CHAT_COLLECT_CV_PROCESSING_FLOOR_MAX_MS : null,
407
+ collect_cv_processing_floor_count: 0,
408
+ collect_cv_processing_floor_delay_requested_ms: 0,
409
+ collect_cv_processing_floor_delay_ms: 0,
410
+ collect_cv_processing_floor_current: null
312
411
  },
313
412
  last_message: "Boss chat detached worker is queued.",
314
413
  context: {
@@ -318,7 +417,13 @@ function buildInitialChatDetachedState(runId, {
318
417
  profile: normalized.profile || args.profile || "default",
319
418
  job: normalized.job || args.job || "",
320
419
  start_from: normalized.startFrom || args.start_from || "",
321
- criteria: normalized.criteria || args.criteria || "",
420
+ criteria,
421
+ criteria_present: Boolean(criteria),
422
+ screening_mode: screeningMode,
423
+ cv_collection_mode: collectCvProcessingFloorEnabled,
424
+ collect_cv_processing_floor_enabled: collectCvProcessingFloorEnabled,
425
+ collect_cv_processing_floor_min_ms: collectCvProcessingFloorEnabled ? CHAT_COLLECT_CV_PROCESSING_FLOOR_MIN_MS : null,
426
+ collect_cv_processing_floor_max_ms: collectCvProcessingFloorEnabled ? CHAT_COLLECT_CV_PROCESSING_FLOOR_MAX_MS : null,
322
427
  greeting_text: normalized.greetingText || args.greeting_text || args.greetingText || DEFAULT_CHAT_GREETING_TEXT,
323
428
  target_count: normalized.publicTargetCount ?? normalized.targetCount ?? null,
324
429
  target_count_semantics: TARGET_COUNT_SEMANTICS,
@@ -385,33 +490,42 @@ function patchPersistedChatControl(runId, controlPatch = {}, {
385
490
  };
386
491
  }
387
492
 
388
- function launchDetachedChatWorker(runId) {
389
- const artifacts = getChatRunArtifacts(runId);
390
- if (!artifacts) throw new Error("Invalid chat run_id");
391
- fs.mkdirSync(path.dirname(artifacts.worker_stdout_path), { recursive: true });
392
- const stdoutFd = fs.openSync(artifacts.worker_stdout_path, "a");
393
- const stderrFd = fs.openSync(artifacts.worker_stderr_path, "a");
394
- let child;
395
- try {
396
- child = spawn(process.execPath, [
397
- DETACHED_WORKER_SCRIPT,
398
- "--domain",
399
- "chat",
400
- "--run-id",
401
- runId
402
- ], {
403
- detached: true,
404
- stdio: ["ignore", stdoutFd, stderrFd],
405
- windowsHide: true,
406
- env: process.env
407
- });
408
- } finally {
409
- fs.closeSync(stdoutFd);
410
- fs.closeSync(stderrFd);
411
- }
412
- if (typeof child?.unref === "function") child.unref();
413
- return child;
414
- }
493
+ function launchDetachedChatWorker(runId, { screenConfigPath = "" } = {}) {
494
+ const artifacts = getChatRunArtifacts(runId);
495
+ if (!artifacts) throw new Error("Invalid chat run_id");
496
+ const child = chatDetachedWorkerLauncherImpl({
497
+ nodePath: process.execPath,
498
+ workerScriptPath: DETACHED_WORKER_SCRIPT,
499
+ domain: "chat",
500
+ runId,
501
+ stdoutPath: artifacts.worker_stdout_path,
502
+ stderrPath: artifacts.worker_stderr_path,
503
+ chatRuntimeHomePath: getBossChatDataDir(),
504
+ screenConfigPath,
505
+ environment: process.env
506
+ });
507
+ if (typeof child?.once === "function") {
508
+ child.once("error", (error) => {
509
+ markBossChatDetachedWorkerFailed(runId, error, {
510
+ code: "CHAT_WORKER_PROCESS_ERROR",
511
+ message: "Boss chat detached worker process emitted an asynchronous error."
512
+ });
513
+ });
514
+ child.once("exit", (exitCode, signal) => {
515
+ const normalizedSignal = normalizeText(signal);
516
+ const exitLabel = Number.isInteger(exitCode) ? `code=${exitCode}` : "code=unknown";
517
+ const signalLabel = normalizedSignal ? `, signal=${normalizedSignal}` : "";
518
+ const error = new Error(`Boss chat detached worker exited before writing a terminal state (${exitLabel}${signalLabel}).`);
519
+ markBossChatDetachedWorkerFailed(runId, error, {
520
+ code: "CHAT_WORKER_EXITED_EARLY",
521
+ workerExitCode: Number.isInteger(exitCode) ? exitCode : null,
522
+ workerSignal: normalizedSignal || null
523
+ });
524
+ });
525
+ }
526
+ if (typeof child?.unref === "function") child.unref();
527
+ return child;
528
+ }
415
529
 
416
530
  function toIsoOrNull(value) {
417
531
  const normalized = normalizeText(value);
@@ -500,16 +614,22 @@ function getChatRunMeta(runId) {
500
614
  return chatRunMeta.get(runId) || {};
501
615
  }
502
616
 
503
- function ensureChatRunArtifacts(snapshot) {
617
+ function ensureChatRunArtifacts(snapshot) {
504
618
  const artifacts = getChatRunArtifacts(snapshot?.runId || snapshot?.run_id);
505
619
  if (!artifacts) return null;
506
620
 
507
621
  const meta = getChatRunMeta(snapshot?.runId || snapshot?.run_id);
508
- const checkpoint = snapshot?.checkpoint && typeof snapshot.checkpoint === "object"
509
- ? snapshot.checkpoint
510
- : {};
511
- writeJsonAtomic(artifacts.checkpoint_path, checkpoint);
512
- if (meta) meta.checkpointPath = artifacts.checkpoint_path;
622
+ const snapshotCheckpoint = snapshot?.checkpoint && typeof snapshot.checkpoint === "object"
623
+ ? snapshot.checkpoint
624
+ : {};
625
+ const existingCheckpoint = readJsonFile(artifacts.checkpoint_path);
626
+ const checkpoint = hasChatCheckpointPayload(snapshotCheckpoint)
627
+ ? snapshotCheckpoint
628
+ : existingCheckpoint || snapshotCheckpoint;
629
+ if (hasChatCheckpointPayload(snapshotCheckpoint) || !existingCheckpoint) {
630
+ writeJsonAtomic(artifacts.checkpoint_path, checkpoint);
631
+ }
632
+ if (meta) meta.checkpointPath = artifacts.checkpoint_path;
513
633
 
514
634
  const summary = snapshot?.summary && typeof snapshot.summary === "object" ? snapshot.summary : null;
515
635
  const checkpointResults = Array.isArray(checkpoint.results) ? checkpoint.results : [];
@@ -547,16 +667,21 @@ function ensureChatRunArtifacts(snapshot) {
547
667
  return artifacts;
548
668
  }
549
669
 
550
- function persistChatCheckpointSnapshot(normalized) {
551
- const artifacts = getChatRunArtifacts(normalized?.run_id || normalized?.runId);
552
- if (!artifacts) return;
553
- const checkpoint = normalized?.checkpoint && typeof normalized.checkpoint === "object"
554
- ? normalized.checkpoint
555
- : {};
556
- writeJsonAtomic(artifacts.checkpoint_path, checkpoint);
557
- const meta = getChatRunMeta(normalized?.run_id || normalized?.runId);
558
- if (meta) meta.checkpointPath = artifacts.checkpoint_path;
559
- }
670
+ function persistChatCheckpointSnapshot(normalized) {
671
+ const artifacts = getChatRunArtifacts(normalized?.run_id || normalized?.runId);
672
+ if (!artifacts) return;
673
+ const meta = getChatRunMeta(normalized?.run_id || normalized?.runId);
674
+ const checkpoint = normalized?.checkpoint && typeof normalized.checkpoint === "object"
675
+ ? normalized.checkpoint
676
+ : {};
677
+ const existingCheckpoint = readJsonFile(artifacts.checkpoint_path);
678
+ if (!hasChatCheckpointPayload(checkpoint) && existingCheckpoint) {
679
+ if (meta) meta.checkpointPath = artifacts.checkpoint_path;
680
+ return;
681
+ }
682
+ writeJsonAtomic(artifacts.checkpoint_path, checkpoint);
683
+ if (meta) meta.checkpointPath = artifacts.checkpoint_path;
684
+ }
560
685
 
561
686
  function isPidAlive(pid) {
562
687
  const numericPid = Number(pid);
@@ -570,15 +695,15 @@ function isPidAlive(pid) {
570
695
  }
571
696
  }
572
697
 
573
- function snapshotFromPersistedChatRun(persisted = {}) {
574
- return {
698
+ function snapshotFromPersistedChatRun(persisted = {}) {
699
+ return {
575
700
  runId: persisted.run_id || persisted.runId,
576
701
  name: persisted.name || persisted.run_id || persisted.runId,
577
702
  status: persisted.status || persisted.state,
578
703
  phase: persisted.stage || persisted.phase,
579
704
  progress: persisted.progress || {},
580
705
  context: persisted.context || {},
581
- checkpoint: persisted.checkpoint || {},
706
+ checkpoint: readHydratedChatCheckpoint(persisted),
582
707
  startedAt: persisted.started_at || persisted.startedAt,
583
708
  updatedAt: persisted.updated_at || persisted.updatedAt,
584
709
  completedAt: persisted.completed_at || persisted.completedAt || null,
@@ -613,7 +738,7 @@ function attachLegacyArtifactsToPersistedChatRun(persisted = {}) {
613
738
  return persistDiskChatRun(runId, next);
614
739
  }
615
740
 
616
- function finalizePersistedChatRun(persisted = {}, {
741
+ function finalizePersistedChatRun(persisted = {}, {
617
742
  status = RUN_STATUS_FAILED,
618
743
  error = null,
619
744
  message = ""
@@ -621,30 +746,51 @@ function finalizePersistedChatRun(persisted = {}, {
621
746
  const runId = normalizeRunId(persisted.run_id || persisted.runId);
622
747
  if (!runId) return persisted;
623
748
  const now = new Date().toISOString();
624
- const normalizedError = status === RUN_STATUS_FAILED
625
- ? {
626
- name: error?.name || "Error",
627
- code: error?.code || "STALE_RUN_PROCESS_EXITED",
628
- message: error?.message || message || "Boss chat run process exited before it wrote a terminal state."
629
- }
630
- : null;
631
- const next = {
749
+ const normalizedError = status === RUN_STATUS_FAILED
750
+ ? {
751
+ name: error?.name || "Error",
752
+ code: error?.code || "STALE_RUN_PROCESS_EXITED",
753
+ message: error?.message || message || "Boss chat run process exited before it wrote a terminal state."
754
+ }
755
+ : null;
756
+ if (normalizedError) {
757
+ for (const field of ["worker_error_code", "worker_exit_code", "worker_signal"]) {
758
+ if (error?.[field] !== undefined && error?.[field] !== null && error?.[field] !== "") {
759
+ normalizedError[field] = error[field];
760
+ }
761
+ }
762
+ if (normalizeText(error?.stack || "")) {
763
+ normalizedError.stack = String(error.stack).slice(0, 8000);
764
+ }
765
+ }
766
+ const workerLastHeartbeatAt = normalizeText(
767
+ persisted.recovery?.worker_last_heartbeat_at
768
+ || persisted.heartbeat_at
769
+ || ""
770
+ ) || null;
771
+ const next = {
632
772
  ...persisted,
633
773
  run_id: runId,
634
774
  state: status,
635
775
  status,
636
- stage: persisted.stage || persisted.phase || "chat:stale",
637
- updated_at: now,
638
- heartbeat_at: now,
639
- completed_at: persisted.completed_at || now,
776
+ stage: persisted.stage || persisted.phase || "chat:stale",
777
+ updated_at: now,
778
+ heartbeat_at: persisted.heartbeat_at || now,
779
+ completed_at: persisted.completed_at || now,
640
780
  last_message: normalizedError?.message || message || status,
641
781
  control: {
642
782
  ...(persisted.control || {}),
643
783
  cancel_requested: false
644
784
  },
645
- error: normalizedError,
646
- summary: persisted.summary || null
647
- };
785
+ error: normalizedError,
786
+ summary: persisted.summary || null,
787
+ recovery: {
788
+ ...(persisted.recovery && typeof persisted.recovery === "object" ? persisted.recovery : {}),
789
+ worker_last_heartbeat_at: workerLastHeartbeatAt,
790
+ reconciled_at: now,
791
+ reconciliation_reason: normalizedError?.code || status
792
+ }
793
+ };
648
794
  return attachLegacyArtifactsToPersistedChatRun(next);
649
795
  }
650
796
 
@@ -654,11 +800,21 @@ export function markBossChatDetachedWorkerFailed(runId, error, options = {}) {
654
800
  const persisted = readChatRunState(normalizedRunId) || buildInitialChatDetachedState(normalizedRunId, {});
655
801
  const state = normalizeText(persisted.state || persisted.status);
656
802
  if (TERMINAL_STATUSES.has(state)) return persisted;
657
- const errorPayload = {
803
+ const errorPayload = {
658
804
  name: error?.name || "Error",
659
805
  code: options.code || error?.code || "CHAT_WORKER_UNHANDLED_EXCEPTION",
660
- message: normalizeText(error?.message || error || options.message) || "Boss chat detached worker exited unexpectedly."
661
- };
806
+ message: normalizeText(error?.message || error || options.message) || "Boss chat detached worker exited unexpectedly."
807
+ };
808
+ const workerErrorCode = normalizeText(error?.code || "");
809
+ if (workerErrorCode && workerErrorCode !== errorPayload.code) {
810
+ errorPayload.worker_error_code = workerErrorCode;
811
+ }
812
+ if (Number.isInteger(options.workerExitCode)) {
813
+ errorPayload.worker_exit_code = options.workerExitCode;
814
+ }
815
+ if (normalizeText(options.workerSignal || "")) {
816
+ errorPayload.worker_signal = normalizeText(options.workerSignal);
817
+ }
662
818
  if (normalizeText(error?.stack || "")) {
663
819
  errorPayload.stack = String(error.stack).slice(0, 8000);
664
820
  }
@@ -1346,9 +1502,10 @@ function getRunOptions(args, normalized, session, { workspaceRoot = "", configRe
1346
1502
  listSettleMs: parsePositiveInteger(args.list_settle_ms, slowLive ? 1800 : 1200),
1347
1503
  listFallbackPoint: null,
1348
1504
  imageOutputDir: resolveBossConfiguredOutputDir("", getChatRunsDir()),
1349
- humanRestEnabled: humanBehavior.restEnabled,
1350
- humanBehavior,
1351
- name: "mcp-boss-chat-run"
1505
+ humanRestEnabled: humanBehavior.restEnabled,
1506
+ humanBehavior,
1507
+ actionJournalScope: `boss-chat-cdp:${normalized.host}:${normalized.port}`,
1508
+ name: "mcp-boss-chat-run"
1352
1509
  };
1353
1510
  }
1354
1511
 
@@ -1816,7 +1973,9 @@ export async function startBossChatDetachedRunTool({ workspaceRoot = "", args =
1816
1973
 
1817
1974
  let child;
1818
1975
  try {
1819
- child = launchDetachedChatWorker(runId);
1976
+ child = launchDetachedChatWorker(runId, {
1977
+ screenConfigPath: defaultConfigResolution?.config_path || ""
1978
+ });
1820
1979
  const now = new Date().toISOString();
1821
1980
  const latest = readChatRunState(runId) || initial;
1822
1981
  const latestState = normalizeText(latest.state || latest.status);
@@ -2181,24 +2340,38 @@ export function cancelBossChatRunTool({ args = {} } = {}) {
2181
2340
  }
2182
2341
  }
2183
2342
 
2184
- export function __setChatMcpConnectorForTests(nextConnector) {
2185
- chatConnectorImpl = typeof nextConnector === "function" ? nextConnector : connectChatChromeSession;
2186
- }
2187
-
2188
- export function __setChatMcpJobReaderForTests(nextReader) {
2343
+ export function __setChatMcpConnectorForTests(nextConnector) {
2344
+ chatConnectorImpl = typeof nextConnector === "function" ? nextConnector : connectChatChromeSession;
2345
+ }
2346
+
2347
+ export function __writeChatJsonAtomicForTests(filePath, payload, options = {}) {
2348
+ return writeJsonAtomic(filePath, payload, options);
2349
+ }
2350
+
2351
+ export function __setChatMcpJobReaderForTests(nextReader) {
2189
2352
  chatJobReaderImpl = typeof nextReader === "function" ? nextReader : readChatJobOptionsFromSession;
2190
2353
  }
2191
2354
 
2192
- export function __setChatMcpWorkflowForTests(nextWorkflow) {
2355
+ export function __setChatMcpWorkflowForTests(nextWorkflow) {
2193
2356
  chatWorkflowImpl = typeof nextWorkflow === "function" ? nextWorkflow : runChatWorkflow;
2194
2357
  chatRunService = createChatRunService({
2195
2358
  idPrefix: "mcp_chat",
2196
2359
  workflow: (...args) => chatWorkflowImpl(...args),
2197
2360
  onSnapshot: persistChatLifecycleSnapshot
2198
2361
  });
2199
- }
2200
-
2201
- export function __resetChatMcpStateForTests() {
2362
+ }
2363
+
2364
+ export function __setChatMcpSpawnForTests(nextSpawn) {
2365
+ chatDetachedWorkerLauncherImpl = typeof nextSpawn === "function"
2366
+ ? (options) => launchDetachedWorker({
2367
+ ...options,
2368
+ platform: "test-posix",
2369
+ spawnImpl: nextSpawn
2370
+ })
2371
+ : launchDetachedWorker;
2372
+ }
2373
+
2374
+ export function __resetChatMcpStateForTests() {
2202
2375
  for (const meta of chatRunMeta.values()) {
2203
2376
  try {
2204
2377
  meta.session?.close?.();
@@ -2207,7 +2380,8 @@ export function __resetChatMcpStateForTests() {
2207
2380
  }
2208
2381
  }
2209
2382
  chatRunMeta.clear();
2210
- __setChatMcpConnectorForTests(null);
2211
- __setChatMcpJobReaderForTests(null);
2212
- __setChatMcpWorkflowForTests(null);
2213
- }
2383
+ __setChatMcpConnectorForTests(null);
2384
+ __setChatMcpJobReaderForTests(null);
2385
+ __setChatMcpWorkflowForTests(null);
2386
+ __setChatMcpSpawnForTests(null);
2387
+ }
@@ -277,11 +277,13 @@ export function resolveHumanBehaviorForRun(args = {}, config = {}) {
277
277
  override.enabled = rawBehavior;
278
278
  } else if (typeof rawBehavior === "string") {
279
279
  applyHumanBehaviorProfileDefaults(override, rawBehavior);
280
- } else if (rawBehavior && typeof rawBehavior === "object" && !Array.isArray(rawBehavior)) {
281
- const rawProfile = readFirstOwn(rawBehavior, ["profile", "mode", "behaviorProfile", "behavior_profile"]);
282
- if (rawProfile !== undefined) applyHumanBehaviorProfileDefaults(override, rawProfile);
283
- Object.assign(override, rawBehavior);
284
- }
280
+ } else if (rawBehavior && typeof rawBehavior === "object" && !Array.isArray(rawBehavior)) {
281
+ const rawProfile = readFirstOwn(rawBehavior, ["profile", "mode", "behaviorProfile", "behavior_profile"]);
282
+ if (rawProfile !== undefined) applyHumanBehaviorProfileDefaults(override, rawProfile);
283
+ Object.assign(override, rawBehavior);
284
+ const rawRestLevel = readFirstOwn(rawBehavior, ["restLevel", "rest_level"]);
285
+ if (rawRestLevel !== undefined) override.restLevel = rawRestLevel;
286
+ }
285
287
 
286
288
  const profile = readFirstOwn(args, ["human_behavior_profile", "humanBehaviorProfile"]);
287
289
  if (profile !== undefined) applyHumanBehaviorProfileDefaults(override, profile);