feat: mirror boss messages via app server
This commit is contained in:
@@ -171,6 +171,7 @@
|
||||
- 当前已补 Codex App Server 受控线程改名:`POST /api/v1/projects/[projectId]/rename` 在 `mode=thread` 且绑定真实 `codexThreadRef` 时,会在本地 Boss 改名后创建 `intentCategory=thread_rename` 任务,`local-agent` 直接调用 `thread/name/set`;该链路不启动普通 turn,不把 thread 原始字段写回 APP,只提示“已同步 Codex 线程名称”。设备离线、并发冲突或 App Server 不可用不会回滚 Boss 本地改名。
|
||||
- 当前已补 Codex App Server 受控线程 Git 元数据同步:`POST /api/v1/projects/[projectId]/thread-metadata` 会创建 `intentCategory=thread_metadata_sync` 任务,`local-agent` 直接调用 `thread/metadata/update`;当前只允许同步 `gitInfo.sha / branch / originUrl`,不会启动普通 turn,也不允许写入任意 metadata。
|
||||
- 当前已补 Codex App Server 受控线程分叉:`POST /api/v1/projects/[projectId]/thread-fork` 会创建 `intentCategory=thread_fork` 任务,`local-agent` 直接调用 `thread/fork`;当前不允许远程覆盖 model、sandbox、instructions 或 config,也不会把 path、cwd、turns、instructionSources 写回 APP。新线程进入 Boss 会话列表仍依赖 thread discovery / 导入链路。
|
||||
- 当前已补 Codex App Server 版 Boss 用户消息镜像:普通单线程 `conversation_reply` 任务携带 `mirrorBossUserMessageToCodexDesktop=true` 时,`local-agent/codex-app-server-runner.mjs` 会在 `thread/resume` 后、`turn/start` 前调用 `thread/inject_items`,把 Boss APP 用户原文作为 `role=user` 的 Responses item 写入目标 Codex 线程模型可见历史;任务结果只回传 `threadHistorySync.threadId / injectedItemCount / source`,不回传消息 ID、内部 prompt 或用户原文。CLI rollout 镜像仍保留为 App Server 不可用前的 fallback 链路。
|
||||
- 当前 boss-agent 已支持 Mac OTA:`local-agent/boss-agent-ota-runner.mjs` 默认开启,每 5 分钟检查服务端最新包;状态页可手动检查或下载并安装,安装时保留原绑定配置,只更新版本号和本机 runtime 路径。最新验证版本为 `20260516221619`,已在 MacBook Air `macbook-air` 上确认 OTA 下载校验、暂存、覆盖安装后不会误切到默认 `config.cloud.json`。正式分发脚本已预留 Developer ID 公证路径:`BOSS_AGENT_NOTARIZE=1` 配合 notary profile 或 Apple ID 凭据。
|
||||
- 当前量产治理已补设备撤权和任务可靠性底座:`revoke_device` 会清空设备 token、标记离线并阻断 heartbeat / 任务认领 / Skill 同步 / 日志上报 / boss-agent OTA;`MasterAgentTask` claim 会记录 attempt 和 lease,运行中任务可按租约重试,超过上限转 `timed_out`,用户或管理员可通过 cancel 接口转 `canceled` 且迟到 complete 不覆盖终态。
|
||||
- 当前群聊 `dispatch_execution` 完成回写已补幂等,重复完成不会再向群聊重复追加结果
|
||||
|
||||
@@ -134,6 +134,7 @@
|
||||
- App Server heartbeat discovery 现在支持 `skills/extraRoots/set`:配置 `codexAppServerSkillExtraRoots` 或环境变量 `BOSS_CODEX_APP_SERVER_SKILL_EXTRA_ROOTS` 后,runner 会先把共享 Skill 根下发给 App Server,再刷新 `skills/list`,并写入 `capabilities.codexAppServer.metadata.skillExtraRootsSummary`。该字段用于 APP/后台展示企业共享 Skill 根是否已下发;只保留数量、basename 和状态,不保存根目录绝对路径、Skill 文件路径或配置原文。
|
||||
- App Server heartbeat discovery 现在支持 `hooks/list`,写入 `capabilities.codexAppServer.metadata.hookSummary`。该字段用于 APP/后台展示本机 Codex hook 治理状态;只保留 workspace 数、hook 数、启用数、受管 / 可信 / 修改 / 未信任计数、warning / error 计数和事件 / handler 类型,不保存 hook key、command、sourcePath、statusMessage、hash、error message 或本地路径。
|
||||
- 当前 Codex App Server runner 已新增第一版 Boss Inter-Thread Broker:任务携带 `intentCategory=thread_collaboration`、`sourceCodexThreadRef` 和 `targetCodexThreadRef` 时,会先 `thread/read` 源线程,再通过 `thread/inject_items` 向目标线程注入受控摘要,最后 `turn/start` 目标线程;服务端入口是 `POST /api/v1/projects/[projectId]/thread-collaboration`,负责权限、源/目标线程校验和任务排队。这不是假设官方线程 P2P,而是 Boss 自己做线程协作编排。
|
||||
- 当前 Codex App Server runner 已新增 Boss 用户消息镜像:普通 `conversation_reply` 任务携带 `mirrorBossUserMessageToCodexDesktop=true`、`sourceMessageBody` 和目标 `codexThreadRef` 时,会先 `thread/resume`,再 `thread/inject_items` 写入 `role=user` 的 Boss APP 用户原文,最后 `turn/start`;该链路用于让 APP 发起的对话进入 Codex Desktop 同一线程历史。执行结果只保存 `threadHistorySync` 安全摘要,不保存 App Server 原始 item、消息 ID、用户原文、系统提示词或内部调度字段。
|
||||
- 当前 Codex App Server runner 已新增受控线程回滚:任务携带 `intentCategory=thread_rollback`、目标 `codexThreadRef` 和 `rollbackNumTurns` 时,会调用 `thread/rollback` 回滚目标线程最近 N 轮,不会启动新 turn,也不会把 App Server 返回的 thread/turn/items 写回 APP。服务端入口是 `POST /api/v1/projects/[projectId]/thread-rollback`,只保存回滚轮数、原因和执行摘要;边界是只回滚 Codex 线程历史,不自动还原本地文件变更。
|
||||
- 当前 Codex App Server runner 已新增受控线程压缩:任务携带 `intentCategory=thread_compact` 和目标 `codexThreadRef` 时,会调用 `thread/compact/start` 发起上下文压缩,不会启动普通 turn,也不会把 contextCompaction item 的原始字段写回 APP。服务端入口是 `POST /api/v1/projects/[projectId]/thread-compact`,只保存压缩原因和执行摘要;边界是只压缩 Codex 线程上下文,不代表代码修改、文件恢复或版本发布完成。
|
||||
- 当前 Codex App Server runner 已新增受控线程归档 / 恢复:任务携带 `intentCategory=thread_archive|thread_unarchive`、目标 `codexThreadRef` 和 `threadLifecycleAction` 时,会直接调用 `thread/archive` 或 `thread/unarchive`,不会先 resume 已归档线程,也不会启动普通 turn。服务端入口是 `POST /api/v1/projects/[projectId]/thread-archive`,只保存生命周期动作、原因和执行摘要;边界是只改变 Codex 线程生命周期状态,不代表代码修改、文件恢复或版本发布完成。
|
||||
|
||||
@@ -146,6 +146,7 @@ UI 参考:
|
||||
- `local-agent/codex-app-server-runner.mjs` 已把 App Server `windowsSandbox/setupCompleted` 归一成 `executionProgress.windowsSandbox`;服务端进度路由和 Android 原生进度卡已支持展示,测试覆盖 setup error 中的 token、本地 Windows 路径和 sourcePath 不外泄
|
||||
- 新增实时进度入口 `POST /api/v1/master-agent/tasks/[taskId]/progress`,设备端可在任务执行中持续刷新同一张 `execution_progress` 卡;`local-agent` 的 App Server runner 已在收到协议进度事件时调用该接口,complete 仍携带最终进度作为兜底
|
||||
- 新增服务端线程协作入口 `POST /api/v1/projects/[projectId]/thread-collaboration`,由 Boss 校验源/目标项目权限并创建 `intentCategory=thread_collaboration` 的 `conversation_reply` 任务;设备端继续通过 App Server runner 执行 `thread/read -> thread/inject_items -> turn/start`,避免把“线程互通”误做成无监管 P2P
|
||||
- 新增 Boss APP 用户消息镜像到 Codex App Server 线程历史:普通 `conversation_reply` 任务已复用 `mirrorBossUserMessageToCodexDesktop` 标记;设备端通过 App Server runner 执行 `thread/resume -> thread/inject_items(user message) -> turn/start`,让 APP 发起的用户输入进入 Codex Desktop 同一线程的模型可见历史。执行结果只返回 `threadHistorySync` 安全摘要,不回写消息 ID、用户原文、内部 prompt 或 App Server 原始 item。
|
||||
- 新增活跃 turn 干预:任务携带 `targetCodexTurnId` / `targetTurnId` 时,App Server runner 会调用 `turn/steer`,并把 `turnControl=steer`、`turnId` 写回执行结果;没有活跃 turn id 时仍使用 `turn/start`
|
||||
- 新增活跃 turn 中断:用户或管理员通过任务取消接口把任务转为 `canceled` 后,设备端会轮询 `GET /api/v1/master-agent/tasks/[taskId]/control-state`;如果当前任务已经启动 App Server turn,runner 会在同一个 JSON-RPC 连接上调用 `turn/interrupt`,并把返回的 `interrupted` 处理成干净取消,不再把用户主动取消误判成 runtime failure
|
||||
- 新增受控线程回滚:服务端入口 `POST /api/v1/projects/[projectId]/thread-rollback` 会创建 `intentCategory=thread_rollback` 任务;设备端通过 App Server 调用 `thread/rollback`,只返回“已回滚最近 N 轮”的用户可见摘要,不启动新 turn,不保存 App Server 返回的 thread/turn/items。该能力只回滚 Codex 线程历史,不自动还原本地文件变更,后续如需文件级撤回必须另走 Git / 文件快照恢复链路。
|
||||
|
||||
@@ -263,6 +263,7 @@ cd /Users/kris/code/boss
|
||||
- 当前已绑定真实 `codexThreadRef` 的普通单线程聊天,会在 `local-agent` 执行 `codex exec resume` 前,先把 Boss 用户消息镜像写入对应 Codex Desktop rollout;这样 APP 发起的消息也能进入桌面版同一线程历史,并按 `sourceMessageId` 去重。rollout 定位优先使用 `state_5.sqlite`,状态库不可用或索引缺失时回退扫描 `~/.codex/sessions`;写入后会尽量刷新 `threads.updated_at / updated_at_ms / has_user_event`,再通过 `codex://threads/{threadId}` 深链提示桌面版打开目标线程
|
||||
- 当前 `local-agent` 已新增 `Codex App Server` provider:boss-agent 默认配置 `codexAppServerEnabled=true`,`conversation_reply / dispatch_execution` 会先通过 `codex app-server` 的 stdio JSON-RPC 恢复或创建线程,也可配置 `codexAppServerTransport=ws + codexAppServerUrl=ws://127.0.0.1:<port>` 或 `codexAppServerTransport=unix + codexAppServerUrl=unix:///absolute/path.sock` 连接同机长驻 App Server;长驻连接可通过 `codexAppServerAuthTokenFile` 或 `BOSS_CODEX_APP_SERVER_AUTH_TOKEN_FILE` 提供 bearer token。随后 runner 下发 `turn/start` 并收集流式 agent 回复;如果单个 JSON-RPC 请求返回 `-32001 / retry later`,runner 会先做指数退避重试;如果任务携带 `targetCodexTurnId`,会改用 `turn/steer` 干预活跃 turn;如果 App Server 在 turn 启动前失败,默认允许回退到 `codex exec resume`,如果 turn 已经启动则不再回退,避免同一轮用户消息被重复执行。桌面控制另有 `codexComputerUseEnabled=true`,默认先走 Codex Computer Use,再回退 CUA Driver。
|
||||
- 当前已新增 Boss 自有 Inter-Thread Broker 第一版:服务端入口 `POST /api/v1/projects/[projectId]/thread-collaboration` 会创建带源/目标 Codex 线程引用的协作任务;App Server runner 执行 `thread/read(source) -> thread/inject_items(target) -> turn/start(target)`,用于让一个线程的结论受控进入另一个线程,不依赖官方任意线程 P2P 互聊能力
|
||||
- 当前已新增 App Server 版 Boss 用户消息镜像:普通 `conversation_reply` 任务携带 `mirrorBossUserMessageToCodexDesktop=true` 时,App Server runner 会在 `thread/resume` 后、`turn/start` 前调用 `thread/inject_items`,把 Boss APP 用户原文写成目标 Codex 线程的 `role=user` Responses item;任务结果只回写 `threadHistorySync.threadId / injectedItemCount / source`,不回写消息 ID、用户原文、内部调度 prompt 或系统约束。CLI rollout 写入仍作为 App Server 不可用前的兼容兜底。
|
||||
- 当前已新增 Codex App Server 受控线程回滚:服务端入口 `POST /api/v1/projects/[projectId]/thread-rollback` 会创建 `intentCategory=thread_rollback` 任务;App Server runner 执行 `thread/rollback(target, numTurns)`,只回写“已回滚最近 N 轮”的用户可见摘要,不启动新 turn,不保存 App Server 返回的 thread/turn/items。该能力只回滚 Codex 线程历史,不自动还原本地文件变更。
|
||||
- 当前已新增 Codex App Server 受控线程压缩:服务端入口 `POST /api/v1/projects/[projectId]/thread-compact` 会创建 `intentCategory=thread_compact` 任务;App Server runner 执行 `thread/compact/start(target)`,只回写“已发起上下文压缩”的用户可见摘要,不启动普通 turn,不保存 contextCompaction item 原始字段。该能力只压缩 Codex 线程上下文,不代表代码修改、文件恢复或版本发布完成。
|
||||
- 当前已新增 Codex App Server 受控线程归档 / 恢复:服务端入口 `POST /api/v1/projects/[projectId]/thread-archive` 会创建 `intentCategory=thread_archive|thread_unarchive` 任务;App Server runner 直接执行 `thread/archive(target)` 或 `thread/unarchive(target)`,不先 resume 已归档线程,不启动普通 turn,不保存 App Server 返回的 thread 原始字段。该能力只改变 Codex 线程生命周期状态,不代表代码修改、文件恢复或版本发布完成。
|
||||
|
||||
@@ -86,6 +86,17 @@ function isThreadForkTask(task) {
|
||||
return task?.intentCategory === "thread_fork" || task?.taskType === "thread_fork";
|
||||
}
|
||||
|
||||
function shouldMirrorBossUserMessageToCodexThread(task) {
|
||||
return (
|
||||
task?.mirrorBossUserMessageToCodexDesktop === true ||
|
||||
task?.syncUserMessageToCodexThread === true
|
||||
);
|
||||
}
|
||||
|
||||
function resolveBossUserMessageText(task) {
|
||||
return trimToDefined(task?.sourceMessageBody) || trimToDefined(task?.requestText);
|
||||
}
|
||||
|
||||
function resolveThreadRenameName(task) {
|
||||
return trimToDefined(task?.threadRenameName || task?.threadName || task?.name);
|
||||
}
|
||||
@@ -2896,6 +2907,50 @@ async function maybeInjectInterThreadContext({ request, task, targetThreadId })
|
||||
};
|
||||
}
|
||||
|
||||
function buildBossUserMessageInjectionItem({ task, text }) {
|
||||
return {
|
||||
type: "message",
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "input_text",
|
||||
text,
|
||||
},
|
||||
],
|
||||
metadata: {
|
||||
boss_source: "boss_app",
|
||||
boss_message_kind: "user_message",
|
||||
...(trimToDefined(task?.sourceMessageSentAt)
|
||||
? { boss_source_sent_at: trimToDefined(task.sourceMessageSentAt) }
|
||||
: {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function maybeInjectBossUserMessage({ request, task, targetThreadId, targetTurnId, hasExistingThreadRef }) {
|
||||
if (
|
||||
!targetThreadId ||
|
||||
targetTurnId ||
|
||||
!hasExistingThreadRef ||
|
||||
!shouldMirrorBossUserMessageToCodexThread(task)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
const text = resolveBossUserMessageText(task);
|
||||
if (!text) {
|
||||
return undefined;
|
||||
}
|
||||
await request("thread/inject_items", {
|
||||
threadId: targetThreadId,
|
||||
items: [buildBossUserMessageInjectionItem({ task, text })],
|
||||
});
|
||||
return {
|
||||
threadId: targetThreadId,
|
||||
injectedItemCount: 1,
|
||||
source: "boss_user_message",
|
||||
};
|
||||
}
|
||||
|
||||
export async function executeCodexAppServerTask(runnerConfig, task) {
|
||||
if (!shouldUseCodexAppServerTaskRunner(runnerConfig, task)) {
|
||||
return createFailure("CODEX_APP_SERVER_DISABLED", { canFallbackToCli: true });
|
||||
@@ -3385,6 +3440,13 @@ export async function executeCodexAppServerTask(runnerConfig, task) {
|
||||
task,
|
||||
targetThreadId: threadId,
|
||||
});
|
||||
const threadHistorySync = await maybeInjectBossUserMessage({
|
||||
request,
|
||||
task,
|
||||
targetThreadId: threadId,
|
||||
targetTurnId: targetTurnRef,
|
||||
hasExistingThreadRef: Boolean(targetThreadRef),
|
||||
});
|
||||
|
||||
const turnControl = targetTurnRef ? "steer" : "start";
|
||||
const turnResult = targetTurnRef
|
||||
@@ -3420,6 +3482,7 @@ export async function executeCodexAppServerTask(runnerConfig, task) {
|
||||
transport: runnerConfig.transport,
|
||||
executionProgress: progressCollector.snapshot(),
|
||||
interThreadBroker,
|
||||
threadHistorySync,
|
||||
canFallbackToCli: false,
|
||||
};
|
||||
}
|
||||
@@ -3433,6 +3496,7 @@ export async function executeCodexAppServerTask(runnerConfig, task) {
|
||||
transport: runnerConfig.transport,
|
||||
executionProgress: progressCollector.snapshot(),
|
||||
interThreadBroker,
|
||||
threadHistorySync,
|
||||
canFallbackToCli: false,
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
10
tests/fixtures/codex-app-server-runtime.mjs
vendored
10
tests/fixtures/codex-app-server-runtime.mjs
vendored
@@ -796,6 +796,12 @@ rl.on("line", (line) => {
|
||||
return;
|
||||
}
|
||||
const text = message.params?.input?.find?.((item) => item?.type === "text")?.text ?? "";
|
||||
const injectedUserMessageText =
|
||||
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_ECHO_INJECTED_USER_MESSAGE === "1"
|
||||
? injectedItems
|
||||
.flatMap((item) => item?.content ?? [])
|
||||
.find((content) => content?.type === "input_text")?.text
|
||||
: "";
|
||||
send({
|
||||
id: message.id,
|
||||
result: {
|
||||
@@ -1654,7 +1660,9 @@ rl.on("line", (line) => {
|
||||
threadId: message.params?.threadId,
|
||||
turnId: "turn-fixture",
|
||||
delta:
|
||||
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_INTER_THREAD === "1"
|
||||
injectedUserMessageText
|
||||
? `INJECTED_USER_MESSAGE:${injectedUserMessageText}`
|
||||
: process.env.BOSS_CODEX_APP_SERVER_FIXTURE_INTER_THREAD === "1"
|
||||
? `INTER_THREAD_INJECTED:${JSON.stringify(injectedItems)}`
|
||||
: `APP_SERVER_REPLY:${text}`,
|
||||
},
|
||||
|
||||
@@ -508,6 +508,50 @@ test("codex app-server runner resumes a thread and collects streamed agent text"
|
||||
assert.equal(result.transport, "stdio");
|
||||
});
|
||||
|
||||
test("codex app-server runner injects Boss user message into the target Codex thread before starting a turn", async () => {
|
||||
const previous = process.env.BOSS_CODEX_APP_SERVER_FIXTURE_ECHO_INJECTED_USER_MESSAGE;
|
||||
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_ECHO_INJECTED_USER_MESSAGE = "1";
|
||||
try {
|
||||
const runnerConfig = getCodexAppServerRunnerConfig(process.env, {
|
||||
codexAppServerEnabled: true,
|
||||
codexAppServerCommand: process.execPath,
|
||||
codexAppServerArgs: ["tests/fixtures/codex-app-server-runtime.mjs"],
|
||||
codexAppServerWorkdir: repoRoot,
|
||||
codexAppServerTimeoutMs: 5000,
|
||||
masterAgentModel: "gpt-5.4",
|
||||
});
|
||||
const task = {
|
||||
taskId: "task-app-server-sync-user-message",
|
||||
taskType: "conversation_reply",
|
||||
targetCodexThreadRef: "019d-app-server-thread",
|
||||
targetCodexFolderRef: repoRoot,
|
||||
executionPrompt: "请修复登录页跳转问题",
|
||||
mirrorBossUserMessageToCodexDesktop: true,
|
||||
userMessageId: "boss-msg-1",
|
||||
sourceMessageId: "boss-msg-1",
|
||||
sourceMessageBody: "请修复登录页跳转问题",
|
||||
userDisplayName: "你",
|
||||
};
|
||||
|
||||
const result = await executeCodexAppServerTask(runnerConfig, task);
|
||||
|
||||
assert.equal(result.status, "completed");
|
||||
assert.equal(result.threadId, "019d-app-server-thread");
|
||||
assert.equal(result.turnControl, "start");
|
||||
assert.equal(result.threadHistorySync?.threadId, "019d-app-server-thread");
|
||||
assert.equal(result.threadHistorySync?.injectedItemCount, 1);
|
||||
assert.equal(result.threadHistorySync?.source, "boss_user_message");
|
||||
assert.match(result.replyBody, /INJECTED_USER_MESSAGE:请修复登录页跳转问题/);
|
||||
assert.doesNotMatch(JSON.stringify(result), /boss-msg-1/);
|
||||
} finally {
|
||||
if (previous === undefined) {
|
||||
delete process.env.BOSS_CODEX_APP_SERVER_FIXTURE_ECHO_INJECTED_USER_MESSAGE;
|
||||
} else {
|
||||
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_ECHO_INJECTED_USER_MESSAGE = previous;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("codex app-server runner converts protocol progress events into Boss execution progress", async () => {
|
||||
const previous = process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_PROGRESS;
|
||||
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_PROGRESS = "1";
|
||||
|
||||
Reference in New Issue
Block a user