Skip to content
Closed
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ src/utils/vendor/
.claude/
.codex/
.omx/

.docs/task/
# Binary / screenshot files (root only)
/*.png
*.bmp
Expand Down
79 changes: 79 additions & 0 deletions DEV-LOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,84 @@
# DEV-LOG

## Integrate 5 Feature Branches + MIME Detection Fix (2026-04-14)

**分支**: `feat/integrate-features`
**基于**: PR #259 (`7f2b7182`, CI 全过) + MIME 修复
**文件**: 124 changed, +13555 / -1901

### 1. MCP tsc 错误修复 (`fix/mcp-tsc-errors`)

上游 MCP 重构后产生的 TypeScript 编译错误,修复 43 个预存在的类型错误。

### 2. Pipe IPC 断开 + /lang 命令 + Mute 状态机 (`feat/pipe-mute-disconnect`)

- `src/hooks/usePipeMuteSync.ts` — Pipe mute 状态同步 hook
- `src/utils/pipeMuteState.ts` — Mute 状态机实现
- `src/commands/lang/` — `/lang` 命令,运行时切换语言
- `src/utils/language.ts` — 语言检测与切换工具

### 3. Stub 恢复 (task 001-012) (`feat/stub-recovery-all`)

恢复全部 stub 为完整实现:

- **Daemon 状态管理**: `src/daemon/state.ts` — 持久化 daemon 状态 (PID、端口、worker 列表)
- **Job 系统**: `src/jobs/state.ts`, `src/jobs/templates.ts`, `src/jobs/classifier.ts` — 任务状态、模板、分类器
- **后台会话**: `src/cli/bg/engine.ts`, `engines/detached.ts`, `engines/tmux.ts` — 跨平台后台引擎抽象
- **Assistant 会话**: `src/assistant/AssistantSessionChooser.tsx` — 会话选择器从 .ts 迁移至 .tsx
- **Proactive/Schedule**: `src/hooks/useScheduledTasks.ts`, `src/proactive/useProactive.ts` — 定时任务与主动提示

### 4. KAIROS 激活 (`feat/kairos-activation`)

- 解除 KAIROS 功能的编译阻塞
- `build.ts` + `scripts/dev.ts` — 添加 `KAIROS_BRIEF` 到默认 feature flag 列表
- `src/hooks/useMasterMonitor.ts` — Master monitor hook 实现
- `src/commands/torch.ts` — Torch 命令增强

### 5. Openclaw 自治系统 (`codex/openclaw-autonomy-pr`)

- `src/utils/autonomyAuthority.ts` — 自治权限管理 (522 行)
- `src/utils/autonomyFlows.ts` — 自治工作流 managed flows (1057 行)
- `src/utils/autonomyRuns.ts` — 运行记录持久化 (797 行)
- `src/commands/autonomy.ts` — `/autonomy` 命令入口
- `src/utils/autonomyPersistence.ts` — 持久化工具

### 6. Daemon/Job 命令层级化

- `src/commands/daemon/` — `daemon.tsx` + `index.ts` (subcommand 架构)
- `src/commands/job/` — `job.tsx` + `index.ts`
- `src/entrypoints/cli.tsx` — 快速路径注册 daemon/job subcommands
- `src/cli/handlers/ant.ts`, `src/cli/handlers/templateJobs.ts` — handler 增强

### 7. 其他改动

- **Remote Control Server**: `packages/remote-control-server/src/logger.ts` — logger 抽象,测试 stderr 静默化
- **InProcessTeammateTask**: `src/tasks/InProcessTeammateTask/` — teammate 任务类型扩展
- **GrowthBook**: `src/services/analytics/growthbook.ts` — gate 增强
- **Away Summary**: `src/services/awaySummary.ts` — 修复调试问题
- **测试**: 新增/重构 30+ 测试文件,mock 隔离 (langfuse, openai)

### 8. Screenshot MIME 类型检测修复

**文件**: `packages/@ant/computer-use-mcp/src/toolCalls.ts`

**问题**: `detectMimeFromBase64()` 用 `charCodeAt(0)` 比较原始字节 magic number 与 base64 编码后的字符。Base64 编码会改变字节值,所有条件永远不命中,函数始终返回 `"image/png"`。Windows Python bridge 输出 JPEG 截图,导致 API 400 错误。

**修复**: 解码 base64 前 16 个字符得到 12 个原始字节,直接比对标准 magic byte 签名:
- PNG: `89 50 4E 47` (4 字节)
- JPEG: `FF D8 FF` (3 字节,覆盖所有 JFIF/EXIF/DQT 变体)
- WebP: `RIFF` (字节 0-3) + `WEBP` (字节 8-11) 双重验证
- GIF: `47 49 46` (覆盖 GIF87a 和 GIF89a)

**验证**: Codex (GPT-5.4) 通过 `Buffer.from().toString('base64')` 实际计算确认所有前缀正确。

### 验证结果

- `bunx tsc --noEmit`: 0 errors
- `bun test`: 2758 pass, 0 fail
- 手动测试: Windows Computer Use screenshot 不再报 API 400

---

## /poor 省流模式 (2026-04-11)

新增 `/poor` 命令,toggle 关闭 `extract_memories` 和 `prompt_suggestion`,省 token。
Expand Down
2 changes: 2 additions & 0 deletions build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ const DEFAULT_BUILD_FEATURES = [
'KAIROS',
'COORDINATOR_MODE',
'LAN_PIPES',
'BG_SESSIONS',
'TEMPLATES',
// 'REVIEW_ARTIFACT', // API 请求无响应,需进一步排查 schema 兼容性
// P3: poor mode (disable extract_memories + prompt_suggestion)
'POOR',
Expand Down
23 changes: 14 additions & 9 deletions packages/@ant/computer-use-mcp/src/toolCalls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,21 @@
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { randomUUID } from "node:crypto";

/** Detect actual image MIME type from base64 data using magic bytes. */
/** Detect actual image MIME type from base64 data by decoding the magic bytes. */
function detectMimeFromBase64(b64: string): string {
// First byte is enough to distinguish PNG (0x89) from JPEG (0xFF)
const c = b64.charCodeAt(0);
if (c === 0x89) return "image/png";
if (c === 0xFF) return "image/jpeg";
// RIFF = WebP
if (c === 0x52) return "image/webp";
// GIF
if (c === 0x47) return "image/gif";
// Decode first 12 raw bytes (16 base64 chars is enough) and check standard magic bytes.
// PNG: 89 50 4E 47
// JPEG: FF D8 FF
// RIFF+WEBP: "RIFF" at 0..3 + "WEBP" at 8..11
// GIF: "GIF" at 0..2
const raw = Buffer.from(b64.slice(0, 16), "base64");
if (raw[0] === 0x89 && raw[1] === 0x50 && raw[2] === 0x4e && raw[3] === 0x47) return "image/png";
if (raw[0] === 0xff && raw[1] === 0xd8 && raw[2] === 0xff) return "image/jpeg";
if (
raw[0] === 0x52 && raw[1] === 0x49 && raw[2] === 0x46 && raw[3] === 0x46 && // RIFF
raw[8] === 0x57 && raw[9] === 0x45 && raw[10] === 0x42 && raw[11] === 0x50 // WEBP
) return "image/webp";
if (raw[0] === 0x47 && raw[1] === 0x49 && raw[2] === 0x46) return "image/gif";
return "image/png";
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { feature } from 'bun:bundle'
import { z } from 'zod/v4'
import type { ToolResultBlockParam } from 'src/Tool.js'
import { buildTool } from 'src/Tool.js'
import { lazySchema } from 'src/utils/lazySchema.js'
import { logForDebugging } from 'src/utils/debug.js'

const PUSH_NOTIFICATION_TOOL_NAME = 'PushNotification'

Expand Down Expand Up @@ -74,14 +76,58 @@ Requires Remote Control to be configured. Respects user notification settings (t
}
},

async call(_input: PushInput) {
// Push delivery is handled by the Remote Control / KAIROS transport layer.
// Without the KAIROS runtime, this tool is not available.
return {
data: {
sent: false,
error: 'PushNotification requires the KAIROS transport layer.',
},
async call(input: PushInput, context) {
const appState = context.getAppState()

// Try bridge delivery first (for remote/mobile viewers)
if (appState.replBridgeEnabled) {
if (feature('BRIDGE_MODE')) {
try {
const { getBridgeAccessToken, getBridgeBaseUrl } = await import(
'src/bridge/bridgeConfig.js'
)
const { getSessionId } = await import('src/bootstrap/state.js')
const token = getBridgeAccessToken()
const sessionId = getSessionId()
if (token && sessionId) {
const baseUrl = getBridgeBaseUrl()
const axios = (await import('axios')).default
const response = await axios.post(
`${baseUrl}/v1/sessions/${sessionId}/events`,
{
events: [
{
type: 'push_notification',
title: input.title,
body: input.body,
priority: input.priority ?? 'normal',
},
],
},
{
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
'anthropic-version': '2023-06-01',
},
timeout: 10_000,
validateStatus: (s: number) => s < 500,
},
)
if (response.status >= 200 && response.status < 300) {
logForDebugging(`[PushNotification] delivered via bridge session=${sessionId}`)
return { data: { sent: true } }
}
logForDebugging(`[PushNotification] bridge delivery failed: status=${response.status}`)
}
} catch (e) {
logForDebugging(`[PushNotification] bridge delivery error: ${e}`)
}
}
}

// Fallback: no bridge available, push was not delivered to a remote device.
logForDebugging(`[PushNotification] no bridge available, not delivered: ${input.title}`)
return { data: { sent: false, error: 'No Remote Control bridge configured. Notification not delivered.' } }
},
})
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,51 @@ Guidelines:
}
},

async call(_input: SendUserFileInput) {
// File transfer is handled by the KAIROS assistant transport layer.
// Without the KAIROS runtime, this tool is not available.
async call(input: SendUserFileInput, context) {
const { file_path } = input
const { stat } = await import('fs/promises')

// Verify file exists and is readable
let fileSize: number
try {
const fileStat = await stat(file_path)
if (!fileStat.isFile()) {
return {
data: { sent: false, file_path, error: 'Path is not a file.' },
}
}
fileSize = fileStat.size
} catch {
return {
data: { sent: false, file_path, error: 'File does not exist or is not readable.' },
}
}

// Attempt bridge upload if available (so web viewers can download)
const appState = context.getAppState()
let fileUuid: string | undefined
if (appState.replBridgeEnabled) {
try {
const { uploadBriefAttachment } = await import(
'@claude-code-best/builtin-tools/tools/BriefTool/upload.js'
)
fileUuid = await uploadBriefAttachment(file_path, fileSize, {
replBridgeEnabled: true,
signal: context.abortController.signal,
})
} catch {
// Best-effort upload — local path is always available
}
}

const delivered = !appState.replBridgeEnabled || Boolean(fileUuid)
return {
data: {
sent: false,
file_path: _input.file_path,
error: 'SendUserFile requires the KAIROS assistant transport layer.',
sent: delivered,
file_path,
size: fileSize,
...(fileUuid ? { file_uuid: fileUuid } : {}),
...(!delivered ? { error: 'Bridge upload failed. File available at local path.' } : {}),
},
}
},
Expand Down
10 changes: 10 additions & 0 deletions packages/remote-control-server/src/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/** Thin logging wrapper — silent in test environment, uses console in production. */
const isTest = process.env.NODE_ENV === "test" || (typeof Bun !== "undefined" && !!Bun.env.BUN_TEST);

export function log(...args: unknown[]): void {
if (!isTest) console.log(...args);
}

export function error(...args: unknown[]): void {
if (!isTest) console.error(...args);
}
11 changes: 6 additions & 5 deletions packages/remote-control-server/src/routes/v1/session-ingress.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { log, error as logError } from "../../logger";
import { Hono } from "hono";
import { createBunWebSocket } from "hono/bun";
import { validateApiKey } from "../../auth/api-key";
Expand Down Expand Up @@ -30,14 +31,14 @@ function authenticateRequest(c: any, label: string, expectedSessionId?: string):
const payload = verifyWorkerJwt(token);
if (payload) {
if (expectedSessionId && payload.session_id !== expectedSessionId) {
console.log(`[Auth] ${label}: FAILED — JWT session_id mismatch`);
log(`[Auth] ${label}: FAILED — JWT session_id mismatch`);
return false;
}
return true;
}
}

console.log(`[Auth] ${label}: FAILED — no valid API key or JWT`);
log(`[Auth] ${label}: FAILED — no valid API key or JWT`);
return false;
}

Expand Down Expand Up @@ -83,15 +84,15 @@ app.get(

const session = getSession(sessionId);
if (!session) {
console.log(`[WS] Upgrade rejected: session ${sessionId} not found`);
log(`[WS] Upgrade rejected: session ${sessionId} not found`);
return {
onOpen(_evt, ws) {
ws.close(4001, "session not found");
},
};
}

console.log(`[WS] Upgrade accepted: session=${sessionId}`);
log(`[WS] Upgrade accepted: session=${sessionId}`);
return {
onOpen(_evt, ws) {
handleWebSocketOpen(ws as any, sessionId);
Expand All @@ -108,7 +109,7 @@ app.get(
handleWebSocketClose(ws as any, sessionId, closeEvt?.code, closeEvt?.reason);
},
onError(evt, ws) {
console.error(`[WS] Error on session=${sessionId}:`, evt);
logError(`[WS] Error on session=${sessionId}:`, evt);
handleWebSocketClose(ws as any, sessionId, 1006, "websocket error");
},
};
Expand Down
3 changes: 2 additions & 1 deletion packages/remote-control-server/src/routes/v1/sessions.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { log, error as logError } from "../../logger";
import { Hono } from "hono";
import {
createSession,
Expand All @@ -22,7 +23,7 @@ app.post("/", acceptCliHeaders, apiKeyAuth, async (c) => {
try {
await createWorkItem(body.environment_id, session.id);
} catch (err) {
console.error(`[RCS] Failed to create work item: ${(err as Error).message}`);
logError(`[RCS] Failed to create work item: ${(err as Error).message}`);
}
}

Expand Down
5 changes: 3 additions & 2 deletions packages/remote-control-server/src/routes/web/control.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { log, error as logError } from "../../logger";
import { Hono } from "hono";
import { uuidAuth } from "../../auth/middleware";
import { getSession, updateSessionStatus } from "../../services/session";
Expand Down Expand Up @@ -29,9 +30,9 @@ app.post("/sessions/:id/events", uuidAuth, async (c) => {

const body = await c.req.json();
const eventType = body.type || "user";
console.log(`[RC-DEBUG] web -> server: POST /web/sessions/${sessionId}/events type=${eventType} content=${JSON.stringify(body).slice(0, 200)}`);
log(`[RC-DEBUG] web -> server: POST /web/sessions/${sessionId}/events type=${eventType} content=${JSON.stringify(body).slice(0, 200)}`);
const event = publishSessionEvent(sessionId, eventType, body, "outbound");
console.log(`[RC-DEBUG] web -> server: published outbound event id=${event.id} type=${event.type} direction=${event.direction} subscribers=${getEventBus(sessionId).subscriberCount()}`);
log(`[RC-DEBUG] web -> server: published outbound event id=${event.id} type=${event.type} direction=${event.direction} subscribers=${getEventBus(sessionId).subscriberCount()}`);
return c.json({ status: "ok", event }, 200);
});

Expand Down
3 changes: 2 additions & 1 deletion packages/remote-control-server/src/routes/web/sessions.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { log, error as logError } from "../../logger";
import { Hono } from "hono";
import { uuidAuth } from "../../auth/middleware";
import { getSession, createSession } from "../../services/session";
Expand Down Expand Up @@ -28,7 +29,7 @@ app.post("/sessions", uuidAuth, async (c) => {
try {
await createWorkItem(body.environment_id, session.id);
} catch (err) {
console.error(`[RCS] Failed to create work item: ${(err as Error).message}`);
logError(`[RCS] Failed to create work item: ${(err as Error).message}`);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { log, error as logError } from "../logger";
import { storeListActiveEnvironments, storeUpdateEnvironment } from "../store";
import { storeListSessions, storeUpdateSession } from "../store";
import { config } from "../config";
Expand All @@ -12,7 +13,7 @@ export function startDisconnectMonitor() {
const envs = storeListActiveEnvironments();
for (const env of envs) {
if (env.lastPollAt && now - env.lastPollAt.getTime() > timeoutMs) {
console.log(`[RCS] Environment ${env.id} timed out (no poll for ${Math.round((now - env.lastPollAt.getTime()) / 1000)}s)`);
log(`[RCS] Environment ${env.id} timed out (no poll for ${Math.round((now - env.lastPollAt.getTime()) / 1000)}s)`);
storeUpdateEnvironment(env.id, { status: "disconnected" });
}
}
Expand All @@ -23,7 +24,7 @@ export function startDisconnectMonitor() {
if (session.status === "running" || session.status === "idle") {
const elapsed = now - session.updatedAt.getTime();
if (elapsed > timeoutMs * 2) {
console.log(`[RCS] Session ${session.id} marked inactive (no update for ${Math.round(elapsed / 1000)}s)`);
log(`[RCS] Session ${session.id} marked inactive (no update for ${Math.round(elapsed / 1000)}s)`);
storeUpdateSession(session.id, { status: "inactive" });
}
}
Expand Down
Loading
Loading