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.
- package/index.js +64 -7
- package/package.json +3 -2
- package/scripts/daemon-agent-commands.js +6 -2
- package/scripts/daemon-bridges.js +23 -9
- package/scripts/daemon-claude-engine.js +87 -28
- package/scripts/daemon-command-router.js +16 -0
- package/scripts/daemon-command-session-route.js +3 -1
- package/scripts/daemon-engine-runtime.js +1 -5
- package/scripts/daemon-message-pipeline.js +113 -44
- package/scripts/daemon-reactive-lifecycle.js +405 -9
- package/scripts/daemon-session-commands.js +3 -2
- package/scripts/daemon-session-store.js +82 -27
- package/scripts/daemon-team-dispatch.js +21 -5
- package/scripts/daemon-utils.js +3 -1
- package/scripts/daemon.js +1 -0
- package/scripts/docs/file-transfer.md +1 -0
- package/scripts/hooks/intent-file-transfer.js +2 -1
- package/scripts/hooks/intent-perpetual.js +109 -0
- package/scripts/hooks/intent-research.js +112 -0
- package/scripts/intent-registry.js +4 -0
- package/scripts/ops-mission-queue.js +258 -0
- package/scripts/ops-verifier.js +197 -0
- package/skills/agent-browser/SKILL.md +153 -0
- package/skills/agent-reach/SKILL.md +66 -0
- package/skills/agent-reach/evolution.json +13 -0
- package/skills/deep-research/SKILL.md +77 -0
- package/skills/find-skills/SKILL.md +133 -0
- package/skills/heartbeat-task-manager/SKILL.md +63 -0
- package/skills/macos-local-orchestrator/SKILL.md +192 -0
- package/skills/macos-local-orchestrator/agents/openai.yaml +4 -0
- package/skills/macos-local-orchestrator/references/tooling-landscape.md +70 -0
- package/skills/macos-mail-calendar/SKILL.md +394 -0
- package/skills/mcp-installer/SKILL.md +138 -0
- package/skills/skill-creator/LICENSE.txt +202 -0
- package/skills/skill-creator/README.md +72 -0
- package/skills/skill-creator/SKILL.md +96 -0
- package/skills/skill-creator/evolution.json +6 -0
- package/skills/skill-creator/references/creation-guide.md +116 -0
- package/skills/skill-creator/references/evolution-guide.md +74 -0
- package/skills/skill-creator/references/output-patterns.md +82 -0
- package/skills/skill-creator/references/workflows.md +28 -0
- package/skills/skill-creator/scripts/align_all.py +32 -0
- package/skills/skill-creator/scripts/auto_evolve_hook.js +247 -0
- package/skills/skill-creator/scripts/init_skill.py +303 -0
- package/skills/skill-creator/scripts/merge_evolution.py +70 -0
- package/skills/skill-creator/scripts/package_skill.py +110 -0
- package/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/skills/skill-creator/scripts/setup.py +141 -0
- package/skills/skill-creator/scripts/smart_stitch.py +82 -0
- package/skills/skill-manager/SKILL.md +112 -0
- package/skills/skill-manager/scripts/delete_skill.py +31 -0
- package/skills/skill-manager/scripts/list_skills.py +61 -0
- package/skills/skill-manager/scripts/scan_and_check.py +125 -0
- package/skills/skill-manager/scripts/sync_index.py +144 -0
- 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
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
)
|
|
190
|
-
const phase =
|
|
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
|
-
//
|
|
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
|
|
1086
|
+
const summary = extractOutputSummary(output);
|
|
691
1087
|
deps.handleDispatchItem({
|
|
692
1088
|
target: parentKey,
|
|
693
|
-
prompt: `[
|
|
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
|
-
|
|
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
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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
|
-
|
|
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
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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
|
-
|
|
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;
|