metame-cli 1.5.17 → 1.5.19
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/README.md +7 -13
- package/package.json +1 -1
- package/scripts/daemon-claude-engine.js +23 -25
- package/scripts/daemon-command-router.js +4 -4
- package/scripts/daemon-engine-runtime.js +27 -2
- package/scripts/daemon-message-pipeline.js +2 -2
- package/scripts/daemon-reactive-lifecycle.js +35 -1
- package/scripts/daemon-task-scheduler.js +0 -3
- package/scripts/daemon.js +16 -63
- package/scripts/docs/maintenance-manual.md +6 -0
- package/scripts/docs/orphan-files-review.md +1 -2
- package/scripts/docs/pointer-map.md +1 -0
- package/scripts/session-summarize.js +0 -118
package/README.md
CHANGED
|
@@ -79,21 +79,16 @@ Start on your laptop, continue on the train. `/stop` to interrupt, `/undo` to ro
|
|
|
79
79
|
|
|
80
80
|
### 3. Layered Memory That Works While You Sleep
|
|
81
81
|
|
|
82
|
-
MetaMe's memory system runs automatically in the background — no prompts, no manual saves.
|
|
82
|
+
MetaMe's memory system runs automatically in the background — no prompts, no manual saves. Three layers, fully autonomous.
|
|
83
83
|
|
|
84
84
|
**Layer 1 — Long-term Facts**
|
|
85
85
|
When you go idle, MetaMe runs memory consolidation: extracts key decisions, patterns, and knowledge from your sessions into a persistent facts store. Facts can also carry concept labels (`fact_labels`) for faster cross-domain retrieval.
|
|
86
86
|
|
|
87
|
-
**Layer 2 —
|
|
88
|
-
|
|
87
|
+
**Layer 2 — History Retrieval**
|
|
88
|
+
When you refer to "that thing we worked on last week", MetaMe uses session routing, topic tags, and memory recall to find the right thread, files, and facts. Same-session continuation stays native — no synthetic resume summary is injected.
|
|
89
89
|
|
|
90
|
-
**Layer 3 —
|
|
91
|
-
Every session gets tagged with topics and intent. This powers future session routing: when you reference "that thing we worked on last week", MetaMe knows where to look.
|
|
92
|
-
|
|
93
|
-
**Layer 4 — Nightly Reflection**
|
|
90
|
+
**Layer 3 — Higher-order Memory**
|
|
94
91
|
Every night at 01:00, MetaMe reviews your most-accessed facts from the past week and distills them into high-level decision logs and operational lessons. Distilled outputs are also written back to `memory.db` as `synthesized_insight`, enabling retrieval in future sessions.
|
|
95
|
-
|
|
96
|
-
**Layer 5 — Memory Index**
|
|
97
92
|
At 01:30, an auto-generated global index (`INDEX.md`) maps every memory document across all categories (including capsules and postmortems). This serves as a fast lookup table so MetaMe always knows where to find relevant context.
|
|
98
93
|
|
|
99
94
|
```
|
|
@@ -101,14 +96,13 @@ At 01:30, an auto-generated global index (`INDEX.md`) maps every memory document
|
|
|
101
96
|
idle 30min → memory consolidation triggered
|
|
102
97
|
→ session_tags.json updated (topics indexed)
|
|
103
98
|
→ facts extracted → ~/.metame/memory.db
|
|
104
|
-
→ session summary cached → daemon_state.json
|
|
105
99
|
01:00 → nightly reflection: hot facts → decisions + lessons
|
|
106
100
|
01:30 → memory index regenerated
|
|
107
101
|
|
|
108
102
|
[Next morning, when you resume]
|
|
109
103
|
"continue from yesterday" →
|
|
110
|
-
|
|
111
|
-
|
|
104
|
+
session lookup + memory recall point to the right thread/files
|
|
105
|
+
without auto-injecting a synthetic conversation summary.
|
|
112
106
|
```
|
|
113
107
|
|
|
114
108
|
### 4. Heartbeat — A Programmable Nervous System
|
|
@@ -121,7 +115,7 @@ The heartbeat system is three-layered:
|
|
|
121
115
|
Built into the daemon. Runs every 60 seconds regardless of what's in your config:
|
|
122
116
|
- Drains the dispatch queue (IPC messages from other agents)
|
|
123
117
|
- Tracks daemon aliveness and rotates logs
|
|
124
|
-
- Detects when you go idle
|
|
118
|
+
- Detects when you go idle and enters sleep mode for gated background work
|
|
125
119
|
|
|
126
120
|
**Layer 1 — System Evolution (built-in defaults)**
|
|
127
121
|
Five tasks shipped out of the box. They are precondition-gated and run only when useful:
|
package/package.json
CHANGED
|
@@ -6,8 +6,8 @@ const {
|
|
|
6
6
|
createEngineRuntimeFactory,
|
|
7
7
|
normalizeEngineName,
|
|
8
8
|
resolveEngineModel,
|
|
9
|
+
_private: { resolveCodexPermissionProfile, resolveEngineTimeouts },
|
|
9
10
|
ENGINE_MODEL_CONFIG,
|
|
10
|
-
_private: { resolveCodexPermissionProfile },
|
|
11
11
|
} = require('./daemon-engine-runtime');
|
|
12
12
|
const { buildIntentHintBlock } = require('./intent-registry');
|
|
13
13
|
const { buildAgentContextForEngine, buildMemorySnapshotContent, refreshMemorySnapshot } = require('./agent-layer');
|
|
@@ -835,12 +835,14 @@ function createClaudeEngine(deps) {
|
|
|
835
835
|
|
|
836
836
|
if (chatId) {
|
|
837
837
|
activeProcesses.set(chatId, {
|
|
838
|
+
chatId,
|
|
838
839
|
child,
|
|
839
840
|
aborted: false,
|
|
840
841
|
abortReason: null,
|
|
841
842
|
startedAt: _spawnAt,
|
|
842
843
|
engine: rt.name,
|
|
843
844
|
killSignal: rt.killSignal || 'SIGTERM',
|
|
845
|
+
reactiveProjectKey: String(options && options.reactiveProjectKey || '').trim(),
|
|
844
846
|
});
|
|
845
847
|
saveActivePids();
|
|
846
848
|
}
|
|
@@ -871,10 +873,10 @@ function createClaudeEngine(deps) {
|
|
|
871
873
|
const toolUsageLog = [];
|
|
872
874
|
|
|
873
875
|
void timeoutMs;
|
|
874
|
-
const engineTimeouts = rt.timeouts || {};
|
|
876
|
+
const engineTimeouts = options.timeouts || rt.timeouts || {};
|
|
875
877
|
const IDLE_TIMEOUT_MS = engineTimeouts.idleMs || (5 * 60 * 1000);
|
|
876
878
|
const TOOL_EXEC_TIMEOUT_MS = engineTimeouts.toolMs || (25 * 60 * 1000);
|
|
877
|
-
const HARD_CEILING_MS = engineTimeouts.ceilingMs
|
|
879
|
+
const HARD_CEILING_MS = Number.isFinite(engineTimeouts.ceilingMs) ? engineTimeouts.ceilingMs : null;
|
|
878
880
|
const startTime = Date.now();
|
|
879
881
|
let waitingForTool = false;
|
|
880
882
|
|
|
@@ -892,7 +894,9 @@ function createClaudeEngine(deps) {
|
|
|
892
894
|
}
|
|
893
895
|
|
|
894
896
|
let idleTimer = setTimeout(() => killChild('idle'), IDLE_TIMEOUT_MS);
|
|
895
|
-
const ceilingTimer =
|
|
897
|
+
const ceilingTimer = HARD_CEILING_MS && HARD_CEILING_MS > 0
|
|
898
|
+
? setTimeout(() => killChild('ceiling'), HARD_CEILING_MS)
|
|
899
|
+
: null;
|
|
896
900
|
|
|
897
901
|
function resetIdleTimer() {
|
|
898
902
|
clearTimeout(idleTimer);
|
|
@@ -1248,7 +1252,7 @@ function createClaudeEngine(deps) {
|
|
|
1248
1252
|
return loadConfig();
|
|
1249
1253
|
}
|
|
1250
1254
|
|
|
1251
|
-
async function askClaude(bot, chatId, prompt, config, readOnly = false, senderId = null) {
|
|
1255
|
+
async function askClaude(bot, chatId, prompt, config, readOnly = false, senderId = null, meta = {}) {
|
|
1252
1256
|
const _t0 = Date.now();
|
|
1253
1257
|
log('INFO', `askClaude for ${chatId}: ${prompt.slice(0, 50)}`);
|
|
1254
1258
|
|
|
@@ -1261,12 +1265,14 @@ function createClaudeEngine(deps) {
|
|
|
1261
1265
|
try { process.kill(-_existing.child.pid, 'SIGTERM'); } catch { try { _existing.child.kill('SIGTERM'); } catch { /* */ } }
|
|
1262
1266
|
}
|
|
1263
1267
|
activeProcesses.set(chatId, {
|
|
1268
|
+
chatId,
|
|
1264
1269
|
child: null, // sentinel: no process yet
|
|
1265
1270
|
aborted: false,
|
|
1266
1271
|
abortReason: null,
|
|
1267
1272
|
startedAt: _t0,
|
|
1268
1273
|
engine: 'pending',
|
|
1269
1274
|
killSignal: 'SIGTERM',
|
|
1275
|
+
reactiveProjectKey: String(meta && meta.reactiveProjectKey || '').trim(),
|
|
1270
1276
|
});
|
|
1271
1277
|
|
|
1272
1278
|
// Track interaction time for idle/sleep detection
|
|
@@ -1391,6 +1397,7 @@ function createClaudeEngine(deps) {
|
|
|
1391
1397
|
(boundProject && boundProject.engine) || getDefaultEngine()
|
|
1392
1398
|
);
|
|
1393
1399
|
const runtime = getEngineRuntime(engineName);
|
|
1400
|
+
const executionTimeouts = resolveEngineTimeouts(engineName, { reactive: !!(meta && meta.reactive) });
|
|
1394
1401
|
const requestedCodexPermissionProfile = engineName === 'codex'
|
|
1395
1402
|
? getCodexPermissionProfile(readOnly, daemonCfg)
|
|
1396
1403
|
: null;
|
|
@@ -1752,25 +1759,6 @@ ${mentorRadarHint}
|
|
|
1752
1759
|
6. Before executing high-risk or non-obvious Bash commands (rm, kill, git reset, overwrite configs), prepend a single-line [Why] explanation. Skip for routine commands (ls, cat, grep).]`;
|
|
1753
1760
|
}
|
|
1754
1761
|
|
|
1755
|
-
// P2-B: inject session summary when resuming after a 2h+ gap
|
|
1756
|
-
let summaryHint = '';
|
|
1757
|
-
if (session.started) {
|
|
1758
|
-
try {
|
|
1759
|
-
const _stSum = loadState();
|
|
1760
|
-
const _sess = _stSum.sessions && _stSum.sessions[chatId];
|
|
1761
|
-
if (_sess && _sess.last_summary && _sess.last_summary_at) {
|
|
1762
|
-
const _idleMs = Date.now() - (_sess.last_active || 0);
|
|
1763
|
-
const _summaryAgeH = (Date.now() - _sess.last_summary_at) / 3600000;
|
|
1764
|
-
if (_idleMs > 2 * 60 * 60 * 1000 && _summaryAgeH < 168) {
|
|
1765
|
-
summaryHint = `
|
|
1766
|
-
|
|
1767
|
-
[上次对话摘要(历史已完成,仅供上下文,请勿重复执行)]: ${_sess.last_summary}`;
|
|
1768
|
-
log('INFO', `[DAEMON] Injected session summary for ${chatId} (idle ${Math.round(_idleMs / 3600000)}h)`);
|
|
1769
|
-
}
|
|
1770
|
-
}
|
|
1771
|
-
} catch { /* non-critical */ }
|
|
1772
|
-
}
|
|
1773
|
-
|
|
1774
1762
|
// Mentor context hook: inject after memoryHint, before langGuard.
|
|
1775
1763
|
let mentorHint = '';
|
|
1776
1764
|
if (mentorEnabled && !mentorExcluded && !mentorSuppressed) {
|
|
@@ -1842,7 +1830,7 @@ ${mentorRadarHint}
|
|
|
1842
1830
|
// (varies per prompt), so include it even on warm reuse.
|
|
1843
1831
|
const fullPrompt = _warmEntry
|
|
1844
1832
|
? routedPrompt + intentHint
|
|
1845
|
-
: routedPrompt + daemonHint + intentHint + agentHint + macAutomationHint +
|
|
1833
|
+
: routedPrompt + daemonHint + intentHint + agentHint + macAutomationHint + memoryHint + mentorHint + langGuard;
|
|
1846
1834
|
if (runtime.name === 'codex' && session.started && session.id && requestedCodexPermissionProfile) {
|
|
1847
1835
|
const actualPermissionProfile = getActualCodexPermissionProfile(session);
|
|
1848
1836
|
if (codexNeedsFallbackForRequestedPermissions(actualPermissionProfile, requestedCodexPermissionProfile)) {
|
|
@@ -2040,6 +2028,8 @@ ${mentorRadarHint}
|
|
|
2040
2028
|
persistent: runtime.name === 'claude' && !!warmPool,
|
|
2041
2029
|
warmPool,
|
|
2042
2030
|
warmSessionKey: _warmSessionKey,
|
|
2031
|
+
reactiveProjectKey: String(meta && meta.reactiveProjectKey || '').trim(),
|
|
2032
|
+
timeouts: executionTimeouts,
|
|
2043
2033
|
},
|
|
2044
2034
|
));
|
|
2045
2035
|
|
|
@@ -2104,6 +2094,10 @@ ${mentorRadarHint}
|
|
|
2104
2094
|
normalizeSenderId(senderId),
|
|
2105
2095
|
runtime,
|
|
2106
2096
|
onSession,
|
|
2097
|
+
{
|
|
2098
|
+
reactiveProjectKey: String(meta && meta.reactiveProjectKey || '').trim(),
|
|
2099
|
+
timeouts: executionTimeouts,
|
|
2100
|
+
},
|
|
2107
2101
|
));
|
|
2108
2102
|
if (sessionId) await onSession(sessionId);
|
|
2109
2103
|
observedRuntimeProfile = getActualCodexPermissionProfile(sessionId ? { id: sessionId } : session);
|
|
@@ -2173,6 +2167,10 @@ ${mentorRadarHint}
|
|
|
2173
2167
|
normalizeSenderId(senderId),
|
|
2174
2168
|
runtime,
|
|
2175
2169
|
onSession,
|
|
2170
|
+
{
|
|
2171
|
+
reactiveProjectKey: String(meta && meta.reactiveProjectKey || '').trim(),
|
|
2172
|
+
timeouts: executionTimeouts,
|
|
2173
|
+
},
|
|
2176
2174
|
));
|
|
2177
2175
|
if (sessionId) await onSession(sessionId);
|
|
2178
2176
|
}
|
|
@@ -574,7 +574,7 @@ function createCommandRouter(deps) {
|
|
|
574
574
|
return false;
|
|
575
575
|
}
|
|
576
576
|
|
|
577
|
-
async function handleCommand(bot, chatId, text, config, executeTaskByName, senderId = null, readOnly = false) {
|
|
577
|
+
async function handleCommand(bot, chatId, text, config, executeTaskByName, senderId = null, readOnly = false, meta = {}) {
|
|
578
578
|
if (text && !text.startsWith('/chatid') && !text.startsWith('/myid')) log('INFO', `CMD [${String(chatId).slice(-8)}]: ${text.slice(0, 80)}`);
|
|
579
579
|
const state = loadState();
|
|
580
580
|
|
|
@@ -654,7 +654,7 @@ function createCommandRouter(deps) {
|
|
|
654
654
|
}
|
|
655
655
|
const btwPrompt = `[Side question — answer concisely from existing context, no need for tools]\n\n${btwQuestion}`;
|
|
656
656
|
resetCooldown(chatId);
|
|
657
|
-
await askClaude(bot, chatId, btwPrompt, config, true, senderId);
|
|
657
|
+
await askClaude(bot, chatId, btwPrompt, config, true, senderId, meta);
|
|
658
658
|
return;
|
|
659
659
|
}
|
|
660
660
|
|
|
@@ -727,7 +727,7 @@ function createCommandRouter(deps) {
|
|
|
727
727
|
if (handled) {
|
|
728
728
|
// /last attached the session — now send "继续" to actually resume the conversation
|
|
729
729
|
resetCooldown(chatId);
|
|
730
|
-
await askClaude(bot, chatId, '继续上面的工作', config, readOnly, senderId);
|
|
730
|
+
await askClaude(bot, chatId, '继续上面的工作', config, readOnly, senderId, meta);
|
|
731
731
|
return;
|
|
732
732
|
}
|
|
733
733
|
// No session found — fall through to normal askClaude
|
|
@@ -775,7 +775,7 @@ function createCommandRouter(deps) {
|
|
|
775
775
|
await bot.sendMessage(chatId, 'Daily token budget exceeded.');
|
|
776
776
|
return;
|
|
777
777
|
}
|
|
778
|
-
const claudeResult = await askClaude(bot, chatId, text, config, readOnly, senderId);
|
|
778
|
+
const claudeResult = await askClaude(bot, chatId, text, config, readOnly, senderId, meta);
|
|
779
779
|
const claudeFailed = !!(claudeResult && claudeResult.ok === false);
|
|
780
780
|
const claudeAborted = !!(claudeResult && claudeResult.error === 'Stopped by user');
|
|
781
781
|
if (claudeFailed && !claudeAborted && !macLocalFirst && macFallbackEnabled && allowLocalMacControl) {
|
|
@@ -15,6 +15,19 @@ const CODEX_TOOL_MAP = Object.freeze({
|
|
|
15
15
|
web_fetch: 'WebFetch',
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
+
const ENGINE_TIMEOUT_DEFAULTS = Object.freeze({
|
|
19
|
+
codex: Object.freeze({
|
|
20
|
+
idleMs: 10 * 60 * 1000,
|
|
21
|
+
toolMs: 25 * 60 * 1000,
|
|
22
|
+
ceilingMs: 60 * 60 * 1000,
|
|
23
|
+
}),
|
|
24
|
+
claude: Object.freeze({
|
|
25
|
+
idleMs: 5 * 60 * 1000,
|
|
26
|
+
toolMs: 25 * 60 * 1000,
|
|
27
|
+
ceilingMs: 60 * 60 * 1000,
|
|
28
|
+
}),
|
|
29
|
+
});
|
|
30
|
+
|
|
18
31
|
function resolveBinary(engineName, deps = {}) {
|
|
19
32
|
const engine = normalizeEngineName(engineName);
|
|
20
33
|
const home = deps.HOME || os.homedir();
|
|
@@ -269,6 +282,16 @@ function parseCodexStreamEvent(line) {
|
|
|
269
282
|
return out;
|
|
270
283
|
}
|
|
271
284
|
|
|
285
|
+
function resolveEngineTimeouts(engineName, opts = {}) {
|
|
286
|
+
const engine = normalizeEngineName(engineName);
|
|
287
|
+
const base = ENGINE_TIMEOUT_DEFAULTS[engine] || ENGINE_TIMEOUT_DEFAULTS.claude;
|
|
288
|
+
if (!opts || !opts.reactive) return { ...base };
|
|
289
|
+
return {
|
|
290
|
+
...base,
|
|
291
|
+
ceilingMs: null,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
272
295
|
function buildClaudeArgs(options = {}) {
|
|
273
296
|
const { model = ENGINE_MODEL_CONFIG.claude.main, readOnly = false, session = {}, addDirs } = options;
|
|
274
297
|
const args = ['-p', '--model', model];
|
|
@@ -421,7 +444,7 @@ function createEngineRuntimeFactory(deps = {}) {
|
|
|
421
444
|
defaultModel: ENGINE_MODEL_CONFIG.codex.main,
|
|
422
445
|
stdinBehavior: 'write-and-close',
|
|
423
446
|
killSignal: 'SIGTERM',
|
|
424
|
-
timeouts:
|
|
447
|
+
timeouts: resolveEngineTimeouts('codex'),
|
|
425
448
|
buildArgs: buildCodexArgs,
|
|
426
449
|
buildEnv: ({ metameProject = '', metameSenderId = '' } = {}) => buildCodexEnv(process.env, { metameProject, metameSenderId }),
|
|
427
450
|
parseStreamEvent: parseCodexStreamEvent,
|
|
@@ -434,7 +457,7 @@ function createEngineRuntimeFactory(deps = {}) {
|
|
|
434
457
|
defaultModel: ENGINE_MODEL_CONFIG.claude.main,
|
|
435
458
|
stdinBehavior: 'write-and-close',
|
|
436
459
|
killSignal: 'SIGTERM',
|
|
437
|
-
timeouts:
|
|
460
|
+
timeouts: resolveEngineTimeouts('claude'),
|
|
438
461
|
buildArgs: buildClaudeArgs,
|
|
439
462
|
buildEnv: ({ metameProject = '', metameSenderId = '' } = {}) => ({
|
|
440
463
|
...(() => {
|
|
@@ -460,6 +483,7 @@ module.exports = {
|
|
|
460
483
|
ENGINE_DISTILL_MAP,
|
|
461
484
|
ENGINE_DEFAULT_MODEL,
|
|
462
485
|
_private: {
|
|
486
|
+
ENGINE_TIMEOUT_DEFAULTS,
|
|
463
487
|
classifyEngineError,
|
|
464
488
|
parseClaudeStreamEvent,
|
|
465
489
|
parseCodexStreamEvent,
|
|
@@ -472,5 +496,6 @@ module.exports = {
|
|
|
472
496
|
BUILTIN_CLAUDE_MODEL_VALUES,
|
|
473
497
|
normalizeClaudeModel,
|
|
474
498
|
looksLikeCodexModel,
|
|
499
|
+
resolveEngineTimeouts,
|
|
475
500
|
},
|
|
476
501
|
};
|
|
@@ -283,9 +283,9 @@ function createMessagePipeline(deps) {
|
|
|
283
283
|
*/
|
|
284
284
|
async function _processOne(chatId, text, ctx) {
|
|
285
285
|
if (resetCooldown) resetCooldown(chatId);
|
|
286
|
-
const { bot, config, executeTaskByName, senderId, readOnly } = ctx;
|
|
286
|
+
const { bot, config, executeTaskByName, senderId, readOnly, meta } = ctx;
|
|
287
287
|
try {
|
|
288
|
-
return await handleCommand(bot, chatId, text, config, executeTaskByName, senderId, readOnly);
|
|
288
|
+
return await handleCommand(bot, chatId, text, config, executeTaskByName, senderId, readOnly, meta || {});
|
|
289
289
|
} catch (err) {
|
|
290
290
|
log('ERROR', `Pipeline: error processing message for ${chatId}: ${err.message}`);
|
|
291
291
|
return { ok: false, error: err.message };
|
|
@@ -87,6 +87,33 @@ function setReactiveStatus(state, projectKey, status, reason) {
|
|
|
87
87
|
rs.updated_at = new Date().toISOString();
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
function isReactiveExecutionActive(projectKey, config, deps) {
|
|
91
|
+
const active = deps && deps.activeProcesses;
|
|
92
|
+
if (!active || typeof active.values !== 'function') return false;
|
|
93
|
+
const key = String(projectKey || '').trim();
|
|
94
|
+
if (!key) return false;
|
|
95
|
+
const parent = config && config.projects ? config.projects[key] : null;
|
|
96
|
+
const memberKeys = new Set(
|
|
97
|
+
Array.isArray(parent && parent.team)
|
|
98
|
+
? parent.team.map(member => String(member && member.key || '').trim()).filter(Boolean)
|
|
99
|
+
: []
|
|
100
|
+
);
|
|
101
|
+
for (const proc of active.values()) {
|
|
102
|
+
if (!proc || proc.aborted) continue;
|
|
103
|
+
const reactiveProjectKey = String(proc.reactiveProjectKey || '').trim();
|
|
104
|
+
if (reactiveProjectKey && reactiveProjectKey === key) return true;
|
|
105
|
+
const procChatId = String(proc.chatId || proc.logicalChatId || '').trim();
|
|
106
|
+
if (!procChatId) continue;
|
|
107
|
+
if (procChatId === `_agent_${key}`) return true;
|
|
108
|
+
if (procChatId.startsWith('_scope_') && procChatId.endsWith(`__${key}`)) return true;
|
|
109
|
+
for (const memberKey of memberKeys) {
|
|
110
|
+
if (procChatId === `_agent_${memberKey}`) return true;
|
|
111
|
+
if (procChatId.startsWith('_scope_') && procChatId.endsWith(`__${memberKey}`)) return true;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
90
117
|
/**
|
|
91
118
|
* Find the reactive parent project key for a given team member.
|
|
92
119
|
* Returns the parent key string, or null if not found.
|
|
@@ -457,6 +484,10 @@ function reconcilePerpetualProjects(config, deps) {
|
|
|
457
484
|
const staleThreshold = staleMinutes * 60 * 1000;
|
|
458
485
|
|
|
459
486
|
if (Date.now() - lastUpdate > staleThreshold) {
|
|
487
|
+
if (isReactiveExecutionActive(key, config, deps)) {
|
|
488
|
+
deps.log('INFO', `Reconcile: ${key} exceeds stale threshold but reactive execution is still active`);
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
460
491
|
deps.log('WARN', `Reconcile: ${key} stuck since ${rs.updated_at}`);
|
|
461
492
|
setReactiveStatus(st, key, 'stale', 'no_activity');
|
|
462
493
|
deps.saveState(st);
|
|
@@ -925,6 +956,7 @@ function handleReactiveOutput(targetProject, output, config, deps) {
|
|
|
925
956
|
prompt: completionResult.nextMissionPrompt,
|
|
926
957
|
from: '_system',
|
|
927
958
|
_reactive: true,
|
|
959
|
+
_reactive_project: projectKey,
|
|
928
960
|
new_session: true,
|
|
929
961
|
}, config);
|
|
930
962
|
}
|
|
@@ -972,6 +1004,7 @@ function handleReactiveOutput(targetProject, output, config, deps) {
|
|
|
972
1004
|
prompt: d.prompt,
|
|
973
1005
|
from: projectKey,
|
|
974
1006
|
_reactive: true,
|
|
1007
|
+
_reactive_project: projectKey,
|
|
975
1008
|
new_session: true,
|
|
976
1009
|
}, config);
|
|
977
1010
|
}
|
|
@@ -1089,6 +1122,7 @@ function handleReactiveOutput(targetProject, output, config, deps) {
|
|
|
1089
1122
|
prompt: `[${targetProject} delivery]${verifierBlock}\n\n${summary}\n\nDecide next step. Use NEXT_DISPATCH or ${signal}.`,
|
|
1090
1123
|
from: targetProject,
|
|
1091
1124
|
_reactive: true,
|
|
1125
|
+
_reactive_project: parentKey,
|
|
1092
1126
|
new_session: true,
|
|
1093
1127
|
}, config);
|
|
1094
1128
|
}
|
|
@@ -1098,5 +1132,5 @@ module.exports = {
|
|
|
1098
1132
|
parseReactiveSignals,
|
|
1099
1133
|
reconcilePerpetualProjects,
|
|
1100
1134
|
replayEventLog,
|
|
1101
|
-
__test: { runProjectVerifier, readPhaseFromState, resolveProjectCwd, appendEvent, projectProgressTsv, generateStateFile, loadProjectManifest, resolveProjectScripts, parseEventLog, buildRunningMemory, scanRelevantArtifacts, buildWorkingMemory, persistMemoryFiles, extractInlineFacts, extractOutputSummary },
|
|
1135
|
+
__test: { runProjectVerifier, readPhaseFromState, resolveProjectCwd, appendEvent, projectProgressTsv, generateStateFile, loadProjectManifest, resolveProjectScripts, parseEventLog, buildRunningMemory, scanRelevantArtifacts, buildWorkingMemory, persistMemoryFiles, extractInlineFacts, extractOutputSummary, isReactiveExecutionActive },
|
|
1102
1136
|
};
|
|
@@ -194,7 +194,6 @@ function createTaskScheduler(deps) {
|
|
|
194
194
|
isUserIdle,
|
|
195
195
|
isInSleepMode,
|
|
196
196
|
setSleepMode,
|
|
197
|
-
spawnSessionSummaries,
|
|
198
197
|
getWakeRecoveryHook,
|
|
199
198
|
skillEvolution,
|
|
200
199
|
} = deps;
|
|
@@ -744,8 +743,6 @@ function createTaskScheduler(deps) {
|
|
|
744
743
|
if (idle && !isInSleepMode()) {
|
|
745
744
|
setSleepMode(true);
|
|
746
745
|
log('INFO', '[DAEMON] Entering Sleep Mode');
|
|
747
|
-
// Generate summaries for sessions idle 2-24h
|
|
748
|
-
spawnSessionSummaries();
|
|
749
746
|
} else if (!idle && isInSleepMode()) {
|
|
750
747
|
setSleepMode(false);
|
|
751
748
|
log('INFO', '[DAEMON] Exiting Sleep Mode — local activity detected');
|
package/scripts/daemon.js
CHANGED
|
@@ -943,6 +943,8 @@ function dispatchTask(targetProject, message, config, replyFn, streamOptions = n
|
|
|
943
943
|
payload,
|
|
944
944
|
callback: message.callback || false,
|
|
945
945
|
new_session: !!message.new_session,
|
|
946
|
+
reactive: !!message._reactive,
|
|
947
|
+
reactive_project_key: String(message._reactive_project || '').trim(),
|
|
946
948
|
chain: [...chain, message.from || 'unknown'],
|
|
947
949
|
task_id: envelope ? envelope.task_id : null,
|
|
948
950
|
scope_id: envelope ? envelope.scope_id : null,
|
|
@@ -1164,7 +1166,19 @@ function dispatchTask(targetProject, message, config, replyFn, streamOptions = n
|
|
|
1164
1166
|
taskBoard.markTaskStatus(envelope.task_id, 'running', { summary: `dispatched via ${sessionMode}` });
|
|
1165
1167
|
taskBoard.appendTaskEvent(envelope.task_id, 'task_started', targetProject, { session_mode: sessionMode });
|
|
1166
1168
|
}
|
|
1167
|
-
_handleCommand(
|
|
1169
|
+
_handleCommand(
|
|
1170
|
+
nullBot,
|
|
1171
|
+
dispatchChatId,
|
|
1172
|
+
prompt,
|
|
1173
|
+
config,
|
|
1174
|
+
null,
|
|
1175
|
+
null,
|
|
1176
|
+
dispatchReadOnly,
|
|
1177
|
+
{
|
|
1178
|
+
reactive: !!fullMsg.reactive,
|
|
1179
|
+
reactiveProjectKey: fullMsg.reactive_project_key || '',
|
|
1180
|
+
},
|
|
1181
|
+
).catch(e => {
|
|
1168
1182
|
log('ERROR', `Dispatch handleCommand failed for ${targetProject}: ${e.message}`);
|
|
1169
1183
|
if (envelope && taskBoard) {
|
|
1170
1184
|
taskBoard.markTaskStatus(envelope.task_id, 'failed', { last_error: e.message, summary: 'dispatch execution failed' });
|
|
@@ -1180,67 +1194,6 @@ function dispatchTask(targetProject, message, config, replyFn, streamOptions = n
|
|
|
1180
1194
|
};
|
|
1181
1195
|
}
|
|
1182
1196
|
|
|
1183
|
-
/**
|
|
1184
|
-
* Spawn memory-extract.js as a detached background process.
|
|
1185
|
-
* Called on sleep mode entry to consolidate session facts.
|
|
1186
|
-
*/
|
|
1187
|
-
/**
|
|
1188
|
-
* Spawn session-summarize.js for sessions that have been idle 2-24 hours.
|
|
1189
|
-
* Called on sleep mode entry. Skips sessions that already have a fresh summary.
|
|
1190
|
-
*/
|
|
1191
|
-
const MAX_CONCURRENT_SUMMARIES = 3;
|
|
1192
|
-
|
|
1193
|
-
function spawnSessionSummaries() {
|
|
1194
|
-
const scriptPath = path.join(__dirname, 'session-summarize.js');
|
|
1195
|
-
if (!fs.existsSync(scriptPath)) return;
|
|
1196
|
-
const state = loadState();
|
|
1197
|
-
const now = Date.now();
|
|
1198
|
-
const TWO_HOURS = 2 * 60 * 60 * 1000;
|
|
1199
|
-
const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1000;
|
|
1200
|
-
// Collect eligible sessions, sort by most recently active first
|
|
1201
|
-
const eligible = [];
|
|
1202
|
-
for (const [cid, sess] of Object.entries(state.sessions || {})) {
|
|
1203
|
-
// Support both old flat format and new per-engine format
|
|
1204
|
-
let sessionId, started;
|
|
1205
|
-
if (sess.engines) {
|
|
1206
|
-
const active = Object.values(sess.engines).find(s => s.id && s.started);
|
|
1207
|
-
if (!active) continue;
|
|
1208
|
-
sessionId = active.id;
|
|
1209
|
-
started = true;
|
|
1210
|
-
} else {
|
|
1211
|
-
sessionId = sess.id;
|
|
1212
|
-
started = sess.started;
|
|
1213
|
-
}
|
|
1214
|
-
if (!sessionId || !started) continue;
|
|
1215
|
-
const lastActive = sess.last_active || 0;
|
|
1216
|
-
const idleMs = now - lastActive;
|
|
1217
|
-
if (idleMs < TWO_HOURS || idleMs > SEVEN_DAYS) continue;
|
|
1218
|
-
if ((sess.last_summary_at || 0) > lastActive) continue;
|
|
1219
|
-
eligible.push({ cid, sess: { ...sess, id: sessionId, started }, lastActive });
|
|
1220
|
-
}
|
|
1221
|
-
eligible.sort((a, b) => b.lastActive - a.lastActive);
|
|
1222
|
-
|
|
1223
|
-
let spawned = 0;
|
|
1224
|
-
for (const { cid, sess } of eligible) {
|
|
1225
|
-
if (spawned >= MAX_CONCURRENT_SUMMARIES) {
|
|
1226
|
-
log('INFO', `[DAEMON] Session summary concurrency limit (${MAX_CONCURRENT_SUMMARIES}) reached, deferring remaining`);
|
|
1227
|
-
break;
|
|
1228
|
-
}
|
|
1229
|
-
const idleMs = now - (sess.last_active || 0);
|
|
1230
|
-
try {
|
|
1231
|
-
const child = spawn(process.execPath, [scriptPath, cid, sess.id], {
|
|
1232
|
-
detached: true, stdio: 'ignore',
|
|
1233
|
-
...(process.platform === 'win32' ? { windowsHide: true } : {}),
|
|
1234
|
-
});
|
|
1235
|
-
child.unref();
|
|
1236
|
-
spawned++;
|
|
1237
|
-
log('INFO', `[DAEMON] Session summary spawned for ${cid} (idle ${Math.round(idleMs / 3600000)}h)`);
|
|
1238
|
-
} catch (e) {
|
|
1239
|
-
log('WARN', `[DAEMON] Failed to spawn session summary: ${e.message}`);
|
|
1240
|
-
}
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
1197
|
/**
|
|
1245
1198
|
* Physiological heartbeat: zero-token awareness check.
|
|
1246
1199
|
* Runs every tick unconditionally.
|
|
@@ -1760,6 +1713,7 @@ function physiologicalHeartbeat(config) {
|
|
|
1760
1713
|
log,
|
|
1761
1714
|
loadState,
|
|
1762
1715
|
saveState,
|
|
1716
|
+
activeProcesses,
|
|
1763
1717
|
notifyUser: (msg) => {
|
|
1764
1718
|
try {
|
|
1765
1719
|
const cfg = loadConfig();
|
|
@@ -2137,7 +2091,6 @@ const {
|
|
|
2137
2091
|
isUserIdle,
|
|
2138
2092
|
isInSleepMode: () => _inSleepMode,
|
|
2139
2093
|
setSleepMode: (next) => { _inSleepMode = !!next; },
|
|
2140
|
-
spawnSessionSummaries,
|
|
2141
2094
|
getWakeRecoveryHook: () => wakeRecoveryHook,
|
|
2142
2095
|
skillEvolution,
|
|
2143
2096
|
});
|
|
@@ -46,6 +46,8 @@ feishu:
|
|
|
46
46
|
- 引擎 runtime 工厂:`scripts/daemon-engine-runtime.js`
|
|
47
47
|
- 会话执行入口:`scripts/daemon-claude-engine.js`(Claude/Codex 共用)
|
|
48
48
|
- Session 回写:`patchSessionSerialized()` 串行化,避免 thread.started 竞态覆盖
|
|
49
|
+
- 同一个底层 session 不做自动“恢复摘要”注入;续聊直接依赖引擎原生上下文
|
|
50
|
+
- 额外上下文只在显式链路进入时注入,例如 `/compact` 产物、`NOW.md`、memory facts / capsules、intent hints
|
|
49
51
|
|
|
50
52
|
### Codex 会话策略
|
|
51
53
|
|
|
@@ -95,6 +97,10 @@ feishu:
|
|
|
95
97
|
- Dispatch 签名密钥:`~/.metame/.dispatch_secret`(自动创建)
|
|
96
98
|
- 自动更新策略:发布版 npm 安装默认开启;源码 checkout / `npm link` 默认关闭,可用 `METAME_AUTO_UPDATE=on|off` 覆盖
|
|
97
99
|
|
|
100
|
+
说明:
|
|
101
|
+
- `daemon_state.json` 仍保存 session 元数据(如 `last_active`),用于路由、恢复和状态判断。
|
|
102
|
+
- 不再缓存或注入“闲置后恢复摘要”;如果需要压缩上下文,只走显式 `/compact`。
|
|
103
|
+
|
|
98
104
|
## 7. 热重载安全机制(三层防护)
|
|
99
105
|
|
|
100
106
|
1. **部署前预检**(`index.js`):`node -c` 语法检查所有 `.js`,不通过则拒绝以 copy 模式部署到 `~/.metame/`
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
| 文件 | 引用方 | 说明 |
|
|
13
13
|
|------|--------|------|
|
|
14
|
-
| `session-analytics.js` | daemon-claude-engine, distill, memory-extract
|
|
14
|
+
| `session-analytics.js` | daemon-claude-engine, distill, memory-extract | 会话分析核心库 |
|
|
15
15
|
| `mentor-engine.js` | daemon-claude-engine, daemon-admin-commands | AI 导师引擎 |
|
|
16
16
|
| `intent-registry.js` | daemon-claude-engine, hooks/intent-engine | 意图识别注册表 |
|
|
17
17
|
| `daemon-command-session-route.js` | daemon-exec-commands, daemon-ops-commands | 会话路由解析 |
|
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
| `daemon-siri-imessage.js` | daemon-siri-bridge.js | iMessage 数据库读取 |
|
|
20
20
|
| `telegram-adapter.js` | daemon-bridges.js | Telegram 适配器 |
|
|
21
21
|
| `feishu-adapter.js` | daemon-bridges.js | 飞书适配器 |
|
|
22
|
-
| `session-summarize.js` | daemon.js (spawn) | 会话总结,由 daemon.js 第1158行 spawn 调用 |
|
|
23
22
|
|
|
24
23
|
### HEARTBEAT — daemon.yaml 心跳任务调用(无需处理)
|
|
25
24
|
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
- 会话与引擎选择:
|
|
29
29
|
- `scripts/daemon-claude-engine.js`
|
|
30
30
|
- 关键点:`askClaude()` 按 `project.engine`/session 选择 runtime;`patchSessionSerialized()` 串行回写 session
|
|
31
|
+
- 说明:同一底层 session 续聊不再注入会话恢复摘要;额外上下文仅来自显式 compact / memory / intent 链路
|
|
31
32
|
- Codex 规则:`exec`/`resume`、10 分钟窗口内一次自动重试、`thread_id` 迁移回写
|
|
32
33
|
|
|
33
34
|
- Agent Soul 身份层(新):
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* session-summarize.js <chatId> <sessionId>
|
|
4
|
-
* Generates a 3-5 sentence summary for an idle session via Haiku,
|
|
5
|
-
* stores it in daemon_state.json for injection on next resume.
|
|
6
|
-
*
|
|
7
|
-
* Uses session-analytics.extractSkeleton() for robust JSONL parsing
|
|
8
|
-
* (handles tool_use, artifacts, empty chunks without crashing).
|
|
9
|
-
*/
|
|
10
|
-
'use strict';
|
|
11
|
-
|
|
12
|
-
const fs = require('fs');
|
|
13
|
-
const path = require('path');
|
|
14
|
-
const os = require('os');
|
|
15
|
-
|
|
16
|
-
const [,, chatId, sessionId] = process.argv;
|
|
17
|
-
if (!chatId || !sessionId) {
|
|
18
|
-
console.error('Usage: session-summarize.js <chatId> <sessionId>');
|
|
19
|
-
process.exit(1);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const HOME = os.homedir();
|
|
23
|
-
const METAME_DIR = path.join(HOME, '.metame');
|
|
24
|
-
const STATE_FILE = path.join(METAME_DIR, 'daemon_state.json');
|
|
25
|
-
const CLAUDE_PROJECTS = path.join(HOME, '.claude', 'projects');
|
|
26
|
-
|
|
27
|
-
function findSessionFile(sid) {
|
|
28
|
-
try {
|
|
29
|
-
for (const dir of fs.readdirSync(CLAUDE_PROJECTS)) {
|
|
30
|
-
const p = path.join(CLAUDE_PROJECTS, dir, `${sid}.jsonl`);
|
|
31
|
-
if (fs.existsSync(p)) return p;
|
|
32
|
-
}
|
|
33
|
-
} catch { /* ignore */ }
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function loadState() {
|
|
38
|
-
try { return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8')); } catch { return {}; }
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function saveState(state) {
|
|
42
|
-
try { fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2), 'utf8'); } catch { /* ignore */ }
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async function main() {
|
|
46
|
-
const sessionFile = findSessionFile(sessionId);
|
|
47
|
-
if (!sessionFile) {
|
|
48
|
-
console.log(`[session-summarize] Session file not found for ${sessionId.slice(0, 8)}`);
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Use extractSkeleton for robust parsing — already battle-tested on 100+ sessions.
|
|
53
|
-
// Handles tool_use blocks, artifacts, empty chunks, malformed lines gracefully.
|
|
54
|
-
let skeleton;
|
|
55
|
-
try {
|
|
56
|
-
const analytics = require('./session-analytics');
|
|
57
|
-
skeleton = analytics.extractSkeleton(sessionFile);
|
|
58
|
-
} catch (e) {
|
|
59
|
-
console.log(`[session-summarize] extractSkeleton failed: ${e.message}`);
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const snippets = skeleton.user_snippets || [];
|
|
64
|
-
if (snippets.length < 2) {
|
|
65
|
-
console.log(`[session-summarize] Too few user messages (${snippets.length}), skipping`);
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
let callHaiku;
|
|
70
|
-
try {
|
|
71
|
-
callHaiku = require('./providers').callHaiku;
|
|
72
|
-
} catch (e) {
|
|
73
|
-
console.log(`[session-summarize] providers not available: ${e.message}`);
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Build compact context from skeleton (safe strings, already sliced to 100 chars each)
|
|
78
|
-
const snippetText = snippets.join('\n- ');
|
|
79
|
-
const meta = [
|
|
80
|
-
skeleton.project ? `项目: ${skeleton.project}` : '',
|
|
81
|
-
skeleton.intent ? `首要意图: ${skeleton.intent}` : '',
|
|
82
|
-
skeleton.duration_min ? `时长: ${skeleton.duration_min}分钟` : '',
|
|
83
|
-
skeleton.total_tool_calls ? `工具调用: ${skeleton.total_tool_calls}次` : '',
|
|
84
|
-
].filter(Boolean).join(',');
|
|
85
|
-
|
|
86
|
-
const prompt = `请用2-4句话简洁总结以下会话的核心内容和关键结论。只说结果和决策,不列举过程。中文输出。
|
|
87
|
-
|
|
88
|
-
${meta}
|
|
89
|
-
|
|
90
|
-
用户主要说了什么:
|
|
91
|
-
- ${snippetText}`;
|
|
92
|
-
|
|
93
|
-
let summary;
|
|
94
|
-
try {
|
|
95
|
-
summary = await Promise.race([
|
|
96
|
-
callHaiku(prompt, {}, 30000),
|
|
97
|
-
new Promise((_, r) => setTimeout(() => r(new Error('timeout')), 35000)),
|
|
98
|
-
]);
|
|
99
|
-
summary = (summary || '').trim().slice(0, 500);
|
|
100
|
-
} catch (e) {
|
|
101
|
-
console.log(`[session-summarize] Haiku call failed: ${e.message}`);
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (!summary) return;
|
|
106
|
-
|
|
107
|
-
const state = loadState();
|
|
108
|
-
if (!state.sessions) state.sessions = {};
|
|
109
|
-
if (!state.sessions[chatId]) state.sessions[chatId] = {};
|
|
110
|
-
state.sessions[chatId].last_summary = summary;
|
|
111
|
-
state.sessions[chatId].last_summary_at = Date.now();
|
|
112
|
-
state.sessions[chatId].last_summary_session_id = sessionId;
|
|
113
|
-
saveState(state);
|
|
114
|
-
|
|
115
|
-
console.log(`[session-summarize] Saved for ${chatId} (${sessionId.slice(0, 8)}): ${summary.slice(0, 80)}...`);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
main().catch(e => console.error(`[session-summarize] Fatal: ${e.message}`));
|