lossless-openclaw-orchestrator 1.1.1 → 1.1.2

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.1.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.2.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 [#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.
13
+ The current release lane is [#396](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/396), which publishes stable `1.1.2` on npm `latest` after the same full check, smoke, scenario, scorecard, release-status, finalization, and post-publish readiness gates used for `1.1.1`. This patch carries the same stable claim scope while adding the #381 issue-grade public-safe live-control smoke failure report.
14
14
 
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).
15
+ The stable 1.0.0, 1.1.0, and 1.1.1 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.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.
23
+ - Promote npm `latest` from stable `1.1.1` to stable `1.1.2` only after #396 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.
@@ -51,11 +51,28 @@ export async function runLiveControlSmoke(options) {
51
51
  mkdirSync(options.evidenceDir, { recursive: true });
52
52
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
53
53
  const message = options.message ?? DEFAULT_MESSAGE;
54
- await options.client.connect();
54
+ let targetState = {
55
+ threadId: options.threadId ?? null,
56
+ source: options.threadId ? "provided_thread" : "unknown"
57
+ };
58
+ let dryRunState = null;
59
+ let liveState = {
60
+ accepted: false,
61
+ approvalAuditId: null,
62
+ method: null,
63
+ completed: false,
64
+ status: null,
65
+ notificationMethods: [],
66
+ approvalRequestCount: 0,
67
+ serverRequestCount: 0
68
+ };
69
+ let failed = false;
55
70
  try {
71
+ await options.client.connect();
56
72
  const target = options.threadId
57
73
  ? { threadId: options.threadId, source: "provided_thread" }
58
74
  : { threadId: await startEphemeralThread(options.client, options.cwd), source: "ephemeral_thread_start" };
75
+ targetState = target;
59
76
  const control = createCodexControl({
60
77
  audit: options.audit,
61
78
  client: {
@@ -74,6 +91,12 @@ export async function runLiveControlSmoke(options) {
74
91
  }
75
92
  });
76
93
  const dryRun = await control.sendMessage({ threadId: target.threadId, message, dryRun: true });
94
+ dryRunState = {
95
+ approvalAuditId: dryRun.approvalAuditId,
96
+ paramsHash: dryRun.paramsHash,
97
+ messageHash: dryRun.messageHash,
98
+ live: false
99
+ };
77
100
  if (!dryRun.messageHash)
78
101
  throw new Error("dry-run did not produce a message hash");
79
102
  const live = await control.sendMessage({
@@ -82,10 +105,24 @@ export async function runLiveControlSmoke(options) {
82
105
  dryRun: false,
83
106
  approvalAuditId: dryRun.approvalAuditId
84
107
  });
108
+ liveState = {
109
+ ...liveState,
110
+ accepted: true,
111
+ approvalAuditId: live.approvalAuditId,
112
+ method: live.method ?? null
113
+ };
85
114
  const turnId = extractTurnId(live.response);
86
115
  if (!turnId)
87
116
  throw new Error("live Codex control response did not include a turn id");
88
117
  const completion = await options.client.waitForTurnCompletion({ threadId: target.threadId, turnId, timeoutMs });
118
+ liveState = {
119
+ ...liveState,
120
+ completed: completion.completed,
121
+ status: completion.status,
122
+ notificationMethods: summarizeMethods(completion.notificationMethods),
123
+ approvalRequestCount: completion.approvalRequestCount,
124
+ serverRequestCount: completion.serverRequestCount
125
+ };
89
126
  if (completion.approvalRequestCount > 0) {
90
127
  throw new Error("live Codex control smoke observed a Codex approval request; harmless smoke must not require approvals");
91
128
  }
@@ -138,8 +175,29 @@ export async function runLiveControlSmoke(options) {
138
175
  writeJson(reportPath, report);
139
176
  return report;
140
177
  }
178
+ catch (error) {
179
+ failed = true;
180
+ try {
181
+ writeFailureReport(options, {
182
+ error,
183
+ target: targetState,
184
+ dryRun: dryRunState,
185
+ live: liveState
186
+ });
187
+ }
188
+ catch {
189
+ // Best-effort evidence: never let report-writing failures mask the original smoke failure.
190
+ }
191
+ throw error;
192
+ }
141
193
  finally {
142
- await options.client.close();
194
+ try {
195
+ await options.client.close();
196
+ }
197
+ catch (closeError) {
198
+ if (!failed)
199
+ throw closeError;
200
+ }
143
201
  }
144
202
  }
145
203
  export function defaultAppServerArgs() {
@@ -197,3 +255,80 @@ function summarizeMethods(methods) {
197
255
  function writeJson(path, value) {
198
256
  writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
199
257
  }
258
+ function writeFailureReport(options, state) {
259
+ const blocker = classifyFailureBlocker(state.error);
260
+ const report = {
261
+ kind: "loo_live_control_smoke_failure",
262
+ ok: false,
263
+ proofReady: false,
264
+ generatedAt: options.now ?? new Date().toISOString(),
265
+ blocker,
266
+ command: {
267
+ action: "send",
268
+ actionClass: "codex_live_control_send"
269
+ },
270
+ target: {
271
+ source: state.target.source,
272
+ refClass: state.target.threadId ? "codex_thread" : "unknown",
273
+ ref: state.target.threadId ? `codex_thread:${state.target.threadId}` : null
274
+ },
275
+ dryRun: {
276
+ attempted: state.dryRun !== null,
277
+ approvalAuditId: state.dryRun?.approvalAuditId ?? null,
278
+ paramsHash: state.dryRun?.paramsHash ?? null,
279
+ messageHash: state.dryRun?.messageHash ?? null,
280
+ live: state.dryRun ? false : null
281
+ },
282
+ transport: {
283
+ class: "codex_app_server",
284
+ connection: "single_connection_sequence"
285
+ },
286
+ live: state.live,
287
+ postActionRefresh: {
288
+ ran: false
289
+ },
290
+ nextDiagnosticStep: nextDiagnosticStep(blocker),
291
+ rawPromptIncluded: false,
292
+ rawTranscriptIncluded: false,
293
+ rawSecretIncluded: false,
294
+ screenshotIncluded: false,
295
+ sqliteIncluded: false
296
+ };
297
+ writeJson(join(options.evidenceDir, "live-control-smoke-failure-report.json"), report);
298
+ }
299
+ function classifyFailureBlocker(error) {
300
+ const message = error instanceof Error ? error.message : String(error);
301
+ if (/connect|handshake|spawn|ENOENT|codex.*not found|not connected/i.test(message)) {
302
+ return "codex_app_server_setup_failed";
303
+ }
304
+ if (/thread(?:\/resume| not found)|control sequence step failed:\s*thread\/resume/i.test(message)) {
305
+ return "same_connection_resume_load_diagnostics_required";
306
+ }
307
+ if (/approval request/i.test(message))
308
+ return "codex_approval_request_observed";
309
+ if (/server request/i.test(message))
310
+ return "codex_server_request_observed";
311
+ if (/turn id/i.test(message))
312
+ return "live_control_turn_id_missing";
313
+ if (/did not complete cleanly|timeout/i.test(message))
314
+ return "live_control_smoke_not_completed_cleanly";
315
+ return "live_control_smoke_failed";
316
+ }
317
+ function nextDiagnosticStep(blocker) {
318
+ switch (blocker) {
319
+ case "codex_app_server_setup_failed":
320
+ return "Check Codex app-server command availability, stdio handshake, and local transport setup before retrying live control.";
321
+ case "same_connection_resume_load_diagnostics_required":
322
+ return "Run same-connection resume/load diagnostics for the selected thread before retrying live control.";
323
+ case "codex_approval_request_observed":
324
+ return "Inspect why the harmless smoke triggered a Codex approval request; do not treat this as approved live-control proof.";
325
+ case "codex_server_request_observed":
326
+ return "Inspect unexpected server requests from the app-server smoke; do not retry live control until the request class is understood.";
327
+ case "live_control_turn_id_missing":
328
+ return "Inspect the app-server turn/start response shape and protocol drift before retrying.";
329
+ case "live_control_smoke_not_completed_cleanly":
330
+ return "Inspect turn completion notifications, status, and timeout behavior before retrying live control.";
331
+ default:
332
+ return "Inspect the public-safe failure report and route a focused issue before retrying live control.";
333
+ }
334
+ }
@@ -0,0 +1,99 @@
1
+ # Release Notes 1.1.2
2
+
3
+ `1.1.2` 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.1` stable claim scope and adds the #381 live-control smoke
7
+ failure-evidence hardening.
8
+
9
+ ## What Changed
10
+
11
+ - Failed `loo codex live-control-smoke` runs now write
12
+ `live-control-smoke-failure-report.json` before exiting non-zero.
13
+ - The failure report is public-safe and issue-ready:
14
+ - blocker code
15
+ - command/action class
16
+ - target ref class
17
+ - dry-run/audit/hash state
18
+ - live acceptance state
19
+ - post-action refresh state
20
+ - next diagnostic step
21
+ - Setup/connect failures now use the same guarded failure-report path, so
22
+ missing Codex app-server binaries, bad app-server args, and handshake failures
23
+ can produce structured public-safe evidence.
24
+ - Failure-report write errors no longer mask the original live-control smoke
25
+ failure.
26
+ - Regression coverage now proves both setup-failure reporting and original
27
+ error preservation.
28
+ - Carries forward the #160 desktop proof-action release boundary:
29
+ `loo_desktop_proof_action` / `loo desktop proof-action` validates the
30
+ action-bound CUA Driver TextEdit scratch proof gate and does not prove
31
+ generic GUI mutation. The proof action still requires exact backend, target
32
+ app, target window, action hash, approval ref, permission state, scratch file
33
+ path, and `execute: true` before the backend can run. Generic gateway
34
+ invocation without exact proof args fails closed, using the same proof
35
+ boundary as `1.1.1`.
36
+ Exact gate phrase: exact backend, target app, target window, action hash, approval ref, permission state, scratch file path, and `execute: true`.
37
+ Exact fail-closed phrase: generic gateway invocation without exact proof args fails closed.
38
+ The desktop proof action keeps the same proof boundary as beta.35.
39
+ - Carries forward strict OpenClaw gateway result handling:
40
+ plugin output with `output.details.ok: false` is reported as
41
+ `openclaw_tool_result_not_ok:<tool>` rather than successful tool proof.
42
+
43
+ ## Stable Claim Scope
44
+
45
+ Allowed stable claim remains:
46
+
47
+ > Codex-first local orchestration through OpenClaw for local Codex sessions,
48
+ > including local indexing, search, describe, bounded expansion, dry-run control
49
+ > envelopes, read-only collaboration cockpit/autonomy cards, read-only Desktop
50
+ > visibility and fallback status, action-bound proof-packet validation,
51
+ > public-safe scorecards, and installed OpenClaw gateway dogfood.
52
+
53
+ This patch does not widen the live-control matrix, GUI mutation scope, Claude
54
+ adapter scope, P1 business-adapter scope, or enterprise/customer readiness
55
+ claim. The same excluded-claim boundaries from `1.1.1` remain in force.
56
+
57
+ ## Release Gate Notes
58
+
59
+ - Stable issue: #396.
60
+ - Included implementation issue: #381.
61
+ - Parent product tracker: #309.
62
+ - Operating-loop tracker: #16.
63
+ - Baseline stable release: `v1.1.1`.
64
+ - Candidate package: `lossless-openclaw-orchestrator@1.1.2`.
65
+ - Expected git tag: `v1.1.2`.
66
+ - Expected GitHub Release:
67
+ `https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/releases/tag/v1.1.2`.
68
+ - Required pre-publish stable gates: `npm run check`, `npm pack --dry-run`,
69
+ strict scenario sweep, strict scorecard sweep, release preflight, release
70
+ bundle, release demo-status, release status, OpenClaw dogfood, and OpenClaw
71
+ tool-smoke for the exact stable candidate SHA.
72
+ - Required PR gates: GitHub CI, CodeQL, current-head review threads clear, and
73
+ any actionable review feedback fixed before merge.
74
+ - Required post-publish stable gates: npm `latest` view, git tag, GitHub
75
+ non-prerelease Release, `loo release finalization-status --expected-dist-tag
76
+ latest --expected-github-prerelease false --strict`, fresh npm `@latest`
77
+ published-smoke, and strict general-readiness before #396 closes.
78
+ - `approved_live_control_smoke_missing` remains the expected blocker when a
79
+ working-app claim is attempted without approved live-control smoke evidence
80
+ for the exact candidate SHA.
81
+ - Working-app status example:
82
+ `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`
83
+ - Reduced-scope status example:
84
+ `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`
85
+ - Post-publish finalization example:
86
+ `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`
87
+
88
+ ## Explicit Non-Claims
89
+
90
+ No new live Codex control smoke is run by this release.
91
+ It does not run generic GUI mutation and does not run Codex GUI mutation.
92
+ No automatic gateway authorization.
93
+ No broad gateway scope approval. No prompt typing. No clicking. No arbitrary app control.
94
+ Claude Code remains an adapter stub, not an adapter-equivalence claim.
95
+ Bundle/status/finalization checks do not publish to npm and do not create a GitHub Release.
96
+ No release-grade enterprise security.
97
+ No Claude Code parity, no Notion/support-control/Stripe/Company Brain P1
98
+ adapter proof, no cloud sync, no unattended desktop takeover, and no
99
+ enterprise/customer-ready security is claimed.
@@ -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.1",
5
+ "version": "1.1.2",
6
6
  "kind": "tool",
7
7
  "tools": {
8
8
  "prefix": "loo_"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lossless-openclaw-orchestrator",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
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",
@@ -63,6 +63,53 @@ export type LiveControlSmokeReport = {
63
63
  rawPromptIncluded: false;
64
64
  };
65
65
 
66
+ export type LiveControlSmokeFailureReport = {
67
+ kind: "loo_live_control_smoke_failure";
68
+ ok: false;
69
+ proofReady: false;
70
+ generatedAt: string;
71
+ blocker: string;
72
+ command: {
73
+ action: "send";
74
+ actionClass: "codex_live_control_send";
75
+ };
76
+ target: {
77
+ source: "ephemeral_thread_start" | "provided_thread" | "unknown";
78
+ refClass: "codex_thread" | "unknown";
79
+ ref: string | null;
80
+ };
81
+ dryRun: {
82
+ attempted: boolean;
83
+ approvalAuditId: string | null;
84
+ paramsHash: string | null;
85
+ messageHash: string | null;
86
+ live: false | null;
87
+ };
88
+ transport: {
89
+ class: "codex_app_server";
90
+ connection: "single_connection_sequence";
91
+ };
92
+ live: {
93
+ accepted: boolean;
94
+ approvalAuditId: string | null;
95
+ method: string | null;
96
+ completed: boolean;
97
+ status: string | null;
98
+ notificationMethods: string[];
99
+ approvalRequestCount: number;
100
+ serverRequestCount: number;
101
+ };
102
+ postActionRefresh: {
103
+ ran: false;
104
+ };
105
+ nextDiagnosticStep: string;
106
+ rawPromptIncluded: false;
107
+ rawTranscriptIncluded: false;
108
+ rawSecretIncluded: false;
109
+ screenshotIncluded: false;
110
+ sqliteIncluded: false;
111
+ };
112
+
66
113
  export type LiveControlSmokeOptions = {
67
114
  client: LiveControlSmokeClient;
68
115
  audit: AuditStore;
@@ -133,11 +180,37 @@ export async function runLiveControlSmoke(options: LiveControlSmokeOptions): Pro
133
180
  mkdirSync(options.evidenceDir, { recursive: true });
134
181
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
135
182
  const message = options.message ?? DEFAULT_MESSAGE;
136
- await options.client.connect();
183
+ let targetState: { threadId: string | null; source: "ephemeral_thread_start" | "provided_thread" | "unknown" } = {
184
+ threadId: options.threadId ?? null,
185
+ source: options.threadId ? "provided_thread" : "unknown"
186
+ };
187
+ let dryRunState: { approvalAuditId: string; paramsHash: string; messageHash?: string; live: false } | null = null;
188
+ let liveState: {
189
+ accepted: boolean;
190
+ approvalAuditId: string | null;
191
+ method: string | null;
192
+ completed: boolean;
193
+ status: string | null;
194
+ notificationMethods: string[];
195
+ approvalRequestCount: number;
196
+ serverRequestCount: number;
197
+ } = {
198
+ accepted: false,
199
+ approvalAuditId: null,
200
+ method: null,
201
+ completed: false,
202
+ status: null,
203
+ notificationMethods: [],
204
+ approvalRequestCount: 0,
205
+ serverRequestCount: 0
206
+ };
207
+ let failed = false;
137
208
  try {
209
+ await options.client.connect();
138
210
  const target = options.threadId
139
211
  ? { threadId: options.threadId, source: "provided_thread" as const }
140
212
  : { threadId: await startEphemeralThread(options.client, options.cwd), source: "ephemeral_thread_start" as const };
213
+ targetState = target;
141
214
  const control = createCodexControl({
142
215
  audit: options.audit,
143
216
  client: {
@@ -156,6 +229,12 @@ export async function runLiveControlSmoke(options: LiveControlSmokeOptions): Pro
156
229
  }
157
230
  });
158
231
  const dryRun = await control.sendMessage({ threadId: target.threadId, message, dryRun: true });
232
+ dryRunState = {
233
+ approvalAuditId: dryRun.approvalAuditId,
234
+ paramsHash: dryRun.paramsHash,
235
+ messageHash: dryRun.messageHash,
236
+ live: false
237
+ };
159
238
  if (!dryRun.messageHash) throw new Error("dry-run did not produce a message hash");
160
239
  const live = await control.sendMessage({
161
240
  threadId: target.threadId,
@@ -163,9 +242,23 @@ export async function runLiveControlSmoke(options: LiveControlSmokeOptions): Pro
163
242
  dryRun: false,
164
243
  approvalAuditId: dryRun.approvalAuditId
165
244
  });
245
+ liveState = {
246
+ ...liveState,
247
+ accepted: true,
248
+ approvalAuditId: live.approvalAuditId,
249
+ method: live.method ?? null
250
+ };
166
251
  const turnId = extractTurnId(live.response);
167
252
  if (!turnId) throw new Error("live Codex control response did not include a turn id");
168
253
  const completion = await options.client.waitForTurnCompletion({ threadId: target.threadId, turnId, timeoutMs });
254
+ liveState = {
255
+ ...liveState,
256
+ completed: completion.completed,
257
+ status: completion.status,
258
+ notificationMethods: summarizeMethods(completion.notificationMethods),
259
+ approvalRequestCount: completion.approvalRequestCount,
260
+ serverRequestCount: completion.serverRequestCount
261
+ };
169
262
  if (completion.approvalRequestCount > 0) {
170
263
  throw new Error("live Codex control smoke observed a Codex approval request; harmless smoke must not require approvals");
171
264
  }
@@ -218,8 +311,25 @@ export async function runLiveControlSmoke(options: LiveControlSmokeOptions): Pro
218
311
  writeJson(proofPath, proof);
219
312
  writeJson(reportPath, report);
220
313
  return report;
314
+ } catch (error) {
315
+ failed = true;
316
+ try {
317
+ writeFailureReport(options, {
318
+ error,
319
+ target: targetState,
320
+ dryRun: dryRunState,
321
+ live: liveState
322
+ });
323
+ } catch {
324
+ // Best-effort evidence: never let report-writing failures mask the original smoke failure.
325
+ }
326
+ throw error;
221
327
  } finally {
222
- await options.client.close();
328
+ try {
329
+ await options.client.close();
330
+ } catch (closeError) {
331
+ if (!failed) throw closeError;
332
+ }
223
333
  }
224
334
  }
225
335
 
@@ -283,3 +393,96 @@ function summarizeMethods(methods: string[]): string[] {
283
393
  function writeJson(path: string, value: unknown): void {
284
394
  writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
285
395
  }
396
+
397
+ function writeFailureReport(
398
+ options: LiveControlSmokeOptions,
399
+ state: {
400
+ error: unknown;
401
+ target: { threadId: string | null; source: "ephemeral_thread_start" | "provided_thread" | "unknown" };
402
+ dryRun: { approvalAuditId: string; paramsHash: string; messageHash?: string; live: false } | null;
403
+ live: {
404
+ accepted: boolean;
405
+ approvalAuditId: string | null;
406
+ method: string | null;
407
+ completed: boolean;
408
+ status: string | null;
409
+ notificationMethods: string[];
410
+ approvalRequestCount: number;
411
+ serverRequestCount: number;
412
+ };
413
+ }
414
+ ): void {
415
+ const blocker = classifyFailureBlocker(state.error);
416
+ const report: LiveControlSmokeFailureReport = {
417
+ kind: "loo_live_control_smoke_failure",
418
+ ok: false,
419
+ proofReady: false,
420
+ generatedAt: options.now ?? new Date().toISOString(),
421
+ blocker,
422
+ command: {
423
+ action: "send",
424
+ actionClass: "codex_live_control_send"
425
+ },
426
+ target: {
427
+ source: state.target.source,
428
+ refClass: state.target.threadId ? "codex_thread" : "unknown",
429
+ ref: state.target.threadId ? `codex_thread:${state.target.threadId}` : null
430
+ },
431
+ dryRun: {
432
+ attempted: state.dryRun !== null,
433
+ approvalAuditId: state.dryRun?.approvalAuditId ?? null,
434
+ paramsHash: state.dryRun?.paramsHash ?? null,
435
+ messageHash: state.dryRun?.messageHash ?? null,
436
+ live: state.dryRun ? false : null
437
+ },
438
+ transport: {
439
+ class: "codex_app_server",
440
+ connection: "single_connection_sequence"
441
+ },
442
+ live: state.live,
443
+ postActionRefresh: {
444
+ ran: false
445
+ },
446
+ nextDiagnosticStep: nextDiagnosticStep(blocker),
447
+ rawPromptIncluded: false,
448
+ rawTranscriptIncluded: false,
449
+ rawSecretIncluded: false,
450
+ screenshotIncluded: false,
451
+ sqliteIncluded: false
452
+ };
453
+ writeJson(join(options.evidenceDir, "live-control-smoke-failure-report.json"), report);
454
+ }
455
+
456
+ function classifyFailureBlocker(error: unknown): string {
457
+ const message = error instanceof Error ? error.message : String(error);
458
+ if (/connect|handshake|spawn|ENOENT|codex.*not found|not connected/i.test(message)) {
459
+ return "codex_app_server_setup_failed";
460
+ }
461
+ if (/thread(?:\/resume| not found)|control sequence step failed:\s*thread\/resume/i.test(message)) {
462
+ return "same_connection_resume_load_diagnostics_required";
463
+ }
464
+ if (/approval request/i.test(message)) return "codex_approval_request_observed";
465
+ if (/server request/i.test(message)) return "codex_server_request_observed";
466
+ if (/turn id/i.test(message)) return "live_control_turn_id_missing";
467
+ if (/did not complete cleanly|timeout/i.test(message)) return "live_control_smoke_not_completed_cleanly";
468
+ return "live_control_smoke_failed";
469
+ }
470
+
471
+ function nextDiagnosticStep(blocker: string): string {
472
+ switch (blocker) {
473
+ case "codex_app_server_setup_failed":
474
+ return "Check Codex app-server command availability, stdio handshake, and local transport setup before retrying live control.";
475
+ case "same_connection_resume_load_diagnostics_required":
476
+ return "Run same-connection resume/load diagnostics for the selected thread before retrying live control.";
477
+ case "codex_approval_request_observed":
478
+ return "Inspect why the harmless smoke triggered a Codex approval request; do not treat this as approved live-control proof.";
479
+ case "codex_server_request_observed":
480
+ return "Inspect unexpected server requests from the app-server smoke; do not retry live control until the request class is understood.";
481
+ case "live_control_turn_id_missing":
482
+ return "Inspect the app-server turn/start response shape and protocol drift before retrying.";
483
+ case "live_control_smoke_not_completed_cleanly":
484
+ return "Inspect turn completion notifications, status, and timeout behavior before retrying live control.";
485
+ default:
486
+ return "Inspect the public-safe failure report and route a focused issue before retrying live control.";
487
+ }
488
+ }
@@ -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.1",
5
+ "version": "1.1.2",
6
6
  "kind": "tool",
7
7
  "tools": {
8
8
  "prefix": "loo_"