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 +1 -1
- package/src/adapters/ticlawk/api.mjs +61 -47
- package/src/adapters/ticlawk/index.mjs +3 -4
- package/src/core/events/worker-events.mjs +32 -36
- package/src/core/runtime-support.mjs +16 -86
- package/src/runtimes/_shared/handbook/COMMUNICATION.md +2 -0
- package/src/runtimes/_shared/standing-prompt.mjs +3 -1
- package/src/runtimes/claude-code/index.mjs +5 -20
- package/src/runtimes/claude-code/session.mjs +2 -7
- package/src/runtimes/codex/index.mjs +5 -33
- package/src/runtimes/codex/session.mjs +2 -12
- package/src/runtimes/opencode/index.mjs +5 -32
- package/src/runtimes/opencode/session.mjs +7 -8
- package/src/runtimes/pi/index.mjs +5 -31
- package/src/runtimes/pi/session.mjs +4 -4
package/package.json
CHANGED
|
@@ -30,8 +30,9 @@ export function getConnectorWsUrl() {
|
|
|
30
30
|
return DEFAULT_CONNECTOR_WS_URL;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
//
|
|
34
|
-
//
|
|
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(
|
|
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
|
-
//
|
|
1172
|
-
//
|
|
1173
|
-
//
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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 `
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
197
|
-
*
|
|
198
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|