metame-cli 1.4.21 → 1.4.23

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 CHANGED
@@ -1727,12 +1727,22 @@ if (isSync) {
1727
1727
  .sort((a, b) => b.mtime - a.mtime)[0] || null;
1728
1728
  } catch { return null; }
1729
1729
  };
1730
- bestSession = findLatest(projDir);
1731
- if (!bestSession) {
1730
+ const localBest = findLatest(projDir);
1731
+ // Always scan globally to find the absolute most recent session
1732
+ // (phone /continue may have worked in a different project's session)
1733
+ let globalBest = null;
1734
+ try {
1732
1735
  for (const d of fs.readdirSync(projectsRoot)) {
1733
1736
  const s = findLatest(path.join(projectsRoot, d));
1734
- if (s && (!bestSession || s.mtime > bestSession.mtime)) bestSession = s;
1737
+ if (s && (!globalBest || s.mtime > globalBest.mtime)) globalBest = s;
1735
1738
  }
1739
+ } catch { /* ignore */ }
1740
+ // Use global best if it's more recent than local; prefer local otherwise
1741
+ if (localBest && globalBest && globalBest.mtime > localBest.mtime) {
1742
+ bestSession = globalBest;
1743
+ console.log(` (global session is newer than local — using global)`);
1744
+ } else {
1745
+ bestSession = localBest || globalBest;
1736
1746
  }
1737
1747
  } catch { }
1738
1748
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metame-cli",
3
- "version": "1.4.21",
3
+ "version": "1.4.23",
4
4
  "description": "The Cognitive Profile Layer for Claude Code. Knows how you think, not just what you said.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -338,22 +338,61 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
338
338
  let buffer = '';
339
339
  let stderr = '';
340
340
  let killed = false;
341
+ let killedReason = 'idle'; // 'idle' | 'ceiling'
341
342
  let finalResult = '';
342
343
  let lastStatusTime = 0;
343
344
  const STATUS_THROTTLE = statusThrottleMs;
344
345
  const writtenFiles = []; // Track files created/modified by Write tool
345
346
  const toolUsageLog = []; // Track all tool invocations for skill evolution
346
347
 
347
- const timer = setTimeout(() => {
348
+ // ── 自适应超时:5min 无输出判卡死 + 1h 绝对上限 ──
349
+ const IDLE_TIMEOUT_MS = 5 * 60 * 1000;
350
+ const HARD_CEILING_MS = 60 * 60 * 1000;
351
+ const startTime = Date.now();
352
+
353
+ let sigkillTimer = null;
354
+ function killChild(reason) {
355
+ if (killed) return;
348
356
  killed = true;
349
- log('WARN', `Claude timeout (${timeoutMs / 60000}min) for chatId ${chatId} — killing process group`);
357
+ killedReason = reason;
358
+ log('WARN', `Claude ${reason} timeout for chatId ${chatId} — killing process group`);
350
359
  try { process.kill(-child.pid, 'SIGTERM'); } catch { child.kill('SIGTERM'); }
351
- setTimeout(() => {
360
+ sigkillTimer = setTimeout(() => {
352
361
  try { process.kill(-child.pid, 'SIGKILL'); } catch { try { child.kill('SIGKILL'); } catch { } }
353
362
  }, 5000);
354
- }, timeoutMs);
363
+ }
364
+
365
+ let idleTimer = setTimeout(() => killChild('idle'), IDLE_TIMEOUT_MS);
366
+ const ceilingTimer = setTimeout(() => killChild('ceiling'), HARD_CEILING_MS);
367
+
368
+ function resetIdleTimer() {
369
+ clearTimeout(idleTimer);
370
+ idleTimer = setTimeout(() => killChild('idle'), IDLE_TIMEOUT_MS);
371
+ }
372
+
373
+ // ── 进度里程碑:2min 首报,之后每 5min 一次 ──
374
+ let toolCallCount = 0;
375
+ let lastMilestoneMin = 0;
376
+ const milestoneTimer = setInterval(() => {
377
+ if (killed) return;
378
+ const elapsedMin = Math.floor((Date.now() - startTime) / 60000);
379
+ const nextMin = lastMilestoneMin === 0 ? 2 : lastMilestoneMin + 5;
380
+ if (elapsedMin >= nextMin) {
381
+ lastMilestoneMin = elapsedMin;
382
+ const parts = [`⏳ 已运行 ${elapsedMin} 分钟`];
383
+ if (toolCallCount > 0) parts.push(`调用 ${toolCallCount} 次工具`);
384
+ if (writtenFiles.length > 0) parts.push(`修改 ${writtenFiles.length} 个文件`);
385
+ const recentTool = toolUsageLog.length > 0 ? toolUsageLog[toolUsageLog.length - 1] : null;
386
+ if (recentTool) {
387
+ const ctx = recentTool.context || recentTool.skill || '';
388
+ parts.push(`最近: ${recentTool.tool}${ctx ? ' ' + ctx : ''}`);
389
+ }
390
+ if (onStatus) onStatus(parts.join(' | ')).catch(() => { });
391
+ }
392
+ }, 30000);
355
393
 
356
394
  child.stdout.on('data', (data) => {
395
+ resetIdleTimer();
357
396
  buffer += data.toString();
358
397
 
359
398
  // Process complete JSON lines
@@ -377,6 +416,7 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
377
416
  if (event.type === 'assistant' && event.message?.content) {
378
417
  for (const block of event.message.content) {
379
418
  if (block.type === 'tool_use') {
419
+ toolCallCount++;
380
420
  const toolName = block.name || 'Tool';
381
421
 
382
422
  // Track tool usage for skill evolution
@@ -467,10 +507,16 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
467
507
  }
468
508
  });
469
509
 
470
- child.stderr.on('data', (data) => { stderr += data.toString(); });
510
+ child.stderr.on('data', (data) => {
511
+ resetIdleTimer();
512
+ stderr += data.toString();
513
+ });
471
514
 
472
515
  child.on('close', (code) => {
473
- clearTimeout(timer);
516
+ clearTimeout(idleTimer);
517
+ clearTimeout(ceilingTimer);
518
+ clearTimeout(sigkillTimer);
519
+ clearInterval(milestoneTimer);
474
520
 
475
521
  // Process any remaining buffer
476
522
  if (buffer.trim()) {
@@ -490,7 +536,11 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
490
536
  if (wasAborted) {
491
537
  resolve({ output: finalResult || null, error: 'Stopped by user', files: writtenFiles, toolUsageLog });
492
538
  } else if (killed) {
493
- resolve({ output: finalResult || null, error: 'Timeout: Claude took too long', files: writtenFiles, toolUsageLog });
539
+ const elapsed = Math.round((Date.now() - startTime) / 60000);
540
+ const reason = killedReason === 'ceiling'
541
+ ? `⏱ 已运行 ${elapsed} 分钟,达到上限(1 小时)`
542
+ : `⏱ 已 5 分钟无输出,判定卡死(共运行 ${elapsed} 分钟)`;
543
+ resolve({ output: finalResult || null, error: reason, timedOut: true, files: writtenFiles, toolUsageLog });
494
544
  } else if (code !== 0) {
495
545
  resolve({ output: finalResult || null, error: stderr || `Exit code ${code}`, files: writtenFiles, toolUsageLog });
496
546
  } else {
@@ -499,7 +549,10 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
499
549
  });
500
550
 
501
551
  child.on('error', (err) => {
502
- clearTimeout(timer);
552
+ clearTimeout(idleTimer);
553
+ clearTimeout(ceilingTimer);
554
+ clearTimeout(sigkillTimer);
555
+ clearInterval(milestoneTimer);
503
556
  if (chatId) { activeProcesses.delete(chatId); saveActivePids(); } // Fix3
504
557
  resolve({ output: null, error: err.message, files: [], toolUsageLog: [] });
505
558
  });
@@ -851,9 +904,9 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
851
904
  } catch { /* ignore status update failures */ }
852
905
  };
853
906
 
854
- let output, error, files, toolUsageLog;
907
+ let output, error, files, toolUsageLog, timedOut;
855
908
  try {
856
- ({ output, error, files, toolUsageLog } = await spawnClaudeStreaming(args, fullPrompt, session.cwd, onStatus, 600000, chatId, boundProjectKey || ''));
909
+ ({ output, error, timedOut, files, toolUsageLog } = await spawnClaudeStreaming(args, fullPrompt, session.cwd, onStatus, 600000, chatId, boundProjectKey || ''));
857
910
  } catch (spawnErr) {
858
911
  clearInterval(typingTimer);
859
912
  if (statusMsgId && bot.deleteMessage) bot.deleteMessage(chatId, statusMsgId).catch(() => { });
@@ -932,7 +985,12 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
932
985
  recordTokens(loadState(), estimated, { category: chatCategory });
933
986
 
934
987
  // Parse [[FILE:...]] markers from output (Claude's explicit file sends)
935
- const { markedFiles, cleanOutput } = parseFileMarkers(output);
988
+ let { markedFiles, cleanOutput } = parseFileMarkers(output);
989
+
990
+ // Timeout with partial results: prepend warning
991
+ if (timedOut) {
992
+ cleanOutput = `⚠️ **任务超时,以下是已完成的部分结果:**\n\n${cleanOutput}`;
993
+ }
936
994
 
937
995
  // Match current session to a project for colored card display
938
996
  let activeProject = null;
@@ -966,11 +1024,16 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
966
1024
 
967
1025
  await sendFileButtons(bot, chatId, mergeFileCollections(markedFiles, files));
968
1026
 
1027
+ // Timeout: also send the reason after the partial result
1028
+ if (timedOut && error) {
1029
+ try { await bot.sendMessage(chatId, error); } catch { /* */ }
1030
+ }
1031
+
969
1032
  // Auto-name: if this was the first message and session has no name, generate one
970
1033
  if (wasNew && !getSessionName(session.id)) {
971
1034
  autoNameSession(chatId, session.id, prompt, session.cwd).catch(() => { });
972
1035
  }
973
- return { ok: true };
1036
+ return { ok: !timedOut };
974
1037
  } else {
975
1038
  const errMsg = error || 'Unknown error';
976
1039
  log('ERROR', `askClaude failed for ${chatId}: ${errMsg.slice(0, 300)}`);
@@ -326,8 +326,26 @@ function createSessionCommandHandler(deps) {
326
326
  const excludeId = currentSession?.id;
327
327
  const recent = listRecentSessions(10);
328
328
  const filtered = excludeId ? recent.filter(s => s.sessionId !== excludeId) : recent;
329
- if (filtered.length > 0 && filtered[0].projectPath) {
330
- const target = filtered[0];
329
+
330
+ // For bound chats, prefer sessions from the same project to avoid
331
+ // the bound-chat guard (handleCommand) immediately overwriting with a new session.
332
+ let boundCwd = null;
333
+ try {
334
+ const cfg = loadConfig();
335
+ const chatAgentMap = { ...(cfg.telegram ? cfg.telegram.chat_agent_map : {}), ...(cfg.feishu ? cfg.feishu.chat_agent_map : {}) };
336
+ const mappedKey = chatAgentMap[String(chatId)];
337
+ const proj = mappedKey && cfg.projects ? cfg.projects[mappedKey] : null;
338
+ if (proj && proj.cwd) boundCwd = normalizeCwd(proj.cwd);
339
+ } catch { /* ignore */ }
340
+
341
+ let candidates = filtered;
342
+ if (boundCwd) {
343
+ const boundFiltered = filtered.filter(s => s.projectPath && normalizeCwd(s.projectPath) === boundCwd);
344
+ if (boundFiltered.length > 0) candidates = boundFiltered;
345
+ }
346
+
347
+ if (candidates.length > 0 && candidates[0].projectPath) {
348
+ const target = candidates[0];
331
349
  // Switch to that session (like /resume) AND its directory
332
350
  const state2 = loadState();
333
351
  state2.sessions[chatId] = {
@@ -275,12 +275,9 @@ function createTaskScheduler(deps) {
275
275
  // Precondition gate: run a cheap shell check before burning tokens
276
276
  const precheck = checkPrecondition(task);
277
277
  if (!precheck.pass) {
278
- state.tasks[task.name] = {
279
- last_run: new Date().toISOString(),
280
- status: 'skipped',
281
- output_preview: 'Precondition not met — no activity',
282
- };
283
- saveState(state);
278
+ // Don't update state a skipped precondition is not a run.
279
+ // Preserves existing success/error status and keeps last_run accurate
280
+ // so interval math in computeInitialNextRun stays correct.
284
281
  return { success: true, output: '(skipped — no activity)', skipped: true };
285
282
  }
286
283