@remembrance-ai/opencode-plugin 0.1.45
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/LICENSE +201 -0
- package/README.md +93 -0
- package/bin/setup.mjs +202 -0
- package/opencode.json +20 -0
- package/package.json +44 -0
- package/scripts/hook-core.mjs +3027 -0
- package/servers/remembrance-mcp.mjs +10015 -0
- package/skills/remembrancer/SKILL.md +669 -0
- package/skills/remembrancer/references/attestation-rest.md +421 -0
- package/skills/remembrancer/references/mpp.md +97 -0
- package/skills/remembrancer/references/remembrance-payloads.md +305 -0
- package/skills/remembrancer/references/remembrance-setup.md +870 -0
- package/skills/remembrancer/references/resource-scout.md +61 -0
- package/skills/remembrancer/references/web-ui-ux-qa.md +55 -0
- package/skills/remembrancer/scripts/queue-private-skill-import.mjs +280 -0
- package/skills/remembrancer/scripts/validate-remembrance.mjs +82 -0
- package/src/index.mjs +551 -0
package/src/index.mjs
ADDED
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
// Remembrance opencode plugin entrypoint.
|
|
2
|
+
//
|
|
3
|
+
// opencode plugins are IN-PROCESS JS/TS modules, not stdin/stdout scripts: the
|
|
4
|
+
// module exports a plugin function that receives
|
|
5
|
+
// `{ project, client, $, directory, worktree }` and returns an object whose
|
|
6
|
+
// `event` callback receives the host's discriminated lifecycle-event union.
|
|
7
|
+
// Specialized mutable hooks such as tool.execute.after remain direct keys.
|
|
8
|
+
// (See opencode.ai/docs/plugins.)
|
|
9
|
+
//
|
|
10
|
+
// This entrypoint ports as much of the Remembrance prompt/completion loop as the
|
|
11
|
+
// host's event surface allows:
|
|
12
|
+
//
|
|
13
|
+
// SESSION START (event -> session.created): records lifecycle health under the
|
|
14
|
+
// "opencode" surface and logs the activation notice, so a user can see the
|
|
15
|
+
// plugin loaded and knows to call get_connection_status.
|
|
16
|
+
//
|
|
17
|
+
// PROMPT (chat.message -> experimental.chat.system.transform): queries from
|
|
18
|
+
// the current user turn, then injects the bounded result into the model's
|
|
19
|
+
// system context before dispatch. Legacy message events remain a fallback
|
|
20
|
+
// for older builds and are deduplicated by message id.
|
|
21
|
+
//
|
|
22
|
+
// TOOL OBSERVER (tool.execute.after): correlates Remembrance tool calls with
|
|
23
|
+
// the active directive exactly like the other hosts.
|
|
24
|
+
//
|
|
25
|
+
// COMPLETION (event -> session.idle): when the session used Remembrance or contained
|
|
26
|
+
// eligible reusable work, logs the contribution nudge once per engagement.
|
|
27
|
+
// Respects REMEMBRANCE_AUTO_CONTRIBUTE=0.
|
|
28
|
+
//
|
|
29
|
+
// All decision/query/format logic lives in scripts/hook-core.mjs (Node-builtins
|
|
30
|
+
// only, runtime-agnostic, copied verbatim from packages/codex-plugin). This
|
|
31
|
+
// module is only the opencode adapter.
|
|
32
|
+
//
|
|
33
|
+
// The event callback and message/session shapes follow OpenCode's published SDK
|
|
34
|
+
// union. Defensive probes retain compatibility with older builds.
|
|
35
|
+
|
|
36
|
+
import { readFileSync } from "node:fs";
|
|
37
|
+
import process from "node:process";
|
|
38
|
+
import {
|
|
39
|
+
clearHighMatchSurfaceForExplicitSelection,
|
|
40
|
+
clearHighMatchSurfaceIfOpened,
|
|
41
|
+
debugLog,
|
|
42
|
+
decideStop,
|
|
43
|
+
directSelectionFromResponse,
|
|
44
|
+
highMatchFromResponse,
|
|
45
|
+
markCurrentEngagementHandled,
|
|
46
|
+
recordDirectiveFollowThroughForTool,
|
|
47
|
+
recordDirectSelectionSurface,
|
|
48
|
+
recordDirectiveSurface,
|
|
49
|
+
recordHighMatchSurface,
|
|
50
|
+
recordPluginLifecycleHealth,
|
|
51
|
+
recordRegistryUse,
|
|
52
|
+
recordTaskEligibility,
|
|
53
|
+
recordValueEpisodeSurface,
|
|
54
|
+
queryResponseHasMatches,
|
|
55
|
+
reportDirectiveEvent,
|
|
56
|
+
reportTaskOutcomesOnStop,
|
|
57
|
+
responseRequestsRemembranceFollowup,
|
|
58
|
+
resolveApiCredential,
|
|
59
|
+
runPromptHook,
|
|
60
|
+
toolResponseIndicatesFailure,
|
|
61
|
+
valueEpisodeFromResponse,
|
|
62
|
+
writePromptedCount,
|
|
63
|
+
} from "../scripts/hook-core.mjs";
|
|
64
|
+
|
|
65
|
+
const SURFACE = "opencode";
|
|
66
|
+
const SERVICE = "remembrance";
|
|
67
|
+
const CONTRIBUTION_TOOLS = [
|
|
68
|
+
"submit_query_feedback",
|
|
69
|
+
"submit_feedback",
|
|
70
|
+
"submit_remembrance",
|
|
71
|
+
"propose_skill_idea",
|
|
72
|
+
"propose_private_skill",
|
|
73
|
+
"submit_suggestion",
|
|
74
|
+
"submit_resource",
|
|
75
|
+
"submit_resource_review",
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
export function pluginVersion(readFile = readFileSync) {
|
|
79
|
+
try {
|
|
80
|
+
return JSON.parse(
|
|
81
|
+
readFile(new URL("../package.json", import.meta.url), "utf8"),
|
|
82
|
+
).version;
|
|
83
|
+
} catch {
|
|
84
|
+
return "unknown";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// OpenCode's published SDK uses the properties.* fields first. Legacy probes
|
|
89
|
+
// keep older host builds compatible, and "not found" always means "skip".
|
|
90
|
+
export function promptFromMessageEvent(input, output) {
|
|
91
|
+
for (const candidate of [
|
|
92
|
+
output?.parts,
|
|
93
|
+
input?.parts,
|
|
94
|
+
output?.properties?.part?.text,
|
|
95
|
+
input?.properties?.part?.text,
|
|
96
|
+
output?.message?.content,
|
|
97
|
+
output?.message?.text,
|
|
98
|
+
output?.content,
|
|
99
|
+
output?.text,
|
|
100
|
+
input?.message?.content,
|
|
101
|
+
input?.message?.text,
|
|
102
|
+
input?.content,
|
|
103
|
+
input?.text,
|
|
104
|
+
]) {
|
|
105
|
+
if (typeof candidate === "string" && candidate.trim()) {
|
|
106
|
+
return candidate;
|
|
107
|
+
}
|
|
108
|
+
if (Array.isArray(candidate)) {
|
|
109
|
+
const joined = candidate
|
|
110
|
+
.map((part) =>
|
|
111
|
+
typeof part === "string" ? part : (part?.text ?? part?.content ?? ""),
|
|
112
|
+
)
|
|
113
|
+
.filter((part) => typeof part === "string")
|
|
114
|
+
.join("\n")
|
|
115
|
+
.trim();
|
|
116
|
+
if (joined) return joined;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function isUserMessageEvent(input, output) {
|
|
123
|
+
const role =
|
|
124
|
+
output?.properties?.info?.role ??
|
|
125
|
+
input?.properties?.info?.role ??
|
|
126
|
+
output?.message?.role ??
|
|
127
|
+
output?.role ??
|
|
128
|
+
input?.message?.role ??
|
|
129
|
+
input?.role ??
|
|
130
|
+
null;
|
|
131
|
+
return role === "user";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function sessionIdFromEvent(input, output) {
|
|
135
|
+
for (const candidate of [
|
|
136
|
+
output?.properties?.sessionID,
|
|
137
|
+
output?.properties?.info?.sessionID,
|
|
138
|
+
output?.properties?.info?.id,
|
|
139
|
+
output?.properties?.part?.sessionID,
|
|
140
|
+
input?.properties?.sessionID,
|
|
141
|
+
input?.properties?.info?.sessionID,
|
|
142
|
+
input?.properties?.info?.id,
|
|
143
|
+
input?.properties?.part?.sessionID,
|
|
144
|
+
output?.sessionID,
|
|
145
|
+
output?.sessionId,
|
|
146
|
+
output?.session?.id,
|
|
147
|
+
output?.message?.sessionID,
|
|
148
|
+
output?.message?.sessionId,
|
|
149
|
+
input?.sessionID,
|
|
150
|
+
input?.sessionId,
|
|
151
|
+
input?.session?.id,
|
|
152
|
+
]) {
|
|
153
|
+
if (typeof candidate === "string" && candidate.trim()) {
|
|
154
|
+
return candidate;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return "opencode-session";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function messageIdFromEvent(input, output) {
|
|
161
|
+
for (const candidate of [
|
|
162
|
+
output?.properties?.info?.id,
|
|
163
|
+
output?.properties?.part?.messageID,
|
|
164
|
+
input?.properties?.info?.id,
|
|
165
|
+
input?.properties?.part?.messageID,
|
|
166
|
+
output?.message?.id,
|
|
167
|
+
output?.messageID,
|
|
168
|
+
input?.message?.id,
|
|
169
|
+
input?.messageID,
|
|
170
|
+
]) {
|
|
171
|
+
if (typeof candidate === "string" && candidate.trim()) {
|
|
172
|
+
return candidate;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function toolNameFromEvent(input, output) {
|
|
179
|
+
for (const candidate of [
|
|
180
|
+
input?.tool,
|
|
181
|
+
input?.toolName,
|
|
182
|
+
output?.tool,
|
|
183
|
+
output?.toolName,
|
|
184
|
+
]) {
|
|
185
|
+
if (typeof candidate === "string" && candidate.trim()) {
|
|
186
|
+
return candidate;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return "";
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function log(client, level, message) {
|
|
193
|
+
try {
|
|
194
|
+
await client?.app?.log?.({ body: { service: SERVICE, level, message } });
|
|
195
|
+
} catch {
|
|
196
|
+
// Logging is best effort; never let it break the host.
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function notify(client, message, variant = "info") {
|
|
201
|
+
try {
|
|
202
|
+
if (client?.tui?.showToast) {
|
|
203
|
+
await client.tui.showToast({
|
|
204
|
+
body: { title: "Remembrance", message, variant },
|
|
205
|
+
});
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
} catch {
|
|
209
|
+
// Fall through to structured logging when this host has no active TUI.
|
|
210
|
+
}
|
|
211
|
+
await log(client, variant === "error" ? "error" : "info", message);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export const Remembrance = async (context = {}) => {
|
|
215
|
+
const { client } = context;
|
|
216
|
+
const env = process.env;
|
|
217
|
+
const version = pluginVersion();
|
|
218
|
+
// OpenCode emits message metadata and message text separately. Track both
|
|
219
|
+
// sides so either delivery order works, then query exactly once.
|
|
220
|
+
const queriedMessages = new Set();
|
|
221
|
+
const messageSessions = new Map();
|
|
222
|
+
const pendingGuidance = new Map();
|
|
223
|
+
const userMessages = new Map();
|
|
224
|
+
const pendingTextParts = new Map();
|
|
225
|
+
|
|
226
|
+
function clearSessionState(sessionId) {
|
|
227
|
+
pendingGuidance.delete(sessionId);
|
|
228
|
+
for (const [messageId, trackedSessionId] of messageSessions) {
|
|
229
|
+
if (trackedSessionId !== sessionId) continue;
|
|
230
|
+
messageSessions.delete(messageId);
|
|
231
|
+
queriedMessages.delete(messageId);
|
|
232
|
+
}
|
|
233
|
+
for (const [messageId, trackedSessionId] of userMessages) {
|
|
234
|
+
if (trackedSessionId === sessionId) userMessages.delete(messageId);
|
|
235
|
+
}
|
|
236
|
+
for (const [messageId, pending] of pendingTextParts) {
|
|
237
|
+
if (pending.sessionId === sessionId) pendingTextParts.delete(messageId);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function recordLifecycle(component, sessionId) {
|
|
242
|
+
try {
|
|
243
|
+
const credential = resolveApiCredential(env);
|
|
244
|
+
recordPluginLifecycleHealth(
|
|
245
|
+
{
|
|
246
|
+
surface: SURFACE,
|
|
247
|
+
component,
|
|
248
|
+
pluginVersion: version,
|
|
249
|
+
hostVersion: "",
|
|
250
|
+
credentialSource: credential.source,
|
|
251
|
+
sessionId,
|
|
252
|
+
},
|
|
253
|
+
env,
|
|
254
|
+
);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
debugLog(env, "opencode_lifecycle_health_failed", {
|
|
257
|
+
component,
|
|
258
|
+
error: String(error),
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function handleSessionCreated(event) {
|
|
264
|
+
try {
|
|
265
|
+
const sessionId = sessionIdFromEvent(event, event);
|
|
266
|
+
recordLifecycle("session_start", sessionId);
|
|
267
|
+
await notify(
|
|
268
|
+
client,
|
|
269
|
+
`Remembrance ${version} is active. Relevant memory is added before eligible turns; ` +
|
|
270
|
+
"get_connection_status verifies tools and organization access without exposing the key.",
|
|
271
|
+
);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
debugLog(env, "opencode_session_created_failed", {
|
|
274
|
+
error: String(error),
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function handlePrompt(prompt, sessionId, messageId) {
|
|
280
|
+
const messageKey = messageId ?? `${sessionId}:${prompt}`;
|
|
281
|
+
if (queriedMessages.has(messageKey)) return;
|
|
282
|
+
queriedMessages.add(messageKey);
|
|
283
|
+
messageSessions.set(messageKey, sessionId);
|
|
284
|
+
recordLifecycle("prompt_hook", sessionId);
|
|
285
|
+
const result = await runPromptHook(prompt, {
|
|
286
|
+
env,
|
|
287
|
+
identity: { provider: SURFACE, model: "opencode" },
|
|
288
|
+
reportDirectiveShown: false,
|
|
289
|
+
userAgent: "@remembrance-ai/opencode-plugin",
|
|
290
|
+
});
|
|
291
|
+
if (result?.eligible) {
|
|
292
|
+
recordTaskEligibility(sessionId, env);
|
|
293
|
+
}
|
|
294
|
+
const guidance = result?.context ?? "";
|
|
295
|
+
if (guidance.trim()) {
|
|
296
|
+
// Registry use is recorded on DELIVERY (handleSystemTransform), not here.
|
|
297
|
+
// Every other host returns guidance from the prompt hook itself, so for
|
|
298
|
+
// them querying and delivering are the same instant. OpenCode delivers in
|
|
299
|
+
// a separate later callback riding an `experimental.*` host API, so
|
|
300
|
+
// counting use at query time would keep reporting consumption — and keep
|
|
301
|
+
// firing the completion nudge — even if that API stopped injecting and the
|
|
302
|
+
// model never saw a single line. Recording at delivery keeps the counter
|
|
303
|
+
// meaning the same thing here as everywhere else, and makes a broken
|
|
304
|
+
// injection observable as an absence of use rather than as silent
|
|
305
|
+
// over-reporting.
|
|
306
|
+
pendingGuidance.set(sessionId, {
|
|
307
|
+
directive: result?.directive ?? null,
|
|
308
|
+
highMatch: result?.highMatch ?? null,
|
|
309
|
+
text: guidance.trim(),
|
|
310
|
+
countsAsUse: Boolean(result?.consumed && result?.matched),
|
|
311
|
+
valueEpisode: result?.valueEpisode ?? null,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function handleChatMessage(input, output) {
|
|
317
|
+
try {
|
|
318
|
+
const prompt = promptFromMessageEvent(input, output);
|
|
319
|
+
if (!prompt) return;
|
|
320
|
+
await handlePrompt(
|
|
321
|
+
prompt,
|
|
322
|
+
sessionIdFromEvent(input, output),
|
|
323
|
+
messageIdFromEvent(input, output),
|
|
324
|
+
);
|
|
325
|
+
} catch (error) {
|
|
326
|
+
debugLog(env, "opencode_chat_message_failed", {
|
|
327
|
+
error: String(error),
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function handleSystemTransform(input, output) {
|
|
333
|
+
try {
|
|
334
|
+
const sessionId = sessionIdFromEvent(input, output);
|
|
335
|
+
const guidance = pendingGuidance.get(sessionId);
|
|
336
|
+
if (!guidance || !Array.isArray(output?.system)) return;
|
|
337
|
+
output.system.push(guidance.text);
|
|
338
|
+
pendingGuidance.delete(sessionId);
|
|
339
|
+
// Delivery happened: only now is the guidance actually in the model's
|
|
340
|
+
// context, so only now does it count as registry use.
|
|
341
|
+
if (guidance.countsAsUse) {
|
|
342
|
+
recordRegistryUse(sessionId, env);
|
|
343
|
+
recordHighMatchSurface(sessionId, guidance.highMatch, env);
|
|
344
|
+
recordValueEpisodeSurface(sessionId, guidance.valueEpisode, env);
|
|
345
|
+
}
|
|
346
|
+
recordDirectiveSurface(sessionId, guidance.directive, env);
|
|
347
|
+
if (guidance.directive) {
|
|
348
|
+
await reportDirectiveEvent(
|
|
349
|
+
{
|
|
350
|
+
event: "shown",
|
|
351
|
+
directive_id: guidance.directive.directive_id,
|
|
352
|
+
surface: "plugin_hook",
|
|
353
|
+
runtime: guidance.directive.runtime,
|
|
354
|
+
trigger_reason: guidance.directive.trigger_reason,
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
env,
|
|
358
|
+
userAgent: "@remembrance-ai/opencode-plugin",
|
|
359
|
+
},
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
await log(
|
|
363
|
+
client,
|
|
364
|
+
"debug",
|
|
365
|
+
"Added Remembrance context to the current model turn.",
|
|
366
|
+
);
|
|
367
|
+
} catch (error) {
|
|
368
|
+
debugLog(env, "opencode_system_transform_failed", {
|
|
369
|
+
error: String(error),
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function handleMessageUpdated(event) {
|
|
375
|
+
try {
|
|
376
|
+
if (!isUserMessageEvent(event, event)) return;
|
|
377
|
+
const sessionId = sessionIdFromEvent(event, event);
|
|
378
|
+
const messageId = messageIdFromEvent(event, event);
|
|
379
|
+
if (!messageId) return;
|
|
380
|
+
userMessages.set(messageId, sessionId);
|
|
381
|
+
const pending = pendingTextParts.get(messageId);
|
|
382
|
+
if (pending) {
|
|
383
|
+
pendingTextParts.delete(messageId);
|
|
384
|
+
await handlePrompt(pending.prompt, pending.sessionId, messageId);
|
|
385
|
+
}
|
|
386
|
+
} catch (error) {
|
|
387
|
+
debugLog(env, "opencode_message_updated_failed", {
|
|
388
|
+
error: String(error),
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function handleMessagePartUpdated(event) {
|
|
394
|
+
try {
|
|
395
|
+
const prompt = promptFromMessageEvent(event, event);
|
|
396
|
+
const messageId = messageIdFromEvent(event, event);
|
|
397
|
+
if (!prompt || !messageId) return;
|
|
398
|
+
const sessionId =
|
|
399
|
+
userMessages.get(messageId) ?? sessionIdFromEvent(event, event);
|
|
400
|
+
if (!userMessages.has(messageId)) {
|
|
401
|
+
pendingTextParts.set(messageId, { prompt, sessionId });
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
await handlePrompt(prompt, sessionId, messageId);
|
|
405
|
+
} catch (error) {
|
|
406
|
+
debugLog(env, "opencode_prompt_hook_failed", { error: String(error) });
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async function handleSessionIdle(event) {
|
|
411
|
+
let sessionId = "opencode-session";
|
|
412
|
+
try {
|
|
413
|
+
sessionId = sessionIdFromEvent(event, event);
|
|
414
|
+
recordLifecycle("completion_hook", sessionId);
|
|
415
|
+
await reportTaskOutcomesOnStop(sessionId, event, {
|
|
416
|
+
env,
|
|
417
|
+
userAgent: "@remembrance-ai/opencode-plugin",
|
|
418
|
+
});
|
|
419
|
+
const decision = decideStop({ session_id: sessionId }, { env });
|
|
420
|
+
if (decision?.allow !== false || !decision?.reason) return;
|
|
421
|
+
markCurrentEngagementHandled(sessionId, env);
|
|
422
|
+
writePromptedCount(sessionId, decision.useCount ?? 1, env);
|
|
423
|
+
await notify(client, decision.reason);
|
|
424
|
+
} catch (error) {
|
|
425
|
+
debugLog(env, "opencode_completion_hook_failed", {
|
|
426
|
+
error: String(error),
|
|
427
|
+
});
|
|
428
|
+
} finally {
|
|
429
|
+
clearSessionState(sessionId);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return {
|
|
434
|
+
"chat.message": handleChatMessage,
|
|
435
|
+
|
|
436
|
+
event: async ({ event } = {}) => {
|
|
437
|
+
try {
|
|
438
|
+
if (!event || typeof event.type !== "string") return;
|
|
439
|
+
if (event.type === "session.created") {
|
|
440
|
+
await handleSessionCreated(event);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
if (event.type === "message.updated") {
|
|
444
|
+
await handleMessageUpdated(event);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (event.type === "message.part.updated") {
|
|
448
|
+
await handleMessagePartUpdated(event);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
if (event.type === "session.idle") {
|
|
452
|
+
await handleSessionIdle(event);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (event.type === "session.deleted") {
|
|
456
|
+
clearSessionState(sessionIdFromEvent(event, event));
|
|
457
|
+
}
|
|
458
|
+
} catch (error) {
|
|
459
|
+
debugLog(env, "opencode_event_dispatch_failed", {
|
|
460
|
+
error: String(error),
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
},
|
|
464
|
+
|
|
465
|
+
"experimental.chat.system.transform": handleSystemTransform,
|
|
466
|
+
|
|
467
|
+
"tool.execute.after": async (input, output) => {
|
|
468
|
+
try {
|
|
469
|
+
const sessionId = sessionIdFromEvent(input, output);
|
|
470
|
+
recordLifecycle("tool_observer", sessionId);
|
|
471
|
+
const tool = toolNameFromEvent(input, output);
|
|
472
|
+
if (!tool) return;
|
|
473
|
+
const normalizedTool = tool.toLowerCase();
|
|
474
|
+
if (toolResponseIndicatesFailure(output)) return;
|
|
475
|
+
if (normalizedTool.endsWith("query_skills")) {
|
|
476
|
+
if (queryResponseHasMatches(output)) {
|
|
477
|
+
recordRegistryUse(sessionId, env);
|
|
478
|
+
}
|
|
479
|
+
recordHighMatchSurface(sessionId, highMatchFromResponse(output), env);
|
|
480
|
+
recordValueEpisodeSurface(
|
|
481
|
+
sessionId,
|
|
482
|
+
valueEpisodeFromResponse(output),
|
|
483
|
+
env,
|
|
484
|
+
);
|
|
485
|
+
await recordDirectiveFollowThroughForTool(sessionId, tool, output, {
|
|
486
|
+
env,
|
|
487
|
+
});
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
if (normalizedTool.endsWith("invoke_skill")) {
|
|
491
|
+
const selection = directSelectionFromResponse(output);
|
|
492
|
+
if (!selection) return;
|
|
493
|
+
const useCount = recordRegistryUse(sessionId, env);
|
|
494
|
+
recordDirectSelectionSurface(
|
|
495
|
+
sessionId,
|
|
496
|
+
{ ...selection, use_count: useCount },
|
|
497
|
+
env,
|
|
498
|
+
);
|
|
499
|
+
recordValueEpisodeSurface(
|
|
500
|
+
sessionId,
|
|
501
|
+
valueEpisodeFromResponse(output),
|
|
502
|
+
env,
|
|
503
|
+
);
|
|
504
|
+
clearHighMatchSurfaceForExplicitSelection(
|
|
505
|
+
sessionId,
|
|
506
|
+
selection.slug,
|
|
507
|
+
env,
|
|
508
|
+
);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
if (
|
|
512
|
+
CONTRIBUTION_TOOLS.some((candidate) =>
|
|
513
|
+
normalizedTool.endsWith(candidate),
|
|
514
|
+
)
|
|
515
|
+
) {
|
|
516
|
+
if (
|
|
517
|
+
normalizedTool.endsWith("submit_feedback") &&
|
|
518
|
+
responseRequestsRemembranceFollowup(output)
|
|
519
|
+
) {
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
markCurrentEngagementHandled(sessionId, env);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
clearHighMatchSurfaceIfOpened(
|
|
526
|
+
sessionId,
|
|
527
|
+
tool,
|
|
528
|
+
toolArgumentsFromEvent(input),
|
|
529
|
+
env,
|
|
530
|
+
);
|
|
531
|
+
} catch (error) {
|
|
532
|
+
debugLog(env, "opencode_tool_observer_failed", {
|
|
533
|
+
error: String(error),
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
},
|
|
537
|
+
};
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
function toolArgumentsFromEvent(input) {
|
|
541
|
+
const value =
|
|
542
|
+
input?.args ??
|
|
543
|
+
input?.arguments ??
|
|
544
|
+
input?.params ??
|
|
545
|
+
input?.tool_input ??
|
|
546
|
+
input?.toolInput ??
|
|
547
|
+
{};
|
|
548
|
+
return value && typeof value === "object" ? value : {};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
export default Remembrance;
|