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.
@@ -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 { AF_ADAPTER_KEY, loadPersistentConfig, persistConfig, TICLAWK_CONNECTOR_API_KEY, TICLAWK_CONNECTOR_WS_URL } from '../../core/config.mjs';
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 normalizeInboundMessage(msg) {
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: msg.agent_id || '',
163
+ bindingId: recipientAgentId,
53
164
  messageId,
54
- text: msg.text || '',
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
- return String(payload?.agent_id || payload?.agentId || '').trim();
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
- function buildBindingFromClaimedMessage(msg) {
98
- const agentId = getAgentIdFromPayload(msg);
99
- const runtime = msg.agent_service_type || msg.service_type;
100
- const rawMeta = msg.agent_meta || msg.meta;
101
- const sourceMeta = (rawMeta && typeof rawMeta === 'object') ? { ...rawMeta } : {};
102
- const fallbackWorkdir = runtime === 'openclaw' && sourceMeta.agentId
103
- ? resolveOpenClawWorkspace(sourceMeta.agentId)
104
- : '';
105
- const workdir = getWorkdir(sourceMeta) || fallbackWorkdir;
106
- if (workdir) {
107
- sourceMeta.workdir = workdir;
108
- sourceMeta.projectDir = sourceMeta.projectDir || workdir;
109
- sourceMeta.cwd = sourceMeta.cwd || workdir;
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
- const rawRuntimeVersion = msg.agent_runtime_version ?? msg.runtime_version;
112
- const runtimeVersion = Number.isInteger(rawRuntimeVersion) ? Number(rawRuntimeVersion) : 0;
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
- agentId,
120
- runtime_host_id: getRuntimeHostIdFromPayload(msg) || undefined,
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
- ...sourceMeta,
127
- runtimeVersion,
276
+ ...projectRuntimeMeta(agent?.meta),
277
+ runtimeVersion: normalizeRuntimeVersion(agent?.runtime_version),
128
278
  },
129
- displayName: msg.agent_display_name || msg.agent_name || agentId,
130
- status: msg.agent_status || msg.status || 'connected',
279
+ displayName: agent?.display_name || agent?.name || agentId,
280
+ status: agent?.status || 'connected',
131
281
  };
132
282
  }
133
283
 
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
- });
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.releaseMessage(msg.message_id, hostId);
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.cacheBinding(buildBindingFromClaimedMessage(msg));
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.releaseMessage(msg.message_id, hostId);
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.completeMessage(msg.message_id, hostId);
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.completeMessage(msg.message_id, hostId);
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.releaseMessage(msg.message_id, hostId);
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.cacheBinding(buildBindingFromChannelSnapshot(agent));
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?.message_id) continue;
760
+ if (!msg?.delivery_id) continue;
596
761
  try {
597
- await api.releaseMessage(msg.message_id, hostId);
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.claimPendingMessages(hostId, 5, excludedChannelIds);
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) || '__default__';
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
- try {
958
- const recovered = await api.recoverClaimedMessages(hostId);
959
- if (recovered?.recoveredCount) {
960
- debugLog('ticlawk', 'message.recovered', {
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();