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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +14 -2
  2. package/bin/ticlawk.mjs +207 -25
  3. package/package.json +1 -1
  4. package/src/adapters/ticlawk/api.mjs +293 -70
  5. package/src/adapters/ticlawk/credentials.mjs +41 -1
  6. package/src/adapters/ticlawk/index.mjs +199 -199
  7. package/src/adapters/ticlawk/wake-client.mjs +1 -1
  8. package/src/cli/agent-commands.mjs +607 -37
  9. package/src/core/agent-cli-handlers.mjs +449 -20
  10. package/src/core/agent-home.mjs +86 -10
  11. package/src/core/argv.mjs +11 -1
  12. package/src/core/events/worker-events.mjs +32 -36
  13. package/src/core/http.mjs +126 -0
  14. package/src/core/runtime-env.mjs +7 -0
  15. package/src/core/runtime-support.mjs +108 -107
  16. package/src/migrate/write-initial-memory.mjs +5 -5
  17. package/src/runtimes/_shared/agent-handbook.mjs +45 -0
  18. package/src/runtimes/_shared/brand.mjs +2 -0
  19. package/src/runtimes/_shared/goal-task-protocol.mjs +50 -0
  20. package/src/runtimes/_shared/handbook/BASICS.md +27 -0
  21. package/src/runtimes/_shared/handbook/COLLABORATION.md +37 -0
  22. package/src/runtimes/_shared/handbook/COMMUNICATION.md +50 -0
  23. package/src/runtimes/_shared/handbook/DM_SCOPE.md +13 -0
  24. package/src/runtimes/_shared/handbook/GOAL_AUTHORITY.md +43 -0
  25. package/src/runtimes/_shared/handbook/GOAL_TASK_CORE.md +43 -0
  26. package/src/runtimes/_shared/handbook/GROUP_ADMIN_SCOPE.md +21 -0
  27. package/src/runtimes/_shared/handbook/GROUP_MEMBER_SCOPE.md +15 -0
  28. package/src/runtimes/_shared/handbook/SURFACES.md +41 -0
  29. package/src/runtimes/_shared/handbook/TASK_WORKER.md +14 -0
  30. package/src/runtimes/_shared/standing-prompt.mjs +111 -262
  31. package/src/runtimes/_shared/wake-prompt.mjs +261 -0
  32. package/src/runtimes/claude-code/index.mjs +34 -127
  33. package/src/runtimes/claude-code/session.mjs +2 -7
  34. package/src/runtimes/codex/index.mjs +117 -54
  35. package/src/runtimes/codex/session.mjs +2 -12
  36. package/src/runtimes/openclaw/index.mjs +16 -26
  37. package/src/runtimes/opencode/index.mjs +45 -66
  38. package/src/runtimes/opencode/session.mjs +12 -12
  39. package/src/runtimes/pi/index.mjs +42 -60
  40. package/src/runtimes/pi/session.mjs +9 -6
  41. package/src/adapters/ticlawk/cards.mjs +0 -149
  42. package/src/core/media/outbound.mjs +0 -163
@@ -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,18 @@ 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
- 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
- sendAdapterMessage,
33
- recordActivity,
34
31
  reportSubprocessFailure,
35
32
  terminalRuntimeFailure,
36
33
  updateBindingRuntimeMeta,
34
+ resolveRuntimeSessionScope,
35
+ buildRuntimeSessionMetaPatch,
37
36
  } from '../../core/runtime-support.mjs';
38
37
 
38
+ const standingPromptSeen = new Set();
39
+
39
40
  export const openCodeRuntime = {
40
41
  name: 'opencode',
41
42
 
@@ -51,6 +52,7 @@ export const openCodeRuntime = {
51
52
  opencodePath,
52
53
  agentEnv,
53
54
  standingPrompt: opts.standingPrompt || null,
55
+ forceStandingPrompt: Boolean(opts.forceStandingPrompt),
54
56
  files: opts.files,
55
57
  timeoutMs: opts.timeoutMs,
56
58
  });
@@ -64,6 +66,7 @@ export const openCodeRuntime = {
64
66
  opencodePath,
65
67
  agentEnv,
66
68
  standingPrompt: opts.standingPrompt || null,
69
+ forceStandingPrompt: Boolean(opts.forceStandingPrompt),
67
70
  files: opts.files,
68
71
  timeoutMs: opts.timeoutMs,
69
72
  onEvent: opts.onEvent,
@@ -146,70 +149,52 @@ export const openCodeRuntime = {
146
149
  const captionText = (inbound.text || '').trim();
147
150
 
148
151
  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
- });
152
+ // Image decode failed and no caption to fall back on — we have
153
+ // nothing meaningful to feed the model. Bail without a user
154
+ // notice; this runtime is non-primary and the dead chat-projection
155
+ // path that used to surface such notices is gone.
155
156
  return true;
156
157
  }
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
- }
158
+ // If files.length === 0 && captionText, fall through with the
159
+ // caption-only message below no inline user notice.
167
160
 
168
161
  // If user sent images with no caption, give the model a minimal
169
162
  // instruction so it has something to anchor on.
170
163
  message = captionText || 'Please analyze the attached image(s).';
171
164
  }
172
- const shouldRotate = !meta.sessionId || meta.rotatePending;
173
-
174
- const deltaAggregator = createDeltaAggregator({
175
- flushDelta: async ({ text, sessionId, cwd }) => {
176
- await emitWorkerEvent({
177
- adapter,
178
- binding,
179
- agent: this.name,
180
- sessionId: sessionId || meta.sessionId || binding.id,
181
- cwd: cwd || agentHome,
182
- replyToMessageId: inbound.messageId || null,
183
- event: {
184
- hook_event_name: 'worker.message.delta',
185
- worker_event_name: 'worker.message.delta',
186
- delta: text,
187
- },
188
- logger: ctx.logger,
189
- });
190
- },
191
- });
165
+ const sessionScope = resolveRuntimeSessionScope(meta, inbound);
166
+ const shouldRotate = sessionScope.shouldRotate;
167
+ const targetSessionId = shouldRotate ? null : sessionScope.sessionId;
192
168
 
193
169
  try {
194
170
  const opencodePath = requireOpenCodePath(runtimeOpenCodePath);
195
171
  const opencodeVersion = getOpenCodeRuntimeHealth(opencodePath).version || meta.opencodeVersion || null;
196
172
  const agentEnv = buildAgentRuntimeEnv({
197
173
  agentId: binding.id,
198
- sessionId: meta.sessionId,
174
+ sessionId: targetSessionId,
199
175
  hostId: binding.runtime_host_id,
176
+ conversationId: inbound.conversationId,
177
+ messageId: inbound.messageId,
178
+ target: inbound.envelopeTarget,
200
179
  });
201
- 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));
202
186
  const result = shouldStreamRuntime(this.name, this)
203
- ? await this.runTurnStream({ sessionId: shouldRotate ? null : meta.sessionId, cwd: agentHome, opencodePath, agentEnv }, message, {
187
+ ? await this.runTurnStream({ sessionId: targetSessionId, cwd: agentHome, opencodePath, agentEnv }, message, {
204
188
  standingPrompt,
189
+ forceStandingPrompt,
205
190
  files,
206
191
  onEvent: async (event) => {
207
192
  if (event?.type === 'turn.started') {
208
- await emitWorkerEvent({
193
+ emitWorkerEventBestEffort({
209
194
  adapter,
210
195
  binding,
211
196
  agent: this.name,
212
- sessionId: event.sessionId || meta.sessionId || binding.id,
197
+ sessionId: event.sessionId || targetSessionId || binding.id,
213
198
  cwd: agentHome,
214
199
  replyToMessageId: inbound.messageId || null,
215
200
  event: {
@@ -218,35 +203,30 @@ export const openCodeRuntime = {
218
203
  },
219
204
  logger: ctx.logger,
220
205
  });
221
- } else if (event?.type === 'message.delta' && event.text) {
222
- deltaAggregator.push(event.text, {
223
- sessionId: event.sessionId || meta.sessionId || binding.id,
224
- cwd: agentHome,
225
- });
226
206
  }
227
207
  },
228
208
  })
229
- : 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 });
230
210
 
231
- await deltaAggregator.flush();
211
+ const observedSessionId = result?.sessionId || targetSessionId;
212
+ if (observedSessionId) {
213
+ standingPromptSeen.add(`${binding.id}|${observedSessionId}|${protocolOverlayKey}`);
214
+ }
232
215
 
233
216
  const nextBinding = await updateBindingRuntimeMeta(ctx, binding, {
234
- sessionId: result?.sessionId || meta.sessionId,
217
+ ...buildRuntimeSessionMetaPatch(meta, sessionScope, {
218
+ sessionId: result?.sessionId,
219
+ path: result?.path,
220
+ }),
235
221
  runtimePath: opencodePath,
236
222
  opencodePath,
237
223
  opencodeVersion,
238
- rotatePending: false,
239
- lastRotatedAt: shouldRotate ? new Date().toISOString() : meta.lastRotatedAt,
240
224
  }, { status: 'connected' });
241
- await recordActivity(adapter, nextBinding, inbound, {
242
- ...result,
243
- media: normalizeOutboundMedia(result),
244
- });
245
- await emitWorkerEvent({
225
+ emitWorkerEventBestEffort({
246
226
  adapter,
247
227
  binding: nextBinding,
248
228
  agent: this.name,
249
- sessionId: result?.sessionId || meta.sessionId || binding.id,
229
+ sessionId: result?.sessionId || targetSessionId || binding.id,
250
230
  cwd: result?.cwd || agentHome,
251
231
  replyToMessageId: inbound.messageId || null,
252
232
  event: {
@@ -257,12 +237,11 @@ export const openCodeRuntime = {
257
237
  });
258
238
  return true;
259
239
  } catch (err) {
260
- await deltaAggregator.flush().catch(() => {});
261
- await emitWorkerEvent({
240
+ emitWorkerEventBestEffort({
262
241
  adapter,
263
242
  binding,
264
243
  agent: this.name,
265
- sessionId: meta.sessionId || binding.id,
244
+ sessionId: targetSessionId || binding.id,
266
245
  cwd: agentHome,
267
246
  replyToMessageId: inbound.messageId || null,
268
247
  event: {
@@ -290,7 +269,7 @@ export const openCodeRuntime = {
290
269
 
291
270
  async reconcileAfterRestart(binding, ctx) {
292
271
  const meta = binding.runtimeMeta || {};
293
- await emitWorkerEvent({
272
+ emitWorkerEventBestEffort({
294
273
  adapter: ctx.adapter,
295
274
  binding,
296
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, ... }`
@@ -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
 
@@ -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 {
@@ -18,17 +19,18 @@ import {
18
19
  requirePiPath,
19
20
  runPiPrompt,
20
21
  } from './session.mjs';
21
- import { emitWorkerEvent } from '../../core/events/worker-events.mjs';
22
+ import { emitWorkerEventBestEffort } from '../../core/events/worker-events.mjs';
22
23
  import {
23
24
  shouldStreamRuntime,
24
- createDeltaAggregator,
25
- sendAdapterMessage,
26
- recordActivity,
27
25
  reportSubprocessFailure,
28
26
  terminalRuntimeFailure,
29
27
  updateBindingRuntimeMeta,
28
+ resolveRuntimeSessionScope,
29
+ buildRuntimeSessionMetaPatch,
30
30
  } from '../../core/runtime-support.mjs';
31
31
 
32
+ const standingPromptSeen = new Set();
33
+
32
34
  export const piRuntime = {
33
35
  name: 'pi',
34
36
 
@@ -41,6 +43,7 @@ export const piRuntime = {
41
43
  piPath,
42
44
  agentEnv,
43
45
  standingPrompt: opts.standingPrompt || null,
46
+ forceStandingPrompt: Boolean(opts.forceStandingPrompt),
44
47
  timeoutMs: opts.timeoutMs,
45
48
  });
46
49
  },
@@ -54,6 +57,7 @@ export const piRuntime = {
54
57
  piPath,
55
58
  agentEnv,
56
59
  standingPrompt: opts.standingPrompt || null,
60
+ forceStandingPrompt: Boolean(opts.forceStandingPrompt),
57
61
  timeoutMs: opts.timeoutMs,
58
62
  onEvent: opts.onEvent,
59
63
  });
@@ -122,65 +126,47 @@ export const piRuntime = {
122
126
  images = await buildPiImagesFromInbound(inbound);
123
127
  const captionText = (inbound.text || '').trim();
124
128
  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
- });
129
+ // Image decode failed and no caption to fall back on. Bail
130
+ // without a user notice; the dead chat-projection path that
131
+ // used to surface such notices is gone.
131
132
  return true;
132
133
  }
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
134
  message = captionText || 'Please analyze the attached image(s).';
142
135
  }
143
136
 
144
- const shouldRotate = !meta.sessionId || meta.rotatePending;
145
- const deltaAggregator = createDeltaAggregator({
146
- flushDelta: async ({ text, sessionId, cwd }) => {
147
- await emitWorkerEvent({
148
- adapter,
149
- binding,
150
- agent: this.name,
151
- sessionId: sessionId || meta.sessionId || binding.id,
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
- });
137
+ const sessionScope = resolveRuntimeSessionScope(meta, inbound);
138
+ const shouldRotate = sessionScope.shouldRotate;
139
+ const targetSessionId = shouldRotate ? null : sessionScope.sessionId;
163
140
 
164
141
  try {
165
142
  const runtimePiPath = requirePiPath(meta.piPath || meta.runtimePath);
166
143
  const runtimePiVersion = getPiRuntimeHealth(runtimePiPath).version || meta.piVersion || null;
167
144
  const agentEnv = buildAgentRuntimeEnv({
168
145
  agentId: binding.id,
169
- sessionId: meta.sessionId,
146
+ sessionId: targetSessionId,
170
147
  hostId: binding.runtime_host_id,
148
+ conversationId: inbound.conversationId,
149
+ messageId: inbound.messageId,
150
+ target: inbound.envelopeTarget,
171
151
  });
172
- const standingPrompt = buildStandingPrompt({ agentId: binding.id });
152
+ const standingPrompt = buildStandingPrompt({ agentId: binding.id, inbound });
153
+ const protocolOverlayKey = getGoalTaskProtocolOverlayKey({ agentId: binding.id, inbound });
154
+ const standingPromptSeenKey = targetSessionId
155
+ ? `${binding.id}|${targetSessionId}|${protocolOverlayKey}`
156
+ : null;
157
+ const forceStandingPrompt = Boolean(standingPromptSeenKey && !standingPromptSeen.has(standingPromptSeenKey));
173
158
  const result = shouldStreamRuntime(this.name, this)
174
- ? await this.runTurnStream({ sessionId: shouldRotate ? null : meta.sessionId, cwd: agentHome, piPath: runtimePiPath, agentEnv }, message, {
159
+ ? await this.runTurnStream({ sessionId: targetSessionId, cwd: agentHome, piPath: runtimePiPath, agentEnv }, message, {
175
160
  standingPrompt,
161
+ forceStandingPrompt,
176
162
  images,
177
163
  onEvent: async (event) => {
178
164
  if (event?.type === 'turn.started') {
179
- await emitWorkerEvent({
165
+ emitWorkerEventBestEffort({
180
166
  adapter,
181
167
  binding,
182
168
  agent: this.name,
183
- sessionId: event.sessionId || meta.sessionId || binding.id,
169
+ sessionId: event.sessionId || targetSessionId || binding.id,
184
170
  cwd: agentHome,
185
171
  replyToMessageId: inbound.messageId || null,
186
172
  event: {
@@ -189,32 +175,29 @@ export const piRuntime = {
189
175
  },
190
176
  logger: ctx.logger,
191
177
  });
192
- } else if (event?.type === 'message.delta' && event.text) {
193
- deltaAggregator.push(event.text, {
194
- sessionId: event.sessionId || meta.sessionId || binding.id,
195
- cwd: agentHome,
196
- });
197
178
  }
198
179
  },
199
180
  })
200
- : await this.runTurn({ sessionId: shouldRotate ? null : meta.sessionId, cwd: agentHome, piPath: runtimePiPath, agentEnv }, message, { images, standingPrompt });
181
+ : await this.runTurn({ sessionId: targetSessionId, cwd: agentHome, piPath: runtimePiPath, agentEnv }, message, { images, standingPrompt, forceStandingPrompt });
201
182
 
202
- await deltaAggregator.flush();
183
+ const observedSessionId = result?.sessionId || targetSessionId;
184
+ if (observedSessionId) {
185
+ standingPromptSeen.add(`${binding.id}|${observedSessionId}|${protocolOverlayKey}`);
186
+ }
203
187
  const nextBinding = await updateBindingRuntimeMeta(ctx, binding, {
204
- sessionId: result?.sessionId || meta.sessionId,
205
- path: result?.path || meta.path || null,
188
+ ...buildRuntimeSessionMetaPatch(meta, sessionScope, {
189
+ sessionId: result?.sessionId,
190
+ path: result?.path,
191
+ }),
206
192
  runtimePath: runtimePiPath,
207
193
  piPath: runtimePiPath,
208
194
  piVersion: runtimePiVersion,
209
- rotatePending: false,
210
- lastRotatedAt: shouldRotate ? new Date().toISOString() : meta.lastRotatedAt,
211
195
  }, { status: 'connected' });
212
- await recordActivity(adapter, nextBinding, inbound, result);
213
- await emitWorkerEvent({
196
+ emitWorkerEventBestEffort({
214
197
  adapter,
215
198
  binding: nextBinding,
216
199
  agent: this.name,
217
- sessionId: result?.sessionId || meta.sessionId || binding.id,
200
+ sessionId: result?.sessionId || targetSessionId || binding.id,
218
201
  cwd: result?.cwd || agentHome,
219
202
  replyToMessageId: inbound.messageId || null,
220
203
  event: {
@@ -225,12 +208,11 @@ export const piRuntime = {
225
208
  });
226
209
  return true;
227
210
  } catch (err) {
228
- await deltaAggregator.flush().catch(() => {});
229
- await emitWorkerEvent({
211
+ emitWorkerEventBestEffort({
230
212
  adapter,
231
213
  binding,
232
214
  agent: this.name,
233
- sessionId: meta.sessionId || binding.id,
215
+ sessionId: targetSessionId || binding.id,
234
216
  cwd: agentHome,
235
217
  replyToMessageId: inbound.messageId || null,
236
218
  event: {
@@ -258,7 +240,7 @@ export const piRuntime = {
258
240
 
259
241
  async reconcileAfterRestart(binding, ctx) {
260
242
  const meta = binding.runtimeMeta || {};
261
- await emitWorkerEvent({
243
+ emitWorkerEventBestEffort({
262
244
  adapter: ctx.adapter,
263
245
  binding,
264
246
  agent: this.name,
@@ -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) => {
@@ -220,12 +223,13 @@ export function runPiPrompt({
220
223
  let activeSessionFile = null;
221
224
  let finalText = '';
222
225
  let settled = false;
223
- let eventChain = Promise.resolve();
224
226
  const pending = new Map();
225
227
 
226
228
  const emit = (event) => {
227
229
  if (typeof onEvent !== 'function') return;
228
- eventChain = eventChain.then(() => onEvent(event)).catch(() => {});
230
+ void Promise.resolve()
231
+ .then(() => onEvent(event))
232
+ .catch(() => {});
229
233
  };
230
234
 
231
235
  const settle = (fn, value) => {
@@ -237,7 +241,7 @@ export function runPiPrompt({
237
241
  }
238
242
  pending.clear();
239
243
  try { child.kill('SIGTERM'); } catch {}
240
- eventChain.catch(() => {}).finally(() => fn(value));
244
+ fn(value);
241
245
  };
242
246
 
243
247
  const sendRaw = (payload) => {
@@ -305,7 +309,6 @@ export function runPiPrompt({
305
309
  const delta = extractDeltaFromEvent(event);
306
310
  if (delta) {
307
311
  finalText += delta;
308
- emit({ type: 'message.delta', sessionId: activeSessionId, text: delta });
309
312
  }
310
313
  return;
311
314
  }
@@ -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
- }