ticlawk 0.1.16-dev.3 → 0.1.16-dev.30

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 (39) 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 +232 -23
  5. package/src/adapters/ticlawk/credentials.mjs +41 -1
  6. package/src/adapters/ticlawk/index.mjs +196 -195
  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/http.mjs +126 -0
  13. package/src/core/runtime-env.mjs +7 -0
  14. package/src/core/runtime-support.mjs +101 -30
  15. package/src/migrate/write-initial-memory.mjs +5 -5
  16. package/src/runtimes/_shared/agent-handbook.mjs +45 -0
  17. package/src/runtimes/_shared/brand.mjs +2 -0
  18. package/src/runtimes/_shared/goal-task-protocol.mjs +50 -0
  19. package/src/runtimes/_shared/handbook/BASICS.md +27 -0
  20. package/src/runtimes/_shared/handbook/COLLABORATION.md +37 -0
  21. package/src/runtimes/_shared/handbook/COMMUNICATION.md +48 -0
  22. package/src/runtimes/_shared/handbook/DM_SCOPE.md +13 -0
  23. package/src/runtimes/_shared/handbook/GOAL_AUTHORITY.md +43 -0
  24. package/src/runtimes/_shared/handbook/GOAL_TASK_CORE.md +43 -0
  25. package/src/runtimes/_shared/handbook/GROUP_ADMIN_SCOPE.md +21 -0
  26. package/src/runtimes/_shared/handbook/GROUP_MEMBER_SCOPE.md +15 -0
  27. package/src/runtimes/_shared/handbook/SURFACES.md +41 -0
  28. package/src/runtimes/_shared/handbook/TASK_WORKER.md +14 -0
  29. package/src/runtimes/_shared/standing-prompt.mjs +111 -264
  30. package/src/runtimes/_shared/wake-prompt.mjs +261 -0
  31. package/src/runtimes/claude-code/index.mjs +30 -108
  32. package/src/runtimes/codex/index.mjs +114 -23
  33. package/src/runtimes/openclaw/index.mjs +16 -26
  34. package/src/runtimes/opencode/index.mjs +42 -36
  35. package/src/runtimes/opencode/session.mjs +5 -4
  36. package/src/runtimes/pi/index.mjs +39 -31
  37. package/src/runtimes/pi/session.mjs +5 -2
  38. package/src/adapters/ticlawk/cards.mjs +0 -149
  39. package/src/core/media/outbound.mjs +0 -163
@@ -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,98 @@ 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
24
  import { emitWorkerEvent } from '../../core/events/worker-events.mjs';
23
25
  import {
24
26
  shouldStreamRuntime,
25
27
  createDeltaAggregator,
26
- sendAdapterMessage,
27
- recordActivity,
28
28
  reportSubprocessFailure,
29
29
  terminalRuntimeFailure,
30
30
  updateBindingRuntimeMeta,
31
31
  isRuntimeGatewayFailure,
32
+ resolveRuntimeSessionScope,
33
+ buildRuntimeSessionMetaPatch,
32
34
  } from '../../core/runtime-support.mjs';
33
35
  import { buildAgentRuntimeEnv } from '../../core/runtime-env.mjs';
34
36
  import { buildStandingPrompt } from '../_shared/standing-prompt.mjs';
35
37
  import { ensureAgentHome } from '../../core/agent-home.mjs';
38
+ import { debugError, debugLog } from '../../core/logger.mjs';
39
+
40
+ function parseBooleanish(value) {
41
+ if (value === undefined || value === null || value === '') return false;
42
+ return ['1', 'true', 'on', 'yes'].includes(String(value).trim().toLowerCase());
43
+ }
44
+
45
+ function sha256(text) {
46
+ return createHash('sha256').update(String(text || ''), 'utf8').digest('hex');
47
+ }
48
+
49
+ function fileSafeId(value) {
50
+ return String(value || 'unknown')
51
+ .replace(/[^a-zA-Z0-9_-]+/g, '-')
52
+ .replace(/^-+|-+$/g, '')
53
+ .slice(0, 80) || 'unknown';
54
+ }
55
+
56
+ function writePromptSnapshot({
57
+ binding,
58
+ inbound,
59
+ agentHome,
60
+ targetSessionId,
61
+ shouldRotate,
62
+ developerInstructions,
63
+ message,
64
+ input,
65
+ }) {
66
+ if (!parseBooleanish(process.env.TICLAWK_LOG_RUNTIME_PROMPTS)) return;
67
+
68
+ try {
69
+ const rootDir = process.env.TICLAWK_RUNTIME_PROMPT_LOG_DIR
70
+ || join(homedir(), '.ticlawk', 'prompt-logs');
71
+ const dir = join(rootDir, fileSafeId(binding?.id));
72
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
73
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
74
+ const messageId = fileSafeId(inbound?.messageId);
75
+ const filePath = join(dir, `${stamp}-${messageId}.json`);
76
+ const payload = {
77
+ createdAt: new Date().toISOString(),
78
+ runtime: 'codex',
79
+ agentId: binding?.id || null,
80
+ conversationId: inbound?.conversationId || null,
81
+ messageId: inbound?.messageId || null,
82
+ target: inbound?.envelopeTarget || null,
83
+ action: inbound?.action || null,
84
+ sessionId: targetSessionId || null,
85
+ shouldRotate: Boolean(shouldRotate),
86
+ agentHome,
87
+ hashes: {
88
+ developerInstructionsSha256: sha256(developerInstructions),
89
+ messageSha256: sha256(message),
90
+ },
91
+ developerInstructions,
92
+ message,
93
+ input: input || null,
94
+ };
95
+ writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600 });
96
+ debugLog('codex', 'prompt.snapshot', {
97
+ agentId: binding?.id,
98
+ conversationId: inbound?.conversationId,
99
+ messageId: inbound?.messageId,
100
+ target: inbound?.envelopeTarget,
101
+ path: filePath,
102
+ developerInstructionChars: String(developerInstructions || '').length,
103
+ messageChars: String(message || '').length,
104
+ developerInstructionsSha256: payload.hashes.developerInstructionsSha256,
105
+ messageSha256: payload.hashes.messageSha256,
106
+ });
107
+ } catch (err) {
108
+ debugError('codex', 'prompt.snapshot.failed', {
109
+ agentId: binding?.id,
110
+ conversationId: inbound?.conversationId,
111
+ messageId: inbound?.messageId,
112
+ error: err?.message || String(err),
113
+ });
114
+ }
115
+ }
36
116
 
37
117
  export const codexRuntime = {
38
118
  name: 'codex',
@@ -130,7 +210,7 @@ export const codexRuntime = {
130
210
  if (!binding) return false;
131
211
  const adapter = ctx.adapter;
132
212
  const meta = binding.runtimeMeta || {};
133
- // slock-style: cwd is the per-agent home dir under ~/.ticlawk/agents/<id>/.
213
+ // cwd is the per-agent home dir under ~/.ticlawk/agents/<id>/.
134
214
  // ensureAgentHome creates it + seeds MEMORY.md if missing.
135
215
  const agentHome = ensureAgentHome(binding.id, {
136
216
  displayName: binding.display_name || binding.name || null,
@@ -143,14 +223,16 @@ export const codexRuntime = {
143
223
  ? (codexInput.find((item) => item?.type === 'text')?.text || inbound.text || '(image attached)')
144
224
  : inbound.text;
145
225
 
146
- const shouldRotate = !meta.sessionId || meta.rotatePending;
226
+ const sessionScope = resolveRuntimeSessionScope(meta, inbound);
227
+ const shouldRotate = sessionScope.shouldRotate;
228
+ const targetSessionId = shouldRotate ? null : sessionScope.sessionId;
147
229
  const deltaAggregator = createDeltaAggregator({
148
230
  flushDelta: async ({ text, sessionId, turnId, cwd }) => {
149
231
  await emitWorkerEvent({
150
232
  adapter,
151
233
  binding,
152
234
  agent: this.name,
153
- sessionId: sessionId || meta.sessionId || binding.id,
235
+ sessionId: sessionId || targetSessionId || binding.id,
154
236
  turnId: turnId || null,
155
237
  cwd: cwd || agentHome,
156
238
  replyToMessageId: inbound.messageId || null,
@@ -168,12 +250,25 @@ export const codexRuntime = {
168
250
  const runtimeCodexVersion = getCodexRuntimeHealth(runtimeCodexPath).version || meta.codexVersion || null;
169
251
  const agentEnv = buildAgentRuntimeEnv({
170
252
  agentId: binding.id,
171
- sessionId: meta.sessionId,
253
+ sessionId: targetSessionId,
172
254
  hostId: binding.runtime_host_id,
255
+ conversationId: inbound.conversationId,
256
+ messageId: inbound.messageId,
257
+ target: inbound.envelopeTarget,
258
+ });
259
+ const developerInstructions = buildStandingPrompt({ agentId: binding.id, inbound });
260
+ writePromptSnapshot({
261
+ binding,
262
+ inbound,
263
+ agentHome,
264
+ targetSessionId,
265
+ shouldRotate,
266
+ developerInstructions,
267
+ message,
268
+ input: codexInput,
173
269
  });
174
- const developerInstructions = buildStandingPrompt({ agentId: binding.id });
175
270
  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, {
271
+ ? await this.runTurnStream({ sessionId: targetSessionId, cwd: agentHome, codexPath: runtimeCodexPath, agentEnv }, message, {
177
272
  input: codexInput,
178
273
  developerInstructions,
179
274
  onEvent: async (event) => {
@@ -182,7 +277,7 @@ export const codexRuntime = {
182
277
  adapter,
183
278
  binding,
184
279
  agent: this.name,
185
- sessionId: event.sessionId || meta.sessionId || binding.id,
280
+ sessionId: event.sessionId || targetSessionId || binding.id,
186
281
  turnId: event.turnId || null,
187
282
  cwd: agentHome,
188
283
  replyToMessageId: inbound.messageId || null,
@@ -194,36 +289,32 @@ export const codexRuntime = {
194
289
  });
195
290
  } else if (event?.type === 'message.delta' && event.text) {
196
291
  deltaAggregator.push(event.text, {
197
- sessionId: event.sessionId || meta.sessionId || binding.id,
292
+ sessionId: event.sessionId || targetSessionId || binding.id,
198
293
  turnId: event.turnId || null,
199
294
  cwd: agentHome,
200
295
  });
201
296
  }
202
297
  },
203
298
  })
204
- : await this.runTurn({ sessionId: meta.sessionId, cwd: agentHome, codexPath: runtimeCodexPath, agentEnv }, message, {
299
+ : await this.runTurn({ sessionId: targetSessionId, cwd: agentHome, codexPath: runtimeCodexPath, agentEnv }, message, {
205
300
  input: codexInput,
206
301
  });
207
302
 
208
303
  await deltaAggregator.flush();
209
304
  const nextBinding = await updateBindingRuntimeMeta(ctx, binding, {
210
- sessionId: result?.sessionId || meta.sessionId,
211
- path: result?.path || meta.path || null,
305
+ ...buildRuntimeSessionMetaPatch(meta, sessionScope, {
306
+ sessionId: result?.sessionId,
307
+ path: result?.path,
308
+ }),
212
309
  runtimePath: runtimeCodexPath,
213
310
  codexPath: runtimeCodexPath,
214
311
  codexVersion: runtimeCodexVersion,
215
- rotatePending: false,
216
- lastRotatedAt: shouldRotate ? new Date().toISOString() : meta.lastRotatedAt,
217
312
  }, { status: 'connected' });
218
- await recordActivity(adapter, nextBinding, inbound, {
219
- ...result,
220
- media: normalizeOutboundMedia(result),
221
- });
222
313
  await emitWorkerEvent({
223
314
  adapter,
224
315
  binding: nextBinding,
225
316
  agent: this.name,
226
- sessionId: result?.sessionId || meta.sessionId || binding.id,
317
+ sessionId: result?.sessionId || targetSessionId || binding.id,
227
318
  turnId: result?.turnId || null,
228
319
  cwd: result?.cwd || agentHome,
229
320
  replyToMessageId: inbound.messageId || null,
@@ -254,7 +345,7 @@ export const codexRuntime = {
254
345
  adapter,
255
346
  binding: failureBinding,
256
347
  agent: this.name,
257
- sessionId: meta.sessionId || binding.id,
348
+ sessionId: targetSessionId || binding.id,
258
349
  turnId: failureInfo?.turnId || null,
259
350
  cwd: agentHome,
260
351
  replyToMessageId: inbound.messageId || null,
@@ -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
 
@@ -10,6 +10,7 @@
10
10
  import { existsSync } from 'node:fs';
11
11
  import { basename } from 'node:path';
12
12
  import { buildAgentRuntimeEnv } from '../../core/runtime-env.mjs';
13
+ import { getGoalTaskProtocolOverlayKey } from '../_shared/goal-task-protocol.mjs';
13
14
  import { buildStandingPrompt } from '../_shared/standing-prompt.mjs';
14
15
  import { ensureAgentHome } from '../../core/agent-home.mjs';
15
16
  import {
@@ -24,18 +25,19 @@ import {
24
25
  requireOpenCodePath,
25
26
  } from './session.mjs';
26
27
  import { buildOpenCodeInputFromInbound } from '../../core/media/inbound.mjs';
27
- import { normalizeOutboundMedia } from '../../core/media/outbound.mjs';
28
28
  import { emitWorkerEvent } from '../../core/events/worker-events.mjs';
29
29
  import {
30
30
  shouldStreamRuntime,
31
31
  createDeltaAggregator,
32
- sendAdapterMessage,
33
- recordActivity,
34
32
  reportSubprocessFailure,
35
33
  terminalRuntimeFailure,
36
34
  updateBindingRuntimeMeta,
35
+ resolveRuntimeSessionScope,
36
+ buildRuntimeSessionMetaPatch,
37
37
  } from '../../core/runtime-support.mjs';
38
38
 
39
+ const standingPromptSeen = new Set();
40
+
39
41
  export const openCodeRuntime = {
40
42
  name: 'opencode',
41
43
 
@@ -51,6 +53,7 @@ export const openCodeRuntime = {
51
53
  opencodePath,
52
54
  agentEnv,
53
55
  standingPrompt: opts.standingPrompt || null,
56
+ forceStandingPrompt: Boolean(opts.forceStandingPrompt),
54
57
  files: opts.files,
55
58
  timeoutMs: opts.timeoutMs,
56
59
  });
@@ -64,6 +67,7 @@ export const openCodeRuntime = {
64
67
  opencodePath,
65
68
  agentEnv,
66
69
  standingPrompt: opts.standingPrompt || null,
70
+ forceStandingPrompt: Boolean(opts.forceStandingPrompt),
67
71
  files: opts.files,
68
72
  timeoutMs: opts.timeoutMs,
69
73
  onEvent: opts.onEvent,
@@ -146,30 +150,22 @@ export const openCodeRuntime = {
146
150
  const captionText = (inbound.text || '').trim();
147
151
 
148
152
  if (files.length === 0 && !captionText) {
149
- await sendAdapterMessage(adapter, binding, {
150
- type: 'assistant',
151
- text: '⚠️ Image attached, but I could not access the image data and there is no caption to fall back to. Try sending the image again, or include a text caption.',
152
- media: [],
153
- replyToMessageId: inbound.messageId || null,
154
- });
153
+ // Image decode failed and no caption to fall back on — we have
154
+ // nothing meaningful to feed the model. Bail without a user
155
+ // notice; this runtime is non-primary and the dead chat-projection
156
+ // path that used to surface such notices is gone.
155
157
  return true;
156
158
  }
157
-
158
- if (files.length === 0 && captionText) {
159
- // Downloads all failed; tell the user we're proceeding with the caption alone.
160
- await sendAdapterMessage(adapter, binding, {
161
- type: 'assistant',
162
- text: '⚠️ Could not access the attached image data; acting on the caption text only.',
163
- media: [],
164
- replyToMessageId: inbound.messageId || null,
165
- });
166
- }
159
+ // If files.length === 0 && captionText, fall through with the
160
+ // caption-only message below no inline user notice.
167
161
 
168
162
  // If user sent images with no caption, give the model a minimal
169
163
  // instruction so it has something to anchor on.
170
164
  message = captionText || 'Please analyze the attached image(s).';
171
165
  }
172
- const shouldRotate = !meta.sessionId || meta.rotatePending;
166
+ const sessionScope = resolveRuntimeSessionScope(meta, inbound);
167
+ const shouldRotate = sessionScope.shouldRotate;
168
+ const targetSessionId = shouldRotate ? null : sessionScope.sessionId;
173
169
 
174
170
  const deltaAggregator = createDeltaAggregator({
175
171
  flushDelta: async ({ text, sessionId, cwd }) => {
@@ -177,7 +173,7 @@ export const openCodeRuntime = {
177
173
  adapter,
178
174
  binding,
179
175
  agent: this.name,
180
- sessionId: sessionId || meta.sessionId || binding.id,
176
+ sessionId: sessionId || targetSessionId || binding.id,
181
177
  cwd: cwd || agentHome,
182
178
  replyToMessageId: inbound.messageId || null,
183
179
  event: {
@@ -195,13 +191,22 @@ export const openCodeRuntime = {
195
191
  const opencodeVersion = getOpenCodeRuntimeHealth(opencodePath).version || meta.opencodeVersion || null;
196
192
  const agentEnv = buildAgentRuntimeEnv({
197
193
  agentId: binding.id,
198
- sessionId: meta.sessionId,
194
+ sessionId: targetSessionId,
199
195
  hostId: binding.runtime_host_id,
196
+ conversationId: inbound.conversationId,
197
+ messageId: inbound.messageId,
198
+ target: inbound.envelopeTarget,
200
199
  });
201
- const standingPrompt = buildStandingPrompt({ agentId: binding.id });
200
+ const standingPrompt = buildStandingPrompt({ agentId: binding.id, inbound });
201
+ const protocolOverlayKey = getGoalTaskProtocolOverlayKey({ agentId: binding.id, inbound });
202
+ const standingPromptSeenKey = targetSessionId
203
+ ? `${binding.id}|${targetSessionId}|${protocolOverlayKey}`
204
+ : null;
205
+ const forceStandingPrompt = Boolean(standingPromptSeenKey && !standingPromptSeen.has(standingPromptSeenKey));
202
206
  const result = shouldStreamRuntime(this.name, this)
203
- ? await this.runTurnStream({ sessionId: shouldRotate ? null : meta.sessionId, cwd: agentHome, opencodePath, agentEnv }, message, {
207
+ ? await this.runTurnStream({ sessionId: targetSessionId, cwd: agentHome, opencodePath, agentEnv }, message, {
204
208
  standingPrompt,
209
+ forceStandingPrompt,
205
210
  files,
206
211
  onEvent: async (event) => {
207
212
  if (event?.type === 'turn.started') {
@@ -209,7 +214,7 @@ export const openCodeRuntime = {
209
214
  adapter,
210
215
  binding,
211
216
  agent: this.name,
212
- sessionId: event.sessionId || meta.sessionId || binding.id,
217
+ sessionId: event.sessionId || targetSessionId || binding.id,
213
218
  cwd: agentHome,
214
219
  replyToMessageId: inbound.messageId || null,
215
220
  event: {
@@ -220,33 +225,34 @@ export const openCodeRuntime = {
220
225
  });
221
226
  } else if (event?.type === 'message.delta' && event.text) {
222
227
  deltaAggregator.push(event.text, {
223
- sessionId: event.sessionId || meta.sessionId || binding.id,
228
+ sessionId: event.sessionId || targetSessionId || binding.id,
224
229
  cwd: agentHome,
225
230
  });
226
231
  }
227
232
  },
228
233
  })
229
- : await this.runTurn({ sessionId: shouldRotate ? null : meta.sessionId, cwd: agentHome, opencodePath, agentEnv }, message, { files, standingPrompt });
234
+ : await this.runTurn({ sessionId: targetSessionId, cwd: agentHome, opencodePath, agentEnv }, message, { files, standingPrompt, forceStandingPrompt });
230
235
 
231
236
  await deltaAggregator.flush();
237
+ const observedSessionId = result?.sessionId || targetSessionId;
238
+ if (observedSessionId) {
239
+ standingPromptSeen.add(`${binding.id}|${observedSessionId}|${protocolOverlayKey}`);
240
+ }
232
241
 
233
242
  const nextBinding = await updateBindingRuntimeMeta(ctx, binding, {
234
- sessionId: result?.sessionId || meta.sessionId,
243
+ ...buildRuntimeSessionMetaPatch(meta, sessionScope, {
244
+ sessionId: result?.sessionId,
245
+ path: result?.path,
246
+ }),
235
247
  runtimePath: opencodePath,
236
248
  opencodePath,
237
249
  opencodeVersion,
238
- rotatePending: false,
239
- lastRotatedAt: shouldRotate ? new Date().toISOString() : meta.lastRotatedAt,
240
250
  }, { status: 'connected' });
241
- await recordActivity(adapter, nextBinding, inbound, {
242
- ...result,
243
- media: normalizeOutboundMedia(result),
244
- });
245
251
  await emitWorkerEvent({
246
252
  adapter,
247
253
  binding: nextBinding,
248
254
  agent: this.name,
249
- sessionId: result?.sessionId || meta.sessionId || binding.id,
255
+ sessionId: result?.sessionId || targetSessionId || binding.id,
250
256
  cwd: result?.cwd || agentHome,
251
257
  replyToMessageId: inbound.messageId || null,
252
258
  event: {
@@ -262,7 +268,7 @@ export const openCodeRuntime = {
262
268
  adapter,
263
269
  binding,
264
270
  agent: this.name,
265
- sessionId: meta.sessionId || binding.id,
271
+ sessionId: targetSessionId || binding.id,
266
272
  cwd: agentHome,
267
273
  replyToMessageId: inbound.messageId || null,
268
274
  event: {
@@ -210,14 +210,15 @@ export function runOpenCodePrompt({
210
210
  opencodePath = null,
211
211
  agentEnv = null,
212
212
  standingPrompt = null,
213
+ forceStandingPrompt = false,
213
214
  timeoutMs = Number(process.env.OPENCODE_RUN_TIMEOUT_MS || DEFAULT_OPENCODE_RUN_TIMEOUT_MS),
214
215
  onEvent,
215
216
  }) {
216
217
  // opencode has no documented `--system` flag, so we prepend the
217
- // standing prompt to the first-turn message body. Resumed sessions
218
- // (sessionId set) skip injection because the model already saw it
219
- // on the originating turn.
220
- const finalMessage = standingPrompt && !sessionId
218
+ // standing prompt to the first-turn message body. Callers may force a
219
+ // resumed-session injection when the selected protocol overlay changes
220
+ // or this daemon process has not yet observed the session.
221
+ const finalMessage = standingPrompt && (!sessionId || forceStandingPrompt)
221
222
  ? `${standingPrompt}\n\n---\n\n${message}`
222
223
  : message;
223
224
  return new Promise((resolve, reject) => {