ticlawk 0.1.16-dev.30 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ticlawk",
3
- "version": "0.1.16-dev.30",
3
+ "version": "0.1.16-dev.31",
4
4
  "description": "Local connector that links agent harnesses (Claude Code, Codex, OpenClaw, opencode, Pi) to the Ticlawk mobile app.",
5
5
  "type": "module",
6
6
  "main": "ticlawk.mjs",
@@ -30,8 +30,9 @@ export function getConnectorWsUrl() {
30
30
  return DEFAULT_CONNECTOR_WS_URL;
31
31
  }
32
32
 
33
- // Agent event writes must preserve per-agent order while still allowing
34
- // different agents to proceed concurrently.
33
+ // Non-terminal event writes are serialized per agent for stable logs.
34
+ // Terminal turn events bypass this queue so telemetry cannot hold up the
35
+ // delivery lifecycle.
35
36
  const agentEventQueues = new Map(); // agentId -> Promise
36
37
 
37
38
  export class TiclawkUpdateRequiredError extends Error {
@@ -710,58 +711,71 @@ export async function setWorkstreamCharter({ actingAgentId, conversationId, char
710
711
  export async function postEvent({ agent, agent_id, runtime_host_id, session_id, cwd, runtime_version, event, required = false }) {
711
712
  const agentId = agent_id || null;
712
713
  const queueKey = agentId || `${agent}:${session_id || ''}`;
713
- const previous = agentEventQueues.get(queueKey) || Promise.resolve(null);
714
714
  const eventName = event?.worker_event_name || event?.hook_event_name || event?.event_name || 'unknown';
715
715
  const turnId = event?.turn_id || event?.reply_to_message_id || null;
716
716
  const seq = event?.event_seq ?? null;
717
717
  const deltaChars = typeof event?.delta === 'string' ? event.delta.length : null;
718
718
 
719
+ if (eventName === 'worker.message.delta') {
720
+ debugLog('events', 'delta.drop', {
721
+ agent,
722
+ agentId,
723
+ sessionId: shortId(session_id),
724
+ turnId: shortId(turnId),
725
+ deltaChars,
726
+ });
727
+ return null;
728
+ }
729
+
730
+ const postOnce = async () => {
731
+ const startedAt = Date.now();
732
+ try {
733
+ const result = await apiFetch('/api/events', {
734
+ method: 'POST',
735
+ body: JSON.stringify({ agent, agent_id: agentId, runtime_host_id, session_id, cwd, runtime_version, event }),
736
+ timeout: 5000,
737
+ });
738
+ if (required && result?.matched === false) {
739
+ throw new Error(`event was not matched (${eventName})`);
740
+ }
741
+ debugLog('events', 'post.ok', {
742
+ agent,
743
+ agentId,
744
+ sessionId: shortId(session_id),
745
+ runtimeVersion: runtime_version ?? null,
746
+ turnId: shortId(turnId),
747
+ eventName,
748
+ seq,
749
+ durationMs: Date.now() - startedAt,
750
+ });
751
+ return result;
752
+ } catch (err) {
753
+ debugError('events', 'post.failed', {
754
+ agent,
755
+ agentId,
756
+ sessionId: shortId(session_id),
757
+ runtimeVersion: runtime_version ?? null,
758
+ turnId: shortId(turnId),
759
+ eventName,
760
+ seq,
761
+ durationMs: Date.now() - startedAt,
762
+ error: err?.message || 'unknown error',
763
+ });
764
+ if (required) {
765
+ throw err;
766
+ }
767
+ return null;
768
+ }
769
+ };
770
+
771
+ if (eventName === 'worker.turn.complete' || eventName === 'worker.turn.error') {
772
+ return postOnce();
773
+ }
774
+
775
+ const previous = agentEventQueues.get(queueKey) || Promise.resolve(null);
719
776
  const queued = previous
720
777
  .catch(() => null)
721
- .then(async () => {
722
- const startedAt = Date.now();
723
- try {
724
- const result = await apiFetch('/api/events', {
725
- method: 'POST',
726
- body: JSON.stringify({ agent, agent_id: agentId, runtime_host_id, session_id, cwd, runtime_version, event }),
727
- timeout: 5000,
728
- });
729
- if (required && result?.matched === false) {
730
- throw new Error(`event was not matched (${eventName})`);
731
- }
732
- debugLog('events', 'post.ok', {
733
- agent,
734
- agentId,
735
- sessionId: shortId(session_id),
736
- runtimeVersion: runtime_version ?? null,
737
- turnId: shortId(turnId),
738
- eventName,
739
- seq,
740
- deltaChars,
741
- durationMs: Date.now() - startedAt,
742
- });
743
- return result;
744
- } catch (err) {
745
- debugError('events', 'post.failed', {
746
- agent,
747
- agentId,
748
- sessionId: shortId(session_id),
749
- runtimeVersion: runtime_version ?? null,
750
- turnId: shortId(turnId),
751
- eventName,
752
- seq,
753
- deltaChars,
754
- durationMs: Date.now() - startedAt,
755
- error: err?.message || 'unknown error',
756
- });
757
- if (required) {
758
- throw err;
759
- }
760
- // Best-effort by default. Callers that need a hard failure set
761
- // `required: true` (used for the final reply path).
762
- return null;
763
- }
764
- });
778
+ .then(postOnce);
765
779
 
766
780
  agentEventQueues.set(queueKey, queued);
767
781
  queued.finally(() => {
@@ -1168,10 +1168,9 @@ export function createTiclawkAdapter(ctx) {
1168
1168
  id: 'ticlawk',
1169
1169
 
1170
1170
  async start() {
1171
- // No stale-delivery recovery anywhere if the daemon is killed
1172
- // mid-claim, the row stays `claimed` forever. lease_expires_at
1173
- // was dropped in X1; no cron has replaced it. Rare in practice;
1174
- // when it happens the fix is a one-row UPDATE in supabase.
1171
+ // Stale claimed deliveries are recovered conservatively by the
1172
+ // claim RPC; this startup path only refreshes local bindings and
1173
+ // asks for the next drain.
1175
1174
  await refreshBindings('startup');
1176
1175
  await syncCredentials('startup');
1177
1176
  await requestDrain('startup');
@@ -1,22 +1,9 @@
1
- import { createHash } from 'node:crypto';
2
-
3
1
  const workerEventSeq = new Map();
4
- const DELTA_LOG_PREVIEW_CHARS = 48;
5
2
 
6
3
  function shortId(value) {
7
4
  return value ? String(value).slice(0, 8) : null;
8
5
  }
9
6
 
10
- function hashText(text) {
11
- return createHash('sha1').update(String(text)).digest('hex').slice(0, 12);
12
- }
13
-
14
- function previewDelta(text) {
15
- const normalized = String(text).replace(/\n/g, '\\n');
16
- if (normalized.length <= DELTA_LOG_PREVIEW_CHARS) return normalized;
17
- return normalized.slice(0, DELTA_LOG_PREVIEW_CHARS) + '…';
18
- }
19
-
20
7
  function nextWorkerEventMeta({ agent, sessionId, turnId }) {
21
8
  const key = `${agent}:${sessionId || ''}:${turnId || 'session'}`;
22
9
  const seq = (workerEventSeq.get(key) || 0) + 1;
@@ -37,6 +24,16 @@ export async function emitWorkerEvent({ adapter, binding, agent, sessionId, cwd
37
24
  if (!sessionId || typeof adapter.emitEvent !== 'function') return;
38
25
  const eventName = event?.worker_event_name || event?.hook_event_name || 'unknown';
39
26
  const logicalTurnId = replyToMessageId || turnId || event?.turn_id || null;
27
+ if (eventName === 'worker.message.delta') {
28
+ logger?.debugLog?.('events', 'delta.drop', {
29
+ bindingId: binding.id,
30
+ agent,
31
+ sessionId: shortId(sessionId),
32
+ turnId: shortId(logicalTurnId),
33
+ chars: typeof event?.delta === 'string' ? event.delta.length : null,
34
+ });
35
+ return;
36
+ }
40
37
  const { key, seq, originTs } = nextWorkerEventMeta({ agent, sessionId, turnId: logicalTurnId });
41
38
  const enrichedEvent = {
42
39
  ...event,
@@ -45,29 +42,14 @@ export async function emitWorkerEvent({ adapter, binding, agent, sessionId, cwd
45
42
  event_seq: seq,
46
43
  origin_ts: originTs,
47
44
  };
48
- if (typeof event?.delta === 'string' && event.delta) {
49
- enrichedEvent.delta_chars = event.delta.length;
50
- enrichedEvent.delta_hash = hashText(event.delta);
51
- logger?.debugLog?.('events', 'delta.recv', {
52
- bindingId: binding.id,
53
- agent,
54
- sessionId: shortId(sessionId),
55
- turnId: shortId(logicalTurnId),
56
- seq,
57
- chars: event.delta.length,
58
- hash: enrichedEvent.delta_hash,
59
- preview: previewDelta(event.delta),
60
- });
61
- } else {
62
- logger?.debugLog?.('events', 'event.recv', {
63
- bindingId: binding.id,
64
- agent,
65
- sessionId: shortId(sessionId),
66
- turnId: shortId(logicalTurnId),
67
- seq,
68
- eventName,
69
- });
70
- }
45
+ logger?.debugLog?.('events', 'event.recv', {
46
+ bindingId: binding.id,
47
+ agent,
48
+ sessionId: shortId(sessionId),
49
+ turnId: shortId(logicalTurnId),
50
+ seq,
51
+ eventName,
52
+ });
71
53
  await adapter.emitEvent(binding, {
72
54
  agent,
73
55
  sessionId,
@@ -78,3 +60,17 @@ export async function emitWorkerEvent({ adapter, binding, agent, sessionId, cwd
78
60
  clearWorkerEventMeta(key);
79
61
  }
80
62
  }
63
+
64
+ export function emitWorkerEventBestEffort(args) {
65
+ void emitWorkerEvent(args).catch((err) => {
66
+ const event = args?.event || {};
67
+ const eventName = event.worker_event_name || event.hook_event_name || 'unknown';
68
+ args?.logger?.debugError?.('events', 'event.best-effort-failed', {
69
+ bindingId: args?.binding?.id || null,
70
+ agent: args?.agent || null,
71
+ sessionId: shortId(args?.sessionId),
72
+ eventName,
73
+ error: err?.message || 'unknown error',
74
+ });
75
+ });
76
+ }
@@ -1,8 +1,7 @@
1
1
  import { getStreamingMode } from './config.mjs';
2
2
  import { getAgentHome } from './agent-home.mjs';
3
+ import { debugError } from './logger.mjs';
3
4
  const ERROR_MAX_CHARS = 500;
4
- const DEFAULT_DELTA_FLUSH_MS = 250;
5
- const DEFAULT_DELTA_FLUSH_CHARS = 64;
6
5
  const MAX_SCOPED_RUNTIME_SESSIONS = 50;
7
6
 
8
7
  function truncateError(text) {
@@ -75,12 +74,21 @@ export async function reportSubprocessFailure({ adapter, binding, inbound, runti
75
74
  // skips fan-out to member-role agents, breaking what would otherwise
76
75
  // be a failure→fan-out→failure cascade when several agents share a
77
76
  // broken runtime.
78
- await adapter.postAgentReply(binding, {
79
- conversationId: inbound?.conversationId || null,
80
- text,
81
- replyToMessageId: inbound?.messageId || null,
82
- visibility: 'admin',
83
- });
77
+ try {
78
+ await adapter.postAgentReply(binding, {
79
+ conversationId: inbound?.conversationId || null,
80
+ text,
81
+ replyToMessageId: inbound?.messageId || null,
82
+ visibility: 'admin',
83
+ });
84
+ } catch (err) {
85
+ debugError('runtime', 'failure-notice.failed', {
86
+ agentId: binding?.id || null,
87
+ runtimeName,
88
+ messageId: inbound?.messageId || null,
89
+ error: err?.message || 'unknown error',
90
+ });
91
+ }
84
92
  }
85
93
 
86
94
  export function terminalRuntimeFailure(reason = 'runtime failure') {
@@ -205,81 +213,3 @@ export function buildRuntimeSessionMetaPatch(meta = {}, scope = {}, result = {})
205
213
  lastRotatedAt,
206
214
  };
207
215
  }
208
-
209
- export function createDeltaAggregator({
210
- flushDelta,
211
- flushMs = DEFAULT_DELTA_FLUSH_MS,
212
- flushChars = DEFAULT_DELTA_FLUSH_CHARS,
213
- }) {
214
- let buffer = '';
215
- let pendingMeta = null;
216
- let timer = null;
217
- let flushChain = Promise.resolve();
218
-
219
- const clearTimer = () => {
220
- if (!timer) return;
221
- clearTimeout(timer);
222
- timer = null;
223
- };
224
-
225
- const startFlush = () => {
226
- if (!buffer || typeof flushDelta !== 'function') return flushChain;
227
- const text = buffer;
228
- const meta = pendingMeta || {};
229
- buffer = '';
230
- pendingMeta = null;
231
- clearTimer();
232
- flushChain = flushChain
233
- .catch(() => {})
234
- .then(() => flushDelta({ text, ...meta }));
235
- return flushChain;
236
- };
237
-
238
- const scheduleFlush = () => {
239
- if (!buffer || timer) return;
240
- timer = setTimeout(() => {
241
- timer = null;
242
- void startFlush();
243
- }, flushMs);
244
- timer.unref?.();
245
- };
246
-
247
- return {
248
- push(text, meta = {}) {
249
- const normalized = typeof text === 'string' ? text : '';
250
- if (!normalized) return;
251
-
252
- const nextMeta = {
253
- sessionId: meta.sessionId || null,
254
- turnId: meta.turnId || null,
255
- cwd: meta.cwd || '',
256
- };
257
-
258
- const sameContext = !pendingMeta
259
- || (
260
- pendingMeta.sessionId === nextMeta.sessionId
261
- && pendingMeta.turnId === nextMeta.turnId
262
- && pendingMeta.cwd === nextMeta.cwd
263
- );
264
-
265
- if (!sameContext && buffer) {
266
- void startFlush();
267
- }
268
-
269
- pendingMeta = nextMeta;
270
- buffer += normalized;
271
-
272
- if (buffer.length >= flushChars) {
273
- void startFlush();
274
- return;
275
- }
276
-
277
- scheduleFlush();
278
- },
279
-
280
- async flush() {
281
- clearTimer();
282
- await startFlush();
283
- },
284
- };
285
- }
@@ -24,6 +24,8 @@ The reply target is the precise chat destination for this message. Treat it as a
24
24
  - When reporting a result to a human, first decide whether the result itself is a file or artifact. If it is, attach it. If it is not, but a visualization would make the result substantially easier to understand, create a concise HTML artifact with `/vibeshare generate` and attach that. Do not create artifacts for ordinary text answers.
25
25
  - Keep external messages clean and actionable: answer, instruction, blocker, decision request, or final result.
26
26
  - Do not send private scratchpad unless the owner explicitly asks for that analysis.
27
+ - When the work is complete or blocked, send one concise final recipient-facing summary through `ticlawk message send --target <target>`.
28
+ - After that final send succeeds, output exactly `<turn_end>` in normal assistant output and stop. Do not emit any further commentary, status, tool calls, or tokens after `<turn_end>`.
27
29
 
28
30
  ## Reading Context
29
31
 
@@ -88,7 +88,9 @@ function buildReadInstructions(ctx = {}) {
88
88
  const handbookFiles = files.filter((name) => name !== 'MEMORY.md');
89
89
  const memoryList = memoryFiles.map((name, index) => `${index + 1}. \`${name}\``).join('\n');
90
90
  const handbookList = handbookFiles.map((name, index) => `${index + 1}. \`${name}\``).join('\n');
91
- return `Read this file every time before acting because it may be updated:
91
+ return `To reply to the user or group, use \`ticlawk message send --target <target>\`. Normal assistant output is private and is not sent to the user. Details are in \`COMMUNICATION.md\`.
92
+
93
+ Read this file every time before acting because it may be updated:
92
94
  ${memoryList}
93
95
 
94
96
  Read these files if you haven't already. Otherwise, skip:
@@ -23,7 +23,7 @@ import {
23
23
  } from './session.mjs';
24
24
  import { discoverSessions } from './transcripts.mjs';
25
25
  import { buildImageMessageFromInbound } from '../../core/media/inbound.mjs';
26
- import { emitWorkerEvent } from '../../core/events/worker-events.mjs';
26
+ import { emitWorkerEventBestEffort } from '../../core/events/worker-events.mjs';
27
27
  import {
28
28
  shouldStreamRuntime,
29
29
  reportSubprocessFailure,
@@ -160,7 +160,7 @@ export const claudeCodeRuntime = {
160
160
  appendSystemPrompt,
161
161
  onEvent: async (event) => {
162
162
  if (event?.type === 'turn.started') {
163
- await emitWorkerEvent({
163
+ emitWorkerEventBestEffort({
164
164
  adapter,
165
165
  binding,
166
166
  agent: this.name,
@@ -173,21 +173,6 @@ export const claudeCodeRuntime = {
173
173
  },
174
174
  logger: ctx.logger,
175
175
  });
176
- } else if (event?.type === 'message.delta' && event.text) {
177
- await emitWorkerEvent({
178
- adapter,
179
- binding,
180
- agent: this.name,
181
- sessionId: event.sessionId || targetSessionId || binding.id,
182
- cwd: projectDir,
183
- replyToMessageId: inbound.messageId || null,
184
- event: {
185
- hook_event_name: 'worker.message.delta',
186
- worker_event_name: 'worker.message.delta',
187
- delta: event.text,
188
- },
189
- logger: ctx.logger,
190
- });
191
176
  }
192
177
  },
193
178
  })
@@ -200,7 +185,7 @@ export const claudeCodeRuntime = {
200
185
  claudePath,
201
186
  claudeVersion,
202
187
  }, { status: 'connected' });
203
- await emitWorkerEvent({
188
+ emitWorkerEventBestEffort({
204
189
  adapter,
205
190
  binding: nextBinding,
206
191
  agent: this.name,
@@ -215,7 +200,7 @@ export const claudeCodeRuntime = {
215
200
  });
216
201
  return true;
217
202
  } catch (err) {
218
- await emitWorkerEvent({
203
+ emitWorkerEventBestEffort({
219
204
  adapter,
220
205
  binding,
221
206
  agent: this.name,
@@ -247,7 +232,7 @@ export const claudeCodeRuntime = {
247
232
 
248
233
  async reconcileAfterRestart(binding, ctx) {
249
234
  const meta = binding.runtimeMeta || {};
250
- await emitWorkerEvent({
235
+ emitWorkerEventBestEffort({
251
236
  adapter: ctx.adapter,
252
237
  binding,
253
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
  }
@@ -21,10 +21,9 @@ import {
21
21
  requireCodexPath,
22
22
  } from './session.mjs';
23
23
  import { buildCodexInputFromInbound } from '../../core/media/inbound.mjs';
24
- import { emitWorkerEvent } from '../../core/events/worker-events.mjs';
24
+ import { emitWorkerEventBestEffort } from '../../core/events/worker-events.mjs';
25
25
  import {
26
26
  shouldStreamRuntime,
27
- createDeltaAggregator,
28
27
  reportSubprocessFailure,
29
28
  terminalRuntimeFailure,
30
29
  updateBindingRuntimeMeta,
@@ -226,25 +225,6 @@ export const codexRuntime = {
226
225
  const sessionScope = resolveRuntimeSessionScope(meta, inbound);
227
226
  const shouldRotate = sessionScope.shouldRotate;
228
227
  const targetSessionId = shouldRotate ? null : sessionScope.sessionId;
229
- const deltaAggregator = createDeltaAggregator({
230
- flushDelta: async ({ text, sessionId, turnId, cwd }) => {
231
- await emitWorkerEvent({
232
- adapter,
233
- binding,
234
- agent: this.name,
235
- sessionId: sessionId || targetSessionId || binding.id,
236
- turnId: turnId || null,
237
- cwd: cwd || agentHome,
238
- replyToMessageId: inbound.messageId || null,
239
- event: {
240
- hook_event_name: 'worker.message.delta',
241
- worker_event_name: 'worker.message.delta',
242
- delta: text,
243
- },
244
- logger: ctx.logger,
245
- });
246
- },
247
- });
248
228
  try {
249
229
  const runtimeCodexPath = requireCodexPath(meta.codexPath || meta.runtimePath);
250
230
  const runtimeCodexVersion = getCodexRuntimeHealth(runtimeCodexPath).version || meta.codexVersion || null;
@@ -273,7 +253,7 @@ export const codexRuntime = {
273
253
  developerInstructions,
274
254
  onEvent: async (event) => {
275
255
  if (event?.type === 'turn.started') {
276
- await emitWorkerEvent({
256
+ emitWorkerEventBestEffort({
277
257
  adapter,
278
258
  binding,
279
259
  agent: this.name,
@@ -287,12 +267,6 @@ export const codexRuntime = {
287
267
  },
288
268
  logger: ctx.logger,
289
269
  });
290
- } else if (event?.type === 'message.delta' && event.text) {
291
- deltaAggregator.push(event.text, {
292
- sessionId: event.sessionId || targetSessionId || binding.id,
293
- turnId: event.turnId || null,
294
- cwd: agentHome,
295
- });
296
270
  }
297
271
  },
298
272
  })
@@ -300,7 +274,6 @@ export const codexRuntime = {
300
274
  input: codexInput,
301
275
  });
302
276
 
303
- await deltaAggregator.flush();
304
277
  const nextBinding = await updateBindingRuntimeMeta(ctx, binding, {
305
278
  ...buildRuntimeSessionMetaPatch(meta, sessionScope, {
306
279
  sessionId: result?.sessionId,
@@ -310,7 +283,7 @@ export const codexRuntime = {
310
283
  codexPath: runtimeCodexPath,
311
284
  codexVersion: runtimeCodexVersion,
312
285
  }, { status: 'connected' });
313
- await emitWorkerEvent({
286
+ emitWorkerEventBestEffort({
314
287
  adapter,
315
288
  binding: nextBinding,
316
289
  agent: this.name,
@@ -326,7 +299,6 @@ export const codexRuntime = {
326
299
  });
327
300
  return true;
328
301
  } catch (err) {
329
- await deltaAggregator.flush().catch(() => {});
330
302
  const failureInfo = err?.info || {
331
303
  ok: false,
332
304
  kind: 'exit-error',
@@ -341,7 +313,7 @@ export const codexRuntime = {
341
313
  lastGatewayFailureReason: failureInfo.errorMessage || err?.message || 'Codex app-server error',
342
314
  }, { status: 'degraded' }).catch(() => binding);
343
315
  }
344
- await emitWorkerEvent({
316
+ emitWorkerEventBestEffort({
345
317
  adapter,
346
318
  binding: failureBinding,
347
319
  agent: this.name,
@@ -369,7 +341,7 @@ export const codexRuntime = {
369
341
 
370
342
  async reconcileAfterRestart(binding, ctx) {
371
343
  const meta = binding.runtimeMeta || {};
372
- await emitWorkerEvent({
344
+ emitWorkerEventBestEffort({
373
345
  adapter: ctx.adapter,
374
346
  binding,
375
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
  }
@@ -25,10 +25,9 @@ import {
25
25
  requireOpenCodePath,
26
26
  } from './session.mjs';
27
27
  import { buildOpenCodeInputFromInbound } from '../../core/media/inbound.mjs';
28
- import { emitWorkerEvent } from '../../core/events/worker-events.mjs';
28
+ import { emitWorkerEventBestEffort } from '../../core/events/worker-events.mjs';
29
29
  import {
30
30
  shouldStreamRuntime,
31
- createDeltaAggregator,
32
31
  reportSubprocessFailure,
33
32
  terminalRuntimeFailure,
34
33
  updateBindingRuntimeMeta,
@@ -167,25 +166,6 @@ export const openCodeRuntime = {
167
166
  const shouldRotate = sessionScope.shouldRotate;
168
167
  const targetSessionId = shouldRotate ? null : sessionScope.sessionId;
169
168
 
170
- const deltaAggregator = createDeltaAggregator({
171
- flushDelta: async ({ text, sessionId, cwd }) => {
172
- await emitWorkerEvent({
173
- adapter,
174
- binding,
175
- agent: this.name,
176
- sessionId: sessionId || targetSessionId || binding.id,
177
- cwd: cwd || agentHome,
178
- replyToMessageId: inbound.messageId || null,
179
- event: {
180
- hook_event_name: 'worker.message.delta',
181
- worker_event_name: 'worker.message.delta',
182
- delta: text,
183
- },
184
- logger: ctx.logger,
185
- });
186
- },
187
- });
188
-
189
169
  try {
190
170
  const opencodePath = requireOpenCodePath(runtimeOpenCodePath);
191
171
  const opencodeVersion = getOpenCodeRuntimeHealth(opencodePath).version || meta.opencodeVersion || null;
@@ -210,7 +190,7 @@ export const openCodeRuntime = {
210
190
  files,
211
191
  onEvent: async (event) => {
212
192
  if (event?.type === 'turn.started') {
213
- await emitWorkerEvent({
193
+ emitWorkerEventBestEffort({
214
194
  adapter,
215
195
  binding,
216
196
  agent: this.name,
@@ -223,17 +203,11 @@ export const openCodeRuntime = {
223
203
  },
224
204
  logger: ctx.logger,
225
205
  });
226
- } else if (event?.type === 'message.delta' && event.text) {
227
- deltaAggregator.push(event.text, {
228
- sessionId: event.sessionId || targetSessionId || binding.id,
229
- cwd: agentHome,
230
- });
231
206
  }
232
207
  },
233
208
  })
234
209
  : await this.runTurn({ sessionId: targetSessionId, cwd: agentHome, opencodePath, agentEnv }, message, { files, standingPrompt, forceStandingPrompt });
235
210
 
236
- await deltaAggregator.flush();
237
211
  const observedSessionId = result?.sessionId || targetSessionId;
238
212
  if (observedSessionId) {
239
213
  standingPromptSeen.add(`${binding.id}|${observedSessionId}|${protocolOverlayKey}`);
@@ -248,7 +222,7 @@ export const openCodeRuntime = {
248
222
  opencodePath,
249
223
  opencodeVersion,
250
224
  }, { status: 'connected' });
251
- await emitWorkerEvent({
225
+ emitWorkerEventBestEffort({
252
226
  adapter,
253
227
  binding: nextBinding,
254
228
  agent: this.name,
@@ -263,8 +237,7 @@ export const openCodeRuntime = {
263
237
  });
264
238
  return true;
265
239
  } catch (err) {
266
- await deltaAggregator.flush().catch(() => {});
267
- await emitWorkerEvent({
240
+ emitWorkerEventBestEffort({
268
241
  adapter,
269
242
  binding,
270
243
  agent: this.name,
@@ -296,7 +269,7 @@ export const openCodeRuntime = {
296
269
 
297
270
  async reconcileAfterRestart(binding, ctx) {
298
271
  const meta = binding.runtimeMeta || {};
299
- await emitWorkerEvent({
272
+ emitWorkerEventBestEffort({
300
273
  adapter: ctx.adapter,
301
274
  binding,
302
275
  agent: this.name,
@@ -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, ... }`
@@ -246,18 +245,19 @@ export function runOpenCodePrompt({
246
245
  let lastError = null;
247
246
  let settled = false;
248
247
  let turnStartedEmitted = false;
249
- let eventChain = Promise.resolve();
250
248
 
251
249
  const emit = (event) => {
252
250
  if (typeof onEvent !== 'function') return;
253
- eventChain = eventChain.then(() => onEvent(event)).catch(() => {});
251
+ void Promise.resolve()
252
+ .then(() => onEvent(event))
253
+ .catch(() => {});
254
254
  };
255
255
 
256
256
  const settle = (fn, value) => {
257
257
  if (settled) return;
258
258
  settled = true;
259
259
  if (timeout) clearTimeout(timeout);
260
- eventChain.catch(() => {}).finally(() => fn(value));
260
+ fn(value);
261
261
  };
262
262
 
263
263
  const handleEvent = (event) => {
@@ -273,7 +273,6 @@ export function runOpenCodePrompt({
273
273
  const delta = extractEventText(event);
274
274
  if (delta) {
275
275
  finalText += delta;
276
- emit({ type: 'message.delta', sessionId: activeSessionId, text: delta });
277
276
  }
278
277
  }
279
278
 
@@ -19,10 +19,9 @@ import {
19
19
  requirePiPath,
20
20
  runPiPrompt,
21
21
  } from './session.mjs';
22
- import { emitWorkerEvent } from '../../core/events/worker-events.mjs';
22
+ import { emitWorkerEventBestEffort } from '../../core/events/worker-events.mjs';
23
23
  import {
24
24
  shouldStreamRuntime,
25
- createDeltaAggregator,
26
25
  reportSubprocessFailure,
27
26
  terminalRuntimeFailure,
28
27
  updateBindingRuntimeMeta,
@@ -138,24 +137,6 @@ export const piRuntime = {
138
137
  const sessionScope = resolveRuntimeSessionScope(meta, inbound);
139
138
  const shouldRotate = sessionScope.shouldRotate;
140
139
  const targetSessionId = shouldRotate ? null : sessionScope.sessionId;
141
- const deltaAggregator = createDeltaAggregator({
142
- flushDelta: async ({ text, sessionId, cwd }) => {
143
- await emitWorkerEvent({
144
- adapter,
145
- binding,
146
- agent: this.name,
147
- sessionId: sessionId || targetSessionId || binding.id,
148
- cwd: cwd || agentHome,
149
- replyToMessageId: inbound.messageId || null,
150
- event: {
151
- hook_event_name: 'worker.message.delta',
152
- worker_event_name: 'worker.message.delta',
153
- delta: text,
154
- },
155
- logger: ctx.logger,
156
- });
157
- },
158
- });
159
140
 
160
141
  try {
161
142
  const runtimePiPath = requirePiPath(meta.piPath || meta.runtimePath);
@@ -181,7 +162,7 @@ export const piRuntime = {
181
162
  images,
182
163
  onEvent: async (event) => {
183
164
  if (event?.type === 'turn.started') {
184
- await emitWorkerEvent({
165
+ emitWorkerEventBestEffort({
185
166
  adapter,
186
167
  binding,
187
168
  agent: this.name,
@@ -194,17 +175,11 @@ export const piRuntime = {
194
175
  },
195
176
  logger: ctx.logger,
196
177
  });
197
- } else if (event?.type === 'message.delta' && event.text) {
198
- deltaAggregator.push(event.text, {
199
- sessionId: event.sessionId || targetSessionId || binding.id,
200
- cwd: agentHome,
201
- });
202
178
  }
203
179
  },
204
180
  })
205
181
  : await this.runTurn({ sessionId: targetSessionId, cwd: agentHome, piPath: runtimePiPath, agentEnv }, message, { images, standingPrompt, forceStandingPrompt });
206
182
 
207
- await deltaAggregator.flush();
208
183
  const observedSessionId = result?.sessionId || targetSessionId;
209
184
  if (observedSessionId) {
210
185
  standingPromptSeen.add(`${binding.id}|${observedSessionId}|${protocolOverlayKey}`);
@@ -218,7 +193,7 @@ export const piRuntime = {
218
193
  piPath: runtimePiPath,
219
194
  piVersion: runtimePiVersion,
220
195
  }, { status: 'connected' });
221
- await emitWorkerEvent({
196
+ emitWorkerEventBestEffort({
222
197
  adapter,
223
198
  binding: nextBinding,
224
199
  agent: this.name,
@@ -233,8 +208,7 @@ export const piRuntime = {
233
208
  });
234
209
  return true;
235
210
  } catch (err) {
236
- await deltaAggregator.flush().catch(() => {});
237
- await emitWorkerEvent({
211
+ emitWorkerEventBestEffort({
238
212
  adapter,
239
213
  binding,
240
214
  agent: this.name,
@@ -266,7 +240,7 @@ export const piRuntime = {
266
240
 
267
241
  async reconcileAfterRestart(binding, ctx) {
268
242
  const meta = binding.runtimeMeta || {};
269
- await emitWorkerEvent({
243
+ emitWorkerEventBestEffort({
270
244
  adapter: ctx.adapter,
271
245
  binding,
272
246
  agent: this.name,
@@ -223,12 +223,13 @@ export function runPiPrompt({
223
223
  let activeSessionFile = null;
224
224
  let finalText = '';
225
225
  let settled = false;
226
- let eventChain = Promise.resolve();
227
226
  const pending = new Map();
228
227
 
229
228
  const emit = (event) => {
230
229
  if (typeof onEvent !== 'function') return;
231
- eventChain = eventChain.then(() => onEvent(event)).catch(() => {});
230
+ void Promise.resolve()
231
+ .then(() => onEvent(event))
232
+ .catch(() => {});
232
233
  };
233
234
 
234
235
  const settle = (fn, value) => {
@@ -240,7 +241,7 @@ export function runPiPrompt({
240
241
  }
241
242
  pending.clear();
242
243
  try { child.kill('SIGTERM'); } catch {}
243
- eventChain.catch(() => {}).finally(() => fn(value));
244
+ fn(value);
244
245
  };
245
246
 
246
247
  const sendRaw = (payload) => {
@@ -308,7 +309,6 @@ export function runPiPrompt({
308
309
  const delta = extractDeltaFromEvent(event);
309
310
  if (delta) {
310
311
  finalText += delta;
311
- emit({ type: 'message.delta', sessionId: activeSessionId, text: delta });
312
312
  }
313
313
  return;
314
314
  }