fix: harden production chat runtime
This commit is contained in:
@@ -54,7 +54,7 @@
|
||||
- `POST http://127.0.0.1:3000/api/v1/projects/[projectId]/messages` 正常,普通单线程会话当前会返回 `conversation_reply` 任务,并等待绑定设备上的真实 Codex 线程回写
|
||||
- `POST http://127.0.0.1:3000/api/auth/logout` 正常,退出后访问受保护 `/api/v1/*` 会返回 `401`
|
||||
- `GET http://127.0.0.1:3000/api/v1/user/ota/package` 正常,当前会返回最新 APK 包
|
||||
- 当前这台开发机在本轮 `launchctl` 重载后,`GET http://127.0.0.1:4317/health` 仍未恢复;代码已改成先起本地 health 监听、再异步执行首次 heartbeat / task poll,剩余问题已收敛到 launchd 环境差异排查
|
||||
- 当前这台开发机的 `launchd` 常驻 `local-agent` 已恢复:`GET http://127.0.0.1:4317/health` 现在可在数十毫秒内返回,且在手动 heartbeat 执行期间仍能正常回包
|
||||
- `GET http://127.0.0.1:4317/api/v1/skills` 正常,已返回本机扫描到的 Codex Skill
|
||||
- `POST http://127.0.0.1:4317/api/v1/heartbeat` 正常,且会顺带触发 `thread-context` 上报
|
||||
- `launchd` 已加载:`~/Library/LaunchAgents/com.hyzq.boss.local-agent.plist`
|
||||
@@ -94,7 +94,7 @@ Android APK:
|
||||
- 已生成 Android debug APK:`android/app/build/outputs/apk/debug/app-debug.apk`
|
||||
- 已生成 Android signed release APK:`android/app/build/outputs/apk/release/app-release.apk`
|
||||
- `npm run apk:release` 还会额外产出带版本号的文件:`android/app/build/outputs/apk/release/boss-android-v{versionName}-release.apk`
|
||||
- 当前最新 release 构建版本:`2.5.6`(`versionCode=19`)
|
||||
- 当前最新 release 构建版本:`2.5.7`(`versionCode=20`)
|
||||
- 当前 APK 已切到原生 Android 客户端:`MainActivity + BossApiClient + 原生 XML 布局`
|
||||
- 当前原生活动页已经覆盖:会话首页、项目详情、项目目标、版本记录、会话信息、群资料、发起群聊、消息转发、线程详情、设备详情、添加设备、账号与安全、设置、AI 账号、技能、运维中心、关于
|
||||
- 当前原生一级体验已回退到微信式交互:`会话 / 设备 / 我的` 固定底部 tab,会话首页是简单聊天列表,`主 Agent / 审计对话` 以普通置顶会话样式排在最前;项目详情页是聊天优先,只保留 `项目目标 / 版本记录` 两个轻入口
|
||||
@@ -203,6 +203,7 @@ device-agent 当前职责:
|
||||
- 对群聊线程分发任务,认领到的 `dispatch_execution` 任务会把原始线程结果和主 Agent 汇总一起回写到群聊消息账本
|
||||
- `local-agent` 对 `conversation_reply / dispatch_execution` 当前会优先使用 `codex exec resume <targetCodexThreadRef>`,只有缺失真实线程引用时才退回 `--ephemeral`
|
||||
- `local-agent` 当前会先启动本地 `4317` 健康监听,再异步执行首次 heartbeat 和 task poll,避免控制面短暂阻塞时本地健康检查一起挂死
|
||||
- Codex 项目/线程扫描当前已搬到 worker 线程执行,避免 `.codex/logs_1.sqlite` 和 `state_5.sqlite` 的同步扫描阻塞主线程 HTTP 响应
|
||||
- 如果某个历史群聊里已经没有真实线程成员,当前不会再表现成“发了没反应”,而是会在群里追加一条 `system_notice`,提示用户先重新整理群成员
|
||||
- 设备导入审核当前也会落 `device_import_resolution` 任务轨迹,但决议内容仍是服务端 heuristic 版;下一阶段可再升级成真正通过 `local-agent -> codex exec` 参与理解
|
||||
- 提供本地 `/health`、`/api/v1/device`、`/api/v1/skills`、`/api/v1/heartbeat`
|
||||
|
||||
@@ -36,8 +36,8 @@ android {
|
||||
applicationId "com.hyzq.boss"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 19
|
||||
versionName "2.5.6"
|
||||
versionCode 20
|
||||
versionName "2.5.7"
|
||||
buildConfigField "String", "BOSS_API_BASE_URL", "\"https://boss.hyzq.net\""
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
@@ -852,7 +852,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
ProjectChatUiState.ReplyWaitSpec waitSpec =
|
||||
ProjectChatUiState.resolveReplyWaitAfterDispatchConfirm(response.json);
|
||||
runOnUiThread(() -> {
|
||||
projectApprovalState = "approval_required".equals(projectCollaborationMode) ? "approved" : "not_required";
|
||||
applyDispatchPlanActionResponse(response.json);
|
||||
if (waitSpec.shouldWait) {
|
||||
startReplyWait(
|
||||
waitSpec,
|
||||
@@ -887,9 +887,8 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
throw new IllegalStateException(response.message());
|
||||
}
|
||||
runOnUiThread(() -> {
|
||||
currentPendingDispatchPlan = null;
|
||||
composerSending = false;
|
||||
projectApprovalState = "rejected";
|
||||
applyDispatchPlanActionResponse(response.json);
|
||||
updateComposerSendButtonState();
|
||||
showMessage("已拒绝主 Agent 推荐");
|
||||
reload(true);
|
||||
@@ -1935,6 +1934,24 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
}
|
||||
}
|
||||
|
||||
private void applyDispatchPlanActionResponse(@Nullable JSONObject response) {
|
||||
if (response == null) {
|
||||
return;
|
||||
}
|
||||
JSONObject collaborationGate = response.optJSONObject("collaborationGate");
|
||||
if (collaborationGate != null) {
|
||||
projectCollaborationMode = collaborationGate.optString("collaborationMode", projectCollaborationMode);
|
||||
projectApprovalState = collaborationGate.optString("approvalState", projectApprovalState);
|
||||
}
|
||||
JSONObject plan = response.optJSONObject("plan");
|
||||
if (plan != null) {
|
||||
String status = plan.optString("status", "");
|
||||
if (!"pending_user_confirmation".equals(status)) {
|
||||
currentPendingDispatchPlan = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> collectMessageIds(@Nullable JSONArray messages) {
|
||||
ArrayList<String> ids = new ArrayList<>();
|
||||
if (messages == null) {
|
||||
|
||||
@@ -353,6 +353,45 @@ public class ProjectDetailActivityUiTest {
|
||||
assertEquals("group-1", nextIntent.getStringExtra(GroupInfoActivity.EXTRA_PROJECT_ID));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void applyDispatchPlanActionResponseClearsPendingPlanAndTracksApprovalState() throws Exception {
|
||||
Intent intent = new Intent()
|
||||
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_ID, "group-1")
|
||||
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME, "巡检协作群");
|
||||
TestProjectDetailActivity activity = Robolectric
|
||||
.buildActivity(TestProjectDetailActivity.class, intent)
|
||||
.setup()
|
||||
.get();
|
||||
|
||||
ReflectionHelpers.setField(activity, "projectCollaborationMode", "approval_required");
|
||||
ReflectionHelpers.setField(activity, "projectApprovalState", "pending_user");
|
||||
ReflectionHelpers.setField(
|
||||
activity,
|
||||
"currentPendingDispatchPlan",
|
||||
new JSONObject().put("planId", "dispatch-plan-1").put("status", "pending_user_confirmation")
|
||||
);
|
||||
|
||||
JSONObject response = new JSONObject()
|
||||
.put("plan", new JSONObject()
|
||||
.put("planId", "dispatch-plan-1")
|
||||
.put("status", "dispatched"))
|
||||
.put("collaborationGate", new JSONObject()
|
||||
.put("isGroup", true)
|
||||
.put("collaborationMode", "approval_required")
|
||||
.put("requiresMasterAgentApproval", true)
|
||||
.put("approvalState", "approved"));
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"applyDispatchPlanActionResponse",
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, response)
|
||||
);
|
||||
|
||||
assertEquals("approval_required", ReflectionHelpers.getField(activity, "projectCollaborationMode"));
|
||||
assertEquals("approved", ReflectionHelpers.getField(activity, "projectApprovalState"));
|
||||
assertEquals(null, ReflectionHelpers.getField(activity, "currentPendingDispatchPlan"));
|
||||
}
|
||||
|
||||
private static JSONObject buildGroupProjectPayload() throws Exception {
|
||||
JSONObject threadMeta = new JSONObject()
|
||||
.put("threadId", "group-thread-3")
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
- 登录恢复接口:`POST http://127.0.0.1:3000/api/auth/restore`
|
||||
- 登出接口:`POST http://127.0.0.1:3000/api/auth/logout`
|
||||
- OTA 包下载接口:`GET http://127.0.0.1:3000/api/v1/user/ota/package`
|
||||
- 本地 agent 健康检查:`http://127.0.0.1:4317/health`。当前这台开发机在本轮 `launchctl` 重载后仍未恢复,但代码已改成先启动本地 health 监听、再异步执行首次 heartbeat / task poll,剩余问题已收敛到 launchd 环境差异排查
|
||||
- 本地 agent 健康检查:`http://127.0.0.1:4317/health`。当前这台开发机的 `launchd` 常驻已经恢复,`/health` 可在数十毫秒内返回,并且在手动 heartbeat 执行期间也不会再被 Codex 线程扫描卡死
|
||||
- 本地 Skill 扫描接口:`http://127.0.0.1:4317/api/v1/skills`
|
||||
- 本地 agent 手动 heartbeat:`POST http://127.0.0.1:4317/api/v1/heartbeat`
|
||||
- `launchd` 已安装:`~/Library/LaunchAgents/com.hyzq.boss.local-agent.plist`
|
||||
@@ -149,7 +149,7 @@ cd /Users/kris/code/boss
|
||||
- 当前已生成 Android debug APK:`android/app/build/outputs/apk/debug/app-debug.apk`
|
||||
- 当前已生成 Android signed release APK:`android/app/build/outputs/apk/release/app-release.apk`
|
||||
- 当前 release 构建还会额外生成带版本号的 APK:`android/app/build/outputs/apk/release/boss-android-v{versionName}-release.apk`
|
||||
- 当前最新 release 构建版本:`2.5.5`(`versionCode=18`)
|
||||
- 当前最新 release 构建版本:`2.5.7`(`versionCode=20`)
|
||||
- 当前 release keystore 位于本机 `android/keystores/boss-release.keystore`,签名参数位于 `android/signing/release-signing.properties`
|
||||
- `2.0.1` 已在本机连接的华为真机上复核通过,修复了 `Theme.SplashScreen` 导致的 `AppCompatActivity` 启动闪退
|
||||
- `2.1.0` 已把 Web 一级页和主要二级页全部补成原生活动页:`MainActivity / ProjectDetailActivity / ProjectGoalsActivity / ProjectVersionsActivity / ProjectForwardActivity / ThreadDetailActivity / DeviceDetailActivity / DeviceEnrollmentActivity / SkillInventoryActivity / SecurityActivity / SettingsActivity / AiAccountsActivity / OpsCenterActivity / AboutActivity`
|
||||
@@ -176,6 +176,7 @@ cd /Users/kris/code/boss
|
||||
- 当前历史脏群如果不再包含真实线程成员,群聊消息不会再表现成“无响应”;服务端会在群内追加明确 `system_notice`,提示先重新添加线程成员
|
||||
- 当前设备导入决议已经会先落 `device_import_resolution` master task 再写回结果,但决议内容仍是服务端 heuristic 版;下一阶段可再升级成真正通过 `local-agent -> codex exec` 参与理解的主 Agent 决议
|
||||
- 当前 `local-agent` 已改成先启动本地 `4317` 健康监听,再异步跑首次 heartbeat 和 task poll,避免控制面短时阻塞时本地健康探针不可用
|
||||
- Codex 项目/线程扫描当前已搬到 worker 线程执行,避免 `.codex/logs_1.sqlite` 和 `state_5.sqlite` 的同步扫描阻塞主线程健康接口
|
||||
- 原生 Android 当前对 `master-agent` 聊天不再依赖长时间同步等待;发送后会先显示“主 Agent 思考中”,右上角改成微信式 `...` 菜单,菜单项包含 `模型 / 推理强度 / 会话信息 / 刷新`
|
||||
|
||||
## 2. 服务器状态
|
||||
|
||||
@@ -3,6 +3,7 @@ import { basename, resolve } from "node:path";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { readFile, readdir } from "node:fs/promises";
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
|
||||
|
||||
function toIsoFromUnixSeconds(value) {
|
||||
if (!Number.isFinite(value) || value <= 0) return null;
|
||||
@@ -259,3 +260,43 @@ export async function discoverCodexProjectCandidates(options = {}) {
|
||||
projectCandidates: candidates,
|
||||
};
|
||||
}
|
||||
|
||||
export async function discoverCodexProjectCandidatesInWorker(options = {}) {
|
||||
return await new Promise((resolvePromise, rejectPromise) => {
|
||||
const worker = new Worker(new URL(import.meta.url), {
|
||||
workerData: {
|
||||
kind: "boss_codex_discovery",
|
||||
options,
|
||||
},
|
||||
});
|
||||
|
||||
worker.once("message", (payload) => {
|
||||
if (payload?.ok) {
|
||||
resolvePromise(payload.result);
|
||||
return;
|
||||
}
|
||||
rejectPromise(new Error(payload?.error ?? "DISCOVERY_WORKER_FAILED"));
|
||||
});
|
||||
|
||||
worker.once("error", rejectPromise);
|
||||
|
||||
worker.once("exit", (code) => {
|
||||
if (code === 0) {
|
||||
return;
|
||||
}
|
||||
rejectPromise(new Error(`DISCOVERY_WORKER_EXIT_${code}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (!isMainThread && workerData?.kind === "boss_codex_discovery") {
|
||||
try {
|
||||
const result = await discoverCodexProjectCandidates(workerData.options ?? {});
|
||||
parentPort?.postMessage({ ok: true, result });
|
||||
} catch (error) {
|
||||
parentPort?.postMessage({
|
||||
ok: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { createServer } from "node:http";
|
||||
import { access, readFile, readdir, rm } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { discoverCodexProjectCandidates } from "./codex-session-discovery.mjs";
|
||||
import { discoverCodexProjectCandidatesInWorker } from "./codex-session-discovery.mjs";
|
||||
import { buildCodexTaskExecution } from "./codex-task-runner.mjs";
|
||||
|
||||
async function loadConfig(configPath) {
|
||||
@@ -24,7 +24,7 @@ async function resolveHeartbeatProjects(config, runtime) {
|
||||
}
|
||||
|
||||
try {
|
||||
const discovered = await discoverCodexProjectCandidates({
|
||||
const discovered = await discoverCodexProjectCandidatesInWorker({
|
||||
stateDbPath: config.codexStateDbPath,
|
||||
logsDbPath: config.codexLogsDbPath,
|
||||
sessionIndexPath: config.codexSessionIndexPath,
|
||||
|
||||
@@ -33,6 +33,7 @@ export async function POST(
|
||||
plan: result.plan,
|
||||
executions: result.executions,
|
||||
notice: result.notice,
|
||||
collaborationGate: result.collaborationGate,
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -23,6 +23,7 @@ export async function POST(
|
||||
ok: true,
|
||||
plan: result.plan,
|
||||
notice: result.notice,
|
||||
collaborationGate: result.collaborationGate,
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -316,6 +316,15 @@ export interface DispatchExecution {
|
||||
completedByDeviceId?: string;
|
||||
}
|
||||
|
||||
function buildCollaborationGate(project: Pick<Project, "isGroup" | "collaborationMode" | "approvalState">) {
|
||||
return {
|
||||
isGroup: project.isGroup,
|
||||
collaborationMode: project.collaborationMode,
|
||||
requiresMasterAgentApproval: project.isGroup && project.collaborationMode === "approval_required",
|
||||
approvalState: project.approvalState,
|
||||
};
|
||||
}
|
||||
|
||||
export interface ProjectAgentControls {
|
||||
modelOverride?: string;
|
||||
reasoningEffortOverride?: ReasoningEffort;
|
||||
@@ -4395,6 +4404,7 @@ export async function rejectDispatchPlan(input: {
|
||||
return {
|
||||
plan: { ...plan },
|
||||
notice: notice ? { ...notice } : null,
|
||||
collaborationGate: buildCollaborationGate(groupProject),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -4659,6 +4669,7 @@ export async function confirmDispatchPlanAndCreateExecutions(input: {
|
||||
plan: { ...plan },
|
||||
executions: executions.map((execution) => ({ ...execution })),
|
||||
notice: createdNotice ? { ...createdNotice } : null,
|
||||
collaborationGate: buildCollaborationGate(groupProject),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -188,6 +188,12 @@ test("POST /api/v1/projects/[projectId]/dispatch-plans/[planId]/confirm confirms
|
||||
plan: { planId: string; status: string; confirmedTargetProjectIds: string[] };
|
||||
executions: Array<{ planId: string; targetProjectId: string; status: string }>;
|
||||
notice: { kind: string; body: string } | null;
|
||||
collaborationGate: {
|
||||
isGroup: boolean;
|
||||
collaborationMode: string;
|
||||
requiresMasterAgentApproval: boolean;
|
||||
approvalState: string;
|
||||
};
|
||||
};
|
||||
assert.equal(payload.ok, true);
|
||||
assert.equal(payload.plan.planId, dispatchPlan.planId);
|
||||
@@ -200,6 +206,10 @@ test("POST /api/v1/projects/[projectId]/dispatch-plans/[planId]/confirm confirms
|
||||
assert.ok(payload.notice, "expected a confirmation notice in the response");
|
||||
assert.equal(payload.notice?.kind, "system_notice");
|
||||
assert.equal(payload.notice?.body, "已确认下发到 1 个线程:《北区试产线回归》。");
|
||||
assert.equal(payload.collaborationGate.isGroup, true);
|
||||
assert.equal(payload.collaborationGate.collaborationMode, "development");
|
||||
assert.equal(payload.collaborationGate.requiresMasterAgentApproval, false);
|
||||
assert.equal(payload.collaborationGate.approvalState, "not_required");
|
||||
|
||||
const nextState = await readState();
|
||||
const notice = nextState.projects
|
||||
@@ -279,12 +289,22 @@ test("rejecting a dispatch plan marks approval_required groups as rejected and w
|
||||
ok: boolean;
|
||||
plan: { planId: string; status: string };
|
||||
notice: { kind: string; body: string };
|
||||
collaborationGate: {
|
||||
isGroup: boolean;
|
||||
collaborationMode: string;
|
||||
requiresMasterAgentApproval: boolean;
|
||||
approvalState: string;
|
||||
};
|
||||
};
|
||||
assert.equal(payload.ok, true);
|
||||
assert.equal(payload.plan.planId, dispatchPlan.planId);
|
||||
assert.equal(payload.plan.status, "rejected");
|
||||
assert.equal(payload.notice.kind, "system_notice");
|
||||
assert.equal(payload.notice.body, "已拒绝主 Agent 推荐,本次不会下发到任何线程。");
|
||||
assert.equal(payload.collaborationGate.isGroup, true);
|
||||
assert.equal(payload.collaborationGate.collaborationMode, "approval_required");
|
||||
assert.equal(payload.collaborationGate.requiresMasterAgentApproval, true);
|
||||
assert.equal(payload.collaborationGate.approvalState, "rejected");
|
||||
|
||||
const nextState = await readState();
|
||||
const nextGroupProject = nextState.projects.find((project) => project.id === groupProject.id);
|
||||
|
||||
167
tests/local-agent-codex-discovery-worker.test.mjs
Normal file
167
tests/local-agent-codex-discovery-worker.test.mjs
Normal file
@@ -0,0 +1,167 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
|
||||
let runtimeRoot = "";
|
||||
let discoverCodexProjectCandidates;
|
||||
let discoverCodexProjectCandidatesInWorker;
|
||||
|
||||
async function setup() {
|
||||
if (runtimeRoot) return;
|
||||
runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-local-agent-discovery-worker-"));
|
||||
({
|
||||
discoverCodexProjectCandidates,
|
||||
discoverCodexProjectCandidatesInWorker,
|
||||
} = await import("../local-agent/codex-session-discovery.mjs"));
|
||||
}
|
||||
|
||||
test.after(async () => {
|
||||
if (runtimeRoot) {
|
||||
await rm(runtimeRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("discoverCodexProjectCandidatesInWorker returns the same folder/thread candidates as sync discovery", async () => {
|
||||
await setup();
|
||||
|
||||
const codexRoot = path.join(runtimeRoot, ".codex");
|
||||
const now = new Date("2026-03-31T12:00:00+08:00");
|
||||
await mkdir(codexRoot, { recursive: true });
|
||||
const stateDbPath = path.join(codexRoot, "state_5.sqlite");
|
||||
const logsDbPath = path.join(codexRoot, "logs_1.sqlite");
|
||||
const sessionIndexPath = path.join(codexRoot, "session_index.jsonl");
|
||||
const globalStatePath = path.join(codexRoot, ".codex-global-state.json");
|
||||
|
||||
const stateDb = new DatabaseSync(stateDbPath);
|
||||
stateDb.exec(`
|
||||
CREATE TABLE threads (
|
||||
id TEXT PRIMARY KEY,
|
||||
rollout_path TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
source TEXT NOT NULL,
|
||||
model_provider TEXT NOT NULL,
|
||||
cwd TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
sandbox_policy TEXT NOT NULL,
|
||||
approval_mode TEXT NOT NULL,
|
||||
tokens_used INTEGER NOT NULL DEFAULT 0,
|
||||
has_user_event INTEGER NOT NULL DEFAULT 0,
|
||||
archived INTEGER NOT NULL DEFAULT 0,
|
||||
archived_at INTEGER,
|
||||
git_sha TEXT,
|
||||
git_branch TEXT,
|
||||
git_origin_url TEXT,
|
||||
cli_version TEXT NOT NULL DEFAULT '',
|
||||
first_user_message TEXT NOT NULL DEFAULT '',
|
||||
agent_nickname TEXT,
|
||||
agent_role TEXT,
|
||||
memory_mode TEXT NOT NULL DEFAULT 'enabled',
|
||||
model TEXT,
|
||||
reasoning_effort TEXT,
|
||||
agent_path TEXT
|
||||
);
|
||||
`);
|
||||
const insertThread = stateDb.prepare(`
|
||||
INSERT INTO threads (
|
||||
id, rollout_path, created_at, updated_at, source, model_provider, cwd, title,
|
||||
sandbox_policy, approval_mode, tokens_used, has_user_event, archived,
|
||||
cli_version, first_user_message, agent_nickname, agent_role, memory_mode, model, reasoning_effort
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 1, ?, '0.118.0', '', ?, ?, 'enabled', 'gpt-5.4', 'medium')
|
||||
`);
|
||||
insertThread.run(
|
||||
"019d-worker-boss-main",
|
||||
path.join(codexRoot, "sessions/2026/03/31/rollout-boss-main.jsonl"),
|
||||
1774929600,
|
||||
1774929612,
|
||||
"desktop",
|
||||
"openai",
|
||||
"/Users/kris/code/boss",
|
||||
"Boss 主线程",
|
||||
"workspace-write",
|
||||
"never",
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
insertThread.run(
|
||||
"019d-worker-meiyesaas",
|
||||
path.join(codexRoot, "sessions/2026/03/31/rollout-meiyesaas.jsonl"),
|
||||
1774929630,
|
||||
1774929644,
|
||||
"desktop",
|
||||
"openai",
|
||||
"/Users/kris/code/meiyesaas",
|
||||
"美业 SaaS",
|
||||
"workspace-write",
|
||||
"never",
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
stateDb.close();
|
||||
|
||||
const logsDb = new DatabaseSync(logsDbPath);
|
||||
logsDb.exec(`
|
||||
CREATE TABLE logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ts INTEGER NOT NULL,
|
||||
ts_nanos INTEGER NOT NULL,
|
||||
level TEXT NOT NULL,
|
||||
target TEXT NOT NULL,
|
||||
feedback_log_body TEXT,
|
||||
module_path TEXT,
|
||||
file TEXT,
|
||||
line INTEGER,
|
||||
thread_id TEXT,
|
||||
process_uuid TEXT,
|
||||
estimated_bytes INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
`);
|
||||
const insertLog = logsDb.prepare(`
|
||||
INSERT INTO logs (ts, ts_nanos, level, target, thread_id, estimated_bytes)
|
||||
VALUES (?, 0, 'info', 'codex', ?, 0)
|
||||
`);
|
||||
insertLog.run(1774929612, "019d-worker-boss-main");
|
||||
insertLog.run(1774929644, "019d-worker-meiyesaas");
|
||||
logsDb.close();
|
||||
|
||||
await writeFile(
|
||||
sessionIndexPath,
|
||||
[
|
||||
JSON.stringify({
|
||||
id: "019d-worker-boss-main",
|
||||
thread_name: "Boss 主线程",
|
||||
updated_at: "2026-03-31T04:00:12.000000Z",
|
||||
}),
|
||||
JSON.stringify({
|
||||
id: "019d-worker-meiyesaas",
|
||||
thread_name: "美业 SaaS 主线程",
|
||||
updated_at: "2026-03-31T04:00:44.000000Z",
|
||||
}),
|
||||
].join("\n") + "\n",
|
||||
"utf8",
|
||||
);
|
||||
await writeFile(
|
||||
globalStatePath,
|
||||
JSON.stringify({ "thread-workspace-root-hints": {} }, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const options = {
|
||||
stateDbPath,
|
||||
logsDbPath,
|
||||
sessionIndexPath,
|
||||
globalStatePath,
|
||||
lookbackHours: 24,
|
||||
now,
|
||||
};
|
||||
|
||||
const syncResult = await discoverCodexProjectCandidates(options);
|
||||
const workerResult = await discoverCodexProjectCandidatesInWorker(options);
|
||||
|
||||
assert.deepEqual(workerResult, syncResult);
|
||||
});
|
||||
Reference in New Issue
Block a user