@aria_asi/cli 0.2.36 → 0.2.38

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.
Files changed (198) hide show
  1. package/CLIENT-ONBOARDING.md +4 -2
  2. package/bin/aria.js +11 -7
  3. package/dist/aria-connector/src/auth.d.ts +14 -0
  4. package/dist/aria-connector/src/auth.d.ts.map +1 -1
  5. package/dist/aria-connector/src/auth.js +103 -1
  6. package/dist/aria-connector/src/auth.js.map +1 -1
  7. package/dist/aria-connector/src/chat.d.ts.map +1 -1
  8. package/dist/aria-connector/src/chat.js +13 -8
  9. package/dist/aria-connector/src/chat.js.map +1 -1
  10. package/dist/aria-connector/src/config.d.ts +6 -1
  11. package/dist/aria-connector/src/config.d.ts.map +1 -1
  12. package/dist/aria-connector/src/config.js.map +1 -1
  13. package/dist/aria-connector/src/connectors/claude-code.d.ts.map +1 -1
  14. package/dist/aria-connector/src/connectors/claude-code.js +50 -6
  15. package/dist/aria-connector/src/connectors/claude-code.js.map +1 -1
  16. package/dist/aria-connector/src/connectors/codex.d.ts.map +1 -1
  17. package/dist/aria-connector/src/connectors/codex.js +290 -32
  18. package/dist/aria-connector/src/connectors/codex.js.map +1 -1
  19. package/dist/aria-connector/src/connectors/opencode.d.ts.map +1 -1
  20. package/dist/aria-connector/src/connectors/opencode.js +35 -11
  21. package/dist/aria-connector/src/connectors/opencode.js.map +1 -1
  22. package/dist/aria-connector/src/connectors/repo-guard.d.ts +10 -0
  23. package/dist/aria-connector/src/connectors/repo-guard.d.ts.map +1 -1
  24. package/dist/aria-connector/src/connectors/repo-guard.js +110 -164
  25. package/dist/aria-connector/src/connectors/repo-guard.js.map +1 -1
  26. package/dist/aria-connector/src/connectors/runtime.d.ts.map +1 -1
  27. package/dist/aria-connector/src/connectors/runtime.js +17 -7
  28. package/dist/aria-connector/src/connectors/runtime.js.map +1 -1
  29. package/dist/aria-connector/src/connectors/shell.d.ts.map +1 -1
  30. package/dist/aria-connector/src/connectors/shell.js +12 -8
  31. package/dist/aria-connector/src/connectors/shell.js.map +1 -1
  32. package/dist/aria-connector/src/harness-client.d.ts +3 -1
  33. package/dist/aria-connector/src/harness-client.d.ts.map +1 -1
  34. package/dist/aria-connector/src/harness-client.js +7 -20
  35. package/dist/aria-connector/src/harness-client.js.map +1 -1
  36. package/dist/aria-connector/src/model-context.d.ts.map +1 -1
  37. package/dist/aria-connector/src/model-context.js +5 -0
  38. package/dist/aria-connector/src/model-context.js.map +1 -1
  39. package/dist/aria-connector/src/providers/types.d.ts +1 -1
  40. package/dist/aria-connector/src/providers/types.d.ts.map +1 -1
  41. package/dist/aria-connector/src/providers/xai.d.ts +3 -0
  42. package/dist/aria-connector/src/providers/xai.d.ts.map +1 -0
  43. package/dist/aria-connector/src/providers/xai.js +40 -0
  44. package/dist/aria-connector/src/providers/xai.js.map +1 -0
  45. package/dist/aria-connector/src/setup-wizard.js +1 -0
  46. package/dist/aria-connector/src/setup-wizard.js.map +1 -1
  47. package/dist/aria-connector/src/types.d.ts +2 -0
  48. package/dist/aria-connector/src/types.d.ts.map +1 -1
  49. package/dist/assets/hooks/aria-cognition-substrate-binding.mjs +51 -9
  50. package/dist/assets/hooks/aria-first-class-coach.mjs +129 -0
  51. package/dist/assets/hooks/aria-harness-via-sdk.mjs +33 -6
  52. package/dist/assets/hooks/aria-pre-tool-gate.mjs +86 -8
  53. package/dist/assets/hooks/aria-pre-tool-use.mjs +75 -0
  54. package/dist/assets/hooks/aria-preprompt-consult.mjs +5 -6
  55. package/dist/assets/hooks/aria-preturn-memory-gate.mjs +5 -0
  56. package/dist/assets/hooks/aria-repo-doctrine-gate.mjs +15 -0
  57. package/dist/assets/hooks/aria-stop-gate.mjs +125 -17
  58. package/dist/assets/hooks/doctrine_trigger_map.json +11 -0
  59. package/dist/assets/hooks/lib/emergency-gateoff-impl.mjs +39 -0
  60. package/dist/assets/hooks/lib/emergency-gateoff.mjs +6 -0
  61. package/dist/assets/hooks/lib/first-class-coach.mjs +755 -0
  62. package/dist/assets/hooks/lib/skill-autoload-gate-impl.mjs +103 -0
  63. package/dist/assets/hooks/lib/skill-autoload-gate.mjs +1 -14
  64. package/dist/assets/opencode-plugins/harness-context/auth-token.mjs +126 -0
  65. package/dist/assets/opencode-plugins/harness-context/inject-context.mjs +62 -22
  66. package/dist/assets/opencode-plugins/harness-context/task-project-ledger.mjs +290 -0
  67. package/dist/assets/opencode-plugins/harness-gate/index.js +87 -27
  68. package/dist/assets/opencode-plugins/harness-gate/lib/skill-autoload-gate.js +1 -14
  69. package/dist/assets/opencode-plugins/harness-outcome/index.js +29 -24
  70. package/dist/assets/opencode-plugins/harness-stop/index.js +229 -68
  71. package/dist/assets/opencode-plugins/harness-stop/lib/skill-autoload-gate.js +1 -14
  72. package/dist/runtime/auth-token.mjs +121 -0
  73. package/dist/runtime/coach-kernel.mjs +377 -0
  74. package/dist/runtime/codex-bridge.mjs +440 -69
  75. package/dist/runtime/discipline/doctrine_trigger_map.json +11 -0
  76. package/dist/runtime/discipline/skills/aria-cognition/aria-essence/SKILL.md +18 -0
  77. package/dist/runtime/discipline/skills/aria-cognition/aria-forge-guardrails/SKILL.md +18 -0
  78. package/dist/runtime/discipline/skills/aria-cognition/aria-repo-doctrine/SKILL.md +18 -0
  79. package/dist/runtime/discipline/skills/aria-cognition/forge-quality-rules/SKILL.md +18 -0
  80. package/dist/runtime/discipline/skills/aria-cognition/ghazali-8lens/SKILL.md +18 -0
  81. package/dist/runtime/discipline/skills/aria-cognition/istiqra-induction/SKILL.md +18 -0
  82. package/dist/runtime/discipline/skills/aria-cognition/ladunni-22/SKILL.md +18 -0
  83. package/dist/runtime/discipline/skills/aria-cognition/mizan/SKILL.md +18 -0
  84. package/dist/runtime/discipline/skills/aria-cognition/nadia/SKILL.md +18 -0
  85. package/dist/runtime/discipline/skills/aria-cognition/nadia-psi/SKILL.md +18 -0
  86. package/dist/runtime/discipline/skills/aria-cognition/predictor/SKILL.md +18 -0
  87. package/dist/runtime/discipline/skills/aria-cognition/qiyas-analogy/SKILL.md +18 -0
  88. package/dist/runtime/discipline/skills/aria-cognition/soul-domains/SKILL.md +18 -0
  89. package/dist/runtime/discipline/skills/aria-harness/aria-aristotle-intra-phase/SKILL.md +18 -0
  90. package/dist/runtime/discipline/skills/aria-harness/aria-aristotle-post-phase/SKILL.md +18 -0
  91. package/dist/runtime/discipline/skills/aria-harness/aria-aristotle-pre-phase/SKILL.md +18 -0
  92. package/dist/runtime/discipline/skills/aria-harness/aria-harness-deploy/SKILL.md +18 -0
  93. package/dist/runtime/discipline/skills/aria-harness/aria-harness-no-stripping/SKILL.md +18 -0
  94. package/dist/runtime/discipline/skills/aria-harness/aria-harness-onboarding/SKILL.md +18 -0
  95. package/dist/runtime/discipline/skills/aria-harness/aria-harness-output-discipline/SKILL.md +18 -0
  96. package/dist/runtime/discipline/skills/aria-harness/aria-harness-substrate-binding/SKILL.md +18 -0
  97. package/dist/runtime/doctrine_trigger_map.json +11 -0
  98. package/dist/runtime/hooks/aria-cognition-substrate-binding.mjs +51 -9
  99. package/dist/runtime/hooks/aria-first-class-coach.mjs +129 -0
  100. package/dist/runtime/hooks/aria-harness-via-sdk.mjs +33 -6
  101. package/dist/runtime/hooks/aria-pre-tool-gate.mjs +86 -8
  102. package/dist/runtime/hooks/aria-pre-tool-use.mjs +75 -0
  103. package/dist/runtime/hooks/aria-preprompt-consult.mjs +5 -6
  104. package/dist/runtime/hooks/aria-preturn-memory-gate.mjs +5 -0
  105. package/dist/runtime/hooks/aria-repo-doctrine-gate.mjs +15 -0
  106. package/dist/runtime/hooks/aria-stop-gate.mjs +125 -17
  107. package/dist/runtime/hooks/doctrine_trigger_map.json +11 -0
  108. package/dist/runtime/hooks/lib/emergency-gateoff-impl.mjs +39 -0
  109. package/dist/runtime/hooks/lib/emergency-gateoff.mjs +6 -0
  110. package/dist/runtime/hooks/lib/first-class-coach.mjs +755 -0
  111. package/dist/runtime/hooks/lib/skill-autoload-gate-impl.mjs +103 -0
  112. package/dist/runtime/hooks/lib/skill-autoload-gate.mjs +1 -14
  113. package/dist/runtime/local-phase.mjs +8 -0
  114. package/dist/runtime/manifest.json +2 -2
  115. package/dist/runtime/provider-proxy.mjs +136 -33
  116. package/dist/runtime/sdk/BUNDLED.json +2 -2
  117. package/dist/runtime/sdk/auth.d.ts +17 -0
  118. package/dist/runtime/sdk/auth.js +158 -0
  119. package/dist/runtime/sdk/auth.js.map +1 -0
  120. package/dist/runtime/sdk/index.d.ts +8 -1
  121. package/dist/runtime/sdk/index.js +15 -1
  122. package/dist/runtime/sdk/index.js.map +1 -1
  123. package/dist/runtime/service.mjs +1711 -74
  124. package/dist/runtime/task-project-ledger.mjs +290 -0
  125. package/dist/sdk/BUNDLED.json +2 -2
  126. package/dist/sdk/auth.d.ts +17 -0
  127. package/dist/sdk/auth.js +158 -0
  128. package/dist/sdk/auth.js.map +1 -0
  129. package/dist/sdk/index.d.ts +8 -1
  130. package/dist/sdk/index.js +15 -1
  131. package/dist/sdk/index.js.map +1 -1
  132. package/hooks/aria-cognition-substrate-binding.mjs +51 -9
  133. package/hooks/aria-first-class-coach.mjs +129 -0
  134. package/hooks/aria-harness-via-sdk.mjs +33 -6
  135. package/hooks/aria-pre-tool-gate.mjs +86 -8
  136. package/hooks/aria-pre-tool-use.mjs +75 -0
  137. package/hooks/aria-preprompt-consult.mjs +5 -6
  138. package/hooks/aria-preturn-memory-gate.mjs +5 -0
  139. package/hooks/aria-repo-doctrine-gate.mjs +15 -0
  140. package/hooks/aria-stop-gate.mjs +125 -17
  141. package/hooks/doctrine_trigger_map.json +11 -0
  142. package/hooks/lib/emergency-gateoff-impl.mjs +39 -0
  143. package/hooks/lib/emergency-gateoff.mjs +6 -0
  144. package/hooks/lib/first-class-coach.mjs +755 -0
  145. package/hooks/lib/skill-autoload-gate-impl.mjs +103 -0
  146. package/hooks/lib/skill-autoload-gate.mjs +1 -14
  147. package/opencode-plugins/harness-context/auth-token.mjs +126 -0
  148. package/opencode-plugins/harness-context/inject-context.mjs +62 -22
  149. package/opencode-plugins/harness-context/task-project-ledger.mjs +290 -0
  150. package/opencode-plugins/harness-gate/index.js +87 -27
  151. package/opencode-plugins/harness-gate/lib/skill-autoload-gate.js +1 -14
  152. package/opencode-plugins/harness-outcome/index.js +29 -24
  153. package/opencode-plugins/harness-stop/index.js +229 -68
  154. package/opencode-plugins/harness-stop/lib/skill-autoload-gate.js +1 -14
  155. package/package.json +8 -2
  156. package/runtime-src/auth-token.mjs +121 -0
  157. package/runtime-src/coach-kernel.mjs +377 -0
  158. package/runtime-src/codex-bridge.mjs +440 -69
  159. package/runtime-src/local-phase.mjs +8 -0
  160. package/runtime-src/provider-proxy.mjs +136 -33
  161. package/runtime-src/service.mjs +1711 -74
  162. package/scripts/bundle-sdk.mjs +8 -0
  163. package/scripts/check-client-compatibility.mjs +422 -0
  164. package/scripts/check-coach-kernel.mjs +204 -0
  165. package/scripts/check-managed-runtime-ledger.mjs +107 -0
  166. package/scripts/check-opencode-config-contract.mjs +78 -0
  167. package/scripts/check-quality-ledger.mjs +121 -0
  168. package/scripts/self-test-harness-gates.mjs +179 -11
  169. package/scripts/self-test-repo-guard.mjs +38 -0
  170. package/scripts/validate-skill-prompts.mjs +14 -1
  171. package/skills/aria-cognition/aria-essence/SKILL.md +18 -0
  172. package/skills/aria-cognition/aria-forge-guardrails/SKILL.md +18 -0
  173. package/skills/aria-cognition/aria-repo-doctrine/SKILL.md +18 -0
  174. package/skills/aria-cognition/forge-quality-rules/SKILL.md +18 -0
  175. package/skills/aria-cognition/ghazali-8lens/SKILL.md +18 -0
  176. package/skills/aria-cognition/istiqra-induction/SKILL.md +18 -0
  177. package/skills/aria-cognition/ladunni-22/SKILL.md +18 -0
  178. package/skills/aria-cognition/mizan/SKILL.md +18 -0
  179. package/skills/aria-cognition/nadia/SKILL.md +18 -0
  180. package/skills/aria-cognition/nadia-psi/SKILL.md +18 -0
  181. package/skills/aria-cognition/predictor/SKILL.md +18 -0
  182. package/skills/aria-cognition/qiyas-analogy/SKILL.md +18 -0
  183. package/skills/aria-cognition/soul-domains/SKILL.md +18 -0
  184. package/src/auth.ts +136 -1
  185. package/src/chat.ts +13 -8
  186. package/src/config.ts +6 -1
  187. package/src/connectors/claude-code.ts +62 -18
  188. package/src/connectors/codex.ts +288 -32
  189. package/src/connectors/opencode.ts +35 -12
  190. package/src/connectors/repo-guard.ts +117 -172
  191. package/src/connectors/runtime.ts +19 -7
  192. package/src/connectors/shell.ts +12 -8
  193. package/src/harness-client.ts +8 -22
  194. package/src/model-context.ts +6 -0
  195. package/src/providers/types.ts +1 -1
  196. package/src/providers/xai.ts +55 -0
  197. package/src/setup-wizard.ts +1 -0
  198. package/src/types.ts +2 -0
@@ -7,6 +7,7 @@ import { delimiter, dirname, resolve } from 'node:path';
7
7
  import { spawn, spawnSync } from 'node:child_process';
8
8
  import { createRequire } from 'node:module';
9
9
  import { evaluateSkillGate, formatSkillGateBlock } from './hooks/lib/skill-autoload-gate.mjs';
10
+ import { resolveAriaAuthToken } from './auth-token.mjs';
10
11
 
11
12
  const require = createRequire(import.meta.url);
12
13
  const { WebSocketServer, WebSocket } = require('ws');
@@ -96,15 +97,7 @@ function makeEvidenceRef(kind, value, metadata = {}) {
96
97
  }
97
98
 
98
99
  function readRuntimeToken() {
99
- const envToken = process.env.ARIA_HARNESS_TOKEN || process.env.ARIA_API_KEY || process.env.OPENAI_API_KEY || process.env.ARIA_MASTER_TOKEN;
100
- if (envToken) return envToken;
101
- const ownerTokenPath = `${HOME}/.aria/owner-token`;
102
- if (existsSync(ownerTokenPath)) {
103
- try {
104
- return readFileSync(ownerTokenPath, 'utf8').trim();
105
- } catch {}
106
- }
107
- return '';
100
+ return resolveAriaAuthToken({ baseUrl: RUNTIME_BASE_URL }).token;
108
101
  }
109
102
 
110
103
  function runtimeHeaders() {
@@ -143,10 +136,11 @@ function ensureTurnState(threadId, turnId) {
143
136
  userText: '',
144
137
  preReceiptId: null,
145
138
  agentText: '',
146
- bufferedAgentNotifications: [],
147
- firstAgentItemId: null,
148
- traceId: `trace_${randomUUID()}`,
149
- };
139
+ bufferedAgentNotifications: [],
140
+ firstAgentItemId: null,
141
+ preRecorded: false,
142
+ traceId: `trace_${randomUUID()}`,
143
+ };
150
144
  turnState.set(key, state);
151
145
  }
152
146
  return state;
@@ -176,6 +170,180 @@ function extractInputText(input) {
176
170
  return parts.join('\n\n').trim();
177
171
  }
178
172
 
173
+ function inferThreadId(params = {}) {
174
+ return params.threadId || params.thread_id || params.turn?.threadId || params.turn?.thread_id || params.item?.threadId || params.item?.thread_id || null;
175
+ }
176
+
177
+ function inferTurnId(params = {}) {
178
+ return params.turnId || params.turn_id || params.turn?.id || params.item?.turnId || params.item?.turn_id || null;
179
+ }
180
+
181
+ function inferItemId(params = {}) {
182
+ return params.itemId || params.item_id || params.item?.id || null;
183
+ }
184
+
185
+ function collectKnownText(value, out = [], depth = 0) {
186
+ if (depth > 8 || value == null) return out;
187
+ if (typeof value === 'string') {
188
+ if (value.trim()) out.push(value);
189
+ return out;
190
+ }
191
+ if (Array.isArray(value)) {
192
+ for (const item of value) collectKnownText(item, out, depth + 1);
193
+ return out;
194
+ }
195
+ if (typeof value !== 'object') return out;
196
+
197
+ const textKeys = ['delta', 'text', 'rawContent', 'raw_content', 'outputText', 'output_text'];
198
+ for (const key of textKeys) {
199
+ if (typeof value[key] === 'string' && value[key].trim()) out.push(value[key]);
200
+ }
201
+ const containerKeys = ['content', 'message', 'fragments', 'parts', 'summary'];
202
+ for (const key of containerKeys) {
203
+ if (value[key] != null) collectKnownText(value[key], out, depth + 1);
204
+ }
205
+ return out;
206
+ }
207
+
208
+ function isAgentMessageItem(item = {}) {
209
+ const role = String(item.role || '').toLowerCase();
210
+ const type = String(item.type || item.kind || '').replace(/[\s-]/g, '_').toLowerCase();
211
+ return role === 'assistant' || type === 'agent_message' || type === 'agentmessage' || type === 'assistant_message';
212
+ }
213
+
214
+ function extractAssistantTextFromTurn(turn = {}) {
215
+ const items = Array.isArray(turn?.items) ? turn.items : [];
216
+ return items
217
+ .filter((item) => item && typeof item === 'object' && isAgentMessageItem(item))
218
+ .map((item) => (typeof item.text === 'string' ? item.text : collectKnownText(item).join('')))
219
+ .filter((text) => typeof text === 'string' && text.trim())
220
+ .join('\n\n');
221
+ }
222
+
223
+ function extractAssistantTextFromDownstream(message) {
224
+ const method = String(message?.method || '');
225
+ const params = message?.params || {};
226
+ if (method === 'item/agentMessage/delta') {
227
+ return typeof params.delta === 'string' ? params.delta : '';
228
+ }
229
+ if (/agentMessage|agent_message|assistantMessage|assistant_message/i.test(method)) {
230
+ return collectKnownText(params).join('');
231
+ }
232
+ if ((method === 'item/completed' || method === 'item.completed') && isAgentMessageItem(params.item || params)) {
233
+ return collectKnownText(params.item || params).join('');
234
+ }
235
+ return '';
236
+ }
237
+
238
+ function extractCompletionText(notification) {
239
+ const params = notification?.params || {};
240
+ const turnText = extractAssistantTextFromTurn(params.turn);
241
+ if (turnText) return turnText;
242
+ return collectKnownText(params.lastAgentMessage || params.last_agent_message || params.output || params.response || '').join('');
243
+ }
244
+
245
+ function isAssistantOutputNotification(message) {
246
+ const method = String(message?.method || '');
247
+ const params = message?.params || {};
248
+ if (method === 'item/agentMessage/delta') return typeof params.delta === 'string';
249
+ if ((method === 'item/completed' || method === 'item.completed') && isAgentMessageItem(params.item || params)) return true;
250
+ return /agentMessage|agent_message|assistantMessage|assistant_message/i.test(method);
251
+ }
252
+
253
+ function mergeAssistantText(state, text) {
254
+ const next = typeof text === 'string' ? text : '';
255
+ if (!next.trim()) return false;
256
+ const current = String(state.agentText || '');
257
+ if (!current) {
258
+ state.agentText = next;
259
+ return true;
260
+ }
261
+ if (current === next || current.includes(next)) return false;
262
+ if (next.includes(current)) {
263
+ state.agentText = next;
264
+ return true;
265
+ }
266
+ state.agentText = `${current}\n\n${next}`;
267
+ return true;
268
+ }
269
+
270
+ function rememberAssistantNotification(state, message) {
271
+ const method = String(message?.method || '');
272
+ const text = extractAssistantTextFromDownstream(message);
273
+ if (method === 'item/agentMessage/delta' && typeof text === 'string') {
274
+ state.agentText += text;
275
+ } else {
276
+ mergeAssistantText(state, text);
277
+ }
278
+
279
+ const itemId = inferItemId(message?.params || {});
280
+ if (!state.firstAgentItemId && itemId) state.firstAgentItemId = itemId;
281
+ state.bufferedAgentNotifications.push(message);
282
+ }
283
+
284
+ function scrubAssistantText(value, replacement, originalText, depth = 0) {
285
+ if (depth > 10 || value == null) return value;
286
+ if (typeof value === 'string') {
287
+ if (!value.trim()) return value;
288
+ const original = String(originalText || '').trim();
289
+ if (original && (value === original || value.trim() === original)) return replacement;
290
+ if (original && value.includes(original)) return value.split(original).join(replacement);
291
+ return value;
292
+ }
293
+ if (Array.isArray(value)) return value.map((item) => scrubAssistantText(item, replacement, originalText, depth + 1));
294
+ if (typeof value !== 'object') return value;
295
+ const cloned = { ...value };
296
+ for (const [key, entry] of Object.entries(cloned)) {
297
+ if (/^(?:delta|text|rawContent|raw_content|outputText|output_text|lastAgentMessage|last_agent_message|response|output)$/i.test(key)) {
298
+ cloned[key] = scrubAssistantText(entry, replacement, originalText, depth + 1);
299
+ } else if (entry && typeof entry === 'object') {
300
+ cloned[key] = scrubAssistantText(entry, replacement, originalText, depth + 1);
301
+ }
302
+ }
303
+ return cloned;
304
+ }
305
+
306
+ function buildBlockedCompletionNotification(notification, refusalText, originalText) {
307
+ const blocked = scrubAssistantText(notification, refusalText, originalText);
308
+ const params = blocked.params || {};
309
+ const existingTurn = params.turn && typeof params.turn === 'object' ? params.turn : {};
310
+ const existingError = existingTurn.error && typeof existingTurn.error === 'object' ? existingTurn.error : {};
311
+ const turnId = existingTurn.id || params.turnId || params.turn_id || null;
312
+ blocked.params = {
313
+ ...params,
314
+ ariaBlocked: true,
315
+ ariaBlockReason: refusalText,
316
+ turn: {
317
+ ...existingTurn,
318
+ ...(turnId ? { id: turnId } : {}),
319
+ status: 'failed',
320
+ error: {
321
+ ...existingError,
322
+ message: refusalText,
323
+ },
324
+ },
325
+ };
326
+ return blocked;
327
+ }
328
+
329
+ function safeSendJson(socket, message, label) {
330
+ if (socket.readyState !== WebSocket.OPEN) {
331
+ log(`skip send ${label || 'message'} because socket state=${socket.readyState}`);
332
+ return false;
333
+ }
334
+ socket.send(JSON.stringify(message));
335
+ return true;
336
+ }
337
+
338
+ function safeSendRaw(socket, payload, label) {
339
+ if (socket.readyState !== WebSocket.OPEN) {
340
+ log(`skip send ${label || 'raw message'} because socket state=${socket.readyState}`);
341
+ return false;
342
+ }
343
+ socket.send(payload);
344
+ return true;
345
+ }
346
+
179
347
  function deriveActionFromCommand(command = '') {
180
348
  const text = String(command || '').trim();
181
349
  if (!text) return 'write';
@@ -207,6 +375,18 @@ async function validateTurnText(threadId, turnId) {
207
375
  if (!text) {
208
376
  return { ok: false, reason: 'No assistant text exists for this turn yet. Codex must emit readable cognition before action.' };
209
377
  }
378
+ const coach = await recordCoachPhaseForTurn(threadId, turnId, 'post_generation', {
379
+ text,
380
+ evidenceRefs: [makeEvidenceRef('assistant_output', text, { threadId, turnId, traceId: state.traceId })],
381
+ metadata: { requireCognitionBlock: true },
382
+ });
383
+ if (coach?.permitted === false) {
384
+ return {
385
+ ok: false,
386
+ reason: coach.clientMessage || 'Coach Kernel held this Codex output before release.',
387
+ result: { coach },
388
+ };
389
+ }
210
390
  const skillGate = evaluateSkillGate({
211
391
  sessionId: `codex:${threadId}:${turnId}`,
212
392
  surface: 'codex-bridge-output',
@@ -232,6 +412,13 @@ async function validateTurnText(threadId, turnId) {
232
412
  traceId: state.traceId,
233
413
  },
234
414
  });
415
+ await recordCoachPhaseForTurn(threadId, turnId, 'pre_output', {
416
+ text,
417
+ validation: result?.validation || null,
418
+ layer3: result?.layer3 || null,
419
+ evidenceRefs: [makeEvidenceRef('runtime_validation', result, { threadId, turnId, traceId: state.traceId })],
420
+ metadata: { validation_passed: result?.validation?.passed !== false, layer3_pass: result?.layer3?.pass !== false },
421
+ });
235
422
  const pass = result?.pass === true && result?.validation?.passed !== false;
236
423
  return pass
237
424
  ? { ok: true, result }
@@ -244,6 +431,20 @@ async function validateTurnText(threadId, turnId) {
244
431
 
245
432
  async function checkActionAgainstRuntime(action, target, threadId, turnId, metadata = {}) {
246
433
  const state = ensureTurnState(threadId, turnId);
434
+ const coach = await recordCoachPhaseForTurn(threadId, turnId, 'pre_tool', {
435
+ text: target,
436
+ action,
437
+ target,
438
+ evidenceRefs: [makeEvidenceRef('codex_tool_request', { action, target, metadata }, { threadId, turnId, traceId: state.traceId })],
439
+ metadata: { ...metadata, requireVerify: metadata.requireVerify === true },
440
+ });
441
+ if (coach?.permitted === false) {
442
+ return {
443
+ ok: false,
444
+ reason: coach.clientMessage || `Coach Kernel denied ${action} before tool approval`,
445
+ result: { coach },
446
+ };
447
+ }
247
448
  const skillGate = evaluateSkillGate({
248
449
  sessionId: `codex:${threadId}:${turnId}`,
249
450
  surface: 'codex-bridge-action',
@@ -276,9 +477,46 @@ async function checkActionAgainstRuntime(action, target, threadId, turnId, metad
276
477
  return { ok: true, result };
277
478
  }
278
479
 
480
+ async function recordCoachPhaseForTurn(threadId, turnId, phase, patch = {}) {
481
+ const state = ensureTurnState(threadId, turnId);
482
+ try {
483
+ return await postRuntime('/coach/phase', {
484
+ phase,
485
+ requestId: state.traceId,
486
+ sessionId: `codex:${threadId}:${turnId}`,
487
+ surface: 'codex-bridge',
488
+ lane: 'codex_bridge',
489
+ text: typeof patch.text === 'string' ? patch.text : state.userText || state.agentText || '',
490
+ action: patch.action || '',
491
+ target: patch.target || '',
492
+ evidenceRefs: Array.isArray(patch.evidenceRefs) ? patch.evidenceRefs : [],
493
+ validation: patch.validation || null,
494
+ layer3: patch.layer3 || null,
495
+ metadata: {
496
+ threadId,
497
+ turnId,
498
+ traceId: state.traceId,
499
+ bridge: 'codex',
500
+ ...(patch.metadata && typeof patch.metadata === 'object' ? patch.metadata : {}),
501
+ },
502
+ });
503
+ } catch (error) {
504
+ log(`warn coach/${phase} thread=${threadId} turn=${turnId} error="${error instanceof Error ? error.message : String(error)}"`);
505
+ return null;
506
+ }
507
+ }
508
+
279
509
  async function recordMizanPre(threadId, turnId) {
280
510
  const state = ensureTurnState(threadId, turnId);
281
511
  try {
512
+ await recordCoachPhaseForTurn(threadId, turnId, 'pre_turn', {
513
+ text: state.userText || 'codex turn start',
514
+ evidenceRefs: [makeEvidenceRef('user_input', state.userText, { threadId, turnId, traceId: state.traceId })],
515
+ });
516
+ await recordCoachPhaseForTurn(threadId, turnId, 'pre_cognition', {
517
+ text: state.userText || 'codex pre-cognition',
518
+ evidenceRefs: [makeEvidenceRef('codex_pre_cognition_request', state.userText, { threadId, turnId, traceId: state.traceId })],
519
+ });
282
520
  const result = await postRuntime('/mizan/pre', {
283
521
  sessionId: `codex:${threadId}:${turnId}`,
284
522
  packetRequest: {
@@ -299,11 +537,25 @@ async function recordMizanPre(threadId, turnId) {
299
537
  },
300
538
  });
301
539
  state.preReceiptId = result?.receipt?.receiptId || null;
540
+ await recordCoachPhaseForTurn(threadId, turnId, 'post_cognition', {
541
+ text: state.userText || 'codex post-cognition',
542
+ evidenceRefs: [
543
+ makeEvidenceRef('mizan_pre_receipt', result?.receipt || null, { threadId, turnId, traceId: state.traceId }),
544
+ ],
545
+ metadata: { pre_receipt_id: state.preReceiptId },
546
+ });
302
547
  } catch (error) {
303
548
  log(`warn mizan/pre thread=${threadId} turn=${turnId} error="${error instanceof Error ? error.message : String(error)}"`);
304
549
  }
305
550
  }
306
551
 
552
+ async function recordMizanPreOnce(threadId, turnId) {
553
+ const state = ensureTurnState(threadId, turnId);
554
+ if (state.preRecorded) return;
555
+ state.preRecorded = true;
556
+ await recordMizanPre(threadId, turnId);
557
+ }
558
+
307
559
  async function recordMizanPost(threadId, turnId, pass, summary) {
308
560
  const state = ensureTurnState(threadId, turnId);
309
561
  try {
@@ -326,6 +578,11 @@ async function recordMizanPost(threadId, turnId, pass, summary) {
326
578
  traceId: state.traceId,
327
579
  },
328
580
  });
581
+ await recordCoachPhaseForTurn(threadId, turnId, 'claim_or_release', {
582
+ text: state.agentText || summary,
583
+ evidenceRefs: [makeEvidenceRef('codex_release_decision', { pass, summary }, { threadId, turnId, traceId: state.traceId })],
584
+ metadata: { validated_output: pass, summary, pre_receipt_id: state.preReceiptId },
585
+ });
329
586
  } catch (error) {
330
587
  log(`warn mizan/post thread=${threadId} turn=${turnId} error="${error instanceof Error ? error.message : String(error)}"`);
331
588
  }
@@ -359,6 +616,39 @@ function makeGuardianWarning(threadId, message) {
359
616
  };
360
617
  }
361
618
 
619
+ function normalizeBridgeIssue(issue) {
620
+ const raw = String(issue || '').replace(/\s+/g, ' ').trim();
621
+ if (!raw) return '';
622
+ if (/No <cognition>/i.test(raw)) return 'missing readable cognition block';
623
+ if (/missing\s+<applied_cognition>/i.test(raw)) return 'missing applied cognition contract';
624
+ if (/qualitative_drift/i.test(raw)) return 'qualitative drift language needs a measurable predicate';
625
+ if (/premature_task_closeout|feedback_no_premature_task_closeout|done\|complete\|completed\|ready\|verified\|fixed/i.test(raw)) {
626
+ return 'completion/readiness claim conflicts with unresolved blocker state';
627
+ }
628
+ if (/\(\?:/.test(raw)) return 'doctrine matcher triggered; re-author with the named doctrine and concrete evidence';
629
+ return raw.slice(0, 600);
630
+ }
631
+
632
+ function formatCodexRecoveryBlock(surface, reason, issues = []) {
633
+ const blockers = Array.from(new Set([reason, ...issues].map(normalizeBridgeIssue).filter(Boolean)));
634
+ return [
635
+ 'ARIA CODEX RECOVERY CONTRACT',
636
+ `surface: ${surface}`,
637
+ 'status: output held for re-authoring',
638
+ '',
639
+ 'Observed blockers:',
640
+ ...(blockers.length ? blockers.map((item) => `- ${item}`) : ['- Aria validation failed.']),
641
+ '',
642
+ 'Recovery contract:',
643
+ '1. Do not retry the same blocked text.',
644
+ '2. Re-author the answer from the original user request, not from this block report.',
645
+ '3. Include <cognition> and <applied_cognition> when the answer is non-trivial.',
646
+ '4. Use bounded status language when evidence is missing; do not use completion/readiness claims without proof.',
647
+ '5. Name a measurable verification predicate, or explicitly state that verification has not run.',
648
+ '6. Re-submit the corrected answer; if the same blocker repeats twice, escalate with this full recovery contract.',
649
+ ].join('\n');
650
+ }
651
+
362
652
  async function handleApprovalRequest(upstream, downstream, message) {
363
653
  const { id, method, params = {} } = message;
364
654
  const threadId = params.threadId || params.conversationId || 'unknown-thread';
@@ -366,8 +656,8 @@ async function handleApprovalRequest(upstream, downstream, message) {
366
656
  const validation = await validateTurnText(threadId, turnId);
367
657
 
368
658
  if (!validation.ok) {
369
- const reason = `Aria runtime blocked action before tool approval: ${validation.reason}`;
370
- upstream.send(JSON.stringify(makeGuardianWarning(threadId, reason)));
659
+ const reason = formatCodexRecoveryBlock('codex-bridge-approval', validation.reason);
660
+ safeSendJson(upstream, makeGuardianWarning(threadId, reason), 'approval warning');
371
661
  const declineResult =
372
662
  method === 'item/commandExecution/requestApproval'
373
663
  ? { decision: 'decline' }
@@ -378,7 +668,7 @@ async function handleApprovalRequest(upstream, downstream, message) {
378
668
  : method === 'execCommandApproval'
379
669
  ? { decision: 'denied' }
380
670
  : { decision: 'denied' };
381
- downstream.send(JSON.stringify({ id, result: declineResult }));
671
+ safeSendJson(downstream, { id, result: declineResult }, 'approval decline');
382
672
  log(`deny approval method=${method} thread=${threadId} turn=${turnId} reason="${validation.reason}"`);
383
673
  return true;
384
674
  }
@@ -397,23 +687,23 @@ async function handleApprovalRequest(upstream, downstream, message) {
397
687
  requireVerify: action === 'delete' || action === 'deploy',
398
688
  });
399
689
  if (!actionCheck.ok) {
400
- const reason = `Aria runtime denied ${action}: ${actionCheck.reason}`;
401
- upstream.send(JSON.stringify(makeGuardianWarning(threadId, reason)));
402
- downstream.send(JSON.stringify({
690
+ const reason = formatCodexRecoveryBlock('codex-bridge-command', actionCheck.reason || `Aria denied ${action}`);
691
+ safeSendJson(upstream, makeGuardianWarning(threadId, reason), 'command denial warning');
692
+ safeSendJson(downstream, {
403
693
  id,
404
694
  result: method === 'item/commandExecution/requestApproval'
405
695
  ? { decision: 'decline' }
406
696
  : { decision: 'denied' },
407
- }));
697
+ }, 'command denial');
408
698
  log(`deny command method=${method} action=${action} thread=${threadId} turn=${turnId} reason="${actionCheck.reason}"`);
409
699
  return true;
410
700
  }
411
- downstream.send(JSON.stringify({
701
+ safeSendJson(downstream, {
412
702
  id,
413
703
  result: method === 'item/commandExecution/requestApproval'
414
704
  ? { decision: 'accept' }
415
705
  : { decision: 'approved' },
416
- }));
706
+ }, 'command approval');
417
707
  log(`allow command method=${method} action=${action} thread=${threadId} turn=${turnId}`);
418
708
  return true;
419
709
  }
@@ -423,23 +713,23 @@ async function handleApprovalRequest(upstream, downstream, message) {
423
713
  const files = [params.path, params.filePath, params.grantRoot].filter((value) => typeof value === 'string' && value.trim());
424
714
  const actionCheck = await checkActionAgainstRuntime('write', String(target), threadId, turnId, { files });
425
715
  if (!actionCheck.ok) {
426
- const reason = `Aria runtime denied file change: ${actionCheck.reason}`;
427
- upstream.send(JSON.stringify(makeGuardianWarning(threadId, reason)));
428
- downstream.send(JSON.stringify({
716
+ const reason = formatCodexRecoveryBlock('codex-bridge-file-change', actionCheck.reason || 'file change denied');
717
+ safeSendJson(upstream, makeGuardianWarning(threadId, reason), 'file-change denial warning');
718
+ safeSendJson(downstream, {
429
719
  id,
430
720
  result: method === 'item/fileChange/requestApproval'
431
721
  ? { decision: 'decline' }
432
722
  : { decision: 'denied' },
433
- }));
723
+ }, 'file-change denial');
434
724
  log(`deny file-change method=${method} thread=${threadId} turn=${turnId} reason="${actionCheck.reason}"`);
435
725
  return true;
436
726
  }
437
- downstream.send(JSON.stringify({
727
+ safeSendJson(downstream, {
438
728
  id,
439
729
  result: method === 'item/fileChange/requestApproval'
440
730
  ? { decision: 'accept' }
441
731
  : { decision: 'approved' },
442
- }));
732
+ }, 'file-change approval');
443
733
  log(`allow file-change method=${method} thread=${threadId} turn=${turnId}`);
444
734
  return true;
445
735
  }
@@ -455,9 +745,9 @@ async function handleApprovalRequest(upstream, downstream, message) {
455
745
  : [];
456
746
  const actionCheck = await checkActionAgainstRuntime('write', target, threadId, turnId, { files });
457
747
  if (!actionCheck.ok) {
458
- const reason = `Aria runtime denied permissions request: ${actionCheck.reason}`;
459
- upstream.send(JSON.stringify(makeGuardianWarning(threadId, reason)));
460
- downstream.send(JSON.stringify({
748
+ const reason = formatCodexRecoveryBlock('codex-bridge-permissions', actionCheck.reason || 'permissions request denied');
749
+ safeSendJson(upstream, makeGuardianWarning(threadId, reason), 'permissions denial warning');
750
+ safeSendJson(downstream, {
461
751
  id,
462
752
  result: {
463
753
  permissions: {
@@ -471,7 +761,7 @@ async function handleApprovalRequest(upstream, downstream, message) {
471
761
  scope: 'turn',
472
762
  strictAutoReview: true,
473
763
  },
474
- }));
764
+ }, 'permissions denial');
475
765
  log(`deny permissions thread=${threadId} turn=${turnId} reason="${actionCheck.reason}"`);
476
766
  return true;
477
767
  }
@@ -481,16 +771,22 @@ async function handleApprovalRequest(upstream, downstream, message) {
481
771
  }
482
772
 
483
773
  async function releaseTurnOutput(upstream, notification) {
484
- const threadId = notification?.params?.threadId;
485
- const turnId = notification?.params?.turn?.id || notification?.params?.turnId;
774
+ const params = notification?.params || {};
775
+ const threadId = inferThreadId(params);
776
+ const turnId = inferTurnId(params);
486
777
  if (!threadId || !turnId) {
487
- upstream.send(JSON.stringify(notification));
778
+ safeSendJson(upstream, notification, 'turn completion without ids');
488
779
  return;
489
780
  }
490
781
 
491
782
  const state = ensureTurnState(threadId, turnId);
492
- if (state.bufferedAgentNotifications.length === 0) {
493
- upstream.send(JSON.stringify(notification));
783
+ const completionText = extractCompletionText(notification);
784
+ mergeAssistantText(state, completionText);
785
+
786
+ const turnStatus = String(params.turn?.status || params.status || '').toLowerCase();
787
+ const hasAssistantText = String(state.agentText || '').trim().length > 0;
788
+ if (!hasAssistantText && turnStatus && turnStatus !== 'completed') {
789
+ safeSendJson(upstream, notification, 'non-completed turn without assistant output');
494
790
  turnState.delete(`${threadId}:${turnId}`);
495
791
  return;
496
792
  }
@@ -498,29 +794,32 @@ async function releaseTurnOutput(upstream, notification) {
498
794
  const validation = await validateTurnText(threadId, turnId);
499
795
  if (validation.ok) {
500
796
  for (const buffered of state.bufferedAgentNotifications) {
501
- upstream.send(JSON.stringify(buffered));
797
+ safeSendJson(upstream, buffered, 'validated assistant notification');
502
798
  }
503
799
  await recordMizanPost(threadId, turnId, true, 'codex turn validated and released');
800
+ safeSendJson(upstream, notification, 'validated turn completion');
504
801
  log(`release turn thread=${threadId} turn=${turnId} chars=${state.agentText.length}`);
505
802
  } else {
506
- const refusalText = `Aria runtime blocked final output for this Codex turn.\n\n${validation.reason}`;
507
- if (state.firstAgentItemId) {
508
- upstream.send(JSON.stringify({
509
- method: 'item/agentMessage/delta',
510
- params: {
511
- threadId,
512
- turnId,
513
- itemId: state.firstAgentItemId,
514
- delta: refusalText,
515
- },
516
- }));
517
- }
518
- upstream.send(JSON.stringify(makeGuardianWarning(threadId, refusalText)));
803
+ const refusalText = formatCodexRecoveryBlock('codex-bridge-output', validation.reason);
804
+ safeSendJson(upstream, {
805
+ method: 'item/agentMessage/delta',
806
+ params: {
807
+ threadId,
808
+ turnId,
809
+ itemId: state.firstAgentItemId || 'aria-blocked-output',
810
+ delta: refusalText,
811
+ },
812
+ }, 'blocked assistant replacement');
813
+ safeSendJson(upstream, makeGuardianWarning(threadId, refusalText), 'blocked turn warning');
519
814
  await recordMizanPost(threadId, turnId, false, validation.reason);
815
+ safeSendJson(
816
+ upstream,
817
+ buildBlockedCompletionNotification(notification, refusalText, state.agentText || completionText),
818
+ 'blocked turn completion'
819
+ );
520
820
  log(`block turn thread=${threadId} turn=${turnId} reason="${validation.reason}"`);
521
821
  }
522
822
 
523
- upstream.send(JSON.stringify(notification));
524
823
  turnState.delete(`${threadId}:${turnId}`);
525
824
  }
526
825
 
@@ -607,6 +906,65 @@ async function startBridge() {
607
906
  wss.on('connection', async (upstream) => {
608
907
  await ensureDownstreamReady();
609
908
  const downstream = new WebSocket(DOWNSTREAM_URL);
909
+ const pendingDownstreamMessages = [];
910
+ const pendingTurnStarts = new Map();
911
+
912
+ function rememberTurnStart(message) {
913
+ const params = message?.params || {};
914
+ const threadId = params.threadId;
915
+ if (!threadId || message?.id == null) return;
916
+ pendingTurnStarts.set(String(message.id), {
917
+ threadId,
918
+ userText: extractInputText(params.input),
919
+ });
920
+ }
921
+
922
+ function takePendingTurnStart(threadId) {
923
+ for (const [requestId, pending] of pendingTurnStarts.entries()) {
924
+ if (pending.threadId === threadId) {
925
+ pendingTurnStarts.delete(requestId);
926
+ return pending;
927
+ }
928
+ }
929
+ return null;
930
+ }
931
+
932
+ function hydrateTurnState(threadId, turnId, userText) {
933
+ const state = ensureTurnState(threadId, turnId);
934
+ if (!state.userText && userText) state.userText = userText;
935
+ void recordMizanPreOnce(threadId, turnId);
936
+ return state;
937
+ }
938
+
939
+ function hydrateTurnStartResponse(message) {
940
+ if (message?.id == null) return;
941
+ const pending = pendingTurnStarts.get(String(message.id));
942
+ if (!pending) return;
943
+ const result = message.result || {};
944
+ const turn = result.turn || result;
945
+ const turnId = turn?.id || result.turnId || result.turn_id;
946
+ if (!turnId) return;
947
+ pendingTurnStarts.delete(String(message.id));
948
+ hydrateTurnState(pending.threadId, turnId, pending.userText);
949
+ }
950
+
951
+ function sendDownstream(payload) {
952
+ if (downstream.readyState === WebSocket.OPEN) {
953
+ downstream.send(payload);
954
+ return;
955
+ }
956
+ if (downstream.readyState === WebSocket.CONNECTING) {
957
+ pendingDownstreamMessages.push(payload);
958
+ return;
959
+ }
960
+ log(`drop upstream message because downstream state=${downstream.readyState}`);
961
+ }
962
+
963
+ downstream.once('open', () => {
964
+ while (pendingDownstreamMessages.length > 0 && downstream.readyState === WebSocket.OPEN) {
965
+ downstream.send(pendingDownstreamMessages.shift());
966
+ }
967
+ });
610
968
 
611
969
  upstream.once('close', () => {
612
970
  try { downstream.close(); } catch {}
@@ -621,20 +979,18 @@ async function startBridge() {
621
979
  try {
622
980
  message = JSON.parse(raw);
623
981
  } catch {
624
- downstream.send(raw);
982
+ sendDownstream(raw);
625
983
  return;
626
984
  }
627
985
 
628
986
  if (message?.method === 'turn/start' && message?.params?.threadId) {
629
- const state = ensureTurnState(message.params.threadId, String(Date.now()));
630
- state.userText = extractInputText(message.params.input);
987
+ rememberTurnStart(message);
631
988
  if (!message.params.approvalPolicy) {
632
989
  message.params.approvalPolicy = 'untrusted';
633
990
  }
634
- void recordMizanPre(message.params.threadId, state.turnId);
635
991
  }
636
992
 
637
- downstream.send(JSON.stringify(message));
993
+ sendDownstream(JSON.stringify(message));
638
994
  });
639
995
 
640
996
  downstream.on('message', async (data) => {
@@ -643,33 +999,47 @@ async function startBridge() {
643
999
  try {
644
1000
  message = JSON.parse(raw);
645
1001
  } catch {
646
- upstream.send(raw);
1002
+ safeSendRaw(upstream, raw, 'downstream raw message');
647
1003
  return;
648
1004
  }
649
1005
 
1006
+ if (message?.id != null && typeof message?.method !== 'string') {
1007
+ hydrateTurnStartResponse(message);
1008
+ }
1009
+
650
1010
  if (message?.id != null && typeof message?.method === 'string') {
651
1011
  const intercepted = await handleApprovalRequest(upstream, downstream, message).catch((error) => {
652
1012
  const threadId = message?.params?.threadId || 'unknown-thread';
653
1013
  const reason = `Aria Codex bridge approval hook failed: ${error instanceof Error ? error.message : String(error)}`;
654
- upstream.send(JSON.stringify(makeGuardianWarning(threadId, reason)));
1014
+ safeSendJson(upstream, makeGuardianWarning(threadId, reason), 'approval hook failure warning');
655
1015
  log(`error approval method=${message.method} reason="${reason}"`);
656
1016
  return false;
657
1017
  });
658
1018
  if (intercepted) return;
659
1019
  }
660
1020
 
661
- if (message?.method === 'item/agentMessage/delta') {
662
- const { threadId, turnId, itemId, delta } = message.params || {};
663
- if (threadId && turnId && typeof delta === 'string') {
1021
+ if (isAssistantOutputNotification(message)) {
1022
+ const params = message.params || {};
1023
+ const threadId = inferThreadId(params);
1024
+ const turnId = inferTurnId(params);
1025
+ if (threadId && turnId) {
664
1026
  const state = ensureTurnState(threadId, turnId);
665
- state.agentText += delta;
666
- state.bufferedAgentNotifications.push(message);
667
- if (!state.firstAgentItemId && itemId) state.firstAgentItemId = itemId;
1027
+ const pending = !state.userText ? takePendingTurnStart(threadId) : null;
1028
+ if (pending?.userText) state.userText = pending.userText;
1029
+ rememberAssistantNotification(state, message);
668
1030
  return;
669
1031
  }
670
1032
  }
671
1033
 
672
1034
  if (message?.method === 'turn/completed') {
1035
+ const params = message.params || {};
1036
+ const threadId = inferThreadId(params);
1037
+ const turnId = inferTurnId(params);
1038
+ if (threadId && turnId) {
1039
+ const state = ensureTurnState(threadId, turnId);
1040
+ const pending = !state.userText ? takePendingTurnStart(threadId) : null;
1041
+ if (pending?.userText) state.userText = pending.userText;
1042
+ }
673
1043
  await releaseTurnOutput(upstream, message);
674
1044
  return;
675
1045
  }
@@ -678,21 +1048,22 @@ async function startBridge() {
678
1048
  const threadId = message?.params?.threadId;
679
1049
  const turnId = message?.params?.turn?.id || message?.params?.turnId;
680
1050
  if (threadId && turnId) {
1051
+ const pending = takePendingTurnStart(threadId);
681
1052
  const state = ensureTurnState(threadId, turnId);
682
1053
  if (!state.userText) {
683
- state.userText = message?.params?.turn?.input?.map?.((entry) => entry?.text).filter(Boolean).join('\n\n') || '';
1054
+ state.userText = message?.params?.turn?.input?.map?.((entry) => entry?.text).filter(Boolean).join('\n\n') || pending?.userText || '';
684
1055
  }
685
- void recordMizanPre(threadId, turnId);
1056
+ void recordMizanPreOnce(threadId, turnId);
686
1057
  }
687
1058
  }
688
1059
 
689
- upstream.send(JSON.stringify(message));
1060
+ safeSendJson(upstream, message, 'downstream message');
690
1061
  });
691
1062
 
692
1063
  downstream.on('error', (error) => {
693
1064
  log(`downstream socket error ${error.message}`);
694
1065
  try {
695
- upstream.send(JSON.stringify(makeGuardianWarning('codex-bridge', `Codex downstream socket error: ${error.message}`)));
1066
+ safeSendJson(upstream, makeGuardianWarning('codex-bridge', `Codex downstream socket error: ${error.message}`), 'downstream socket warning');
696
1067
  } catch {}
697
1068
  });
698
1069
  upstream.on('error', (error) => log(`upstream socket error ${error.message}`));