metame-cli 1.5.18 → 1.5.20
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 +157 -80
- package/package.json +2 -2
- package/scripts/bin/bootstrap-worktree.sh +20 -0
- package/scripts/core/audit.js +190 -0
- package/scripts/core/handoff.js +780 -0
- package/scripts/core/handoff.test.js +1074 -0
- package/scripts/core/memory-model.js +183 -0
- package/scripts/core/memory-model.test.js +486 -0
- package/scripts/core/reactive-paths.js +44 -0
- package/scripts/core/reactive-paths.test.js +35 -0
- package/scripts/core/reactive-prompt.js +51 -0
- package/scripts/core/reactive-prompt.test.js +88 -0
- package/scripts/core/reactive-signal.js +40 -0
- package/scripts/core/reactive-signal.test.js +88 -0
- package/scripts/core/thread-chat-id.js +52 -0
- package/scripts/core/thread-chat-id.test.js +113 -0
- package/scripts/daemon-bridges.js +79 -35
- package/scripts/daemon-claude-engine.js +371 -425
- package/scripts/daemon-command-router.js +80 -6
- package/scripts/daemon-engine-runtime.js +26 -4
- package/scripts/daemon-message-pipeline.js +2 -2
- package/scripts/daemon-reactive-lifecycle.js +134 -33
- package/scripts/daemon-session-commands.js +133 -43
- package/scripts/daemon-session-store.js +300 -82
- package/scripts/daemon-team-dispatch.js +16 -16
- package/scripts/daemon.js +37 -176
- package/scripts/deploy-manifest.js +90 -0
- package/scripts/docs/maintenance-manual.md +14 -11
- package/scripts/docs/pointer-map.md +13 -4
- package/scripts/feishu-adapter.js +31 -27
- package/scripts/hooks/intent-engine.js +6 -3
- package/scripts/hooks/intent-memory-recall.js +1 -0
- package/scripts/hooks/intent-perpetual.js +1 -1
- package/scripts/memory-extract.js +5 -97
- package/scripts/memory-gc.js +35 -90
- package/scripts/memory-migrate-v2.js +304 -0
- package/scripts/memory-nightly-reflect.js +40 -41
- package/scripts/memory.js +340 -859
- package/scripts/migrate-reactive-paths.js +122 -0
- package/scripts/signal-capture.js +4 -0
- package/scripts/sync-plugin.js +56 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { resolveEngineModel } = require('./daemon-engine-runtime');
|
|
4
|
+
const { rawChatId: extractOriginalChatId } = require('./core/thread-chat-id');
|
|
4
5
|
|
|
5
6
|
function createCommandRouter(deps) {
|
|
6
7
|
const {
|
|
@@ -224,6 +225,50 @@ function createCommandRouter(deps) {
|
|
|
224
225
|
return inferredKey ? `_bound_${inferredKey}` : rawChatId;
|
|
225
226
|
}
|
|
226
227
|
|
|
228
|
+
function resolveCurrentSessionContext(chatId, config) {
|
|
229
|
+
const chatIdStr = String(chatId || '');
|
|
230
|
+
const chatAgentMap = {
|
|
231
|
+
...(config && config.telegram ? config.telegram.chat_agent_map : {}),
|
|
232
|
+
...(config && config.feishu ? config.feishu.chat_agent_map : {}),
|
|
233
|
+
...(config && config.imessage ? config.imessage.chat_agent_map : {}),
|
|
234
|
+
...(config && config.siri_bridge ? config.siri_bridge.chat_agent_map : {}),
|
|
235
|
+
};
|
|
236
|
+
const _rawChatId = extractOriginalChatId(chatIdStr);
|
|
237
|
+
const mappedKey = chatAgentMap[chatIdStr] || chatAgentMap[_rawChatId] || projectKeyFromVirtualChatId(chatIdStr);
|
|
238
|
+
const mappedProject = mappedKey && config && config.projects ? config.projects[mappedKey] : null;
|
|
239
|
+
const preferredEngine = String((mappedProject && mappedProject.engine) || getDefaultEngine()).toLowerCase();
|
|
240
|
+
const state = loadState() || {};
|
|
241
|
+
const sessions = state.sessions || {};
|
|
242
|
+
const candidateIds = [
|
|
243
|
+
mappedKey ? buildSessionChatId(chatIdStr, mappedKey) : null,
|
|
244
|
+
buildSessionChatId(chatIdStr),
|
|
245
|
+
chatIdStr,
|
|
246
|
+
].filter(Boolean);
|
|
247
|
+
|
|
248
|
+
for (const candidateId of candidateIds) {
|
|
249
|
+
const record = sessions[candidateId];
|
|
250
|
+
if (!record) continue;
|
|
251
|
+
const candidateSlots = [];
|
|
252
|
+
if (record.engines && typeof record.engines === 'object') {
|
|
253
|
+
if (record.engines[preferredEngine]) candidateSlots.push([preferredEngine, record.engines[preferredEngine]]);
|
|
254
|
+
for (const [engineName, slot] of Object.entries(record.engines)) {
|
|
255
|
+
if (engineName === preferredEngine) continue;
|
|
256
|
+
candidateSlots.push([engineName, slot]);
|
|
257
|
+
}
|
|
258
|
+
} else if (record.engine) {
|
|
259
|
+
candidateSlots.push([String(record.engine).toLowerCase(), record]);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
for (const [engineName, slot] of candidateSlots) {
|
|
263
|
+
if (!slot) continue;
|
|
264
|
+
if (slot.id || record.cwd || slot.runtimeSessionObserved === false) {
|
|
265
|
+
return { record, slot, sessionChatId: candidateId, engine: engineName || preferredEngine };
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
|
|
227
272
|
function getBoundProjectForChat(chatId, cfg) {
|
|
228
273
|
const map = {
|
|
229
274
|
...(cfg.telegram ? cfg.telegram.chat_agent_map : {}),
|
|
@@ -445,6 +490,13 @@ function createCommandRouter(deps) {
|
|
|
445
490
|
const workspaceDir = extractPathFromText(input);
|
|
446
491
|
const hasWorkspacePath = !!workspaceDir;
|
|
447
492
|
|
|
493
|
+
// Exclude third-party product context — "智能体" about other companies is NOT about our agents
|
|
494
|
+
// Requires BOTH a company name AND agent-related keyword to trigger, avoiding false positives on generic verbs
|
|
495
|
+
const _hasThirdPartyName = /(阿里|百度|腾讯|字节|谷歌|google|openai|微软|microsoft|deepseek|豆包|通义|文心|kimi)/i.test(input);
|
|
496
|
+
const _hasAgentWord = /(智能体|agent|助手|机器人)/i.test(input);
|
|
497
|
+
const _isAboutOurAgents = /(我的|我们的|当前|这个群|这里的|metame)/i.test(input);
|
|
498
|
+
if (_hasThirdPartyName && _hasAgentWord && !_isAboutOurAgents) return false;
|
|
499
|
+
|
|
448
500
|
const hasAgentContext = /(agent|智能体|工作区|人设|绑定|当前群|这个群|chat|workspace)/i.test(input);
|
|
449
501
|
const wantsList = /(列出|查看|显示|有哪些|list|show)/i.test(input) && /(agent|智能体|工作区|绑定)/i.test(input);
|
|
450
502
|
const wantsUnbind = /(解绑|取消绑定|断开绑定|unbind|unassign)/i.test(input) && hasAgentContext;
|
|
@@ -574,7 +626,7 @@ function createCommandRouter(deps) {
|
|
|
574
626
|
return false;
|
|
575
627
|
}
|
|
576
628
|
|
|
577
|
-
async function handleCommand(bot, chatId, text, config, executeTaskByName, senderId = null, readOnly = false) {
|
|
629
|
+
async function handleCommand(bot, chatId, text, config, executeTaskByName, senderId = null, readOnly = false, meta = {}) {
|
|
578
630
|
if (text && !text.startsWith('/chatid') && !text.startsWith('/myid')) log('INFO', `CMD [${String(chatId).slice(-8)}]: ${text.slice(0, 80)}`);
|
|
579
631
|
const state = loadState();
|
|
580
632
|
|
|
@@ -604,20 +656,36 @@ function createCommandRouter(deps) {
|
|
|
604
656
|
...(config.siri_bridge ? config.siri_bridge.chat_agent_map : {}),
|
|
605
657
|
};
|
|
606
658
|
const _chatIdStr = String(chatId);
|
|
659
|
+
const _rawChatId2 = extractOriginalChatId(_chatIdStr);
|
|
607
660
|
const mappedKey = chatAgentMap[_chatIdStr] ||
|
|
661
|
+
chatAgentMap[_rawChatId2] ||
|
|
608
662
|
projectKeyFromVirtualChatId(_chatIdStr);
|
|
609
663
|
if (mappedKey && config.projects && config.projects[mappedKey]) {
|
|
610
664
|
const proj = config.projects[mappedKey];
|
|
611
665
|
const projCwd = normalizeCwd(proj.cwd);
|
|
612
666
|
const sessionChatId = buildSessionChatId(chatId, mappedKey);
|
|
613
|
-
const
|
|
667
|
+
const sessions = loadState().sessions || {};
|
|
668
|
+
const cur = sessions[sessionChatId];
|
|
669
|
+
const rawSession = sessions[String(chatId)];
|
|
614
670
|
const projEngine = String((proj && proj.engine) || getDefaultEngine()).toLowerCase();
|
|
615
671
|
// Multi-engine format stores engines in cur.engines object; legacy format uses cur.engine string.
|
|
616
672
|
// Check whether the session already has a slot for the project's configured engine.
|
|
617
673
|
const curHasEngine = cur && (
|
|
618
674
|
cur.engines ? !!cur.engines[projEngine] : String(cur.engine || '').toLowerCase() === projEngine
|
|
619
675
|
);
|
|
620
|
-
|
|
676
|
+
const rawHasEngine = rawSession && (
|
|
677
|
+
rawSession.engines ? !!rawSession.engines[projEngine] : String(rawSession.engine || '').toLowerCase() === projEngine
|
|
678
|
+
);
|
|
679
|
+
const isVirtualSession = _chatIdStr.startsWith('_agent_') || _chatIdStr.startsWith('_scope_');
|
|
680
|
+
const shouldReattachForCwdChange =
|
|
681
|
+
!isVirtualSession &&
|
|
682
|
+
!!cur &&
|
|
683
|
+
!!curHasEngine &&
|
|
684
|
+
cur.cwd !== projCwd &&
|
|
685
|
+
!rawHasEngine;
|
|
686
|
+
if (!cur || !curHasEngine || shouldReattachForCwdChange) {
|
|
687
|
+
const initReason = !cur ? 'no-session' : (!curHasEngine ? 'engine-missing' : 'cwd-changed');
|
|
688
|
+
log('INFO', `SESSION-INIT [${String(sessionChatId).slice(-32)}] ${initReason}`);
|
|
621
689
|
attachOrCreateSession(sessionChatId, projCwd, proj.name || mappedKey, proj.engine || getDefaultEngine());
|
|
622
690
|
}
|
|
623
691
|
}
|
|
@@ -722,10 +790,15 @@ function createCommandRouter(deps) {
|
|
|
722
790
|
// "继续" when no task running → resume most recent session via /last, then send prompt
|
|
723
791
|
const CONTINUE_RE = /^(继续|接着|go\s*on|continue)$/i;
|
|
724
792
|
if (!activeProcesses.has(chatId) && CONTINUE_RE.test(text.trim())) {
|
|
725
|
-
|
|
793
|
+
const currentSession = resolveCurrentSessionContext(chatId, config);
|
|
794
|
+
if (currentSession) {
|
|
795
|
+
resetCooldown(chatId);
|
|
796
|
+
await askClaude(bot, chatId, '继续上面的工作', config, readOnly, senderId);
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
// No current session bound to this chat — delegate to /last as a fallback.
|
|
726
800
|
const handled = await handleSessionCommand({ bot, chatId, text: '/last' });
|
|
727
801
|
if (handled) {
|
|
728
|
-
// /last attached the session — now send "继续" to actually resume the conversation
|
|
729
802
|
resetCooldown(chatId);
|
|
730
803
|
await askClaude(bot, chatId, '继续上面的工作', config, readOnly, senderId);
|
|
731
804
|
return;
|
|
@@ -740,7 +813,8 @@ function createCommandRouter(deps) {
|
|
|
740
813
|
...(config.imessage ? config.imessage.chat_agent_map : {}),
|
|
741
814
|
...(config.siri_bridge ? config.siri_bridge.chat_agent_map : {}),
|
|
742
815
|
};
|
|
743
|
-
const
|
|
816
|
+
const _rawChatId3 = extractOriginalChatId(String(chatId));
|
|
817
|
+
const _isStrictChat = !!(_strictChatAgentMap[String(chatId)] || _strictChatAgentMap[_rawChatId3] || projectKeyFromVirtualChatId(String(chatId)));
|
|
744
818
|
|
|
745
819
|
// Nickname-only switch: bypass cooldown + budget (no Claude call)
|
|
746
820
|
// Skipped for strict chats (fixed-agent groups)
|
|
@@ -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: null,
|
|
23
|
+
}),
|
|
24
|
+
claude: Object.freeze({
|
|
25
|
+
idleMs: 5 * 60 * 1000,
|
|
26
|
+
toolMs: 25 * 60 * 1000,
|
|
27
|
+
ceilingMs: null,
|
|
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,12 @@ function parseCodexStreamEvent(line) {
|
|
|
269
282
|
return out;
|
|
270
283
|
}
|
|
271
284
|
|
|
285
|
+
function resolveEngineTimeouts(engineName) {
|
|
286
|
+
const engine = normalizeEngineName(engineName);
|
|
287
|
+
const base = ENGINE_TIMEOUT_DEFAULTS[engine] || ENGINE_TIMEOUT_DEFAULTS.claude;
|
|
288
|
+
return { ...base };
|
|
289
|
+
}
|
|
290
|
+
|
|
272
291
|
function buildClaudeArgs(options = {}) {
|
|
273
292
|
const { model = ENGINE_MODEL_CONFIG.claude.main, readOnly = false, session = {}, addDirs } = options;
|
|
274
293
|
const args = ['-p', '--model', model];
|
|
@@ -392,7 +411,7 @@ function buildCodexArgs(options = {}) {
|
|
|
392
411
|
return args;
|
|
393
412
|
}
|
|
394
413
|
|
|
395
|
-
function buildCodexEnv(baseEnv = {}, { metameProject = '', metameSenderId = '' } = {}) {
|
|
414
|
+
function buildCodexEnv(baseEnv = {}, { metameProject = '', metameSenderId = '', cwd = '' } = {}) {
|
|
396
415
|
const env = { ...baseEnv, METAME_PROJECT: metameProject, METAME_SENDER_ID: String(metameSenderId || '') };
|
|
397
416
|
const strippedKeys = [
|
|
398
417
|
'CODEX_THREAD_ID',
|
|
@@ -400,6 +419,7 @@ function buildCodexEnv(baseEnv = {}, { metameProject = '', metameSenderId = '' }
|
|
|
400
419
|
'CLAUDE_CODE_SSE_PORT',
|
|
401
420
|
];
|
|
402
421
|
for (const key of strippedKeys) delete env[key];
|
|
422
|
+
void cwd;
|
|
403
423
|
if (env.CODEX_HOME && !fs.existsSync(env.CODEX_HOME)) delete env.CODEX_HOME;
|
|
404
424
|
return env;
|
|
405
425
|
}
|
|
@@ -421,9 +441,9 @@ function createEngineRuntimeFactory(deps = {}) {
|
|
|
421
441
|
defaultModel: ENGINE_MODEL_CONFIG.codex.main,
|
|
422
442
|
stdinBehavior: 'write-and-close',
|
|
423
443
|
killSignal: 'SIGTERM',
|
|
424
|
-
timeouts:
|
|
444
|
+
timeouts: resolveEngineTimeouts('codex'),
|
|
425
445
|
buildArgs: buildCodexArgs,
|
|
426
|
-
buildEnv: ({ metameProject = '', metameSenderId = '' } = {}) => buildCodexEnv(process.env, { metameProject, metameSenderId }),
|
|
446
|
+
buildEnv: ({ metameProject = '', metameSenderId = '', cwd = '' } = {}) => buildCodexEnv(process.env, { metameProject, metameSenderId, cwd }),
|
|
427
447
|
parseStreamEvent: parseCodexStreamEvent,
|
|
428
448
|
classifyError: classifyEngineError,
|
|
429
449
|
};
|
|
@@ -434,7 +454,7 @@ function createEngineRuntimeFactory(deps = {}) {
|
|
|
434
454
|
defaultModel: ENGINE_MODEL_CONFIG.claude.main,
|
|
435
455
|
stdinBehavior: 'write-and-close',
|
|
436
456
|
killSignal: 'SIGTERM',
|
|
437
|
-
timeouts:
|
|
457
|
+
timeouts: resolveEngineTimeouts('claude'),
|
|
438
458
|
buildArgs: buildClaudeArgs,
|
|
439
459
|
buildEnv: ({ metameProject = '', metameSenderId = '' } = {}) => ({
|
|
440
460
|
...(() => {
|
|
@@ -460,6 +480,7 @@ module.exports = {
|
|
|
460
480
|
ENGINE_DISTILL_MAP,
|
|
461
481
|
ENGINE_DEFAULT_MODEL,
|
|
462
482
|
_private: {
|
|
483
|
+
ENGINE_TIMEOUT_DEFAULTS,
|
|
463
484
|
classifyEngineError,
|
|
464
485
|
parseClaudeStreamEvent,
|
|
465
486
|
parseCodexStreamEvent,
|
|
@@ -472,5 +493,6 @@ module.exports = {
|
|
|
472
493
|
BUILTIN_CLAUDE_MODEL_VALUES,
|
|
473
494
|
normalizeClaudeModel,
|
|
474
495
|
looksLikeCodexModel,
|
|
496
|
+
resolveEngineTimeouts,
|
|
475
497
|
},
|
|
476
498
|
};
|
|
@@ -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 };
|
|
@@ -4,7 +4,9 @@ const path = require('path');
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const { execSync } = require('child_process');
|
|
7
|
-
const
|
|
7
|
+
const { buildReactivePrompt } = require('./core/reactive-prompt');
|
|
8
|
+
const { calculateNextAction } = require('./core/reactive-signal');
|
|
9
|
+
const { resolveReactivePaths } = require('./core/reactive-paths');
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* daemon-reactive-lifecycle.js — Reactive Loop Lifecycle Module
|
|
@@ -16,7 +18,7 @@ const EVENTS_DIR = path.join(os.homedir(), '.metame', 'events');
|
|
|
16
18
|
* 3. Fresh session — every reactive dispatch uses new_session: true
|
|
17
19
|
* 4. Completion signal — configurable per project (default: MISSION_COMPLETE)
|
|
18
20
|
* 5. Verifier hook — runs project verifier before waking parent
|
|
19
|
-
* 6. Event sourcing — all state changes logged to ~/.metame/
|
|
21
|
+
* 6. Event sourcing — all state changes logged to ~/.metame/reactive/<key>/
|
|
20
22
|
*/
|
|
21
23
|
|
|
22
24
|
// ── Signal parsing ──────────────────────────────────────────────
|
|
@@ -87,6 +89,33 @@ function setReactiveStatus(state, projectKey, status, reason) {
|
|
|
87
89
|
rs.updated_at = new Date().toISOString();
|
|
88
90
|
}
|
|
89
91
|
|
|
92
|
+
function isReactiveExecutionActive(projectKey, config, deps) {
|
|
93
|
+
const active = deps && deps.activeProcesses;
|
|
94
|
+
if (!active || typeof active.values !== 'function') return false;
|
|
95
|
+
const key = String(projectKey || '').trim();
|
|
96
|
+
if (!key) return false;
|
|
97
|
+
const parent = config && config.projects ? config.projects[key] : null;
|
|
98
|
+
const memberKeys = new Set(
|
|
99
|
+
Array.isArray(parent && parent.team)
|
|
100
|
+
? parent.team.map(member => String(member && member.key || '').trim()).filter(Boolean)
|
|
101
|
+
: []
|
|
102
|
+
);
|
|
103
|
+
for (const proc of active.values()) {
|
|
104
|
+
if (!proc || proc.aborted) continue;
|
|
105
|
+
const reactiveProjectKey = String(proc.reactiveProjectKey || '').trim();
|
|
106
|
+
if (reactiveProjectKey && reactiveProjectKey === key) return true;
|
|
107
|
+
const procChatId = String(proc.chatId || proc.logicalChatId || '').trim();
|
|
108
|
+
if (!procChatId) continue;
|
|
109
|
+
if (procChatId === `_agent_${key}`) return true;
|
|
110
|
+
if (procChatId.startsWith('_scope_') && procChatId.endsWith(`__${key}`)) return true;
|
|
111
|
+
for (const memberKey of memberKeys) {
|
|
112
|
+
if (procChatId === `_agent_${memberKey}`) return true;
|
|
113
|
+
if (procChatId.startsWith('_scope_') && procChatId.endsWith(`__${memberKey}`)) return true;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
90
119
|
/**
|
|
91
120
|
* Find the reactive parent project key for a given team member.
|
|
92
121
|
* Returns the parent key string, or null if not found.
|
|
@@ -171,6 +200,24 @@ function readPhaseFromState(statePath) {
|
|
|
171
200
|
} catch { return ''; }
|
|
172
201
|
}
|
|
173
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Load working memory file for a project.
|
|
205
|
+
* Returns the file content as a string, or empty string if not found.
|
|
206
|
+
*
|
|
207
|
+
* @param {string} projectKey
|
|
208
|
+
* @param {string} [metameDir] - Override ~/.metame path (for testing)
|
|
209
|
+
* @returns {string}
|
|
210
|
+
*/
|
|
211
|
+
function loadWorkingMemory(projectKey, metameDir) {
|
|
212
|
+
const base = metameDir || path.join(os.homedir(), '.metame');
|
|
213
|
+
const paths = resolveReactivePaths(projectKey, base);
|
|
214
|
+
const memPath = paths.memory;
|
|
215
|
+
try {
|
|
216
|
+
const content = fs.readFileSync(memPath, 'utf8').trim();
|
|
217
|
+
return content || '';
|
|
218
|
+
} catch { return ''; }
|
|
219
|
+
}
|
|
220
|
+
|
|
174
221
|
/**
|
|
175
222
|
* Run project-level verifier script if it exists.
|
|
176
223
|
* Returns parsed JSON result or null if no verifier / error.
|
|
@@ -184,7 +231,8 @@ function runProjectVerifier(projectKey, config, deps) {
|
|
|
184
231
|
if (!fs.existsSync(scripts.verifier)) return null;
|
|
185
232
|
|
|
186
233
|
const metameDir = deps.metameDir || path.join(os.homedir(), '.metame');
|
|
187
|
-
const
|
|
234
|
+
const paths = resolveReactivePaths(projectKey, metameDir);
|
|
235
|
+
const statePath = paths.state;
|
|
188
236
|
|
|
189
237
|
// Read phase from event log (SoT), fall back to state file for backward compat
|
|
190
238
|
const { phase: eventPhase } = replayEventLog(projectKey, deps);
|
|
@@ -223,10 +271,9 @@ function runCompletionHooks(projectKey, projectCwd, deps) {
|
|
|
223
271
|
// 1. Archive (if script exists)
|
|
224
272
|
if (fs.existsSync(scripts.archiver)) {
|
|
225
273
|
try {
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
);
|
|
274
|
+
const metameDir = deps.metameDir || path.join(os.homedir(), '.metame');
|
|
275
|
+
const rPaths = resolveReactivePaths(projectKey, metameDir);
|
|
276
|
+
const statePath = rPaths.state;
|
|
230
277
|
let projectName = projectKey;
|
|
231
278
|
try {
|
|
232
279
|
const stateContent = fs.readFileSync(statePath, 'utf8');
|
|
@@ -304,12 +351,13 @@ function runCompletionHooks(projectKey, projectCwd, deps) {
|
|
|
304
351
|
|
|
305
352
|
/**
|
|
306
353
|
* Append an event to the project's event log.
|
|
307
|
-
* Daemon-exclusive: agents cannot write to ~/.metame/
|
|
354
|
+
* Daemon-exclusive: agents cannot write to ~/.metame/reactive/<key>/.
|
|
308
355
|
*/
|
|
309
356
|
function appendEvent(projectKey, event, metameDir) {
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
357
|
+
const base = metameDir || path.join(os.homedir(), '.metame');
|
|
358
|
+
const paths = resolveReactivePaths(projectKey, base);
|
|
359
|
+
fs.mkdirSync(paths.dir, { recursive: true });
|
|
360
|
+
const logPath = paths.events;
|
|
313
361
|
const line = JSON.stringify({ ts: new Date().toISOString(), ...event }) + '\n';
|
|
314
362
|
fs.appendFileSync(logPath, line, 'utf8');
|
|
315
363
|
}
|
|
@@ -323,8 +371,9 @@ function appendEvent(projectKey, event, metameDir) {
|
|
|
323
371
|
* This function NEVER throws.
|
|
324
372
|
*/
|
|
325
373
|
function replayEventLog(projectKey, deps) {
|
|
326
|
-
const
|
|
327
|
-
const
|
|
374
|
+
const base = deps?.metameDir || path.join(os.homedir(), '.metame');
|
|
375
|
+
const paths = resolveReactivePaths(projectKey, base);
|
|
376
|
+
const logPath = paths.events;
|
|
328
377
|
if (!fs.existsSync(logPath)) return { phase: '', mission: null, history: [] };
|
|
329
378
|
|
|
330
379
|
const lines = fs.readFileSync(logPath, 'utf8').trim().split('\n').filter(Boolean);
|
|
@@ -365,8 +414,9 @@ function projectProgressTsv(projectCwd, projectKey, metameDir) {
|
|
|
365
414
|
const tsvPath = path.join(projectCwd, 'workspace', 'progress.tsv');
|
|
366
415
|
const header = 'phase\tresult\tverifier_passed\tartifact\ttimestamp\tnotes\n';
|
|
367
416
|
|
|
368
|
-
const
|
|
369
|
-
const
|
|
417
|
+
const base = metameDir || path.join(os.homedir(), '.metame');
|
|
418
|
+
const paths = resolveReactivePaths(projectKey, base);
|
|
419
|
+
const logPath = paths.events;
|
|
370
420
|
if (!fs.existsSync(logPath)) return;
|
|
371
421
|
|
|
372
422
|
const lines = fs.readFileSync(logPath, 'utf8').trim().split('\n').filter(Boolean);
|
|
@@ -403,7 +453,8 @@ function projectProgressTsv(projectCwd, projectKey, metameDir) {
|
|
|
403
453
|
*/
|
|
404
454
|
function generateStateFile(projectKey, config, deps) {
|
|
405
455
|
const metameDir = deps.metameDir || path.join(os.homedir(), '.metame');
|
|
406
|
-
const
|
|
456
|
+
const paths = resolveReactivePaths(projectKey, metameDir);
|
|
457
|
+
const statePath = paths.state;
|
|
407
458
|
|
|
408
459
|
const { phase, mission, history } = replayEventLog(projectKey, deps);
|
|
409
460
|
|
|
@@ -457,6 +508,10 @@ function reconcilePerpetualProjects(config, deps) {
|
|
|
457
508
|
const staleThreshold = staleMinutes * 60 * 1000;
|
|
458
509
|
|
|
459
510
|
if (Date.now() - lastUpdate > staleThreshold) {
|
|
511
|
+
if (isReactiveExecutionActive(key, config, deps)) {
|
|
512
|
+
deps.log('INFO', `Reconcile: ${key} exceeds stale threshold but reactive execution is still active`);
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
460
515
|
deps.log('WARN', `Reconcile: ${key} stuck since ${rs.updated_at}`);
|
|
461
516
|
setReactiveStatus(st, key, 'stale', 'no_activity');
|
|
462
517
|
deps.saveState(st);
|
|
@@ -481,8 +536,8 @@ function reconcilePerpetualProjects(config, deps) {
|
|
|
481
536
|
*/
|
|
482
537
|
function parseEventLog(projectKey, deps) {
|
|
483
538
|
const metameDir = deps.metameDir || path.join(os.homedir(), '.metame');
|
|
484
|
-
const
|
|
485
|
-
const logPath =
|
|
539
|
+
const paths = resolveReactivePaths(projectKey, metameDir);
|
|
540
|
+
const logPath = paths.events;
|
|
486
541
|
if (!fs.existsSync(logPath)) return [];
|
|
487
542
|
|
|
488
543
|
const raw = fs.readFileSync(logPath, 'utf8').trim().split('\n').filter(Boolean);
|
|
@@ -689,9 +744,9 @@ function buildWorkingMemory(projectKey, config, deps) {
|
|
|
689
744
|
*/
|
|
690
745
|
function persistMemoryFiles(projectKey, config, deps, opts = {}) {
|
|
691
746
|
const metameDir = deps.metameDir || path.join(os.homedir(), '.metame');
|
|
692
|
-
const
|
|
693
|
-
fs.mkdirSync(
|
|
694
|
-
const memPath =
|
|
747
|
+
const paths = resolveReactivePaths(projectKey, metameDir);
|
|
748
|
+
fs.mkdirSync(paths.dir, { recursive: true });
|
|
749
|
+
const memPath = paths.memory;
|
|
695
750
|
|
|
696
751
|
// Single parse of event log — shared across L1 and round counting
|
|
697
752
|
const events = parseEventLog(projectKey, deps);
|
|
@@ -724,15 +779,13 @@ function persistMemoryFiles(projectKey, config, deps, opts = {}) {
|
|
|
724
779
|
l2 = buildWorkingMemory(projectKey, config, deps);
|
|
725
780
|
// Stash L2 for next time
|
|
726
781
|
try {
|
|
727
|
-
|
|
728
|
-
fs.writeFileSync(l2CachePath, l2, 'utf8');
|
|
782
|
+
fs.writeFileSync(paths.l2cache, l2, 'utf8');
|
|
729
783
|
} catch { /* non-critical */ }
|
|
730
784
|
} else {
|
|
731
785
|
// Read stale L2 from cache
|
|
732
786
|
try {
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
l2 = fs.readFileSync(l2CachePath, 'utf8').trim();
|
|
787
|
+
if (fs.existsSync(paths.l2cache)) {
|
|
788
|
+
l2 = fs.readFileSync(paths.l2cache, 'utf8').trim();
|
|
736
789
|
}
|
|
737
790
|
} catch { /* non-critical */ }
|
|
738
791
|
}
|
|
@@ -880,12 +933,51 @@ function handleReactiveOutput(targetProject, output, config, deps) {
|
|
|
880
933
|
|
|
881
934
|
// ── Case 1: targetProject is a reactive parent ──
|
|
882
935
|
if (isReactiveParent(targetProject, config)) {
|
|
883
|
-
if (!hasSignals) return;
|
|
884
|
-
|
|
885
936
|
const projectKey = targetProject;
|
|
886
937
|
const pName = config.projects[projectKey]?.name || projectKey;
|
|
887
938
|
const st = deps.loadState();
|
|
888
939
|
const rs = getReactiveState(st, projectKey);
|
|
940
|
+
const maxRetries = manifest?.no_signal_max_retries || 3;
|
|
941
|
+
|
|
942
|
+
const decision = calculateNextAction({
|
|
943
|
+
hasSignals,
|
|
944
|
+
isComplete: signals.complete,
|
|
945
|
+
noSignalCount: rs._no_signal_count || 0,
|
|
946
|
+
maxRetries,
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
rs._no_signal_count = decision.nextNoSignalCount;
|
|
950
|
+
|
|
951
|
+
if (decision.action === 'pause') {
|
|
952
|
+
setReactiveStatus(st, projectKey, 'paused', decision.pauseReason);
|
|
953
|
+
deps.saveState(st);
|
|
954
|
+
logEvent(projectKey, { type: 'NO_SIGNAL_PAUSE', count: decision.nextNoSignalCount });
|
|
955
|
+
if (deps.notifyUser) {
|
|
956
|
+
deps.notifyUser(`\u26a0\ufe0f ${pName} continuous ${maxRetries} rounds without signal, paused`);
|
|
957
|
+
}
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
if (decision.action === 'retry') {
|
|
962
|
+
deps.saveState(st);
|
|
963
|
+
logEvent(projectKey, { type: 'NO_SIGNAL_RETRY', count: decision.nextNoSignalCount, output_length: output.length });
|
|
964
|
+
const maxDepthForRetry = manifest?.max_depth || rs.max_depth || 50;
|
|
965
|
+
const workingMemory = loadWorkingMemory(projectKey, deps.metameDir);
|
|
966
|
+
deps.handleDispatchItem({
|
|
967
|
+
target: projectKey,
|
|
968
|
+
prompt: buildReactivePrompt(
|
|
969
|
+
'Check progress and continue executing the task.',
|
|
970
|
+
{ depth: rs.depth, maxDepth: maxDepthForRetry, completionSignal, workingMemory, isRetry: true }
|
|
971
|
+
),
|
|
972
|
+
from: '_system',
|
|
973
|
+
_reactive: true,
|
|
974
|
+
_reactive_project: projectKey,
|
|
975
|
+
new_session: true,
|
|
976
|
+
}, config);
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// decision.action === 'proceed' — reset no-signal count, continue normal flow
|
|
889
981
|
|
|
890
982
|
// Mission complete takes priority
|
|
891
983
|
if (signals.complete) {
|
|
@@ -925,6 +1017,7 @@ function handleReactiveOutput(targetProject, output, config, deps) {
|
|
|
925
1017
|
prompt: completionResult.nextMissionPrompt,
|
|
926
1018
|
from: '_system',
|
|
927
1019
|
_reactive: true,
|
|
1020
|
+
_reactive_project: projectKey,
|
|
928
1021
|
new_session: true,
|
|
929
1022
|
}, config);
|
|
930
1023
|
}
|
|
@@ -965,13 +1058,17 @@ function handleReactiveOutput(targetProject, output, config, deps) {
|
|
|
965
1058
|
deps.saveState(st);
|
|
966
1059
|
|
|
967
1060
|
// Dispatch each directive with fresh session
|
|
1061
|
+
const workingMemory = loadWorkingMemory(projectKey, deps.metameDir);
|
|
968
1062
|
for (const d of signals.directives) {
|
|
969
1063
|
logEvent(projectKey, { type: 'DISPATCH', target: d.target, prompt: d.prompt.slice(0, 200) });
|
|
970
1064
|
deps.handleDispatchItem({
|
|
971
1065
|
target: d.target,
|
|
972
|
-
prompt: d.prompt,
|
|
1066
|
+
prompt: buildReactivePrompt(d.prompt, {
|
|
1067
|
+
depth: rs.depth, maxDepth, completionSignal, workingMemory,
|
|
1068
|
+
}),
|
|
973
1069
|
from: projectKey,
|
|
974
1070
|
_reactive: true,
|
|
1071
|
+
_reactive_project: projectKey,
|
|
975
1072
|
new_session: true,
|
|
976
1073
|
}, config);
|
|
977
1074
|
}
|
|
@@ -1001,8 +1098,7 @@ function handleReactiveOutput(targetProject, output, config, deps) {
|
|
|
1001
1098
|
|
|
1002
1099
|
// Depth gate (manifest max_depth overrides default)
|
|
1003
1100
|
const rs = getReactiveState(st, parentKey);
|
|
1004
|
-
const
|
|
1005
|
-
const maxDepth = parentManifestForDepth?.max_depth || rs.max_depth || 50;
|
|
1101
|
+
const maxDepth = manifest?.max_depth || rs.max_depth || 50;
|
|
1006
1102
|
if (rs.depth >= maxDepth) {
|
|
1007
1103
|
deps.log('WARN', `Reactive: depth ${rs.depth} >= ${maxDepth}, pausing ${parentKey} (via member ${targetProject})`);
|
|
1008
1104
|
setReactiveStatus(st, parentKey, 'paused', 'depth_exceeded');
|
|
@@ -1084,11 +1180,16 @@ function handleReactiveOutput(targetProject, output, config, deps) {
|
|
|
1084
1180
|
const parentManifest = parentCwd ? loadProjectManifest(parentCwd) : null;
|
|
1085
1181
|
const signal = parentManifest?.completion_signal || 'MISSION_COMPLETE';
|
|
1086
1182
|
const summary = extractOutputSummary(output);
|
|
1183
|
+
const parentWorkingMemory = loadWorkingMemory(parentKey, deps.metameDir);
|
|
1087
1184
|
deps.handleDispatchItem({
|
|
1088
1185
|
target: parentKey,
|
|
1089
|
-
prompt:
|
|
1186
|
+
prompt: buildReactivePrompt(
|
|
1187
|
+
`[${targetProject} delivery]${verifierBlock}\n\n${summary}\n\nDecide next step.`,
|
|
1188
|
+
{ depth: rs.depth, maxDepth: manifest?.max_depth || rs.max_depth || 50, completionSignal: signal, workingMemory: parentWorkingMemory }
|
|
1189
|
+
),
|
|
1090
1190
|
from: targetProject,
|
|
1091
1191
|
_reactive: true,
|
|
1192
|
+
_reactive_project: parentKey,
|
|
1092
1193
|
new_session: true,
|
|
1093
1194
|
}, config);
|
|
1094
1195
|
}
|
|
@@ -1098,5 +1199,5 @@ module.exports = {
|
|
|
1098
1199
|
parseReactiveSignals,
|
|
1099
1200
|
reconcilePerpetualProjects,
|
|
1100
1201
|
replayEventLog,
|
|
1101
|
-
__test: { runProjectVerifier, readPhaseFromState, resolveProjectCwd, appendEvent, projectProgressTsv, generateStateFile, loadProjectManifest, resolveProjectScripts, parseEventLog, buildRunningMemory, scanRelevantArtifacts, buildWorkingMemory, persistMemoryFiles, extractInlineFacts, extractOutputSummary },
|
|
1202
|
+
__test: { runProjectVerifier, readPhaseFromState, resolveProjectCwd, appendEvent, projectProgressTsv, generateStateFile, loadProjectManifest, resolveProjectScripts, parseEventLog, buildRunningMemory, scanRelevantArtifacts, buildWorkingMemory, persistMemoryFiles, extractInlineFacts, extractOutputSummary, isReactiveExecutionActive, loadWorkingMemory },
|
|
1102
1203
|
};
|