ticlawk 0.1.15 → 0.1.16-dev.2
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 +228 -46
- package/package.json +2 -5
- package/src/adapters/ticlawk/api.mjs +303 -26
- package/src/adapters/ticlawk/credentials.mjs +1 -2
- package/src/adapters/ticlawk/index.mjs +273 -77
- package/src/cli/agent-commands.mjs +829 -0
- package/src/core/adapter-registry.mjs +12 -28
- package/src/core/agent-cli-handlers.mjs +704 -0
- package/src/core/agent-home.mjs +89 -0
- package/src/core/config.mjs +0 -15
- package/src/core/http.mjs +204 -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 +47 -30
- package/src/core/ticlawk-control.mjs +7 -6
- package/src/migrate/write-initial-memory.mjs +101 -0
- package/src/runtimes/_shared/standing-prompt.mjs +290 -0
- package/src/runtimes/claude-code/index.mjs +34 -34
- package/src/runtimes/claude-code/session.mjs +15 -7
- package/src/runtimes/codex/index.mjs +31 -36
- package/src/runtimes/codex/session.mjs +9 -5
- package/src/runtimes/openclaw/index.mjs +56 -16
- package/src/runtimes/openclaw/target.mjs +0 -30
- package/src/runtimes/opencode/index.mjs +30 -35
- package/src/runtimes/opencode/session.mjs +11 -2
- package/src/runtimes/pi/index.mjs +30 -36
- 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
|
@@ -2,14 +2,13 @@ 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
13
|
import { processAndSaveResult } from './cards.mjs';
|
|
15
14
|
import { persistApiCredential } from './credentials.mjs';
|
|
@@ -45,13 +44,132 @@ function normalizeInboundMediaAssets(msg) {
|
|
|
45
44
|
.filter(Boolean);
|
|
46
45
|
}
|
|
47
46
|
|
|
48
|
-
function
|
|
47
|
+
function buildEnvelopeTarget(msg) {
|
|
48
|
+
const convType = msg.conversation_type || 'dm';
|
|
49
|
+
const conversationId = msg.conversation_id || '';
|
|
50
|
+
const senderHandle = msg.sender_display_name || msg.sender_user_id || msg.sender_agent_id || '';
|
|
51
|
+
if (convType === 'dm') {
|
|
52
|
+
return senderHandle ? `dm:@${senderHandle}` : `dm:${conversationId}`;
|
|
53
|
+
}
|
|
54
|
+
if (convType === 'thread') {
|
|
55
|
+
const groupName = msg.conversation_name || conversationId;
|
|
56
|
+
const threadRoot = msg.thread_root_message_id || msg.message_id || '';
|
|
57
|
+
return `#${groupName}:${threadRoot}`;
|
|
58
|
+
}
|
|
59
|
+
// group
|
|
60
|
+
const groupName = msg.conversation_name || conversationId;
|
|
61
|
+
return `#${groupName}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildTaskSuffix(msg) {
|
|
65
|
+
if (msg.task_number == null) return '';
|
|
66
|
+
const status = msg.task_status || 'todo';
|
|
67
|
+
const parts = [`task #${msg.task_number} status=${status}`];
|
|
68
|
+
if (msg.task_assignee_agent_id || msg.task_assignee_user_id) {
|
|
69
|
+
const t = msg.task_assignee_type || 'agent';
|
|
70
|
+
const id = msg.task_assignee_agent_id || msg.task_assignee_user_id;
|
|
71
|
+
parts.push(`assignee=${t}:${id}`);
|
|
72
|
+
}
|
|
73
|
+
if (msg.task_title) {
|
|
74
|
+
parts.push(`title=${JSON.stringify(msg.task_title)}`);
|
|
75
|
+
}
|
|
76
|
+
return ` [${parts.join(' ')}]`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function buildReactionsSuffix(msg) {
|
|
80
|
+
const entries = Array.isArray(msg.reactions_summary) ? msg.reactions_summary : [];
|
|
81
|
+
if (entries.length === 0) return '';
|
|
82
|
+
return ` [reactions: ${entries.join('; ')}]`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function buildEnvelopeHeader(msg) {
|
|
86
|
+
const target = buildEnvelopeTarget(msg);
|
|
87
|
+
const msgId = msg.id || msg.message_id || '';
|
|
88
|
+
const seq = msg.seq != null ? msg.seq : '';
|
|
89
|
+
const time = msg.created_at || new Date().toISOString();
|
|
90
|
+
const type = msg.sender_type || 'human';
|
|
91
|
+
const sender = msg.sender_display_name || msg.sender_user_id || msg.sender_agent_id || 'unknown';
|
|
92
|
+
// `reason` tells the agent how this delivery was routed: 'mention'
|
|
93
|
+
// / 'assignment' = you were directly addressed → respond by default;
|
|
94
|
+
// 'ambient' = you are in the room and saw it → respond only if
|
|
95
|
+
// clearly the right responder; 'dm' / 'thread_follow' / 'manual' =
|
|
96
|
+
// the legacy direct paths. The agent's behaviour split is in the
|
|
97
|
+
// standing prompt; we just surface the field.
|
|
98
|
+
const reason = msg.reason ? ` reason=${msg.reason}` : '';
|
|
99
|
+
return `[target=${target} msg=${msgId} seq=${seq} time=${time} type=${type}${reason}] @${sender}:`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function buildGroupContextBlock(msg) {
|
|
103
|
+
// Only useful in groups — DMs don't need group-purpose context.
|
|
104
|
+
if ((msg.conversation_type || 'dm') !== 'group') return '';
|
|
105
|
+
const lines = [];
|
|
106
|
+
const name = msg.conversation_name || msg.conversation_display_name || '';
|
|
107
|
+
if (name) lines.push(`name: ${name}`);
|
|
108
|
+
const description = (msg.conversation_description || '').trim();
|
|
109
|
+
if (description) lines.push(`purpose: ${description}`);
|
|
110
|
+
if (lines.length === 0) return '';
|
|
111
|
+
return [
|
|
112
|
+
'Group context:',
|
|
113
|
+
...lines.map((l) => ` ${l}`),
|
|
114
|
+
'Use `ticlawk group members --target <target>` if you need to see who else is here.',
|
|
115
|
+
].join('\n');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Wrap each per-turn message with an explicit reply instruction so the
|
|
119
|
+
// runtime LLM never has to remember the standing prompt to figure out
|
|
120
|
+
// HOW to reply. Codex in particular treats the developerInstructions as
|
|
121
|
+
// background and ignores the chat-send pattern without this per-turn
|
|
122
|
+
// nudge (Slock does the same — see chunk-M4A5QPUN.js dynamicReplyInstruction).
|
|
123
|
+
function buildWakePromptText({ envelopeHeader, target, rawText, groupContext }) {
|
|
124
|
+
const body = `${envelopeHeader} ${rawText || ''}`.trim();
|
|
125
|
+
const lines = ['New message received:', '', body];
|
|
126
|
+
if (groupContext) {
|
|
127
|
+
lines.push('', groupContext);
|
|
128
|
+
}
|
|
129
|
+
lines.push(
|
|
130
|
+
'',
|
|
131
|
+
`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.`,
|
|
132
|
+
'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.',
|
|
133
|
+
'',
|
|
134
|
+
'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.',
|
|
135
|
+
);
|
|
136
|
+
return lines.join('\n');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function normalizeInboundMessage(msg) {
|
|
140
|
+
// Claimed delivery rows carry the recipient agent id; legacy messages
|
|
141
|
+
// (history sync, manual inserts) may still use plain agent_id. Prefer
|
|
142
|
+
// the new field but fall back so the same normalizer works for both.
|
|
143
|
+
const recipientAgentId = msg.recipient_agent_id || '';
|
|
49
144
|
const messageId = msg.id || msg.message_id || null;
|
|
145
|
+
const deliveryId = msg.delivery_id || null;
|
|
50
146
|
const media = normalizeInboundMediaAssets(msg);
|
|
147
|
+
const rawText = msg.text || '';
|
|
148
|
+
const enriched = { ...msg, id: messageId };
|
|
149
|
+
const baseHeader = buildEnvelopeHeader(enriched);
|
|
150
|
+
// Task + reactions suffixes are appended INSIDE the envelope so an
|
|
151
|
+
// agent can see at a glance whether a message is already claimed and
|
|
152
|
+
// who has already acknowledged it — same shape as slock's incoming
|
|
153
|
+
// message render (`[task #N status=… assignee=…]` + `[reactions: …]`).
|
|
154
|
+
const taskSuffix = buildTaskSuffix(enriched);
|
|
155
|
+
const reactionsSuffix = buildReactionsSuffix(enriched);
|
|
156
|
+
const header = baseHeader + taskSuffix + reactionsSuffix;
|
|
157
|
+
const target = buildEnvelopeTarget(enriched);
|
|
158
|
+
const groupContext = buildGroupContextBlock(enriched);
|
|
159
|
+
const text = header
|
|
160
|
+
? buildWakePromptText({ envelopeHeader: header, target, rawText, groupContext })
|
|
161
|
+
: rawText;
|
|
51
162
|
return {
|
|
52
|
-
bindingId:
|
|
163
|
+
bindingId: recipientAgentId,
|
|
53
164
|
messageId,
|
|
54
|
-
|
|
165
|
+
deliveryId,
|
|
166
|
+
conversationId: msg.conversation_id || null,
|
|
167
|
+
seq: msg.seq != null ? Number(msg.seq) : null,
|
|
168
|
+
senderType: msg.sender_type || 'human',
|
|
169
|
+
envelopeHeader: header,
|
|
170
|
+
envelopeTarget: target,
|
|
171
|
+
text,
|
|
172
|
+
rawText,
|
|
55
173
|
action: msg.action || (media.length > 0 ? 'image' : 'task'),
|
|
56
174
|
media,
|
|
57
175
|
raw: {
|
|
@@ -65,10 +183,6 @@ function getBindingSessionId(binding) {
|
|
|
65
183
|
return binding?.runtimeMeta?.sessionId || binding?.runtimeMeta?.sessionKey || binding?.runtimeMeta?.agentId || binding?.id || null;
|
|
66
184
|
}
|
|
67
185
|
|
|
68
|
-
function getWorkdir(meta = {}) {
|
|
69
|
-
return String(meta.workdir || meta.projectDir || meta.cwd || '').trim();
|
|
70
|
-
}
|
|
71
|
-
|
|
72
186
|
function getRuntimeVersion(bindingOrMeta) {
|
|
73
187
|
const value = bindingOrMeta?.runtimeMeta?.runtimeVersion ?? bindingOrMeta?.runtimeVersion ?? bindingOrMeta?.agent_runtime_version ?? null;
|
|
74
188
|
return Number.isInteger(value) ? Number(value) : null;
|
|
@@ -91,58 +205,104 @@ function getRuntimeHostLabelFromPayload(payload) {
|
|
|
91
205
|
}
|
|
92
206
|
|
|
93
207
|
function getAgentIdFromPayload(payload) {
|
|
94
|
-
|
|
208
|
+
// claim_pending_deliveries is the canonical source and returns
|
|
209
|
+
// `recipient_agent_id`. The historical fallbacks (`agent_id`,
|
|
210
|
+
// `agentId`) were left in by an earlier partial cleanup but they
|
|
211
|
+
// never fire against the current schema — dead branches.
|
|
212
|
+
return String(payload?.recipient_agent_id || '').trim();
|
|
95
213
|
}
|
|
96
214
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
215
|
+
// Whitelist of meta keys runtimes actually consume. Anything else in
|
|
216
|
+
// the source row's meta blob is dropped on the floor so stale fields
|
|
217
|
+
// (like the post-Y2 workdir/cwd/projectDir leftovers) can't sneak
|
|
218
|
+
// back into runtimeMeta and confuse downstream code.
|
|
219
|
+
const RUNTIME_META_KEYS = [
|
|
220
|
+
'sessionId',
|
|
221
|
+
'path',
|
|
222
|
+
'runtimePath',
|
|
223
|
+
'rotatePending',
|
|
224
|
+
'lastRotatedAt',
|
|
225
|
+
// claude_code
|
|
226
|
+
'claudePath',
|
|
227
|
+
'claudeVersion',
|
|
228
|
+
'project',
|
|
229
|
+
// codex
|
|
230
|
+
'codexPath',
|
|
231
|
+
'codexVersion',
|
|
232
|
+
// opencode
|
|
233
|
+
'opencodePath',
|
|
234
|
+
'opencodeVersion',
|
|
235
|
+
// pi
|
|
236
|
+
'piPath',
|
|
237
|
+
'piVersion',
|
|
238
|
+
// openclaw (gateway-based)
|
|
239
|
+
'agentId',
|
|
240
|
+
'sessionKey',
|
|
241
|
+
'gatewayHost',
|
|
242
|
+
'gatewayPort',
|
|
243
|
+
'lastGatewayFailureAt',
|
|
244
|
+
'lastGatewayFailureReason',
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
function projectRuntimeMeta(source) {
|
|
248
|
+
const out = {};
|
|
249
|
+
if (!source || typeof source !== 'object') return out;
|
|
250
|
+
for (const key of RUNTIME_META_KEYS) {
|
|
251
|
+
if (source[key] !== undefined) out[key] = source[key];
|
|
110
252
|
}
|
|
111
|
-
|
|
112
|
-
|
|
253
|
+
return out;
|
|
254
|
+
}
|
|
113
255
|
|
|
256
|
+
function normalizeRuntimeVersion(value) {
|
|
257
|
+
return Number.isInteger(value) ? Number(value) : 0;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Build a binding from the agent table snapshot (returned by
|
|
261
|
+
// /api/agents and used by the startup hydrate path). Reads agent.id,
|
|
262
|
+
// agent.service_type, agent.meta, etc. — the agent-row shape.
|
|
263
|
+
function buildBindingFromAgentRow(agent) {
|
|
264
|
+
const agentId = String(agent?.id || agent?.agent_id || '').trim();
|
|
265
|
+
const runtime = agent?.service_type;
|
|
266
|
+
const hostId = getRuntimeHostIdFromPayload(agent) || undefined;
|
|
114
267
|
return {
|
|
115
268
|
id: agentId,
|
|
116
269
|
adapter: 'ticlawk',
|
|
117
270
|
targetKey: agentId,
|
|
118
|
-
targetMeta: {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
},
|
|
122
|
-
runtime_host_id: getRuntimeHostIdFromPayload(msg) || undefined,
|
|
123
|
-
runtime_host_label: getRuntimeHostLabelFromPayload(msg) || undefined,
|
|
271
|
+
targetMeta: { agentId, runtime_host_id: hostId },
|
|
272
|
+
runtime_host_id: hostId,
|
|
273
|
+
runtime_host_label: getRuntimeHostLabelFromPayload(agent) || undefined,
|
|
124
274
|
runtime,
|
|
125
275
|
runtimeMeta: {
|
|
126
|
-
...
|
|
127
|
-
runtimeVersion,
|
|
276
|
+
...projectRuntimeMeta(agent?.meta),
|
|
277
|
+
runtimeVersion: normalizeRuntimeVersion(agent?.runtime_version),
|
|
128
278
|
},
|
|
129
|
-
displayName:
|
|
130
|
-
status:
|
|
279
|
+
displayName: agent?.display_name || agent?.name || agentId,
|
|
280
|
+
status: agent?.status || 'connected',
|
|
131
281
|
};
|
|
132
282
|
}
|
|
133
283
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
284
|
+
// Build a binding from a claimed delivery row. Reads
|
|
285
|
+
// recipient_agent_id, agent_service_type, agent_meta, etc. — the
|
|
286
|
+
// claim-payload shape returned by claim_pending_deliveries.
|
|
287
|
+
function buildBindingFromDeliveryRow(msg) {
|
|
288
|
+
const agentId = String(msg?.recipient_agent_id || '').trim();
|
|
289
|
+
const runtime = msg?.agent_service_type;
|
|
290
|
+
const hostId = getRuntimeHostIdFromPayload(msg) || undefined;
|
|
291
|
+
return {
|
|
292
|
+
id: agentId,
|
|
293
|
+
adapter: 'ticlawk',
|
|
294
|
+
targetKey: agentId,
|
|
295
|
+
targetMeta: { agentId, runtime_host_id: hostId },
|
|
296
|
+
runtime_host_id: hostId,
|
|
297
|
+
runtime_host_label: getRuntimeHostLabelFromPayload(msg) || undefined,
|
|
298
|
+
runtime,
|
|
299
|
+
runtimeMeta: {
|
|
300
|
+
...projectRuntimeMeta(msg?.agent_meta),
|
|
301
|
+
runtimeVersion: normalizeRuntimeVersion(msg?.agent_runtime_version),
|
|
302
|
+
},
|
|
303
|
+
displayName: msg?.agent_display_name || msg?.agent_name || agentId,
|
|
304
|
+
status: msg?.agent_status || 'connected',
|
|
305
|
+
};
|
|
146
306
|
}
|
|
147
307
|
|
|
148
308
|
function maskIdentity(identity = {}) {
|
|
@@ -458,12 +618,14 @@ export function createTiclawkAdapter(ctx) {
|
|
|
458
618
|
|
|
459
619
|
async function processPendingMessagesForAgent(agentId, messages) {
|
|
460
620
|
for (const msg of messages) {
|
|
621
|
+
const deliveryId = msg.delivery_id;
|
|
461
622
|
try {
|
|
462
623
|
const messageHostId = getRuntimeHostIdFromPayload(msg);
|
|
463
624
|
if (messageHostId && messageHostId !== hostId) {
|
|
464
|
-
await api.
|
|
625
|
+
await api.releaseDelivery(deliveryId, hostId, 'host-mismatch');
|
|
465
626
|
debugError('ticlawk', 'message.host-mismatch', {
|
|
466
627
|
agentId,
|
|
628
|
+
deliveryId,
|
|
467
629
|
messageId: msg.message_id,
|
|
468
630
|
hostId,
|
|
469
631
|
runtime_host_id: messageHostId,
|
|
@@ -471,14 +633,15 @@ export function createTiclawkAdapter(ctx) {
|
|
|
471
633
|
continue;
|
|
472
634
|
}
|
|
473
635
|
|
|
474
|
-
const binding = await ctx.
|
|
636
|
+
const binding = await ctx.persistBinding(buildBindingFromDeliveryRow(msg));
|
|
475
637
|
if (!binding?.runtime) {
|
|
476
638
|
throw new Error('claimed message missing runtime binding');
|
|
477
639
|
}
|
|
478
640
|
if (!belongsToRuntimeHost(binding, hostId)) {
|
|
479
|
-
await api.
|
|
641
|
+
await api.releaseDelivery(deliveryId, hostId, 'binding-host-mismatch');
|
|
480
642
|
debugError('ticlawk', 'message.binding-host-mismatch', {
|
|
481
643
|
agentId,
|
|
644
|
+
deliveryId,
|
|
482
645
|
messageId: msg.message_id,
|
|
483
646
|
hostId,
|
|
484
647
|
runtime_host_id: getBindingRuntimeHostId(binding),
|
|
@@ -489,10 +652,11 @@ export function createTiclawkAdapter(ctx) {
|
|
|
489
652
|
const completed = await ctx.bus.dispatchToAgent(binding.runtime, binding.id, normalizeInboundMessage(msg));
|
|
490
653
|
if (completed !== true) {
|
|
491
654
|
if (isTerminalRuntimeFailure(completed)) {
|
|
492
|
-
await api.
|
|
655
|
+
await api.completeDelivery(deliveryId, hostId);
|
|
493
656
|
void requestDrain('message.terminal-completed');
|
|
494
657
|
debugError('ticlawk', 'message.terminal-failed', {
|
|
495
658
|
agentId,
|
|
659
|
+
deliveryId,
|
|
496
660
|
messageId: msg.message_id,
|
|
497
661
|
runtime: binding.runtime,
|
|
498
662
|
runtimeVersion: getRuntimeVersion(binding),
|
|
@@ -502,10 +666,11 @@ export function createTiclawkAdapter(ctx) {
|
|
|
502
666
|
}
|
|
503
667
|
throw new Error('runtime did not complete turn');
|
|
504
668
|
}
|
|
505
|
-
await api.
|
|
669
|
+
await api.completeDelivery(deliveryId, hostId);
|
|
506
670
|
void requestDrain('message.completed');
|
|
507
671
|
debugLog('ticlawk', 'message.completed', {
|
|
508
672
|
agentId,
|
|
673
|
+
deliveryId,
|
|
509
674
|
messageId: msg.message_id,
|
|
510
675
|
runtime: binding.runtime,
|
|
511
676
|
runtimeVersion: getRuntimeVersion(binding),
|
|
@@ -515,7 +680,7 @@ export function createTiclawkAdapter(ctx) {
|
|
|
515
680
|
recordUpdateRequired(err, 'message.dispatch');
|
|
516
681
|
}
|
|
517
682
|
try {
|
|
518
|
-
await api.
|
|
683
|
+
await api.releaseDelivery(deliveryId, hostId, 'dispatch-error');
|
|
519
684
|
} catch (releaseErr) {
|
|
520
685
|
debugError('ticlawk', 'message.release-failed', {
|
|
521
686
|
agentId,
|
|
@@ -568,7 +733,7 @@ export function createTiclawkAdapter(ctx) {
|
|
|
568
733
|
continue;
|
|
569
734
|
}
|
|
570
735
|
try {
|
|
571
|
-
await ctx.
|
|
736
|
+
await ctx.persistBinding(buildBindingFromAgentRow(agent));
|
|
572
737
|
hydrated += 1;
|
|
573
738
|
} catch (err) {
|
|
574
739
|
debugError('ticlawk', 'binding.hydrate-failed', {
|
|
@@ -592,13 +757,14 @@ export function createTiclawkAdapter(ctx) {
|
|
|
592
757
|
|
|
593
758
|
async function releaseBlockedRows(agentId, messages, reason) {
|
|
594
759
|
for (const msg of messages) {
|
|
595
|
-
if (!msg?.
|
|
760
|
+
if (!msg?.delivery_id) continue;
|
|
596
761
|
try {
|
|
597
|
-
await api.
|
|
762
|
+
await api.releaseDelivery(msg.delivery_id, hostId, reason);
|
|
598
763
|
} catch (err) {
|
|
599
764
|
debugError('ticlawk', 'claim.blocked-release-failed', {
|
|
600
765
|
reason,
|
|
601
766
|
agentId,
|
|
767
|
+
deliveryId: msg.delivery_id,
|
|
602
768
|
messageId: msg.message_id,
|
|
603
769
|
error: err?.message || 'unknown error',
|
|
604
770
|
});
|
|
@@ -620,7 +786,7 @@ export function createTiclawkAdapter(ctx) {
|
|
|
620
786
|
return { failed: true, claimed: 0, launched: 0, updateRequired: true };
|
|
621
787
|
}
|
|
622
788
|
try {
|
|
623
|
-
data = await api.
|
|
789
|
+
data = await api.claimPendingDeliveries(hostId, 5, excludedChannelIds);
|
|
624
790
|
clearUpdateRequired('claim');
|
|
625
791
|
} catch (err) {
|
|
626
792
|
if (api.isUpdateRequiredError(err)) {
|
|
@@ -662,7 +828,18 @@ export function createTiclawkAdapter(ctx) {
|
|
|
662
828
|
|
|
663
829
|
const grouped = new Map();
|
|
664
830
|
for (const msg of data) {
|
|
665
|
-
const agentId = getAgentIdFromPayload(msg)
|
|
831
|
+
const agentId = getAgentIdFromPayload(msg);
|
|
832
|
+
if (!agentId) {
|
|
833
|
+
// Claim rows must carry the recipient agent id; a missing value
|
|
834
|
+
// is a contract violation upstream, not something we paper over
|
|
835
|
+
// with a synthetic key.
|
|
836
|
+
debugError('ticlawk', 'claim.missing-agent-id', {
|
|
837
|
+
reason,
|
|
838
|
+
deliveryId: msg?.delivery_id,
|
|
839
|
+
messageId: msg?.message_id,
|
|
840
|
+
});
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
666
843
|
const bucket = grouped.get(agentId) || [];
|
|
667
844
|
bucket.push(msg);
|
|
668
845
|
grouped.set(agentId, bucket);
|
|
@@ -744,12 +921,46 @@ export function createTiclawkAdapter(ctx) {
|
|
|
744
921
|
bindingsWakeTimer.unref?.();
|
|
745
922
|
}
|
|
746
923
|
|
|
924
|
+
async function reportHostCapabilitiesNow() {
|
|
925
|
+
const entries = await Promise.all(Object.entries(ctx.runtimes || {})
|
|
926
|
+
.filter(([, runtime]) => typeof runtime?.health === 'function')
|
|
927
|
+
.map(async ([name, runtime]) => {
|
|
928
|
+
try {
|
|
929
|
+
return [name, await runtime.health()];
|
|
930
|
+
} catch (err) {
|
|
931
|
+
return [name, { available: false, error: err?.message || 'health probe failed' }];
|
|
932
|
+
}
|
|
933
|
+
}));
|
|
934
|
+
const runtimesHealth = Object.fromEntries(entries);
|
|
935
|
+
try {
|
|
936
|
+
await api.reportHostCapabilities({
|
|
937
|
+
hostId,
|
|
938
|
+
hostLabel,
|
|
939
|
+
runtimesHealth,
|
|
940
|
+
daemonVersion: api.getTiclawkVersion(),
|
|
941
|
+
});
|
|
942
|
+
debugLog('ticlawk', 'host.capabilities.reported', {
|
|
943
|
+
hostId,
|
|
944
|
+
runtimes: Object.entries(runtimesHealth)
|
|
945
|
+
.filter(([, v]) => v?.available)
|
|
946
|
+
.map(([k]) => k)
|
|
947
|
+
.join(','),
|
|
948
|
+
});
|
|
949
|
+
} catch (err) {
|
|
950
|
+
debugError('ticlawk', 'host.capabilities.failed', {
|
|
951
|
+
hostId,
|
|
952
|
+
error: err?.message || 'report failed',
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
747
957
|
function handleWakeEvent(event) {
|
|
748
958
|
wakeState.lastEventAt = new Date().toISOString();
|
|
749
959
|
if (event?.type === 'hello') {
|
|
750
960
|
void refreshBindings('wake.hello')
|
|
751
961
|
.then(() => requestDrain('wake.hello'))
|
|
752
962
|
.catch(() => {});
|
|
963
|
+
void reportHostCapabilitiesNow();
|
|
753
964
|
return;
|
|
754
965
|
}
|
|
755
966
|
if (event?.type === 'jobs.available') {
|
|
@@ -954,24 +1165,10 @@ export function createTiclawkAdapter(ctx) {
|
|
|
954
1165
|
id: 'ticlawk',
|
|
955
1166
|
|
|
956
1167
|
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
|
-
|
|
1168
|
+
// No stale-delivery recovery anywhere — if the daemon is killed
|
|
1169
|
+
// mid-claim, the row stays `claimed` forever. lease_expires_at
|
|
1170
|
+
// was dropped in X1; no cron has replaced it. Rare in practice;
|
|
1171
|
+
// when it happens the fix is a one-row UPDATE in supabase.
|
|
975
1172
|
await refreshBindings('startup');
|
|
976
1173
|
await requestDrain('startup');
|
|
977
1174
|
connectWakeSocket();
|
|
@@ -1234,7 +1431,6 @@ export async function runTiclawkAuth(rawArgs) {
|
|
|
1234
1431
|
};
|
|
1235
1432
|
}
|
|
1236
1433
|
const updates = {
|
|
1237
|
-
[AF_ADAPTER_KEY]: 'ticlawk',
|
|
1238
1434
|
TICLAWK_SETUP_CODE: code,
|
|
1239
1435
|
};
|
|
1240
1436
|
const apiUrl = String(args['api-url'] || '').trim();
|