ticlawk 0.1.16-dev.7 → 0.1.16-dev.8
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/package.json +1 -1
- package/src/adapters/ticlawk/api.mjs +2 -28
- package/src/adapters/ticlawk/index.mjs +0 -24
- package/src/cli/agent-commands.mjs +31 -1
- package/src/core/agent-cli-handlers.mjs +5 -0
- package/src/core/runtime-support.mjs +0 -30
- package/src/runtimes/_shared/standing-prompt.mjs +21 -0
- package/src/runtimes/claude-code/index.mjs +0 -6
- package/src/runtimes/codex/index.mjs +0 -7
- package/src/runtimes/openclaw/index.mjs +5 -17
- package/src/runtimes/opencode/index.mjs +6 -23
- package/src/runtimes/pi/index.mjs +3 -17
- package/src/adapters/ticlawk/cards.mjs +0 -149
- package/src/core/media/outbound.mjs +0 -163
package/package.json
CHANGED
|
@@ -149,18 +149,6 @@ export async function updateChannel(id, updates) {
|
|
|
149
149
|
return updateAgent(id, updates);
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
export async function postRuntimeResult(body) {
|
|
153
|
-
// Activity-only: backend records the agent_event for UI trajectory
|
|
154
|
-
// display but does NOT project it into a chat message. The agent CLI's
|
|
155
|
-
// `ticlawk message send` is the only path that produces chat rows.
|
|
156
|
-
const result = await apiFetch('/api/runtime-results', {
|
|
157
|
-
method: 'POST',
|
|
158
|
-
body: JSON.stringify(body),
|
|
159
|
-
timeout: 30000,
|
|
160
|
-
});
|
|
161
|
-
return result;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
152
|
// ── Deliveries (replaces legacy message_jobs poll/ack) ──
|
|
165
153
|
|
|
166
154
|
export async function claimPendingDeliveries(hostId, limit = 5, excludedAgentIds = []) {
|
|
@@ -199,6 +187,7 @@ export async function sendAgentMessage({
|
|
|
199
187
|
replyToMessageId,
|
|
200
188
|
runtimeHostId,
|
|
201
189
|
visibility,
|
|
190
|
+
mediaAssetIds,
|
|
202
191
|
}) {
|
|
203
192
|
const { data } = await apiFetch('/api/agent/messages/send', {
|
|
204
193
|
method: 'POST',
|
|
@@ -210,6 +199,7 @@ export async function sendAgentMessage({
|
|
|
210
199
|
reply_to_message_id: replyToMessageId ?? null,
|
|
211
200
|
runtime_host_id: runtimeHostId ?? null,
|
|
212
201
|
visibility: visibility || null,
|
|
202
|
+
media_asset_ids: Array.isArray(mediaAssetIds) && mediaAssetIds.length > 0 ? mediaAssetIds : undefined,
|
|
213
203
|
}),
|
|
214
204
|
});
|
|
215
205
|
return data || null;
|
|
@@ -494,22 +484,6 @@ export async function removeAgentGroupMember({
|
|
|
494
484
|
);
|
|
495
485
|
}
|
|
496
486
|
|
|
497
|
-
// ── Upload ──
|
|
498
|
-
|
|
499
|
-
export async function uploadAsset(fileName, fileData, contentType, kind = 'chat_media') {
|
|
500
|
-
const formData = new FormData();
|
|
501
|
-
formData.append('file', new Blob([fileData], { type: contentType }), fileName);
|
|
502
|
-
formData.append('kind', kind);
|
|
503
|
-
if (contentType) formData.append('content_type', contentType);
|
|
504
|
-
|
|
505
|
-
const { data } = await apiFetch('/api/assets/upload', {
|
|
506
|
-
method: 'POST',
|
|
507
|
-
body: formData,
|
|
508
|
-
timeout: 30000,
|
|
509
|
-
});
|
|
510
|
-
return data;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
487
|
// ── Channel event pipe ──
|
|
514
488
|
|
|
515
489
|
export async function postEvent({ agent, agent_id, runtime_host_id, session_id, cwd, runtime_version, event, required = false }) {
|
|
@@ -10,7 +10,6 @@ import { isTerminalRuntimeFailure } from '../../core/runtime-support.mjs';
|
|
|
10
10
|
import { clearUpdateRequiredState, readUpdateState, setUpdateRequiredState } from '../../core/update-state.mjs';
|
|
11
11
|
import { isManagedInstall, startDetachedSelfUpdate } from '../../core/update.mjs';
|
|
12
12
|
import * as api from './api.mjs';
|
|
13
|
-
import { processAndSaveResult } from './cards.mjs';
|
|
14
13
|
import { persistApiCredential } from './credentials.mjs';
|
|
15
14
|
import { TiclawkWakeClient } from './wake-client.mjs';
|
|
16
15
|
|
|
@@ -205,10 +204,6 @@ export function normalizeInboundMessage(msg) {
|
|
|
205
204
|
};
|
|
206
205
|
}
|
|
207
206
|
|
|
208
|
-
function getBindingSessionId(binding) {
|
|
209
|
-
return binding?.runtimeMeta?.sessionId || binding?.runtimeMeta?.sessionKey || binding?.runtimeMeta?.agentId || binding?.id || null;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
207
|
function getRuntimeVersion(bindingOrMeta) {
|
|
213
208
|
const value = bindingOrMeta?.runtimeMeta?.runtimeVersion ?? bindingOrMeta?.runtimeVersion ?? bindingOrMeta?.agent_runtime_version ?? null;
|
|
214
209
|
return Number.isInteger(value) ? Number(value) : null;
|
|
@@ -1341,25 +1336,6 @@ export function createTiclawkAdapter(ctx) {
|
|
|
1341
1336
|
}
|
|
1342
1337
|
},
|
|
1343
1338
|
|
|
1344
|
-
async send(binding, outbound) {
|
|
1345
|
-
const localMediaPaths = (outbound.media || [])
|
|
1346
|
-
.filter((item) => item.kind === 'local_path')
|
|
1347
|
-
.map((item) => item.value);
|
|
1348
|
-
await processAndSaveResult({
|
|
1349
|
-
text: outbound.text || '',
|
|
1350
|
-
mediaUrls: localMediaPaths,
|
|
1351
|
-
}, {
|
|
1352
|
-
agentId: binding.id,
|
|
1353
|
-
hostId,
|
|
1354
|
-
agent: binding.runtime,
|
|
1355
|
-
sessionId: getBindingSessionId(binding),
|
|
1356
|
-
runtimeVersion: getRuntimeVersion(binding),
|
|
1357
|
-
turnId: outbound.turnId || outbound.replyToMessageId || null,
|
|
1358
|
-
type: outbound.type || 'agent_message',
|
|
1359
|
-
replyToMessageId: outbound.replyToMessageId || null,
|
|
1360
|
-
});
|
|
1361
|
-
},
|
|
1362
|
-
|
|
1363
1339
|
// Daemon-driven impersonated reply: the runtime never reached
|
|
1364
1340
|
// `ticlawk message send` (subprocess died, timeout, etc.), so the
|
|
1365
1341
|
// daemon speaks for the agent. Used by reportSubprocessFailure
|
|
@@ -136,12 +136,37 @@ export async function runMessageSendCommand(args) {
|
|
|
136
136
|
console.error(' EOF');
|
|
137
137
|
return 2;
|
|
138
138
|
}
|
|
139
|
+
|
|
140
|
+
// --attach <path> may be passed multiple times. Each file is uploaded
|
|
141
|
+
// through the same daemon endpoint as `ticlawk attachment upload`, then
|
|
142
|
+
// its asset_id is linked to the message via media_asset_ids.
|
|
143
|
+
const attachArg = args.attach;
|
|
144
|
+
const attachPaths = Array.isArray(attachArg)
|
|
145
|
+
? attachArg
|
|
146
|
+
: attachArg
|
|
147
|
+
? [attachArg]
|
|
148
|
+
: [];
|
|
149
|
+
if (attachPaths.length > 10) {
|
|
150
|
+
console.error('at most 10 --attach files per message');
|
|
151
|
+
return 2;
|
|
152
|
+
}
|
|
153
|
+
const mediaAssetIds = [];
|
|
154
|
+
for (const filePath of attachPaths) {
|
|
155
|
+
const upload = await uploadFileViaDaemon(env, String(filePath));
|
|
156
|
+
if (!upload.ok) {
|
|
157
|
+
console.error(`attachment upload failed for ${filePath}: ${upload.error}`);
|
|
158
|
+
return 1;
|
|
159
|
+
}
|
|
160
|
+
mediaAssetIds.push(upload.assetId);
|
|
161
|
+
}
|
|
162
|
+
|
|
139
163
|
const body = {
|
|
140
164
|
target,
|
|
141
165
|
conversation_id: conversationId,
|
|
142
166
|
text: text.replace(/\n+$/, ''),
|
|
143
167
|
seen_up_to_seq: getNumberArg(args, 'seen-up-to-seq'),
|
|
144
168
|
reply_to_message_id: getArg(args, 'reply-to'),
|
|
169
|
+
media_asset_ids: mediaAssetIds.length > 0 ? mediaAssetIds : undefined,
|
|
145
170
|
};
|
|
146
171
|
const res = await daemonRequest({
|
|
147
172
|
method: 'POST',
|
|
@@ -802,12 +827,14 @@ export async function runServerInfoCommand(args) {
|
|
|
802
827
|
|
|
803
828
|
export const AGENT_COMMAND_HELP = {
|
|
804
829
|
message: `ticlawk message <send|read|check|search|react>
|
|
805
|
-
ticlawk message send --target "<target>" [--seen-up-to-seq N] [--reply-to <msg-id>]
|
|
830
|
+
ticlawk message send --target "<target>" [--seen-up-to-seq N] [--reply-to <msg-id>] [--attach <file> ...]
|
|
806
831
|
Body is read from stdin (use <<'EOF' ... EOF for multiline).
|
|
807
832
|
Targets:
|
|
808
833
|
dm:@<user> private message
|
|
809
834
|
#<group> group conversation
|
|
810
835
|
#<group>:<msgid> thread under a top-level message in that group
|
|
836
|
+
--attach <file> uploads a local file and attaches it to the message
|
|
837
|
+
(repeatable; max 10 attachments per message).
|
|
811
838
|
ticlawk message read --target "<target>" [--around <msg-id>] [--before-seq N] [--limit N]
|
|
812
839
|
ticlawk message check [--target "<target>"]
|
|
813
840
|
Non-blocking poll for new/unprocessed messages.
|
|
@@ -823,6 +850,9 @@ export const AGENT_COMMAND_HELP = {
|
|
|
823
850
|
`,
|
|
824
851
|
attachment: `ticlawk attachment <upload|view>
|
|
825
852
|
ticlawk attachment upload <file>
|
|
853
|
+
Upload-only — the user is NOT notified. Almost always you want
|
|
854
|
+
\`ticlawk message send --attach <file>\` instead, which uploads AND
|
|
855
|
+
surfaces the file to the user inside a chat message.
|
|
826
856
|
ticlawk attachment view <asset-id> [--out <path>]
|
|
827
857
|
`,
|
|
828
858
|
reminder: `ticlawk reminder <schedule|list|snooze|update|cancel|log>
|
|
@@ -145,6 +145,10 @@ export async function handleMessageSend(req, body, ctx) {
|
|
|
145
145
|
return { status: 400, body: { error: 'target or conversation_id is required' } };
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
const mediaAssetIds = Array.isArray(body?.media_asset_ids)
|
|
149
|
+
? body.media_asset_ids.map((v) => String(v).trim()).filter(Boolean)
|
|
150
|
+
: [];
|
|
151
|
+
|
|
148
152
|
try {
|
|
149
153
|
const data = await api.sendAgentMessage({
|
|
150
154
|
actingAgentId,
|
|
@@ -153,6 +157,7 @@ export async function handleMessageSend(req, body, ctx) {
|
|
|
153
157
|
seenUpToSeq: body?.seen_up_to_seq,
|
|
154
158
|
replyToMessageId: body?.reply_to_message_id || threadRootMsgId || null,
|
|
155
159
|
runtimeHostId: getRuntimeHostId(req, body),
|
|
160
|
+
mediaAssetIds,
|
|
156
161
|
});
|
|
157
162
|
debugLog('agent-cli', 'send.ok', {
|
|
158
163
|
actingAgentId,
|
|
@@ -42,36 +42,6 @@ export function shouldStreamRuntime(runtimeName, runtime) {
|
|
|
42
42
|
return Boolean(runtime?.runTurnStream) && getStreamingMode(runtimeName);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
export async function sendAdapterMessage(adapter, binding, payload) {
|
|
46
|
-
await adapter.send(binding, payload);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Record the runtime's final turn output as activity (NOT chat).
|
|
51
|
-
*
|
|
52
|
-
* Previously called `sendResult` and treated as "the chat reply path".
|
|
53
|
-
* After the group-chat upgrade, chat is produced exclusively by the
|
|
54
|
-
* agent invoking `ticlawk message send` via the CLI. The runtime's
|
|
55
|
-
* raw final output is still surfaced for trajectory/debug UI, but it
|
|
56
|
-
* no longer materializes as a `messages` row — the trigger
|
|
57
|
-
* `project_agent_event` was updated in PR-2b to drop the chat
|
|
58
|
-
* projection.
|
|
59
|
-
*
|
|
60
|
-
* Renamed so the call sites read self-evidently: "record activity"
|
|
61
|
-
* never reads as "send a chat message".
|
|
62
|
-
*/
|
|
63
|
-
export async function recordActivity(adapter, binding, inbound, result) {
|
|
64
|
-
if (!result?.text && (!result?.mediaUrls || result.mediaUrls.length === 0)) return;
|
|
65
|
-
await sendAdapterMessage(adapter, binding, {
|
|
66
|
-
type: 'assistant',
|
|
67
|
-
text: result.text || '',
|
|
68
|
-
media: result.media || [],
|
|
69
|
-
turnId: inbound.messageId || result?.turnId || null,
|
|
70
|
-
replyToMessageId: inbound.messageId || null,
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
45
|
export async function reportSubprocessFailure({ adapter, binding, inbound, runtimeName, info }) {
|
|
76
46
|
if (!info || info.ok) return;
|
|
77
47
|
const seconds = ((info.durationMs || 0) / 1000).toFixed(1);
|
|
@@ -63,6 +63,27 @@ EOF
|
|
|
63
63
|
|
|
64
64
|
**IMPORTANT**: To reply to any message, always reuse the exact \`target\` from the received message. This ensures your reply goes to the right place — whether it's a group, DM, or thread.
|
|
65
65
|
|
|
66
|
+
### Sending files
|
|
67
|
+
|
|
68
|
+
When you produce a file the user should see (a generated report, a
|
|
69
|
+
screenshot, a downloaded asset, a code patch as a file), attach it
|
|
70
|
+
inline with the chat message:
|
|
71
|
+
|
|
72
|
+
\`\`\`bash
|
|
73
|
+
ticlawk message send --target "#group" --attach /tmp/report.pdf --attach /tmp/chart.png <<'EOF'
|
|
74
|
+
Here's the analysis you asked for — full report attached, plus the
|
|
75
|
+
key chart pulled out for quick reference.
|
|
76
|
+
EOF
|
|
77
|
+
\`\`\`
|
|
78
|
+
|
|
79
|
+
- \`--attach <path>\` may be repeated; up to 10 files per message.
|
|
80
|
+
- Each file uploads to the secure file layer and surfaces to the user
|
|
81
|
+
inside the chat UI alongside your text.
|
|
82
|
+
- \`ticlawk attachment upload <file>\` exists as a separate command but
|
|
83
|
+
**only uploads** — the user is not notified and cannot see the file.
|
|
84
|
+
Almost never the right command. Only use it if you have a specific
|
|
85
|
+
reason to stash a file for later use without telling the user.
|
|
86
|
+
|
|
66
87
|
### Threads
|
|
67
88
|
|
|
68
89
|
Threads are sub-conversations attached to a specific message. They let you discuss a topic without cluttering the main group.
|
|
@@ -23,11 +23,9 @@ import {
|
|
|
23
23
|
} from './session.mjs';
|
|
24
24
|
import { discoverSessions } from './transcripts.mjs';
|
|
25
25
|
import { buildImageMessageFromInbound } from '../../core/media/inbound.mjs';
|
|
26
|
-
import { normalizeOutboundMedia } from '../../core/media/outbound.mjs';
|
|
27
26
|
import { emitWorkerEvent } from '../../core/events/worker-events.mjs';
|
|
28
27
|
import {
|
|
29
28
|
shouldStreamRuntime,
|
|
30
|
-
recordActivity,
|
|
31
29
|
reportSubprocessFailure,
|
|
32
30
|
terminalRuntimeFailure,
|
|
33
31
|
updateBindingRuntimeMeta,
|
|
@@ -196,10 +194,6 @@ export const claudeCodeRuntime = {
|
|
|
196
194
|
rotatePending: false,
|
|
197
195
|
lastRotatedAt: shouldRotate ? new Date().toISOString() : meta.lastRotatedAt,
|
|
198
196
|
}, { status: 'connected' });
|
|
199
|
-
await recordActivity(adapter, nextBinding, inbound, {
|
|
200
|
-
...result,
|
|
201
|
-
media: normalizeOutboundMedia(result),
|
|
202
|
-
});
|
|
203
197
|
await emitWorkerEvent({
|
|
204
198
|
adapter,
|
|
205
199
|
binding: nextBinding,
|
|
@@ -18,13 +18,10 @@ import {
|
|
|
18
18
|
requireCodexPath,
|
|
19
19
|
} from './session.mjs';
|
|
20
20
|
import { buildCodexInputFromInbound } from '../../core/media/inbound.mjs';
|
|
21
|
-
import { normalizeOutboundMedia } from '../../core/media/outbound.mjs';
|
|
22
21
|
import { emitWorkerEvent } from '../../core/events/worker-events.mjs';
|
|
23
22
|
import {
|
|
24
23
|
shouldStreamRuntime,
|
|
25
24
|
createDeltaAggregator,
|
|
26
|
-
sendAdapterMessage,
|
|
27
|
-
recordActivity,
|
|
28
25
|
reportSubprocessFailure,
|
|
29
26
|
terminalRuntimeFailure,
|
|
30
27
|
updateBindingRuntimeMeta,
|
|
@@ -215,10 +212,6 @@ export const codexRuntime = {
|
|
|
215
212
|
rotatePending: false,
|
|
216
213
|
lastRotatedAt: shouldRotate ? new Date().toISOString() : meta.lastRotatedAt,
|
|
217
214
|
}, { status: 'connected' });
|
|
218
|
-
await recordActivity(adapter, nextBinding, inbound, {
|
|
219
|
-
...result,
|
|
220
|
-
media: normalizeOutboundMedia(result),
|
|
221
|
-
});
|
|
222
215
|
await emitWorkerEvent({
|
|
223
216
|
adapter,
|
|
224
217
|
binding: nextBinding,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
4
|
|
|
@@ -192,10 +191,6 @@ export const openClawRuntime = {
|
|
|
192
191
|
});
|
|
193
192
|
},
|
|
194
193
|
});
|
|
195
|
-
await recordActivity(adapter, binding, inbound, {
|
|
196
|
-
...result,
|
|
197
|
-
media: normalizeOutboundMedia(result),
|
|
198
|
-
});
|
|
199
194
|
return true;
|
|
200
195
|
} catch (err) {
|
|
201
196
|
if (typeof adapter.emitEvent === 'function') {
|
|
@@ -233,18 +228,11 @@ export const openClawRuntime = {
|
|
|
233
228
|
}
|
|
234
229
|
},
|
|
235
230
|
|
|
236
|
-
async recoverInFlight(
|
|
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.
|
|
237
235
|
const entries = recoverInFlightEntries();
|
|
238
|
-
for (const entry of entries) {
|
|
239
|
-
const binding = ctx.getBinding(entry.bindingId);
|
|
240
|
-
if (!binding) continue;
|
|
241
|
-
await sendAdapterMessage(ctx.adapter, binding, {
|
|
242
|
-
type: 'assistant',
|
|
243
|
-
text: '⚠️ Lost while ticlawk restarted.\n\nThis OpenClaw message was in flight when ticlawk restarted. Please retry your message.',
|
|
244
|
-
media: [],
|
|
245
|
-
replyToMessageId: entry.messageId || null,
|
|
246
|
-
}).catch(() => {});
|
|
247
|
-
}
|
|
248
236
|
return entries.length;
|
|
249
237
|
},
|
|
250
238
|
|
|
@@ -24,13 +24,10 @@ import {
|
|
|
24
24
|
requireOpenCodePath,
|
|
25
25
|
} from './session.mjs';
|
|
26
26
|
import { buildOpenCodeInputFromInbound } from '../../core/media/inbound.mjs';
|
|
27
|
-
import { normalizeOutboundMedia } from '../../core/media/outbound.mjs';
|
|
28
27
|
import { emitWorkerEvent } from '../../core/events/worker-events.mjs';
|
|
29
28
|
import {
|
|
30
29
|
shouldStreamRuntime,
|
|
31
30
|
createDeltaAggregator,
|
|
32
|
-
sendAdapterMessage,
|
|
33
|
-
recordActivity,
|
|
34
31
|
reportSubprocessFailure,
|
|
35
32
|
terminalRuntimeFailure,
|
|
36
33
|
updateBindingRuntimeMeta,
|
|
@@ -146,24 +143,14 @@ export const openCodeRuntime = {
|
|
|
146
143
|
const captionText = (inbound.text || '').trim();
|
|
147
144
|
|
|
148
145
|
if (files.length === 0 && !captionText) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
replyToMessageId: inbound.messageId || null,
|
|
154
|
-
});
|
|
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.
|
|
155
150
|
return true;
|
|
156
151
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
// Downloads all failed; tell the user we're proceeding with the caption alone.
|
|
160
|
-
await sendAdapterMessage(adapter, binding, {
|
|
161
|
-
type: 'assistant',
|
|
162
|
-
text: '⚠️ Could not access the attached image data; acting on the caption text only.',
|
|
163
|
-
media: [],
|
|
164
|
-
replyToMessageId: inbound.messageId || null,
|
|
165
|
-
});
|
|
166
|
-
}
|
|
152
|
+
// If files.length === 0 && captionText, fall through with the
|
|
153
|
+
// caption-only message below — no inline user notice.
|
|
167
154
|
|
|
168
155
|
// If user sent images with no caption, give the model a minimal
|
|
169
156
|
// instruction so it has something to anchor on.
|
|
@@ -238,10 +225,6 @@ export const openCodeRuntime = {
|
|
|
238
225
|
rotatePending: false,
|
|
239
226
|
lastRotatedAt: shouldRotate ? new Date().toISOString() : meta.lastRotatedAt,
|
|
240
227
|
}, { status: 'connected' });
|
|
241
|
-
await recordActivity(adapter, nextBinding, inbound, {
|
|
242
|
-
...result,
|
|
243
|
-
media: normalizeOutboundMedia(result),
|
|
244
|
-
});
|
|
245
228
|
await emitWorkerEvent({
|
|
246
229
|
adapter,
|
|
247
230
|
binding: nextBinding,
|
|
@@ -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,
|
|
@@ -122,22 +120,11 @@ export const piRuntime = {
|
|
|
122
120
|
images = await buildPiImagesFromInbound(inbound);
|
|
123
121
|
const captionText = (inbound.text || '').trim();
|
|
124
122
|
if (images.length === 0 && !captionText) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
media: [],
|
|
129
|
-
replyToMessageId: inbound.messageId || null,
|
|
130
|
-
});
|
|
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.
|
|
131
126
|
return true;
|
|
132
127
|
}
|
|
133
|
-
if (images.length === 0 && captionText) {
|
|
134
|
-
await sendAdapterMessage(adapter, binding, {
|
|
135
|
-
type: 'assistant',
|
|
136
|
-
text: '⚠️ Could not access the attached image data; acting on the caption text only.',
|
|
137
|
-
media: [],
|
|
138
|
-
replyToMessageId: inbound.messageId || null,
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
128
|
message = captionText || 'Please analyze the attached image(s).';
|
|
142
129
|
}
|
|
143
130
|
|
|
@@ -209,7 +196,6 @@ export const piRuntime = {
|
|
|
209
196
|
rotatePending: false,
|
|
210
197
|
lastRotatedAt: shouldRotate ? new Date().toISOString() : meta.lastRotatedAt,
|
|
211
198
|
}, { status: 'connected' });
|
|
212
|
-
await recordActivity(adapter, nextBinding, inbound, result);
|
|
213
199
|
await emitWorkerEvent({
|
|
214
200
|
adapter,
|
|
215
201
|
binding: nextBinding,
|
|
@@ -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
|
-
}
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import { extname } from 'node:path';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
|
-
|
|
5
|
-
export const MIME_MAP = {
|
|
6
|
-
'.png': 'image/png',
|
|
7
|
-
'.jpg': 'image/jpeg',
|
|
8
|
-
'.jpeg': 'image/jpeg',
|
|
9
|
-
'.gif': 'image/gif',
|
|
10
|
-
'.webp': 'image/webp',
|
|
11
|
-
'.svg': 'image/svg+xml',
|
|
12
|
-
'.mp4': 'video/mp4',
|
|
13
|
-
'.webm': 'video/webm',
|
|
14
|
-
'.pdf': 'application/pdf',
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
function mediaExtensionPattern() {
|
|
18
|
-
return Object.keys(MIME_MAP).map((entry) => entry.replace('.', '\\.')).join('|');
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function stripUrlSuffix(value) {
|
|
22
|
-
return String(value || '').replace(/[?#].*$/, '');
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function trimCandidate(value) {
|
|
26
|
-
return String(value || '')
|
|
27
|
-
.trim()
|
|
28
|
-
.replace(/^<|>$/g, '')
|
|
29
|
-
.replace(/^["']|["']$/g, '')
|
|
30
|
-
.replace(/\\\//g, '/');
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function hasKnownMediaExtension(value) {
|
|
34
|
-
return Boolean(MIME_MAP[extname(stripUrlSuffix(value)).toLowerCase()]);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function normalizeMediaPath(value) {
|
|
38
|
-
let candidate = trimCandidate(value);
|
|
39
|
-
if (!candidate) return null;
|
|
40
|
-
|
|
41
|
-
candidate = candidate.replace(/^about:/i, '');
|
|
42
|
-
|
|
43
|
-
if (/^file:/i.test(candidate)) {
|
|
44
|
-
try {
|
|
45
|
-
candidate = fileURLToPath(candidate);
|
|
46
|
-
} catch {
|
|
47
|
-
candidate = candidate.replace(/^file:(?:\/\/)?/i, '');
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
candidate = stripUrlSuffix(candidate);
|
|
52
|
-
|
|
53
|
-
if (candidate.startsWith('~')) {
|
|
54
|
-
candidate = candidate.replace(/^~/, homedir());
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (candidate.startsWith('/')) {
|
|
58
|
-
candidate = candidate.replace(/^\/{2,}/, '/');
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (!candidate.startsWith('/')) return null;
|
|
62
|
-
if (!hasKnownMediaExtension(candidate)) return null;
|
|
63
|
-
return candidate;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function addMediaCandidate(paths, value) {
|
|
67
|
-
const normalized = normalizeMediaPath(value);
|
|
68
|
-
if (normalized) paths.add(normalized);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function extractMediaPaths(text) {
|
|
72
|
-
const extPattern = mediaExtensionPattern();
|
|
73
|
-
const paths = new Set();
|
|
74
|
-
const candidates = [];
|
|
75
|
-
|
|
76
|
-
const body = String(text || '');
|
|
77
|
-
for (const match of body.matchAll(/<img[^>]+src=["']([^"']+)["'][^>]*>/gi)) {
|
|
78
|
-
candidates.push({ index: match.index || 0, value: match[1] });
|
|
79
|
-
}
|
|
80
|
-
for (const match of body.matchAll(/!?\[[^\]]*\]\(\s*<([^>\n]+)>/gi)) {
|
|
81
|
-
candidates.push({ index: match.index || 0, value: match[1] });
|
|
82
|
-
}
|
|
83
|
-
for (const match of body.matchAll(/!?\[[^\]]*\]\(\s*([^\s)>]+)>?/gi)) {
|
|
84
|
-
candidates.push({ index: match.index || 0, value: match[1] });
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const re = new RegExp(`((?:file:(?://)?|about:(?://)?)?(?:~|/{1,3})[^\\s"'<>)]*?(?:${extPattern})(?:[?#][^\\s"'<>)]*)?)`, 'gi');
|
|
88
|
-
for (const match of body.matchAll(re)) {
|
|
89
|
-
candidates.push({ index: match.index || 0, value: match[1] });
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
candidates
|
|
93
|
-
.sort((a, b) => a.index - b.index)
|
|
94
|
-
.forEach((candidate) => addMediaCandidate(paths, candidate.value));
|
|
95
|
-
|
|
96
|
-
return [...paths];
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function escapeRegExp(value) {
|
|
100
|
-
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function referencesPath(value, normalizedPaths) {
|
|
104
|
-
const normalized = normalizeMediaPath(value);
|
|
105
|
-
return Boolean(normalized && normalizedPaths.has(normalized));
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function pathReferenceVariants(path) {
|
|
109
|
-
const normalized = normalizeMediaPath(path) || path;
|
|
110
|
-
const variants = new Set([normalized]);
|
|
111
|
-
if (normalized.startsWith('/')) {
|
|
112
|
-
variants.add(pathToFileURL(normalized).href);
|
|
113
|
-
variants.add(`about:${normalized}`);
|
|
114
|
-
variants.add(`about://${normalized}`);
|
|
115
|
-
}
|
|
116
|
-
return [...variants].sort((a, b) => b.length - a.length);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function stripMediaPathReferences(text, explicitPaths = []) {
|
|
120
|
-
let next = String(text || '');
|
|
121
|
-
const paths = [...new Set([
|
|
122
|
-
...explicitPaths,
|
|
123
|
-
...extractMediaPaths(next),
|
|
124
|
-
].map((path) => normalizeMediaPath(path)).filter(Boolean))];
|
|
125
|
-
const normalizedPaths = new Set(paths);
|
|
126
|
-
|
|
127
|
-
next = next
|
|
128
|
-
.replace(/<img[^>]+src=["']([^"']+)["'][^>]*>/gi, (match, src) => (
|
|
129
|
-
referencesPath(src, normalizedPaths) ? '' : match
|
|
130
|
-
))
|
|
131
|
-
.replace(/!\[[^\]]*\]\(\s*<([^>\n]+)>(?:\s+["'][^"']*["'])?\s*\)/gi, (match, target) => (
|
|
132
|
-
referencesPath(target, normalizedPaths) ? '' : match
|
|
133
|
-
))
|
|
134
|
-
.replace(/!\[[^\]]*\]\(\s*([^\s)>]+)>?(?:\s+["'][^"']*["'])?\s*\)/gi, (match, target) => (
|
|
135
|
-
referencesPath(target, normalizedPaths) ? '' : match
|
|
136
|
-
));
|
|
137
|
-
|
|
138
|
-
for (const path of paths) {
|
|
139
|
-
for (const variant of pathReferenceVariants(path)) {
|
|
140
|
-
const escapedPath = escapeRegExp(variant);
|
|
141
|
-
next = next
|
|
142
|
-
.replace(new RegExp(`\\[media attached[^\\]]*${escapedPath}[^\\]]*\\]\\s*`, 'gi'), '')
|
|
143
|
-
.replace(new RegExp(`^\\s*${escapedPath}\\s*$`, 'gim'), '')
|
|
144
|
-
.replace(new RegExp(escapedPath, 'g'), '');
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return next
|
|
149
|
-
.replace(/<img[^>]+src=["'](?:file:|about:)(?:\/){0,3}(?:tmp|private|var|Users|home)[^"']*["'][^>]*>/gi, '')
|
|
150
|
-
.replace(/!\[[^\]]*\]\(\s*<?(?:file:|about:)(?:\/){0,3}(?:tmp|private|var|Users|home)[^)]*\)/gi, '')
|
|
151
|
-
.replace(/\n{3,}/g, '\n\n')
|
|
152
|
-
.trim();
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export function normalizeOutboundMedia(result) {
|
|
156
|
-
const explicit = Array.isArray(result?.mediaUrls) ? result.mediaUrls : [];
|
|
157
|
-
const inferred = extractMediaPaths(result?.text || '');
|
|
158
|
-
return [...new Set([...explicit, ...inferred])].map((path) => ({
|
|
159
|
-
kind: 'local_path',
|
|
160
|
-
value: path,
|
|
161
|
-
mime: MIME_MAP[extname(path).toLowerCase()] || 'application/octet-stream',
|
|
162
|
-
}));
|
|
163
|
-
}
|