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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ jobs:
- run: ./tests/deployment-package.sh
- uses: actions/upload-artifact@v4
with:
name: farhelm-V0.7.0-linux-x86_64
name: farhelm-V0.7.1-linux-x86_64
path: |
dist/release/farhelm-*
dist/release/SHA256SUMS
Expand Down
14 changes: 7 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ members = [
]

[workspace.package]
version = "0.7.0"
version = "0.7.1"
edition = "2024"
rust-version = "1.98"
license = "Apache-2.0"
Expand Down
12 changes: 10 additions & 2 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
<p><strong>A remote control plane for personal research and GPU training environments</strong></p>
<p>See training-host status from your phone and safely extend remote control without exposing inbound ports on training machines.</p>
<p>
<a href="https://github.com/Xiiiing/FarHelm/releases/tag/V0.7.0">V0.7.0</a> ·
<a href="https://github.com/Xiiiing/FarHelm/releases/tag/V0.7.1">V0.7.1</a> ·
<a href="./deploy/README.en.md">Deployment guide</a> ·
<a href="./README.md">简体中文</a>
</p>
</div>

> [!IMPORTANT]
> `V0.7.0` adds training-script reports, a unified notification center, and in-page alerts, with improved Codex queues, large-message continuation, and restart recovery. The Agent persists Codex content; Hub only relays it temporarily. Training continues to run locally.
> `V0.7.1` fixes the Codex workspace: global navigation stays visible, projects, sessions, and execution details have clear hierarchy, and Markdown, math, session labels, and search across all imported sessions render correctly. It also fixes composer placement, streamed message identity, and session-switch races. The Agent persists Codex content; Hub only relays it temporarily.

## Quick install

Expand Down Expand Up @@ -114,6 +114,14 @@ The notification center provides pagination, type/Agent/result filters, synchron

Browser commands and schedules are acknowledged after Agent persistence. Failed submissions retain the current-page draft and operation identity for retries. On restart, saved terminal receipts reconcile completed work; running tasks without a terminal receipt become orphaned and are not replayed. Upgrade Hub before Agent; the new content relay requires the Agent's V0.7 capability.

## Codex workspace

Desktop global navigation and mobile bottom navigation remain visible in Codex. Project groups collapse, and sessions use formal names or a temporary first-user-message summary. Search covers all imported sessions in the selected scope; offline Agents are clearly marked as incomplete results. Summaries and search results never enter Hub databases, logs, or notification titles.

Assistant replies support Markdown tables, code copying, math, and safe HTTPS links. Consecutive execution details in a turn form a collapsed group with visible failures. The composer stays visible, and reading older content does not jump to the bottom on new messages. Large-message continuation is separate from loading earlier conversations. Each session retains its draft; steer and interrupt target the visible active turn.

Upgrade Hub before Agent when moving from V0.7.0. SQLite schema stays at 7. Temporary labels and search require a V0.7.1 Agent; older Agents show an upgrade prompt.

## What is implemented

- `farhelm-hub`: Rust control plane with password login, SQLite-backed 30-day sessions, short-code pairing, Secure HttpOnly cookies, CSRF, login throttling, reliable events, SSE replay, and Web Push.
Expand Down
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
<p><strong>面向个人科研与 GPU 训练环境的远程控制平面</strong></p>
<p>从手机查看训练服务器状态,并在不开放训练机入站端口的前提下安全扩展远程控制能力。</p>
<p>
<a href="https://github.com/Xiiiing/FarHelm/releases/tag/V0.7.0">V0.7.0</a> ·
<a href="https://github.com/Xiiiing/FarHelm/releases/tag/V0.7.1">V0.7.1</a> ·
<a href="./deploy/README.md">部署文档</a> ·
<a href="./README.en.md">English</a>
</p>
</div>

> [!IMPORTANT]
> `V0.7.0` 加入训练脚本结果上报、统一通知中心和页面提醒,完善 Codex 排队、长消息续读与重启恢复。Codex 正文由 Agent 持久化,Hub 仅短暂中转。训练继续在本地运行。
> `V0.7.1` 修复 Codex 工作区:保留系统导航,明确项目、会话与执行过程层级;补齐 Markdown、公式、会话名称与全部已导入会话搜索,并修复输入区、流式消息及切换竞态。Codex 正文由 Agent 持久化,Hub 仅短暂中转。

## 快速安装

Expand Down Expand Up @@ -114,6 +114,14 @@ printf '训练失败,请登录查看详情' | farhelm-agent experiment report

网页指令和调度在 Agent 保存后才确认提交;失败时当前页面保留草稿并沿用操作身份重试。运行中的任务在 Agent 重启后按已保存的终态收据恢复;无终态收据时标记 orphaned,不自动重放。升级先 Hub 后 Agent,新正文交付要求 Agent 的 V0.7 能力标识。

## Codex 工作区

进入 Codex 后保留桌面全局侧栏和手机底部导航。项目分组可折叠,会话名称优先使用正式标题,没有标题时临时读取首条用户消息摘要。搜索覆盖筛选范围内全部已导入会话;离线 Agent 会明确提示结果不完整。摘要与搜索结果不写入 Hub 数据库或日志,也不用于通知标题。

助手回复支持 Markdown 表格、代码复制、公式及安全 HTTPS 链接。同一轮连续执行过程默认合并折叠,失败可直接辨认。输入区固定可见,阅读旧内容时不会被新消息拉回底部;大消息的“继续加载此消息”与“加载更早对话”分别处理。切换会话保留各自草稿,补充与中断只针对可见的活动轮次。

从 V0.7.0 升级先更新 Hub,再更新 Agent。SQLite schema 保持 7,临时会话名称和搜索需要 V0.7.1 Agent;旧 Agent 会显示升级提示。

## 当前实现

- `farhelm-hub`:Rust 控制平面、密码登录、SQLite 30 天会话、短码配对、Secure HttpOnly Cookie、CSRF、登录限速、可靠事件、SSE 补发和 Web Push。
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.7.0
0.7.1
2 changes: 1 addition & 1 deletion crates/farhelm-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,6 @@ mod tests {
fn build_info_uses_workspace_identity() {
let info = BuildInfo::current();
assert_eq!(info.product, "FarHelm");
assert_eq!(info.version, "0.7.0");
assert_eq!(info.version, "0.7.1");
}
}
90 changes: 88 additions & 2 deletions crates/farhelm-protocol/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,11 @@ impl AgentHeartbeat {
agent_version: impl Into<String>,
) -> Self {
Self {
capabilities: vec!["codex.ephemeral_submit".to_owned()],
capabilities: vec![
"codex.ephemeral_submit".to_owned(),
"codex.session_display".to_owned(),
"codex.item_offsets".to_owned(),
],
protocol: FARHELM_PROTOCOL.to_owned(),
agent_id: agent_id.into(),
hostname: hostname.into(),
Expand Down Expand Up @@ -283,6 +287,25 @@ pub struct CodexSessionListResponse {
pub next_cursor: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SessionDisplayRequest {
pub mode: String,
#[serde(default)]
pub session_ids: Vec<String>,
pub query: Option<String>,
pub agent_id: Option<String>,
pub project_id: Option<String>,
#[serde(default = "display_archive_filter")]
pub archived: String,
pub cursor: Option<String>,
pub limit: Option<usize>,
}

fn display_archive_filter() -> String {
"false".into()
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CodexTranscriptItemKind {
Expand All @@ -298,6 +321,16 @@ pub struct CodexTranscriptItem {
pub item_id: String,
pub kind: CodexTranscriptItemKind,
pub text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub text_offset: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub text_complete: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exit_code: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub duration_ms: Option<u64>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
Expand All @@ -313,11 +346,22 @@ pub struct CodexTranscriptTurn {

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CodexTranscriptPage {
#[serde(default)]
pub protocol: String,
pub session_id: String,
pub turns: Vec<CodexTranscriptTurn>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub continuation: Option<TranscriptContinuation>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TranscriptContinuation {
pub kind: String,
pub turn_id: String,
pub item_id: String,
pub text_offset: u64,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
Expand Down Expand Up @@ -487,6 +531,8 @@ pub enum PromptDelivery {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SendCodexMessageRequest {
pub prompt: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub turn_id: Option<String>,
#[serde(default = "default_prompt_delivery")]
pub delivery: PromptDelivery,
}
Expand Down Expand Up @@ -625,6 +671,19 @@ where
}

/// Only this metadata crosses into durable Hub events. Transcript content uses the read relay.
#[must_use]
pub fn valid_session_title(title: &str) -> bool {
!matches!(
title.trim().to_lowercase().as_str(),
"" | "codex session"
| "untitled"
| "new conversation"
| "new chat"
| "未命名会话"
| "新会话"
)
}

#[must_use]
pub fn public_event_payload(event_type: &str, payload: &serde_json::Value) -> serde_json::Value {
use serde_json::{Map, Value};
Expand All @@ -651,6 +710,7 @@ pub fn public_event_payload(event_type: &str, payload: &serde_json::Value) -> se
"updated_at_unix",
],
"codex.session.updated" => &[
"update_kind",
"session_id",
"project_id",
"mode",
Expand Down Expand Up @@ -726,6 +786,11 @@ pub fn public_event_payload(event_type: &str, payload: &serde_json::Value) -> se
{
safe.insert("delta".to_owned(), delta.clone());
}
if event_type == "codex.message.delta"
&& let Some(offset) = data.get("text_offset").and_then(Value::as_u64)
{
safe.insert("text_offset".to_owned(), Value::from(offset));
}
result.insert("data".to_owned(), Value::Object(safe));
}
Value::Object(result)
Expand All @@ -735,6 +800,27 @@ pub fn public_event_payload(event_type: &str, payload: &serde_json::Value) -> se
mod tests {
use super::*;

#[test]
fn display_and_extended_history_contract_keep_content_out_of_event_fields() {
let fixture: Value =
serde_json::from_str(include_str!("../tests/fixtures/session-display.json")).unwrap();
let request: SessionDisplayRequest = serde_json::from_value(fixture.clone()).unwrap();
assert_eq!(serde_json::to_value(request).unwrap(), fixture);
let page: CodexTranscriptPage = serde_json::from_value(serde_json::json!({"session_id":"s","turns":[{"turn_id":"t","status":"completed","cwd":"private","items":[{"item_id":"i","kind":"assistant_message","text":"完整🙂","text_offset":7,"text_complete":false,"raw_output":"private"}]}],"continuation":{"kind":"message","turn_id":"t","item_id":"i","text_offset":10}})).unwrap();
let safe = serde_json::to_value(page).unwrap();
assert!(safe["turns"][0].get("cwd").is_none());
assert!(safe["turns"][0]["items"][0].get("raw_output").is_none());
assert_eq!(safe["turns"][0]["items"][0]["text_offset"], 7);
let delta = public_event_payload(
"codex.message.delta",
&serde_json::json!({"session_id":"s","data":{"turn_id":"t","item_id":"i","text_offset":7,"delta":"完整🙂","cwd":"private"}}),
);
assert_eq!(delta["data"]["text_offset"], 7);
assert!(delta["data"].get("cwd").is_none());
let terminal = public_event_payload("codex.turn.completed", &delta);
assert!(terminal["data"].get("delta").is_none());
}

#[test]
fn health_shape_is_stable() {
let value = serde_json::to_value(HealthResponse::hub("0.1.0")).unwrap();
Expand All @@ -760,7 +846,7 @@ mod tests {
"agent_id": "gpu-a",
"hostname": "trainer-a",
"agent_version": "0.1.0",
"capabilities": ["codex.ephemeral_submit"]
"capabilities": ["codex.ephemeral_submit", "codex.session_display", "codex.item_offsets"]
})
);
}
Expand Down
10 changes: 10 additions & 0 deletions crates/farhelm-protocol/tests/fixtures/session-display.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"mode": "search",
"session_ids": [],
"query": "synthetic preview",
"agent_id": "gpu-a",
"project_id": "cc08",
"archived": "all",
"cursor": null,
"limit": 50
}
4 changes: 2 additions & 2 deletions crates/farhelm-test-support/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use farhelm_protocol::{HealthResponse, WORKER_PROTOCOL, WorkerResponse};

#[must_use]
pub fn health_fixture() -> HealthResponse {
HealthResponse::hub("0.7.0")
HealthResponse::hub("0.7.1")
}

#[must_use]
Expand All @@ -16,7 +16,7 @@ pub fn worker_hello_fixture(request_id: impl Into<String>) -> WorkerResponse {
ok: true,
result: Some(serde_json::json!({
"worker": "farhelm-worker-codex",
"version": "0.7.0",
"version": "0.7.1",
"capabilities": ["worker.hello"]
})),
error: None,
Expand Down
12 changes: 6 additions & 6 deletions deploy/README.en.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# FarHelm V0.7.0 deployment and lifecycle
# FarHelm V0.7.1 deployment and lifecycle

[简体中文](README.md) · [English](README.en.md)

Expand All @@ -21,7 +21,7 @@ sudo env \
./farhelm-hub install
```

V0.5.0 upgrades directly with `update`; SQLite session, pairing, project, and schedule tables are created idempotently on restart. Old TOTP and token fields remain for local rollback, but V0.7.0 does not require TOTP. A backup is still recommended before the first V0.3.0 cross-generation install:
V0.5.0 upgrades directly with `update`; SQLite session, pairing, project, and schedule tables are created idempotently on restart. Old TOTP and token fields remain for local rollback, but V0.7.1 does not require TOTP. A backup is still recommended before the first V0.3.0 cross-generation install:

```bash
sudo cp /var/lib/farhelm/farhelm.db /var/lib/farhelm/farhelm.db.v0.3.bak
Expand Down Expand Up @@ -86,8 +86,8 @@ chmod +x farhelm-agent
Agent downloads the independent Python 3.12/Codex runtime from the matching immutable Release and verifies its length and SHA-256. For an offline installation, transfer the matching runtime asset first and provide trusted verification metadata:

```bash
FARHELM_CODEX_RUNTIME_ARCHIVE="$PWD/farhelm-codex-runtime-0.7.0-linux-x86_64.tar.gz" \
FARHELM_CODEX_RUNTIME_SIZE="$(stat -c '%s' farhelm-codex-runtime-0.7.0-linux-x86_64.tar.gz)" \
FARHELM_CODEX_RUNTIME_ARCHIVE="$PWD/farhelm-codex-runtime-0.7.1-linux-x86_64.tar.gz" \
FARHELM_CODEX_RUNTIME_SIZE="$(stat -c '%s' farhelm-codex-runtime-0.7.1-linux-x86_64.tar.gz)" \
FARHELM_CODEX_RUNTIME_SHA256="copy-from-trusted-SHA256SUMS" \
./farhelm-agent install
```
Expand Down Expand Up @@ -150,9 +150,9 @@ farhelm-agent uninstall --keep-data

## Migrating from V0.2.0

Hosts already on `V0.3.0` through `V0.5.0` can run `farhelm-hub update` or `farhelm-agent update` directly. `V0.2.0` must first upgrade to `V0.3.0` to migrate the old layout, then upgrade to V0.7.0.
Hosts already on `V0.3.0` through `V0.5.0` can run `farhelm-hub update` or `farhelm-agent update` directly. `V0.2.0` must first upgrade to `V0.3.0` to migrate the old layout, then upgrade to V0.7.1.

Lowercase legacy `v0.1.0/v0.2.0` releases are outside the formal update sequence. Remove them with their matching old uninstaller before installing V0.7.0.
Lowercase legacy `v0.1.0/v0.2.0` releases are outside the formal update sequence. Remove them with their matching old uninstaller before installing V0.7.1.

## Security notes

Expand Down
Loading
Loading