metame-cli 1.5.19 → 1.5.21
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 +92 -38
- package/scripts/daemon-claude-engine.js +373 -444
- package/scripts/daemon-command-router.js +82 -8
- package/scripts/daemon-engine-runtime.js +7 -10
- package/scripts/daemon-reactive-lifecycle.js +100 -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 +21 -175
- 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;
|
|
@@ -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
|
}
|
|
@@ -654,7 +722,7 @@ function createCommandRouter(deps) {
|
|
|
654
722
|
}
|
|
655
723
|
const btwPrompt = `[Side question — answer concisely from existing context, no need for tools]\n\n${btwQuestion}`;
|
|
656
724
|
resetCooldown(chatId);
|
|
657
|
-
await askClaude(bot, chatId, btwPrompt, config, true, senderId
|
|
725
|
+
await askClaude(bot, chatId, btwPrompt, config, true, senderId);
|
|
658
726
|
return;
|
|
659
727
|
}
|
|
660
728
|
|
|
@@ -722,12 +790,17 @@ 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
|
-
await askClaude(bot, chatId, '继续上面的工作', config, readOnly, senderId
|
|
803
|
+
await askClaude(bot, chatId, '继续上面的工作', config, readOnly, senderId);
|
|
731
804
|
return;
|
|
732
805
|
}
|
|
733
806
|
// No session found — fall through to normal askClaude
|
|
@@ -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)
|
|
@@ -775,7 +849,7 @@ function createCommandRouter(deps) {
|
|
|
775
849
|
await bot.sendMessage(chatId, 'Daily token budget exceeded.');
|
|
776
850
|
return;
|
|
777
851
|
}
|
|
778
|
-
const claudeResult = await askClaude(bot, chatId, text, config, readOnly, senderId
|
|
852
|
+
const claudeResult = await askClaude(bot, chatId, text, config, readOnly, senderId);
|
|
779
853
|
const claudeFailed = !!(claudeResult && claudeResult.ok === false);
|
|
780
854
|
const claudeAborted = !!(claudeResult && claudeResult.error === 'Stopped by user');
|
|
781
855
|
if (claudeFailed && !claudeAborted && !macLocalFirst && macFallbackEnabled && allowLocalMacControl) {
|
|
@@ -19,12 +19,12 @@ const ENGINE_TIMEOUT_DEFAULTS = Object.freeze({
|
|
|
19
19
|
codex: Object.freeze({
|
|
20
20
|
idleMs: 10 * 60 * 1000,
|
|
21
21
|
toolMs: 25 * 60 * 1000,
|
|
22
|
-
ceilingMs:
|
|
22
|
+
ceilingMs: null,
|
|
23
23
|
}),
|
|
24
24
|
claude: Object.freeze({
|
|
25
25
|
idleMs: 5 * 60 * 1000,
|
|
26
26
|
toolMs: 25 * 60 * 1000,
|
|
27
|
-
ceilingMs:
|
|
27
|
+
ceilingMs: null,
|
|
28
28
|
}),
|
|
29
29
|
});
|
|
30
30
|
|
|
@@ -282,14 +282,10 @@ function parseCodexStreamEvent(line) {
|
|
|
282
282
|
return out;
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
-
function resolveEngineTimeouts(engineName
|
|
285
|
+
function resolveEngineTimeouts(engineName) {
|
|
286
286
|
const engine = normalizeEngineName(engineName);
|
|
287
287
|
const base = ENGINE_TIMEOUT_DEFAULTS[engine] || ENGINE_TIMEOUT_DEFAULTS.claude;
|
|
288
|
-
|
|
289
|
-
return {
|
|
290
|
-
...base,
|
|
291
|
-
ceilingMs: null,
|
|
292
|
-
};
|
|
288
|
+
return { ...base };
|
|
293
289
|
}
|
|
294
290
|
|
|
295
291
|
function buildClaudeArgs(options = {}) {
|
|
@@ -415,7 +411,7 @@ function buildCodexArgs(options = {}) {
|
|
|
415
411
|
return args;
|
|
416
412
|
}
|
|
417
413
|
|
|
418
|
-
function buildCodexEnv(baseEnv = {}, { metameProject = '', metameSenderId = '' } = {}) {
|
|
414
|
+
function buildCodexEnv(baseEnv = {}, { metameProject = '', metameSenderId = '', cwd = '' } = {}) {
|
|
419
415
|
const env = { ...baseEnv, METAME_PROJECT: metameProject, METAME_SENDER_ID: String(metameSenderId || '') };
|
|
420
416
|
const strippedKeys = [
|
|
421
417
|
'CODEX_THREAD_ID',
|
|
@@ -423,6 +419,7 @@ function buildCodexEnv(baseEnv = {}, { metameProject = '', metameSenderId = '' }
|
|
|
423
419
|
'CLAUDE_CODE_SSE_PORT',
|
|
424
420
|
];
|
|
425
421
|
for (const key of strippedKeys) delete env[key];
|
|
422
|
+
void cwd;
|
|
426
423
|
if (env.CODEX_HOME && !fs.existsSync(env.CODEX_HOME)) delete env.CODEX_HOME;
|
|
427
424
|
return env;
|
|
428
425
|
}
|
|
@@ -446,7 +443,7 @@ function createEngineRuntimeFactory(deps = {}) {
|
|
|
446
443
|
killSignal: 'SIGTERM',
|
|
447
444
|
timeouts: resolveEngineTimeouts('codex'),
|
|
448
445
|
buildArgs: buildCodexArgs,
|
|
449
|
-
buildEnv: ({ metameProject = '', metameSenderId = '' } = {}) => buildCodexEnv(process.env, { metameProject, metameSenderId }),
|
|
446
|
+
buildEnv: ({ metameProject = '', metameSenderId = '', cwd = '' } = {}) => buildCodexEnv(process.env, { metameProject, metameSenderId, cwd }),
|
|
450
447
|
parseStreamEvent: parseCodexStreamEvent,
|
|
451
448
|
classifyError: classifyEngineError,
|
|
452
449
|
};
|
|
@@ -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 ──────────────────────────────────────────────
|
|
@@ -198,6 +200,24 @@ function readPhaseFromState(statePath) {
|
|
|
198
200
|
} catch { return ''; }
|
|
199
201
|
}
|
|
200
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
|
+
|
|
201
221
|
/**
|
|
202
222
|
* Run project-level verifier script if it exists.
|
|
203
223
|
* Returns parsed JSON result or null if no verifier / error.
|
|
@@ -211,7 +231,8 @@ function runProjectVerifier(projectKey, config, deps) {
|
|
|
211
231
|
if (!fs.existsSync(scripts.verifier)) return null;
|
|
212
232
|
|
|
213
233
|
const metameDir = deps.metameDir || path.join(os.homedir(), '.metame');
|
|
214
|
-
const
|
|
234
|
+
const paths = resolveReactivePaths(projectKey, metameDir);
|
|
235
|
+
const statePath = paths.state;
|
|
215
236
|
|
|
216
237
|
// Read phase from event log (SoT), fall back to state file for backward compat
|
|
217
238
|
const { phase: eventPhase } = replayEventLog(projectKey, deps);
|
|
@@ -250,10 +271,9 @@ function runCompletionHooks(projectKey, projectCwd, deps) {
|
|
|
250
271
|
// 1. Archive (if script exists)
|
|
251
272
|
if (fs.existsSync(scripts.archiver)) {
|
|
252
273
|
try {
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
);
|
|
274
|
+
const metameDir = deps.metameDir || path.join(os.homedir(), '.metame');
|
|
275
|
+
const rPaths = resolveReactivePaths(projectKey, metameDir);
|
|
276
|
+
const statePath = rPaths.state;
|
|
257
277
|
let projectName = projectKey;
|
|
258
278
|
try {
|
|
259
279
|
const stateContent = fs.readFileSync(statePath, 'utf8');
|
|
@@ -331,12 +351,13 @@ function runCompletionHooks(projectKey, projectCwd, deps) {
|
|
|
331
351
|
|
|
332
352
|
/**
|
|
333
353
|
* Append an event to the project's event log.
|
|
334
|
-
* Daemon-exclusive: agents cannot write to ~/.metame/
|
|
354
|
+
* Daemon-exclusive: agents cannot write to ~/.metame/reactive/<key>/.
|
|
335
355
|
*/
|
|
336
356
|
function appendEvent(projectKey, event, metameDir) {
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
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;
|
|
340
361
|
const line = JSON.stringify({ ts: new Date().toISOString(), ...event }) + '\n';
|
|
341
362
|
fs.appendFileSync(logPath, line, 'utf8');
|
|
342
363
|
}
|
|
@@ -350,8 +371,9 @@ function appendEvent(projectKey, event, metameDir) {
|
|
|
350
371
|
* This function NEVER throws.
|
|
351
372
|
*/
|
|
352
373
|
function replayEventLog(projectKey, deps) {
|
|
353
|
-
const
|
|
354
|
-
const
|
|
374
|
+
const base = deps?.metameDir || path.join(os.homedir(), '.metame');
|
|
375
|
+
const paths = resolveReactivePaths(projectKey, base);
|
|
376
|
+
const logPath = paths.events;
|
|
355
377
|
if (!fs.existsSync(logPath)) return { phase: '', mission: null, history: [] };
|
|
356
378
|
|
|
357
379
|
const lines = fs.readFileSync(logPath, 'utf8').trim().split('\n').filter(Boolean);
|
|
@@ -392,8 +414,9 @@ function projectProgressTsv(projectCwd, projectKey, metameDir) {
|
|
|
392
414
|
const tsvPath = path.join(projectCwd, 'workspace', 'progress.tsv');
|
|
393
415
|
const header = 'phase\tresult\tverifier_passed\tartifact\ttimestamp\tnotes\n';
|
|
394
416
|
|
|
395
|
-
const
|
|
396
|
-
const
|
|
417
|
+
const base = metameDir || path.join(os.homedir(), '.metame');
|
|
418
|
+
const paths = resolveReactivePaths(projectKey, base);
|
|
419
|
+
const logPath = paths.events;
|
|
397
420
|
if (!fs.existsSync(logPath)) return;
|
|
398
421
|
|
|
399
422
|
const lines = fs.readFileSync(logPath, 'utf8').trim().split('\n').filter(Boolean);
|
|
@@ -430,7 +453,8 @@ function projectProgressTsv(projectCwd, projectKey, metameDir) {
|
|
|
430
453
|
*/
|
|
431
454
|
function generateStateFile(projectKey, config, deps) {
|
|
432
455
|
const metameDir = deps.metameDir || path.join(os.homedir(), '.metame');
|
|
433
|
-
const
|
|
456
|
+
const paths = resolveReactivePaths(projectKey, metameDir);
|
|
457
|
+
const statePath = paths.state;
|
|
434
458
|
|
|
435
459
|
const { phase, mission, history } = replayEventLog(projectKey, deps);
|
|
436
460
|
|
|
@@ -512,8 +536,8 @@ function reconcilePerpetualProjects(config, deps) {
|
|
|
512
536
|
*/
|
|
513
537
|
function parseEventLog(projectKey, deps) {
|
|
514
538
|
const metameDir = deps.metameDir || path.join(os.homedir(), '.metame');
|
|
515
|
-
const
|
|
516
|
-
const logPath =
|
|
539
|
+
const paths = resolveReactivePaths(projectKey, metameDir);
|
|
540
|
+
const logPath = paths.events;
|
|
517
541
|
if (!fs.existsSync(logPath)) return [];
|
|
518
542
|
|
|
519
543
|
const raw = fs.readFileSync(logPath, 'utf8').trim().split('\n').filter(Boolean);
|
|
@@ -720,9 +744,9 @@ function buildWorkingMemory(projectKey, config, deps) {
|
|
|
720
744
|
*/
|
|
721
745
|
function persistMemoryFiles(projectKey, config, deps, opts = {}) {
|
|
722
746
|
const metameDir = deps.metameDir || path.join(os.homedir(), '.metame');
|
|
723
|
-
const
|
|
724
|
-
fs.mkdirSync(
|
|
725
|
-
const memPath =
|
|
747
|
+
const paths = resolveReactivePaths(projectKey, metameDir);
|
|
748
|
+
fs.mkdirSync(paths.dir, { recursive: true });
|
|
749
|
+
const memPath = paths.memory;
|
|
726
750
|
|
|
727
751
|
// Single parse of event log — shared across L1 and round counting
|
|
728
752
|
const events = parseEventLog(projectKey, deps);
|
|
@@ -755,15 +779,13 @@ function persistMemoryFiles(projectKey, config, deps, opts = {}) {
|
|
|
755
779
|
l2 = buildWorkingMemory(projectKey, config, deps);
|
|
756
780
|
// Stash L2 for next time
|
|
757
781
|
try {
|
|
758
|
-
|
|
759
|
-
fs.writeFileSync(l2CachePath, l2, 'utf8');
|
|
782
|
+
fs.writeFileSync(paths.l2cache, l2, 'utf8');
|
|
760
783
|
} catch { /* non-critical */ }
|
|
761
784
|
} else {
|
|
762
785
|
// Read stale L2 from cache
|
|
763
786
|
try {
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
l2 = fs.readFileSync(l2CachePath, 'utf8').trim();
|
|
787
|
+
if (fs.existsSync(paths.l2cache)) {
|
|
788
|
+
l2 = fs.readFileSync(paths.l2cache, 'utf8').trim();
|
|
767
789
|
}
|
|
768
790
|
} catch { /* non-critical */ }
|
|
769
791
|
}
|
|
@@ -911,12 +933,51 @@ function handleReactiveOutput(targetProject, output, config, deps) {
|
|
|
911
933
|
|
|
912
934
|
// ── Case 1: targetProject is a reactive parent ──
|
|
913
935
|
if (isReactiveParent(targetProject, config)) {
|
|
914
|
-
if (!hasSignals) return;
|
|
915
|
-
|
|
916
936
|
const projectKey = targetProject;
|
|
917
937
|
const pName = config.projects[projectKey]?.name || projectKey;
|
|
918
938
|
const st = deps.loadState();
|
|
919
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
|
|
920
981
|
|
|
921
982
|
// Mission complete takes priority
|
|
922
983
|
if (signals.complete) {
|
|
@@ -997,11 +1058,14 @@ function handleReactiveOutput(targetProject, output, config, deps) {
|
|
|
997
1058
|
deps.saveState(st);
|
|
998
1059
|
|
|
999
1060
|
// Dispatch each directive with fresh session
|
|
1061
|
+
const workingMemory = loadWorkingMemory(projectKey, deps.metameDir);
|
|
1000
1062
|
for (const d of signals.directives) {
|
|
1001
1063
|
logEvent(projectKey, { type: 'DISPATCH', target: d.target, prompt: d.prompt.slice(0, 200) });
|
|
1002
1064
|
deps.handleDispatchItem({
|
|
1003
1065
|
target: d.target,
|
|
1004
|
-
prompt: d.prompt,
|
|
1066
|
+
prompt: buildReactivePrompt(d.prompt, {
|
|
1067
|
+
depth: rs.depth, maxDepth, completionSignal, workingMemory,
|
|
1068
|
+
}),
|
|
1005
1069
|
from: projectKey,
|
|
1006
1070
|
_reactive: true,
|
|
1007
1071
|
_reactive_project: projectKey,
|
|
@@ -1034,8 +1098,7 @@ function handleReactiveOutput(targetProject, output, config, deps) {
|
|
|
1034
1098
|
|
|
1035
1099
|
// Depth gate (manifest max_depth overrides default)
|
|
1036
1100
|
const rs = getReactiveState(st, parentKey);
|
|
1037
|
-
const
|
|
1038
|
-
const maxDepth = parentManifestForDepth?.max_depth || rs.max_depth || 50;
|
|
1101
|
+
const maxDepth = manifest?.max_depth || rs.max_depth || 50;
|
|
1039
1102
|
if (rs.depth >= maxDepth) {
|
|
1040
1103
|
deps.log('WARN', `Reactive: depth ${rs.depth} >= ${maxDepth}, pausing ${parentKey} (via member ${targetProject})`);
|
|
1041
1104
|
setReactiveStatus(st, parentKey, 'paused', 'depth_exceeded');
|
|
@@ -1117,9 +1180,13 @@ function handleReactiveOutput(targetProject, output, config, deps) {
|
|
|
1117
1180
|
const parentManifest = parentCwd ? loadProjectManifest(parentCwd) : null;
|
|
1118
1181
|
const signal = parentManifest?.completion_signal || 'MISSION_COMPLETE';
|
|
1119
1182
|
const summary = extractOutputSummary(output);
|
|
1183
|
+
const parentWorkingMemory = loadWorkingMemory(parentKey, deps.metameDir);
|
|
1120
1184
|
deps.handleDispatchItem({
|
|
1121
1185
|
target: parentKey,
|
|
1122
|
-
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
|
+
),
|
|
1123
1190
|
from: targetProject,
|
|
1124
1191
|
_reactive: true,
|
|
1125
1192
|
_reactive_project: parentKey,
|
|
@@ -1132,5 +1199,5 @@ module.exports = {
|
|
|
1132
1199
|
parseReactiveSignals,
|
|
1133
1200
|
reconcilePerpetualProjects,
|
|
1134
1201
|
replayEventLog,
|
|
1135
|
-
__test: { runProjectVerifier, readPhaseFromState, resolveProjectCwd, appendEvent, projectProgressTsv, generateStateFile, loadProjectManifest, resolveProjectScripts, parseEventLog, buildRunningMemory, scanRelevantArtifacts, buildWorkingMemory, persistMemoryFiles, extractInlineFacts, extractOutputSummary, isReactiveExecutionActive },
|
|
1202
|
+
__test: { runProjectVerifier, readPhaseFromState, resolveProjectCwd, appendEvent, projectProgressTsv, generateStateFile, loadProjectManifest, resolveProjectScripts, parseEventLog, buildRunningMemory, scanRelevantArtifacts, buildWorkingMemory, persistMemoryFiles, extractInlineFacts, extractOutputSummary, isReactiveExecutionActive, loadWorkingMemory },
|
|
1136
1203
|
};
|