Phase5: All-RPC Architecture & Ask Mode Implementation Plan
1. 目标与范围
交付物
| # |
交付物 |
可验证标准 |
| 1 |
Ask 模式 |
Runtime 提供带会话上下文、支持压缩的单轮推理路径(无工具、无预算、无验收),供诊断与 IDM 使用 |
| 2 |
诊断链路 RPC 化 |
CLI diag 命令和 ptyproxy 通过 Gateway RPC 通信,移除 Unix Domain Socket 信令 |
| 3 |
IDM 改良 |
IDM 从 gateway.run(完整 ReAct)切换到 gateway.ask,延迟降低,工具权限彻底移除 |
| 4 |
Windows 诊断 |
neocode diag 和 neocode shell 在 Windows 11 CMD/PowerShell 中原生可用 |
非目标
- CLI 管理命令(
provider/model/use/config)不改动,维持直接文件读写
- 不改变 TUI 的主链路
- 不在本期引入除诊断以外的 Ask 使用场景
2. 关键决策与取舍
决策 1:Ask 模式 = 带会话上下文的单轮推理
为什么不是纯无状态?
IDM 模式下一个诊断会话可能持续 10-20 轮 @ai 交互。如果 Ask 完全无状态,ptyproxy 必须自行管理消息历史并在每次调用时完整传入。这带来两个问题:
- 历史膨胀后每次请求的 payload 越来越大,IPC 传输开销线性增长
- ptyproxy 需要感知 compaction 时机并自行裁剪历史——这本质上是 Runtime 层的职责,不应泄漏到 Shell 代理
为什么不是完整 ReAct(Run)?
Run 模式包含预算评估、工具选择、任务验收、checkpoint 等机制,诊断场景不需要这些。对比:
| 维度 |
Run (ReAct) |
Ask (新) |
| 工具定义 |
注入完整工具 schema |
不注入(tool_choice: "none") |
| Provider 调用 |
可能多轮(tool call 循环) |
严格单轮 |
| 预算评估 |
evaluateTurnBudget() |
跳过 |
| 任务验收 |
Acceptance 阶段 |
跳过 |
| Checkpoint |
自动创建 |
不创建 |
| Todo 跟踪 |
有 |
无 |
| 消息持久化 |
写入 session |
写入 AskSession |
| 上下文压缩 |
完整 compact(含 transcript) |
轻量 summarize(见决策 2) |
| 典型延迟(诊断) |
2-5s |
0.5-2s |
做什么?
- Runtime 层新增
AskSession——轻量级会话,仅管理消息历史和上下文
gateway.ask 接受 session_id:若为空则自动创建 AskSession,若已有则复用
- 每次 Ask 调用 = 单次 Provider Generate(
tool_choice: "none")→ 流式返回
- AskSession 不参与 checkpoint、不接受 acceptance、不跟踪 Todo
决策 2:Ask 压缩 = 轻量 Summarize(非 Run 的完整 Compact)
为什么不复用 context/compact?
Run 模式的 compact 机制设计目标是:
- 生成结构化 transcript(task_state、verification_profile、goals 等)
- 保留工具调用和结果
- 支持从 compact 状态恢复完整上下文
- 需要额外一次 Provider 调用来生成 summary
Ask 场景不需要这些:没有工具调用需要保留,不需要从 compact 恢复,诊断上下文的语义简单。
做什么?
AskSession 的压缩机制:
- 触发条件:估算 token 数超过阈值(默认 8000,可配置)
- 压缩方法:将最早的消息对(user + assistant)替换为摘要段落,保留最近 N 轮(默认 5 轮)
- 摘要内容:用户问题要点 + AI 回答要点
- 不做:不生成 transcript 文件、不保存到磁盘、不触发 Provider 调用(纯本地文本操作)
压缩前: [msg1, msg2, msg3, msg4, msg5, msg6, msg7, msg8, msg9, msg10]
压缩后: [summary("msg1-msg5"), msg6, msg7, msg8, msg9, msg10]
这比完整 compact 快得多(无 Provider 调用),且对于诊断场景足够有效。
决策 3:CLI 管理命令不 RPC 化
为什么?
- 鸡生蛋问题:
neocode provider add 等命令需要写配置。如果强制走 RPC,用户必须确保 Gateway 已运行——但添加 provider 很可能是用户的第一步操作
- 配置真相源是文件系统:
~/.neocode/config.yaml 是持久化存储。CLI 写文件 + Gateway 启动时读取是最简单的正确方案
- 收益不成比例:管理命令延迟不敏感(用户手动执行),"状态一致性"对于低频管理操作几乎感知不到
做什么?
- CLI 管理命令(
provider/model/use/config)维持当前直接文件读写
- 如果 Gateway 正在运行,CLI 写入配置后可通过
gateway.reloadConfig 通知刷新(此 RPC 方法可选,本期不做硬要求)
决策 4:Windows ptyproxy 需要 ConPTY 实现
为什么必须本期实现?
neocode shell 是诊断功能的核心入口。如果 Windows 上没有 Shell 代理,用户只能使用 neocode diag --error-log 手动传入日志,体验不完整
- Named Pipe 传输层已经存在(
transport/listen_windows.go),传输层不是瓶颈
- 当前
proxy_windows.go 所有函数返回 errUnsupportedPlatform,这一点必须改变
做什么?
- 使用 Windows ConPTY API(
CreatePseudoConsole / ClosePseudoConsole)实现 proxy_windows.go
- 支持启动 CMD 和 PowerShell
- 输出捕获到 Ring Buffer
- 通过 Named Pipe 连接 Gateway,注册
role=shell
- 支持
triggerAction 通知和 IDM 模式
- 详见第 7 节
3. Ask 模式设计
3.1 AskSession 模型
// AskSession 是 Ask 模式的轻量级会话。
type AskSession struct {
ID string
Workdir string
Skills []string // 已激活的 Skill ID 列表
Messages []AskMessage // 对话历史
CreatedAt time.Time
UpdatedAt time.Time
}
type AskMessage struct {
Role string // "user" | "assistant"
Content string
}
区别于 Run 的 agentsession.Session:
- 没有 Todo 列表
- 没有 ToolCall 记录
- 没有 Checkpoint 状态
- 没有 Budget 信息
- 没有 AgentMode
3.2 RPC 协议
// gateway.ask — 请求
{
"method": "gateway.ask",
"params": {
"session_id": "", // 空 = 自动创建 AskSession
"user_query": "npm install 失败了怎么办?",
"workdir": "/home/user/project", // 首次调用时生效
"skills": ["terminal-diagnosis"] // 首次调用时生效
}
}
// gateway.ask — 流式响应事件
// 事件1: ask_chunk (可多次)
{
"type": "event",
"event": "ask_chunk",
"payload": {
"session_id": "ask-abc123",
"delta": "根据错误信息分析..."
}
}
// 事件2: ask_done (一次)
{
"type": "event",
"event": "ask_done",
"payload": {
"session_id": "ask-abc123",
"full_response": "完整的回答文本...",
"compacted": false, // 本轮是否触发了压缩
"usage": {
"input_tokens": 450,
"output_tokens": 120
}
}
}
// 事件3: ask_error (错误时)
{
"type": "event",
"event": "ask_error",
"payload": {
"session_id": "ask-abc123",
"code": "PROVIDER_ERROR",
"message": "provider returned 429"
}
}
3.3 Runtime 实现路径
在 internal/runtime 中新增 AskService(或 Service 新增 Ask 方法):
func (s *Service) Ask(ctx context.Context, input AskInput) error
Ask() 方法的执行流程:
1. 解析 session_id
├── 空 → 创建新 AskSession(生成 ID,写入 workdir/skills)
└── 非空 → 加载已有 AskSession
2. 追加用户消息到 AskSession.Messages
3. 上下文构建 (context.BuildAskPrompt)
├── System Prompt = 基础指令 + Skill instructions
├── 检查 token 估算
│ ├── 超过阈值 → 轻量压缩(合并早期消息为摘要)
│ └── 未超过 → 直接使用
├── 拼接历史消息
└── 拼接当前用户问题
4. Provider 调用
├── tool_choice: "none"
├── stream: true
└── 事件: ask_chunk (每个 delta)
5. 追加助手回答到 AskSession.Messages
6. 发射 ask_done 事件(含 usage + session_id)
7. 持久化 AskSession(轻量存储,可选)
与 Run() 的代码路径对比:
| 步骤 |
Run() |
Ask() |
| 会话加载 |
acquireSessionLock + LoadSession |
loadAskSession(无锁,无并发) |
| Prompt 构建 |
context.Build() 含工具定义 |
context.BuildAskPrompt() 不含工具 |
| 预算检查 |
evaluateTurnBudget() |
无 |
| Provider |
generateStreamingMessage() |
generateStreamingMessage()(同) |
| 工具执行 |
executeAssistantToolCalls() |
跳过 |
| 验收 |
acceptance.Evaluate() |
跳过 |
| Checkpoint |
MaybeCreateCodeCheckpoint() |
跳过 |
| 消息保存 |
sessionStore.SaveSession() |
saveAskSession()(轻量) |
3.4 上下文构建 (BuildAskPrompt)
BuildAskPrompt 输入:
- askSession (消息历史)
- userQuery (当前问题)
- skills (Skill ID 列表)
- config (模型配置)
构建步骤:
1. 加载基础 System Prompt(不含工具定义的最小指令集)
2. 遍历 skills → 从 Skills Registry 加载 instruction → 拼接
3. 估算当前总 token 数
├── 超阈值 → 执行轻量压缩
│ ├── 保留最近 N 轮消息
│ ├── 将早期消息合并为一段摘要
│ └── 摘要放在 System Prompt 之后、历史消息之前
└── 未超阈值 → 直接使用
4. 拼接最终 prompt:
[System Prompt]
[Skill Instructions]
[压缩摘要(如有)]
[历史消息(最近 N 轮)]
[当前用户问题]
5. 转换为 Provider GenerateRequest(tool_choice: "none")
3.5 轻量压缩算法
输入: messages ([]AskMessage), keepLastN (int)
输出: summary (string), kept ([]AskMessage)
算法:
1. 如果 len(messages)/2 <= keepLastN: 不需要压缩
2. splitPoint = (len(messages) - keepLastN*2)
3. oldMessages = messages[:splitPoint]
4. keptMessages = messages[splitPoint:]
5. summary = 本地文本模板:
"之前的对话摘要:用户询问了[提取的关键问题列表],AI 提供了[关键结论]。"
6. 返回 summary, keptMessages
注意这是本地提取不是模型生成——不调用 Provider。对诊断场景,早期的错误分析上下文可以通过简单拼接保留关键信息。
4. 信令中继 (triggerAction)
4.0 为什么要添加信令中继?
问题的本质:跨进程指令传递。
当前诊断链路中,CLI 进程和 Shell 进程是两个独立的操作系统进程:
CLI 进程 (neocode diag) Shell 进程 (neocode shell)
│ │
│ 用户按下 Ctrl+G 或运行 diag │ 持有终端 Buffer
│ 但它没有终端 Buffer │ 但它不知道用户想触发诊断
│ │
└──── 需要一个通信通道 ──────────────┘
CLI 进程知道"用户想触发诊断",Shell 进程持有"终端报错日志"。两者需要协作才能完成诊断。同样的情况也存在于 IDM 进入(neocode diag -i)和自动诊断模式切换(neocode diag auto on/off)。
现状为什么有问题?
当前这个通信通道是 Unix Domain Socket——一个仅 Unix 平台可用的 IPC 机制。它带来三个问题:
-
平台锁定:Windows 不支持 Unix Domain Socket,导致 neocode diag 系列命令在 Windows 上完全不可用。这不是"功能受限",而是"完全不存在"——proxy_windows.go 中所有信号函数返回 errUnsupportedPlatform
-
双重通信通道:Shell 进程已经通过 GatewayRPCClient 与 Gateway 维持一条 JSON-RPC 长连接(用于执行诊断工具调用),同时又监听一个独立的 Unix Socket 等待 CLI 信号。两条通道做两件事,增加了 Shell 代理的复杂度和故障面
-
Socket 生命周期管理复杂:Socket 文件基于 PID 命名并放在 ~/.neocode/run/ 目录下。CLI 需要通过 --socket flag、环境变量(NEOCODE_DIAG_SOCKET / NEOCODE_IDM_SOCKET)或扫描目录来发现目标 Socket。进程异常退出时 Socket 文件残留、多个 Shell 实例并存时的歧义、权限问题——这些都是已发生的维护负担
信令中继如何解决?
核心思路:Gateway 是唯一的中介。 Shell 进程和 CLI 进程各自只与 Gateway 通信,Gateway 负责指令路由。
CLI 进程 Gateway Shell 进程
│ │ │
│-- triggerAction(sess, act)-->│ │
│ │-- 查找路由表 │
│ │ sess + role=shell │
│ │ → 目标连接 │
│ │ │
│ │-- notification(action) ------->│
│<-- {ok: true} --------------│ │
│ │ │
│ │ [Shell 捕获 Buffer │
│ │ 调用 gateway.ask │
│ │ 流式渲染结果] │
三个好处:
- 平台统一:Gateway 的传输层已经支持 Unix Socket 和 Named Pipe,信令中继天然跨平台
- 单一通道:Shell 进程只需要维护与 Gateway 的一条 RPC 连接,所有指令(信令 + 诊断推理)都走这条连接
- 生命周期简化:不再需要 Socket 文件管理。Shell 连接断开时 Gateway 自动清理路由表,CLI 收到明确的"no active shell"错误
为什么必须经由 Gateway 而不是 CLI 直接连 Shell?
如果 CLI 直接通过某种 IPC 连接 Shell 进程:
- 需要引入新的认证机制(谁有权给 Shell 发指令?)
- Gateway 已有的连接管理和 ACL 体系完全无法复用
- CLI 需要知道 Shell 进程的内部地址(又回到 Socket 发现问题)
Gateway 已经在 CLl 和 Shell 之间——它是双方都信任的中立中介,拥有成熟的认证、ACL 和路由基础设施。信令中继只是在这个基础设施上增加一个路由规则。
4.1 对现有客户端(TUI / Web / 桌面端)的影响评估
结论:零影响。 以下是逐客户端分析。
TUI (Bubble Tea)
TUI 与 Gateway 的交互方式:
- 通过
RemoteRuntimeAdapter → GatewayRPCClient 建立 IPC 连接
- 调用
authenticate → bindStream → gateway.run
- 订阅
gateway.event 流式事件
role 字段:TUI 在 bindStream 时不传 role 参数。Gateway 将其视为 role="" (空角色,等同于"未声明")。StreamRelay 的路由查找使用精确匹配(role="shell"),空角色连接不会被匹配到,因此 TUI 连接永远不会收到 gateway.notification 事件。
triggerAction 方法:TUI 不调用此方法。即便将来 TUI 需要触发 Shell 诊断,也只需在 bindStream 时声明 role="tui" 即可获得权限。
gateway.ask 方法:TUI 不使用。主链路(gateway.run)完全不受影响。
StreamRelay 路由表变更:在 relayBinding 结构体中新增 Role 字段是纯增量变更。已有绑定记录中 Role 为空字符串,不影响现有路由逻辑。
Web (Network Server)
Web 前端通过 HTTP/WebSocket/SSE 连接 Gateway:
- WebSocket 连接用于流式事件订阅
- HTTP POST 用于 JSON-RPC 调用(
gateway.run 等)
与 TUI 相同:不声明 role,不调用 triggerAction,不使用 gateway.ask。零影响。
桌面端 (URL Scheme Daemon)
internal/gateway/adapters/urlscheme/ 下的守护进程仅处理 neocode:// URL scheme 唤醒——它不维持 Shell 连接,不参与诊断链路。零影响。
安全边界
triggerAction 的 ACL 独立于现有的 control-plane ACL:
| 调用方 role |
可调用的 action |
说明 |
cli |
diagnose, idm_enter, auto_on, auto_off, auto_status |
CLI 命令触发 |
tui |
diagnose, idm_enter |
TUI 将来可选支持 |
shell |
无 |
Shell 不能反向发指令 |
| 空 (未声明) |
无 |
向后兼容,无权限 |
gateway.notification 事件仅发送给精确匹配 session_id + role 的连接,不是广播。Web/TUI 连接即使绑定到同一 session,由于 role 不匹配(空 ≠ "shell"),也不会收到 Shell 的通知事件。
4.2 协议定义
// gateway.experimental.triggerAction — 请求
{
"method": "gateway.experimental.triggerAction",
"params": {
"session_id": "pty-shell-abc",
"action": "diagnose", // diagnose | idm_enter | auto_on | auto_off | auto_status
"payload": {} // action 特定数据,可选
}
}
// gateway.experimental.triggerAction — 响应(同步)
{
"ok": true,
"message": "action delivered to shell"
}
// gateway.notification — 服务器推送(发往 role=shell 的连接)
{
"type": "notification",
"method": "gateway.notification",
"params": {
"action": "diagnose",
"payload": {},
"source_session_id": "cli-session-xyz"
}
}
4.3 连接角色注册
修改 gateway.bindStream,增加 role 参数:
{
"method": "gateway.bindStream",
"params": {
"session_id": "pty-shell-abc",
"role": "shell" // 新增: "shell" | "cli" | "tui" (默认)
}
}
Gateway StreamRelay 路由表扩展:
type relayBinding struct {
ConnectionID ConnectionID
SessionID string
Role string // 新增
Channel StreamChannel
}
4.4 triggerAction 执行流程
CLI 进程 Gateway Shell 进程
| | |
|-- triggerAction --------->| |
| (session_id, "diagnose")| |
| |-- 查找路由表 |
| | session_id + role="shell" |
| | → conn_id=shell-42 |
| | |
| |-- notification -------------->|
| | (action="diagnose") |
|<-- {ok: true} ------------| |
| | |
| | [Shell 执行诊断] |
| |<-- gateway.ask -------------|
| | (user_query=buffer) |
| | |
| |-- ask_chunk events --------->|
4.5 错误语义
| 场景 |
响应 |
| 目标 session 无 shell 连接 |
{"ok": false, "message": "no active shell for session X"} |
| shell 连接存在但诊断进行中 |
{"ok": false, "message": "shell busy: diagnosis in-flight"} |
| action 未知 |
{"ok": false, "message": "unknown action: X"} |
| 送达成功 |
{"ok": true, "message": "delivered"} |
4.6 安全约束
triggerAction 仅允许 role=cli 或 role=tui 的连接发起
- Shell 连接不能调用
triggerAction(避免权限提升)
- Action 白名单:
diagnose, idm_enter, auto_on, auto_off, auto_status
5. IDM 改良
5.1 当前 IDM vs 新 IDM
| 维度 |
当前(Run-based) |
新(Ask-based) |
| 推理路径 |
完整 ReAct 循环 |
单次 Provider Generate |
| 工具权限 |
工具 schema 存在(模型可见) |
完全不注入(tool_choice: "none") |
| 上下文管理 |
gateway.run 自动管理 session |
gateway.ask 管理 AskSession |
| 压缩 |
完整 compact(耗时、生成 transcript) |
轻量 summarize(本地,不调 Provider) |
| 典型首响延迟 |
2-5s |
0.5-2s |
| 会话管理 |
创建临时 Runtime Session → 用完删除 |
创建 AskSession → 用完删除 |
| 输出渲染 |
glamour Markdown → ANSI |
同(保留 glamour 做最终格式化) |
5.2 交互流程
用户运行 neocode shell → 自动进入 ptyproxy
ptyproxy 连接 Gateway → authenticate → createSession → bindStream(role=shell)
[用户按 Ctrl+G 或命令以非零退出]
ptyproxy 自动触发诊断:
→ 不需要 triggerAction(自己在 shell 进程内)
→ 直接调用 gateway.ask
session_id="" (新建 AskSession)
user_query=<terminal buffer + error log>
skills=["terminal-diagnosis"]
→ 流式渲染结果到终端
[用户在 IDM 模式中]
用户输入 @ai <问题>
→ ptyproxy 调用 gateway.ask
session_id=<已有 AskSession ID>
user_query=<问题>
→ 流式渲染回答
→ AskSession 自动管理上下文 + 压缩
用户输入 exit → 退出 IDM → ptyproxy 调用 gateway.deleteAskSession
5.3 高亮
上下文自动管理:IDM 从几轮到几十轮的对话,ptproxy 不需要感知压缩逻辑。Runtime 在每次 gateway.ask 时自动检查 token 数并决定是否压缩。
工具权限彻底移除:老 IDM 通过 Skill prompt "禁止"工具调用,但工具 schema 仍然注入在 System Prompt 中,模型可能尝试 call。新 IDM 在 Provider 层设置 tool_choice: "none",模型根本不知道工具的存在。
延迟可预期:Ask 模式每次都只是一次 Provider Generate 调用,没有"模型决定调用工具 → 返回 tool call → Runtime 执行 → 模型再总结"的多轮循环。
6. CLI 诊断迁移
6.1 命令重构
# 触发运行中 Shell 的诊断(通过 Gateway 信令中继)
neocode diag
# 直接诊断一段错误日志(不依赖 Shell,Windows 也可用)
neocode diag --error-log "npm install failed: ECONNREFUSED"
# 从 stdin 读取错误日志进行诊断
cat error.log | neocode diag
# 交互式诊断(需 Shell)
neocode diag -i
# 诊断模式开关(需 Shell)
neocode diag auto on|off|status
6.2 实现方式
neocode diag(触发 Shell 诊断):
1. 初始化 GatewayRPCClient(连接 Named Pipe / Unix Socket)
2. authenticate
3. 解析目标 session_id(通过 --session flag / env / 自动发现)
4. gateway.experimental.triggerAction(session_id, "diagnose")
5. 显示结果(触发成功/失败)
neocode diag --error-log(独立诊断):
1. 初始化 GatewayRPCClient
2. authenticate
3. gateway.ask(session_id="", user_query=error_log, skills=["terminal-diagnosis"])
4. 流式输出 ask_chunk → stdout
5. 显示 ask_done 汇总
neocode diag -i(IDM 进入):
1. 初始化 GatewayRPCClient
2. authenticate
3. gateway.experimental.triggerAction(session_id, "idm_enter")
4. Shell 收到通知后进入 IDM 循环
6.3 关键变更点
shell_diag_commands.go:移除 sendDiagnoseSignalFn → ptyproxy.SendDiagnoseSignal(Unix Socket),改为调用 gatewayclient.GatewayRPCClient 的方法
- 新增
--error-log flag 和 stdin 管道读取
- 新增
--session flag 用于指定目标 Shell Session
7. Windows ptyproxy (ConPTY)
7.1 目标
使 neocode shell 在 Windows 10+(1809+)上可用,支持:
- CMD 和 PowerShell
- 终端输出捕获到 Ring Buffer
- Gateway RPC 连接(Named Pipe)
triggerAction 通知接收
- IDM 模式
- 自动诊断(auto mode)
7.2 技术方案
使用 Windows ConPTY API(CreatePseudoConsole / ClosePseudoConsole / ResizePseudoConsole),通过 golang.org/x/sys/windows 的 syscall 封装直接调用。
ConPTY 创建流程:
1. 创建双向 Pipe
├── inputPipe (ptyproxy → ConPTY: 用户输入)
└── outputPipe (ConPTY → ptyproxy: 终端输出)
2. CreatePseudoConsole(size, inputPipe, outputPipe, 0)
→ HPCON (pseudo console handle)
3. 构建 STARTUPINFOEX
├── InitializeProcThreadAttributeList
└── UpdateProcThreadAttribute(PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, HPCON)
4. CreateProcess(cmd.exe / powershell.exe, ..., EXTENDED_STARTUPINFO_PRESENT)
→ 进程在 ConPTY 中运行
5. 主循环
├── goroutine 1: 读取 outputPipe → Ring Buffer + 终端输出
├── goroutine 2: 读取用户输入 → 写入 inputPipe
└── goroutine 3: Gateway RPC 通知监听
7.3 与 Unix ptyproxy 的差异处理
| 功能 |
Unix |
Windows |
| PTY 创建 |
github.com/creack/pty |
ConPTY API(syscall) |
| 终端大小同步 |
pty.InheritSize + SIGWINCH |
ResizePseudoConsole + Window Buffer Size Event |
| 原始模式 |
term.MakeRaw |
SetConsoleMode (ENABLE_VIRTUAL_TERMINAL_INPUT) |
| 信号转发 |
SIGINT/SIGTERM → Process.Signal |
GenerateConsoleCtrlEvent |
| ANSI 支持 |
原生 |
需启用 ENABLE_VIRTUAL_TERMINAL_PROCESSING |
| Socket 信令 |
Unix Domain Socket |
Named Pipe(RPC 通知,同 Unix 改造后) |
| Gateway 连接 |
Unix Socket |
Named Pipe (winio.DialPipe) |
7.4 文件结构
internal/ptyproxy/
├── proxy_unix.go # Unix PTY 实现(现有,改造)
├── proxy_windows.go # Windows: 移除 errUnsupportedPlatform,实现 ConPTY
├── proxy_windows_conpty.go # ConPTY API 封装
├── proxy_common.go # 跨平台共享逻辑(Ring Buffer、诊断协调器、IDM)
├── idm_controller.go # IDM 控制器(去掉 //go:build !windows)
├── diagnosis_coordinator.go # 诊断协调器(去掉 //go:build !windows)
└── ring_buffer.go # Ring Buffer(现有)
关键变化:
idm_controller.go 去掉 //go:build !windows 构建标签
diagnosis_coordinator.go 去掉 //go:build !windows 构建标签
proxy_common.go 提取 PTY 生命周期管理中的跨平台逻辑
7.5 ConPTY 依赖
使用 golang.org/x/sys/windows 中的 syscall 原语,不引入新的第三方库。需要封装的 API:
//go:build windows
// Syscall 封装
var (
procCreatePseudoConsole = kernel32.NewProc("CreatePseudoConsole")
procClosePseudoConsole = kernel32.NewProc("ClosePseudoConsole")
procResizePseudoConsole = kernel32.NewProc("ResizePseudoConsole")
)
// CreatePseudoConsole 创建伪控制台
func CreatePseudoConsole(size windows.Coord, input, output windows.Handle, flags uint32) (windows.Handle, error)
// ClosePseudoConsole 关闭伪控制台
func ClosePseudoConsole(hpc windows.Handle) error
// ResizePseudoConsole 调整伪控制台大小
func ResizePseudoConsole(hpc windows.Handle, size windows.Coord) error
PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE 常量值: 0x20016(Windows SDK 定义)。
8. 任务清单(含依赖关系)
Phase 5a: Ask 模式基础 [阻塞 5b/5c/5d]
Phase 5b: 信令 RPC 化 [依赖 5a]
Phase 5c: IDM 切换到 Ask [依赖 5a, 5b]
Phase 5d: CLI 诊断迁移 [依赖 5a, 5b]
Phase 5e: Windows ptyproxy [依赖 5b, 5c]
Phase 5f: 测试与验证 [依赖全部前序]
9. 风险与缓解
| 风险 |
概率 |
影响 |
缓解措施 |
| Ask 压缩过于激进导致上下文丢失关键信息 |
中 |
中 |
保留最近 5 轮完整消息 + 摘要包含关键结论;阈值可配置 |
| triggerAction 通知在 Shell 连接瞬时断开时丢失 |
低 |
高 |
triggerAction 返回送达确认;CLI 端收到 no shell 错误后明确提示用户 |
| ConPTY 在旧版 Windows (< 1809) 不可用 |
低 |
中 |
启动时检测 OS 版本,给出明确的升级提示 |
| IDM 切换到 Ask 后回答质量下降(缺少工具上下文) |
低 |
中 |
诊断场景本身不需要工具;Skill prompt 质量是主要变量,P5a-6 中标准化 |
| Named Pipe ACL 与杀毒软件冲突 |
低 |
中 |
复用现有 SDDL ACL(已验证),必要时提供 --acl-mode 覆盖 |
10. 里程碑
| 里程碑 |
内容 |
可验证标准 |
| M1: Ask 就绪 |
P5a 全部完成 |
gateway.ask 可通过 RPC 调用,返回流式回答;AskSession 支持多轮上下文 + 自动压缩 |
| M2: 信令就绪 |
P5b 全部完成 |
Shell 连接通过 Gateway RPC 收到 diagnose 通知;Unix Socket 代码全部移除 |
| M3: IDM 切换 |
P5c 全部完成 |
IDM @ai 通过 Ask 模式回答;工具 schema 完全不注入 |
| M4: CLI 就绪 |
P5d 全部完成 |
neocode diag --error-log 在任何平台可用 |
| M5: Windows 就绪 |
P5e 全部完成 |
neocode shell 在 Windows 11 CMD/PowerShell 中运行 |
| M6: 交付 |
P5f 全部完成 |
全平台测试通过,Socket 代码清理干净 |
11. 不变约束
- 主链路不破坏:
TUI → Gateway → Runtime → Provider → Tools 闭环保持
- 不分层接线:ptyproxy 不直接调用 Provider;上下文构建不走 CLI 层
- 工具定义隔离:Ask 模式下 System Prompt 不含工具定义;Provider 层
tool_choice: "none"
- 配置安全:不硬编码路径/密钥/提示词;配置通过 config 注入
- 向后兼容:
neocode diag CLI 接口不变(仅底层实现变);neocode shell CLI 接口不变
- 构建标签规范:平台特定代码使用
//go:build 标签;跨平台逻辑不加标签
Phase5: All-RPC Architecture & Ask Mode Implementation Plan
1. 目标与范围
交付物
diag命令和 ptyproxy 通过 Gateway RPC 通信,移除 Unix Domain Socket 信令gateway.run(完整 ReAct)切换到gateway.ask,延迟降低,工具权限彻底移除neocode diag和neocode shell在 Windows 11 CMD/PowerShell 中原生可用非目标
provider/model/use/config)不改动,维持直接文件读写2. 关键决策与取舍
决策 1:Ask 模式 = 带会话上下文的单轮推理
为什么不是纯无状态?
IDM 模式下一个诊断会话可能持续 10-20 轮
@ai交互。如果 Ask 完全无状态,ptyproxy 必须自行管理消息历史并在每次调用时完整传入。这带来两个问题:为什么不是完整 ReAct(Run)?
Run 模式包含预算评估、工具选择、任务验收、checkpoint 等机制,诊断场景不需要这些。对比:
tool_choice: "none")evaluateTurnBudget()做什么?
AskSession——轻量级会话,仅管理消息历史和上下文gateway.ask接受session_id:若为空则自动创建 AskSession,若已有则复用tool_choice: "none")→ 流式返回决策 2:Ask 压缩 = 轻量 Summarize(非 Run 的完整 Compact)
为什么不复用
context/compact?Run 模式的 compact 机制设计目标是:
Ask 场景不需要这些:没有工具调用需要保留,不需要从 compact 恢复,诊断上下文的语义简单。
做什么?
AskSession 的压缩机制:
这比完整 compact 快得多(无 Provider 调用),且对于诊断场景足够有效。
决策 3:CLI 管理命令不 RPC 化
为什么?
neocode provider add等命令需要写配置。如果强制走 RPC,用户必须确保 Gateway 已运行——但添加 provider 很可能是用户的第一步操作~/.neocode/config.yaml是持久化存储。CLI 写文件 + Gateway 启动时读取是最简单的正确方案做什么?
provider/model/use/config)维持当前直接文件读写gateway.reloadConfig通知刷新(此 RPC 方法可选,本期不做硬要求)决策 4:Windows ptyproxy 需要 ConPTY 实现
为什么必须本期实现?
neocode shell是诊断功能的核心入口。如果 Windows 上没有 Shell 代理,用户只能使用neocode diag --error-log手动传入日志,体验不完整transport/listen_windows.go),传输层不是瓶颈proxy_windows.go所有函数返回errUnsupportedPlatform,这一点必须改变做什么?
CreatePseudoConsole/ClosePseudoConsole)实现proxy_windows.gorole=shelltriggerAction通知和 IDM 模式3. Ask 模式设计
3.1 AskSession 模型
区别于 Run 的
agentsession.Session:3.2 RPC 协议
3.3 Runtime 实现路径
在
internal/runtime中新增AskService(或Service新增Ask方法):Ask()方法的执行流程:与 Run() 的代码路径对比:
acquireSessionLock+LoadSessionloadAskSession(无锁,无并发)context.Build()含工具定义context.BuildAskPrompt()不含工具evaluateTurnBudget()generateStreamingMessage()generateStreamingMessage()(同)executeAssistantToolCalls()acceptance.Evaluate()MaybeCreateCodeCheckpoint()sessionStore.SaveSession()saveAskSession()(轻量)3.4 上下文构建 (BuildAskPrompt)
3.5 轻量压缩算法
注意这是本地提取不是模型生成——不调用 Provider。对诊断场景,早期的错误分析上下文可以通过简单拼接保留关键信息。
4. 信令中继 (triggerAction)
4.0 为什么要添加信令中继?
问题的本质:跨进程指令传递。
当前诊断链路中,CLI 进程和 Shell 进程是两个独立的操作系统进程:
CLI 进程知道"用户想触发诊断",Shell 进程持有"终端报错日志"。两者需要协作才能完成诊断。同样的情况也存在于 IDM 进入(
neocode diag -i)和自动诊断模式切换(neocode diag auto on/off)。现状为什么有问题?
当前这个通信通道是 Unix Domain Socket——一个仅 Unix 平台可用的 IPC 机制。它带来三个问题:
平台锁定:Windows 不支持 Unix Domain Socket,导致
neocode diag系列命令在 Windows 上完全不可用。这不是"功能受限",而是"完全不存在"——proxy_windows.go中所有信号函数返回errUnsupportedPlatform双重通信通道:Shell 进程已经通过
GatewayRPCClient与 Gateway 维持一条 JSON-RPC 长连接(用于执行诊断工具调用),同时又监听一个独立的 Unix Socket 等待 CLI 信号。两条通道做两件事,增加了 Shell 代理的复杂度和故障面Socket 生命周期管理复杂:Socket 文件基于 PID 命名并放在
~/.neocode/run/目录下。CLI 需要通过--socketflag、环境变量(NEOCODE_DIAG_SOCKET/NEOCODE_IDM_SOCKET)或扫描目录来发现目标 Socket。进程异常退出时 Socket 文件残留、多个 Shell 实例并存时的歧义、权限问题——这些都是已发生的维护负担信令中继如何解决?
核心思路:Gateway 是唯一的中介。 Shell 进程和 CLI 进程各自只与 Gateway 通信,Gateway 负责指令路由。
三个好处:
为什么必须经由 Gateway 而不是 CLI 直接连 Shell?
如果 CLI 直接通过某种 IPC 连接 Shell 进程:
Gateway 已经在 CLl 和 Shell 之间——它是双方都信任的中立中介,拥有成熟的认证、ACL 和路由基础设施。信令中继只是在这个基础设施上增加一个路由规则。
4.1 对现有客户端(TUI / Web / 桌面端)的影响评估
结论:零影响。 以下是逐客户端分析。
TUI (Bubble Tea)
TUI 与 Gateway 的交互方式:
RemoteRuntimeAdapter→GatewayRPCClient建立 IPC 连接authenticate→bindStream→gateway.rungateway.event流式事件role字段:TUI 在bindStream时不传role参数。Gateway 将其视为role=""(空角色,等同于"未声明")。StreamRelay 的路由查找使用精确匹配(role="shell"),空角色连接不会被匹配到,因此 TUI 连接永远不会收到gateway.notification事件。triggerAction方法:TUI 不调用此方法。即便将来 TUI 需要触发 Shell 诊断,也只需在bindStream时声明role="tui"即可获得权限。gateway.ask方法:TUI 不使用。主链路(gateway.run)完全不受影响。StreamRelay 路由表变更:在
relayBinding结构体中新增Role字段是纯增量变更。已有绑定记录中 Role 为空字符串,不影响现有路由逻辑。Web (Network Server)
Web 前端通过 HTTP/WebSocket/SSE 连接 Gateway:
gateway.run等)与 TUI 相同:不声明
role,不调用triggerAction,不使用gateway.ask。零影响。桌面端 (URL Scheme Daemon)
internal/gateway/adapters/urlscheme/下的守护进程仅处理neocode://URL scheme 唤醒——它不维持 Shell 连接,不参与诊断链路。零影响。安全边界
triggerAction的 ACL 独立于现有的 control-plane ACL:clituishellgateway.notification事件仅发送给精确匹配session_id + role的连接,不是广播。Web/TUI 连接即使绑定到同一 session,由于 role 不匹配(空 ≠ "shell"),也不会收到 Shell 的通知事件。4.2 协议定义
4.3 连接角色注册
修改
gateway.bindStream,增加role参数:{ "method": "gateway.bindStream", "params": { "session_id": "pty-shell-abc", "role": "shell" // 新增: "shell" | "cli" | "tui" (默认) } }Gateway StreamRelay 路由表扩展:
4.4 triggerAction 执行流程
4.5 错误语义
{"ok": false, "message": "no active shell for session X"}{"ok": false, "message": "shell busy: diagnosis in-flight"}{"ok": false, "message": "unknown action: X"}{"ok": true, "message": "delivered"}4.6 安全约束
triggerAction仅允许role=cli或role=tui的连接发起triggerAction(避免权限提升)diagnose,idm_enter,auto_on,auto_off,auto_status5. IDM 改良
5.1 当前 IDM vs 新 IDM
tool_choice: "none")gateway.run自动管理 sessiongateway.ask管理 AskSession5.2 交互流程
5.3 高亮
上下文自动管理:IDM 从几轮到几十轮的对话,ptproxy 不需要感知压缩逻辑。Runtime 在每次
gateway.ask时自动检查 token 数并决定是否压缩。工具权限彻底移除:老 IDM 通过 Skill prompt "禁止"工具调用,但工具 schema 仍然注入在 System Prompt 中,模型可能尝试 call。新 IDM 在 Provider 层设置
tool_choice: "none",模型根本不知道工具的存在。延迟可预期:Ask 模式每次都只是一次 Provider Generate 调用,没有"模型决定调用工具 → 返回 tool call → Runtime 执行 → 模型再总结"的多轮循环。
6. CLI 诊断迁移
6.1 命令重构
6.2 实现方式
neocode diag(触发 Shell 诊断):neocode diag --error-log(独立诊断):neocode diag -i(IDM 进入):6.3 关键变更点
shell_diag_commands.go:移除sendDiagnoseSignalFn→ptyproxy.SendDiagnoseSignal(Unix Socket),改为调用gatewayclient.GatewayRPCClient的方法--error-logflag 和 stdin 管道读取--sessionflag 用于指定目标 Shell Session7. Windows ptyproxy (ConPTY)
7.1 目标
使
neocode shell在 Windows 10+(1809+)上可用,支持:triggerAction通知接收7.2 技术方案
使用 Windows ConPTY API(
CreatePseudoConsole/ClosePseudoConsole/ResizePseudoConsole),通过golang.org/x/sys/windows的 syscall 封装直接调用。ConPTY 创建流程:
7.3 与 Unix ptyproxy 的差异处理
github.com/creack/ptypty.InheritSize+ SIGWINCHResizePseudoConsole+ Window Buffer Size Eventterm.MakeRawSetConsoleMode(ENABLE_VIRTUAL_TERMINAL_INPUT)GenerateConsoleCtrlEventENABLE_VIRTUAL_TERMINAL_PROCESSINGwinio.DialPipe)7.4 文件结构
关键变化:
idm_controller.go去掉//go:build !windows构建标签diagnosis_coordinator.go去掉//go:build !windows构建标签proxy_common.go提取 PTY 生命周期管理中的跨平台逻辑7.5 ConPTY 依赖
使用
golang.org/x/sys/windows中的 syscall 原语,不引入新的第三方库。需要封装的 API:PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE 常量值:
0x20016(Windows SDK 定义)。8. 任务清单(含依赖关系)
Phase 5a: Ask 模式基础 [阻塞 5b/5c/5d]
P5a-1: 协议与类型定义
internal/gateway/protocol/:定义MethodGatewayAsk常量、AskParams、AskResultinternal/gateway/types.go:新增FrameActionAsk、FrameActionDeleteAskSessionask_chunk、ask_done、ask_errorP5a-2: AskSession 模型
internal/runtime/ask_session.go:定义AskSession结构体internal/runtime/ask_store.go:AskSession 持久化(简单文件或 SQLite,与 agent session 隔离)P5a-3: 上下文构建
internal/context/ask_prompt.go:实现BuildAskPrompt()P5a-4: Runtime Ask 实现
internal/runtime/ask.go:实现Service.Ask(ctx, AskInput) errortool_choice: "none")P5a-5: Gateway Ask dispatch
internal/gateway/contracts.go:RuntimePort接口新增Ask()internal/gateway/rpc_dispatch.go:注册gateway.askhandlerinternal/gateway/stream_relay.go:确保 ask_chunk/done 事件流式路由P5a-6: 诊断 Skill 标准化
idm_controller.go中的terminalDiagnosisSkillMarkdown常量提取为 Skill 文件~/.neocode/skills/terminal-diagnosis/SKILL.md可被 Skills Registry 加载P5a-7: 客户端支持
internal/gateway/client/gateway_rpc_client.go:新增Ask()方法Phase 5b: 信令 RPC 化 [依赖 5a]
P5b-1: triggerAction 协议
internal/gateway/protocol/:定义MethodGatewayExperimentalTriggerAction、TriggerActionParams、TriggerActionResultinternal/gateway/types.go:新增FrameActionTriggerActionActionDiagnose、ActionIDMEnter、ActionAutoOn、ActionAutoOff、ActionAutoStatusP5b-2: StreamRelay 角色管理
internal/gateway/connection_context.go:ConnectionRegistration增加Role字段internal/gateway/stream_relay.go:bindStreamhandler 解析role参数FindConnectionsBySessionAndRole(sessionID, role)P5b-3: triggerAction dispatch
internal/gateway/rpc_dispatch.go:注册gateway.experimental.triggerActionhandlerrole=cli|tui的连接可发起P5b-4: ptyproxy Unix 适配
internal/ptyproxy/proxy_unix.go:listenDiagSocket/listenIDMSocket/serveDiagSocket/serveIDMSocketRunManualShell启动时执行bindStream(role="shell")diagnose/idm_enter/auto_on/auto_off通知 → 执行对应逻辑P5b-5: 废弃 Socket 信令文件
internal/ptyproxy/ipc_protocol.go(IPC 指令结构体)internal/ptyproxy/socket_paths.go(Socket 路径管理)proxy_windows.go中的 stub(SendDiagnoseSignal 等函数不再需要)resolveDiagSocketPath等)Phase 5c: IDM 切换到 Ask [依赖 5a, 5b]
P5c-1: IDM Ask 会话管理
internal/ptyproxy/idm_controller.go:askSessionID字段(跟踪当前 AskSession)Enter()时创建AskSession(调用gateway.createAskSession或首次gateway.ask)Exit()时删除AskSessionP5c-2: 替换 sendAIMessage
gateway.run→gateway.askbindStream/run/cancel/createSession/deleteSession/activateSessionSkill调用ask_chunk+ask_done事件P5c-3: 移除构建标签
idm_controller.go:移除//go:build !windowsdiagnosis_coordinator.go:移除//go:build !windowsPhase 5d: CLI 诊断迁移 [依赖 5a, 5b]
P5d-1: diag 命令重构
shell_diag_commands.go:defaultDiagCommandRunner→ 改用gatewayclient.GatewayRPCClient+triggerAction--error-logflag--sessionflagP5d-2: 直接诊断路径
neocode diag --error-log "..."→gateway.ask(不依赖 Shell)gateway.askP5d-3: 移除旧依赖
ptyproxy.SendDiagnoseSignal/ptyproxy.SendIDMEnterSignal等函数Phase 5e: Windows ptyproxy [依赖 5b, 5c]
P5e-1: ConPTY API 封装
internal/ptyproxy/proxy_windows_conpty.go:CreatePseudoConsole/ClosePseudoConsole/ResizePseudoConsolesyscall 封装startProcessInConPty(shellPath, workdir, hpc) (*os.Process, error)P5e-2: Windows PTY 主循环
internal/ptyproxy/proxy_windows.go:RunManualShell()(替换errUnsupportedPlatform)P5e-3: 跨平台共享逻辑提取
internal/ptyproxy/proxy_common.go(可选)或直接在现有文件中减少重复newDiagnosisCoordinator、consumeDiagSignals、idmController等已在 5c-3 中移除构建标签,直接复用P5e-4: Windows 终端设置
ENABLE_VIRTUAL_TERMINAL_PROCESSING确保 ANSI 转义序列正常工作ENABLE_VIRTUAL_TERMINAL_INPUT处理鼠标和特殊键Phase 5f: 测试与验证 [依赖全部前序]
P5f-1: Ask 模式单元测试
Ask()正常路径 / 空输入 / 空 session_id(自动创建)P5f-2: triggerAction 集成测试
P5f-3: IDM Ask 对比测试
gateway.runvsgateway.ask延迟对比P5f-4: 跨进程联动测试
neocode shell(Unix)neocode diagP5f-5: Windows 专项测试
neocode shell启动 CMD / PowerShellneocode diag --error-log "..."通过 Named Pipe 获取诊断neocode diag触发 Shell 诊断(通过 triggerAction)P5f-6: 回归测试
neocode→ TUI → Gateway → Runtime → Tools)provider list,model list,use)9. 风险与缓解
no shell错误后明确提示用户--acl-mode覆盖10. 里程碑
gateway.ask可通过 RPC 调用,返回流式回答;AskSession 支持多轮上下文 + 自动压缩diagnose通知;Unix Socket 代码全部移除@ai通过 Ask 模式回答;工具 schema 完全不注入neocode diag --error-log在任何平台可用neocode shell在 Windows 11 CMD/PowerShell 中运行11. 不变约束
TUI → Gateway → Runtime → Provider → Tools闭环保持tool_choice: "none"neocode diagCLI 接口不变(仅底层实现变);neocode shellCLI 接口不变//go:build标签;跨平台逻辑不加标签