@mclawnet/agent 0.6.35 → 0.6.36

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 (69) hide show
  1. package/cli.js +6 -6
  2. package/dist/__tests__/collect-manifest.test.d.ts +2 -0
  3. package/dist/__tests__/collect-manifest.test.d.ts.map +1 -0
  4. package/dist/__tests__/hub-connection-on-activity.test.d.ts +2 -0
  5. package/dist/__tests__/hub-connection-on-activity.test.d.ts.map +1 -0
  6. package/dist/__tests__/hub-connection-wake-watch.test.d.ts +2 -0
  7. package/dist/__tests__/hub-connection-wake-watch.test.d.ts.map +1 -0
  8. package/dist/__tests__/ideas-rest-client.test.d.ts +2 -0
  9. package/dist/__tests__/ideas-rest-client.test.d.ts.map +1 -0
  10. package/dist/__tests__/legacy-claude-execute-compat.test.d.ts +2 -0
  11. package/dist/__tests__/legacy-claude-execute-compat.test.d.ts.map +1 -0
  12. package/dist/__tests__/no-adapter-cycle.test.d.ts +2 -0
  13. package/dist/__tests__/no-adapter-cycle.test.d.ts.map +1 -0
  14. package/dist/__tests__/session-manager-merge.test.d.ts +2 -0
  15. package/dist/__tests__/session-manager-merge.test.d.ts.map +1 -0
  16. package/dist/__tests__/session-manager-sticky.test.d.ts +2 -0
  17. package/dist/__tests__/session-manager-sticky.test.d.ts.map +1 -0
  18. package/dist/__tests__/session-protocol-dispatch.test.d.ts +2 -0
  19. package/dist/__tests__/session-protocol-dispatch.test.d.ts.map +1 -0
  20. package/dist/__tests__/worktree-bridge.test.d.ts +2 -0
  21. package/dist/__tests__/worktree-bridge.test.d.ts.map +1 -0
  22. package/dist/backend-adapter.d.ts +6 -232
  23. package/dist/backend-adapter.d.ts.map +1 -1
  24. package/dist/backend-factory-AFF6I7YF.js +11 -0
  25. package/dist/backend-factory.d.ts +23 -1
  26. package/dist/backend-factory.d.ts.map +1 -1
  27. package/dist/bootstrap-deps.d.ts +22 -0
  28. package/dist/bootstrap-deps.d.ts.map +1 -1
  29. package/dist/bootstrap-deps.js +23 -4
  30. package/dist/bootstrap-deps.js.map +1 -1
  31. package/dist/{chunk-PJ5M6Q36.js → chunk-376QZ7JB.js} +2 -2
  32. package/dist/chunk-376QZ7JB.js.map +1 -0
  33. package/dist/{chunk-B733MQCA.js → chunk-GOCWMRBB.js} +1772 -284
  34. package/dist/chunk-GOCWMRBB.js.map +1 -0
  35. package/dist/{chunk-M2CDVPQF.js → chunk-JH6RGJBQ.js} +2 -2
  36. package/dist/{chunk-FYM7CXUI.js → chunk-VAEFJLPL.js} +25 -3
  37. package/dist/chunk-VAEFJLPL.js.map +1 -0
  38. package/dist/{dist-EGT2NQEW.js → dist-NWVHAP5R.js} +155 -13
  39. package/dist/dist-NWVHAP5R.js.map +1 -0
  40. package/dist/errors.d.ts +20 -0
  41. package/dist/errors.d.ts.map +1 -1
  42. package/dist/hub-connection.d.ts +25 -1
  43. package/dist/hub-connection.d.ts.map +1 -1
  44. package/dist/ideas-rest-client.d.ts +25 -0
  45. package/dist/ideas-rest-client.d.ts.map +1 -0
  46. package/dist/index.js +3 -3
  47. package/dist/{linux-IHA4O633.js → linux-MBU6ERXL.js} +3 -3
  48. package/dist/{macos-G4VK2253.js → macos-I2DUWFUH.js} +3 -3
  49. package/dist/projects-handler.d.ts +146 -1
  50. package/dist/projects-handler.d.ts.map +1 -1
  51. package/dist/service/index.js +5 -5
  52. package/dist/session-manager.d.ts +58 -0
  53. package/dist/session-manager.d.ts.map +1 -1
  54. package/dist/start.d.ts.map +1 -1
  55. package/dist/start.js +3 -2
  56. package/dist/{windows-P6U3JLUZ.js → windows-PEJ3KOLC.js} +3 -3
  57. package/dist/worktree-bridge.d.ts +51 -0
  58. package/dist/worktree-bridge.d.ts.map +1 -0
  59. package/package.json +9 -8
  60. package/dist/backend-factory-VRPU3534.js +0 -9
  61. package/dist/chunk-B733MQCA.js.map +0 -1
  62. package/dist/chunk-FYM7CXUI.js.map +0 -1
  63. package/dist/chunk-PJ5M6Q36.js.map +0 -1
  64. package/dist/dist-EGT2NQEW.js.map +0 -1
  65. /package/dist/{backend-factory-VRPU3534.js.map → backend-factory-AFF6I7YF.js.map} +0 -0
  66. /package/dist/{chunk-M2CDVPQF.js.map → chunk-JH6RGJBQ.js.map} +0 -0
  67. /package/dist/{linux-IHA4O633.js.map → linux-MBU6ERXL.js.map} +0 -0
  68. /package/dist/{macos-G4VK2253.js.map → macos-I2DUWFUH.js.map} +0 -0
  69. /package/dist/{windows-P6U3JLUZ.js.map → windows-PEJ3KOLC.js.map} +0 -0
@@ -1,10 +1,14 @@
1
+ import {
2
+ collectAgentManifest
3
+ } from "./chunk-VAEFJLPL.js";
1
4
  import {
2
5
  loadConfig
3
- } from "./chunk-PJ5M6Q36.js";
6
+ } from "./chunk-376QZ7JB.js";
4
7
 
5
8
  // src/start.ts
6
9
  import { homedir as homedir5 } from "os";
7
- import { join as join6 } from "path";
10
+ import { join as join7 } from "path";
11
+ import { existsSync as existsSync8 } from "fs";
8
12
 
9
13
  // src/hub-connection.ts
10
14
  import { hostname as osHostname } from "os";
@@ -248,24 +252,164 @@ async function handleLoadSessionHistory(workDir, backendSessionId, opts) {
248
252
 
249
253
  // src/projects-handler.ts
250
254
  import { homedir as homedir2 } from "os";
251
- import { join as join2 } from "path";
252
- import { existsSync as existsSync2, readdirSync as readdirSync2 } from "fs";
253
- import { TaskStore, projectRoot } from "@mclawnet/task";
255
+ import { dirname, isAbsolute, join as join2, parse, resolve } from "path";
256
+ import { existsSync as existsSync2, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
257
+ import { TaskStore, projectRoot, ensureProjectMeta, metaFile, encodeCwd } from "@mclawnet/task";
258
+ import { createLogger } from "@mclawnet/logger";
254
259
  import {
255
260
  listAllProjectSummaries,
256
261
  loadProjectSummary,
257
262
  loadSwarmSummaries,
258
263
  resolveWorkDir,
259
264
  loadSwarmSnapshot,
265
+ loadShipmentResult,
260
266
  InboxStore,
261
267
  listProjectFiles,
262
268
  readProjectFile,
263
269
  writeProjectFile,
264
- ProjectFilesError
270
+ ProjectFilesError,
271
+ applyShipment,
272
+ mergeShipment,
273
+ discardShipment,
274
+ loadAlwaysOnConfig,
275
+ saveAlwaysOnConfig,
276
+ mergeAlwaysOnConfig
265
277
  } from "@mclawnet/swarm";
278
+ var log = createLogger({ module: "projects-handler" });
279
+ var FORBIDDEN_WORKDIR_PREFIXES = [
280
+ "/etc",
281
+ "/usr",
282
+ "/bin",
283
+ "/sbin",
284
+ "/var",
285
+ "/private",
286
+ "/System",
287
+ "/Library",
288
+ "/proc",
289
+ "/sys",
290
+ "/boot"
291
+ ];
292
+ function forbiddenWorkDirPrefix(workDir) {
293
+ for (const forbidden of FORBIDDEN_WORKDIR_PREFIXES) {
294
+ if (workDir === forbidden) return forbidden;
295
+ if (workDir.startsWith(`${forbidden}/`)) return forbidden;
296
+ }
297
+ return null;
298
+ }
266
299
  function getHome() {
267
300
  return process.env.CLAWNET_HOME ?? homedir2();
268
301
  }
302
+ function ensureError(status, code, message, details) {
303
+ return { status, code, message, ...details ? { details } : {} };
304
+ }
305
+ function normalizeExistingWorkDir(input) {
306
+ const normalized = normalizeAbsoluteNonRootPath(input);
307
+ if (normalized.error || !normalized.workDir) return normalized;
308
+ let st;
309
+ try {
310
+ st = statSync2(normalized.workDir);
311
+ } catch (err) {
312
+ return {
313
+ error: ensureError(
314
+ 404,
315
+ "workdir_not_found",
316
+ "workDir does not exist on disk",
317
+ { workDir: normalized.workDir, cause: err instanceof Error ? err.message : String(err) }
318
+ )
319
+ };
320
+ }
321
+ if (!st.isDirectory()) {
322
+ return { error: ensureError(400, "not_directory", "workDir is not a directory", { workDir: normalized.workDir }) };
323
+ }
324
+ return { workDir: normalized.workDir };
325
+ }
326
+ function normalizeAbsoluteNonRootPath(input) {
327
+ const trimmed = typeof input === "string" ? input.trim() : "";
328
+ if (!trimmed) return { error: ensureError(400, "invalid_path", "workDir is required") };
329
+ if (trimmed.length > 4096) {
330
+ return { error: ensureError(400, "invalid_path", "workDir is too long") };
331
+ }
332
+ if (trimmed.includes("\0")) {
333
+ return { error: ensureError(400, "invalid_path", "workDir contains NUL byte") };
334
+ }
335
+ if (!isAbsolute(trimmed)) {
336
+ return { error: ensureError(400, "invalid_path", "workDir must be an absolute path") };
337
+ }
338
+ const parsed = parse(trimmed);
339
+ const normalized = resolve(trimmed);
340
+ if (normalized === parsed.root) {
341
+ return { error: ensureError(400, "root_forbidden", "filesystem root cannot be used as a project") };
342
+ }
343
+ const forbiddenPrefix = forbiddenWorkDirPrefix(normalized);
344
+ if (forbiddenPrefix) {
345
+ return {
346
+ error: ensureError(
347
+ 400,
348
+ "path_forbidden",
349
+ "workDir is inside a protected system path",
350
+ { workDir: normalized, forbiddenPrefix }
351
+ )
352
+ };
353
+ }
354
+ return { workDir: normalized };
355
+ }
356
+ async function handleWorkdirMkdir(workDirInput) {
357
+ const normalized = normalizeAbsoluteNonRootPath(workDirInput);
358
+ if (normalized.error || !normalized.workDir) {
359
+ return { ok: false, error: normalized.error ?? ensureError(400, "invalid_path", "invalid workDir") };
360
+ }
361
+ const workDir = normalized.workDir;
362
+ const parsed = parse(workDir);
363
+ if (!parsed.base || parsed.base === "." || parsed.base === "..") {
364
+ return { ok: false, error: ensureError(400, "invalid_path", "invalid directory name", { workDir }) };
365
+ }
366
+ let st;
367
+ try {
368
+ st = statSync2(workDir);
369
+ if (st.isDirectory()) {
370
+ return { ok: true, workDir, created: false, error: null };
371
+ }
372
+ return { ok: false, error: ensureError(400, "not_directory", "workDir exists and is not a directory", { workDir }) };
373
+ } catch {
374
+ }
375
+ const parent = dirname(workDir);
376
+ try {
377
+ st = statSync2(parent);
378
+ } catch (err) {
379
+ return {
380
+ ok: false,
381
+ error: ensureError(
382
+ 404,
383
+ "parent_not_found",
384
+ "parent directory does not exist",
385
+ { workDir, parent, cause: err instanceof Error ? err.message : String(err) }
386
+ )
387
+ };
388
+ }
389
+ if (!st.isDirectory()) {
390
+ return { ok: false, error: ensureError(400, "parent_not_directory", "parent path is not a directory", { workDir, parent }) };
391
+ }
392
+ try {
393
+ mkdirSync(workDir);
394
+ } catch (err) {
395
+ try {
396
+ if (statSync2(workDir).isDirectory()) {
397
+ return { ok: true, workDir, created: false, error: null };
398
+ }
399
+ } catch {
400
+ }
401
+ return {
402
+ ok: false,
403
+ error: ensureError(
404
+ 500,
405
+ "mkdir_failed",
406
+ "failed to create directory",
407
+ { workDir, cause: err instanceof Error ? err.message : String(err) }
408
+ )
409
+ };
410
+ }
411
+ return { ok: true, workDir, created: true, error: null };
412
+ }
269
413
  async function handleProjectsList() {
270
414
  return { projects: listAllProjectSummaries(getHome()) };
271
415
  }
@@ -276,6 +420,61 @@ async function handleProjectsGet(encodedCwd) {
276
420
  const swarms = loadSwarmSummaries(home, summary.workDir);
277
421
  return { project: { ...summary, swarms } };
278
422
  }
423
+ async function handleProjectsEnsureProject(workDirInput, opts = {}) {
424
+ if (opts.createDir) {
425
+ return {
426
+ ok: false,
427
+ project: null,
428
+ error: ensureError(
429
+ 400,
430
+ "create_dir_unsupported",
431
+ "createDir is not supported by this agent; create/select an existing directory first"
432
+ )
433
+ };
434
+ }
435
+ const normalized = normalizeExistingWorkDir(workDirInput);
436
+ if (normalized.error || !normalized.workDir) {
437
+ return { ok: false, project: null, error: normalized.error ?? ensureError(400, "invalid_path", "invalid workDir") };
438
+ }
439
+ const home = getHome();
440
+ const encodedCwd = encodeCwd(normalized.workDir);
441
+ const metaPath = metaFile(normalized.workDir, home);
442
+ const existedBefore = existsSync2(metaPath);
443
+ try {
444
+ ensureProjectMeta(normalized.workDir, home);
445
+ } catch (err) {
446
+ return {
447
+ ok: false,
448
+ project: null,
449
+ encodedCwd,
450
+ workDir: normalized.workDir,
451
+ error: ensureError(
452
+ 500,
453
+ "meta_write_failed",
454
+ "failed to write project meta",
455
+ { cause: err instanceof Error ? err.message : String(err) }
456
+ )
457
+ };
458
+ }
459
+ const project = loadProjectSummary(home, encodedCwd);
460
+ if (!project) {
461
+ return {
462
+ ok: false,
463
+ project: null,
464
+ encodedCwd,
465
+ workDir: normalized.workDir,
466
+ error: ensureError(500, "project_load_failed", "project meta was written but summary could not be loaded")
467
+ };
468
+ }
469
+ return {
470
+ ok: true,
471
+ project,
472
+ encodedCwd,
473
+ workDir: normalized.workDir,
474
+ createdMeta: !existedBefore,
475
+ error: null
476
+ };
477
+ }
279
478
  async function handleProjectsTasks(encodedCwd, filters = {}) {
280
479
  const home = getHome();
281
480
  const workDir = resolveWorkDir(home, encodedCwd);
@@ -412,12 +611,247 @@ async function handleProjectsFileWrite(encodedCwd, relPath, content, ifMatchEtag
412
611
  };
413
612
  }
414
613
  }
614
+ var SHIPMENT_INLINE_MAX_BYTES = 1024 * 1024;
615
+ function tryReadInline(path) {
616
+ if (!path) return void 0;
617
+ try {
618
+ const st = statSync2(path);
619
+ if (!st.isFile() || st.size >= SHIPMENT_INLINE_MAX_BYTES) return void 0;
620
+ return readFileSync2(path, "utf-8");
621
+ } catch {
622
+ return void 0;
623
+ }
624
+ }
625
+ async function handleProjectsShipmentGet(encodedCwd, swarmId) {
626
+ const home = getHome();
627
+ const workDir = resolveWorkDir(home, encodedCwd);
628
+ if (!workDir) return { projectFound: false, shipment: null };
629
+ const shipment = loadShipmentResult(workDir, swarmId);
630
+ if (!shipment) {
631
+ return { projectFound: true, shipment: null };
632
+ }
633
+ const diffContent = tryReadInline(shipment.diffPath);
634
+ const reportContent = tryReadInline(shipment.reportPath);
635
+ return {
636
+ projectFound: true,
637
+ shipment,
638
+ ...diffContent !== void 0 ? { diffContent } : {},
639
+ ...reportContent !== void 0 ? { reportContent } : {}
640
+ };
641
+ }
642
+ async function handleProjectsShipmentDiffFull(encodedCwd, swarmId) {
643
+ const home = getHome();
644
+ const workDir = resolveWorkDir(home, encodedCwd);
645
+ if (!workDir) return { projectFound: false, content: null };
646
+ const shipment = loadShipmentResult(workDir, swarmId);
647
+ if (!shipment || !shipment.diffPath) {
648
+ return { projectFound: true, content: null };
649
+ }
650
+ try {
651
+ const st = statSync2(shipment.diffPath);
652
+ if (!st.isFile()) return { projectFound: true, content: null };
653
+ return { projectFound: true, content: readFileSync2(shipment.diffPath, "utf-8") };
654
+ } catch {
655
+ return { projectFound: true, content: null };
656
+ }
657
+ }
658
+ var inflightShipmentActions = /* @__PURE__ */ new Map();
659
+ async function handleProjectsShipmentAction(encodedCwd, swarmId, action, force) {
660
+ const home = getHome();
661
+ const workDir = resolveWorkDir(home, encodedCwd);
662
+ if (!workDir) return { projectFound: false, ok: false, error: "project not found" };
663
+ const existing = inflightShipmentActions.get(swarmId);
664
+ if (existing) {
665
+ const r2 = await existing;
666
+ return { projectFound: true, ...r2 };
667
+ }
668
+ const runAction = async () => {
669
+ if (action === "merge") return mergeShipment({ workDir, swarmId });
670
+ if (action === "apply") return applyShipment({ workDir, swarmId, force });
671
+ if (action === "discard") return discardShipment({ workDir, swarmId });
672
+ return { ok: false, error: `unknown action: ${action}` };
673
+ };
674
+ const promise = runAction().finally(() => {
675
+ inflightShipmentActions.delete(swarmId);
676
+ });
677
+ inflightShipmentActions.set(swarmId, promise);
678
+ const r = await promise;
679
+ return { projectFound: true, ...r };
680
+ }
681
+ async function handleProjectsShipmentsList(encodedCwd) {
682
+ const home = getHome();
683
+ const workDir = resolveWorkDir(home, encodedCwd);
684
+ if (!workDir) return { projectFound: false, shipments: [] };
685
+ const swarmsDir = join2(projectRoot(workDir, home), "swarms");
686
+ if (!existsSync2(swarmsDir)) return { projectFound: true, shipments: [] };
687
+ let entries;
688
+ try {
689
+ entries = readdirSync2(swarmsDir, { withFileTypes: true });
690
+ } catch (err) {
691
+ log.warn(
692
+ { err: err instanceof Error ? err.message : String(err), swarmsDir },
693
+ "shipments_list: readdir failed; returning empty list"
694
+ );
695
+ return { projectFound: true, shipments: [] };
696
+ }
697
+ const shipments = [];
698
+ for (const ent of entries) {
699
+ if (!ent.isDirectory()) continue;
700
+ const shipmentPath = join2(swarmsDir, ent.name, "shipment.json");
701
+ if (!existsSync2(shipmentPath)) continue;
702
+ try {
703
+ const data = JSON.parse(readFileSync2(shipmentPath, "utf-8"));
704
+ shipments.push({ swarmId: ent.name, shipment: data });
705
+ } catch (err) {
706
+ log.warn(
707
+ { err: err instanceof Error ? err.message : String(err), swarmId: ent.name },
708
+ "shipments_list: parse failed for one swarm; skipping"
709
+ );
710
+ }
711
+ }
712
+ shipments.sort((a, b) => {
713
+ const aT = a.shipment.createdAt ?? "";
714
+ const bT = b.shipment.createdAt ?? "";
715
+ return bT.localeCompare(aT);
716
+ });
717
+ return { projectFound: true, shipments };
718
+ }
719
+ var EMPTY_ALWAYS_ON_SNAPSHOT = {
720
+ mode: "off",
721
+ ticksToday: 0,
722
+ lastFireAt: null,
723
+ nextTickAt: null,
724
+ recentTicks: []
725
+ };
726
+ var alwaysOnHooks = {
727
+ getSnapshot: () => null,
728
+ applyConfig: () => {
729
+ },
730
+ recordActivity: () => {
731
+ }
732
+ // forceTick left undefined: handler returns projectFound=false when null
733
+ };
734
+ function _registerAlwaysOnHooks(hooks) {
735
+ alwaysOnHooks = {
736
+ getSnapshot: hooks.getSnapshot ?? (() => null),
737
+ applyConfig: hooks.applyConfig ?? (() => void 0),
738
+ recordActivity: hooks.recordActivity ?? (() => void 0),
739
+ ...hooks.forceTick ? { forceTick: hooks.forceTick } : {}
740
+ };
741
+ }
742
+ function _recordAlwaysOnActivity() {
743
+ try {
744
+ alwaysOnHooks.recordActivity();
745
+ } catch (err) {
746
+ log.warn(
747
+ { err: err instanceof Error ? err.message : String(err) },
748
+ "always-on recordActivity hook threw; ignored"
749
+ );
750
+ }
751
+ }
752
+ function snapshotForProject(workDir) {
753
+ const live = alwaysOnHooks.getSnapshot(workDir);
754
+ return live ?? EMPTY_ALWAYS_ON_SNAPSHOT;
755
+ }
756
+ async function handleProjectsAlwaysOnGet(encodedCwd) {
757
+ const home = getHome();
758
+ const workDir = resolveWorkDir(home, encodedCwd);
759
+ if (!workDir) {
760
+ return {
761
+ projectFound: false,
762
+ config: { mode: "off", dailyBudget: 3 },
763
+ snapshot: EMPTY_ALWAYS_ON_SNAPSHOT
764
+ };
765
+ }
766
+ const config = loadAlwaysOnConfig(workDir, home);
767
+ return { projectFound: true, config, snapshot: snapshotForProject(workDir) };
768
+ }
769
+ async function handleProjectsAlwaysOnSet(encodedCwd, patch) {
770
+ const home = getHome();
771
+ const workDir = resolveWorkDir(home, encodedCwd);
772
+ if (!workDir) {
773
+ return {
774
+ projectFound: false,
775
+ config: { mode: "off", dailyBudget: 3 },
776
+ snapshot: EMPTY_ALWAYS_ON_SNAPSHOT
777
+ };
778
+ }
779
+ const current = loadAlwaysOnConfig(workDir, home);
780
+ const next = mergeAlwaysOnConfig(current, patch);
781
+ try {
782
+ saveAlwaysOnConfig(workDir, home, next);
783
+ } catch (err) {
784
+ return {
785
+ projectFound: true,
786
+ config: current,
787
+ snapshot: snapshotForProject(workDir),
788
+ error: err instanceof Error ? err.message : String(err)
789
+ };
790
+ }
791
+ try {
792
+ alwaysOnHooks.applyConfig(workDir, next);
793
+ } catch (err) {
794
+ log.warn(
795
+ { err: err instanceof Error ? err.message : String(err) },
796
+ "always-on applyConfig hook threw; config was persisted"
797
+ );
798
+ }
799
+ return {
800
+ projectFound: true,
801
+ config: next,
802
+ snapshot: snapshotForProject(workDir)
803
+ };
804
+ }
805
+ async function handleProjectsAlwaysOnTickNow(encodedCwd) {
806
+ const home = getHome();
807
+ const workDir = resolveWorkDir(home, encodedCwd);
808
+ if (!workDir) return { projectFound: false };
809
+ if (!alwaysOnHooks.forceTick) {
810
+ return {
811
+ projectFound: true,
812
+ error: "always-on manager not wired (forceTick unavailable)"
813
+ };
814
+ }
815
+ try {
816
+ const out = await alwaysOnHooks.forceTick(workDir);
817
+ if (!out) {
818
+ return {
819
+ projectFound: true,
820
+ error: "always-on scheduler not booted for this project (enable always-on via PATCH /api/projects/:id/always-on first)"
821
+ };
822
+ }
823
+ return {
824
+ projectFound: true,
825
+ decision: out.record.decision,
826
+ gateBreakdown: out.record.gateBreakdown,
827
+ snapshot: out.snapshot
828
+ };
829
+ } catch (err) {
830
+ const msg = err instanceof Error ? err.message : String(err);
831
+ if (msg.includes("no scheduler for")) {
832
+ return {
833
+ projectFound: true,
834
+ error: "always-on scheduler not booted for this project (enable always-on via PATCH /api/projects/:id/always-on first)"
835
+ };
836
+ }
837
+ if (msg.includes("tick is already in progress")) {
838
+ return {
839
+ projectFound: true,
840
+ error: `another tick is currently in flight \u2014 wait for the runner to finish or check the agent log (underlying: ${msg})`
841
+ };
842
+ }
843
+ return {
844
+ projectFound: true,
845
+ error: `force-tick failed: ${msg}`
846
+ };
847
+ }
848
+ }
415
849
 
416
850
  // src/swarm-control-dispatch.ts
417
851
  import { randomUUID } from "crypto";
418
- import { createLogger } from "@mclawnet/logger";
852
+ import { createLogger as createLogger2 } from "@mclawnet/logger";
419
853
  import { InboxStore as InboxStore2 } from "@mclawnet/swarm";
420
- var log = createLogger({ module: "agent/swarm-control" });
854
+ var log2 = createLogger2({ module: "agent/swarm-control" });
421
855
  var MAX_ROLES_PER_SWARM = 20;
422
856
  async function handleSwarmControl(coord, msg, opts) {
423
857
  if (!msg || msg.type !== "swarm_spawn" && msg.type !== "swarm_resume" && msg.type !== "swarm_add_role") {
@@ -426,11 +860,11 @@ async function handleSwarmControl(coord, msg, opts) {
426
860
  if (msg.type === "swarm_resume") {
427
861
  try {
428
862
  await coord.recover(msg.swarmId);
429
- log.info({ swarmId: msg.swarmId }, "swarm_resume: recovered");
863
+ log2.info({ swarmId: msg.swarmId }, "swarm_resume: recovered");
430
864
  return { ok: true, swarmId: msg.swarmId };
431
865
  } catch (err) {
432
866
  const error = err instanceof Error ? err.message : String(err);
433
- log.error({ err, swarmId: msg.swarmId }, "swarm_resume failed");
867
+ log2.error({ err, swarmId: msg.swarmId }, "swarm_resume failed");
434
868
  return { ok: false, error };
435
869
  }
436
870
  }
@@ -467,14 +901,14 @@ async function handleSwarmControl(coord, msg, opts) {
467
901
  taskPrompt: msg.taskPrompt,
468
902
  isDynamicAdd: true
469
903
  });
470
- log.info(
904
+ log2.info(
471
905
  { swarmId: msg.swarmId, roleName: msg.roleName, instanceId: role.instanceId },
472
906
  "swarm_add_role: spawned"
473
907
  );
474
908
  return { ok: true, swarmId: msg.swarmId, instanceId: role.instanceId };
475
909
  } catch (err) {
476
910
  const error = err instanceof Error ? err.message : String(err);
477
- log.error({ err, swarmId: msg.swarmId, roleName: msg.roleName }, "swarm_add_role failed");
911
+ log2.error({ err, swarmId: msg.swarmId, roleName: msg.roleName }, "swarm_add_role failed");
478
912
  return { ok: false, error };
479
913
  }
480
914
  }
@@ -484,7 +918,7 @@ async function handleSwarmControl(coord, msg, opts) {
484
918
  await coord.create(swarmId, { workDir, templateName: msg.teamName, displayName: msg.displayName });
485
919
  } catch (err) {
486
920
  const error = err instanceof Error ? err.message : String(err);
487
- log.error({ err, teamName: msg.teamName }, "swarm_spawn: create failed");
921
+ log2.error({ err, teamName: msg.teamName }, "swarm_spawn: create failed");
488
922
  return { ok: false, error };
489
923
  }
490
924
  if (msg.msg && msg.msg.length > 0) {
@@ -503,20 +937,28 @@ async function handleSwarmControl(coord, msg, opts) {
503
937
  });
504
938
  await coord.inboxRelay.deliver(swarmId, queen.instanceId);
505
939
  } catch (err) {
506
- log.warn({ err, swarmId }, "swarm_spawn: inbox seed failed (non-fatal)");
940
+ log2.warn({ err, swarmId }, "swarm_spawn: inbox seed failed (non-fatal)");
507
941
  }
508
942
  } else {
509
- log.warn({ swarmId, hasSwarm: !!swarm, hasQueen: !!queen }, "swarm_spawn: queen instance not found, skipping inbox seed");
943
+ log2.warn({ swarmId, hasSwarm: !!swarm, hasQueen: !!queen }, "swarm_spawn: queen instance not found, skipping inbox seed");
510
944
  }
511
945
  }
512
- log.info({ swarmId, teamName: msg.teamName }, "swarm_spawn: created");
946
+ log2.info({ swarmId, teamName: msg.teamName }, "swarm_spawn: created");
513
947
  return { ok: true, swarmId };
514
948
  }
515
949
 
516
950
  // src/hub-connection.ts
517
- import { createLogger as createLogger2, previewFields } from "@mclawnet/logger";
951
+ import { createLogger as createLogger3, previewFields } from "@mclawnet/logger";
518
952
  import { DEFAULT_ASSISTANT_ROLE_ID } from "@mclawnet/memory";
519
- var log2 = createLogger2({ module: "agent" });
953
+ var log3 = createLogger3({ module: "agent" });
954
+ var WAKE_WATCH_INTERVAL_MS = 1e3;
955
+ var WAKE_WATCH_JUMP_THRESHOLD_MS = 15e3;
956
+ function extractErrorCode(err) {
957
+ if (err && typeof err === "object" && "code" in err && typeof err.code === "string") {
958
+ return err.code;
959
+ }
960
+ return void 0;
961
+ }
520
962
  var HubConnection = class {
521
963
  ws = null;
522
964
  heartbeatTimer = null;
@@ -556,6 +998,9 @@ var HubConnection = class {
556
998
  onConnectCb;
557
999
  onDisconnect;
558
1000
  onError;
1001
+ onActivity;
1002
+ /** (N5) Shared WorkspaceManager for chat `useWorktree` flow. */
1003
+ workspaceManager;
559
1004
  constructor(opts) {
560
1005
  this.hubUrl = opts.hubUrl;
561
1006
  this.token = opts.token;
@@ -569,6 +1014,8 @@ var HubConnection = class {
569
1014
  this.onConnectCb = opts.onConnect;
570
1015
  this.onDisconnect = opts.onDisconnect;
571
1016
  this.onError = opts.onError;
1017
+ this.onActivity = opts.onActivity;
1018
+ this.workspaceManager = opts.workspaceManager;
572
1019
  this.startWakeWatch();
573
1020
  }
574
1021
  setSessionManager(manager) {
@@ -620,7 +1067,7 @@ var HubConnection = class {
620
1067
  return;
621
1068
  }
622
1069
  if (this.authState === "pending" && data.type === "auth_required") {
623
- log2.info("hub requires auth, sending credentials");
1070
+ log3.info("hub requires auth, sending credentials");
624
1071
  this.authState = "authenticating";
625
1072
  this.sendRaw({
626
1073
  type: "auth",
@@ -632,11 +1079,17 @@ var HubConnection = class {
632
1079
  return;
633
1080
  }
634
1081
  if (this.authState === "authenticating" && data.type === "registered") {
635
- log2.info({ agentId: data.agentId }, "registered with hub");
1082
+ log3.info({ agentId: data.agentId }, "registered with hub");
636
1083
  this.authState = "authenticated";
637
1084
  this.agentId = data.agentId ?? null;
638
1085
  this.reconnectDelay = DEFAULT_RECONNECT_MS;
639
1086
  this.startHeartbeat();
1087
+ collectAgentManifest().then((payload) => {
1088
+ log3.debug({ backends: payload.backends.length }, "sending agent.manifest");
1089
+ this.send({ type: "agent.manifest", payload });
1090
+ }).catch((err) => {
1091
+ log3.warn({ err }, "failed to collect agent manifest \u2014 skipping upload");
1092
+ });
640
1093
  this.onConnectCb?.(this.agentId);
641
1094
  this.tryRecoverSwarms();
642
1095
  this.reportLiveSessions();
@@ -644,9 +1097,20 @@ var HubConnection = class {
644
1097
  }
645
1098
  if (this.authState === "authenticated") {
646
1099
  this.lastAckAt = Date.now();
647
- if (data && data.type === "heartbeat_ack") {
1100
+ const protocolType = data.type;
1101
+ if (protocolType === "heartbeat_ack") {
648
1102
  return;
649
1103
  }
1104
+ if (protocolType === "ack") {
1105
+ return;
1106
+ }
1107
+ if (this.onActivity) {
1108
+ try {
1109
+ this.onActivity();
1110
+ } catch (err) {
1111
+ log3.warn({ err }, "onActivity callback threw; ignored");
1112
+ }
1113
+ }
650
1114
  if (this.handleSessionMessage(data)) return;
651
1115
  this.onMessage?.(data);
652
1116
  }
@@ -655,19 +1119,19 @@ var HubConnection = class {
655
1119
  this.lastAckAt = Date.now();
656
1120
  });
657
1121
  this.ws.on("close", (code, reason) => {
658
- log2.warn({ code, reason: reason.toString() }, "disconnected from hub");
1122
+ log3.warn({ code, reason: reason.toString() }, "disconnected from hub");
659
1123
  this.stopHeartbeat();
660
1124
  this.ws = null;
661
1125
  this.authState = "pending";
662
1126
  this.onDisconnect?.(code, reason.toString());
663
1127
  if (code === WS_CLOSE_INVALID_TOKEN) {
664
- log2.error("auth failed \u2014 not reconnecting, check your token");
1128
+ log3.error("auth failed \u2014 not reconnecting, check your token");
665
1129
  return;
666
1130
  }
667
1131
  this.scheduleReconnect();
668
1132
  });
669
1133
  this.ws.on("error", (err) => {
670
- log2.error({ err }, "ws connection error");
1134
+ log3.error({ err }, "ws connection error");
671
1135
  this.onError?.(err);
672
1136
  if (this.ws) {
673
1137
  const dying = this.ws;
@@ -702,11 +1166,11 @@ var HubConnection = class {
702
1166
  return true;
703
1167
  }
704
1168
  if (msg.type === "fs.list_dir") {
705
- log2.info({ path: msg.path }, "fs.list_dir");
1169
+ log3.info({ path: msg.path }, "fs.list_dir");
706
1170
  handleListDir(msg.path).then((result) => {
707
1171
  this.send({ type: "fs.list_dir_result", requestId: msg.requestId, ...result });
708
1172
  }).catch((err) => {
709
- log2.error({ err, path: msg.path }, "fs.list_dir failed");
1173
+ log3.error({ err, path: msg.path }, "fs.list_dir failed");
710
1174
  this.send({
711
1175
  type: "fs.list_dir_result",
712
1176
  requestId: msg.requestId,
@@ -717,21 +1181,21 @@ var HubConnection = class {
717
1181
  return true;
718
1182
  }
719
1183
  if (msg.type === "list_folders") {
720
- log2.info("list_folders");
1184
+ log3.info("list_folders");
721
1185
  handleListFolders().then((result) => {
722
1186
  this.send({ type: "folders_list_result", requestId: msg.requestId, ...result });
723
1187
  }).catch((err) => {
724
- log2.error({ err }, "list_folders failed");
1188
+ log3.error({ err }, "list_folders failed");
725
1189
  this.send({ type: "folders_list_result", requestId: msg.requestId, folders: [] });
726
1190
  });
727
1191
  return true;
728
1192
  }
729
1193
  if (msg.type === "list_history_sessions") {
730
- log2.info({ workDir: msg.workDir }, "list_history_sessions");
1194
+ log3.info({ workDir: msg.workDir }, "list_history_sessions");
731
1195
  handleListHistorySessions(msg.workDir).then((result) => {
732
1196
  this.send({ type: "history_sessions_result", requestId: msg.requestId, ...result });
733
1197
  }).catch((err) => {
734
- log2.error({ err, workDir: msg.workDir }, "list_history_sessions failed");
1198
+ log3.error({ err, workDir: msg.workDir }, "list_history_sessions failed");
735
1199
  this.send({
736
1200
  type: "history_sessions_result",
737
1201
  requestId: msg.requestId,
@@ -742,70 +1206,70 @@ var HubConnection = class {
742
1206
  return true;
743
1207
  }
744
1208
  if (msg.type === "load_session_history") {
745
- log2.info(
1209
+ log3.info(
746
1210
  { workDir: msg.workDir, backendSessionId: msg.backendSessionId, before: msg.before, limit: msg.limit },
747
1211
  "load_session_history"
748
1212
  );
749
1213
  handleLoadSessionHistory(msg.workDir, msg.backendSessionId, { before: msg.before, limit: msg.limit }).then((result) => {
750
1214
  this.send({ type: "session_history_result", requestId: msg.requestId, ...result });
751
1215
  }).catch((err) => {
752
- log2.error({ err, workDir: msg.workDir }, "load_session_history failed");
1216
+ log3.error({ err, workDir: msg.workDir }, "load_session_history failed");
753
1217
  this.send({ type: "session_history_result", requestId: msg.requestId, messages: [], oldestSeq: 0, hasMore: false });
754
1218
  });
755
1219
  return true;
756
1220
  }
757
1221
  if (msg.type === "projects.list") {
758
- log2.info("projects.list");
1222
+ log3.info("projects.list");
759
1223
  handleProjectsList().then((r) => this.send({ type: "projects.list_result", requestId: msg.requestId, ...r })).catch((err) => {
760
- log2.error({ err }, "projects.list failed");
1224
+ log3.error({ err }, "projects.list failed");
761
1225
  this.send({ type: "projects.list_result", requestId: msg.requestId, projects: [] });
762
1226
  });
763
1227
  return true;
764
1228
  }
765
1229
  if (msg.type === "projects.get") {
766
- log2.info({ encodedCwd: msg.encodedCwd }, "projects.get");
1230
+ log3.info({ encodedCwd: msg.encodedCwd }, "projects.get");
767
1231
  handleProjectsGet(msg.encodedCwd).then((r) => this.send({ type: "projects.get_result", requestId: msg.requestId, ...r })).catch((err) => {
768
- log2.error({ err, encodedCwd: msg.encodedCwd }, "projects.get failed");
1232
+ log3.error({ err, encodedCwd: msg.encodedCwd }, "projects.get failed");
769
1233
  this.send({ type: "projects.get_result", requestId: msg.requestId, project: null });
770
1234
  });
771
1235
  return true;
772
1236
  }
773
1237
  if (msg.type === "projects.tasks") {
774
- log2.info({ encodedCwd: msg.encodedCwd, status: msg.status, swarmId: msg.swarmId, owner: msg.owner }, "projects.tasks");
1238
+ log3.info({ encodedCwd: msg.encodedCwd, status: msg.status, swarmId: msg.swarmId, owner: msg.owner }, "projects.tasks");
775
1239
  handleProjectsTasks(msg.encodedCwd, { status: msg.status, swarmId: msg.swarmId, owner: msg.owner }).then((r) => this.send({ type: "projects.tasks_result", requestId: msg.requestId, ...r })).catch((err) => {
776
- log2.error({ err, encodedCwd: msg.encodedCwd, status: msg.status, swarmId: msg.swarmId, owner: msg.owner }, "projects.tasks failed");
1240
+ log3.error({ err, encodedCwd: msg.encodedCwd, status: msg.status, swarmId: msg.swarmId, owner: msg.owner }, "projects.tasks failed");
777
1241
  this.send({ type: "projects.tasks_result", requestId: msg.requestId, tasks: [] });
778
1242
  });
779
1243
  return true;
780
1244
  }
781
1245
  if (msg.type === "projects.task") {
782
- log2.info({ encodedCwd: msg.encodedCwd, taskId: msg.taskId }, "projects.task");
1246
+ log3.info({ encodedCwd: msg.encodedCwd, taskId: msg.taskId }, "projects.task");
783
1247
  handleProjectsTask(msg.encodedCwd, msg.taskId).then((r) => this.send({ type: "projects.task_result", requestId: msg.requestId, ...r })).catch((err) => {
784
- log2.error({ err, encodedCwd: msg.encodedCwd, taskId: msg.taskId }, "projects.task failed");
1248
+ log3.error({ err, encodedCwd: msg.encodedCwd, taskId: msg.taskId }, "projects.task failed");
785
1249
  this.send({ type: "projects.task_result", requestId: msg.requestId, task: null });
786
1250
  });
787
1251
  return true;
788
1252
  }
789
1253
  if (msg.type === "projects.swarms") {
790
- log2.info({ encodedCwd: msg.encodedCwd }, "projects.swarms");
1254
+ log3.info({ encodedCwd: msg.encodedCwd }, "projects.swarms");
791
1255
  handleProjectsSwarms(msg.encodedCwd).then((r) => this.send({ type: "projects.swarms_result", requestId: msg.requestId, ...r })).catch((err) => {
792
- log2.error({ err, encodedCwd: msg.encodedCwd }, "projects.swarms failed");
1256
+ log3.error({ err, encodedCwd: msg.encodedCwd }, "projects.swarms failed");
793
1257
  this.send({ type: "projects.swarms_result", requestId: msg.requestId, swarms: [] });
794
1258
  });
795
1259
  return true;
796
1260
  }
797
1261
  if (msg.type === "projects.swarm") {
798
- log2.info({ encodedCwd: msg.encodedCwd, swarmId: msg.swarmId }, "projects.swarm");
1262
+ log3.info({ encodedCwd: msg.encodedCwd, swarmId: msg.swarmId }, "projects.swarm");
799
1263
  handleProjectsSwarm(msg.encodedCwd, msg.swarmId).then((r) => this.send({ type: "projects.swarm_result", requestId: msg.requestId, ...r })).catch((err) => {
800
- log2.error({ err, encodedCwd: msg.encodedCwd, swarmId: msg.swarmId }, "projects.swarm failed");
1264
+ log3.error({ err, encodedCwd: msg.encodedCwd, swarmId: msg.swarmId }, "projects.swarm failed");
801
1265
  this.send({ type: "projects.swarm_result", requestId: msg.requestId, snapshot: null, tasks: [] });
802
1266
  });
803
1267
  return true;
804
1268
  }
805
1269
  if (msg.type === "projects.inboxes") {
806
- log2.info({ encodedCwd: msg.encodedCwd, swarmId: msg.swarmId }, "projects.inboxes");
1270
+ log3.info({ encodedCwd: msg.encodedCwd, swarmId: msg.swarmId }, "projects.inboxes");
807
1271
  handleProjectsInboxes(msg.encodedCwd, msg.swarmId).then((r) => this.send({ type: "projects.inboxes_result", requestId: msg.requestId, ...r })).catch((err) => {
808
- log2.error({ err, encodedCwd: msg.encodedCwd, swarmId: msg.swarmId }, "projects.inboxes failed");
1272
+ log3.error({ err, encodedCwd: msg.encodedCwd, swarmId: msg.swarmId }, "projects.inboxes failed");
809
1273
  this.send({
810
1274
  type: "projects.inboxes_result",
811
1275
  requestId: msg.requestId,
@@ -817,9 +1281,9 @@ var HubConnection = class {
817
1281
  return true;
818
1282
  }
819
1283
  if (msg.type === "projects.inbox") {
820
- log2.info({ encodedCwd: msg.encodedCwd, swarmId: msg.swarmId, instanceId: msg.instanceId }, "projects.inbox");
1284
+ log3.info({ encodedCwd: msg.encodedCwd, swarmId: msg.swarmId, instanceId: msg.instanceId }, "projects.inbox");
821
1285
  handleProjectsInbox(msg.encodedCwd, msg.swarmId, msg.instanceId).then((r) => this.send({ type: "projects.inbox_result", requestId: msg.requestId, ...r })).catch((err) => {
822
- log2.error({ err, encodedCwd: msg.encodedCwd, swarmId: msg.swarmId, instanceId: msg.instanceId }, "projects.inbox failed");
1286
+ log3.error({ err, encodedCwd: msg.encodedCwd, swarmId: msg.swarmId, instanceId: msg.instanceId }, "projects.inbox failed");
823
1287
  this.send({
824
1288
  type: "projects.inbox_result",
825
1289
  requestId: msg.requestId,
@@ -831,9 +1295,9 @@ var HubConnection = class {
831
1295
  return true;
832
1296
  }
833
1297
  if (msg.type === "projects.swarm_session_id") {
834
- log2.info({ encodedCwd: msg.encodedCwd, swarmId: msg.swarmId }, "projects.swarm_session_id");
1298
+ log3.info({ encodedCwd: msg.encodedCwd, swarmId: msg.swarmId }, "projects.swarm_session_id");
835
1299
  handleProjectsSwarmSessionId(msg.encodedCwd, msg.swarmId).then((r) => this.send({ type: "projects.swarm_session_id_result", requestId: msg.requestId, ...r })).catch((err) => {
836
- log2.error({ err, encodedCwd: msg.encodedCwd, swarmId: msg.swarmId }, "projects.swarm_session_id failed");
1300
+ log3.error({ err, encodedCwd: msg.encodedCwd, swarmId: msg.swarmId }, "projects.swarm_session_id failed");
837
1301
  this.send({
838
1302
  type: "projects.swarm_session_id_result",
839
1303
  requestId: msg.requestId,
@@ -845,17 +1309,17 @@ var HubConnection = class {
845
1309
  return true;
846
1310
  }
847
1311
  if (msg.type === "projects.files_list") {
848
- log2.info({ encodedCwd: msg.encodedCwd }, "projects.files_list");
1312
+ log3.info({ encodedCwd: msg.encodedCwd }, "projects.files_list");
849
1313
  handleProjectsFilesList(msg.encodedCwd).then((r) => this.send({ type: "projects.files_list_result", requestId: msg.requestId, ...r })).catch((err) => {
850
- log2.error({ err, encodedCwd: msg.encodedCwd }, "projects.files_list failed");
1314
+ log3.error({ err, encodedCwd: msg.encodedCwd }, "projects.files_list failed");
851
1315
  this.send({ type: "projects.files_list_result", requestId: msg.requestId, projectFound: false, files: [] });
852
1316
  });
853
1317
  return true;
854
1318
  }
855
1319
  if (msg.type === "projects.file_read") {
856
- log2.info({ encodedCwd: msg.encodedCwd, relPath: msg.relPath }, "projects.file_read");
1320
+ log3.info({ encodedCwd: msg.encodedCwd, relPath: msg.relPath }, "projects.file_read");
857
1321
  handleProjectsFileRead(msg.encodedCwd, msg.relPath).then((r) => this.send({ type: "projects.file_read_result", requestId: msg.requestId, ...r })).catch((err) => {
858
- log2.error({ err, encodedCwd: msg.encodedCwd, relPath: msg.relPath }, "projects.file_read failed");
1322
+ log3.error({ err, encodedCwd: msg.encodedCwd, relPath: msg.relPath }, "projects.file_read failed");
859
1323
  this.send({
860
1324
  type: "projects.file_read_result",
861
1325
  requestId: msg.requestId,
@@ -867,9 +1331,9 @@ var HubConnection = class {
867
1331
  return true;
868
1332
  }
869
1333
  if (msg.type === "projects.file_write") {
870
- log2.info({ encodedCwd: msg.encodedCwd, relPath: msg.relPath, hasIfMatch: !!msg.ifMatchEtag }, "projects.file_write");
1334
+ log3.info({ encodedCwd: msg.encodedCwd, relPath: msg.relPath, hasIfMatch: !!msg.ifMatchEtag }, "projects.file_write");
871
1335
  handleProjectsFileWrite(msg.encodedCwd, msg.relPath, msg.content, msg.ifMatchEtag).then((r) => this.send({ type: "projects.file_write_result", requestId: msg.requestId, ...r })).catch((err) => {
872
- log2.error({ err, encodedCwd: msg.encodedCwd, relPath: msg.relPath }, "projects.file_write failed");
1336
+ log3.error({ err, encodedCwd: msg.encodedCwd, relPath: msg.relPath }, "projects.file_write failed");
873
1337
  this.send({
874
1338
  type: "projects.file_write_result",
875
1339
  requestId: msg.requestId,
@@ -880,8 +1344,193 @@ var HubConnection = class {
880
1344
  });
881
1345
  return true;
882
1346
  }
1347
+ if (msg.type === "projects.ensure_project") {
1348
+ log3.info({ workDir: msg.workDir, createDir: msg.createDir }, "projects.ensure_project");
1349
+ handleProjectsEnsureProject(msg.workDir, { createDir: msg.createDir }).then((r) => this.send({ type: "projects.ensure_project_result", requestId: msg.requestId, ...r })).catch((err) => {
1350
+ log3.error({ err, workDir: msg.workDir }, "projects.ensure_project failed");
1351
+ this.send({
1352
+ type: "projects.ensure_project_result",
1353
+ requestId: msg.requestId,
1354
+ ok: false,
1355
+ project: null,
1356
+ error: { status: 500, code: "agent_error", message: "agent error" }
1357
+ });
1358
+ });
1359
+ return true;
1360
+ }
1361
+ if (msg.type === "workdir.mkdir") {
1362
+ log3.info({ workDir: msg.workDir }, "workdir.mkdir");
1363
+ handleWorkdirMkdir(msg.workDir).then((r) => this.send({ type: "workdir.mkdir_result", requestId: msg.requestId, ...r })).catch((err) => {
1364
+ log3.error({ err, workDir: msg.workDir }, "workdir.mkdir failed");
1365
+ this.send({
1366
+ type: "workdir.mkdir_result",
1367
+ requestId: msg.requestId,
1368
+ ok: false,
1369
+ error: { status: 500, code: "agent_error", message: "agent error" }
1370
+ });
1371
+ });
1372
+ return true;
1373
+ }
1374
+ if (msg.type === "projects.shipment_get") {
1375
+ log3.info({ encodedCwd: msg.encodedCwd, swarmId: msg.swarmId }, "projects.shipment_get");
1376
+ handleProjectsShipmentGet(msg.encodedCwd, msg.swarmId).then((r) => this.send({ type: "projects.shipment_get_result", requestId: msg.requestId, ...r })).catch((err) => {
1377
+ log3.error({ err, encodedCwd: msg.encodedCwd, swarmId: msg.swarmId }, "projects.shipment_get failed");
1378
+ this.send({
1379
+ type: "projects.shipment_get_result",
1380
+ requestId: msg.requestId,
1381
+ projectFound: false,
1382
+ shipment: null
1383
+ });
1384
+ });
1385
+ return true;
1386
+ }
1387
+ if (msg.type === "projects.shipment_diff") {
1388
+ log3.info({ encodedCwd: msg.encodedCwd, swarmId: msg.swarmId }, "projects.shipment_diff");
1389
+ handleProjectsShipmentDiffFull(msg.encodedCwd, msg.swarmId).then((r) => this.send({ type: "projects.shipment_diff_result", requestId: msg.requestId, ...r })).catch((err) => {
1390
+ log3.error({ err, encodedCwd: msg.encodedCwd, swarmId: msg.swarmId }, "projects.shipment_diff failed");
1391
+ this.send({
1392
+ type: "projects.shipment_diff_result",
1393
+ requestId: msg.requestId,
1394
+ projectFound: false,
1395
+ content: null
1396
+ });
1397
+ });
1398
+ return true;
1399
+ }
1400
+ if (msg.type === "projects.shipment_action") {
1401
+ log3.info(
1402
+ { encodedCwd: msg.encodedCwd, swarmId: msg.swarmId, action: msg.action, force: msg.force },
1403
+ "projects.shipment_action"
1404
+ );
1405
+ handleProjectsShipmentAction(msg.encodedCwd, msg.swarmId, msg.action, msg.force).then(
1406
+ (r) => this.send({
1407
+ type: "projects.shipment_action_result",
1408
+ requestId: msg.requestId,
1409
+ ...r
1410
+ })
1411
+ ).catch((err) => {
1412
+ log3.error(
1413
+ { err, encodedCwd: msg.encodedCwd, swarmId: msg.swarmId, action: msg.action },
1414
+ "projects.shipment_action failed"
1415
+ );
1416
+ this.send({
1417
+ type: "projects.shipment_action_result",
1418
+ requestId: msg.requestId,
1419
+ projectFound: false,
1420
+ ok: false,
1421
+ error: err instanceof Error ? err.message : String(err)
1422
+ });
1423
+ });
1424
+ return true;
1425
+ }
1426
+ if (msg.type === "projects.shipments_list") {
1427
+ log3.info({ encodedCwd: msg.encodedCwd }, "projects.shipments_list");
1428
+ handleProjectsShipmentsList(msg.encodedCwd).then(
1429
+ (r) => this.send({
1430
+ type: "projects.shipments_list_result",
1431
+ requestId: msg.requestId,
1432
+ ...r
1433
+ })
1434
+ ).catch((err) => {
1435
+ log3.error(
1436
+ { err, encodedCwd: msg.encodedCwd },
1437
+ "projects.shipments_list failed"
1438
+ );
1439
+ this.send({
1440
+ type: "projects.shipments_list_result",
1441
+ requestId: msg.requestId,
1442
+ projectFound: false,
1443
+ shipments: []
1444
+ });
1445
+ });
1446
+ return true;
1447
+ }
1448
+ if (msg.type === "projects.always_on_get") {
1449
+ log3.info({ encodedCwd: msg.encodedCwd }, "projects.always_on_get");
1450
+ handleProjectsAlwaysOnGet(msg.encodedCwd).then(
1451
+ (r) => this.send({
1452
+ type: "projects.always_on_get_result",
1453
+ requestId: msg.requestId,
1454
+ ...r
1455
+ })
1456
+ ).catch((err) => {
1457
+ log3.error(
1458
+ { err, encodedCwd: msg.encodedCwd },
1459
+ "projects.always_on_get failed"
1460
+ );
1461
+ this.send({
1462
+ type: "projects.always_on_get_result",
1463
+ requestId: msg.requestId,
1464
+ projectFound: false,
1465
+ config: { mode: "off", dailyBudget: 3 },
1466
+ snapshot: {
1467
+ mode: "off",
1468
+ ticksToday: 0,
1469
+ lastFireAt: null,
1470
+ nextTickAt: null,
1471
+ recentTicks: []
1472
+ }
1473
+ });
1474
+ });
1475
+ return true;
1476
+ }
1477
+ if (msg.type === "projects.always_on_set") {
1478
+ log3.info(
1479
+ { encodedCwd: msg.encodedCwd, patch: msg.config },
1480
+ "projects.always_on_set"
1481
+ );
1482
+ handleProjectsAlwaysOnSet(msg.encodedCwd, msg.config).then(
1483
+ (r) => this.send({
1484
+ type: "projects.always_on_set_result",
1485
+ requestId: msg.requestId,
1486
+ ...r
1487
+ })
1488
+ ).catch((err) => {
1489
+ log3.error(
1490
+ { err, encodedCwd: msg.encodedCwd },
1491
+ "projects.always_on_set failed"
1492
+ );
1493
+ this.send({
1494
+ type: "projects.always_on_set_result",
1495
+ requestId: msg.requestId,
1496
+ projectFound: false,
1497
+ config: { mode: "off", dailyBudget: 3 },
1498
+ snapshot: {
1499
+ mode: "off",
1500
+ ticksToday: 0,
1501
+ lastFireAt: null,
1502
+ nextTickAt: null,
1503
+ recentTicks: []
1504
+ },
1505
+ error: err instanceof Error ? err.message : String(err)
1506
+ });
1507
+ });
1508
+ return true;
1509
+ }
1510
+ if (msg.type === "projects.always_on_tick_now") {
1511
+ log3.info({ encodedCwd: msg.encodedCwd }, "projects.always_on_tick_now");
1512
+ handleProjectsAlwaysOnTickNow(msg.encodedCwd).then(
1513
+ (r) => this.send({
1514
+ type: "projects.always_on_tick_now_result",
1515
+ requestId: msg.requestId,
1516
+ ...r
1517
+ })
1518
+ ).catch((err) => {
1519
+ log3.error(
1520
+ { err, encodedCwd: msg.encodedCwd },
1521
+ "projects.always_on_tick_now failed"
1522
+ );
1523
+ this.send({
1524
+ type: "projects.always_on_tick_now_result",
1525
+ requestId: msg.requestId,
1526
+ projectFound: false,
1527
+ error: err instanceof Error ? err.message : String(err)
1528
+ });
1529
+ });
1530
+ return true;
1531
+ }
883
1532
  if (msg.type === "list_roles") {
884
- log2.info("list_roles");
1533
+ log3.info("list_roles");
885
1534
  const roleNames = listRoles();
886
1535
  const roles = roleNames.map((name) => {
887
1536
  try {
@@ -907,7 +1556,7 @@ var HubConnection = class {
907
1556
  return true;
908
1557
  }
909
1558
  if (msg.type === "list_templates") {
910
- log2.info("list_templates");
1559
+ log3.info("list_templates");
911
1560
  try {
912
1561
  const names = listTemplates();
913
1562
  const templates = names.map((name) => {
@@ -926,7 +1575,7 @@ var HubConnection = class {
926
1575
  templates
927
1576
  });
928
1577
  } catch (err) {
929
- log2.error({ err }, "list_templates failed");
1578
+ log3.error({ err }, "list_templates failed");
930
1579
  this.send({
931
1580
  type: "templates_list_result",
932
1581
  sessionId: msg.sessionId,
@@ -938,9 +1587,9 @@ var HubConnection = class {
938
1587
  if (msg.type === "generic.request") {
939
1588
  const handler = this.namespaceHandlers.get(msg.namespace);
940
1589
  if (handler) {
941
- log2.info({ namespace: msg.namespace, action: msg.action, requestId: msg.requestId }, "generic.request received");
1590
+ log3.info({ namespace: msg.namespace, action: msg.action, requestId: msg.requestId }, "generic.request received");
942
1591
  handler(msg).then((result) => {
943
- log2.info({ namespace: msg.namespace, action: msg.action, requestId: msg.requestId }, "generic.request handled OK");
1592
+ log3.info({ namespace: msg.namespace, action: msg.action, requestId: msg.requestId }, "generic.request handled OK");
944
1593
  this.send({
945
1594
  type: "generic.response",
946
1595
  namespace: msg.namespace,
@@ -949,7 +1598,7 @@ var HubConnection = class {
949
1598
  requestId: msg.requestId
950
1599
  });
951
1600
  }).catch((err) => {
952
- log2.error({ namespace: msg.namespace, action: msg.action, requestId: msg.requestId, err }, "generic.request handler error");
1601
+ log3.error({ namespace: msg.namespace, action: msg.action, requestId: msg.requestId, err }, "generic.request handler error");
953
1602
  this.send({
954
1603
  type: "generic.response",
955
1604
  namespace: msg.namespace,
@@ -961,7 +1610,7 @@ var HubConnection = class {
961
1610
  });
962
1611
  return true;
963
1612
  }
964
- log2.warn({ namespace: msg.namespace, action: msg.action, requestId: msg.requestId }, "generic.request unknown namespace");
1613
+ log3.warn({ namespace: msg.namespace, action: msg.action, requestId: msg.requestId }, "generic.request unknown namespace");
965
1614
  this.send({
966
1615
  type: "generic.response",
967
1616
  namespace: msg.namespace,
@@ -978,9 +1627,9 @@ var HubConnection = class {
978
1627
  defaultWorkDir: process.cwd(),
979
1628
  home: process.env.CLAWNET_HOME
980
1629
  }).then((result) => {
981
- log2.info({ type: msg.type, ok: result.ok, swarmId: result.swarmId, error: result.error }, "swarm control handled");
1630
+ log3.info({ type: msg.type, ok: result.ok, swarmId: result.swarmId, error: result.error }, "swarm control handled");
982
1631
  }).catch((err) => {
983
- log2.error({ err, type: msg.type }, "swarm control crashed");
1632
+ log3.error({ err, type: msg.type }, "swarm control crashed");
984
1633
  });
985
1634
  return true;
986
1635
  }
@@ -1009,7 +1658,7 @@ var HubConnection = class {
1009
1658
  ...result.error ? { error: result.error } : {}
1010
1659
  });
1011
1660
  }).catch((err) => {
1012
- log2.error({ err, swarmId: msg.swarmId }, "projects.swarm_add_role crashed");
1661
+ log3.error({ err, swarmId: msg.swarmId }, "projects.swarm_add_role crashed");
1013
1662
  this.send({
1014
1663
  type: "projects.swarm_add_role_result",
1015
1664
  requestId,
@@ -1022,12 +1671,13 @@ var HubConnection = class {
1022
1671
  if (msg.type === "swarm.execute" && this.swarmCoordinator) {
1023
1672
  const { sessionId, content, workDir, targetInstance, crewConfig } = msg;
1024
1673
  if (this.swarmCoordinator.hasSwarm(sessionId)) {
1025
- log2.info({ sessionId, targetInstance }, "swarm.execute: forwarding to existing swarm");
1674
+ log3.info({ sessionId, targetInstance }, "swarm.execute: forwarding to existing swarm");
1026
1675
  this.swarmCoordinator.handleUserMessage(sessionId, content, targetInstance).catch((err) => {
1027
1676
  this.send({
1028
1677
  type: "session.error",
1029
1678
  sessionId,
1030
- error: err instanceof Error ? err.message : String(err)
1679
+ error: err instanceof Error ? err.message : String(err),
1680
+ code: extractErrorCode(err)
1031
1681
  });
1032
1682
  });
1033
1683
  } else if (this.finishedSwarms.has(sessionId)) {
@@ -1036,16 +1686,17 @@ var HubConnection = class {
1036
1686
  const templateName = crewConfig.templateName;
1037
1687
  const roles = crewConfig.roles;
1038
1688
  const displayName = crewConfig.displayName;
1039
- log2.info({ sessionId, templateName, rolesCount: roles?.length }, "swarm.execute: continuing finished swarm");
1689
+ log3.info({ sessionId, templateName, rolesCount: roles?.length }, "swarm.execute: continuing finished swarm");
1040
1690
  this.swarmCoordinator.create(sessionId, { workDir, templateName, displayName, roles, task: content, isContinuation: true }).catch((err) => {
1041
1691
  this.send({
1042
1692
  type: "session.error",
1043
1693
  sessionId,
1044
- error: err instanceof Error ? err.message : String(err)
1694
+ error: err instanceof Error ? err.message : String(err),
1695
+ code: extractErrorCode(err)
1045
1696
  });
1046
1697
  });
1047
1698
  } else {
1048
- log2.info({ sessionId }, "swarm.execute ignored: swarm finished, no config to recreate");
1699
+ log3.info({ sessionId }, "swarm.execute ignored: swarm finished, no config to recreate");
1049
1700
  this.send({
1050
1701
  type: "session.error",
1051
1702
  sessionId,
@@ -1056,16 +1707,17 @@ var HubConnection = class {
1056
1707
  const templateName = crewConfig.templateName;
1057
1708
  const roles = crewConfig.roles;
1058
1709
  const displayName = crewConfig.displayName;
1059
- log2.info({ sessionId, templateName, rolesCount: roles?.length }, "swarm.execute: creating new swarm");
1710
+ log3.info({ sessionId, templateName, rolesCount: roles?.length }, "swarm.execute: creating new swarm");
1060
1711
  this.swarmCoordinator.create(sessionId, { workDir, templateName, displayName, roles, task: content }).catch((err) => {
1061
1712
  this.send({
1062
1713
  type: "session.error",
1063
1714
  sessionId,
1064
- error: err instanceof Error ? err.message : String(err)
1715
+ error: err instanceof Error ? err.message : String(err),
1716
+ code: extractErrorCode(err)
1065
1717
  });
1066
1718
  });
1067
1719
  } else {
1068
- log2.info({ sessionId }, "swarm.execute ignored: swarm not found, no config");
1720
+ log3.info({ sessionId }, "swarm.execute ignored: swarm not found, no config");
1069
1721
  this.send({
1070
1722
  type: "session.error",
1071
1723
  sessionId,
@@ -1077,7 +1729,7 @@ var HubConnection = class {
1077
1729
  if (!this.sessionManager) return false;
1078
1730
  if (msg.type === "abort_execution") {
1079
1731
  const { sessionId } = msg;
1080
- log2.info({ sessionId }, "abort_execution");
1732
+ log3.info({ sessionId }, "abort_execution");
1081
1733
  if (this.sessionManager?.hasSession(sessionId)) {
1082
1734
  this.sessionManager.abortSession(sessionId).then(() => {
1083
1735
  this.send({ type: "execution_aborted", sessionId });
@@ -1093,11 +1745,11 @@ var HubConnection = class {
1093
1745
  const { sessionId, content, workDir, backendSessionId, useBrainCore } = msg;
1094
1746
  const sm = this.sessionManager;
1095
1747
  const spawnAndSend = (resumeId, label, maxOutputTokens) => {
1096
- log2.info(
1748
+ log3.info(
1097
1749
  { sessionId, backendSessionId: resumeId, workDir, label, maxOutputTokens },
1098
1750
  "claude.execute: spawning"
1099
1751
  );
1100
- log2.debug({ sessionId, ...previewFields(content) }, "claude.execute: input");
1752
+ log3.debug({ sessionId, ...previewFields(content) }, "claude.execute: input");
1101
1753
  sm.createSession({
1102
1754
  sessionId,
1103
1755
  workDir,
@@ -1107,47 +1759,260 @@ var HubConnection = class {
1107
1759
  maxOutputTokens
1108
1760
  }).then(() => {
1109
1761
  sm.sendUserInput(sessionId, content).catch((err) => {
1110
- log2.warn({ err, sessionId }, "sendUserInput failed");
1762
+ log3.warn({ err, sessionId }, "sendUserInput failed");
1111
1763
  });
1112
1764
  }).catch((err) => {
1113
1765
  this.send({
1114
1766
  type: "session.error",
1115
1767
  sessionId,
1116
- error: err instanceof Error ? err.message : String(err)
1768
+ error: err instanceof Error ? err.message : String(err),
1769
+ code: extractErrorCode(err)
1770
+ });
1771
+ });
1772
+ };
1773
+ if (sm.isHealthy(sessionId)) {
1774
+ log3.info({ sessionId }, "claude.execute: reusing healthy session");
1775
+ log3.debug({ sessionId, ...previewFields(content) }, "claude.execute: input");
1776
+ sm.sendUserInput(sessionId, content).catch((err) => {
1777
+ log3.warn({ err, sessionId }, "sendUserInput failed");
1778
+ });
1779
+ } else if (sm.hasSession(sessionId) && backendSessionId) {
1780
+ const recommendedMax = sm.getRecommendedMaxOutputTokens(sessionId);
1781
+ log3.warn({ sessionId, backendSessionId }, "claude.execute: session unhealthy, recreating with --resume");
1782
+ this.send({
1783
+ type: "session.died",
1784
+ sessionId,
1785
+ reason: "unhealthy_before_input"
1786
+ });
1787
+ sm.abortSession(sessionId).catch((err) => {
1788
+ log3.warn({ sessionId, err: err instanceof Error ? err.message : String(err) }, "abortSession failed during unhealthy fallback, proceeding to respawn");
1789
+ }).then(() => spawnAndSend(backendSessionId, "unhealthy_fallback_resume", recommendedMax));
1790
+ } else if (backendSessionId) {
1791
+ spawnAndSend(backendSessionId, "fresh_resume", sm.getRecommendedMaxOutputTokens(sessionId));
1792
+ } else {
1793
+ log3.warn(
1794
+ { sessionId, workDir },
1795
+ "claude.execute: entering brand_new spawn path \u2014 no backendSessionId from hub; if this sessionId already has prior turns in db, upstream hub\u2192agent hydration is broken"
1796
+ );
1797
+ spawnAndSend(void 0, "brand_new", void 0);
1798
+ }
1799
+ return true;
1800
+ }
1801
+ if (msg.type === "session.create") {
1802
+ const {
1803
+ sessionId,
1804
+ workDir,
1805
+ backend,
1806
+ sticky,
1807
+ systemPrompt,
1808
+ useBrainCore,
1809
+ initialInput,
1810
+ backendSessionId,
1811
+ useWorktree,
1812
+ worktreeBranchName
1813
+ } = msg;
1814
+ const sm = this.sessionManager;
1815
+ log3.info(
1816
+ {
1817
+ sessionId,
1818
+ backend,
1819
+ hasSticky: !!sticky,
1820
+ hasInitialInput: !!initialInput,
1821
+ useWorktree: !!useWorktree
1822
+ },
1823
+ "session.create"
1824
+ );
1825
+ if (sm.hasSession(sessionId)) {
1826
+ log3.warn({ sessionId }, "session.create: session already exists, ignoring");
1827
+ return true;
1828
+ }
1829
+ (async () => {
1830
+ let effectiveWorkDir = workDir;
1831
+ let workspaceHandle;
1832
+ let workspaceEnv;
1833
+ if (useWorktree && workDir && this.workspaceManager) {
1834
+ try {
1835
+ const swarmId = `chat-${sessionId.slice(0, 8)}`;
1836
+ workspaceHandle = await this.workspaceManager.prepare({
1837
+ swarmId,
1838
+ projectRoot: workDir,
1839
+ // PR-A: forward user-supplied branch hint (hub already prepended
1840
+ // `clawnet/chat-` so this is the fully-qualified name). When
1841
+ // omitted, GitWorktreeProvider falls back to
1842
+ // `clawnet/swarm-chat-<sid8>` (its own default).
1843
+ ...worktreeBranchName ? { branchName: worktreeBranchName } : {}
1844
+ });
1845
+ effectiveWorkDir = workspaceHandle.cwd;
1846
+ workspaceEnv = { CLAWNET_PROJECT_ROOT: workDir };
1847
+ this.send({
1848
+ type: "session.worktree_ready",
1849
+ sessionId,
1850
+ cwd: workspaceHandle.cwd,
1851
+ strategy: workspaceHandle.strategy,
1852
+ branchName: workspaceHandle.metadata.branchName ?? null,
1853
+ // PR-A review fix I3: forward the real base branch so ship
1854
+ // can target it instead of guessing "main".
1855
+ baseBranch: workspaceHandle.metadata.baseBranch ?? null,
1856
+ projectRoot: workDir
1857
+ });
1858
+ } catch (err) {
1859
+ log3.warn(
1860
+ {
1861
+ err: err instanceof Error ? err.message : String(err),
1862
+ sessionId,
1863
+ workDir
1864
+ },
1865
+ "session.create: workspace prepare failed, falling back to direct workDir"
1866
+ );
1867
+ workspaceHandle = void 0;
1868
+ workspaceEnv = void 0;
1869
+ effectiveWorkDir = workDir;
1870
+ }
1871
+ }
1872
+ try {
1873
+ await sm.createSession({
1874
+ sessionId,
1875
+ workDir: effectiveWorkDir,
1876
+ resumeId: backendSessionId,
1877
+ backend,
1878
+ model: sticky?.model,
1879
+ mode: sticky?.mode,
1880
+ systemPrompt,
1881
+ useBrainCore,
1882
+ roleId: DEFAULT_ASSISTANT_ROLE_ID,
1883
+ initialUserInput: initialInput,
1884
+ ...workspaceEnv ? { env: workspaceEnv } : {}
1885
+ });
1886
+ if (workspaceHandle) {
1887
+ sm.attachWorkspace(sessionId, workspaceHandle);
1888
+ }
1889
+ if (initialInput !== void 0) {
1890
+ await sm.sendUserInput(sessionId, initialInput);
1891
+ }
1892
+ } catch (err) {
1893
+ if (workspaceHandle && this.workspaceManager && sm.getWorkspaceHandle(sessionId) === void 0) {
1894
+ this.workspaceManager.dispose(workspaceHandle.swarmId, { keep: false }).catch(
1895
+ (e) => log3.warn(
1896
+ { err: e instanceof Error ? e.message : String(e), sessionId },
1897
+ "best-effort workspace dispose after createSession failure"
1898
+ )
1899
+ );
1900
+ }
1901
+ this.send({
1902
+ type: "session.error",
1903
+ sessionId,
1904
+ error: err instanceof Error ? err.message : String(err),
1905
+ code: extractErrorCode(err)
1117
1906
  });
1118
- });
1119
- };
1120
- if (sm.isHealthy(sessionId)) {
1121
- log2.info({ sessionId }, "claude.execute: reusing healthy session");
1122
- log2.debug({ sessionId, ...previewFields(content) }, "claude.execute: input");
1123
- sm.sendUserInput(sessionId, content).catch((err) => {
1124
- log2.warn({ err, sessionId }, "sendUserInput failed");
1125
- });
1126
- } else if (sm.hasSession(sessionId) && backendSessionId) {
1127
- const recommendedMax = sm.getRecommendedMaxOutputTokens(sessionId);
1128
- log2.warn({ sessionId, backendSessionId }, "claude.execute: session unhealthy, recreating with --resume");
1907
+ }
1908
+ })().catch((err) => {
1909
+ log3.error(
1910
+ { sessionId, err: err instanceof Error ? err.message : String(err) },
1911
+ "session.create IIFE unexpectedly threw"
1912
+ );
1913
+ });
1914
+ return true;
1915
+ }
1916
+ if (msg.type === "session.input") {
1917
+ const { sessionId, content, messageMeta, backendSessionId, workDir, useBrainCore } = msg;
1918
+ const sm = this.sessionManager;
1919
+ log3.debug(
1920
+ {
1921
+ sessionId,
1922
+ hasMessageMeta: !!messageMeta,
1923
+ hasBackendSessionId: !!backendSessionId,
1924
+ ...previewFields(content)
1925
+ },
1926
+ "session.input"
1927
+ );
1928
+ if (!sm.isHealthy(sessionId)) {
1929
+ if (backendSessionId) {
1930
+ log3.info(
1931
+ { sessionId, backendSessionId, hadSession: sm.hasSession(sessionId) },
1932
+ "session.input: auto-recovering via --resume"
1933
+ );
1934
+ (async () => {
1935
+ try {
1936
+ if (sm.hasSession(sessionId)) {
1937
+ this.send({
1938
+ type: "session.died",
1939
+ sessionId,
1940
+ reason: "unhealthy_before_input"
1941
+ });
1942
+ await sm.abortSession(sessionId).catch((err) => {
1943
+ log3.warn(
1944
+ { sessionId, err: err instanceof Error ? err.message : String(err) },
1945
+ "abortSession failed during session.input recovery, continuing"
1946
+ );
1947
+ });
1948
+ }
1949
+ if (messageMeta) {
1950
+ sm.setTurnOverride(sessionId, {
1951
+ model: messageMeta.model,
1952
+ mode: messageMeta.mode
1953
+ });
1954
+ }
1955
+ await sm.createSession({
1956
+ sessionId,
1957
+ resumeId: backendSessionId,
1958
+ roleId: DEFAULT_ASSISTANT_ROLE_ID,
1959
+ // (Fix Important #2) Recovery spawn needs cwd + braincore mounts.
1960
+ // Hub forwards these from chat_sessions.{work_dir,use_brain_core}
1961
+ // on the session.input frame. The healthy path doesn't need them
1962
+ // (existing process has its own cwd already).
1963
+ workDir,
1964
+ useBrainCore
1965
+ });
1966
+ await sm.sendUserInput(sessionId, content);
1967
+ } catch (err) {
1968
+ this.send({
1969
+ type: "session.error",
1970
+ sessionId,
1971
+ error: err instanceof Error ? err.message : String(err),
1972
+ code: extractErrorCode(err)
1973
+ });
1974
+ }
1975
+ })().catch((err) => {
1976
+ log3.error(
1977
+ { sessionId, err: err instanceof Error ? err.message : String(err) },
1978
+ "session.input recovery IIFE unexpectedly threw"
1979
+ );
1980
+ });
1981
+ return true;
1982
+ }
1983
+ log3.warn(
1984
+ { sessionId, hasSession: sm.hasSession(sessionId) },
1985
+ "session.input: session not healthy and no backendSessionId \u2014 cannot recover"
1986
+ );
1129
1987
  this.send({
1130
- type: "session.died",
1988
+ type: "session.error",
1131
1989
  sessionId,
1132
- reason: "unhealthy_before_input"
1990
+ error: "session.input received but session not healthy and no backendSessionId provided for recovery"
1133
1991
  });
1134
- sm.abortSession(sessionId).catch((err) => {
1135
- log2.warn({ sessionId, err: err instanceof Error ? err.message : String(err) }, "abortSession failed during unhealthy fallback, proceeding to respawn");
1136
- }).then(() => spawnAndSend(backendSessionId, "unhealthy_fallback_resume", recommendedMax));
1137
- } else if (backendSessionId) {
1138
- spawnAndSend(backendSessionId, "fresh_resume", sm.getRecommendedMaxOutputTokens(sessionId));
1139
- } else {
1140
- log2.warn(
1141
- { sessionId, workDir },
1142
- "claude.execute: entering brand_new spawn path \u2014 no backendSessionId from hub; if this sessionId already has prior turns in db, upstream hub\u2192agent hydration is broken"
1143
- );
1144
- spawnAndSend(void 0, "brand_new", void 0);
1992
+ return true;
1993
+ }
1994
+ if (messageMeta) {
1995
+ sm.setTurnOverride(sessionId, { model: messageMeta.model, mode: messageMeta.mode });
1145
1996
  }
1997
+ sm.sendUserInput(sessionId, content).catch((err) => {
1998
+ this.send({
1999
+ type: "session.error",
2000
+ sessionId,
2001
+ error: err instanceof Error ? err.message : String(err),
2002
+ code: extractErrorCode(err)
2003
+ });
2004
+ });
2005
+ return true;
2006
+ }
2007
+ if (msg.type === "session.update_sticky") {
2008
+ const { sessionId, sticky } = msg;
2009
+ log3.info({ sessionId, sticky }, "session.update_sticky");
2010
+ this.sessionManager.updateSticky(sessionId, sticky);
1146
2011
  return true;
1147
2012
  }
1148
2013
  if (msg.type === "session.force_restart") {
1149
2014
  const { sessionId, reason } = msg;
1150
- log2.warn({ sessionId, reason }, "session.force_restart received");
2015
+ log3.warn({ sessionId, reason }, "session.force_restart received");
1151
2016
  const sm = this.sessionManager;
1152
2017
  const reply = {
1153
2018
  type: "session.died",
@@ -1156,7 +2021,7 @@ var HubConnection = class {
1156
2021
  };
1157
2022
  if (sm.hasSession(sessionId)) {
1158
2023
  sm.abortSession(sessionId).catch((err) => {
1159
- log2.warn({ sessionId, err: err instanceof Error ? err.message : String(err) }, "abortSession failed during force_restart");
2024
+ log3.warn({ sessionId, err: err instanceof Error ? err.message : String(err) }, "abortSession failed during force_restart");
1160
2025
  }).finally(() => {
1161
2026
  this.send(reply);
1162
2027
  });
@@ -1165,49 +2030,27 @@ var HubConnection = class {
1165
2030
  }
1166
2031
  return true;
1167
2032
  }
1168
- if (msg.type === "session.create") {
1169
- log2.info({ sessionId: msg.sessionId, roleId: DEFAULT_ASSISTANT_ROLE_ID }, "session.create with memory injection");
1170
- this.sessionManager.createSession({
1171
- sessionId: msg.sessionId,
1172
- workDir: msg.workDir,
1173
- resumeId: msg.resumeId,
1174
- roleId: DEFAULT_ASSISTANT_ROLE_ID
1175
- }).then((backendSessionId) => {
1176
- this.send({
1177
- type: "session.created",
1178
- sessionId: msg.sessionId,
1179
- backendSessionId
1180
- });
1181
- }).catch((err) => {
1182
- this.send({
1183
- type: "session.error",
1184
- sessionId: msg.sessionId,
1185
- error: err instanceof Error ? err.message : String(err)
1186
- });
1187
- });
1188
- return true;
1189
- }
1190
2033
  if (msg.type === "session.close") {
1191
- log2.info({ sessionId: msg.sessionId }, "session.close");
2034
+ log3.info({ sessionId: msg.sessionId }, "session.close");
1192
2035
  this.sessionManager.closeSession(msg.sessionId).catch(() => {
1193
2036
  });
1194
2037
  return true;
1195
2038
  }
1196
2039
  if (msg.type === "claude.input") {
1197
- log2.debug(
2040
+ log3.debug(
1198
2041
  { sessionId: msg.sessionId, ...previewFields(msg.content) },
1199
2042
  "claude.input"
1200
2043
  );
1201
2044
  this.sessionManager.sendUserInput(msg.sessionId, msg.content).catch((err) => {
1202
- log2.warn({ err, sessionId: msg.sessionId }, "sendUserInput failed");
2045
+ log3.warn({ err, sessionId: msg.sessionId }, "sendUserInput failed");
1203
2046
  });
1204
2047
  return true;
1205
2048
  }
1206
2049
  if (msg.type === "client.permission_decision") {
1207
2050
  const { sessionId, requestId, decision, reason } = msg;
1208
- log2.info({ sessionId, requestId, decision }, "client.permission_decision");
2051
+ log3.info({ sessionId, requestId, decision }, "client.permission_decision");
1209
2052
  this.sessionManager.respondToPermission(sessionId, { callId: requestId, decision, reason }).catch((err) => {
1210
- log2.warn({ err, sessionId, requestId }, "respondToPermission failed");
2053
+ log3.warn({ err, sessionId, requestId }, "respondToPermission failed");
1211
2054
  });
1212
2055
  return true;
1213
2056
  }
@@ -1222,24 +2065,24 @@ var HubConnection = class {
1222
2065
  const recoverableIds = new Set(snapshots.map((s) => s.id));
1223
2066
  for (const { swarmId } of allIds) {
1224
2067
  if (!recoverableIds.has(swarmId)) {
1225
- log2.info({ swarmId }, "skipping non-recoverable swarm snapshot (kept on disk)");
2068
+ log3.info({ swarmId }, "skipping non-recoverable swarm snapshot (kept on disk)");
1226
2069
  }
1227
2070
  }
1228
2071
  for (const snap of snapshots) {
1229
- log2.info({ swarmId: snap.id }, "recovering swarm");
2072
+ log3.info({ swarmId: snap.id }, "recovering swarm");
1230
2073
  recoverSwarm(this.swarmCoordinator, snap).catch((err) => {
1231
- log2.error({ err, swarmId: snap.id }, "failed to recover swarm");
2074
+ log3.error({ err, swarmId: snap.id }, "failed to recover swarm");
1232
2075
  });
1233
2076
  }
1234
2077
  } catch (err) {
1235
- log2.error({ err }, "swarm recovery failed");
2078
+ log3.error({ err }, "swarm recovery failed");
1236
2079
  }
1237
2080
  }
1238
2081
  reportLiveSessions() {
1239
2082
  if (!this.sessionManager) return;
1240
2083
  const ids = this.sessionManager.getActiveSessionIds();
1241
2084
  if (ids.length === 0) return;
1242
- log2.info({ count: ids.length }, "reporting live sessions to hub after reconnect");
2085
+ log3.info({ count: ids.length }, "reporting live sessions to hub after reconnect");
1243
2086
  for (const sessionId of ids) {
1244
2087
  this.send({
1245
2088
  type: "session.state",
@@ -1259,7 +2102,7 @@ var HubConnection = class {
1259
2102
  this.heartbeatTimer = setInterval(() => {
1260
2103
  const sinceAck = Date.now() - this.lastAckAt;
1261
2104
  if (sinceAck > this.heartbeatInterval * 2.5) {
1262
- log2.warn(
2105
+ log3.warn(
1263
2106
  { sinceAckMs: sinceAck, heartbeatIntervalMs: this.heartbeatInterval },
1264
2107
  "no ack from hub \u2014 terminating dead socket"
1265
2108
  );
@@ -1289,15 +2132,15 @@ var HubConnection = class {
1289
2132
  */
1290
2133
  startWakeWatch() {
1291
2134
  this.stopWakeWatch();
1292
- const intervalMs = 1e3;
1293
- const jumpThresholdMs = 5e3;
2135
+ const intervalMs = WAKE_WATCH_INTERVAL_MS;
2136
+ const jumpThresholdMs = WAKE_WATCH_JUMP_THRESHOLD_MS;
1294
2137
  this.lastWakeTickAt = Date.now();
1295
2138
  this.wakeWatchTimer = setInterval(() => {
1296
2139
  const now = Date.now();
1297
2140
  const drift = now - this.lastWakeTickAt - intervalMs;
1298
2141
  this.lastWakeTickAt = now;
1299
2142
  if (drift <= jumpThresholdMs) return;
1300
- log2.warn(
2143
+ log3.warn(
1301
2144
  { driftMs: drift, hasSocket: !!this.ws },
1302
2145
  "wake detected (clock jump) \u2014 forcing reconnect"
1303
2146
  );
@@ -1352,7 +2195,7 @@ var HubConnection = class {
1352
2195
  }
1353
2196
  this.ws = null;
1354
2197
  }
1355
- log2.warn({ reason }, "forced reconnect");
2198
+ log3.warn({ reason }, "forced reconnect");
1356
2199
  this.authState = "pending";
1357
2200
  this.scheduleReconnect();
1358
2201
  }
@@ -1362,7 +2205,7 @@ var HubConnection = class {
1362
2205
  const cap = Math.min(this.reconnectDelay, this.maxReconnectDelay);
1363
2206
  const base = Math.min(DEFAULT_RECONNECT_MS, cap);
1364
2207
  const delay = Math.floor(base + Math.random() * Math.max(0, cap - base));
1365
- log2.warn({ delayMs: delay, capMs: cap }, "reconnecting to hub...");
2208
+ log3.warn({ delayMs: delay, capMs: cap }, "reconnecting to hub...");
1366
2209
  this.reconnectTimer = setTimeout(() => {
1367
2210
  this.reconnectTimer = null;
1368
2211
  this.connect();
@@ -1401,8 +2244,8 @@ import {
1401
2244
  MAX_STDOUT_BYTES,
1402
2245
  MAX_STDERR_BYTES
1403
2246
  } from "@mclawnet/scheduler";
1404
- import { createLogger as createLogger3 } from "@mclawnet/logger";
1405
- var log3 = createLogger3({ module: "agent/schedule-runtime" });
2247
+ import { createLogger as createLogger4 } from "@mclawnet/logger";
2248
+ var log4 = createLogger4({ module: "agent/schedule-runtime" });
1406
2249
  var SPAWN_STDOUT_CAP_BYTES = 1024 * 1024;
1407
2250
  var SPAWN_STDERR_CAP_BYTES = 256 * 1024;
1408
2251
  var ScheduleRuntime = class {
@@ -1512,7 +2355,7 @@ var ScheduleRuntime = class {
1512
2355
  void this.track(this.handleLog(m, requestId));
1513
2356
  return;
1514
2357
  default:
1515
- log3.warn({ type: msg.type }, "schedule-runtime: unknown message type");
2358
+ log4.warn({ type: msg.type }, "schedule-runtime: unknown message type");
1516
2359
  if (requestId !== void 0) {
1517
2360
  this.safeSend({
1518
2361
  type: "schedule:error",
@@ -1645,7 +2488,7 @@ var ScheduleRuntime = class {
1645
2488
  }
1646
2489
  sendError(requestId, err) {
1647
2490
  const message = err instanceof Error ? err.message : String(err);
1648
- log3.warn({ err: message, requestId }, "schedule-runtime: branch failed");
2491
+ log4.warn({ err: message, requestId }, "schedule-runtime: branch failed");
1649
2492
  this.safeSend({
1650
2493
  type: "schedule:error",
1651
2494
  requestId,
@@ -1775,7 +2618,7 @@ function makeRealOneShotSpawn() {
1775
2618
  };
1776
2619
  if (opts.signal.aborted) onAbort();
1777
2620
  else opts.signal.addEventListener("abort", onAbort, { once: true });
1778
- return new Promise((resolve, reject) => {
2621
+ return new Promise((resolve3, reject) => {
1779
2622
  proc.once("error", (err) => {
1780
2623
  opts.signal.removeEventListener("abort", onAbort);
1781
2624
  reject(err);
@@ -1786,7 +2629,7 @@ function makeRealOneShotSpawn() {
1786
2629
  tryExtractAssistant(stdoutLineResidual);
1787
2630
  stdoutLineResidual = "";
1788
2631
  }
1789
- resolve({
2632
+ resolve3({
1790
2633
  exitCode: code ?? -1,
1791
2634
  stdout: stdoutBuf.toString(),
1792
2635
  stderr: stderrBuf.toString(),
@@ -1803,7 +2646,7 @@ function makeRealSwarmStarter(_deps) {
1803
2646
  }
1804
2647
 
1805
2648
  // src/session-manager.ts
1806
- import { createLogger as createLogger5, previewFields as previewFields2 } from "@mclawnet/logger";
2649
+ import { createLogger as createLogger6, previewFields as previewFields2 } from "@mclawnet/logger";
1807
2650
  import { existsSync as existsSync4 } from "fs";
1808
2651
  import { homedir as homedir4 } from "os";
1809
2652
  import { join as join4 } from "path";
@@ -1816,15 +2659,15 @@ import {
1816
2659
  import { MAX_TOKENS_LADDER, clampLadderIndex } from "@mclawnet/shared";
1817
2660
 
1818
2661
  // src/skill-loader.ts
1819
- import { existsSync as existsSync3, mkdirSync, readdirSync as readdirSync3, readFileSync as readFileSync2, writeFileSync } from "fs";
2662
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync as readdirSync3, readFileSync as readFileSync3, writeFileSync } from "fs";
1820
2663
  import { createHash } from "crypto";
1821
- import { join as join3, dirname } from "path";
2664
+ import { join as join3, dirname as dirname2 } from "path";
1822
2665
  import { homedir as homedir3 } from "os";
1823
2666
  import { createRequire } from "module";
1824
2667
  import { fileURLToPath } from "url";
1825
- import { createLogger as createLogger4 } from "@mclawnet/logger";
2668
+ import { createLogger as createLogger5 } from "@mclawnet/logger";
1826
2669
  import { ManifestManager, mergeSkillSections } from "@mclawnet/skill-manager";
1827
- var log4 = createLogger4({ module: "agent/skill-loader" });
2670
+ var log5 = createLogger5({ module: "agent/skill-loader" });
1828
2671
  var CLAWNET_DIR = join3(homedir3(), ".clawnet");
1829
2672
  var SKILLS_DIR = join3(CLAWNET_DIR, ".claude", "skills");
1830
2673
  var MCP_CONFIG_PATH = join3(CLAWNET_DIR, "mcp.json");
@@ -1836,7 +2679,7 @@ async function initSkills() {
1836
2679
  }
1837
2680
  function defaultBuiltinSourceDir() {
1838
2681
  const thisFile = fileURLToPath(import.meta.url);
1839
- return join3(dirname(thisFile), "..", "skills");
2682
+ return join3(dirname2(thisFile), "..", "skills");
1840
2683
  }
1841
2684
  function sha256(s) {
1842
2685
  return createHash("sha256").update(s).digest("hex");
@@ -1847,9 +2690,9 @@ function readBuiltinVersion(content) {
1847
2690
  }
1848
2691
  function syncBuiltinSkills(rootDir, srcDir) {
1849
2692
  const skillsDir = join3(rootDir, ".claude", "skills");
1850
- if (!existsSync3(skillsDir)) mkdirSync(skillsDir, { recursive: true });
2693
+ if (!existsSync3(skillsDir)) mkdirSync2(skillsDir, { recursive: true });
1851
2694
  if (!existsSync3(srcDir)) {
1852
- log4.debug({ srcDir }, "no built-in skills directory found, skipping");
2695
+ log5.debug({ srcDir }, "no built-in skills directory found, skipping");
1853
2696
  return;
1854
2697
  }
1855
2698
  const manifest = new ManifestManager(rootDir);
@@ -1857,7 +2700,7 @@ function syncBuiltinSkills(rootDir, srcDir) {
1857
2700
  try {
1858
2701
  entries = readdirSync3(srcDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
1859
2702
  } catch {
1860
- log4.warn({ srcDir }, "failed to read built-in skills directory");
2703
+ log5.warn({ srcDir }, "failed to read built-in skills directory");
1861
2704
  return;
1862
2705
  }
1863
2706
  for (const skillName of entries) {
@@ -1868,26 +2711,26 @@ function syncBuiltinSkills(rootDir, srcDir) {
1868
2711
  const baseSnapshotPath = join3(destDir, ".base.md");
1869
2712
  let officialContent;
1870
2713
  try {
1871
- officialContent = readFileSync2(srcSkillMd, "utf-8");
2714
+ officialContent = readFileSync3(srcSkillMd, "utf-8");
1872
2715
  } catch (err) {
1873
- log4.warn({ skill: skillName, err }, "failed to read built-in skill");
2716
+ log5.warn({ skill: skillName, err }, "failed to read built-in skill");
1874
2717
  continue;
1875
2718
  }
1876
2719
  const officialHash = sha256(officialContent);
1877
2720
  const officialVersion = readBuiltinVersion(officialContent);
1878
2721
  if (!existsSync3(destSkillMd)) {
1879
2722
  try {
1880
- mkdirSync(destDir, { recursive: true });
2723
+ mkdirSync2(destDir, { recursive: true });
1881
2724
  writeFileSync(destSkillMd, officialContent);
1882
2725
  writeFileSync(baseSnapshotPath, officialContent);
1883
2726
  manifest.register(skillName, officialHash, officialVersion);
1884
- log4.info({ skill: skillName, version: officialVersion }, "installed built-in skill");
2727
+ log5.info({ skill: skillName, version: officialVersion }, "installed built-in skill");
1885
2728
  } catch (err) {
1886
- log4.warn({ skill: skillName, err }, "failed to install built-in skill");
2729
+ log5.warn({ skill: skillName, err }, "failed to install built-in skill");
1887
2730
  }
1888
2731
  continue;
1889
2732
  }
1890
- const userContent = readFileSync2(destSkillMd, "utf-8");
2733
+ const userContent = readFileSync3(destSkillMd, "utf-8");
1891
2734
  const userHash = sha256(userContent);
1892
2735
  manifest.refresh(skillName, userHash);
1893
2736
  if (!existsSync3(baseSnapshotPath)) {
@@ -1896,27 +2739,27 @@ function syncBuiltinSkills(rootDir, srcDir) {
1896
2739
  try {
1897
2740
  writeFileSync(baseSnapshotPath, userContent);
1898
2741
  } catch (err) {
1899
- log4.debug({ skill: skillName, err }, "failed to write lazy .base.md");
2742
+ log5.debug({ skill: skillName, err }, "failed to write lazy .base.md");
1900
2743
  }
1901
2744
  }
1902
2745
  }
1903
2746
  const action = manifest.determineSyncAction(skillName, officialHash, officialVersion);
1904
2747
  switch (action) {
1905
2748
  case "skip":
1906
- log4.debug({ skill: skillName }, "skill already up to date");
2749
+ log5.debug({ skill: skillName }, "skill already up to date");
1907
2750
  break;
1908
2751
  case "direct-overwrite":
1909
2752
  try {
1910
2753
  writeFileSync(destSkillMd, officialContent);
1911
2754
  writeFileSync(baseSnapshotPath, officialContent);
1912
2755
  manifest.markSynced(skillName, officialHash, officialHash, officialVersion);
1913
- log4.info({ skill: skillName, version: officialVersion }, "upgraded built-in skill");
2756
+ log5.info({ skill: skillName, version: officialVersion }, "upgraded built-in skill");
1914
2757
  } catch (err) {
1915
- log4.warn({ skill: skillName, err }, "failed to overwrite skill");
2758
+ log5.warn({ skill: skillName, err }, "failed to overwrite skill");
1916
2759
  }
1917
2760
  break;
1918
2761
  case "keep-user":
1919
- log4.debug({ skill: skillName }, "keeping user-modified skill");
2762
+ log5.debug({ skill: skillName }, "keeping user-modified skill");
1920
2763
  break;
1921
2764
  case "needs-merge": {
1922
2765
  if (!existsSync3(baseSnapshotPath)) {
@@ -1931,16 +2774,16 @@ function syncBuiltinSkills(rootDir, srcDir) {
1931
2774
  }
1932
2775
  ])
1933
2776
  );
1934
- log4.warn(
2777
+ log5.warn(
1935
2778
  { skill: skillName },
1936
2779
  "missing .base.md snapshot \u2014 skipping auto-merge, wrote .conflict advisory"
1937
2780
  );
1938
2781
  } catch (err) {
1939
- log4.warn({ skill: skillName, err }, "failed to write .conflict advisory");
2782
+ log5.warn({ skill: skillName, err }, "failed to write .conflict advisory");
1940
2783
  }
1941
2784
  break;
1942
2785
  }
1943
- const baseContent = readFileSync2(baseSnapshotPath, "utf-8");
2786
+ const baseContent = readFileSync3(baseSnapshotPath, "utf-8");
1944
2787
  const merged = mergeSkillSections(baseContent, officialContent, userContent);
1945
2788
  if (merged.success && merged.content) {
1946
2789
  try {
@@ -1948,9 +2791,9 @@ function syncBuiltinSkills(rootDir, srcDir) {
1948
2791
  writeFileSync(baseSnapshotPath, officialContent);
1949
2792
  const newCurrentHash = sha256(merged.content);
1950
2793
  manifest.markSynced(skillName, officialHash, newCurrentHash, officialVersion);
1951
- log4.info({ skill: skillName }, "auto-merged skill upgrade");
2794
+ log5.info({ skill: skillName }, "auto-merged skill upgrade");
1952
2795
  } catch (err) {
1953
- log4.warn({ skill: skillName, err }, "failed to write merged skill");
2796
+ log5.warn({ skill: skillName, err }, "failed to write merged skill");
1954
2797
  }
1955
2798
  } else {
1956
2799
  try {
@@ -1958,12 +2801,12 @@ function syncBuiltinSkills(rootDir, srcDir) {
1958
2801
  destSkillMd + ".conflict",
1959
2802
  renderConflictFile(skillName, officialContent, merged.conflicts ?? [])
1960
2803
  );
1961
- log4.warn(
2804
+ log5.warn(
1962
2805
  { skill: skillName, conflicts: merged.conflicts?.length },
1963
2806
  "skill merge conflict \u2014 user version kept, .conflict file written"
1964
2807
  );
1965
2808
  } catch (err) {
1966
- log4.warn({ skill: skillName, err }, "failed to write .conflict file");
2809
+ log5.warn({ skill: skillName, err }, "failed to write .conflict file");
1967
2810
  }
1968
2811
  }
1969
2812
  break;
@@ -2003,8 +2846,8 @@ function renderConflictFile(skillName, officialContent, conflicts) {
2003
2846
  }
2004
2847
  function ensureSkillsDir() {
2005
2848
  if (!existsSync3(SKILLS_DIR)) {
2006
- mkdirSync(SKILLS_DIR, { recursive: true });
2007
- log4.info({ dir: SKILLS_DIR }, "created skills directory");
2849
+ mkdirSync2(SKILLS_DIR, { recursive: true });
2850
+ log5.info({ dir: SKILLS_DIR }, "created skills directory");
2008
2851
  }
2009
2852
  }
2010
2853
  var cachedSkills = [];
@@ -2014,7 +2857,7 @@ function scanSkills() {
2014
2857
  try {
2015
2858
  dirs = readdirSync3(SKILLS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
2016
2859
  } catch {
2017
- log4.warn("failed to read skills directory for scanning");
2860
+ log5.warn("failed to read skills directory for scanning");
2018
2861
  return [];
2019
2862
  }
2020
2863
  const skills = [];
@@ -2022,7 +2865,7 @@ function scanSkills() {
2022
2865
  const skillMd = join3(SKILLS_DIR, dirName, "SKILL.md");
2023
2866
  if (!existsSync3(skillMd)) continue;
2024
2867
  try {
2025
- const content = readFileSync2(skillMd, "utf-8");
2868
+ const content = readFileSync3(skillMd, "utf-8");
2026
2869
  const parsed = parseFrontmatter(content);
2027
2870
  if (parsed.name) {
2028
2871
  skills.push({
@@ -2031,11 +2874,11 @@ function scanSkills() {
2031
2874
  });
2032
2875
  }
2033
2876
  } catch (err) {
2034
- log4.debug({ skill: dirName, err }, "failed to parse SKILL.md frontmatter");
2877
+ log5.debug({ skill: dirName, err }, "failed to parse SKILL.md frontmatter");
2035
2878
  }
2036
2879
  }
2037
2880
  cachedSkills = skills;
2038
- log4.info({ count: skills.length }, "scanned skills");
2881
+ log5.info({ count: skills.length }, "scanned skills");
2039
2882
  return skills;
2040
2883
  }
2041
2884
  function getSkillList() {
@@ -2045,7 +2888,7 @@ function getPendingNotification() {
2045
2888
  const pendingPath = join3(CLAWNET_DIR, ".claude", "pending-evolutions.json");
2046
2889
  if (!existsSync3(pendingPath)) return null;
2047
2890
  try {
2048
- const raw = JSON.parse(readFileSync2(pendingPath, "utf-8"));
2891
+ const raw = JSON.parse(readFileSync3(pendingPath, "utf-8"));
2049
2892
  const pending = Array.isArray(raw?.pending) ? raw.pending : [];
2050
2893
  if (pending.length === 0) return null;
2051
2894
  const lines = pending.map(
@@ -2056,7 +2899,7 @@ ${lines.join("\n")}
2056
2899
  \u4F7F\u7528 skill_pending_list \u67E5\u770B\u8BE6\u60C5\uFF0Cskill_pending_approve / skill_pending_reject \u5904\u7406\u3002`;
2057
2900
  return { count: pending.length, text };
2058
2901
  } catch (err) {
2059
- log4.debug({ err }, "failed to read pending-evolutions.json");
2902
+ log5.debug({ err }, "failed to read pending-evolutions.json");
2060
2903
  return null;
2061
2904
  }
2062
2905
  }
@@ -2076,10 +2919,10 @@ function ensureMcpConfig() {
2076
2919
  let mcpServerPath;
2077
2920
  try {
2078
2921
  const req = createRequire(import.meta.url);
2079
- const mcpPkgDir = dirname(req.resolve("@mclawnet/mcp-server/package.json"));
2922
+ const mcpPkgDir = dirname2(req.resolve("@mclawnet/mcp-server/package.json"));
2080
2923
  mcpServerPath = join3(mcpPkgDir, "dist", "server.js");
2081
2924
  } catch {
2082
- log4.warn("could not resolve @mclawnet/mcp-server package path, skipping mcp.json generation");
2925
+ log5.warn("could not resolve @mclawnet/mcp-server package path, skipping mcp.json generation");
2083
2926
  return;
2084
2927
  }
2085
2928
  const desired = { command: "node", args: [mcpServerPath] };
@@ -2088,9 +2931,9 @@ function ensureMcpConfig() {
2088
2931
  if (existsSync3(MCP_CONFIG_PATH)) {
2089
2932
  fileExists = true;
2090
2933
  try {
2091
- config = JSON.parse(readFileSync2(MCP_CONFIG_PATH, "utf-8"));
2934
+ config = JSON.parse(readFileSync3(MCP_CONFIG_PATH, "utf-8"));
2092
2935
  } catch (err) {
2093
- log4.warn({ err, path: MCP_CONFIG_PATH }, "mcp.json malformed \u2014 leaving alone");
2936
+ log5.warn({ err, path: MCP_CONFIG_PATH }, "mcp.json malformed \u2014 leaving alone");
2094
2937
  return;
2095
2938
  }
2096
2939
  if (!config.mcpServers || typeof config.mcpServers !== "object") {
@@ -2115,18 +2958,18 @@ function ensureMcpConfig() {
2115
2958
  changed = true;
2116
2959
  }
2117
2960
  if (!changed) {
2118
- log4.debug("mcp.json already up-to-date \u2014 no write");
2961
+ log5.debug("mcp.json already up-to-date \u2014 no write");
2119
2962
  return;
2120
2963
  }
2121
2964
  try {
2122
- mkdirSync(CLAWNET_DIR, { recursive: true });
2965
+ mkdirSync2(CLAWNET_DIR, { recursive: true });
2123
2966
  writeFileSync(MCP_CONFIG_PATH, JSON.stringify(config, null, 2) + "\n");
2124
- log4.info(
2967
+ log5.info(
2125
2968
  { path: MCP_CONFIG_PATH, fresh: !fileExists },
2126
2969
  fileExists ? "updated mcp.json (clawnet-mcp ensured)" : "generated default mcp.json"
2127
2970
  );
2128
2971
  } catch (err) {
2129
- log4.warn({ err }, "failed to write mcp.json");
2972
+ log5.warn({ err }, "failed to write mcp.json");
2130
2973
  }
2131
2974
  }
2132
2975
 
@@ -2139,6 +2982,23 @@ var SessionLimitReachedError = class extends Error {
2139
2982
  }
2140
2983
  code = "SESSION_LIMIT_REACHED";
2141
2984
  };
2985
+ var RespawnRequiredError = class extends Error {
2986
+ code = "RESPAWN_REQUIRED";
2987
+ sessionId;
2988
+ newModel;
2989
+ currentModel;
2990
+ backendSessionId;
2991
+ constructor(args) {
2992
+ super(
2993
+ `per-turn model change to "${args.newModel}" requires session respawn; caller should retry as session.create with resumeId=${args.backendSessionId ?? "<unknown>"}`
2994
+ );
2995
+ this.name = "RespawnRequiredError";
2996
+ this.sessionId = args.sessionId;
2997
+ this.newModel = args.newModel;
2998
+ this.currentModel = args.currentModel;
2999
+ this.backendSessionId = args.backendSessionId;
3000
+ }
3001
+ };
2142
3002
  function buildMessage(d) {
2143
3003
  const kindParts = Object.entries(d.byKind).map(([k, n]) => `${k}=${n}`).join(", ");
2144
3004
  const oldest = d.oldestIdleSessionId ? `Oldest idle: ${d.oldestIdleSessionId} (${d.oldestIdleKind}, idle ${Math.round(
@@ -2243,7 +3103,7 @@ function getSharedEmbeddingService() {
2243
3103
  sharedEmbeddingService = new EmbeddingService(db, createEmbeddingProviders());
2244
3104
  return sharedEmbeddingService;
2245
3105
  }
2246
- var log5 = createLogger5({ module: "agent/session-manager" });
3106
+ var log6 = createLogger6({ module: "agent/session-manager" });
2247
3107
  var DEFAULT_MAX_PROCESSES = 30;
2248
3108
  var MAX_PROCESSES = Number(process.env.CLAWNET_MAX_PROCESSES) || DEFAULT_MAX_PROCESSES;
2249
3109
  var DEFAULT_CHAT_IDLE_TTL_MS = 60 * 60 * 1e3;
@@ -2265,6 +3125,9 @@ var SessionManager = class {
2265
3125
  conversationBuffer = /* @__PURE__ */ new Map();
2266
3126
  // PR-A: parallel metadata map (kind, timestamps). See SessionMeta jsdoc.
2267
3127
  sessionMeta = /* @__PURE__ */ new Map();
3128
+ // (N3) Sticky/turn-override state keyed by sessionId. See StickyState jsdoc
3129
+ // for the lifecycle rationale (it's intentionally NOT cleared by abort).
3130
+ stickyState = /* @__PURE__ */ new Map();
2268
3131
  // PR-A: sessions currently inside a sendInput→turn_complete window. Idle
2269
3132
  // sweeper must skip these even if lastActivityAt looks ancient — a
2270
3133
  // long-running deep-research turn would otherwise be killed mid-flight.
@@ -2348,6 +3211,14 @@ var SessionManager = class {
2348
3211
  */
2349
3212
  onSessionExit;
2350
3213
  onPermissionRequest;
3214
+ /**
3215
+ * (N5) Fires when `closeSession` runs against a session that had a
3216
+ * workspace handle attached via {@link SessionManager.attachWorkspace}.
3217
+ * Receives the original handle so the host can dispose it (keep=true) and
3218
+ * notify hub via `session.worktree_closed`. Never blocks the close path —
3219
+ * exceptions are caught and logged.
3220
+ */
3221
+ onWorkspaceClose;
2351
3222
  // PR-A: classifies a sessionId as 'chat' or 'swarm-role'. Injected by
2352
3223
  // start.ts via SwarmCoordinator.isSwarmSession to keep SessionManager
2353
3224
  // independent of the swarm package. Defaults to 'chat' if absent — safe
@@ -2364,6 +3235,7 @@ var SessionManager = class {
2364
3235
  this.onBeforeClose = options.onBeforeClose;
2365
3236
  this.onSessionExit = options.onSessionExit;
2366
3237
  this.onPermissionRequest = options.onPermissionRequest;
3238
+ this.onWorkspaceClose = options.onWorkspaceClose;
2367
3239
  this.classify = options.classify ?? (() => "chat");
2368
3240
  this.checkpointPath = options.checkpointPath ?? null;
2369
3241
  this.checkpointDebounceMs = options.checkpointDebounceMs ?? 5e3;
@@ -2386,12 +3258,12 @@ var SessionManager = class {
2386
3258
  options.systemPrompt = options.systemPrompt ? `${memorySection}
2387
3259
 
2388
3260
  ${options.systemPrompt}` : memorySection;
2389
- log5.debug({ workDir: options.workDir, sessionId: options.sessionId }, "memory prompt injected");
3261
+ log6.debug({ workDir: options.workDir, sessionId: options.sessionId }, "memory prompt injected");
2390
3262
  } catch (err) {
2391
- log5.warn({ err, workDir: options.workDir }, "failed to build memory section, proceeding without");
3263
+ log6.warn({ err, workDir: options.workDir }, "failed to build memory section, proceeding without");
2392
3264
  }
2393
3265
  } else {
2394
- log5.warn(
3266
+ log6.warn(
2395
3267
  { sessionId: options.sessionId, roleId: options.roleId },
2396
3268
  "session created without workDir \u2014 memory injection skipped (all memories would be cross-workdir; pass workDir to enable scoped retrieval)"
2397
3269
  );
@@ -2404,13 +3276,13 @@ ${options.systemPrompt}` : memorySection;
2404
3276
  [Skill \u8FDB\u5316\u63D0\u6848]
2405
3277
  ${notice.text}`;
2406
3278
  options.systemPrompt = options.systemPrompt ? `${options.systemPrompt}${noticeBlock}` : noticeBlock.trimStart();
2407
- log5.debug(
3279
+ log6.debug(
2408
3280
  { sessionId: options.sessionId, pending: notice.count },
2409
3281
  "pending skill evolution notice injected"
2410
3282
  );
2411
3283
  }
2412
3284
  } catch (err) {
2413
- log5.debug({ err }, "failed to inject pending notification");
3285
+ log6.debug({ err }, "failed to inject pending notification");
2414
3286
  }
2415
3287
  if (this.classify(options.sessionId) === "chat") {
2416
3288
  const ideaHint = `
@@ -2422,7 +3294,7 @@ ${notice.text}`;
2422
3294
  let spawnAdapter = this.adapter;
2423
3295
  if (options.backend && this.resolveAdapter) {
2424
3296
  const resolved = await this.resolveAdapter(options.backend);
2425
- log5.info(
3297
+ log6.info(
2426
3298
  {
2427
3299
  sessionId: options.sessionId,
2428
3300
  requested: options.backend,
@@ -2436,7 +3308,7 @@ ${notice.text}`;
2436
3308
  spawnAdapter = resolved;
2437
3309
  }
2438
3310
  } else {
2439
- log5.info(
3311
+ log6.info(
2440
3312
  {
2441
3313
  sessionId: options.sessionId,
2442
3314
  requestedBackend: options.backend,
@@ -2446,7 +3318,24 @@ ${notice.text}`;
2446
3318
  "createSession: using default adapter"
2447
3319
  );
2448
3320
  }
2449
- const process2 = await spawnAdapter.spawn(options);
3321
+ const sticky = this.stickyState.get(options.sessionId);
3322
+ const effectiveModel = sticky?.pendingTurnOverride?.model ?? sticky?.model ?? options.model;
3323
+ const effectiveMode = sticky?.pendingTurnOverride?.mode ?? sticky?.mode ?? options.mode;
3324
+ const effectiveOptions = {
3325
+ ...options,
3326
+ model: effectiveModel,
3327
+ mode: effectiveMode
3328
+ };
3329
+ if (sticky?.pendingTurnOverride) {
3330
+ log6.debug(
3331
+ { sessionId: options.sessionId, override: sticky.pendingTurnOverride },
3332
+ "N3: consuming pendingTurnOverride for spawn"
3333
+ );
3334
+ delete sticky.pendingTurnOverride;
3335
+ const prevMeta = this.sessionMeta.get(options.sessionId);
3336
+ if (prevMeta) delete prevMeta.pendingTurnOverride;
3337
+ }
3338
+ const process2 = await spawnAdapter.spawn(effectiveOptions);
2450
3339
  if (spawnAdapter !== this.adapter) {
2451
3340
  this.processAdapters.set(process2, spawnAdapter);
2452
3341
  }
@@ -2464,12 +3353,22 @@ ${notice.text}`;
2464
3353
  // Restore ladder index if this sessionId was carried over from the
2465
3354
  // previous run's checkpoint. drain-on-read so a future re-creation of
2466
3355
  // a different session with the same id starts fresh.
2467
- currentLadderIndex: this.recoveredLadderIndex.get(options.sessionId)
3356
+ currentLadderIndex: this.recoveredLadderIndex.get(options.sessionId),
3357
+ // (N3) Mirror sticky model/mode onto meta so existing meta-readers
3358
+ // (debug snapshots, future PR consumers) see the resolved value.
3359
+ stickyModel: sticky?.model,
3360
+ stickyMode: sticky?.mode
2468
3361
  });
2469
3362
  this.recoveredLadderIndex.delete(options.sessionId);
3363
+ let postSpawnSticky = this.stickyState.get(options.sessionId);
3364
+ if (!postSpawnSticky) {
3365
+ postSpawnSticky = {};
3366
+ this.stickyState.set(options.sessionId, postSpawnSticky);
3367
+ }
3368
+ postSpawnSticky.lastSpawnModel = effectiveModel;
2470
3369
  this.scheduleCheckpoint();
2471
3370
  spawnAdapter.onOutput(process2, (rawData) => {
2472
- const data = normalizeBackendOutput(rawData, log5);
3371
+ const data = normalizeBackendOutput(rawData, log6);
2473
3372
  if (data === null) return;
2474
3373
  const msg = data;
2475
3374
  if (msg?.type === "assistant" && msg?.message?.content) {
@@ -2502,7 +3401,7 @@ ${notice.text}`;
2502
3401
  const currentIndex = clampLadderIndex(meta.currentLadderIndex);
2503
3402
  const atFloor = currentIndex >= MAX_TOKENS_LADDER.length - 1;
2504
3403
  if (atFloor) {
2505
- log5.warn(
3404
+ log6.warn(
2506
3405
  {
2507
3406
  sessionId: options.sessionId,
2508
3407
  used: info.used,
@@ -2515,7 +3414,7 @@ ${notice.text}`;
2515
3414
  }
2516
3415
  const nextIndex = currentIndex + 1;
2517
3416
  meta.currentLadderIndex = nextIndex;
2518
- log5.warn(
3417
+ log6.warn(
2519
3418
  {
2520
3419
  sessionId: options.sessionId,
2521
3420
  used: info.used,
@@ -2531,7 +3430,7 @@ ${notice.text}`;
2531
3430
  if (this.onSessionStarted) {
2532
3431
  spawnAdapter.onSessionStarted?.(process2, (info) => {
2533
3432
  if (this.aborting.has(options.sessionId)) {
2534
- log5.debug(
3433
+ log6.debug(
2535
3434
  { sessionId: options.sessionId },
2536
3435
  "suppressing late session_started \u2014 session is aborting"
2537
3436
  );
@@ -2555,7 +3454,7 @@ ${notice.text}`;
2555
3454
  `${sid}.jsonl`
2556
3455
  );
2557
3456
  if (!existsSync4(jsonlPath)) {
2558
- log5.warn(
3457
+ log6.warn(
2559
3458
  {
2560
3459
  sessionId: options.sessionId,
2561
3460
  backendSessionId: sid,
@@ -2592,10 +3491,10 @@ ${notice.text}`;
2592
3491
  this.conversationBuffer.delete(options.sessionId);
2593
3492
  this.scheduleCheckpoint();
2594
3493
  if (!expected) {
2595
- log5.warn({ sessionId: options.sessionId, exitCode: code, reason: enriched }, "backend process exited unexpectedly, evicted from session map");
3494
+ log6.warn({ sessionId: options.sessionId, exitCode: code, reason: enriched }, "backend process exited unexpectedly, evicted from session map");
2596
3495
  this.onSessionError(options.sessionId, `backend process exited (code=${code ?? "null"})`);
2597
3496
  } else {
2598
- log5.debug({ sessionId: options.sessionId, exitCode: code }, "backend process exited as expected");
3497
+ log6.debug({ sessionId: options.sessionId, exitCode: code }, "backend process exited as expected");
2599
3498
  }
2600
3499
  try {
2601
3500
  this.onSessionExit?.(options.sessionId, {
@@ -2604,7 +3503,7 @@ ${notice.text}`;
2604
3503
  ...expected ? {} : { reason: reasonText }
2605
3504
  });
2606
3505
  } catch (err) {
2607
- log5.warn({ err, sessionId: options.sessionId }, "onSessionExit listener threw");
3506
+ log6.warn({ err, sessionId: options.sessionId }, "onSessionExit listener threw");
2608
3507
  }
2609
3508
  } else {
2610
3509
  const expected = this.expectedExits.delete(options.sessionId);
@@ -2614,7 +3513,7 @@ ${notice.text}`;
2614
3513
  expected
2615
3514
  });
2616
3515
  } catch (err) {
2617
- log5.warn({ err, sessionId: options.sessionId }, "onSessionExit listener threw");
3516
+ log6.warn({ err, sessionId: options.sessionId }, "onSessionExit listener threw");
2618
3517
  }
2619
3518
  }
2620
3519
  });
@@ -2634,7 +3533,7 @@ ${notice.text}`;
2634
3533
  this.activelyExecuting.add(sessionId);
2635
3534
  const meta = this.sessionMeta.get(sessionId);
2636
3535
  if (meta) meta.lastActivityAt = Date.now();
2637
- log5.debug(
3536
+ log6.debug(
2638
3537
  { sessionId, ...previewFields2(input) },
2639
3538
  "sendInput \u2192 backend stdin"
2640
3539
  );
@@ -2649,6 +3548,17 @@ ${notice.text}`;
2649
3548
  async sendUserInput(sessionId, content) {
2650
3549
  const meta = this.sessionMeta.get(sessionId);
2651
3550
  const workDir = meta?.workDir;
3551
+ const st = this.stickyState.get(sessionId);
3552
+ const effectiveModel = st?.pendingTurnOverride?.model ?? st?.model;
3553
+ if (effectiveModel !== void 0 && st?.lastSpawnModel !== void 0 && effectiveModel !== st.lastSpawnModel) {
3554
+ const backendSessionId = meta?.backendSessionId;
3555
+ throw new RespawnRequiredError({
3556
+ sessionId,
3557
+ newModel: effectiveModel,
3558
+ currentModel: st.lastSpawnModel,
3559
+ backendSessionId
3560
+ });
3561
+ }
2652
3562
  let prefixed = content;
2653
3563
  try {
2654
3564
  const section = await Promise.race([
@@ -2658,7 +3568,7 @@ ${notice.text}`;
2658
3568
  embeddingService: getSharedEmbeddingService()
2659
3569
  }),
2660
3570
  new Promise(
2661
- (resolve) => setTimeout(() => resolve(""), 200)
3571
+ (resolve3) => setTimeout(() => resolve3(""), 200)
2662
3572
  )
2663
3573
  ]);
2664
3574
  if (section && section.trim().length > 0) {
@@ -2669,10 +3579,84 @@ ${section}
2669
3579
  ${content}`;
2670
3580
  }
2671
3581
  } catch (err) {
2672
- log5.debug({ err, sessionId }, "sendUserInput: memory retrieval skipped");
3582
+ log6.debug({ err, sessionId }, "sendUserInput: memory retrieval skipped");
2673
3583
  }
2674
3584
  this.sendInput(sessionId, prefixed);
2675
3585
  }
3586
+ /**
3587
+ * (N2/N3) Update sticky model/mode on a session. Survives abort/respawn
3588
+ * because it writes to `stickyState`, which is independent of `sessionMeta`
3589
+ * (cleared on abort). Partial: fields with `undefined` are not touched.
3590
+ *
3591
+ * Valid use cases:
3592
+ * - patch sticky on a live session (mutates meta + stickyState)
3593
+ * - stage sticky BEFORE the first session.create lands (writes only to
3594
+ * stickyState — meta is created later by createSession, which mirrors
3595
+ * from stickyState)
3596
+ *
3597
+ * No-throw on missing session; this method is best-effort state staging.
3598
+ */
3599
+ updateSticky(sessionId, sticky) {
3600
+ const meta = this.sessionMeta.get(sessionId);
3601
+ if (meta) {
3602
+ if (sticky.model !== void 0) meta.stickyModel = sticky.model;
3603
+ if (sticky.mode !== void 0) meta.stickyMode = sticky.mode;
3604
+ }
3605
+ let st = this.stickyState.get(sessionId);
3606
+ if (!st) {
3607
+ st = {};
3608
+ this.stickyState.set(sessionId, st);
3609
+ }
3610
+ if (sticky.model !== void 0) st.model = sticky.model;
3611
+ if (sticky.mode !== void 0) st.mode = sticky.mode;
3612
+ log6.debug(
3613
+ { sessionId, stickyModel: st.model, stickyMode: st.mode, hadMeta: !!meta },
3614
+ "updateSticky applied"
3615
+ );
3616
+ }
3617
+ /**
3618
+ * (N2/N3) Stage a per-turn override for the very next spawn. Consumed
3619
+ * (and deleted from stickyState) by the next `createSession` for this
3620
+ * sessionId. Survives abort/respawn until consumed — so a hub-side
3621
+ * abort+recreate driven by a model switch still carries the override.
3622
+ *
3623
+ * No-throw on missing session: same staging-before-alive use case as
3624
+ * `updateSticky`.
3625
+ */
3626
+ setTurnOverride(sessionId, override) {
3627
+ const meta = this.sessionMeta.get(sessionId);
3628
+ if (meta) {
3629
+ meta.pendingTurnOverride = { model: override.model, mode: override.mode };
3630
+ }
3631
+ let st = this.stickyState.get(sessionId);
3632
+ if (!st) {
3633
+ st = {};
3634
+ this.stickyState.set(sessionId, st);
3635
+ }
3636
+ st.pendingTurnOverride = { model: override.model, mode: override.mode };
3637
+ log6.debug({ sessionId, override, hadMeta: !!meta }, "setTurnOverride staged");
3638
+ }
3639
+ /**
3640
+ * (N5) Attach a workspace handle to an existing session's meta so that a
3641
+ * later `closeSession` can fire `onWorkspaceClose` with it. Idempotent: a
3642
+ * second call overwrites the existing handle (acceptable since the host
3643
+ * never adopts two workspaces for the same session in practice). No-op
3644
+ * with a warn when the session is unknown — the caller's prepare succeeded
3645
+ * before createSession failed, so the workspace is already cleaning itself
3646
+ * up via the error path in hub-connection.
3647
+ */
3648
+ attachWorkspace(sessionId, handle) {
3649
+ const meta = this.sessionMeta.get(sessionId);
3650
+ if (!meta) {
3651
+ log6.warn({ sessionId }, "attachWorkspace: session not in meta map, ignoring");
3652
+ return;
3653
+ }
3654
+ meta.workspaceHandle = handle;
3655
+ }
3656
+ /** (N5) Inspect the attached workspace handle for a session, if any. */
3657
+ getWorkspaceHandle(sessionId) {
3658
+ return this.sessionMeta.get(sessionId)?.workspaceHandle;
3659
+ }
2676
3660
  async abortSession(sessionId) {
2677
3661
  const process2 = this.sessions.get(sessionId);
2678
3662
  if (!process2) return;
@@ -2711,10 +3695,20 @@ ${content}`;
2711
3695
  });
2712
3696
  }
2713
3697
  this.conversationBuffer.delete(sessionId);
3698
+ const meta = this.sessionMeta.get(sessionId);
3699
+ const handle = meta?.workspaceHandle;
3700
+ if (handle && this.onWorkspaceClose) {
3701
+ try {
3702
+ this.onWorkspaceClose(sessionId, handle);
3703
+ } catch (err) {
3704
+ log6.warn({ err, sessionId }, "onWorkspaceClose callback threw");
3705
+ }
3706
+ }
2714
3707
  this.expectedExits.add(sessionId);
2715
3708
  this.sessions.delete(sessionId);
2716
3709
  this.sessionMeta.delete(sessionId);
2717
3710
  this.activelyExecuting.delete(sessionId);
3711
+ this.stickyState.delete(sessionId);
2718
3712
  this.scheduleCheckpoint();
2719
3713
  await this.getAdapterFor(process2).stop(process2);
2720
3714
  }
@@ -2732,6 +3726,7 @@ ${content}`;
2732
3726
  this.sessions.delete(sessionId);
2733
3727
  this.sessionMeta.delete(sessionId);
2734
3728
  this.activelyExecuting.delete(sessionId);
3729
+ this.stickyState.delete(sessionId);
2735
3730
  await this.getAdapterFor(process2).stop(process2).catch(() => {
2736
3731
  });
2737
3732
  }
@@ -2743,7 +3738,7 @@ ${content}`;
2743
3738
  this.checkpointDebouncer = null;
2744
3739
  }
2745
3740
  await this.flushCheckpoint().catch(
2746
- (err) => log5.warn({ err }, "checkpoint: final flushCheckpoint failed")
3741
+ (err) => log6.warn({ err }, "checkpoint: final flushCheckpoint failed")
2747
3742
  );
2748
3743
  }
2749
3744
  }
@@ -2800,16 +3795,16 @@ ${content}`;
2800
3795
  this.idleSweepIntervalMs = overrides.intervalMs;
2801
3796
  if (this.idleSweepTimer) return;
2802
3797
  if (this.idleTtlMs <= 0) {
2803
- log5.info("idleSweeper disabled (ttlMs <= 0)");
3798
+ log6.info("idleSweeper disabled (ttlMs <= 0)");
2804
3799
  return;
2805
3800
  }
2806
3801
  this.idleSweepTimer = setInterval(() => {
2807
3802
  this.sweepIdleSessions().catch(
2808
- (err) => log5.warn({ err }, "idleSweeper: tick failed")
3803
+ (err) => log6.warn({ err }, "idleSweeper: tick failed")
2809
3804
  );
2810
3805
  }, this.idleSweepIntervalMs);
2811
3806
  this.idleSweepTimer.unref?.();
2812
- log5.info(
3807
+ log6.info(
2813
3808
  { ttlMs: this.idleTtlMs, intervalMs: this.idleSweepIntervalMs },
2814
3809
  "idleSweeper started"
2815
3810
  );
@@ -2840,12 +3835,12 @@ ${content}`;
2840
3835
  }
2841
3836
  for (const sid of victims) {
2842
3837
  const idleMs = now - (this.sessionMeta.get(sid)?.lastActivityAt ?? now);
2843
- log5.info(
3838
+ log6.info(
2844
3839
  { sessionId: sid, idleMs, ttlMs: ttl },
2845
3840
  "idleSweeper: closing idle chat session"
2846
3841
  );
2847
3842
  await this.closeSession(sid).catch(
2848
- (err) => log5.warn({ err, sessionId: sid }, "idleSweeper: closeSession failed, continuing")
3843
+ (err) => log6.warn({ err, sessionId: sid }, "idleSweeper: closeSession failed, continuing")
2849
3844
  );
2850
3845
  }
2851
3846
  }
@@ -2899,7 +3894,7 @@ ${content}`;
2899
3894
  this.checkpointDebouncer = setTimeout(() => {
2900
3895
  this.checkpointDebouncer = null;
2901
3896
  this.flushCheckpoint().catch(
2902
- (err) => log5.warn({ err }, "checkpoint: flush failed")
3897
+ (err) => log6.warn({ err }, "checkpoint: flush failed")
2903
3898
  );
2904
3899
  }, this.checkpointDebounceMs);
2905
3900
  this.checkpointDebouncer.unref?.();
@@ -2963,7 +3958,7 @@ ${content}`;
2963
3958
  }
2964
3959
  if (isPidAlive(entry.pid)) {
2965
3960
  orphan.push(entry);
2966
- log5.warn(
3961
+ log6.warn(
2967
3962
  { sessionId: entry.sessionId, pid: entry.pid },
2968
3963
  "recover: pid still alive but not owned by current agent (orphan)"
2969
3964
  );
@@ -2984,14 +3979,97 @@ function ensureRuntimeEnvDefaults() {
2984
3979
  }
2985
3980
 
2986
3981
  // src/start.ts
2987
- import { SwarmCoordinator, initRoles, WakeupScheduler, listRecoverableSwarmIds as listRecoverableSwarmIds2 } from "@mclawnet/swarm";
3982
+ import { SwarmCoordinator, initRoles, WakeupScheduler, listRecoverableSwarmIds as listRecoverableSwarmIds2, AlwaysOnManager, listProjectDirs, resolveWorkDir as resolveWorkDir2, alwaysOnConfigPath, WorkspaceManager, createIdeaTodoSource, createIdeaResearchSource, createIntrospectionSource, loadAlwaysOnConfig as loadAlwaysOnConfig2, mergeAlwaysOnConfig as mergeAlwaysOnConfig2, saveAlwaysOnConfig as saveAlwaysOnConfig2, reconcileOrphanedResearching } from "@mclawnet/swarm";
2988
3983
  import { TaskStore as TaskStore2 } from "@mclawnet/task";
2989
3984
 
3985
+ // src/ideas-rest-client.ts
3986
+ function normalizeHubUrlToHttp(hubUrl) {
3987
+ let url = hubUrl;
3988
+ url = url.replace(/^wss:\/\//i, "https://").replace(/^ws:\/\//i, "http://");
3989
+ url = url.replace(/\/ws\/agent\/?$/i, "");
3990
+ if (url.endsWith("/")) url = url.slice(0, -1);
3991
+ return url;
3992
+ }
3993
+ function createIdeasRestClient(opts) {
3994
+ const base = normalizeHubUrlToHttp(opts.hubUrl);
3995
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
3996
+ if (typeof fetchImpl !== "function") {
3997
+ throw new Error("createIdeasRestClient: no fetch implementation available");
3998
+ }
3999
+ async function authHeaders() {
4000
+ const token = await opts.getAuthToken();
4001
+ if (!token) {
4002
+ throw new Error("ideas-rest-client: no auth token available");
4003
+ }
4004
+ return { Authorization: `Bearer ${token}` };
4005
+ }
4006
+ async function request(path, init = {}) {
4007
+ const headers = {
4008
+ "Content-Type": "application/json",
4009
+ ...await authHeaders(),
4010
+ ...init.headers ?? {}
4011
+ };
4012
+ const res = await fetchImpl(`${base}${path}`, { ...init, headers });
4013
+ if (!res.ok) {
4014
+ let detail = "";
4015
+ try {
4016
+ const body = await res.json();
4017
+ if (body && typeof body.error === "string") detail = body.error;
4018
+ } catch {
4019
+ }
4020
+ throw new Error(
4021
+ `ideas-rest-client: ${init.method ?? "GET"} ${path} \u2192 ${res.status}${detail ? ` (${detail})` : ""}`
4022
+ );
4023
+ }
4024
+ return await res.json();
4025
+ }
4026
+ function buildListQuery(filter) {
4027
+ const p = new URLSearchParams();
4028
+ if (filter.status) p.set("status", filter.status);
4029
+ if (filter.priority) p.set("priority", filter.priority);
4030
+ if (filter.projectId) p.set("projectId", filter.projectId);
4031
+ if (filter.tag) p.set("tag", filter.tag);
4032
+ if (filter.q) p.set("q", filter.q);
4033
+ if (typeof filter.limit === "number") p.set("limit", String(filter.limit));
4034
+ const s = p.toString();
4035
+ return s ? `?${s}` : "";
4036
+ }
4037
+ return {
4038
+ async list(filter) {
4039
+ const res = await request(`/api/ideas${buildListQuery(filter)}`);
4040
+ return res.items ?? [];
4041
+ },
4042
+ async get(id) {
4043
+ try {
4044
+ const res = await request(`/api/ideas/${encodeURIComponent(id)}`);
4045
+ return res.idea ?? null;
4046
+ } catch (err) {
4047
+ if (err instanceof Error && /→\s*404/.test(err.message)) return null;
4048
+ throw err;
4049
+ }
4050
+ },
4051
+ async patch(id, partial) {
4052
+ const res = await request(`/api/ideas/${encodeURIComponent(id)}`, {
4053
+ method: "PATCH",
4054
+ body: JSON.stringify(partial)
4055
+ });
4056
+ return res.idea;
4057
+ },
4058
+ async create(input) {
4059
+ const res = await request(`/api/ideas`, {
4060
+ method: "POST",
4061
+ body: JSON.stringify(input)
4062
+ });
4063
+ return res.idea;
4064
+ }
4065
+ };
4066
+ }
4067
+
2990
4068
  // src/brain-bridge.ts
2991
- import { existsSync as existsSync5, readFileSync as readFileSync3, readdirSync as readdirSync4 } from "fs";
4069
+ import { existsSync as existsSync5, readFileSync as readFileSync4, readdirSync as readdirSync4 } from "fs";
2992
4070
  import { join as join5 } from "path";
2993
- import { createLogger as createLogger6 } from "@mclawnet/logger";
2994
- var log6 = createLogger6({ module: "brain-bridge" });
4071
+ import { createLogger as createLogger7 } from "@mclawnet/logger";
4072
+ var log7 = createLogger7({ module: "brain-bridge" });
2995
4073
  var BrainBridge = class {
2996
4074
  constructor(hub, options) {
2997
4075
  this.hub = hub;
@@ -2999,7 +4077,7 @@ var BrainBridge = class {
2999
4077
  this.brainHome = options?.brainHomePath || process.env.BRAIN_HOME || join5(home, "BrainData");
3000
4078
  this.brainCorePath = options?.brainCorePath || join5(home, ".brain", "BrainCore");
3001
4079
  this.hub.registerNamespace("brain", (msg) => this.handleRequest(msg));
3002
- log6.info(
4080
+ log7.info(
3003
4081
  { brainHome: this.brainHome, brainCorePath: this.brainCorePath },
3004
4082
  "BrainBridge initialized"
3005
4083
  );
@@ -3007,11 +4085,11 @@ var BrainBridge = class {
3007
4085
  brainHome;
3008
4086
  brainCorePath;
3009
4087
  async handleRequest(msg) {
3010
- log6.info({ action: msg.action, requestId: msg.requestId }, "brain request");
4088
+ log7.info({ action: msg.action, requestId: msg.requestId }, "brain request");
3011
4089
  switch (msg.action) {
3012
4090
  case "setup_status": {
3013
4091
  const status = this.checkSetup();
3014
- log6.info({ status }, "setup_status result");
4092
+ log7.info({ status }, "setup_status result");
3015
4093
  return { status };
3016
4094
  }
3017
4095
  case "get_briefing":
@@ -3019,7 +4097,7 @@ var BrainBridge = class {
3019
4097
  case "get_meeting_recap":
3020
4098
  return await this.getMeetingRecap(msg.params.recapPath);
3021
4099
  default:
3022
- log6.warn({ action: msg.action }, "unknown brain action");
4100
+ log7.warn({ action: msg.action }, "unknown brain action");
3023
4101
  throw new Error(`Unknown brain action: ${msg.action}`);
3024
4102
  }
3025
4103
  }
@@ -3042,23 +4120,23 @@ var BrainBridge = class {
3042
4120
  const targetDate = date || (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3043
4121
  const reportsDir = join5(this.brainHome, "reports", "daily");
3044
4122
  if (!existsSync5(reportsDir)) {
3045
- log6.info({ reportsDir }, "get_briefing: reports dir not found");
4123
+ log7.info({ reportsDir }, "get_briefing: reports dir not found");
3046
4124
  return { briefing: null, actions: [], projects: [], meetings: [], feed: [] };
3047
4125
  }
3048
4126
  const files = readdirSync4(reportsDir).filter((f) => f.includes(targetDate) && f.endsWith(".md")).sort().reverse();
3049
4127
  if (files.length === 0) {
3050
- log6.info({ targetDate }, "get_briefing: no report for date");
4128
+ log7.info({ targetDate }, "get_briefing: no report for date");
3051
4129
  return { briefing: null, actions: [], projects: [], meetings: [], feed: [] };
3052
4130
  }
3053
- log6.info({ targetDate, file: files[0] }, "get_briefing: reading report");
3054
- const content = readFileSync3(join5(reportsDir, files[0]), "utf-8");
4131
+ log7.info({ targetDate, file: files[0] }, "get_briefing: reading report");
4132
+ const content = readFileSync4(join5(reportsDir, files[0]), "utf-8");
3055
4133
  const tldrMatch = content.match(/## TL;DR\n([\s\S]*?)(?=\n---|\n## )/);
3056
4134
  const tldr = tldrMatch ? tldrMatch[1].trim() : content.slice(0, 200);
3057
4135
  const actions = this.parseActions(content);
3058
4136
  const projects = this.parseProjects(content);
3059
4137
  const meetings = this.parseMeetings(content);
3060
4138
  const feed = this.parseFeed(content);
3061
- log6.info(
4139
+ log7.info(
3062
4140
  { actions: actions.length, projects: projects.length, meetings: meetings.length, feed: feed.length },
3063
4141
  "get_briefing: parsed"
3064
4142
  );
@@ -3252,10 +4330,10 @@ var BrainBridge = class {
3252
4330
  }
3253
4331
  const fullPath = join5(this.brainHome, recapPath);
3254
4332
  if (!existsSync5(fullPath)) {
3255
- log6.warn({ fullPath }, "meeting recap file not found");
4333
+ log7.warn({ fullPath }, "meeting recap file not found");
3256
4334
  return { error: "Recap file not found" };
3257
4335
  }
3258
- const content = readFileSync3(fullPath, "utf-8");
4336
+ const content = readFileSync4(fullPath, "utf-8");
3259
4337
  return { recap: this.parseRecapMarkdown(content) };
3260
4338
  }
3261
4339
  /** Parse meeting recap markdown into structured sections */
@@ -3288,10 +4366,10 @@ var BrainBridge = class {
3288
4366
  };
3289
4367
 
3290
4368
  // src/fs-bridge.ts
3291
- import { existsSync as existsSync6, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
3292
- import { extname, isAbsolute } from "path";
3293
- import { createLogger as createLogger7 } from "@mclawnet/logger";
3294
- var log7 = createLogger7({ module: "fs-bridge" });
4369
+ import { existsSync as existsSync6, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
4370
+ import { extname, isAbsolute as isAbsolute2 } from "path";
4371
+ import { createLogger as createLogger8 } from "@mclawnet/logger";
4372
+ var log8 = createLogger8({ module: "fs-bridge" });
3295
4373
  var MAX_TEXT_SIZE = 5 * 1024 * 1024;
3296
4374
  var MIME_MAP = {
3297
4375
  ".ts": "text/typescript",
@@ -3329,10 +4407,10 @@ var FsBridge = class {
3329
4407
  constructor(hub) {
3330
4408
  this.hub = hub;
3331
4409
  this.hub.registerNamespace("fs", (msg) => this.handleRequest(msg));
3332
- log7.info("FsBridge initialized");
4410
+ log8.info("FsBridge initialized");
3333
4411
  }
3334
4412
  async handleRequest(msg) {
3335
- log7.info({ action: msg.action, requestId: msg.requestId }, "fs request");
4413
+ log8.info({ action: msg.action, requestId: msg.requestId }, "fs request");
3336
4414
  switch (msg.action) {
3337
4415
  case "read":
3338
4416
  return this.readFile(msg.params);
@@ -3343,33 +4421,292 @@ var FsBridge = class {
3343
4421
  readFile(params) {
3344
4422
  const filePath = params.path;
3345
4423
  if (!filePath) throw new Error("Missing path parameter");
3346
- if (!isAbsolute(filePath)) {
4424
+ if (!isAbsolute2(filePath)) {
3347
4425
  throw new Error(`Access denied: only absolute paths allowed`);
3348
4426
  }
3349
4427
  if (!existsSync6(filePath)) {
3350
4428
  throw new Error(`File not found: ${filePath}`);
3351
4429
  }
3352
- const stat2 = statSync2(filePath);
4430
+ const stat2 = statSync3(filePath);
3353
4431
  if (!stat2.isFile()) {
3354
4432
  throw new Error(`Not a file: ${filePath}`);
3355
4433
  }
3356
4434
  const mimeType = getMimeType(filePath);
3357
4435
  const totalSize = stat2.size;
3358
4436
  if (isTextMime(mimeType)) {
3359
- const raw = readFileSync4(filePath, "utf-8");
4437
+ const raw = readFileSync5(filePath, "utf-8");
3360
4438
  const truncated2 = raw.length > MAX_TEXT_SIZE;
3361
4439
  const content = truncated2 ? raw.slice(0, MAX_TEXT_SIZE) : raw;
3362
4440
  return { content, encoding: "utf-8", size: content.length, totalSize, mimeType, truncated: truncated2 };
3363
4441
  }
3364
- const buf = readFileSync4(filePath);
4442
+ const buf = readFileSync5(filePath);
3365
4443
  const truncated = buf.length > MAX_TEXT_SIZE;
3366
4444
  const slice = truncated ? buf.subarray(0, MAX_TEXT_SIZE) : buf;
3367
4445
  return { content: slice.toString("base64"), encoding: "base64", size: slice.length, totalSize, mimeType, truncated };
3368
4446
  }
3369
4447
  };
3370
4448
 
4449
+ // src/worktree-bridge.ts
4450
+ import { spawn as spawn2 } from "child_process";
4451
+ import { existsSync as existsSync7 } from "fs";
4452
+ import { writeFile, rm } from "fs/promises";
4453
+ import { resolve as resolve2, isAbsolute as isAbsolute3, normalize, join as join6 } from "path";
4454
+ import { createLogger as createLogger9 } from "@mclawnet/logger";
4455
+ import { createDraftPR } from "@mclawnet/swarm";
4456
+ var log9 = createLogger9({ module: "worktree-bridge" });
4457
+ var WorktreeBridge = class {
4458
+ constructor(hub) {
4459
+ this.hub = hub;
4460
+ this.hub.registerNamespace("worktree", (msg) => this.handle(msg));
4461
+ log9.info("WorktreeBridge initialized");
4462
+ }
4463
+ async handle(msg) {
4464
+ log9.info({ action: msg.action, requestId: msg.requestId }, "worktree request");
4465
+ switch (msg.action) {
4466
+ case "delete":
4467
+ return this.deleteWorktree(msg.params);
4468
+ case "ship":
4469
+ return this.shipWorktree(msg.params);
4470
+ default:
4471
+ throw new Error(`Unknown worktree action: ${msg.action}`);
4472
+ }
4473
+ }
4474
+ async deleteWorktree(params) {
4475
+ const worktreePath = params.worktreePath;
4476
+ const projectRoot2 = params.projectRoot;
4477
+ const branchName = params.branchName;
4478
+ const sessionId = params.sessionId;
4479
+ const force = params.force === true;
4480
+ if (typeof worktreePath !== "string" || !worktreePath.trim()) {
4481
+ throw new Error("worktreePath required");
4482
+ }
4483
+ if (typeof projectRoot2 !== "string" || !projectRoot2.trim()) {
4484
+ throw new Error("projectRoot required (used to find the repo root)");
4485
+ }
4486
+ if (!isAbsolute3(worktreePath) || !isAbsolute3(projectRoot2)) {
4487
+ throw new Error("worktreePath and projectRoot must be absolute");
4488
+ }
4489
+ const expectedPrefix = normalize(resolve2(projectRoot2, ".worktrees")) + "/";
4490
+ const candidate = normalize(resolve2(worktreePath));
4491
+ if (!candidate.startsWith(expectedPrefix)) {
4492
+ throw new Error(
4493
+ `worktreePath ${worktreePath} is not under ${projectRoot2}/.worktrees/`
4494
+ );
4495
+ }
4496
+ if (!existsSync7(candidate)) {
4497
+ log9.info({ worktreePath: candidate }, "worktree path already gone");
4498
+ } else {
4499
+ const rm1 = await runGit(projectRoot2, ["worktree", "remove", "--force", candidate]);
4500
+ if (rm1.exitCode !== 0) {
4501
+ log9.warn(
4502
+ { stderr: rm1.stderr, worktreePath: candidate },
4503
+ "git worktree remove failed; falling back to rm -rf"
4504
+ );
4505
+ await rm(candidate, { recursive: true, force: true }).catch((err) => {
4506
+ log9.warn({ err, worktreePath: candidate }, "rm -rf fallback also failed");
4507
+ });
4508
+ await runGit(projectRoot2, ["worktree", "prune"]).catch(() => null);
4509
+ }
4510
+ }
4511
+ let branchDeleted = false;
4512
+ if (typeof branchName === "string" && branchName.trim()) {
4513
+ const branchFlag = force ? "-D" : "-d";
4514
+ const br = await runGit(projectRoot2, ["branch", branchFlag, branchName]).catch(() => null);
4515
+ if (br && br.exitCode === 0) {
4516
+ branchDeleted = true;
4517
+ } else if (br) {
4518
+ log9.warn(
4519
+ { branchName, stderr: br.stderr, force },
4520
+ "branch delete refused; caller can retry with force=true"
4521
+ );
4522
+ }
4523
+ }
4524
+ const worktreeRemoved = !existsSync7(candidate);
4525
+ this.maybeEmitDeleted(sessionId, candidate);
4526
+ return {
4527
+ ok: true,
4528
+ worktreePath: candidate,
4529
+ worktreeRemoved,
4530
+ branchDeleted
4531
+ };
4532
+ }
4533
+ /**
4534
+ * Emit a session.worktree_deleted push frame after a successful delete so
4535
+ * the hub can clear the DB columns and broadcast to other tabs. Caller
4536
+ * passes sessionId via params; missing sessionId silently skips the push
4537
+ * (delete still works — just no DB sync, used by cleanup tooling that
4538
+ * doesn't have a session context).
4539
+ */
4540
+ maybeEmitDeleted(sessionId, cwd) {
4541
+ if (typeof sessionId === "string" && sessionId) {
4542
+ this.hub.send({
4543
+ type: "session.worktree_deleted",
4544
+ sessionId,
4545
+ cwd
4546
+ });
4547
+ }
4548
+ }
4549
+ /**
4550
+ * Ship a chat-session worktree: stage all changes, commit if dirty, then
4551
+ * either push + open draft PR (when `gh` CLI is available) or fall back to
4552
+ * writing diff.patch + report.md inside the worktree for manual `git apply`.
4553
+ *
4554
+ * Result shape mirrors M6 swarm ShipmentResult so the UI can render both
4555
+ * with the same component (Banner + future shipment list view).
4556
+ *
4557
+ * No LLM involvement (chat sessions don't have a queen role to generate
4558
+ * report bodies). Title/body are caller-provided (UI) or default to the
4559
+ * branch name + short stat summary.
4560
+ */
4561
+ async shipWorktree(params) {
4562
+ const worktreePath = params.worktreePath;
4563
+ const projectRoot2 = params.projectRoot;
4564
+ const branchName = params.branchName;
4565
+ const sessionId = params.sessionId;
4566
+ const hintedBaseBranch = typeof params.baseBranch === "string" ? params.baseBranch : null;
4567
+ const title = params.title?.trim();
4568
+ const body = params.body?.trim();
4569
+ if (typeof worktreePath !== "string" || !isAbsolute3(worktreePath)) {
4570
+ throw new Error("worktreePath required (absolute)");
4571
+ }
4572
+ if (typeof projectRoot2 !== "string" || !isAbsolute3(projectRoot2)) {
4573
+ throw new Error("projectRoot required (absolute)");
4574
+ }
4575
+ if (typeof branchName !== "string" || !branchName.trim()) {
4576
+ throw new Error("branchName required");
4577
+ }
4578
+ const expectedPrefix = normalize(resolve2(projectRoot2, ".worktrees")) + "/";
4579
+ const cwd = normalize(resolve2(worktreePath));
4580
+ if (!cwd.startsWith(expectedPrefix)) {
4581
+ throw new Error(`worktreePath not under ${projectRoot2}/.worktrees/`);
4582
+ }
4583
+ if (!existsSync7(cwd)) {
4584
+ throw new Error(`Worktree no longer exists at ${cwd}`);
4585
+ }
4586
+ const add = await runGit(cwd, ["add", "-A"]);
4587
+ if (add.exitCode !== 0) {
4588
+ throw new Error(`git add failed: ${add.stderr || add.stdout}`);
4589
+ }
4590
+ const status = await runGit(cwd, ["status", "--porcelain"]);
4591
+ const dirty = status.exitCode === 0 && status.stdout.trim().length > 0;
4592
+ if (dirty) {
4593
+ const commitMsg = title || `chat: ${branchName.replace(/^clawnet\/chat-/, "")}`;
4594
+ const commit = await runGit(cwd, ["commit", "-m", commitMsg]);
4595
+ if (commit.exitCode !== 0) {
4596
+ log9.warn(
4597
+ { stderr: commit.stderr },
4598
+ "chat ship: git commit failed (pre-commit hook?); retrying --no-verify"
4599
+ );
4600
+ const retry = await runGit(cwd, ["commit", "--no-verify", "-m", commitMsg]);
4601
+ if (retry.exitCode !== 0) {
4602
+ throw new Error(`git commit failed even with --no-verify: ${retry.stderr || retry.stdout}`);
4603
+ }
4604
+ }
4605
+ }
4606
+ let baseBranch = hintedBaseBranch ?? "main";
4607
+ if (!hintedBaseBranch) {
4608
+ const baseRes = await runGit(projectRoot2, [
4609
+ "symbolic-ref",
4610
+ "--short",
4611
+ "refs/remotes/origin/HEAD"
4612
+ ]).catch(() => null);
4613
+ if (baseRes && baseRes.exitCode === 0) {
4614
+ const trimmed = baseRes.stdout.trim();
4615
+ const m = trimmed.match(/^origin\/(.+)$/);
4616
+ if (m) baseBranch = m[1];
4617
+ }
4618
+ }
4619
+ const ghCheck = await new Promise((res) => {
4620
+ const c = spawn2("gh", ["--version"], { stdio: "ignore" });
4621
+ c.on("error", () => res(false));
4622
+ c.on("close", (code) => res(code === 0));
4623
+ });
4624
+ const finalTitle = title || `Chat session: ${branchName.replace(/^clawnet\/chat-/, "")}`;
4625
+ const finalBody = body || `Automated ship of chat-session worktree \`${branchName}\`.
4626
+
4627
+ Review the diff before merging.
4628
+ `;
4629
+ if (ghCheck) {
4630
+ const pr = await createDraftPR({
4631
+ cwd,
4632
+ branchName,
4633
+ baseBranch,
4634
+ title: finalTitle,
4635
+ bodyMarkdown: finalBody
4636
+ });
4637
+ if (pr.ok) {
4638
+ const result = {
4639
+ ok: true,
4640
+ strategy: "pr",
4641
+ prUrl: pr.url,
4642
+ branchName,
4643
+ baseBranch,
4644
+ dirty,
4645
+ mergedAt: null
4646
+ };
4647
+ if (typeof sessionId === "string" && sessionId) {
4648
+ this.hub.send({
4649
+ type: "session.shipment_updated",
4650
+ sessionId,
4651
+ result
4652
+ });
4653
+ }
4654
+ return result;
4655
+ }
4656
+ log9.warn(
4657
+ { step: pr.step, error: pr.error, branchName },
4658
+ "chat ship: PR creation failed, falling back to diff.patch"
4659
+ );
4660
+ }
4661
+ const diffRes = await runGit(cwd, ["diff", baseBranch + "...HEAD"]);
4662
+ const diff = diffRes.exitCode === 0 ? diffRes.stdout : "";
4663
+ const diffPath = join6(cwd, "diff.patch");
4664
+ const reportPath = join6(cwd, "report.md");
4665
+ await writeFile(diffPath, diff, "utf-8");
4666
+ await writeFile(reportPath, `# ${finalTitle}
4667
+
4668
+ ${finalBody}
4669
+
4670
+ ## Stats
4671
+
4672
+ - Branch: \`${branchName}\`
4673
+ - Base: \`${baseBranch}\`
4674
+ - Diff size: ${diff.length} bytes
4675
+ `, "utf-8");
4676
+ const fallbackResult = {
4677
+ ok: true,
4678
+ strategy: "diff-only",
4679
+ branchName,
4680
+ baseBranch,
4681
+ dirty,
4682
+ diffPath,
4683
+ reportPath,
4684
+ prUrl: null,
4685
+ mergedAt: null
4686
+ };
4687
+ if (typeof sessionId === "string" && sessionId) {
4688
+ this.hub.send({
4689
+ type: "session.shipment_updated",
4690
+ sessionId,
4691
+ result: fallbackResult
4692
+ });
4693
+ }
4694
+ return fallbackResult;
4695
+ }
4696
+ };
4697
+ function runGit(cwd, args) {
4698
+ return new Promise((resolveR) => {
4699
+ const child = spawn2("git", ["-C", cwd, ...args], { stdio: ["ignore", "pipe", "pipe"] });
4700
+ let stdout = "", stderr = "";
4701
+ child.stdout?.on("data", (c) => stdout += c.toString("utf-8"));
4702
+ child.stderr?.on("data", (c) => stderr += c.toString("utf-8"));
4703
+ child.on("error", (e) => resolveR({ exitCode: -1, stdout, stderr: e.message }));
4704
+ child.on("close", (code) => resolveR({ exitCode: code ?? -1, stdout, stderr }));
4705
+ });
4706
+ }
4707
+
3371
4708
  // src/templates-roles-bridge.ts
3372
- import { createLogger as createLogger8 } from "@mclawnet/logger";
4709
+ import { createLogger as createLogger10 } from "@mclawnet/logger";
3373
4710
  import {
3374
4711
  listTemplatesWithSource,
3375
4712
  getTemplateRaw,
@@ -3384,7 +4721,7 @@ import {
3384
4721
  deleteRole,
3385
4722
  loadRole as loadRole2
3386
4723
  } from "@mclawnet/swarm";
3387
- var log8 = createLogger8({ module: "templates-roles-bridge" });
4724
+ var log10 = createLogger10({ module: "templates-roles-bridge" });
3388
4725
  function requireStringParam(params, key) {
3389
4726
  const v = params[key];
3390
4727
  if (typeof v !== "string" || !v) throw new Error(`Missing '${key}' parameter`);
@@ -3395,10 +4732,10 @@ var TemplatesRolesBridge = class {
3395
4732
  this.hub = hub;
3396
4733
  this.hub.registerNamespace("templates", (msg) => this.handleTemplates(msg));
3397
4734
  this.hub.registerNamespace("roles", (msg) => this.handleRoles(msg));
3398
- log8.info("TemplatesRolesBridge initialized");
4735
+ log10.info("TemplatesRolesBridge initialized");
3399
4736
  }
3400
4737
  async handleTemplates(msg) {
3401
- log8.info({ action: msg.action, requestId: msg.requestId }, "templates request");
4738
+ log10.info({ action: msg.action, requestId: msg.requestId }, "templates request");
3402
4739
  switch (msg.action) {
3403
4740
  case "list":
3404
4741
  return { items: listTemplatesWithSource() };
@@ -3426,7 +4763,7 @@ var TemplatesRolesBridge = class {
3426
4763
  }
3427
4764
  }
3428
4765
  async handleRoles(msg) {
3429
- log8.info({ action: msg.action, requestId: msg.requestId }, "roles request");
4766
+ log10.info({ action: msg.action, requestId: msg.requestId }, "roles request");
3430
4767
  switch (msg.action) {
3431
4768
  case "list":
3432
4769
  return { items: listRolesWithSource() };
@@ -3456,8 +4793,8 @@ var TemplatesRolesBridge = class {
3456
4793
  };
3457
4794
 
3458
4795
  // src/swarm-session-bridge.ts
3459
- import { createLogger as createLogger9 } from "@mclawnet/logger";
3460
- var log9 = createLogger9({ module: "agent:swarm-bridge" });
4796
+ import { createLogger as createLogger11 } from "@mclawnet/logger";
4797
+ var log11 = createLogger11({ module: "agent:swarm-bridge" });
3461
4798
  function createSwarmAwareSessionStartedHandler(deps) {
3462
4799
  return (sessionId, info) => {
3463
4800
  deps.hub.send({
@@ -3472,20 +4809,20 @@ function createSwarmAwareSessionStartedHandler(deps) {
3472
4809
  info.backendSessionId
3473
4810
  );
3474
4811
  if (!ok) {
3475
- log9.debug(
4812
+ log11.debug(
3476
4813
  { sessionId },
3477
4814
  "session_started for swarm-shaped sessionId, but no matching role (already destroyed?)"
3478
4815
  );
3479
4816
  }
3480
4817
  } catch (err) {
3481
- log9.warn({ err, sessionId }, "failed to bridge session_started into swarm");
4818
+ log11.warn({ err, sessionId }, "failed to bridge session_started into swarm");
3482
4819
  }
3483
4820
  }
3484
4821
  };
3485
4822
  }
3486
4823
 
3487
4824
  // src/start.ts
3488
- import { createLogger as createLogger10 } from "@mclawnet/logger";
4825
+ import { createLogger as createLogger12 } from "@mclawnet/logger";
3489
4826
  import {
3490
4827
  initDatabase as initDatabase2,
3491
4828
  MemoryStore,
@@ -3500,30 +4837,31 @@ import {
3500
4837
  AccumulationScanner,
3501
4838
  triggerFromAccumulation
3502
4839
  } from "@mclawnet/skill-manager";
3503
- import { PROJECTS_RPC_CAPABILITY } from "@mclawnet/shared";
3504
- var log10 = createLogger10({ module: "agent" });
4840
+ import { PROJECTS_RPC_CAPABILITY, WORKDIR_RPC_CAPABILITY } from "@mclawnet/shared";
4841
+ var log12 = createLogger12({ module: "agent" });
3505
4842
  async function startAgent(options) {
3506
4843
  ensureRuntimeEnvDefaults();
3507
4844
  const config = loadConfig(options.config);
3508
4845
  if (!config.token) {
3509
- log10.error("no token configured \u2014 set CLAWNET_TOKEN or use --token");
4846
+ log12.error("no token configured \u2014 set CLAWNET_TOKEN or use --token");
3510
4847
  process.exit(1);
3511
4848
  }
3512
- log10.info({ backend: options.adapter.type }, "starting agent");
3513
- log10.info({ hubUrl: config.hubUrl }, "connecting to hub");
4849
+ log12.info({ backend: options.adapter.type }, "starting agent");
4850
+ log12.info({ hubUrl: config.hubUrl }, "connecting to hub");
4851
+ const workspaceManager = new WorkspaceManager();
3514
4852
  await initRoles();
3515
4853
  await initSkills();
3516
4854
  const hub = new HubConnection({
3517
4855
  hubUrl: config.hubUrl,
3518
4856
  token: config.token,
3519
4857
  hostname: config.name,
3520
- capabilities: [PROJECTS_RPC_CAPABILITY],
4858
+ capabilities: [PROJECTS_RPC_CAPABILITY, WORKDIR_RPC_CAPABILITY],
3521
4859
  onConnect: (agentId) => {
3522
- log10.info({ agentId }, "connected to hub");
4860
+ log12.info({ agentId }, "connected to hub");
3523
4861
  const skills = getSkillList();
3524
4862
  if (skills.length > 0) {
3525
4863
  hub.sendSkillList(skills);
3526
- log10.info({ count: skills.length }, "pushed skill list to hub");
4864
+ log12.info({ count: skills.length }, "pushed skill list to hub");
3527
4865
  }
3528
4866
  void sessionManager.recoverFromCheckpoint().then((report) => {
3529
4867
  for (const entry of report.dead) {
@@ -3534,24 +4872,30 @@ async function startAgent(options) {
3534
4872
  backendSessionId: entry.backendSessionId
3535
4873
  });
3536
4874
  }
3537
- log10.info(
4875
+ log12.info(
3538
4876
  { deadCount: report.dead.length, orphanCount: report.orphan.length },
3539
4877
  "checkpoint recover: cleanup complete"
3540
4878
  );
3541
4879
  }).catch((err) => {
3542
- log10.warn({ err }, "checkpoint recover: failed (degrading to no-op)");
4880
+ log12.warn({ err }, "checkpoint recover: failed (degrading to no-op)");
3543
4881
  });
3544
4882
  },
3545
4883
  onDisconnect: (code, reason) => {
3546
- log10.info({ code, reason }, "disconnected from hub");
4884
+ log12.info({ code, reason }, "disconnected from hub");
3547
4885
  },
3548
4886
  onError: (err) => {
3549
- log10.error({ err }, "hub connection error");
3550
- }
4887
+ log12.error({ err }, "hub connection error");
4888
+ },
4889
+ // M7.3 user-active signal: route every inbound hub message through the
4890
+ // projects-handler shim — at boot the AlwaysOnManager isn't constructed
4891
+ // yet, but _registerAlwaysOnHooks (called below) backs the indirection.
4892
+ onActivity: () => _recordAlwaysOnActivity(),
4893
+ // (N5) Shared workspace manager for chat `useWorktree` prepare/dispose.
4894
+ workspaceManager
3551
4895
  });
3552
4896
  let swarmCoordinator;
3553
- const clawnetDir = process.env.CLAWNET_DIR ?? join6(homedir5(), ".clawnet");
3554
- const dbPath = process.env.CLAWNET_MEMORY_DB ?? join6(clawnetDir, "memory.db");
4897
+ const clawnetDir = process.env.CLAWNET_DIR ?? join7(homedir5(), ".clawnet");
4898
+ const dbPath = process.env.CLAWNET_MEMORY_DB ?? join7(clawnetDir, "memory.db");
3555
4899
  let memoryStore = null;
3556
4900
  let embeddingService = null;
3557
4901
  let evolutionPipeline = null;
@@ -3564,7 +4908,7 @@ async function startAgent(options) {
3564
4908
  skillStore = new SkillStore(clawnetDir);
3565
4909
  evolutionPipeline = new EvolutionPipeline(clawnetDir);
3566
4910
  } catch (err) {
3567
- log10.warn({ err }, "failed to init memory/skill infra; distillation disabled");
4911
+ log12.warn({ err }, "failed to init memory/skill infra; distillation disabled");
3568
4912
  }
3569
4913
  const onBeforeClose = async (sessionId, messages) => {
3570
4914
  if (messages.length === 0) return;
@@ -3577,7 +4921,7 @@ async function startAgent(options) {
3577
4921
  embeddingService
3578
4922
  );
3579
4923
  } catch (err) {
3580
- log10.warn({ err, sessionId }, "distillation failed (non-fatal)");
4924
+ log12.warn({ err, sessionId }, "distillation failed (non-fatal)");
3581
4925
  }
3582
4926
  try {
3583
4927
  if (!skillStore || !evolutionPipeline) return;
@@ -3599,13 +4943,13 @@ async function startAgent(options) {
3599
4943
  await triggerFromAccumulation(signals, evolutionPipeline);
3600
4944
  }
3601
4945
  } catch (err) {
3602
- log10.warn({ err, sessionId }, "accumulation scan failed (non-fatal)");
4946
+ log12.warn({ err, sessionId }, "accumulation scan failed (non-fatal)");
3603
4947
  }
3604
4948
  };
3605
4949
  const sessionManager = new SessionManager({
3606
4950
  adapter: options.adapter,
3607
4951
  resolveAdapter: async (kind) => {
3608
- const { createBackendAdapter } = await import("./backend-factory-VRPU3534.js");
4952
+ const { createBackendAdapter } = await import("./backend-factory-AFF6I7YF.js");
3609
4953
  return createBackendAdapter(kind);
3610
4954
  },
3611
4955
  onOutput: (sessionId, data) => {
@@ -3647,7 +4991,7 @@ async function startAgent(options) {
3647
4991
  try {
3648
4992
  swarmCoordinator.handleRoleCrashed(sessionId, info.reason ?? `exit code=${info.code ?? "null"}`);
3649
4993
  } catch (err) {
3650
- log10.warn({ err, sessionId }, "swarmCoordinator.handleRoleCrashed threw");
4994
+ log12.warn({ err, sessionId }, "swarmCoordinator.handleRoleCrashed threw");
3651
4995
  }
3652
4996
  },
3653
4997
  onBeforeClose,
@@ -3663,6 +5007,28 @@ async function startAgent(options) {
3663
5007
  meta: req.meta
3664
5008
  });
3665
5009
  },
5010
+ // (N5) Chat session close → dispose the attached workspace (keep=true)
5011
+ // and notify hub so the browser can render the "worktree retained" toast.
5012
+ // dispose accepts swarmId, not the handle itself (see
5013
+ // packages/@clawnet/swarm/src/workspace/workspace-manager.ts:75).
5014
+ onWorkspaceClose: (sessionId, handle) => {
5015
+ log12.info(
5016
+ { sessionId, swarmId: handle.swarmId, cwd: handle.cwd, strategy: handle.strategy },
5017
+ "N5: disposing workspace (keep=true) for chat session close"
5018
+ );
5019
+ workspaceManager.dispose(handle.swarmId, { keep: true }).catch((err) => {
5020
+ log12.warn(
5021
+ { err, sessionId, swarmId: handle.swarmId, cwd: handle.cwd },
5022
+ "workspace dispose failed (best-effort)"
5023
+ );
5024
+ });
5025
+ hub.send({
5026
+ type: "session.worktree_closed",
5027
+ sessionId,
5028
+ cwd: handle.cwd,
5029
+ branchName: handle.metadata.branchName ?? null
5030
+ });
5031
+ },
3666
5032
  // PR-A: classify session kind for the idle sweeper. SessionManager stays
3667
5033
  // independent of the swarm package; we hand it a closure that defers to
3668
5034
  // SwarmCoordinator (created below) at call time. Lazy-read is safe because
@@ -3672,7 +5038,7 @@ async function startAgent(options) {
3672
5038
  // PR-C: enable logical-state checkpoint. Lives next to memory.db under
3673
5039
  // CLAWNET_DIR so backup/wipe affects both consistently. Per-call writes
3674
5040
  // are debounced to 5s by SessionManager itself.
3675
- checkpointPath: join6(clawnetDir, "agent-sessions.json")
5041
+ checkpointPath: join7(clawnetDir, "agent-sessions.json")
3676
5042
  });
3677
5043
  swarmCoordinator = new SwarmCoordinator(sessionManager, hub, (workDir) => {
3678
5044
  try {
@@ -3680,10 +5046,10 @@ async function startAgent(options) {
3680
5046
  const home = env ? env.replace(/\/\.clawnet\/?$/, "") : homedir5();
3681
5047
  return new TaskStore2({ workDir, home });
3682
5048
  } catch (err) {
3683
- log10.warn({ err, workDir }, "TaskStore factory failed");
5049
+ log12.warn({ err, workDir }, "TaskStore factory failed");
3684
5050
  return void 0;
3685
5051
  }
3686
- }, process.env.CLAWNET_HOME ?? homedir5());
5052
+ }, process.env.CLAWNET_HOME ?? homedir5(), workspaceManager);
3687
5053
  hub.setSessionManager(sessionManager);
3688
5054
  hub.setSwarmCoordinator(swarmCoordinator);
3689
5055
  const wakeupHome = process.env.CLAWNET_HOME ?? homedir5();
@@ -3695,18 +5061,140 @@ async function startAgent(options) {
3695
5061
  }));
3696
5062
  await wakeupScheduler.restoreFromInbox(wakeupHome, knownSwarms);
3697
5063
  } catch (err) {
3698
- log10.warn({ err }, "WakeupScheduler.restoreFromInbox failed (non-fatal)");
5064
+ log12.warn({ err }, "WakeupScheduler.restoreFromInbox failed (non-fatal)");
3699
5065
  }
3700
5066
  const scheduleRuntime = new ScheduleRuntime({ hub, sessionManager, swarmCoordinator });
3701
5067
  hub.setScheduleRuntime(scheduleRuntime);
3702
5068
  await scheduleRuntime.start();
5069
+ const alwaysOnHome = process.env.CLAWNET_HOME ?? homedir5();
5070
+ const taskStoreFactory = (projectRoot2) => {
5071
+ try {
5072
+ return new TaskStore2({ workDir: projectRoot2, home: alwaysOnHome });
5073
+ } catch (err) {
5074
+ log12.warn(
5075
+ { err: err instanceof Error ? err.message : String(err), projectRoot: projectRoot2 },
5076
+ "TaskStore factory failed (always-on)"
5077
+ );
5078
+ return void 0;
5079
+ }
5080
+ };
5081
+ let ideasClient = null;
5082
+ try {
5083
+ ideasClient = createIdeasRestClient({
5084
+ hubUrl: config.hubUrl,
5085
+ getAuthToken: () => config.token || null
5086
+ });
5087
+ } catch (err) {
5088
+ log12.warn(
5089
+ { err: err instanceof Error ? err.message : String(err) },
5090
+ "ideas REST client init failed \u2014 IdeaTodoSource will be skipped"
5091
+ );
5092
+ }
5093
+ const taskSources = [];
5094
+ if (ideasClient) {
5095
+ taskSources.push(
5096
+ createIdeaTodoSource({
5097
+ ideasClient,
5098
+ warn: (obj, msg) => log12.warn(obj, msg),
5099
+ info: (obj, msg) => log12.info(obj, msg)
5100
+ }),
5101
+ // M8.1 — research cycle source. Consumes status="idea" ideas, runs the
5102
+ // `research-only` template, and writes a ResearchRef + transitions the
5103
+ // idea status per the report's frontmatter `conclusion`.
5104
+ createIdeaResearchSource({
5105
+ ideasClient,
5106
+ warn: (obj, msg) => log12.warn(obj, msg),
5107
+ info: (obj, msg) => log12.info(obj, msg)
5108
+ }),
5109
+ // M8.2 — queen self-introspection (opt-in via
5110
+ // always-on.json `taskSources.introspection: true`). Pick() side-effects
5111
+ // a small introspection swarm that POSTs new ideas to /api/ideas and
5112
+ // always returns null — the cycle "skips" from the runner's perspective
5113
+ // and the user reviews the auto-discovered ideas next time. Independent
5114
+ // 24h cooldown; self-suppresses when the prior cohort was all archived.
5115
+ createIntrospectionSource({
5116
+ ideasClient,
5117
+ swarmCoordinator,
5118
+ getConfig: (projectRoot2) => loadAlwaysOnConfig2(projectRoot2, alwaysOnHome),
5119
+ updateConfig: async (projectRoot2, patch) => {
5120
+ const current = loadAlwaysOnConfig2(projectRoot2, alwaysOnHome);
5121
+ const next = mergeAlwaysOnConfig2(current, patch);
5122
+ saveAlwaysOnConfig2(projectRoot2, alwaysOnHome, next);
5123
+ },
5124
+ warn: (obj, msg) => log12.warn(obj, msg),
5125
+ info: (obj, msg) => log12.info(obj, msg)
5126
+ })
5127
+ );
5128
+ }
5129
+ const alwaysOnManager = new AlwaysOnManager({
5130
+ home: alwaysOnHome,
5131
+ swarmCoordinator,
5132
+ taskSources
5133
+ });
5134
+ _registerAlwaysOnHooks({
5135
+ getSnapshot: (workDir) => alwaysOnManager.snapshot(workDir),
5136
+ applyConfig: (workDir) => {
5137
+ void alwaysOnManager.refreshFromDisk(workDir).catch((err) => {
5138
+ log12.warn({ err, workDir }, "always-on refreshFromDisk failed");
5139
+ });
5140
+ },
5141
+ recordActivity: () => alwaysOnManager.recordActivity(),
5142
+ // B6 — dev-only force-tick. Hub gates the HTTP route on
5143
+ // NODE_ENV !== "production", so this hook is only reachable from
5144
+ // localhost dev tooling. Errors propagate verbatim so the handler can
5145
+ // classify them (no scheduler / tick already in flight / other) and
5146
+ // return an actionable message to the dev tool. The previous version
5147
+ // caught everything and returned null, which made the handler always
5148
+ // report "scheduler not booted" even when the real cause differed.
5149
+ forceTick: async (workDir) => alwaysOnManager.forceTick(workDir)
5150
+ });
5151
+ const bootedProjectRoots = [];
5152
+ try {
5153
+ const dirs = listProjectDirs(alwaysOnHome);
5154
+ for (const encoded of dirs) {
5155
+ const workDir = resolveWorkDir2(alwaysOnHome, encoded);
5156
+ if (!workDir) continue;
5157
+ const cfgPath = alwaysOnConfigPath(workDir, alwaysOnHome);
5158
+ if (!existsSync8(cfgPath)) continue;
5159
+ try {
5160
+ await alwaysOnManager.start(workDir);
5161
+ bootedProjectRoots.push(workDir);
5162
+ } catch (err) {
5163
+ log12.warn(
5164
+ { err: err instanceof Error ? err.message : String(err), workDir },
5165
+ "always-on scheduler boot failed for project (continuing with others)"
5166
+ );
5167
+ }
5168
+ }
5169
+ } catch (err) {
5170
+ log12.warn({ err }, "always-on boot scan failed (non-fatal)");
5171
+ }
5172
+ if (ideasClient && bootedProjectRoots.length > 0) {
5173
+ void reconcileOrphanedResearching({
5174
+ ideasClient,
5175
+ getProjectRoots: () => bootedProjectRoots,
5176
+ listRecoverableSwarmIds: listRecoverableSwarmIds2,
5177
+ log: {
5178
+ warn: (obj, msg) => log12.warn(obj, msg),
5179
+ info: (obj, msg) => log12.info(obj, msg)
5180
+ }
5181
+ }).catch((err) => {
5182
+ log12.warn({ err: err instanceof Error ? err.message : String(err) }, "reconcile sweep threw");
5183
+ });
5184
+ }
3703
5185
  const brainBridge = new BrainBridge(hub);
3704
5186
  const fsBridge = new FsBridge(hub);
5187
+ new WorktreeBridge(hub);
3705
5188
  const templatesRolesBridge = new TemplatesRolesBridge(hub);
3706
5189
  sessionManager.startIdleSweeper();
3707
5190
  const shutdown = async () => {
3708
- log10.info("shutting down");
5191
+ log12.info("shutting down");
3709
5192
  wakeupScheduler.dispose();
5193
+ try {
5194
+ await alwaysOnManager.disposeAll();
5195
+ } catch (err) {
5196
+ log12.warn({ err }, "alwaysOnManager.disposeAll failed");
5197
+ }
3710
5198
  await sessionManager.closeAll();
3711
5199
  await scheduleRuntime.stop();
3712
5200
  hub.destroy();
@@ -3714,7 +5202,7 @@ async function startAgent(options) {
3714
5202
  try {
3715
5203
  memoryDb.close();
3716
5204
  } catch (err) {
3717
- log10.warn({ err }, "failed to close memory db");
5205
+ log12.warn({ err }, "failed to close memory db");
3718
5206
  }
3719
5207
  }
3720
5208
  process.exit(0);
@@ -3732,4 +5220,4 @@ export {
3732
5220
  FsBridge,
3733
5221
  startAgent
3734
5222
  };
3735
- //# sourceMappingURL=chunk-B733MQCA.js.map
5223
+ //# sourceMappingURL=chunk-GOCWMRBB.js.map