@kkelly-offical/kkcode 0.1.2 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +120 -178
- package/package.json +46 -46
- package/src/agent/agent.mjs +41 -0
- package/src/agent/prompt/frontend-designer.txt +58 -0
- package/src/agent/prompt/longagent-blueprint-agent.txt +83 -0
- package/src/agent/prompt/longagent-coding-agent.txt +37 -0
- package/src/agent/prompt/longagent-debugging-agent.txt +46 -0
- package/src/agent/prompt/longagent-preview-agent.txt +63 -0
- package/src/config/defaults.mjs +260 -195
- package/src/config/schema.mjs +71 -6
- package/src/core/constants.mjs +91 -46
- package/src/index.mjs +1 -1
- package/src/knowledge/frontend-aesthetics.txt +39 -0
- package/src/knowledge/loader.mjs +2 -1
- package/src/knowledge/tailwind.txt +12 -3
- package/src/mcp/client-http.mjs +141 -157
- package/src/mcp/client-sse.mjs +288 -286
- package/src/mcp/client-stdio.mjs +533 -451
- package/src/mcp/constants.mjs +2 -0
- package/src/mcp/registry.mjs +479 -394
- package/src/mcp/stdio-framing.mjs +133 -127
- package/src/mcp/tool-result.mjs +24 -0
- package/src/observability/index.mjs +42 -0
- package/src/observability/metrics.mjs +137 -0
- package/src/observability/tracer.mjs +137 -0
- package/src/orchestration/background-manager.mjs +372 -358
- package/src/orchestration/background-worker.mjs +305 -245
- package/src/orchestration/longagent-manager.mjs +171 -116
- package/src/orchestration/stage-scheduler.mjs +728 -489
- package/src/permission/exec-policy.mjs +9 -11
- package/src/provider/anthropic.mjs +1 -0
- package/src/provider/openai.mjs +340 -339
- package/src/provider/retry-policy.mjs +68 -68
- package/src/provider/router.mjs +241 -228
- package/src/provider/sse.mjs +104 -91
- package/src/repl.mjs +1 -1
- package/src/session/checkpoint.mjs +66 -3
- package/src/session/engine.mjs +227 -225
- package/src/session/longagent-4stage.mjs +460 -0
- package/src/session/longagent-hybrid.mjs +1081 -0
- package/src/session/longagent-plan.mjs +365 -329
- package/src/session/longagent-project-memory.mjs +53 -0
- package/src/session/longagent-scaffold.mjs +291 -100
- package/src/session/longagent-task-bus.mjs +54 -0
- package/src/session/longagent-utils.mjs +472 -0
- package/src/session/longagent.mjs +884 -1462
- package/src/session/project-context.mjs +30 -0
- package/src/session/store.mjs +510 -503
- package/src/session/task-validator.mjs +4 -3
- package/src/skill/builtin/design.mjs +76 -0
- package/src/skill/builtin/frontend.mjs +8 -0
- package/src/skill/registry.mjs +390 -336
- package/src/storage/ghost-commit-store.mjs +18 -8
- package/src/tool/executor.mjs +11 -0
- package/src/tool/git-auto.mjs +0 -19
- package/src/tool/registry.mjs +71 -37
- package/src/ui/activity-renderer.mjs +664 -410
- package/src/util/git.mjs +23 -0
package/src/mcp/client-sse.mjs
CHANGED
|
@@ -1,286 +1,288 @@
|
|
|
1
|
-
import { McpError } from "../core/errors.mjs"
|
|
2
|
-
import { EventBus } from "../core/events.mjs"
|
|
3
|
-
import { EVENT_TYPES } from "../core/constants.mjs"
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* -
|
|
11
|
-
* -
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
let
|
|
21
|
-
let
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
server:
|
|
92
|
-
action: method,
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
let
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
return { ok:
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
return []
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
return []
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
return []
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}
|
|
1
|
+
import { McpError } from "../core/errors.mjs"
|
|
2
|
+
import { EventBus } from "../core/events.mjs"
|
|
3
|
+
import { EVENT_TYPES } from "../core/constants.mjs"
|
|
4
|
+
import { normalizeToolResult } from "./tool-result.mjs"
|
|
5
|
+
import { MCP_PROTOCOL_VERSION, MCP_CLIENT_INFO } from "./constants.mjs"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* MCP Streamable HTTP (SSE) client.
|
|
9
|
+
*
|
|
10
|
+
* Protocol: JSON-RPC 2.0 over HTTP POST with optional SSE response streaming.
|
|
11
|
+
* - POST to endpoint: send JSON-RPC request, receive JSON or SSE stream
|
|
12
|
+
* - GET to endpoint: open persistent SSE stream for server-initiated notifications
|
|
13
|
+
* - Session management via Mcp-Session-Id header
|
|
14
|
+
*/
|
|
15
|
+
export function createSseMcpClient(serverName, config) {
|
|
16
|
+
const baseUrl = String(config.url || "").replace(/\/$/, "")
|
|
17
|
+
const timeoutMs = Number(config.timeout_ms || 30000)
|
|
18
|
+
const headers = config.headers || {}
|
|
19
|
+
|
|
20
|
+
let sessionId = null
|
|
21
|
+
let nextId = 1
|
|
22
|
+
let initialized = false
|
|
23
|
+
let notificationStream = null
|
|
24
|
+
|
|
25
|
+
function buildHeaders(extra = {}) {
|
|
26
|
+
const h = {
|
|
27
|
+
"content-type": "application/json",
|
|
28
|
+
accept: "application/json, text/event-stream",
|
|
29
|
+
...headers,
|
|
30
|
+
...extra
|
|
31
|
+
}
|
|
32
|
+
if (sessionId) h["mcp-session-id"] = sessionId
|
|
33
|
+
return h
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function sendRequest(method, params = {}, { signal: parentSignal = null } = {}) {
|
|
37
|
+
if (nextId > Number.MAX_SAFE_INTEGER - 1) nextId = 1
|
|
38
|
+
const id = nextId++
|
|
39
|
+
const body = { jsonrpc: "2.0", id, method, params }
|
|
40
|
+
const startedAt = Date.now()
|
|
41
|
+
|
|
42
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs)
|
|
43
|
+
const combinedSignal = parentSignal
|
|
44
|
+
? AbortSignal.any([parentSignal, timeoutSignal])
|
|
45
|
+
: timeoutSignal
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const res = await fetch(baseUrl, {
|
|
49
|
+
method: "POST",
|
|
50
|
+
headers: buildHeaders(),
|
|
51
|
+
body: JSON.stringify(body),
|
|
52
|
+
signal: combinedSignal
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const elapsed = Date.now() - startedAt
|
|
56
|
+
|
|
57
|
+
// Capture session ID from response
|
|
58
|
+
const newSessionId = res.headers.get("mcp-session-id")
|
|
59
|
+
if (newSessionId) sessionId = newSessionId
|
|
60
|
+
|
|
61
|
+
EventBus.emit({
|
|
62
|
+
type: EVENT_TYPES.MCP_REQUEST,
|
|
63
|
+
payload: { server: serverName, action: method, elapsed, status: res.status }
|
|
64
|
+
}).catch(() => {})
|
|
65
|
+
|
|
66
|
+
if (!res.ok) {
|
|
67
|
+
const text = await res.text().catch(() => "")
|
|
68
|
+
throw new McpError(
|
|
69
|
+
`mcp server "${serverName}" HTTP ${res.status}: ${text.slice(0, 500)}`,
|
|
70
|
+
{
|
|
71
|
+
reason: res.status >= 500 ? "server_crash" : "bad_response",
|
|
72
|
+
server: serverName,
|
|
73
|
+
action: method,
|
|
74
|
+
phase: "request",
|
|
75
|
+
statusCode: res.status
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const contentType = res.headers.get("content-type") || ""
|
|
81
|
+
|
|
82
|
+
// SSE response — parse events and return the final result
|
|
83
|
+
if (contentType.includes("text/event-stream")) {
|
|
84
|
+
return await parseSseResponse(res.body, id)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Regular JSON response
|
|
88
|
+
const json = await res.json().catch((parseErr) => {
|
|
89
|
+
if (method === "initialize") {
|
|
90
|
+
throw new McpError(
|
|
91
|
+
`mcp server "${serverName}" malformed JSON in initialize response: ${parseErr.message}`,
|
|
92
|
+
{ reason: "bad_response", server: serverName, action: method, phase: "request" }
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
EventBus.emit({
|
|
96
|
+
type: EVENT_TYPES.MCP_REQUEST,
|
|
97
|
+
payload: { server: serverName, action: method, warning: "malformed_json_response" }
|
|
98
|
+
}).catch(() => {})
|
|
99
|
+
return {}
|
|
100
|
+
})
|
|
101
|
+
if (json.error) {
|
|
102
|
+
throw new McpError(
|
|
103
|
+
`mcp server "${serverName}" error: ${json.error.message || JSON.stringify(json.error)}`,
|
|
104
|
+
{ reason: "bad_response", server: serverName, action: method, code: json.error.code, phase: "request" }
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
return json.result ?? json
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (error instanceof McpError) throw error
|
|
110
|
+
const reason = (error.name === "AbortError" || error.name === "TimeoutError") ? "timeout" : "connection_refused"
|
|
111
|
+
throw new McpError(
|
|
112
|
+
`mcp server "${serverName}" ${reason}: ${error.message}`,
|
|
113
|
+
{ reason, server: serverName, action: method, phase: "request" }
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const maxSseBufferBytes = Number(config.max_sse_buffer_bytes || 4 * 1024 * 1024)
|
|
119
|
+
|
|
120
|
+
async function parseSseResponse(body, requestId) {
|
|
121
|
+
const reader = body.getReader()
|
|
122
|
+
const decoder = new TextDecoder()
|
|
123
|
+
let buffer = ""
|
|
124
|
+
let result = null
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
while (true) {
|
|
128
|
+
const { done, value } = await reader.read()
|
|
129
|
+
if (done) break
|
|
130
|
+
buffer += decoder.decode(value, { stream: true })
|
|
131
|
+
if (Buffer.byteLength(buffer, "utf8") > maxSseBufferBytes) {
|
|
132
|
+
try { reader.releaseLock() } catch {}
|
|
133
|
+
throw new McpError(
|
|
134
|
+
`mcp server "${serverName}" SSE buffer exceeded ${maxSseBufferBytes} bytes`,
|
|
135
|
+
{ reason: "bad_response", server: serverName, phase: "request" }
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const parts = buffer.split("\n\n")
|
|
140
|
+
buffer = parts.pop()
|
|
141
|
+
|
|
142
|
+
for (const part of parts) {
|
|
143
|
+
const event = parseSsePart(part)
|
|
144
|
+
if (!event) continue
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const msg = JSON.parse(event.data)
|
|
148
|
+
// Match our request ID
|
|
149
|
+
if (msg.id === requestId) {
|
|
150
|
+
if (msg.error) {
|
|
151
|
+
throw new McpError(
|
|
152
|
+
`mcp server "${serverName}" error: ${msg.error.message || JSON.stringify(msg.error)}`,
|
|
153
|
+
{ reason: "bad_response", server: serverName, code: msg.error.code, phase: "request" }
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
result = msg.result ?? msg
|
|
157
|
+
}
|
|
158
|
+
// Server notifications — emit as events
|
|
159
|
+
if (!msg.id && msg.method) {
|
|
160
|
+
EventBus.emit({
|
|
161
|
+
type: EVENT_TYPES.MCP_REQUEST,
|
|
162
|
+
payload: { server: serverName, action: `notification:${msg.method}`, notification: true }
|
|
163
|
+
}).catch(() => {})
|
|
164
|
+
}
|
|
165
|
+
} catch (e) {
|
|
166
|
+
if (e instanceof McpError) throw e
|
|
167
|
+
// Non-JSON SSE data — skip
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} finally {
|
|
172
|
+
try { reader.releaseLock() } catch { /* reader may have pending read if stream was force-closed */ }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return result ?? {}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function parseSsePart(part) {
|
|
179
|
+
const trimmed = part.trim()
|
|
180
|
+
if (!trimmed) return null
|
|
181
|
+
let event = null
|
|
182
|
+
let data = ""
|
|
183
|
+
for (const line of trimmed.split("\n")) {
|
|
184
|
+
if (line.startsWith("event:")) event = line.slice(6).trim()
|
|
185
|
+
else if (line.startsWith("data:")) data += line.slice(5).trim()
|
|
186
|
+
}
|
|
187
|
+
if (!data) return null
|
|
188
|
+
return { event, data }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function ensureInitialized() {
|
|
192
|
+
if (initialized) return
|
|
193
|
+
const result = await sendRequest("initialize", {
|
|
194
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
195
|
+
capabilities: {},
|
|
196
|
+
clientInfo: MCP_CLIENT_INFO
|
|
197
|
+
})
|
|
198
|
+
// Send initialized notification
|
|
199
|
+
try {
|
|
200
|
+
await fetch(baseUrl, {
|
|
201
|
+
method: "POST",
|
|
202
|
+
headers: buildHeaders(),
|
|
203
|
+
body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }),
|
|
204
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
205
|
+
})
|
|
206
|
+
} catch { /* best-effort */ }
|
|
207
|
+
initialized = true
|
|
208
|
+
return result
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
serverName,
|
|
213
|
+
transport: "sse",
|
|
214
|
+
|
|
215
|
+
async health() {
|
|
216
|
+
try {
|
|
217
|
+
await ensureInitialized()
|
|
218
|
+
await sendRequest("ping")
|
|
219
|
+
return { ok: true }
|
|
220
|
+
} catch (error) {
|
|
221
|
+
return { ok: false, error: error.message, reason: error.reason || "unknown" }
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
async listTools() {
|
|
226
|
+
await ensureInitialized()
|
|
227
|
+
const out = await sendRequest("tools/list")
|
|
228
|
+
return Array.isArray(out?.tools) ? out.tools : []
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
async listPrompts() {
|
|
232
|
+
await ensureInitialized()
|
|
233
|
+
try {
|
|
234
|
+
const out = await sendRequest("prompts/list")
|
|
235
|
+
return Array.isArray(out?.prompts) ? out.prompts : []
|
|
236
|
+
} catch {
|
|
237
|
+
return []
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
async getPrompt(name, args = {}) {
|
|
242
|
+
await ensureInitialized()
|
|
243
|
+
return sendRequest("prompts/get", { name, arguments: args })
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
async listResources() {
|
|
247
|
+
await ensureInitialized()
|
|
248
|
+
try {
|
|
249
|
+
const out = await sendRequest("resources/list")
|
|
250
|
+
return Array.isArray(out?.resources) ? out.resources : []
|
|
251
|
+
} catch {
|
|
252
|
+
return []
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
async listTemplates() {
|
|
257
|
+
await ensureInitialized()
|
|
258
|
+
try {
|
|
259
|
+
const out = await sendRequest("resources/templates/list")
|
|
260
|
+
return Array.isArray(out?.templates) ? out.templates : []
|
|
261
|
+
} catch {
|
|
262
|
+
return []
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
async callTool(name, args = {}, signal = null) {
|
|
267
|
+
await ensureInitialized()
|
|
268
|
+
const result = await sendRequest("tools/call", { name, arguments: args }, { signal })
|
|
269
|
+
return normalizeToolResult(result, serverName, name)
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
shutdown() {
|
|
273
|
+
if (notificationStream) {
|
|
274
|
+
try { notificationStream.cancel() } catch { /* ignore */ }
|
|
275
|
+
notificationStream = null
|
|
276
|
+
}
|
|
277
|
+
// Send session termination if we have a session
|
|
278
|
+
if (sessionId) {
|
|
279
|
+
fetch(baseUrl, {
|
|
280
|
+
method: "DELETE",
|
|
281
|
+
headers: buildHeaders()
|
|
282
|
+
}).catch(() => {})
|
|
283
|
+
}
|
|
284
|
+
sessionId = null
|
|
285
|
+
initialized = false
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|