@openviking/opencode-plugin 0.1.5 → 0.2.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/INSTALL-ZH.md +50 -49
- package/INSTALL.md +50 -49
- package/README.md +40 -123
- package/index.mjs +20 -12
- package/lib/config.mjs +272 -0
- package/lib/mcp-config.mjs +21 -0
- package/lib/memory-recall.mjs +21 -172
- package/lib/memory-session.mjs +301 -478
- package/lib/repo-context.mjs +5 -5
- package/lib/session-inject.mjs +71 -0
- package/lib/shared/capture-utils.mjs +500 -0
- package/lib/shared/credentials.mjs +207 -0
- package/lib/shared/debug-log.mjs +58 -0
- package/lib/shared/mcp-proxy-core.mjs +451 -0
- package/lib/shared/pending-queue.mjs +439 -0
- package/lib/shared/profile-inject.mjs +294 -0
- package/lib/shared/recall-core.mjs +282 -0
- package/lib/shared/session-model.mjs +57 -0
- package/lib/shared/setup-wizard.mjs +111 -0
- package/lib/shared/uri-guard.mjs +56 -0
- package/lib/utils.mjs +33 -116
- package/lib/viking-uri-guard.mjs +9 -24
- package/package.json +13 -8
- package/scripts/setup.mjs +8 -0
- package/servers/mcp-proxy.mjs +67 -0
- package/servers/mcp-proxy.test.mjs +100 -0
- package/lib/code-tools.mjs +0 -134
- package/lib/memadd-local.mjs +0 -118
- package/lib/memory-tools.mjs +0 -410
package/lib/memory-session.mjs
CHANGED
|
@@ -1,111 +1,127 @@
|
|
|
1
1
|
import fs from "fs"
|
|
2
2
|
import path from "path"
|
|
3
|
+
import {
|
|
4
|
+
extractPartsFromPayload,
|
|
5
|
+
extractTextFromPayload,
|
|
6
|
+
shouldCaptureText,
|
|
7
|
+
} from "./shared/capture-utils.mjs"
|
|
8
|
+
import {
|
|
9
|
+
deriveHarnessSessionId,
|
|
10
|
+
} from "./shared/session-model.mjs"
|
|
11
|
+
import {
|
|
12
|
+
enqueue,
|
|
13
|
+
replayPending,
|
|
14
|
+
} from "./shared/pending-queue.mjs"
|
|
3
15
|
import {
|
|
4
16
|
log,
|
|
5
17
|
effectivePeerId,
|
|
6
|
-
|
|
18
|
+
fetchJSON,
|
|
7
19
|
safeStringify,
|
|
8
|
-
unwrapResponse,
|
|
9
20
|
} from "./utils.mjs"
|
|
10
21
|
|
|
11
|
-
const MAX_BUFFERED_MESSAGES_PER_SESSION = 100
|
|
12
|
-
const BUFFERED_MESSAGE_TTL_MS = 15 * 60 * 1000
|
|
13
|
-
const BUFFER_CLEANUP_INTERVAL_MS = 30 * 1000
|
|
14
|
-
const COMMIT_WAIT_TIMEOUT_MS = 180000
|
|
15
|
-
|
|
16
22
|
export function createMemorySessionManager({ config, pluginRoot }) {
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
let sessionMapPath = path.join(pluginRoot, "openviking-session-map.json")
|
|
23
|
+
const sessions = new Map()
|
|
24
|
+
const statePath = path.join(pluginRoot, "openviking-session-state.json")
|
|
25
|
+
const oldSessionMapPath = path.join(pluginRoot, "openviking-session-map.json")
|
|
21
26
|
let saveTimer = null
|
|
22
|
-
let lastBufferCleanupAt = 0
|
|
23
27
|
|
|
24
28
|
async function init() {
|
|
25
|
-
await
|
|
26
|
-
|
|
29
|
+
await migrateLegacySessionMap()
|
|
30
|
+
await loadState()
|
|
31
|
+
const health = await fetchJSON(config, "/health", {}, { timeoutMs: 5000 })
|
|
32
|
+
if (health.ok) {
|
|
33
|
+
await replayPending(
|
|
34
|
+
(endpoint, init = {}, options = {}) => fetchJSON(config, endpoint, init, options),
|
|
35
|
+
(stage, data) => log("DEBUG", "pending", stage, data),
|
|
36
|
+
)
|
|
37
|
+
}
|
|
27
38
|
}
|
|
28
39
|
|
|
29
|
-
async function
|
|
40
|
+
async function loadState() {
|
|
30
41
|
try {
|
|
31
|
-
if (!fs.existsSync(
|
|
32
|
-
log("INFO", "persistence", "No session
|
|
42
|
+
if (!fs.existsSync(statePath)) {
|
|
43
|
+
log("INFO", "persistence", "No session state file found, starting fresh")
|
|
33
44
|
return
|
|
34
45
|
}
|
|
35
|
-
const data = JSON.parse(await fs.promises.readFile(
|
|
36
|
-
if (data.version !==
|
|
46
|
+
const data = JSON.parse(await fs.promises.readFile(statePath, "utf8"))
|
|
47
|
+
if (data.version !== 2) {
|
|
37
48
|
log("ERROR", "persistence", "Unsupported session map version", { version: data.version })
|
|
38
49
|
return
|
|
39
50
|
}
|
|
40
51
|
for (const [opencodeSessionId, persisted] of Object.entries(data.sessions ?? {})) {
|
|
41
|
-
|
|
52
|
+
sessions.set(opencodeSessionId, deserializeSessionState(persisted))
|
|
42
53
|
}
|
|
43
|
-
log("INFO", "persistence", "Session
|
|
54
|
+
log("INFO", "persistence", "Session state loaded", { count: sessions.size })
|
|
44
55
|
} catch (error) {
|
|
45
|
-
log("ERROR", "persistence", "Failed to load session
|
|
46
|
-
if (fs.existsSync(
|
|
47
|
-
await fs.promises.rename(
|
|
56
|
+
log("ERROR", "persistence", "Failed to load session state", { error: error?.message })
|
|
57
|
+
if (fs.existsSync(statePath)) {
|
|
58
|
+
await fs.promises.rename(statePath, `${statePath}.corrupted.${Date.now()}`)
|
|
48
59
|
}
|
|
49
60
|
}
|
|
50
61
|
}
|
|
51
62
|
|
|
52
|
-
async function
|
|
63
|
+
async function saveState() {
|
|
53
64
|
try {
|
|
54
|
-
const
|
|
55
|
-
for (const [opencodeSessionId,
|
|
56
|
-
|
|
65
|
+
const persisted = {}
|
|
66
|
+
for (const [opencodeSessionId, state] of sessions.entries()) {
|
|
67
|
+
persisted[opencodeSessionId] = serializeSessionState(state)
|
|
57
68
|
}
|
|
58
|
-
const tempPath = `${
|
|
59
|
-
await fs.promises.writeFile(tempPath, JSON.stringify({ version:
|
|
60
|
-
await fs.promises.rename(tempPath,
|
|
61
|
-
log("DEBUG", "persistence", "Session
|
|
69
|
+
const tempPath = `${statePath}.tmp`
|
|
70
|
+
await fs.promises.writeFile(tempPath, JSON.stringify({ version: 2, sessions: persisted, lastSaved: Date.now() }, null, 2), "utf8")
|
|
71
|
+
await fs.promises.rename(tempPath, statePath)
|
|
72
|
+
log("DEBUG", "persistence", "Session state saved", { count: sessions.size })
|
|
62
73
|
} catch (error) {
|
|
63
|
-
log("ERROR", "persistence", "Failed to save session
|
|
74
|
+
log("ERROR", "persistence", "Failed to save session state", { error: error?.message })
|
|
64
75
|
}
|
|
65
76
|
}
|
|
66
77
|
|
|
67
|
-
function
|
|
78
|
+
function debouncedSaveState() {
|
|
68
79
|
if (saveTimer) clearTimeout(saveTimer)
|
|
69
80
|
saveTimer = setTimeout(() => {
|
|
70
|
-
|
|
81
|
+
saveState().catch((error) => {
|
|
71
82
|
log("ERROR", "persistence", "Debounced save failed", { error: error?.message })
|
|
72
83
|
})
|
|
73
84
|
}, 300)
|
|
74
85
|
}
|
|
75
86
|
|
|
76
|
-
function
|
|
87
|
+
function serializeSessionState(state) {
|
|
77
88
|
return {
|
|
78
|
-
ovSessionId:
|
|
79
|
-
createdAt:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
ovSessionId: state.ovSessionId,
|
|
90
|
+
createdAt: state.createdAt,
|
|
91
|
+
lastActivityAt: state.lastActivityAt,
|
|
92
|
+
lastCommitTime: state.lastCommitTime,
|
|
93
|
+
compactedAt: state.compactedAt,
|
|
94
|
+
messages: Array.from(state.messages.entries()).map(([messageId, message]) => ([
|
|
95
|
+
messageId,
|
|
96
|
+
{
|
|
97
|
+
role: message.role,
|
|
98
|
+
captured: message.captured,
|
|
99
|
+
parts: Array.from(message.parts.entries()),
|
|
100
|
+
},
|
|
101
|
+
])),
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function deserializeSessionState(persisted) {
|
|
92
106
|
return {
|
|
93
107
|
ovSessionId: persisted.ovSessionId,
|
|
94
108
|
createdAt: persisted.createdAt,
|
|
95
|
-
|
|
96
|
-
messageRoles: new Map(persisted.messageRoles ?? []),
|
|
97
|
-
pendingMessages: new Map(persisted.pendingMessages ?? []),
|
|
98
|
-
sendingMessages: new Set(),
|
|
109
|
+
lastActivityAt: persisted.lastActivityAt,
|
|
99
110
|
lastCommitTime: persisted.lastCommitTime,
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
111
|
+
compactedAt: persisted.compactedAt,
|
|
112
|
+
messages: new Map((persisted.messages ?? []).map(([messageId, message]) => ([
|
|
113
|
+
messageId,
|
|
114
|
+
{
|
|
115
|
+
role: message.role,
|
|
116
|
+
captured: Boolean(message.captured),
|
|
117
|
+
parts: new Map(message.parts ?? []),
|
|
118
|
+
},
|
|
119
|
+
]))),
|
|
104
120
|
}
|
|
105
121
|
}
|
|
106
122
|
|
|
107
123
|
function getMappedSessionId(opencodeSessionId) {
|
|
108
|
-
return
|
|
124
|
+
return getOrCreateSession(opencodeSessionId).ovSessionId
|
|
109
125
|
}
|
|
110
126
|
|
|
111
127
|
async function handleEvent(event) {
|
|
@@ -119,6 +135,8 @@ export function createMemorySessionManager({ config, pluginRoot }) {
|
|
|
119
135
|
await handleSessionError(event)
|
|
120
136
|
} else if (event.type === "session.compacted") {
|
|
121
137
|
await handleSessionCompacted(event)
|
|
138
|
+
} else if (event.type === "session.idle") {
|
|
139
|
+
await handleSessionIdle(event)
|
|
122
140
|
} else if (event.type === "message.updated") {
|
|
123
141
|
await handleMessageUpdated(event)
|
|
124
142
|
} else if (event.type === "message.part.updated") {
|
|
@@ -132,56 +150,27 @@ export function createMemorySessionManager({ config, pluginRoot }) {
|
|
|
132
150
|
log("ERROR", "event", "session.created event missing sessionId", { event: safeStringify(event) })
|
|
133
151
|
return
|
|
134
152
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const bufferedMessages = sessionMessageBuffer.get(sessionId)
|
|
145
|
-
if (bufferedMessages?.length) {
|
|
146
|
-
for (const buffered of bufferedMessages) {
|
|
147
|
-
if (buffered.role) mapping.messageRoles.set(buffered.messageId, buffered.role)
|
|
148
|
-
if (buffered.content) {
|
|
149
|
-
mapping.pendingMessages.set(
|
|
150
|
-
buffered.messageId,
|
|
151
|
-
mergeMessageContent(mapping.pendingMessages.get(buffered.messageId), buffered.content),
|
|
152
|
-
)
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
sessionMessageBuffer.delete(sessionId)
|
|
156
|
-
await flushPendingMessages(sessionId, mapping)
|
|
153
|
+
const state = getOrCreateSession(sessionId, event)
|
|
154
|
+
debouncedSaveState()
|
|
155
|
+
const health = await fetchJSON(config, "/health", {}, { timeoutMs: 5000 })
|
|
156
|
+
if (health.ok) {
|
|
157
|
+
await replayPending(
|
|
158
|
+
(endpoint, init = {}, options = {}) => fetchJSON(config, endpoint, init, options),
|
|
159
|
+
(stage, data) => log("DEBUG", "pending", stage, data),
|
|
160
|
+
)
|
|
157
161
|
}
|
|
158
|
-
|
|
159
|
-
debouncedSaveSessionMap()
|
|
160
|
-
log("INFO", "event", "Session mapping established", {
|
|
162
|
+
log("INFO", "event", "OpenViking session derived", {
|
|
161
163
|
opencode_session: sessionId,
|
|
162
|
-
openviking_session: ovSessionId,
|
|
164
|
+
openviking_session: state.ovSessionId,
|
|
163
165
|
})
|
|
164
166
|
}
|
|
165
167
|
|
|
166
168
|
async function handleSessionDeleted(event) {
|
|
167
169
|
const sessionId = resolveEventSessionId(event)
|
|
168
170
|
if (!sessionId) return
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
sessionMessageBuffer.delete(sessionId)
|
|
173
|
-
return
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
await flushPendingMessages(sessionId, mapping)
|
|
177
|
-
if (mapping.capturedMessages.size > 0 || mapping.commitInFlight) {
|
|
178
|
-
mapping.pendingCleanup = true
|
|
179
|
-
if (!mapping.commitInFlight) await startBackgroundCommit(mapping, sessionId)
|
|
180
|
-
} else {
|
|
181
|
-
sessionMap.delete(sessionId)
|
|
182
|
-
sessionMessageBuffer.delete(sessionId)
|
|
183
|
-
await saveSessionMap()
|
|
184
|
-
}
|
|
171
|
+
await flushSession(sessionId, { commit: true, reason: event.type })
|
|
172
|
+
sessions.delete(sessionId)
|
|
173
|
+
await saveState()
|
|
185
174
|
}
|
|
186
175
|
|
|
187
176
|
async function handleSessionError(event) {
|
|
@@ -195,26 +184,18 @@ export function createMemorySessionManager({ config, pluginRoot }) {
|
|
|
195
184
|
await commitSessionBoundary(event, "session.compacted")
|
|
196
185
|
}
|
|
197
186
|
|
|
198
|
-
async function
|
|
187
|
+
async function handleSessionIdle(event) {
|
|
199
188
|
const sessionId = resolveEventSessionId(event)
|
|
200
189
|
if (!sessionId) return
|
|
190
|
+
await flushSession(sessionId, { commit: false, reason: "session.idle" })
|
|
191
|
+
}
|
|
201
192
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
return
|
|
209
|
-
}
|
|
210
|
-
if (mapping.capturedMessages.size > 0) {
|
|
211
|
-
log("INFO", "session", "Committing OpenViking session at lifecycle boundary", {
|
|
212
|
-
opencode_session: sessionId,
|
|
213
|
-
openviking_session: mapping.ovSessionId,
|
|
214
|
-
reason,
|
|
215
|
-
})
|
|
216
|
-
await startBackgroundCommit(mapping, sessionId)
|
|
217
|
-
}
|
|
193
|
+
async function commitSessionBoundary(event, reason) {
|
|
194
|
+
const sessionId = resolveEventSessionId(event)
|
|
195
|
+
if (!sessionId) return
|
|
196
|
+
const state = getOrCreateSession(sessionId, event)
|
|
197
|
+
state.compactedAt = Date.now()
|
|
198
|
+
await flushSession(sessionId, { commit: true, reason })
|
|
218
199
|
}
|
|
219
200
|
|
|
220
201
|
async function handleMessageUpdated(event) {
|
|
@@ -227,19 +208,17 @@ export function createMemorySessionManager({ config, pluginRoot }) {
|
|
|
227
208
|
const finish = message.finish
|
|
228
209
|
if (!sessionId || !messageId) return
|
|
229
210
|
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
return
|
|
234
|
-
}
|
|
235
|
-
|
|
211
|
+
const state = getOrCreateSession(sessionId, event)
|
|
212
|
+
const captured = state.messages.get(messageId)
|
|
213
|
+
const next = captured ?? createMessageState()
|
|
236
214
|
if (role === "user") {
|
|
237
|
-
|
|
238
|
-
} else if (role === "assistant"
|
|
239
|
-
|
|
215
|
+
next.role = role
|
|
216
|
+
} else if (role === "assistant") {
|
|
217
|
+
next.role = role
|
|
240
218
|
}
|
|
241
|
-
|
|
242
|
-
|
|
219
|
+
state.messages.set(messageId, next)
|
|
220
|
+
state.lastActivityAt = Date.now()
|
|
221
|
+
debouncedSaveState()
|
|
243
222
|
}
|
|
244
223
|
|
|
245
224
|
async function handleMessagePartUpdated(event) {
|
|
@@ -248,256 +227,16 @@ export function createMemorySessionManager({ config, pluginRoot }) {
|
|
|
248
227
|
|
|
249
228
|
const sessionId = part.sessionID
|
|
250
229
|
const messageId = part.messageID
|
|
251
|
-
if (!sessionId || !messageId
|
|
252
|
-
|
|
253
|
-
const mapping = sessionMap.get(sessionId)
|
|
254
|
-
if (!mapping) {
|
|
255
|
-
upsertBufferedMessage(sessionId, messageId, { content: part.text })
|
|
256
|
-
return
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if (mapping.capturedMessages.has(messageId)) return
|
|
260
|
-
mapping.pendingMessages.set(messageId, mergeMessageContent(mapping.pendingMessages.get(messageId), part.text))
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
async function ensureOpenVikingSession(opencodeSessionId) {
|
|
264
|
-
const knownSessionId = sessionMap.get(opencodeSessionId)?.ovSessionId
|
|
265
|
-
if (knownSessionId) {
|
|
266
|
-
try {
|
|
267
|
-
const response = await makeRequest(config, {
|
|
268
|
-
method: "GET",
|
|
269
|
-
endpoint: `/api/v1/sessions/${encodeURIComponent(knownSessionId)}`,
|
|
270
|
-
timeoutMs: 5000,
|
|
271
|
-
})
|
|
272
|
-
if (unwrapResponse(response)) return knownSessionId
|
|
273
|
-
} catch (error) {
|
|
274
|
-
log("INFO", "session", "Persisted OpenViking session unavailable, creating a new one", {
|
|
275
|
-
opencode_session: opencodeSessionId,
|
|
276
|
-
openviking_session: knownSessionId,
|
|
277
|
-
error: error?.message,
|
|
278
|
-
})
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
try {
|
|
283
|
-
const response = await makeRequest(config, {
|
|
284
|
-
method: "POST",
|
|
285
|
-
endpoint: "/api/v1/sessions",
|
|
286
|
-
body: {},
|
|
287
|
-
timeoutMs: 5000,
|
|
288
|
-
})
|
|
289
|
-
const sessionId = unwrapResponse(response)?.session_id
|
|
290
|
-
if (!sessionId) throw new Error("OpenViking did not return a session_id")
|
|
291
|
-
return sessionId
|
|
292
|
-
} catch (error) {
|
|
293
|
-
log("ERROR", "session", "Failed to create OpenViking session", {
|
|
294
|
-
opencode_session: opencodeSessionId,
|
|
295
|
-
error: error?.message,
|
|
296
|
-
})
|
|
297
|
-
return null
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
async function flushPendingMessages(opencodeSessionId, mapping) {
|
|
302
|
-
if (mapping.commitInFlight) return
|
|
303
|
-
|
|
304
|
-
for (const messageId of Array.from(mapping.pendingMessages.keys())) {
|
|
305
|
-
if (mapping.capturedMessages.has(messageId) || mapping.sendingMessages.has(messageId)) continue
|
|
306
|
-
const role = mapping.messageRoles.get(messageId)
|
|
307
|
-
const content = mapping.pendingMessages.get(messageId)
|
|
308
|
-
if (!role || !content?.trim()) continue
|
|
309
|
-
|
|
310
|
-
mapping.sendingMessages.add(messageId)
|
|
311
|
-
try {
|
|
312
|
-
const success = await addMessageToSession(mapping.ovSessionId, role, content)
|
|
313
|
-
if (success) {
|
|
314
|
-
const latest = mapping.pendingMessages.get(messageId)
|
|
315
|
-
if (latest && latest !== content) {
|
|
316
|
-
continue
|
|
317
|
-
}
|
|
318
|
-
mapping.pendingMessages.delete(messageId)
|
|
319
|
-
mapping.capturedMessages.add(messageId)
|
|
320
|
-
debouncedSaveSessionMap()
|
|
321
|
-
}
|
|
322
|
-
} finally {
|
|
323
|
-
mapping.sendingMessages.delete(messageId)
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
async function addMessageToSession(ovSessionId, role, content) {
|
|
329
|
-
try {
|
|
330
|
-
const body = { role, content }
|
|
331
|
-
const peerId = effectivePeerId(config)
|
|
332
|
-
if (peerId) body.peer_id = peerId
|
|
333
|
-
const response = await makeRequest(config, {
|
|
334
|
-
method: "POST",
|
|
335
|
-
endpoint: `/api/v1/sessions/${encodeURIComponent(ovSessionId)}/messages`,
|
|
336
|
-
body,
|
|
337
|
-
timeoutMs: 5000,
|
|
338
|
-
})
|
|
339
|
-
unwrapResponse(response)
|
|
340
|
-
return true
|
|
341
|
-
} catch (error) {
|
|
342
|
-
log("ERROR", "message", "Failed to add message to OpenViking session", {
|
|
343
|
-
openviking_session: ovSessionId,
|
|
344
|
-
role,
|
|
345
|
-
error: error?.message,
|
|
346
|
-
})
|
|
347
|
-
return false
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
async function startBackgroundCommit(mapping, opencodeSessionId, abortSignal) {
|
|
352
|
-
if (mapping.commitInFlight && mapping.commitTaskId) {
|
|
353
|
-
if (!abortSignal) monitorBackgroundCommit(mapping, opencodeSessionId)
|
|
354
|
-
return { mode: "background", taskId: mapping.commitTaskId }
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
try {
|
|
358
|
-
const response = await makeRequest(config, {
|
|
359
|
-
method: "POST",
|
|
360
|
-
endpoint: `/api/v1/sessions/${encodeURIComponent(mapping.ovSessionId)}/commit`,
|
|
361
|
-
timeoutMs: 10000,
|
|
362
|
-
abortSignal,
|
|
363
|
-
})
|
|
364
|
-
const result = unwrapResponse(response)
|
|
365
|
-
const taskId = result?.task_id
|
|
366
|
-
|
|
367
|
-
if (!taskId) {
|
|
368
|
-
await finalizeCommitSuccess(mapping, opencodeSessionId)
|
|
369
|
-
return { mode: "completed", result }
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
mapping.commitInFlight = true
|
|
373
|
-
mapping.commitTaskId = taskId
|
|
374
|
-
mapping.commitStartedAt = Date.now()
|
|
375
|
-
debouncedSaveSessionMap()
|
|
376
|
-
if (!abortSignal) monitorBackgroundCommit(mapping, opencodeSessionId)
|
|
377
|
-
return { mode: "background", taskId }
|
|
378
|
-
} catch (error) {
|
|
379
|
-
if (error?.message?.includes("already has a commit in progress")) {
|
|
380
|
-
const taskId = await findRunningCommitTaskId(mapping.ovSessionId)
|
|
381
|
-
if (taskId) {
|
|
382
|
-
mapping.commitInFlight = true
|
|
383
|
-
mapping.commitTaskId = taskId
|
|
384
|
-
mapping.commitStartedAt = mapping.commitStartedAt ?? Date.now()
|
|
385
|
-
debouncedSaveSessionMap()
|
|
386
|
-
if (!abortSignal) monitorBackgroundCommit(mapping, opencodeSessionId)
|
|
387
|
-
return { mode: "background", taskId }
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
log("ERROR", "session", "Failed to start OpenViking commit", {
|
|
391
|
-
openviking_session: mapping.ovSessionId,
|
|
392
|
-
opencode_session: opencodeSessionId,
|
|
393
|
-
error: error?.message,
|
|
394
|
-
})
|
|
395
|
-
return null
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
async function waitForCommitCompletion(mapping, opencodeSessionId, abortSignal, timeoutMs = COMMIT_WAIT_TIMEOUT_MS) {
|
|
400
|
-
const startedAt = Date.now()
|
|
401
|
-
while (Date.now() - startedAt < timeoutMs) {
|
|
402
|
-
if (abortSignal?.aborted) throw new Error("Operation aborted")
|
|
403
|
-
if (!mapping.commitInFlight) return null
|
|
404
|
-
if (!mapping.commitTaskId) {
|
|
405
|
-
mapping.commitTaskId = await findRunningCommitTaskId(mapping.ovSessionId)
|
|
406
|
-
if (!mapping.commitTaskId) {
|
|
407
|
-
clearCommitState(mapping)
|
|
408
|
-
debouncedSaveSessionMap()
|
|
409
|
-
return null
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
const task = await getTask(mapping.commitTaskId, abortSignal)
|
|
414
|
-
if (task.status === "completed") {
|
|
415
|
-
await finalizeCommitSuccess(mapping, opencodeSessionId)
|
|
416
|
-
return task
|
|
417
|
-
}
|
|
418
|
-
if (task.status === "failed") {
|
|
419
|
-
clearCommitState(mapping)
|
|
420
|
-
debouncedSaveSessionMap()
|
|
421
|
-
throw new Error(task.error || "Background commit failed")
|
|
422
|
-
}
|
|
423
|
-
await sleep(2000, abortSignal)
|
|
424
|
-
}
|
|
425
|
-
return null
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
async function getTask(taskId, abortSignal) {
|
|
429
|
-
const response = await makeRequest(config, {
|
|
430
|
-
method: "GET",
|
|
431
|
-
endpoint: `/api/v1/tasks/${encodeURIComponent(taskId)}`,
|
|
432
|
-
timeoutMs: 5000,
|
|
433
|
-
abortSignal,
|
|
434
|
-
})
|
|
435
|
-
return unwrapResponse(response)
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
async function findRunningCommitTaskId(ovSessionId) {
|
|
439
|
-
try {
|
|
440
|
-
const response = await makeRequest(config, {
|
|
441
|
-
method: "GET",
|
|
442
|
-
endpoint: `/api/v1/tasks?task_type=session_commit&resource_id=${encodeURIComponent(ovSessionId)}&limit=10`,
|
|
443
|
-
timeoutMs: 5000,
|
|
444
|
-
})
|
|
445
|
-
const tasks = unwrapResponse(response) ?? []
|
|
446
|
-
return tasks.find((task) => task.status === "pending" || task.status === "running")?.task_id
|
|
447
|
-
} catch (error) {
|
|
448
|
-
log("WARN", "session", "Failed to query running commit tasks", { error: error?.message })
|
|
449
|
-
return undefined
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
async function finalizeCommitSuccess(mapping, opencodeSessionId) {
|
|
454
|
-
mapping.lastCommitTime = Date.now()
|
|
455
|
-
mapping.capturedMessages.clear()
|
|
456
|
-
clearCommitState(mapping)
|
|
457
|
-
debouncedSaveSessionMap()
|
|
458
|
-
|
|
459
|
-
await flushPendingMessages(opencodeSessionId, mapping)
|
|
460
|
-
|
|
461
|
-
if (mapping.pendingCleanup) {
|
|
462
|
-
sessionMap.delete(opencodeSessionId)
|
|
463
|
-
sessionMessageBuffer.delete(opencodeSessionId)
|
|
464
|
-
await saveSessionMap()
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
function resumeBackgroundCommits() {
|
|
469
|
-
for (const [opencodeSessionId, mapping] of sessionMap.entries()) {
|
|
470
|
-
if (mapping.commitInFlight) monitorBackgroundCommit(mapping, opencodeSessionId)
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
function monitorBackgroundCommit(mapping, opencodeSessionId) {
|
|
475
|
-
if (!mapping.commitTaskId) return
|
|
476
|
-
if (commitWatchers.has(mapping.commitTaskId)) return
|
|
230
|
+
if (!sessionId || !messageId) return
|
|
477
231
|
|
|
478
|
-
const
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
})
|
|
487
|
-
}
|
|
488
|
-
})
|
|
489
|
-
.catch((error) => {
|
|
490
|
-
log("ERROR", "session", "Background commit watcher failed", {
|
|
491
|
-
task_id: taskId,
|
|
492
|
-
openviking_session: mapping.ovSessionId,
|
|
493
|
-
opencode_session: opencodeSessionId,
|
|
494
|
-
error: error?.message,
|
|
495
|
-
})
|
|
496
|
-
})
|
|
497
|
-
.finally(() => {
|
|
498
|
-
commitWatchers.delete(taskId)
|
|
499
|
-
})
|
|
500
|
-
commitWatchers.set(taskId, watcher)
|
|
232
|
+
const state = getOrCreateSession(sessionId, event)
|
|
233
|
+
const message = state.messages.get(messageId) ?? createMessageState()
|
|
234
|
+
if (message.captured) return
|
|
235
|
+
const partId = part.id ?? `${messageId}:${message.parts.size}`
|
|
236
|
+
message.parts.set(partId, part)
|
|
237
|
+
state.messages.set(messageId, message)
|
|
238
|
+
state.lastActivityAt = Date.now()
|
|
239
|
+
debouncedSaveState()
|
|
501
240
|
}
|
|
502
241
|
|
|
503
242
|
async function flushAll({ commit = false } = {}) {
|
|
@@ -505,60 +244,33 @@ export function createMemorySessionManager({ config, pluginRoot }) {
|
|
|
505
244
|
clearTimeout(saveTimer)
|
|
506
245
|
saveTimer = null
|
|
507
246
|
}
|
|
508
|
-
for (const
|
|
509
|
-
await
|
|
510
|
-
if (commit) {
|
|
511
|
-
if (mapping.commitInFlight) {
|
|
512
|
-
monitorBackgroundCommit(mapping, sessionId)
|
|
513
|
-
} else if (mapping.capturedMessages.size > 0) {
|
|
514
|
-
await startBackgroundCommit(mapping, sessionId)
|
|
515
|
-
}
|
|
516
|
-
}
|
|
247
|
+
for (const sessionId of sessions.keys()) {
|
|
248
|
+
await flushSession(sessionId, { commit, reason: "flushAll" })
|
|
517
249
|
}
|
|
518
|
-
await
|
|
250
|
+
await saveState()
|
|
519
251
|
}
|
|
520
252
|
|
|
521
253
|
async function flushSession(opencodeSessionId, { commit = false, reason = "manual" } = {}) {
|
|
522
|
-
|
|
523
|
-
|
|
254
|
+
if (!opencodeSessionId) return false
|
|
255
|
+
const state = sessions.get(opencodeSessionId)
|
|
256
|
+
if (!state) return false
|
|
524
257
|
|
|
525
|
-
await flushPendingMessages(opencodeSessionId,
|
|
258
|
+
const added = await flushPendingMessages(opencodeSessionId, state)
|
|
526
259
|
if (commit) {
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
log("INFO", "session", "Committing OpenViking session at lifecycle boundary", {
|
|
531
|
-
opencode_session: opencodeSessionId,
|
|
532
|
-
openviking_session: mapping.ovSessionId,
|
|
533
|
-
reason,
|
|
534
|
-
})
|
|
535
|
-
await startBackgroundCommit(mapping, opencodeSessionId)
|
|
536
|
-
}
|
|
260
|
+
await commitOvSession(state.ovSessionId, { force: true, reason })
|
|
261
|
+
} else if (added > 0) {
|
|
262
|
+
await maybeCommitByThreshold(state)
|
|
537
263
|
}
|
|
538
|
-
await
|
|
264
|
+
await saveState()
|
|
539
265
|
return true
|
|
540
266
|
}
|
|
541
267
|
|
|
542
268
|
async function commitSession(sessionId, opencodeSessionId, abortSignal) {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
} else {
|
|
547
|
-
await flushPendingMessages(opencodeSessionId, mapping)
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
if (mapping.commitInFlight) {
|
|
551
|
-
const task = await waitForCommitCompletion(mapping, opencodeSessionId ?? sessionId, abortSignal)
|
|
552
|
-
if (task?.status === "completed") return { status: "completed", task }
|
|
269
|
+
if (opencodeSessionId) {
|
|
270
|
+
const state = sessions.get(opencodeSessionId)
|
|
271
|
+
if (state) await flushPendingMessages(opencodeSessionId, state)
|
|
553
272
|
}
|
|
554
|
-
|
|
555
|
-
const start = await startBackgroundCommit(mapping, opencodeSessionId ?? sessionId, abortSignal)
|
|
556
|
-
if (!start) throw new Error("Failed to start OpenViking session commit")
|
|
557
|
-
if (start.mode === "completed") return { status: "completed", result: start.result }
|
|
558
|
-
|
|
559
|
-
const task = await waitForCommitCompletion(mapping, opencodeSessionId ?? sessionId, abortSignal)
|
|
560
|
-
if (!task) return { status: "accepted", task_id: start.taskId }
|
|
561
|
-
return { status: task.status, task }
|
|
273
|
+
return commitOvSession(sessionId, { force: true, abortSignal, reason: "tool" })
|
|
562
274
|
}
|
|
563
275
|
|
|
564
276
|
return {
|
|
@@ -570,82 +282,193 @@ export function createMemorySessionManager({ config, pluginRoot }) {
|
|
|
570
282
|
flushSession,
|
|
571
283
|
}
|
|
572
284
|
|
|
573
|
-
function
|
|
285
|
+
function createSessionState(opencodeSessionId, event = {}) {
|
|
286
|
+
const parentId = event?.properties?.info?.parentID ?? event?.properties?.parentID ?? event?.parentID ?? ""
|
|
287
|
+
const ovSessionId = parentId
|
|
288
|
+
? deriveHarnessSessionId("oc-", parentId, `subagent-${opencodeSessionId}`)
|
|
289
|
+
: deriveHarnessSessionId("oc-", opencodeSessionId)
|
|
574
290
|
return {
|
|
575
291
|
ovSessionId,
|
|
576
292
|
createdAt: Date.now(),
|
|
577
|
-
|
|
578
|
-
messageRoles: new Map(),
|
|
579
|
-
pendingMessages: new Map(),
|
|
580
|
-
sendingMessages: new Set(),
|
|
293
|
+
lastActivityAt: Date.now(),
|
|
581
294
|
lastCommitTime: undefined,
|
|
582
|
-
|
|
295
|
+
compactedAt: undefined,
|
|
296
|
+
messages: new Map(),
|
|
583
297
|
}
|
|
584
298
|
}
|
|
585
299
|
|
|
586
|
-
function
|
|
587
|
-
return
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
const next = incoming?.trim()
|
|
592
|
-
if (!next) return existing ?? ""
|
|
593
|
-
if (!existing) return next
|
|
594
|
-
if (next === existing) return existing
|
|
595
|
-
if (next.startsWith(existing)) return next
|
|
596
|
-
if (existing.startsWith(next)) return existing
|
|
597
|
-
if (next.includes(existing)) return next
|
|
598
|
-
if (existing.includes(next)) return existing
|
|
599
|
-
return `${existing}\n${next}`.trim()
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
function upsertBufferedMessage(sessionId, messageId, updates) {
|
|
603
|
-
const now = Date.now()
|
|
604
|
-
if (now - lastBufferCleanupAt >= BUFFER_CLEANUP_INTERVAL_MS) {
|
|
605
|
-
cleanupOrphanedMessageBuffers(now)
|
|
606
|
-
lastBufferCleanupAt = now
|
|
300
|
+
function createMessageState() {
|
|
301
|
+
return {
|
|
302
|
+
role: "",
|
|
303
|
+
parts: new Map(),
|
|
304
|
+
captured: false,
|
|
607
305
|
}
|
|
306
|
+
}
|
|
608
307
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
buffered = { messageId, timestamp: now }
|
|
615
|
-
freshBuffer.push(buffered)
|
|
616
|
-
} else {
|
|
617
|
-
buffered.timestamp = now
|
|
308
|
+
function getOrCreateSession(opencodeSessionId, event = {}) {
|
|
309
|
+
let state = sessions.get(opencodeSessionId)
|
|
310
|
+
if (!state) {
|
|
311
|
+
state = createSessionState(opencodeSessionId, event)
|
|
312
|
+
sessions.set(opencodeSessionId, state)
|
|
618
313
|
}
|
|
619
|
-
|
|
620
|
-
if (updates.content) buffered.content = mergeMessageContent(buffered.content, updates.content)
|
|
621
|
-
sessionMessageBuffer.set(sessionId, freshBuffer)
|
|
314
|
+
return state
|
|
622
315
|
}
|
|
623
316
|
|
|
624
|
-
function
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
317
|
+
function resolveEventSessionId(event) {
|
|
318
|
+
return event?.properties?.info?.id ??
|
|
319
|
+
event?.properties?.info?.sessionID ??
|
|
320
|
+
event?.properties?.info?.sessionId ??
|
|
321
|
+
event?.properties?.sessionID ??
|
|
322
|
+
event?.properties?.sessionId ??
|
|
323
|
+
event?.sessionID ??
|
|
324
|
+
event?.sessionId ??
|
|
325
|
+
event?.id
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function resolvePartRole(part, fallbackRole) {
|
|
329
|
+
if (fallbackRole) return fallbackRole
|
|
330
|
+
const type = String(part?.type || part?.kind || "").toLowerCase()
|
|
331
|
+
if (type.includes("tool") && type.includes("call")) return "assistant"
|
|
332
|
+
if (type.includes("tool")) return "user"
|
|
333
|
+
return ""
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function buildCapturePayload(message) {
|
|
337
|
+
const partsRaw = Array.from(message.parts.values())
|
|
338
|
+
if (partsRaw.length === 0) return null
|
|
339
|
+
const role = resolvePartRole(partsRaw[0], message.role)
|
|
340
|
+
if (!role) return null
|
|
341
|
+
if (role === "assistant" && !config.captureAssistantTurns) return null
|
|
342
|
+
|
|
343
|
+
const rawText = partsRaw
|
|
344
|
+
.map((part) => extractTextFromPayload(part, { toolMaxChars: config.captureToolMaxChars }))
|
|
345
|
+
.filter(Boolean)
|
|
346
|
+
.join("\n\n")
|
|
347
|
+
const captureParts = partsRaw.flatMap((part) => extractPartsFromPayload(part, {
|
|
348
|
+
toolMaxChars: config.captureToolMaxChars,
|
|
349
|
+
}))
|
|
350
|
+
const decision = shouldCaptureText(rawText, role, config)
|
|
351
|
+
if (!decision.shouldCapture && captureParts.length === 0) return null
|
|
352
|
+
const body = captureParts.length > 0
|
|
353
|
+
? { role, parts: captureParts }
|
|
354
|
+
: { role, content: decision.text }
|
|
355
|
+
const peerId = effectivePeerId(config)
|
|
356
|
+
if (peerId) body.peer_id = peerId
|
|
357
|
+
return body
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function flushPendingMessages(opencodeSessionId, state) {
|
|
361
|
+
let added = 0
|
|
362
|
+
for (const [messageId, message] of state.messages.entries()) {
|
|
363
|
+
if (message.captured) continue
|
|
364
|
+
const body = buildCapturePayload(message)
|
|
365
|
+
if (!body) {
|
|
366
|
+
message.captured = true
|
|
367
|
+
continue
|
|
630
368
|
}
|
|
369
|
+
const ok = await addMessageToSession(state.ovSessionId, body)
|
|
370
|
+
if (!ok) break
|
|
371
|
+
message.captured = true
|
|
372
|
+
added += 1
|
|
373
|
+
}
|
|
374
|
+
if (added > 0) {
|
|
375
|
+
state.lastActivityAt = Date.now()
|
|
376
|
+
debouncedSaveState()
|
|
377
|
+
}
|
|
378
|
+
return added
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async function addMessageToSession(ovSessionId, body) {
|
|
382
|
+
const health = await fetchJSON(config, "/health", {}, { timeoutMs: 5000 })
|
|
383
|
+
if (!health.ok) {
|
|
384
|
+
await enqueue("addMessage", ovSessionId, body)
|
|
385
|
+
return true
|
|
386
|
+
}
|
|
387
|
+
const res = await fetchJSON(config, `/api/v1/sessions/${encodeURIComponent(ovSessionId)}/messages`, {
|
|
388
|
+
method: "POST",
|
|
389
|
+
body: JSON.stringify(body),
|
|
390
|
+
}, { timeoutMs: 10000 })
|
|
391
|
+
if (res.ok) return true
|
|
392
|
+
if (isRetryableFailure(res)) {
|
|
393
|
+
const queued = await enqueue("addMessage", ovSessionId, body)
|
|
394
|
+
return Boolean(queued.ok)
|
|
631
395
|
}
|
|
396
|
+
log("ERROR", "message", "Failed to add message to OpenViking session", {
|
|
397
|
+
openviking_session: ovSessionId,
|
|
398
|
+
role: body.role,
|
|
399
|
+
status: res.status,
|
|
400
|
+
error: res.error,
|
|
401
|
+
})
|
|
402
|
+
return false
|
|
632
403
|
}
|
|
633
404
|
|
|
634
|
-
function
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
405
|
+
async function maybeCommitByThreshold(state) {
|
|
406
|
+
if (config.commitTokenThreshold <= 0) return { committed: false }
|
|
407
|
+
const meta = await fetchJSON(config, `/api/v1/sessions/${encodeURIComponent(state.ovSessionId)}`, {}, {
|
|
408
|
+
timeoutMs: 5000,
|
|
409
|
+
})
|
|
410
|
+
const pendingTokens = Number(meta.result?.pending_tokens || 0)
|
|
411
|
+
log("DEBUG", "session", "Pending token check", {
|
|
412
|
+
openviking_session: state.ovSessionId,
|
|
413
|
+
pendingTokens,
|
|
414
|
+
threshold: config.commitTokenThreshold,
|
|
415
|
+
})
|
|
416
|
+
if (!meta.ok || pendingTokens < config.commitTokenThreshold) return { committed: false, pendingTokens }
|
|
417
|
+
return commitOvSession(state.ovSessionId, { force: true, reason: "threshold" })
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async function commitOvSession(ovSessionId, { force = false, reason = "manual", abortSignal } = {}) {
|
|
421
|
+
if (!force && config.commitTokenThreshold <= 0) return { status: "skipped" }
|
|
422
|
+
const body = { keep_recent_count: config.commitKeepRecentCount }
|
|
423
|
+
const res = await fetchJSON(config, `/api/v1/sessions/${encodeURIComponent(ovSessionId)}/commit`, {
|
|
424
|
+
method: "POST",
|
|
425
|
+
body: JSON.stringify(body),
|
|
426
|
+
signal: abortSignal,
|
|
427
|
+
}, { timeoutMs: 30000 })
|
|
428
|
+
if (res.ok) {
|
|
429
|
+
for (const state of sessions.values()) {
|
|
430
|
+
if (state.ovSessionId === ovSessionId) state.lastCommitTime = Date.now()
|
|
431
|
+
}
|
|
432
|
+
log("INFO", "session", "Committed OpenViking session", { openviking_session: ovSessionId, reason })
|
|
433
|
+
return { status: "accepted", result: res.result }
|
|
434
|
+
}
|
|
435
|
+
if (isRetryableFailure(res)) {
|
|
436
|
+
await enqueue("commitSession", ovSessionId, body)
|
|
437
|
+
log("WARN", "session", "Queued OpenViking session commit", { openviking_session: ovSessionId, reason })
|
|
438
|
+
return { status: "queued" }
|
|
439
|
+
}
|
|
440
|
+
throw new Error(`Failed to commit OpenViking session ${ovSessionId}: ${res.error?.message || res.status}`)
|
|
638
441
|
}
|
|
639
442
|
|
|
640
|
-
async function
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
const
|
|
645
|
-
|
|
646
|
-
|
|
443
|
+
async function migrateLegacySessionMap() {
|
|
444
|
+
if (!fs.existsSync(oldSessionMapPath)) return
|
|
445
|
+
if (fs.existsSync(`${oldSessionMapPath}.migrated`)) return
|
|
446
|
+
try {
|
|
447
|
+
const data = JSON.parse(await fs.promises.readFile(oldSessionMapPath, "utf8"))
|
|
448
|
+
const ovSessionIds = new Set()
|
|
449
|
+
for (const persisted of Object.values(data.sessions ?? {})) {
|
|
450
|
+
if (persisted?.ovSessionId) ovSessionIds.add(persisted.ovSessionId)
|
|
647
451
|
}
|
|
648
|
-
|
|
649
|
-
|
|
452
|
+
for (const ovSessionId of ovSessionIds) {
|
|
453
|
+
try {
|
|
454
|
+
await commitOvSession(ovSessionId, { force: true, reason: "legacy-migration" })
|
|
455
|
+
} catch (error) {
|
|
456
|
+
log("WARN", "migration", "Legacy orphan session commit failed", {
|
|
457
|
+
openviking_session: ovSessionId,
|
|
458
|
+
error: error?.message,
|
|
459
|
+
})
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
await fs.promises.rename(oldSessionMapPath, `${oldSessionMapPath}.migrated`)
|
|
463
|
+
log("INFO", "migration", "Migrated legacy session map", { count: ovSessionIds.size })
|
|
464
|
+
} catch (error) {
|
|
465
|
+
log("ERROR", "migration", "Failed to migrate legacy session map", { error: error?.message })
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function isRetryableFailure(res) {
|
|
470
|
+
if (!res || res.ok) return false
|
|
471
|
+
const status = Number(res.status || 0)
|
|
472
|
+
return !status || status >= 500 || status === 408 || status === 429
|
|
650
473
|
}
|
|
651
474
|
}
|