ticlawk 0.1.16-dev.1 → 0.1.16-dev.11

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.
@@ -7,12 +7,11 @@
7
7
  * runtime owns the binary-level details.
8
8
  */
9
9
 
10
- import { existsSync } from 'node:fs';
11
10
  import { basename } from 'node:path';
12
11
  import { buildAgentRuntimeEnv } from '../../core/runtime-env.mjs';
13
12
  import { buildStandingPrompt } from '../_shared/standing-prompt.mjs';
13
+ import { ensureAgentHome } from '../../core/agent-home.mjs';
14
14
  import {
15
- createCCSession,
16
15
  getClaudeCodeRuntimeHealth,
17
16
  runCCPrompt,
18
17
  streamCCPrompt,
@@ -24,12 +23,9 @@ import {
24
23
  } from './session.mjs';
25
24
  import { discoverSessions } from './transcripts.mjs';
26
25
  import { buildImageMessageFromInbound } from '../../core/media/inbound.mjs';
27
- import { normalizeOutboundMedia } from '../../core/media/outbound.mjs';
28
26
  import { emitWorkerEvent } from '../../core/events/worker-events.mjs';
29
27
  import {
30
28
  shouldStreamRuntime,
31
- sendAdapterMessage,
32
- recordActivity,
33
29
  reportSubprocessFailure,
34
30
  terminalRuntimeFailure,
35
31
  updateBindingRuntimeMeta,
@@ -38,15 +34,9 @@ import {
38
34
  export const claudeCodeRuntime = {
39
35
  name: 'claude_code',
40
36
 
41
- // Start a new Claude session without blocking on local transcript
42
- // discovery. The live turn path trusts stdout/session_id; transcript
43
- // indexing is a separate concern.
44
- async createSession({ projectDir, text, claudePath }) {
45
- return createCCSession({ projectDir, message: text, claudePath });
46
- },
47
-
48
- // Run a Claude turn and wait for the final result on stdout. This is
49
- // the worker-first path used by the adapter for direct reply delivery.
37
+ // Run a Claude turn and wait for the final result on stdout. Used by
38
+ // deliverTurn when streaming is disabled; both fresh sessions
39
+ // (sessionId=null) and resumed sessions go through here.
50
40
  runTurn({ sessionId, projectDir, claudePath, agentEnv }, text, opts = {}) {
51
41
  return runCCPrompt({
52
42
  sessionId,
@@ -97,8 +87,6 @@ export const claudeCodeRuntime = {
97
87
  runtimeMeta: {
98
88
  sessionId: session.sessionId,
99
89
  project: session.project,
100
- workdir: session.projectDir,
101
- projectDir: session.projectDir,
102
90
  path: session.path,
103
91
  runtimePath: claudePath,
104
92
  claudePath,
@@ -108,21 +96,12 @@ export const claudeCodeRuntime = {
108
96
  };
109
97
  }
110
98
 
111
- if (!requestedProjectDir) {
112
- throw new Error('projectDir or sessionId is required for claude_code binding');
113
- }
114
- if (!existsSync(requestedProjectDir)) {
115
- throw new Error(`project dir not found locally: ${requestedProjectDir}`);
116
- }
117
-
118
99
  return {
119
100
  runtime: this.name,
120
- displayName: payload?.name || basename(requestedProjectDir) || 'Claude Code',
101
+ displayName: payload?.name || (requestedProjectDir ? basename(requestedProjectDir) : 'Claude Code'),
121
102
  runtimeMeta: {
122
103
  sessionId: null,
123
- project: basename(requestedProjectDir) || '',
124
- workdir: requestedProjectDir,
125
- projectDir: requestedProjectDir,
104
+ project: requestedProjectDir ? basename(requestedProjectDir) : '',
126
105
  path: null,
127
106
  runtimePath: claudePath,
128
107
  claudePath,
@@ -141,117 +120,37 @@ export const claudeCodeRuntime = {
141
120
  if (!binding) return false;
142
121
  const adapter = ctx.adapter;
143
122
  const meta = binding.runtimeMeta || {};
144
- const projectDir = meta.projectDir;
145
- const sessionId = meta.sessionId || binding.id;
123
+ // cwd is the per-agent home dir under ~/.ticlawk/agents/<id>/.
124
+ const projectDir = ensureAgentHome(binding.id, {
125
+ displayName: binding.display_name || binding.name || null,
126
+ });
146
127
  const runtimeClaudePath = meta.claudePath || meta.runtimePath || null;
147
128
 
148
- if (!projectDir || !existsSync(projectDir)) {
149
- await sendAdapterMessage(adapter, binding, {
150
- type: 'assistant',
151
- text: `⚠️ Claude Code project dir not found: ${projectDir || '(missing)'}`,
152
- media: [],
153
- replyToMessageId: inbound.messageId || null,
154
- });
155
- return true;
156
- }
157
-
158
129
  const message = inbound.action === 'image'
159
130
  ? await buildImageMessageFromInbound(inbound, 'claude-code')
160
131
  : inbound.text;
161
132
 
133
+ // shouldRotate=true means meta.sessionId is missing or invalidated.
134
+ // We pass sessionId=null so `claude` creates a fresh session; the new
135
+ // session_id is captured from stream events and persisted below.
136
+ // Unifying rotate + non-rotate into one path means the standing prompt
137
+ // is always attached, so the agent uses the CLI to reply on every
138
+ // turn — including the first.
162
139
  const shouldRotate = !meta.sessionId || meta.rotatePending;
163
- if (shouldRotate) {
164
- await emitWorkerEvent({
165
- adapter,
166
- binding,
167
- agent: this.name,
168
- sessionId: sessionId || binding.id,
169
- cwd: projectDir,
170
- replyToMessageId: inbound.messageId || null,
171
- event: {
172
- hook_event_name: 'worker.turn.start',
173
- worker_event_name: 'worker.turn.start',
174
- },
175
- logger: ctx.logger,
176
- });
177
- try {
178
- const claudePath = requireClaudePath(runtimeClaudePath);
179
- const claudeVersion = getClaudeCodeRuntimeHealth(claudePath).version || meta.claudeVersion || null;
180
- const created = await this.createSession({ projectDir, text: message, claudePath });
181
- const nextBinding = await updateBindingRuntimeMeta(ctx, binding, {
182
- sessionId: created.sessionId,
183
- projectDir,
184
- path: null,
185
- runtimePath: claudePath,
186
- claudePath,
187
- claudeVersion,
188
- rotatePending: false,
189
- lastRotatedAt: new Date().toISOString(),
190
- }, { status: 'connected' });
191
- if (created.resultText && created.resultText.trim()) {
192
- await sendAdapterMessage(adapter, nextBinding, {
193
- type: 'assistant',
194
- text: created.resultText,
195
- media: [],
196
- replyToMessageId: inbound.messageId || null,
197
- });
198
- }
199
- await emitWorkerEvent({
200
- adapter,
201
- binding: nextBinding,
202
- agent: this.name,
203
- sessionId: created.sessionId,
204
- cwd: projectDir,
205
- replyToMessageId: inbound.messageId || null,
206
- event: {
207
- hook_event_name: 'Stop',
208
- worker_event_name: 'worker.turn.complete',
209
- },
210
- logger: ctx.logger,
211
- });
212
- return true;
213
- } catch (err) {
214
- await emitWorkerEvent({
215
- adapter,
216
- binding,
217
- agent: this.name,
218
- sessionId: sessionId || binding.id,
219
- cwd: projectDir,
220
- replyToMessageId: inbound.messageId || null,
221
- event: {
222
- hook_event_name: 'worker.turn.error',
223
- worker_event_name: 'worker.turn.error',
224
- error: err?.message || 'Claude Code failed',
225
- },
226
- logger: ctx.logger,
227
- });
228
- await reportSubprocessFailure({
229
- adapter,
230
- binding,
231
- inbound,
232
- runtimeName: 'Claude Code',
233
- info: err?.info || {
234
- ok: false,
235
- kind: 'exit-error',
236
- errorMessage: err?.message || 'Claude Code failed',
237
- durationMs: 0,
238
- },
239
- });
240
- return terminalRuntimeFailure(err?.message || 'Claude Code failed');
241
- }
242
- }
140
+ const targetSessionId = shouldRotate ? null : meta.sessionId;
141
+ const errEventSessionId = meta.sessionId || binding.id;
243
142
 
244
143
  try {
245
144
  const claudePath = requireClaudePath(runtimeClaudePath);
246
145
  const claudeVersion = getClaudeCodeRuntimeHealth(claudePath).version || meta.claudeVersion || null;
247
146
  const agentEnv = buildAgentRuntimeEnv({
248
147
  agentId: binding.id,
249
- sessionId,
148
+ sessionId: meta.sessionId,
250
149
  hostId: binding.runtime_host_id,
251
150
  });
252
151
  const appendSystemPrompt = buildStandingPrompt({ agentId: binding.id });
253
152
  const result = shouldStreamRuntime(this.name, this)
254
- ? await this.runTurnStream({ sessionId, projectDir, claudePath, agentEnv }, message, {
153
+ ? await this.runTurnStream({ sessionId: targetSessionId, projectDir, claudePath, agentEnv }, message, {
255
154
  appendSystemPrompt,
256
155
  onEvent: async (event) => {
257
156
  if (event?.type === 'turn.started') {
@@ -259,7 +158,7 @@ export const claudeCodeRuntime = {
259
158
  adapter,
260
159
  binding,
261
160
  agent: this.name,
262
- sessionId: event.sessionId || sessionId,
161
+ sessionId: event.sessionId || targetSessionId || binding.id,
263
162
  cwd: projectDir,
264
163
  replyToMessageId: inbound.messageId || null,
265
164
  event: {
@@ -273,7 +172,7 @@ export const claudeCodeRuntime = {
273
172
  adapter,
274
173
  binding,
275
174
  agent: this.name,
276
- sessionId: event.sessionId || sessionId,
175
+ sessionId: event.sessionId || targetSessionId || binding.id,
277
176
  cwd: projectDir,
278
177
  replyToMessageId: inbound.messageId || null,
279
178
  event: {
@@ -286,22 +185,20 @@ export const claudeCodeRuntime = {
286
185
  }
287
186
  },
288
187
  })
289
- : await this.runTurn({ sessionId, projectDir, claudePath, agentEnv }, message, { appendSystemPrompt });
188
+ : await this.runTurn({ sessionId: targetSessionId, projectDir, claudePath, agentEnv }, message, { appendSystemPrompt });
290
189
  const nextBinding = await updateBindingRuntimeMeta(ctx, binding, {
291
190
  sessionId: result?.sessionId || meta.sessionId,
292
191
  runtimePath: claudePath,
293
192
  claudePath,
294
193
  claudeVersion,
194
+ rotatePending: false,
195
+ lastRotatedAt: shouldRotate ? new Date().toISOString() : meta.lastRotatedAt,
295
196
  }, { status: 'connected' });
296
- await recordActivity(adapter, nextBinding, inbound, {
297
- ...result,
298
- media: normalizeOutboundMedia(result),
299
- });
300
197
  await emitWorkerEvent({
301
198
  adapter,
302
199
  binding: nextBinding,
303
200
  agent: this.name,
304
- sessionId: result?.sessionId || sessionId,
201
+ sessionId: result?.sessionId || targetSessionId || binding.id,
305
202
  cwd: projectDir,
306
203
  replyToMessageId: inbound.messageId || null,
307
204
  event: {
@@ -316,7 +213,7 @@ export const claudeCodeRuntime = {
316
213
  adapter,
317
214
  binding,
318
215
  agent: this.name,
319
- sessionId,
216
+ sessionId: errEventSessionId,
320
217
  cwd: projectDir,
321
218
  replyToMessageId: inbound.messageId || null,
322
219
  event: {
@@ -349,7 +246,7 @@ export const claudeCodeRuntime = {
349
246
  binding,
350
247
  agent: this.name,
351
248
  sessionId: meta.sessionId || binding.id,
352
- cwd: meta.projectDir || '',
249
+ cwd: ensureAgentHome(binding.id) || '',
353
250
  event: {
354
251
  hook_event_name: 'Stop',
355
252
  worker_event_name: 'worker.turn.complete',
@@ -5,7 +5,6 @@
5
5
  * and discover local sessions.
6
6
  */
7
7
 
8
- import { existsSync } from 'node:fs';
9
8
  import { basename } from 'node:path';
10
9
  import {
11
10
  createCodexSession,
@@ -19,13 +18,10 @@ import {
19
18
  requireCodexPath,
20
19
  } from './session.mjs';
21
20
  import { buildCodexInputFromInbound } from '../../core/media/inbound.mjs';
22
- import { normalizeOutboundMedia } from '../../core/media/outbound.mjs';
23
21
  import { emitWorkerEvent } from '../../core/events/worker-events.mjs';
24
22
  import {
25
23
  shouldStreamRuntime,
26
24
  createDeltaAggregator,
27
- sendAdapterMessage,
28
- recordActivity,
29
25
  reportSubprocessFailure,
30
26
  terminalRuntimeFailure,
31
27
  updateBindingRuntimeMeta,
@@ -33,6 +29,7 @@ import {
33
29
  } from '../../core/runtime-support.mjs';
34
30
  import { buildAgentRuntimeEnv } from '../../core/runtime-env.mjs';
35
31
  import { buildStandingPrompt } from '../_shared/standing-prompt.mjs';
32
+ import { ensureAgentHome } from '../../core/agent-home.mjs';
36
33
 
37
34
  export const codexRuntime = {
38
35
  name: 'codex',
@@ -98,8 +95,6 @@ export const codexRuntime = {
98
95
  displayName: payload.name || basename(session.cwd) || 'Codex',
99
96
  runtimeMeta: {
100
97
  sessionId: session.sessionId,
101
- workdir: session.cwd,
102
- cwd: session.cwd,
103
98
  path: session.path || null,
104
99
  runtimePath: codexPath,
105
100
  codexPath,
@@ -109,20 +104,11 @@ export const codexRuntime = {
109
104
  };
110
105
  }
111
106
 
112
- if (!requestedCwd) {
113
- throw new Error('cwd or sessionId is required for codex binding');
114
- }
115
- if (!existsSync(requestedCwd)) {
116
- throw new Error(`codex cwd not found locally: ${requestedCwd}`);
117
- }
118
-
119
107
  return {
120
108
  runtime: this.name,
121
- displayName: payload.name || basename(requestedCwd) || 'Codex',
109
+ displayName: payload.name || (requestedCwd ? basename(requestedCwd) : 'Codex'),
122
110
  runtimeMeta: {
123
111
  sessionId: null,
124
- workdir: requestedCwd,
125
- cwd: requestedCwd,
126
112
  path: null,
127
113
  runtimePath: codexPath,
128
114
  codexPath,
@@ -141,16 +127,11 @@ export const codexRuntime = {
141
127
  if (!binding) return false;
142
128
  const adapter = ctx.adapter;
143
129
  const meta = binding.runtimeMeta || {};
144
-
145
- if (!meta.cwd || !existsSync(meta.cwd)) {
146
- await sendAdapterMessage(adapter, binding, {
147
- type: 'assistant',
148
- text: `⚠️ Codex cwd not found: ${meta.cwd || '(missing)'}`,
149
- media: [],
150
- replyToMessageId: inbound.messageId || null,
151
- });
152
- return true;
153
- }
130
+ // cwd is the per-agent home dir under ~/.ticlawk/agents/<id>/.
131
+ // ensureAgentHome creates it + seeds MEMORY.md if missing.
132
+ const agentHome = ensureAgentHome(binding.id, {
133
+ displayName: binding.display_name || binding.name || null,
134
+ });
154
135
 
155
136
  const codexInput = inbound.action === 'image'
156
137
  ? await buildCodexInputFromInbound(inbound, 'codex')
@@ -168,7 +149,7 @@ export const codexRuntime = {
168
149
  agent: this.name,
169
150
  sessionId: sessionId || meta.sessionId || binding.id,
170
151
  turnId: turnId || null,
171
- cwd: cwd || meta.cwd,
152
+ cwd: cwd || agentHome,
172
153
  replyToMessageId: inbound.messageId || null,
173
154
  event: {
174
155
  hook_event_name: 'worker.message.delta',
@@ -189,7 +170,7 @@ export const codexRuntime = {
189
170
  });
190
171
  const developerInstructions = buildStandingPrompt({ agentId: binding.id });
191
172
  const result = (shouldRotate || inbound.action === 'image' || shouldStreamRuntime(this.name, this))
192
- ? await this.runTurnStream({ sessionId: shouldRotate ? null : meta.sessionId, cwd: meta.cwd, codexPath: runtimeCodexPath, agentEnv }, message, {
173
+ ? await this.runTurnStream({ sessionId: shouldRotate ? null : meta.sessionId, cwd: agentHome, codexPath: runtimeCodexPath, agentEnv }, message, {
193
174
  input: codexInput,
194
175
  developerInstructions,
195
176
  onEvent: async (event) => {
@@ -200,7 +181,7 @@ export const codexRuntime = {
200
181
  agent: this.name,
201
182
  sessionId: event.sessionId || meta.sessionId || binding.id,
202
183
  turnId: event.turnId || null,
203
- cwd: meta.cwd,
184
+ cwd: agentHome,
204
185
  replyToMessageId: inbound.messageId || null,
205
186
  event: {
206
187
  hook_event_name: 'worker.turn.start',
@@ -212,19 +193,18 @@ export const codexRuntime = {
212
193
  deltaAggregator.push(event.text, {
213
194
  sessionId: event.sessionId || meta.sessionId || binding.id,
214
195
  turnId: event.turnId || null,
215
- cwd: meta.cwd,
196
+ cwd: agentHome,
216
197
  });
217
198
  }
218
199
  },
219
200
  })
220
- : await this.runTurn({ sessionId: meta.sessionId, cwd: meta.cwd, codexPath: runtimeCodexPath, agentEnv }, message, {
201
+ : await this.runTurn({ sessionId: meta.sessionId, cwd: agentHome, codexPath: runtimeCodexPath, agentEnv }, message, {
221
202
  input: codexInput,
222
203
  });
223
204
 
224
205
  await deltaAggregator.flush();
225
206
  const nextBinding = await updateBindingRuntimeMeta(ctx, binding, {
226
207
  sessionId: result?.sessionId || meta.sessionId,
227
- cwd: result?.cwd || meta.cwd,
228
208
  path: result?.path || meta.path || null,
229
209
  runtimePath: runtimeCodexPath,
230
210
  codexPath: runtimeCodexPath,
@@ -232,17 +212,13 @@ export const codexRuntime = {
232
212
  rotatePending: false,
233
213
  lastRotatedAt: shouldRotate ? new Date().toISOString() : meta.lastRotatedAt,
234
214
  }, { status: 'connected' });
235
- await recordActivity(adapter, nextBinding, inbound, {
236
- ...result,
237
- media: normalizeOutboundMedia(result),
238
- });
239
215
  await emitWorkerEvent({
240
216
  adapter,
241
217
  binding: nextBinding,
242
218
  agent: this.name,
243
219
  sessionId: result?.sessionId || meta.sessionId || binding.id,
244
220
  turnId: result?.turnId || null,
245
- cwd: result?.cwd || meta.cwd,
221
+ cwd: result?.cwd || agentHome,
246
222
  replyToMessageId: inbound.messageId || null,
247
223
  event: {
248
224
  hook_event_name: 'Stop',
@@ -273,7 +249,7 @@ export const codexRuntime = {
273
249
  agent: this.name,
274
250
  sessionId: meta.sessionId || binding.id,
275
251
  turnId: failureInfo?.turnId || null,
276
- cwd: meta.cwd,
252
+ cwd: agentHome,
277
253
  replyToMessageId: inbound.messageId || null,
278
254
  event: {
279
255
  hook_event_name: 'worker.turn.error',
@@ -300,7 +276,7 @@ export const codexRuntime = {
300
276
  binding,
301
277
  agent: this.name,
302
278
  sessionId: meta.sessionId || binding.id,
303
- cwd: meta.cwd || '',
279
+ cwd: ensureAgentHome(binding.id) || '',
304
280
  event: {
305
281
  hook_event_name: 'Stop',
306
282
  worker_event_name: 'worker.turn.complete',
@@ -1,8 +1,27 @@
1
- import { normalizeOutboundMedia } from '../../core/media/outbound.mjs';
2
- import { reportSubprocessFailure, sendAdapterMessage, recordActivity, terminalRuntimeFailure } from '../../core/runtime-support.mjs';
1
+ import { reportSubprocessFailure, terminalRuntimeFailure } from '../../core/runtime-support.mjs';
3
2
  import { buildStandingPrompt } from '../_shared/standing-prompt.mjs';
4
3
  import { GATEWAY_HOST, GATEWAY_PORT } from './identity.mjs';
5
- import { buildOpenClawSessionKey, normalizeOpenClawAgentId, resolveOpenClawWorkspace } from './target.mjs';
4
+
5
+ // Cheap availability probe used by the harness picker. We can't use
6
+ // isGatewayReady() here because the daemon only opens the gateway WS
7
+ // after at least one openclaw binding exists (registerOpenClawChannel),
8
+ // so an empty install would always look "unavailable" and you'd never
9
+ // be able to pick it for the first time. Probing the gateway's plain
10
+ // HTTP /health avoids that chicken-and-egg.
11
+ const OPENCLAW_HEALTH_TIMEOUT_MS = 1500;
12
+ async function probeOpenClawGatewayHealth() {
13
+ try {
14
+ const res = await fetch(`http://${GATEWAY_HOST}:${GATEWAY_PORT}/health`, {
15
+ signal: AbortSignal.timeout(OPENCLAW_HEALTH_TIMEOUT_MS),
16
+ });
17
+ if (!res.ok) return false;
18
+ const body = await res.json().catch(() => null);
19
+ return body?.ok === true;
20
+ } catch {
21
+ return false;
22
+ }
23
+ }
24
+ import { buildOpenClawSessionKey, normalizeOpenClawAgentId } from './target.mjs';
6
25
  import { askGateway, isGatewayReady, registerOpenClawChannel } from './gateway.mjs';
7
26
 
8
27
  // Tracks which (agentId, sessionKey) pairs already saw the standing
@@ -87,18 +106,24 @@ export const openClawRuntime = {
87
106
  return isGatewayReady();
88
107
  },
89
108
 
109
+ // Health probe for the harness picker. openclaw is gateway-based —
110
+ // "available" means the local openclaw gateway is up and responds to
111
+ // an unauthenticated /health request. Doesn't depend on the daemon
112
+ // having an open WS, so the picker can offer openclaw on a fresh
113
+ // install where no binding exists yet.
114
+ async health() {
115
+ return {
116
+ available: await probeOpenClawGatewayHealth(),
117
+ path: null,
118
+ version: null,
119
+ };
120
+ },
121
+
90
122
  async resolveBinding(payload) {
91
123
  if (!payload?.agentId) {
92
124
  throw new Error('agentId is required for openclaw binding');
93
125
  }
94
126
  const agentId = normalizeOpenClawAgentId(payload.agentId);
95
- const workspace = String(
96
- payload.workdir
97
- || payload.projectDir
98
- || payload.cwd
99
- || resolveOpenClawWorkspace(agentId)
100
- || '',
101
- ).trim();
102
127
  const sessionKey = payload.sessionKey
103
128
  ? String(payload.sessionKey).trim()
104
129
  : buildOpenClawSessionKey(agentId);
@@ -110,11 +135,6 @@ export const openClawRuntime = {
110
135
  sessionKey,
111
136
  gatewayHost: GATEWAY_HOST,
112
137
  gatewayPort: GATEWAY_PORT,
113
- ...(workspace ? {
114
- workdir: workspace,
115
- projectDir: workspace,
116
- cwd: workspace,
117
- } : {}),
118
138
  },
119
139
  };
120
140
  },
@@ -171,10 +191,6 @@ export const openClawRuntime = {
171
191
  });
172
192
  },
173
193
  });
174
- await recordActivity(adapter, binding, inbound, {
175
- ...result,
176
- media: normalizeOutboundMedia(result),
177
- });
178
194
  return true;
179
195
  } catch (err) {
180
196
  if (typeof adapter.emitEvent === 'function') {
@@ -212,18 +228,11 @@ export const openClawRuntime = {
212
228
  }
213
229
  },
214
230
 
215
- async recoverInFlight(ctx) {
231
+ async recoverInFlight() {
232
+ // Just drain the persisted in-flight set — the user-facing notice
233
+ // that used to surface here went through the dead chat-projection
234
+ // path. OpenClaw is non-primary; this no-ops cleanly for now.
216
235
  const entries = recoverInFlightEntries();
217
- for (const entry of entries) {
218
- const binding = ctx.getBinding(entry.bindingId);
219
- if (!binding) continue;
220
- await sendAdapterMessage(ctx.adapter, binding, {
221
- type: 'assistant',
222
- text: '⚠️ Lost while ticlawk restarted.\n\nThis OpenClaw message was in flight when ticlawk restarted. Please retry your message.',
223
- media: [],
224
- replyToMessageId: entry.messageId || null,
225
- }).catch(() => {});
226
- }
227
236
  return entries.length;
228
237
  },
229
238
 
@@ -5,42 +5,12 @@
5
5
  * `agent:<id>:main` session key is derived internally when dispatching.
6
6
  */
7
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
8
  export function normalizeOpenClawAgentId(value) {
30
9
  const trimmed = String(value || '').trim().toLowerCase();
31
10
  if (!trimmed) return 'main';
32
11
  return trimmed.replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '') || 'main';
33
12
  }
34
13
 
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
14
  export function buildOpenClawSessionKey(agentId = 'main') {
45
15
  return `agent:${normalizeOpenClawAgentId(agentId)}:main`;
46
16
  }