ticlawk 0.1.16-dev.15 → 0.1.16-dev.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -2
- package/bin/ticlawk.mjs +70 -20
- package/package.json +1 -1
- package/src/adapters/ticlawk/api.mjs +18 -6
- package/src/adapters/ticlawk/credentials.mjs +41 -1
- package/src/adapters/ticlawk/index.mjs +91 -196
- package/src/adapters/ticlawk/wake-client.mjs +1 -1
- package/src/cli/agent-commands.mjs +131 -79
- package/src/core/agent-cli-handlers.mjs +83 -24
- package/src/core/http.mjs +5 -0
- package/src/core/runtime-env.mjs +7 -0
- package/src/core/runtime-support.mjs +101 -0
- package/src/runtimes/_shared/brand.mjs +1 -0
- package/src/runtimes/_shared/goal-task-protocol.mjs +196 -0
- package/src/runtimes/_shared/standing-prompt.mjs +103 -294
- package/src/runtimes/_shared/wake-prompt.mjs +173 -0
- package/src/runtimes/claude-code/index.mjs +15 -9
- package/src/runtimes/codex/index.mjs +21 -14
- package/src/runtimes/openclaw/index.mjs +11 -9
- package/src/runtimes/opencode/index.mjs +36 -13
- package/src/runtimes/opencode/session.mjs +5 -4
- package/src/runtimes/pi/index.mjs +36 -14
- package/src/runtimes/pi/session.mjs +5 -2
|
@@ -10,13 +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 { persistApiCredential } from './credentials.mjs';
|
|
13
|
+
import { persistApiCredential, persistRuntimeCredentials } from './credentials.mjs';
|
|
14
14
|
import { TiclawkWakeClient } from './wake-client.mjs';
|
|
15
|
+
import { buildInboundWakePrompt } from '../../runtimes/_shared/wake-prompt.mjs';
|
|
15
16
|
|
|
16
17
|
const require = createRequire(import.meta.url);
|
|
17
18
|
const qrcode = require('qrcode-terminal');
|
|
18
19
|
const JOBS_WAKE_DEBOUNCE_MS = 100;
|
|
19
20
|
const BINDINGS_WAKE_DEBOUNCE_MS = 500;
|
|
21
|
+
const CREDENTIALS_WAKE_DEBOUNCE_MS = 500;
|
|
20
22
|
const RECOVERY_AUDIT_INTERVAL_MS = 5 * 60 * 1000;
|
|
21
23
|
const BINDING_AUDIT_INTERVAL_MS = 5 * 60 * 1000;
|
|
22
24
|
const UPDATE_RETRY_COOLDOWN_MS = 15 * 60 * 1000;
|
|
@@ -69,175 +71,6 @@ function normalizeInboundMediaAssets(msg) {
|
|
|
69
71
|
.filter(Boolean);
|
|
70
72
|
}
|
|
71
73
|
|
|
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
|
-
// Charter is the workstream's CoS-authored markdown spec. It's a stable
|
|
144
|
-
// prefix-cacheable block — same bytes across every turn in the same
|
|
145
|
-
// conversation — so it sits above the per-turn envelope.
|
|
146
|
-
function buildCharterBlock(msg) {
|
|
147
|
-
const charter = (msg.conversation_charter || '').trim();
|
|
148
|
-
if (!charter) return '';
|
|
149
|
-
return ['[charter]', charter, '[/charter]'].join('\n');
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// CoS-only addendum. Splice above the per-turn envelope (after charter,
|
|
153
|
-
// before group context) when the recipient is the user's CoS.
|
|
154
|
-
// Same bytes every turn → still prefix-cache-friendly.
|
|
155
|
-
const COS_ADDENDUM = [
|
|
156
|
-
'[cos-role]',
|
|
157
|
-
'You are the user\'s Chief of Staff (CoS). You answer to the user. In every workstream:',
|
|
158
|
-
'',
|
|
159
|
-
'1. Push — break goals into agent-executable tasks, supervise progress, do not let work stall in place.',
|
|
160
|
-
'2. Simplify — agents should work simply and reliably. Stop over-engineering, junk-code piles, and detours.',
|
|
161
|
-
'3. Track — keep an entry per workstream in MEMORY.md; update it whenever state changes.',
|
|
162
|
-
'4. Represent — bundle resource / decision / approval requests and bring them to the user; do not flood the user with raw asks.',
|
|
163
|
-
'',
|
|
164
|
-
'Wake-on-event: every turn (including ambient delivery) you must read the message, update MEMORY.md if anything changed, and only reply when you need to intervene. Otherwise stop silently.',
|
|
165
|
-
'',
|
|
166
|
-
'Posture: bring a proposal, default to acting on it, change course if the user objects. "I propose A — reasons 1/2/3. Unless you redirect within 24h, I\'ll proceed with A." Not "should we do A or B?"',
|
|
167
|
-
'',
|
|
168
|
-
'Owner model: hold the user\'s preferences and constraints across workstreams. Apply them consistently. Do not "for-your-own-good" around stated preferences.',
|
|
169
|
-
'',
|
|
170
|
-
'Briefing posture: publish briefings via `ticlawk briefing publish` — this is a SEPARATE surface from chat. Two content formats: `--text "<≤100 chars>"` for short pings, or `--html <path/to/file.html>` for rich one-page bodies. Briefings do NOT appear in the chat stream — they only render in Office → Briefings as full-screen cards owner steps through. Do not over-spam — one per meaningful state change. When the owner taps "comment" on a briefing, the inbound reply message carries metadata.context_ref = { kind: "briefing", briefing_id }; thread the conversation from there.',
|
|
171
|
-
'',
|
|
172
|
-
'Dashboard posture: maintain a per-workstream dashboard via `ticlawk dashboard set --target "#<ws>"` (stdin JSON: { data_json, html_template }). data_json holds the live numbers; html_template is the hand-written rendering. CoS replaces wholesale — there is no partial update protocol, and there is no schema lock-in on data_json. Use the dashboard for at-a-glance state; use Briefings for narrative. When the owner replies to a dashboard via the Office "聊一下" path, the inbound message carries metadata.context_ref pointing at the workstream; thread the conversation from there.',
|
|
173
|
-
'',
|
|
174
|
-
'Cadence: schedule your own daily morning briefing via `ticlawk reminder schedule --title "morning briefing" --in-minutes 1440 --anchor-conversation-id <your DM with owner>` after you handle this turn. The reminder fires by waking you with a system message; treat that wake as the trigger to compile the day\'s briefings across workstreams. Re-schedule on each fire so the cadence persists. Skip a day only if you are explicitly told to.',
|
|
175
|
-
'[/cos-role]',
|
|
176
|
-
].join('\n');
|
|
177
|
-
|
|
178
|
-
function buildCosAddendum(msg) {
|
|
179
|
-
if (!msg.recipient_is_cos) return '';
|
|
180
|
-
return COS_ADDENDUM;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Quote block: surfaced just above the user's reply so the agent sees
|
|
184
|
-
// what artifact the reply is *about*. Source of truth is
|
|
185
|
-
// `messages.metadata.quote = { kind, ref, snippet }`. We render a
|
|
186
|
-
// short, prefix-cache-friendly block and tell the agent how to fetch
|
|
187
|
-
// the full content if needed.
|
|
188
|
-
function buildQuoteBlock(msg) {
|
|
189
|
-
const meta = msg.message_metadata || msg.metadata || null;
|
|
190
|
-
const quote = meta && typeof meta === 'object' ? meta.quote : null;
|
|
191
|
-
if (!quote || typeof quote !== 'object') return '';
|
|
192
|
-
const kind = String(quote.kind || '').trim();
|
|
193
|
-
const ref = String(quote.ref || '').trim();
|
|
194
|
-
const snippet = String(quote.snippet || '').trim();
|
|
195
|
-
if (!kind || !ref) return '';
|
|
196
|
-
const fetchHint = kind === 'briefing'
|
|
197
|
-
? `ticlawk briefing get ${ref}`
|
|
198
|
-
: kind === 'dashboard'
|
|
199
|
-
? `ticlawk dashboard get --target "#${ref}"`
|
|
200
|
-
: kind === 'message'
|
|
201
|
-
? `ticlawk message read --around ${ref}`
|
|
202
|
-
: '';
|
|
203
|
-
const lines = ['[quote', ` kind=${kind} ref=${ref}`];
|
|
204
|
-
if (snippet) lines.push(` "${snippet.replace(/"/g, '\\"')}"`);
|
|
205
|
-
if (fetchHint) lines.push(` fetch: ${fetchHint}`);
|
|
206
|
-
lines.push('[/quote]');
|
|
207
|
-
return lines.join('\n');
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Wrap each per-turn message with an explicit reply instruction so the
|
|
211
|
-
// runtime LLM never has to remember the standing prompt to figure out
|
|
212
|
-
// HOW to reply. Codex in particular treats the developerInstructions as
|
|
213
|
-
// background and ignores the chat-send pattern without this per-turn
|
|
214
|
-
// nudge.
|
|
215
|
-
function buildWakePromptText({ envelopeHeader, target, rawText, groupContext, charterBlock, cosAddendum, quoteBlock }) {
|
|
216
|
-
const body = `${envelopeHeader} ${rawText || ''}`.trim();
|
|
217
|
-
const lines = [];
|
|
218
|
-
if (charterBlock) {
|
|
219
|
-
lines.push(charterBlock, '');
|
|
220
|
-
}
|
|
221
|
-
if (cosAddendum) {
|
|
222
|
-
lines.push(cosAddendum, '');
|
|
223
|
-
}
|
|
224
|
-
if (quoteBlock) {
|
|
225
|
-
lines.push(quoteBlock, '');
|
|
226
|
-
}
|
|
227
|
-
lines.push('New message received:', '', body);
|
|
228
|
-
if (groupContext) {
|
|
229
|
-
lines.push('', groupContext);
|
|
230
|
-
}
|
|
231
|
-
lines.push(
|
|
232
|
-
'',
|
|
233
|
-
`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.`,
|
|
234
|
-
'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.',
|
|
235
|
-
'',
|
|
236
|
-
'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.',
|
|
237
|
-
);
|
|
238
|
-
return lines.join('\n');
|
|
239
|
-
}
|
|
240
|
-
|
|
241
74
|
export function normalizeInboundMessage(msg) {
|
|
242
75
|
// Claimed delivery rows carry the recipient agent id; legacy messages
|
|
243
76
|
// (history sync, manual inserts) may still use plain agent_id. Prefer
|
|
@@ -246,24 +79,8 @@ export function normalizeInboundMessage(msg) {
|
|
|
246
79
|
const messageId = msg.id || msg.message_id || null;
|
|
247
80
|
const deliveryId = msg.delivery_id || null;
|
|
248
81
|
const media = normalizeInboundMediaAssets(msg);
|
|
249
|
-
const rawText = msg.text || '';
|
|
250
82
|
const enriched = { ...msg, id: messageId };
|
|
251
|
-
const
|
|
252
|
-
// Task + reactions suffixes are appended INSIDE the envelope so an
|
|
253
|
-
// agent can see at a glance whether a message is already claimed and
|
|
254
|
-
// who has already acknowledged it: `[task #N status=… assignee=…]` +
|
|
255
|
-
// `[reactions: …]`.
|
|
256
|
-
const taskSuffix = buildTaskSuffix(enriched);
|
|
257
|
-
const reactionsSuffix = buildReactionsSuffix(enriched);
|
|
258
|
-
const header = baseHeader + taskSuffix + reactionsSuffix;
|
|
259
|
-
const target = buildEnvelopeTarget(enriched);
|
|
260
|
-
const groupContext = buildGroupContextBlock(enriched);
|
|
261
|
-
const charterBlock = buildCharterBlock(enriched);
|
|
262
|
-
const cosAddendum = buildCosAddendum(enriched);
|
|
263
|
-
const quoteBlock = buildQuoteBlock(enriched);
|
|
264
|
-
const text = header
|
|
265
|
-
? buildWakePromptText({ envelopeHeader: header, target, rawText, groupContext, charterBlock, cosAddendum, quoteBlock })
|
|
266
|
-
: rawText;
|
|
83
|
+
const wakePrompt = buildInboundWakePrompt(enriched);
|
|
267
84
|
return {
|
|
268
85
|
bindingId: recipientAgentId,
|
|
269
86
|
messageId,
|
|
@@ -271,10 +88,10 @@ export function normalizeInboundMessage(msg) {
|
|
|
271
88
|
conversationId: msg.conversation_id || null,
|
|
272
89
|
seq: msg.seq != null ? Number(msg.seq) : null,
|
|
273
90
|
senderType: msg.sender_type || 'human',
|
|
274
|
-
envelopeHeader: header,
|
|
275
|
-
envelopeTarget: target,
|
|
276
|
-
text,
|
|
277
|
-
rawText,
|
|
91
|
+
envelopeHeader: wakePrompt.header,
|
|
92
|
+
envelopeTarget: wakePrompt.target,
|
|
93
|
+
text: wakePrompt.text,
|
|
94
|
+
rawText: wakePrompt.rawText,
|
|
278
95
|
action: msg.action || (media.length > 0 ? 'image' : 'task'),
|
|
279
96
|
media,
|
|
280
97
|
raw: {
|
|
@@ -323,6 +140,7 @@ const RUNTIME_META_KEYS = [
|
|
|
323
140
|
'runtimePath',
|
|
324
141
|
'rotatePending',
|
|
325
142
|
'lastRotatedAt',
|
|
143
|
+
'conversationSessions',
|
|
326
144
|
// claude_code
|
|
327
145
|
'claudePath',
|
|
328
146
|
'claudeVersion',
|
|
@@ -439,6 +257,31 @@ function getRuntimeWorkdir(runtimeMeta = {}) {
|
|
|
439
257
|
return String(runtimeMeta.workdir || runtimeMeta.cwd || runtimeMeta.projectDir || '').trim();
|
|
440
258
|
}
|
|
441
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
|
+
|
|
442
285
|
function runtimeLabel(runtime) {
|
|
443
286
|
if (runtime === 'claude_code') return 'Claude Code';
|
|
444
287
|
if (runtime === 'opencode') return 'OpenCode';
|
|
@@ -552,8 +395,10 @@ export function createTiclawkAdapter(ctx) {
|
|
|
552
395
|
let bindingAuditTimer = null;
|
|
553
396
|
let jobsWakeTimer = null;
|
|
554
397
|
let bindingsWakeTimer = null;
|
|
398
|
+
let credentialsWakeTimer = null;
|
|
555
399
|
let lastJobsWakeAt = 0;
|
|
556
400
|
let lastBindingsWakeAt = 0;
|
|
401
|
+
let lastCredentialsWakeAt = 0;
|
|
557
402
|
let updateRequired = null;
|
|
558
403
|
let lastUpdateRequiredLogAt = 0;
|
|
559
404
|
|
|
@@ -729,7 +574,7 @@ export function createTiclawkAdapter(ctx) {
|
|
|
729
574
|
|
|
730
575
|
const binding = await ctx.persistBinding(buildBindingFromSource(msg));
|
|
731
576
|
if (!binding?.runtime) {
|
|
732
|
-
throw new Error('claimed
|
|
577
|
+
throw new Error('claimed delivery missing runtime binding');
|
|
733
578
|
}
|
|
734
579
|
if (!belongsToRuntimeHost(binding, hostId)) {
|
|
735
580
|
await api.releaseDelivery(deliveryId, hostId, 'binding-host-mismatch');
|
|
@@ -870,6 +715,33 @@ export function createTiclawkAdapter(ctx) {
|
|
|
870
715
|
return hydrated;
|
|
871
716
|
}
|
|
872
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
|
+
|
|
873
745
|
async function releaseBlockedRows(agentId, messages, reason) {
|
|
874
746
|
for (const msg of messages) {
|
|
875
747
|
if (!msg?.delivery_id) continue;
|
|
@@ -918,7 +790,8 @@ export function createTiclawkAdapter(ctx) {
|
|
|
918
790
|
return { failed: true, claimed: 0, launched: 0 };
|
|
919
791
|
}
|
|
920
792
|
|
|
921
|
-
const
|
|
793
|
+
const orderedData = Array.isArray(data) ? [...data].sort(compareClaimedDeliveries) : [];
|
|
794
|
+
const claimed = orderedData.length;
|
|
922
795
|
debugLog('ticlawk', 'claim.result', {
|
|
923
796
|
reason,
|
|
924
797
|
hostId,
|
|
@@ -937,12 +810,12 @@ export function createTiclawkAdapter(ctx) {
|
|
|
937
810
|
});
|
|
938
811
|
}
|
|
939
812
|
|
|
940
|
-
if (
|
|
813
|
+
if (orderedData.length === 0) {
|
|
941
814
|
return { failed: false, claimed: 0, launched: 0 };
|
|
942
815
|
}
|
|
943
816
|
|
|
944
817
|
const grouped = new Map();
|
|
945
|
-
for (const msg of
|
|
818
|
+
for (const msg of orderedData) {
|
|
946
819
|
const agentId = getAgentIdFromPayload(msg);
|
|
947
820
|
if (!agentId) {
|
|
948
821
|
// Claim rows must carry the recipient agent id; a missing value
|
|
@@ -981,7 +854,7 @@ export function createTiclawkAdapter(ctx) {
|
|
|
981
854
|
launched += messages.length;
|
|
982
855
|
}
|
|
983
856
|
|
|
984
|
-
return { failed: false, claimed:
|
|
857
|
+
return { failed: false, claimed: orderedData.length, launched };
|
|
985
858
|
}
|
|
986
859
|
|
|
987
860
|
async function runDrain(reason) {
|
|
@@ -1020,6 +893,15 @@ export function createTiclawkAdapter(ctx) {
|
|
|
1020
893
|
bindingsWakeTimer.unref?.();
|
|
1021
894
|
}
|
|
1022
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
|
+
|
|
1023
905
|
async function reportHostCapabilitiesNow() {
|
|
1024
906
|
const entries = await Promise.all(Object.entries(ctx.runtimes || {})
|
|
1025
907
|
.filter(([, runtime]) => typeof runtime?.health === 'function')
|
|
@@ -1059,6 +941,7 @@ export function createTiclawkAdapter(ctx) {
|
|
|
1059
941
|
void refreshBindings('wake.hello')
|
|
1060
942
|
.then(() => requestDrain('wake.hello'))
|
|
1061
943
|
.catch(() => {});
|
|
944
|
+
void syncCredentials('wake.hello');
|
|
1062
945
|
void reportHostCapabilitiesNow();
|
|
1063
946
|
return;
|
|
1064
947
|
}
|
|
@@ -1080,6 +963,17 @@ export function createTiclawkAdapter(ctx) {
|
|
|
1080
963
|
scheduleRefreshAndDrain('wake.bindings.changed');
|
|
1081
964
|
return;
|
|
1082
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
|
+
}
|
|
1083
977
|
if (event?.type === 'auth.revoked') {
|
|
1084
978
|
wakeState.lastError = 'auth revoked';
|
|
1085
979
|
debugError('ticlawk-wake', 'auth.revoked', {});
|
|
@@ -1279,6 +1173,7 @@ export function createTiclawkAdapter(ctx) {
|
|
|
1279
1173
|
// was dropped in X1; no cron has replaced it. Rare in practice;
|
|
1280
1174
|
// when it happens the fix is a one-row UPDATE in supabase.
|
|
1281
1175
|
await refreshBindings('startup');
|
|
1176
|
+
await syncCredentials('startup');
|
|
1282
1177
|
await requestDrain('startup');
|
|
1283
1178
|
connectWakeSocket();
|
|
1284
1179
|
startAuditTimers();
|
|
@@ -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
|
}
|