ticlawk 0.1.16-dev.3 → 0.1.16-dev.31

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 (42) hide show
  1. package/README.md +14 -2
  2. package/bin/ticlawk.mjs +207 -25
  3. package/package.json +1 -1
  4. package/src/adapters/ticlawk/api.mjs +293 -70
  5. package/src/adapters/ticlawk/credentials.mjs +41 -1
  6. package/src/adapters/ticlawk/index.mjs +199 -199
  7. package/src/adapters/ticlawk/wake-client.mjs +1 -1
  8. package/src/cli/agent-commands.mjs +607 -37
  9. package/src/core/agent-cli-handlers.mjs +449 -20
  10. package/src/core/agent-home.mjs +86 -10
  11. package/src/core/argv.mjs +11 -1
  12. package/src/core/events/worker-events.mjs +32 -36
  13. package/src/core/http.mjs +126 -0
  14. package/src/core/runtime-env.mjs +7 -0
  15. package/src/core/runtime-support.mjs +108 -107
  16. package/src/migrate/write-initial-memory.mjs +5 -5
  17. package/src/runtimes/_shared/agent-handbook.mjs +45 -0
  18. package/src/runtimes/_shared/brand.mjs +2 -0
  19. package/src/runtimes/_shared/goal-task-protocol.mjs +50 -0
  20. package/src/runtimes/_shared/handbook/BASICS.md +27 -0
  21. package/src/runtimes/_shared/handbook/COLLABORATION.md +37 -0
  22. package/src/runtimes/_shared/handbook/COMMUNICATION.md +50 -0
  23. package/src/runtimes/_shared/handbook/DM_SCOPE.md +13 -0
  24. package/src/runtimes/_shared/handbook/GOAL_AUTHORITY.md +43 -0
  25. package/src/runtimes/_shared/handbook/GOAL_TASK_CORE.md +43 -0
  26. package/src/runtimes/_shared/handbook/GROUP_ADMIN_SCOPE.md +21 -0
  27. package/src/runtimes/_shared/handbook/GROUP_MEMBER_SCOPE.md +15 -0
  28. package/src/runtimes/_shared/handbook/SURFACES.md +41 -0
  29. package/src/runtimes/_shared/handbook/TASK_WORKER.md +14 -0
  30. package/src/runtimes/_shared/standing-prompt.mjs +111 -262
  31. package/src/runtimes/_shared/wake-prompt.mjs +261 -0
  32. package/src/runtimes/claude-code/index.mjs +34 -127
  33. package/src/runtimes/claude-code/session.mjs +2 -7
  34. package/src/runtimes/codex/index.mjs +117 -54
  35. package/src/runtimes/codex/session.mjs +2 -12
  36. package/src/runtimes/openclaw/index.mjs +16 -26
  37. package/src/runtimes/opencode/index.mjs +45 -66
  38. package/src/runtimes/opencode/session.mjs +12 -12
  39. package/src/runtimes/pi/index.mjs +42 -60
  40. package/src/runtimes/pi/session.mjs +9 -6
  41. package/src/adapters/ticlawk/cards.mjs +0 -149
  42. package/src/core/media/outbound.mjs +0 -163
@@ -12,7 +12,6 @@ import { buildAgentRuntimeEnv } from '../../core/runtime-env.mjs';
12
12
  import { buildStandingPrompt } from '../_shared/standing-prompt.mjs';
13
13
  import { ensureAgentHome } from '../../core/agent-home.mjs';
14
14
  import {
15
- createCCSession,
16
15
  getClaudeCodeRuntimeHealth,
17
16
  runCCPrompt,
18
17
  streamCCPrompt,
@@ -24,29 +23,22 @@ import {
24
23
  } from './session.mjs';
25
24
  import { discoverSessions } from './transcripts.mjs';
26
25
  import { buildImageMessageFromInbound } from '../../core/media/inbound.mjs';
27
- import { normalizeOutboundMedia } from '../../core/media/outbound.mjs';
28
- import { emitWorkerEvent } from '../../core/events/worker-events.mjs';
26
+ import { emitWorkerEventBestEffort } from '../../core/events/worker-events.mjs';
29
27
  import {
30
28
  shouldStreamRuntime,
31
- sendAdapterMessage,
32
- recordActivity,
33
29
  reportSubprocessFailure,
34
30
  terminalRuntimeFailure,
35
31
  updateBindingRuntimeMeta,
32
+ resolveRuntimeSessionScope,
33
+ buildRuntimeSessionMetaPatch,
36
34
  } from '../../core/runtime-support.mjs';
37
35
 
38
36
  export const claudeCodeRuntime = {
39
37
  name: 'claude_code',
40
38
 
41
- // Start a new Claude session without blocking on local transcript
42
- // discovery. The live turn path trusts stdout/session_id; transcript
43
- // indexing is a separate concern.
44
- async createSession({ projectDir, text, claudePath }) {
45
- return createCCSession({ projectDir, message: text, claudePath });
46
- },
47
-
48
- // Run a Claude turn and wait for the final result on stdout. This is
49
- // the worker-first path used by the adapter for direct reply delivery.
39
+ // Run a Claude turn and wait for the final result on stdout. Used by
40
+ // deliverTurn when streaming is disabled; both fresh sessions
41
+ // (sessionId=null) and resumed sessions go through here.
50
42
  runTurn({ sessionId, projectDir, claudePath, agentEnv }, text, opts = {}) {
51
43
  return runCCPrompt({
52
44
  sessionId,
@@ -130,117 +122,49 @@ export const claudeCodeRuntime = {
130
122
  if (!binding) return false;
131
123
  const adapter = ctx.adapter;
132
124
  const meta = binding.runtimeMeta || {};
133
- // slock-style: cwd is the per-agent home dir under ~/.ticlawk/agents/<id>/.
125
+ // cwd is the per-agent home dir under ~/.ticlawk/agents/<id>/.
134
126
  const projectDir = ensureAgentHome(binding.id, {
135
127
  displayName: binding.display_name || binding.name || null,
136
128
  });
137
- const sessionId = meta.sessionId || binding.id;
138
129
  const runtimeClaudePath = meta.claudePath || meta.runtimePath || null;
139
130
 
140
131
  const message = inbound.action === 'image'
141
132
  ? await buildImageMessageFromInbound(inbound, 'claude-code')
142
133
  : inbound.text;
143
134
 
144
- const shouldRotate = !meta.sessionId || meta.rotatePending;
145
- if (shouldRotate) {
146
- await emitWorkerEvent({
147
- adapter,
148
- binding,
149
- agent: this.name,
150
- sessionId: sessionId || binding.id,
151
- cwd: projectDir,
152
- replyToMessageId: inbound.messageId || null,
153
- event: {
154
- hook_event_name: 'worker.turn.start',
155
- worker_event_name: 'worker.turn.start',
156
- },
157
- logger: ctx.logger,
158
- });
159
- try {
160
- const claudePath = requireClaudePath(runtimeClaudePath);
161
- const claudeVersion = getClaudeCodeRuntimeHealth(claudePath).version || meta.claudeVersion || null;
162
- const created = await this.createSession({ projectDir, text: message, claudePath });
163
- const nextBinding = await updateBindingRuntimeMeta(ctx, binding, {
164
- sessionId: created.sessionId,
165
- path: null,
166
- runtimePath: claudePath,
167
- claudePath,
168
- claudeVersion,
169
- rotatePending: false,
170
- lastRotatedAt: new Date().toISOString(),
171
- }, { status: 'connected' });
172
- if (created.resultText && created.resultText.trim()) {
173
- await sendAdapterMessage(adapter, nextBinding, {
174
- type: 'assistant',
175
- text: created.resultText,
176
- media: [],
177
- replyToMessageId: inbound.messageId || null,
178
- });
179
- }
180
- await emitWorkerEvent({
181
- adapter,
182
- binding: nextBinding,
183
- agent: this.name,
184
- sessionId: created.sessionId,
185
- cwd: projectDir,
186
- replyToMessageId: inbound.messageId || null,
187
- event: {
188
- hook_event_name: 'Stop',
189
- worker_event_name: 'worker.turn.complete',
190
- },
191
- logger: ctx.logger,
192
- });
193
- return true;
194
- } catch (err) {
195
- await emitWorkerEvent({
196
- adapter,
197
- binding,
198
- agent: this.name,
199
- sessionId: sessionId || binding.id,
200
- cwd: projectDir,
201
- replyToMessageId: inbound.messageId || null,
202
- event: {
203
- hook_event_name: 'worker.turn.error',
204
- worker_event_name: 'worker.turn.error',
205
- error: err?.message || 'Claude Code failed',
206
- },
207
- logger: ctx.logger,
208
- });
209
- await reportSubprocessFailure({
210
- adapter,
211
- binding,
212
- inbound,
213
- runtimeName: 'Claude Code',
214
- info: err?.info || {
215
- ok: false,
216
- kind: 'exit-error',
217
- errorMessage: err?.message || 'Claude Code failed',
218
- durationMs: 0,
219
- },
220
- });
221
- return terminalRuntimeFailure(err?.message || 'Claude Code failed');
222
- }
223
- }
135
+ // shouldRotate=true means this conversation has no runtime session
136
+ // yet, or the agent was reset and all scoped sessions are invalid.
137
+ // We pass sessionId=null so `claude` creates a fresh session; the new
138
+ // session_id is captured from stream events and persisted below.
139
+ // Unifying rotate + non-rotate into one path means the standing prompt
140
+ // is always attached, so the agent uses the CLI to reply on every
141
+ // turn including the first.
142
+ const sessionScope = resolveRuntimeSessionScope(meta, inbound);
143
+ const targetSessionId = sessionScope.shouldRotate ? null : sessionScope.sessionId;
144
+ const errEventSessionId = targetSessionId || binding.id;
224
145
 
225
146
  try {
226
147
  const claudePath = requireClaudePath(runtimeClaudePath);
227
148
  const claudeVersion = getClaudeCodeRuntimeHealth(claudePath).version || meta.claudeVersion || null;
228
149
  const agentEnv = buildAgentRuntimeEnv({
229
150
  agentId: binding.id,
230
- sessionId,
151
+ sessionId: targetSessionId,
231
152
  hostId: binding.runtime_host_id,
153
+ conversationId: inbound.conversationId,
154
+ messageId: inbound.messageId,
155
+ target: inbound.envelopeTarget,
232
156
  });
233
- const appendSystemPrompt = buildStandingPrompt({ agentId: binding.id });
157
+ const appendSystemPrompt = buildStandingPrompt({ agentId: binding.id, inbound });
234
158
  const result = shouldStreamRuntime(this.name, this)
235
- ? await this.runTurnStream({ sessionId, projectDir, claudePath, agentEnv }, message, {
159
+ ? await this.runTurnStream({ sessionId: targetSessionId, projectDir, claudePath, agentEnv }, message, {
236
160
  appendSystemPrompt,
237
161
  onEvent: async (event) => {
238
162
  if (event?.type === 'turn.started') {
239
- await emitWorkerEvent({
163
+ emitWorkerEventBestEffort({
240
164
  adapter,
241
165
  binding,
242
166
  agent: this.name,
243
- sessionId: event.sessionId || sessionId,
167
+ sessionId: event.sessionId || targetSessionId || binding.id,
244
168
  cwd: projectDir,
245
169
  replyToMessageId: inbound.messageId || null,
246
170
  event: {
@@ -249,40 +173,23 @@ export const claudeCodeRuntime = {
249
173
  },
250
174
  logger: ctx.logger,
251
175
  });
252
- } else if (event?.type === 'message.delta' && event.text) {
253
- await emitWorkerEvent({
254
- adapter,
255
- binding,
256
- agent: this.name,
257
- sessionId: event.sessionId || sessionId,
258
- cwd: projectDir,
259
- replyToMessageId: inbound.messageId || null,
260
- event: {
261
- hook_event_name: 'worker.message.delta',
262
- worker_event_name: 'worker.message.delta',
263
- delta: event.text,
264
- },
265
- logger: ctx.logger,
266
- });
267
176
  }
268
177
  },
269
178
  })
270
- : await this.runTurn({ sessionId, projectDir, claudePath, agentEnv }, message, { appendSystemPrompt });
179
+ : await this.runTurn({ sessionId: targetSessionId, projectDir, claudePath, agentEnv }, message, { appendSystemPrompt });
271
180
  const nextBinding = await updateBindingRuntimeMeta(ctx, binding, {
272
- sessionId: result?.sessionId || meta.sessionId,
181
+ ...buildRuntimeSessionMetaPatch(meta, sessionScope, {
182
+ sessionId: result?.sessionId,
183
+ }),
273
184
  runtimePath: claudePath,
274
185
  claudePath,
275
186
  claudeVersion,
276
187
  }, { status: 'connected' });
277
- await recordActivity(adapter, nextBinding, inbound, {
278
- ...result,
279
- media: normalizeOutboundMedia(result),
280
- });
281
- await emitWorkerEvent({
188
+ emitWorkerEventBestEffort({
282
189
  adapter,
283
190
  binding: nextBinding,
284
191
  agent: this.name,
285
- sessionId: result?.sessionId || sessionId,
192
+ sessionId: result?.sessionId || targetSessionId || binding.id,
286
193
  cwd: projectDir,
287
194
  replyToMessageId: inbound.messageId || null,
288
195
  event: {
@@ -293,11 +200,11 @@ export const claudeCodeRuntime = {
293
200
  });
294
201
  return true;
295
202
  } catch (err) {
296
- await emitWorkerEvent({
203
+ emitWorkerEventBestEffort({
297
204
  adapter,
298
205
  binding,
299
206
  agent: this.name,
300
- sessionId,
207
+ sessionId: errEventSessionId,
301
208
  cwd: projectDir,
302
209
  replyToMessageId: inbound.messageId || null,
303
210
  event: {
@@ -325,7 +232,7 @@ export const claudeCodeRuntime = {
325
232
 
326
233
  async reconcileAfterRestart(binding, ctx) {
327
234
  const meta = binding.runtimeMeta || {};
328
- await emitWorkerEvent({
235
+ emitWorkerEventBestEffort({
329
236
  adapter: ctx.adapter,
330
237
  binding,
331
238
  agent: this.name,
@@ -231,23 +231,19 @@ export function streamCCPrompt({
231
231
  let seenTurnStart = false;
232
232
  let activeSessionId = sessionId || null;
233
233
  let finalText = '';
234
- let eventChain = Promise.resolve();
235
234
 
236
235
  const emit = (event) => {
237
236
  if (typeof onEvent !== 'function') return;
238
- eventChain = eventChain
237
+ void Promise.resolve()
239
238
  .then(() => onEvent(event))
240
239
  .catch(() => {});
241
- return eventChain;
242
240
  };
243
241
 
244
242
  const settle = (fn, value) => {
245
243
  if (settled) return;
246
244
  settled = true;
247
245
  if (timeout) clearTimeout(timeout);
248
- eventChain
249
- .catch(() => {})
250
- .finally(() => fn(value));
246
+ fn(value);
251
247
  };
252
248
 
253
249
  const parseLine = (line) => {
@@ -267,7 +263,6 @@ export function streamCCPrompt({
267
263
  const deltaText = parsed.event?.delta?.text;
268
264
  if (typeof deltaText === 'string' && deltaText) {
269
265
  finalText += deltaText;
270
- emit({ type: 'message.delta', sessionId: activeSessionId, text: deltaText });
271
266
  }
272
267
  return;
273
268
  }
@@ -5,7 +5,10 @@
5
5
  * and discover local sessions.
6
6
  */
7
7
 
8
- import { basename } from 'node:path';
8
+ import { createHash } from 'node:crypto';
9
+ import { mkdirSync, writeFileSync } from 'node:fs';
10
+ import { homedir } from 'node:os';
11
+ import { basename, join } from 'node:path';
9
12
  import {
10
13
  createCodexSession,
11
14
  runCodexPrompt,
@@ -18,21 +21,97 @@ import {
18
21
  requireCodexPath,
19
22
  } from './session.mjs';
20
23
  import { buildCodexInputFromInbound } from '../../core/media/inbound.mjs';
21
- import { normalizeOutboundMedia } from '../../core/media/outbound.mjs';
22
- import { emitWorkerEvent } from '../../core/events/worker-events.mjs';
24
+ import { emitWorkerEventBestEffort } from '../../core/events/worker-events.mjs';
23
25
  import {
24
26
  shouldStreamRuntime,
25
- createDeltaAggregator,
26
- sendAdapterMessage,
27
- recordActivity,
28
27
  reportSubprocessFailure,
29
28
  terminalRuntimeFailure,
30
29
  updateBindingRuntimeMeta,
31
30
  isRuntimeGatewayFailure,
31
+ resolveRuntimeSessionScope,
32
+ buildRuntimeSessionMetaPatch,
32
33
  } from '../../core/runtime-support.mjs';
33
34
  import { buildAgentRuntimeEnv } from '../../core/runtime-env.mjs';
34
35
  import { buildStandingPrompt } from '../_shared/standing-prompt.mjs';
35
36
  import { ensureAgentHome } from '../../core/agent-home.mjs';
37
+ import { debugError, debugLog } from '../../core/logger.mjs';
38
+
39
+ function parseBooleanish(value) {
40
+ if (value === undefined || value === null || value === '') return false;
41
+ return ['1', 'true', 'on', 'yes'].includes(String(value).trim().toLowerCase());
42
+ }
43
+
44
+ function sha256(text) {
45
+ return createHash('sha256').update(String(text || ''), 'utf8').digest('hex');
46
+ }
47
+
48
+ function fileSafeId(value) {
49
+ return String(value || 'unknown')
50
+ .replace(/[^a-zA-Z0-9_-]+/g, '-')
51
+ .replace(/^-+|-+$/g, '')
52
+ .slice(0, 80) || 'unknown';
53
+ }
54
+
55
+ function writePromptSnapshot({
56
+ binding,
57
+ inbound,
58
+ agentHome,
59
+ targetSessionId,
60
+ shouldRotate,
61
+ developerInstructions,
62
+ message,
63
+ input,
64
+ }) {
65
+ if (!parseBooleanish(process.env.TICLAWK_LOG_RUNTIME_PROMPTS)) return;
66
+
67
+ try {
68
+ const rootDir = process.env.TICLAWK_RUNTIME_PROMPT_LOG_DIR
69
+ || join(homedir(), '.ticlawk', 'prompt-logs');
70
+ const dir = join(rootDir, fileSafeId(binding?.id));
71
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
72
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
73
+ const messageId = fileSafeId(inbound?.messageId);
74
+ const filePath = join(dir, `${stamp}-${messageId}.json`);
75
+ const payload = {
76
+ createdAt: new Date().toISOString(),
77
+ runtime: 'codex',
78
+ agentId: binding?.id || null,
79
+ conversationId: inbound?.conversationId || null,
80
+ messageId: inbound?.messageId || null,
81
+ target: inbound?.envelopeTarget || null,
82
+ action: inbound?.action || null,
83
+ sessionId: targetSessionId || null,
84
+ shouldRotate: Boolean(shouldRotate),
85
+ agentHome,
86
+ hashes: {
87
+ developerInstructionsSha256: sha256(developerInstructions),
88
+ messageSha256: sha256(message),
89
+ },
90
+ developerInstructions,
91
+ message,
92
+ input: input || null,
93
+ };
94
+ writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600 });
95
+ debugLog('codex', 'prompt.snapshot', {
96
+ agentId: binding?.id,
97
+ conversationId: inbound?.conversationId,
98
+ messageId: inbound?.messageId,
99
+ target: inbound?.envelopeTarget,
100
+ path: filePath,
101
+ developerInstructionChars: String(developerInstructions || '').length,
102
+ messageChars: String(message || '').length,
103
+ developerInstructionsSha256: payload.hashes.developerInstructionsSha256,
104
+ messageSha256: payload.hashes.messageSha256,
105
+ });
106
+ } catch (err) {
107
+ debugError('codex', 'prompt.snapshot.failed', {
108
+ agentId: binding?.id,
109
+ conversationId: inbound?.conversationId,
110
+ messageId: inbound?.messageId,
111
+ error: err?.message || String(err),
112
+ });
113
+ }
114
+ }
36
115
 
37
116
  export const codexRuntime = {
38
117
  name: 'codex',
@@ -130,7 +209,7 @@ export const codexRuntime = {
130
209
  if (!binding) return false;
131
210
  const adapter = ctx.adapter;
132
211
  const meta = binding.runtimeMeta || {};
133
- // slock-style: cwd is the per-agent home dir under ~/.ticlawk/agents/<id>/.
212
+ // cwd is the per-agent home dir under ~/.ticlawk/agents/<id>/.
134
213
  // ensureAgentHome creates it + seeds MEMORY.md if missing.
135
214
  const agentHome = ensureAgentHome(binding.id, {
136
215
  displayName: binding.display_name || binding.name || null,
@@ -143,46 +222,42 @@ export const codexRuntime = {
143
222
  ? (codexInput.find((item) => item?.type === 'text')?.text || inbound.text || '(image attached)')
144
223
  : inbound.text;
145
224
 
146
- const shouldRotate = !meta.sessionId || meta.rotatePending;
147
- const deltaAggregator = createDeltaAggregator({
148
- flushDelta: async ({ text, sessionId, turnId, cwd }) => {
149
- await emitWorkerEvent({
150
- adapter,
151
- binding,
152
- agent: this.name,
153
- sessionId: sessionId || meta.sessionId || binding.id,
154
- turnId: turnId || null,
155
- cwd: cwd || agentHome,
156
- replyToMessageId: inbound.messageId || null,
157
- event: {
158
- hook_event_name: 'worker.message.delta',
159
- worker_event_name: 'worker.message.delta',
160
- delta: text,
161
- },
162
- logger: ctx.logger,
163
- });
164
- },
165
- });
225
+ const sessionScope = resolveRuntimeSessionScope(meta, inbound);
226
+ const shouldRotate = sessionScope.shouldRotate;
227
+ const targetSessionId = shouldRotate ? null : sessionScope.sessionId;
166
228
  try {
167
229
  const runtimeCodexPath = requireCodexPath(meta.codexPath || meta.runtimePath);
168
230
  const runtimeCodexVersion = getCodexRuntimeHealth(runtimeCodexPath).version || meta.codexVersion || null;
169
231
  const agentEnv = buildAgentRuntimeEnv({
170
232
  agentId: binding.id,
171
- sessionId: meta.sessionId,
233
+ sessionId: targetSessionId,
172
234
  hostId: binding.runtime_host_id,
235
+ conversationId: inbound.conversationId,
236
+ messageId: inbound.messageId,
237
+ target: inbound.envelopeTarget,
238
+ });
239
+ const developerInstructions = buildStandingPrompt({ agentId: binding.id, inbound });
240
+ writePromptSnapshot({
241
+ binding,
242
+ inbound,
243
+ agentHome,
244
+ targetSessionId,
245
+ shouldRotate,
246
+ developerInstructions,
247
+ message,
248
+ input: codexInput,
173
249
  });
174
- const developerInstructions = buildStandingPrompt({ agentId: binding.id });
175
250
  const result = (shouldRotate || inbound.action === 'image' || shouldStreamRuntime(this.name, this))
176
- ? await this.runTurnStream({ sessionId: shouldRotate ? null : meta.sessionId, cwd: agentHome, codexPath: runtimeCodexPath, agentEnv }, message, {
251
+ ? await this.runTurnStream({ sessionId: targetSessionId, cwd: agentHome, codexPath: runtimeCodexPath, agentEnv }, message, {
177
252
  input: codexInput,
178
253
  developerInstructions,
179
254
  onEvent: async (event) => {
180
255
  if (event?.type === 'turn.started') {
181
- await emitWorkerEvent({
256
+ emitWorkerEventBestEffort({
182
257
  adapter,
183
258
  binding,
184
259
  agent: this.name,
185
- sessionId: event.sessionId || meta.sessionId || binding.id,
260
+ sessionId: event.sessionId || targetSessionId || binding.id,
186
261
  turnId: event.turnId || null,
187
262
  cwd: agentHome,
188
263
  replyToMessageId: inbound.messageId || null,
@@ -192,38 +267,27 @@ export const codexRuntime = {
192
267
  },
193
268
  logger: ctx.logger,
194
269
  });
195
- } else if (event?.type === 'message.delta' && event.text) {
196
- deltaAggregator.push(event.text, {
197
- sessionId: event.sessionId || meta.sessionId || binding.id,
198
- turnId: event.turnId || null,
199
- cwd: agentHome,
200
- });
201
270
  }
202
271
  },
203
272
  })
204
- : await this.runTurn({ sessionId: meta.sessionId, cwd: agentHome, codexPath: runtimeCodexPath, agentEnv }, message, {
273
+ : await this.runTurn({ sessionId: targetSessionId, cwd: agentHome, codexPath: runtimeCodexPath, agentEnv }, message, {
205
274
  input: codexInput,
206
275
  });
207
276
 
208
- await deltaAggregator.flush();
209
277
  const nextBinding = await updateBindingRuntimeMeta(ctx, binding, {
210
- sessionId: result?.sessionId || meta.sessionId,
211
- path: result?.path || meta.path || null,
278
+ ...buildRuntimeSessionMetaPatch(meta, sessionScope, {
279
+ sessionId: result?.sessionId,
280
+ path: result?.path,
281
+ }),
212
282
  runtimePath: runtimeCodexPath,
213
283
  codexPath: runtimeCodexPath,
214
284
  codexVersion: runtimeCodexVersion,
215
- rotatePending: false,
216
- lastRotatedAt: shouldRotate ? new Date().toISOString() : meta.lastRotatedAt,
217
285
  }, { status: 'connected' });
218
- await recordActivity(adapter, nextBinding, inbound, {
219
- ...result,
220
- media: normalizeOutboundMedia(result),
221
- });
222
- await emitWorkerEvent({
286
+ emitWorkerEventBestEffort({
223
287
  adapter,
224
288
  binding: nextBinding,
225
289
  agent: this.name,
226
- sessionId: result?.sessionId || meta.sessionId || binding.id,
290
+ sessionId: result?.sessionId || targetSessionId || binding.id,
227
291
  turnId: result?.turnId || null,
228
292
  cwd: result?.cwd || agentHome,
229
293
  replyToMessageId: inbound.messageId || null,
@@ -235,7 +299,6 @@ export const codexRuntime = {
235
299
  });
236
300
  return true;
237
301
  } catch (err) {
238
- await deltaAggregator.flush().catch(() => {});
239
302
  const failureInfo = err?.info || {
240
303
  ok: false,
241
304
  kind: 'exit-error',
@@ -250,11 +313,11 @@ export const codexRuntime = {
250
313
  lastGatewayFailureReason: failureInfo.errorMessage || err?.message || 'Codex app-server error',
251
314
  }, { status: 'degraded' }).catch(() => binding);
252
315
  }
253
- await emitWorkerEvent({
316
+ emitWorkerEventBestEffort({
254
317
  adapter,
255
318
  binding: failureBinding,
256
319
  agent: this.name,
257
- sessionId: meta.sessionId || binding.id,
320
+ sessionId: targetSessionId || binding.id,
258
321
  turnId: failureInfo?.turnId || null,
259
322
  cwd: agentHome,
260
323
  replyToMessageId: inbound.messageId || null,
@@ -278,7 +341,7 @@ export const codexRuntime = {
278
341
 
279
342
  async reconcileAfterRestart(binding, ctx) {
280
343
  const meta = binding.runtimeMeta || {};
281
- await emitWorkerEvent({
344
+ emitWorkerEventBestEffort({
282
345
  adapter: ctx.adapter,
283
346
  binding,
284
347
  agent: this.name,
@@ -368,14 +368,12 @@ export function streamCodexPrompt({
368
368
  let finalText = '';
369
369
  let threadPath = null;
370
370
  const ignoredChildEventsLogged = new Set();
371
- let eventChain = Promise.resolve();
372
371
 
373
372
  const emit = (event) => {
374
373
  if (typeof onEvent !== 'function') return;
375
- eventChain = eventChain
374
+ void Promise.resolve()
376
375
  .then(() => onEvent(event))
377
376
  .catch(() => {});
378
- return eventChain;
379
377
  };
380
378
 
381
379
  const settle = (fn, value) => {
@@ -387,9 +385,7 @@ export function streamCodexPrompt({
387
385
  }
388
386
  pending.clear();
389
387
  try { child.kill('SIGTERM'); } catch {}
390
- eventChain
391
- .catch(() => {})
392
- .finally(() => fn(value));
388
+ fn(value);
393
389
  };
394
390
 
395
391
  const send = (method, params = {}) => {
@@ -451,12 +447,6 @@ export function streamCodexPrompt({
451
447
  const delta = params?.delta;
452
448
  if (typeof delta === 'string' && delta && isRootContext(params)) {
453
449
  finalText += delta;
454
- emit({
455
- type: 'message.delta',
456
- sessionId: rootThreadId,
457
- turnId: rootTurnId,
458
- text: delta,
459
- });
460
450
  } else if (typeof delta === 'string' && delta) {
461
451
  logIgnoredChildEvent('codex.non-root-agent-delta', params);
462
452
  }
@@ -1,5 +1,5 @@
1
- import { normalizeOutboundMedia } from '../../core/media/outbound.mjs';
2
- import { reportSubprocessFailure, sendAdapterMessage, recordActivity, terminalRuntimeFailure } from '../../core/runtime-support.mjs';
1
+ import { reportSubprocessFailure, terminalRuntimeFailure } from '../../core/runtime-support.mjs';
2
+ import { getGoalTaskProtocolOverlayKey } from '../_shared/goal-task-protocol.mjs';
3
3
  import { buildStandingPrompt } from '../_shared/standing-prompt.mjs';
4
4
  import { GATEWAY_HOST, GATEWAY_PORT } from './identity.mjs';
5
5
 
@@ -25,10 +25,11 @@ async function probeOpenClawGatewayHealth() {
25
25
  import { buildOpenClawSessionKey, normalizeOpenClawAgentId } from './target.mjs';
26
26
  import { askGateway, isGatewayReady, registerOpenClawChannel } from './gateway.mjs';
27
27
 
28
- // Tracks which (agentId, sessionKey) pairs already saw the standing
29
- // prompt this process lifetime. OpenClaw's gateway holds session state
30
- // out of process, so we err on the side of "inject on first observed
31
- // turn after daemon restart"; the gateway dedupes redundant context.
28
+ // Tracks which (agentId, sessionKey, protocol overlay) combinations
29
+ // already saw the standing prompt this process lifetime. OpenClaw's
30
+ // gateway holds session state out of process, so we err on the side of
31
+ // "inject on first observed overlay after daemon restart"; the gateway
32
+ // dedupes redundant context.
32
33
  const standingPromptSeen = new Set();
33
34
  import { addInFlight, recoverInFlightEntries, removeInFlight } from './inflight.mjs';
34
35
  import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
@@ -160,13 +161,13 @@ export const openClawRuntime = {
160
161
  const sessionId = String(meta.sessionKey || buildOpenClawSessionKey(agentId)).trim();
161
162
 
162
163
  // Inject the standing prompt on the first observed turn after a
163
- // daemon start for this (agent, session) pair. OpenClaw has no
164
- // separate "system prompt" parameter on the gateway, so we prepend
165
- // exactly once.
166
- const standingKey = `${agentId}|${sessionId}`;
164
+ // daemon start for this (agent, session, protocol overlay). OpenClaw
165
+ // has no separate "system prompt" parameter on the gateway, so we
166
+ // prepend through the prompt body.
167
+ const standingKey = `${agentId}|${sessionId}|${getGoalTaskProtocolOverlayKey({ agentId: binding.id, inbound })}`;
167
168
  let prompt = rawPrompt;
168
169
  if (!standingPromptSeen.has(standingKey)) {
169
- const standing = buildStandingPrompt({ agentId: binding.id });
170
+ const standing = buildStandingPrompt({ agentId: binding.id, inbound });
170
171
  prompt = `${standing}\n\n---\n\n${rawPrompt}`;
171
172
  standingPromptSeen.add(standingKey);
172
173
  }
@@ -192,10 +193,6 @@ export const openClawRuntime = {
192
193
  });
193
194
  },
194
195
  });
195
- await recordActivity(adapter, binding, inbound, {
196
- ...result,
197
- media: normalizeOutboundMedia(result),
198
- });
199
196
  return true;
200
197
  } catch (err) {
201
198
  if (typeof adapter.emitEvent === 'function') {
@@ -233,18 +230,11 @@ export const openClawRuntime = {
233
230
  }
234
231
  },
235
232
 
236
- async recoverInFlight(ctx) {
233
+ async recoverInFlight() {
234
+ // Just drain the persisted in-flight set — the user-facing notice
235
+ // that used to surface here went through the dead chat-projection
236
+ // path. OpenClaw is non-primary; this no-ops cleanly for now.
237
237
  const entries = recoverInFlightEntries();
238
- for (const entry of entries) {
239
- const binding = ctx.getBinding(entry.bindingId);
240
- if (!binding) continue;
241
- await sendAdapterMessage(ctx.adapter, binding, {
242
- type: 'assistant',
243
- text: '⚠️ Lost while ticlawk restarted.\n\nThis OpenClaw message was in flight when ticlawk restarted. Please retry your message.',
244
- media: [],
245
- replyToMessageId: entry.messageId || null,
246
- }).catch(() => {});
247
- }
248
238
  return entries.length;
249
239
  },
250
240