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
43 changes: 35 additions & 8 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,11 +293,35 @@ mosa thumbnails <rebuild|repair> [--library <path>]

### 7.1 Codex

`lib/codex-image-bridge.ts` 监视配置的 Codex 生成图像目录,默认是
`$HOME/.codex/generated_images`,并读取配置的会话目录(默认
`$HOME/.codex/sessions`)匹配任务、Prompt、模型和生成时间。它同时使用文件
watcher 与轮询,并按内容哈希和来源路径去重;找不到可靠 Prompt 时记录不可用,
不会凭空生成 Prompt。
`lib/codex-image-bridge.ts` 监视 Codex 自己的生成图像目录,并读取同一 Codex 根
下的会话数据匹配任务、Prompt、模型和生成时间。Codex 根优先取 `CODEX_HOME`;
未设置时 macOS/Linux 默认为 `$HOME/.codex`,Windows 默认为
`%USERPROFILE%\.codex`。因此默认素材目录为 `<CODEX_HOME>/generated_images`,
会话目录为 `<CODEX_HOME>/sessions`。

Codex 自动收录是双来源模型,而不是只靠一个图片目录:

- **文件来源**:监听 `<CODEX_HOME>/generated_images`,发现真实落盘图片后按来源路径、
SHA-256 和像素哈希去重,并从对应 session 补齐 Prompt、模型、call ID 和生成时间。
- **会话来源**:同时监听 `<CODEX_HOME>/sessions`。`lib/codex-session-index.ts` 对 JSONL
维护进程内字节游标;首次读取完整文件,后续只读取新增的完整行。若
`image_generation_call` / `image_generation_end` 携带图片 `result`,但标准
`generated_images` 文件不存在,MOSA 可以从 session 恢复图片,不会因为 Codex
某次未落盘而漏收。

标准文件始终优先。只有标准图片不可用时才启用 session result 恢复。
`lib/codex-session-recovery.ts` 会先限制事件与图片体积、校验 base64 并按二进制签名
识别真实图片格式,再写入 MOSA 私有临时目录;素材库完成 copy 后立即删除临时文件。
session 中的任意 `saved_path` 不会因此成为新的受信任文件根。恢复出来的素材继续走
与标准文件相同的 automatic-ingest suppression、内容哈希、像素哈希和资产创建链。

图片目录和 session 目录都使用 watcher;watcher 不可用时由轮询兜底。MOSA 不会为了
监听而创建 Codex 的 `sessions` 目录,Codex 后续创建目录后轮询会自动重新挂载 watcher。
找不到可靠 Prompt 时明确记录不可用,不会凭空生成 Prompt。

MOSA 不把 Codex 的 workspace、Documents、Desktop、Downloads 或任意项目目录当作
默认素材扫描根。若用户确实修改了 Codex 本身的存储根,应配置 Codex 官方的
`CODEX_HOME`;MOSA 会跟随同一个根,而不是另外发明一套默认目录。

### 7.2 Grok Build CLI

Expand All @@ -312,7 +336,8 @@ watcher 与轮询,并按内容哈希和来源路径去重;找不到可靠 Pr
`lib/cowart-canvas-discovery.ts` 可从近期 Codex 会话发现 Cowart 项目;
`lib/cowart-bridge-manager.ts` 管理主画布和已注册外部项目;

默认管理画布目录为 `$HOME/.codex/cowart-data/mosa`。外部画布必须通过路径、
默认管理画布目录为 `<CODEX_HOME>/cowart-data/mosa`,MOSA 自己的 Cowart registry
默认为 `<CODEX_HOME>/mosa/cowart-projects.json`。外部画布必须通过路径、
目录标记、非 symlink 和项目内边界校验;不可信条目可以移除,但不会启动 watcher。

### 7.4 Web Capture
Expand Down Expand Up @@ -349,6 +374,7 @@ Token 写入公共文档、日志或仓库:
| `MOSA_DESKTOP_PORT` | Electron 壳使用的端口,默认 `43517` |
| `MOSA_LIBRARY_DIR` | SQLite/库目录;显式指定后用于库隔离和迁移 |
| `MOSA_PROJECT_DIR` | 项目根目录覆盖 |
| `CODEX_HOME` | Codex 本地数据根;默认 `$HOME/.codex` / `%USERPROFILE%\.codex` |
| `CODEX_GENERATED_IMAGES_DIR` | Codex 生成图片目录覆盖 |
| `CODEX_SESSIONS_DIR` | Codex 会话目录覆盖 |
| `GROK_SESSIONS_DIR` | Grok 会话目录覆盖 |
Expand All @@ -357,8 +383,9 @@ Token 写入公共文档、日志或仓库:
| `MOSA_WEB_CAPTURE_TOKEN` | 启用网页捕获的本地 Token |
| `MOSA_WEB_CAPTURE_ORIGINS` | 网页捕获允许的扩展 origin 列表 |

MOSA 只读取已配置的 Codex、Grok 和 Cowart 位置,不扫描 Downloads、Desktop
或任意图片目录。导入路径、画布路径、外部来源和打开文件夹操作均经过边界
MOSA 只读取已配置的 Codex、Grok 和 Cowart 位置;不会把 Downloads、Desktop、
Documents 或任意项目图片目录作为默认扫描根。导入路径、画布路径、外部来源和
打开文件夹操作均经过边界
校验;运行时隔离检查在写入库或监听端口前失败即停止。请把 Prompt、会话 ID、
页面 URL、素材和 Token 视为私人数据。

Expand Down
15 changes: 14 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,20 @@ This file records user-visible changes. Internal deployment notes, local paths,

## Unreleased

No user-visible changes recorded yet.
## 0.2.1-rc.13 — 2026-09-13 / Release Candidate

### Codex default source alignment / Codex 默认来源对齐

- MOSA now treats Codex's own `CODEX_HOME` as the single default root for Codex-owned local data. When `CODEX_HOME` is unset, the default remains `$HOME/.codex` on macOS/Linux and `%USERPROFILE%\.codex` on Windows.
- Automatic Codex image archive reads `generated_images` under that root, while Prompt/model/provenance matching reads `sessions` under the same root. Cowart's MOSA canvas and registry defaults are rebased from that same Codex root as well.
- Codex automatic archive now treats `generated_images` and `sessions` as two coordinated sources: standard generated files remain primary, while inline `image_generation_call` / `image_generation_end` results can recover an image when Codex did not persist the advertised generated file.
- Session JSONL processing was redesigned as an incremental byte-indexed reader. After the initial scan, an active runtime reads only newly appended complete records instead of reparsing whole session files on every bridge reconciliation.
- Inline session images are size-limited, base64-validated, binary-signature checked, staged only inside MOSA's private recovery directory, copied into the library, and then removed. Session `saved_path` values do not widen MOSA's filesystem trust boundary.
- Removed the MOSA-invented Windows `Documents\\codex` default and project-directory image scanning path. Custom source variables remain explicit overrides, but they no longer redefine Codex's defaults implicitly.
- Runtime startup resolves the Codex/Cowart source locations once and passes the same paths to every integration, preventing image, session, and Cowart components from drifting onto different roots.
- Codex automatic archive now reconciles two native sources: files under `<CODEX_HOME>/generated_images` and image-generation events appended to `<CODEX_HOME>/sessions`, so result-only generations can still be recovered when Codex does not persist the image file.
- Session JSONL processing is incremental by byte offset, merges duplicate call/end surfaces by generation identity, carries incomplete records across scans, and bounds inline-result recovery without rescanning whole sessions on every poll.
- Recovered session results are validated into a MOSA-private temporary area, copied into the library, then removed; they are never treated as external trusted roots or later hard-linked back to Codex session data.

## 0.2.1-rc.12 — 2026-09-12 / Release Candidate

Expand Down
14 changes: 10 additions & 4 deletions docs/guide.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,17 @@ npm exec mosa -- thumbnails rebuild --library /absolute/path/to/library

### Codex 生图

服务运行时,MOSA 只监听 `~/.codex/generated_images/`。它会匹配对应 Codex 任务的本地会话 JSONL:优先保存生图事件中的 `revised_prompt`,缺失时才回退保存任务最后一条用户指令,并明确记录 Prompt 的来源状态
服务运行时,MOSA 跟随 Codex 自己的本地数据根。若设置了 `CODEX_HOME`,就以该目录为准;未设置时 macOS/Linux 使用 `$HOME/.codex`,Windows 使用 `%USERPROFILE%\.codex`

Windows 10/11 x64 真机已经验证 Codex 自动收录链路可工作。来源路径仍通过统一的 source-location resolver 处理;后续若 Codex 官方 Windows 存储布局发生变化,应以真机检测结果为准,而不是在业务代码中散落平台判断
默认自动收录使用两条互补通道:`<CODEX_HOME>/generated_images/` 是标准图片来源,`<CODEX_HOME>/sessions/` 是生成事件与元数据来源。正常情况下 MOSA 优先收标准落盘图片,再按同一 Codex session 补齐 Prompt、模型、call ID 和生成时间

MOSA 不扫描 Downloads、桌面或任意本地图片目录。原图在同一文件系统时优先硬链接入库,跨文件系统时才复制。
如果 Codex 的 session 已经记录了 `image_generation_call` / `image_generation_end` 图片结果,但标准 `generated_images` 文件没有真正写出来,MOSA 会自动从 session 中恢复这张图片并正常入库。恢复结果先经过体积限制、base64 校验和真实图片格式检查,只暂存在 MOSA 自己的私有目录,完成素材库 copy 后立即删除;session 里的任意 `saved_path` 不会因此获得额外文件读取权限。

运行期间 MOSA 同时监听图片目录和 session 目录。session JSONL 首次建立索引后只读取后续新增的完整记录,不会在每次轮询时重新解析整份会话。watcher 不可用时仍有轮询兜底。MOSA 不再把 `Documents\\codex`、当前 workspace 或任意项目目录当成 Codex 默认素材来源。

如果你以前改过 Codex 自己的存储根,应修改 Codex 的 `CODEX_HOME`,MOSA 会跟随同一个根。`CODEX_GENERATED_IMAGES_DIR` 和 `CODEX_SESSIONS_DIR` 仍可作为显式高级覆盖,但它们不是新的默认规则。

MOSA 不扫描 Downloads、桌面、Documents 或任意本地图片目录。原图在同一文件系统时优先硬链接入库,跨文件系统时才复制。

### 网页生图

Expand Down Expand Up @@ -113,7 +119,7 @@ Prompt 优先级:仅在工具调用与 tool_result 能匹配到该媒体路径

### Cowart 画布

MOSA 始终监听自己的专用画布 `~/.codex/cowart-data/mosa/`。其他项目必须先在 Codex 中真实打开 Cowart 画布;MOSA 从本地启动记录识别项目,验证 `<项目>/canvas/` 的画布标记后才加入允许列表并监听。
MOSA 始终监听 Codex 根下的专用画布 `<CODEX_HOME>/cowart-data/mosa/`;未设置 `CODEX_HOME` 时就是用户默认的 `.codex/cowart-data/mosa/`。其他项目必须先在 Codex 中真实打开 Cowart 画布;MOSA 从本地启动记录识别项目,验证 `<项目>/canvas/` 的画布标记后才加入允许列表并监听。

Cowart 快照可提供画布说明与来源,但不保证具有完整生图 Prompt。MOSA 会保留这种差异,避免把画布描述误写成完整 Prompt。

Expand Down
3 changes: 3 additions & 0 deletions docs/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ Expected conditions:

- `storage` is `sqlite` only after a completed migration.
- Codex, Grok, and Cowart report `enabled: true`; watcher/polling availability depends on the local source directories.
- Codex image and session sources should resolve beneath the same `CODEX_HOME` unless an explicit per-source override is configured. With no overrides, use `<CODEX_HOME>/generated_images` and `<CODEX_HOME>/sessions`; when `CODEX_HOME` is unset it defaults to the user's `.codex` directory.
- Codex bridge diagnostics expose independent `watchingImages` and `watchingSessions` flags. `pendingSessionResults` is the number of generation events still waiting for a standard file or recoverable session result, and `lastSessionBytesRead` shows how many JSONL bytes the most recent incremental scan actually consumed.
- Standard files under `generated_images` are always preferred. Session-result recovery is only a fallback when the standard file is unavailable; recovered bytes are validated and staged inside MOSA's private assets area, then copied into the library and removed from staging.
- `grok.sessionsDir` points at the configured Grok sessions root (default `~/.grok/sessions`).
- `lastError` is empty or `null`.
- `cowartDiscovery` is enabled when the service can read local Codex session records.
Expand Down
13 changes: 12 additions & 1 deletion lib/codex-hardlink.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createHash, randomUUID } from "node:crypto";
import { link, readFile, rename, stat, unlink } from "node:fs/promises";
import { basename, dirname, join, resolve } from "node:path";
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";

interface Asset {
id: string;
Expand Down Expand Up @@ -49,6 +49,12 @@ export async function relinkCodexAsset(asset: Asset): Promise<SingleRelinkResult
const sourcePath = asset.source?.path ? resolve(asset.source.path as string) : null;
const targetPath = asset.image_path ? resolve(asset.image_path) : null;
if (!sourcePath || !targetPath) return { status: "missing-path" };
const generatedImagesRoot = typeof asset.source?.codex_generated_images_root === "string"
? resolve(asset.source.codex_generated_images_root as string)
: null;
if (!generatedImagesRoot || !isStrictChildPath(generatedImagesRoot, sourcePath)) {
return { status: "non-generated-images-source" };
}
let sourceStat, targetStat;
try {
[sourceStat, targetStat] = await Promise.all([stat(sourcePath), stat(targetPath)]);
Expand All @@ -72,3 +78,8 @@ export async function relinkCodexAsset(asset: Asset): Promise<SingleRelinkResult
await unlink(temporaryPath).catch(() => {});
}
}

function isStrictChildPath(parent: string, child: string): boolean {
const rel = relative(resolve(parent), resolve(child));
return Boolean(rel) && !rel.startsWith("..") && !isAbsolute(rel);
}
Loading
Loading