tandem-editor 0.11.2 → 0.13.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +3 -3
- package/CHANGELOG.md +201 -72
- package/README.md +141 -238
- package/dist/channel/index.js +211 -81
- package/dist/channel/index.js.map +1 -1
- package/dist/cli/index.js +749 -170
- package/dist/cli/index.js.map +1 -1
- package/dist/client/assets/CoworkSettings-BOYbyKul.js +3 -0
- package/dist/client/assets/event-CNdo2oXa.js +1 -0
- package/dist/client/assets/index-D8uS4cj7.css +1 -0
- package/dist/client/assets/index-Dm_QtxGQ.js +1 -0
- package/dist/client/assets/index-g-KwmRn9.js +271 -0
- package/dist/client/assets/webview-KiZyy_pC.js +1 -0
- package/dist/client/assets/window-DePn7tLG.js +1 -0
- package/dist/client/fonts/OFL-Hanuman.txt +93 -0
- package/dist/client/fonts/OFL-InterTight.txt +93 -0
- package/dist/client/fonts/OFL-JetBrainsMono.txt +93 -0
- package/dist/client/fonts/OFL-SNPro.txt +93 -0
- package/dist/client/fonts/OFL-Sono.txt +93 -0
- package/dist/client/fonts/OFL-SourceSerif4.txt +93 -0
- package/dist/client/fonts/hanuman-latin.woff2 +0 -0
- package/dist/client/fonts/jetbrains-mono-latin.woff2 +0 -0
- package/dist/client/fonts/sn-pro-latin.woff2 +0 -0
- package/dist/client/fonts/sono-latin.woff2 +0 -0
- package/dist/client/fonts/source-serif-4-latin.woff2 +0 -0
- package/dist/client/index.html +206 -17
- package/dist/client/logo.png +0 -0
- package/dist/monitor/index.js +241 -160
- package/dist/monitor/index.js.map +1 -1
- package/dist/server/index.js +22828 -19659
- package/dist/server/index.js.map +1 -1
- package/package.json +12 -4
- package/sample/welcome.md +6 -6
- package/skills/tandem/SKILL.md +15 -0
- package/dist/client/assets/CoworkSettings-DK3jjdwK.js +0 -3
- package/dist/client/assets/index-CfT503n4.js +0 -297
- package/dist/client/assets/index-DeJe09pn.css +0 -1
- package/dist/client/assets/webview-Ben21ZLJ.js +0 -1
- package/dist/client/assets/window-BxBvHL5k.js +0 -1
package/dist/monitor/index.js
CHANGED
|
@@ -14,12 +14,16 @@ var DEFAULT_MCP_PORT = 3479;
|
|
|
14
14
|
var TANDEM_REPO_URL = "https://github.com/bloknayrb/tandem";
|
|
15
15
|
var TANDEM_ISSUES_NEW_URL = `${TANDEM_REPO_URL}/issues/new`;
|
|
16
16
|
var MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
17
|
-
var MAX_WS_PAYLOAD = 10 * 1024 * 1024;
|
|
18
|
-
var IDLE_TIMEOUT = 30 * 60 * 1e3;
|
|
19
17
|
var SESSION_MAX_AGE = 30 * 24 * 60 * 60 * 1e3;
|
|
20
18
|
var TANDEM_MODE_DEFAULT = "tandem";
|
|
21
19
|
var CHANNEL_MAX_RETRIES = 5;
|
|
22
20
|
var CHANNEL_RETRY_DELAY_MS = 2e3;
|
|
21
|
+
var CHANNEL_CONNECT_FETCH_TIMEOUT_MS = 1e4;
|
|
22
|
+
var CHANNEL_SSE_INACTIVITY_TIMEOUT_MS = 6e4;
|
|
23
|
+
var CHANNEL_MODE_FETCH_TIMEOUT_MS = 2e3;
|
|
24
|
+
var CHANNEL_AWARENESS_FETCH_TIMEOUT_MS = 5e3;
|
|
25
|
+
var CHANNEL_ERROR_REPORT_TIMEOUT_MS = 3e3;
|
|
26
|
+
var CHANNEL_MAX_SSE_BUFFER_BYTES = 1e6;
|
|
23
27
|
|
|
24
28
|
// src/shared/cli-runtime.ts
|
|
25
29
|
function resolveTandemUrl(override) {
|
|
@@ -34,7 +38,7 @@ function resolveTandemUrlCandidate(override) {
|
|
|
34
38
|
for (const url of candidates) {
|
|
35
39
|
if (url !== void 0 && url.trim() !== "") return url.trim();
|
|
36
40
|
}
|
|
37
|
-
return `http://
|
|
41
|
+
return `http://127.0.0.1:${DEFAULT_MCP_PORT}`;
|
|
38
42
|
}
|
|
39
43
|
function resolveAuthTokenCandidate(override) {
|
|
40
44
|
const candidates = [
|
|
@@ -139,6 +143,12 @@ function formatEventContent(event) {
|
|
|
139
143
|
}
|
|
140
144
|
}
|
|
141
145
|
|
|
146
|
+
// src/shared/api-paths.ts
|
|
147
|
+
var API_EVENTS = "/api/events";
|
|
148
|
+
var API_CHANNEL_AWARENESS = "/api/channel-awareness";
|
|
149
|
+
var API_CHANNEL_ERROR = "/api/channel-error";
|
|
150
|
+
var API_MODE = "/api/mode";
|
|
151
|
+
|
|
142
152
|
// src/shared/fetch-with-timeout.ts
|
|
143
153
|
async function fetchWithTimeout(url, init, timeoutMs) {
|
|
144
154
|
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
@@ -4211,107 +4221,104 @@ var ToolErrorCodeSchema = external_exports.enum([
|
|
|
4211
4221
|
"FILE_NOT_FOUND",
|
|
4212
4222
|
"NO_DOCUMENT",
|
|
4213
4223
|
"INVALID_RANGE",
|
|
4224
|
+
"INVALID_ARGUMENT",
|
|
4225
|
+
"NOT_FOUND",
|
|
4226
|
+
"ANNOTATION_RESOLVED",
|
|
4214
4227
|
"FORMAT_ERROR",
|
|
4215
4228
|
"PERMISSION_DENIED"
|
|
4216
4229
|
]);
|
|
4230
|
+
var ChannelErrorCodeSchema = external_exports.enum(["CHANNEL_CONNECT_FAILED", "MONITOR_CONNECT_FAILED"]);
|
|
4231
|
+
var MONITOR_CONNECT_FAILED = "MONITOR_CONNECT_FAILED";
|
|
4217
4232
|
|
|
4218
|
-
// src/
|
|
4219
|
-
var IS_VITEST = process.env.VITEST === "true";
|
|
4220
|
-
if (!IS_VITEST) {
|
|
4221
|
-
console.log = console.error;
|
|
4222
|
-
console.warn = console.error;
|
|
4223
|
-
console.info = console.error;
|
|
4224
|
-
}
|
|
4225
|
-
var TANDEM_URL = resolveTandemUrl();
|
|
4233
|
+
// src/shared/sse-consumer.ts
|
|
4226
4234
|
var AWARENESS_DEBOUNCE_MS = 500;
|
|
4227
4235
|
var AWARENESS_CLEAR_MS = 3e3;
|
|
4228
4236
|
var MODE_CACHE_TTL_MS = 2e3;
|
|
4229
|
-
var MAX_SSE_BUFFER_BYTES = 1e6;
|
|
4230
|
-
var CONNECT_FETCH_TIMEOUT_MS = 1e4;
|
|
4231
|
-
var SSE_INACTIVITY_TIMEOUT_MS = 6e4;
|
|
4232
|
-
var MODE_FETCH_TIMEOUT_MS = 2e3;
|
|
4233
|
-
var AWARENESS_FETCH_TIMEOUT_MS = 5e3;
|
|
4234
|
-
var ERROR_REPORT_TIMEOUT_MS = 3e3;
|
|
4235
4237
|
var STABLE_CONNECTION_MS = 6e4;
|
|
4236
|
-
var
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4238
|
+
var RETRY_MAX_DELAY_MS = 3e4;
|
|
4239
|
+
var shutdownTimers = { awarenessTimer: null, clearAwarenessTimer: null, lastDocumentId: null };
|
|
4240
|
+
var outstandingAwareness = /* @__PURE__ */ new Set();
|
|
4241
|
+
function trackAwareness(p) {
|
|
4242
|
+
outstandingAwareness.add(p);
|
|
4243
|
+
p.finally(() => outstandingAwareness.delete(p));
|
|
4244
|
+
}
|
|
4245
|
+
var cachedMode = TANDEM_MODE_DEFAULT;
|
|
4246
|
+
var cachedModeAt = 0;
|
|
4247
|
+
var cachedModeFailedAt = 0;
|
|
4248
|
+
var _modeRefreshInFlight = null;
|
|
4249
|
+
async function runEventConsumer(opts) {
|
|
4250
|
+
await getCachedMode(opts.tandemUrl, opts.logPrefix).catch(() => {
|
|
4242
4251
|
});
|
|
4243
4252
|
let retries = 0;
|
|
4244
4253
|
let lastEventId;
|
|
4245
4254
|
while (retries < CHANNEL_MAX_RETRIES) {
|
|
4246
4255
|
try {
|
|
4247
|
-
await
|
|
4248
|
-
|
|
4249
|
-
(id) => {
|
|
4256
|
+
await connectAndStreamOnce(opts, lastEventId, {
|
|
4257
|
+
onEventId: (id) => {
|
|
4250
4258
|
lastEventId = id;
|
|
4251
4259
|
},
|
|
4252
|
-
() => {
|
|
4260
|
+
onStable: () => {
|
|
4253
4261
|
retries = 0;
|
|
4254
4262
|
}
|
|
4255
|
-
);
|
|
4263
|
+
});
|
|
4256
4264
|
} catch (err) {
|
|
4257
4265
|
retries++;
|
|
4258
4266
|
console.error(
|
|
4259
|
-
|
|
4267
|
+
`${opts.logPrefix} SSE connection failed (${retries}/${CHANNEL_MAX_RETRIES}):`,
|
|
4260
4268
|
err instanceof Error ? err.message : err
|
|
4261
4269
|
);
|
|
4262
4270
|
if (retries >= CHANNEL_MAX_RETRIES) {
|
|
4263
|
-
console.error(
|
|
4271
|
+
console.error(`${opts.logPrefix} SSE connection exhausted, reporting error and exiting`);
|
|
4264
4272
|
try {
|
|
4265
4273
|
await fetchWithTimeout(
|
|
4266
|
-
`${
|
|
4274
|
+
`${opts.tandemUrl}${API_CHANNEL_ERROR}`,
|
|
4267
4275
|
{
|
|
4268
4276
|
method: "POST",
|
|
4269
4277
|
headers: { "Content-Type": "application/json" },
|
|
4270
4278
|
body: JSON.stringify({
|
|
4271
|
-
error:
|
|
4272
|
-
message:
|
|
4279
|
+
error: opts.errorCode,
|
|
4280
|
+
message: `${opts.logPrefix} lost connection after ${CHANNEL_MAX_RETRIES} retries.`
|
|
4273
4281
|
})
|
|
4274
4282
|
},
|
|
4275
|
-
|
|
4283
|
+
CHANNEL_ERROR_REPORT_TIMEOUT_MS
|
|
4276
4284
|
);
|
|
4277
4285
|
} catch (reportErr) {
|
|
4278
4286
|
console.error(
|
|
4279
|
-
|
|
4280
|
-
reportErr
|
|
4287
|
+
`${opts.logPrefix} Could not report failure to server:`,
|
|
4288
|
+
describeFetchError(reportErr, API_CHANNEL_ERROR, CHANNEL_ERROR_REPORT_TIMEOUT_MS)
|
|
4281
4289
|
);
|
|
4282
4290
|
}
|
|
4283
|
-
|
|
4284
|
-
"Tandem monitor disconnected \u2014 restart Tandem to restore real-time events\n"
|
|
4285
|
-
);
|
|
4291
|
+
opts.onExhaustion?.();
|
|
4286
4292
|
process.exit(1);
|
|
4287
4293
|
}
|
|
4288
|
-
const delay = Math.min(
|
|
4289
|
-
CHANNEL_RETRY_DELAY_MS * 2 ** (retries - 1),
|
|
4290
|
-
CHANNEL_RETRY_MAX_DELAY_MS
|
|
4291
|
-
);
|
|
4294
|
+
const delay = Math.min(CHANNEL_RETRY_DELAY_MS * 2 ** (retries - 1), RETRY_MAX_DELAY_MS);
|
|
4292
4295
|
console.error(
|
|
4293
|
-
|
|
4296
|
+
`${opts.logPrefix} Retrying in ${delay}ms (attempt ${retries}/${CHANNEL_MAX_RETRIES})...`
|
|
4294
4297
|
);
|
|
4295
4298
|
await new Promise((r) => setTimeout(r, delay));
|
|
4296
4299
|
}
|
|
4297
4300
|
}
|
|
4298
4301
|
console.error(
|
|
4299
|
-
|
|
4302
|
+
`${opts.logPrefix} Retry loop exited unexpectedly (retries=${retries}/${CHANNEL_MAX_RETRIES})`
|
|
4300
4303
|
);
|
|
4301
4304
|
process.exit(1);
|
|
4302
4305
|
}
|
|
4303
|
-
async function
|
|
4304
|
-
|
|
4306
|
+
async function connectAndStreamOnce(opts, lastEventId, cb) {
|
|
4307
|
+
const onStable = cb.onStable ?? (() => {
|
|
4308
|
+
});
|
|
4305
4309
|
const headers = { Accept: "text/event-stream" };
|
|
4306
4310
|
if (lastEventId) headers["Last-Event-ID"] = lastEventId;
|
|
4307
4311
|
const connectCtrl = new AbortController();
|
|
4308
4312
|
const connectTimer = setTimeout(
|
|
4309
4313
|
() => connectCtrl.abort(new Error("handshake timeout")),
|
|
4310
|
-
|
|
4314
|
+
CHANNEL_CONNECT_FETCH_TIMEOUT_MS
|
|
4311
4315
|
);
|
|
4312
4316
|
let res;
|
|
4313
4317
|
try {
|
|
4314
|
-
res = await authFetch(`${
|
|
4318
|
+
res = await authFetch(`${opts.tandemUrl}${API_EVENTS}`, {
|
|
4319
|
+
headers,
|
|
4320
|
+
signal: connectCtrl.signal
|
|
4321
|
+
});
|
|
4315
4322
|
} finally {
|
|
4316
4323
|
clearTimeout(connectTimer);
|
|
4317
4324
|
}
|
|
@@ -4324,16 +4331,16 @@ async function connectAndStream(lastEventId, onEventId, onStable = () => {
|
|
|
4324
4331
|
let lastActivityAt = Date.now();
|
|
4325
4332
|
let inactivityTimedOut = false;
|
|
4326
4333
|
const watchdog = setInterval(() => {
|
|
4327
|
-
if (Date.now() - lastActivityAt >
|
|
4334
|
+
if (Date.now() - lastActivityAt > CHANNEL_SSE_INACTIVITY_TIMEOUT_MS) {
|
|
4328
4335
|
inactivityTimedOut = true;
|
|
4329
4336
|
reader.cancel(new Error("SSE inactivity timeout")).catch(() => {
|
|
4330
4337
|
});
|
|
4331
4338
|
}
|
|
4332
|
-
},
|
|
4339
|
+
}, CHANNEL_SSE_INACTIVITY_TIMEOUT_MS / 4);
|
|
4333
4340
|
let pendingAwareness = null;
|
|
4334
|
-
function
|
|
4341
|
+
function clearAwarenessNow(documentId) {
|
|
4335
4342
|
const p = fetchWithTimeout(
|
|
4336
|
-
`${
|
|
4343
|
+
`${opts.tandemUrl}${API_CHANNEL_AWARENESS}`,
|
|
4337
4344
|
{
|
|
4338
4345
|
method: "POST",
|
|
4339
4346
|
headers: { "Content-Type": "application/json" },
|
|
@@ -4343,11 +4350,15 @@ async function connectAndStream(lastEventId, onEventId, onStable = () => {
|
|
|
4343
4350
|
active: false
|
|
4344
4351
|
})
|
|
4345
4352
|
},
|
|
4346
|
-
|
|
4353
|
+
CHANNEL_AWARENESS_FETCH_TIMEOUT_MS
|
|
4347
4354
|
).catch((err) => {
|
|
4348
4355
|
console.error(
|
|
4349
|
-
|
|
4350
|
-
describeFetchError(
|
|
4356
|
+
`${opts.logPrefix} Awareness clear failed:`,
|
|
4357
|
+
describeFetchError(
|
|
4358
|
+
err,
|
|
4359
|
+
`${API_CHANNEL_AWARENESS} clear`,
|
|
4360
|
+
CHANNEL_AWARENESS_FETCH_TIMEOUT_MS
|
|
4361
|
+
)
|
|
4351
4362
|
);
|
|
4352
4363
|
});
|
|
4353
4364
|
trackAwareness(p);
|
|
@@ -4358,7 +4369,7 @@ async function connectAndStream(lastEventId, onEventId, onStable = () => {
|
|
|
4358
4369
|
pendingAwareness = null;
|
|
4359
4370
|
if (event.documentId) shutdownTimers.lastDocumentId = event.documentId;
|
|
4360
4371
|
const p = fetchWithTimeout(
|
|
4361
|
-
`${
|
|
4372
|
+
`${opts.tandemUrl}${API_CHANNEL_AWARENESS}`,
|
|
4362
4373
|
{
|
|
4363
4374
|
method: "POST",
|
|
4364
4375
|
headers: { "Content-Type": "application/json" },
|
|
@@ -4368,17 +4379,21 @@ async function connectAndStream(lastEventId, onEventId, onStable = () => {
|
|
|
4368
4379
|
active: true
|
|
4369
4380
|
})
|
|
4370
4381
|
},
|
|
4371
|
-
|
|
4382
|
+
CHANNEL_AWARENESS_FETCH_TIMEOUT_MS
|
|
4372
4383
|
).catch((err) => {
|
|
4373
4384
|
console.error(
|
|
4374
|
-
|
|
4375
|
-
describeFetchError(
|
|
4385
|
+
`${opts.logPrefix} Awareness update failed:`,
|
|
4386
|
+
describeFetchError(
|
|
4387
|
+
err,
|
|
4388
|
+
`${API_CHANNEL_AWARENESS} update`,
|
|
4389
|
+
CHANNEL_AWARENESS_FETCH_TIMEOUT_MS
|
|
4390
|
+
)
|
|
4376
4391
|
);
|
|
4377
4392
|
});
|
|
4378
4393
|
trackAwareness(p);
|
|
4379
4394
|
if (shutdownTimers.clearAwarenessTimer) clearTimeout(shutdownTimers.clearAwarenessTimer);
|
|
4380
4395
|
shutdownTimers.clearAwarenessTimer = setTimeout(
|
|
4381
|
-
() =>
|
|
4396
|
+
() => clearAwarenessNow(event.documentId),
|
|
4382
4397
|
AWARENESS_CLEAR_MS
|
|
4383
4398
|
);
|
|
4384
4399
|
}
|
|
@@ -4396,9 +4411,9 @@ async function connectAndStream(lastEventId, onEventId, onStable = () => {
|
|
|
4396
4411
|
}
|
|
4397
4412
|
lastActivityAt = Date.now();
|
|
4398
4413
|
buffer += decoder.decode(value, { stream: true });
|
|
4399
|
-
if (buffer.length >
|
|
4414
|
+
if (buffer.length > CHANNEL_MAX_SSE_BUFFER_BYTES) {
|
|
4400
4415
|
throw new Error(
|
|
4401
|
-
`SSE buffer exceeded ${
|
|
4416
|
+
`SSE buffer exceeded ${CHANNEL_MAX_SSE_BUFFER_BYTES} bytes without a frame boundary`
|
|
4402
4417
|
);
|
|
4403
4418
|
}
|
|
4404
4419
|
let boundary;
|
|
@@ -4418,29 +4433,35 @@ async function connectAndStream(lastEventId, onEventId, onStable = () => {
|
|
|
4418
4433
|
raw = JSON.parse(data);
|
|
4419
4434
|
} catch (err) {
|
|
4420
4435
|
console.error(
|
|
4421
|
-
|
|
4436
|
+
`${opts.logPrefix} SSE JSON parse failed (eventId=${eventId ?? "none"}, len=${data.length}): ${err instanceof Error ? err.message : err}. Tail:`,
|
|
4422
4437
|
data.slice(Math.max(0, data.length - 200))
|
|
4423
4438
|
);
|
|
4439
|
+
if (eventId) cb.onEventId(eventId);
|
|
4424
4440
|
continue;
|
|
4425
4441
|
}
|
|
4426
4442
|
const event = parseTandemEvent(raw);
|
|
4427
4443
|
if (!event) {
|
|
4428
4444
|
console.error(
|
|
4429
|
-
|
|
4445
|
+
`${opts.logPrefix} SSE event failed validation (eventId=${eventId ?? "none"}): shape mismatch`
|
|
4430
4446
|
);
|
|
4447
|
+
if (eventId) cb.onEventId(eventId);
|
|
4431
4448
|
continue;
|
|
4432
4449
|
}
|
|
4433
4450
|
if (event.type !== "chat:message") {
|
|
4434
|
-
refreshMode();
|
|
4451
|
+
refreshMode(opts.tandemUrl, opts.logPrefix);
|
|
4435
4452
|
if (getModeSync() === "solo") {
|
|
4436
|
-
console.error(
|
|
4437
|
-
if (eventId) onEventId(eventId);
|
|
4453
|
+
console.error(`${opts.logPrefix} Solo mode: suppressed ${event.type} event`);
|
|
4454
|
+
if (eventId) cb.onEventId(eventId);
|
|
4438
4455
|
continue;
|
|
4439
4456
|
}
|
|
4440
4457
|
}
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4458
|
+
try {
|
|
4459
|
+
await opts.onEvent(event, eventId);
|
|
4460
|
+
} catch (err) {
|
|
4461
|
+
console.error(`${opts.logPrefix} onEvent failed (transport broken?):`, err);
|
|
4462
|
+
throw err;
|
|
4463
|
+
}
|
|
4464
|
+
if (eventId) cb.onEventId(eventId);
|
|
4444
4465
|
scheduleAwareness(event);
|
|
4445
4466
|
}
|
|
4446
4467
|
}
|
|
@@ -4454,14 +4475,73 @@ async function connectAndStream(lastEventId, onEventId, onStable = () => {
|
|
|
4454
4475
|
pendingAwareness = null;
|
|
4455
4476
|
}
|
|
4456
4477
|
}
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4478
|
+
async function fetchMode(tandemUrl) {
|
|
4479
|
+
try {
|
|
4480
|
+
const res = await fetchWithTimeout(
|
|
4481
|
+
`${tandemUrl}${API_MODE}`,
|
|
4482
|
+
{},
|
|
4483
|
+
CHANNEL_MODE_FETCH_TIMEOUT_MS
|
|
4484
|
+
);
|
|
4485
|
+
if (!res.ok) return { ok: false, reason: `status ${res.status}` };
|
|
4486
|
+
const body = await res.json();
|
|
4487
|
+
const parsed = TandemModeSchema.safeParse(body.mode);
|
|
4488
|
+
if (!parsed.success) return { ok: false, reason: `invalid mode ${JSON.stringify(body.mode)}` };
|
|
4489
|
+
return { ok: true, mode: parsed.data };
|
|
4490
|
+
} catch (err) {
|
|
4491
|
+
return { ok: false, reason: describeFetchError(err, API_MODE, CHANNEL_MODE_FETCH_TIMEOUT_MS) };
|
|
4492
|
+
}
|
|
4493
|
+
}
|
|
4494
|
+
async function getCachedMode(tandemUrl, logPrefix = "[Tandem]") {
|
|
4495
|
+
const now = Date.now();
|
|
4496
|
+
if (now - cachedModeAt < MODE_CACHE_TTL_MS && cachedModeAt !== 0) return cachedMode;
|
|
4497
|
+
const result = await fetchMode(tandemUrl);
|
|
4498
|
+
if (!result.ok) {
|
|
4499
|
+
if (cachedModeAt !== 0) {
|
|
4500
|
+
console.error(
|
|
4501
|
+
`${logPrefix} Mode check failed (${result.reason}), preserving last known mode '${cachedMode}'`
|
|
4502
|
+
);
|
|
4503
|
+
return cachedMode;
|
|
4504
|
+
}
|
|
4505
|
+
console.error(
|
|
4506
|
+
`${logPrefix} Mode check failed (${result.reason}), no prior mode \u2014 using cold-start default '${TANDEM_MODE_DEFAULT}'`
|
|
4507
|
+
);
|
|
4508
|
+
cachedMode = TANDEM_MODE_DEFAULT;
|
|
4509
|
+
return TANDEM_MODE_DEFAULT;
|
|
4510
|
+
}
|
|
4511
|
+
cachedMode = result.mode;
|
|
4512
|
+
cachedModeAt = now;
|
|
4513
|
+
return cachedMode;
|
|
4463
4514
|
}
|
|
4464
|
-
|
|
4515
|
+
function getModeSync() {
|
|
4516
|
+
return cachedMode;
|
|
4517
|
+
}
|
|
4518
|
+
function refreshMode(tandemUrl, logPrefix) {
|
|
4519
|
+
if (_modeRefreshInFlight) return;
|
|
4520
|
+
const now = Date.now();
|
|
4521
|
+
if (now - cachedModeAt < MODE_CACHE_TTL_MS) return;
|
|
4522
|
+
if (now - cachedModeFailedAt < MODE_CACHE_TTL_MS) return;
|
|
4523
|
+
_modeRefreshInFlight = (async () => {
|
|
4524
|
+
try {
|
|
4525
|
+
const result = await fetchMode(tandemUrl);
|
|
4526
|
+
if (result.ok) {
|
|
4527
|
+
cachedMode = result.mode;
|
|
4528
|
+
cachedModeAt = Date.now();
|
|
4529
|
+
cachedModeFailedAt = 0;
|
|
4530
|
+
} else {
|
|
4531
|
+
cachedModeFailedAt = Date.now();
|
|
4532
|
+
console.error(
|
|
4533
|
+
`${logPrefix} Background mode refresh failed (${result.reason}), keeping cached`
|
|
4534
|
+
);
|
|
4535
|
+
}
|
|
4536
|
+
} finally {
|
|
4537
|
+
_modeRefreshInFlight = null;
|
|
4538
|
+
}
|
|
4539
|
+
})().catch((err) => {
|
|
4540
|
+
console.error(`${logPrefix} refreshMode unexpected error:`, err);
|
|
4541
|
+
cachedModeFailedAt = Date.now();
|
|
4542
|
+
});
|
|
4543
|
+
}
|
|
4544
|
+
async function flushFinalAwareness(tandemUrl, logPrefix = "[Tandem]") {
|
|
4465
4545
|
if (shutdownTimers.awarenessTimer) clearTimeout(shutdownTimers.awarenessTimer);
|
|
4466
4546
|
if (shutdownTimers.clearAwarenessTimer) clearTimeout(shutdownTimers.clearAwarenessTimer);
|
|
4467
4547
|
if (outstandingAwareness.size > 0) {
|
|
@@ -4470,7 +4550,7 @@ async function finalClearAwareness() {
|
|
|
4470
4550
|
if (shutdownTimers.lastDocumentId === null) return true;
|
|
4471
4551
|
try {
|
|
4472
4552
|
const res = await fetchWithTimeout(
|
|
4473
|
-
`${
|
|
4553
|
+
`${tandemUrl}${API_CHANNEL_AWARENESS}`,
|
|
4474
4554
|
{
|
|
4475
4555
|
method: "POST",
|
|
4476
4556
|
headers: { "Content-Type": "application/json" },
|
|
@@ -4480,27 +4560,35 @@ async function finalClearAwareness() {
|
|
|
4480
4560
|
active: false
|
|
4481
4561
|
})
|
|
4482
4562
|
},
|
|
4483
|
-
|
|
4563
|
+
CHANNEL_AWARENESS_FETCH_TIMEOUT_MS
|
|
4484
4564
|
);
|
|
4485
4565
|
if (!res.ok) {
|
|
4486
|
-
console.error(
|
|
4566
|
+
console.error(`${logPrefix} Shutdown awareness clear returned ${res.status}`);
|
|
4487
4567
|
return false;
|
|
4488
4568
|
}
|
|
4489
4569
|
return true;
|
|
4490
4570
|
} catch (err) {
|
|
4491
4571
|
console.error(
|
|
4492
|
-
|
|
4493
|
-
describeFetchError(
|
|
4572
|
+
`${logPrefix} Shutdown awareness clear failed:`,
|
|
4573
|
+
describeFetchError(
|
|
4574
|
+
err,
|
|
4575
|
+
`${API_CHANNEL_AWARENESS} shutdown`,
|
|
4576
|
+
CHANNEL_AWARENESS_FETCH_TIMEOUT_MS
|
|
4577
|
+
)
|
|
4494
4578
|
);
|
|
4495
4579
|
return false;
|
|
4496
4580
|
}
|
|
4497
4581
|
}
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4582
|
+
function _resetSseConsumerStateForTests() {
|
|
4583
|
+
cachedMode = TANDEM_MODE_DEFAULT;
|
|
4584
|
+
cachedModeAt = 0;
|
|
4585
|
+
cachedModeFailedAt = 0;
|
|
4586
|
+
_modeRefreshInFlight = null;
|
|
4587
|
+
shutdownTimers.awarenessTimer = null;
|
|
4588
|
+
shutdownTimers.clearAwarenessTimer = null;
|
|
4589
|
+
shutdownTimers.lastDocumentId = null;
|
|
4590
|
+
outstandingAwareness.clear();
|
|
4502
4591
|
}
|
|
4503
|
-
var shutdownForTests = shutdownMonitor;
|
|
4504
4592
|
function _setLastDocumentIdForTests(id) {
|
|
4505
4593
|
shutdownTimers.lastDocumentId = id;
|
|
4506
4594
|
}
|
|
@@ -4510,11 +4598,63 @@ function _getLastDocumentIdForTests() {
|
|
|
4510
4598
|
function _addOutstandingAwarenessForTests(p) {
|
|
4511
4599
|
trackAwareness(p);
|
|
4512
4600
|
}
|
|
4601
|
+
|
|
4602
|
+
// src/monitor/index.ts
|
|
4603
|
+
var IS_VITEST = process.env.VITEST === "true";
|
|
4604
|
+
if (!IS_VITEST) {
|
|
4605
|
+
console.log = console.error;
|
|
4606
|
+
console.warn = console.error;
|
|
4607
|
+
console.info = console.error;
|
|
4608
|
+
}
|
|
4609
|
+
var TANDEM_URL = resolveTandemUrl();
|
|
4610
|
+
var LOG_PREFIX = "[Monitor]";
|
|
4611
|
+
function buildOptions() {
|
|
4612
|
+
return {
|
|
4613
|
+
tandemUrl: TANDEM_URL,
|
|
4614
|
+
logPrefix: LOG_PREFIX,
|
|
4615
|
+
errorCode: MONITOR_CONNECT_FAILED,
|
|
4616
|
+
onEvent: (event) => {
|
|
4617
|
+
const content = formatEventContent(event).replace(/\n/g, " ");
|
|
4618
|
+
process.stdout.write(content + "\n");
|
|
4619
|
+
},
|
|
4620
|
+
onExhaustion: () => {
|
|
4621
|
+
process.stdout.write(
|
|
4622
|
+
"Tandem monitor disconnected \u2014 restart Tandem to restore real-time events\n"
|
|
4623
|
+
);
|
|
4624
|
+
}
|
|
4625
|
+
};
|
|
4626
|
+
}
|
|
4627
|
+
async function main() {
|
|
4628
|
+
installShutdownHandlers();
|
|
4629
|
+
installStdoutErrorHandler();
|
|
4630
|
+
console.error(`${LOG_PREFIX} Tandem monitor starting (server: ${TANDEM_URL})`);
|
|
4631
|
+
await getCachedMode2().catch(() => {
|
|
4632
|
+
});
|
|
4633
|
+
await runEventConsumer(buildOptions());
|
|
4634
|
+
}
|
|
4635
|
+
async function connectAndStream(lastEventId, onEventId, onStable = () => {
|
|
4636
|
+
}) {
|
|
4637
|
+
return connectAndStreamOnce(buildOptions(), lastEventId, { onEventId, onStable });
|
|
4638
|
+
}
|
|
4639
|
+
async function shutdownMonitor(signal) {
|
|
4640
|
+
console.error(`${LOG_PREFIX} Received ${signal}, clearing awareness and exiting`);
|
|
4641
|
+
const ok = await flushFinalAwareness(TANDEM_URL, LOG_PREFIX);
|
|
4642
|
+
process.exit(ok ? 0 : 1);
|
|
4643
|
+
}
|
|
4644
|
+
function _setLastDocumentIdForTests2(id) {
|
|
4645
|
+
_setLastDocumentIdForTests(id);
|
|
4646
|
+
}
|
|
4647
|
+
function _getLastDocumentIdForTests2() {
|
|
4648
|
+
return _getLastDocumentIdForTests();
|
|
4649
|
+
}
|
|
4650
|
+
function _addOutstandingAwarenessForTests2(p) {
|
|
4651
|
+
_addOutstandingAwarenessForTests(p);
|
|
4652
|
+
}
|
|
4513
4653
|
function installShutdownHandlers() {
|
|
4514
4654
|
if (IS_VITEST) return;
|
|
4515
4655
|
const handler = (signal) => {
|
|
4516
4656
|
shutdownMonitor(signal).catch((err) => {
|
|
4517
|
-
console.error(
|
|
4657
|
+
console.error(`${LOG_PREFIX} Shutdown handler failed:`, err);
|
|
4518
4658
|
process.exit(1);
|
|
4519
4659
|
});
|
|
4520
4660
|
};
|
|
@@ -4522,79 +4662,21 @@ function installShutdownHandlers() {
|
|
|
4522
4662
|
process.on("SIGTERM", () => handler("SIGTERM"));
|
|
4523
4663
|
}
|
|
4524
4664
|
function onStdoutError(err) {
|
|
4525
|
-
console.error(
|
|
4665
|
+
console.error(`${LOG_PREFIX} stdout error (plugin-host pipe likely closed):`, err);
|
|
4526
4666
|
process.exit(1);
|
|
4527
4667
|
}
|
|
4528
4668
|
function installStdoutErrorHandler() {
|
|
4529
4669
|
if (IS_VITEST) return;
|
|
4530
4670
|
process.stdout.on("error", onStdoutError);
|
|
4531
4671
|
}
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
var cachedModeFailedAt = 0;
|
|
4535
|
-
async function fetchMode() {
|
|
4536
|
-
try {
|
|
4537
|
-
const res = await fetchWithTimeout(`${TANDEM_URL}/api/mode`, {}, MODE_FETCH_TIMEOUT_MS);
|
|
4538
|
-
if (!res.ok) return { ok: false, reason: `status ${res.status}` };
|
|
4539
|
-
const body = await res.json();
|
|
4540
|
-
const parsed = TandemModeSchema.safeParse(body.mode);
|
|
4541
|
-
if (!parsed.success) return { ok: false, reason: `invalid mode ${JSON.stringify(body.mode)}` };
|
|
4542
|
-
return { ok: true, mode: parsed.data };
|
|
4543
|
-
} catch (err) {
|
|
4544
|
-
return { ok: false, reason: describeFetchError(err, "/api/mode", MODE_FETCH_TIMEOUT_MS) };
|
|
4545
|
-
}
|
|
4546
|
-
}
|
|
4547
|
-
async function getCachedMode() {
|
|
4548
|
-
const now = Date.now();
|
|
4549
|
-
if (now - cachedModeAt < MODE_CACHE_TTL_MS) return cachedMode;
|
|
4550
|
-
const result = await fetchMode();
|
|
4551
|
-
if (!result.ok) {
|
|
4552
|
-
console.error(`[Monitor] Mode check failed (${result.reason}), failing closed to 'solo'`);
|
|
4553
|
-
cachedMode = "solo";
|
|
4554
|
-
return "solo";
|
|
4555
|
-
}
|
|
4556
|
-
cachedMode = result.mode;
|
|
4557
|
-
cachedModeAt = now;
|
|
4558
|
-
return cachedMode;
|
|
4672
|
+
async function getCachedMode2() {
|
|
4673
|
+
return getCachedMode(TANDEM_URL, LOG_PREFIX);
|
|
4559
4674
|
}
|
|
4560
|
-
function
|
|
4561
|
-
return
|
|
4562
|
-
}
|
|
4563
|
-
function refreshMode() {
|
|
4564
|
-
if (_modeRefreshInFlight) return;
|
|
4565
|
-
const now = Date.now();
|
|
4566
|
-
if (now - cachedModeAt < MODE_CACHE_TTL_MS) return;
|
|
4567
|
-
if (now - cachedModeFailedAt < MODE_CACHE_TTL_MS) return;
|
|
4568
|
-
_modeRefreshInFlight = (async () => {
|
|
4569
|
-
try {
|
|
4570
|
-
const result = await fetchMode();
|
|
4571
|
-
if (result.ok) {
|
|
4572
|
-
cachedMode = result.mode;
|
|
4573
|
-
cachedModeAt = Date.now();
|
|
4574
|
-
cachedModeFailedAt = 0;
|
|
4575
|
-
} else {
|
|
4576
|
-
cachedModeFailedAt = Date.now();
|
|
4577
|
-
console.error(
|
|
4578
|
-
`[Monitor] Background mode refresh failed (${result.reason}), keeping cached`
|
|
4579
|
-
);
|
|
4580
|
-
}
|
|
4581
|
-
} finally {
|
|
4582
|
-
_modeRefreshInFlight = null;
|
|
4583
|
-
}
|
|
4584
|
-
})().catch((err) => {
|
|
4585
|
-
console.error("[Monitor] refreshMode unexpected error:", err);
|
|
4586
|
-
cachedModeFailedAt = Date.now();
|
|
4587
|
-
});
|
|
4675
|
+
function getModeSync2() {
|
|
4676
|
+
return getModeSync();
|
|
4588
4677
|
}
|
|
4589
4678
|
function _resetMonitorStateForTests() {
|
|
4590
|
-
|
|
4591
|
-
cachedModeAt = 0;
|
|
4592
|
-
cachedModeFailedAt = 0;
|
|
4593
|
-
_modeRefreshInFlight = null;
|
|
4594
|
-
shutdownTimers.awarenessTimer = null;
|
|
4595
|
-
shutdownTimers.clearAwarenessTimer = null;
|
|
4596
|
-
shutdownTimers.lastDocumentId = null;
|
|
4597
|
-
outstandingAwareness.clear();
|
|
4679
|
+
_resetSseConsumerStateForTests();
|
|
4598
4680
|
process.removeAllListeners("SIGINT");
|
|
4599
4681
|
process.removeAllListeners("SIGTERM");
|
|
4600
4682
|
}
|
|
@@ -4609,21 +4691,20 @@ var __thisFileNormalized = normalizeForCompare(fileURLToPath(import.meta.url));
|
|
|
4609
4691
|
var isDirectRun = typeof process.argv[1] === "string" && normalizeForCompare(process.argv[1]) === __thisFileNormalized;
|
|
4610
4692
|
if (isDirectRun && !IS_VITEST) {
|
|
4611
4693
|
main().catch((err) => {
|
|
4612
|
-
console.error(
|
|
4694
|
+
console.error(`${LOG_PREFIX} Fatal error:`, err);
|
|
4613
4695
|
process.exit(1);
|
|
4614
4696
|
});
|
|
4615
4697
|
}
|
|
4616
4698
|
export {
|
|
4617
|
-
_addOutstandingAwarenessForTests,
|
|
4618
|
-
_getLastDocumentIdForTests,
|
|
4699
|
+
_addOutstandingAwarenessForTests2 as _addOutstandingAwarenessForTests,
|
|
4700
|
+
_getLastDocumentIdForTests2 as _getLastDocumentIdForTests,
|
|
4619
4701
|
_monitorTestExports,
|
|
4620
4702
|
_resetMonitorStateForTests,
|
|
4621
|
-
_setLastDocumentIdForTests,
|
|
4703
|
+
_setLastDocumentIdForTests2 as _setLastDocumentIdForTests,
|
|
4622
4704
|
connectAndStream,
|
|
4623
|
-
getCachedMode,
|
|
4624
|
-
getModeSync,
|
|
4705
|
+
getCachedMode2 as getCachedMode,
|
|
4706
|
+
getModeSync2 as getModeSync,
|
|
4625
4707
|
main,
|
|
4626
|
-
shutdownForTests,
|
|
4627
4708
|
shutdownMonitor
|
|
4628
4709
|
};
|
|
4629
4710
|
//# sourceMappingURL=index.js.map
|