@mirrowel/opencode-openshare 0.1.1 → 0.2.0-dev.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/README.md +36 -37
- package/ci/README.md +9 -3
- package/ci/openshare.js +916 -199
- package/dist/binding-queue.d.ts +1 -0
- package/dist/binding-queue.js +22 -0
- package/dist/commands.js +41 -285
- package/dist/contract.d.ts +156 -0
- package/dist/contract.js +263 -0
- package/dist/core.js +888 -96
- package/dist/dialogs.js +39 -1
- package/dist/settings.js +141 -16
- package/dist/share-flow.d.ts +7 -2
- package/dist/share-flow.js +32 -17
- package/dist/sidebar.d.ts +3 -1
- package/dist/sidebar.js +3 -38
- package/dist/tui.js +286 -421
- package/dist/v2-server.d.ts +0 -27
- package/dist/v2-server.js +61 -121
- package/package.json +3 -1
- package/src/binding-queue.ts +26 -0
- package/src/commands.ts +41 -280
- package/src/contract.ts +320 -0
- package/src/core.ts +864 -99
- package/src/dialogs.tsx +45 -2
- package/src/settings.ts +133 -15
- package/src/share-flow.ts +33 -19
- package/src/sidebar.tsx +4 -45
- package/src/v2-server.ts +57 -110
package/ci/openshare.js
CHANGED
|
@@ -345,7 +345,226 @@ function resolveShareMode(cfg, input) {
|
|
|
345
345
|
|
|
346
346
|
// src/core.ts
|
|
347
347
|
import { appendFileSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
348
|
+
import { createHash } from "node:crypto";
|
|
348
349
|
import { join as join2 } from "node:path";
|
|
350
|
+
|
|
351
|
+
// src/contract.ts
|
|
352
|
+
function messageTombstone(sessionID, messageID) {
|
|
353
|
+
return { type: "message", del: true, sessionID, data: { id: messageID } };
|
|
354
|
+
}
|
|
355
|
+
function isV2NativeMessage(x) {
|
|
356
|
+
if (!x || typeof x !== "object")
|
|
357
|
+
return false;
|
|
358
|
+
const r = x;
|
|
359
|
+
return typeof r.type === "string" && r.type !== "" && r.role === undefined && typeof r.id === "string";
|
|
360
|
+
}
|
|
361
|
+
var TOOL_NAME_MAP = {
|
|
362
|
+
bash: "shell",
|
|
363
|
+
task: "subagent"
|
|
364
|
+
};
|
|
365
|
+
var INPUT_KEY_MAP = {
|
|
366
|
+
filePath: "path",
|
|
367
|
+
subagent_type: "agent"
|
|
368
|
+
};
|
|
369
|
+
function normalizeToolInput(input, title) {
|
|
370
|
+
const out = {};
|
|
371
|
+
for (const [k, v] of Object.entries(input ?? {}))
|
|
372
|
+
out[INPUT_KEY_MAP[k] ?? k] = v;
|
|
373
|
+
if (title && out.description === undefined)
|
|
374
|
+
out.description = title;
|
|
375
|
+
return out;
|
|
376
|
+
}
|
|
377
|
+
function normalizeToolMetadata(meta) {
|
|
378
|
+
const out = {};
|
|
379
|
+
for (const [k, v] of Object.entries(meta ?? {})) {
|
|
380
|
+
if (k === "filediff")
|
|
381
|
+
out.files = [v];
|
|
382
|
+
else if (k === "sessionId")
|
|
383
|
+
out.sessionID = v;
|
|
384
|
+
else
|
|
385
|
+
out[k] = v;
|
|
386
|
+
}
|
|
387
|
+
return out;
|
|
388
|
+
}
|
|
389
|
+
function toolContent(output) {
|
|
390
|
+
const text = typeof output === "string" ? output : "";
|
|
391
|
+
return [{ type: "text", text: text || " " }];
|
|
392
|
+
}
|
|
393
|
+
function normalizeToolState(state) {
|
|
394
|
+
const status = String(state?.status ?? "pending");
|
|
395
|
+
if (status === "pending") {
|
|
396
|
+
return { status: "streaming", input: typeof state.raw === "string" ? state.raw : JSON.stringify(state.input ?? {}) };
|
|
397
|
+
}
|
|
398
|
+
if (status === "running") {
|
|
399
|
+
return { status: "running", input: normalizeToolInput(state.input ?? {}, state.title), metadata: normalizeToolMetadata(state.metadata) };
|
|
400
|
+
}
|
|
401
|
+
if (status === "completed") {
|
|
402
|
+
return { status: "completed", input: normalizeToolInput(state.input ?? {}, state.title), content: toolContent(state.output), metadata: normalizeToolMetadata(state.metadata) };
|
|
403
|
+
}
|
|
404
|
+
return {
|
|
405
|
+
status: "error",
|
|
406
|
+
input: normalizeToolInput(state.input ?? {}),
|
|
407
|
+
error: { type: "unknown", message: typeof state.error === "string" ? state.error : "tool failed" },
|
|
408
|
+
metadata: normalizeToolMetadata(state.metadata)
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
function normalizeFilePart(p) {
|
|
412
|
+
const url = typeof p.url === "string" ? p.url : "";
|
|
413
|
+
if (!url)
|
|
414
|
+
return;
|
|
415
|
+
const inline = url.startsWith("data:");
|
|
416
|
+
const comma = url.indexOf(",");
|
|
417
|
+
return {
|
|
418
|
+
data: inline ? url.slice(comma + 1) : url,
|
|
419
|
+
mime: typeof p.mime === "string" ? p.mime : "application/octet-stream",
|
|
420
|
+
source: inline ? { type: "inline" } : { type: "uri", uri: url },
|
|
421
|
+
...typeof p.filename === "string" ? { name: p.filename } : {}
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function normalizeV1Message(info, parts) {
|
|
425
|
+
if (!info || typeof info.id !== "string")
|
|
426
|
+
return;
|
|
427
|
+
const id = info.id;
|
|
428
|
+
const created = info.time?.created ?? 0;
|
|
429
|
+
if (info.role === "user") {
|
|
430
|
+
const compaction = parts.find((p) => p?.type === "compaction");
|
|
431
|
+
if (compaction) {
|
|
432
|
+
const message = {
|
|
433
|
+
id,
|
|
434
|
+
type: "compaction",
|
|
435
|
+
status: "completed",
|
|
436
|
+
reason: compaction.auto === false ? "manual" : "auto",
|
|
437
|
+
summary: "",
|
|
438
|
+
recent: "",
|
|
439
|
+
time: { created }
|
|
440
|
+
};
|
|
441
|
+
return { message };
|
|
442
|
+
}
|
|
443
|
+
const text = parts.filter((p) => p?.type === "text" && typeof p.text === "string").map((p) => p.text).join(`
|
|
444
|
+
`);
|
|
445
|
+
const files = parts.filter((p) => p?.type === "file").map(normalizeFilePart).filter((f) => f !== undefined);
|
|
446
|
+
const agents = parts.filter((p) => p?.type === "agent").map((p) => ({ name: String(p.name ?? "") })).filter((a) => a.name);
|
|
447
|
+
const model = info.model;
|
|
448
|
+
const message = {
|
|
449
|
+
id,
|
|
450
|
+
type: "user",
|
|
451
|
+
time: { created },
|
|
452
|
+
text,
|
|
453
|
+
...files.length ? { files } : {},
|
|
454
|
+
...agents.length ? { agents } : {},
|
|
455
|
+
metadata: {
|
|
456
|
+
...info.agent !== undefined ? { agent: String(info.agent) } : {},
|
|
457
|
+
...model ? { model: { id: String(model.modelID ?? ""), providerID: String(model.providerID ?? "") } } : {}
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
return { message };
|
|
461
|
+
}
|
|
462
|
+
if (info.role === "assistant") {
|
|
463
|
+
const time = info.time;
|
|
464
|
+
if (info.summary === true && info.mode === "compaction") {
|
|
465
|
+
const summary = parts.filter((p) => p?.type === "text" && typeof p.text === "string").map((p) => p.text).join(`
|
|
466
|
+
`);
|
|
467
|
+
return { refill: String(info.parentID ?? ""), summary };
|
|
468
|
+
}
|
|
469
|
+
const content = [];
|
|
470
|
+
let cost = typeof info.cost === "number" ? info.cost : 0;
|
|
471
|
+
let tokens;
|
|
472
|
+
let retry;
|
|
473
|
+
for (const p of parts) {
|
|
474
|
+
switch (p?.type) {
|
|
475
|
+
case "text":
|
|
476
|
+
content.push({ type: "text", text: String(p.text ?? "") });
|
|
477
|
+
break;
|
|
478
|
+
case "reasoning": {
|
|
479
|
+
const rt = p.time;
|
|
480
|
+
content.push({ type: "reasoning", text: String(p.text ?? ""), time: { created: rt?.start ?? undefined, completed: rt?.end ?? undefined } });
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
case "tool": {
|
|
484
|
+
const st = p.state;
|
|
485
|
+
const tt = st?.time ?? {};
|
|
486
|
+
content.push({
|
|
487
|
+
type: "tool",
|
|
488
|
+
id: String(p.id ?? ""),
|
|
489
|
+
name: TOOL_NAME_MAP[String(p.tool ?? "unknown")] ?? String(p.tool ?? "unknown"),
|
|
490
|
+
state: normalizeToolState(st ?? {}),
|
|
491
|
+
time: { created: tt.start ?? undefined, completed: tt.end ?? undefined }
|
|
492
|
+
});
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
case "retry": {
|
|
496
|
+
const err = p.error;
|
|
497
|
+
retry = {
|
|
498
|
+
attempt: Number(p.attempt ?? 0),
|
|
499
|
+
at: p.time?.created ?? 0,
|
|
500
|
+
error: { type: "unknown", message: String(err?.message ?? err?.name ?? "retry") }
|
|
501
|
+
};
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
504
|
+
case "step-finish": {
|
|
505
|
+
if (typeof p.cost === "number")
|
|
506
|
+
cost += p.cost;
|
|
507
|
+
if (p.tokens && typeof p.tokens === "object")
|
|
508
|
+
tokens = p.tokens;
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
const finish = typeof info.finish === "string" ? info.finish : undefined;
|
|
514
|
+
const errObj = info.error;
|
|
515
|
+
const message = {
|
|
516
|
+
id,
|
|
517
|
+
type: "assistant",
|
|
518
|
+
agent: String(info.agent ?? "build"),
|
|
519
|
+
model: { id: String(info.modelID ?? ""), providerID: String(info.providerID ?? "") },
|
|
520
|
+
time: { created, streamed: time?.started ?? undefined, completed: time?.completed ?? undefined },
|
|
521
|
+
content,
|
|
522
|
+
cost,
|
|
523
|
+
...tokens ? { tokens } : {},
|
|
524
|
+
...finish ? { finish } : {},
|
|
525
|
+
...errObj ? { error: { type: String(errObj.name ?? "UnknownError"), message: String(errObj.message ?? "turn failed") } } : {},
|
|
526
|
+
...retry ? { retry } : {}
|
|
527
|
+
};
|
|
528
|
+
return { message };
|
|
529
|
+
}
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
function normalizeV1Diff(diff) {
|
|
533
|
+
if (!Array.isArray(diff))
|
|
534
|
+
return diff;
|
|
535
|
+
return diff.map((d) => {
|
|
536
|
+
if (!d || typeof d !== "object")
|
|
537
|
+
return d;
|
|
538
|
+
const r = d;
|
|
539
|
+
if (typeof r.additions === "number" && typeof r.deletions === "number")
|
|
540
|
+
return r;
|
|
541
|
+
const patch = typeof r.patch === "string" ? r.patch : "";
|
|
542
|
+
let additions = 0;
|
|
543
|
+
let deletions = 0;
|
|
544
|
+
for (const line of patch.split(`
|
|
545
|
+
`)) {
|
|
546
|
+
if (line.startsWith("+") && !line.startsWith("+++"))
|
|
547
|
+
additions++;
|
|
548
|
+
else if (line.startsWith("-") && !line.startsWith("---"))
|
|
549
|
+
deletions++;
|
|
550
|
+
}
|
|
551
|
+
return { ...r, additions: typeof r.additions === "number" ? r.additions : additions, deletions: typeof r.deletions === "number" ? r.deletions : deletions };
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
function cursorFor(id, time) {
|
|
555
|
+
return Buffer.from(JSON.stringify({ id, time })).toString("base64url");
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// src/binding-queue.ts
|
|
559
|
+
var queues = new Map;
|
|
560
|
+
function queueBindingWrite(sid, fn) {
|
|
561
|
+
const prev = queues.get(sid) ?? Promise.resolve();
|
|
562
|
+
const run = prev.then(fn, fn);
|
|
563
|
+
queues.set(sid, run.catch(() => {}));
|
|
564
|
+
return run;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// src/core.ts
|
|
349
568
|
var DEFAULT_POLICY = {
|
|
350
569
|
mode: "persistent",
|
|
351
570
|
turnGraceMs: 120000,
|
|
@@ -381,11 +600,9 @@ function itemFor(type, data, sessionID) {
|
|
|
381
600
|
if (type === "prefs")
|
|
382
601
|
return { key: "prefs", item: { type, data } };
|
|
383
602
|
if (type === "session")
|
|
384
|
-
return { key: `session/${data?.id ?? ""}`, item: { type, data } };
|
|
603
|
+
return { key: `session/${data?.id ?? ""}`, item: { type, sessionID, data } };
|
|
385
604
|
if (type === "message")
|
|
386
|
-
return { key: `message/${data?.id ?? ""}`, item: { type, data } };
|
|
387
|
-
if (type === "part")
|
|
388
|
-
return { key: `part/${data?.messageID ?? ""}/${data?.id ?? ""}`, item: { type, data } };
|
|
605
|
+
return { key: `message/${data?.id ?? ""}`, item: { type, sessionID, data } };
|
|
389
606
|
if (type === "session_diff")
|
|
390
607
|
return { key: `session_diff/${sessionID}`, item: { type, sessionID, data } };
|
|
391
608
|
return;
|
|
@@ -542,6 +759,8 @@ function createEngine(deps) {
|
|
|
542
759
|
initTimer: null,
|
|
543
760
|
initWasAttach: false,
|
|
544
761
|
backoff: 1000,
|
|
762
|
+
backoffResetTimer: null,
|
|
763
|
+
spoolSaveTimer: null,
|
|
545
764
|
reconnectTimer: null,
|
|
546
765
|
intentionalClose: false,
|
|
547
766
|
connectQueued: false,
|
|
@@ -599,10 +818,14 @@ function createEngine(deps) {
|
|
|
599
818
|
if (b && typeof b === "object" && b.policy) {
|
|
600
819
|
applyPolicy(b.policy, url);
|
|
601
820
|
}
|
|
821
|
+
if (b && typeof b === "object" && b.frameAuth) {
|
|
822
|
+
frameAuthUrls.add(url);
|
|
823
|
+
}
|
|
602
824
|
}).catch(() => {
|
|
603
825
|
policyAsked.delete(url);
|
|
604
826
|
});
|
|
605
827
|
};
|
|
828
|
+
const frameAuthUrls = new Set;
|
|
606
829
|
const wsUrl = (url) => url.replace(/^http/, "ws");
|
|
607
830
|
let spoolRoot = spoolDirFor(cfg);
|
|
608
831
|
try {
|
|
@@ -637,7 +860,9 @@ function createEngine(deps) {
|
|
|
637
860
|
opts: undefined,
|
|
638
861
|
activated: false,
|
|
639
862
|
wroteBinding: false,
|
|
640
|
-
everConnected: false
|
|
863
|
+
everConnected: false,
|
|
864
|
+
merged: new Map,
|
|
865
|
+
sentHash: new Map
|
|
641
866
|
});
|
|
642
867
|
const refreshConfig = () => {
|
|
643
868
|
const next = loadConfig(deps.pluginDir, deps.options);
|
|
@@ -670,6 +895,7 @@ function createEngine(deps) {
|
|
|
670
895
|
}
|
|
671
896
|
st.activated = true;
|
|
672
897
|
st.opts = opts;
|
|
898
|
+
st.sentHash.clear();
|
|
673
899
|
const gated = mode === "manual" || opts2?.gated === true;
|
|
674
900
|
if (gated) {
|
|
675
901
|
chain = chain.then(() => seedManualStatus(st)).then(() => backfill(st, info)).then(() => queueConnectAll(st)).catch((e) => logError("activation error", e?.message || e));
|
|
@@ -721,24 +947,26 @@ function createEngine(deps) {
|
|
|
721
947
|
async function writeMetadata(st, value) {
|
|
722
948
|
if (!st.client)
|
|
723
949
|
return;
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
950
|
+
await queueBindingWrite(st.sid, async () => {
|
|
951
|
+
try {
|
|
952
|
+
const info = unwrap(await st.client.session.get({
|
|
953
|
+
path: { id: st.sid },
|
|
954
|
+
...deps.directory ? { query: { directory: deps.directory } } : {}
|
|
955
|
+
}));
|
|
956
|
+
const metadata = { ...info?.metadata ?? {} };
|
|
957
|
+
if (value === null)
|
|
958
|
+
delete metadata[METADATA_KEY];
|
|
959
|
+
else
|
|
960
|
+
metadata[METADATA_KEY] = value;
|
|
961
|
+
unwrap(await st.client.session.update({
|
|
962
|
+
path: { id: st.sid },
|
|
963
|
+
body: { metadata },
|
|
964
|
+
...deps.directory ? { query: { directory: deps.directory } } : {}
|
|
965
|
+
}));
|
|
966
|
+
} catch (e) {
|
|
967
|
+
log("metadata write failed (spool still covers reconnects):", e?.message || e);
|
|
968
|
+
}
|
|
969
|
+
});
|
|
742
970
|
}
|
|
743
971
|
const parseBinding = (record) => {
|
|
744
972
|
const out = new Map;
|
|
@@ -842,13 +1070,33 @@ function createEngine(deps) {
|
|
|
842
1070
|
const spoolSave = (st, r) => {
|
|
843
1071
|
try {
|
|
844
1072
|
writeFileSync2(metaPath(st, r.url), JSON.stringify({ shareId: r.shareId, secret: r.secret, ackedRev: r.ackedRev, sid: st.sid, url: r.url }));
|
|
845
|
-
writeFileSync2(queuePath(st, r.url), r.pending.map((f) => JSON.stringify(f)).join(`
|
|
1073
|
+
writeFileSync2(queuePath(st, r.url), r.pending.map((f) => f.wire ?? JSON.stringify(f)).join(`
|
|
846
1074
|
`) + (r.pending.length ? `
|
|
847
1075
|
` : ""));
|
|
1076
|
+
rmSync(legacyMetaPath(st), { force: true });
|
|
1077
|
+
rmSync(legacyQueuePath(st), { force: true });
|
|
848
1078
|
} catch (e) {
|
|
849
1079
|
logError("spool save failed", e?.message || e);
|
|
850
1080
|
}
|
|
851
1081
|
};
|
|
1082
|
+
const spoolAppend = (st, r, f) => {
|
|
1083
|
+
try {
|
|
1084
|
+
appendFileSync(queuePath(st, r.url), (f.wire ?? JSON.stringify(f)) + `
|
|
1085
|
+
`);
|
|
1086
|
+
} catch (e) {
|
|
1087
|
+
r.spoolAppendFailed = true;
|
|
1088
|
+
logError("spool append failed", e?.message || e);
|
|
1089
|
+
}
|
|
1090
|
+
};
|
|
1091
|
+
const scheduleSpoolSave = (st, r) => {
|
|
1092
|
+
if (r.spoolSaveTimer)
|
|
1093
|
+
return;
|
|
1094
|
+
r.spoolSaveTimer = setTimeout(() => {
|
|
1095
|
+
r.spoolSaveTimer = null;
|
|
1096
|
+
if (!st.closed)
|
|
1097
|
+
spoolSave(st, r);
|
|
1098
|
+
}, 300);
|
|
1099
|
+
};
|
|
852
1100
|
const spoolLoad = (st, r) => {
|
|
853
1101
|
try {
|
|
854
1102
|
const meta = JSON.parse(readFileSync2(metaPath(st, r.url), "utf8"));
|
|
@@ -857,13 +1105,33 @@ function createEngine(deps) {
|
|
|
857
1105
|
r.secret = meta.secret;
|
|
858
1106
|
r.ackedRev = meta.ackedRev ?? 0;
|
|
859
1107
|
}
|
|
1108
|
+
} catch {}
|
|
1109
|
+
try {
|
|
860
1110
|
if (existsSync2(queuePath(st, r.url))) {
|
|
861
|
-
|
|
862
|
-
|
|
1111
|
+
const disk = [];
|
|
1112
|
+
for (const line of readFileSync2(queuePath(st, r.url), "utf8").split(`
|
|
1113
|
+
`)) {
|
|
1114
|
+
if (!line.trim())
|
|
1115
|
+
continue;
|
|
1116
|
+
try {
|
|
1117
|
+
disk.push(JSON.parse(line));
|
|
1118
|
+
} catch {
|
|
1119
|
+
log(`spool load: dropped torn tail line for ${st.sid.slice(0, 18)}`);
|
|
1120
|
+
break;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
for (const f of disk) {
|
|
1124
|
+
if (typeof f.seq !== "number")
|
|
1125
|
+
f.seq = ++nextSeq;
|
|
1126
|
+
else if (f.seq >= nextSeq)
|
|
1127
|
+
nextSeq = f.seq + 1;
|
|
1128
|
+
}
|
|
1129
|
+
if (!r.spoolAppendFailed)
|
|
1130
|
+
r.pending = disk;
|
|
1131
|
+
else
|
|
1132
|
+
r.spoolAppendFailed = false;
|
|
863
1133
|
}
|
|
864
|
-
} catch {
|
|
865
|
-
r.pending = [];
|
|
866
|
-
}
|
|
1134
|
+
} catch {}
|
|
867
1135
|
if (!r.shareId && targets[0]?.url === r.url) {
|
|
868
1136
|
try {
|
|
869
1137
|
const meta = JSON.parse(readFileSync2(legacyMetaPath(st), "utf8"));
|
|
@@ -882,10 +1150,17 @@ function createEngine(deps) {
|
|
|
882
1150
|
};
|
|
883
1151
|
const spoolClear = (st, r) => {
|
|
884
1152
|
try {
|
|
1153
|
+
if (r.spoolSaveTimer) {
|
|
1154
|
+
clearTimeout(r.spoolSaveTimer);
|
|
1155
|
+
r.spoolSaveTimer = null;
|
|
1156
|
+
}
|
|
885
1157
|
rmSync(metaPath(st, r.url), { force: true });
|
|
886
1158
|
rmSync(queuePath(st, r.url), { force: true });
|
|
1159
|
+
rmSync(legacyMetaPath(st), { force: true });
|
|
1160
|
+
rmSync(legacyQueuePath(st), { force: true });
|
|
887
1161
|
} catch {}
|
|
888
1162
|
};
|
|
1163
|
+
let nextSeq = 0;
|
|
889
1164
|
const wsSend = (r, obj) => {
|
|
890
1165
|
if (debugEvents)
|
|
891
1166
|
log(`ws ${r.url.slice(0, 24)} #${r.socketSeq} send ${obj?.op} (ready=${r.wsReady})`);
|
|
@@ -896,13 +1171,50 @@ function createEngine(deps) {
|
|
|
896
1171
|
}
|
|
897
1172
|
return true;
|
|
898
1173
|
};
|
|
899
|
-
const
|
|
1174
|
+
const chunkedFrames = (items, maxItems, maxBytes) => {
|
|
1175
|
+
const chunks = [];
|
|
1176
|
+
let cur = [];
|
|
1177
|
+
let curJson = [];
|
|
1178
|
+
let curBytes = 0;
|
|
1179
|
+
for (const it of items) {
|
|
1180
|
+
const j = JSON.stringify(it) ?? "null";
|
|
1181
|
+
if (cur.length && (cur.length >= maxItems || curBytes + j.length > maxBytes)) {
|
|
1182
|
+
chunks.push({ items: cur, json: "[" + curJson.join(",") + "]" });
|
|
1183
|
+
cur = [];
|
|
1184
|
+
curJson = [];
|
|
1185
|
+
curBytes = 0;
|
|
1186
|
+
}
|
|
1187
|
+
cur.push(it);
|
|
1188
|
+
curJson.push(j);
|
|
1189
|
+
curBytes += j.length;
|
|
1190
|
+
}
|
|
1191
|
+
if (cur.length)
|
|
1192
|
+
chunks.push({ items: cur, json: "[" + curJson.join(",") + "]" });
|
|
1193
|
+
return chunks;
|
|
1194
|
+
};
|
|
1195
|
+
const chunkItems = (items, maxItems, maxBytes) => chunkedFrames(items, maxItems, maxBytes).map((c) => c.items);
|
|
1196
|
+
const syncFrameStr = (itemsJson, status, seq) => `{"op":"sync","items":${itemsJson},"status":${JSON.stringify(status)}${seq !== undefined ? `,"seq":${seq}` : ""}}`;
|
|
1197
|
+
const wsSendRaw = (r, s) => {
|
|
1198
|
+
if (debugEvents)
|
|
1199
|
+
log(`ws ${r.url.slice(0, 24)} #${r.socketSeq} send raw (${Math.round(s.length / 1024)}KB, ready=${r.wsReady})`);
|
|
1200
|
+
try {
|
|
1201
|
+
r.ws?.send(s);
|
|
1202
|
+
} catch {
|
|
1203
|
+
return false;
|
|
1204
|
+
}
|
|
1205
|
+
return true;
|
|
1206
|
+
};
|
|
900
1207
|
const sendSync = (r, f) => {
|
|
901
1208
|
if (!f.items.length)
|
|
902
|
-
return wsSend(r, { op: "sync", items: [], status: f.status });
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1209
|
+
return wsSend(r, { op: "sync", items: [], status: f.status, seq: f.seq });
|
|
1210
|
+
if (f.wire)
|
|
1211
|
+
return wsSendRaw(r, f.wire);
|
|
1212
|
+
const itemsJson = JSON.stringify(f.items);
|
|
1213
|
+
if (f.items.length <= 200 && itemsJson.length <= 512 * 1024) {
|
|
1214
|
+
return wsSendRaw(r, syncFrameStr(itemsJson, f.status, f.seq));
|
|
1215
|
+
}
|
|
1216
|
+
for (const c of chunkedFrames(f.items, 200, 512 * 1024))
|
|
1217
|
+
wsSendRaw(r, syncFrameStr(c.json, f.status, f.seq));
|
|
906
1218
|
return true;
|
|
907
1219
|
};
|
|
908
1220
|
const announceUrl = (st, r) => {
|
|
@@ -1044,7 +1356,8 @@ function createEngine(deps) {
|
|
|
1044
1356
|
logError(`sharing to ${r.url} disabled (no credential): ${cred.error}`);
|
|
1045
1357
|
return;
|
|
1046
1358
|
}
|
|
1047
|
-
const
|
|
1359
|
+
const useFrameAuth = !!token && frameAuthUrls.has(r.url);
|
|
1360
|
+
const ws = new WebSocket(`${wsUrl(r.url)}/api/ws${useFrameAuth ? "" : `?token=${encodeURIComponent(token ?? "")}`}`);
|
|
1048
1361
|
r.ws = ws;
|
|
1049
1362
|
await new Promise((resolve, reject) => {
|
|
1050
1363
|
const t = setTimeout(() => reject(new Error("ws open timeout")), 15000);
|
|
@@ -1080,6 +1393,27 @@ function createEngine(deps) {
|
|
|
1080
1393
|
if (!st.closed)
|
|
1081
1394
|
scheduleReconnect(st, r);
|
|
1082
1395
|
});
|
|
1396
|
+
if (useFrameAuth) {
|
|
1397
|
+
wsSend(r, { op: "auth", token });
|
|
1398
|
+
const authed = await new Promise((resolve) => {
|
|
1399
|
+
const t = setTimeout(() => resolve(false), 3000);
|
|
1400
|
+
r.authWaiter = () => {
|
|
1401
|
+
clearTimeout(t);
|
|
1402
|
+
resolve(true);
|
|
1403
|
+
};
|
|
1404
|
+
});
|
|
1405
|
+
r.authWaiter = undefined;
|
|
1406
|
+
if (!authed) {
|
|
1407
|
+
logError(`ws ${r.url.slice(0, 24)}: frame-auth ack timeout - reconnecting with query token`);
|
|
1408
|
+
frameAuthUrls.delete(r.url);
|
|
1409
|
+
try {
|
|
1410
|
+
ws.close(4000, "auth timeout");
|
|
1411
|
+
} catch {}
|
|
1412
|
+
if (!st.closed)
|
|
1413
|
+
scheduleReconnect(st, r);
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1083
1417
|
if (r.initWasAttach) {
|
|
1084
1418
|
wsSend(r, { op: "init", attach: { id: r.shareId, secret: r.secret, ...authFrame(st) } });
|
|
1085
1419
|
} else {
|
|
@@ -1108,7 +1442,7 @@ function createEngine(deps) {
|
|
|
1108
1442
|
const scheduleReconnect = (st, r) => {
|
|
1109
1443
|
if (st.closed || r.reconnectTimer)
|
|
1110
1444
|
return;
|
|
1111
|
-
const wait = r.backoff;
|
|
1445
|
+
const wait = Math.max(250, Math.round(r.backoff * (0.8 + Math.random() * 0.4)));
|
|
1112
1446
|
r.backoff = Math.min(r.backoff * 2, 60000);
|
|
1113
1447
|
r.reconnectTimer = setTimeout(() => {
|
|
1114
1448
|
r.reconnectTimer = null;
|
|
@@ -1190,6 +1524,11 @@ function createEngine(deps) {
|
|
|
1190
1524
|
}
|
|
1191
1525
|
};
|
|
1192
1526
|
const handleFrame = async (st, r, frame) => {
|
|
1527
|
+
if (frame.op === "authed") {
|
|
1528
|
+
r.authWaiter?.();
|
|
1529
|
+
r.authWaiter = undefined;
|
|
1530
|
+
return;
|
|
1531
|
+
}
|
|
1193
1532
|
if (frame.op === "init") {
|
|
1194
1533
|
if (!frame.ok) {
|
|
1195
1534
|
const err = String(frame.error ?? "");
|
|
@@ -1228,6 +1567,16 @@ function createEngine(deps) {
|
|
|
1228
1567
|
queueConnect(st, r);
|
|
1229
1568
|
return;
|
|
1230
1569
|
}
|
|
1570
|
+
if (/limit|too many|rate/i.test(err)) {
|
|
1571
|
+
logError(`${r.url}: create refused:`, frame.error, "- retrying with backoff");
|
|
1572
|
+
r.connecting = false;
|
|
1573
|
+
try {
|
|
1574
|
+
r.ws?.close(4000, "rate limited");
|
|
1575
|
+
} catch {}
|
|
1576
|
+
if (!st.closed)
|
|
1577
|
+
scheduleReconnect(st, r);
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1231
1580
|
logError(`${r.url}: init refused:`, frame.error, "- creating fresh share");
|
|
1232
1581
|
deleteAbandoned(r);
|
|
1233
1582
|
r.shareId = null;
|
|
@@ -1256,11 +1605,19 @@ function createEngine(deps) {
|
|
|
1256
1605
|
r.connecting = false;
|
|
1257
1606
|
r.initWasAttach = false;
|
|
1258
1607
|
r.collided = null;
|
|
1608
|
+
if (r.frozenRemote && !frame.error)
|
|
1609
|
+
r.frozenRemote = false;
|
|
1259
1610
|
if (r.initTimer) {
|
|
1260
1611
|
clearTimeout(r.initTimer);
|
|
1261
1612
|
r.initTimer = null;
|
|
1262
1613
|
}
|
|
1263
|
-
r.
|
|
1614
|
+
if (r.backoffResetTimer)
|
|
1615
|
+
clearTimeout(r.backoffResetTimer);
|
|
1616
|
+
r.backoffResetTimer = setTimeout(() => {
|
|
1617
|
+
r.backoffResetTimer = null;
|
|
1618
|
+
if (r.wsReady && !r.reconnectTimer)
|
|
1619
|
+
r.backoff = 1000;
|
|
1620
|
+
}, 30000);
|
|
1264
1621
|
st.everConnected = true;
|
|
1265
1622
|
if (frame.policy)
|
|
1266
1623
|
applyPolicy(frame.policy, r.url);
|
|
@@ -1280,18 +1637,49 @@ function createEngine(deps) {
|
|
|
1280
1637
|
flush(st);
|
|
1281
1638
|
}
|
|
1282
1639
|
if (wasFanoutCreate) {
|
|
1640
|
+
st.sentHash.clear();
|
|
1283
1641
|
chain = chain.then(() => backfill(st)).catch((e) => logError("fan-out backfill error", e?.message || e));
|
|
1284
1642
|
}
|
|
1285
1643
|
return;
|
|
1286
1644
|
}
|
|
1287
1645
|
if (frame.op === "ack") {
|
|
1288
1646
|
r.ackedRev = frame.rev ?? r.ackedRev;
|
|
1289
|
-
r.
|
|
1290
|
-
|
|
1647
|
+
if (r.backoffResetTimer) {
|
|
1648
|
+
clearTimeout(r.backoffResetTimer);
|
|
1649
|
+
r.backoffResetTimer = null;
|
|
1650
|
+
r.backoff = 1000;
|
|
1651
|
+
}
|
|
1652
|
+
if (typeof frame.seq === "number") {
|
|
1653
|
+
r.pending = r.pending.filter((p) => p.seq !== frame.seq);
|
|
1654
|
+
} else {
|
|
1655
|
+
r.pending = [];
|
|
1656
|
+
}
|
|
1657
|
+
scheduleSpoolSave(st, r);
|
|
1291
1658
|
scheduleBindingWrite(st);
|
|
1292
1659
|
return;
|
|
1293
1660
|
}
|
|
1294
1661
|
if (frame.op === "nack" || frame.op === "error") {
|
|
1662
|
+
const errText = String(frame.error ?? "");
|
|
1663
|
+
if (frame.op === "nack") {
|
|
1664
|
+
if (/not found|gone/i.test(errText)) {
|
|
1665
|
+
logError(`${r.url}: share vanished server-side (nack: ${errText}) - recreating`);
|
|
1666
|
+
deleteAbandoned(r);
|
|
1667
|
+
r.shareId = null;
|
|
1668
|
+
r.secret = null;
|
|
1669
|
+
r.ackedRev = 0;
|
|
1670
|
+
try {
|
|
1671
|
+
r.ws?.close(4000, "share gone");
|
|
1672
|
+
} catch {}
|
|
1673
|
+
if (!st.closed)
|
|
1674
|
+
queueConnect(st, r);
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
if (/frozen/i.test(errText)) {
|
|
1678
|
+
r.frozenRemote = true;
|
|
1679
|
+
logError(`${r.url}: share frozen by the hoster - edits refused; contact the server admin`);
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1295
1683
|
logError(`${r.url}: worker refused frame:`, JSON.stringify(frame).slice(0, 200));
|
|
1296
1684
|
if (frame.op === "error" && r.wsReady && !st.closed) {
|
|
1297
1685
|
r.wsReady = false;
|
|
@@ -1314,6 +1702,7 @@ function createEngine(deps) {
|
|
|
1314
1702
|
queueConnect(st, r);
|
|
1315
1703
|
};
|
|
1316
1704
|
const frameFrom = (st) => {
|
|
1705
|
+
stampsPass(st);
|
|
1317
1706
|
const items = Array.from(st.queue.values());
|
|
1318
1707
|
st.queue.clear();
|
|
1319
1708
|
const statusChanged = st.status !== st.lastSentStatus;
|
|
@@ -1333,14 +1722,32 @@ function createEngine(deps) {
|
|
|
1333
1722
|
const f = frameFrom(st);
|
|
1334
1723
|
if (!f)
|
|
1335
1724
|
return;
|
|
1725
|
+
const chunks = f.items.length ? chunkedFrames(f.items, 200, 512 * 1024) : [];
|
|
1336
1726
|
let httpWork;
|
|
1337
1727
|
for (const r of st.remotes.values()) {
|
|
1338
|
-
r.
|
|
1339
|
-
|
|
1728
|
+
if (r.frozenRemote)
|
|
1729
|
+
continue;
|
|
1730
|
+
const fresh = [];
|
|
1731
|
+
if (chunks.length) {
|
|
1732
|
+
for (const c of chunks) {
|
|
1733
|
+
const seq = ++nextSeq;
|
|
1734
|
+
const cf = { items: c.items, status: f.status, seq, wire: syncFrameStr(c.json, f.status, seq) };
|
|
1735
|
+
fresh.push(cf);
|
|
1736
|
+
r.pending.push(cf);
|
|
1737
|
+
spoolAppend(st, r, cf);
|
|
1738
|
+
}
|
|
1739
|
+
} else {
|
|
1740
|
+
const seq = ++nextSeq;
|
|
1741
|
+
const cf = { items: [], status: f.status, seq };
|
|
1742
|
+
fresh.push(cf);
|
|
1743
|
+
r.pending.push(cf);
|
|
1744
|
+
spoolAppend(st, r, cf);
|
|
1745
|
+
}
|
|
1340
1746
|
if (r.ws && !r.wsReady)
|
|
1341
1747
|
continue;
|
|
1342
1748
|
if (r.wsReady) {
|
|
1343
|
-
|
|
1749
|
+
for (const cf of fresh)
|
|
1750
|
+
sendSync(r, cf);
|
|
1344
1751
|
continue;
|
|
1345
1752
|
}
|
|
1346
1753
|
const viaHttp = flushViaHttp(st, r);
|
|
@@ -1349,32 +1756,68 @@ function createEngine(deps) {
|
|
|
1349
1756
|
}
|
|
1350
1757
|
if (st.remotes.size)
|
|
1351
1758
|
st.lastFlushAt = Date.now();
|
|
1759
|
+
if (httpWork)
|
|
1760
|
+
httpWork.catch((e) => logError("http flush error", e?.message || e));
|
|
1352
1761
|
return httpWork;
|
|
1353
1762
|
};
|
|
1763
|
+
const httpDedupeKey = (it) => {
|
|
1764
|
+
if (!it || typeof it !== "object")
|
|
1765
|
+
return null;
|
|
1766
|
+
const o = it;
|
|
1767
|
+
switch (o.type) {
|
|
1768
|
+
case "session":
|
|
1769
|
+
return "session/" + (o.data?.id ?? "");
|
|
1770
|
+
case "session_diff":
|
|
1771
|
+
return "session_diff/" + (o.sessionID ?? o.data?.sessionID ?? "");
|
|
1772
|
+
case "message":
|
|
1773
|
+
return "message/" + (o.data?.id ?? "");
|
|
1774
|
+
case "part":
|
|
1775
|
+
return "part/" + (o.data?.messageID ?? "") + "/" + (o.data?.id ?? "");
|
|
1776
|
+
case "model":
|
|
1777
|
+
return "model/" + (o.sessionID ?? o.data?.sessionID ?? "");
|
|
1778
|
+
case "prefs":
|
|
1779
|
+
return "prefs";
|
|
1780
|
+
default:
|
|
1781
|
+
return null;
|
|
1782
|
+
}
|
|
1783
|
+
};
|
|
1354
1784
|
const flushViaHttp = async (st, r) => {
|
|
1355
1785
|
if (!r.shareId || !r.pending.length)
|
|
1356
1786
|
return;
|
|
1357
|
-
const
|
|
1787
|
+
const byKey = new Map;
|
|
1788
|
+
const unkeyed = [];
|
|
1358
1789
|
let status;
|
|
1790
|
+
const sentSeqs = new Set;
|
|
1359
1791
|
for (const f of r.pending) {
|
|
1360
|
-
for (const it of f.items)
|
|
1361
|
-
|
|
1792
|
+
for (const it of f.items) {
|
|
1793
|
+
const k = httpDedupeKey(it);
|
|
1794
|
+
if (k)
|
|
1795
|
+
byKey.set(k, it);
|
|
1796
|
+
else
|
|
1797
|
+
unkeyed.push(it);
|
|
1798
|
+
}
|
|
1362
1799
|
if (f.status)
|
|
1363
1800
|
status = f.status;
|
|
1801
|
+
if (typeof f.seq === "number")
|
|
1802
|
+
sentSeqs.add(f.seq);
|
|
1364
1803
|
}
|
|
1804
|
+
const items = [...byKey.values(), ...unkeyed];
|
|
1365
1805
|
const cred = credOf(r.url);
|
|
1366
|
-
const
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1806
|
+
const chunks = items.length ? chunkItems(items, 500, 4 * 1024 * 1024) : [[]];
|
|
1807
|
+
for (const chunk of chunks) {
|
|
1808
|
+
const res = await fetch(`${r.url}/api/share/${r.shareId}/sync`, {
|
|
1809
|
+
method: "POST",
|
|
1810
|
+
headers: { "content-type": "application/json", ...cred.credential ? { authorization: `Bearer ${cred.credential}` } : {} },
|
|
1811
|
+
body: JSON.stringify({ secret: r.secret ?? "", data: chunk, status }),
|
|
1812
|
+
signal: AbortSignal.timeout(15000)
|
|
1813
|
+
}).catch(() => null);
|
|
1814
|
+
if (!res || !res.ok)
|
|
1815
|
+
return;
|
|
1373
1816
|
const body = await res.json().catch(() => ({}));
|
|
1374
1817
|
r.ackedRev = body.rev ?? r.ackedRev;
|
|
1375
|
-
r.pending = [];
|
|
1376
|
-
spoolSave(st, r);
|
|
1377
1818
|
}
|
|
1819
|
+
r.pending = r.pending.filter((p) => !sentSeqs.has(p.seq));
|
|
1820
|
+
scheduleSpoolSave(st, r);
|
|
1378
1821
|
};
|
|
1379
1822
|
const scheduleFlush = (st) => {
|
|
1380
1823
|
if (st.timer)
|
|
@@ -1385,19 +1828,316 @@ function createEngine(deps) {
|
|
|
1385
1828
|
st.timer = null;
|
|
1386
1829
|
if (debugEvents)
|
|
1387
1830
|
log(`flush timer fired sid=${st.sid.slice(0, 18)} queue=${st.queue.size} remotes=${st.remotes.size}`);
|
|
1388
|
-
chain = chain.then(() =>
|
|
1831
|
+
chain = chain.then(() => {
|
|
1832
|
+
flush(st);
|
|
1833
|
+
}).catch((e) => logError("flush error", e?.message || e));
|
|
1389
1834
|
}, wait);
|
|
1390
1835
|
};
|
|
1391
1836
|
const enqueue = (st, entry) => {
|
|
1392
1837
|
if (!st || !entry)
|
|
1393
1838
|
return;
|
|
1839
|
+
let h;
|
|
1840
|
+
try {
|
|
1841
|
+
h = createHash("sha256").update(JSON.stringify(entry.item)).digest("hex");
|
|
1842
|
+
} catch {
|
|
1843
|
+
h = "";
|
|
1844
|
+
}
|
|
1845
|
+
if (h && st.sentHash.get(entry.key) === h) {
|
|
1846
|
+
if (debugEvents)
|
|
1847
|
+
log(`enqueue dedupe ${entry.item.type} ${entry.key.slice(0, 40)}`);
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1850
|
+
if (h)
|
|
1851
|
+
st.sentHash.set(entry.key, h);
|
|
1394
1852
|
st.queue.set(entry.key, entry.item);
|
|
1853
|
+
indexMessage(entry);
|
|
1395
1854
|
if (debugEvents)
|
|
1396
1855
|
log(`enqueue sid=${st.sid.slice(0, 18)} ${entry.item.type} queue=${st.queue.size}`);
|
|
1397
1856
|
touch(st);
|
|
1398
1857
|
ensureConnected(st);
|
|
1399
1858
|
scheduleFlush(st);
|
|
1400
1859
|
};
|
|
1860
|
+
const msgIndex = new Map;
|
|
1861
|
+
const tombstoned = new Set;
|
|
1862
|
+
const indexMessage = (entry) => {
|
|
1863
|
+
if (entry.item.type !== "message")
|
|
1864
|
+
return;
|
|
1865
|
+
const d = entry.item.data;
|
|
1866
|
+
if (!d || typeof d.id !== "string")
|
|
1867
|
+
return;
|
|
1868
|
+
const role = String(d.type ?? "");
|
|
1869
|
+
if (role !== "user" && role !== "assistant")
|
|
1870
|
+
return;
|
|
1871
|
+
const key = `${entry.item.sessionID}:${d.id}`;
|
|
1872
|
+
const prev = msgIndex.get(key);
|
|
1873
|
+
msgIndex.set(key, {
|
|
1874
|
+
sid: String(entry.item.sessionID ?? ""),
|
|
1875
|
+
role,
|
|
1876
|
+
agent: role === "assistant" ? typeof d.agent === "string" ? d.agent : prev?.agent : typeof d.metadata?.agent === "string" ? d.metadata.agent : prev?.agent,
|
|
1877
|
+
created: Number(d.time?.created ?? prev?.created ?? 0),
|
|
1878
|
+
data: d
|
|
1879
|
+
});
|
|
1880
|
+
};
|
|
1881
|
+
const sessionStateOf = (sid) => sessions.get(childToParent.get(sid) ?? sid);
|
|
1882
|
+
const reEnqueueMessage = (m) => {
|
|
1883
|
+
if (tombstoned.has(`${m.sid}:${m.data.id}`))
|
|
1884
|
+
return;
|
|
1885
|
+
const st = sessionStateOf(m.sid);
|
|
1886
|
+
if (st)
|
|
1887
|
+
enqueue(st, itemFor("message", m.data, m.sid));
|
|
1888
|
+
};
|
|
1889
|
+
const segmentAgent = (ordered, at) => {
|
|
1890
|
+
const own = ordered[at]?.agent;
|
|
1891
|
+
if (own)
|
|
1892
|
+
return own;
|
|
1893
|
+
for (let i = at + 1;i < ordered.length; i++) {
|
|
1894
|
+
if (ordered[i].role === "assistant" && ordered[i].agent)
|
|
1895
|
+
return ordered[i].agent;
|
|
1896
|
+
if (ordered[i].role === "user")
|
|
1897
|
+
break;
|
|
1898
|
+
}
|
|
1899
|
+
return;
|
|
1900
|
+
};
|
|
1901
|
+
const stampAgentSwitches = (bySession) => {
|
|
1902
|
+
for (const [, list] of bySession) {
|
|
1903
|
+
list.sort((a, b) => a.created - b.created);
|
|
1904
|
+
const users = list.map((m, i) => m.role === "user" ? i : -1).filter((i) => i >= 0);
|
|
1905
|
+
let prev;
|
|
1906
|
+
for (let k = 0;k < users.length; k++) {
|
|
1907
|
+
const agent = segmentAgent(list, users[k]);
|
|
1908
|
+
const d = list[users[k]].data;
|
|
1909
|
+
const want = k > 0 && agent && prev && agent !== prev ? agent : undefined;
|
|
1910
|
+
if (want !== d.metadata?.agentSwitch) {
|
|
1911
|
+
if (want) {
|
|
1912
|
+
const meta = d.metadata ??= {};
|
|
1913
|
+
meta.agentSwitch = want;
|
|
1914
|
+
} else if (d.metadata) {
|
|
1915
|
+
delete d.metadata.agentSwitch;
|
|
1916
|
+
}
|
|
1917
|
+
reEnqueueMessage(list[users[k]]);
|
|
1918
|
+
}
|
|
1919
|
+
if (agent)
|
|
1920
|
+
prev = agent;
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
};
|
|
1924
|
+
const stampTaskAgents = (bySession) => {
|
|
1925
|
+
for (const list of bySession.values())
|
|
1926
|
+
list.sort((a, b) => a.created - b.created);
|
|
1927
|
+
for (const m of msgIndex.values()) {
|
|
1928
|
+
if (m.role !== "assistant")
|
|
1929
|
+
continue;
|
|
1930
|
+
const content = m.data.content;
|
|
1931
|
+
if (!Array.isArray(content))
|
|
1932
|
+
continue;
|
|
1933
|
+
let mutated = false;
|
|
1934
|
+
for (const part of content) {
|
|
1935
|
+
if (!part || part.type !== "tool")
|
|
1936
|
+
continue;
|
|
1937
|
+
if (part.name !== "task" && part.name !== "subagent")
|
|
1938
|
+
continue;
|
|
1939
|
+
const state = part.state;
|
|
1940
|
+
const meta = state?.metadata;
|
|
1941
|
+
const childSid = typeof meta?.sessionID === "string" ? meta.sessionID : undefined;
|
|
1942
|
+
if (!childSid || typeof meta.agent === "string")
|
|
1943
|
+
continue;
|
|
1944
|
+
const partStart = Number(part.time?.created ?? 0);
|
|
1945
|
+
const child = bySession.get(childSid);
|
|
1946
|
+
if (!child)
|
|
1947
|
+
continue;
|
|
1948
|
+
let agent;
|
|
1949
|
+
for (const c of child) {
|
|
1950
|
+
if (c.created < partStart)
|
|
1951
|
+
continue;
|
|
1952
|
+
if (c.role === "user" && c.agent) {
|
|
1953
|
+
agent = c.agent;
|
|
1954
|
+
break;
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
if (!agent) {
|
|
1958
|
+
for (const c of child) {
|
|
1959
|
+
if (c.created < partStart)
|
|
1960
|
+
continue;
|
|
1961
|
+
if (c.role === "assistant" && c.agent) {
|
|
1962
|
+
agent = c.agent;
|
|
1963
|
+
break;
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
if (agent) {
|
|
1968
|
+
meta.agent = agent;
|
|
1969
|
+
mutated = true;
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
if (mutated)
|
|
1973
|
+
reEnqueueMessage(m);
|
|
1974
|
+
}
|
|
1975
|
+
};
|
|
1976
|
+
const LANG_BY_EXT = {
|
|
1977
|
+
ts: "typescript",
|
|
1978
|
+
tsx: "tsx",
|
|
1979
|
+
mts: "typescript",
|
|
1980
|
+
cts: "typescript",
|
|
1981
|
+
js: "javascript",
|
|
1982
|
+
jsx: "jsx",
|
|
1983
|
+
mjs: "javascript",
|
|
1984
|
+
cjs: "javascript",
|
|
1985
|
+
json: "json",
|
|
1986
|
+
jsonc: "json",
|
|
1987
|
+
md: "markdown",
|
|
1988
|
+
mdx: "mdx",
|
|
1989
|
+
py: "python",
|
|
1990
|
+
rs: "rust",
|
|
1991
|
+
go: "go",
|
|
1992
|
+
java: "java",
|
|
1993
|
+
rb: "ruby",
|
|
1994
|
+
css: "css",
|
|
1995
|
+
scss: "scss",
|
|
1996
|
+
html: "html",
|
|
1997
|
+
xml: "xml",
|
|
1998
|
+
yaml: "yaml",
|
|
1999
|
+
yml: "yaml",
|
|
2000
|
+
toml: "toml",
|
|
2001
|
+
sh: "shellscript",
|
|
2002
|
+
bash: "shellscript",
|
|
2003
|
+
ps1: "powershell",
|
|
2004
|
+
sql: "sql",
|
|
2005
|
+
c: "c",
|
|
2006
|
+
h: "c",
|
|
2007
|
+
cpp: "cpp",
|
|
2008
|
+
hpp: "cpp",
|
|
2009
|
+
cs: "csharp",
|
|
2010
|
+
swift: "swift",
|
|
2011
|
+
kt: "kotlin",
|
|
2012
|
+
php: "php",
|
|
2013
|
+
lua: "lua",
|
|
2014
|
+
vue: "vue",
|
|
2015
|
+
svelte: "svelte"
|
|
2016
|
+
};
|
|
2017
|
+
const FILE_TOOLS = new Set(["read", "write", "edit", "patch"]);
|
|
2018
|
+
const stampFileLanguages = () => {
|
|
2019
|
+
for (const m of msgIndex.values()) {
|
|
2020
|
+
if (m.role !== "assistant")
|
|
2021
|
+
continue;
|
|
2022
|
+
const content = m.data.content;
|
|
2023
|
+
if (!Array.isArray(content))
|
|
2024
|
+
continue;
|
|
2025
|
+
let mutated = false;
|
|
2026
|
+
for (const part of content) {
|
|
2027
|
+
if (!part || part.type !== "tool")
|
|
2028
|
+
continue;
|
|
2029
|
+
if (!FILE_TOOLS.has(part.name))
|
|
2030
|
+
continue;
|
|
2031
|
+
const state = part.state;
|
|
2032
|
+
const meta = state?.metadata;
|
|
2033
|
+
if (!meta)
|
|
2034
|
+
continue;
|
|
2035
|
+
const raw = state.input?.path ?? state.input?.filePath;
|
|
2036
|
+
if (typeof meta.language !== "string" && typeof raw === "string") {
|
|
2037
|
+
const ext = raw.split(".").pop()?.toLowerCase() ?? "";
|
|
2038
|
+
const lang = LANG_BY_EXT[ext];
|
|
2039
|
+
if (lang) {
|
|
2040
|
+
meta.language = lang;
|
|
2041
|
+
mutated = true;
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
if (typeof meta.lineStart !== "number" && part.name === "read") {
|
|
2045
|
+
const off = state.input?.offset;
|
|
2046
|
+
if (typeof off === "number" && off >= 0 && off > 0) {
|
|
2047
|
+
meta.lineStart = off + 1;
|
|
2048
|
+
mutated = true;
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
if (mutated)
|
|
2053
|
+
reEnqueueMessage(m);
|
|
2054
|
+
}
|
|
2055
|
+
};
|
|
2056
|
+
const stampsPass = (st) => {
|
|
2057
|
+
let any = false;
|
|
2058
|
+
for (const it of st.queue.values()) {
|
|
2059
|
+
if (it.type === "message") {
|
|
2060
|
+
any = true;
|
|
2061
|
+
break;
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
if (!any)
|
|
2065
|
+
return;
|
|
2066
|
+
const bySession = new Map;
|
|
2067
|
+
for (const m of msgIndex.values()) {
|
|
2068
|
+
const list = bySession.get(m.sid);
|
|
2069
|
+
if (list)
|
|
2070
|
+
list.push(m);
|
|
2071
|
+
else
|
|
2072
|
+
bySession.set(m.sid, [m]);
|
|
2073
|
+
}
|
|
2074
|
+
for (const list of bySession.values())
|
|
2075
|
+
list.sort((a, b) => a.created - b.created);
|
|
2076
|
+
stampAgentSwitches(bySession);
|
|
2077
|
+
stampTaskAgents(bySession);
|
|
2078
|
+
stampFileLanguages();
|
|
2079
|
+
};
|
|
2080
|
+
const mergeEntry = (st, messageID) => {
|
|
2081
|
+
let e = st.merged.get(messageID);
|
|
2082
|
+
if (!e) {
|
|
2083
|
+
e = { parts: new Map };
|
|
2084
|
+
st.merged.set(messageID, e);
|
|
2085
|
+
}
|
|
2086
|
+
return e;
|
|
2087
|
+
};
|
|
2088
|
+
const emitMerged = (st, sid, messageID) => {
|
|
2089
|
+
const e = st.merged.get(messageID);
|
|
2090
|
+
if (!e?.info)
|
|
2091
|
+
return;
|
|
2092
|
+
const res = normalizeV1Message(e.info, [...e.parts.values()]);
|
|
2093
|
+
if (!res)
|
|
2094
|
+
return;
|
|
2095
|
+
if ("refill" in res) {
|
|
2096
|
+
const parent = st.merged.get(res.refill);
|
|
2097
|
+
if (parent?.info) {
|
|
2098
|
+
const compaction = normalizeV1Message(parent.info, [...parent.parts.values()]);
|
|
2099
|
+
if (compaction && "message" in compaction) {
|
|
2100
|
+
const msg = compaction.message;
|
|
2101
|
+
msg.summary = res.summary;
|
|
2102
|
+
enqueue(st, itemFor("message", msg, sid));
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
return;
|
|
2106
|
+
}
|
|
2107
|
+
enqueue(st, itemFor("message", res.message, sid));
|
|
2108
|
+
};
|
|
2109
|
+
const ingestMessageInfo = (st, sid, info) => {
|
|
2110
|
+
if (isV2NativeMessage(info)) {
|
|
2111
|
+
enqueue(st, itemFor("message", info, sid));
|
|
2112
|
+
return;
|
|
2113
|
+
}
|
|
2114
|
+
if (typeof info?.id !== "string")
|
|
2115
|
+
return;
|
|
2116
|
+
const e = mergeEntry(st, info.id);
|
|
2117
|
+
e.info = info;
|
|
2118
|
+
emitMerged(st, sid, info.id);
|
|
2119
|
+
};
|
|
2120
|
+
const ingestPart = (st, sid, part) => {
|
|
2121
|
+
if (typeof part?.id !== "string" || typeof part.messageID !== "string")
|
|
2122
|
+
return;
|
|
2123
|
+
const e = mergeEntry(st, part.messageID);
|
|
2124
|
+
e.parts.set(part.id, part);
|
|
2125
|
+
emitMerged(st, sid, part.messageID);
|
|
2126
|
+
};
|
|
2127
|
+
const dropMergedMessage = (st, messageID, evSid) => {
|
|
2128
|
+
const sid = evSid ?? st.sid;
|
|
2129
|
+
st.merged.delete(messageID);
|
|
2130
|
+
msgIndex.delete(`${sid}:${messageID}`);
|
|
2131
|
+
tombstoned.add(`${sid}:${messageID}`);
|
|
2132
|
+
};
|
|
2133
|
+
const dropMergedPart = (st, sid, messageID, partID) => {
|
|
2134
|
+
const e = st.merged.get(messageID);
|
|
2135
|
+
if (!e)
|
|
2136
|
+
return;
|
|
2137
|
+
e.parts.delete(partID);
|
|
2138
|
+
if (e.info)
|
|
2139
|
+
emitMerged(st, sid, messageID);
|
|
2140
|
+
};
|
|
1401
2141
|
const remove = (st) => {
|
|
1402
2142
|
st.closed = true;
|
|
1403
2143
|
st.activated = false;
|
|
@@ -1416,6 +2156,10 @@ function createEngine(deps) {
|
|
|
1416
2156
|
clearTimeout(r.initTimer);
|
|
1417
2157
|
r.initTimer = null;
|
|
1418
2158
|
}
|
|
2159
|
+
if (r.backoffResetTimer) {
|
|
2160
|
+
clearTimeout(r.backoffResetTimer);
|
|
2161
|
+
r.backoffResetTimer = null;
|
|
2162
|
+
}
|
|
1419
2163
|
if (r.reconnectTimer) {
|
|
1420
2164
|
clearTimeout(r.reconnectTimer);
|
|
1421
2165
|
r.reconnectTimer = null;
|
|
@@ -1425,9 +2169,21 @@ function createEngine(deps) {
|
|
|
1425
2169
|
clearTimeout(r.idleDropTimer);
|
|
1426
2170
|
r.idleDropTimer = null;
|
|
1427
2171
|
}
|
|
1428
|
-
if (r.wsReady)
|
|
2172
|
+
if (r.wsReady && r.shareId) {
|
|
1429
2173
|
wsSend(r, { op: "delete" });
|
|
1430
|
-
|
|
2174
|
+
if (r.shareId && r.secret) {
|
|
2175
|
+
const sid = r.shareId;
|
|
2176
|
+
const sec = r.secret;
|
|
2177
|
+
setTimeout(() => {
|
|
2178
|
+
fetch(`${r.url}/api/share/${sid}`, {
|
|
2179
|
+
method: "DELETE",
|
|
2180
|
+
headers: { "content-type": "application/json" },
|
|
2181
|
+
body: JSON.stringify({ secret: sec }),
|
|
2182
|
+
signal: AbortSignal.timeout(8000)
|
|
2183
|
+
}).catch(() => {});
|
|
2184
|
+
}, 2000);
|
|
2185
|
+
}
|
|
2186
|
+
} else if (r.shareId && r.secret) {
|
|
1431
2187
|
fetch(`${r.url}/api/share/${r.shareId}`, {
|
|
1432
2188
|
method: "DELETE",
|
|
1433
2189
|
headers: { "content-type": "application/json" },
|
|
@@ -1445,6 +2201,17 @@ function createEngine(deps) {
|
|
|
1445
2201
|
}, 1000);
|
|
1446
2202
|
for (const r of st.remotes.values())
|
|
1447
2203
|
spoolClear(st, r);
|
|
2204
|
+
const stSid = st.sid;
|
|
2205
|
+
const pfx = `${stSid}:`;
|
|
2206
|
+
for (const k of msgIndex.keys())
|
|
2207
|
+
if (k.startsWith(pfx))
|
|
2208
|
+
msgIndex.delete(k);
|
|
2209
|
+
for (const k of tombstoned)
|
|
2210
|
+
if (k.startsWith(pfx))
|
|
2211
|
+
tombstoned.delete(k);
|
|
2212
|
+
for (const [child, parent] of childToParent)
|
|
2213
|
+
if (parent === stSid || child === stSid)
|
|
2214
|
+
childToParent.delete(child);
|
|
1448
2215
|
sessions.delete(st.sid);
|
|
1449
2216
|
try {
|
|
1450
2217
|
chain = chain.then(async () => {
|
|
@@ -1470,7 +2237,7 @@ function createEngine(deps) {
|
|
|
1470
2237
|
const PAGE_SIZE = 100;
|
|
1471
2238
|
const MAX_PAGES = 50;
|
|
1472
2239
|
const query = () => deps.directory ? { directory: deps.directory } : {};
|
|
1473
|
-
async function fetchMessages(client, sid) {
|
|
2240
|
+
async function fetchMessages(client, sid, onPage) {
|
|
1474
2241
|
if (typeof client.session.messages !== "function")
|
|
1475
2242
|
return [];
|
|
1476
2243
|
const out = [];
|
|
@@ -1486,41 +2253,58 @@ function createEngine(deps) {
|
|
|
1486
2253
|
if (!Array.isArray(result) || result.length === 0)
|
|
1487
2254
|
break;
|
|
1488
2255
|
out.push(...result);
|
|
2256
|
+
if (onPage) {
|
|
2257
|
+
try {
|
|
2258
|
+
onPage(result);
|
|
2259
|
+
} catch {}
|
|
2260
|
+
}
|
|
1489
2261
|
if (result.length < PAGE_SIZE)
|
|
1490
2262
|
break;
|
|
1491
2263
|
const last = result[result.length - 1];
|
|
1492
|
-
const
|
|
1493
|
-
|
|
2264
|
+
const lastInfo = last?.info ?? last;
|
|
2265
|
+
const lastID = typeof lastInfo?.id === "string" ? lastInfo.id : undefined;
|
|
2266
|
+
const lastTime = lastInfo?.time?.created;
|
|
2267
|
+
if (!lastID || typeof lastTime !== "number")
|
|
2268
|
+
break;
|
|
2269
|
+
const nextBefore = cursorFor(lastID, lastTime);
|
|
2270
|
+
if (nextBefore === before)
|
|
1494
2271
|
break;
|
|
1495
2272
|
before = nextBefore;
|
|
1496
2273
|
}
|
|
2274
|
+
if (out.length >= MAX_PAGES * PAGE_SIZE) {
|
|
2275
|
+
logError(`backfill truncated at ${out.length} messages (page cap) for ${sid.slice(0, 18)} — oldest history not shared`);
|
|
2276
|
+
}
|
|
1497
2277
|
return out;
|
|
1498
2278
|
}
|
|
1499
2279
|
const backfillSession = async (st, client, sid, info) => {
|
|
1500
2280
|
const sessionInfoValue = info ?? await sessionInfo(client, sid);
|
|
1501
2281
|
if (sessionInfoValue)
|
|
1502
2282
|
enqueue(st, itemFor("session", redact(sessionInfoValue), sid));
|
|
1503
|
-
const messages = await fetchMessages(client, sid);
|
|
1504
2283
|
let modelSeen = false;
|
|
1505
|
-
|
|
1506
|
-
const
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
enqueue(st, itemFor("message", m, sid));
|
|
1510
|
-
if (m.role === "user" && m.model) {
|
|
1511
|
-
modelSeen = true;
|
|
1512
|
-
const label = formatLabel(st.opts?.modelLabel ?? cfg.modelLabel, m.model);
|
|
1513
|
-
enqueue(st, itemFor("model", [{ id: m.model.modelID, providerID: m.model.providerID, name: label }], sid));
|
|
1514
|
-
}
|
|
1515
|
-
for (const part of message.parts ?? []) {
|
|
1516
|
-
const p = part;
|
|
1517
|
-
if (!p || typeof p.id !== "string" || typeof p.messageID !== "string")
|
|
2284
|
+
const consume = (messages) => {
|
|
2285
|
+
for (const message of messages) {
|
|
2286
|
+
const m = message.info ?? message;
|
|
2287
|
+
if (!m || typeof m.id !== "string")
|
|
1518
2288
|
continue;
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
2289
|
+
const e = mergeEntry(st, m.id);
|
|
2290
|
+
e.info = m;
|
|
2291
|
+
for (const rawPart of message.parts ?? []) {
|
|
2292
|
+
const p = rawPart;
|
|
2293
|
+
if (!p || typeof p.id !== "string" || typeof p.messageID !== "string")
|
|
2294
|
+
continue;
|
|
2295
|
+
if (p.state?.status === "error")
|
|
2296
|
+
st.badSignal = true;
|
|
2297
|
+
e.parts.set(p.id, p);
|
|
2298
|
+
}
|
|
2299
|
+
emitMerged(st, sid, m.id);
|
|
2300
|
+
if (m.role === "user" && m.model) {
|
|
2301
|
+
modelSeen = true;
|
|
2302
|
+
const label = formatLabel(st.opts?.modelLabel ?? cfg.modelLabel, m.model);
|
|
2303
|
+
enqueue(st, itemFor("model", [{ id: m.model.modelID, providerID: m.model.providerID, name: label }], sid));
|
|
2304
|
+
}
|
|
1522
2305
|
}
|
|
1523
|
-
}
|
|
2306
|
+
};
|
|
2307
|
+
const messages = await fetchMessages(client, sid, (page) => consume(page));
|
|
1524
2308
|
if (!modelSeen && sessionInfoValue?.model) {
|
|
1525
2309
|
const label = formatLabel(st.opts?.modelLabel ?? cfg.modelLabel, sessionInfoValue.model);
|
|
1526
2310
|
enqueue(st, itemFor("model", [{ id: sessionInfoValue.model.modelID, providerID: sessionInfoValue.model.providerID, name: label }], sid));
|
|
@@ -1734,7 +2518,9 @@ function createEngine(deps) {
|
|
|
1734
2518
|
if (st.timer)
|
|
1735
2519
|
clearTimeout(st.timer);
|
|
1736
2520
|
st.timer = null;
|
|
1737
|
-
chain = chain.then(() =>
|
|
2521
|
+
chain = chain.then(() => {
|
|
2522
|
+
flush(st);
|
|
2523
|
+
}).catch((e) => logError("idle flush error", e?.message || e));
|
|
1738
2524
|
const epoch = st.statusEpoch;
|
|
1739
2525
|
const settle = () => {
|
|
1740
2526
|
st.status = st.badSignal ? "error" : "completed";
|
|
@@ -1742,7 +2528,9 @@ function createEngine(deps) {
|
|
|
1742
2528
|
if (st.timer)
|
|
1743
2529
|
clearTimeout(st.timer);
|
|
1744
2530
|
st.timer = null;
|
|
1745
|
-
chain = chain.then(() =>
|
|
2531
|
+
chain = chain.then(() => {
|
|
2532
|
+
flush(st);
|
|
2533
|
+
}).then(() => {
|
|
1746
2534
|
armSettleDrop(st);
|
|
1747
2535
|
}).catch((e) => logError("settle flush error", e?.message || e));
|
|
1748
2536
|
};
|
|
@@ -1773,12 +2561,13 @@ function createEngine(deps) {
|
|
|
1773
2561
|
if (!messageID)
|
|
1774
2562
|
return;
|
|
1775
2563
|
if (t === "message.removed") {
|
|
1776
|
-
|
|
2564
|
+
dropMergedMessage(st, messageID, sid);
|
|
2565
|
+
enqueue(st, { key: `message/${messageID}`, item: messageTombstone(sid, messageID) });
|
|
1777
2566
|
} else {
|
|
1778
2567
|
const partID = p.partID;
|
|
1779
2568
|
if (!partID)
|
|
1780
2569
|
return;
|
|
1781
|
-
|
|
2570
|
+
dropMergedPart(st, sid, messageID, partID);
|
|
1782
2571
|
}
|
|
1783
2572
|
} else if (t === "message.updated") {
|
|
1784
2573
|
const main = childToParent.get(sid) ?? sid;
|
|
@@ -1819,13 +2608,13 @@ function createEngine(deps) {
|
|
|
1819
2608
|
}
|
|
1820
2609
|
if (!st || !st.activated || st.closed)
|
|
1821
2610
|
return;
|
|
1822
|
-
|
|
2611
|
+
ingestMessageInfo(st, sid, p.info);
|
|
1823
2612
|
const info = p.info;
|
|
1824
2613
|
if (info?.role === "user" && info?.model) {
|
|
1825
2614
|
const label = formatLabel(st.opts?.modelLabel ?? cfg.modelLabel, info.model);
|
|
1826
2615
|
enqueue(st, itemFor("model", [{ id: info.model.modelID, providerID: info.model.providerID, name: label }], sid));
|
|
1827
2616
|
}
|
|
1828
|
-
if (info?.
|
|
2617
|
+
if (info?.finish === "error") {
|
|
1829
2618
|
st.badSignal = true;
|
|
1830
2619
|
}
|
|
1831
2620
|
} else if (t === "message.part.updated") {
|
|
@@ -1835,12 +2624,12 @@ function createEngine(deps) {
|
|
|
1835
2624
|
const part = p.part;
|
|
1836
2625
|
if (part?.state?.status === "error")
|
|
1837
2626
|
st.badSignal = true;
|
|
1838
|
-
|
|
2627
|
+
ingestPart(st, sid, part);
|
|
1839
2628
|
} else if (t === "session.diff") {
|
|
1840
2629
|
const st = stFor(sid);
|
|
1841
2630
|
if (!st || !st.activated || st.closed)
|
|
1842
2631
|
return;
|
|
1843
|
-
enqueue(st, itemFor("session_diff", p.diff, sid));
|
|
2632
|
+
enqueue(st, itemFor("session_diff", normalizeV1Diff(p.diff), sid));
|
|
1844
2633
|
} else if (t === "session.deleted") {
|
|
1845
2634
|
const isChild = childToParent.has(sid);
|
|
1846
2635
|
if (isChild) {
|
|
@@ -1884,6 +2673,13 @@ function createEngine(deps) {
|
|
|
1884
2673
|
await flushViaHttp(st, r).catch(() => {});
|
|
1885
2674
|
}
|
|
1886
2675
|
}
|
|
2676
|
+
for (const r of st.remotes.values()) {
|
|
2677
|
+
if (r.spoolSaveTimer) {
|
|
2678
|
+
clearTimeout(r.spoolSaveTimer);
|
|
2679
|
+
r.spoolSaveTimer = null;
|
|
2680
|
+
spoolSave(st, r);
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
1887
2683
|
if (st.metaTimer)
|
|
1888
2684
|
clearTimeout(st.metaTimer);
|
|
1889
2685
|
st.metaTimer = null;
|
|
@@ -1904,116 +2700,6 @@ function createEngine(deps) {
|
|
|
1904
2700
|
// src/v2-server.ts
|
|
1905
2701
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
1906
2702
|
import { join as join3 } from "node:path";
|
|
1907
|
-
function toolContentText(content) {
|
|
1908
|
-
if (typeof content === "string")
|
|
1909
|
-
return content;
|
|
1910
|
-
if (Array.isArray(content)) {
|
|
1911
|
-
return content.map((item) => {
|
|
1912
|
-
if (typeof item === "string")
|
|
1913
|
-
return item;
|
|
1914
|
-
if (item && typeof item === "object") {
|
|
1915
|
-
const record = item;
|
|
1916
|
-
if (typeof record.text === "string")
|
|
1917
|
-
return record.text;
|
|
1918
|
-
}
|
|
1919
|
-
return "";
|
|
1920
|
-
}).join("");
|
|
1921
|
-
}
|
|
1922
|
-
return "";
|
|
1923
|
-
}
|
|
1924
|
-
function convertToolState(state) {
|
|
1925
|
-
if (!state || typeof state !== "object")
|
|
1926
|
-
return { status: "pending" };
|
|
1927
|
-
const record = state;
|
|
1928
|
-
const status = String(record.status ?? "pending");
|
|
1929
|
-
const mapped = {};
|
|
1930
|
-
if (status === "streaming" || status === "running")
|
|
1931
|
-
mapped.status = "running";
|
|
1932
|
-
else if (status === "completed")
|
|
1933
|
-
mapped.status = "completed";
|
|
1934
|
-
else if (status === "failed" || status === "error")
|
|
1935
|
-
mapped.status = "error";
|
|
1936
|
-
else
|
|
1937
|
-
mapped.status = "pending";
|
|
1938
|
-
if (record.input !== undefined)
|
|
1939
|
-
mapped.input = record.input;
|
|
1940
|
-
if (record.output !== undefined)
|
|
1941
|
-
mapped.output = toolContentText(record.output);
|
|
1942
|
-
if (record.error !== undefined)
|
|
1943
|
-
mapped.error = record.error;
|
|
1944
|
-
if (record.time !== undefined)
|
|
1945
|
-
mapped.time = record.time;
|
|
1946
|
-
return mapped;
|
|
1947
|
-
}
|
|
1948
|
-
function convertV2Message(message, sessionID) {
|
|
1949
|
-
if (!message || typeof message.id !== "string")
|
|
1950
|
-
return;
|
|
1951
|
-
const base = {
|
|
1952
|
-
id: message.id,
|
|
1953
|
-
sessionID
|
|
1954
|
-
};
|
|
1955
|
-
if (message.type === "user") {
|
|
1956
|
-
return {
|
|
1957
|
-
info: {
|
|
1958
|
-
...base,
|
|
1959
|
-
role: "user",
|
|
1960
|
-
time: { created: message.time?.created ?? 0 },
|
|
1961
|
-
text: message.text ?? ""
|
|
1962
|
-
},
|
|
1963
|
-
parts: []
|
|
1964
|
-
};
|
|
1965
|
-
}
|
|
1966
|
-
if (message.type === "assistant") {
|
|
1967
|
-
const parts = [];
|
|
1968
|
-
const content = Array.isArray(message.content) ? message.content : [];
|
|
1969
|
-
content.forEach((block, index) => {
|
|
1970
|
-
const record = block;
|
|
1971
|
-
const kind = String(record.type ?? "");
|
|
1972
|
-
if (kind === "text") {
|
|
1973
|
-
parts.push({
|
|
1974
|
-
id: `${message.id}_t${index}`,
|
|
1975
|
-
messageID: message.id,
|
|
1976
|
-
sessionID,
|
|
1977
|
-
type: "text",
|
|
1978
|
-
text: String(record.text ?? ""),
|
|
1979
|
-
synthetic: false,
|
|
1980
|
-
time: { start: message.time?.created ?? 0, end: message.time?.completed ?? 0 }
|
|
1981
|
-
});
|
|
1982
|
-
} else if (kind === "reasoning") {
|
|
1983
|
-
parts.push({
|
|
1984
|
-
id: `${message.id}_r${index}`,
|
|
1985
|
-
messageID: message.id,
|
|
1986
|
-
sessionID,
|
|
1987
|
-
type: "reasoning",
|
|
1988
|
-
text: String(record.text ?? ""),
|
|
1989
|
-
synthetic: false,
|
|
1990
|
-
time: { start: message.time?.created ?? 0, end: message.time?.completed ?? 0 }
|
|
1991
|
-
});
|
|
1992
|
-
} else if (kind === "tool") {
|
|
1993
|
-
parts.push({
|
|
1994
|
-
id: String(record.id ?? `${message.id}_tool${index}`),
|
|
1995
|
-
messageID: message.id,
|
|
1996
|
-
sessionID,
|
|
1997
|
-
type: "tool",
|
|
1998
|
-
tool: String(record.name ?? "unknown"),
|
|
1999
|
-
state: convertToolState(record.state),
|
|
2000
|
-
synthetic: false
|
|
2001
|
-
});
|
|
2002
|
-
}
|
|
2003
|
-
});
|
|
2004
|
-
return {
|
|
2005
|
-
info: {
|
|
2006
|
-
...base,
|
|
2007
|
-
role: "assistant",
|
|
2008
|
-
agent: message.agent,
|
|
2009
|
-
model: message.model ? { providerID: message.model.providerID, modelID: message.model.modelID ?? message.model.id } : undefined,
|
|
2010
|
-
time: { created: message.time?.created ?? 0, completed: message.time?.completed ?? 0 }
|
|
2011
|
-
},
|
|
2012
|
-
parts
|
|
2013
|
-
};
|
|
2014
|
-
}
|
|
2015
|
-
return;
|
|
2016
|
-
}
|
|
2017
2703
|
function adaptSessionClient(session, message, directory) {
|
|
2018
2704
|
if (!session)
|
|
2019
2705
|
return;
|
|
@@ -2044,9 +2730,9 @@ function adaptSessionClient(session, message, directory) {
|
|
|
2044
2730
|
if (!Array.isArray(data))
|
|
2045
2731
|
break;
|
|
2046
2732
|
for (const raw of data) {
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2733
|
+
if (raw && typeof raw === "object" && typeof raw.id === "string") {
|
|
2734
|
+
out.push({ info: raw, parts: [] });
|
|
2735
|
+
}
|
|
2050
2736
|
}
|
|
2051
2737
|
const next = res?.cursor?.next ?? undefined;
|
|
2052
2738
|
if (!next || data.length === 0)
|
|
@@ -2146,6 +2832,7 @@ function createV2ServerSetup() {
|
|
|
2146
2832
|
return () => {};
|
|
2147
2833
|
}
|
|
2148
2834
|
const pendingRefresh = new Map;
|
|
2835
|
+
const queuedInbox = new Map;
|
|
2149
2836
|
const refresh = (sessionID) => {
|
|
2150
2837
|
let timer = pendingRefresh.get(sessionID);
|
|
2151
2838
|
if (timer)
|
|
@@ -2160,7 +2847,10 @@ function createV2ServerSetup() {
|
|
|
2160
2847
|
});
|
|
2161
2848
|
const messages = await client.session.messages?.({ path: { id: sessionID } });
|
|
2162
2849
|
const list = messages?.data ?? [];
|
|
2850
|
+
const held = queuedInbox.get(sessionID);
|
|
2163
2851
|
for (const message of list) {
|
|
2852
|
+
if (held?.has(message.info.id))
|
|
2853
|
+
continue;
|
|
2164
2854
|
await engine.hooks.event({
|
|
2165
2855
|
event: { type: "message.updated", properties: { sessionID, info: message.info } }
|
|
2166
2856
|
});
|
|
@@ -2193,6 +2883,33 @@ function createV2ServerSetup() {
|
|
|
2193
2883
|
await engine.hooks.event({ event: { type, properties: { sessionID: sid } } });
|
|
2194
2884
|
continue;
|
|
2195
2885
|
}
|
|
2886
|
+
if (type.startsWith("session.inbox.")) {
|
|
2887
|
+
const data = event.data ?? {};
|
|
2888
|
+
const inboxID = data.inboxID;
|
|
2889
|
+
if (inboxID) {
|
|
2890
|
+
let set = queuedInbox.get(sid);
|
|
2891
|
+
if (!set) {
|
|
2892
|
+
set = new Set;
|
|
2893
|
+
queuedInbox.set(sid, set);
|
|
2894
|
+
}
|
|
2895
|
+
if (type === "session.inbox.enqueued" && data.item?.delivery === "queue") {
|
|
2896
|
+
set.add(inboxID);
|
|
2897
|
+
} else if (type === "session.inbox.delivered") {
|
|
2898
|
+
set.delete(inboxID);
|
|
2899
|
+
refresh(sid);
|
|
2900
|
+
} else if (type === "session.inbox.cancelled") {
|
|
2901
|
+
set.add(inboxID);
|
|
2902
|
+
} else if (type === "session.inbox.delivery.changed") {
|
|
2903
|
+
if (data.item?.delivery === "queue")
|
|
2904
|
+
set.add(inboxID);
|
|
2905
|
+
else {
|
|
2906
|
+
set.delete(inboxID);
|
|
2907
|
+
refresh(sid);
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
continue;
|
|
2912
|
+
}
|
|
2196
2913
|
if (type === "message.removed" || type === "message.part.removed") {
|
|
2197
2914
|
const data = event.data ?? {};
|
|
2198
2915
|
await engine.hooks.event({
|