@mrc2204/opencode-bridge 0.1.1 → 0.1.2
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/dist/chunk-LCJRXKI3.js +288 -0
- package/dist/index.js +131 -14
- package/dist/observability.d.ts +45 -1
- package/dist/observability.js +3 -1
- package/package.json +1 -1
- package/skills/opencode-orchestration/SKILL.md +32 -0
- package/dist/chunk-6NIQKNRA.js +0 -176
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// src/observability.ts
|
|
2
|
+
function parseSseFramesFromBuffer(input) {
|
|
3
|
+
const normalized = input.replace(/\r\n/g, "\n");
|
|
4
|
+
const parts = normalized.split("\n\n");
|
|
5
|
+
const remainder = parts.pop() ?? "";
|
|
6
|
+
const frames = [];
|
|
7
|
+
for (const rawFrame of parts) {
|
|
8
|
+
const lines = rawFrame.split("\n");
|
|
9
|
+
let event;
|
|
10
|
+
let id;
|
|
11
|
+
let retry;
|
|
12
|
+
const dataLines = [];
|
|
13
|
+
for (const line of lines) {
|
|
14
|
+
if (!line || line.startsWith(":")) continue;
|
|
15
|
+
const idx = line.indexOf(":");
|
|
16
|
+
const field = idx >= 0 ? line.slice(0, idx).trim() : line.trim();
|
|
17
|
+
const value = idx >= 0 ? line.slice(idx + 1).replace(/^\s/, "") : "";
|
|
18
|
+
if (field === "event") event = value;
|
|
19
|
+
else if (field === "id") id = value;
|
|
20
|
+
else if (field === "retry") {
|
|
21
|
+
const n = Number(value);
|
|
22
|
+
if (Number.isFinite(n)) retry = n;
|
|
23
|
+
} else if (field === "data") dataLines.push(value);
|
|
24
|
+
}
|
|
25
|
+
if (dataLines.length <= 0) continue;
|
|
26
|
+
frames.push({
|
|
27
|
+
event,
|
|
28
|
+
id,
|
|
29
|
+
retry,
|
|
30
|
+
data: dataLines.join("\n"),
|
|
31
|
+
raw: rawFrame
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return { frames, remainder };
|
|
35
|
+
}
|
|
36
|
+
function parseSseData(data) {
|
|
37
|
+
const trimmed = data.trim();
|
|
38
|
+
if (!trimmed) return null;
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(trimmed);
|
|
41
|
+
} catch {
|
|
42
|
+
return { raw: trimmed };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function unwrapGlobalPayload(raw) {
|
|
46
|
+
let current = raw;
|
|
47
|
+
const wrappers = [];
|
|
48
|
+
for (let i = 0; i < 3; i += 1) {
|
|
49
|
+
if (!current || typeof current !== "object") break;
|
|
50
|
+
if ("payload" in current && current.payload !== void 0) {
|
|
51
|
+
wrappers.push("payload");
|
|
52
|
+
current = current.payload;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if ("data" in current && current.data !== void 0 && Object.keys(current).length <= 4) {
|
|
56
|
+
wrappers.push("data");
|
|
57
|
+
current = current.data;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
return { payload: current, wrappers };
|
|
63
|
+
}
|
|
64
|
+
function pickFirstString(obj, keys) {
|
|
65
|
+
if (!obj || typeof obj !== "object") return void 0;
|
|
66
|
+
for (const key of keys) {
|
|
67
|
+
const value = obj[key];
|
|
68
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
69
|
+
}
|
|
70
|
+
return void 0;
|
|
71
|
+
}
|
|
72
|
+
function asObject(value) {
|
|
73
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
74
|
+
}
|
|
75
|
+
function asArray(value) {
|
|
76
|
+
return Array.isArray(value) ? value : [];
|
|
77
|
+
}
|
|
78
|
+
function uniqueStrings(values) {
|
|
79
|
+
return [...new Set(values.filter((item) => typeof item === "string" && item.trim()).map((item) => item.trim()))];
|
|
80
|
+
}
|
|
81
|
+
function inferLifecycleState(raw) {
|
|
82
|
+
const text = JSON.stringify(raw || {}).toLowerCase();
|
|
83
|
+
if (text.includes("permission") || text.includes("question.asked")) return "awaiting_permission";
|
|
84
|
+
if (text.includes("blocked") || text.includes("missing config") || text.includes("missing token") || text.includes("missing api_key")) return "blocked";
|
|
85
|
+
if (text.includes("error") || text.includes("failed")) return "failed";
|
|
86
|
+
if (text.includes("stalled")) return "stalled";
|
|
87
|
+
if (text.includes("compile") || text.includes("pytest") || text.includes("test") || text.includes("verify")) return "verifying";
|
|
88
|
+
if (text.includes("apply_patch") || text.includes("diff") || text.includes("edit") || text.includes("write")) return "coding";
|
|
89
|
+
if (text.includes("plan") || text.includes("discovery") || text.includes("ground") || text.includes("calibrate")) return "planning";
|
|
90
|
+
if (text.includes("complete") || text.includes("completed") || text.includes("done")) return "completed";
|
|
91
|
+
return "running";
|
|
92
|
+
}
|
|
93
|
+
function extractFilesChanged(raw) {
|
|
94
|
+
const files = [];
|
|
95
|
+
const root = asObject(raw) || {};
|
|
96
|
+
const payload = asObject(root.payload) || root;
|
|
97
|
+
const state = asObject(root.state) || asObject(payload.state) || {};
|
|
98
|
+
const metadata = asObject(state.metadata) || asObject(payload.metadata) || {};
|
|
99
|
+
for (const file of asArray(metadata.files)) {
|
|
100
|
+
const obj = asObject(file);
|
|
101
|
+
if (!obj) continue;
|
|
102
|
+
if (typeof obj.relativePath === "string") files.push(obj.relativePath);
|
|
103
|
+
else if (typeof obj.filePath === "string") files.push(obj.filePath);
|
|
104
|
+
}
|
|
105
|
+
for (const file of asArray(root.files)) {
|
|
106
|
+
if (typeof file === "string") files.push(file);
|
|
107
|
+
}
|
|
108
|
+
return uniqueStrings(files);
|
|
109
|
+
}
|
|
110
|
+
function extractVerifySummary(raw) {
|
|
111
|
+
const root = asObject(raw) || {};
|
|
112
|
+
const payload = asObject(root.payload) || root;
|
|
113
|
+
const state = asObject(root.state) || asObject(payload.state);
|
|
114
|
+
const input = asObject(state?.input);
|
|
115
|
+
const metadata = asObject(state?.metadata);
|
|
116
|
+
const command = typeof input?.command === "string" ? input.command : void 0;
|
|
117
|
+
const exit = typeof metadata?.exit === "number" ? metadata.exit : null;
|
|
118
|
+
const output = typeof state?.output === "string" ? state.output : typeof metadata?.output === "string" ? metadata.output : null;
|
|
119
|
+
if (!command && output == null) return null;
|
|
120
|
+
return {
|
|
121
|
+
command,
|
|
122
|
+
exit,
|
|
123
|
+
output_preview: typeof output === "string" ? output.slice(0, 500) : null
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function extractBlockers(raw) {
|
|
127
|
+
const text = JSON.stringify(raw || {}).toLowerCase();
|
|
128
|
+
const blockers = [];
|
|
129
|
+
if (text.includes("no module named pytest")) blockers.push("pytest_missing");
|
|
130
|
+
if (text.includes("missing token") || text.includes("missing api_key") || text.includes("pow service not configured")) blockers.push("runtime_config_missing");
|
|
131
|
+
if (text.includes("permission") || text.includes("question.asked")) blockers.push("awaiting_permission");
|
|
132
|
+
return uniqueStrings(blockers);
|
|
133
|
+
}
|
|
134
|
+
function extractCompletionSummary(raw) {
|
|
135
|
+
const root = asObject(raw) || {};
|
|
136
|
+
const text = typeof root.text === "string" ? root.text : typeof root.summary === "string" ? root.summary : null;
|
|
137
|
+
return text ? text.slice(0, 2e3) : null;
|
|
138
|
+
}
|
|
139
|
+
function normalizeOpenCodeEvent(raw) {
|
|
140
|
+
const text = JSON.stringify(raw || {}).toLowerCase();
|
|
141
|
+
let kind = null;
|
|
142
|
+
let summary;
|
|
143
|
+
if (text.includes("permission") || text.includes("question.asked")) {
|
|
144
|
+
kind = "permission.requested";
|
|
145
|
+
summary = "OpenCode \u0111ang ch\u1EDD permission/user input";
|
|
146
|
+
} else if (text.includes("error") || text.includes("failed")) {
|
|
147
|
+
kind = "task.failed";
|
|
148
|
+
summary = "OpenCode task failed";
|
|
149
|
+
} else if (text.includes("stalled")) {
|
|
150
|
+
kind = "task.stalled";
|
|
151
|
+
summary = "OpenCode task stalled";
|
|
152
|
+
} else if (text.includes("complete") || text.includes("completed") || text.includes("done")) {
|
|
153
|
+
kind = "task.completed";
|
|
154
|
+
summary = "OpenCode task completed";
|
|
155
|
+
} else if (text.includes("progress") || text.includes("delta") || text.includes("assistant")) {
|
|
156
|
+
kind = "task.progress";
|
|
157
|
+
summary = "OpenCode task made progress";
|
|
158
|
+
} else if (text.includes("start") || text.includes("created") || text.includes("connected")) {
|
|
159
|
+
kind = "task.started";
|
|
160
|
+
summary = "OpenCode task/server started";
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
kind,
|
|
164
|
+
summary,
|
|
165
|
+
raw,
|
|
166
|
+
lifecycleState: inferLifecycleState(raw),
|
|
167
|
+
filesChanged: extractFilesChanged(raw),
|
|
168
|
+
verifySummary: extractVerifySummary(raw),
|
|
169
|
+
blockers: extractBlockers(raw),
|
|
170
|
+
completionSummary: extractCompletionSummary(raw)
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function normalizeTypedEventV1(frame, scope) {
|
|
174
|
+
const parsed = parseSseData(frame.data);
|
|
175
|
+
const unwrapped = unwrapGlobalPayload(parsed);
|
|
176
|
+
const normalized = normalizeOpenCodeEvent(unwrapped.payload);
|
|
177
|
+
return {
|
|
178
|
+
schema: "opencode.event.v1",
|
|
179
|
+
scope,
|
|
180
|
+
eventName: frame.event,
|
|
181
|
+
eventId: frame.id,
|
|
182
|
+
kind: normalized.kind,
|
|
183
|
+
summary: normalized.summary,
|
|
184
|
+
lifecycleState: normalized.lifecycleState,
|
|
185
|
+
filesChanged: normalized.filesChanged,
|
|
186
|
+
verifySummary: normalized.verifySummary,
|
|
187
|
+
blockers: normalized.blockers,
|
|
188
|
+
completionSummary: normalized.completionSummary,
|
|
189
|
+
runId: pickFirstString(unwrapped.payload, ["run_id", "runId"]),
|
|
190
|
+
taskId: pickFirstString(unwrapped.payload, ["task_id", "taskId"]),
|
|
191
|
+
sessionId: pickFirstString(unwrapped.payload, ["session_id", "sessionId", "session"]),
|
|
192
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
193
|
+
wrappers: unwrapped.wrappers,
|
|
194
|
+
payload: unwrapped.payload
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function asString(value) {
|
|
198
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
199
|
+
}
|
|
200
|
+
function scoreSessionCandidate(candidate, ctx) {
|
|
201
|
+
const id = asString(candidate?.id) || "";
|
|
202
|
+
const candidateText = JSON.stringify(candidate || {}).toLowerCase();
|
|
203
|
+
const runId = ctx.runId?.toLowerCase();
|
|
204
|
+
const taskId = ctx.taskId?.toLowerCase();
|
|
205
|
+
const sessionKey = ctx.sessionKey?.toLowerCase();
|
|
206
|
+
const artifactSessionId = ctx.artifactSessionId;
|
|
207
|
+
let score = 0;
|
|
208
|
+
if (artifactSessionId && id === artifactSessionId) score += 1e3;
|
|
209
|
+
if (runId && candidateText.includes(runId)) score += 120;
|
|
210
|
+
if (taskId && candidateText.includes(taskId)) score += 70;
|
|
211
|
+
if (sessionKey && candidateText.includes(sessionKey)) score += 60;
|
|
212
|
+
if (runId && id.toLowerCase().includes(runId)) score += 30;
|
|
213
|
+
if (taskId && id.toLowerCase().includes(taskId)) score += 20;
|
|
214
|
+
score += Math.max(0, 20 - ctx.recencyRank);
|
|
215
|
+
return score;
|
|
216
|
+
}
|
|
217
|
+
function summarizeLifecycle(events = []) {
|
|
218
|
+
let current_state = null;
|
|
219
|
+
let last_event_kind = null;
|
|
220
|
+
let last_event_at = null;
|
|
221
|
+
const files_changed = /* @__PURE__ */ new Set();
|
|
222
|
+
const verify_summary = [];
|
|
223
|
+
const blockers = /* @__PURE__ */ new Set();
|
|
224
|
+
let completion_summary = null;
|
|
225
|
+
for (const item of events) {
|
|
226
|
+
if (item.lifecycleState) current_state = item.lifecycleState;
|
|
227
|
+
if (item.kind) last_event_kind = item.kind;
|
|
228
|
+
if (item.timestamp) last_event_at = item.timestamp;
|
|
229
|
+
for (const file of item.filesChanged || []) files_changed.add(file);
|
|
230
|
+
if (item.verifySummary) verify_summary.push(item.verifySummary);
|
|
231
|
+
for (const blocker of item.blockers || []) blockers.add(blocker);
|
|
232
|
+
if (item.completionSummary) completion_summary = item.completionSummary;
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
current_state,
|
|
236
|
+
last_event_kind,
|
|
237
|
+
last_event_at,
|
|
238
|
+
files_changed: [...files_changed],
|
|
239
|
+
verify_summary,
|
|
240
|
+
blockers: [...blockers],
|
|
241
|
+
completion_summary
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function resolveSessionId(input) {
|
|
245
|
+
if (input.explicitSessionId) {
|
|
246
|
+
return { sessionId: input.explicitSessionId, strategy: "explicit", score: 9999 };
|
|
247
|
+
}
|
|
248
|
+
const list = Array.isArray(input.sessionList) ? input.sessionList : [];
|
|
249
|
+
const withRecency = [...list].map((item, idx) => {
|
|
250
|
+
const updated = Number(item?.time?.updated || 0);
|
|
251
|
+
return { item, idx, updated };
|
|
252
|
+
}).sort((a, b) => b.updated - a.updated).map((x, rank) => ({ ...x, rank }));
|
|
253
|
+
if (input.artifactSessionId) {
|
|
254
|
+
const direct = withRecency.find((x) => asString(x.item?.id) === input.artifactSessionId);
|
|
255
|
+
if (direct) {
|
|
256
|
+
return { sessionId: input.artifactSessionId, strategy: "artifact", score: 1e3 };
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
let best = { score: -1 };
|
|
260
|
+
for (const candidate of withRecency) {
|
|
261
|
+
const id = asString(candidate.item?.id);
|
|
262
|
+
if (!id) continue;
|
|
263
|
+
const score = scoreSessionCandidate(candidate.item, {
|
|
264
|
+
runId: input.runId,
|
|
265
|
+
taskId: input.taskId,
|
|
266
|
+
sessionKey: input.sessionKey,
|
|
267
|
+
artifactSessionId: input.artifactSessionId,
|
|
268
|
+
recencyRank: candidate.rank
|
|
269
|
+
});
|
|
270
|
+
if (score > best.score) best = { id, score };
|
|
271
|
+
}
|
|
272
|
+
if (best.id && best.score > 0) {
|
|
273
|
+
return { sessionId: best.id, strategy: "scored_fallback", score: best.score };
|
|
274
|
+
}
|
|
275
|
+
const latest = withRecency.find((x) => asString(x.item?.id));
|
|
276
|
+
if (latest) return { sessionId: asString(latest.item?.id), strategy: "latest", score: 0 };
|
|
277
|
+
return { strategy: "none", score: -1 };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export {
|
|
281
|
+
parseSseFramesFromBuffer,
|
|
282
|
+
parseSseData,
|
|
283
|
+
unwrapGlobalPayload,
|
|
284
|
+
normalizeOpenCodeEvent,
|
|
285
|
+
normalizeTypedEventV1,
|
|
286
|
+
summarizeLifecycle,
|
|
287
|
+
resolveSessionId
|
|
288
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
normalizeTypedEventV1,
|
|
3
3
|
parseSseFramesFromBuffer,
|
|
4
|
-
resolveSessionId
|
|
5
|
-
|
|
4
|
+
resolveSessionId,
|
|
5
|
+
summarizeLifecycle
|
|
6
|
+
} from "./chunk-LCJRXKI3.js";
|
|
6
7
|
|
|
7
8
|
// src/runtime.ts
|
|
8
9
|
import { mkdirSync, readFileSync, writeFileSync, existsSync } from "fs";
|
|
@@ -55,6 +56,7 @@ function getRuntimeConfig(cfg) {
|
|
|
55
56
|
return {
|
|
56
57
|
opencodeServerUrl: fileCfg.opencodeServerUrl || cfg?.opencodeServerUrl,
|
|
57
58
|
projectRegistry: fileCfg.projectRegistry || cfg?.projectRegistry || [],
|
|
59
|
+
executionAgentMappings: fileCfg.executionAgentMappings || cfg?.executionAgentMappings || [],
|
|
58
60
|
hookBaseUrl: fileCfg.hookBaseUrl || cfg?.hookBaseUrl,
|
|
59
61
|
hookToken: fileCfg.hookToken || cfg?.hookToken
|
|
60
62
|
};
|
|
@@ -94,13 +96,66 @@ function findRegistryEntry(cfg, projectId, repoRoot) {
|
|
|
94
96
|
}
|
|
95
97
|
return void 0;
|
|
96
98
|
}
|
|
99
|
+
function resolveExecutionAgent(input) {
|
|
100
|
+
const requestedAgentId = asString(input.requestedAgentId);
|
|
101
|
+
if (!requestedAgentId) {
|
|
102
|
+
return {
|
|
103
|
+
ok: false,
|
|
104
|
+
requestedAgentId: "",
|
|
105
|
+
mappingConfigured: false,
|
|
106
|
+
error: "requestedAgentId is required"
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const explicitExecutionAgentId = asString(input.explicitExecutionAgentId);
|
|
110
|
+
if (explicitExecutionAgentId) {
|
|
111
|
+
return {
|
|
112
|
+
ok: true,
|
|
113
|
+
requestedAgentId,
|
|
114
|
+
resolvedAgentId: explicitExecutionAgentId,
|
|
115
|
+
strategy: "explicit_param"
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const runtimeCfg = getRuntimeConfig(input.cfg);
|
|
119
|
+
const mappings = asArray(runtimeCfg.executionAgentMappings).map((x) => x && typeof x === "object" ? x : {}).map((x) => ({
|
|
120
|
+
requestedAgentId: asString(x.requestedAgentId),
|
|
121
|
+
executionAgentId: asString(x.executionAgentId)
|
|
122
|
+
})).filter((x) => x.requestedAgentId && x.executionAgentId);
|
|
123
|
+
if (mappings.length > 0) {
|
|
124
|
+
const matched = mappings.find((x) => x.requestedAgentId === requestedAgentId);
|
|
125
|
+
if (!matched) {
|
|
126
|
+
return {
|
|
127
|
+
ok: false,
|
|
128
|
+
requestedAgentId,
|
|
129
|
+
mappingConfigured: true,
|
|
130
|
+
error: `Execution agent mapping is configured but no mapping matched requestedAgentId=${requestedAgentId}`
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
ok: true,
|
|
135
|
+
requestedAgentId,
|
|
136
|
+
resolvedAgentId: matched.executionAgentId,
|
|
137
|
+
strategy: "config_mapping"
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
ok: true,
|
|
142
|
+
requestedAgentId,
|
|
143
|
+
resolvedAgentId: requestedAgentId,
|
|
144
|
+
strategy: "identity"
|
|
145
|
+
};
|
|
146
|
+
}
|
|
97
147
|
function buildEnvelope(input) {
|
|
98
148
|
return {
|
|
99
149
|
task_id: input.taskId,
|
|
100
150
|
run_id: input.runId,
|
|
101
|
-
agent_id: input.
|
|
102
|
-
|
|
151
|
+
agent_id: input.resolvedAgentId,
|
|
152
|
+
requested_agent_id: input.requestedAgentId,
|
|
153
|
+
resolved_agent_id: input.resolvedAgentId,
|
|
154
|
+
session_key: buildSessionKey(input.resolvedAgentId, input.taskId),
|
|
103
155
|
origin_session_key: input.originSessionKey,
|
|
156
|
+
...input.originSessionId ? { origin_session_id: input.originSessionId } : {},
|
|
157
|
+
callback_target_session_key: input.originSessionKey,
|
|
158
|
+
...input.originSessionId ? { callback_target_session_id: input.originSessionId } : {},
|
|
104
159
|
project_id: input.projectId,
|
|
105
160
|
repo_root: input.repoRoot,
|
|
106
161
|
opencode_server_url: input.serverUrl,
|
|
@@ -113,6 +168,7 @@ function buildEnvelope(input) {
|
|
|
113
168
|
function mapEventToState(event) {
|
|
114
169
|
switch (event) {
|
|
115
170
|
case "task.started":
|
|
171
|
+
return "planning";
|
|
116
172
|
case "task.progress":
|
|
117
173
|
return "running";
|
|
118
174
|
case "permission.requested":
|
|
@@ -314,12 +370,20 @@ async function fetchJsonSafe(url) {
|
|
|
314
370
|
}
|
|
315
371
|
function resolveSessionForRun(input) {
|
|
316
372
|
const artifactEnvelope = input.runStatus?.envelope;
|
|
373
|
+
const callbackTargetSessionId = typeof artifactEnvelope?.callback_target_session_id === "string" ? artifactEnvelope.callback_target_session_id : void 0;
|
|
374
|
+
const originSessionId = typeof artifactEnvelope?.origin_session_id === "string" ? artifactEnvelope.origin_session_id : void 0;
|
|
375
|
+
const callbackTargetSessionKey = typeof artifactEnvelope?.callback_target_session_key === "string" ? artifactEnvelope.callback_target_session_key : void 0;
|
|
376
|
+
const originSessionKey = typeof artifactEnvelope?.origin_session_key === "string" ? artifactEnvelope.origin_session_key : void 0;
|
|
377
|
+
const executionSessionKey = typeof artifactEnvelope?.session_key === "string" ? artifactEnvelope.session_key : void 0;
|
|
378
|
+
const artifactSessionId = callbackTargetSessionId || originSessionId || (typeof artifactEnvelope?.session_id === "string" ? artifactEnvelope.session_id : void 0) || (typeof artifactEnvelope?.sessionId === "string" ? artifactEnvelope.sessionId : void 0);
|
|
379
|
+
const artifactSessionKey = callbackTargetSessionKey || originSessionKey || executionSessionKey;
|
|
380
|
+
const hasCallerSessionKey = Boolean(callbackTargetSessionKey || originSessionKey);
|
|
317
381
|
const resolved = resolveSessionId({
|
|
318
382
|
explicitSessionId: input.sessionId,
|
|
319
|
-
runId: input.runId || input.runStatus?.runId,
|
|
320
|
-
taskId: input.runStatus?.taskId,
|
|
321
|
-
sessionKey:
|
|
322
|
-
artifactSessionId
|
|
383
|
+
runId: hasCallerSessionKey ? void 0 : input.runId || input.runStatus?.runId,
|
|
384
|
+
taskId: hasCallerSessionKey ? void 0 : input.runStatus?.taskId,
|
|
385
|
+
sessionKey: artifactSessionKey,
|
|
386
|
+
artifactSessionId,
|
|
323
387
|
sessionList: input.sessionList
|
|
324
388
|
});
|
|
325
389
|
return { sessionId: resolved.sessionId, strategy: resolved.strategy, ...resolved.score !== void 0 ? { score: resolved.score } : {} };
|
|
@@ -355,6 +419,11 @@ async function collectSseEvents(serverUrl, scope, options) {
|
|
|
355
419
|
data: typed.payload,
|
|
356
420
|
normalizedKind: typed.kind,
|
|
357
421
|
summary: typed.summary,
|
|
422
|
+
lifecycle_state: typed.lifecycleState,
|
|
423
|
+
files_changed: typed.filesChanged,
|
|
424
|
+
verify_summary: typed.verifySummary,
|
|
425
|
+
blockers: typed.blockers,
|
|
426
|
+
completion_summary: typed.completionSummary,
|
|
358
427
|
runId: typed.runId || options?.runIdHint,
|
|
359
428
|
taskId: typed.taskId || options?.taskIdHint,
|
|
360
429
|
sessionId: typed.sessionId || options?.sessionIdHint,
|
|
@@ -377,6 +446,11 @@ async function collectSseEvents(serverUrl, scope, options) {
|
|
|
377
446
|
data: typed.payload,
|
|
378
447
|
normalizedKind: typed.kind,
|
|
379
448
|
summary: typed.summary,
|
|
449
|
+
lifecycle_state: typed.lifecycleState,
|
|
450
|
+
files_changed: typed.filesChanged,
|
|
451
|
+
verify_summary: typed.verifySummary,
|
|
452
|
+
blockers: typed.blockers,
|
|
453
|
+
completion_summary: typed.completionSummary,
|
|
380
454
|
runId: typed.runId || options?.runIdHint,
|
|
381
455
|
taskId: typed.taskId || options?.taskIdHint,
|
|
382
456
|
sessionId: typed.sessionId || options?.sessionIdHint,
|
|
@@ -424,6 +498,7 @@ function buildHookPolicyChecklist(agentId, sessionKey) {
|
|
|
424
498
|
}
|
|
425
499
|
|
|
426
500
|
// src/registrar.ts
|
|
501
|
+
var PLUGIN_VERSION = "0.1.1";
|
|
427
502
|
function registerOpenCodeBridgeTools(api, cfg) {
|
|
428
503
|
console.log("[opencode-bridge] scaffold loaded");
|
|
429
504
|
console.log(`[opencode-bridge] opencodeServerUrl=${cfg.opencodeServerUrl || "(unset)"}`);
|
|
@@ -437,7 +512,7 @@ function registerOpenCodeBridgeTools(api, cfg) {
|
|
|
437
512
|
const runtimeCfg = getRuntimeConfig(cfg);
|
|
438
513
|
const registry = normalizeRegistry(runtimeCfg.projectRegistry);
|
|
439
514
|
return {
|
|
440
|
-
content: [{ type: "text", text: JSON.stringify({ ok: true, pluginId: "opencode-bridge", version:
|
|
515
|
+
content: [{ type: "text", text: JSON.stringify({ ok: true, pluginId: "opencode-bridge", version: PLUGIN_VERSION, assumption: "1 project = 1 opencode serve instance", sessionKeyConvention: "hook:opencode:<agentId>:<taskId>", lifecycleStates: ["queued", "server_ready", "session_created", "prompt_sent", "running", "awaiting_permission", "stalled", "failed", "completed"], requiredEnvelopeFields: ["task_id", "run_id", "agent_id", "requested_agent_id", "resolved_agent_id", "session_key", "origin_session_key", "callback_target_session_key", "project_id", "repo_root", "opencode_server_url"], callbackPrimary: "/hooks/agent", callbackNotPrimary: ["/hooks/wake", "cron", "group:sessions"], config: { bridgeConfigPath: getBridgeConfigPath(), opencodeServerUrl: runtimeCfg.opencodeServerUrl || null, hookBaseUrl: runtimeCfg.hookBaseUrl || null, hookTokenPresent: Boolean(runtimeCfg.hookToken), projectRegistry: registry, executionAgentMappings: runtimeCfg.executionAgentMappings || [], stateDir: getBridgeStateDir(), runStateDir: getRunStateDir(), auditDir: getAuditDir() }, note: "Runtime-ops scaffold in progress. Plugin-owned config/state is stored under ~/.openclaw/opencode-bridge. New projects are auto-registered only when using opencode_serve_spawn (not by passive envelope build alone)." }, null, 2) }]
|
|
441
516
|
};
|
|
442
517
|
}
|
|
443
518
|
}, { optional: true });
|
|
@@ -455,7 +530,7 @@ function registerOpenCodeBridgeTools(api, cfg) {
|
|
|
455
530
|
name: "opencode_build_envelope",
|
|
456
531
|
label: "OpenCode Build Envelope",
|
|
457
532
|
description: "D\u1EF1ng routing envelope chu\u1EA9n cho task delegate sang OpenCode v\u1EDBi sessionKey convention hook:opencode:<agentId>:<taskId>.",
|
|
458
|
-
parameters: { type: "object", properties: { taskId: { type: "string" }, runId: { type: "string" }, agentId: { type: "string" }, originSessionKey: { type: "string" }, projectId: { type: "string" }, repoRoot: { type: "string" }, channel: { type: "string" }, to: { type: "string" }, deliver: { type: "boolean" }, priority: { type: "string" } }, required: ["taskId", "runId", "agentId", "originSessionKey", "projectId", "repoRoot"] },
|
|
533
|
+
parameters: { type: "object", properties: { taskId: { type: "string" }, runId: { type: "string" }, agentId: { type: "string", description: "Requester/origin agent id" }, executionAgentId: { type: "string", description: "Optional explicit OpenCode execution agent id" }, originSessionKey: { type: "string" }, originSessionId: { type: "string" }, projectId: { type: "string" }, repoRoot: { type: "string" }, channel: { type: "string" }, to: { type: "string" }, deliver: { type: "boolean" }, priority: { type: "string" } }, required: ["taskId", "runId", "agentId", "originSessionKey", "projectId", "repoRoot"] },
|
|
459
534
|
async execute(_id, params) {
|
|
460
535
|
const entry = findRegistryEntry(cfg, params?.projectId, params?.repoRoot);
|
|
461
536
|
const serverUrl = entry?.serverUrl;
|
|
@@ -465,11 +540,25 @@ function registerOpenCodeBridgeTools(api, cfg) {
|
|
|
465
540
|
content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Missing project registry mapping. Use opencode_serve_spawn for the project or add a matching projectRegistry entry in ~/.openclaw/opencode-bridge/config.json first." }, null, 2) }]
|
|
466
541
|
};
|
|
467
542
|
}
|
|
543
|
+
const requestedAgentId = asString(params?.agentId);
|
|
544
|
+
const resolved = resolveExecutionAgent({
|
|
545
|
+
cfg,
|
|
546
|
+
requestedAgentId: requestedAgentId || "",
|
|
547
|
+
explicitExecutionAgentId: asString(params?.executionAgentId)
|
|
548
|
+
});
|
|
549
|
+
if (!resolved.ok) {
|
|
550
|
+
return {
|
|
551
|
+
isError: true,
|
|
552
|
+
content: [{ type: "text", text: JSON.stringify({ ok: false, error: resolved.error, requestedAgentId: resolved.requestedAgentId, mappingConfigured: resolved.mappingConfigured }, null, 2) }]
|
|
553
|
+
};
|
|
554
|
+
}
|
|
468
555
|
const envelope = buildEnvelope({
|
|
469
556
|
taskId: params.taskId,
|
|
470
557
|
runId: params.runId,
|
|
471
|
-
|
|
558
|
+
requestedAgentId: resolved.requestedAgentId,
|
|
559
|
+
resolvedAgentId: resolved.resolvedAgentId,
|
|
472
560
|
originSessionKey: params.originSessionKey,
|
|
561
|
+
originSessionId: asString(params.originSessionId),
|
|
473
562
|
projectId: params.projectId,
|
|
474
563
|
repoRoot: params.repoRoot,
|
|
475
564
|
serverUrl,
|
|
@@ -479,7 +568,7 @@ function registerOpenCodeBridgeTools(api, cfg) {
|
|
|
479
568
|
priority: params.priority
|
|
480
569
|
});
|
|
481
570
|
return {
|
|
482
|
-
content: [{ type: "text", text: JSON.stringify({ ok: true, envelope, registryMatch: entry || null }, null, 2) }]
|
|
571
|
+
content: [{ type: "text", text: JSON.stringify({ ok: true, envelope, agentResolution: resolved, registryMatch: entry || null }, null, 2) }]
|
|
483
572
|
};
|
|
484
573
|
}
|
|
485
574
|
}, { optional: true });
|
|
@@ -532,7 +621,27 @@ function registerOpenCodeBridgeTools(api, cfg) {
|
|
|
532
621
|
runId
|
|
533
622
|
});
|
|
534
623
|
const sessionId = resolution.sessionId;
|
|
535
|
-
const
|
|
624
|
+
const eventScope = sessionId ? "session" : "global";
|
|
625
|
+
const events = await collectSseEvents(serverUrl, eventScope, {
|
|
626
|
+
limit: DEFAULT_EVENT_LIMIT,
|
|
627
|
+
timeoutMs: DEFAULT_OBS_TIMEOUT_MS,
|
|
628
|
+
runIdHint: runId,
|
|
629
|
+
taskIdHint: artifact?.taskId,
|
|
630
|
+
sessionIdHint: sessionId
|
|
631
|
+
});
|
|
632
|
+
const lifecycleSummary = summarizeLifecycle(
|
|
633
|
+
events.map((event) => ({
|
|
634
|
+
kind: event.normalizedKind,
|
|
635
|
+
summary: event.summary,
|
|
636
|
+
lifecycleState: event.lifecycle_state,
|
|
637
|
+
filesChanged: event.files_changed,
|
|
638
|
+
verifySummary: event.verify_summary,
|
|
639
|
+
blockers: event.blockers,
|
|
640
|
+
completionSummary: event.completion_summary,
|
|
641
|
+
timestamp: event.timestamp
|
|
642
|
+
}))
|
|
643
|
+
);
|
|
644
|
+
const state = lifecycleSummary.current_state || artifact?.state || (sessionId ? "running" : "queued");
|
|
536
645
|
const response = {
|
|
537
646
|
ok: true,
|
|
538
647
|
source: {
|
|
@@ -550,8 +659,15 @@ function registerOpenCodeBridgeTools(api, cfg) {
|
|
|
550
659
|
}
|
|
551
660
|
},
|
|
552
661
|
state,
|
|
662
|
+
current_state: lifecycleSummary.current_state || state,
|
|
553
663
|
lastEvent: artifact?.lastEvent,
|
|
664
|
+
last_event_kind: artifact?.lastEvent,
|
|
554
665
|
lastSummary: artifact?.lastSummary,
|
|
666
|
+
last_event_at: artifact?.updatedAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
667
|
+
files_changed: lifecycleSummary.files_changed,
|
|
668
|
+
verify_summary: lifecycleSummary.verify_summary,
|
|
669
|
+
blockers: lifecycleSummary.blockers,
|
|
670
|
+
completion_summary: lifecycleSummary.completion_summary || artifact?.lastSummary || null,
|
|
555
671
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
556
672
|
timestamps: {
|
|
557
673
|
...artifact?.updatedAt ? { artifactUpdatedAt: artifact.updatedAt } : {},
|
|
@@ -799,10 +915,11 @@ function registerOpenCodeBridgeTools(api, cfg) {
|
|
|
799
915
|
}
|
|
800
916
|
|
|
801
917
|
// src/index.ts
|
|
918
|
+
var PLUGIN_VERSION2 = "0.1.1";
|
|
802
919
|
var plugin = {
|
|
803
920
|
id: "opencode-bridge",
|
|
804
921
|
name: "OpenCode Bridge",
|
|
805
|
-
version:
|
|
922
|
+
version: PLUGIN_VERSION2,
|
|
806
923
|
register(api) {
|
|
807
924
|
const cfg = api?.pluginConfig || {};
|
|
808
925
|
registerOpenCodeBridgeTools(api, cfg);
|
package/dist/observability.d.ts
CHANGED
|
@@ -14,6 +14,15 @@ type TypedEventV1 = {
|
|
|
14
14
|
eventId?: string;
|
|
15
15
|
kind: OpenCodeEventKind | null;
|
|
16
16
|
summary?: string;
|
|
17
|
+
lifecycleState?: "planning" | "coding" | "verifying" | "blocked" | "running" | "awaiting_permission" | "stalled" | "failed" | "completed";
|
|
18
|
+
filesChanged?: string[];
|
|
19
|
+
verifySummary?: {
|
|
20
|
+
command?: string;
|
|
21
|
+
exit?: number | null;
|
|
22
|
+
output_preview?: string | null;
|
|
23
|
+
} | null;
|
|
24
|
+
blockers?: string[];
|
|
25
|
+
completionSummary?: string | null;
|
|
17
26
|
runId?: string;
|
|
18
27
|
taskId?: string;
|
|
19
28
|
sessionId?: string;
|
|
@@ -34,8 +43,43 @@ declare function normalizeOpenCodeEvent(raw: any): {
|
|
|
34
43
|
kind: OpenCodeEventKind | null;
|
|
35
44
|
summary?: string;
|
|
36
45
|
raw: any;
|
|
46
|
+
lifecycleState?: TypedEventV1["lifecycleState"];
|
|
47
|
+
filesChanged?: string[];
|
|
48
|
+
verifySummary?: {
|
|
49
|
+
command?: string;
|
|
50
|
+
exit?: number | null;
|
|
51
|
+
output_preview?: string | null;
|
|
52
|
+
} | null;
|
|
53
|
+
blockers?: string[];
|
|
54
|
+
completionSummary?: string | null;
|
|
37
55
|
};
|
|
38
56
|
declare function normalizeTypedEventV1(frame: SseFrame, scope: EventScope): TypedEventV1;
|
|
57
|
+
declare function summarizeLifecycle(events?: Array<{
|
|
58
|
+
kind?: OpenCodeEventKind | null;
|
|
59
|
+
summary?: string;
|
|
60
|
+
lifecycleState?: TypedEventV1["lifecycleState"];
|
|
61
|
+
filesChanged?: string[];
|
|
62
|
+
verifySummary?: {
|
|
63
|
+
command?: string;
|
|
64
|
+
exit?: number | null;
|
|
65
|
+
output_preview?: string | null;
|
|
66
|
+
} | null;
|
|
67
|
+
blockers?: string[];
|
|
68
|
+
completionSummary?: string | null;
|
|
69
|
+
timestamp?: string;
|
|
70
|
+
}>): {
|
|
71
|
+
current_state: "planning" | "coding" | "verifying" | "blocked" | "running" | "awaiting_permission" | "stalled" | "failed" | "completed" | null;
|
|
72
|
+
last_event_kind: OpenCodeEventKind | null;
|
|
73
|
+
last_event_at: string | null;
|
|
74
|
+
files_changed: string[];
|
|
75
|
+
verify_summary: {
|
|
76
|
+
command?: string;
|
|
77
|
+
exit?: number | null;
|
|
78
|
+
output_preview?: string | null;
|
|
79
|
+
}[];
|
|
80
|
+
blockers: string[];
|
|
81
|
+
completion_summary: string | null;
|
|
82
|
+
};
|
|
39
83
|
declare function resolveSessionId(input: {
|
|
40
84
|
explicitSessionId?: string;
|
|
41
85
|
runId?: string;
|
|
@@ -49,4 +93,4 @@ declare function resolveSessionId(input: {
|
|
|
49
93
|
score?: number;
|
|
50
94
|
};
|
|
51
95
|
|
|
52
|
-
export { type EventScope, type OpenCodeEventKind, type SseFrame, type TypedEventV1, normalizeOpenCodeEvent, normalizeTypedEventV1, parseSseData, parseSseFramesFromBuffer, resolveSessionId, unwrapGlobalPayload };
|
|
96
|
+
export { type EventScope, type OpenCodeEventKind, type SseFrame, type TypedEventV1, normalizeOpenCodeEvent, normalizeTypedEventV1, parseSseData, parseSseFramesFromBuffer, resolveSessionId, summarizeLifecycle, unwrapGlobalPayload };
|
package/dist/observability.js
CHANGED
|
@@ -4,13 +4,15 @@ import {
|
|
|
4
4
|
parseSseData,
|
|
5
5
|
parseSseFramesFromBuffer,
|
|
6
6
|
resolveSessionId,
|
|
7
|
+
summarizeLifecycle,
|
|
7
8
|
unwrapGlobalPayload
|
|
8
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-LCJRXKI3.js";
|
|
9
10
|
export {
|
|
10
11
|
normalizeOpenCodeEvent,
|
|
11
12
|
normalizeTypedEventV1,
|
|
12
13
|
parseSseData,
|
|
13
14
|
parseSseFramesFromBuffer,
|
|
14
15
|
resolveSessionId,
|
|
16
|
+
summarizeLifecycle,
|
|
15
17
|
unwrapGlobalPayload
|
|
16
18
|
};
|
package/package.json
CHANGED
|
@@ -500,6 +500,38 @@ Agents must describe these limits honestly and avoid over-claiming completion.
|
|
|
500
500
|
|
|
501
501
|
### Execution lane assumptions (MANDATORY)
|
|
502
502
|
|
|
503
|
+
### Current-session callback integrity (MANDATORY)
|
|
504
|
+
|
|
505
|
+
Bridge-aware execution must preserve caller identity and callback destination explicitly.
|
|
506
|
+
|
|
507
|
+
Required fields to preserve whenever available:
|
|
508
|
+
- `requested_agent_id`
|
|
509
|
+
- `resolved_agent_id`
|
|
510
|
+
- `origin_session_key`
|
|
511
|
+
- `origin_session_id`
|
|
512
|
+
- `callback_target_session_key`
|
|
513
|
+
- `callback_target_session_id`
|
|
514
|
+
|
|
515
|
+
Operational rules:
|
|
516
|
+
- Callback must return to the **current caller session**, not an arbitrary latest/execution session.
|
|
517
|
+
- If bridge/runtime cannot resolve the requested execution agent cleanly, it must **fail fast** instead of silently falling back to a default agent.
|
|
518
|
+
- If callback target session information is missing or inconsistent, treat that as a routing integrity error, not a soft warning.
|
|
519
|
+
- Reporting/trace should always expose:
|
|
520
|
+
- requested agent
|
|
521
|
+
- resolved execution agent
|
|
522
|
+
- callback target session key/id
|
|
523
|
+
- whether fallback was used
|
|
524
|
+
|
|
525
|
+
### Runtime hygiene after completion (MANDATORY)
|
|
526
|
+
|
|
527
|
+
When a task or epic is marked `done/closed`, any OpenCode serve/process that was started only for that execution must be shut down immediately unless there is an explicit reason to keep it alive.
|
|
528
|
+
|
|
529
|
+
Operational rules:
|
|
530
|
+
- Do not leave lingering OpenCode serves after work completes.
|
|
531
|
+
- Treat serve shutdown as part of completion hygiene, not optional cleanup.
|
|
532
|
+
- If a serve remains alive intentionally, the agent must report the reason explicitly.
|
|
533
|
+
|
|
534
|
+
|
|
503
535
|
1. **One project = one OpenCode serve instance**
|
|
504
536
|
- Do not assume one shared serve is safe for multiple repos.
|
|
505
537
|
- Always bind execution to a single project/repo root.
|
package/dist/chunk-6NIQKNRA.js
DELETED
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
// src/observability.ts
|
|
2
|
-
function parseSseFramesFromBuffer(input) {
|
|
3
|
-
const normalized = input.replace(/\r\n/g, "\n");
|
|
4
|
-
const parts = normalized.split("\n\n");
|
|
5
|
-
const remainder = parts.pop() ?? "";
|
|
6
|
-
const frames = [];
|
|
7
|
-
for (const rawFrame of parts) {
|
|
8
|
-
const lines = rawFrame.split("\n");
|
|
9
|
-
let event;
|
|
10
|
-
let id;
|
|
11
|
-
let retry;
|
|
12
|
-
const dataLines = [];
|
|
13
|
-
for (const line of lines) {
|
|
14
|
-
if (!line || line.startsWith(":")) continue;
|
|
15
|
-
const idx = line.indexOf(":");
|
|
16
|
-
const field = idx >= 0 ? line.slice(0, idx).trim() : line.trim();
|
|
17
|
-
const value = idx >= 0 ? line.slice(idx + 1).replace(/^\s/, "") : "";
|
|
18
|
-
if (field === "event") event = value;
|
|
19
|
-
else if (field === "id") id = value;
|
|
20
|
-
else if (field === "retry") {
|
|
21
|
-
const n = Number(value);
|
|
22
|
-
if (Number.isFinite(n)) retry = n;
|
|
23
|
-
} else if (field === "data") dataLines.push(value);
|
|
24
|
-
}
|
|
25
|
-
if (dataLines.length <= 0) continue;
|
|
26
|
-
frames.push({
|
|
27
|
-
event,
|
|
28
|
-
id,
|
|
29
|
-
retry,
|
|
30
|
-
data: dataLines.join("\n"),
|
|
31
|
-
raw: rawFrame
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
return { frames, remainder };
|
|
35
|
-
}
|
|
36
|
-
function parseSseData(data) {
|
|
37
|
-
const trimmed = data.trim();
|
|
38
|
-
if (!trimmed) return null;
|
|
39
|
-
try {
|
|
40
|
-
return JSON.parse(trimmed);
|
|
41
|
-
} catch {
|
|
42
|
-
return { raw: trimmed };
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
function unwrapGlobalPayload(raw) {
|
|
46
|
-
let current = raw;
|
|
47
|
-
const wrappers = [];
|
|
48
|
-
for (let i = 0; i < 3; i += 1) {
|
|
49
|
-
if (!current || typeof current !== "object") break;
|
|
50
|
-
if ("payload" in current && current.payload !== void 0) {
|
|
51
|
-
wrappers.push("payload");
|
|
52
|
-
current = current.payload;
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
if ("data" in current && current.data !== void 0 && Object.keys(current).length <= 4) {
|
|
56
|
-
wrappers.push("data");
|
|
57
|
-
current = current.data;
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
break;
|
|
61
|
-
}
|
|
62
|
-
return { payload: current, wrappers };
|
|
63
|
-
}
|
|
64
|
-
function pickFirstString(obj, keys) {
|
|
65
|
-
if (!obj || typeof obj !== "object") return void 0;
|
|
66
|
-
for (const key of keys) {
|
|
67
|
-
const value = obj[key];
|
|
68
|
-
if (typeof value === "string" && value.trim()) return value.trim();
|
|
69
|
-
}
|
|
70
|
-
return void 0;
|
|
71
|
-
}
|
|
72
|
-
function normalizeOpenCodeEvent(raw) {
|
|
73
|
-
const text = JSON.stringify(raw || {}).toLowerCase();
|
|
74
|
-
if (text.includes("permission") || text.includes("question.asked")) {
|
|
75
|
-
return { kind: "permission.requested", summary: "OpenCode \u0111ang ch\u1EDD permission/user input", raw };
|
|
76
|
-
}
|
|
77
|
-
if (text.includes("error") || text.includes("failed")) {
|
|
78
|
-
return { kind: "task.failed", summary: "OpenCode task failed", raw };
|
|
79
|
-
}
|
|
80
|
-
if (text.includes("stalled")) {
|
|
81
|
-
return { kind: "task.stalled", summary: "OpenCode task stalled", raw };
|
|
82
|
-
}
|
|
83
|
-
if (text.includes("complete") || text.includes("completed") || text.includes("done")) {
|
|
84
|
-
return { kind: "task.completed", summary: "OpenCode task completed", raw };
|
|
85
|
-
}
|
|
86
|
-
if (text.includes("progress") || text.includes("delta") || text.includes("assistant")) {
|
|
87
|
-
return { kind: "task.progress", summary: "OpenCode task made progress", raw };
|
|
88
|
-
}
|
|
89
|
-
if (text.includes("start") || text.includes("created") || text.includes("connected")) {
|
|
90
|
-
return { kind: "task.started", summary: "OpenCode task/server started", raw };
|
|
91
|
-
}
|
|
92
|
-
return { kind: null, raw };
|
|
93
|
-
}
|
|
94
|
-
function normalizeTypedEventV1(frame, scope) {
|
|
95
|
-
const parsed = parseSseData(frame.data);
|
|
96
|
-
const unwrapped = unwrapGlobalPayload(parsed);
|
|
97
|
-
const normalized = normalizeOpenCodeEvent(unwrapped.payload);
|
|
98
|
-
return {
|
|
99
|
-
schema: "opencode.event.v1",
|
|
100
|
-
scope,
|
|
101
|
-
eventName: frame.event,
|
|
102
|
-
eventId: frame.id,
|
|
103
|
-
kind: normalized.kind,
|
|
104
|
-
summary: normalized.summary,
|
|
105
|
-
runId: pickFirstString(unwrapped.payload, ["run_id", "runId"]),
|
|
106
|
-
taskId: pickFirstString(unwrapped.payload, ["task_id", "taskId"]),
|
|
107
|
-
sessionId: pickFirstString(unwrapped.payload, ["session_id", "sessionId", "session"]),
|
|
108
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
109
|
-
wrappers: unwrapped.wrappers,
|
|
110
|
-
payload: unwrapped.payload
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
function asString(value) {
|
|
114
|
-
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
115
|
-
}
|
|
116
|
-
function scoreSessionCandidate(candidate, ctx) {
|
|
117
|
-
const id = asString(candidate?.id) || "";
|
|
118
|
-
const candidateText = JSON.stringify(candidate || {}).toLowerCase();
|
|
119
|
-
const runId = ctx.runId?.toLowerCase();
|
|
120
|
-
const taskId = ctx.taskId?.toLowerCase();
|
|
121
|
-
const sessionKey = ctx.sessionKey?.toLowerCase();
|
|
122
|
-
const artifactSessionId = ctx.artifactSessionId;
|
|
123
|
-
let score = 0;
|
|
124
|
-
if (artifactSessionId && id === artifactSessionId) score += 1e3;
|
|
125
|
-
if (runId && candidateText.includes(runId)) score += 120;
|
|
126
|
-
if (taskId && candidateText.includes(taskId)) score += 70;
|
|
127
|
-
if (sessionKey && candidateText.includes(sessionKey)) score += 60;
|
|
128
|
-
if (runId && id.toLowerCase().includes(runId)) score += 30;
|
|
129
|
-
if (taskId && id.toLowerCase().includes(taskId)) score += 20;
|
|
130
|
-
score += Math.max(0, 20 - ctx.recencyRank);
|
|
131
|
-
return score;
|
|
132
|
-
}
|
|
133
|
-
function resolveSessionId(input) {
|
|
134
|
-
if (input.explicitSessionId) {
|
|
135
|
-
return { sessionId: input.explicitSessionId, strategy: "explicit", score: 9999 };
|
|
136
|
-
}
|
|
137
|
-
const list = Array.isArray(input.sessionList) ? input.sessionList : [];
|
|
138
|
-
const withRecency = [...list].map((item, idx) => {
|
|
139
|
-
const updated = Number(item?.time?.updated || 0);
|
|
140
|
-
return { item, idx, updated };
|
|
141
|
-
}).sort((a, b) => b.updated - a.updated).map((x, rank) => ({ ...x, rank }));
|
|
142
|
-
if (input.artifactSessionId) {
|
|
143
|
-
const direct = withRecency.find((x) => asString(x.item?.id) === input.artifactSessionId);
|
|
144
|
-
if (direct) {
|
|
145
|
-
return { sessionId: input.artifactSessionId, strategy: "artifact", score: 1e3 };
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
let best = { score: -1 };
|
|
149
|
-
for (const candidate of withRecency) {
|
|
150
|
-
const id = asString(candidate.item?.id);
|
|
151
|
-
if (!id) continue;
|
|
152
|
-
const score = scoreSessionCandidate(candidate.item, {
|
|
153
|
-
runId: input.runId,
|
|
154
|
-
taskId: input.taskId,
|
|
155
|
-
sessionKey: input.sessionKey,
|
|
156
|
-
artifactSessionId: input.artifactSessionId,
|
|
157
|
-
recencyRank: candidate.rank
|
|
158
|
-
});
|
|
159
|
-
if (score > best.score) best = { id, score };
|
|
160
|
-
}
|
|
161
|
-
if (best.id && best.score > 0) {
|
|
162
|
-
return { sessionId: best.id, strategy: "scored_fallback", score: best.score };
|
|
163
|
-
}
|
|
164
|
-
const latest = withRecency.find((x) => asString(x.item?.id));
|
|
165
|
-
if (latest) return { sessionId: asString(latest.item?.id), strategy: "latest", score: 0 };
|
|
166
|
-
return { strategy: "none", score: -1 };
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export {
|
|
170
|
-
parseSseFramesFromBuffer,
|
|
171
|
-
parseSseData,
|
|
172
|
-
unwrapGlobalPayload,
|
|
173
|
-
normalizeOpenCodeEvent,
|
|
174
|
-
normalizeTypedEventV1,
|
|
175
|
-
resolveSessionId
|
|
176
|
-
};
|