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 .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "lossless-openclaw-orchestrator",
"version": "1.6.0",
"version": "1.6.1",
"description": "Index, search, and prepare local Codex sessions with LCO, including one-shot thread title aliases for easier recall.",
"author": {
"name": "100yenadmin",
Expand Down
13 changes: 6 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,10 +171,9 @@ npm install -g lossless-codex-orchestrator@beta

Package channels:

- Published stable at this candidate snapshot: `1.5.0` on npm `latest` and
GitHub Release `v1.5.0`.
- This source tree is a `1.6.0` release candidate. It is not proof that `1.6.0`
has been published.
- npm `latest` and GitHub Releases are the stable publication authorities.
- A version in the source tree is a candidate until those publication
authorities show the same version; merged source alone is not release proof.
- `latest` is the stable public channel.
- `beta` is the active prerelease train.
- `next` is reserved for release candidates.
Expand Down Expand Up @@ -387,9 +386,9 @@ environment-variable value.
Before a release, verify the candidate without changing a Hermes profile:

```bash
lco qa-lab cli-mcp-smoke --evidence-dir /tmp/lco-package-smoke --package-version 1.6.0 --candidate-sha <candidate-sha> --cli-bin <candidate-prefix>/node_modules/.bin/lco --mcp-bin <candidate-prefix>/node_modules/.bin/lco-mcp-server --strict
lco hermes smoke --evidence-dir /tmp/lco-hermes-smoke --package-version 1.6.0 --candidate-sha <candidate-sha> --cli-bin <candidate-prefix>/node_modules/.bin/lco --mcp-bin <candidate-prefix>/node_modules/.bin/lco-mcp-server --strict
lco release hermes-readiness --evidence-dir /tmp/lco-hermes-readiness --package-version 1.6.0 --candidate-sha <candidate-sha> --hermes-smoke /tmp/lco-hermes-smoke/hermes-smoke.json --package-smoke /tmp/lco-package-smoke/cli-mcp-product-smoke.json --strict
lco qa-lab cli-mcp-smoke --evidence-dir /tmp/lco-package-smoke --package-version <package-version> --candidate-sha <candidate-sha> --cli-bin <candidate-prefix>/node_modules/.bin/lco --mcp-bin <candidate-prefix>/node_modules/.bin/lco-mcp-server --strict
lco hermes smoke --evidence-dir /tmp/lco-hermes-smoke --package-version <package-version> --candidate-sha <candidate-sha> --cli-bin <candidate-prefix>/node_modules/.bin/lco --mcp-bin <candidate-prefix>/node_modules/.bin/lco-mcp-server --strict
lco release hermes-readiness --evidence-dir /tmp/lco-hermes-readiness --package-version <package-version> --candidate-sha <candidate-sha> --hermes-smoke /tmp/lco-hermes-smoke/hermes-smoke.json --package-smoke /tmp/lco-package-smoke/cli-mcp-product-smoke.json --strict
```

The reports are public-safe candidate evidence. They do not prove publication,
Expand Down
11 changes: 6 additions & 5 deletions VISION.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ Hermes or generic MCP until each is independently proven.

## Current Release Roadmap: 1.6 Control Plane

Published stable at this candidate snapshot: `1.5.0` on npm `latest` and GitHub
Release `v1.5.0`. Current source candidate: `1.6.0`. Source metadata is not
published release proof; npm dist-tags and GitHub Releases remain the
publication authorities. The Control Plane candidate keeps
The published stable release is whatever npm `latest` and GitHub Releases show
for the same version. A version in the source tree remains a candidate until
those publication authorities agree; source metadata is not published release
proof. The Control Plane candidate keeps
`lossless-codex-orchestrator` as the canonical npm package, keeps
`lossless-openclaw-orchestrator` as a maintained compatibility package, adds
bounded session-diff cursors and review-then-drive dry-runs, verifies the Codex
Expand All @@ -63,7 +63,8 @@ prepared cards with peer diagnostics, validates Claude as a second target
family without claiming Claude live control, and stabilizes the Hermes stdio
MCP path.

The stable 1.0.0 through `1.5.0` packages are completed release lines. M11 proved
The stable packages through the latest published release are completed release
lines. M11 proved
the first scoped stable GA-assurance lane, and M12 built the
release-assurance foundation: fresh npm install checks, OpenClaw gateway
dogfood, QA Lab coverage, release aggregation, public-safe evidence rules, and
Expand Down
2 changes: 1 addition & 1 deletion docs/BETA_RELEASE_RUNBOOK.md
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@ Record `npm dist-tag ls lossless-codex-orchestrator` in the release evidence
after every npm publication. Stable releases publish with
`npm publish --tag latest`; public betas publish with `npm publish --tag beta`;
release candidates publish with `npm publish --tag next`. The stable channel
target for this package version is `1.6.0`; npm `latest` must move only after
target for this package version is `1.6.1`; npm `latest` must move only after
the separate stable-promotion gate proves the exact candidate. Keep beta and
other prereleases on prerelease tags. Do not publish a fake stable package just
to move a dist-tag. Release candidates must publish with `npm publish --tag next`;
Expand Down
4 changes: 2 additions & 2 deletions docs/CLAIM_AUDIT.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Public Claim Audit

## Allowed 1.6.0 Stable Claim
## Allowed 1.6.1 Stable Claim

This wording becomes allowed only after npm, Git tag, and GitHub Release
publication truth agree for the exact candidate. The current source version
Expand Down Expand Up @@ -102,7 +102,7 @@ blockers instead of allowing a working-app claim.

Install stable releases through the `latest` dist-tag, public betas through the
`beta` dist-tag, and release candidates through `next`. The stable channel
target for this package version is `1.6.0`; npm `latest` must move only after
target for this package version is `1.6.1`; npm `latest` must move only after
the separate stable-promotion gate proves the exact candidate. Keep beta and
other prereleases on prerelease tags. Do not publish a fake stable package just
to move a dist-tag.
Expand Down
6 changes: 3 additions & 3 deletions docs/SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,23 +360,23 @@ Maintainers can prove a candidate without writing a Hermes profile:
```bash
lco qa-lab cli-mcp-smoke \
--evidence-dir /tmp/lco-package-smoke \
--package-version 1.6.0 \
--package-version <package-version> \
--candidate-sha <candidate-sha> \
--cli-bin <candidate-prefix>/node_modules/.bin/lco \
--mcp-bin <candidate-prefix>/node_modules/.bin/lco-mcp-server \
--strict

lco hermes smoke \
--evidence-dir /tmp/lco-hermes-smoke \
--package-version 1.6.0 \
--package-version <package-version> \
--candidate-sha <candidate-sha> \
--cli-bin <candidate-prefix>/node_modules/.bin/lco \
--mcp-bin <candidate-prefix>/node_modules/.bin/lco-mcp-server \
--strict

lco release hermes-readiness \
--evidence-dir /tmp/lco-hermes-readiness \
--package-version 1.6.0 \
--package-version <package-version> \
--candidate-sha <candidate-sha> \
--hermes-smoke /tmp/lco-hermes-smoke/hermes-smoke.json \
--package-smoke /tmp/lco-package-smoke/cli-mcp-product-smoke.json \
Expand Down
3 changes: 2 additions & 1 deletion docs/releases/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ One line per released version, newest first. Each line links to the full
release notes for that version. Draft notes stay outside the released list
until npm, tag, and GitHub Release truth agree.

Draft candidate notes: [1.6.0](RELEASE_NOTES_1.6.0.md)
Draft candidate notes: [1.6.1](RELEASE_NOTES_1.6.1.md)

- [1.6.0](RELEASE_NOTES_1.6.0.md)
Comment thread
100yenadmin marked this conversation as resolved.
- [1.5.0](RELEASE_NOTES_1.5.0.md)
- [1.4.5](RELEASE_NOTES_1.4.5.md)
- [1.4.4](RELEASE_NOTES_1.4.4.md)
Expand Down
36 changes: 36 additions & 0 deletions docs/releases/RELEASE_NOTES_1.6.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Release Notes 1.6.1

`1.6.1` is a focused Hermes live-control hotfix for local Codex sessions.

## Highlights

- Canonical MCP control tools now accept the public-safe
`codex_thread:<thread-id>` references returned by `lco_recent_sessions`.
- LCO removes the supported `codex_thread:` prefix before approval hashing and
Codex app-server requests, so a dry run made with the public reference also
matches a live call made with the raw Codex thread id.
- Raw Codex thread ids continue to pass through unchanged.

## Behavior Preserved

The hotfix preserves the matching `approval_audit_id` requirement and the
fixed never-approve, read-only, no-network Codex posture. It does not enable
unattended control, change a Hermes profile, expose raw transcripts, or widen
the stdio MCP integration into a native Hermes adapter.

## Upgrade

```bash
npm install -g lossless-codex-orchestrator@latest
lco doctor
```

## Validation

The candidate is covered by MCP control regression tests for public and raw
thread references, including approval-audit matching and transport inputs.

## Links

- Thread-reference live-control bug: #795
- LCO 1.6 Control Plane tracker: #673
2 changes: 1 addition & 1 deletion openclaw.plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"id": "lossless-openclaw-orchestrator",
"name": "Lossless OpenClaw Orchestrator",
"description": "Index, search, and prepare local Codex sessions for OpenClaw with approval-gated dry-runs and optional Codex controls.",
"version": "1.6.0",
"version": "1.6.1",
"kind": "tool",
"tools": {
"prefix": "lco_"
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "lossless-openclaw-orchestrator",
"version": "1.6.0",
"version": "1.6.1",
Comment thread
100yenadmin marked this conversation as resolved.
"description": "Index, search, and prepare local Codex sessions for Hermes and MCP clients, with OpenClaw compatibility, approval-gated dry-runs, and optional Codex controls.",
"type": "module",
"license": "PolyForm-Noncommercial-1.0.0",
Expand Down
14 changes: 11 additions & 3 deletions packages/mcp-server/src/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1686,7 +1686,7 @@ function dispatchControl(control: ReturnType<typeof createCodexControl>, input:
const action = requiredString(input.action, "action");
if (action === "start") return control.startThread({ dryRun });
const common = {
threadId: requiredString(input.thread_id, "thread_id"),
threadId: requiredControlThreadId(input.thread_id),
message: action === "send" || action === "steer"
? requiredString(input.message, "message")
: optionalString(input.message) ?? "continue",
Expand Down Expand Up @@ -1726,7 +1726,7 @@ function startControlSchema(): Record<string, unknown> {

function controlInput(input: Record<string, unknown>, message = false, expectedTurn = false) {
return {
threadId: requiredString(input.thread_id, "thread_id"),
threadId: requiredControlThreadId(input.thread_id),
...(message ? { message: requiredString(input.message, "message") } : {}),
...(expectedTurn ? { expectedTurnId: optionalString(input.expected_turn_id), turnWaitMs: optionalNumber(input.turn_wait_ms) } : {}),
dryRun: input.dry_run !== false,
Expand All @@ -1743,7 +1743,7 @@ function startControlInput(input: Record<string, unknown>) {

function messageControlInput(input: Record<string, unknown>, expectedTurn = false, turnWait = false) {
return {
threadId: requiredString(input.thread_id, "thread_id"),
threadId: requiredControlThreadId(input.thread_id),
message: requiredString(input.message, "message"),
...(expectedTurn ? { expectedTurnId: requiredString(input.expected_turn_id, "expected_turn_id") } : {}),
...(turnWait ? { turnWaitMs: optionalNumber(input.turn_wait_ms) } : {}),
Expand All @@ -1752,6 +1752,14 @@ function messageControlInput(input: Record<string, unknown>, expectedTurn = fals
};
}

function requiredControlThreadId(value: unknown): string {
Comment thread
100yenadmin marked this conversation as resolved.
const threadId = requiredString(value, "thread_id");
return requiredString(
Comment thread
100yenadmin marked this conversation as resolved.
threadId.startsWith("codex_thread:") ? threadId.slice("codex_thread:".length) : threadId,
"thread_id"
);
}

async function snakeCaseControlResult(value: Promise<any>) {
const result = await value;
const approvalPacket = result.live === false ? approvalPacketFromControlResult(result) : undefined;
Expand Down
2 changes: 1 addition & 1 deletion packages/openclaw-plugin/openclaw.plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"id": "lossless-openclaw-orchestrator",
"name": "Lossless OpenClaw Orchestrator",
"description": "Index, search, and prepare local Codex sessions for OpenClaw with approval-gated dry-runs and optional Codex controls.",
"version": "1.6.0",
"version": "1.6.1",
"kind": "tool",
"tools": {
"prefix": "lco_"
Expand Down
71 changes: 71 additions & 0 deletions tests/control-and-mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,77 @@ test("Codex control requires dry-run audit before live message, steer, resume, o
}
});

test("MCP Codex control normalizes public codex_thread refs before approval hashing and transport", async () => {
const root = mkdtempSync(join(tmpdir(), "lco-control-thread-ref-"));
const db = createDatabase(join(root, "orchestrator.sqlite"));
const audit = createAuditStore(join(root, "audit.jsonl"));
Comment thread
100yenadmin marked this conversation as resolved.
const sequenceCalls: CodexControlStep[][] = [];
const sequenceOptions: CodexControlSequenceOptions[] = [];
const tools = createLooTools({
db,
audit,
includeAliases: false,
codexClient: {
request: async () => ({ ok: true }),
requestSequenceUntilTurnResolved: async (steps, options) => {
sequenceCalls.push(steps);
sequenceOptions.push(options);
return {
responses: [
{ ok: true, result: { thread: { id: "thr_public_ref", loaded: true } } },
{ ok: true, result: { turn: { id: "turn_public_ref", status: "completed" } } }
],
turn: {
id: "turn_public_ref",
status: "completed",
completed: true,
notificationMethods: ["turn/completed"],
approvalRequestCount: 0,
serverRequestCount: 0
}
};
}
}
});

try {
const dryRunTool = tools.find((tool) => tool.name === "lco_codex_control_dry_run");
const sendTool = tools.find((tool) => tool.name === "lco_codex_send_message");
assert.ok(dryRunTool);
assert.ok(sendTool);

const dryRun = await dryRunTool.execute({
action: "send",
thread_id: "codex_thread:thr_public_ref",
message: "continue"
}) as { approval_audit_id: string; thread_id: string };
assert.equal(dryRun.thread_id, "thr_public_ref");

const live = await sendTool.execute({
thread_id: "thr_public_ref",
message: "continue",
dry_run: false,
approval_audit_id: dryRun.approval_audit_id,
turn_wait_ms: 25
}) as { live: boolean; thread_id: string; status: string };

assert.equal(live.live, true);
assert.equal(live.thread_id, "thr_public_ref");
assert.equal(live.status, "completed");
assert.deepEqual(sequenceCalls[0]?.map((step) => ({
Comment thread
100yenadmin marked this conversation as resolved.
method: step.method,
threadId: step.params.threadId
})), [
{ method: "thread/resume", threadId: "thr_public_ref" },
{ method: "turn/start", threadId: "thr_public_ref" }
]);
assert.equal(sequenceOptions[0]?.threadId, "thr_public_ref");
} finally {
db.close();
rmSync(root, { recursive: true, force: true });
}
});

test("Codex control redacts live transport responses before returning them through tools", async () => {
const root = mkdtempSync(join(tmpdir(), "loo-control-redaction-"));
const audit = createAuditStore(join(root, "audit.jsonl"));
Expand Down
14 changes: 9 additions & 5 deletions tests/release-claim-audit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,18 +187,22 @@ test("npm dist-tag policy is explicit for stable, beta, and rc channels", () =>
assert.match(runbook, /npm `latest` must move only after/i);
});

test("README and VISION distinguish the published stable package from the active release candidate", () => {
test("README and VISION distinguish publication authority from source candidates", () => {
const readme = read("README.md");
const vision = read("VISION.md");

assert.match(readme, /Published stable at this candidate snapshot:\s+`1\.5\.0`[\s\S]{0,120}npm `latest`/i);
assert.match(readme, /source tree is a `1\.6\.0` release candidate[\s\S]{0,120}not proof[\s\S]{0,80}published/i);
assert.match(readme, /npm `latest` and GitHub Releases are the stable publication authorities/i);
assert.match(readme, /version in the source tree is a candidate[\s\S]{0,160}merged source alone is not release proof/i);
assert.match(readme, /npm install -g lossless-codex-orchestrator@latest/i);
assert.match(readme, /`latest` is the stable public channel/i);
assert.match(readme, /Stable today:[\s\S]{0,240}prepared cards/i);
assert.match(readme, /Since 1\.2\.x[\s\S]{0,120}1\.2 prepared-state and summary-leaves lane/i);
assert.match(vision, /Published stable at this candidate snapshot:\s+`1\.5\.0`[\s\S]{0,160}Current source candidate:\s+`1\.6\.0`/i);
assert.match(vision, /Source metadata is not\s+published release proof/i);
assert.match(vision, /published stable release is whatever npm `latest` and GitHub Releases show[\s\S]{0,180}same version/i);
assert.match(vision, /version in the source tree remains a candidate[\s\S]{0,160}publication authorities agree/i);
assert.match(vision, /source metadata is not published release/i);
assert.match(vision, /published release\s+proof/i);
assert.doesNotMatch(readme, /Published stable at this candidate snapshot|Current source candidate/i);
assert.doesNotMatch(vision, /Published stable at this candidate snapshot|Current source candidate/i);
assert.doesNotMatch(readme, /`1\.3\.[0-9]+` release candidate/i);
assert.doesNotMatch(vision, /`1\.3\.[0-9]+` release candidate/i);
assert.doesNotMatch(readme, /release candidate carries post-sprint feature hardening/i);
Expand Down