Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
5860235
fix(refresh): retain original settlement recovery through maintenance
huangruiteng Nov 11, 2026
6a42539
fix(checkpoint): classify occupied destinations
loopx-agent Nov 11, 2026
dd9488e
Merge commit '97bd31386aaace034b22dbcc913da537dc98f22f' into codex/pr…
loopx-agent Nov 11, 2026
7d810cc
Recover admitted replan settlement when refresh omits the Turn
loopx-agent Nov 11, 2026
ee5226a
fix(dsh): read local scheduler cadence from scheduler hint detail
xiaobaifly7 Nov 11, 2026
465bcc1
Merge remote-tracking branch 'origin/main' into codex/pr8602-replan-r…
loopx-agent Nov 11, 2026
661ff60
fix(change-window): preserve LF in managed Git scripts (#8598)
xiaobaifly7 Nov 11, 2026
b092c70
fix(content-ops): generate runtime report and aggregate timestamps at…
xiaobaifly7 Nov 11, 2026
43b4699
Merge pull request #8609 from xiaobaifly7/fix/dsh-driver-scheduler-hi…
huangruiteng Nov 11, 2026
938673b
fix(chat): retain steward admission across executor publication
loopx-agent Nov 11, 2026
fb6ea1d
fix(chat): pin the original steward Session during scope refresh
loopx-agent Nov 11, 2026
43bd8bd
fix(lark): verify original message after steward executor adoption
loopx-agent Nov 11, 2026
be0e0df
Merge pull request #8614 from loopx-project/codex/steward-executor-ad…
loopx-agent Nov 11, 2026
f905c49
perf(chat): skip unrelated transcript reads in bound Session discovery
loopx-agent Nov 11, 2026
6ab22a7
agent_turn_recall: build the canonical hash through the digest envelo…
psychologister Nov 11, 2026
04958a8
Merge pull request #8608 from loopx-project/codex/checkpoint-destinat…
huangruiteng Nov 11, 2026
4dc8933
fix(accounts): detect stale adapter pins during provider readiness
loopx-agent Nov 11, 2026
3ac127c
fix(accounts): preserve readiness JSON for invalid saved policy
loopx-agent Nov 11, 2026
538e43c
Merge pull request #8619 from loopx-project/codex/native-account-adap…
loopx-agent Nov 11, 2026
b9111c3
Merge pull request #8617 from loopx-project/codex/steward-bound-sessi…
loopx-agent Nov 11, 2026
0f9efb3
Resume Chat history after an interrupted response body
loopx-agent Nov 11, 2026
9ce3a46
Merge pull request #8625 from loopx-project/codex/steward-history-str…
loopx-agent Nov 12, 2026
73f58b2
Wait for the original steward session before showing runtime setup
loopx-agent Nov 12, 2026
b7c9178
Merge pull request #8628 from loopx-project/codex/steward-pending-sou…
loopx-agent Nov 12, 2026
b8dd172
issue_fix: single-source the issue-close event-type set
psychologister Nov 12, 2026
a777437
Keep oversized diagnostics out of private chat backlog reads
loopx-agent Nov 12, 2026
a1e4384
Merge pull request #8639 from loopx-project/codex/compact-bot-recover…
huangruiteng Nov 12, 2026
c6f087d
Merge pull request #8636 from Luoluoluo0110/codex/issue-fix-close-eve…
huangruiteng Nov 12, 2026
4390d59
control_plane/projects: single-source the safe project-id grammar (#8…
Luoluoluo0110 Nov 12, 2026
86ba59d
Resume attached chat history after a dropped response body
loopx-agent Nov 12, 2026
fdda55c
state_projection: single-source the backlog-hygiene bullet grammar (#…
Luoluoluo0110 Nov 12, 2026
78aa3bf
Align public entry with goal-first experience (#8590)
cocolord Nov 12, 2026
b21e739
PR #8599 — qualify Windows file identity and bind backup evidence (#8…
xiaobaifly7 Nov 12, 2026
8bed1e5
Show the bound Yardstick reversal in replan opening (#8622)
huangruiteng Nov 12, 2026
9df7fb5
fix(scheduler): reject illegal calendar timestamps instead of rolling…
Duang777 Nov 12, 2026
b14ff7f
docs(blog): bind context comparisons to their measured baselines (#8616)
loopx-agent Nov 12, 2026
ec419e8
fix(research): scope the timeline to loaded history during recovery (…
loopx-agent Nov 12, 2026
271809a
fix(replan): recover an open Vision review on its admitted receipt (#…
loopx-agent Nov 12, 2026
63db1cd
fix(benchmark): seal resumed worker phase deadlines (#8634)
loopx-agent Nov 12, 2026
d9432f2
test(dsh): clean up temporary admission fixtures (#8640)
loopx-agent Nov 12, 2026
0abd0dc
feat(core): qualify standalone installation and fenced task recovery …
cocolord Nov 12, 2026
708c68e
perf(todos): limit replan reads to consumed views (#8624)
cocolord Nov 12, 2026
90b6291
fix(refresh): qualify explicit Turn recovery against original admission
loopx-agent Nov 12, 2026
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
33 changes: 33 additions & 0 deletions .github/workflows/package-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ on:
pull_request:
paths:
- ".github/workflows/package-smoke.yml"
- "packages/loopx-core/**"
- "loopx/**"
- "tests/test_core_distribution.py"
- "packages/loopx-repo-health/**"
- "packages/loopx-obelisk/**"
- "packages/loopx-codex-provider-routing/**"
Expand All @@ -14,6 +17,9 @@ on:
- main
paths:
- ".github/workflows/package-smoke.yml"
- "packages/loopx-core/**"
- "loopx/**"
- "tests/test_core_distribution.py"
- "packages/loopx-repo-health/**"
- "packages/loopx-obelisk/**"
- "packages/loopx-codex-provider-routing/**"
Expand All @@ -28,6 +34,33 @@ concurrency:
cancel-in-progress: true

jobs:
core:
name: loopx-core
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: "3.11"
- uses: actions/setup-node@v6
with:
node-version: "26.0.0"
- uses: astral-sh/setup-uv@v7
- name: Prepare source validation environment
run: uv sync --extra test
- name: Build and separately install the standalone core
run: |
uv run --extra test python packages/loopx-core/prepare.py --output "$RUNNER_TEMP/core-source"
uv build "$RUNNER_TEMP/core-source" --out-dir "$RUNNER_TEMP/core-dist"
uv venv "$RUNNER_TEMP/core-env" --python .venv/bin/python
uv pip install --python "$RUNNER_TEMP/core-env/bin/python" "$RUNNER_TEMP"/core-dist/loopx_core-*.whl
echo "LOOPX_CORE_TEST_PYTHON=$RUNNER_TEMP/core-env/bin/python" >> "$GITHUB_ENV"
- name: Qualify installed execution, recovery, replay and refusal
env:
LOOPX_USAGE_PING: "0"
run: uv run --extra test python -m pytest -q tests/test_core_distribution.py

codex-provider-routing:
name: loopx-codex-provider-routing
runs-on: ubuntu-latest
Expand Down
37 changes: 19 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@

**Start with one goal. Keep long-running agent work verifiable.**

Tell the Agent you already use what you want done. LoopX keeps the goal,
work, approvals and evidence durable behind the scenes, so the next session can
continue without making you learn LoopX internals first.<br>
<sub>Work with Codex, Claude Code, DeepSeek Harness and other runtimes. Advanced docs still expose the local-first semantic kernel, ownership model and research workbench.</sub>
Tell the Agent you already use what you want done. LoopX keeps the goal, work,
needed decisions and verifiable results durable behind the scenes, so the next
session can continue without making you learn LoopX internals first.<br>
<sub>Work with Codex, Claude Code, DeepSeek Harness and other runtimes. Advanced docs still expose the local-first architecture, ownership model and research workbench.</sub>

[![Release](https://img.shields.io/github/v/release/loopx-project/loopx?color=brightgreen)](https://github.com/loopx-project/loopx/releases/latest) [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE) [![TypeScript core](https://img.shields.io/badge/core-TypeScript-3178C6?logo=typescript&logoColor=white)](docs/architecture/rfcs/typescript-control-plane-migration-v0.md) [![Python](https://img.shields.io/badge/python-3.11%2B-blue.svg)](pyproject.toml)

[![Local first](https://img.shields.io/badge/control--plane-local--first-brightgreen.svg)](docs/public-private-boundary.md) [![Loop Agents](https://img.shields.io/badge/status-loop%20agents%20supported-brightgreen.svg)](docs/product/release-readiness.md) [![GitHub contributors](https://img.shields.io/github/contributors-anon/loopx-project/loopx?color=blue)](https://github.com/loopx-project/loopx/graphs/contributors) [![Discord](https://img.shields.io/badge/Discord-Join-5865F2?logo=discord&logoColor=white)](https://discord.gg/XmGgQyCFZd) [![Listed on dsh-plugin.org](https://dsh-plugin.org/badges/listed.svg)](https://dsh-plugin.org/plugins/loopx-project/loopx)
[![Local-first state](https://img.shields.io/badge/local--first-state-brightgreen.svg)](docs/public-private-boundary.md) [![Loop Agents](https://img.shields.io/badge/status-loop%20agents%20supported-brightgreen.svg)](docs/product/release-readiness.md) [![GitHub contributors](https://img.shields.io/github/contributors-anon/loopx-project/loopx?color=blue)](https://github.com/loopx-project/loopx/graphs/contributors) [![Discord](https://img.shields.io/badge/Discord-Join-5865F2?logo=discord&logoColor=white)](https://discord.gg/XmGgQyCFZd) [![Listed on dsh-plugin.org](https://dsh-plugin.org/badges/listed.svg)](https://dsh-plugin.org/plugins/loopx-project/loopx)

[Get started](#quick-start) · [Why LoopX](#why-loopx) · [See it in action](#see-loopx-in-action) · [Evidence](#evidence) · [Contribute](#contributing) · [Docs](https://loopx-project.github.io/loopx/docs/) · [简体中文](README.zh-CN.md)

Expand Down Expand Up @@ -41,9 +41,10 @@ then send one natural-language goal:
$loopx speed up this project's slowest workflow; compare approaches and verify the gains
```

LoopX turns that into visible tasks, gates and evidence as needed. Claude Code
uses `/loopx <complex task>`; see [host setup](#start-from-your-agent) for other
runtimes. Prefer a guided first run? [Choose a task below](#try-loopx).
LoopX turns that into a visible plan, decisions that need you, and verification
as needed. Claude Code uses `/loopx <complex task>`; see
[host setup](#start-from-your-agent) for other runtimes. Prefer a guided first
run? [Choose a task below](#try-loopx).

## 📰 News

Expand All @@ -54,10 +55,10 @@ runtimes. Prefer a guided first run? [Choose a task below](#try-loopx).
dates (UTC). -->

- **2026-10-25 · 🤝 [Long-horizon agent research: collaborators welcome](https://loopx-project.github.io/loopx/blog/edgebench-feedback-and-memory/)** — Exploring **breakthroughs in long-horizon outcomes under equal budgets**, through feedback, replanning and reproducible evaluation on EdgeBench and real-world tasks. Academic labs and industry teams are welcome to collaborate, comparing outcomes and total cost. [Discuss collaboration](https://github.com/loopx-project/loopx/discussions).
- **2026-10-20 · 🚀 [LoopX 2.0](https://github.com/loopx-project/loopx/releases/tag/v2.0.0)** — A shared TypeScript control plane and long-horizon research workbench. [Migration guide](docs/product/releases/v2.0.0.md) · [Engineering story](https://loopx-project.github.io/loopx/blog/typescript-control-plane-refactor/).
- **2026-10-20 · 🚀 [LoopX 2.0](https://github.com/loopx-project/loopx/releases/tag/v2.0.0)** — A shared TypeScript state layer and long-horizon research workbench. [Migration guide](docs/product/releases/v2.0.0.md) · [Engineering story](https://loopx-project.github.io/loopx/blog/typescript-control-plane-refactor/).
- **2026-10-14 · 🤖 [Agentic software](https://loopx-project.github.io/loopx/blog/semantic-software-development/)** — Connecting goals, evidence and human feedback to engineering delivery.
- **2026-10-02 · 🧠 [Long-horizon bootstrapping & RSI](https://loopx-project.github.io/loopx/blog/long-horizon-rsi/)** — Four months of developing LoopX with LoopX: public project history and a design perspective on system-level self-improvement.
- **2026-09-18 · 🧪 [LoopX × LHTB](https://loopx-project.github.io/loopx/benchmarks/lhtb/)** — A 46-task, same-model study provides empirical support for **trading more compute for better outcomes**: LoopX Heartbeat mean reward is 17.3% above Plain Codex and 10.6% above native Goal.
- **2026-09-18 · 🧪 [LoopX × LHTB](https://loopx-project.github.io/loopx/benchmarks/lhtb/)** — A 46-task, same-model study provides empirical support for **trading more compute for better outcomes**: LoopX 1.0.3 Heartbeat continued runs reach mean reward 17.3% above Plain Codex and 10.6% above native Goal.
- **2026-09-06 · 🎉 [LoopX 1.0](https://github.com/loopx-project/loopx/releases/tag/v1.0.0)** — A Personal Agent Workspace to follow agent work, act on decisions, and inspect delivered results across sessions. [Workspace guide](https://github.com/loopx-project/loopx/blob/v1.0.0/docs/guides/personal-workspace-user-guide.md).
- **2026-08-02 · ⏱️ [200+ hour trajectories shared on X](https://x.com/huangruiteng/status/2083904257494024425)** — Early owner-run examples spanning 200+ elapsed hours drew broad attention on X, showing work across execution, waits, human decisions and recovery. [Trajectories and limits](#open-source-issue-fix).

Expand All @@ -78,12 +79,12 @@ hand work to peers. A timer can keep a loop running without making it useful.

- **Continue without starting over.** Keep the Goal, accepted requirements,
decisions and evidence across sessions and restarts.
- **[Coordinate without losing ownership.](https://loopx-project.github.io/loopx/blog/agent-facing-kanban/#walkthrough)** Connect delegated work, dependent
- **[Coordinate without dropping context.](https://loopx-project.github.io/loopx/blog/agent-facing-kanban/#walkthrough)** Connect delegated work, dependent
artifacts, review and returned results while keeping responsibility explicit.
- **Judge progress by what changed.** Inspect deliverables, supporting and
refuting evidence, costs and the next decision—not just activity.

Your Agent runtime supplies the model and tools. LoopX’s semantic control plane
Your Agent runtime supplies the model and tools. LoopX’s durable state layer
preserves goals, acceptance, decisions, ownership and evidence across sessions
and runtimes for engineering, research and operations. Agents and people use
those facts to reassess what to do next. Human authorization still governs
Expand All @@ -97,25 +98,25 @@ protected actions.
| Manage personal projects, schedules and decisions in one place | [Personal Agent Workspace](#meet-the-personal-agent-workspace) |
| Let agents collaborate and deliver verifiable results | [Agent collaboration guide](docs/product/use-cases/cross-runtime/README.md) |

LoopX keeps the durable control state in one compact layer:
LoopX keeps the durable work state in one compact layer:

<picture>
<source media="(max-width: 640px)" srcset="docs/assets/harness-neutral.en.mobile.svg">
<img src="docs/assets/harness-neutral.en.svg" alt="Your harness runs the turn. LoopX carries the work." width="1200">
</picture>

Agent runtimes execute the work. LoopX governs the state that lets engineering,
Agent runtimes execute the work. LoopX keeps the state that lets engineering,
research, discovery, and operations loops continue across runs. It is not
another agent framework or a provider-specific orchestration runtime.

<details>
<summary>Advanced: how the control layer works and where it fits</summary>

<picture>
<source media="(max-width: 640px)" srcset="docs/assets/control-plane-board.en.mobile.svg">
<img src="docs/assets/control-plane-board.en.svg" alt="One turn is a governed state transition—not just a timer." width="1200">
<img src="docs/assets/control-plane-board.en.svg" alt="Advanced view of a governed state transition, not just a timer." width="1200">
</picture>

<details>
<summary>How the control plane works and where it fits</summary>

A useful mental model is an
**[agent-native Kanban for long-running work](docs/development/control-plane-course/00-concept-primer.md)**.
Cards carry identity, authority, evidence, and continuation. Moves are validated
Expand Down
13 changes: 11 additions & 2 deletions apps/presentation/dashboard/smoke/bound-conversation-smoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ try {
assert.equal(complete.messages.length, expectedRows);
const recoveredOffsets: number[] = [];
const failedOffsets = new Set<number>();
const interruptedHistoryBody = (status: number) => new Response(new ReadableStream({
start(controller) {
controller.error(new Error("Synthetic interrupted history response body"));
},
}), {status});
globalThis.fetch = async (input, init) => {
const response = await routedRead(input, init);
if (new URL(String(input), origin).pathname !== `/api/chat/sessions/${sessionId}`) return response;
Expand All @@ -140,7 +145,8 @@ try {
recoveredOffsets.push(offset);
if (offset > 0 && !failedOffsets.has(offset)) {
failedOffsets.add(offset);
throw new Error("Synthetic interrupted transport after a verified prefix");
if (offset === 200) throw new Error("Synthetic interrupted transport after a verified prefix");
return interruptedHistoryBody(response.status);
}
return response;
};
Expand All @@ -150,6 +156,8 @@ try {
assert.deepEqual(interrupted.snapshots, []);
const interruptedAgain = await fetchChatHistory(options, interrupted);
assert.deepEqual(interruptedAgain.messages, []);
assert.equal(interruptedAgain.pendingReads?.get(sessionId)?.messages.length, 400,
"A lost body after successful response headers keeps the verified prefix private");
const recovered = await fetchChatHistory(options, interruptedAgain);
assert.deepEqual(recovered.unavailableSessionIds, []);
assert.deepEqual(recovered.snapshots[0].messages, complete.messages);
Expand Down Expand Up @@ -188,11 +196,12 @@ try {
child.stdin.write("recover\n"); assert.equal((await next()).source_verified, true);
// Permanent denial, a changed source or malformed prefix cannot be retained
// as transport progress. The following read must validate page zero again.
for (const fault of ["denied", "source", "digest"]) {
for (const fault of ["denied", "denied-body", "source", "digest"]) {
globalThis.fetch = async (input, init) => {
const response = await routedRead(input, init);
if (!new URL(String(input), origin).searchParams.has("message_cursor")) return response;
if (fault === "denied") return new Response("{}", {status: 403});
if (fault === "denied-body") return interruptedHistoryBody(403);
const value = await response.json();
if (fault === "source") value.session.goal_id = "unrelated-goal";
else value.message_page.snapshot_sha256 = "0".repeat(64);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,25 @@ function next() {
if (queue.length) receive(queue.shift()); else waiting.push(receive);
});
}
async function openWhileCheckingOriginalConversation(ready, page) {
const isOriginalSource = url => url.pathname === "/api/chat/sessions"
&& url.searchParams.get("conversation_binding_id") === ready.bindingId;
const {promise: allowSourceRead, resolve: finishSourceRead} = Promise.withResolvers();
const holdOriginalSource = async route => {
await allowSourceRead;
await route.continue();
};
await page.route(isOriginalSource, holdOriginalSource);
try {
await page.goto(ready.origin + "/chat/", {waitUntil: "domcontentloaded"});
await page.getByText("正在核验原会话…", {exact: true}).waitFor({timeout: 4_000});
assert.equal(await page.getByTestId("runtime-readiness").count(), 0,
"Checking the original conversation must not show machine runtime setup");
} finally {
finishSourceRead();
await page.unrouteAll({behavior: "wait"});
}
}
async function checkReturnedFiles(ready, page, errors, writes) {
const snapshot = await (await page.request.get(`${ready.origin}/api/chat/sessions/${ready.sessionId}`)).json();
const answer = snapshot.messages.find(row => row.message_id === ready.fileMessageId);
Expand Down Expand Up @@ -113,7 +132,11 @@ try {
const snapshot = await (await page.request.get(`${ready.origin}/api/chat/sessions/${ready.sessionId}`)).json();
const source = snapshot.messages.find(row => row.role === "user" && row.turn_id);
assert.ok(source.text.startsWith("Referenced message ("), "Canonical model input stays unchanged");
await page.goto(ready.origin + "/chat/");
if (process.env.LOOPX_REPLY_BEFORE === "1") {
await page.goto(ready.origin + "/chat/");
} else {
await openWhileCheckingOriginalConversation(ready, page);
}
await page.getByRole("navigation", {name: "管家视图"}).getByRole("button", {name: /^(Chat|对话)$/}).click();
const body = "请比较这次修改,并保留原话题中的限制。Compare this change against the original constraints.";
const parent = "原要求:保留已有结果和失败记录。Original requirement: retain results and failed attempts.";
Expand Down
6 changes: 4 additions & 2 deletions apps/presentation/dashboard/src/data/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1297,8 +1297,10 @@ function sameHistoryRevision(current: ChatSessionSummary, read: ChatSessionSumma
}

function historyTransportUnavailable(error: unknown) {
return error instanceof ChatApiError && error.payload.error_code === "chat_api_unavailable"
&& (error.payload.http_status === undefined || Number(error.payload.http_status) >= 500);
if (!(error instanceof ChatApiError) || error.payload.error_code !== "chat_api_unavailable") return false;
const status = error.payload.http_status;
// A response body can be interrupted after successful history headers.
return status === undefined || status === 200 || Number(status) >= 500;
}

export type ChatHistory = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ type LiveObservation = {kind: "records"; revision: number; records: Message[]} |
{kind: "unavailable"; retry: boolean};

class ConversationRequestError extends Error {
constructor(message: string, readonly unsubmittedOperation?: string, readonly httpStatus?: number) {super(message);}
constructor(
message: string,
readonly unsubmittedOperation?: string,
readonly httpStatus?: number,
readonly interruptedSuccessfulResponse = false,
) {super(message);}
}

export async function request<T>(url: string, options?: RequestInit): Promise<T> {
Expand All @@ -30,11 +35,9 @@ export async function request<T>(url: string, options?: RequestInit): Promise<T>
try {
body = await response.text();
} catch {
// The headers arrived, so the status is already known even though the body
// did not. Drop the status here and a stalled 503 body looks like a plain
// transport failure while a truncated 403 body looks retryable; the caller
// must classify on this status, not on how the body read failed.
throw new ConversationRequestError("Conversation unavailable", undefined, response.status);
// Preserve rejected statuses even when their bodies drop. A successful
// response interrupted in transit is different from a complete invalid body.
throw new ConversationRequestError("Conversation unavailable", undefined, response.status, response.ok);
}
let value: Record<string, unknown> | null = null;
try {value = body ? JSON.parse(body) as Record<string, unknown> : null;} catch {value = null;}
Expand All @@ -51,7 +54,8 @@ function interruptedRead(failure: unknown): boolean {
}

export function retryableRead(failure: unknown): boolean {
return interruptedRead(failure) || (failure instanceof ConversationRequestError && (failure.httpStatus ?? 0) >= 500);
return interruptedRead(failure) || (failure instanceof ConversationRequestError &&
(failure.interruptedSuccessfulResponse || (failure.httpStatus ?? 0) >= 500));
}

export function readFailureMessage(failure: unknown, zh: boolean): string {
Expand Down
Loading
Loading