ticlawk 0.1.12-dev.0

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.
Files changed (55) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +426 -0
  3. package/agent-freeway.mjs +2 -0
  4. package/assets/ticlawk-concept.svg +137 -0
  5. package/bin/agent-freeway.mjs +4 -0
  6. package/bin/ticlawk.mjs +594 -0
  7. package/cc-watcher.mjs +3 -0
  8. package/package.json +72 -0
  9. package/scripts/postinstall.mjs +61 -0
  10. package/src/adapters/telegram/index.mjs +359 -0
  11. package/src/adapters/ticlawk/api.mjs +360 -0
  12. package/src/adapters/ticlawk/cards.mjs +149 -0
  13. package/src/adapters/ticlawk/credentials.mjs +25 -0
  14. package/src/adapters/ticlawk/index.mjs +1229 -0
  15. package/src/adapters/ticlawk/wake-client.mjs +204 -0
  16. package/src/core/adapter-registry.mjs +50 -0
  17. package/src/core/argv.mjs +38 -0
  18. package/src/core/bindings/store.mjs +81 -0
  19. package/src/core/bus.mjs +91 -0
  20. package/src/core/config.mjs +203 -0
  21. package/src/core/daemon-install.mjs +246 -0
  22. package/src/core/diagnostics.mjs +79 -0
  23. package/src/core/events/worker-events.mjs +80 -0
  24. package/src/core/executables.mjs +106 -0
  25. package/src/core/host-id.mjs +48 -0
  26. package/src/core/http.mjs +65 -0
  27. package/src/core/logger.mjs +34 -0
  28. package/src/core/media/inbound.mjs +127 -0
  29. package/src/core/media/outbound.mjs +163 -0
  30. package/src/core/profiles.mjs +173 -0
  31. package/src/core/runtime-contract.mjs +68 -0
  32. package/src/core/runtime-env.mjs +9 -0
  33. package/src/core/runtime-registry.mjs +93 -0
  34. package/src/core/runtime-support.mjs +197 -0
  35. package/src/core/setup-readiness.mjs +86 -0
  36. package/src/core/store/json-file-store.mjs +47 -0
  37. package/src/core/ticlawk-control.mjs +92 -0
  38. package/src/core/uninstall.mjs +142 -0
  39. package/src/core/update-state.mjs +62 -0
  40. package/src/core/update.mjs +178 -0
  41. package/src/runtimes/claude-code/index.mjs +363 -0
  42. package/src/runtimes/claude-code/session.mjs +388 -0
  43. package/src/runtimes/claude-code/transcripts.mjs +206 -0
  44. package/src/runtimes/codex/index.mjs +306 -0
  45. package/src/runtimes/codex/session.mjs +750 -0
  46. package/src/runtimes/openclaw/gateway.mjs +269 -0
  47. package/src/runtimes/openclaw/identity.mjs +34 -0
  48. package/src/runtimes/openclaw/index.mjs +228 -0
  49. package/src/runtimes/openclaw/inflight.mjs +46 -0
  50. package/src/runtimes/openclaw/target.mjs +57 -0
  51. package/src/runtimes/opencode/index.mjs +318 -0
  52. package/src/runtimes/opencode/session.mjs +413 -0
  53. package/src/runtimes/pi/index.mjs +287 -0
  54. package/src/runtimes/pi/session.mjs +423 -0
  55. package/ticlawk.mjs +260 -0
@@ -0,0 +1,269 @@
1
+ /**
2
+ * OpenClaw Gateway WebSocket runtime state.
3
+ *
4
+ * Owns the single persistent gateway connection and the in-flight
5
+ * promise map used by `askGateway`. Module-level closures hold the
6
+ * state — the gateway is a singleton per ticlawk process, so there's
7
+ * no need for a class.
8
+ *
9
+ * Exported surface:
10
+ * askGateway(text, agentId, opts) — ask an agent, returns
11
+ * { text, mediaUrls }
12
+ * registerOpenClawChannel({ channelId, ... }) — called after an
13
+ * OpenClaw binding is
14
+ * available; ensures
15
+ * the gateway connects
16
+ * on first call
17
+ * This module is runtime-only. It knows nothing about adapters or
18
+ * backend channel lookups; callers inject any event forwarding they need
19
+ * via `askGateway(..., { onEvent })`.
20
+ */
21
+
22
+ import { WebSocket } from 'ws';
23
+ import { createPrivateKey, randomUUID, sign } from 'node:crypto';
24
+ import { GATEWAY_HOST, GATEWAY_PORT, loadOpenClawConfig } from './identity.mjs';
25
+ import {
26
+ buildOpenClawSessionKey,
27
+ normalizeOpenClawAgentId,
28
+ describeGatewayRequestContext,
29
+ } from './target.mjs';
30
+
31
+ // ── Module-level closure state ──────────────────────────────────────────
32
+
33
+ const openclawConfig = loadOpenClawConfig();
34
+ let gw = null;
35
+ let gwReady = false;
36
+ const pendingRequests = new Map();
37
+ let gatewayStarted = false;
38
+
39
+ // ── Connection ──────────────────────────────────────────────────────────
40
+
41
+ function connectGateway() {
42
+ const url = `ws://${GATEWAY_HOST}:${GATEWAY_PORT}`;
43
+ console.log(`[relay] connecting to Gateway at ${url}`);
44
+ gw = new WebSocket(url);
45
+
46
+ gw.on('open', () => {
47
+ console.log('[relay] WebSocket open, waiting for challenge...');
48
+ });
49
+
50
+ gw.on('message', (raw) => {
51
+ let msg;
52
+ try { msg = JSON.parse(raw.toString()); } catch { return; }
53
+
54
+ if (msg.type === 'event' && msg.event === 'connect.challenge' && !gwReady) {
55
+ const nonce = msg.payload?.nonce;
56
+ const { gatewayToken, deviceId, publicKey, privateKeyPem } = openclawConfig;
57
+ const scopes = ['operator.admin', 'operator.read', 'operator.write', 'operator.approvals', 'operator.pairing'];
58
+ const params = {
59
+ minProtocol: 3, maxProtocol: 3,
60
+ client: { id: 'cli', version: '2026.3.13', platform: 'linux', mode: 'cli' },
61
+ role: 'operator', scopes,
62
+ auth: { token: gatewayToken },
63
+ locale: 'en', userAgent: 'ticlawk/0.6',
64
+ };
65
+
66
+ if (deviceId && privateKeyPem) {
67
+ const privKey = createPrivateKey(privateKeyPem);
68
+ const signedAt = Date.now();
69
+ const payload = ['v2', deviceId, 'cli', 'cli', 'operator', scopes.join(','), String(signedAt), gatewayToken, nonce].join('|');
70
+ const signature = sign(null, Buffer.from(payload), privKey).toString('base64url');
71
+ params.device = { id: deviceId, publicKey, signature, signedAt, nonce };
72
+ console.log('[relay] sending signed connect...');
73
+ }
74
+
75
+ gw.send(JSON.stringify({ type: 'req', id: 'connect', method: 'connect', params }));
76
+ return;
77
+ }
78
+
79
+ handleGatewayMessage(msg);
80
+ });
81
+
82
+ gw.on('close', () => {
83
+ console.log('[relay] Gateway connection closed, reconnecting in 5s...');
84
+ gwReady = false;
85
+ setTimeout(connectGateway, 5000);
86
+ });
87
+
88
+ gw.on('error', (err) => {
89
+ console.error('[relay] Gateway WebSocket error:', err.message);
90
+ });
91
+ }
92
+
93
+ function emitGatewayEvent(pending, event) {
94
+ if (!pending || typeof pending.onEvent !== 'function') return;
95
+ try {
96
+ pending.onEvent({
97
+ runId: pending.runId || null,
98
+ agentId: pending.agentId,
99
+ sessionKey: pending.sessionKey,
100
+ channelId: pending.channelId,
101
+ ...event,
102
+ });
103
+ } catch {}
104
+ }
105
+
106
+ function handleGatewayMessage(msg) {
107
+ if (msg.type === 'res' && msg.id === 'connect') {
108
+ if (msg.error) { console.error('[relay] connect failed:', msg.error); return; }
109
+ console.log('[relay] connected to Gateway ✓');
110
+ gwReady = true;
111
+ return;
112
+ }
113
+
114
+ if (msg.type === 'event' && msg.event === 'agent') {
115
+ const p = msg.payload || {};
116
+ const d = p.data || {};
117
+ const runId = p.runId;
118
+
119
+ let pending = null;
120
+ for (const [, req] of pendingRequests) {
121
+ if (req.runId === runId) { pending = req; break; }
122
+ }
123
+ if (!pending) return;
124
+
125
+ if (p.stream === 'assistant') {
126
+ if (typeof d.text === 'string') pending.buffer = d.text;
127
+ else if (typeof d.delta === 'string') pending.buffer += d.delta;
128
+ }
129
+
130
+ if (d.mediaUrl) pending.mediaUrls.add(d.mediaUrl);
131
+ if (Array.isArray(d.mediaUrls)) d.mediaUrls.forEach((u) => pending.mediaUrls.add(u));
132
+
133
+ if (p.stream === 'compaction') {
134
+ if (d.phase === 'start') {
135
+ emitGatewayEvent(pending, {
136
+ hook_event_name: 'agent.compaction.start',
137
+ phase: d.phase,
138
+ });
139
+ } else if (d.phase === 'end') {
140
+ emitGatewayEvent(pending, {
141
+ hook_event_name: 'agent.compaction.end',
142
+ willRetry: !!d.willRetry,
143
+ completed: !!d.completed,
144
+ phase: d.phase,
145
+ });
146
+ }
147
+ }
148
+
149
+ if (p.stream === 'lifecycle') {
150
+ // OpenClaw emits the human-readable error string in `data.error`
151
+ // (NOT `data.message`). Old protocol used `message` so we keep
152
+ // it as a fallback.
153
+ const phaseError = typeof d.error === 'string' ? d.error : (typeof d.message === 'string' ? d.message : '');
154
+ console.log(`[gateway] event lifecycle ${describeGatewayRequestContext(pending)} phase=${d.phase || 'unknown'} error=${JSON.stringify(phaseError)}`);
155
+ if (d.phase === 'start') {
156
+ emitGatewayEvent(pending, {
157
+ hook_event_name: 'agent.run.start',
158
+ startedAt: typeof d.startedAt === 'number' ? d.startedAt : null,
159
+ phase: d.phase,
160
+ });
161
+ } else if (d.phase === 'end') {
162
+ emitGatewayEvent(pending, {
163
+ hook_event_name: 'agent.run.end',
164
+ endedAt: typeof d.endedAt === 'number' ? d.endedAt : null,
165
+ durationMs: Date.now() - pending.startedAt,
166
+ messageLength: pending.buffer.length,
167
+ phase: d.phase,
168
+ });
169
+ pending.resolve({ text: pending.buffer, mediaUrls: [...pending.mediaUrls] });
170
+ pendingRequests.delete(pending.id);
171
+ } else if (d.phase === 'error') {
172
+ emitGatewayEvent(pending, {
173
+ hook_event_name: 'agent.run.error',
174
+ error: phaseError || 'agent error',
175
+ durationMs: Date.now() - pending.startedAt,
176
+ phase: d.phase,
177
+ });
178
+ const err = new Error(phaseError || 'agent error');
179
+ err.gatewayKind = 'agent-error';
180
+ pending.reject(err);
181
+ pendingRequests.delete(pending.id);
182
+ }
183
+ }
184
+ }
185
+ }
186
+
187
+ // ── Status ──────────────────────────────────────────────────────────────
188
+
189
+ /** True iff the gateway WebSocket is connected and authenticated. */
190
+ export function isGatewayReady() {
191
+ return gwReady;
192
+ }
193
+
194
+ // ── Ask / channel target resolution ─────────────────────────────────────
195
+
196
+ export function askGateway(text, agentId = 'main', opts = {}) {
197
+ return new Promise((resolve, reject) => {
198
+ if (!gwReady || !gw) return reject(new Error('Gateway not connected'));
199
+
200
+ const normalizedAgentId = normalizeOpenClawAgentId(agentId);
201
+ const sessionKey = String(opts.sessionKey || buildOpenClawSessionKey(normalizedAgentId)).trim();
202
+ const id = randomUUID();
203
+ const pending = {
204
+ id,
205
+ channelId: normalizedAgentId,
206
+ agentId: normalizedAgentId,
207
+ sessionKey,
208
+ runId: null,
209
+ resolve,
210
+ reject,
211
+ buffer: '',
212
+ mediaUrls: new Set(),
213
+ startedAt: Date.now(),
214
+ onEvent: typeof opts.onEvent === 'function' ? opts.onEvent : null,
215
+ };
216
+ console.log(`[gateway] send agent request id=${id.slice(0, 8)} ${describeGatewayRequestContext(pending)}`);
217
+ gw.send(JSON.stringify({
218
+ type: 'req', id, method: 'agent',
219
+ params: {
220
+ message: text,
221
+ agentId: normalizedAgentId,
222
+ sessionKey,
223
+ deliver: false,
224
+ idempotencyKey: randomUUID(),
225
+ },
226
+ }));
227
+
228
+ pendingRequests.set(id, pending);
229
+
230
+ const onMsg = (raw) => {
231
+ try {
232
+ const msg = JSON.parse(raw.toString());
233
+ if (msg.type === 'res' && msg.id === id) {
234
+ const req = pendingRequests.get(id);
235
+ if (req) req.runId = msg.result?.runId || msg.payload?.runId;
236
+ console.log(`[gateway] agent response id=${id.slice(0, 8)} ${describeGatewayRequestContext(req || pending)} ok=${msg.error ? 'false' : 'true'} error=${JSON.stringify(msg.error || '')}`);
237
+ gw.off('message', onMsg);
238
+ }
239
+ } catch { /* ignore */ }
240
+ };
241
+ gw.on('message', onMsg);
242
+
243
+ setTimeout(() => {
244
+ if (pendingRequests.has(id)) {
245
+ const req = pendingRequests.get(id);
246
+ pendingRequests.delete(id);
247
+ console.error(`[gateway] agent request timed out id=${id.slice(0, 8)} ${describeGatewayRequestContext(req || pending)} ageMs=${Date.now() - pending.startedAt}`);
248
+ const err = new Error('agent request timed out');
249
+ err.gatewayKind = 'timeout';
250
+ reject(err);
251
+ }
252
+ }, 5 * 60 * 1000);
253
+ });
254
+ }
255
+
256
+ // ── Registration (adapter setup + startup) ──────────────────────────────
257
+
258
+ /**
259
+ * Register an OpenClaw channel and connect the gateway if this is the
260
+ * first OpenClaw binding in the current process.
261
+ */
262
+ export function registerOpenClawChannel({ channelId, agentId }) {
263
+ if (!gatewayStarted) {
264
+ const normalizedAgentId = normalizeOpenClawAgentId(agentId || 'main');
265
+ console.log(`[gateway] registered OpenClaw binding ${channelId} agentId=${normalizedAgentId}, connecting gateway...`);
266
+ connectGateway();
267
+ gatewayStarted = true;
268
+ }
269
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * OpenClaw Gateway connection config and device identity.
3
+ *
4
+ * Reads the user's local OpenClaw install (`~/.openclaw/openclaw.json`,
5
+ * `~/.openclaw/identity/device.json`) to recover the gateway token,
6
+ * deviceId, and the keypair used to sign the connect challenge.
7
+ */
8
+
9
+ import { readFileSync } from 'node:fs';
10
+ import { homedir } from 'node:os';
11
+
12
+ export const GATEWAY_HOST = process.env.GATEWAY_HOST || '127.0.0.1';
13
+ export const GATEWAY_PORT = process.env.GATEWAY_PORT || 18789;
14
+
15
+ export function loadOpenClawConfig() {
16
+ try {
17
+ const configPath = `${homedir()}/.openclaw/openclaw.json`;
18
+ const config = JSON.parse(readFileSync(configPath, 'utf8'));
19
+ const identityPath = `${homedir()}/.openclaw/identity/device.json`;
20
+ const identity = JSON.parse(readFileSync(identityPath, 'utf8'));
21
+ return {
22
+ gatewayToken: config.gateway?.auth?.token || '',
23
+ deviceId: identity.deviceId,
24
+ publicKey: (() => {
25
+ const spki = identity.publicKeyPem?.match(/-----BEGIN PUBLIC KEY-----\n(.*?)\n-----END/s)?.[1]?.replace(/\n/g, '') || '';
26
+ const der = Buffer.from(spki, 'base64');
27
+ return der.length === 44 ? der.subarray(12).toString('base64url') : spki;
28
+ })(),
29
+ privateKeyPem: identity.privateKeyPem,
30
+ };
31
+ } catch {
32
+ return { gatewayToken: process.env.GATEWAY_TOKEN || '', deviceId: '', publicKey: '', privateKeyPem: '' };
33
+ }
34
+ }
@@ -0,0 +1,228 @@
1
+ import { normalizeOutboundMedia } from '../../core/media/outbound.mjs';
2
+ import { reportSubprocessFailure, sendAdapterMessage, sendResult, terminalRuntimeFailure } from '../../core/runtime-support.mjs';
3
+ import { GATEWAY_HOST, GATEWAY_PORT } from './identity.mjs';
4
+ import { buildOpenClawSessionKey, normalizeOpenClawAgentId, resolveOpenClawWorkspace } from './target.mjs';
5
+ import { askGateway, isGatewayReady, registerOpenClawChannel } from './gateway.mjs';
6
+ import { addInFlight, recoverInFlightEntries, removeInFlight } from './inflight.mjs';
7
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
8
+ import { extname } from 'node:path';
9
+ import { randomUUID } from 'node:crypto';
10
+
11
+ const EXT_TO_MIME = {
12
+ '.jpg': 'image/jpeg',
13
+ '.jpeg': 'image/jpeg',
14
+ '.png': 'image/png',
15
+ '.gif': 'image/gif',
16
+ '.webp': 'image/webp',
17
+ '.bmp': 'image/bmp',
18
+ '.tiff': 'image/tiff',
19
+ '.tif': 'image/tiff',
20
+ '.heic': 'image/heic',
21
+ '.heif': 'image/heif',
22
+ };
23
+
24
+ async function buildOpenClawImagePrompt(inbound) {
25
+ const mediaDir = '/tmp/openclaw/ticlawk-media';
26
+ mkdirSync(mediaDir, { recursive: true });
27
+ const successful = [];
28
+
29
+ for (const item of inbound.media || []) {
30
+ if (item.kind === 'local_path' && item.value && existsSync(item.value)) {
31
+ successful.push({
32
+ path: item.value,
33
+ mime: item.mime || EXT_TO_MIME[extname(item.value).toLowerCase()] || 'application/octet-stream',
34
+ source: item.value,
35
+ });
36
+ continue;
37
+ }
38
+ if (item.kind !== 'remote_url' || !item.value) continue;
39
+ const guessedExt = extname(item.value.split('?')[0] || '').toLowerCase() || '.jpg';
40
+ const localPath = `${mediaDir}/${randomUUID()}${guessedExt}`;
41
+ try {
42
+ const res = await fetch(item.value);
43
+ const buf = Buffer.from(await res.arrayBuffer());
44
+ writeFileSync(localPath, buf);
45
+ successful.push({
46
+ path: localPath,
47
+ mime: item.mime || EXT_TO_MIME[guessedExt] || 'application/octet-stream',
48
+ source: item.value,
49
+ });
50
+ } catch {}
51
+ }
52
+
53
+ const userText = (inbound.text || '').trim();
54
+ if (successful.length === 0) {
55
+ const fallbackList = (inbound.media || [])
56
+ .map((item) => `[media attached: ${item.value}]`)
57
+ .join('\n');
58
+ return [userText, fallbackList].filter(Boolean).join('\n\n').trim() || '(image attached)';
59
+ }
60
+
61
+ const lines = successful.map((entry, index) => {
62
+ const prefix = successful.length > 1
63
+ ? `[media attached ${index + 1}/${successful.length}: `
64
+ : '[media attached: ';
65
+ const mimePart = entry.mime ? ` (${entry.mime})` : '';
66
+ const sourcePart = entry.source ? ` | ${entry.source}` : '';
67
+ return `${prefix}${entry.path}${mimePart}${sourcePart}]`;
68
+ });
69
+
70
+ console.log(`[openclaw] inbound image prepared count=${successful.length} firstPath=${successful[0]?.path || 'none'}`);
71
+ return [userText, ...lines].filter(Boolean).join('\n\n').trim();
72
+ }
73
+
74
+ export const openClawRuntime = {
75
+ name: 'openclaw',
76
+
77
+ askGateway,
78
+
79
+ isReady() {
80
+ return isGatewayReady();
81
+ },
82
+
83
+ async resolveBinding(payload) {
84
+ if (!payload?.agentId) {
85
+ throw new Error('agentId is required for openclaw binding');
86
+ }
87
+ const agentId = normalizeOpenClawAgentId(payload.agentId);
88
+ const workspace = String(
89
+ payload.workdir
90
+ || payload.projectDir
91
+ || payload.cwd
92
+ || resolveOpenClawWorkspace(agentId)
93
+ || '',
94
+ ).trim();
95
+ const sessionKey = payload.sessionKey
96
+ ? String(payload.sessionKey).trim()
97
+ : buildOpenClawSessionKey(agentId);
98
+ return {
99
+ runtime: this.name,
100
+ displayName: payload.name || agentId,
101
+ runtimeMeta: {
102
+ agentId,
103
+ sessionKey,
104
+ gatewayHost: GATEWAY_HOST,
105
+ gatewayPort: GATEWAY_PORT,
106
+ ...(workspace ? {
107
+ workdir: workspace,
108
+ projectDir: workspace,
109
+ cwd: workspace,
110
+ } : {}),
111
+ },
112
+ };
113
+ },
114
+
115
+ onBindingUpdated(binding) {
116
+ if (binding.runtime !== this.name) return;
117
+ registerOpenClawChannel({
118
+ channelId: binding.id,
119
+ agentId: binding.runtimeMeta?.agentId || binding.id,
120
+ });
121
+ },
122
+
123
+ async deliverTurn(inbound, ctx) {
124
+ const binding = ctx.getBinding(inbound.bindingId);
125
+ if (!binding) return false;
126
+ const adapter = ctx.adapter;
127
+ const meta = binding.runtimeMeta || {};
128
+ const prompt = inbound.action === 'image'
129
+ ? await buildOpenClawImagePrompt(inbound)
130
+ : (inbound.text || '').trim();
131
+ const agentId = normalizeOpenClawAgentId(meta.agentId || binding.id);
132
+ const sessionId = String(meta.sessionKey || buildOpenClawSessionKey(agentId)).trim();
133
+
134
+ if (inbound.messageId) {
135
+ addInFlight({
136
+ messageId: inbound.messageId,
137
+ bindingId: binding.id,
138
+ sentAt: Date.now(),
139
+ });
140
+ }
141
+
142
+ try {
143
+ const result = await askGateway(prompt, agentId, {
144
+ sessionKey: sessionId,
145
+ onEvent: (event) => {
146
+ if (typeof adapter.emitEvent !== 'function') return;
147
+ void adapter.emitEvent(binding, {
148
+ agent: this.name,
149
+ sessionId,
150
+ cwd: '',
151
+ event,
152
+ });
153
+ },
154
+ });
155
+ await sendResult(adapter, binding, inbound, {
156
+ ...result,
157
+ media: normalizeOutboundMedia(result),
158
+ });
159
+ return true;
160
+ } catch (err) {
161
+ if (typeof adapter.emitEvent === 'function') {
162
+ await adapter.emitEvent(binding, {
163
+ agent: this.name,
164
+ sessionId,
165
+ cwd: '',
166
+ event: {
167
+ hook_event_name: 'agent.run.error',
168
+ error: err?.message || 'OpenClaw failed',
169
+ durationMs: 0,
170
+ phase: 'error',
171
+ reply_to_message_id: inbound.messageId || null,
172
+ turn_id: inbound.messageId || null,
173
+ },
174
+ }).catch(() => {});
175
+ }
176
+ await reportSubprocessFailure({
177
+ adapter,
178
+ binding,
179
+ inbound,
180
+ runtimeName: `OpenClaw ${binding.displayName || binding.id}`,
181
+ info: {
182
+ ok: false,
183
+ durationMs: 0,
184
+ kind: err.gatewayKind === 'timeout' ? 'gateway-timeout' : 'gateway-error',
185
+ errorMessage: err.message,
186
+ },
187
+ });
188
+ return terminalRuntimeFailure(err?.message || 'OpenClaw failed');
189
+ } finally {
190
+ if (inbound.messageId) {
191
+ removeInFlight(inbound.messageId);
192
+ }
193
+ }
194
+ },
195
+
196
+ async recoverInFlight(ctx) {
197
+ const entries = recoverInFlightEntries();
198
+ for (const entry of entries) {
199
+ const binding = ctx.getBinding(entry.bindingId);
200
+ if (!binding) continue;
201
+ await sendAdapterMessage(ctx.adapter, binding, {
202
+ type: 'assistant',
203
+ text: '⚠️ Lost while ticlawk restarted.\n\nThis OpenClaw message was in flight when ticlawk restarted. Please retry your message.',
204
+ media: [],
205
+ replyToMessageId: entry.messageId || null,
206
+ }).catch(() => {});
207
+ }
208
+ return entries.length;
209
+ },
210
+
211
+ async reconcileAfterRestart(binding, ctx) {
212
+ const meta = binding.runtimeMeta || {};
213
+ if (typeof ctx.adapter.emitEvent === 'function') {
214
+ await ctx.adapter.emitEvent(binding, {
215
+ agent: this.name,
216
+ sessionId: String(meta.sessionKey || buildOpenClawSessionKey(meta.agentId || binding.id)).trim(),
217
+ cwd: '',
218
+ event: {
219
+ hook_event_name: 'agent.run.end',
220
+ reason: 'connector.restart.reconcile',
221
+ },
222
+ });
223
+ }
224
+ return 1;
225
+ },
226
+ };
227
+
228
+ export default openClawRuntime;
@@ -0,0 +1,46 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { AF_HOME } from '../../core/config.mjs';
4
+
5
+ const IN_FLIGHT_PATH = join(AF_HOME, 'in-flight-openclaw.json');
6
+
7
+ function readEntries() {
8
+ if (!existsSync(IN_FLIGHT_PATH)) return [];
9
+ try {
10
+ const parsed = JSON.parse(readFileSync(IN_FLIGHT_PATH, 'utf8'));
11
+ return Array.isArray(parsed) ? parsed : [];
12
+ } catch {
13
+ return [];
14
+ }
15
+ }
16
+
17
+ function writeEntries(entries) {
18
+ try {
19
+ mkdirSync(dirname(IN_FLIGHT_PATH), { recursive: true });
20
+ writeFileSync(IN_FLIGHT_PATH, JSON.stringify(entries, null, 2));
21
+ } catch {}
22
+ }
23
+
24
+ export function addInFlight(entry) {
25
+ if (!entry?.messageId || !entry?.bindingId) return;
26
+ const entries = readEntries().filter((current) => current.messageId !== entry.messageId);
27
+ entries.push(entry);
28
+ writeEntries(entries);
29
+ }
30
+
31
+ export function removeInFlight(messageId) {
32
+ if (!messageId) return;
33
+ const entries = readEntries();
34
+ const filtered = entries.filter((entry) => entry.messageId !== messageId);
35
+ if (filtered.length !== entries.length) {
36
+ writeEntries(filtered);
37
+ }
38
+ }
39
+
40
+ export function recoverInFlightEntries() {
41
+ const entries = readEntries();
42
+ if (entries.length > 0) {
43
+ writeEntries([]);
44
+ }
45
+ return entries;
46
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * OpenClaw target helpers.
3
+ *
4
+ * Public and persisted state only carries `agentId`. The gateway-specific
5
+ * `agent:<id>:main` session key is derived internally when dispatching.
6
+ */
7
+
8
+ import { existsSync, readFileSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+
11
+ let cachedOpenClawConfig = null;
12
+
13
+ function loadOpenClawConfig() {
14
+ if (cachedOpenClawConfig !== null) return cachedOpenClawConfig;
15
+ const home = process.env.HOME || '';
16
+ const configPath = home ? join(home, '.openclaw', 'openclaw.json') : '';
17
+ if (!configPath || !existsSync(configPath)) {
18
+ cachedOpenClawConfig = {};
19
+ return cachedOpenClawConfig;
20
+ }
21
+ try {
22
+ cachedOpenClawConfig = JSON.parse(readFileSync(configPath, 'utf8')) || {};
23
+ } catch {
24
+ cachedOpenClawConfig = {};
25
+ }
26
+ return cachedOpenClawConfig;
27
+ }
28
+
29
+ export function normalizeOpenClawAgentId(value) {
30
+ const trimmed = String(value || '').trim().toLowerCase();
31
+ if (!trimmed) return 'main';
32
+ return trimmed.replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '') || 'main';
33
+ }
34
+
35
+ export function resolveOpenClawWorkspace(agentId) {
36
+ const normalizedId = normalizeOpenClawAgentId(agentId);
37
+ const config = loadOpenClawConfig();
38
+ const agents = Array.isArray(config?.agents?.list) ? config.agents.list : [];
39
+ const matched = agents.find((agent) => normalizeOpenClawAgentId(agent?.id || agent?.name || '') === normalizedId);
40
+ const workspace = String(matched?.workspace || '').trim();
41
+ return workspace || '';
42
+ }
43
+
44
+ export function buildOpenClawSessionKey(agentId = 'main') {
45
+ return `agent:${normalizeOpenClawAgentId(agentId)}:main`;
46
+ }
47
+
48
+ export function describeGatewayRequestContext(req = {}) {
49
+ const agentId = normalizeOpenClawAgentId(req.agentId || 'main');
50
+ const parts = [
51
+ `channelId=${req.channelId || 'unknown'}`,
52
+ `agentId=${agentId}`,
53
+ `sessionKey=${req.sessionKey || buildOpenClawSessionKey(agentId)}`,
54
+ ];
55
+ if (req.runId) parts.push(`runId=${req.runId}`);
56
+ return parts.join(' ');
57
+ }