Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/tray/src/hosts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ mod snapshot;
mod split;
#[cfg(test)]
mod tests;
mod timeouts;
mod transaction;

use crate::detect::executable;
Expand Down
8 changes: 4 additions & 4 deletions apps/tray/src/hosts/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ impl Hosts {
*latest = Some(json!({}));
}
patch(latest.as_mut().unwrap(), before.entry.as_ref(), desired);
timeouts::ensure(latest.as_mut().unwrap(), host)
.map_err(|error| format!("write_failed:{error}"))?;
} else {
*latest = None;
}
Expand Down Expand Up @@ -405,11 +407,9 @@ impl Hosts {
));
}
desired = Some(if id == "deepseek" {
// dsh cuts a tool call off at 60 s; `wait` blocks for up to 300 s.
json!({"serverName":"agentnave","transport":"stdio","command":path,"env":host_env(host),"toolCallTimeoutMs":310_000})
json!({"serverName":"agentnave","transport":"stdio","command":path,"env":host_env(host)})
} else if id == "zcode" {
// ZCode stops a tool call after 30 s by default; `wait` blocks up to 300 s.
json!({"type":"stdio","command":path,"args":[],"env":host_env(host),"timeoutMs":310000})
json!({"type":"stdio","command":path,"args":[],"env":host_env(host)})
} else if id == "hermes" {
json!({"command":path,"args":[],"env":host_env(host),"enabled":true})
} else {
Expand Down
2 changes: 2 additions & 0 deletions apps/tray/src/hosts/refresh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ impl Hosts {
return Ok(());
};
set_host_env(desired, host, "takeover_failed")?;
timeouts::ensure(desired, host)
.map_err(|error| format!("takeover_failed:{error}"))?;
// Repair missing commands, or migrate our existing launchers once.
if let Some(path) = self.launcher()
&& let Some(command) = layer.command(desired)
Expand Down
3 changes: 2 additions & 1 deletion apps/tray/src/hosts/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::data::Reader;
use tempfile::TempDir;

mod reload_guidance;
mod timeouts;
mod versioned;

fn fixture() -> (TempDir, Hosts) {
Expand Down Expand Up @@ -504,7 +505,7 @@ fn all_host_formats_connect_refresh_and_remove_without_running_host_clis() {
// dsh would otherwise cut `wait` off after 60 s.
assert_eq!(
h.read().unwrap().entry.unwrap()["toolCallTimeoutMs"],
310_000
720_000
);
}
assert_eq!(
Expand Down
87 changes: 87 additions & 0 deletions apps/tray/src/hosts/tests/timeouts.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
use super::*;

#[test]
fn refresh_raises_existing_tool_timeouts_and_keeps_other_configurations() {
for (id, key, minimum) in [
("codex", "tool_timeout_sec", 720),
("claude", "timeout", 720_000),
("antigravity", "timeoutSeconds", 720),
("pi", "timeout", 720),
("hermes", "timeout", 720),
("deepseek", "toolCallTimeoutMs", 720_000),
("zcode", "timeoutMs", 720_000),
] {
let (_temp, mut hosts) = fixture();
let h = host(&hosts, id);
executable_file(
&hosts.context.search[0].join(h.binary),
"#!/bin/sh\nexit 99\n",
);
save(&h.config, &seed(&h));
let before = h.read().unwrap();
let mut entry = json!({"command":"agentnave-mcp", "env":{"KEEP":"yes"}, "opaque":true});
entry[key] = json!(60);
save(&h.config, &before.render(Some(&entry)).unwrap());
hosts.refresh();
let updated = h.read().unwrap();
let entry = updated.entry.as_ref().unwrap();
assert_eq!(entry[key], minimum, "{id}: {entry}");
assert_eq!(entry["env"]["KEEP"], "yes");
assert_eq!(entry["opaque"], true);
assert_eq!(updated.rest, before.rest, "{id}");
let bytes = fs::read(&h.config).unwrap();
hosts.refresh();
assert_eq!(fs::read(&h.config).unwrap(), bytes, "{id}");
// Reconnecting must not shorten a value increased by the user.
let mut longer = entry.clone();
longer[key] = json!(minimum * 2);
save(&h.config, &updated.render(Some(&longer)).unwrap());
let result = hosts.action("connect", &json!({"id":id})).unwrap();
assert_eq!(result["ok"], true, "{id}: {result}");
assert_eq!(h.read().unwrap().entry.unwrap()[key], minimum * 2, "{id}");
}
}

#[test]
fn native_long_defaults_unlimited_and_grok_tool_overrides_are_preserved() {
let (_temp, hosts) = fixture();
for id in ["grok", "codebuddy", "workbuddy"] {
let mut entry = json!({"command":"agentnave-mcp","env":{"KEEP":"yes"}});
let before = entry.clone();
super::super::timeouts::ensure(&mut entry, &host(&hosts, id)).unwrap();
assert_eq!(entry, before, "{id}");
}
let mut unlimited = json!({"timeoutSeconds":-1});
super::super::timeouts::ensure(&mut unlimited, &host(&hosts, "antigravity")).unwrap();
assert_eq!(unlimited["timeoutSeconds"], -1);
let mut grok =
json!({"tool_timeout_sec":1800,"tool_timeouts":{"wait":60,"start_agent":900,"other":30}});
super::super::timeouts::ensure(&mut grok, &host(&hosts, "grok")).unwrap();
assert_eq!(
grok,
json!({"tool_timeout_sec":1800,"tool_timeouts":{"wait":720,"start_agent":900,"other":30}})
);
}

#[test]
fn invalid_timeout_is_reported_without_rewriting_the_file() {
let (_temp, mut hosts) = fixture();
let h = host(&hosts, "pi");
executable_file(
&hosts.context.search[0].join(h.binary),
"#!/bin/sh\nexit 99\n",
);
let text = r#"{"mcpServers":{"agentnave":{"command":"agentnave-mcp","timeout":"custom"}}}"#;
save(&h.config, text);
hosts.refresh();
assert_eq!(fs::read_to_string(&h.config).unwrap(), text);
let result = hosts.action("connect", &json!({"id":"pi"})).unwrap();
assert_eq!(result["ok"], false);
assert!(
result["message"]
.as_str()
.unwrap()
.contains("timeout 必须为数字")
);
assert_eq!(fs::read_to_string(&h.config).unwrap(), text);
}
45 changes: 45 additions & 0 deletions apps/tray/src/hosts/timeouts.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//! Tool-call limits for AgentNave's ten-minute wait, with two minutes for the reply.
use super::{Host, Value, json};

pub(super) fn ensure(entry: &mut Value, host: &Host) -> Result<(), String> {
let (key, minimum) = match host.boss {
"codex" => ("tool_timeout_sec", 720),
"claude" => ("timeout", 720_000),
"antigravity" => ("timeoutSeconds", 720),
"pi" | "hermes" => ("timeout", 720),
"deepseek" => ("toolCallTimeoutMs", 720_000),
"zcode" => ("timeoutMs", 720_000),
"grok" => ("tool_timeout_sec", 720),
// CodeBuddy reads a host-process variable (default one hour), not a server field.
// WorkBuddy's limit is unverified. Neither belongs in the child service's env.
_ => return Ok(()),
};
// Grok's native default is 6000 s; do not shorten it by adding an override.
if host.boss != "grok" || entry.get(key).is_some() {
raise(entry, key, minimum, host.boss == "antigravity")?;
}
if host.boss == "grok"
&& let Some(overrides) = entry.get_mut("tool_timeouts")
{
if !overrides.is_object() {
return Err("tool_timeouts 必须为对象".into());
}
for tool in ["wait", "start_agent", "run_round"] {
if overrides.get(tool).is_some() {
raise(overrides, tool, 720, false)?;
}
}
}
Ok(())
}

fn raise(entry: &mut Value, key: &str, minimum: u64, unlimited: bool) -> Result<(), String> {
if let Some(current) = entry.get(key) {
let value = current.as_f64().ok_or(format!("{key} 必须为数字"))?;
if (unlimited && value == -1.0) || value >= minimum as f64 {
return Ok(());
}
}
entry[key] = json!(minimum);
Ok(())
}
2 changes: 1 addition & 1 deletion apps/tray/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const SHARED: &str = "## AgentNave 编排偏好
- 探索、规划派 Explorer,实现派 Worker。
- 同一组文件同一时间只有一个 Worker 在写;同一职责续接只传 `session_id`,其余由服务继承;换职责时传新的 `role` 或 `seat` 并给自足简报。
- 本偏好不决定何时评审,用户要求或其规则要求时才评审。评审时有 agentnave-review Skill 就按它定级、选工位,不另派 Explorer;没有时派 Explorer,不交给写这段代码的 Worker,尽量换模型。
- 结果不会主动推送:派发后结束本轮前用 `wait` 收齐全部终态;任务在跑时要先问用户,也先 `wait` 或 `cancel`。
- 结果不会主动推送:派发后结束本轮前用 `wait` 收齐全部终态;每次最多等十分钟,常规省略 `wait_seconds`,`seq` 从 1 开始逐次递增以区分连续等待;任务在跑时要先问用户,也先 `wait` 或 `cancel`。
- `succeeded` 不等于完成,boss 核对产物后验收;失败或取消时先核对现状,再决定续接、重派还是收尾。";

const IMAGE_GENERATION: &str = "\n- 生成图片派生图工位,`seat` 用 `image-generator`,prompt 写清图像需求和产物路径,由它的对话模型自行调用生图服务。";
Expand Down
1 change: 1 addition & 0 deletions docs/adr/0007-wait-any-ready-subset.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- 服务主动终止情形与终态句柄保留七天的边界由 [ADR-0049](0049-service-initiated-termination.md) 补充
- 由 [ADR-0057](0057-boss-answers-native-prompts.md) 补充:待答弹框是新的就绪原因,收件箱同样返回。
- 由 [ADR-0064](0064-dispatch-receipts-steer-to-wait.md) 修订:派发与等待回执在仍有待收结果时带 `next` 引导,派发调用可选顺带等一次。
- 由 [ADR-0066](0066-unified-wait-window-and-call-sequence.md) 修订:所有宿主统一等待十分钟,移除旧的 240 秒档位;连续等待可用 `seq` 区分。

## 背景

Expand Down
1 change: 1 addition & 0 deletions docs/adr/0015-app-host-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- 外部 Skill 不覆盖与所有权校验由 [ADR-0036](0036-app-owns-service-skills.md) 修订
- 配置根目录手填入口由 [ADR-0029](0029-settings-without-free-text.md) 取代
- 宿主接入行按实际配置目录划分的规则由 [ADR-0040](0040-boss-app-from-session-records.md) 补充
- 已有 AgentNave 条目的工具超时下限由 [ADR-0066](0066-unified-wait-window-and-call-sequence.md) 补充。

App 优先的接入需要同时维护 MCP 与配套 Skill。已有手写条目包含用户选择的环境、启停与工具过滤,整体重建会丢失这些选择;已有 Skill 常由跨宿主部署管理器维护。用户级接入由托盘后端管理,核心与工作室服务不增加宿主写入能力,手动接入方式继续可用。

Expand Down
13 changes: 13 additions & 0 deletions docs/adr/0066-unified-wait-window-and-call-sequence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# ADR-0066:统一十分钟等待窗口,用序号区分连续等待

- 状态:已接受
- 日期:2026-11-08
- 修订 ADR-0007 的固定 240 秒等待窗口、ADR-0015 的宿主配置刷新范围。

固定四分钟窗口让长任务的 boss 反复醒来,而直接延长服务等待会超过某些宿主的工具超时。所有宿主(包括 WorkBuddy 和未知宿主)统一每次等待最多十分钟,不保留按宿主回退的旧窗口。任务结束、阻塞、待答弹框和审核仍提前唤醒,任务自身没有总时限。

App 在接入与刷新已有用户级 AgentNave 条目时,为支持条目级工具超时的宿主补足十二分钟,已有更长值及已核实的无上限值保留。只处理 AgentNave 这一项,不修改宿主的全局超时或其他服务。CodeBuddy 的工具超时是宿主进程的环境设置,不能通过子服务环境代设;保留已核实的一小时原生默认值,Grok 保留其更长的原生默认值。手动接入须按安装文档设置超时,升级后重连旧连接。

`wait_seconds=0` 继续即时收取;`600` 与省略参数均选择十分钟。移除 `240`,不保留旧窗口兼容;尚未核实工具超时配置的宿主也使用十分钟,发生问题再按该宿主处理。

连续调用 `wait` 是正常操作,但部分宿主会把相同工具入参视为无效重复。增加可选 `seq`,由 boss 逐次递增,回执原样带回;它只区分调用,不保存计数、不校验递增、不去重或重放结果,不改变收件箱的一次性交付语义,也不承诺绕过所有宿主的循环检测。
5 changes: 3 additions & 2 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@
| 0004 | [Windows 使用 Job Object 监督 Provider 进程树](0004-windows-job-object-supervision.md) | 已接受 | — |
| 0005 | [由主控 Agent 导演的公共发言区与独立工作台](0005-director-controlled-discussion-rooms.md) | 已被 ADR-0006 取代 | [ADR-0006](0006-core-rounds-and-studio-split.md)、[ADR-0008](0008-tray-app-reads-studio-snapshots.md) |
| 0006 | [核心收敛为子代理与协作轮次,节目、历史与看板拆为工作室包](0006-core-rounds-and-studio-split.md) | 部分取代 | [ADR-0009](0009-single-package-two-entry-points.md)、[ADR-0013](0013-members-defaults-and-host-identity.md)、[ADR-0016](0016-single-core-and-mcp-entrypoint.md)、[ADR-0024](0024-consolidated-tools-and-private-review.md)、[ADR-0025](0025-session-desks-and-running-limit.md)、[ADR-0045](0045-remove-browser-dashboard.md);2026-10-20 代码修订(席位上限 7) |
| 0007 | [`wait_any` 返回就绪子集并只消费回复中的阻塞](0007-wait-any-ready-subset.md) | 部分取代 | [ADR-0024](0024-consolidated-tools-and-private-review.md)、[ADR-0039](0039-retry-lost-seat-within-round.md)、[ADR-0049](0049-service-initiated-termination.md)、[ADR-0057](0057-boss-answers-native-prompts.md) |
| 0007 | [`wait_any` 返回就绪子集并只消费回复中的阻塞](0007-wait-any-ready-subset.md) | 部分取代 | [ADR-0024](0024-consolidated-tools-and-private-review.md)、[ADR-0039](0039-retry-lost-seat-within-round.md)、[ADR-0049](0049-service-initiated-termination.md)、[ADR-0057](0057-boss-answers-native-prompts.md)、[ADR-0066](0066-unified-wait-window-and-call-sequence.md) |
| 0008 | [托盘应用只读展示工作室数据,工作室写出视图快照与运行文件](0008-tray-app-reads-studio-snapshots.md) | 部分取代 | [ADR-0010](0010-launcher-relay-swaps-upgraded-server.md)、[ADR-0012](0012-tray-conversation-cleanup.md)、[ADR-0018](0018-office-workspace-desks-and-meeting-rooms.md)、[ADR-0026](0026-sqlite-conversation-store.md)、[ADR-0044](0044-meeting-voice-cloud-and-key-input.md)、[ADR-0045](0045-remove-browser-dashboard.md)、[ADR-0050](0050-v1-compatibility-baseline.md)、[ADR-0060](0060-tray-detects-orphaned-running-work.md) |
| 0009 | [核心与工作室合为一个发行包,保留两个入口](0009-single-package-two-entry-points.md) | 已被 ADR-0016 取代 | [ADR-0016](0016-single-core-and-mcp-entrypoint.md) |
| 0010 | [入口经中继启动,升级后在空闲时自动换用新服务](0010-launcher-relay-swaps-upgraded-server.md) | 部分取代 | [ADR-0014](0014-tray-members-and-one-click-update.md)、[ADR-0016](0016-single-core-and-mcp-entrypoint.md)、[ADR-0045](0045-remove-browser-dashboard.md)、[ADR-0047](0047-versioned-service-install.md) |
| 0011 | [被派出的 CLI 只能使用零工具 AgentNave 服务](0011-disable-nested-orchestration.md) | 已被 ADR-0031 取代 | [ADR-0031](0031-prompt-only-orchestration.md) |
| 0012 | [托盘在原生会话锁下清理记录](0012-tray-conversation-cleanup.md) | 部分取代 | [ADR-0013](0013-members-defaults-and-host-identity.md)、[ADR-0014](0014-tray-members-and-one-click-update.md)、[ADR-0024](0024-consolidated-tools-and-private-review.md)、[ADR-0026](0026-sqlite-conversation-store.md)、[ADR-0045](0045-remove-browser-dashboard.md)、[ADR-0046](0046-service-history-retention.md) |
| 0013 | [App 组员默认值、模式强制与宿主归属](0013-members-defaults-and-host-identity.md) | 部分取代 | [ADR-0016](0016-single-core-and-mcp-entrypoint.md)、[ADR-0017](0017-manager-chat-session-identity.md)、[ADR-0018](0018-office-workspace-desks-and-meeting-rooms.md)、[ADR-0020](0020-prefill-main-model-defaults.md)、[ADR-0021](0021-keep-default-models-when-unrestricted.md)、[ADR-0024](0024-consolidated-tools-and-private-review.md)、[ADR-0028](0028-follow-cli-default-model.md)、[ADR-0032](0032-cli-permission-tiers.md)、[ADR-0038](0038-role-applies-app-seat.md) |
| 0014 | [托盘维护组员配置、探测 CLI 模型,并一键全部更新](0014-tray-members-and-one-click-update.md) | 部分取代 | [ADR-0029](0029-settings-without-free-text.md)、[ADR-0034](0034-models-and-efforts-from-each-cli.md)、[ADR-0047](0047-versioned-service-install.md)、[ADR-0050](0050-v1-compatibility-baseline.md) |
| 0015 | [App 定点编辑宿主配置,保留外部 Skill 所有权](0015-app-host-integration.md) | 部分取代 | [ADR-0029](0029-settings-without-free-text.md)、[ADR-0036](0036-app-owns-service-skills.md)、[ADR-0040](0040-boss-app-from-session-records.md) |
| 0015 | [App 定点编辑宿主配置,保留外部 Skill 所有权](0015-app-host-integration.md) | 部分取代 | [ADR-0029](0029-settings-without-free-text.md)、[ADR-0036](0036-app-owns-service-skills.md)、[ADR-0040](0040-boss-app-from-session-records.md)、[ADR-0066](0066-unified-wait-window-and-call-sequence.md) |
| 0016 | [统一核心与 MCP 入口](0016-single-core-and-mcp-entrypoint.md) | 部分取代 | [ADR-0024](0024-consolidated-tools-and-private-review.md)、[ADR-0031](0031-prompt-only-orchestration.md)、[ADR-0033](0033-boss-workspace-handover.md)、[ADR-0045](0045-remove-browser-dashboard.md) |
| 0017 | [按主控聊天归集 App 群聊](0017-manager-chat-session-identity.md) | 部分取代 | [ADR-0018](0018-office-workspace-desks-and-meeting-rooms.md)、[ADR-0033](0033-boss-workspace-handover.md)、[ADR-0050](0050-v1-compatibility-baseline.md) |
| 0018 | [主控工作区、工位与会议室](0018-office-workspace-desks-and-meeting-rooms.md) | 部分取代 | [ADR-0023](0023-explicit-desk-continuation.md)、[ADR-0024](0024-consolidated-tools-and-private-review.md)、[ADR-0025](0025-session-desks-and-running-limit.md)、[ADR-0038](0038-role-applies-app-seat.md)、[ADR-0050](0050-v1-compatibility-baseline.md) |
Expand Down Expand Up @@ -68,3 +68,4 @@
| 0062 | [服务统一生成成员称呼](0062-service-owned-member-names.md) | 已接受 | — |
| 0063 | [CLI 共用模型池,编排替代普通任务目录](0063-cli-model-pool-and-orchestration-precedence.md) | 已接受 | 修订 ADR-0038、ADR-0053 |
| 0064 | [派发回执引导收取,可选派发后先等](0064-dispatch-receipts-steer-to-wait.md) | 已接受 | 修订 ADR-0007 |
| 0066 | [统一十分钟等待窗口,用序号区分连续等待](0066-unified-wait-window-and-call-sequence.md) | 已接受 | 修订 ADR-0007、ADR-0015 |
Loading
Loading