ticlawk 0.1.16-dev.1 → 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 +13 -0
- package/bin/ticlawk.mjs +121 -0
- package/package.json +1 -1
- package/src/adapters/ticlawk/api.mjs +216 -6
- package/src/adapters/ticlawk/index.mjs +221 -71
- package/src/cli/agent-commands.mjs +545 -6
- package/src/core/agent-cli-handlers.mjs +416 -3
- package/src/core/agent-home.mjs +89 -0
- package/src/core/http.mjs +114 -0
- package/src/core/reminder-ticker.mjs +70 -0
- package/src/core/runtime-contract.mjs +1 -1
- package/src/core/runtime-support.mjs +31 -29
- package/src/core/ticlawk-control.mjs +3 -3
- package/src/migrate/write-initial-memory.mjs +101 -0
- package/src/runtimes/_shared/standing-prompt.mjs +277 -76
- package/src/runtimes/claude-code/index.mjs +8 -27
- package/src/runtimes/codex/index.mjs +15 -32
- package/src/runtimes/openclaw/index.mjs +34 -13
- package/src/runtimes/openclaw/target.mjs +0 -30
- package/src/runtimes/opencode/index.mjs +13 -31
- package/src/runtimes/pi/index.mjs +13 -32
- package/ticlawk.mjs +31 -6
|
@@ -9,7 +9,6 @@ import { getActiveProfile, ensureLegacyProfile, readProfileMeta, saveAndActivate
|
|
|
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,10 +44,6 @@ function normalizeInboundMediaAssets(msg) {
|
|
|
45
44
|
.filter(Boolean);
|
|
46
45
|
}
|
|
47
46
|
|
|
48
|
-
function shortMsgId(value) {
|
|
49
|
-
return value ? String(value).slice(0, 8) : '';
|
|
50
|
-
}
|
|
51
|
-
|
|
52
47
|
function buildEnvelopeTarget(msg) {
|
|
53
48
|
const convType = msg.conversation_type || 'dm';
|
|
54
49
|
const conversationId = msg.conversation_id || '';
|
|
@@ -57,13 +52,8 @@ function buildEnvelopeTarget(msg) {
|
|
|
57
52
|
return senderHandle ? `dm:@${senderHandle}` : `dm:${conversationId}`;
|
|
58
53
|
}
|
|
59
54
|
if (convType === 'thread') {
|
|
60
|
-
// For thread deliveries the message itself is in-thread; reply target
|
|
61
|
-
// is the parent group's id + the thread root's short id. The RPC
|
|
62
|
-
// returns conversation_type='thread' only when the message lives in
|
|
63
|
-
// a thread conversation directly, which we represent as
|
|
64
|
-
// `<group>:<thread-root>`.
|
|
65
55
|
const groupName = msg.conversation_name || conversationId;
|
|
66
|
-
const threadRoot =
|
|
56
|
+
const threadRoot = msg.thread_root_message_id || msg.message_id || '';
|
|
67
57
|
return `#${groupName}:${threadRoot}`;
|
|
68
58
|
}
|
|
69
59
|
// group
|
|
@@ -71,26 +61,104 @@ function buildEnvelopeTarget(msg) {
|
|
|
71
61
|
return `#${groupName}`;
|
|
72
62
|
}
|
|
73
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
|
+
|
|
74
85
|
function buildEnvelopeHeader(msg) {
|
|
75
86
|
const target = buildEnvelopeTarget(msg);
|
|
76
|
-
const msgId =
|
|
87
|
+
const msgId = msg.id || msg.message_id || '';
|
|
77
88
|
const seq = msg.seq != null ? msg.seq : '';
|
|
78
89
|
const time = msg.created_at || new Date().toISOString();
|
|
79
90
|
const type = msg.sender_type || 'human';
|
|
80
91
|
const sender = msg.sender_display_name || msg.sender_user_id || msg.sender_agent_id || 'unknown';
|
|
81
|
-
|
|
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');
|
|
82
137
|
}
|
|
83
138
|
|
|
84
139
|
export function normalizeInboundMessage(msg) {
|
|
85
140
|
// Claimed delivery rows carry the recipient agent id; legacy messages
|
|
86
141
|
// (history sync, manual inserts) may still use plain agent_id. Prefer
|
|
87
142
|
// the new field but fall back so the same normalizer works for both.
|
|
88
|
-
const recipientAgentId = msg.recipient_agent_id ||
|
|
143
|
+
const recipientAgentId = msg.recipient_agent_id || '';
|
|
89
144
|
const messageId = msg.id || msg.message_id || null;
|
|
90
145
|
const deliveryId = msg.delivery_id || null;
|
|
91
146
|
const media = normalizeInboundMediaAssets(msg);
|
|
92
147
|
const rawText = msg.text || '';
|
|
93
|
-
const
|
|
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;
|
|
94
162
|
return {
|
|
95
163
|
bindingId: recipientAgentId,
|
|
96
164
|
messageId,
|
|
@@ -99,7 +167,8 @@ export function normalizeInboundMessage(msg) {
|
|
|
99
167
|
seq: msg.seq != null ? Number(msg.seq) : null,
|
|
100
168
|
senderType: msg.sender_type || 'human',
|
|
101
169
|
envelopeHeader: header,
|
|
102
|
-
|
|
170
|
+
envelopeTarget: target,
|
|
171
|
+
text,
|
|
103
172
|
rawText,
|
|
104
173
|
action: msg.action || (media.length > 0 ? 'image' : 'task'),
|
|
105
174
|
media,
|
|
@@ -114,10 +183,6 @@ function getBindingSessionId(binding) {
|
|
|
114
183
|
return binding?.runtimeMeta?.sessionId || binding?.runtimeMeta?.sessionKey || binding?.runtimeMeta?.agentId || binding?.id || null;
|
|
115
184
|
}
|
|
116
185
|
|
|
117
|
-
function getWorkdir(meta = {}) {
|
|
118
|
-
return String(meta.workdir || meta.projectDir || meta.cwd || '').trim();
|
|
119
|
-
}
|
|
120
|
-
|
|
121
186
|
function getRuntimeVersion(bindingOrMeta) {
|
|
122
187
|
const value = bindingOrMeta?.runtimeMeta?.runtimeVersion ?? bindingOrMeta?.runtimeVersion ?? bindingOrMeta?.agent_runtime_version ?? null;
|
|
123
188
|
return Number.isInteger(value) ? Number(value) : null;
|
|
@@ -140,65 +205,104 @@ function getRuntimeHostLabelFromPayload(payload) {
|
|
|
140
205
|
}
|
|
141
206
|
|
|
142
207
|
function getAgentIdFromPayload(payload) {
|
|
143
|
-
|
|
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();
|
|
144
213
|
}
|
|
145
214
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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];
|
|
166
252
|
}
|
|
167
|
-
|
|
168
|
-
|
|
253
|
+
return out;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function normalizeRuntimeVersion(value) {
|
|
257
|
+
return Number.isInteger(value) ? Number(value) : 0;
|
|
258
|
+
}
|
|
169
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;
|
|
170
267
|
return {
|
|
171
268
|
id: agentId,
|
|
172
269
|
adapter: 'ticlawk',
|
|
173
270
|
targetKey: agentId,
|
|
174
|
-
targetMeta: {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
},
|
|
178
|
-
runtime_host_id: getRuntimeHostIdFromPayload(msg) || undefined,
|
|
179
|
-
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,
|
|
180
274
|
runtime,
|
|
181
275
|
runtimeMeta: {
|
|
182
|
-
...
|
|
183
|
-
runtimeVersion,
|
|
276
|
+
...projectRuntimeMeta(agent?.meta),
|
|
277
|
+
runtimeVersion: normalizeRuntimeVersion(agent?.runtime_version),
|
|
184
278
|
},
|
|
185
|
-
displayName:
|
|
186
|
-
status:
|
|
279
|
+
displayName: agent?.display_name || agent?.name || agentId,
|
|
280
|
+
status: agent?.status || 'connected',
|
|
187
281
|
};
|
|
188
282
|
}
|
|
189
283
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
+
};
|
|
202
306
|
}
|
|
203
307
|
|
|
204
308
|
function maskIdentity(identity = {}) {
|
|
@@ -529,7 +633,7 @@ export function createTiclawkAdapter(ctx) {
|
|
|
529
633
|
continue;
|
|
530
634
|
}
|
|
531
635
|
|
|
532
|
-
const binding = await ctx.
|
|
636
|
+
const binding = await ctx.persistBinding(buildBindingFromDeliveryRow(msg));
|
|
533
637
|
if (!binding?.runtime) {
|
|
534
638
|
throw new Error('claimed message missing runtime binding');
|
|
535
639
|
}
|
|
@@ -629,7 +733,7 @@ export function createTiclawkAdapter(ctx) {
|
|
|
629
733
|
continue;
|
|
630
734
|
}
|
|
631
735
|
try {
|
|
632
|
-
await ctx.
|
|
736
|
+
await ctx.persistBinding(buildBindingFromAgentRow(agent));
|
|
633
737
|
hydrated += 1;
|
|
634
738
|
} catch (err) {
|
|
635
739
|
debugError('ticlawk', 'binding.hydrate-failed', {
|
|
@@ -724,7 +828,18 @@ export function createTiclawkAdapter(ctx) {
|
|
|
724
828
|
|
|
725
829
|
const grouped = new Map();
|
|
726
830
|
for (const msg of data) {
|
|
727
|
-
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
|
+
}
|
|
728
843
|
const bucket = grouped.get(agentId) || [];
|
|
729
844
|
bucket.push(msg);
|
|
730
845
|
grouped.set(agentId, bucket);
|
|
@@ -806,12 +921,46 @@ export function createTiclawkAdapter(ctx) {
|
|
|
806
921
|
bindingsWakeTimer.unref?.();
|
|
807
922
|
}
|
|
808
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
|
+
|
|
809
957
|
function handleWakeEvent(event) {
|
|
810
958
|
wakeState.lastEventAt = new Date().toISOString();
|
|
811
959
|
if (event?.type === 'hello') {
|
|
812
960
|
void refreshBindings('wake.hello')
|
|
813
961
|
.then(() => requestDrain('wake.hello'))
|
|
814
962
|
.catch(() => {});
|
|
963
|
+
void reportHostCapabilitiesNow();
|
|
815
964
|
return;
|
|
816
965
|
}
|
|
817
966
|
if (event?.type === 'jobs.available') {
|
|
@@ -1016,9 +1165,10 @@ export function createTiclawkAdapter(ctx) {
|
|
|
1016
1165
|
id: 'ticlawk',
|
|
1017
1166
|
|
|
1018
1167
|
async start() {
|
|
1019
|
-
//
|
|
1020
|
-
//
|
|
1021
|
-
//
|
|
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.
|
|
1022
1172
|
await refreshBindings('startup');
|
|
1023
1173
|
await requestDrain('startup');
|
|
1024
1174
|
connectWakeSocket();
|