ticlawk 0.1.16-dev.8 → 0.1.16

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 (41) hide show
  1. package/README.md +15 -3
  2. package/bin/ticlawk.mjs +208 -26
  3. package/package.json +1 -1
  4. package/src/adapters/ticlawk/api.mjs +283 -48
  5. package/src/adapters/ticlawk/credentials.mjs +41 -1
  6. package/src/adapters/ticlawk/index.mjs +126 -121
  7. package/src/adapters/ticlawk/wake-client.mjs +1 -1
  8. package/src/cli/agent-commands.mjs +560 -36
  9. package/src/core/agent-cli-handlers.mjs +435 -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 +119 -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 +108 -77
  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 +55 -0
  23. package/src/runtimes/_shared/handbook/DM_SCOPE.md +13 -0
  24. package/src/runtimes/_shared/handbook/GOAL_AUTHORITY.md +46 -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 +134 -275
  31. package/src/runtimes/_shared/wake-prompt.mjs +261 -0
  32. package/src/runtimes/claude-code/index.mjs +19 -46
  33. package/src/runtimes/claude-code/session.mjs +2 -7
  34. package/src/runtimes/codex/index.mjs +115 -63
  35. package/src/runtimes/codex/session.mjs +2 -12
  36. package/src/runtimes/openclaw/index.mjs +11 -24
  37. package/src/runtimes/opencode/index.mjs +38 -60
  38. package/src/runtimes/opencode/session.mjs +12 -12
  39. package/src/runtimes/pi/index.mjs +38 -60
  40. package/src/runtimes/pi/session.mjs +9 -6
  41. package/ticlawk.mjs +0 -30
@@ -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
 
@@ -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,15 +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
25
  reportSubprocessFailure,
26
26
  terminalRuntimeFailure,
27
27
  updateBindingRuntimeMeta,
28
+ resolveRuntimeSessionScope,
29
+ buildRuntimeSessionMetaPatch,
28
30
  } from '../../core/runtime-support.mjs';
29
31
 
32
+ const standingPromptSeen = new Set();
33
+
30
34
  export const piRuntime = {
31
35
  name: 'pi',
32
36
 
@@ -39,6 +43,7 @@ export const piRuntime = {
39
43
  piPath,
40
44
  agentEnv,
41
45
  standingPrompt: opts.standingPrompt || null,
46
+ forceStandingPrompt: Boolean(opts.forceStandingPrompt),
42
47
  timeoutMs: opts.timeoutMs,
43
48
  });
44
49
  },
@@ -52,6 +57,7 @@ export const piRuntime = {
52
57
  piPath,
53
58
  agentEnv,
54
59
  standingPrompt: opts.standingPrompt || null,
60
+ forceStandingPrompt: Boolean(opts.forceStandingPrompt),
55
61
  timeoutMs: opts.timeoutMs,
56
62
  onEvent: opts.onEvent,
57
63
  });
@@ -128,46 +134,39 @@ export const piRuntime = {
128
134
  message = captionText || 'Please analyze the attached image(s).';
129
135
  }
130
136
 
131
- const shouldRotate = !meta.sessionId || meta.rotatePending;
132
- const deltaAggregator = createDeltaAggregator({
133
- flushDelta: async ({ text, sessionId, cwd }) => {
134
- await emitWorkerEvent({
135
- adapter,
136
- binding,
137
- agent: this.name,
138
- sessionId: sessionId || meta.sessionId || binding.id,
139
- cwd: cwd || agentHome,
140
- replyToMessageId: inbound.messageId || null,
141
- event: {
142
- hook_event_name: 'worker.message.delta',
143
- worker_event_name: 'worker.message.delta',
144
- delta: text,
145
- },
146
- logger: ctx.logger,
147
- });
148
- },
149
- });
137
+ const sessionScope = resolveRuntimeSessionScope(meta, inbound);
138
+ const shouldRotate = sessionScope.shouldRotate;
139
+ const targetSessionId = shouldRotate ? null : sessionScope.sessionId;
150
140
 
151
141
  try {
152
142
  const runtimePiPath = requirePiPath(meta.piPath || meta.runtimePath);
153
143
  const runtimePiVersion = getPiRuntimeHealth(runtimePiPath).version || meta.piVersion || null;
154
144
  const agentEnv = buildAgentRuntimeEnv({
155
145
  agentId: binding.id,
156
- sessionId: meta.sessionId,
146
+ sessionId: targetSessionId,
157
147
  hostId: binding.runtime_host_id,
148
+ conversationId: inbound.conversationId,
149
+ messageId: inbound.messageId,
150
+ target: inbound.envelopeTarget,
158
151
  });
159
- 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));
160
158
  const result = shouldStreamRuntime(this.name, this)
161
- ? 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, {
162
160
  standingPrompt,
161
+ forceStandingPrompt,
163
162
  images,
164
163
  onEvent: async (event) => {
165
164
  if (event?.type === 'turn.started') {
166
- await emitWorkerEvent({
165
+ emitWorkerEventBestEffort({
167
166
  adapter,
168
167
  binding,
169
168
  agent: this.name,
170
- sessionId: event.sessionId || meta.sessionId || binding.id,
169
+ sessionId: event.sessionId || targetSessionId || binding.id,
171
170
  cwd: agentHome,
172
171
  replyToMessageId: inbound.messageId || null,
173
172
  event: {
@@ -176,31 +175,29 @@ export const piRuntime = {
176
175
  },
177
176
  logger: ctx.logger,
178
177
  });
179
- } else if (event?.type === 'message.delta' && event.text) {
180
- deltaAggregator.push(event.text, {
181
- sessionId: event.sessionId || meta.sessionId || binding.id,
182
- cwd: agentHome,
183
- });
184
178
  }
185
179
  },
186
180
  })
187
- : 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 });
188
182
 
189
- await deltaAggregator.flush();
183
+ const observedSessionId = result?.sessionId || targetSessionId;
184
+ if (observedSessionId) {
185
+ standingPromptSeen.add(`${binding.id}|${observedSessionId}|${protocolOverlayKey}`);
186
+ }
190
187
  const nextBinding = await updateBindingRuntimeMeta(ctx, binding, {
191
- sessionId: result?.sessionId || meta.sessionId,
192
- path: result?.path || meta.path || null,
188
+ ...buildRuntimeSessionMetaPatch(meta, sessionScope, {
189
+ sessionId: result?.sessionId,
190
+ path: result?.path,
191
+ }),
193
192
  runtimePath: runtimePiPath,
194
193
  piPath: runtimePiPath,
195
194
  piVersion: runtimePiVersion,
196
- rotatePending: false,
197
- lastRotatedAt: shouldRotate ? new Date().toISOString() : meta.lastRotatedAt,
198
195
  }, { status: 'connected' });
199
- await emitWorkerEvent({
196
+ emitWorkerEventBestEffort({
200
197
  adapter,
201
198
  binding: nextBinding,
202
199
  agent: this.name,
203
- sessionId: result?.sessionId || meta.sessionId || binding.id,
200
+ sessionId: result?.sessionId || targetSessionId || binding.id,
204
201
  cwd: result?.cwd || agentHome,
205
202
  replyToMessageId: inbound.messageId || null,
206
203
  event: {
@@ -211,12 +208,11 @@ export const piRuntime = {
211
208
  });
212
209
  return true;
213
210
  } catch (err) {
214
- await deltaAggregator.flush().catch(() => {});
215
- await emitWorkerEvent({
211
+ emitWorkerEventBestEffort({
216
212
  adapter,
217
213
  binding,
218
214
  agent: this.name,
219
- sessionId: meta.sessionId || binding.id,
215
+ sessionId: targetSessionId || binding.id,
220
216
  cwd: agentHome,
221
217
  replyToMessageId: inbound.messageId || null,
222
218
  event: {
@@ -242,24 +238,6 @@ export const piRuntime = {
242
238
  }
243
239
  },
244
240
 
245
- async reconcileAfterRestart(binding, ctx) {
246
- const meta = binding.runtimeMeta || {};
247
- await emitWorkerEvent({
248
- adapter: ctx.adapter,
249
- binding,
250
- agent: this.name,
251
- sessionId: meta.sessionId || binding.id,
252
- cwd: ensureAgentHome(binding.id) || '',
253
- event: {
254
- hook_event_name: 'Stop',
255
- worker_event_name: 'worker.turn.complete',
256
- reason: 'connector.restart.reconcile',
257
- },
258
- logger: ctx.logger,
259
- });
260
- return 1;
261
- },
262
-
263
241
  sessionsDir: PI_SESSIONS_DIR,
264
242
  maxAgeMs: PI_MAX_AGE_MS,
265
243
  };
@@ -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
  }
package/ticlawk.mjs CHANGED
@@ -211,35 +211,6 @@ async function recoverAllRuntimes(runtimeList, adapter) {
211
211
  }
212
212
  }
213
213
 
214
- async function reconcileBindingsAfterRestart(runtimes, adapter) {
215
- const hostId = getHostId();
216
- for (const binding of listBindings({ adapter: adapter.id })) {
217
- if (!belongsToRuntimeHost(binding, hostId)) {
218
- logger.debugError('core', 'binding.reconcile-host-mismatch', {
219
- bindingId: binding.id,
220
- adapter: binding.adapter,
221
- hostId,
222
- runtime_host_id: getBindingRuntimeHostId(binding),
223
- });
224
- continue;
225
- }
226
- const runtime = binding.runtime ? runtimes[binding.runtime] : null;
227
- if (typeof runtime?.reconcileAfterRestart !== 'function') continue;
228
- try {
229
- await runtime.reconcileAfterRestart(binding, {
230
- adapter,
231
- logger,
232
- });
233
- } catch (err) {
234
- logger.debugError('startup', 'reconcileAfterRestart.failed', {
235
- runtime: binding.runtime,
236
- bindingId: binding.id,
237
- error: err?.message || String(err),
238
- });
239
- }
240
- }
241
- }
242
-
243
214
  export async function startTiclawk() {
244
215
  if (started) return;
245
216
  started = true;
@@ -278,7 +249,6 @@ export async function startTiclawk() {
278
249
  });
279
250
  startReminderTicker();
280
251
  await recoverAllRuntimes(runtimeList, adapter);
281
- await reconcileBindingsAfterRestart(runtimes, adapter);
282
252
  await adapter.start();
283
253
  }
284
254