discoclaw 1.2.4 → 2.0.0
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/.context/voice.md +30 -2
- package/.env.example +7 -3
- package/.env.example.full +13 -32
- package/README.md +1 -1
- package/dist/cli/dashboard.js +7 -1
- package/dist/cli/dashboard.test.js +0 -4
- package/dist/cli/init-wizard.js +4 -8
- package/dist/cli/init-wizard.test.js +4 -10
- package/dist/config.js +5 -38
- package/dist/config.test.js +8 -72
- package/dist/cron/executor.js +72 -1
- package/dist/dashboard/api/metrics.js +7 -0
- package/dist/dashboard/api/metrics.test.js +16 -0
- package/dist/dashboard/api/traces.js +14 -0
- package/dist/dashboard/api/traces.test.js +40 -0
- package/dist/dashboard/page.js +187 -8
- package/dist/dashboard/server.js +82 -19
- package/dist/dashboard/server.test.js +123 -10
- package/dist/discord/actions.js +112 -6
- package/dist/discord/actions.test.js +117 -1
- package/dist/discord/deferred-runner.js +306 -219
- package/dist/discord/help-command.js +1 -1
- package/dist/discord/message-coordinator.js +4 -36
- package/dist/discord/models-command.js +1 -1
- package/dist/discord/reaction-handler.js +83 -5
- package/dist/discord/reaction-handler.test.js +55 -0
- package/dist/discord/verify-push.js +31 -36
- package/dist/discord/verify-push.test.js +34 -6
- package/dist/discord/voice-command.js +1 -31
- package/dist/discord/voice-command.test.js +21 -259
- package/dist/discord/voice-status-command.js +3 -22
- package/dist/discord/voice-status-command.test.js +16 -124
- package/dist/discord-followup.test.js +133 -0
- package/dist/health/config-doctor.js +5 -27
- package/dist/health/config-doctor.test.js +1 -4
- package/dist/index.js +15 -28
- package/dist/observability/trace-store.js +56 -0
- package/dist/observability/trace-utils.js +31 -0
- package/dist/runtime/codex-cli.js +3 -2
- package/dist/runtime/codex-cli.test.js +33 -0
- package/dist/runtime/model-tiers.js +1 -1
- package/dist/runtime/model-tiers.test.js +9 -0
- package/dist/runtime/openai-tool-schemas.js +17 -0
- package/dist/runtime-overrides.js +2 -3
- package/dist/runtime-overrides.test.js +27 -193
- package/dist/tasks/store.js +10 -6
- package/dist/tasks/store.test.js +44 -0
- package/dist/tasks/task-action-executor.test.js +162 -50
- package/dist/tasks/task-action-mutations.js +22 -2
- package/dist/tasks/task-action-read-ops.js +7 -1
- package/dist/tasks/task-action-runner-types.js +19 -1
- package/dist/voice/audio-pipeline.js +183 -96
- package/dist/voice/audio-receiver.js +8 -0
- package/dist/voice/audio-receiver.test.js +16 -0
- package/dist/voice/conversation-buffer.js +16 -6
- package/dist/voice/providers/gemini-live-provider.js +481 -0
- package/dist/voice/providers/gemini-live-provider.test.js +834 -0
- package/dist/voice/providers/gemini-live-responder.js +267 -0
- package/dist/voice/providers/gemini-live-responder.test.js +615 -0
- package/dist/voice/providers/gemini-live-token-estimator.js +100 -0
- package/dist/voice/providers/gemini-live-token-estimator.test.js +160 -0
- package/dist/voice/providers/gemini-live-types.js +32 -0
- package/dist/voice/providers/gemini-tool-mapper.js +91 -0
- package/dist/voice/providers/gemini-tool-mapper.test.js +253 -0
- package/dist/voice/providers/index.js +3 -0
- package/dist/voice/voice-prompt-builder.js +26 -17
- package/dist/voice/voice-prompt-builder.test.js +16 -1
- package/docs/configuration.md +4 -9
- package/docs/official-docs.md +6 -9
- package/docs/runtime-switching.md +1 -1
- package/package.json +1 -1
- package/dist/voice/audio-pipeline.test.js +0 -619
- package/dist/voice/stt-deepgram.js +0 -154
- package/dist/voice/stt-deepgram.test.js +0 -275
- package/dist/voice/stt-factory.js +0 -42
- package/dist/voice/stt-factory.test.js +0 -45
- package/dist/voice/stt-openai.js +0 -156
- package/dist/voice/stt-openai.test.js +0 -281
- package/dist/voice/tts-cartesia.js +0 -169
- package/dist/voice/tts-cartesia.test.js +0 -228
- package/dist/voice/tts-deepgram.js +0 -84
- package/dist/voice/tts-deepgram.test.js +0 -220
- package/dist/voice/tts-factory.js +0 -52
- package/dist/voice/tts-factory.test.js +0 -53
- package/dist/voice/tts-openai.js +0 -70
- package/dist/voice/tts-openai.test.js +0 -138
- package/dist/voice/types.test.js +0 -84
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { ChannelType, PermissionFlagsBits } from 'discord.js';
|
|
2
3
|
import { appendActionResults, buildTieredDiscordActionsPromptSection, executeDiscordActions, parseDiscordActions } from './actions.js';
|
|
3
4
|
import { DiscordTransportClient } from './transport-client.js';
|
|
@@ -12,6 +13,7 @@ import { buildPromptSectionEstimates, buildContextFiles, buildOpenTasksSection,
|
|
|
12
13
|
import { mapRuntimeErrorToUserMessage } from './user-errors.js';
|
|
13
14
|
import { resolveModel } from '../runtime/model-tiers.js';
|
|
14
15
|
import { globalMetrics } from '../observability/metrics.js';
|
|
16
|
+
import { globalTraceStore } from '../observability/trace-store.js';
|
|
15
17
|
import { buildPlanForgeAvailabilityNote } from './plan-forge-availability.js';
|
|
16
18
|
const REQUESTER_DENY_ALL = { __requesterDenyAll: true };
|
|
17
19
|
function getThreadParentId(candidate) {
|
|
@@ -78,247 +80,332 @@ function buildDeferredActionFlags(state, depth, maxDepth) {
|
|
|
78
80
|
export function configureDeferredScheduler(opts) {
|
|
79
81
|
const handleDeferredRun = async (run) => {
|
|
80
82
|
const { action, context } = run;
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const parentId = getThreadParentId(channel) ?? '';
|
|
97
|
-
const allowed = opts.state.allowChannelIds.has(channel.id) ||
|
|
98
|
-
(parentId && opts.state.allowChannelIds.has(parentId));
|
|
99
|
-
if (!allowed) {
|
|
100
|
-
opts.log?.warn({ flow: 'defer', channelId: channel.id }, 'defer:target channel not allowlisted');
|
|
83
|
+
const traceId = `defer_${randomUUID()}`;
|
|
84
|
+
const sessionKey = `defer:${action.channel}`;
|
|
85
|
+
let traceOutcome = 'success';
|
|
86
|
+
globalTraceStore.startTrace(traceId, sessionKey, 'defer', undefined);
|
|
87
|
+
try {
|
|
88
|
+
const guild = context.guild;
|
|
89
|
+
if (!guild) {
|
|
90
|
+
opts.log?.warn({ flow: 'defer', run, action }, 'defer:missing-guild');
|
|
91
|
+
traceOutcome = 'error';
|
|
92
|
+
globalTraceStore.addEvent(traceId, {
|
|
93
|
+
type: 'error',
|
|
94
|
+
at: Date.now(),
|
|
95
|
+
message: 'deferred run skipped: no guild context',
|
|
96
|
+
stage: 'defer_setup',
|
|
97
|
+
});
|
|
101
98
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
102
|
-
opts.status?.handlerError({ sessionKey:
|
|
99
|
+
opts.status?.handlerError({ sessionKey: 'defer' }, 'deferred run skipped: no guild context');
|
|
103
100
|
return;
|
|
104
101
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
102
|
+
const channel = resolveChannel(guild, action.channel);
|
|
103
|
+
if (!channel) {
|
|
104
|
+
opts.log?.warn({ flow: 'defer', run, channel: action.channel }, 'defer:target channel not found');
|
|
105
|
+
traceOutcome = 'error';
|
|
106
|
+
globalTraceStore.addEvent(traceId, {
|
|
107
|
+
type: 'error',
|
|
108
|
+
at: Date.now(),
|
|
109
|
+
message: `target channel "${action.channel}" not found`,
|
|
110
|
+
stage: 'defer_setup',
|
|
111
|
+
});
|
|
112
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
113
|
+
opts.status?.handlerError({ sessionKey: `defer:${action.channel}` }, `deferred run skipped: channel "${action.channel}" not found`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (opts.state.allowChannelIds?.size) {
|
|
117
|
+
const parentId = getThreadParentId(channel) ?? '';
|
|
118
|
+
const allowed = opts.state.allowChannelIds.has(channel.id) ||
|
|
119
|
+
(parentId && opts.state.allowChannelIds.has(parentId));
|
|
120
|
+
if (!allowed) {
|
|
121
|
+
opts.log?.warn({ flow: 'defer', channelId: channel.id }, 'defer:target channel not allowlisted');
|
|
122
|
+
traceOutcome = 'error';
|
|
123
|
+
globalTraceStore.addEvent(traceId, {
|
|
124
|
+
type: 'error',
|
|
125
|
+
at: Date.now(),
|
|
126
|
+
message: `target channel "${action.channel}" not allowlisted`,
|
|
127
|
+
stage: 'defer_setup',
|
|
128
|
+
});
|
|
129
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
130
|
+
opts.status?.handlerError({ sessionKey: `defer:${channel.id}` }, `deferred run skipped: channel ${channel.id} not in allowlist`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const threadParentId = getThreadParentId(channel);
|
|
135
|
+
const requesterMember = await resolveRequesterMember(context);
|
|
136
|
+
if (isRequesterDenyAll(requesterMember)
|
|
137
|
+
|| (requesterMember && !requesterCanAccessTargetChannel(channel, requesterMember))) {
|
|
138
|
+
opts.log?.warn({ flow: 'defer', channelId: channel.id, requesterId: context.requesterId }, 'defer:target channel permission denied');
|
|
139
|
+
traceOutcome = 'error';
|
|
140
|
+
globalTraceStore.addEvent(traceId, {
|
|
141
|
+
type: 'error',
|
|
142
|
+
at: Date.now(),
|
|
143
|
+
message: `requester lacks permission for channel ${channel.id}`,
|
|
144
|
+
stage: 'defer_setup',
|
|
145
|
+
});
|
|
146
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
147
|
+
opts.status?.handlerError({ sessionKey: `defer:${channel.id}` }, `deferred run skipped: requester lacks permission for channel ${channel.id}`);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const channelCtx = resolveDiscordChannelContext({
|
|
151
|
+
ctx: opts.discordChannelContext,
|
|
152
|
+
isDm: false,
|
|
153
|
+
channelId: channel.id,
|
|
154
|
+
threadParentId,
|
|
155
|
+
});
|
|
156
|
+
const paFiles = await loadWorkspacePaFiles(opts.workspaceCwd, { skip: !!opts.appendSystemPrompt });
|
|
157
|
+
const contextFiles = buildContextFiles(paFiles, opts.discordChannelContext, channelCtx.contextPath);
|
|
158
|
+
let inlinedContext = {
|
|
159
|
+
text: '',
|
|
160
|
+
sections: [],
|
|
161
|
+
};
|
|
162
|
+
if (contextFiles.length > 0) {
|
|
163
|
+
try {
|
|
164
|
+
inlinedContext = await inlineContextFilesWithMeta(contextFiles, {
|
|
165
|
+
required: new Set(opts.discordChannelContext?.paContextFiles ?? []),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
opts.log?.warn({ flow: 'defer', channelId: channel.id, err }, 'defer:context inline failed');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const deferDepth = (context.deferDepth ?? 0) + 1;
|
|
173
|
+
const deferredActionFlags = buildDeferredActionFlags(opts.state, deferDepth, opts.deferMaxDepth);
|
|
174
|
+
const openTasksSection = buildOpenTasksSection(opts.state.taskCtx?.store);
|
|
175
|
+
let actionsReferenceSection = '';
|
|
176
|
+
let actionSchemaSelection = null;
|
|
177
|
+
if (opts.state.discordActionsEnabled) {
|
|
178
|
+
const actionSelection = buildTieredDiscordActionsPromptSection(deferredActionFlags, opts.botDisplayName, {
|
|
179
|
+
channelName: channelCtx.channelName,
|
|
180
|
+
channelContextPath: channelCtx.contextPath,
|
|
181
|
+
isThread: threadParentId !== null,
|
|
182
|
+
userText: action.prompt,
|
|
183
|
+
canvasWriteBridgeEnabled: opts.state.canvasCtx?.writeBridgeEnabled,
|
|
184
|
+
imagegenDefaultModel: opts.state.imagegenCtx ? resolveDefaultModel(opts.state.imagegenCtx) : undefined,
|
|
185
|
+
});
|
|
186
|
+
actionsReferenceSection = actionSelection.prompt;
|
|
187
|
+
actionSchemaSelection = {
|
|
188
|
+
includedCategories: actionSelection.includedCategories,
|
|
189
|
+
tierBuckets: actionSelection.tierBuckets,
|
|
190
|
+
keywordHits: actionSelection.keywordHits,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
const promptSectionEstimates = buildPromptSectionEstimates({
|
|
194
|
+
contextSections: inlinedContext.sections,
|
|
195
|
+
channelContextPath: channelCtx.contextPath,
|
|
196
|
+
openTasksSection,
|
|
197
|
+
actionsReferenceSection,
|
|
198
|
+
});
|
|
199
|
+
opts.log?.info({
|
|
200
|
+
flow: 'defer',
|
|
201
|
+
channelId: channel.id,
|
|
202
|
+
sections: promptSectionEstimates.sections,
|
|
203
|
+
totalChars: promptSectionEstimates.totalChars,
|
|
204
|
+
totalEstTokens: promptSectionEstimates.totalEstTokens,
|
|
205
|
+
actionSchemaSelection,
|
|
206
|
+
}, 'defer:prompt:section-estimates');
|
|
207
|
+
const noteLines = [];
|
|
208
|
+
let effectiveTools = opts.runtimeTools;
|
|
128
209
|
try {
|
|
129
|
-
|
|
130
|
-
|
|
210
|
+
const toolsInfo = await resolveEffectiveTools({
|
|
211
|
+
workspaceCwd: opts.workspaceCwd,
|
|
212
|
+
runtimeTools: opts.runtimeTools,
|
|
213
|
+
runtimeCapabilities: resolveGroundedToolCapabilities(opts.runtime),
|
|
214
|
+
runtimeId: opts.runtime.id,
|
|
215
|
+
log: opts.log,
|
|
131
216
|
});
|
|
217
|
+
effectiveTools = toolsInfo.effectiveTools;
|
|
218
|
+
if (toolsInfo.permissionNote)
|
|
219
|
+
noteLines.push(`Permission note: ${toolsInfo.permissionNote}`);
|
|
220
|
+
if (toolsInfo.runtimeCapabilityNote)
|
|
221
|
+
noteLines.push(`Runtime capability note: ${toolsInfo.runtimeCapabilityNote}`);
|
|
132
222
|
}
|
|
133
223
|
catch (err) {
|
|
134
|
-
opts.log?.warn({ flow: 'defer', channelId: channel.id, err }, 'defer:
|
|
224
|
+
opts.log?.warn({ flow: 'defer', channelId: channel.id, err }, 'defer:resolve effective tools failed');
|
|
135
225
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
let actionSchemaSelection = null;
|
|
142
|
-
if (opts.state.discordActionsEnabled) {
|
|
143
|
-
const actionSelection = buildTieredDiscordActionsPromptSection(deferredActionFlags, opts.botDisplayName, {
|
|
144
|
-
channelName: channelCtx.channelName,
|
|
145
|
-
channelContextPath: channelCtx.contextPath,
|
|
146
|
-
isThread: threadParentId !== null,
|
|
147
|
-
userText: action.prompt,
|
|
148
|
-
canvasWriteBridgeEnabled: opts.state.canvasCtx?.writeBridgeEnabled,
|
|
149
|
-
imagegenDefaultModel: opts.state.imagegenCtx ? resolveDefaultModel(opts.state.imagegenCtx) : undefined,
|
|
226
|
+
const planForgeAvailabilityNote = buildPlanForgeAvailabilityNote({
|
|
227
|
+
planCommandsEnabled: opts.state.planCommandsEnabled !== false,
|
|
228
|
+
forgeCommandsEnabled: opts.state.forgeCommandsEnabled !== false,
|
|
229
|
+
planActionsEnabled: Boolean(opts.state.discordActionsPlan),
|
|
230
|
+
forgeActionsEnabled: Boolean(opts.state.discordActionsForge),
|
|
150
231
|
});
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const promptSectionEstimates = buildPromptSectionEstimates({
|
|
159
|
-
contextSections: inlinedContext.sections,
|
|
160
|
-
channelContextPath: channelCtx.contextPath,
|
|
161
|
-
openTasksSection,
|
|
162
|
-
actionsReferenceSection,
|
|
163
|
-
});
|
|
164
|
-
opts.log?.info({
|
|
165
|
-
flow: 'defer',
|
|
166
|
-
channelId: channel.id,
|
|
167
|
-
sections: promptSectionEstimates.sections,
|
|
168
|
-
totalChars: promptSectionEstimates.totalChars,
|
|
169
|
-
totalEstTokens: promptSectionEstimates.totalEstTokens,
|
|
170
|
-
actionSchemaSelection,
|
|
171
|
-
}, 'defer:prompt:section-estimates');
|
|
172
|
-
const noteLines = [];
|
|
173
|
-
let effectiveTools = opts.runtimeTools;
|
|
174
|
-
try {
|
|
175
|
-
const toolsInfo = await resolveEffectiveTools({
|
|
176
|
-
workspaceCwd: opts.workspaceCwd,
|
|
177
|
-
runtimeTools: opts.runtimeTools,
|
|
178
|
-
runtimeCapabilities: resolveGroundedToolCapabilities(opts.runtime),
|
|
232
|
+
if (planForgeAvailabilityNote)
|
|
233
|
+
noteLines.push(`Runtime capability note: ${planForgeAvailabilityNote}`);
|
|
234
|
+
const prompt = buildScheduledSelfInvocationPrompt({
|
|
235
|
+
inlinedContext: inlinedContext.text,
|
|
236
|
+
openTasksSection,
|
|
237
|
+
actionsReferenceSection,
|
|
238
|
+
noteLines,
|
|
179
239
|
runtimeId: opts.runtime.id,
|
|
180
|
-
|
|
240
|
+
runtimeCapabilities: opts.runtime.capabilities,
|
|
241
|
+
runtimeTools: opts.runtimeTools,
|
|
242
|
+
enableHybridPipeline: opts.state.enableHybridPipeline,
|
|
243
|
+
invocationNotice: `Deferred follow-up scheduled for <#${channel.id}> (runs at ${fmtTime(run.runsAt)}).`,
|
|
244
|
+
userMessage: action.prompt,
|
|
181
245
|
});
|
|
182
|
-
|
|
183
|
-
if (
|
|
184
|
-
|
|
185
|
-
if (
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if (evt.type === 'text_final') {
|
|
234
|
-
finalText = evt.text;
|
|
235
|
-
}
|
|
236
|
-
else if (evt.type === 'text_delta') {
|
|
237
|
-
deltaText += evt.text;
|
|
246
|
+
const addDirs = [];
|
|
247
|
+
if (opts.useGroupDirCwd)
|
|
248
|
+
addDirs.push(opts.workspaceCwd);
|
|
249
|
+
if (opts.discordChannelContext)
|
|
250
|
+
addDirs.push(opts.discordChannelContext.contentDir);
|
|
251
|
+
const uniqueAddDirs = addDirs.length > 0 ? Array.from(new Set(addDirs)) : undefined;
|
|
252
|
+
const t0 = Date.now();
|
|
253
|
+
globalMetrics.recordInvokeStart('defer');
|
|
254
|
+
globalTraceStore.addEvent(traceId, {
|
|
255
|
+
type: 'invoke_start',
|
|
256
|
+
at: t0,
|
|
257
|
+
summary: `deferred run for <#${channel.id}>`,
|
|
258
|
+
promptPreview: action.prompt.slice(0, 220),
|
|
259
|
+
});
|
|
260
|
+
opts.log?.info({ flow: 'defer', channelId: channel.id }, 'obs.invoke.start');
|
|
261
|
+
let finalText = '';
|
|
262
|
+
let deltaText = '';
|
|
263
|
+
let runtimeError;
|
|
264
|
+
let invokeResultRecorded = false;
|
|
265
|
+
try {
|
|
266
|
+
for await (const evt of opts.runtime.invoke({
|
|
267
|
+
prompt,
|
|
268
|
+
model: resolveModel(opts.state.runtimeModel, opts.runtime.id),
|
|
269
|
+
cwd: opts.workspaceCwd,
|
|
270
|
+
addDirs: uniqueAddDirs,
|
|
271
|
+
tools: effectiveTools,
|
|
272
|
+
timeoutMs: opts.runtimeTimeoutMs,
|
|
273
|
+
})) {
|
|
274
|
+
if (evt.type === 'text_final') {
|
|
275
|
+
finalText = evt.text;
|
|
276
|
+
}
|
|
277
|
+
else if (evt.type === 'text_delta') {
|
|
278
|
+
deltaText += evt.text;
|
|
279
|
+
}
|
|
280
|
+
else if (evt.type === 'error') {
|
|
281
|
+
runtimeError = evt.message;
|
|
282
|
+
finalText = mapRuntimeErrorToUserMessage(evt.message);
|
|
283
|
+
traceOutcome = 'error';
|
|
284
|
+
globalTraceStore.addEvent(traceId, {
|
|
285
|
+
type: 'error',
|
|
286
|
+
at: Date.now(),
|
|
287
|
+
message: evt.message,
|
|
288
|
+
stage: 'runtime',
|
|
289
|
+
});
|
|
290
|
+
globalMetrics.recordInvokeResult('defer', Date.now() - t0, false, evt.message);
|
|
291
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
292
|
+
opts.status?.runtimeError({ sessionKey: `defer:${channel.id}` }, evt.message);
|
|
293
|
+
opts.log?.warn({ flow: 'defer', channelId: channel.id, error: evt.message }, 'obs.invoke.error');
|
|
294
|
+
invokeResultRecorded = true;
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
238
297
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
298
|
+
}
|
|
299
|
+
catch (err) {
|
|
300
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
301
|
+
runtimeError ??= msg;
|
|
302
|
+
finalText = mapRuntimeErrorToUserMessage(msg);
|
|
303
|
+
traceOutcome = 'error';
|
|
304
|
+
globalTraceStore.addEvent(traceId, {
|
|
305
|
+
type: 'error',
|
|
306
|
+
at: Date.now(),
|
|
307
|
+
message: msg,
|
|
308
|
+
name: err instanceof Error ? err.name : undefined,
|
|
309
|
+
stage: 'runtime',
|
|
310
|
+
stack: err instanceof Error ? err.stack?.slice(0, 400) : undefined,
|
|
311
|
+
});
|
|
312
|
+
if (!invokeResultRecorded) {
|
|
313
|
+
globalMetrics.recordInvokeResult('defer', Date.now() - t0, false, msg);
|
|
243
314
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
244
|
-
opts.status?.runtimeError({ sessionKey: `defer:${channel.id}` },
|
|
245
|
-
opts.log?.warn({ flow: 'defer', channelId: channel.id, error: evt.message }, 'obs.invoke.error');
|
|
315
|
+
opts.status?.runtimeError({ sessionKey: `defer:${channel.id}` }, msg);
|
|
246
316
|
invokeResultRecorded = true;
|
|
247
|
-
break;
|
|
248
317
|
}
|
|
318
|
+
opts.log?.warn({ flow: 'defer', channelId: channel.id, err }, 'defer:runtime invocation failed');
|
|
249
319
|
}
|
|
250
|
-
}
|
|
251
|
-
catch (err) {
|
|
252
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
253
|
-
runtimeError ??= msg;
|
|
254
|
-
finalText = mapRuntimeErrorToUserMessage(msg);
|
|
255
320
|
if (!invokeResultRecorded) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
321
|
+
globalTraceStore.addEvent(traceId, {
|
|
322
|
+
type: 'invoke_end',
|
|
323
|
+
at: Date.now(),
|
|
324
|
+
ok: true,
|
|
325
|
+
summary: `completed in ${Date.now() - t0}ms`,
|
|
326
|
+
});
|
|
327
|
+
globalMetrics.recordInvokeResult('defer', Date.now() - t0, true);
|
|
328
|
+
opts.log?.info({ flow: 'defer', channelId: channel.id, ms: Date.now() - t0, ok: true }, 'obs.invoke.end');
|
|
260
329
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
330
|
+
const processedText = finalText || deltaText || '';
|
|
331
|
+
const parsed = parseDiscordActions(processedText, deferredActionFlags);
|
|
332
|
+
const actCtx = {
|
|
333
|
+
guild,
|
|
334
|
+
client: context.client,
|
|
335
|
+
channelId: channel.id,
|
|
336
|
+
messageId: `defer-${Date.now()}`,
|
|
337
|
+
requesterId: context.requesterId,
|
|
338
|
+
threadParentId,
|
|
339
|
+
deferScheduler: context.deferScheduler,
|
|
340
|
+
deferDepth,
|
|
341
|
+
transport: new DiscordTransportClient(guild, context.client),
|
|
342
|
+
confirmation: {
|
|
343
|
+
mode: 'automated',
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
let actionResults = [];
|
|
347
|
+
if (parsed.actions.length > 0) {
|
|
348
|
+
actionResults = await executeDiscordActions(parsed.actions, actCtx, opts.log, {
|
|
349
|
+
taskCtx: opts.state.taskCtx,
|
|
350
|
+
cronCtx: opts.state.cronCtx,
|
|
351
|
+
forgeCtx: opts.state.forgeCtx,
|
|
352
|
+
planCtx: opts.state.planCtx,
|
|
353
|
+
memoryCtx: opts.state.memoryCtx,
|
|
354
|
+
configCtx: opts.state.configCtx,
|
|
355
|
+
canvasCtx: opts.state.canvasCtx,
|
|
356
|
+
imagegenCtx: opts.state.imagegenCtx,
|
|
357
|
+
voiceCtx: opts.state.voiceCtx,
|
|
358
|
+
spawnCtx: opts.state.spawnCtx,
|
|
359
|
+
});
|
|
360
|
+
for (let i = 0; i < actionResults.length; i++) {
|
|
361
|
+
const result = actionResults[i];
|
|
362
|
+
globalMetrics.recordActionResult(result.ok);
|
|
363
|
+
globalTraceStore.addEvent(traceId, {
|
|
364
|
+
type: 'action_result',
|
|
365
|
+
at: Date.now(),
|
|
366
|
+
action: parsed.actions[i].type,
|
|
367
|
+
ok: result.ok,
|
|
368
|
+
detail: result.ok ? undefined : ('error' in result ? result.error : undefined),
|
|
369
|
+
});
|
|
370
|
+
opts.log?.info({ flow: 'defer', channelId: channel.id, ok: result.ok }, 'obs.action.result');
|
|
371
|
+
if (!result.ok) {
|
|
372
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
373
|
+
opts.status?.actionFailed(parsed.actions[i].type, result.error);
|
|
374
|
+
}
|
|
304
375
|
}
|
|
305
376
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
377
|
+
let outgoingText = appendActionResults(parsed.cleanText.trim(), parsed.actions, actionResults);
|
|
378
|
+
outgoingText = appendUnavailableActionTypesNotice(outgoingText, parsed.strippedUnrecognizedTypes).trim();
|
|
379
|
+
outgoingText = appendParseFailureNotice(outgoingText, parsed.parseFailures).trim();
|
|
380
|
+
if (!outgoingText && runtimeError) {
|
|
381
|
+
outgoingText = runtimeError;
|
|
382
|
+
}
|
|
383
|
+
if (!outgoingText) {
|
|
384
|
+
opts.log?.warn({ flow: 'defer', channelId: channel.id }, 'defer:empty output, nothing to send');
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
try {
|
|
388
|
+
await channel.send({ content: outgoingText, allowedMentions: NO_MENTIONS });
|
|
389
|
+
}
|
|
390
|
+
catch (err) {
|
|
391
|
+
opts.log?.warn({ flow: 'defer', channelId: channel.id, err }, 'defer:failed to post follow-up');
|
|
392
|
+
}
|
|
319
393
|
}
|
|
320
394
|
catch (err) {
|
|
321
|
-
|
|
395
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
396
|
+
traceOutcome = 'error';
|
|
397
|
+
globalTraceStore.addEvent(traceId, {
|
|
398
|
+
type: 'error',
|
|
399
|
+
at: Date.now(),
|
|
400
|
+
message: msg,
|
|
401
|
+
name: err instanceof Error ? err.name : undefined,
|
|
402
|
+
stage: 'defer_flow',
|
|
403
|
+
stack: err instanceof Error ? err.stack?.slice(0, 400) : undefined,
|
|
404
|
+
});
|
|
405
|
+
opts.log?.error({ flow: 'defer', err }, 'defer:handler failed');
|
|
406
|
+
}
|
|
407
|
+
finally {
|
|
408
|
+
globalTraceStore.endTrace(traceId, traceOutcome);
|
|
322
409
|
}
|
|
323
410
|
};
|
|
324
411
|
const deferScheduler = new DeferSchedulerImpl({
|
|
@@ -23,7 +23,7 @@ export function handleHelpCommand() {
|
|
|
23
23
|
'- `!update` — check for or apply code updates; `!update help` for details',
|
|
24
24
|
'- `!restart` — restart the discoclaw service; `!restart help` for details',
|
|
25
25
|
'- `!stop` — abort all active AI streams and cancel any running forge',
|
|
26
|
-
'- `!voice status` — show voice
|
|
26
|
+
'- `!voice status` — show Gemini Live voice status and active connections',
|
|
27
27
|
'- `!help` — this message',
|
|
28
28
|
].join('\n');
|
|
29
29
|
}
|