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 +1 -1
- package/VISION.md +3 -3
- package/dist/packages/adapters/src/codex-jsonrpc.js +20 -0
- package/dist/packages/adapters/src/index.js +69 -7
- package/dist/packages/cli/src/live-control-smoke.js +20 -1
- package/dist/packages/mcp-server/src/tools.js +23 -9
- package/docs/RELEASE_NOTES_1.1.1.md +103 -0
- package/evals/scenarios/v1.1/desktop-first-daily-orchestration.json +81 -0
- package/openclaw.plugin.json +8 -2
- package/package.json +1 -1
- package/packages/adapters/src/codex-jsonrpc.ts +20 -0
- package/packages/adapters/src/index.ts +80 -8
- package/packages/cli/src/live-control-smoke.ts +21 -1
- package/packages/mcp-server/src/tools.ts +23 -9
- package/packages/openclaw-plugin/openclaw.plugin.json +8 -2
- package/skills/lossless-openclaw-orchestrator/SKILL.md +28 -0
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ safe action without rereading raw transcripts.
|
|
|
14
14
|
[](LICENSE)
|
|
15
15
|
[](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.
|
|
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 [#
|
|
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
|
|
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.
|
|
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
|
|
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 {
|
|
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 =
|
|
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 {
|
|
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:
|
|
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({
|
|
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 = {
|
|
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
|
|
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
|
+
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
|
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 {
|
|
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 =
|
|
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 {
|
|
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:
|
|
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({
|
|
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 = {
|
|
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
|
|
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.
|
|
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:
|