ticlawk 0.1.15 → 0.1.16-dev.10
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/README.md +96 -212
- package/bin/ticlawk.mjs +223 -46
- package/package.json +2 -5
- package/src/adapters/ticlawk/api.mjs +308 -43
- package/src/adapters/ticlawk/credentials.mjs +1 -2
- package/src/adapters/ticlawk/index.mjs +310 -119
- package/src/cli/agent-commands.mjs +876 -0
- package/src/core/adapter-registry.mjs +12 -28
- package/src/core/agent-cli-handlers.mjs +731 -0
- package/src/core/agent-home.mjs +85 -0
- package/src/core/config.mjs +0 -15
- package/src/core/http.mjs +211 -18
- package/src/core/reminder-ticker.mjs +70 -0
- package/src/core/runtime-contract.mjs +1 -1
- package/src/core/runtime-env.mjs +41 -5
- package/src/core/runtime-support.mjs +31 -44
- package/src/core/ticlawk-control.mjs +7 -6
- package/src/migrate/write-initial-memory.mjs +101 -0
- package/src/runtimes/_shared/standing-prompt.mjs +308 -0
- package/src/runtimes/claude-code/index.mjs +49 -133
- package/src/runtimes/claude-code/session.mjs +15 -7
- package/src/runtimes/codex/index.mjs +29 -41
- package/src/runtimes/codex/session.mjs +9 -5
- package/src/runtimes/openclaw/index.mjs +59 -31
- package/src/runtimes/openclaw/target.mjs +0 -30
- package/src/runtimes/opencode/index.mjs +34 -56
- package/src/runtimes/opencode/session.mjs +11 -2
- package/src/runtimes/pi/index.mjs +31 -51
- package/src/runtimes/pi/session.mjs +8 -2
- package/ticlawk.mjs +37 -10
- package/assets/ticlawk-concept.svg +0 -137
- package/src/adapters/telegram/index.mjs +0 -359
- package/src/adapters/ticlawk/cards.mjs +0 -149
- package/src/core/media/outbound.mjs +0 -163
|
@@ -2,16 +2,14 @@ import { parseOptionArgs } from '../../core/argv.mjs';
|
|
|
2
2
|
import { createHash, randomBytes } from 'node:crypto';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import { basename } from 'node:path';
|
|
5
|
-
import {
|
|
5
|
+
import { loadPersistentConfig, persistConfig, TICLAWK_CONNECTOR_API_KEY, TICLAWK_CONNECTOR_WS_URL } from '../../core/config.mjs';
|
|
6
6
|
import { belongsToRuntimeHost, getBindingRuntimeHostId, getHostId, getHostLabel } from '../../core/host-id.mjs';
|
|
7
7
|
import { debugError, debugLog } from '../../core/logger.mjs';
|
|
8
8
|
import { getActiveProfile, ensureLegacyProfile, readProfileMeta, saveAndActivateProfile } from '../../core/profiles.mjs';
|
|
9
9
|
import { isTerminalRuntimeFailure } from '../../core/runtime-support.mjs';
|
|
10
10
|
import { clearUpdateRequiredState, readUpdateState, setUpdateRequiredState } from '../../core/update-state.mjs';
|
|
11
11
|
import { isManagedInstall, startDetachedSelfUpdate } from '../../core/update.mjs';
|
|
12
|
-
import { resolveOpenClawWorkspace } from '../../runtimes/openclaw/target.mjs';
|
|
13
12
|
import * as api from './api.mjs';
|
|
14
|
-
import { processAndSaveResult } from './cards.mjs';
|
|
15
13
|
import { persistApiCredential } from './credentials.mjs';
|
|
16
14
|
import { TiclawkWakeClient } from './wake-client.mjs';
|
|
17
15
|
|
|
@@ -28,6 +26,32 @@ function connectError(statusCode, error) {
|
|
|
28
26
|
return { statusCode, body: { ok: false, error } };
|
|
29
27
|
}
|
|
30
28
|
|
|
29
|
+
// Coalesce: returns a wrapped fn that runs at most one invocation at a
|
|
30
|
+
// time. If it's called while an invocation is in flight, the most recent
|
|
31
|
+
// args are stashed and the wrapped fn re-runs exactly once after the
|
|
32
|
+
// current run completes (regardless of how many times it was called
|
|
33
|
+
// during the run). The wrapped fn always returns the in-flight Promise.
|
|
34
|
+
function coalesce(fn) {
|
|
35
|
+
let running = null;
|
|
36
|
+
let pendingArgs = null;
|
|
37
|
+
return function call(...args) {
|
|
38
|
+
if (running) {
|
|
39
|
+
pendingArgs = args;
|
|
40
|
+
return running;
|
|
41
|
+
}
|
|
42
|
+
running = (async () => {
|
|
43
|
+
let currentArgs = args;
|
|
44
|
+
while (true) {
|
|
45
|
+
pendingArgs = null;
|
|
46
|
+
await fn(...currentArgs);
|
|
47
|
+
if (pendingArgs === null) return;
|
|
48
|
+
currentArgs = pendingArgs;
|
|
49
|
+
}
|
|
50
|
+
})().finally(() => { running = null; });
|
|
51
|
+
return running;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
31
55
|
function normalizeInboundMediaAssets(msg) {
|
|
32
56
|
if (!Array.isArray(msg?.media_assets)) return [];
|
|
33
57
|
return msg.media_assets
|
|
@@ -45,13 +69,132 @@ function normalizeInboundMediaAssets(msg) {
|
|
|
45
69
|
.filter(Boolean);
|
|
46
70
|
}
|
|
47
71
|
|
|
48
|
-
function
|
|
72
|
+
function buildEnvelopeTarget(msg) {
|
|
73
|
+
const convType = msg.conversation_type || 'dm';
|
|
74
|
+
const conversationId = msg.conversation_id || '';
|
|
75
|
+
const senderHandle = msg.sender_display_name || msg.sender_user_id || msg.sender_agent_id || '';
|
|
76
|
+
if (convType === 'dm') {
|
|
77
|
+
return senderHandle ? `dm:@${senderHandle}` : `dm:${conversationId}`;
|
|
78
|
+
}
|
|
79
|
+
if (convType === 'thread') {
|
|
80
|
+
const groupName = msg.conversation_name || conversationId;
|
|
81
|
+
const threadRoot = msg.thread_root_message_id || msg.message_id || '';
|
|
82
|
+
return `#${groupName}:${threadRoot}`;
|
|
83
|
+
}
|
|
84
|
+
// group
|
|
85
|
+
const groupName = msg.conversation_name || conversationId;
|
|
86
|
+
return `#${groupName}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function buildTaskSuffix(msg) {
|
|
90
|
+
if (msg.task_number == null) return '';
|
|
91
|
+
const status = msg.task_status || 'todo';
|
|
92
|
+
const parts = [`task #${msg.task_number} status=${status}`];
|
|
93
|
+
if (msg.task_assignee_agent_id || msg.task_assignee_user_id) {
|
|
94
|
+
const t = msg.task_assignee_type || 'agent';
|
|
95
|
+
const id = msg.task_assignee_agent_id || msg.task_assignee_user_id;
|
|
96
|
+
parts.push(`assignee=${t}:${id}`);
|
|
97
|
+
}
|
|
98
|
+
if (msg.task_title) {
|
|
99
|
+
parts.push(`title=${JSON.stringify(msg.task_title)}`);
|
|
100
|
+
}
|
|
101
|
+
return ` [${parts.join(' ')}]`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function buildReactionsSuffix(msg) {
|
|
105
|
+
const entries = Array.isArray(msg.reactions_summary) ? msg.reactions_summary : [];
|
|
106
|
+
if (entries.length === 0) return '';
|
|
107
|
+
return ` [reactions: ${entries.join('; ')}]`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function buildEnvelopeHeader(msg) {
|
|
111
|
+
const target = buildEnvelopeTarget(msg);
|
|
112
|
+
const msgId = msg.id || msg.message_id || '';
|
|
113
|
+
const seq = msg.seq != null ? msg.seq : '';
|
|
114
|
+
const time = msg.created_at || new Date().toISOString();
|
|
115
|
+
const type = msg.sender_type || 'human';
|
|
116
|
+
const sender = msg.sender_display_name || msg.sender_user_id || msg.sender_agent_id || 'unknown';
|
|
117
|
+
// `reason` tells the agent how this delivery was routed: 'mention'
|
|
118
|
+
// / 'assignment' = you were directly addressed → respond by default;
|
|
119
|
+
// 'ambient' = you are in the room and saw it → respond only if
|
|
120
|
+
// clearly the right responder; 'dm' / 'thread_follow' / 'manual' =
|
|
121
|
+
// the legacy direct paths. The agent's behaviour split is in the
|
|
122
|
+
// standing prompt; we just surface the field.
|
|
123
|
+
const reason = msg.reason ? ` reason=${msg.reason}` : '';
|
|
124
|
+
return `[target=${target} msg=${msgId} seq=${seq} time=${time} type=${type}${reason}] @${sender}:`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function buildGroupContextBlock(msg) {
|
|
128
|
+
// Only useful in groups — DMs don't need group-purpose context.
|
|
129
|
+
if ((msg.conversation_type || 'dm') !== 'group') return '';
|
|
130
|
+
const lines = [];
|
|
131
|
+
const name = msg.conversation_name || msg.conversation_display_name || '';
|
|
132
|
+
if (name) lines.push(`name: ${name}`);
|
|
133
|
+
const description = (msg.conversation_description || '').trim();
|
|
134
|
+
if (description) lines.push(`purpose: ${description}`);
|
|
135
|
+
if (lines.length === 0) return '';
|
|
136
|
+
return [
|
|
137
|
+
'Group context:',
|
|
138
|
+
...lines.map((l) => ` ${l}`),
|
|
139
|
+
'Use `ticlawk group members --target <target>` if you need to see who else is here.',
|
|
140
|
+
].join('\n');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Wrap each per-turn message with an explicit reply instruction so the
|
|
144
|
+
// runtime LLM never has to remember the standing prompt to figure out
|
|
145
|
+
// HOW to reply. Codex in particular treats the developerInstructions as
|
|
146
|
+
// background and ignores the chat-send pattern without this per-turn
|
|
147
|
+
// nudge.
|
|
148
|
+
function buildWakePromptText({ envelopeHeader, target, rawText, groupContext }) {
|
|
149
|
+
const body = `${envelopeHeader} ${rawText || ''}`.trim();
|
|
150
|
+
const lines = ['New message received:', '', body];
|
|
151
|
+
if (groupContext) {
|
|
152
|
+
lines.push('', groupContext);
|
|
153
|
+
}
|
|
154
|
+
lines.push(
|
|
155
|
+
'',
|
|
156
|
+
`Respond as appropriate — reply using \`ticlawk message send --target "${target}"\` (body via stdin / heredoc), or take action as needed. Complete ALL your work before stopping.`,
|
|
157
|
+
'Reply in the channel or create/reply in a thread as appropriate; use each message\'s `target` and `msg` fields to choose the exact target.',
|
|
158
|
+
'',
|
|
159
|
+
'IMPORTANT: If the message requires multi-step work (research, code changes, testing), complete ALL steps before stopping. Sending a progress update does NOT mean your task is done — only stop when you have NO more work to do. The daemon will wake you again automatically when new messages arrive.',
|
|
160
|
+
);
|
|
161
|
+
return lines.join('\n');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function normalizeInboundMessage(msg) {
|
|
165
|
+
// Claimed delivery rows carry the recipient agent id; legacy messages
|
|
166
|
+
// (history sync, manual inserts) may still use plain agent_id. Prefer
|
|
167
|
+
// the new field but fall back so the same normalizer works for both.
|
|
168
|
+
const recipientAgentId = msg.recipient_agent_id || '';
|
|
49
169
|
const messageId = msg.id || msg.message_id || null;
|
|
170
|
+
const deliveryId = msg.delivery_id || null;
|
|
50
171
|
const media = normalizeInboundMediaAssets(msg);
|
|
172
|
+
const rawText = msg.text || '';
|
|
173
|
+
const enriched = { ...msg, id: messageId };
|
|
174
|
+
const baseHeader = buildEnvelopeHeader(enriched);
|
|
175
|
+
// Task + reactions suffixes are appended INSIDE the envelope so an
|
|
176
|
+
// agent can see at a glance whether a message is already claimed and
|
|
177
|
+
// who has already acknowledged it: `[task #N status=… assignee=…]` +
|
|
178
|
+
// `[reactions: …]`.
|
|
179
|
+
const taskSuffix = buildTaskSuffix(enriched);
|
|
180
|
+
const reactionsSuffix = buildReactionsSuffix(enriched);
|
|
181
|
+
const header = baseHeader + taskSuffix + reactionsSuffix;
|
|
182
|
+
const target = buildEnvelopeTarget(enriched);
|
|
183
|
+
const groupContext = buildGroupContextBlock(enriched);
|
|
184
|
+
const text = header
|
|
185
|
+
? buildWakePromptText({ envelopeHeader: header, target, rawText, groupContext })
|
|
186
|
+
: rawText;
|
|
51
187
|
return {
|
|
52
|
-
bindingId:
|
|
188
|
+
bindingId: recipientAgentId,
|
|
53
189
|
messageId,
|
|
54
|
-
|
|
190
|
+
deliveryId,
|
|
191
|
+
conversationId: msg.conversation_id || null,
|
|
192
|
+
seq: msg.seq != null ? Number(msg.seq) : null,
|
|
193
|
+
senderType: msg.sender_type || 'human',
|
|
194
|
+
envelopeHeader: header,
|
|
195
|
+
envelopeTarget: target,
|
|
196
|
+
text,
|
|
197
|
+
rawText,
|
|
55
198
|
action: msg.action || (media.length > 0 ? 'image' : 'task'),
|
|
56
199
|
media,
|
|
57
200
|
raw: {
|
|
@@ -61,14 +204,6 @@ function normalizeInboundMessage(msg) {
|
|
|
61
204
|
};
|
|
62
205
|
}
|
|
63
206
|
|
|
64
|
-
function getBindingSessionId(binding) {
|
|
65
|
-
return binding?.runtimeMeta?.sessionId || binding?.runtimeMeta?.sessionKey || binding?.runtimeMeta?.agentId || binding?.id || null;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function getWorkdir(meta = {}) {
|
|
69
|
-
return String(meta.workdir || meta.projectDir || meta.cwd || '').trim();
|
|
70
|
-
}
|
|
71
|
-
|
|
72
207
|
function getRuntimeVersion(bindingOrMeta) {
|
|
73
208
|
const value = bindingOrMeta?.runtimeMeta?.runtimeVersion ?? bindingOrMeta?.runtimeVersion ?? bindingOrMeta?.agent_runtime_version ?? null;
|
|
74
209
|
return Number.isInteger(value) ? Number(value) : null;
|
|
@@ -91,60 +226,101 @@ function getRuntimeHostLabelFromPayload(payload) {
|
|
|
91
226
|
}
|
|
92
227
|
|
|
93
228
|
function getAgentIdFromPayload(payload) {
|
|
94
|
-
|
|
229
|
+
// claim_pending_deliveries is the canonical source and returns
|
|
230
|
+
// `recipient_agent_id`. The historical fallbacks (`agent_id`,
|
|
231
|
+
// `agentId`) were left in by an earlier partial cleanup but they
|
|
232
|
+
// never fire against the current schema — dead branches.
|
|
233
|
+
return String(payload?.recipient_agent_id || '').trim();
|
|
95
234
|
}
|
|
96
235
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
236
|
+
// Whitelist of meta keys runtimes actually consume. Anything else in
|
|
237
|
+
// the source row's meta blob is dropped on the floor so stale fields
|
|
238
|
+
// (like the post-Y2 workdir/cwd/projectDir leftovers) can't sneak
|
|
239
|
+
// back into runtimeMeta and confuse downstream code.
|
|
240
|
+
const RUNTIME_META_KEYS = [
|
|
241
|
+
'sessionId',
|
|
242
|
+
'path',
|
|
243
|
+
'runtimePath',
|
|
244
|
+
'rotatePending',
|
|
245
|
+
'lastRotatedAt',
|
|
246
|
+
// claude_code
|
|
247
|
+
'claudePath',
|
|
248
|
+
'claudeVersion',
|
|
249
|
+
'project',
|
|
250
|
+
// codex
|
|
251
|
+
'codexPath',
|
|
252
|
+
'codexVersion',
|
|
253
|
+
// opencode
|
|
254
|
+
'opencodePath',
|
|
255
|
+
'opencodeVersion',
|
|
256
|
+
// pi
|
|
257
|
+
'piPath',
|
|
258
|
+
'piVersion',
|
|
259
|
+
// openclaw (gateway-based)
|
|
260
|
+
'agentId',
|
|
261
|
+
'sessionKey',
|
|
262
|
+
'gatewayHost',
|
|
263
|
+
'gatewayPort',
|
|
264
|
+
'lastGatewayFailureAt',
|
|
265
|
+
'lastGatewayFailureReason',
|
|
266
|
+
];
|
|
267
|
+
|
|
268
|
+
function projectRuntimeMeta(source) {
|
|
269
|
+
const out = {};
|
|
270
|
+
if (!source || typeof source !== 'object') return out;
|
|
271
|
+
for (const key of RUNTIME_META_KEYS) {
|
|
272
|
+
if (source[key] !== undefined) out[key] = source[key];
|
|
110
273
|
}
|
|
111
|
-
|
|
112
|
-
|
|
274
|
+
return out;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function normalizeRuntimeVersion(value) {
|
|
278
|
+
return Number.isInteger(value) ? Number(value) : 0;
|
|
279
|
+
}
|
|
113
280
|
|
|
281
|
+
// Build a binding from any source row that carries agent metadata.
|
|
282
|
+
// Two source shapes feed in:
|
|
283
|
+
// /api/agents row — id, service_type, meta, …
|
|
284
|
+
// claim_pending_deliveries — recipient_agent_id, agent_service_type,
|
|
285
|
+
// agent_meta, … (prefixed because the
|
|
286
|
+
// delivery row also carries message fields)
|
|
287
|
+
// We try the prefixed names first, then fall through to the bare names,
|
|
288
|
+
// so one builder handles both without the caller having to remember
|
|
289
|
+
// which shape it has.
|
|
290
|
+
function buildBindingFromSource(source) {
|
|
291
|
+
const agentId = String(
|
|
292
|
+
source?.recipient_agent_id
|
|
293
|
+
|| source?.id
|
|
294
|
+
|| source?.agent_id
|
|
295
|
+
|| ''
|
|
296
|
+
).trim();
|
|
297
|
+
const runtime = source?.agent_service_type || source?.service_type;
|
|
298
|
+
const meta = source?.agent_meta ?? source?.meta;
|
|
299
|
+
const runtimeVersion = source?.agent_runtime_version ?? source?.runtime_version;
|
|
300
|
+
const displayName = source?.agent_display_name
|
|
301
|
+
|| source?.agent_name
|
|
302
|
+
|| source?.display_name
|
|
303
|
+
|| source?.name
|
|
304
|
+
|| agentId;
|
|
305
|
+
const status = source?.agent_status || source?.status || 'connected';
|
|
306
|
+
const hostId = getRuntimeHostIdFromPayload(source) || undefined;
|
|
114
307
|
return {
|
|
115
308
|
id: agentId,
|
|
116
309
|
adapter: 'ticlawk',
|
|
117
310
|
targetKey: agentId,
|
|
118
|
-
targetMeta: {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
},
|
|
122
|
-
runtime_host_id: getRuntimeHostIdFromPayload(msg) || undefined,
|
|
123
|
-
runtime_host_label: getRuntimeHostLabelFromPayload(msg) || undefined,
|
|
311
|
+
targetMeta: { agentId, runtime_host_id: hostId },
|
|
312
|
+
runtime_host_id: hostId,
|
|
313
|
+
runtime_host_label: getRuntimeHostLabelFromPayload(source) || undefined,
|
|
124
314
|
runtime,
|
|
125
315
|
runtimeMeta: {
|
|
126
|
-
...
|
|
127
|
-
runtimeVersion,
|
|
316
|
+
...projectRuntimeMeta(meta),
|
|
317
|
+
runtimeVersion: normalizeRuntimeVersion(runtimeVersion),
|
|
128
318
|
},
|
|
129
|
-
displayName
|
|
130
|
-
status
|
|
319
|
+
displayName,
|
|
320
|
+
status,
|
|
131
321
|
};
|
|
132
322
|
}
|
|
133
323
|
|
|
134
|
-
function buildBindingFromChannelSnapshot(agent) {
|
|
135
|
-
return buildBindingFromClaimedMessage({
|
|
136
|
-
agent_id: agent.id || agent.agent_id,
|
|
137
|
-
agent_name: agent.name,
|
|
138
|
-
agent_display_name: agent.display_name,
|
|
139
|
-
agent_status: agent.status,
|
|
140
|
-
agent_service_type: agent.service_type,
|
|
141
|
-
agent_meta: agent.meta,
|
|
142
|
-
agent_runtime_version: agent.runtime_version,
|
|
143
|
-
runtime_host_id: agent.runtime_host_id,
|
|
144
|
-
runtime_host_label: agent.runtime_host_label,
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
|
|
148
324
|
function maskIdentity(identity = {}) {
|
|
149
325
|
return {
|
|
150
326
|
userId: identity.userId || identity.user_id || identity.id || null,
|
|
@@ -296,8 +472,6 @@ export function createTiclawkAdapter(ctx) {
|
|
|
296
472
|
let bindingAuditTimer = null;
|
|
297
473
|
let jobsWakeTimer = null;
|
|
298
474
|
let bindingsWakeTimer = null;
|
|
299
|
-
let drainPromise = null;
|
|
300
|
-
let drainRequested = false;
|
|
301
475
|
let lastJobsWakeAt = 0;
|
|
302
476
|
let lastBindingsWakeAt = 0;
|
|
303
477
|
let updateRequired = null;
|
|
@@ -458,12 +632,14 @@ export function createTiclawkAdapter(ctx) {
|
|
|
458
632
|
|
|
459
633
|
async function processPendingMessagesForAgent(agentId, messages) {
|
|
460
634
|
for (const msg of messages) {
|
|
635
|
+
const deliveryId = msg.delivery_id;
|
|
461
636
|
try {
|
|
462
637
|
const messageHostId = getRuntimeHostIdFromPayload(msg);
|
|
463
638
|
if (messageHostId && messageHostId !== hostId) {
|
|
464
|
-
await api.
|
|
639
|
+
await api.releaseDelivery(deliveryId, hostId, 'host-mismatch');
|
|
465
640
|
debugError('ticlawk', 'message.host-mismatch', {
|
|
466
641
|
agentId,
|
|
642
|
+
deliveryId,
|
|
467
643
|
messageId: msg.message_id,
|
|
468
644
|
hostId,
|
|
469
645
|
runtime_host_id: messageHostId,
|
|
@@ -471,14 +647,15 @@ export function createTiclawkAdapter(ctx) {
|
|
|
471
647
|
continue;
|
|
472
648
|
}
|
|
473
649
|
|
|
474
|
-
const binding = await ctx.
|
|
650
|
+
const binding = await ctx.persistBinding(buildBindingFromSource(msg));
|
|
475
651
|
if (!binding?.runtime) {
|
|
476
652
|
throw new Error('claimed message missing runtime binding');
|
|
477
653
|
}
|
|
478
654
|
if (!belongsToRuntimeHost(binding, hostId)) {
|
|
479
|
-
await api.
|
|
655
|
+
await api.releaseDelivery(deliveryId, hostId, 'binding-host-mismatch');
|
|
480
656
|
debugError('ticlawk', 'message.binding-host-mismatch', {
|
|
481
657
|
agentId,
|
|
658
|
+
deliveryId,
|
|
482
659
|
messageId: msg.message_id,
|
|
483
660
|
hostId,
|
|
484
661
|
runtime_host_id: getBindingRuntimeHostId(binding),
|
|
@@ -489,10 +666,11 @@ export function createTiclawkAdapter(ctx) {
|
|
|
489
666
|
const completed = await ctx.bus.dispatchToAgent(binding.runtime, binding.id, normalizeInboundMessage(msg));
|
|
490
667
|
if (completed !== true) {
|
|
491
668
|
if (isTerminalRuntimeFailure(completed)) {
|
|
492
|
-
await api.
|
|
669
|
+
await api.completeDelivery(deliveryId, hostId);
|
|
493
670
|
void requestDrain('message.terminal-completed');
|
|
494
671
|
debugError('ticlawk', 'message.terminal-failed', {
|
|
495
672
|
agentId,
|
|
673
|
+
deliveryId,
|
|
496
674
|
messageId: msg.message_id,
|
|
497
675
|
runtime: binding.runtime,
|
|
498
676
|
runtimeVersion: getRuntimeVersion(binding),
|
|
@@ -502,10 +680,11 @@ export function createTiclawkAdapter(ctx) {
|
|
|
502
680
|
}
|
|
503
681
|
throw new Error('runtime did not complete turn');
|
|
504
682
|
}
|
|
505
|
-
await api.
|
|
683
|
+
await api.completeDelivery(deliveryId, hostId);
|
|
506
684
|
void requestDrain('message.completed');
|
|
507
685
|
debugLog('ticlawk', 'message.completed', {
|
|
508
686
|
agentId,
|
|
687
|
+
deliveryId,
|
|
509
688
|
messageId: msg.message_id,
|
|
510
689
|
runtime: binding.runtime,
|
|
511
690
|
runtimeVersion: getRuntimeVersion(binding),
|
|
@@ -515,7 +694,7 @@ export function createTiclawkAdapter(ctx) {
|
|
|
515
694
|
recordUpdateRequired(err, 'message.dispatch');
|
|
516
695
|
}
|
|
517
696
|
try {
|
|
518
|
-
await api.
|
|
697
|
+
await api.releaseDelivery(deliveryId, hostId, 'dispatch-error');
|
|
519
698
|
} catch (releaseErr) {
|
|
520
699
|
debugError('ticlawk', 'message.release-failed', {
|
|
521
700
|
agentId,
|
|
@@ -568,7 +747,7 @@ export function createTiclawkAdapter(ctx) {
|
|
|
568
747
|
continue;
|
|
569
748
|
}
|
|
570
749
|
try {
|
|
571
|
-
await ctx.
|
|
750
|
+
await ctx.persistBinding(buildBindingFromSource(agent));
|
|
572
751
|
hydrated += 1;
|
|
573
752
|
} catch (err) {
|
|
574
753
|
debugError('ticlawk', 'binding.hydrate-failed', {
|
|
@@ -592,13 +771,14 @@ export function createTiclawkAdapter(ctx) {
|
|
|
592
771
|
|
|
593
772
|
async function releaseBlockedRows(agentId, messages, reason) {
|
|
594
773
|
for (const msg of messages) {
|
|
595
|
-
if (!msg?.
|
|
774
|
+
if (!msg?.delivery_id) continue;
|
|
596
775
|
try {
|
|
597
|
-
await api.
|
|
776
|
+
await api.releaseDelivery(msg.delivery_id, hostId, reason);
|
|
598
777
|
} catch (err) {
|
|
599
778
|
debugError('ticlawk', 'claim.blocked-release-failed', {
|
|
600
779
|
reason,
|
|
601
780
|
agentId,
|
|
781
|
+
deliveryId: msg.delivery_id,
|
|
602
782
|
messageId: msg.message_id,
|
|
603
783
|
error: err?.message || 'unknown error',
|
|
604
784
|
});
|
|
@@ -620,7 +800,7 @@ export function createTiclawkAdapter(ctx) {
|
|
|
620
800
|
return { failed: true, claimed: 0, launched: 0, updateRequired: true };
|
|
621
801
|
}
|
|
622
802
|
try {
|
|
623
|
-
data = await api.
|
|
803
|
+
data = await api.claimPendingDeliveries(hostId, 5, excludedChannelIds);
|
|
624
804
|
clearUpdateRequired('claim');
|
|
625
805
|
} catch (err) {
|
|
626
806
|
if (api.isUpdateRequiredError(err)) {
|
|
@@ -662,7 +842,18 @@ export function createTiclawkAdapter(ctx) {
|
|
|
662
842
|
|
|
663
843
|
const grouped = new Map();
|
|
664
844
|
for (const msg of data) {
|
|
665
|
-
const agentId = getAgentIdFromPayload(msg)
|
|
845
|
+
const agentId = getAgentIdFromPayload(msg);
|
|
846
|
+
if (!agentId) {
|
|
847
|
+
// Claim rows must carry the recipient agent id; a missing value
|
|
848
|
+
// is a contract violation upstream, not something we paper over
|
|
849
|
+
// with a synthetic key.
|
|
850
|
+
debugError('ticlawk', 'claim.missing-agent-id', {
|
|
851
|
+
reason,
|
|
852
|
+
deliveryId: msg?.delivery_id,
|
|
853
|
+
messageId: msg?.message_id,
|
|
854
|
+
});
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
666
857
|
const bucket = grouped.get(agentId) || [];
|
|
667
858
|
bucket.push(msg);
|
|
668
859
|
grouped.set(agentId, bucket);
|
|
@@ -706,23 +897,7 @@ export function createTiclawkAdapter(ctx) {
|
|
|
706
897
|
return { totalClaimed, iterations };
|
|
707
898
|
}
|
|
708
899
|
|
|
709
|
-
|
|
710
|
-
if (drainPromise) {
|
|
711
|
-
drainRequested = true;
|
|
712
|
-
return drainPromise;
|
|
713
|
-
}
|
|
714
|
-
drainPromise = (async () => {
|
|
715
|
-
let currentReason = reason;
|
|
716
|
-
do {
|
|
717
|
-
drainRequested = false;
|
|
718
|
-
await runDrain(currentReason);
|
|
719
|
-
currentReason = 'drain.requested-again';
|
|
720
|
-
} while (drainRequested);
|
|
721
|
-
})().finally(() => {
|
|
722
|
-
drainPromise = null;
|
|
723
|
-
});
|
|
724
|
-
return drainPromise;
|
|
725
|
-
}
|
|
900
|
+
const requestDrain = coalesce(runDrain);
|
|
726
901
|
|
|
727
902
|
function scheduleDrain(reason) {
|
|
728
903
|
jobsWakeTimer = clearDebounce(jobsWakeTimer);
|
|
@@ -744,12 +919,46 @@ export function createTiclawkAdapter(ctx) {
|
|
|
744
919
|
bindingsWakeTimer.unref?.();
|
|
745
920
|
}
|
|
746
921
|
|
|
922
|
+
async function reportHostCapabilitiesNow() {
|
|
923
|
+
const entries = await Promise.all(Object.entries(ctx.runtimes || {})
|
|
924
|
+
.filter(([, runtime]) => typeof runtime?.health === 'function')
|
|
925
|
+
.map(async ([name, runtime]) => {
|
|
926
|
+
try {
|
|
927
|
+
return [name, await runtime.health()];
|
|
928
|
+
} catch (err) {
|
|
929
|
+
return [name, { available: false, error: err?.message || 'health probe failed' }];
|
|
930
|
+
}
|
|
931
|
+
}));
|
|
932
|
+
const runtimesHealth = Object.fromEntries(entries);
|
|
933
|
+
try {
|
|
934
|
+
await api.reportHostCapabilities({
|
|
935
|
+
hostId,
|
|
936
|
+
hostLabel,
|
|
937
|
+
runtimesHealth,
|
|
938
|
+
daemonVersion: api.getTiclawkVersion(),
|
|
939
|
+
});
|
|
940
|
+
debugLog('ticlawk', 'host.capabilities.reported', {
|
|
941
|
+
hostId,
|
|
942
|
+
runtimes: Object.entries(runtimesHealth)
|
|
943
|
+
.filter(([, v]) => v?.available)
|
|
944
|
+
.map(([k]) => k)
|
|
945
|
+
.join(','),
|
|
946
|
+
});
|
|
947
|
+
} catch (err) {
|
|
948
|
+
debugError('ticlawk', 'host.capabilities.failed', {
|
|
949
|
+
hostId,
|
|
950
|
+
error: err?.message || 'report failed',
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
747
955
|
function handleWakeEvent(event) {
|
|
748
956
|
wakeState.lastEventAt = new Date().toISOString();
|
|
749
957
|
if (event?.type === 'hello') {
|
|
750
958
|
void refreshBindings('wake.hello')
|
|
751
959
|
.then(() => requestDrain('wake.hello'))
|
|
752
960
|
.catch(() => {});
|
|
961
|
+
void reportHostCapabilitiesNow();
|
|
753
962
|
return;
|
|
754
963
|
}
|
|
755
964
|
if (event?.type === 'jobs.available') {
|
|
@@ -954,24 +1163,10 @@ export function createTiclawkAdapter(ctx) {
|
|
|
954
1163
|
id: 'ticlawk',
|
|
955
1164
|
|
|
956
1165
|
async start() {
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
recoveredCount: recovered.recoveredCount,
|
|
962
|
-
hostId,
|
|
963
|
-
});
|
|
964
|
-
}
|
|
965
|
-
} catch (err) {
|
|
966
|
-
if (api.isUpdateRequiredError(err)) {
|
|
967
|
-
recordUpdateRequired(err, 'recover');
|
|
968
|
-
}
|
|
969
|
-
debugError('ticlawk', 'message.recover-failed', {
|
|
970
|
-
hostId,
|
|
971
|
-
error: err?.message || 'unknown error',
|
|
972
|
-
});
|
|
973
|
-
}
|
|
974
|
-
|
|
1166
|
+
// No stale-delivery recovery anywhere — if the daemon is killed
|
|
1167
|
+
// mid-claim, the row stays `claimed` forever. lease_expires_at
|
|
1168
|
+
// was dropped in X1; no cron has replaced it. Rare in practice;
|
|
1169
|
+
// when it happens the fix is a one-row UPDATE in supabase.
|
|
975
1170
|
await refreshBindings('startup');
|
|
976
1171
|
await requestDrain('startup');
|
|
977
1172
|
connectWakeSocket();
|
|
@@ -1141,22 +1336,19 @@ export function createTiclawkAdapter(ctx) {
|
|
|
1141
1336
|
}
|
|
1142
1337
|
},
|
|
1143
1338
|
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
turnId: outbound.turnId || outbound.replyToMessageId || null,
|
|
1158
|
-
type: outbound.type || 'agent_message',
|
|
1159
|
-
replyToMessageId: outbound.replyToMessageId || null,
|
|
1339
|
+
// Daemon-driven impersonated reply: the runtime never reached
|
|
1340
|
+
// `ticlawk message send` (subprocess died, timeout, etc.), so the
|
|
1341
|
+
// daemon speaks for the agent. Used by reportSubprocessFailure
|
|
1342
|
+
// with visibility='admin' so the notice only reaches owners.
|
|
1343
|
+
async postAgentReply(binding, { conversationId, text, replyToMessageId, visibility }) {
|
|
1344
|
+
if (!conversationId || !text) return null;
|
|
1345
|
+
return api.sendAgentMessage({
|
|
1346
|
+
actingAgentId: binding.id,
|
|
1347
|
+
conversationId,
|
|
1348
|
+
text,
|
|
1349
|
+
replyToMessageId: replyToMessageId || null,
|
|
1350
|
+
runtimeHostId: hostId,
|
|
1351
|
+
visibility: visibility || null,
|
|
1160
1352
|
});
|
|
1161
1353
|
},
|
|
1162
1354
|
|
|
@@ -1234,7 +1426,6 @@ export async function runTiclawkAuth(rawArgs) {
|
|
|
1234
1426
|
};
|
|
1235
1427
|
}
|
|
1236
1428
|
const updates = {
|
|
1237
|
-
[AF_ADAPTER_KEY]: 'ticlawk',
|
|
1238
1429
|
TICLAWK_SETUP_CODE: code,
|
|
1239
1430
|
};
|
|
1240
1431
|
const apiUrl = String(args['api-url'] || '').trim();
|