ticlawk 0.1.16-dev.3 → 0.1.16-dev.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -2
- package/bin/ticlawk.mjs +207 -25
- package/package.json +1 -1
- package/src/adapters/ticlawk/api.mjs +293 -70
- package/src/adapters/ticlawk/credentials.mjs +41 -1
- package/src/adapters/ticlawk/index.mjs +199 -199
- package/src/adapters/ticlawk/wake-client.mjs +1 -1
- package/src/cli/agent-commands.mjs +607 -37
- package/src/core/agent-cli-handlers.mjs +449 -20
- package/src/core/agent-home.mjs +86 -10
- package/src/core/argv.mjs +11 -1
- package/src/core/events/worker-events.mjs +32 -36
- package/src/core/http.mjs +126 -0
- package/src/core/runtime-env.mjs +7 -0
- package/src/core/runtime-support.mjs +108 -107
- package/src/migrate/write-initial-memory.mjs +5 -5
- package/src/runtimes/_shared/agent-handbook.mjs +45 -0
- package/src/runtimes/_shared/brand.mjs +2 -0
- package/src/runtimes/_shared/goal-task-protocol.mjs +50 -0
- package/src/runtimes/_shared/handbook/BASICS.md +27 -0
- package/src/runtimes/_shared/handbook/COLLABORATION.md +37 -0
- package/src/runtimes/_shared/handbook/COMMUNICATION.md +50 -0
- package/src/runtimes/_shared/handbook/DM_SCOPE.md +13 -0
- package/src/runtimes/_shared/handbook/GOAL_AUTHORITY.md +43 -0
- package/src/runtimes/_shared/handbook/GOAL_TASK_CORE.md +43 -0
- package/src/runtimes/_shared/handbook/GROUP_ADMIN_SCOPE.md +21 -0
- package/src/runtimes/_shared/handbook/GROUP_MEMBER_SCOPE.md +15 -0
- package/src/runtimes/_shared/handbook/SURFACES.md +41 -0
- package/src/runtimes/_shared/handbook/TASK_WORKER.md +14 -0
- package/src/runtimes/_shared/standing-prompt.mjs +111 -262
- package/src/runtimes/_shared/wake-prompt.mjs +261 -0
- package/src/runtimes/claude-code/index.mjs +34 -127
- package/src/runtimes/claude-code/session.mjs +2 -7
- package/src/runtimes/codex/index.mjs +117 -54
- package/src/runtimes/codex/session.mjs +2 -12
- package/src/runtimes/openclaw/index.mjs +16 -26
- package/src/runtimes/opencode/index.mjs +45 -66
- package/src/runtimes/opencode/session.mjs +12 -12
- package/src/runtimes/pi/index.mjs +42 -60
- package/src/runtimes/pi/session.mjs +9 -6
- package/src/adapters/ticlawk/cards.mjs +0 -149
- package/src/core/media/outbound.mjs +0 -163
|
@@ -10,14 +10,15 @@ 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
12
|
import * as api from './api.mjs';
|
|
13
|
-
import {
|
|
14
|
-
import { persistApiCredential } from './credentials.mjs';
|
|
13
|
+
import { persistApiCredential, persistRuntimeCredentials } from './credentials.mjs';
|
|
15
14
|
import { TiclawkWakeClient } from './wake-client.mjs';
|
|
15
|
+
import { buildInboundWakePrompt } from '../../runtimes/_shared/wake-prompt.mjs';
|
|
16
16
|
|
|
17
17
|
const require = createRequire(import.meta.url);
|
|
18
18
|
const qrcode = require('qrcode-terminal');
|
|
19
19
|
const JOBS_WAKE_DEBOUNCE_MS = 100;
|
|
20
20
|
const BINDINGS_WAKE_DEBOUNCE_MS = 500;
|
|
21
|
+
const CREDENTIALS_WAKE_DEBOUNCE_MS = 500;
|
|
21
22
|
const RECOVERY_AUDIT_INTERVAL_MS = 5 * 60 * 1000;
|
|
22
23
|
const BINDING_AUDIT_INTERVAL_MS = 5 * 60 * 1000;
|
|
23
24
|
const UPDATE_RETRY_COOLDOWN_MS = 15 * 60 * 1000;
|
|
@@ -27,6 +28,32 @@ function connectError(statusCode, error) {
|
|
|
27
28
|
return { statusCode, body: { ok: false, error } };
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
// Coalesce: returns a wrapped fn that runs at most one invocation at a
|
|
32
|
+
// time. If it's called while an invocation is in flight, the most recent
|
|
33
|
+
// args are stashed and the wrapped fn re-runs exactly once after the
|
|
34
|
+
// current run completes (regardless of how many times it was called
|
|
35
|
+
// during the run). The wrapped fn always returns the in-flight Promise.
|
|
36
|
+
function coalesce(fn) {
|
|
37
|
+
let running = null;
|
|
38
|
+
let pendingArgs = null;
|
|
39
|
+
return function call(...args) {
|
|
40
|
+
if (running) {
|
|
41
|
+
pendingArgs = args;
|
|
42
|
+
return running;
|
|
43
|
+
}
|
|
44
|
+
running = (async () => {
|
|
45
|
+
let currentArgs = args;
|
|
46
|
+
while (true) {
|
|
47
|
+
pendingArgs = null;
|
|
48
|
+
await fn(...currentArgs);
|
|
49
|
+
if (pendingArgs === null) return;
|
|
50
|
+
currentArgs = pendingArgs;
|
|
51
|
+
}
|
|
52
|
+
})().finally(() => { running = null; });
|
|
53
|
+
return running;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
30
57
|
function normalizeInboundMediaAssets(msg) {
|
|
31
58
|
if (!Array.isArray(msg?.media_assets)) return [];
|
|
32
59
|
return msg.media_assets
|
|
@@ -44,98 +71,6 @@ function normalizeInboundMediaAssets(msg) {
|
|
|
44
71
|
.filter(Boolean);
|
|
45
72
|
}
|
|
46
73
|
|
|
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
74
|
export function normalizeInboundMessage(msg) {
|
|
140
75
|
// Claimed delivery rows carry the recipient agent id; legacy messages
|
|
141
76
|
// (history sync, manual inserts) may still use plain agent_id. Prefer
|
|
@@ -144,21 +79,8 @@ export function normalizeInboundMessage(msg) {
|
|
|
144
79
|
const messageId = msg.id || msg.message_id || null;
|
|
145
80
|
const deliveryId = msg.delivery_id || null;
|
|
146
81
|
const media = normalizeInboundMediaAssets(msg);
|
|
147
|
-
const rawText = msg.text || '';
|
|
148
82
|
const enriched = { ...msg, id: messageId };
|
|
149
|
-
const
|
|
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;
|
|
83
|
+
const wakePrompt = buildInboundWakePrompt(enriched);
|
|
162
84
|
return {
|
|
163
85
|
bindingId: recipientAgentId,
|
|
164
86
|
messageId,
|
|
@@ -166,10 +88,10 @@ export function normalizeInboundMessage(msg) {
|
|
|
166
88
|
conversationId: msg.conversation_id || null,
|
|
167
89
|
seq: msg.seq != null ? Number(msg.seq) : null,
|
|
168
90
|
senderType: msg.sender_type || 'human',
|
|
169
|
-
envelopeHeader: header,
|
|
170
|
-
envelopeTarget: target,
|
|
171
|
-
text,
|
|
172
|
-
rawText,
|
|
91
|
+
envelopeHeader: wakePrompt.header,
|
|
92
|
+
envelopeTarget: wakePrompt.target,
|
|
93
|
+
text: wakePrompt.text,
|
|
94
|
+
rawText: wakePrompt.rawText,
|
|
173
95
|
action: msg.action || (media.length > 0 ? 'image' : 'task'),
|
|
174
96
|
media,
|
|
175
97
|
raw: {
|
|
@@ -179,10 +101,6 @@ export function normalizeInboundMessage(msg) {
|
|
|
179
101
|
};
|
|
180
102
|
}
|
|
181
103
|
|
|
182
|
-
function getBindingSessionId(binding) {
|
|
183
|
-
return binding?.runtimeMeta?.sessionId || binding?.runtimeMeta?.sessionKey || binding?.runtimeMeta?.agentId || binding?.id || null;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
104
|
function getRuntimeVersion(bindingOrMeta) {
|
|
187
105
|
const value = bindingOrMeta?.runtimeMeta?.runtimeVersion ?? bindingOrMeta?.runtimeVersion ?? bindingOrMeta?.agent_runtime_version ?? null;
|
|
188
106
|
return Number.isInteger(value) ? Number(value) : null;
|
|
@@ -222,6 +140,7 @@ const RUNTIME_META_KEYS = [
|
|
|
222
140
|
'runtimePath',
|
|
223
141
|
'rotatePending',
|
|
224
142
|
'lastRotatedAt',
|
|
143
|
+
'conversationSessions',
|
|
225
144
|
// claude_code
|
|
226
145
|
'claudePath',
|
|
227
146
|
'claudeVersion',
|
|
@@ -257,51 +176,46 @@ function normalizeRuntimeVersion(value) {
|
|
|
257
176
|
return Number.isInteger(value) ? Number(value) : 0;
|
|
258
177
|
}
|
|
259
178
|
|
|
260
|
-
// Build a binding from
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
179
|
+
// Build a binding from any source row that carries agent metadata.
|
|
180
|
+
// Two source shapes feed in:
|
|
181
|
+
// /api/agents row — id, service_type, meta, …
|
|
182
|
+
// claim_pending_deliveries — recipient_agent_id, agent_service_type,
|
|
183
|
+
// agent_meta, … (prefixed because the
|
|
184
|
+
// delivery row also carries message fields)
|
|
185
|
+
// We try the prefixed names first, then fall through to the bare names,
|
|
186
|
+
// so one builder handles both without the caller having to remember
|
|
187
|
+
// which shape it has.
|
|
188
|
+
function buildBindingFromSource(source) {
|
|
189
|
+
const agentId = String(
|
|
190
|
+
source?.recipient_agent_id
|
|
191
|
+
|| source?.id
|
|
192
|
+
|| source?.agent_id
|
|
193
|
+
|| ''
|
|
194
|
+
).trim();
|
|
195
|
+
const runtime = source?.agent_service_type || source?.service_type;
|
|
196
|
+
const meta = source?.agent_meta ?? source?.meta;
|
|
197
|
+
const runtimeVersion = source?.agent_runtime_version ?? source?.runtime_version;
|
|
198
|
+
const displayName = source?.agent_display_name
|
|
199
|
+
|| source?.agent_name
|
|
200
|
+
|| source?.display_name
|
|
201
|
+
|| source?.name
|
|
202
|
+
|| agentId;
|
|
203
|
+
const status = source?.agent_status || source?.status || 'connected';
|
|
204
|
+
const hostId = getRuntimeHostIdFromPayload(source) || undefined;
|
|
267
205
|
return {
|
|
268
206
|
id: agentId,
|
|
269
207
|
adapter: 'ticlawk',
|
|
270
208
|
targetKey: agentId,
|
|
271
209
|
targetMeta: { agentId, runtime_host_id: hostId },
|
|
272
210
|
runtime_host_id: hostId,
|
|
273
|
-
runtime_host_label: getRuntimeHostLabelFromPayload(
|
|
211
|
+
runtime_host_label: getRuntimeHostLabelFromPayload(source) || undefined,
|
|
274
212
|
runtime,
|
|
275
213
|
runtimeMeta: {
|
|
276
|
-
...projectRuntimeMeta(
|
|
277
|
-
runtimeVersion: normalizeRuntimeVersion(
|
|
214
|
+
...projectRuntimeMeta(meta),
|
|
215
|
+
runtimeVersion: normalizeRuntimeVersion(runtimeVersion),
|
|
278
216
|
},
|
|
279
|
-
displayName
|
|
280
|
-
status
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
|
|
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',
|
|
217
|
+
displayName,
|
|
218
|
+
status,
|
|
305
219
|
};
|
|
306
220
|
}
|
|
307
221
|
|
|
@@ -343,6 +257,31 @@ function getRuntimeWorkdir(runtimeMeta = {}) {
|
|
|
343
257
|
return String(runtimeMeta.workdir || runtimeMeta.cwd || runtimeMeta.projectDir || '').trim();
|
|
344
258
|
}
|
|
345
259
|
|
|
260
|
+
function compareStrings(a, b) {
|
|
261
|
+
const av = String(a || '');
|
|
262
|
+
const bv = String(b || '');
|
|
263
|
+
if (av < bv) return -1;
|
|
264
|
+
if (av > bv) return 1;
|
|
265
|
+
return 0;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function compareNumbers(a, b) {
|
|
269
|
+
const av = Number(a);
|
|
270
|
+
const bv = Number(b);
|
|
271
|
+
const aFinite = Number.isFinite(av);
|
|
272
|
+
const bFinite = Number.isFinite(bv);
|
|
273
|
+
if (aFinite && bFinite && av !== bv) return av - bv;
|
|
274
|
+
if (aFinite !== bFinite) return aFinite ? -1 : 1;
|
|
275
|
+
return 0;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function compareClaimedDeliveries(a, b) {
|
|
279
|
+
return compareStrings(a?.created_at, b?.created_at)
|
|
280
|
+
|| compareStrings(a?.conversation_id, b?.conversation_id)
|
|
281
|
+
|| compareNumbers(a?.seq, b?.seq)
|
|
282
|
+
|| compareStrings(a?.delivery_id, b?.delivery_id);
|
|
283
|
+
}
|
|
284
|
+
|
|
346
285
|
function runtimeLabel(runtime) {
|
|
347
286
|
if (runtime === 'claude_code') return 'Claude Code';
|
|
348
287
|
if (runtime === 'opencode') return 'OpenCode';
|
|
@@ -456,10 +395,10 @@ export function createTiclawkAdapter(ctx) {
|
|
|
456
395
|
let bindingAuditTimer = null;
|
|
457
396
|
let jobsWakeTimer = null;
|
|
458
397
|
let bindingsWakeTimer = null;
|
|
459
|
-
let
|
|
460
|
-
let drainRequested = false;
|
|
398
|
+
let credentialsWakeTimer = null;
|
|
461
399
|
let lastJobsWakeAt = 0;
|
|
462
400
|
let lastBindingsWakeAt = 0;
|
|
401
|
+
let lastCredentialsWakeAt = 0;
|
|
463
402
|
let updateRequired = null;
|
|
464
403
|
let lastUpdateRequiredLogAt = 0;
|
|
465
404
|
|
|
@@ -633,9 +572,9 @@ export function createTiclawkAdapter(ctx) {
|
|
|
633
572
|
continue;
|
|
634
573
|
}
|
|
635
574
|
|
|
636
|
-
const binding = await ctx.persistBinding(
|
|
575
|
+
const binding = await ctx.persistBinding(buildBindingFromSource(msg));
|
|
637
576
|
if (!binding?.runtime) {
|
|
638
|
-
throw new Error('claimed
|
|
577
|
+
throw new Error('claimed delivery missing runtime binding');
|
|
639
578
|
}
|
|
640
579
|
if (!belongsToRuntimeHost(binding, hostId)) {
|
|
641
580
|
await api.releaseDelivery(deliveryId, hostId, 'binding-host-mismatch');
|
|
@@ -733,7 +672,7 @@ export function createTiclawkAdapter(ctx) {
|
|
|
733
672
|
continue;
|
|
734
673
|
}
|
|
735
674
|
try {
|
|
736
|
-
await ctx.persistBinding(
|
|
675
|
+
await ctx.persistBinding(buildBindingFromSource(agent));
|
|
737
676
|
hydrated += 1;
|
|
738
677
|
} catch (err) {
|
|
739
678
|
debugError('ticlawk', 'binding.hydrate-failed', {
|
|
@@ -741,6 +680,27 @@ export function createTiclawkAdapter(ctx) {
|
|
|
741
680
|
error: err?.message || 'unknown error',
|
|
742
681
|
});
|
|
743
682
|
}
|
|
683
|
+
|
|
684
|
+
// Agents created from the App via POST /me/agents land here in
|
|
685
|
+
// status='unpaired'. Once we've successfully registered the
|
|
686
|
+
// binding locally, flip them to 'connected' — same end state the
|
|
687
|
+
// legacy QR pairing flow leaves agents in. spawn itself stays
|
|
688
|
+
// lazy (happens on first delivery via deliverTurn).
|
|
689
|
+
if (agent.status === 'unpaired' && agentHostId === hostId) {
|
|
690
|
+
try {
|
|
691
|
+
await api.updateAgent(agent.id, {
|
|
692
|
+
status: 'connected',
|
|
693
|
+
runtime_host_id: hostId,
|
|
694
|
+
runtime_host_label: getHostLabel(),
|
|
695
|
+
});
|
|
696
|
+
debugLog('ticlawk', 'binding.unpaired-claimed', { agentId: agent.id });
|
|
697
|
+
} catch (err) {
|
|
698
|
+
debugError('ticlawk', 'binding.unpaired-claim-failed', {
|
|
699
|
+
agentId: agent.id,
|
|
700
|
+
error: err?.message || 'unknown error',
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
}
|
|
744
704
|
}
|
|
745
705
|
const pruned = await pruneDeletedBindings(channels, reason);
|
|
746
706
|
debugLog('ticlawk', 'binding.refresh-ok', {
|
|
@@ -755,6 +715,33 @@ export function createTiclawkAdapter(ctx) {
|
|
|
755
715
|
return hydrated;
|
|
756
716
|
}
|
|
757
717
|
|
|
718
|
+
const syncCredentials = coalesce(async (reason = 'manual') => {
|
|
719
|
+
const startedAt = Date.now();
|
|
720
|
+
try {
|
|
721
|
+
const payload = await api.fetchCredentials();
|
|
722
|
+
const credentials = Array.isArray(payload?.credentials) ? payload.credentials : [];
|
|
723
|
+
const result = persistRuntimeCredentials(credentials);
|
|
724
|
+
debugLog('ticlawk-credentials', 'sync-ok', {
|
|
725
|
+
reason,
|
|
726
|
+
saved: result.saved,
|
|
727
|
+
removed: result.removed,
|
|
728
|
+
durationMs: Date.now() - startedAt,
|
|
729
|
+
wakeToSyncMs: String(reason || '').startsWith('wake') && lastCredentialsWakeAt
|
|
730
|
+
? Date.now() - lastCredentialsWakeAt
|
|
731
|
+
: null,
|
|
732
|
+
});
|
|
733
|
+
} catch (err) {
|
|
734
|
+
if (api.isUpdateRequiredError(err)) {
|
|
735
|
+
recordUpdateRequired(err, 'credentials.sync');
|
|
736
|
+
}
|
|
737
|
+
debugError('ticlawk-credentials', 'sync-failed', {
|
|
738
|
+
reason,
|
|
739
|
+
durationMs: Date.now() - startedAt,
|
|
740
|
+
error: err?.message || 'unknown error',
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
|
|
758
745
|
async function releaseBlockedRows(agentId, messages, reason) {
|
|
759
746
|
for (const msg of messages) {
|
|
760
747
|
if (!msg?.delivery_id) continue;
|
|
@@ -803,7 +790,8 @@ export function createTiclawkAdapter(ctx) {
|
|
|
803
790
|
return { failed: true, claimed: 0, launched: 0 };
|
|
804
791
|
}
|
|
805
792
|
|
|
806
|
-
const
|
|
793
|
+
const orderedData = Array.isArray(data) ? [...data].sort(compareClaimedDeliveries) : [];
|
|
794
|
+
const claimed = orderedData.length;
|
|
807
795
|
debugLog('ticlawk', 'claim.result', {
|
|
808
796
|
reason,
|
|
809
797
|
hostId,
|
|
@@ -822,12 +810,12 @@ export function createTiclawkAdapter(ctx) {
|
|
|
822
810
|
});
|
|
823
811
|
}
|
|
824
812
|
|
|
825
|
-
if (
|
|
813
|
+
if (orderedData.length === 0) {
|
|
826
814
|
return { failed: false, claimed: 0, launched: 0 };
|
|
827
815
|
}
|
|
828
816
|
|
|
829
817
|
const grouped = new Map();
|
|
830
|
-
for (const msg of
|
|
818
|
+
for (const msg of orderedData) {
|
|
831
819
|
const agentId = getAgentIdFromPayload(msg);
|
|
832
820
|
if (!agentId) {
|
|
833
821
|
// Claim rows must carry the recipient agent id; a missing value
|
|
@@ -866,7 +854,7 @@ export function createTiclawkAdapter(ctx) {
|
|
|
866
854
|
launched += messages.length;
|
|
867
855
|
}
|
|
868
856
|
|
|
869
|
-
return { failed: false, claimed:
|
|
857
|
+
return { failed: false, claimed: orderedData.length, launched };
|
|
870
858
|
}
|
|
871
859
|
|
|
872
860
|
async function runDrain(reason) {
|
|
@@ -883,23 +871,7 @@ export function createTiclawkAdapter(ctx) {
|
|
|
883
871
|
return { totalClaimed, iterations };
|
|
884
872
|
}
|
|
885
873
|
|
|
886
|
-
|
|
887
|
-
if (drainPromise) {
|
|
888
|
-
drainRequested = true;
|
|
889
|
-
return drainPromise;
|
|
890
|
-
}
|
|
891
|
-
drainPromise = (async () => {
|
|
892
|
-
let currentReason = reason;
|
|
893
|
-
do {
|
|
894
|
-
drainRequested = false;
|
|
895
|
-
await runDrain(currentReason);
|
|
896
|
-
currentReason = 'drain.requested-again';
|
|
897
|
-
} while (drainRequested);
|
|
898
|
-
})().finally(() => {
|
|
899
|
-
drainPromise = null;
|
|
900
|
-
});
|
|
901
|
-
return drainPromise;
|
|
902
|
-
}
|
|
874
|
+
const requestDrain = coalesce(runDrain);
|
|
903
875
|
|
|
904
876
|
function scheduleDrain(reason) {
|
|
905
877
|
jobsWakeTimer = clearDebounce(jobsWakeTimer);
|
|
@@ -921,6 +893,15 @@ export function createTiclawkAdapter(ctx) {
|
|
|
921
893
|
bindingsWakeTimer.unref?.();
|
|
922
894
|
}
|
|
923
895
|
|
|
896
|
+
function scheduleCredentialSync(reason) {
|
|
897
|
+
credentialsWakeTimer = clearDebounce(credentialsWakeTimer);
|
|
898
|
+
credentialsWakeTimer = setTimeout(() => {
|
|
899
|
+
credentialsWakeTimer = null;
|
|
900
|
+
void syncCredentials(reason);
|
|
901
|
+
}, CREDENTIALS_WAKE_DEBOUNCE_MS);
|
|
902
|
+
credentialsWakeTimer.unref?.();
|
|
903
|
+
}
|
|
904
|
+
|
|
924
905
|
async function reportHostCapabilitiesNow() {
|
|
925
906
|
const entries = await Promise.all(Object.entries(ctx.runtimes || {})
|
|
926
907
|
.filter(([, runtime]) => typeof runtime?.health === 'function')
|
|
@@ -960,6 +941,7 @@ export function createTiclawkAdapter(ctx) {
|
|
|
960
941
|
void refreshBindings('wake.hello')
|
|
961
942
|
.then(() => requestDrain('wake.hello'))
|
|
962
943
|
.catch(() => {});
|
|
944
|
+
void syncCredentials('wake.hello');
|
|
963
945
|
void reportHostCapabilitiesNow();
|
|
964
946
|
return;
|
|
965
947
|
}
|
|
@@ -981,6 +963,17 @@ export function createTiclawkAdapter(ctx) {
|
|
|
981
963
|
scheduleRefreshAndDrain('wake.bindings.changed');
|
|
982
964
|
return;
|
|
983
965
|
}
|
|
966
|
+
if (event?.type === 'credentials.changed') {
|
|
967
|
+
lastCredentialsWakeAt = Date.now();
|
|
968
|
+
debugLog('ticlawk-wake', 'credentials.changed', {
|
|
969
|
+
credentialId: event.credential_id || null,
|
|
970
|
+
name: event.name || null,
|
|
971
|
+
status: event.status || null,
|
|
972
|
+
reason: event.reason || null,
|
|
973
|
+
});
|
|
974
|
+
scheduleCredentialSync('wake.credentials.changed');
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
984
977
|
if (event?.type === 'auth.revoked') {
|
|
985
978
|
wakeState.lastError = 'auth revoked';
|
|
986
979
|
debugError('ticlawk-wake', 'auth.revoked', {});
|
|
@@ -1015,7 +1008,17 @@ export function createTiclawkAdapter(ctx) {
|
|
|
1015
1008
|
|
|
1016
1009
|
function connectWakeSocket() {
|
|
1017
1010
|
connectorSocket = new TiclawkWakeClient({
|
|
1018
|
-
|
|
1011
|
+
// host_id rides on the WS query string so connector-wake can flip
|
|
1012
|
+
// runtime_hosts.online for the right row. Empty host_id is
|
|
1013
|
+
// tolerated by the server (it just skips the state write).
|
|
1014
|
+
getUrl: () => {
|
|
1015
|
+
const base = String(api.getConnectorWsUrl() || '').trim();
|
|
1016
|
+
if (!base) return '';
|
|
1017
|
+
const hostId = String(getHostId() || '').trim();
|
|
1018
|
+
if (!hostId) return base;
|
|
1019
|
+
const sep = base.includes('?') ? '&' : '?';
|
|
1020
|
+
return `${base}${sep}host_id=${encodeURIComponent(hostId)}`;
|
|
1021
|
+
},
|
|
1019
1022
|
getApiKey: api.getApiKey,
|
|
1020
1023
|
onEvent: handleWakeEvent,
|
|
1021
1024
|
onStatus: handleWakeStatus,
|
|
@@ -1165,11 +1168,11 @@ export function createTiclawkAdapter(ctx) {
|
|
|
1165
1168
|
id: 'ticlawk',
|
|
1166
1169
|
|
|
1167
1170
|
async start() {
|
|
1168
|
-
//
|
|
1169
|
-
//
|
|
1170
|
-
//
|
|
1171
|
-
// 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.
|
|
1172
1174
|
await refreshBindings('startup');
|
|
1175
|
+
await syncCredentials('startup');
|
|
1173
1176
|
await requestDrain('startup');
|
|
1174
1177
|
connectWakeSocket();
|
|
1175
1178
|
startAuditTimers();
|
|
@@ -1338,22 +1341,19 @@ export function createTiclawkAdapter(ctx) {
|
|
|
1338
1341
|
}
|
|
1339
1342
|
},
|
|
1340
1343
|
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
turnId: outbound.turnId || outbound.replyToMessageId || null,
|
|
1355
|
-
type: outbound.type || 'agent_message',
|
|
1356
|
-
replyToMessageId: outbound.replyToMessageId || null,
|
|
1344
|
+
// Daemon-driven impersonated reply: the runtime never reached
|
|
1345
|
+
// `ticlawk message send` (subprocess died, timeout, etc.), so the
|
|
1346
|
+
// daemon speaks for the agent. Used by reportSubprocessFailure
|
|
1347
|
+
// with visibility='admin' so the notice only reaches owners.
|
|
1348
|
+
async postAgentReply(binding, { conversationId, text, replyToMessageId, visibility }) {
|
|
1349
|
+
if (!conversationId || !text) return null;
|
|
1350
|
+
return api.sendAgentMessage({
|
|
1351
|
+
actingAgentId: binding.id,
|
|
1352
|
+
conversationId,
|
|
1353
|
+
text,
|
|
1354
|
+
replyToMessageId: replyToMessageId || null,
|
|
1355
|
+
runtimeHostId: hostId,
|
|
1356
|
+
visibility: visibility || null,
|
|
1357
1357
|
});
|
|
1358
1358
|
},
|
|
1359
1359
|
|
|
@@ -165,7 +165,7 @@ export class TiclawkWakeClient {
|
|
|
165
165
|
return;
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
if (type === 'jobs.available' || type === 'bindings.changed') {
|
|
168
|
+
if (type === 'jobs.available' || type === 'bindings.changed' || type === 'credentials.changed') {
|
|
169
169
|
this.onEvent?.(msg);
|
|
170
170
|
return;
|
|
171
171
|
}
|