@tloncorp/openclaw 0.4.3 → 0.6.1
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 +130 -141
- package/dist/index.js +703 -152
- package/dist/index.js.map +1 -1
- package/dist/setup-api.js +2 -2
- package/dist/setup-entry.js +2 -2
- package/dist/setup-entry.js.map +1 -1
- package/dist/src/account-fields.js +7 -3
- package/dist/src/account-fields.js.map +1 -1
- package/dist/src/actions.js +73 -52
- package/dist/src/actions.js.map +1 -1
- package/dist/src/channel.js +63 -39
- package/dist/src/channel.js.map +1 -1
- package/dist/src/channel.runtime.js +61 -32
- package/dist/src/channel.runtime.js.map +1 -1
- package/dist/src/config-schema.js +24 -4
- package/dist/src/config-schema.js.map +1 -1
- package/dist/src/diagnostic-subscriptions.js +49 -0
- package/dist/src/diagnostic-subscriptions.js.map +1 -0
- package/dist/src/effective-owner.js.map +1 -1
- package/dist/src/gateway-status.js +55 -7
- package/dist/src/gateway-status.js.map +1 -1
- package/dist/src/monitor/approval.js +71 -62
- package/dist/src/monitor/approval.js.map +1 -1
- package/dist/src/monitor/command-auth.js +7 -7
- package/dist/src/monitor/command-auth.js.map +1 -1
- package/dist/src/monitor/command-bridge.js +3 -2
- package/dist/src/monitor/command-bridge.js.map +1 -1
- package/dist/src/monitor/computing-presence.js +76 -12
- package/dist/src/monitor/computing-presence.js.map +1 -1
- package/dist/src/monitor/discovery.js +16 -9
- package/dist/src/monitor/discovery.js.map +1 -1
- package/dist/src/monitor/history.js +58 -26
- package/dist/src/monitor/history.js.map +1 -1
- package/dist/src/monitor/index.js +3018 -2496
- package/dist/src/monitor/index.js.map +1 -1
- package/dist/src/monitor/media.js +106 -78
- package/dist/src/monitor/media.js.map +1 -1
- package/dist/src/monitor/nudge-runner.js +36 -27
- package/dist/src/monitor/nudge-runner.js.map +1 -1
- package/dist/src/monitor/nudge-state.js +7 -11
- package/dist/src/monitor/nudge-state.js.map +1 -1
- package/dist/src/monitor/owner-reply-persistence.js +27 -26
- package/dist/src/monitor/owner-reply-persistence.js.map +1 -1
- package/dist/src/monitor/processed-messages.js.map +1 -1
- package/dist/src/monitor/session-routing.js +261 -0
- package/dist/src/monitor/session-routing.js.map +1 -0
- package/dist/src/monitor/settings-sync.js +1 -8
- package/dist/src/monitor/settings-sync.js.map +1 -1
- package/dist/src/monitor/utils.js +77 -71
- package/dist/src/monitor/utils.js.map +1 -1
- package/dist/src/nudge-decision.js +40 -43
- package/dist/src/nudge-decision.js.map +1 -1
- package/dist/src/nudge-messages.js +9 -9
- package/dist/src/nudge-scheduler.js.map +1 -1
- package/dist/src/owner-listen-command.js +38 -28
- package/dist/src/owner-listen-command.js.map +1 -1
- package/dist/src/pending-nudge.js.map +1 -1
- package/dist/src/runtime.js +10 -6
- package/dist/src/runtime.js.map +1 -1
- package/dist/src/session-roles.js +2 -1
- package/dist/src/session-roles.js.map +1 -1
- package/dist/src/session-route.js +44 -0
- package/dist/src/session-route.js.map +1 -0
- package/dist/src/settings.js +233 -102
- package/dist/src/settings.js.map +1 -1
- package/dist/src/setup-core.js +32 -32
- package/dist/src/setup-core.js.map +1 -1
- package/dist/src/setup-surface.js +19 -19
- package/dist/src/setup-surface.js.map +1 -1
- package/dist/src/shared-state.js +46 -0
- package/dist/src/shared-state.js.map +1 -0
- package/dist/src/targets.js +17 -10
- package/dist/src/targets.js.map +1 -1
- package/dist/src/telemetry.js +764 -34
- package/dist/src/telemetry.js.map +1 -1
- package/dist/src/tlon-binary.js +20 -12
- package/dist/src/tlon-binary.js.map +1 -1
- package/dist/src/tlon-tool-guard.js +5 -5
- package/dist/src/tool-trace.js +17 -13
- package/dist/src/tool-trace.js.map +1 -1
- package/dist/src/types.js +30 -12
- package/dist/src/types.js.map +1 -1
- package/dist/src/urbit/api-client.js +16 -12
- package/dist/src/urbit/api-client.js.map +1 -1
- package/dist/src/urbit/auth.js +9 -9
- package/dist/src/urbit/auth.js.map +1 -1
- package/dist/src/urbit/base-url.js +11 -11
- package/dist/src/urbit/base-url.js.map +1 -1
- package/dist/src/urbit/channel-ops.js +25 -19
- package/dist/src/urbit/channel-ops.js.map +1 -1
- package/dist/src/urbit/context.js +8 -8
- package/dist/src/urbit/context.js.map +1 -1
- package/dist/src/urbit/errors.js +33 -7
- package/dist/src/urbit/errors.js.map +1 -1
- package/dist/src/urbit/fetch.js +3 -3
- package/dist/src/urbit/fetch.js.map +1 -1
- package/dist/src/urbit/http-poke.js +10 -10
- package/dist/src/urbit/http-poke.js.map +1 -1
- package/dist/src/urbit/send.js +27 -23
- package/dist/src/urbit/send.js.map +1 -1
- package/dist/src/urbit/sse-client.js +45 -41
- package/dist/src/urbit/sse-client.js.map +1 -1
- package/dist/src/urbit/story.js +31 -30
- package/dist/src/urbit/story.js.map +1 -1
- package/dist/src/urbit/upload.js +8 -8
- package/dist/src/urbit/upload.js.map +1 -1
- package/dist/src/version.generated.js +2 -1
- package/dist/src/version.generated.js.map +1 -1
- package/dist/src/version.js +134 -0
- package/dist/src/version.js.map +1 -0
- package/openclaw.plugin.json +37 -0
- package/package.json +9 -15
package/dist/index.js
CHANGED
|
@@ -1,59 +1,54 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { defineChannelPluginEntry, } from 'openclaw/plugin-sdk/core';
|
|
6
|
+
import { onDiagnosticEvent, onInternalDiagnosticEvent, } from 'openclaw/plugin-sdk/diagnostic-runtime';
|
|
7
|
+
import { tlonPlugin } from './src/channel.js';
|
|
8
|
+
import { installTlonDiagnosticSubscriptions, shouldInstallTlonDiagnosticSubscriptions, } from './src/diagnostic-subscriptions.js';
|
|
9
|
+
import { sendGatewayStop } from './src/gateway-status.js';
|
|
10
|
+
import { createGatewayStatusManager, setGatewayStatusManager, } from './src/gateway-status.js';
|
|
11
|
+
import { resolveBridgeForCommand } from './src/monitor/command-auth.js';
|
|
12
|
+
import { isRouteDebugEnabled } from './src/monitor/session-routing.js';
|
|
13
|
+
import { handleOwnerListenCommand } from './src/owner-listen-command.js';
|
|
14
|
+
import { setTlonRuntime } from './src/runtime.js';
|
|
15
|
+
import { getSessionRole } from './src/session-roles.js';
|
|
16
|
+
import { parseTlonTarget } from './src/targets.js';
|
|
17
|
+
import { formatTlonTelemetryErrorText, recordToolCall, reportHarnessError, reportOutboundRoute, reportPluginError, reportSessionDiagnostic, reportSessionLifecycle, reportSessionTurnCreated, reportTelemetryError, } from './src/telemetry.js';
|
|
18
|
+
import { resolveTlonBinary } from './src/tlon-binary.js';
|
|
19
|
+
import { checkBlockedSendOperation } from './src/tlon-tool-guard.js';
|
|
20
|
+
import { formatToolTraceEvent, liveToolTraceContentsEnabled, shouldLogAfterToolTrace, } from './src/tool-trace.js';
|
|
21
|
+
import { listTlonAccountIds, resolveTlonAccount } from './src/types.js';
|
|
22
|
+
import { formatTlonVersionIdentity, resolveTlonSkillVersion, setTlonSkillVersionResolver, } from './src/version.js';
|
|
23
|
+
export { tlonPlugin } from './src/channel.js';
|
|
24
|
+
export { setTlonRuntime } from './src/runtime.js';
|
|
21
25
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
22
26
|
const require = createRequire(import.meta.url);
|
|
23
|
-
function readPluginVersion() {
|
|
24
|
-
try {
|
|
25
|
-
const { version } = require("./package.json");
|
|
26
|
-
return version;
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
try {
|
|
30
|
-
const raw = readFileSync(new URL("./package.json", import.meta.url), "utf-8");
|
|
31
|
-
return JSON.parse(raw).version;
|
|
32
|
-
}
|
|
33
|
-
catch {
|
|
34
|
-
return "unknown";
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
27
|
// Whitelist of allowed tlon subcommands
|
|
39
28
|
const ALLOWED_TLON_COMMANDS = new Set([
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
29
|
+
'activity',
|
|
30
|
+
'channels',
|
|
31
|
+
'contacts',
|
|
32
|
+
'dms',
|
|
33
|
+
'expose',
|
|
34
|
+
'groups',
|
|
35
|
+
'hooks',
|
|
36
|
+
'messages',
|
|
37
|
+
'notebook',
|
|
38
|
+
'posts',
|
|
39
|
+
'settings',
|
|
40
|
+
'upload',
|
|
41
|
+
'help',
|
|
42
|
+
'version',
|
|
54
43
|
]);
|
|
55
44
|
/** Credential flags that the tlon skill binary accepts before the subcommand. */
|
|
56
|
-
const CREDENTIAL_FLAGS_WITH_VALUE = new Set([
|
|
45
|
+
const CREDENTIAL_FLAGS_WITH_VALUE = new Set([
|
|
46
|
+
'--config',
|
|
47
|
+
'--url',
|
|
48
|
+
'--ship',
|
|
49
|
+
'--code',
|
|
50
|
+
'--cookie',
|
|
51
|
+
]);
|
|
57
52
|
/**
|
|
58
53
|
* Find the first positional argument (subcommand) by skipping credential flags
|
|
59
54
|
* and their values. Returns the index into `args`, or -1 if none found.
|
|
@@ -63,8 +58,8 @@ function findSubcommandIndex(args) {
|
|
|
63
58
|
while (i < args.length) {
|
|
64
59
|
const arg = args[i];
|
|
65
60
|
// --flag=value form: skip one token
|
|
66
|
-
if (arg.startsWith(
|
|
67
|
-
const flag = arg.slice(0, arg.indexOf(
|
|
61
|
+
if (arg.startsWith('--') && arg.includes('=')) {
|
|
62
|
+
const flag = arg.slice(0, arg.indexOf('='));
|
|
68
63
|
if (CREDENTIAL_FLAGS_WITH_VALUE.has(flag)) {
|
|
69
64
|
i += 1;
|
|
70
65
|
continue;
|
|
@@ -85,7 +80,7 @@ function findSubcommandIndex(args) {
|
|
|
85
80
|
*/
|
|
86
81
|
function shellSplit(str) {
|
|
87
82
|
const args = [];
|
|
88
|
-
let cur =
|
|
83
|
+
let cur = '';
|
|
89
84
|
let inDouble = false;
|
|
90
85
|
let inSingle = false;
|
|
91
86
|
let escape = false;
|
|
@@ -95,7 +90,7 @@ function shellSplit(str) {
|
|
|
95
90
|
escape = false;
|
|
96
91
|
continue;
|
|
97
92
|
}
|
|
98
|
-
if (ch ===
|
|
93
|
+
if (ch === '\\' && !inSingle) {
|
|
99
94
|
escape = true;
|
|
100
95
|
continue;
|
|
101
96
|
}
|
|
@@ -110,7 +105,7 @@ function shellSplit(str) {
|
|
|
110
105
|
if (/\s/.test(ch) && !inDouble && !inSingle) {
|
|
111
106
|
if (cur) {
|
|
112
107
|
args.push(cur);
|
|
113
|
-
cur =
|
|
108
|
+
cur = '';
|
|
114
109
|
}
|
|
115
110
|
continue;
|
|
116
111
|
}
|
|
@@ -124,7 +119,7 @@ function shellSplit(str) {
|
|
|
124
119
|
/**
|
|
125
120
|
* Run the tlon command and return the result
|
|
126
121
|
*/
|
|
127
|
-
function runTlonCommand(binary, args, credentials) {
|
|
122
|
+
function runTlonCommand(binary, args, credentials, options) {
|
|
128
123
|
return new Promise((resolve, reject) => {
|
|
129
124
|
const env = { ...process.env };
|
|
130
125
|
if (credentials) {
|
|
@@ -133,19 +128,39 @@ function runTlonCommand(binary, args, credentials) {
|
|
|
133
128
|
env.URBIT_CODE = credentials.code;
|
|
134
129
|
}
|
|
135
130
|
const child = spawn(binary, args, { env });
|
|
136
|
-
let stdout =
|
|
137
|
-
let stderr =
|
|
138
|
-
|
|
131
|
+
let stdout = '';
|
|
132
|
+
let stderr = '';
|
|
133
|
+
let timedOut = false;
|
|
134
|
+
let timeout;
|
|
135
|
+
const timeoutMs = options?.timeoutMs;
|
|
136
|
+
const cleanup = () => {
|
|
137
|
+
if (timeout) {
|
|
138
|
+
clearTimeout(timeout);
|
|
139
|
+
timeout = undefined;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
child.stdout.on('data', (data) => {
|
|
139
143
|
stdout += data.toString();
|
|
140
144
|
});
|
|
141
|
-
child.stderr.on(
|
|
145
|
+
child.stderr.on('data', (data) => {
|
|
142
146
|
stderr += data.toString();
|
|
143
147
|
});
|
|
144
|
-
child.on(
|
|
148
|
+
child.on('error', (err) => {
|
|
149
|
+
cleanup();
|
|
145
150
|
reject(new Error(`Failed to run tlon: ${err.message}`));
|
|
146
151
|
});
|
|
147
|
-
|
|
148
|
-
|
|
152
|
+
if (timeoutMs) {
|
|
153
|
+
timeout = setTimeout(() => {
|
|
154
|
+
timedOut = true;
|
|
155
|
+
child.kill('SIGTERM');
|
|
156
|
+
}, timeoutMs);
|
|
157
|
+
}
|
|
158
|
+
child.on('close', (code) => {
|
|
159
|
+
cleanup();
|
|
160
|
+
if (timedOut) {
|
|
161
|
+
reject(new Error(`tlon timed out after ${timeoutMs}ms`));
|
|
162
|
+
}
|
|
163
|
+
else if (code !== 0) {
|
|
149
164
|
reject(new Error(stderr || `tlon exited with code ${code}`));
|
|
150
165
|
}
|
|
151
166
|
else {
|
|
@@ -154,29 +169,388 @@ function runTlonCommand(binary, args, credentials) {
|
|
|
154
169
|
});
|
|
155
170
|
});
|
|
156
171
|
}
|
|
172
|
+
function firstLine(value) {
|
|
173
|
+
return value.trim().split(/\r?\n/)[0]?.trim() || 'unknown';
|
|
174
|
+
}
|
|
175
|
+
function summarizeError(error) {
|
|
176
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
177
|
+
return firstLine(message).slice(0, 180);
|
|
178
|
+
}
|
|
179
|
+
async function readTlonSkillVersion(binary) {
|
|
180
|
+
try {
|
|
181
|
+
return firstLine(await runTlonCommand(binary, ['--version'], undefined, {
|
|
182
|
+
timeoutMs: 5_000,
|
|
183
|
+
}));
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
return `unavailable (${summarizeError(error)})`;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function isTlonSessionDiagnosticEvent(event) {
|
|
190
|
+
return (event.type === 'session.stalled' ||
|
|
191
|
+
event.type === 'session.stuck' ||
|
|
192
|
+
event.type === 'session.recovery.requested' ||
|
|
193
|
+
event.type === 'session.recovery.completed');
|
|
194
|
+
}
|
|
195
|
+
function stringField(event, key) {
|
|
196
|
+
const value = event[key];
|
|
197
|
+
return typeof value === 'string' && value.trim() ? value : null;
|
|
198
|
+
}
|
|
199
|
+
function numberField(event, key) {
|
|
200
|
+
const value = event[key];
|
|
201
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
202
|
+
}
|
|
203
|
+
function diagnosticErrorText(event) {
|
|
204
|
+
return stringField(event, 'error') ?? stringField(event, 'message');
|
|
205
|
+
}
|
|
206
|
+
function stringListField(event, key) {
|
|
207
|
+
const value = event[key];
|
|
208
|
+
if (!Array.isArray(value)) {
|
|
209
|
+
return [];
|
|
210
|
+
}
|
|
211
|
+
return value
|
|
212
|
+
.map((entry) => (typeof entry === 'string' ? entry.trim() : ''))
|
|
213
|
+
.filter(Boolean);
|
|
214
|
+
}
|
|
215
|
+
function diagnosticSummary(parts) {
|
|
216
|
+
return parts
|
|
217
|
+
.filter(([, value]) => value !== null && value !== undefined && value !== '')
|
|
218
|
+
.map(([key, value]) => `${key}=${String(value)}`)
|
|
219
|
+
.join(' ');
|
|
220
|
+
}
|
|
221
|
+
function reportHarnessDiagnostic(event) {
|
|
222
|
+
const type = stringField(event, 'type');
|
|
223
|
+
if (!type) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (type === 'session.turn.created') {
|
|
227
|
+
reportSessionTurnCreated({
|
|
228
|
+
type,
|
|
229
|
+
sessionKey: stringField(event, 'sessionKey'),
|
|
230
|
+
sessionId: stringField(event, 'sessionId'),
|
|
231
|
+
runId: stringField(event, 'runId'),
|
|
232
|
+
agentId: stringField(event, 'agentId'),
|
|
233
|
+
});
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const common = {
|
|
237
|
+
harnessEventType: type,
|
|
238
|
+
sessionKey: stringField(event, 'sessionKey'),
|
|
239
|
+
sessionId: stringField(event, 'sessionId'),
|
|
240
|
+
runId: stringField(event, 'runId'),
|
|
241
|
+
agentId: stringField(event, 'agentId'),
|
|
242
|
+
provider: stringField(event, 'provider'),
|
|
243
|
+
model: stringField(event, 'model'),
|
|
244
|
+
phase: stringField(event, 'phase'),
|
|
245
|
+
outcome: stringField(event, 'outcome'),
|
|
246
|
+
errorCategory: stringField(event, 'errorCategory'),
|
|
247
|
+
failureKind: stringField(event, 'failureKind'),
|
|
248
|
+
durationMs: numberField(event, 'durationMs'),
|
|
249
|
+
errorText: diagnosticErrorText(event),
|
|
250
|
+
};
|
|
251
|
+
switch (type) {
|
|
252
|
+
case 'harness.run.error':
|
|
253
|
+
reportHarnessError({
|
|
254
|
+
...common,
|
|
255
|
+
errorScope: 'harness',
|
|
256
|
+
});
|
|
257
|
+
return;
|
|
258
|
+
case 'harness.run.completed':
|
|
259
|
+
if (common.outcome === 'completed') {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
reportHarnessError({
|
|
263
|
+
...common,
|
|
264
|
+
errorScope: 'harness',
|
|
265
|
+
});
|
|
266
|
+
return;
|
|
267
|
+
case 'model.call.error':
|
|
268
|
+
reportHarnessError({
|
|
269
|
+
...common,
|
|
270
|
+
errorScope: 'model',
|
|
271
|
+
});
|
|
272
|
+
return;
|
|
273
|
+
case 'model.failover': {
|
|
274
|
+
const reason = stringField(event, 'reason');
|
|
275
|
+
const fromProvider = stringField(event, 'fromProvider');
|
|
276
|
+
const fromModel = stringField(event, 'fromModel');
|
|
277
|
+
const toProvider = stringField(event, 'toProvider');
|
|
278
|
+
const toModel = stringField(event, 'toModel');
|
|
279
|
+
reportHarnessError({
|
|
280
|
+
...common,
|
|
281
|
+
errorScope: 'model',
|
|
282
|
+
provider: fromProvider,
|
|
283
|
+
model: fromModel,
|
|
284
|
+
phase: stringField(event, 'lane'),
|
|
285
|
+
outcome: 'failover',
|
|
286
|
+
errorCategory: 'model_failover',
|
|
287
|
+
failureKind: reason,
|
|
288
|
+
errorText: diagnosticSummary([
|
|
289
|
+
['fromProvider', fromProvider],
|
|
290
|
+
['fromModel', fromModel],
|
|
291
|
+
['toProvider', toProvider],
|
|
292
|
+
['toModel', toModel],
|
|
293
|
+
['reason', reason],
|
|
294
|
+
['cascadeDepth', numberField(event, 'cascadeDepth')],
|
|
295
|
+
]),
|
|
296
|
+
});
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
case 'tool.execution.error':
|
|
300
|
+
reportHarnessError({
|
|
301
|
+
...common,
|
|
302
|
+
errorScope: 'tool',
|
|
303
|
+
toolName: stringField(event, 'toolName'),
|
|
304
|
+
});
|
|
305
|
+
return;
|
|
306
|
+
case 'tool.execution.blocked': {
|
|
307
|
+
const deniedReason = stringField(event, 'deniedReason');
|
|
308
|
+
const reason = stringField(event, 'reason');
|
|
309
|
+
reportHarnessError({
|
|
310
|
+
...common,
|
|
311
|
+
errorScope: 'tool',
|
|
312
|
+
toolName: stringField(event, 'toolName'),
|
|
313
|
+
phase: stringField(event, 'toolSource'),
|
|
314
|
+
outcome: 'blocked',
|
|
315
|
+
errorCategory: 'tool_blocked',
|
|
316
|
+
failureKind: deniedReason,
|
|
317
|
+
errorText: reason ?? deniedReason,
|
|
318
|
+
});
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
case 'tool.loop': {
|
|
322
|
+
const level = stringField(event, 'level');
|
|
323
|
+
const action = stringField(event, 'action');
|
|
324
|
+
if (level !== 'critical' && action !== 'block') {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
reportHarnessError({
|
|
328
|
+
...common,
|
|
329
|
+
errorScope: 'tool',
|
|
330
|
+
toolName: stringField(event, 'toolName'),
|
|
331
|
+
phase: level,
|
|
332
|
+
outcome: action,
|
|
333
|
+
errorCategory: 'tool_loop',
|
|
334
|
+
failureKind: stringField(event, 'detector'),
|
|
335
|
+
errorText: stringField(event, 'message') ??
|
|
336
|
+
diagnosticSummary([
|
|
337
|
+
['level', level],
|
|
338
|
+
['action', action],
|
|
339
|
+
['detector', stringField(event, 'detector')],
|
|
340
|
+
['count', numberField(event, 'count')],
|
|
341
|
+
]),
|
|
342
|
+
});
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
case 'run.completed':
|
|
346
|
+
if (common.outcome === 'completed') {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
reportHarnessError({
|
|
350
|
+
...common,
|
|
351
|
+
errorScope: 'run',
|
|
352
|
+
});
|
|
353
|
+
return;
|
|
354
|
+
case 'message.delivery.error':
|
|
355
|
+
reportHarnessError({
|
|
356
|
+
...common,
|
|
357
|
+
errorScope: 'message_delivery',
|
|
358
|
+
phase: stringField(event, 'deliveryKind'),
|
|
359
|
+
});
|
|
360
|
+
return;
|
|
361
|
+
case 'message.dispatch.completed':
|
|
362
|
+
if (common.outcome !== 'error') {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
reportHarnessError({
|
|
366
|
+
...common,
|
|
367
|
+
errorScope: 'message_dispatch',
|
|
368
|
+
phase: stringField(event, 'source'),
|
|
369
|
+
});
|
|
370
|
+
return;
|
|
371
|
+
case 'message.processed':
|
|
372
|
+
if (common.outcome !== 'error') {
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
reportHarnessError({
|
|
376
|
+
...common,
|
|
377
|
+
errorScope: 'message_processing',
|
|
378
|
+
phase: stringField(event, 'channel'),
|
|
379
|
+
});
|
|
380
|
+
return;
|
|
381
|
+
case 'diagnostic.async_queue.dropped':
|
|
382
|
+
reportHarnessError({
|
|
383
|
+
...common,
|
|
384
|
+
errorScope: 'diagnostics',
|
|
385
|
+
outcome: 'dropped',
|
|
386
|
+
errorCategory: 'diagnostic_async_queue_dropped',
|
|
387
|
+
failureKind: 'queue_full',
|
|
388
|
+
errorText: diagnosticSummary([
|
|
389
|
+
['droppedEvents', numberField(event, 'droppedEvents')],
|
|
390
|
+
['droppedTrustedEvents', numberField(event, 'droppedTrustedEvents')],
|
|
391
|
+
[
|
|
392
|
+
'droppedUntrustedEvents',
|
|
393
|
+
numberField(event, 'droppedUntrustedEvents'),
|
|
394
|
+
],
|
|
395
|
+
['queueLength', numberField(event, 'queueLength')],
|
|
396
|
+
['maxQueueLength', numberField(event, 'maxQueueLength')],
|
|
397
|
+
]),
|
|
398
|
+
});
|
|
399
|
+
return;
|
|
400
|
+
case 'diagnostic.liveness.warning': {
|
|
401
|
+
const reasons = stringListField(event, 'reasons');
|
|
402
|
+
reportHarnessError({
|
|
403
|
+
...common,
|
|
404
|
+
errorScope: 'runtime',
|
|
405
|
+
phase: stringField(event, 'phase'),
|
|
406
|
+
outcome: 'warning',
|
|
407
|
+
errorCategory: 'liveness_warning',
|
|
408
|
+
failureKind: reasons.join(',') || null,
|
|
409
|
+
durationMs: numberField(event, 'intervalMs'),
|
|
410
|
+
errorText: diagnosticSummary([
|
|
411
|
+
['reasons', reasons.join(',')],
|
|
412
|
+
['eventLoopDelayP99Ms', numberField(event, 'eventLoopDelayP99Ms')],
|
|
413
|
+
['eventLoopDelayMaxMs', numberField(event, 'eventLoopDelayMaxMs')],
|
|
414
|
+
['cpuCoreRatio', numberField(event, 'cpuCoreRatio')],
|
|
415
|
+
['active', numberField(event, 'active')],
|
|
416
|
+
['waiting', numberField(event, 'waiting')],
|
|
417
|
+
['queued', numberField(event, 'queued')],
|
|
418
|
+
]),
|
|
419
|
+
});
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
case 'diagnostic.memory.pressure': {
|
|
423
|
+
const memory = event.memory;
|
|
424
|
+
const memoryNumber = (key) => {
|
|
425
|
+
const value = memory?.[key];
|
|
426
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
427
|
+
? value
|
|
428
|
+
: null;
|
|
429
|
+
};
|
|
430
|
+
reportHarnessError({
|
|
431
|
+
...common,
|
|
432
|
+
errorScope: 'runtime',
|
|
433
|
+
outcome: stringField(event, 'level'),
|
|
434
|
+
errorCategory: 'memory_pressure',
|
|
435
|
+
failureKind: stringField(event, 'reason'),
|
|
436
|
+
durationMs: numberField(event, 'windowMs'),
|
|
437
|
+
errorText: diagnosticSummary([
|
|
438
|
+
['level', stringField(event, 'level')],
|
|
439
|
+
['reason', stringField(event, 'reason')],
|
|
440
|
+
['rssBytes', memoryNumber('rssBytes')],
|
|
441
|
+
['heapUsedBytes', memoryNumber('heapUsedBytes')],
|
|
442
|
+
['thresholdBytes', numberField(event, 'thresholdBytes')],
|
|
443
|
+
['rssGrowthBytes', numberField(event, 'rssGrowthBytes')],
|
|
444
|
+
]),
|
|
445
|
+
});
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
case 'payload.large':
|
|
449
|
+
if (stringField(event, 'action') !== 'rejected') {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
reportHarnessError({
|
|
453
|
+
...common,
|
|
454
|
+
errorScope: 'payload',
|
|
455
|
+
phase: stringField(event, 'surface'),
|
|
456
|
+
outcome: 'rejected',
|
|
457
|
+
errorCategory: 'payload_large',
|
|
458
|
+
failureKind: stringField(event, 'reason'),
|
|
459
|
+
errorText: diagnosticSummary([
|
|
460
|
+
['surface', stringField(event, 'surface')],
|
|
461
|
+
['channel', stringField(event, 'channel')],
|
|
462
|
+
['pluginId', stringField(event, 'pluginId')],
|
|
463
|
+
['bytes', numberField(event, 'bytes')],
|
|
464
|
+
['limitBytes', numberField(event, 'limitBytes')],
|
|
465
|
+
['count', numberField(event, 'count')],
|
|
466
|
+
['reason', stringField(event, 'reason')],
|
|
467
|
+
]),
|
|
468
|
+
});
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
function safeTelemetryObserver(params) {
|
|
473
|
+
try {
|
|
474
|
+
params.run();
|
|
475
|
+
}
|
|
476
|
+
catch (error) {
|
|
477
|
+
params.logger.warn(`[tlon] Telemetry observer failed (${params.telemetrySource}${params.sourceEventName ? `:${params.sourceEventName}` : ''}): ${String(error)}`);
|
|
478
|
+
try {
|
|
479
|
+
reportTelemetryError({
|
|
480
|
+
telemetrySource: params.telemetrySource,
|
|
481
|
+
sourceEventName: params.sourceEventName,
|
|
482
|
+
sessionKey: params.sessionKey,
|
|
483
|
+
sessionId: params.sessionId,
|
|
484
|
+
runId: params.runId,
|
|
485
|
+
agentId: params.agentId,
|
|
486
|
+
errorKind: error instanceof Error ? error.name : typeof error,
|
|
487
|
+
errorText: formatTlonTelemetryErrorText(error),
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
catch (reportError) {
|
|
491
|
+
params.logger.warn(`[tlon] Telemetry error reporting failed: ${String(reportError)}`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
function installTelemetryDiagnosticObservers(api) {
|
|
496
|
+
return installTlonDiagnosticSubscriptions(() => {
|
|
497
|
+
const unsubscribeDiagnosticEvents = onDiagnosticEvent((event) => {
|
|
498
|
+
const candidate = event;
|
|
499
|
+
safeTelemetryObserver({
|
|
500
|
+
logger: api.logger,
|
|
501
|
+
telemetrySource: 'diagnostic_session',
|
|
502
|
+
sourceEventName: candidate.type,
|
|
503
|
+
sessionKey: candidate.sessionKey,
|
|
504
|
+
sessionId: candidate.sessionId,
|
|
505
|
+
run: () => {
|
|
506
|
+
if (isTlonSessionDiagnosticEvent(candidate)) {
|
|
507
|
+
reportSessionDiagnostic(candidate);
|
|
508
|
+
}
|
|
509
|
+
},
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
const unsubscribeInternalDiagnosticEvents = onInternalDiagnosticEvent((event) => {
|
|
513
|
+
const candidate = event;
|
|
514
|
+
safeTelemetryObserver({
|
|
515
|
+
logger: api.logger,
|
|
516
|
+
telemetrySource: 'diagnostic_internal',
|
|
517
|
+
sourceEventName: stringField(candidate, 'type'),
|
|
518
|
+
sessionKey: stringField(candidate, 'sessionKey'),
|
|
519
|
+
sessionId: stringField(candidate, 'sessionId'),
|
|
520
|
+
runId: stringField(candidate, 'runId'),
|
|
521
|
+
agentId: stringField(candidate, 'agentId'),
|
|
522
|
+
run: () => reportHarnessDiagnostic(candidate),
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
return () => {
|
|
526
|
+
unsubscribeDiagnosticEvents();
|
|
527
|
+
unsubscribeInternalDiagnosticEvents();
|
|
528
|
+
};
|
|
529
|
+
});
|
|
530
|
+
}
|
|
157
531
|
export default defineChannelPluginEntry({
|
|
158
|
-
id:
|
|
159
|
-
name:
|
|
160
|
-
description:
|
|
532
|
+
id: 'tlon',
|
|
533
|
+
name: 'Tlon',
|
|
534
|
+
description: 'Tlon/Urbit channel plugin',
|
|
161
535
|
plugin: tlonPlugin,
|
|
162
536
|
setRuntime: setTlonRuntime,
|
|
163
537
|
registerFull(api) {
|
|
164
|
-
// Import version info lazily
|
|
165
|
-
const PLUGIN_VERSION = readPluginVersion();
|
|
166
|
-
let PLUGIN_COMMIT = "unknown";
|
|
167
|
-
try {
|
|
168
|
-
PLUGIN_COMMIT = require("./src/version.generated.js")
|
|
169
|
-
.PLUGIN_COMMIT;
|
|
170
|
-
}
|
|
171
|
-
catch {
|
|
172
|
-
// version.generated.js may not exist in all environments
|
|
173
|
-
}
|
|
174
538
|
// ── Gateway-status liveness integration ───────────────────
|
|
175
539
|
//
|
|
176
540
|
// v1 requires exactly one Tlon account. With multiple accounts, multiple
|
|
177
|
-
// monitors call configureTlonApiWithPoke() and the last one wins the
|
|
178
|
-
// @tloncorp/api singleton — making it unsafe to route heartbeats or
|
|
179
|
-
// pokes to a specific ship. Disable entirely rather than route to the
|
|
541
|
+
// monitors call configureTlonApiWithPoke() and the last one wins the
|
|
542
|
+
// global @tloncorp/api singleton — making it unsafe to route heartbeats or
|
|
543
|
+
// stop pokes to a specific ship. Disable entirely rather than route to the
|
|
544
|
+
// wrong ship.
|
|
545
|
+
//
|
|
546
|
+
// We count ALL configured account entries (not just currently-runnable
|
|
547
|
+
// ones) on purpose. The manager is a process-lifetime singleton created
|
|
548
|
+
// here in registerFull, which does NOT re-run on config reload. If we
|
|
549
|
+
// counted only runnable accounts, a config of one complete account plus a
|
|
550
|
+
// disabled/unconfigured stub would enable the singleton, and later
|
|
551
|
+
// completing the stub would start a second monitor that races the shared
|
|
552
|
+
// API slot — without registerFull re-evaluating the gate. Counting every
|
|
553
|
+
// entry keeps the feature off whenever a second account exists at all.
|
|
180
554
|
const gsAccountIds = listTlonAccountIds(api.config);
|
|
181
555
|
setGatewayStatusManager(null);
|
|
182
556
|
if (gsAccountIds.length > 1) {
|
|
@@ -187,26 +561,49 @@ export default defineChannelPluginEntry({
|
|
|
187
561
|
const gsManager = createGatewayStatusManager({
|
|
188
562
|
logger: {
|
|
189
563
|
log: (m) => api.logger.info(m),
|
|
190
|
-
error: (m) =>
|
|
564
|
+
error: (m) => {
|
|
565
|
+
reportPluginError({
|
|
566
|
+
pluginErrorSource: 'gateway_status_heartbeat',
|
|
567
|
+
errorKind: 'heartbeat',
|
|
568
|
+
errorText: m,
|
|
569
|
+
});
|
|
570
|
+
api.logger.warn(m);
|
|
571
|
+
},
|
|
191
572
|
},
|
|
192
573
|
});
|
|
193
574
|
setGatewayStatusManager(gsManager);
|
|
194
|
-
api.on(
|
|
575
|
+
api.on('gateway_start', () => {
|
|
195
576
|
gsManager.signalGatewayStarted();
|
|
196
|
-
api.logger.info(
|
|
577
|
+
api.logger.info('[gateway-status] gateway_start received');
|
|
197
578
|
});
|
|
198
|
-
api.on(
|
|
199
|
-
if (
|
|
579
|
+
api.on('gateway_stop', async (event) => {
|
|
580
|
+
if (gsManager.stopped) {
|
|
200
581
|
return;
|
|
201
582
|
}
|
|
583
|
+
// Latch stopped FIRST, unconditionally. An activation task may be
|
|
584
|
+
// in flight (between the %gateway-start poke and markActivated());
|
|
585
|
+
// latching here makes its post-poke recheck bail so it can't start a
|
|
586
|
+
// heartbeat after we've already passed the shutdown hook.
|
|
587
|
+
const startPokeInFlightOrDone = gsManager.activated || gsManager.starting;
|
|
202
588
|
gsManager.stopHeartbeat();
|
|
203
589
|
gsManager.markStopped();
|
|
590
|
+
// Only send %gateway-stop if a %gateway-start has been or is being
|
|
591
|
+
// sent. If activation never reached the start poke, there is nothing
|
|
592
|
+
// for the ship to stop.
|
|
593
|
+
if (!startPokeInFlightOrDone) {
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
204
596
|
try {
|
|
205
|
-
await
|
|
597
|
+
const sent = await sendGatewayStop({
|
|
206
598
|
bootId: gsManager.bootId,
|
|
207
|
-
reason: event.reason ??
|
|
599
|
+
reason: event.reason ?? 'shutdown',
|
|
208
600
|
});
|
|
209
|
-
|
|
601
|
+
if (sent) {
|
|
602
|
+
api.logger.info(`[gateway-status] stopped (reason=${event.reason ?? 'shutdown'})`);
|
|
603
|
+
}
|
|
604
|
+
else {
|
|
605
|
+
api.logger.warn('[gateway-status] stop skipped: api-client params not published');
|
|
606
|
+
}
|
|
210
607
|
}
|
|
211
608
|
catch (err) {
|
|
212
609
|
api.logger.warn(`[gateway-status] stop poke failed: ${String(err)}`);
|
|
@@ -214,21 +611,49 @@ export default defineChannelPluginEntry({
|
|
|
214
611
|
});
|
|
215
612
|
}
|
|
216
613
|
// else: zero accounts configured — nothing to do
|
|
217
|
-
//
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
description: "Show Tlon plugin version.",
|
|
221
|
-
handler: async () => {
|
|
222
|
-
return { text: `Tlon plugin v${PLUGIN_VERSION} (${PLUGIN_COMMIT})` };
|
|
223
|
-
},
|
|
224
|
-
});
|
|
225
|
-
// Register the tlon tool
|
|
614
|
+
// Resolve the tlon tool binary once. The tool itself and version
|
|
615
|
+
// diagnostics share this path so telemetry reports what OpenClaw will
|
|
616
|
+
// actually execute.
|
|
226
617
|
const tlonBinary = resolveTlonBinary({
|
|
227
618
|
moduleDir: __dirname,
|
|
228
619
|
resolveModule: require.resolve,
|
|
229
620
|
log: (msg) => api.logger.debug?.(msg),
|
|
230
621
|
});
|
|
231
622
|
api.logger.info(`[tlon] Registering tlon tool, binary: ${tlonBinary}`);
|
|
623
|
+
setTlonSkillVersionResolver(() => readTlonSkillVersion(tlonBinary));
|
|
624
|
+
const renderTlonVersion = async () => ({
|
|
625
|
+
text: formatTlonVersionIdentity({
|
|
626
|
+
tlonSkillVersion: await resolveTlonSkillVersion(),
|
|
627
|
+
}),
|
|
628
|
+
});
|
|
629
|
+
void resolveTlonSkillVersion().then((version) => {
|
|
630
|
+
api.logger.info(`[tlon] Tlon skill version: ${version}`);
|
|
631
|
+
});
|
|
632
|
+
// Register /tlon-version command
|
|
633
|
+
api.registerCommand({
|
|
634
|
+
name: 'tlon-version',
|
|
635
|
+
description: 'Show Tlon plugin version.',
|
|
636
|
+
handler: async () => {
|
|
637
|
+
return renderTlonVersion();
|
|
638
|
+
},
|
|
639
|
+
});
|
|
640
|
+
api.registerCommand({
|
|
641
|
+
name: 'tlon',
|
|
642
|
+
description: 'Tlon plugin diagnostics. Usage: /tlon version',
|
|
643
|
+
acceptsArgs: true,
|
|
644
|
+
handler: async (ctx) => {
|
|
645
|
+
const args = (ctx.args ?? '').trim().toLowerCase();
|
|
646
|
+
if (args !== 'version') {
|
|
647
|
+
return { text: 'Usage: /tlon version' };
|
|
648
|
+
}
|
|
649
|
+
const result = resolveBridgeForCommand(ctx);
|
|
650
|
+
if ('error' in result) {
|
|
651
|
+
return { text: result.error };
|
|
652
|
+
}
|
|
653
|
+
return renderTlonVersion();
|
|
654
|
+
},
|
|
655
|
+
});
|
|
656
|
+
// Register the tlon tool
|
|
232
657
|
// Capture credentials from config at registration time
|
|
233
658
|
const account = resolveTlonAccount(api.config);
|
|
234
659
|
const credentials = account.configured && account.url && account.ship && account.code
|
|
@@ -241,22 +666,22 @@ export default defineChannelPluginEntry({
|
|
|
241
666
|
api.logger.warn(`[tlon] No credentials configured - tlon tool will rely on env vars`);
|
|
242
667
|
}
|
|
243
668
|
api.registerTool({
|
|
244
|
-
name:
|
|
245
|
-
label:
|
|
246
|
-
description:
|
|
247
|
-
|
|
669
|
+
name: 'tlon',
|
|
670
|
+
label: 'Tlon CLI',
|
|
671
|
+
description: 'Tlon/Urbit API for reading data and administration: activity, channels, contacts, groups, messages, posts, settings, upload, expose, hooks. ' +
|
|
672
|
+
'DO NOT use this tool to send messages — use the `message` tool instead. ' +
|
|
248
673
|
"Examples: 'activity mentions --limit 10', 'channels groups', 'contacts self', 'groups list'",
|
|
249
674
|
parameters: {
|
|
250
|
-
type:
|
|
675
|
+
type: 'object',
|
|
251
676
|
properties: {
|
|
252
677
|
command: {
|
|
253
|
-
type:
|
|
254
|
-
description:
|
|
255
|
-
|
|
678
|
+
type: 'string',
|
|
679
|
+
description: 'The tlon command and arguments (read/admin operations). ' +
|
|
680
|
+
'To send messages, use the `message` tool, not this tool. ' +
|
|
256
681
|
"Examples: 'activity mentions --limit 10', 'contacts get ~sampel-palnet', 'groups list', 'messages dm ~ship --limit 20'",
|
|
257
682
|
},
|
|
258
683
|
},
|
|
259
|
-
required: [
|
|
684
|
+
required: ['command'],
|
|
260
685
|
},
|
|
261
686
|
async execute(_id, params) {
|
|
262
687
|
try {
|
|
@@ -267,8 +692,8 @@ export default defineChannelPluginEntry({
|
|
|
267
692
|
return {
|
|
268
693
|
content: [
|
|
269
694
|
{
|
|
270
|
-
type:
|
|
271
|
-
text: `Error: Unknown tlon subcommand '${subcommand ??
|
|
695
|
+
type: 'text',
|
|
696
|
+
text: `Error: Unknown tlon subcommand '${subcommand ?? '(none)'}'. Allowed: ${[...ALLOWED_TLON_COMMANDS].join(', ')}`,
|
|
272
697
|
},
|
|
273
698
|
],
|
|
274
699
|
details: { error: true },
|
|
@@ -278,41 +703,43 @@ export default defineChannelPluginEntry({
|
|
|
278
703
|
const blocked = checkBlockedSendOperation(args.slice(subIdx));
|
|
279
704
|
if (blocked) {
|
|
280
705
|
return {
|
|
281
|
-
content: [{ type:
|
|
282
|
-
details: { blocked: true, reason:
|
|
706
|
+
content: [{ type: 'text', text: blocked }],
|
|
707
|
+
details: { blocked: true, reason: 'send_operation' },
|
|
283
708
|
};
|
|
284
709
|
}
|
|
285
710
|
const output = await runTlonCommand(tlonBinary, args, credentials);
|
|
286
711
|
return {
|
|
287
|
-
content: [{ type:
|
|
712
|
+
content: [{ type: 'text', text: output }],
|
|
288
713
|
details: undefined,
|
|
289
714
|
};
|
|
290
715
|
}
|
|
291
716
|
catch (error) {
|
|
292
717
|
const message = error instanceof Error ? error.message : String(error);
|
|
293
718
|
return {
|
|
294
|
-
content: [{ type:
|
|
719
|
+
content: [{ type: 'text', text: `Error: ${message}` }],
|
|
295
720
|
details: { error: true },
|
|
296
721
|
};
|
|
297
722
|
}
|
|
298
723
|
},
|
|
299
724
|
});
|
|
300
725
|
// Tool access control: block sensitive tools for non-owners
|
|
301
|
-
const ownerOnlyTools = new Set([
|
|
726
|
+
const ownerOnlyTools = new Set(['tlon', 'cron', 'read']);
|
|
302
727
|
const logToolTraceContents = liveToolTraceContentsEnabled();
|
|
303
|
-
api.on(
|
|
304
|
-
const role = getSessionRole(ctx.sessionKey ??
|
|
728
|
+
api.on('before_tool_call', (event, ctx) => {
|
|
729
|
+
const role = getSessionRole(ctx.sessionKey ?? '');
|
|
305
730
|
const isOwnerOnlyTool = ownerOnlyTools.has(event.toolName);
|
|
306
|
-
const isBlocked = isOwnerOnlyTool && role ===
|
|
307
|
-
const blockReason = isBlocked
|
|
731
|
+
const isBlocked = isOwnerOnlyTool && role === 'user';
|
|
732
|
+
const blockReason = isBlocked
|
|
733
|
+
? `The ${event.toolName} tool is not available.`
|
|
734
|
+
: undefined;
|
|
308
735
|
if (logToolTraceContents) {
|
|
309
736
|
api.logger.info(formatToolTraceEvent({
|
|
310
|
-
phase:
|
|
737
|
+
phase: 'before',
|
|
311
738
|
sessionKey: ctx.sessionKey,
|
|
312
739
|
toolName: event.toolName,
|
|
313
740
|
payload: {
|
|
314
741
|
params: event.params,
|
|
315
|
-
role: role ??
|
|
742
|
+
role: role ?? 'internal',
|
|
316
743
|
blocked: isBlocked,
|
|
317
744
|
...(blockReason ? { blockReason } : {}),
|
|
318
745
|
},
|
|
@@ -331,12 +758,12 @@ export default defineChannelPluginEntry({
|
|
|
331
758
|
blockReason,
|
|
332
759
|
};
|
|
333
760
|
}
|
|
334
|
-
api.logger.info(`[tlon] Allowed ${event.toolName} tool for ${role ??
|
|
761
|
+
api.logger.info(`[tlon] Allowed ${event.toolName} tool for ${role ?? 'internal'} session. Session: ${ctx.sessionKey}`);
|
|
335
762
|
});
|
|
336
|
-
api.on(
|
|
763
|
+
api.on('after_tool_call', (event, ctx) => {
|
|
337
764
|
if (logToolTraceContents && shouldLogAfterToolTrace(event)) {
|
|
338
765
|
api.logger.info(formatToolTraceEvent({
|
|
339
|
-
phase:
|
|
766
|
+
phase: 'after',
|
|
340
767
|
sessionKey: ctx.sessionKey,
|
|
341
768
|
toolName: event.toolName,
|
|
342
769
|
payload: {
|
|
@@ -347,103 +774,227 @@ export default defineChannelPluginEntry({
|
|
|
347
774
|
},
|
|
348
775
|
}));
|
|
349
776
|
}
|
|
350
|
-
|
|
777
|
+
safeTelemetryObserver({
|
|
778
|
+
logger: api.logger,
|
|
779
|
+
telemetrySource: 'after_tool_call',
|
|
780
|
+
sourceEventName: event.toolName,
|
|
351
781
|
sessionKey: ctx.sessionKey,
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
782
|
+
run: () => {
|
|
783
|
+
recordToolCall({
|
|
784
|
+
sessionKey: ctx.sessionKey,
|
|
785
|
+
toolName: event.toolName,
|
|
786
|
+
durationMs: event.durationMs,
|
|
787
|
+
error: event.error,
|
|
788
|
+
});
|
|
789
|
+
},
|
|
790
|
+
});
|
|
791
|
+
});
|
|
792
|
+
// ── Session lifecycle / watchdog telemetry ─────────────────────────
|
|
793
|
+
// These hooks are global to OpenClaw, so telemetry.ts filters them through
|
|
794
|
+
// session keys remembered from Tlon inbound replies before emitting.
|
|
795
|
+
api.on('session_start', (event, ctx) => {
|
|
796
|
+
safeTelemetryObserver({
|
|
797
|
+
logger: api.logger,
|
|
798
|
+
telemetrySource: 'session_start',
|
|
799
|
+
sourceEventName: 'session_start',
|
|
800
|
+
sessionKey: event.sessionKey ?? ctx.sessionKey,
|
|
801
|
+
sessionId: event.sessionId ?? ctx.sessionId,
|
|
802
|
+
agentId: ctx.agentId,
|
|
803
|
+
run: () => {
|
|
804
|
+
reportSessionLifecycle({
|
|
805
|
+
lifecycleEvent: 'session_start',
|
|
806
|
+
sessionKey: event.sessionKey ?? ctx.sessionKey,
|
|
807
|
+
sessionId: event.sessionId ?? ctx.sessionId,
|
|
808
|
+
agentId: ctx.agentId,
|
|
809
|
+
hasNextSession: false,
|
|
810
|
+
});
|
|
811
|
+
},
|
|
812
|
+
});
|
|
813
|
+
});
|
|
814
|
+
api.on('session_end', (event, ctx) => {
|
|
815
|
+
safeTelemetryObserver({
|
|
816
|
+
logger: api.logger,
|
|
817
|
+
telemetrySource: 'session_end',
|
|
818
|
+
sourceEventName: 'session_end',
|
|
819
|
+
sessionKey: event.sessionKey ?? ctx.sessionKey,
|
|
820
|
+
sessionId: event.sessionId ?? ctx.sessionId,
|
|
821
|
+
agentId: ctx.agentId,
|
|
822
|
+
run: () => {
|
|
823
|
+
reportSessionLifecycle({
|
|
824
|
+
lifecycleEvent: 'session_end',
|
|
825
|
+
sessionKey: event.sessionKey ?? ctx.sessionKey,
|
|
826
|
+
sessionId: event.sessionId ?? ctx.sessionId,
|
|
827
|
+
agentId: ctx.agentId,
|
|
828
|
+
reason: event.reason ?? null,
|
|
829
|
+
messageCount: event.messageCount,
|
|
830
|
+
durationMs: event.durationMs ?? null,
|
|
831
|
+
transcriptArchived: event.transcriptArchived ?? null,
|
|
832
|
+
hasNextSession: Boolean(event.nextSessionId ?? event.nextSessionKey),
|
|
833
|
+
});
|
|
834
|
+
},
|
|
835
|
+
});
|
|
836
|
+
});
|
|
837
|
+
if (shouldInstallTlonDiagnosticSubscriptions(api.registrationMode)) {
|
|
838
|
+
const unsubscribeDiagnosticEvents = installTelemetryDiagnosticObservers(api);
|
|
839
|
+
api.on('gateway_stop', unsubscribeDiagnosticEvents);
|
|
840
|
+
}
|
|
841
|
+
// ── Route diagnostics ───────────────────────────────────────────────
|
|
842
|
+
// Fires for every outbound send OpenClaw routes — the primary streamed
|
|
843
|
+
// reply (resolves to `tlon`) and route-dependent sends (the shared
|
|
844
|
+
// `message` tool, subagents, which can resolve elsewhere). `ctx.channelId`
|
|
845
|
+
// is where the send resolved; `routedToTlon: false` (e.g. `webchat`) is the
|
|
846
|
+
// leak this work targets. Read-only; never alters delivery.
|
|
847
|
+
//
|
|
848
|
+
// Two sinks: a PostHog event (the primary, fleet-wide signal — gated by the
|
|
849
|
+
// existing telemetry config, on in hosted prod) so we can count how often
|
|
850
|
+
// sends land off-Tlon; and a debug-gated local log for single-gateway
|
|
851
|
+
// triage.
|
|
852
|
+
api.on('message_sending', (event, ctx) => {
|
|
853
|
+
safeTelemetryObserver({
|
|
854
|
+
logger: api.logger,
|
|
855
|
+
telemetrySource: 'message_sending',
|
|
856
|
+
sourceEventName: 'message_sending',
|
|
857
|
+
sessionKey: ctx.sessionKey,
|
|
858
|
+
runId: ctx.runId,
|
|
859
|
+
run: () => {
|
|
860
|
+
const resolvedChannel = ctx.channelId;
|
|
861
|
+
const routedToTlon = resolvedChannel === 'tlon';
|
|
862
|
+
// Only infer target kind for Tlon targets; a webchat target id is not
|
|
863
|
+
// a Tlon target and must not be misclassified.
|
|
864
|
+
const parsedTarget = routedToTlon ? parseTlonTarget(event.to) : null;
|
|
865
|
+
const targetKind = parsedTarget?.kind === 'dm'
|
|
866
|
+
? 'dm'
|
|
867
|
+
: parsedTarget?.kind === 'channel'
|
|
868
|
+
? 'group'
|
|
869
|
+
: 'unknown';
|
|
870
|
+
reportOutboundRoute({ resolvedChannel, routedToTlon, targetKind });
|
|
871
|
+
if (isRouteDebugEnabled()) {
|
|
872
|
+
api.logger.info(`[tlon][route-debug] message_sending ${JSON.stringify({
|
|
873
|
+
channelId: ctx.channelId,
|
|
874
|
+
to: event.to,
|
|
875
|
+
routedToTlon,
|
|
876
|
+
targetKind,
|
|
877
|
+
sessionKey: ctx.sessionKey ?? null,
|
|
878
|
+
conversationId: ctx.conversationId ?? null,
|
|
879
|
+
messageId: ctx.messageId ?? null,
|
|
880
|
+
threadId: event.threadId ?? null,
|
|
881
|
+
})}`);
|
|
882
|
+
}
|
|
883
|
+
},
|
|
884
|
+
});
|
|
885
|
+
});
|
|
886
|
+
api.on('message_sent', (event, ctx) => {
|
|
887
|
+
safeTelemetryObserver({
|
|
888
|
+
logger: api.logger,
|
|
889
|
+
telemetrySource: 'message_sent',
|
|
890
|
+
sourceEventName: 'message_sent',
|
|
891
|
+
sessionKey: event.sessionKey ?? ctx.sessionKey,
|
|
892
|
+
runId: event.runId ?? ctx.runId,
|
|
893
|
+
run: () => {
|
|
894
|
+
if (event.success !== false) {
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
reportHarnessError({
|
|
898
|
+
harnessEventType: 'message_sent',
|
|
899
|
+
errorScope: 'message_delivery',
|
|
900
|
+
sessionKey: event.sessionKey ?? ctx.sessionKey,
|
|
901
|
+
runId: event.runId ?? ctx.runId,
|
|
902
|
+
errorText: event.error ?? null,
|
|
903
|
+
outcome: 'error',
|
|
904
|
+
});
|
|
905
|
+
},
|
|
355
906
|
});
|
|
356
907
|
});
|
|
357
908
|
// ── Slash commands for approval & admin ────────────────────────────
|
|
358
909
|
api.registerCommand({
|
|
359
|
-
name:
|
|
360
|
-
description:
|
|
910
|
+
name: 'allow',
|
|
911
|
+
description: 'Allow a pending DM/channel/group request',
|
|
361
912
|
acceptsArgs: true,
|
|
362
913
|
handler: async (ctx) => {
|
|
363
914
|
const result = resolveBridgeForCommand(ctx);
|
|
364
|
-
if (
|
|
915
|
+
if ('error' in result) {
|
|
365
916
|
return { text: result.error };
|
|
366
917
|
}
|
|
367
918
|
return {
|
|
368
|
-
text: await result.bridge.handleAction(
|
|
919
|
+
text: await result.bridge.handleAction('approve', ctx.args?.trim() || undefined),
|
|
369
920
|
};
|
|
370
921
|
},
|
|
371
922
|
});
|
|
372
923
|
api.registerCommand({
|
|
373
|
-
name:
|
|
374
|
-
description:
|
|
924
|
+
name: 'reject',
|
|
925
|
+
description: 'Reject a pending DM/channel/group request',
|
|
375
926
|
acceptsArgs: true,
|
|
376
927
|
handler: async (ctx) => {
|
|
377
928
|
const result = resolveBridgeForCommand(ctx);
|
|
378
|
-
if (
|
|
929
|
+
if ('error' in result) {
|
|
379
930
|
return { text: result.error };
|
|
380
931
|
}
|
|
381
932
|
return {
|
|
382
|
-
text: await result.bridge.handleAction(
|
|
933
|
+
text: await result.bridge.handleAction('deny', ctx.args?.trim() || undefined),
|
|
383
934
|
};
|
|
384
935
|
},
|
|
385
936
|
});
|
|
386
937
|
api.registerCommand({
|
|
387
|
-
name:
|
|
388
|
-
description:
|
|
938
|
+
name: 'ban',
|
|
939
|
+
description: 'Ban a ship and deny its pending request',
|
|
389
940
|
acceptsArgs: true,
|
|
390
941
|
handler: async (ctx) => {
|
|
391
942
|
const result = resolveBridgeForCommand(ctx);
|
|
392
|
-
if (
|
|
943
|
+
if ('error' in result) {
|
|
393
944
|
return { text: result.error };
|
|
394
945
|
}
|
|
395
946
|
return {
|
|
396
|
-
text: await result.bridge.handleAction(
|
|
947
|
+
text: await result.bridge.handleAction('block', ctx.args?.trim() || undefined),
|
|
397
948
|
};
|
|
398
949
|
},
|
|
399
950
|
});
|
|
400
951
|
api.registerCommand({
|
|
401
|
-
name:
|
|
402
|
-
description:
|
|
952
|
+
name: 'pending',
|
|
953
|
+
description: 'List pending approval requests',
|
|
403
954
|
handler: async (ctx) => {
|
|
404
955
|
const result = resolveBridgeForCommand(ctx);
|
|
405
|
-
if (
|
|
956
|
+
if ('error' in result) {
|
|
406
957
|
return { text: result.error };
|
|
407
958
|
}
|
|
408
959
|
return { text: await result.bridge.getPendingList() };
|
|
409
960
|
},
|
|
410
961
|
});
|
|
411
962
|
api.registerCommand({
|
|
412
|
-
name:
|
|
413
|
-
description:
|
|
963
|
+
name: 'banned',
|
|
964
|
+
description: 'List banned ships',
|
|
414
965
|
handler: async (ctx) => {
|
|
415
966
|
const result = resolveBridgeForCommand(ctx);
|
|
416
|
-
if (
|
|
967
|
+
if ('error' in result) {
|
|
417
968
|
return { text: result.error };
|
|
418
969
|
}
|
|
419
970
|
return { text: await result.bridge.getBlockedList() };
|
|
420
971
|
},
|
|
421
972
|
});
|
|
422
973
|
api.registerCommand({
|
|
423
|
-
name:
|
|
424
|
-
description:
|
|
974
|
+
name: 'unban',
|
|
975
|
+
description: 'Unban a ship (e.g. /unban ~sampel-palnet)',
|
|
425
976
|
acceptsArgs: true,
|
|
426
977
|
handler: async (ctx) => {
|
|
427
978
|
const result = resolveBridgeForCommand(ctx);
|
|
428
|
-
if (
|
|
979
|
+
if ('error' in result) {
|
|
429
980
|
return { text: result.error };
|
|
430
981
|
}
|
|
431
982
|
const ship = ctx.args?.trim();
|
|
432
983
|
if (!ship) {
|
|
433
|
-
return { text:
|
|
984
|
+
return { text: 'Usage: /unban ~ship-name' };
|
|
434
985
|
}
|
|
435
986
|
return { text: await result.bridge.handleUnblock(ship) };
|
|
436
987
|
},
|
|
437
988
|
});
|
|
438
989
|
api.registerCommand({
|
|
439
|
-
name:
|
|
440
|
-
description:
|
|
441
|
-
|
|
442
|
-
|
|
990
|
+
name: 'owner-listen',
|
|
991
|
+
description: 'Control whether the bot listens for the owner without @-mention in owned channels. ' +
|
|
992
|
+
'Usage: /owner-listen [on|off|status|list] [<channel-nest>]; ' +
|
|
993
|
+
'/owner-listen all [on|off] for the global kill switch.',
|
|
443
994
|
acceptsArgs: true,
|
|
444
995
|
handler: async (ctx) => {
|
|
445
996
|
const result = resolveBridgeForCommand(ctx);
|
|
446
|
-
if (
|
|
997
|
+
if ('error' in result) {
|
|
447
998
|
return { text: result.error };
|
|
448
999
|
}
|
|
449
1000
|
const text = await handleOwnerListenCommand(result.bridge, ctx.args, ctx.from);
|