ticlawk 0.1.16-dev.1 → 0.1.16-dev.3
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
|
@@ -2,7 +2,27 @@ import { normalizeOutboundMedia } from '../../core/media/outbound.mjs';
|
|
|
2
2
|
import { reportSubprocessFailure, sendAdapterMessage, recordActivity, terminalRuntimeFailure } from '../../core/runtime-support.mjs';
|
|
3
3
|
import { buildStandingPrompt } from '../_shared/standing-prompt.mjs';
|
|
4
4
|
import { GATEWAY_HOST, GATEWAY_PORT } from './identity.mjs';
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
// Cheap availability probe used by the harness picker. We can't use
|
|
7
|
+
// isGatewayReady() here because the daemon only opens the gateway WS
|
|
8
|
+
// after at least one openclaw binding exists (registerOpenClawChannel),
|
|
9
|
+
// so an empty install would always look "unavailable" and you'd never
|
|
10
|
+
// be able to pick it for the first time. Probing the gateway's plain
|
|
11
|
+
// HTTP /health avoids that chicken-and-egg.
|
|
12
|
+
const OPENCLAW_HEALTH_TIMEOUT_MS = 1500;
|
|
13
|
+
async function probeOpenClawGatewayHealth() {
|
|
14
|
+
try {
|
|
15
|
+
const res = await fetch(`http://${GATEWAY_HOST}:${GATEWAY_PORT}/health`, {
|
|
16
|
+
signal: AbortSignal.timeout(OPENCLAW_HEALTH_TIMEOUT_MS),
|
|
17
|
+
});
|
|
18
|
+
if (!res.ok) return false;
|
|
19
|
+
const body = await res.json().catch(() => null);
|
|
20
|
+
return body?.ok === true;
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
import { buildOpenClawSessionKey, normalizeOpenClawAgentId } from './target.mjs';
|
|
6
26
|
import { askGateway, isGatewayReady, registerOpenClawChannel } from './gateway.mjs';
|
|
7
27
|
|
|
8
28
|
// Tracks which (agentId, sessionKey) pairs already saw the standing
|
|
@@ -87,18 +107,24 @@ export const openClawRuntime = {
|
|
|
87
107
|
return isGatewayReady();
|
|
88
108
|
},
|
|
89
109
|
|
|
110
|
+
// Health probe for the harness picker. openclaw is gateway-based —
|
|
111
|
+
// "available" means the local openclaw gateway is up and responds to
|
|
112
|
+
// an unauthenticated /health request. Doesn't depend on the daemon
|
|
113
|
+
// having an open WS, so the picker can offer openclaw on a fresh
|
|
114
|
+
// install where no binding exists yet.
|
|
115
|
+
async health() {
|
|
116
|
+
return {
|
|
117
|
+
available: await probeOpenClawGatewayHealth(),
|
|
118
|
+
path: null,
|
|
119
|
+
version: null,
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
|
|
90
123
|
async resolveBinding(payload) {
|
|
91
124
|
if (!payload?.agentId) {
|
|
92
125
|
throw new Error('agentId is required for openclaw binding');
|
|
93
126
|
}
|
|
94
127
|
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
128
|
const sessionKey = payload.sessionKey
|
|
103
129
|
? String(payload.sessionKey).trim()
|
|
104
130
|
: buildOpenClawSessionKey(agentId);
|
|
@@ -110,11 +136,6 @@ export const openClawRuntime = {
|
|
|
110
136
|
sessionKey,
|
|
111
137
|
gatewayHost: GATEWAY_HOST,
|
|
112
138
|
gatewayPort: GATEWAY_PORT,
|
|
113
|
-
...(workspace ? {
|
|
114
|
-
workdir: workspace,
|
|
115
|
-
projectDir: workspace,
|
|
116
|
-
cwd: workspace,
|
|
117
|
-
} : {}),
|
|
118
139
|
},
|
|
119
140
|
};
|
|
120
141
|
},
|
|
@@ -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
|
}
|
|
@@ -11,6 +11,7 @@ import { existsSync } from 'node:fs';
|
|
|
11
11
|
import { basename } from 'node:path';
|
|
12
12
|
import { buildAgentRuntimeEnv } from '../../core/runtime-env.mjs';
|
|
13
13
|
import { buildStandingPrompt } from '../_shared/standing-prompt.mjs';
|
|
14
|
+
import { ensureAgentHome } from '../../core/agent-home.mjs';
|
|
14
15
|
import {
|
|
15
16
|
createOpenCodeSession,
|
|
16
17
|
getOpenCodeRuntimeHealth,
|
|
@@ -97,8 +98,6 @@ export const openCodeRuntime = {
|
|
|
97
98
|
displayName: payload.name || session.title || basename(requestedCwd) || 'opencode',
|
|
98
99
|
runtimeMeta: {
|
|
99
100
|
sessionId: session.sessionId,
|
|
100
|
-
workdir: requestedCwd,
|
|
101
|
-
cwd: requestedCwd,
|
|
102
101
|
runtimePath: opencodePath,
|
|
103
102
|
opencodePath,
|
|
104
103
|
opencodeVersion,
|
|
@@ -107,20 +106,11 @@ export const openCodeRuntime = {
|
|
|
107
106
|
};
|
|
108
107
|
}
|
|
109
108
|
|
|
110
|
-
if (!requestedCwd) {
|
|
111
|
-
throw new Error('cwd or sessionId is required for opencode binding');
|
|
112
|
-
}
|
|
113
|
-
if (!existsSync(requestedCwd)) {
|
|
114
|
-
throw new Error(`opencode cwd not found locally: ${requestedCwd}`);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
109
|
return {
|
|
118
110
|
runtime: this.name,
|
|
119
|
-
displayName: payload.name || basename(requestedCwd)
|
|
111
|
+
displayName: payload.name || (requestedCwd ? basename(requestedCwd) : 'opencode'),
|
|
120
112
|
runtimeMeta: {
|
|
121
113
|
sessionId: null,
|
|
122
|
-
workdir: requestedCwd,
|
|
123
|
-
cwd: requestedCwd,
|
|
124
114
|
runtimePath: opencodePath,
|
|
125
115
|
opencodePath,
|
|
126
116
|
opencodeVersion,
|
|
@@ -139,16 +129,9 @@ export const openCodeRuntime = {
|
|
|
139
129
|
const adapter = ctx.adapter;
|
|
140
130
|
const meta = binding.runtimeMeta || {};
|
|
141
131
|
const runtimeOpenCodePath = meta.opencodePath || meta.runtimePath || null;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
type: 'assistant',
|
|
146
|
-
text: `⚠️ opencode cwd not found: ${meta.cwd || '(missing)'}`,
|
|
147
|
-
media: [],
|
|
148
|
-
replyToMessageId: inbound.messageId || null,
|
|
149
|
-
});
|
|
150
|
-
return true;
|
|
151
|
-
}
|
|
132
|
+
const agentHome = ensureAgentHome(binding.id, {
|
|
133
|
+
displayName: binding.display_name || binding.name || null,
|
|
134
|
+
});
|
|
152
135
|
|
|
153
136
|
// For image inbound, resolve the attached media to local file paths
|
|
154
137
|
// and forward them to opencode via `--file` (mirrors how Codex uses
|
|
@@ -195,7 +178,7 @@ export const openCodeRuntime = {
|
|
|
195
178
|
binding,
|
|
196
179
|
agent: this.name,
|
|
197
180
|
sessionId: sessionId || meta.sessionId || binding.id,
|
|
198
|
-
cwd: cwd ||
|
|
181
|
+
cwd: cwd || agentHome,
|
|
199
182
|
replyToMessageId: inbound.messageId || null,
|
|
200
183
|
event: {
|
|
201
184
|
hook_event_name: 'worker.message.delta',
|
|
@@ -217,7 +200,7 @@ export const openCodeRuntime = {
|
|
|
217
200
|
});
|
|
218
201
|
const standingPrompt = buildStandingPrompt({ agentId: binding.id });
|
|
219
202
|
const result = shouldStreamRuntime(this.name, this)
|
|
220
|
-
? await this.runTurnStream({ sessionId: shouldRotate ? null : meta.sessionId, cwd:
|
|
203
|
+
? await this.runTurnStream({ sessionId: shouldRotate ? null : meta.sessionId, cwd: agentHome, opencodePath, agentEnv }, message, {
|
|
221
204
|
standingPrompt,
|
|
222
205
|
files,
|
|
223
206
|
onEvent: async (event) => {
|
|
@@ -227,7 +210,7 @@ export const openCodeRuntime = {
|
|
|
227
210
|
binding,
|
|
228
211
|
agent: this.name,
|
|
229
212
|
sessionId: event.sessionId || meta.sessionId || binding.id,
|
|
230
|
-
cwd:
|
|
213
|
+
cwd: agentHome,
|
|
231
214
|
replyToMessageId: inbound.messageId || null,
|
|
232
215
|
event: {
|
|
233
216
|
hook_event_name: 'worker.turn.start',
|
|
@@ -238,18 +221,17 @@ export const openCodeRuntime = {
|
|
|
238
221
|
} else if (event?.type === 'message.delta' && event.text) {
|
|
239
222
|
deltaAggregator.push(event.text, {
|
|
240
223
|
sessionId: event.sessionId || meta.sessionId || binding.id,
|
|
241
|
-
cwd:
|
|
224
|
+
cwd: agentHome,
|
|
242
225
|
});
|
|
243
226
|
}
|
|
244
227
|
},
|
|
245
228
|
})
|
|
246
|
-
: await this.runTurn({ sessionId: shouldRotate ? null : meta.sessionId, cwd:
|
|
229
|
+
: await this.runTurn({ sessionId: shouldRotate ? null : meta.sessionId, cwd: agentHome, opencodePath, agentEnv }, message, { files, standingPrompt });
|
|
247
230
|
|
|
248
231
|
await deltaAggregator.flush();
|
|
249
232
|
|
|
250
233
|
const nextBinding = await updateBindingRuntimeMeta(ctx, binding, {
|
|
251
234
|
sessionId: result?.sessionId || meta.sessionId,
|
|
252
|
-
cwd: result?.cwd || meta.cwd,
|
|
253
235
|
runtimePath: opencodePath,
|
|
254
236
|
opencodePath,
|
|
255
237
|
opencodeVersion,
|
|
@@ -265,7 +247,7 @@ export const openCodeRuntime = {
|
|
|
265
247
|
binding: nextBinding,
|
|
266
248
|
agent: this.name,
|
|
267
249
|
sessionId: result?.sessionId || meta.sessionId || binding.id,
|
|
268
|
-
cwd: result?.cwd ||
|
|
250
|
+
cwd: result?.cwd || agentHome,
|
|
269
251
|
replyToMessageId: inbound.messageId || null,
|
|
270
252
|
event: {
|
|
271
253
|
hook_event_name: 'Stop',
|
|
@@ -281,7 +263,7 @@ export const openCodeRuntime = {
|
|
|
281
263
|
binding,
|
|
282
264
|
agent: this.name,
|
|
283
265
|
sessionId: meta.sessionId || binding.id,
|
|
284
|
-
cwd:
|
|
266
|
+
cwd: agentHome,
|
|
285
267
|
replyToMessageId: inbound.messageId || null,
|
|
286
268
|
event: {
|
|
287
269
|
hook_event_name: 'worker.turn.error',
|
|
@@ -313,7 +295,7 @@ export const openCodeRuntime = {
|
|
|
313
295
|
binding,
|
|
314
296
|
agent: this.name,
|
|
315
297
|
sessionId: meta.sessionId || binding.id,
|
|
316
|
-
cwd:
|
|
298
|
+
cwd: ensureAgentHome(binding.id) || '',
|
|
317
299
|
event: {
|
|
318
300
|
hook_event_name: 'Stop',
|
|
319
301
|
worker_event_name: 'worker.turn.complete',
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
* Wraps the pi CLI RPC mode and exposes the ticlawk runtime contract.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { existsSync } from 'node:fs';
|
|
8
7
|
import { basename } from 'node:path';
|
|
9
8
|
import { buildAgentRuntimeEnv } from '../../core/runtime-env.mjs';
|
|
10
9
|
import { buildStandingPrompt } from '../_shared/standing-prompt.mjs';
|
|
10
|
+
import { ensureAgentHome } from '../../core/agent-home.mjs';
|
|
11
11
|
import {
|
|
12
12
|
buildPiImagesFromInbound,
|
|
13
13
|
discoverPiSessions,
|
|
@@ -80,8 +80,6 @@ export const piRuntime = {
|
|
|
80
80
|
displayName: payload.name || basename(session.cwd || requestedCwd) || 'pi',
|
|
81
81
|
runtimeMeta: {
|
|
82
82
|
sessionId: session.sessionId,
|
|
83
|
-
workdir: session.cwd || requestedCwd,
|
|
84
|
-
cwd: session.cwd || requestedCwd,
|
|
85
83
|
path: session.path || null,
|
|
86
84
|
runtimePath: piPath,
|
|
87
85
|
piPath,
|
|
@@ -91,20 +89,11 @@ export const piRuntime = {
|
|
|
91
89
|
};
|
|
92
90
|
}
|
|
93
91
|
|
|
94
|
-
if (!requestedCwd) {
|
|
95
|
-
throw new Error('cwd or sessionId is required for pi binding');
|
|
96
|
-
}
|
|
97
|
-
if (!existsSync(requestedCwd)) {
|
|
98
|
-
throw new Error(`pi cwd not found locally: ${requestedCwd}`);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
92
|
return {
|
|
102
93
|
runtime: this.name,
|
|
103
|
-
displayName: payload.name || basename(requestedCwd)
|
|
94
|
+
displayName: payload.name || (requestedCwd ? basename(requestedCwd) : 'pi'),
|
|
104
95
|
runtimeMeta: {
|
|
105
96
|
sessionId: null,
|
|
106
|
-
workdir: requestedCwd,
|
|
107
|
-
cwd: requestedCwd,
|
|
108
97
|
path: null,
|
|
109
98
|
runtimePath: piPath,
|
|
110
99
|
piPath,
|
|
@@ -123,16 +112,9 @@ export const piRuntime = {
|
|
|
123
112
|
if (!binding) return false;
|
|
124
113
|
const adapter = ctx.adapter;
|
|
125
114
|
const meta = binding.runtimeMeta || {};
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
type: 'assistant',
|
|
130
|
-
text: `⚠️ pi cwd not found: ${meta.cwd || '(missing)'}`,
|
|
131
|
-
media: [],
|
|
132
|
-
replyToMessageId: inbound.messageId || null,
|
|
133
|
-
});
|
|
134
|
-
return true;
|
|
135
|
-
}
|
|
115
|
+
const agentHome = ensureAgentHome(binding.id, {
|
|
116
|
+
displayName: binding.display_name || binding.name || null,
|
|
117
|
+
});
|
|
136
118
|
|
|
137
119
|
let images = [];
|
|
138
120
|
let message = inbound.text || '';
|
|
@@ -167,7 +149,7 @@ export const piRuntime = {
|
|
|
167
149
|
binding,
|
|
168
150
|
agent: this.name,
|
|
169
151
|
sessionId: sessionId || meta.sessionId || binding.id,
|
|
170
|
-
cwd: cwd ||
|
|
152
|
+
cwd: cwd || agentHome,
|
|
171
153
|
replyToMessageId: inbound.messageId || null,
|
|
172
154
|
event: {
|
|
173
155
|
hook_event_name: 'worker.message.delta',
|
|
@@ -189,7 +171,7 @@ export const piRuntime = {
|
|
|
189
171
|
});
|
|
190
172
|
const standingPrompt = buildStandingPrompt({ agentId: binding.id });
|
|
191
173
|
const result = shouldStreamRuntime(this.name, this)
|
|
192
|
-
? await this.runTurnStream({ sessionId: shouldRotate ? null : meta.sessionId, cwd:
|
|
174
|
+
? await this.runTurnStream({ sessionId: shouldRotate ? null : meta.sessionId, cwd: agentHome, piPath: runtimePiPath, agentEnv }, message, {
|
|
193
175
|
standingPrompt,
|
|
194
176
|
images,
|
|
195
177
|
onEvent: async (event) => {
|
|
@@ -199,7 +181,7 @@ export const piRuntime = {
|
|
|
199
181
|
binding,
|
|
200
182
|
agent: this.name,
|
|
201
183
|
sessionId: event.sessionId || meta.sessionId || binding.id,
|
|
202
|
-
cwd:
|
|
184
|
+
cwd: agentHome,
|
|
203
185
|
replyToMessageId: inbound.messageId || null,
|
|
204
186
|
event: {
|
|
205
187
|
hook_event_name: 'worker.turn.start',
|
|
@@ -210,17 +192,16 @@ export const piRuntime = {
|
|
|
210
192
|
} else if (event?.type === 'message.delta' && event.text) {
|
|
211
193
|
deltaAggregator.push(event.text, {
|
|
212
194
|
sessionId: event.sessionId || meta.sessionId || binding.id,
|
|
213
|
-
cwd:
|
|
195
|
+
cwd: agentHome,
|
|
214
196
|
});
|
|
215
197
|
}
|
|
216
198
|
},
|
|
217
199
|
})
|
|
218
|
-
: await this.runTurn({ sessionId: shouldRotate ? null : meta.sessionId, cwd:
|
|
200
|
+
: await this.runTurn({ sessionId: shouldRotate ? null : meta.sessionId, cwd: agentHome, piPath: runtimePiPath, agentEnv }, message, { images, standingPrompt });
|
|
219
201
|
|
|
220
202
|
await deltaAggregator.flush();
|
|
221
203
|
const nextBinding = await updateBindingRuntimeMeta(ctx, binding, {
|
|
222
204
|
sessionId: result?.sessionId || meta.sessionId,
|
|
223
|
-
cwd: result?.cwd || meta.cwd,
|
|
224
205
|
path: result?.path || meta.path || null,
|
|
225
206
|
runtimePath: runtimePiPath,
|
|
226
207
|
piPath: runtimePiPath,
|
|
@@ -234,7 +215,7 @@ export const piRuntime = {
|
|
|
234
215
|
binding: nextBinding,
|
|
235
216
|
agent: this.name,
|
|
236
217
|
sessionId: result?.sessionId || meta.sessionId || binding.id,
|
|
237
|
-
cwd: result?.cwd ||
|
|
218
|
+
cwd: result?.cwd || agentHome,
|
|
238
219
|
replyToMessageId: inbound.messageId || null,
|
|
239
220
|
event: {
|
|
240
221
|
hook_event_name: 'Stop',
|
|
@@ -250,7 +231,7 @@ export const piRuntime = {
|
|
|
250
231
|
binding,
|
|
251
232
|
agent: this.name,
|
|
252
233
|
sessionId: meta.sessionId || binding.id,
|
|
253
|
-
cwd:
|
|
234
|
+
cwd: agentHome,
|
|
254
235
|
replyToMessageId: inbound.messageId || null,
|
|
255
236
|
event: {
|
|
256
237
|
hook_event_name: 'worker.turn.error',
|
|
@@ -282,7 +263,7 @@ export const piRuntime = {
|
|
|
282
263
|
binding,
|
|
283
264
|
agent: this.name,
|
|
284
265
|
sessionId: meta.sessionId || binding.id,
|
|
285
|
-
cwd:
|
|
266
|
+
cwd: ensureAgentHome(binding.id) || '',
|
|
286
267
|
event: {
|
|
287
268
|
hook_event_name: 'Stop',
|
|
288
269
|
worker_event_name: 'worker.turn.complete',
|
package/ticlawk.mjs
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
persistConfig,
|
|
22
22
|
} from './src/core/config.mjs';
|
|
23
23
|
import { startLocalHttpServer } from './src/core/http.mjs';
|
|
24
|
+
import { startReminderTicker } from './src/core/reminder-ticker.mjs';
|
|
24
25
|
import { installProcessDiagnostics } from './src/core/diagnostics.mjs';
|
|
25
26
|
import * as logger from './src/core/logger.mjs';
|
|
26
27
|
import { Bus } from './src/core/bus.mjs';
|
|
@@ -30,6 +31,28 @@ import { buildRuntimeContext, normalizeServiceType } from './src/core/runtime-re
|
|
|
30
31
|
import { belongsToRuntimeHost, getBindingRuntimeHostId, getHostId } from './src/core/host-id.mjs';
|
|
31
32
|
import { readPkgVersion } from './src/core/update.mjs';
|
|
32
33
|
import { buildImageMessageFromInbound } from './src/core/media/inbound.mjs';
|
|
34
|
+
import { existsSync as _fsExists, unlinkSync as _fsUnlink } from 'node:fs';
|
|
35
|
+
import { join as _pathJoin } from 'node:path';
|
|
36
|
+
import { getActiveProfile } from './src/core/profiles.mjs';
|
|
37
|
+
|
|
38
|
+
// Pre-profile builds wrote bindings to ~/.ticlawk/bindings.json. The
|
|
39
|
+
// profile flow now owns binding persistence under
|
|
40
|
+
// ~/.ticlawk/profiles/<adapter>/<userId>/bindings.json. The root file
|
|
41
|
+
// hasn't had a writer for a while but lingers as stale state. Prune
|
|
42
|
+
// once per daemon start so it doesn't confuse `jq` audits later.
|
|
43
|
+
function pruneLegacyRootBindings() {
|
|
44
|
+
if (!getActiveProfile()) return;
|
|
45
|
+
const rootPath = _pathJoin(AF_HOME, 'bindings.json');
|
|
46
|
+
if (!_fsExists(rootPath)) return;
|
|
47
|
+
try {
|
|
48
|
+
_fsUnlink(rootPath);
|
|
49
|
+
logger.debugLog?.('startup', 'legacy-root-bindings.pruned', { path: rootPath });
|
|
50
|
+
} catch (err) {
|
|
51
|
+
logger.debugError?.('startup', 'legacy-root-bindings.prune-failed', {
|
|
52
|
+
path: rootPath, error: err?.message || String(err),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
33
56
|
|
|
34
57
|
// Re-export the config-owned paths so local tooling can inspect the
|
|
35
58
|
// ticlawk home/config/log locations without reaching into src/.
|
|
@@ -80,7 +103,7 @@ function createUpsertBindingWithSync(runtimes, adapter) {
|
|
|
80
103
|
};
|
|
81
104
|
}
|
|
82
105
|
|
|
83
|
-
function
|
|
106
|
+
function createPersistBinding(runtimes, getAdapter) {
|
|
84
107
|
return async (binding) => {
|
|
85
108
|
const nextBinding = await upsertBinding(binding);
|
|
86
109
|
if (!belongsToRuntimeHost(nextBinding)) {
|
|
@@ -101,13 +124,13 @@ function createCacheBinding(runtimes, getAdapter) {
|
|
|
101
124
|
};
|
|
102
125
|
}
|
|
103
126
|
|
|
104
|
-
function createBaseRuntimeCtx(runtimes,
|
|
127
|
+
function createBaseRuntimeCtx(runtimes, persistBinding, upsertBindingWithSync) {
|
|
105
128
|
return {
|
|
106
129
|
runtimes,
|
|
107
130
|
getBinding,
|
|
108
131
|
listBindings,
|
|
109
132
|
deleteBinding,
|
|
110
|
-
|
|
133
|
+
persistBinding,
|
|
111
134
|
upsertBinding: upsertBindingWithSync,
|
|
112
135
|
buildImageMessageFromInbound,
|
|
113
136
|
logger,
|
|
@@ -221,11 +244,12 @@ export async function startTiclawk() {
|
|
|
221
244
|
if (started) return;
|
|
222
245
|
started = true;
|
|
223
246
|
installProcessDiagnostics();
|
|
247
|
+
pruneLegacyRootBindings();
|
|
224
248
|
|
|
225
249
|
const { runtimeList, runtimes } = await buildRuntimeContext();
|
|
226
250
|
const resolveRuntimeBinding = createResolveRuntimeBinding(runtimes);
|
|
227
251
|
let adapter;
|
|
228
|
-
const
|
|
252
|
+
const persistBinding = createPersistBinding(runtimes, () => adapter);
|
|
229
253
|
let baseRuntimeCtx;
|
|
230
254
|
let syncBinding = async (binding) => {
|
|
231
255
|
if (!adapter) {
|
|
@@ -233,13 +257,13 @@ export async function startTiclawk() {
|
|
|
233
257
|
}
|
|
234
258
|
return upsertBinding(binding);
|
|
235
259
|
};
|
|
236
|
-
baseRuntimeCtx = createBaseRuntimeCtx(runtimes,
|
|
260
|
+
baseRuntimeCtx = createBaseRuntimeCtx(runtimes, persistBinding, (binding) => syncBinding(binding));
|
|
237
261
|
adapter = createAdapter(
|
|
238
262
|
'ticlawk',
|
|
239
263
|
createAdapterContext(baseRuntimeCtx, resolveRuntimeBinding)
|
|
240
264
|
);
|
|
241
265
|
syncBinding = createUpsertBindingWithSync(runtimes, adapter);
|
|
242
|
-
baseRuntimeCtx = createBaseRuntimeCtx(runtimes,
|
|
266
|
+
baseRuntimeCtx = createBaseRuntimeCtx(runtimes, persistBinding, (binding) => syncBinding(binding));
|
|
243
267
|
|
|
244
268
|
printBanner(adapter);
|
|
245
269
|
if (typeof adapter.refreshBindings === 'function') {
|
|
@@ -252,6 +276,7 @@ export async function startTiclawk() {
|
|
|
252
276
|
adapter,
|
|
253
277
|
ctx: { listBindings, getBinding },
|
|
254
278
|
});
|
|
279
|
+
startReminderTicker();
|
|
255
280
|
await recoverAllRuntimes(runtimeList, adapter);
|
|
256
281
|
await reconcileBindingsAfterRestart(runtimes, adapter);
|
|
257
282
|
await adapter.start();
|