@mclawnet/agent 0.6.10 → 0.6.12

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.
@@ -2,6 +2,10 @@ import {
2
2
  loadConfig
3
3
  } from "./chunk-CBZIH6FY.js";
4
4
 
5
+ // src/start.ts
6
+ import { homedir as homedir3 } from "os";
7
+ import { join as join4 } from "path";
8
+
5
9
  // src/hub-connection.ts
6
10
  import { hostname as osHostname } from "os";
7
11
  import WebSocket from "ws";
@@ -169,12 +173,12 @@ async function handleListHistorySessions(workDir) {
169
173
  sessions.sort((a, b) => (b.lastModified ?? 0) - (a.lastModified ?? 0));
170
174
  return { workDir, sessions };
171
175
  }
172
- async function handleLoadSessionHistory(workDir, claudeSessionId) {
176
+ async function handleLoadSessionHistory(workDir, claudeSessionId, opts) {
173
177
  const projectsDir = getClaudeProjectsDir();
174
178
  const projectFolder = pathToProjectFolder(workDir);
175
179
  const filePath = join(projectsDir, projectFolder, `${claudeSessionId}.jsonl`);
176
180
  if (!existsSync(filePath)) {
177
- return { messages: [] };
181
+ return { messages: [], oldestSeq: 0, hasMore: false };
178
182
  }
179
183
  const messages = [];
180
184
  try {
@@ -236,11 +240,19 @@ async function handleLoadSessionHistory(workDir, claudeSessionId) {
236
240
  }
237
241
  } catch {
238
242
  }
239
- return { messages: messages.slice(-50) };
243
+ const limit = Math.max(1, Math.min(opts?.limit ?? 50, 200));
244
+ const upper = opts?.before === void 0 || opts.before <= 0 || opts.before > messages.length ? messages.length : opts.before;
245
+ const lower = Math.max(0, upper - limit);
246
+ const slice = messages.slice(lower, upper);
247
+ return {
248
+ messages: slice,
249
+ oldestSeq: lower,
250
+ hasMore: lower > 0
251
+ };
240
252
  }
241
253
 
242
254
  // src/hub-connection.ts
243
- import { createLogger } from "@mclawnet/logger";
255
+ import { createLogger, previewFields } from "@mclawnet/logger";
244
256
  var log = createLogger({ module: "agent" });
245
257
  var HubConnection = class {
246
258
  ws = null;
@@ -425,12 +437,15 @@ var HubConnection = class {
425
437
  return true;
426
438
  }
427
439
  if (msg.type === "load_session_history") {
428
- log.info({ workDir: msg.workDir, claudeSessionId: msg.claudeSessionId }, "load_session_history");
429
- handleLoadSessionHistory(msg.workDir, msg.claudeSessionId).then((result) => {
440
+ log.info(
441
+ { workDir: msg.workDir, claudeSessionId: msg.claudeSessionId, before: msg.before, limit: msg.limit },
442
+ "load_session_history"
443
+ );
444
+ handleLoadSessionHistory(msg.workDir, msg.claudeSessionId, { before: msg.before, limit: msg.limit }).then((result) => {
430
445
  this.send({ type: "session_history_result", requestId: msg.requestId, ...result });
431
446
  }).catch((err) => {
432
447
  log.error({ err, workDir: msg.workDir }, "load_session_history failed");
433
- this.send({ type: "session_history_result", requestId: msg.requestId, messages: [] });
448
+ this.send({ type: "session_history_result", requestId: msg.requestId, messages: [], oldestSeq: 0, hasMore: false });
434
449
  });
435
450
  return true;
436
451
  }
@@ -596,13 +611,12 @@ var HubConnection = class {
596
611
  }
597
612
  if (msg.type === "claude.execute") {
598
613
  const { sessionId, content, workDir, claudeSessionId, useBrainCore } = msg;
599
- if (this.sessionManager.hasSession(sessionId)) {
600
- log.info({ sessionId }, "claude.execute: reusing existing session");
601
- this.sessionManager.sendInput(sessionId, content);
602
- } else {
603
- log.info({ sessionId, workDir, roleId: "role-__assistant__" }, "claude.execute: creating new session with memory injection");
604
- this.sessionManager.createSession({ sessionId, workDir, resumeId: claudeSessionId, useBrainCore, roleId: "role-__assistant__" }).then(() => {
605
- this.sessionManager.sendInput(sessionId, content);
614
+ const sm = this.sessionManager;
615
+ const spawnAndSend = (resumeId, label) => {
616
+ log.info({ sessionId, claudeSessionId: resumeId, workDir, label }, "claude.execute: spawning");
617
+ log.debug({ sessionId, ...previewFields(content) }, "claude.execute: input");
618
+ sm.createSession({ sessionId, workDir, resumeId, useBrainCore, roleId: "role-__assistant__" }).then(() => {
619
+ sm.sendInput(sessionId, content);
606
620
  }).catch((err) => {
607
621
  this.send({
608
622
  type: "session.error",
@@ -610,6 +624,45 @@ var HubConnection = class {
610
624
  error: err instanceof Error ? err.message : String(err)
611
625
  });
612
626
  });
627
+ };
628
+ if (sm.isHealthy(sessionId)) {
629
+ log.info({ sessionId }, "claude.execute: reusing healthy session");
630
+ log.debug({ sessionId, ...previewFields(content) }, "claude.execute: input");
631
+ sm.sendInput(sessionId, content);
632
+ } else if (sm.hasSession(sessionId) && claudeSessionId) {
633
+ log.warn({ sessionId, claudeSessionId }, "claude.execute: session unhealthy, recreating with --resume");
634
+ this.send({
635
+ type: "session.died",
636
+ sessionId,
637
+ reason: "unhealthy_before_input"
638
+ });
639
+ sm.abortSession(sessionId).catch((err) => {
640
+ log.warn({ sessionId, err: err instanceof Error ? err.message : String(err) }, "abortSession failed during unhealthy fallback, proceeding to respawn");
641
+ }).then(() => spawnAndSend(claudeSessionId, "unhealthy_fallback_resume"));
642
+ } else if (claudeSessionId) {
643
+ spawnAndSend(claudeSessionId, "fresh_resume");
644
+ } else {
645
+ spawnAndSend(void 0, "brand_new");
646
+ }
647
+ return true;
648
+ }
649
+ if (msg.type === "session.force_restart") {
650
+ const { sessionId, reason } = msg;
651
+ log.warn({ sessionId, reason }, "session.force_restart received");
652
+ const sm = this.sessionManager;
653
+ const reply = {
654
+ type: "session.died",
655
+ sessionId,
656
+ reason: reason ?? "force_restart"
657
+ };
658
+ if (sm.hasSession(sessionId)) {
659
+ sm.abortSession(sessionId).catch((err) => {
660
+ log.warn({ sessionId, err: err instanceof Error ? err.message : String(err) }, "abortSession failed during force_restart");
661
+ }).finally(() => {
662
+ this.send(reply);
663
+ });
664
+ } else {
665
+ this.send(reply);
613
666
  }
614
667
  return true;
615
668
  }
@@ -642,7 +695,10 @@ var HubConnection = class {
642
695
  return true;
643
696
  }
644
697
  if (msg.type === "claude.input") {
645
- log.info({ sessionId: msg.sessionId }, "claude.input");
698
+ log.debug(
699
+ { sessionId: msg.sessionId, ...previewFields(msg.content) },
700
+ "claude.input"
701
+ );
646
702
  this.sessionManager.sendInput(msg.sessionId, msg.content);
647
703
  return true;
648
704
  }
@@ -731,24 +787,321 @@ var HubConnection = class {
731
787
  };
732
788
 
733
789
  // src/session-manager.ts
734
- import { createLogger as createLogger2 } from "@mclawnet/logger";
790
+ import { createLogger as createLogger3, previewFields as previewFields2 } from "@mclawnet/logger";
735
791
  import { buildMemorySection } from "@mclawnet/memory";
736
- var log2 = createLogger2({ module: "agent/session-manager" });
792
+
793
+ // src/skill-loader.ts
794
+ import { existsSync as existsSync2, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
795
+ import { createHash } from "crypto";
796
+ import { join as join2, dirname } from "path";
797
+ import { homedir as homedir2 } from "os";
798
+ import { createRequire } from "module";
799
+ import { fileURLToPath } from "url";
800
+ import { createLogger as createLogger2 } from "@mclawnet/logger";
801
+ import { ManifestManager, mergeSkillSections } from "@mclawnet/skill-manager";
802
+ var log2 = createLogger2({ module: "agent/skill-loader" });
803
+ var CLAWNET_DIR = join2(homedir2(), ".clawnet");
804
+ var SKILLS_DIR = join2(CLAWNET_DIR, ".claude", "skills");
805
+ var MCP_CONFIG_PATH = join2(CLAWNET_DIR, "mcp.json");
806
+ async function initSkills() {
807
+ ensureSkillsDir();
808
+ syncBuiltinSkills(CLAWNET_DIR, defaultBuiltinSourceDir());
809
+ ensureMcpConfig();
810
+ scanSkills();
811
+ }
812
+ function defaultBuiltinSourceDir() {
813
+ const thisFile = fileURLToPath(import.meta.url);
814
+ return join2(dirname(thisFile), "..", "skills");
815
+ }
816
+ function sha256(s) {
817
+ return createHash("sha256").update(s).digest("hex");
818
+ }
819
+ function readBuiltinVersion(content) {
820
+ const match = content.match(/^version:\s*(.+)$/m);
821
+ return match ? match[1].trim().replace(/^["']|["']$/g, "") : "1.0.0";
822
+ }
823
+ function syncBuiltinSkills(rootDir, srcDir) {
824
+ const skillsDir = join2(rootDir, ".claude", "skills");
825
+ if (!existsSync2(skillsDir)) mkdirSync(skillsDir, { recursive: true });
826
+ if (!existsSync2(srcDir)) {
827
+ log2.debug({ srcDir }, "no built-in skills directory found, skipping");
828
+ return;
829
+ }
830
+ const manifest = new ManifestManager(rootDir);
831
+ let entries;
832
+ try {
833
+ entries = readdirSync2(srcDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
834
+ } catch {
835
+ log2.warn({ srcDir }, "failed to read built-in skills directory");
836
+ return;
837
+ }
838
+ for (const skillName of entries) {
839
+ const srcSkillMd = join2(srcDir, skillName, "SKILL.md");
840
+ if (!existsSync2(srcSkillMd)) continue;
841
+ const destDir = join2(skillsDir, skillName);
842
+ const destSkillMd = join2(destDir, "SKILL.md");
843
+ const baseSnapshotPath = join2(destDir, ".base.md");
844
+ let officialContent;
845
+ try {
846
+ officialContent = readFileSync2(srcSkillMd, "utf-8");
847
+ } catch (err) {
848
+ log2.warn({ skill: skillName, err }, "failed to read built-in skill");
849
+ continue;
850
+ }
851
+ const officialHash = sha256(officialContent);
852
+ const officialVersion = readBuiltinVersion(officialContent);
853
+ if (!existsSync2(destSkillMd)) {
854
+ try {
855
+ mkdirSync(destDir, { recursive: true });
856
+ writeFileSync(destSkillMd, officialContent);
857
+ writeFileSync(baseSnapshotPath, officialContent);
858
+ manifest.register(skillName, officialHash, officialVersion);
859
+ log2.info({ skill: skillName, version: officialVersion }, "installed built-in skill");
860
+ } catch (err) {
861
+ log2.warn({ skill: skillName, err }, "failed to install built-in skill");
862
+ }
863
+ continue;
864
+ }
865
+ const userContent = readFileSync2(destSkillMd, "utf-8");
866
+ const userHash = sha256(userContent);
867
+ manifest.refresh(skillName, userHash);
868
+ if (!existsSync2(baseSnapshotPath)) {
869
+ const entry = manifest.load().skills[skillName];
870
+ if (entry && !entry.userModified) {
871
+ try {
872
+ writeFileSync(baseSnapshotPath, userContent);
873
+ } catch (err) {
874
+ log2.debug({ skill: skillName, err }, "failed to write lazy .base.md");
875
+ }
876
+ }
877
+ }
878
+ const action = manifest.determineSyncAction(skillName, officialHash, officialVersion);
879
+ switch (action) {
880
+ case "skip":
881
+ log2.debug({ skill: skillName }, "skill already up to date");
882
+ break;
883
+ case "direct-overwrite":
884
+ try {
885
+ writeFileSync(destSkillMd, officialContent);
886
+ writeFileSync(baseSnapshotPath, officialContent);
887
+ manifest.markSynced(skillName, officialHash, officialHash, officialVersion);
888
+ log2.info({ skill: skillName, version: officialVersion }, "upgraded built-in skill");
889
+ } catch (err) {
890
+ log2.warn({ skill: skillName, err }, "failed to overwrite skill");
891
+ }
892
+ break;
893
+ case "keep-user":
894
+ log2.debug({ skill: skillName }, "keeping user-modified skill");
895
+ break;
896
+ case "needs-merge": {
897
+ if (!existsSync2(baseSnapshotPath)) {
898
+ try {
899
+ writeFileSync(
900
+ destSkillMd + ".conflict",
901
+ renderConflictFile(skillName, officialContent, [
902
+ {
903
+ section: "(entire file)",
904
+ official: officialContent,
905
+ user: userContent
906
+ }
907
+ ])
908
+ );
909
+ log2.warn(
910
+ { skill: skillName },
911
+ "missing .base.md snapshot \u2014 skipping auto-merge, wrote .conflict advisory"
912
+ );
913
+ } catch (err) {
914
+ log2.warn({ skill: skillName, err }, "failed to write .conflict advisory");
915
+ }
916
+ break;
917
+ }
918
+ const baseContent = readFileSync2(baseSnapshotPath, "utf-8");
919
+ const merged = mergeSkillSections(baseContent, officialContent, userContent);
920
+ if (merged.success && merged.content) {
921
+ try {
922
+ writeFileSync(destSkillMd, merged.content);
923
+ writeFileSync(baseSnapshotPath, officialContent);
924
+ const newCurrentHash = sha256(merged.content);
925
+ manifest.markSynced(skillName, officialHash, newCurrentHash, officialVersion);
926
+ log2.info({ skill: skillName }, "auto-merged skill upgrade");
927
+ } catch (err) {
928
+ log2.warn({ skill: skillName, err }, "failed to write merged skill");
929
+ }
930
+ } else {
931
+ try {
932
+ writeFileSync(
933
+ destSkillMd + ".conflict",
934
+ renderConflictFile(skillName, officialContent, merged.conflicts ?? [])
935
+ );
936
+ log2.warn(
937
+ { skill: skillName, conflicts: merged.conflicts?.length },
938
+ "skill merge conflict \u2014 user version kept, .conflict file written"
939
+ );
940
+ } catch (err) {
941
+ log2.warn({ skill: skillName, err }, "failed to write .conflict file");
942
+ }
943
+ }
944
+ break;
945
+ }
946
+ }
947
+ }
948
+ }
949
+ function renderConflictFile(skillName, officialContent, conflicts) {
950
+ const lines = [
951
+ `# Skill upgrade conflict: ${skillName}`,
952
+ ``,
953
+ `The official skill was updated, and your local copy was also modified.`,
954
+ `Please review each section below and reconcile manually.`,
955
+ ``,
956
+ `## Conflicted sections`,
957
+ ``
958
+ ];
959
+ for (const c of conflicts) {
960
+ lines.push(`### ${c.section}`);
961
+ lines.push("");
962
+ lines.push("**Official:**");
963
+ lines.push("```");
964
+ lines.push(c.official);
965
+ lines.push("```");
966
+ lines.push("");
967
+ lines.push("**Your version:**");
968
+ lines.push("```");
969
+ lines.push(c.user);
970
+ lines.push("```");
971
+ lines.push("");
972
+ }
973
+ lines.push("## Full official content");
974
+ lines.push("```");
975
+ lines.push(officialContent);
976
+ lines.push("```");
977
+ return lines.join("\n");
978
+ }
979
+ function ensureSkillsDir() {
980
+ if (!existsSync2(SKILLS_DIR)) {
981
+ mkdirSync(SKILLS_DIR, { recursive: true });
982
+ log2.info({ dir: SKILLS_DIR }, "created skills directory");
983
+ }
984
+ }
985
+ var cachedSkills = [];
986
+ function scanSkills() {
987
+ if (!existsSync2(SKILLS_DIR)) return [];
988
+ let dirs;
989
+ try {
990
+ dirs = readdirSync2(SKILLS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
991
+ } catch {
992
+ log2.warn("failed to read skills directory for scanning");
993
+ return [];
994
+ }
995
+ const skills = [];
996
+ for (const dirName of dirs) {
997
+ const skillMd = join2(SKILLS_DIR, dirName, "SKILL.md");
998
+ if (!existsSync2(skillMd)) continue;
999
+ try {
1000
+ const content = readFileSync2(skillMd, "utf-8");
1001
+ const parsed = parseFrontmatter(content);
1002
+ if (parsed.name) {
1003
+ skills.push({
1004
+ name: parsed.name,
1005
+ description: parsed.description || dirName
1006
+ });
1007
+ }
1008
+ } catch (err) {
1009
+ log2.debug({ skill: dirName, err }, "failed to parse SKILL.md frontmatter");
1010
+ }
1011
+ }
1012
+ cachedSkills = skills;
1013
+ log2.info({ count: skills.length }, "scanned skills");
1014
+ return skills;
1015
+ }
1016
+ function getSkillList() {
1017
+ return cachedSkills;
1018
+ }
1019
+ function getPendingNotification() {
1020
+ const pendingPath = join2(CLAWNET_DIR, ".claude", "pending-evolutions.json");
1021
+ if (!existsSync2(pendingPath)) return null;
1022
+ try {
1023
+ const raw = JSON.parse(readFileSync2(pendingPath, "utf-8"));
1024
+ const pending = Array.isArray(raw?.pending) ? raw.pending : [];
1025
+ if (pending.length === 0) return null;
1026
+ const lines = pending.map(
1027
+ (p) => `- [${p.signal ?? "?"}] ${p.skillName ?? "?"}: ${p.problem ?? ""} (confidence: ${p.confidence ?? "?"})`
1028
+ );
1029
+ const text = `\u6709 ${pending.length} \u6761 Skill \u8FDB\u5316\u63D0\u6848\u5F85\u5BA1\u6838\uFF1A
1030
+ ${lines.join("\n")}
1031
+ \u4F7F\u7528 skill_pending_list \u67E5\u770B\u8BE6\u60C5\uFF0Cskill_pending_approve / skill_pending_reject \u5904\u7406\u3002`;
1032
+ return { count: pending.length, text };
1033
+ } catch (err) {
1034
+ log2.debug({ err }, "failed to read pending-evolutions.json");
1035
+ return null;
1036
+ }
1037
+ }
1038
+ function parseFrontmatter(content) {
1039
+ const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
1040
+ if (!match) return { name: "", description: "" };
1041
+ const yaml = match[1];
1042
+ const nameMatch = yaml.match(/^name:\s*(.+)$/m);
1043
+ const descMatch = yaml.match(/^description:\s*(.+)$/m);
1044
+ const strip = (s) => s.trim().replace(/^["']|["']$/g, "");
1045
+ return {
1046
+ name: strip(nameMatch?.[1] ?? ""),
1047
+ description: strip(descMatch?.[1] ?? "")
1048
+ };
1049
+ }
1050
+ function ensureMcpConfig() {
1051
+ if (existsSync2(MCP_CONFIG_PATH)) {
1052
+ log2.debug("mcp.json already exists, skipping");
1053
+ return;
1054
+ }
1055
+ let mcpServerPath;
1056
+ try {
1057
+ const req = createRequire(import.meta.url);
1058
+ const mcpPkgDir = dirname(req.resolve("@mclawnet/mcp-server/package.json"));
1059
+ mcpServerPath = join2(mcpPkgDir, "dist", "server.js");
1060
+ } catch {
1061
+ log2.warn("could not resolve @mclawnet/mcp-server package path, skipping mcp.json generation");
1062
+ return;
1063
+ }
1064
+ const config = {
1065
+ mcpServers: {
1066
+ "clawnet-mcp": {
1067
+ command: "node",
1068
+ args: [mcpServerPath]
1069
+ }
1070
+ }
1071
+ };
1072
+ try {
1073
+ mkdirSync(CLAWNET_DIR, { recursive: true });
1074
+ writeFileSync(MCP_CONFIG_PATH, JSON.stringify(config, null, 2) + "\n");
1075
+ log2.info({ path: MCP_CONFIG_PATH }, "generated default mcp.json");
1076
+ } catch (err) {
1077
+ log2.warn({ err }, "failed to generate mcp.json");
1078
+ }
1079
+ }
1080
+
1081
+ // src/session-manager.ts
1082
+ var log3 = createLogger3({ module: "agent/session-manager" });
737
1083
  var DEFAULT_MAX_PROCESSES = 10;
738
1084
  var MAX_PROCESSES = Number(process.env.CLAWNET_MAX_PROCESSES) || DEFAULT_MAX_PROCESSES;
739
1085
  var SessionManager = class {
740
1086
  sessions = /* @__PURE__ */ new Map();
741
1087
  conversationBuffer = /* @__PURE__ */ new Map();
1088
+ // Sessions whose abort is in flight. Treated as not-present by hasSession /
1089
+ // isHealthy so a racing claude.execute (e.g., right after force_restart) is
1090
+ // correctly routed to the "spawn new + --resume" branch instead of trying
1091
+ // to write to a process we are about to kill.
1092
+ aborting = /* @__PURE__ */ new Set();
742
1093
  adapter;
743
1094
  onOutput;
744
1095
  onTurnComplete;
745
1096
  onSessionError;
1097
+ onSessionStarted;
746
1098
  onBeforeClose;
747
1099
  constructor(options) {
748
1100
  this.adapter = options.adapter;
749
1101
  this.onOutput = options.onOutput;
750
1102
  this.onTurnComplete = options.onTurnComplete;
751
1103
  this.onSessionError = options.onSessionError;
1104
+ this.onSessionStarted = options.onSessionStarted;
752
1105
  this.onBeforeClose = options.onBeforeClose;
753
1106
  }
754
1107
  async createSession(options) {
@@ -767,11 +1120,27 @@ var SessionManager = class {
767
1120
  options.systemPrompt = options.systemPrompt ? `${memorySection}${roleHint}
768
1121
 
769
1122
  ${options.systemPrompt}` : `${memorySection}${roleHint}`;
770
- log2.debug({ roleId: options.roleId, sessionId: options.sessionId }, "memory prompt + roleId hint injected");
1123
+ log3.debug({ roleId: options.roleId, sessionId: options.sessionId }, "memory prompt + roleId hint injected");
771
1124
  } catch (err) {
772
- log2.warn({ err, roleId: options.roleId }, "failed to build memory section, proceeding without");
1125
+ log3.warn({ err, roleId: options.roleId }, "failed to build memory section, proceeding without");
773
1126
  }
774
1127
  }
1128
+ try {
1129
+ const notice = getPendingNotification();
1130
+ if (notice) {
1131
+ const noticeBlock = `
1132
+
1133
+ [Skill \u8FDB\u5316\u63D0\u6848]
1134
+ ${notice.text}`;
1135
+ options.systemPrompt = options.systemPrompt ? `${options.systemPrompt}${noticeBlock}` : noticeBlock.trimStart();
1136
+ log3.debug(
1137
+ { sessionId: options.sessionId, pending: notice.count },
1138
+ "pending skill evolution notice injected"
1139
+ );
1140
+ }
1141
+ } catch (err) {
1142
+ log3.debug({ err }, "failed to inject pending notification");
1143
+ }
775
1144
  try {
776
1145
  const process2 = await this.adapter.spawn(options);
777
1146
  this.sessions.set(options.sessionId, process2);
@@ -793,6 +1162,29 @@ ${options.systemPrompt}` : `${memorySection}${roleHint}`;
793
1162
  this.adapter.onError?.(process2, (error) => {
794
1163
  this.onSessionError(options.sessionId, error.message);
795
1164
  });
1165
+ if (this.onSessionStarted) {
1166
+ this.adapter.onSessionStarted?.(process2, (info) => {
1167
+ if (this.aborting.has(options.sessionId)) {
1168
+ log3.debug(
1169
+ { sessionId: options.sessionId },
1170
+ "suppressing late session_started \u2014 session is aborting"
1171
+ );
1172
+ return;
1173
+ }
1174
+ if (this.sessions.get(options.sessionId) !== process2) {
1175
+ return;
1176
+ }
1177
+ this.onSessionStarted(options.sessionId, info);
1178
+ });
1179
+ }
1180
+ this.adapter.onExit?.(process2, (code) => {
1181
+ if (this.sessions.get(options.sessionId) === process2) {
1182
+ this.sessions.delete(options.sessionId);
1183
+ this.conversationBuffer.delete(options.sessionId);
1184
+ log3.warn({ sessionId: options.sessionId, exitCode: code }, "backend process exited unexpectedly, evicted from session map");
1185
+ this.onSessionError(options.sessionId, `backend process exited (code=${code ?? "null"})`);
1186
+ }
1187
+ });
796
1188
  return process2.id;
797
1189
  } catch (err) {
798
1190
  const message = err instanceof Error ? err.message : String(err);
@@ -806,14 +1198,23 @@ ${options.systemPrompt}` : `${memorySection}${roleHint}`;
806
1198
  this.onSessionError(sessionId, `No active session: ${sessionId}`);
807
1199
  return;
808
1200
  }
1201
+ log3.debug(
1202
+ { sessionId, ...previewFields2(input) },
1203
+ "sendInput \u2192 backend stdin"
1204
+ );
809
1205
  this.adapter.send(process2, input);
810
1206
  }
811
1207
  async abortSession(sessionId) {
812
1208
  const process2 = this.sessions.get(sessionId);
813
1209
  if (!process2) return;
1210
+ this.aborting.add(sessionId);
814
1211
  this.conversationBuffer.delete(sessionId);
815
1212
  this.sessions.delete(sessionId);
816
- await this.adapter.stop(process2);
1213
+ try {
1214
+ await this.adapter.stop(process2);
1215
+ } finally {
1216
+ this.aborting.delete(sessionId);
1217
+ }
817
1218
  }
818
1219
  async closeSession(sessionId) {
819
1220
  const process2 = this.sessions.get(sessionId);
@@ -844,7 +1245,18 @@ ${options.systemPrompt}` : `${memorySection}${roleHint}`;
844
1245
  await Promise.all(promises);
845
1246
  }
846
1247
  hasSession(sessionId) {
847
- return this.sessions.has(sessionId);
1248
+ return this.sessions.has(sessionId) && !this.aborting.has(sessionId);
1249
+ }
1250
+ /**
1251
+ * Stricter than hasSession(): also requires the underlying process to still
1252
+ * be alive (not exited, stdin still writable). Callers in the reuse path
1253
+ * must use this to avoid sending input into a dead pipe — if it returns
1254
+ * false they should fall back to `--resume` against db's claudeSessionId.
1255
+ */
1256
+ isHealthy(sessionId) {
1257
+ if (this.aborting.has(sessionId)) return false;
1258
+ const process2 = this.sessions.get(sessionId);
1259
+ return !!process2 && process2.isAlive();
848
1260
  }
849
1261
  get activeSessionCount() {
850
1262
  return this.sessions.size;
@@ -857,138 +1269,6 @@ ${options.systemPrompt}` : `${memorySection}${roleHint}`;
857
1269
  // src/start.ts
858
1270
  import { SwarmCoordinator, initRoles } from "@mclawnet/swarm";
859
1271
 
860
- // src/skill-loader.ts
861
- import { existsSync as existsSync2, mkdirSync, copyFileSync, readdirSync as readdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
862
- import { join as join2, dirname } from "path";
863
- import { homedir as homedir2 } from "os";
864
- import { createRequire } from "module";
865
- import { fileURLToPath } from "url";
866
- import { createLogger as createLogger3 } from "@mclawnet/logger";
867
- var log3 = createLogger3({ module: "agent/skill-loader" });
868
- var CLAWNET_DIR = join2(homedir2(), ".clawnet");
869
- var SKILLS_DIR = join2(CLAWNET_DIR, ".claude", "skills");
870
- var MCP_CONFIG_PATH = join2(CLAWNET_DIR, "mcp.json");
871
- async function initSkills() {
872
- ensureSkillsDir();
873
- copyBuiltinSkills();
874
- ensureMcpConfig();
875
- scanSkills();
876
- }
877
- function ensureSkillsDir() {
878
- if (!existsSync2(SKILLS_DIR)) {
879
- mkdirSync(SKILLS_DIR, { recursive: true });
880
- log3.info({ dir: SKILLS_DIR }, "created skills directory");
881
- }
882
- }
883
- function copyBuiltinSkills() {
884
- const thisFile = fileURLToPath(import.meta.url);
885
- const srcDir = join2(dirname(thisFile), "..", "skills");
886
- if (!existsSync2(srcDir)) {
887
- log3.debug({ srcDir }, "no built-in skills directory found, skipping");
888
- return;
889
- }
890
- let entries;
891
- try {
892
- entries = readdirSync2(srcDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
893
- } catch {
894
- log3.warn({ srcDir }, "failed to read built-in skills directory");
895
- return;
896
- }
897
- for (const skillName of entries) {
898
- const srcSkillMd = join2(srcDir, skillName, "SKILL.md");
899
- const destDir = join2(SKILLS_DIR, skillName);
900
- const destSkillMd = join2(destDir, "SKILL.md");
901
- if (!existsSync2(srcSkillMd)) continue;
902
- if (existsSync2(destSkillMd)) {
903
- log3.debug({ skill: skillName }, "skill already exists, skipping");
904
- continue;
905
- }
906
- try {
907
- mkdirSync(destDir, { recursive: true });
908
- copyFileSync(srcSkillMd, destSkillMd);
909
- log3.info({ skill: skillName }, "copied built-in skill");
910
- } catch (err) {
911
- log3.warn({ skill: skillName, err }, "failed to copy built-in skill");
912
- }
913
- }
914
- }
915
- var cachedSkills = [];
916
- function scanSkills() {
917
- if (!existsSync2(SKILLS_DIR)) return [];
918
- let dirs;
919
- try {
920
- dirs = readdirSync2(SKILLS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
921
- } catch {
922
- log3.warn("failed to read skills directory for scanning");
923
- return [];
924
- }
925
- const skills = [];
926
- for (const dirName of dirs) {
927
- const skillMd = join2(SKILLS_DIR, dirName, "SKILL.md");
928
- if (!existsSync2(skillMd)) continue;
929
- try {
930
- const content = readFileSync2(skillMd, "utf-8");
931
- const parsed = parseFrontmatter(content);
932
- if (parsed.name) {
933
- skills.push({
934
- name: parsed.name,
935
- description: parsed.description || dirName
936
- });
937
- }
938
- } catch (err) {
939
- log3.debug({ skill: dirName, err }, "failed to parse SKILL.md frontmatter");
940
- }
941
- }
942
- cachedSkills = skills;
943
- log3.info({ count: skills.length }, "scanned skills");
944
- return skills;
945
- }
946
- function getSkillList() {
947
- return cachedSkills;
948
- }
949
- function parseFrontmatter(content) {
950
- const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
951
- if (!match) return { name: "", description: "" };
952
- const yaml = match[1];
953
- const nameMatch = yaml.match(/^name:\s*(.+)$/m);
954
- const descMatch = yaml.match(/^description:\s*(.+)$/m);
955
- const strip = (s) => s.trim().replace(/^["']|["']$/g, "");
956
- return {
957
- name: strip(nameMatch?.[1] ?? ""),
958
- description: strip(descMatch?.[1] ?? "")
959
- };
960
- }
961
- function ensureMcpConfig() {
962
- if (existsSync2(MCP_CONFIG_PATH)) {
963
- log3.debug("mcp.json already exists, skipping");
964
- return;
965
- }
966
- let mcpServerPath;
967
- try {
968
- const req = createRequire(import.meta.url);
969
- const memoryPkgDir = dirname(req.resolve("@mclawnet/memory/package.json"));
970
- mcpServerPath = join2(memoryPkgDir, "dist", "mcp", "server.js");
971
- } catch {
972
- log3.warn("could not resolve @mclawnet/memory package path, skipping mcp.json generation");
973
- return;
974
- }
975
- const config = {
976
- mcpServers: {
977
- "clawnet-memory": {
978
- command: "node",
979
- args: [mcpServerPath]
980
- }
981
- }
982
- };
983
- try {
984
- mkdirSync(CLAWNET_DIR, { recursive: true });
985
- writeFileSync(MCP_CONFIG_PATH, JSON.stringify(config, null, 2) + "\n");
986
- log3.info({ path: MCP_CONFIG_PATH }, "generated default mcp.json");
987
- } catch (err) {
988
- log3.warn({ err }, "failed to generate mcp.json");
989
- }
990
- }
991
-
992
1272
  // src/brain-bridge.ts
993
1273
  import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync as readdirSync3 } from "fs";
994
1274
  import { join as join3 } from "path";
@@ -1372,6 +1652,19 @@ var FsBridge = class {
1372
1652
 
1373
1653
  // src/start.ts
1374
1654
  import { createLogger as createLogger6 } from "@mclawnet/logger";
1655
+ import {
1656
+ initDatabase,
1657
+ MemoryStore,
1658
+ EmbeddingService,
1659
+ createEmbeddingProviders,
1660
+ distillConversation
1661
+ } from "@mclawnet/memory";
1662
+ import {
1663
+ SkillStore,
1664
+ EvolutionPipeline,
1665
+ AccumulationScanner,
1666
+ triggerFromAccumulation
1667
+ } from "@mclawnet/skill-manager";
1375
1668
  var log6 = createLogger6({ module: "agent" });
1376
1669
  async function startAgent(options) {
1377
1670
  const config = loadConfig(options.config);
@@ -1403,6 +1696,58 @@ async function startAgent(options) {
1403
1696
  }
1404
1697
  });
1405
1698
  let swarmCoordinator;
1699
+ const clawnetDir = process.env.CLAWNET_DIR ?? join4(homedir3(), ".clawnet");
1700
+ const dbPath = process.env.CLAWNET_MEMORY_DB ?? join4(clawnetDir, "memory.db");
1701
+ let memoryStore = null;
1702
+ let embeddingService = null;
1703
+ let evolutionPipeline = null;
1704
+ let skillStore = null;
1705
+ let memoryDb = null;
1706
+ try {
1707
+ memoryDb = initDatabase(dbPath);
1708
+ memoryStore = new MemoryStore(memoryDb);
1709
+ embeddingService = new EmbeddingService(memoryDb, createEmbeddingProviders());
1710
+ skillStore = new SkillStore(clawnetDir);
1711
+ evolutionPipeline = new EvolutionPipeline(clawnetDir);
1712
+ } catch (err) {
1713
+ log6.warn({ err }, "failed to init memory/skill infra; distillation disabled");
1714
+ }
1715
+ const onBeforeClose = async (sessionId, messages) => {
1716
+ if (messages.length === 0) return;
1717
+ if (!memoryStore || !embeddingService) return;
1718
+ try {
1719
+ await distillConversation(
1720
+ messages,
1721
+ "role-__assistant__",
1722
+ memoryStore,
1723
+ embeddingService
1724
+ );
1725
+ } catch (err) {
1726
+ log6.warn({ err, sessionId }, "distillation failed (non-fatal)");
1727
+ }
1728
+ try {
1729
+ if (!skillStore || !evolutionPipeline) return;
1730
+ const skills = skillStore.scan();
1731
+ if (skills.length === 0) return;
1732
+ const recent = memoryStore.getMemoriesByRole("role-__assistant__", "working");
1733
+ const refs = recent.map((m) => ({
1734
+ id: m.id,
1735
+ type: m.type,
1736
+ content: m.content,
1737
+ domain: m.domain
1738
+ }));
1739
+ const scanner = new AccumulationScanner();
1740
+ const signals = scanner.scan(
1741
+ refs,
1742
+ skills.map((s) => s.name)
1743
+ );
1744
+ if (signals.length > 0) {
1745
+ await triggerFromAccumulation(signals, evolutionPipeline);
1746
+ }
1747
+ } catch (err) {
1748
+ log6.warn({ err, sessionId }, "accumulation scan failed (non-fatal)");
1749
+ }
1750
+ };
1406
1751
  const sessionManager = new SessionManager({
1407
1752
  adapter: options.adapter,
1408
1753
  onOutput: (sessionId, data) => {
@@ -1424,13 +1769,21 @@ async function startAgent(options) {
1424
1769
  contextUsage: info.contextUsage
1425
1770
  });
1426
1771
  },
1772
+ onSessionStarted: (sessionId, info) => {
1773
+ hub.send({
1774
+ type: "claude.session_started",
1775
+ sessionId,
1776
+ claudeSessionId: info.claudeSessionId
1777
+ });
1778
+ },
1427
1779
  onSessionError: (sessionId, error) => {
1428
1780
  hub.send({
1429
1781
  type: "session.error",
1430
1782
  sessionId,
1431
1783
  error
1432
1784
  });
1433
- }
1785
+ },
1786
+ onBeforeClose
1434
1787
  });
1435
1788
  swarmCoordinator = new SwarmCoordinator(sessionManager, hub);
1436
1789
  hub.setSessionManager(sessionManager);
@@ -1441,6 +1794,13 @@ async function startAgent(options) {
1441
1794
  log6.info("shutting down");
1442
1795
  await sessionManager.closeAll();
1443
1796
  hub.destroy();
1797
+ if (memoryDb) {
1798
+ try {
1799
+ memoryDb.close();
1800
+ } catch (err) {
1801
+ log6.warn({ err }, "failed to close memory db");
1802
+ }
1803
+ }
1444
1804
  process.exit(0);
1445
1805
  };
1446
1806
  process.on("SIGINT", shutdown);
@@ -1456,4 +1816,4 @@ export {
1456
1816
  FsBridge,
1457
1817
  startAgent
1458
1818
  };
1459
- //# sourceMappingURL=chunk-AZDR76NN.js.map
1819
+ //# sourceMappingURL=chunk-MSDIRBXF.js.map