ticlawk 0.1.16-dev.9 → 0.1.17-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +17 -3
  2. package/bin/ticlawk.mjs +255 -21
  3. package/package.json +1 -1
  4. package/src/adapters/ticlawk/api.mjs +350 -50
  5. package/src/adapters/ticlawk/credentials.mjs +41 -1
  6. package/src/adapters/ticlawk/index.mjs +248 -130
  7. package/src/adapters/ticlawk/wake-client.mjs +1 -1
  8. package/src/cli/agent-commands.mjs +715 -18
  9. package/src/core/agent-cli-handlers.mjs +556 -18
  10. package/src/core/agent-home.mjs +81 -1
  11. package/src/core/argv.mjs +11 -1
  12. package/src/core/events/worker-events.mjs +32 -36
  13. package/src/core/http.mjs +152 -0
  14. package/src/core/runtime-contract.mjs +0 -1
  15. package/src/core/runtime-env.mjs +7 -0
  16. package/src/core/runtime-support.mjs +130 -78
  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-step-prompt.mjs +98 -0
  20. package/src/runtimes/_shared/goal-task-protocol.mjs +50 -0
  21. package/src/runtimes/_shared/handbook/BASICS.md +27 -0
  22. package/src/runtimes/_shared/handbook/COLLABORATION.md +37 -0
  23. package/src/runtimes/_shared/handbook/COMMUNICATION.md +55 -0
  24. package/src/runtimes/_shared/handbook/DM_SCOPE.md +13 -0
  25. package/src/runtimes/_shared/handbook/GOAL_AUTHORITY.md +47 -0
  26. package/src/runtimes/_shared/handbook/GOAL_TASK_CORE.md +43 -0
  27. package/src/runtimes/_shared/handbook/GROUP_ADMIN_SCOPE.md +21 -0
  28. package/src/runtimes/_shared/handbook/GROUP_MEMBER_SCOPE.md +15 -0
  29. package/src/runtimes/_shared/handbook/SURFACES.md +41 -0
  30. package/src/runtimes/_shared/handbook/TASK_WORKER.md +14 -0
  31. package/src/runtimes/_shared/standing-prompt.mjs +124 -279
  32. package/src/runtimes/_shared/wake-prompt.mjs +268 -0
  33. package/src/runtimes/claude-code/index.mjs +19 -46
  34. package/src/runtimes/claude-code/session.mjs +2 -7
  35. package/src/runtimes/codex/index.mjs +115 -63
  36. package/src/runtimes/codex/session.mjs +2 -12
  37. package/src/runtimes/openclaw/index.mjs +11 -24
  38. package/src/runtimes/opencode/index.mjs +38 -60
  39. package/src/runtimes/opencode/session.mjs +12 -12
  40. package/src/runtimes/pi/index.mjs +38 -60
  41. package/src/runtimes/pi/session.mjs +9 -6
  42. package/ticlawk.mjs +0 -30
@@ -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,18 +21,97 @@ import {
18
21
  requireCodexPath,
19
22
  } from './session.mjs';
20
23
  import { buildCodexInputFromInbound } from '../../core/media/inbound.mjs';
21
- import { emitWorkerEvent } from '../../core/events/worker-events.mjs';
24
+ import { emitWorkerEventBestEffort } from '../../core/events/worker-events.mjs';
22
25
  import {
23
26
  shouldStreamRuntime,
24
- createDeltaAggregator,
25
27
  reportSubprocessFailure,
26
28
  terminalRuntimeFailure,
27
29
  updateBindingRuntimeMeta,
28
30
  isRuntimeGatewayFailure,
31
+ resolveRuntimeSessionScope,
32
+ buildRuntimeSessionMetaPatch,
29
33
  } from '../../core/runtime-support.mjs';
30
34
  import { buildAgentRuntimeEnv } from '../../core/runtime-env.mjs';
31
35
  import { buildStandingPrompt } from '../_shared/standing-prompt.mjs';
32
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
+ }
33
115
 
34
116
  export const codexRuntime = {
35
117
  name: 'codex',
@@ -140,46 +222,42 @@ export const codexRuntime = {
140
222
  ? (codexInput.find((item) => item?.type === 'text')?.text || inbound.text || '(image attached)')
141
223
  : inbound.text;
142
224
 
143
- const shouldRotate = !meta.sessionId || meta.rotatePending;
144
- const deltaAggregator = createDeltaAggregator({
145
- flushDelta: async ({ text, sessionId, turnId, cwd }) => {
146
- await emitWorkerEvent({
147
- adapter,
148
- binding,
149
- agent: this.name,
150
- sessionId: sessionId || meta.sessionId || binding.id,
151
- turnId: turnId || null,
152
- cwd: cwd || agentHome,
153
- replyToMessageId: inbound.messageId || null,
154
- event: {
155
- hook_event_name: 'worker.message.delta',
156
- worker_event_name: 'worker.message.delta',
157
- delta: text,
158
- },
159
- logger: ctx.logger,
160
- });
161
- },
162
- });
225
+ const sessionScope = resolveRuntimeSessionScope(meta, inbound);
226
+ const shouldRotate = sessionScope.shouldRotate;
227
+ const targetSessionId = shouldRotate ? null : sessionScope.sessionId;
163
228
  try {
164
229
  const runtimeCodexPath = requireCodexPath(meta.codexPath || meta.runtimePath);
165
230
  const runtimeCodexVersion = getCodexRuntimeHealth(runtimeCodexPath).version || meta.codexVersion || null;
166
231
  const agentEnv = buildAgentRuntimeEnv({
167
232
  agentId: binding.id,
168
- sessionId: meta.sessionId,
233
+ sessionId: targetSessionId,
169
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,
170
249
  });
171
- const developerInstructions = buildStandingPrompt({ agentId: binding.id });
172
250
  const result = (shouldRotate || inbound.action === 'image' || shouldStreamRuntime(this.name, this))
173
- ? 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, {
174
252
  input: codexInput,
175
253
  developerInstructions,
176
254
  onEvent: async (event) => {
177
255
  if (event?.type === 'turn.started') {
178
- await emitWorkerEvent({
256
+ emitWorkerEventBestEffort({
179
257
  adapter,
180
258
  binding,
181
259
  agent: this.name,
182
- sessionId: event.sessionId || meta.sessionId || binding.id,
260
+ sessionId: event.sessionId || targetSessionId || binding.id,
183
261
  turnId: event.turnId || null,
184
262
  cwd: agentHome,
185
263
  replyToMessageId: inbound.messageId || null,
@@ -189,34 +267,27 @@ export const codexRuntime = {
189
267
  },
190
268
  logger: ctx.logger,
191
269
  });
192
- } else if (event?.type === 'message.delta' && event.text) {
193
- deltaAggregator.push(event.text, {
194
- sessionId: event.sessionId || meta.sessionId || binding.id,
195
- turnId: event.turnId || null,
196
- cwd: agentHome,
197
- });
198
270
  }
199
271
  },
200
272
  })
201
- : await this.runTurn({ sessionId: meta.sessionId, cwd: agentHome, codexPath: runtimeCodexPath, agentEnv }, message, {
273
+ : await this.runTurn({ sessionId: targetSessionId, cwd: agentHome, codexPath: runtimeCodexPath, agentEnv }, message, {
202
274
  input: codexInput,
203
275
  });
204
276
 
205
- await deltaAggregator.flush();
206
277
  const nextBinding = await updateBindingRuntimeMeta(ctx, binding, {
207
- sessionId: result?.sessionId || meta.sessionId,
208
- path: result?.path || meta.path || null,
278
+ ...buildRuntimeSessionMetaPatch(meta, sessionScope, {
279
+ sessionId: result?.sessionId,
280
+ path: result?.path,
281
+ }),
209
282
  runtimePath: runtimeCodexPath,
210
283
  codexPath: runtimeCodexPath,
211
284
  codexVersion: runtimeCodexVersion,
212
- rotatePending: false,
213
- lastRotatedAt: shouldRotate ? new Date().toISOString() : meta.lastRotatedAt,
214
285
  }, { status: 'connected' });
215
- await emitWorkerEvent({
286
+ emitWorkerEventBestEffort({
216
287
  adapter,
217
288
  binding: nextBinding,
218
289
  agent: this.name,
219
- sessionId: result?.sessionId || meta.sessionId || binding.id,
290
+ sessionId: result?.sessionId || targetSessionId || binding.id,
220
291
  turnId: result?.turnId || null,
221
292
  cwd: result?.cwd || agentHome,
222
293
  replyToMessageId: inbound.messageId || null,
@@ -228,7 +299,6 @@ export const codexRuntime = {
228
299
  });
229
300
  return true;
230
301
  } catch (err) {
231
- await deltaAggregator.flush().catch(() => {});
232
302
  const failureInfo = err?.info || {
233
303
  ok: false,
234
304
  kind: 'exit-error',
@@ -243,11 +313,11 @@ export const codexRuntime = {
243
313
  lastGatewayFailureReason: failureInfo.errorMessage || err?.message || 'Codex app-server error',
244
314
  }, { status: 'degraded' }).catch(() => binding);
245
315
  }
246
- await emitWorkerEvent({
316
+ emitWorkerEventBestEffort({
247
317
  adapter,
248
318
  binding: failureBinding,
249
319
  agent: this.name,
250
- sessionId: meta.sessionId || binding.id,
320
+ sessionId: targetSessionId || binding.id,
251
321
  turnId: failureInfo?.turnId || null,
252
322
  cwd: agentHome,
253
323
  replyToMessageId: inbound.messageId || null,
@@ -269,24 +339,6 @@ export const codexRuntime = {
269
339
  }
270
340
  },
271
341
 
272
- async reconcileAfterRestart(binding, ctx) {
273
- const meta = binding.runtimeMeta || {};
274
- await emitWorkerEvent({
275
- adapter: ctx.adapter,
276
- binding,
277
- agent: this.name,
278
- sessionId: meta.sessionId || binding.id,
279
- cwd: ensureAgentHome(binding.id) || '',
280
- event: {
281
- hook_event_name: 'Stop',
282
- worker_event_name: 'worker.turn.complete',
283
- reason: 'connector.restart.reconcile',
284
- },
285
- logger: ctx.logger,
286
- });
287
- return 1;
288
- },
289
-
290
342
  sessionsDir: CODEX_SESSIONS_DIR,
291
343
  maxAgeMs: CODEX_MAX_AGE_MS,
292
344
  };
@@ -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,4 +1,5 @@
1
1
  import { reportSubprocessFailure, terminalRuntimeFailure } from '../../core/runtime-support.mjs';
2
+ import { getGoalTaskProtocolOverlayKey } from '../_shared/goal-task-protocol.mjs';
2
3
  import { buildStandingPrompt } from '../_shared/standing-prompt.mjs';
3
4
  import { GATEWAY_HOST, GATEWAY_PORT } from './identity.mjs';
4
5
 
@@ -24,10 +25,11 @@ async function probeOpenClawGatewayHealth() {
24
25
  import { buildOpenClawSessionKey, normalizeOpenClawAgentId } from './target.mjs';
25
26
  import { askGateway, isGatewayReady, registerOpenClawChannel } from './gateway.mjs';
26
27
 
27
- // Tracks which (agentId, sessionKey) pairs already saw the standing
28
- // prompt this process lifetime. OpenClaw's gateway holds session state
29
- // out of process, so we err on the side of "inject on first observed
30
- // 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.
31
33
  const standingPromptSeen = new Set();
32
34
  import { addInFlight, recoverInFlightEntries, removeInFlight } from './inflight.mjs';
33
35
  import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
@@ -159,13 +161,13 @@ export const openClawRuntime = {
159
161
  const sessionId = String(meta.sessionKey || buildOpenClawSessionKey(agentId)).trim();
160
162
 
161
163
  // Inject the standing prompt on the first observed turn after a
162
- // daemon start for this (agent, session) pair. OpenClaw has no
163
- // separate "system prompt" parameter on the gateway, so we prepend
164
- // exactly once.
165
- 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 })}`;
166
168
  let prompt = rawPrompt;
167
169
  if (!standingPromptSeen.has(standingKey)) {
168
- const standing = buildStandingPrompt({ agentId: binding.id });
170
+ const standing = buildStandingPrompt({ agentId: binding.id, inbound });
169
171
  prompt = `${standing}\n\n---\n\n${rawPrompt}`;
170
172
  standingPromptSeen.add(standingKey);
171
173
  }
@@ -236,21 +238,6 @@ export const openClawRuntime = {
236
238
  return entries.length;
237
239
  },
238
240
 
239
- async reconcileAfterRestart(binding, ctx) {
240
- const meta = binding.runtimeMeta || {};
241
- if (typeof ctx.adapter.emitEvent === 'function') {
242
- await ctx.adapter.emitEvent(binding, {
243
- agent: this.name,
244
- sessionId: String(meta.sessionKey || buildOpenClawSessionKey(meta.agentId || binding.id)).trim(),
245
- cwd: '',
246
- event: {
247
- hook_event_name: 'agent.run.end',
248
- reason: 'connector.restart.reconcile',
249
- },
250
- });
251
- }
252
- return 1;
253
- },
254
241
  };
255
242
 
256
243
  export default openClawRuntime;
@@ -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,15 +25,18 @@ import {
24
25
  requireOpenCodePath,
25
26
  } from './session.mjs';
26
27
  import { buildOpenCodeInputFromInbound } from '../../core/media/inbound.mjs';
27
- import { emitWorkerEvent } from '../../core/events/worker-events.mjs';
28
+ import { emitWorkerEventBestEffort } from '../../core/events/worker-events.mjs';
28
29
  import {
29
30
  shouldStreamRuntime,
30
- createDeltaAggregator,
31
31
  reportSubprocessFailure,
32
32
  terminalRuntimeFailure,
33
33
  updateBindingRuntimeMeta,
34
+ resolveRuntimeSessionScope,
35
+ buildRuntimeSessionMetaPatch,
34
36
  } from '../../core/runtime-support.mjs';
35
37
 
38
+ const standingPromptSeen = new Set();
39
+
36
40
  export const openCodeRuntime = {
37
41
  name: 'opencode',
38
42
 
@@ -48,6 +52,7 @@ export const openCodeRuntime = {
48
52
  opencodePath,
49
53
  agentEnv,
50
54
  standingPrompt: opts.standingPrompt || null,
55
+ forceStandingPrompt: Boolean(opts.forceStandingPrompt),
51
56
  files: opts.files,
52
57
  timeoutMs: opts.timeoutMs,
53
58
  });
@@ -61,6 +66,7 @@ export const openCodeRuntime = {
61
66
  opencodePath,
62
67
  agentEnv,
63
68
  standingPrompt: opts.standingPrompt || null,
69
+ forceStandingPrompt: Boolean(opts.forceStandingPrompt),
64
70
  files: opts.files,
65
71
  timeoutMs: opts.timeoutMs,
66
72
  onEvent: opts.onEvent,
@@ -156,47 +162,39 @@ export const openCodeRuntime = {
156
162
  // instruction so it has something to anchor on.
157
163
  message = captionText || 'Please analyze the attached image(s).';
158
164
  }
159
- const shouldRotate = !meta.sessionId || meta.rotatePending;
160
-
161
- const deltaAggregator = createDeltaAggregator({
162
- flushDelta: async ({ text, sessionId, cwd }) => {
163
- await emitWorkerEvent({
164
- adapter,
165
- binding,
166
- agent: this.name,
167
- sessionId: sessionId || meta.sessionId || binding.id,
168
- cwd: cwd || agentHome,
169
- replyToMessageId: inbound.messageId || null,
170
- event: {
171
- hook_event_name: 'worker.message.delta',
172
- worker_event_name: 'worker.message.delta',
173
- delta: text,
174
- },
175
- logger: ctx.logger,
176
- });
177
- },
178
- });
165
+ const sessionScope = resolveRuntimeSessionScope(meta, inbound);
166
+ const shouldRotate = sessionScope.shouldRotate;
167
+ const targetSessionId = shouldRotate ? null : sessionScope.sessionId;
179
168
 
180
169
  try {
181
170
  const opencodePath = requireOpenCodePath(runtimeOpenCodePath);
182
171
  const opencodeVersion = getOpenCodeRuntimeHealth(opencodePath).version || meta.opencodeVersion || null;
183
172
  const agentEnv = buildAgentRuntimeEnv({
184
173
  agentId: binding.id,
185
- sessionId: meta.sessionId,
174
+ sessionId: targetSessionId,
186
175
  hostId: binding.runtime_host_id,
176
+ conversationId: inbound.conversationId,
177
+ messageId: inbound.messageId,
178
+ target: inbound.envelopeTarget,
187
179
  });
188
- const standingPrompt = buildStandingPrompt({ agentId: binding.id });
180
+ const standingPrompt = buildStandingPrompt({ agentId: binding.id, inbound });
181
+ const protocolOverlayKey = getGoalTaskProtocolOverlayKey({ agentId: binding.id, inbound });
182
+ const standingPromptSeenKey = targetSessionId
183
+ ? `${binding.id}|${targetSessionId}|${protocolOverlayKey}`
184
+ : null;
185
+ const forceStandingPrompt = Boolean(standingPromptSeenKey && !standingPromptSeen.has(standingPromptSeenKey));
189
186
  const result = shouldStreamRuntime(this.name, this)
190
- ? await this.runTurnStream({ sessionId: shouldRotate ? null : meta.sessionId, cwd: agentHome, opencodePath, agentEnv }, message, {
187
+ ? await this.runTurnStream({ sessionId: targetSessionId, cwd: agentHome, opencodePath, agentEnv }, message, {
191
188
  standingPrompt,
189
+ forceStandingPrompt,
192
190
  files,
193
191
  onEvent: async (event) => {
194
192
  if (event?.type === 'turn.started') {
195
- await emitWorkerEvent({
193
+ emitWorkerEventBestEffort({
196
194
  adapter,
197
195
  binding,
198
196
  agent: this.name,
199
- sessionId: event.sessionId || meta.sessionId || binding.id,
197
+ sessionId: event.sessionId || targetSessionId || binding.id,
200
198
  cwd: agentHome,
201
199
  replyToMessageId: inbound.messageId || null,
202
200
  event: {
@@ -205,31 +203,30 @@ export const openCodeRuntime = {
205
203
  },
206
204
  logger: ctx.logger,
207
205
  });
208
- } else if (event?.type === 'message.delta' && event.text) {
209
- deltaAggregator.push(event.text, {
210
- sessionId: event.sessionId || meta.sessionId || binding.id,
211
- cwd: agentHome,
212
- });
213
206
  }
214
207
  },
215
208
  })
216
- : await this.runTurn({ sessionId: shouldRotate ? null : meta.sessionId, cwd: agentHome, opencodePath, agentEnv }, message, { files, standingPrompt });
209
+ : await this.runTurn({ sessionId: targetSessionId, cwd: agentHome, opencodePath, agentEnv }, message, { files, standingPrompt, forceStandingPrompt });
217
210
 
218
- await deltaAggregator.flush();
211
+ const observedSessionId = result?.sessionId || targetSessionId;
212
+ if (observedSessionId) {
213
+ standingPromptSeen.add(`${binding.id}|${observedSessionId}|${protocolOverlayKey}`);
214
+ }
219
215
 
220
216
  const nextBinding = await updateBindingRuntimeMeta(ctx, binding, {
221
- sessionId: result?.sessionId || meta.sessionId,
217
+ ...buildRuntimeSessionMetaPatch(meta, sessionScope, {
218
+ sessionId: result?.sessionId,
219
+ path: result?.path,
220
+ }),
222
221
  runtimePath: opencodePath,
223
222
  opencodePath,
224
223
  opencodeVersion,
225
- rotatePending: false,
226
- lastRotatedAt: shouldRotate ? new Date().toISOString() : meta.lastRotatedAt,
227
224
  }, { status: 'connected' });
228
- await emitWorkerEvent({
225
+ emitWorkerEventBestEffort({
229
226
  adapter,
230
227
  binding: nextBinding,
231
228
  agent: this.name,
232
- sessionId: result?.sessionId || meta.sessionId || binding.id,
229
+ sessionId: result?.sessionId || targetSessionId || binding.id,
233
230
  cwd: result?.cwd || agentHome,
234
231
  replyToMessageId: inbound.messageId || null,
235
232
  event: {
@@ -240,12 +237,11 @@ export const openCodeRuntime = {
240
237
  });
241
238
  return true;
242
239
  } catch (err) {
243
- await deltaAggregator.flush().catch(() => {});
244
- await emitWorkerEvent({
240
+ emitWorkerEventBestEffort({
245
241
  adapter,
246
242
  binding,
247
243
  agent: this.name,
248
- sessionId: meta.sessionId || binding.id,
244
+ sessionId: targetSessionId || binding.id,
249
245
  cwd: agentHome,
250
246
  replyToMessageId: inbound.messageId || null,
251
247
  event: {
@@ -271,24 +267,6 @@ export const openCodeRuntime = {
271
267
  }
272
268
  },
273
269
 
274
- async reconcileAfterRestart(binding, ctx) {
275
- const meta = binding.runtimeMeta || {};
276
- await emitWorkerEvent({
277
- adapter: ctx.adapter,
278
- binding,
279
- agent: this.name,
280
- sessionId: meta.sessionId || binding.id,
281
- cwd: ensureAgentHome(binding.id) || '',
282
- event: {
283
- hook_event_name: 'Stop',
284
- worker_event_name: 'worker.turn.complete',
285
- reason: 'connector.restart.reconcile',
286
- },
287
- logger: ctx.logger,
288
- });
289
- return 1;
290
- },
291
-
292
270
  dataDir: OPENCODE_DATA_DIR,
293
271
  maxAgeMs: OPENCODE_MAX_AGE_MS,
294
272
  };
@@ -193,10 +193,9 @@ function buildOpenCodeRunArgs({ sessionId, message, files = [] }) {
193
193
  /**
194
194
  * Spawn `opencode run` and resolve with the final result.
195
195
  *
196
- * If `onEvent` is provided it is invoked for each parsed event during
197
- * the turn — `{ type: 'turn.started', sessionId }`,
198
- * `{ type: 'message.delta', sessionId, text }`, etc. so callers can
199
- * forward typing indicators / partial text to the chat client.
196
+ * If `onEvent` is provided it is invoked for lifecycle events such as
197
+ * `{ type: 'turn.started', sessionId }`. Token deltas are kept local to
198
+ * build the final text and are not emitted to the caller.
200
199
  *
201
200
  * Resolves with `{ sessionId, cwd, text, durationMs }` on success.
202
201
  * Rejects with an Error whose `.info` carries `{ ok: false, kind, ... }`
@@ -210,14 +209,15 @@ export function runOpenCodePrompt({
210
209
  opencodePath = null,
211
210
  agentEnv = null,
212
211
  standingPrompt = null,
212
+ forceStandingPrompt = false,
213
213
  timeoutMs = Number(process.env.OPENCODE_RUN_TIMEOUT_MS || DEFAULT_OPENCODE_RUN_TIMEOUT_MS),
214
214
  onEvent,
215
215
  }) {
216
216
  // 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
217
+ // standing prompt to the first-turn message body. Callers may force a
218
+ // resumed-session injection when the selected protocol overlay changes
219
+ // or this daemon process has not yet observed the session.
220
+ const finalMessage = standingPrompt && (!sessionId || forceStandingPrompt)
221
221
  ? `${standingPrompt}\n\n---\n\n${message}`
222
222
  : message;
223
223
  return new Promise((resolve, reject) => {
@@ -245,18 +245,19 @@ export function runOpenCodePrompt({
245
245
  let lastError = null;
246
246
  let settled = false;
247
247
  let turnStartedEmitted = false;
248
- let eventChain = Promise.resolve();
249
248
 
250
249
  const emit = (event) => {
251
250
  if (typeof onEvent !== 'function') return;
252
- eventChain = eventChain.then(() => onEvent(event)).catch(() => {});
251
+ void Promise.resolve()
252
+ .then(() => onEvent(event))
253
+ .catch(() => {});
253
254
  };
254
255
 
255
256
  const settle = (fn, value) => {
256
257
  if (settled) return;
257
258
  settled = true;
258
259
  if (timeout) clearTimeout(timeout);
259
- eventChain.catch(() => {}).finally(() => fn(value));
260
+ fn(value);
260
261
  };
261
262
 
262
263
  const handleEvent = (event) => {
@@ -272,7 +273,6 @@ export function runOpenCodePrompt({
272
273
  const delta = extractEventText(event);
273
274
  if (delta) {
274
275
  finalText += delta;
275
- emit({ type: 'message.delta', sessionId: activeSessionId, text: delta });
276
276
  }
277
277
  }
278
278