evolcore 0.0.34 → 0.0.35
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/CHANGELOG.md +22 -0
- package/bin/install-codex-managed-hooks.mjs +26 -3
- package/dist/agents/claude-runner.js +11 -1
- package/dist/agents/codex/diagnostics.js +42 -11
- package/dist/agents/codex-runner.js +27 -3
- package/dist/agents/ecagent-runner.js +17 -7
- package/dist/aun/aid/agentmd.js +13 -2
- package/dist/aun/aid/client.js +1 -1
- package/dist/aun/msg/managed-operation.js +16 -0
- package/dist/aun/msg/p2p.js +18 -2
- package/dist/channels/aun.js +113 -7
- package/dist/cli/aun-commands.js +37 -13
- package/dist/cli/daemon-commands.js +10 -4
- package/dist/cli/fs-command.js +1 -1
- package/dist/cli/getmeta-command.js +211 -0
- package/dist/cli/index.js +6 -0
- package/dist/cli/managed-command-guard.js +3 -0
- package/dist/cli/memory-command.js +46 -15
- package/dist/cli/thread-command.js +12 -4
- package/dist/config/resolved-config-op.js +25 -8
- package/dist/core/auth/operation-authorizer.js +21 -2
- package/dist/core/channel-runtime.js +1 -1
- package/dist/core/command/command-handler.js +7 -1
- package/dist/core/command/menu-handler.js +2 -0
- package/dist/core/command/topic-slash.js +4 -7
- package/dist/core/event-catalog.js +14 -0
- package/dist/core/ipc-registration.js +11 -1
- package/dist/core/memory/batch-meta.js +1 -0
- package/dist/core/memory/consolidation-context.js +97 -14
- package/dist/core/memory/consolidation-trigger.js +15 -7
- package/dist/core/memory/consolidation.js +55 -0
- package/dist/core/memory/memory-writer.js +20 -4
- package/dist/core/message/aun-ipc-sender.js +19 -1
- package/dist/core/message/message-bridge.js +3 -3
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/peer-mode.js +17 -0
- package/dist/core/message/response-engine.js +15 -4
- package/dist/core/permission/ec-command-parser.js +76 -40
- package/dist/core/permission/tool-policy.js +32 -2
- package/dist/core/relation/peer-identity.js +87 -12
- package/dist/core/session/session-manager.js +56 -1
- package/dist/core/session/session-title.js +26 -1
- package/dist/core/system-session/memory.js +87 -20
- package/dist/core/thread/command-parser.js +6 -1
- package/dist/core/thread/ipc-operation.js +33 -16
- package/dist/core/thread/thread-operation.js +62 -24
- package/dist/core/thread/thread-service.js +246 -32
- package/dist/utils/cross-platform.js +6 -3
- package/kits/docs/evolcore/fs-architecture.md +5 -5
- package/kits/docs/evolcore/fs.md +1 -1
- package/kits/docs/evolcore/group-collaboration.md +1 -1
- package/kits/docs/evolcore/group.md +5 -3
- package/kits/docs/evolcore/memory-consolidation.md +1 -2
- package/kits/docs/evolcore/memory-tags.md +1 -1
- package/kits/docs/evolcore/memory.md +2 -2
- package/kits/docs/evolcore/stats.md +1 -1
- package/kits/docs/evolcore/thread-chain.md +25 -9
- package/kits/eck_manifest.auxiliary.json +9 -0
- package/kits/eck_manifest.json +20 -0
- package/kits/eck_manifest.memory.json +20 -0
- package/kits/rules/01-overview.md +3 -3
- package/kits/rules/07-memory-and-thread.md +1 -1
- package/kits/skills/ec-group/SKILL.md +6 -3
- package/kits/skills/thread-ops/SKILL.md +18 -18
- package/kits/templates/memory-consolidation/notes.md +1 -1
- package/kits/templates/memory-consolidation/workflow.md +36 -1
- package/kits/templates/system-fragments/bash-description.md +9 -0
- package/kits/templates/system-fragments/memory.md +2 -2
- package/package.json +1 -1
|
@@ -619,6 +619,10 @@ function isLiteralArgBoundary(value) {
|
|
|
619
619
|
* 判为 `shell-composition`,拦截力度不变,只让错误归因准确。
|
|
620
620
|
*/
|
|
621
621
|
function isPosixQuotedTokenBoundary(value) {
|
|
622
|
+
// 相邻引号段直接拼接(`'a''b'`、`$'a''b'`、`'a'"b"`)与 bash 一致:
|
|
623
|
+
// 闭合引号后紧跟另一个开引号不是错误,两个段拼进同一个 token。
|
|
624
|
+
if (value === "'" || value === '"')
|
|
625
|
+
return true;
|
|
622
626
|
return value === undefined || isShellSpacing(value) || SHELL_UNQUOTED_META.has(value);
|
|
623
627
|
}
|
|
624
628
|
function isShellParameterExpansionStart(value) {
|
|
@@ -1657,6 +1661,54 @@ export function parseLiteralShellCommand(command, options = {}) {
|
|
|
1657
1661
|
token += next;
|
|
1658
1662
|
continue;
|
|
1659
1663
|
}
|
|
1664
|
+
// ANSI-C quoting: $'...' allows \n \t \r \\ \' escapes, same as bash $'...'.
|
|
1665
|
+
// Must be checked before SHELL_UNQUOTED_META because $ is in that set.
|
|
1666
|
+
if (char === '$' && input[index + 1] === "'") {
|
|
1667
|
+
tokenStarted = true;
|
|
1668
|
+
index += 2; // skip $ and opening '
|
|
1669
|
+
let ansiClosed = false;
|
|
1670
|
+
while (index < input.length) {
|
|
1671
|
+
const c = input[index];
|
|
1672
|
+
if (c === "'") {
|
|
1673
|
+
ansiClosed = true;
|
|
1674
|
+
break; // leave index at closing '; for-loop increment moves past it
|
|
1675
|
+
}
|
|
1676
|
+
if (c === '\\') {
|
|
1677
|
+
const esc = input[++index];
|
|
1678
|
+
if (esc === undefined)
|
|
1679
|
+
break;
|
|
1680
|
+
switch (esc) {
|
|
1681
|
+
case 'n':
|
|
1682
|
+
token += '\n';
|
|
1683
|
+
break;
|
|
1684
|
+
case 't':
|
|
1685
|
+
token += '\t';
|
|
1686
|
+
break;
|
|
1687
|
+
case 'r':
|
|
1688
|
+
token += '\r';
|
|
1689
|
+
break;
|
|
1690
|
+
case '\\':
|
|
1691
|
+
token += '\\';
|
|
1692
|
+
break;
|
|
1693
|
+
case "'":
|
|
1694
|
+
token += "'";
|
|
1695
|
+
break;
|
|
1696
|
+
default:
|
|
1697
|
+
token += `\\${esc}`;
|
|
1698
|
+
break;
|
|
1699
|
+
}
|
|
1700
|
+
index++;
|
|
1701
|
+
continue;
|
|
1702
|
+
}
|
|
1703
|
+
token += c;
|
|
1704
|
+
index++;
|
|
1705
|
+
}
|
|
1706
|
+
if (!ansiClosed)
|
|
1707
|
+
return parseFailure('invalid-quote', input, index, argv.length, 'posix');
|
|
1708
|
+
if (!isPosixQuotedTokenBoundary(input[index + 1]))
|
|
1709
|
+
return parseFailure('invalid-quote', input, index, argv.length, 'posix');
|
|
1710
|
+
continue;
|
|
1711
|
+
}
|
|
1660
1712
|
// `#` starts a shell comment only at the beginning of a word. Channel
|
|
1661
1713
|
// peer keys such as `aun#alice` are ordinary literal arguments.
|
|
1662
1714
|
if ((SHELL_UNQUOTED_META.has(char) && (char !== '#' || !tokenStarted))
|
|
@@ -1712,6 +1764,11 @@ export function parseCodexShellCommandArgv(command, options = {}) {
|
|
|
1712
1764
|
});
|
|
1713
1765
|
return parsed.ok ? canonicalizeEvolcoreCommandArgv(parsed.argv) : null;
|
|
1714
1766
|
}
|
|
1767
|
+
const stringCarrier = resolveCodexShellCarrierString(command);
|
|
1768
|
+
if (stringCarrier) {
|
|
1769
|
+
const parsed = parseCodexCarrierCommand(stringCarrier, options);
|
|
1770
|
+
return parsed.ok ? normalizeEvolcoreCommandArgv(parsed.argv) : null;
|
|
1771
|
+
}
|
|
1715
1772
|
const outer = parseLiteralShellArgv(command, options);
|
|
1716
1773
|
if (outer) {
|
|
1717
1774
|
const carrier = resolveCodexShellCarrierArgv(outer);
|
|
@@ -1719,20 +1776,11 @@ export function parseCodexShellCommandArgv(command, options = {}) {
|
|
|
1719
1776
|
const parsed = parseCodexCarrierCommand(carrier, options);
|
|
1720
1777
|
return parsed.ok ? parsed.argv : null;
|
|
1721
1778
|
}
|
|
1722
|
-
const stringCarrier = resolveCodexShellCarrierString(command);
|
|
1723
|
-
if (stringCarrier) {
|
|
1724
|
-
const parsed = parseCodexCarrierCommand(stringCarrier, options);
|
|
1725
|
-
return parsed.ok ? parsed.argv : null;
|
|
1726
|
-
}
|
|
1727
1779
|
if (parseCodexCmdCarrierStringFailure(command, options))
|
|
1728
1780
|
return null;
|
|
1729
1781
|
return normalizeEvolcoreCommandArgv(outer);
|
|
1730
1782
|
}
|
|
1731
|
-
|
|
1732
|
-
if (!stringCarrier)
|
|
1733
|
-
return null;
|
|
1734
|
-
const parsed = parseCodexCarrierCommand(stringCarrier, options);
|
|
1735
|
-
return parsed.ok ? normalizeEvolcoreCommandArgv(parsed.argv) : null;
|
|
1783
|
+
return null;
|
|
1736
1784
|
}
|
|
1737
1785
|
/** Parse a Codex tool input while preserving safe syntax failure metadata. */
|
|
1738
1786
|
export function parseCodexToolCommand(input, options = {}) {
|
|
@@ -1776,22 +1824,20 @@ export function parseCodexToolCommand(input, options = {}) {
|
|
|
1776
1824
|
? { ...parsed, argv: normalizeEvolcoreCommandArgv(parsed.argv) }
|
|
1777
1825
|
: parsed;
|
|
1778
1826
|
}
|
|
1827
|
+
const stringCarrier = resolveCodexShellCarrierString(command);
|
|
1828
|
+
if (stringCarrier) {
|
|
1829
|
+
return parseCodexCarrierCommand(stringCarrier, { allowManagedTmpDir: true, ...options });
|
|
1830
|
+
}
|
|
1779
1831
|
const outer = parseLiteralShellCommand(command, { allowManagedTmpDir: true });
|
|
1780
1832
|
if (outer.ok) {
|
|
1781
1833
|
const carrier = resolveCodexShellCarrierArgv(outer.argv);
|
|
1782
1834
|
if (carrier !== undefined) {
|
|
1783
1835
|
return parseCodexCarrierCommand(carrier, { allowManagedTmpDir: true, ...options });
|
|
1784
1836
|
}
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
? parseCodexCarrierCommand(stringCarrier, { allowManagedTmpDir: true, ...options })
|
|
1788
|
-
: parseCodexCmdCarrierStringFailure(command, { allowManagedTmpDir: true })
|
|
1789
|
-
?? { ...outer, argv: normalizeEvolcoreCommandArgv(outer.argv) };
|
|
1837
|
+
return parseCodexCmdCarrierStringFailure(command, { allowManagedTmpDir: true })
|
|
1838
|
+
?? { ...outer, argv: normalizeEvolcoreCommandArgv(outer.argv) };
|
|
1790
1839
|
}
|
|
1791
|
-
|
|
1792
|
-
return stringCarrier
|
|
1793
|
-
? parseCodexCarrierCommand(stringCarrier, { allowManagedTmpDir: true, ...options })
|
|
1794
|
-
: parseCodexCmdCarrierStringFailure(command, { allowManagedTmpDir: true }) ?? outer;
|
|
1840
|
+
return parseCodexCmdCarrierStringFailure(command, { allowManagedTmpDir: true }) ?? outer;
|
|
1795
1841
|
}
|
|
1796
1842
|
function isApplyPatchExecutable(value) {
|
|
1797
1843
|
const normalized = value.replaceAll('\\', '/').replace(/\/+$/u, '');
|
|
@@ -2747,6 +2793,17 @@ export function classifyEvolcoreShellCommand(command, options = {}) {
|
|
|
2747
2793
|
});
|
|
2748
2794
|
return { kind: 'composite', issue: parsed.ok ? 'shell-composition' : parsed.issue };
|
|
2749
2795
|
}
|
|
2796
|
+
const stringCarrier = resolveCodexShellCarrierString(stripped);
|
|
2797
|
+
if (stringCarrier) {
|
|
2798
|
+
const nested = parseCodexCarrierCommand(stringCarrier, parseOptions);
|
|
2799
|
+
if (nested.ok && nested.argv[0] === 'ec')
|
|
2800
|
+
return { kind: 'literal' };
|
|
2801
|
+
if (!nested.ok && (hasCarrierEcCommandPrefix(stringCarrier.command, stringCarrier.dialect)
|
|
2802
|
+
|| (stringCarrier.dialect === 'powershell' && hasPowerShellCredentialAssignment(stringCarrier.command)))) {
|
|
2803
|
+
return { kind: 'composite', issue: nested.issue };
|
|
2804
|
+
}
|
|
2805
|
+
return { kind: 'none' };
|
|
2806
|
+
}
|
|
2750
2807
|
const literal = parseLiteralShellCommand(stripped, parseOptions);
|
|
2751
2808
|
if (literal.ok) {
|
|
2752
2809
|
if (normalizeEvolcoreCommandArgv(literal.argv)[0] === 'ec')
|
|
@@ -2761,16 +2818,6 @@ export function classifyEvolcoreShellCommand(command, options = {}) {
|
|
|
2761
2818
|
return { kind: 'composite', issue: nested.issue };
|
|
2762
2819
|
}
|
|
2763
2820
|
}
|
|
2764
|
-
const stringCarrier = resolveCodexShellCarrierString(stripped);
|
|
2765
|
-
if (stringCarrier) {
|
|
2766
|
-
const nested = parseCodexCarrierCommand(stringCarrier, parseOptions);
|
|
2767
|
-
if (nested.ok && nested.argv[0] === 'ec')
|
|
2768
|
-
return { kind: 'literal' };
|
|
2769
|
-
if (!nested.ok && (hasCarrierEcCommandPrefix(stringCarrier.command, stringCarrier.dialect)
|
|
2770
|
-
|| (stringCarrier.dialect === 'powershell' && hasPowerShellCredentialAssignment(stringCarrier.command)))) {
|
|
2771
|
-
return { kind: 'composite', issue: nested.issue };
|
|
2772
|
-
}
|
|
2773
|
-
}
|
|
2774
2821
|
const malformedCmdBody = extractCodexCmdCarrierBody(stripped);
|
|
2775
2822
|
if (malformedCmdBody && looksLikeEcCommandBody(malformedCmdBody)) {
|
|
2776
2823
|
const nested = parseLiteralShellCommand(malformedCmdBody, {
|
|
@@ -2783,17 +2830,6 @@ export function classifyEvolcoreShellCommand(command, options = {}) {
|
|
|
2783
2830
|
}
|
|
2784
2831
|
return { kind: 'none' };
|
|
2785
2832
|
}
|
|
2786
|
-
const stringCarrier = resolveCodexShellCarrierString(stripped);
|
|
2787
|
-
if (stringCarrier) {
|
|
2788
|
-
const nested = parseCodexCarrierCommand(stringCarrier, parseOptions);
|
|
2789
|
-
if (nested.ok && nested.argv[0] === 'ec')
|
|
2790
|
-
return { kind: 'literal' };
|
|
2791
|
-
if (!nested.ok && (hasCarrierEcCommandPrefix(stringCarrier.command, stringCarrier.dialect)
|
|
2792
|
-
|| (stringCarrier.dialect === 'powershell' && hasPowerShellCredentialAssignment(stringCarrier.command)))) {
|
|
2793
|
-
return { kind: 'composite', issue: nested.issue };
|
|
2794
|
-
}
|
|
2795
|
-
return { kind: 'none' };
|
|
2796
|
-
}
|
|
2797
2833
|
const malformedCmdBody = extractCodexCmdCarrierBody(stripped);
|
|
2798
2834
|
if (malformedCmdBody && looksLikeEcCommandBody(malformedCmdBody)) {
|
|
2799
2835
|
const nested = parseLiteralShellCommand(malformedCmdBody, {
|
|
@@ -2314,6 +2314,36 @@ function prepareBoundedOutputInput(input, command, kind, context) {
|
|
|
2314
2314
|
reason: kind === 'ec' ? 'ec-command' : 'host-process-command',
|
|
2315
2315
|
};
|
|
2316
2316
|
}
|
|
2317
|
+
const BASH_RISK_LEVELS = new Set(['低', '中', '高']);
|
|
2318
|
+
/**
|
|
2319
|
+
* Bash 调用的 description 必须是「<命令目标描述> [<风险等级>]」。
|
|
2320
|
+
* 只适用于 Bash 工具 schema 带 description 字段的基座(Claude Code、ecagent),
|
|
2321
|
+
* 是调用格式约定而非权限判定,不区分会话类型与 permissionMode。
|
|
2322
|
+
*/
|
|
2323
|
+
export function checkBashDescription(toolName, input) {
|
|
2324
|
+
if (toolName !== 'Bash')
|
|
2325
|
+
return undefined;
|
|
2326
|
+
const FORMAT_HINT = 'description 格式为「<命令目标描述> [<风险等级>]」,风险等级只能是 [低]、[中]、[高] 之一。示例:"查看工作区未提交的改动 [低]"、"安装项目依赖 [中]"、"推送 main 分支到远端 [高]"。这是格式问题,不是权限问题,补全后重试。';
|
|
2327
|
+
const deny = (problem, policyCode) => ({
|
|
2328
|
+
behavior: 'deny',
|
|
2329
|
+
input,
|
|
2330
|
+
message: `Bash 调用被拒:${problem}。${FORMAT_HINT}`,
|
|
2331
|
+
policyCode,
|
|
2332
|
+
});
|
|
2333
|
+
const description = typeof input.description === 'string' ? input.description.trim() : '';
|
|
2334
|
+
if (!description) {
|
|
2335
|
+
return deny('缺少 description,既没有命令目标描述,也没有风险等级标记', 'bash_description_missing');
|
|
2336
|
+
}
|
|
2337
|
+
const tag = description.match(/\[([^[\]]*)\]$/);
|
|
2338
|
+
if (!tag)
|
|
2339
|
+
return deny('description 末尾缺少风险等级标记', 'bash_risk_level_missing');
|
|
2340
|
+
if (!BASH_RISK_LEVELS.has(tag[1]))
|
|
2341
|
+
return deny(`风险等级标记 [${tag[1]}] 无效`, 'bash_risk_level_invalid');
|
|
2342
|
+
if (!description.slice(0, tag.index).trim()) {
|
|
2343
|
+
return deny('description 缺少命令目标描述,只有风险等级标记', 'bash_description_target_missing');
|
|
2344
|
+
}
|
|
2345
|
+
return undefined;
|
|
2346
|
+
}
|
|
2317
2347
|
/** Shared fail-closed preflight used before runner-specific mode and approval handling. */
|
|
2318
2348
|
export function evaluateToolPreflight(toolName, input, context) {
|
|
2319
2349
|
let applyPatchFileChanges;
|
|
@@ -2570,7 +2600,7 @@ export function evaluateToolPreflight(toolName, input, context) {
|
|
|
2570
2600
|
? '🔒 EC 命令包含无法安全解析的 PowerShell 展开;临时路径仅支持已登记的当前会话 $env:TMPDIR 或 ${env:TMPDIR},也可使用完整路径。纯文字请使用单引号,或用 PowerShell 反引号转义 $ 和正文内的双引号。'
|
|
2571
2601
|
: shellDialect === 'cmd'
|
|
2572
2602
|
? '🔒 EC 命令包含未转义的 cmd.exe 展开;请不要使用 %VAR%、!VAR! 或 ^ 转义。cmd.exe 传给 ec 时必须保持原文字面量'
|
|
2573
|
-
: '🔒 EC 自由文本参数(正文、--desc、--name、--keywords 等)一律用单引号包住;双引号内的反引号与 $ 会被 shell
|
|
2603
|
+
: '🔒 EC 自由文本参数(正文、--desc、--name、--keywords 等)一律用单引号包住;双引号内的反引号与 $ 会被 shell 展开。正文含单引号时改用 $\'…\' 语法(支持 \\n \\t \\\\ \\\')。';
|
|
2574
2604
|
return {
|
|
2575
2605
|
behavior: 'deny',
|
|
2576
2606
|
input,
|
|
@@ -2583,7 +2613,7 @@ export function evaluateToolPreflight(toolName, input, context) {
|
|
|
2583
2613
|
? '🔒 EC 命令引号未闭合或存在错误嵌套。Note: PowerShell 把 U+2018/U+2019/U+201A/U+201B 也当引号定界符——正文里出现这四个字符同样会报此错。正文一律用半角单引号(ASCII 0x27)包住,正文内部的半角撇号用 `ʼ`(U+02BC)或 `'`(U+FF07)代替,或把消息正文写入当前 $TMPDIR 后使用 --text-from-file'
|
|
2584
2614
|
: shellDialect === 'cmd'
|
|
2585
2615
|
? '🔒 EC 命令引号未闭合或存在错误嵌套;cmd.exe carrier 的命令正文请保持一个完整参数,不要用未配对的双引号'
|
|
2586
|
-
: '🔒 EC
|
|
2616
|
+
: '🔒 EC 命令引号未闭合或存在错误嵌套。自由文本参数用单引号包住;正文含单引号时改用 $\'…\' 语法,内部用 \\\' 转义,如 $\'it\\\'s fine\'。';
|
|
2587
2617
|
return {
|
|
2588
2618
|
behavior: 'deny',
|
|
2589
2619
|
input,
|
|
@@ -1,3 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 解析 agent.md 的 YAML frontmatter。
|
|
3
|
+
*
|
|
4
|
+
* agent.md 形如:
|
|
5
|
+
* ---
|
|
6
|
+
* aid: xxx
|
|
7
|
+
* type: human
|
|
8
|
+
* ...
|
|
9
|
+
* ---
|
|
10
|
+
* <正文>
|
|
11
|
+
*
|
|
12
|
+
* 返回整份 frontmatter(不是只挑 type/name),调用方据此写完整画像。
|
|
13
|
+
* 文档不合法(无 frontmatter、YAML 解析失败)时返回空对象——身份缓存
|
|
14
|
+
* 不应因为一个可选的元数据块解析失败而整体失效。
|
|
15
|
+
*/
|
|
16
|
+
export function parseAgentMdFrontmatter(content) {
|
|
17
|
+
const text = content.replace(/^\uFEFF/, '');
|
|
18
|
+
const lines = text.split(/\r\n|\n|\r/);
|
|
19
|
+
if (lines[0]?.trim() !== '---')
|
|
20
|
+
return {};
|
|
21
|
+
const end = lines.slice(1).findIndex(line => line.trim() === '---');
|
|
22
|
+
if (end < 0)
|
|
23
|
+
return {};
|
|
24
|
+
try {
|
|
25
|
+
const parsed = parseYaml(lines.slice(1, end + 1).join('\n'));
|
|
26
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
27
|
+
? parsed
|
|
28
|
+
: {};
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 兼容旧缓存文件:字段从 `isAgent` 改名为 `isMachine` 之前写下的
|
|
36
|
+
* peer-identity.json 只有 isAgent。两者语义相同(type !== 'human'),
|
|
37
|
+
* 缓存有效期长达 30 天,不能指望靠下一次刷新自然迁移——
|
|
38
|
+
* 读取时直接归一,缺字段就用旧字段或 type 推出。
|
|
39
|
+
*/
|
|
40
|
+
function normalizeCachedIdentity(raw) {
|
|
41
|
+
if (!raw || typeof raw !== 'object')
|
|
42
|
+
return null;
|
|
43
|
+
const value = raw;
|
|
44
|
+
const type = typeof value.type === 'string' ? value.type : 'unknown';
|
|
45
|
+
const isMachine = typeof value.isMachine === 'boolean'
|
|
46
|
+
? value.isMachine
|
|
47
|
+
: typeof value.isAgent === 'boolean'
|
|
48
|
+
? value.isAgent
|
|
49
|
+
: type !== 'human';
|
|
50
|
+
return { ...value, type, isMachine, isAgent: isMachine };
|
|
51
|
+
}
|
|
1
52
|
/**
|
|
2
53
|
* PeerIdentityCache - 对端身份缓存管理
|
|
3
54
|
*
|
|
@@ -13,6 +64,7 @@
|
|
|
13
64
|
import * as fs from 'fs';
|
|
14
65
|
import * as path from 'path';
|
|
15
66
|
import * as crypto from 'crypto';
|
|
67
|
+
import { parse as parseYaml } from 'yaml';
|
|
16
68
|
import { logger } from '../../utils/logger.js';
|
|
17
69
|
import { agentMdPath } from '../../paths.js';
|
|
18
70
|
/**
|
|
@@ -56,7 +108,7 @@ export class PeerIdentityCache {
|
|
|
56
108
|
const filePath = this.getFilePath(channelType, peerId, agentDir);
|
|
57
109
|
try {
|
|
58
110
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
59
|
-
return JSON.parse(content);
|
|
111
|
+
return normalizeCachedIdentity(JSON.parse(content));
|
|
60
112
|
}
|
|
61
113
|
catch {
|
|
62
114
|
return null;
|
|
@@ -87,6 +139,10 @@ export class PeerIdentityCache {
|
|
|
87
139
|
const cached = this.get(channelType, peerId, agentDir);
|
|
88
140
|
if (!cached)
|
|
89
141
|
return true;
|
|
142
|
+
// 旧快照缺 metadata(字段补全之前写入)时需要重建一次,否则 30 天内
|
|
143
|
+
// 都会直接命中缓存,新字段永远补不上。
|
|
144
|
+
if (cached.source !== 'unknown' && cached.metadata === undefined)
|
|
145
|
+
return true;
|
|
90
146
|
const effectiveMaxAge = cached.source === 'unknown'
|
|
91
147
|
? Math.min(maxAgeMs, this.UNKNOWN_CACHE_MAX_AGE_MS)
|
|
92
148
|
: maxAgeMs;
|
|
@@ -98,17 +154,28 @@ export class PeerIdentityCache {
|
|
|
98
154
|
* @param verifiedAt 验签通过时间戳;未验签传 0
|
|
99
155
|
*/
|
|
100
156
|
static updateFromAgentMd(channelType, peerId, agentDir, agentMd, verifiedAt, source = 'agentmd') {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
|
|
157
|
+
// 保留整份 frontmatter:这份缓存是对端自我声明的完整画像,
|
|
158
|
+
// 只截取 type/name 会在上游新增字段(exttype/owner/tags/avatar…)时静默丢失。
|
|
159
|
+
const metadata = parseAgentMdFrontmatter(agentMd);
|
|
160
|
+
const readString = (key) => {
|
|
161
|
+
const value = metadata[key];
|
|
162
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
163
|
+
};
|
|
164
|
+
const type = readString('type') ?? 'unknown';
|
|
165
|
+
const isMachine = type !== 'human';
|
|
166
|
+
const name = readString('name');
|
|
167
|
+
const exttype = readString('exttype');
|
|
168
|
+
const owner = readString('owner');
|
|
106
169
|
const agentMdHash = 'sha256:' + crypto.createHash('sha256').update(agentMd, 'utf-8').digest('hex');
|
|
107
170
|
const now = Date.now();
|
|
108
171
|
const identity = {
|
|
109
172
|
aid: peerId,
|
|
110
173
|
type,
|
|
111
|
-
|
|
174
|
+
isMachine,
|
|
175
|
+
isAgent: isMachine,
|
|
176
|
+
...(exttype ? { exttype } : {}),
|
|
177
|
+
...(owner ? { owner } : {}),
|
|
178
|
+
metadata,
|
|
112
179
|
name,
|
|
113
180
|
agentMdHash,
|
|
114
181
|
agentMdUpdatedAt: now,
|
|
@@ -120,7 +187,7 @@ export class PeerIdentityCache {
|
|
|
120
187
|
try {
|
|
121
188
|
fs.mkdirSync(path.join(path.dirname(filePath), 'shared'), { recursive: true });
|
|
122
189
|
fs.writeFileSync(filePath, JSON.stringify(identity, null, 2), 'utf-8');
|
|
123
|
-
logger.debug(`[PeerIdentityCache] Updated: ${channelType}#${peerId} type=${type}
|
|
190
|
+
logger.debug(`[PeerIdentityCache] Updated: ${channelType}#${peerId} type=${type} isMachine=${isMachine} source=${source}`);
|
|
124
191
|
}
|
|
125
192
|
catch (err) {
|
|
126
193
|
logger.warn(`[PeerIdentityCache] Failed to write cache: ${filePath} err=${err}`);
|
|
@@ -146,6 +213,7 @@ export class PeerIdentityCache {
|
|
|
146
213
|
const identity = {
|
|
147
214
|
aid: peerId,
|
|
148
215
|
type: 'unknown',
|
|
216
|
+
isMachine: true,
|
|
149
217
|
isAgent: true,
|
|
150
218
|
agentMdHash: '',
|
|
151
219
|
agentMdUpdatedAt: 0,
|
|
@@ -188,6 +256,7 @@ export class PeerIdentityCache {
|
|
|
188
256
|
return {
|
|
189
257
|
aid: '',
|
|
190
258
|
type: 'unknown',
|
|
259
|
+
isMachine: true,
|
|
191
260
|
isAgent: true,
|
|
192
261
|
agentMdHash: '',
|
|
193
262
|
agentMdUpdatedAt: 0,
|
|
@@ -212,7 +281,10 @@ export class PeerIdentityCache {
|
|
|
212
281
|
try {
|
|
213
282
|
logger.debug(`[PeerIdentityCache] Syncing agent.md: ${channelType}#${peerId}`);
|
|
214
283
|
const { agentmdSync } = await import('../../aun/aid/agentmd.js');
|
|
215
|
-
|
|
284
|
+
// 缺 metadata 的旧快照(字段补全之前写入的)也要重写一次:
|
|
285
|
+
// 否则命中 hash 相同分支只会 touch,永远补不上 exttype/owner/metadata。
|
|
286
|
+
const invalidated = stale?.lastCheckedAt === 0 || revision > 0
|
|
287
|
+
|| (!!stale && stale.source !== 'unknown' && stale.metadata === undefined);
|
|
216
288
|
const result = await agentmdSync(peerId, {
|
|
217
289
|
store,
|
|
218
290
|
aunPath: resolvedAunPath,
|
|
@@ -222,7 +294,7 @@ export class PeerIdentityCache {
|
|
|
222
294
|
if ((this.invalidationRevisions.get(cachePath) ?? 0) !== revision) {
|
|
223
295
|
// A newer event arrived during this read. Its next lookup must refresh.
|
|
224
296
|
return this.get(channelType, peerId, agentDir) ?? {
|
|
225
|
-
aid: peerId, type: 'unknown', isAgent: true, agentMdHash: '', agentMdUpdatedAt: 0,
|
|
297
|
+
aid: peerId, type: 'unknown', isMachine: true, isAgent: true, agentMdHash: '', agentMdUpdatedAt: 0,
|
|
226
298
|
verifiedAt: 0, lastCheckedAt: 0, source: 'unknown',
|
|
227
299
|
};
|
|
228
300
|
}
|
|
@@ -241,10 +313,13 @@ export class PeerIdentityCache {
|
|
|
241
313
|
if (!verified) {
|
|
242
314
|
logger.info(`[PeerIdentityCache] agent.md unverified for ${peerId}: status=${result.verification?.status ?? 'unknown'} reason=${result.verification?.reason ?? '-'} (type 仍按声明解析)`);
|
|
243
315
|
}
|
|
244
|
-
// 3. 比较 hash,内容与可信级别都未变时仅 touch
|
|
316
|
+
// 3. 比较 hash,内容与可信级别都未变时仅 touch。
|
|
317
|
+
// 例外:旧快照若缺 metadata(字段补全之前写入),必须重写一次,
|
|
318
|
+
// 否则这里每次都走 touch,新字段永远补不上。
|
|
245
319
|
const newHash = 'sha256:' + crypto.createHash('sha256').update(content, 'utf-8').digest('hex');
|
|
246
320
|
const cached = this.get(channelType, peerId, agentDir);
|
|
247
|
-
if (cached && cached.agentMdHash === newHash && cached.source === source
|
|
321
|
+
if (cached && cached.agentMdHash === newHash && cached.source === source
|
|
322
|
+
&& cached.metadata !== undefined) {
|
|
248
323
|
return this.touchLastChecked(channelType, peerId, agentDir, cached);
|
|
249
324
|
}
|
|
250
325
|
return this.updateFromAgentMd(channelType, peerId, agentDir, content, verified ? Date.now() : 0, source);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { normalizeSessionLoopConfig, resolveSessionLoopTarget, sessionLoopError } from './session-idle-monitor.js';
|
|
2
2
|
import { ensureDir } from '../../utils/atomic-write.js';
|
|
3
|
-
import { resolvePaths } from '../../paths.js';
|
|
3
|
+
import { resolvePaths, agentThreadIndexPath } from '../../paths.js';
|
|
4
4
|
import { logger } from '../../utils/logger.js';
|
|
5
5
|
import { encodePath } from '../../utils/cross-platform.js';
|
|
6
6
|
import { readRunnerEventSummary } from './runner-event-summary.js';
|
|
@@ -1555,6 +1555,20 @@ export class SessionManager {
|
|
|
1555
1555
|
metaFile: `${session.id}.jsonl`,
|
|
1556
1556
|
};
|
|
1557
1557
|
writeThreadIndex(chatDir, threadIndex);
|
|
1558
|
+
// 被动建档(收到消息时自动创建话题会话)也必须同步到 agent 级话题索引,
|
|
1559
|
+
// 否则 `ec thread rename current` / list 等按索引寻址的操作会报"话题不存在"。
|
|
1560
|
+
this.upsertAgentThreadIndex({
|
|
1561
|
+
selfAid: selfAID ?? '',
|
|
1562
|
+
threadId,
|
|
1563
|
+
sessionId: session.id,
|
|
1564
|
+
peer: channelId,
|
|
1565
|
+
channelType: channelType ?? 'aun',
|
|
1566
|
+
peerKind: effectiveChatType === 'group' ? 'group' : 'private',
|
|
1567
|
+
name: session.name,
|
|
1568
|
+
label: normalizeTopicLabel(label) ?? null,
|
|
1569
|
+
topicStatus: session.topicStatus ?? 'pending',
|
|
1570
|
+
now: session.createdAt ?? Date.now(),
|
|
1571
|
+
});
|
|
1558
1572
|
this.eventBus.publish({
|
|
1559
1573
|
type: 'session:created',
|
|
1560
1574
|
sessionId: session.id,
|
|
@@ -1567,6 +1581,47 @@ export class SessionManager {
|
|
|
1567
1581
|
});
|
|
1568
1582
|
return session;
|
|
1569
1583
|
}
|
|
1584
|
+
/**
|
|
1585
|
+
* 把一条话题会话合并进 agent 级索引(`data/threads/index.json`)。
|
|
1586
|
+
*
|
|
1587
|
+
* 全局索引是 `ec thread` 各命令的寻址源;被动建档只写过 chatDir 下的
|
|
1588
|
+
* `_threads/thread-index.json`,导致 `rename current` 之类找不到自己。
|
|
1589
|
+
* 这里按 (peer, threadId) 幂等 upsert:已存在则只补缺失字段,不覆盖
|
|
1590
|
+
* 名称/摘要等既有属性。
|
|
1591
|
+
*/
|
|
1592
|
+
upsertAgentThreadIndex(input) {
|
|
1593
|
+
if (!input.selfAid || !input.threadId)
|
|
1594
|
+
return;
|
|
1595
|
+
try {
|
|
1596
|
+
const indexPath = agentThreadIndexPath(input.selfAid);
|
|
1597
|
+
const raw = readJsonFile(indexPath);
|
|
1598
|
+
const index = raw && Array.isArray(raw.threads) ? raw : { version: 1, threads: [] };
|
|
1599
|
+
const existing = index.threads.find((t) => t.threadId === input.threadId
|
|
1600
|
+
&& String(t.peer ?? '').toLowerCase() === input.peer.toLowerCase());
|
|
1601
|
+
if (existing) {
|
|
1602
|
+
existing.sessionId = existing.sessionId || input.sessionId;
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
index.threads.push({
|
|
1606
|
+
threadId: input.threadId,
|
|
1607
|
+
sessionId: input.sessionId,
|
|
1608
|
+
peer: input.peer,
|
|
1609
|
+
channelType: input.channelType,
|
|
1610
|
+
peerKind: input.peerKind,
|
|
1611
|
+
name: input.name || '话题会话',
|
|
1612
|
+
label: input.label ?? null,
|
|
1613
|
+
topicStatus: input.topicStatus,
|
|
1614
|
+
createdAt: input.now,
|
|
1615
|
+
lastActiveAt: input.now,
|
|
1616
|
+
});
|
|
1617
|
+
ensureDir(path.dirname(indexPath));
|
|
1618
|
+
atomicWriteJson(indexPath, index);
|
|
1619
|
+
}
|
|
1620
|
+
catch (error) {
|
|
1621
|
+
// 索引同步失败不能阻断消息处理:会话本身已落盘,rebuild-index 可补齐。
|
|
1622
|
+
logger.warn(`[SessionManager] agent thread index upsert failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1570
1625
|
async switchProject(channel, channelId, newProjectPath, baseagent) {
|
|
1571
1626
|
if (!baseagent) {
|
|
1572
1627
|
throw new Error(`[SessionManager] switchProject: baseagent is empty for channel=${channel} channelId=${channelId}`);
|
|
@@ -94,6 +94,31 @@ export function buildCliSessionPreview(messages, backend, options = {}) {
|
|
|
94
94
|
: null;
|
|
95
95
|
return { title, hasReal: !!summary.firstRealMessage };
|
|
96
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* 会话/话题名称的长度常量,自动生成与手动改名共用。
|
|
99
|
+
*
|
|
100
|
+
* - `SESSION_TITLE_MAX_LEN`(80):正文字数,超长文本截到这里再加省略号;
|
|
101
|
+
* - `SESSION_TITLE_STORED_MAX_LEN`(83):落盘上限 = 80 + `...`。
|
|
102
|
+
*
|
|
103
|
+
* 80~83 字符的名字本身就是合法落盘形态,不再二次截断——
|
|
104
|
+
* 否则「系统自动生成的名字」会变成改一次短一点。
|
|
105
|
+
*/
|
|
106
|
+
export const SESSION_TITLE_MAX_LEN = 80;
|
|
107
|
+
export const SESSION_TITLE_ELLIPSIS = '...';
|
|
108
|
+
/** 落盘名称的硬上限(截断后的最长长度)。 */
|
|
109
|
+
export const SESSION_TITLE_STORED_MAX_LEN = SESSION_TITLE_MAX_LEN + SESSION_TITLE_ELLIPSIS.length;
|
|
110
|
+
/**
|
|
111
|
+
* 截断到落盘上限并补省略号;未超长时原样返回。
|
|
112
|
+
*
|
|
113
|
+
* 判据是 `SESSION_TITLE_STORED_MAX_LEN`(83)而不是正文上限(80):
|
|
114
|
+
* 80~83 字符的名称本来就是合法落盘形态(含历史遗留),截断它们会把
|
|
115
|
+
* 「系统生成的名字原样改回去」变成「改一次短一点」。
|
|
116
|
+
*/
|
|
117
|
+
export function truncateSessionTitle(title) {
|
|
118
|
+
return title.length > SESSION_TITLE_STORED_MAX_LEN
|
|
119
|
+
? `${title.slice(0, SESSION_TITLE_MAX_LEN)}${SESSION_TITLE_ELLIPSIS}`
|
|
120
|
+
: title;
|
|
121
|
+
}
|
|
97
122
|
export function sanitizeSessionTitle(title) {
|
|
98
123
|
if (typeof title !== 'string')
|
|
99
124
|
return undefined;
|
|
@@ -109,7 +134,7 @@ export function sanitizeSessionTitle(title) {
|
|
|
109
134
|
return undefined;
|
|
110
135
|
if (/^\.+$/.test(cleanTitle))
|
|
111
136
|
return undefined;
|
|
112
|
-
return cleanTitle
|
|
137
|
+
return truncateSessionTitle(cleanTitle);
|
|
113
138
|
}
|
|
114
139
|
export function displaySessionTitle(title, fallback = '默认会话') {
|
|
115
140
|
return sanitizeSessionTitle(title) || sanitizeSessionTitle(fallback) || fallback;
|