lossless-openclaw-orchestrator 1.1.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,7 +14,7 @@ safe action without rereading raw transcripts.
14
14
  [![License](https://img.shields.io/badge/license-PolyForm%20Noncommercial-blue)](LICENSE)
15
15
  [![PRs welcome](https://img.shields.io/badge/PRs-welcome-brightgreen)](CONTRIBUTING.md)
16
16
 
17
- [Setup](docs/SETUP.md) · [Contributing](CONTRIBUTING.md) · [Agent Instructions](AGENTS.md) · [Agent Skill](skills/lossless-openclaw-orchestrator/SKILL.md) · [OpenClaw Plugin](docs/OPENCLAW_PLUGIN.md) · [Security](SECURITY.md) · [Code of Conduct](CODE_OF_CONDUCT.md) · [Vision](VISION.md) · [Privacy](docs/PRIVACY.md) · [Claude Boundary](docs/CLAUDE_ADAPTER_BOUNDARY.md) · [Claim Audit](docs/CLAIM_AUDIT.md) · [Release Notes](docs/RELEASE_NOTES_1.1.0.md) · [1.0 Notes](docs/RELEASE_NOTES_1.0.0.md) · [License](LICENSE)
17
+ [Setup](docs/SETUP.md) · [Contributing](CONTRIBUTING.md) · [Agent Instructions](AGENTS.md) · [Agent Skill](skills/lossless-openclaw-orchestrator/SKILL.md) · [OpenClaw Plugin](docs/OPENCLAW_PLUGIN.md) · [Security](SECURITY.md) · [Code of Conduct](CODE_OF_CONDUCT.md) · [Vision](VISION.md) · [Privacy](docs/PRIVACY.md) · [Claude Boundary](docs/CLAUDE_ADAPTER_BOUNDARY.md) · [Claim Audit](docs/CLAIM_AUDIT.md) · [Release Notes](docs/RELEASE_NOTES_1.1.1.md) · [1.0 Notes](docs/RELEASE_NOTES_1.0.0.md) · [License](LICENSE)
18
18
 
19
19
  ## Why It Matters
20
20
 
package/VISION.md CHANGED
@@ -10,9 +10,9 @@ The stable product should feel like a local orchestration cockpit: OpenClaw can
10
10
 
11
11
  ## Current Milestone: 1.1 Desktop Collaboration Cockpit
12
12
 
13
- The current release lane is [#387](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/387), which promotes the proven post-1.0 Codex collaboration cockpit and OpenClaw gateway work into stable `1.1.0` on npm `latest` after full check, smoke, scenario, scorecard, release-status, finalization, and post-publish readiness gates pass.
13
+ The current release lane is [#393](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/393), which publishes stable `1.1.1` on npm `latest` after the same full check, smoke, scenario, scorecard, release-status, finalization, and post-publish readiness gates used for `1.1.0`. This patch carries the same stable claim scope while adding the #380 same-connection live-control safety fix, the #385 Desktop-first agent skill wording fix, and the #382 Desktop-first daily orchestration scenario contract.
14
14
 
15
- The stable 1.0.0 package has shipped on npm `latest` and the GitHub Release is published. The post-GA Desktop claim-validation lane [#306](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/306) records that Desktop-visible classification and fallback readiness/status are proven, while actual Codex GUI mutation remains excluded. Desktop parity [#307](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/307) added the coherence classifier; desktop fallback [#308](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/308) added the CUA-first and Peekaboo-secondary readiness report. The active product tracker is the 1.1 collaboration cockpit [#309](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/309), with the operating loop [#16](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/16).
15
+ The stable 1.0.0 and 1.1.0 packages have shipped on npm `latest` and their GitHub Releases are published. The post-GA Desktop claim-validation lane [#306](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/306) records that Desktop-visible classification and fallback readiness/status are proven, while actual Codex GUI mutation remains excluded. Desktop parity [#307](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/307) added the coherence classifier; desktop fallback [#308](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/308) added the CUA-first and Peekaboo-secondary readiness report. The active product tracker is the 1.1 collaboration cockpit [#309](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/309), with the operating loop [#16](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/16).
16
16
 
17
17
  The Codex Autonomy Cockpit [#254](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/254) and Eva Operating Picture [#255](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/255) P0 lanes are completed beta foundation, not the current active child-work list. Completed P0 children include shared contracts [#256](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/256), source authority [#258](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/258), watcher/resume requests [#259](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/259), visible Codex map joins [#260](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/260), deterministic GitHub operating inputs [#264](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/264)/[#265](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/265), current-lane source balancing [#269](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/269), GitHub check-state fidelity [#270](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/270), cockpit card cleanup [#271](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/271), and end-to-end Eva cockpit dogfood [#272](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/272). The sprint brief remains the historical handoff for that P0 work: [docs/sprints/brief-lco-codex-autonomy-cockpit-sprint-2026-07-01.md](docs/sprints/brief-lco-codex-autonomy-cockpit-sprint-2026-07-01.md).
18
18
 
@@ -20,7 +20,7 @@ The core Codex recall, M9 handoff paths, Codex Autonomy Cockpit, Eva Operating P
20
20
 
21
21
  The current target is:
22
22
 
23
- - Promote npm `latest` from stable `1.0.0` to stable `1.1.0` only after #387 passes full check, smoke, scenario, scorecard, release-status, finalization, and post-publish readiness gates; keep `beta` aligned with beta trains and use `next` for future release candidates.
23
+ - Promote npm `latest` from stable `1.1.0` to stable `1.1.1` only after #393 passes full check, smoke, scenario, scorecard, release-status, finalization, and post-publish readiness gates; keep `beta` aligned with beta trains and use `next` for future release candidates.
24
24
  - Keep README, VISION, release notes, and scorecards current so completed release gates are recorded as completed proof, not active work.
25
25
  - Treat #307/#308 as completed proof for Desktop-visible classification and fallback readiness/status, not as proof of Codex GUI mutation, prompt typing, clicking, refresh/restart automation, or unattended visible collaboration.
26
26
  - Use #309 to design the next collaboration cockpit layer from structured public-safe cards, explicit action-bound proof gates, and clear excluded-claim wording.
@@ -209,6 +209,26 @@ export function createCodexMcpStdioClient(options = {}) {
209
209
  finally {
210
210
  await client.close();
211
211
  }
212
+ },
213
+ async requestSequence(steps) {
214
+ const surface = options.surface ?? "control";
215
+ for (const step of steps)
216
+ assertCodexMethodAllowed(step.method, surface);
217
+ const client = new CodexJsonRpcClient(() => new LineProcessTransport(options.command ?? "codex", options.args ?? ["app-server", "--stdio"], options.timeoutMs), { timeoutMs: options.timeoutMs, surface });
218
+ await client.connect();
219
+ try {
220
+ const responses = [];
221
+ for (const step of steps) {
222
+ const response = await client.request(step.method, step.params);
223
+ responses.push(response);
224
+ if (!response.ok)
225
+ break;
226
+ }
227
+ return responses;
228
+ }
229
+ finally {
230
+ await client.close();
231
+ }
212
232
  }
213
233
  };
214
234
  }
@@ -74,7 +74,19 @@ export function createAuditStore(path) {
74
74
  export function createCodexControl(options) {
75
75
  const execute = async (spec) => {
76
76
  assertCodexMethodAllowed(spec.method, "control");
77
- const paramsHash = options.audit.fingerprintValue({ action: spec.action, method: spec.method, threadId: spec.threadId, params: spec.params });
77
+ const steps = spec.steps?.length ? spec.steps : [{ method: spec.method, params: spec.params }];
78
+ const methodSequence = steps.map((step) => step.method);
79
+ for (const step of steps)
80
+ assertCodexMethodAllowed(step.method, "control");
81
+ const connectionScope = steps.length > 1 ? "same_connection_sequence" : "single_request";
82
+ const paramsHash = options.audit.fingerprintValue({
83
+ action: spec.action,
84
+ method: spec.method,
85
+ methodSequence,
86
+ threadId: spec.threadId,
87
+ params: spec.params,
88
+ steps
89
+ });
78
90
  const messageHash = spec.message === undefined ? undefined : options.audit.fingerprintText(spec.message);
79
91
  if (spec.dryRun !== false) {
80
92
  const record = options.audit.append({
@@ -84,7 +96,18 @@ export function createCodexControl(options) {
84
96
  messageHash,
85
97
  live: false
86
98
  });
87
- return { action: spec.action, threadId: spec.threadId, live: false, approvalAuditId: record.id, paramsHash, messageHash, method: spec.method };
99
+ return {
100
+ action: spec.action,
101
+ threadId: spec.threadId,
102
+ live: false,
103
+ approvalAuditId: record.id,
104
+ paramsHash,
105
+ messageHash,
106
+ method: spec.method,
107
+ methodSequence,
108
+ connectionScope,
109
+ loadedThreadReusable: spec.loadedThreadReusable
110
+ };
88
111
  }
89
112
  if (!spec.approvalAuditId) {
90
113
  throw new Error("approval_audit_id is required for live Codex control actions");
@@ -103,12 +126,28 @@ export function createCodexControl(options) {
103
126
  if (!Number.isFinite(dryRunCreatedAtMs) || dryRunCreatedAtMs + CODEX_CONTROL_DRY_RUN_TTL_MS <= Date.now()) {
104
127
  throw new Error("approval_audit_id dry-run record expired");
105
128
  }
106
- const response = await options.client.request(spec.method, spec.params);
129
+ const response = steps.length > 1
130
+ ? await requestCodexControlSequence(options.client, steps)
131
+ : await options.client.request(spec.method, spec.params);
107
132
  const liveRecord = options.audit.append({ action: spec.action, target: spec.threadId, paramsHash, messageHash, live: true });
108
- return { action: spec.action, threadId: spec.threadId, live: true, approvalAuditId: liveRecord.id, paramsHash, messageHash, method: spec.method, response: redactValue(response) };
133
+ return {
134
+ action: spec.action,
135
+ threadId: spec.threadId,
136
+ live: true,
137
+ approvalAuditId: liveRecord.id,
138
+ paramsHash,
139
+ messageHash,
140
+ method: spec.method,
141
+ methodSequence,
142
+ connectionScope,
143
+ loadedThreadReusable: spec.loadedThreadReusable,
144
+ response: redactValue(response)
145
+ };
109
146
  };
110
147
  return {
111
148
  sendMessage(input) {
149
+ const resumeParams = { threadId: input.threadId, excludeTurns: true };
150
+ const turnStartParams = { threadId: input.threadId, input: [{ type: "text", text: input.message }] };
112
151
  return execute({
113
152
  action: "codex_send_message",
114
153
  method: "turn/start",
@@ -116,7 +155,12 @@ export function createCodexControl(options) {
116
155
  message: input.message,
117
156
  dryRun: input.dryRun,
118
157
  approvalAuditId: input.approvalAuditId,
119
- params: { threadId: input.threadId, input: [{ type: "text", text: input.message }] }
158
+ params: turnStartParams,
159
+ steps: [
160
+ { method: "thread/resume", params: resumeParams },
161
+ { method: "turn/start", params: turnStartParams }
162
+ ],
163
+ loadedThreadReusable: true
120
164
  });
121
165
  },
122
166
  resumeThread(input) {
@@ -126,10 +170,13 @@ export function createCodexControl(options) {
126
170
  threadId: input.threadId,
127
171
  dryRun: input.dryRun,
128
172
  approvalAuditId: input.approvalAuditId,
129
- params: { threadId: input.threadId, excludeTurns: true }
173
+ params: { threadId: input.threadId, excludeTurns: true },
174
+ loadedThreadReusable: false
130
175
  });
131
176
  },
132
177
  steerThread(input) {
178
+ if (!input.expectedTurnId)
179
+ throw new Error("expected_turn_id is required for steer actions");
133
180
  return execute({
134
181
  action: "codex_steer_thread",
135
182
  method: "turn/steer",
@@ -137,7 +184,7 @@ export function createCodexControl(options) {
137
184
  message: input.message,
138
185
  dryRun: input.dryRun,
139
186
  approvalAuditId: input.approvalAuditId,
140
- params: { threadId: input.threadId, input: [{ type: "text", text: input.message }] }
187
+ params: { threadId: input.threadId, expectedTurnId: input.expectedTurnId, input: [{ type: "text", text: input.message }] }
141
188
  });
142
189
  },
143
190
  interruptThread(input) {
@@ -152,6 +199,21 @@ export function createCodexControl(options) {
152
199
  }
153
200
  };
154
201
  }
202
+ async function requestCodexControlSequence(client, steps) {
203
+ if (!client.requestSequence) {
204
+ throw new Error("same-connection control sequence is required for this Codex control action");
205
+ }
206
+ const responses = await client.requestSequence(steps);
207
+ for (let index = 0; index < responses.length; index += 1) {
208
+ if (asRecord(responses[index])?.ok === false) {
209
+ throw new Error(`Codex control sequence step failed: ${steps[index]?.method ?? "unknown"}`);
210
+ }
211
+ }
212
+ if (responses.length !== steps.length) {
213
+ throw new Error(`Codex control sequence returned ${responses.length} response(s) for ${steps.length} step(s)`);
214
+ }
215
+ return responses.at(-1) ?? { ok: true };
216
+ }
155
217
  export async function createCodexAppServerStatusReport(options) {
156
218
  const transport = options.transport ?? codexTransportStatus({ command: options.command ?? process.env.LOO_CODEX_BIN ?? "codex" });
157
219
  const remoteControl = await codexReadRequest(options.client, "remoteControl/status/read", {});
@@ -56,7 +56,23 @@ export async function runLiveControlSmoke(options) {
56
56
  const target = options.threadId
57
57
  ? { threadId: options.threadId, source: "provided_thread" }
58
58
  : { threadId: await startEphemeralThread(options.client, options.cwd), source: "ephemeral_thread_start" };
59
- const control = createCodexControl({ audit: options.audit, client: { request: (method, params) => options.client.request(method, params) } });
59
+ const control = createCodexControl({
60
+ audit: options.audit,
61
+ client: {
62
+ request: (method, params) => options.client.request(method, params),
63
+ requestSequence: async (steps) => {
64
+ const responses = [];
65
+ for (const step of steps) {
66
+ const response = await options.client.request(step.method, step.params);
67
+ responses.push(response);
68
+ if (isFailedCodexResponse(response)) {
69
+ throw new Error(`Codex control sequence step failed: ${step.method}`);
70
+ }
71
+ }
72
+ return responses;
73
+ }
74
+ }
75
+ });
60
76
  const dryRun = await control.sendMessage({ threadId: target.threadId, message, dryRun: true });
61
77
  if (!dryRun.messageHash)
62
78
  throw new Error("dry-run did not produce a message hash");
@@ -151,6 +167,9 @@ function extractTurnId(response) {
151
167
  const turn = objectField(result.turn);
152
168
  return stringField(turn, "id");
153
169
  }
170
+ function isFailedCodexResponse(value) {
171
+ return Boolean(value && typeof value === "object" && !Array.isArray(value) && value.ok === false);
172
+ }
154
173
  function unwrapResult(value) {
155
174
  if (!value || typeof value !== "object" || Array.isArray(value))
156
175
  return {};
@@ -492,11 +492,12 @@ export function createLooTools(options) {
492
492
  tool("loo_codex_control_dry_run", "Create a dry-run audit id for a Codex control action.", {
493
493
  action: { type: "string", enum: ["send", "resume", "steer", "interrupt"] },
494
494
  thread_id: { type: "string" },
495
- message: { type: "string" }
495
+ message: { type: "string" },
496
+ expected_turn_id: { type: "string" }
496
497
  }, (input) => snakeCaseControlResult(dispatchControl(control, input, true))),
497
498
  tool("loo_codex_resume_thread", "Resume or rejoin a Codex thread. Live mode requires approval_audit_id.", controlSchema(), (input) => snakeCaseControlResult(control.resumeThread(controlInput(input)))),
498
499
  tool("loo_codex_send_message", "Send a message to a Codex thread. Live mode requires approval_audit_id.", controlSchema(true), (input) => snakeCaseControlResult(control.sendMessage(messageControlInput(input)))),
499
- tool("loo_codex_steer_thread", "Steer a running Codex thread. Live mode requires approval_audit_id.", controlSchema(true), (input) => snakeCaseControlResult(control.steerThread(messageControlInput(input)))),
500
+ tool("loo_codex_steer_thread", "Steer a running Codex thread. Live mode requires approval_audit_id and expected_turn_id.", controlSchema(true, true), (input) => snakeCaseControlResult(control.steerThread(messageControlInput(input, true)))),
500
501
  tool("loo_codex_interrupt_thread", "Interrupt a Codex thread. Live mode requires approval_audit_id.", controlSchema(), (input) => snakeCaseControlResult(control.interruptThread(controlInput(input)))),
501
502
  tool("loo_desktop_see", "Inspect desktop fallback readiness through direct/CUA/Peekaboo backends.", {
502
503
  backend: { type: "string", enum: ["direct", "cua-driver", "peekaboo"] },
@@ -680,7 +681,14 @@ function targetThreadIdFromCoherenceInput(input) {
680
681
  }
681
682
  function dispatchControl(control, input, dryRun) {
682
683
  const action = requiredString(input.action, "action");
683
- const common = { threadId: requiredString(input.thread_id, "thread_id"), message: optionalString(input.message) ?? "continue", dryRun };
684
+ const common = {
685
+ threadId: requiredString(input.thread_id, "thread_id"),
686
+ message: action === "send" || action === "steer"
687
+ ? requiredString(input.message, "message")
688
+ : optionalString(input.message) ?? "continue",
689
+ expectedTurnId: optionalString(input.expected_turn_id),
690
+ dryRun
691
+ };
684
692
  if (action === "send")
685
693
  return control.sendMessage(common);
686
694
  if (action === "resume")
@@ -691,10 +699,11 @@ function dispatchControl(control, input, dryRun) {
691
699
  return control.interruptThread(common);
692
700
  throw new Error(`Unsupported control action: ${action}`);
693
701
  }
694
- function controlSchema(message = false) {
702
+ function controlSchema(message = false, expectedTurn = false) {
695
703
  return {
696
704
  thread_id: { type: "string" },
697
705
  ...(message ? { message: { type: "string" } } : {}),
706
+ ...(expectedTurn ? { expected_turn_id: { type: "string" } } : {}),
698
707
  dry_run: { type: "boolean", default: true },
699
708
  approval_audit_id: { type: "string" }
700
709
  };
@@ -707,10 +716,11 @@ function controlInput(input, message = false) {
707
716
  approvalAuditId: optionalString(input.approval_audit_id)
708
717
  };
709
718
  }
710
- function messageControlInput(input) {
719
+ function messageControlInput(input, expectedTurn = false) {
711
720
  return {
712
721
  threadId: requiredString(input.thread_id, "thread_id"),
713
722
  message: requiredString(input.message, "message"),
723
+ ...(expectedTurn ? { expectedTurnId: requiredString(input.expected_turn_id, "expected_turn_id") } : {}),
714
724
  dryRun: input.dry_run !== false,
715
725
  approvalAuditId: optionalString(input.approval_audit_id)
716
726
  };
@@ -728,14 +738,17 @@ async function snakeCaseControlResult(value) {
728
738
  }
729
739
  function approvalPacketFromControlResult(result) {
730
740
  const action = String(result.action ?? "resume");
731
- const packetAction = action === "send"
741
+ const packetAction = action === "send" || action === "codex_send_message"
732
742
  ? "send_message"
733
- : action === "steer"
743
+ : action === "steer" || action === "codex_steer_thread"
734
744
  ? "steer_thread"
735
- : action === "interrupt"
745
+ : action === "interrupt" || action === "codex_interrupt_thread"
736
746
  ? "interrupt_thread"
737
747
  : "resume_session";
738
748
  const expiresAt = new Date(Date.now() + 15 * 60 * 1000).toISOString();
749
+ const predictedMutation = Array.isArray(result.methodSequence) && result.methodSequence.length
750
+ ? result.methodSequence.map((method) => String(method))
751
+ : [String(result.method ?? "codex_control")];
739
752
  return {
740
753
  schema: "lco.approvalPacket.v1",
741
754
  packetId: `ap_${String(result.approvalAuditId ?? "unknown").replace(/^loo_audit_/, "")}`,
@@ -745,7 +758,8 @@ function approvalPacketFromControlResult(result) {
745
758
  title: String(result.threadId ?? "unknown")
746
759
  },
747
760
  intent: `${packetAction} dry-run for ${String(result.threadId ?? "unknown")}`,
748
- predictedMutation: [String(result.method ?? "codex_control")],
761
+ predictedMutation,
762
+ ...(Array.isArray(result.methodSequence) ? { methodSequence: result.methodSequence } : {}),
749
763
  preconditions: ["dry_run_record_exists", "matching_params_hash_required", "approval_packet_not_expired"],
750
764
  risk: {
751
765
  level: action === "interrupt" ? "high" : "medium",
@@ -0,0 +1,103 @@
1
+ # Release Notes 1.1.1
2
+
3
+ `1.1.1` is a stable patch release for the post-1.1 Codex collaboration
4
+ cockpit and OpenClaw gateway lane.
5
+
6
+ It keeps the `1.1.0` stable claim scope and adds three release-train fixes:
7
+ same-connection live-control failure handling, Desktop-first daily skill
8
+ guidance, and a runtime-required daily orchestration scenario contract.
9
+
10
+ ## What Changed
11
+
12
+ - Hardens same-connection Codex live control from #380 / PR #390:
13
+ - `loo_codex_send_message` keeps `thread/resume` and `turn/start` on the
14
+ same Codex app-server connection.
15
+ - failed or incomplete multi-step responses now fail closed before a live
16
+ audit success record is appended.
17
+ - the live-control smoke helper stops after the first failed sequence step,
18
+ so a failed `thread/resume` cannot be followed by `turn/start`.
19
+ - Updates the packaged OpenClaw agent skill from #385 / PR #391:
20
+ - replaces private-name approval wording with explicit requesting-user
21
+ approval for the exact target and action.
22
+ - clarifies that autonomy tick steps are recommendations until separately
23
+ requested and approved.
24
+ - Adds the Desktop-first daily orchestration scenario from #382 / PR #392:
25
+ - app-server status, app-server thread signals, visible Codex map,
26
+ Desktop coherence, fallback status, collaboration cockpit, runtime Desktop
27
+ visibility, active-thread state, and autonomy tick appear in one
28
+ runtime-required scenario contract.
29
+ - the scenario requires public-safe evidence and explicitly blocks live
30
+ control, GUI mutation, screenshots, npm publish, and GitHub Release
31
+ creation during the scenario itself.
32
+ - Carries forward the #160 desktop proof-action release boundary:
33
+ `loo_desktop_proof_action` / `loo desktop proof-action` validates the
34
+ action-bound CUA Driver TextEdit scratch proof gate and does not prove
35
+ generic GUI mutation. The proof action still requires exact backend, target
36
+ app, target window, action hash, approval ref, permission state, scratch file
37
+ path, and `execute: true` before the backend can run. Generic gateway
38
+ invocation without exact proof args fails closed, using the same proof
39
+ boundary as beta.35.
40
+ The proof action still requires exact backend, target app, target window, action hash, approval ref, permission state, scratch file path, and `execute: true` before the backend can run.
41
+ generic gateway invocation without exact proof args fails closed.
42
+ This release uses the same proof boundary as beta.35.
43
+ - Carries forward strict OpenClaw gateway result handling:
44
+ plugin output with `output.details.ok: false` is reported as
45
+ `openclaw_tool_result_not_ok:<tool>` rather than successful tool proof.
46
+
47
+ ## Stable Claim Scope
48
+
49
+ Allowed stable claim remains:
50
+
51
+ > Codex-first local orchestration through OpenClaw for local Codex sessions,
52
+ > including local indexing, search, describe, bounded expansion, dry-run control
53
+ > envelopes, read-only collaboration cockpit/autonomy cards, read-only Desktop
54
+ > visibility and fallback status, action-bound proof-packet validation,
55
+ > public-safe scorecards, and installed OpenClaw gateway dogfood.
56
+
57
+ This patch does not widen the live-control matrix, GUI mutation scope, Claude
58
+ adapter scope, P1 business-adapter scope, or enterprise/customer readiness
59
+ claim. The same excluded-claim boundaries from `1.1.0` remain in force.
60
+
61
+ ## Release Gate Notes
62
+
63
+ - Stable issue: #393.
64
+ - Included implementation issues: #380, #385, and #382.
65
+ - Parent product tracker: #309.
66
+ - Operating-loop tracker: #16.
67
+ - Baseline stable release: `v1.1.0`.
68
+ - Candidate package: `lossless-openclaw-orchestrator@1.1.1`.
69
+ - Expected git tag: `v1.1.1`.
70
+ - Expected GitHub Release:
71
+ `https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/releases/tag/v1.1.1`.
72
+ - Required pre-publish stable gates: `npm run check`, `npm pack --dry-run`,
73
+ strict scenario sweep, strict scorecard sweep, release preflight, release
74
+ bundle, release demo-status, release status, OpenClaw dogfood, and OpenClaw
75
+ tool-smoke for the exact stable candidate SHA.
76
+ - Required PR gates: GitHub CI, CodeQL, current-head review threads clear, and
77
+ any actionable review feedback fixed before merge.
78
+ - Required post-publish stable gates: npm `latest` view, git tag, GitHub
79
+ non-prerelease Release, `loo release finalization-status --expected-dist-tag
80
+ latest --expected-github-prerelease false --strict`, fresh npm `@latest`
81
+ published-smoke, and strict general-readiness before #393 closes.
82
+ - `approved_live_control_smoke_missing` remains the expected blocker when a
83
+ working-app claim is attempted without approved live-control smoke evidence
84
+ for the exact candidate SHA.
85
+ - Working-app status example:
86
+ `loo release status --claim-scope codex-working-app-proof --runtime-proof-dir <path> --approved-live-control-evidence <path> --npm-publish-approval-evidence <path> --github-release-approval-evidence <path> --candidate-sha <sha> --github-ci-evidence <path> --codeql-evidence <path> --evidence-dir <path> --strict`
87
+ - Reduced-scope status example:
88
+ `loo release status --claim-scope codex-read-search-expand-dry-run --npm-publish-approval-evidence <path> --github-release-approval-evidence <path> --candidate-sha <sha> --github-ci-evidence <path> --codeql-evidence <path> --evidence-dir <path> --strict`
89
+ - Post-publish finalization example:
90
+ `loo release finalization-status --evidence-dir <path> --candidate-sha <sha> --npm-publish-evidence <path> --git-tag-evidence <path> --github-release-evidence <path> --expected-dist-tag latest --expected-github-prerelease false --strict`
91
+
92
+ ## Explicit Non-Claims
93
+
94
+ No new live Codex control smoke is run by this release.
95
+ It does not run generic GUI mutation and does not run Codex GUI mutation.
96
+ No automatic gateway authorization.
97
+ No broad gateway scope approval. No prompt typing. No clicking. No arbitrary app control.
98
+ Claude Code remains an adapter stub, not an adapter-equivalence claim.
99
+ Bundle/status/finalization checks do not publish to npm and do not create a GitHub Release.
100
+ No release-grade enterprise security.
101
+ No Claude Code parity, no Notion/support-control/Stripe/Company Brain P1
102
+ adapter proof, no cloud sync, no unattended desktop takeover, and no
103
+ enterprise/customer-ready security is claimed.
@@ -0,0 +1,81 @@
1
+ {
2
+ "scenario_version": "1.1",
3
+ "id": "desktop-first-daily-orchestration-v1-1",
4
+ "title": "Desktop-first Codex daily orchestration scenario",
5
+ "claim_scope": "codex-working-app-proof",
6
+ "proof_mode": "runtime_required",
7
+ "issue": "#382",
8
+ "related_issues": [
9
+ "#309",
10
+ "#307",
11
+ "#308",
12
+ "#313",
13
+ "#333",
14
+ "#342",
15
+ "#385"
16
+ ],
17
+ "user_task": "Run the Desktop-first daily Codex operating loop: inspect app-server and visible-map state, classify active lanes, select safe next probes or control dry-runs, and produce public-safe evidence without executing live control.",
18
+ "surface": "codex-desktop-first-orchestration",
19
+ "required_tools": [
20
+ "loo_codex_app_server_status",
21
+ "loo_codex_app_server_threads",
22
+ "loo_visible_codex_map",
23
+ "loo_codex_desktop_coherence",
24
+ "loo_codex_desktop_fallback_status",
25
+ "loo_codex_collaboration_cockpit",
26
+ "loo_codex_runtime_desktop_visibility_status",
27
+ "loo_codex_active_thread_state",
28
+ "loo_codex_autonomy_tick"
29
+ ],
30
+ "allowed_live_behaviors": [
31
+ "read-only app-server status and thread probes",
32
+ "read-only visible Codex metadata correlation",
33
+ "public-safe Desktop coherence and fallback classification",
34
+ "execute=false dry-run handoff recommendations"
35
+ ],
36
+ "forbidden_behaviors": [
37
+ "raw_transcript_read",
38
+ "unauthorized_live_control",
39
+ "unscoped_gui_mutation",
40
+ "secret_or_private_data_output",
41
+ "screenshot_or_video_capture",
42
+ "unattended_desktop_takeover",
43
+ "codex_desktop_select_click_type",
44
+ "codex_desktop_refresh_restart",
45
+ "direct_github_write_without_approval",
46
+ "npm_publish",
47
+ "github_release_creation"
48
+ ],
49
+ "private_data_exclusions": [
50
+ "raw Codex transcripts",
51
+ "raw prompts or message text",
52
+ "SQLite DBs",
53
+ "screenshots or videos",
54
+ "tokens, credentials, API keys, cookies",
55
+ "private customer data",
56
+ "absolute local transcript paths"
57
+ ],
58
+ "expected_public_safe_evidence": [
59
+ "ordered Desktop-first tool sequence from app-server status through autonomy tick",
60
+ "multiple lane states: desktop_visible, cli_visible, fallback_blocked, needs_nudge, needs_approval, stale, and unknown/conflict",
61
+ "source refs for active lanes without raw transcript paths or raw prompt text",
62
+ "safe next action selection with execute=false tool calls until explicit approval",
63
+ "Desktop coherence and fallback source coverage without screenshots or raw window text",
64
+ "active-thread and autonomy reason codes for nudge and approval lanes",
65
+ "actionsPerformed flags showing no live Codex control, no GUI mutation, no screenshot capture, no npm publish, and no GitHub Release creation",
66
+ "public-safe scan proving no raw transcript, raw prompt, screenshot, SQLite DB, token, cookie, credential, or private customer data leaks"
67
+ ],
68
+ "metrics": {
69
+ "requires_desktop_first_tool_sequence": true,
70
+ "requires_lane_state_mix": true,
71
+ "requires_execute_false_next_tool_calls": true,
72
+ "requires_safe_next_action_selection": true,
73
+ "requires_public_safe_scan": true,
74
+ "max_live_actions": 0,
75
+ "max_raw_transcript_spans": 0,
76
+ "max_raw_prompt_chars": 0,
77
+ "max_screenshots_in_public_evidence": 0
78
+ },
79
+ "proof_boundary": "Proves the Desktop-first collaboration cockpit scenario contract for daily Codex orchestration, safe next-action selection, and public-safe handoff evidence. It does not prove live Codex control, generic GUI mutation, unattended Desktop collaboration, npm publish, GitHub Release creation, release readiness, or customer readiness.",
80
+ "next_action": "Run this scenario with public-safe runtime markers when the installed gateway has produced app-server, visible-map, coherence, fallback, cockpit, active-thread, autonomy, and execute=false next-action evidence."
81
+ }
@@ -2,7 +2,7 @@
2
2
  "id": "lossless-openclaw-orchestrator",
3
3
  "name": "Lossless OpenClaw Orchestrator",
4
4
  "description": "Control and collaborate with local Codex sessions through OpenClaw using local indexing, bounded recall, and approval-gated controls.",
5
- "version": "1.1.0",
5
+ "version": "1.1.1",
6
6
  "kind": "tool",
7
7
  "tools": {
8
8
  "prefix": "loo_"
@@ -1330,6 +1330,9 @@
1330
1330
  },
1331
1331
  "message": {
1332
1332
  "type": "string"
1333
+ },
1334
+ "expected_turn_id": {
1335
+ "type": "string"
1333
1336
  }
1334
1337
  }
1335
1338
  }
@@ -1379,7 +1382,7 @@
1379
1382
  },
1380
1383
  {
1381
1384
  "name": "loo_codex_steer_thread",
1382
- "description": "Steer a running Codex thread. Live mode requires approval_audit_id.",
1385
+ "description": "Steer a running Codex thread. Live mode requires approval_audit_id and expected_turn_id.",
1383
1386
  "inputSchema": {
1384
1387
  "type": "object",
1385
1388
  "additionalProperties": false,
@@ -1390,6 +1393,9 @@
1390
1393
  "message": {
1391
1394
  "type": "string"
1392
1395
  },
1396
+ "expected_turn_id": {
1397
+ "type": "string"
1398
+ },
1393
1399
  "dry_run": {
1394
1400
  "type": "boolean",
1395
1401
  "default": true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lossless-openclaw-orchestrator",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Index, search, and control local Codex sessions through OpenClaw with approval-gated safety; Claude Code remains an experimental adapter stub.",
5
5
  "type": "module",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",
@@ -268,6 +268,26 @@ export function createCodexMcpStdioClient(options: {
268
268
  } finally {
269
269
  await client.close();
270
270
  }
271
+ },
272
+ async requestSequence(steps: Array<{ method: string; params: Record<string, unknown> }>) {
273
+ const surface = options.surface ?? "control";
274
+ for (const step of steps) assertCodexMethodAllowed(step.method, surface);
275
+ const client = new CodexJsonRpcClient(
276
+ () => new LineProcessTransport(options.command ?? "codex", options.args ?? ["app-server", "--stdio"], options.timeoutMs),
277
+ { timeoutMs: options.timeoutMs, surface }
278
+ );
279
+ await client.connect();
280
+ try {
281
+ const responses: CodexJsonRpcResponse[] = [];
282
+ for (const step of steps) {
283
+ const response = await client.request(step.method, step.params);
284
+ responses.push(response);
285
+ if (!response.ok) break;
286
+ }
287
+ return responses;
288
+ } finally {
289
+ await client.close();
290
+ }
271
291
  }
272
292
  };
273
293
  }
@@ -12,6 +12,12 @@ export * from "./redaction.js";
12
12
 
13
13
  export type CodexClient = {
14
14
  request(method: string, params: Record<string, unknown>): Promise<unknown>;
15
+ requestSequence?(steps: CodexControlStep[]): Promise<unknown[]>;
16
+ };
17
+
18
+ export type CodexControlStep = {
19
+ method: string;
20
+ params: Record<string, unknown>;
15
21
  };
16
22
 
17
23
  export type SourceCoverageState = "ok" | "partial" | "unavailable" | "not_configured";
@@ -581,6 +587,9 @@ export type ControlResult = {
581
587
  paramsHash: string;
582
588
  messageHash?: string;
583
589
  method?: string;
590
+ methodSequence?: string[];
591
+ connectionScope?: "single_request" | "same_connection_sequence";
592
+ loadedThreadReusable?: boolean;
584
593
  response?: unknown;
585
594
  };
586
595
 
@@ -654,12 +663,25 @@ export function createCodexControl(options: { audit: ControlAuditStore; client:
654
663
  method: string;
655
664
  threadId: string;
656
665
  params: Record<string, unknown>;
666
+ steps?: CodexControlStep[];
657
667
  message?: string;
658
668
  dryRun?: boolean;
659
669
  approvalAuditId?: string;
670
+ loadedThreadReusable?: boolean;
660
671
  }): Promise<ControlResult> => {
661
672
  assertCodexMethodAllowed(spec.method, "control");
662
- const paramsHash = options.audit.fingerprintValue({ action: spec.action, method: spec.method, threadId: spec.threadId, params: spec.params });
673
+ const steps = spec.steps?.length ? spec.steps : [{ method: spec.method, params: spec.params }];
674
+ const methodSequence = steps.map((step) => step.method);
675
+ for (const step of steps) assertCodexMethodAllowed(step.method, "control");
676
+ const connectionScope = steps.length > 1 ? "same_connection_sequence" : "single_request";
677
+ const paramsHash = options.audit.fingerprintValue({
678
+ action: spec.action,
679
+ method: spec.method,
680
+ methodSequence,
681
+ threadId: spec.threadId,
682
+ params: spec.params,
683
+ steps
684
+ });
663
685
  const messageHash = spec.message === undefined ? undefined : options.audit.fingerprintText(spec.message);
664
686
  if (spec.dryRun !== false) {
665
687
  const record = options.audit.append({
@@ -669,7 +691,18 @@ export function createCodexControl(options: { audit: ControlAuditStore; client:
669
691
  messageHash,
670
692
  live: false
671
693
  });
672
- return { action: spec.action, threadId: spec.threadId, live: false, approvalAuditId: record.id, paramsHash, messageHash, method: spec.method };
694
+ return {
695
+ action: spec.action,
696
+ threadId: spec.threadId,
697
+ live: false,
698
+ approvalAuditId: record.id,
699
+ paramsHash,
700
+ messageHash,
701
+ method: spec.method,
702
+ methodSequence,
703
+ connectionScope,
704
+ loadedThreadReusable: spec.loadedThreadReusable
705
+ };
673
706
  }
674
707
 
675
708
  if (!spec.approvalAuditId) {
@@ -689,13 +722,29 @@ export function createCodexControl(options: { audit: ControlAuditStore; client:
689
722
  if (!Number.isFinite(dryRunCreatedAtMs) || dryRunCreatedAtMs + CODEX_CONTROL_DRY_RUN_TTL_MS <= Date.now()) {
690
723
  throw new Error("approval_audit_id dry-run record expired");
691
724
  }
692
- const response = await options.client.request(spec.method, spec.params);
725
+ const response = steps.length > 1
726
+ ? await requestCodexControlSequence(options.client, steps)
727
+ : await options.client.request(spec.method, spec.params);
693
728
  const liveRecord = options.audit.append({ action: spec.action, target: spec.threadId, paramsHash, messageHash, live: true });
694
- return { action: spec.action, threadId: spec.threadId, live: true, approvalAuditId: liveRecord.id, paramsHash, messageHash, method: spec.method, response: redactValue(response) };
729
+ return {
730
+ action: spec.action,
731
+ threadId: spec.threadId,
732
+ live: true,
733
+ approvalAuditId: liveRecord.id,
734
+ paramsHash,
735
+ messageHash,
736
+ method: spec.method,
737
+ methodSequence,
738
+ connectionScope,
739
+ loadedThreadReusable: spec.loadedThreadReusable,
740
+ response: redactValue(response)
741
+ };
695
742
  };
696
743
 
697
744
  return {
698
745
  sendMessage(input: { threadId: string; message: string; dryRun?: boolean; approvalAuditId?: string }) {
746
+ const resumeParams = { threadId: input.threadId, excludeTurns: true };
747
+ const turnStartParams = { threadId: input.threadId, input: [{ type: "text", text: input.message }] };
699
748
  return execute({
700
749
  action: "codex_send_message",
701
750
  method: "turn/start",
@@ -703,7 +752,12 @@ export function createCodexControl(options: { audit: ControlAuditStore; client:
703
752
  message: input.message,
704
753
  dryRun: input.dryRun,
705
754
  approvalAuditId: input.approvalAuditId,
706
- params: { threadId: input.threadId, input: [{ type: "text", text: input.message }] }
755
+ params: turnStartParams,
756
+ steps: [
757
+ { method: "thread/resume", params: resumeParams },
758
+ { method: "turn/start", params: turnStartParams }
759
+ ],
760
+ loadedThreadReusable: true
707
761
  });
708
762
  },
709
763
  resumeThread(input: { threadId: string; dryRun?: boolean; approvalAuditId?: string }) {
@@ -713,10 +767,12 @@ export function createCodexControl(options: { audit: ControlAuditStore; client:
713
767
  threadId: input.threadId,
714
768
  dryRun: input.dryRun,
715
769
  approvalAuditId: input.approvalAuditId,
716
- params: { threadId: input.threadId, excludeTurns: true }
770
+ params: { threadId: input.threadId, excludeTurns: true },
771
+ loadedThreadReusable: false
717
772
  });
718
773
  },
719
- steerThread(input: { threadId: string; message: string; dryRun?: boolean; approvalAuditId?: string }) {
774
+ steerThread(input: { threadId: string; message: string; expectedTurnId?: string; dryRun?: boolean; approvalAuditId?: string }) {
775
+ if (!input.expectedTurnId) throw new Error("expected_turn_id is required for steer actions");
720
776
  return execute({
721
777
  action: "codex_steer_thread",
722
778
  method: "turn/steer",
@@ -724,7 +780,7 @@ export function createCodexControl(options: { audit: ControlAuditStore; client:
724
780
  message: input.message,
725
781
  dryRun: input.dryRun,
726
782
  approvalAuditId: input.approvalAuditId,
727
- params: { threadId: input.threadId, input: [{ type: "text", text: input.message }] }
783
+ params: { threadId: input.threadId, expectedTurnId: input.expectedTurnId, input: [{ type: "text", text: input.message }] }
728
784
  });
729
785
  },
730
786
  interruptThread(input: { threadId: string; dryRun?: boolean; approvalAuditId?: string }) {
@@ -740,6 +796,22 @@ export function createCodexControl(options: { audit: ControlAuditStore; client:
740
796
  };
741
797
  }
742
798
 
799
+ async function requestCodexControlSequence(client: CodexClient, steps: CodexControlStep[]): Promise<unknown> {
800
+ if (!client.requestSequence) {
801
+ throw new Error("same-connection control sequence is required for this Codex control action");
802
+ }
803
+ const responses = await client.requestSequence(steps);
804
+ for (let index = 0; index < responses.length; index += 1) {
805
+ if (asRecord(responses[index])?.ok === false) {
806
+ throw new Error(`Codex control sequence step failed: ${steps[index]?.method ?? "unknown"}`);
807
+ }
808
+ }
809
+ if (responses.length !== steps.length) {
810
+ throw new Error(`Codex control sequence returned ${responses.length} response(s) for ${steps.length} step(s)`);
811
+ }
812
+ return responses.at(-1) ?? { ok: true };
813
+ }
814
+
743
815
  export async function createCodexAppServerStatusReport(options: {
744
816
  client: CodexClient;
745
817
  transport?: ReturnType<typeof codexTransportStatus>;
@@ -138,7 +138,23 @@ export async function runLiveControlSmoke(options: LiveControlSmokeOptions): Pro
138
138
  const target = options.threadId
139
139
  ? { threadId: options.threadId, source: "provided_thread" as const }
140
140
  : { threadId: await startEphemeralThread(options.client, options.cwd), source: "ephemeral_thread_start" as const };
141
- const control = createCodexControl({ audit: options.audit, client: { request: (method, params) => options.client.request(method, params) } });
141
+ const control = createCodexControl({
142
+ audit: options.audit,
143
+ client: {
144
+ request: (method, params) => options.client.request(method, params),
145
+ requestSequence: async (steps) => {
146
+ const responses = [];
147
+ for (const step of steps) {
148
+ const response = await options.client.request(step.method, step.params);
149
+ responses.push(response);
150
+ if (isFailedCodexResponse(response)) {
151
+ throw new Error(`Codex control sequence step failed: ${step.method}`);
152
+ }
153
+ }
154
+ return responses;
155
+ }
156
+ }
157
+ });
142
158
  const dryRun = await control.sendMessage({ threadId: target.threadId, message, dryRun: true });
143
159
  if (!dryRun.messageHash) throw new Error("dry-run did not produce a message hash");
144
160
  const live = await control.sendMessage({
@@ -233,6 +249,10 @@ function extractTurnId(response: unknown): string | null {
233
249
  return stringField(turn, "id");
234
250
  }
235
251
 
252
+ function isFailedCodexResponse(value: unknown): boolean {
253
+ return Boolean(value && typeof value === "object" && !Array.isArray(value) && (value as Record<string, unknown>).ok === false);
254
+ }
255
+
236
256
  function unwrapResult(value: unknown): Record<string, unknown> {
237
257
  if (!value || typeof value !== "object" || Array.isArray(value)) return {};
238
258
  const record = value as Record<string, unknown>;
@@ -566,11 +566,12 @@ export function createLooTools(options: { db: LooDatabase; audit: AuditStore; co
566
566
  tool("loo_codex_control_dry_run", "Create a dry-run audit id for a Codex control action.", {
567
567
  action: { type: "string", enum: ["send", "resume", "steer", "interrupt"] },
568
568
  thread_id: { type: "string" },
569
- message: { type: "string" }
569
+ message: { type: "string" },
570
+ expected_turn_id: { type: "string" }
570
571
  }, (input) => snakeCaseControlResult(dispatchControl(control, input, true))),
571
572
  tool("loo_codex_resume_thread", "Resume or rejoin a Codex thread. Live mode requires approval_audit_id.", controlSchema(), (input) => snakeCaseControlResult(control.resumeThread(controlInput(input)))),
572
573
  tool("loo_codex_send_message", "Send a message to a Codex thread. Live mode requires approval_audit_id.", controlSchema(true), (input) => snakeCaseControlResult(control.sendMessage(messageControlInput(input)))),
573
- tool("loo_codex_steer_thread", "Steer a running Codex thread. Live mode requires approval_audit_id.", controlSchema(true), (input) => snakeCaseControlResult(control.steerThread(messageControlInput(input)))),
574
+ tool("loo_codex_steer_thread", "Steer a running Codex thread. Live mode requires approval_audit_id and expected_turn_id.", controlSchema(true, true), (input) => snakeCaseControlResult(control.steerThread(messageControlInput(input, true)))),
574
575
  tool("loo_codex_interrupt_thread", "Interrupt a Codex thread. Live mode requires approval_audit_id.", controlSchema(), (input) => snakeCaseControlResult(control.interruptThread(controlInput(input)))),
575
576
  tool("loo_desktop_see", "Inspect desktop fallback readiness through direct/CUA/Peekaboo backends.", {
576
577
  backend: { type: "string", enum: ["direct", "cua-driver", "peekaboo"] },
@@ -774,7 +775,14 @@ function targetThreadIdFromCoherenceInput(input: Record<string, unknown>): strin
774
775
 
775
776
  function dispatchControl(control: ReturnType<typeof createCodexControl>, input: Record<string, unknown>, dryRun: boolean) {
776
777
  const action = requiredString(input.action, "action");
777
- const common = { threadId: requiredString(input.thread_id, "thread_id"), message: optionalString(input.message) ?? "continue", dryRun };
778
+ const common = {
779
+ threadId: requiredString(input.thread_id, "thread_id"),
780
+ message: action === "send" || action === "steer"
781
+ ? requiredString(input.message, "message")
782
+ : optionalString(input.message) ?? "continue",
783
+ expectedTurnId: optionalString(input.expected_turn_id),
784
+ dryRun
785
+ };
778
786
  if (action === "send") return control.sendMessage(common);
779
787
  if (action === "resume") return control.resumeThread(common);
780
788
  if (action === "steer") return control.steerThread(common);
@@ -782,10 +790,11 @@ function dispatchControl(control: ReturnType<typeof createCodexControl>, input:
782
790
  throw new Error(`Unsupported control action: ${action}`);
783
791
  }
784
792
 
785
- function controlSchema(message = false): Record<string, unknown> {
793
+ function controlSchema(message = false, expectedTurn = false): Record<string, unknown> {
786
794
  return {
787
795
  thread_id: { type: "string" },
788
796
  ...(message ? { message: { type: "string" } } : {}),
797
+ ...(expectedTurn ? { expected_turn_id: { type: "string" } } : {}),
789
798
  dry_run: { type: "boolean", default: true },
790
799
  approval_audit_id: { type: "string" }
791
800
  };
@@ -800,10 +809,11 @@ function controlInput(input: Record<string, unknown>, message = false) {
800
809
  };
801
810
  }
802
811
 
803
- function messageControlInput(input: Record<string, unknown>) {
812
+ function messageControlInput(input: Record<string, unknown>, expectedTurn = false) {
804
813
  return {
805
814
  threadId: requiredString(input.thread_id, "thread_id"),
806
815
  message: requiredString(input.message, "message"),
816
+ ...(expectedTurn ? { expectedTurnId: requiredString(input.expected_turn_id, "expected_turn_id") } : {}),
807
817
  dryRun: input.dry_run !== false,
808
818
  approvalAuditId: optionalString(input.approval_audit_id)
809
819
  };
@@ -823,14 +833,17 @@ async function snakeCaseControlResult(value: Promise<any>) {
823
833
 
824
834
  function approvalPacketFromControlResult(result: any): Record<string, unknown> {
825
835
  const action = String(result.action ?? "resume");
826
- const packetAction = action === "send"
836
+ const packetAction = action === "send" || action === "codex_send_message"
827
837
  ? "send_message"
828
- : action === "steer"
838
+ : action === "steer" || action === "codex_steer_thread"
829
839
  ? "steer_thread"
830
- : action === "interrupt"
840
+ : action === "interrupt" || action === "codex_interrupt_thread"
831
841
  ? "interrupt_thread"
832
842
  : "resume_session";
833
843
  const expiresAt = new Date(Date.now() + 15 * 60 * 1000).toISOString();
844
+ const predictedMutation = Array.isArray(result.methodSequence) && result.methodSequence.length
845
+ ? result.methodSequence.map((method: unknown) => String(method))
846
+ : [String(result.method ?? "codex_control")];
834
847
  return {
835
848
  schema: "lco.approvalPacket.v1",
836
849
  packetId: `ap_${String(result.approvalAuditId ?? "unknown").replace(/^loo_audit_/, "")}`,
@@ -840,7 +853,8 @@ function approvalPacketFromControlResult(result: any): Record<string, unknown> {
840
853
  title: String(result.threadId ?? "unknown")
841
854
  },
842
855
  intent: `${packetAction} dry-run for ${String(result.threadId ?? "unknown")}`,
843
- predictedMutation: [String(result.method ?? "codex_control")],
856
+ predictedMutation,
857
+ ...(Array.isArray(result.methodSequence) ? { methodSequence: result.methodSequence } : {}),
844
858
  preconditions: ["dry_run_record_exists", "matching_params_hash_required", "approval_packet_not_expired"],
845
859
  risk: {
846
860
  level: action === "interrupt" ? "high" : "medium",
@@ -2,7 +2,7 @@
2
2
  "id": "lossless-openclaw-orchestrator",
3
3
  "name": "Lossless OpenClaw Orchestrator",
4
4
  "description": "Control and collaborate with local Codex sessions through OpenClaw using local indexing, bounded recall, and approval-gated controls.",
5
- "version": "1.1.0",
5
+ "version": "1.1.1",
6
6
  "kind": "tool",
7
7
  "tools": {
8
8
  "prefix": "loo_"
@@ -1330,6 +1330,9 @@
1330
1330
  },
1331
1331
  "message": {
1332
1332
  "type": "string"
1333
+ },
1334
+ "expected_turn_id": {
1335
+ "type": "string"
1333
1336
  }
1334
1337
  }
1335
1338
  }
@@ -1379,7 +1382,7 @@
1379
1382
  },
1380
1383
  {
1381
1384
  "name": "loo_codex_steer_thread",
1382
- "description": "Steer a running Codex thread. Live mode requires approval_audit_id.",
1385
+ "description": "Steer a running Codex thread. Live mode requires approval_audit_id and expected_turn_id.",
1383
1386
  "inputSchema": {
1384
1387
  "type": "object",
1385
1388
  "additionalProperties": false,
@@ -1390,6 +1393,9 @@
1390
1393
  "message": {
1391
1394
  "type": "string"
1392
1395
  },
1396
+ "expected_turn_id": {
1397
+ "type": "string"
1398
+ },
1393
1399
  "dry_run": {
1394
1400
  "type": "boolean",
1395
1401
  "default": true
@@ -151,6 +151,34 @@ Typical live tools after approval are `loo_codex_resume_thread`,
151
151
  13. If action is requested, run `loo_codex_control_dry_run`
152
152
  14. Wait for explicit approval before any live control
153
153
 
154
+ ## Codex Desktop-First Daily Loop
155
+
156
+ Use this loop when the user wants the daily Codex operating picture, active
157
+ Desktop collaboration state, or a safe next nudge recommendation.
158
+
159
+ 1. Start read-only with `loo_codex_app_server_status`,
160
+ `loo_codex_app_server_threads`, and `loo_visible_codex_map`.
161
+ 2. Run `loo_codex_desktop_coherence` before any Codex Desktop-visible claim.
162
+ Treat `cli_visible`, `desktop_refresh_required`,
163
+ `desktop_restart_required`, and `unknown` as proof gaps.
164
+ 3. If coherence exists, use `loo_codex_desktop_fallback_status` for fallback
165
+ readiness. If it returns `coherence_input_missing`, follow the returned
166
+ `nextToolCall` for coherence first.
167
+ 4. Build the daily attention view with `loo_codex_collaboration_cockpit`,
168
+ `loo_codex_runtime_desktop_visibility_status`,
169
+ `loo_codex_active_thread_state`, and `loo_codex_autonomy_tick`.
170
+ 5. For `needs_nudge` or `needs_approval`, show `nextControlDryRun` as an
171
+ `execute=false` handoff only. Do not run live control from this packet.
172
+ Treat all autonomy tick steps as recommendations until the requesting user
173
+ separately asks for and approves the exact action.
174
+ 6. Live control requires the exact dry-run audit id, matching
175
+ `approval_audit_id`, and explicit requesting-user approval for the exact
176
+ target and action.
177
+ 7. After an approved live action, run post-action refresh before claiming
178
+ success or updating the operating picture.
179
+ 8. If proof fails, create an issue-ready public-safe packet instead of pasting
180
+ raw logs, raw transcripts, screenshots, or unredacted tool evidence.
181
+
154
182
  ## Public-Safe Output Shape
155
183
 
156
184
  When reporting to a user or another agent, include: