metame-cli 1.5.11 → 1.5.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.
Files changed (55) hide show
  1. package/index.js +64 -7
  2. package/package.json +3 -2
  3. package/scripts/daemon-agent-commands.js +6 -2
  4. package/scripts/daemon-bridges.js +23 -9
  5. package/scripts/daemon-claude-engine.js +87 -28
  6. package/scripts/daemon-command-router.js +16 -0
  7. package/scripts/daemon-command-session-route.js +3 -1
  8. package/scripts/daemon-engine-runtime.js +1 -5
  9. package/scripts/daemon-message-pipeline.js +113 -44
  10. package/scripts/daemon-reactive-lifecycle.js +405 -9
  11. package/scripts/daemon-session-commands.js +3 -2
  12. package/scripts/daemon-session-store.js +82 -27
  13. package/scripts/daemon-team-dispatch.js +21 -5
  14. package/scripts/daemon-utils.js +3 -1
  15. package/scripts/daemon.js +1 -0
  16. package/scripts/docs/file-transfer.md +1 -0
  17. package/scripts/hooks/intent-file-transfer.js +2 -1
  18. package/scripts/hooks/intent-perpetual.js +109 -0
  19. package/scripts/hooks/intent-research.js +112 -0
  20. package/scripts/intent-registry.js +4 -0
  21. package/scripts/ops-mission-queue.js +258 -0
  22. package/scripts/ops-verifier.js +197 -0
  23. package/skills/agent-browser/SKILL.md +153 -0
  24. package/skills/agent-reach/SKILL.md +66 -0
  25. package/skills/agent-reach/evolution.json +13 -0
  26. package/skills/deep-research/SKILL.md +77 -0
  27. package/skills/find-skills/SKILL.md +133 -0
  28. package/skills/heartbeat-task-manager/SKILL.md +63 -0
  29. package/skills/macos-local-orchestrator/SKILL.md +192 -0
  30. package/skills/macos-local-orchestrator/agents/openai.yaml +4 -0
  31. package/skills/macos-local-orchestrator/references/tooling-landscape.md +70 -0
  32. package/skills/macos-mail-calendar/SKILL.md +394 -0
  33. package/skills/mcp-installer/SKILL.md +138 -0
  34. package/skills/skill-creator/LICENSE.txt +202 -0
  35. package/skills/skill-creator/README.md +72 -0
  36. package/skills/skill-creator/SKILL.md +96 -0
  37. package/skills/skill-creator/evolution.json +6 -0
  38. package/skills/skill-creator/references/creation-guide.md +116 -0
  39. package/skills/skill-creator/references/evolution-guide.md +74 -0
  40. package/skills/skill-creator/references/output-patterns.md +82 -0
  41. package/skills/skill-creator/references/workflows.md +28 -0
  42. package/skills/skill-creator/scripts/align_all.py +32 -0
  43. package/skills/skill-creator/scripts/auto_evolve_hook.js +247 -0
  44. package/skills/skill-creator/scripts/init_skill.py +303 -0
  45. package/skills/skill-creator/scripts/merge_evolution.py +70 -0
  46. package/skills/skill-creator/scripts/package_skill.py +110 -0
  47. package/skills/skill-creator/scripts/quick_validate.py +103 -0
  48. package/skills/skill-creator/scripts/setup.py +141 -0
  49. package/skills/skill-creator/scripts/smart_stitch.py +82 -0
  50. package/skills/skill-manager/SKILL.md +112 -0
  51. package/skills/skill-manager/scripts/delete_skill.py +31 -0
  52. package/skills/skill-manager/scripts/list_skills.py +61 -0
  53. package/skills/skill-manager/scripts/scan_and_check.py +125 -0
  54. package/skills/skill-manager/scripts/sync_index.py +144 -0
  55. package/skills/skill-manager/scripts/update_helper.py +39 -0
@@ -183,11 +183,12 @@ function runProjectVerifier(projectKey, config, deps) {
183
183
  const scripts = resolveProjectScripts(projectCwd, manifest);
184
184
  if (!fs.existsSync(scripts.verifier)) return null;
185
185
 
186
- const statePath = path.join(
187
- deps.metameDir || path.join(os.homedir(), '.metame'),
188
- 'memory', 'now', `${projectKey}.md`
189
- );
190
- const phase = readPhaseFromState(statePath);
186
+ const metameDir = deps.metameDir || path.join(os.homedir(), '.metame');
187
+ const statePath = path.join(metameDir, 'memory', 'now', `${projectKey}.md`);
188
+
189
+ // Read phase from event log (SoT), fall back to state file for backward compat
190
+ const { phase: eventPhase } = replayEventLog(projectKey, deps);
191
+ const phase = eventPhase || readPhaseFromState(statePath);
191
192
  const relVerifier = path.relative(projectCwd, scripts.verifier);
192
193
 
193
194
  try {
@@ -468,6 +469,379 @@ function reconcilePerpetualProjects(config, deps) {
468
469
  }
469
470
  }
470
471
 
472
+ // ── Memory System (L1/L2) ───────────────────────────────────────
473
+
474
+ /**
475
+ * Parse event log file into an array of event objects.
476
+ * Single read — callers share the result to avoid redundant I/O.
477
+ *
478
+ * @param {string} projectKey
479
+ * @param {object} deps
480
+ * @returns {Array<object>} Parsed events (malformed lines silently skipped)
481
+ */
482
+ function parseEventLog(projectKey, deps) {
483
+ const metameDir = deps.metameDir || path.join(os.homedir(), '.metame');
484
+ const evDir = path.join(metameDir, 'events');
485
+ const logPath = path.join(evDir, `${projectKey}.jsonl`);
486
+ if (!fs.existsSync(logPath)) return [];
487
+
488
+ const raw = fs.readFileSync(logPath, 'utf8').trim().split('\n').filter(Boolean);
489
+ const events = [];
490
+ for (const line of raw) {
491
+ try { events.push(JSON.parse(line)); } catch { /* skip malformed */ }
492
+ }
493
+ return events;
494
+ }
495
+
496
+ /**
497
+ * Build L1 running memory from parsed events.
498
+ * Extracts key decisions, lessons, phase trail, and round count
499
+ * from the current mission (since last MISSION_START).
500
+ *
501
+ * @param {string} projectKey
502
+ * @param {object} config
503
+ * @param {object} deps
504
+ * @param {Array<object>} [parsedEvents] - Pre-parsed events (avoids re-read)
505
+ * @returns {string} Markdown string (~600-800 tokens)
506
+ */
507
+ function buildRunningMemory(projectKey, config, deps, parsedEvents) {
508
+ const events = parsedEvents || parseEventLog(projectKey, deps);
509
+ if (events.length === 0) return '';
510
+
511
+ // Find last MISSION_START to scope to current mission
512
+ let missionStartIdx = 0;
513
+ for (let i = events.length - 1; i >= 0; i--) {
514
+ if (events[i].type === 'MISSION_START') {
515
+ missionStartIdx = i;
516
+ break;
517
+ }
518
+ }
519
+
520
+ const decisions = [];
521
+ const lessons = [];
522
+ const phaseTrail = [];
523
+ let roundCount = 0;
524
+
525
+ const decisionVerbs = /(?:chose|decided|switched|because|instead|using|adopted|rejected)/i;
526
+
527
+ for (let i = missionStartIdx; i < events.length; i++) {
528
+ const evt = events[i];
529
+
530
+ if (evt.type === 'MEMBER_COMPLETE') roundCount++;
531
+
532
+ if (evt.type === 'DISPATCH' && evt.prompt && evt.prompt.length > 80 && decisionVerbs.test(evt.prompt)) {
533
+ decisions.push({ round: roundCount, text: evt.prompt.slice(0, 150) });
534
+ }
535
+
536
+ if (evt.type === 'PHASE_GATE' && !evt.passed && evt.details) {
537
+ lessons.push({ round: roundCount, text: evt.details.slice(0, 120) });
538
+ }
539
+
540
+ if (evt.type === 'PHASE_GATE' && evt.passed) {
541
+ phaseTrail.push({ phase: evt.phase, round: roundCount });
542
+ }
543
+ }
544
+
545
+ const recentDecisions = decisions.slice(-5);
546
+ const recentLessons = lessons.slice(-5);
547
+
548
+ const parts = [];
549
+
550
+ if (recentDecisions.length > 0) {
551
+ if (parts.length > 0) parts.push('');
552
+ parts.push('## Recent Decisions');
553
+ for (const d of recentDecisions) {
554
+ parts.push(`- [R${d.round}] ${d.text}`);
555
+ }
556
+ }
557
+
558
+ if (recentLessons.length > 0) {
559
+ if (parts.length > 0) parts.push('');
560
+ parts.push('## Lessons Learned');
561
+ for (const l of recentLessons) {
562
+ parts.push(`- [R${l.round}] ${l.text}`);
563
+ }
564
+ }
565
+
566
+ if (phaseTrail.length > 0) {
567
+ if (parts.length > 0) parts.push('');
568
+ parts.push('## Phase Trail');
569
+ parts.push(phaseTrail.map(p => `${p.phase}(R${p.round})`).join(' → '));
570
+ }
571
+
572
+ if (parts.length === 0) return '';
573
+ return parts.join('\n');
574
+ }
575
+
576
+ /**
577
+ * Scan workspace for relevant artifacts (files sorted by mtime).
578
+ *
579
+ * @param {string} projectKey
580
+ * @param {object} config
581
+ * @param {object} deps
582
+ * @returns {Array<{ path: string, desc: string }>} Top 5 artifacts
583
+ */
584
+ function scanRelevantArtifacts(projectKey, config, deps) {
585
+ const projectCwd = resolveProjectCwd(projectKey, config);
586
+ if (!projectCwd) return [];
587
+
588
+ const wsDir = path.join(projectCwd, 'workspace');
589
+ if (!fs.existsSync(wsDir)) return [];
590
+
591
+ const validExts = new Set(['.md', '.json', '.tsv', '.py', '.csv']);
592
+ const files = [];
593
+
594
+ // Walk max depth 2
595
+ try {
596
+ const d1Entries = fs.readdirSync(wsDir, { withFileTypes: true });
597
+ for (const e1 of d1Entries) {
598
+ const p1 = path.join(wsDir, e1.name);
599
+ if (e1.isFile() && validExts.has(path.extname(e1.name))) {
600
+ try { files.push({ abs: p1, rel: `workspace/${e1.name}`, mtime: fs.statSync(p1).mtimeMs }); } catch { /* skip */ }
601
+ } else if (e1.isDirectory()) {
602
+ try {
603
+ const d2Entries = fs.readdirSync(p1, { withFileTypes: true });
604
+ for (const e2 of d2Entries) {
605
+ if (e2.isFile() && validExts.has(path.extname(e2.name))) {
606
+ const p2 = path.join(p1, e2.name);
607
+ try { files.push({ abs: p2, rel: `workspace/${e1.name}/${e2.name}`, mtime: fs.statSync(p2).mtimeMs }); } catch { /* skip */ }
608
+ }
609
+ }
610
+ } catch { /* skip unreadable dirs */ }
611
+ }
612
+ }
613
+ } catch { return []; }
614
+
615
+ // Sort by mtime descending, take top 5
616
+ files.sort((a, b) => b.mtime - a.mtime);
617
+ const top = files.slice(0, 5);
618
+
619
+ // Heuristic descriptions based on path/name
620
+ const descMap = {
621
+ 'progress.tsv': 'phase progress tracker',
622
+ 'results': 'experiment results',
623
+ 'proposal': 'research proposal',
624
+ 'draft': 'paper draft',
625
+ 'notes': 'research notes',
626
+ 'config': 'configuration',
627
+ 'data': 'dataset',
628
+ };
629
+
630
+ return top.map(f => {
631
+ let desc = path.extname(f.rel).slice(1) + ' file';
632
+ for (const [key, label] of Object.entries(descMap)) {
633
+ if (f.rel.toLowerCase().includes(key)) { desc = label; break; }
634
+ }
635
+ return { path: f.rel, desc };
636
+ });
637
+ }
638
+
639
+ /**
640
+ * Build L2 working memory from event log replay + memory.db FTS5.
641
+ *
642
+ * @param {string} projectKey
643
+ * @param {object} config
644
+ * @param {object} deps
645
+ * @returns {string} Markdown string (~300-500 tokens)
646
+ */
647
+ function buildWorkingMemory(projectKey, config, deps) {
648
+ const parts = [];
649
+
650
+ // Phase history as causal chain from event replay
651
+ const { phase, mission, history } = replayEventLog(projectKey, deps);
652
+
653
+ // FTS5 query: mission title + current phase (fixed rule, no smart inference)
654
+ const query = ((mission?.title || '') + ' ' + (phase || '')).trim();
655
+ if (!query) return '';
656
+
657
+ let facts = [];
658
+ try {
659
+ const memory = require('./memory');
660
+ memory.acquire();
661
+ try {
662
+ facts = memory.searchFacts(query, { limit: 5, project: projectKey });
663
+ } finally {
664
+ memory.release();
665
+ }
666
+ } catch { /* memory.db unavailable — graceful degradation */ }
667
+
668
+ if (facts.length > 0) {
669
+ parts.push('## Long-term Context');
670
+ for (const f of facts) {
671
+ const tag = f.relation || f.entity || 'fact';
672
+ parts.push(`- [${tag}] ${f.value}`);
673
+ }
674
+ }
675
+
676
+ if (parts.length === 0) return '';
677
+ return parts.join('\n');
678
+ }
679
+
680
+ /**
681
+ * Persist unified memory file (L1 + L2 merged).
682
+ * L1 rebuilds every round; L2 refreshes every 5 rounds or on phase change.
683
+ *
684
+ * @param {string} projectKey
685
+ * @param {object} config
686
+ * @param {object} deps
687
+ * @param {object} [opts]
688
+ * @param {boolean} [opts.phaseChanged]
689
+ */
690
+ function persistMemoryFiles(projectKey, config, deps, opts = {}) {
691
+ const metameDir = deps.metameDir || path.join(os.homedir(), '.metame');
692
+ const memDir = path.join(metameDir, 'memory', 'now');
693
+ fs.mkdirSync(memDir, { recursive: true });
694
+ const memPath = path.join(memDir, `${projectKey}_memory.md`);
695
+
696
+ // Single parse of event log — shared across L1 and round counting
697
+ const events = parseEventLog(projectKey, deps);
698
+
699
+ // Derive round count and mission title from parsed events
700
+ let roundCount = 0;
701
+ let missionTitle = 'unknown';
702
+ let maxDepth = 50;
703
+ for (const evt of events) {
704
+ if (evt.type === 'MEMBER_COMPLETE') roundCount++;
705
+ if (evt.type === 'MISSION_START') { missionTitle = evt.mission_title || 'unknown'; roundCount = 0; }
706
+ if (evt.type === 'MISSION_COMPLETE') roundCount = 0;
707
+ }
708
+
709
+ // Read manifest for max_depth
710
+ const projectCwd = resolveProjectCwd(projectKey, config);
711
+ if (projectCwd) {
712
+ const manifest = loadProjectManifest(projectCwd);
713
+ if (manifest?.max_depth) maxDepth = manifest.max_depth;
714
+ }
715
+
716
+ // Always rebuild L1 (pass pre-parsed events to avoid re-read)
717
+ const l1 = buildRunningMemory(projectKey, config, deps, events);
718
+ const artifacts = scanRelevantArtifacts(projectKey, config, deps);
719
+
720
+ // Conditionally rebuild L2 (every 5 rounds or phase change)
721
+ const shouldRefreshL2 = opts.phaseChanged || (roundCount % 5 === 0);
722
+ let l2 = '';
723
+ if (shouldRefreshL2) {
724
+ l2 = buildWorkingMemory(projectKey, config, deps);
725
+ // Stash L2 for next time
726
+ try {
727
+ const l2CachePath = path.join(memDir, `${projectKey}_l2cache.md`);
728
+ fs.writeFileSync(l2CachePath, l2, 'utf8');
729
+ } catch { /* non-critical */ }
730
+ } else {
731
+ // Read stale L2 from cache
732
+ try {
733
+ const l2CachePath = path.join(memDir, `${projectKey}_l2cache.md`);
734
+ if (fs.existsSync(l2CachePath)) {
735
+ l2 = fs.readFileSync(l2CachePath, 'utf8').trim();
736
+ }
737
+ } catch { /* non-critical */ }
738
+ }
739
+
740
+ // Build merged document
741
+ const parts = [`# Memory Context: ${missionTitle} (round ${roundCount}/${maxDepth})`];
742
+
743
+ if (l1) parts.push('', l1);
744
+
745
+ if (artifacts.length > 0) {
746
+ parts.push('', '## Current Artifacts');
747
+ for (const a of artifacts) {
748
+ parts.push(`- ${a.path} — ${a.desc}`);
749
+ }
750
+ }
751
+
752
+ if (l2) parts.push('', l2);
753
+
754
+ const content = parts.join('\n') + '\n';
755
+ fs.writeFileSync(memPath, content, 'utf8');
756
+ return memPath;
757
+ }
758
+
759
+ /**
760
+ * Pattern-based inline fact extraction from agent output.
761
+ * Zero LLM, zero agent format dependency.
762
+ *
763
+ * @param {string} projectKey
764
+ * @param {string} memberOutput
765
+ * @param {string} [phase]
766
+ * @returns {Array<{ entity: string, relation: string, value: string, confidence: string }>}
767
+ */
768
+ function extractInlineFacts(projectKey, memberOutput, phase) {
769
+ if (!memberOutput || typeof memberOutput !== 'string') return [];
770
+
771
+ const facts = [];
772
+ const CAP = 3;
773
+
774
+ // Pattern 1: Error/OOM patterns → bug_lesson
775
+ const errorRe = /(?:OOM|out of memory|CUDA error|killed|Error:|Exception:|Failed:)\s*(.{15,150})/gi;
776
+ let match;
777
+ while ((match = errorRe.exec(memberOutput)) !== null && facts.length < CAP) {
778
+ facts.push({
779
+ entity: projectKey,
780
+ relation: 'bug_lesson',
781
+ value: match[0].trim().slice(0, 150),
782
+ confidence: 'medium',
783
+ });
784
+ }
785
+
786
+ // Pattern 2: Decision verbs → tech_decision
787
+ const decisionRe = /(?:decided|chose|selected|switched to|rejected|using|adopted)\s+(.{20,150})/gi;
788
+ while ((match = decisionRe.exec(memberOutput)) !== null && facts.length < CAP) {
789
+ facts.push({
790
+ entity: projectKey,
791
+ relation: 'tech_decision',
792
+ value: match[0].trim().slice(0, 150),
793
+ confidence: 'low',
794
+ });
795
+ }
796
+
797
+ return facts.slice(0, CAP);
798
+ }
799
+
800
+ /**
801
+ * Extract a high-density summary from agent output.
802
+ * Tail-biased: conclusions and results are usually at the end.
803
+ * Zero LLM — pure heuristic.
804
+ *
805
+ * Strategy:
806
+ * - Head (~200 chars): who's speaking, opening context
807
+ * - Key lines: lines containing signal words (conclusions, decisions, errors)
808
+ * - Tail (~600 chars): final output, conclusions, recommendations
809
+ *
810
+ * @param {string} output - Raw agent output
811
+ * @param {number} [maxLen=1200] - Max total length
812
+ * @returns {string}
813
+ */
814
+ function extractOutputSummary(output, maxLen = 1200) {
815
+ if (!output || output.length <= maxLen) return output || '';
816
+
817
+ // Adaptive head/tail sizes — scale down for small maxLen
818
+ const HEAD_LEN = Math.min(200, Math.floor(maxLen * 0.25));
819
+ const TAIL_LEN = Math.min(600, Math.floor(maxLen * 0.6));
820
+ const KEY_BUDGET = Math.max(0, maxLen - HEAD_LEN - TAIL_LEN - 40);
821
+
822
+ const head = output.slice(0, HEAD_LEN);
823
+ const tail = output.slice(-TAIL_LEN);
824
+
825
+ // Extract key signal lines from the middle (skip head/tail zones)
826
+ let keyLines = '';
827
+ if (KEY_BUDGET > 0 && output.length > HEAD_LEN + TAIL_LEN) {
828
+ const middleZone = output.slice(HEAD_LEN, -TAIL_LEN);
829
+ const signalRe = /(?:结论|conclusion|found that|result|决定|recommend|建议|发现|关键|key finding|error|OOM|failed|chose|decided|switched|important|注意|warning)/i;
830
+ keyLines = middleZone.split('\n')
831
+ .filter(line => line.trim().length > 15 && signalRe.test(line))
832
+ .slice(0, 5)
833
+ .join('\n')
834
+ .slice(0, KEY_BUDGET);
835
+ }
836
+
837
+ const parts = [head.trimEnd()];
838
+ if (keyLines) parts.push('[...key findings...]', keyLines);
839
+ else parts.push('[...]');
840
+ parts.push(tail.trimStart());
841
+
842
+ return parts.join('\n').slice(0, maxLen);
843
+ }
844
+
471
845
  // ── Main handler ────────────────────────────────────────────────
472
846
 
473
847
  /**
@@ -601,6 +975,10 @@ function handleReactiveOutput(targetProject, output, config, deps) {
601
975
  new_session: true,
602
976
  }, config);
603
977
  }
978
+
979
+ // Point B: Persist memory after parent dispatches
980
+ try { persistMemoryFiles(projectKey, config, deps); } catch { /* non-critical */ }
981
+
604
982
  return;
605
983
  }
606
984
 
@@ -684,13 +1062,31 @@ function handleReactiveOutput(targetProject, output, config, deps) {
684
1062
  try { generateStateFile(parentKey, config, deps); } catch { /* non-critical */ }
685
1063
  }
686
1064
 
687
- // Trigger parent with member's output summary
1065
+ // Point A: Persist memory + extract inline facts after verifier, before waking parent
1066
+ const phaseChanged = verifyResult?.passed && !!verifyResult?.phase;
1067
+ try { persistMemoryFiles(parentKey, config, deps, { phaseChanged }); } catch { /* non-critical */ }
1068
+
1069
+ // Inline fact extraction from member output
1070
+ try {
1071
+ const inlineFacts = extractInlineFacts(parentKey, output, verifyResult?.phase);
1072
+ if (inlineFacts.length > 0) {
1073
+ const memory = require('./memory');
1074
+ memory.acquire();
1075
+ try {
1076
+ memory.saveFacts(`reactive-${parentKey}-${Date.now()}`, parentKey, inlineFacts);
1077
+ } finally {
1078
+ memory.release();
1079
+ }
1080
+ }
1081
+ } catch { /* non-critical */ }
1082
+
1083
+ // Trigger parent with member's output summary (tail-biased extraction)
688
1084
  const parentManifest = parentCwd ? loadProjectManifest(parentCwd) : null;
689
1085
  const signal = parentManifest?.completion_signal || 'MISSION_COMPLETE';
690
- const summary = output.slice(0, 1200);
1086
+ const summary = extractOutputSummary(output);
691
1087
  deps.handleDispatchItem({
692
1088
  target: parentKey,
693
- prompt: `[Team delivery] ${targetProject} completed task.\n\nOutput summary:\n${summary}${verifierBlock}\n\nEvaluate quality and decide next step.\nTo dispatch tasks, use NEXT_DISPATCH.\nWhen all tasks are done, output ${signal}.`,
1089
+ prompt: `[${targetProject} delivery]${verifierBlock}\n\n${summary}\n\nDecide next step. Use NEXT_DISPATCH or ${signal}.`,
694
1090
  from: targetProject,
695
1091
  _reactive: true,
696
1092
  new_session: true,
@@ -702,5 +1098,5 @@ module.exports = {
702
1098
  parseReactiveSignals,
703
1099
  reconcilePerpetualProjects,
704
1100
  replayEventLog,
705
- __test: { runProjectVerifier, readPhaseFromState, resolveProjectCwd, appendEvent, projectProgressTsv, generateStateFile, loadProjectManifest, resolveProjectScripts },
1101
+ __test: { runProjectVerifier, readPhaseFromState, resolveProjectCwd, appendEvent, projectProgressTsv, generateStateFile, loadProjectManifest, resolveProjectScripts, parseEventLog, buildRunningMemory, scanRelevantArtifacts, buildWorkingMemory, persistMemoryFiles, extractInlineFacts, extractOutputSummary },
706
1102
  };
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { normalizeEngineName: _normalizeEngine } = require('./daemon-utils');
4
+
3
5
  function createSessionCommandHandler(deps) {
4
6
  const {
5
7
  fs,
@@ -29,8 +31,7 @@ function createSessionCommandHandler(deps) {
29
31
  } = deps;
30
32
 
31
33
  function normalizeEngineName(name) {
32
- const n = String(name || '').trim().toLowerCase();
33
- return n === 'codex' ? 'codex' : getDefaultEngine();
34
+ return _normalizeEngine(name, getDefaultEngine);
34
35
  }
35
36
 
36
37
  function inferStoredEngine(rawSession) {
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const crypto = require('crypto');
4
+ const { normalizeEngineName } = require('./daemon-utils');
4
5
 
5
6
  function normalizeCodexSandboxMode(value, fallback = null) {
6
7
  const text = String(value || '').trim().toLowerCase();
@@ -45,10 +46,6 @@ function normalizeCodexPermissionMeta(meta = {}) {
45
46
  };
46
47
  }
47
48
 
48
- function normalizeEngineName(name) {
49
- return String(name || 'claude').trim().toLowerCase() === 'codex' ? 'codex' : 'claude';
50
- }
51
-
52
49
  function stripCodexInjectedHints(text) {
53
50
  return String(text || '')
54
51
  .replace(/\r\n/g, '\n')
@@ -217,17 +214,36 @@ function createSessionStore(deps) {
217
214
  }
218
215
 
219
216
  // [M3] 共享辅助:从 reversed JSONL 行数组中提取最后一条外部用户消息(统一规则)
217
+ function _extractMessageText(d) {
218
+ const content = d.message && d.message.content;
219
+ let raw = typeof content === 'string' ? content
220
+ : Array.isArray(content) ? (content.find(c => c.type === 'text') || {}).text || '' : '';
221
+ raw = raw.replace(/\[System hints[\s\S]*/i, '')
222
+ .replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '').trim();
223
+ return raw;
224
+ }
225
+
220
226
  function extractLastUserFromLines(lines) {
221
227
  for (const line of lines) {
222
228
  if (!line) continue;
223
229
  try {
224
230
  const d = JSON.parse(line);
225
231
  if (d.type === 'user' && d.message && d.userType !== 'internal') {
226
- const content = d.message.content;
227
- let raw = typeof content === 'string' ? content
228
- : Array.isArray(content) ? (content.find(c => c.type === 'text') || {}).text || '' : '';
229
- raw = raw.replace(/\[System hints[\s\S]*/i, '')
230
- .replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '').trim();
232
+ const raw = _extractMessageText(d);
233
+ if (raw.length > 2) return raw.slice(0, 80);
234
+ }
235
+ } catch { /* skip */ }
236
+ }
237
+ return '';
238
+ }
239
+
240
+ function extractLastAssistantFromLines(lines) {
241
+ for (const line of lines) {
242
+ if (!line) continue;
243
+ try {
244
+ const d = JSON.parse(line);
245
+ if (d.type === 'assistant' && d.message) {
246
+ const raw = _extractMessageText(d);
231
247
  if (raw.length > 2) return raw.slice(0, 80);
232
248
  }
233
249
  } catch { /* skip */ }
@@ -256,11 +272,30 @@ function createSessionStore(deps) {
256
272
  }
257
273
  }
258
274
  }
259
- // Fallback: decode projectPath from directory name (e.g. -Users-yaron-AGI-AChat → /Users/yaron/AGI/AChat)
275
+ // Fallback: decode projectPath from directory name (macOS: -Users-foo → /Users/foo)
260
276
  if (!projPathCache.has(proj) && proj.startsWith('-')) {
261
277
  const decoded = proj.replace(/-/g, '/');
262
278
  if (fs.existsSync(decoded)) projPathCache.set(proj, decoded);
263
279
  }
280
+ // Fallback 2: read cwd from first JSONL entry (works on all platforms,
281
+ // handles directory names that can't be reliably decoded, e.g. Windows paths
282
+ // with drive letters or filenames containing hyphens)
283
+ if (!projPathCache.has(proj)) {
284
+ try {
285
+ const _jsonls = fs.readdirSync(projDir).filter(f => f.endsWith('.jsonl'));
286
+ if (_jsonls.length > 0) {
287
+ const _fd = fs.openSync(path.join(projDir, _jsonls[0]), 'r');
288
+ try {
289
+ const _buf = Buffer.alloc(4096);
290
+ const _bytes = fs.readSync(_fd, _buf, 0, 4096, 0);
291
+ for (const _line of _buf.toString('utf8', 0, _bytes).split('\n')) {
292
+ if (!_line) continue;
293
+ try { const _d = JSON.parse(_line); if (_d.cwd) { projPathCache.set(proj, path.resolve(_d.cwd)); break; } } catch {}
294
+ }
295
+ } finally { fs.closeSync(_fd); }
296
+ }
297
+ } catch {}
298
+ }
264
299
  } catch { /* skip */ }
265
300
 
266
301
  try {
@@ -287,6 +322,8 @@ function createSessionStore(deps) {
287
322
  }
288
323
 
289
324
  const all = Array.from(sessionMap.values()).map((entry) => ({ ...entry, engine: 'claude' }));
325
+ // Sort by recency BEFORE enrichment so we enrich the most recent sessions first
326
+ all.sort((a, b) => (b.fileMtime || 0) - (a.fileMtime || 0));
290
327
  const ENRICH_LIMIT = 20;
291
328
  for (let i = 0; i < Math.min(all.length, ENRICH_LIMIT); i++) {
292
329
  const s = all[i];
@@ -338,6 +375,9 @@ function createSessionStore(deps) {
338
375
  if (!s.lastUser) {
339
376
  s.lastUser = extractLastUserFromLines(tailLines);
340
377
  }
378
+ if (!s.lastAssistant) {
379
+ s.lastAssistant = extractLastAssistantFromLines(tailLines);
380
+ }
341
381
  } finally {
342
382
  fs.closeSync(fd);
343
383
  }
@@ -613,6 +653,13 @@ function createSessionStore(deps) {
613
653
  return s.sessionId ? s.sessionId.slice(0, 8) : '';
614
654
  }
615
655
 
656
+ // ── Display helpers (shared by sessionRichLabel & buildSessionCardElements) ──
657
+ const _escapeMd = (t) => t.replace(/[_*`\\]/g, '\\$&');
658
+ function _cleanSnippet(raw, maxLen) {
659
+ if (!raw || raw.length <= 2) return '';
660
+ return _escapeMd(raw.replace(/\n/g, ' ').slice(0, maxLen)) + (raw.length > maxLen ? '…' : '');
661
+ }
662
+
616
663
  function sessionRichLabel(s, index, sessionTags) {
617
664
  sessionTags = sessionTags || loadSessionTags();
618
665
  const title = sessionDisplayTitle(s, 50, sessionTags);
@@ -622,17 +669,15 @@ function createSessionStore(deps) {
622
669
  const tags = (sessionTags[s.sessionId] && sessionTags[s.sessionId].tags || []).slice(0, 3);
623
670
  const engineLabel = (s.engine || 'claude') === 'codex' ? 'codex' : 'claude';
624
671
 
625
- // [M2] 转义 markdown 特殊字符,防止用户历史消息破坏渲染
626
- const escapeMd = (t) => t.replace(/[_*`\\]/g, '\\$&');
627
- // fallback to firstPrompt when lastUser not found in tail
628
- const snippetRaw = s.lastUser || (s.firstPrompt || '').replace(/<[^>]+>/g, '').replace(/\[System hints[\s\S]*/i, '').trim().slice(0, 80);
629
- let line = `${index}. ${title}${title.length >= 50 ? '..' : ''}`; // [M4] title 已有 sessionId 兜底,不会为空
672
+ let line = `${index}. ${title}${title.length >= 50 ? '..' : ''}`;
630
673
  if (tags.length) line += ` ${tags.map(t => `#${t}`).join(' ')}`;
631
674
  line += `\n 📁${proj} · ${ago} · ${engineLabel}`;
632
- if (snippetRaw && snippetRaw.length > 2) {
633
- const snippet = escapeMd(snippetRaw.replace(/\n/g, ' ').slice(0, 60));
634
- line += `\n 💬 ${snippet}${snippetRaw.length > 60 ? '…' : ''}`;
635
- }
675
+ const firstSnippet = _cleanSnippet(s.firstPrompt, 50);
676
+ const lastUserSnippet = _cleanSnippet(s.lastUser, 50);
677
+ const lastAiSnippet = _cleanSnippet(s.lastAssistant, 50);
678
+ if (firstSnippet) line += `\n 📝 ${firstSnippet}`;
679
+ if (lastUserSnippet && lastUserSnippet !== firstSnippet) line += `\n 💬 ${lastUserSnippet}`;
680
+ if (lastAiSnippet) line += `\n 🤖 ${lastAiSnippet}`;
636
681
  line += `\n /resume ${shortId}`;
637
682
  return line;
638
683
  }
@@ -648,15 +693,16 @@ function createSessionStore(deps) {
648
693
  const shortId = s.sessionId.slice(0, 6);
649
694
  const tags = (sessionTags[s.sessionId] && sessionTags[s.sessionId].tags || []).slice(0, 4);
650
695
  const engineLabel = (s.engine || 'claude') === 'codex' ? 'codex' : 'claude';
651
- // [M2] 转义 markdown 特殊字符;[M4] title 已有 sessionId 兜底
652
- const escapeMd = (t) => t.replace(/[_*`\\]/g, '\\$&');
653
- const snippetRaw = s.lastUser || (s.firstPrompt || '').replace(/<[^>]+>/g, '').replace(/\[System hints[\s\S]*/i, '').trim().slice(0, 80);
696
+
654
697
  let desc = `**${i + 1}. ${title}**\n📁${proj} · ${ago} · ${engineLabel}`;
655
698
  if (tags.length) desc += `\n${tags.map(t => `\`${t}\``).join(' ')}`;
656
- if (snippetRaw && snippetRaw.length > 2) {
657
- const snippet = escapeMd(snippetRaw.replace(/\n/g, ' ').slice(0, 60));
658
- desc += `\n💬 ${snippet}${snippetRaw.length > 60 ? '…' : ''}`;
659
- }
699
+ // Show first prompt, last user message, and last assistant reply
700
+ const firstSnippet = _cleanSnippet(s.firstPrompt, 50);
701
+ const lastUserSnippet = _cleanSnippet(s.lastUser, 50);
702
+ const lastAiSnippet = _cleanSnippet(s.lastAssistant, 50);
703
+ if (firstSnippet) desc += `\n📝 ${firstSnippet}`;
704
+ if (lastUserSnippet && lastUserSnippet !== firstSnippet) desc += `\n💬 ${lastUserSnippet}`;
705
+ if (lastAiSnippet) desc += `\n🤖 ${lastAiSnippet}`;
660
706
  elements.push({ tag: 'div', text: { tag: 'lark_md', content: desc } });
661
707
  elements.push({ tag: 'action', actions: [{ tag: 'button', text: { tag: 'plain_text', content: `▶️ Switch #${shortId}` }, type: 'primary', value: { cmd: `/resume ${s.sessionId}` } }] });
662
708
  });
@@ -924,6 +970,9 @@ function createSessionStore(deps) {
924
970
  }
925
971
  slot.started = true;
926
972
  s.last_active = Date.now();
973
+ // Clear stale findSessionFile cache: the JSONL/SQLite file now exists
974
+ // but may have been cached as null during createSession (before CLI created it).
975
+ if (slot.id) clearSessionFileCache(slot.id);
927
976
  } else {
928
977
  s.started = true; // old flat format
929
978
  s.last_active = Date.now();
@@ -970,7 +1019,13 @@ function createSessionStore(deps) {
970
1019
  // Best approach: read cwd directly from session file content (not from dir name)
971
1020
  function _isClaudeSessionValid(sessionId, normCwd) {
972
1021
  try {
973
- const sessionFile = findSessionFile(sessionId);
1022
+ let sessionFile = findSessionFile(sessionId);
1023
+ if (!sessionFile) {
1024
+ // Cache may hold a stale null from createSession (before CLI wrote the JSONL).
1025
+ // Clear and retry once to avoid false invalidation.
1026
+ clearSessionFileCache(sessionId);
1027
+ sessionFile = findSessionFile(sessionId);
1028
+ }
974
1029
  if (!sessionFile) {
975
1030
  log('WARN', `[SessionValid] ${sessionId.slice(0, 8)}: JSONL file not found`);
976
1031
  return false;