Skip to content

[优化建议] OpenHorse v0.1.10 综合代码体检报告 #32

@tianxingzhivlog-droid

Description

@tianxingzhivlog-droid

[优化建议] OpenHorse v0.1.10 综合代码体检报告

基于 main 分支最新提交 98a386f(v0.1.10)做的静态代码体检报告。
共识别 20 项优化点,按【严重度 × 模块】整理,每项均附 file:line 定位、问题描述、风险说明与可落地的修复代码示例。
目的:帮助维护者在 v0.1.11 / v0.2 周期内分流到独立 PR 实现。本 issue 仅作建议,欢迎指正与拒绝。


目录


一、问题分级总览

# 模块 简述 严重度 类型
2.1 core/brain submitTask 触发 fire-and-forget dispatch Critical 稳定性
2.2 framework query 工具执行无 try/catch,单次工具异常即崩溃整个对话 High 稳定性
2.3 harness catch (error: any) 假定 error.message 存在 High 稳定性
2.4 core/agent params / TaskResult.data / triggerSkill 大量 any High 类型安全
3.1 harness/safety auditLog 无上限,长会话内存泄漏 Medium 性能
3.2 framework 工具执行未透传 abortSignal,用户取消后仍跑 Medium UX/稳定
3.3 memory/vector upsert 删旧向量后插入,失败留下孤儿记录 Medium 数据一致
3.4 memory/vector hashProject 仅取末两段路径,跨用户同名项目冲突 Medium 数据正确
3.5 memory/embed embedBatchAbortSignal,单次慢调用阻塞整批 Medium 性能
3.6 services/mcp SSE/WebSocket 重连固定 3s 间隔,无指数退避 Medium 鲁棒性
3.7 tools/web web_fetch 无 SSRF 拦截 + 无 Content-Length 上限 Medium 安全
3.8 memory/storage searchMemoriesgetMemoriesByType 每次全量读盘 Medium 性能
3.9 tools/index exec_command stdout/stderr 截断逻辑不一致 Medium 正确性
3.10 ui/cmd-panel \x1b[J 清屏过激,会清除面板下方非自身内容 Medium UX
3.11 ui SIGWINCH 监听 + chalk 颜色未走 NO_COLOR 通道 Medium UX/无障碍
4.1 repo CONTRIBUTING.md、issue/PR 模板、.eslintrc Low 社区基建
4.2 package.json react / react-compiler-runtime / react-reconciler / bidi-js 已声明但未使用 Low 依赖卫生
4.3 tests 91 个 TS 源文件对 26 个测试文件,UI 层与异常路径覆盖缺失 Low 测试
4.4 tools/index 单文件 1132 行,13+ 工具内联,缺乏参数校验抽象 Low 可维护
4.5 memory/storage MEMORY.md 超过 25KB 后静默丢条目 Low UX

二、Critical / High(强烈建议优先合入)

2.1 [Critical] Brain.submitTask 触发 fire-and-forget dispatch

  • 位置:src/core/brain.ts:36-39

  • 现状:

    submitTask(task: Task): void {
      this.taskQueue.push(task);
      this.dispatch();          // 未 await,不消费 Promise
    }
    
    private async dispatch(): Promise<void> {
      // ... 内部 try/catch 仅吞掉 error,无外部感知
    }
  • 问题:dispatch() 返回 Promise<void>,调用方既不 await 也不 .catch。一旦内部 await agent.execute(task) 抛出 unhandled rejection(虽然 dispatch 自身有 try/catch,但任何在 try 之外抛出的异常 — 例如 findBestAgent 中的逻辑 bug — 都将变成 Node 全局未处理拒绝),taskQueue 状态可能停留在中间态,且无任何事件透传给调用方。

  • 建议修复:

    submitTask(task: Task): void {
      this.taskQueue.push(task);
      this.dispatch().catch(err => {
        task.status = 'failed';
        this.emit('dispatch-error', { taskId: task.id, error: err });
      });
    }

    并在类构造前 extends EventEmitter(目前 Brain 未继承 EventEmitter,可考虑加入以统一事件模型)。

  • 影响范围:任何调用 submitTask 的上层(如 multi-agent 路由器)。


2.2 [High] query 中工具执行无 try/catch,单次失败崩溃整个对话

  • 位置:src/framework/query.ts:171-189

  • 现状:

    if (perm.behavior === 'deny') { ... }
    else if (perm.behavior === 'ask' && params.permissionMode === 'default') { ... }
    else {
      result = await toolExecutor(tc.function.name, args);   // 未捕获
    }
  • 问题:工具内部 throw(网络异常、spawn ENOENT、JSON 解析失败等)会直接冒泡到 query() 的外层 generator,导致整轮对话终止,用户看到的是 unhandled rejection。strategyTracker 也不会记录这次失败。

  • 建议修复:

    try {
      result = await toolExecutor(tc.function.name, args);
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      result = JSON.stringify({
        success: false,
        error: `Tool ${tc.function.name} threw: ${msg}`,
      });
    }

    另外,abortSignal 应透传给 toolExecutor(见 3.2)。

  • 影响范围:所有依赖 query() 主循环的 chat handler。


2.3 [High] harness.tscatch (error: any) 假定 error.message 存在

  • 位置:src/harness/harness.ts:264-270

  • 现状:

    } catch (error: any) {
      taskResult = {
        success: false,
        error: error.message ?? 'Execution failed',
        duration: Date.now() - context.startedAt,
      };
    }
  • 问题:agent.execute() 抛出的不一定是 Error,有可能是字符串、对象或 undefined(setTimeout/Promise.reject 等场景),此时 error.messageundefined,虽然 ?? 兜底但丢失了原始信息。

  • 建议修复:

    } catch (error) {
      const msg = error instanceof Error
        ? error.message
        : typeof error === 'string'
          ? error
          : JSON.stringify(error);
      taskResult = {
        success: false,
        error: msg || 'Execution failed',
        duration: Date.now() - context.startedAt,
      };
    }
  • 影响范围:所有 Harness 包装的 agent 执行错误上报。


2.4 [High] agent.tsany 滥用,失去 TypeScript 类型保护

  • 位置:src/core/agent.ts:26, 69-79, 90-95, 98

  • 现状:

    params?: Record<string, any>;                                      // L26
    registerSkill(name: string, handler: (...args: any[]) => any): void; // L69
    async triggerSkill(name: string, params?: any): Promise<any[]>;    // L76
    data?: any;                                                        // L92
    getStatus(): any { ... }                                           // brain.ts:98
  • 问题:strict: true 已开启,但核心类型完全失控:TaskResult.dataharness.ts:196,208 处被强制 as Record<string, number>,任何上游不一致都是运行时崩溃。

  • 建议修复:引入鉴别联合 + 泛型。

    // core/agent.ts
    export interface SkillContext<P = Record<string, unknown>> {
      name: string;
      params?: P;
    }
    
    export type TaskResultData =
      | { kind: 'file'; content: string }
      | { kind: 'command'; stdout: string; stderr?: string; exitCode: number }
      | { kind: 'metrics'; steps?: number; metrics?: Record<string, number> }
      | { kind: 'generic'; value: unknown };
    
    export interface TaskResult {
      success: boolean;
      data?: TaskResultData;
      error?: string;
      duration?: number;
    }
    
    registerSkill<P = unknown, R = unknown>(
      name: string,
      handler: (ctx: SkillContext<P>) => Promise<R>,
    ): void { ... }

    并把 brain.ts:98getStatus(): any 改成显式 BrainStatus 接口。

  • 影响范围:所有继承 BaseAgent 的子类、Harness、Brain;改动是兼容性的,但需要按模块逐步引入。建议作为单独 feat(typing): tighten core agent types 专项 PR。


三、Medium(架构 / 性能 / 安全)

3.1 SafetyChecker.auditLog 无上限,长会话内存泄漏

  • 位置:src/harness/safety.ts:94, 241-252

  • 现状:

    private auditLog: AuditLogEntry[] = [];
    // record() 中只 push,无 shift
  • 建议修复:

    private auditLog: AuditLogEntry[] = [];
    private maxAuditLog = 1000;          // 可由 policy 配置
    
    private record(check: SafetyCheck & { action: string }): SafetyCheck {
      this.auditLog.push({ ... });
      if (this.auditLog.length > this.maxAuditLog) {
        this.auditLog.splice(0, this.auditLog.length - this.maxAuditLog);
      }
      this.emit('check', check);
      return check;
    }

3.2 工具执行未透传 abortSignal,用户取消后仍跑

  • 位置:src/framework/query.ts:43, 102-109, 185-189

  • 现状:QueryParams.abortSignal 仅在循环头 check,工具一旦开始 await 就忽略它。

  • 建议修复:扩展 toolExecutor 签名:

    toolExecutor: (
      name: string,
      args: Record<string, unknown>,
      abortSignal?: AbortSignal,
    ) => Promise<string>;
    
    // 调用处:
    result = await toolExecutor(tc.function.name, args, abortSignal);

    并在 exec_command / web_fetch / embed 等长耗时工具内部监听 abortSignal.aborted 主动 abort(child.kill() / fetch({signal}))。


3.3 VectorStore.upsert 非事务,失败留下孤儿记录

  • 位置:src/memory/vector-store.ts:118-160

  • 现状:memories 表先 INSERT OR REPLACE,然后才 embed()。若 embedding 服务超时(embeddings.ts:83 默认 30s),memory_vectorsvec_memories 未写入,但 memories 已更新,造成索引漂移。

  • 建议修复:使用 better-sqlite3 的 transaction;同时 embed 失败时回滚 memories 表或保留旧 vector。

    async upsert(entry: MemoryEntry, projectPath?: string): Promise<void> {
      const projectHash = projectPath ? this.hashProject(projectPath) : 'global';
      let vector: number[] | null = null;
      if (this.initialized) {
        try {
          vector = await this.embeddingService.embed(entry.content);
        } catch (err: any) {
          console.warn(`[VectorStore] embed failed, skip vector write: ${err.message}`);
        }
      }
      const tx = this.db.transaction((v: number[] | null) => {
        this.db.prepare(`INSERT OR REPLACE INTO memories (...) VALUES (...)`).run(...);
        if (v) {
          this.db.prepare('DELETE FROM memory_vectors WHERE memory_id = ?').run(entry.name);
          this.db.prepare('DELETE FROM vec_memories WHERE rowid IN (SELECT vector_rowid FROM memory_vectors WHERE memory_id = ?)').run(entry.name);
          const r = this.db.prepare(`INSERT INTO vec_memories (embedding) VALUES (?)`).run(JSON.stringify(v));
          this.db.prepare('INSERT INTO memory_vectors (memory_id, vector_rowid) VALUES (?, ?)').run(entry.name, r.lastInsertRowid);
        }
      });
      tx(vector);
    }

    顺便补一个组合索引:

    CREATE INDEX IF NOT EXISTS idx_memories_project_type ON memories(project, type);

3.4 hashProject 仅取末两段路径,跨用户同名项目冲突

  • 位置:src/memory/vector-store.ts:295-299

  • 现状:

    private hashProject(projectPath: string): string {
      const hash = projectPath.split('/').slice(-2).join('/');
      return hash;        // /home/alice/code 与 /home/bob/code 冲突
    }
  • 建议修复:使用稳定哈希,与 src/memory/storage.tsgetProjectHash() 保持一致(假设其使用 SHA-256):

    import { createHash } from 'crypto';
    
    private hashProject(projectPath: string): string {
      return createHash('sha256').update(projectPath).digest('hex').slice(0, 16);
    }

    注意:迁移期需要一次性 UPDATE memories SET project = new_hash WHERE project = old_hash,可写在初始化时的数据迁移函数中。


3.5 embedBatchAbortSignal,单次慢调用阻塞整批

  • 位置:src/memory/embeddings.ts:59-71

  • 现状:Promise.all(batch.map(t => this.embed(t))),任一调用卡到 30s 超时,整批等待。无并发熔断。

  • 建议修复:支持 abort + 单调用更短超时 + Promise.allSettled:

    async embedBatch(texts: string[], opts: { signal?: AbortSignal; perCallTimeoutMs?: number } = {}): Promise<number[][]> {
      const batchSize = 10;
      const results: number[][] = [];
      for (let i = 0; i < texts.length; i += batchSize) {
        if (opts.signal?.aborted) throw new DOMException('Aborted', 'AbortError');
        const batch = texts.slice(i, i + batchSize);
        const settled = await Promise.allSettled(
          batch.map(t => this.embed(t /* 把 signal/timeout 透传到 axios */)),
        );
        for (const s of settled) {
          results.push(s.status === 'fulfilled' ? s.value : new Array(this.dimension).fill(0));
        }
      }
      return results;
    }

3.6 SSE / WebSocket 重连固定 3s,无指数退避

  • 位置:src/services/mcp/transports.ts:161-178(SSE) 与 247-264(WebSocket)

  • 现状:两个 attemptReconnect 都用 interval = config.reconnectInterval || 3000 的等间隔重试,5 次 = 15s 即放弃。

  • 建议修复:

    private attemptReconnect(): void {
      const maxAttempts = this.config.maxReconnectAttempts || 5;
      const baseInterval = this.config.reconnectInterval || 1000;
      if (this.reconnectAttempts >= maxAttempts) {
        this.emitError(new Error('Max reconnect attempts reached'));
        return;
      }
      this.reconnectAttempts++;
      // 指数退避 + 抖动,封顶 30s
      const delay = Math.min(
        baseInterval * Math.pow(2, this.reconnectAttempts - 1) + Math.floor(Math.random() * 250),
        30_000,
      );
      this.emit('reconnecting', this.reconnectAttempts);
      setTimeout(() => {
        this.connect().catch(err => this.emitError(err));
      }, delay);
    }

    考虑把这段抽到 BaseTransport 中复用。


3.7 web_fetch 无 SSRF 拦截,且未限制响应体大小

  • 位置:src/tools/web.ts:128-192, 254-262

  • 现状:

    • 仅校验 protocol.startsWith('http'),未拒绝 http://127.0.0.1 / http://169.254.169.254(云元数据)/ http://[::1]
    • await response.text() 未读 Content-Length,响应体可能数 GB,只在拿到完整 text 之后才截断到 100KB,期间已经占满内存。
  • 建议修复:

    function isPrivateOrLoopback(hostname: string): boolean {
      if (hostname === 'localhost') return true;
      // IPv4
      if (/^127\./.test(hostname) || /^10\./.test(hostname) ||
          /^192\.168\./.test(hostname) || /^172\.(1[6-9]|2\d|3[01])\./.test(hostname) ||
          hostname === '169.254.169.254') return true;
      // IPv6
      if (hostname === '::1' || hostname.startsWith('fc') || hostname.startsWith('fd')) return true;
      return false;
    }
    
    // 在 fetchUrl 入口:
    const parsed = new URL(url);
    if (isPrivateOrLoopback(parsed.hostname)) {
      return { content: 'Blocked: private/loopback host', code: 0, contentType: 'text/plain', errorType: 'SSRF_BLOCKED' };
    }
    
    // 读取响应时尊重 Content-Length:
    const cl = parseInt(response.headers.get('content-length') || '0', 10);
    const HARD_LIMIT = MAX_MARKDOWN_LENGTH * 4;       // 给 HTML→MD 预留压缩空间
    if (cl > HARD_LIMIT) {
      return { content: `Response too large (${cl} bytes)`, code: response.status, contentType, errorType: 'SIZE_LIMIT' };
    }
    // 没 Content-Length 时,改用流式读取,边读边累计长度,超限提前 controller.abort()。

3.8 searchMemories / getMemoriesByType 每次全量读盘

  • 位置:src/memory/storage.ts:288-307

  • 现状:loadAllMemories 每次调用都重新 readdir + readFile 全部 .md 文件,作 in-memory filter。对大型记忆库随条目数线性退化,且每次 memory_recall fallback 都触发。

  • 建议修复:走 SQLite 索引:

    // 利用现有 vector-store 表(其 textSearch 已有 SQL 路径)
    // 不必重写,直接代理到 VectorStore.search() / textSearch()。
    export function searchMemories(query: string, projectPath?: string): MemoryEntry[] {
      const store = getVectorStore();
      return store.search(query, 50, projectPath)
        .then(results => results.map(toMemoryEntry));   // 注意改成 async
    }

    迁移注意:searchMemories 当前是同步函数,改 async 会传染调用方。可保留旧路径作 fallback,在初始化时给 sqlite 表做一次全量 reindex。


3.9 exec_command stdout/stderr 截断逻辑不一致

  • 位置:src/tools/index.ts:741-823

  • 现状:

    // stdout 用 totalBytes 累计
    totalBytes += chunk.length;
    if (totalBytes > maxBytes) { stdoutTruncated = true; ... }
    
    // stderr 用 stderrData.length(独立计数)
    if (!stderrTruncated && stderrData.length < maxBytes) {
      if (stderrData.length + chunk.length > maxBytes) { ... }
    }

    两条路径的"是否达到上限"判断口径不同 — stdout 用累计 totalBytes(包含 stdout 全部历史),stderr 用 stderrData.length(只自己)。当 stdout 已经 truncate 后,stderr 仍可独自再写 50KB,两个流加起来最多 100KB,与单一 50KB 上限的语义不符。

  • 建议修复:统一在外层维护单一 totalBytes:

    let totalBytes = 0;
    const appendBounded = (target: 'stdout' | 'stderr', chunk: string) => {
      if (totalBytes >= maxBytes) {
        if (target === 'stdout') stdoutTruncated = true;
        else stderrTruncated = true;
        return;
      }
      const remain = maxBytes - totalBytes;
      const take = chunk.slice(0, remain);
      totalBytes += take.length;
      if (target === 'stdout') stdoutData += take; else stderrData += take;
      if (chunk.length > take.length) {
        if (target === 'stdout') stdoutTruncated = true;
        else stderrTruncated = true;
      }
    };
    
    child.stdout.on('data', d => appendBounded('stdout', d.toString()));
    child.stderr.on('data', d => appendBounded('stderr', d.toString()));

3.10 命令面板 \x1b[J 清屏过激,清除下方非自身内容

  • 位置:src/ui/command-panel.ts:211-225, 227-243

  • 现状:render()clearPanel() 都用 \x1b[s 存光标 → \x1b[J(清光标到屏幕底部所有内容) → \x1b[u 复位。任何在面板下方已经存在的输出(例如刚刚 stream 出来的助手回复尾部)都会被擦除。已知的 commit 9a9a5f6 修复了光标越界但没解决"过度清屏"问题。

  • 建议修复:精确按 lastPanelLines.length 行覆盖,而不是 \x1b[J:

    function clearPanel(): void {
      const height = lastPanelLines.length;
      if (height === 0) return;
      process.stdout.write('\x1b[s');
      for (let i = 0; i < height; i++) {
        process.stdout.write('\n\x1b[2K');   // 下移一行 + 清当前行
      }
      process.stdout.write('\x1b[u');        // 复位光标
      lastPanelLines = [];
      panelHeight = 0;
    }
    
    // render() 同样不要使用 \x1b[J,改成"先 clearPanel(),再按行写入"。

    额外建议:面板渲染换用 readline.cursorTo()readline.clearScreenDown() 的 stream 版本,避免直接写 ANSI 序列(更易跨终端兼容)。


3.11 无 SIGWINCH 监听 + 颜色未走 NO_COLOR 通道

  • 位置:src/ui/command-panel.ts:15-18, 169

  • 现状:terminalWidth = process.stdout.columns || 80 只在 render 当下取一次;终端 resize 后面板会错位。SELECTED = chalk.bgHex(...) 硬编码颜色,未尊重 NO_COLOR / FORCE_COLOR=0 / 非 TTY 输出。

  • 建议修复:

    import { isatty } from 'tty';
    const useColor = process.env.NO_COLOR !== '1' && isatty(1);
    const SELECTED = useColor ? chalk.bgHex('#1E293B').hex('#E2E8F0') : (s: string) => `[${s}]`;
    
    // 在 cli 启动处:
    process.stdout.on('resize', () => {
      if (isPanelVisible()) {
        // 重新渲染(隐藏再显示)
        const f = (currentFilter as string);
        hideCommandPanel();
        showCommandPanel(f);
      }
    });

四、Low(工程化 / 文档 / 依赖)

4.1 缺 CONTRIBUTING.md、issue/PR 模板与 lint 配置

  • 现状:仓库根目录无 .github/,无 CONTRIBUTING.md、无 .eslintrc.* / .prettierrc.* 文件;package.jsonlint / format 脚本依赖外部默认配置,易跑出不可复现的结果。
  • 建议修复:新建以下文件骨架(可作为单独 PR):
    • .github/ISSUE_TEMPLATE/bug_report.md(参考 GitHub 官方模板)
    • .github/ISSUE_TEMPLATE/feature_request.md
    • .github/PULL_REQUEST_TEMPLATE.md
    • CONTRIBUTING.md(包含本地开发、跑测试、提交格式、PR 检查项)
    • .eslintrc.cjs(继承 @typescript-eslint/recommended + prettier)
    • .prettierrc(2 空格、单引号、行宽 100,与现有代码风格对齐)

4.2 package.json 中含未使用的 React 系列依赖

  • 位置:package.json:dependencies

  • 现状:react@^19.2.6 / react-compiler-runtime@^1.0.0 / react-reconciler@^0.33.0 / bidi-js@^1.0.3src/ 中 0 处 from 'react' 引用(grep -rE "from ['\"]react" src/ 为空)。

  • 风险:增加安装体积(React 19 全家桶约 5+ MB)、提升潜在 CVE 暴露面、误导贡献者以为存在 React UI。

  • 建议修复:

    // 若不计划做 React UI:删除
    // 若计划做(roadmap v0.2 ?):放入 optionalDependencies 或注释说明

    执行前用 depcheck 二次验证,避免误删动态 require。


4.3 测试覆盖偏低,UI 与异常路径无回归保护

  • 位置:src/ 91 个 TS 文件 vs tests/ 26 个测试文件
  • 问题:UI 渲染层(command-panel.tsmarkdown.tsbox.ts)、query 主循环、MCP 重连、exec_command 截断 — 这些近期都修过 bug,但都没有对应单测。
  • 建议修复:
    • command-panel.ts 加 snapshot test(mock stdout,断言写入序列)
    • query() 加单测:工具抛错、abortSignal 中途触发、strategyExhausted 路径
    • exec_command 加单测:stdout/stderr 双截断、超时、ENOENT
    • SseTransport.attemptReconnect 加 fake-timer 测指数退避
    • 目标:覆盖率 v0.1.11 提到 60%+,v0.2 提到 75%+

4.4 src/tools/index.ts 1132 行,13+ 工具内联

  • 位置:src/tools/index.ts

  • 建议:按工具族拆分,每个文件单独导出,在 tools/index.ts 中只做聚合 + 注册。同时抽出通用参数校验:

    // tools/_validate.ts
    export function requireString(args: Record<string, unknown>, key: string, tool: string): string {
      const v = args[key];
      if (typeof v !== 'string' || !v) {
        throw new ToolArgError(`${tool} requires string parameter "${key}"`);
      }
      return v;
    }

    再在每个工具的 execute 入口使用,统一错误消息格式。


4.5 MEMORY.md 超过 25KB 后静默丢条目

  • 位置:src/memory/storage.ts:265-278

  • 现状:

    if (content.length > MAX_ENTRYPOINT_BYTES) {
      while (content.length > MAX_ENTRYPOINT_BYTES && lines.length > 10) {
        lines.pop();         // 静默丢失尾部条目
      }
    }

    注意:contentwhile 中未重新计算,这是一个 bug — lines.pop() 不会改变 content 长度,循环条件永远成立,会把 lines 一直 pop 到 10 行。

  • 建议修复:

    let joined = lines.join('\n');
    while (joined.length > MAX_ENTRYPOINT_BYTES && lines.length > 10) {
      lines.pop();
      joined = lines.join('\n');
    }
    if (lines[lines.length - 1] !== '> WARNING: MEMORY.md truncated. Some entries omitted.') {
      lines.push('', '> WARNING: MEMORY.md truncated. Some entries omitted.');
    }
    atomicWriteFileSync(getEntrypointPath(projectPath), lines.join('\n'));

    这条其实属于 bug,可单独提一个 fix-pr。


五、关于实施节奏的建议

阶段 推荐合入项
v0.1.11 hotfix 周期 2.1 / 2.2 / 2.3 / 3.3 / 3.9 / 4.5(全是稳定性 bug)
v0.1.12 quality 专项 2.4(typing)、3.1 / 3.2 / 3.6
v0.2.0 security 强化 3.7 / 3.4 / 3.11、配合补充 bash flag 拦截能力
v0.2.x 工程化 4.1 / 4.2 / 4.3 / 4.4 / 3.10

建议每个分组拆 1 个独立 PR,避免单 PR 体积过大、review 成本高。优先 hotfix 部分(均为现网可触发问题),其余按 roadmap 排期。


六、贡献意愿

如果维护者认可上述改动方向,我愿意按以下范围认领 PR:

  • 2.1 Brain fire-and-forget(改动小、影响可控)
  • 2.3 harness error cast(2 行修改)
  • 4.5 MEMORY.md while 死循环 bug(可独立)
  • 4.1 .github/ 模板与 CONTRIBUTING.md 骨架
  • 其他视维护者优先级而定

请在评论中告知偏好(全部认领 / 拒绝某些 / 自己来),我会按你给出的拆分粒度依次提交 PR。


审计方法说明:基于 commit 98a386f 静态阅读,未做动态 profiling 或运行时验证,部分性能结论(如 3.5、3.8)需在真实负载下复测。若某条与你掌握的设计意图不符,请直接 close,会再讨论。

整体而言,OpenHorse 在 v0.1.10 已经具备非常完整的 agent 框架雏形 — Harness 治理、MCP、memory、多 agent 路由的分层都很清晰;以上建议主要面向鲁棒性、类型严格度与社区基建三方面提升,期待在 v0.2 看到这些方向的迭代。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions