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.
- package/README.md +13 -0
- package/bin/ticlawk.mjs +116 -0
- package/package.json +1 -1
- package/src/adapters/ticlawk/api.mjs +226 -28
- package/src/adapters/ticlawk/index.mjs +258 -113
- package/src/cli/agent-commands.mjs +594 -8
- package/src/core/agent-cli-handlers.mjs +443 -3
- package/src/core/agent-home.mjs +85 -0
- package/src/core/argv.mjs +11 -1
- package/src/core/http.mjs +121 -0
- package/src/core/reminder-ticker.mjs +70 -0
- package/src/core/runtime-contract.mjs +1 -1
- package/src/core/runtime-support.mjs +31 -59
- package/src/core/ticlawk-control.mjs +3 -3
- package/src/migrate/write-initial-memory.mjs +101 -0
- package/src/runtimes/_shared/standing-prompt.mjs +296 -77
- package/src/runtimes/claude-code/index.mjs +28 -131
- package/src/runtimes/codex/index.mjs +15 -39
- package/src/runtimes/openclaw/index.mjs +39 -30
- package/src/runtimes/openclaw/target.mjs +0 -30
- package/src/runtimes/opencode/index.mjs +19 -54
- package/src/runtimes/pi/index.mjs +16 -49
- package/ticlawk.mjs +31 -6
- package/src/adapters/ticlawk/cards.mjs +0 -149
- package/src/core/media/outbound.mjs +0 -163
|
@@ -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,
|
|
@@ -23,13 +24,10 @@ import {
|
|
|
23
24
|
requireOpenCodePath,
|
|
24
25
|
} from './session.mjs';
|
|
25
26
|
import { buildOpenCodeInputFromInbound } from '../../core/media/inbound.mjs';
|
|
26
|
-
import { normalizeOutboundMedia } from '../../core/media/outbound.mjs';
|
|
27
27
|
import { emitWorkerEvent } from '../../core/events/worker-events.mjs';
|
|
28
28
|
import {
|
|
29
29
|
shouldStreamRuntime,
|
|
30
30
|
createDeltaAggregator,
|
|
31
|
-
sendAdapterMessage,
|
|
32
|
-
recordActivity,
|
|
33
31
|
reportSubprocessFailure,
|
|
34
32
|
terminalRuntimeFailure,
|
|
35
33
|
updateBindingRuntimeMeta,
|
|
@@ -97,8 +95,6 @@ export const openCodeRuntime = {
|
|
|
97
95
|
displayName: payload.name || session.title || basename(requestedCwd) || 'opencode',
|
|
98
96
|
runtimeMeta: {
|
|
99
97
|
sessionId: session.sessionId,
|
|
100
|
-
workdir: requestedCwd,
|
|
101
|
-
cwd: requestedCwd,
|
|
102
98
|
runtimePath: opencodePath,
|
|
103
99
|
opencodePath,
|
|
104
100
|
opencodeVersion,
|
|
@@ -107,20 +103,11 @@ export const openCodeRuntime = {
|
|
|
107
103
|
};
|
|
108
104
|
}
|
|
109
105
|
|
|
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
106
|
return {
|
|
118
107
|
runtime: this.name,
|
|
119
|
-
displayName: payload.name || basename(requestedCwd)
|
|
108
|
+
displayName: payload.name || (requestedCwd ? basename(requestedCwd) : 'opencode'),
|
|
120
109
|
runtimeMeta: {
|
|
121
110
|
sessionId: null,
|
|
122
|
-
workdir: requestedCwd,
|
|
123
|
-
cwd: requestedCwd,
|
|
124
111
|
runtimePath: opencodePath,
|
|
125
112
|
opencodePath,
|
|
126
113
|
opencodeVersion,
|
|
@@ -139,16 +126,9 @@ export const openCodeRuntime = {
|
|
|
139
126
|
const adapter = ctx.adapter;
|
|
140
127
|
const meta = binding.runtimeMeta || {};
|
|
141
128
|
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
|
-
}
|
|
129
|
+
const agentHome = ensureAgentHome(binding.id, {
|
|
130
|
+
displayName: binding.display_name || binding.name || null,
|
|
131
|
+
});
|
|
152
132
|
|
|
153
133
|
// For image inbound, resolve the attached media to local file paths
|
|
154
134
|
// and forward them to opencode via `--file` (mirrors how Codex uses
|
|
@@ -163,24 +143,14 @@ export const openCodeRuntime = {
|
|
|
163
143
|
const captionText = (inbound.text || '').trim();
|
|
164
144
|
|
|
165
145
|
if (files.length === 0 && !captionText) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
replyToMessageId: inbound.messageId || null,
|
|
171
|
-
});
|
|
146
|
+
// Image decode failed and no caption to fall back on — we have
|
|
147
|
+
// nothing meaningful to feed the model. Bail without a user
|
|
148
|
+
// notice; this runtime is non-primary and the dead chat-projection
|
|
149
|
+
// path that used to surface such notices is gone.
|
|
172
150
|
return true;
|
|
173
151
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
// Downloads all failed; tell the user we're proceeding with the caption alone.
|
|
177
|
-
await sendAdapterMessage(adapter, binding, {
|
|
178
|
-
type: 'assistant',
|
|
179
|
-
text: '⚠️ Could not access the attached image data; acting on the caption text only.',
|
|
180
|
-
media: [],
|
|
181
|
-
replyToMessageId: inbound.messageId || null,
|
|
182
|
-
});
|
|
183
|
-
}
|
|
152
|
+
// If files.length === 0 && captionText, fall through with the
|
|
153
|
+
// caption-only message below — no inline user notice.
|
|
184
154
|
|
|
185
155
|
// If user sent images with no caption, give the model a minimal
|
|
186
156
|
// instruction so it has something to anchor on.
|
|
@@ -195,7 +165,7 @@ export const openCodeRuntime = {
|
|
|
195
165
|
binding,
|
|
196
166
|
agent: this.name,
|
|
197
167
|
sessionId: sessionId || meta.sessionId || binding.id,
|
|
198
|
-
cwd: cwd ||
|
|
168
|
+
cwd: cwd || agentHome,
|
|
199
169
|
replyToMessageId: inbound.messageId || null,
|
|
200
170
|
event: {
|
|
201
171
|
hook_event_name: 'worker.message.delta',
|
|
@@ -217,7 +187,7 @@ export const openCodeRuntime = {
|
|
|
217
187
|
});
|
|
218
188
|
const standingPrompt = buildStandingPrompt({ agentId: binding.id });
|
|
219
189
|
const result = shouldStreamRuntime(this.name, this)
|
|
220
|
-
? await this.runTurnStream({ sessionId: shouldRotate ? null : meta.sessionId, cwd:
|
|
190
|
+
? await this.runTurnStream({ sessionId: shouldRotate ? null : meta.sessionId, cwd: agentHome, opencodePath, agentEnv }, message, {
|
|
221
191
|
standingPrompt,
|
|
222
192
|
files,
|
|
223
193
|
onEvent: async (event) => {
|
|
@@ -227,7 +197,7 @@ export const openCodeRuntime = {
|
|
|
227
197
|
binding,
|
|
228
198
|
agent: this.name,
|
|
229
199
|
sessionId: event.sessionId || meta.sessionId || binding.id,
|
|
230
|
-
cwd:
|
|
200
|
+
cwd: agentHome,
|
|
231
201
|
replyToMessageId: inbound.messageId || null,
|
|
232
202
|
event: {
|
|
233
203
|
hook_event_name: 'worker.turn.start',
|
|
@@ -238,34 +208,29 @@ export const openCodeRuntime = {
|
|
|
238
208
|
} else if (event?.type === 'message.delta' && event.text) {
|
|
239
209
|
deltaAggregator.push(event.text, {
|
|
240
210
|
sessionId: event.sessionId || meta.sessionId || binding.id,
|
|
241
|
-
cwd:
|
|
211
|
+
cwd: agentHome,
|
|
242
212
|
});
|
|
243
213
|
}
|
|
244
214
|
},
|
|
245
215
|
})
|
|
246
|
-
: await this.runTurn({ sessionId: shouldRotate ? null : meta.sessionId, cwd:
|
|
216
|
+
: await this.runTurn({ sessionId: shouldRotate ? null : meta.sessionId, cwd: agentHome, opencodePath, agentEnv }, message, { files, standingPrompt });
|
|
247
217
|
|
|
248
218
|
await deltaAggregator.flush();
|
|
249
219
|
|
|
250
220
|
const nextBinding = await updateBindingRuntimeMeta(ctx, binding, {
|
|
251
221
|
sessionId: result?.sessionId || meta.sessionId,
|
|
252
|
-
cwd: result?.cwd || meta.cwd,
|
|
253
222
|
runtimePath: opencodePath,
|
|
254
223
|
opencodePath,
|
|
255
224
|
opencodeVersion,
|
|
256
225
|
rotatePending: false,
|
|
257
226
|
lastRotatedAt: shouldRotate ? new Date().toISOString() : meta.lastRotatedAt,
|
|
258
227
|
}, { status: 'connected' });
|
|
259
|
-
await recordActivity(adapter, nextBinding, inbound, {
|
|
260
|
-
...result,
|
|
261
|
-
media: normalizeOutboundMedia(result),
|
|
262
|
-
});
|
|
263
228
|
await emitWorkerEvent({
|
|
264
229
|
adapter,
|
|
265
230
|
binding: nextBinding,
|
|
266
231
|
agent: this.name,
|
|
267
232
|
sessionId: result?.sessionId || meta.sessionId || binding.id,
|
|
268
|
-
cwd: result?.cwd ||
|
|
233
|
+
cwd: result?.cwd || agentHome,
|
|
269
234
|
replyToMessageId: inbound.messageId || null,
|
|
270
235
|
event: {
|
|
271
236
|
hook_event_name: 'Stop',
|
|
@@ -281,7 +246,7 @@ export const openCodeRuntime = {
|
|
|
281
246
|
binding,
|
|
282
247
|
agent: this.name,
|
|
283
248
|
sessionId: meta.sessionId || binding.id,
|
|
284
|
-
cwd:
|
|
249
|
+
cwd: agentHome,
|
|
285
250
|
replyToMessageId: inbound.messageId || null,
|
|
286
251
|
event: {
|
|
287
252
|
hook_event_name: 'worker.turn.error',
|
|
@@ -313,7 +278,7 @@ export const openCodeRuntime = {
|
|
|
313
278
|
binding,
|
|
314
279
|
agent: this.name,
|
|
315
280
|
sessionId: meta.sessionId || binding.id,
|
|
316
|
-
cwd:
|
|
281
|
+
cwd: ensureAgentHome(binding.id) || '',
|
|
317
282
|
event: {
|
|
318
283
|
hook_event_name: 'Stop',
|
|
319
284
|
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,
|
|
@@ -22,8 +22,6 @@ import { emitWorkerEvent } from '../../core/events/worker-events.mjs';
|
|
|
22
22
|
import {
|
|
23
23
|
shouldStreamRuntime,
|
|
24
24
|
createDeltaAggregator,
|
|
25
|
-
sendAdapterMessage,
|
|
26
|
-
recordActivity,
|
|
27
25
|
reportSubprocessFailure,
|
|
28
26
|
terminalRuntimeFailure,
|
|
29
27
|
updateBindingRuntimeMeta,
|
|
@@ -80,8 +78,6 @@ export const piRuntime = {
|
|
|
80
78
|
displayName: payload.name || basename(session.cwd || requestedCwd) || 'pi',
|
|
81
79
|
runtimeMeta: {
|
|
82
80
|
sessionId: session.sessionId,
|
|
83
|
-
workdir: session.cwd || requestedCwd,
|
|
84
|
-
cwd: session.cwd || requestedCwd,
|
|
85
81
|
path: session.path || null,
|
|
86
82
|
runtimePath: piPath,
|
|
87
83
|
piPath,
|
|
@@ -91,20 +87,11 @@ export const piRuntime = {
|
|
|
91
87
|
};
|
|
92
88
|
}
|
|
93
89
|
|
|
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
90
|
return {
|
|
102
91
|
runtime: this.name,
|
|
103
|
-
displayName: payload.name || basename(requestedCwd)
|
|
92
|
+
displayName: payload.name || (requestedCwd ? basename(requestedCwd) : 'pi'),
|
|
104
93
|
runtimeMeta: {
|
|
105
94
|
sessionId: null,
|
|
106
|
-
workdir: requestedCwd,
|
|
107
|
-
cwd: requestedCwd,
|
|
108
95
|
path: null,
|
|
109
96
|
runtimePath: piPath,
|
|
110
97
|
piPath,
|
|
@@ -123,16 +110,9 @@ export const piRuntime = {
|
|
|
123
110
|
if (!binding) return false;
|
|
124
111
|
const adapter = ctx.adapter;
|
|
125
112
|
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
|
-
}
|
|
113
|
+
const agentHome = ensureAgentHome(binding.id, {
|
|
114
|
+
displayName: binding.display_name || binding.name || null,
|
|
115
|
+
});
|
|
136
116
|
|
|
137
117
|
let images = [];
|
|
138
118
|
let message = inbound.text || '';
|
|
@@ -140,22 +120,11 @@ export const piRuntime = {
|
|
|
140
120
|
images = await buildPiImagesFromInbound(inbound);
|
|
141
121
|
const captionText = (inbound.text || '').trim();
|
|
142
122
|
if (images.length === 0 && !captionText) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
media: [],
|
|
147
|
-
replyToMessageId: inbound.messageId || null,
|
|
148
|
-
});
|
|
123
|
+
// Image decode failed and no caption to fall back on. Bail
|
|
124
|
+
// without a user notice; the dead chat-projection path that
|
|
125
|
+
// used to surface such notices is gone.
|
|
149
126
|
return true;
|
|
150
127
|
}
|
|
151
|
-
if (images.length === 0 && captionText) {
|
|
152
|
-
await sendAdapterMessage(adapter, binding, {
|
|
153
|
-
type: 'assistant',
|
|
154
|
-
text: '⚠️ Could not access the attached image data; acting on the caption text only.',
|
|
155
|
-
media: [],
|
|
156
|
-
replyToMessageId: inbound.messageId || null,
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
128
|
message = captionText || 'Please analyze the attached image(s).';
|
|
160
129
|
}
|
|
161
130
|
|
|
@@ -167,7 +136,7 @@ export const piRuntime = {
|
|
|
167
136
|
binding,
|
|
168
137
|
agent: this.name,
|
|
169
138
|
sessionId: sessionId || meta.sessionId || binding.id,
|
|
170
|
-
cwd: cwd ||
|
|
139
|
+
cwd: cwd || agentHome,
|
|
171
140
|
replyToMessageId: inbound.messageId || null,
|
|
172
141
|
event: {
|
|
173
142
|
hook_event_name: 'worker.message.delta',
|
|
@@ -189,7 +158,7 @@ export const piRuntime = {
|
|
|
189
158
|
});
|
|
190
159
|
const standingPrompt = buildStandingPrompt({ agentId: binding.id });
|
|
191
160
|
const result = shouldStreamRuntime(this.name, this)
|
|
192
|
-
? await this.runTurnStream({ sessionId: shouldRotate ? null : meta.sessionId, cwd:
|
|
161
|
+
? await this.runTurnStream({ sessionId: shouldRotate ? null : meta.sessionId, cwd: agentHome, piPath: runtimePiPath, agentEnv }, message, {
|
|
193
162
|
standingPrompt,
|
|
194
163
|
images,
|
|
195
164
|
onEvent: async (event) => {
|
|
@@ -199,7 +168,7 @@ export const piRuntime = {
|
|
|
199
168
|
binding,
|
|
200
169
|
agent: this.name,
|
|
201
170
|
sessionId: event.sessionId || meta.sessionId || binding.id,
|
|
202
|
-
cwd:
|
|
171
|
+
cwd: agentHome,
|
|
203
172
|
replyToMessageId: inbound.messageId || null,
|
|
204
173
|
event: {
|
|
205
174
|
hook_event_name: 'worker.turn.start',
|
|
@@ -210,17 +179,16 @@ export const piRuntime = {
|
|
|
210
179
|
} else if (event?.type === 'message.delta' && event.text) {
|
|
211
180
|
deltaAggregator.push(event.text, {
|
|
212
181
|
sessionId: event.sessionId || meta.sessionId || binding.id,
|
|
213
|
-
cwd:
|
|
182
|
+
cwd: agentHome,
|
|
214
183
|
});
|
|
215
184
|
}
|
|
216
185
|
},
|
|
217
186
|
})
|
|
218
|
-
: await this.runTurn({ sessionId: shouldRotate ? null : meta.sessionId, cwd:
|
|
187
|
+
: await this.runTurn({ sessionId: shouldRotate ? null : meta.sessionId, cwd: agentHome, piPath: runtimePiPath, agentEnv }, message, { images, standingPrompt });
|
|
219
188
|
|
|
220
189
|
await deltaAggregator.flush();
|
|
221
190
|
const nextBinding = await updateBindingRuntimeMeta(ctx, binding, {
|
|
222
191
|
sessionId: result?.sessionId || meta.sessionId,
|
|
223
|
-
cwd: result?.cwd || meta.cwd,
|
|
224
192
|
path: result?.path || meta.path || null,
|
|
225
193
|
runtimePath: runtimePiPath,
|
|
226
194
|
piPath: runtimePiPath,
|
|
@@ -228,13 +196,12 @@ export const piRuntime = {
|
|
|
228
196
|
rotatePending: false,
|
|
229
197
|
lastRotatedAt: shouldRotate ? new Date().toISOString() : meta.lastRotatedAt,
|
|
230
198
|
}, { status: 'connected' });
|
|
231
|
-
await recordActivity(adapter, nextBinding, inbound, result);
|
|
232
199
|
await emitWorkerEvent({
|
|
233
200
|
adapter,
|
|
234
201
|
binding: nextBinding,
|
|
235
202
|
agent: this.name,
|
|
236
203
|
sessionId: result?.sessionId || meta.sessionId || binding.id,
|
|
237
|
-
cwd: result?.cwd ||
|
|
204
|
+
cwd: result?.cwd || agentHome,
|
|
238
205
|
replyToMessageId: inbound.messageId || null,
|
|
239
206
|
event: {
|
|
240
207
|
hook_event_name: 'Stop',
|
|
@@ -250,7 +217,7 @@ export const piRuntime = {
|
|
|
250
217
|
binding,
|
|
251
218
|
agent: this.name,
|
|
252
219
|
sessionId: meta.sessionId || binding.id,
|
|
253
|
-
cwd:
|
|
220
|
+
cwd: agentHome,
|
|
254
221
|
replyToMessageId: inbound.messageId || null,
|
|
255
222
|
event: {
|
|
256
223
|
hook_event_name: 'worker.turn.error',
|
|
@@ -282,7 +249,7 @@ export const piRuntime = {
|
|
|
282
249
|
binding,
|
|
283
250
|
agent: this.name,
|
|
284
251
|
sessionId: meta.sessionId || binding.id,
|
|
285
|
-
cwd:
|
|
252
|
+
cwd: ensureAgentHome(binding.id) || '',
|
|
286
253
|
event: {
|
|
287
254
|
hook_event_name: 'Stop',
|
|
288
255
|
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();
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ticlawk adapter — final agent message / media write path.
|
|
3
|
-
*
|
|
4
|
-
* Everything here turns an agent result (text + optional media local paths)
|
|
5
|
-
* into a terminal runtime result written back to ticlawk.
|
|
6
|
-
*
|
|
7
|
-
* This module imports from `./api.mjs` (HTTP client) and from
|
|
8
|
-
* `../../core/logger.mjs` (structured logging). No runtime dependencies.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
12
|
-
import { extname } from 'node:path';
|
|
13
|
-
import { randomUUID } from 'node:crypto';
|
|
14
|
-
import * as api from './api.mjs';
|
|
15
|
-
import { extractMediaPaths } from '../../core/media/outbound.mjs';
|
|
16
|
-
import { debugLog, debugError } from '../../core/logger.mjs';
|
|
17
|
-
import { TICLAWK_CONNECTOR_API_KEY } from '../../core/config.mjs';
|
|
18
|
-
|
|
19
|
-
const MIME_MAP = {
|
|
20
|
-
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
21
|
-
'.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml',
|
|
22
|
-
'.mp4': 'video/mp4', '.webm': 'video/webm',
|
|
23
|
-
'.opus': 'audio/opus', '.mp3': 'audio/mpeg', '.wav': 'audio/wav',
|
|
24
|
-
'.pdf': 'application/pdf',
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
async function uploadLocalAsset(localPath) {
|
|
28
|
-
if (!process.env[TICLAWK_CONNECTOR_API_KEY]) {
|
|
29
|
-
debugError('relay', 'upload.skipped', {
|
|
30
|
-
localPath,
|
|
31
|
-
reason: 'missing connector api key',
|
|
32
|
-
});
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
if (!existsSync(localPath)) {
|
|
36
|
-
debugError('relay', 'upload.skipped', {
|
|
37
|
-
localPath,
|
|
38
|
-
reason: 'file not found',
|
|
39
|
-
});
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const ext = extname(localPath).toLowerCase();
|
|
44
|
-
const contentType = MIME_MAP[ext] || 'application/octet-stream';
|
|
45
|
-
const fileName = `${Date.now()}-${randomUUID().slice(0, 8)}${ext}`;
|
|
46
|
-
const fileData = readFileSync(localPath);
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
const asset = await api.uploadAsset(fileName, fileData, contentType);
|
|
50
|
-
if (!asset?.asset_id) {
|
|
51
|
-
debugError('relay', 'upload.failed', {
|
|
52
|
-
localPath,
|
|
53
|
-
fileName,
|
|
54
|
-
error: 'missing asset_id in upload response',
|
|
55
|
-
});
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
debugLog('relay', 'upload.ok', {
|
|
59
|
-
localPath,
|
|
60
|
-
fileName,
|
|
61
|
-
assetId: asset.asset_id,
|
|
62
|
-
contentType: asset.content_type || contentType,
|
|
63
|
-
sizeBytes: asset.size_bytes ?? null,
|
|
64
|
-
});
|
|
65
|
-
return asset;
|
|
66
|
-
} catch (err) {
|
|
67
|
-
debugError('relay', 'upload.failed', {
|
|
68
|
-
localPath,
|
|
69
|
-
fileName,
|
|
70
|
-
error: err.message,
|
|
71
|
-
});
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export async function uploadMediaAssets(localPaths) {
|
|
77
|
-
const assets = [];
|
|
78
|
-
for (const p of localPaths) {
|
|
79
|
-
const asset = await uploadLocalAsset(p);
|
|
80
|
-
if (asset) assets.push(asset);
|
|
81
|
-
}
|
|
82
|
-
return assets;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export async function processAndSaveResult(result, opts) {
|
|
86
|
-
const { agentId: explicitAgentId, sessionKey, hostId, type, replyToMessageId, agent, sessionId, turnId, runtimeVersion } = opts;
|
|
87
|
-
const agentId = explicitAgentId || sessionKey;
|
|
88
|
-
const startedAt = Date.now();
|
|
89
|
-
|
|
90
|
-
// Collect media: from agent mediaUrls + parsed from text
|
|
91
|
-
const allLocalPaths = [...new Set([
|
|
92
|
-
...(result.mediaUrls || []),
|
|
93
|
-
...extractMediaPaths(result.text || ''),
|
|
94
|
-
])];
|
|
95
|
-
|
|
96
|
-
debugLog('relay', 'process-result.begin', {
|
|
97
|
-
agentId,
|
|
98
|
-
type,
|
|
99
|
-
parentMessageId: replyToMessageId || null,
|
|
100
|
-
textLength: result.text?.length || 0,
|
|
101
|
-
localMediaCount: allLocalPaths.length,
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
// Upload local media to Ticlawk private chat assets.
|
|
105
|
-
const uploadedAssets = await uploadMediaAssets(allLocalPaths);
|
|
106
|
-
const uploadedAssetIds = uploadedAssets
|
|
107
|
-
.map((asset) => asset?.asset_id)
|
|
108
|
-
.filter(Boolean);
|
|
109
|
-
|
|
110
|
-
const updateId = randomUUID();
|
|
111
|
-
debugLog('relay', 'post-final.begin', {
|
|
112
|
-
agentId,
|
|
113
|
-
updateId,
|
|
114
|
-
durationMs: Date.now() - startedAt,
|
|
115
|
-
uploadedMediaCount: uploadedAssetIds.length,
|
|
116
|
-
});
|
|
117
|
-
try {
|
|
118
|
-
await api.postRuntimeResult({
|
|
119
|
-
agent,
|
|
120
|
-
agent_id: agentId,
|
|
121
|
-
runtime_host_id: hostId,
|
|
122
|
-
session_id: sessionId || null,
|
|
123
|
-
cwd: '',
|
|
124
|
-
runtime_version: runtimeVersion ?? null,
|
|
125
|
-
result_id: updateId,
|
|
126
|
-
turn_id: turnId || replyToMessageId || null,
|
|
127
|
-
reply_to_message_id: replyToMessageId || null,
|
|
128
|
-
origin_ts: new Date().toISOString(),
|
|
129
|
-
text: result.text || '',
|
|
130
|
-
media_asset_ids: uploadedAssetIds,
|
|
131
|
-
output_type: type || 'agent_message',
|
|
132
|
-
});
|
|
133
|
-
} catch (err) {
|
|
134
|
-
debugError('relay', 'post-final.failed', {
|
|
135
|
-
agentId,
|
|
136
|
-
updateId,
|
|
137
|
-
durationMs: Date.now() - startedAt,
|
|
138
|
-
error: err.message,
|
|
139
|
-
});
|
|
140
|
-
throw err;
|
|
141
|
-
}
|
|
142
|
-
debugLog('relay', 'process-result.ok', {
|
|
143
|
-
agentId,
|
|
144
|
-
updateId,
|
|
145
|
-
durationMs: Date.now() - startedAt,
|
|
146
|
-
uploadedMediaCount: uploadedAssetIds.length,
|
|
147
|
-
});
|
|
148
|
-
return { id: updateId, agentId };
|
|
149
|
-
}
|