ticlawk 0.1.16-dev.2 → 0.1.16-dev.21

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.
@@ -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) => {
@@ -6,6 +6,7 @@
6
6
 
7
7
  import { basename } from 'node:path';
8
8
  import { buildAgentRuntimeEnv } from '../../core/runtime-env.mjs';
9
+ import { getGoalTaskProtocolOverlayKey } from '../_shared/goal-task-protocol.mjs';
9
10
  import { buildStandingPrompt } from '../_shared/standing-prompt.mjs';
10
11
  import { ensureAgentHome } from '../../core/agent-home.mjs';
11
12
  import {
@@ -22,13 +23,15 @@ import { emitWorkerEvent } from '../../core/events/worker-events.mjs';
22
23
  import {
23
24
  shouldStreamRuntime,
24
25
  createDeltaAggregator,
25
- sendAdapterMessage,
26
- recordActivity,
27
26
  reportSubprocessFailure,
28
27
  terminalRuntimeFailure,
29
28
  updateBindingRuntimeMeta,
29
+ resolveRuntimeSessionScope,
30
+ buildRuntimeSessionMetaPatch,
30
31
  } from '../../core/runtime-support.mjs';
31
32
 
33
+ const standingPromptSeen = new Set();
34
+
32
35
  export const piRuntime = {
33
36
  name: 'pi',
34
37
 
@@ -41,6 +44,7 @@ export const piRuntime = {
41
44
  piPath,
42
45
  agentEnv,
43
46
  standingPrompt: opts.standingPrompt || null,
47
+ forceStandingPrompt: Boolean(opts.forceStandingPrompt),
44
48
  timeoutMs: opts.timeoutMs,
45
49
  });
46
50
  },
@@ -54,6 +58,7 @@ export const piRuntime = {
54
58
  piPath,
55
59
  agentEnv,
56
60
  standingPrompt: opts.standingPrompt || null,
61
+ forceStandingPrompt: Boolean(opts.forceStandingPrompt),
57
62
  timeoutMs: opts.timeoutMs,
58
63
  onEvent: opts.onEvent,
59
64
  });
@@ -122,33 +127,24 @@ export const piRuntime = {
122
127
  images = await buildPiImagesFromInbound(inbound);
123
128
  const captionText = (inbound.text || '').trim();
124
129
  if (images.length === 0 && !captionText) {
125
- await sendAdapterMessage(adapter, binding, {
126
- type: 'assistant',
127
- text: '⚠️ Image attached, but I could not access the image data and there is no caption to fall back on. Try sending the image again, or include a text caption.',
128
- media: [],
129
- replyToMessageId: inbound.messageId || null,
130
- });
130
+ // Image decode failed and no caption to fall back on. Bail
131
+ // without a user notice; the dead chat-projection path that
132
+ // used to surface such notices is gone.
131
133
  return true;
132
134
  }
133
- if (images.length === 0 && captionText) {
134
- await sendAdapterMessage(adapter, binding, {
135
- type: 'assistant',
136
- text: '⚠️ Could not access the attached image data; acting on the caption text only.',
137
- media: [],
138
- replyToMessageId: inbound.messageId || null,
139
- });
140
- }
141
135
  message = captionText || 'Please analyze the attached image(s).';
142
136
  }
143
137
 
144
- const shouldRotate = !meta.sessionId || meta.rotatePending;
138
+ const sessionScope = resolveRuntimeSessionScope(meta, inbound);
139
+ const shouldRotate = sessionScope.shouldRotate;
140
+ const targetSessionId = shouldRotate ? null : sessionScope.sessionId;
145
141
  const deltaAggregator = createDeltaAggregator({
146
142
  flushDelta: async ({ text, sessionId, cwd }) => {
147
143
  await emitWorkerEvent({
148
144
  adapter,
149
145
  binding,
150
146
  agent: this.name,
151
- sessionId: sessionId || meta.sessionId || binding.id,
147
+ sessionId: sessionId || targetSessionId || binding.id,
152
148
  cwd: cwd || agentHome,
153
149
  replyToMessageId: inbound.messageId || null,
154
150
  event: {
@@ -166,13 +162,22 @@ export const piRuntime = {
166
162
  const runtimePiVersion = getPiRuntimeHealth(runtimePiPath).version || meta.piVersion || null;
167
163
  const agentEnv = buildAgentRuntimeEnv({
168
164
  agentId: binding.id,
169
- sessionId: meta.sessionId,
165
+ sessionId: targetSessionId,
170
166
  hostId: binding.runtime_host_id,
167
+ conversationId: inbound.conversationId,
168
+ messageId: inbound.messageId,
169
+ target: inbound.envelopeTarget,
171
170
  });
172
- const standingPrompt = buildStandingPrompt({ agentId: binding.id });
171
+ const standingPrompt = buildStandingPrompt({ agentId: binding.id, inbound });
172
+ const protocolOverlayKey = getGoalTaskProtocolOverlayKey({ agentId: binding.id, inbound });
173
+ const standingPromptSeenKey = targetSessionId
174
+ ? `${binding.id}|${targetSessionId}|${protocolOverlayKey}`
175
+ : null;
176
+ const forceStandingPrompt = Boolean(standingPromptSeenKey && !standingPromptSeen.has(standingPromptSeenKey));
173
177
  const result = shouldStreamRuntime(this.name, this)
174
- ? await this.runTurnStream({ sessionId: shouldRotate ? null : meta.sessionId, cwd: agentHome, piPath: runtimePiPath, agentEnv }, message, {
178
+ ? await this.runTurnStream({ sessionId: targetSessionId, cwd: agentHome, piPath: runtimePiPath, agentEnv }, message, {
175
179
  standingPrompt,
180
+ forceStandingPrompt,
176
181
  images,
177
182
  onEvent: async (event) => {
178
183
  if (event?.type === 'turn.started') {
@@ -180,7 +185,7 @@ export const piRuntime = {
180
185
  adapter,
181
186
  binding,
182
187
  agent: this.name,
183
- sessionId: event.sessionId || meta.sessionId || binding.id,
188
+ sessionId: event.sessionId || targetSessionId || binding.id,
184
189
  cwd: agentHome,
185
190
  replyToMessageId: inbound.messageId || null,
186
191
  event: {
@@ -191,30 +196,33 @@ export const piRuntime = {
191
196
  });
192
197
  } else if (event?.type === 'message.delta' && event.text) {
193
198
  deltaAggregator.push(event.text, {
194
- sessionId: event.sessionId || meta.sessionId || binding.id,
199
+ sessionId: event.sessionId || targetSessionId || binding.id,
195
200
  cwd: agentHome,
196
201
  });
197
202
  }
198
203
  },
199
204
  })
200
- : await this.runTurn({ sessionId: shouldRotate ? null : meta.sessionId, cwd: agentHome, piPath: runtimePiPath, agentEnv }, message, { images, standingPrompt });
205
+ : await this.runTurn({ sessionId: targetSessionId, cwd: agentHome, piPath: runtimePiPath, agentEnv }, message, { images, standingPrompt, forceStandingPrompt });
201
206
 
202
207
  await deltaAggregator.flush();
208
+ const observedSessionId = result?.sessionId || targetSessionId;
209
+ if (observedSessionId) {
210
+ standingPromptSeen.add(`${binding.id}|${observedSessionId}|${protocolOverlayKey}`);
211
+ }
203
212
  const nextBinding = await updateBindingRuntimeMeta(ctx, binding, {
204
- sessionId: result?.sessionId || meta.sessionId,
205
- path: result?.path || meta.path || null,
213
+ ...buildRuntimeSessionMetaPatch(meta, sessionScope, {
214
+ sessionId: result?.sessionId,
215
+ path: result?.path,
216
+ }),
206
217
  runtimePath: runtimePiPath,
207
218
  piPath: runtimePiPath,
208
219
  piVersion: runtimePiVersion,
209
- rotatePending: false,
210
- lastRotatedAt: shouldRotate ? new Date().toISOString() : meta.lastRotatedAt,
211
220
  }, { status: 'connected' });
212
- await recordActivity(adapter, nextBinding, inbound, result);
213
221
  await emitWorkerEvent({
214
222
  adapter,
215
223
  binding: nextBinding,
216
224
  agent: this.name,
217
- sessionId: result?.sessionId || meta.sessionId || binding.id,
225
+ sessionId: result?.sessionId || targetSessionId || binding.id,
218
226
  cwd: result?.cwd || agentHome,
219
227
  replyToMessageId: inbound.messageId || null,
220
228
  event: {
@@ -230,7 +238,7 @@ export const piRuntime = {
230
238
  adapter,
231
239
  binding,
232
240
  agent: this.name,
233
- sessionId: meta.sessionId || binding.id,
241
+ sessionId: targetSessionId || binding.id,
234
242
  cwd: agentHome,
235
243
  replyToMessageId: inbound.messageId || null,
236
244
  event: {
@@ -197,11 +197,14 @@ export function runPiPrompt({
197
197
  piPath = null,
198
198
  agentEnv = null,
199
199
  standingPrompt = null,
200
+ forceStandingPrompt = false,
200
201
  timeoutMs = Number(process.env.PI_RUN_TIMEOUT_MS || DEFAULT_PI_RUN_TIMEOUT_MS),
201
202
  onEvent,
202
203
  }) {
203
- // pi has no documented system-prompt flag prepend on first turn.
204
- const finalMessage = standingPrompt && !sessionId
204
+ // pi has no documented system-prompt flag. Prepend on first turn, and
205
+ // let callers force one resumed-session injection for a newly selected
206
+ // protocol overlay.
207
+ const finalMessage = standingPrompt && (!sessionId || forceStandingPrompt)
205
208
  ? `${standingPrompt}\n\n---\n\n${message}`
206
209
  : message;
207
210
  return new Promise((resolve, reject) => {
@@ -1,149 +0,0 @@
1
- /**
2
- * ticlawk adapter — final agent message / media write path.
3
- *
4
- * Everything here turns an agent result (text + optional media local paths)
5
- * into a terminal runtime result written back to ticlawk.
6
- *
7
- * This module imports from `./api.mjs` (HTTP client) and from
8
- * `../../core/logger.mjs` (structured logging). No runtime dependencies.
9
- */
10
-
11
- import { existsSync, readFileSync } from 'node:fs';
12
- import { extname } from 'node:path';
13
- import { randomUUID } from 'node:crypto';
14
- import * as api from './api.mjs';
15
- import { extractMediaPaths } from '../../core/media/outbound.mjs';
16
- import { debugLog, debugError } from '../../core/logger.mjs';
17
- import { TICLAWK_CONNECTOR_API_KEY } from '../../core/config.mjs';
18
-
19
- const MIME_MAP = {
20
- '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
21
- '.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml',
22
- '.mp4': 'video/mp4', '.webm': 'video/webm',
23
- '.opus': 'audio/opus', '.mp3': 'audio/mpeg', '.wav': 'audio/wav',
24
- '.pdf': 'application/pdf',
25
- };
26
-
27
- async function uploadLocalAsset(localPath) {
28
- if (!process.env[TICLAWK_CONNECTOR_API_KEY]) {
29
- debugError('relay', 'upload.skipped', {
30
- localPath,
31
- reason: 'missing connector api key',
32
- });
33
- return null;
34
- }
35
- if (!existsSync(localPath)) {
36
- debugError('relay', 'upload.skipped', {
37
- localPath,
38
- reason: 'file not found',
39
- });
40
- return null;
41
- }
42
-
43
- const ext = extname(localPath).toLowerCase();
44
- const contentType = MIME_MAP[ext] || 'application/octet-stream';
45
- const fileName = `${Date.now()}-${randomUUID().slice(0, 8)}${ext}`;
46
- const fileData = readFileSync(localPath);
47
-
48
- try {
49
- const asset = await api.uploadAsset(fileName, fileData, contentType);
50
- if (!asset?.asset_id) {
51
- debugError('relay', 'upload.failed', {
52
- localPath,
53
- fileName,
54
- error: 'missing asset_id in upload response',
55
- });
56
- return null;
57
- }
58
- debugLog('relay', 'upload.ok', {
59
- localPath,
60
- fileName,
61
- assetId: asset.asset_id,
62
- contentType: asset.content_type || contentType,
63
- sizeBytes: asset.size_bytes ?? null,
64
- });
65
- return asset;
66
- } catch (err) {
67
- debugError('relay', 'upload.failed', {
68
- localPath,
69
- fileName,
70
- error: err.message,
71
- });
72
- return null;
73
- }
74
- }
75
-
76
- export async function uploadMediaAssets(localPaths) {
77
- const assets = [];
78
- for (const p of localPaths) {
79
- const asset = await uploadLocalAsset(p);
80
- if (asset) assets.push(asset);
81
- }
82
- return assets;
83
- }
84
-
85
- export async function processAndSaveResult(result, opts) {
86
- const { agentId: explicitAgentId, sessionKey, hostId, type, replyToMessageId, agent, sessionId, turnId, runtimeVersion } = opts;
87
- const agentId = explicitAgentId || sessionKey;
88
- const startedAt = Date.now();
89
-
90
- // Collect media: from agent mediaUrls + parsed from text
91
- const allLocalPaths = [...new Set([
92
- ...(result.mediaUrls || []),
93
- ...extractMediaPaths(result.text || ''),
94
- ])];
95
-
96
- debugLog('relay', 'process-result.begin', {
97
- agentId,
98
- type,
99
- parentMessageId: replyToMessageId || null,
100
- textLength: result.text?.length || 0,
101
- localMediaCount: allLocalPaths.length,
102
- });
103
-
104
- // Upload local media to Ticlawk private chat assets.
105
- const uploadedAssets = await uploadMediaAssets(allLocalPaths);
106
- const uploadedAssetIds = uploadedAssets
107
- .map((asset) => asset?.asset_id)
108
- .filter(Boolean);
109
-
110
- const updateId = randomUUID();
111
- debugLog('relay', 'post-final.begin', {
112
- agentId,
113
- updateId,
114
- durationMs: Date.now() - startedAt,
115
- uploadedMediaCount: uploadedAssetIds.length,
116
- });
117
- try {
118
- await api.postRuntimeResult({
119
- agent,
120
- agent_id: agentId,
121
- runtime_host_id: hostId,
122
- session_id: sessionId || null,
123
- cwd: '',
124
- runtime_version: runtimeVersion ?? null,
125
- result_id: updateId,
126
- turn_id: turnId || replyToMessageId || null,
127
- reply_to_message_id: replyToMessageId || null,
128
- origin_ts: new Date().toISOString(),
129
- text: result.text || '',
130
- media_asset_ids: uploadedAssetIds,
131
- output_type: type || 'agent_message',
132
- });
133
- } catch (err) {
134
- debugError('relay', 'post-final.failed', {
135
- agentId,
136
- updateId,
137
- durationMs: Date.now() - startedAt,
138
- error: err.message,
139
- });
140
- throw err;
141
- }
142
- debugLog('relay', 'process-result.ok', {
143
- agentId,
144
- updateId,
145
- durationMs: Date.now() - startedAt,
146
- uploadedMediaCount: uploadedAssetIds.length,
147
- });
148
- return { id: updateId, agentId };
149
- }